203. 移除链表元素
题目:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
解题思路
- 时间复杂度:O(n)
- 空间复杂度:O(1)
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
if head is None:
return head
dummy = ListNode(-1, next=head)
p = dummy
# 删除一个节点需要它的前驱
while p.next is not None:
if p.next.val == val: # 注意:判断p.next.val节点的值(p=dummy时,p.next是第一个节点)
p.next = p.next.next
else:
p = p.next
return dummy.next
总结
这道题需要我们仔细思考链表的操作细节,尤其是对头节点和中间节点的处理。使用虚拟头节点可以简化代码,并统一处理头节点被删除的情况。
707. 设计链表
题目:
实现 `MyLinkedList` 类:
- `MyLinkedList()` 初始化 `MyLinkedList` 对象。
- `int get(int index)` 获取链表中下标为 `index` 的节点的值。如果下标无效,则返回 `-1` 。
- `void addAtHead(int val)` 将一个值为 `val` 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
- `void addAtTail(int val)` 将一个值为 `val` 的节点追加到链表中作为链表的最后一个元素。
- `void addAtIndex(int index, int val)` 将一个值为 `val` 的节点插入到链表中下标为 `index` 的节点之前。如果 `index` 等于链表的长度,那么该节点会被追加到链表的末尾。如果 `index` 比长度更大,该节点将 **不会插入** 到链表中。
- `void deleteAtIndex(int index)` 如果下标有效,则删除链表中下标为 `index` 的节点。
解题思路
实现单向链表,即每个节点仅存储本身的值和后继节点。除此之外,我们还需要一个虚拟节点dummy作为头节点,和一个 size 参数保存有效节点数。额外维护一个tail节点,方便删除尾部节点。
- 时间复杂度:插入删除O(1),查找O(n)
- 空间复杂度:O(1)
class ListNode:
def __init__(self, val, next=None):
self.val = val
self.next = next
class MyLinkedList:
def __init__(self):
self.dummy = ListNode(-1)
self.size = 0
self.tail = self.dummy
def get(self, index: int) -> int:
if index < 0 or index >= self.size:
return -1
current = self.dummy.next
for i in range(index):
current = current.next
return current.val
def addAtHead(self, val: int) -> None:
new_node = ListNode(val, self.dummy.next)
self.dummy.next = new_node
if self.size == 0:
self.tail = new_node
self.size += 1
def addAtTail(self, val: int) -> None:
new_node = ListNode(val)
self.tail.next = new_node
self.tail = new_node
self.size += 1
def addAtIndex(self, index: int, val: int) -> None:
''' 插入节点到idx之前 '''
if index<0 or index>self.size: # =size时插入到末尾
return
# 找到idx的前驱
cur = self.dummy
for i in range(index):
cur = cur.next
# 插入节点
node = ListNode(val, cur.next)
cur.next = node
# 处理tail和size
if index == self.size:
self.tail = node
self.size += 1
def deleteAtIndex(self, index: int) -> None:
''' 删除第idx个节点 '''
if index<0 or index>= self.size:
return
# 找到idx的前驱
cur = self.dummy
for i in range(index):
cur = cur.next
# 删除节点
cur.next = cur.next.next
# 处理tail和size
if index == self.size-1:
self.tail = cur
self.size -= 1
总结
这道题的主要难点和知识点可以总结为以下几点:
-
1 虚拟头节点的使用: 使用虚拟头节点可以简化链表的操作,避免对头节点的特殊处理。
-
2 维护
size和tail指针: 通过维护链表的大小size和尾部指针tail,可以方便地进行插入和删除操作。 -
3 定位目标节点的前驱节点: 在插入和删除时,需要先找到目标节点的前驱节点,然后进行相应的操作。这需要遍历链表,时间复杂度与操作的位置有关。
-
4 边界条件的处理: 需要仔细考虑一些边界条件,比如插入时
index等于链表长度的情况,或者删除时index超出链表长度的情况。
206. 反转链表
题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
解题思路1: 递归
首先确保有两个节点;然后按照函数定义反转第二个开始的节点,并返回last节点作为反转链表的头节点;最后把head作为尾节点,接在second节点的后面。
- 时间复杂度: O(n), 要递归处理链表的每个节点
- 空间复杂度: O(n), 递归调用了 n 层栈空间
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 确保两个节点以上
if head is None or head.next is None:
return head
# 反转第二个节点开始的链表
last_node = self.reverseList(head.next)
# 第二个节点变成倒数第二个,指向head
head.next.next = head
head.next = None
return last_node
解题思路2:迭代(双指针)
使用双指针挨个反转相邻的两个节点。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head is None or head.next is None:
return head
prev = None
cur = head
while cur is not None:
nxt = cur.next
cur.next = prev
prev= cur
cur = nxt
# 注意:cur迭代到None了,此时prev在last node位置
return prev
总结
两种解法各有优缺点:
- 递归解法代码更加简洁易懂,但需要额外的栈空间,在链表很长的情况下可能会导致栈溢出。
- 迭代解法不需要额外的栈空间,但代码相对复杂一些。