LeetCode - 題集(Java)

  • 計算機語言和開發平臺日新月異,但萬變不離其宗的是那些算法和理論。
  • 算法和理論永遠是程序員最重要的內功,禿頭也得學呀

記錄部分 當時的思路或者學習其他優秀作者的思路,也幫自己重新梳理邏輯,爭取加深印象,加油 (^-^)V

LeetCode算法

1,兩數之和

在這裏插入圖片描述
思路較簡單:

  • 蠻力法:遍歷數組每個元素a,查看是否存在b令b=target-a
  • 利用map數據結構,用犧牲內存的方式來大大加快運行速度。

這題也給了提示,翻譯過來,不做過多的累述:
第二個思路是,在不更改數組的情況下,我們可以以某種方式使用額外的空間嗎? 像是散列表來加快搜索速度?

唯有這題用的C++

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
       vector<int> a = { 0,0 };
			map<int, int>m;
			int t;
			for (int i = 0; i < nums.size(); i++) {
				t = target - nums[i];
				if (m.find(t) == m.end())
					m[nums[i]] = i;
				else {
					a[0] = m[t];
					a[1] = i;
					return a;
				}
					
			}
			return a;
    }
};

3、無重複字符的最長子串

在這裏插入圖片描述
第一思路:HashSet容器,以每個字符開頭,遍歷n-1次(字符串長度n),每次將元素加入容器,當有重複元素時喊停記錄長度l,然後在清空容器,但是頻繁的加入容器和清空消耗大量的時間。(打敗全國5%的人T.T)

然後認識了一種叫滑動窗口法的方法:

定義兩個指針,start和end,代表當前窗口的開始和結束位置,同樣使用hashset,當窗口中出現重複的字符c且c的索引值>start時,start移動到容器中已有的c後面,沒有重複時,end++,每次更新長度的最大值

從1000ms減到10ms,減少了頻繁加入元素和清除容器的時間。這裏參考下別人的優秀作業:

public int lengthOfLongestSubstring(String s) {
        int res = 0;
        int end=0,start=0;
       Map<Character,Integer> map=new HashMap<>();
        for(;end<s.length();end++){
           if(map.containsKey(s.charAt(end))){
               start=Math.max(map.get(s.charAt(end)),start);//從有重複的下一個位置繼續找
           }     
            map.put(s.charAt(end),end+1);//map每次更新
            res=Math.max(res,end-start+1);//結果每次更新
        }
        return res;
    }

6、Z字形變換 (String和StringBuilder)

在這裏插入圖片描述

行數爲3:
0		4		8
1 	3	5	7	9
2		6
行數爲4:
0			6			12
1		5	7		11
2	4		8	10
3			9

分析上面的兩個例子,可以發現其實有規律可循。

String res="";//結果
int n=s.length();
//l爲兩個N之間的距離,是一個固定值,N的那條斜線距離兩豎距離是根據rows變化的,用l1記錄。
int l=2*numRows-2,l1=0,p=0;//p爲s的指針

實現1: 用String存儲拼接結果

public String convert1(String s, int numRows) {
        String res="";
        int n=s.length();
        int l=2*numRows-2,l1=0,p=0;
        while(p<n){
            res+=s.charAt(p);
            p+=l;
        }
        for (int i = 1; i <numRows-1 ; i++) {
            l1+=2;
            p=i;
            while(p<n){
                res+=s.charAt(p);
                p+=l-l1;
                if(p<n){
                    res+=s.charAt(p);
                    p+=l1;
                }
            }
        }
        p=numRows-1;
        while(p<n){
            res+=(s.charAt(p));
            p+=l;
        }
        return res;
    }

通過是沒問題的
在這裏插入圖片描述
在res字符串中,完全是進行字符串拼接操作。
但是瞭解StringBuilder的同學肯定知道,String是final修飾不可變的,每次修改值都會創建新的對象,大大加重了計算負擔,而Stringbuilder和StringBuffer是在原有的值上進行修改不用創建新對象引用,大大提高了效率。

實現2: 用StringBuilder:快了好幾倍。

public String convert(String s, int numRows) {
       if(numRows==1)return s;
       StringBuilder res=new StringBuilder();
       int n=s.length();
       int l=2*numRows-2,l1=0,p=0;
       while(p<n){
           res.append(s.charAt(p));
           p+=l;
       }
       for (int i = 1; i <numRows-1 ; i++) {
           l1+=2;
           p=i;
           while(p<n){
               res.append(s.charAt(p));
               p+=l-l1;
               if(p<n){
                   res.append(s.charAt(p));
                   p+=l1;
               }
           }
       }
       p=numRows-1;
       while(p<n){
           res.append(s.charAt(p));
           p+=l;
       }
       return res.toString();
   }

在這裏插入圖片描述

11、盛水最多的容器

在這裏插入圖片描述
題意已經很明確了,就是找到兩個柱子和X軸圍成最大的面積。

第一思路肯定是蠻力法窮舉所有可能的面積取最大值,思考如何改進。

變量: 設置指針i,j分別從左右端開始掃描,高度分別爲l,r,最大面積max爲(j-i)*min(l,r)

思考: 如果數組的值(柱高)是隨機分佈,那麼從數組兩端開始更可能先獲取最大面積,之後過濾不可能的值。

步驟:
在這裏插入圖片描述

//開始
i=0 ;     j=8;
l=height[i]	;	r=height[j];
max=1*7=7;
//選擇柱子低的一方指針,往中間移動,直到height[i]>l
l=height[1]=8
//計算面積是否更大
7*7=49>max;
max=49
//現在是r<l了,右邊指針往中間移動,直到j=6
8*6<49
繼續移動

index=2,3,4,5,的柱子都比 左邊最高的l 或者右邊最高的r 短,忽略,從而得到最大值49,省略了不必要的計算。

public int maxArea(int[] height) {
        int i=0,j=height.length-1,l=0,r=0,max=0;
        while(i<j){
            l=height[i];
            r=height[j];
            if(l<=r){
                max=Math.max(max,(j-i)*l);
                while (i<height.length&&height[i]<=l)i++;
            }else {
                max=Math.max(max,(j-i)*r);
                while (j>=0&&height[j]<=r)j--;
            }
        }
        return max;
    }

用內存換取執行時間是不錯的選擇
在這裏插入圖片描述

15、三數之和

在這裏插入圖片描述
要求:a+b+c=0,且三元組不重複。
簡單起見,假設他們是一個有序數組,從第一個數開始作爲a,target=-a,之後尋找b和c,令左指針指向a的下一個數b,右指針指向最後一個數c,while(l<r)

  • 如果b+c==target,加入結果集,兩指針向中間靠攏,繼續尋找答案。
  • 如果b+c<target,左指針右移
  • 如果b+c>target,右指針左移
  • 內部去重:如下圖,此時nums[i]=-1,target=1,指針的當前位置已經符合條件,那麼r繼續左移不查重的話會導致答案重複,所以需要內部去重
    在這裏插入圖片描述
  • 外部去重:如圖,當第一個 i 指向第一個 -1,並且已經找到所有target=1的情況了,那麼i就沒有必要再指向第二三個-1了,所以需要外部去重。
    在這裏插入圖片描述
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res=new ArrayList<>();
        if(nums.length==0)return res;
        Arrays.sort(nums);
        
        for (int i = 0; i < nums.length-2; i++) {
            int target=0-nums[i];
            int l=i+1,r=nums.length-1; //左指針和右指針
            while (l<r){
                if(nums[l]+nums[r]==target){//等於答案,加入res
                    List<Integer> list=new ArrayList<>();
                    list.add(nums[i]);
                    list.add(nums[l]);
                    list.add(nums[r]);
                    res.add(list);
                    l++;r--;
                    while (l<r&&nums[l]==nums[l-1]) l++;//內部去重
                    while (r>l&&nums[r]==nums[r+1]) r--;
                }else if(nums[l]+nums[r]>target){//大於目標 ,右指針往左
                    r--;
                }else l++; ////小於目標 ,左指針往右
            }
             while (i+1<nums.length&&nums[i+1]==nums[i])i++;//外部去重
        }
        return res;
    }
}

17、電話號碼的字母組合(全排序)

在看別人代碼的時候,不要以爲看懂你就會敲了
自己敲一遍下來,以後類似的題目,做起來也也可以得心應手~

在這裏插入圖片描述
思路

這個和全排序有相似之處,順便把全排序做了吧~
(遞歸大法好)

按照深度優先的方式,保存所有可能的結果。
在這裏插入圖片描述

class Solution {
   char [][] digitsLetter={{'a','b','c'},{'d','e','f'},{'g','h','i'},{'j','k','l'},{'m','n','o'}
   						,{'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}};
    public List<String> letterCombinations(String digits) {
      List<String> res=new ArrayList<>(); //存儲結果集
      if(digits.length()==0)return res;
      char[] currentRes=new char[digits.length()];//存儲可能的排序
      letterComb(res,0,currentRes,digits);//遞歸
      return res;
    }
    public void letterComb(List<String> res,int i,char[] currentRes,String digits){
      boolean isLast=false;//判斷是否是最後一個數字
      if(i==digits.length()-1) isLast=true;
      for (int j = 0; j < digitsLetter[digits.charAt(i)-50].length; j++) {
        currentRes[i]=digitsLetter[digits.charAt(i)-50][j];
        if(isLast){//最後一個數字,加入結果集
          StringBuilder stringBuilder=new StringBuilder();
          for (int k = 0; k <=i; k++) {
            stringBuilder.append(currentRes[k]);
          }
          res.add(stringBuilder.toString());
        }else//否則繼續遞歸
          letterComb(res,i+1,currentRes,digits);
      }
    }
}

全排序

public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0) {
            return res;
        }
        permute(nums, 0, res);
        return res;
    }

    private void permute(int[] nums, int i, List<List<Integer>> res) {
        if (i == nums.length) {
            ArrayList<Integer> tmp = new ArrayList<>();
            for (int num : nums) {
                tmp.add(num);
            }
            res.add(tmp);
            return;
        }
        for (int j = i; j < nums.length; j++) {
            swap(nums, i, j);
            permute(nums, i + 1, res);
            swap(nums, i, j);
        }
    }

    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

18、四數之和

在這裏插入圖片描述
思路:在三數之和的基礎上
我們已經做出來了三數之和,先指定一個數a,然後在定義左右指針尋找b,c。
那麼四個數也可以這樣做,指針i指定a以後,指針k每次指定d首先爲最後一個數,然後指針l指向a的後一個數,指針r指向d的前一個數。

也就是在指針i內部加一個指針k循環,最內側循環依然是l,r
在這裏插入圖片描述
注意:這時需要加一個去重條件,防止k指向大小相同的數。

while (k>i && nums[k - 1] == nums[k]) k--;

這樣大體思路就完成了,來實現一下:

public List<List<Integer>> fourSum(int[] nums,int target) {
        List<List<Integer>> res=new ArrayList<>();
        if(nums.length==0)return res;
        Arrays.sort(nums);
        for (int i = 0; i < nums.length - 3; i++) {
            for (int k = nums.length-1; k > i+2; k--) {
                int target1 = target - nums[i]-nums[k];
                int l = i + 1, r = k- 1;
                if(nums[i]*2>target1 || nums[k]*2<target1)continue;
                while (l < r) {
                    if (nums[l] + nums[r] == target1) {
                        List<Integer> list = new ArrayList<>();
                        list.add(nums[i]);
                        list.add(nums[l]);
                        list.add(nums[r]);
                        list.add(nums[k]);
                        res.add(list);
                        l++;
                        r--;
                        while (l < r && nums[l] == nums[l - 1]) l++;
                        while (r > l && nums[r] == nums[r + 1]) r--;
                    } else if (nums[l] + nums[r] > target1) {
                        r--;
                    } else {
                        l++;
                    }
                }
                while (k>i && nums[k - 1] == nums[k]) k--;
            }
            while (i + 1 < nums.length && nums[i + 1] == nums[i]) i++;
        }
        return res;
    }

== 優化前執行結果==
在這裏插入圖片描述

優化
執行用時的誤差還是比較小的,執行的結果還不是特別令人滿意,我希望可以優化一下。優化無非希望他可以減少遍歷不可能的答案,當已經定位了ad,我們便可以得出接下來獲得的最大和、最小和。

最大和a+d*3<target ===那麼l和r不再遍歷
最小和a*3+d>target ===那麼l和r不再遍歷

那麼優化其實只要再l,r遍歷之前加上方框內的判斷條件:
其中target1target減去ad
在這裏插入圖片描述

優化後執行結果
在這裏插入圖片描述

19、刪除鏈表的倒數第N個節點

在這裏插入圖片描述
思路
刪掉倒數第n個節點,如果不考慮代碼質量的話,你是否和我一樣,首先考慮到先遍歷一遍得到長度Length,然後再遍歷Length-n次找到要刪除的點呢。這樣重複的遍歷無疑是多餘的。
快慢指針
不妨使用快慢指針的方式,如圖,fastP指針先走n步,然後slowP也開始出發,跟着fastP一起走Length-n步,就可以到達指定點了~
在這裏插入圖片描述

public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode fastP=head;//快指針
        ListNode slowP=head;//慢指針
        for (int i = 0; i < n; i++) {
          fastP=fastP.next;
        }
        if(fastP==null){
          return head.next;
        }
        fastP=fastP.next;
        while (fastP!=null){
          fastP=fastP.next;
          slowP=slowP.next;
        }
        ListNode t=slowP;
        t=t.next;
        slowP.next=t.next;
        return head;
    }

24、兩兩交換鏈表中的節點

在這裏插入圖片描述

基礎鏈表題目,就當複習數據結構吧!
鏈表的增刪都只需要額外的2個輔助指針,而這一題的要求是交換節點,而不是節點內容,需要3個額外指針共同協作

具體思路是新增一個虛擬的頭結點,這樣head指針指向的第一個節點我無用節點,再用x,y分別指向head.next和x.next,理解圖:
在這裏插入圖片描述

public ListNode swapPairs(ListNode head) {
        if (head == null){
            return head;
        }
        ListNode f = new ListNode(0);
        f.next = head;
        head = f;
        ListNode cur = head;
        while (cur!=null && cur.next!=null && cur.next.next!=null){
            ListNode x = cur.next;
            ListNode y = x.next; 
            x.next = y.next;
            cur.next = y;
            y.next = x;

            cur = cur.next.next;
        }
        return head.next;
    }

1010、總持續時間可被60整除的歌曲

在這裏插入圖片描述

剛遇到這道題,給我的第一感覺:太簡單了吧!

對的,如果要做出來結果來是非常的簡單,你可以用暴力雙重循環列出所有2個數的組合和,然後對60取模。結果就是:
在這裏插入圖片描述

這是一道數組題,時刻提醒我們運用數組解決問題

暴力法的時間複雜度爲O(n2),暴力法的時間花在越往後的數字,重複取模的次數會越來越多,因爲所有數的取模結果只有60種,可以考慮用一個長度60的數組存儲所有取模結果再計算結果,時間複雜度 爲O(n)。

  • 我先想到的是。從數組尾巴n=time[tail]%60出發,每次對count+=res[60-n]計算,就是算法1,但是結果不是特別令人滿意,3ms,擊敗60%,於是參考別人的代碼;
  • 優化:題目給了一個例子[60,60,60],他們取模都是0,且也需要取模爲0的數配對。假設n個數都是取模爲0或30的數,那他們對count操作次數就是n-1次。
    如果一次知道取模爲0的個數爲(假設)i,那麼他們配對個數就是i*(i-1)/2。只需要計算一次。其他情況通用適用,於是有解法2。
//解法1
public int numPairsDivisibleBy60(int[] time) {
	int[]res=new int[60];
    int count=0;
    for (int i = time.length-1; i >=0; i--) {
        int left=time[i]%60;
        if(left!=0)
            count+=res[60-left];
        else count+=res[0];
        res[left]++;
    }
    return count;
}
//解法2
public int numPairsDivisibleBy60(int[] time) {
        int[]res=new int[60];
        int count=0;
        for(int i:time){
            res[i%60]++;
        }
        count+=(res[0]*(res[0]-1))/2;
        count+=(res[30]*(res[30]-1))/2;
        int i=1,j=59;
        while (i<j){
            count+=res[i++]*res[j--];
        }
        return count;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章