上篇文章讲了二叉树的后序遍历,并且提到了一个题目:p 和 q 为二叉树的任意两个节点,请找出 p 和 q 的最近公共祖先节点 r
这篇文章就来分享用 JS 代码解决这个问题
要找到两个节点的公共祖先,就先要拿到两个节点的祖先路径,然后对比祖先路径,找到最长的路径,路径最后的节点就是最近公共祖先节点了
准备数据
javascript
const data = [0, 1, 2, 3, 4, 5, 6, null];
const generateTree = (data, i) => {
if (!data[i]) return null;
const root = { value: data[i] };
root.left = generateTree(data, 2 * i);
root.right = generateTree(data, 2 * i + 1);
return root;
};
const tree = generateTree(data, 1);
这里用数组来生成二叉树的数据结构。生成的过程借助了完全二叉树的思想
假设一个节点的序号是 n,那么左子节点的序号就是 2n,右子节点的序号就是 2n+1
数组按照层次遍历的顺序来排列,最后生成的二叉树就是下面这个样子
6 是 3 的 左节点
返回祖先路径
javascript
const getParents = (tree, p) => {
if (!tree) return;
const stack = [];
let node = tree;
let pre = null;
while (stack.length !== 0 || node !== null) {
while (node) {
stack.push(node);
node = node.left;
}
node = stack.slice(-1)[0];
if (node.right && pre !== node.right) {
node = node.right;
} else {
if (node.value == p.value) return stack.map((item) => item.value).slice(0, -1);
stack.pop();
pre = node;
node = null;
}
}
return null;
};
这个代码和上篇文章提到的后序遍历代码几乎一致,唯一不同的是在访问当前节点的时候,并不是输出它,而是将其内容与 p 节点做对比,如果相等,就意味着找到了 p 节点,并且此时 stack 中存储的就是 p 节点的祖先路径。所以一旦node.value == p.value
条件成立,就返回stack.map((item) => item.value).slice(0, -1)
stack.map((item) => item.value).slice(0, -1)
表达式的作用是放回祖先路径节点的值
执行代码看看:
javascript
console.log(getParents(tree, { value: 6 }));
符合,6 节点的祖先确实是 1,3
javascript
console.log(getParents(tree, { value: 2 }));
符合,2 节点的祖先确实是 1
找到公共祖先
javascript
const findCommonParent = (tree, n1, n2) => {
const path1 = getParents(tree, n1);
const path2 = getParents(tree, n2);
let lastParent = null;
for (let i = 0; i < path1.length && i < path2.length; i++) {
if (path1[i] == path2[i]) lastParent = path1[i];
else return lastParent;
}
return lastParent;
};
先取到两个节点的祖先路径,然后开始从0
下标对比,如果相同就将遍历到的节点的值赋值给 lastParent
, 一旦碰到不相同的,就将 lastParent
返回出去。lastParent
的值一致就保存着上次相同的节点值,也就相当于最近的公共祖先了
运行代码:
javascript
console.log(findCommonParent(tree, { value: 4 }, { value: 6 }));
没问题,最近的公共祖先就是 1
javascript
console.log(findCommonParent(tree, { value: 4 }, { value: 5 }));
没问题,最近的公共祖先就是 2
算法复杂度 O(2n + logn)
这个代码的过程是很清晰的,但有个缺点,就是要后续遍历两次二叉树。
这其实是没有必要的。因为完全遍历一次二叉树,两个节点的祖先路径信息就已经可以拿到了,没必要遍历两次。下面来优化一下
优化
javascript
const findCommonParent2 = (tree, n1, n2) => {
const stack = [];
let stack1 = null,
stack2 = null;
let node = tree;
let pre = null;
while (stack.length || node) {
while (node) {
stack.push(node);
node = node.left;
}
[node] = stack.slice(-1);
if (node.right && pre !== node.right) {
node = node.right;
} else {
if (stack1 == null && node.value == n1.value) {
stack1 = [...stack.slice(0, -1)];
}
if (stack2 == null && node.value == n2.value) {
stack2 = [...stack.slice(0, -1)];
}
if (stack1 !== null && stack2 !== null) {
return getLastParent(stack1, stack2);
}
stack.pop();
pre = node;
node = null;
}
}
};
const getLastParent = (path1, path2) => {
let lastParent = null;
for (let i = 0; i < path1.length && i < path2.length; i++) {
if (path1[i].value == path2[i].value) lastParent = path1[i].value;
else return lastParent;
}
return lastParent;
};
原先的遍历代码不能用了,重写一套遍历流程。遍历流程也是一套经典的后序遍历。在visit
当前的节点的时候,不再是简单地将其输出了。而是分别与两个参数n1
,n2
比较。
在遍历过程中,可能会先找到其中一个节点,这时候将该节点地相先路径保存下来。然后继续遍历,直到找到另一个节点,然后将第二个节点的祖先路径保存下来,这样就可以开始比较了(getLastParent
)
由于两个节点传入的顺序是随机的,所以不知道会先遍历到哪个,也不知道两个节点是否相等。所以在visit
阶段,每个节点都要去比较。当然如果之前找到了该节点,自然不用再去比较。所以会有一个stack !== null
的判断
算法复杂度 O(n + logn)
执行代码:
javascript
console.log(findCommonParent2(tree, { value: 2 }, { value: 6}));
javascript
console.log(findCommonParent2(tree, { value: 2 }, { value: 1 }));
1 和 2 没有公共父祖先, 执行结果都符合预期
完整代码
javascript
//准备数据
const data = [0, 1, 2, 3, 4, 5, 6, null];
const generateTree = (data, i) => {
if (!data[i]) return null;
const root = { value: data[i] };
root.left = generateTree(data, 2 * i);
root.right = generateTree(data, 2 * i + 1);
return root;
};
const tree = generateTree(data, 1);
// 返回祖先路径
const getParents = (tree, p) => {
if (!tree) return;
const stack = [];
let node = tree;
let pre = null;
while (stack.length !== 0 || node !== null) {
while (node) {
stack.push(node);
node = node.left;
}
node = stack.slice(-1)[0];
if (node.right && pre !== node.right) {
node = node.right;
} else {
if (node.value == p.value) return stack.map((item) => item.value).slice(0, -1);
stack.pop();
pre = node;
node = null;
}
}
return null;
};
//找到最近公共祖先
const findCommonParent = (tree, n1, n2) => {
const path1 = getParents(tree, n1);
const path2 = getParents(tree, n2);
let lastParent = null;
for (let i = 0; i < path1.length && i < path2.length; i++) {
if (path1[i] == path2[i]) lastParent = path1[i];
else return lastParent;
}
return lastParent;
};
//找到最近公共祖先--优化
const findCommonParent2 = (tree, n1, n2) => {
const stack = [];
let stack1 = null,
stack2 = null;
let node = tree;
let pre = null;
while (stack.length || node) {
while (node) {
stack.push(node);
node = node.left;
}
[node] = stack.slice(-1);
if (node.right && pre !== node.right) {
node = node.right;
} else {
if (stack1 == null && node.value == n1.value) {
stack1 = [...stack.slice(0, -1)];
}
if (stack2 == null && node.value == n2.value) {
stack2 = [...stack.slice(0, -1)];
}
if (stack1 !== null && stack2 !== null) {
return getLastParent(stack1, stack2);
}
stack.pop();
pre = node;
node = null;
}
}
};
const getLastParent = (path1, path2) => {
let lastParent = null;
for (let i = 0; i < path1.length && i < path2.length; i++) {
if (path1[i].value == path2[i].value) lastParent = path1[i].value;
else return lastParent;
}
return lastParent;
};
总结
这篇文章分享用 JS 代码找二叉树任意两个节点的最近公共父祖先,并且给出了两个复杂度不相同的方法,从中可以得知,减少代码复杂度的关键就是利用好每个信息,不做多余的动作。
测试样例很清晰。代码可以直接 copy 下来在本地运行