降低時間複雜度的幾種方法【持續更新】

降低時間複雜度的幾種方法【持續更新】

  LeetCode的許多題目都對時間複雜度有相應的要求,大家在刷題時遇到一道題可能有自己的解決方法,也確實可行,然而時間複雜度達不到要求也無法通過。這裏給大家持續總結LeetCode中遇到的那些降低時間複雜度的方法。

充分利用已有信息(如DP中的備忘錄)

  LeetCode 652. Find Duplicate Subtrees

Given a binary tree, return all duplicate subtrees. For each kind of duplicate subtrees, you only need to return the root node of any one of them.

Two trees are duplicate if they have the same structure with same node values.

Example 1:
1
/ \
2 3
/ / \
4 2 4
/
4
The following are two duplicate subtrees:
2
/
4
and
4
Therefore, you need to return above trees’ root in the form of a list.

  字符串的題,找出相同的子樹,並返回重複子樹的根節點。
  思考:
  1. 除了層序遍歷以外,樹的遍歷就三種——preorder、inorder、postorder。對於找出子樹而言,三種遍歷選哪一個都可以。
  2. 如果是判斷兩棵樹是否相同,可以採取任一種遍歷方式,同步對兩棵樹進行相同的遍歷,並比較每次遍歷的值,完全相同即可。(初步想法,low了)
  3.1 題意只要返回重複子樹的根節點,聯想到初次遍歷樹時,每次發現一樣的節點值,作爲一個子樹的根節點,跟之前相同的值作爲根節點的子樹進行比較。選取Map<Integer,TreeNode>數據結構。(每一棵子樹只記錄根節點進行表示,結果發現複雜度太高,難以實現)
  3.2 既然是相同的子樹,那麼除了根節點數值相同以外,樹的高度也應該相同,那麼第一輪遍歷記錄下①子樹根節點的值②子樹高度③子樹根節點引用 就好了。(每一棵子樹記錄根節點和子樹高度,雖然相比3.1稍好點,但是複雜度依舊很高)
  求助:
  2、3兩步都會造成在第1步遍歷整棵樹後,對已有的信息未充分利用,而是不必要的重複加工,例如已經遍歷過的整棵樹,在判斷各個子樹是否相等時,又要把子樹全部重新遍歷一遍。導致複雜度高
  每一棵子樹完整記錄,這樣比較時就不再需要同時遍歷兩棵子樹了,可以降低比較的複雜度。
  爲了降低完整記錄時的複雜度,採取如下方案(類似DP中的備忘錄):每一棵子樹由其子樹根節點+左子樹+右子樹構建而成即可
  代碼如下:

    public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        Map<String, List<TreeNode>> map = new HashMap<String, List<TreeNode>>();//key存儲以每個節點爲根節點的子樹信息,value存儲子樹重複的節點
        helper(root,map);
        List<TreeNode> list = new LinkedList<TreeNode>();
        for(List<TreeNode> l:map.values())
            if(l.size() > 1)
                list.add(l.get(0));

        // for(Map.Entry<String, List<TreeNode>> entry:map.entrySet())
        //     if(entry.getValue().size() > 1)
        //         list.add(entry.getValue().get(0));        
        return list;
    }
    private String helper(TreeNode root, Map<String, List<TreeNode>> map){
        if(root == null)
            return "";
            //充分利用左右子樹來構建自己。
        String s = (new Integer(root.val)).toString() + "(" 
            + helper(root.left,map) + ")" 
            + helper(root.right,map);
        if(!map.containsKey(s))
            map.put(s, new LinkedList<TreeNode>());
        map.get(s).add(root);
        return s;
    }

使用某種數據結構

  優先級隊列 使用的兩種場景:

  1. 想要根據Map的value值對Map進行排序

  2. 想要對某幾個元素的集合進行排序,此時可以針對這幾個元素定義一個類class

類似參考代碼如下:

public class Solution {
    public void method(){
        //根據Map的value值對Map進行排序
        PriorityQueue<Map.Entry<Integer, Integer>> q = new PriorityQueue<>((a,b) -> a.getValue() - b.getValue());//升序
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        q.addAll(map.entrySet());//用map構建PriorityQueue
        Map.Entry<Integer, Integer> entry = q.poll();
        int value = entry.getValue();
        int key = entry.getKey();
        //由於Map.Entry<,>沒法new出來,所以在key值不需要改變時,可以用剛剛poll出來的,進行value修改,再offer回去使用
        entry.setValue(min);
        q.offer(minEntry);

        //想要對某幾個元素的集合進行排序,此時可以針對這幾個元素定義一個類class,對這個類進行自動排序
        PriorityQueue<Element> q2 = new PriorityQueue<>((a,b) -> a.x != b.x ? a.x - b.x : b.y - a.y));//多優先級排序,根據x升序,根據y降序
    }
}
class Element(){
    int x;
    int y;
    int z;
    public Element(int x, int y, int z){
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

雙指針遍歷

  LeetCode 234題

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words. You may assume the dictionary does not contain duplicate words.

For example, given
s = “leetcode”,
dict = [“leet”, “code”].

Return true because “leetcode” can be segmented as “leet code”.

  判斷一個字符串能否由字典中的單詞組成。剛開始有想過逆向思維——將字典中的單詞進行排列組合,看能否與所給字符串相同。然而想想時間複雜度O(n!)還是算了。這一題就比較適合用雙指針進行遍歷。也是動態規劃的一類題。
  代碼很簡單,如下:

public class Solution {
    public boolean wordBreak(String s, Set<String> dict) {

        boolean[] f = new boolean[s.length() + 1];

        f[0] = true;

        for(int i=1; i <= s.length(); i++){
            for(int j=0; j < i; j++){
                if(f[j] && dict.contains(s.substring(j, i))){
                    f[i] = true;
                    break;
                }
            }
        }

        return f[s.length()];
    }
}

  f[i]代表從 0 至 i 處的字符串是否滿足題目要求。不一定每一個f[i]都滿足,但每隔一段就會有一個滿足,0 至 j , j 至 i 滿足條件時,進行記錄。從而保證最後一個f[s.length]也是滿足的。

空間換時間

  Leetcode 454. 4Sum II

Given four lists A, B, C, D of integer values, compute how many tuples (i, j, k, l) there are such that A[i] + B[j] + C[k] + D[l] is zero.

To make problem a bit easier, all A, B, C, D have same length of N where 0 ≤ N ≤ 500. All integers are in the range of -228 to 228 - 1 and the result is guaranteed to be at most 231 - 1.

Example:
Input:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]
Output:
2

Explanation:
The two tuples are:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

  本題可以將所給數組兩兩“合併”,再進行比較。合併後一個數組的大小是原來的 n² 倍,但是後面比較的時間複雜度可以下降2個數量級。
  採用空間換取時間的方法。

代碼如下:

public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
    //空間換時間
    Map<Integer, Integer> map = new HashMap<>();
    int N = A.length;
    for(int i = 0; i<N; i++)
        for(int j = 0; j<N; j++)
            map.put(A[i] + B[j],map.getOrDefault((A[i] + B[j]),0)+1);

    int result = 0;
    for(int i = 0; i<N; i++)
        for(int j = 0; j<N; j++)      
            result += map.getOrDefault(-1 * (C[i] + D[j]),0);
    return result;
}

數據預處理

  LeetCode 234題。

Given a singly linked list, determine if it is a palindrome.

Follow up:
Could you do it in O(n) time and O(1) space?

  判斷一個單向鏈表是否是迴文鏈表。容易想到對鏈表第一個和最後一個進行比較,沒問題的話,繼續比較第二個和倒數第二個,以此類推。然而無法通過最後一個元素直接得到倒數第二個元素。
  初步想法是,設本次比較了第i和第j個元素,下次比較第(i+1)和第(j-1)兩個元素,那麼第(j-1)通過第i個元素向後依次遍歷得到。然而這樣時間複雜度還是不達標。此時,採用數據預處理會是個不錯的選擇(當然,數據預處理的時間複雜度足夠低)。
  對於所給單鏈表,先遍歷得到中間元素,以中間元素爲首節點,將後一半鏈表進行反轉。這樣單鏈表就被分成子鏈表1和子鏈表2,其中子鏈表2由單鏈表後一半鏈表反轉所得。接下來只要順序比較子鏈表1和子鏈表2即可。是不是就很簡單了呢?

public boolean isPalindrome(ListNode head) {
    ListNode fast = head, slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
    }
    if (fast != null) { // odd nodes: let right half smaller
        slow = slow.next;
    }
    slow = reverse(slow);// 反轉後一半鏈表,得到 子鏈表2
    fast = head; //子鏈表1

    while (slow != null) {
        if (fast.val != slow.val) {
            return false;
        }
        fast = fast.next;
        slow = slow.next;
    }
    return true;
}

public ListNode reverse(ListNode head) {
    ListNode prev = null;
    while (head != null) {
        ListNode next = head.next;
        head.next = prev;
        prev = head;
        head = next;
    }
    return prev;
}

二分查找

  一遇到排序好的數組,下意識就得想到使用二分查找來獲得其中某個值。可以參考Java自帶的Arrays.binarySearch()方法,注意返回值的正負問題即可。

數學推理

  LeetCode 258題。

Given a non-negative integer num, repeatedly add all its digits until the result has only one digit.
For example:
Given num = 38, the process is like: 3 + 8 = 11, 1 + 1 = 2. Since 2 has only one digit, return it.

Follow up:
Could you do it without any loop/recursion in O(1) runtime?

  大概數學好的人,這題確實比較簡單吧。僅從計算機的角度確實想了很久沒想到好方法。
  數學推理通常可以將一個過程直接化爲一個公式得到結果,耐下性子分析過程,推導公式,將非常有利於降低時間複雜度和空間複雜度。

public class Solution {
    public int addDigits(int num) {
        return num==0?0:(num%9==0?9:(num%9));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章