二叉树
二叉树是一种基础的数据结构,它由节点(Nodes)组成,每个节点最多有两个子节点,通常被称为左子节点(left child)和右子节点(right child),其中空树也可以称为二叉树。
二叉树的基本概念
- 根节点(Root):位于树的顶部,没有父节点
- 叶子节点(Leaf):没有子节点的节点。
- 度(Degree):一个节点的子节点数量,二叉树中节点的最大度为2。如下图所示,右子节点3的度为1,叶子节点6的度为0。
- 边(Edge):连接父子节点的线。
- 路径(Path):从一个节点到另一个节点所经过的边的序列。
- 深度(Depth):从根节点到某个节点的路径上边的数量。如果根节点的深度定义为0,那么下图的二叉树的深度为2。
- 高度(Height):树中任意节点的最大深度,整棵树的高度是根节点的高度。下图的高度为3。
二叉树的类型
- 二叉搜索树(Binary Search Tree, BST):每个节点的值都大于等于其左子树中所有节点的值,并且小于等于其右子树中所有节点的值。
- 满二叉树(Full Binary Tree):所有层次的节点数都是最大节点数,即每个节点都有两个子节点,除了叶子节点。
- 完全二叉树(Complete Binary Tree):除了最后一层外,每一层的节点都被完全填充,且最后一层的节点都尽可能地靠左排列。
- 平衡二叉树(Balanced Binary Tree):左右两个子树的高度差不超过1,比如AVL树和红黑树都是平衡二叉树的典型代表。
二叉树常用于实现动态查找表、堆排序、表达式求值等领域,它的遍历算法(前序遍历、中序遍历、后序遍历和层序遍历)也是数据结构中的基本知识点。
二叉树的遍历
接下来都用这张图片来介绍一下二叉树的遍历方法,遍历二叉树的方法有很多种,今天就介绍四种方法,先序遍历、中序遍历、后序遍历、迭代方法(用栈和数组实现)。
用代码来实现这个二叉树,在这个例子中,root 是树的根节点,它的值为 1。从根节点出发,二叉树分为两部分:左子树和右子树。每个节点包含一个值(val)以及指向其左子节点(left)和右子节点(right)的引用。未提及的子节点默认为 null 或不存在。
js
const root = {
val:1,
left: {
val: 2,
left: {
val: 4
},
right: {
val: 5
}
},
right: {
val: 3,
right: {
val: 6
}
}
}
方法一 先序遍历 --- 根左右(1 2 4 5 3 6)
先序遍历、中序遍历和后序遍历其实都是递归思维,在函数中再调用自身。先序遍历,就是按照根左右的顺序,每次都先遍历根节点,其次遍历左节点,最后遍历右节点,像上图用先序遍历的结果就是1 2 4 5 3 6
。
js
// 定义先序遍历函数,接收一个参数 root,即当前正在访问的节点
function preorder(root) {
// 如果当前节点为空(到达了叶子节点以下的位置),则直接返回,结束本次递归
if (!root) return
// 访问当前节点:打印当前节点的值
console.log(root.val);
// 递归遍历左子树:调用 preorder 函数,传入当前节点的左子节点作为新的根节点
preorder(root.left)
// 递归遍历右子树:调用 preorder 函数,传入当前节点的右子节点作为新的根节点
preorder(root.right)
}
总结一下先序遍历用代码实现就是先访问当前节点,再递归遍历左子树,接着递归遍历右子树即可。打印结果如下所示。
方法二 中序遍历 --- 左根右(4 2 5 1 3 6)
中序遍历的顺序是:左子树-根节点-右子树。
js
//中序遍历
function inorder(root) {
if(!root) return
inorder(root.left)
console.log(root.val);
inorder(root.right)
}
inorder(root)
-
递归出口 :首先检查
root
是否为空,如果为空,则直接返回,遍历结束。 -
递归遍历左子树 :
inorder(root.left)
。在访问当前节点之前,先递归地遍历它的左子树。这样可以确保所有左子树的节点在根节点之前被访问。 -
访问当前节点 :
console.log(root.val);
。一旦到达树的最左边节点,打印(或处理)该节点的值,然后回溯到父节点。 -
递归遍历右子树 :
inorder(root.right)
。在左子树和根节点都被访问过后,开始遍历右子树。这保证了所有右子树的节点在当前节点之后被访问。
最后输出结果如下所示。
方法三 后序遍历 --- 左右根(4 5 2 6 3 1)
后序遍历的顺序是左子树-右子树-根节点
js
// 后序遍历
function lastorder(root) {
if(!root) return
lastorder(root.left)
lastorder(root.right)
console.log(root.val);
}
lastorder(root)
先序、中序和后序的代码基本一样,只是换了一下顺序,这里就简单介绍一下后序遍历,先找递归出口,root
是否为空直接返回。接着递归遍历左子树,再递归遍历右子树,当左右子树都已被访问之后,处理当前节点,打印其值。这确保了节点的访问顺序遵循"左右根"的后序遍历规则。
打印结果如下所示。
方法四 迭代方法(用栈和数组实现)
用迭代方法实现先序遍历
在执行前序遍历的过程中,遵循以下步骤:
-
初始化 : 将根节点
root
压入栈中,作为遍历的起点。 -
循环处理:
- 弹出并访问 : 开始循环,每次从栈顶弹出一个节点赋值给
cur
,并将cur
的值加入结果数组res
,表示当前节点已被访问。 - 压入子节点 :
- 先右后左 : 首先检查
cur
的右子节点是否存在,若存在,则将其压入栈中。这一操作基于栈的后进先出特性,右子节点先入栈,那么左子节点先出栈,符合先序遍历"根左右"的规则。 - 然后检查
cur
的左子节点,若存在,同样将其压入栈中。左子节点虽然后压入,但由于栈的机制,它会在下一个循环中先于右子节点被访问。
- 先右后左 : 首先检查
- 这个过程持续进行,直到栈变空,意味着所有的节点都已按先序遍历的顺序被访问并加入结果数组
res
。
- 弹出并访问 : 开始循环,每次从栈顶弹出一个节点赋值给
js
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
};
console.log(preorderTraversal(root));
初始化变量
res
:一个空数组,用于存储遍历结果。stack
:一个空数组,作为栈来辅助迭代过程,存储待访问的节点。cur
:当前正在访问的节点,初始化为根节点root
。
执行步骤
针对这棵特定的二叉树,我们可以按照迭代方法的遍历逻辑来逐步分析其先序遍历的过程。二叉树结构如下:
markdown
1
/ \
2 3
/ \ \
4 5 6
遍历步骤:
-
初始化 : 根节点
1
入栈。 -
第一次循环:
- 弹出并访问 : 弹出
1
,加入res
,得到res = [1]
。 - 压入子节点 : 先压入右子节点
3
,再压入左子节点2
,栈状态变为[3, 2]
。
- 弹出并访问 : 弹出
-
第二次循环:
- 弹出并访问 : 弹出
2
,加入res
,得到res = [1, 2]
。 - 压入子节点 : 先压入右子节点
5
,再压入左子节点4
,栈状态变为[3, 5, 4]
。
- 弹出并访问 : 弹出
-
第三次循环:
- 弹出并访问 : 弹出
4
,加入res
,得到res = [1, 2, 4]
。 - 无子节点 :
4
无子节点,直接进入下一次循环。
- 弹出并访问 : 弹出
-
第四次循环:
- 弹出并访问 : 弹出
5
,加入res
,得到res = [1, 2, 4, 5]
。 - 无子节点 :
5
无子节点,继续。
- 弹出并访问 : 弹出
-
第五次循环:
- 弹出并访问 : 弹出
3
,加入res
,得到res = [1, 2, 4, 5, 3]
。 - 压入子节点 :
3
只有右子节点6
,压入栈中,栈状态变为[6]
。
- 弹出并访问 : 弹出
-
第六次循环:
- 弹出并访问 : 弹出
6
,加入res
,得到res = [1, 2, 4, 5, 3, 6]
。 - 无子节点 :
6
无子节点,栈变空。
- 弹出并访问 : 弹出
至此,所有节点均被访问,栈也为空,先序遍历结束。最终的遍历结果数组res
为[1, 2, 4, 5, 3, 6]
,这正是按照先序遍历(根->左->右)的顺序访问节点所得到的结果。
结语
在这篇文章只介绍了先序遍历的迭代方法,那你知道如何用迭代方法来实现中序遍历和后序遍历吗?可以评论区告诉我。