陣列系列
力扣資料結構之陣列-00-概覽
力扣.53 最大子陣列和 maximum-subarray
力扣.128 最長連續序列 longest-consecutive-sequence
力扣.1 兩數之和 N 種解法 two-sum
力扣.167 兩數之和 II two-sum-ii
力扣.170 兩數之和 III two-sum-iii
力扣.653 兩數之和 IV two-sum-IV
力扣.015 三數之和 three-sum
力扣.016 最接近的三數之和 three-sum-closest
力扣.259 較小的三數之和 three-sum-smaller
題目
給定一個二叉搜尋樹 root 和一個目標結果 k,如果二叉搜尋樹中存在兩個元素且它們的和等於給定的目標結果,則返回 true。
示例 1:
5
/ \
3 6
/ \ \
2 4 7
輸入: root = [5,3,6,2,4,null,7], k = 9
輸出: true
示例 2:
5
/ \
3 6
/ \ \
2 4 7
輸入: root = [5,3,6,2,4,null,7], k = 28
輸出: false
提示:
二叉樹的節點個數的範圍是 [1, 10^4].
-10^4 <= Node.val <= 10^4
題目資料保證,輸入的 root 是一棵 有效 的二叉搜尋樹
-10^5 <= k <= 10^5
思路
這種二叉樹的題目,我們可以分為兩步:
1)二叉樹遍歷轉換為陣列
2)陣列,然後複用前面 T001/T167 的解法。
常見演算法
樹的遍歷
面試演算法:二叉樹的前序/中序/後序/層序遍歷方式彙總 preorder/Inorder/postorder/levelorder
樹的遍歷有多種方式:前序 中序 後序 層序
找到符合的結果
- 暴力
2)藉助 Hash
- 排序+二分
4)雙指標==》針對有序陣列
在這個場景裡面,最簡單好用的應該是 Hash 的方式。其他的我們就不再演示。
本文主要在複習一下樹的遍歷,太久沒做了,忘記了。
樹的遍歷回顧
在二叉樹中,前序遍歷、中序遍歷和後序遍歷是三種常見的遍歷方式,遞迴實現是最直觀和常用的方式。
下面是這三種遍歷的基本概念和 Java 遞迴實現的程式碼示例。
1. 前序遍歷 (Preorder Traversal)
遍歷順序: 根節點 -> 左子樹 -> 右子樹
遞迴實現:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
public class BinaryTree {
public void preorderTraversal(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " "); // 先訪問根節點
preorderTraversal(root.left); // 遍歷左子樹
preorderTraversal(root.right); // 遍歷右子樹
}
}
2. 中序遍歷 (Inorder Traversal)
遍歷順序: 左子樹 -> 根節點 -> 右子樹
遞迴實現:
public class BinaryTree {
public void inorderTraversal(TreeNode root) {
if (root == null) {
return;
}
inorderTraversal(root.left); // 遍歷左子樹
System.out.print(root.val + " "); // 訪問根節點
inorderTraversal(root.right); // 遍歷右子樹
}
}
3. 後序遍歷 (Postorder Traversal)
遍歷順序: 左子樹 -> 右子樹 -> 根節點
遞迴實現:
public class BinaryTree {
public void postorderTraversal(TreeNode root) {
if (root == null) {
return;
}
postorderTraversal(root.left); // 遍歷左子樹
postorderTraversal(root.right); // 遍歷右子樹
System.out.print(root.val + " "); // 訪問根節點
}
}
總結
-
前序遍歷:先訪問根節點,再遍歷左子樹和右子樹。
-
中序遍歷:先遍歷左子樹,再訪問根節點,最後遍歷右子樹。
-
後序遍歷:先遍歷左子樹,再遍歷右子樹,最後訪問根節點。
這些遍歷方式的遞迴實現思路基本相同,區別在於訪問根節點的時機不同。在實際應用中,可以根據需求選擇不同的遍歷方式。
前中後是以 root 的節點為主視角,看什麼時候被訪問。
v1-前序遍歷
思路
我們可以把整個陣列完全構建出來,然後複用以前的解法。
當然這樣會比較慢,我們可以在遍歷的時候找到對應的結果。
傳遞的值更新問題,我們用 resFlag 陣列來記錄最後的結果。
實現
class Solution {
public boolean findTarget(TreeNode root, int k) {
// 構建結果列表
Set<Integer> numSet = new HashSet<>();
int[] resFlag = new int[]{1};
resFlag[0] = 0;
preOrderTravel(numSet, root, k, resFlag);
return resFlag[0] != 0;
}
private void preOrderTravel(Set<Integer> numSet,
TreeNode root,
int k,
int[] resFlag) {
if(root == null || resFlag[0] != 0) {
return;
}
// 符合
int value = root.val;
if(numSet.contains(k - value)) {
resFlag[0] = 1;
return;
}
numSet.add(value);
preOrderTravel(numSet, root.left, k, resFlag);
preOrderTravel(numSet, root.right, k, resFlag);
}
}
效果
3ms 79.82
v2-中序遍歷
思路
採用中序遍歷,其他保持不變。
程式碼
public boolean findTarget(TreeNode root, int k) {
// 構建結果列表
Set<Integer> numSet = new HashSet<>();
int[] resFlag = new int[]{1};
resFlag[0] = 0;
inOrderTravel(numSet, root, k, resFlag);
return resFlag[0] != 0;
}
private void inOrderTravel(Set<Integer> numSet,
TreeNode root,
int k,
int[] resFlag) {
if(root == null || resFlag[0] != 0) {
return;
}
inOrderTravel(numSet, root.left, k, resFlag);
// 符合
int value = root.val;
if(numSet.contains(k - value)) {
resFlag[0] = 1;
return;
}
numSet.add(value);
inOrderTravel(numSet, root.right, k, resFlag);
}
效果
3ms 79.82%
v3-後序遍歷
思路
很簡單,調整為後續遍歷即可。
實現
public boolean findTarget(TreeNode root, int k) {
// 構建結果列表
Set<Integer> numSet = new HashSet<>();
int[] resFlag = new int[]{1};
resFlag[0] = 0;
postOrderTravel(numSet, root, k, resFlag);
return resFlag[0] != 0;
}
private void postOrderTravel(Set<Integer> numSet,
TreeNode root,
int k,
int[] resFlag) {
if(root == null || resFlag[0] != 0) {
return;
}
postOrderTravel(numSet, root.left, k, resFlag);
postOrderTravel(numSet, root.right, k, resFlag);
// 符合
int value = root.val;
if(numSet.contains(k - value)) {
resFlag[0] = 1;
return;
}
numSet.add(value);
}
效果
4ms 29.82%
估計是伺服器波動,也和測試用例有一定的關係。
v4-層序遍歷
層序遍歷
層序遍歷(Level Order Traversal)是按層級順序從上到下、從左到右遍歷二叉樹。
與前序、中序、後序不同,層序遍歷通常是使用廣度優先搜尋(BFS)實現的,常見的做法是使用佇列來輔助遍歷。
層序遍歷的實現步驟:
-
使用一個佇列儲存當前層的節點。
-
先將根節點加入佇列。
-
然後逐層遍歷佇列,取出隊首節點,訪問該節點,並將它的左右子節點(如果有的話)依次加入佇列。
-
重複這個過程,直到佇列為空。
層序遍歷的 Java 實現:
// 層序遍歷
public void levelOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 將根節點加入佇列
while (!queue.isEmpty()) {
TreeNode node = queue.poll(); // 取出隊首元素
System.out.print(node.val + " "); // 訪問當前節點
if (node.left != null) {
queue.offer(node.left); // 左子節點加入佇列
}
if (node.right != null) {
queue.offer(node.right); // 右子節點加入佇列
}
}
}
程式碼說明:
-
佇列:我們使用
LinkedList
來實現佇列,因為佇列的特點是先入先出(FIFO)。 -
訪問節點:每次從佇列中取出一個節點,訪問它並將其左右子節點加入佇列。
-
層級遍歷:這種方式會保證節點按照層次順序被訪問,父節點先於子節點。
結合本題
public boolean findTarget(TreeNode root, int k) {
// 構建結果列表
Set<Integer> numSet = new HashSet<>();
// 佇列 模擬
int[] resFlag = new int[]{1};
resFlag[0] = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
levelOrderTravel(numSet, queue, k, resFlag);
return resFlag[0] != 0;
}
private void levelOrderTravel(Set<Integer> numSet,
Queue<TreeNode> queue,
int k,
int[] resFlag) {
while (!queue.isEmpty()) {
// 取出
TreeNode root = queue.poll();
// 符合
int value = root.val;
if(numSet.contains(k - value)) {
resFlag[0] = 1;
return;
}
numSet.add(value);
// 加入左右
if(root.left != null) {
queue.offer(root.left);
}
if(root.right != null) {
queue.offer(root.right);
}
}
}
效果
4ms 29.82
小結
層序遍歷放在本題看起來沒有特別大的優勢。
不過層序遍歷在有些場景還是很有用的,比如 T337 打家劫舍 III。
v5-還有高手
思路
除了這 4 種方式,還有其他更快的方式嗎?
那就是我們其實對二叉樹的理解還是不夠深入。
中序遍歷之後,結果其實是一個升序陣列。
也就是我們可以利用排序後的陣列進行處理,結合 T167.
中序是:left>val>right
回顧 T167
其實就是兩步
1)構建有序陣列
2)雙指標直接獲取
當然雙指標也可以用二分法,此處不再贅述、
java
public boolean findTarget(TreeNode root, int k) {
List<Integer> sortList = new ArrayList<>();
// 中序獲取排序陣列
inOrderTravel(sortList, root);
// 雙指標
return twoSum(sortList, k);
}
public boolean twoSum(List<Integer> sortList, int target) {
int n = sortList.size();
int left = 0;
int right = n-1;
while (left < right) {
int sum = sortList.get(left) + sortList.get(right);
if(sum == target) {
return true;
}
if(sum < target) {
left++;
}
if(sum > target) {
right--;
}
}
return false;
}
private void inOrderTravel(List<Integer> sortList,
TreeNode root) {
if(root == null) {
return;
}
inOrderTravel(sortList, root.left);
// add
sortList.add(root.val);
inOrderTravel(sortList, root.right);
}
效果
3ms 79.82%
小結
這種解法,其實已經很巧妙了。
本題的難度定位在簡單有點浪費,用到這種方式實際上已經結合了多個知識點。