函數與演算法的關鍵技巧:
從結構化程式設計、參數傳遞、遞迴到排序與搜尋的全方位解析
在軟體開發的旅程中,我們常需要面對龐大而複雜的程式碼,特別是當功能不斷拓展、需求不斷增加時,程式規模會快速膨脹。這時,如何有效地將程式碼分門別類、降低複雜度、提升可讀性與可維護性,成為開發者無可迴避的挑戰。透過「函數 (Function)」的概念,我們能將程式碼化整為零,將邏輯分解成數個各自獨立的模組,讓多人分工協作成為可能。而在程式設計領域中,除了函數的運用外,「演算法 (Algorithm)」也是不可或缺的基石。演算法指示了一組明確而有限的步驟,用於解決問題或達成特定目的,例如排序資料、搜尋元素、計算階層值或求最大公因數等。掌握函數和演算法的技巧,將使開發者在面對複雜的問題時,能夠以更為優雅的方式找到高效率、高品質的解決方案。
本章中,我們將循序漸進地探索函數在C++中的角色與功能,包括函數的原型宣告、定義、呼叫、參數傳遞與回傳值的規則,同時也會介紹三種參數傳遞策略──傳值呼叫、傳址呼叫、傳參考呼叫,並在各種情境中分析其適用場合與特性。我們也會討論行內函數(inline function)與函數多載(Function Overloading)的機制,展示C++對程式設計靈活性與高效性的支持。此外,我們也將深入遞迴函數(Recursive Function)的本質與應用,並透過介紹演算法的基本特性,帶領讀者從函數的角度體會演算法的奧祕。
最後,我們將以實際範例演示「排序」與「搜尋」這兩種在程式設計與資料處理中極其常見的演算法問題。從最基本的氣泡排序法(Bubble Sort)到二分搜尋法(Binary Search),透過實作和範例運行,協助讀者深刻理解這些演算法的運作原理與應用場景。
6-1 大話函數
在C/C++程式中,函數(Function)是程式碼的基本模組與核心架構,也是結構化程式設計的重要元素。以C++為例,每個程式必定有一個main()函數作為程式進入點,但除此之外,我們也可以自行定義多個具有特定功能的函數,透過函數間的呼叫與組合,將龐雜的問題拆解成較小、易管理的模組。
函數原型宣告與定義
為了讓編譯器在呼叫函數時能提前知道該函數的參數型態與回傳型態,我們需要在呼叫函數前,使用「函數原型宣告(Function Prototype)」。例如:
這行宣告讓編譯器知道存在一個名稱為sum的函數,接收兩個整數參數並回傳整數結果。有了函數原型後,我們才能在程式中安心呼叫sum()。而函數定義(Function Definition)則是在程式某處實際撰寫該函數的細節,如:
函數呼叫
定義好函數後,我們就可以在程式碼中透過「函數呼叫」來使用該功能。例如:
如上所述,sum(x, y)將x與y傳遞給sum函數,以進行計算後返回結果。
6-2 參數傳遞與其他應用
在呼叫函數時,主程式中的變數(稱為「引數」,Argument)會與函數定義中的形參(稱為「參數」,Parameter)發生對應。C++中有三種典型的參數傳遞方式:
傳值呼叫(Call by Value):
在此模式下,函數接收的是參數值的複本,在函數內對參數修改不會影響主程式中原始變數的值。這是C/C++的預設傳遞方式。傳址呼叫(Call by Address):
在此模式下,我們傳入變數的記憶體位址(使用指標),允許函數內直接修改主程式中的變數值。這對需要在函數內直接影響原始資料的情況相當實用。傳參考呼叫(Call by Reference):
在C++中,參考(reference)是一種比指標更直觀的記憶體位址引用方式。透過在參數型態後加上&,讓函數參數成為實際參數的別名,因此在函數內對參數的更動,會直接影響呼叫者程式中的變數。
陣列參數傳遞
當我們需要處理多筆相關資料時,多半會使用陣列(array)。將陣列傳遞給函數時,其實只要傳入陣列名稱(等同於指向第一個元素的指標),再加上必要的長度資訊,即可在函數內操作整個陣列。由於陣列名稱代表一個位址,因此在函數中對陣列元素的更改,也會反映到呼叫者的陣列上。
6-2-5 行內函數
行內函數(inline function)是C++為提升程式執行效率所提供的特性。對於執行次數頻繁、程式碼體積不大的函數,使用inline關鍵字標記後,編譯器可在呼叫該函數時,直接將函數本體展開插入呼叫處,省略實際的函數呼叫開銷。儘管行內函數能提高效率,但濫用可能使程式碼體積膨脹,在使用時仍須謹慎衡量。
6-2-6 函數多載
C++支援「函數多載(Function Overloading)」,使得同一個函數名稱可以對應多個函數實作,只要這些函數的參數數量或型態組合不同即可。透過函數多載,我們可以根據傳入參數的型態或個數,讓編譯器自動選擇合適的函數版本,提升程式可讀性與彈性,減少函數命名衝突。
6-3 認識遞迴
遞迴(Recursion)是另一個讓程式結構更簡潔的技巧。遞迴函數在定義中呼叫自己本身,藉此將問題分解成更小的子問題,直到滿足跳出條件為止。遞迴經常用於結構明確的問題,如階乘計算(n!)、費氏數列(Fibonacci)求值、樹狀資料結構處理等。
撰寫遞迴函數時,必須確保兩件事:
- 反覆過程:函數必須在條件尚未達成時再度呼叫自身,將問題規模縮小。
- 出口條件:必須設計一個清晰可判斷的條件,使遞迴在有限次呼叫後結束。
6-4 探索演算法的趣味
演算法是計算機科學的精髓,任何問題的解決都可化為一組明確且有效的步驟來處理。優秀的演算法能節省執行時間與資源,使程式更高效。演算法必須具備以下五項特性:
- 輸入(Input):有0個或多個輸入。
- 輸出(Output):至少有一個輸出結果。
- 明確性(Definiteness):每個步驟清晰明確,不容歧義。
- 有限性(Finiteness):在有限步驟內必須結束運算。
- 有效性(Effectiveness):每個步驟都可行且可由人類用紙筆推導。
演算法描述可用自然語言、虛擬語言(Pseudo Code)或流程圖(Flow Diagram)來表示,選擇最有助於問題理解與溝通的方法。
6-4-1 排序演算法
排序(Sorting)是演算法中經典又常用的問題。將資料由小到大或由大到小排列有助於後續的搜尋與分析。排序演算法繁多,包括氣泡排序(Bubble Sort)、選擇排序(Selection Sort)、合併排序(Merge Sort)、快速排序(Quick Sort)等,每種演算法各有其特性與適用場景。
此處以氣泡排序法為例:
氣泡排序的想法是透過反覆地比較相鄰元素並加以交換,宛如氣泡往上浮使最大值逐步移動至最後,而經過多次掃描後,即可完成整個陣列的排序。儘管氣泡排序效率不佳(O(n²)),但易於理解與實作。
6-4-2 搜尋演算法
搜尋(Search)演算法是為了解決在大量資料中快速找到特定目標的問題。最基本的搜尋方法是線性搜尋(Linear Search),依序檢視每個元素,直到找到目標或搜尋完所有元素為止。線性搜尋簡單卻效率不佳(O(n))。
若資料事先排序好,我們能使用二分搜尋法(Binary Search),透過在每次比較後將搜尋空間縮小為一半,將搜尋效率提升至O(log n)。二分搜尋的前提是資料已排序,若無法保證排序,即使有二分搜尋的演算法,也無法直接套用。
課後評量與深入思考
- 在理解函數、參數傳遞與遞迴後,嘗試思考:何種情況下應使用傳值呼叫、何時應使用傳址或傳參考呼叫?
- 嘗試將一般的迴圈解決的問題轉換為遞迴的寫法,以感受遞迴程式碼簡化思考過程的優點。
- 嘗試將未排序資料先排序再進行二分搜尋,感受搜尋時間從O(n)降到O(log n)的效率差異。
總結
在本章中,我們從認識函數的角色開始,探討參數傳遞、回傳值與呼叫方式,並進一步掌握遞迴函數的概念與應用。透過函數多載與行內函數,我們領略C++語言對程式設計靈活性的支持。在掌握了函數的工具後,我們接續探討演算法設計中極常見的排序與搜尋問題,透過具體案例(氣泡排序、二分搜尋)深入理解演算法的基本精神與流程。
這些基礎技能與概念是日後學習更複雜演算法(如動態規劃、圖演算法)的堅實基礎。掌握函數與演算法,可謂程式設計師進入更高層次領域的必修課程。在未來面對大規模專案或極其複雜的問題時,您將能以更加優雅而高效的方式解決它們。