一、栈
1. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
解题思路
嵌套括号,需要由内而外生成与拼接字符串 -> 栈的先入后出
代码
class Solution:
def decodeString(self, s: str) -> str:
stack,res,multi=[],'',0
for c in s:
if c=='[':
stack.append([multi,res])
res,multi='',0
elif c==']':
curmulti,lastres=stack.pop()
res=lastres+curmulti*res
elif '0'<=c<='9':
multi=multi*10+int(c)
else:
res+=c
return res
** multi=multi*10+int(c) 的含义是防止数字十位、百位的出现,例如:
"100":第一个字符是’1‘ : 0 * 10 + 1 = 1; 第二个字符是’0‘ : 1 * 10 + 0 = 10; 第三个字符是’0‘ : 10 * 10 + 0 = 100
二、排序
1. 快速排序
排序过程
a. 取一个元素p,使得元素p归位
b. 列表p被分为2部分,左边比p小,右边比p大
c. 递归完成排序
算法模板
def patition(li,left,right):
tmp=li[left]
while left < right:
while left < right and li[right]>=tmp: #右边比p大
right-=1
li[left]=li[right]
while left < right and li[left]<=tmp: #左边比p小
left+=1
li[right]=li[left]
li[left]=tmp
return left
def quick_sort(li,left,right):
if left < right:
mid=patition(li,left,right)
quick_sort(li,left,mid-1) #递归左区间
quick_sort(li,mid+1,right) #递归右区间
return li
(1)数组中第k个最大的元素
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
解题思路:普通方法sort的时间复杂度为O(nlogn),因此需要使用快速排序,如果元素p恰好在第k个位置,则立即返回
代码:
import random
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def quick_sort(nums,k):
#随机选择基数
pivot=random.choice(nums)
big,small,equal=[],[],[]
#将大于、小于、等于pivot的元素分至big,small,equal中
for num in nums:
if num>pivot: #大于
big.append(num)
elif num<pivot: #小于
small.append(num)
else:
equal.append(num)
if k<=len(big): #第k大元素在big内
return quick_sort(big,k)
if len(nums)-len(small)<k:
#第k大元素在small中
return quick_sort(small,k-len(nums)+len(small))
return pivot
return quick_sort(nums,k)
2. 堆排序
小根堆:根节点的值比它的孩子都小
大根堆:根节点的值比它的孩子都大
实现:heapq(小顶堆)
heapq.heappush(堆,值) 将值加入堆,自动维护小根堆
heapq.heappop(堆,值) 弹出堆顶的值,并自动维护小根堆
heap[0]:取出堆顶元素
实现大顶堆的方法: 小顶堆的插入和弹出操作均将元素 取反 即可。
(1)数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
-
MedianFinder()
初始化MedianFinder
对象。 -
void addNum(int num)
将数据流中的整数num
添加到数据结构中。 -
double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
解题思路:用大根堆B保存较小的一半,用小根堆A保存较大的一半
函数 addNum(num)
当 m=n(即 N 为 偶数):需向 A 添加一个元素。实现方法:将新元素 num 插入至 B ,再将 B 堆顶元素插入至 A 。
当 m=n(即 N 为 奇数):需向 B 添加一个元素。实现方法:将新元素 num 插入至 A ,再将 A 堆顶元素插入至 B 。
函数findMedian()
当 m=n( N 为 偶数):则中位数为 ( A 的堆顶元素 + B 的堆顶元素 )/2。
当 m=n( N 为 奇数):则中位数为 A 的堆顶元素。
代码:
class MedianFinder:
def __init__(self):
self.A=[] #小顶堆,保存较大的一半
self.B=[] #大顶堆,保存较小的一半
def addNum(self, num: int) -> None:
if len(self.A) != len(self.B):
heappush(self.A,num)
heappush(self.B,-heappop(self.A))
else:
heappush(self.B,-num)
heappush(self.A,-heappop(self.B))
def findMedian(self) -> float:
return self.A[0] if len(self.A) !=len(self.B) else (self.A[0]-self.B[0])/2.0
# Your MedianFinder object will be instantiated and called as such:
# obj = MedianFinder()
# obj.addNum(num)
# param_2 = obj.findMedian()
3. 冒泡排序
对于长度为n的数组,进行n-1趟(最后一趟剩余一个元素,不用交换),每趟进行n-i-1次移动(i表示趟数),移动需要判断前一个元素是否比后一个元素大,如果是,则交换前后元素
如果一趟下来,没有交换过,则认为数组已经有序
def bubble_sort(lst):
for i in range(len(lst)-1): #第i趟
exchange=False
for j in range(len(lst)-i-1): #指针移动位置
if lst[j]>lst[j+1]:
lst[j],lst[j+1]=lst[j+1],lst[j] #前后交换
exchange=True
if not exchange:
return
三、贪心算法
1. 跳跃游戏
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
解题思路
查看当前左边加上跳跃长度是否覆盖到最后一个下标,如果覆盖则True,否则False
代码
class Solution:
def canJump(self, nums: List[int]) -> bool:
if len(nums)==1 : return True
cover=0
for i in range(len(nums)):
if i<=cover:
cover=max(i+nums[i],cover)
if cover >=len(nums)-1:
return True
return False
2. 跳跃游戏Ⅱ
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i
向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
解题思路
看当前覆盖距离是否到达倒数第二个点,如果是,步数还要加1
代码
class Solution:
def jump(self, nums: List[int]) -> int:
cur_distance=0
res=0
next_distance=0
for i in range(len(nums)-1):
next_distance=max(i+nums[i],next_distance)
if i==cur_distance:
res+=1
cur_distance=next_distance
return res
3. 划分字母区间
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
解题思路
记录每个字符最后出现的位置,当遍历到这个位置的时候可以切割
代码
class Solution:
def partitionLabels(self, s: str) -> List[int]:
dicts={} #记录最远的位置
for i,w in enumerate(s):
dicts[w]=i
res=[]
start,end=0,0
for i,ch in enumerate(s):
end=max(end,dicts[ch])
if i==end:
res.append(end-start+1)
start=i+1
return res
四、动态规划
1. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
解题思路
这是一个01背包问题
背包容量:分割的两个子集和,数组和的一半
物品:数字
物品的重量和价值:数字大小
动规五部曲
(1)dp数组的含义:dp[i]表示容量为i的背包,所背物品的最大价值为dp[i]
(2)递推公式:求能装的最大价值
装入物品i:dp[i-nums[i]]+nums[i];不装入物品i,背包价值和容量不变:dp[i]
dp[i] = max(dp[i], dp[i-nums[i]]+nums[i])
(3)初始化:题目说数组元素不超过100,数组大小不超过200,因此总和不大于20000,则等和子集最大为10000
dp=[0]*10001
dp[0]一定为0
(4)遍历顺序:一维01背包遍历需要先物品后背包,背包倒序
(5)举例推导
代码
class Solution:
def canPartition(self, nums: List[int]) -> bool:
#01背包
#dp[i]表示第i个位置分割的子集和为dp[i]
#背包:和的一半
#物品:数字;种类和价值:数字大小
#如果数组有奇数个,则无法平分
if sum(nums)%2==1:
return False
target=sum(nums)//2 #背包容量
dp=[0]*10001
#遍历顺序:先物品后背包,背包倒序
for n in nums:
for i in range(target,n-1,-1):
dp[i]=max(dp[i],dp[i-n]+n)
if dp[target]==target:
return True
else:
return False
2. 最长有效括号
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号
子串的长度。
解题思路
有效括号必须以’)‘结尾
当遇到')'时,括号成对出现,需要个数+2
如果出现嵌套括号,'(())' 则需要加上嵌套括号的个数,dp[i-1]+2
判断前面是否还存在括号,'()(())' dp[i-dp[i-1]-2]
动规五部曲
(1)dp[i]的含义:以i为结尾的子串包含的最长有小括号数为dp[i]
(2)递推公式:dp[i]= dp[i-1] + dp[i-dp[i-1]-2] +2
条件:当前字符为')',s[i-dp[i-1]-1]为'(',并且i-dp[i-1]-1>=0(为什么不是-2,因为-2则是-1,dp[-1]表示最后一个元素)
(3)初始化:dp初始化为0,dp[0]=0
(4)遍历顺序:依次遍历字符串s
代码
class Solution:
def longestValidParentheses(self, s: str) -> int:
if s=='': return 0
#dp[i]表示以i结尾的子串的最长有效括号为dp[i]
dp=[0]*len(s)
for i,ch in enumerate(s):
if ch==')' and i-dp[i-1]-1>=0 and s[i-dp[i-1]-1]=='(':
dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2
return max(dp)
3. 最长回文子串
给你一个字符串 s
,找到 s
中最长的回文子串。
动规五部曲
(1)dp[i][j]的含义:dp[i][j]表示子串[i,```,j]是否为回文子串
(2)递推公式:
当s[i]==s[j]时,当前子串是否为回文取决于内部是否为回文子串,即dp[i][j]=dp[i+1][j-1]
当[i+1,j-1]只有一个元素时或者没有元素,则一定回文,因此需要判断j-i<3
(3)初始化:初始化为False,对角元素即i=j时为True
(4)遍历顺序:由于i<=j,因此先遍历j(1~len(s)),再遍历i(0~j)
需要一个记录最大回文子串和起始下标的变量,如果dp[i][j]为True,则判断j-i+1>length
代码
class Solution:
def longestPalindrome(self, s: str) -> str:
#dp[i][j]:表示子串s[i,```,j]的是否为回文子串
#初始化
dp=[[False]*len(s) for _ in range(len(s))]
for i in range(len(s)): #对角线元素一定为回文子串
dp[i][i]=True
length=1 #用于更新最长回文子串的长度
begin=0 #用于记录回文子串的起始位置
for j in range(1,len(s)):
for i in range(j):
if s[i]!=s[j]:
dp[i][j]=False
else:
if j-i<3:
dp[i][j]=True
else:
dp[i][j]=dp[i+1][j-1]
if dp[i][j] and j-i+1 > length:
length=j-i+1
begin=i
return s[begin:begin+length]
五、技巧
1. 只出现一次的数字
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
解题思路
使用字典使用了O(n)空间,题目要求只使用常量空间,因此使用位运算方法
异或运算的结果就是只出现一次的元素
代码
class Solution:
def singleNumber(self, nums: List[int]) -> int:
#位运算,O(1)空间
#异或运算的结果就是只出现一次的数字
return reduce(lambda x,y:x^y,nums)
**reduce接受一个函数和一个可迭代对象,函数会被应用到可迭代对象的每个元素上,然后将结果累积起来。函数接受x和y两个参数,x和y进行异或运算,应用到nums的每个元素并累积起来。
2. 下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3]
,以下这些都可以视作arr
的排列:[1,2,3]
、[1,3,2]
、[3,1,2]
、[2,3,1]
。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]
的下一个排列是[1,3,2]
。 - 类似地,
arr = [2,3,1]
的下一个排列是[3,1,2]
。 - 而
arr = [3,2,1]
的下一个排列是[1,2,3]
,因为[3,2,1]
不存在一个字典序更大的排列。
给你一个整数数组 nums
,找出 nums
的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
解题思路
(1)从后向前,找第一个前大于后的元素
(2)从后向前找第一个大于替换元素的最小元素
(3)交换
(4)交换的后部分升序排列
代码
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
i=len(nums)-1
while i>0 and nums[i-1]>=nums[i]: #找到第一个前面大于后面的元素
i-=1
if i!=0:
j=len(nums)-1
while nums[j]<=nums[i-1]: #找到第一个替换元素的最小元素
j-=1
#交换
nums[i-1],nums[j]=nums[j],nums[i-1]
#排序
nums[i:]=sorted(nums[i:])
3. 寻找重复元素
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1)
的额外空间。
解题思路
方法一:二分搜索
统计 nums[i] <= 当前元素的个数cnt,如果cnt大于当前当前元素说明有重复,找到第一个大于的值;否则说明没有重复
方法二:快慢指针法
先让慢指针一次走一步,快指针一次走两步;如果相遇,说明存在重复元素;再让慢指针回到起始点,快指针在相遇点,同时一次一步,相遇的点就是重复的元素(环形链表题)
代码
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
#方法一:二分搜索
#思路:统计nums[i]<=当前元素的个数cnt,如果cnt大于当前当前元素说明有重复,找到第一个大于的值;否则说明没有重复
l,r=1,len(nums)-1
ans=-1
while l<=r:
mid=(l+r)//2
cnt=0
for i in range(len(nums)):
cnt+= nums[i] <= mid
if cnt <= mid:
l=mid+1
else:
r=mid-1
ans=mid
return ans
#方法二:快慢指针法
#思路:先让慢指针一次走一步,快指针一次走两步;如果相遇,说明存在重复元素;再让慢指针回到起始点,快指针在相遇点,同时一次一步,相遇的点就是重复的元素
slow,fast=0,0
while True:
fast=nums[fast]
fast=nums[fast]
slow=nums[slow]
if fast ==slow:
break
slow=0
while slow != fast:
slow=nums[slow]
fast=nums[fast]
return slow
六、链表
1. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
解题思路
逆序存储-逆序创建新节点,这个新节点的值由l1的值、l2的值和进位累加得到,存储的是累加值的余数,同时需要更新进位数(累加值的除数),然后更新当前节点、l1节点和l2节点
代码
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
cur=dummy=ListNode() #创建虚拟节点
carry=0 #进位
while l1 or l2 or carry:
s=carry+(l1.val if l1 else 0)+(l2.val if l2 else 0) #累加
cur.next=ListNode(s%10) #余数作为新节点
carry=s//10 #更新进位
cur=cur.next
if l1: l1=l1.next
if l2: l2=l2.next
return dummy.next
2. 删除链表倒数第n个节点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
解题思路
快慢指针法:倒数第n个节点,快慢指针相差n+1(因为慢指针需要指向倒数第n个节点的前一个节点才能删除);先让快指针走n+1步,再让块、慢指针同时移动,直至快指针指向链表末尾,此时慢指针指向倒数第n+1个节点
虚拟节点:如果倒数第n个节点是头节点,则涉及到头节点的处理,因此使用虚拟节点
代码
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
#双指针法:先让快指针走n+1步,再让快慢指针同时移动,直至快指针到达链表末尾
#虚拟节点,防止倒数第n个节点为头节点
dummy=ListNode(next=head)
fast,slow=dummy,dummy
for _ in range(n+1):
fast=fast.next
while fast:
fast=fast.next
slow=slow.next
slow.next=slow.next.next
return dummy.next
3. 两两交换链表的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)
解题思路
虚拟节点:设计头节点的处理,使用虚拟节点dummy
注意交换顺序:dummy->2->1->3
临时保存重要节点:1、3
更新下一个节点:cur=cur.next.next
代码
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head: return
#虚拟节点
cur=dummy=ListNode(next=head)
while cur.next and cur.next.next:
tmp1=cur.next
tmp2=cur.next.next.next
cur.next=cur.next.next
cur.next.next=tmp1
tmp1.next=tmp2
cur=cur.next.next
return dummy.next
4. K 个一组翻转链表
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路
代码
class Solution:
# 翻转一个子链表,并且返回新的头与尾
def reverse(self, head: ListNode, tail: ListNode):
prev = tail.next
p = head
while prev != tail:
nex = p.next
p.next = prev
prev = p
p = nex
return tail, head
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
hair = ListNode(0)
hair.next = head
pre = hair
while head:
tail = pre
# 查看剩余部分长度是否大于等于 k
for i in range(k):
tail = tail.next
if not tail:
return hair.next
nex = tail.next
head, tail = self.reverse(head, tail)
# 把子链表重新接回原链表
pre.next = head
tail.next = nex
pre = tail
head = tail.next
return hair.next
5. 随机链表的复制
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
用一个由 n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。
你的代码 只 接受原链表的头节点 head
作为传入参数。
解题思路
使用字典,键源节点,值新节点
遍历每个节点,创建新的引用next和random
代码
class Solution:
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
if not head : return
#初始化字典 {源节点:新节点}
dicts={}
cur=head
#复制链表,遍历建立新节点,添加键值对
while cur:
node=Node(cur.val)
dicts[cur]=node
cur=cur.next
#构建新链表的引用指向next和random
cur=head
while cur:
dicts[cur].next=dicts.get(cur.next)
dicts[cur].random=dicts.get(cur.random)
cur=cur.next
return dicts[head]