算法导论学习001-堆排序
堆排序
堆排序基本简介
1991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W.Floyd)和威廉姆斯(J.Williams)在1964年共同发明了著名的堆排序算法。
堆排序属于比较排序中的一种,具有O(nLgn)的时间复杂度,空间复杂度为O(1)。因此堆排序具有空间原址性,即任何时候都只需要常数个额外的元素空间存储临时数据。堆排序利用一种我们称为“堆”的数据结构来进行排序,因此在了解堆排序之前我们需要对堆这种数据结构有个了解,才能对堆排序是如何利用该数据结果完成对数据的排序的。
堆的定义
堆是一个数组,它可以被看成是一个近似的完全二叉树,树上的每一个节点对应数组中的一个元素。除了最底层外,该树是完全充满的,而且是从左向右填充。
堆的一些特性:
堆实际上是一棵完全二叉树,根据完全二叉树的一些特性我们可以得到父子节点之间的位置关系:对于给定的一个下标为i的节点我们可以很容计算到它的父节点下标为i/2 左孩子下标为2i,右孩子节点为2i+1(以上是在下标为1开始计算的,对于数组从0开始的计算作简单调整即可)
最大堆 最小堆
在堆排序中我们把一个无序的序列变为有序的过程使用的并不是一棵随意建立的二叉堆,对于升序和降序排列分别用到了最大堆以及最小堆。因此我们需要了解一下最大堆以及最小堆相对于普通二叉堆有什么特殊特性:
对于最大堆中 除了根节点以外的所有子节点i都要满足
A[Parent(i)]>=A[i]
这个定义表明父节点的值是不小于其任意的一个子节点的值。 因此根节点存储的是序列的最大值,并且任一子树中,该子树所包含的的所有节点的值都不大于该子树根节点的值。
对于最小堆则刚好与最大堆相反这里就不在介绍。
了解了最大堆和最小堆我们来看看堆排序的排序过程。(以下我们利用最大堆来进行排序)
堆排序的过程
- BUILD-MAX-HEAP
- MAX-HEAPIPY
- HEAP-SORT
堆排序由以上三个步骤分别是建立最大堆,最大堆维护 堆排序。
由于BUILD-MAX-HEAP实际是通过循环调用 MAX-HEAPIPY过程来完成的 因此我们先介绍MAX-HEAPIPY:
MAX-HEAPIPY是用于维护最大堆性质的重要过程。它的输入为一个数组A和一个下标i,在调用MAX-HEAPIPY的时候,我们假定根节点为Left[i]和Right[i]的子树都是最大堆,但这时A[i]可能小于其孩子节点,这样就违背了最大堆的性质。MAX-HEAPIPY通过让A[i]的值在堆中“逐级下降”从而使得以下标i为根节点的子树重新遵循最大堆的性质。
MAX-HEAPIPY伪代码如下:
- l = LEFT(i)
- r = RIGHT(i)
- if l <= A.heap-size && A[l] > A [i]
- largest = l;
- else largest = i
- if(r<= A.heap-size && A[r] > A [i]
- largest = r
- if largest != i
- exchange A[i] with A[largest]
- MAX-HEAP(A,largest)
由以上伪代码可知维护最大堆性质就是将i节点开始遍历它的左右子树,从未保证i节点是这棵子树的最大子节点,由于在前面的假定中i的子树已经是一棵满足最大堆的二叉树因此经过MAX-HEAP过程加上i这一层后继续满足最大堆的特性
BUILD-MAX-HEAP 创建最大堆就是将一个大小n=Array.length的数组A[0,1...n-1]转换为最大堆的过程。
用伪代码表示创建最大堆如下
BUILD-MAX—HEAP(A)
- A.heap-size = A.lenth
- for i = A.length/2 down to 0
- MAX-HEAPRIFY(A,i)
由以上伪代码可知创建最大堆过程就是从树的底部向树的根部遍历调用MAX-HEAPRIFY方法来完成的A.length/2 表示从树的倒数第二层开始调用MAX-HEAP 从底部往上通过循环逐渐将一棵二叉树变为最大堆。在这个过程中先是把倒数第二层的子树变换为最大堆,然后根据MAX-HEAP的假定在最底层满足最大堆后遍历倒数第二层时 这样遍历倒数第三层进行MAX-HEAP时倒数第三层也满足了最大堆的特性,因此建立最大堆是一个从底部网上逐级满足最大堆的特性,遍历完根节点后整棵二叉树就成为了一棵最大堆。
最后我们来看HEAP-SORT即堆排序就非常简单了
先给出伪代码
HEAP-SORT(A)
- BUILD-MAX-HEAP(A)
- for(A.length dowm to 1
- exchange A[i] whih A[0]
- MAX-HEAPIFY(A,0)
从以上伪代码可以看出堆排序是怎么利用堆这种数据结构进行排序以及非常巧妙的利用了最大堆的特性非常高效的就将一个序列变为了一个有序的序列。
首先是根据输入数组建立一个最大堆,根据最大堆的性质此时序列最大元素就是根节点的元素,这时把根节点移到最后一个节点以后,根节点的左右子树仍然满足最大堆的条件,这时在调用MAX-HEAPIFY就可以很快的让整棵树重新成为一棵最大堆。经过遍历和交换过程中A[0]到A[i-1]最大堆 而A[i] 到A[A.length-1]为从小到大排列的数组,遍历过程就是不断的取出处于根节点的最大元素,然后将剩余元素从未维护为最大堆的过程。
样整个堆排序的过程就介绍完了,了解了堆排序后是不是得感叹发明此算法的人是有多聪明呀,想到这么巧妙数据构,和利用该数据结构巧妙高效的完成了排序,而我等普通人不是不奢望能发明算法,但能够把一种算法理解透并能够灵活运用到实际的项目中也是不错的了。
算法复时间空间复杂度分析
- 空间复杂度
于排序过程是在原数组上对元素进行处理,仅仅只是在元素交换时开辟了常数个元素空间,因此堆排序空间复杂度很简单就是O(1)
- 时间复杂度
由上面的堆排序的伪代码可知首先建立最大堆是逐层建立根据完全二叉树的性质我们很容易得到完全二叉树的深度为lg(n) 而遍历一趟的时间复杂度为O(n),遍历趟数为lg(n)因此堆排序的时间复杂度为O(nlgn)
END Talk is cheap show me your code
void maxHeapify(int *unSortArray, int root, int bottom) {
int rootValue = unSortArray[root];
int left = root * 2 + 1;
while (left <= bottom) {
if (left < bottom) {
if (unSortArray[left] < unSortArray[left + 1]) {
left = left + 1;
}
}
if (rootValue < unSortArray[left]) {
unSortArray[root] = unSortArray[left];
root = left;
left = root * 2 + 1;
} else {
left = bottom + 1;
}
}
unSortArray[root] = rootValue;
}
void buildMaxHeap(int *unSortArray, int arraySize) {
for (int i = arraySize / 2 - 1; i >= 0; i--) {
maxHeapify(unSortArray, i, arraySize - 1);
}
}
void heapSortByMaxHeap(int *unSortArray, int arraySize) {
buildMaxHeap(unSortArray, arraySize);
for (int i = arraySize - 1; i >= 0; i--) {
int max = unSortArray[0];
unSortArray[0] = unSortArray[i];
unSortArray[i] = max;
maxHeapify(unSortArray, 0, i - 1);
}
}