1. 程式人生 > >資料結構筆記_連結串列

資料結構筆記_連結串列

一.連結串列

線性資料結構:

  • 動態陣列
  • 佇列

以上三種資料結構的底層均是依託於靜態陣列,解決自動變容問題靠的是reSize操作。

連結串列的認識:

  • 是線性資料結構
  • 是一種真正的動態資料結構,同時也是最簡單的動態資料結構
  • 更深入的理解引用(指標)
  • 更深入的理解遞迴
  • 輔助組成其他資料結構

連結串列(Linked List):

  • 資料儲存在 “節點”(Node)中

class Node{
    E e;
    Node next;
}
  • 優點:是真正的動態儲存,不需要處理固定容量的問題 ,需要儲存多少個數據就生成多少個節點
  • 缺點:喪失了陣列的隨機訪問的能力

陣列和連結串列的對比

  • 陣列最好用於索引有語義的情況,陣列最大的優點就是支援快速chaxun查詢
  • 連結串列不適合用於索引有語義的情況,連結串列最大的優點就是動態的儲存

連結串列的新增操作:

  • 連結串列不同於陣列,在連結串列中,最容易新增的位置是頭部,直接將新節點的指標指向頭結點,然後更新頭結點為新新增的節點

  • 在連結串列中間新增元素的過程:先找到新增位置的前一個節點prev,然後經過兩步操作(不可調換位置)完成,(頭部節點是沒有前一個節點的,需特殊處理)

為連結串列設立虛擬頭結點:

  • 由於連結串列中的頭結點相比於其他節點有些特殊,即頭結點沒有字首節點,故在實際操作中頭結點經常需要特殊考慮。為了解決頭結點的特殊性,連結串列實現中一個很常用的技巧就是為連結串列新增一個虛擬頭結點(dummyHead)。
  • 虛擬頭結點不儲存資料,但需要指向一個真正的頭結點(真正的頭結點在初始化時可以為null,即 dummyHead = new Node(null, null) ),換句話說就是即使對於一個空連結串列來說,它也是存在一個節點的,這個節點就是 dummyHead = new Node(null, null) 。
  • 虛擬頭結點僅僅是為了程式編寫的方便。

連結串列中的刪除操作:

連結串列的查詢和遍歷操作: 

          連結串列的遍歷需要通過新建引用(指標)的方式實現,指標的後移操作為  cur=cur.next

//for迴圈遍歷整個連結串列
        for(Node cur = dummyHead.next; cur != null; cur=cur.next){
           //do something
        }

//while迴圈遍歷整個連結串列
        Node cur = dummyHead.next;
        while(cur != null){

            //do something

            //迴圈退出條件
            cur = cur.next;
        }

//查詢連結串列中索引為index(索引從0開始)的元素
       Node cur = dummyHead.next;
       for(int i=0; i<index; i++){
            cur = cur.next;
       }

連結串列的時間複雜度分析:

 由於連結串列的增刪改查操作都需要遍歷查詢過程,所以它們的時間複雜度均為O(n), 但是,對錶頭的增刪查操作,其時間複雜度為O(1)。

     基於以上特點,可以將連結串列頭作為棧頂來實現一個棧。

連結串列的簡單改進:帶有尾指標的連結串列,可以用於實現佇列

  • 在頭結點增加和刪除元素都容易,時間複雜度均為O(1)
  • 在尾節點新增節點容易,時間複雜度為O(1),但刪除節點不容易(因為指標是單向的,刪除尾節點需要從頭節點遍歷找到尾節點的前一個元素)
  • 基於以上特點,帶有尾指標的連結串列,可以用於實現佇列

連結串列的進一步改進:雙鏈表

在頭結點和尾節點的增加和刪除元素時,時間複雜度均為O(1)

連結串列的進一步改進:迴圈雙向連結串列

將雙鏈表新增虛擬頭結點,並將指向null的尾節點改為指向虛擬頭結點

陣列連結串列:

       在陣列中不僅僅只儲存一個值,還要儲存一個指向下一個空間的索引(陣列下標),在明確知道要處理的元素有多少個時,可以考慮使用這種連結串列。

二.連結串列與遞迴

遞迴:

  • 本質上,是將原來的問題轉化為更小的同一問題 

//遞迴求和  計算arr[l, n)這個區間內所有數字的和
    private static int sum(int[] arr, int rangeL){
        if(rangeL == arr.length){
            return 0;
        }

       return  arr[rangeL]+sum(arr, rangeL+1);
    }

編寫遞迴程式的基本原則:

      所有的遞迴程式都可以分為兩部分:

  1. 求解最基本問題,這個最基本問題是不能自動解決的,需要通過編寫程式解決,但這個最基本問題往往十分簡單;
  2. 把原問題轉化為更小的問題,這部分是遞迴的核心,即需要根據最小問題的解構造出原問題的解 

編寫遞迴程式的經驗 (巨集觀解讀) :

  • 注意遞迴函式的“巨集觀”語義;(如:計算一個數組內一段區間的和)
  • 遞迴函式就是一個函式,用來完成完成一個功能,函式A裡呼叫函式B與函式A裡呼叫函式A本質上是一樣的,可以把遞迴函式想成一個普通的子函式,這個子函式會完成一定的功能(巨集觀語義)。

連結串列天然的遞迴性質:

        我們可以把連結串列看作是一個一個連線的節點,換個思路,也可以看作是一個節點後面掛載了一個更短的連結串列,直到最後null也是一個連結串列。對於連結串列中的許多操作都可以利用遞迴的思路來完成。

遞迴函式的“微觀解讀”:

  • 遞迴函式的呼叫本質就是函式的呼叫,只不過呼叫的函式是自己而已。
  • 遞迴呼叫是有代價的:函式呼叫+系統棧空間

1>. 陣列求和的微觀過程:

2>. 連結串列刪除元素的微觀過程: