【数据结构】二叉树(带图详解)


文章目录

    • 1.树的概念
      • 1.2 树的结构
        • 孩子表示法
        • 孩子兄弟表示法
      • 1.3 相关概念
    • 2.二叉树的概念及结构
      • 2.1 二叉树的概念
      • 2.2 数据结构中的二叉树-五种形态
      • 2.3 特殊的二叉树
      • 2.4 二叉树的存储结构
        • 顺序存储
        • 链式存储
      • 2.5 二叉树的性质
    • 3. 堆
      • 3.1 堆的定义
      • 3.2 堆的实现
        • 堆的结构
        • 堆的插入
        • 向上调整算法
        • 堆的删除
        • 向下调整算法
        • 建堆
          • 方法1:向上调整
          • 方法2:向下调整
          • 建堆复杂度
      • 3.4 堆的应用
        • 堆排序
          • 建堆分析
          • 排序分析
        • Top-K问题
    • 4. 二叉树的链式结构
      • 4.1 二叉树的遍历
        • 链式结构
        • 前中后序遍历
        • 层序遍历
      • 4.2 二叉树基本练习
        • 二叉树结点个数
        • 二叉树叶结点个数
        • 二叉树任意层结点个数
        • 二叉树高度
        • 二叉树查找结点
        • 二叉树销毁


1.树的概念

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

在这里插入图片描述

  • 有一个特殊的结点,称为根结点,根节点没有前驱结点

  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。

  • 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继 因此,树是递归定义的。

1.2 树的结构

定义树的结构的方式有很多种,关键在于如何表示相邻结点之间的关系。

孩子表示法

孩子表示法,若已知树的度 N N N,我们可以定义出这样的结构,

struct TreeNode 
{
    TNDataType data;
    struct Node* subs[N];
};

每个结点存储结点数据和一个数组用以存储其所有子结点的指针。

已知树的度,故subs[N]足够存储,但不可避免的是一定会浪费空间。

struct TreeNode 
{
    TNDataType data;
    SeqList sl;//顺序表存储
};
typedef struct TreeNode* SLDataTypde;

针对浪费空间和树的度未知的问题,可以使用线性表替代静态数组存储子结点的指针。但缺点是结构过于复杂。

双亲表示法,结点存自身数据和父结点的下标。用结构体数组存储结点的信息,遍历数组即遍历二叉树。

struct TreeNode 
{
  	TNDataTypde data;
    int parenti;
};

孩子兄弟表示法

上面的方式各有优劣,表示树结构的最优方法是左孩子右兄弟表示法

struct TreeNode 
{
    TNDataType data; 

    struct TreeNode* firstChild;
    struct TreeNode* nextBrother;
};

结点的指针域只存两个指针:

  • firstChild指向该结点的第一个子结点,
  • nextBrother指向子结点右边的第一个兄弟结点。以此像单链表的形式链接兄弟节点。

第一层,根结点 A A A ,无兄弟结点。

第二层,结点 A A A 的第一个子结点为 B B B,其兄弟结点为 C C C

第三层,结点 B B B 的第一个子结点为 D D D,其兄弟结点为 E E E F F F。结点 C C C 的子结点为 G G G

第四层,结点 D D D 无子结点,结点 E E E 有子结点为 H H H。结点 F F F G G G 无子结点 … ….

只要确定根结点,其余所有的结点都可以从其父结点或兄弟结点的指针处找到,如果没有指针就为空。

这种方法不需要确定树的度 N N N,也不需要使用线性表存储,结构不复杂也不浪费空间,不失为树结构的最优表示法。

树在计算机中最经典的应用就是文件管理系统即目录树。当打开文件夹时,弹出的一系列子文件夹,更类似于先找到子结点再找到其兄弟结点。

1.3 相关概念

名称定义
叶结点没有子结点的结点,即整个树中最下方的结点,也称终端结点
分支结点含有子结点的结点,除根结点以外的内部结点,或称非终端结点。
子结点一个结点的子树的根结点,即一个结点的下一个结点
父结点若该结点含有子结点,则该结点即为该子结点的父节点
兄弟结点所属于相同父节点的子结点,互为兄弟结点
结点的层次从根开始,根结点为第1层,根的子结点为第2层,以此类推
树的高度树中各个结点的层次的最大值称为树的高度,可以看成树的深度
结点的度拥有的子树的个数,即子结点的个数,即为结点的度
树的度树中各个节点的度的最大值称为树的度,可以看成是树的宽度
堂兄弟结点父节点在同一层次的结点,即其父节点是一个同结点的子节点
祖先结点从根结点到该结点,所在分支上的所有结点,都是该结点的祖先结点
子孙结点与祖先相反,以祖先结点为根的子树中的所有结点都为祖结点的子孙
森林所有互不相交的树的集合称为森林,一个结点的所有子树即是一个森林

2.二叉树的概念及结构

2.1 二叉树的概念

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

特点:

  • 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
  • 左子树和右子树是有顺序的,次序不能任意颠倒。
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

在这里插入图片描述

2.2 数据结构中的二叉树-五种形态

  1. 空二叉树
    在这里插入图片描述

  2. 只有1个根结点
    在这里插入图片描述

  3. 根结点只有左子树
    在这里插入图片描述

  4. 根结点只有右子树
    在这里插入图片描述

  5. 根结点既有左子树又有右子树
    在这里插入图片描述

那么拥有三个节点二叉树有几种形态呢?
答案是五种!
在这里插入图片描述

2.3 特殊的二叉树

特殊的二叉树类型包括完全二叉树、满二叉树、平衡二叉树和二叉搜索树,每种都有其独特的性质。

  1. 完全二叉树(Complete Binary Tree)
  • 性质: 在一棵完全二叉树中,所有层次的节点都填满,除了最底层,最底层的节点从左到右依次填入,缺失的节点只能在最底层的右侧。
  • 特点: 完全二叉树通常用数组来表示,对于节点 i,其左子节点在位置 (2i+1),右子节点在位置 (2i+2)。

在这里插入图片描述

  1. 满二叉树(Full Binary Tree)
  • 性质: 在一棵满二叉树中,除了最底层,每个节点都有两个子节点。
  • 特点: 满二叉树的节点总数是 (2^{h+1} - 1),其中 h 是树的高度。

在这里插入图片描述

  1. 平衡二叉树(Balanced Binary Tree)
  • 性质: 平衡二叉树是一棵空树或左右两个子树的高度差不超过 1的二叉树。
  • 特点: 通过旋转等操作来保持平衡,确保搜索、插入和删除的平均时间复杂度为 O(log n)。
    在这里插入图片描述
  1. 二叉搜索树(Binary Search Tree,BST)
  • 性质: 二叉搜索树是一种二叉树,其中每个节点的左子树都小于该节点,右子树都大于该节点。
  • 特点: 具有高效的搜索、插入和删除操作,但在最坏情况下可能出现不平衡。
    在这里插入图片描述

2.4 二叉树的存储结构

普通二叉树的增删查改无甚意义,更多是学习对二叉树结构的控制。为后期学习搜索二叉树、AVL树和红黑树夯实基础。

顺序存储

顺序存储即用数组按层序顺序一层一层的存储节点。

有些“缺枝少叶”的树存入数组,若不浪费空间便不好规律地表示结构。故一般数组只适用于表示完全二叉树

更重要的是,可以利用数组下标计算结点的父子结点位置。如图:


l e f t C h i l d = p a r e n t ∗ 2 + 1 r i g h t C h i l d = p a r e n t ∗ 2 + 2 leftChild=parent*2+1\\ rightChild=parent*2+2 leftChild=parent2+1rightChild=parent2+2

如果计算得的孩子下标越界,则说明该节点不存在对应的子节点。
p a r e n t = ( c h i l d − 1 )    /    2 parent=(child-1)\;/\;2 parent=(child1)/2

链式存储

使用链表表示二叉树,更加的直观。通常方案有两种一个是二叉链表,一个是三叉链表。二叉链表即存数据域和左右指针域,三叉则多存一个父结点指针。

当前数据结构一般都是二叉链,红黑树等高阶数据结构会用到三叉链。当前仅作了解。

在这里插入图片描述

// 二叉链
struct BinaryTreeNode {
    struct BinTreeNode* leftChild;
    struct BinTreeNode* rightChild;
	BTDataType _data; 
};


// 三叉链
struct BinaryTreeNode {
	struct BinTreeNode* parentChild;
	struct BinTreeNode* leftChild;
	struct BinTreeNode* _pRight; 
	BTDataType _data;
};

2.5 二叉树的性质

  1. 二叉树的第 i i i 层上最多有 2 i − 1 2^{i-1} 2i1 个结点。
  2. 对于深度为 h h h 的二叉树,最大结点数为 2 h − 1 2^h-1 2h1,最少节点数为 2 h − 1 2^{h-1} 2h1
  3. 任意二叉树,假设其叶结点个数 n 0 n0 n0 总比 度为2的分支结点 n 2 n2 n2 个数大 1 1 1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1

二叉树的特点就是:每增加一个分支结点,必然会增加一个叶节点。

  1. 完全二叉树度为 1 1 1 的结点个数,要么为 0 0 0,要么为 1 1 1
  2. 若满二叉树结点总数为 N N N, 则树的高度为 h = l o g 2 ( N + 1 ) h=log_2(N+1) h=log2(N+1)

3. 堆

3.1 堆的定义

堆是一种数据结构,他是完全二叉树的一种应用,故堆的底层采用数组作底层结构。

需注意,此刻所讨论的堆是一种抽象数据结构,和内存中的堆没有关系。

定义一个值的集合 { k 0 , k 1 , k 2 , . . . , k n − 1 } \lbrace k_0,k_1,k_2,...,k_{n-1} \rbrace {k0,k1,k2,...,kn1},将其以二叉树顺序存储(层序遍历)的方式存储于数组中,且满足一定规律:
K i ≤ K 2 ∗ i + 1    & &    K i ≤ K 2 ∗ i + 2 K_i ≤ K_{2*i+1}\; \&\& \; K_i ≤ K_{2*i+2} KiK2i+1&&KiK2i+2

K i ≥ K 2 ∗ i + 1    & &    K i ≥ K 2 ∗ i + 2 K_i ≥ K_{2*i+1}\; \&\& \; K_i ≥ K_{2*i+2} KiK2i+1&&KiK2i+2

  • 公式 ( 5 ) (5) (5) 要求每个结点都比其子结点小或相等,这样的堆被称为小堆或小根堆
  • 反之,公式 ( 6 ) (6) (6) 要求每个结点都比其子结点大或相等,这样的堆被称为大堆或大根堆

可以看出,堆是一个完全二叉树,且堆中某个结点的值总是不大于或不小于其子结点的值。但堆并不是有序的,只有存储堆的数组有序,才称堆有序。

3.2 堆的实现

堆的逻辑结构是一个完全二叉树,物理结构是一个数组。也可以认为完全二叉树实际上就是个数组,或着是把数组想象成完全二叉树。

堆的结构
typedef int HPDataType;


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

堆的插入
void HeapPush(HP* php, HPDataType x)
{
    assert(php);  // 确保堆指针不为空

    // 如果堆的大小等于容量,则需要扩容
    if (php->size == php->capacity) {

        // 计算新的容量,如果当前容量为0,则设为4,否则扩大一倍
        int newcap = php->capacity == 0 ? 4 : php->capacity * 2;

        // 使用realloc函数重新分配内存空间,并将数据复制到新空间中
        HPDataType* tmp = (HPDataType*)realloc(php->data, sizeof(HPDataType) * newcap);
        if (tmp == NULL) {
            perror("fail");  // 输出错误信息
            exit(-1);  // 退出程序
        }

        // 更新堆的数据指针和容量
        php->data = tmp;
        php->capacity = newcap;
    }

    // 将元素x添加到堆的末尾
    php->data[php->size] = x;

    // 堆的大小加一
    php->size++;

    // 调用AdjustUp函数,将新添加的元素向上调整,以满足堆的性质
    AdjustUp(php->data, php->size - 1);
}

堆插入就是在数组的末尾进行插入,就是在二叉树上加一个叶结点。

由于插入的数值不一定,堆的性质可能被破坏。但插入新结点只会影响其到根结点的这条路径上的结点,故需要顺势向上调整:一直交换结点数值直到满足堆的性质即可。

向上调整算法
void AdjustUp(HPDataType* a, int child)
{
    int parent = (child - 1) / 2; // 计算子节点的父节点索引

    while (child > 0)
    {
        // 如果子节点的值大于父节点的值(大堆性质)
        if (a[child] > a[parent]) 
            Swap(&a[child], &a[parent]); // 交换子节点和父节点的值
        else 
            break; // 否则跳出循环

        // 更新子节点和父节点的索引
        child = parent;
        parent = (child - 1) / 2;
    }
}

向上调整算法,从child处一直向上找父结点,满足子结点比父节点大或小的条件就交换,直到调整到根结点或不满足条件为止。

堆的向上调整较为容易,因为结点的父结点只有一个,只需要和父节点比较即可。

堆的删除
void HeapPop(HP* php)
{
    assert(php); // 确保堆指针不为空
    if (php->size == 0) return; // 如果堆为空,则直接返回

    // 将堆顶元素与最后一个元素交换
    Swap(&php->data[0], &php->data[php->size - 1]);
    php->size--; // 堆的大小减一

    // 调用AdjustDown函数,将交换后的堆顶元素向下调整,以满足堆的性质
    AdjustDown(php->data, php->size, 0);
}

堆的删除就是删除堆顶元素,但不能简单的将数组整体向前挪一位,这样会使破坏堆的结构。

应该先修改堆顶元素的值为数组末尾元素的值,再删除数组末尾元素。此时再从堆顶位置向下调整,就能恢复堆结构。

向下调整算法
//大根堆
void AdjustDown(HPDataType* a, int size, int parent) 
{
	int child = parent * 2 + 1;

	while (child < size) // 等遍历到叶节点时,child迭代到叶节点的子节点必越界
    {
		if (child + 1 < size && a[child + 1] > a[child]) // 选出大子结点
			child++;
		
		//交换
		if (a[child] > a[parent]) 
			Swap(&a[child], &a[parent]);
		else 
			break;
        
        //迭代
        parent = child;
        child = parent * 2 + 1;
	}
}

把尾元素换到堆顶,必然会改变堆的性质。但根结点的左右子树还是保持原有的性质。所以只需要将堆顶元素逐步向下调整。

以大根堆为例,从根开始,将当前结点与其较大的子结点进行交换,直到走到叶结点或不满足条件为止。

将较大的子结点换上来就是在恢复大堆性质,将较小的子结点交换上来是在恢复小堆性质。

堆的插入删除的时间复杂度,也就是向上向下调整算法的时间复杂度都是 l o g N logN logN

建堆

给出数组a,数组逻辑上可以看成完全二叉树,但并不一定是堆。建堆就是将数组调整成堆。

方法1:向上调整

从根结点开始,依次将数组元素“插入”堆,与其说是“插入”不如说是“加入”。利用下标遍历数组,每插入一个就调整一次。

假设需要将a排成升序,不妨先试试将a数组构建成小堆:

//建堆
void HeapBuild(int* a, int sz) {
    //向上调整
	for (int i = 1; i < sz; i++) {//从第二个结点开始遍历到尾结点
		AdjustUp(a, sz, i);
	}
}

每加入一个元素,就向上调整。思想上其实和接口Push是一样的,都是插入再调整。也可以理解为“边建边调”。

在这里插入图片描述

方法2:向下调整

此时数组当然还不是堆,向下调整算法要求左右子树必须满足堆的性质,才能将当前节点向下调整。应先从最后一个子树开始向下调整,从后向前倒着遍历。

准确来说,因为叶结点必然满足堆的性质,所以不用关心。应从尾结点的父结点所在子树开始,遍历到根结点进行调整。

//建堆
void HeapBuild(int* a, int sz) {
    //向下调整
	for (int i = (sz - 1 - 1) / 2; i >= 0; i--) {//从最后一个叶结点的父结点开始到根结点
		AdjustDown(a, sz, i);
	}
}	

从一个完全二叉树的尾结点的父结点开始,从后往前调,也可以看成“建完在调”。

建堆的两种方式,向上调整和向下调整都是可行的。建大堆还是建小堆,只要改比较符号即可。

建堆复杂度

遍历数组 N 个节点,每个节点调整 logN 次,故向上调整建堆的时间复杂度为 O ( N ∗ l o g N ) O(N*logN) O(NlogN)

向上调整算法复杂度过高,建堆一般配合堆排序使用的是向下调整算法。

向下调整的最复杂情况是从根结点一直调整到叶结点,并以满二叉树为例,看最复杂情况。

假设当前树有 n n n 个结点,树的高度为 h h h ,可得:

  1. 第 1 层有 2 0 2^0 20 个结点,每个结点最多调整 h − 1 h-1 h1 次,
  2. 第 2 层有 2 1 2^1 21 个结点,每个结点最多调整 h − 2 h-2 h2 次,
  3. 以此类推,第 h − 1 h-1 h1 层有 2 h − 2 2^{h-2} 2h2 个结点,每个结点最多调整 1 1 1 次。

精确计算下,第 x x x 层的所有节点的总调整次数,应为 2 x − 1 ∗ ( h − x ) 2^{x-1}*(h-x) 2x1(hx)

T ( n ) T(n) T(n) 为差比数列,利用错位相减法得 T ( n ) T(n) T(n) 关于 h h h 的表达式,再由 n = 2 h − 1 , h = l o g 2 ( n + 1 ) n=2^h-1,h=log_2{(n+1)} n=2h1,h=log2(n+1) T ( n ) T(n) T(n)转换成关于的 n n n的表达式。

由此可得,向下调整建堆的时间复杂度为 O ( N ) O(N) O(N)

3.4 堆的应用

堆排序

堆排序,即利用堆的实现思想对现有的数组进行排序。

假设数组a={70,56,30,25,15,10,75},我们需要先将数组建成堆,然后才能再进行堆排序。

建堆分析

前面已经介绍过堆的创建的两种方式,调用建堆函数即可。

假设要将a排成升序,构建成大堆还是小堆呢?

如果建小堆,堆顶元素即最小的数。若想选出次小的数,就要从第二个位置开始重新建堆,也就是破坏堆的结构重新建堆。

不允许开新空间,那只能重新建堆。重新建堆的复杂度为 O ( N ) O(N) O(N),整体为 O ( N 2 ) O(N^2) O(N2),这显然是不可取的。

排序分析

**利用堆的删除思想进行排序。**升序,建大堆的话,可以按照如下逻辑:

  1. 建大堆,选出最大的数;
  2. 首尾元素互换,致使最大的数被移至末尾;
  3. 将尾元素排除出堆,从根结点开始向下调整,选出次大的数被移到首位。

再首尾互换,如此循环往复,直到调整到根结点即元素个数“减少”到 0。时间复杂度为 O ( N ∗ l o g N ) O(N*logN) O(NlogN)

由此可得,排升序建大堆,排降序建小堆

void HeapSort(int* a, int n) 
{
	//1. 建堆
	for (int i = (n - 2) / 2; i >= 0; --i)
        AdjustDown(a, n, i);
	
    //2. 排序
	for (int i = sz - 1; i > 0; i--) // i==0就结束,i=0时无意义且逻辑错误
    {
		Swap(&a[0], &a[i]); // 首尾互换
		AdjustDown(a, i, 0);// 向下调整
	}
}

可见,排升序建大堆是从尾遍历到头,取出最值放在数组的后面。堆排序的时间复杂度为 O ( N ∗ l o g N ) O(N*logN) O(NlogN)
在这里插入图片描述

不管是升序降序,都是取出本应放在后面的数将其放在后面,都是向下调整算法的应用。

Top-K问题

Top-K问题,即在 N N N 个元素中找出前 K K K 个最值。求最大值则建小堆,求最小值则建大堆。

以 N 个数求前 K 大的数为例。

最容易想到的方案:建立一个 N 个数的大堆。去堆顶 K 次。缺点:浪费空间,复杂度高。那什么样的好呢?

  1. 用前 K K K 个数建立一个 K K K 个元素的小堆;

  2. 剩下 N − K N-K NK 个元素依次跟堆顶的数据比较,比堆顶大则替换堆顶元素并向下调整;

  3. 遍历结束,最后小堆中的 K K K 个元素就是最大值。

这个方法保证:数组中比这K个数大的数都进堆了,剩余没有排出出堆的也是符合要求的。

void TopK(int* a, int n, int k) 
{
    int minHeap[5];
    
	// 建堆
	for (int i = 0; i < k; i++)
		minHeap[i] = a[i];
	for (int i = (k - 2) / 2; i >= 0; --i)
		AdjustDown(minHeap, k, i);

	// 比较
	for (int i = k; i < n; i++)
	{
		if (val > minHeap[0]) 
        {
            minHeap[0] = a[i];
        	AdjustDown(minHeap, k, 0);    
        }		
	}
}

最坏情况可以是数组剩余N-K个数全部被K个数大,全部要进堆调整。时间复杂度为 O ( N ∗ l o g K ) O(N*logK) O(NlogK),空间复杂度为 O ( K ) O(K) O(K)

 

4. 二叉树的链式结构

4.1 二叉树的遍历

链式结构

链式二叉树的结构不利于存储数据,二叉树的增删查改没有意义。

二叉树的价值体现在一些特定的二叉树上,如搜索二叉,平衡搜索树,AVL树,红黑树,B树等。

二叉树链式结构的特点在于整棵树可以被分成三个组成部分:根结点,左子树,右子树

任意的二叉树都可以被拆分成根、左子树、右子树,空树是不可再分的最小单位。

前中后序遍历

学习二叉树结构,先要学习遍历。

二叉树遍历即按照某种特定的规则,依次访问并操作二叉树的每个结点,且每个结点仅访问一次。

遍历方式解释
前序遍历先访问根结点,再访问左子树,最后访问右子树,也称先序遍历
中序遍历先访问左子树,再访问根结点,最后访问右子树
后序遍历先访问左子树,再访问右子树,最后访问根结点

访问任意一棵二叉树都是按照固定的一种方式。三者的区别是访问根结点的次序不同。

上述二叉树以前序、中序、后序遍历所得结果分别为:
前序:
在这里插入图片描述
中序:
在这里插入图片描述
后序
在这里插入图片描述

写出空树才能反映出遍历的全部过程,省略掉空树就是结果。

前中后序遍历都是递归分治思想的体现:

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

//中序遍历
void InOrder(BTNode* root) 
{
	if (root == NULL) {
		printf("\\0 ");
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
    InOrder(root->right);
}

//后序遍历
void PostOrder(BTNode* root) 
{
	if (root == NULL) {
		printf("\\0 ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	
    printf("%c ", root->data);
}

前序遍历递归代码递归具体情况如图所示:

在这里插入图片描述

三种遍历方式的递归调用逻辑完全相同,访问结点的顺序是相同的。只是打印数据的时机不同,故结果不同

层序遍历

层序遍历即从上往下一层一层遍历,层序遍历用队列实现。

void levelOrder(BTNode* root) {
    if (root == NULL) {
        return;
    }
	Queue q;
	QueueInit(&q);
    //1. 头结点入队
	QueuePush(&q, root);
	while (!QueueEmpty(&q)) {
		BTNode* front = QueueFront(&q);
		printf("%d ", front->data);
		//2. 队头出队
        QueuePop(&q);
        //3. 子结点入队
		if (front->left) {
			QueuePush(&q, front->left);
		}
		if (front->right) {
			QueuePush(&q, front->right);
		}
	}
	QueueDestroy(&q);
}
  1. 创建一个队列,先入根结点,

  2. 出队头结点,再入队头的子结点。这样一层结束会把下一层全带进队。

  3. 队列为空时,遍历结束。

保持队列不为空的情况下循环往复,最后一层遍历完子结点全为空才会导致队列元素越来越少最终队列为空。

4.2 二叉树基本练习

递归也就是分治思想,分而治之——大事化小,小事化了。接下来的几个二叉树基础练习全部采用递归的策略实现。

二叉树结点个数
//1. 
void BinaryTreeSize(BTNode* root, int* pcount) {
	if (root == NULL) {
		return;
	}
	(*pcount)++;
	BinaryTreeSize(root->left, pcount);
	BinaryTreeSize(root->right, pcount);
}
//2. 
int BinaryTreeSize(BTNode* root) 
{
    if (root == NULL)
		return 0;        
    
    return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

使用计数器的话,要像OJ一样传入主函数中变量的地址。不推荐。

用递归分治的思想的话,求任意树的结点个数都可以看成一类相同的问题,即左子树结点个数+右子树结点个数+1,然后再去大事化小:

二叉树叶结点个数
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);
}

空树的叶节点个数为0。其他普通树的叶结点个数是其左右子树的叶结点个数之和。
叶结点特征是左右子结点都为空。

二叉树任意层结点个数
int BinaryTreeLevelkSize(BTNode* root, int k) {
	if (root == NULL) 
		return 0;
	if (k == 1) 
		return 1;
    
    return BinaryTreeLevelkSize(root->left, k-1) + BinaryTreeLevelkSize(root->right, k-1);
}
  1. 求A树的第 k k k层结点个数,可以转化成就其左右子树,即B树的第 k − 1 k-1 k1层结点个数+C树的第 k − 1 k-1 k1层结点个数。
  2. 求B树的第 k − 1 k-1 k1层结点个数,即D树的第 k − 2 k-2 k2层结点个数+null树的第 k − 2 k-2 k2层结点个数。
  3. 以此类推,空树结点个数为0,当k=1即遍历到第k层的结点。非空k也不等于0则转换成求左右子树的结点个数。
二叉树高度
int BinaryTreeDepth(BTNode* root) {
	if (root == NULL)
		return 0;
    
    return max(BinaryTreeDepth(root->left), BinaryTreeDepth(root->right)) + 1;
}

空树的高度为0,其他树的高度是左右子树的高度最大值+1即可。

求树的结点总数和求树的高度都是经典的后序遍历问题,都是先遍历左右树再访问根结点。

二叉树查找结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) 
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
    
	BTNode* ret = BinaryTreeFind(root->left, x);
	if (ret)
		return ret;
	
    ret = BinaryTreeFind(root->right, x);
	if (ret) 
		return ret;
	
    return NULL;
}

二叉树查找结点是典型的前序遍历。A不是就到A的左右子树中去找。第三种情况下,必须加以判断,不为空时才返回不然无法遍历右子树。

二叉树销毁
void BinaryTreeDestroy(BTNode* root) {
	if (!root) {
		return;
	}
	BinaryTreeDestroy(root->left);
	BinaryTreeDestroy(root->right);	
	free(root);
}

释放了结点就找不到它的子结点了,所以采用后序遍历的方式。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

springcloud按版本发布微服务达到不停机更新的效果

本文基于以下环境完成 spring-boot 2.3.2.RELEASEspring-cloud Hoxton.SR9spring-cloud-alibaba 2.2.6.RELEASEspring-cloud-starter-gateway 2.2.6.RELEASEspring-cloud-starter-loadbalancer 2.2.6.RELEASEnacos 2.0.3 一、思路 实现思路&#xff1a; 前端项目在请求后端接…

SVN--基本原理与使用(超详细)

目录 一、SVN概述二、SVN服务端软件安装三、SVN服务端配置四、SVN客户端软件安装与使用五、SVN三大指令六、SVN图标集与忽略功能6.1 图标集6.2 忽略功能 七、SVN版本回退八、SVN版本冲突九、SVN配置多仓库与权限控制9.1 配置多仓库9.2 权限控制 十、服务配置与管理十一、模拟真…

Java | Leetcode Java题解之第52题N皇后II

题目&#xff1a; 题解&#xff1a; class Solution {public int totalNQueens(int n) {Set<Integer> columns new HashSet<Integer>();Set<Integer> diagonals1 new HashSet<Integer>();Set<Integer> diagonals2 new HashSet<Integer>…

手写文本识别系统的最佳实践

手写文本识别系统的最佳实践 摘要IntroductionRelated WorkProposed HTR SystemConvolutional Backbone:Flattening Operation:Recurrent Head:CTC shortcut: Best Practices for a Handwritten Text Recognition System 摘要 手写文本识别在近年来随着深度学习及其应用的兴起…

文件夹惊变文件?揭秘原因及解决方案

在日常工作和生活中&#xff0c;电脑已经成为我们不可或缺的助手。然而&#xff0c;有时我们会遇到一些令人困惑的问题&#xff0c;比如&#xff0c;文件夹突然变成了文件。这听起来可能有些匪夷所思&#xff0c;但它确实会发生&#xff0c;而且给用户带来了不小的麻烦。当熟悉…

java-spring-mvc(知识点讲解-第一天)-欢迎各位大佬提建议

目录 &#x1f383;MVC定义 &#x1f9e8;创建工程 &#x1f3a8;SpringMVC处理请求 请求分类及处理方式 静态请求 处理静态前端页面方式 动态请求 处理动态前端页面方式 ⚙小试牛刀 &#x1f3c6;常见问题 &#x1f4cc;HTTP协议 超文本传输协议 请求 &#x1f383;MVC…

Web前端开发 小实训(二) 简易计算器

实训目的 学生能够使用函数完成简易计算器编写 操作步骤 1、请将加减乘除四个方法生成为以下函数&#xff0c;且有返回值 中文英语加法add减法subtract乘法multi除法division次幂pow()平方根sqrt() 提示&#xff1a; 除法中的除数不能为0&#xff01; 参考代码&#xff1…

在线培训考试系统在线考试功能注意事项

在线培训考试系统在线考试功能注意事项 考试前务必注意是否开启防切屏、摄像头监考等防作弊措施&#xff0c;系统一旦检测到触发了疑似作弊行为会立刻自动交卷&#xff0c;考试终止&#xff1b; 答题者准备好后&#xff0c;可点击“开始答题”按钮进入考试&#xff0c;注意考…

代码随想录第49天|121. 买卖股票的最佳时机 122.买卖股票的最佳时机II

121. 买卖股票的最佳时机 121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 动态规划之 LeetCode&#xff1a;121.买卖股票的最佳时机1_哔哩哔哩_bilibili 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一…

#C++里的引用#

目录 引用概念 定义引用类型 引用特性 常引用 传引用传参 传引用做返回值 1.引用概念 引用不是新定义一个变量&#xff0c;而是给已存在变量取了一个别名&#xff0c;编译器不会为引用变量开辟内存空间&#xff0c;它和它引用的变量共用同一块内存空间。 比如&#xff1a…

【AI】一文介绍索引增强生成RAG的原理和结构

今天向大家介绍一下关于RAG的一些知识和经验。 这里说的RAG可以理解为目前针对企业知识库问答等AI应用场景的解决方案,这个场景就是利用自然语言大模型LLM与用户自有的文件进行对话的能力。 【RAG的优势】 首先,讲一讲RAG的优势特征。 如果把AI想象成一个待上岗的人类助手,…

1、Flink DataStreamAPI 概述(上)

一、DataStream API 1、概述 1&#xff09;Flink程序剖析 1.Flink程序组成 a&#xff09;Flink程序基本组成 获取一个执行环境&#xff08;execution environment&#xff09;&#xff1b;加载/创建初始数据&#xff1b;指定数据相关的转换&#xff1b;指定计算结果的存储…

模型 AIPL(认知、兴趣、购买、忠诚)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。品牌营销的量化与链路化。 1 AIPL模型的应用 1.1 耐克如何利用AIPL模型来推广其运动鞋和服装产品? 耐克利用AIPL模型来推广其运动鞋和服装产品&#xff0c;具体如下&#xff1a; 认知&#xff08;…

为什么总说抖店做不好?做抖店需要注意这些问题,是不是都占了?

大家好&#xff0c;我是电商花花。 今天给大家总结了4个新手不注意又常犯的几个原因&#xff0c;这些建议都是我们做店这些年的实操经验&#xff0c;可以帮助大家做店自查&#xff0c;有责改之&#xff0c;无责加冕。 1、抖店核心是选品而不是玩法 很多新手在刚开始做店抖音小…

8K+/月!学习就业一把梭,祝贺誉天数通陈同学收获心仪offer !

大家好&#xff0c;我是誉天数通的陈同学。 在武汉&#xff0c;网络运维圈的人应该都会听过誉天的名字。作为华为的授权培训合作伙伴&#xff0c;誉天不仅提供专业全面的工程师培训&#xff0c;还以其独特的环境和氛围吸引着无数有志于在网络领域发展的朋友。 一年前的我&#…

用坐标与碰撞检测效果

用pygame碰撞检测子弹 与 子弹 格子检查是否在墙内 结果差不了多少 输出的是碰撞点坐标 在计算角色与强的距离是 你用了几条射线 2024/4/26 13:20:48 在代码中&#xff0c;用于计算角色与墙之间距离的部分是通过投射多条射线来实现的。具体来说&#xff0c;在循环中&#xf…

IDEA插件

POJO to JSON —— 实体转Json Smart Input —— 自动切换中英文 Translation —— 翻译 Maven Helper —— 依赖管理 .ignore —— 忽略提交文件 enum-quick-generate —— 枚举代码生成 粘贴到项目包下

前端更优雅的使用 jsonp

前端更优雅的使用 jsonp 背景&#xff1a;最近项目中又使用到了 jsonp 这一项跨域的技术&#xff0c;&#xff08;主要还是受同源策略影响&#xff09;&#xff0c;下面有为大家提供封装好的函数及对应使用示例&#xff0c;欢迎大家阅读理解 文章目录 前端更优雅的使用 jsonp同…

基于车载点云数据的城市道路特征目标提取与三维重构

作者&#xff1a;邓宇彤&#xff0c;李峰&#xff0c;周思齐等 来源&#xff1a;《北京工业大学学报》 编辑&#xff1a;东岸因为一点人工一点智能公众号 基于车载点云数据的城市道路特征目标提取与三维重构本研究旨在弥补现有研究在处理复杂环境和大数据量上的不足&#xf…

Qt设置可执行程序图标,并打包发布

一、设置图标 图标png转ico: https://www.toolhelper.cn/Image/ImageToIco设置可执行程序图标 修改可执行程序图标 添加一个rc文件,操作如下,记得后缀改为rc 打开logo.rc文件添加代码IDI_ICON1 ICON DISCARDABLE "logo.ico"在项目pro后缀名的文件中添加代码 RC_…
最新文章