截至2020年6月2號,牛客+LeetCode,一共刷了170道左右。從3月底開始每天早上雷打不動地刷兩道算法,已經成為了個習慣,即使以后上班了也會保持這個習慣,但是題量可能會降到每天一道。也許日常開發中算法用的不多,但是刷多了算法,自然而然的就養成了一個寫代碼非常嚴謹、追求簡潔的心態。而且對各種數據結構和位運算的應用也越來越熟悉。
雖然很多大佬說吃透LeetCode Top100+劍指Offer,解決開發崗的筆面試中的算法題問題應該不大。但是問題就在於這個吃透,我現在回過頭去做以前做過的題依舊很多不會,但比起第一次刷明顯有個區別就是:起碼有一些思路了。但是這還不夠,下一步我就打算分類刷題,比如DFS、BFS、動態規划、雙指針等等,每個類型的吃透。
剛開始刷真的幾度懷疑自己的智商,自己沒思路,很多題解也看不懂,即使看懂了也得花半天,這里的半天是真半天,不是形容詞。后來想了想,這么下去不行,2道算法題就弄一早上,其他的任務還做不做了。后來想了個方法:如果題目有思路就自己先嘗試寫,如果想了一會還是完全沒思路就直接看題解,題解一看就懂的,明白題解意思后自己去敲,如果題解都看的費勁直接Copy代碼到IDEA一步一步調試着看。
下面就簡單的記錄一下DFS、BFS等算法的總結,注:這篇不是正式算法總結,過段時間我會先看看其他大神寫的這些算法類型的博客,然后結合自己刷過的題,再寫一篇正式的DFS、動態規划等等類型的總結博客。
1.雙指針
雙指針一般用在數組或鏈表當中,還要求在原數組中進行操作,也就是額外空間復雜度為O(1)。雙指針顧名思義就是定義兩個指針,在Java代碼中就是兩個索引變量。可以看看下面這道題(劍指Offer21題):
public class No21調整數組順序使奇數位於偶數前面 { /** * 輸入一個整數數組,實現一個函數來調整該數組中數字的順序, * 使得所有奇數位於數組的前半部分,所有偶數位於數組的后半部分。 * 示例: * 輸入:nums = [1,2,3,4] * 輸出:[1,3,2,4] * 注:[3,1,2,4] 也是正確的答案之一。 * */ /** * 思路:雙指針。 * */ public int[] exchange(int[] nums) { //左指針 int left=0; //右指針 int right=nums.length-1; while(left<right){ if((nums[left]&1)==0){ nums[left]^=nums[right]; nums[right]^=nums[left]; nums[left]^=nums[right]; right--; }else{ left++; } } return nums; } }
左右指針定義在兩頭,當左右指針相遇,也就是left<right時循環結束,這個算法整體思路很簡單:奇數放在左指針位置,左指針+1.偶數放在右指針位置,右指針-1.
其中用到了兩個位運算技巧,下面就分享幾個常用的位運算:
2的n次方:
2<<2=8//n=3,后面的數為n-1 2<<3=16//n=4
判斷奇偶性
10&1=0//n=10 9&1=1//n=9
不用輔助參數交換兩個數
a=a^b; b=b^a; a=a^b;
整數除以2
num>>1//等同於num/2
上面那道示例題兩個指針是從頭和尾出發的,還有的可能都從頭出發,實際的要看不同的題目。可以在LeetCode題庫頁面的標簽分類中選擇雙指針類型,自己練習一下。
有的雙指針可能還分為快慢指針,如下題,注釋就算題解說明:
public class No234回文鏈表 { /** * 請判斷一個鏈表是否為回文鏈表。 * 示例 1: * 輸入: 1->2 * 輸出: false * 示例 2: * 輸入: 1->2->2->1 * 輸出: true * */ /** * 思路:定義一個快指針一個慢指針 * 慢指針一次走一步,快指針一次走兩步 * 當快指針都到鏈表結尾,慢指針一定在鏈表中間,為了確保鏈表長度為偶數時,慢指針在左邊,所以快指針從第2個節點開始 * 等快指針到達結尾,創建一個棧把慢指針后面的入棧,這樣出棧順序如果和慢指針前面的數一樣,那就是回文 * */ public boolean isPalindrome(ListNode head) { if(head==null) return true; if(head.next==null) return true; Stack<ListNode> stack=new Stack<>(); ListNode lower=head; ListNode quick=head.next; while (quick.next!=null){ lower=lower.next; if(quick.next.next==null) break; quick=quick.next.next; } ListNode left=lower.next; while (left!=null){ stack.push(left); left=left.next; } while (!stack.isEmpty()){ if(stack.pop().val!=head.val){ return false; } head=head.next; } return true; } }
2.動態規划
動態規划一般都會定義一個dp數組,根據不同題目的需求,有可能是二維的有可能是一維的。dp數組的意思一般都是當前索引位置滿足要求的值,那么數組最后一個值就是最終結果。比如經典的跳台階問題,即給你n階台階,一次只能跳1階或2階,問你一共有多少種方法跳完n階台階。做過這道題的朋友可能知道,這其他就是個斐波那契數列,但是還可以用動態規划來做,即定義一個長度為n+1的dp數組,為什么是n+1?因為我們要用到索引n,而數組索引是從0開始的,所以得+1。定義完數組后,dp[1]=1,代表到1階台階只有一種方法,dp[2]=2,代表到2階台階有兩種方法,即:跳兩個1階或一個2階。dp[3]=dp[1]+dp[2],為什么是dp[1]+dp[2],因為在第1階台階上跳2階就到3階了,在第2階台階上跳1階就到3階了。以此類推,dp[n]就是最終結果。
這么說可能不明白,看看這一題:
public class No198打家劫舍 { /** * 你是一個專業的小偷,計划偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。 * 給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。 * 輸入: [2,7,9,3,1] * 輸出: 12 * 解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。 * 偷竊到的最高金額 = 2 + 9 + 1 = 12 。 * */ /** * 思路:動態規划 * dp數組代表當前位置,能偷盜的最大值 * 如2 7 9 3 1 * 初始化dp[0]=2.dp[1]=7。前兩個位置很好確定。 * 當i=2,因為不能連着偷,所以只能用前兩個位置的dp加上本位置和前一個dp比較 * 如果小於就取前一個dp,如果大於就取dp[i-2]+nums[i] * */ public static int rob(int[] nums) { int len=nums.length; if(len==0) return 0; if(len==1) return nums[0]; int[] dp=new int[len]; dp[0]=nums[0]; dp[1]=nums[1]>nums[0]?nums[1]:nums[0]; for(int i=2;i<len;i++){ if(dp[i-2]+nums[i]>dp[i-1]) dp[i]=dp[i-2]+nums[i]; else dp[i]=dp[i-1]; } return dp[len-1]; } public static void main(String[] args) { int[] nums={2,7,9,3,1}; System.out.println(rob(nums)); } }
3.DFS和BFS
DFS:深度優先搜索。BFS:廣度優先搜索。一般用在圖、樹、矩陣中。深度優先搜索就是一條路走到底,要么得到正確結果,要么路徑錯誤,進行回溯。廣度優先搜索就是在岔路口記錄每個可行的路口,然后廣撒網,同時鋪開。這個同時鋪開是怎么做到的?這里就用到了隊列。以二叉樹為例。當前節點設為n1,它的兩個子樹節點n2和n3都符合要求,如果是DFS的話,會選擇其中一條走到底,如果不符合要求會一直回溯到n1,然后再選擇另一個節點。如果是BFS會把n2和n3入隊。然后n2出隊,又得到n2的兩個子樹節點n4和n5,它們也符合要求,接着把它們也入隊。此時隊列為:n3,n4,n5。接着n3出隊,以此類推,是不是就類似於廣度上的鋪開了。
這題以DFS為例:
public class No13機器人的運動范圍 { /** * 地上有一個m行n列的方格,從坐標 [0,0] 到坐標 [m-1,n-1] 。 * 一個機器人從坐標 [0, 0] 的格子開始移動,它每次可以向左、右、上、下移動一格(不能移動到方格外), * 也不能進入行坐標和列坐標的數位之和大於k的格子。例如,當k為18時,機器人能夠進入方格 [35, 37] , * 因為3+5+3+7=18。但它不能進入方格 [35, 38],因為3+5+3+8=19。請問該機器人能夠到達多少個格子? * 示例 1: * 輸入:m = 2, n = 3, k = 1 * 輸出:3 * */ /** * 思路:DFS * DFS:關鍵在於選擇、標記和終止條件,這道題比較特殊,不用回溯,只需要統計,有的題只有一種正確路徑的,就需要在遞歸后回溯一下 * 所謂回溯,就是刪除前一步的改變,比如前一步令變量count+1了,或者令標記數組發生改變,如果遞歸后發現前一步不滿足要求,return以后 * 就要令count-1,復原標記數組。如果一直沒發生回溯,一直到滿足最終要求,說明當前就是正確路徑。 * 接着開頭說的,關鍵在於選擇、標記和終止條件。標記就是一個數組,標記走過的地方,因為一般都不能重復走同一個點,但在二叉樹中一般不需要標記,因為只能往子樹走。 * 選擇就是有幾條路可以走,比如這題中的上下左右,但並不是都能滿足,所以一定要認真找出判斷條件 * 終止條件其實就是正確路徑的判斷,比如count==10,就算正確路徑,那么一般就要在dfs方法最前就判斷一下,滿足的話直接返回並改變某個變量,如上一題中的result * * BFS:除了DFS的一些關鍵點,還需要用隊列來滿足其特性,即廣度,因為需要走滿足條件的所有條件。如,我發現1 2 3可以走 * 那么就可以在隊列中加入1 2 3,然后根據出隊順序依次去探索1 2 3,如果在1里面又發現了路徑4 5,接着入隊4 5,此時隊列是:2 3 4 5 * 接着探索2,如果發現了其他路徑,接着入隊。 * 可以發現,BFS就如其名,廣度優先搜索,探索的路徑都是廣度鋪開的。而深度優先搜索就是一條路走到低,直到滿足條件或者不滿足條件回溯。 * */ static int count=1; static boolean[][] flag; public static int movingCount(int m, int n, int k) { flag=new boolean[m][n]; flag[0][0]=true; dfs(0,0,m,n,k); return count; } private static void dfs(int curM,int curN,int m,int n,int k){ //右 if(curN+1<n&&!flag[curM][curN+1]&&helper(curM,curN+1,k)){ flag[curM][curN+1]=true; count++; dfs(curM,curN+1,m,n,k); } //左 if(curN-1>=0&&!flag[curM][curN-1]&&helper(curM,curN-1,k)){ flag[curM][curN-1]=true; count++; dfs(curM,curN-1,m,n,k); } //上 if(curM-1>=0&&!flag[curM-1][curN]&&helper(curM-1,curN,k)){ flag[curM-1][curN]=true; count++; dfs(curM-1,curN,m,n,k); } //下 if(curM+1<m&&!flag[curM+1][curN]&&helper(curM+1,curN,k)){ flag[curM+1][curN]=true; count++; dfs(curM+1,curN,m,n,k); } return; } private static boolean helper(int curM,int curN,int k){ int m=0; int n=0; while (curM>0){ m+=curM%10; curM/=10; } while (curN>0){ n+=curN%10; curN/=10; } return m+n>k?false:true; } public static void main(String[] args) { System.out.println(movingCount(8,5,6)); } }
就寫這么多吧。可能很多地方表達的不是很規范嚴謹,畢竟我也才是剛刷兩個多月的菜鳥。這篇也不指望能幫到什么人,只是看到很多人都說剛開始刷算法心態都或多或少崩過,但只要堅持下來還是會有一些改變的。