纸上得来终觉浅,绝知此事要躬行. 看完了这篇博客之后 , https://blog.csdn.net/u014600626/article/details/103752297 , 自然要自己实现一下看看效果了.
堆排序,
1.把数组初始化成一个大顶堆,大顶堆: 任何一个根节点>左孩子 && 根节点>右孩子
2.将堆顶元素与最后一个元素交换,堆的数量-1,需要循环N次,
3.将剩余堆中元素重新构建,每次重构需要logN,
4.重复2,3步,直到堆中为空.
- (void)sortAction {
/*
堆排序,
1.把数组初始化成一个大顶堆,大顶堆: 任何一个根节点>左孩子 && 根节点>右孩子
2.将堆顶元素与最后一个元素交换,堆的数量-1,需要循环N次,
3.将剩余堆中元素重新构建,每次重构需要logN,
4.重复2,3步,直到堆中为空.
*/
int num = 50000;
int a[num];
for (int i = 0; i<num; i++) {
a[i] = arc4random()%(num*10);
}
// 初始化堆, 假设有一种最差的情况,初始化的时候堆顶为最小元素,
// 此时需要把最小元素放到叶子节点上,每次循环都会把最小元素下移一层,需要循环的次数为树的深度
int leftNum = num;
while (leftNum>0) {
// 把最小的元素向下移动一层,循环次数为完全二叉树的深度
[self initHeap:a endIndex:num];
leftNum /=2;
}
leftNum = num;
while (leftNum>0) {
// 交换堆顶元素与最后一个,
int currentMax = a[0];
a[0] = a[leftNum-1];
a[leftNum-1] = currentMax;
leftNum--;
// 重新构建堆
[self rebuildHeap:a endIndex:leftNum];
}
for (int i = 0; i<num; i++) {
printf("%d -> ",a[i]);
}
printf("排序完成\n");
}
// 初始化堆结构, 从第一个非叶子节点开始逐个倒序遍历, 完成堆结构搭建
- (void)initHeap:(int [])heap endIndex:(int)endIndex {
// 从第一个有叶子的节点开始
for (int i = endIndex/2-1; i>=0; i--) {
int left = 2*i+1;
int right = 2*i+2;
// 保证 i>左叶子,
if (heap[i]<heap[left]) {
int temp = heap[i];
heap[i] = heap[left];
heap[left] = temp;
}
// right节点可能是没有值的
if (right>=endIndex) {
continue;
}
// 保证i>右叶子
if (heap[i]<heap[right]) {
int temp = heap[i];
heap[i] = heap[right];
heap[right] = temp;
}
}
}
/// 重构堆, 把堆顶元素放到堆中的合适位置
- (void)rebuildHeap:(int [])heap endIndex:(int)endIndex {
// 堆在前面已经初始化完成了,但是刚才交换了堆顶元素,需要重新构建堆,
// 如何构建?堆顶元素是一个比较小的值,需要重新再堆中找合适的位置
// 从第一个根节点开始构建,然后构建左子树或者右子树,
// 比如堆顶为3, 左节点为5,右节点为7, 此时把堆顶和右节点交换, 重构右子树,左子树不需要处理, 这样时间复杂度度就是O(logN)
int nextIndex = 0; // 下一个要重构的节点
// 最后一个带叶子的节点endIndex/2-1,
for (int i = 0; i<=endIndex/2-1; i=nextIndex) {
int left = 2*i+1;
int right = 2*i+2;
// 找出左右节点中的较大者,默认左边大
nextIndex = left;
if (right<endIndex && heap[left] < heap[right]) {
nextIndex = right;
}
if (heap[i]<heap[nextIndex]) {
int temp = heap[i];
heap[i] = heap[nextIndex];
heap[nextIndex] = temp;
} else {
// 说明 已经满足条件了, 节点>左节点 && 节点>右节点, 堆顶元素找到了合适的位置, 结束循环
break;
}
}
}
时间复杂度分析, 初始化堆需要N/2 * logN次; 循环2,3步,需要N*logN次, 总体是N*logN, 所以是O(NlogN), 比起O(N^2)的复杂度,这样效率明显改进了.
堆这个结构还有一个常用的地方, 比如数组长度超级长,长到无法用内存完全读取, 需要获取前K个较大的数字, 如何做?
1.声明一个大小为K的小顶堆,
2.然后从磁盘上循环读取数组, 比较堆顶元素和读入数字的大小,
3.堆顶元素>读入数字, 继续; 堆顶元素<读入数字, 堆顶元素pop, 读入数字入堆;
4.循环2,3步直到数组遍历完, 此时堆中的元素就是前K个较大的数字.
这里没有把关于堆的代码抽象出来, 在上面代码的基础上封装成一个数据结构,可以再任意地方使用,
git地址: https://github.com/guochaoshun/ios_heap