- [ 转载 ]https://www.jianshu.com/p/c5f4255b8148
1.概念
堆,是一种十分基础的数据结构,也是优先队列实现的最好方法,其本身的实现也挺简单的。废话不多说,我们直接来看堆的一些描述和特性。
1.1拓展概念
- 二叉树
首先,堆其实就是一颗完全二叉树,在描述一颗二叉树的时候,我们完全可以使用类似链表的方式,一个数据域来储存数据,两个指针来指向其左右节点。但这样储存会导致空间的浪费,所以可以采用数组来储存二叉树。
堆正是一种特殊的二叉树,完全二叉树,这样子的设计,可以保证在数组中,空间不会被浪费,因此他的储存效率还是蛮高的。
- 完全二叉树
还是刚刚那棵树,原汁原味,不过这回我把标号从1开始了,这样储存的时候计算下标会更方便。
可以看出来,这棵树如果一层层,从左到右标上号,是连续的,这正符合了数组这种连续的数据结构的特点,因此非常适合用数组(本质上就是连续的内存空间)来进行实现。
不难发现,任何一个节点的父节点的标号p 等于 该节点标号i除以2,并向下取整,即p = [i / 2]。
同样的,一个节点的两个子节点i1,i2分别等于该节点p的下标 乘以2 和 乘2加1。即i1 = p * 2,i2 = p * 2 + 1。
因此,我们在储存的时候只要注意下,数组下标从1开始计算。
2.堆的描述
堆,正如他的字面意思,是一层层堆上去的,每一层之间都有一些特殊的关系。
在这其中就分为了两种堆,一种叫做大顶堆,另一种叫做小顶堆,其区分的方式便是父节点和其子节点之间的关系不同。
- 小顶堆
字面意思,也就是在最上面的顶点(root节点)是整个堆最小的,往下走,每一个上面的节点都比下面的小。每个父节点都比子节点小。
- 大顶堆
同小顶堆的描述,也就是在最上面的顶点(root节点)是整个堆最大的,往下走,每一个上面的节点都比下面的大,每个父节点都比子节点大。
3.实现
3.1.创建一个数组:
int a[1000];
int n = 0; // 代表当前堆中元素数量
3.2.插入
void push(int num)
{
a[++ n] = num;
}
但这样也只仅仅是插入元素至数组,那么我们怎么来维护一个堆呢?
我们这边以大顶堆作为例子来进行演示。
3.3维护堆
3.3.1插入
很容易理解,如果一个堆里没有元素或者只有一个元素,那他就是符合堆的描述的。
那么,我们在插入第二个元素的时候,就有可能出现子节点比父节点大的情况,这时候我们就需要进行交换。而每个这样的上下交换,便叫做shift-up。
上图描绘的便是一个堆简单的维护过程。在大顶堆中,只要发现新插入的元素比其父节点来得大,那就进行交换,然后一直重复这个操作到root节点。很明显,插入一次的时间复杂度是O(logn)。
接下来写一下代码,先实现交换函数;
void swap(int &a, int &b)
{
if (a == b) return; // 防止交换相同元素导致都=0
a ^= b;
b ^= a;
a ^= b;
}
然后就是我们的shift-up的代码:
- 递归实现
void _up(int i)
{
int temp = i / 2;
if(temp == 0) return;
if(a[temp] < a[i]) {
swap(a[temp], a[i]);
_up(temp);
}
}
2.循环实现
void up(int i)
{
int temp = i / 2;
if(temp == 0) return;
while(temp != 0 && a[temp] < a[i]) {
swap(a[temp], a[i]);
i = temp;
temp /= 2;
}
}
代码的意思很直白,先计算一个节点的父节点的下标p,然后判断a[i]与a[p]之间的大小关系,不对就交换,然后移动到父节点,继续这个过程。
因此堆的push方法就是这样的了:
void push(int num)
{
a[++ n] = num;
up(n);
}
3.3.2 删除
根据堆的设计,我们一般删除的节点就是root节点,也就是对应数组的a[1]。
删除的方式,其实也挺简单,交换a[1]和a[n],也就是交换root节点和最后一个节点,然后在从上到下进行一遍维护,也就是shift-down操作,恰好和之前的shift-up操作相反。
图中,红色的代表删除的节点,橙色的代表位置不正确的节点,最后一直进行shift-down操作,使所有节点归位,复杂度为O(logn)。
同时,最后这个堆中所有的元素是呈一定的顺序的,将它以普通的数组展现出来的时候,它便是升序排序排好的:[1, 2, 3, 4, 5],因此有一种排序就叫做堆排序。
同样的,我还是给出两个版本。
- 递归实现:
void _down(int i)
{
int temp = i * 2;
if(temp > n) return;
if(temp + 1 <= n && a[temp + 1] > a[temp]) temp++;
if(a[i] < a[temp]) {
swap(a[temp], a[i]);
_down(temp);
}
}
- 循环实现
void down(int i)
{
int k = i * 2;
if (k + 1 <= n && a[k + 1] > a[k]) k ++;
while (k <= n && a[i] < a[k])
{
swap(a[i], a[k]);
i = k;
k *= 2;
if (k + 1 <= n && a[k + 1] > a[k]) k ++;
}
}
上述代码的意思就是首先计算他的儿子节点k的下标,然后比较左右两个儿子的大小,选择大的那个,之后再与之交换,然后一直进行这个过程,直到符合为止。
有了shift-down函数做基础,那么删除函数,也就变得十分简单,只要交换头尾元素即可。
void pop()
{
if (n > 0) // n是元素个数,要注意>0才能pop
{
swap(a[1], a[n --]); // 交换头尾元素
down(1); // 开始shift-down
}
}
3.4. 建堆
建堆,是将一个不符合堆的描述的数组 转化成 一个符合堆的描述的一个数组。
不要把这个过程想得太复杂,其实很简单,仅仅只需要用到上文中的shift-down函数即可。
还是以上文中的那棵树为例,我们假设需要构建一个大顶堆,而输入的数组为:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12](如图所示)。
那么,我们只需要从最后一个拥有子节点的父节点开始递减,直到root节点。
很显然,图中最后一个父节点便是6号节点,所以说我们只需for (int i = 6;i >= 1;i --)一直递减即可。
由上文的规律可知:
任何一个节点的父节点的标号p 等于 该节点标号i除以2,并向下取整,即p = [i / 2]。
所以,只要根据最后一个节点的下标,即可求出最后一个父节点的下标。即P = [n/2]。
建堆代码:
void heapify()
{
for (int i = n / 2;i >= 1;i --)
{
down(i);
}
}
值得注意的是,这种方式的时间复杂度是O(n);
完整代码:
//
// main.cpp
// Heap
//
// Created by JackLee on 2020/2/20.
// Copyright © 2020 JackLee. All rights reserved.
//
#include <stdio.h>
//------------------------------------------------------------------------
//------------------------------打印堆函数BEGIN-----------------------------
#include <math.h>
#include <vector>
using namespace std;
struct ps {
int dps;
int length;
int type;
};
void print_binTree(int *root, int n, int index, int d, int lr, vector<ps> dps) // 打印堆函数,用于直观的显示堆中元素
{
if (index > n) return;
ps p = {d, (int) log10(root[index]) + 1, index * 2 + 1 <= n && lr == 0};
if (dps.size() <= d) dps.push_back(p);
else dps[d] = p;
print_binTree(root, n, index * 2 + 1, d + 1, 1, dps);
for (vector<ps>::iterator i = dps.begin();i != dps.end() - 1;i ++)
{
if (i -> type && i -> dps != 0) printf("|");
else printf(".");
for (int j = 0;j < i -> length + ((i -> dps) != 0) * 2;j ++)
{
printf(".");
}
}
if (d != 0) printf("|-");
printf("%d",root[index]);
if (index * 2 <= n || index * 2 + 1 <= n) printf("-|");
printf("\n");
dps[d].type = index * 2 <= n && lr;
print_binTree(root, n, index * 2, d + 1, 0, dps);
}
//------------------------------打印堆函数END-------------------------------
//------------------------------------------------------------------------
int a[1000]; // 从1开始
int n = 0;
void swap(int &a, int &b)
{
if (a == b) return; // 防止交换相同元素导致都=0
a ^= b;
b ^= a;
a ^= b;
}
void _up(int i)
{
int p = i / 2;
if (p == 0) return;
if (a[i] > a[p]) {
swap(a[i], a[p]);
_up(p);
}
}
void up(int i)
{
int p = i / 2;
while (p != 0 && a[i] > a[p])
{
swap(a[i], a[p]);
i = p;
p /= 2;
}
}
void _down(int i)
{
int k = i * 2;
if (k > n) return;
if (k + 1 <= n && a[k + 1] > a[k]) k ++;
if (a[i] < a[k])
{
swap(a[i], a[k]);
_down(k);
}
}
void down(int i)
{
int k = i * 2;
if (k + 1 <= n && a[k + 1] > a[k]) k ++;
while (k <= n && a[i] < a[k])
{
swap(a[i], a[k]);
i = k;
k *= 2;
if (k + 1 <= n && a[k + 1] > a[k]) k ++;
}
}
void push(int num)
{
a[++ n] = num;
up(n);
}
void pop()
{
if (n > 0)
{
swap(a[1], a[n --]);
down(1);
}
}
void heapify()
{
for (int i = n / 2;i >= 1;i --)
{
down(i);
}
}
int main(int argc, const char * argv[]) {
// insert code here...
int t;
printf("The heap array's length: ");
scanf("%d",&t);
int in;
for (int i = 1;i <= t;i ++) scanf("%d",a + i);
n = t;
heapify();
printf("Heapified\n");
print_binTree(a, n, 1, 0, 1, vector<ps>());
printf("\n");
while (1)
{
printf("Operation? (0 - push, 1 - pop, 2 - sort and quit, else - quit): ");
scanf("%d",&in);
if (!in)
{
printf("Your number: ");
scanf("%d",&in);
push(in);
print_binTree(a, n, 1, 0, 1, vector<ps>());
printf("\n");
} else if (in == 1)
{
printf("Poped: %d\n",a[1]);
pop();
print_binTree(a, n, 1, 0, 1, vector<ps>());
printf("\n");
} else if (in == 2)
{
t = n;
printf("The final array: [");
while (n) pop();
for (int i = 1;i <= t;i ++)
{
if (i != 1) printf(", ");
printf("%d",a[i]);
}
printf("]\n");
break;
} else break;
}
return 0;
}