降低时间复杂度的几种方法【持续更新】

2021年9月6日 7点热度 0条评论 来源: 泥土中的阳光

降低时间复杂度的几种方法【持续更新】

  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));
    }
}
    原文作者:泥土中的阳光
    原文地址: https://blog.csdn.net/tree123tree123/article/details/73129450
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。