關(guān)于統(tǒng)計數(shù)字問題的算法
一本書的頁碼從自然數(shù)1開始順序編碼直到自然數(shù)n。書的頁碼按照通常的習(xí)慣編排,每個頁碼都不含多余的前導(dǎo)數(shù)字0。例如第6頁用6表示而不是06或006。數(shù)字統(tǒng)計問題要求對給定書的總頁碼,計算出書的全部頁碼中分別用到多少次數(shù)字0,1,2,3,.....9。
這個題目有個最容易想到的n*log10(n)的算法。這是自己寫的復(fù)雜度為O(n*log10(n))的代碼:
void statNumber(int n) {
int i, t;
int count[10] = {0};
for(i = 1; i <= n; i++) {
t = i;
while(t) {
count[t%10]++;
t/=10;
}
}
for(i = 0; i < 10; i++) {
printf("%d/n", count[i]);
}
}
仔細(xì)考慮m個n位十進制數(shù)的特點,在一個n位十進制數(shù)的由低到高的第i個數(shù)位上,總是連續(xù)出現(xiàn)10^i個0,然后是10^i個1……一直到10^i個9,9之后又是連續(xù)的10^i個0,這樣循環(huán)出現(xiàn)。找到這個規(guī)律,就可以在常數(shù)時間內(nèi)算出第i個數(shù)位上每個數(shù)字出現(xiàn)的次數(shù)。而在第i個數(shù)位上,最前面的10^i個0是前導(dǎo)0,應(yīng)該把它們減掉。
這樣,可以只分析給定的輸入整數(shù)n的每個數(shù)位,從面可以得到一個log10(n)的算法,代碼如下:
void statNumber(int n) {
int m, i, j, k, t, x, len = log10(n);
char d[16];
int pow10[12] = {1}, count[10] = {0};
for(i = 1; i < 12; i++) {
pow10[i] = pow10[i-1] * 10;
}
sprintf(d, "%d", n);
m = n+1;
for(i = 0; i <= len; i++) {
x = d[i] - '0';
t = (m-1) / pow10[len-i];
count[x] += m - t * pow10[len-i];
t /= 10;
j = 0;
while(j <= x-1) {
count[j] += (t + 1) * pow10[len-i];
j++;
}
while(j < 10) {
count[j] += t * pow10[len - i];
j++;
}
count[0] -= pow10[len-i]; /* 第i個數(shù)位上前10^i個0是無意義的 */
}
for(j = 0; j < 10; j++) {
printf("%d/n", count[j]);
}
}
通過對隨機生成的測試數(shù)據(jù)的比較,可以驗證第二段代碼是正確的。
對兩段代碼做效率測試,第一次隨機產(chǎn)生20萬個整數(shù),結(jié)果在我的電腦上,第二段代碼執(zhí)行1.744秒。第一段代碼等我吃完鈑回來看還是沒反應(yīng),就強行關(guān)了它。
第二次產(chǎn)生了1000個整數(shù),再次測試,結(jié)果第一段代碼在我的電腦上執(zhí)行的時間是
10.1440秒,而第二段代碼的執(zhí)行時間是0.0800秒。
其原因是第一段代碼時間復(fù)雜度為O(n*log10(n)),對m個輸入整數(shù)進行計算,則需要的時間為 1*log10(1) + 2*log10(2) + ... + m*log10(m), 當(dāng)n > 10時,有n*log10(n) > n,所以上式的下界為11+12+....+m,其漸近界為m*m。對于20萬個測試數(shù)據(jù),其運行時間的下界就是4*10^10。
同樣可得第二段代碼對于n個輸入數(shù)據(jù)的運行時間界是n*log10(n)的。
上面的代碼中有個pow10數(shù)組用來記錄10^i,但10^10左右就已經(jīng)超過了2^32,但是題目給定的輸入整數(shù)的范圍在10^9以內(nèi),所以沒有影響。
原著中給出的分析如下:
考察由0,1,2...9組成的所有n位數(shù)。從n個0到n個9共有10^n個n位數(shù)。在這10^n個n位數(shù)中,0,1,2.....9第個數(shù)字使用次數(shù)相同,設(shè)為f(n)。f(n)滿足如下遞推式:
n>1:
f(n) = 10f(n-1)+10^(n-1)
n = 1:
f(n) =1
由此可知,f(n) = n*10^(n-1)。
據(jù)此,可從高位向低位進行統(tǒng)計,再減去多余的0的個數(shù)即可。
著者的思想說的更清楚些應(yīng)該是這樣:
對于一個m位整數(shù),我們可以把0到n之間的n+1個整數(shù)從小到大這樣來排列:
000......0
.............
199......9
200......0
299......9
.........
這樣一直排到自然數(shù)n。對于從0到199......9這個區(qū)間來說,拋去最高位的數(shù)字不看,其低m-1位恰好就是m-1個0到m-1個9共10^(m-1)個數(shù)。利用原著中的遞推公式,在這個區(qū)間里,每個數(shù)字出現(xiàn)的次數(shù)(不包括最高位數(shù)字)為(m-1)*10^(m-2)。假設(shè)n的最高位數(shù)字是x,那么在n之間上述所說的區(qū)間共有x個。那么每個數(shù)字出現(xiàn)的次數(shù)x倍就可以統(tǒng)計完這些區(qū)間。再看最高位數(shù)字的情況,顯然0到x-1這些數(shù)字在最高位上再現(xiàn)的次數(shù)為10^(m-1),因為一個區(qū)間長度為10^(m-1)。而x在最高位上出現(xiàn)次數(shù)就是n%10^(m-1)+1了。接下來對n%10^(m-1),即n去掉最高位后的那個數(shù)字再繼續(xù)重復(fù)上面的方法。直到個位,就可以完成題目要求了。
比如,對于一個數(shù)字34567,我們可以這樣來計算從1到34567之間所有數(shù)字中每個數(shù)字出現(xiàn)的次數(shù):
從0到9999,這個區(qū)間的每個數(shù)字的出現(xiàn)次數(shù)可以使用原著中給出的遞推公式,即每個數(shù)字出現(xiàn)4000次。
從10000到19999,中間除去萬位的1不算,又是一個從0000到9999的排列,這樣的話,從0到34567之間的這樣的區(qū)間共有3個。所以從00000到29999之間除萬位外每個數(shù)字出現(xiàn)次數(shù)為3*4000次。然后再統(tǒng)計萬位數(shù)字,每個區(qū)間長度為10000,所以0,1,2在萬位上各出現(xiàn)10000次。而3則出現(xiàn)4567+1=4568次。
之后,拋掉萬位數(shù)字,對于4567,再使用上面的方法計算,一直計算到個位即可。
下面是自己的實現(xiàn)代碼:
void statNumber_iterative(int n) {
int len, i, k, h, m;
int count[10] = {0};
int pow10[12] = {1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};
char d[16];
len = log10(n); /* len表示當(dāng)前數(shù)字的位權(quán) */
m = len;
sprintf(d, "%d", n);
k = 0; /* k記錄當(dāng)前最高位數(shù)字在d數(shù)組中的下標(biāo) */
h = d[k] - '0'; /* h表示當(dāng)前最高位的數(shù)字 */
n %= pow10[len]; /* 去掉n的最高位 */
while(len > 0) {
if(h == 0) {
count[0] += n + 1;
h = d[++k] - '0';
--len;
n %= pow10[len];
continue;
}
for(i = 0; i < 10; i++) {
count[i] += h * len * pow10[len-1];
}
for(i = 0; i < h; i++) {
count[i] += pow10[len];
}
count[h] += n + 1;
--len;
h = d[++k] - '0';
n %= pow10[len];
}
for(i = 0; i <= h; i++) {
count[i] += 1;
}
/* 減去前導(dǎo)0的個數(shù) */
for(i = 0; i <= m; i++) {
count[0] -= pow10[i];
}
for(i = 0; i < 10; i++) {
printf("%d/n", count[i]);
}
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助。
相關(guān)文章
淺談C++的語句語法與強制數(shù)據(jù)類型轉(zhuǎn)換
這篇文章主要介紹了淺談C++的語句語法與強制數(shù)據(jù)類型轉(zhuǎn)換,是C++入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-09-09
c++11多種格式時間轉(zhuǎn)化為字符串的方法實現(xiàn)
本文主要介紹了c++11多種格式時間轉(zhuǎn)化為字符串的方法實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
c++中為什么可以通過指針或引用實現(xiàn)多態(tài)詳解
這篇文章主要給大家介紹了關(guān)于c++中為何可以通過指針或引用實現(xiàn)多態(tài),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
學(xué)好C++必須做到的50條 絕對經(jīng)典!
學(xué)好C++必須做到的50條,絕對經(jīng)典!想要學(xué)好C++的朋友一定要認(rèn)真閱讀本文,更要做到以下50條2016-09-09

