【算法学习】堆排序(Heap Sorting)

堆排序引入了另外一种算法设计技术:利用某种数据结构(在此算法中为“堆”)来管理算法执行中的信息。


一、堆

我们通常使用的堆的二叉堆,它是一种数组对象,可以被视为一棵完全二叉树。树中的每个节点与数组中的节点相对应。如下图所示:


表示堆的数组通常由两个属性:数组中元素的个数length[A],存放在A中的堆的元素的个数heap-size[A]。也就是说存放在A中的一些元素可能不属于对应的堆。因此:

heap-size[A] <= length[A]。

给定了某个节点的下标i,其父节点、左儿子以及右儿子可以很容易计算出来:

注意:下标均以1开始,而不是0

父节点:PARENT(i) = floor(i / 2)(向下取整)

左儿子:LEFT(i) = 2 * i

右儿子:RIGHT(i) = 2 * i + 1


堆的分类

二叉堆通常分为大根堆和小根堆。

在大根堆中,对于以某个节点为根的子树,其各节点的值都不大于其根节点的值,即A[PARENT(i)] >= A[i]

小根堆则正好相反。


在堆排序算法中,我们使用的是大根堆。小根堆通常在构造优先队列时使用。


堆的高度

节点在堆中的高度被定义为此节点到叶子的最长简单下降路径中边的数目。

堆的高度即根节点的高度。


二、堆的调整

很多时候,一棵二叉树不满足大根堆的性质,我们需要采用某种算法进行调整以使其变为大根堆。下面的函数MaxHeapify将会实现此功能。我们假定以某个节点i的左儿子节点和右儿子节点为根的子树都是大根堆,但是A[i]可能小于其子节点的值,这样就违背了大根堆的性质。

算法大致思想:

首先找出i节点和其左右子节点共3个节点中值最大的节点,如果不是i,则将i与值最大的节点互换。这样确保了根i处的值是最大的。然后调整以刚才与i互换的子节点为根的子树,递归调用算法MaxHeapify。

算法C++代码实现:

//得到父节点索引 int getParent(int i) { return (int)floor((float)i / 2); } //得到左子树索引 int getLeftSon(int i) { return (2 * i); } //得到右子树索引 int getRightSon(int i) { return (2 * i + 1); } //调整以某个节点i为根节点的子树为大根堆 void MaxHeapify(int A[],int i,int HeapSize) { int left = getLeftSon(i); int right = getRightSon(i); int largest = i;//记录值最大的元素的索引 if (left <= HeapSize && A[left] > A[i]) { largest = left; } if (right <= HeapSize && A[right] > A[largest]) { largest = right; } if (largest != i)//此子树不满足大根堆的性质,需要进行调整 { //进行交换 int temp = A[i]; A[i] = A[largest]; A[largest] = temp; MaxHeapify(A,largest,HeapSize);//递归调用,继续调整子树 } }

图示说明:


时间复杂度的分析:

调整A[i]、A[left]、A[right]的时间为常量时间。下面分析递归调整子树所需的时间。

假设树的节点个数为n,则i节点的子树节点个数最多为2n/3(在最底层刚好半满的时候),推导过程:

树总的节点个数

n = pow(2,0) + pow(2,1) + ... + pow(2,h - 1 - 1) + 1 / 2 *pow(2,h - 1),其中h为树的高度(根为第一层)

= 3 * pow(2,h - 2) - 1

假设根节点的左儿子所对应的子树节点数大于右子树的节点,其高度应为h - 1,节点数:

pow(2,0) + pow(2,1) + ... + pow(2,h - 1 - 1) = pow(2,h - 1) - 1
结合上面的式子可达到,子树的最大节点数为(2 * n - 1) / 3.


这样,调整堆的时间复杂度的推算为:

T(n) <= T(2n / 3) + O(1)

求解递归式得到:T(n) = O(lgn)。或者是O(h),h为树的高度。


三、建堆

我们可以自底向上地依次调整子树来获得大根堆。

注意:子数组A[floor(n/2) + 1]...A[n]是树的叶子。显然叶子可以看成只有一个元素的大根堆,不用调整。只需从非叶子节点自后向前依次调整即可。

C++代码实现:

//建堆 void buildMaxHeap(int A[],int HeapSize) { for (int i = (int)floor((float)HeapSize / 2);i > 0;--i) { MaxHeapify(A,i,HeapSize); } cout << "建成的大根堆:" << endl; printHeap(A,HeapSize); }
一个有n个元素堆的高度为floor(lgn),并且在任意高度h上之多有ceil(n/pow(2,h+1))个节点。

这样,时间复杂度推算为:


即可以在线性时间内将一个无序数组建成大根堆。


4、堆排序

首先是将无序数组建成大根堆,然后通过把最大元素即根与A[n]互换使得最大元素到达正确位置。

然后将节点n从堆中去掉,原来根的子女仍然是大根堆,但是新的根元素可能违反了大根堆的规则,必须重新调整A[1,...,n - 1]为大根堆。这样重复进行。直至堆的大小变为1才结束。

C++代码实现:

//堆排序 void heapSort(int A[],int HeapSize) { buildMaxHeap(A,HeapSize); for (int i = HeapSize;i > 0;--i) { int temp = A[1]; A[1] = A[i]; A[i] = temp; MaxHeapify(A,1,i - 1); } }图示说明:


堆排序的时间复杂度是O(nlgn)。而且是一种原地排序算法,即在任何时刻数组中只有常数个元素存储在输入数组以外。


程序实例:

#include <iostream> #include <cmath> using namespace std; //注意:下表都以1开始,而不是0 //得到父节点索引 int getParent(int i) { return (int)floor((float)i / 2); } //得到左子树索引 int getLeftSon(int i) { return (2 * i); } //得到右子树索引 int getRightSon(int i) { return (2 * i + 1); } //调整以某个节点i为根节点的子树为大根堆 void MaxHeapify(int A[],int i,int HeapSize) { int left = getLeftSon(i); int right = getRightSon(i); int largest = i;//记录值最大的元素的索引 if (left <= HeapSize && A[left] > A[i]) { largest = left; } if (right <= HeapSize && A[right] > A[largest]) { largest = right; } if (largest != i)//此子树不满足大根堆的性质,需要进行调整 { //进行交换 int temp = A[i]; A[i] = A[largest]; A[largest] = temp; MaxHeapify(A,largest,HeapSize);//递归调用,继续调整子树 } } //输出数组元素 void printHeap(int A[],int HeapSize) { for(int i = 1;i <= HeapSize;++i) { cout << A[i] << " "; } cout << endl; } //建堆 void buildMaxHeap(int A[],int HeapSize) { for (int i = (int)floor((float)HeapSize / 2);i > 0;--i) { MaxHeapify(A,i,HeapSize); } cout << "建成的大根堆:" << endl; printHeap(A,HeapSize); } //堆排序 void heapSort(int A[],int HeapSize) { buildMaxHeap(A,HeapSize); for (int i = HeapSize;i > 0;--i) { int temp = A[1]; A[1] = A[i]; A[i] = temp; MaxHeapify(A,1,i - 1); } } int main() { const int length = 11; //堆元素下班从1开始 int A[length] = {0,4,1,3,2,16,9,10,14,8,7}; int HeapSize = length - 1; heapSort(A,HeapSize); cout << "堆排序之后:" << endl; printHeap(A,HeapSize); }
运行结果:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值