编程之美2 -数字中的技巧

1、求2进制中1的个数 

解法一:利用位操作,原数与它减一的结果相与,得到的结果为将最后一个1置为0,由此类推。

  解法二:查表法,0~256每个数的结果保存在表里,这种方法是一种用空间换时间的做法,适合于频繁调用算法的应用中。

2、与阶乘有关

问题一: 给定一个整数N,求N!中有多少个0

方法一:0与5有关,因此只要判断N个数中有多少个5的指数即可。while(i%5) {ret++;  i /= 5; }

方法二:根据公式,z =  [N/5] + [N/5^2] + [N/ 5^3] + …,while(N){ ret += N/5; N /= 5;

问题二:求N!二进制表示中最低位1的位置

对问题进行转化,将N!一直除以2,直到第一个出现余数的位置即为1,问题转化为N!中含有质因数2的个数。

解法一:类似于上题解法2

解法二:N!含有质因数2的个数,等于N减去N的二进制中含有1的数目。举例说明,例如N为11011,则N的质因数个数为[N/2] + [N/4] + [N/8] + [N/16] = 1101 + 110 + 11 + 1  = 1000 + 100 + 1 + 100 + 10 + 10 + 1 + 1 = 1111+ 111+1 = (10000 – 1) + (1000 – 1) + (10 – 1)  + 1 – 1 = 11011 – N中含1的个数

3、寻找发帖水王,某个n个数的数组中,某一个数出现次数大于n/2

每次删除两个不一样的数,使用一个临时变量保存candidate,记载现在的数。

5、寻找最大的k个数,从无序数组中找到最大的k个数

解法一:快速排序,找到前k个,复杂度O(n*logn),缺点,没有考虑数组大小,有可能超过内存容量

解法二:类似于快速排序,随机取一个值,把比它小的都放在左边,大的都放在右边,得到这个值得排序位置,根据这个位置与k的关系,进行下一步计算。

        解法三:根据二分搜索,找到排序在k位置的那个数,根据解法二,它右侧k个值即为所求值。

解法四:以上方法都需要对数据访问多次,为了避免这一情况,可以维护一个最小堆,这个最小堆的容量为k。由于维护一个最小堆花费的时间复杂度为O(logK),则整个过程时间复杂度为O(N*logk);

解法五:如果这些数范围不大,在某一范围内,可以开辟一段空间,记录每一个数的出现次数

  举例:搜索引擎所在网页,每页都有权威性,即权重。如果要寻找权重最大的k个网页,而网页的权重会不断更新,那么算法如何变动能最快更新并及时返回权重最大的k个网页?

使用堆排序,权重更新时更新堆。要达到快速的更新,我们可以解法5,使用映射二分堆,可以使更新的操作达到O(logn)

映射二分堆其与普通堆不同的地方是它的节点并不真正保存数据单元本身,而是保存指向数据单元的指针。因此当需要交换父子节点的数据时,我们可以避免拷贝大量数据所消耗的时间。同时,映射二分堆还有一个功能,它可以根据具体的数据单元的索引来删除该单元,即使这个单元不是堆中的最值。

6、精确表达浮点数

给定一个有限小数或者无限循环小数,能否以分母最小的分数形式返回这个小数呢?例如0.333 = 1/ 3

解法:可以将浮点数分为整数部分和小数部分,对于有限小数X = 0.a1a2a3,分数形式为a1a2a3/10^3,对于无限循环小数,y = 0.b1b2…bmb1b2…bm => y * 10^m = b1b2..bm.b1b2…bm => y*10^m – y  = b1b2…bm = > y = b1b2…bm / (10^m – 1). 无限循环小数可以分为有限部分和无限循环部分,要考虑到。

最后得到的结果进行约分即可。A / B = A / gcd(A,B) / B/ gcd(A,B),问题转化为求两个数的最大公约数

7、最大公约数问题

解法一:使用辗转相除法,f(x, y) = f( y, x % y),直到x%y为0,此时的y就是最大公约数。使用递归的方法进行计算。

解法二:类似与辗转相除法,一个数如果能被x、y整除,也一定能被x-y整除,则上面的函数变为f (x-y, y),这样就将取余计算转化为减法,注意,可以取x、y中较大的数减去较小的数。这种方法的缺点在于迭代次数过多。

解法三:考虑到如果x = k * x1, y = k* y1, 则 f(x,y) = k * f(x1,y1) ,如果p是素数,x = p * x1, y%p!=0,则 f(x,y) = f(x1,y)。因为2是素数,则可以用2代替f,若x、y均为偶数,则f(x,y) = 2 * f(x/2,y/2), 若x为偶数,因为奇,则f(x, y) = f(x/2, y), 若x、y均为奇数,则  f(x, y) = f(x-y, y)。属于一个折中的方案

8、给出一个任意整数N,求一个最小的正整数M(M>1),使得N*M只包含0和1

涉及到大数问题,不能按常规思路解题。如果枚举M,可能导致溢出,因此采用枚举N*M的值,只要某个数X%N = 0,则得到所求结果。为了减小多余的计算,采用记录之前的结果。例如 如果已经 遍历k位的情况,需要计算K+1位的情况时,即 X = 10^K + Y,只要保存之前10^K个数的余数,根据余数将它们分组,判断每组最小的值与10^k的和是否能被整除即可,这样搜索空间就从2^K-1维压缩到N-1维。因此本题注意维护一个余数数组。

9、裴波那切数列,参考上一篇日志

10、寻找数组中的最大值和最小值

解法一:将二者看成独立问题,需要2n次比较

解法二:将数组中相邻的两个数分在同一组,通过二者的比较,将较小的与min比较,较大的与max进行比较,这样需要1.5*n次比较。

解法三:使用分治的方法,分别计算n/2个数中的最大最小值,再合并,f(n)=2* f(n/2)+2,最后结果fn = 1.5N -2

11、寻找最近点对,给定平面上N个点的坐标,找出最近的两个点。

对一维的情况,有如下的解法:

解法一:两两计算距离,比较,O(N^2)

解法二:先进行一次排序,之后再两两计算距离,所需复杂度O(n*logn + n)

解法三:借用分治思想,利用一个数将数组分为left、right部分,则最近的两个点要么来自与left、要么来自与right,要么就是left最大数和right中最小数的插值,复杂度为O(n*logn)

扩展到二维情况,仍旧使用分治思想

通过一个水平线,将所有的点分为left和right两个部分,则问题化解为在left中或者right中找最近点,还要考虑的是分别在left和right中的点。假设目前最短距离为mindist,m为中间划分线的位置,则只需要检索 x为(m-mindist,m+mindits)这部分区域里的点。另外如果一个点对的距离小于mindist,则它一定在一个mindist*(2*mindist)的区域内。由于一个区域内,最多有8个顶点,因此针对带状区域内的所有点,进行一个排序,一个点只需要与它紧接着的7个点进行比较就可以,复杂度为O(N)

所以整体复杂度为 f(n) = 2 f(n/2) + O(n) 最后时间是O(n*logn)

扩展问题:

给定数组arr,要找到相邻的两个数的最大差值。

如果数组中最小的数为 min,最大的数为max,根据抽屉原理,则相邻两个数的最大差值一定不会小于delta = (max – min)/(n-1)。把区间[min,max]分成n个桶,则最大差值应该为一个桶的最小值与另一个桶的最大值。整个算法的时间复杂度和空间复杂度都是O(n)

12、快速找到一个数组中的两个数字,让这两个数字之和等于一个定值。

解法一:穷举

解法二:先排序,对于任意一个数字,转化为查看sum-arr[i]是否存在数组中,每次查找要花费O(logn)的时间,总时间为O(n*logn),如果使用hash表存储,则时间变为O(N)

解法三:先排序,计算数组首尾的和,根据如果和比sum大,则尾指针向后一位,如果小,则首指针向前指一位,总共的时间复杂度是O(n*logn)

扩展:

两个数字变为三个数字或者任意数字,如何解?

果数组已排序,利用解法1的双指针遍历法,可以在O(n)的时间内找到两个数之和等于一个給定的数。我们假设找到的三个数ai,aj,ak有ai<=aj<=ak,要让ai+aj+ak=sum,也就是要ai+aj=sum-ak,设subsum=sum-ak,很容易发现subsum的值只有n个,而确定ai+aj=subsum中的ai,aj只需要O(n)的时间,所以总的时间复杂度为O(nlogn+n*n)=O(n^2)

如果没有完全相等的,能否找到一对近似的?

在计算的时候,保存一个最近似的值

13、给定一个长度为n的数组,计算任意n-1个数的组合中最大乘积(只允许用乘法,不允许用除法)

解法1:空间换时间,使用两个大小为n的数组,分别保存从前到后的乘机和从后到前的乘积

解法2:若n个整数的乘机为p,p为0,则除去0即可,若p为负数,则除去负数最大值,若p为正数,则去掉一个最小值

14、给定一个n个整数的一维数组,求子数组和的最大值

解法一:穷举,复杂度为O(n^3)

解法二:分治法,从中间分成两段,递归求解,O(N*logN)

解法三:动态划归,a[i]在最大值中,或者不在,复杂度O(n),max{a[0],start[1]+a[0], all[1]}.start[i] = max{a[i], start[i+1]+[i]}, 

all[i] = max{start[i], all[i+1]},为了进一步简化空间,可以之存储nstart和nall

扩展问题:数组首尾相邻

分成两种情况:1、子数组没有跨过a[0],则类似与上题解

2、子数组跨过a[0],则可以转化为去掉a[i]到a[j], 0<i<j<n,使得a[i]到a[j]的值最小

15、子数组和的最大值,二维情况

解法一:穷举+保存部分和,时间复杂度为O(N^2 * M^2)

解法二:将二维问题转化为一维问题,例如确定了矩形区域的上下边界是第a行和第c行,现在要确定左右边界,可以将一列看成一个数,整个看成一个一维数组,最后的时间复杂度为O(N^2 * M). 也可以先确定左右边界,所以最后的复杂度为O(N * M * min(N,M))。


16、求数组中最长递增子序列

解法一:根据递推公式:lis[i+1] = max{ 1, list[k] + 1}, array[i+1] > array[k],只需要保存每个点的最长递增子序列即可,时间复杂度O(N^2

解法二:基于解法一的优化,采用一个数组记录某个长度的递增子序列的最大值的最小值,可以减少每次比较次数,但是时间复杂度仍为O(N^2)

解法三:由于记录递增子序列中,如果i<j,maxV[i] < maxV[j],是一个有序的数组,因此可以使用二分查找法,时间复杂度为O(N * logN)


17、数组循环移位

见之前的博客


18、数组分割,将一个无序的2n数组分割成元素个数为n的两个数组,并且使得两个数组的和最接近。

解法一:排序,将其按照序列位置奇偶分成两个集合S1、S2,从S1、S2中找两个数交换,使得s1、s2的差值最小。缺点:无法找到最优解

解法二:考虑到整体和可能大于sum/2或者小于sum/2或者等于,则考虑小于sum/2的情况,假设总共需要2n步完成,第k步计算前k个元素中,任意i个元素总共能有多少种和的取值,假设取值集合为Sk-1 = {vi},则Sk = Sk-1 并上 {Vi+arr[k]}。由于插入的次数至多是O(2^N)

解法三:使用动态划归方法,给定一个数组arr[i][v]表示是否可以找到i个数,和为v,复杂度为O(N^2 *sum

19、给定一个源区间,和任意n个无序区间,判断源区间是不是在目标区间内

解法一:将所有区间映射到坐标轴上,由此的复杂度为ON^2

解法二:先将所有区间按照x轴排序,再进行一次合并,最后使用二分查找,看源区间是否被某一不相交区间包含,如果包含,则说明在。排序时间复杂度为On*logn),合并时间复杂度为ON),单次查找时间复杂都为logn,总的时间为On*logn+k*logn

    原文作者:moonboat0331
    原文地址: https://blog.csdn.net/moonboat0331/article/details/9859605
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞