数算再回顾(二)二叉树常见算法
前言
数(据结构)算(法)再回顾,加深记忆抗遗忘。
前序遍历
基本规律:二叉树前序遍历结果一定是[root, left, right]
// 递归
var preorderTraversal = function (root) {
if (!root) return [];
const r1 = preorderTraversal(root.left);
const r2 = preorderTraversal(root.right);
return [root.val].concat(r1).concat(r2);
};
// 迭代
var preorderTraversal = function (root) {
const stack = [];
const result = [];
while (root || stack.length) {
// 沿左子树遍历
while (root) {
stack.push(root);
result.push(root.val);
root = root.left;
}
// 回到父节点
const node = stack.pop();
// 切换到右子树
root = node.right;
}
return result;
};
中序遍历
基本规律:二叉树中序遍历结果一定是[left, root, right]
// 递归
var inorderTraversal = function (root) {
if (!root) return [];
const r1 = inorderTraversal(root.left);
const r2 = inorderTraversal(root.right);
return r1.concat([root.val]).concat(r2);
};
// 迭代
var inorderTraversal = function (root) {
const stack = [];
const result = [];
while (root || stack.length) {
// 沿左子树遍历
while (root) {
stack.push(root);
root = root.left;
}
// 回溯到父节点
const node = stack.pop();
result.push(node.val);
// 切换到右子树
root = node.right;
}
return result;
};
后序遍历
基本规律:二叉树后序遍历结果一定是[left, right, root]
// 递归
var postorderTraversal = function (root) {
if (!root) return [];
const r1 = postorderTraversal(root.left);
const r2 = postorderTraversal(root.right);
return r1.concat(r2).concat([root.val]);
};
// 迭代
var postorderTraversal = function (root) {
const stack = [];
const result = [];
let prevTraversedNode = null;
while (root || stack.length) {
// 沿左子树遍历
while (root) {
stack.push(root);
root = root.left;
}
// 回溯到父节点
const node = stack.pop();
if (!node.right || node.right === prevTraversedNode) {
// 确定右子树不存在,或右子树已经访问过,再来访问该节点
result.push(node.val);
// 更新已访问节点为该节点
prevTraversedNode = node;
} else {
// 切换到右子树
stack.push(node);
root = node.right;
}
}
return result;
};
前中后序遍历总结
递归实现只需记住返回结果时的规律:
- 前:[root, left, right]
- 中:[left, root, right]
- 后:[left, right, root]
同理,该规律也可以用来解决这类经典问题:已知中序 + 前 or 后序遍历结果,重建二叉树;
迭代实现记住先左后右,然后注意何时取值;
- 前:沿左遍历时取值;
- 中:回溯到父节点时取值;
- 后:回溯到父节点时:
- 如果右子树不存在或已经遍历过,此时再取值;
- 如果右子树存在且没有遍历过,之前 pop 的节点要再 push 回去,因为此时还不能取值;
层序遍历
- 102. 二叉树的层序遍历
- 107. 二叉树的层序遍历 II
- 103. 二叉树的锯齿形层序遍历
- 剑指 Offer 32 - II. 从上到下打印二叉树 II
- 剑指 Offer 32 - III. 从上到下打印二叉树 III
// 从根到叶
var levelOrder = function (root) {
const queue = [root];
const result = [];
while (root && queue.length) {
const layer = [];
let len = queue.length;
while (len) {
const node = queue.pop();
layer.push(node.val);
node.left && queue.unshift(node.left);
node.right && queue.unshift(node.right);
len--;
}
result.push(layer);
}
return result;
};
// 从叶到根
var levelOrderBottom = function (root) {
const queue = [root];
const result = [];
while (root && queue.length) {
const layer = [];
let len = queue.length;
while (len) {
const node = queue.pop();
layer.push(node.val);
node.left && queue.unshift(node.left);
node.right && queue.unshift(node.right);
len--;
}
result.push(layer);
}
return result.reverse();
};
// 交错层序
var zigzagLevelOrder = function (root) {
const queue = [root];
const result = [];
let shouldReverse = false;
while (root && queue.length) {
let len = queue.length;
const layer = [];
while (len) {
const node = queue.pop();
layer.push(node.val);
node.left && queue.unshift(node.left);
node.right && queue.unshift(node.right);
len--;
}
result.push(shouldReverse ? layer.reverse() : layer);
shouldReverse = !shouldReverse;
}
return result;
};
根据遍历结果重建二叉树
此类问题只有两种:
- 中序和前序遍历序列;
- 中序和后序遍历序列;
原理是前中后序遍历所得序列符合固定规则,而且重点在于,任意子树对应的序列也符合该规则:
- 前:[root, left, right]
- 中:[left, root, right]
- 后:[left, right, root]
这也是数据结构考研题型中经常出现的一类问题,从上述规则可以总结出:
- 前序数组的首元素是根节点;
- 中序数组的根节点对应元素的左右两侧分别是左右子树;
- 后序数组的尾元素是根节点;
综上,通过前或后序确定根节点,通过中序确定左右子树节点个数,通过节点个数切分前或后序数组;
function buildTree(inorder: number[], postorder: number[]): TreeNode | null {
// 结束条件
if (inorder.length === 0) {
return null;
}
if (inorder.length === 1) {
return new TreeNode(inorder[0], null, null);
}
// 找到根节点
const root = postorder[postorder.length - 1];
const rootIndex = inorder.findIndex((v) => v === root);
// 划分左右子树
const leftInorder = inorder.slice(0, rootIndex);
const rightInorder = inorder.slice(rootIndex + 1);
const leftPostorder = postorder.slice(0, leftInorder.length);
const rightPostorder = postorder.slice(
leftInorder.length,
postorder.length - 1
);
// 进入下一层递归调用,记录结果
const leftChild = buildTree(leftInorder, leftPostorder);
const rightChild = buildTree(rightInorder, rightPostorder);
// 构建树结构并返回
return new TreeNode(root, leftChild, rightChild);
}
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
// 结束条件
if (inorder.length === 0) {
return null;
}
if (inorder.length === 1) {
return new TreeNode(inorder[0], null, null);
}
// 找到根节点
const root = preorder[0];
const rootIndexOfInorder = inorder.findIndex((v) => v === root);
// 划分左右子树
const leftInorder = inorder.slice(0, rootIndexOfInorder);
const rightInorder = inorder.slice(rootIndexOfInorder + 1);
const leftPreorder = preorder.slice(1, 1 + leftInorder.length);
const rightPreorder = preorder.slice(1 + leftInorder.length);
// 进入下一层递归调用,记录结果
const leftChild = buildTree(leftPreorder, leftInorder);
const rightChild = buildTree(rightPreorder, rightInorder);
// 构建树结构并返回
return new TreeNode(root, leftChild, rightChild);
}
注意 Array.prototype.slice(start, end) 的用法即可,切分出的数组不包含下标为 end 的元素,且 end 默认为数组的长度。