剑指offer|解析和答案(C++/Python) (五)下
参考剑指offer(第二版),这里做一个学习汇总,包括解析及代码。代码均在牛客网进行验证(摘自自己的牛客网笔记)。
整个剑指offer解析链接:
剑指offer|解析和答案(C++/Python) (一).
剑指offer|解析和答案(C++/Python) (二).
剑指offer|解析和答案(C++/Python) (三).
剑指offer|解析和答案(C++/Python) (四).
剑指offer|解析和答案(C++/Python) (五)上.
剑指offer|解析和答案(C++/Python) (五)下.
剑指offer|解析和答案(C++/Python) (六).
习题
面试中各项能力
15.扑克牌中的顺子
16.圆圈中最后剩下的数字
17.股票的最大利润
18.求1 + 2 + 3 +…+n
19.不用加减乘除做加法
20.构建乘积数组
15.扑克牌中的顺子
LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
思路:
1.把数组进行排序。
2.统计数组中0的个数。
3.统计数组中排序之后的数组中相邻数字之间的空缺总数。
4.进行比较。如果空缺总数大于0的个数,则不能凑成顺子。反之,空缺总数小于等于0的个数,则能凑成顺子。
代码:
C++
class Solution {
public:
bool IsContinuous( vector<int> numbers ) {
int length = numbers.size();
if(length == 0)
return false;
//对vector 进行排序
sort(vector.begin(), vector.end());
int numbersOf0 = 0;//0的个数
int numbersOfGrap = 0;//数字空缺数
//统计0的个数
for(int i = 0; i < length && numbers[i] == 0; ++i){
++numbersOf0;
}
//统计数字中间隔数
int small = numbersOf0;
int big = small + 1;
while(big < length){
//如果存在两个数字相等,则比如存在对子,不可能是顺子
if(numbers[small] == numbers[big])
return false;
numbersOfGrap += numbers[big] - numbers[small] - 1;
small = big;
++big;
}
if(numbersOfGrap > numbersOf0)
return false;
else
return true;
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
def IsContinuous(self, numbers):
# write code here
length = len(numbers)
if length == 0:
return False
#进行排序
numbers.sort()
numbersOf0 = 0
numbersOfGrap = 0
#统计数组中0的个数
for i in range(length):
if numbers[i] == 0:
numbersOf0 += 1
#统计数组中的间隔数目
small = numbersOf0
big = small + 1
while big < length:
#两个数字相等 则不可能是顺子
if numbers[big] == numbers[small]:
return False
numbersOfGrap = numbers[big] - numbers[small] - 1
small = big
big += 1
if numbersOfGrap > numbersOf0:
return False
else:
return True
16.圆圈中最后剩下的数字
0,1,…,n-1,这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里最后剩下的数字。
思路:
解法一:经典的解法,用环形链表模拟。
既然题目中有一个数字圆圈,就使用一个数据结果来模拟这个圆圈。可以创建一共有n个节点的环形链表,然后每次在这个链表中删除第m个节点。
代码:
C++
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if(n < 1 || m < 1)
return -1;
unsigned int i = 0;
list<int> numbers;//新建一个队列
for(i = 0; i < n; ++i){
numbers.push_back(i);//0 ~ n - 1
}
list<int>::iterator current = numbers.begin();
while(numbers.size() > 1){
for(int i = 1; i < m; ++i){//从0开始计算m个数字
++current;
//可以调用list容器的end()函数来得到list末端下一位置
if(current == numbers.end())
current = numbers.begin();
}
list<int>::iterator next = ++current;
if(next == numbers.end())
next = numbers.begin();
--current;
numbers.erase(current);//删除
current = next;
}
return *(current);
}
};
Python
class Solution:
def LastRemaining_Solution(self, n, m):
# write code here
if n < 1 or m < 1:
return -1
numbers = []
for i in range(n):
numbers.append(i)
current = 0
while len(numbers) > 1:
#移步
for i in range(1, m):
if current == len(numbers) - 1:
current = 0
else:
current += 1
if current == len(numbers) - 1:
next = 0
else:
next = current + 1
nextNum = numbers[next]
numbers.pop(current)
current = numbers.index(nextNum)
return numbers[0]
解法二:创新的解法,数学推导。
1.定义一个关于n和m的方程
f
(
n
,
m
)
f(n, m)
f(n,m),表示每次在n个数字0,1,…,n - 1,中删除第m个数字最后剩下的数字。
2.在这n个数字中,第一个每删除的数字是
(
m
−
1
)
%
n
(m - 1)\%n
(m−1)%n,为方便起见把
(
m
−
1
)
%
n
(m - 1)\%n
(m−1)%n定义为k。
3.删除k之后剩下的n - 1个数字为0,1,…,k-1,k+1,…,n-1。 在剩下的序列中,k+1排在最前面,从而形成k+1,…,n-1,0,1,…,k-1。 该序列最后剩下的数字也应该是关于n和m的函数,记为
f
′
(
n
−
1
,
m
)
f'(n-1, m)
f′(n−1,m)。
4.最初序列最后剩下的数字
f
(
n
,
m
)
f(n, m)
f(n,m)一定是删除一个数字之后的序列最后剩下的数字,即
f
(
n
,
m
)
=
f
′
(
n
−
1
,
m
)
f(n, m) = f'(n-1, m)
f(n,m)=f′(n−1,m)。
5.把剩下的n - 1个数字的序列k+1,…,n-1,0,1,…,k-1进行映射,映射结果是形成一个0 - n-2的序列。
6.把映射定义成
p
p
p,则
p
(
x
)
=
(
x
−
k
−
1
)
%
n
p(x)=(x-k-1) \% n
p(x)=(x−k−1)%n。如果映射前的数字是
x
x
x,那么映射后的数字是
(
x
−
k
−
1
)
%
n
(x-k-1) \% n
(x−k−1)%n。该映射的逆映射是
p
−
1
(
x
)
=
(
x
+
k
+
1
)
%
n
p^{-1}(x)=(x+k+1) \% n
p−1(x)=(x+k+1)%n。
7.由于映射之后的序列和最初的序列具有相同的形式,都是从0开始的连续序列,仍然可以使用
f
f
f来表示,记为
f
(
n
−
1
,
m
)
f(n-1,m)
f(n−1,m)。根据映射规则,映射之前的序列中最后剩下的数字
f
′
(
n
−
1
,
m
)
=
p
−
1
[
f
(
n
−
1
,
m
)
]
=
[
f
(
n
−
1
,
m
)
+
k
+
1
]
%
n
f^{\prime}(n-1, m)=p^{-1}[f(n-1, m)]=[f(n-1, m)+k+1] \% n
f′(n−1,m)=p−1[f(n−1,m)]=[f(n−1,m)+k+1]%n,把
k
=
(
m
−
1
)
%
n
k=(m - 1)\%n
k=(m−1)%n代入得到
f
(
n
,
m
)
=
f
′
(
n
−
1
,
m
)
=
[
f
(
n
−
1
,
m
)
+
m
]
%
n
f(n, m)=f^{\prime}(n-1, m)=[f(n-1, m)+m] \% n
f(n,m)=f′(n−1,m)=[f(n−1,m)+m]%n。
根据上面的推导,可以得到递归公式:
f
(
n
,
m
)
=
{
0
,
n
=
1
[
f
(
n
−
1
,
m
)
+
m
]
%
n
,
n
>
m
f(n, m)=\left\{\begin{array}{l}{0, n=1} \\ {[f(n-1, m)+m] \% n, n>m}\end{array}\right.
f(n,m)={0,n=1[f(n−1,m)+m]%n,n>m
代码:
C++
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if(n < 1 || m < 1)
return -1;
int last = 0;
for(int i = 2; i <= n; ++i){
last = (last + m)%i;
}
return last;
}
};
Python
class Solution:
def LastRemaining_Solution(self, n, m):
# write code here
if n < 1 or m < 1:
return -1
last = 0
if n > 1:
for i in range(2, n + 1):
last = (last + m)%i
return last
17.股票的最大利润
假设把某股票的价格按时间先后顺序存储在数组中,请问买卖股票一次可能获得的最大利润是多少?例如,一只股票在某些时间节点的价格为{9,11,8,5,7,12,16,14}。如果我们能在价格为5的时候买入并在价格为16的时候卖出,则能收获最大的利润是11
思路:
股票交易的利润来自股票买入和卖出价格的差价。当然必须在买入股票之后才能卖出。如果买入和卖出价两个数字组成一个数对,那么利润就是这个数对的差值。最大利润就是数组中所有利润的最大差值。
传统方法:遍历所有数对来进行求解。由于长度为n的数组中存在O(n2)个数对,因此这种算法时间复杂度为O(n2)。
差值法:定义函数
d
i
f
f
(
i
)
diff(i)
diff(i)为当卖出价为数组中第i个数字时可能获得的最大利润。在卖出价固定时,买入价越低则获得的利润越大。如果在扫描数组中的第i个数字时,只需要得到之前i - 1个数字中的最小值,就能算出当前价位卖出时可能得到的最大利润。
代码:
C++
class Solution {
public:
int MaxDiff(const int* numbers, unsigned length){
if(numbers == nullptr && length < 2)
return 0;
int min = numbers[0];
int maxDiff = numbers[1] - min;
for(int i = 2; i < length; ++i){
if(numbers[i - 1] < min)//需要先买入
min = numbers[i - 1];
//当前利润
int currentDiff = numbers[i] - min;
if(currentDiff > maxDiff)
maxDiff = currentDiff;
}
return maxDiff;
}
};
18.求1 + 2 + 3 +…+n
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
思路:
解法一:利用构造函数求解
解法二:利用虚函数求解
解法三:利用函数指针求解
解法四:利用模板类型求解
解法五:利用逻辑与短路特性求解
逻辑运算的短路特性:
(表达式1)&&(表达式2) 如果表达式1为假,则表达式2不会进行运算,即表达式2“被短路”
(表达式1)||(表达式2) 如果表达式1为真,则表达式2不会进行运算,即表达式2“被短路”
使用这种方法就不需要使用if来对n的大小进行判断。
代码:
C++
class Solution {
public:
int Sum_Solution(int n) {
int sum = 0;
//n = 0 时 返回 sum = 0
//n > 0 时 返回 sum=n+Sum_Solution(n-1) 进行递归
(n>0) && (sum=n+Sum_Solution(n-1));
return sum;
}
};
Python
注意:a = (True) and num , a = num
# -*- coding:utf-8 -*-
class Solution:
def Sum_Solution(self, n):
# write code here
num = n
temp = n > 0 and self.Sum_Solution(n - 1)
return num + temp
19.不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
思路:
由于不能使用四则运算,则使用二进制中的位操作。
举例:5 + 17 =22。5的二进制是101,17的二进制是10001。第一步:各位相加但不记进位,得到的结果是10001。第二步:记下进位,这个例子中结果是10。第三步:把前两部结果相加,得到结果是10110。
得到规律:
1.不考虑进位对每一位相加。这和异或的结果是一样的。
2.考虑进位。只有1 + 1才会产生进位,所以是与操作。
3.把前两个步骤的结果相加。同时重复前面两步,直到不产生进位为止(因为进位可能会产生新的进位)。
代码:
C++
class Solution {
public:
int Add(int num1, int num2)
{
int sum;//和
int carry;//进位
do{
sum = num1 ^ num2;//异或 +
carry = (num1 & num2)<<1;//与计算进位
num1 = sum;
num2 = carry;
}while(num2 != 0)
return sum;
}
};
Python
Python的位操作不同。在Python内部对整数的处理分为普通整数和长整数,普通整数长度为机器位长,通常都是32位,超过这个范围的整数就自动当长整数处理,而长整数的范围几乎完全没限制所以long类型运算内部使用大数字算法实现,可以做到无长度限制。
- 移位不会溢出。 所以使用0xFFFFFFFF把数字限定在32位。但这样会造成符号位丢失。详情可见:
https://blog.csdn.net/s1314p/article/details/95769392 - 找回符号位。即 ~(num1^0xFFFFFFFF)
class Solution:
def Add(self, num1, num2):
# write code here
sum = 0
carry = 0
while 1:
sum = (num1 ^ num2) & 0xFFFFFFFF#限定成一个32位的数字
carry = ((num1 & num2) << 1) & 0xFFFFFFFF
num1 = sum
num2 = carry
if num2 == 0:
break
#python 会把int 转long 就不会溢出
#num1第一位是符号位 0是正数 1是负数 但是 &0xFFFFFFFF 会丢失符号位
#所以使用~(num1^0xFFFFFFFF) 找回符号位
return num1 if num1<=0x7FFFFFFF else ~(num1^0xFFFFFFFF)
20.构建乘积数组
给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]。不能使用除法。
思路:
1.传统方法。由于不能使用除法,一种直观的解法是使用连乘n-1个数字得到B[i]。时间复杂度位O(n2)。
2.高效的方法。可以把
B
[
i
]
=
A
[
0
]
×
A
[
1
]
×
…
×
A
[
i
−
1
]
×
A
[
i
+
1
]
×
…
×
A
[
n
−
1
]
B[i]=A[0] \times A[1] \times \ldots \times A[i-1] \times A[i+1] \times \ldots \times A[n-1]
B[i]=A[0]×A[1]×…×A[i−1]×A[i+1]×…×A[n−1]看成
C
[
i
]
=
A
[
0
]
×
A
[
1
]
×
…
×
A
[
i
−
1
]
C[i]=A[0] \times A[1] \times \ldots \times A[i-1]
C[i]=A[0]×A[1]×…×A[i−1]和
D
[
i
]
=
A
[
i
+
1
]
×
…
×
A
[
n
−
2
]
×
A
[
n
−
1
]
D[i]=A[i+1] \times \ldots \times A[n-2] \times A[n-1]
D[i]=A[i+1]×…×A[n−2]×A[n−1]两者的乘积。并且
C
[
i
]
C[i]
C[i]可以用自上而下的顺序计算出来:
C
[
i
]
=
C
[
i
−
1
]
×
A
[
i
−
1
]
C[i]=C[i-1] \times A[i-1]
C[i]=C[i−1]×A[i−1]。同理,
D
[
i
]
D[i]
D[i]可以通过自下而上的顺序计算出来:
D
[
i
]
=
D
[
i
+
1
]
×
A
[
i
+
1
]
D[i]=D[i+1] \times A[i+1]
D[i]=D[i+1]×A[i+1]。参考图示:
代码:
C++
class Solution {
public:
vector<int> multiply(const vector<int>& A) {
int length = A.size();
vector<int> B(length);
if(length > 1){
B[0] = 1;
for(int i = 1; i < length; ++i){
B[i] = B[i - 1] * A[i - 1];//C[i] = C[i-1] * A[i-1]
}
double temp = 1;
for(int i = length - 2; i >= 0; --i){
temp *= A[i + 1];//D[i] = D[i+1] * A[i+1]
B[i] *= temp; //B[i] = C[i] * D[i]
}
}
return B;
}
};
Python
class Solution:
def multiply(self, A):
# write code here
length = len(A)
B = []
if length > 1:
for i in range(length):
B.append(1)#初始化
for i in range(1,length):
B[i] = B[i-1] * A[i-1]
temp = 1
for i in rang(length - 2, -1, -1):
temp *= A[i+1]
B[i] *= temp
return B