c++ || 详解二叉树

本文详细介绍了二叉树的概念,包括树的定义、度数和结点分类。讨论了二叉树的存储结构,如双亲表示法、孩子表示法及其优缺点,并重点讲解了二叉树的遍历方法,如前序、中序、后序和层序遍历。此外,还提到了满二叉树和完全二叉树的特性,以及线索二叉树、二叉排序树(BST)和平衡二叉树(AVL树)的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树的定义

树(Tree)是n(n≥0)个结点的有限集。n=O时称为空树。
在任意一棵非空树中:
(1)有且仅有一个特定的称为根( Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree )

1)n>0时,根节点是唯一的
2)m>0时,子树的个数没有限制,但是子树一定是互不相交的

  • 结点分类
  1. 结点拥有的子树数称为结点的度(Degree),度为0的结点称为叶节点或终端节点,度不为0的结点称为非终端结点或分支节点(内部节点)。
  2. 树的度是树内各节点的度的最大值
    5
    结点关系
    在这里插入图片描述

树的存储结构

  1. 双亲表示法
    每个结点中,附设一个指示器指向其双亲结点到链表中的位置
    3

data:数据域,存储结点的数据信息
parent:指针域,存储该结点的双亲在数组中的下标

  1. 孩子表示法

每个结点有多个指针域,其中每个指针指向一颗子树的根结点,我们吧这种方法叫做多重链表表示法

方案1:指针域的个数等于树的度。
如果树中各结点的度相差很大时,是很浪费空间的,有很多的结点,指针域都是空的,但是当各结点的度相差很小时,那就意味开辟的空间被充分的利用
3方案2:每个结点的指针域的个数等于该结点的度,专门取一个位置存储结点指针域的个数
克服浪费空间的缺点,对空间的利用率提高,但是由于各结点的链表是不同的结构,还需要维护结点度的数值,带来了时间上的损耗
在这里插入图片描述- 孩子表示法: 每个结点的孩子结点排列起来,以单链表做存储结构,则n个结点有n个孩子链表,叶子结点则表示该单链表为空,然后n个头指针组成一个线性表,采用顺序存储结构,存放进一维数组中
在这里插入图片描述1

  1. 孩子兄弟表示法

任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此设计两个指针,分别指向该结点的第一个孩子和此结点的右兄弟
4

二叉树

  1. 定义: 是n个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树的二叉树构成。

满足两个条件:

  1. 本身是有序树
  2. 树中包含的各个结点的度不能超过2,只能是0,1,2

二叉树的性质

  1. 二叉树中,第i层最多有2i-1个结点
  2. 如果二叉树的深度为k,那么此二叉树最多有2k-1个结点
  3. 二叉树中,终端结点数(叶子结点数)为n0,度为2的结点数为n2,则n0=n2+1
  4. 5

满二叉树

二叉树中除了叶子结点,每个结点的度都为2,则此二叉树称为满二叉树

4

  • 满二叉树的性质
    1.满二叉树中第 i 层的节点数为 2(i-1) 个。
    2.深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1
    3.满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。
    4.具有 n 个节点的满二叉树的深度为 log2(n+1)

完全二叉树

二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树
9

  • 满二叉树一定是完全二叉树,但完全二叉树不一定是满的

完全二叉树从右向左依次标号,对于任意结点i来说:

  1. 当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点)
  2. 如果 2i>n(总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2i
  3. 如果 2i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2i+1

二叉树的存储结构

顺序存储、链式存储

二叉树的顺序存储

使用顺序表(数组)存储二叉树,顺序存储只适用于完全二叉树,如果想顺序存储普通二叉树,则需要提前将普通二叉树转化为完全二叉树

9完全二叉树的顺序存储仅需从根结点开始,按照层次依次将树中结点存储到数组即可
在这里插入图片描述
若结点i有左右孩子,则其左孩子结点为2i,右孩子结点为2i+1

二叉树的链式存储

顺序存储二叉树,如果是普通二叉树会造成空间浪费的现象。
31

结点结构:

  • 指向左孩子结点的指针(Lchild)
  • 结点存储的数据(data)
  • 指向右孩子结点的指针(Rchild)
    3
typedef struct BiTNode
{
	ElemType data;
	BiTNode* Lchild; //左孩子
	BiTNode* Rchild; //右孩子
}BiTNode,*BiTree;

二叉树的建立

将二叉树的每个结点的空指针引出一个虚结点,它的值是特定值,比如‘#’,称这种处理后的二叉树为原二叉树的扩展二叉树
3

 //递归建立
void CreateBiTree(BiTree* T) //AB#D##C##扩展二叉树
{
	ElemType ch;
	cin >> ch;
	if (ch == '#')
		*T = NULL;
	else {
		*T = (BiTree)malloc(sizeof(BiTNode));
		if (!*T)
		{
			exit(OVERFLOW);
		}
		(*T)->data = ch;
		CreateBiTree(&(*T)->Lchild);  //构造左子树
		CreateBiTree(&(*T)->Rchild);  //构造右子树
	}
}

二叉树的遍历

  • 指从根结点出发,按照某种次序以此访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次

主要有两种遍历方式

  1. 深度优先遍历:先往深走,遇到叶子结点在往回走
  • 前序遍历(递归法,迭代法):根左右
  • 中序遍历(递归法,迭代法):左根右
  • 后序遍历(递归法,迭代法):左右根
  1. 广度优先遍历
  • 层次遍历(迭代法)
    0

二叉树前序遍历

  • 规则: 若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树
    在这里插入图片描述
    前序遍历结果为:ABDGHCEIF

  • 终止条件: 当子问题到达叶子节点后,后一个不管左右都是空,因此遇到空节点就返回。

  • 返回值: 每次处理完子问题后,就是将子问题访问过的元素返回,依次存入了数组中。

  • 每个子问题优先访问这棵子树的根节点,然后递归进入左子树和右子树。

前序遍历迭代法实现

要访问的元素和要处理的元素顺序是一致的

  1. 处理:先将元素放进result数组中
  2. 访问:遍历节点

前序遍历是中左右,每次先处理的是中间节点,那么先将跟节点放入栈中,然后将右孩子加入栈,再加入左孩子。 为什么要先加入
右孩子,再加入左孩子呢?因为这样出栈的时候才是中左右的顺序。

二叉树中序遍历

  • 规则: 若树为空,则空操作返回,否则从根结点开始,中序遍历根结点的左子树,然后是访问根节点,最后中序遍历右子树
    4
    中序遍历结果为:GDHBAEICF

迭代法实现

  • 中序遍历先访问二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(节点数值放进result数组中),处理顺序和访问顺序不一致。
  • 使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

二叉树后序遍历

  • 规则: 若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根节点
    3
    后序遍历结果为:GHDBIEFCA

  • 迭代法
    调整先序遍历的代码顺序,就变成中右左的遍历顺序,在进行反转result数组,输出的结果顺序就是左右中
    在这里插入图片描述

class Solution {
public:
	void traversal_1(TreeNode* cur, vector<int>& vec)   //递归实现
	{
		if (cur == NULL)return;
		vec.push_back(cur->val);    //根
		traversal_1(cur->left, vec);//左
		traversal_1(cur->right, vec);//右
	}
	void traversal_2(TreeNode* cur, vector<int>& vec) {
		if (cur == NULL) return;
		traversal_2(cur->left, vec);  // 左
		vec.push_back(cur->val);    // 中
		traversal_2(cur->right, vec); // 右
	}
	void traversal_3(TreeNode* cur, vector<int>& vec) {
		if (cur == NULL) return;
		traversal_3(cur->left, vec);  // 左
		traversal_3(cur->right, vec); // 右
		vec.push_back(cur->val);    // 中
	}
	vector<int> preorderTraversal(TreeNode* root) //前序遍历
	{
		vector<int> res;
		traversal_1(root, res);
		return res;
	}
	vector<int> preorderTraversal_1(TreeNode* root)   //前序遍历栈实现 迭代法
	{
		stack<TreeNode*> st; //先进后出
		vector<int> vec;   //存放所有节点的数据
		if (root == NULL) return vec;
		st.push(root);     //先存放根节点
		while (!st.empty())
		{
			TreeNode* node = st.top();    //根结点先存放进数组中
			st.pop();
			vec.push_back(node->val);
			if (node->right) st.push(node->right);   //先入右 最后右才出
			if (node->left) st.push(node->left);     //再入左 空节点不入栈
		}
		return vec;
	}
	vector<int> inorderTraversal(TreeNode* root) //中序遍历
	{
		vector<int> res;
		traversal_2(root, res);
		return res;
	}
	vector<int> inorderTraversal_2(TreeNode* root) //中序遍历栈实现 迭代法
	{ 
		vector<int> result;
		stack<TreeNode*> st;
		TreeNode* cur = root;
		while (cur != NULL || !st.empty()) {
			if (cur != NULL) { // 指针来访问节点,访问到最底层
				st.push(cur); // 将访问的节点放进栈
				cur = cur->left;                // 左
			}
			else {
				cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
				st.pop();
				result.push_back(cur->val);     // 中
				cur = cur->right;               // 右
			}
		}
		return result;
	}
	vector<int> postorderTraversal(TreeNode* root) //后序遍历
	{
		vector<int> res;
		traversal_3(root, res);
		return res;
	}
	//调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,
	//然后在反转result数组,输出的结果顺序就是左右中了
	vector<int> postorderTraversal_2(TreeNode* root) //后序遍历栈实现 迭代法
	{
		stack<TreeNode*> st;
		vector<int> result;
		if (root == NULL) return result;
		st.push(root);
		while (!st.empty()) {
			TreeNode* node = st.top();
			st.pop();
			result.push_back(node->val);
			if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
			if (node->right) st.push(node->right); // 空节点不入栈
		}
		reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
		return result;
	}
};

二叉树层序遍历

  • 规则: 若树为空,则空操作返回,否则从树的第一层,根结点开始访问,从上到下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
    2
    层序遍历结果为:ABCDEFGHI
/*
二叉树的层序遍历
利用队列 先进先出
*/
vector<vector<int>> LevelOrder(TreeNode* root)//输出一个二维数组
{
	queue<TreeNode*> qu;
	vector<vector<int>> res;
	if (root != NULL) qu.push(root);
	while (!qu.empty())
	{
		int size = qu.size();
		// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
		vector<int> vec;
		for (int i = 0; i < size; i++)
		{
			TreeNode* t = qu.front();
			qu.pop();
			vec.push_back(t->val);//把本层的节点数据都存入数组中
			if (root->left) qu.push(root->left); //把下一层的左右节点都入队列
			if (root->right) qu.push(root->right);
		}
		res.push_back(vec);
	}
	return res;
}

线索二叉树

  • 指向前驱和后继电脑指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树
  • 线索二叉树上的线索数目等于空指针域的数
  • n个结点的线索二叉树上含有的线索数为n+1,一个有n个节点的线索二叉树,每个节点都有指向左右孩子的两个指针域,则共有2n个指针域,而n个节点共有n-1条分支,所以共有2n-(n-1)个空指针域,即有n+1个线索
    1
typedef enum{Link,Thread} PointerTag;
//link==0表示指向左右孩子指针
//thread==1表示指向前驱或后继的线索
typedef struct BiThrNode
{
	ElemType data; //数据
	BiThrNode *Lchild,*Rchild;
	PointerTag Ltag;
	PointerTag Rtag;
};

二叉排序树-BST树

在这里插入图片描述

二叉排序树(Binary Sort Tree)或者是一棵空树;或者是具有下列性质的二叉树:
1.若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2.若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3.左、右子树也分别为二叉排序树;
没有键值相等的节点

  • 二叉排序树的优点: 查询时间复杂度比链表快,链表查询的时间复杂度是O(n),二叉排序树的时间复杂度是O(logn)
  • 缺点: 如果插入的结点的值的顺序是越来越大或越来越小的,那么BST树就会变成一个链表,时间复杂度就会降低为O(n)
    在这里插入图片描述

平衡二叉树-AVL树

  • 特点
    1.根结点的值大于其左子树中任意一个节点的值,小于其右节点中任意一个节点的值
    2.AVL树上任意结点的左右子树的高度差最大为1
    3.由于第2个特点,所以AVL树不会退化为BST树,查找、添加、删除的时间复杂度都是O(logn)

0- 平衡因子 将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1,只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的

  • 有了二叉搜索树,为什么还需要平衡二叉树?

二叉搜索树容易退化成一条链,就导致查找的时间复杂度从O(log2N)退化成O(N),通过引入左右子树的高度差有限制的平衡二叉树来保证查找操作的最坏时间复杂度为O(log2N)

树、二叉树转换

步骤:

  1. 加线:所有的兄弟结点之间加一条连线
  2. 去线:对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点的连线
  3. 层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。第一个孩子是二叉树结点的左孩子,兄弟转换过来是孩子结点的右孩子
    1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值