0


树、二叉树与堆

一、树的基本知识

二、二叉树的基本知识

三、堆

   堆的基本知识

   初始准备

   初始化和销毁堆

   堆为空

   向上和向下调整堆

   插入和删除数据

   取出堆顶数据

   堆的数据个数

   建堆时间复杂度

   全部代码

   堆排序

四、TOP-K问题

五、二叉树的链式存储


一、树的基本知识

树是一种非线性结构,像是一颗倒挂着的树,根朝上,叶朝下,最上面的节点称为根节点,如下图的A

注意:树形结构中,子树之间不能有交集,否则就不是树形结构,如下图

** **

节点的度:一个节点含有的子树的个数称为该节点的度

如第一张图的A,有2个子树,度就为2

叶节点或终端节点:度为0的节点称为叶节点

如第一张图的D、E、F、G都是叶节点

双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点

如第一张图的C就是G的父节点

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点

如第一张图的B、C都是A的孩子节点

树的高度或深度:树中节点的最大层次

如将根节点视为第一层,树的高度就为3

节点的祖先:从根到该节点所经分支上的所有节点

如第一张图的A是所有节点的祖先

树的表示方法

最常用的是孩子兄弟表示法

根只要指向第一个节点,剩下的节点就可以依次连接,如下图

二、二叉树的基本知识

概念:由一个根节点加上两棵别称为左子树和右子树的二叉树组成(或者为空),如下图

二叉树特点:

不存在度大于2的节点

二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

特殊的二叉树

完全二叉树

前h-1层都有左右节点(根节点除外),第h层从左到右是连续存储的,不存在有右节点,没有左节点的情况

满二叉树

每层都有左右节点,所以满二叉树是特殊的完全二叉树

二叉树的性质

若规定根节点的层数为1,则一棵非空二叉树的第i层上最多2^(i-1)个节点

当其为满二叉树时,第i层节点数最多,即1、2、4...2^(i-1),是一个等比数列

若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2^h-1

当其为满二叉树时,有最大节点数,即1、2、4...2^(h-1),是等比数列,求和为2^h-1

对任何一棵二叉树, 如果度为0其叶结点个数为 , 度为2的分支结点个数为 ,则有n0=n2+1

若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1)

二叉树的存储结构

可以用顺序结构和链式结构两种来存储

不过顺序结构只适用于完全二叉树,普通二叉树存储时,可能会造成大量空间浪费

如上图,存储时,有缺失,则必须空出来,否则会打乱结构

假设parent是父亲节点在数组中下标

leftchild = parent * 2 + 1

rightchild = parent * 2 + 2

假设孩子的下标是child,不管是左孩子还是右孩子

**parent = (child - 1) / 2 **

三、堆

堆的基本知识

概念:

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆

性质:

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

堆总是一棵完全二叉树

小堆:

大堆:

初始准备

为了存储的数据类型能够便于修改,所以用typedef 将其数据类型名修改

这里采用数组结构来存储堆,首先创建一个结构体,有3个成员,第一个是数组的起始地址(指针),第二个是有效数据个数,第三个是已经开辟的数组大小(容量),用typedef将其类型名简化

打印堆的数据

按照下标访问即可,就和静态数组一样

交换两个数

由于后面的向上和向下调整都得用到交换,所以用一个函数来实现,想使用时只需调用即可

初始化和销毁堆

将其后两个成员置为0,将指针置为NULL即可

销毁堆和初始化堆类似,只是要将开辟的空间用free释放

** 堆为空**

** 如果没有数据则就为空,这里采用bool类型,同时要引用头文件stdbool.h**

** 向上和向下调整堆**

** 以建立大堆为例,向上调整**

** **

如上图,如果子节点数据比父节点数据大,两者就交换,且把父节点下标赋值给孩子,父节点下标为(child-1)/ 2,否则就直接跳出循环

不能用parent>=0作循环判断条件,当child = 0时,parent = 0循环还是会继续,然后下一步才会跳出循环,可是并不算正常退出

2个参数,一个数组的起始地址,另一个是孩子节点的下标

以建立大堆为例,向下调整

首先得判断左右孩子节点数据哪个更大,父节点与大的那个交换,同时右孩子节点下标不能越界

再判断孩子节点数据是否比父节点数据大,大就交换,且将孩子节点下标赋值给父节点,孩子节点为2 * parent + 1,否则就跳出循环

**循环判断条件为孩子节点下标小于有效数据个数 **

有3个参数,一个是数组的起始地址,一个是有效数据个数,还有一个是父节点下标

** **

注意:对于向下调整,左右子树必须是一个堆,才能调整

插入和删除数据

插入数据时,首先得判断是否还有空间,没有就增容,同时要判断增容是否失败

插入数据后,有效数据个数要+1,然后再向上调整,保证是大堆或者小堆

删除堆顶数据后,不能挪动数据,否则会打乱堆结构

首先得判断堆是否为空

然后得交换堆顶数据与最后一个数据

再删除最后一个数据,即有效数据个数-1即可

最后还要向下调整堆,保证是大堆或者小堆

取出堆顶数据

首先判断堆是否为空,然后再取数据

堆的数据个数

全部代码

//头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<time.h>

typedef int DataType;

typedef struct Heap
{
    DataType* a;
    int size;
    int capacity;
}HP;

//初始化堆
void HeapInit(HP* hp);
//销毁堆
void HeapDestroy(HP* hp);
//打印
void HeapPrint(HP* hp);
//交换2个数
void swap(DataType* px, DataType* py);
//插入数据进堆
void HeapPush(HP* hp, DataType x);
//删除数据出堆
void HeapPopTop(HP* hp);
//向上调整
void HeapAdjustUp(DataType* a,int child);
//向下调整
void HeapAdjustDown(DataType* a,int n,int parent);
//取出堆顶数据
DataType HeapTop(HP* hp);
//堆为空
bool HeapEmpty(HP* hp);
//堆的大小
int HeapSize(HP* hp);

//定义文件
#include"Heap.h"

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

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

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

void swap(DataType* px, DataType* py)
{
    DataType* tmp = *px;
    *px = *py;
    *py = tmp;
}

void HeapPush(HP* hp, DataType x)
{
    assert(hp);
    if (hp->size == hp->capacity)
    {
        int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
        DataType* tmp = (DataType*)realloc(hp->a, sizeof(DataType) * newcapacity);
        if (tmp == NULL)
        {
            printf("realloc fail\n");
            exit(-1);
        }
        hp->a = tmp;
        hp->capacity = newcapacity;
    }
    hp->a[hp->size] = x;
    hp->size++;
    HeapAdjustUp(hp->a,hp->size-1);
}

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

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

void HeapPopTop(HP* hp) 
{
    assert(hp);
    assert(!HeapEmpty(hp));
    swap(&(hp->a[0]), &(hp->a[hp->size - 1]));
    hp->size--;
    HeapAdjustDown(hp->a, hp->size, 0);
}

void HeapAdjustDown(DataType* a, int n, int parent)
{
    assert(a);
    int child = 2 * parent + 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 = 2 * parent + 1;
        }
        else
        {
            break;
        }
    }
}

DataType HeapTop(HP* hp)
{
    assert(hp);
    assert(!HeapEmpty(hp));
    return hp->a[0];
}

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

测试文件
void test1()
{
    HP hp;
    HeapInit(&hp);
    HeapPush(&hp, 17);
    HeapPush(&hp, 53);
    HeapPush(&hp, 62);
    HeapPush(&hp, 37);
    HeapPush(&hp, 65);
    HeapPush(&hp, 25);
    HeapPrint(&hp);
    int sz = HeapSize(&hp);
    printf("%d\n", sz);

    int Top = HeapTop(&hp);
    HeapPopTop(&hp);
    printf("%d\n", Top);
    sz = HeapSize(&hp);
    printf("%d\n", sz);

    Top = HeapTop(&hp);
    HeapPopTop(&hp);
    printf("%d\n", Top);
    sz = HeapSize(&hp);
    printf("%d\n", sz);

    Top = HeapTop(&hp);
    HeapPopTop(&hp);
    printf("%d\n", Top);
    sz = HeapSize(&hp);
    printf("%d\n", sz);
    HeapDestroy(&hp);
}
int main()
{
    test1();
    return 0;
}

建堆时间复杂度

如上图,把根节点所在层看为第一层,这里用满二叉树,方便计算,多几个节点不影响结果

采用的方法是错位相减法

堆排序

首先创建一个数组,并且赋值,再求数组数据个数

这里排升序,建大堆

然后再先将堆顶数据与最后一个数据交换,再有效数据-1,最后向下调整

最后打印数据即可

四、TOP-K问题

从n个数据中取出k个最大或者最小的数据,一般k远小于n

首先创建一个1000个数的数组,并且都赋值,其中有9个都大于10000

这里以取出前k个最大数据为例,建小堆

后面991个数,满足大于堆顶数据就删除堆顶数据,重新调整,换成新的堆顶数据

这里无论是插入数据还是删除堆顶数据,以及方法二的向下调整都是建小堆

五、二叉树的链式存储

二叉树是:

空树

非空:根节点,根节点的左子树、根节点的右子树组成的。

二叉树定义是递归式的

二叉树的遍历有:前序/中序/后序的递归结构遍历

前序遍历——访问根结点的操作发生在遍历其左右子树之前

**如上图,15 17 24 19 20 36 35 13 24 **

中序遍历——访问根结点的操作发生在遍历其左右子树之中(间)

如上图,19 24 20 17 36 15 13 35 24

后序遍历——访问根结点的操作发生在遍历其左右子树之后

如上图,19 20 24 36 17 13 24 35 15

递归实现前序遍历,下图是递归展开图,为了便于画图,所以减少了数据

前中后序遍历全部代码

//创建节点
BN* BuyBinaryNode(DataType x) 
{
    BN* newnode = (BN*)malloc(sizeof(BN));
    if (newnode == NULL)
    {
        printf("malloc fail\n");
        exit(-1);
    }
    newnode->data = x;
    newnode->leftchild = NULL;
    newnode->rightchild = NULL;
    return newnode;
}

//前序遍历
void prevorder(BN* bn)
{
    if (bn == NULL)
    {
        return NULL;
    }
    printf("%d ", bn->data);
    prevorder(bn->leftchild);
    prevorder(bn->rightchild);
}
//中序遍历
void inorder(BN* bn)
{
    if (bn == NULL)
    {
        return;
    }
    inorder(bn->leftchild);
    printf("%d ", bn->data);
    inorder(bn->rightchild);
}
//后序遍历
void postorder(BN* bn)
{
    if (bn == NULL)
    {
        return;
    }
    postorder(bn->leftchild);
    postorder(bn->rightchild);
    printf("%d ", bn->data);
}
void test3()
{
    BN* bn1 = BuyBinaryNode(15);
    BN* bn2 = BuyBinaryNode(17);
    BN* bn3 = BuyBinaryNode(35);
    BN* bn4 = BuyBinaryNode(24);
    BN* bn5 = BuyBinaryNode(36);
    BN* bn6 = BuyBinaryNode(13);
    BN* bn7 = BuyBinaryNode(24);
    BN* bn8= BuyBinaryNode(19);
    BN* bn9 = BuyBinaryNode(20);

    bn1->leftchild = bn2;
    bn1->rightchild = bn3;
    bn2->leftchild = bn4;
    bn2->rightchild = bn5;
    bn4->leftchild = bn8;
    bn4->rightchild = bn9;
    bn3->leftchild = bn6;
    bn3->rightchild = bn7;
    
    prevorder(bn1);
    //inorder(bn1);
    //postorder(bn1);
}

计算二叉树的节点个数

首先得判断根节点是否为空,为空就返回0

将所有节点都遍历一遍,为空就返回0,不为空就+1

计算二叉树叶子节点的个数

首先得判断根节点是否为空,为空就返回0

将所有节点都遍历一遍,如果左右孩子节点都为空,那么该节点就是叶子节点

计算二叉树第k层的节点个数

首先得判断根节点是否为空,为空就返回0

将根节点视为第一层,首先得判断k是否大于等于1,然后判断k是否为1,为1就返回1

可以转化成第k-1层的所有节点的左右孩子节点的总和

在二叉树中查找值为x的节点

首先得判断根节点是否为空,为空就返回空

如果等于该节点存储的数据等于x,那就返回该节点的地址,说明找到了

如果左孩子或右孩子节点等于空,那就返回空

上述都不满足,就返回空

为了避免在判断左右孩子节点是否为空,以及返回时,多次调用,所以用变量将其保存

求二叉树的高度或层数

首先得判断根节点是否为空,为空就返回0

通过判断左右子树那个大,就+1后返回哪个

为了减少函数在返回语句时的多次调用,所以用变量将其值保存起来

层序遍历

概念:自上而下,自左至右逐层访问树的结点的过程

这里得借用队列来实现

首先得判断根节点是否为空,为空则函数结束直接return

然后创建一个队列的结构体变量,并初始化,再把根节点插入队列中

然后创建一个变量,保存父节点,才能把左右孩子带进队列中

最后销毁队列即可

判断是否是完全二叉树

首先得创建一个队列的结构体变量,并初始化,然后把根节点插入队列中

然后判断保存队头节点并删除,以便能带入左右孩子节点,如果队头节点为空,就直接跳出循环

因为完全二叉树的最后一层是连续存储的,如果后面全是空,那就是完全二叉树,否则就不是

//层序遍历
void LevelOrder(BN* root)
{
    if (root == NULL)
        return;
    Queue q;
    QueueInit(&q);
    QueuePushBack(&q, root);
    while (!QueueEmpty(&q))
    {
        BN* Front = QueueTop(&q);
        QueuePopFront(&q);
        printf("%d ", Front->data);

        if (Front->leftchild)
            QueuePushBack(&q, Front->leftchild);
        if(Front->rightchild)
            QueuePushBack(&q, Front->rightchild);
    }
    printf("\n");
    QueueDestroy(&q);
}

//判断是否是完全二叉树
bool BinaryTreeComplete(BN* root)
{
    Queue q;
    QueueInit(&q);
    QueuePushBack(&q, root);
    BN* Front = NULL;
    while (!QueueEmpty(&q))
    {
        Front = QueueTop(&q);
        QueuePopFront(&q);

        if (Front == NULL)
        {
            break;
        }
        else
        {
            QueuePushBack(&q, root->leftchild);
            QueuePushBack(&q, root->rightchild);
        }
    }
    while (!QueueEmpty(&q))
    {
        Front = QueueTop(&q);
        QueuePopFront(&q);
        if (Front)
        {
            QueueDestroy(&q);
            return false;
        }
    }
    QueueDestroy(&q);
    return true;
}

二叉树销毁

首先得判断根节点是否为空,为空就return即可

然后把每个节点都遍历,自下而上的删除节点即可


本文转载自: https://blog.csdn.net/weixin_58867976/article/details/122778770
版权归原作者 风影66666 所有, 如有侵权,请联系我们删除。

“树、二叉树与堆”的评论:

还没有评论