文章目录
0. 前言
题目来源:牛客网剑指offer专题
编程语言为 python
1. 搜索算法
1.1 JZ53 数字在升序数组中出现的次数(二分法)
描述
给定一个长度为 n 的非降序数组和一个非负数整数 k ,要求统计 k 在数组中出现的次数
数据范围:
0
≤
n
≤
1000
,
0
≤
k
≤
100
0 \le n \le 1000 , 0 \le k \le 100
0≤n≤1000,0≤k≤100,数组中每个元素的值满足
0
≤
v
a
l
≤
100
0 \le val \le 100
0≤val≤100
要求:空间复杂度
O
(
1
)
O(1)
O(1),时间复杂度
O
(
l
o
g
n
)
O(logn)
O(logn)
示例1
输入:[1,2,3,3,3,3,4,5],3
返回值:4
示例2
输入:[1,3,4,5],6
返回值:0
二分法
思路:
因为data是一个非降序数组,它是有序的,这种时候我们可能会想到用二分查找。但是一个数组可能有多个k,而且我们要查找的并非常规二分法中k出现的位置,而是k出现的左界和k出现的右界。要是能刚好找到恰好小于k的数字位置和恰好大于k的数字的位置就好了。
再有因为数组中全是整数,因此我们可以考虑,用二分查找找到 k + 0.5 k+0.5 k+0.5应该出现的位置和 k − 0.5 k−0.5 k−0.5应该出现的位置,二者相减就是k出现的次数。
具体做法:
- step 1:写一个二分查找的函数在数组中找到某个元素出现的位置。每次检查区间中点值,根据与中点的大小比较,确定下一次的区间。
- step 2:分别使用二分查找,找到 k + 0.5 k+0.5 k+0.5 和 k − 0.5 k-0.5 k−0.5 应该出现的位置,中间的部分就全是 k k k,相减计算次数就可以了。
class Solution:
def GetNumberOfK(self , data: List[int], k: int) -> int:
def binary_search(k):
low = 0
high = len(data) - 1
while low <= high:
mid = (low + high) // 2
if data[mid] < k:
low = mid + 1
elif data[mid] > k:
high = mid - 1
return low # 返回第1个大于k的位置
return binary_search(k + 0.5) - binary_search(k - 0.5)
![](https://img-blog.csdnimg.cn/7d8d5e50414f4afbb7a8aede6fcf1333.png)
1.2 JZ4 二维数组中的查找(二分法)
描述
在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。
数据范围:矩阵的长宽满足 0 ≤ n , m ≤ 500 0 \le n,m \le 500 0≤n,m≤500, 矩阵中的值满足 0 ≤ v a l ≤ 1 0 9 0 \le val \le 10^9 0≤val≤109
进阶:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n + m ) O(n+m) O(n+m)
示例1
输入:7,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
返回值:true
说明:存在7,返回true
示例2
输入:1,[[2]]
返回值:false
示例3
输入:3,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
返回值:false
说明:不存在3,返回false
暴力搜索
略。
二分法
class Solution:
def Find(self , target: int, array: List[List[int]]) -> bool:
def binary_search(arr, k):
low = 0
high = len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] < k:
low = mid + 1
elif arr[mid] > k:
high = mid - 1
else:
return True
return False
for subarray in array:
if binary_search(subarray, target):
return True
return False
![](https://img-blog.csdnimg.cn/027883b68c284f5db81db7378d530ca3.png)
1.3 JZ11 旋转数组的最小数字(二分法)
描述
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
数据范围:
1
≤
n
≤
10000
1 \le n \le 10000
1≤n≤10000,数组中任意元素的值:
0
≤
v
a
l
≤
10000
0 \le val \le 10000
0≤val≤10000
要求:空间复杂度:
O
(
1
)
O(1)
O(1),时间复杂度:
O
(
l
o
g
n
)
O(logn)
O(logn)
示例1
输入:[3,4,5,1,2]
返回值:1
示例2
输入:[3,100,200,3]
返回值:3
暴力搜索
略。
二分法
这道题依旧可以用二分法做,不过更新 low 和 high 的条件变了。
- rotateArray[mid] > rotateArray[high],此时最小值一定在右半区(不包含mid),如
[3,4,5,1,2]
- rotateArray[mid] < rotateArray[high],此时最小值一定在左半区(包括mid),如
[4,5,1,2,3]
- rotateArray[mid] = rotateArray[high],此时最小值无法判断在哪,如
[3,1,2,2,2]
,[2,3,1,1,1]
,此时可以通过high = high - 1
缩小判断范围。
class Solution:
def minNumberInRotateArray(self , rotateArray: List[int]) -> int:
low = 0
high = len(rotateArray) - 1
while low <= high:
mid = (low + high) // 2
if rotateArray[mid] > rotateArray[high]:
# 此时最小值一定在右半区
low = mid + 1
elif rotateArray[mid] < rotateArray[high]:
# 此时最小值一定在左半区(包括mid)
high = mid
else: # rotateArray[mid] == rotateArray[high]
# 此时无法判断最小值在哪,只能缩小下范围
high = high - 1
return rotateArray[low]
![](https://img-blog.csdnimg.cn/ed0280bb1d03440ba1975ccdc50d9dc5.png)
1.4 JZ38 字符串的排列(递归+回溯)
描述
输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。
例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。
数据范围:
n
<
10
n < 10
n<10
要求:空间复杂度
O
(
n
!
)
O(n!)
O(n!),时间复杂度
O
(
n
!
)
O(n!)
O(n!)
输入描述:
输入一个字符串,长度不超过10,字符只包括大小写字母。
示例1
输入:"ab"
返回值:["ab","ba"]
说明:返回["ba","ab"]也是正确的
示例2
输入:"aab"
返回值:["aab","aba","baa"]
示例3
输入:"abc"
返回值:["abc","acb","bac","bca","cab","cba"]
示例4
输入:""
返回值:[]
递归+回溯
都是求元素的全排列,字符串与数组没有区别,一个是数字全排列,一个是字符全排列,因此大致思路与有重复项数字的全排列类似,只是这道题输出顺序没有要求。但是为了便于去掉重复情况,我们还是应该参照数组全排列,优先按照字典序排序,因为排序后重复的字符就会相邻,后续递归找起来也很方便。
使用临时变量去组装一个排列的情况:每当我们选取一个字符以后,就确定了其位置,相当于对字符串中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归。
终止条件: 临时字符串中选取了n个元素,已经形成了一种排列情况了,可以将其加入输出数组中。
返回值: 每一层给上一层返回的就是本层级在临时字符串中添加的元素,递归到末尾的时候就能添加全部元素。
本级任务: 每一级都需要选择一个元素加入到临时字符串末尾(遍历原字符串选择)。
递归过程也需要回溯,比如说对于字符串“abbc”,如果事先在临时字符串中加入了a,后续子问题只能是"bbc"的全排列接在a后面,对于b开头的分支达不到,因此也需要回溯:将临时字符串刚刚加入的字符去掉,同时vis修改为没有加入,这样才能正常进入别的分支。
具体做法:
- step 1:先对字符串按照字典序排序,获取第一个排列情况。
- step 2:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
- step 3:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用,也不需要将其纳入。
- step 4:进入下一层递归前将vis数组当前位置标记为使用过。
- step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素,
- step 6:临时字符串长度到达原串长度就是一种排列情况。
class Solution:
def Permutation(self , str: str) -> List[str]:
res = []
def recursion(string, tmp_str, vis):
if len(tmp_str) == len(string):
res.append(tmp_str)
return
for i in range(len(string)):
if vis[i] == 1:
continue
if i > 0 and string[i-1] == string[i] and vis[i-1] == 1:
# 当前str_list[i]与本层str_list[i-1]相同且str_list[i-1]已经用过
continue
vis[i] = 1
tmp_str += string[i]
# 递归
recursion(string, tmp_str, vis)
# 回溯
vis[i] = 0
tmp_str = tmp_str[:-1]
str_sort = sorted(list(str)) # 排序,得到的是list
vis = [0] * len(str_sort) # 标记每个位置的字符是否被使用过
tmp_str = ""
recursion(str_sort, tmp_str, vis)
return res
![](https://img-blog.csdnimg.cn/f4b138af1a474433b4c37e89e9222903.png)
1.5 JZ44 数字序列中某一位的数字(位数减法)
描述
数字以 0123456789101112131415… 的格式作为一个字符序列,在这个序列中第 2 位(从下标 0 开始计算)是 2 ,第 10 位是 1 ,第 13 位是 1 ,以此类题,请你输出第 n 位对应的数字。
数据范围: 0 ≤ n ≤ 1 0 9 0 \le n \le 10^9 0≤n≤109
示例1
输入:0
返回值:0
示例2
输入:2
返回值:2
示例3
输入:10
返回值:1
示例4
输入:13
返回值:1
位数减法
思路:
我们尝试来找一下规律:
小于10的数字一位数,1~9,共9个数字,9位;
小于100的数字两位数,10~99,共90个数字,180位;
小于1000的数字三位数,100~999,共900个数字,2700位;
……
我们可以用这样的方式,不断减去减去前面位数较少的数字的那些位,锁定第n位所在的区间,即第n位是几位数。这个区间的起点值加上剩余部分除以这个区间的位数就可以定位n在哪个数字上,再通过n对位数取模可以定位是哪一位。(下标从0开始,需要对n减1)
具体做法:
- step 1:通过对每个区间起点数字的计算,按照上述规律求得该区间的位数,n不断减去它前面区间的位数,定位到属于它的区间。
- step 2:通过除以位数定位n在哪个数字上,用字符串形式表示。
- step 3:通过在字符串上位置对几位数取模定位目标数字。
class Solution:
def findNthDigit(self , n: int) -> int:
digit = 1 # 位数,如1、2、3
start = 1 # 当前位数起始数字,如1、10、100
num_digit = 9 # 该位数对应数字所占的空间,如9*1、90*2、900*3
while n > num_digit:
n -= num_digit
digit += 1
start *= 10
num_digit = 9 * start * digit
num = start + (n - 1) // digit # 定位是哪个数字
index = (n - 1) % digit
return int(str(num)[index])
![](https://img-blog.csdnimg.cn/f7ab63682ae14526a67c8fe5f0b9e713.png)
2. 动态规划
2.1 JZ42 连续子数组的最大和()
描述
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。
数据范围:
1
<
=
n
<
=
2
×
1
0
5
1 <= n <= 2\times10^5
1<=n<=2×105
−
100
<
=
a
[
i
]
<
=
100
-100 <= a[i] <= 100
−100<=a[i]<=100
要求:时间复杂度为
O
(
n
)
O(n)
O(n),空间复杂度为
O
(
n
)
O(n)
O(n)
进阶:时间复杂度为
O
(
n
)
O(n)
O(n),空间复杂度为
O
(
1
)
O(1)
O(1)
示例1
输入:[1,-2,3,10,-4,7,2,-5]
返回值:18
说明:经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18
示例2
输入:[2]
返回值:2
示例3
输入:[-10]
返回值:-10
动态规划(时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n))
设动态规划列表 max_sum,max_sum[i] 代表以元素 array[i] 为结尾的连续子数组最大和。
状态转移方程: max_sum[i] = max(max_sum[i-1] + array[i], array[i]);
具体思路:
- 遍历数组,比较 max_sum[i-1] + array[i] 和 array[i] 的大小;
- 为了保证子数组的和最大,每次比较都取两者的最大值;
- 用 res 变量记录计算过程中产生的最大的连续和 max_sum[i];
class Solution:
def FindGreatestSumOfSubArray(self , array: List[int]) -> int:
max_sum = [array[0]] # 记录到array[i]为结尾的最大和
res = array[0]
for i in range(1, len(array)):
max_sum.append(max(max_sum[i-1] + array[i], array[i]))
res = max(max_sum[i], res)
return res
动态规划(时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1))
因为遍历中每次都只用到了 max_sum[i-1],前面的都没用了,因此可以直接用 变量 代替 列表,可以达到同样的效果。
class Solution:
def FindGreatestSumOfSubArray(self , array: List[int]) -> int:
max_sum = array[0] # 记录到array[i]为结尾的最大和
res = array[0]
for i in range(1, len(array)):
max_sum = max(max_sum + array[i], array[i])
res = max(max_sum, res)
return res