如果要用 JavaScript 儲存一群資料,我們其實很容易想到一個最直覺的答案:
const users = ["Alice", "Bob", "Charlie"];
需要增加資料?
users.push("David");
需要找其中一筆?
users.find(user => user === "Bob");
需要排序?
users.sort();
看起來 Array 幾乎什麼事情都能做。
那問題來了:
如果所有資料都可以塞進 Array,為什麼我們還需要 Queue、Stack、Tree、Graph,甚至 Heap、Hash Table 這麼多不同的資料結構?
這也是這個系列想從第一篇開始回答的問題。
因為資料結構真正處理的,從來不只是:
資料要放在哪裡?
而是:
這些資料彼此之間是什麼關係,還有我們要怎麼操作它們?
假設今天有三個人在排隊:
const waitingList = [
"Alice",
"Bob",
"Charlie"
];
第四個人加入隊伍:
waitingList.push("David");
輪到 Alice 時,我們把第一個人拿掉:
const next = waitingList.shift();
這段程式完全可以運作。
所以我們是不是根本不需要 Queue?

因為 Queue 並不是因為 Array 做不到才存在的。
Queue 真正表達的是一個規則:
First In, First Out。
也就是:
先進來的人,先離開。
當我們說「這是一個 Queue」,我們其實已經對資料加上了一層語意。
資料不再單純表示:
Alice
Bob
Charlie
還要講順序:
進入順序
↓
Alice → Bob → Charlie
↑
離開順序
這個差異才是重點
Array 告訴我們:
我有一群資料。
Queue 告訴我們:
我有一群按照進入順序等待處理的資料。
資料可能完全相同,但問題的表示方式已經不同了。
這可能是整個系列最重要的一個觀念:
Data Structure is a form of problem representation.
我們選擇資料結構,其實是在決定:
我要用什麼方式描述眼前的問題?
例如今天我們有以下資料:
const people = [
"Alice",
"Bob",
"Charlie",
"David"
];
如果只是要保存姓名,Array 處理綽綽有餘。
但假設需求變成:
Alice 認識 Bob 和 Charlie,Bob 認識 David,Charlie 也認識 David。
現在問題的重點已經不再是「有哪些人」,還要能承載:
誰和誰有關係?
我們可能會開始把資料寫成:
const friends = {
Alice: ["Bob", "Charlie"],
Bob: ["Alice", "David"],
Charlie: ["Alice", "David"],
David: ["Bob", "Charlie"]
};
底層依然可以看見 Object 和 Array。
但我們真正描述的東西,其實已經變成:
Alice ─── Bob
│ │
│ │
Charlie ─ David
這就是 Graph 想表達的世界,Graph 並不是一種神奇的新資料。
它只是剛好適合去描述:
這個問題的核心,是節點之間的關係。
假設我們要表示公司的組織:
CEO
├─ Engineering
│ ├─ Frontend
│ └─ Backend
└─ Marketing
當然,我們還是可以把所有資料塞進 Array:
const departments = [
"CEO",
"Engineering",
"Frontend",
"Backend",
"Marketing"
];
資料都在,但更重要的從屬關係消失了。
當 Frontend 是 Engineering 下面的部門,Engineering 又屬於 CEO。
這時候我們想要表達的是:
parent
↓
child
也就是階層關係,於是 Tree 自然出現了。
不是因為 Array 無法儲存這些字串,只是因為 Array 本身已經沒有辦法幫我們表達這種階層。

這也是為什麼不存在一種「最好的資料結構」。
假設今天我們一直需要:
找到下一個等待處理的人。
Queue 很直覺。
如果我們需要:
永遠先處理最高優先權的任務。
那 Priority Queue 或 Heap 可能更合理。
如果我們需要:
表達資料之間的從屬關係。
Tree 會很自然。
如果問題是:
從台北到高雄有哪些可能的路線?
那麼 Graph 可能比 Tree 更接近問題本身。
因此選擇資料結構時,我們真正問的不是:
哪一個比較快?
還是得先問:
我的問題到底長什麼樣子?
當問題被正確表示之後,「快不快」才開始有意義。
我們學演算法時,幾乎一定會碰到資料結構。
因為演算法並不是憑空想捏造的操作步驟,它通常會建立在某種資料表示方式上。
例如我們之後會看到:
Graph
↓
BFS / DFS
↓
Shortest Path
又或者:
Heap
↓
Priority Queue
↓
Scheduling
甚至:
Tree
↓
Tree Traversal
↓
Search
資料結構決定:
問題被表示成什麼形狀。
演算法則決定:
我們準備怎麼在這個結構上移動、搜尋、修改或做決策。
所以很多時候,一個 hard level 演算法問題真正困難的地方,並不在考你那個演算法怎麼寫。
是在考你有沒有看出:
這個問題其實可以被表示成什麼。
講到資料結構和演算法,很快就會遇到另一個讓很多人頭痛的東西:
O(1)
O(log n)
O(n)
O(n²)
我們先不用急著證明它們,甚至暫時不用背。
現在只需要建立一個非常簡單的認知:
當資料越來越多時,我們做某件事情的成本會怎麼變?
假設只有 5 筆資料,從第一筆一路找下去,可能根本沒有感覺。
如果有:
10
100
1,000
1,000,000
筆資料呢?
資料增加之後,不同操作方式之間的差距才會慢慢被放大。
所以 Big-O 真正想幫我們描述的是一種:
成長趨勢。
迫使我們去思考:
如果問題規模變大,這個方法會變得多昂貴?
完整的複雜度比較我們之後再慢慢談。
如果只從語法看:
[]
{}
new Map()
new Set()
我們很容易把資料結構理解成:
JavaScript 提供了很多不同方式讓我們裝資料。
但這只是表面,裡面還是對映著:
Queue
Stack
Tree
Graph
Heap
Hash Table
每一種結構背後,其實都代表某一類問題的形狀:
所以學資料結構真正值得學的是去想:
什麼問題會自然形成 Queue?
如果只記得怎麼 call API,很快就會忘記。
但如果你理解它為什麼存在,那麼即使今天換成 JavaScript、Python、Go,甚至換了一套完全不同的 API,你仍然知道自己需要的是什麼。
所以回到一開始的問題:
如果所有東西都可以塞進 Array,為什麼還需要其他資料結構?
答案其實是:
因為「放得進去」和「適合描述這個問題」是兩件完全不同的事情。
我們當然可以用 Array 模擬 Queue,也可以用 Object 和 Array 建立 Graph,甚至可以自己決定怎麼表示一棵 Tree。
最重要的還是:
你是否理解這些資料之間真正存在的關係。
這也是接下來這個系列真正想討論的事情。
我們不會只是背:
Queue 是 FIFO。
Stack 是 LIFO。
BFS 使用 Queue。
DFS 使用 Stack。
是學會一直追問:
為什麼?
當你開始看見這些結構,演算法就不再只是 LeetCode 裡的一道題目,它開始變成一種理解現實問題的方法。
而這,才是我們真正需要資料結構的原因。
今天先留下四個觀念:
下一篇,我們就可以從最熟悉的 Array 開始。
因為即使是每天都在使用的 Array,背後其實也藏著一個很值得問的問題:
為什麼「按照位置取得資料」這件事情,可以這麼直覺?