完全二叉堆(优先级队列)
一、堆的基本概念及结构
1.1基本概念
堆: 如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足ki<=k2i+1且ki<=k2i+2(或满足ki>=k2i+1且ki>=k2i+2),其中i=0,1,2,…,则称该集合为堆。
小堆: 将根结点最小的堆叫做小堆,也叫最小堆或小根堆。
大堆: 将根结点最大的堆叫做大堆,也叫最大堆或大根堆。
完全二叉树的特点:
叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。
1.2结构
需要满足结构性和堆序性
结构性:
逻辑上,等同于完全二叉树
物理上,借助向量实现
逻辑结点的孩子和父亲结点在物理上下标对应的关系(以下标i为例)
Parent:
(i-1)/2
Lchild:
(2*i)+1
Rchild:
(i+1)*2
一共n个结点时,内部结点(除叶结点的结点)的最大下标:
(n-2)/2
堆序性(这里以大根堆为例):
堆中的任意结点总是不大于其父结点的值
二、插入与上滤(以大根堆为例)
2.1算法
在堆中插入数据(e)的步骤如下:
-
将e作为末元素插入向量
- 当堆中有效元素的个数等于堆的容量,则需要扩容
-
向上调整(上滤)
-
检测e和其父结点的堆序性是否被破坏,如果没有则完成插入;否则,将其与父结点换位
-
如果换位之后堆序性恢复则完成插入;否则e和祖父结点的堆序性也被破坏,因此继续换位
-
直到e和其父亲结点满足堆序性或者e到达堆顶
-
以下图为例:
e不断向上”攀爬“的过程也称为上滤
2.2代码实现
//创建堆数据类型
struct Heap
{
int* m_A; //用于存储数据的数组
int m_Size; //记录堆中已有元素个数
int m_Capacity; //记录堆的容量
};
//向上调整
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = *a;
}
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
while (child)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入
void HeapInsert(Heap* php, int x)
{
//1.将x作为末元素插入数组
assert(php);
//判断是否需要扩容
if (php->m_Size == php->m_Capacity)
{
int* tmp = (int*)realloc(php->m_A, 2 * php->m_Capacity * sizeof(int));
if (!tmp)
{
cout << "realloc fail!" << endl;
exit(-1);
}
php->m_A = tmp;
php->m_Size *= 2;
}
php->m_A[php->m_Size] = x;
php->m_Size++;
//2.向上调整
AdjustUp(php->m_A, php->m_Size - 1);
}
时间复杂度: O(logn)
三、删除与下滤(以大根堆为例)
注意:这里所说的删除是指删除堆顶元素
3.1算法
在堆中删除数据(e)的步骤:
- 先判断堆的有效元素个数是否为0,如果为0就退出程序
- 如果堆中有元素,交换堆顶和最后一个结点元素,有效元素个数减1
- 向下调整
- 找出堆顶的孩子中较大的孩子(判断是否存在,如果存在)与父结点比较,如果较大的孩子大就交换两个结点;否则,删除结束
- 并将父结点设置为较大的孩子,再继续检查他的孩子,直到父结点不再小于他的任一孩子或者他的孩子不存在
以下图为例:
3.2代码实现
//堆的判空
bool HeapEmpty(Heap* php)
{
assert(php);
return php->m_Size == 0;
}
//向下调整
void AdjustDown(int* a, int n, int parent)
{
int child = 2 * parent + 1;
//当有孩子时才向下调整
while (child < n)
{
//寻找最大的孩子
if (child+1 < n && a[child] < a[child+1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
//堆的删除
void HeapErase(Heap* php)
{
assert(php);
//1.判断堆的有效元素是否为0
assert(!HeapEmpty(php));
//2.交换顶部元素和最后一个元素,有效元素个数减小1
Swap(&php->m_A[0], &php->m_A[php->m_Size - 1]);
php->m_Size--;
//3.向下调整
AdjustDown(php->m_A, php->m_Size, 0);
}
时间复杂度: O(logn)
四、批量建堆
批量建堆方式有两种:
- 自上而下的上滤
- 自下而上的下滤
那么我们应该选用哪种方法呢,哪种方法的效率最高呢?
自上而下的上滤:等效于将数组中的每一个元素插入到堆中,一共要插入n次,每次插入为logn的时间,所以时间复杂度:
O(nlogn)
自下而上的下滤:等效于将数组中的内部结点从右到左,从下到上做了一次下滤操作,这里时间复杂度的推导较复杂不做解释,时间复杂度:
O(n)
从效率来看,我们将采用方法二。
4.1算法
批量建堆的主要步骤:
- 申请堆中存放数据的空间
- 将要原始建堆的数据拷贝到那个空间中,设置大小,容量
- 从下往上,从左向右依次下滤内部结点(非叶结点)
以下图为例:
4.2代码实现
//批量建堆
void HeapInit(Heap* php, int* sc, int n)
{
assert(php);
//1.申请堆中存放数据的空间
int* tmp = (int*)malloc(n * sizeof(int));
if (!tmp)
{
cout << "malloc fail!" << endl;
}
php->m_A = tmp;
//2.将原始数据拷贝到堆的空间中
memcpy(php->m_A, sc, n * sizeof(int));
php->m_Size = n;
php->m_Capacity = n;
//3.下滤内部结点
int i = 0;
for (i = (php->m_Size - 2) / 2; i >= 0; i--)
{
AdjustDown(php->m_A, php->m_Size, i);
}
}
时间复杂度: O(n)
五、堆排序
5.1算法
堆排序的主要步骤:
- 将数组建堆
- 将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最大的数不参与向下调整。
- 完成步骤1后,这棵树除最后一个数之外,其余数又成一个大堆,然后又将堆顶数据与堆的最后一个数据交换,这样一来,第二大的数就被放到了倒数第二个位置上,然后该数又不参与堆的向下调整…反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个升序。
以下图为例:
5.2代码实现
void HeapSort(int* a, int n)
{
//1.建堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
//2.交换数据并重新建堆
while (end)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
时间复杂度: O(nlogn)
六、完整代码
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cstring>
using namespace std;
//创建堆数据类型
struct Heap
{
int* m_A; //用于存储数据的数组
int m_Size; //记录堆中已有元素个数
int m_Capacity; //记录堆的容量
};
//交换函数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//向上调整
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
while (child)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入
void HeapInsert(Heap* php, int x)
{
//1.将x作为末元素插入数组
assert(php);
//判断是否需要扩容
if (php->m_Size == php->m_Capacity)
{
int* tmp = (int*)realloc(php->m_A, 2 * php->m_Capacity * sizeof(int));
if (!tmp)
{
cout << "realloc fail!" << endl;
exit(-1);
}
php->m_A = tmp;
php->m_Capacity *= 2;
}
php->m_A[php->m_Size] = x;
php->m_Size++;
//2.向上调整
AdjustUp(php->m_A, php->m_Size - 1);
}
//堆的判空
bool HeapEmpty(Heap* php)
{
assert(php);
return php->m_Size == 0;
}
//向下调整
void AdjustDown(int* a, int n, int parent)
{
int child = 2 * parent + 1;
//当有孩子时才向下调整
while (child < n)
{
//寻找最大的孩子
if (child+1 < n && a[child] < a[child+1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
//堆的删除
void HeapErase(Heap* php)
{
assert(php);
//1.判断堆的有效元素是否为0
assert(!HeapEmpty(php));
//2.交换顶部元素和最后一个元素,有效元素个数减小1
Swap(&php->m_A[0], &php->m_A[php->m_Size - 1]);
php->m_Size--;
//3.向下调整
AdjustDown(php->m_A, php->m_Size, 0);
}
//批量建堆
void HeapInit(Heap* php, int* sc, int n)
{
assert(php);
//1.申请堆中存放数据的空间
int* tmp = (int*)malloc(n * sizeof(int));
if (!tmp)
{
cout << "malloc fail!" << endl;
}
php->m_A = tmp;
//2.将原始数据拷贝到堆的空间中
memcpy(php->m_A, sc, n * sizeof(int));
php->m_Size = n;
php->m_Capacity = n;
//3.下滤内部结点
int i = 0;
for (i = (php->m_Size - 2) / 2; i >= 0; i--)
{
AdjustDown(php->m_A, php->m_Size, i);
}
}
//销毁堆
void HeapDestroy(Heap* php)
{
assert(php);
free(php->m_A);
php->m_A = NULL;
php->m_Size = 0;
php->m_Capacity = 0;
}
//堆排序
void HeapSort(int* a, int n)
{
//1.建堆
for (int i = (n - 2) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
//2.交换数据并重新建堆
while (end)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
int main()
{
Heap HP;
int arr[] = { 2,1,6,3,9,7,4,8,5 };
int sz = sizeof(arr) / sizeof(arr[0]);
//批量建堆
HeapInit(&HP, arr, sz);
//插入
HeapInsert(&HP, 11);
//删除
HeapErase(&HP);
//销毁堆
HeapDestroy(&HP);
//堆排序
int unsorted[] = { 3,21,4,6,23,63,42 };
int size = sizeof(unsorted) / sizeof(unsorted[0]);
HeapSort(unsorted, size);
system("pause");
return 0;
}