前言
在这个数字化时代,树状结构以其高效的组织能力在计算机科学中扮演着关键角色。本文将深入探讨二叉树及其遍历方法,为理解和应用这一基础数据结构提供指导。
正文
树状结构
树状结构是一种层次化的数据组织方式,它由节点组成,每个节点有零个或多个子节点,但只有一个父节点(除了根节点以外)。
树状结构通常使用对象来表示,其中每个节点可以是一个对象,具有以下属性:
- 值(Value):节点存储的数据。
- 子节点(Children):一个子节点数组或列表,包含该节点的所有子节点。
- 层次(Level) :从根节点到某一节点的路径上的节点数。
- 高度(Height) :树中最长路径的层次数。
- 度(Degree) :一个节点的子节点数量。对于叶子节点,度为0。
二叉树
- 满二叉树(Full Binary Tree) :
- 完全填满的层:除了最后一层之外,满二叉树的所有层都被完全填满。这意味着除了最后一层,每一层的所有节点都有两个子节点。
- 最后一层的填充:在满二叉树中,最后一层的节点尽可能地集中在左侧。也就是说,如果最后一层不能被完全填满,那么所有存在的节点都应该位于左侧,从左到右排列。
- 高度:满二叉树的高度是其节点数的对数(以2为底)。例如,一个有7个节点的满二叉树的高度是3,因为23=823=8,而7小于8且最接近8的2的幂是8。
- 节点数:满二叉树的节点数是其高度的指数。如果一个满二叉树的高度是ℎh,那么它最多可以有2ℎ−12h−1个节点。
- 形状:满二叉树的形状是对称的,左边的子树和右边的子树具有相同的高度,并且都是满二叉树。
- 性质:满二叉树是二叉树中的一种特殊情况,它具有一些有用的性质,例如,可以通过节点的层和位置来快速计算节点的索引,或者通过索引来快速定位节点的父节点和子节点。
遍历方法
关于遍历方法,笔者在这里推荐这篇文章,作者采用了动画的形式帮助我们理解:
数据结构——二叉树先序、中序、后序及层次四种遍历(C语言版)_中序遍历-CSDN博客
先序遍历(Preorder Traversal)
- 访问根节点。
- 递归地先序遍历左子树。
- 递归地先序遍历右子树。
秒解:
前序遍历可以想象为,一个小人从一棵二叉树根节点为起点,沿着二叉树外沿,逆时针走一圈回到根节点,路上遇到的元素顺序,就是先序遍历的结果
先序遍历结果为:A B D H I E J C F K G
实现代码
function preorder(root) {
if (!root) {
return
}
console.log(root.val);
preorder(root.left)
preorder(root.right)
}
preorder(root)
中序遍历(Inorder Traversal)
- 递归地中序遍历左子树。
- 访问根节点。
- 递归地中序遍历右子树。
秒解:
中序遍历可以看成,二叉树每个节点,垂直方向投影下来(可以理解为每个节点从最左边开始垂直掉到地上),然后从左往右数,得出的结果便是中序遍历的结果
中遍历结果为:H D I B E J A F K C G
实现代码
function inorder(root) {
if (!root) {
return
}
inorder(root.left)
console.log(root.val)
inorder(root.right)
}
inorder(root)
后序遍历(Postorder Traversal)
- 递归地后序遍历左子树。
- 递归地后序遍历右子树。
- 访问根节点。
秒解:
后序遍历就像是剪葡萄,我们要把一串葡萄剪成一颗一颗的。
就是围着树的外围绕一圈,如果发现一剪刀就能剪下的葡萄(必须是一颗葡萄)(也就是葡萄要一个一个掉下来,不能一口气掉超过1个这样),就把它剪下来,组成的就是后序遍历了。
后序遍历中,根节点默认最后面
实现代码
function postorder(root) {
if (!root) {
return
}
postorder(root.left)
postorder(root.right)
console.log(root.val)
}
postorder(root)
例题
先序遍历
力扣第144题: leetcode.cn/problems/bi…
题解:
- 思路:
- 检查空树:首先看看给的树(用
root表示)是不是空的。如果是空的,就直接返回一个空的列表,因为空树里啥也没有。 - 准备工具:准备两个工具,一个是
res,用来存放我们访问过的树节点的值,最后要返回的就是这个;另一个是stack,一个模拟的栈,用来帮助我们记住接下来要访问哪些节点。 - 开始遍历:把树的根节点放进栈里,然后开始一个循环,只要栈里还有节点,就继续。
- 访问节点:在循环里,我们从栈里拿出一个节点(称为
cur),然后把这个节点的值加到res列表的末尾。这就像是我们先看到了树的根。 - 处理子节点:接着,我们把
cur节点的右孩子(如果有的话)放进栈里,然后再把左孩子放进栈里。这样做是因为我们想先访问左孩子,再访问右孩子,但因为栈是后进先出的,所以后放的节点会先被拿出来。 - 重复过程:循环继续,每次都是拿出一个节点,访问它,然后按顺序把孩子们放进栈里。
- 结束遍历:当栈空了,说明我们已经访问了所有的节点,可以停止了。
- 返回结果:最后,我们返回
res列表,这个列表里按顺序存放了我们访问过的节点的值,这就是前序遍历的结果。
- 代码:
var preorderTraversal = function(root) {
if (!root) {
return []; // 如果根节点为空,直接返回空数组
}
const res = []; // 结果数组,存储遍历的节点值
const stack = []; // 使用数组模拟栈
stack.push(root); // 将根节点入栈
while (stack.length) {
const cur = stack.pop(); // 弹出栈顶元素
res.push(cur.val); // 访问当前节点的值,并加入结果数组
// 先压右子节点,再压左子节点,因为栈是后进先出,
// 这样可以保证先弹出左子节点,然后是右子节点,实现前序遍历
if (cur.right) {
stack.push(cur.right);
}
if (cur.left) {
stack.push(cur.left);
}
}
return res; // 返回遍历结果
};
中序遍历
力扣第94题: leetcode.cn/problems/bi…
题解:
- 思路:
- 检查空树:首先检查二叉树的根节点
root是否为空。如果为空,直接返回空数组,因为空树的遍历结果是空的。 - 准备工具:定义一个数组
res用于存储遍历过程中访问到的节点值。定义一个栈stack用于辅助遍历,模拟递归过程中的函数调用栈。 - 设置当前节点:设置一个指针
cur指向根节点,作为遍历的起始点。 - 遍历循环:使用一个
while循环,循环条件是栈非空或cur非空。这个循环确保了整个树的所有节点都能被访问。 - 向左深入:在循环内部,首先执行一个内层循环,将
cur指针沿着左子树一直向下移动,直到找到最左侧的节点。在此过程中,将遇到的所有节点依次压入栈中。 - 访问节点:内层循环结束后,
cur指向当前子树的最左侧节点。此时从栈中弹出一个节点,并将该节点的值添加到结果数组res中。 - 转向右子树:将
cur指针移动到弹出节点的右子节点,准备开始遍历右子树。 - 继续或结束遍历:如果弹出的节点有右子树,则外层循环继续,
cur沿着右子树向下移动;如果没有右子树,则继续执行外层循环,直到栈空且cur为空,此时说明所有节点已经被访问完毕。 - 返回结果:遍历完成后,返回存储了中序遍历节点值的结果数组
res。
- 代码:
var inorderTraversal = function(root) {
if (!root) {
return []; // 如果根节点为空,返回空数组
}
const res = []; // 存储遍历结果的数组
const stack = []; // 辅助栈
let cur = root; // 当前节点指针
// 循环直到当前节点为空或栈为空
while (stack.length || cur) {
// 遍历到当前节点的左子树底部
while (cur) {
stack.push(cur); // 将当前节点压入栈
cur = cur.left; // 移动到左子节点
}
// 弹出栈顶节点并访问它,然后移动到右子树
cur = stack.pop();
res.push(cur.val); // 访问节点并添加到结果数组
cur = cur.right; // 移动到右子节点
}
return res; // 返回中序遍历的结果
};
中序遍历比较复杂,这里画了 帮助理解的草图:
第一次,
- 根节点有值(1),进入while并入栈,
- 根节点的左节点(2)有值,进入while并入栈,
- 根节点左节点的左节点有值(4),进入while并入栈
第二次,
- 4的左节点为空,结束while,弹出4
第三次,
- cur移动到4的右节点,
- 没有值,
- 直接跳过while,弹出2
第四次,
- 移动到2的右节点(5),
- 进入while并入栈,
- 5左子节点为空,跳出while,
- 弹出5
第五次,
- 移动到5的右子节点,为空,
- cur没值,跳过while
- 弹出栈顶元素,此时为1,就弹出1
第六次,
- cur移到1的右子节点3,
- cur有值进入while并把3入栈,
- cur移到3的左子节点6,
- cur还是有值,进入while并把6入栈,
- 移到6的左节点,为空,跳出while
- 弹出栈顶元素,此时为6,弹出6
第七次,
- 移到6的右子节点,为空,
- cur为空,跳过while,
- 弹出栈顶元素3
第八次,
- 移到3的右子节点7,
- cur有值,进入while并把cur值(7)入栈
- 移到7右子节点,没有,为空,结束while
- 弹出栈顶元素7
第九次,
- 移到7的右节点,为空,此时的stack长度为0,结束了外部的while, 返回 res(4 2 5 1 6 3 7)
后序遍历
力扣第145题: leetcode.cn/problems/bi…
题解:
- 初始化检查:首先检查传入的根节点
root是否为空。如果为空,直接返回空数组,因为空树的遍历结果是空的。 - 定义存储结构:定义一个数组
res用于存储遍历过程中访问到的节点值。使用unshift方法将节点值插入到数组的开头,这样可以保证最终结果数组中的节点顺序是正确的后序遍历顺序。 - 初始化栈:定义一个栈
stack,使用数组来模拟。初始化时将根节点root入栈。 - 遍历循环:使用
while循环,循环条件是栈stack非空。这个循环确保了整个树的所有节点都能被访问。 - 弹出节点:在循环内部,弹出栈顶元素
cur,这是当前需要访问的节点。 - 插入结果数组:将弹出的节点值
cur.val插入到结果数组res的开头。这样做是因为后序遍历先访问子节点,最后访问根节点,所以先弹出的节点应放在数组的前面。 - 压栈子节点:在弹出当前节点后,先压入右子节点(如果存在),再压入左子节点(如果存在)。这样做是因为栈是后进先出(LIFO)的数据结构,先压入的节点后弹出,这样可以保证左子节点先被弹出访问。
- 继续遍历:循环继续,直到栈空,此时说明所有节点已经被访问完毕。
- 返回结果:遍历完成后,返回存储了后序遍历节点值的结果数组
res。
var postorderTraversal = function(root) {
if (!root) return []; // 如果根节点为空,直接返回空数组
const res = []; // 结果数组,存储遍历的节点值
const stack = [root]; // 使用数组模拟栈,初始化时将根节点入栈
while (stack.length) {
const cur = stack.pop(); // 弹出栈顶元素
res.unshift(cur.val); // 将弹出元素的值插入到结果数组的开头
// 由于是后序遍历,先压右子节点再压左子节点,
// 这样能保证左子节点先被弹出,然后是右子节点
if (cur.left) stack.push(cur.left);
if (cur.right) stack.push(cur.right);
}
return res; // 返回遍历结果
};
结语
以上就是本文全部内容,感谢您的阅读,希望本文对我们了解二叉树及其遍历方法有些许帮助。