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

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

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

初看這個(gè)題目,比較容易理解,也無需牽扯到數(shù)據(jù)結(jié)構(gòu)或者高級(jí)的算法,看起來問題比較簡單,但實(shí)際上解決起來且并沒有想象中的容易。

大家極有可能想到循環(huán)嵌套的方式,套用幾個(gè) for 循環(huán)就可以啦。

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

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

public class Test15 {

    private static void print(int[][] nums) {
        if (nums == null)
            return;

        int rows = nums.length;
        int columns = nums[0].length;
        // 因?yàn)橐淮窝h(huán)后 里面的矩陣會(huì)少 2 行,所以我們步長應(yīng)該設(shè)置為 2
        // 因?yàn)橐淮窝h(huán)后 里面的矩陣會(huì)少 2 行,所以我們步長應(yīng)該設(shè)置為 2
        for (int i = 0; i * 2 < rows || i * 2 < columns; i++) {
            // 向右打印,i 代表第 i 行,用 j 代表列,從 0 到 列數(shù)-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);
    }

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

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

不知道小伙伴有沒有被繞暈,反正我已經(jīng)云里霧里了,我是誰?我在哪?

各種試,會(huì)發(fā)現(xiàn)坑還不少,其實(shí)上面貼的這個(gè)代碼已經(jīng)是經(jīng)過上面這樣走流程走了好幾次修正的,但特別無奈,這個(gè)坑始終填不滿。

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

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

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

我們確實(shí)可以用這樣先打印矩陣最外層環(huán),打印完后把里面的再當(dāng)做一個(gè)環(huán),重復(fù)外面的情況打印。環(huán)的打印次數(shù)上面也提了,限制結(jié)束的條件就是環(huán)數(shù) <= 行數(shù)的二分之一 && 環(huán)數(shù) <= 列數(shù)的 二分之一。

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

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) 的代碼。

仔細(xì)分析,我們打印一圈實(shí)際上就分為四步:

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

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

劍指 offer

因此我們需要仔細(xì)分析打印時(shí)每一步的前提條件。

  • 第一步總是需要的,不管你是一個(gè)數(shù)字,還是只有一行。
  • 如果只有一行,那就不用第二步了,所以第二步能進(jìn)去的條件是終止的行號(hào)大于起始的行號(hào);
  • 如果剛剛兩行并且大于兩列,則可進(jìn)行第三步打印;
  • 要想進(jìn)行第四步的話,除了終止列號(hào)大于起始行號(hào)以外,還得至少有三行。

此外,依然得額外地注意:數(shù)組的下標(biāo)是從 0 開始的,所以尾坐標(biāo)總是得減 1 ,并且每進(jìn)行一次循環(huán),尾列和尾行的坐標(biāo)總是得減去 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) {
        // 設(shè)置兩個(gè)變量,endRow 代表當(dāng)前環(huán)尾行坐標(biāo);endCol 代表當(dāng)前環(huán)尾列坐標(biāo);
        int endRow = rows - 1 - start;
        int endCol = columns - 1 - start;

        // 第一步:打印第一行,行不變列變,列從起到尾
        for (int i = start; i <= endCol; i++) {
            System.out.print(nums[start][i] + ",");
        }
        // 假設(shè)有多行才需要打印第二步
        if (endRow > start) {
            // 第二步,打印尾列,行變列不變,需要注意的是尾列第一行已經(jīng)打印過
            for (int i = start + 1; i <= endRow; i++) {
                System.out.print(nums[i][endCol] + ",");
            }
        }
        // 至少兩行并且 2 列才會(huì)有第三步逆序打印
        if (endCol > start && endRow > start) {
            // 第三步,打印尾行,行不變,列變。需要注意尾行最后一列第二步已經(jīng)打印
            for (int i = endCol - 1; i >= start; i--) {
                System.out.print(nums[endRow][i] + ",");
            }
        }  
        // 至少大于 2 行 并且大于等于 2 列才會(huì)有第四步打印
        if (endCol > start && endRow - 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);
    }
}

用自己準(zhǔn)備的測試用例輸入測試,沒有問題,通過。

上面的代碼中用兩個(gè)變量 endRowendCol 以及畫圖完美地解決了我們思路混亂并且代碼難以看明白的問題?!?strong>其實(shí)不用吐槽判斷方法有重復(fù)的情況,我們都是為了看起來思路更加清晰。」

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

緊張之余,還是要留下明天的習(xí)題,記得提前思考和動(dòng)手練習(xí)喲~

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容