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

news2024/11/18 1:25:01

文章目录

    • 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.coloradmin.cn/o/1637425.html

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

相关文章

Android binder死亡通知机制

在Andorid 的binder系统中&#xff0c;当Bn端由于种种原因死亡时&#xff0c;需要通知Bp端&#xff0c;Bp端感知Bn端死亡后&#xff0c;做相应的处理。 使用 Bp需要先注册一个死亡通知&#xff0c;当Bn端死亡时&#xff0c;回调到Bp端。 1&#xff0c;java代码注册死亡通知 …

Springboot+Vue+小程序+基于微信小程序护农远程看护系统

开发平台为idea&#xff0c;maven管理工具&#xff0c;Mybatis操作数据库&#xff0c;根据市场数字化需要为农户打造小程序可远程查看农场的种植情况。项目是调试&#xff0c;讲解服务均可有偿获取&#xff0c;需要可在最下方QQ二维码处联系我。 SpringbootVue小程序&#xff…

Android --- 消息机制与异步任务

在Android中&#xff0c;只有在UIThread(主线程)中才能直接更新界面&#xff0c; 在Android中&#xff0c;长时间的工作联网都需要在workThread(分线程)中执行 在分线程中获取服务器数据后&#xff0c;需要立即到主线程中去更新UI来显示数据&#xff0c; 所以&#xff0c;如…

50. 【Android教程】xml 数据解析

xml 是一种标记扩展语言&#xff08;Extension Mark-up Language&#xff09;&#xff0c;学到这里大家对 xml 语言一定不陌生&#xff0c;但是它在 Android 中的运用其实只是冰山一角。抛开 Android&#xff0c;XML 也被广泛运用于各种数据结构中。在运用 xml 编写 Android 布…

Docker创建镜像之--------------基于Dockerfile创建

目录 一、在编写 Dockerfile 时&#xff0c;有严格的格式需要遵循 二、Dockerfile 操作常用的指令 2.1ENTRYPOINT和CMD共存的情形 2.2ENTRYPOINT和CMD的区别 2.3ADD 与COPY的区别 三、Dockerfile案例 3.1构建apache镜像 3.1.1 创建镜像目录方便管理 3.1.2创建编写dock…

基于Springboot的音乐翻唱与分享平台

基于SpringbootVue的音乐翻唱与分享平台设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页 音乐资讯 音乐翻唱 在线听歌 后台登录 后台首页 用户管理 音乐资讯管理…

基础安全:CSRF攻击原理与防范

CSRF的概念 CSRF(Cross-Site Request Forgery)中文名为“跨站请求伪造”。这是一种常见的网络攻击手段,攻击者通过构造恶意请求,诱骗已登录的合法用户在不知情的情况下执行非本意的操作。这种攻击方式利用了Web应用程序中用户身份验证的漏洞,即浏览器在用户完成登录后会自…

JavaEE 初阶篇-深入了解网络原理中传输层的端口号与 UDP 协议报文格式

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 端口号概述 1.1 端口号的作用 1.2 端口号不能重复被多个进程绑定 2.0 传输层协议 - UDP 2.1 UDP 的特性 2.2 UDP 的报文格式 1.0 端口号概述 端口号是计算机网络中…

进一步了解android studio 里 AGP,gradle等关系

目录 &#xff08;1&#xff09; gradle是什么 &#xff08;2&#xff09; 工程的jdk版本&#xff0c;及引用包的编译版本的关系 实践 问题与解决 编译成功与运行成功 编译成功 运行成功 &#xff08;1&#xff09; gradle是什么 Gradle是一个构建工具&#xff0c;它是…

1.6 Java全栈开发前端+后端(全栈工程师进阶之路)-前置课程Jdbc编程,使用Java通过Jdbc对数据库进行基础操作

原理图 用java代码实现连接数据库&#xff08;mysql&#xff09;的操作 因为数据库连接需要使用到API和URL&#xff0c;下面简单介绍下API和URL的概念&#xff0c; API&#xff1a; Application Programming Interface应用程序编程接口&#xff0c;就是一套类库 Java中的AP…

2024中国绿电制氢技术趋势分析报告

来源&#xff1a;ATC & 大东时代 国家级规划《氢能产业发展中长期规划&#xff08;2021-2035&#xff09;》出台 • 主要宗旨&#xff1a;明确“能源”的角色定位以及在绿色低碳转型中的作用&#xff0c;为产业发展构建清晰的蓝图。 • 阶段目标设立&#xff1a; • 2025/…

如何不使用代理服务从hugging face上下载大模型?

前言&#xff1a;中国大陆的朋友会发现hugging face经常无法访问了&#xff0c;特别是在服务器上下载大型模型/数据集&#xff0c;如果先在电脑上下载完再传输到服务器上&#xff0c;对于大模型来说会非常麻烦&#xff0c;这篇博客一共提供了三种有效的方法不使用代理服务从hug…

【Java】何为JShell?——有趣的Java学习小工具

前言&#xff1a;上一篇中我们已经看到了如何编译和运行一个Java程序。Java1.9&#xff08;即Java9&#xff09;中引入了另一种使用Java的方式。JShell(Java Shell)程序提供了一个“读取-计算-打印循环”&#xff08;Read-Evaluate-Print Loop,REPL&#xff09;。当你键入一个J…

【综述】多核处理器芯片

文章目录 前言 Infineon处理器 AURIX™系列 TC399XX-256F300S 典型应用 开发工具 参考资料 前言 见《【综述】DSP处理器芯片》 Infineon处理器 AURIX™系列&#xff0c;基于TriCore内核&#xff0c;用于汽车和工业领域。 XMC™系列&#xff0c;基于ARM Cortex-M内核&…

基于 Evan_song1234 开发,MoonSpaceCat 增补的2D 我的世界,增加双缓冲实现 cmd控制台窗口或 Powershell 流畅运行

游戏玩法&#xff1a; awsd移动 1234567890 各有功能 t 是命令行 q 是刷新 e 是重开 z 是挖 其他还没来及探索代码 代码来源 C我的世界2D控制台版_cminecraft-CSDN博客 其中解决颜色被双缓冲刷新没的方法 参考于自己的博客 用ReadConsoleOutput 解决双缓冲ReadConsol…

短视频素材哪个App最好?短视频素材哪里有免费的?

在数字媒体的黄金时代&#xff0c;富有创意的视频内容已成为吸引观众的关键。高质量的视频素材不仅能增强视觉效果&#xff0c;还能提升整体叙述的力度。以下列出了一系列全球顶尖的视频素材提供网站&#xff0c;它们将为你的广告制作、社交媒体或任何视频项目提供极具影响力的…

Python制作精美表格——plottable

plottable是一个基础matplotlib的绘制精美图形表格的库。他将表格内容美化并转为一张图片 使用前提&#xff1a; 1、原始数据数量较少&#xff0c;可以一屏展示。这个库会将原始表格的所有数据都放到一个图片里&#xff0c;数据太多展示效果较差。 2、pandas读取时会将index列…

vue3步骤条带边框点击切换高亮

如果是div使用clip-path: polygon(0% 0%, 92% 0%, 100% 50%, 92% 100%, 0% 100%, 8% 50%);进行裁剪加边框没实现成功。目前这个使用svg完成带边框的。 形状可自行更改path 标签里的 :d“[num ! 1 ? ‘M 0 0 L 160 0 L 176 18 L 160 38 L 0 38 L 15.5 18 Z’ : ‘M 0,0 L 160,0…

飞腾D2000+X100 TYPE6全国产核心板

飞腾D2000X100 TYPE6核心板 产品概述 飞腾D2000X100 TYPE6核心板为增强型自主控制器核心板&#xff0c;其核心芯片CPU采用飞腾D2000/8核工业版CPU、飞腾桥片X100、双通道DDR4L插槽、PHY芯片等。 产品特点 l 基于飞腾D2000X100桥片 l 丰富的PCIE扩展资源&#xff0c;一路PCIE…

Java设计模式 _结构型模式_过滤器模式

一、过滤器模式 1、过滤器模式 过滤器模式&#xff08;Filter Pattern&#xff09;是这一种结构型设计模式。过滤器&#xff0c;顾名思义&#xff0c;就是对一组数据进行过滤&#xff0c;从而最终获取到我们预期的数据。 2、实现思路 &#xff08;1&#xff09;、定义过滤器的…