1. 程式人生 > >線性表—單鏈表

線性表—單鏈表

描述 尾插 amp 復制 收回 move head ear 鏈表

1.鏈式存儲結構實現

單鏈表和雙鏈表(這邊講單鏈表)。

2.基礎概念

a.結點:結點由數據域和地址域(鏈)兩部分組成。而結點整體在效果上可以看作是該結點的地址(指針)。
這個地址域一般是後繼元素的地址(即下一個結點的總體)。所以最後一個元素的地址域為^,其表示空,即沒有後續元素。
b.單鏈表:每個結點只有一個地址域的線性鏈表稱為單鏈表。
c.雙鏈表:每個結點有兩個地址域的線性表鏈稱為雙鏈表,兩個地址域分別指向
前驅元素和後繼元素。

3.單鏈表的實現


線性表接口LList:

package com.clarck.datastructure.linked;

/**
* 線性表接口LList,描述線性表抽象數據類型,泛型參數T表示數據元素的數據類型
*
* @author clarck
*
*/
public interface LList<T> {
/**
* 判斷線性表是否空
* @return
*/
boolean isEmpty();

/**
* 返回線性表長度
* @return
*/
int length();

/**
* 返回第i(i≥0)個元素
* @param i
* @return
*/
T get(int i);

/**
* 設置第i個元素值為x
* @param i
* @param x
*/
void set(int i, T x);

/**
* 插入x作為第i個元素
* @param i
* @param x
*/
void insert(int i, T x);

/**
* 在線性表最後插入x元素
* @param x
*/
void append(T x);

/**
* 刪除第i個元素並返回被刪除對象
* @param i
* @return
*/
T remove(int i);

/**
* 刪除線性表所有元素
*/
void removeAll();

/**
* 查找,返回首次出現的關鍵字為key元素
* @param key
* @return
*/
T search(T key);
}


單鏈表結點類:

package com.clarck.datastructure.linked;

/**
* 單鏈表結點類,T指定結點的元素類型
*
* @author clarck
*
* @param <T>
*/
public class Node<T> {
/**
* 數據域,保存數據元素
*/
public T data;

/**
* 地址域,引用後繼結點
*/
public Node<T> next;

/**
* 構造結點,data指定數據元素,next指定後繼結點
*
* @param data
* @param next
*/
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}

/**
* 構造節點
*/
public Node() {
this(null, null);
}

/**
* 返回結點元素值對應的字符串
*/
@Override
public String toString() {
return this.data.toString();
}

/**
* 比較兩個結點值是否相等,覆蓋Object類的equals(obj)方法
*/
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object obj) {
return obj == this || obj instanceof Node && this.data.equals(((Node<T>)obj).data);
}

}

 

線性表的鏈式表示和實現:

package com.clarck.datastructure.linked;

/**
* 線性表的鏈式表示和實現 帶頭結點的單鏈表類,實現線性表接口
*
* @author clarck
*
* @param <T>
*/
public class SinglyLinkedList<T> implements LList<T> {
/**
* 頭指針,指向單鏈表的頭結點
*/
public Node<T> head;

/**
* 默認構造方法,構造空單鏈表
*/
public SinglyLinkedList() {
// 創建頭結點,data和next值均為null
this.head = new Node<T>();
}

/**
* 由指定數組中的多個對象構造單鏈表。采用尾插入構造單鏈表
* 若element==null,Java將拋出空對象異常;若element.length==0,構造空鏈表
*
* @param element
*/
public SinglyLinkedList(T[] element) {
// 創建空單鏈表,只有頭結點
this();
// rear指向單鏈表最後一個結點
Node<T> rear = this.head;
for (int i = 0; i < element.length; i++) {
rear.next = new Node<T>(element[i], null);
rear = rear.next;
}
}

/**
* 判斷單鏈表是否空,O(1)
*/
@Override
public boolean isEmpty() {
return this.head.next == null;
}

/**
* 返回單鏈表長度,O(n), 基於單鏈表遍歷算法
*/
@Override
public int length() {
int i = 0;
// p從單鏈表第一個結點開始
Node<T> p = this.head.next;
// 若單鏈表未結束
while (p != null) {
i++;
// p到達後繼結點
p = p.next;
}
return i;
}

/**
* 返回第i(≥0)個元素,若i<0或大於表長則返回null,O(n)
*/
@Override
public T get(int i) {
if (i >= 0) {
Node<T> p = this.head.next;
for (int j = 0; p != null && j < i; j++) {
p = p.next;
}

// p指向第i個結點
if (p != null) {
return p.data;
}
}
return null;
}

/**
* 設置第i(≥0)個元素值為x。若i<0或大於表長則拋出序號越界異常;若x==null,不操作。O(n)
*/
@Override
public void set(int i, T x) {
if (x == null)
return;

Node<T> p = this.head.next;
for (int j = 0; p != null && j < i; j++) {
p = p.next;
}

if (i >= 0 && p != null) {
p.data = x;
} else {
throw new IndexOutOfBoundsException(i + "");
}
}

/**
* 插入第i(≥0)個元素值為x。若x==null,不插入。 若i<0,插入x作為第0個元素;若i大於表長,插入x作為最後一個元素。O(n)
*/
@Override
public void insert(int i, T x) {
// 不能插入空對象
if (x == null) {
return;
}

// p指向頭結點
Node<T> p = this.head;
// 尋找插入位置
for (int j = 0; p.next != null && j < i; j++) {
// 循環停止時,p指向第i-1結點或最後一個結點
p = p.next;
}
// 插入x作為p結點的後繼結點,包括頭插入(i<=0)、中間/尾插入(i>0)
p.next = new Node<T>(x, p.next);
}

/**
* 在單鏈表最後添加x對象,O(n)
*/
@Override
public void append(T x) {
insert(Integer.MAX_VALUE, x);
}

/**
* 刪除第i(≥0)個元素,返回被刪除對象。若i<0或i大於表長,不刪除,返回null。O(n)
*/
@Override
public T remove(int i) {
if (i >= 0) {
Node<T> p = this.head;
for (int j = 0; p.next != null && j < i; j++) {
p = p.next;
}

if (p != null) {
// 獲得原對象
T old = p.next.data;
// 刪除p的後繼結點
p.next = p.next.next;
return old;
}
}
return null;
}

/**
* 刪除單鏈表所有元素 Java將自動收回各結點所占用的內存空間
*/
@Override
public void removeAll() {
this.head.next = null;
}

/**
* 順序查找關鍵字為key元素,返回首次出現的元素,若查找不成功返回null
* key可以只包含關鍵字數據項,由T類的equals()方法提供比較對象相等的依據
*/
@Override
public T search(T key) {
if (key == null)
return null;
for (Node<T> p = this.head.next; p != null; p = p.next)
if (p.data.equals(key))
return p.data;
return null;
}

/**
* 返回單鏈表所有元素的描述字符串,形式為“(,)”,覆蓋Object類的toString()方法,O(n)
*/
@Override
public String toString() {
String str = "(";
for (Node<T> p = this.head.next; p != null; p = p.next) {
str += p.data.toString();
if (p.next != null)
str += ","; // 不是最後一個結點時後加分隔符
}
return str + ")"; // 空表返回()
}

/**
* 比較兩條單鏈表是否相等
*/
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;

if (obj instanceof SinglyLinkedList) {
SinglyLinkedList<T> list = (SinglyLinkedList<T>) obj;
return equals(this.head.next, list.head.next);
}
return false;
}

/**
* 比較兩條單鏈表是否相等,遞歸方法
*
* @param p
* @param q
* @return
*/
private boolean equals(Node<T> p, Node<T> q) {
return p == null && q == null || p != null && q != null
&& p.data.equals(q.data) && equals(p.next, q.next);
}
}

 

測試類:

package com.clarck.datastructure.linked;

/**
* 單鏈表的測試
*
* @author clarck
*
*/
public class SinglyLinkedList_test {
public static void main(String args[]) {
SinglyLinkedList<String> lista = new SinglyLinkedList<String>();
for (int i = 0; i <= 5; i++)
lista.insert(i, new String((char) (‘A‘ + i) + ""));
System.out.println("lista: " + lista.toString() + ",length()="
+ lista.length());
lista.set(3, new String((char) (lista.get(0).charAt(0) + 32) + ""));
lista.remove(0);
lista.remove(3);
System.out.println("lista: " + lista.toString());
}
}


測試結果:

lista: (A,B,C,D,E,F),length()=6
lista: (B,C,a,F)

提醒:實現源碼下載鏈接http://www.cnblogs.com/tanlon/p/4027046.html

4.實現類具體說明

a.如何將結點連接起來形成單鏈表

1.建立結點(對象)
2.抓住箭頭的作用,在建立兌現的時候的變量內部設立或者利用next變量來設立後繼元素,從而連成一條鏈。


b.對於頭指針的理解

1.next在等號左邊,其為訪問一個結點。我們首先要有箭頭指向,而其他結點的指針就是next特性,指針就是幫我們?到對象,從而使用對象,所以其看起來和用起來都可以看作是對象,但如果說它就是對象,那就是錯的。指針就是
幫我們訪問結點的。
2.next在等號左邊,其為指向對象,我們只需要把握箭頭就好。因為那都不是訪問對象,而且改變單鏈表的結構。對於雙鏈表也是,改變雙鏈表的結構的話把握好箭頭指向就好。
(兩者要分開,不要混淆)


c.怎麽抓住箭頭?

1.從左邊指向右邊。比如,p.next=q,那麽箭頭由p指向q。
2.如果一個出發點有多個終點,取最後一個終點為主。比如說,p.next=q; p.next=b;都是從p出發,但終點不同,取其終點為b,而指向q的箭頭斷掉。
註意:這也說明了,有時需要註意操作順序的問題。比如說:單鏈表中p後面插入一個q結點,必須先q.next=p.next,之後才能p.next=q;因為如p.next=q先的話,那麽之前的箭頭斷掉,就訪問不了沒插入之前p的後繼結點了。


d.對於單鏈表的常見操作說明

1).單鏈表的遍歷操作
思路:只需要知道一個結點就可以獲得其他結點.
(遍歷單鏈表是指從第一個結點開始,我們訪問是通過指針去訪問,頭指針不能改變,為head,之後改變指針(跟地址域相同)一個一個去訪問後面的元素,直到最後一個結點。)
註意:head在本類中直接用。

代碼實現:
Node<T> p=head; //p從head指向的結點開始
while(p!=null) //當單鏈表未結束時
{
System.out.print(p.data.toString()+""); //執行訪問p結點的相關操作
p=p.next; //p到達後繼結點
}


2).單鏈表的插入操作
插入思路:就是自己確定新的結點進行連接。
一共有四種情況的插入
情況一和二:空表插入/頭插入
思路:若單鏈表為空,插入一個結點,head指向被插入 的結點,這是空表插入;
若單鏈表非空,則在head結點之間插入q結點,插入後q結點成為單鏈表的第一個結點,head指向該結點,這是頭插入。
(註意:這兩種情況都將改變單鏈表的頭指針head)
if(head==null){
head=new Node<T>(x,null); //空表插入
}
else
{
Node<T> q=new Node<T>(x,null); //頭插入
q.next=head;
head=q;
}

註意:上述程序段可合並成一句,即”head=new Node<T>(x,head);",其包含了空表插入和頭插入情況。

情況三和四:中間插入/尾插入
思路:先把x節點和下個結點連在一起,然後再和前一個結點連在一起。
註意:中間插入和尾部插入都不會改變單鏈表的頭指針head
Node<T> q=new Node<T>(x,null);
q.next=p.next; //q的後繼結點應是p的原後繼結點
p.next=q; //q作為p的後繼結點


3).單鏈表的刪除操作
思路:就是自己確定新的結點連接。
根據被刪除的結點的位置的不同,分為兩種情況來了解:

情況一:頭刪除
思路:刪除單鏈表第一個結點,只要是head指向其後繼結點即可。
即:head=head.next;
(註意:如果該鏈表只有一個元素,即刪除該結點後,單鏈表為空,執行完上述語句後,head為null。)

情況二:中間/尾刪除
思路:設p指向單鏈表中除最後一個結點外的某個結點,只需要改變一個結點的next值
if(p.next!=null){
p.next=p.next.next;

4).排序單鏈表
d1.概念:就是各結點按照data域值遞增或遞減順序鏈接。
d2.思路:
d3.代碼實現:
public class SortedSinglyLinked<T extends Comparable<T>> extends SinglyLinkedList<T>
{
//默默構造方法,調用父類默認構造方法
public SortedSinglyLinkedList(){ super();}
//將elements數組中所有對象插入構造排序的單鏈表,直接插入排序

public SortedSinglyLinkedList(T[] element)
{ super(); //創建空單鏈表,調用父類默認構造方法,默認調用
if(element!=null){
for(int i=0;i<element.length;i++){
this.insert(element[i]); //插入一個結點,根據值的大小決定插入位置
}
}
}

提醒:具體實現鏈接為:

http://www.cnblogs.com/tanlon/p/4027219.html

5).循環單鏈表
思路:把最後一個結點(對象)的next數據變為head,就可以完成循環,就變成了一條圓圈的單鏈表。
代碼實例:
public class CirSinglyLinkedList<T>
{
public Node<T> head; //頭指針,指向循環單鏈表的頭結點(簡單點說就是最後一個結點的next值為null,而它的地址就是null,達到循環的效果)

public CirSinglyLinkedList() //默認構造方法,構造空循環單鏈表
{
this.head=new Node<T>(); //創建頭結點

this.head.next=this.head;

}

public boolean isEmpty()
{
return this.head.next==this.head;
} //判斷循環單鏈表是否為空

public String toString() //返回所有元素的描述字符串
{
String str="(";
Node<T> p=this.head.next;
while(p!=this.head) //遍歷單鏈表的循環條件改變了
{
str+=p.data.toString();
if(p!=this.head){
str+=",";
} //不是最後一個結點時後加分隔符

p=p.next;
}
return str+")";
}
.........

提醒:具體實現鏈接為:

http://www.cnblogs.com/tanlon/p/4028264.html

6).比較單鏈表是否相等(對不同鏈表,特殊,上述都是在同一單鏈表進行的操作)
思路:一個一個數據進行比較,直到最後都為null.
方法如下:
public boolean equals(Object obj)
{
if(obj==this)
return true;
if(!(obj instanceof SinglyLinkedList))
reture false;
Node<T> p=this.head.next;
Node<T> q=(SinglyLinkedList<T>)obj.head.next;
while(p!=null&&q!=null&&p.data.equals(q.data))
{
p=p.next;
q=q.next;
}
return p==null&&q==null;
}

其覆蓋了Object類的equals(obj)方法。

7).帶頭結點的單鏈表
a.頭結點概念:就是指在單鏈表的第一個結點之前增加一個特殊的結點,稱為頭結點。
b.頭結點的作用:1.是使所有鏈表(包括空表)的頭指針非空。
2.使對單鏈表的插入,刪除操作不需要區分是否為空表
3.不用管是否在第一個位置進行,從而與其他位置的插入,刪除操作一致。

8).單鏈表操作的效率分析
a.常見方法的時間復雜度
a1.isEmpty(),insertAfter(p,x),removeAfter(p)的時間復雜度是O(l)。
a2. length(),get(i),set(i),insert(i,x),remove(i)的時間復雜度是O(n)。
(註意:後四種方法的時間復雜度取決於i,當i最大值時其時間復雜度為O(n))

b.對於單鏈表操作效率的理解:對比順序表,其提高了運行效率和存儲空間的利用率。
詳細說明:
對單鏈表進行插入和刪除操作,只需要改變少量結點的鏈,不需要移動數據元素。單鏈表中結點的
存儲空間實在插入和刪除過程中動態申請和釋放的,不需要預先給單鏈表分配存儲空間,從而避免了
順序表因存儲空間的不足擴充空間和復制元素的過程,提高了運行效率和存儲空間的利用率。

9).提高單鏈表操作效率的措施
a.在某些需要使用長度的情況下,應該註意避免兩次遍歷單鏈表。比如:在單鏈表最後添加元素的append()方法(其中還用到了length())
b.如果在單鏈表類中增加某些私有成員變量,則可提高某些操作效率。

10).單鏈表的淺拷貝和深拷貝
淺拷貝:它只對單鏈表的頭指針進行賦值,則導致有兩個頭指針,從而形成公用其余結點數據的兩條單鏈表。

構造函數淺拷貝代碼實現:
public SinglyLinkedList(SinglyLinkedList<T> list)
{
this.head=list.head;
}

深拷貝:僅僅復制單鏈表list的所有結點,但沒有復制元素對象。

深度拷貝:不僅僅復制所有的結點,還復制了所有元素的對象。

構造函數深拷貝代碼實現:
public SinglyLinkedList(SinglyLinkedList<T> list)
{
this(); //創建空單鏈表,只有頭結點
Node<T> p=list.head.next; //若list==null,拋出空對象異常
Node<T> rear=this.head;
while(p!=null)
{
rear.next=new Node<T>(p.data,null);
rear=rear.next;
p=p.next;
}
}

11).單鏈表的應用----利用單鏈表解決約瑟夫問題。
思路:

1.先把指針通過while方法轉移到我們開始的元素
2.之後進行遍歷,在遍歷裏面使用while方法,將指針轉移到我們要使其出局但是仍然存在的元素,要做到這個只能將其flag改為0.
註意:只有flag為1,其count才能加1
3.如果flag為0,將指針指向下一個結點。重復直到只有一個元素跳出遍歷。

代碼實現:
public void yuesefu(Node head,int num,int start,int distance){
//head頭結點,num為元素總數,start為開始的元素號(這個數我們得根據頭結點和要求我們開始的元素的距離所共同決定的),distance為距離多少就刪除的數字

Node p=head;
int i=1;
int count=1; //count的存在是為了讓元素移到我們指定的距離的元素

//指向開始元素

while(i<start){
p=p.next;
}

//下面進行遍歷
while(num!=1){
while(count<distance){
p=p.next;
if(p.flag=1){ count++;}
}
p.flag=0;
while(p.flag==0){
p=p.next;
num--}
//讓count恢復原值
count=1;

}

//輸出最後剩下的元素
System.out.println(p);

}

//提醒,所以這邊的結點,還需要加多一個數據域為flag,默認等於1

線性表—單鏈表