堆排序及数组模拟堆
目录
步骤解释:for(int i = n / 2; i >= 0; i -- ) down(i);
数组模拟小根堆的形式
一棵完全二叉树,父节点元素值小于等于左右孩子
存储方法:从下标1开始存储,1就表示根节点,节点x的左孩子为2*x,右孩子为2*x+1,(正因为0*2=0,所以我们不用0节点作为根节点)。
基本操作
(1)插入一个元素(2)删除一个元素(3)更改一个元素的信息(4)查找集合当中的最小值。即:增删改查。这四步操作都要借助由两个更新堆的函数完成:(1)下沉down()函数(2)上浮up()函数
更新函数的解释:
(0)首先要明白我们这里的堆是最小堆,父亲节点的值小于左右孩子,根节点是最小值。堆顶是根节点,是最小值。
(1)down()函数:如果一个父亲节点(包括根节点)的值大于它的左右孩子的值,那么将这个父亲节点和值最小的孩子节点 互换,注意只能和值最小的孩子节点互换位置
例如:父亲节点的值为6,左孩子为4,右孩子为5,如果6和值最小的孩子(4)交换位置,那么父亲节点为4,左孩子为6,右孩子为5,满足条件。但如果6和值不是最小的孩子(5)交换位置,那么父亲节点为5,左孩子为4,右孩子为6,父亲节点的值还大于左孩子,那么交换就没有意义了。
(2)up()函数:如果一个孩子节点的值大于他的父亲,那么就交换它和父亲节点的位置
(3)综上,up()和down()函数都是一个递归的过程,值大了就要下沉,值小了就要上浮
完全二叉树
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同。
完全二叉树与平衡二叉树
完全二叉树不一定是平衡二叉树(AVL树),因为平衡二叉树要求树是一棵排序树,并且左右孩子的高度差值不能超过1。而完全二叉树仅仅满足了后面一条。
同样,平衡二叉树也不一定是完全二叉树,因为最后一层节点不一定是靠左边的。
堆排序(一般用于找最小值)
#include <iostream>
using namespace std;
const int N = 100010;
int a[N], n, s, m;
void down(int x)
{
int t = x; //找到最小的数
if((x * 2) <= s && a[t] > a[x * 2]) t = 2 * x;
if((x * 2 + 1) <= s && a[t] > a[x * 2 + 1]) t = 2 * x + 1;
if(t != x)
{
swap(a[t], a[x]);
down(t); //因为交换过后a[x]变成了a[t],所以下次下沉的是t
}
}
int main()
{
cin >> n >> m;
s = n;
for(int i = 1; i <= n; i ++ ) cin >> a[i];
for(int i = n / 2; i >= 0; i -- ) down(i);
for(int i = 1; i <= m; i ++ )
{
cout << a[1] << " ";
a[1] = a[s --];
down(1);
}
cout << endl;
return 0;
}
步骤解释:for(int i = n / 2; i >= 0; i -- ) down(i);
(1)i为什么从n/2开始down?
首先要明确要进行down操作时必须满足左儿子和右儿子已经是个堆。
因为如果一个父亲的左右孩子不满足堆的性质的话,我们知道down()是递归执行的,那么递归的过程中,原本正确的状态可能在递归的过程中被修改成错误的。如图:
这里我们down()根节点(5),5先和2交换位置,根节点及其左右孩子满足堆的性质,然后5和1交换位置,到叶子节点,结束。
但我们发现5和1交换位置的时候,根节点2不满足堆的性质了(1 < 2)。
开始创建堆的时候,元素是随机插入的,所以不能从根节点开始down,而是要找到满足下面三个性质的结点:
1.左右儿子满足堆的性质。
2.下标最大(因为要往上遍历)
3.不是叶结点(叶节点一定满足堆的性质)
满足上面两个条件就是倒数第二层的n/2个点(比n/2大的没有左右儿子,比n/2小的下标不最大),也就是第一个非叶子节点。
(2)推导:为什么[n/2]是第一个非叶子节点的下标(跟节点下标为1)
对于一个k层n个节点的完全平衡二叉树(含有所有的叶子节点)来说,他的第k-1层的最后一个节点的边号为n/2
k层完全二叉树的节点总数n = 2^0 + 2^1 + ... + 2^k = 2^(k)-1
前k-1层完全二叉树的节点总数m = 2^0 + 2^1 + ... + 2^(k-1) = 2^(k-1)-1
m = n / 2 - 0.5,因为 -0.5相当于下取整,所以 m =(n / 2)下取整
当然,上面的证明在完全二叉树是一棵满二叉树的情况下是显而易见的,单如果最后一层节点不是满的呢,这个时候该怎么办呢?
我们考虑一下,如果二叉树是满二叉树,那么所有的叶子结点都在最后一层。
而最后一层少一个节点,叶子结点少1个,非叶子节点数不变;
最后一层少两个节点,叶子结点还是少一个,非叶子节点数少了一个,上一层的一个非叶子节点变成了叶子结点;
最后一层少三个节点,叶子结点少了两个,非叶子节点数多了1个;
最后一层少四个节点,叶子结点少了两个,非叶子节点数少了2个;
。。。
我们发现,这是有规律的啊,最后一层少x个节点,叶子结点就会少 (x/2 上取整)个,非叶子节点数就会多(x/2下取整)个。
原本,k层满二叉树,叶子结点也就是最后一层的节点数等于2^(k-1),而非叶子结点树也就是前(k-1)层节点数等于2^(k-1)-1;
此时,如果第k层少了x个节点,叶子结点数a = 2^(k-1)-(x/2+0.5),非叶子结点数b = 2^(k-1)-1-(x/2-0.5)
a - b = 1 + (x/2下取整)-(x/2上取整)
由于上取整>=下取整
因此 0 <= a-b <= 1,
由此也就可以证明得到叶子节点数仍然等于非叶子节点数或者比非叶子节点数多1个,仍然等于总结点数➗2下取整!
实现堆的增删改查操作
#include <iostream>
using namespace std;
const int N = 100010;
int h[N], ph[N], hp[N];//h表示堆,ph表示第i个插入的数在堆中的位置,hp表示堆中第i个元素是第几个插入的
int n, m, cut;
void heap_swap(int a, int b) //不能定义一个swap名的函数,重名segmentation fault
{
swap(h[a], h[b]);
swap(hp[a], hp[b]); //交换插入顺序
swap(ph[hp[a]], ph[hp[b]]); //交换这两个数的插入指向堆中的元素
}
void up(int x)
{
while(x >> 1 && h[x] < h[x >> 1]) //只要这个节点大于父亲节点并且它不是根节点
{
heap_swap(x, x >> 1);
x >>= 1;
}
}
void down(int x)
{
int t = x;
if((2 * x) <= cut && h[2 * x] < h[t]) t = 2 * x;
if((2 * x + 1) <= cut && h[2 * x + 1] < h[t]) t = 2 * x + 1;
if(t != x)
{
heap_swap(t, x);
down(t);
}
}
int main()
{
cin >> n;
while(n -- )
{
string s;
cin >> s;
int k, x;
//I x,插入一个数 x;
if(s == "I")
{
int x;
cin >> x;
cut ++ ; //堆的下标
m ++ ; //第m个插入的
ph[m] = cut; //第m个插入的数在堆中的位置
hp[cut] = m; //堆中第cut个元素是第几个插入的
h[cut] = x;
up(cut);
}
//PM,输出当前集合中的最小值;
else if(s == "PM") cout << h[1] << endl;
//DM,删除当前集合中的最小值(数据保证此时的最小值唯一)
else if(s == "DM")
{
heap_swap(1, cut);
cut -- ;
down(1);
}
//D k,删除第 k 个插入的数
else if(s == "D")
{
cin >> k;
k = ph[k]; //获得第k个插入的数在堆当中的下标
heap_swap(k, cut);
cut -- ;
down(k); up(k);
}
//C k x,修改第 k 个插入的数,将其变为 x;
else
{
cin >> k >> x;
k = ph[k]; //获得第k个插入的数在堆当中的下标
h[k] = x;
up(k); down(k); //更新堆
}
}
return 0;
}
关于hp和ph数组的解释:
- h表示heap堆,p表示point指针
- 因为题目中需要我们找到第k个插入的数,所以设置一个由插入次序映射到堆下标的数组ph,使得可以由一个数插入的次序索引到它在堆中的下标
- 因为在交换两个值的时候,还要交换他们的插入顺序,所以设置一个由堆下标映射到插入次序的数组,使得可以由堆的下标索引到该数的插入次序