面试算法
Posted 乐学编程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试算法相关的知识,希望对你有一定的参考价值。
面试算法第二章
本章课程承接作者主页中《面试中的算法》的第一章内容。重点讲解面试的算法,助你拿到自己期望的offer。
本教程中的练习题,请移步 http://www.eluzhu.com:1818/my/course/60 进行练习。
您也可以在该网站免费学习到更多课程
求最大公约数
hi,今天我们讲解面试经常会考察的求最大公约数的问题。
哈?! 这个问题太简单了,使用我们之前学过的暴力枚举方法。看看这个整数能否被 a 和 b 整除。
你这个方法虽然实现了所要求的功能,但是效率不行啊,想想看,如果我传入的整数是10000和10001用你的方法就需要循环10000/2-1=4999次
给你讲一个方法叫 辗转相除法,又名欧几里得算法, 该算法的目的是求出两个正整数的最大公约数。它是已知最古老的算法,其产生时间可追溯至公元前300年前。
这条算法基于一个定理:两个正整数 a 和 b (a>b),它们的最大公约数等于 a 除以 b 的余数 c 和 b 之间的最大公约数。
例如 10 和 25,25 除以 10 商 2 余 5,那么 10 和 25 的最大公约数,等同于 10 和 5 的最大公约数。
有了这条定理,求最大公约数就变得简单了。我们可以使用递归的方法把问题逐步简化。
首先,计算出 a 除以 b 的余数 c,把问题转化成求b和c的最大公约数;然后计算出 b 除以 c 的余数 d,把问题转化成求 c 和 d 的最大公约数;再计算出 c 除以 d 的余数 e,把问题转化成求 d 和 e 的最大公约数……
以此类推,逐渐把两个较大整数之间的运算简化成两个较小整数之间的运算,直到两个数可以整除,或者其中一个数减小到1为止。
辗转相除法代码实现:
#include <iostream> using namespace std; int GetGreat(int a, int b){ int big = a > b ? a : b; int small = a < b ? a : b; if(big % small == 0){ return small; } return GetGreat(big % small , small); } int main(){ cout << GetGreat(25, 5) << endl; cout << GetGreat(100, 80) << endl; return 0; }
更相减损术
a % b 取模运算的性能会比较差。
所以我们还要讲另一个算法,更相减损术。出自中国古代的《九章算术》,也是一种求最大公约数的算法。古腊人很聪明,可是我们炎黄子孙也不差。它的原理更加简单:两个正整数 a 和 b (a>b),它们的最大公约数等于 a-b 的差值和较小数 b 的最大公约数。例如10和25,25减10的差是15,那么10和25的最大公约等同于10和15的最大公约数。
由此,我们同样可以通过递归来简化问题。首先,计算出a和b的差值 c( 假设a>b),把问题转化成求 b 和 c 的最大公约数;然后计算出 c 和 b 的差值 d (假设c>b),把问题转化成求 b 和d 的最大公约数;再计算出 b 和 d 的差值 e(假设b>d),把问题转化成求 d 和 e 的最大公约数……
以此类推,逐渐把两个较大整数之间的运算简化成两个较小整数之间的运算,直到两个数可以相等为止,最大公约数就是最终相等的这两个数的值。
下面你来把代码补全吧,请移步到该网站面试中的算法 《求最大公约数》小节中,习题在内容中部。
http://www.eluzhu.com:1818/my/course/60
更相减损术的过程就是这样。我们避免了大整数取模可能出现的性能问题。
但是!这种方式依靠两个数求差的方式递归,运算次数远大于辗转相除法~
移位运算
更相减损术是不稳定的算法,当两数相差悬殊时,如计算10000和1的最大公约数,就要递归9999次!
下面就是我要说的最优方法:把辗转相除法和更相减损术的优势结合起来,在更相减损术的基础上使用移位运算。
众所周知,移位运算的性能非常好。对于给出的正整数 a 和 b,不难得到如下的结论。
当 a 和 b 均为偶数时, gcd(a, b) = 2×gcd(a/2, b/2) = 2×gcd(a>>1, b>>1) 。
当 a 为偶数, b 为奇数时, gcd(a, b) = gcd(a/2, b) = gcd(a>>1, b) 。
当 a 为奇数, b 为偶数时, gcd(a, b) = gcd(a, b/2) = gcd(a, b>>1) 。
当 a 和 b 均为奇教时, 先利用更相减损术运算一次, gcd(a, b) = gcd(b, a-b) , 此时a-b必然是偶数,然后又可以继续进行移位运算。
例如计算10和25的最大公约数的步骤如下。
1.整数10通过移位,可以转换成求5和25的最大公约数。
2.利用更相减损术,计算出25-5=20,转换成求5和20的最大公约数。
3.整数20通过移位,可以转换成求5和10的最大公约数。
4.整数10通过移位,可以转换成求5和5的最大公约数。
5.利用更相减损术,因为两数相等,所以最大公约数是5。
这种方式在两数都比较小时,数的减少就会越明显。
最终版本代码:
#include <iostream> using namespace std; int gcd(int a, int b){ if(a == b){ return a; } if ((a & 1) == 0 && (b & 1) == 0){ return gcd(a >> 1 , b >> 1) << 1; }else if ((a & 1) == 0 && (b & 1) != 0){ return gcd(a >> 1, b); }else if ((a & 1) == 0 && (b & 1) == 0){ return gcd(a, b >> 1); }else{ int big = a > b ? a : b; int small = a < b ? a : b; return gcd(big - small , small); } } int main(){ cout << gcd(25, 5) << endl; cout << gcd(100, 80) << endl; return 0; }
运行结果为:
5
20
在上述代码中,判断整数奇偶性的方式是让整数和 1 进行与运算,如果 ( a & 1 ) == 0 则说明整数a是偶数;如果( a & 1 ) != 0,则说明整数a是奇数。
作为程序员,就是需要反复推敲,追求代码的极致!
让我们来总结一下上述解法的时间复杂度。
1.暴力枚举法:时间复杂度是O(min(a,b) ) 。
2.辗转相除法:时间复杂度不太好计算, 可以近似为O(log(max(a,b) ) ) , 但是取模运算性能较差。
3.更相减损术:避免了取模运算,但是算法性能不稳定,最坏时间复杂度为O(max(a, b) ) 。
4.更相减损术与移位相结合:不但避免了取模运算,而且算法性能稳定,时间复杂度为O(log(max(a, b) ) ) 。
2的整数次幂
hi,少年。下面我们讲一下面试中经常被问到的问题,给你一个正整数,如何判断它是不是2的整数次幂?
题目:
实现一个方法,来判断一个正整数是否是2的整数次幂 ( 如 16 是 2 的 4 次方,返回 true ,18 不是 2 的整数次幂, 则返回 false)
你想想,有什么思路么?
陷入沉思。。。可以利用一个整型变量,让它从1开始不断乘以2,将每一次乘2的结果和目标整数进行比较。
那我们这样,创建一个中间变量temp, 初始值是1。然后进入一个循环, 每次循环都让temp和目标整数相比较, 如果相等, 则说明目标整数是2的整数次幂; 如果不相等, 则让temp增大1倍, 继纯循环并进行比较。当temp的值大于目标整数时, 说明目标整数不是2的整数次幂。
举个例子。
给出一个整数19,则
1X2=2,
2X2=4
4X2=8,
8X2=16,
16X2=32,
由于32>19,所以19不是2的整数次幂。
如果目标整数的大小是n, 则此方法的时间复杂度是O(logn) 。
代码实现如下:
#include <iostream> using namespace std; int isPower(int num){ int temp = 1; while(temp <= num){ if(temp == num){ return true; } temp = temp*2; } return false; } int main() { cout << isPower(32) << endl; cout << isPower(19) << endl; return 0; }
这样写实现了所要求的功能,你思考一下该怎么提高其性能呢?
陷入沉思。。。可以把乘以2的操作改成向左位移,位移的性能比乘法高得多。
嗯嗯,确实有一定的优化,位移操作你还记得怎么写么?你自己来练习一下吧。请移步到该网站面试中的算法 《2的整数次幂》小节中,习题在内容中部。
http://www.eluzhu.com:1818/my/course/60
位移的性能虽然好一些,但是目前算法的时间复杂度仍然是O(logn),本质上没有变化。
难道还有时间复杂度只有O(1)的方法?
当然有的,你先想一想,如果把 2 的整数次幂转换成二进制数,会有什么样的共同点? 十进制的 2 转换成二进制是10B,4 转换成二进制是100B、8 转化成二进制是1000B……
十进制 | 二进制 | 是否为2的整数次幂 |
---|---|---|
8 | 1000B | 是 |
16 | 10000B | 是 |
32 | 100000B | 是 |
64 | 1000000B | 是 |
100 | 1100100B | 否 |
如果一个整数是2的整数次幂,那么当他转化为二进制时,只有最高位是1,其他位是0!
接下来如果把这些 2 的整数次幂各自减 1,再转化成二进制 .
十进制 | 二进制 | 原数值-1 | 是否为2的整数次幂 |
---|---|---|---|
8 | 1000B | 111B | 是 |
16 | 10000B | 1111B | 是 |
32 | 100000B | 11111B | 是 |
64 | 1000000B | 111111B | 是 |
100 | 1100100B | 1100011B | 否 |
位运算妙用
2 的整数次幂一旦减 1,它的二进制数字就全部变成了 1,这时候如果用原数值( 2 的整数次幂)和它减1的结果进行按位与运算,也就是 n&(n-1),如下列表
十进制 | 二进制 | 原数值-1 | n&(n-1) | 是否为2的整数次幂 |
---|---|---|---|---|
8 | 1000B | 111B | 0 | 是 |
16 | 10000B | 1111B | 0 | 是 |
32 | 100000B | 11111B | 0 | 是 |
64 | 1000000B | 111111B | 0 | 是 |
100 | 1100100B | 1100011B | 11000B | 否 |
0 和 1 按位与运算的结果是 0,所以凡是 2 的整数次幂和它本身减 1 的结果进行与运算,结果都必定是 0。反之,如果一个整数不是 2 的整数次幂,结果一定不是 0。
对于一个整数 n,只需要计算 n&(n-1) 的结果是不是0。这个方法的时间复杂度只有O(1)。
#include <iostream> using namespace std; int isPower(int num) { return ( num & num - 1 ) == 0 ; } int main() { cout << isPower(32) << endl; cout << isPower(19) << endl; return 0; }
好了,这就是位运算得妙用,您可以进入下面的地址免费学习完整的算法课程。
http://www.eluzhu.com:1818/my/course/60
以上是关于面试算法的主要内容,如果未能解决你的问题,请参考以下文章
有人可以解释啥是 SVN 平分算法吗?理论上和通过代码片段[重复]
片段(Java) | 机试题+算法思路+考点+代码解析 2023
2021-12-24:划分字母区间。 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 力扣763。某大厂面试