这个系列是我多年前找工做时对数据结构和算法总结,其中有基础部分,也有各大公司的经典的面试题,最先发布在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;
}
复制代码