前言
栈、队列和链表及有树根。这些都是最最基本的数据结构。
栈和队列
栈实现一种后进先出(LIFO)策略;队列实现一种先进先出(FIFO)策略
栈
STACK-EMPTY(S)
if S.top==0
return TRUE
else
return FALSE
PUSH(S, x)
S.top=S.top+1
S[S.top]=x
POP(S)
if STACK-EMPTY(S)
error "underflow"
else
S.top=S.top-1
return S[S.top+1]
队列
EUQUEUE(Q, x)
Q[Q.tail]=x
if Q.tail==Q.length
Q.tail=1
else
Q.tail=Q.tail+1
DEQUEUE(Q)
x=Q[Q.head]
if Q.head==Q.length
Q.head=1
else
Q.head=Q.head+1
return x
练习
1-1
PUSH(S,4) | 4
PUSH(S,1) | 4 1
PUSH(S,3) | 4 1 3
POP(S) | 4 1
PUSH(S,8) | 4 1 8
POP(S) | 4 1
1-2
- 第一个栈以下标为[0]为栈底
- 第二个栈以下标为[n-1]为栈底
1-3
ENQUEUE(Q,4) | 4
ENQUEUE(Q,1) | 4 1
ENQUEUE(Q,3) | 4 1 3
DEQUEUE(Q) | 1 3
ENQUEUE(Q,8) | 1 3 8
DEQUEUE(Q) | 3 8
1-4
QUEUE-EMPTY(Q)
if Q.head == Q.tail
return true
else return false
QUEUE-FULL(Q)
if Q.head == Q.tail + 1 or (Q.head == 1 and Q.tail == Q.length)
return true
else return false
ENQUEUE(Q, x)
if QUEUE-FULL(Q)
error "overflow"
else
Q[Q.tail] = x
if Q.tail == Q.length
Q.tail = 1
else Q.tail = Q.tail + 1
DEQUEUE(Q)
if QUEUE-EMPTY(Q)
error "underflow"
else
x = Q[Q.head]
if Q.head == Q.length
Q.head = 1
else Q.head = Q.head + 1
return x
1-5
HEAD-ENQUEUE(Q, x)
if QUEUE-FULL(Q)
error "overflow"
else
if Q.head == 1
Q.head = Q.length
else Q.head = Q.head - 1
Q[Q.head] = x
TAIL-ENQUEUE(Q, x)
if QUEUE-FULL(Q)
error "overflow"
else
Q[Q.tail] = x
if Q.tail == Q.length
Q.tail = 1
else Q.tail = Q.tail + 1
HEAD-DEQUEUE(Q)
if QUEUE-EMPTY(Q)
error "underflow"
else
x = Q[Q.head]
if Q.head == Q.length
Q.head = 1
else Q.head = Q.head + 1
return x
TAIL-DEQUEUE(Q)
if QUEUE-EMPTY(Q)
error "underflow"
else
if Q.tail == 1
Q.tail = Q.length
else Q.tail = Q.tail - 1
x = Q[Q.tail]
return x
1-6
- 设第一个栈a,栈底为x,栈顶为y
- 即第二个栈b,栈底为y,栈顶为x
- ENQUEUE a.y++ 即对应的b栈y变化
- DEQUEUE b.x++ 即对应的a栈x变化
1-7
- 设第一个队列a,队列入口为x,队列出口为y
- 即第二个队列b,队列入口为y,队列出口为x
- push a.x-- 即对应的b队列x变化
- pop b.y-- 即对应的a队列y变化
链表
在链表这种数据结构中,各对象按线性顺序排序。链表与数组不同,数组的线性序由数组下标决定,而链表中的顺序是各对象中的指针所决定的。链表可用来简单而灵活表示动态集合。
LIST-SEARCH(L, k)
x=L.head
while x!=null and x.key!=k
x=x.next
return x
LIST-INSERT(L, x)
x.next=L.head
if L.head!=null
L.head.prev=x
L.head=x
x.prev=null
LIST-DELETE(L, x)
if x.prev!=null
x.prev.next=x.next
else
L.head=x.next
if x.next!=null
x.next.prev=x.prev
哨兵
哨兵(sentinel)是一个哑对象,其作用是简化边界条件的处理。LIST-DELETE的代码可以更简单些
LIST-DELETE(L, x)
x.prev.next=x.next
x.next.prev=x.prev
练习
2-1
- 插入操作:可以。
- 删除操作:如果需要删除指定的数据,那么需要查询操作在O(n)时间内找出这个值,然后再删除。如果按顺序删除,则只需O(1)时间,所以最坏情况下是O(n).
2-2
STACK-EMPTY(L)
if L.head == NIL
return true
else return false
PUSH(L, x)
x.next = L.head
L.head = x
POP(L)
if STACK-EMPTY(L)
error "underflow"
else
x = L.head
L.head = L.head.next
return x
2-3
QUEUE-EMPTY(L)
if L.head == NIL
return true
else return false
ENQUEUE(L, x)
if QUEUE-EMPTY(L)
L.head = x
else L.tail.next = x
L.tail = x
x.next = NIL
DEQUEUE(L)
if QUEUE-EMPTY(L)
error "underflow"
else
x = L.head
if L.head == L.tail
L.tail = NIL
L.head = L.head.next
return x
2-4
LIST-SEARCH'(L, k)
x = L.nil.next
L.nil.key = k
while x.key != k
x = x.next
return x
2-5
LIST-INSERT''(L, x)
x.next = L.nil.next
L.nil.next = x
LIST-DELETE''(L, x)
prev = L.nil
while prev.next != x
if prev.next == L.nil
error "element not exist"
prev = prev.next
prev.next = x.next
LIST-SEARCH''(L, k)
x = L.nil.next
while x != L.nil and x.key != k
x = x.next
return x
2-6
双向(环形)链表
LIST-UNION(L[1], L[2])
L[2].nil.next.prev = L[1].nil.prev
L[1].nil.prev.next = L[2].nil.next
L[2].nil.prev.next = L[1].nil
L[1].nil.prev = L[2].nil.prev
2-7
# p[1]-[4] 是临时变量
LIST-REVERSE(L)
p[1] = NIL
p[2] = L.head
while p[2] != NIL
p[3] = p[2].next
p[2].next = p[1]
p[1] = p[2]
p[2] = p[3]
L.head = p[1]
2-8
由题得,根据 s.np 已知尾指示可求出头指针, 已知头指示可求出尾指针
LIST-SEARCH(L, k)
prev = NIL
x = L.head
while x != NIL and x.key != k
next = prev XOR x.np
prev = x
x = next
return x
LIST-INSERT(L, x)
x.np = NIL XOR L.tail
if L.tail != NIL
L.tail.np = (L.tail.np XOR NIL) XOR x // tail.prev XOR x
if L.head == NIL
L.head = x
L.tail = x
LIST-DELETE(L, x)
y = L.head
prev = NIL
while y != NIL
next = prev XOR y.np
if y != x
prev = y
y = next
else
if prev != NIL
prev.np = (prev.np XOR y) XOR next // prev.prev XOR next
else L.head = next
if next != NIL
next.np = prev XOR (y XOR next.np) // prev XOR next.next
else L.tail = prev
LIST-DELETE(L, x)
y = L.head
prev = NIL
while y != NIL
next = prev XOR y.np
if y != x
prev = y
y = next
else
if prev != NIL
prev.np = (prev.np XOR y) XOR next // prev.prev XOR next
else L.head = next
if next != NIL
next.np = prev XOR (y XOR next.np) // prev XOR next.next
else L.tail = prev
LIST-REVERSE(L)
tmp = L.head
L.head = L.tail
L.tail = tmp
指针和对象的实现
如果所用的语言或者环境不支持指针和对象,那我们该怎么用数组来将其转化呢?实质上可以将这个问题的本质转化为数组和链表这两种数据结构的转换,准确来说,是将链表表示的数据用数组表示。
对象的多维数组表示
对于给定的数组下标x,key[x],next[x]和prev[x]就共同表示链表中的一个对象
对象单数组表示
子数组A[j…k],对象的每一个域对应着0到k-j的偏移量,假设key,next,pre占位相同都为1,则key,next和prev对应的偏移量为0,1和2.
分配和释放对象
基于“对象的多维数组表示“,我们把自由对象保存在一个单链表中,称为自由表(free list)。自由表只使用next数组,该数组只存储表中的next指针。自由表的表头保存在全局变量free中。
自由表类似于一个栈:下一个被分配的对象就是最后被释放的那个。
练习
3-1 请画出序列<13,4,8,19,5,11>存储在以多重数组表示的爽链表中的形式。另画出在单数组表示下的形式
3-2 对一组用单数组表示的同构对象,写出其过程ALLOCATE-OBJECT和FREE-OBJECT。
ALLOCATE-OBJECT()
if free == NIL
error "out of space"
else x = free
free = A[x + 1]
return x
FREE-OBJECT(x)
A[x + 1] = free
free = x
3-3 在ALLOCATE-OBJECT和FREE-OBJEC过程的实现中,为什么不需要设置重置对象的prev属性呢?
在自由表中没使用到对象的pre,所以不用担心pre没设置,对正常使用的对象产生影响
3-4 我们往往希望双向链表的所有元素在存储器中保持紧凑,例如,在多数组表示中占用前m个下标位置.(在页式虚拟存储的计算环境下,即为这种情况。)假设除指向链表本身的指针外没有其他指针指向该链表元素,试说明如何实现过程ALLOCATE-OBJECT和FREE-OBJECT,使得该表示保持紧凑。(提示,使用栈的数组实现)。
# 紧凑型,当申请空间时,只经从自由表中移出头节点就行
ALLOCATE-OBJECT()
if STACK-EMPTY(F)
error "out of space"
else x = POP(F)
return x
# 紧凑型,当释放空间时,我们需要将,栈头外面的空间和所以释放对象空间交换位置
# 这样自由表就可以紧凑的扩充,而不影响正在使用的数据
FREE-OBJECT(x)
p = F.top - 1
p.prev.next = x
p.next.prev = x
x.key = p.key
x.prev = p.prev
x.next = p.next
PUSH(F, p)
3-5 设L是一个长度为n的双向链表,存储于长度为m的数组key,prev和next中。假设这些数组由维护双链自由表F的两个过程ALLOCATE-OBJECT和FREE-OBJECT进行管理。又假设m个元素中,恰有n个元素在链表L上,m-n个在自由表上。给定链表L和自由表F,试写出一个过程COMPACTIFY-LIST(L,F),用来移动L的元素使其占用数组中1,2,。。。n的位置,调整自由表F以保持其正确性,并且占用数组中n+1,n+2…,m的位置。要求缩写的过程运行时间应为θ(n),且只使用固定量的额外存储空间
COMPACTIFY-LIST(L, F)
TRANSPOSE(A[L.head], A[1])
if F.head == 1
F.head = L.head
L.head = 1
l = A[L.head].next
i = 2
while l != NIL
TRANSPOSE(A[l], A[i])
if F == i
F = l
l = A[l].next
i = i + 1
TRANSPOSE(a, b)
SWAP(a.prev.next, b.prev.next)
SWAP(a.prev, b.prev)
SWAP(a.next.prev, b.next.prev)
SWAP(a.next, b.next)
有根树的表示
链表的表示方法可以推广至任意的同构的数据结构上。如有根树(有向无环图)。
二叉树
用域 p,left,right 来存入指向二叉树T的 父亲,左孩子,右孩子的指针
分支数无限制有根数
如果将分支数扩展的任意树k,用二叉树的结构管理方式比较难。可以用左孩子右兄弟表示法:
- x.left-child指向结点x最左边的孩子结点;
- x.right-sibling指向x右侧相邻的兄弟结点
如果x没有孩子结点,则x.left-child=null,如果结点x是其父结点的最右孩子,则x.right-sibling=null。
练习
4-1画出下列属性表所示的二叉树,其根节点下标为6
4-2 给定一个n结点的二叉树,写出一个O(n)时间的递归过程,将该树每个结点的关键字输出
PRINT-BINARY-TREE(T)
x = T.root
if x != NIL
PRINT-BINARY-TREE(x.left)
print x.key
PRINT-BINARY-TREE(x.right)
4-3给定一个n结点的二叉树,写出一个O(n)时间的非递归过程,将该树每个结点的关键字输出。可以使用一个栈作为辅助数据结构
PRINT-BINARY-TREE(T, S)
PUSH(S, T.root)
while !STACK-EMPTY(S)
x = S[S.top]
while x != NIL // store all nodes on the path towards the leftmost leaf
PUSH(S, x.left)
x = S[S.top]
POP(S) // S has NIL on its top, so pop it
if !STACK-EMPTY(S) // print this nodes, leap to its in-order successor
x = POP(S)
print x.key
PUSH(S, x.right)
4-4 给定一个n结点的任意有根树,写出一个O(n)时间的过程,将该树每个结点的关键字输出,该树以左孩子右兄弟表示法存储。
PRINT-LCRS-TREE(T)
x = T.root
if x != NIL
print x.key
lc = x.left-child
if lc != NIL
PRINT-LCRS-TREE(lc)
rs = lc.right-sibling
while rs != NIL
PRINT-LCRS-TREE(rs)
rs = rs.right-sibling
4-5给定一个n结点的二叉树,写出一个O(n)时间的非递归过程,将该树每个结点的关键字输出。要求除该树本身的存储空间外只能使用固定量的额外存储空间,且在过程中不得修改该树,即使是暂时的修改也不允许
PRINT-KEY(T)
prev = NIL
x = T.root
while x != NIL
if prev = x.parent
print x.key
prev = x
if x.left
x = x.left
else
if x.right
x = x.right
else
x = x.parent
else if prev == x.left and x.right != NIL
prev = x
x = x.right
else
prev = x
x = x.parent
4-6 任意有根树的左孩子右兄弟表示法中每个结点用到三个指针:left-child,right-sibling和parent。对于任何结点,都可以在常数时间到达其父结点,并在与其孩子数呈线性关系的时间内到达所有孩子结点。说明如何在每个结点中只使用两个指针和一个布尔值的情况下,使结点的父结点或者其所有孩子结点可以在与其孩子数呈线性关系的时间内到达。
既然少了parent指针,多了布尔值,那么布尔值正好只有ture和false两种值,可以在false的时候right-sibling指向其parent结点,其它情况正常