-
數據元素可以存在內存未被佔用的任意位置。
-
除了存數據元素信息外,還要存儲它的直接後繼元素的存儲地址。
-
數據元素的存儲映像,稱爲結點(Node):數據域、指針域(指針or鏈)
-
結點由存放數據元素的數據域和存放後繼元素結點地址的指針域組成
-
假設P爲指向線性表第i個元素的指針
-
結點的數據域:
-
結點的指針域:P.next
-
-
n個結點鏈接成一個鏈表,即線性表的鏈式存儲結構,因爲每個結點中只包含一個指針域,所以叫做單鏈表
-
鏈表中第一個結點的存儲位置叫做頭指針,整個鏈表的存取必須從頭指針開始進行。
-
鏈表的最後一個結點指針爲空(NULL或^)。
-
頭結點:
- 頭結點的數據域可以不存儲任何信息,也可以存儲線性表的長度等附加信息。
- 頭結點的指針域存儲指向第一個結點的指針。
-
頭指針與頭結點的異同:
- 頭指針:
- 頭指針是指鏈表指向第一個結點的指針
- 若鏈表有頭結點,則是指向頭結點的指針
- 頭指針具有標識作用,所有常用頭指針冠以鏈表的名字
- 無論鏈表是否爲空,頭指針均不爲空。
- 頭指針是鏈表的必要元素。
- 頭結點:
- 頭結點是爲了操作的統一和方便而設立的,放在第一元素的結點之前,其數據域一般無意義(也可存放鏈表的長度)
- 有了頭結點,對在第一元素結點前插入結點和刪除第一結點,其操作與其它結點的操作就統一了。
- 頭結點不一定是鏈表的必須要素。
- 頭指針:
3.2.1單鏈表的讀取
獲得鏈表第i個數據的算法思路:
-
核心思想:工作指針後移
-
聲明一個指針指向鏈表第一個結點,初始化從1開始。
-
當時,就遍歷鏈表,讓的指針向後移動,不斷指向下一結點,累加1.
-
若到鏈表末尾p爲空,這說明第i個結點不存在。
-
否則查找成功,返回結點p的數據。
GetElem(L,i):
p = L.next # 讓p指向鏈表L的第一個結點
j = 1
while p and j<i: # p不爲空且計數器j還沒有等於i時,循環繼續
p = p.next # 讓p指向下一個結點
j++
if !p or j>i:
return ERROR # 第i個結點不存在
e = p.data # 取第i個結點的數據
這個算法的時間複雜度取決於i的位置,當i=1時,不需遍歷,第一個就取出數據了,而當i=n時則遍歷n-1次纔可以。因此最壞時間複雜度爲O(n)
3.2.2單鏈表的插入與刪除
- 單鏈表的插入
算法思路:
-
聲明一指針p指向鏈表頭結點,初始化j從1開始;
-
當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1;
-
若到鏈表末尾p爲空,則說明第i個結點不存在;
-
否則查找成功,在系統中生成一個空結點s;
-
將數據元素e賦值給s.data;
-
單鏈表的插入標準語句
s.next = p.next p.next = s
-
返回成功
ListInsert(L,i,e): p = L.next # 讓p指向鏈表L的第一個結點 j = 1 while p and j < i: # 尋找第i-1個結點 p = p.next j++ if !p or j > i: return ERROR # 第i個結點不存在 s.data = e # 生成新結點 s.next = p.next # 將p的後繼結點賦值給s的後繼 p.next = s # 將s賦值給p的後繼 return OK
- 單鏈表的刪除
將它的前繼結點的指針繞過,指向它的後繼結點即可。(把p的後繼結點改成p的後繼的後繼結點)
算法思路:
-
聲明一指針p指向鏈表頭結點,初始化j從1開始;
-
當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1;
-
若到鏈表末尾p爲空,則說明第i個結點不存在;
-
否則查找成功,將欲刪除的結點p.next賦值給q;
-
單鏈表的刪除標準語句
q = p.next p.next = q.next
-
將q結點中的數據賦值給e,作爲返回;
-
釋放q結點
-
返回成功
ListDelete(L,i): p = L.next # 讓p指向鏈表L的第一個結點 j = 1 while p and j < i: # 尋找第i-1個結點 p = p.next j++ if !p or j > i: return ERROR # 第i個結點不存在 q = p.next p.next = q.next # 將q的後繼賦值給p的後繼 e = q.data # 將q結點中的數據給e free(e) # 讓系統回收此結點,釋放內存 return OK
單鏈表的插入與刪除算法由兩部分組成:
- 遍歷查找第i個結點
- 插入和刪除結點
時間複雜度都是O(n)
如果我們不知道第i個結點的指針位置,單鏈表在插入和刪除操作上與順序存儲結構沒有太大的優勢。
但是,如果我們希望從第i個位置,插入10個結點,對於順序存儲結構,每一次插入都需要移動n-i個結點,每次都是O(n)。
而對於單鏈表,我們只需要在第一次時,找到第i個位置的指針,此時爲O(n),接下來只是簡單的通過賦值移動指針而已,時間複雜度爲O(1)
對於插入和刪除數據越頻繁的操作,單鏈表的效率優勢就越是明顯
3.2.3單鏈表的整表創建
- 頭插法(始終讓新結點在第一的位置)
算法思路:
-
聲明一指針p和計數器變量i;
-
初始化一空鏈表L;
-
讓L的頭結點的指針指向NULL,即建立一個帶頭結點的單鏈表
-
循環:
- 生成一新結點賦值給p
- 隨機生成一數字賦值給p的數據域p.data
- 將p插入到頭結點與前一新結點之間
CreateListHead(L,n): i = 0 L.next = NULL while i<n: i ++ p.data = random(100) # 隨機生成100以內的數字 p.next = L.next L.next = p # 插入到表頭
- 尾插法(把新結點放在最後)
CreateListTail(L,n):
i = 0
r = L # r爲指向尾部的結點
while i<n:
i++
p.data = random(100) # 隨機生成100以內的數字
r.next = p # 將表尾終端結點的指針指向新結點
r = p # 將當前的新結點定義爲表尾終端結點
r.next = NULL # 表示當前鏈表結束(循環結束後,讓這個結點的指針域置空,以便以後遍歷時可以確認其是尾部)
L是指整個單鏈表,r是指向尾結點的變量。
3.2.3單鏈表的整表刪除
算法思路:
- 聲明一結點p和q
- 將第一個結點賦值給p
- 循環:
- 將下一結點賦值給q
- 釋放p
- 將q賦值給p
考慮一個問題:q變量有沒有存在的必要?
p結點除了有數據域還有指針域,在釋放p時,是對它整個結點進行刪除和內存釋放的工作。變量q的作用,它使得下一個結點是誰得到了記錄,以便於等當前結點釋放後,把下一結點拿回來補充。
ClearList(L):
p = L.next # p指向第一個結點
while p: # 沒到表尾
q = p.next
free(p)
p = q
L.next = NULL # 頭結點指針域爲空
return OK
3.3 單鏈表結構與順序存儲結構的優缺點
-
存儲分配方式:
- 順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素
- 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素
-
時間性能:
- 查找:
- 順序存儲結構O(1)
- 單鏈表O(n)
- 插入和刪除:
- 順序存儲結構需要平均移動表長一半的元素,時間爲O(n)
- 單鏈表在得出某位置的指針後,插入和刪除時間僅爲O(1)
- 查找:
-
空間性能:
- 順序存儲結構需要預分配存儲空間,分大了,浪費,分小了容易發生上溢
- 單鏈表不需要分配存儲空間,只要有就可以分配,元素個數也不受限制
-
經驗性結論:
- 若線性表需要頻繁查找,很少進行插入和刪除操作,採用順序存儲結構
- 若需要頻繁插入和刪除操作,採用單鏈表結構
- 當線性表中的元素個數變化較大或者根本不知道有多大時,最好用單鏈表結構。這樣不用考慮存儲空間的大小問題
- 如果事先知道線性表大致長度,用順序存儲結構效率會高一些。