双指针法-栈与队列
- 双指针法章节
- 移除元素
- 删除有序数组中的重复项
- 移动零
- 比较含退格的字符串
- 有序数组的平方
- 反转字符串
- 反转字符串II
- 替换空格
- 反转字符串中的单词
- 纯手撕版,需要注意的地方还蛮多的
- 反转链表
- 删除链表的倒数第 N 个结点
- 这道题之前做的时候没有注意,边界处理是需要着重考虑的,就是删除节点是链表尾结点的情况。
- 为什么会有上述问题?要从本质上去分析!我们要删除倒数第N个节点,那么slow指针最后就应该停在倒数第N+1个节点,假设我们最后fast指针指向None(这个可由while循环的判断条件来控制),那么在结果处,fast和slow指针之间,相隔N个节点,注意是“相隔”,所以在一开始移动时,要让fast移动N+1步,才能达到目标。而移动N+1步可能造成链表越界,所以要加入虚拟头。
- 不加入虚拟头,会面临:当N等于链表长度时,按照我的错误做法,fast会走到尾结点后的None,那么fast.next就会报错,目前我没想到好的解决方案,同样,当链表只有一个节点时,不加入虚拟头,slow.next=slow.next.next也会报错。
- 本题告诉我,虚拟头节点多么重要,但是我还没有理解到:必须加入虚拟头的原因,不加的话,有很多边界情况难以处理,但这似乎不是必须加入的理由?
- 单独处理边界情况:N等于链表长度(换句话说就是:删除头结点)
- 链表相交
- 环形链表 II
- 三数之和
- 四数之和
- 用栈实现队列
- 用队列实现栈
- 有效的括号
- 删除字符串中的所有相邻重复项
- 滑动窗口最大值
- 前K个高频元素
双指针法章节
移除元素
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
n = len(nums)
slow = 0
fast = 0
while fast < n :
if nums[fast]!=val :
nums[slow] = nums[fast]
slow += 1
fast += 1
else :
fast += 1
return slow
删除有序数组中的重复项
注意本题的判断条件,是 slow-1 , 所以需要先判定 slow > 0
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
n = len(nums)
slow = 0
fast = 0
while fast < n :
# 注意这个判断条件,是 slow-1 , 所以需要先判定 slow > 0
if slow > 0 and nums[fast] == nums[slow-1] :
fast += 1
else :
nums[slow] = nums[fast]
fast += 1
slow += 1
return slow
移动零
注意本题,要最后进行一步赋值0的操作
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
slow = 0
fast = 0
while fast < n :
if nums[fast] != 0 :
nums[slow] = nums[fast]
slow += 1
fast += 1
else :
fast += 1
while slow < n :
nums[slow] = 0
slow += 1
比较含退格的字符串
class Solution:
def backspaceCompare(self, s: str, t: str) -> bool:
slow = 0
fast = 0
s = list(s)
t = list(t)
ns = len(s)
nt = len(t)
# 先做s
while fast < ns :
if s[fast] != '#':
s[slow] = s[fast]
slow += 1
fast += 1
else :
if slow > 0 :
slow -= 1
fast += 1
slen = slow
slow = 0
fast = 0
# 再做t
while fast < nt :
if t[fast] != '#':
t[slow] = t[fast]
slow += 1
fast += 1
else :
if slow > 0 :
slow -= 1
fast += 1
tlen = slow
if slen != tlen :
return False
else :
for i in range(slen):
if s[i]!=t[i]:
return False
return True
有序数组的平方
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
n = len(nums)
res = [0]*n
start = 0
end = n-1
# 倒序给结果数组赋值,这样才能保证是非递减顺序
index = n-1
while start <= end :
if nums[start]+nums[end] > 0 :
res[index] = nums[end]**2
end -= 1
index -= 1
elif nums[start]+nums[end] < 0 :
res[index] = nums[start]**2
index -= 1
start += 1
else :
res[index] = nums[start]**2
index -= 1
start += 1
return res
反转字符串
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
n = len(s)
slow = 0
fast = n-1
while slow < fast :
s[slow],s[fast] = s[fast],s[slow]
slow += 1
fast -= 1
反转字符串II
class Solution:
def reverseStr(self, s: str, k: int) -> str:
n = len(s)
for i in range(0,n,2*k):
temp = s[i:i+k]
s = s[:i] + temp[::-1] + s[i+k:]
return s
替换空格
class Solution:
def replaceSpace(self, s: str) -> str:
s = list(s)
n = len(s)
count = 0
for i in s :
if i == ' ':
count += 1
extend = [0]*count*2
s = s + extend
newn = n + count*2
slow = n-1
fast = newn-1
while slow > -1 :
if s[slow]!=' ' :
s[fast] = s[slow]
slow -= 1
fast -= 1
else :
s[fast-2:fast+1] = '%20'
fast -= 3
slow -= 1
return ''.join(s)
反转字符串中的单词
使用 split() 函数版
class Solution:
def reverseWords(self, s: str) -> str:
# 甚至可以不用这个strip
# 直接split也会把首尾的空格去掉
s = s.strip()
s = s[::-1]
s = s.split()
n = len(s)
for i in range(n) :
temp = s[i]
s[i] = temp[::-1]
return ' '.join(s)
纯手撕版,需要注意的地方还蛮多的
class Solution:
def reverseWords(self, s: str) -> str:
s = list(s)
n = len(s)
slow = 0
fast = 0
while fast < n :
if slow == 0 and s[fast]==' ':
fast += 1
elif slow > 0 and s[fast]==' ' and s[slow-1] != ' ':
s[slow] = s[fast]
slow += 1
fast += 1
elif slow > 0 and s[fast]==' ' and s[slow-1] == ' ':
fast += 1
# 上面对 fast 做了加一操作,下面这里的 if 是和上面独立的
# 所以在进行一切操作之前,一定要加上最外层while循环的判断条件 fast < n
if fast < n and s[fast] != ' ':
s[slow] = s[fast]
slow += 1
fast += 1
# 最后一个字符是否是空格,是的话,就去掉
# 我上面的逻辑就会导致,最后一位可能是空格
if s[slow-1] == ' ':
s = s[:slow-1]
length = slow-1
else :
s = s[:slow]
lenght = slow
s = s[::-1]
left = 0
right = 0
while right < slow :
if s[right] != ' ':
right += 1
else :
temp = s[left:right]
s[left:right] = temp[::-1]
right += 1
left = right
# 这里也要注意,要单独处理 right 走到最后一个位置的情况,由于前面已经去掉了空格
# 所以要单独处理
if right == slow-1 :
temp = s[left:]
s[left:] = temp[::-1]
right += 1
left = right
return ''.join(s)
反转链表
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre = None
cur = head
while cur :
temp = cur.next
cur.next = pre
pre = cur
cur = temp
return pre
删除链表的倒数第 N 个结点
这道题之前做的时候没有注意,边界处理是需要着重考虑的,就是删除节点是链表尾结点的情况。
边界错误情况:
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
slow = head
fast = head
while n > 0:
fast = fast.next
n -= 1
while fast and fast.next :
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return head
歪打正着+虚拟头节点:
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
virtual = ListNode(0,head)
slow = virtual
fast = virtual
while n > 0 :
fast = fast.next
n -= 1
while fast.next :
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return virtual.next
卡哥的代码
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
# 创建一个虚拟节点,并将其下一个指针设置为链表的头部
dummy_head = ListNode(0, head)
# 创建两个指针,慢指针和快指针,并将它们初始化为虚拟节点
slow = fast = dummy_head
# 快指针比慢指针快 n+1 步
for i in range(n+1):
fast = fast.next
# 移动两个指针,直到快速指针到达链表的末尾
while fast:
slow = slow.next
fast = fast.next
# 通过更新第 (n-1) 个节点的 next 指针删除第 n 个节点
slow.next = slow.next.next
return dummy_head.next
为什么会有上述问题?要从本质上去分析!我们要删除倒数第N个节点,那么slow指针最后就应该停在倒数第N+1个节点,假设我们最后fast指针指向None(这个可由while循环的判断条件来控制),那么在结果处,fast和slow指针之间,相隔N个节点,注意是“相隔”,所以在一开始移动时,要让fast移动N+1步,才能达到目标。而移动N+1步可能造成链表越界,所以要加入虚拟头。
不加入虚拟头,会面临:当N等于链表长度时,按照我的错误做法,fast会走到尾结点后的None,那么fast.next就会报错,目前我没想到好的解决方案,同样,当链表只有一个节点时,不加入虚拟头,slow.next=slow.next.next也会报错。
本题告诉我,虚拟头节点多么重要,但是我还没有理解到:必须加入虚拟头的原因,不加的话,有很多边界情况难以处理,但这似乎不是必须加入的理由?
单独处理边界情况:N等于链表长度(换句话说就是:删除头结点)
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
slow = head
fast = head
while n > 0 :
fast = fast.next
n -= 1
# 单独处理,当链表长度等于N的情况,即:删除头结点
if fast == None :
head = head.next
else :
while fast.next :
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return head
链表相交
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
curA = headA
curB = headB
countA = 0
countB = 0
while curA :
curA = curA.next
countA += 1
while curB :
curB = curB.next
countB += 1
# 在下面这个判断之后,只需要考虑 countA >= countB 的情况了
if countA < countB :
headA,headB = headB,headA
countA,countB = countB,countA
diff = countA - countB
while diff > 0 :
headA = headA.next
diff -= 1
while headA :
if headA == headB :
return headA
headA = headA.next
headB = headB.next
return None
环形链表 II
这里列出一个代码对比:
我上一次写这道题的代码
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# If there is a cycle, the slow and fast pointers will eventually meet
if slow == fast:
# Move one of the pointers back to the start of the list
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
# If there is no cycle, return None
return None
这一次学习了卡哥的写法后的代码
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next :
slow = slow.next
fast = fast.next.next
if slow == fast :
fast = head
while fast != slow :
slow = slow.next
fast = fast.next
return slow
return None
可以看出,选择 while 循环的判断条件也非常有讲究
三数之和
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
n = len(nums)
res = []
for i in range(0,n-2):
if nums[i] > 0 :
break
if i > 0 and nums[i] == nums[i-1] :
continue
left = i+1
right = n-1
while left < right :
if nums[i] + nums[left] + nums[right] > 0 :
right -= 1
elif nums[i] + nums[left] + nums[right] < 0 :
left += 1
else :
path = [nums[i] , nums[left] , nums[right]]
res.append(path)
while left < right and nums[right] == nums[right-1]:
right -= 1
while left < right and nums[left] == nums[left+1]:
left += 1
left += 1
right -= 1
return res
四数之和
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
nums = sorted(nums)
n = len(nums)
res = []
for i in range(0,n-3):
if i > 0 and nums[i]==nums[i-1]:
continue
# 剪枝细节,一定注意,要考虑target是负数的情况
if nums[i] > 0 and nums[i] > target:
# 这里因为是最外层循环,所以break还是return 无所谓
break
for j in range(i+1,n-2):
if j > i+1 and nums[j]==nums[j-1]:
continue
# 剪枝细节,一定注意,要考虑target是负数的情况,同时要留意到:
# 多个负数相加是更小的负数,所以(nums[i]+nums[j]) > 0是必须的
# 比如 target=-8,解是[-1,-2,-2,-3],前两个的加和是大于target的!
if (nums[i]+nums[j]) > 0 and (nums[i]+nums[j]) > target:
# 注意这里,不能是return ,而是break
break
left = j+1
right = n-1
temp = target - (nums[i]+nums[j])
while left < right :
if nums[left]+nums[right] > temp :
right -= 1
elif nums[left]+nums[right] < temp :
left += 1
else :
path = [nums[i],nums[j],nums[left],nums[right]]
res.append(path)
while left < right and nums[left]==nums[left+1]:
left += 1
while left < right and nums[right]==nums[right-1]:
right -= 1
left += 1
right -= 1
return res
用栈实现队列
class MyQueue:
def __init__(self):
self.pushin = []
self.pushout = []
def push(self, x: int) -> None:
self.pushin.append(x)
def pop(self) -> int:
if self.pushout != [] :
return self.pushout.pop()
else :
self.pushout = self.pushin[::-1]
self.pushin = []
return self.pushout.pop()
def peek(self) -> int:
# 我觉得这里是唯一的难点,搞清楚代码复用的逻辑
# pop出来后,在对pushout,append进去就好了
# 这样下次再pop,还是这个值,是正确的!
# 因为pushout已经可以看做是一个队列了
num = self.pop()
self.pushout.append(num)
return num
def empty(self) -> bool:
if self.pushin == [] and self.pushout == []:
return True
else :
return False
用队列实现栈
两个队列实现:
from collections import deque
class MyStack:
def __init__(self):
self.dq1 = deque()
self.dq2 = deque()
self.number = 0
def push(self, x: int) -> None:
self.dq1.append(x)
self.number += 1
def pop(self) -> int:
if self.empty():
return None
for i in range(0,self.number-1):
self.dq2.append(self.dq1.popleft())
num = self.dq1.popleft()
self.dq1 , self.dq2 = self.dq2 , self.dq1
self.number -= 1
return num
def top(self) -> int:
return self.dq1[-1]
def empty(self) -> bool:
if len(self.dq1) == 0:
return True
else :
return False
一个队列实现:
from collections import deque
class MyStack:
def __init__(self):
self.dq1 = deque()
self.number = 0
def push(self, x: int) -> None:
self.dq1.append(x)
self.number += 1
def pop(self) -> int:
if self.empty():
return None
for i in range(0,self.number-1):
self.dq1.append(self.dq1.popleft())
num = self.dq1.popleft()
self.number -= 1
return num
def top(self) -> int:
return self.dq1[-1]
def empty(self) -> bool:
if len(self.dq1) == 0:
return True
else :
return False
有效的括号
class Solution:
def isValid(self, s: str) -> bool:
stack = []
n = len(s)
i = 0
while i < n :
if s[i]=='(' :
stack.append(')')
elif s[i]=='[' :
stack.append(']')
elif s[i]=='{' :
stack.append('}')
else :
if stack == [] or stack.pop() != s[i]:
return False
i += 1
if len(stack)!=0 :
return False
else :
return True
删除字符串中的所有相邻重复项
class Solution:
def removeDuplicates(self, s: str) -> str:
stack = []
n = len(s)
i = 0
while i < n :
if stack == [] :
stack.append(s[i])
else :
if s[i] == stack[-1]:
stack.pop()
else :
stack.append(s[i])
i += 1
return ''.join(stack)
滑动窗口最大值
自己写的代码,直接就AC了,证明单调队列这里,我还是有点印象的。
from collections import deque
class DQ:
def __init__(self):
self.dq = deque()
def push(self,val):
if len(self.dq) == 0 :
self.dq.append(val)
else :
while len(self.dq) != 0 and val > self.dq[-1] :
self.dq.pop()
self.dq.append(val)
def top(self):
return self.dq[0]
def popleft(self):
self.dq.popleft()
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
res = []
dq = DQ()
n = len(nums)
for i in range(k):
dq.push(nums[i])
res.append(dq.top())
for i in range(k,n):
if nums[i-k] == dq.top() :
dq.popleft()
dq.push(nums[i])
res.append(dq.top())
return res
卡哥的代码,可以做对比
from collections import deque
class MyQueue: #单调队列(从大到小
def __init__(self):
self.queue = deque() #这里需要使用deque实现单调队列,直接使用list会超时
#每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
#同时pop之前判断队列当前是否为空。
def pop(self, value):
if self.queue and value == self.queue[0]:
self.queue.popleft()#list.pop()时间复杂度为O(n),这里需要使用collections.deque()
#如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
#这样就保持了队列里的数值是单调从大到小的了。
def push(self, value):
while self.queue and value > self.queue[-1]:
self.queue.pop()
self.queue.append(value)
#查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
def front(self):
return self.queue[0]
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
que = MyQueue()
result = []
for i in range(k): #先将前k的元素放进队列
que.push(nums[i])
result.append(que.front()) #result 记录前k的元素的最大值
for i in range(k, len(nums)):
que.pop(nums[i - k]) #滑动窗口移除最前面元素
que.push(nums[i]) #滑动窗口前加入最后面的元素
result.append(que.front()) #记录对应的最大值
return result
我觉得最主要的区别就在于:一些极端错误情况的处理上,在实际工程中可能出现类似问题,但是在力扣的示例中,肯定不会有这种情况的。
前K个高频元素
不会,是涉及大顶堆,小顶堆的内容。
卡哥的代码:
#时间复杂度:O(nlogk)
#空间复杂度:O(n)
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
#要统计元素出现频率
map_ = {} #nums[i]:对应出现的次数
for i in range(len(nums)):
map_[nums[i]] = map_.get(nums[i], 0) + 1
#对频率排序
#定义一个小顶堆,大小为k
pri_que = [] #小顶堆
#用固定大小为k的小顶堆,扫描所有频率的数值
for key, freq in map_.items():
heapq.heappush(pri_que, (freq, key))
if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
heapq.heappop(pri_que)
#找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
result = [0] * k
for i in range(k-1, -1, -1):
result[i] = heapq.heappop(pri_que)[1]
return result