目錄
- 前言
- 0. *經驗總結
- 0.1 程式員面試金典 P85
- 0.2 各種樹的特點
- 1. 二叉搜索樹(二叉排序樹)
- 2. 平衡二叉樹
- 3. 完整二叉樹
- 4. 滿二叉樹
- 5. 完滿二叉樹
- 6. 二叉堆(小頂堆和大頂堆)
- 7. 單次查找樹(前序樹)
- 8. 順序存盤二叉樹
- 9. 線索化二叉樹
- 10. 霍夫曼樹
- 11. B樹、B+樹和B*樹
- 0.3 樹的三種遍歷方式
- 0.4 圖的表示形式與搜索
- 1. 節點間通路 [medium]
- 1.1 考慮點
- 1.2 解法
- 1.2.1 廣度優先遍歷法(優)
- 1.2.2 深度優先遍歷法
- 1.2.3 鄰接矩陣的冪的性質
- 1.2.4 當重復邊為雙向邊時
- 2. 最小高度樹 [easy]
- 2.1 考慮點
- 2.2 解法
- 2.2.1 遞回法(優)
- 2.2.2 BFS廣度優先遍歷
- 3. 特定深度節點鏈表 [medium]
- 3.1 考慮點
- 3.2 解法
- 3.2.1 使用堆疊暴力法
- 3.2.2 使用堆疊暴力法(代碼優化縮短)
- 3.2.3 遞回法(優)
- 4. 檢查平衡性 [easy]
- 4.1 考慮點
- 4.2 解法
- 4.2.1 自頂向下的遞回
- 4.2.2 自頂向下的遞回
- 4.2.3 自底向上的遞回(優)
- 5. 合法二叉搜索樹 [medium]
- 5.1 考慮點
- 5.2 解法
- 5.2.1 遞回法
- 5.2.2 中序遍歷非遞回
- 5.2.3 中序遍歷遞回(優)
- 6. 后繼者 [medium]
- 6.1 考慮點
- 6.2 解法
- 6.2.1 中序遍歷遞回法
- 6.2.2 中序遍歷非遞回法(優)
- 8. 首個共同祖先 [medium]
- 8.1 考慮點
- 8.2 解法
- 8.2.1 深度優先遍歷
- 8.2.2 深度優先遍歷的簡便寫法(優)
- 9. 二叉搜索樹序列 [hard]
- 9.1 考慮點
- 9.2 解法
- 9.2.1 回溯法+廣度優先遍歷
- 10. 檢查子樹 [medium]
- 10.1 考慮點
- 10.2 解法
- 10.2.1 遞回法(優)
- 12. 求和路徑 [medium]
- 12.1 考慮點
- 12.2 解法
- 12.2.1 暴力法(優)
- 12.2.2 回溯法
- 最后
前言
本系列筆記主要記錄筆者刷《程式員面試金典》演算法的一些想法與經驗總結,按專題分類,主要由兩部分構成:經驗值點和經典題目,其中重點放在經典題目上;
本章有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. 完滿二叉樹

- 完滿二叉樹既是完整二叉樹,又是滿二叉樹;
- 正好有2k-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 = https://www.cnblogs.com/dlhjw/p/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(n2),其中 n 是二叉樹中的節點個數,最壞情況下,二叉樹是滿二叉樹,需要遍歷二叉樹中的所有節點,時間復雜度是 O(n),對于節點 p,如果它的高度是 d,則 height(p) 最多會被呼叫 d 次(即遍歷到它的每一個祖先節點時),對于平均的情況,一棵樹的高度 h 滿足 O(h)=O(logn),因為 d≤h,所以總時間復雜度為 O(nlogn),對于最壞的情況,二叉樹形成鏈式結構,高度為 O(n),此時總時間復雜度為 O(n2);
-
空間復雜度: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%;
最后

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/336149.html
標籤:其他
上一篇:求求你了,用Docker吧
