二叉树是一种重要的数据结构,在计算机科学中被广泛应用,例如在搜索算法、排序算法以及数据库索引等方面,在面试中也是超高频的考点。本文会详细介绍二叉树及其遍历、构建、深度、平衡以及路径。
在计算机科学中,二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。
二叉树的节点定义通常包含一个数据元素以及指向左右子节点的指针。以下是一个简单的二叉树节点类的示例(以 Java 为例):
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
满二叉树:在一颗二叉树中,除了叶节点外,每个节点都有 2 个子节点。

完全二叉树:在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干节点。以根节点深度为 1 计算,具有 n 个节点的完全二叉树的深度为 logn + 1。深度为 k 的完全二叉树,至少有2^(k - 1) 个节点,至多有 2^k - 1 个节点。

记忆小技巧:

二叉树的前序,中序和后序遍历通常有两种方法可以实现:递归和迭代。(非常建议不熟悉递归概念的小伙伴先学习我之前写的分享 递归详解)下面我们分别来看这两种方法是如何实现的,先看递归。
递归的思路非常简单,代码也是非常简洁。以前序为例,我们先访问根节点,再左,最后右。递归的写法如下
// 前序遍历
public void preOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " "); // 也可以使用相应的数据结构进行存储,这里我们选择打印
preOrderTraversal(root.left);
preOrderTraversal(root.right);
}同理,中序和后序遍历的递归写法,分别为
// 中序遍历
public void inOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
inOrderTraversal(root.left);
System.out.print(root.val + " "); // 也可以使用相应的数据结构进行存储,这里我们选择打印
inOrderTraversal(root.right);
}// 后序遍历
public void postOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
postOrderTraversal(root.left);
postOrderTraversal(root.right);
System.out.print(root.val + " "); // 也可以使用相应的数据结构进行存储,这里我们选择打印
}递归前中后序遍历的时间复杂度和空间复杂度相同,为
时间复杂度:O(n),每个节点均被访问一次
空间复杂度:O(h), h 为树的高度,为递归栈调用的空间;如果我们需要存储节点或者节点的值到数组或者List, 则空间复杂度为O(n)
写完递归,再看迭代,用迭代的方法完成树的遍历需基于以下两点:
前序遍历
迭代解法基本步骤:

代码(Java)
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Deque<TreeNode> stack = new LinkedList<>();
stack.offerLast(root);
while (!stack.isEmpty()) {
TreeNode current = stack.pollLast();
result.add(current.val);
if (current.right!= null) {
stack.offerLast(current.right);
}
if (current.left!= null) {
stack.offerLast(current.left);
}
}
return result;
}
中序遍历
迭代解法基本步骤:

代码(Java)
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode current = root;
while (current != null || !stack.isEmpty()) {
if (current != null) {
stack.offerLast(current);
current = current.left;
} else {
current = stack.pollLast();
result.add(current.val);
current = current.right;
}
}
return result;
}后序遍历
迭代解法基本步骤:

代码(Java)
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode current = root;
TreeNode pre = null;
stack.offerLast(current);
while (!stack.isEmpty()) {
current = stack.peekLast();
if ((current.left == null && current.right == null) ||
(pre != null && pre == current.left) ||
(pre != null && pre == current.right)) {
result.add(current.val);
stack.pollLast();
pre = current;
} else {
if (current.right != null) {
stack.offerLast(current.right);
}
if (current.left != null) {
stack.offerLast(current.left);
}
}
}
return result;
}迭代前中后序遍历的时间复杂度和空间复杂度相同,为
时间复杂度:O(n),每个节点均被访问一次
空间复杂度:O(h), h 为树的高度,为栈的空间;如果我们需要存储节点或者节点的值到数组或者List, 则空间复杂度为O(n)
层序遍历
树的层序遍历是一种对树进行遍历的方式,它按照树的层次依次访问每个节点。

层序遍历的核心在于按照树的层次来访问节点,这与深度优先遍历(如前序、中序、后序遍历)不同。实现层序遍历的关键是要有一种机制,能够记录并按顺序访问同一层的节点,然后再访问下一层。
public List<List<Integer>> printNodes(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> curResult = new ArrayList<>();
while (size-- > 0) {
TreeNode cur = queue.poll();
curResult.add(cur.value);
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
result.add(curResult);
}
return result;
}时间复杂度:O(n), n 为节点的个数
空间复杂度:O(n)
蛇形层序遍历(Zig-zag 遍历)
树的蛇形层序遍历是一种特殊的树的遍历方式,它在进行层序遍历的基础上,按照层次交替改变遍历的方向,类似蛇形的行进方式。

思路:使用双端队列,奇数层从Last取,从右子树到左子树依次往First端存;偶数层从First端取,从左至右依次往Last端存。
public List<Integer> snakeTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Deque<TreeNode> deque = new LinkedList<>();
deque.offerLast(root);
int level = 0;
while (!deque.isEmpty()) {
int size = deque.size();
level++;
while (size > 0) {
TreeNode cur;
if (level % 2 == 0) {
cur = deque.pollFirst();
if (cur.left != null) {
deque.offerLast(cur.left);
}
if (cur.right != null) {
deque.offerLast(cur.right);
}
} else {
cur = deque.pollLast();
if (cur.right != null) {
deque.offerFirst(cur.right);
}
if (cur.left != null) {
deque.offerFirst(cur.left);
}
}
size--;
result.add(cur.value);
}
}
return result;
}时间复杂度:O(n), n 为节点的个数
空间复杂度:O(n)
根据给定的遍历序列构建二叉树,比如根据前序遍历和中序遍历序列,或者后序遍历和中序遍历序列。解法关键在于利用遍历序列的特点找到根节点在序列中的位置,从而划分左右子树的序列,递归构建。
Leetcode 105 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
思路分析:

代码
public TreeNode buildTree(int[] preorder, int[] inorder) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return buildTree(preorder, inorder, 0, preorder.length - 1, 0, inorder.length - 1, map);
}
private TreeNode buildTree(int[] preorder, int[] inorder, int preStart, int preEnd, int inStart, int inEnd, Map<Integer, Integer> map) {
if (preStart > preEnd || inStart > inEnd) {
return null;
}
TreeNode root = new TreeNode(preorder[preStart]);
int indexOfRootInInorder = map.get(preorder[preStart]);
root.left = buildTree(preorder, inorder, preStart + 1, preStart + indexOfRootInInorder - inStart, inStart, indexOfRootInInorder - 1, map);
root.right = buildTree(preorder, inorder, preStart + indexOfRootInInorder - inStart + 1, preEnd, indexOfRootInInorder + 1, inEnd, map);
return root;
}时间复杂度:O(n)
空间复杂度:O(n)
Leetcode 106 从中序与后序遍历序列构造二叉树
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
思路分析:同上,主要的区别在于,根节点的位置在后序遍历数组中的最后一个,确定了根节点的位置再根据中序遍历确定左右子树的区间,再递归求解
public TreeNode buildTree(int[] inorder, int[] postorder) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return buildTree(inorder, postorder, 0, inorder.length - 1, 0, postorder.length - 1, map);
}
private TreeNode buildTree(int[] inorder, int[] postorder, int inStart, int inEnd, int postStart, int postEnd, Map<Integer, Integer> map) {
if (inStart > inEnd || postStart > postEnd) {
return null;
}
TreeNode root = new TreeNode(postorder[postEnd]);
int indexOfRootInInorder = map.get(postorder[postEnd]);
root.left = buildTree(inorder, postorder, inStart, indexOfRootInInorder - 1, postStart, postStart + indexOfRootInInorder - 1 - inStart, map);
root.right = buildTree(inorder, postorder, indexOfRootInInorder + 1, inEnd, postStart + indexOfRootInInorder - inStart, postEnd - 1, map);
return root;
}时间复杂度:O(n)
空间复杂度:O(n)
Leetcode 104 二叉树的最大深度
题目描述:给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
思路分析:
对于二叉树的最大深度问题,我们可以采用递归的方法来求解。二叉树的最大深度定义为根节点到最远叶子节点的最长路径上的节点数。递归计算左子树和右子树的最大深度,然后取两者中的较大值,再加上 1(根节点这一层)就是整个二叉树的最大深度。递归的基本情况是当节点为 null 时,此时深度为 0,表示已经到达叶子节点的下一层(空节点层)。
代码:
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}时间复杂度:O(n), 每个节点访问一次
空间复杂度:O(h), h 为树的高度,如果树为完全二叉树或接近于完全二叉树,h = logn。空间复杂度主要来自于递归栈的调用
public boolean isBalanced(TreeNode root) {
return treeHeight(root) >= 0;
}
private int treeHeight(TreeNode root) {
if (root == null) {
return 0;
}
int leftHeight = treeHeight(root.left);
int rightHeight = treeHeight(root.right);
if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
return -1;
}
return Math.max(leftHeight, rightHeight) + 1;
}时间复杂度:O(n), 每个节点访问一次
空间复杂度:O(h), h 为树的高度,如果树为完全二叉树或接近于完全二叉树,h = logn。空间复杂度主要来自于递归栈的调用
问题描述:给定一个二叉树,返回从一个叶子节点到另一个叶子节点最大路径的和。

问题分析:
解决树的路径问题需要考虑三个核心问题:
以上的三个问题对应递归三要素的基准、分解以及求解。那我们分别来回答以上的核心问题
代码
public int maximumLeavesPath(TreeNode root) {
int[] res = {Integer.MIN_VALUE};
findLargestPath(root, res);
return res[0];
}
private int findLargestPath(TreeNode root, int[] res) {
if (root == null) {
return 0;
}
int leftLargestPath = findLargestPath(root.left, res);
int rightLargestPath = findLargestPath(root.right, res);
if (root.left != null && root.right != null) {
// 如果左右子树都不为空,说明找到一条从叶子节点到另一个叶子节点的路径,更新结果
res[0] = Math.max(res[0], leftLargestPath + rightLargestPath + root.value);
// 从左右子树中挑一个更大的路径,加上当前节点的值,返回给上一层 (告诉父节点:这是到我这为止的最大单边路径)
return root.value + Math.max(leftLargestPath, rightLargestPath);
}
// 如果左边为空,选右边的(如果右边也为空,rightLargestPath 也为0,不影响结果);如果右边为空,选左边的;
// 再加上自己,组成一条新的单边路径,返回给上一层 (告诉父节点:这是到我这为止的最大单边路径)
return root.left == null ? rightLargestPath + root.value : leftLargestPath + root.value;
}图解

时间复杂度和空间复杂度
假设这个二叉树有 n 个节点
时间复杂度为O(n), 因为每个节点都访问了一次,每次访问都是常数级操作。
空间复杂度主要来自递归栈的调用,而递归的深度取决于树的高度,n个节点的树,最差情况的高度为n, 树的每一层只有一个节点,看似一条直线,此时来自栈的空间复杂度为O(n)。如果是完全二叉树高度约为O(logn), 来自栈调用的空间复杂度为O(logn)。综上,最差情况的栈空间复杂度为O(n)。