三、数组————相关算法题探讨(持续更新中)



前言

  • 学习了数组的相关知识后,大家可能发现在题目中不知道如何使用数组的思想,以后每个知识点学习结束后,作者都将结合 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} numsr1 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]]

总结

  • 以上就是力扣中有关数组的题目的解题思路跟代码,我只是列举出来 我能想到的几种办法,如有其他解法,可以后台私信我。
  • 38
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值