初阶数据结构学习记录——여덟 二叉树和堆(1)

目录 

一、树

概念 

二、二叉树

二叉树的特点

三、这篇重点在于堆的实现

该用数组还是链表体现二叉树?

假设是链表

假设是数组

头文件里

Heap.c(包括push和pop,向上/下比较法)

Heap.h

Heap.c

Test.c


一、树

顾名思义,结构即为树,由一个根节点分出多个节点,这几个节点再继续往下连接其他节点形成一个个子树。不过这棵树是根朝上,叶朝下的。一个根不限制连接多少个节点,把第二层的几个节点也看成根节点,最终形成一整个树结构。形象的图可以搜到,这里就不写了。

来点形式主义

概念 

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

~ 有一个特殊的节点,称为根节点,根节点没有前驱节点。

~ 除根节点外,其余节点被分成M(M > 0)个互不相交的集合T1,T2....TM, 其中每一个集合Ti(1 <= i <= M)又是一棵结构与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继节点。

~ 因此,树是递归定义的。

接下来要看一下树的一些定义

 

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

叶节点或终端节点:度为0的节点称为叶节点,如上图:B、C、H、I.....等节点为叶节点

非终端节点或分支节点:度不为0的节点;如上图:D、E、F、G....等节点为分支节点

双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。如上图, A是B的节点

孩子节点或者子节点:一个节点含有的子树的根节点称为该节点的子节点。如上图:B是A的子节点

兄弟节点:具有相同父节点的节点互称为兄弟节点。如上图:B、C是兄弟节点

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

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。

树的高度或深度:树中节点的最大层次。如上图,树的高度为4

堂兄弟节点:双亲在同一层的节点互为堂兄弟。如上图:H、I互为堂兄弟节点

节点的祖先:从根到该节点所经分支上的所有节点。如上图:A是所有节点的祖先

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

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

这些概念是树中常用名词,无论是为了做题还是为了工作,都应当记住。

二、二叉树

二叉树的每个节点最多有两个子节点,分为左子树和右子树。二叉树中又有满二叉树和完全二叉树。

满二叉树是每一层的节点数都达到最大。所以如果高度为h的满二叉树,它的节点数应当是2 ^ h - 1。

完全二叉树:完全二叉树是一个效率比较高的结构,由满二叉树变种而来。要求除去最后一层外其他层节点数达到最大,最后一层节点数至少为1,且节点必须连续,比如A为根节点,BC作为第二层节点,B和C下各一个左子树或者右子树就是不连续,就不是完全二叉树。

二叉树的特点

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

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

3、对任何一棵二叉树,如果度为0,其叶节点个数为n0,度为2的分支节点个数为n2,则有n0 = n2 + 1。(度为0总比度为2的多一个

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

具体推算过程就不写了。

三、这篇重点在于堆的实现

二叉树有大堆和小堆之分,大堆就是父节点大于等于子节点,小堆就是父节点小于等于子节点。这篇写大堆。

该用数组还是链表体现二叉树?

假设是链表

插入第一个后,需要用一个指针指向第二个。根节点如果有好几个子节点,那就得需要多个指针,可以在创建结构体时,里面放入多个指针,也可以简单点,建立一个指针数组,大小就是树的度。不过这样或许还是不够好,另有一个方法,创建结构体后,里面放入一个child和brother指针,child指向自己的子节点,brother则指向兄弟节点。比如A为祖先,下有3个子节点,child指向节点B,B的brother指向C,C的brother指向d。BCD三个节点如果有子节点,那就用child指向子节点,这样就方便了。

现在想一下push功能,这是不是相当于尾插功能?所以我们需要第三个指针来指向尾部。插入后新数字需要和之前的数字比较。可是他该如何和其他元素比较?插入的新元素应当是brother或者child指向的数字,想访问指向新数字的节点,这里就得需要一个prev指针指向前面了吧?

所以可以发现一个问题,我没办法随意的访问其他节点元素。push或者pop时就总会有些麻烦,那么现在转向数组。

假设是数组

数组可以随意访问其他元素,只要找到下标之间的规律即可。不过数组又该如何体现二叉树结构?这个并不是难事,我们需要脱离树状结构这个臆想图,真正在电脑里面存储时都是一块块空间,我们只需要使空间里的数字符合规律,可以简单地访问其他元素即可。现在以数组来完成二叉树。

调用整个树之前先创建好空间,所以初始化函数里就不写malloc了。

头文件里
typedef int HPDataType;
typedef struct Heap
{
    HPDataType* a;
    HPDataType size;
    HPDataType capacity;
}HP;
Heap.c(包括push和pop,向上/下比较法)
void HeapInit(HP* php)
{
    assert(php);
    php->a = NULL;
    php->size = php->capacity = 0;
}

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

先放上这两个函数。然后开始push和pop的代码。

现在的情况是已经存入了一些数据,要继续存入。

该怎么体现树状结构?我们确定了要使用数组,数组为空的时候,push一个就是放在下标0位置处,push第二个就需要和数组里的元素进行比较来确定谁当祖先。之后一个个存入,我们都需要比较,放好位置,所以搞清楚这个做法也就完成了push函数的思路。进行比较的时候,应该跟谁比较?怎么去访问?如果是链表,我们可以通过指针,不过数组就需要找规律。先前已经说过,抛掉两个树状结构的臆想图,一个根节点只能访问两个子节点,那么就把两个子节点放在根节点之后,通过下标转换来找到子节点。比如根节点下标为0,两个子节点下标分别是1和2,1这个节点继续分支,占据了3和4下标,2这个节点继续分支,占据5和6下标,这样找到规律即可随机访问。那么我们开始写push和pop函数。

插入之前先判断为不空

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

}

先写出来这些代码。现在已经在尾部插入一个数据了,那么开始判断大小吧。兄弟节点没必要比较,如果新数字比父节点大,那么自然也就比兄弟节点大;如果小,那就不需要动位置。之后一步步往上挪,整个过程都不需要跟兄弟节点比较,只跟父节点比较,大于就交换位置,直到祖先节点,如果比根节点大,那么新插入的数据就称为新的祖先。和父节点交换位置,父节点也不需要再跟其他节点进行比较。这是向上比较法。两个比较的对象,一个下标是新插入的位置,另一个则是通过规律来寻找,规律就是(i - 1) / 2,就能找到它的父节点,交换完后继续向上找父节点。

void Swap(HPDataType* s1, HPDataType* s2)
{
    HPDataType tmp = *s1;
    *s1 = *s2;
    *s2 = tmp;
}

void AdjustUp(HPDataType* a, int child)
{
    int parent = (child - 1) / 2;
    while (child)
    {
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }
        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 = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
        if (tmp == NULL)
        {
            perror("realloc fail");
            exit(-1);
        }
        php->a = tmp;
        php->capacity = newCapacity;
    }
    php->a[php->size] = x;
    php->size++;
    AdjustUp(php->a, php->size - 1);
}

向上比较函数里,判断条件是child > 0。为什么不能是 >= 0?当最后和祖先节点比较时,如果还是更大,那么child就变成下标0位置了,这时候整个过程应当结束,如果是>= 0,while还会继续,就会越界访问了。

那么数组为空时,这个push函数有没有效?还是可以的,因为child为0,循环进不去,UP函数就会break了。再插入一个数据,进入函数,就进行比较,排好位置,会发现整个结构会按照大堆方向排列。写一个测试代码

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

void TestHeap1()
{
    int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    HP hp;
    HeapInit(&hp);
    for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    {
        HeapPush(&hp, arr[i]);
    }
    HeapPrint(&hp);
    HeapDestroy(&hp);
}

int main()
{
    TestHeap1();
    return 0;
}

结果就是


所以没问题

接下来看pop函数。

删除根部元素如何删除?尾部元素很好删除,就是尾删功能,下标最后一个删除,而且也没有改变节点之间的关系,因为它是一个子节点,但是根部元素的删除确实有点麻烦。因为这相当于删除祖先,我们就需要再找一个节点当祖先。这里能不能挪动覆盖?把49及之后的数据往前挪一次,这样头部就删除了,但是挪动数据时间复杂度为O(N),,且这样挪动的话,节点之间的关系就乱了,很有可能子节点比父节点数值大,所以不能挪动。那如果是把49覆盖到65上,然后再往后找数值放到49这个位置呢?其实也不好做,画图仔细想想,会发现挪动的每个数据的下标不好找规律。

现在有另一个方法。把最后一个数字和第一个数字调换一下,这两个下标很好找,尾删一下,也就是size--,此时根部位置的数字变成了尾部数字,这时根部位置的数字一定比它现在的子节点的数要小。要改成正确的顺序其实就可以参照push函数的做法,不过这里是向下比较法。换过来后,和第二层的数字比较,找出大的那个,让它来做新的祖先,然后一层层向下探索。

void AdjustDown(HPDataType* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while (child < n)
    {
        //确认child指向大的那个孩子并且child要小于size
        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 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);
}

在之前的测试代码再加上几行

void TestHeap1()
{
    int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    HP hp;
    HeapInit(&hp);
    for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    {
        HeapPush(&hp, arr[i]);
    }
    HeapPrint(&hp);

    int k = 5;
    while (k--)
    {
        printf("%d ", HeapTop(&hp));
        HeapPop(&hp);
    }
    HeapDestroy(&hp);
}

难题攻克了,我们把剩下的点一一写完

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

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

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

最后,还有一个问题,有的时候给的接口里面会有创建堆这个函数,关于这个之后再写。先放上所有的代码

Heap.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

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

void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPrint(HP* php);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);

HPDataType HeapTop(HP* php);
HPDataType HeapSize(HP* hp);
bool HeapEmpty(HP* hp);

Heap.c

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

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

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

void Swap(HPDataType* s1, HPDataType* s2)
{
    HPDataType tmp = *s1;
    *s1 = *s2;
    *s2 = tmp;
}

void AdjustUp(HPDataType* a, int child)
{
    int parent = (child - 1) / 2;
    while (child)
    {
        if (a[child] < a[parent])
        {
            Swap(&a[child], &a[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }
        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 = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
        if (tmp == NULL)
        {
            perror("realloc fail");
            exit(-1);
        }
        php->a = tmp;
        php->capacity = newCapacity;
    }
    php->a[php->size] = x;
    php->size++;
    AdjustUp(php->a, php->size - 1);
}

void AdjustDown(HPDataType* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while (child < n)
    {
        //确认child指向大的那个孩子并且child要小于size
        if (child + 1 < n && a[child + 1] < a[child])
        {
            ++child;
        }
        //1、孩子大于父亲,交换,继续向下调整
        //2、孩子小于父亲,则调整结束
        if (a[child] < a[parent])
        {
            Swap(&a[child], &a[parent]);
            parent = child;
            child = parent * 2 + 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);
}

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

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

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


 

Test.c

#include "Heap.h"

/*void TestHeap1()
{
    int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    HP hp;
    HeapInit(&hp);
    for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    {
        HeapPush(&hp, arr[i]);
    }
    HeapPrint(&hp);

    int k = 5;
    while (k--)
    {
        printf("%d ", HeapTop(&hp));
        HeapPop(&hp);
    }
    HeapDestroy(&hp);
}*/

void TestHeap2()
{
    int arr[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
    HP hp;
    HeapInit(&hp);
    for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
    {
        HeapPush(&hp, arr[i]);
    }
    HeapPrint(&hp);
    while (!HeapEmpty(&hp))
    {
        printf("%d ", HeapTop(&hp));
        HeapPop(&hp);
    }
    HeapDestroy(&hp);
}

int main()
{
    //TestHeap1();
    TestHeap2();
    return 0;
}

结束。下一篇再补上创建堆的接口。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值