【数据结构与算法】树(下):堆的实现与应用,二叉树的实现与基本操作

目录

本文概要:本篇主要讲解了堆的实现与原理,同时扩展了堆的应用------堆排序与TopK问题带你感受堆的核心思想与用途。第二节主要讲解了二叉树链式的实现以及二叉树的各种操作带你感受递归的暴力美学

堆(Heap)

1. 堆的概念

是一种特殊的完全二叉树,其满足如下性质:

堆中的任意一个子树满足子树的根节点是这个子树所有节点的最值

因此 又分为大堆(大根堆)小堆(小根堆)

  • 大堆:子树的根节点是这个子树所有节点的最大值
  • 小堆:子树的根节点是这个子树所有节点的最小值


小堆示意图

如上图:

"123456"子树的根节点 10 满足为子树最小值

"245"子树的根节点 13 满足为子树的最小值

"36"子树的根节点 43 满足......

!IMPORTANT

【关键知识】对于一个从0下标开始存储数据的堆满足:

  • 任意一个节点 i父节点 parent = (i - 1) / 2; 特别的parent < 0 代表该节点没有父节点
  • 任意一个节点 i左孩子节点 leftchild = i * 2 + 1; 特别的 leftchild > n 代表该节点没有左孩子
  • 任意一个节点 i右孩子节点 rightchild = i * 2 + 2; 特别的 rightchild > n 代表该节点没有右孩子

2. 堆的实现

堆的结构

cpp 复制代码
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* data; //数组
	int size;         //堆中有效元素个数
	int capacity;     //堆的容量
}Heap;

初始化与销毁

c 复制代码
//初始化
void HeapInit(Heap* php)
{
	assert(php);
	php->data = NULL
	php->size = php->capacity = 0;
}
//销毁
void HeapDestroy(Heap* php)
{
	assert(php);
	if(php->data)
		free(php->data);
	php->data = NULL;
	php->size = php->capacity = 0;
}

以上两个操作较为简单,读者直接对照即可

2.1 堆的插入数据

堆的插入数据只能从尾部插入,因为堆满足根节点是子树所有节点的最值,如果从任意位置插入的话会破坏整个堆的结构(因为从任意位置插入的话根据数组后面的元素整体要往后移动这样堆的结构会被破坏了)

c 复制代码
void HeapPush(Heap* php, HPDataType x)
{
    assert(php);
    //判断是否需要扩容
    if(php->size = php->capacity)
    {
        int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
        int* tmp = realloc(php->data, sizeof(HPDataType) * newcapacity);
        if(tmp == NULL)
        {
            perror("realloc fail!");
            exit(1);
        }
        php->data = tmp;
        php->capacity = newcapacity;
    }
    //插入元素
    php->data[php->size] = x;
    //向上调整
    HeapUp(php->data, size);
    php->size++;   //先调整再++
}

到这一步数据确实是插入了,但是这样就完了吗?到这一步仅仅只是插入了元素,但是这个结构还没有维护,所以此时这个插入操作还没有完成

堆的维护(插入元素后)

假设插入元素后此时堆的元素个数为 n ,所以易得此时前 n - 1 个节点还是保持堆结构的,所以维护堆结构就是给这个新插入的元素找到它适合的位置。

2.2 向上调整算法

以小堆为例:此时需要给新插入的元素找到适合的位置使得堆结构完好。根据小堆的性质,每个子树的根节点是这个子树的最小值,反过来看就是每个节点都会比自己的父节点更大 。所以调整新节点其实就是判断这个节点的值会不会比它的父节点更小,如果更小的话就需要将这两个节点交换位置(这样才能满足堆的特性)。继续用这个节点和它新的父节点比较,直到满足父节点值会比它小或者到根节点。这个过程就是向上调整算法

c 复制代码
//时间复杂的O(logn)
void HeapUp(HPDataType* hp, int child)
{
    int parent = (child - 1) / 2;
    //小堆:<
    //大堆:>
    while(child > 0)
    {
        if(hp[child] < hp[parent])
        {
            Swap(&hp[child], &hp[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }else{
            break;
        }
    }
}

所以在插入元素之后,还需要为新插入的元素执行一次向上调整算法以满足堆的结构

2.3 堆的头部删除数据

堆的头部删除元素和普通数组的头删不一样,因为堆的头删需要保证在操作完之后还能维护这个结构。当直接让堆的所有数据往前覆盖一个的话,同样会发现上面的问题,即堆的结构完全被破坏了 。那堆的头删操作要如何进行呢?将堆顶元素与堆的最后一个元素交换位置,再将最后一个元素删掉

经过上面的操作后,不难发现此时堆结构除了堆顶元素可能不满足堆的性质,其他的点都还是和原来一样的。所以此时只需要处理堆顶这一个元素即可,这就是为什么头删要先交换,再删除。处理堆顶的元素的思路和向上调整算法的核心思路大差不差,主要就是要满足堆的性质

以小堆为例:此时要满足子树的根节点比它的所有孩子节点小,但是此时有两个孩子,只需要找到更小的孩子(如果它比更小的孩子还要小那肯定会比另外一个孩子更小),如果此时这个根节点的值比更小的孩子的值更小,那就不用调整它的位置,反之就让两个节点交换,然后继续向下比较看是否能满足堆的性质,这个操作就叫向下调整算法

2.4 向下调整算法
c 复制代码
//向下调整算法 时间复杂度O(logn)
void HeapDown(HPDataType* hp, int parent, int n)
{
    int child = parent * 2 + 1;
    //小堆:<
    //大堆:>
    while(child < n)
    {
        if(child + 1 < n && hp[child + 1] < hp[child])
            child++;
        if(hp[child] < hp[parent])
        {
            Swap(&hp[child], &hp[parent]);
            parent = child;
            child = parent * 2 + 1;
        }else{
            break;
        }
    }
}
//判断堆是否为空
bool HeapEmpty(Heap* php)
{
    assert(php);
    return php->size == 0;
}

//头删
void HeapPop(Heap* php)
{
    assert(!HeapEmpty(php));   //首先要判断堆是否为空
    //交换头部元素和尾部元素
    Swap(&php->data[0], &php->data[size - 1]);
    php->size--;
    //向下调整算法
    HeapDown(php->data, 0, php->size);
}

3. 堆的应用

3.1 堆排序

由上面的堆的插入和删除以及向上调整算法和向下调整算法可以发现,这些操作本质上都是在维护堆顶是这个堆的最值的性质。

【思考】给定一个数组,是否可以使用堆来实现排序呢?

  • 方案一(升序):将数组中的元素全部插入进堆结构中,每次拿堆顶元素从数组第一个位置开始尾插。因为小堆的堆顶元素一直是最小值,所以理论上是可以实现的
cpp 复制代码
void Heapsort(int* arr, int n)
{
    //将数据先全部插入进堆中
    for(int i = 0; i < n; i++)
    {
        HeapPush(Heap* php, arr[i]);
    }
    //往数组中赋值
    for(int i = 0; i < n; i++)
    {
        arr[i] = php->data[0];
        HeapPop(php);
    }
}

方案一是可以的,但是问题又来了。使用方案一来实现堆排序之前你需要先写一个这个数据结构出来啊!!!这样效率实在是太低了,堆排序的实现方法也不是这样的

  • 方案二(升序):在原数组上建大堆,拿堆顶元素和堆的最后一个元素交换,堆的元素个数减一同时对堆顶元素执行向下调整算法

【思考】如何在原数组上建堆呢?

由前面堆的实现我们可以发现,在执行向上调整算法和向上调整算法来维护堆的前提是堆除了这个需要调整的子树之外,其他的子树都是一个堆。而原数组的话不一定满足这样的条件所以不能从堆顶开始入手。那么大的子树无法下手,是否可以从小的子树开始调整,答案是可以的

3.1.1 向下调整算法建堆

【方法】将原数组看成是一个堆从最后一个非叶子节点开始往堆顶执行向下调整算法,循环结束时堆结构完成

提示:最后一个非叶子节点也就是最后一个节点的父节点即(n - 1 - 1) / 2

cpp 复制代码
void Heapsort(int* arr, int n)
{
    for(int i = (n - 1 - 1) / 2; i >= 0; i++)
    {
        HeapDown(arr, i, n);
    }
}

当这个循环结束时,堆就在原数组上建好了


堆建好之后就开始交换,每次拿堆顶元素和最后一个元素交换,堆的元素个数减一(也就是不要最后那个点了),在堆顶执行一次向下调整算法

【原理】因为维护的是大堆,所以堆顶元素一直是当前堆中所有元素的最大值。将堆顶元素和堆最后一个元素交换就是将当前的最大值放在数组的后面,同时堆元素减一就是确定的该元素的位置。之后因为堆顶元素被改变了所以队堆顶执行一次向下调整算法即可

cpp 复制代码
void Heapsort(int* arr, int n)
{
    //建堆
    for(int i = (n - 1 - 1) / 2; i >= 0; i++)
    {
        HeapDown(arr, i, n);
    }
    int end = n - 1;
    while(end > 0)
    {
        Swap(&arr[0], &arr[end]);
        HeapDown(arr, 0, end);   //end先不用减一因为此时end刚好是堆中有效元素的个数
        end--;
    }
}

堆排序的原理简单说就是建好堆之后,每次都可以确定一个堆中当前最大值将它从后往前依次放在数组中

此时,排序就完成了


3.1.2 向上调整算法建堆

上面实现了使用向下调整算法建堆,那么堆排序是否可以用到向上调整算法呢?

【思考】使用向上调整算法建堆

向下调整算法建堆的核心思路就是从最后一个节点个数大于1的子树开始执行 ,那么向上调整算法也可以照着这个思路。往上的最后一个子树,刚好就是根节点。即从根节点开始执行向上调整算法直到堆的最后一个节点

cpp 复制代码
//堆排序 大堆-->升序   小堆-->降序
void Heapsort(int* arr, int n)
{
	//向下调整建堆 从最后一个非叶子节点开始
	/*for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		HeapDown(arr, i, n);
	}*/

	//向上调整建堆
	for (int i = 0; i < n; i++)
	{
		HeapUp(arr, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		HeapDown(arr, 0, end);   //end刚好是此时堆里面元素的个数
		end--;
	}
}

使用向上调整算法来建堆也可以得出结果

【结论与思考】堆排序需要先在原数组上建堆,可以使用向下调整算法建堆,也可以使用向上调整算法建堆。既然有两种方法建堆,那肯定有一个方法是更优的,那到底哪种方法是最优的呢?可以对比两种方法的时间复杂度

时间复杂度的分析

为了方便计算时间复杂度,就以满二叉树的为例计算建堆的时间复杂度

向下调整算法建堆

假设满二叉树的高度为 k,且每次执行向下调整算法都是最坏的情况

所以不难推导出如下:

  • 第一层:节点个数:1 每个节点向下调整的次数:k - 1
  • 第二层:节点个数:2 每个节点向下调整的次数:k - 2
  • 第三层:节点个数:4 每个节点向下调整的次数:k - 3
  • ......
  • 第k - 1层:节点个数:2^(k - 2) 每个节点向下调整的次数:1

总调整次数: 2 0 ∗ ( k − 1 ) + 2 1 ∗ ( k − 2 ) + 2 2 ∗ ( k − 3 ) + ... ... + 2 k − 2 2^0 * (k - 1) + 2^1 * (k - 2) + 2^2 * (k - 3) + ...... + 2^{k-2} 20∗(k−1)+21∗(k−2)+22∗(k−3)+......+2k−2

不难发现这就是一个等差数列乘等比数列直接使用错位相减法,最终得到:

T ( k ) = 2 k − 1 − k T(k) = 2^k - 1 - k T(k)=2k−1−k

根据满二叉的性质: k = l o g 2 n , n = 2 k − 1 k = log_2 n, n = 2^k - 1 k=log2n,n=2k−1

所以向下调整算法建堆的时间复杂度为: O ( n − l o g 2 n − 1 ) ≈ O ( n ) O(n - log_2 n - 1) \approx O(n) O(n−log2n−1)≈O(n)

向上调整算法建堆

前置情况也是和上面的一样,所以可以推导出:

  • 第一层:节点个数:1 每个节点向上调整的次数:0
  • 第二层:节点个数:2 每个节点向上调整的次数:1
  • 第三层:节点个数:4 每个节点向上调整的次数:2
  • ......
  • 第k - 1层:节点个数:2^(k - 2) 每个节点向上调整的次数:k - 2
  • 第k层:节点个数:2^(k - 1) 每个节点向上调整的次数:k - 1

总调整次数: 2 1 ∗ 1 + 2 2 ∗ 2 + 2 3 ∗ 3 + ... ... + 2 k − 2 ∗ ( k − 2 ) + 2 k − 1 ∗ ( k − 1 ) 2^1 * 1 + 2^2 * 2 + 2^3 * 3 +...... + 2^{k - 2} * (k - 2) + 2^{k - 1} * (k - 1) 21∗1+22∗2+23∗3+......+2k−2∗(k−2)+2k−1∗(k−1)

由上式可以得到: T ( k ) = 2 k ∗ ( k − 2 ) + 2 T(k) = 2^k * (k - 2) + 2 T(k)=2k∗(k−2)+2

所以向上调整算法建堆的时间复杂度为: O ( ( n + 1 ) ∗ ( l o g 2 ( n + 1 ) − 2 ) + 2 ) ≈ O ( n l o g 2 n ) O((n + 1) * (log_2 (n + 1) - 2) + 2) \approx O(nlog_2 n) O((n+1)∗(log2(n+1)−2)+2)≈O(nlog2n)

【总结】综上使用向下调整算法的时间复杂度更加低,效率更高。注意这里不要搞混了,上面推导的是建堆的复杂度,而向上调整算法和向下调整算法建堆的时间复杂都是 O ( l o g 2 n ) O(log_2 n) O(log2n)

由上面的分析可知堆排序使用向下调整算法建堆更高效,同时可以算出堆排序的时间复杂度为: O ( n + n l o g 2 n ) ≈ O ( n l o g 2 n ) O(n + nlog_2 n) \approx O(nlog_2 n) O(n+nlog2n)≈O(nlog2n)


3.2 TopK问题

还可以用来解决TopK问题,例如:

世界前k强公司,n个人中最大的得分的前k个人,n个球手中投篮失误率最低的前k个人......

!TIP

场景设置

假设现在你在一个公司面试,面试官给你n(0 <= n <= 1000000000)个数据让你求出这个n个数据的前k(k <= 100)小的数据,并且空间限制为1kb

  • 方法一:首先开一个数组将n个数据存下来,同时在数组上建堆,每次拿到堆顶的元素从小到大依次放进结果数组中(首先存储数据就行不通了,因为空间最大只有1kb所以这个方案pass)

!IMPORTANT

上述的方案不可行,此时就需要利用堆的性质来解决这个问题。我们可以发现一个性质即前k个小的数据的最大值一定会小于等于那n - k个数据。因为堆可以保持让堆中元素最值在堆顶,所以可以先建一个容量为k的大堆将前k个数据存储到这个堆中。因为大堆可以让这前k个数据的最大值在堆顶,所以可以遍历剩下的n - k个数据,每次和堆顶元素比较,如果比堆顶元素更小就代表堆顶这个元素肯定不在前k小的数据中,用这个元素覆盖堆顶再维护堆结构。同样的,如果是前k小的数据,那就一定会入堆

  • 方法二:维护一个容量为k的大堆同时将前k个数据插入到堆中,遍历剩下的元素,每次和堆顶比较,如果比堆顶小就覆盖堆顶的值,再继续维护大堆

关于如何存储n个数据:可以先将n个数据放进文件中,然后在文件里面读取数据即可

cpp 复制代码
void TopK(int k)
{
    FILE* fout = fopen("data.txt", "r");
    if(fout == NULL)
    {
        perror("fopen fail");
        exit(1);
    }
    //动态开辟k个空间
    int* heap = (int*)malloc(sizeof(int) * k);
    if(heap == NULL)
    {
        perror("malloc fail");
        exit(2);
    }
    //将前k个数据读取进堆中
    for(int i = 0; i < k; i++)
    {
        fscanf(fout, "%d", &heap[i]);
    }
    //建堆
    for(int i = (k - 1 - 1) / 2; k >= 0; k--)
    {
        HeapDown(heap, i, k);
    }
    //读取剩下的元素
    int x = 0;
    while(fscanf(fout, "%d", &x) != EOF)
    {
        if(heap[0] > x)
        {
            //覆盖堆顶元素
            heap[0] = x;
            HeapDown(heap, 0, k);
        }
    }
    //此时在原有堆上进行堆排序
    int end  = k - 1;
    while(end > 0)
    {
        Swap(&heap[0], &heap[end]);
        HeapDown(heap, 0, end);
        end--;
    }
    //最终结果
    for(int i = 0; i < k; i++)
    {
        printf("%d ", heap[i]);
    }
}

以上就是TopK问题,当然如果是自己尝试的话还有一步将数据写入文件的操作,操作如下:

cpp 复制代码
FILE* CreateData()
{
	const char* file = "data.txt";
	FILE* pfin = fopen(file, "w");
	if (pfin == NULL)
	{
		perror("fopen fail!");
		return;
	}
	int n = 100000;

	srand(time(0));
	for (int i = 0; i < n; i++) 
	{
		int x = (rand() + i) % 100000;
		fprintf(pfin, "%d\n", x);
	}

	fclose(pfin);
}

二叉树(BinaryTree)

1. 二叉树的实现与基本操作

【前置知识】上一篇文章讲了由于二叉树的多样性,如果使用数组来存储二叉树会浪费空间,所以一般使用链式存储来实现二叉树。且因为二叉树每个节点最多只有两个节点,所以使用二叉链来实现二叉树

1.1 二叉树的结构
c 复制代码
typedef char TDataType;   //树存储的数据类型
typedef struct BinaryTreeNode
{
	TDataType data;
	struct BinaryTreeNode* left;   //指向左孩子的指针
	struct BinaryTreeNode* right;  //指向右孩子的指针
}BTNode;
1.2 二叉树的基本操作

【注】因为树本身就是一个递归结构,所有关于树的各种操作基本上都是通过递归来实现的

c 复制代码
//创建节点
BTNode* BuyNode(TDataType x);

//创建二叉树
BTNode* CreateBinaryTree();

//前序遍历
void PreOrder(BTNode* root);

//中序遍历
void InOrder(BTNode* root);

//后序遍历
void PostOrder(BTNode* root);

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

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

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

//二叉树的深度/高度
int BinaryTreeDepth(BTNode * root);

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

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

//二叉树的层序遍历
void LevelOrder(BTNode* root);

//判断是否为完全二叉树
bool isPerfectBinaryTree(BTNode* root);

< 二叉树的基本操作 > <二叉树的基本操作> <二叉树的基本操作>

构建二叉树

在实现二叉树之前得先有二叉树,所以就需要手动先构建一个二叉树方便后续实现对二叉树得各种操作以及对操作的测试。构建二叉树就是正常的动态申请节点,然后构建树的结构即可,代码如下:

c 复制代码
//创建节点
BTNode* BuyNode(TDataType x)
{
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	if (newNode == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	newNode->data = x;
	newNode->left = newNode->right = NULL;
	return newNode;
}

//创建二叉树
BTNode* CreateBinaryTree()
{
	BTNode* Anode = BuyNode('A');
	BTNode* Bnode = BuyNode('B');
	BTNode* Cnode = BuyNode('C');
	BTNode* Dnode = BuyNode('D');
	BTNode* Enode = BuyNode('E');
	BTNode* Fnode = BuyNode('F');

	Anode->left = Bnode;
	Anode->right = Cnode;
	Bnode->left = Dnode;
	Bnode->right = Enode;
	Cnode->left = Fnode;

	return Anode;
}
前序遍历

在实现二叉树的遍历之前还需要先了解一个知识。即二叉树前中后序的遍历顺序

  • 前序遍历:根节点-->左子树-->右子树-->根节点-->左子树......
  • 中序遍历:左子树-->跟节点-->右子树-->左子树-->根节点......
  • 后序遍历:左子树-->右子树-->根节点-->左子树-->右子树......

前中后序的遍历就是分别按照如上的遍历顺序,这些遍历的节点都是以当前所在的子树为参考系的,就以如下图例:

前序遍历的结果为:A-->B-->D-->H-->I-->E-->C-->F-->G

中序遍历的结果为:H-->D-->I-->B-->E-->A-->F-->C-->G

后序遍历的结果为:H-->I-->D-->E-->B-->F-->G-->C-->A

所以前序遍历核心就是先遍历根节点,再左子树,再遍历右子树,因为这样的方案对于每个节点都适用且树本身就是递归来实现的,所以使用递归来实现遍历

c 复制代码
//前序遍历
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	printf("%c ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

代码解释

代码中首先的就是递归的出口,当遇到空节点的时候肯定就不用继续遍历了,所以直接返回。之后前序遍历的核心根,左,右,所以此时就先打印这个节点的值,之后要遍历左子树就再调用一次前序遍历传参是左孩子节点,之后再是右子树,这样前序遍历就完成了

【小技巧】实现递归时可以把递归函数当成是一个已经实现好了的函数,我们相信这个函数一定可以完成它的任务,只要处理好了当前子问题的所有情况和递归出口那这个相同名字的函数一定可以完成任务

中序遍历

中序遍历相比前序遍历只是变了一下顺序而已,所以递归函数里面改变一下顺序即可

c 复制代码
//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}
后序遍历
c 复制代码
//后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%c ", root->data);
}
二叉树节点的个数

求一个二叉树节点的个数,可以将问题分成小问题来看,对于每个二叉树都满足一个情况二叉树的节点个数 = 根节点 + 左子树的节点个数 + 右子树的节点个数,所以可以使用递归来解决这个重复的子问题

当递归遇到树的底层,即遇到叶子节点时因为左右子树都为空,所以它的节点个数为1,然后带着1返回上一层,这样一层一层的返回,最终得到整个二叉树的节点个数

c 复制代码
// ⼆叉树结点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
二叉树叶子节点个数

二叉树叶子节点的个数的思路和求节点个数的很像,一个树的根节点如果有左右子树那这个树的叶子节点个数 = 左子树的叶子节点个数 + 右子树的叶子节点个数

再来看递归的出口,判断一个节点是否为叶子节点直接看它的左右子树是否都为空即可,如果满足说明这个节点就是叶子节点,返回1即可。而如果是NULL的话直接返回0即可,空节点自然没有叶子节点

c 复制代码
// ⼆叉树叶⼦结点个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
二叉树第k层节点个数

对于根节点来说,第k层节点个数 = 它的左孩子和右孩子第k - 1层节点个数之和 ,所以可以每递归一层k--k = 1时代表找到该层的节点了,返回1即可,遇到NULL就返回0

c 复制代码
// ⼆叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	//如果不是在第k层 返回左子树开始第k - 1层的节点+右子树......
	return BinaryTreeLevelKSize(root->left, k - 1) 
	+ BinaryTreeLevelKSize(root->right, k - 1);
}
二叉树的深度/高度

二叉树的高度 = 根节点的高度 1 +左右子树更高的。那个就是整棵树的高度

c 复制代码
//⼆叉树的深度/⾼度
int BinaryTreeDepth(BTNode* root)
{
	if(root == NULL)
	{
		return 0;
	}
	//左子树的高度
	int leftDep = BinaryTreeDepth(root->left);
	//右子树的高度
	int rightDep = BinaryTreeDepth(root->right);
	return 1 + (leftDep > rightDep ? leftDep : rightDep);
}
二叉树查找值为x的节点

查找值为x的节点先判空,之后看根节点的值是否为x,如果不是就去左子树找,如果找到了就返回,没有找到就去右子树中找,如果都没有找到说明不存在该节点,返回NULL

c 复制代码
// ⼆叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, TDataType x)
{
	if(root = NULL)
	{
		return NULL;
	}
	if(root->data == x)
	{
		return root;
	}
	//在左子树中找该节点
	BTNode* leftFind = BinaryTreeFind(root->left, x);
	if(leftFind)
	{
		return leftFind;  //找到就返回
	}
	//在右子树中找该节点
	BTNode* rightFind = BinaryTreeFind(root->right, x);
	if(rightFind)
	{
		return rightFind;   //找打就返回
	}
	//左子树和右子树都没有找到直接返回NULL
	return NULL;
}
二叉树的销毁

二叉树的销毁其实就是像是后序遍历,因为如果先销毁了根节点它的左右孩子就找不到了,所以只能最后销毁根节点,而这销毁顺序就和后序遍历一样了先销毁左子树,再销毁右子树,最后销毁根节点

cpp 复制代码
// ⼆叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	if(root == NULL)
	{
		return;
	}
	//先销毁左右子树 注意这里传的是二级指针
	BinaryTreeDestory(&(*root)->left);
	BinaryTreeDestory(&(*root)->right);
	//最后再销毁根节点
	free(*root);
	*root = NULL;
}
相关推荐
JieE21216 小时前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
Jack201 天前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法
小小杨树1 天前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
JieE2122 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2122 天前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
vivo互联网技术2 天前
CVPR 2026 | 全新强化学习框架 BeautyGRPO:重塑真实人像
算法·大模型·cvpr·影像
Darling噜啦啦2 天前
列表转树算法深度解析:从 Map 到 Reduce 的两种实现,面试高频考点
数据结构·算法·面试
用户497863050732 天前
(一)小红的数组操作
算法·编程语言
怕浪猫2 天前
Electron 系列文章封面图
算法·架构·前端框架