字節跳動不按照常理出牌,居然筆試的時候出這種題目 題意 題解 整理邏輯 把握主次 仔細謹慎 結尾

要說今天的題目,着實把我嚇了一跳。它實在是太不按照常理出牌了,我還是第一次見過這樣的題目,剛看到的時候覺得它有點不講武德。但是仔細想想,這題的立意以及考點還是很不錯的。用在筆試當中也非常合適,一樣有區分度。

好了,我們廢話不多說了,直接來看題吧。

題意

給定一棵樹的根節點, 在已知該樹最大深度的情況下, 求節點數最多的那一層並返回具體的層數。

如果最後答案有多層, 輸出最淺的那一層,樹的深度不會超過100000。實現代碼如下,請指出代碼中的多處錯誤:

struct Node {
    vector<Node*> sons;
};

 

void dfsFind(Node *node, int dep, int counter[]) {
    counter[dep]++;

    for(int i = 0; i < node.sons.size(); i++) {
        dfsFind(node.sons[i], dep, counter);
    }
}

 

int find(Node *root, int maxDep) {

    int depCounter[100000];
    dfsFind(root, 0, depCounter);

    int max, maxDep;
    for (int i = 1; i <= maxDep; i++) {
        if (depCounter[i] > max) {
            max = depCounter[i];
            maxDep = i;
        }
    }
    return maxDep;
}

題解

怎麼樣,是不是有點沒有想到?

爲什麼說這題出得好,就在這裏。除了自己寫代碼debug之外,對於工程師而言,閱讀並且理解其他人的代碼和邏輯也非常重要。不僅如此,想要準確地找出代碼當中的bug,也需要我們有豐富的編碼經驗。經驗不足的話,一些隱藏比較深的問題是看不出來的。而且它只是給了我們代碼,並沒有給我們測試的環境以及數據,說白了所有的問題都需要我們通過肉眼發現,對於很多不習慣肉眼debug的同學來說還是有一定難度的。

所以這道題還是很考驗同學們的水平的,真正有多少斤兩,一下子就被試出來了。

結合這道題呢,我也會分享一些我個人的關於閱讀代碼的一些技巧和方法,希望可以幫助到大家。

整理邏輯

首先我們先來看這道題目,題意我們都很好理解,也就是計算樹上每一層的元素個數,找到元素個數最多的那一層。那麼很簡單,我們把每一層的元素數量存儲下來,最後遍歷一下找到最大的。

整個題意和邏輯都算是比較清楚的,我們可以看到,題目給我們的代碼一共包含兩個函數以及一個結構體。結構體是定義的數據格式,和主體的運算邏輯關聯不大,我們可以首先搞定。結構體當中只有一個Node指針的vector,這也是一個通過指針實現樹的一個經典的寫法。

struct Node {
    vector<Node*> sons;
};

排除了結構體的問題之後,我們接下來關注的重點就是兩個函數了。

把握主次

我們剩下要搞定的兩個函數分別是find函數以及dfsFind函數,簡單略讀一下代碼就可以發現,這兩個函數之間是存在一個調用的關係的。在find函數當中調用了dfsFind函數執行了遞歸,所以find函數就是dfsFind的上層。從邏輯上來說,dfsFind執行了是通過遞歸計算每一層元素個數的工作,而find函數則是通過遍歷dfsFind得到的結果,最終進行返回。

所以這兩個函數存在一個很明顯的主次關係,當我們梳理出了代碼的主次關係之後,不同的人有不同的習慣。有些人喜歡先細枝後主幹,有些人喜歡先主幹後細節。我個人比較傾向於後者,先閱讀主幹邏輯,再去細扣具體的實現細節。所以我們先把dfsFind放在一邊,具體來看一下find函數當中的邏輯。

int find(Node *root, int maxDep) {

    int depCounter[100000];
    dfsFind(root, 0, depCounter);

    int max, maxDep;
    for (int i = 1; i <= maxDep; i++) {
        if (depCounter[i] > max) {
            max = depCounter[i];
            maxDep = i;
        }
    }

這段代碼也不長,一共只有幾行,我們一行一行來看。第一行當中我們創建了一個depCounter的數組,數組的長度是1000000。看起來沒有問題對嗎,其實有一點問題。因爲題目沒有明說樹深從1還是從0開始,如果從0開始,並且樹深爲1000000的話,需要1000001長度的數組纔可以存儲,我們這裏創建了1000000會越界引發錯誤。

一般來說我們習慣申請數組的時候,額外多幾個單位的長度可以防止極端情況下的越界。

第二行是一個遞歸調用,目前看起來沒有問題,因爲我們還沒有具體查看遞歸函數內部的實現邏輯。第三行是聲明瞭兩個變量,變量的聲明部分大家很容易一眼掃過,但其實變量聲明的地方因此反而變得隱蔽,經常有bug很難發現。這裏的變量聲明就是錯誤的,這裏聲明瞭兩個變量,兩個變量都錯了。

第一個是max是C++中的保留關鍵詞,我們一般不用這種關鍵字爲變量取名,第二個maxDep和find函數傳入的參數maxDep重名了。這樣是無法通過編譯的,因爲編譯器無法區分兩者。所以我們需要對這兩個變量更換名字,比如一個更換成maxi,另外一個更換成maxIdx。

接下來是一個循環,通過循環找到數組中的最大元素的下標。這段邏輯非常簡單,以至於很多人看不出來這裏埋了雷。埋地雷在maxi上,maxi是在函數find當中創建的局部變量,在C++當中系統是不會爲局部變量初始化的。所以這裏的maxi被聲明的時候到底默認值是多少是無法得知的。因爲我們需要通過maxi找到最大值,所以我們一開始的時候需要爲它初始化。

改完之後的代碼應該是這樣的:

int find(Node *root, int maxDep) {

    int depCounter[100000];
    dfsFind(root, 0, depCounter);

    int maxi=0, maxIdx=0;
    for (int i = 1; i <= maxDep; i++) {
        if (depCounter[i] > maxi) {
            maxi = depCounter[i];
            maxIdx = i;
        }
    }
    return maxDep;
}

仔細謹慎

最後我們閱讀一下dfsFind函數,這個函數也簡單,只有三行,實現了一個遞歸統計的功能。很多人一樣會匆匆一眼掃過去會發現好像沒什麼問題。但魔鬼都藏在細節裏,越是看起來沒問題的地方越可能有問題,一定要仔細謹慎,千萬不可以想當然。

void dfsFind(Node *node, int dep, int counter[]) {
    counter[dep]++;

    for(int i = 0; i < node.sons.size(); i++) {
        dfsFind(node.sons[i], dep, counter);
    }

這一段代碼當中有兩個問題,我先說其中比較明顯的,這裏比較明顯的問題是我們遞歸的時候傳入的不應該是dep而應該是dep+1。這裏的dep表示樹深,我們處理完當前深度的節點之後,再往下顯然處理的應該是下一層節點,既然是下一層節點,那麼它的深度顯然應該需要+1。

第二個問題比較隱蔽,需要非常仔細才能發現,關於變量node。這裏的node類型是Node的指針,對於指針類型的變量,我們是不可以通過.操作去訪問它指向的結構體當中的成員變量的,想要訪問只能通過->符號來訪問,下面遞歸的時候的傳參同樣有這個問題。由於我們是眼睛來閱讀代碼,而不是編譯器編譯,所以很容易忽略這個細節。而且還有些同學可能對於C++的指針本身就不是很熟悉,這樣一來就更難發現了。

最後,我們來貼一下所有找到的bug。

void dfsFind(Node *node, int dep, int counter[]) {
    counter[dep]++;

    for(int i = 0; i < node->sons.size(); i++) {   // node需要需要->符號
        dfsFind(node->sons[i], dep+1, counter);    // node需要用->符號,dep+1替換dep
    }
}

 

int find(Node *root, int maxDep) {

    int depCounter[100005];  // 數組長度增加5
    dfsFind(root, 0, depCounter); 

    int maxi = 0, maxIdx = 0; // 變量重新命名,並且初始化
    for (int i = 1; i <= maxDep; i++) {
        if (depCounter[i] > maxi) {
            maxi = depCounter[i];
            maxIdx = i;
        }
    }
    return maxIdx;
}

結尾

這一段短短的邏輯當中,我們一共找到了大大小小七八個bug。這些bug當中有些很明顯,有些則相對隱蔽,想要全部找齊的確不是一件容易的事情,一不小心就遺漏了。

不僅是筆試做題,我們在日常的編碼過程當中也是一樣的,對於我們編寫的代碼一定要小心謹慎,抱有精益求精不斷追求卓越的心思,纔可以讓寫出來的代碼質量越來越好。另外我們也要養成肉眼debug的習慣,不是所有情況下我們都可以有調試工具可以使用的,有的時候逼自己一把,不妨也是一種進步的途徑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章