剑指offer刷题
1. 二维数组中的查找
题目要求
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
代码
# -*- coding:utf-8 -*-
class Solution:
def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool:
# 可以从右上角或者左下角开始查找
# 第一种是从左下角开始查找
# if not matrix:
# return False
# rows = len(matrix)
# columns = len(matrix[0])
# column = 0
# row = rows - 1
# while column < columns and row >= 0:
# if matrix[row][column] < target:
# column += 1
# elif matrix[row][column] > target:
# row -= 1
# else:
# return True
# return False
# 第二种是从右上角开始查找
# if not matrix:
# return False
# rows = len(matrix)
# columns = len(matrix[0])
# row = 0
# column = columns - 1
# while row < rows and column >= 0:
# if matrix[row][column] > target:
# column -= 1
# elif matrix[row][column] < target:
# row += 1
# else:
# return True
# return False
# 暴力法
for mat in matrix:
if target in mat:
return True
return False
搜索法是从左下角开始查找,若数组元素>目标数,就向上查找;若小于,则向右查找。
该方法同理可使用于右上角,但是不可在左上角和右下角查找,因为左上角的下面和右边的数值都比当前元素的大,判断不出来。而右下角则是上面和左边元素都比当前元素小,因此也判断不出来。
时间复杂度O(m+n):其中,NN 和 MM 分别为矩阵行数和列数,此算法最多循环 M+NM+N 次。
空间复杂度O(1):row 和 column 指针使用常数大小额外空间。
暴力法是直接判断数组元素与目标元素是否相同,没有判断使用题目所给出的条件,从左到右和从上到下的递增排序。
2. 旋转数组的最小数字
题目要求
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0
思路
看到排序数组首先想到二分法,可以将遍历法的线性级别时间复杂度降低为对数级别。
代码
# -*- coding:utf-8 -*-
class Solution:
def minNumberInRotateArray(self, rotateArray):
# write code here
# 直接查找法
# if rotateArray == []:
# return 0
# return min(rotateArray)
# 二分查找法
if rotateArray == []:
return 0
left = 0
right = len(rotateArray) - 1
while (left < right):
mid = left + (right - left) // 2
# if rotateArray[left] < rotateArray[mid]:
# left += 1 # 注意
if rotateArray[mid] > rotateArray[right]:
left = mid + 1 # 注意
else:
right = mid # 注意
return rotateArray[left] # 注意
while True:
try:
s = Solution()
array = list(eval(raw_input()))
print(s.minNumberInRotateArray(array))
except:
break
直接查找法是暴力查找法,使用的是python自带的最小值函数
min()
.
二分查找法则是通过将旋转数组变成有序的,该链接详细地描述了二分查找算法的步骤和案例。
我们设定为left是最小的数字,即我们所输出的index。
上面标了四处“注意”字样。第一处表示当最左边的数字小于中间数字时,我们可以往后left + 1查找(这个也可以不用)。第二处是当中间的数字大于最右边数字时,将最左边索引变为mid+1,即中间后面一位数字
eval() 函数用来执行一个字符串表达式,并返回表达式的值。
raw_input() 将所有输入作为字符串看待,返回字符串类型。
更新代码(2021.8.19):
class Solution:
def minArray(self, numbers: List[int]) -> int:
if numbers == []: # 要考虑数组中没有元素的情况
return 0
left = 0 # 声明两个指针,分别指向数组的首尾
right = len(numbers) - 1
while left < right:
middle = (left + right) // 2
if numbers[middle] > numbers[right]:
left = middle + 1
elif numbers[middle] < numbers[right]:
right = middle
else:
# right -= 1 # 超出时间限制
# 遇到nums[m] = nums[j]时,一定有区间[i, m]内所有元素相等 或者 区间 [m, j][m,j] 内所有元素相等(或两者皆满足),直接放弃二分查找,改用线性查找
return min(numbers[left:right])
return numbers[left]
- 时间复杂度:O(logn),在特殊情况下(如整个非递减数组中各元素均相等,其时间复杂度会变成O(n)。
- 空间复杂度:O(1),因为left、right、middle变量均使用常数大小的额外空间。
3. 获取只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
思路
使用异或运算法则来编写代码。
-
任何数与0异或为任何数,数学表示:
0 ^ n => n
-
相同的数异或为0,数学表示为:
n ^ n => 0
python3 代码
class Solution:
def singleNumber(self, nums):
num = 0
for i in nums:
num = num ^ i
print(num)
return num
遇到的问题
-
def singleNumber(self, nums: List[int]) -> int:
这是什么意思?
答案: 这是新增的语法,为了说明参数和返回值的数据类型。不过仅仅的给人看的,实际上程序并不检查是否是相符的。
https://fishc.com.cn/thread-134130-1-1.html -
TypeError: singleNumber() missing 1 required positional argument: 'nums' Solution.singleNumber(a) Line 8 in <module> (Solution.py)
4. 合并两个有序的数组
说明
给出两个有序的整数数组A和B ,请将数组B合并到数组A中,变成一个有序的数组。
思路
列表型数组能够进行相加组合(用到一个知识点,“双指针”),然后使用sort()
或者sorted()
进行排序即可。
python3 代码
输入:[],[1]
输出:[1]
class Solution:
def merge(self , A, m, B, n):
# write code here
A[:] = A[:m] + B
A.sort()
“思路碰碰车”
-
不能仅使用
A = A + B
。 -
A[:m]
是把所有的原A数组选取,使用A[:]
是因为要对合并数组进行选取。 -
试过
A[:] = A[:] + B
会报错,输出的是[0,1]
。
猜测是因为原
A = []
,这样数组的长度就为0,A[:] + B
直接相加,那么长度应该是1才对,为什么有两个数???
- 也试过
A[:m+n] = A[:m] + B[:]
,这样是可以的。其中,[:]
表示全部取完,[:m]
仅取到m-1之前的数。 A[:m] = sorted(A[:m] + B[:])
这样输出的是[1,0]
。
5. 二进制中1的个数
题目要求
请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
解题思路
使用bin先将n转化为2进制,然后再转为字符串,最后使用count()来数出1的个数。
代码
class Solution:
def hammingWeight(self, n: int) -> int:
# return bin(n).count("1")
return collections.Counter(bin(n)[2:])["1"]
一行代码解决它~~~
6. 0~n-1中缺失的数字
题目要求
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
解题思路
(以下内容感谢leetcode的KK大佬)
更新: 题目说到是递增排序的数组,立马就要想到二分法。先假设两个指针,头指针i
和尾指针j
。在i<=j
的循环情况下执行下面的代码,与第9题一样,定义一个中间变量的值m,然后对nums[m]与计算出来的中间值进行判断,因为题目说了是从0开始的递增数组,所以可以使用这样的方法做判断。若满足nums[m] == m,表示0~m的排序没有问题,数值和索引是对应的,则把左边界缩紧;若不满足,则修改右边界的范围。
代码
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
7. 扑克牌中的顺子
题目要求
从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
解题思路
代码
class Solution:
def isStraight(self, nums: List[int]) -> bool:
# 排序+遍历
# joker = 0 # 用于记录大小王的个数,若存在,则joker++
# # 首先,先对数组进行排序
# nums.sort()
# # 判断5张牌中是否有重复
# for i in range(4): # 遍历nums里面的数
# if nums[i] == 0: joker += 1 # 若存在大小王,joker就加1
# elif nums[i] == nums[i+1]: return False # 如果数组中存在重复的数,则提前返回false
# # 获取最大/最小的牌,若满足条件,则可构成顺子
# return nums[4] - nums[joker] < 5
# 集合+遍历
repeat = set()
# 设置辅助变量mi,ma,用于统计最大值和最小值,来判断数组中“最大值-最小值<5”
mi, ma = 14, 0
for num in nums:
if num == 0: continue
ma = max(ma, num)
mi = min(mi, num)
# 若数组中在repeat集合中,说明存在重复的数,则提前返回false
if num in repeat: return False
# 若不存在repeat集合中,那就把数添加到集合repeat中
repeat.add(num)
return ma - mi < 5
8. 最小的k个数
题目要求
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
解题思路
先排序,然后取前k个即可。
代码
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
arr.sort()
return arr[:k]
9. 在排序数组中查找数字 I
题目要求
统计一个数字在排序数组中出现的次数。
解题思路
解法有多种,现在分享代码量最好和效率最高的方法。
解法一: 一行代码直接解。直接计算目标值的数量。(面试肯定不能这样写,虽然是一句话的代码。CPython的C源码的时间复杂度是O(n),明面上并没有二分搜索的O(logn)来得快。但由于list.count()的实现是C语言,所以它实际上很快)
解法二: 排序数组中的搜索问题,首先想到 二分法 解决。这是一个双指针的解法。设i为头指针,j为尾指针。在i<=j
的情况下一直循环,若不满足则跳出循环。定义一个表示中间变量的值mean = (i + j) // 2
,然后判断这个mean与我们所需查找的数字的差距,若大于所要查找的数字,那就尾指针减一;反之,则头指针加一。
- 时间复杂度 O(log N): 二分法为对数级别复杂度。
- 空间复杂度 O(1) : 几个变量使用常数大小的额外空间。
为什么不是/而是//呢?
因为/会有小数点,而//会向下取整
代码
class Solution:
def search(self, nums: List[int], target: int) -> int:
# 解法一
# return nums.count(target)
# 解法二
def SearchRight(nums, target):
i, j = 0, len(nums) - 1
while i <= j:
mean = (i + j) // 2
if nums[mean] <= target: # 这里是“小于等于”,目的是为了确定右边界,就是说当mid等于target时,因为不确定后面还有没有target,所以同样需要左边收缩范围
i = mean + 1
else:
j = mean - 1
return i
return SearchRight(nums, target) - SearchRight(nums, target-1) # searchNum(nums, target - 1)为的是找到左边界
解法一:
解法二:
10. 两数之和
题目要求
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
解题思路
解法一: 使用类似哈希表的做法,构建一个字典,用dic.get来获取对应值的索引。
- 时间复杂度:O(n),其中n是数组中的元素数量。对于每一个元素num,可以O(1)的寻找差值。
- 空间复杂度:O(n),同样的n也是数组中的元素数量。
解法二: 同样使用哈希表的做法。
代码
解法一:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
dic = {}
for i, num in enumerate(nums):
if target - num in dic: # 检查哈希表中是否有这个差值,如果存在,那就返回索引结果
return [i, dic[target - num]]
dic[nums[i]] = i # 如果不满足,那就以当前的数值作为key,而索引作为value存入哈希表中
return []
解法二:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
# dic_map = {x: i for i, x in enumerate(nums)}
dic_map = dict()
for i in range(len(nums)):
x = nums[i]
if (target - x) in dic_map:
index = dic_map[target - x]
# if i != index:
return [i, index]
dic_map[x] = i
return []