首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> 關於C

C 語言實作不重複隨機序列:Fisher-Yates 洗牌演算法的記憶體高效變體

     編輯:關於C

需求:產生 0 到 N-1 的隨機排列,每個數字恰好出現一次。最直覺的「隨機產生 + 檢查重複」在 N 較大時會陷入極端惡化的重試迴圈 (耦合佔用問題)。本文介紹一種原地陣列壓縮法,本質上是 Fisher-Yates 洗牌演算法的記憶體友善變體,時間 O(N)、空間 O(N),無重試、無死循環風險。

核心演算法邏輯

  1. 建立兩個陣列:result[N] (輸出)、pool[N] (候選池,初始為 0,1,2...N-1)
  2. 第 i 輪 (i = 0 ~ N-1):從 pool[0 ~ N-1-i] 中隨機選一個索引 r
  3. 將 pool[r] 複製到 result[i]
  4. 將 pool[r] 標記為已用 (如 -1),並將 pool 陣列向前壓縮,移除該空位
  5. 重複直到候選池為空

完整可編譯原始碼 (C99 標準)

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#define MAX_VALUE 10

/* 將陣列中標記為 -1 的元素向前壓縮,返回新的有效長度 */
static int compress_pool(int *pool, int size)
{
    int write = 0;
    for (int read = 0; read < size; ++read) {
        if (pool[read] != -1) {
            pool[write++] = pool[read];
        }
    }
    return write;  // 新的有效長度
}

/* 產生 0..n-1 的隨機排列,存入 result 陣列 */
int rand_array(int *result, int n)
{
    if (!result || n <= 0) return -1;

    int *pool = malloc(n * sizeof *pool);
    if (!pool) return -1;

    /* 初始化候選池 */
    for (int i = 0; i < n; ++i) pool[i] = i;

    int valid = n;  // 目前候選池有效長度
    for (int i = 0; i < n; ++i) {
        int r = rand() % valid;       // 0 ~ valid-1
        result[i] = pool[r];          // 取出隨機元素
        pool[r] = -1;                 // 標記已用
        valid = compress_pool(pool, valid);  // 壓縮陣列
    }

    free(pool);
    return 0;
}

int main(void)
{
    srand((unsigned)time(NULL));  // 真實專案請用更強隨機源
    int list[MAX_VALUE];
    if (rand_array(list, MAX_VALUE) == 0) {
        for (int i = 0; i < MAX_VALUE; ++i)
            printf("list[%d] = %d\n", i, list[i]);
    }
    return 0;
}

關鍵點解析

為何不直接用 Fisher-Yates 原地交換?
標準 Fisher-Yates 直接在單一陣列上交換 a[i] ↔ a[r],最省記憶體。本文的「雙陣列 + 壓縮」寫法較易理解「從候選池抽取」的語意,適合教學展示;生產環境建議改用單陣列交換版。
壓縮步驟的時間成本
每輪壓縮 O(valid),總和為 O(N²)。若 N > 10,000 會明顯變慢。優化:改用單陣列交換法,整體降為 O(N)。
隨機種子問題
範例用 srand(time(NULL)),同一秒內多次執行會產生相同序列。生產環境請採用:
  • Linux: getrandom() / /dev/urandom
  • Windows: BCryptGenRandom() / RtlGenRandom()
  • C++11: std::random_device + std::mt19937

標準 Fisher-Yates (單陣列版) 對照

void fisher_yates_shuffle(int *arr, int n)
{
    for (int i = n - 1; i > 0; --i) {
        int j = rand() % (i + 1);  // 0 ~ i
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

/* 使用方式 */
int main(void)
{
    int a[MAX_VALUE];
    for (int i = 0; i < MAX_VALUE; ++i) a[i] = i;
    fisher_yates_shuffle(a, MAX_VALUE);
    // a 即為隨機排列
}

效能對比表 (N=100,000)

演算法時間複雜度空間複雜度實測耗時 (參考)
隨機產生 + 重試檢查O(N²) 期望,最壞無窮O(N)> 60 秒 (常死循環)
雙陣列壓縮法 (本文)O(N²)O(N)~1.2 秒
Fisher-Yates 原地交換O(N)O(1) 額外~3 ms

工程建議

  1. N < 10,000:雙陣列壓縮法可讀性好,效能可接受
  2. N ≥ 10,000 或高頻呼叫:務必用 Fisher-Yates 原地交換版
  3. 密碼學級隨機性需求:rand() 完全不夠,必須用 CSPRNG
  4. 多執行緒環境:rand() 非執行緒安全,改用 rand_r() 或 thread-local RNG

核心洞見:隨機排列的本質是「從遞減的候選集合中均勻取樣」。Fisher-Yates 用「交換到尾端」隱性實現候選集合縮減,不需額外陣列、不需壓縮,是理論最優解。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved