树形结构是一种重要的数据结构,它由节点和连接节点的边组成。树形结构的遍历是指按照一定顺序访问数的所有节点。在二叉树中,常见的遍历方式有前序遍历、中序遍历和后序遍历。这些遍历方式在不同场景下有着广泛的应用,特别是在处理递归问题和数据结构的操作时。
定义:在前序遍历中,先访问根节点,然后递归地遍历左子树,最后递归地遍历右子树。
前序遍历的访问顺序是:根节点 -> 左子树 -> 右子树。
1、递归方式
递归的前序遍历非常直接,每访问一个节点就立即处理该节点,然后递归访问左子树和右子树。这种实现容易理解,代码简洁。
// 二叉树节点结构
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// 前序遍历(递归)
function preOrderTraversal(root) {
if (root === null) return; // 空树直接返回
console.log(root.value); // 访问根节点
preOrderTraversal(root.left); // 遍历左子树
preOrderTraversal(root.right); // 遍历右子树
}
// 示例二叉树
// 1
// / \
// 2 3
// / \
// 4 5
let root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
preOrderTraversal(root); // 输出: 1 2 4 5 3
2、非递归方式
通过 栈 模拟递归过程,栈用于保存当前节点,并控制遍历顺序。非递归实现的关键是先访问根节点,然后将右子节点和左子节点入栈(顺序需要反过来,保证左子树优先处理)。
function preOrderTraversalNonRecursive(root) {
if (!root) return;
let stack = [root];
while (stack.length) {
let node = stack.pop();
console.log(node.value); // 处理根节点
if (node.right) stack.push(node.right); // 先右后左
if (node.left) stack.push(node.left); // 确保左子树优先
}
}
1、文件系统遍历
操作系统中文件系统是树形结构,前序遍历可以用来遍历目录和文件。比如,列出所有文件时,可能会先访问当前目录,再访问子目录和文件。
2、序列化树
如果需要将树转换为字符串进行存储或传输,前序遍历通常是最常用的选择。因为可以按节点顺序来记录树的结构,确保树的结构能够被复原。
定义:中序遍历的顺序是:先递归地遍历左子树,接着访问根节点,然后递归地遍历右子树。
中序遍历的访问顺序是:左子树 -> 根节点 -> 右子树。
1、递归方式
递归实现中序遍历时,首先递归遍历左子树,然后处理根节点,最后递归遍历右子树。实现简单:
// 中序遍历(递归)
function inOrderTraversal(root) {
if (root === null) return; // 空树直接返回
inOrderTraversal(root.left); // 遍历左子树
console.log(root.value); // 访问根节点
inOrderTraversal(root.right); // 遍历右子树
}
inOrderTraversal(root); // 输出: 4 2 5 1 3
2、非递归方式
使用栈来模拟递归过程。栈会保存当前遍历的路径,每当我们到达一个节点的左子节点,就压入栈。当节点的左子树遍历完毕后,处理当前节点,然后递归遍历右子树。
function inOrderTraversalNonRecursive(root) {
let stack = [];
let current = root;
while (stack.length > 0 || current !== null) {
while (current !== null) {
stack.push(current);
current = current.left; // 一直遍历左子树
}
current = stack.pop();
console.log(current.value); // 处理根节点
current = current.right; // 遍历右子树
}
}
1、二叉搜索树的排序
在二叉搜索树(BST)中,中序遍历可以得到一个递增序列,因为在 BST 中,左子树的值小于根节点,右子树的值大于根节点,所以中序遍历自然地按照升序顺序访问节点。
2、表达式树的中缀表达式
中序遍历非常适用于计算表达式树的中缀表达式(如 a + b * c ),遍历过程中可以直接得到正确的符号顺序。
定义:在后序遍历中,首先递归地遍历左子树,然后递归地遍历右子树,最后访问根节点。
后序遍历的访问顺序是:左子树 -> 右子树 -> 根节点。
1、递归方式
递归实现时,先遍历左子树,然后遍历右子树,最后处理根节点。递归的顺序最直接地反映了“先销毁子节点再销毁父节点”的思想:
// 后序遍历(递归)
function postOrderTraversal(root) {
if (root === null) return; // 空树直接返回
postOrderTraversal(root.left); // 遍历左子树
postOrderTraversal(root.right); // 遍历右子树
console.log(root.value); // 访问根节点
}
postOrderTraversal(root); // 输出: 4 5 2 3 1
2、非递归方式
非递归实现较复杂,通常使用两个 栈 来辅助实现,或者使用一个栈并处理节点的状态。关键是要确保在遍历到一个节点时,其左右子树已被完全访问。
function postOrderTraversalNonRecursive(root) {
if (!root) return;
let stack = [root];
let output = [];
while (stack.length) {
let node = stack.pop();
output.push(node.value);
if (node.left) stack.push(node.left);
if (node.right) stack.push(node.right);
}
// 输出的节点是根节点在最后处理,所以要反转结果
while (output.length) {
console.log(output.pop());
}
}
1、树节点的销毁
后序遍历常用于树形结构的销毁操作,因为它保证了先删除子节点,再删除父节点。比如,在删除一个节点时,确保它的子节点已经被正确释放。
2、表达式树的求值
后序遍历非常适用于表达式树的后缀表达式求值。在求值时,先处理操作数,再执行操作符,符合后缀表达式的计算规则。
1、前序 + 中序
给定前序和中序遍历结果,可以唯一地重建一棵二叉树。前序遍历告诉我们根节点,而中序遍历帮助我们确定根节点左右子树的边界。
2、中序 + 后序
给定中序和后序遍历结果,也可以重建二叉树。后序遍历提供了根节点的信息,而中序遍历则帮助我们拆分左右子树。