1. 程式人生 > >楊輝三角形實現過程詳解-C語言基礎

楊輝三角形實現過程詳解-C語言基礎

在這裡插入圖片描述
這一篇要探討的是“楊輝三角形的實現以及如何人工走迴圈”。涉及的知識點和內容很少,主要是想說明如何看懂迴圈,如何跟著迴圈走。屬於C語言基礎篇


學習程式設計的人,在學習的初期,幾乎都會接觸楊輝三角形。但與其說用程式碼實現輸出楊輝三角形是一道程式設計題,倒不如說它是一道IQ題。因為在楊輝三角形的程式碼實現過程中,所涉及語法知識程式設計概念其實很少。

類似於楊輝三角形這類的程式設計題還有很多,它們在真正的實際開發中,用的很少,幾乎可以說是除了在初學程式設計階段以及在面試過程中會接觸到之外,你就再也不會遇到了。

這類問題真正難的地方在於它邏輯上面的複雜,很多時候,即便拿到了實現程式碼,看的也雲裡霧裡的。特別是那些涉及雙重迴圈

三重迴圈遞迴呼叫二叉樹雙向迴圈核心連結串列等等等等的,更煩的是這些東西之間還互相交叉重疊,繞來繞去的簡直不要太噁心了。

雖然大多數情況下,我們都不用真正獨立寫出這些邏輯演算法,只要套用前人已有演算法的就行了。但是看的懂還是必要的。


所以這篇就簡單的分析一下楊輝三角形實現過程中程式碼執行的流程。跟著流程從頭到尾的走一遍程式。雖然對於一些大工程來說,跟著流程走這個過程往往是交由機器來幫我們完成的,像是GDB之類的除錯工具。

但是提高自身的閱讀程式碼能力還是很有必要的,否則你跟著GDB一步一步走,程式走錯了,你也看不出來,那就毫無意義了。GDB只能說是作為輔助工具,能否除錯好程式碼,還是得看你的程式碼閱讀能力。


好了,先來看一下楊輝三角的實現程式碼,這篇程式碼引用自百度百科詞條“楊輝三角”(https://baike.baidu.com/item/楊輝三角/215098?fr=aladdin),這裡我就不用自己的程式碼了,以後有機會我想辦法會把它再優化一下更新上來的。

#include <stdio.h>
 
int main()
{
	int s = 1, h;			// 數值和高度
	int i, j;			// 迴圈的計數變數
	scanf("%d", &h);		// 使用者輸入層數
	printf("1\n");			// 輸出第一行 1
	for (i = 2; i <= h;
s = 1, i++) // 行數 i 從 2 到層高 { printf("1 "); // 輸出該行的第一個 1 for (j = 1; j <= i - 2; j++)//列位置 j 繞過第一個直接開始迴圈 printf("%d ", (s = (i - j) * s / j)); printf("1\n"); // 輸出最後一個 1,換行 } getchar(); // 暫停等待 return 0; }

執行結果:

在這裡插入圖片描述

下面我們開始一步一步的跟著程式走下去(建議在電腦上面看的時候把程式碼複製到編輯器上面,編輯器和瀏覽器左右分屏對照著看,雖然我每一步都會配圖片)。


第一步:程式先是建立了兩個變數,一個“s”一個“h”,其中“s”用來儲存演算法運算出來的結果(也就是應該輸出的值),“h”則是楊輝三角形的高度(也就是需要輸出的行數)。

在這裡插入圖片描述


第二步:接收使用者的輸入,由使用者決定楊輝三角形的高度

在這裡插入圖片描述


第三步:輸出一個“1”同時換行,也就是手動把第一行給輸出完成了。

在這裡插入圖片描述


第四步:然後就是進入了程式碼的關鍵演算法了(為了方便說明,下面我以使用者輸入數字“6”為例進行分析,用“-”代替空格方便顯示)。
當用戶輸入“6”的時候,最外層迴圈的判斷條件為“i =2; i<= 6”迴圈會執行5次

在這裡插入圖片描述

最外層迴圈有一個不太常見的用法需要說明一下,在外層迴圈的末尾迴圈體中,同時有兩個表示式,多出來的“s = 1”的作用是在於控制“s”在每次外層迴圈開始之前初始化為1。
在這裡插入圖片描述
其實程式中的這一段也可以改為下面這個樣子:
在這裡插入圖片描述


外層第一次迴圈

輸出第二行的時候(此時“i = 2, s = 1”):
首先輸出“1-”(此時緩衝區裡面有“1-”)。
  內層迴圈(“j = 1, j <= 0”)不進入迴圈(此時緩衝區裡面有”1-”)。
結尾輸出“1\n”,把緩衝區裡面的資料輸出到使用者的控制面板(此時緩衝區裡面有“1-1”)。
在這裡插入圖片描述


外層第二次迴圈

輸出第三行的時候(此時“i = 3, s = 1”):
首先輸出“1-”(此時緩衝區裡面有“1-”)。
  內層迴圈(“j = 1, j <= 1”)進入一次迴圈(此時緩衝區裡面有”1-”)。
    內層第一次迴圈(“i = 3, j = 1, s = 1”):
    s = (3 - 1)*1/1 = 2 輸出“2-”(此時緩衝區裡面有“1-2-”)。
  退出內層迴圈。
結尾輸出“1\n”,把緩衝區裡面的資料輸出到使用者的控制面板(此時緩衝區裡面有“1-2-1”)。
在這裡插入圖片描述


外層第三次迴圈

輸出第四行的時候(此時“i = 4, s = 1”):
首先輸出“1-”(此時緩衝區裡面有“1-”)。
  內層迴圈(“j = 1, j <= 2”)執行兩次迴圈(此時緩衝區裡面有”1-”)。
    內層第一次迴圈(“i = 4, j = 1, s = 1”):
    s = (4 - 1)*1/1 = 3 輸出“3-”(此時緩衝區裡面有“1-3-”)。
    內層第二次迴圈(“i = 4, j = 2, s = 3”):
    s = (4 - 2)*3/2 = 3 輸出“3-”(此時緩衝區裡面有“1-3-3-”)。
  退出內層迴圈。
結尾輸出“1\n”,把緩衝區裡面的資料輸出到使用者的控制面板(此時緩衝區裡面有“1-3-3-1”)。
在這裡插入圖片描述


外層第四次迴圈

輸出第五行的時候(此時“i = 5, s = 1”):
首先輸出“1-”(此時緩衝區裡面有“1-”)。
  內層迴圈(“j = 1, j <= 3”)執行三次迴圈(此時緩衝區裡面有”1-”)。
    內層第一次迴圈(“i = 5, j = 1, s = 1”):
    s = (5 - 1)*1/1 = 4 輸出“4-”(此時緩衝區裡面有“1-4-”)。
    內層第二次迴圈(“i = 5, j = 2, s = 4”):
    s = (5 - 2)*4/2 = 6 輸出“6-”(此時緩衝區裡面有“1-4-6-”)。
    內層第三次迴圈(“i = 5, j = 3, s = 6”):
    s = (5 - 3)*6/3 = 4 輸出“4-”(此時緩衝區裡面有“1-4-6-4-”)。
  退出內層迴圈。
結尾輸出“1\n”,把緩衝區裡面的資料輸出到使用者的控制面板(此時緩衝區裡面有“1-4-6-4-1”)。
在這裡插入圖片描述


外層第五次迴圈

輸出第六行的時候(此時“i = 6, s = 1”):
首先輸出“1-”(此時緩衝區裡面有“1-”)。
  內層迴圈(“j = 1, j <= 4”)執行四次迴圈(此時緩衝區裡面有”1-”)。
    內層第一次迴圈(“i = 6, j = 1, s = 1”):
    s = (6 - 1)*1/1 = 5 輸出“5-”(此時緩衝區裡面有“1-5-”)。
    內層第二次迴圈(“i = 6, j = 2, s = 4”):
    s = (6 - 2)*5/2 = 10 輸出“10-”(此時緩衝區裡面有“1-5-10-”)。
    內層第三次迴圈(“i = 6, j = 3, s = 6”):
    s = (6 - 3)*10/3 = 10 輸出“10-”(此時緩衝區裡面有“1-5–10-10-”)。
    內層第四次迴圈(“i = 6, j = 4, s = 10”):
    s = (6 - 4)*10/4 = 5 輸出“5-”(此時緩衝區裡面有“1-5-10-10-5-”)
  退出內層迴圈。
結尾輸出“1\n”,把緩衝區裡面的資料輸出到使用者的控制面板(此時緩衝區裡面有“1-5–10-10-5-1”)。
在這裡插入圖片描述


第五步暫停等待程式退出。

在這裡插入圖片描述
好了,至此楊輝三角的實現過程就算分析完了。


下面這個例子的功能是畫一個由字母按照特定順序排列組成的三角形,感興趣的也可以看一下。

先看程式碼

#include <stdio.h>
 
int main()
{
  int i,j;
  char ch, n;
	
  scanf("%c",&ch);//C
	
  n = ch-'A'+1;	  //n=3	  //第一次進入  第二次進入  第三次進入
			  //最外層迴圈
			  //一次迴圈代表輸出一行
  for(i=1; i<=n; i++)	  //(i=1;i<=3)  (i=2;i<=3)       (i=3;i<=3)
  {
  			  //內層第一個迴圈
    for(j=1; j<=n-i; j++) //(j=1;j<=2)  (j=1;j<=1)       (j=1;j<=0)
      printf(" ");	  //打了2個空格   打了1個空格        打了0個空格
			  //內層第二個迴圈
    for(j=1; j<=i; j++)	  //(j=1;j<=1)  (j=1;j<=2)       (j=1;j<=3)
      printf("%c",ch-n+j);//C-3+1=A    C-3+1=A,C-3+2=B   C-3+2=B,C-3+3=C
 			  //打了個A	打了A和B		 打了B和C
			  //內層第三個迴圈
    for(j=i-1; j>=1; j--) //(j=0;j>=1)   (j=1;j>=1)      (j=2;j>=1)
      printf("%c",ch-n+j);//不進入迴圈	 C-3+1=A	 C-3+2=B,C-3+1=A
		     	  //什麼也沒打	 打了個A		 打了個B和A
		     	  
     printf("\n");	  //||A		 |ABA		 ABCBA
  }
  return 0;		  //		 ||A
			  //		 |ABA
			  //		 ABCBA
}

執行結果。

在這裡插入圖片描述

其實過程分析在程式碼中已經寫明白了,不過在這裡,我還想逐條逐句再分析一遍,我儘量寫的通俗易懂一些。在這個例子裡面,分析起來應該還沒有太大問題。以後有機會我可能還會分享另外一篇類似這種的人工走迴圈的邏輯分析文章,在那篇文章中的例子涉及到一個很複雜(至少在我當時是這麼覺得的)的三層迴圈,那個分析起來可能就很費勁了。說實話那篇文章的話,我也不太有信心能把裡面的邏輯關係說的很清楚明白。有些東西,你知道是那麼一回事,但是卻不一定能說的明白,你覺得你自己說明白了,別人卻又不一定能聽的懂。


在這個例子裡面用的是printf()輸出,由於printf()緩衝區的存在,所以輸出的資料不會立即出現在使用者面板上,只有當遇到要輸出“/n”的時候,緩衝區裡面的內容才會被推出來。雖然在這個程式裡面看不出區別,不過還是要意識到這個緩衝區的存在。

如果不想要緩衝區的存在這裡可以用putchar()來代替printf()作為輸出函式,理論上你應該是可以看到用putchar()輸出比用printf()輸出順暢不少的,但實際上由於在這個程式裡面的輸出之間的間隔時間太短,你的肉眼是察覺不到區別的。但是速度快的同時也不是沒有犧牲的,這樣子做會造成程式對於CPU的佔用率提高,降低整體的效率,綜合考慮下還是用printf()會比較好。

在開始之前,先說一下這篇程式碼的整體思路架構。
最外層的for迴圈用於控制輸出的行數。n等於要輸出的行數。
內層第一個for迴圈用於控制輸出該行“第一個字母前面的空格”個數。
內層第二個for迴圈用於控制輸出該行“第一個字母該行中間的最大字母”。
內層第三個for迴圈用於控制輸出該行“中間的最大字母該行的最後一個字母

下面我們開始一步一步的跟著程式走下去(建議在電腦上面看的時候把程式碼複製到編輯器上面,編輯器和瀏覽器左右分屏對照著看,雖然我每一步都會配圖片)。


首先,程式執行到scanf()函式的時候,要求輸入(這可以檢測一下輸入是否合法),這裡我們以輸入“C”為例進行分析。

在這裡插入圖片描述

當我們輸入“C”的時候程式中的n就等於3了。n的作用就在於確定該楊輝三角形的層數(既輸出的行數),同時也用於計算應該輸出的字母。這個程式碼中用的是分而治之的方法。整體從上到下一行一行的輸出。先是控制輸出每一行前面的空格,再控制輸出從第一個字母開始到該行中間的最大那個字母,然後輸出剩下的字母。
在這裡插入圖片描述

題外話:在這裡突然想起了一個挺有意思的閱讀技巧。有時候看別人的分析解釋說明的時候,如果不怎麼看的懂,可以試著在腦海中把某些字眼改為自己易於理解的東西。比如把上面那段話中的”輸出“兩個字改成”畫“字,再試著看一遍。

當n = 3的時候,最外層迴圈一共迴圈3遍,也就是輸出三行。

在這裡插入圖片描述


為了便於說明用“|”代替空格。

輸出第一行的時候(i = 1):

內層第個for迴圈的時候(此時緩衝區裡面什麼都沒有:“”):
  初始化是“j = 1”判斷條件是“j <= 2”所以會迴圈2次,每一遍輸出1個空格,輸出2個空格
記憶體第個for迴圈的時候(此時緩衝區裡面有兩個空格:“||”):
  初始化是“j = 1”判斷條件是“j <= 1”所以會迴圈1次,輸出一個A
記憶體第個for迴圈的時候(此時緩衝區裡面有兩個空格和一個A:“||A”):
  初始化是“j = 0”判斷條件是“j >= 1”所以不會進入迴圈,什麼也不輸出
把緩衝區裡面的內容輸出(此時緩衝區裡面有兩個空格和一個A:“||A”)。

在這裡插入圖片描述

輸出第二行的時候(i = 2):

內層第個for迴圈的時候(此時緩衝區裡面什麼都沒有:“”):
  初始化是“j = 1”判斷條件是“j <= 1”所以會迴圈1次,每一遍輸出1個空格,輸出1個空格
內層第個for迴圈的時候(此時緩衝區裡面有一個空格:“|”):
  初始化是“j = 1”判斷條件是“j <= 2”所以會迴圈2次,輸出一個A和一個B
內層第個for迴圈的時候(此時緩衝區裡面有一個空格一個A一個B:“|AB”):
  初始化是“j = 1”判斷條件是“j >= 1”所以會迴圈1次,輸出一個A
把緩衝區裡面的內容輸出(此時緩衝區裡面有一個空格一個A一個B一個A:“|ABA”)。

在這裡插入圖片描述

輸出第三行的時候(i = 3):

內層第個for迴圈的時候(此時緩衝區裡面什麼都沒有:“”):
  初始化是“j = 1”判斷條件是“j <= 0”所以不會進入迴圈,什麼也不輸出
內層第個for迴圈的時候(此時緩衝區裡面什麼都沒有:“”):
  初始化是“j = 1”判斷條件是“j <= 3”所以會迴圈3次,輸出一個A和一個B一個C
內層第個for迴圈的時候(此時緩衝區裡面有一個空格一個A一個B一個C:“ABC”):
  初始化是“j = 2”判斷條件是“j >= 1”所以會迴圈2次, 輸出一個B一個A
把緩衝區裡面的內容輸出(此時緩衝區裡面有一個空格一個A一個B一個C一個B一個A:“ABCBA”)。

在這裡插入圖片描述


最後退出迴圈結束程式。

好了,說到這裡整個程式就算分析完了。初學者閱讀一些比較複雜的程式碼的時候,可以先嚐試像程式碼中那樣用註釋跟著程式走上一遍。接觸多了之後,慢慢的就可以做到把這些步驟都在腦海中走完而不需要寫出來了。


附上這個例子的精簡版程式碼:

#include <stdio.h>
int main()
{
	int i,j;
	char ch;
	scanf("%c",&ch);
	for(i=0; i<ch-'A'+1; i++)
	{
		for(j=0; j<=ch-'A'-i; j++)
			putchar(' ');

		for(j=0; j<=i; j++)
			putchar('A'+j);

		for(j=i-1; j>=0; j--)
			putchar('A'+j);

		putchar('\n');
	}
	return 0;
}

以及數字版的三角形實現程式碼(這個程式碼目前只能實現10層以下的三角形,以後有機會再優化):

#include <stdio.h>
int main()
{
	int i,j,n;	
	scanf("%d",&n);	
	for(i=0; i<n; i++)
	{
		for(j=0; j<=n-i-1; j++)
			putchar(' ');

		for(j=0; j<=i; j++)
			putchar(j+'1');

		for(j=i-1; j>=0; j--)
			putchar(j+'1');

		putchar('\n');
	}
	return 0;
}

還有星號符版三角形實現程式碼(單位為“*”):

#include <stdio.h>
int main()
{
	int i,j,n;
	scanf("%d",&n);
	for(i=0; i<n; i++)
	{
		for(j=0; j<=n-i-1; j++)
			putchar(' ');
		for(j=0; j< 2*i+1; j++)
			putchar('*');

		putchar('\n');
	}
	return 0;
}

題外話(純屬瞎扯,感興趣的可以看下)

其實閱讀程式不是什麼太難的事情,難的還是自己想出來這麼一個演算法,這個我還真沒法說明白是怎麼想的出來這些演算法的。很多人可以看的懂別人寫的演算法,知道別人演算法的巧妙性,但是要是自己去寫的話,卻怎麼也寫不出來,沒這個思路,不知道該怎麼去寫。

感覺嘛,寫演算法這件事情還是挺需要一點靈性和一點運氣的,先是要有個大概的思路,但有時候同一個需求你會想到有很多種不同的實現思路,而且每一個思路都是那種感覺可能行,又有可能不行的樣子。在你沒有著手去實現這思路之前,你都不知道它的真正可行性。

這就好像你面前同時有好幾條路可以走,但是在你沒有跟著走下去之前,你又不知道到底那條路是可以走到終點的,有可能是A思路,有可能是B思路,也有可能是AB思路都可以, 更有可能是ABC思路都不可以,走到最後你還是要回到原點去尋找D思路。又或者,可能ABC思路都是可以的,只是你走到了半路以為自己走錯路了,走不下去了,沒耐心了,看不到希望了,你半路回頭去尋找渺茫的D思路去了而已。這些都有可能。

或者,你運氣還算好,一開始就選對了路,一路堅持走到了終點。但是,你有沒有想過,除了你選的A思路是可行的以外,C思路也是可行的。而且如果你一開始順著C思路走,整個程式碼實現的流程將會更加簡潔,邏輯結構將會更加簡單,同時程式執行的效率也會更加高效呢?而這所有的所有,如果你不先嚐試性的走出第一步,你永遠都不會知道。

我的方法是,覺得這個思路可行,就嘗試性的去實現它,不撞南牆不回頭,撞了再回頭。很多時候,你一開始只有一個思路,但是在你實現這個思路的過程中,你慢慢的又會突然之間靈光一閃,又想到了另外一個思路。有時候人腦就是這麼一個神奇的東西。你不斷的去接觸一個問題,接觸的多了,靈感就這麼突然來了。

實現一個演算法是一件很有成就感又很驚喜的事情。就好像程式碼中的那個變數n,它本身即可以用來控制輸出的行數,又可以參與到內層迴圈中參與運算實現別的功能。這種感覺就好像是自己一開始定義n這個變數僅僅只是為了用來控制輸出行數的,但是你寫程式碼的過程中卻突然發現這麼一個自己隨手寫的變數,竟然還可以二次利用來幹一些別的事情。你突然就會很驚喜自己怎麼這麼聰明啊,竟然寫出了這麼巧妙的程式碼(雖然你一開始真沒這麼想過),然後你就會把你的程式碼拿給你的同學看,然後自豪的說你在寫這篇程式碼的時候,一開始就想好了這個演算法了,巧妙的利用了這個地方的重合,減少了程式碼重複性的同時又節約了記憶體,滿滿的成就感油然而生(此處應該有掌聲)。哈哈哈,開個玩笑(雖然我經常就是這麼幹的)。

但是寫演算法的過程中確實經常會遇到這種情況的,有時候一個變數同時有好幾種用途,這種情況我也不知道該怎麼解釋,感覺冥冥中總有某些東西連線著整個演算法,使得某些演算法可以做到很巧妙,很完美,很無可挑剔。
這說的有點玄,就像是你為了一個大需求下面的某個小需求建立了一個變數,那麼這個變數就很有可能會和大需求下面的其他小變數產生一些直接或者間接的關係,然後這個變數就很有可能會有多種用途。可能是由於事物不同屬性之間總會或多或少有著某種聯絡的原因吧。
就好像你的需求是計算一個長方體,你在計算它的面積的時候建立了長寬的變數,然後你在計算它的體積的時候突然發現你前面的長寬變數也可以用來二次使用。雖然這種關係在平時程式設計開發面對具體需求的時候可能表現不是那麼明顯,雖然我沒法證明,但我覺得這種東西是真的存在的。
否則為什麼有些程式碼的演算法就是可以做到那麼巧妙,那麼完美,那麼無可挑剔呢?或者也僅僅是巧合,是由於人類的主觀能動性,使得我們更傾向於去尋找程式演算法中的各種巧合然後加以利用呢?不過我感覺嘛,應該是要先存在著那層巧妙的聯絡,而後我們才能把這層巧妙的聯絡尋找出來加以利用的。

其實有時候我真覺得這個世界真是奇妙,明明你在寫一個演算法的時候沒有想過太多,就這麼用最笨的辦法一路跌跌撞撞的走到了終點,但是當你回過頭來看一下你原來走過的路,你又總能找到一些捷徑,就好像這些捷徑是必然存在一樣。說的有點玄對吧?我其實就是在說演算法優化啦。


寫在後面

一開始我在翻找我以前寫的程式碼尋找素材的時候,找到了一個畫三角形的程式碼,而且乍一看還挺像楊輝三角形的,於是這篇文章我的創作初衷就是為了詳細分析一下楊輝三角形的實現流程的。但在分析的過程中越寫越覺得不對勁,因為楊輝三角形的概念我大概還是有點印象的。
最後不放心才再去查了一下楊輝三角形的結構,才發現我找到的程式碼不是畫楊輝三角形的,但是我整篇文章都快要定稿了都。然後我看著文章發呆了很久,在想怎麼去改。最終的決定方案是——我同時再把楊輝三角形也給分析完算了。
當時22點(原本計劃是十二點之前完成這篇文章然後美美的睡一覺的,結果熬到了現在),然後我就先去洗了個澡,洗完衣服再晾起來之後就已經23點多了。從23點半到1點半這段時間,我都是在想辦法用我自己的程式碼去實現畫楊輝三角形的功能,但是很慚愧的說,一直到2點我都沒想到很好的辦法。
這期間我也不是沒想過用陣列來儲存然後再輸出,網上的程式碼也大多數是用這個方法的,但用陣列的方法就和這篇文章要討論迴圈演算法內容不太對的上,就放棄了。我甚至想過用連結串列來建立佇列,利用佇列或棧的儲存特點來實現,還想過用二叉樹來實現。但都不是我想要的,我真正想要的實現過程就是像這篇文章中的例子那樣,單純的用迴圈和演算法來實現畫楊輝三角形。
只有用這種方式的程式碼才是最高效的。所以我才引用了文章中那個例子的程式碼來分析,感謝原作者。大概3點的時候我才開始動手寫分析,現在剛好5點完稿,還要上傳加排版,希望6點前能睡覺吧。


零BUG是原則性問題。