日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

《算法竞赛入门经典》——刘汝佳

發布時間:2023/12/20 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《算法竞赛入门经典》——刘汝佳 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

“構造性”和“可行性”是計算機學科的兩個最根本特征。
比賽的核心是算法

#1 語言篇

編程不是看會的,也不是聽會的,而是練會的,所以應盡量在計算機旁閱讀書本,以便把書中的程序輸入到計算機中進行調試,順便再做做上機練習。千萬不要圖快—如果沒有足夠的時間來實踐,那么學的快,忘得也快!

1.1算數表達式

原因并不重要,重要的是規范:根據規范做事情,則一切盡在掌握中。

提示1-1:整數值用%d輸出,實數用%f輸出。
提示1-2:整數/整數=整數,浮點數/浮點數=浮點數。

整數-浮點數=浮點數。
一般來說,只要程序中用到了數學函數,就需要在程序最開始處包含頭文件math.h,并在編譯時連接數學庫。

1.2 變量及其輸入

提示1-3: scanf 中的占位符和變量的數據類型應一一對應,且每個變量前需要加“&”符號。 可以暫時把變量理解成“存放值的場所”。

提示1-4:
在算法競賽中,輸入前不要打印提示信息。輸出完畢后應立即終止程序,不要等待用戶按鍵,因為輸入輸出過程都是自動的,沒有人工干預。
提示1-5:
在算法競賽中不要使用頭文件conio.h, 包含getch(),clrscr()等函數。
提示1-6:
在算法競賽中,每行輸出均應以回車符結束,包括最后一行。除非特別說明,每行的行首不應有空格,單行末通常可以有多余空格。另外,輸出的每兩個數或者字符串之間應以單個空格隔開。

總結:算法競賽的程序只做3件事:
讀入數據,計算結果,打印輸出。
Const double pi = acos(-1.0); Const 關鍵字表明它的值是不可以改變的

提示1-7: 盡量用const 關鍵字聲明常熟。
提示1-8: 賦值是個動作,先計算右邊的值,再賦給左邊的變量,覆蓋它原來的值。
提示1-9: printf 的格式字符串中可以包含其他可打印符號,打印時原樣輸出。

1.3 順序結構程序設計

提示1-10:算法競賽的題目應當是嚴密的,各種情況下的輸出均應有嚴格規定。如果在比賽中發現題目有漏洞,應向相關人員詢問,盡量不要自己隨意假定。

提示1-11: 賦值a=b;之后,變量a原來的值被覆蓋,而b的值不變。

提示1-12:可以通過手工模擬的方法理解程序的執行方式,重點在于記錄每條語句執行之后各個變量的值。
提示1-13:交換兩個變量的三變量法適用范圍廣,推薦使用。 提示1-14: 算法競賽是在比誰能更好地解決問題,而不是在比誰學的程序看上去更高級。

1.4 分支結構程序設計

提示1-15

If語句的一般格式: If(條件)語句1; Else語句2

在C語言中單個整數值也可以表示真假,其中0為假,其他值為真。

提示1-17:
C語言中邏輯運算符都是短路運算符。一旦能夠確定整個表達式的值,就不再繼續計算。
提示1-18:
算法競賽的目標是編程對任意輸入均得到正確的結果,而不僅是樣例數據。
提示1-19:
如果有多個并列,情況不交叉的條件需要一一處理,可以用else if 語句。
提示1-20:
適當在程序中編寫注釋不僅能讓其他用戶更快地搞懂你的程序,還能幫你自己瀝青思路。
提示1-21:
可以用花括號把若干條語句整合成一個整體。這些語句仍然按順序執行。

1.5 注解與習題

編譯器任務:把人類可以看懂的源代碼變成機器可以直接執行的命令。

建議:

  • 重視實驗
  • 學會模仿
  • #2 循環結構程序設計

    提示2-1:
    for循環的格式為:for(初始化;條件;調整)循環體;
    提示2-2:
    盡管for循環反復執行相同的語句,但這些語句每次的執行效果往往不同。
    提示2-3:
    編寫程序時,要特別留意“當前行”的跳轉和變量的改變。
    提示2-4:
    建議盡量縮短變量的定義范圍。

    偽代碼:不是真正程序的代碼

    提示2-5: 不拘一格地使用偽代碼來思考和描述算法是一種值得推薦的做法。
    提示2-6: 把偽代碼改寫成代碼時,一般選擇較為容易的任務來完成。

    Question:如何判斷n是否為完全平方數?
    用“開平方”函數,先求出其平方根,然后看它是否為正數,即用一個int 型變量m存儲sqrt(n)四舍五入后的整數,然后判斷m×m是否等于n。函數floor(x)返回不超過x的最大整數。

    讀者可能會問: 可不可以這樣寫?

    if( sqrt( n) = =floor( sqrt( n) ) ) printf( "%d\n", n);

    即直接判斷sqrt( n) 是否為整數。 理論上當然沒問題, 但這樣寫不保險, 因為浮點數的運算( 和函數) 有可能存在誤差。
    假設在經過大量計算后, 由于誤差的影響, 整數1變成了0.9999999999, floor的結果會是0而不是1。 為了減小誤差的影響, 一般改成四舍五入, 即floor( x+ 0.5) (2)。
    如果難以理解, 可以想象成在數軸上把一個單位區間往左移動0.5個單位的距離。 floor( x) 等于1的區間為[1, 2) , 而floor( x+ 0.5) 等于1的區間為[0.5, 1.5) 。

    提示2-7: 浮點運算可能存在誤差。在進行浮點數比較時,應考慮浮點誤差。

    2.2 while、循環和do-while循環

    提示2-8;
    while循環的格式為“while(條件)循環體;”。
    提示2-9
    當需要統計某個事物的個數時,可以用一個變量來充當計數器。
    提示2-10
    不要忘記測試。一個看上去正確的能隱含錯誤。
    提示2-11
    在觀察無法找出錯誤時,可以用“輸出中間結果;”的方法查錯。 Int整數的大小:-2147483648~2147483647
    提示2-12
    C99并沒有規定int類型的確切大小,但在當前流行的競賽平臺中,int都是32位整數。
    提示2-13
    long long在linux下的輸入輸出格式符為%lld,但windows平臺中有時為%I64d。為保險起見,可以用后面介紹的C++流,或者編寫自定義輸入輸出函數。

    提示2-14
    do-while循環的格式為“do{循環體}while( 條件) ; ”, 其中循環體至少執 行一次,
    每次執行完循環體后判斷條件, 當條件滿足時繼續循環。 只要末6位, 即輸出對10的6次方取模。
    提示2-15
    在循環體開始處定義的變量,
    每次執行循環體時會重新聲明并初始化。
    提示2-16
    要計算只包含加法、 減法和乘法的整數表達式除以正整數n的余數,
    可以在每步計算之后對n取余, 結果不變。

    這個程序真正的特別之處在于計時函數clock( ) 的使用。 該函數返回程序目前為止運行
    的時間。 這樣, 在程序結束之前調用此函數, 便可獲得整個程序的運行時間。 這個時間除以
    常數CLOCKS_PER_SEC之后得到的值以“秒”為單位。

    提示2-17: 可以使用time.h和clock( ) 函數獲得程序運行時間。
    常數CLOCKS_PER_SEC和操作系統相關,
    請不要直接使用clock( ) 的返回值, 而應總是除以CLOCKS_PER_SEC。

    提示2-18
    很多程序的運行時間與規模n存在著近似的簡單關系。 可以通過計時函數來發現或驗證這一關系。

    2.3 算法競賽中的輸入輸出框架

    提示2-19: 在Windows下, 輸入完畢后先按Enter鍵, 再按Ctrl+ Z鍵, 最后再按Enter鍵, 即可結束輸入。
    在Linux下, 輸入完畢后按Ctrl+ D鍵即可結束輸入。
    提示2-20: 變量在未賦值之前的值是不確定的。

    特別地, 它不一定等于0,在使用之前賦初值。 由于min保存的是最小值, 其初值應該是
    一個很大的數; 反過來, max的初值應該是一個很小的數。 一種方法是定義一個很大的常數, 如INF= 1000000000, 然后讓max= -INF, 而min= INF, 另一種方法是先讀取第一個整數x, 然后令max= min= x。
    事實上, 幾乎所有算法競賽的輸入數據和標準答案都是保存在文件中的。
    使用文件最簡單的方法是使用輸入輸出重定向, 只需在main函數的入口處加入以下兩條
    語句:

    freopen("input.txt", "r", stdin); freopen("output.txt", "w", stdout);

    提示2-21: 請在比賽之前了解文件讀寫的相關規定: 是標準輸入輸出( 也稱標準I/O,即直接讀鍵盤、 寫屏幕) , 還是文件輸入輸出? 如果是文件輸入輸出, 是否禁止用重定向方
    式訪問文件?

    例如, 如果題目規定程序名稱為test, 輸入文件名為test.in, 輸出文件名為test.out, 就不 要犯以下錯誤。
    錯誤1:程序存為t1.c( 應該改成test.c) 。
    錯誤2: 從input.txt讀取數據( 應該從test.in讀取) 。
    錯誤3:從tset.in讀取數據( 拼寫錯誤, 應該從test.in讀取) 。
    錯誤4: 數據寫到test.ans( 擴展名錯誤,應該是test.out) 。
    錯誤5: 數據寫到c: \\contest\\test.out( 不能加路徑, 哪怕是相對路徑。 文件名應該只有8個字符: test.out) 。


    加粗樣式

    提示2-22
    在算法競賽中, 選手應嚴格遵守比賽的文件名規定, 包括程序文件名和輸入輸出文件名。 不要弄錯大小寫, 不要拼錯文件名,不要使用絕對路徑或相對路徑。

    這是一份典型的比賽代碼, 包含了幾個特殊之處:
    重定向的部分被寫在了#ifdef和#endif中。 其含義是: 只有定義了符號LOCAL, 才編譯兩條freopen語句。
    輸出中間結果的printf語句寫在了注釋中——它在最后版本的程序中不應該出現, 但是又舍不得刪除它( 萬一發現了新的bug, 需要再次用它輸出中間信息) 。 將其注釋的好處是: 一旦需要時, 把注釋符去掉即可。
    上面的代碼在程序首部就定義了符號LOCAL, 因此在本機測試時使用重定向方式讀寫文件。
    如果比賽要求讀寫標準輸入輸出, 只需在提交之前刪除#defineLOCAL即可。 一個更好的方法是在編譯選項而不是程序里定義這個LOCAL符號( 不知道如何在編譯選項里定義符號的, 這樣, 提交之前不需要修改程序, 進一步降低了出錯的可能。
    提示2-23: 在算法競賽中, 有經驗的選手往往會使用條件編譯指令并且將重要的測試語句注釋掉而非刪除、
    提示2-24: 在算法競賽中, 如果不允許使用重定向方式讀寫數據, 應使用fopen和fscanf/fprintf進行輸入輸出。
    提示2-25: 在算法競賽中, 偶爾會出現輸入輸出錯誤的情況。 如果程序魯棒性強, 有時能在數據有瑕疵的情況下仍然給出正確的結果。 程序的魯棒性在工程中也非常重要。
    提示2-26: 在多數據的題目中, 一個常見的錯誤是: 在計算完一組數據后某些變量沒有重置, 影響到下組數據的求解。
    提示2-27: 當嵌套的兩個代碼塊中有同名變量時,內層的變量會屏蔽外層變量,有時會引起十分隱蔽的錯誤。

    初學者在求解“多數據輸入”的題目時常范的錯誤,請讀者留意。這種問題通常很隱蔽,但也不是發現不了:對于這個例子來說,編譯時加一個-Wall 就會看到一條提示:

    Warning : unused variable’s’[-Wunused-variable](警告:沒有用過的變量’s’)

    提示2-28:用編譯選項-Wall 編譯程序時,會給出很多(但不是所有)警告信息,以幫助程序員查錯。但這并不能解決所有的問題:有些“錯誤”程序是合法的,只是動作不是所期望的。

    #3 數組和字符串

    3.1 數組

    提示3-1: 語句“int a[maxn]”聲明了一個包含maxn 個整形變量的數組,即a[0],a[1], … ,a[maxn-1],但不包含a[maxn]。Maxn必須是常數,不能是變量。
    提示3-2: 在算法競賽中,常常難以精確計算出需要的數組大小,數組一般會聲明得稍大一些。在空間夠用的前提下,浪費一點不會有太大影響。
    提示3-3: 對于變量n,n++和++都會給n加1,但當它們用在一個表達式中時,行為有所差別:n++會使用加1前得值計算表達式,而++n會使用加1后得值計算表達式。
    只有只有在放外面時,數組a才可以開得很大;放在main函數內時,數組稍大就會異常退出。
    提示3-4: 比較大的數組應盡量聲明在main函數外,否則程序可能無法運行。


    “memset(a,0,sizeof(a))”得作用是把數組a清零,它也在string.h中定義。雖然也能用for循環完成相同的任務,但是用memset又方便又快捷。另一個技巧在輸出:為了避免輸出多余空格,設置了一個標志變量吧first,可以表示當前要輸出得變量是否為第一個。第一個變量前不應有空格,但其他變量都有。

    提示3-5: 可以用“int a[maxn][maxn]”生成提個整型得二維數組,其中maxn 和maxn 不必相等。這個數組共有maxn*maxn個元素,分別為a[0][0],a[0][1],…, a[0][maxm-1],a[1][0],a[1][1],…,a[1][maxm-1],…,a[maxn-1][0],a[maxn-1][1],…, a[maxn-1] [maxm -1]。

    提示3-6: 可以利用C語言簡潔的語法,但前提是保持代碼的可讀性。

    那4條while語句有些難懂,不過十分相似,因此只需介紹其中的第一條:不斷向下走,并且填數。我們的原則是:先判斷,再移動,而不是走一步以后發現越界了再退回來。這樣,則需要進行“預判”,即是否越界,以及如果繼續往下走會不會到達一個已經填過的格子。越界只需要判斷x+1<n因為y的值并沒有修改;下一個格子是(x+1,y),因此只需“a[x+1][y]==0”,簡寫成“!a[x+1][y]”(其中“!”是“邏輯非”運算符)。

    提示3-7: 在很多情況下,最好是在做一件事之前檢查是不是可以做,而不是做完再后悔。因為“悔棋”往往比較麻煩。

    細心地讀者也許會發現這里的一個“潛在Bug”:如果越界,x+1會等于n,a【x+1】【y】將訪問非法內存!幸運的是,這樣的擔心是不必要的。“&&”是短路運算符(還記得我們在哪里提到過嗎?)。如果x+1<n為假,將不會計算“!a【x+1】【y】”,也就不會越界了。
    至于為什么是++tot而不是tot++,留給讀者思考。

    3.2 字符數組


    提示3-8:C語言中的字符型用關鍵詞char表示,它實際存儲的是字符的ASCII碼。字符常量可以用單引號法表示。在語法上可以把字符當作int型使用。

    另一個新內容是“scanf(“%s”,s)”和scanf(“%d”,&n)類似,它會讀入一個不含空格,TAB和回車符的字符串,存入字符數組s。注意,不是“scanf(“%s”,&s)”,s前面沒有“&”字符號。
    提示3-9: 在“scanf("%s", s)”中, 不要在s前面加上“&”符號。 如果是字符串數組chars[maxn] [maxl], 可以用“scanf("%s", s[i])”讀取第i個字符串。 注意, “scanf("%s", s)”遇到空白字符會停下來。

    還有兩個函數是以前沒有遇到的:sprintf和strchr。Strchr的作用是在一個字符串中查找單個字符,而這個sprintf似曾相識:之前用過printf和fprintf。沒錯!這3個函數是“親兄弟”,printf輸出到屏幕,fprintf輸出到文件,而sprintf輸出到字符串。多數情況下,屏幕總是可以輸出的,文件一般也能寫(除非磁盤滿或者硬件損壞),但字符串就不一樣了:應該保證寫入的字符串有足夠的空間。
    提示3-10: 可以用sprintf把信息叔叔到字符串,用法和printf,fprintf類似。但應當保證字符串足夠大,可以容納輸出信息。
    提示3-11: C語言中的字符串是以“\0”結尾的字符數組, 可以用strlen(s)返回字符串s中結束標記之前的字符個數。 字符串中的各個字符是s[0], s[1],…,s[strlen(s)-1]。
    提示3-12: 由于字符串的本質是數組, 它也不是“一等公民”, 只能用strcpy(a, b),strcmp(a, b), strcat(a, b)來執行“賦值”、 “比較”和“連接”操作, 而不能用“=”、 “==”、“<=”、 “+”等運算符。 上述函數都在string.h中聲明。
    另一個例子是“count=count++”。這里對count++的解釋是:count++在表達式中的值是加1之前的值(即原來的值),但計算count++之后count會增加1。問題出現了:這個“”“”稍后再加1”到底是何時進行的呢?如果是計算完復制的右邊(即count++)之后就立刻執行,最后count的值不會變(別忘了最后執行的是賦值);但如果是整個賦值完成之后才加1,最后count的值會比原來多1.如果在理解剛才這段話時感到吃力,最好的方法就是避開它。
    提示3-13: 濫用“++”,“–”,“+=”等可以修改變量值的運算符很容易帶來隱蔽的錯誤。建議每條語句最多只能用一次這種運算符,并且所修改的變量在整條語句中只出現一次。
    提示3-14: 使用fgetc(fin)可以從打開的文件fin中讀取讀取一個字符。一般情況下應當在檢查它不是EOF再將其轉換成char值。從標準輸入讀取一個字符可以用getchar ,它等價于fgetc(stdin)。

    這里有個潛在的陷阱:不同操作系統的回車換行符是不一致的。Windows是“\r”和“\n”兩個字符,linux是“\n”,而Macos是“\r”。如果在windows下讀取windows文件,fgetc和getchar會把“\r”吃掉,只剩下“\n”;但如果要在linux下讀取同樣一個文件,它們會忠實地先讀取“\r”,然后才是“\n”。如果編程時不注意,所寫程序可能會在某個操作系統上是完美的,但是在另一個操作系統上就錯得一塌糊涂。當然,比賽的組織方應該避免在Linux下使用windows格式得文件,但正如前面所強調過的:選手也應該把自己的程序寫得更魯棒,即容錯性更好。

    提示3-15: 在使用fgetc和getchar時,應該避免寫出和操作系統相關的程序。
    提示3-16: "fgets(buf, maxn, fin)“將讀取完整的一行放在字符數組buf中。 應當保證
    buf足夠存放下文件的一行內容。 除了在文件結束前沒有遇到“\n”這種特殊情況外, buf總是以“\n”結尾。 當一個字符都沒有讀到時, fgets返回NULL。
    提示3-17: C語言并不禁止程序讀寫"非法內存”。 例如, 聲明的是char s[100], 完全可以賦值s[10000] = ‘a’( 甚至-Wall也不會警告) , 但后果自負。
    提示3-18: C語言中的gets(s)存在緩沖區溢出漏洞, 不推薦使用。 在C11標準里, 該函數已被正式刪除。
    提示3-19: 善用常量數組往往能簡化代碼。 定義常量數組時無須指明大小, 編譯器會計算。
    提示3-20: 頭文件ctype.h中定義的isalpha、 isdigit、 isprint等工具可以用來判斷字符
    的屬性, 而toupper、 tolower等工具可以用來轉換大小寫。 如果ch是大寫字母, 則ch-'A’就是它在字母表中的序號( A的序號是0, B的序號是1, 依此類推) ; 類似地, 如果ch是數字,則ch-'0’就是這個數字的數值本身。
    提示3-21: 字符還可以直接用ASCII碼表示。如果用八進制,應該寫成:“\o”,”\oo”或”\ooo”(o為一個八進制數字);如果用十六進制,應該寫成“\xh”(h為十六進制數字串)。

    在二進制中,8位最大整數就是8個1,即28-1, 用C語言寫出來就是(1<<8)-1。 注意括號是必需的,
    因為“<<”運算符的優先級沒有減法高。

    補碼表示法。 計算機中的二進制是沒有符號的。 盡管123的二進制值是1111011, -123在計算機內并不表示為-1111011——這個“負號”也需要用二進制位來表示。

    “正號和符號”只有兩種情況, 因此用一個二進制位就可以了。 容易想到一個表示“帶符號32位整數”的方法: 用最高位表示符號( 0: 正數;1: 負數) , 剩下31位表示數的絕對值。 可惜, 這并不是機器內部真正的實現方法。
    在筆者的機器上,語句“printf("%u\n",-1)”的輸出是4294967295(4)。 把-1換成-2、 -3、 -4……后,
    很容易總結出一個規律: -n的內部表示是232-n。 這就是著名的“補碼表示法”( Complement Representation) 。

    提示3-22: 在多數計算機內部,,整數采用的是補碼表示法。
    為什么計算機要用這樣一個奇怪的表示方法呢?前面提到的“符號位+絕對值”的方法哪里不好了?
    答案是:運算不方便。
    試想,要計算1+(-1)的值(為了簡單起見,假設兩個數都是帶符號8位整數)。如果用“符號位+絕對值”法,將要計算00000001+10000001,而答案應該是00000000。似乎想不到什么簡單的方法進行這個“加法”。但如果采用補碼表示,計算的是00000001+11111111,只需要直接相加,并丟掉最高位的進位即可。“符號位+絕對值”還有一個好玩的Bug:存在兩種不同的0:一個是吧00000000(正0),一個是10000000(負0)。這個問題在補碼表示法中不會出現。
    http://uva.onlinejudge.org/

    #4 函數和遞歸

    函數是“過程是程序設計”的自然產物,單頁產生了局部變量,參數傳遞方式,遞歸等諸多新的知識點。
    主要目的在于理解這紛繁復雜的,最后的語法。同時,通過gdb,可以從根本上幫助讀者理解,看清事物的本質。

    4.1 自定義函數和結構體

    提示4-1:C語言中的數學函數可以定義成“返回類型 函數名(參數列表){函數體}”,其中函數體的最后一條語句應該是“return表達式;”。
    提示4-2: 函數的參數和返回值最好是“一等公民”,如int,char或者double等。其他“非一等公民”作為參數和返回值要復雜一些。如果函數不需要返回值,則返回類型應寫成void。

    注意:這里的return是一個動作,而不是描述。

    提示4-3: 如果在執行函數的·過程中碰到了return語句,將直接退出這個函數,不去執行后面的語句。相反,如果在執行過程中始終沒有retutrn語句,則會返回一個不確定的值。幸好,-wall可以捕捉到這一可疑情況并產生警告。
    順便說一句,main函數也是有返回值的!到目前為止,我們總是讓它返回0,這個0是什么意思呢?盡管沒有專門說明,讀者應該已經發現了,main函數是整個程序的入口。換句話說,有一個“其他的程序”來調用這個main函數——如操作系統,IDE,調試器,甚至自動評測系統。這個0代表“正常結束”,即返回給調用者。在算法競賽中,除了有特殊規定之外,請總是讓其返回0,避免評測系統錯誤地認為程序異常退出了。
    提示4-4: 在算法競賽中,請總是讓main函數返回0。
    提示4-5: 在C語言中,定義結構體的方法為”struct 結構體名稱{域定義};”,注意花括號的后面還有一個分號。
    提示4-6:為了使用方便,旺旺用“typedef struct{域定義;}類型名;”的方式定義一個新類型名。這樣,就可以像原生數據類型一樣使用這個自定義類型。
    提示4-7: 即使最終答案在所選擇的數據類型范圍之內,計算的中間結果仍然可能溢出。
    如何避免溢出?
    辦法是進行“約分”。
    一個簡單的方法是利用n!/m!=(m+1)(m+2)…(n-1)n.雖然不能完全避免中間結果溢出,但對于題目給出的范圍已經可以保證得到正確的結果了。
    提示4-8: 對復雜的表達式進行化簡有時不僅能減少計算量,還能減少甚至避免中間結果溢出。
    提示4-9: 建議把謂詞(用來判斷某事物是否具有某種特性的函數)命名成“is_xxx”的形式,返回int值,非0表示真能,0表示假。
    提示4-10: 編寫函數時,應盡量保證該函數能對任何合法參數得到正確的結果。如若不然,應在顯著位置標明函數的缺陷,以避免誤用。

    4.2 函數調用與參數傳遞

    程序里有兩個變量a,一個在main函數里定義,一個是swap的形參,二者不會混淆嗎?不會。函數(包括main函數)的形參和在該函數里定義的變量都被稱為該函數的局部變量(local variable)。不同函數的局部變量相互獨立,即無法訪問其他函數的局部變量。需要注意的是,局部變量的存儲空間是臨時分配的,函數執行完畢時,局部變量的空間將被釋放,其中的值無法保留到下次使用。與此對應的是全局變量(global variable):此變量在函數外聲明,可以在任何時候,由任何函數訪問。需要注意的是,應該謹慎使用全局變量。

    4.2.2 調用棧(call stack)

    For循環的學習:多演示程序執行的過程,把注意力集中在“當前代碼行”的轉移和變量值的變化。

    調用棧描述的是函數之間的調用關系。它由多個棧幀(stack frame)組成,每個棧幀對應著一個未運行完的函數。棧幀中保存了該函數的返回地址和局部變量,因而不僅能在執行完畢后找到正確的返回地址,還很自然地保證了不同函數間的局部變量互不相干——因為不同函數對應著不同的棧幀。
    提示4-12: C語言用調用棧(call stack)來描述函數之間的調用關系。調用棧由棧幀(stack frame)組成,每個棧幀對應著一個未運行完的函數。在gdb中可以用backtrace(簡稱bt)命令打印所有棧幀信息。若要用p命令打印一個非當前棧幀的局部變量,可以用frame命令選擇另一個棧幀。

    提示4-13: C語言的變量都是放在內存中的,而內存中的每個字節都有一個稱為地址(address)的編號。每個變量都占有一定數目的字節(可用sizeof運算符獲得),其中第一個字節的地址稱為變量的地址。
    提示4-14: 用int* a聲明的變量a是指向int型變量的指針。賦值a=&b的含義是把變量b的地址存放在指針a中,表達式*a代表a指向的變量,既可以放在賦值符號的左邊(左值),也可以放在右邊(右值)。

    注意:**a是指“a指向的變量”,而不僅是“a指向的變量所擁有的值”。理解這一點相當重要。例如,a=a+1就是讓a指向的變量自增1.甚至可以把它寫成(a)++。注意不要寫成a++,因為“++”運算符的優先級高于“取內容”運算符“”,實際上會被解釋成(a++)。 有了指針,C語言變得復雜了很多。一方面,需要了解更多底層的內容才能徹底解釋一些問題,包括運行時的地址空間布局,以及操作系統的內存管理方式等。另一方面,指針的存在,是的C語言中變量的說明變得異常復雜——你能輕易地說出用

    char * const **next)()

    聲明的next是什么類型的嗎?
    這是一個指向函數的指針,該函數返回一個指針,該函數指向一個只讀的指針,此指針指向一個字符變量。

    算法競賽的核心是算法,沒有必要糾纏如此復雜的語言特性。了解底層的細節是有益的(事實上,前面已經介紹了一些底層細節),但在編程時應盡量避開,只遵守一些注意事項即可。

    提示4-15: 千萬不要濫用指針,這不僅會把自己搞糊涂,還會讓程序產生各種奇怪的錯誤。
    提示4-16: 以數組為參數調用函數時,實際上只有數組首地址傳遞給了函數,需要另加一個參數表示元素個數。除了把數組首地址本身作為實參外,還可以利用指針加減法吧其他元素的首地址傳遞給函數。

    寫法一先進行了一次指針減法,算出了從begin到end(不含end)的元素個數n,然后再像前面那樣把begin作為“數組名”進行累加。
    寫法二看起來更“高級”,事實上也更具一般性,用一個新指針p作為循環變量,同時累加其指向的值。
    這兩個函數的調用方式與之前相似,例如,聲明了一個長度為10的數組a,則它的元素之和就是sum(a,a+10);
    若要計算a【i】,a【i+1】,…,a【j】,則需要調用sum(a+i,a+j+1)。
    把數組作為指針傳遞給函數時,數組內容是可以修改的。因此如果要寫一個“返回數組”的函數,可以加一個數組參數,然后在函數內修改這個數組的內容。

    【分析】
    既然字母可以重排,則每個字母的位置并不重要,重要的是每個字母出現的次數。這樣可以先統計出兩個字符串中各個字母出現的次數,得到兩個數組cnt1【26】和cnt2【26】。下一步需要一點想象力:只要兩個數組排序之后的結果相同,輸入的;;兩個串就可以通過重排和一一映像變得相同。這樣,問題的核心就是排序。
    C語言的stdlib.h中有一個叫qsort的庫函數,實現了著名的快速排序算法。它的聲明是這樣的:

    void qsort ( void * base, size_t num, size_t size, int ( * comparator ) ( const void *, const void *) );

    前3個參數不難理解,分別是待排序的數組起始地址,元素個數和每個元素的大小。最后一個參數比較特別,是一個指向函數的指針,該函數應當具有這樣的形式:

    int cmp(const void *, const void *) {}

    這里的新內容是指向常數的“萬能”的指針:const void * ,它可以通過強制類型轉化變成任意類型的指針。

    4.3 遞歸

    定義
    (1)1是正整數。
    (2)如果n是正整數,n+1也是正整數。
    (3)只有通過(1),(2)定義出來的才是正整數。
    這樣的定義也是遞歸的:在“正整數”還沒有定義完時,就用到了“正整數”的定義。這和前面“參見遞歸”在本質上是相同的,只是沒有它那么直接和明顯。
    同樣地,可以遞歸定義“常量表達式”(以下簡稱表達式):
    (1) 整數和浮點數都是表達式。
    (2) 如果A是表達式,則(A)是表達式。
    (3) 如果A和B都是表達式,則A+B,A-B,A*B,A/B都是表達式。
    簡潔而嚴密,這就是遞歸定義的優點。

    4.3.2 遞歸函數

    數學函數也可以遞歸定義。例如,階乘函數f(n)=n!可以定義為:

    提示4-17: C語言支持遞歸,即函數可以直接或間接地調用自己。但要注意為遞歸函數編寫終止條件,否則將產生無限遞歸。

    4.3.3 C語言對遞歸的支持

    首先用bf命令設置斷點——除了可以按行號設置外,也可以直接給出函數名,斷點將設置在函數的開頭。下面用r命令運行程序,,并在斷點處停下來。接下來用s命令單步執行:

    看到了嗎?在第一次斷點處,n=3(3是main函數中的調用參數),接下來將調用f(3-1),即f(2),因此單步一次后顯示n=2.由于n==0仍然不成立,繼續遞歸調用,直到n=0.這時不再遞歸調用了,執行一次s命令以后會到達函數的結束位置。
    提示4-18:由于使用了調用棧,C語言支持遞歸。在C語言中,調用自己和調用其他函數并沒有本質不同。

    如果仍然無法理解上面的調用棧,可以作如下的比喻:
    皇帝(擁有main函數的棧幀):大臣,你給我算一下f(3)。
    大臣(擁有f(3)的棧幀):知府,你給我算一下f(2)。
    知府(擁有f(2)的棧幀):縣令,你給我算一下f(1)。
    縣令(擁有f(1)的棧幀):師爺,你給我算一下f(0)。
    師爺( 擁有f(0)的棧幀) : 回老爺, f(0)=1。
    縣令: (心算f(1)=f(0)*1=1) 回知府大人, f(1)=1。
    知府: ( 心算f(2)=f(1)*2=2) 回大人, f(2)=2。
    大臣: ( 心算f(3)=f(2)*3=6) 回皇上, f(3)=6。
    皇帝滿意了。

    雖然比喻不恰當,但也可以說明一些問題。遞歸調用時新建了一個棧幀,,并且跳轉到了函數開頭處執行,就好比皇帝找大臣,大臣找知府這樣的過程。盡管同一時刻可以有多個棧幀(皇帝,大臣,知府同時處于“等待下級回話”的狀態),但“當前代碼行”只有一個。

    4.3.4 段錯誤與棧溢出

    總結

    以上是生活随笔為你收集整理的《算法竞赛入门经典》——刘汝佳的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。