[读书笔记]编程之美(二)
2.1求二进制数中1的个数
- 问题:对于一个字节(8bit)的无符号整形变量,求其二进制表示中“1”的个数,要求算法的执行效率尽可能高。
- 思路:
n = n & (n-1);
当然因为只有8bit所以直接定义一个256的int数组表,直接返回结果,时间复杂度是O(1)。
2.2不要被阶乘吓到
- 问题:1.给定一个整数N,那么N的阶乘N!末尾有多少个0呢?例如:N=10,N!=3628800,N!的末尾有两个0。
2.求N!的二进制表示中最低位1的位置。 - 思路:从“那些数相乘能得到10”这个角度来考虑,对N!进行质因数分解,N!=(2X方)(3Y)(5Z)…所以res = min(X,Z)。
问题1的解法一
for(int i = 1; i <= N; i++)
{
j = i;
while(j % 5 == 0)
{
res ++;
j /= 5;
}
}
解法二: Z = [N/5] + [N/25] + [N/125] + …
while(N)
{
res += N/5;
N /= 5;
}
问题二:等于求N!含有质因数2的个数。即等于N!含有质因数2的个数加一。
while(N)
{
N = N >>= 1;
res += N;
}
N!含有质因数2的个数,还等于N减去N的二进制中表示1的数目。
2.3寻找发帖“水王”
- 问题:传说,Tango有一“水王”,他不但喜欢发帖,还会回复其他ID发的每个帖子。坊间风闻该“水王”发帖数目超过了帖子总数的一半。如果你有一所有帖子(包括回帖)的列表,其中帖子作者的ID也在表中,你能快速找到这个传说中的Tango水王吗?
- 思路:如果每次删除两个不同的ID(不管是否包含”水王“的ID),那么,在剩下的ID列表中,”水王“ID出现的次数仍然超过总数的一半。
for(int i = nTimes = 0; i < N; i++)
{
if(nTimes == 0)
{
candidate = ID[i], nTimes = 1;
}
else
{
if(candidate == ID[i])
nTimes++;
else
nTimes--;
}
}
return candidate;
2.4 1的数目
- 问题:1.给定一个十进制正整数N,写下从1开始,到N的所有整数,然后数一下其中出现所有”1“的个数。f(12) = 5。
2.满足条件“f(N) = N”的最大的N是多少? - 思路:假设N=abcde,这里a、b、c、d、e分别是十进制数N的各个数位上的数字。如果要计算百位上出现1的次数,它将会受到三个因素的影响:百位上的数字,百位以下的数字,百位以上的数字。
while(n / iFactor != 0) //这里iFactor可以是0~9任意一个数
{
iLowerNum = n - (n/iFactor) * iFactor;
iCurrNum = (n/iFactor) % 10;
iHigherNum = n / (iFactor * 10);
switch(iCurrNum)
{
case 0:
iCount += iHigherNum * iFactor;
break;
case 1:
iCount += iHigherNum * iFactor + iLowerNum + 1;
break;
default:
iCount += (iHigherNum + 1) * iFactor;
break;
}
iFactor *= 10;
}
问题二的解法:
容易从上面的式子归纳出:
f(10n−1)=n∗10n−1
。容易得出
n=1010−1
时,
f(n)
的值大于n。
通过类似数学归纳法的思路来推理这个问题,证明满足条件
f(n)=n
的数存在一个上界。
计算这个最大数n
令
N=1011−1=99999999999
,让n从N往0递减,依次检查是否有
f(n)=n
,第一个满足条件的就是我们要求的整数。得出n = 1 111 111 110 是满足
f(n)=n
的最大整数。
2.5寻找最大的K个数
- 问题:有很多无序的数,假定他们各不相等,怎么选出其中最大的若干个数呢?
- 思路:
1.在数据量不大的情况下,可以选择快排或者堆排序O( N∗/log2N ),选择排序和交换排序O( N∗K )。在 K(K<=log2N) 较小的情况下,可以选择部分排序。
2.改进快速排序,(1) Sa 中元素的个数小于K, Sa 中所有的数和 Sb 中最大的 k−∣∣Sa∣∣ 个元素( ∣∣Sa∣∣ 指 Sa 中元素的个数)就是数组S中最大的K个数。
(2) Sa 中元素的个数大于或等于K,则需要返回 Sa 中最大的K个元素。平均时间复杂度O( N∗/log2K )
3.寻找N个数中最大的K个数,本质上就是寻找最大的K个数中最小的那个,也就是第K大的数。可以采用二分搜索的策略,整个算法的时间复杂度为O( N∗log2∣∣Vmax−Vmin∣∣/delta )。由于delta的取值小于元素差值的最小值,所以时间复杂度跟数据分布相关。
4.如果N很大,我们可以用容量为K的最小堆来存储最大的K个数。时间复杂度为O( N∗/log2K )。如果K仍然很大,我们可以采用分治法。
5.如果N个数都是正整数,且它们的取值范围不大,可以考虑申请空间,采用基数排序、计数排序
2.6精确表达浮点数
- 问题:0.9 = 9/10 , 0.333(3)= 1/3
- 思路:X = 0.a1a2…an(b1b2…bm)
X=(a1a2...an+b1b2...bm/(10m−1))/10n
X=(a1a2...an)∗(10m−1)+(b1b2...bm)/((10m−1)∗10n
对于任意一个A/B,可以简化为(A/Gcd(A,B))/(B/Gcd(A,B))。
2.7最大公约数问题
- 问题:写一个程序,求两个正整数的最大公约数。如果两个正整数都很大,有什么简单的算法吗?
- 思路:解法一:辗转相除法(开销大)
int gcd(int x,int y)
{
return (!y)?x:gcd(y,x%y);
}
解法二:
BigInt gcd(BigInt x, BigInt y)
{
if(x < y)
return gcd(y,x);
if(y == 0)
return x;
else
return gcd(x-y,y);
}
解法三:
解法一的问题在于计算复杂的大整数除法运算,而解法二虽然将大整数的除法运算转换成了减法运算,降低了计算的复杂度,但它的问题在于减法的迭代次数太多。
我们知道,2是一个素数,同时对于二进制表示的大整数而言,可以很容易地将除以2和乘以2的运算转换成移位运算,从而避免大整数除法。
BigInt gcd(BigInt x, BigInt y)
{
if(x < y)
return gcd(y, x);
if(y == 0)
return x;
else
{
if(IsEven(x))
{
if(IsEven(y))
return (gcd(x >> 1, y >> 1) << 1);
else
return gcd(x >> 1, y);
}
else
{
if(IsEven(y))
return gcd(x, y >> 1);
else
return gcd(y, x - y);
}
}
}
2.8找符合条件的整数
- 问题:任意给定一个正整数N,求一个最小的正整数M(M>1),使得N * M的十进制表示形式里只含有1和0.
- 思路:直接求算法复杂度太高,把问题转化为,求一个最小的正整数X,使得X的十进制表示形式里只含有1和0,并且X被N整除。
综上所述,假设最终的结果X有K位,那么直接遍历X,须要 2K 次,而按照我们保留余数信息避免不必要的循环的方法,最多只需要(K-1)* N步。当最终结果比较大时,保留余数信息的算法具有明显的优势。
2.9斐波那契(Fibonacci)数列
- 问题:Fibonacci
- 思路:(1)递归、循环
(2)分治策略: (FnFn−1)=(Fn−1Fn−2)∗A 矩阵 A=(1110) - 核心代码:
Matrix MatrixPow(const Matrix& m, int n)
int Fibonacci(int i)
{
Matrix an = MatrixPow(A, n - 1);
return F1 * an(0,0) + F0 * an(1, 0); //返回Fn
}
2.10寻找数组中的最大值和最小值
- 问题:对于由N个整数组成的数组,需要比较多少次才能把最大和最小的数找出来呢?
- 思路:把数组分成两部分,然后再从这两部分中分别找出最大的数和最小的数。相邻的两个数分到一组,或者分治法,共需要比较1.5N次。
2.11寻找最近点对
- 问题:给定平面上N个点的坐标,找出距离最近的两个点。
- 思路:分治法,通过直线x = M将所有的点分成x < M 和 x > M两部分,在分别求出两部分的最近点对之后,只需要考虑点对CD。一个MDist * (2 * Mdist)的区域里最多有8个点。算法复杂度O(NlgN)。
2.12快速寻找满足条件的两个数
- 问题:能够快速找出一个数组中的两个数字,让这两个数字之和等于一个给定的值。
- 核心代码:
for(int i = 0, j = n - 1; i < j;)
{
if(arr[i] + arr[j] == Sum)
return (i,j);
else if(arr[i] + arr[j] < Sum)
i++;
else
j--;
}
2.13子数组的最大乘积
- 问题:给定一个长度为N的整数数组,只允许用乘法,不能用除法,计算任意(N - 1)个数的组合中乘积最大的一组,并写出算法复杂度。
- 思路:(1)空间换时间从头到尾,再从尾到头扫描一遍数组,再查找最大值,O(N)
(2)可以做一个小的转变,不需要直接求乘积,而是求出数组中正数(+)、负数(-)和0的个数,从而判断总乘积的正负性。
2.14求数组的子数组之和的最大值
- 问题:一个有N个整数元素的一维数组(A[0],A[1],…,A[n-2],A[n-1]),这个数组当然有很多子数组,那么子数组之和的最大值是什么呢?
- 思路:max{A[0],A[0]+Start[1],All[1]}。O(N)
- 核心代码:
int MaxSum(int* A, int n)
{
nStart = A[n-1];
nAll = A[n-1];
for(i = n - 2; i >= 0; i--)
{
nStart = max(A[i], nStart + A[i]);
nAll = max(nStart, nAll);
}
return nAll
}
2.15子数组之和的最大值(二维)
- 问题:i_min,i_max,j_min,j_max
- 思路:我们枚举矩形上下边界然后再用一维情况下的方法确定左右边界,就可以得到二维问题的解。新方法的时间复杂度为 O(N2∗M)
- 核心代码:
int MaxSum(int* A, int n, int m)
{
maximum = -INF;
for(int a = 1; a <= n; a++)
for(int c = a; c <= n; c++)
{
Start = BC(a, c, m);
All = BC(a, c, m);
for(int i = m-1; i >= 1; i--)
{
if(Start < 0)
Start = 0;
Start += BC(a, c, i);
if(Start > All)
All = Start;
}
if(All > maximum)
maximum = All;
}
return maximum;
}
2.16求数组中最长递增子序列
- 问题:写一个时间复杂度尽可能低的程序,求一个一维数组(N个元素)中最长递增子序列的长度。
- 思路:(1)O(
n2
)这个问题可以转换为最长公共子序列问题。如例子中的数组A{5,6, 7, 1, 2, 8},则我们排序该数组得到数组A‘{1, 2, 5, 6, 7, 8},然后找出数组A和A’的最长公共子序列即可。显然这里最长公共子序列为{5, 6, 7, 8},也就是原数组A最长递增子序列。
(2)(nlgn)第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。注意,这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。
2.17数组循环移位
- 问题:设计一个算法,把一个含有N个元素的数组循环右移K位,要求时间复杂度为O(N),且只允许使用两个附加变量。
- 思路:逆序部分,逆序部分,逆序全部
- 核心代码:
RightShift(int* str, int N, int k)
{
K %= N;
Reverse(arr, 0, N - K -1);
Reverse(arr, N - K, N - 1);
Reverse(arr, 0, N - 1);
}
2.18数组分割
- 问题:有一个无序、元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组的和最接近?
- 思路:(1)O(
n2
)动态规划的0-1背包(其实是看了原文才晓得的),将heap[M](M表示从2N中所有可能的M个元素和组成的集合),从下到上(m->1…->N)最终求的完整的heap[N]。要点:可以想成求不大于sum/2的最接近集合
(2)O( N2∗Sum )去求不大于sum/2的所有数能否用从2N中的N个提取的数组合出来,标记所有可能,
2.19区间重合判断
- 问题:给定一个元区间[x,y](y >= x)和N个无序的目标区间[x1,y1],[x2,y2]…[xn,yn],判断源区间[x,y]是不是在目标区间内。
- 思路:对现有的数组进行一些预处理,将无需的目标区间合并成几个有序的区间,这样就可以进行区间的比较。先将目标区间数组按X轴从小到大排序,接着合并成若干个互不相交的区间,运用二分查找来判断源区间[x,y]是否被合并后的这些互不相交的区间中的某一个包含。
排序的时间:O(N * lgN)
合并的时间:O(N)
单次查找的时间:lgN
总的时间:O(N * lgN + k * lgN),k为查询次数。
2.20程序理解和时间分析
略
2.21只考加法的面试题
问题1. 写一个程序,对于一个64位正整数,输出它所有可能的连续自然数(两个以上)之和的算式;
问题2. 例如32就找不到这样的表达,这样的数字有什么规律?
问题3. 在64位正整数中,子序列数目最多的是哪一个?能否用数学知识推导出来?
-思路:将一个正整数表示成连续自然数之和,即N=s+(s+1)+(s+2)+…+(e-1)+e。利用等差数列求和公式,
N=(s+e)×(e−s+1)2
2N=(s+e)(e-s+1),该等式表示可以将2N分解成两个正整数的乘积。我们设x=s+e, y=e-s+1(其中x>y)。利用x、y我们可以求解获得s和e:
⎧⎩⎨⎪⎪s=x−y+12e=x+y−12
利用公式(2),我们即可获得正整数N的一个连续自然数序列。为了输出所有的连续自然数序列,我们需要获得所有的x和y组合,也即求2N的所有因子组合。我们可以利用算法基本定理将2N分解成有限个质数的乘积:
2N=2t∗3j∗5k...
我们将2N按照质因子进行分解,由于x和y只有一个偶数,所以2N的分解式中质因子2的所有组合都只能在其中一个数中,否则x和y都是偶数。不妨假设x是偶数。