本文总结了关于二叉树的常见算法题
判断叶子节点:if (root.left == null && root.right == null)
1、递归遍历
每个节点会到达三次,前序为输出第一次到达,中序为输出第二次到达,后序为第三次到达
1.1、递归先序
public void preOrderRecur(TreeNode head) {
if (head == null) {
return;
}
System.out.print(head.val+" ");
preOrderRecur(head.left);
preOrderRecur(head.right);
}
1.2、递归中序
public void inOrderRecur(TreeNode head) {
if (head == null) {
return;
}
inOrderRecur(head.left);
System.out.print(head.val + " ");
inOrderRecur(head.right);
}
1.3、递归后序
public void postOrderRecur(TreeNode head) {
if (head == null) {
return;
}
postOrderRecur(head.left);
postOrderRecur(head.right);
System.out.print(head.val + " ");
}
2、非递归遍历
2.1、非递归先序
需要自己用栈记录,每次取出栈顶cur,如果cur的右孩子不为空,则先放右孩子,如果cur的左孩子不为空,在把左孩子放入栈中。
public void preOrderUnRecur(TreeNode head) {
if (head == null) {
return;
}
Stack stack = new Stack<>();
stack.push(head);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
System.out.print(cur.val + " ");
if (cur.right != null) stack.push(cur.right);
if (cur.left != null) stack.push(cur.left);
}
}
2.2、非递归中序
如果当前节点不为空,就把当前节点放入栈中,并向左孩子移动;如果当前节点为空,就去取出栈顶的元素,打印它,并向它的右孩子移动。
public void inOrderUnRecur(TreeNode cur) {
if (cur == null) {
return;
}
Stack stack = new Stack<>();
while (!stack.isEmpty() || cur != null) {
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
cur = stack.pop();
System.out.print(cur.val + " ");
cur = cur.right;
}
}
}
2.3、非递归后序
两个栈,一个栈用来存放结果res,一个存放还没放入res的栈stack。每次从stack栈顶pop取出元素,放入res栈中,同时把pop的左孩子放入stack,右孩子放入stack,当然要判断是否为空。
public void posOrderUnRecur1(TreeNode head) {
if (head == null) {
return;
}
Stack stack = new Stack<>();
Stack res = new Stack<>();
stack.push(head);
while (!stack.isEmpty()) {
TreeNode pop = stack.pop();
res.push(pop);
if (pop.left != null) stack.push(pop.left);
if (pop.right != null) stack.push(pop.right);
}
while (!res.isEmpty()) {
System.out.print(res.pop().val + " ");
}
}
3、Morris遍历
普通的递归遍历 和 非递归遍历的时间复杂度是O(N),额外空间复杂度是O(h);
二叉树的Morris遍历,时间复杂度O(N),额外空间复杂度O(1)。可以用来优化大部分基于遍历二叉树的问题。
特点:一个节点,有左孩子就会访问两次,没有左孩子只会访问一次
规则:
假设当前节点是cur,开始时cur指向头结点
①. 如果cur没有左孩子,cur直接向右移动【cur = cur.right】
②. 如果cur有左孩子,找到cur左子树上最右的节点【mostRight】
1、如果mostRight的右指针指向空,让右指针指向cur,然后cur向左移动【cur = cur.left】
2、如果mostRight的右指针指向cur,让右指针指向null,然后cur向右移动【cur = cur.right】
当cur为空时遍历结束
public void morrisPure(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null) {
if (cur.left == null) {
cur = cur.right;//没有左孩子,第一次访问【也可以理解为第一次第二次同时】
} else {
mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {//有左孩子,第一次访问
mostRight.right = cur;
cur = cur.left;
} else {
mostRight.right = null;//有左孩子,第二次访问
cur = cur.right;
}
}
}
}
3.1、Morris先序
morrisPure中第一次访问输出
3.2、Morris中序
morrisPure中第二次访问输出
3.3、Morris后序
只关心能访问两次的节点,也就是有左孩子的节点,
1、第二次访问这些节点时,逆序打印节点的左子树,这时就需要反转左子树,打印,在反转回来
2、遍历完成后,逆序打印整个树的右边界
public void morrisPos(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
TreeNode mostRight = null;
while (cur != null) {
if (cur.left == null) {
cur = cur.right;
} else {
mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
} else {
mostRight.right = null;
reversePrintEdge(cur.left); //有左孩子,第二次访问,才逆序打印左孩子的右边界
cur = cur.right;
}
}
}
reversePrintEdge(root); //最后逆序打印根节点的右边界
System.out.println();
}
public void reversePrintEdge(TreeNode left) {
TreeNode tail = reverseEdge(left);
TreeNode cur = tail;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.right;
}
reverseEdge(tail);
}
public TreeNode reverseEdge(TreeNode node) {
TreeNode next = null;
TreeNode pre = null;
while (node != null) {
next = node.right;
node.right = pre;
pre = node;
node = next;
}
return pre;
}
4、二叉树的最大深度
//使用递归
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
//使用广度优先遍历
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue queue = new LinkedList<>();
queue.add(root);
int height = 0;
while (!queue.isEmpty()) {
int size = queue.size();
height++;
for (int i = 0; i < size; i++) {
TreeNode cur = queue.remove();
if (cur.left != null) queue.add(cur.left);
if (cur.right != null) queue.add(cur.right);
}
}
return height;
}
5、二叉树的最小深度
public int minDepth2(TreeNode root) {
if (root == null) {
return 0;
}
if (root.left == null && root.right == null) {
return 1;
}//判断叶子节点
int left = root.left != null ? minDepth2(root.left) : Integer.MAX_VALUE;
int right = root.right != null ? minDepth2(root.right) : Integer.MAX_VALUE;
return Math.min(left, right) + 1;
}
//使用广度优先遍历
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue queue = new LinkedList<>();
queue.add(root);
int min = 0;
while (!queue.isEmpty()) {
int size = queue.size();
min++;
for (int i = 0; i < size; i++) {
TreeNode cur = queue.remove();
if (cur.left == null && cur.right == null) {
return min;
}//判断叶子节点
if (cur.left != null) queue.add(cur.left);
if (cur.right != null) queue.add(cur.right);
}
}
return min;
}
6、二叉树层次遍历
6.1、普通层次遍历
输出:1、2、3、4、5、6、7
使用队列,从对头取出cur,把cur的左孩子,右孩子,依次放入对尾
public void levelOrder(TreeNode head) {
if (head == null) {
return;
}
Queue queue = new LinkedList<>();
queue.add(head);
while (!queue.isEmpty()) {
TreeNode cur = queue.remove();
System.out.print(cur.val + " ");
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
}
7、 二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/
2 7
/ \ /
1 3 6 9
镜像输出:
4
/
7 2
/ \ /
9 6 3 1
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
mirrorTree(root.left);
mirrorTree(root.right);
return root;
}
8、 判断是否是对称的二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的
1
/
2 2
/ \ /
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/
2 2
\
3 3
//递归,也可以用层次遍历做
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
return isEqual(root.left, root.right);
}
public boolean isEqual(TreeNode left, TreeNode right) {
if (left == null && right == null) {
return true;
}
if (left == null || right == null) {
return false;
}
if (left.val != right.val) {
return false;
}
return isEqual(left.left, right.right) && isEqual(left.right, right.left);
}
9、 二叉树中和为某一值的路径
回溯法:
注意:1、必须要是叶子节点才添加和满足条件的列表;2、可能存在负数
public List> pathSum(TreeNode root, int sum) {
if (root == null) {
return new ArrayList<>();
}
List> res = new ArrayList<>();
List temp = new ArrayList<>();
traceBack(root, res, temp, 0, sum);
return res;
}
public void traceBack(TreeNode root, List> res, List temp, int cur, int sum) {
if (root == null) {
return;
}
temp.add(root.val);
if (root.left == null && root.right == null) {//必须要是叶子节点
if (cur + root.val == sum) {//和要满足要求
res.add(new ArrayList<>(temp));
}
}
traceBack(root.left, res, temp, cur + root.val, sum);
traceBack(root.right, res, temp, cur + root.val, sum);
temp.remove(temp.size() - 1);
}
10、 重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/
9 20
/
15 7
思路:
1、先序的第一个位置node1是root节点;
2、在中序中找到node1的位置k,以及计数count;
3、根据k和count进一步对先序数组和中序数组划分,如上图
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null || preorder.length == 0 ||
inorder == null || inorder.length == 0 ||
preorder.length != inorder.length) {
return null;
}
return build(preorder, inorder, 0, preorder.length - 1, 0, inorder.length - 1);
}
/**
* @param preI 表示先序的开始位置
* @param preJ 表示先序的结束位置
* @param inI 表示中序的开始位置
* @param inJ 表示中序的结束位置
*/
public TreeNode build(int[] preorder, int[] inorder, int preI, int preJ, int inI, int inJ) {
if (preI > preJ || inI > inJ) {
return null;
}
//前序遍历的第一个是root
TreeNode head = new TreeNode(preorder[preI]);
//在中序中找到前序的头节点的位置k,
int k = inI;
int count = 0;
while (inorder[k] != head.val) {
k++;
count++;
}
//将先序,中序,按照count,和k划分
head.left = build(preorder, inorder, preI + 1, preI + count, inI, k - 1);
head.right = build(preorder, inorder, preI + count + 1, preJ, k + 1, inJ);
return head;
}
11、序列化反序列化二叉树
1
/ \
2 3
/ / \
4 5 6
序列化:序列化的时候需要将所有的null节点都序列化进去,并且节点之间要有分隔,比如null节点用#
表示,例子中的序列化结果为:1,2,4,#,#,#,3,5,#,#,6,#,#,
反序列化:
public String serialize(TreeNode root) {
if (root == null) {
return "#,";
}
String cur = root.val + ",";
return cur + serialize(root.left) + serialize(root.right);
}
public TreeNode deserialize(String data) {
if (data == null || data.length() == 0) {
return null;
}
String[] nodes = data.split(",");
return buildTree(nodes, new int[]{0});
}
public TreeNode buildTree(String[] nodes, int[] cur) {
if (cur[0] >= nodes.length) {
return null;
}
String val = nodes[cur[0]++];
if (val.equals("#")) return null;
TreeNode root = new TreeNode(Integer.parseInt(val));
root.left = buildTree(nodes, cur);
root.right = buildTree(nodes, cur);
return root;
}
12、判断平衡二叉树
一个平衡二叉树要求:左子树是平衡二叉树、右子树是平衡二叉树、而且,左右子树的高度差不超过1。
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
return height(root) >= 0;
}
//height = -1 表示已经不平衡了
public int height(TreeNode root) {
if (root == null) {
return 0;
}
int left = height(root.left);
int right = height(root.right);
if (left == -1 || right == -1 || Math.abs(left - right) > 1) {
return -1;
}
return 1 + Math.max(left, right);
}
13、判断二叉搜索树
一个二叉搜索树:左孩子<父节点<右孩子
,BST的中序遍历的结果递增的
思路:在中序遍历的过程中判断是否满足目前节点的值比前一个节点的值大,可以使用普通非递归遍历,或者是Moriss遍历
//普通非递归遍历
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
TreeNode cur = root;
Integer pre = null;
Stack stack = new Stack<>();
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
TreeNode pop = stack.pop();
if (pre != null && pop.val <= pre) { //这是为了避免第一个节点出现最小整数无法判断
return false;
}
System.out.println(pop.val);
pre = pop.val;
cur = pop.right;
}
}
return true;
}
//Moriss遍历
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
TreeNode cur = root;
TreeNode mostRight = null;
Integer pre = null;
while (cur != null) {
if (cur.left == null) {
if (pre != null && cur.val <= pre) {
return false;
}
pre = cur.val;
cur = cur.right;//没有左孩子,第一次访问【也可以理解为第一次第二次同时】
} else {
mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {//有左孩子,第一次访问
mostRight.right = cur;
cur = cur.left;
} else {
if (pre != null && cur.val <= pre) {
return false;
}
pre = cur.val;
mostRight.right = null;//有左孩子,第二次访问
cur = cur.right;
}
}
}
return true;
}
14、判断完全二叉树
完全二叉树:要么是一颗满二叉树,要么叶子节点层是从左到右排布的
思路:1、通过层次遍历来实现
2、如果一个节点只有右孩子 、没有左孩子,直接返回false
3、如果出现了一个节点【只有左孩子,没有右孩子】or【没有左右孩子】,那么说明之后的节点都该是叶子节点了
public boolean isCBT(TreeNode root) {
if (root == null) {
return true;
}
Queue queue = new LinkedList<>();
boolean leafStart = false;
queue.add(root);
while (!queue.isEmpty()) {
TreeNode cur = queue.remove();
TreeNode left = cur.left;
TreeNode right = cur.right;
if (left == null && right != null) {//不符合条件,直接返回false
return false;
}
if (leafStart && (left != null || right != null)) {//开始都是叶子节点了
return false;
}
if (left != null) {
queue.add(left);
}
if (right != null) {
queue.add(right);
}
if ((left != null && right == null) || (left == null && right == null)) {//开始都是叶子节点了
leafStart = true;
}
}
return true;
}
15、完全二叉树的节点个数
要求时间复杂度小于 o(n)
思路:
1、一个完全二叉树的高度可以通过求mostLeft孩子得到
2、一颗二叉树的节点个数为
3、如果一个节点的右孩子的mostLeft的高度能够达到整棵树的高度,那么说明节点的左子树是满的;
4、如果右孩子的mostLeft不能到达整棵树的高度,那么说明右子树是满的。
1
/ \
2 3 3
/ \ / \ / \
4 5 6 7 6 7
/ \ / \ / /
8 9 10 11 12 12
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
int treeHeight = height(root, 1);
return count(root, treeHeight, 1);
}
/**
* @param treeHeight 原始树的高度
* @param cur 当前节点在第几层
*/
public int count(TreeNode root, int treeHeight, int cur) {
if (cur == treeHeight) {
return 1;
}
int right = height(root.right, cur + 1);
if (right == treeHeight) { //右节点的最左能到底,说明左子树是满的
return (1 << (treeHeight - cur)) + count(root.right, treeHeight, cur + 1);
} else { //不然右子树是满的
return (1 << (treeHeight - cur - 1)) + count(root.left, treeHeight, cur + 1);
}
}
//cur表示当前节点在第几层
public int height(TreeNode root, int cur) {
while (root != null) {
cur++;
root = root.left;
}
return cur - 1;
}
16、! 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
思路:1、首先找到给定节点到root节点的这样一条路径,保存在栈中
2、比较两个栈,找到第一个相等的节点
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || p == null || q == null) {
return null;
}
Stack path1 = new Stack<>();
Stack path2 = new Stack<>();
findPath(root, p, path1);
findPath(root, q, path2);
return getLastCom(path1, path2);
}
//寻找指定节点到root的路径,保留在stack中
public boolean findPath(TreeNode root, TreeNode node, Stack path) {
if (root == null) {
return false;
}
path.push(root);
if (root == node) {
return true;
}
if (findPath(root.left, node, path)) {
return true;
}
if (findPath(root.right, node, path)) {
return true;
}
path.pop();
return false;
}
//找到两个路径中第一个相等的节点
public TreeNode getLastCom(Stack pathLeft, Stack pathRight) {
while (pathLeft.size() != pathRight.size()) {
if (pathLeft.size() > pathRight.size()) {
pathLeft.pop();
} else {
pathRight.pop();
}
}
while (!pathRight.isEmpty()) {
TreeNode popleft = pathLeft.pop();
TreeNode popRight = pathRight.pop();
if (popleft == popRight) return popleft;
}
return null;
}
17、树的子结构
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
18、Trie (前缀树)
Trie:前缀树、字典树
通过树的形式,保留一堆字符串的信息,可以实现
1、判断这些字符串中是否有以【"ab"】为前缀的字符串
2、判断这些字符串中是否有【"ab"】这个字符串
3、判断这些字符串中以【"ab"】为前缀的字符串有多少个
思路:
1、就是一个树,把每一个字符串的char保留在树的路径上
2、在节点中增加以该节点为结尾的字符串的数量nodeEnd
3、在节点中增加有多少个字符串精活该节点prefix
class TrieNode {
int prefix = 0;
int nodeEnd = 0;
HashMap nexts = new HashMap<>();
//保留了当前节点下的路径,数据保留在Character,TrieNode是下一个节点
}
class Trie {
TrieNode root;
public Trie() {
this.root = new TrieNode();
}
/**
* Inserts a word into the trie.
*/
public void insert(String word) {
if (word == null || word.length() == 0) {
return;
}
TrieNode cur = root;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
if (!cur.nexts.containsKey(ch)) {
cur.nexts.put(ch, new TrieNode());
}
cur = cur.nexts.get(ch);
cur.prefix++;
}
cur.nodeEnd++;
}
/**
* Returns if the word is in the trie.
*/
public int search(String word) {
if (word == null || word.length() == 0) {
return -1;
}
TrieNode cur = root;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
if(!cur.nexts.containsKey(ch)){
return 0;
}
cur = cur.nexts.get(ch);
}
return cur.nodeEnd;
}
/**
* Returns if there is any word in the trie that starts with the given prefix.
*/
public int startsWith(String prefix) {
if (prefix == null || prefix.length() == 0) {
return -1;
}
TrieNode cur = root;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
if(!cur.nexts.containsKey(ch)){
return 0;
}
cur = cur.nexts.get(ch);
}
return cur.prefix;
}
/**
*delete the infomation of the word in the Trie
*/
public boolean deleteWord(String word){
//首先判断Trie中是否有这个word
if(search(word) <= 0){
return false;
}
TrieNode cur = root;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
if(cur.nexts.get(ch).prefix == 1){
cur.nexts.remove(ch);
return true;
}
cur = cur.nexts.get(ch);
cur.prefix--;
}
cur.nodeEnd--;
return true;
}
}