算法導論--數據結構篇第十章上篇:棧/隊列和鏈表

10.1 棧和隊列
 
棧和隊列都是動態集合, 棧(stack)是後進先出, 隊列(queue)是先進先出;
 
棧就至關於壘盤子, 盤子能夠放到櫥櫃中,每次想要往外拿盤子的時候只能從最上面開始拿; 即後進先出
以下圖,能夠用一個數組 S[1..n]來實現一個最多可容納n個元素的棧. 該數組有一個屬性S.top, 指向最新插入的元素, 棧中包含的元素爲S[1..S.top], 其中S[1]是棧底元素, S[S.top]是棧頂元素. 當S.top = 0時, 棧中不包含任何元素, 棧爲空. 若是試圖對一個空棧執行彈出操做, 則棧下溢(underflow), 若是S.top超過了n, 則棧上溢(overflow);
棧的主要操做有Stack_Empty(查詢是否爲空) / Push(入棧) / Pop(出棧)
 

 

 

僞代碼:
 
查詢是否爲空
Stack_Empty(S)
if S.top == 0
    return True
else return False

 

 入棧
 
Push(S,x)
if S.top != n
    S.top += 1
    S[S.top] = x
else error "overflow"

 出棧 出棧是不用指定元素的, 由於棧只能Pop出最頂端的元素前端

Pop(S)
if Stack_Emypty(S)
    error "underflow"
else S.top -= 1
    return S[S.top+1]

 

 

隊列數組

隊列就像排隊等待同樣, 按照順序出入; 能夠用queue[n]來實現存儲n個元素的隊列, 該隊列有一個屬性queue.head 指向隊頭元素, queue.tail 指向下一個元素將要插入的位置. 隊列中的元素存放在位置 queue.head, queue.head+1, ... , queue.tail-1. 要判斷隊列是否爲空是個重點, 由於當 head == tail 時,多是空也可能爲滿. 所以咱們能夠提早定義一個tag,來幫助咱們判斷.
 
隊列的主要操做有Queue_Empty(判斷隊列是否爲空) / Enqueue(入隊) / Dequeue(出隊)

 

 

 

 

僞代碼
 
判斷隊列是否爲空
 
Queue_Empty(Q)
if head == tail && tag
    return False
else if head == tail && !tag
    return True

入隊數據結構

n 爲 Q的長度
Enqueue(Q,x)
if head == tail && tag
    return "overflow"
else {
    Q[tail++] = x
    tail %= n
    tag = 1

}

出棧spa

n 爲 Q的長度
Dequeue(Q)
if head == tail && !tag
    return "underflow"
else {
    head = (head+1) % n
    tag = 0 
}

 

 

10.2鏈表
 
鏈表其實和數組很像, 可是與數組不一樣的是, 鏈表的順序是由各個對象裏的指針決定的. 鏈表中每個對象都由一個關鍵字key和兩個指針: next 和 prev. 具體說明, 咱們假設 x 爲鏈表中的一個元素, 那麼x.next 就指向下一個元素, x.prev 指向前一個元素. 若是 x.next指向爲空, 說明x爲鏈表的尾(tail); 同理, 若是x.prev指向爲空, 那麼x爲鏈表的頭(head).

 

鏈表的形式有單連接/雙連接/已排序/未排序/循環和非循環的. 其中, 單連接的鏈表省略每一個元素中的prev指針; 循環鏈表表頭元素的prev指針指向表尾元素, 而表尾元素的next指針則指向表頭元素.
 
基本操做有List_Search(搜索)/List_Insert(插入)/List_Delete(刪除)
 
搜索, 這裏就採用簡單的線性搜索方法, 對於List_Search(L, k) , 查找鏈表L中, 第一個關鍵字爲k的元素, 並返回指向該元素的指針.
List_Search(L, k)
x = L.head  // 從頭開始查找
while x != Null && x.key != k
    x = x.next  // 沒有找到且不是最後一個元素就一直往下找
return x
若是鏈表中有n個對象, 時間複雜度最壞狀況下爲O(n), 須要遍歷全部元素
 
插入(只考慮插入到前端的狀況) 時間複雜度O(1)
List_Insert(L, x)
x.next = L.head
if L.head != Null
    L.head.prev = x
L.head = x
x.prev = Null
 
刪除
將一個元素x 從鏈表中移除, 須要給定一個指向x的指針, 而後經過修改一些指針, 將x"刪除出"該鏈表. 若是要刪除具備給定關鍵字值的元素, 則必須先調用List_Search 找到該元素. 如下僞代碼, 省略了查找的過程.
List_Delete(L, x)
if x.prev != Null // 若是x的前驅不爲空, 那麼就讓它指向x的後驅元素
    x.prev.next = x.next
else
    L.head = x.next
if x.next != null // 若是x的後驅不爲空, 那麼就讓它指向x的前驅元素
    x.next.prev = x.prev

 

如上能夠看到在進行插入和刪除操做的時候咱們都要考慮表頭和表尾的邊界條件, 代碼看起來就會有些繁瑣. 下面就引入哨兵(sentinel)的概念, 來簡化邊界條件的處理.
咱們在鏈表L中設置一個對象L.nil, 它表明爲Null, 可是也具備和其餘對象相同的屬性. 對於鏈表代碼中出現的每一處對Null的引用, 都代之以對哨兵L.nil的引用. 這樣就能夠將常規的雙向鏈表轉變爲一個有哨兵的雙向循環鏈表, 哨兵位於表頭和表尾之間, L.nil.next 指向表頭, L.nil.prev指向表尾, 相似的, 表尾的next 屬性和表頭的prev屬性都指向L.nil.
 
 

 

 

如下爲加了哨兵改動過的代碼:3d

搜索指針

List_Search'(L, k)
x = L.nil.next  // 從頭開始查找
while x != L.nil && x.key != k
    x = x.next  // 沒有找到且不是最後一個元素就一直往下找
return x

 

 插入
List_Insert'(L, x)
x.next = L.nil.next
L.nil.next.prev = x
L.nil.next = x
x.prev = L.nil

 刪除code

List_Delete'(L, x)
x.prev.next = x.next
x.next.prev = x.prev
 
注意:
哨兵基本上不能下降數據結構相關操做的漸近時間界也就是時間複雜度, 它能夠下降的是常數因子. 循環語句中使用哨兵的好處在於能夠使代碼簡潔, 而非提升速度.
然而, 咱們應當慎用哨兵. 由於若是在不少很短的鏈表中使用哨兵, 哨兵所佔用的額外存儲空間會形成嚴重的存儲浪費.
相關文章
相關標籤/搜索