说明:题目均来自:Leetcode;总结了很多大神们的解题思路,因为链接太多,我就没逐一放引用链接,如有侵权请告知,将删除。
(1)4.20- 03. 数组中重复的数字
考点:沟通能力,问面试官要时间/空间需求
代码:
(A) 时间复杂度O(N)空间复杂度 O(N)
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
#方法一 ----->enumerate()
dic = {}
for key,values in enumerate(nums):
dic[values] = dic.get(values,0)+1
if dic[values] >=2:
return values
#方法二 ----->collections.Counter()
dic = collections.Counter(nums)
for x in dic:
if dic[x] >1:
return x
(B) 时间复杂度O(NlogN) 空间复杂度O(1)
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
nums.sort() ##排序
for i in range(0,len(nums)-1):
if nums[i] == nums[i+1]:
return nums[i]
return -1
空间复杂度为什么是O(NlogN) ?sort()函数的时间复杂度是:O(NlogN),详见链接。
(C)原地操作(原地哈希)->鸽舍原理
鸽舍原理:有N+1个鸽子,有N个笼子,至少有两只鸽子会在一个笼子里面(证明:反证法)
给定的元素值均小于len(nums),将见到的元素 放到索引的位置,如果交换时,发现索引处已存在该元素,则重复 。
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
for index in range(0,len(nums)):
while(nums[index] != index):
if nums[nums[index]] == nums[index] :return nums[index]
#nums[index]已经有元素了
temp = nums[index]
nums[index] = nums[temp]
nums[temp] = temp
return -1
(2)4-21-顺时针打印矩阵
考点:边界值的处理问题
(3)4-22-青蛙跳台阶问题
考点:(A)总结规律,找关系。 多少种可能性的题目一般都有递推性质。
(B)大数的问题。详解->点链接。
青蛙跳N台阶前,有两种可能N-1和N-2,再跳1下就能够跳完。
所以有 f(N)=f(N-1)+f(N-2)。这个递推公式。f(N-1)对应x种方案,f(N-2)对应y种方案。
找一下临界点:f(0) = 1 f(1) = 1 f(2) = 2 = f(1)+f(0),故n>=2 时,即可用递推来解决。
方法1:递归形式->超时 从大到小的做法。
class Solution:
def numWays(self, n: int) -> int:
def t(n):
if n == 1:return 1
if n == 0:return 1
return (t(n-1)+t(n-2))%1000000007
return t(n)
方法2:将每一次的f(n)存起来,从小到大的做法。 时间复杂度O(N) 空间复杂度O(N)
class Solution:
def numWays(self, n: int) -> int:
if n == 1:return 1
if n == 0:return 1
t = [0 for _ in range(0,n+1)]
t[0],t[1] = 1,1
for i in range(2,n+1):
t[i] = (t[i-1]+t[i-2])%1000000007
return t[-1]
方法3:DP做法,因为每次f(n)只与前两项相关,所以可以只存储前两项就好。空间复杂度O(1)
class Solution:
def numWays(self, n: int) -> int:
if n ==1 : return 1
if n ==0 : return 1
a,b= 1,1
sum_ = 0
for i in range(0,n-1):
sum_ = a+b
a,b = b,sum_
return sum_ % 1000000007
盲点:
【1】 Python 中整形数字的大小限制取决计算机的内存 (可理解为无限大),因此可不考虑大数越界问题。
【2】为什么要模1000000007?
大数相乘,大数的排列组合等为什么要取模?
1,1000000007是一个质数(素数),对质数取余能最大程度避免结果冲突/重复
2,int32位的最大值为2147483647,所以对于int32位来说1000000007足够大。
3,int64位的最大值为2^63-1,用最大值模1000000007的结果求平方,不会在int64中溢出。
所以在大数相乘问题中,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出。
这道题为什么要取模,取模前后的值不就变了吗?
1,确实:取模前 f(43) = 701408733, f(44) = 1134903170, f(45) = 1836311903, 但是 f(46) > 2147483647结果就溢出了。
2,_____,取模后 f(43) = 701408733, f(44) = 134903163 , f(45) = 836311896, f(46) = 971215059没有溢出。
3,取模之后能够计算更多的情况,如 f(46)
4,这道题的测试答案与取模后的结果一致。
5,总结一下,这道题要模1000000007的根本原因是标准答案模了1000000007。不过大数情况下为了防止溢出,模1000000007是通用做法,原因见第一点。
(4)4-23-链表中倒数第k个节点
考点:双指针的使用,以及链表的基本操作。放在了 python-Leetcode-链表相关中。
(5)4-24-面试题21. 调整数组顺序使奇数位于偶数前面
考点:双指针,借助快排的思想。 时间复杂度O(N),空间复杂度 O(1)
class Solution:
def exchange(self, nums: List[int]) -> List[int]:
''' # 快排
n = len(nums)
i = 0
j = n-1
while(i < j):
while(i < j and nums[i] %2 == 1): i+=1
while(i < j and nums[j] %2 ==0 ): j-=1
temp = nums[i]
nums[i] = nums[j]
nums[j] = temp ##changed
i += 1
j -= 1
return nums'''
#双指针
i ,j = 0,len(nums)-1
while i<=j:
if nums[i] % 2 ==1:
i += 1
elif nums[j] %2 ==0:
j -= 1
else:
temp = nums[i]
nums[i] = nums[j]
nums[j] = temp ##changed
i += 1
j -= 1
return nums
其他做法:
(1)把基数和偶数挑出来,在组合。空间O(N) 时间O(N)
a = []
b = []
for i in range(0,len(nums)):
if nums[i] %2 == 1:a.append(nums[i])
else: b.append(nums[i])
return a+b
扩展:
如果我们将题目改一下,要求将是3的倍数的数字放到前面,不是3的倍数的数字放到后面;或者是要求将负数放在前面将正数放在后面。思考下这中类似的问题,应该怎么处理呢?
应该不能看出只需要改变下判断条件即可,我们是不是能把判断条件(nums[i] %2 ==1)更换就好,这样做的目的增加了我们函数的可扩展性,在这种模式下很方便地把已有的解决方案扩展到同类的问题上。
(6)4-25-面试题20. 表示数值的字符串
考点:这个题测试用例太多了,也有很多种反人类的情况。
(一) 利用try_except 来做
class Solution:
def isNumber(self, s: str) -> bool:
try:
a = float(s)
return True
except:
return False
(二)正则表达式
(三)确定有限自动机DFA 编译原理里面的,DFA走的是合法的路径。 存在开始状态和结束状态,以及-1(非法状态)。根据题目要求,画出状态图和矩阵,然后在做就简单了。主要是画DFA。
(四)基础做法
这个题,包括三个部分判断,1,不含小数点和指数 2,含有小数点 3,含有指数形式并且含有小数点
(1)其中 包括空格这种类型,第一步先处理,空格。空格在一个数字串左右都可以,但不可以将两部分分开。提取前面不是空格的字符串。
(2)判断是否含有非法字符, 'A-Z','a-d','f-z'。
(3)判断是否是常数(没有小数点和指数):判断+是否单独出现,判断+是否出现在第二位以后。
(4)判断是否只含有小数点。
看小数点分隔后的每个部分是否满足条件: '+.8' , 是满足条件的,'.+'是不满足条件的。
规律就是:+ 在前面第一位可以【小数点后非空】,但是后面不行。 + 不能在后面。
(5)判断是否含有e。
看e前后是否满足条件。后面:不能有小数,+单独出现,+不能出现在第二位及以后。
前面:如果有小数:跳转到小数。如果没有按照常数判断。
(6)将以上结果综合,就是最后结果了。【这是我的思路,对应用例写了半天,才写对】
(五)剑指offer里面的做法
思路是:从前往后扫描,先去除前面空格。然后进行有符号扫描,来判断‘+’。跳过第一个‘+’,向后扫描无符号。若碰到了符号,则判断符号类型,'.' 或者 ‘e’ 或者其他。如果是'.'则跳过 '.' 进行 无符号扫描。如果是'e'则进行有符号扫描。除了后面的空格之外,能扫描到最后,那么就是对的。
这种想法,借助了 点 和 e 只能出现1次的状态。
class Solution:
def isNumber(self, s: str) -> bool:
def scanSigned(s,start):
if start<len(s) and (s[start]=="+" or s[start] == "-") :
start+=1
return scanUnsinged(s,start)
def scanUnsinged(s,start):
i = start
while(start<len(s) and s[start] <='9' and s[start]>='0'):
start += 1
return start > i,start #
index = 0
while(index < len(s) and s[index] ==' '):index+=1 #去除空格
num,index = scanSigned(s,index) #扫描整数部分
if index < len(s) and s[index] == '.':
index += 1
Flag,index = scanUnsinged(s,index)
num = Flag or num #扫描小数部分
if index < len(s) and s[index] =='e':
index += 1
Flag,index = scanSigned(s,index)
num = Flag and num
while(index < len(s) and s[index]==' ') :index+=1
return num and index == len(s)
(7)4-26- 面试题63. 股票的最大利润
考点:这个题做过了, 主要是[a,b] a<b,b-a的最大值。
可以用单调栈来做,可以用DP来做。
普通做法:O(N^2)把max值存起来。普通做法超时。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
## O(N^2) 超时
max_ = -float('inf')
n = len(prices)
for i in range(0,n):
for j in range(i+1,n):
if prices[j] > prices[i]:
max_ = max(max_, prices[j]-prices[i])
if max_ < 0:
return 0
else:
return max_
单调栈:栈中元素是从小到大的,每次入栈先判断与栈顶的大小,若比栈顶大,则直接入栈(计算差最大)。若比栈顶小,则与栈中元素作比较,压入适合的位置。 时间 70.96% 空间:100%。 O(N) O(N)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) <= 1 :return 0
max_ = -float('inf')
s = []
s.append(prices[0])
i = 1
while(i<len(prices)):
if prices[i] > s[-1]: #跟栈顶元素比大小
s.append(prices[i])
max_ = max(max_,prices[i] - s[0]) # 和栈底作加减法
while(len(s)>0 and prices[i] < s[-1]): s.pop(-1)
# 找合适位置入栈
s.append(prices[i])
i += 1
if max_ < 0:
return 0
else:
return max_
DP方法:在O(N^2) 的时候考虑用DP降低时间复杂度。 时间 O(N) 空间 O(1)。 时间优化 : 97.88%。
DP三步骤:状态定义,转移方程,继续优化。其中我认为状态定义是最难的。
在状态定义的时候,要找一个可以表示从小到大关系的状态。这个题,求解的是最大利润,每天股票价格是按照时间排序的,所以状态就是前X天的最大利润。
转移方程,体现的是当前i状态是否改变。x天股票卖与不卖。x天时,状态是否改变。
dp[i] = max( dp[i-1] 不改变 , prices[i] - min(prices[0:i+1]) 改变 )
继续优化:按照转移方程,进行一些优化。一般可以分为时间和空间上的。
空间:只存储当前状态相关的信息。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) <= 1 :return 0
cost = float('inf')
profit = -float('inf')
for i in range(0,len(prices)):
profit = max(profit,prices[i]- min(cost,prices[i]))
cost = min(cost,prices[i])
return profit
(8)4-27- 面试题58 - I. 翻转单词顺序
比较简单,python一行就能解决,但是面试的时候最好不这样做。
class Solution:
def reverseWords(self, s: str) -> str:
return ' '.join(s.strip().split()[::-1])
方法一:双指针:从后往前数。 time O(N) space O(N)
class Solution:
def reverseWords(self, s: str) -> str:
n = len(s)
if n == 0 :return s
i,j = n-1,n-1
ans = ''
while(j >= 0):
if s[j] != ' ':
i = j
while(i>=0 and s[i] != ' '): i -= 1
ans += s[i+1:j+1] + ' '
j = i
else:
j -= 1
return ans[:len(ans)-1]
方法二:利用栈先入后出。[不写了]
(9)4-28- 面试题53 - II. 0~n-1中缺失的数字
排序数组中的搜索问题,首先想到 二分法 解决,双指针也是高频选项[摘录]。二分法为对数级别复杂度。
分析一下这个题。
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
这三个点是有效信息。
二分法最原始的就是遍历每个元素,然后查看哪个元素和其位置不对应。【没有利用递增这个信息点】。
利用递增可以这样想,如果查到某个元素,nums[m] = m,则前面的都是满足条件的,因为递增。则不满足条件的应该是后半部分。这样避免了二分法中左右两个部分全部都要遍历。 代码: O(logN) O(1)
class Solution:
def missingNumber(self, nums: List[int]) -> int:
i,j = 0,len(nums)-1
while(i<=j):
m = (i+j)//2
if nums[m] == m : i = m+1
else:j = m-1
return i
注意看 m如何推导出来的,看左边或者右面是如何赋值的,如何查找边界。
做法二:求和
按照题目要求,因为数组中每个数都在 【0-n】范围内,每个数字都是唯一的。
所以可以计算,【0-n】时,总和是多少。然后看下现在数组总和,做差就是题目要求。
class Solution:
def missingNumber(self, nums: List[int]) -> int:
# 求和
n = len(nums)
return (n+1)*n//2 - sum(nums) # [0,1,...,n] (n+1个数)
(10) 4-29-面试题50. 第一个只出现一次的字符
考点:哈希表(有序哈希表)
哈希表 时间复杂度O(2N),空间复杂度O(N)。 N 为字符串 s
的长度;需遍历 s
两轮;
有序哈希表 建立表时,需要O(N),查询 O(N)。遍历 s
一轮,遍历 dic
一轮
哈希表是 去重 的,即哈希表中键值对数量 ≤ 字符串 s 的长度。因此,相比于方法一(哈希表),方法二(有序哈希表)减少了第二轮遍历的循环次数。当字符串很长(重复字符很多)时,方法二则效率更高。 以上分析来自:链接。
Python 3.6 后,默认字典就是有序的,因此无需使用 OrderedDict()
。
class Solution:
def firstUniqChar(self, s: str) -> str:
## 自己的
if s == '':return ' '
# dic
l = list(s)
dic = collections.Counter(l)
ans = sorted(dic.items(),key = lambda x:x[1])
if ans[0][1] == 1:
return ans[0][0]
else:
return ' '
# 题解中的方法 作者:@ Krahets
dic = {}
for c in s:
dic[c] = not c in dic #第一次True 第二次碰见 False
for c in dic:
if dic[c] : #第一次 True,只出现一次
return c
return ' '
题解中,巧妙利用True False 统计出现第几次。只用了一个dic。
我的代码 用了O(3N)的空间。时间复杂度相同。
(11)4-30-面试题47. 礼物的最大价值
考点:DP。
分析:这个题目,第一反应就是DP。奈何我还是没正确写出来,转移方程。
DP题目,首先做的就是定状态,dp表示到X步骤时,获得的最大价值。
然后我就写出了下面的方程:ans = ans + max(grid[i+1][j] , grid[i][j+1]) 未确定往哪里走。
这样一写,其实和DP没什么关系了,就是顺着(0,0)往下走,一直到最后一个点(m-1,n-1)。其实算是一个贪心策略的想法,只考虑当前步骤往下面应该如何走。
但是,定状态的时候,定的是 x状态下,与x-1状态的关系,即x状态是一个确定的状态,而不是下一个状态。因为是一个矩阵问题,所以状态应该是当前走到 i,j 时,最大价值。
f(i,j) = max(f(i-1,j), f(i,j-1)) +grid[i][j] 当前状态已经确定了,就是i,j。 看的是和上一个状态的关系情况。[写完方程找边界]
正是因为转移方程描述的是与前一个状态,DP才有了从上到下的一个过程(递归从下到上),也才有了仅利用O(1)的空间来解决问题。
非递归从上到下,time O(MN) space O(MN)
建立数组,来存储,每个状态中最大。 【将f多给一行一列 m+1,n+1】 这样就直接写 转移方程就可以了。
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
m,n = len(grid),len(grid[0])
f = [[ 0 for i in range(n)] for j in range(m)]
# 建立数组
f[0][0] = grid[0][0]
# i-1 >= 0 and j-1>= 0:
# i-1 < 0 and j-1 >=0
# i-1 >=0 and j-1 < 0
for i in range(0,m):
for j in range(0,n):
if i-1>=0 and j-1>=0:
f[i][j] = max(f[i-1][j],f[i][j-1]) + grid[i][j]
elif i-1 <0 and j-1>=0:
f[i][j] = f[i][j-1] + grid[i][j]
elif i-1 >=0 and j-1<0:
f[i][j] = f[i-1][j] + grid[i][j]
return f[m-1][n-1]
从上到下,time O(MN) space O(1) :利用grid原地做。
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
m,n = len(grid),len(grid[0])
for i in range(0,m):
for j in range(0,n):
if i-1>=0 and j-1>=0:
grid[i][j] = max(grid[i-1][j],grid[i][j-1])+grid[i][j]
elif i-1 <0 and j-1>=0:
grid[i][j] = grid[i][j-1] + grid[i][j]
elif i-1 >=0 and j-1<0:
grid[i][j] = grid[i-1][j] + grid[i][j]
return grid[m-1][n-1]
有一个问题就是grid会被重复刷新吗?不会,因为for 循环保证了每个ij位置,只遍历一遍,只做一次更改。
当 grid矩阵很大时, i=0 或 j=0 的情况仅占极少数,相当循环每轮都冗余了一次判断。因此,可先初始化矩阵第一行和第一列,再开始遍历递推。思想来自:链接。
m, n = len(grid), len(grid[0])
for j in range(1, n): # 初始化第一行
grid[0][j] += grid[0][j - 1]
for i in range(1, m): # 初始化第一列
grid[i][0] += grid[i - 1][0]
for i in range(1, m):
for j in range(1, n):
grid[i][j] += max(grid[i][j - 1], grid[i - 1][j])
return grid[-1][-1]