【算法精练】位运算 && 阶乘

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41035588/article/details/88746962

位运算


最近碰到很多通过巧妙着`来巧妙解决复杂问题的算法,今天分享的这道题,或许能够开拓你的一些算法思维。


案例一

有一组存放 ID 的数据。并且 ID 取值为 0 - (N-1) 之间,其中只有一个 ID 出现的次数为 1,其他的 ID 出现的次数都等于 2,问如何找到这个次数为 1 的 ID ?

解法一:巧用数组下标

我的第一想法便是采用下标法来解决,把 ID 作为数组 arr 的下标,在遍历 ID 的过程中,用数组记下每个 ID 出现的次数,即每次遍历到 ID = n,则 arr[n]++。

之后我们在遍历数组 arr,找到arr[n] = 1 的ID,该下标 n 便是我们要寻找的目的 ID。

这种方法的时间复杂度为 O(N),空间复杂度为 O(N)。

解法二:巧用哈希表

显然时间复杂度是无法再降低的了,因为我们必须要遍历所有的 ID,所以时间复杂度最少都得为 O(N)了,所以我们要想办法降低空间复杂度。

大家想一个问题,假如我们检测到某个 ID 已经出现了 2 次了,那么这个 ID 的数据我们还需要存储记录吗?大部分的 ID 都出现了 2 次,这一大部分的数据真的需要存储吗?

答是不用的,因为出现 2 次的 ID 不是我们所要找的。所以我们可以优化解法一,我们可以采用哈希表来记录ID 出现的次数:利用哈希表记下每个 ID 出现的次数,每次遇见一个 ID,就把这个 ID 放进 哈希表,如果这个 ID 出现了次数已经为 2 了,我们就把这个 ID 从哈希表中移除,最后哈希表只会剩下一个我们要寻找的 ID。

这个方法最好的情况下 空间复杂度可以降低到 O(1),最坏的情况仍然了 O(N)。

解法三:巧用位运算

那究竟有没办法让空间复杂度在最坏的情况下也是 O(1) 呢?

答是有的,按就是采用异或运算。异或运算有个特点:

相同的两个数异或之后,结果为 0,任何数与0异或运算,其结果不变并且,异或运算支持结合律。

所以,我们可以把所有的 ID 进行异或运算,由于那些出现两次的 ID 通过异或运算之后,结果都为 0,而出现一次的 ID 与 0 异或之后不变,又因为异或支持结合律,所以,把所有 ID 进行异或之后,最后的结果便是我们要找的 ID。

这个方法的空间复杂度为 O(1),巧妙利用了位运算,而且运算的效率是非常高效的。

问题拓展

假如有 2 个 ID 出现的次数为 1,其他 ID 出现的次数都为 2 呢?有该如何解决呢?是否还是可以用位运算呢?

为了方便这里我们先假设 异或 的符号为@
答是必须的,假如这两个出现一次的 ID 分别为 A, B,则所有 ID 异或后的结果为 A@B,这时我们遇到的问题是无法确定 A,B的值。

由于A 和 B 是不一样的值,所以 A@B 的结果不为 0,也就是说,这个异或值二进制中某一位为1。显然,A 和 B 中有且仅有一个数的相同位上也为 1。

这个时候,我们可以把所有 ID 分为两类一类在这个位上为 1,另一类为 0,那么对于这两类,``一类会含有 A,另一类会含有 B`。于是,我们可以分别计算这两类 ID 的异或值,几可得到 A 和 B 的值。

总结

做题的时候,不妨多加上一个想法:是否可以用的上位运算这种思路。

阶乘详析


对于如何算 n 的阶乘,只要你知道阶乘的定义,我想你都知道怎么算,但如果在面试中,面试官抛给你一道与阶乘相关,看似简单的算法题,你还真不一定能够给出优雅的答案!本文将分享几道与阶乘相关的案例,且难度递增。


案例一

给定一个整数 N,那么 N 的阶乘 N! 末尾有多少个 0?
例如: N = 10,则 N!= 3628800,那么 N! 的末尾有两个0。
有些人心想,这还不简单,直接算出 N!的值,然后用除以 10 来判断多少个 0 就可以了。

有些人则这样想,直接算 N!的值再来除以 10 判断多少个 0 的话肯定会出现溢出问题于是开始思索:一个数乘以 10 就一定会在末尾产生一个零,于是,我们可以从“哪些数相乘能够得到 10”入手。

没错,只有2 * 5才会产生 10。

注意,4 * 5 = 20 也能产生 0 啊,不过我们也可以把 20 进行分解啊,20 = 10 * 2。
于是,问题转化为 N! 种能够分解成多少对 2*5,再一步分析会发现,在 N!中能够被 2 整除的数一定比能够被 5 整除的数多,于是问题就转化为求1…n 这 n 个数中能够被 5 整除的数有多少个,注意,像 25 能够被 5整除两次,所以25是能够产生 2 对 2 * 5滴。有了这个思路,代码如下:

 int f(int n)
 {
 	int sum = 0;
 	for(int i = 1; i <= n; i++){
 	int j = i;
 	while(j % 5 == 0){
 	sum++;
 	j = j / 5;
 	}
 	}
 return sum;
 }

有些人想出了这个规律,很是得意,然而,这还不是面试官要的答案,大家想一个问题,

当 N = 20 时,1~20 可以产生几个 5 ?答是 4 个,此时有 N / 5 = 4。

当 N = 24 时,1~24 可以产生几个 5 ?答是 4 个,此时有 N / 5 = 4。

当 N = 25 时,1~25 可以产生几个 5?答是 6 个,主要是因为 25 贡献了两个 5,此时有 N / 5 + N / 5^2 = 6。

…

可以发现 产生 5 的个数为sum = N/5 + N/5^2 +N/5^3+….

于是,最优雅的写法应该是这样:

 int f(int n){
 int sum = 0;
 while(n! = 0){
  sum += n / 5;
  n = n / 5;
  }
 }

这时,你就可以自信这把代码扔给面试官了。

案例2

求 N! 的二进制表示中最低位 1 的位置。
例如 3!=6,二进制为 1010,所以 最低位 1 的位置在第二位。
没有思路?请仔细想一下这道题与上道题的关系!

仔细想一下,这道题不也是求末尾有多少个 0 吗?你求出了末尾有多少个0自然知道 1 的位置(0的个数加1就是1的位置了),只不过,这道题是求二进制末尾有多少个 0

由于是二进制,所以每次乘以 2 末尾就会产生一个 0 。

于是,模仿上面一道题,求 N!含有多少个 2 的个数。心想,幸好我做个类似了,于是一波操作猛如虎,一分钟就写出了代码:

 int f(int n){
 	int sum = 0;
 	while(n! = 0){
 	sum += n / 2;
 	n = n / 2;
 	}
 }

面试官:还能在优化吗?

什么鬼?还要在优化?我都 O(logn)时间复杂度了。

还记得位运算吗?这道题确实可以用位运算来优化,除以n / 2 == n >> 1。不过位运算的速度快多了,于是,优化后的代码如下:

 int f(int n){
 	int sum = 0;
 	while(n! = 0){
 	n >>= 1;
 	sum += n;
 	}
 }

还能在优化吗?

当然还可以优化。
如果你能写出这样的代码,已经算很牛了。

案例三

给你一个数 N,输出 N! 的值。
没错,就是这么简单,直接让你求阶乘的值。

这个时候,你就要小心了,千万别一波操作

 long long sum = 1;
 for(int i = 1; i <= n; i++){
 sum *= i;
 }

一个 for 循环,马上搞定。

因为你不知道 n 的范围,有可能你用long long 也是会溢出的,所以这个时候应该要问一下面试官有没有限定 n 的范围。

面试官:没有限定!

这下好了,这道阶乘的题,难度顿时上升了,说实话,我敢保证大部分人还真没去实现过。所以,今天我就带大家来实现一下,以防以后真的被问到。结果最熟悉的题顿时不知道怎么下手好。

这个时候,我们就必须用字符串来存放所求的值的,在相乘的时候也是用字符串来相乘,说白了,就是要会求两个大整数相乘。

下面我们先来实现一个求两个大整数相乘的函数。一种比较简单的方法就是,像我们小学那会一样,让个位数与另一个数的其他数相乘,然后让十位数与另一个的其他数相乘,最后在把他们进行相加。
在这里插入图片描述
参考博文:
大数相乘
大数计算
大数计算-------------N的阶乘

总结

是不是觉得,阶乘也没有那么简单了?这三道与阶乘相关的题,应该是比较常见的题吧,今天跟大家分享一波,希望大家有时间的话,自己也用代码实现一下,特别是算 N!

猜你喜欢

转载自blog.csdn.net/qq_41035588/article/details/88746962
今日推荐