算法刷题之三一维列表

1|0列表


列表和字符串是算法中最常见的数据结构,在列表中又分为一维列表和二维列表。一维列表的数据结构即使很简单也会有很多复杂的算法思想。

一维列表

  1. 删除排序数组中的重复项
  2. 移动非0元素到头部
  3. 盛最多水的容器
  4. 三数之和
  5. 长度最小的子数组
  6. 无重复字符的最长子串
  7. 前缀和
  8. 合并区间

二维列表

  1. 二维矩阵最大子矩阵和
  2. 旋转图像
  3. 杨辉三角
  4. 对角线遍历
  5. 矩阵元素查找
  6. 容器盛水

2|0删除排序数组中的重复项


题目:
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2gy9m/

方法:只要将不同的数字全部集中到前面就可以了。设置双指针,慢指针用来指向列表前部分里不重复的下标,快指针向后遍历找不重复的数字。找到一个就让慢指针加1,然后将快指针指向的不重复的值赋值给慢指针。
双指针:一种常用的,好用的数据处理方式。在快速排序中,将一个数组分成大小两个部分,用的就是双指针。

class Solution: def removeDuplicates(self, nums: List[int]) -> int: #时间复杂度为O(n),空间O(1) # 慢指针 i = 1 # 快指针 for j in range(1, len(nums)): if nums[j] != nums[j - 1]: nums[i] = nums[j] i += 1 nums[:] = nums[:i] return

地道的写法:

class Solution: def removeDuplicates(self, nums: List[int]) -> int: if not nums: return 0 i = 0 for j in range(1, len(nums)): if nums[i] != nums[j]: i += 1 nums[i] = nums[j] return i + 1

3|0移动非0元素到头部


题目:
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。

链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/x2ba4i/
方法:将0全部集中到列表尾部,换一个思路就是将所有非0保持顺序,移动到列表前部。和上一题思想类似,使用双指针,慢指针指向0的下标,快指针向后寻找数值不为0的元素,交换快慢指针的数值,将所有0全部移动到尾部

class Solution: def moveZeroes(self, nums: List[int]) -> None: """ Do not return anything, modify nums in-place instead. """ # 慢指针 i = 0 # 快指针 j = 0 while j < len(nums): # 不等于快慢指针交换,慢指针遇到0时会停下来,等待快指针指向非0时的交换 if nums[j] != 0: nums[i],nums[j] = nums[j],nums[i] i += 1 j += 1

思想:j遇到0元素不管,i遇到0元素停下。当j再遇到0元素时,和i的元素互相交换,最终完成将非零元素向前移动的目的

def func(arr): i = 0 length = len(arr) for j in range(len(arr)): if arr[j] != 0: arr[i],arr[j] = arr[j],arr[i] i = i + 1 arr = [0,1,0,3,12] func(arr) print(arr)

4|0盛最多水的容器


提示:
给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器。
链接:https://leetcode-cn.com/problems/container-with-most-water

输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

方法:
首先想到的是暴力解法,两层循环能够找到任意两个柱子之间的距离,乘以高度,取最大值即可。时间复杂度为O(2),
然后使用双指针也能解决这个问题。以左右两边为边界向中间进发,比较两个柱子中较小的一个,并向中间移动,一直到两个柱子相遇。在这个过程中能够找到面积最大的那一对。

class Solution: def maxArea(self, height: List[int]) -> int: if not height: return 0 n = len(height) max_ares = 0 l = 0 r = n - 1 while l < r: max_ares = max(max_ares, min(height[l], height[r]) * (r-l)) if height[l] < height[r]: l += 1 else: r -= 1 return max_ares

关于这一题的详细解法:
https://leetcode-cn.com/problems/container-with-most-water/solution/on-shuang-zhi-zhen-jie-fa-li-jie-zheng-que-xing-tu/

5|0三数之和


题目:
给一个数组[4,2,6,7,4,6],找到其中三个数的和为14的所有组合
方法
三数之和是使用双指针完成遍历,固定住第一个,然后使用双指针一头一尾向中间逼近
因为我们要同时找三个数,所以采取固定一个数,同时用双指针来查找另外两个数的方式。所以初始化时,我们选择固定第一个元素(当然,这一轮走完了,这个蓝框框我们就要也往前移动),同时将下一个元素和末尾元素分别设上 left 和 right 指针。画出图来就是下面这个样子:

当三个数的和大于0时,表明right需要左移以减少和,当小于0时,left需要右移,以增大和。
知识点: 双指针配合循环

class Solution: def threeSum(self, nums: List[int]) -> List[List[int]]: length = len(nums) arr = [] nums.sort() for i in range(length - 2): if nums[i] > 0: continue if i > 0 and nums[i] == nums[i-1]: continue L = i + 1 R = length - 1 while L < R: s = nums[i] + nums[L] + nums[R] if s == 0: arr.append([nums[i],nums[L],nums[R]]) L += 1 R -= 1 while L < R and nums[L] == nums[L-1]:L += 1 while L < R and nums[R] == nums[R+1]:R -= 1 elif s > 0: R -= 1 while L < R and nums[R] == nums[R+1]:R -= 1 else: L += 1 while L < R and nums[L] == nums[L-1]:L += 1 return arr

双指针小结
以上题目能力体现双指针的使用技巧,通常使用双指针,可以用在如下场景中:

  1. 从两端向中间迭代数组。

这时你可以使用双指针技巧:一个指针从头部开始,而另一个指针从尾部开始。这种技巧经常在排序数组中使用。

  1. 同时有一个慢指针和一个快指针。

确定两个指针的移动策略。与前一个场景类似,你有时可能需要在使用双指针技巧之前对数组进行排序,也可能需要运用贪心法则来决定你的运动策略。

6|0长度最小的子数组


题目:
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

方法:使用滑动窗户能够完美解决这个问题。窗口右端加入元素,判断窗口中元素总和是否大于target,如果大于计算长度,然后再左端缩小窗口,元素总和小于target,这是右端会继续向下滑动。
知识点:变长滑动窗口

class Solution: def minSubArrayLen(self, s: int, nums: List[int]) -> int: left = 0 res = float("inf") cur = 0 for right in range(len(nums)): cur += nums[right] while cur >= s: print('cur:%d -left: %d' % (cur,nums[left])) res = min(res, right-left+1) cur = cur - nums[left] left += 1 if res != float("inf"): return res else: return 0

7|0无重复字符的最长子串


题目:
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

方法:判断一个子数组中是否有重复有很多种办法:

  1. 使用暴力双层遍历
  2. 使用字典,判断某元素是否在字典中
  3. 使用数组,判断元素是否在数组中

方法:使用滑动窗解决:窗口左侧不断加入元素,判断元素是否存在于窗口中,不在则继续加入,在则将重复的元素去掉,继续添加。在这个过程中会有一个最大值出现。
知识点: 变长滑动窗口

class Solution(object): def lengthOfLongestSubstring(self, s: str) -> int: # 字符串为空则返回零 if not s: return 0 window = [] # 滑动窗口数组 max_length = 0 # 最长串长度 # 遍历字符串 for c in s: # 如果字符不在滑动窗口中,则直接扩展窗口 if c not in window: # 使用当前字符扩展窗口 window.append(c) # 如果字符在滑动窗口中,则 # 1. 从窗口中移除重复字符及之前的字符串部分 # 2. 再扩展窗口 else: # 从窗口中移除重复字符及之前的字符串部分,新字符串即为无重复字符的字符串 window[:] = window[window.index(c) + 1:] # 扩展窗口 window.append(c) # 更新最大长度 max_length = max(len(window), max_length) return max_length if max_length != 0 else len(s)

这一道题目还有更精彩的解法,滑动窗口不是只能用列表实现,还可以用字符串,双指针,字典等实现。
https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/python-hua-dong-chuang-kou-xun-xu-jian-jin-de-3ge-/

滑动窗口小结
滑动窗口顾名思义就是对于序列取一段子序列,通常就是有一个大小可变的窗口,左右两端方向一致的向前滑动,右端固定,左端滑动;左端固定,右端滑动。
可以想象成队列,一端在push元素,另一端在pop元素,如下所示:

假设有数组[a b c d e f g h]
一个大小为3的滑动窗口在其上滑动,则有:

[a b c] [b c d] [c d e] [d e f] [e f g] [f g h]

适用范围
1、一般是字符串或者列表
2、一般是要求最值(最大长度,最短长度等等)或者子序列
算法思想
1、在序列中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个窗口。
2、先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的序列符合要求。
3、此时,停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的序列不再符合要求。同时,每次增加 left前,都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right 到达序列的尽头。
思路其实很简单:第 2 步相当于在寻找一个可行解,然后第 3 步在优化这个可行解,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。

8|0前缀和


给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
输入:nums = [1,3,7,2,8]
输出:2,, [3,7] [2,8]

方法:
想要获取列表中一段子列表,使用双层循环就可以做到。能够获取到任意一段子列表,然后再将这一段nums[1:j]列表的和求解出来,这样时间复杂度会很高是O3,所以使用前缀和每一次内层循环时能够直接算出nums[i:j]的值。
知识点:前缀和

def subarraySum(self, nums: List[int], k: int) -> int: num_times = 0 for i in range(len(nums)): presum = 0 for j in range(i,len(nums)): presum += nums[j] if presum == k: num_times += 1 return num_times

其中的presum += nums[j]就是前缀和,每次内层循环的和都记录下来,避免了计算nums[i:j]
但这种方式还不是最优,时间复杂度为O2,如果想要将时间复杂度降低到O,还需要使用字典这种方式。类似的解法是两数之和

使用一个字典维护前缀和与其出现的次数。

前3项的和为11,k=11。第一项和+sum(3,7) = sum(1,3,7),即sum(1) + 10 = sum(1,3,7),如果存在子数组sum=10,那么sum(1,3,7) - 10 就等于sum(1)。
即:sum(n) - k 在字典中,那么就说明有一个子数组=k

将两个数相加等于固定值,变成使用固定值-其中一个数=差值,判断差值在不在列表中

def subarraySum(self, nums: List[int], k: int) -> int: sum_dict = {0:1} pre_sum = 0 num_times = 0 for i in range(len(nums)): pre_sum += nums[i] if pre_sum - k in sum_dict: num_times += sum_dict[pre_sum-k] if pre_sum in sum_dict: sum_dict[pre_sum] += 1 else: sum_dict[pre_sum] = 1 return num_times

9|0合并区间


题目:
给出一组区间,请合并所有重叠的区间。
请保证合并后的区间按区间起点升序排列。

示例1
复制
[[10,30],[20,60],[80,100],[150,180]]
返回值
[[10,60],[80,100],[150,180]]

方法:区间问题两边都是极限,解决这一类问题需要固定住一端,然后对另一端判断并处理
知识点:区间处理思想

class Solution: def merge(self , intervals ): if not intervals: return [] intervals.sort(key=lambda x: x.start) res = [intervals[0]] for i in intervals[1:]: if res[-1].end < i.start: res.append(i) elif res[-1].end <= i.end: res[-1].end = i.end return res

10|0一维列表小结


对于一维列表的算法求解有很多中技巧,像上面提到的双指针滑动窗口等。在这些技巧之下,需要对一维列表的最基础操作有深刻认识,那就是循环。下面来总结一下一维列表的循环有什么需要掌握的。
单层循环:单层循环能取到列表中每一个值,这没什么特别的。

arr = [5,2,9,6,10,6] for i in range(len(arr)): print(arr[i])

5 2 9 6 10 6

双层循环:双层循环能对列表任意两个元素操作,比如两个元素之和,两个元素之积,甚至是两个元素中间元素的和。

arr = [5,2,9,6] length = len(arr) for i in range(length): for j in range(i, length): print('{}--{}'.format(arr[i], arr[j]))

5--5
5--2
5--9
5--6
2--2
2--9
2--6
9--9
9--6
6--6

双层循环使用的比较多,记住一点,双层循环能获取任意两个元素的组合。在这个最基础的操作之上再去看到一些技巧,技巧是对最基础解法的优化,所以对于一道题如果知道套路就上套路,不知道套路先用最暴力的解法,然后再往套路上靠。


__EOF__

本文作者goldsunshine
本文链接https://www.cnblogs.com/goldsunshine/p/14921440.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   金色旭光  阅读(584)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示