首頁>技術>

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

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

題意

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

如果最後答案有多層, 輸出最淺的那一層,樹的深度不會超過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;        }    }    return maxDep;}

這段程式碼也不長,一共只有幾行,我們一行一行來看。第一行當中我們建立了一個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的習慣,不是所有情況下我們都可以有除錯工具可以使用的,有的時候逼自己一把,不妨也是一種進步的途徑。

10
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 用資料庫實現了一個分散式鎖,雖簡陋,但能用