Introduction_to_algorithms_6

来源:互联网 发布:英译汉软件下载 编辑:程序博客网 时间:2024/04/30 20:58

第六章 堆排序

堆排序(大根堆)&&优先队列

问题描述:

  堆排序:堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
  优先队列:在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (largest-in,first-out)的行为特征。

伪代码:

// 维护一个最大堆MAX-HEAPIFY(A, i):   //递归版本1  l = LEFT(i)2  r = RIGHT(i)3  if l <= A.heap-size && A[l] > A[i]4      largest = l5  else6      largest = i7  if r <= A.heap-size && A[r] > A[i]8      largest = r9  if largest != i10      exchange A[i] with A[largest]11 MAX-HEAPIFY(A, largest)MAX-HEAPIFY(A, i):   //非递归版本1  while true2      l = LEFT(i)3      r = RIGHT(i)4      if l <= A.heap-size && A[l] > A[i]5          largest = l6      else7          largest = i8      if r <= A.heap-size && A[r] > A[largest]9          largest = r10     if largest != i11         exchange A[i] with A[largest]12         i = largest13     else14         break// 创建一个最大堆BUILD-MAX-HEAP(A):1  A.heap-size = A.length2  for i = ⌊A.length/2⌋ downto 13      MAX-HEAPIFY(A, i)// 堆排序HEAPSORT(A):1  BUILD-MAX-HEAP(A)2  for i = A.length downto 23      exchange A[1] = A[i]4      A.heap-size = A.heap-size - 15      MAX-HEAPIFY(A, 1)// 返回优先序列的第一个元素HEAP-MAXIMUM(A):1  return A[1]// 返回优先队列的第一个元素,并删去这个元素HEAP-EXTRACT-MAX:1  if A.heap-size < 12      error "heap underflow"3  max = A[1]4  A[1] = A[A.heap-size]5  A.heap-size = A.heap-size-16  MAX-HEAPIFY(A, 1)7  return max// 将下标为i的元素的值增加到keyHEAP-INCREASE-KEY(A, i, key):1  if key < A[i]2      error "new key is smaller than current key"3  A[i] = key4  while i > 1 && A[PARENT(i)] < A[i]5      exhange A[i] with A[PARENT(i)]6      i = PARENT(i)// 向最大堆中增加一个新的元素keyMAX-HEAP-INSERT(A, key):1  A.heap-size = A.heap-size + 12  A[A.heap-size] = -∞3  HEAP-INCREASE-KEY(A, A.heap-size, key)

c语言实现:

代码详见: github

练习&&思考题

6.1-1

  元素最少的情况是最底层只有一个叶子,即 \(2^h\) ;元素最多的情况是整棵树是满的,即 \(2^{h+1}−1\) 。(这里按照叶子到根的最大边数来定义高度)

6.1-2

  设高度为h,那么可知

2hn2h+11

hlgn<h+1

lgn1<hlgn

即 \(h = \lfloor \lg n \rfloor\) 。

6.1-3

令p(i)为第i个元素的父亲下标,那么对任意i>1,都有 \(A[i] \le A[p(i)] \le A[p(p(i))] \le \cdots \le A[1]\) ,即都小于根元素。

6.1-4

  最小的元素处于最底层或次底层的叶子结点,即该结点没有子结点。这时因为大根堆(max heap)中父结点一定不小于子结点,所以最小的元素必须出现在叶子结点上。

6.1-5

对于有序的数列肯定有 \(\forall i \lt j, a_i \lt a_j\) ,而 \(Parent(i) = i/2 \lt i\) ,故有 \(a_{Parent(i)} \lt a_i\) ,满足小根堆的性质。

6.1-6

树结构如下,元素7和6不满足大根堆的性质,故不是大根堆。
         23
    17        14
  6    13  10    1
 5  7  12

6.1-7

设堆的高度为h,那么除去最底层的结点的数量为 \(2^h = 2^{h+1}/2\) ,所以底层结点的下标为 \(2^{h+1}/2 + 1, 2^{h+1}/2 + 2 \cdots ,n\) ,即 \(\lfloor n/2 \rfloor + 1, \lfloor n/2 \rfloor + 2, \cdots , n\)

6.2-1

简单起见,我们只列出受影响的部分子树
     3
 10     1

8  9  0

    10
  3     1

8  9  0

    10
  9     1
8  3  0

6.2-2

只要把相应的最小元素移到父亲的位置即可

Min-Heapify(A, i)1  l = Left(A, i)2  r = Right(A, i)3  if l <= A.heap-size and A[l] < A[i]4      smallest = l5  else6      smallest = i7  if r <= A.heap-size and A[r] < A[smallest]8      smallest = r9  if smallest != i10     swap A[i] with A[smallest]11     Min-Heapify(A, smallest)

由于只改变了比较部分,该算法的时间复杂度与Max-Heapify一致。

6.2-3

不会有任何改变。

6.2-4

当 \(i \gt A.heap-size/2\) 时,结点为叶子结点没有孩子,所以不会有任何改变。

6.2-5

Max-Heapify(A, i)1  while true2      l = Left(A, i)3      r = Right(A, i)4      if l <= A.heap-size and A[l] > A[i]5          largest = l6      else7          largest = i8      if r <= A.heap-size and A[r] > A[largest]9          largest = r10     if largest != i11         swap A[i] with A[largest]12         i = largest13     else14         break

6.2-6

Max-Heapify最坏的情况下对从根结点到最深的叶子结点中的所有结点都调用一遍自身,例如叶子结点大于到根结点路径上所有结点的值,如果结点数为n,那么高度为 \(\lg n\) ,如果每次比较和交换的时间为 \(\Theta (1)\) ,那么总时间为 \(\Theta (\lg n)\) 。

6.3-1

        5
    3        17
 10   84   19    6

22 9

         5
    3       17
 22   84    19  6

10 9

        5
    3      19
 22   84   17   6

10 9

        5
    84      19
 22    3    17   6

10 9

       84
    22      19
 10    3    17   6
5  9

6.3-2

因为调用Max-Heapify都假定当前结点的左右子树都是堆,只有从下往上调用才能满足这样的前提。

6.3-3

  首先考虑h=0,即叶子结点的情况。假设堆的高度为H,当该堆是一个满二叉树时,显然有叶子结点的数量为 \(2^{H+1}-2^H=2^H=n/2=n/2^{h+1}\) 。当堆不是满二叉树时,叶子结点分布在深度为H和H-1的结点中。我们令x表示深度为H的结点数,那么n-x即为高度为H-1的满二叉树的结点数,所以n-x必为奇数。由此可见x和n中有一个奇数一个偶数。

  • 当n是奇数x是偶数时,说明所有的非叶结点都有两个孩子,那么 \(n_{leaf} + n_{internal} = 2n_{internal} + 1\) 可得 \(n_{leaf} = n_{internal} + 1\) ,所以 \(n = n_{leaf} + n_{internal} = 2 n_{leaf} - 1 \Rightarrow n_{leaf} = (n+1)/2 = \lceil n/2 \rceil\)
  • 当n是偶数x是奇数时,我们为最右下的叶子结点添加一个兄弟使其变为第一种情况,这时可得 \(n_{leaf} + 1 = \lfloor (n+1)/2 \rfloor = \lceil n/2 \rceil + 1 \Rightarrow n_{leaf} = \lceil n/2 \rceil\)

      接下来我们假设当高度为h-1时命题成立,证明当高度为h时也成立。令 \(n_h\) 表示高度为h的结点的个数, \(n_h’\) 表示去掉叶子结点后高度为h的结点的个数,由于去掉叶子结点后所有结点的高度减1,所以有 \(n_h = n_{h-1}’\) 。又由于叶子结点的数量前面已经求出为 \(\lceil n/2 \rceil\) 所以去掉后结点的数量变为 \(\lfloor n/2 \rfloor\) ,代入上式可得:

n_h=n_h1n2h=n/22hn/22h=n2h+1

于是我们利用数学归纳法得证。

6.4-2

  Initialization: 开始时由于使用Build-Max-Heap把A[1..n]变成一个堆,而A[n+1,n]即空集显然包含0个A种的最大值,所以满足循环不变条件;
  Maintenance: 一遍循环开始前有A[1]为A[1..i]中的最大值,同时 \(A[1] \le A[i+1] \le A[i+2] \le \cdots A[n]\) ,这时交换A[1]和A[i]使得 \(A[i] \le A[i+1] \le A[i+2] \le \cdots A[n]\) 。之后将heap-size减1调用Max-Heapify,使得A[1..i-1]重新成为一个堆,这在i增1之后满足了下一遍循环的前提条件;
  Termination: 当循环终止时i=1,此时有 \(A[1] \le A[1+1] \le A[1+2] \le \cdots A[n]\) ,即序列有序。

6.4-3

  • 当序列已经有序时,建堆的时候每次调用Max-Heapify都要进行最大次数的交换,根据6.3节所求的上界,时间为O(n)。之后排序的时间为 \(O(n \lg n)\) ,所以总时间为 \(O(n \lg n)\)
  • 当序列为递减时,建堆的时候每次调用Max-Heapify只进行一次交换,共n/2次,所以时间为O(n)。之后排序的时间为 \(O(n \lg n)\) ,所以总时间为 \(O(n \lg n)\)

6.4-4

当排序阶段每次调用Max-Heapify时都进行最大次数的交换时,总的交换次数为:

T_i=2nlgn=Θ(nlgn)=Ω(nlgn)

6.5-3

Heap-Minimum(A)1  return A[1]Heap-Min(A)1  if A.heap-size == 02      error "heap underflow"3  min = A[1]4  A[1] = A[A.heap-size]5  A.heap-size = A.heap-size - 16  Min-Heapify(A, 1)7  return minHeap-Decrease-Key(A, i, key)1  if A[i] < key2      error "new key is bigger than original"3  A[i] = key4  while i > 1 and A[i] < A[Parent(i)]5      exchange A[i]  A[Parent(i)]6      i = Parent(i)Heap-Insert(A, key)1  A.heap-size = A.heap-size + 12  A[A.heap-size] = INFINITE3  Heap-Decrease-Key(A, A.heap-size, key)

6.5-4

因为Heap-Increase-Key的前提条件是原来的key小于新的key,将原来的key设为负无穷就可以保证Heap-Increase-Key调用成功。

6.5-5

  Initialization: 在还没有增加之前A[1..A.heap-size]是一个大根堆,所以在A[i]增加为key之后只有可能A[i]和A[Parent(i)]之间不满足堆的性质;
  Maintenance:假如一遍循环开始前满足循环条件,那么将A[i]和A[Parent(i)]之中大的那个放到A[Parent(i)]使得他们之间满足堆的性质,而有可能使得A[Parent(i)]和A[Parent(Parent(i))]违背堆的性质,当i变为Parent(i)之后满足了下一次循环的前提条件;
  Termination:当循环终止时i=1或者A[i]

6.5-6

可以先向下移动小于他的祖先,直到没有小于他的祖先后放在空出的位置上

Heap-Increase-Key(A, i, key)1  if A[i] < key2      error "new key is smaller than original"3  while i > 1 and A[Parent(i)] < key4      A[i] = A[Parent(i)]5      i = Parent(i)6  A[i] = key

6.5-7

  要使得队列先进先出就要使先进入元素的key大于后进入的key即可,最简单的方式就是第一个进入的key值最大,后面进入的key值比前面的少1。 反过来第一个进入的key值为0,之后进入的key值递增就能实现先进后出的栈。

6.5-8

相当于对A[i]为根的堆进行Extract-Max操作

Heap-Delete(A, i)1  A[i] = A[A.heap-size]2  A.heap-size = A.heap-size - 13  Heapify(A, i)

6.5-9

  建一个大小为k的堆,堆中的每个元素代表一个List,元素的key为List当前最小元素的值,每次取出堆顶的元素,然后插入相应List中下一个元素的值,如果该List没有下一个元素就不插入,直到n个元素归并完毕。伪代码如下

MIN-HEAPIFY(A, Ai, K, i)1  while true2      l = LEFT(i)3      r = RIGHT(i)4      if l <= A.heap-size && A[l] < A[i]5          smallest = l6      else7          smallest = i8      if r <= A.heap-size && A[r] < A[smallest]9          smallest = r10     if smallest != i11         exchange A[i] with A[smallest]12         exchange Ai[i] with Ai[smallest]13         i = smallest14     else15         breakBUILD-MIN-HEAP(A, Ai, k)1  A.heap-size = A.length2  for i = ⌊A.length/2⌋ downto 13      MAX-HEAPIFY(A, i)K-MERGE(Lists, k, Result)1  for i = 1 to k:2      A[i] = Lists[i].value3      Ai[i] = i4  BUILD_MIN_HEAP(A, Ai, k);5  while k > 06      increase new node in Result's rear, new node's value = A[1]7      if Lists[Ai[1]]->next == NULL8          exchange A[1] with A[k]9          exchange Ai[1] with Ai[k]10         k = k - 111     else12         lists[Ai[1]] = lists[Ai[1]]->next;13         A[1] = lists[Ai[1]];14     HEAPIFY(A, Ai, K, 1)

c语言实现:

代码详见: github /k_queue_merge(exercise6.5-9).c

6-1

a) 不一定能产生相同的堆,例如A={1,2,3,4,5},利用heapify产生的堆为
   5
 4   3
1 2
利用heap-insert产生的堆为

   5
 4   2
1 3

b) 当A初始时元素递增排列时,每次调用heap-insert都要将该元素移到堆顶,所以移动的次数

T(n)=_i=2nlgi_i=2nlgi=O(nlgn)

6-2

a) 当用A[1]表示堆顶的时候,他的孩子下标范围是[2, d+1],A[2]的孩子的下标范围是[d+2, 2d+1],A[3]孩子的下标范围是[2d+2, 3d+1],由此可得A[i]孩子下标范围是[d(i-1)+2, di+1]。同理可求元素父亲的下标。用Parent(i)来求下标为i的元素的父亲,用Child(i,j)用来求下标为i的元素的第j个孩子

Parent(i)1  return (i - 2) / d + 1Child(i, j)1  return d * (i - 1) + j + 1

b) 高度为H的堆节点数量为

ndHH=_h=0H1dh+j(1jdH)=dH1d1+j=(nj)(d1)+1=O(log_dn))=O(lgn)

c)

Max-Heapify(A, i)1  max = i2  for j = 1 to d3      c = Child(i, j)4      if (c <= A.heap-size && A[c] > A[max])5          max = c6  if i != max7      exchange A[i] with A[max]8      Max-Heapify(A, max)Extract-Max(A)1  max = A[1]2  A[1] = A[A.heap-size]3  A.heap-size = A.heap-size - 14  Max-Heapify(A, 1)5  return max

最坏情况下,换到A[1]位置的叶子元素需要移到堆底,共需 \(O(\log_d n)\) 次交换,每次交换需要d次比较,共 \(O(d \log_d n)\) 次比较。

d)

Increase(A, i, key)1  if A[i] > key2      error "new key should be lagger than original"3  A[i] = key4  while i > 1 && A[i] > A[Parent(i)]5      exchange A[i] with A[Parent(i)]6      i = Parent(i)Max-Insert(A, key)1  A.heap-size = A.heap-size + 12  A[A.heap-size] = -INFINITE3  Increase(A, A.heap-size, key)

最坏情况下,增大的元素需要换到堆顶,而且该元素处于堆底,共需 \(O(\log_d n)\) 次交换。

e) 见d)

6-3

a) 最简单的方法就是按照顺序从左到右从上到下依次填充数字即可,剩余的位置填上无穷。

281639412514

b) 如果Y[1,1]为无穷则其右边和下边的数都不能小于无穷,只有所有的元素都是无穷(即Y为空)才能满足;同理,如果Y[m,n]小于无穷则其左上元素都小于无穷,即Y全满。

c) 类似heapify的过程,只不过每次与右边和下边的元素进行比较。

Youngify(Y, i, j)1  i2 = i2  j2 = j3  if i + 1 <= Y.m && Y[i + 1, j] < Y[i, j]4      i2 = i + 15  if j + 1 <= Y.n && Y[i2, j + 1] < Y[i2, j]6      j2 = j + 17  if i != i2 or j != j28      exchange Y[i, j] with Y[i2, j2]9      Youngify(Y, i2, j2)Extract-Min(Y)1  min = Y[1, 1]2  Y[1, 1] = Y[Y.m, Y.n]3  Y[Y.m, Y.n] = INFINITE4  Youngify(Y, 1, 1)5  return min

最坏情况下左上的元素要移到右下,共m+n-2次交换,所以时间复杂度为O(m+n)

d) 类似于堆的insert,每次与上方和左方的元素比较

Decrease(Y, i, j, key)1  Y[i, j] = key2  i2 = i3  j2 = j4  if i > 1 && Y[i - 1, j] > Y[i, j]5      i2 = i - 16  if j > 1 && Y[i, j - 1] > Y[i2, j]7      j2 = j - 18  if i != i2 or j != j29      key = Y[i, j]10     Y[i, j] = Y[i2, j2]11     Decrease(Y, i2, j2, key)Insert(Y, key)1  Decrease(Y, Y.m, Y.n, key)

最坏情况下右下的元素要移到左上,共m+n-2次交换,所以时间复杂度为O(m+n)

e)

Sort(A)1  n = Sqrt(A.length)2  Y.m = n3  Y.n = n4  for i = 1 to n5      for j = 1 to n6          Y[i, j] = INFINITE7  for i = 1 to A.length8      Insert(Y, A[i])9  for i = 1 to A.length10     A[i] = Extract-Min(Y)

第7行共执行 \(n^2\) 次,复杂度为 \(O(n^2)\) ,第9行共执行 \(n^2\) 次,复杂度为 \(O(n+n)O(n^2)=O(n^3)\) ,第11行共执行n2n2次,复杂度为 \(O(n+n)O(n^2)=O(n^3)\) ,所以总时间复杂度为 \(O(n^3)\)

f)

Contains(Y, key, i, j)1  if i > Y.m || j < 12      return false3  if Y[i, j] == key4      return true5  ii = i6  jj = j7  if i == Y.m || Y[i + 1, j] > key8      jj = jj + 19  if j == 1 || Y[i, j - 1] < key10     ii == ii + 111 return Contains(Y, key, ii, jj)Contains(Y, key)1 return Contains(Y, key, 1, Y.n)

最坏情况下需要从右上的元素检查到左下角的元素,每次向右或向下移动1,所以共需检查m+n次,时间复杂度为O(m+n)

0 0