- 計算機語言和開發平臺日新月異,但萬變不離其宗的是那些算法和理論。
- 算法和理論永遠是程序員最重要的內功,禿頭也得學呀
記錄部分 當時的思路或者學習其他優秀作者的思路,也幫自己重新梳理邏輯,爭取加深印象,加油 (^-^)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;
}
== 優化前執行結果==
優化
執行用時的誤差還是比較小的,執行的結果還不是特別令人滿意,我希望可以優化一下。優化無非希望他可以減少遍歷不可能的答案,當已經定位了a
和d
,我們便可以得出接下來獲得的最大和、最小和。
最大和a+d*3
<target ===那麼l和r不再遍歷
最小和a*3+d
>target ===那麼l和r不再遍歷
那麼優化其實只要再l,r遍歷之前加上方框內的判斷條件:
其中target1
爲target
減去a
和d
優化後執行結果
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;
}