138. 随机链表的复制
浅拷贝和深拷贝:
前者新建一个数据结构,不创建元素副本;
后者不仅新建一个数据结构,而且新建一个数据副本。
深拷贝(Deep Copy)和浅拷贝(Shallow
Copy)是在复制数据结构时常见的两个概念,特别是在涉及复杂对象(如链表、树等)时。这两种拷贝方式的主要区别在于它们对原数据结构中对象的处理方式。浅拷贝(Shallow Copy)
- 浅拷贝创建一个新的数据结构(如新的链表或数组)但是不创建数据结构中的元素的副本。
- 在浅拷贝中,新数据结构中的元素仍然指向原有数据结构中的相同元素(即对象的引用被复制,而不是对象本身)。
- 如果原数据结构中的对象被修改,这些修改也会反映在浅拷贝的数据结构中,因为它们共享相同的对象。
深拷贝(Deep Copy)
- 深拷贝不仅创建一个新的数据结构,还为原数据结构中的每个元素创建副本。
- 在深拷贝中,新数据结构的元素是原元素的副本,这意味着它们是完全独立的。
- 对原数据结构或深拷贝数据结构中的对象进行的修改不会影响另一个,因为它们不共享对象。
在链表的上下文中
假设有一个链表,其中每个节点都有一个
random
指针,可以指向链表中的任何节点或null
。
- 深拷贝这个链表意味着创建一个新链表,其中包含原链表每个节点的副本,且每个副本节点的
next
和random
指针都指向新链表中的对应节点,而不是原链表中的节点。- 浅拷贝这个链表则意味着创建一个新链表,但是不为原链表中的节点创建副本。在这种情况下,复制链表中的节点将直接指向原链表中的节点,这通常不是我们想要的。
深拷贝链表的方法
- 遍历原链表,为每个原节点创建一个新节点,这个新节点的值与原节点相同,但是
next
和random
指针暂时留空。- 建立一个映射关系,将原节点映射到对应的新节点,以便能够设置
random
指针。- 再次遍历原链表,这次使用映射来设置每个新节点的
next
和random
指针。这种方法确保了每个新节点都与原链表中对应的节点分离,实现了深拷贝。
"""
# Definition for a Node.
class Node:
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution:
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
if not head: return
# 建立映射关系 {原节点:新节点}
old_new = dict()
cur = head # 当前节点
while cur:
old_new[cur] = Node(cur.val, None, None) # 指针部分留空,不然就指向原始链表了
cur = cur.next # 往后遍历
# 设置新节点的指针,也是从原始的头节点开始
cur = head
while cur:
# 如果有后序节点,就将其从Null更新为原始链表中的后续节点
if cur.next:
old_new[cur].next = old_new[cur.next]
if cur.random:
old_new[cur].random = old_new[cur.random]
cur = cur.next
return old_new[head]
297. 二叉树的序列化与反序列化
解题思路:
序列化是将二叉树编码成一个字符串
反序列化是将这个字符串解码成一个二叉树。
针对序列化:考虑使用层序遍历的方法,将二叉树的节点自上而下、从左往右地放在字符串里。不能忘了空节点,使用特殊符号占位,并分隔;
针对反序列化:针对字符串根据分隔符转换为list,初始化根节点,另其值等于list的首个元素。然后按照层序遍历的方式,将每层的节点指向列表的对应元素值,使用一个全局的索引变量解决。
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string.
:type root: TreeNode
:rtype: str
"""
# 序列化
# 层序遍历,将其结果都存储到一个字符串,用;分隔
if not root: return '#;'
res = ''
queue = [root]
while queue: # 因为最后只要一个字符串,所以就省略了
node = queue.pop(0)
# 如果节点存在,结果字符串加上节点的值
if node:
res += f'{node.val};'
queue.append(node.left) # 加左子节点
queue.append(node.right) # 右子节点
else: # 如果是空,那就加上空值占位符
res += '#;'
return res
def deserialize(self, data):
"""Decodes your encoded data to tree.
:type data: str
:rtype: TreeNode
"""
# 反序列化
# 将字符串拆分为list
if data == '#;': return None
nodes = data.split(';')[:-1] # 因为这样分隔最后会出现空格,删除
root = TreeNode(int(nodes[0])) # 根节点设置
# 类似层序遍历
queue = [root] # 层序遍历的起始
index = 1 # 记录数组索引,0已经被定义了(根节点)
while queue:
node = queue.pop(0) # 第一次就是根节点
# 如果当前节点不为空,也就是 不是#
if nodes[index] != '#':
node.left = TreeNode(int(nodes[index])) # 按照层序遍历,是从左到右的
queue.append(node.left) # 队列结尾加上下一层的左节点
index += 1 # 左边的经过处理后就索引+1,不管是不是空值
# 对右边执行相同操作
if nodes[index] != '#':
node.right = TreeNode(int(nodes[index]))
queue.append(node.right)
index += 1
# 返回根节点
return root
# Your Codec object will be instantiated and called as such:
# ser = Codec()
# deser = Codec()
# ans = deser.deserialize(ser.serialize(root))
209. 长度最小的子数组
解题思路:双指针,滑动窗口。
从开始,逐步扩展窗口,检测是否满足求和条件;一旦满足,就缩减窗口,满足的时候也要比较最小的窗口长度。
可以使用for循环或者两层while。要注意的是for循环默认在执行完循环主体后进入下一个遍历。while循环需要手动在最后加,如果在前面加那么最小长度就不能+1,因为此时右指针正好跑过满足条件的窗口了。
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
# 双指针,滑动窗口
# 先不断扩展窗口,等满足条件就逐步缩小窗口
# 左指针
left = 0
temp = 0 # 窗口内的和
min_len = float('inf') # 最小长度
# 循环
for right in range(len(nums)):
# 窗口内总和
temp += nums[right]
# 满足条件,开始缩减窗口
while temp >= target:
min_len = min(min_len, right-left+1)
temp -= nums[left]
left += 1
# 检测最终是否有最短的存在
if min_len != float('inf'):
return min_len
else:
return 0
使用两层while循环
def minSubArrayLen(target, nums):
n = len(nums)
min_len = float('inf') # 初始化为无穷大,用以比较找到最小长度
sum = 0 # 窗口内元素的总和
left, right = 0, 0 # 双指针,都初始化为0
while right < n:
# 扩大窗口
sum += nums[right]
right += 1
# 当窗口内的总和大于等于target时,尝试缩小窗口以找到最小长度
while sum >= target:
min_len = min(min_len, right - left)
sum -= nums[left]
left += 1
return min_len if min_len != float('inf') else 0
139. 单词拆分
这个问题可以通过动态规划来解决。动态规划是一种解决复杂问题的方法,它将问题分解成更小的子问题,通过解决子问题来解决整个问题。
动态规划思路
定义状态:定义一个布尔类型的动态规划数组
dp
,其中dp[i]
表示字符串s
的前i
个字符(s[0..i-1]
)能否被字典wordDict
中的一个或多个单词拼接得出。状态转移:为了确定
dp[i]
的值,我们需要遍历[0, i)
的所有可能的分割点j
,检查两个条件:
- 字符串
s[j..i-1]
(即从j
到i-1
的子串)是否存在于字典wordDict
中。dp[j]
是否为true
,表示s[0..j-1]
可以被拼接得出。如果上述两个条件中的任何一个为
true
,则dp[i]
也为true
。初始化:
dp[0] = true
,表示空字符串总是可以被表示。目标:检查
dp[s.length()]
的值,如果为true
,表示整个字符串s
可以用字典中的一个或多个单词拼接得出。
示例代码
def wordBreak(s, wordDict):
word_set = set(wordDict) # 将字典转换为集合,便于查找
dp = [False] * (len(s) + 1)
dp[0] = True # 空字符串总是可以被表示
for i in range(1, len(s) + 1):
for j in range(i):
if dp[j] and s[j:i] in word_set:
dp[i] = True
break # 一旦找到一个有效的分割点,就可以停止查找
return dp[-1]
解释
- 循环遍历字符串
s
的所有子串。- 对于每个子串
s[j:i]
,如果dp[j]
为true
且子串在wordDict
中,说明找到了一个有效的分割点j
,将dp[i]
设置为true
。- 最终,
dp[-1]
(或dp[len(s)]
)的值会告诉我们是否可以用字典中的单词拼接出整个字符串s
。这种方法通过动态规划避免了重复计算和不必要的搜索,有效地解决了问题。