LeetCode Hot 100 精练🥸(2)
记忆中的东西一定会消退,真正留下的才是学到的,一定要及时回顾。
215. 数组中的第K个最大元素
题目大意:
给定一个整数数组 nums
和一个整数 k
,要求返回数组中排序后的第 k
个最大的元素。你必须设计一个时间复杂度为 O(n) 的算法来解决此问题。
思路分析:
- 快速选择算法:
- 该题的核心思想是利用快速排序的思想,通过分治的方式来找到第
k
个最大元素。传统的快速排序时间复杂度是 O(n log n),但通过改造快速排序成快速选择算法,我们可以将时间复杂度降低到 O(n),从而满足题目的要求。
- 该题的核心思想是利用快速排序的思想,通过分治的方式来找到第
- 算法步骤:
- 选择枢纽元素:选择数组中的一个元素作为枢纽(通常是数组的中间元素)。
- 分区:将数组分成两个部分:一部分所有元素都大于等于枢纽,另一部分所有元素都小于枢纽。
- 递归查找:如果
k
比较接近右半部分的元素,则在右半部分继续查找;如果k
比较接近左半部分的元素,则在左半部分继续查找。
- 实现步骤:
- 在每次划分后,通过比较
k
的位置来决定继续处理哪一部分。 - 当找到的元素刚好是第
k
个最大的元素时,返回该元素。
- 在每次划分后,通过比较
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def qs(q, l, r, k):
if l >= r:
return
i, j, x = l - 1, r + 1, q[l + r >> 1]
while i < j:
i += 1
while q[i] < x:
i += 1
j -= 1
while q[j] > x:
j -= 1
if i < j:
q[i], q[j] = q[j], q[i]
if j < k:
qs(q, j + 1, r, k)
else:
qs(q, l, j, k)
# qs(q, i, j, k)
# qs(q, j + 1, r, k)
n = len(nums)
k = n - k # n - 1 - k + 1
qs(nums, 0, n - 1, k)
return nums[k]
208. 实现 Trie (前缀树)
题目大意:
实现一个 Trie(前缀树) 数据结构,它支持以下三种操作:
insert(String word)
:向前缀树中插入一个字符串word
。search(String word)
:检查字符串word
是否已经存在于前缀树中。startsWith(String prefix)
:检查是否有已插入的字符串以prefix
作为前缀。
思路分析:
- 数据结构选择:
- 使用字典(
dict
)来表示前缀树的节点。每个节点的键是字符,值是下一个节点或者一个特殊标记(如'#'
)来表示一个字符串的结束。
- 使用字典(
- 插入操作 (
insert
):- 从根节点开始,逐个字符检查该字符是否存在于当前节点的字典中。如果不存在,则创建新的节点。
- 当字符串遍历完毕时,设置一个特殊的标记(例如
'#'
)来标记该节点为一个完整单词的结尾。
- 查找操作 (
search
):- 从根节点开始,逐个字符查找,若任意字符不存在,则返回
False
。 - 如果查找到字符串末尾,并且该节点有
'#'
标记,则返回True
,表示该字符串已经插入。
- 从根节点开始,逐个字符查找,若任意字符不存在,则返回
- 前缀查找操作 (
startsWith
):- 类似
search
,但不需要判断是否到达字符串末尾,只要能够遍历完前缀即可。
- 类似
class Trie:
def __init__(self):
self.trie = {}
def insert(self, word: str) -> None:
cur = self.trie
for ch in word:
if ch not in cur:
cur[ch] = {}
cur = cur[ch]
cur['#'] = True
def search(self, word: str) -> bool:
cur = self.trie
for ch in word:
if ch not in cur:
return False
cur = cur[ch]
return cur['#'] if '#' in cur else False
def startsWith(self, prefix: str) -> bool:
cur = self.trie
for ch in prefix:
if ch not in cur:
return False
cur = cur[ch]
return True
146. LRU 缓存
题目大意:
设计并实现一个支持 LRU(最近最少使用)缓存的数据结构,该缓存具有以下功能:
get(key)
:如果关键字key
存在于缓存中,则返回其对应的值;否则,返回 -1。put(key, value)
:将key
与value
存入缓存。如果缓存容量已满,则删除最近最少使用的元素。
要求:
get
和put
操作都必须在 O(1) 的平均时间复杂度内完成。
思路分析:
LRU 缓存要求最近访问的元素保持在缓存中,而最久未访问的元素被淘汰。为了实现这个功能,我们可以使用一个 双向链表 和一个 哈希表。
数据结构设计:
- 双向链表:
- 双向链表的节点包含
key
和value
。 - 维护两个虚拟节点,
head
(头部)和tail
(尾部)。head
的下一个节点是最先访问的节点,而tail
的前一个节点是最近访问的节点。 - 当一个节点被访问或插入时,它会被移动到链表的头部;当缓存满时,我们会移除尾部的节点(最久未使用)。
- 双向链表的节点包含
- 哈希表:
- 哈希表用于存储缓存的
key
和对应的节点。这样我们可以通过key
快速找到对应的节点,并进行 O(1) 的操作。
- 哈希表用于存储缓存的
主要操作:
- get(key):
- 查找哈希表中是否存在该
key
。如果存在,将对应的节点移动到链表的头部,并返回其值。如果不存在,返回 -1。
- 查找哈希表中是否存在该
- put(key, value):
- 如果
key
已存在,则更新节点的值并将其移到链表头部。 - 如果
key
不存在,则创建一个新节点并插入链表头部。如果缓存已满,移除尾部节点。
- 如果
class DoubleLinkedList:
def __init__(self, key = 0, value = 0):
self.key = key
self.value = value
self.pre = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.size = 0
self.cache = dict()
self.head = DoubleLinkedList()
self.tail = DoubleLinkedList()
self.head.next = self.tail
self.tail.pre = self.head
def get(self, key: int) -> int:
if key not in self.cache:
return -1
node = self.cache[key]
self.moveToHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key in self.cache:
node = self.cache[key]
node.value = value
self.moveToHead(node)
else:
node = DoubleLinkedList(key, value)
self.cache[key] = node
if self.size < self.capacity:
self.size += 1
else:
tail = self.removeTail()
del self.cache[tail.key]
self.addToHead(node)
def removeNode(self, node):
node.next.pre = node.pre
node.pre.next = node.next
def addToHead(self, node):
node.next = self.head.next
node.pre = self.head
self.head.next.pre = node
self.head.next = node
def moveToHead(self, node):
self.removeNode(node)
self.addToHead(node)
def removeTail(self):
node = self.tail.pre
self.removeNode(node)
return node
124. 二叉树中的最大路径和
题目大意:
给定一棵二叉树,要求找出其中路径的最大和。路径可以经过任意节点,但每个节点最多只能被使用一次。路径的定义是从一个节点到另一个节点的连续路径,路径和是路径中所有节点值的和。
实现思路:
- DFS遍历: 采用深度优先搜索 (DFS) 来遍历二叉树,计算以每个节点为起点的路径和。
- 递归的返回值: 对每个节点,我们需要计算其包含左子树和右子树的最大贡献值。为了计算最大路径和,我们需要考虑以下两种情况:
- 路径和:节点自身的值加上其左或右子树的最大路径和。
- 路径通过当前节点:考虑从当前节点出发,左子树、右子树都参与的路径。
- 最大路径和的更新: 递归过程中维护一个全局变量
res
,记录当前最大的路径和。对于每个节点,计算其通过左子树和右子树的路径和,然后更新res
。 - 递归返回: 每次递归返回的是“当前节点及其左右子树路径的最大和”,即一个节点到其父节点的路径和。
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
res = -inf
def dfs(r):
if not r:
return 0
nonlocal res
d1 = dfs(r.left)
d2 = dfs(r.right)
cur = max(r.val, r.val + max(d1, d2))
res = max(res, cur, r.val + d1 + d2)
return cur
dfs(root)
return res