Hello 算法:重識搜尋演算法——從暴力搜尋到自適應查找的系統化指南 Hello 算法重識搜尋演算法——從暴力搜尋到自適應查找的系統化指南【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo搜尋演算法用於在陣列、鏈結串列、樹或圖等資料結構中定位一個或一組滿足特定條件的元素是資料結構與演算法領域最基礎也最常被重複使用的知識點。本文以《Hello 算法》「重識搜尋演算法」一節為核心從暴力搜尋與自適應搜尋兩大實現思路出發系統梳理線性搜尋、二分搜尋、樹查詢、雜湊查詢的原理、複雜度與適用場景並結合本倉庫中的多語言源碼以 C 語言為例給出可執行的實戰驗證幫助讀者在實際專案中做出正確的搜尋方法選型。搜尋演算法的兩種實現思路搜尋演算法searching algorithm用於在資料結構例如陣列、鏈結串列、樹或圖中搜尋一個或一組滿足特定條件的元素。按照實現思路可以將所有搜尋演算法劃分為兩大類透過走訪資料結構來定位目標元素例如陣列、鏈結串列、樹和圖的走訪等。此類方法不做任何「巧勁」靠完整掃描資料來尋找目標。利用資料組織結構或資料包含的先驗資訊實現高效元素查詢例如二分搜尋、雜湊查詢和二元搜尋樹查詢等。此類方法依賴資料的某種特性如有序性或預先構建的索引結構從而跳過大量無關元素。不難發現這些知識點都已在《Hello 算法》前面的章節中介紹過因此搜尋演算法對讀者而言並不陌生。本節的價值在於從更加系統的視角重新審視它們不再孤立地看待某個演算法而是把它們放在「暴力搜尋 vs 自適應搜尋」的框架下對比理解各自的效率邊界與適用前提。暴力搜尋以時間換通用性暴力搜尋透過走訪資料結構的每個元素來定位目標元素其核心手段就是「遍歷」。典型的代表包括線性搜尋適用於陣列和鏈結串列等線性資料結構。它從資料結構的一端開始逐個訪問元素直到找到目標元素若到達另一端仍未找到則宣告搜尋失敗。在 codes/c/chapter_searching/ 目錄中沒有獨立的線性搜尋範例文件但該思想在two_sum.c的暴力解法中體現得最為直接——雙重迴圈逐一枚舉所有元素對/* 方法一暴力列舉 */ int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) { for (int i 0; i numsSize; i) { for (int j i 1; j numsSize; j) { if (nums[i] nums[j] target) { int *res malloc(sizeof(int) * 2); res[0] i, res[1] j; *returnSize 2; return res; } } } *returnSize 0; return NULL; }該程式碼見 two_sum.c其時間複雜度為 $O(n^2)$是「透過遍歷定位目標」思路的極致體現。廣度優先搜尋BFS與深度優先搜尋DFS這是圖和樹的兩種遍歷策略。廣度優先搜尋從初始節點開始逐層搜尋由近及遠地訪問各個節點其實現依賴佇列深度優先搜尋從初始節點出發沿著一條路徑走到頭再回溯嘗試其他路徑直到遍歷完整個資料結構其實現依賴堆疊或遞迴。本倉庫中的 graph_bfs.c 使用鄰接表與佇列實現了完整的 BFS 流程先將起始頂點入隊並標記為已訪問然後迴圈出隊隊首頂點、記錄訪問結果再將其所有未訪問的鄰接頂點入隊。暴力搜尋的優點是簡單且通用性好無須對資料做預處理也無須藉助額外的資料結構——任何線性結構、樹或圖都可以直接套用。然而此類演算法的時間複雜度為 $O(n)$遍歷一次資料其中 $n$ 為元素數量因此在資料量較大的情況下效能較差。若搜尋過程還伴隨額外操作如上述雙層迴圈代價還會進一步放大。自適應搜尋以預處理換效率自適應搜尋利用資料的特有屬性例如有序性來最佳化搜尋過程從而更高效地定位目標元素。常見的自適應搜尋演算法包括二分搜尋利用資料的有序性實現高效查詢僅適用於陣列因為需要隨機存取與連續記憶體。雜湊查詢利用雜湊表將搜尋資料和目標資料建立為鍵值對映射從而以 $O(1)$ 的平均代價完成查詢。樹查詢在特定的樹結構例如二元搜尋樹中基於比較節點值來快速排除一整棵子樹從而定位目標元素。此類演算法的優點是效率高時間複雜度可達到 $O(\log n)$ 甚至 $O(1)$遠優於暴力搜尋的 $O(n)$。然而使用這些演算法往往需要對資料進行預處理二分搜尋需要預先對陣列進行排序排序本身代價為 $O(n \log n)$雜湊查詢需要構建並維護雜湊表建表代價為 $O(n)$樹查詢需要先建樹$O(n \log n)$且插入、刪除操作還需維護樹的結構如平衡因子。維護這些額外資料結構需要持續的時間與空間開銷。因此自適應搜尋並非「免費午餐」它本質上是用一次性的或持續的預處理成本換取後續每次查詢的高效。!!! tip 術語說明自適應搜尋演算法常被稱為**查詢演算法**主要用於在特定資料結構中快速檢索目標元素。在《Hello 算法》中「搜尋」與「查詢」往往混用但都指向同一類技術。搜尋方法選取四種策略的效率對比給定大小為 $n$ 的一組資料我們可以使用線性搜尋、二分搜尋、樹查詢、雜湊查詢等多種方法從中搜尋目標元素。各方法的操作效率與特性如下表所示對應 searching_algorithm_revisited.md 中的核心對比表線性搜尋二分搜尋樹查詢雜湊查詢查詢元素$O(n)$$O(\log n)$$O(\log n)$$O(1)$插入元素$O(1)$$O(n)$$O(\log n)$$O(1)$刪除元素$O(n)$$O(n)$$O(\log n)$$O(1)$額外空間$O(1)$$O(1)$$O(n)$$O(n)$資料預處理/排序 $O(n \log n)$建樹 $O(n \log n)$建雜湊表 $O(n)$資料是否有序無序有序有序無序從表中可以讀出三條關鍵結論雜湊查詢的單次操作效率最高$O(1)$但它無法維護有序性也無法支援範圍查詢樹查詢是唯一在查詢、插入、刪除三方面都保持 $O(\log n)$ 的選擇是「動態有序資料」場景下的均衡解線性搜尋雖然查詢慢但插入/刪除僅需 $O(1)$ 且無預處理成本在資料量小或更新頻繁時反而可能勝出。搜尋演算法的選擇還取決於資料規模、搜尋效能要求、資料查詢與更新頻率等因素下面逐一展開。線性搜尋通用但慢通用性較好無須任何資料預處理操作。假如我們僅需查詢一次資料那麼其他三種方法的資料預處理時間可能比線性搜尋本身的時間還要長——預處理成本攤不薄就不值得做。適用於體量較小的資料此情況下時間複雜度對效率影響較小。適用於資料更新頻率較高的場景因為該方法不需要對資料進行任何額外維護插入刪除即插即用。二分搜尋穩定但挑剔適用於大資料量的情況效率表現穩定最差時間複雜度為 $O(\log n)$。資料量不能過大因為儲存陣列需要連續的記憶體空間無法像鏈結串列那樣分散存放。不適用於高頻增刪資料的場景因為維護有序陣列的開銷較大——每次插入/刪除都可能觸發 $O(n)$ 的移動。本倉庫在 binary_search.c 中提供了「雙閉區間」與「左閉右開區間」兩種寫法的完整實作/* 二分查找双闭区间 */ int binarySearch(int *nums, int len, int target) { // 初始化双闭区间 [0, n-1] 即 i, j 分别指向数组首元素、尾元素 int i 0, j len - 1; // 循环当搜索区间为空时跳出当 i j 时为空 while (i j) { int m i (j - i) / 2; // 计算中点索引 m if (nums[m] target) // 此情况说明 target 在区间 [m1, j] 中 i m 1; else if (nums[m] target) // 此情况说明 target 在区间 [i, m-1] 中 j m - 1; else // 找到目标元素返回其索引 return m; } // 未找到目标元素返回 -1 return -1; }值得注意的是程式碼中使用m i (j - i) / 2而非(i j) / 2是為了避免i j整數溢位這在n極大時尤為重要。若資料中存在重複元素還需要邊界查找技巧見 binary_search_edge.c將「找最左邊界」等價轉化為「找插入點」再將「找最右邊界」轉化為「找 target1 的插入點後退一位」。雜湊查詢最快但有條件適合對查詢效能要求很高的場景平均時間複雜度為 $O(1)$。不適合需要有序資料或範圍查詢的場景因為雜湊表無法維護資料的有序性。對雜湊函式和雜湊衝突處理策略的依賴性較高具有較大的效能劣化風險——設計不佳的雜湊函式或衝突解決方案可能將查詢退化到 $O(n)$。不適合資料量過大的情況因為雜湊表需要額外空間來最大程度地減少衝突從而提供良好的查詢效能空間換時間。上述「衝突處理」的風險在 hash_map_chaining.c 中有直觀體現該實現採用鏈式位址法將每個桶設計為一條鏈結串列並透過loadThres負載因數閾值預設 2/3與extendRatio擴容倍數預設 2來動態擴容避免單桶鏈條過長。負載因數越高衝突越頻繁查詢退化風險越大——這正是「雜湊表需要額外空間換效能」的底層原因。雜湊查詢的另一個經典應用是把 $O(n^2)$ 的暴力枚舉優化為 $O(n)$。在 two_sum.c 中「兩數之和」問題的雜湊解法將每個元素以「值 → 索引」的形式存入雜湊表之後只需一次遍歷即可在 $O(1)$ 時間內查詢target - nums[i]是否已出現/* 方法二辅助哈希表 */ int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) { HashTable *hashtable NULL; for (int i 0; i numsSize; i) { HashTable *t find(hashtable, target - nums[i]); if (t ! NULL) { int *res malloc(sizeof(int) * 2); res[0] t-val, res[1] i; *returnSize 2; return res; } insert(hashtable, nums[i], i); } *returnSize 0; return NULL; }該解法正是《Hello 算法》replace_linear_by_hashing.md 一節「用雜湊查詢替換線性搜尋」思想的可執行範例。樹查詢海量資料與範圍查詢的均衡解適用於海量資料因為樹節點在記憶體中是分散儲存的不要求連續記憶體。適合需要維護有序資料或範圍查詢的場景中序遍歷即可得到有序序列。在持續增刪節點的過程中二元搜尋樹可能產生傾斜退化為鏈結串列時間複雜度劣化至 $O(n)$。若使用 AVL 樹或紅黑樹則各項操作可在 $O(\log n)$ 效率下穩定執行但維護樹平衡的操作會增加額外的開銷。本倉庫的 binary_search_tree.c 展示了二元搜尋樹的查詢核心——每比較一次就能排除一整棵子樹/* 查找节点 */ TreeNode *search(BinarySearchTree *bst, int num) { TreeNode *cur bst-root; // 循环查找越过叶节点后跳出 while (cur ! NULL) { if (cur-val num) { // 目标节点在 cur 的右子树中 cur cur-right; } else if (cur-val num) { // 目标节点在 cur 的左子树中 cur cur-left; } else { // 找到目标节点跳出循环 break; } } // 返回目标节点 return cur; }而「傾斜風險」則由 avl_tree.c 中的平衡因子與旋轉操作來對沖balanceFactor定義為左子樹高度減右子樹高度插入或刪除節點後若平衡因子的絕對值大於 1則透過右旋、左旋或其組合先左後右、先右後左使子樹重新平衡確保樹高始終維持在 $O(\log n)$ 量級。實戰驗證如何在本倉庫中執行程式上述所有結論都可以在本倉庫中直接驗證。以 C 語言為例進入 C 程式碼目錄codes/c/使用 CMake 構建見 codes/c/CMakeLists.txt後編譯對應章節的目標執行binary_search、two_sum、binary_search_edge等可執行檔觀察輸出結果與理論複雜度是否吻合。例如two_sum.c 的 Driver Code 會依次列印暴力枚舉與雜湊解法的結果二者應返回相同的索引對binary_search_edge.c 則以含重複元素的陣列{1, 3, 6, 6, 6, 6, 6, 10, 12, 15}驗證左右邊界查找的準確性。此外同一份邏輯還提供了 Python、Java、C、Go、Rust、TypeScript 等十餘種語言實現見 codes/ 各語言目錄方便讀者對照學習。C 語言版本的完整原始碼位於 codes/c/chapter_searching/繁中版本對應 zh-hant/codes/c/chapter_searching/。總結搜尋方法的選型框架回到「重識搜尋演算法」的初衷可以用一張決策清單收束全文只查詢一次、資料量小、或更新極其頻繁→ 線性搜尋避免為一次性查詢付出預處理成本靜態大資料、需要穩定 $O(\log n)$ 查詢→ 先排序再二分搜尋前提是記憶體連續且無高頻增刪查詢為主、對速度極致敏感、不要求有序輸出→ 雜湊查詢但需設計好雜湊函式與衝突處理如鏈式位址 動態擴容並接受額外空間開銷海量動態資料、需要有序性與範圍查詢→ 樹查詢優先選擇 AVL 樹或紅黑樹等自平衡樹以少量平衡維護開銷換取穩定的 $O(\log n)$。搜尋演算法的本質是在「遍歷代價」與「預處理/維護代價」之間做取捨。理解這條主線就能在具體業務場景中快速定位最合適的方案——這也正是本倉庫將此節置於搜尋章節末尾、作為系統性總結的用意所在。【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考