《算法导论》第三版第10章 基本数据结构 练习&思考题 个人答案

这篇博客详细解答了《算法导论》第三版第10章关于栈、队列、链表和指针对象实现的练习题,包括如何用数组实现两个栈避免上溢、如何用链表实现队列和双端队列,以及如何用栈和队列互换角色。同时,博主探讨了如何处理各种操作的运行时间和特殊情况,如上溢和下溢。

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{Xtr}=(nnr)t=(1nr)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=1Pr{Xtr}=r=1nPr{Xtr}=r=1n(1nr)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=0n1rt0nxtdx=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=1n(1nr)t=r=0n1(nr)t=nt1r=0n1rtnt1t+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,那么算法仍然需要迭代到链表的末尾,因为它根本不会跳过。

算法导论第三版中文版 pdf高清版 在有关算法的书中,有一些叙述非常严谨,但不够全面;另一些涉及了大量的题材,但又缺乏严谨性。算法导论第三版中文版将严谨性和全面性融为一体,深入讨论各类算法,并着力使这些算法的设计和分析能为各个层次的读者接受。全书各自成体系,可以作为独立的学习单元;算法以英语和伪代码的形式描述,具备初步程序设计经验的人就能看懂;说明和解释力求浅显易懂,不失深度和数学严谨性。全书选材经典、内容丰富、结构合理、逻辑清晰,对本科生的数据结构课程和研究生的算法课程都是非常实用的教材,在IT专业人员的职业生涯中,算法导论第三版也是一本案头必备的参考书或工程实践手册。 第3版的主要变化 1、新增了van Emde Boas树和多线程算法,并且将矩阵基础移至附录。 2、修订了递归式(现在称为“分治策略”)那一的内容,更广泛地覆盖分治法。 3、移除两很少讲授的内容:二项堆和排序网络。 4、修订了动态规划和贪心算法相关内容。 5、流网络相关材料现在基于边上的全部流。 6、由于关于矩阵基础和Strassen算法的材料移到了其他,矩阵运算这一的内容所占篇幅更小。 7、修改了对Knuth-Morris-Pratt字符串匹配算法的讨论。 8、新增100道练习和28道思考题,还更新并补充了参考文献。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值