二叉树和堆(理论)

开启树之旅

1.树其实就是不包含回路的连通无向图。

2.一棵树中的任意两个结点有且仅有唯一的一条路径连通。

3.一棵树如果有n个结点,那么它一定恰好有n-1条边。

二叉树

二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。

【满二叉树与完全二叉树】

如果二叉树中每个内部结点都有两个儿子,这样的二叉树叫做满二叉树。


如果一棵二叉树除了最右边位置上一个或者几个叶结点缺少外其它是丰满的,那么这样的二叉树就是完全二叉树。


【完全二叉树如何储存】完全二叉树中父亲和儿子之间有着神奇的规律,我们只需用一个一维数组就可以存储完全二叉树。首先将完全二叉树进行从上到下,从左到右编号。我们发现如果完全二叉树的一个父结点编号为k,那么它左儿子的编号就是2*k,右儿子的编号就是2*k+1。如果已知儿子(左儿子或右儿子)的编号是x,那么它父结点的编号就是x/2。

堆(上)

堆是什么?堆是一种特殊的完全二叉树。

所有父结点都比子结点要小的完全二叉树我们称为最小堆。反之,如果所有父结点都比子结点要大,这样的完全二叉树称为最大堆。

若最小堆要进行多次删掉最小数并插入一个新的数的操作,我们只需要删掉堆顶元素即最小数,将新的数放在堆顶,通过比较与左右儿子的大小向下调整即可恢复为最小堆。

void siftdown(int i) //传入一个需要向下调整的结点编号i,这里传入1,即从堆的顶点开始向下调整 
{
    int t,flag=0;//flag用来标记是否需要继续向下调整 
    //当i结点有儿子的时候(其实是至少有左儿子的情况下)并且有需要继续调整的时候循环窒执行
    while( i*2<=n && flag==0 )
    {        
        //首先判断他和他左儿子的关系,并用t记录值较小的结点编号 
        if( h[ i] > h[ i*2] )
            t=i*2;
        else
            t=i; 
        //如果他有右儿子的情况下,再对右儿子进行讨论 
        if(i*2+1 <= n)
        {
            //如果右儿子的值更小,更新较小的结点编号  
            if(h[ t] > h[ i*2+1])
                t=i*2+1;
        }
        //如果发现最小的结点编号不是自己,说明子结点中有比父结点更小的  
        if(t!=i)
        {
            swap(t,i);//交换它们,注意swap函数需要自己来写
            i=t;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整 
        }
        else
            flag=1;//则否说明当前的父结点已经比两个子结点都要小了,不需要在进行调整了 
    }
}

若最小堆只需进行插入一个新的数,我们只需要把新的数放在末尾,通过与父亲的比较向上调整即可恢复最小堆。

void siftup(int i) //传入一个需要向上调整的结点编号i
{
    int flag=0; //用来标记是否需要继续向上调整
    if(i==1)  return; //如果是堆顶,就返回,不需要调整了    
    //不在堆顶 并且 当前结点i的值比父结点小的时候继续向上调整 
    while(i!=1 && flag==0)
    {
        //判断是否比父结点的小 
        if(h[ i]<h[ i/2])
            swap(i,i/2);//交换他和他爸爸的位置 
        else
            flag=1;//表示已经不需要调整了,当前结点的值比父结点的值要大 
        i=i/2; //这句话很重要,更新编号i为它父结点的编号,从而便于下一次继续向上调整 
    }
}

堆(下)

【堆的创建】

n个元素建立一个堆,首先我可以将这n结点以自顶向下、从左到右的方式从1n编码,这样就可以把这n结点转换成为一棵完全二叉树。紧接着从最后一个非叶结点(结点编号为n/2)开始到根结点(结点编号为1),逐个扫描所有的结点,根据需要将当前结点向下调整,直到以当前结点为根结点的子树符合堆的特性。

【堆排序】

比如我们现在要进行从小到大排序,可以先建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入一个新的数组中,直到堆为空为止。

#include <stdio.h>
int h[ 101];//用来存放堆的数组
int n;//用来存储堆中元素的个数,也就是堆的大小  
//交换函数,用来交换堆中的两个元素的值
void swap(int x,int y)
{
    int t;
    t=h[ x];
    h[ x]=h[ y];
    h[ y]=t;
}
//向下调整函数
void siftdown(int i) //传入一个需要向下调整的结点编号i,这里传入1,即从堆的顶点开始向下调整
{
    int t,flag=0;//flag用来标记是否需要继续向下调整
    //当i结点有儿子的时候(其实是至少有左儿子的情况下)并且有需要继续调整的时候循环窒执行
    while( i*2<=n && flag==0 )
    {        
        //首先判断他和他左儿子的关系,并用t记录值较小的结点编号
        if( h[ i] > h[ i*2] )
            t=i*2;
        else
            t=i;
        //如果他有右儿子的情况下,再对右儿子进行讨论
        if(i*2+1 <= n)
        {
            //如果右儿子的值更小,更新较小的结点编号  
            if(h[ t] > h[ i*2+1])
                t=i*2+1;
        }
        //如果发现最小的结点编号不是自己,说明子结点中有比父结点更小的  
        if(t!=i)
        {
            swap(t,i);//交换它们,注意swap函数需要自己来写
            i=t;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整
        }
        else
            flag=1;//则否说明当前的父结点已经比两个子结点都要小了,不需要在进行调整了
    }
}
//建立堆的函数
void creat()
{
    int i;
    //从最后一个非叶结点到第1个结点依次进行向上调整
    for(i=n/2;i>=1;i--)
    {
        siftdown(i);
    }  
}
//删除最大的元素
int deletemax()
{
    int t;
    t=h[ 1];//用一个临时变量记录堆顶点的值
    h[ 1]=h[ n];//将堆得最后一个点赋值到堆顶
    n--;//堆的元素减少1
    siftdown(1);//向下调整
    return t;//返回之前记录的堆得顶点的最大值
}
int main()
{
    int i,num;
    //读入数的个数
    scanf("%d",&num);
    for(i=1;i<=num;i++)
        scanf("%d",&h[ i]);
    n=num;   
    //建堆
    creat();
    //删除顶部元素,连续删除n次,其实夜就是从大到小把数输出来
    for(i=1;i<=num;i++)
        printf("%d ",deletemax());
    getchar();
    getchar();
    return 0;
}

当然堆排序还有一种更好的方法。从小到大排序的时候不建立最小堆而建立最大堆。

#include <stdio.h>
int h[ 101];//用来存放堆的数组
int n;//用来存储堆中元素的个数,也就是堆的大小
//交换函数,用来交换堆中的两个元素的值
void swap(int x,int y)
{
    int t;
    t=h[ x];
    h[ x]=h[ y];
    h[ y]=t;
}
//向下调整函数
void siftdown(int i) //传入一个需要向下调整的结点编号i,这里传入1,即从堆的顶点开始向下调整
{
    int t,flag=0;//flag用来标记是否需要继续向下调整
    //当i结点有儿子的时候(其实是至少有左儿子的情况下)并且有需要继续调整的时候循环窒执行
    while( i*2<=n && flag==0 )
    {        
        //首先判断他和他左儿子的关系,并用t记录值较大的结点编号
        if( h[ i] < h[ i*2] )
            t=i*2;
        else
            t=i;
        //如果他有右儿子的情况下,再对右儿子进行讨论
        if(i*2+1 <= n)
        {
            //如果右儿子的值更大,更新较小的结点编号  
            if(h[ t] < h[ i*2+1])
                t=i*2+1;
        }
        //如果发现最大的结点编号不是自己,说明子结点中有比父结点更大的  
        if(t!=i)
        {
            swap(t,i);//交换它们,注意swap函数需要自己来写
            i=t;//更新i为刚才与它交换的儿子结点的编号,便于接下来继续向下调整
        }
        else
            flag=1;//则否说明当前的父结点已经比两个子结点都要大了,不需要在进行调整了
    }
}
//建立堆的函数
void creat()
{
    int i;
    //从最后一个非叶结点到第1个结点依次进行向上调整
    for(i=n/2;i>=1;i--)
    {
        siftdown(i);
    }  
}
//堆排序
void heapsort()
{
        while(n>1)
        {
                swap(1,n);
        n--;
        siftdown(1);
        }
}
int main()
{
    int i,num;
    //读入n个数
    scanf("%d",&num);
    for(i=1;i<=num;i++)
        scanf("%d",&h[ i]);
    n=num;   
    //建堆
    creat();
    //堆排序
    heapsort();
    //输出
    for(i=1;i<=num;i++)
        printf("%d ",h[ i]);
    getchar();
    getchar();
    return 0;
}



猜你喜欢

转载自blog.csdn.net/qq_41117236/article/details/81029618