(一)算法与数据结构 | 二叉树


简介

二叉树是算法与数据结构中的重要一环,本文介绍二叉树的相关知识,包括二叉树的构建、二叉树的遍历等。二叉树的存储可以采用链式存储或顺序存储。在链式存储中,使用结构体表示;在顺序存储中,使用数组表示。


0. 结构体定义

struct TreeNode 
{
	int val;	// 值
	TreeNode *left;		// 左孩子
	TreeNode *right;	// 右孩子
	TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}	// 初始化列表
};

根据上述结构体,使用初始化列表初始化一个二叉树节点:

TreeNode* node = new TreeNode(-1);

1. 二叉树的构建和遍历

在算法与数据结构中,针对二叉树的操作分为构建和遍历。在介绍构造之前,首先介绍二叉树的三种基本遍历方式。对于一个只包含三个节点的二叉树来说:
在这里插入图片描述

图1:三个节点的二叉树

在上图中,节点 A 称为节点 B 和节点 C 的父节点,节点 B 称为节点 A 的左孩子节点,节点 C 称为节点 A 的右孩子节点。二叉树的三种基本遍历方式由父节点与其子节点的遍历顺序决定:ABC 的遍历顺序为先序遍历,BAC 为中序遍历,BCA 为后序遍历。

上面以三个节点为例说明了二叉树的遍历方式,扩展到多个节点时类似:
在这里插入图片描述
先序遍历为 324516,中序遍历为 425316,后序遍历为 452613。二叉树的三种遍历可以以递归或迭代的方式完成:

// 二叉树的先序遍历
void preVisitBiTree(TreeNode* root)
{
	if (root == nullptr)
	{
		return;
	}
	visit(root);
	preVisitBiTree(root->left);
	preVisitBiTree(root->right);
}

// 二叉树的中序遍历
void inVisitBiTree(TreeNode* root)
{
	if (root == nullptr)
	{
		return;
	}
	inVisitBiTree(root->left);
	visit(root);
	inVisitBiTree(root->right);
}

// 二叉树的后序遍历
void postVisitBiTree(TreeNode* root)
{
	if (root == nullptr)
	{
		return;
	}
	postVisitBiTree(root->left);
	postVisitBiTree(root->right);
	visit(root);
}

三种递归遍历对应了三种迭代遍历的方式,使用栈来模拟递归即可:

// 二叉树的先序遍历
void preVisitBiTree(TreeNode* root)
{
	if (root == nullptr)
	{
		return;
	}
	stack<TreeNode*> stk;
	stk.push(root);
	while (!stk.empty())
	{
		TreeNode* t = stk.top();
		stk.pop();
		visit(t);
		if (root->right != nullptr)
		{
			stk.push(root->right);
		}
		if (root->left != nullptr)
		{
			stk.push(root->left);
		}
	}
}

// 二叉树的中序遍历
void inVisitBiTree(TreeNode* root)
{
	if (root == nullptr)
	{
		return;
	}
	stack<TreeNode*> stk;
	while (!stk.empty() || root != nullptr)
	{
		while (root != nullptr)
		{
			stk.push(root);
			root = root->left;
		}
		if (!stk.empty())
		{
			root = stk.top();
			stk.pop();
			visit(root);
			root = root->right;
		}
	}
}

// 二叉树的后序遍历
void postVisitBiTree(TreeNode* root)
{
	if (root == nullptr)
	{
		return;
	}
	stack<TreeNode*> stk1;
	stack<TreeNode*> stk2;
	stk1.push(root);
	while (!stk1.empty())
	{
		TreeNode* t = stk1.top();
		stk1.pop();
		stk2.push(t);
		if (t->left != nullptr)
		{
			stk1.push(t->left);
		}
		if (t->right != nullptr)
		{
			stk2.push(t->right);
		}
	}
	while (!stk2.empty())
	{
		TreeNode* t = stk2.top();
		stk2.pop();
		visit(t);
	}
}

在二叉树的迭代遍历中,后序遍历为交换先序遍历左右孩子的入栈顺序并逆序。

同样地,二叉树的构建也有对应以上三种遍历遍历方式的构建构建顺序。以先序构建为例(以 -1 表示空节点):

void createBiTree(TreeNode*& root)
{
	int num;
	cin >> num;
	if (num == -1)
	{
		root = nullptr;
	}
	else
	{
		root = new TreeNode(ch);
		createBiTree(root->left);
		createBiTree(root->right);
	}
}

在访问二叉树的层级属性时可借助队列实现二叉树的层序遍历:

void levelVisitBiTree(TreeNode* root)
{
	if (root == nullptr)
	{
		return;
	}
	queue<TreeNode*> q;
	q.push(root);
	while (!q.empty())
	{
		TreeNode* t = q.front();
		q.pop();
		visit(t);
		if (t->left != nullptr)
		{
			q.push(t->left);
		}
		if (t->right != nullptr)
		{
			q.push(t->right);
		}
	}
}

3. 二叉树的高级操作


3.1 二叉树的递归

在二叉树的操作中,基本上是基于递归实现。二叉树的递归分为三大步骤:寻找递归出口确定返回值递归遍历左右子树。其中,二叉树遍历递归出口一般为空节点或叶节点;通常根据题目要求确定当前递归的返回值;采用先序遍历、中序遍历或后序遍历递归遍历左右子树。


3.2 二叉树的深度

题目来源:二叉树的最大深度

二叉树的最大深度指二叉树的根节点到最远叶节点路径上的节点数。当遍历到空节点时返回零,表示当前节点的深度为零;题目求的是二叉树的最大深度,所以当前递归应该返回最大深度。对于当前层递归来说,当前节点对应的最大深度等于左右子树的最大深度加上根节点;三种遍历方式均可。

int maxDepth(TreeNode* root)
{
	// 递归出口
	if (root == nullptr)
	{
		return 0;
	}
	// 递归求左右子树的最大深度
	int depth_l = maxDepth(root->left);
	int depth_r = maxDepth(root->right);
	// 返回当前节点对应的最大深度等于左右子树的最大深度加上根节点
	return max(l, r) + 1;
}

题目来源:二叉树的最小深度

二叉树的最小深度指二叉树的根节点到最近叶节点路径上的节点数。当遍历到空节点时返回零,表示当前节点的深度为零;题目求的是二叉树的最小深度,所以当前递归应该返回最小深度。与求最大深度不同,由于题目求的是到叶子节点的距离,如果当前节点的某一子树的深度不为零(该节点不是叶子节点),则最小深度由不为空的子树决定(叶节点在该子树上)。对应地递归条件加上叶子节点返回一的情况。当前节点的左右子树均不为空时,返回最小子树深度加上根节点;三种遍历方式均可。

所以该题的关键是找到递归结束的条件:空节点,叶子节点,当前节点的某一子树为空。

int minDepth(TreeNode* root)
{
	if (root == nullptr)
	{
		return 0;
	}
	// 叶子节点
	if (root->left == nullptr && root->right == nullptr)
	{
		return 1;
	}
	// 递归求左右子树的最小深度
	int depth_l = minDepth(root->left);
	int depth_r = minDepth(root->right);
	// 如果当前节点的某一子树为空
	if (depth_l == 0 || depth_r == 0)
	{
		return depth_l + depth_r + 1;
	}
	// 返回当前节点对应的最小深度等于左右子树的最小深度加上根节点
	return min(depth_l, depth_r) + 1;
}

3.3 二叉树的路径

二叉树的路径问题分为三大类:根节点开始到叶子节点的路径,这也是最简单的路径,即直接在叶子节点开始判断是否结束递归;根节点开始到非叶子节点的路径,在中间节点判断是否满足递归结束条件;任意节点开始到任意节点结束的路径,枚举所有起点和终点,有时候可能会用到双递归。


题目来源:二叉树的所有路径

题目求从根节点开始到叶子节点的所有路径,返回一个一维数组,路径上的节点用右箭头拼接起来形成一个字符串。根据上一节,递归出口有两类:遇到空节点时直接返回;遇到叶子节点时添加结果;由于直接在变量中添加结果,所以不需要在当前递归中返回结果;三种遍历方式均可。

在二叉树的递归中,有时候需要定义辅助函数来协助结果的获取,主要分为两种情况:包含临时变量(如本题中的临时路径)和同时处理二叉树的左右子树。

void helper(TreeNode* root, string path, vector<string>& ans)
{
	// 递归出口,遇到空节点
	if (root == nullptr)
	{
		return;
	}
	// 添加当前节点到路径
	path = path + to_string(root->val);
	// 递归出口,遇到叶子节点
	if (root->left == nullptr && root->right == nullptr)
	{
		ans.push_back(path);
		return;
	}
	// 添加连接节点的箭头
	path = path + "->";
	// 递归地处理左右子树
	helper(root->left, path, ans);
	helper(root->right, path, ans);
}

vector<string> binaryTreePaths(TreeNode* root)
{
	// 存放结果的变量
	vector<string> ans;
	helper(root, "", ans);
	return ans;
}

题目来源:二叉树中和为某一值的路径

在上一题目的基础上,本题求路径上节点值得和为给定值的路径,返回一个二维数组表示各路径上的节点。处理方法和上一题非常类似,在递归过程中实时维护目标值即可。如果到达叶子节点时维护的目标值为零,则找到一条可行的路径。

void helper(TreeNode* root, int& target, vector<int>& path, vector<vector<int>>& path)
{
	// 递归出口,遇到空节点
	if (root == nullptr)
	{
		return;
	}
	// 添加当前节点到路径
	path.push_back(root->val);
	// 实时维护目标值
	target = target - root->val;
	// 到达叶子节点时的目标值为零
	if (target == 0 && root->left == nullptr && root->right == nullptr)
	{
		ans.push_back(path);
	}
	// 递归地处理左右子树
	helper(root->left, target, path, ans);
	helper(root->right, target, path, ans);
	// 恢复相关值
	target = target + root->val;
	path.pop_back();
}

vector<vector<int>> pathSum(TreeNode* root, int target)
{
	vector<vector<int>> ans;
	vector<int> path;
	helper(root, target, path, ans);
	return ans;
}

4. 总结

二叉树的大多数算法都是以二叉树的遍历为基础的。由于二叉树的不同遍历方式具有不同的特点,往往可以应用到不同场景。二叉树的先序、中序、后序遍历的顺序是从上到下,再从下到上反复此过程直至结点访问完成,采用非递归实现是需要借助栈。主要应用场景为求二叉树的深度、打印路径等。二叉树的层次遍历以二叉树的每层为单位进行遍历,实现需要借助队列。主要应用场景为求二叉树每层的结点数、二叉树的宽度等。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值