我个人的节奏是一般是一天 4 ~ 5 题, 早上两题,写完休息。下午再做两题,并且复习早上做的题,写完再休息。晚上只做一题或者一题都不做,利用全部时间来 复习归纳 一整天做的题 (关于为什么要将一天时间进行这样的切分以及为什么要这样复习, Section 3 会进行详细解释)。
即一段时间内 (一般是5 ~ 7天) 只做该类型题目, 结合其他质量高的题解以及自身的见解归纳总结出一套属于自己的框架。
早上, 中午和晚上三个学习区段,每一个区段只刷很少数量的题 (1 ~ 2题),并且开始每一个区段之前,都会首先复习之前做过的题,晚上的时候甚至一题也不做,将全部精力都用来复习。
每 2 小时,12小时, 2天, 7天,15天,一个月, 三个月
数组
有序数组首先要想到用双指针
1)快慢指针
模版:
fast, slow = 0,0
while fast < len(nums):
假如fast遍历到希望删除的元素:
skip
fast遍历到新元素:
把fast上的元素给slow上的元素
slow += 1 (有可能先换元素,有可能先+1)
fast += 1
return slow 或者slow + 1
数组问题中比较常见的快慢指针技巧,是让你原地修改数组。
26题https://leetcode.cn/problems/remove-duplicates-from-sorted-array/ 高效解决这道题就要用到快慢指针技巧:
我们让慢指针 slow 走在后面,快指针 fast 走在前面探路, fast 找到一个不重复的元素,就让 slow 前进一步,并赋值给 slow 。slow + 1是整个的长度。
def removeDuplicates(nums: List[int]) -> int:
if len(nums) == 0:
return 0
# 维护 nums[0..slow] 无重复
slow = 0
fast = 1
while fast < len(nums):
if nums[fast] != nums[slow]:
slow += 1
# 维护 nums[0..slow] 无重复
nums[slow] = nums[fast]
fast += 1
# 数组长度为索引 + 1
return slow + 1
83题https://leetcode.cn/problems/remove-duplicates-from-sorted-list/submissions/
高效解决这道链表题也要用到快慢指针技巧:
我们让慢指针 slow 走在后面,快指针 fast 走在前面探路, fast 找到一个不重复的元素,就让 slow连接到fast,slow来到fast指针位置,fast再往前走。不需要覆盖元素,只需要连接需要的元素。注意最后断开slow和后面重复node的链接。
def deleteDuplicates(head: ListNode) -> ListNode:
if not head: # 如果链表为空,返回 null 。
return None
slow, fast = head, head
while fast: # 遍历链表,并找到重复元素。
if fast.val != slow.val: # 如果 fast 指向的节点与 slow 指向的节点的值不相等,说明链表中出现了重复元素。
slow.next = fast # 将 slow 指向 fast 的位置。
slow = slow.next # slow 指向下一个节点。
fast = fast.next # fast 指向下一个节点。
slow.next = None # 断开与后面重复元素的连接
return head # 返回处理后的链表头节点。
27题https://leetcode.cn/problems/remove-element/
题目要求我们把
nums
中所有值为val
的元素原地删除,依然需要使用快慢指针技巧:如果
fast
遇到值为val
的元素,则直接跳过,否则就赋值给slow
指针,并让slow
前进一步。注意这里和有序数组去重的解法有一个细节差异,我们这里是先给
nums[slow]
赋值然后再给slow++。
def removeElement(nums: List[int], val: int) -> int:
fast, slow = 0, 0
while fast < len(nums):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
283题https://leetcode.cn/problems/move-zeroes/submissions/
题目让我们将所有 0 移到最后,其实就相当于移除
nums
中的所有 0,然后再把后面的元素都赋值为 0 即可。
def moveZeroes(self, nums):
slow = 0
fast = 0
while fast < len(nums):
if nums[fast] != 0:
nums[slow]=nums[fast]
slow += 1
fast += 1
for i in range(slow,len(nums)):
nums[i] = 0
滑动窗口(时间复杂度O(N))
left
指针在后,right
指针在前,两个指针中间的部分就是「窗口」,算法通过扩大和缩小「窗口」来解决某些问题。1、我们在字符串
S
中使用双指针中的左右指针技巧,初始化left = right = 0
,把索引左闭右开区间[left, right)
称为一个「窗口」。2、我们先不断地增加
right
指针扩大窗口[left, right)
,直到窗口中的字符串符合要求(包含了T
中的所有字符)。3、此时,我们停止增加
right
,转而不断增加left
指针缩小窗口[left, right)
,直到窗口中的字符串不再符合要求(不包含T
中的所有字符了)。同时,每次增加left
,我们都要更新一轮结果。4、重复第 2 和第 3 步,直到
right
到达字符串S
的尽头。这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
增加
right
,直到窗口[left, right)
包含了T
中所有字符:
现在开始增加
left
,缩小窗口[left, right)
:
直到窗口中的字符串不再符合要求,
left
不再继续移动
模版:
class Solution:
def problemName(self, s: str) -> int:
# Step 1: 定义需要维护的变量们 (对于滑动窗口类题目,这些变量通常是最小长度,最大长度,或者哈希表)
x, y = ..., ...# Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
start = 0
for end in range(len(s)):
# Step 3: 更新需要维护的变量, 有的变量需要一个if语句来维护 (比如最大最小长度)
x = new_x
if condition:
y = new_y'''
------------- 下面是两种情况,读者请根据题意二选1 -------------
'''
# Step 4 - 情况1
# 如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否达到了限定长度
# 如果达到了,窗口左指针前移一个单位,从而保证下一次右指针右移时,窗口长度保持不变,
# 左指针移动之前, 先更新Step 1定义的(部分或所有)维护变量
if 窗口长度达到了限定长度:
# 更新 (部分或所有) 维护变量
# 窗口左指针前移一个单位保证下一次右指针右移时窗口长度保持不变# Step 4 - 情况2
# 如果题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
# 如果当前窗口不合法时, 用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
# 在左指针移动之前更新Step 1定义的(部分或所有)维护变量
while 不合法:
# 更新 (部分或所有) 维护变量
# 不断移动窗口左指针直到窗口再次合法# Step 5: 返回答案
return ...
现在开始套模板,只需要思考以下几个问题:
1、什么时候应该移动
right
扩大窗口?窗口加入字符时,应该更新哪些数据?2、什么时候窗口应该暂停扩大,开始移动
left
缩小窗口?从窗口移出字符时,应该更新哪些数据?3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
643题https://leetcode.cn/problems/maximum-average-subarray-i/
def findMaxAverage(self, nums: List[int], k: int) -> float:
maxAvg = float('-inf')
left = 0
sum = 0
for right in range(len(nums)):
sum += nums[right]
if right - left + 1 == k:
maxAvg = max(maxAvg, sum / k)
while right - left + 1 >= k:
sum -= nums[left]
left += 1
return maxAvg
2)左右指针
1. 二分查找
2.两数之和
3、反转数组
344题https://leetcode.cn/problems/reverse-string/submissions/
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
left = 0
right = len(s) - 1
while left <= right:
s[left],s[right] = s[right],s[left]
left += 1
right -= 1
return s
第5题https://leetcode.cn/problems/longest-palindromic-substring/4、回文串判断
回文串有两种:
一种是判断是否是回文串,用左右指针
一种是求最长的回文子串,用从中心向两端扩散的双指针(子串为什么不用滑动窗口?因为没有需要减的,不需要左指针右移。并且滑动窗口是快慢指针,都是从左边开始。而回文串比较特殊,回文串是左右指针。)
如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。
那么可以事先写一个函数,在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串(即s[l]==s[r],然后在不越界的前提下左移l,右移r),这样,如果输入相同的 l 和 r,就相当于寻找长度为奇数的回文串,如果输入相邻的 l 和 r,则相当于寻找长度为偶数的回文串:
def palindrome(s, l, r):
# 防止索引越界
while l >= 0 and r < len(s) and s[l] == s[r]:
# 双指针,向两边展开,左往左,右往右
l -= 1
r += 1
# 返回以 s[l] 和 s[r] 为中心的最长回文串
return s[l+1: r] # 结束循环时l比回文串的内容多左移了一个 r多右移了一个
那么求最长回文串,就相当于是:
for i in range(len(s)):
找到以 s[i] 为中心的回文串
找到以 s[i]和s[i+1] 为中心的回文串
更新答案
def longestPalindrome(self, s: str) -> str:
def palindrome(s,l,r):
while l >=0 and r< len(s) and s[l] == s[r]:
l -= 1
r += 1
return s[l+1: r]
res = ""
for i in range(len(s)):
sub1 = palindrome(s,i,i)
sub2 = palindrome(s,i, i+1)
res = sub1 if len(res) < len(sub1) else res
res = sub2 if len(res) < len(sub2) else res
return res