前言
本文讲述关于二叉树的创建逻辑,各种遍历算法等等。
将会持续更新关于二叉树的知识点
构建二叉树
这里说的是,给出一个数组,通过不断的往树中添加节点,构建出二叉树。
原则:二叉树需要保证左子树小于父节点,右子树大于父节点。
先定义一个节点 Class
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
构建树
class BinaryTree {
constructor() {
this.root = null;
}
createTree(list) {
if (!Array.isArray(list)) return null;
for (const value of list) {
this.setNode(value);
}
return this.root;
}
setNode(value) {
if (!this.root) {
this.root = new TreeNode(value);
} else {
this.buildTree(this.root, value);
}
}
buildTree(node, value) {
if (node.value > value) {
if (!node.left) {
node.left = new TreeNode(value);
} else {
this.buildTree(node.left, value)
}
} else {
if (!node.right) {
node.right = new TreeNode(value);
} else {
this.buildTree(node.right, value)
}
}
}
}
const nodes = [8, 1, 3, 6, 7, 4, 10, 14, 13];
// New 一个二叉树构造器
const treeConstructor = new BinaryTree();
// Create Tree
const tree = treeConstructor.createTree(nodes);
console.log(tree); // 输出二叉树
二叉树的相关遍历
这里使用上面构建出来的二叉树来进行遍历
前序遍历
class BinaryTree {
constructor() {
this.root = null;
this.pre = [];
}
// 前序遍历
prologueTraverse(node) {
this.pre.push(node.value);
if (node.left) {
this.prologueTraverse(node.left);
}
if (node.right) {
this.prologueTraverse(node.right);
}
return this.pre;
}
}
// 前序遍历(先序遍历)
const pre = treeConstructor.prologueTraverse(tree);
console.log('前序遍历', pre); // 前序遍历 [8, 1, 3, 6, 4, 7, 10, 14, 13]
中序遍历
class BinaryTree {
constructor() {
this.root = null;
this.mid = [];
}
// 中序遍历
middleTraverse(node) {
if (node.left) {
this.middleTraverse(node.left);
}
this.mid.push(node.value);
if (node.right) {
this.middleTraverse(node.right);
}
return this.mid;
}
}
// 中序遍历
const mid = treeConstructor.middleTraverse(tree);
console.log('中序遍历', mid); // 中序遍历 [1, 3, 4, 6, 7, 8, 10, 13, 14]
后序遍历
class BinaryTree {
constructor() {
this.root = null;
this.aft = [];
}
afterTraverse(node) {
if (node.left) {
this.afterTraverse(node.left);
}
if (node.right) {
this.afterTraverse(node.right);
}
this.aft.push(node.value);
return this.aft;
}
}
// 后序遍历
const aft = treeConstructor.afterTraverse(tree);
console.log('后序遍历', aft); // 后序遍历 [4, 7, 6, 3, 1, 13, 14, 10, 8]
深度优先遍历(利用栈 - 先进后出)
class BinaryTree {
// 深度优先遍历(利用栈 - 先进后出)
depthFirstTraversal(node) {
const depth = [];
const stack = [];
stack.push(node);
function traversal(stack) {
if (!Array.isArray(stack) || stack.length <= 0) return;
const _node = stack.pop();
depth.push(_node.value);
if (_node.right) stack.push(_node.right);
if (_node.left) stack.push(_node.left);
traversal(stack); // 递归
}
traversal(stack);
return depth;
}
}
// 深度优先遍历
const depth = treeConstructor.depthFirstTraversal(tree);
console.log('深度优先遍历', depth); // 深度优先遍历 [8, 1, 3, 6, 4, 7, 10, 14, 13]
广度优先遍历(利用队列 - 先进先出)
class BinaryTree {
// 广度优先遍历(利用队列 - 先进先出)
breadthFirstTraversal(node) {
const breadth = [];
const queue = [];
queue.push(node)
function traversal(queue) {
if (!Array.isArray(queue) || queue.length <= 0) return;
const _node = queue.shift();
breadth.push(_node.value);
if (_node.left) queue.push(_node.left);
if (_node.right) queue.push(_node.right);
traversal(queue); // 递归
}
traversal(queue);
return breadth;
}
}
// 广度优先遍历
const breadth = treeConstructor.breadthFirstTraversal(tree);
console.log('广度优先遍历', breadth); // 广度优先遍历 [8, 1, 10, 3, 14, 6, 13, 4, 7]
根据前序、中序、后续构建二叉树
二叉树只能通过【前序 + 中序】或者【后序 + 中序】 构建二叉树。
无法通过【前序 + 后序】构建二叉树,因为,前序 和 后序,只能说面父子节点关系,而无法说明左右节点关系,比如:
前序:ab
后续:ba
他可能为
a a
/ \
b b
上诉可看出无法知道 b 是 a 的左节点还是右节点
前序遍历 + 中序遍历 = 构建出树
前序遍历 [8, 1, 3, 6, 4, 7, 10, 14, 13]
中序遍历 [1, 3, 4, 6, 7, 8, 10, 13, 14]
首先这里第一想到要用递归容易一些!
二叉树前序遍历的顺序为:
-
先遍历根节点;
-
随后递归地遍历左子树;
-
最后递归地遍历右子树。
二叉树中序遍历的顺序为:
-
先递归地遍历左子树;
-
随后遍历根节点;
-
最后递归地遍历右子树。
在「递归」地遍历某个子树的过程中,我们也是将这颗子树看成一颗全新的树,按照上述的顺序进行遍历。挖掘「前序遍历」和「中序遍历」的性质,我们就可以得出本题的做法。
思路:
对于任意一颗树而言,前序遍历的形式总是
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
这里就是一种分而治之的思想,把整个序列就看成是一个根节点、一个左子树、一个右子数。
因为前序遍历结果第一个一定是根节点,那么就秉着一个想法,拿到第一个节点 New 一个 TreeNode,然后赋值根节点的 Left 节点和 Right 节点,
当然,这里的 Left 节点当然就是 [左子树的前序遍历结果] 作为完整子树重新递归获取了。
到这里可知道每次递归的时候,需要知道当前作为完整子树的左右节点,在[前序遍历结果]的位置,这样才能够从[前序遍历结果]中拿到左右节点的子数组。
下面来计算一下各个节点的索引:
/**
* y - (preLeft + 1) = pIndex - 1 - midLeft
* y = pIndex - midLeft + preLeft
*
* 重点:分而治之思想
*
* 前序遍历:
*
* [ 根 ][ 左节点 ][ 右节点 ]
* ↑ ↑ ↑ ↑ ↑
* preLeft preLeft + 1 pIndex - midLeft + preLeft pIndex - midLeft + preLeft + 1 preRight
*
* 中序遍历:
*
* [ 左节点 ][ 根 ][ 右节点 ]
* ↑ ↑ ↑ ↑ ↑
* midLeft pIndex - 1 pIndex pIndex + 1 midRight
*/
- 先定义 preLeft、preRight、midLeft、midRight 这些都是一开始可知道的数值
- 之后再定义 pIndex,这个值可以通过获取[前序遍历结果的]第一个元素在[中序遍历结果]中找到位置(这里注明:节点值没有重复)
- 进而计算可得到:preLeft + 1、pIndex - 1、pIndex + 1
- 接下来剩下[前序遍历结果]的左节点右边和右节点左边的值未得到,首先这里假设左节点右边为y,有因为前序和中序的左节点个数肯定相等,可得到等式:
y - (preLeft + 1) = pIndex - 1 - midLeft
y = pIndex - midLeft + preLeft
- 这样[前序遍历结果]的左节点右边位置就为:pIndex - midLeft + preLeft
- 这样[前序遍历结果]的右节点左边位置就为 pIndex - midLeft + preLeft + 1
编码:
class BinaryTree {
// 前序遍历 + 中序遍历 = 构建出树
createTreeByPreAndMid(pre = [], mid = []) {
const preLength = pre.length;
const midLength = mid.length;
// 针对中序遍历结果遍历出 hash 值,方便快速找到根节点索引
const hashMap = new Map();
for (let i = 0; i < midLength; i ++) {
hashMap.set(mid[i], i);
}
return this.buildTreeByPreAndMid(pre, 0, preLength - 1, hashMap, 0, midLength - 1);
}
buildTreeByPreAndMid(pre, preLeft, preRight, hashMap, midLeft, midRight) {
if (preLeft > preRight || midLeft > midRight) return null;
const rootValue = pre[preLeft];
const root = new TreeNode(rootValue);
const pIndex = hashMap.get(rootValue);
// 分而治之的思想,将左右子树看做一个完整的前序遍历结果,重新放入
root.left = this.buildTreeByPreAndMid(pre, preLeft + 1, pIndex - midLeft + preLeft, hashMap, midLeft, pIndex - 1);
root.right = this.buildTreeByPreAndMid(pre, pIndex - midLeft + preLeft + 1, preRight, hashMap, pIndex + 1, midRight);
return root;
}
}
后序遍历 + 中序遍历 = 构建出树
中序遍历 [1, 3, 4, 6, 7, 8, 10, 13, 14]
后序遍历 [4, 7, 6, 3, 1, 13, 14, 10, 8]
这里和上面的类似的,只要稍作调整即可:
思路:
**
* y - aftLeft = pIndex - 1 - midLeft
* y = pIndex - 1 - midLeft + aftLeft
*
* 重点:分而治之思想
*
* 前序遍历:
* [ 左节点 ][ 右节点 ][ 根 ]
* ↑ ↑ ↑ ↑ ↑
* aftLeft pIndex - 1 - midLeft + aftLeft pIndex - midLeft + aftLeft aftRight - 1 aftRight
* 中序遍历:
* [ 左节点 ][ 根 ][ 右节点 ]
* ↑ ↑ ↑ ↑ ↑
* midLeft pIndex - 1 pIndex pIndex + 1 midRight
*/
编码:
class BinaryTree {
// 后序遍历 + 中序遍历 = 构建出树
createTreeByAftAndMid(aft = [], mid = []) {
const aftLength = aft.length;
const midLength = mid.length;
const hashMap = new Map();
for (let i = 0; i < midLength; i ++) {
hashMap.set(mid[i], i);
}
return this.buildTreeByAftAndMid(aft, 0, aftLength - 1, hashMap, 0, midLength - 1);
}
buildTreeByAftAndMid(aft, aftLeft, aftRight, hashMap, midLeft, midRight) {
if (aftLeft > aftRight || midLeft > midRight) return null;
const rootValue = aft[aftRight];
const root = new TreeNode(rootValue);
const pIndex = hashMap.get(rootValue);
// 分而治之的思想,将左右子树看做一个完整的后续遍历结果,重新放入
root.left = this.buildTreeByAftAndMid(aft, aftLeft, pIndex - 1 - midLeft + aftLeft, hashMap, midLeft, pIndex - 1);
root.right = this.buildTreeByAftAndMid(aft, pIndex - midLeft + aftLeft, aftRight - 1, hashMap, pIndex + 1, midRight);
return root;
}
}
源码
Github:链接