數據結構和算法5——鏈表

  前面已經介紹了數組,咱們看到數組做爲數據存儲結構是有必定缺陷的,在無序數組中,搜索是低效的;而在有序數組中,插入效率很低;無論哪種數組,刪除效率都很低。並且建立一個數組後,數組大小不可變。
  本部分介紹鏈表,它是一種新的數據結構,能夠解決上面的一些問題,這種數據結構就是鏈表,鏈表也是使用很是普遍的數據結構。這裏將介紹單鏈表、雙端鏈表、有序鏈表、雙向鏈表和有迭代器的鏈表。算法

1 鏈結點(Link)                                         

  在鏈表中,每一個數據項都被包含在鏈結點中,一個鏈結點是某個類的對象,這個類能夠叫作Link。由於一個鏈表中有許多相似的鏈結點,因此有必要用一個不一樣於鏈表的類來表達鏈結點。每一個Link對象中都包含一個對下一個鏈結點引用的字段,一般叫next,可是鏈表自己的對象中有一個字段指向對第一個鏈結點的引用。編程

class Link{
    public int iData;
    public double dData;
    public Link next;
}

  這種類定義有時叫作自引用式,由於它包含了一個和本身類型相同的字段,本例中叫作next。
  鏈結點中僅包含兩個數據項:一個int類型的數據,一個double類型的數據。在一個真正的應用程序中,可能包含更多的數據項。一般用一個包含這些數據的類的對象來代替這些數據項.
  上面類對象裏面有一個Link next類型的變量,這個變量是Link類型的,指向下一個節點。小程序

2 鏈表訪問與數組的區別                                  

  鏈表不一樣於數組的訪問,在一個數組中,每一項佔用一個特定的位置,這個位置能夠用一個下標來直接訪問,好比要訪問第7個元素,只要下標6確定能訪問,不須要知道前6個元素是什麼。可是在鏈表中不是這樣的。在鏈表中,尋找一個特定元素的惟一方法就是沿着這個元素的鏈一直向下尋找。它很像人們之間的關係。可能你問Harry,Bob在哪,Harry不知道,可是他想Jane知道,因此又去問Jane,Jane看到Bob和Sally一塊兒離開,因此你又去問Sally,如此循環,總有線索,最終找到Bob。數組

3 鏈表及相關操做的實現                                  

3.1 單鏈表                             

先定義鏈表的節點Node類數據結構

class Node{
    public int iData;
    public double dData;
    public Node next;
    //有參數構造器
    public Node(int iData, double dData){
        this.iData = iData;
        this.dData = dData;
    };
    //該方法顯示鏈結點的數據值,例如{22, 33.1}
    public void displayNode(){
        System.out.print("{" + iData + "," + dData + "}  ");
    }
};

 

  上面的構造函數並無初始化next字段,由於當它建立時候自動賦成null的值,不須要指向其餘任何結點。
LinkList類
  LinkList類只包含一個數據項:即對鏈表中第一個鏈表結點的引用,叫作first,它是惟一的鏈表須要維護的永久信息,用以定位全部其餘的鏈表結點。從first出發,沿着鏈表結點的next字段,就能夠找到其餘結點。編輯器

class LinkList{
private Node first =  null;
    //鏈表是否爲空
    public boolean isEmpty(){
        return (first == null);
    }
}

上面就實現了一個鏈表。下面實現鏈表的相關方法。函數

插入一個表頭結點this

  該方法是insertFirst,該方法是在表頭插入一個新鏈結點。這是很容易插入的一個結點,first當前指向的是第一個鏈結點,爲了插入表頭鏈結點,只須要使新建立的鏈結點的next字段等於原來的first值,而後改變first值,使它指向新建立的鏈結點。編碼

 

  在insertFirst方法中,首先建立一個新連接結點,把數據做爲參數傳入,而後改變鏈結點的引用。spa

View Code

  上面咱們看到插入一個新結點是在表頭插入的,爲何不在表末尾插入呢?在尾巴上面添加不是很好嗎?這個想法很好,但末尾結點並很差索引到,因此會很麻煩。

刪除結點

  該方法是deleteFirst,是插入的逆操做。它經過把first從新指向第二個鏈結點,斷開了和第一個鏈結點的鏈接,經過查看第一個鏈結點的next字段能夠找到第二個鏈結點。

//刪除結點
public Node deleteFirst(){
    Node temp = first;
    first = first.next;
    return temp;
}

顯示鏈表

  displayList()方法能夠從first開始,沿着引用鏈從一個鏈表結點到下一個鏈表結點。

public void displayList(){
    System.out.println("List(first --> last):");
    Node current = first;
    while(current != null){
        current.displayNode();
        current = current.next;
    }
    System.out.println("");
}

  鏈表的尾端是最後一個鏈結點,它的next字段爲null值,而不是其餘的鏈結點,由於建立結點時候,這個字段就是null,而該鏈表結點老是停留在鏈表尾端,後來再也沒有變過,當執行到鏈表的尾端的時候,while循環使用這個條件來終止本身。
  把上面幾個小程序合在一塊兒便可:

View Code

輸出結果:
List(first --> last):
{77,7.7} {7,7.7} {2,7.7} {7,2.7} {2,2.7} 
List(first --> last):
{7,7.7} {2,7.7} {7,2.7} {2,2.7} 
能夠看出鏈表刪除時候也是後入先出的。

查找指定結點

  查找指定結點就是查找值在某個結點上的結點。

public Node find(int key){
        Node current = first;
        while(current.iData != key){
            //若是已達結尾
            if(current.next == null){
                return null;
            }else{
                current = current.next;
            }
        }
        return current;
}

刪除指定結點:

public Node delete(int key){
    Node current = first;
    Node previous = first;
    while(current.iData != key){
        if(current.next == null){
            return null;
        }else{
            previous = current;
            current = current.next;
        }
        if(current == first){
            first = first.next;
        }else{
            previous.next = current.next;
        }
    }
    return current;
}

3.2 雙端鏈表                        

  雙端鏈表和傳統的鏈表很是類似,可是它有一個新增的特性:即對最後一個鏈表結點的引用,就像對第一個鏈表結點的引用同樣。

  對最後一個鏈表結點的引用容許像在表頭同樣,在表尾直接插入一個鏈表結點,固然,仍然能夠在普通的單鏈表的表尾插入一個鏈表結點,方法是遍歷整個鏈表直到到達表尾,可是這種方法效率很低。
  像訪問表頭同樣訪問表尾的特性,使雙端鏈表更適合於一些普通鏈表不方便操做的場合,隊列的實現就是這樣的狀況。

View Code

3.3 鏈表的效率

  在表頭插入和刪除速度很快,僅須要改變一兩個引用值,因此花費O(1)時間。
  平均起來,查找、刪除和在指定鏈結點後面插入都須要搜索鏈表中的一半鏈結點,須要O(N)次比較。在數組中執行這些操做也須要O(N)次比較,但鏈表仍然要快一些,由於當插入和刪除鏈表結點時,鏈表不須要移動任何東西,增長的效率是很顯著的,特別是當複製時間遠遠大於比較時間的時候。

4 抽象數據類型                                               

  抽象數據類型更注重數據結構能作什麼,而不是怎麼作。是對一些傳統的數據結構的封裝。下面咱們用鏈表來實現棧及隊列兩種數據結構。

4.1 用鏈表實現棧                   

  前面用數組來實現棧的時候,棧的push和pop操做實際是經過數組操做來完成的,例以下面的代碼
  arr[++top] = data;和data = arr[top--];
  下面用鏈表來實現一個棧。

View Code

輸出結果:
Stack (top --> bottom): 35 20 
Stack (top --> bottom): 33 21 35 20 
Stack (top --> bottom): 35 20

  注意整個程序的組織:LinkStackApp類中的main()方法只和LinkStack類有關,LinkStack類只和LinkList類有關,main方法和LinkList類是不進行通訊的。

4.2 用雙端鏈表實現的隊列    

  隊列是先進先出的,也就是插隻日後插,刪只刪前面的。

View Code

運行結果:
Queue (front-->rear): 20 40 
Queue (front-->rear): 20 40 60 80 
Queue (front-->rear): 60 80

5 抽象:數據結構、數據類型和算法                 

  數據結構是數據類型在幾何結構上的一種描述,棧是一種數據結構,這種數據結構是寄託在一種數據類型上的,也就是一個類上的,數據結構上有一些操做,也就是一些算法。int類型也是有數據結構的,它是存儲一個變量的,操做有不少種,好比加減乘除。

5.1 抽象數據類型                  

  抽象數據類型就是不考慮數據類型的描述和實現,而是對外提供一些操做。面向對象編程中,一個抽象數據類型是一個類,且不考慮它的實現。它是對類中數據的描述和數據上執行的一系列方法以及如何使用這些操做的說明,每一個方法如何執行任務對其餘類來講是不可知的。好比對於棧這種數據類型來講,其餘類只知道它有push和pop等方法的存在,以及如何用它們工做,用戶不須要知道它如何運做,或者數據是否存儲在數組裏、鏈表裏或者其餘數據結構中。

ADT設計

  ADT的概念在軟件設計中是頗有用的,若是須要存儲數據,那麼就從考慮須要在數據上實現的操做開始,須要存取最後一個插入的數據項嗎?仍是第一個?是特定值的項?仍是在特定位置的項?回答這些問題會引出ADT的定義,只有在完整定義了ADT後,才應該考慮細節問題,例如如何表示數據,如何編碼使方法能夠存取數據等等。
  一旦設計好ADT,必須仔細選擇內部的數據結構,以使規定的操做的效率儘量高。例如,若是須要隨機存取元素N,使用鏈表就不夠好,由於對鏈表來講,隨機訪問不是一個高效操做,選擇數組會獲得更好的效果。

5.2有序鏈表                          

  有序鏈表中,數據是有序存儲的。有序鏈表的刪除經常只限於刪除在鏈表頭部的最小火最大的鏈表結點。不過有時候也用find方法和delete方法在整個鏈表中搜索某一特定點。
  通常,在大多數須要使用有序數組的場合也可使用有序鏈表,有序鏈表優於有序數組的地方是插入的速度(由於元素不須要移動),另外鏈表能夠擴展到所有有效的使用內存,而數組只能侷限於一個固定的大小中。可是有序鏈表實現起來比有序數組更困難一些。
下面實現這一點:

View Code

上面實現了一個有序鏈表,而且在表頭、表中、表尾都插入了數據。

5.3 有序鏈表的效率               

  在有序鏈表插入和刪除最多須要O(N)次比較,(平均N/2),由於必須沿着鏈表上一步一步走才能找到正確的位置,然而能夠在O(1)的時間內找到或刪除最小值,由於它老是在表頭。

5.4 利用鏈表進行插入排序      

  有序鏈表能夠用於一種高效的排序機制,假設有一個無序數組,若是從這個數組中取出數據,而後一個個地插入有序鏈表,它們自動地按順序排列,把它們從有序表中刪除,從新放入數組,那麼數組就會排好序了。
  這種排序方式整體上比在數組中用經常使用的插入排序效率更高。由於複製次數更少,數組的插入排序算法時間級是O(N^2),在有序鏈表中每插入一個新的鏈結點,平均要與一半已存在的數據進行比較,若是插入N個新數據,就進行了N^2/4 次比較,每個鏈結點只進行兩次複製:一次從數組到鏈表,一次從鏈表到數組,在數組中進行插入排序須要N^2 次移動,相比之下,2*N次移動更好。

View Code

運行結果:
Unsorted Array: 
81 94 67 13 35 0 14 42 58 89 
Sorted Array: 
0 13 14 35 42 58 67 81 89 94 
上面生成的結果是隨機的。

5.4 雙向鏈表                          

  下面討論另外一種鏈表:雙向鏈表(注意和雙端鏈表是不一樣的)。傳統鏈表的一個潛在問題是沿着鏈表的反向遍歷是困難的,用這樣一個語句current = current.next能夠很方便地到達下一個鏈結點,然而沒有對應的方法回到前一個鏈結點。好比說有下面的情形:對於一個編輯器來講,移動光標能夠從左往右移動,可是若是隻能從左往右移動效率就很低,由於有時候咱們也想從右往左邊移動。
  雙向鏈表容許前向遍歷也容許後向遍歷。對於每個結點有兩個指向其餘鏈結點的引用,一個指向前一個結點,另外一個指向後一個結點。
下面是示意圖:

在雙向鏈表中,Link類定義的開頭是這樣聲明的:

class Node{
    public long dData;
    public Node next;
    public Node previous;
    ......
}

  雙向鏈表每次插入或刪除一個結點時候,須要處理四個結點的引用,而不是兩個:兩個鏈接前一個結點,兩個鏈接後一個鏈結點。
遍歷
從前日後遍歷,和以前的鏈表同樣,從後往前遍歷以下:

Node current = last;
while(current != null){
    current = current.previous;
}

插入
  除非這個鏈表是空的,不然insertFirst()方法把原先first指向的鏈表結點的previous字段指向新鏈表結點,把新鏈表結點指向原來first指向的結點,最後再把first指向新鏈表結點。

若是鏈表是空的,last字段必須改變,而不是first.previous字段改變,下面是代碼:

if(isEmpty()){
    last = newNode;
}else{
    first.previous = newNode;
}
newNode.next = first;
first = newNode;

  inserLast是在鏈表末尾插入,和頭沒有多少區別。
  中間插入有點複雜,須要創建4個鏈接。若是結點在中間,這個和以前說的find方法沒有什麼區別,若是在末尾,next字段必須設爲null,last值必須指向新結點。

if(current == last){
    newNode.next = null;
    last = newNode;
}else{
    newNode.next = current.next;
    current.next.previous = newNode;
}
newNode.previous = current;
current.next = newNode;

刪除
  刪除一樣須要處理四個結點:

current.previous.next = current.next;
current.next.previous = current.previous;

下面是雙向鏈表的實現:

View Code

輸出結果:
List (first-->last:)37 30 20 
List(last-->first:)20 30 37 
List (first-->last:)37 30 20 23 24 25 
List (first-->last:)37 30 20 23 33 24 25

6 迭代器                                                     

  前面咱們查找數值所在的結點時候,都是從頭開始查找的,或者逆着查找,直到找到匹配值。可是這些方法沒有提供給用戶任何遍歷上的控制手段,就是說找到這些並進行處理。好比你要遍歷一個鏈表,並在某些特定的鏈結點上執行一些操做,好比提升拿最低工資的人員工工資,而不影響其餘員工。

迭代器類
  迭代器包含對數據結構中數據項的引用,並用來遍歷這些數據結構的對象。下面是迭代器的定義:

class ListIterator(){
  private Node current;
  ....
}

  注意迭代器是必須和對應的鏈表相關聯的,不能單獨存在。迭代器老是指向鏈表中的一些鏈表結點

相關文章
相關標籤/搜索