面试算法

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),如下列表

十进制二进制原数值-1n&(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 平分算法吗?理论上和通过代码片段[重复]

前端面试题之手写promise

片段(Java) | 机试题+算法思路+考点+代码解析 2023

2021-12-24:划分字母区间。 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 力扣763。某大厂面试

代码面试最常用的10大算法