006+limou+C语言“堆的实现”与“树的相关概念”

news2024/11/15 20:08:58

0.前言

这里是limou3434的一篇个人博文,感兴趣可以看看我的其他内容。本次我给您带来的是树的相关只是,并且把堆这一数据结构做了实现,后面还有大量的oj题目。但是树重点也就在这十多道oj题目中,您可以尝试着自己做一下,或者看看力扣的其他题友的解法。

1.树的基础概念和相关术语

1.1.一般树

树时一种非线性结构,由n(n>=0)个有限节点构成一个具有层次关系的集合,看起来就像一棵倒挂的树

  • 有一个特殊的节点,称为根节点,根节点没有父亲节点
  • 除根节点外的其余节点被分为M(M>0)个互不相交的集合T1、T2、…Tm,其中每一个集合T(1<=i<=m)又是一棵结构与树类似的子树,每个子树的根节点有且只有一个前驱,可以有0或多个后继
  • 因此树是递归定义的
    树的构成 = 父节 点 1 + N 1 棵子树 = 父节 点 1 + ( 父节 点 2 + N 2 棵子树 ) + ( 父节 点 3 + N 3 棵子树 ) + ( 父节 点 4 + N 4 棵子树 ) + … + ( 父节 点 N + 1 + N N + 1 棵子树 ) ) 树的构成 = 父节点_1 + N_1 棵子树= 父节点_1 + (父节点_2 + N_2 棵子树) + (父节点_3 + N_3 棵子树) + (父节点_4 + N_4 棵子树) + … + (父节点_{N+1} + N_{N+1} 棵子树)) 树的构成=父节1+N1棵子树=父节1+(父节2+N2棵子树)+(父节3+N3棵子树)+(父节4+N4棵子树)++(父节N+1+NN+1棵子树))

在这里插入图片描述

判断树的时候需要注意,树的子树是不会相交的,而且除了根节点,每一个节点都有且只有一个父亲节点

1.2.二叉树

有的时候我们不需要度很大的树,因此就有了二叉树。二叉树的度最大为2,由一个根节点加上左右子树构成(递归定义)

1.2.1.完全二叉树

假设有一棵二叉树,高度为h,则前h-1层都是满的,最后一层是不满的,但呈现连续的状态。则我们称这棵二叉树为完全二叉树。
在这里插入图片描述

1.2.2.满二叉树

满二叉树就是二叉树的每一层都是满的(每一层都塞满了节点),而可以认为满二叉树是特殊的完全二叉树
在这里插入图片描述

1.3.相关术语

  • 度:一个节点含有的子树个数称为该节点的度
  • 根节点:根节点没有前驱节点,一棵树有且只有一个根节点
  • 叶节点/终端节点:叶节点没有后驱节点,其“度”为零,一棵树最少也有一个叶节点
  • 分支节点/非终端节点:“度”不为零的节点都叫分支节点
  • 节点的层次:从根开始定义起,根为第一层,根的子节点为第二层……
  • 树的高度/深度:树中节点最大层次数即树的深度
  • 节点之前的亲缘关系
    • 父亲节点/双亲节点:若一个节点含有子节点,则这个节点时其子节点的父亲节点
    • 兄弟节点:具有相同父亲节点的子节点互称为兄弟节点
    • 孩子节点:一个节点若含有子树根节点称为节点的孩子节点
    • 堂兄弟节点:双亲在同一层的节点互为堂兄弟
    • 节点的祖先:从根节点到该节点所经分支上的所有节点,除了它本身都是他的祖先。其中根节点时所有非根节点的祖先
    • 子孙:以某节点为根的子树中任一节点都称为该节点上的子孙
  • 森林:由m(m>0)棵互不相交的多棵树的集合称为森林(在数据结构并查集就会用到)(森林的概念在并查集中会用到)

1.4.树的应用

树的应用最经典的地方是在文件系统上,可以叫系统里的所有文件目录为一颗“目录树”。另外,二叉树不是真的为了存储数据,而是优化搜索,比如:搜索二叉树(在搜索二叉树里,任何一棵树,左子树都比根要小,右子树都比根要大,这个特性特别适合搜索,最多查找它的高度K次,时间复杂度为O(N),而左右平衡节点个数还能影响搜索效率,这就涉及到平衡树:AVL树、红黑树、B树(多叉搜索树、数据库原理))

2.树的多种存储结构

2.1.暴力存储法

理论上来说如果明确树的最大度,的确可以暴力穷举,但是对于有新数据插入的树这个方法就有些差劲了,因为谁也不知道到底最终会插入多少个子节点,更加不知道度的最大值是否会产生变化

typedef struct TreeNode
{
	int data;//存储节点的数据
	struct TreeNode* child_1;//存储节点指向的子孩子1
	struct TreeNode* child_2;//存储节点指向的子孩子2
	struct TreeNode* child_3;//存储节点指向的子孩子3
	//…
}TreeNode;

2.2.亲缘关系存储法

2.2.1.双亲表示法

  • 存储原理:使用数组依次存储树中各个节点,每个节点只存储数据和指向双亲节点的指针或下标,只不过这个方法用的比较少。
typedef struct TreeNode
{
	int data;//存储节点的数据
	int parenti;
}TreeNode;
//然后创建一个数据元素是TreeNode的结构体数组

在这里插入图片描述

  • 结构优势和劣势(以后再谈)

2.2.2.孩子表示法

  • 存储原理:使用数组依次存储树中各个节点。节点不止存储数据,还分别配备了一个链表/顺序表,用于存储各节点的孩子节点位于顺序表中的位置
typedef struct TreeNode 
{ 
  	int data;//存储节点的数据 
  	ListNode* childArr;//或者使用顺序表SeqList childArr; 
}TreeNode;

在这里插入图片描述

  • 结构优势和劣势(以后再谈)

2.2.3.孩子兄弟表示法

  • 存储原理:每一个节点除了存储自己携带的数据,还携带了指向兄弟节点的指针,以及指向第一个孩子节点的指针。这个结构很优秀,无论度有多大都可以存储起来,并且可以将多叉树转化为一种类二叉树的存在。

在这里插入图片描述

  • 结构优势和劣势(以后再谈)

3.二叉树的相关性质

3.1.一般二叉树性质

3.1.1.性质一:深度K与节点数N的关系[f(K,N)]

把一棵二叉树的N个节点想象成两个极端:安全二叉树(最少)和斜树(最多),因此K的范围就是[⌊log(2)(N)⌋ + 1(⌊x⌋表示取不小于x的最大整数), N]这个全闭区间

3.1.2.性质二:层阶i的max节点数N

若规定根节点的层数为1,则一颗非空二叉树的第i层最多拥有节点数N为“2^(i - 1)”

3.1.3.性质三:深度K的max节点数N

若规定根节点的层数为1,则深度为K的二叉树的最大节点数N为“(2^K) - 1”

3.1.4.性质四:“度0节点数量n0”和“度2节点数量n2”的关系[f(n0,n2)]

对任何一棵二叉树,如果度为0的分支节点个数为n0,度为2的分支节点个数为n2,则有关系式“n0 == n2 + 1”

1.假设有n个节点,其中度为0的节点有n0个、度为1的节点有n1个、度为2的节点有n2个,那么“n0 + n1 + n2 == n”①
2.假设所有的祖先节点和其拥有的所有子节点都有连线,则向上看除了根节点,每一个节点拥有一根连线,那么总共有“n - 1”条连线。而由于度为0的节点向下引出0根线,度为1的节点向下引出1根线,度为2的节点向下引出2根线。则“n - 1 == 2 * n2 +
1 * n1 + 0 * n0”,即“n - 1 == 2 * n2 + n1”②
3.结合①和②得出“n0 == n2 + 1”

3.2.完全二叉树性质

在这里插入图片描述

3.2.1.性质一:深度K与节点数N的关系[f(K,N)]

对于完全二叉树,假设其具有N个节点,则其深度K为“⌊log(2)(N)⌋ + 1(⌊x⌋表示取不小于x的最大整数)”

1.对于完全二叉树来说,其总节点数有N,深度为K,“(2^(K - 1)) - 1 + 1 <= N <= (2^K) - 1”,即“(2^(K - 1)) <= N <= (2^K)”
2.两边同时取对数,得到“k - 1 <= log(2)(N) <
k”,得到“log(2)(N) < k <= log(2)(N) +
1”,这就会有一个类似高斯函数的问题,则k的取值可以表示为“⌊log(2)(N) + 1⌋ == ⌊log(2)(N)⌋ + 1”
实际上也可以根据“完全二叉树 = 满二叉树 + 一层不完整子节点”来得出节点的范围数,逆推完全二叉树的高度

3.2.2.性质二:“节点index”和“子节点index”的关系

对一颗有n个节点的完全二叉树,对每个节点进行编号(从1开始,从左到右,从上到下)对任意节点i都有:

  • 若“i=1”,则节点i是根,没有双亲节点;若i > 1,则节点i的双亲节点是⌊i/2⌋
  • 若“2i>n”,则节点i没有左孩子,否则左孩子为2i
  • 若“2i+1>n”,则节点i没有右孩子,否则左孩子为2i+1

实际上还可以理解为:
1.父节点找子节点 如果从0开始编号,某个节点的编号为i,则“2i+1”为其左孩子,“2i+2”为其右孩子,证明方法实际上也简单,因为每一个左孩子都是偶索引,右孩子都是奇索引,满足奇偶数的通项公式
leftChild = 2 * parent rightChild = 2 * parent + 1
2.子节点找父节点 在C语言中“/”是向0取整的,所以不使用数学表达式而是使用C语言表达式,那么只需要一个式子就可以表示 parent = (child - 1) / 2
3.依靠这个性质使得完全二叉树和满二叉树非常适合用数组存储,而且也不会空间浪费

3.3.满二叉树性质

在这里插入图片描述

3.3.1.性质一:深度K与节点数N的关系[f(K,N)]

若规定根节点的层数为1,具有N个节点的满二叉树的深度K为“log(2)(N + 1)”

因为(2^K) - 1 = N,则K = log(2)(N + 1)

4.二叉树的堆接口实现

4.1.堆的概念

这里提到的堆是一种数据结构,而不是操作系统里的堆空间。堆的表现形式是一棵完全二叉树,不过这棵完全二叉树的要求是“任何一个父节点都大于或等于子节点”或者“任何一个父节点都小于或等于子节点”,因此堆也被分为“大堆”或“小堆”。堆的实现可以采用逻辑结构为“完全二叉树”、物理结构为“数组”的方式来实现。
在这里插入图片描述

4.1.1.入堆

  • 将要插入的元素放在堆的最后一个位置(即数组的最后一个位置)
  • 将其与其父节点进行比较,使用向上调整算法来调整节点位置,直到满足堆的特性为止

4.1.2.出堆

  • 将堆的最后一个节点和堆顶节点互换,然后将原来的堆顶节点隔离(也就是说不再将其视为堆的节点)
  • 将此时新的堆顶节点与其子节点进行比较,使用向下调整算法调整节点位置,直到满足堆的特性为止

4.2.堆的实现

4.2.1.具体代码

//  堆的实现(以下是大堆结构)
typedef int HPDataType;
typedef struct Heap
{
    HPDataType* _a;
    int _size;
    int _capacity;
}Heap;
//  1.堆的初始化
void HeapInit(Heap* hp)
{
    assert(hp);
    hp->_a = NULL;
    hp->_size = hp->_capacity = 0;
}
//  2.堆的销毁
void HeapDestory(Heap* hp)
{
    free(hp->_a);
    hp->_capacity = hp->_size = 0;
}
//  3.堆的判空
bool HeapEmpty(Heap* hp)
{
    return hp->_size == 0;//空则true
}
//  4.堆的插入(堆的插入要保持大堆/小堆的条件)
//  辅助函数:向上调整算法
/*
*   前提:一个完全二叉树满足堆性质,
*   但是某个节点的值发生了变化,
*   需要将其调整到合适的位置以恢复堆的性质
*/
void AdjustUp(HPDataType* arr, int childi)//这里也可以不加childi解决,但是由于以后用向上调整算法的时候不一定直接从最后一个节点开始,所以就设计了这个参数
{
    assert(arr);
    int parenti = (childi - 1) / 2;
    while (childi > 0)//这里不能用parent
    {
        //1.不满足父节点大于子节点就交换
        if (arr[parenti] < arr[childi])
        {
            int tmp = arr[parenti];
            arr[parenti] = arr[childi];
            arr[childi] = tmp;

            childi = parenti;//2.迭代更新
            parenti = (childi - 1) / 2;
        }
        else
        {
            break;
        }
    }
    //3.补充一下,向上调整算法的时间复杂度为O(log(2)(n))
}
void HeapPush(Heap* hp, HPDataType x)
{
    assert(hp);
    //1.查看容量是否充足,不足就扩容
    if (hp->_capacity == hp->_size)
    {
        int newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
        HPDataType* cache = (HPDataType*)realloc(hp->_a, sizeof(HPDataType) * newcapacity);
        if (!cache)
        {
            perror("realloc fail\n");
            return;
        }
        hp->_a = cache;
        hp->_capacity = newcapacity;
    }
    //2.直接插入
    hp->_a[hp->_size] = x;
    hp->_size++;
    //3.使用向上调整算法
    AdjustUp(hp->_a, hp->_size - 1);
}
//  5.堆的删除
//  辅助函数:向下调整算法
/*
*   前提:一个完全二叉树满足堆性质,
*   但是某个节点的值发生了变化,
*   需要将其调整到合适的位置以恢复堆的性质
*/
void AdjustDown(HPDataType* arr, int arrsize, int parenti)//在本例中不加上参数parenti也是可以解决的,但是长远来看,在以后使用向下调整算法的时候不一定从根节点开始也有可能从任意节点开始
{
    //这是我一开始的写法,有什么问题呢?发生了数组越界,但其实如果在开头正确处理了数组越界问题那也是可以这么写的,只是不够整洁
    //assert(arr);
    //int childi = arr[parenti * 2 + 1] > arr[parenti * 2 + 2] ? (parenti * 2 + 1) : (parenti * 2 + 2);

    //while (childi < arrsize)
    //{
    //    if (arr[parenti] < arr[childi])
    //    {
    //        int tmp = arr[parenti];
    //        arr[parenti] = arr[childi];
    //        arr[childi] = tmp;

    //        parenti = childi;
    //        childi = arr[parenti * 2 + 1] > arr[parenti * 2 + 2] ? (parenti * 2 + 1) : (parenti * 2 + 2);
    //    }
    //    else
    //    {
    //        break;
    //    }
    //}
    int childi = parenti * 2 + 1;//先将左孩子索引赋给childi
    while (childi < arrsize)//childi + 1 < arrsize不能写在这里,因为有可能左孩子存在但是没有右孩子 
    {
        //1.修正孩子坐标
        if (childi + 1 < arrsize && arr[childi] < arr[childi + 1])
        {
            childi++;
        }
        //2.判断是否需要交换
        if (arr[childi] > arr[parenti])
        {
            int tmp = arr[childi];
            arr[childi] = arr[parenti];
            arr[parenti] = tmp;

            parenti = childi;
            childi = parenti * 2 + 1;
        }
        else
        {
            break;
        }
    }
}
void HeapPop(Heap* hp)
{
    assert(hp);
    assert(!HeapEmpty(hp));
    //1.交换堆顶节点和堆的最后一个节点
    int tmp = hp->_a[0];
    hp->_a[0] = hp->_a[hp->_size - 1];
    hp->_a[hp->_size - 1] = tmp;
    hp->_size--;//隔离
    //2.使用向下调整算法
    AdjustDown(hp->_a, hp->_size, 0);
}
//   6.取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
    assert(hp);
    assert(hp->_size > 0);
    return hp->_a[0];
}
//   7.堆的数据个数
int HeapSize(Heap* hp)
{
    assert(hp);
    return hp->_size;
}

4.2.2.测试用例

void test(void)
{
    Heap hp;//创建堆
    HeapInit(&hp);//初始化堆
    int arr[10] = { 1, 2, 5, 23, 100, 234, 12, 0, -3, -313 };
    for (int i = 0; i < sizeof(arr)/sizeof(int); i++)
    {
        HeapPush(&hp, arr[i]);//不断入堆
    }
    //不断出堆
    for (int i = hp._size; i > 0; i--)
    {
        for (int i = 0; i < hp._size; i++)
        {
            printf("%d ", hp._a[i]);
        }
        printf(" 此时堆有%d个元素,堆顶元素是%d", HeapSize(&hp), HeapTop(&hp));
        printf("\n");
        HeapPop(&hp);//出堆
    }
    printf("此时堆有%d个元素\n", HeapSize(&hp));
    //HeapPop(&hp);//非法出堆
    //HeapTop(&hp);//非法取堆顶
    HeapDestory(&hp);//销毁堆
}

4.3.堆的作用

4.3.1.Top-k问题的解决

堆的插入和删除时间复杂度都为O(log(2)(n)),因此堆的插入和删除效率很高

最坏都要调整树的高度/深度次,而高度/深度为K = ⌊log(2)(N)⌋ +
1(⌊x⌋表示取不小于x的最大整数),考虑最坏情况并且简化公式就可以得到时间复杂度为log(2)(n)

堆可以高效解决top-k问题。每一次入堆/出堆后,堆顶节点携带的数据都是最大的,并且无需全部排序就能得到最大的数据,而其他普通的排序则需要全部排序,择取前k位才能得到的数据,对比起来堆的效率更高。

4.3.2.堆排序(一种选择排序)

之前有关堆顶(大堆)的实现中,如果一边取堆顶节点数据打印,一边进行出堆,不难发现构成了一种不太严格的“排序算法”。为什么说是不严格呢?因为排序是要在内存中排序,这个只是打印的数据是有序的,并没有真正的改变存储推节点的数据的顺序。

  • 堆排实现方法一(只使用之前写过的堆接口来实现)
//利用前面写好的堆接口
void HeapSort_1(int* arr, int arrsize)
{
    Heap hp;
    HeapInit(&hp);//初始化
    for (int i = 0; i < arrsize; i++)
    {
        HeapPush(&hp, arr[i]);//不断插入
    }
    for (int i = 0; i < arrsize; i++)
    {
        arr[i] = HeapTop(&hp);//不断取出拷贝回原来传递过来的数组
        HeapPop(&hp);
    }
    HeapDestory(&hp);
}
int main()
{
	int arr[10] = { 1, 192, 34, 412, 32, 0, -2134, 23, 45, 89 };
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    HeapSort_1(arr, sizeof(arr) / sizeof(int));//堆排序
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
	return 0;
}
//但是这种比较粗糙,不是直接排序出来,而是通过拷贝再粘贴的方式,比较粗糙,效率可以,但还需要先建立一个堆,空间复杂度高
  • 堆排实现方法二(模拟堆的入堆和出堆,建堆使用向下调整算法,排序使用向上调整算法)
//利用前面写好的向上和向下调整算法,模拟堆的入堆和出堆
void AdjustUp(HPDataType* arr, int childi)
{
    assert(arr);
    int parenti = (childi - 1) / 2;
    while (childi > 0)//这里不能用parent
    {
        //1.不满足父节点大于子节点就交换
        if (arr[parenti] < arr[childi])
        {
            int tmp = arr[parenti];
            arr[parenti] = arr[childi];
            arr[childi] = tmp;

            childi = parenti;//2.迭代更新
            parenti = (childi - 1) / 2;
        }
        else
        {
            break;
        }
    }
    //3.补充一下,向上调整算法的时间复杂度为O(log(2)(n))
}
void AdjustDown(HPDataType* arr, int arrsize, int parenti)
{
    int childi = parenti * 2 + 1;//先将左孩子索引赋给childi
    while (childi < arrsize)//childi + 1 < arrsize不能写在这里,因为有可能左孩子存在但是没有右孩子 
    {
        //1.修正孩子坐标
        if (childi + 1 < arrsize && arr[childi] < arr[childi + 1])
        {
            childi++;
        }
        //2.判断是否需要交换
        if (arr[childi] > arr[parenti])
        {
            int tmp = arr[childi];
            arr[childi] = arr[parenti];
            arr[parenti] = tmp;

            parenti = childi;
            childi = parenti * 2 + 1;
        }
        else
        {
            break;
        }
    }
}
//--------------------------
void HeapSort_2(int* arr, int arrsize)
{
    //直接把数组内的元素数据调整为堆,因为任何数组在逻辑结构上都可以看作是一棵完全二叉树,所以我们利用这一点把原有的数组调整成堆
    //1.将根节点看作一个堆,然后将剩余的元素按照顺序一个一个插入到堆里,而插入就要写向上调整算法
    for (int i = 1; i < arrsize; i++)
    {
        AdjustUp(arr, i);
    }//2.一个循环走下来就建好了一个堆(注意我的向上调整算法是拿来建大堆的)
    //3.接下来模拟出堆的过程,将每一次生成的堆顶扔(换)到原数组的后面
    for (int i = arrsize - 1; i > 0; i--)
    {
        int tmp = arr[0];
        arr[0] = arr[i];
        arr[i] = tmp;
        AdjustDown(arr, i, 0);
    }
}
int main()
{
    int arr[10] = { 1, 192, 34, 412, 32, 0, -2134, 23, 45, 89 };
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    HeapSort_2(arr, sizeof(arr) / sizeof(int));//堆排序
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
  • 堆排实现方法三(模拟堆的入堆和出堆,建堆使用向下调整算法,排序使用向下调整算法)
//利用前面写好的向下调整算法,模拟堆的入堆和出堆 
void AdjustDown(HPDataType* arr, int arrsize, int parenti)
{
    int childi = parenti * 2 + 1;//先将左孩子索引赋给childi
    while (childi < arrsize)//childi + 1 < arrsize不能写在这里,因为有可能左孩子存在但是没有右孩子 
    {
        //1.修正孩子坐标
        if (childi + 1 < arrsize && arr[childi] < arr[childi + 1])
        {
            childi++;
        }
        //2.判断是否需要交换
        if (arr[childi] > arr[parenti])
        {
            int tmp = arr[childi];
            arr[childi] = arr[parenti];
            arr[parenti] = tmp;

            parenti = childi;
            childi = parenti * 2 + 1;
        }
        else
        {
            break;
        }
    }
}
void HeapSort_3(int* arr, int arrsize)
{
    //从叶子节点开始倒着调整(自成一个堆),所以从第一个非叶子节点开始调整
  	//1.利用向下调整来建堆
  	for(int i = ((arrsize - 1) - 1) / 2; i >= 0; i--)
    {
    	AdjustDown(arr, arrsize, i);
    }
  	//2.利用向下调整法来排序
  	for (int i = arrsize - 1; i > 0; i--)
    {
        int tmp = arr[0];
        arr[0] = arr[i];
        arr[i] = tmp;
        AdjustDown(arr, i, 0);
    }
}
int main()
{
    int arr[10] = { 1, 192, 34, 412, 32, 0, -2134, 23, 45, 89 };
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    HeapSort_3(arr, sizeof(arr) / sizeof(int));//堆排序
    for (int i = 0; i < 10; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

1.方法二(使用向上调整建堆)
1.1.建堆时间复杂度: 假设一棵树深度为K,则最坏情况下有如下分析 第[ 1 ]层有[ 2^0 ]个节点,需要向上移动[ 0 ]层 第[ 2 ]层有[ 2^1 ]个节点,需要向上移动[ 1 ]层 第[ 3 ]层有[ 2^1 ]个节点,需要向上移动[ 2 ]层 ……
第[K-2]层有[2^(K-3)]个节点,需要向上移动[K-3]层 第[K-1]层有[2^(K-2)]个节点,需要向上移动[K-2]层 第[
K ]层有[2^(K-1)]个节点,需要向上移动[K-1]层
综上所述,一共要移动:(21)*(1)+(22)(2)+……+(2^(K-1))(K-1) = 2^K*(K-2)+2 =
(N+1)(log(N+1)-2)+2,因此时间复杂度就是O(Nlog(2)(N))
1.2.排序时间复杂度: 对于一棵已成堆的树,最后一层有2^(K-1)个节点,每次都要和堆顶互换,最坏情况下需要调整K-1次,以此类推,这就和向上调整算法的时间复杂度是一样的,都是O(Nlog(2)(N))
(注意,不要错误认为每次向下调整的而时间复杂度是log(2)(N),就觉得这个时间复杂度是log(2)(N)+log(2)(N-1)+…log(2)(1)
= log(2)(N!)这样错误的答案,这是因为一开始树的高度有可能保持一段时间的固定)
1.3.整体时间复杂度: O(N
log(2)(N))

2.方法三(使用向下调整建堆)
2.1.建堆时间复杂度: 假设一棵树深度为K,则最坏情况下有如下分析 第[ 1 ]层有[ 2^0 ]个节点,需要向下移动[K-1]层 第[ 2 ]层有[ 2^1 ]个节点,需要向下移动[K-2]层 第[ 3 ]层有[ 2^1 ]个节点,需要向下移动[K-3]层 ……
第[K-2]层有[2^(K-3)]个节点,需要向下移动[ 2 ]层 第[K-1]层有[2^(K-2)]个节点,需要向下移动[ 1 ]层 第[
K ]层有[2^(K-1)]个节点,需要向下移动[ 0 ]层
综上所述,一共要移动:(20)*(K-1)+(21)(K-2)+(22)*(K-3)+……+(2(K-1))(1) =
2^K-1-K = N-log(2)(N+1),因此时间复杂度就是O(N)
(注意,不要因为向下调整算法的单次调整是log(2)(N),就觉得每个节点都是log(2)(N),于是得到Nlog(2)(N)这个错误结果,错误的原因是因为,每一个节点的log(2)(N)不同的,因此不是简单的相乘)
2.2.排序时间复杂度: 对于一棵已成堆的树,最后一层有2^(K-1)个节点,每次都要和堆顶互换,最坏情况下需要调整K-1次,以此类推,这就和向上调整算法的时间复杂度是一样的,都是O(N
log(2)(N))
2.3.整体时间复杂度 O(N+Nlog(2)(N)) = O(N(1+log(2)(N))) = O(N*log(2)(N))

3.总结:向上调整算法效率低的原因就是节点少就调整少,节点多就调整多

4.3.3.堆排解决Top-k的缺陷

  • 正常思路:如果在N个数中选取k个数据,则如果这个N过大,建堆的空间复杂度会很大,数据量过大就要装在磁盘中(比如存储在文件中),这个时候不可能全部把文件里的数据全部读取到内存。因为超出内存的数据会被推出内存,以便加载新的数据进入内存,但是这样就始终没有办法把数据全部加载到内存中,那就更加别提说使用堆排序了

  • 优化思路:既然全部数据不可能同时加载到内存中,那么我们就只加载一部分把!

    • 情况一:先将前k个数据加载进内存,利用向下调整算法建成小堆,则后面N-k个数,逐一何堆顶数据比较,比堆顶数据大就替换堆顶数据,最后这个小堆存储的就是最大的前k个数据,这样就可以找到这其中的k个数据了
    • 情况二:先将前k个数据加载进内存,利用向下调整算法建成大堆,则后面N-k个数,逐一何堆顶数据比较,比堆顶数据小就替换堆顶数据,最后这个小堆存储的就是最小的前k个数据,这样就可以找到这其中的k个数据了
//下面选出最小的前k个数据
void AdjustDown(HPDataType* arr, int arrsize, int parenti)
{
    int childi = parenti * 2 + 1;//先将左孩子索引赋给childi
    while (childi < arrsize)//childi + 1 < arrsize不能写在这里,因为有可能左孩子存在但是没有右孩子 
    {
        //1.修正孩子坐标
        if (childi + 1 < arrsize && arr[childi] < arr[childi + 1])
        {
            childi++;
        }
        //2.判断是否需要交换
        if (arr[childi] > arr[parenti])
        {
            int tmp = arr[childi];
            arr[childi] = arr[parenti];
            arr[parenti] = tmp;

            parenti = childi;
            childi = parenti * 2 + 1;
        }
        else
        {
            break;
        }
    }
}
//开始排序
void HeapSort_4(int* arr, int arrSize, int k)
{
    //1.先建一个大堆
    for (int i = k - 1; i >= 0; i--)
    {
        AdjustDown(arr, k, i);//使用向下调整算法
    }
    //2.再拿后arrSize-k个数据和堆顶一一对比
    for (int j = k; j < arrSize; j++)
    {
        if (arr[0] > arr[j])
        {
            int tmp = arr[0];
            arr[0] = arr[j];
            arr[j] = tmp;
            AdjustDown(arr, k, 0);
        }
    }
    //3.再复用前面的HeapSort_3()对k个元素单独堆排
    HeapSort_3(arr, k);
}
  • 模拟情况
//模拟针对大量数据的Top-k的堆排
void HeapSort_Topk(int k)
{
    //1.打开文件
    const char* file = "data.txt";
    FILE* fout = fopen(file, "r");
    if (fout == NULL)//打开失败就提示
    {
        perror("fopen error");
        return;
    }
    //2.先只读取k个数据
    int* kmaxheap = (int*)malloc(sizeof(int) * k);//开辟对应大堆的空间
    if (kmaxheap == NULL)//开辟失败就提示
    {
        perror("malloc error");
        return;
    }
    for (int i = 0; i < k; i++)
    {
        int fscanfreturn = fscanf(fout, "%d", &kmaxheap[i]);//利用格式化输入将文件中前k个数据读取到数组中
    }
    //3.建大堆
    for (int i = (k - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(kmaxheap, k, i);//使用向下调整算法
    }
    //4.读取N-k个数据然后一一对比
    int val = 0;
    while (!feof(fout))
    {
        int fscanfreturn = fscanf(fout, "%d", &val);
        if (val < kmaxheap[0])
        {
            kmaxheap[0] = val;
            AdjustDown(kmaxheap, k, 0);
        }
    }
    fclose(fout);
    //5.打印前最小的k个数据
    HeapSort_3(kmaxheap, k);
    for (int i = 0; i < k; i++)
    {
        printf("%d ", kmaxheap[i]);
    }
    printf("\n");
    free(kmaxheap);
}
//模拟数据在文件中存储
void _make(void)
{
    //造数据
    int n = 100;//控制数据个数
    int m = 100;//控制数据范围
    int x = 0;//存储数据本身
    srand((unsigned)time(0));//随机数种子
    const char* file = "data.txt";//定义文件名字
    FILE* fin = fopen(file, "w");//以只写的方式打开该文件,如果该文件不存在就自动创建,存在则覆盖原有的内容
    if (fin == NULL)//打开失败就提示错误
    {
        perror("fopen error");
        return;
    }
    for (size_t i = 0; i < n; ++i)//不断写入n个随机数据
    {
        x = rand() % m;
        fprintf(fin, "%d\n", x);
    }
    fclose(fin);//关闭该文件
}
int main()
{
	_make();
    HeapSort_Topk(10);
  	return 0;
}

5.二叉树的遍历方法

在这里插入图片描述

5.1.前中后序遍历

首先无论是什么二叉树结构都需要被理解成三部分,“根+左子树+右子树”,下面我们使用三种同类型的方法来遍历上面这棵二叉树。

  • 前中后序概念
    • 前序/先根:先访问根、再访问左子树、再访问右子树:A-B-D-G-NULL-NULL-NULL-E-H-NULL-NULL-I-NULL-NULL-C-NULL-F-NULL-NULL-end,简化为:A-B-D-G-E-H-I-C-F(有的时候也叫“先序”)
    • 中序/中根:先访问左子树、再访问根、再访问右子树:NULL-G-NULL-D-NULL-B-NULL-H-NULL-E-NULL-I-NULL-A-NULL-C-NULL-F-NULL-end,简化为:G-D-B-H-E-I-A-C-F
    • 后序/后根:先访问左子树、再访问右子树、再访问根:NULL-NULL-G-NULL-D-NULL-NULL-H-NULL-NULL-I-E-B-NULL-NULL-NULL-F-C-A-end,简化为:G-D-H-I-E-B-F-C-A
  • 前、中、后序的时间和空间复杂度(假设树有N个节点,深度为K)
    • 时间复杂度:由于每一个节点都要访问一次,因此时间复杂度都是O(N)
    • 空间复杂度:由于有N个节点,所以树的最大深度是K是N,最小深度是log(2)(N+1)。因此在考虑共用栈帧情况下,最多建立N个栈帧,即空间复杂度为O(K)

5.2.层序遍历

层序遍历从遍历方式上比较容易理解,例如上面那棵树的层序遍历就是:A-B-C-D-E-F-NULL-NULL-G-H-I-NULL-NULL-NULL-NULL-NULL-NULL,但是其具体实现就比较难以想到

实现思路就是:
1.假设有一个队列Q
2.这个时候将指向A节点指针入队
3.此时取队头,打印A节点携带的数据,
4.并把指向A的左孩子B和右孩子C的两个指针依次入队
5.取出队头元素的数据
6.然后根据“先入先出”原则,先让指向B节点的指针出队
7.并把指向B的左孩子D和右孩子E的两个指针依次入队
……

例如下面这棵树对应的层序遍历过程

void LevelOrder(BinaryTreeNode* root)
{
    //1.初始化队列
    Queue qu;
    QueueInit(&qu);
    //2.将树的根节点入队
    if (root)
        QueuePush(&qu, root);
    //3.将队头的树节点的左右子树入队
    while (!QueueEmpty(&qu))
    {
        BinaryTreeNode* p = QueueFront(&qu);//取出队头
        QueuePop(&qu);
        if (p != NULL)
        {
            printf("%c ", p->data);
        }
        else
        {
            printf("NULL ");
        }
        if (p)
        {
            QueuePush(&qu, p->left);//入左子树
            QueuePush(&qu, p->right);//入右子树
        }
    }
    //4.销毁队列
    QueueDestroy(&qu);
}

6.二叉树的相关练习

6.1.性质选择题

6.1.1.题目一

某二叉树共有399个节点,其中共有199个度为2的节点,则二叉树的叶子节点数量为?

根据n0 = n2 + 1得到n0 = 199 + 1 = 200

6.1.2.题目二

在具有2n个节点的完全二叉树,叶子节点有多少个呢?

根据n0 = n2 + 1,将X(0) + X(1) + X(2) = 2n转化为2X(0) + X(1) -1 = 2n,而一个完全二叉树度为1的节点个数要不是1,要不就是0,因此x(1) = 1/0,因此2X(0) + X(1) -1 = 2n解得x(0) = 2n / 2或(2n +1) /2,因此正确答案为n个(毕竟有偶数个节点,因此一定有一个度为1的节点)

6.1.3.题目三

一棵完全二叉树的节点数为531个,那么这棵树的高度为多少?

假设树的高度为K,假设最后一层缺了X个,则2^K - 1 - X = 531,而X的范围为[0, 2^(K-1) - 1](没缺,却得只剩一个)个节点,这样就可以解出532 <= 2^K <= 1062

6.1.4.题目四

某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH 。该完全二叉树的前序序列为?
A.ABDHECFG
B.ABCDEFGH
C.HDBEAFCG
D.HDEBFGCA

6.1.5.题目五

二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为?
A.E
B.F
C.G
D.H

尽管简单,但是根据两序是可以推导出整棵树的,前提是要有一序为中序,就是要注意空节点的情况

6.1.6.题目六

设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为?
A.adbce
B.decab
C.debac
D.abcde

6.2.编程练习题

6.2.1.相同的树

力扣https://leetcode.cn/problems/same-tree/

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    if(p == NULL && q == NULL)
    {
        return true;
    }
    if(p == NULL && q != NULL)
    {
        return false;
    }
    if( p!= NULL && q == NULL)
    {
        return false;
    }
    if(p->val != q->val)
    {
        return false;
    }
    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right); 
}

6.2.2.对称二叉树

力扣https://leetcode.cn/problems/symmetric-tree/

bool _isSymmetric(struct TreeNode* leftTree, struct TreeNode* rightTree)
{
    if(leftTree == NULL && rightTree == NULL)
    {
        return true;
    }
    if(leftTree == NULL && rightTree != NULL)
    {
        return false;
    }
    if(leftTree != NULL && rightTree == NULL)
    {
        return false;
    }
    if(leftTree->val != rightTree->val)
    {
        return false;
    }
    return _isSymmetric(leftTree->left, rightTree->right) && _isSymmetric(leftTree->right, rightTree->left);
}
bool isSymmetric(struct TreeNode* root)
{
    if(root == NULL)
    {
        return true;
    }
    return _isSymmetric(root->left, root->right);
}

6.2.3.二叉树的前序遍历

力扣https://leetcode.cn/problems/binary-tree-preorder-traversal/

int __preorderTraversal(struct BinaryTreeNode* root)
{
    if (root == NULL)
    {
        return 0;
    }
    return 1 + __preorderTraversal(root->left) + __preorderTraversal(root->right);
}
void _preorderTraversal(struct BinaryTreeNode* root, int* arr, int* i)
{
    if (root == NULL)
    {
        return;
    }
    arr[*i] = root->data;
    if (root->left != NULL)
    {
        (*i)++;
        _preorderTraversal(root->left, arr, i);
    }
    if (root->right != NULL)
    {
        (*i)++;
        _preorderTraversal(root->right, arr, i);
    }
}

6.2.4.二叉树的中序遍历

力扣https://leetcode.cn/problems/binary-tree-inorder-traversal/

6.2.5.二叉树的后序遍历

力扣https://leetcode.cn/problems/binary-tree-postorder-traversal/

6.2.6.另一棵树的子树

力扣https://leetcode.cn/problems/subtree-of-another-tree/

bool isSameTree(struct TreeNode* p, struct TreeNode* q)//本质是前序遍历
{
    if(p == NULL && q == NULL)
    {
        return true;
    }
    if(p == NULL && q != NULL)
    {
        return false;
    }
    if(p!= NULL && q == NULL)
    {
        return false;
    }
    if(p->val != q->val)
    {
        return false;
    }
    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right); 
}
bool isSubtree(struct TreeNode* root_1, struct TreeNode* root_2)
{
    if(root_1 == NULL && root_2 == NULL)
    {
        return true;
    }
    if(root_1 == NULL || root_2 == NULL)
    {
        return false;
    }
    bool b = isSameTree(root_1, root_2);
    return b || isSubtree(root_1->left, root_2) || isSubtree(root_1->right, root_2);
}

6.2.7.二叉树遍历

https://www.nowcoder.com/practice/4b91205483694f449f94c179883c1fef?tpId=60&&tqId=29483&rp=1&ru=/activity/oj&qru=/ta/tsing-kaoyan/question-ranking(牛客)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdbool.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BinaryTreeNode;
BinaryTreeNode* BuyNode(BTDataType x)
{
	BinaryTreeNode* cache = (BinaryTreeNode*)malloc(sizeof(BinaryTreeNode));
	if (!cache) exit(-1);
	cache->data = x;
	cache->left = NULL;
	cache->right = NULL;
	return cache;
}
BinaryTreeNode* BinaryTreeCreate(BTDataType* arr, int n, int* pi)
{
	BinaryTreeNode* root = NULL;
	if (arr[*pi] == '#')
	{
		(*pi)++;
		return root;
	}
	root = BuyNode(arr[(*pi)++]);
	root->left = BinaryTreeCreate(arr, n, pi);
	root->right = BinaryTreeCreate(arr, n, pi);
	return root;
}
void BinaryTreeInOrder(BinaryTreeNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%c ", root->data);
	BinaryTreeInOrder(root->right);
}
int main()
{
    char arr[100];
	int scanfreturn = scanf("%s", arr);
	int n = strlen(arr);
	int i = 0;
	BinaryTreeNode* tree = BinaryTreeCreate(arr, n, &i);
    BinaryTreeInOrder(tree);
    return 0;
}

6.2.8.二叉树的最大深度/高度

力扣https://leetcode.cn/problems/maximum-depth-of-binary-tree/

int maxDepth(struct BinaryTreeNode* root)
{
    if (root != NULL)
    {
        int a = maxDepth(root->left);
        int b = maxDepth(root->right);
        return a >= b ? a + 1 : b + 1;
    }
    return 0;
}
//在三目表达式中直接放入函数效率会很低

6.2.9.平衡二叉树

力扣https://leetcode.cn/problems/balanced-binary-tree/

6.2.10.二叉树的层序遍历

力扣https://leetcode.cn/problems/binary-tree-level-order-traversal/

6.2.11.求二叉树的节点个数

力扣https://leetcode.cn/problems/count-complete-tree-nodes/
这道题目比较捞的写法就是强行返回静态变量或使用全局变量,因此使用递归专有的计数方法(在return中计数是目前最好的)

//强行返回静态变量(已经经过编译,但是过不去力扣,只能过一次)
int countNodes(struct BinaryTreeNode* root)
{ 
  	static int SIZE = 0;
  	if (root == NULL)
    {
      	return SIZE;
    } 
  	SIZE++; 
  	countLeafNode(root->left);
  	countLeafNode(root->right); 
  	return SIZE; 
}
//并且哪怕是调用完后也没有办法将SIZE重置为0,这是因为SIZE是一个局部的静态变量,在main函数中不可见
//使用全局变量(已经经过编译,但是过不去力扣,只能过一次)
int SIZE = 0;
int countNodes(struct BinaryTreeNode* root)
{
  	if(root == NULL)
    {
      	return SIZE;
    }
  	SIZE++;
	countLeafNode(root->left);
  	countLeafNode(root->right);
  	return SIZE;
}
int main()
{
	printf("%d\n", SIZE);
  	return 0;
}
//这个函数调用完后还可以将SISE初始化为0,这是因为SIZE在本代码中是一个全局变量,因此对于main函数来说是可见的,但是但是,这会大大增加函数使用的错误概率,因为每一次都需要手动置空,否则就会重复计数。
//在return中计数:较优秀的写法(本质是使用了后序)
int countNodes(struct BinaryTreeNode* root)
{
    if (root == NULL)
    {
        return 0;
    }
    return 1 + countNodes(root->left) + countNodes(root->right);
}
//在return中计数:较简洁的写法(本质是使用了后序)
int countNodes(struct TreeNode* root)
{
    return root == NULL ? 0 : 1 + countNodes(root->left) + countNodes(root->right);
}

6.2.12.求二叉树的叶子节点个数

我暂时没有在牛客网和力扣找到这个题目,不过您也可以自己测一测

//写法一(已经过编译测试)
int countLeafNode(struct BinaryTreeNode* root)
{ 
  	if (root == NULL) 
    { 
      	return 0; 
    }
  	//1.根节点知道自己不是叶子节点的情况
  	if (root->left != NULL || root->right != NULL)
    { 
      	return countLeafNode(root->left) + countLeafNode(root->right); 
    }
  	//2.根节点知道自己是叶子节点的情况 
  	return 1;
}
//写法二(已经过编译测试,和上面的写法是一样的)
int countLeafNode(struct BinaryTreeNode* root)
{
    if (root == NULL)
    {
        return 0;
    }
    if (root->left == NULL && root->right == NULL)
    {
        return 1;
    }
    return (countLeafNode(root->left) + countLeafNode(root->right));
}

6.2.13.求二叉树某一层的节点个数

我暂时没有在牛客网和力扣找到这个题目,不过您也可以自己测一测

int BTreeLevelKSize(BTNode* root,int k)
{
	assert(k > 0);
	if (root == NULL)
	{
		return 0;
    }
	if(k == 1)	
	{
      	return 1;
    }
	return BTreeLevelKSize(root->left, k - 1) + BTreeLevelKSize(root->right, k - 1);
}

6.2.14.查找二叉树的某一节点并返回指向该节点的指针

我暂时没有在牛客网和力扣找到这个题目,不过您也可以自己测一测

BinaryTreeNode* BTreeFind_1(BinaryTreeNode* root, int x)
{
    if (root == NULL)
    {
        return NULL;
    }
    if (root->data == x)
    {
        return root;
    }
    BinaryTreeNode* p1 = BTreeFind_1(root->left, x);
    if (p1) return p1;
    BinaryTreeNode* p2 = BTreeFind_1(root->right, x);
    if (p2) return p2;

    return NULL;
    //这里要深刻理解递归的返回值有可能不会直接回到主函数
}
//----------------------------------------
BinaryTreeNode* BTreeFind_2(BinaryTreeNode* root, int x)
{
    if (root == NULL)
    {
        return NULL;
    }
    if (root->data == x)
    {
        return root;
    }
    BinaryTreeNode* p1 = BTreeFind_2(root->left, x);
    if (p1) 
        return p1;

    return BTreeFind_2(root->right, x);
    //这里要深刻理解递归的返回值有可能不会直接回到主函数
}

6.2.15.单值二叉树

力扣https://leetcode.cn/problems/univalued-binary-tree/

//多加一个小接口比较好操作
bool _isUnivalTree(struct TreeNode* root, int x)
{
    if(root == NULL)
    {
        return true;
    }
    if(root->val != x)
    {
        return false;
    }
    return _isUnivalTree(root->left, x) && _isUnivalTree(root->right, x);
}
bool isUnivalTree(struct TreeNode* root)
{
    if(root == NULL)
    {
        return false;
    }
    return _isUnivalTree(root, root->val);
}
bool isUnivalTree(BinaryTreeNode* root)
{
    if (root == NULL)
    {
        return true;
    }
    if (root->left && root->data != root->left->data)
    {
        return false;
    }
    if (root->right && root->data != root->right->data)
    {
        return false;
    }
    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

6.2.16.二叉树查找值为x的节点

我暂时没有在牛客网和力扣找到这个题目,不过您也可以自己测一测

BinaryTreeNode* BinarytreeFind(BinaryTreeNode* root, int x)
{
    if (root == NULL)
    {
        return NULL;
    }
    if (root->data == x)
    {
        return root;
    }
    BinaryTreeNode* a = BinarytreeFind(root->left , x);
    BinaryTreeNode* b = BinarytreeFind(root->right, x);
    if (a != NULL)
    {
        return a;
    }
    if (b != NULL)
    {
        return b;
    }
    return NULL;
}
//优化代码
BinaryTreeNode* BinarytreeFind(BinaryTreeNode* root, int x)
{
    if (root == NULL)
    {
        return NULL;
    }
    if (root->data == x)
    {
        return root;
    }
    BinaryTreeNode* a = BinarytreeFind(root->left , x);
    if (a != NULL)//这里拉到前面可以减少运算
    {
        return a;
    }
    BinaryTreeNode* b = BinarytreeFind(root->right, x);
    if (b != NULL)
    {
        return b;
    }
    return NULL;
}
//这个思路很清奇,但是可维护性、易读性降低了
BinaryTreeNode* BinarytreeFind(BinaryTreeNode* root, int x)
{
    if (root == NULL)
    {
        return NULL;
    }
    if (root->data == x)
    {
        return root;
    }
    BinaryTreeNode* a = BinarytreeFind(root->left , x);
    if (a != NULL)
    {
        return a;
    }
    return BinarytreeFind(root->right, x);
}
  • 从大量的递归练习中,可以发现除了设立count变量计数,还可以使用递归计数,这是一个很重要的知识
  • 递归需要注意其返回值不一定是返回主函数,返回值永远是返回调用函数的地方
  • 在二叉树中理解递归的最快方法是画递归展开图,而递归展开图有两种图解方法
    • 第一种是直接在代码内展开,符合直线逻辑,但是写起来复杂
    • 第二种是靠代码图/二叉树图展开来理解,这种比较简单,但是有可能绕晕,画多了优于第一种方法

7.二叉树的相关常用接口

  • 如果单纯使用二叉树存储数据就没有太大意义(因为有链表这样优秀的存储结构,二叉树就太复杂了),但是如果加以一定的规则,比如:规定左子树上所有节点小于根节点,右子树上所有节点大于根节点,变成一棵搜索二叉树(下面就是一棵典型的二叉树例子),那么该二叉树就具有很大的意义了。
  • 现在,我们针对二叉树在日常生活中的使用,给出几个具体实用的常用接口,由于篇幅原因,在后续的博文里我会给出下面接口的具体实现方式。
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树 
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi); 

// 二叉树销毁 
void BinaryTreeDestory(BTNode** root); 

// 二叉树节点个数
int BinaryTreeSize(BTNode* root);

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root); 

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k); 

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);

// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root); 

// 二叉树中序遍历 
void BinaryTreeInOrder(BTNode* root); 

// 二叉树后序遍历 
void BinaryTreePostOrder(BTNode* root); 

// 层序遍历 
void BinaryTreeLevelOrder(BTNode* root); 

// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/612468.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

我的创作纪念日|写在CSDN创作第512天

机缘 今天无意中发现CSDN后台给我发送私信&#xff0c;才发觉原来我的第一篇博客更新已经过去512天了&#xff0c;512天一晃而过居然还有点恍然。 作为一名网络专业的在校大学生&#xff0c;最初开始查找相关的资料其实更习惯于从外站进行查找&#xff0c;却总是在不经意中进入…

人事管理项目-前端实现

人事管理项目-前端实现 引入Element和Axios开发Login页面配置路由配置请求转发启动前端项目 引入Element和Axios 前端UI使用Element&#xff0c;网络请求则使用Axios&#xff0c;因此首先安装Element和Axios依赖&#xff0c;代码如下&#xff1a; 依赖添加成功后&#xff0c;接…

N-propargyloxycarbonyl-L-lysine,1215204-46-8,是一种基于赖氨酸的非天然氨基酸 (UAA)

产品描述&#xff1a; N-ε-propargyloxycarbonyl-L-lysine (H-L-Lys(Poc)-OH) 是一种基于赖氨酸的非天然氨基酸 (UAA)。 广泛用于多种生物体中荧光探针的生物偶联。 N- ε- Propargyloxycarbonyl-L-lysine (H-L-Lys (Poc) - OH) is a non natural amino acid (UAA) based on …

Kotlin Channel系列(一)之读懂Channel每一行源码

文章目录 有话说概述初识ChannelChannel种类Channel五大金刚SendReceiveClosedQueueBuffer Channel的行为Channel源码分析发送数据大动脉接收数据大动脉父类默认实现方式(RendezvousChannel)发送流程send()函数onSend()函数 接收流程receiveCatching()函数onReceiveCatching()函…

基于图像处理的圆检测与深度学习

基于图像处理的圆检测与深度学习 摘 要一、 绪论二 、图像预处理2.1 滤波算法2.2 边缘检测 三 、圆识别与定位算法3.2 定位算法3.2.1 迭代算法 4.1 数据处理 五、深度学习介绍&#xff1a;参考文献 摘 要 本文主要论述在图像处理的的基础上&#xff0c;为了克服图像背景中的亮…

SpringBootWeb案例-2(下)

3. 修改员工 需求&#xff1a;修改员工信息 在进行修改员工信息的时候&#xff0c;我们首先先要根据员工的ID查询员工的信息用于页面回显展示&#xff0c;然后用户修改员工数据之后&#xff0c;点击保存按钮&#xff0c;就可以将修改的数据提交到服务端&#xff0c;保存到数据…

面试专题:java虚拟机(2)

8.垃圾收集有哪些算法&#xff0c;各自的特点&#xff1f; 标记清除 直接将要回收的对象标记&#xff0c;发送gc的时候直接回收&#xff1a;特点回收特别快&#xff0c;但是回收以后会造成 很多不连续的内存空间&#xff0c;因此适合在老年代进行回收&#xff0c;CMS(current…

Linux--ServerProgramming--(4)详解 I/O复用

1. I/O 复用功能 I/O 复用能同时监听多个文件描述符。 I/O 复用本身是阻塞的。 当有多个文件描述符同时就绪时&#xff1a;若不采取额外措施&#xff0c;程序就只能按顺序一次处理其中的每一个文件描述符&#xff0c;这使得服务器程序看起来是串行工作的。若要实现并发&#…

ChatGPT市场营销指南震撼出炉,你错过了?!

ChatGPT是一种基于AI技术的语言模型&#xff0c;它可以与用户进行对话和交互。它被广泛应用于各个领域&#xff0c;包括市场营销。作为一名市场营销人员&#xff0c;您可以使用ChatGPT来获得创意、解决问题和生成内容。 下面是190个ChatGPT提示&#xff0c;可帮助营销人员更好…

oracle自定义函数 for in loop示例

1、新建type&#xff0c;就是返回结果集有什么&#xff0c;这里就写什么&#xff08;相当于表的字段&#xff09; CREATE OR REPLACE TYPE "TYPE_NQ_FORM_STATISTICS" as object (recordid varchar2(500),form_name varchar2(200),sortone varchar2(100),sorttwo …

华为od机试题目回顾

今天去做了华为机试&#xff0c;两道一星题&#xff0c;一道二星题。 一星题 1&#xff1a; 题目主要大意&#xff1a; 输入一串字符串&#xff0c;里面可能包含有(x,y)的坐标。 0<x<1000&#xff0c;0<y<1000&#xff0c;类似(01,1)、(1,01)、(0,100)的都是非法坐…

Java开发手册中为什么不建议在for循环中使用“+“进行字符串操作

场景 java开发手册中对于循环体中进行字符串的拼接要求如下&#xff1a; 【推荐】循环体内&#xff0c;字符串的连接方式&#xff0c;使用 StringBuilder 的 append 方法进行扩展。 说明&#xff1a;下例中&#xff0c;反编译出的字节码文件显示每次循环都会 new 出一个 Str…

【Linux】-自动化构建工具(make/makefile)

作者&#xff1a;小树苗渴望变成参天大树 作者宣言&#xff1a;认真写好每一篇博客 作者gitee:gitee 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; 文章目录 前言 前言 今天我们来讲讲再Linux中开发必备的一项技能&#xff0c;没有这个…

【OpenMMLab AI实战营第二期】深度学习预训练与MMPretrain

深度学习预训练与MMPretrain MMPreTrain 算法库介绍 MMPretrain 是一个全新升级的预训练开源算法框架&#xff0c;旨在提供各种强大的预训练主干网络&#xff0c; 并支持了不同的预训练策略。MMPretrain 源自著名的开源项目 MMClassification 和 MMSelfSup&#xff0c;并开发…

项目干系人管理实用方法,让你的项目顺风顺水

项目管理中的干系人是每个项目的一个重要方面&#xff0c;因为项目的结果取决于他们。然而&#xff0c;管理各种各样的干系人的艺术很有挑战性。在项目管理中根本没有出错的余地&#xff0c;本文将带你了解项目干系人以及如何管理他们以促进项目的全面成功。 谁是项目管理的干…

第3章:SpringMVC获取请求参数

一、SpringMVC获取请求参数 1.通过servletAPI获取 将HttpServletRequest作为控制器方法的形参&#xff0c;此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象 <a th:href"{/testServletAPI(usernameadmin,password123456)}">测试API<…

MCGS昆仑通态触摸屏导入博途自定义数据类型和DB块变量的具体方法演示

MCGS昆仑通态触摸屏导入博途自定义数据类型和DB块变量的具体方法演示 如下图所示,在博途中新建项目后,添加自己所需的数据类型,然后选中该数据类型,右击选择“从块生成源“—”仅所选块“, 如下图所示,设置文件名后点击保存, 如下图所示,选中需要导出的DB块,右击选…

人事项目开发记录-登录模块

人事项目开发记录 后端接口实现 后端接口实现 后端权限认证采用Spring Security实现&#xff08;本小节中大量知识点与第10章的内容相关&#xff0c;需要读者熟练掌握第10章的内容&#xff09;&#xff0c;数据库访问使用MyBatis&#xff0c;同时使用Redis实现认证信息缓存。因…

如何在Centos的SSH2终端中终止-停止-结束某个Python程序的运行?

python3 /opt/python_scripts/retr/P-0006.py &我在运行上面的命令后&#xff0c;得到了下面的提示&#xff1a; [1] 42335 如果我想终止这个我自己写的Python程序&#xff0c;该怎么做呢&#xff1f; 答&#xff1a; 当您在后台运行一个命令时&#xff0c;终端会显示类似…

Django中使用openldap实现账号的统一管理

了解和安装 ldap ldap介绍 LDAP&#xff08;Lightweight Directory Access Protocol&#xff09;是一种轻量级的目录访问协议&#xff0c;它用于访问和维护分布式目录服务。 LDAP最初设计用于提供对X.500目录服务的简化访问&#xff0c;后来被广泛应用于各种应用程序和系统中…