数组中相关算法题探讨
前言
- 学习了数组的相关知识后,大家可能发现在题目中不知道如何使用数组的思想,以后每个知识点学习结束后,作者都将结合 Leetcode 中的相关题目来跟大家一起探讨每个知识点在具体题目中的使用。
一、二分查找
- 力扣算法题目第704题:二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
- 示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
- 示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
-
前提:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
1.1 思路分析
- 题目的前提是有序数组,并且强调元素是不重复的,因为一旦有重复元素,那么返回的下标就不唯一了
- 因为数组是可以遍历的,所以最基本的办法是用数组的遍历。
- 由于数组是有序的,并且没有重复元素,那么我们就可以使用二分查找来缩短时间。
1.2 数组遍历的做法
我们对数组进行遍历, 发现是目标元素就返回下标, 找不见就返回-1
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
class Solution(object):
def search(self, nums, target):
"""
:param nums: 传进来的数组
:param target: 要找的目标元素
:return: 找见目标,返回下标, 找不见返回-1
"""
# 遍历数组中的元素
for i in range(len(nums)):
# 如果数组中的元素等于目标元素
if nums[i] == target:
# 返回元素下标
return i
# 找不见返回 -1
return -1
if __name__ == '__main__':
nums = [-1, 0, 3, 5, 9, 12]
solution = Solution()
res = solution.search(nums, 2)
print(res)
1.3 二分查找递归版做法
1.3.1 递归版做法 (只能做到找到元素就返回 True)
时间复杂度 O ( l o g ( n ) ) O(log(n)) O(log(n))
空间复杂度 O ( l o g n ) O(log n) O(logn)
如果按照力扣上给的参数来做的话,由于你创建了新的列表,原始列表的索引在切片后的新列表中不再有效。特别是,当你处理右半部分(nums[mid + 1:])时,你的递归调用没有考虑到这一点,因为它基于原始列表的长度来索引,这在新切片中是不正确的。
class Solution(object):
def search(self, nums, target):
# 定义变量来存储 数组长度
n = len(nums)
# 如果数组为空, 那就返回 -1
if n <= 0:
return -1
# 用变量 mid 来标记中间位置, 这里必须是整除,因为索引都是正整数
mid = n // 2
# 如果 数组的中间位置刚好等于 目标值
if nums[mid] == target:
return mid
# 如果 目标值 小于 数组中间的位置, 那就去数组的左半区 查找
elif target < nums[mid]:
return search(nums[:mid], target)
# 如果 目标值 大于 数组中间的位置, 那就去数组的右半区 查找
else:
return search(nums[mid + 1:], target)
if __name__ == '__main__':
nums = [-1, 0, 3, 5, 9, 12]
solution = Solution()
res = solution.search(nums, 9)
print(res) # 0 因为递归的时候,返回的是新列表,索引会发生变化
1.3.2 递归版做法 (修正版)
- 对应下边的二分查找的 不同区间
1.3.2.1 左闭右闭 区间
时间复杂度 O ( l o g ( n ) ) O(log(n)) O(log(n))
空间复杂度 O ( l o g n ) O(log n) O(logn)
class Solution(object):
def search(self, nums, target, left=0, right=None):
if right is None:
right = len(nums) - 1
# 如果左边界大于右边界,说明目标值不在数组中
if left > right:
return -1
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif target < nums[mid]:
return self.search(nums, target, left, mid - 1)
else:
return self.search(nums, target, mid + 1, right)
if __name__ == '__main__':
nums = [-1, 0, 3, 5, 9, 12]
solution = Solution()
res = solution.search(nums, 9)
print(res) # 4
1.3.2.1 左闭右开 区间
时间复杂度 O ( l o g ( n ) ) O(log(n)) O(log(n))
空间复杂度 O ( l o g n ) O(log n) O(logn)
class Solution(object):
def search(self, nums, target, left=0, right=None):
if right is None:
right = len(nums)
# 如果左边界大于右边界,说明目标值不在数组中
if left >= right:
return -1
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif target < nums[mid]:
return self.search(nums, target, left, mid)
else:
return self.search(nums, target, mid + 1, right)
if __name__ == '__main__':
nums = [-1, 0, 3, 5, 9, 12]
solution = Solution()
res = solution.search(nums, 9)
print(res) # 4
1.4 二分查找的做法
1.4.1 二分查找定义
"""
二分查找(Binary Search),也称为折半查找,是一种在有序数组中查找特定元素的高效算法。
它的基本思想是通过不断地将查找区间分成两半,从而快速定位目标值的位置。
二分查找的时间复杂度为 O(log n),这使得它比线性查找(O(n))快得多,特别是在大数据集上。
二分查找的基本步骤如下:
初始化:设定两个指针,low 和 high,分别指向数组的第一个元素和最后一个元素的位置。
查找中间点:计算中间位置 mid = (low + high) // 2 或者为了避免整数溢出,使用 mid = low + (high - low) // 2。
比较中间元素:如果中间元素正好是要查找的目标值,则返回该元素的索引。
如果中间元素小于目标值,则调整 low 指针为 mid + 1,表示目标值在右半部分。
如果中间元素大于目标值,则调整 high 指针为 mid - 1,表示目标值在左半部分。
重复步骤:重复上述过程,直到 low > high,这时表明目标值不在数组中,返回一个指示未找到的值(通常是 -1)。
"""
二分查找虽然逻辑比较简单,但是涉及到的边界条件,很容易写错
例如 while(left < right) 还是 while(left <= right) ,到底是right = middle 还是right = middle - 1
经常写错就是因为搞不清楚区间的变化与定义
- 二分法的区间定义一般有两种,一种是左闭右闭[left, right] , 另一种是左闭右开 [left, right)。
- 我们在进行边界判断的时候,就是要看 在 区间内合不合法,也就是我们说的在算法实现过程中要遵循 循环不变量的思想,在后续我们还会用到这个思想。
1.4.2 二分查找代码实现(左闭右闭)
区间为左闭右闭[left, right],代码如下:
时间复杂度 O ( l o g ( n ) ) O(log(n)) O(log(n))
空间复杂度 O ( 1 ) O(1) O(1)
class Solution(object):
def search(self, nums, target):
# 定义left指向 索引为 0 的位置(也就是首位)
left = 0
# 定义right 指向数组最后一位
# 也就是设置区间为左闭右闭的区间内 即 [left, right]
right = len(nums)-1
# 这必须设置为 小于等于 因为可能出现left 和 right 同时指向的那个位置刚好就是我们要返回的目标元素
# 因为 [left, right] 是左闭右闭区间 即 [1, 1]是有意义的
# 要使用 <= ,因为left == right是有意义的,所以使用 <=
while left <= right:
# 因为是有序数组, 所以折半查找, 这里要用 // 因为要整数
middle = (left + right) // 2
# 如果 数组中间索引刚好就是目标值,那么就返回中间索引
if nums[middle] == target:
return middle
# 如果 数组的中间值 大于目标值,说明目标值在此时的左半区
elif nums[middle] > target:
# 因为 nums[middle] != target 所以 右边区间要比middle小一位
right = middle - 1
# 如果 数组的中间值 小于 目标值,说明目标值在此时的右半区
else:
# 因为 nums[middle] != target 所以 左边区间要比middle大一位
left = middle + 1
# 如果没找到 就返回 -1
return -1
if __name__ == '__main__':
nums = [-1, 0, 3, 5, 9, 12]
solution = Solution()
res = solution.search(nums, 12)
print(res) # 5
1.4.3 二分查找代码实现(左闭右开)
区间为左闭右开 [left, right) ,代码如下:
时间复杂度 O ( l o g ( n ) ) O(log(n)) O(log(n))
空间复杂度 O ( 1 ) O(1) O(1)
class Solution(object):
def search(self, nums, target):
# 定义left指向 索引为 0 的位置(也就是首位)
left = 0
# 定义right
# 也就是设置区间为左闭右开的区间内 即 [left, right)
right = len(nums)
# 这必须设置为 小于
# 因为 [left, right) 是左闭右开区间 即 [1, 1) 无意义的
# 这里使用 < , 因为left == right在区间[left, right)是没有意义的
while left < right:
# 因为是有序数组, 所以折半查找, 这里要用 // 因为要整数
middle = (left + right) // 2
# 如果 数组中间索引刚好就是目标值,那么就返回中间索引
if nums[middle] == target:
return middle
# 如果 数组的中间值 大于目标值,说明目标值在此时的左半区
elif nums[middle] > target:
# 因为 是 左闭右闭 不会检查最右边的元素, 所以 右边区间 = middle
right = middle
# 如果 数组的中间值 小于 目标值,说明目标值在此时的右半区
else:
# 因为 nums[middle] != target 所以 左边区间要比middle大一位
left = middle + 1
# 如果没找到 就返回 -1
return -1
if __name__ == '__main__':
nums = [-1, 0, 3, 5, 9, 12]
solution = Solution()
res = solution.search(nums, 9)
print(res) # 4
二、移除数组中的元素
- 力扣第27题 : 移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
-
示例 1:
- 输入:nums = [3,2,2,3], val = 3
- 输出:2, nums = [2,2, ,]
- 解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
- 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。 示例 2:
- 输入:nums = [0,1,2,2,3,0,4,2], val = 2
- 输出:5, nums = [0,1,4,0,3, ,,_]
- 解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
- 注意这五个元素可以任意顺序返回。
- 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
2.1 思路分析
- 要注意的是,数组在内存上是 连续存在的, 所以不能单独删除数组中的元素, 只能用后边的元素往前覆盖
2.2 三种解法
2.2.1 一层for循环解法
使用 count变量记录要存放的位置, 遍历数组的时候,发现不是目标元素的时候, 就放在count变量指示的位置, 这样不是目标元素的位置就会被占用
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 初始化一个计数器,用于记录不等于val的元素数量
count = 0
# 第一层循环遍历整个数组
for i in range(len(nums)):
# 如果当前元素不等于val
if nums[i] != val:
# 将当前元素放置在数组的前count个位置
nums[count] = nums[i]
# 增加计数器
count += 1
# 由于 题目中说 留下了什么并不重要, 所以下边这几步可以不做
# 清除多余的部分,使数组看起来像是被截断了一样
# for i in range(count, len(nums)):
# nums[i] = None # 或者使用其他方式标记这些位置
# 返回不等于val的元素的数量
return count
return nums
2.2.2 两层循环解法
使用 两层循环 解决 ,外层循环控制要遍历的数组长度, 内层循环控制 要覆盖的元素下标
时间复杂度 O ( n 2 ) O(n^2) O(n2)
空间复杂度 O ( 1 ) O(1) O(1)
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
i, lenght = 0, len(nums)
while i < lenght:
if nums[i] == val: # 找到等于目标值的节点
for j in range(i+1, lenght): # 移除该元素,并将后面元素向前平移
nums[j - 1] = nums[j]
lenght -= 1
i -= 1
i += 1
return lenght
2.2.3 双指针解法(快慢指针)
双指针就是为了解决 2.2.2 中的双层循环的, 通过一个快指针跟一个慢指针在一个for循环中完成两个循环的工作
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 快慢指针
fast = 0 # 快指针
slow = 0 # 慢指针
size = len(nums)
while fast < size: # 不加等于是因为,a = size 时,nums[a] 会越界
# slow 用来收集不等于 val 的值,如果 fast 对应值不等于 val,则把它与 slow 替换
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
2.2.4 双向指针
双向指针是快慢指针的拓展, 跟快慢指针不一样的的是 一个指向 首位 ,一个指向末尾
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
n = len(nums)
left, right = 0, n - 1
while left <= right:
while left <= right and nums[left] != val:
left += 1
while left <= right and nums[right] == val:
right -= 1
if left < right:
nums[left] = nums[right]
left += 1
right -= 1
return left
实际运行起来 2.2.3 执行时间最快
三、长度最小的子数组
- 力扣算法题目第209题:长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其总和大于等于 target 的长度最小的子数组 [ n u m s l nums_l numsl, n u m s l + 1 nums_{l+1} numsl+1, …, n u m s r − 1 nums_{r-1} numsr−1, n u m s r nums_r numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
- 示例 1:
输入: target = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的子数组。
- 示例 2:
输入: target = 4, nums = [1,4,4]
输出: 1
解释: 2 不存在 nums 中因此返回 -1
- 示例 2:
输入: target = 11, nums = [1,1,1,1,1,1,1,1]
输出: 0
- 前提:
- 1 <= target <= 1 0 9 10^9 109
- 1 <= nums.length <= 1 0 5 10^5 105
- 1 <= nums [ i ] <= 1 0 5 10^5 105
3.1 思路分析
- 我们需要从数组中不断遍历, 寻找求和等于目标值的数组,当他刚好等于目标值的时候,用变量接受此时数组中的元素个数,然后再次进行遍历,不断更新变量中最小数组的元素个数,最后返回这个变量
3.2 两种解法
3.2.1 双层 for 循环
使用双循环解决问题 , 外循环控制从哪个 元素开始进行累加, 内循环控制加多少个
但是这个算法在 力扣 上提交会超时, 在Pycharm中能正常执行
时间复杂度 O ( n 2 ) O(n^2) O(n2)
空间复杂度 O ( 1 ) O(1) O(1)
"""
正无穷大(Python 中表示为 float('inf'))
负无穷大(Python 中表示为 float('-inf'))
"""
class Solution:
def minSubArrayLen(self, target, nums):
# 定义变量用来 表示数组长度
length = len(nums)
# 定义变量来接受最小数组长度,在这的初始值为无穷大
min_len = float('inf')
# 定义外层循环用来控制每次开始累加的第一位
for i in range(length):
# 定义变量来接受 数组的和
sum = 0
# 设置内层循环用来控制加几个数
for j in range(i, length):
# 从 i 位置的数 开始累加
sum = sum + nums[j]
# 如果 累加结果 大于等于 target 的时候,记录此时数组长度
if sum >= target:
# 此时要取最小值
min_len = min(min_len, j - i + 1)
break
# 如果 min_len 有更新的话 那就返回更新的值 如果没更新的话就返回0
return min_len if min_len != float('inf') else 0
if __name__ == '__main__':
# nums = [2, 3, 1, 2, 4, 3]
# target = 7
target = 4
nums = [1, 4, 4]
print(Solution().minSubArrayLen(target, nums))
3.2.2 滑动窗口的思想
滑动窗口可以 看作是 双指针的一种 ,开始的时候,左右指针都指向开始的位置,当程序开始的时候,右指针开始滑动,没扫描一个元素,就统计累加值,当累加值等于或者超过目标值的时候,更新此时的最小子数组的长度,更新完以后,此时的左指针需要向右扫描,直到累加值小于目标值之后,再移动右指针,重复上述动作。
虽然 是循环嵌套循环,但是此时的时间复杂度不是 O ( n 2 ) O(n^2) O(n2) ,因为每个元素被累加的时候扫描一次 和 被剔除出累加数组的时候又被扫描一次,所以时间复杂度是 O ( 2 n ) O(2n) O(2n)
所以下面代码的时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( 1 ) O(1) O(1)
"""
正无穷大(Python 中表示为 float('inf'))
负无穷大(Python 中表示为 float('-inf'))
"""
class Solution:
def minSubArrayLen(self, target, nums):
# 定义变量用来 表示数组长度
length = len(nums)
# 定义变量来接受最小数组长度,在这的初始值为无穷大
min_len = float('inf')
# 定义左指针
left = 0
# 定义右指针
right = 0
# 定义 变量记录累加值
sum = 0
# right 指针先动
while right < length:
# 对元素进行求和
sum = sum + nums[right]
# 当窗口内的元素之和大于等于目标值时,开始收缩窗口
while sum >= target:
# 更新最小数组长度
min_len = min(min_len, right - left + 1)
# 减去左侧元素,尝试缩小窗口
sum = sum - nums[left]
# # 移动左边界,也就是说开始从下一位开始继续进行累加
left += 1
right += 1
return min_len if min_len != float('inf') else 0
if __name__ == '__main__':
# nums = [2, 3, 1, 2, 4, 3]
# target = 7
target = 4
nums = [1, 4, 4]
print(Solution().minSubArrayLen(target, nums))
四、螺旋矩阵 Ⅱ
- 力扣算法题目第 59 题:螺旋矩阵 Ⅱ
给你一个正整数 n n n ,生成一个包含 1 到 n 2 n^2 n2 所有元素,且元素按顺时针顺序螺旋排列的 n n n × n n n 的正方形矩阵 matrix 。
- 示例 1:
输入: n = 3
输出: [ [1,2,3], [8,9,4], [7,6,5] ]
- 示例 2:
输入: 输入:n = 1
输出: [ [1] ]
- 提示:
1 < = n < = 20
4.1 思路分析
-
在这个算法中,循环填充的时候,需要多次判断边界条件,所以我们仍要坚持 循环不变量的 原则。
-
要坚持区间是 左开右闭 或者 左闭右开
-
具体过程为:
- 从左到右填充上行
- 从上到下填充右行
- 从右往左填充下行
- 从下到上填充左行
4.2 解法
在循环中,当 n 为奇数的时候, 我们发现最后循环不到中间位置,此时需要补充中心位置的元素, 每次循环的时候需要控制好边界条件, 做题的时候可以拿n = 3 的时候来带入的模拟边界变化条件
时间复杂度 O ( n 2 ) O(n^2) O(n2)
空间复杂度 O ( n 2 ) O(n^2) O(n2)
class Solution:
def generateMatrix(self, n: int):
# 生成 n × n 的一个列表 格式: [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
matrix = [[0] * n for _ in range(n)]
# 设置起点跟终点
x, y = 0, 0
# 设置循环次数,这里要取整
loop = n // 2
# 如果 n 是 奇数的时候, 需要单独更新中间位置的值
mid = n // 2
# 设置变量用来计数,也就是更新要填充的值,初始值 为1 因为填充要从 1 开始
count = 1
# 不同的 n 需要 转的圈数是不一样的,这里用的是 左闭右开的 区间
for offset in range(1, loop + 1):
# 开始从左往右 填充上行
for i in range(y, n - offset):
matrix[x][i] = count
count += 1
# 从上往下 填充右行
for i in range(x, n - offset):
matrix[i][n-offset] = count
count += 1
# 从右往左填充下行
for i in range(n - offset, y, -1):
matrix[n - offset][i] = count
count += 1
# 从下往上 填充左行
for i in range(n - offset, x, -1):
matrix[i][y] = count
count += 1
# 更新起始点
x += 1
y += 1
# 如果 n 为奇数的时候要自己填充中心点
if n % 2 != 0:
matrix[mid][mid] = count
return matrix
# 测试代码, 当 n = 3 的时候
if __name__ == '__main__':
solution = Solution()
print(solution.generateMatrix(3)) # [[1, 2, 3], [8, 9, 4], [7, 6, 5]]
总结
- 以上就是力扣中有关数组的题目的解题思路跟代码,我只是列举出来 我能想到的几种办法,如有其他解法,可以后台私信我。