面試 15:順時針從外往里打印數字(劍指 Offer 第 20 題)


面試 15:順時針從外往里打印數字

題目:輸入一個矩陣,按照從外向里以順時針的順序依次打印每一個數字。例如輸入: {{1,2,3}, {4,5,6}, {7,8,9}} 則依次打印數字為 1、2、3、6、9、8、7、4、5

這是昨天最后給大家留下的題目,相信大家也有去思考如何處理這道題目了。

初看這個題目,比較容易理解,也無需牽扯到數據結構或者高級的算法,看起來問題比較簡單,但實際上解決起來且並沒有想象中的容易。

大家極有可能想到循環嵌套的方式,套用幾個 for 循環就可以啦。

  1. 首先打印第 1 行,然后第一個 for 循環從第一列打印到最后一列。
  2. 到最后一列,開始向下打印,為了防止重復打印第一行最后一列的數字,所以應該從第二行開始打印;
  3. 上面步驟 2 到底的時候,再在最后一行從倒數第二列開始往前打印一直到第一列;
  4. 用步驟 3 到最后一行第一列的時候再往上打印,第一行第一列由於步驟 1 已經打印過,所以這次只需要從倒數第二行第一列開始打印到順數第二行第一列即可;
  5. 然后里面其實是一樣的,不難看出里面其實就是對一個更小的矩陣重復上面的步驟 1 到步驟 4;
  6. 由於之前說了一定注意邊界值,所以我們再步驟 1 之前嚴格注意一下傳入矩陣為 null 的情況。

思路想好了,所以開始下筆寫起代碼:

public class Test15 { private static void print(int[][] nums) { if (nums == null) return; int rows = nums.length; int columns = nums[0].length; // 因為一次循環后 里面的矩陣會少 2 行,所以我們步長應該設置為 2 // 因為一次循環后 里面的矩陣會少 2 行,所以我們步長應該設置為 2 for (int i = 0; i * 2 < rows || i * 2 < columns; i++) { // 向右打印,i 代表第 i 行,用 j 代表列,從 0 到 列數-1-2*i for (int j = i; j < columns - 2 * i; j++) { System.out.print(nums[i][j] + ","); } // 向下打印,j 代表行,列固定為最后一列-i*2 for (int j = i + 1; j < rows - 2 * i; j++) { System.out.print(nums[j][rows - 1 - 2 * i] + ","); } // 向左打印,j 代表列,行固定為最后一列-i*2 for (int j = rows - 2 - 2 * i; j >= 2 * i; j--) { System.out.print(nums[rows - 1 - 2 * i][j] + ","); } // 向上打印,j 代表行,列固定為第一列 +i*2 for (int j = rows - 2 - 2 * i; j > 2 * i; j++) { System.out.print(nums[j][2 * i] + ","); } } } public static void main(String[] args) { int[][] nums = {{1, 2, 3}, {4,5,6}, {7,8,9}}; print(nums); } 復制代碼

上面的代碼可能大家會覺得看的很繞,實際上我也很暈,在這種很暈的情況下通常是極易出現問題的。不信?不妨我們分析來看看。

  1. 首先我們做了 null 的輸入值判斷,挺好的,這沒問題;
  2. 然后我們做了一個循環,輸出看成一個環一個環的輸出,因為輸出完成一個環后總會少 2 行和 2 列,最后一次輸出例外,所以我們給出步長為 2 ,並且中間的判斷采用 || 而不是 &&,這里也沒啥問題;
  3. 我們直接代入題干中的例子試一試。
  4. rows = 3,columns = 3,最外層循環會進行 2 次,符合條件;
  5. 進入第一次循環,第一次打印向右,j 從 0 一直遞增到 2 循環 3 次,打印出 1, 2, 3,沒問題;
  6. 進入第二次循環,本次循環我們希望打印 6,9;我們從 i + 1 列開始,一直到最后一列,正確,沒問題;
  7. 進入第三次循環,測試沒問題,可以正常打印 8,7;
  8. 進入第四次循環,測試沒問題,可以正常打印 4;
  9. 最外層循環進入第二次,此時 i = 1, i < 1,出現錯誤。額,這里循環結束條件應該 i <= columns - 2 * i
  10. ....

不知道小伙伴有沒有被繞暈,反正我已經雲里霧里了,我是誰?我在哪?

各種試,會發現坑還不少,其實上面貼的這個代碼已經是經過上面這樣走流程走了好幾次修正的,但特別無奈,這個坑始終填不滿。

有時候,不得不說,其實能有上面這般思考的小伙伴已經很優秀了,但在算法上還是欠了點火候。在面試中,我們當然希望竭盡全力完成健壯性很棒,又能實現功能的代碼,但不得不說,人都有思維愚鈍的時候,有時候就是怎么也弄不出來。

我們在解題前,其實不妨通過畫圖或者其他的方式先和面試官交流自己的思路,雖然他不會告訴你這樣做對與否。但這其實就形成了一種非常好的溝通方式,當然也是展現你溝通能力的一種體現!

前面的思路其實沒毛病,只是即使我們得到了正解,但這樣的一連串代碼,別說面試官,你自己可能都看的頭大。

我們確實可以用這樣先打印矩陣最外層環,打印完后把里面的再當做一個環,重復外面的情況打印。環的打印次數上面也提了,限制結束的條件就是環數 <= 行數的二分之一 && 環數 <= 列數的 二分之一。

所以我們極易得到這樣的代碼:

private static void print(int[][] nums) { if (nums == null) return; int rows = nums.length; int columns = nums[0].length; for (int i = 0; i * 2 < rows && i * 2 < columns; i++) { printRing(nums, i, rows, columns); } } 復制代碼

我們着重是需要編寫 printRing(nums,i) 的代碼。

仔細分析,我們打印一圈實際上就分為四步:

  1. 從左到右打印一行;
  2. 從上到下打印一列;
  3. 從右到左打印一行;
  4. 從下到上打印一列;

不過值得注意的是,最后一圈有可能退化為只有一行,只有一列,甚至只有 1 個數字,因此這樣的打印並不需要 4 步。下圖是幾個退化的例子,他們打印一圈分別只需要 3 步、2 步 甚至 1 步。

 

劍指 Offer

 

因此我們需要仔細分析打印時每一步的前提條件。

  • 第一步總是需要的,不管你是一個數字,還是只有一行。
  • 如果只有一行,那就不用第二步了,所以第二步能進去的條件是終止的行號大於起始的行號;
  • 如果剛剛兩行並且大於兩列,則可進行第三步打印;
  • 要想進行第四步的話,除了終止列號大於起始行號以外,還得至少有三行。

此外,依然得額外地注意:數組的下標是從 0 開始的,所以尾坐標總是得減 1 ,並且每進行一次循環,尾列和尾行的坐標總是得減去 1。

所以,完整的代碼就奉上了:

public class Test15 { private static void print(int[][] nums) { if (nums == null) return; int rows = nums.length; int columns = nums[0].length; for (int i = 0; i * 2 < rows && i * 2 < columns; i++) { printRing(nums, i, rows, columns); } } private static void printRing(int[][] nums, int start, int rows, int columns) { // 設置兩個變量,endRow 代表當前環尾行坐標;endCol 代表當前環尾列坐標; int endRow = rows - 1 - start; int endCol = columns - 1 - start; // 第一步:打印第一行,行不變列變,列從起到尾 for (int i = start; i <= endCol; i++) { System.out.print(nums[start][i] + ","); } // 假設有多行才需要打印第二步 if (endRow > start) { // 第二步,打印尾列,行變列不變,需要注意的是尾列第一行已經打印過 for (int i = start + 1; i <= endRow; i++) { System.out.print(nums[i][endCol] + ","); } } // 至少兩行並且 2 列才會有第三步逆序打印 if (endCol > start && endRow > start) { // 第三步,打印尾行,行不變,列變。需要注意尾行最后一列第二步已經打印 for (int i = endCol - 1; i >= start; i--) { System.out.print(nums[endRow][i] + ","); } } // 至少大於 2 行 並且大於等於 2 列才會有第四步打印 if (endRow > start && endCol - 1 > start) { // 第四步,打印首列,行變,列不變。需要注意尾行和首行的都打印過 for (int i = endRow - 1; i >= start + 1; i--) { System.out.print(nums[i][start] + ","); } } } public static void main(String[] args) { int[][] nums = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; print(nums); } } 復制代碼

用自己准備的測試用例輸入測試,沒有問題,通過。

上面的代碼中用兩個變量 endRowendCol 以及畫圖完美地解決了我們思路混亂並且代碼難以看明白的問題。「其實不用吐槽判斷方法有重復的情況,我們都是為了看起來思路更加清晰。

只看不練,很明顯這樣的題是容易被繞進去的,思路其實我們很好想到,但實現出來完全是另外一回事,所以大家不妨再去動手試試吧~

緊張之余,還是要留下明天的習題,記得提前思考和動手練習喲~

面試題:輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如:壓入序列為{1,2,3,4,5},那{4,5,3,2,1} 就是該棧的彈出順序,而{4,3,5,1,2} 明顯就不符合要求;




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM