【算法导论】第六章 堆 学习笔记

二叉堆是一个,可以看做一个近似完全二叉树,对于一个长度为 n 的数组给定一个数组下标 i ,   i = 0 , 1 , 2 , , n 1 ,则:

p a r e n t ( i ) = i / 2 l e f t ( i ) = i 2 + 1 r i g h t ( i ) = i 2 + 2

最大堆

除根节点外,所有节点满足:

A [ p a r e n t ( i ) ] A [ i ]

最小堆

除根节点外,所有节点满足:

A [ p a r e n t ( i ) ] A [ i ]

堆的高度

定义:叶节点到根的最短路径上边的长度为堆的高。

定理6.1

对于节点数为 n 的堆,其所有节点的可能取值高度(即,以节点 i 根节点的子树的高)为: h = 0 , 1 , l g n ,且高度为 h 的节点,至多有 n / n h + 1 个。

堆排序

以最大堆为例:

排序的整个过程可以看做如下:

1. 建堆 BUILD_MAX_HEAP
2. 堆维护 MAX_HEAPITY
3. 断尾重构 HEAPSORT

堆维护 MAX_HEAPITY

MAX_HEAPITY堆维护是建堆过程中至关重要的一步,它负责维护最大堆的性质不变,即根节点一定大于等于子节点。

代码思想(伪代码)

MAX_HEAPITY(A,i):
    left=i*2+1;
    right=i*2+2;
    //假设以left和right均为最大堆
    //判断left和right的合理性 < heap_size
    //比较节点与left、right的大小,
    //将大的节点与i交换,并递归调用MAX_HEAPITY(A,max_index)

复杂度分析

从上可知,对于节点 i 至多交换 h i 次,即被从堆顶被交换到堆底。所以时间复杂度为 O ( h ) = O ( l g n )

也可用递推公式 T ( n ) T ( 2 n / 3 ) + Θ ( 1 ) ,通过主定理求解得 T ( n ) = O ( l g n )

建堆 BUILD_MAX_HEAP

建堆主要是将无序数组按照树顺序,建立成最大(小)堆的过程。

已知对于节点数为 n ,   i = 0 , 1 , 2 n 1 的二叉树,其叶子节点的序号一定从 ( n + 1 ) / 2 开始,则在建堆初始化时,可以把第 ( n + 1 ) / 2 , ( n + 1 ) / 2 + 1 , , n 1 个叶子节点看做单节点最大堆。故建堆循环应从后往前,直到根节点 i = 0

代码思想(伪代码)

BUILD_MAX_HEAP(A):
    for i=(n+1)/2-1 to 0:
        MAX_HEAPITY(A,i)

复杂度分析

结论: O ( n ) 的线性复杂度。

分析:由伪代码可知,时间复杂度的上界为 ( n + 1 ) / 2 O ( M A X _ H E A P I T Y ) ,由于MAX_HEAPITY的复杂度与节点i所处的高度有关,我们可以利用定理6.1得到一个更紧确的界:即把要经过MAX_HEAPIFY的节点复杂度按其高度进行复杂度求和。通过变换和得线性复杂度 O ( n ) 。详见算法导论p88。

堆排序算法

用断尾重构的方法:每次把堆顶元素放至数组尾部,相应的堆size减小1

代码思想(伪代码)

HEAPSORT(A):
    for i=A.length to 1:
        exchange A[0],A[i]
        --A.heap_size;
        MAX_HEAPIFY(A,0);//对新的堆顶进行排序维护

复杂度分析

结论: O ( n l g n )

完整代码

//MaxHeap.h
#ifndef LINTCODE_MAXHEAP_H
#define LINTCODE_MAXHEAP_H

#include <vector>

using namespace std;

//原地排序算法
class MaxHeap {
private:
    void exchangeNum(vector<int> &A, int i, int j);

    void maxHeapity(vector<int> &A, int index, int &heap_size);

    void buildMaxHeap(vector<int>& A, int &heap_size);

public:
    void heapSort(vector<int>& A);
};


#endif //LINTCODE_MAXHEAP_H
//MaxHeap.cpp

#include "MaxHeap.h"
using namespace std;

void MaxHeap::exchangeNum(vector<int> &A, int i, int j) {
  int temp = A[i];
  A[i] = A[j];
  A[j] = temp;
}

//数组下标从0开始
void MaxHeap::maxHeapity(vector<int> &A, int index, int &heap_size) {
  int left = index * 2 + 1;
  int right = index * 2 + 2;
  int i = index;
  i = (left < heap_size && A[left] > A[i]) ? left : i;
  i = (right < heap_size && A[right] > A[i]) ? right : i;

  if (i != index) {
    exchangeNum(A, i, index);
    //递归调用
    maxHeapity(A, i, heap_size);
  }

}

void MaxHeap::buildMaxHeap(vector<int> &A, int &heap_size) {
  int n = A.size();
  int leaf_num = (n + 1) / 2; //节点数为n的堆,的叶子节点号为(n+1)/2,(n+1)/2+1,...,n
  for (int i = leaf_num - 1; i >= 0; --i) {
    maxHeapity(A, i, heap_size);
  }
}

//断尾法,得排序数组
void MaxHeap::heapSort(vector<int> &A) {
  int heap_size = A.size();
  buildMaxHeap(A, heap_size);

  for (int i = A.size() - 1; i > 0; --i) {
    exchangeNum(A, 0, i); //交换尾部和当前堆顶元素
    --heap_size;
    maxHeapity(A, 0, heap_size);
  }
}

优先队列

我们常用最大堆和最小堆来维护一个优先队列。

总结

  1. 堆排序是基于比较的排序,性能稳定,排序不稳定。
  2. 性能稳定:堆排序的(最好/平均/最差)时间复杂度均为 O ( n l g n )
  3. 只需要O(1)的辅助空间,既最高效率又节省空间
  4. 缺点:堆维护比较频繁,对于数组元素经常变动的情况下,不适合采用堆排序

猜你喜欢

转载自blog.csdn.net/tan_change/article/details/80104532