這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏。git
數學是科學之基礎,數字題每每也是被面試玩出花來。數學自己是有趣味的一門學科,前段時間有點遊手好閒,對數學產生了濃厚的興趣,因而看了幾本數學史論的書,也買了《幾何本來》和《陶哲軒的實分析》,看了部分章節,受益良多,有興趣的朋友能夠看看。特別是幾何本來,歐幾里得上千年前的著做,裏面的思考和證實方式真的富有啓發性,老小咸宜。本文先總結下面試題中的數字題,我儘可能增長了一些數學方面的證實,若有錯誤,也請指正。本文代碼都在 這裏。github
題: 寫一個程序,找出前N個質數。好比N爲100,則找出前100個質數。面試
分析: 質數(或者叫素數)指在大於1的天然數中,除了1和該數自身外,沒法被其餘天然數整除的數,如 2,3,5...
。最基本的想法就是對 1 到 N 的每一個數進行判斷,若是是質數則輸出。一種改進的方法是不須要對 1 到 N 全部的數都進行判斷,由於除了 2 外的偶數確定不是質數,而奇數多是質數,可能不是。而後咱們能夠跳過2與3的倍數,即對於 6n,6n+1, 6n+2, 6n+3, 6n+4, 6n+5
,咱們只須要判斷 6n+1
與 6n+5
是不是質數便可。算法
判斷某個數m是不是質數,最基本的方法就是對 2 到 m-1 之間的數整除 m,若是有一個數可以整除 m,則 m 就不是質數。判斷 m 是不是質數還能夠進一步改進,不須要對 2 到 m-1 之間的數所有整除 m,只須要對 2 到 根號m 之間的數整除m就能夠。如用 2,3,4,5...根號m
整除 m。其實這仍是有浪費,由於若是2不能整除,則2的倍數也不能整除,同理3不能整除則3的倍數也不能整除,所以能夠只用2到根號m之間小於根號m的質數去除便可。編程
解: 預先可得2,3,5爲質數,而後跳過2與3的倍數,從7開始,而後判斷11,而後判斷13,再是17...規律就是從5加2,而後加4,而後加2,而後再加4。如此反覆便可,以下圖所示,只須要判斷 7,11,13,17,19,23,25,29...
這些數字。數組
判斷是不是質數採用改進後的方案,即對2到根號m之間的數整除m來進行判斷。須要注意一點,不能直接用根號m判斷,由於對於某些數字,好比 121 開根號多是 10.999999,因此最好使用乘法判斷,如代碼中所示。bash
/**
* 找出前N個質數, N > 3
*/
int primeGeneration(int n)
{
int *prime = (int *)malloc(sizeof(int) * n);
int gap = 2;
int count = 3;
int maybePrime = 5;
int i, isPrime;
/* 注意:2, 3, 5 是質數 */
prime[0] = 2;
prime[1] = 3;
prime[2] = 5;
while (count < n) {
maybePrime += gap;
gap = 6 - gap;
isPrime = 1;
for (i = 2; prime[i]*prime[i] <= maybePrime && isPrime; i++)
if (maybePrime % prime[i] == 0)
isPrime = 0;
if (isPrime)
prime[count++] = maybePrime;
}
printf("\nFirst %d Prime Numbers are :\n", count);
for (i = 0; i < count; i++) {
if (i % 10 == 0) printf("\n");
printf("%5d", prime[i]);
}
printf("\n");
return 0;
}
複製代碼
題: 給定一個整數N,那麼N的階乘N!末尾有多少個0呢?(該題取自《編程之美》)數據結構
解1: 流行的解法是,若是 N!= K10M,且K不能被10整除,則 N!末尾有 M 個0。考慮 N!能夠進行質因數分解,N!= (2X) * (3Y) * (5Z)..., 則因爲10 = 25,因此0的個數只與 X
和 Z
相關,每一對2和5相乘獲得一個 10,因此 0 的個數 M = min(X, Z)
,顯然 2 出現的數目比 5 要多,因此 0 的個數就是 5 出現的個數。由此能夠寫出以下代碼:數據結構和算法
/**
* N!末尾0的個數
*/
int numOfZero(int n)
{
int cnt = 0, i, j;
for (i = 1; i <= n; i++) {
j = i;
while (j % 5 == 0) {
cnt++;
j /= 5;
}
}
return cnt;
}
複製代碼
解2: 繼續分析能夠改進上面的代碼,爲求出1到N的因式分解中有多少個5,令 Z=N/5 + N/(52) + N/(53)+... 即 N/5 表示 1 到 N 的數中 5 的倍數貢獻一個 5,N/(52) 表示 52 的倍數再貢獻一個 5...。舉個簡單的例子,好比求1到100的數因式分解中有多少個5,能夠知道5的倍數有20個,25的倍數有4個,因此一共有24個5。代碼以下:優化
/**
* N!末尾0的個數-優化版
*/
int numOfZero2(int n)
{
int cnt = 0;
while (n) {
cnt += n/5;
n /= 5;
}
return cnt;
}
複製代碼
總結: 上面的分析乏善可陳,不過須要提到的一點就是其中涉及到的一條算術基本定理,也就是 任意大於1的天然數均可以分解爲質數的乘積,並且該分解方式是惟一的。 定理證實分爲兩個部分,存在性和惟一性。證實以下:
存在性證實
使用反證法來證實,假設存在大於1的天然數不能寫成質數的乘積,把最小的那個稱爲n。天然數能夠根據其可除性(是否能表示成兩個不是自身的天然數的乘積)分紅3類:質數、合數和1。
n = a*b
,a 和 b都是大於1小於n的數,由假設可知,a和b均可以分解爲質數的乘積,所以n也能夠分解爲質數的乘積,因此這與假設矛盾。由此證實全部大於1的天然數都能分解爲質數的乘積。惟一性證實
題: 給定一個十進制正整數N,求出從 1 到 N 的全部整數中包含 1 的個數。好比給定 N=23,則包含1的個數爲13。其中個位出現1的數字有 1,11,21,共3個,十位出現1的數字有 10,11...19 共10個,因此總共包含 1 的個數爲 3 + 10 = 13
個。
分析: 最天然的想法莫過於直接遍歷1到N,求出每一個數中包含的1的個數,而後將這些個數相加就是總的 1 的個數。須要遍歷 N 個數,每次計算 1 的個數須要 O(log10N),該算法複雜度爲 O(Nlog10N)。當數字N很大的時候,該算法會耗費很長的時間,應該還有更好的方法。
解: 咱們能夠從1位數開始分析,慢慢找尋規律。
當 N 爲 1 位數時,對於 N>=1,1 的個數 f(N) 爲1。
當 N 爲 2 位數時,則個位上1的個數不只與個位數有關,還和十位數字有關。
f(N) = 3+10 = 13
。若是 N 的個位數 >=1,則個位出現1的次數爲十位數的數字加1;若是 N 的個位數爲0,則個位出現 1 的次數等於十位數的數字。當 N 爲 3 位數時,一樣分析可得1的個數。如 N=123,可得 1出現次數 = 13+20+24 = 57
。
當 N 爲 4,5...K 位數時,咱們假設 N=abcde
,則要計算百位上出現1的數目,則它受到三個因素影響:百位上的數字,百位如下的數字,百位以上的數字。
有以上分析思路,寫出下面的代碼。其中 low 表示低位數字,curr 表示當前考慮位的數字,high 表示高位數字。一個簡單的分析,考慮數字 123,則首先考慮個位,則 curr 爲 3,低位爲 0,高位爲 12;而後考慮十位,此時 curr 爲 2,低位爲 3,高位爲 1。其餘的數字能夠以此類推,實現代碼以下:
/**
* 1-N正整數中1的個數
*/
int numOfOne(int n)
{
int factor = 1, cnt = 0; //factor爲乘數因子
int low = 0, curr = 0, high = 0;
while (n / factor != 0) {
low = n - n/factor * factor; //low爲低位數字,curr爲當前考慮位的數字,high爲高位數字
curr = n/factor % 10;
high = n/(factor * 10);
switch(curr) {
case 0: //當前位爲0的狀況
cnt += high * factor;
break;
case 1: //當前位爲1的狀況
cnt += high * factor + low + 1;
break;
default: //當前位爲其餘數字的狀況
cnt += (high+1) * factor;
break;
}
factor *= 10;
}
return cnt;
}
複製代碼
題: 輸入一個正整數數N,輸出全部和爲N連續正整數序列。例如輸入 15,因爲 1+2+3+4+5=4+5+6=7+8=15
,因此輸出 3 個連續序列 1-五、4-6和7-8。
解1:運用數學規律
假定有 k 個連續的正整數和爲 N,其中連續序列的第一個數爲 x,則有 x+(x+1)+(x+2)+...+(x+k-1) = N
。從而能夠求得 x = (N - k*(k-1)/2) / k
。當 x<=0
時,則說明已經沒有正整數序列的和爲 N 了,此時循環退出。初始化 k=2,表示2個連續的正整數和爲 N,則能夠求出 x 的值,並判斷從 x 開始是否存在2個連續正整數和爲 N,若不存在則 k++,繼續循環。
/**
* 查找和爲N的連續序列
*/
int findContinuousSequence(int n)
{
int found = 0;
int k = 2, x, m, i; // k爲連續序列的數字的數目,x爲起始的值,m用於判斷是否有知足條件的值。
while (1) {
x = (n - k*(k-1)/2) / k; // 求出k個連續正整數和爲n的起始值x
m = (n - k*(k-1)/2) % k; // m用於判斷是否有知足條件的連續正整數值
if (x <= 0)
break; // 退出條件,若是x<=0,則循環退出。
if (!m) { // m爲0,表示找到了連續子序列和爲n。
found = 1;
printContinuousSequence(x, k);
}
k++;
}
return found;
}
/**
* 打印連續子序列
*/
void printContinuousSequence(int x, int k)
{
int i;
for (i = 0; i < k; i++) {
printf("%d ", x++);
}
printf("\n");
}
複製代碼
解2:序列結尾位置法
由於序列至少有兩個數,能夠先斷定以數字2結束的連續序列和是否有等於 n 的,而後是以3結束的連續序列和是否有等於 n 的,...以此類推。此解法思路參考的何海濤先生的博文,代碼以下:
/**
* 查找和爲N的連續序列-序列結尾位置法
*/
int findContinuousSequenceEndIndex(int n)
{
if (n < 3) return 0;
int found = 0;
int begin = 1, end = 2;
int mid = (1 + n) / 2;
int sum = begin + end;
while (begin < mid) {
if (sum > n) {
sum -= begin;
begin++;
} else {
if (sum == n) {
found = 1;
printContinuousSequence(begin, end-begin+1);
}
end++;
sum += end;
}
}
return found;
}
複製代碼
擴展: 是否是全部的正整數都能分解爲連續正整數序列呢?
答案: 不是。並非全部的正整數都能分解爲連續的正整數和,如 32 就不能分解爲連續正整數和。對於奇數,咱們老是能寫成 2k+1
的形式,所以能夠分解爲 [k,k+1]
,因此老是能分解成連續正整數序列。對於每個偶數,都可以分解爲質因數之積,即 n = 2i * 3j * 5k...,若是除了i以外,j,k...均爲0,那麼 n = 2i,對於這種數,其全部的因數均爲偶數,是不存在連續子序列和爲 n 的,所以除了2的冪以外的全部 n>=3
的正整數都可以寫成一個連續的天然數之和。
題: 求取數組中最大連續子序列和,例如給定數組爲 A = {1, 3, -2, 4, -5}
, 則最大連續子序列和爲 6,即 1 + 3 +(-2)+ 4 = 6
。
分析: 最大連續子序列和問題是個很老的面試題了,最佳的解法是 O(N)
複雜度,固然有些值得注意的地方。這裏總結三種常見的解法,重點關注最後一種 O(N)
的解法便可。須要注意的是有些題目中的最大連續子序列和若是爲負,則返回0;而本題若是是全爲負數,則返回最大的負數便可。
解1: 由於最大連續子序列和只可能從數組 0 到 n-1 中某個位置開始,咱們能夠遍歷 0 到 n-1 個位置,計算由這個位置開始的全部連續子序列和中的最大值。最終求出最大值便可。
/**
* 最大連續子序列和
*/
int maxSumOfContinuousSequence(int a[], int n)
{
int max = a[0], i, j, sum; // 初始化最大值爲第一個元素
for (i = 0; i < n; i++) {
sum = 0; // sum必須清零
for (j = i; j < n; j++) { //從位置i開始計算從i開始的最大連續子序列和的大小,若是大於max,則更新max。
sum += a[j];
if (sum > max)
max = sum;
}
}
return max;
}
複製代碼
解2: 該問題還能夠經過分治法來求解,最大連續子序列和要麼出如今數組左半部分,要麼出如今數組右半部分,要麼橫跨左右兩半部分。所以求出這三種狀況下的最大值就能夠獲得最大連續子序列和。
/**
* 最大連續子序列和-分治法
*/
int maxSumOfContinuousSequenceSub(int a[], int l, int u)
{
if (l > u) return 0;
if (l == u) return a[l];
int m = (l + u) / 2;
/*求橫跨左右的最大連續子序列左半部分*/
int lmax = a[m], lsum = 0;
int i;
for (i = m; i >= l; i--) {
lsum += a[i];
if (lsum > lmax)
lmax = lsum;
}
/*求橫跨左右的最大連續子序列右半部分*/
int rmax=a[m+1], rsum = 0;
for (i = m+1; i <= u; i++) {
rsum += a[i];
if (rsum > rmax)
rmax = rsum;
}
return max3(lmax+rmax, maxSumOfContinuousSequenceSub(a, l, m),
maxSumOfContinuousSequenceSub(a, m+1, u)); //返回三者最大值
}
複製代碼
解3: 還有一種更好的解法,只須要 O(N)
的時間。由於最大 連續子序列和只多是以位置 0~n-1
中某個位置結尾。當遍歷到第 i 個元素時,判斷在它前面的連續子序列和是否大於0,若是大於0,則以位置 i 結尾的最大連續子序列和爲元素 i 和前面的連續子序列和相加;不然,則以位置 i 結尾最大連續子序列和爲a[i]。
/**
* 最打連續子序列和-結束位置法
*/
int maxSumOfContinuousSequenceEndIndex(int a[], int n)
{
int maxSum, maxHere, i;
maxSum = maxHere = a[0]; // 初始化最大和爲a[0]
for (i = 1; i < n; i++) {
if (maxHere <= 0)
maxHere = a[i]; // 若是前面位置最大連續子序列和小於等於0,則以當前位置i結尾的最大連續子序列和爲a[i]
else
maxHere += a[i]; // 若是前面位置最大連續子序列和大於0,則以當前位置i結尾的最大連續子序列和爲它們二者之和
if (maxHere > maxSum) {
maxSum = maxHere; //更新最大連續子序列和
}
}
return maxSum;
}
複製代碼
題: 給定一個整數序列(可能有正數,0和負數),求它的一個最大連續子序列乘積。好比給定數組a[] = {3, -4, -5, 6, -2}
,則最大連續子序列乘積爲 360,即 3*(-4)*(-5)*6=360
。
解: 求最大連續子序列乘積與最大連續子序列和問題有所不一樣,由於其中有正有負還有可能有0,能夠直接利用動歸來求解,考慮到可能存在負數的狀況,咱們用 max[i] 來表示以 a[i] 結尾的最大連續子序列的乘積值,用 min[i] 表示以 a[i] 結尾的最小的連續子序列的乘積值,那麼狀態轉移方程爲:
max[i] = max{a[i], max[i-1]*a[i], min[i-1]*a[i]};
min[i] = min{a[i], max[i-1]*a[i], min[i-1]*a[i]};
複製代碼
初始狀態爲 max[0] = min[0] = a[0]
。代碼以下:
/**
* 最大連續子序列乘積
*/
int maxMultipleOfContinuousSequence(int a[], int n)
{
int minSofar, maxSofar, max, i;
int maxHere, minHere;
max = minSofar = maxSofar = a[0];
for(i = 1; i < n; i++){
int maxHere = max3(maxSofar*a[i], minSofar*a[i], a[i]);
int minHere = min3(maxSofar*a[i], minSofar*a[i], a[i]);
maxSofar = maxHere, minSofar = minHere;
if(max < maxSofar)
max = maxSofar;
}
return max;
}
複製代碼
題: 給定一個正整數 n,判斷該正整數是不是 2 的整數次冪。
解1: 一個基本的解法是設定 i=1
開始,循環乘以2直到 i>=n
,而後判斷 i 是否等於 n 便可。
解2: 還有一個更好的方法,那就是觀察數字的二進制表示,如 n=101000
,則咱們能夠發現n-1=100111
。也就是說 n -> n-1
是將 n 最右邊的 1 變成了 0,同時將 n 最右邊的 1 右邊的全部比特位由0變成了1。所以若是 n & (n-1) == 0
就能夠斷定正整數 n 爲 2的整數次冪。
/**
* 判斷正整數是2的冪次
*/
int powOf2(int n)
{
assert(n > 0);
return !(n & (n-1));
}
複製代碼
題: 求整數二進制表示中1的個數,如給定 N=6,它的二進制表示爲 0110,二進制表示中1的個數爲2。
解1: 一個天然的方法是將N和1按位與,而後將N除以2,直到N爲0爲止。該算法代碼以下:
int numOfBit1(int n)
{
int cnt = 0;
while (n) {
if (n & 1)
++cnt;
n >>= 1;
}
return cnt;
}
複製代碼
上面的代碼存在一個問題,若是輸入爲負數的話,會致使死循環,爲了解決負數的問題,若是使用的是JAVA,能夠直接使用無符號右移操做符 >>> 來解決,若是是在C/C++裏面,爲了不死循環,咱們能夠不右移輸入的數字i。首先 i & 1
,判斷i的最低位是否是爲1。接着把1左移一位獲得2,再和i作與運算,就能判斷i的次高位是否是1...,這樣反覆左移,每次都能判斷i的其中一位是否是1。
/**
* 二進制表示中1的個數-解法1
*/
int numOfBit1(int n)
{
int cnt = 0;
unsigned int flag = 1;
while (flag) {
if(n & flag)
cnt++;
flag = flag << 1;
if (flag > n)
break;
}
return cnt;
}
複製代碼
解2: 一個更好的解法是採用第一個題中相似的思路,每次 n&(n-1)
就能把n中最右邊的1變爲0,因此很容易求的1的總數目。
/**
* 二進制表示中1的個數-解法2
*/
int numOfBit1WithCheck(int n)
{
int cnt = 0;
while (n != 0) {
n = (n & (n-1));
cnt++;
}
return cnt;
}
複製代碼
題: 給定一個無符號整數N,反轉該整數的全部比特位。例若有一個 8 位的數字 01101001
,則反轉後變成 10010110
,32 位的無符號整數的反轉與之相似。
解1: 天然的解法就是參照字符串反轉的算法,假設無符號整數有n位,先將第0位和第n位交換,而後是第1位和第n-1位交換...注意一點就是交換兩個位是能夠經過異或操做 XOR 來實現的,由於 0 XOR 0 = 0
, 1 XOR 1 = 0
, 0 XOR 1 = 1
, 1 XOR 0 = 1
,使用 1 異或 0/1 會讓其值反轉。
/**
* 反轉比特位
*/
uint swapBits(uint x, uint i, uint j)
{
uint lo = ((x >> i) & 1); // 取x的第i位的值
uint hi = ((x >> j) & 1); // 取x的第j位的值
if (lo ^ hi) {
x ^= ((1U << i) | (1U << j)); // 若是第i位和第j位值不一樣,則交換i和j這兩個位的值,使用異或操做實現
}
return x;
}
/**
* 反轉整數比特位-仿字符串反轉
*/
uint reverseXOR(uint x)
{
uint n = sizeof(x) * 8;
uint i;
for (i = 0; i < n/2; i++) {
x = swapBits(x, i, n-i-1);
}
return x;
}
複製代碼
解2: 採用分治策略,首先交換數字x的相鄰位,而後再是 2 個位交換,而後是 4 個位交換...好比給定一個 8 位數 01101010
,則首先交換相鄰位,變成 10 01 01 01
,而後交換相鄰的 2 個位,獲得 01 10 01 01
,而後再是 4 個位交換,獲得 0101 0110
。總的時間複雜度爲 O(lgN)
,其中 N 爲整數的位數。下面給出一個反轉32位整數的代碼,若是整數是64位的能夠以此類推。
/**
* 反轉整數比特位-分治法
*/
uint reverseMask(uint x)
{
assert(sizeof(x) == 4); // special case: only works for 4 bytes (32 bits).
x = ((x & 0x55555555) << 1) | ((x & 0xAAAAAAAA) >> 1);
x = ((x & 0x33333333) << 2) | ((x & 0xCCCCCCCC) >> 2);
x = ((x & 0x0F0F0F0F) << 4) | ((x & 0xF0F0F0F0) >> 4);
x = ((x & 0x00FF00FF) << 8) | ((x & 0xFF00FF00) >> 8);
x = ((x & 0x0000FFFF) << 16) | ((x & 0xFFFF0000) >> 16);
return x;
}
複製代碼