跳轉到內容

C++ 程式設計作為一系列問題/第一章

來自華夏公益教科書

簡介 - 解決漢諾塔

[編輯 | 編輯原始碼]
一個初始位置的 10 環漢諾塔謎題。

漢諾塔是一個相當簡單的謎題。它主要由三個杆和一組環組成。環有不同的尺寸,從最大到最小堆疊在第一個杆上。你的目標:將所有環移動到第二個或第三個塔。聽起來很簡單?規則:你只能移動給定塔上的最頂部的環,並且不能將更大的環放在較小的環上面。如果你已經知道如何獲得解決方案,你可以跳過下一部分。

讓我們從漢諾塔的簡單版本開始,我們將以此為基礎構建更大的版本。讓我們從兩個環的漢諾塔開始。我相信你應該能夠輕鬆地將兩個環移動到目標位置。我們將這些環分別稱為 #1 和 #2。#1 堆疊在 #2 上,位於第一個杆上。我們希望將 #1 和 #2 都移動到第二個杆上;所以最終,我們必須將 #2 移動到第二個杆上。#2 是最底部的環,因此它不能放在任何其他東西的頂部;第二個杆必須是空的。因此,從這一點出發,我們制定出一個解決方案。我們將 #1 環移動到另一個杆上,然後將 #2 移動到目標杆上,然後將 #1 移動回目標杆上。

現在,讓我們進入一個更大的示例。4 環漢諾塔怎麼樣?我們將嘗試使用前面示例中的一些知識。首先,我們必須在解決方案的某個時刻將 #4 環移動到第二個杆上,為此,第二個杆必須是空的。為了使 #4 可移動,它的上面不能有任何東西,所以我們必須先將 #1、#2 和 #3 堆疊在另一個杆上。現在我們可以將 #4 移動到目標杆上,並在它的上面重新組裝塔。

解決方案變得稍微複雜了。我們不能簡單地拿起 #1、#2 和 #3。我們必須根據相同的規則移動它們。所以我們應用相同的模式,#3 必須移動到另一個杆上,而那個杆必須清空比 #3 數字小的環,也就是說沒有 #2 或 #1。所以,另一個杆變成了我們的目標杆,只是我們移動的環更少。看到規律了嗎?它被稱為遞迴模式。所以,讓我們定義一個基本規則:要使用c 作為備用杆,將n 個環從杆a 移動到杆b,首先我們將n-1 個環使用相同的規則移動到杆c,然後我們將環n 移動到杆b,然後我們將n-1 個環從杆c 移動回杆b。注意,當 n=0 時,我們應該什麼也不做。現在,讓我們將它變成一個程式。

自豪地,你的第一個程式

[編輯 | 編輯原始碼]

開啟你喜歡的文字編輯器,建立一個名為“hanoi.cpp”的新檔案。此檔案將包含我們的原始碼

首先,我們告訴自己存在一個遞迴模式。這個模式似乎使用了四個變數,n、a、b、c。我們應該給它們起一些更好的名字,比如depthfromtoalternate。所以,讓我們建立一個函式。函式,就像數學中的函式一樣,只是一組計算機要遵循的指令。它有一個名稱,不像數學中所有函式都叫“f”或“x”,這毫無意義。我們將函式命名為“hanoi”。在你的文字程式中輸入或貼上以下內容

 void hanoi(int depth, int from, int to, int alternate)
 {
 }

讓我們花點時間分析一下。這行程式碼的第一部分是“void”。這意味著什麼?實際上,這行程式碼的第一部分是返回型別。這是函式將返回的內容,即它的返回值,它的結果。型別僅僅意味著一種資料型別,例如一個數字、一個字母、一個字串(一系列組成句子和內容的字母)甚至我們自己的資料型別(我們將在後面探討)。void 型別表示無,也就是說,此函式沒有結果,它會做其他一些事情,例如將結果輸出給我們,而不是返回它。這行程式碼的第二部分是“hanoi”,正如你可能猜到的,它是函式的名稱。

然後是一對括號,“(”和“)” ,在它們內部是一組引數。你可能也猜到這些引數是我們之前命名的:depth、from、to 和 alternate。它們用逗號隔開,這是合乎邏輯的。但是,你可能想知道int 部分是什麼?請記住,我之前說過所有資料都有型別。int 是一種編譯器認識的型別,它代表整數。因此,我們的四個變數是整數,因為它們代表負數或正數。depth 是一個數字,代表系統應該移動的環的數量。from、to 和 alternate 都是整數,用於指定我們正在使用哪個杆。事實上,這些也可以用字串來完成,這樣輸出將更像“將環 #3 從塔 a 移動到塔 b”,而不是“將環 #3 從塔 1 移動到塔 2”。我們稍後可以這樣做。

接下來的幾行完全由一個左大括號“{”和一個右大括號“}”組成。這兩個括號表示函式內部的內容,函式執行的操作。

現在,我們將開始建立函式。還記得我們的規則嗎?它的一部分說,如果 n 或depth 為 0,我們應該什麼也不做。所以,讓我們先做到這一點。在括號之間新建一行,我們將新增一個if 語句。if 語句包含幾個部分,主要遵循以下模式

 if(something)
 { ..do something.. }

你可以注意到,“..do something..” 只有在“something” 為真時才會執行。在我們的例子中,“something” 是“depth 為 0”,而“do something” 是什麼也不做,聽起來很奇怪。因此,我們的程式碼是

 if(depth == 0)
 {
     return;
 }

我已經聽到尖叫聲了!你大喊“等等,這與模式不匹配!它應該在一行上,而不是三行!”。讓我解釋一下。你可以在程式碼中的任何地方新增空格和換行符,絕對任何地方,除了在一些愚蠢的地方,比如“if” 中的“i”和“f”之間,或者在“depth” 內部。但在大多數其他地方,它都是完全可以的。所以你可能會問,“為什麼還要這樣做?”。將程式碼分隔開有很多明顯的優勢。它使程式碼更易讀。你會發現,隨著你變得更有經驗,你將花費超過三分之二的時間來閱讀你在過去某個時間點編寫的程式碼,而不是在那裡和那時編寫程式碼。你會花很多時間尋找程式碼中的錯誤或計算機錯誤。所以,你的程式碼易讀變得至關重要。

話雖如此,我們應該討論一下程式碼。首先,我們if 語句中的“something” 是“depth == 0”。你可能想知道為什麼有兩個等號而不是一個。這是因為只使用一個等號將改變 depth,“depth = 0” 會導致 depth 變為 0,而我們丟失了 depth 的舊值。兩個等號表示比較,我們正在將 depth 0 比較,如果為真,我們將執行return 語句,否則我們將跳過括號中包含的所有內容。return 語句會導致函式立即返回,這意味著在該函式中沒有更多操作要執行,因此在該函式中不會執行更多程式碼。這正是我們想要的:如果 depth 為 0,則不再執行任何操作,只需返回上一級。還要注意,return 語句在末尾有一個分號 (;),而 if 語句沒有。記住這一點非常重要:所有語句(除了使用括號的語句)都必須以分號結尾。

所以,進入下一部分。首先,我們將第 n 個環之上的所有環移動到備用杆上。所以,基本上,函式正在呼叫自身來執行此操作。新增以下程式碼

 hanoi(depth-1, from, alternate, to);

你可以看到,我們正在再次呼叫 hanoi 函式。但是,我們現在交換了 alternate 和 to,以表示我們要將所有上面的環移動到備用杆上,而不是移動到我們要移動底部環的杆上。這很簡單。

進入下一部分。我們必須將第 n 個環從from 移動到to。讓我們決定如何執行此操作。我們不是在程式碼中實際執行此操作 - 這裡沒有 Pole 資料,我們也沒有在杆之間移動 Ring 資料,我們只是發出命令,以便站在三個杆前面的讀者可以自己執行操作。我們將在後面討論更高階的形式。現在,新增以下程式碼

 std::cout << "Move ring " << depth << " from pole " << from << " to pole " << to << "." << std::endl;

這是將輸出我們的資訊的程式碼。讓我們分析一下。首先,它以“std::cout” 開頭。“std::” 部分指定我們正在使用標準庫中的某些東西。標準庫包含許多有用的東西,並且每個 C++ 實現都包含它。“cout” 部分表示我們正在使用標準庫中的cout,它代表“字元輸出”。下一部分是“<<”。這被稱為格式運算子。它將右邊的內容傳送到左邊的內容。這個運算子幾乎專門用於“std::cout”,儘管如果你真的想這樣做,你也可以將其用於自己的目的。下一部分是“Move ring ”。這僅僅意味著我們將文字“Move ring ” 傳送到 std::cout,而 std::cout 又會將它輸出,以便我們看到。

然後你看到另一個“<<”,所以我們向 std::cout 傳送更多內容,只是這次我們不是傳送要精確複製的文字,而是傳送深度,這意味著無論 depth 的值是多少,無論是 17、42 還是 15843,它都會輸出該數字。下一部分是另一段文字。再次注意,開頭有一個空格。這是因為當程式輸出“depth”時,它不會在開頭和結尾新增空格,因為那可能不是你想要的。因此,你必須手動新增空格。該行的其餘部分很容易理解;我們只是輸出更多資訊。然而,最後一部分是 std::endl。標準庫中的另一件東西。“endl”代表“end-line”,它使該行結束,並將所有先前的輸出傳送到螢幕。

讓我們繼續我們模式的下一部分。現在我們必須將所有環,現在應該在備用柱子上,移回目標柱子上。為此,我們使用與第一次使用類似的命令

 hanoi(depth-1, alternate, to, from);

我們再次交換了東西。我們從備用柱子移到目標柱子(命名為“to”),並將從柱子用作新的備用柱子。感到困惑了嗎?我希望沒有。現在事情已經結束了,所有環應該都在正確的柱子上,所以這是我們函式的結尾。完整的函式如下所示

 void hanoi(int depth, int from, int to, int alternate)
 {
     if(depth == 0)
     {
         return;
     }
     hanoi(depth-1, from, alternate, to);
     std::cout << "Move ring " << depth << " from pole " << from << " to pole " << to << "." << std::endl;
     hanoi(depth-1, alternate, to, from);
 }

你可以記下我如何對事物進行空格,因為它可能不像你在文字編輯器中那樣。你可以看到,對於每個“{”括號,我都會將內容縮排四個空格。同樣,這是一種風格選擇,但人們普遍認為四個空格可以使程式碼看起來整潔有序,同時仍然允許大量程式碼在螢幕上同時顯示。

恐怖還沒有結束!我們只有函式。我們還沒有告訴計算機我們要用該函式做什麼。我們還忘記了包含一些標頭檔案,但我們稍後會詳細介紹。現在,讓我們新增以下程式碼

 int main(int argc, char** argv)
 {
     hanoi(4, 1, 2, 3);
     return 0;
 }

我們建立了一個新函式,main,它返回一個整數。main 函式是特殊的,它指定程式的入口點。它接受兩個引數。第一個我們可以很容易地弄清楚 - 它是另一個整數。第二個有點令人困惑。“char** 的東西是什麼?”好吧,“char”代表字元。但兩個星號以某種方式改變了這一點。我現在還不能完全解釋它做了什麼,這相當複雜。現在,只要確保它代表在命令列中傳遞給函式的引數即可。

讓我們跳入函式體。它似乎執行了我們的 hanoi 函式,它提供了一個深度為 4 的深度,並且它希望你將環從塔 1 移動到塔 2,使用塔 3 作為備用。然後我們看到了return 語句再次出現,只是這次我們沒有返回任何內容(就像我們在 hanoi 函式中所做的那樣,該函式返回 void,這基本上是空的)。這次我們返回一個 0,它與 int 返回型別相容。這就是我們 main 函式的結尾。看起來很簡單,對吧?

我們只缺少最後一步才能使程式完整。還記得我們如何使用 std::cout 嗎?好吧,編譯器不會僅僅假設 std::cout 存在,你必須請求它。為此,你“包含標頭檔案”。這是透過在檔案的最開頭新增以下內容來完成的

 #include <iostream>

這會在編譯時將 iostream 標頭檔案複製並貼上到你的檔案中,而不是你手動執行此操作(這既容易出錯又浪費時間,並且如果 iostream 標頭檔案發生更改,你將不得不重複整個過程)。你必須記住這一點,std::cout 包含在 iostream 標頭檔案中。如果你想使用 std::cout,你必須 #include iostream 標頭檔案。

這是整個程式。請注意,編譯器可能關心順序,因此“main”應放在“hanoi()”函式定義之後。

 #include <iostream>
 void hanoi(int depth, int from, int to, int alternate)
 {
     if(depth == 0)
     {
         return;
     }
     hanoi(depth-1, from, alternate, to);
     std::cout << "Move ring " << depth << " from pole " << from << " to pole " << to << "." << std::endl;
     hanoi(depth-1, alternate, to, from);
 }
 int main(int argc, char** argv)
 {
     hanoi(4, 1, 2, 3);
     return 0;
 }

我們現在有一個完整的程式!你的第一個!恭喜!我認為是時候測試它了,你覺得呢?現在,開啟一個控制檯,進入 hanoi.cpp 所在的目錄,並輸入“g++ hanoi.cpp -o hanoi”。等待大約 2 秒,你就會得到你的程式。你可以透過執行“./hanoi”來執行它。太棒了,不是嗎?它正在為你提供 4 環漢諾塔的命令。我認為你應該給自己拍拍手,並在準備好時進入下一部分。

擴充套件,一個人/女人還能想要什麼?

[edit | edit source]

所以,我們有一個漢諾塔程式。它告訴我們如何解決漢諾塔。大事。這不會給在 Google 給你面試的“開發主管”留下深刻印象。我們的程式需要變得更加複雜。以下是我們希望程式執行的一些事情

  1. 允許我們在啟動程式時選擇環的數量
  2. 向我們展示實際的漢諾塔,以及環的位置。
  3. 在計算機找到解決方案時,即時更新控制檯上的影像
  4. 不要太快,因為我們想看到解決方案,而不是讓它在一眨眼的功夫內從我們眼前閃過。
華夏公益教科書