淺談鏈表–數據結構的重要根基

  • 2019 年 11 月 13 日
  • 筆記

鏈表是什麼

鏈表、列表,說起來有點相似,作用也有點類似,但可別傻傻分不清楚。我們一般說的列表,是一個連續的序列,用來存儲一組數據。而鏈表,雖然也是有序的存儲結構,但它不限定要「連續」的。

打個比方:列表就好像火車,每一節車廂連在一起,如果你知道車頭在哪裡,大致就也能知道第8節車廂在哪。

鏈表,就像是去往同個目的地的車隊,有很多輛車,大家排着隊行駛。雖是一個整體,但隊伍的順序是可以很容易調整的。

從概念定義上來說,鏈表是一種以線性順序存儲的數據結構,但並不要求按順序存儲。它由節點組成,每個節點除了儲存自身數據外,還包含一個指向下一個節點的引用(或指針)。

這就是一個包含5個節點的鏈表示意圖:

通常,我們會有一個 head 引用指向鏈表的開頭,而鏈表的結尾,下一個節點則指向空值 None。

除了上圖演示的單向鏈表外,還存在雙向鏈表(每個節點還增加一個指向前一個節點的引用)和循環鏈表(最後一個節點的下一個節點會指向第一個節點)。

鏈表有什麼用

老問題又來了:為什麼要有鏈表?

鏈表相較順序存儲列表,最大的好處就是很容易往序列中添加和刪除元素,單看插入和刪除操作,最優可達到O(1)的複雜度。這個從上面舉的火車和車隊的例子就可以想像出來。另外,鏈表的好處還有不需要連續的存儲空間,且不需要預先知道數據集的大小

但鏈表也有它的不足,就是如果你要查找某個節點,或訪問指定序號的節點,效率則比較低。

所以,鏈表更適合需要頻繁增刪元素但很少查找元素,或者無法預知數據規模的場景

舉一個例子:玩過格鬥遊戲的人,都知道有個「搓招」的設定,就是你按下一定順序的按鍵(如 →↓↘→AB),就會觸發角色的特殊招式。當玩家操作角色時,會不停按下各個按鍵,這時如果你想判斷最近的按鍵組合是否符合某一固定招式,就可以用鏈表來記錄最近的按鍵歷史,並且在過程中不斷更新。

那麼,為何我們標題說鏈表是數據結構的重要根基呢?因為從上面的描述我們可以看出,鏈表的節點是非常靈活的,可以組織成不同的結構。例如我們上次提到的棧,就完全可以改為用鏈表實現。其他的一些數據結構,如隊列、樹、圖,一些算法,如 LRU(最近最少使用算法),文件系統等,均會用到鏈表這種數據結構。

最近又火起來的概念:區塊鏈,它也是某種意義上的鏈表。

鏈表的實現

我們用 Python 語言來自己實現一個單向鏈表結構,以加深理解。

功能需求:

創建一個 SingleLinkedList 類,具備以下功能:

  1. SingleLinkedList() – 創建新的單鏈表,不需要參數,返回空鏈表。
  2. addFirst(item) – 將元素添加到鏈表頭,需要參數,無返回值。
  3. remove(item) – 刪除鏈表內元素,需要參數,並修改單鏈表的內容。
  4. isEmpty() – 檢查單鏈表是否為空,不需要參數,返回布爾值。
  5. length() – 返回單鏈表中元素個數,不需要參數,返回整數。

開發思路:

照例先來幾張示意圖,理一下上述幾個功能:

1. 創建節點、單鏈表並 addFirst(item)

2. 繼續 addFirst(item) 添加節點。

3. 多次添加節點後就會出現我們開頭的單鏈表。

4. 刪除儲存數據為4的頭部節點

5. 刪除鏈表中間儲存元素為2的中間節點

在刪除鏈表元素的過程包含兩個步驟:

1. 遍歷鏈表,找到刪除的元素。我們從 head 節點找起,直到 next 節點為 None 的尾部節點

2. 當找到元素後,將其前一個節點的 next 節點指向它原本的 next 節點,也就實現了從列表中刪除元素

代碼實現:

class Node:    def __init__(self, initdata):        self.data = initdata        self.next = None      def __str__(self):        # 方便後續輸出觀察數據          return str(self.data)  class SingleLinkedList:    def __init__(self):        self.head = None # 記錄頭部節點        self.size = 0 # 記錄鏈表長度      def isEmpty(self):        return self.head == None      def addFirst(self, item):        temp = Node(item) # 創建節點        temp.next = self.head # 新節點指向原先的頭節點         self.head = temp # 重新指向頭節點        self.size += 1      def remove(self, item):        currentNode = self.head # 查找節點,從頭部節點開始        found = False # False 表示還沒找到要刪除的節點        previous = None # 記錄當前節點的前一個節點,當刪除節點為頭節點時,previous 等於 None        while currentNode != None and not found: # 尾節點指向 None,所以currentNode != None, 表示鏈表還沒有查找完。            if currentNode.data == item: # 發現數據                found = True            else: # 沒有找到節點,就接着向下一個節點尋找                previous = currentNode # 記錄當前節點的上一個節點                currentNode = currentNode.next # 將當前節點向下一個節點移動        if not found: # 數據不存在鏈表中            raise raise ValueError('SingleLinkedList.remove(item): item not in SingleLinkedList')        if previous == None: # 發現要刪除的節點是頭節點            self.head = currentNode.next        else: # 要刪除的節點是中間節點或尾部節點            previous.next = currentNode.next # 將當前節點的上一個節點指向當前節點的下一個節點        self.size -= 1      def length(self):        return self.size            if __name__ == '__main__':        u = SingleLinkedList()    u.isEmpty()    print(u.head)    u.addFirst(4)    print(u.head)    u.addFirst('dog')    print(u.head)    u.addFirst(True)    print(u.head)    u.addFirst('ok')    print(u.head)    u.remove('dog')    print(u.head)    u.length()

以上便是一個單鏈表的 Python 實現。