写在前面:
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把 堆 (一种二叉树) 使用顺序结构的数组来存储。
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构,今天总结一下顺序结构。
首先来了解一下完全二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树
这里主要涉及到堆排序的问题,通过将一维数组构建成一个堆,然后进行排序插入,查找,删除等操作。
初始化堆与调整成为大堆或小堆
开始初始化为:下图(一个无序的堆),是没有太大的意义的,因此我们需要通过算法的调整来构建成一个小堆或大堆,以方便以后的查找和删除等操作。
这里引入两个个概念: 小堆 大堆
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
简单来说:
1.大堆:根节点是最大的,且除根节点外所有的父亲结点都大于它的孩子结点
2.小堆:根节点是最小的,且除根节点外所有的父亲结点都小于它的孩子结点
比如:
如何初始化堆并调整成为一个小堆或大堆
我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。如何调整为大堆或小堆呢?这里我们从根节点开始调整,一直调整到非叶子节点,就可以调整成堆。
所以首先我们需要初始化一个堆,但在这之前需要定义堆类型的结构体。
具体实现
因为我们知道堆在实际上是用数组来存储的
1.定义结构体:
typedef int HPDataType;
typedef struct Heap//定义结构体
{
HPDataType* _a;//堆在实际上是一个数组,完全二叉树是逻辑上的存储,但物理上还是存在数组里的
int _size;//大小
int _capacity;//容量
}Heap;
2.定义一个结构体变量和一个数组:
int a[]={2,13,23,6,35,16,56,36};
Heap hp;
3.初始化一个堆并构建成小堆或者大堆:
这里就以初始化小堆为例
(1)先申请内存,初始化为一个堆:
void HeapInit1(Heap*hp,HPDataType* a,int n)//初始化为小堆
{ //这里的 n 是接受数组元素个数的形参
int i;
hp->_a=(HPDataType*)malloc(sizeof(HPDataType)*n);//申请内存
memcpy(hp->_a,a,sizeof(HPDataType)*n);//将数据复制到申请的内存中
hp->_size=n;//大小和容量
hp->_capacity=n;
//构建成堆
for(i=(n-1-1)/2;i>=0;--i)//循环调整堆
{
AdjustDown1(hp->_a,hp->_size,i);//构建成小堆
}
}
(2)向下调整为小堆:
void AdjustDown1(HPDataType* a,size_t n,size_t parent)//向下调整构建小堆
{//通过父亲节点和孩子节点进行比较
size_t child=parent*2+1;//先找到左孩子,就找到右孩子通过下标访问
while(child<n)
{
if((child+1)<n &&a[child+1]<a[child])//找到左右孩子中小的那个数
{//调整为大堆则与此相反,找到左右孩子中大的那个数,如果大于父亲节点就交换
++child;
}
if(a[child]<a[parent])//如果小于父亲节点的话就交换
{
Swap(&a[child],&a[parent]);//传地址才能真正的交换值
parent=child;//并且继续向下遍历
child=parent*2+1;
}
else
{
break;
}
}
}
先来看一下结果:
再直观的感受一下:
我们可以看到从根节点,父亲节点均小于它的左右两个孩子节点,那么我们就成功的构建成了一个小堆。
构建成堆了之后就可以进行一些对堆的操作了。
功能实现:
void HeapInit1(Heap*hp,HPDataType* a,int n);//初始化为小堆
void HeapInit2(Heap*hp,HPDataType* a,int n);//初始化为大堆
void AdjustDown1(HPDataType* a,size_t parent,size_t n);//向下调整为小堆
void AdjustDown2(HPDataType* a,size_t n,size_t parent);//向下调整为大堆
void AdjustUp(HPDataType* a,size_t child);//向上调整
void HeapPrint(Heap*hp);//打印数据
void HeapDestory(Heap*hp);//删除堆
void HeapPush(Heap*hp,HPDataType x);//插入数据
void HeapPop(Heap*hp);//删除堆顶
HPDataType HeapTop(Heap*hp);//取堆顶元素
size_t HeapSize(Heap*hp);//堆的大小
这里我们讲一个向上调整,因为在做数据的插入时如果从堆顶插入的话,实现起来不太现实,而且有可能会导致堆顺序的混乱,因此我们采用向上调整法,从数组的最后一个元素后插入,再向上调整构建成大堆或小堆。
同样以小堆为例:
void HeapPush(Heap*hp,HPDataType x)//插入一个数,使用向上调整法
{
if(hp->_size==hp->_capacity)//如果满了之后
{
size_t newcapacity=hp->_capacity== 0 ? 2 : hp->_capacity*2;//成2倍的增
hp->_a=(HPDataType*)realloc(hp->_a,sizeof(HPDataType)*newcapacity);
hp->_capacity=newcapacity;//将新的容量给到原来的容量
}
hp->_a[hp->_size]=x;
hp->_size++;
AdjustUp(hp->_a,hp->_size-1);//向上调,从最后一个下标开始调
}
void AdjustUp(HPDataType* a,size_t child)//向上调整
{
int parent = (child -1)/2;//首先计算插入进来的数据的parent,
//while(parent>=0)//这个有问题
while(child>0)//走到根
{
if(a[child]<a[parent])//如果插入进来的小于parent就交换
//if(a[child]>a[parent])//如果插入进来的大于parent就交换
{
Swap(&a[child],&a[parent]);//只是交换了值,位置并没有交换
child=parent;//child继续往上走,把parent变成下一个child
parent=(child-1)/2;//重新计算parent
}
else
{
break;//没有返回值的情况尽量使用break;表示跳出向上调整的循环
}
}
}
测试一下:
直观验证一下:
由此可见,通过“尾插”的方式,插入一个数据,然后先找到他的父亲节点,然后与他的父亲节点进行比较,如果它之前是一个小堆的话,如果新插入的数据比父亲节点大则交换,依次循环,而如果是大堆的话则判断它是否大于父亲节点,如果比父亲节点小的话则交换,并继续循环,最终调整成为小堆或大堆。
代码汇总:
头文件:
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
typedef int HPDataType;
typedef struct Heap//定义结构体
{
HPDataType* _a;//堆在实际上是一个数组,完全二叉树是逻辑上的存储,但物理上还是存在数组里的
int _size;
int _capacity;
}Heap;
void HeapInit1(Heap*hp,HPDataType* a,int n);//初始化为小堆
void HeapInit2(Heap*hp,HPDataType* a,int n);//初始化为大堆
void AdjustDown1(HPDataType* a,size_t parent,size_t n);//向下调整
void AdjustDown2(HPDataType* a,size_t n,size_t parent);//向下调整
void AdjustUp(HPDataType* a,size_t child);//向上调整
void HeapPrint(Heap*hp);//打印数据
void HeapDestory(Heap*hp);//删除堆
void HeapPush(Heap*hp,HPDataType x);//插入数据
void HeapPop(Heap*hp);//删除堆顶
HPDataType HeapTop(Heap*hp);//取堆顶元素
size_t HeapSize(Heap*hp);//堆的大小
测试代码:
#include<stdio.h>
#include"Heap.h"
void TestHeap()
{
int a[]={2,13,23,6,35,16,56,36};
Heap hp;
printf("构建的小堆为:\n");
HeapInit1(&hp,a,sizeof(a)/sizeof(int ));//初始化
HeapPrint(&hp);
printf("插入一个数据调整为小堆则为:\n");
HeapPush(&hp,4);//插入一个数,使用向上调整法
HeapPrint(&hp);
//printf("构建的大堆为:\n");
//HeapInit2(&hp,a,sizeof(a)/sizeof(int ));//初始化
//HeapPush(&hp,-1);//插入一个数,使用向上调整法
//HeapPrint(&hp);
//HeapPop(&hp);//删除,不能动相对位置,只能动头和尾
//HeapPrint(&hp);
//HeapPop(&hp);//删除,不能动相对位置,只能动头和尾
//printf("%d\n",HeapTop(&hp));
//HeapPrint(&hp);
}
int main()
{
TestHeap();
system("pause");
return 0;
}
子函数:
#include"Heap.h"
void HeapInit1(Heap*hp,HPDataType* a,int n)//初始化为小堆
{
int i;
hp->_a=(HPDataType*)malloc(sizeof(HPDataType)*n);//申请内存
memcpy(hp->_a,a,sizeof(HPDataType)*n);//将数据复制到申请的内存中
hp->_size=n;//大小和容量
hp->_capacity=n;
//构建成堆
for(i=(n-1-1)/2;i>=0;--i)//循环调整堆
{
AdjustDown1(hp->_a,hp->_size,i);//构建成小堆
}
}
void HeapInit2(Heap*hp,HPDataType* a,int n)
{
int i;
hp->_a=(HPDataType*)malloc(sizeof(HPDataType)*n);
memcpy(hp->_a,a,sizeof(HPDataType)*n);
hp->_size=n;
hp->_capacity=n;
//构建成堆
for(i=(n-1-1)/2;i>=0;--i)//循环调整数据
{
AdjustDown2(hp->_a,hp->_size,i);//构建成大堆
}
}
void Swap(HPDataType*p1,HPDataType* p2)//交换函数
{
HPDataType x=*p1;//注意解引用
*p1=*p2;
*p2=x;
}
void AdjustDown1(HPDataType* a,size_t n,size_t parent)//向下调整构建小堆
{
size_t child=parent*2+1;
while(child<n)
{
if((child+1)<n &&a[child+1]<a[child])//找到左右孩子中小的那个数
{
++child;
}
if(a[child]<a[parent])//如果小于父亲节点的话就交换
{
Swap(&a[child],&a[parent]);//传地址才能真正的交换值
parent=child;//并且继续向下遍历
child=parent*2+1;
}
else
{
break;
}
}
}
void AdjustDown2(HPDataType* a,size_t n,size_t parent)//向下调整构建为大堆
{
size_t child=parent*2+1;
while(child<n)
{
if((child+1)<n &&a[child+1]>a[child])//找到左右孩子中大的那个数
{
++child;
}
if(a[child]>a[parent])//如果大于父亲节点的话就交换
{
Swap(&a[child],&a[parent]);//传地址才能真正的交换值
parent=child;//并且继续向下遍历,重新计算孩子和父亲
child=parent*2+1;
}
else
{
break;
}
}
}
void HeapPrint(Heap*hp)//打印数据
{
int i=0;
for(i=0; i<hp->_size; ++i)
{
printf("%d ",hp->_a[i]);
}
printf("\n");
}
void HeapDestory(Heap*hp)//删除堆
{
if(hp->_a)
{
free(hp->_a);
}
hp->_size=hp->_capacity=0;
}
void HeapPush(Heap*hp,HPDataType x)//插入一个数,使用向上调整法
{
if(hp->_size==hp->_capacity)//如果满了之后
{
size_t newcapacity=hp->_capacity== 0 ? 2 : hp->_capacity*2;//成2倍的增
hp->_a=(HPDataType*)realloc(hp->_a,sizeof(HPDataType)*newcapacity);
hp->_capacity=newcapacity;//将新的容量给到原来的容量
}
hp->_a[hp->_size]=x;
hp->_size++;
AdjustUp(hp->_a,hp->_size-1);//向上调,从最后一个下标开始调
}
void AdjustUp(HPDataType* a,size_t child)//向上调整
{
int parent = (child -1)/2;//首先计算插入进来的数据的parent,
//while(parent>=0)//这个有问题
while(child>0)//走到根
{
if(a[child]<a[parent])//如果插入进来的小于parent就交换
//if(a[child]>a[parent])//如果插入进来的大于parent就交换
{
Swap(&a[child],&a[parent]);//只是交换了值,位置并没有交换
child=parent;//child继续往上走,把parent变成下一个child
parent=(child-1)/2;//重新计算parent
}
else
{
break;//没有返回值的情况尽量使用break;表示跳出向上调整的循环
}
}
}
void HeapPop(Heap*hp)//删除,不能动相对位置,只能动头和尾
{
Swap(&hp->_a[0],&hp->_a[hp->_size-1]);
hp->_size--;//删掉最后一个数即删掉头
AdjustDown1(hp->_a,hp->_size,0);//再次向下调整成小堆
}
HPDataType HeapTop(Heap*hp)
{
return hp->_a[0];
}
size_t HeapSize(Heap*hp)
{
return hp->_size;
}
另外还有堆排序,会在后面的排序一起总结。