約瑟夫斯問題 (Josephus Problem) 是經典循環淘汰遊戲:N 人圍圈,每數到 M 者出列,求倖存者。本文以三種資料結構實作對比,展示面向物件抽象如何提升可讀性、可維護性與擴充性。
void JosephusArray(int N, int M) {
std::vector a(N);
for (int i = 0; i < N; ++i) a[i] = i + 1; // 編號 1..N
int pos = -1, eliminated = 0;
while (eliminated < N - 1) {
for (int step = 0; step < M; ) {
pos = (pos + 1) % N; // 環狀索引
if (a[pos] != 0) ++step; // 跳過已淘汰 (0 標記)
}
std::cout << a[pos] << " eliminated\n";
a[pos] = 0; // 標記淘汰
++eliminated;
}
// 找倖存者
for (int v : a) if (v != 0) { std::cout << "Survivor: " << v; break; }
}
struct Person {
int id;
bool alive;
};
void JosephusStruct(int N, int M) {
std::vector circle(N);
for (int i = 0; i < N; ++i) circle[i] = {i+1, true};
int pos = -1, aliveCount = N;
while (aliveCount > 1) {
for (int step = 0; step < M; ) {
pos = (pos + 1) % N;
if (circle[pos].alive) ++step;
}
circle[pos].alive = false;
std::cout << circle[pos].id << " eliminated\n";
--aliveCount;
}
for (auto& p : circle) if (p.alive) std::cout << "Survivor: " << p.id;
}
alive 替代魔術數 0)alive)class JosephusNode {
int id;
JosephusNode* next;
bool alive;
public:
JosephusNode(int id_) : id(id_), next(nullptr), alive(true) {}
int GetId() const { return id; }
bool IsAlive() const { return alive; }
void Eliminate() { alive = false; }
JosephusNode* Next() const { return next; }
void LinkNext(JosephusNode* n) { next = n; }
// 找下一個存活節點 (封裝遍歷邏輯)
JosephusNode* NextAlive() const {
JosephusNode* cur = next;
while (!cur->alive) cur = cur->next;
return cur;
}
};
class JosephusGame {
JosephusNode* head;
int n, m;
public:
JosephusGame(int N, int M) : n(N), m(M) {
// 建立循環鏈表
head = new JosephusNode(1);
JosephusNode* prev = head;
for (int i = 2; i <= N; ++i) {
JosephusNode* cur = new JosephusNode(i);
prev->LinkNext(cur);
prev = cur;
}
prev->LinkNext(head); // 閉環
}
~JosephusGame() {
// 釋放鏈表記憶體
JosephusNode* cur = head;
do {
JosephusNode* next = cur->Next();
delete cur;
cur = next;
} while (cur != head);
}
int Play() {
JosephusNode* cur = head;
int aliveCount = n;
while (aliveCount > 1) {
// 移動 m-1 步 (當前算 1)
for (int i = 1; i < m; ++i)
cur = cur->NextAlive();
JosephusNode* victim = cur->NextAlive();
std::cout << "Eliminated: " << victim->GetId() << "\n";
victim->Eliminate();
--aliveCount;
cur = victim->NextAlive(); // 下一輪從下一位開始
}
return cur->GetId();
}
};
int main() {
JosephusGame game(41, 3); // 經典 41 人、間隔 3
int survivor = game.Play();
std::cout << "Survivor: " << survivor << "\n"; // 輸出 31
return 0;
}
| 維度 | 陣列 | 結構體 | 類別 (OOP) |
|---|---|---|---|
| 資料語意 | 隱含 (0=淘汰) | 明確 (alive) | 封裝 (Eliminate()) |
| 演算法複雜度 | O(N×M) | O(N×M) | O(N×M) *可優化為 O(N log N) |
| 程式碼組織 | 單一函數 | 資料+函數分離 | 資料行為綁定、職責分離 |
| 擴充性 | 極差 | 中等 (加欄位易) | 優 (繼承/組合/策略模式) |
| 測試性 | 難 (全域狀態) | 中等 | 優 (可 Mock、單元測試) |
| 記憶體管理 | 自動 (vector) | 自動 | RAII (建構/解構自動管理) |
| 可讀性 | 低 (魔術數、索引運算) | 中 | 高 (領域語言風格) |
// 遞推公式:f(1) = 0; f(i) = (f(i-1) + M) % i
// 最後倖存者編號 = f(N) + 1 (0-based → 1-based)
int JosephusMath(int N, int M) {
int result = 0;
for (int i = 2; i <= N; ++i)
result = (result + M) % i;
return result + 1;
}
此為純演算法優化,不涉及資料結構抽象。實務中若需記錄淘汰順序、支援動態增減人員、視覺化動畫,類別版本優勢明顯。
面向物件抽象不只是「加上 class 關鍵字」,而是識別領域概念、封裝不變式、定義清晰介面、分離關注點。約瑟夫斯問題展示:從陣列的索引運算,到結構體的欄位語意,再到類別的行為封裝與 RAII 管理,每一步都讓程式碼更接近問題領域,而非機器細節。這正是 OOP 的核心價值。