二叉树的顺序结构与实现(堆)
一、二叉树的顺序结构与实现
1.二叉树的顺序结构
普通的二叉树,是不适合用数组来存储的,可能会存在大量的空间浪费。相对而言,完全二叉树更适合使用顺序结构来进行存储。在此,通常把堆使用顺序结构的数组来存储。补充:这里的堆和操作系统虚拟进程地址空间中的堆是不是一个概念,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
2.堆的概念及结构
如果有一个关键码的集合A = { a(1),a(2) ,…, a(n-1)},把它的所有元素按完全二叉树的顺序存储方式存储至一个一维数组中,并满足: a(i)<=a(2i+1) 且 a(i)<=a(2i+2) ( a(i)>= a(2i+1)且 a(i)>= a(2i+2)), i = 0,1,
2…,则称为小堆(或大堆)。那么根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
1.堆中某个节点的值总是不大于或不小于它的父节点的值;
2.堆总是一棵完全二叉树。
二、堆的实现
主要算法:堆的向上调整算法与堆的向下调整算法
1.堆的向下调整算法
将一个数组逻辑上看成一个完全二叉树,利用从根节点开始的向下调整算法可以将其调整为一个小堆。这里值得注意的是:当左右子树必须是一个堆的前提下,才能进行调整。
在此是小顶堆,从27开始,很明显它比它的两个孩子都大,要和两个孩子中的较为小的孩子进行交换,如果与19交换,那么19比15大,不符合小堆的性质。27换下去之后再和当前的两个孩子进行比较,与小的那个孩子换,一直比较到不比两个孩子大或者到了最后一层的时候就停止。
代码实现:
a:数组首元素地址,n:数组中有效元素的个数,parent:要被调整的元素,HPDataType:自定义类型
代码实现:
void AdjustDown(HPDataType * a, int n, int parent)
//这里是以大顶堆向下调整,若是小顶堆,要把下面的两个'>'换成'<'
{
int child = parent * 2 + 1;//假设就把左孩子当作最小的那个
while (child < n)//要小于当前数组总共的有效元素个数
{
// 找出较大的那个孩子
if (child + 1 < n && a[child + 1] > a[child])
//child + 1 < n 有左孩子不一定有右孩子,右孩子要小于元素个数
{
child++;
}
//找出大的然后往上替换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
2.堆的向上调整算法
这里以最下层的10为例,在小堆的结构中向上调整,10和它的父节点进行比较,小于它的父节点,进行交换,由于父节点是最小或者最大的,所以换完之后不改变原来堆的结构,然后向上一层一层的进行交换,一直到10不比父节点小的时候,或者到了最顶端(下标为0)的时候就停下来。
代码实现:
a:首元素地址,child:要被修改的节点下标,HPDataType:自定义数据类型
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0),有bug,当child为0的时候,parent为0,不符合预期
while (child > 0)//到了最顶端(下标为0)的时候就停止。
{
if (a[child] > a[parent])
//跟祖先换,因为不论是大顶堆还是小顶堆祖先都是最大的那个或者最小的那个
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;//通过孩子结点的下标求双亲结点
}
else
{
break;
}
}
}
3.堆的创建
int a[]={1,5,3,8,7,6};
在这个数组中,逻辑上可以看成一个完全二叉树,但是它并不是一个堆,这里需要将其调整成为一个堆,方法是从倒数的第一个非叶子节点的子树进行调整,一直调整到根节点的树(向下调整),就可以调整形成堆。
原因如下:
1.假设此树满二叉树,最后一层占到了总结点个数的一半。而向上调整中,除了最上层的那一个结点,每个结点都要进行一次向上调整,而向下调整,情况就反过来,直接省去了最下面一层的结点,因此从倒数的第一个非叶子节点的子树开始调整即可。
2.层数越往下走,结点数就越多,向上调整需要交换的次数就越多,而向下调整,越下层的结点交换的次数反而越少。
补充:求倒数第一个非叶子节点
在此满二叉树中,i从1开始,如果
i是1,那么为根节点,如果i>1,那么满足父节点:(i-1)/2。即:倒数第一个非叶子节点=(最后一个结点的下标-1)/2
4.建堆的时间复杂度
综上可得,时间复杂度为O(N)。
5.堆的插入
在数组尾部进行插入,然后进行向上调整算法。
代码实现:
void HeapPush(HP* php, HPDataType x)
{
assert(php);
// 扩容
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity*sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);//向上调整算法
}
6.堆的删除
堆删除,删除的是堆顶的元素,将堆顶的元素和最后一个数据一换,再删除数组的最后一个元素,然后进行向下调整算法。
代码实现:
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
三、堆的应用
1.堆排序
利用堆的思想来进行排序,分为两个步骤:
1.建堆,升序建大堆,降序建小堆。
2.运用堆删除的思想来进行排序。
因为,堆顶元素一定是最小的或者最大的,首先让堆顶元素与下标为n-1的元素进行交换,交换之后再进行向下调整。再让堆顶与下标为n-2的元素进行交换,交换之后再进行向下调整。循环下去,一直到把下标为1的元素和小标为0的元素
代码实现:
typedef int HPDataType;
void Swap(HPDataType* p1, HPDataType* p2)//交换函数
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(HPDataType * a, int n, int parent)
//这里是以大顶堆向下调整,若是小顶堆,要把下面的两个'>'换成'<'
{
int child = parent * 2 + 1;//假设就把左孩子当作最小的那个
while (child < n)//要小于当前数组总共的有效元素个数
{
// 找出较大的那个孩子
if (child + 1 < n && a[child + 1] > a[child])
//child + 1 < n 有左孩子不一定有右孩子,右孩子要小于元素个数
{
child++;
}
//找出大的然后往上替换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)//堆排序
{
// 堆排序思路:选择排序,依次选数,从后往前排
// 升序 --- 大堆
// 降序 --- 小堆
// 先建堆 -- 然后向下调整建堆 - O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
// 选数
int i = 1;
while (i < n)
{
Swap(&a[0], &a[n - i]);
AdjustDown(a, n - i, 0);
++i;
}
}
int main()
{
int a[] = { 17, 1, 20, 21, 8, 32, 61, 4, 29, 7 };
HeapSort(a, sizeof(a) / sizeof(int));
for (size_t i = 0; i < sizeof(a) / sizeof(int); ++i)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
2.TOP-K问题
就是求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如给1亿个数,求最大的10个数。
想到解决这个问题,大多数人的想法是用排序,但是,如果数据太大,可能数据都不能放到内存中去,因此,最佳的解决方法是用堆来进行解决。
1.用给的数据集合中前K个元素来建堆。
如果求前k个最大的元素,则建小堆;如果求前k个最小的元素,则建大堆。
2.用剩余的N-K个元素依次与堆顶元素来比较,替换,进行向下调整。
若求前k个最大的元素,则建小堆,遍历剩余的N-K个元素。若比堆顶元素小,不用动;若比堆顶元素大,那么就换到堆顶,然后再进行向下调整,就这样直到遍历到最后一个元素,最终堆中剩余的K个元素就是所求的前K个最大的元素。
相反,如果求前k个最小的元素,就建大堆,遍历剩余N-K个元素。若比堆顶元素大,不用动;若比堆顶元素小,就换到堆顶,然后进行向下调整,直到遍历到最后一个元素,最终堆中剩余的K个元素就是所求的前K个最小的元素。
typedef int HPDataType;
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
// 找出小的那个孩子
if (minChild + 1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
void PrintTopK(int* a, int k, int num)
{
assert(a);
// 首先建k个数的小堆
for (int j = (k - 1 - 1) / 2; j >= 0; --j)
{
AdjustDown(a, k, j);
}
// 继续看后面N-K个数
int tmp = k;
while (tmp < num)
{
tmp++;
if (a[tmp] > a[0])//遍历后面的元素,比堆顶大就交换
{
a[0] = a[tmp];
AdjustDown(a, k, 0);
}
}
}
int main()
{
int a[] = { 12,6,8,36,56,8,3,96,82,59,14,1,562,10010,10020,10030,10040 };
int num = sizeof(a) / sizeof(int);
PrintTopK(a, 6, num);
for (int i = 0; i < k; ++i)
{
printf("%d ", a[i]);
}
return 0;
}