数学类问题
偶尔闲谈几句,数学类问题在各种面试中都会出现,不过一般出现形式都不会让你写代码。笔者某次面试银行的时候,就被问到一个数学问题,说有一个四位数的完全平方数aabb,问它是谁的平方。2分钟,无纸笔。笔者当场虽然发现了这个aabb的一些特征,但关键的步骤上,还是通过心算强行搞出来的。大家如果有兴致,可以想一想,这道题其实只需要小学生的计算能力就可以了。
在《剑指offer》中,也有一些数学类问题。这次就把三个问题放在一起,它们披着数学的背景,本质上还是考察一定的计算机知识。
斐波那契数列的第n项
offer09的要求是输出斐波那契数列的第n项。斐波那契数列满足:
{
F
(
1
)
=
1
F
(
2
)
=
1
F
(
n
)
=
F
(
n
−
1
)
+
F
(
n
−
2
)
n
≥
3
\left\{\begin{array}{c} F(1)=1 \\ F(2)=1 \\ F(n)=F(n-1)+F(n-2) \quad n \geq 3 \end{array}\right.
⎩⎨⎧F(1)=1F(2)=1F(n)=F(n−1)+F(n−2)n≥3
n = int(input())
递推
首先自然而然的,递推式都给出来了,一个for循环就能解决的问题。
# offer09-solution1
def Fibonacci(n):
if n <= 2:
print("1")
return
F = [1, 1]
for i in range(2, n, 1):
F.append(F[i-2]+F[i-1])
print(F[n-1])
return
Fibonacci(n)
时间复杂度是 O(n),呈线性增长。
除此之外呢?
递归
所谓递归,就是函数自身调用自己,一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,只需少量的程序就可描述出解题过程所需要的多次重复计算。一般来说,递归需要有边界条件、递归前进
段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
我们举一个简单的例子:计算
∑
i
=
1
n
i
!
\sum_{i=1}^{n} i !
∑i=1ni!
# n表示要求的阶乘
def factorial(n):
if n==1:
return n # 递归返回段:1!=1
n = n*factorial(n-1) # 递归前进段:n! = n*(n-1)!
return n # 返回结果并退出
print(factorial(4))
>> 33
既然有推导式,那就可以考虑递归了。
# offer09-solution2
def Fibonacci(n):
if n <= 2:
return 1
a = Fibonacci(n-1)+Fibonacci(n-2)
return a
print(Fibonacci02(n))
递归的问题在于:效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出)。在本题中,时间复杂度O( 1.61 8 n 1.618^n 1.618n),非常爆炸。
通项公式
通过繁复的推导(需要高中数学知识,又称特征根法,涉及到的背景是矩阵运算和特征值),我们可以知道,斐波那契数列的第n项通项公式为:
F
n
=
(
1
+
5
2
)
n
−
(
1
−
5
2
)
n
5
F_{n}=\frac{\left(\frac{1+\sqrt{5}}{2}\right)^{n}-\left(\frac{1-\sqrt{5}}{2}\right)^{n}}{\sqrt{5}}
Fn=5(21+5)n−(21−5)n
至于通项到底是怎么求的,参考文末链接。笔者在高中的时候非常擅长数列类问题,但显然在这里不是重点。
那么就……
# offer09-solution3
def Fibonacci(n):
print(int(((((1 + 5 ** 0.5) / 2) ** n)-(((1 - 5 ** 0.5) / 2) ** n)) / (5 ** 0.5)))
return
Fibonacci(n)
二进制数中1的个数
offer13的要求是:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
补码
首先第一个概念就是二进制的补码,这里还需要介绍一点背景:经典计算机体系结构框架中,计算机由运算器,控制器,存储器,输入和输出设备组成。其中运算器只有加法运算器,没有减法运算器。所以计算机中的减法是通过加法来实现的。这就为什么我们需要引入一个符号位,而码的概念也应运而生。原码,反码,补码的产生过程,就是为了解决,计算机做减法和引入符号位(正号和负号)的问题。
原码:是最简单的机器数表示法。用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。
反码:正数的反码还是等于原码;负数的反码就是他的原码除符号位外,按位取反。
补码:正数的补码等于他的原码;负数的补码等于反码+1。对于负数可以用另外一种定义:负数的补码等于他的原码自低位向高位,尾数的第一个‘1’及其右边的‘0’保持不变,左边的各位按位取反,符号位不变。
其实,严格意义上,补码的两种定义都只是“求法”,而不是严格上的定义。我们是可以不求反码直接求补码的,这点请直接参考链接。
题解
本题其实考察的主要是二进制的结构,常见的思路是:如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。
比如二进制数14=1110,14-1=13=1101,可以看到最后两位从01变成了10;而14&13=1110&1101=1100,可以看到,这样的一次并操作,“1”的个数少了1。
继续考虑1100(12),12-1=11=1011,12&11=1000,“1”的个数又少了1。
所以答案呼之欲出。我们只需要不断地进行并操作直到二进制数变成0,然后count操作次数就行。再加上一点对负数的处理就好。
# offer13-solution1
def NumberOf1(n):
count = 0
if n < 0:
n = n & 0xffffffff
while (n):
n = (n - 1) & n
count += 1
print(count)
return
NumberOf1(8)
NumberOf1(-10)
>> 1
>> 30
顺带一提的是,python库里还是有好用的函数,可以魔法般地解题的,这里一并附上。
# offer13-solution2
def NumberOf1(n):
print(bin(n & 0xffffffff).count("1"))
return
NumberOf1(8)
NumberOf1(-10)
>> 1
>> 30
数值的整数次方
offer14:输入一个double,输出它的整数次方,我觉得没什么好说的。连递归都不想写。
# offer14-solution1
class Solution:
def Power(self, base, exponent):
if base == 0.0:
return 0.0
if exponent >= 0:
return self.UnsignedExponent(base, exponent)
return 1.0 / self.UnsignedExponent(base, -exponent)
def UnsignedExponent(self, base, exponent):
result = 1.0
for i in range(exponent):
result *= base
return result
当然,直接pow就啥都完事了。
# offer14-solution2
class Solution:
def Power(self, base, exponent):
return pow(base,exponent)
参考
Python 平方根
Python递归的经典案例
斐波那契数列通项公式是怎样推导出来的?
原码,反码,补码的深入理解与原理
Python pow() 函数