662. Maximum Width of Binary Tree

221 阅读5分钟

题干

662. Maximum Width of Binary Tree

Difficulty: Medium

Given a binary tree, write a function to get the maximum width of the given tree. The width of a tree is the maximum width among all levels. The binary tree has the same structure as a full binary tree, but some nodes are null.

The width of one level is defined as the length between the end-nodes (the leftmost and right most non-null nodes in the level, where the null nodes between the end-nodes are also counted into the length calculation.

给予一个二叉树,得到这个树的最大宽度。 最大宽度指的是二叉树的同一层中,最左边的非空节点和最右边的非空节点之间的距离。

解题思路

如同题干中分析的,只需要找到每一层最左边的节点和最右边的节点就可以了,中间所有的节点我们都不care。 不过说来惭愧,这道中等难度的题,解了两个小时,写了三种写法,其中第一种是错的,第两种都谈不上最优解,而如果一个认真学数据结构的人来做这题的话,可能第一次就能写出最优解了。

第一种错误写法:
var widthOfBinaryTree = function (root) {
	let left = [];
	let right = [];
	if (!root) {
		return 0
	}
	const traverse = (root) => {
		if (root.left) {
			left.push(0);
			traverse(root.left);
		} else if (root.right) {
			left.push(1);
			traverse(root.right)
		}
	};
	const inversiveTraverse = (root) => {
		if (root.right) {
			right.push(0);
			inversiveTraverse(root.right);
		} else if (root.left) {
			right.push(1);
			inversiveTraverse(root.left)
		}
	};
	traverse(root);
	inversiveTraverse(root);
	left.map((x, i, arr) => {
		if (i) {
			return arr[i - 1] * 2 + x;
		} else {
			return x;
		}
	});
	right.map((x, i, arr) => {
		if (i) {
			return arr[i - 1] * 2 + x;
		} else {
			return x;
		}
	});
	let max = -1;
	left.forEach((x, i) => {
		let tmp = (2 ** (i + 1) - left[i] - right[i]);
		if (tmp > max) {
			max = tmp;
		}
	})
	return max;
};

其实思路上没有太大的问题,遍历每个层级上最靠左和最靠右的节点即可,之所以这么想是因为我觉得没有必要遍历所有的节点,尽可能的遍历两边的节点就可以了,然后通过记录走过的路径来计算出最靠边的节点的位置,进而计算出最大的宽度,然而这个写法的问题在于我没法处理某个不在最后一层的叶子节点,进而我想到了一个改进写法,DP。

动态规划:

如果在遍历的时候走到了某个不处于最后一层的叶子节点,为了遍历到更深的层级,我需要回溯到父节点,然而一般二叉树是不会有父节点指针的,所以我需要额外维护两个队列,记录遍历时走过的路径,以及走过的方向。

var widthOfBinaryTreeDP = function (root) {
	if (!root) {
		return 0;
	}
	// 计算所处的位置
	const calcPos = (arr) => {
		let sum = 0;
		arr.forEach((x) => {
			sum = sum * 2 + x;
		});
		return sum;
	}
	let left = []; // 记录每一层最左边的节点
	let leftNode = []; // 遍历时的节点路径队列
	let leftDirection = []; // 遍历时的路径方向队列
	const leftDp = (root) => {
		if (!root) return;
		if (root.left) {
			// 记录路径的节点和方向
			leftNode.push(root);
			leftDirection.push(0);
			// 第一个找到的节点必定是最远的那个,记录距离
			if (left[leftNode.length - 1] === undefined) {
				left.push(calcPos(leftDirection));
			}
			leftDp(root.left);
		} else if (root.right) {
			leftNode.push(root);
			leftDirection.push(1);
			if (left[leftNode.length - 1] === undefined) {
				left.push(calcPos(leftDirection));
			}
			leftDp(root.right);
		} else {
			// 如果找到叶子节点,则开始回溯
			let node = leftNode.pop();
			let direction = leftDirection.pop();
			// 回溯到第一个存在右节点且在上次遍历中走向了左节点的节点
			while ((direction === 1 || (direction === 0 && !node.right)) && leftNode.length) {
				node = leftNode.pop();
				direction = leftDirection.pop();
			}
			// 回溯成功,改变路径方向,改为走向右节点
			if (leftNode.length || direction === 0) {
				leftNode.push(node);
				leftDirection.push(1);
				if (left[leftNode.length - 1] === undefined) {
					left.push(calcPos(leftDirection));
				}
				leftDp(node.right);
			} else {
				// 根节点再上一次遍历中已经走向了右节点,说明已经遍历所有可能性
				return;
			}
		}
	}

	// 对最右节点的遍历与上面类似,只是优先找最右节点。
	let right = [];
	let rightNode = [];
	let rightDirection = [];
	const rightDp = (root) => {
		if (!root) return;
		if (root.right) {
			rightNode.push(root);
			rightDirection.push(0);
			if (right[rightNode.length - 1] === undefined) {
				right.push(calcPos(rightDirection));
			}
			rightDp(root.right);
		} else if (root.left) {
			rightNode.push(root);
			rightDirection.push(1);
			if (right[rightNode.length - 1] === undefined) {
				right.push(calcPos(rightDirection));
			}
			rightDp(root.left);
		} else {
			let node = rightNode.pop();
			let direction = rightDirection.pop();
			while ((direction === 1 || (direction === 0 && !node.left)) && rightNode.length) {
				node = rightNode.pop();
				direction = rightDirection.pop();
			}
			if (rightNode.length || direction === 0) {
				rightNode.push(node);
				rightDirection.push(1);
				if (right[rightNode.length - 1] === undefined) {
					right.push(calcPos(rightDirection));
				}
				rightDp(node.left);
			} else {
				return;
			}
		}
	}
	leftDp(root);
	rightDp(root);
	let max = 1;
	// 计算最大宽度
	left.forEach((x, i) => {
		let tmp = (2 ** (i + 1) - left[i] - right[i]);
		if (tmp > max) {
			max = tmp;
		}
	})
	return max;
};

成功AC,本来写到这就结束了,然而却看到一句Your runtime beats 5.83% of javascript submission。 这也太丢脸了,这算法我调了整整一个小时啊,不行我得写个好看的。

深度优先遍历

一开始要我用遍历的时候,我是拒绝的,毕竟我只需要最靠边的节点就可以了,没有必要去遍历那些中间的节点,但是由于我一开始并不知道深度,所以我在做DP的时候其实也把大半的节点给遍历了,但是DP我需要做两次,第一次找最左,第二次找最右,于是乎总体的时间是大于做一次遍历的。 如果我知道树的深度,那么用DP则是更好的一个解法,可惜求解树的深度的算法本身就是一个BFS,所以这题的解法最优解实际上是BFS和DFS,我自己写了一个BFS版本的。

var widthOfBinaryTreeDFS = function (root) {
	let ranges = [];
	let max = 0;
	const traverse = (root, depth, pos) => {
		if (!root) {
			return;
		}
		if (root.left) {
			left = traverse(root.left, depth + 1, pos * 2);
		}
		if (root.right) {
			right = traverse(root.right, depth + 1, pos * 2 + 1);
		}
		if (ranges[depth]) {
			if (pos <= ranges[depth][1] && pos > ranges[depth][1]) {
				return;
			} else {
				if (pos > ranges[depth][1]) {
					ranges[depth][1] = pos;
				} else if (pos < ranges[depth][0]) {
					ranges[depth][0] = pos;
				}
				let range = ranges[depth][1] - ranges[depth][0] + 1;
				if (max < range) {
					max = range;
				}
			}
		} else {
			ranges[depth] = [pos, pos];
			if (max === 0) {
				max = 1;
			}
		}
	}
	traverse(root, 0, 0);
	return max;
};

记录下每个节点的深度和位置,再判断该节点是否比同深度其他节点要更加靠近边界,若是则更新当前深度的宽度,并和最大宽度做比较,来更新最大宽度。 写的简单,算的还快。做了一点小优化以后得出了上面的算法,成功AC,得到一句Your runtime beats 98.06 % of javascript submissions。