【好書推薦】《劍指Offer》之硬技能(程式設計題12~16)
本文例子完整原始碼地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword
《【好書推薦】《劍指Offer》之軟技能》
《【好書推薦】《劍指Offer》之硬技能(程式設計題1~6)》
《【好書推薦】《劍指Offer》之硬技能(程式設計題7~11)》
持續更新,敬請關注公眾號:coderbuff,回覆關鍵字“sword”獲取相關電子書。
12.矩陣中的路徑
題目:請設計一個函式,用來判斷一個矩陣中是否存在一條包含其字串所有字元的路徑。路徑可以從矩陣中的任意一格開始,每一步可以在矩陣中向左、右、上、下移動一格。如果一條路徑經過了矩陣的某一格,那麼該路徑不能再次進入該格子。*回溯法:適合由多個步驟組成的問題,並且每個步驟有多個選項。
1 /** 2 * 矩陣中是否存在給定路徑 3 * @author OKevin 4 * @date 2019/6/4 5 **/ 6 public class Solution { 7 8 /** 9 * 10 * @param matrix 一位陣列表示矩陣 11 * @param rows 行數 12 * @param cols 列數 13 * @param path 路徑 14 * @return true-存在;false-不存在 15 */ 16 public boolean findPath(char[] matrix, Integer rows, Integer cols, char[] path) { 17 if (matrix == null || rows <= 0 || cols <= 0 || path == null) { 18 return false; 19 } 20 boolean[] visited = new boolean[rows * cols]; 21 int pathLength = 0; 22 for (int row = 0; row < rows; row++) { 23 for (int col = 0; col < cols; col++) { 24 if (findPathCore(matrix, rows, cols, row, col, path, pathLength, visited)) { 25 return true; 26 } 27 } 28 } 29 return false; 30 } 31 32 private boolean findPathCore(char[] matrix, Integer rows, Integer cols, int row, int col, char[] path, int pathLength, boolean[] visited) { 33 if (pathLength == path.length) { 34 return true; 35 } 36 if (row >= 0 && row < rows && col >= 0 && col < cols && matrix[row * cols + col] == path[pathLength] && !visited[row * cols + col]) { 37 visited[row * cols + col] = true; 38 pathLength++; 39 if (findPathCore(matrix, rows, cols, row, col - 1, path, pathLength, visited) 40 || findPathCore(matrix, rows, cols, row - 1, col, path, pathLength, visited) 41 || findPathCore(matrix, rows, cols, row, col + 1, path, pathLength, visited) 42 || findPathCore(matrix, rows, cols, row + 1, col, path, pathLength, visited)) { 43 return true; 44 } 45 visited[row * cols + col] = false; 46 47 } 48 return false; 49 } 50 }
13.機器人的運動範圍
題目:地上有一個m行n列的小方格,一個機器人從座標(0,0)的格子開始移動,它每次可以向上、下、左、右移動一格,但不能進入行座標和列座標的數位之和大於k的格子。例如:當k=18,機器人能夠進入方格(35,37),因為3+5+3+7=18,但不能進入(35,38),因為3+5+3+8=19。請問k=18時,機器人能夠到達多少個格子。此題有一個小的點需要靠平時的積累,數位和的計算。
1 /** 2 * 計算數位和 3 * 例如:85的數位和為8+5=13 4 * 計算過程: 5 * 85 % 10 = 5(個位) 6 * 85 / 10 = 8(移除個位) 7 * 8 % 10 = 8(十位) 8 * 5 + 8 = 13 9 * @param number 數字 10 * @return 數位和 11 */ 12 private int getDigitSum(int number) { 13 int sum = 0; 14 while (number > 0) { 15 sum += number % 10; 16 number /= 10; 17 } 18 return sum; 19 }
另外還需要注意幾個臨界條件:
-
訪問的行和列一定是大於等於0;
-
訪問的行和列一定是小於總行數和總列數(並不是小於等於,因為是從第0行開始)
-
行和列的數位和小於閾值
-
沒有被訪問過
row >= 0 && row < rows && col >= 0 && col < cols && (getDigitSum(row) + getDigitSum(col) < threshold) && !visited[row * cols + col]
題目中看似提到了m行n列,立馬想到了用二維數字來表示。實際上如果用二維陣列是增加了複雜性,用一維陣列同樣能表示出二維陣列。例如:m行n列就一共又m*n個元素,visited[m*n]。訪問第1行第1列,在一維陣列中則為visited[1*m+1],訪問第1行第2列則為visited[1*m+2],也就是在一位陣列中,資料是按照一列一列存放的。如果要訪問第2行是2*cols+第幾列。
另外既然需要求出達到多少個格子,則是需要訪問格子周圍即:(i - 1, j)、(i, j - 1)、(i + 1, j)、(i, j + 1)。
1 /** 2 * Description: 3 * 機器人的運動範圍 4 * 2019-06-18 5 * Created with OKevin. 6 */ 7 public class Solution { 8 public int movingCount(int threshold, int rows, int cols) { 9 if (threshold < 0 || rows <= 0 || cols <= 0) { 10 return 0; 11 } 12 boolean[] visited = new boolean[rows * cols]; 13 int count = movingCountCore(threshold, rows, cols, 0, 0, visited); 14 return count; 15 } 16 17 private int movingCountCore(int threshold, int rows, int cols, int row, int col, boolean[] visited) { 18 int count = 0; 19 if (check(threshold, rows, cols, row, col, visited)) { 20 visited[row * cols + col] = true; 21 /** 22 * 當前訪問到了(i, j)座標,此時則繼續訪問(i - 1, j)、(i, j - 1)、(i + 1, j)、(i, j + 1) 23 */ 24 count = 1 + movingCountCore(threshold, rows, cols, row - 1, col, visited) + movingCountCore(threshold, rows, cols, row, col-1, visited) + movingCountCore(threshold, rows, cols, row + 1, col, visited) + movingCountCore(threshold, rows, cols, row + 1, col, visited); 25 } 26 return count; 27 } 28 29 private boolean check(int threshold, int rows, int cols, int row, int col, boolean[] visited) { 30 //橫座標與縱座標的數位和相加小於閾值,且沒有訪問過 31 if (row >= 0 && row < rows && col >= 0 && col < cols && (getDigitSum(row) + getDigitSum(col) <= threshold) && !visited[row * cols + col]) { 32 return true; 33 } 34 return false; 35 } 36 37 /** 38 * 計算數位和 39 * 例如:85的數位和為8+5=13 40 * 計算過程: 41 * 85 % 10 = 5(個位) 42 * 85 / 10 = 8(移除個位) 43 * 8 % 10 = 8(十位) 44 * 5 + 8 = 13 45 * @param number 數字 46 * @return 數位和 47 */ 48 private int getDigitSum(int number) { 49 int sum = 0; 50 while (number > 0) { 51 sum += number % 10; 52 number /= 10; 53 } 54 55 return sum; 56 } 57 }
14.剪繩子
題目:一段長度為n的繩子,請把繩子剪成m段(m、n都是整數,n>1且m>1),每段繩子的長度為k[0]、k[1]、……、k[m]。請問k[0]*k[1]*……*k[m]可能的最大乘積是多少?例如,當繩子的長度為8時,我們把它剪成長度分別為2,3,3的三段,此時的最大乘積是18。這道題是求解最優化問題。理論上講,在題目中出現最大、最小、一共有多少種解法都可以用動態規劃求解。
解法一:動態規劃
拿到這道題,習慣性的可能會先從由上往下的解題思路去想,比如:長度為9,可以分為幾段:1,1,7;1,2,6等等。會去思考這個長度會分成幾個段,再將每個段的乘積求出來,取最大的那個段。
但實際上,對於求解最優化問題,可以轉換為一系列子問題。對於本題一段繩子來講,它無論如何都至少被切為2段。例如長度為8時,可能被切為:1,7;2,6;3,5;4,4。當然還有5,3,這實際上又和前面重複了,所以一段繩子如果被切為2段,就只有n/2種可能性。
切為2段並不是最終的最大乘積長度,例如8切為了以上4種可能性的兩段,並不意味著8的切成m段的最大乘積長度為15(3*5)。它當然還能切為2*3*3=18。那為什麼說只需要切為2段呢?
這是因為我們需要把這個問題不斷地劃分為小的問題。
例如8被切為了1和7,這兩段不能再繼續切分,它就是最小的問題;同理,8被切為了2和6,但是6仍然可以繼續被切為1和5,2和4,3和3,所以2和6並不是最小的問題,以此類推,最終推出長度為6的繩子切成m段的最大乘積是9(3*3),那麼8被切為2和6時,2*9就等於18。同理繼續推3和5,4和4。
上面的分析得出了什麼樣的結論呢?結論就是,只需要想象成2段,再各自繼續切2段。也就是說假設長度為n的繩子,f(n)是它的各段最大乘積長度,它在被切第一刀時,第一段長度為(1,2,...n-1),第二段的長度為(n-1,n-2,...,1)。推出f(n)=max(f(i)*f(n-1))的關聯關係。這裡一定需要好好理解,切成2段後,並不是直接將兩段相乘,而是再繼續將各段切分直至不能再切且取最大乘積長度。
在《演算法筆記》(刁瑞 謝妍著)一書中對動態規劃做了求解步驟的總結:
-
定義子問題
-
定義狀態轉換規則,即遞推關係
-
定義初始狀態
套用到這套題上,我認為就是需要明確以下3點:
-
該問題的核心在於求出每段的最大乘積長度,這是子問題,也就是上文所述,再被切為兩段時,需要明確是否能繼續切直至不能再切且取最大乘積長度。
-
遞推關係,也已明確(n)=max(f(i)*f(n-1))
-
初始狀態,長度為1不能切,長度為2最長為1,長度為3最長為2。
1 /** 2 * Description: 3 * 剪繩子——動態規劃 4 * 2019-06-19 5 * Created with OKevin. 6 */ 7 public class Solution1 { 8 9 public int maxProductAfterCutting(int length) { 10 if (length < 2) { 11 return 0; 12 } 13 if (length == 2) { 14 return 1; 15 } 16 if (length == 3) { 17 return 2; 18 } 19 int[] products = new int[length + 1]; //陣列中儲存的是每段的最優解 20 //大於長度3的繩子,當然可以劃分出1,2,3長度的繩子 21 products[0] = 0; 22 products[1] = 1; 23 products[2] = 2; 24 products[3] = 3; 25 int max = 0; 26 for (int i = 4; i <= length; i++) { 27 max = 0; 28 for (int j = 1; j <= i / 2; j++) { //除以2的原因在上文中也以提到,將一段繩子劃分為2段時,實際上中間後的切分和前面是重複的 29 int product = products[j] * products[i - j]; //遞推關係f(i)*f(n-1) 30 if (max < product) { 31 max = product; 32 } 33 products[i] = max; 34 } 35 } 36 max = products[length]; 37 return max; 38 } 39 }
優點:動態規劃類似於分治演算法,將大的問題逐步劃分為小的問題求解。
缺點:此題採用動態規劃的時間複雜度為O(n^2),且空間複雜度為O(n)
解法二:貪婪演算法
貪婪演算法的核心是,先挑最大的,再挑比較大的,再挑小的(貪婪嘛)。
本題對於長度為n(n>=5)的繩子應儘量多劃分為長度3的段。對於長度為4的段,應劃分為長度為2的段。
也即是,如果長度為10,那麼10/3=3個長度為3的段,劃分結果為3*3*3*1,最後一個段為1,劃分為3*3*4。
1 /** 2 * Description: 3 * 剪繩子——貪婪演算法 4 * 2019-06-20 5 * Created with OKevin. 6 */ 7 public class Solution2 { 8 public int maxProductAfterCutting(int length) { 9 if (length < 2) { 10 return 0; 11 } 12 if (length == 2) { 13 return 1; 14 } 15 if (length == 3) { 16 return 2; 17 } 18 int timesOf3 = length / 3; 19 if (length - timesOf3 * 3 == 1) { 20 timesOf3 -= 1; 21 } 22 int timesOf2 = (length - timesOf3*3) / 2; 23 return (int) (Math.pow(3, timesOf3) * Math.pow(2, timesOf2)); 24 } 25 }
15.二進位制中1的個數
題目:請實現一個函式,輸入一個整數,輸出該數二進位制表示中1的個數。例如,把9表示成二進位制是1001,有2位是1。因此,如果輸入9,則該函式輸出2。此題可採用移位運算+與運算求解
1 /** 2 * Description: 3 * 移位運算+與運算 4 * 2019-06-20 5 * Created with OKevin. 6 */ 7 public class Solution { 8 public int NumberOf1(int num) { 9 int count = 0; 10 while (num != 0) { 11 if ((num & 1) == 1) { 12 count++; 13 } 14 num = num >>> 1; //因為運算>>>表示無符號右移,意味著如果是負數,仍然會向右移,同時用0補齊。如果使用>>有符號右移,那麼符號位1永遠會存在,也就是會產生死迴圈 15 } 16 return count; 17 } 18 }
16.數值的整數次方
題目:實現函式Math.pow,求m的n次方。迴圈暴力法
1 /** 2 * Description: 3 * 迴圈暴力法 4 * 2019-06-20 5 * Created with OKevin. 6 */ 7 public class Solution1 { 8 public int pow(int m, int n) { 9 int result = 1; 10 for (int i = 0; i < n; i++) { 11 result *= m; 12 } 13 return result; 14 } 15 }
很遺憾,這種解法連校招級都算不上,頂多算是剛學習程式設計時的水平。
其實這道題,並沒有考查過多的演算法,更多的是考查對細節的把握。一個數的整數次方,不光是整數,還有可能是負數,也有可能是0。如果數值為0,則0的冪是沒有意義的。
1 /** 2 * Description: 3 * 考慮指數為0,負數,整數;數值為0的情況;0^0在數學上沒有意義 4 * 2019-06-21 5 * Created with OKevin. 6 */ 7 public class Solution2 { 8 9 public double pow(int m, int n) { 10 double result = 0; 11 if (m == 0 && n < 0) { 12 return -1; 13 } 14 int absN = Math.abs(n); //取絕對值 15 result = calc(m, absN); 16 if (n < 0) { 17 result = 1 / result; 18 } 19 return result; 20 } 21 22 private int calc(int m, int n) { 23 int result = 1; 24 for (int i = 0; i < n; i++) { 25 result *= m; 26 } 27 return result; 28 } 29 }
改進後的程式碼考慮到了指數是負數的情況。但實際上這仍然有優化的空間。如果指數是32,意味著calc方法需要迴圈31次。然而實際上迴圈到一半的時候就可以求它本身。也就是說a^n/2 * a^n/2,n為偶數;a^(n-1)/2 * a^(n-1)/2 * a,n為奇數。
改進後的calc方法:
1 private int calc(int m, int n) { 2 if (n == 0) { 3 return 1; 4 } 5 if (n == 1) { 6 return m; 7 } 8 int result = calc(m, n >> 1); //右移1位表示除以2 9 result *= result; 10 if ((m & 1) == 1) { //位運算判斷是會否為奇數,奇數的二進位制第一位一定是1與1做與運算即可判斷是否為奇數,代替m%2是否等於0 11 result *= m; 12 } 13 return result; 14 }
本文例子完整原始碼地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword
《【好書推薦】《劍指Offer》之軟技能》
《【好書推薦】《劍指Offer》之硬技能(程式設計題1~6)》
《【好書推薦】《劍指Offer》之硬技能(程式設計題7~11)》
持續更新,敬請關注公眾號:coderbuff,回覆關鍵字“sword”獲取相關電子書。
這是一個能給程式設計師加buff的公眾號 (CoderBuff)