【数据结构】二叉树



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);
}

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

  • 39
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SuhyOvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值