更完堆,再更一期堆排序
利用容器实现堆排序
在C++等高级语言中,基本上都有堆或者优先队列等容器,借助这些容器很容易实现堆排序。
将数组里面的元素先插入到容器中,建好堆,
接着将容器中的元素,按照升序或降序重新放回数组中
即可实现好排序
#include <iostream>
#include <queue>
using namespace std;
int main()
{
int a[] = { 2,1,4,2,5,5,1,2,3,5,3,2,3,22,31,4,213,12 };
//默认建大堆降序
//priority_queue<int> pq;
//建小堆,可升序
priority_queue<int, vector<int>, greater<int>> pq;
for (auto e : a)
{
pq.push(e);
}
int i = 0;
while (!pq.empty())
{
a[i++] = pq.top();
pq.pop();
}
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
return 0;
}
利用容器实现堆排序的缺点
1、空间复杂度较高,O(N)
2、万一面试的时候面试官让你手写堆排序,不能使用容器,就很尴尬了。
利用数组原地实现堆排序(手写堆排序)
首先,我们需要模拟建堆,将数组建成一个堆。
这里有两种方法,向上建堆和向下建堆。
向上建堆
我们向上调整建堆,就需要调整之前本身就是一个堆,不破坏堆的性质,
那么我们就可以直接从第二个元素开始,每一个元素都向上调整,一直去维护这个堆。
(第一个元素不用调整,本身就是一个堆)
向上调整算法在上一篇文章《数据结构:堆》中有所提及,这里就不详细介绍了
传送门:数据结构:堆
向上建堆代码:
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
{
swap(a[child], a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else break;
}
}
int main()
{
int a[] = { 2,1,4,2,5,5,1,2,3,5,3,2,3,22,31,4,213,12 };
int n = sizeof(a) / sizeof(a[0]);
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
return 0;
}
向下建堆
我们向下建堆,要求左右子树都是堆,才能完成向下调整建堆。
那么我们从最后一个非叶子节点开始向下调整建堆,一直向前,直到根节点。
(叶子节点没有子树,不用向下调整建堆)
向下调整算法在上一篇文章《数据结构:堆》中有所提及,这里就不详细介绍了
传送门:数据结构:堆
向下建堆代码:
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
swap(a[child], a[parent]);
parent = child;
child = child * 2 + 1;
}
else break;
}
}
int main()
{
int a[] = { 2,1,4,2,5,5,1,2,3,5,3,2,3,22,31,4,213,12 };
int n = sizeof(a) / sizeof(a[0]);
for (int i = (n - 1 -1) /2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
return 0;
}
理性分析一下向上建堆和向下建堆的时间复杂度
算时间复杂度,要考虑最坏情况,这里以满二叉树为例
假设共h层高度
向上建堆
从第二层开始,都需要向上移动柜(k-1)层
每层的个数是2k-1 ,每一层需要移动的次数就是2k-1 * (k-1)
把每一层相加求和即可,
最后,时间复杂度是关于n的函数,最后把h换成n即可。
以下为草稿借鉴
向下建堆
从倒数第二层开始,每一层向下调整 h - k层
每层的个数是2k-1 ,每一层需要移动的次数就是2k-1 * (h - k)
把每一层相加求和即可,
最后,时间复杂度是关于n的函数,最后把h换成n即可。
向下建堆草稿借鉴:
综上,向上建堆的时间复杂度是O(N*logN),向下建堆的时间复杂度是O(N),差距十分显著。
所以,我们采用向下建堆算法。
感性分析一下为啥向下调整算法效率更高
我们可以发现,
向上调整建堆,越下面调整的次数越多,但是越下面每一层的个数却越多,这就是多 * 多
向下调整建堆,越下面调整的次数越少,但是越下面每一层的个数却越少,这就是多 * 少
可以联系田忌赛马去理解。
升序建大堆,降序建小堆
看到标题,我相信很多小伙伴都不理解,为啥升序建大堆,不应该升序建小堆吗?
且听我慢慢分析
注意,我们需要不借助容器,也就是在原地进行排序。
如果升序建小堆,那么我们找到了最小的元素,怎么找次小的元素呢,怎么找再次小的元素呢?
比如一个小堆
1 2 10 112 111 3 ……
这样会破坏堆的性质,是不可取的
如果升序建大堆有什么好处呢?
假设一个大堆:
213 12 31 4 5 5 22 2 3 5 3 2 3 4 1 2 2 1
我们有最大的元素,
将其与最后一个元素交换,size–
将此时的第一个元素进行向下调整算法,就又可以维护堆的性质。
依此循环,就可以排好序了。
降序建小堆同理,这里就不分析了。
堆排序代码(升序为例)
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
swap(a[child], a[parent]);
parent = child;
child = child * 2 + 1;
}
else break;
}
}
int main()
{
int a[] = { 2,1,4,2,5,5,1,2,3,5,3,2,3,22,31,4,213,12 };
int n = sizeof(a) / sizeof(a[0]);
for (int i = (n - 1 -1) /2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end)
{
swap(a[end], a[0]);
--end;
AdjustDown(a, end + 1, 0);
}
return 0;
}
祝大家中秋节快乐!!!