數據結構與算法2:線性表的順序存儲與鏈式存儲

線性表

定義

  • 線性表(List):零個或多個數據元素的有限序列
  • 元素之間存在順序。若存在多個元素,則第一個元素無前驅,最後一個元素無後繼,其他元素有且只有一個前驅和後繼。
  • 記法:若將線性表記爲(a1,a2,...,ai1,ai,ai+1,...,an)(a_1,a_2,...,a_{i-1},a_i,a_{i+1},...,a_n),則稱ai1a_{i-1}aia_i的直接前驅元素,ai+1a_{i+1}aia_i的直接後繼元素。當i=1,2,...,n1i=1,2,...,n-1時,aia_i有且僅有一個直接後繼,當i=2,3,...,ni=2,3,...,n時,aia_i有且僅有一個直接前驅。
    在這裏插入圖片描述
  • 線性表元素的個數n(n0)n(n\ge 0)定義爲線性表的長度,當n=0n=0時,稱爲空表。

線性表的抽象數據類型(Abstract Data Type)

  • 線性表的基本操作
ADT List:
   List(self)           #創建一個新表
   is_empty(self)       #判斷self是否是一個空表
   len(self)            #返回表長度
   prepend(self,elem)   #在表頭插入元素
   append(self,elem)    #在表尾加入元素
   insert(self,elem,i)  #在表的位置i處插入元素
   pop(self)            #刪除並彈出第一個元素
   poplast(self)        #刪除並彈出最後一個元素
   del(self,i)          #刪除並彈出第i個元素
   get(self,i)          #獲取第i個元素
   count(self,elem)     #統計元素出現的次數
   index(self,elem)     #查找元素在表中第一次出現的位置
   forall(self,op)      #對錶元素的遍歷操作,op操作
  • 實現兩個線性表集合A和B的並集操作
def union(a, b):
    ```將所有在線性表b中但不在a中的數據元素插入到a中```
    for i in range(len(b)):
        x = b.get(i)
        if a.count(x) == 0:
            a.append(x)
    return a

線性表的順序存儲結構

  • 定義:用一段地址連續的存儲單元依次存儲線性表的數據元素。
  • 順序存儲方式
    • 使用一維數組來實現順序存儲結構
    • 使用C語言進行順序存儲的結構代碼:
    #define MAXSIZE 20           /*存儲空間初始分配量*/
    typedef int ElemType;        /*ElemType類型根據實際情況而定,這裏假設爲int*/
    typedef struct
    {
        ElemType data[MAXSIZE];  /*數組存儲數據元素,最大值爲MAXSIZE*/
        int length;              /*線性表當前長度*/
    }SqList;
    
    • 順序存儲結構需要的三個屬性:
      • 1.存儲空間的起始位置:數組data的存儲位置就是存儲空間的存儲位置
      • 2.線性表的最大存儲容量:數組長度Maxsize
      • 3.線性表的當前長度:length
  • 數組長度與線性表長度
    • 數組長度:存放線性表的存儲空間的長度,存儲分配後這個量一般是不變的。
    • 線性表長度:線性表中數據元素的個數,隨着線性表插入和刪除操作而變化
    • 在任意時刻,線性表的長度應該小於等於數組的長度
  • 線性表的地址計算方法
    • 由於數組下標從0開始,第i個元素存儲在數組的下標爲i-1的位置
    • 存儲器中的每個存儲單元都有自己的編號,這個編號稱爲地址。
    • 地址計算方式:假設每個數據元素佔用c個存儲空間
      • Loc(ai+1)=Loc(ai)+cLoc(a_{i+1}) = Loc(a_i) + c
      • Loc(ai)=Loc(a1)+(i1)×cLoc(a_i) = Loc(a_1) + (i-1) \times c
        在這裏插入圖片描述
  • 順序存儲結構的操作
    由於Python內部的tuplelist採用的就是順序存儲結構,不同點在於tuple是固定結構,list是可變動結構,具有線性表ADT描述的全部操作,這裏只列舉列表list的對象方法:
方法 說明
list.append(x) 在列表末尾添加元素,相當於a[len(a):] = [x]
list.extend(iterable) 使用可迭代對象的所有元素來擴展列表,相當於a[len(a):] = x
list.insert(i, x) 在指定位置插入元素x,在列表頭部插入x是a.insert(0, x),在列表尾部插入x是a.insert(len(a), x),相當於a.append(x)
list.remove(x) 移除列表中第一個值爲x的元素.如果沒有這樣的元素,則拋出ValueError異常.
list.pop([i]) 刪除列表中給定位置的元素並返回它.默認刪除並返回列表中的最後一個元素.
list.clear() 刪除列表中所有的元素,相當於del a[:]
list.index(x[, start[, end]]) 返回列表中第一個值爲x的元素從零開始的索引.如果沒有這樣的元素,則拋出ValueError異常.start和end可用於限制搜索範圍.
list.count(x) 返回元素x在列表中出現的次數.
list.sort(key=None, reverse=False) 對列表中的元素進行排序
list.reverse() 反轉列表中的元素
list.copy() 返回列表的一個淺拷貝,相當於a[:]
  • 不同操作的時間複雜度:
操作 時間複雜度
append(x) O(1)
prepend(x) O(1)
insert(i,x) O(n)
pop() O(1)
poplast() O(1)
remove(i) O(n)
get(i) O(1)
count(x) O(n)
index(x) O(n)
  • 優點:
    • 無須爲表示表中元素之間的邏輯關係而增加額外的存儲空間
    • 可以快速的存取表中任一位置的元素
  • 缺點:
    • 插入和刪除操作需要移動大量元素
    • 當線性表長度變化較大時,難以確定存儲空間的容量
    • 容易造成存儲空間的“碎片”

線性表的鏈式存儲結構

  • 定義:用一組任意的存儲單元存儲線性表的數據元素。
    • aia_i來說,除了存儲本身的信息之外,還需要存儲其直接後繼的存儲位置。我們將存儲數據元素信息的域稱爲數據域,將存儲直接後繼位置的域稱爲指針域。這兩部分信息構成了數據元素aia_i的存儲影像,稱爲結點(Node)。
    • n個結點構成一個鏈表,即爲線性表的鏈式存儲結構。因爲此鏈表的每個結點只包含一個指針域,所以叫做單鏈表。單鏈表通過每個結點的指針域將線性表的數據元素按照其邏輯次序鏈接在一起。
    • 我們將鏈表中第一個結點的存儲位置叫做頭指針,後面的每一個結點都是前一個結點的後繼指針指向的位置。而最後一個子結點指針爲空。
    • 有時,爲了方便對鏈表進行操作,會在單鏈表的第一個結點前附設一個結點,稱爲頭結點。頭結點的數據域可以不存儲任何信息,也可以存儲如線性表的長度等附加信息。頭結點的指針域存儲指向第一個結點的指針。
  • 頭指針與頭結點的區別
    • 頭指針是鏈表的必要元素,是鏈表指向第一個結點的指針,具有標識作用,常用於表示鏈表的名字。
    • 頭結點是爲了操作的統一和方便而設立的、放在第一元素的結點之前。頭結點的指針域存儲指向第一個結點的指針。有了頭結點,對在第一元素之前插入結點和刪除第一結點,其操作與其他結點的操作就統一了。

單鏈表(single linked list)

  • 單鏈表結構:
    • 不帶頭結點的單鏈表:
      在這裏插入圖片描述
    • 帶有頭結點的單鏈表:
      在這裏插入圖片描述
  • 單鏈表的實現:見LinkedList.py中的LinkedList類.
  • 單鏈表的改造
    • 傳統的單鏈表訪問尾結點的時間複雜度爲O(n),爲了同時便於訪問頭結點和尾結點,可以添加一個指向尾結點的尾指針rear。實現見LinkedList.py中的_LinkedList類.

靜態鏈表 (static linked list)

  • 定義:對於早期的高級編程語言,沒有指針的存在,人們使用數組代替指針來描述單鏈表。數組的每個下標對應一個data和cur。數據域data用來存放數據元素,遊標cur相當於單鏈表中的next指針,存放該元素的後繼在數組中的下標。我們把用數組描述的鏈表叫做靜態鏈表
  • 存儲結構:
    在這裏插入圖片描述
  • 實現:見LinkedList.py中的StaticLinkedList類.
  • 優缺點:

循環鏈表(circular linked list)

  • 定義:將單鏈表中終端結點的指針由空指針改爲指向頭結點,使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱爲單循環鏈表,簡稱循環鏈表(circular linked list)
  • 循環鏈表使得可以從任意一個結點出發,訪問到鏈表的全部結點。
  • 帶頭結點的頭指針非空循環鏈表結構:
    在這裏插入圖片描述
  • 循環鏈表和單鏈表的差異:
    • 兩者對於循環的判斷條件不同。單鏈表若p->next爲空,則循環結束;循環鏈表若p->next不等於頭結點,則循環未結束。
  • 循環鏈表的改造:
    • 改造循環鏈表,不用頭指針,而是用指向終端結點的尾指針rear來表示循環鏈表,此時用rear->next->next查找開始結點,用rear查找終端結點,時間複雜度都爲O(1)
    • 改造得到的帶頭結點的尾指針非空循環鏈表結構:
      在這裏插入圖片描述
    • 實現:見03LinkedList.py中的CirLinkedList類.
  • 合併兩個循環鏈表
    • 對循環鏈表加入尾指針後,很方便對多個循環鏈表進行合併,合併兩個循環鏈表的操作示意圖如下:
      在這裏插入圖片描述
    • 假設兩個循環鏈表的尾指針爲rearA和rearB,則合併操作如下:
      1. p = rearA -> next;
      2. rearA -> next = rearB -> next -> next;
      3. rearB -> next = p.
    • Python實現:
    def concated(a, b):
        p = a._rear.next
        a._rear.next = b._rear.next.next
        b._rear.next = p
    

雙向鏈表(double linked list)

  • 定義:在單鏈表的每個結點中,再設置一個指向其前驅結點的指針域prior

  • 結構:

    • 帶頭結點的雙向循環空鏈表:
      在這裏插入圖片描述
    • 非空的循環的帶頭結點的雙向鏈表
      [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-i8FR90x7-1570589044787)(images/3-10.png)]
  • 與單鏈表的比較

    • 求鏈表長度,查找元素,獲取元素位置等方法與單鏈表相同
    • 插入和刪除操作需要更改兩個指針變量
  • 插入操作示意圖:
    在這裏插入圖片描述

    1. s -> prior = p
    2. s -> next = p -> next
    3. p -> next -> prior = s
    4. p -> next = s
  • 刪除操作示意圖:
    在這裏插入圖片描述

    1. p -> prior -> next = p -> next
    2. p -> next -> prior = p -> prior
  • 實現:見LinkedList.py中的DoubleLinkedList類.

鏈式存儲結構與順序存儲結構的比較

  • 存儲分配方式
    • 順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素
    • 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素
  • 時間性能
    • 查找
      • 順序存儲結構O(1)
      • 單鏈表O(n)
    • 插入和刪除
      • 順序存儲結構需要平均移動表長一半的元素,時間爲O(n)
      • 單鏈表得到某結點的指針後,插入和刪除時間僅爲O(1)
  • 空間性能
    • 順序存儲結構需要預分配存儲空間,分大了浪費,分小了容易發生上溢
    • 單鏈表不需要分配存儲空間,元素個數不受限制
  • 結論:
    • 對於需要頻繁查找、很少進行插入和刪除操作的線性表來說,宜採用順序存儲結構;若需要頻繁插入和刪除,宜採用單鏈表結構。
    • 當線性表的元素個數變化較大或者不知道有多大時,宜採用單鏈表結構;若事先知道線性表的大致長度,宜採用順序存儲結構。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章