列表
列表和字符串是算法中最常见的数据结构,在列表中又分为一维列表和二维列表。一维列表的数据结构即使很简单也会有很多复杂的算法思想。
一维列表
- 删除排序数组中的重复项
- 移动非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
移动非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)
盛最多水的容器
提示:
给你 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
三数之和
题目:
给一个数组[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
双指针小结
以上题目能力体现双指针的使用技巧,通常使用双指针,可以用在如下场景中:
- 从两端向中间迭代数组。
这时你可以使用双指针技巧:一个指针从头部开始,而另一个指针从尾部开始。这种技巧经常在排序数组中使用。
- 同时有一个慢指针和一个快指针。
确定两个指针的移动策略。与前一个场景类似,你有时可能需要在使用双指针技巧之前对数组进行排序,也可能需要运用贪心法则来决定你的运动策略。
长度最小的子数组
题目:
给定一个含有 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
无重复字符的最长子串
题目:
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
方法:判断一个子数组中是否有重复有很多种办法:
- 使用暴力双层遍历
- 使用字典,判断某元素是否在字典中
- 使用数组,判断元素是否在数组中
方法:使用滑动窗解决:窗口左侧不断加入元素,判断元素是否存在于窗口中,不在则继续加入,在则将重复的元素去掉,继续添加。在这个过程中会有一个最大值出现。
知识点
: 变长滑动窗口
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 步在优化这个可行解,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。
前缀和
给定一个整数数组和一个整数 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
合并区间
题目:
给出一组区间,请合并所有重叠的区间。
请保证合并后的区间按区间起点升序排列。
示例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
一维列表小结
对于一维列表的算法求解有很多中技巧,像上面提到的双指针
,滑动窗口
等。在这些技巧之下,需要对一维列表的最基础操作有深刻认识,那就是循环。下面来总结一下一维列表的循环有什么需要掌握的。
单层循环
:单层循环能取到列表中每一个值,这没什么特别的。
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
双层循环使用的比较多,记住一点,双层循环能获取任意两个元素的组合。在这个最基础的操作之上再去看到一些技巧,技巧是对最基础解法的优化,所以对于一道题如果知道套路就上套路,不知道套路先用最暴力的解法,然后再往套路上靠。