秋招接近尾聲,我總結了 牛客、WanAndroid 上,有關筆試面經的帖子中出現的算法題,結合往年考題寫了這一系列文章,全部文章均與 LeetCode 進行覈對、測試。歡迎食用css
本文將覆蓋 「字符串處理」 + 「動態規劃」 方面的面試算法題,文中我將給出:html
倉庫地址:超級乾貨!精心概括視頻、歸類、總結
,各位路過的老鐵支持一下!給個 Star !
java
如今就讓咱們開始吧!android
字符串普遍應用 在 Java
編程中,在 Java
中字符串屬於對象,Java
提供了 String
類來建立和操做字符串。面試中的字符串處理問題,主要是對於字符串各類方法的靈活應用。下面結合實例,講講常見的考點:git
給定 n
,表示有 n
對括號, 請寫一個函數以將其生成全部的括號組合,並返回組合結果。github
例如面試
給出 n = 3,生成結果爲: [ "((()))", "(()())", "(())()", "()(())", "()()()" ]
使用 回溯法算法
只有在咱們知道序列仍然保持有效時才添加 '(' or ')',而不是像 方法一 那樣每次添加。咱們能夠經過跟蹤到目前爲止放置的左括號和右括號的數目來作到這一點,編程
若是咱們還剩一個位置,咱們能夠開始放一個左括號。 若是它不超過左括號的數量,咱們能夠放一個右括號。數組
public List<String> generateParenthesis(int n) { List<String> res = new ArrayList<>(); helper(n, n, "", res); return res; } // DFS private void helper(int nL, int nR, String parenthesis, List<String> res) { // nL 和 nR 分別表明左右括號剩餘的數量 if (nL < 0 || nR < 0) { return; } if (nL == 0 && nR == 0) { res.add(parenthesis); return; } helper(nL - 1, nR, parenthesis + "(", res); if (nL >= nR) { return; } helper(nL, nR - 1, parenthesis + ")", res); }
給定一個正整數,返回相應的列標題,如Excel表中所示。如:
1 -> A,
2 -> B
...
26 -> Z,
27 -> AA
示例 :
輸入: 28 輸出: "AB"
%26
這樣獲得的就是一個 0%26 + 64
,也就是 0 + ‘A’ = 'A'
,正確答案固然是 ‘Z’,因而加了一堆判斷public String convertToTitle (int n) { StringBuilder str = new StringBuilder(); while (n > 0) { n--; str.append ( (char) ( (n % 26) + 'A')); n /= 26; } return str.reverse().toString(); }
給定一個只包含兩種
字符的字符串:+和-,你和你的小夥伴輪流翻轉"++"變成"--"。當一我的沒法
採起行動時遊戲結束,另外一我的將是贏家。編寫一個函數,計算字符串在一次有效移動後的全部
可能狀態。
示例 :
輸入:s = "++++" [ "--++", "+--+", "++--" ]
第二個
字母開始遍歷+
,和以前那個字母是否爲+
翻轉
後的字符串存入結果中便可public List<String> generatePossibleNextMoves (String s) { List list = new ArrayList(); // indexOf 方法使用 看下方拓展 for (int i = -1; (i = s.indexOf ("++", i + 1)) >= 0;) { list.add (s.substring (0, i) + "--" + s.substring (i + 2)); } return list; }
Java中字符串中子串的查找共有四種方法,以下:
int indexOf(String str)
:返回第一次出現的指定子字符串在此字符串中的索引。int indexOf(String str, int startIndex)
:從指定的索引處開始,返回第一次出現的指定子字符串在此字符串中的索引。int lastIndexOf(String str)
:返回在此字符串中最右邊出現的指定子字符串的索引。int lastIndexOf(String str, int startIndex)
:從指定的索引處開始向後搜索,返回在此字符串中最後一次出現的指定子字符串的索引。substring()
方法返回字符串的子字符串。
public String substring(int beginIndex)
返回 beginIndex 後的字符串public String substring(int beginIndex, int endIndex)
返回 beginIndex 到 endIndex 之間的字符串
給定一個字符串,逐個翻轉字符串中的每一個單詞。
示例 :
輸入: "a good example" 輸出: "example good a" 解釋: 若是兩個單詞間有多餘的空格,將反轉後單詞間的空格減小到只含一個。
split
方法,以 「 」 爲標識符爲基準拆分字符串public String reverseWords(String s) { if(s.length() == 0 || s == null){ return " "; } //按照空格將s切分 String[] array = s.split(" "); StringBuilder sb = new StringBuilder(); //從後往前遍歷array,在sb中插入單詞 for(int i = array.length - 1; i >= 0; i--){ if(!array[i].equals("")) { // 爲防止字符串首多一個 「 」 判斷當前是否是空字符串 // 是字符串第一個就不輸出空格 if (sb.length() > 0) { sb.append(" "); } sb.append(array[i]); } } return sb.toString(); }
實現atoi這個函數,將一個字符串轉換爲整數。若是沒有合法
的整數,返回0。若是整數超出
了32位整數的範圍,返回INT_MAX(2147483647)
若是是正整數,或者 INT_MIN(-2147483648)
若是是負整數。
示例 :
輸入: "4193 with words" 輸出: 4193 解釋: 轉換截止於數字 '3' ,由於它的下一個字符不爲數字。 示例 4: 輸入: "words and 987" 輸出: 0 解釋: 第一個非空字符是 'w', 但它不是數字或正、負號。 所以沒法執行有效的轉換。
trim()
去掉空格‘+’ ‘-’
其餘falg
叫作sign
默認值爲一,若是監測到 ‘-’ 則設爲 -1結果
乘以 sigh 就能帶上正負值答案數值
兩種
狀況-+
號,根據題意直接退出循環*10
倍,再將其加入 sum 中MAX_VALUE
跳出循環*sigh
輸出正負值,或者MAX_VALUE
或MIN_VALUE
便可public int myAtoi(String str) { if(str == null) { return 0; } str = str.trim(); if (str.length() == 0) { return 0; } int sign = 1; int index = 0; if (str.charAt(index) == '+') { index++; } else if (str.charAt(index) == '-') { sign = -1; index++; } long num = 0; for (; index < str.length(); index++) { if (str.charAt(index) < '0' || str.charAt(index) > '9') { break; } num = num * 10 + (str.charAt(index) - '0'); if (num > Integer.MAX_VALUE ) { break; } } if (num * sign >= Integer.MAX_VALUE) { return Integer.MAX_VALUE; } if (num * sign <= Integer.MIN_VALUE) { return Integer.MIN_VALUE; } return (int)num * sign; }
注:trim() 函數是去掉String字符串的首尾空格;
編寫一個函數來查找字符串數組中的最長公共前綴。
若是不存在公共前綴,返回空字符串 ""。
示例 :
輸入: ["flower","flow","flight"] 輸出: "fl"
標籤:鏈表
當字符串數組長度爲 0
時則公共前綴爲空,直接返回
令最長公共前綴ans
的值爲第一個字符串,進行初始化
遍歷後面的字符串,依次將其與 ans
進行比較,兩兩找出公共前綴,最終結果即爲最長公共前綴
若是查找過程當中出現了ans
爲空的狀況,則公共前綴不存在直接返回
s
爲全部字符串的長度之和
public String longestCommonPrefix(String[] strs) { if (strs == null || strs.length == 0) { return ""; } String prefix = strs[0]; for(int i = 1; i < strs.length; i++) { int j = 0; while (j < strs[i].length() && j < prefix.length() && strs[i].charAt(j) == prefix.charAt(j)) { j++; } if( j == 0) { return ""; } prefix = prefix.substring(0, j); } return prefix; }
\(O(s)\)
判斷一個正整數是否是迴文數。迴文數的定義是,將這個數反轉以後,獲得的數仍然是同一個數。
示例 :
輸入: 121 輸出: true
經過取整和取餘操做獲取整數中對應的數字進行比較。
舉個例子:1221
這個數字。
經過計算1221 / 1000
, 得首位1
經過計算1221 % 10
, 可得末位 1
進行比較
再將 22 取出來繼續比較
public boolean palindromeNumber(int num) { // Write your code here if(num < 0){ return false; } int div = 1; while(num / div >= 10){ div *= 10; } while(num > 0){ if(num / div != num % 10){ return false; } num = (num % div) / 10; div /= 100; } return true; }
動態規劃
經常適用於有重疊子問題和最優子結構
性質的問題,動態規劃方法所耗時間每每遠少於樸素
解法。其背後的基本思想很是簡單。大體上,若要解一個給定問題,咱們須要解其不一樣部分(即子問題),再根據子問題的解以得出原問題的解。
一般許多子問題很是類似,爲此動態規劃法試圖僅僅解決每一個子問題一次,從而減小計算量:一旦某個給定子問題的解已經
算出,則將其記憶化存儲,以便下次須要同一個子問題解之時直接查表。這種作法在重複子問題的數目關於輸入的規模呈指數增長
時特別有用。
給定字符串 s 和單詞字典 dict
,肯定 s 是否能夠分紅一個或多個以空格分隔的子串,而且這些子串都在字典中存在。
示例 :
輸入: s = "applepenapple", wordDict = ["apple", "pen"] 輸出: true 解釋: 返回 true 由於 "applepenapple" 能夠被拆分紅 "apple pen apple"。 注意你能夠重複使用字典中的單詞。
這個方法的想法是對於給定的字符串 s 能夠被拆分
成子問題 s1 和 s2 。若是這些子問題均可以獨立地被拆分紅符合要求的子問題,那麼整個問題 s 也能夠知足。也就是,若是 \(catsanddog\) 能夠拆分紅兩個子字符串 "\(catsand\)" 和 "\(dog\)" 。子問題 "\(catsand\)" 能夠進一步拆分紅 "\(cats\)" 和 "\(and\)" ,這兩個獨立的部分都是字典的一部分,因此 "\(catsand\)" 知足題意條件,再往前, "\(catsand\)" 和 "\(dog\)" 也分別知足條件,因此整個字符串 "\(catsanddog\)" 也知足條件。
如今,咱們考慮 \(dp\) 數組求解的過程:
2
個下標指針 \(i\) 和 \(j\) ,其中 \(i\) 是當前字符串從頭開始的子字符串\((s')\)的長度, \(j\) 是當前子字符串\((s')\)的拆分位置,拆分紅 \(s'(0,j)\)和 \(s'(j+1,i)\)。s1'
和 s2'
(注意 i
如今指向 s2'
的結尾)。s1′
是否知足題目要求。若是知足,咱們接下來檢查 s2′
是否在字典中。若是包含,咱們接下來檢查 s2′
是否在字典中,若是兩個字符串都知足要求,咱們讓 \(dp[i]\) 爲 \(true\) ,不然令其爲 \(false\) 。public boolean wordBreak(String s, List<String> wordDict) { Set<String> wordDictSet=new HashSet(wordDict); boolean[] dp = new boolean[s.length() + 1]; dp[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = 0; j < i; j++) { if (dp[j] && wordDictSet.contains(s.substring(j, i))) { dp[i] = true; break; } } } return dp[s.length()]; }
時間複雜度:\(O(n^2)\) 。求出 \(dp\) 數組須要兩重循環。
空間複雜度:\(O(n)\)。\(dp\) 數組的長度是 \(n+1\)。
假設你正在爬樓梯。須要 n
階你才能到達樓頂。
每次你能夠爬 1 或 2
個臺階。你有多少種不一樣的方法能夠爬到樓頂呢?
注意:給定 n
是一個正整數。
示例 :
輸入: 3 輸出: 3 解釋: 有三種方法能夠爬到樓頂。 1 階 + 1 階 + 1 階 1 階 + 2 階 2 階 + 1 階
感受這題相似斐波那契數列。不難發現,這個問題能夠被分解爲一些包含最優子結構的子問題,即它的最優解能夠從其子問題
的最優解來有效地構建,咱們可使用動態規劃來解決這一問題。
第 \(i\) 階能夠由如下兩種
方法獲得:
在第 \((i−1)\) 階後向上爬 1
階。
在第 \((i−2)\) 階後向上爬 2
階。
因此到達第 \(i\) 階的方法總數就是到第 \((i−1)\) 階和第 \((i−2)\) 階的方法數之和
。
令 \(dp[i]\) 表示能到達第 \(i\) 階的方法總數
:
\(dp[i]=dp[i-1]+dp[i-2]\)
\(dp[i]=dp[i−1]+dp[i−2]\)
public int climbStairs(int n) { if (n == 0) return 0; int[] array = new int[n + 1]; array[0] = 1; if (array.length > 1) { array[1] = 1; } for(int i = 2; i < array.length; i++) { array[i] = array[i - 1] + array[i - 2]; } return array[n]; }
假設你是一個專業的竊賊,準備沿着一條街打劫房屋。每一個房子都存放着特定金額的錢。你面臨的惟一約束條件是:相鄰的房子裝着相互聯繫的防盜系統,且 當相鄰的兩個房子同一天被打劫時,該系統會自動報警。給定一個非負整數列表,表示每一個房子中存放的錢, 算一算,若是今晚去打劫,在不觸動報警裝置的狀況下, 你最多能夠獲得多少錢 。
示例 :
輸入: [2,7,9,3,1] 輸出: 12 解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。 偷竊到的最高金額 = 2 + 9 + 1 = 12 。
考慮全部可能的搶劫方案過於困難。一個天然而然的想法是首先從最簡單的狀況開始。記:
\(f(k) =\) 從前
k
個房屋中能搶劫到的最大數額,\(A_i\) = 第i
個房屋的錢數。
首先看 n = 1
的狀況,顯然 f(1) = \(A_1\) 。
再看 n = 2
,\(f(2) = max(A_1 , A_2 )\)。
對於 n = 3
,有兩個選項:
搶第三個房子,將數額與第一個房子相加。
不搶第三個房子,保持現有最大數額。
顯然,你想選擇數額更大的選項。因而,能夠總結出公式:
\(f(k) = max(f(k – 2) + A_k , f(k – 1))\)
咱們選擇 \(f(–1) = f(0) = 0\) 爲初始狀況,這將極大地簡化代碼。
答案爲 \(f(n)\)。能夠用一個數組來存儲並計算結果。不過因爲每一步你只須要前兩個最大值,兩個變量就足夠用了。
public long houseRobber(int[] A) { if (A.length == 0) return 0; long[] res = new long[A.length + 1]; res[0] = 0; res[1] = A[0]; for (int i = 2; i < res.length; i++) { res[i] = Math.max(res[i - 2] + A[i - 1], res[i - 1]); } return res[A.length]; }
時間複雜度:\(O(n)\)。其中 n
爲房子的數量。
空間複雜度:\(O(1)\)。
給出兩個單詞word1
和word2
,計算出將 word1
轉換爲word2
的最少操做次數。你總共三種操做方法:插入一個字符、刪除一個字符、替換一個字符。
示例 :
輸入: word1 = "horse", word2 = "ros" 輸出: 3 解釋: horse -> rorse (將 'h' 替換爲 'r') rorse -> rose (刪除 'r') rose -> ros (刪除 'e') 輸入: word1 = "intention", word2 = "execution" 輸出: 5 解釋: intention -> inention (刪除 't') inention -> enention (將 'i' 替換爲 'e') enention -> exention (將 'n' 替換爲 'x') exention -> exection (將 'n' 替換爲 'c') exection -> execution (插入 'u')
咱們的目的是讓問題簡單化,好比說兩個單詞 horse
和 ros
計算他們之間的編輯距離 D
,容易發現,若是把單詞變短會讓這個問題變得簡單,很天然的想到用 D[n][m]
表示輸入單詞長度爲 n
和 m
的編輯距離。
具體來講,D[i][j]
表示 word1
的前 i
個字母和 word2
的前 j
個字母之間的編輯距離。
當咱們得到 D[i-1][j],D[i][j-1] 和 D[i-1][j-1] 的值以後就能夠計算出 D[i][j]。
每次只能夠往單個或者兩個字符串中插入一個字符
那麼遞推公式很顯然了
若是兩個子串的最後一個字母相同,word1[i] = word2[i] 的狀況下:
\(D[i][j] = 1 + \min(D[i - 1][j], D[i][j - 1], D[i - 1][j - 1] - 1)\)
\(D[i][j]=1+min(D[i−1][j],D[i][j−1],D[i−1][j−1]−1)\)
不然,word1[i] != word2[i]
咱們將考慮替換最後一個字符使得他們相同:
\(D[i][j] = 1 + \min(D[i - 1][j], D[i][j - 1], D[i - 1][j - 1])\)
\(D[i][j]=1+min(D[i−1][j],D[i][j−1],D[i−1][j−1])\)
因此每一步結果都將基於上一步的計算結果,示意以下:
同時,對於邊界狀況,一個空串和一個非空串的編輯距離爲 D[i][0] = i
和 D[0][j] = j
。
綜上咱們獲得了算法的所有流程。
舒適提示,若是思惟很差理解的話,把解題思路記清楚就行
public int minDistance(String word1, String word2) { // write your code here int n = word1.length(); int m = word2.length(); int[][] dp = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++){ dp[i][0] = i; } for (int j = 0; j < m + 1; j++){ dp[0][j] = j; } for (int i = 1; i< n + 1; i++){ for (int j = 1; j < m + 1; j++){ if (word1.charAt(i - 1) == word2.charAt(j - 1)){ dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])); } } } return dp[n][m]; }
時間複雜度 :\(O(m n)\),兩層循環顯而易見。
空間複雜度 :\(O(m n)\),循環的每一步都要記錄結果。
給定一個整數數組 nums ,找出一個序列中乘積最大的連續子序列(該序列至少包含一個數)。
示例 :
輸入: [-2,0,-1] 輸出: 0 解釋: 結果不能爲 2, 由於 [-2,-1] 不是子數組。
imax
爲當前最大值,則當前最大值爲 imax = max(imax * nums[i], nums[i])
imin
,imin = min(imin * nums[i], nums[i])
imax
與imin
進行交換再進行下一步計算public int maxProduct(int[] nums) { int max = Integer.MIN_VALUE, imax = 1, imin = 1; for(int i=0; i<nums.length; i++){ if(nums[i] < 0){ int tmp = imax; imax = imin; imin = tmp; } imax = Math.max(imax*nums[i], nums[i]); imin = Math.min(imin*nums[i], nums[i]); max = Math.max(max, imax); } return max; }
本片文章篇幅總結越長。我一直以爲,一片過長的文章,就像一堂超長的 會議/課堂,體驗很很差,因此我打算再開一篇文章
在後續文章中,我將繼續針對鏈表
棧
隊列
堆
動態規劃
矩陣
位運算
等近百種,面試高頻算法題,及其圖文解析 + 教學視頻 + 範例代碼
,進行深刻剖析有興趣能夠繼續關注 _yuanhao 的編程世界
不求快,只求優質,每篇文章將以 2 ~ 3 天的週期進行更新,力求保持高質量輸出
「面試原題 + 圖文詳解 + 實例代碼」二叉搜索樹-雙指針-貪心 面試題彙總
面試高頻算法題彙總「圖文解析 + 教學視頻 + 範例代碼」之 二分 + 哈希表 + 堆 + 優先隊列 合集
🔥面試必備:高頻算法題彙總「圖文解析 + 教學視頻 + 範例代碼」必知必會 排序 + 二叉樹 部分!🔥
每一個人都要學的圖片壓縮終極奧義,有效解決 Android 程序 OOM
Android 讓你的 Room 搭上 RxJava 的順風車 從重複的代碼中解脫出來
ViewModel 和 ViewModelProvider.Factory:ViewModel 的建立者
單例模式-全局可用的 context 對象,這一篇就夠了
縮放手勢 ScaleGestureDetector 源碼解析,這一篇就夠了
Android 屬性動畫框架 ObjectAnimator、ValueAnimator ,這一篇就夠了
看完這篇再不會 View 的動畫框架,我跪搓衣板