力扣 653. 兩數之和 IV 二叉樹/binary-tree two-sum IV

老马啸西风發表於2024-11-12

陣列系列

力扣資料結構之陣列-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

樹的遍歷有多種方式:前序 中序 後序 層序

找到符合的結果

  1. 暴力

2)藉助 Hash

  1. 排序+二分

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)實現的,常見的做法是使用佇列來輔助遍歷。

層序遍歷的實現步驟:

  1. 使用一個佇列儲存當前層的節點。

  2. 先將根節點加入佇列。

  3. 然後逐層遍歷佇列,取出隊首節點,訪問該節點,並將它的左右子節點(如果有的話)依次加入佇列。

  4. 重複這個過程,直到佇列為空。

層序遍歷的 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); // 右子節點加入佇列
        }
    }
}

程式碼說明:

  1. 佇列:我們使用 LinkedList 來實現佇列,因為佇列的特點是先入先出(FIFO)。

  2. 訪問節點:每次從佇列中取出一個節點,訪問它並將其左右子節點加入佇列。

  3. 層級遍歷:這種方式會保證節點按照層次順序被訪問,父節點先於子節點。

結合本題

    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%

小結

這種解法,其實已經很巧妙了。

本題的難度定位在簡單有點浪費,用到這種方式實際上已經結合了多個知識點。

相關文章