2-2 数组相关题目


版权声明:以下题目均来自 LeetCode, 仅仅提供跳转到力扣官网的链接,不在本页面出现题目内容,本文章内容禁止商业用途。

目录

2.2.1 一维数组的相关题目

2.2.1.1 轮转数组

LeetCode 189.轮转数组 | | 返回2.2.1目录

方案一: 复制一个数组为参照,在原数组上进行修改。
'''时间复杂度O(n), 空间复杂度O(n)'''
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
tmp, n = nums.copy(), len(nums)
for i in range(n):
nums[(i+k)%n] = tmp[i]
方案二: 不另外创建数组。
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
N = len(nums)
if N == 1:
return
# 题目给的例子中,有k>N的情况,所以先求模运算
k = k % N
if k == 0:
return

# 如果假设数组首尾相接,是一个循环数组
# 那么向右轮转k个位置,就相当于后面k个数被挤压到前面去了
# 前面的 N-k 个数, 被挤压到后面全了
# 所以就相当于 前 N-k 个数,和后 k个数,整体区域做一下交换
# 那么如果我们直接从 第 N-k个数字后面的逗号,将数组旋转180度,他们的区域就交换了
nums[:] = nums[::-1] # 反转数组
# 注意!如果写成 nums = nums[::-1],原数组num的值是不会被改变的,这种写法相当于创建了一个新的临时数组。

# 但是反转数组后,各区域内的顺序也被反转了一遍,我们再将各区域的顺序调回来
# 将转过来的前k个数字恢复原来的顺序(注意前k个数字序号是 0~k-1)
nums[:k] = nums[k-1::-1]
# 将转过来的后的N-K个数字恢复原顺序(注意后N-K个数字序号是 k~N-1)
nums[k:] = nums[N-1:k-1:-1]
'''
但是问题是,python在进行切片操作的时候,
实际上是会在等式右边产生一个新的临时list,
然后将值赋给等式左边。也并不见得能省多少空间.
还有的解法是在反转数组的时候,自己写反转函数:

def reverse_list(nums, start, end):
while start < end:
nums[start], nums[end] = nums[end], nums[start]
start +=1
end -=1

reverse_list(nums, 0, N-1)
reverse_list(nums, 0, k-1)
reverse_list(nums, k, N-1)

这种的空间复杂度比起直接使用链表切片来说,理论上会少一些吧,反正leetcode的提交结果来看,
使用python2的话,空间消耗确实有减少,但是时间消耗一下就增加了
使用python3的话,空间消耗是真没有多少区别,时间消耗也是增加了。
总得来说,用切片就是快
'''

2.2.1.2 寻找数组的中心下标

LeetCode 724.寻找数组的中心下标 | | 返回2.2.1目录

方案:最直接的思路就是遍历元素的时候,每一次都计算左侧的和 sum(nums[:i]) 和 右侧的和 sum(nums[i+1:]);但是这中间包含了大量重复计算,时间复杂度会很高。所以可以维护两个状态变量,分别记录左和右的累加和,每一次遍历的时候,直接修改状态变量即可。
class Solution:
def pivotIndex(self, nums: List[int]) -> int:
N = len(nums)
# 把左右结果放入中间变量缓存,就不用每次都调用sum了
left, right = 0, sum(nums)
for i in range(N):
cur_val = nums[i]
if left == right - cur_val:
return i
else:
left += cur_val
right -= cur_val
return -1

2.2.1.3 最大连续 1 的个数

LeetCode 485.最大连续 1 的个数 | | 返回2.2.1目录

方案:这种求连续间隔的题,一般都是用索引相减来做。即右边界的索引,减去左边界的索引,就是这段区间的宽度。
class Solution:
def findMaxConsecutiveOnes(self, nums: List[int]) -> int:
# index_0 用来记录上一次0出现的位置,作为连续1区间的左边界
index_0 = -1
N, gap = len(nums), 0
# 在原始数组后面多加一个0,表示区域的最大右边界
nums.append(0)
for i in range(N+1):
if nums[i] == 0:
# 当遇到为0的时候,取该次i作为右边界,计算两个0之间的间隔
gap = max(gap, i-index_0-1)
# 更新完gap之后也要把index0更新为当前的0的索引。
index_0 = i
return gap

'''
当然也可以使用1来作为计算gap的判断条件,原理是一样的
index_0 = -1
N, gap = len(nums), 0
for i in range(N):
if nums[i] == 1:
gap = max(gap, i-index_0)
else:
index_0 = i
return gap
'''

2.2.1.4 除自身以外数组的乘积

LeetCode 238.除自身以外数组的乘积 | | 返回2.2.1目录

方案:最简单的做法就是先求所有元素的积,然后每遍历一个位置就除以该位置的值。但是题目规定不能使用除法!这也是为了避免出现0元素作为除数的情况。那么参考【2.2.2 寻找数组的中心下标】,我们可以维护两个状态,对于每一个位置,存储该数左侧全部元素的积,和右侧全部元素的积。由于不能使用除法,只好将这两个状态分别存入两个数组中,反正题目也没有要求空间复杂度。 最终用了2次for循环,2个额外数组。
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
N = len(nums)
l, r = [1]*N, [1]*N
res = []

# 原始数据从下表1开始遍历,因为0位置上的左侧积默认是1
for i in range(1, N):
# 对于i位置的左侧积,就是 i-1位置的左侧积,乘以 i-1 位置的值
l[i] = l[i-1]*nums[i-1]
i2 = N-i
# 同时倒着取索引,计算右侧积
r[i2-1] = r[i2]*nums[i2]

return [l[i]*r[i] for i in range(N)]

方案二:此题确实有空间复杂度更低的方法。因为输出结果本来就是一个数组,是不占额外空间复杂度的,可以利用该结果数组作为中间辅助数组。

  • 即第一轮从左往右遍历的时候,先把每个位置的左侧积暂存到结果数组res中;
  • 第二次从右往左遍历,这个时候只需要用一个变量来缓存右侧积即可,将每个位置的右侧积,乘以已经缓存在res数组中的左侧积,就可得到最终结果了。
最终还是只使用了2次for循环,但是没有额外用两个辅助数组,只用了一个额外变量,空间复杂度成了O(1)
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
N = len(nums)

#l,r = [1]*N, [1]*N
# 不使用左右缓存数组,而是直接用结果数组来缓存
res = [1] * N
for i in range(1,N):
# 照常计算左侧积
res[i] = res[i-1]*nums[i-1]

# 第二次从右往左遍历,初始化右侧积 R_mul 为1
R_mul = 1
for j in range(N-1,-1, -1):
# j 位置结果 = 缓存的 j 位置左侧积 * 右侧积
res[j] = res[j]*R_mul
# 更新下一位置的右侧积
R_mul *= nums[j]

return res

2.2.2 二维数组相关题目

2.2.2.1 旋转图像

LeetCode 48.旋转图像 | | 返回2.2.2目录

方案:顺时针旋转 90°,行变成了列,列变成了行。 原来是第 _i_ 行, 现在就是 倒数 第 _i_ 列 (_N_-1-_i_);原来是第 _j_ 列, 现在到了第 _j_ 行。 对于原有的一个元素 _M[i][j]_, 旋转后出现在 倒数 第 _i_ 列的第 _j_ 行位置:_M[j][N-1-i]_ 对应四个位置上的元素的变化: _M[N-1-j][i] --> M[i][j] --> M[j][N-1-i] --> M[N-1-i][N-1-j]_
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
# 转换关系 M[N-1-j][i] --> M[i][j] --> M[j][N-1-i] --> M[N-1-i][N-1-j]

# 可以发现元素互换都是在自己所在的那个圈层进行交换
# 每处理完最外一圈,可以视为(待处理)矩阵缩小了一圈
# 所以我们可以从外到内去处理,一共有 K圈, K = N//2
# K=0时,圈层左上角是(0,0), K=1时,圈层左上角是(1,1), ...以此类推
N = len(matrix)
K = N // 2
for i in range(K):
# 对于每一个圈层,如果一次换4个对应位置的元素
# 那么只要把该圈层的首行的每个元素都进行一次【4位置】交换
# 该圈层就完成了交换了

# 这里一定要注意,对于k圈层的首行,左起列是 k,右侧截止是 N-1-k
# 但是最右侧那一列可以不用管,因为最后那一列的元素就是首行填充过去的
# 所以右侧只用取到 N-1-k -1即可
# 比如k=0时,首行如果是 1,2,3;则只需要移动元素 1及其对应4个位置的,和2及其4个对应位置的;
# 不用考虑 3及其4个对应位置的数,因为3已经在元素1的4个对应位置当中处理了
for j in range(i, N-1-i):
matrix[i][j], matrix[j][N-1-i], matrix[N-1-i][N-1-j], matrix[N-1-j][i] = \
matrix[N-1-j][i], matrix[i][j], matrix[j][N-1-i], matrix[N-1-i][N-1-j]


2.2.2.2 对角线遍历

LeetCode 498.对角线遍历 | | 返回2.2.2目录

方案:这种矩阵遍历的问题,最关键的点就在于,如果考虑好边界条件,达到某一个边界条件之后,换方向.
class Solution:
def findDiagonalOrder(self, mat: List[List[int]]) -> List[int]:
m,n = len(mat), len(mat[0])
res = []
left, top, right, bottom = 0, 0, n-1, m-1
# i,j表示矩阵的i 行 j 列, k表示矩阵已经遍历了多少个元素
i, j, k = 0, 0, 0
while k < m*n:
# 朝着右上遍历
while i >= top and j <= right and k < m*n:
# 当 i 未超过上边界, j 未超过右边界, k 未超过元素总数
res.append(mat[i][j])
i -= 1 # 往上移动, 故 i 减小
j += 1 # 往右移动, 故 j 增大
k += 1 # 已经遍历过一个元素
# 当跳出了这个while循环时:
# 1.如果是 k < m*n 不满足, 可以不用管
# 2.如果是 i >= top 不满足, 说明上方出界, i要往下回来一行才能继续遍历
# 3.如果是 j <= right 不满足, 说明右侧出界, j要往左侧回来一列才能继续遍历;
# 同时, 出右界, 说明刚刚那一行已经被遍历完了!!所以i即使没有越上界,依然要向下, 而且是向下两行!
# 比如如果是 3行2列 的矩阵,(行多列少,j一定先出界) 就会出现这种情况
# 如果是2.3都出现不满足, 说明刚刚遍历过的位置是右上角, 和情况3一样
# 总结起来, 伪代码 就为 :
# i = i + 1 (j没有越右界) or i + 2(j越了右界)
# j = j (j没有越右界) or j - 1 (j越了右界)
i, j = (i+1, j) if (j <=right) else (i+2, j-1)

# 朝着左下遍历
while i <= bottom and j >= left and k<m*n:
res.append(mat[i][j])
i += 1 # 往下移动, 故 i 增大
j -= 1 # 往左移动, 故 j 减小
k += 1 # 已经遍历过一个元素
# 分析同上(可以用3行4列的矩阵帮助思考,列多行少,i一定先出界), 此处不赘述, 伪代码 就为 :
# j = j + 1 (i没有越下界) or j + 2(i越了下界)
# i = i (i没有越下界) or i - 1 (i越了下界)
j, i = (j+1, i) if (i<=bottom) else (j+2, i-1)

return res

2.2.2.3 螺旋矩阵

LeetCode 54. 螺旋矩阵 | | 返回2.2.2目录

方案:设立四个边界值,遍历的指针碰到边界值之后,就停下来,换方向,同时更新边界值。
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
res = []
m, n = len(matrix), len(matrix[0])
i, j, k = 0, 0, 0
left, up, right, down = 0, 0, n-1, m-1
while 1:
for j in range(left, right+1):
res.append(matrix[i][j])
k+=1
if k >= m*n : break
up += 1

for i in range(up, down+1):
res.append(matrix[i][j])
k+=1
if k >= m*n : break
right -=1

for j in range(right, left-1, -1):
res.append(matrix[i][j])
k+=1
if k >= m*n : break
down -=1

for i in range(down, up-1, -1):
res.append(matrix[i][j])
k+=1
if k >= m*n : break
left +=1
return res

2.2.2.4 螺旋矩阵II

LeetCode 59. 螺旋矩阵II | | 返回2.2.2.2目录

方案:上一题中的遍历顺序搞懂了之后,这个题思路很简单,代码也差不多。
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
'''
# 创建一个 N*N的矩阵
# res = n*[n*[0]]
# 注意,这么写是错的,这样写的后果就是里面的每行都是来源于同一行 的copy,
# 会共享存储空间,属于python的浅拷贝!

# 应该这么写:
# res = []
# for i in range(n):
# res.append([0]*n)
# 即每一行都是新创建一个 [0]*n的行,然后添加进去
'''
# 简化为如下写法:
res = [ [0]*n for _ in range(n)]

i, j, k = 0, 0, 1
left, up, right, down = 0, 0, n-1, n-1
while True:
for j in range(left, right+1):
res[i][j]=k
k+=1
if k > n*n: break
up +=1


for i in range(up, down+1):
res[i][j]=k
k+=1
if k > n*n: break
right -=1


for j in range(right, left-1, -1):
res[i][j]=k
k+=1
if k > n*n: break
down -=1


for i in range(down, up-1, -1):
res[i][j]=k
k+=1
if k > n*n: break
left +=1

return res

2.2.2.5 矩阵置零

LeetCode 73. 矩阵置零 | | 返回2.2.2目录

方案:我们需要知道原矩阵中哪些位置为0,如果在遍历的同时修改,那么原来不为0的元素可能会被置为0,会影响后面的元素的判断。比如如果左上角是0,在遍历的时候同时修改原数组,那么第一行和第一列的元素会全部变为0;会导致后面所有元素全部为0. 所以我们只能先遍历矩阵,然后找个地方把为0的位置先记住,然后再第二次遍历的时候进行修改。
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
# 如果在遍历的时候就修改,那么前面如果出现了0,修改完之后,后面很可能都被改为0
# 所以先记住这些为0的行/列索引
# 再改

rows, cols = [], []
M, N = len(matrix), len(matrix[0])
for i in range(M):
for j in range(N):
if matrix[i][j] == 0:
if not i in rows:
rows.append(i)
if not j in cols:
cols.append(j)

'''注意,python的二维list不支持以下写法:
for i in rows:
matrix[i][:] =0
for j in cols:
matrix[:][j] = 0
这是 numpy array 才支持的写法
'''
for i in range(M):
for j in range(N):
if i in rows or j in cols:
matrix[i][j] =0
'''思路2.直接用第一列来记录哪一行出现过0,用第一行来记录哪一列出现过0;
后续的元素出现0时,虽然第一行和第一列被修改了,但是第一行第一列对应的位置,按照规则本来也会被改成0.
不过要提前记录第一行和第一列是否有0元素,有的话,最后再把他们全部变为0;没有的话就不用管'''
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
m, n = len(matrix), len(matrix[0])
flag_col0 = any(matrix[i][0] == 0 for i in range(m))
flag_row0 = any(matrix[0][j] == 0 for j in range(n))

# 先用第一行和第一列来记录出现0的列和行
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[i][0] = matrix[0][j] = 0

# 根据第一行和第一列的记录,来修改原始矩阵
for i in range(1, m):
for j in range(1, n):
if matrix[i][0] == 0 or matrix[0][j] == 0:
matrix[i][j] = 0

# 再来处理第一列
if flag_col0:
for i in range(m):
matrix[i][0] = 0

# 再来处理第一行
if flag_row0:
for j in range(n):
matrix[0][j] = 0

2.2.2.6 生命游戏

LeetCode 289. 生命游戏 | | 返回2.2.2目录

方案:规则看起来很唬人,很多,但是就是对于每一个元素判断,判断其3*3窗口内的值,然后根据该值去改变当前元素的值,的这么一个条件判断语句。
另外,由于是同时发生改变,所以不能让变化后的值,影响到后面的元素的判断,所以也要先找一个地方,记录状态,和上题一样,只不过这里直接拷贝一个矩阵,拿他来记录原始状态

class Solution:
def gameOfLife(self, board: List[List[int]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
# 因为是同时发生的,所以我们不能在原有矩阵上直接修改
# 而是参照原有矩阵的值,对一个新的矩阵的值进行判定
# 二维数组的 深度拷贝的方式有以下几种,不能直接用 = 号去拷贝,那样是python的浅拷贝,会共享存储区域
import copy
copy_board = copy.deepcopy(board)
# copy_board = [copy.deepcopy(row) for row in board]
# copy_board = [[board[i][j] for j in range(n)] for i in range(m)]
m, n = len(board), len(board[0])

for i in range(m):
for j in range(n):
# left: max(0, j-1), right: min(n, j+1+1), top: max(0, i-1), bottom: min(m, i+1+1)
if copy_board[i][j] == 1:
# 如果原始状态是1,进入下面的规则
# 由于我们要算周围8个格子的和
# 先将初始值置为-1,这样就直接考虑9宫格的和(相当于减去当前元素1)
area_sum = -1
# 然后就是如何遍历以当前元素为中心的9宫格的问题了
# 理想情况下,i的遍历范围[i-1, i+1 +1), j的遍历范围 [j-1, j+1 +1)
# 但是要考虑格子本身就在矩阵边界的情况,所以:
# 上边界最小只能到 0: 取max(0, i-1); 左边界最小只能到 0: 取max(0, j-1)
# 下边界最大只能到 m: 取min(m, i+1 +1); 右边界最大只能到 n:取min(n, j+1 +1);
for p in range(max(0,i-1), min(m,i+1 +1)):
for q in range(max(0,j-1), min(n, j+1 +1)):
area_sum += copy_board[p][q]
if area_sum < 2:
board[i][j] = 0
elif area_sum > 3:
board[i][j] = 0
else:
pass
else:
# 如果原始状态是0,进入下面的规则
area_sum = 0
for p in range(max(i-1,0), min(m,i+1+1)):
for q in range(max(0,j-1), min(n, j+1+1)):
area_sum += copy_board[p][q]
if area_sum == 3:
board[i][j] = 1
else:
pass