《算法导论》第三版第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
  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值