10.1 栈和队列
10.1-1 仿照图10-1,画图表示依次执行操作PUSH(S, 4)、PUSH(S, 1)、PUSH(S, 3)、POP(S)、PUSH(S, 8)和POP(S)每一步的结果,栈S初始为空,存储于数组S[1…6]中。
解:
4→41→413→41→418→41
10.1-2 说明如何在一个数组A[1…n]中实现两个栈,使得当两个栈的元素个数之和不为n时,两者都不会发生上溢。要求PUSH和POP操作的运行时间为O(1)。
思路:分别将数组的首尾两端作为两个栈的栈底即可。
解:
STACK-EMPTY(A)
if A.top == 0
return TRUE
else return FALSE
STACK-EMPTY(B)
if B.top == n + 1
return TRUE
else return FALSE
PUSH(A, x)
if A.top + 1 == B.top
error "overflow"
else A.top = A.top + 1
A[A.top] = x
PUSH(B, x)
if B.top - 1 == A.top
error "overflow"
else B.top = B.top - 1
A[B.top] = x
POP(A)
if STACK-EMPTY(A)
error “underflow”
else A.top = A.top - 1
return A[A.top+1]
POP(B)
if STACK-EMPTY(B)
error “underflow”
else B.top = B.top + 1
return A[B.top-1]
10.1-3 仿照图10-2,画图表示依次执行操作ENQUEUE(Q, 4)、ENQUEUE(Q, 1)、ENQUEUE(Q, 3)、DEQUEUE(Q)、ENQUEUE(Q, 8)和DEQUEUE(Q)每一步的结果,队列初始为空,存储于数据Q[1…6]中。
解:(N代表空)
4NNNNN→41NNNN→413NNN→N13NNN→N138NN→NN38NN
10.1-4 重写ENQUEUE和DEQUEUE的代码,使之能处理队列的下溢和上溢。
思路:在代码中添加对下溢和上溢的检查即可。
解:
ENQUEUE(Q, x)
if Q.head == (Q.tail + 1) mod Q.length
error "overflow"
else
Q[Q.tail] = x
if Q.tail == Q.length
Q.tail = 1
else Q.tail = Q.tail + 1
DEQUEUE(Q)
if Q.head == Q.tail
error "underflow"
else
x = Q[Q.head]
if Q.head == Q.length
Q.head = 1
else Q.head = Q.head + 1
return x
10.1-5 栈插入和删除元素只能在同一端进行,队列的插入和删除操作分别在两端进行,与他们不同的,有一种双端队列(deque),其插入和删除操作都可以在两端进行。写出4个时间均为O(1)的过程,分别实现在双端队列的两端插入和删除元素的操作,该队列是用一个数组实现的。
思路:注意检查队列的上溢和下溢。
解:
ENQUEUE-TAIL(D, x)
if D.head == (D.tail + 1) mod D.length
error "overflow"
else
D[D.tail] = x
if D.tail == D.length
D.tail = 1
else D.tail = D.tail + 1
ENQUEUE-HEAD(D, x)
if D.head == (D.tail + 1) mod D.length
error "overflow"
else
if D.head == 1
D.head = D.length
else D.head = D.head - 1
D[D.head] = x
DEQUEUE-HEAD(D)
if D.head == D.tail
error "underflow"
else
x = D[D.head]
if D.head == D.length
D.head = 1
else D.head = D.head + 1
return x
DEQUEUE-TAIL(D)
if D.head == D.tail
error "underflow“
else
if D.tail == 1
D.tail = D.length
else D.tail = D.tail - 1
return D[D.tail]
10.1-6 说明如何用两个栈实现一个队列,并分析相关队列操作的运行时间。
思路:栈A作为入队栈,栈B作为出队栈。在入队时,将栈B的所有元素POP出栈B(直到栈B为空),PUSH入栈A,再将新元素PUSH入栈A;在出队时,将栈A的所有元素POP出栈A(直到栈A为空),PUSH入栈B,再将栈B中元素进行POP。在整个过程中,栈A和栈B不能同时有元素。同时注意检查两个栈(整个队列)的上溢和下溢。
解:
ENQUEUE(x)
if A.top == A.length or B.top == B.length
error "overflow"
else
while B.top > 0
PUSH(A, POP(B))
PUSH(A, x)
DEQUEUE()
if STACK-EMPTY(A) and STACK-EMPTY(B)
error "underflow"
else if B.top > 0
return POP(B)
else
while A.top > 0
PUSH(B, POP(A))
return POP(B)
队列操作的运行时间取决于队列中的元素数(即两个栈的元素总数)。
10.1-7 说明如何用两个队列实现一个栈,并分析相关栈操作的运行时间。
思路:与上一题不同的是,本题的两个队列都能用于入栈和出栈。在入栈时,将新元素插入不为空的那个队列(若都为空则随意,我选择插入A);在出栈时,对不为空的那个队列执行出队操作(出队元素依次入队另一队列)直至剩下一个元素,最后将剩下的那个元素出队(即出栈的元素)。在整个过程中,队列A和队列B不能同时有元素。同时也要注意上溢和下溢的检测。
解:
PUSH(x)
if (A.head == (A.tail + 1) mod A.length) or (B.head == (B.tail + 1) mod B.length)
error "overflow"
else if B.head == (B.tail + 1) mod B.length // B为空,A可能为空也可能不为空
ENQUEUE(A, x)
else ENQUEUE(B, x) // B不为空
POP()
if (A.head == A.tail) and (B.head == B.tail)
error "underflow"
else if B.head == B.tail // B为空,A不为空
while A.tail > (A.head + 1) mod A.length
ENQUEUE(B, DEQUEUE(A))
return DEQUEUE(A)
else // A为空,B不为空
while B.tail > (B.head + 1) mod B.length
ENQUEUE(A, DEQUEUE(B))
return DEQUEUE(B)
栈操作的运行时间取决于栈中的元素数(即两个队列的元素总数)。
10.2 链表
10.2-1
解:能;否(除非在调用DELETE时明确给出删除元素的指针)。
10.2-2
思路:很简单,PUSH就在链表头部添加,POP就从链表头部删除。
解:
PUSH(L, x)
LIST-INSERT(L, x)
POP(L)
LIST-DELETE(L, L.head.next)
10.2-3
思路:稍微复杂一点,需要加一个指针,在插入元素时更新到新元素上,或是头插+尾删,或是尾插+头删。
10.2-4
解:
LIST-SEARCH'(L, k)
L.nil.key = k
x = L.nil.next
while x.key != k
x = x.next
return x
10.2-5
注:我选择的是使用哨兵。
解:
ONEWAY-CIRCULAR-LIST-INSERT(L, x)
x.next = L.nil.next
L.nil.next = x
ONEWAY-CIRCULAR-LIST-SEARCH(L, k)
L.nil.key = k
x = L.nil.next
while x.key != k
x = x.next
return x
ONEWAY-CIRCULAR-LIST-DELETE(L, x)
i = L.nil.next
while i.next != x
i = i.next
if i == L.nil
return "error"
i.next = i.next.next
O ( 1 ) ; O ( n ) ; O ( n ) O(1);O(n);O(n) O(1);O(n);O(n)
10.2-6
思路:如果两个集合都是双向链表,我们只需将第一个列表的最后一个元素链接到第二个元素中的第一个元素。如果实现使用了哨兵,我们需要摧毁其中一个。
10.2-7
思路:使用两个哨兵,在原哨兵前再添加一个哨兵,作为新链表的哨兵,每次将原哨兵后的第一个元素插入到新哨兵后,再删除该元素,最后删除原哨兵即可。需要一个新元素作为媒介。
解:
REVERSE(L)
L.newnil = nil
L.newnil.next = L.nil
while (L.nil.next != NIL)
intermedia = L.nil.next // 为媒介赋值
L.nil.next = intermedia // 将原哨兵的指针指向媒介
L.newnil.next = L.nil.next // 将原元素插入新哨兵后
DELETE(L, L.nil.next)
DELETE(L, L.nil)
10.2-8
我们可以通过np和prev的异或找到next,反之亦然。如果链表头部的前一个指针是NIL而尾部的下一个指针是NIL,那么我们只需要指向链表另一端的指针来访问它。逆转链表只是交换头部和尾部。
10.3 指针和对象的实现
10.3-1
略。。。
10.3-2
解:
ALLOCATE-OBJECT(A)
if free == NIL
error "out of space"
else x = free
free = A[free + 1]
FREE-OBJECT(A, x)
A[x+1] = free
free = x
10.3-3
解:可以设置,但没必要,因为没有使用,不设置还能省点时间。
10.3-4
解:我们可以在数组的开头分配元素。每当我们释放一个元素(除了最后唯一的元素)时,我们需要将它和栈顶之间所有元素的下标减1。需要线性时间。作为优化,每当我们释放数组中的最后一个元素时,我们就不需要扫描数组并更新指针。
10.3-5
解:来自https://ita.skanev.com/10/03/05.html
(1)我们遍历F并通过在其prev字段中放置一个特殊值来标记每个元素;
(2)我们启动两个指针,一个从内存开始处开始,一个从结尾开始。我们递增左指针直到它到达一个空单元格并递减右指针直到它到达非空单元格。我们将右指针指向的元素移动到左指针指向的位置,并在next字段中保留转发地址。当两个指针相遇时终止。此时,L位于数组的开头,而F位于最后。记录这个分界。
(3)我们通过使用next转发地址线性扫描数组的第一部分并更新超出分界的所有指针。
(4)最后,我们在自由列表中组织超出阈值的内存。
10.4 有根树的表示
10.4-1
略。。。注意下标为2和8的元素不在二叉树中。
10.4-2
解:
RECURSIVE-TRAVERSE-BINARY-TREE(T, x)
output x.key
if x.left != NIL
RECURSIVE-TRAVERSE-BINARY-TREE(T, x.left)
if x.right != NIL
RECURSIVE-TRAVERSE-BINARY-TREE(T, x.right)
初始调用RECURSIVE-TRAVERSE-BINARY-TREE(T, T.root)。
10.4-3
解:
ITERATIVE-TRAVERSE-BINARY-TREE(T)
let A[n] be a new array // A as a stack
PUSH(A, T.root)
output T.root.key
while !(STACK-EMPTY(A))
x = POP(A)
if x.right != NIL
PUSH(A. x.right)
output x.right.key
if x.left != NIL
PUSH(A, x.left)
output x.right.key
10.4-4
解:
RECURSIVE-TRAVERSE-TREE(T, x)
output x.key
if x.left-child != NIL
RECURSIVE-TRAVERSE-TREE(T, x.left-child)
else if x.right-sibling != NIL
RECURSIVE-TRAVERSE-TREE(T, x.right-sibling)
else return
10.4-5
解:
ITERATIVE-TRAVERSE-BINARY-TREE(T)
prev = NIL
x = T.root
while prev != T.root.right
if prev == x.p
if x.left != NIL
prev = x
x = x.left
output x
else if x.right != NIL
prev = x
x = x.right
output x
else
prev = x
x = x.p
else if prev == x.left
if x.right != NIL
prev = x
x = x.right
output x
else prev = x
x = x.p
else
prev = x
x = x.p
10.4-6
解:两个指针将是left-child和next。布尔值应该被称为last-sibling。识别孩子是从left-child开始,然后通过next,直到到达最后一个孩子。识别父结点应该通过next,直到到达last-sibling,然后再次通过next。
思考题
10-1 (链表间的比较)
解:
| 未排序的单链表 | 已排序的单链表 | 未排序的双向链表 | 已排序的双向链表 | |
|---|---|---|---|---|
| SEARCH(L, k) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) |
| INSERT(L, x) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
| DELETE(L, x) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
| SUCCESSOR(L, x) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
| PREDECESSOR(L, x) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
| MINIMUM(L) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
| MAXIMUM(L) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
10-2 (利用链表实现可合并堆)
a.
| MAKE-HEAP | O ( 1 ) O(1) O(1) |
| INSERT | O ( n ) O(n) O(n) |
| MINIMUM | O ( 1 ) O(1) O(1) |
| EXTRACT-MIN | O ( 1 ) O(1) O(1) |
| UNION | O ( n ) O(n) O(n) |
b.
| MAKE-HEAP | O ( 1 ) O(1) O(1) |
| INSERT | O ( 1 ) O(1) O(1) |
| MINIMUM | O ( n ) O(n) O(n) |
| EXTRACT-MIN | O ( n ) O(n) O(n) |
| UNION | O ( n ) O(n) O(n) |
c.
| MAKE-HEAP | O ( 1 ) O(1) O(1) |
| INSERT | O ( 1 ) O(1) O(1) |
| MINIMUM | O ( n ) O(n) O(n) |
| EXTRACT-MIN | O ( n ) O(n) O(n) |
| UNION | O ( n ) O(n) O(n) |
10-3 (搜索已排序的紧凑链表)
a.
证明:
(1)首先需要证明这两种函数会返回同样的结果。其实这是很显然的,因为两种函数都是正确的,如果链表中有(或)要搜索的元素,两种函数都会输出相应的结果。
(2)要证明COMPACT-LIST-SEARCH’中的for循环和while循环的迭代次数之和至少为t,其实从COMPACT-LIST-SEARCH的角度考虑比较容易。因为COMPACT-LIST-SEARCH的迭代次数是给定的,而且在迭代结束之前,COMPACT-LIST-SEARCH迭代的结果不是跳跃,就是步进。
①假设COMPACT-LIST-SEARCH的
t
t
t次迭代中,有
t
′
t'
t′次是跳跃,则COMPACT-LIST-SEARCH’也至少跳跃t’次。
解释:这是因为两函数的跳跃条件其实都是生成的随机数在
i
i
i和不大于
k
k
k的最大元素的下标之间,而COMPACT-LIST-SEARCH的
t
′
t'
t′次跳跃之间可能还夹杂着数次步进,这样会使COMPACT-LIST-SEARCH的
i
i
i增大,跳跃条件更加苛刻。形象一点,COMPACT-LIST-SEARCH’的跳跃范围是(前一次跳跃的结果,k),而COMPACT-LIST-SEARCH的跳跃范围是(前一次跳跃的结果+前一次跳跃后步进的次数,k)。
②即使COMPACT-LIST-SEARCH’的跳跃次数大于等于COMPACT-LIST-SEARCH,它也不会比COMPACT-LIST-SEARCH更接近目的元素。
解释:因为COMPACT-LIST-SEARCH’会跳跃,而COMPACT-LIST-SEARCH不会跳跃的范围是(COMPACT-LIST-SEARCH前一次跳跃的结果,COMPACT-LIST-SEARCH前一次跳跃的结果+前一次跳跃后步进的次数),因此即使COMPACT-LIST-SEARCH’多跳了,也不会超过目前COMPACT-LIST-SEARCH所在的元素。
分析:以COMPACT-LIST-SEARCH的最后一次跳跃作为分界,在最后一次跳跃前两函数的for循环迭代次数相等,且小于等于t,而只要最后一次跳跃后COMPACT-LIST-SEARCH进行了步进,则COMPACT-LIST-SEARCH’一定也进行了步进,所以它的for+while的迭代次数必然大于等于t。
b.
这很显然。 t t t就是尝试跳跃的次数, E [ X t ] E[X_t] E[Xt]就是尝试跳跃后步进的次数。
c.
证明:
Pr
{
X
t
≥
r
}
=
(
n
−
r
n
)
t
=
(
1
−
r
n
)
t
\Pr\{X_t \ge r\} = \bigg(\frac{n - r}{n}\bigg)^t = \bigg(1 - \frac{r}{n}\bigg)^t
Pr{Xt≥r}=(nn−r)t=(1−nr)t
E
[
X
t
]
=
∑
r
=
1
∞
Pr
{
X
t
≥
r
}
=
∑
r
=
1
n
Pr
{
X
t
≥
r
}
=
∑
r
=
1
n
(
1
−
r
n
)
t
E[X_t] = \sum_{r=1}^{\infty} \Pr\{X_t \ge r\} = \sum_{r=1}^n \Pr\{X_t \ge r\} = \sum_{r=1}^n \bigg(1 - \frac{r}{n}\bigg)^t
E[Xt]=∑r=1∞Pr{Xt≥r}=∑r=1nPr{Xt≥r}=∑r=1n(1−nr)t
d.
证明: ∑ r = 0 n − 1 r t ≤ ∫ 0 n x t d x = n t + 1 t + 1 \sum_{r=0}^{n-1} r^t \le \int_0^n x^t dx = \frac{n^{t+1}}{t+1} ∑r=0n−1rt≤∫0nxtdx=t+1nt+1
e.
证明:
E
[
X
t
]
=
∑
r
=
1
n
(
1
−
r
n
)
t
=
∑
r
=
0
n
−
1
(
r
n
)
t
=
1
n
t
∑
r
=
0
n
−
1
r
t
≤
1
n
t
⋅
n
t
+
1
t
+
1
=
n
t
+
1
\begin{aligned} E[X_t] &= \sum_{r=1}^n \bigg(1 - \frac{r}{n}\bigg)^t \\ &= \sum_{r=0}^{n-1} \bigg(\frac{r}{n}\bigg)^t \\ &= \frac{1}{n^t} \sum_{r=0}^{n-1} r^t \\ &\le \frac{1}{n^t} \cdot \frac{n^{t+1}}{t + 1} \\ &= \frac{n}{t+1} \end{aligned}
E[Xt]=r=1∑n(1−nr)t=r=0∑n−1(nr)t=nt1r=0∑n−1rt≤nt1⋅t+1nt+1=t+1n
f.
证明: O ( t + E [ X t ] ) = O ( t + n / ( t + 1 ) ) = O ( t + n / t ) O(t + E[X_t]) = O(t + n/(t+1)) = O(t + n/t) O(t+E[Xt])=O(t+n/(t+1))=O(t+n/t)
g.
证明:因为COMPACT-LIST-SEARCH的运行时间必然小于等于COMPACT-LIST-SEARCH’,所以对 t + n / t t + n/t t+n/t取最小值,易得期望运行时间为 O ( n ) O(\sqrt n) O(n)。
h.
证明:当链表中包含重复的关键字时,无法得出c的结论。只有当RANDOM找到的值大于当前值时,该算法才能跳跃。例如,如果我们有一串0的链表并且我们正在寻找1,那么算法仍然需要迭代到链表的末尾,因为它根本不会跳过。
这篇博客详细解答了《算法导论》第三版第10章关于栈、队列、链表和指针对象实现的练习题,包括如何用数组实现两个栈避免上溢、如何用链表实现队列和双端队列,以及如何用栈和队列互换角色。同时,博主探讨了如何处理各种操作的运行时间和特殊情况,如上溢和下溢。

1万+

被折叠的 条评论
为什么被折叠?



