許多教材一開始就用 Josephus 問題(約瑟夫環)講授演算法。本文先回顧陣列與結構體解法,再展示如何以面向物件抽象思維重構,對比三種寫法的可讀性、維護性與擴充性,體會 OOP 的核心價值。
N 位小孩圍成一圈,從 1 開始報數,報到指定數字 M 的小孩退出,剩餘小孩繼續報數,直到最後剩下一位獲勝者。編寫程式找出獲勝者編號。
用陣列 a[i] 記錄小孩編號,0 表示已退出。利用 (p+1)%N 形成環狀索引。
const int N = 10;
int a[N] = {1,2,3,4,5,6,7,8,9,10};
int interval; cin >> interval;
int p = -1, k = 1;
while (true) {
for (int j = 0; j < interval; ) {
p = (p + 1) % N;
if (a[p] != 0) j++;
}
if (k == N) break;
cout << a[p] << ", ";
a[p] = 0; k++;
}
cout << "\nNo." << a[p] << " boy've won.\n";
缺點: 程式碼緊湊但難讀,邏輯與資料混雜,無封裝,難以擴充(如改變退出規則、加入記錄功能)。
定義 Children { int number; Children* next; },動態配置陣列並串成環。操作指標完成節點刪除。
struct Children { int number; Children* next; };
Children* josephus = new Children[num];
Children* point = josephus;
for (int i = 1; i <= num; i++) {
point->number = i;
point->next = josephus + i % num; // 形成環
point = point->next;
}
// 刪除節點邏輯...
delete[] josephus;
進步: 資料結構更貼近問題本質(環狀),但演算法細節(指標操作、終止條件)仍暴露於主流程,可讀性有限。
將問題分解為兩個核心類別:
Ring 物件並委派任務。類別圖概念:
Josephus (遊戲控制器) - num, interval + initial() - Ring* ring (has-a) Ring (環狀資料結構) - Children* josephus, *point, *cut_point + Ring(int num) // 建構環 + ~Ring() // 釋放記憶體 + ShowRing(int) + CountInterval(int) + OutChild() + ShowWinnerLoser()
主程式極度精簡:
// test4.cpp
#include
#include "josephus.h"
using namespace std;
int main() {
Josephus game;
game.initial();
return 0;
}
Josephus.h / .cpp:
// josephus.h
class Josephus {
public:
Josephus(int n=10, int m=1) : num(n), interval(m) {}
void initial();
protected:
int num, interval;
};
// josephus.cpp
#include "josephus.h"
#include "ring.h"
void Josephus::initial() {
cout << "請輸入孩子總數: "; cin >> num;
if (num < 2) { cout << "至少需 2 人構成環!"; return; }
cout << "請輸入抽選號碼: "; cin >> interval;
if (interval < 1 || interval > num) { cout << "號碼範圍錯誤!"; return; }
Ring ring(num);
ring.ShowRing(num); cout << endl;
for (int i = 1; i < num; i++) {
ring.CountInterval(interval);
ring.ShowWinnerLoser();
ring.OutChild();
}
cout << "\n勝利者是: "; ring.ShowWinnerLoser();
}
Ring.h / .cpp:
// ring.h
struct Children { int number; Children* next; };
class Ring {
public:
Ring(int n);
~Ring();
void ShowRing(int n);
void CountInterval(int interval);
void OutChild();
void ShowWinnerLoser();
protected:
Children *josephus, *point, *cut_point;
};
// ring.cpp
Ring::Ring(int n) {
josephus = new Children[n];
point = josephus;
for (int i = 1; i <= n; i++) {
point->number = i;
point->next = josephus + i % n;
point = point->next;
}
point = &josephus[n-1]; // 起始指標指向最後一節點
}
Ring::~Ring() { delete[] josephus; }
void Ring::ShowRing(int n) {
point = josephus;
for (int i = 1; i <= n; i++) { cout << point->number << ","; point = point->next; }
point = &josephus[n-1];
}
void Ring::CountInterval(int interval) {
for (int i = 0; i < interval; i++) { cut_point = point; point = cut_point->next; }
}
void Ring::OutChild() { cut_point->next = point->next; point = cut_point; }
void Ring::ShowWinnerLoser() { cout << point->number << ","; }
| 維度 | 陣列/結構體 | 面向物件 |
|---|---|---|
| 資料與行為 | 分離,散落在函式中 | 封裝於類別內部 |
| 介面暴露 | 內部指標、索引運算全暴露 | 僅公開 initial() 等高階操作 |
| 修改影響範圍 | 牽一髮動全身 | 修改 Ring 不影響 Josephus,反之亦然 |
| 團隊協作 | 需通讀全碼 | 分工明確:資料結構組維護 Ring,邏輯組維護 Josephus |
| 黑盒效應 | 無 | 使用者只需知道「建立物件、呼叫 initial()」 |
OOP 不是為了寫更少的程式碼,而是為了建立清晰的責任邊界。Josephus 問題雖小,卻完美展示了「封裝、委派、介面導向」如何將複雜度局部化。真實專案中,Ring 可替換為 STL list、資料庫游標或分散式佇列,Josephus 完全不用動一行碼——這才是抽象思維的威力。