n log nlog n
CHAPTER 00A

AI 時代,為什麼還要學資料結構與演算法?

AI 三秒就寫得出一棵二元搜尋樹。那你坐在這裡,到底在學什麼?
能編譯 ≠ 跑得動|選錯結構的代價|記憶體是你的責任|學習迴圈
向下捲動開始
📌 本頁使用方式(課前準備 · 讀完再進第 01 章)

照節次讀:每一節都短,不要跳著看。 ② 動手驗證:有互動元件的地方先自己預測答案,再按按鈕對照。 ③ 做完自我檢核再往下:答錯就回頭重讀該節。 ④ 最後翻 關鍵詞彙卡,能不看答案講出定義才算過關。

CONTENTS · 內容目錄
PROLOGUE · 一個誠實的問題

「AI 三秒就寫得出 BST,那我在這裡幹嘛?」

這個問題值得認真回答,而不是用「基本功很重要」六個字打發掉。

先承認事實:把「幫我用 C++ 實作一棵二元搜尋樹,含插入、搜尋、刪除」貼給任何一個主流模型,你會在幾秒內拿到一份看起來很專業、而且大部分情況真的會編譯過的程式碼。這門課教的每一個資料結構,AI 都寫得出來。

所以真正的問題只有一個:換你來當那個要為結果負責的人時,你憑什麼判斷它寫得對不對。

💡 這一頁想說服你的一件事

AI 讓「產出程式碼」變得幾乎免費,價值就整個移到另一端:選擇、驗證與取捨。這三件事剛好就是資料結構與演算法在教的東西。而 C++ 還多一層 —— 記憶體要你自己管,AI 幫你漏掉的 delete 不會有人提醒你。

PART 01 · 能編譯 ≠ 跑得動

AI 給你的是「能編譯」,不是「n 變成一百萬還能跑」

你要 AI 排序,它給你一段程式。編譯過了,用 20 筆資料測試,0.001 秒,完美。三個月後資料變成一百萬筆,同一段程式跑了四十分鐘還沒結束。

AI 沒有寫錯。你沒說要多快,它也不知道你的 n 會長多大。而「n 長大時會怎樣」正是這門課第 02 章要教的東西:漸近複雜度

資料量 n 1,000
成長級數典型代表操作次數估計時間
(每秒 10⁸ 次操作)
拉動滑桿,看同樣一件事在不同演算法下差多少。
重點 WHY

n 小的時候,所有演算法看起來都一樣快。這正是「用小資料測試」會騙人的原因。

這個差距要用數量級來算:n = 100 萬時,$O(n^2)$ 要花的時間是 $O(n \log n)$ 的五萬倍。到這個地步就不是效能調校的問題了,是能不能做的問題。

課程對照 CH02

第 02 章〈演算法分析〉會帶你用 <chrono> 實際量出這張表,而不是只看理論曲線。

AI 不會主動告訴你「你選的這個做法在 n 很大時會爆炸」,除非你問。而你要先知道有這回事,才問得出來

PART 02 · 選錯結構的代價

AI 會一本正經地選錯資料結構

下面兩個都是真實常見的狀況:程式完全正確、測試全過、AI 也覺得沒問題,但效能是災難。而且這兩個坑,課程自己都準備好了教材。

① 從容器的哪一端進出

課程的 pythonds3/cppds/stack.hpp 裡放了兩個堆疊:Stack 把頂端放在 vector尾端Stack2 放在前端。兩者都提供後進先出的堆疊操作。

比較堆疊頂端放在 vector 前端與尾端的成本
#include "pythonds3/cppds/stack.hpp" #include <iostream> #include <chrono> using namespace std; using namespace std::chrono; int main() { const int N = 50000; Stack<int> good; // top 在 vector 尾端:push/pop 都是 O(1) Stack2<int> bad; // top 在 vector 前端:每次都要搬整排 auto t0 = high_resolution_clock::now(); for (int i = 0; i < N; i++) good.push(i); while (!good.isEmpty()) good.pop(); auto t1 = high_resolution_clock::now(); for (int i = 0; i < N; i++) bad.push(i); while (!bad.isEmpty()) bad.pop(); auto t2 = high_resolution_clock::now(); cout << "Stack (top 在尾端): " << duration<double, milli>(t1 - t0).count() << " ms" << endl; cout << "Stack2 (top 在前端): " << duration<double, milli>(t2 - t1).count() << " ms" << endl; return 0; }
耗時範例
Stack  (top 在尾端): 1.13921 ms
Stack2 (top 在前端): 122.423 ms

vector 的前端插入或刪除,都需要搬移後續元素,單次操作是 $O(n)$。尾端插入是攤還 $O(1)$,尾端刪除是 $O(1)$。執行 StackStack2,比較相同堆疊操作在兩種實作中的耗時。

② 用什麼結構做「在不在裡面」的檢查

逐一比對 vs 雜湊查表(20 萬筆、查 200 次)
#include <iostream> #include <vector> #include <unordered_set> #include <chrono> using namespace std; using namespace std::chrono; int main() { const int N = 200000, ROUNDS = 200; vector<int> v; for (int i = 0; i < N; i++) v.push_back(i); unordered_set<int> s(v.begin(), v.end()); auto t0 = high_resolution_clock::now(); for (int k = 0; k < ROUNDS; k++) { volatile bool hit = false; for (int x : v) if (x == N - 1) { hit = true; break; } // 逐一比對 } auto t1 = high_resolution_clock::now(); for (int k = 0; k < ROUNDS; k++) { volatile bool hit = s.count(N - 1) > 0; // 雜湊查表 (void)hit; } auto t2 = high_resolution_clock::now(); cout << "vector 逐一比對: " << duration<double, milli>(t1 - t0).count() << " ms" << endl; cout << "unordered_set 雜湊查表: " << duration<double, milli>(t2 - t1).count() << " ms" << endl; return 0; }
耗時範例
vector        逐一比對: 102.673 ms
unordered_set 雜湊查表: 0.006698 ms

vector 的搜尋逐一比對元素;unordered_set 利用雜湊值定位,在雜湊分布良好、負載因子受控時,平均查詢成本為 O(1),最差為 O(n)。第 07 章會說明雜湊表如何完成查詢。

⚠️ 最危險的不是編譯錯誤,是「安靜的錯誤」

編譯不過你至少知道要修。但「跑得出正確答案、資料增加後越跑越慢」不會噴任何訊息,它只會在你上線之後、資料長大之後才開始傷人。要抓到這種問題,你必須看得懂那段程式在做什麼。

🔗 這在資料結構課哪裡會用到 · 第 02、05、07 章

第 02 章教你怎麼估這些成本;第 05 章的 StackQueueDeque 全部在做「選哪一端」的決定;第 07 章則是把 unordered_set 背後的雜湊表拆開來自己刻一遍。

PART 03 · C++ 額外的一層

記憶體是你的責任,AI 不會幫你記得還

這一節是 C++ 學生獨有的。用 Python 的人不會遇到,因為那邊有垃圾回收;在 C++,new 出來的東西你不還就永遠不會回來

從鏈結串列到樹,新增節點時都要安排它的生命週期:誰建立、誰持有,以及何時釋放。

資料結構建立什麼何時釋放
鏈結串列儲存資料與連結的節點刪除節點,或銷毀整條串列時
鏈結實作的堆疊與佇列存放元素的節點移除元素,或銷毀容器時
頂點與邊的資料由擁有這些資料的物件管理
儲存鍵與子節點連結的節點刪除節點,或銷毀整棵樹時
忘記還會怎樣:用一個計數器看給你看
#include <iostream> using namespace std; class Node { public: static int alive; // 目前還活著的節點數 int data; Node *next; Node(int d) { data = d; next = NULL; alive++; } ~Node() { alive--; } }; int Node::alive = 0; void buildAndForget() { Node *head = new Node(93); // 借了記憶體 head->next = new Node(26); // ...用完就 return,忘記 delete } void buildAndFree() { Node *head = new Node(93); head->next = new Node(26); delete head->next; // 借了就要還 delete head; } int main() { cout << "一開始還活著的節點: " << Node::alive << endl; buildAndFree(); cout << "buildAndFree 之後: " << Node::alive << " <- 借兩個、還兩個,歸零" << endl; buildAndForget(); cout << "buildAndForget 之後: " << Node::alive << " <- 借兩個、沒還,永遠回不來了" << endl; for (int i = 0; i < 3; i++) buildAndForget(); cout << "再呼叫三次之後 : " << Node::alive << " <- 每呼叫一次就漏兩個" << endl; return 0; }
預期輸出
一開始還活著的節點: 0
buildAndFree   之後: 0  <- 借兩個、還兩個,歸零
buildAndForget 之後: 2  <- 借兩個、沒還,永遠回不來了
再呼叫三次之後    : 8  <- 每呼叫一次就漏兩個

Node::alive 記錄目前還活著的節點數。buildAndForget 每呼叫一次就漏兩個,而且漏掉的那塊記憶體在程式結束前拿不回來。這種錯誤不會噴訊息,只會讓長時間執行的程式越吃越多記憶體。

💡 讓資源的所有者負責釋放

建立動態物件時,先決定誰負責管理它的生命週期。由容器擁有的節點,可在容器的解構子中釋放;使用智慧指標則可讓物件隨擁有者的生命週期自動釋放。先備頁 P4 會把這件事講完整。

還不熟悉 C++ 的基本語法,可以先從P1 的最小程式開始。

回到 AI:你請它「幫我實作一個鏈結串列」,它八成給你一份能跑的程式碼。但有沒有解構式?remove() 之後那個節點 delete 了嗎?它不會主動講,而編譯器也不會抱怨。這是你要看得出來的。

🔗 這在資料結構課哪裡會用到 · 第 04、09 章

第 04 章的 UnorderedList::remove() 與第 09 章的 BST 刪除,都是「把節點從結構裡摘掉」的操作 —— 摘掉之後那塊記憶體歸誰管,就是這一節在問的問題。

PART 04 · 學習迴圈

為什麼「學習迴圈」不能被跳過

學會一件事的迴圈長這樣:想法 → 動手試 → 觀察結果 → 修正理解。四步缺一不可,而且卡住、debug、恍然大悟的那個瞬間,正是理解真正發生的地方。

直接把題目丟給 AI、拿回答案貼上去編譯,等於把整個迴圈砍掉只剩「觀察結果」。程式會動,作業會過,但你腦袋裡什麼都沒長出來。

🍳 一個比喻

照著食譜做菜,你會做出那道菜;但你不會變成廚師。廚師的能力在於:食材換了、火力不同、客人臨時說不吃辣,他還能重新設計一道菜。你想成為哪一種?

這門課裡真正難的從來不是「寫出 quicksort」。難的是「為什麼 quicksort 平均是 $O(n \log n)$、最壞卻是 $O(n^2)$」、「為什麼要選這個 pivot」、「這個指標在遞迴回來之後還指著同一個地方嗎」。這些都是推理,抄一百遍程式碼也長不出來。

✅ 那 AI 到底能不能用?

能,而且應該用,但用法要對。把 AI 當助教用,不要當代工:先自己寫、卡住了問「我這個 segfault 是怎麼來的」、拿到解釋之後自己改。C++ 的編譯錯誤訊息又長又難讀,請 AI 幫你翻譯錯誤訊息是很好的用法 —— 那是在幫你完成迴圈,不是繞過它。

PART 05 · 這關你什麼事

說真的,這門課跟你的未來有什麼關係?

① 這是所有量化領域的共同語言

不管你之後做的是遊戲引擎、嵌入式、高頻交易、影像處理還是科學計算,你都會遇到「資料太多、跑太慢」的那一天。到那天,你會需要雜湊表、堆積、圖的最短路徑這些詞。「再租一台更大的機器」買不了多少時間。

② C++ 的世界裡,看得懂原始碼是基本生存技能

這門課用的 pythonds3/cppds/、你未來會用的 STL、Boost、Eigen,全部是別人寫的資料結構。C++ 的文件出了名地難讀,而 template 的錯誤訊息更是一長串。當文件講不清楚,唯一的辦法就是打開標頭檔看。而標頭檔裡面就是 class、就是指標、就是樹和圖。

③ 「看得懂」正在變稀有

當產出程式碼變得免費,會產出的人就不值錢了。真正變貴的是能審查的人:能看一眼就說「這裡會 $O(n^2)$」「這個 new 沒有配對的 delete」「這個迴圈裡 push_back 之後 iterator 就失效了」。技術面試考白板題、團隊要人 code review,考的都是這一層。

🎯 講白一點

AI 會放大你的能力,也會放大你的無知。懂的人用它跑得更快;不懂的人用它把錯誤生產得更快。

PART 06 · 行動指南

開始之前:五個具體習慣

先估 Big-O 再動手
先預測再執行
問為什麼
每個 new 都找到它的 delete
逐行讀自己的碼

① 先估 Big-O,再動手寫。拿到題目先問自己:n 會多大?我這個做法是幾次方?光是養成這個反射,你就贏過一半的人。

② 先預測輸出,再按執行。本站每一個互動元件都是為了這件事設計的:先在心裡(或紙上)寫下你認為的答案,再按按鈕。猜錯的那一刻,才是你真正學到東西的一刻。

③ 問「為什麼」而不是「幫我寫」。把「幫我實作 AVL 樹」換成「我的旋轉寫在這裡,為什麼平衡因子沒有更新?」—— 前者你拿到程式碼,後者你拿到理解。

④ 每寫一個 new,當場找到它的 delete不是寫完再回頭補,是當下就想清楚「這塊記憶體誰負責還、什麼時候還」。想不清楚,就是你的設計還沒想好。

⑤ 交出去之前逐行讀自己的程式。如果有任何一行你說不出「這行在幹嘛、拿掉會怎樣」,那行就不算你的。

▶ 下一步

讀完這頁,先去 00B · 課前準備與環境安裝 把 compiler 與 notebook 環境弄好;Windows 作業再完成 00C · VS Code 作業實戰,然後從 第 01 章 開始。如果你對 C++ 本身還不太有把握,先掃一遍 先備頁 P1–P9(選讀,不評分)—— 尤其是 P4 的指標與動態記憶體,第 04 章以後幾乎每一頁都要用到。

EX · 自我檢核

讀完了嗎?用 3 題檢查你的理解 觀念題 · 不考語法

EXERCISE 1 · 成本直覺

一段程式在 1,000 筆資料上跑 0.01 秒。如果它是 $O(n^2)$,資料變成 100,000 筆時大約要多久?

(A) 約 1 秒
(B) 約 100 秒
(C) 約 3 小時
EXERCISE 2 · 判斷 AI 的產出

AI 幫你寫了一個鏈結串列,remove() 的實作是把前一個節點的 next 接到後一個節點,然後 return。編譯過了,測試也過了。有問題嗎?

(A) 沒問題,測試過了就好
(B) 摘下來的節點沒有 delete,會洩漏
(C) 指標接的順序一定是錯的
EXERCISE 3 · 學習方法

下面哪一種用 AI 的方式,最能幫助你真的學會這門課?

(A) 直接請它完整實作,再讀一遍它寫的
(B) 自己先寫,卡住時問「我這段為什麼不對」
(C) 完全不用 AI,一切自己來
REFERENCE · 延伸閱讀

資料來源與延伸閱讀

資源為什麼值得看
Martin Fowler — Exploring Generative AI軟體工程視角看生成式 AI:什麼場合適合、什麼場合會付出長期維護代價。
cppds(本課教科書)Problem Solving with Algorithms and Data Structures using C++,本站每章 §徽章都對應此書節號。
cppreference · ContainersSTL 各容器操作的複雜度保證,官方權威。想知道 vector::insert 為什麼慢,這裡有答案。
C++ Tutor逐行視覺化記憶體與指標變化。看不懂指標或遞迴時,貼進去按下一步。
nsysu-math208課程講義與投影片 repo,本站是它的互動版配套。
CARDS · 關鍵詞彙卡

關鍵詞彙卡:點卡片翻面

先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。