算法导论--数据结构篇第十章上篇:栈/队列和链表

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
 
注意:
哨兵基本上不能下降数据结构相关操做的渐近时间界也就是时间复杂度, 它能够下降的是常数因子. 循环语句中使用哨兵的好处在于能够使代码简洁, 而非提升速度.
然而, 咱们应当慎用哨兵. 由于若是在不少很短的链表中使用哨兵, 哨兵所占用的额外存储空间会形成严重的存储浪费.
相关文章
相关标签/搜索