[读书笔记]编程之美(二)

本文是《编程之美》读书笔记的第二部分,涵盖了多个编程问题的解决方案,包括求二进制数中1的个数、阶乘末尾0的数量、寻找发帖“水王”、数组中1的个数、寻找最大的K个数、精确表达浮点数、最大公约数问题、找符合条件的整数、斐波那契数列、寻找数组中的最大值和最小值、最近点对、快速寻找和为特定值的两个数、子数组的最大乘积、子数组之和的最大值等。文章通过算法解析和实例探讨了这些问题的高效解法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

[读书笔记]编程之美(二)

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(10n1)=n10n1 。容易得出 n=10101 时, f(n) 的值大于n。
通过类似数学归纳法的思路来推理这个问题,证明满足条件 f(n)=n 的数存在一个上界。
计算这个最大数n
N=10111=99999999999 ,让n从N往0递减,依次检查是否有 f(n)=n ,第一个满足条件的就是我们要求的整数。得出n = 1 111 111 110 是满足 f(n)=n 的最大整数。

2.5寻找最大的K个数

  • 问题:有很多无序的数,假定他们各不相等,怎么选出其中最大的若干个数呢?
  • 思路:
    1.在数据量不大的情况下,可以选择快排或者堆排序O( N/log2N ),选择排序和交换排序O( NK )。在 K(K<=log2N) 较小的情况下,可以选择部分排序。
    2.改进快速排序,(1) Sa 中元素的个数小于K, Sa 中所有的数和 Sb 中最大的 kSa 个元素( Sa Sa 中元素的个数)就是数组S中最大的K个数。
    (2) Sa 中元素的个数大于或等于K,则需要返回 Sa 中最大的K个元素。平均时间复杂度O( N/log2K
    3.寻找N个数中最大的K个数,本质上就是寻找最大的K个数中最小的那个,也就是第K大的数。可以采用二分搜索的策略,整个算法的时间复杂度为O( Nlog2VmaxVmin/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/(10m1))/10n
    X=(a1a2...an)(10m1)+(b1b2...bm)/((10m1)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)分治策略: (FnFn1)=(Fn1Fn2)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(N2M)
  • 核心代码:
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( N2Sum )去求不大于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)×(es+1)2
2N=(s+e)(e-s+1),该等式表示可以将2N分解成两个正整数的乘积。我们设x=s+e, y=e-s+1(其中x>y)。利用x、y我们可以求解获得s和e:

s=xy+12e=x+y12
利用公式(2),我们即可获得正整数N的一个连续自然数序列。为了输出所有的连续自然数序列,我们需要获得所有的x和y组合,也即求2N的所有因子组合。我们可以利用算法基本定理将2N分解成有限个质数的乘积:
2N=2t3j5k...
我们将2N按照质因子进行分解,由于x和y只有一个偶数,所以2N的分解式中质因子2的所有组合都只能在其中一个数中,否则x和y都是偶数。不妨假设x是偶数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值