目录
前言
在学习堆之前需要先了解树的相关概念。如果了解过树可以直接跳过预备知识。
预备知识
树的概念:
树是一种非线性的数据结构。树(tree)可以用几种方式定义。定义树的一种自然方式是递归的方法。一棵树是一些节点的集合。这个集合可以是空集;若非空,则一颗树由称做根的节点r以及0个或多个非空的子树T1,T2,...Tk组成,这些子树中的每一颗的根都被来自根r的一条有向的边(edge)所连接。
每一颗子树的根叫做根r的儿子(child),而r是每一颗子树的根的父亲(parent)。图4-1显示用递归定义的典型的树。
树的相关概念
- 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的度为6、B的度为0。
- 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H...等节点为叶节点。
- 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
- 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。 如上图:A是B的父节点。
- 子节点:与子树的根节点直接相连的节点称为该根节点的子节点。 如上图:B是A的孩子节点。
二叉树的概念
二叉树(binary tree)是一棵特殊的树,其中每个节点都不能有多于2个的子节点。
图4-11显示一颗由一个根和两颗子树组成的二叉树,TL和TR均可能为空。
特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
- 完全二叉树:对于深度为h的,有n个结点的二叉树,当且仅当其每一个结点都与深度为h的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有
个结点。
- 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是
。
- 对任何一棵二叉树, 如果叶结点个数为
, 度为2的分支结点个数为
,则有
=
+1
- 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=
。
- 若n>0,n位置节点的父节点(n-1)/2;当n=0,n为根节点编号,无父节点 。若2n+1<
,左孩子序号:2i+1,若2n+1>=
否则无左孩子 。 若2n+2<
,右孩子序号:2n+2,若2n+2>=
否则无右孩子。
堆的概念
堆又称二叉堆(binary heap),C++中也称优先队列(priority_queue)。堆是一颗完全二叉树。完全二叉树很有规律,所以堆可以用一个数组表示,不需要指针。图6-3中的数组对应图6-2中的堆。
堆的定义
typedef int HeapDataType;
typedef struct Heap
{
HeapDataType* _arr;
int _size;
int _capacity;
}Heap;
堆的分类
- 大堆:每颗子树的根节点总是大于其他节点的值,也就是说根节点的值就是最大的。
- 小堆:每颗子树的根节点总是小于其他节点的值,也就是说根节点的值就是最小的。
堆的基本操作
通过建立小堆案例分析,我们通过下图可知,根节点的左右子树都是一个小堆,那我们可以从根节点开始向下去调整堆的结构,保持堆的特性。这种的操作称为下滤。下滤的前提一定是左右子树必须是一个堆,才能进行操作。
下滤操作的过程:
记调整的位置为parent,首先选出parent的左右孩子中最小的一个记作child,让child和parent相比较。以小堆为例,如果parent < child 那就不需要调整直接结束,如果parent > child那么将parent和child的关键值互换,并把child赋值给parent进行迭代,重复上述过程直到结束。
下滤算法代码参考:
void Swap(HeapDataType* x, HeapDataType* y)
{
HeapDataType temp = *x;
*x = *y;
*y = temp;
}
void AdjustDown(HeapDataType* arr, int n, int root)
{
int parent = root;//根结点
int child = parent * 2 + 1;//左孩子
while (child < n)
{
//注意右孩子不能超出数组大小
if ( (child + 1 < n) && (arr[child + 1] < arr[child]))
{
child++;//寻找左右孩子中最小的数
}
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
建堆(BuildHeap)
建堆需要利用下滤算法,而下滤算法的前提是下调位置的左右子树均为堆,而我们要怎么保证左右子树均为堆?
如下图所示,利用一随意的数组来建立堆,不能直接从根节点开始下滤。通过观察可以发现最后一层的树都没有左右子树,我们其实就可以从数组的最后一个元素开始调,但是,我们可以发现最后一层的叶子节点都不需要调,只需要从数组的最后一个元素的父节点开始调。
建堆代码演示:
void BuildHeap(Heap* php, HeapDataType* arr, int n)
{
assert(php);
php->_arr = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
if (php->_arr == NULL)
{
printf("内存不足\n");
exit(1);
}
//数组的数据拷贝到堆中
memcpy(php->_arr, arr, sizeof(HeapDataType) * n);
php->_size = n;
php->_capacity = n;
//构造堆
//n -1 代表数组的最后一个元素
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->_arr, php->_size, i);
}
}
插入操作(HeapInsert)
为将元素x插入堆中,我们要先把x插入数组下标为size的位置,否则可能会破坏堆的特性。如果x可以放在size位置而不破坏堆的序,那么插入完成。否则,我们需要往上去调整,该操作称为上滤。
上滤算法:
如下图所示;我们只需要调整插入位置到根节点的路径上的节点。记插入位置为child,插入位置的父节点为parent。通过比较child的值和parent的值。如果child > parent 那就不需要调整。如果child < parent,那就需要进行上滤调整,将parent赋值给child进行迭代重复上述操作,直到调整结束。
上滤算法演示:
void AdjustUp(HeapDataType* arr, int n, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[parent] > arr[child])//小堆
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;;
}
else
{
break;
}
}
}
插入操作代码演示:
void HeapInsert(Heap* php, HeapDataType x)
{
assert(php);
if (php->_size == php->_capacity)//扩容
{
php->_capacity *= 2;
HeapDataType* temp = (HeapDataType*)realloc(php->_arr,
sizeof(HeapDataType) * php->_capacity);
assert(temp);
php->_arr = temp;
}
php->_arr[php->_size++] = x;
AdjustUp(php->_arr, php->_size, php->_size - 1);
}
删除操作(HeapPop)
如下图所示;删除最后一个元素是没有任何意义的,我们应该删除(pop)堆顶的数据,来获得第二大或第二小的元素才有意义。
- 具体操作:将堆顶的元素和最后一个元素互换,size--,再从堆顶位置进行下滤算法操作,就可以得到第二大或第二小的元素。
删除代码演示:
void HeapPop(Heap* php)
{
assert(php);
assert(php->_size > 0);
Swap(&php->_arr[0], &php->_arr[php->_size - 1]);
php->_size--;
AdjustDown(php->_arr, php->_size, 0);
}
到此堆的一些基本操作以及基本实现。完整的代码如下所示。(含测试案例)
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
typedef int HeapDataType;
typedef struct Heap
{
HeapDataType* _arr;
int _size;
int _capacity;
}Heap;
void Swap(HeapDataType* x, HeapDataType* y)
{
HeapDataType temp = *x;
*x = *y;
*y = temp;
}
void AdjustDown(HeapDataType* arr, int n, int root)
{
int parent = root;//根结点
int child = parent * 2 + 1;//左孩子
while (child < n)
{
//注意右孩子不能超出数组大小
if ( (child + 1 < n) && (arr[child + 1] < arr[child]))
{
child++;//寻找左右孩子中最小的数
}
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapInit(Heap* php, HeapDataType* arr, int n)
{
assert(php);
php->_arr = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
if (php->_arr == NULL)
{
printf("内存不足\n");
exit(1);
}
//数组的数据拷贝到堆中
memcpy(php->_arr, arr, sizeof(HeapDataType) * n);
php->_size = n;
php->_capacity = n;
//构造堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->_arr, php->_size, i);
}
}
void HeapDestory(Heap* php)
{
assert(php);
free(php->_arr);
php->_arr = NULL;
php->_capacity = php->_size = 0;
}
void AdjustUp(HeapDataType* arr, int n, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[parent] > arr[child])//小堆
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;;
}
else
{
break;
}
}
}
void HeapInsert(Heap* php, HeapDataType x)
{
assert(php);
if (php->_size == php->_capacity)//扩容
{
php->_capacity *= 2;
HeapDataType* temp = (HeapDataType*)realloc(php->_arr,
sizeof(HeapDataType) * php->_capacity);
assert(temp);
php->_arr = temp;
}
php->_arr[php->_size++] = x;
AdjustUp(php->_arr, php->_size, php->_size - 1);
}
void HeapPop(Heap* php)
{
assert(php);
assert(php->_size > 0);
Swap(&php->_arr[0], &php->_arr[php->_size - 1]);
php->_size--;
AdjustDown(php->_arr, php->_size, 0);
}
HeapDataType HeapTop(Heap* php)
{
assert(php);
assert(php->_size > 0);
return php->_arr[0];
}
int main()
{
HeapDataType a[] = { 27,10,19,16,28,35,65,48,30,45 };
Heap hp;
BuildHeap(&hp, a, sizeof(a) / sizeof(HeapDataType));
for (int i = 0; i < hp._size; i++)
{
printf("%d ", hp._arr[i]);
}
printf("\n");
HeapInsert(&hp, 15);
for (int i = 0; i < hp._size; i++)
{
printf("%d ", hp._arr[i]);
}
printf("\n");
HeapPop(&hp);
for (int i = 0; i < hp._size; i++)
{
printf("%d ", hp._arr[i]);
}
return 0;
}
堆的应用
堆排序就是利用堆的思想进行排序。
堆排步骤:
1、建堆
- 升序:建大堆
- 降序:建小堆
2、利用堆删除的思想进行排序(以升序为例)
- 堆采取的存储结构为数组,假设这个数组的大小为n。堆顶(数组首元素)的数据一定是最大的,将堆顶的数据与数组的最后一个元素进行交换,那么最大的元素就出现在数组的最后一个元素;这时只要将n--并且在堆顶进行一次下滤算法,就可以得到第二大的元素,同时再将堆顶的元素和当前数组的最后一个元素交换,就得到第二大的元素,如此重复上述的操作,直到n == 0就停止,最终就会得到一个升序的数组。具体操作下图所示。
堆排代码演示:
void HeapSort(int* a, int n)
{
//1、建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
有关堆排的特点这里不做详解,后续会在排序部分一起分析。
Top-k问题
求数据集合N中前k个最大或最小的元素,这个数据集合一般会很大。
对于这部分的问题我们可以有2种思路
第一种思路:
- 直接利用堆排对数据进行排序,从而筛选出前K个最大或最小的数据。这个思路就比较暴力,因为这个数据的集合很大,而堆排需要建堆,空间复杂度为O(n),万一内存存不下这么多数据就没有办法进行堆排。
第二种思路(推荐)
- 首先建立k个数的大堆或小堆,根据题目要求来决定。如果是要选出最大的k个数,那么建立小堆;否则建立大堆。其次遍历N-k个数据,如果比堆顶的元素大就覆盖堆顶的元素并对堆顶进行下滤操作。遍历结束后,这k个数的小堆中,就是N中最大的前K个数。
总结
堆就是一颗完全二叉树,关于对堆的后续学习可以去参考C++的优先队列(priority_queue)