目录标题
问题一:LT146. LRU 缓存机制
先了解什么是LRU
LRU的英文全称是Least Recently Used,也即最不经常使用。我们看着好像挺迷糊的,其实这个含义要结合缓存一起使用。对于工程而言,缓存是非常非常重要的机制,尤其是在当下的互联网应用环境当中,起到的作用非常重要。为了便于大家更好地理解,我们从缓存的机制开始说起。
我们很容易发现,对于一些经常打开淘宝的用户来说,其实没有必要每一次都把完整的链路走一遍。我们大可以把之前展示的结果存储在内存里,下一次再遇到同样请求的时候,直接从内存当中读取并且返回就可以了。这样可以节省大量的时间以及计算资源,比如在大促的时候,就可以把计算资源节省下来用在更加需要的地方。
缓存虽然好用,但是也不是万能的,因为内存是很贵的,我们不可能把所有数据都存在内存里。内存里只能放一些我们认为比较高价值的数据,在这种情况下,计算科学家们想出了种种策略来调度缓存,保持缓存当中数据的高价值。LRU就是其中一种比较常用的策略。
LRU的意思是最长不经常使用,也可以理解成最久没有使用。在这种策略下我们用最近一次使用的时间来衡量一块内存的价值,越久之前使用的价值也就越低,最近刚刚使用过的,后面接着会用到的概率也就越大,那么自然也就价值越高。
显然我们不能通过数组来实现这个缓存。因为数组的查询速度是很慢的
其次我们用HashMap好像也不行,因为虽然查询的速度可以做到 ,但是我们没办法做到更新最近使用的时间,并且快速找出最远更新的数据。
首先HashMap是一定要用的,因为只有HashMap才可以做到 时间内的读写,其他的数据结构几乎都不可行。但是只有HashMap解决不了更新以及淘汰的问题,必须要配合其他数据结构进行。这个数据结构需要能够做到快速地插入和删除,其实我这么一说已经很明显了,只有一个数据结构可以做到,就是链表。
链表有一个问题是我们想要查询链表当中的某一个节点需要O(N)的时间,这也是我们无法接受的。但这个问题并非无法解决,实际上解决也很简单,我们只需要把链表当中的节点作为HashMap中的value进行储存即可
所以总结来说,对于python的数据结构,就是用哈希表和链表来做。
哈希表+链表实现缓冲
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
(1)获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
(2)写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
cache = LRUCache(2)#最大空间是2
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
这里结合链表和哈希表来做,把最常用的节点放在后面,如果有put数据进来,则删掉链表中的第一个节点。
查询的话 就用哈希表来做 时间复杂度为O(1)
【哈希表的一个重要特性就是查询的时间复杂度为O(1)】
其中一个面试题 在考察 哈希表的特性: 问了两次
给定两个数列,判断两个数列中元素的差异。
假设两个数列的长度为n,使用for-loop逐个比较,时间复杂度是O(n^2).
使用哈希表这种数据结构,先遍历一个数列建立一个哈希表,时间复杂度O(n); 然后查询第二列中的每个元素是否在表中,时间复杂度为O(n),所以总时间为O(2n)。效率提高很多。
#创建ListNode的类
class ListNode:
def __init__(self,key = None,value = None):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.hashtable = {}
self.head = ListNode()
self.tail = ListNode()
#初始化链表为 head <-> tail
self.head.next = self.tail
self.tail.prev = self.head
#把node放在链表的末尾
def moveNode2Tail(self,key):
node = self.hashtable[key]
#首先第一做的是 删掉链表的这个node 即 让node的prev和next互相指向
node.prev.next = node.next
node.next.prev = node.prev
#第二要做的是把node放链表的末尾
#用上tail的定义
node.prev = self.tail.prev
node.next = self.tail
self.tail.prev.next = node
self.tail.prev = node
def get(self, key: int) -> int:
if key in self.hashtable:
self.moveNode2Tail(key)
ans = self.hashtable.get(key,-1)
if ans == -1:return ans
else: return ans.value
def put(self, key: int, value: int) -> None:
#如果插入的这个数key 已经存在哈希表的话,直接更新到末尾
if key in self.hashtable:
self.hashtable[key].value = value
self.moveNode2Tail(key)
else:
if len(self.hashtable)==self.capacity:
#需要pop走前面的 头节点
#记住链表是 head - 数字 --tail -- head
#这里的操作时删掉一个node
self.hashtable.pop(self.head.next.key)
self.head.next = self.head.next.next
self.head.next.prev = self.head
new = ListNode(key,value)
self.hashtable[key] = new
#放在末尾
new.prev = self.tail.prev
new.next = self.tail
self.tail.prev.next = new
self.tail.prev = new
这里需要熟悉的操作是 链表的删除某节点 链表的插入 以及哈希表的应用。
问题二:LT394. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
输入:s = "3[a2[c]]"
输出:"accaccacc"
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
class Solution:
def decodeString(self, s: str) -> str:
stack = []
num = 0
ans = ''
for c in s:
if c == '[':
stack.append([num, ans])
num = 0
ans = ''
elif c == ']':
n , last_s = stack.pop()
ans = last_s + n*ans
elif '0' <= c <='9':
num = num*10 + int(c)
#这是因为100 字符 输入的是 1 -- 0 -- 0 不是直接输入100数字
#"100[leetcode]"
else:
ans += c
return ans
题目三:LT:148. 排序链表
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
输入:head = [4,2,1,3]
输出:[1,2,3,4]
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
输入:head = []
输出:[]
思路是:
利用快慢指针来找到链表的中点,然后断开。
快慢指针返回的是 奇数的是刚好中点,偶数则返回左端中点。
然后使得slow.next = None 来断开
不断重复直到不能再分 再用双指针来合并
class Solution:
def getMid(self,node):
if not node: return node
fast= slow = node
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
return slow
def merge(self,p,q):
if not p: return q
if not q: return p
if p.val < q.val:
p.next = self.merge(p.next, q)
return p
else:
q.next = self.merge(p,q.next)
return q
def sortList(self, head: ListNode) -> ListNode:
if not head or not head.next: return head
mid = self.getMid(head)
left = head
right = mid.next
mid.next = None
return self.merge(self.sortList(left), self.sortList(right))
其中的合并两个链表 还有一个便于理解的做法
def merge(p,q)
tmp = ListNode(0)
h = tmp
while p and q:
if p.val < q.val:
h.next = p
p = p.next
else:
h.next = q
q = q.next
if p: h.next = p
if q: h.next = q
return tmp.next
题目四:23. 合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def merge2(self, p, q):
if not p: return q
if not q: return p
if p.val < q.val:
p.next = self.merge2(p.next,q)
return p
else:
q.next = self.merge2(p,q.next)
return q
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
#special case
if len(lists) == 0: return None
if len(lists) == 1:return lists[0]
if len(lists) ==2: return self.merge2(lists[0],lists[1])
ans = self.merge2(lists[0],lists[1])
N = len(lists)-2
while N:
ans = self.merge2(lists[-N],ans)
N-=1
return ans
超出时间限制,需要别的方法
方法二:并归法
并归法先分成一半一半的 最后合并
class Solution:
def merge2(self, p, q):
if not p: return q
if not q: return p
if p.val < q.val:
p.next = self.merge2(p.next,q)
return p
else:
q.next = self.merge2(p,q.next)
return q
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
#special case
if len(lists) == 0: return None
if len(lists) == 1:return lists[0]
n = len(lists)
mid = n//2
return self.merge2(self.mergeKLists(lists[:mid]),self.mergeKLists(lists[mid:]) )
题目五:气球游戏🎈
有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量
class Solution:
def max_score(self,x,y,nums,dp):
ans = 0
for k in range(x+1,y):
left = dp[x][k]
right = dp[k][y]
ans = max(ans, left + nums[x]*nums[k]*nums[y]+right)
dp[x][y] = ans
def maxCoins(self, nums: List[int]) -> int:
nums = [1] + nums + [1]
N = len(nums)
dp = [[0]*(N) for _ in range(N)]
#j作为一个缩小区间的作用
for j in range(2,N):
for i in range(0,N-j):
self.max_score(i,i+j,nums,dp)
return dp[0][N-1]
题目六:LT:486. 预测赢家
给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。
给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。
输入:[1, 5, 2]
输出:False
解释:一开始,玩家1可以从1和2中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 False 。
输入:[1, 5, 233, 7]
输出:True
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 True,表示玩家 1 可以成为赢家。
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
N = len(nums)
dp = [[0]*N for _ in range(N)]
for i in range(N):
dp[i][i] = nums[i]
for i in range(N-2,-1,-1):
for j in range(i+1, N):
dp[i][j] = max((nums[i] - dp[i+1][j]), (nums[j]-dp[i][j-1]))
return dp[0][N-1]>=0
题目七: LT:887. 鸡蛋掉落
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
??? continue
def superEgg(k,N):
dp=[[0]*(k+1) for _ in range(N+1)]
m = 0
while dp[m][k] < N:
m += 1
for i in range(1,k+1):
dp[m][i] = dp[m-1][i-1] + 1 + dp[m-1][i]
return m
题目八: LT:678. 有效的括号字符串
给定一个只包含三种字符的字符串:( ,) 和 *,写一个函数来检验这个字符串是否为有效字符串。有效字符串具有如下规则:
任何左括号 ( 必须有相应的右括号 )。
任何右括号 ) 必须有相应的左括号 ( 。
左括号 ( 必须在对应的右括号之前 )。*可以被视为单个右括号 ) ,或单个左括号 ( ,或一个空字符串。
一个空字符串也被视为有效字符串。
输入: "()"
输出: True
输入: "(*)"
输出: True
输入: "(*))"
输出: True
class Solution:
def checkValidString(self, s: str) -> bool:
#dp[0]代表着需要的最少右括号数量
#dp[1]代表着需要的最多右括号数量
dp = [0,0]
for c in s:
if c =='(':
dp[0]+=1
dp[1] +=1
elif c =='*':
if dp[0]: dp[0]-=1
dp[1] += 1
else:
if dp[0]:dp[0] -=1
dp[1] -= 1
if dp[1] <0:return False
return dp[0]==0