数组篇
目录
数组基础知识
- 数组是存放在连续内存空间上的相同类型数据的集合。(二维也是连续存储空间);
- 数组的元素是不能删的,只能覆盖
- 优缺点:
优:查、改元素很容易 时间复杂度O(1)
缺:插入、删除元素需要移动大量元素 时间复杂度O(n);
大小固定,只能存储一种类型的数据;
基本题
1-704 二分查找
题目
分析
有序数组(以升序为例),将中间位置middle的值与target比较,若相等则直接return;若小于target,则说明middle以左包括middle位置的所有值均比target小,只要研究右边部分,改变搜索的左边界,继续执行操作;同理,若大于target,则仅需研究左半,改变搜索的右边界;
易错点
边界的修改
是 while(left < right)
还是 while(left <= right)
,到底是right = middle
呢,还是要right = middle - 1
while内条件要满足使得区间有意义
代码
# 左闭右开[)
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)
while left < right: # 区间合法 则一直进行
mid = left + (right - left)//2
# 不用(left + right)//2 是为了防止两个32位的整数相加溢出
# //2是为了向下取整 防止小数 和c++中不同 int/int还是int
if nums[mid]>target:
right = mid # 新的搜索区间为[left,mid-1] 由于右开 可直接令right=mid
elif nums[mid]<target:
left = mid+1
else:
return mid
return -1
# 左闭右闭[]
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)-1
while left<=right:
mid = left+(right-left)//2
if nums[mid]<target:
left = mid+1
elif nums[mid]>target:
right = mid-1
else:
return mid
return -1
小结
二分法适合范围:升序数组
while 条件;左右区间更改;mid更新防溢出
二分法时间复杂度O(logN)
33. 搜索旋转排序数组(二分法进阶)
class Solution:
def search(self, nums: List[int], target: int) -> int:
l = 0
r = len(nums)-1
while l<=r:
mid = l + (r-l)//2
if nums[mid] == target:
return mid
if nums[0]<= nums[mid]: # 说明mid落在左半 0-mid一定都是有序的
if nums[0]<=target<nums[mid]: # 就转化为有序数组nums[0]-nums[mid]二分法找目标值
r = mid -1
else: # 落在右半 无序
l = mid + 1
else: # 说明mid落在右半 mid-末尾一定是有序的
if nums[mid] < target <= nums[len(nums)-1]:
l = mid+ 1
else:
r = mid -1
return -1 # nums中不存在目标值,返回-1
34. 在排序数组中查找元素的第一个和最后一个位置(二分法进阶)
思路:两次使用二分法分别找到左边界和右边界
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
# 使用两次二分法找到左右边界
def searchBound(nums, target, lor):
res = -1
l = 0
r = len(nums) - 1
while l <= r:
mid = l + (r - l) // 2
if nums[mid] < target:
l = mid + 1
elif nums[mid] > target:
r = mid - 1
else:
res = mid # 记录当前边界
if lor == 0: # 并继续更新边界
r = mid - 1
else:
l = mid + 1
return res
left = searchBound(nums, target, 0) # 左边界
right = searchBound(nums, target, 1) # 右边界
return [left, right]
4. 寻找两个正序数组的中位数
k = m + n
k为奇:找第(k+1)//2个数
k为偶:找第k//2和第k//2+ 1个数
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
def getKthElement(k): # 找到第k小的元素
index1, index2 = 0, 0
while True:
if index1 == n1: # 越过nums1
return nums2[index2 + k-1]
if index2 == n2:
return nums1[index1 + k-1]
if k == 1: # 终止条件
return min(nums1[index1], nums2[index2])
newIndex1 = min(index1 + k//2-1, n1-1)
newIndex2 = min(index2 + k//2-1, n2-1)
if nums1[newIndex1]<=nums2[newIndex2]:
k = k - (newIndex1-index1+1)
index1 = newIndex1 + 1
else:
k = k - (newIndex2-index2+1)
index2 = newIndex2 + 1
n1 = len(nums1)
n2 = len(nums2)
m = n1+n2
if m%2==1: # 奇数
return getKthElement((m+1)//2)
else:
return (getKthElement(m//2) + getKthElement(m//2+1))/2
1-27 移除元素
题目
分析
由于vector内存的连续性,是不能直接对某个元素直接删除的,而是需要用其他元素对其进行覆盖,表现出删除的效果。
本题本质是实现vector的erase操作,注意erase也是对数据进行覆盖实现的,一次erase操作的时间复杂度为O(n)
方法
法1 暴力解
通过两个for循环实现:
第一个for循环对数组遍历
当第一个for循环遇到与val相等的值时,使用第二个for循环将后面的数组前移一位
注意:前移后需从第一层循环的当前位置开始遍历,而不是后一个,放出几个val相连的情况出现
法2 快慢指针(同起点、首尾双起点)
使用双指针要明确指针的含义
快:找到新数组需要的元素(即不等于val的元素)
慢:新数组索引
代码
# 暴力解法
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
size = len(nums)
i = 0
while i < size: # size随着前移在变化
if nums[i] == val:
for j in range(i+1,size):
nums[j-1] = nums[j]
i -= 1
size -= 1
i += 1
return i
# 双指针(头开始) ---好处:不改变原数组的元素顺序
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
n = len(nums)
p1 = 0 # 新数组索引
p2 = 0 # 找到新数组中的元素
while p2<n:
if nums[p2] != val:
nums[p1] = nums[p2]
p1 += 1
p2 += 1
return p1
# 双指针(首尾)
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
n = len(nums)
left = 0
right = n-1
while left<=right:
while left<=right and nums[left] != val:
left += 1 # 右指针所处的位置不等于val
while left<=right and nums[right] == val:
right -= 1 # 左指针所处的位置等于val
if left < right: # 经过上面两个while left和right不可能在同一位置
nums[left] = nums[right]
left += 1
right -= 1
return left
易错点
使用暴力解法时循环的索引位的变化
小结
双指针(头开始)——不改变原数组的元素顺序
双指针(首尾)——两指针合起来只遍历了数组一次。与方法二不同的是,两端开始避免了需要保留的元素的重复赋值操作。
283. 移动零
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
j = 0
for i in range(n):
if j == n:
nums[i:]=[0]*(n-i)
break
while j+1<n and nums[j]==0:
j += 1 # j指向不为0的元素
nums[i] = nums[j]
j += 1
977 有序数组的平方
题目
分析
特点: 已排序好的数组,考虑的问题就是负数平方之后可能成为最大数。
数组平方的最大值就在数组的两端,不是最左边就是最右边,由两边向中间减小。
此时可以考虑双指针法了,左指向起始位置,右指向终止位置。哪个指针对应的元素大就把这个值赋给新数组,指针移动。
方法
法1 暴力法
每个数平方之后,排个序,时:O(nlogn) 空:O(1)
法2 双指针法
时:O(n) 空:O(n)
代码
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
n = len(nums)
for i in range(n):
nums[i] = nums[i]**2 # 平方的三种写法 num*num num**2 math.pow(num,2)
return sorted(nums)
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
n = len(nums)
nums2 = [None]*n # 初始化数组大小
left = 0
right = n-1
for i in range(n-1,-1,-1):
lm = nums[left]**2
rm = nums[right]**2
if lm < rm: # 右指针对应的元素更大
nums2[i] = rm
right -= 1
else:
nums2[i] = lm
left += 1
return nums2
代码小点
1、平方的三种写法:num*num; num**2; math.pow(num,2)
2、数组初始化:
若数组是追加一个元素的可以不用定义长度;a=[]; a.append(i)
若初始化一个列表然后要修改其中的值的话,就要定义长度了。b = [None]*size
448. 找到所有数组中消失的数字
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
# 遍历nums 找到数组中nums[i]-1的元素,将其值取为负的 负的表示这个索引对应的元素+1 出现过(鸽笼)
n = len(nums)
ans = []
for i in range(n):
nums[abs(nums[i])-1] = -abs(nums[abs(nums[i])-1])
for i in range(n):
if nums[i]>0:
ans.append(i+1)
return ans
209 长度最小的子数组
题目
分析
要找到一段区间,需要起始和终止位置,由于也要考虑区间内部,称为滑动窗口法
关键:大于目标值缩小窗口,小于目标值扩大窗口。滑动窗口for循环中是窗口终止
方法
滑动窗口法 时:O(n)
代码
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
# 滑动窗口
n = len(nums)
j = 0 # 窗口起始
res = n+1 # 返回的窗口大小
s = 0 # 窗口内数据和
for i in range(n): # 窗口终止右移 窗口变大
s = s + nums[i]
while s >= target:
lenth = i-j+1 # 满足要求的长度
res = min(lenth,res)
s = s - nums[j] # 减去窗口起始处的值
j += 1 # 窗口起始右移 窗口缩小
if res <= n:
return res
else:
return 0
59 螺旋矩阵II
题目
分析
就是若干圈循环,共有n//2圈,奇数圈最中间的结点单独赋值;
每一圈最好保证循环/遍历条件同,否则会加大分析难度
以3*3为例,最外面一圈,每n-1个一次循环;下一圈每n-3个一循环,所以每圈结束后要有个偏移量offset。
综上:外循环(圈数);内循环(行列四个方向对矩阵元素赋值)
注意for i in range(3),i最终为2;while i<3,最终i=3 有差别
代码
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
a = [[None]*n for _ in range(n)] # n*n矩阵
offset = 1
i = 0
count = 1
for cir in range(n//2): # 注意到每圈的起始行列位置相等
for j in range(cir,n-offset): # 左往右
a[i][j] = count
count += 1
for i in range(cir,n-offset): # 上往下
a[i][j+1] = count
count += 1
for j in range(n-offset,cir,-1): # 右往左
a[i+1][j] = count
count += 1
for i in range(n-offset,cir,-1): # 下往上
a[i][j-1] = count
count +=1
offset += 1
if n %2 ==1: # n时奇数 对中间的最后一个赋值
a[n//2][n//2] = n**2
return a
*** 代码易错处:预设矩阵——[[]*n for _ in range(n)](×)
*** cir 本身也可以作为偏移量
238. 除自身以外数组的乘积
思路:两个数组分别对应当前位置的前缀积和后缀积,则res[i]等于前缀积和后缀积的乘积
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
ans = [0]*n
# prefix product 前缀积
prepd = [1]*n
# suffix product 后缀积
supd = [1]*n
for i in range(1, n):
prepd[i] = prepd[i-1]*nums[i-1]
supd[n-1-i] = supd[n-i]*nums[n-i]
for i in range(n):
ans[i] = prepd[i]*supd[i] # i位置的结果等于前缀积和后缀积的乘积
return ans