🥳每日一练-二叉树中,找到最近公共祖先-JS

上篇文章讲了二叉树的后序遍历,并且提到了一个题目: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 当前的节点的时候,不再是简单地将其输出了。而是分别与两个参数n1n2比较。

在遍历过程中,可能会先找到其中一个节点,这时候将该节点地相先路径保存下来。然后继续遍历,直到找到另一个节点,然后将第二个节点的祖先路径保存下来,这样就可以开始比较了(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 下来在本地运行

相关推荐
小桥风满袖1 小时前
极简三分钟ES6 - ES9中字符串扩展
前端·javascript
前端人类学1 小时前
掌控异步洪流:多请求并发下的顺序控制艺术
javascript·promise
CryptoRzz1 小时前
印度尼西亚股票数据API对接实现
javascript·后端
lecepin2 小时前
AI Coding 资讯 2025-09-17
前端·javascript·面试
猩兵哥哥3 小时前
前端面向对象设计原则运用 - 策略模式
前端·javascript·vue.js
江城开朗的豌豆3 小时前
解密React虚拟DOM:我的高效渲染秘诀 🚀
前端·javascript·react.js
江城开朗的豌豆4 小时前
React应用优化指南:让我的项目性能“起飞”✨
前端·javascript·react.js
Asort4 小时前
JavaScript 从零开始(六):控制流语句详解——让代码拥有决策与重复能力
前端·javascript
EMT4 小时前
在 Vue 项目中使用 URL Query 保存和恢复搜索条件
javascript·vue.js
艾小码4 小时前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js