数据结构之树,实现堆的增删改查接口及堆的应用

目录

一、树

1.树的概念及结构

1.1树的概念

1.2树的相关概念

1.3树的表示

2.二叉树的概念及结构

2.1二叉树的概念

2.2特殊的二叉树

2.3二叉树的性质

2.4二叉树的存储结构

3.二叉树的顺序结构及实现

3.1二叉树的顺序结构

3.2堆的概念及结构

二、堆的实现

0.定义堆

1.初始化堆

2.堆的创建

3.堆的删除

4.堆的判空问题

5.堆的大小

6.堆的销毁

三、堆的应用

1.堆排序

2.topK问题

总结


一、树

1.树的概念及结构

1.1树的概念

树是一种非线性的数据结构,它是由N(N>=0)个有限节点组成一个具有层次关系的集合,叫它为树是因为它看起来像一颗倒挂的树,根朝上,叶子朝下

  • 树有一个特殊的节点,称为根节点,根节点没有前驱节点
  • 除了根节点外,其余节点被分为M个互不相交的集合,其中每一个集合又是一颗结构与树类似的子树,每棵子树的根节点有且只有一个前驱,可以有0或多个后继,因此树是递归定义的。

1.2树的相关概念

  • 节点的度:一个节点含有的子树的个数称为该节点的度
  • 叶节点或终端节点:度为0的节点称为叶子节点
  • 树的度:一个树中,最大的节点的度称为树的度

1.3树的表示

树需要保存值域,也需要保存节点和节点之间的关系,其中树有很多表示方法,如双亲表示法,孩子表示法,双亲孩子表示法以及孩子兄弟表示法,常用孩子兄弟表示法

typedef int DataType;
struct Node
{
    struct Node * child;
    struct Node * brother;
    DataType data;
};

2.二叉树的概念及结构

2.1二叉树的概念

一棵二叉树是节点的一个有限集合,该集合或者为空,或者由一个根节点加上两棵分别称为左子树和右子树的二叉树组成

 从上图可以看出,二叉树不存在度大于2的节点;二叉树有左右之分,次序不能颠倒,因此二叉树是有序树

2.2特殊的二叉树

  1. 满二叉树:一个二叉树,如果每次的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且节点总数为2^K-1,则它就是满二叉树
  2. 完全二叉树:完全二叉树是效率很高的数据结构,对于深度为K的有N个节点的二叉树,当且仅当每个节点都与深度为K的满二叉树从1到N的节点一一对应,就成为完全二叉树

2.3二叉树的性质

  • 若规定根节点为第一层,则一棵非空二叉树的第i层上最多有2^(i-1)个节点
  • 若规定根节点为第一层,则深度为h的二叉树的最大节点数是2^h-1
  • 对于任意一棵二叉树,如果度为0的叶子节点个数为N0,度为2的分支节点为N2,N0 = N2+1
  • 若规定根节点的层数为1,具有n个节点的满二叉树的深度,h=log2(n+1)
  • 对于一个具有n个节点的完全二叉树,按照从上至下从左至右的数组顺序对所有节点从0开始编号,对于序号为i的节点:
    • 若i>0,i节点的双亲序号:(i-1)/2; i=0,i为根节点编号,无双亲节点
    • 若2i+1<n,左孩子序号:2i+1,2i+1>n无左孩子
    • 若2i+2<n,右孩子序号:2i+2,2i+2>n无右孩子

2.4二叉树的存储结构

二叉树一般可以使用两种存储结构,一种是顺序结构,一种是链式结构

1.顺序存储

顺序结构存储就是用数组来存储,一般数组只适合表示完全二叉树,不是完全二叉树会有空间的浪费,现实中只有堆使用数组来存储,二叉树顺序存储在物理结构上是一个数组,在逻辑上是一颗二叉树。

 2.链式存储

二叉树的链式存储是使用链表表示一颗二叉树,即用链来指示元素的逻辑关系,通常是链表中的每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出左右孩子所在链节点的存储地址,链式结构分为二叉链和三叉链,一般使用二叉链,红黑树等会用到三叉链

typedef int BTDataType;

//二叉链
struct BinaryTreeNode
{
    struct BinaryTreeNode * leftchild;
    struct BinaryTreeNode * rightchild;
    BTDataType    data;
};


//三叉链
struct BinaryTreeNode
{
    struct BinaryTreeNode *parent;
    struct BinaryTreeNode * leftchild;
    struct BinaryTreeNode * rightchild;
};

3.二叉树的顺序结构及实现

3.1二叉树的顺序结构

普通的二叉树一般不使用数组存储,因为会造成大量的空间浪费,完全二叉树适合顺序存储。

3.2堆的概念及结构

如果有一个关键码的集合K={k0,k1,k2,k3...kN-1},把它所有的元素按完全二叉树的顺序存储方式存储到一维数组中,并满足:Ki<=K2i+1,且Ki<=K2i+2(或者小于)则称为小堆,或者大堆,将根节点最大的堆叫做最大堆或者大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

      堆中某个节点的值总是不大于或不小于父节点的值;

       堆总是一颗完全二叉树

二、堆的实现

0.定义堆

堆的逻辑结构是一棵完全二叉树,物理结构为一个数组,要想实现堆的插入删除,需要再定义size和capacity两个值,分别表示当前完全二叉树中存储的有效数据和完全二叉树的容量。

typedef int HPDataType;

typedef struct
{
    HPDataType * a;
    int size;
    int capacity;
}HP;

1.初始化堆

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

2.堆的创建

堆的创建需要先插入节点,插入节点后需要保证仍然是一个堆,则需要调整顺序,即根据定义是大堆或小堆进行调整

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])   //如果要创建小堆 if(a[child] >a[parent])进行调整
        {
            Swap(&a[child],&a[parent]);  //传递的是数组的值,所以类型为HPDataType,传递的是一个地址,所以用一个指针接收,将child指向的值换为parent指向的值,下标没变
            child = parent; //向上走,更换下标
            parent = (child -1)/2;  //重新找到parent节点
        }
        else
        {
            break;
        }
}


void HeapPush(HP * php,HPDataType x)
{
    assert(php);
    //插入前先判断容量,如果不够需要对数组扩容
    if(php->size == php->capacity)
    {
        int newCapacity = php->capacity == 0?4:php->capacity *2;
        HPDataType * tmp = realloc(php->a,sizeof(HPDataType)*newCapacity);
        if(tmp ==NULL)
            exit(-1);
        php->a = tmp;
        php->capacity = newCapacity;
    }
    //先插入,再调整
    php->a[php->size] = x;
    php->size++;
    
    //如果默认建大堆,则插入的节点向上调整
    //向上调整的时候,调整的是数组的顺序
    //以及知道当前数据的下标
    
    AdjustUp(php->a,php->size-1);
}


//形参为定义的堆,要实现堆结构的数组,数组的个数
void HeapCreate(HP * php,HPDataType * a,int n)
{
    assert(php);
    HeapInit(php);
    for(int i = 0;i<n;i++)
    {
        //依次插入,插入到哪个堆,以及数据
        HeapPush(php,a[i]);
    }
}

3.堆的删除

同堆的插入问题,删除节点后需要仍然保持堆的有序问题,对于顺序表来说,头删时间复杂度为O(N),尾删时间复杂度为O(1),所以删除堆顶数据时,先和最后一个交换顺序,删除表尾的数据,然后再根据为大堆或小堆,进行调整堆的顺序。


void AdjustDown(HPDataType * a, int n, int parent)
{
    int child = 2*parent +1;
    while(child < n )
    {
        //此时可能左孩子大,也可能右孩子大,需要和大的交换,上面定义左孩子
        //child + 1 <n 表示的意思是:可能没有右孩子,如果没有就不++
        if(a[child] <a[child+1]  && child+1 <n)
        {
            ++child;
        }
        
       //向下交换
        if(a[parent] <a[child])    //此时,如果要调整为小堆 箭头 > 
        {
            Swap(&a[parent],&a[child]);
            parent = child;
            child = 2*parent +1;
        }
        else
        {
            break;
        }
}
    
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);
}

4.堆的判空问题

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

5.堆的大小

int HeapSize(HP * php)
{
    assert(php);
    return php->size;
  }

6.堆的销毁

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

三、堆的应用

1.堆排序

堆排序即利用堆的思想进行排序,总共分为两个步骤:

  1. 建堆 升序 建大堆 降序 建小堆
  2. 利用堆删除思想进行排序

实现代码如下:

void HeapSort(int * a, int n)
{

    //如果是升序,建立一个大堆

    //每次选出次大的数
    for(int i = (n-1-1)/2 ;i>=0; --i)
    {
        AdjustDown(a,n,i);
    }


    int end = n-1;

    while(end > 0)
    {
        Swap(&a[0],a[end]);
        AdjustDown(a,end,0);
        --end;
    }

}

2.topK问题

      即求数据中前K个最大的元素或者最小的元素,一般数据量较大。对于topK问题,最简单的就是排序,但是数据量非常大的时候,排序不可取,最佳方式用堆解决,

思路1:建立N个数的大堆,popK次,依次取堆顶元素

假设N很大,K很小,N=100亿,K=10,100亿个整数,需要100亿*4字节(40G)的空间,有可能数据量太大存不到内存,放到磁盘中 。

改进思路2如下:

        建立前K个数的小堆,依次遍历数据,比堆顶的数据大,就替换堆顶,再向下调整,最后最大的K个数就在这个小堆里面,这样的好处就在于节约了空间,时间复杂度O(N * logK),空间复杂度O(K);前K个最大的元素,建立小堆;前K个最小的元素,建立大堆。

//思路1
HPDataType HeapPop(HP * php)
{
    asser(php);
    assert(php->size >0);
    return php->a[0];
}


void TestTopK()
{
    //根据现有的数据建立堆 建立大或小在push中定义
    int a[] = {27,15,19,18,28,34,65,49,25,37};
    HP hp;
    HeapInit(&hp);
    for(int i = 0; i<sizeof(a)/sizeof(int);i++)
    {
        HeapPush(hp,a[i]);
    }
    //取前5个
    int k = 5;
    while(k--)
    {
        printf("%d" ,HeapTop(&hp);
        HeapPop(&hp);
    }
    HeapDestroy(&hp);
}

思路2的实现代码:

//先建立一个K个数的小堆

for(int i = (k-1-1)/2;i>=0;--i)
{
    AdjustDown(miniHeap,k,i);
}

//然后读取数据 把大的数更换堆顶,然后向下调整
while(fscanf(file)!=EOF)
{
    minHeap[0] = val;
    AdjustDown(minHeap,k,0);
}

总结

本文主要介绍了树的概念及性质,堆的概念及增删改查的接口,堆的应用堆排序以及topK问题,技术有限,如有错误请指正。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值