三個人的名單,均不均勻能直接算出來
打亂名單決定發言順序有兩種寫法:每次從剩下的位置裡挑一個,或給排序塞一個隨機比較器。後者短,卻從一開始就不在抽籤——三四個元素時路徑少得能一條條走完,分布是算出來的,不是統計出來的。
線上文字隨機打亂是前一種,模板第 80-88 行的迴圈本體:
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;
}
第 81 行讓 i 從 n-1 遞減到 1,第 82 行讓 j 均勻取自 0 到 i 這 (i+1) 檔,第 83-85 行三行全交換、無 i !== j 守衛,每輪消耗隨機數 n-1 個。一條抽樣序列對應 n × (n-1) × …… × 2 = n! 條等機率路徑,每條 1/n!,少到能枚舉(有多陡見排列與組合)。枚舉得:n=3 的 6 條路徑落在 6 個排列上、各 16.67%,n=4 的 24 條落在 24 個上、各 4.17%,最大/最小 1.0000。枚舉靠往第 82 行餵強制值,共 647 個、斷言失敗 0 次。
隨機比較器錯在比較順序不由抽籤決定
反面教材是 arr.sort(() => Math.random() - 0.5)。模板裡 .sort( 出現 0 次,可它在範文裡最常見。排序不是抽籤機器:引擎自己決定先比哪兩個。V8 對短陣列走 TimSort 的二分插入分支,插入第 k 個元素時比幾次取決於前面已給出的答案,同樣 3 個元素比較次數會是 2、3 或 4 次。
枚舉它只需一步抽象:比較器忽略傳入的引數、只回答小於或大於,第 c 次比較是一個二分支點,路徑機率 = 2^(-c),同深度葉節點通分即得每個排列的機率。n=3 共 8 條比較路徑、通分母 2^4 = 16,n=4 共 34 條、樹深 3/5/6、通分母 2^6 = 64。
圖 1|3 個元素時隨機比較器 6 個排列的機率,由 8 條比較路徑全部窮舉而得,非抽樣;虛線是窮舉 6 條抽樣路徑得到的 Fisher-Yates 16.67%。
圖 2|同一寫法在 4 個元素上分成 6 檔、合計 24 個排列,由 34 條比較路徑全部窮舉而得,非抽樣;虛線是枚舉 24 條抽樣路徑的 4.17%。
3 個元素時原序 0 1 2 獨佔 37.50%,是均勻值的 2.2500 倍,最冷三檔各 6.25%,最大/最小 6.0000 倍;4 個元素時 18.75% 對 1.56%,差 12.0000 倍。n=5 的 172 條是 48.0000 倍,n=6 的 1034 條是 192.0000 倍,而均勻值只有 1/720 = 0.14%。
三條邊界:這四個長度上窮舉到的可達排列是 6/6、24/24、120/120、720/720,機率為 0 的有 0 個,所以「某些順序永遠抽不出來」講不了,只能說不均;數字是 V8 的 Array.prototype.sort 實測枚舉,Chrome、Edge、Node 同源,Firefox 與 Safari 不同;跑很多遍只能佐證,那 100000 次用的是 mulberry32、種子 20260213,n=4 給出 1.0696 對 12.8487,與窮舉吻合。權重不同時得換加權抽籤。
貼進輸入框後,誰被釘在原位更要命
第 42 行提示寫著「將文字中所有字元隨機打亂順序」,可第 24 行的「保留空格和換行」預設就 checked,第 113 行判的是 input[i] === ' ' || input[i] === '\n' || input[i] === '\t':錨點恰好是 U+0020、U+000A、U+0009 三個碼位,先被單獨存下,只洗其餘字元再按原下標貼回。
輸入 ab[SP]cd[LF]ef[TAB]gh[SP]ij 輸出 ac[SP]dg[LF]bh[TAB]ji[SP]ef
預設檔消耗 9 個隨機數,洗的是那 10 個碼元;不勾選就改走第 138 行 input.split(""),4 處錨點被改寫 3 處。第 113 行還不含 \r:輸入 aa[SP]bb[CR][LF]cc[SP]dd,勾選保留,CR 仍從下標 5 搬到 11,LF 還在下標 6,行尾就此打散;U+00A0、U+3000 也參與打亂。行模式是另一套:第 152 行只按 LF 切,三行含空行是 3 行、2 個隨機數,勾「打亂前去除空行」後第 161 行顯示過濾過的 總行數=2;它只讀第 149 行那個控制項,隱藏的 ts-times 改成 50 毫無影響。
「打亂次數」這一欄,保底規則和想的不一樣
第 105 與第 149 行都是 parseInt(…, 10) || 1,只接住 0、空與 NaN,負數照樣放行:
| 次數欄填 | 實際輪數 | 隨機數 | 統計區顯示 |
|---|---|---|---|
| 0 | 1 | 5 | 打亂次數=1 |
| 留空 | 1 | 5 | 打亂次數=1 |
| 2.9 | 2 | 10 | 打亂次數=2 |
| -5 | 0 | 0 | 打亂次數=-5 |
| 1000 | 1000 | 9000 | 打亂次數=1000 |
隨機數一欄按 t*(n-1) 計,前四行輸入 6 個碼元、末行 10 個。-5 最彆扭:第 119 行 t < times 直接為假,0 輪、0 個隨機數、輸出即輸入,介面仍顯示「打亂次數=-5」。另一頭也沒人管:第 28/35 行的 max="100" 只是 HTML 屬性,JS 裡 0 處範圍校驗。
沒有種子入口,重現這件事就做不到
seed、crypto、getRandomValues、localStorage 在模板裡命中 0 行,唯一隨機來源是第 82 行的 Math.random()。同輸入同參數,再點一次必然換結果:第 49 行的按鈕只是再跑 times 輪,不是換種子。所以「密碼產生:打亂字元增加隨機性」不成立——它只排列使用者給的字元、不引入新熵,Math.random 是 xorshift128+,狀態 128 位元、輸出 53 位元,不抗預測。
Q:為什麼點了兩次,結果一樣或不一樣?能重現嗎?
可動的只剩一個時必然一樣:單字元 0 個隨機數,六個全同字元照消耗 5 個而輸出不變,三個空格全是錨點;n=2 也只有 1/2 的機率改變順序。想重現也不行,0 個種子參數。
Q:勾上「保留空格和換行」保住了什麼?
空格、換行、定位字元的原位,代價是被洗的字元變少;CR(U+000D)不在名單裡照樣被搬走,那句「所有字元」不準確。
Q:均勻性是跑很多次驗證的嗎?
不是,兩邊都是窮舉:一邊枚舉第 82 行的全部抽樣序列,一邊枚舉全部比較分支,100000 次只是複核。