系統實現並測試主流內部排序算法,統一測試框架記錄關鍵碼比較次數 (KCN) 與記錄移動次數 (RMN),在 10,000/100,000 整數隨機/有序/逆序數據下量化性能,揭示理論復雜度與實際運行的差異。
template <class T>
void test(T a[], int N, void(*sort)(T[], int, int&, int&)) {
int KCN=0, RMN=0; clock_t t = clock();
sort(a, N, KCN, RMN);
cout << "Time: " << clock()-t << "ms\n"
<< "KCN=" << KCN << " KCN/N=" << (double)KCN/N
<< " KCN/N²=" << (double)KCN/N/N
<< " KCN/NlogN=" << (double)KCN/N/log2(N) << "\n"
<< "RMN=" << RMN << " RMN/N=" << (double)RMN/N
<< " RMN/N²=" << (double)RMN/N/N
<< " RMN/NlogN=" << (double)RMN/N/log2(N) << endl;
}
測試數據:升序、降序、隨機排列(固定種子保證可比性)。
void InsertSort(T a[], int N, int& KCN, int& RMN) {
for (int i=1; i<N; i++) {
T tmp = a[i]; RMN++;
int j; for (j=i; j>0 && ++KCN && tmp < a[j-1]; j--) { a[j]=a[j-1]; RMN++; }
a[j] = tmp; RMN++;
}
}
測試:升序 0ms (KCN≈N),隨機 330ms (KCN≈N²/4),降序 711ms (KCN≈N²/2)。最好/最壞/平均符合理論。
移動指針而非記錄,RMN 極低(理論最大 3(N-1)),但平均耗時反超直接插入(整數移動開銷極小,指針操作額外負擔)。適合大記錄、鏈表結構。
void ShellSort(T a[], int N, int& KCN, int& RMN) {
for (int gap=N/2; gap; gap/=2)
for (int i=gap; i<N; i++) {
T tmp=a[i]; RMN++;
for (int j=i; j>=gap && ++KCN && tmp<a[j-gap]; j-=gap)
{ a[j]=a[j-gap]; RMN++; }
a[j]=tmp; RMN++;
}
}
測試 (10萬):升序 140ms,隨機 230ms,降序 151ms。無明顯最壞情況,附加空間 O(1),在不明數據分布時是優於快排的穩健選擇。
優化:設置 exchange 標誌,無交換提前結束。測試:升序 0ms (KCN=N),隨機 1161ms,降序 1022ms。效率低於直接插入,教學價值大於實用。
核心 Partition:單趟掃描確定樞軸最終位置。
int Partition(T a[], int L, int R, int& KCN, int& RMN) {
int pos = L; T piv = a[L];
for (int i=L+1; i<=R; i++)
if (++KCN && a[i] < piv && ++pos != i)
{ swap(a[i], a[pos]); RMN+=3; }
swap(a[L], a[pos]); RMN+=3;
return pos;
}
void QSort(T a[], int L, int R, int& KCN, int& RMN) {
if (L < R) {
int p = Partition(a, L, R, KCN, RMN);
QSort(a, L, p-1, KCN, RMN);
QSort(a, p+1, R, KCN, RMN);
}
}
測試:隨機 10萬僅 110ms (KCN≈21N),但升序/降序 10萬耗時 42秒!最壞情況極其危險。
取首、中、末中位數作樞軸。升序/降序改善明顯,但代碼複雜,隨機選樞軸效果更好且簡單:
swap(a[L], a[rand(R-L)+L]); // 隨機樞軸
隨機快排測試 (10萬):升序 90ms,隨機 120ms,降序 110ms。質的飛躍,消除最壞情況,工程首選。
KCN 固定 N(N-1)/2,RMN 極小 (0~3N)。測試三種數據耗時接近 (711ms),CPU 流水線優化使比較密集型代碼表現優於移動密集型。
void FilterDown(T a[], int i, int N, int& KCN, int& RMN) {
int c = 2*i+1; T tmp = a[i];
while (c < N) {
if (c+1= a[c]) break;
a[i] = a[c]; RMN++; i = c; c = 2*i+1;
}
a[i] = tmp; RMN++;
}
void HeapSort(T a[], int N, int& KCN, int& RMN) {
for (int i=(N-2)/2; i>=0; i--) FilterDown(a, i, N, KCN, RMN); // 建堆
for (int i=N-1; i>0; i--) { swap(a[0], a[i]); RMN+=3; FilterDown(a, 0, i, KCN, RMN); }
}
測試 (10萬):升序 110ms,隨機 160ms,降序 90ms。無最壞情況,附加空間 O(1),不信任快排時的最佳替代。
| 算法 | 平均 | 最壞 | 空間 | 穩定 | 適用場景 |
|---|---|---|---|---|---|
| 直接插入 | O(N²) | O(N²) | O(1) | 是 | 小數據、近乎有序 |
| 希爾排序 | ~O(N^1.3) | O(N²) | O(1) | 否 | 中等數據、未知分布、嵌入式 |
| 快速排序(隨機) | O(N log N) | O(N²) | O(log N) | 否 | 大數據通用首選、內存敏感 |
| 堆排序 | O(N log N) | O(N log N) | O(1) | 否 | 實時系統、禁止最壞情況 |
| 歸併排序 | O(N log N) | O(N log N) | O(N) | 是 | 穩定要求、鏈表、外部排序 |
| 基數排序 | O(dN) | O(dN) | O(N+r) | 是 | 固定長度整數/字符串鍵 |
排序無銀彈,理解數據特徵(規模、分布、鍵類型、穩定性要求、內存限制)才能做最優選擇。