因为面试前需要快速过一遍剑指offer,共68道,题目太多非常耗费时间,在此只做一遍值得一做的题目。凭我个人的能力(约100+道leetcode水平)是可以完全忽略一些题目的。这里仅给出python代码及思路
1.两个栈实现一个队列。
此题不难,但是需要考虑减少无用操作,时刻记住第二个栈已经是倒序的了,所以只要没pop完就不要再往里append。
class CQueue:
def __init__(self):
self.stk1=[]
self.stk2=[]
def appendTail(self, value: int) -> None:
self.stk1.append(value)
def deleteHead(self) -> int:
if len(self.stk1)+len(self.stk2)==0:
return -1
#注意,stk2是原有stk1的倒序,所以只要stk2里面还有元素就可以直接往外pop
if self.stk2:
return self.stk2.pop()
else:
while self.stk1:
self.stk2.append(self.stk1.pop())
return self.stk2.pop()
2.斐波那契数列第N个数字。
这里有个溢出的问题(当然python是没有的)一旦有着中需要结果取模的,其实只要在运算中取模就好了。这里给出c++代码。
class Solution {
public:
int fib(int n) {
int a=0,b=1,c;
while(n-->0){
c=(a+b)%1000000007;
a=b;
b=c;
}
return a;
}
};
3.数组中的重复数字
这里最优秀的办法是将数组当做哈希表用,从题目可以看出会有多个数字对应一个索引,那么只要遇到一个不在索引位置上的数字,就将其swap到索引位置,只要发现已经被交换过了,说明已经被放置过一个对应索引数字,就返回就行了。
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
i=0
while i<len(nums):
if i==nums[i]:#已经在对应位置了,不需要管,假如说这个数字无重复,那无所谓;假如有重复,那么下一次swap就会判断出啦
i+=1
continue
idx=nums[i]
if idx==nums[idx]:#这个表示即将swap的位置已经被对应idx占领了,直接返回
return idx
nums[idx],nums[i]=nums[i],nums[idx]
return -1
4.旋转数组中位数
这题只要说有重复数字了,那可是相当不简单
class Solution:
def minArray(self, numbers: List[int]) -> int:
lo,hi=0,len(numbers)-1
while lo<=hi:
mid=lo+(hi-lo)//2
#如果中间的比最右边的小比如3 4 1 2 3 3那不用犹豫,最小值一定在左半边
if numbers[mid]<numbers[hi]:
hi=mid
#如果中间的比最右边的还大,那一定在右半边
elif numbers[mid]>numbers[hi]:
lo=mid+1
#如果中间的和最右边一样大,尝试向左缩减范围直到number[lo]变小再比较,就成了情况2
else:
hi-=1
return numbers[lo]
5.剪绳子
其实是一道不难的dp题(有人做成数学题了),我觉得用dp足够了。
假设dp[i]是长度为i的绳子能达到的最大乘积,那么如果在j的位置剪了一次,那么就是j*(j-i),如果是大于1次的,就是j*dp[j-i],也就是其中一段是j,乘以剩下能剪出的最大值。
class Solution:
def cuttingRope(self, n: int) -> int:
dp=[0]*(n+1)
for i in range(2,n+1):
for j in range(1,i):
dp[i]=max(j*(i-j),j*dp[i-j],dp[i])
return dp[-1]
6.二进制中1个数
这个题除了逐位计算,巧妙一点的是抵消法
考虑n=1001 ,n-1=1000,二者相&,的得到n=1000
迭代n-1=0111,n&=n-1等于0就可以终止了
事实上这个方法能够快速的抹去其中的一串0,但是不会放过任何一个最后一个1.
class Solution {
public:
int hammingWeight(uint32_t n) {
int cnt=0;
while(n){
n&=n-1;
cnt++;
}
return cnt;
}
};
7.x的n次幂
对于这题总想用递归算,其实不用,类似动态规划一样即可
我们知道2^5=2^(0b101)=2^1*2^4,这样
而我们知道
x=x^1
x*=x == x^2
x*=x == x^4
这不就有了你看
class Solution:
def myPow(self, x: float, n: int) -> float:
if x==0.0:
return x
if n<0:
x,n=1.0/x,-n
res=1.0
while n:
if n%2==1:
res*=x
x*=x
n>>=1
return res
8.正则匹配
此题为一道找规律的题,但似乎有更牛逼的数学算法,但是这里还是用找规律的算法,这样面试时做出来也不太尴尬。
考虑一个两位数,比如36,他的十位和个位分别能产生多少个1呢?答案是14,但是怎么来的?
14=10**1 + 4,只要十分位超过了1,那么在10,11,12....19这个序列中就有了10个(仅考虑十分位),而在十分位0,1,2,3循环后,个位出现了4次1。
我们再考虑一个更复杂的,365325.
按照思路第一位(第一个3)经历前一位0-1循环,第二位经历0-36循环,第三位经历0-365循环。
经历循环后又怎么计算呢?其实就很容易得出整数的计算法,j就像我们前面考虑10,11...19的时候,就知道其实每位能产生的1个数,是和当前位置有关系的。也就是
1*10**5+4*10**4+36*10**3+366*10**2+3654*10**1+36533
但是考虑两个特殊情况,如果这一位是0的话(当然不可能出现在第一位),比如32033,那么实际上0这一位没有构成1,所以这个时候应该以32*10**2方式计算这一位的1而不是33*10**2;
如果这一位恰好为1的时候,比如32133,那么1这一位并没有由于经历这一位从1变为2的过程,所以后面不应该丢掉取整数,相当于10,11,12,13我们应该只计算成4个而不是10个。这样的话相当于32*10**2+33,或者更粗暴一点的描述就是“去掉这一位构造出1的个数。
这样求和就很容计算了。
class Solution:
def countDigitOne(self, n: int) -> int:
if n<10:
return 1
nums=[int(c) for c in str(n)]
ns=str(n)
ret=0
for i in range(len(nums)):
if i==0:
if nums[i]==1:
ret+=int(ns[1:])+1
else:
ret+=10**(len(nums)-1)
else:
if nums[i]==0:
ret+=int(ns[:i])*10**(len(nums)-i-1)
elif nums[i]==1:
ret+=int(ns[:i]+ns[i+1:])+1
else:
ret+=(int(ns[:i])+1)*10**(len(nums)-i-1)
return ret
10.判断是否为后序遍历
此题很容易发现只要递归判断即可。因为序列都是左-右-根模式,扩展一下,那么序列一定是[左子树节点集合,右子树节点集合,根],我们每次把根取出来然后递归判断两部分即可。
由于BST的性质,左子树应该是小于根的部分,右子树为大于根的部分,根据这个来递归判断。但是这里面有个要注意的地方,我们可以根据一些条件提前终止判断。比如,如果发现右子树依然有小于根的数字,那么一定是不成立的。
class Solution:
def verifyPostorder(self, postorder: List[int]) -> bool:
if len(postorder)<=1:
return True
root=postorder.pop()
i=0
while i<len(postorder) and postorder[i]<root:
i+=1
lefts=postorder[:i]
rights=postorder[i:]
if rights and min(rights)<root:
return False
return self.verifyPostorder(lefts) and self.verifyPostorder(rights)
11. 只出现一次的数字
首先很明显,我们有个通用算法,如果数组只有一个出现一次,其他都出现K次,那么只要位统计一下,把次数%K的位找到,然后还原出即可。
但是这个题目有个看起来很优美的解法,我们知道位统计的过程中,能够出现0,1,2,3....一些数字,但结合上面提到的,其实也就3个结果,0,1,2,表示统计结果mod 3之后的结果。这样的话,一个bit自然是不够的,我们需要两个bit来保存3个状态。
假设对于某一位,遇到多次,两个记录位变化应当是
b1 b2
0 0
0 1
1 0
0 0
....
b1=b1^b& ~b2
b2=b2^b& ~b1
此题以前总用双指针加数组判重做,属实有点low。这回采用动态规划。
我们考虑以s[i]结尾的字符能构造的最长不重复子串长度为dp[i]。
- 如果这个字符第一次出现,那么dp[i]=dp[i-1]+1,即可以直接追加到上一个子串末尾;
- 如果这字符上次出现的位置为j,但j比i-1-dp[i-1]小,也就是说j在上一个不重复子串的范围之外,那么可以直接追加到末尾,因为不会影响整体的不重复性,也就是dp[i-1]+1
- 反之,如果在上一个不重复子串之内,那么就要i-j了。
而且,很容易得知,可以利用哈希表获取前一个同字符位置,那么就很容易获得了。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if not s:
return 0
dp=[1]*len(s)
last={}
last[s[0]]=0
ret=1
for i in range(1,len(s)):
j=last.get(s[i],-1)
last[s[i]]=i
if j==-1:
dp[i]=dp[i-1]+1
else:
dp[i]=min(dp[i-1]+1,i-j)
ret= max(ret,dp[i])
return ret
12.位运算求和
这题就没意思了,首先用一个变量保存所有进位然后迭代相异或。而且要注意的是保存进位时,要先转换为无符号数。
class Solution {
public:
int add(int a, int b) {
while(b){
int c=(unsigned int)(a&b)<<1;
a^=b;
b=c;
}
return a;
}
};
13.滑动窗口最大值
此题比较经典,主要考察队列的使用,首先构造一个递减序列,那么只要序列前面的值没有被窗口丢弃,就一直是最大值,依照这个思路,放置下标更容易一些。每次构造后都必须判断是否队头已经过期。边界的问题比较难搞。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if k==1:
return nums
from collections import deque
q=deque()
ret=[]
for i in range(len(nums)):
while q and nums[q[-1]]<=nums[i]:
q.pop()
q.append(i)
if i-q[0]>=k:
q.popleft()
if i>=k-1:
ret.append(nums[q[0]])
return ret
14.最大队列
此题与最小值有异曲同工之妙,而且还结合了”滑动窗口最大值“的思路。
首先,需要两个队列,一个正常保存,一个保存最大值,需要注意的是,每次添加元素时,如果与最大队列里的值相等,那么添加进去,代表这个最大值出现了多少次,一旦出队时候恰好是这个值,那么就也出队一个最大值。所以关键点是递减栈一定是不严格递减 。
举个例子,入队顺序7 8 7 8 8 6 4,那么最大队列应该是8 8 8 6 4,而不是8 6 4,否则出队一次就把全部的8弄出去了。
class MaxQueue:
def __init__(self):
from collections import deque
self.q,self.mq=deque(),deque()
def max_value(self) -> int:
if not self.mq:
return -1
return self.mq[0]
def push_back(self, value: int) -> None:
self.q.append(value)
while self.mq and self.mq[-1]<value:
self.mq.pop()
self.mq.append(value)
def pop_front(self) -> int:
if not self.q:
return -1
t=self.q.popleft()
if t==self.mq[0]:
self.mq.popleft()
return t
15.概率
这个题目首先需要确定的是返回数组到底有多大。
迭代1次[1,2,3,4,5,6]
迭代2次[2,3....12]
也就是从k...6k一共5*k+1个数字。
假如某次迭代是从[k,k+1...6k]这5*k+1数字得到的概率为[Pk,Pk+1,Pk+2,.....P6k]
那么计算下一次迭代[k+1,....6k+6]的概率
每次循环6次,上次概率/6,循环加进新概率里,也就是
dp[n][j]=i=1∑6dp[n−1][j−i]
所有第k+1次的数值,能由第k次的数字经过与1-6相加得到的,都会乘以1/6。
class Solution:
def dicesProbability(self, n: int) -> List[float]:
arr=[1/6]*6
if n==1:
return arr
for i in range(2,n+1):
tmp=[0]*(5*i+1)
for j in range(len(arr)):
for k in range(6):
tmp[j+k]+=arr[j]/6
arr=tmp
return arr
16.圆圈剩下的最后数字
此题仍然是一道数学题,我们考虑示例1,以追加一个复制数组模拟环:
迭代0:0,1,2,3,4,5,0,1,2,3,4,5...
迭代1:3,4,5,0,3,4,5,0...
迭代2:0,3,4,0,3,4...
迭代3:0,3,0,3...
迭代4:3
可以看出迭代4中我们只要把3位置加上m mod 当前上一个迭代长度就得到上一个迭代的位置。
(0+3)%2=1
(1+3)%3=1
(1+3)%4=0
(0+3)%5=3
根据这个就很容易得到在原数组中的位置。
class Solution:
def lastRemaining(self, n: int, m: int) -> int:
b=2
a=0
while b<=n:
a=(a+m)%b
b+=1
return a