我最近有机会教高中生如何编码。关于用JavaScript编码的算法,并没有那么多适合初学者的教程,而JavaScript正是他们正在学习的语言。所以我决定做一个。
在这篇文章中,我将尽力解释一些你在编码面试前应该学习的核心算法。
如果你对二叉树的概念不熟悉,我鼓励你去看看维基百科的页面。如果你完全掌握了这些基本算法,你会更容易解决更复杂的问题。
什么是二进制搜索树(BST)?
BST在编码面试中很常见,它是一种树状的数据结构,在最顶端有一个单根。它们是存储数值的好方法,因为它们的有序性允许快速搜索和查找。
与普通的树相比,BST具有以下特性:
- 每一个左边的孩子都比它的父辈有一个更小的值
- 每个右边的子节点的数值都比其父节点的数值大
- 每个节点可以包含0到2个孩子。
下面的图表应该能更清楚地说明问题。
二叉树节点的定义

一棵二进制搜索树
我们通常用Javascript中的以下函数来定义二叉树节点。
function TreeNode(val, left, right) {
this.val = val
this.left = left
this.right = right
}
二叉树基本遍历(Inorder, Postorder, Preorder)
首先要知道的是如何循环浏览BST的每个节点。这使我们能够对我们的BST的所有节点执行一个函数。例如,如果我们想在我们的BST中找到一个值x ,我们就需要这些节点。
有三种主要的方法可以做到这一点。幸运的是,它们有共同的主题。
顺序遍历
递归算法是最容易开始使用二叉树顺序遍历的方法。其思路如下:
- 如果节点是空的,什么都不做--否则,递归调用该节点左侧子节点上的函数。
- 然后,在遍历了所有的左侧子节点后,对该节点做一些操作。我们当前的节点被保证是最左边的节点。
- 最后,在node.right上调用该函数。
Inorder算法从左到中再到右遍历了树的节点:
/**
* @param {TreeNode} root
*/
const inorder = (root) => {
const nodes = []
if (root) {
inorder(root.left)
nodes.push(root.val)
inorder(root.right)
}
return nodes
}
// for our example tree, this returns [1,2,3,4,5,6]
后序遍历
递归算法是启动后序遍历的最简单的方法:
- 如果节点是空的,什么都不做--否则,递归调用该节点左边子节点上的函数。
- 当没有更多的左侧子节点时,调用node.right上的函数。
- 最后,对节点做一些操作。
后序遍历从左到右,再到中间访问树节点:
/**
* @param {TreeNode} root
*/
const postorder = (root) => {
const nodes = []
if (root) {
postorder(root.left)
postorder(root.right)
nodes.push(root.val)
}
return nodes
}
// for our example tree, this returns [1,3,2,6,5,4]
前序遍历
递归算法是前序遍历的最简单的方法:
- 如果节点为空,什么都不做--否则,对节点做一些操作。
- 遍历到节点的左边的子节点并重复。
- 遍历到节点的右侧子节点并重复。
后序遍历从中间到左边再到右边访问树节点:
/**
* @param {TreeNode} root
*/
const preorder = (root) => {
const nodes = []
if (root) {
nodes.push(root.val)
preorder(root.left)
preorder(root.right)
}
return nodes
}
// for our example tree, this returns [4,2,1,3,5,6]
什么是有效的二进制搜索树?
一棵有效的二进制搜索树(BST)有所有左边子节点的值小于父节点,所有右边子节点的值大于父节点。
为了验证一棵树是否是有效的二进制搜索树:
- 定义当前节点可以拥有的最小和最大值
- 如果一个节点的值不在这些范围内,返回false
- 递归验证该节点的左侧子节点,最大边界设置为该节点的值
- 递归验证节点的右侧子节点,最小边界设置为节点的值
/**
* @param {TreeNode} root
*/
const isValidBST = (root) => {
const helper = (node, min, max) => {
if (!node) return true
if (node.val <= min || node.val >= max) return false
return helper(node.left, min, node.val) && helper(node.right, node.val, max)
}
return helper(root, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
}
如何找到二叉树的最大深度
这里,该算法试图找到我们的BST的高度/深度。换句话说,我们在看一个BST包含多少个 "层次":
- 如果节点为空,我们返回0,因为它没有增加任何深度
- 否则,我们在当前的深度上加+1(我们穿越了一个层次)。
- 递归计算节点的子节点的深度,并返回node.left和node.right之间的最大和。
/**
* @param {TreeNode} root
*/
const maxDepth = function(root) {
const calc = (node) => {
if (!node) return 0
return Math.max(1 + calc(node.left), 1 + calc(node.right))
}
return calc(root)
};
如何找到两个树节点之间的最低共同祖先
让我们提升一下难度。我们如何找到二叉树中两个树节点之间的共同祖先?让我们看看一些例子:

一棵二进制搜索树
在这棵树上,3和1的最低共同祖先是2,3和2的LCA是2,6和1及6的LCA是4。
看到这里的模式了吗?两个树节点之间的LCA要么是节点本身(3和2的情况),要么是一个父节点,其第一个孩子在其左子树的某个地方被发现,第二个孩子在其右子树的某个地方。
找到两个树节点p和q之间的最低共同祖先(LCA)的算法如下:
- 验证p或q是否在左子树或右子树中被发现
- 然后,验证当前节点是否为p或q
- 如果在左子树或右子树中找到了p或q,并且p或q中的一个是节点本身,我们就找到了LCA。
- 如果p和q都在左边或右边的子树上,我们就找到了LCA。
/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
*/
const lowestCommonAncestor = function(root, p, q) {
let lca = null
const isCommonPath = (node) => {
if (!node) return false
var isLeft = isCommonPath(node.left)
var isRight = isCommonPath(node.right)
var isMid = node == p || node == q
if (isMid && isLeft || isMid && isRight || isLeft && isRight) {
lca = node
}
return isLeft || isRight || isMid
}
isCommonPath(root)
return lca
};
总结
综上所述,我们已经学会了如何遍历、验证和计算BST的深度。
这些算法经常在编码面试中被问及。在练习更高级的BST应用(如寻找两个节点的LCA)之前,了解它们很重要。