堆排序
堆分为大根堆、小根堆,其就是一棵完全二叉树。
一、二叉树
1.1 定义
一棵深度为k且有2k-1个结点的二叉树称为满二叉树。满二叉树每一层的结点个数都达到了最大值, 即满二叉树的第i层上有2i-1个结点 (i≥1) 。
如果对满二叉树的结点进行编号(从1开始), 约定编号从根结点起, 自上而下, 自左而右,则深度为k的, 有n个结点的二叉树, 当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时, 称之为完全二叉树。
从满二叉树和完全二叉树的定义可以看出, 满二叉树是完全二叉树的特殊形态, 即如果一棵二叉树是满二叉树, 则它必定是完全二叉树。
完全二叉树除了最后一个分支外,每个分支都必有两个孩子,最后一个可能有一个或两个孩子。
1.2 性质
如果对一棵有n个结点的完全二叉树的结点按层序编号, 则对任一结点i (1≤i≤n) 有:(注:[ ]表示向下取整,舍弃)
- 如果i=1, 则结点i是二叉树的根, 无双亲;如果i>1, 则其双亲parent (i) 是结点[i/2]。
- 如果2i>n, 则结点i无左孩子, 否则其左孩子lchild (i) 是结点2i。
- 如果2i+1>n, 则结点i无右孩子, 否则其右孩子rchild (i) 是结点2i+1。
1.3 特点
完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。
二、堆排序实现
用一个一维数组模拟完全二叉树,数的结点的编号对应数组的下标,因此,数组从下标1开始存储。
2.1 小根堆
小根堆的性质:每个子节点的值都要大于它的父结点的值, 但左右孩子大小之间没限制。因此,小根堆中,父结点一定小于它的孩子结点,根据递归实现,根节点一定是一棵完全二叉树中最小的值。
如何手写一个堆?
首先需要两个函数:down(x)
和 up(x)
down函数表示将结点编号为x的结点向下调整;up函数表示将结点编号为x的结点向上调整。
在这两个函数的基础上可以实现以下功能:
- 插入一个数:heap[++sz]=x; up(sz); 尾部插入
- 求集合中的最小值:heap[1]
- 删除最小值:heap[1]=heap[sz]; sz– – ; down(1); 尾部代替头部
- 删除任一个结点为k的元素:heap[k]=heap[sz]; sz– – ; down(k); up(k);
- 修改任一个结点为k的元素:heap[k]=x; down(k); up(k);
删除最小值
【AcWing 838. 堆排序】
输入一个长度为n的整数数列,从小到大输出前m小的数。
#include <iostream>
using namespace std;
const int N=1e5+10;
int heap[N],sz;//用一维数组heap模拟完全二叉树,sz表示用到的最大结点编号,即树上结点个数
void down(int x)
{
//结点为x的 左孩子 2x 右孩子 2x+1
//小根堆:需要调整时满足条件:父结点大于子节点
int t=x;
if (x*2<=sz && heap[t]>heap[x*2]) t=x*2;//假设先和左孩子交换
if (x*2+1<=sz && heap[t]>heap[x*2+1]) t=x*2+1; //判断是否还需要和右孩子交换
if (x!=t){
swap(heap[t],heap[x]);
down(t);
}
}
void up(int x)
{
//结点为x(左孩子:偶数,右孩子:奇数)的父结点 : x/2
//向上调整时,不必管兄弟节点,只用判断父结点
while (x/2 && heap[x]<heap[x/2]) {
swap(heap[x],heap[x/2]);
x/=2;
}
}
int main() {
int n,m;
cin>>n>>m;
// 初始化小根堆
for (int i = 1; i <= n; ++i) cin>>heap[i]; //从下标1开始存储
sz=n;
for (int i = n/2; i >= 1; --i) down(i);
//最后一个叶子结点编号为n,其父结点为n/2,所以最后一个非叶子结点编号为n/2,从这儿开始向下调整,时间复杂度O(n)
//输出前m小的数,每输出一个数,调整一次小根堆
for (int i = 0; i < m; ++i) {
//循环m次即可
cout<<heap[1]<<" ";
//删除最小的数
heap[1]=heap[sz];
sz--;
down(1);
}
return 0;
}
删除、修改结点为k的结点
1. 删除、修改下标为k的结点
void del_k(int k)
{
heap[k]=heap[sz--];
down(k);
up(k);
}
void rep_k(int k,int x)
{
heap[k]=x;
down(k);
up(k);
}
2. 删除、修改第k次插入的结点
【AcWing 839. 模拟堆】
维护一个集合,初始时集合为空,支持如下几种操作:
- “I x”,插入一个数x;
- “PM”,输出当前集合中的最小值;
- “DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);
- “D k”,删除第k个插入的数;
- “C k x”,修改第k个插入的数,将其变为x;
#include <iostream>
using namespace std;
const int N=1e5+10;
int heap[N],sz;//sz为0时,代表树为null
int getp[N],getb[N],num; //num记录第几次插入
void swap_heap(int i,int j) //交换的结点的下标i j
{
swap(heap[i],heap[j]);
int ki=getb[i],kj=getb[j];
swap(getp[ki],getp[kj]);
swap(getb[i],getb[j]);
}
void down(int x)
{
//向下调整,看左右孩子
int t=x;
if (x*2<=sz && heap[t]>heap[x*2]) t=x*2;
if (x*2+1<=sz && heap[t]>heap[x*2+1]) t=x*2+1;
if (x!=t) {
swap_heap(x,t);
down(t);
}
}
void up(int x)
{
//向上调整,只看父结点
while (x/2 && heap[x]<heap[x/2]){
swap_heap(x/2,x);
x/=2;
}
}
void insert(int x)
{
getp[++num]=++sz;
getb[sz]=num;
heap[sz]=x;
up(sz);
}
void del_i(int i)
{
//删除下标为i结点,等价于和最后一个结点出互换并调整该结点
swap_heap(i,sz--); //删除根节点 i=1 也算在内
down(i);
up(i);
}
void rep_i(int i,int x)
{
//替换下标为i结点的值为x
heap[i]=x;
down(i);
up(i);
}
int main()
{
int n;
cin>>n;
string op;
int k,x;
while (n--){
cin>>op;
if (op=="I") {
cin>>x; insert(x);
} else if (op=="PM") {
cout<<heap[1]<<endl;
} else if (op=="DM") {
del_i(1);
} else if (op=="D") {
cin>>k; k=getp[k];
del_i(k);
} else if (op=="C"){
cin>>k>>x; k=getp[k];
rep_i(k,x);
}
}
return 0;
}