🥳每日一练-二叉树中,找到最近公共祖先-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 分钟前
webpack 核心模块 — loader & plugins
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
shymoy13 分钟前
Radix Sorts
数据结构·算法·排序算法
风影小子21 分钟前
注册登录学生管理系统小项目
算法
黑龙江亿林等保23 分钟前
深入探索哈尔滨二级等保下的负载均衡SLB及其核心算法
运维·算法·负载均衡
lucy1530275107926 分钟前
【青牛科技】GC5931:工业风扇驱动芯片的卓越替代者
人工智能·科技·单片机·嵌入式硬件·算法·机器学习
四喜花露水40 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
杜杜的man42 分钟前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
前端Hardy1 小时前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
小沈熬夜秃头中୧⍤⃝1 小时前
【贪心算法】No.1---贪心算法(1)
算法·贪心算法
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript