数组
Leetcode数组刷题记录
二分查找
二分查找的难点在于区间的判断,主要有两个易错点:
while left <= right:
orwhile left < right
- 当
nums[mid] > target
时right = mid
还是right = mid - 1
防止int
数据类型相加越界,可以采用 mid = left + (right - left) // 2
区间的定义和概念才是解决这类问题的根本
- 对于
[left, right]
左闭右闭的区间(初始赋值right = len - 1)
只要区间合法,就进入循环,所以while left <= right:
判断过nums[mid] > target
,mid一定不在区间内,所以right = mid - 1
,把mid排除在外,如果用了right = mid
就会出现[1, 2, 3, 4]
target = 5
left = right = 3
一直死循环 - 对于
[left, right)
左闭右闭的区间(初始赋值right = len)
只要区间合法,就进入循环,所以while left < right:
当left = right的时候,区间里已经不包含任何一个数了
判断过nums[mid] > target
,mid一定不在区间内,所以right = mid
,把mid排除在外
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
循环不变量:每次的区间都“可能”包含了target,区间的定义是不变量
变体应用:
- 搜索升序 无重复数的数组,若存在返回索引,若不存在返回应该插入位置的索引
个人觉得难点在于没有找到的时候return left
还是return right
注意根据左闭右闭区间的定义,此时nums[right] < target
target 显然不能插在right的位置,right刚好是target前一位return left
orreturn rignt+1
- 算
x
\sqrt x
x,找不到的时候,如果是取下整,就返回
return right
,理由同前
数组双指针
题1:我们需要删除数组中某个元素,保证其他不被删除的元素排列在数组的前K位,并返回K+1(有效元素的个数)
不知道双指针解法的我,用了一种类似的思路
def removeElement(self, nums: List[int], val: int) -> int:
n = len(nums) - 1
i = 0
while n >= i:
if nums[i] == val:
while nums[n] == val and n > i:
n -= 1
nums[i] = nums[n]
n -= 1
i += 1
return n + 1
其中i从前往后遍历,n从后往前遍历,效率也是O(n),但是由于存在循环嵌套,比双指针的解法还是差点
用双指针的方式可以在一个for循环下完成两个for的工作双指针问题一定要明确两个指针的含义是什么(个人理解:另一种循环不变量,在每次循环中表达的含义都相同)
- 慢指针:“自成体系”的新建了一个总共有K+1个元素的数组,它指向这个新数组的下标索引
- 快指针: 指向“老”的数组中的元素,方便我们判断这个元素是否合适被放进“新”数组里面;或者可以理解成,指向新数组的元素(可能被纳入新数组的元素)
def removeElement(self, nums: List[int], val: int) -> int:
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast] #填补空缺
slow += 1
return slow
最后return就是slow,因为每次slow都加1,所以相当于在下一次循环开始前,slow指向新数组的后一位
题2:要求把0都放到数组最后,保持前面的数排序不变
我一开始的解答
def moveZeroes(self, nums: List[int]) -> None:
slow = 0
for fast in range(1, len(nums)):
if nums[slow] == 0 and nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
elif nums[slow] != 0:
slow += 1
但如果明确在这道题目里面的双指针含义,根本不需要用这么复杂的分类讨论
- 快指针:指向有效的元素开头(还未处理的部分)
- 慢指针:指向已经处理好的序列尾部
- 慢指针和快指针中间全部是0
抓住这些循环不变量,程序完全可以套用一般的双指针,只要快指针指向的元素非0就交换并更新慢指针
def moveZeroes(self, nums: List[int]) -> None:
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
题3:比较两个字符串是否相同,其中#表示回车,会删除之前的字母,对着空字符串按回车,仍然为空字符串
不用双指针,暴力遍历解答,用了一下栈这种数据结构,毕竟回车操作是栈的典型代表
def backspaceCompare(self, s: str, t: str) -> bool:
ls = [''] * len(s)
lt = [''] * len(t)
ptrs = -1
ptrt = -1
# 后进先出是栈的数据结构
for letter in s:
if letter != '#':
ptrs += 1
ls[ptrs] = letter
elif ptrs != -1:
ptrs -= 1
for letter in t:
if letter != '#':
ptrt += 1
lt[ptrt] = letter
elif ptrt != -1:
ptrt -= 1
if ls[:ptrs+1] == lt[:ptrt+1]:
return True
else:
return False
双指针,不过此处的双指针,非彼处的双指针,之前都是用在一个数组里的快慢指针,现在是遍历两个字符串的指针
此题的关键在于看到,如果从前往后比较,我们不知道这个字符会不会被删除,但是如果从后往前比较,我们可以肯定这个字符是否被删除
def get_next_char(self, string: str, ptr: int, skip : int) -> tuple:
while ptr >= 0:
if string[ptr] == "#":
skip += 1
ptr -= 1
elif skip != 0:
skip -= 1
ptr -= 1
else:
break # 需要比较的字符
return (ptr, skip)
def backspaceCompare(self, s: str, t: str) -> bool:
ptrs = len(s) - 1
ptrt = len(t) - 1
skips = 0
skipt = 0
while ptrs >= 0 or ptrt >= 0:
ptrs, skips = self.get_next_char(s, ptrs, skips)
ptrt, skipt = self.get_next_char(t, ptrt, skipt)
if ptrs >= 0 and ptrt >= 0 and s[ptrs] != t[ptrt]:
return False
ptrs -= 1
ptrt -= 1
if ptrs == ptrt:
return True
else:
return False
做了很久需要回看
题4:给你一个按非递减顺 排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序
有思维定势,总觉得非降序排序一定要从小排到大,但是如果从两边开始确定,就根本不需要找“0”作为正负临界点啦,对比一下代码,明显第二个更简单
def sortedSquares(self, nums: List[int]) -> List[int]:
neg = 0
pos = 0
lit = []
while pos < len(nums) and nums[pos] < 0:
pos += 1
neg = pos - 1
while neg >= 0 and pos < len(nums):
if nums[neg] * nums[neg] < nums[pos] * nums[pos]:
lit.append(nums[neg] * nums[neg])
neg -= 1
else:
lit.append(nums[pos] * nums[pos])
pos += 1
while neg >= 0:
lit.append(nums[neg] * nums[neg])
neg -= 1
while pos < len(nums):
lit.append(nums[pos] * nums[pos])
pos += 1
return lit
def sortedSquares(self, nums: List[int]) -> List[int]:
neg = 0
pos = len(nums) - 1
lit = [0]* len(nums)
i = len(nums) - 1
while neg <= pos:
if nums[neg] * nums[neg] < nums[pos] * nums[pos]:
lit[i] = nums[pos] * nums[pos]
pos -= 1
else:
lit[i] = nums[neg] * nums[neg]
neg += 1
i -= 1
return lit
滑动窗口
也是一种双指针,只不过此处指针的作用是确定子区间
题1: 给定一个含有 n 个正整数的数组和一个正整数 target,找出该数组中满足其总和大于等于 target 的长度最小的,子数组
没有接触双指针之前,我写了这样一种错误答案
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
left = 0
right = 1
# 左闭右开区间
while right <= len(nums):
if sum(nums[left:right]) < target:
right += 1
elif sum(nums[left+1:right]) >= target:
left += 1
elif right < len(nums) and nums[left] <= nums[right]:
left += 1
right += 1
else:
break
print(left, right)
if left == 0 and right > len(nums):
return 0
else:
return right - left
这个在跑测试nums = [10,5,13,4,8,4,5,11,14,9,16,10,20,8]
过不了,原因在于更新指针的方法没有想清楚,规定当nums[left] <= nums[right]
时更新,但是这样有问题,因为有可能 right往后移动遇到了更大的数字,虽然目前的right指向的数字是小于left的,但是移动后还是能找到更短的区间
构建双指针的思路,应该从暴力解法开始想(本质上,滑动窗口就是对暴力遍历解的一种优化)在暴力遍历解法中,一个循环确定区间初始位置,一个循环确定区间终止位置,那么哪一个循环可以优化呢?终止位置显然不能优化(必须把每个数都遍历一遍才能找到最短区间),但是起始位置却不是每次都要更新的,如果当前区间都没达到target的要求,那起始位置不需要更新
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
left = 0
count = 0
length = len(nums)
# 左闭右闭区间
for right in range(len(nums)):
count += nums[right]
while count >= target:
if right - left + 1 < length:
length = right - left + 1
count -= nums[left]
left += 1
if length == len(nums) and left == 0 and count < target:
return 0
else:
return length
个人思考比较久的几个点
- 计数器(length)的使用 一开始的思路没有想着记录最短长度,认为直接通过滑动left 和 right,使得最后找到的就是最短长度。不使用计数器length来记录当前的最短长度,会导致更新条件非常难设计 — 回到我的解法错误的原因
while
循环取不取等while count >= target
orwhile count > target
这个等于号如果不加上的话,会导致找出来的区间长一个单位。要明确每次for 循环的时候的不变量,每次for 循环的时候当前子序列的sum一定是target的,不然就不需要加上nums[right]啦(如果都满足target要求,再加上一个不就多出来了吗?区间也更长了)while
循环的作用:其实这种思路在我看来就是确定子区间的尾(通过外循环for),找出以这个数结尾的最短子区间,并和已有记录的最短子区间(length)比较,如果小于目前全局的最短子区间,就更新记录,而while循环就是在找当前以right结尾区间中,符合要求的最短子区间- 什么时候
return 0
一开始写了if length == len(nums) and count < target:
当sum(nums) == target
时有问题,因为while经历了left + 1和count - nums[left]的过程,所以加上了left == 0
这个判断
题2: 每个树长一种果子,篮子里只能装两种不同类型的果子,但是对于果子的数量没有限制,问最多能装多少果子
def totalFruit(self, fruits: List[int]) -> int:
result = 0
basket = [0] * len(fruits)
fruittype = 0
left = 0
for right in range(len(fruits)):
basket[fruits[right]] += 1
if basket[fruits[right]] == 1:
fruittype += 1
while fruittype > 2:
basket[fruits[left]] -= 1
if basket[fruits[left]] == 0:
fruittype -= 1
left += 1
a = right - left + 1
if a > result:
result = a
return result
关键:
- 滑动窗口:滑动窗口之所以能把 O ( n 2 ) O(n^2) O(n2) 的时间复杂度降低到 O ( n ) O(n) O(n) 关键在于左指针的移动,右指针还是要遍历每个数的,但是左指针每次都是移动到使得区间符合要求(有效的区间)左指针的移动次数有上界 ≤ n \leq n ≤n
- 计数容器basket:如何记录目前篮子里的水果种类,用数组可以,也可以用字典
题3:最小覆盖子串
思路同,还是滑动窗口,主要增加了判断一个字符串是否为子串的逻辑
新学一下python default dict的用法
from collections import defaultdict
# 创建一个默认字典,默认值为 0
need = defaultdict(int)
# 访问不存在的键
print(need['a']) # 输出: 0
# 修改键的值
need['a'] += 1
print(need['a']) # 输出: 1
# 访问另一个不存在的键
print(need['b']) # 输出: 0
defualtdict当访问不存在的键的时候返回0,可以简单理解为一个所有键值对都初始化为0的字典
class Solution:
def check (self, alpha1: dict, alpha2: dict) -> bool:
for key in alpha1:
if key in alpha2:
if alpha1[key] > alpha2[key]:
return False
else:
return False
return True
## 判断覆盖所有字符
def minWindow(self, s: str, t: str) -> str:
alphat = {}
alphas = {}
result = float('inf')
left_a = 0
right_a = 0
for char in t:
if char in alphat:
alphat[char] += 1
else:
alphat[char] = 1
left = 0
for right in range(len(s)):
if s[right] in alphas:
alphas[s[right]] += 1
else:
alphas[s[right]] = 1
while self.check(alphat, alphas):
length = right - left + 1
if length < result:
result = length
left_a = left
right_a = right
if alphas[s[left]] == 1:
del alphas[s[left]]
else:
alphas[s[left]] -= 1
left += 1
if result == float('inf'):
return ""
else:
return s[left_a:right_a+1]
螺旋矩阵
感觉这类题目没什么特殊的算法思想,也不是很难,主要考察对边界条件的判断,个人做题习惯是通过例子去想边界条件应该怎样设计,配上适当的调试
题1:给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix
def generateMatrix(self, n: int) -> List[List[int]]:
lit = [[0 for _ in range(n)] for _ in range(n)]
col = 0
row = 0
k = 0
i = 1
while i <= n*n:
for col in range(k,n-k):
lit[row][col] = i
i += 1
k += 1
for row in range(k, n + 1 - k):
lit[row][col] = i
i += 1
for col in range(n - k - 1, k - 2,-1):
lit[row][col] = i
i += 1
for row in range(n - k - 1,k - 1,-1):
lit[row][col] = i
i += 1
return lit
但是对比解答,我的这个代码还是有点不够清晰,我处理的时候遵循,第一条边包含头尾,第二条边包含尾部,第三条边包含尾部,第四条边去头去尾,在每次循环的时候处理方式是统一的
其实可以做到对每条边处理方式是统一的,然后 k k k明确的含义是 n − k n-k n−k为子矩阵(还未被处理的矩阵)的大小
另外:lit = [[0] * n] * n
这样创建的列表是浅拷贝,牵一发而动全身,改一个值会影响到其他值,要用lit = [[0 for _ in range(n)] for _ in range(n)]
这样的方式创建列表
代码优化
def generateMatrix(self, n: int) -> List[List[int]]:
lit = [[0 for _ in range(n)] for _ in range(n)]
startx = 0
starty = 0
k = 1
i = 1
while i < n*n:
for col in range(starty, n - k):
lit[startx][col] = i
i += 1
for row in range(startx, n - k):
lit[row][n - k] = i
i += 1
for col in range(n - k, starty,-1):
lit[n - k][col] = i
i += 1
for row in range(n - k,startx,-1):
lit[row][starty] = i
i += 1
startx += 1
starty += 1
k += 1
if n % 2 == 1: #奇数填补中间
lit[n//2][n//2] = n*n
return lit
这种方法奇数偶数要单独判断,通过startx和starty来遍历的话,最后一次不会进入for循环
题2:螺旋打印 m × n m \times n m×n的矩阵
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
lit = []
m = len(matrix) #行数
n = len(matrix[0]) #列数
startx = 0
starty = 0
endx = m - 1 #左闭右闭
endy = n - 1
# 通过startx starty endx endy边界控制螺旋矩阵
while startx <= endx and starty <= endy:
for i in range(starty, endy + 1): #从左往右
lit.append(matrix[startx][i])
startx += 1 #最上排的遍历完了,上界下移
for i in range(startx, endx + 1):
lit.append(matrix[i][endy])
endy -= 1 #最右排的遍历完了,右界左移
if endx >= startx:
for i in range(endy, starty - 1, -1):
lit.append(matrix[endx][i])
endx -= 1
if endy >= starty:
for i in range(endx,startx - 1, -1):
lit.append(matrix[i][starty])
starty += 1
return lit
关键:
- 四个边界:通过
startx
starty
endx
endy
直接的框定边界大大方便了程序的逻辑判断,相当于每次确定一个子矩阵,在子矩阵里重复相同的运算 - 加入
if
判断防止重复打印:if endx >= startx
这个是一开始写没有加入的条件,但是这个是必须的,因为for i in range(endy, starty - 1, -1)
这个for循环只能判断横向区间是否有效,但是不能判断纵向区间是否有效,不加入会出现重复打印,如下图。实际上通过前两个for,上边界从黄色被移动到绿色和下边界重合,右边界从黄色移动到绿色,但是如果不判断上下边界,右边界和左边界之间还夹了一个6,会重复打印
前缀和思想
用于快速计算任意子区间的和,方便查询,把查询的时间复杂度从 O ( n ) O(n) O(n) 降低到 O ( 1 ) O(1) O(1)
题1:给定一个整数数组 Array,计算该数组在每个指定区间内元素的总和
注意点:
- 区间 假设要计算
[
a
,
b
]
[a,b]
[a,b]闭区间的sum(a和b都是下标索引)要用
n
u
m
s
[
b
]
−
n
u
m
s
[
a
−
1
]
nums[b] - nums[a - 1]
nums[b]−nums[a−1]因为前缀和数组
nums
里面,每个下标存储的都是从 [ 0 , i ] [0,i] [0,i]的sum,包括 i i i,这样的话就会存在数组越界的问题, - 前缀和数组定义 n u m s [ b ] − n u m s [ a − 1 ] nums[b] - nums[a - 1] nums[b]−nums[a−1] 算法当a = 0的时候要出错,我个人喜欢给前缀和数组前加一个0,原数组若length为n,前缀和数组定义为n+1
n = int(input())
array = [0]
count = 0
for _ in range(n):
count += int(input())
array.append(count)
while True:
try:
line = input().strip()
parts = line.split()
except:
break
print(array[int(parts[1]) + 1] - array[int(parts[0])])
题2:有 n × m n \times m n×m块土地,要使得横/竖分割土地的价值差异最小
line = input().strip()
parts = line.split()
m = int(parts[1]) # 列数
n = int(parts[0]) # 行数
pre_sum = []
count = 0
for _ in range(n):
a = [count]
line = input().strip()
parts = line.split()
numbers = list(map(int, parts))
for i in numbers:
count += i
a.append(count)
pre_sum.append(a)
result = float('inf')
for i in range(1 , n): #非空
#尝试所有的行分割(横向)
sum1 = pre_sum[i-1][m] - pre_sum[0][0]
sum2 = pre_sum[n-1][m] - pre_sum[i][0]
if abs(sum2 - sum1) < result:
result = abs(sum2 - sum1)
for i in range(1,m):
#尝试所有的列分割(竖向)
sum1 = 0
sum2 = 0
for j in range(n):
# 计算当前行的左半部分和右半部分
sum1 += pre_sum[j][i] - pre_sum[j][0]
sum2 += pre_sum[j][m] - pre_sum[j][i]
if abs(sum2 - sum1) < result:
result = abs(sum2 - sum1)
print(result)