算法 | 第4章 树与图相关《程序员面试金典》#yyds干货盘点#
Posted 多氯环己烷
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法 | 第4章 树与图相关《程序员面试金典》#yyds干货盘点#相关的知识,希望对你有一定的参考价值。
@[TOC](第4章 树与图相关《程序员面试金典》)
前言
本系列笔记主要记录笔者刷《程序员面试金典》算法的一些想法与经验总结,按专题分类,主要由两部分构成:经验值点和经典题目。其中重点放在经典题目上;
本章有10题,标号到12,没有第7和第11题;
0. *经验总结
0.1 程序员面试金典 P85
- 树的基本组成单位是节点node,节点可以封装左右指针、父节点指针等;
- 可以使用一个名为Tree的类来封装节点,但在面试中不常用;如果使用Tree类能使代码简单或完善,可以使用Tree类;
- 注意区分以下概念,以及一些树的基本特点与遍历特点《详情见第0.2点 各种树的特点》:
- 树与二叉树;
- 二叉树和二叉搜索树(又称二叉排序树);
- 平衡与不平衡;
- 完整二叉树;
- 满二叉树;
- 完美二叉树;
- 二叉堆(小顶堆和大顶堆)
- 单次查找树(前序树)
- 顺序存储二叉树;
- 线索化二叉树;
- 霍夫曼树;
- B树、B+树和B*树;
0.2 各种树的特点
1. 二叉搜索树(二叉排序树)
- 全部左边子孙节点 <= n <= 全部右边子孙节点;
- 碰到二叉树时,很多面试者会假定面试官问的是二叉搜索树,此时要务必问清是否的二叉搜索树;
2. 平衡二叉树
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树;
- 树是否平衡需要跟面试官确认;
- 思考方向可以有:“平衡”树实际上多半意味着“不是非常不平衡的树”。其平衡性足以保证执行insert和find操作可以在O(log n)的时间复杂度内完成,但不一定是严格意义上的平衡树;
- 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等;
3. 完整二叉树
- 除最后一层,其它层被填满,并且最后一层节点从左到右填充;
- 中序遍历结果按顺序排列;
4. 满二叉树
- 每个节点都有0个或两个子节点,不存在只有一个子节点的节点;
5. 完满二叉树
- 完满二叉树既是完整二叉树,又是满二叉树;
- 正好有2^k^-1个节点;
6. 二叉堆(小顶堆和大顶堆)
- 一个二叉堆是一个完整二叉树,每个节点值小于其左右子节点;
- 向最小堆插入元素,总是从最底部、最右边节点开始,通过与其祖先节点交换来向上传递较小值;
- 删除最小堆最小元素,将堆中最后一个元素放在顶节点,通过与子节点交换来向下传递较大值;
- 上述两个算法时间复杂度为O(log n);
7. 单次查找树(前序树)
- *节点(有时称空节点)代表一个完整单次;
- 相比散列表可以识别字符串是否是任何有效单次的前缀,可以在O(K)的时间复杂度内检查字符串是否为有效前缀;
- 涉及一组有效单词的问题可以使用单次查找树进行优化;
8. 顺序存储二叉树
- 从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组;
- 顺序二叉树通常只考虑完全二叉树;
- 第n个元素的左子节点为 2*n+1;
- 第n个元素的右子节点为 2*n+2;
- 第n个元素的父节点为 (n-1)/2;
- n : 表示二叉树中的第几个元素(按0开始编号如图所示);
9. 线索化二叉树
- n个结点的二叉链表中含有 2n-(n-1)=n+1 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索");
10. 霍夫曼树
- 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree);
11. B树、B+树和B*树
0.3 树的三种遍历方式
中序遍历:
void inOrderTraversal(TreeNode node)
if(node != null)
inOrderTraversal(node.left);
visit(node);
inOrderTraversal(node.right);
前序遍历:
void preOrderTraversal(TreeNode node)
if(node != null)
visit(node);
preOrderTraversal(node.left);
preOrderTraversal(node.right);
后序遍历:
void postOrderTraversal(TreeNode node)
if(node != null)
postOrderTraversal(node.left);
postOrderTraversal(node.right);
visit(node);
0.4 图的表示形式与搜索
图的表示方式:
- 邻接链表法;
- 邻接矩阵法;
- 邻接矩阵法使用BFS搜索,效率会比较低,因为需要迭代所有结点以便找到相邻节点;
图的搜索:
- 深度优先搜索(DFS);
- 访问图中所有节点,或者访问最少节点直至找到某节点,DFS一般比较简单;
- 广度优先搜索(BFS);
- 如果想找到两个节点的最短路径(或任意路径),BFS一般比较适宜;
- BFS是通过队列实现;
- 双向搜索;
- 双向搜索用于查找起始节点和目的节点间的最短路径;
- 本质上是从起始节点和目的节点同时开始的两个广度优先搜索;
- 当两个搜索相遇时,即找到一条路径
深度优先搜索(DFS):
- 前序和树遍历的其他形式都是一种DFS;
void search(Node root)
if(root == null)
return;
visit(root);
root.visited = true;
for each(Node n in root.adjacent)
if(n.visited == false)
search(n);
广度优先搜索(BFS):
void search(Node root)
Queue queue = new Queue();
root.marked = true;
queue.enqueue(root); //加入队尾
while(!queue.isEmpty())
Node r = queue.dequeue(); //从队列头部删除
visit(r);
for each(Node n in r.adjacent)
if(n.mark == false)
n.marked = true;
queue.enqueue(n);
1. 节点间通路 [medium]
1.1 考虑点
- 询问面试官重复的边是否是双向边,本题中不是。笔者以为是双向边,第五种解法给出思路;
- 可以跟面试官讨论广度优先搜索和深度优先搜索的利弊:
- 广度优先搜索:适合查找最短路径;
- 深度优先搜索:实现比较简单,递归即可;在访问邻近节点之前,可能会深度遍历其中一个邻近节点;
1.2 解法
1.2.1 广度优先遍历法(优)
public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target)
Queue<Integer> queue = new LinkedList<>();
boolean[] isVisit = new boolean[n];
isVisit[start] = true;
queue.offer(start);
while( !queue.isEmpty() )
int thisNode = queue.poll();
int toNode;
for(int i = 0; i < graph.length; i++)
if(graph[i][0] == thisNode)
toNode = graph[i][1];
if(toNode == target)
return true;
if(!isVisit[toNode])
isVisit[toNode] = true;
queue.offer(toNode);
return false;
- 执行时间:91.21%;内存消耗:92.42%;
- 适用于n较少的时候,当n过大会超时;
1.2.2 深度优先遍历法
private boolean[] visited = null;
public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target)
this.visited = new boolean[graph.length];
return helper(graph, start, target);
private boolean helper(int[][] graph, int start, int target)
for (int i = 0; i < graph.length; ++i)
// 确保当前路径未被访问(该判断主要是为了防止图中自环出现死循环的情况)
if (!visited[i])
// 若当前路径起点与终点相符,则直接返回结果
if (graph[i][0] == start && graph[i][1] == target)
return true;
// 设置访问标志
visited[i] = true;
// DFS关键代码,思路:同时逐渐压缩搜索区间
if (graph[i][1] == target && helper(graph, start, graph[i][0]))
return true;
// 清除访问标志
visited[i] = false;
return false;
- 执行时间:81.17%;内存消耗:97.41%;
- 首先设置访问状态数组
- 使用DFS「深度优先搜索」进行递归搜索,逐渐压缩搜索区间,直至最终找到start与target在同一个路径内,则说明查找成功;
1.2.3 邻接矩阵的幂的性质
public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target)
int[][] adjacentMatrix = new int[n][n];
for (int[] edge : graph)
adjacentMatrix[edge[0]][edge[1]] = 1;
for (int i = 1; i <= n; i++)
int[][] matrix = matrixPow(adjacentMatrix, i);
if (matrix[start][target] != 0)
return true;
return false;
public int[][] matrixPow(int[][] matrix, int n)
int size = matrix.length;
int[][] res = new int[size][];
for (int i = 0; i < size; i++)
res[i] = Arrays.copyOf(matrix[i], size);
for (int i = 1; i < n; i++)
for (int row = 0; row < size; row++)
int[] tmp = new int[size];
for (int col = 0; col < size; col++)
for (int j = 0; j < size; j++)
tmp[col] += res[row][j] * matrix[j][col];
res[row] = Arrays.copyOf(tmp, size);
return res;
- 在本测试用例中超时;
- 用到邻接矩阵的幂的性质;
1.2.4 当重复边为双向边时
public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target)
int[] count = new int[n*(n-1)/2];
for(int i = 0; i < graph.length; i++)
int mark = (2*n-1-graph[i][0])*graph[i][0]/2-1+graph[i][1]-graph[i][0];
count[mark]++;
if(count[mark] == 2)
int cache = graph[i][0];
graph[i][0] = graph[i][1];
graph[i][1] = cache;
Queue<Integer> queue = new LinkedList<>();
boolean[] isVisit = new boolean[n*(n-1)/2];
queue.offer(start);
while( !queue.isEmpty() )
int thisNode = queue.poll();
int toNode;
for(int i = 0; i < graph.length; i++)
int mark = (2*n-1-graph[i][0])*graph[i][0]/2-1+graph[i][1]-graph[i][0];
if(graph[i][0] == thisNode && !isVisit[mark])
toNode = graph[i][1];
if(toNode == target)
return true;
queue.offer(toNode);
return false;
- 相比前面做法增加了个方向倒转,也就是把重复边的方向倒转;
2. 最小高度树 [easy]
2.1 考虑点
- 要创建最小生成树,就必须让左右子树的节点数量尽可能接近;
2.2 解法
2.2.1 递归法(优)
public TreeNode sortedArrayToBST(int[] nums)
if(nums.length == 0)
return null;
int l = 0;
int r = nums.length-1;
return recur(l, r, nums);
public TreeNode recur(int l, int r, int[] nums)
if(l > r)
return null;
int mid = (l+r)/2;
int headVal = nums[mid];
TreeNode head = new TreeNode(headVal);
head.left = recur(l, mid-1, nums);
head.right = recur(mid+1, r, nums);
return head;
- 执行时间:100.00%;内存消耗:53.37%;
- 时间复杂度:O(n)。数组中的元素都使用1次,时间复杂度为O(n);
- 空间复杂度:O(log n )。递归使用栈辅助空间,空间复杂度O(log n );
- 注意递归结束条件,没有等于是因为递归里有对mid增减进行操作;
- 注意递归的参数
l
和r
,表示需要递归的范围,不包括mid头结点;
2.2.2 BFS广度优先遍历
public TreeNode sortedArrayToBST(int[] num)
if (num.length == 0)
return null;
Queue<int[]> rangeQueue = new LinkedList<>();
Queue<TreeNode> nodeQueue = new LinkedList<>();
int lo = 0;
int hi = num.length - 1;
int mid = (lo + hi) >> 1;
TreeNode node = new TreeNode(num[mid]);
rangeQueue.add(new int[]lo, mid - 1);
rangeQueue.add(new int[]mid + 1, hi);
nodeQueue.add(node);
nodeQueue.add(node);
while (!rangeQueue.isEmpty())
int[] range = rangeQueue.poll();
TreeNode currentNode = nodeQueue.poll();
lo = range[0];
hi = range[1];
if (lo > hi)
continue;
mid = (lo + hi) >> 1;
int midValue = num[mid];
TreeNode newNode = new TreeNode(midValue);
if (midValue > currentNode.val)
currentNode.right = newNode;
else
currentNode.left = newNode;
if (lo < hi)
rangeQueue.add(new int[]lo, mid - 1);
rangeQueue.add(new int[]mid + 1, hi);
nodeQueue.add(newNode);
nodeQueue.add(newNode);
return node;
- 执行时间:100.00%;内存消耗:5.01%;
- 把数组不停的分为两部分,保存在队列中,然后不停的出队,创建结点;
3. 特定深度节点链表 [medium]
3.1 考虑点
- 不需要逐一遍历每一层,可以用任意方式遍历整棵树,只需要记住节点位于哪一层即可;
3.2 解法
3.2.1 使用栈暴力法
public ListNode[] listOfDepth(TreeNode tree)
if(tree == null)
return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(tree);
int floor = 1; //第x-1层节点个数
List<ListNode> listArr = new ArrayList<>();
//当队列不为空
while(!queue.isEmpty())
//按层处理
int count = 0; //count用来储存第x层节点个数
ListNode head = null;
ListNode cur = null;
for(int i = 0; i < floor; i++)
TreeNode node = queue.poll();
//创建链表
if(i == 0)
head = new ListNode(node.val);
cur = head;
else
ListNode nodeL = new ListNode(node.val);
cur.next = nodeL;
cur = nodeL;
//加入队列
if( node.left != null)
count++;
queue.offer(node.left);
if(node.right != null)
count++;
queue.offer(node.right);
listArr.add(head);
floor = count; //替换floor
ListNode[] list = new ListNode[listArr.size()];
for(int i = 0; i < listArr.size(); i++)
list[i] = listArr.get(i);
return list;
- 执行时间:89.59%;内存消耗:75.08%;
- 需要注意几个细节:
ListNode cur = null
:需要对cur初始化;floor = count
:记得要替换floor;
- 由于事先不知道数组个数,先用一个ArrayList装起来,然后再转换成数组;
3.2.2 使用栈暴力法(代码优化缩短)
public ListNode[] listOfDepth(TreeNode tree)
if(tree == null)
return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(tree);
List<ListNode> listArr = new ArrayList<>();
//当队列不为空
while(!queue.isEmpty())
//按层处理
int size = queue.size();
ListNode head = new ListNode(0);
ListNode cur = head;
for(int i = 0; i < size; i++)
TreeNode node = queue.poll();
//创建链表
cur.next = new ListNode(node.val);
//加入队列
if( node.left != null)
queue.offer(node.left);
if(node.right != null)
queue.offer(node.right);
cur = cur.next;
listArr.add(head.next);
return listArr.toArray(new ListNode[] );
- 执行时间:89.59%;内存消耗:25.97%;
- 思路同上,更多调用java的api简化代码;
3.2.3 递归法(优)
public ListNode[] listOfDepth(TreeNode tree)
List<ListNode> list = new ArrayList<>();
dfs(list, tree, 1);
ListNode[] arr = new ListNode[list.size()];
for (int i = 0; i < arr.length; i++)
arr[i] = list.get(i);
return arr;
private void dfs(List<ListNode> list, TreeNode node, int deep)
if (node == null)
return;
if (deep > list.size())
list.add(new ListNode(node.val));
else
ListNode n = list.get(deep - 1);
while (n.next != null)
n = n.next;
n.next = new ListNode(node.val);
dfs(list, node.left, deep + 1);
dfs(list, node.right, deep + 1);
- 执行时间:100.00%;内存消耗:96.63%;
- 定义树深度为deep,同一个深度的保存到同一个ListNode;
4. 检查平衡性 [easy]
4.1 考虑点
- 其实,只需要一个checkHeight函数即可,它既可以计算高度,也可以平衡检查。可以使用整数返回值表示两者;
4.2 解法
4.2.1 自顶向下的递归
public boolean isBalanced(TreeNode root)
if(root == null)
return true;
int deep = 1;
int rootLeftDeep = findDeep(root.left, deep);
int rootRightDeep = findDeep(root.right, deep);
if(rootLeftDeep == -1 || rootRightDeep == -1)
return false;
int result = Math.abs(rootLeftDeep - rootRightDeep);
return result<2;
public int findDeep(TreeNode node, int deep)
if(node == null)
return deep-1;
if(node.left == null && node.right == null)
return deep;
deep++;
int leftDeep = findDeep(node.left,deep);
int rightDeep = findDeep(node.right,deep);
int deepDiff = Math.abs(leftDeep-rightDeep);
return deepDiff>1 ? -1 : Math.max(leftDeep,rightDeep);
- 执行时间:88.57%;内存消耗:20.48%;
- 方法虽然可行,但不高效,Math.abs()方法会被反复调用计算同一个节点的高度;
4.2.2 自顶向下的递归
public boolean isBalanced(TreeNode root)
if (root == null)
return true;
else
return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
public int height(TreeNode root)
if (root == null)
return 0;
else
return Math.max(height(root.left), height(root.right)) + 1;
- 执行时间:88.57%;内存消耗:74.16%;
-
时间复杂度:O(n^2^),其中 n 是二叉树中的节点个数。最坏情况下,二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是 O(n)。对于节点 p,如果它的高度是 d,则 height(p) 最多会被调用 d 次(即遍历到它的每一个祖先节点时)。对于平均的情况,一棵树的高度 h 满足 O(h)=O(logn),因为 d≤h,所以总时间复杂度为 O(nlogn)。对于最坏的情况,二叉树形成链式结构,高度为 O(n),此时总时间复杂度为 O(n^2^);
- 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n;
4.2.3 自底向上的递归(优)
public boolean isBalanced(TreeNode root)
return height(root) >= 0;
public int height(TreeNode root)
if (root == null)
return 0;
int leftHeight = height(root.left);
int rightHeight = height(root.right);
if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1)
return -1;
else
return Math.max(leftHeight, rightHeight) + 1;
- 执行时间:88.57%;内存消耗:96.72%;
- 时间复杂度:O(n),其中 n 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n);
- 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n;
5. 合法二叉搜索树 [medium]
5.1 考虑点
- 有两种思路:
- 一是利用中序遍历;
- 二是建立在 left <current<right 这项特性上;
5.2 解法
5.2.1 递归法
class Solution
Stack<Integer> stack = new Stack<>();
boolean isFlag = false;
public boolean isValidBST(TreeNode root)
if(root == null)
return true;
inOrderTraversal(root);
return !isFlag;
public void inOrderTraversal(TreeNode node)
if(isFlag)
return;
if(node == null)
return;
inOrderTraversal(node.left);
if(stack.isEmpty())
stack.push(node.val);
else
if(stack.peek() >= node.val)
isFlag = true;
return;
else
stack.push(node.val);
inOrderTraversal(node.right);
- 执行时间:32.30%;内存消耗:91.41%;
5.2.2 中序遍历非递归
public boolean isValidBST(TreeNode root)
Deque<TreeNode> stack = new LinkedList<TreeNode>();
double inorder = -Double.MAX_VALUE;
while (!stack.isEmpty() || root != null)
while (root != null)
stack.push(root);
root = root.left;
root = stack.pop();
// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if (root.val <= inorder)
return false;
inorder = root.val;
root = root.right;
return true;
- 执行时间:24.37%;内存消耗:98.61%;
- 时间复杂度 : O(n),其中 n 为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n);
- 空间复杂度 : O(n),其中 n 为二叉树的节点个数。栈最多存储 n 个节点,因此需要额外的 O(n) 的空间;
5.2.3 中序遍历递归(优)
//前一个结点,全局的
TreeNode prev;
public boolean isValidBST(TreeNode root)
if (root == null)
return true;
//访问左子树
if (!isValidBST(root.left))
return false;
//访问当前节点:如果当前节点小于等于中序遍历的前一个节点直接返回false。
if (prev != null && prev.val >= root.val)
return false;
prev = root;
//访问右子树
if (!isValidBST(root.right))
return false;
return true;
- 执行时间:100.00%;内存消耗:65.74%;
- 中序遍历时,判断当前节点是否大于中序遍历的前一个节点,也就是判断是否有序,如果不大于直接返回 false
6. 后继者 [medium]
6.1 考虑点
- 因为是二叉搜索树,可以很方便找到节点,再根据是否有右子树分类判断;
6.2 解法
6.2.1 中序遍历递归法
class Solution
Stack<TreeNode> stack = new Stack<>();
TreeNode findNode = null;
public TreeNode inorderSuccessor(TreeNode root, TreeNode p)
if(root == null || p == null)
return null;
inOrderTravarsal(root,p);
return findNode!=null ? findNode : null;
public void inOrderTravarsal(TreeNode node, TreeNode p)
if(findNode != null)
return;
if(node == null)
return;
inOrderTravarsal(node.left, p);
if(stack.isEmpty())
stack.push(node);
else
if(p.equals(stack.peek()))
findNode = node;
stack.pop(); //忘记pop
return;
else
stack.push(node);
inOrderTravarsal(node.right, p);
- 执行时间:72.07%;内存消耗:9.51%;
6.2.2 中序遍历非递归法(优)
public TreeNode inorderSuccessor(TreeNode root, TreeNode p)
TreeNode pre = null;
while(root.val!=p.val)
//右边
if(p.val > root.val)
root = root.right;
//左边
else
pre = root;
root = root.left;
//假如没有右子树
if(root.right==null)
return pre;
else
root = root.right;
while(root.left!=null)
root = root.left;
return root;
- 执行时间:100.00%;内存消耗:100.00%;
- 找到节点后,如果右子树存在,那就是右子树最左边的节点。如果右子树不存在,表示已经访问n层子树,需要回到n的父节点q;如果n在q的左边,那就是q。反之需要往上访问,直到找到还未完全遍历的节点x;
8. 首个共同祖先 [medium]
8.1 考虑点
- 如果是二叉搜索树,可以看看两条路径在哪开始分叉;
8.2 解法
8.2.1 深度优先遍历
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)
if(root == null)
return null;
else if(root.equals(p))
return p;
else if(root.equals(q))
return q;
TreeNode leftNode = lowestCommonAncestor(root.left,p,q);
TreeNode rightNode = lowestCommonAncestor(root.right,p,q);
if(leftNode == null && rightNode == null)
return null;
if(leftNode != null && rightNode == null)
if(leftNode.equals(q))
return q;
else if(leftNode.equals(p))
return p;
else
return leftNode;
if(leftNode == null && rightNode != null)
if(rightNode.equals(q))
return q;
else if(rightNode.equals(p))
return p;
else
return rightNode;
//注意非空校验
if((leftNode.equals(p) && rightNode.equals(q)) || (leftNode.equals(q) && rightNode.equals(p)))
return root;
return null;
- 执行时间:52.31%;内存消耗:5.19%;
8.2.2 深度优先遍历的简便写法(优)
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)
// 到底了还没找到,返回 null
if (root == null)
return null;
// 如果找到了 p 或 q,返回它
if (root == p || root == q)
return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 如果 left 和 right 都记录了找到的节点,那么肯定是一个记录了 p ,另一个记录了 q
// 它们分别在以 root 为根的左右子树中,所以 root 就是它们的最近公共祖先
if (left != null && right != null)
return root;
// 由于节点 p,q 一定在二叉树中,left和right不会同时为null
// 若 left != null && right == null,说明在左子树中找到 p 或 q,而在右子树找不到 p 或 q,则剩下一个也在左子树
// 所以 left 就是最近公共祖先
// 另一种情况同理
return (left != null) ? left : right;
- 执行时间:100.00%;内存消耗:91.55%;
9. 二叉搜索树序列 [hard]
9.1 考虑点
- 数组的第一个数必须为顶节点;
- 与左右节点的插入顺序无关紧要,但子节点的添加一定要在父节点之后;
9.2 解法
9.2.1 回溯法+广度优先遍历
private List<List<Integer>> ans;
public List<List<Integer>> BSTSequences(TreeNode root)
ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
// 如果 root==null 返回 [[]]
if (root == null)
ans.add(path);
return ans;
List<TreeNode> queue = new LinkedList<>();
queue.add(root);
// 开始进行回溯
bfs(queue, path);
return ans;
// 回溯法+广度优先遍历
private void bfs(List<TreeNode> queue, List<Integer> path)
// queue 为空说明遍历完了,可以返回了
if (queue.isEmpty())
ans.add(new ArrayList<>(path));
return;
// 将 queue 拷贝一份,用于稍后回溯
List<TreeNode> copy = new ArrayList<>(queue);
// 对 queue 进行循环,每循环考虑 “是否 「将当前 cur 节点从 queue 中取出并将其左右子
// 节点加入 queue ,然后将 cur.val 加入到 path 末尾」 ” 的情况进行回溯
for (int i = 0; i < queue.size(); i++)
TreeNode cur = queue.get(i);
path.add(cur.val);
queue.remove(i);
// 将左右子节点加入队列
if (cur.left != null) queue.add(cur.left);
if (cur.right != null) queue.add(cur.right);
bfs(queue, path);
// 恢复 path 和 queue ,进行回溯
path.remove(path.size() - 1);
queue = new ArrayList<>(copy);
- 执行时间:90.30%;内存消耗:93.98%;
- 对于这种找出所有情况的题目,回溯法是最容易想到的方法之一了,这道题也可以用回溯法,可以发现刚才选元素的过程和层序遍历的过程其实是一致的:
- 最开始 queue 中只有 12 ,只能选12,将 12 出队并将它的两个子节点入队,得到 [12];
选了12之后 queue 中剩下 5、19 ,就从 5 和 19 中选一个,得到 [12,5],[12,19]; - 如果选了 5 ,将 5 出队并将它的两个子节点入队,那么此时 queue 中剩下 19、2、9,得到 [12,5,2],[12,5,9],[12,5,19];
- 如果选了 19 ,将 19 出队并将它的子节点入队,那么此时 queue 中剩下 5、15,得到 [12,19,5],[12,19,15];
- 后续同理;
- 最开始 queue 中只有 12 ,只能选12,将 12 出队并将它的两个子节点入队,得到 [12];
10. 检查子树 [medium]
10.1 考虑点
- 这里的“万”是干扰的;
10.2 解法
10.2.1 递归法(优)
boolean isFound =false;
public boolean checkSubTree(TreeNode t1, TreeNode t2)
if(t1 == null)
return false;
if(t1.val == t2.val)
isFound = true;
return true;
else
isFound = false;
boolean left;
boolean right;
if(isFound)
left = checkSubTree(t1.left, t2.left);
right = checkSubTree(t1.right, t2.right);
return left && right;
else
left = checkSubTree(t1.left, t2);
right = checkSubTree(t1.right, t2);
if(left)
return checkSubTree(t1.left, t2);
if(right)
return checkSubTree(t1.right, t2);
return false;
- 执行时间:100.00%;内存消耗:46.52%;
12. 求和路径 [medium]
12.1 考虑点
- 可以使用散列表优化算法,下面解法没给出;
12.2 解法
12.2.1 暴力法(优)
class Solution
int res = 0;
public int pathSum(TreeNode root, int sum)
int dep = depth(root);
int[] paths = new int[dep];
pathSum(root, sum, 0, paths);
return res;
public void pathSum(TreeNode root, int sum, int level, int[] paths)
if (root == null)
return;
paths[level] = root.val;
int t = 0;
for (int i = level; i >= 0; i--)
t += paths[i];
if (t == sum)
res += 1;
pathSum(root.left, sum, level + 1, paths);
pathSum(root.right, sum, level + 1, paths);
public int depth(TreeNode root)
if (root == null)
return 0;
return Math.max(depth(root.left), depth(root.right)) + 1;
- 执行时间:100.00%;内存消耗:69.08%;
12.2.2 回溯法
private int res = 0;
public int pathSum(TreeNode root, int sum)
if(root == null) return res;
helper(root, sum);
pathSum(root.left, sum);
pathSum(root.right, sum);
return res;
private void helper(TreeNode node, int sum)
if(node == null) return;
sum -= node.val;
if(sum == 0)
res ++;
helper(node.left, sum);
helper(node.right, sum);
sum += node.val;
- 执行时间:58.34%;内存消耗:10.27%;
最后
::: hljs-center
新人制作,如有错误,欢迎指出,感激不尽!
:::
::: hljs-center
欢迎关注公众号,会分享一些更日常的东西!
:::
::: hljs-center
如需转载,请标注出处!
:::
::: hljs-center
:::
以上是关于算法 | 第4章 树与图相关《程序员面试金典》#yyds干货盘点#的主要内容,如果未能解决你的问题,请参考以下文章
算法 | 第5章 位操作相关《程序员面试金典》#yyds干货盘点#
算法 | 第2章 链表相关《程序员面试金典》#yyds干货盘点#
算法 | 第1章 数组与字符串相关《程序员面试金典》#yyds干货盘点#