第 9 講:循環(huán)的嵌套
嵌套?
什么是嵌套(Nest)?嵌套是 C 語言和其它眾多編程語言里的一個(gè)重要的語法術(shù)語,它表示執(zhí)行的控制流語句(if
、switch
、while
、do-while
和 for
語句統(tǒng)稱控制流語句,它們控制著整個(gè)程序的執(zhí)行流程:是條件判斷來走不同分支,還是反復(fù)執(zhí)行相同的執(zhí)行語句)的內(nèi)部繼續(xù)使用這些控制流語句的寫法。比如前文介紹的 if
內(nèi)還有 if
的情況:
比如這個(gè)寫法格式里的 j
的結(jié)果是 3,它先走了 i == 2
這個(gè) if
條件的 else
部分,然后發(fā)現(xiàn)它也不滿足 i < 2
的情況,所以它最終走了 i < 2
這個(gè) if
條件里的 else
部分,獲得了 j = 3
的賦值。
很有意思的是,循環(huán)也是可以嵌套使用的。正因?yàn)槿绱?,循環(huán)和條件判斷的各種語句才會產(chǎn)生各種不同的組合模式,產(chǎn)生不同的代碼書寫技巧,比如枚舉法(Enumeration)。不過,我們先要介紹,代碼到底是怎么運(yùn)算執(zhí)行嵌套循環(huán)的。
實(shí)例:質(zhì)數(shù)的判斷
最好理解的、最基礎(chǔ)的例子就是求質(zhì)數(shù)了。
質(zhì)數(shù)的定義是,在把一個(gè)大于等于 2 的整數(shù)分解為多個(gè)正整數(shù)的乘積時(shí),如果只能分解為 1 × 自己
的話,這個(gè)數(shù)就是質(zhì)數(shù)(也稱素?cái)?shù),Prime),一般常見的質(zhì)數(shù)有 2、3、5、7、11、13、17、19 等。
現(xiàn)在我們嘗試輸出所有 100 以內(nèi)的所有質(zhì)數(shù)。我們的代碼怎么寫呢?
先實(shí)現(xiàn)判別質(zhì)數(shù)的功能
首先,我們嘗試用爆破的方式,對一個(gè)整數(shù)嘗試進(jìn)行挨個(gè)數(shù)字的試除。如果這個(gè)整數(shù)無法被整除掉,說明它只能被 1 整除掉,故這個(gè)數(shù)肯定就是質(zhì)數(shù)了。比如,我們要計(jì)算 17 是否是質(zhì)數(shù)(即使我們背了表,知道它是質(zhì)數(shù)),邏輯大致就是從 2 開始,一直計(jì)算到 16(17 除以 17 顯然等于 1,所以這個(gè)除法已經(jīng)沒有意義了,所以我們只要求計(jì)算到 16 即可),讓 17 挨個(gè)去除以這個(gè)數(shù),如果每一次除法得到的結(jié)果都不能被整除,說明這個(gè)數(shù)肯定就是質(zhì)數(shù)了):
我們來大致看下邏輯。首先,我們?yōu)樾枰玫降臈l件進(jìn)行變量的聲明(定義),然后設(shè)置一個(gè)結(jié)果量 isPrime
,來表示它是否是質(zhì)數(shù)(1 表示這個(gè)數(shù)是質(zhì)數(shù),0 表示這個(gè)數(shù)不是質(zhì)數(shù))。
然后考慮 while
循環(huán)里的內(nèi)容。我們肯定要讓 n
從 2 計(jì)算到 digit - 1
,然后嘗試記錄每一次試除得到的余數(shù) remainder
。記錄余數(shù)有什么用呢?在 C 語言里,判斷一個(gè)數(shù)是否能被整除,用的方法是得到余數(shù),看余數(shù)是否為 0。比如 12 % 2
的結(jié)果為 0,因?yàn)?12 是可以整除 2 的。
如果 remainder
為 0,說明被整除了。在除法過程里得到完全整除的現(xiàn)象,就標(biāo)志著這個(gè)數(shù)肯定就不是質(zhì)數(shù)了。比如在計(jì)算 15(digit
是 15)的時(shí)候,如果 n
此時(shí)是 3,那么 remainder
就是 0 了,所以 digit
不是質(zhì)數(shù),我們需要將結(jié)果量 isPrime
改為 0。 標(biāo)志著這個(gè)數(shù)肯定不是質(zhì)數(shù)了。
不過,如果在得到
remainder
為 0 的情況時(shí),就標(biāo)志著這個(gè)數(shù)不是質(zhì)數(shù)了。既然它已經(jīng)肯定不是質(zhì)數(shù)了,所以此時(shí)就不必再往下算了,所以我們此時(shí)想要嘗試跳到循環(huán)外面,繼續(xù)執(zhí)行下面的語句。不過,我們目前還沒有講解這一點(diǎn),所以我們先不考慮這一點(diǎn)的優(yōu)化。哪怕后面再次遇到余數(shù) 0 的情況,因?yàn)檠h(huán)里只有對isPrime = 0
的賦值,所以局面完全不可能被反轉(zhuǎn),即isPrime
完全不可能被重新改為 1,所以大不了多幾次被賦值為 0 罷了。另外,循環(huán)條件其實(shí)也可以優(yōu)化,不過為了邏輯的清晰易懂,所以這里我們不提及任何有關(guān)優(yōu)化的部分。
當(dāng)試除循環(huán)執(zhí)行完畢,發(fā)現(xiàn) isPrime
還是為初始賦值的情況 1,就說明在我們試除的循環(huán)里完全沒有遇到余數(shù)為 0,即被整除的情況。所以這個(gè)數(shù) digit
應(yīng)為質(zhì)數(shù),所以我們認(rèn)為這個(gè)數(shù)就是質(zhì)數(shù),輸出 digit
是一個(gè)質(zhì)數(shù)的信息,反之輸出不是質(zhì)數(shù)的信息。
所以,前面給出的算法邏輯是完整的、正確的。
為遍歷 2 到 100 寫一層循環(huán)
那么,我們既然做出了這個(gè)模塊,那么,要想輸出所有質(zhì)數(shù)的信息,只需要把最后 isPrime
的判斷的 if
條件去掉 else
部分,然后把輸出語句改為直接輸出這個(gè)數(shù)字,最后再在整個(gè)執(zhí)行流程的外部加入一個(gè) while
循環(huán),計(jì)算 2 到 100 的所有數(shù)字即可。
可以看到,Old code
代碼塊里面的這一大堆代碼都是前文用過的代碼,被我原封不動(dòng)地復(fù)制過來了。我們把 digit
從 17 改為了 2。因?yàn)槲覀冞@個(gè)題目的真正目的是為了遍歷前面 100 個(gè)正整數(shù),而 0 和 1 既不是質(zhì)數(shù)也不是合數(shù),所以我們直接從 2 開始計(jì)算。
然后,程序執(zhí)行到 digit <= 100
時(shí),顯然滿足條件,所以進(jìn)入到循環(huán)里。然后為 n
和 isPrime
進(jìn)行初始化的賦值。然后進(jìn)入之前的判斷質(zhì)數(shù)的代碼塊里。如果 digit
是質(zhì)數(shù),則肯定會進(jìn)入到上述代碼第 23 行代碼里,并輸出這個(gè)數(shù)字,然后緊跟一個(gè)空格,為了保證后面可能會輸出的數(shù)字會和這個(gè)當(dāng)前數(shù)字之間有一個(gè)空格的分隔。
然后,最后到達(dá)第 27 行代碼,digit
會增大一個(gè)單位,比如 digit
從 2 變?yōu)?3,整個(gè)循環(huán)執(zhí)行完畢。接著,倒回 digit <= 100
的條件處判斷。顯然,它還是滿足要求,所以重新進(jìn)入到循環(huán)里。然后又一次對 n
和 isPrime
執(zhí)行初始化的賦值。
實(shí)際上,從這里就可以看出來,把這兩句初始賦值寫在循環(huán)里的原因。因?yàn)槊恳粋€(gè)不同的數(shù)字判斷是否是質(zhì)數(shù)的時(shí)候,都采用的是
n
和isPrime
兩個(gè)變量。重新賦值初始值是為了避免計(jì)算過后,更改了這兩個(gè)變量的數(shù)值,而我們沒有重新修正數(shù)值,就會影響到程序執(zhí)行,并出現(xiàn)沒有達(dá)到預(yù)期結(jié)果的 bug。
可以看到,這一則實(shí)例用到了兩層 while
循環(huán),外層 while
循環(huán)表示的意思是,想要從 2 計(jì)算到 100,把每一個(gè)數(shù)字都判斷一遍,它們是否是質(zhì)數(shù);而內(nèi)層的 while
循環(huán)在這里表示的意思是,嘗試把外層循環(huán)得到的當(dāng)前需要計(jì)算的數(shù)字 digit
去逐個(gè)用 2 開始試除,看看這個(gè)數(shù)是否能被內(nèi)層循環(huán)得到的這個(gè)數(shù)字所除盡。
其它循環(huán)的嵌套
for
循環(huán)的嵌套
while
循環(huán)的嵌套還是很簡單的,不過 for
循環(huán)嵌套的理解難點(diǎn)就在于它的小括號的三個(gè)部分的執(zhí)行順序。只要你不要忘了 for
循環(huán)的執(zhí)行順序:初始值、條件、循環(huán)體、增量、條件、循環(huán)體、增量、條件、循環(huán)體、增量……
實(shí)際上,改寫代碼后,代碼量更少了,邏輯也清晰了,只是代碼可能會稍微難理解一些。但只要你每一步按部就班地來,就不會出現(xiàn)邏輯執(zhí)行的順序錯(cuò)誤。
do-while
循環(huán)的嵌套
用得最少的就是 do-while
循環(huán)了,所以它的嵌套很多時(shí)候,雖然簡單,但難理解。不過一般我們都用不上,所以這里就隨便舉了個(gè)例,然后改寫成 do-while
循環(huán)書寫。
這個(gè)例子的輸出結(jié)果是
和 if
語句一樣,當(dāng)執(zhí)行循環(huán)的語句部分只有一句話的時(shí)候,我們可以去掉大括號不寫,比如:
它等價(jià)于
for
循環(huán)嵌套了例如 if
和大括號,或者 if-else
可以簡寫為
只要你覺得看起來?xiàng)l理清晰的時(shí)候,就可以這么書寫。
編碼技巧:枚舉法
枚舉法就是采用大量循環(huán)的嵌套,來枚舉出每一個(gè)循環(huán)的執(zhí)行邏輯的一種情況,然后挨個(gè)判斷條件是否滿足的邏輯。這種邏輯雖然很暴力,但枚舉出的所有情況保證了程序執(zhí)行的正確性。
想必很多人聽說過水仙花數(shù)字,它是一個(gè)三位數(shù),每一個(gè)位置的數(shù)字的三次方的和,等于它自己的數(shù)字。我們既然找不到解決方式,就干脆枚舉每一個(gè)情況,讓每一個(gè)變量表示這個(gè)數(shù)的每一個(gè)位置,大不了用三個(gè)變量 a
、b
和 c
表示百位、十位和個(gè)位。不過要注意,a
作為百位數(shù),是不可能為 0 的,否則它就不是一個(gè)合理的三位數(shù)了。
我相信,這樣的書寫模式一定會讓你對多重循環(huán)有一個(gè)更好的認(rèn)知:首先進(jìn)入 a = 1
的初始值賦值,然后顯然滿足條件,進(jìn)入 b = 0
的初始賦值,顯然滿足條件,所以進(jìn)入最內(nèi)層的循環(huán),c = 0
的初始值和條件。
然后全部滿足,所以我們開始整合這個(gè)整數(shù),把每一個(gè)整數(shù)合并起來變?yōu)橐粋€(gè)整數(shù)。然后嘗試對這三個(gè)數(shù)字 a
、b
和 c
執(zhí)行三次方計(jì)算再求和。如果它等于 digit
,便滿足條件,所以此時(shí)就算滿足了水仙花數(shù)的要求,輸出它即可。否則,我們就計(jì)算下一次循環(huán)。注意,此時(shí)循環(huán)還在最內(nèi)層,所以執(zhí)行完畢這一次循環(huán)的時(shí)候,應(yīng)該走增量 c++
的這個(gè)部分,此時(shí) c
表示的個(gè)位變?yōu)?1。然后再判斷條件 c <= 9
。滿足條件,進(jìn)入循環(huán),再一次判斷條件是否滿足。
直到 c
計(jì)算到 9 的時(shí)候,增量使得 c
變?yōu)?10,此時(shí)條件不滿足。于是整個(gè)最內(nèi)層的 for
循環(huán)執(zhí)行完畢,發(fā)現(xiàn)它被包含在 b
計(jì)算的循環(huán)的這坨循環(huán)里,而這坨循環(huán)里只有關(guān)于 c
變量的循環(huán)一個(gè)部分,所以這便意味著 b
循環(huán)執(zhí)行完一次操作,此時(shí) b
才會增大一個(gè)單位(進(jìn)入 b++
增量,使之變?yōu)?1)。然后再次進(jìn)入 c = 0
的初始賦值處。
如果用人為的邏輯理解的話,就是把它們?nèi)齻€(gè)整理為 100、101、102、103、……、109、110、111、……、999 的順序執(zhí)行每一個(gè)數(shù)字。
大致邏輯如下:
一些練習(xí)
嵌套循環(huán)還需要多多練習(xí)和鞏固,因?yàn)樗雌饋砗唵?,?shí)際上每一步執(zhí)行起來還是費(fèi)工夫的。所以我們來一些練習(xí),希望你們能夠明白和看懂它們的執(zhí)行。
方陣
將會輸出一個(gè)星星矩陣:
將會是一個(gè)左上三角:
將會是一個(gè)左下三角:
右上三角
將會是一個(gè)右上三角:
這樣便會輸出一個(gè)九九乘法表:
菱形
最難的就是這個(gè)例子了。我們嘗試輸出一個(gè)菱形圖案。
將會得到一個(gè)菱形圖案: