【算法总结】堆排序

如何实现堆排序

  1. 使用 C++ STL派生容器 priority_queue 优先队列
  2. 自己写一个小根堆

两种方式各有好处,STL容器的方法用起来方便,而自己写的灵活性更大,可以自定义实现更多操作。

下面介绍一下 priority_queue 在做题的常用方法,以及手撕堆的实现。

优先队列堆排序

优先队列:队列中每个元素都有优先级,出队时按最高优先级先出。

默认是降序排列,也就是大顶堆。

声明方式:priority_queue<Type, Container, Functional>

第一个参数为优先队列里的元素类型,第二个参数为优先队列基于哪个容器实现(vector/deque),第三个参数为排序规则(greater/less)。

//默认
priority_queue<int> q;
//降序队列,与上式等同
priority_queue<int,vector<int>,less<int>> q;
//升序队列,基于deque
priority_queue<int,deque<int>,greater<int>> q;

priority_queue的头文件为 ,操作和队列基本相同:

  1. empty(): 如果队列为空,则返回真
  2. pop(): 删除对顶元素,删除第一个元素
  3. push(): 加入一个元素
  4. size(): 返回优先队列中拥有的元素个数
  5. top(): 返回优先队列对顶元素,返回优先队列中有最高优先级的元素

利用这一特性,可以快速的实现排序(堆排序)

输入n,m和n个数
输出前m个最小的数

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int main()
{
    cin >> n >> m;
    priority_queue<int,vector<int>,greater<int>> h;
    for(int i = 0; i < n; i++) {
        int input;
        cin >> input;
        h.push(input);
    }
    while(m--) {
        cout << h.top() << ' ';
        h.pop();
    }
    return 0;
}

思路简单,将要排序的序列都push进优先队列中,然后一个个出队就好了。

手写小根堆实现堆排序

堆是一个完全二叉树的数据结构。因此堆的存储可以用一维数组heap[]表示。根节点下标为1(不能为0,否则无法索引到子节点)。

完全二叉树的性质有:

  • 节点x的左子节点为2x,右子节点为2x+1
  • 节点x的父节点为x/2

接下来只要实现堆的操作就行,主要有:

  1. 插入一个数:heap[++size]=x; up(size);
  2. 获取最小值:heap[1];
  3. 删除最小值:heap[1]=heap[size--]; down(1);
  4. 删除任意值:heap[k]=heap[size--]; up(k); down(k);
  5. 修改任意值:heap[k]=x; up(k); down(k);

第4和第5要访问堆中任意值的操作比较复杂,暂时不讨论。

由上面的操作可以看到,对数组的索引都是直接访问,需要实现的只有两个函数:up()down()

down()的实现

只要判断当前节点小于其两个子节点,否则与最小的子节点交换,之后递归调用,使其满足堆的条件。
注意要检查是否存在子节点,防止越界。

设当前节点为u,则其左子节点为2u,右子节点为2u+1,可以得出如下:

void down(int u)
{
    int t = u;  //t保存最小的节点,默认父节点是最小的
    if (2 * u <= mySize && h[t] > h[2 * u]) t = 2 * u;
    if (2 * u + 1 <= mySize && h[t] > h[2 * u + 1]) t = 2 * u + 1;
    if (u != t) {  //如果父节点不是最小的,则要交换
        swap(h[u], h[t]);
        down(t);  //递归调用直到插入正确位置
    }
}
up()的实现

每个节点的父节点只有一个,因此只要判断父节点是否小于当前节点,否则进行交换,之后递归调用即可。
注意要检查是否存在父节点,防止越界。

设当前节点为u,则其父节点为u/2,可以得出如下:

void up(int u)
{
    while( u/2 && h[u/2] > h[u]) {
        swap(h[u/2], h[u]);
        u /= 2;  
    }
}
插入一个数
void myPush(int u)
{
    h[++size] = u;
    up(u);
}
删除一个数
void myPop()  //队头元素出队(移除堆顶)
{
    h[1] = h[size--];
    down(1);
}

void myRemove(int u)  //移除堆任意下标的元素
{
    h[u] = h[size--];
    up(u), down(u);
}

通过上面堆的封装,同样可以实现排序(堆排序)

输入n,m和n个数
输出前m个最小的数

#include <bits/stdc++>
using namespace std;
const int N = 100010;
int h[N], mySize;
int n, m;
void down(int u)
{
    int t = u;
    if (2 * u <= mySize && h[t] > h[2 * u]) t = 2 * u;
    if (2 * u + 1 <= mySize && h[t] > h[2 * u + 1]) t = 2 * u + 1;
    if (u != t) {
        swap(h[u], h[t]);
        down(t);
    }
}

int main()
{
    cin >> n >> m;
    mySize = n;
    for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
    for (int i = n / 2; i; i--) down(i);
    while (m--) {
        cout << h[1] << " ";
        h[1] = h[mySize--];
        down(1);
    }
    return 0;
}

上面的第4和第5个操作需要额外记录两个数组,分别存放第k个插入的数在堆数组中的索引;堆数组中的第k个数为第几个插入的数。因为堆中的数的索引值是会随着插入的数的大小而改变的,因此需要自己记录插入顺序到堆索引的映射,以及堆索引到插入顺序的映射,这样才能做到对第几个插入的数的删除和修改。

冷知识

sort()函数,可以直接对多个字符串进行排序。

同理,优先队列中的 greater 仿函数和 less 仿函数也可以对字符串排序。

两者都是默认从小到大排序,也就是有个默认参数是仿函数的greater。

这两种排序的方式最终调用的都是同个方法,因此只要 sort() 可以实现的,优先队列也可以实现。这是个人猜测,没有去剖析底层验证,就算底层不一样也通用。

因此,做题遇到需要对字符串进行排序的,可以大胆使用 sort() 或者 priority_queue<string> 来排序。

修改时间:2022.08.30

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值