目录
一、神马是堆?
1.堆
2.大根堆
二、堆的数据结构如何表示?
1.基本的结构
2.堆中节点的下标表示方法
三、堆排序的前置问题
1.heapInsert函数的设计
1.1我们先来看代码:
1.2代码分析:
2.heapify函数的设计
2.1话不多说上代码!
2.2代码分析:
三、正片开始,堆排序!
1.main函数:
1.代码总览:
四、堆排序扩展题目
1.题目内容:
2.题目解析:
3.代码部分:
4.代码解读:
5.运行结果:
本博客为B站大佬算法讲解的学习笔记https://www.bilibili.com/video/BV13g41157hK?p=5&vd_source=303cbbf37245b25cfd1ed1c1cf374d06
堆,顾名思义,就是一堆东西。想到堆,我们脑海中可能会浮现出堆在一起的某些物体的形状。
实际上,堆,就是这么一个东西。
如果你知道到二叉树这个概念的话,我可以告诉你,堆其实就是一个满二叉树。
想要弄懂堆排序,就一定要知道大根堆这么一个东西。
我们可以看到,上面这一堆节点,所构成的图形就是一个满二叉树(每个父节点有两个子节点,最后一层添加新元素一定满足先加在最后一层所有父节点左边的原则)
有关二叉树的概念,相信大家都知道,这里就不过多赘述了,就是一点点概念性的东西,比较简单,这里主要来介绍大根堆的概念。
大根堆,如上图所示,就是一个标准的大根堆:它满足每个父节点的值都大于他的子节点。意思就是,如果所有的父亲一定比他们的儿子大,那么这个大家庭就叫做大根堆,很好理解。
堆,可以用结构体表示,也可以用类的方法表示。这里因为要进行堆排序,我们使用数组的数据结构来抽象出一个堆。
我们可以这么做,准备一个数组,将堆中的元素,按照从上到下,从左到右的顺序依次将每个元素填入数组中(图中红色数组即是每个元素在数组中的下标表示)
我们先来观察一下这个堆的下标,看看你是否能找到其中每个节点与它的左右节点下标的关系。
想必这点小规律一定逃不了你的火眼金睛,
所有的节点都满足这样的关系:
父节点的下标:i
左子节点:2 * i + 1
右子节点:2 * i + 2
想要弄懂堆排序,光是知道堆是什么还不够,还要学会对应的操作,实际上,任何一个数据结构的学习都要经历这样一个过程。
假设你现在有一个整整齐齐的大根堆,这时用户突然抽风了,跑过来告诉你:我要把 55(下标为4的节点) 改成 200,并且改完你还得给我把堆弄成大根堆的样子!!
请你思考一下,要怎样才能完成这个操作呢?
很显然,你不能直接直接把 200 和 190 交换。那该要怎么做呢?
其实很简单,我们可以这样想,就像现实生活中一样,你想象一下你拿着200这个圆形,每次往它的父节点看,如果父节点没你手上的200大,那说明什么?
它太 low 了,不配做你爸,那你就做它的父节点吧,然后你把他们的位置交换,你重新站在了[1]这个索引位,然后抬头往上看,你又发现190也不太够格,你把它拉了下来,进行交换,站在了第一个位置,这时你发现你头上已经没有元素了,你把200放下来,跑去和用户开心的交代你的成果......
emmm,所以这个过程设计成代码是个什么思路?
void heapInsert(vector& heap, int index) {
int father = (index - 1) >> 1;
while (heap[index] > heap[father])
{
swap(heap[index], heap[father]);
index = father;
father = (index - 1) >> 1;
}
}//使索引位置元素网上到达一个比他大的元素下方
1.找到200所对应的索引位置。
2.找到父节点的坐标 (i - 1) / 2 这里要注意,如果 i 是 0 ,-(1/2)返回的还是 0,待会循环结束条件会用到!
3.如果 i 大小比father父节点要大,交换,把father给i,重新计算father,
否则,退出循环。
需要注意的是,当 i 到零位置[0]的时候,father的索引也会被计算为 [0] ,这时会将自己和自己的值比较,因为相等所以并不比father大,所以依旧可以退出循环。
承接上文,你找到了用户。但是,用户好像不太满意,没错,他又抽风了。他又给你提了一个新的要求:我要把最大的那个 200 再改回 55!你很是无语,但是面对“上帝”的要求,你很无奈,只好照做。
和上次有些许不同,这次你手上拿的 55 每次就都要面临两个元素了,思考一下,要怎么设计呢?
有了上次的经验,你很快就知道,你要把55和下面两个元素比较,把大的往上移动,55小嘛,那就往下移动,实际上就是找到子节点最大的那一个交换就完事了,然后重复这个过程,直到下一个预定的左子节点超出数组的长度(左节点都超了,右节点在它右边,必然也不存在)。
void heapify(vector& heap, int index, int heapSize) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int biggest;
while (left < heapSize) {
biggest = (right < heapSize && heap[right] > heap[left]) ? right : left;
biggest = (heap[index] > heap[biggest]) ? index : biggest;
if (biggest == index) break;
swap(heap[index], heap[biggest]);
index = biggest;
right = index * 2 + 2;
left = index * 2 + 1;
}
}//从该索引位置往下把大的网上传直到下面没有可以向上移动的元素
这里需要注意的是:
1.biggest存放上下两个节点之间最大的数,如果最大的位置就是你手上拿的这个数,那就break,不要再往下走了,完成任务。
2.还有就是,我们每次只要判断到右节点存在,并且右子节点大,那么就拿右节点和手上的节点比较,如果它更大让他往上走,交换。
对于其他的情况,一定是左子节点和你手上拿的这个节点比较!!
有了以上的学习,此时的你已经凑齐了堆排序的两大件,heapInsert 和 heapify 两大函数。
总结一下,我们的堆排序,实际上就用到了两个函数,一个向上交换,一个向下交换。
那么,即使我们给定一个乱序的数组,我们可以假定数组的大小是0,也就是从第一个元素开始,每个元素都进行一遍heapInsert的操作,这样一趟下来,每个元素都向上走到它该去的位置,最后堆就变成了一个大根堆。
然后我们每次将最大的元素,也就是数组的第一个元素和最后一个元素交换,再把认为规定的heapSize缩小,相当于把最大的排序好了,然后从数组中“丢出去暂时不管”。
这时,一个可能比较小的数来到了第一个位置,我们heapify将它往下移到该去的地方。
重复该操作,就得到了一个升序的数组。
1.main函数:
int main() { srand(time(0)); vector
heap; initHeap(heap, 50); cout << "Unsorted heap:"; printHeap(heap); cout << endl << "The sorted heap:"; heapSort(heap); printHeap(heap); // system("pause"); VsCode中加这个防止输出窗口一闪而过 }
1.代码总览:
#include
#include #include //#include 注意在有些编辑器中 rand()需要导入这个库 using namespace std; int randint(int a, int b); void heapSort(vector & heap); void heapify(vector & heap, int index, int heapSize); void heapInsert(vector & heap, int index); void initHeap(vector & heap, int size); void printHeap(vector & heap); int main() { srand(time(0)); vector heap; initHeap(heap, 50); cout << "Unsorted heap:"; printHeap(heap); cout << endl << "The sorted heap:"; heapSort(heap); printHeap(heap); system("pause"); } void heapInsert(vector & heap, int index) { int father = (index - 1) >> 1; while (heap[index] > heap[father]) { swap(heap[index], heap[father]); index = father; father = (index - 1) >> 1; } }//使索引位置元素网上到达一个比他大的元素下方 void heapify(vector & heap, int index, int heapSize) { int left = index * 2 + 1; int right = index * 2 + 2; int biggest; while (left < heapSize) { biggest = (right < heapSize && heap[right] > heap[left]) ? right : left; biggest = (heap[index] > heap[biggest]) ? index : biggest; if (biggest == index) break; swap(heap[index], heap[biggest]); index = biggest; right = index * 2 + 2; left = index * 2 + 1; } }//从该索引位置往下把大的网上传直到下面没有可以向上移动的元素 void heapSort(vector & heap) { // for (int i = 0; i < heap.size(); i++) heapInsert(heap, i); //O(logN) //第一种写法 for (int i = heap.size() - 1; i > -1; i--) heapify(heap, i, heap.size()); //O(logN) //第二种写法(第一步更快,但是复杂度基本一样) int heapSize = heap.size(); while (heapSize) { swap(heap[0], heap[heapSize - 1]); heapSize--; heapify(heap, 0, heapSize); } }//堆排序主函数 void initHeap(vector & heap, int size) { for (int i = 0; i < size; i++) heap.push_back(randint(1, 100)); }//随机创建堆 void printHeap(vector & heap) { vector ::iterator it = heap.begin(); for (; it < heap.end(); it++) cout << *it << ' '; cout << endl; }//打印堆(数组) int randint(int a, int b) { return rand() % (b - a + 1) + a; }//生成[a, b]之间的随机数
排序结果:
补充,这里的k是一个比较小的数,并且要求复杂度比较低。
思路:我们定义一个小根堆,先将数组前7个元素压入优先级队列中(小根堆)。定义 i 从 k+1 位置开始到最后一位结束,然后每次弹出优先级队列中第一个元素(最小值)并
放入临时数组, 再压入 i 位置的元素,i 后移。最后当 i 越界后,将剩余元素依次压入临时数
组即可。
#include
#include
#include
using namespace std;
template
T kMin(T a, T b)
{
return (a < b) ? a : b;
}
void heapSort(vector &arr, int k);
int main()
{
vector arr = {2, 3, 5, 3, 1};
heapSort(arr, arr.size());
for (auto i : arr)
cout << i << ' ';
cout << endl;
system("pause");
}
void heapSort(vector &arr, int k)
{
vector res;
priority_queue, greater> que; // define a large root heap.
for (int i = 0; i <= (kMin(arr.size() - 1, k)); i++) // prevent crossing the boundary.
que.push(arr[i]); // press in the last six numbers.
for (int i = k + 1; i < arr.size(); i++)
{
res.push_back(que.top()); // add the smallest number to the result array.
que.pop(); // delete the smallest number.
que.push(arr[i]);
}
while (!que.empty())
{
res.push_back(que.top());
que.pop();
} // process the last numbers in the priority queue.
arr = res; // cover the array which is needed to processed.
}
这里我们用到了C++中的优先级队列(实际上就是一个根堆),
1.priority_queue
, greater 定义了一个小根堆,参数一表示堆元素的数据类型,参数二表示用什么样的容器来承载这个堆,参数三表示堆元素向下递增。> que; 2.que.pop()操作弹出元素,
3.que.top()操作取堆顶元素,
4.que.push()操作压入元素,
5.que.empty()判断是优先队列否为空。
需要注意的是:我们单独定义了一个 kMin() 函数来取 k 和数组大小的最小值,来防止 k 大于数组边界从而防止越界。