再談非重複隨機序列號生成演算法
這段時間專案中又要開發兌換碼功能,此前的專案已經開發過,但是為了保證這個功能在將來的可重複利用,我決定重構一下相關模組。
原來的模組不是我開發的,但也已經可以完成這個要求。但其中存在兩個問題,這也是基本上非重複隨機序列生成演算法都要面對的問題
(1)是非重複性
(2)是效率
我們原來的程式設計師開發的,是使用的最低效的方式,即隨機生成後,遍歷已經生成的所有隨機序列號,如果重複則放棄這組隨機序列號,重新生成。在這個過程中,首先暴露的是低效,其次是隻能保證本批次的序列號不重複,再次啟動工具生成,兩個批次的隨機序列號就無法保證不重複了。
那麼也會有人問,為什麼不使用GUID之類的演算法,因為兌換碼這個東西,其實有很多額外的需求,比如長度,隨機碼中使用的字元,有純數字的序列號,也有純字母的,所以諸如GUID,MD5,SHA等都不完全適合。
我前幾天曾經轉了一篇非重複隨機序列生成演算法 ,其中作者的思路確實提供我很大的幫助,但是仔細看了作者的實現後,其實也發現了一點欠缺,該文中的演算法解決了效率問題,也解決了重複問題,但是其能生成的序列號數量大大減少。打個比方,如果是生成長度10的序列號,文中的演算法將保證[0~9]的陣列只出現一次,按照排列組合的演算法,最終出現的序列號總數是10*9*7*6*5*4*3*2*1,但實際我們需求的序列號,每一位的數字可以重複,但整個序列號不能產生重複,換句話說,我們的序列號總數應該是10的10次冪。
改進方案
(一)對於一個非重複的隨機序列號,要做到非重複,其N位上出現的字元的順序不能有相同的情況。使用排列組合的知識我們知道,假設我們有N位,每一位上允許的字元有m個的話,我們能產生的不重複的排列組合總量為m1
(二)隨機性,也就是我們從上述m的N次冪的序列中,隨機抽取K個,就能完成我們的目標。當然,不能用隨機數來決定抽取哪一個,這樣還得遍歷已經抽取出來的序列號,來判斷是否兩次挑選序列號是否挑到了同一個
(三)我的演算法思路,首先求得一個排列組合的子集,即s1 * s2 * s3 *,,,*sN = C,C為我們要生成的序列號總量,s是m的一個字元子集,s這個子集是從m集合中抽取的隨機的不重複的字元。這樣就能保證C的序列號是完全不會重複的序列號。s的子集,我使用了轉載文章中作者的演算法,通過構建一個包含m個不同字元的陣列,並對陣列隨機抽取索引,進行移位交換,最後得到一個包含不重複字元的s子集
(四)序列號需求,對於任意長度的序列號,我們只需要增加N,對於序列號中出現的字元限制,我們只需要更改m集合中的字元內容,就可以得到純字元,或是純數字,或是混合的序列號
(五)不同批次的永不重複性,這個我選擇了使用增加字首的方法,比如採用時間,來確定唯一性,然後將時間作為序列號的一部分,加入字首或是字尾,也可以自定義。
其實還有一種方法就是先利用本演算法生成K個較短長度的非重複隨機序列號,K足夠大,保證這批序列號足夠使用,然後存入一個檔案中,以後再生成時,將這K箇中隨機抽取一個,作為批次的標識,生成一次,就拋棄K中的一個,就可以了
附上相應的只要演算法程式碼
if (count.ToString().Length > length)
{
Console.WriteLine("生成數量大於位數,無法生成");
return null;
}
var seed = DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0);
var random = new Random((int)seed.TotalSeconds);
int[] bitTimes = new int[length];
int total_count = 1;
int idx = 0;
while (idx < length)
{
int f = random.Next(0, 10);
bitTimes[idx] = f;
total_count *= f > 0 ? f : 1;
idx++;
if (total_count >= count)
{
break;
}
}
while (total_count < count)
{
for (int i = 0; i < length; i++)
{
if (bitTimes[i] < 10)
{
var c = bitTimes[i];
bitTimes[i] += 1;
total_count = total_count + total_count / c;
}
if (total_count >= count)
{
break;
}
}
}
var codes = new string[total_count];
for (int i = 0; i < length; i++)
{
int coloum = bitTimes[i];
coloum = Math.Max(1, coloum);
int row = total_count / coloum;
int[] num_array = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int[] num = new int[coloum];
int x = 0, tmp = 0, n = 0;
for (int m = num_array.Length - 1; m > 0; m--)
{
x = random.Next(0, m + 1);
tmp = num_array[m];
num_array[m] = num_array[x];
num_array[x] = tmp;
num[n] = num_array[m];
n++;
if (n >= coloum)
{
break;
}
}
for (int j = 0; j < row; j++)
{
for (int k = 0; k < coloum; k++)
{
var index = j * coloum + k;
codes[index] = i == 0 ? num[k].ToString() : codes[index] + num[k];
}
}
}
return codes;