二叉树基础及堆的实现

复习数据结构

顺序表:

  1. 数组:
    缺点:
  • 中间或者头部插入删除数据要挪动数据,效率低
  • 空间不够,要扩容
  • 按倍数扩容,存在空间浪费
    优点:
  • 下标随机访问,排序,二分查找方便
  • CPU高速缓存命中率高,缓存利用率高
  1. 链表
    优点:
  • 任意位置插入删除效率高
  • 按需申请释放,不存在扩容
    缺点:
  • 不能下标随机访问
  • CPU高速缓存命中率低
  1. 栈和队列
    底层还是数组或者链表
    先进先出,后进先出

注意:

  • 链表和顺序表是互补的数据结构
  • 顺序表是一种抽象的数据结构,他可以用数组,链表等其他数 据结构来实现
  • 数组是一种数据结构,在内存中以连续的方式存储相同类型元素,数组可以是一维或多维,具有随机访问和快速访问,适合顺序表
  • 可以说数组是顺序表的一种实现方式
    如:数组实现:顺序表,哈希表,堆,B树

1. 树的概念(族谱)及结构

二叉树,多叉树,结构为树形,更复杂
二叉树
多叉树

1.1 节点的度

     一个结点含有的子树的个数称为结点的度,如上图:A的为6

1.2 叶节点或终端结点(重要)

     度为0的结点称为叶结点;如上图:B,C,H,I等结点为叶结点

1.3 非终端结点或分支结点(重要)

     度不为0的结点;如上图:D,E,F,G等结点为分支节点

1.4 双亲结点或父结点(重要)

     若一个结点含有子结点,则这个节点称为其子结点的父结点;如上图:A是B的父节点

1.5 孩子结点或子结点(重要)

    一个结点含有的子树的跟结点称为该结点的子结点;如上图:B是A的孩子结点

1.6 兄弟结点

    具有相同父结点的结点称为兄弟结点;如上图:B,C是兄弟结点

1.7 树的度

    一棵树中,最大的结点的度称为树的度;如上图:树的度为6

1.8 结点的层次(重要)

    从跟开始定义起,跟为第一层,跟的子结点为第二层,以此类推

在这里插入图片描述

1.9 树的高度或深度

    树中结点的最大层次,如上图:树的高度为4

1.10 堂兄弟结点

     双亲在同一层的结点互为堂兄弟,如上图,H,I互为堂兄弟结点

1.11 结点的祖先

      从跟到该结点所经分支上的所有结点,如上图,A是所有结点的祖先

1.12 子孙

      以某结点为根的子树中任一结点都称为该节点的子孙,如上图,所有节点都是A的子孙

1.13 森林

       由m(m>0)棵互不相交的树的集合称为森林;

树与非树:

  1. 子树是不相交的
  2. 除了根结点外,每个结点有且只有一个父结点(树没有相交的结点)
    如果出现相交的,是图
  3. 一颗N个结点的树有N-1条边

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它的根朝上,而叶朝下。

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点
  • 除根结点外,其余结点被分成M(M>0)个不相交的集合T1,T2…,Tm,其中每一个集合Ti(1<=i<=m)又是一颗结构与树类似的子树。每棵树的根节点有且只有一个前驱,可以有0个或多个后继
  • 因此,树是递归定义的

树的存储方式

树结构相对线性表来说就较为复杂,要储存表示起来就较为麻烦,既要保存值域,又要保存结点与结点之间的关系,实际中树有很多种表示方式:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法等

struct TreeNode
{
   int val;
   struct TreeNode*child1;
   struct TreeNode*child2;
   struct TreeNode*child3;
   //......
   //不确定有几个子节点
}

#define N 3
struct TreeNode
{
   int val;
   struct TreeNode*childArr[N];
}  //顺序表存储孩子指针

1. 左孩子右兄弟(孩子兄弟表示法)

struct TreeNode
{
   int val;
   struct TreeNode*firstchild;
   struct TreeNode*nextbrother;
}

孩子兄弟表示图
这种结构,只需要遍历类似链表的结构就可以找出一个结点的所有子节点在这里插入图片描述

2. 双亲表示法

双亲表示法,只存储双亲的下标或指针,一般是数组实现
双亲表示法示例图
森林的示例
两个结点有没有在同一棵树上,只需要找他们的根是否相同(跟无父亲,用-1表示)

3. 二叉树概念及结构

3.1概念

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵分别称为左子树和右子树的二叉树组成

在这里插入图片描述

  1. 二叉树不存在度大于2的结点,最多两个孩子,可以为1或者是0
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序数
    二叉树可以出现以下情况:
    在这里插入图片描述

3.2 特殊二叉树

1.满二叉树

一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,切结点总数是2^k-1,则为满二叉树

2.完全二叉树

完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为k的,有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应时,称之为完全二叉树,

满二叉树是一种特殊的完全二叉树

在这里插入图片描述

  1. 满二叉树:
  • 每一层都是2^(i-1)个结点 每一层都是满的
    高度为h的满二叉树有多少个结点:
    F(h)=20+21+22+…………2(h-2)+2^(h-1);
    F(h)=2^h-1;(等比求和,错位相减)
    假设这棵满二叉树的结点是N个
    N=2^h-1;
    h=log2(N+1)
    如果N是10亿,h约等于30
  1. 完全二叉树:
  • 假设他的高度是h,前h-1层是满的,最后一层不一定满,从左到右是连续的
    节点范围[2(h-1),2h-1]

4. 堆的实现

1.逻辑实现

在这里插入图片描述
任意位置通过下标可以找到父亲和孩子
如果是非完全二叉树
在这里插入图片描述

满二叉树和完全二叉树适合用数组储存

适合这种结构–

栈:线性表,后进先出
堆:非线性结构,二叉树(完全二叉树),和内存的堆不一样,适    合用数组存储
小堆:树中的任意一个父亲(值)都小于等于孩子
大堆:树中的任意一个父亲(值)都大于等于孩子
底层:物理结构,数组
     逻辑结构,完全二叉树
小堆,底层数组是否升序?(不一定)
小堆的根是整棵树的最小值(topk,堆排序)->O(N*logN)

堆只分大堆和小堆
在这里插入图片描述
堆只是想象出来的结构,实际上的储存还是按照数组的方式储存,不管是插入还是删除数据,都是在数组上面修改在这里插入图片描述
在这里插入图片描述

1. 不管是大数字还是小数字,先要将其插入数组,数组尾插方便就先尾插

2. 尾插后,这个数字的值不会对其他分支的结点产生影响,只会与自己的父亲结点相比

3. 可以使用parent=(leftchild-1)/2或者(rightchild-2)/2来算出父结点的下标
  50下标为6,(6-2)/2=2,也就是父结点,然后进行数据交换

在这里插入图片描述
如果小堆插入的数字小于根结点,则需要一直替换直到根结点最小

堆的代码实现

int main()
{
        int a[] = { 65,100,70,32,50,60 };
        HP hp;
        HeapInit(&hp);
        for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); i++)
        {
                HeapPush(&hp, a[i]);
        }
        HeapPrint(&hp);
        HeapDestory(&hp);

    return 0;
}

符合小堆请添加图片描述

3.1堆的结构体

和创建顺序表类似,先有个动态数组

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
 typedef int HPdatatype;
 typedef struct Heap
 {
     HPdatatype* a;
     int size;
     int capacity;
 }HP;

3.2.初始化堆

void HeapInit(HP*php)
{
   assert(php);
   php->a=NULL;
   php->size=0;
   php->capacity=0;
}    

3.3 销毁堆

void HeapDestory(HP*php)
{
    free(php->a);
    php->a=NULL;
    php->size=0;
    php->capacity=0;
}

3.4 入堆

在这里插入图片描述

void HeapPush(HP*php,HPdatatype x)
{
    assert(php);
    if(php->size==php->capacity)
    {
        int newCapacity=(php->capacity==0?4:2*php->capacity);
        HPdatatype*tmp=(HDdatatype*)realloc(php->a,newCapacity*sizeof(HPdatatype));
        if(tmp==NULL)
        {
            perror("realloc falied");
            exit(-1);
        }
        php->a=tmp;
        php->capacity=newCapacity;
    }
    php->a[php->size++]=x;
    AdjustUp(php->a,php->size-1);
}

3.向上调整

向上调整,前提:前面的数据是大堆/小堆

void Swap(HPdatatype*p1,HPdatatype*p2)
{
    HPdatatype tmp=*p1;
    *p1=*p2;
    *p2=tmp;
}
void AdjustUp(HPdatatype*a,int child)
{
    int parent=(child-1)/2;
    while(child>0)
    {
        if(a[child)<a[parent])
        {
            Swap(&a[child],&a[parent]);
            child=parent;
            parent=(parent-1)/2;
        }
        else
        {
            break;
        }
    }
}

3.6输出

void HeapPrint(HP*php)
{
 assert(php);
 for(size_t i=0;i<php->size;i++)
 {
     printf("%d  ",php-a[i]);
 }
 printf("\n");
}       

3.7删除数据

删除根结点
1.数据向前覆盖(failed)
覆盖前的小堆
覆盖后

上面的数据是小堆,如果采用覆盖的方法,覆盖后的不能构成堆(要么大堆,要么小堆)
  1. 首尾交换
    请添加图片描述
    根和最后一个值交换,然后再删除
    这种方法是向下调整,前提:左右子树是大堆/小堆
    请添加图片描述然后将转化后的根,与两个子结点比较,和小的结点互换,直到无结点可换,或者两个子结点都大于该结点(小堆)
void HeapPop(HP*php)
{
    assert(php);
    assert(php->size>0);
    
    swap(&php->a[0],&php->a[php->size-1]);
    --php->size;
    
    Adjustdown(php->a,php->size,0);
}

时间复杂度:O(logN)(也就是树的层数)

同时,通过这个方法,可以找到第二小的元素(大于删除的根)
加入输出根的这段代码,就可以得到排序号的一串树

        while (!HeapEmpty(&hp))
        {
                printf("%d   ", HeapTop(&hp));
                HeapPop(&hp);
        }

在这里插入图片描述

3.8向下调整位置

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;
        }
    }
}  

3.9返回根的值

HPdatatype HeapTop(HP* php)
{
    assert(php);
    assert(php->size>0);
    
    return php->a[0];
}

3.10判断是否为空

bool HeapEmpty(HP*php)
{
    assert(php);
    
    return php->size==0;
}

3.11改变向上和向下调整的符号

void AdjustUp(HPdatatype* a,int child )
{
        int parent = (child - 1) / 2;
        while(child>0)
        {
                if (a[child] > a[parent])
                {
                        swap(&a[child], &a[parent]);
                        child = parent;
                        parent = (parent - 1) / 2;
                }
                else
                {
                        break;
                }
        }
}

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;
                }
                if (a[child] > a[parent])
                {
                        swap(&a[child], &a[parent]);
                        parent = child;
                        child = parent * 2 + 1;
                }
                else
                {
                        break;
                }
        }
}

大堆
我们就可以得到一个由大到小的排列
第一行为大堆
第二行为不断根删的结果
在这里插入图片描述

3.12建堆

通过上面建堆,不断删除根节点,从而排出数组的顺序

void HeapSort(int *a,int n)
{
    int a[] = { 60,100,70,32,50,65 };
        HP hp;
        HeapInit(&hp);
        for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); i++)
        {
                HeapPush(&hp, a[i]);
        }
        HeapPrint(&hp);
        int i=0;
        while (!HeapEmpty(&hp))
        {
                printf("%d   ", HeapTop(&hp));
                a[i++]=HeapTop(&hp);
                HeapPop(&hp);
        }
        HeapDestory(&hp);
 }

在这里插入图片描述
方法缺点:

  1. 先有个堆的数据结构
  2. 空间复杂度的消耗大
    建堆输入数组的值太麻烦,干脆直接对修改输入的数组
向上建堆处理

向上调整和向下调整只能是,下方已经是堆的情况,所以对于下方不是堆的不能直接使用向上和向下调整
直接向上处理不能将数组转换成堆
在这里插入图片描述

请添加图片描述

原数组不是堆,我们可以通过变换,将原数组转化成堆,虽然不能直接使用,但是我们可以借助这个思想
在这里插入图片描述
我们可以先把70单独看做一个堆,然后通过数列后面的元素不断与根元素替换请添加图片描述
请添加图片描述

void HeapSort(int*a,
{
    for(int i=0;i<n;i++)
    {
        AdjustUp(a,i);
    }
}

3.14堆排序

堆排序是一种选择排序
升序;建大堆
堆顶跟最后一个位置交换,最大的数据排好了,剩下的数据向下调整,选出次大的,代价是logN
请添加图片描述
向下调整需要下方为大堆
先找到非叶子结点的最后一个结点,然后与下方最大的子比较交换,将下方转换为大堆,此时上方节点可以通过向下调整转换大堆请添加图片描述

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;
                }
                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 end = n - 1;
        
        //时间复杂度N*(logN)
        while (end > 0)
        {
                swap(&a[0], &a[end]);
                Adjustdown(a, end, 0);
                --end;
        }
}

在这里插入图片描述
在这里插入图片描述

降序:建小堆

void HeapSort(int* a, int n)
{
        for (int i = 0; i < n; i++)
        {
                AdjustUp(a, i);
        }
        int end = n - 1;
        while (end > 0)
        {
                swap(&a[0], &a[end]);
                Adjustdown(a, end, 0);
                --end;
        }    
}

在这里插入图片描述

4. topk问题

假设有10亿个数据,要找到最大的前k个
k=10;

1. 读取文件前10个数据,在内存数组中建立一个小堆
2. 在依次读取剩下的数据,跟堆顶进行比较,大于堆顶,就替换他进堆,向下调整
3. 所有数据读完,堆里面数据就是最大的前10个

创建一个含有10000个随机数的文件

void DataCreate()
{
      int n = 10000;
      FILE* fin = fopen("data.txt", "w");
      //以写入打开文件
      if (fin == NULL)//判断文件是否打开成功
      {
              perror("fopen failed");
              return;
      }
      srand(time(0));
      for (int i = 0; i < n; i++)
      {
              int x = (rand() + i) % 10000000;
              fprintf(fin, "%d\n", x);//写入文件
      }
      fclose(fin);
}

先创建一个k空间的内存,然后将10000个数据中的k个数据传入,组成一个小堆
然后后续进来的值都与根相比,直到所有的数据都遍历过

void Testtopk(int k)
{
        int* a = (int*)malloc(k * sizeof(int));
        FILE* fout = fopen("data.txt", "r");
        if (fout == NULL)
        {
                perror("fopen failed");
                exit(-1);
        }//打开文件
        int n = 0;
        for (int i = 0; i < k; i++)
        {
                fscanf(fout, "%d", &a[i]);
        }
        //前k个数建小堆
        for (int i = (k - 2) / 2; i > 0; i--)
        {
                AdjustDown(a, k, i);
        }
        int x = 0;
        //遍历所有数据
        while (fscanf(fout, "%d", &x) != EOF)
        {
                if (x > a[0])
                {
                        a[0] = x;
                        AdjustDown(a, k, 0);
                }
        }
        //输出堆的值
        for (int i = 0; i < k; i++)
        {
                printf("%d  ", a[i]);
        }
        printf("\n");

        fclose(fout);
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值