Leetcode算法筆記

Leetcode算法筆記

做題思路

1 拿到題目首先仔細的理解問題,找到問題的特點,有些題目有暴力的方式會很慢,但是隻要找到問題本身特點,就會很快。

1423. 可獲得的最大點數此題暴力方式是dfs,遍歷全部,指數級別 的複雜度,但是可以找到突破口就是實際上就是左右選擇長度的問題,左邊選1個,右邊選k-1個,複雜度降到了線性。

2 找到問題的切入點,有些題目看似無從下手,但是可以有個切入點供我們突破

312. 戳氣球此題切入點就是計算一個序列最後戳破的氣球

3 心中預計一下時空複雜度,想想可不可以或者有沒有更好

4 遇到模板題或者做過的題目不要急,思考一下。

1 算法數據結構精髓

結構化

結構化的根本目的是用特殊的結構保存已有信息避免重複計算。很多算法都利用了這個特性。比如:

快速排序,我們在對數組進行排序,直觀的來說,必須把全部數據兩兩比較,才能分出大小,不然如果存在兩個數沒有比較,我們如何才能知道這兩個數誰大誰小呢?其實也可以,假如我們知道a<b,b<c,那麼不用比較ac我們也知道ac的大小。所以快排排序用人爲規定的結構來記錄這些信息,避免重複比較:

每次選擇一個數,把小於這個數的放在左邊,把大於這個數的方法,這樣左邊右邊的大小就不必比較,從而節省大量的比較次數。

二叉搜索樹,人爲去規定樹的結構,即:對於一個節點,左子樹全部元素小於它,右子樹全部元素大於它,這樣無論是搜索還是插入,都節約了很多搜索次數。

單調隊列單調棧,人爲定義其單調性,這樣能極大提高搜索速度,比如用二分搜索代替遍歷,用隊列的頭直接找到最大值,而不用遍歷整個隊列去找最大值,而維護他們也能保持在O(n)的複雜度,因爲每個元素,最多都會進去一次,出來一次。

貪心化

不少問題都是尋優問題,比如求最值等,這樣可能問題存在貪心性質,當前的選擇可以不全部遍歷,而是選擇當前看起來比較好的選擇,這樣可以避免遍歷所有選擇,從而加快運算。比如最短路問題,最小生成樹問題。

記憶化

很多子問題是重複的,我們不必重複計算它,可以直接把子問題結果保存下來,從而極大減少運算。比如記憶化的回溯或者動態規劃,他們的區別是一個是自頂向下,一個是自底向上。

分治

將一個問題分爲若干個子問題,從而避免重複計算,如快速排序。

2 java技巧

subList 可以輕鬆取一段List
lmabda 表達式可以套用別的函數,hashmap
新建list 可以直接導入Collection

平均值:(a&b) + ((a^b)>>1)

Arrays.binarySearch(int[] a, start,end,key) 找到返回key的下標,找不到返回-x-1

3 常見坑

  • 連續的if判斷一定要用if else 而不是 if if,因爲第一個if 會改變狀態,導致互斥的兩個if可能同時滿足!

  • 一定要看清楚題目的數據範圍,輸出條件!

  • dp 搞清楚狀態和初始值就成功了一大半。

  • Integer 不能用==判斷

4 繞人的遞歸

有些題目的遞歸非常繞人,很難想出正確的遞歸函數,但是把握住一點:只關心當前函數的輸入,功能和返回值,基本就成功了,並且假設子遞歸已經成功的完成了功能(實際也是完成了),我們只需要在子遞歸的基礎上做出當前的計算就行了。

比如:

337. 打家劫舍 III,此題難點在於對於樹的動態規劃,需要自底向上,從樹葉開始到樹根,必須寫出一個正確的遞歸函數。我們現在只關心函數本身的功能,就是返回一個數組,表示當前子樹偷或者不偷的最大值,那麼狀態轉移方程可以直接從子樹裏面得到。

輸入:一個root

返回值:一個數組

功能:計算數組

class Solution {
    public int rob(TreeNode root) {
        int[] res = dfs(root);
        return Math.max(res[0],res[1]);
    }
    public int[] dfs(TreeNode root){
        if(root==null) return new int[2];
        int[] res =new int[2];
        int[] left = dfs(root.left);//關鍵點,把函數看作一個功能的黑盒,不管它內部,我們把當前
        int[] right=dfs(root.right);//函數寫對,其他的自然就對了,此時假設這兩個數組都成功計算
        //接下來我們只需要在這個功能的基礎上算出當前的就行了!!
        res[0]=Math.max(left[0]+right[0],Math.max(left[1]+right[1],Math.max(left[0]+right[1],left[1]+right[0])));
        res[1]=root.val+left[0]+right[0];
        return res;
        
    }
}

206. 反轉鏈表此題用遞歸或者循環都非常燒腦,先說遞歸:

我們首先關心遞歸的本身功能:翻轉一個鏈表,輸入表頭head,返回翻轉後的表頭,並且把 head的next變成null

輸入:一個head

返回值:head連接的鏈表翻轉之後的表頭

功能:翻轉鏈表,並且把head的next 變成空

class Solution {
    public ListNode reverseList(ListNode head) {
        if(head==null) return null;
        if(head.next==null) return head;
        ListNode root = reverseList(head.next);//假設後面的已經完成!!!
        head.next.next=head;//翻轉當前的
        head.next=null;//
        return root;//返回值
        
    }

}

5 左右兩趟

有些題目需要左邊的右邊的信息來計算,一種好用的方式是左右兩趟計算出一些信息,再進行綜合。

例題:

135. 分發糖果此題一個人的糖果由左右的最低谷決定,很方便的可以 分別從左向右 從右向左跑一邊最低值,然後在兩者之前找最大值。

class Solution {
    public int candy(int[] ratings) {
        if(ratings.length==0) return 0;
        if(ratings.length==1) return 1;
        if(ratings.length==2) return ratings[0]-ratings[1]==0?2:3;
        int ans=0;
        int[][] t = new int[2][ratings.length];
        //t[0][0]=1;
        for(int i=1 ;i<ratings.length;i++){
                if(ratings[i]>ratings[i-1]){//左邊最小值
                    t[0][i]=t[0][i-1]+1;
                }
               // else{
                 //   t[0][i]=1;
               // }
        }
        ans+=Math.max(t[0][ratings.length-1],t[1][ratings.length-1]);
        for(int i=ratings.length-2;i>=0;i--){
            if(ratings[i]>ratings[i+1]) t[1][i]=t[1][i+1]+1;//右邊最小值
            ans+=Math.max(t[0][i],t[1][i]);//必須同時滿足左邊和右邊
        }
        return ans+ratings.length;
    }
}

239. 滑動窗口最大值 分別記錄左右窗口的最大值

581. 最短無序連續子數組用棧找左右邊界

6 動態規劃

將一個完整的問題拆分成能一步步擴大的子問題。主要目的是三個:

1 便於求解

當我們面對一個問題束手無策的時候,動態規劃或者類似的思想能讓我們不全部考慮,而是碰瓷一樣,故意把一個完整的問題搜小一步,找到狀態方程,類似於左腳踩右腳上天。

需要注意的地方有兩個:

一是定義的狀態能給完整的表達問題,不能表達,就增加狀態的維度,不怕狀態多就怕狀態少,比如子串問題一般是兩個狀態dp(i)(j)表示從第i到第j長度的字串的某個性質。

而狀態方程要考慮所有可能情況,不怕考慮多,就怕考慮少。(這也是可以優化的地方,比如最長上升子序列用單調性+二分查找)

2 記憶化

尤其在DFS中,回重複計算子問題,如果把子問題的解都記錄下來,那麼複雜度會和動態規劃一模一樣,DFS是自頂向下,更方便理解,動態規劃是自底向上。當面對一個問題束手無策的時候,可以用最暴力的DFS搜索所有解,然後考慮記憶化。

3 狀態化

幾乎所有動態規劃問題都能畫出狀態圖

7 滑動窗口

特點:窗口中記錄信息,窗口向右滑動的時候更新左右邊界信息

經典問題

滑動窗口中位數

滑動窗口中位數難度困難64中位數是有序序列最中間的那個數。如果序列的大小是偶數,則沒有最中間的數;此時中位數是最中間的兩個數的平均數。

例如:

[2,3,4],中位數是 3
[2,3],中位數是 (2 + 3) / 2 = 2.5

給你一個數組 nums,有一個大小爲 k 的窗口從最左端滑動到最右端。窗口中有 k 個數,每次窗口向右移動 1 位。你的任務是找出每次窗口移動後得到的新窗口中元素的中位數,並輸出由它們組成的數組。

鏈接:https://leetcode-cn.com/problems/sliding-window-median

方法:用一個大堆和小堆記錄中位數,每次更新。

滑動窗口和單調隊列結合

滑動窗口最大值

給定一個數組 nums,有一個大小爲 k 的滑動窗口從數組的最左側移動到數組的最右側。你只可以看到在滑動窗口內的 k 個數字。滑動窗口每次只向右移動一位。

返回滑動窗口中的最大值。

鏈接:https://leetcode-cn.com/problems/sliding-window-maximum

需要用單調隊列記錄窗口中的信息,不然每次都需要掃描窗口。或者巧妙用dp(見左右兩趟),不過很難想到。

8 字典樹、前綴樹

特點:用字典樹去記錄單詞,把時間複雜度從單詞個數n*L降低到O(L)

細節:樹的開頭最好不存放單詞

典型結構:

class Tree{
    Tree[] c;
    boolean isword;
    public Tree(){
        c = new Tree[26];
    }
}

9 並查集

特點:快速合併兩個子圖,可以用來判斷連通性,尋找連通子圖個數,壓縮路徑的並查集union和find 可以認爲時間複雜度是O(1)。

壓縮路徑方法:

int find(int[] a,int x){
    while(a[x]!=x){
        a[x]=a[a[x]];//壓縮路徑
        x=a[x];
    }
    return x;
}

合併方法:

void union(int[] a,int i,int j){
    int x = find(a,i);
    int y = find(a,j);
    a[x]=y;
}

帶權值的並查集

399. 除法求值方法:給每個方向加上權值進行計算。

10 線段樹

特點:有一顆用數組表示的完全二叉樹,每個節點都有區間,葉子節點的區間是1

結構:

class Tree{
    int l;
    int r;
    int value;
}

11 單調隊列

可以用來找子數組子序列的最大值最小值,比如滑動窗口最大值問題: 滑動窗口最大值

此處用單調隊列的目的是,我們可以不用遍歷,就能通過單調隊列來始終保存窗口最大值。

而且單調隊列可以二分查找,參考最大上升子序列

12 單調棧

和單調隊列類似,我們用結構性的數據結構來保存信息,達到不用重複計算的目的。這也是個人理解數據結構的精髓之一,類似的結構很多,比如快速排序,二叉查找樹,最大堆等。而單調棧特別適合處理邊界問題!

只要確定左右邊界,就立馬出棧計算!

單調棧的典型問題就是接雨水:接雨水,每一個空間能接的雨水數量僅僅和它的左右比它高的邊界有關係,而用單調棧很容易維護這種邊界關係,不是邊界就入棧,是邊界就會出棧,每一個元素都會在出棧的時候計算。

import java.util.*;
class Solution {
    public int trap(int[] height) {
        if(height.length<=2){
            return 0;
        }
        int ans=0;
        int lmax=0;
        LinkedList<Integer> s = new LinkedList<Integer>();
        s.addLast(0);
        for(int i=0;i<height.length;i++){
            while(s.size()>1 && height[i]>=lmax){//確定左邊邊界,立馬計算
                int d = s.removeLast();
                ans=ans+Math.max(0,Math.min(height[i],lmax)-d);
            }
            s.addLast(height[i]);
            lmax=Math.max(lmax,height[i]);
        }
        int rmax=s.getLast();
        while(s.size()>1){
            int d =s.removeLast();
            ans=ans+Math.max(0,Math.min(rmax,lmax)-d);//計算
            rmax = Math.max(rmax,d);
        }
        
        return ans;
    }
}

還有柱狀圖中最大的矩形,同樣是邊界關係。

class Solution {
    public int largestRectangleArea(int[] heights) {
        if(heights.length==0)
            return 0;
        int ans=0;
        Stack<Integer> s =new Stack<>();
        s.push(-1);
        for(int i=0;i<heights.length;i++){
            
            
            while(s.peek()!=-1&&heights[s.peek()]>heights[i]){//不單調說明出現邊界
                    ans=Math.max(ans,heights[s.pop()]*(i-1-s.peek()));//邊界僅僅和相鄰的元素有關係
                }
            s.push(i);
        }
        int t=s.peek();
        while(s.peek()!=-1){
                ans=Math.max(ans,heights[s.pop()]*(t-s.peek()));
        }
        
        return ans;
        
    }
}

同樣的應用還有

739. 每日溫度

85. 最大矩形

581. 最短無序連續子數組用棧找左右邊界

13 子序列問題

最長上升子序列

方法一:用dp[i]記錄第i個元素爲結尾的最長序列,每次回頭找最長的:

dp[i]=max(dp[i-1]) i=0~i-1 ,num[i-1]<num[i]

複雜度n^2

方法二:維護一個單調dp,dp[i]表示i爲長度爲i的子序列的末尾的最小值

dp是單調上升的,每次用二分查找更新dp,這也是性能可以提高的原理

複雜度nlogn

單調隊列用數組模擬會快很多

相關問題

俄羅斯信封套娃

堆箱子

14 染色/二分圖問題

可能的二分法

1.深度優先搜索

搜索這個圖,並且交替染色,如果出現衝突則不是二分圖,複雜度O(V+E)

class Solution {
    ArrayList<Integer>[] graph;
    Map<Integer, Integer> color;

    public boolean possibleBipartition(int N, int[][] dislikes) {
        graph = new ArrayList[N+1];
        for (int i = 1; i <= N; ++i)
            graph[i] = new ArrayList();

        for (int[] edge: dislikes) {
            graph[edge[0]].add(edge[1]);
            graph[edge[1]].add(edge[0]);
        }

        color = new HashMap();
        for (int node = 1; node <= N; ++node)
            if (!color.containsKey(node) && !dfs(node, 0))
                return false;
        return true;
    }

    public boolean dfs(int node, int c) {
        if (color.containsKey(node))
            return color.get(node) == c;
        color.put(node, c);

        for (int nei: graph[node])
            if (!dfs(nei, c ^ 1))
                return false;
        return true;
    }
}

2.奇偶狀態轉移

現在只考慮邊,首先對邊進行排序,然後交替對邊的頂點進行奇偶賦值,出現衝突就不是二分圖

class Solution {
    public boolean possibleBipartition(int N, int[][] dislikes) {
        int[] dp = new int[N+1];
        int k = 1;
        
        for (int[] dislike : dislikes) {
            int a = dislike[0], b = dislike[1];
            if (dp[a] == 0 && dp[b] == 0) {//都爲初始狀態
                dp[a] = k++;
                dp[b] = k++;
            } else if (dp[a] == 0) {
                dp[a] = dp[b] % 2 == 0 ? dp[b] - 1 : dp[b] + 1;
            } else if (dp[b] == 0) {
                dp[b] = dp[a] % 2 == 0 ? dp[a] - 1 : dp[a] + 1;
            } else { //都不爲初始狀態
                if(dp[a] == dp[b]) return false;
            }
        }

        return true;
    }
}

複雜度O(E)或者O(E+log(E))

15 劃分分治

將一個數組劃分成兩個部分,用於快速排序,求第K大的數,數組逆序數。

快速排序:

public void qiuksort(int[] a,int l,int r){
        if(l>=r) return;
        int p = partition(a,l,r);
        qiuksort(a,l,p-1);
        qiuksort(a,p+1,r);
    }
    public int partition(int[] a,int l,int r){
        int t =a[l];
        while(l<r){
            while(l<r){
                if(a[r]<t){
                    a[l]=a[r];
                    break;
                }
                else r--;
            }
            while(l<r){
                if(a[l]>t){
                    a[r]=a[l];
                    break;
                }
                else l++;
            }
        }
        a[l]=t;
        return l;
    }

三分快速排序

public void qiuksort2(int[] a,int l,int r){
        if(l>=r) return;
        int ll=l;
        int rr=r;
        int i = l+1;
        int t = a[l];
        while(i<=rr){
            if(a[i]==t) i++;
            else if(a[i]>t) exchange(a,rr--,i);
            else exchange(a,ll++,i++);
        }
        qiuksort(a,l,ll-1);
        qiuksort(a,rr+1,r);
    }

數組中的第K大元素:

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int n = nums.length;
        
        for(int i=n-1;i>=1;i--){
            int x=(int)(Math.random()*(i+1));
            int t = nums[x];
            nums[x]=nums[i];
            nums[i]=t;
        }
        return partition(nums,k,0,n-1);
    }
    public int partition(int[] nums,int k,int l,int r){
        int x=l;
        int y=r;
        if(l>=r) return nums[l];
        int t = nums[l];
        while(l<r){
            while(l<r){
                if(nums[r]>t){
                    nums[l]=nums[r];
                    break;
                }
                r--;
            }
            while(l<r){
                if(nums[l]<t){
                    nums[r]=nums[l];
                    break;
                }
                l++;
            }
        }
        nums[l]=t;//劃分
        if(l==k-1) return nums[l];。。找到第k個
        if(l<k-1) return partition(nums,k,l+1,y);//之後
        return partition(nums,k,x,l-1);//之前
    }

}

逆序對問題,在分治排序和合並過程中計算逆序對,由於兩邊都是排序的,所以不用回溯,並且對下標排序不改變原來數組,並且計算右邊有多少個元素比左邊小的時候,由於右邊是排好序的,所以每次應該計算區間。

class Solution {
    int[] index;
    int[] temp;
    int ans=0;
    public int reversePairs(int[] nums) {
        int n = nums.length;
        if(n<2) return 0; 
        index = new int[n];
        temp = new int[n];
        for(int i=0;i<n;i++){
            index[i]=i;
        }
        mergesort(nums,0,n-1);
       // System.out.println(Arrays.toString(index));
        return ans;
    }
    public void mergesort(int[] nums,int l,int r){
       // System.out.println(l+" "+r);
        if(l>=r) return;
        int mid = l+((r-l)>>1);
        mergesort(nums,l,mid);
        mergesort(nums,mid+1,r);
        if(nums[index[mid]]<=nums[index[mid+1]]) return;
        merge(nums,l,mid,r);
    }
    public void merge(int[] nums,int l,int mid,int r){
        for(int i=l;i<=r;i++){
            temp[i]=index[i];
        }
        int i=l;
        int j=mid+1;
        for(int k=l;k<=r;k++){//排序後,因爲左右都是有序的,可以不回溯,所以複雜度是n
            if(i>mid){
                temp[k]=index[j++];
            }
            else if(j>r){
                temp[k]=index[i++];
                ans=ans+r-mid;//計算區間
            }
            else if(nums[index[i]]<=nums[index[j]]){
                temp[k]=index[i++];
                ans=ans+j-mid-1;//計算區間,而不是隻算一個
            }
            else{
                temp[k]=index[j++];
            }
        }
        for(i=l;i<=r;i++){
            index[i]=temp[i];
        }
    }
}

翻轉對:排序後,因爲左右都是有序的,可以不回溯,所以複雜度是nlongn

class Solution {
    int ans;
    int[] t;
    public int reversePairs(int[] nums) {
        int n =nums.length;
        if(n<2) return 0;
        t = new int[n];
        ans=0;
        mergersort(nums,0,n-1);
        //System.out.println(Arrays.toString(nums));
        return ans;

    }
    public void mergersort(int[] nums,int l,int r){
       // System.out.println(l+" "+r);
        if(l==r) return;
        int mid = l+((r-l)>>1);
        mergersort(nums,l,mid);
        mergersort(nums,mid+1,r);
        merge(nums,l,mid,r);
    }
    public void merge(int[] nums,int l,int mid,int r){
        int j = mid+1;
        for(int i=l;i<=mid;i++){//排序後,因爲左右都是有序的,可以不回溯,所以複雜度是n
            while(j<=r){
                long g=nums[j];
                if(nums[i]>(g<<1)){
                    j++;
                }
                else break;
            }
            ans=ans+j-mid-1;

        }
        int x=l;
        int y=mid+1;
        for(int i=l;i<=r;i++){
            t[i]=nums[i];
        }
        for(int i=l;i<=r;i++){
            if(x>mid){
                nums[i]=t[y++];
            }
            else if(y>r){
                nums[i]=t[x++];
            }
            else if(t[x]<t[y]){
                nums[i]=t[x++];
            }
            else{
                nums[i]=t[y++];
            }
        }
    }
}

16 左右指針/雙指針

和滑動窗口類似,核心是指針不回溯,從而達到時間複雜度是線性。

比如三數只和四數之和 ,可以排序之後左右指針進行夾逼,不回溯指針

17 哈希表

用哈希表來存儲信息,達到隨機搜索的目的,應用很多,比如

兩數之和問題:兩數之和用哈希表記錄一個數

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] ans = new int[2];
        HashMap<Integer,Integer> mp = new HashMap<>();
        for(int i=0;i<nums.length;i++){
            if(mp.containsKey(target-nums[i])){
                ans[0]=mp.get(target-nums[i]);
                ans[1]=i;
                return ans;
            }
            mp.put(nums[i],i);
        }
        return ans;
    }
}

和爲K的子數組,兩數之和的變形

public class Solution {
    public int subarraySum(int[] nums, int k) {
        int count = 0, pre = 0;
        HashMap < Integer, Integer > mp = new HashMap < > ();
        mp.put(0, 1);
        for (int i = 0; i < nums.length; i++) {
            pre += nums[i];
            if (mp.containsKey(pre - k))
                count += mp.get(pre - k);
            mp.put(pre, mp.getOrDefault(pre, 0) + 1);
        }
        return count;
    }
}

一些和前綴和有關的問題都可以往兩數之和上面靠攏

18 摩爾投票

169. 多數元素相互抵消,每次選擇兩個不同的元素相互抵消,最終留下來的就是大多數元素

class Solution {
    public int majorityElement(int[] nums) {
        int n=0,count=0;
        for(int a:nums){
            if(count==0) n=a;
            count+=(a==n?1:-1);
        } 
        return n;
    }
}

19 二叉搜索樹

二叉搜索樹在很多地方都有妙用,比如排序,邊界問題

220. 存在重複元素 III

此題就是在滑動窗口內找元素,一般的方法是遍歷,但是我們只需要找出最接近當前元素的兩個值,可以用二叉搜索,維持一種序列,很方便的就找出最接近當前元素的兩個值(此題也可用同桶排序)

注意此題不適合用單調隊列,因爲單調隊列維護的是窗口裏面的最值,此題不是找最值,而是最接近目標元素的值

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章