树
1.树其实就是不包含回路的连通无向图。
2.一棵树中的任意两个结点有且仅有唯一的一条路径连通。
3.一棵树如果有n个结点,那么它一定恰好有n-1条边。
二叉树
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。
【满二叉树与完全二叉树】
如果二叉树中每个内部结点都有两个儿子,这样的二叉树叫做满二叉树。如下所示:
如果一棵二叉树除了最右边位置上一个或者几个叶结点缺少外其它是丰满的,那么这样的二叉树就是完全二叉树。如下所示:
【完全二叉树如何储存】
完全二叉树中父亲和儿子之间有着神奇的规律,我们只需用一个一维数组就可以存储完全二叉树。首先将完全二叉树进行从上到下,从左到右编号。我们发现如果完全二叉树的一个父结点编号为k,那么它左儿子的编号就是2*k,右儿子的编号就是2*k+1。如果已知儿子(左儿子或右儿子)的编号是x,那么它父结点的编号就是x/2。
堆
堆是什么?堆是一种特殊的完全二叉树。
所有父结点都比子结点要小的完全二叉树我们称为最小堆。反之,如果所有父结点都比子结点要大,这样的完全二叉树称为最大堆。
如下图所示,左边为最小堆,右边为最大堆。
【堆的创建】
把n个元素建立一个堆,首先我们可以将这n个结点以自顶向下、从左到右的方式从1到n编码,即以层序遍历的顺序编码,这样就可以把这n个结点转换成为一棵完全二叉树。紧接着从最后一个非叶结点(结点编号为n/2)开始到根结点(结点编号为1),逐个扫描所有的结点,根据需要将当前结点向下调整,直到以当前结点为根结点的子树符合堆的特性。
下面以建立最小堆为例:
#include <stdio.h>
#define maxn 105
int h[maxn]; //用来存放堆
int n; //堆的大小
//交换堆中的两个元素的值
void swap(int x,int y)
{
int t=h[x];
h[x]=h[y];
h[y]=t;
}
//向下调整成最小堆
void siftdown(int pos)
{
int t,flag=0; //flag用来标记是否需要继续向下调整
while(!flag)
{
int t=pos; //用t记录父结点和左右儿子中值较小的结点编号
if(pos*2<=n&&h[t]>h[pos*2]) t=pos*2;
if(pos*2+1<=n&&h[t]>h[pos*2+1]) t=pos*2+1;
//如果最小的结点不是父结点
if(t!=pos)
{
swap(t,pos);
pos=t;
}
else flag=1;
}
}
//建堆
void create()
{
//从最后一个非叶结点到第1个结点依次进行向下调整
for(int i=n/2;i>=1;i--)
siftdown(i);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
create();
for(int i=1;i<=n;i++) printf(i==n?"%d\n":"%d ",h[i]);
return 0;
}
【堆的插入与删除】
若最小(大)堆要进行删除最小(大)数并返回最小(大)数的操作,我们只需要删掉堆顶元素即最小(大)数,将最后一个数放在堆顶,通过比较与左右儿子的大小向下调整即可恢复为最小(大)堆。
下面以最小堆为例。
//向下调整成最小堆
void siftdown(int pos)
{
int t,flag=0; //flag用来标记是否需要继续向下调整
while(!flag)
{
int t=pos; //用t记录父结点和左右儿子中值较小的结点编号
if(pos*2<=n&&h[t]>h[pos*2]) t=pos*2;
if(pos*2+1<=n&&h[t]>h[pos*2+1]) t=pos*2+1;
//如果最小的结点不是父结点
if(t!=pos)
{
swap(t,pos);
pos=t;
}
else flag=1;
}
}
//返回并删除最大的元素
int deletemax()
{
int t=h[1];
h[1]=h[n]; //将堆的最后一个点赋值到堆顶
n--; //堆的元素减少1
siftdown(1); //向下调整
return t; //返回最大值
}
若最小(大)堆要进行删除最小(大)数并插入一个新的数的操作,我们只需要删掉堆顶元素即最小(大)数,将新的数放在堆顶,通过比较与左右儿子的大小向下调整即可恢复为最小(大)堆。
若最小(大)堆只需进行插入一个新的数,我们只需要把新的数放在末尾,通过与父亲的比较向上调整即可恢复最小(大)堆。
下面以最小堆为例。
//向上调整成最小堆
void siftup(int pos)
{
int flag=0; //用来标记是否需要继续向上调整
if(pos==1) return; //如果是堆顶,就不需要调整了
//不在堆顶 并且 当前结点pos的值比父结点小的时候继续向上调整
while(pos!=1&&!flag)
{
//判断是否比父结点的小
if(h[pos]<h[pos/2]) swap(pos,pos/2);
else flag=1;
pos=pos/2; //更新编号pos为它父结点的编号
}
}
【堆排序(Heap Sort)】
堆排序在寻找最小值(或最大值)的过程中使用了堆这种数据结构,提高了效率。堆是一个近似二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
那么我们要如何实现堆排序呢?下面以升序排序为例讲解。
算法一:
将需要排序的序列建堆,调整成最小堆。
每次删除并输出堆顶结点的值即最小值,再将最后一个数放置于堆顶结点的位置,最小堆的大小-1,向下调整成最小堆。重复上述步骤直到堆为空。
算法二:
将需要排序的序列建堆,调整成最大堆。
每次把堆顶结点的值即最大值与最后一个结点交换位置,最大堆的大小-1,向下调整成最大堆。重复上述步骤直到结束。
最后层序遍历输出堆排序后的堆,即为升序序列。
下面为算法二的代码,以升序堆排序为例:
#include <stdio.h>
#define maxn 105
int h[maxn];
int n;
//交换函数
void swap(int x,int y)
{
int t=h[x];
h[x]=h[y];
h[y]=t;
}
//向下比较调整成最大堆
void siftdown(int pos,int num)
{
int t,flag=0; //flag用来标记是否需要继续向下调整
while(!flag)
{
int t=pos; //用t记录父结点和左右儿子中值较大的结点编号
if(pos*2<=num&&h[t]<h[pos*2]) t=pos*2;
if(pos*2+1<=num&&h[t]<h[pos*2+1]) t=pos*2+1;
//如果最大的结点不是父结点
if(t!=pos)
{
swap(t,pos);
pos=t;
}
else flag=1;
}
}
void create()
{
//从最后一个非叶结点到第1个结点依次进行向下调整
for(int i=n/2;i>=1;i--)
siftdown(i,n);
}
//堆排序(升序)
void heapSort()
{
create(); //建堆
int num=n;
for(int i=n;i>1;i--)
{
swap(1,i); //交换最大值与最后一个数
num--;
siftdown(1,num); //前num个数调整成最大堆
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
heapSort();
for(int i=1;i<=n;i++) printf(i==n?"%d\n":"%d ",h[i]);
return 0;
}
堆排序是不稳定的排序。最差时间复杂度为O(nlogn),平均时间复杂度为O(nlogn)。