Solution to CLRS Chapter 6

来源:互联网 发布:步步高所有软件下载 编辑:程序博客网 时间:2024/05/24 01:51

6.1-1
由于heap是一个满二叉树,所以计算也比较简单最多是2^(h+1)-1,最少是2^h

6.1-2
和上一题一样,注意到heap是满二叉树。假设高度时h,第h层的数量为x,那么总数n=2^h-1+x
从而有h=log(n-x+1)

MIN-HEAPIFY(A, i)        smallest = i        if left(i) <= A.length && A[left(i)] < A[smallest]                smallest = left(i)        if right(i) <= A.length && A[right(i)] < A[smallest]                smallest = right(i)        if smallest != i                swap(A[i], A[smallest])                MIN-HEAPIFY(A, smallest)

6.2-3
nothing happens..

6.2-4
nothing happens..

6.2-5

MAX-HEAPIFY-ITERATION(A, i)        while left(i) <= A.length                largest = i                if A[left(i)] > A[i]                        largest = left(i)                if right(i) <= A.length && A[right(i)] > A[largest]                        largest = right(i)                if largest == i                        break                swap(A[i], A[largest])                i = largest

6.2-6
由于堆是满二叉树,所以高度是log(n),MAX-HEAPIFY遇到的最坏情况,就是从根节点移到底部,这样的复杂度就正好是堆的高度。是一个挺可观的复杂度

6.3-1
0: 5,3,17,10,84,19,6,22,9
1: 5,3,17,22,84,19,6,10,9
2: 5,3,19,22,84,17,6,10,9
3: 5,84,19,22,3,17,6,10,9
4: 84,22,19,10,3,17,6,5,9

6.3-2
这个和之前的一道题有联系,注意到从n/2开始,就不存在父节点了,每一个都是叶子节点,对于这种节点而言,即使用了MAX-HEAPIFY函数,也是不会做任何的更新的。所以可以直接从n/2开始向1递减的方式来更新。之所以要从后往前更新,就是为了保证,在父节点更新的时候,其子节点一定是满足MAX-HEAP property的,而这个则是这个MAX-HEAPIFY函数调用的条件.

6.3-3
不得不说,这个看上去像是nlogn的算法,竟然实际上只有n的复杂度!
这里把根节点标成了第log(n)层。。。由于第i层满足条件的话n/2^(i+1),第i-1层(往下数的话,i-1才是归纳法的下一个)最多是第i层的两倍n/2^i也成立。故结论正确。

6.4-1
0: 5,13,2,25,7,17,20,8,4
1: 25,13,20,8,7,17,2,5,4 (BUILD-MAX-HEAP)
2: 4,13,20,8,7,17,2,5,25
20,13,17,8,7,4,2,5,25
3: 5,13,17,8,7,4,2,20,25
17,13,5,8,7,4,2,20,25
4: 2,13,5,8,7,4,17,20,25
13,8,5,2,7,4,17,20,25
5: 4,8,5,2,7,13,17,20,25
8,7,5,2,4,13,17,20,25
6: 4,7,5,2,8,13,17,20,25
7,4,5,2,8,13,17,20,25
7: 2,4,5,7,8,13,17,20,25
5,4,2,7,8,13,17,20,25
8: 2,4,5,7,8,13,17,20,25

6.4-2
每一次循环,都将前i个元素中最大的放在第i个位置,所以之前的i-1一定维持是1-n个中最小的。

6.4-3
已经排好序的情况应该是heapSort的worst-case,因为heap-sort要花很多时间将其反转,然后再变回来。降序排列heapsort应该同样是nlogn的复杂度,因为将小元素放在堆顶,需要logn的更新。由此可见,堆排序的时间复杂度是稳定的nlogn。

6.4-4
BUILD-HEAP需要O(N)的复杂度,然后排序的过程最大的复杂度是nlogn,所以worst-case也是如此。

6.4-5
这个不就是6.4-3么。

6.5-1
0: 15,13,9,5,12,8,7,4,0,6,2,1
1: 1,13,9,5,12,8,7,4,0,6,2(返回15)
2: 13,12,9,5,6,8,7,4,0,1,2

6.5-2
0: 15,13,9,5,12,8,7,4,0,6,2,1
1: 15,13,9,5,12,8,7,4,0,6,2,1,10
2: 15,13,9,5,12,10,7,4,0,6,2,1,8
3: 15,13,10,5,12,9,7,4,0,6,2,1,8

6.5-3

HEAP-MINIMUN(A)        return A[1]HEAP-EXTRACT-MIN(A)        MINIMUM = A[1]        heap-size = A.length        A[1] = A[heap-size]        heap-size = heap-size - 1        MIN-HEAPIFY(A, 1)        return MINIMUNHEAP-DECREASE-KEY(A, i, key)        if key > A[i]                error "illeagal"        A[i] = key        while i > 1 &&  A[i] < A[parent(i)]                swap(A[i], A[parent(i)]                i = parent(i)MIN-HEAP-INSERT(A, key)        heap-size = heap-size + 1        A[heap-size] = -INF        HEAP-DECREASE-KEY(A, heap-size, key)

6.5-4
这个做法是必须的,因为在调用HEAP-INCREASE-KEY的时候,如果插入的值比当前的值小,会报错,提前将
A[heap-size]置为负无穷,就是为了确保正确执行

6.5-5
首先,保证插入的key一定比原来位置的大,这就保证了插入的i最多只对其父节点构成violation。而且,如果交换了i和parent(i),原来parent(i)一定能在i的位置维护MAX-HEAP property,这是因为原来的parent(i)大于i,i能够满足最大堆性质,parent(i)一定可以,另一个角度,新的i到了parent(i)的位置,其实情况就转变成了:在parent(i)插入一个key,且key比原来大,这个循环不变量是正确的!

6.5-6
这个题目是让你去优化插入操作中的exchange。这个操作是有一定的浪费的,浪费的根源就在于,插入的key还没有找到合适的位置,这个算法就已经急着先把它放下了。所以可以如此优化:

HEAP-INCREASE-KEY(A, i, key)        if key < A[i]                error "illegal"        while i > 1 && key > A[parent(i)]                A[i] = A[parent(i)]                i = parent[i]        A[i] = key

6.5-7
这个题目的意思是,如果用一个优先队列来模拟栈和队列。这个比较简单吧,只要把赋予元素优先级就可以了,比如可以用时间作为元素的优先级,那么min-heap就是队列,max-heap就是栈。
仔细想想,莫非栈和队列只是优先队列的一个特例吗?可以这样理解吗?

6.5-8

HEAP-DELETE(A, i)        heap-size = A.length        element = A[i]        A[i] = A[heap-size]        heap-size = heap-size - 1        MAX-HEAPIFY(A, i)        return element

该算法的复杂度和MAX-HEAPIFY一样,所以是nlog(n)
那么为什么这样子是对的? 首先,元素缺失删了,堆的大小正确的改变了,整个堆,只有i为根节点的子树才可能违背堆的性质,然而MAX-HEAPIFY可以正确的去维护这颗子树,所以算法是对的!

6.5-9
这是一个很有意思的题目,题目应该是来自于对merge sort二级优化(第一级是insertion sort),这个问题问如何去合并k个已排序的序列。
我的想法是:以每一个序列的首元素为优先级,以序列为元素,构建一个min-heap,这样每次得到的最小值,一定是整体上最小的。得到了一个最小值后,将该序列的头指针(用下标来模拟)指向下一个元素,没有就直接删除这个序列,否则的话,用MAX-HEAPIFY来维护这个堆。这样时间复杂度应该是log(k)的(k个元素的堆),然后这样进行n次。

Problem

6-1 Building a heap using insertion
(a)
通过插入的方式构建一个堆是否和在原数组上直接建堆得到相同的序列?这个是不一定的,我自己写了一个随机数据生成器,然后发现它们有时候是不一样的。
(b)
这个很明显啊,如果每次插入的时候,都需要从头插到根,那么插n次复杂度就是nlog(n)

6-2 Analysis of d-ary heaps
堆一般来说是指二叉堆,这里扩展了一下,变成了d叉堆。
(a)
如何在数组中表示一个d-ary heap,这个可以借鉴binary heap的思路,编号为i的元素,它的儿子应该是
(i-1)*d+1,(i-1)*d+2,..,(i-1)*d+d.
那么编号为j的元素,它的父节点就是(j/d)+1(int除法)

(b)
假设是一颗完全d叉树,高度为h,那么有(d^(h+1)-1)/(d-1)个节点,等于n
故而高度

(c)(d)(e)
这三道题都是对二叉堆的一个扩展,一种比较直接的方法,就是模仿二叉堆,只是遍历儿子节点的时候,从2个儿子变成了d个,这样一来,复杂度就是dlog(d),如果d是很大的话(应该不会很大,因为那样子的话,数组就需要很大,这个算法没什么用处……)效率不是很可观。我想到了一种优化是:将每一个d数组都构建成一个堆,这样子的话,查找d数组元素里的数字元素就是log(d)的复杂度,总的来说,就是(log(d))^2的复杂度,这是可以接受的!!!

6-3 Young tableaus
这道题给出了一个很巧妙的数据结构,国内似乎翻译成了杨氏矩阵。这个数据结构是一个长方形的,随意对空间的分配不像其他数据结构那么灵活,加之应用的范围可能很有限,所以没有其他的基本数据结构那么火吧。
如果从一个堆的角度来分析young tableaus,和堆相同之处在于,young tableaus有两个儿子,右和下,而且两个儿子都比他自己大(最小堆),但是略有区别的地方在于,每一个点,都有两个父亲,左和上。
young tableaus的规则是这个样子的:每一行有序,每一列有序。那么显然,young tableaus的A[1][1]就是整个的最小元素,A[n][m]就是整个的最大元素。而且我自己发现了一个比较简单但是很有趣的意思,如果在tableaus中,向右或向下两种选择不间断的进行划线,那么得到的这条路径一定是一条递增的路径。
(a)
这个画法有很多种,不唯一。我发现数据结构必须要牺牲一些精准性,才能够提高某一个方面的效率。heap也是这样子,能够很快的给出最大的,但是第二大的就开始略有些模糊。
(b)
young tableaus是按照行列递增的形式给出的,那么如果A[1][1]是INF,那么由A[1][1]画出的所有的线,都必须大于A[1][1],这样一来,由于从A[1][1]出发可以到达矩阵的任意点(只向左,向右),那么这个矩阵都是INF,是一个空矩阵。
如果A[n][m]不是INF,那么任何到A[n][m]的线,都不能超过A[n][m],也就是苏红所有元素都不是INF,那么这个就是一个满矩阵。
(c)
实现一个提取最小元素的算法O(m+n)的复杂度。
首先,最小的元素显然是A[1][1],关键在于如何删除A[1][1]后,维护这个矩阵的性质。这个需要仔细的做一下分析。
首先,如果是下面元素比右边元素小,应该选哪一个呢?如果这个时候选择了右边的元素,那么这个矩阵的性质就不再保持了,因为此时A[1][1]>A[2][1],如果选择了下面的元素,那么至少对右边而言,是没有违反性质的,那么问题就变成了:从(2,1)开始到(N,M)的矩阵,删除A(2,1)也就是首元素。这样递归下去,直到遇到已经无法违背的情况,或者是超出了边界。为什么这样做是对的呢?因为每一次进入下一个缩减的规模前,我都保证了现有的矩阵只在下一个要处理的部分是可能违法的。

YOUNG-TABLEAUS-EXTRACT-MIN(A)        MINIMUN = A[1][1]        A[1][1] = INF        YOUNG-TABLEAUS-EXTRACT(A, 1, 1, N, M)        return MINIMUNYOUNG-TABLEAUS-EXTRACT(A, topx, topy, N, M)        if topx == N && topy == M                return        smallestx = topx        smallesty = topy        if topx + 1 <= N && A[topx+1][topy] < A[topx][topy]                smallestx = topx + 1                smallesty = topy        if topy + 1 <= M && A[topx][topy+1] < A[smallestx][smallesty]                smallestx = topx                smallesty = topy + 1        if smallestx != topx || smallesty != topy                swap(A[topx][topy], A[smallestx][smallesty])                YOUNG-TABLEAUS-EXTRACT(A, smallestx, smallesty, N, M)

(d)
实现插入操作。
我认为最好的插入点,是右下角A[N][M],直觉。
假设更新进行到了A[x][y],那么可能会遇到这么几种情况:1,A[x][y]>A[x-1][y],A[x][y]>A[x][y-1],这也就是说,到了(x,y)这一点,矩阵的性质左和上没有违背,如果之前的更新都是对的,我既然是从右下来的,那么右下的正确性是有保证的。
2,A[x][y]

YOUNG-TABLEAUS-INSERT(A, key)        if A[N][M] != INF                return FULL        A[N][M] = key        YOUNG-TABLEAUS-INSERT-RECURSION(N, M)YOUNG-TABLEAUS-INSERT-RECURSION(x, y)        largestx = x        largesty = y        if x > 1 && A[x-1][y] > A[x][y]                largestx = x                largesty = y        if y > 1 && A[x][y-1] > A[largestx][largesty]                largestx = x                largesty = y        if largestx != x && largesty != y                swap(A[x][y], A[largestx][largesty])                YOUNG-TABLEAUS-INSERT-RECURSION(largestx, largesty)

(f)
一开始想到的矩阵四分法。就是找矩阵中心,然后判断这个点是否比key大,或者小,然后决定在哪块区域,但事实上这种算法效率比较低,复杂度是高于O(m+n)的,应该是n^1.4。其实这题可以这么做,从左下角开始找,如果value>key,那么就往上走,如果value

0 0
原创粉丝点击