堆
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储
,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段
堆的定义
如果有一个关键码的集合K = { K0, K1 , K2 ,…, Kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K2*i+1且Ki<=K2*i+2(Ki>=K2*i+1且Ki>=K2*i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质
- 堆中某个节点的值总是不大于或不小于其父节点的值
- 堆总是一棵完全二叉树
堆的实现
向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆
int array[] = {88,14,43,67,124,54,45};
向下调整算法有一个前提:左右子树必须是一个堆,才能调整
- 若想将其调整为大堆,那么根结点的左右子树必须都为大堆
- 若想将其调整为小堆,那么根结点的左右子树必须都为小堆
向下调整算法的实现
- 从根节点开始,先找到根节点的左右孩子中小(大)的一个孩子
- 让小(大)的孩子和父亲节点进行比较
- 如果孩子的节点值小于(大于)父亲节点,就把孩子节点和父亲节点进行交换,并将原来小(大)的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止
- 若小(大)的孩子比父亲大(小),则不需处理了,调整完成,整个树已经是小(大)堆了
void AdJistDown(int*a, int a_size, int start){
int parent = start;
int child = parent * 2 + 1;
while(child < a_size){
//找到根节点的左右孩子中小的一个孩子
if(child + 1 < a_size && a[child + 1] < a[child]){
child++;
}
//让小的孩子和父亲节点进行比较
if(a[child] < a[parent]){
std::swap(a[child], a[parent]);
parent = child;
child = parent * 2 + 1;
}
//调整完成,整个树已经是小堆了
else{
break;
}
}
}
使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1
次(h为树的高度)。而h = log2(N+1)
(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN)
堆的创建
现在给出一个随机的数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆
int array[] = {17,41,66,51,2,39,46,24,47,49,14,97,9};
-
首选,使用向下调整算法的前提是要调整节点的子树必须是小(大)堆
-
但是这个数组是一个随机的数组,没法使用向下调整算法
-
但是叶子节点的左右子树都为空,可以把叶子节点看成左右子树都是堆的节点,而且叶子节点本身就可以看作是一个建好的堆,也就是说,叶子节点的父节点的左右子树都是叶子节点,符合向下调整算法的要求
-
所以我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆
for(int i = (n - 1 - 1) / 2; i >= 0; --i){
AdJustDown(array, 8, i);
}
建堆的时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)
需要移动节点总的交换步数为:
T
(
n
)
=
2
0
×
(
h
−
1
)
+
2
1
×
(
h
−
2
)
+
2
2
×
(
h
−
3
)
+
2
3
×
(
h
−
4
)
+
.
.
.
+
2
h
−
3
×
2
+
2
h
−
2
×
1
T(n)=2^0×(h−1)+2^1×(h−2)+2^2×(h−3)+2^3×(h−4)+...+2^{h−3}×2+2^{h−2}×1
T(n)=20×(h−1)+21×(h−2)+22×(h−3)+23×(h−4)+...+2h−3×2+2h−2×1
两边同时乘2得
2
∗
T
(
n
)
=
2
1
×
(
h
−
1
)
+
2
2
×
(
h
−
2
)
+
2
3
×
(
h
−
3
)
+
2
4
×
(
h
−
4
)
+
.
.
.
+
2
h
−
2
×
2
+
2
h
−
1
×
1
2*T(n)=2^1×(h−1)+2^2×(h−2)+2^3×(h−3)+2^4×(h−4)+...+2^{h−2}×2+2^{h−1}×1
2∗T(n)=21×(h−1)+22×(h−2)+23×(h−3)+24×(h−4)+...+2h−2×2+2h−1×1
2 ∗ T ( n ) − T ( n ) 2*T(n)-T(n) 2∗T(n)−T(n)
得
T
(
n
)
=
1
−
h
+
2
1
+
2
2
+
2
3
+
2
4
.
.
.
+
2
h
−
2
+
2
h
−
1
T(n)=1−h+2^1+2^2+2^3+2^4...+2^{h−2}+2^{h−1}
T(n)=1−h+21+22+23+24...+2h−2+2h−1
T ( n ) = 2 0 + 2 1 + 2 2 + 2 3 + 2 4 . . . + 2 h − 2 + 2 h − 1 − h T(n)=2^0+2^1+2^2+2^3+2^4...+2^{h−2}+2^{h−1}-h T(n)=20+21+22+23+24...+2h−2+2h−1−h
等比数列求和得:
T
(
n
)
=
2
h
−
1
−
h
T(n)=2^h-1-h
T(n)=2h−1−h
由二叉树的性质,有N = 2h-1和 h = log2(N+1)
T
(
n
)
=
N
−
l
o
g
2
(
N
+
1
)
T(n)=N-log_2(N+1)
T(n)=N−log2(N+1)
用大O的渐进表示法
T
(
n
)
=
O
(
N
)
T(n)=O(N)
T(n)=O(N)
堆的插入
堆的插入,要看堆的插入位置,不能直接把数据插入到堆顶,因为一旦把数据插入堆顶,那么堆的结构就会被破坏,就要重新建堆,所以我们在进行堆的插入时,只需要把数据插入到堆的最后一个元素的下一个元素,然后对这个位置进行向上调整算法,就能实现把这个数据插入到堆中适合的位置
动图演示内容和上面图片不是一个堆
向上调整算法代码实现
void AdJustUp(int* a, int a_size, int start){
int child = start;
int parent = (child - 1) / 2;
//当child节点为0时,调整结束
whlie(child > 0){
if(a[child] < a[parent]){
std::swap(a[child], a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else{
break;
}
}
}
堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据交换,然后删除数组最后一个数据,再对堆顶元素进行向下调整算法
动图演示内容和上面图片不是一个堆
堆的完整代码实现
这里用容器来充当数组,完成数据管理
#ifndef CLION_HEAP_HPP
#define CLION_HEAP_HPP
#include <vector>
#include <iostream>
#include <cassert>
#include <valarray>
namespace ns_Heap{
template <class T>
class heap{
private:
std::vector<T> arr;
public:
//空构造
heap()
:arr(){}
//区间构造
template<class iterator>
heap(iterator begin,iterator end)
:arr(begin,end){
//从最后一个有叶子的节点开始,向下调整,调完整个容器
for(int i = (arr.size()-1-1)/2; i >= 0; --i){
AdJushDown(i);
}
}
~heap(){
//容器自动销毁
}
public:
//插入数据到堆中,并使堆结构保持
void push(const T& val){
//插入数据到容器末
arr.push_back(val);
//此时除了最后一个数据,剩下的数据是堆结构,向上调整算法把整个容器调成堆结构
AdJustUp(arr.size()-1);
}
void pop(){
if(empty())
return;
//交换堆顶元素和最后一个元素的位置
std::swap(arr[0], arr[arr.size()-1]);
//删除交换到最后一个元素位置的元素
arr.pop_back();
//向下调整算法把容器调成堆
AdJushDown(0);
}
//判空
bool empty(){
return arr.empty();
}
//返回堆的大小
size_t size()const {
return arr.size();
}
//取堆顶元素
const T& top(){
return arr.front();
}
///打印堆
//求结点数为n的二叉树的深度
int depth(int n){
assert(n >= 0);
if (n>0){
int m = 2;
int hight = 1;
while (m < n + 1){
m *= 2;
hight++;
}
return hight;
}
else{
return 0;
}
}
//打印堆
void print(){
//按照物理结构进行打印
int i = 0;
for (i = 0; i < arr.size(); i++){
printf("%d ", arr[i]);
}
printf("\n");
//按照树形结构进行打印
int h = depth(arr.size());
int N = (int)pow(2, h) - 1;//与该二叉树深度相同的满二叉树的结点总数
int space = N - 1;//记录每一行前面的空格数
int row = 1;//当前打印的行数
int pos = 0;//待打印数据的下标
while (1){
//打印前面的空格
int i = 0;
for (i = 0; i < space; i++){
printf(" ");
}
//打印数据和间距
int count = (int)pow(2, row - 1);//每一行的数字个数
while (count--){//打印一行
printf("%02d", arr[pos++]);//打印数据
if (pos >= arr.size()){//数据打印完毕
printf("\n");
return;
}
int distance = (space + 1) * 2;//两个数之间的空格数
while (distance--){//打印两个数之间的空格
printf(" ");
}
}
printf("\n");
row++;
space = space / 2 - 1;
}
}
protected:
void AdJushDown(int start){
int parent = start;
int child = (parent * 2) + 1;
while(child < arr.size()){
//建大堆,就找出左右孩子中大的那个,如果是建小堆,就找小的那个
if(child + 1 < arr.size() && arr[child + 1] > arr[child]){
child++;
}
//如果孩子节点大于父节点,就交换孩子节点和父亲节点的数据,再更新孩子和父亲节点的位置
if(arr[child] > arr[parent]){
std::swap(arr[child], arr[parent]);
parent = child;
child = (parent * 2) + 1;
}
//如果调整完毕,就退出循环
else{
break;
}
}
}
void AdJustUp(int start){
//算出父节点位置
int child = start;
int parent = (child - 1) / 2;
//如果孩子节点数据大于父亲节点,就向上调整
while(child > 0){//调整到根结点的位置截止
if(arr[child] > arr[parent]){
std::swap(arr[child], arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
//如果调整完毕,就退出循环
else{
break;
}
}
}
};
}
#endif //CLION_HEAP_HPP