1. 链表
文章目录
写在前面
本系列笔记主要作为笔者刷题的题解,所用的语言为Python3
,若于您有助,不胜荣幸。
1.1 理论基础
链表[linked list]是一种线性的数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”,即“指针”,相互连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。链表的引用设计使得各个节点可以分散存储在内存的各处,它们的内存地址无需连续。链表中的最小的单元称为节点:
class ListNode:
def __init__(self, val: int):
self.val: int = val
self.next: ListNode | None = next
链表还有其他的常见结构:
- 单向链表:单链表就是普通、常见的链表。
- 环形(循环)链表:循环链表就是链表首尾相连,循环链表可以用于解决约瑟夫环的问题。
- 双向链表:双向链表中,每一个节点有两个指针,一个指针指向下一个节点,一个指针指向上一个节点,双向链表既可以向前查询也可以向后查询。
链表的结构使得在链表中插入节点变得非常容易,如果我们需要在相邻的节点之间添加节点,那么我们只需要修改前后节点的“指针”即可,时间复杂度为 O ( 1 ) \mathcal{O}(1) O(1)。
Operation | Time Complexity |
---|---|
插入、删除 | O ( 1 ) \mathcal{O}(1) O(1) |
查找 | O ( n ) \mathcal{O}(n) O(n) |
1.2 移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
在链表中针对头节点和非头节点,移除元素的方式是不一样的,为了保证一致性,我们可以采用虚拟头节点的方法。
问题的关键在于为什么要让current
临时指针的初始值和链表的头节点head
保持一致,i.e.current=head or current=dummyhead
?而不是使用current=head.next or current=dummyhead.next
,这是因为当我们使用current=head.next
时,由于时单向列表,我们无法访问current
的前一个节点,这样就无法对其前面进行操作。
# 一、 常规方法,将头节点和其他节点分开处理
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
while head and head.val == val:
head = head.next
current = head
while current and current.next:
if current.next.val == val:
current.next = current.next.next
else:
current = current.next
return head
# 二、虚拟头节点法
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
dummyhead: ListNode = ListNode(next=head) # 创建一个虚拟的链表指针,指向链表的头部
current = dummyhead
while current.next:
if current.next.val == val:
current.next = current.next.next
else:
current = current.next
return dummyhead.next
1.3 设计链表
如何判断自己的循环边界条件是否正确,我们可以取极端情况来进行查看,当index=0
时,我们判断条件是否满足从而来判断自己的边界条件是否正确。
class ListNode:
def __init__(self, val:int = 0, next:Optional[ListNode] = None):
self.val: int = val
self.next: Optional[ListNode] = next
class MyLinkedList:
def __init__(self):
self.dummy_head: Optional[ListNode] = ListNode()
self.size: int = 0
def get(self, index: int) -> int:
# 保证index是合法的
if index < 0 or index >= self.size:
return -1
cur: Optional[ListNode] = self.dummy_head.next # 这里是dummy_head.next的原因是,极端情况下,index=0时,我们需要能够访问到第一个节点
for _ in range(index):
cur = cur.next
return cur.val
def addAtHead(self, val: int) -> None:
self.dummy_head.next = ListNode(val, self.dummy_head.next)
self.size += 1
def addAtTail(self, val: int) -> None:
cur: Optional[ListNode] = self.dummy_head
while cur.next:
cur = cur.next
cur.next = ListNode(val)
self.size += 1
def addAtIndex(self, index: int, val: int) -> None:
if index < 0 or index > self.size: # 这里不是index >= self.size的原因是,我们可以在index=self.size即,末尾添加Node
return
cur: Optional[ListNode] = self.dummy_head
for _ in range(index):
cur = cur.next
cur.next = ListNode(val, cur.next)
self.size += 1
def deleteAtIndex(self, index: int) -> None:
if index < 0 or index >= self.size:
return
cur: Optional[ListNode] = self.dummy_head
for _ in range(index):
cur = cur.next
cur.next = cur.next.next
self.size -= 1
# Your MyLinkedList object will be instantiated and called as such:
# obj = MyLinkedList()
# param_1 = obj.get(index)
# obj.addAtHead(val)
# obj.addAtTail(val)
# obj.addAtIndex(index,val)
# obj.deleteAtIndex(index)
1.4 翻转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
双指针解法
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre: Optional[ListNode] = None
cur: Optional[ListNode] = head
while cur: # 当cur指向None的时候表示迭代到了链表的尾部,此时迭代完成
temp = cur.next # 临时保存cur.next
cur.next = pre
pre = cur
cur = temp
return pre
递归解法
通过双指针解法,我们可以很简单地将其修改为递归的写法
class Solution:
# 从双指针修改而来的递归写法
def reverse(self, cur: Optional[ListNode], pre: Optional[ListNode]):
if cur == None: return pre
temp = cur.next
cur.next = pre
return self.reverse(temp, cur)
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
return self.reverse(head, None)
1.5 两两交换链表中的节点
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummyhead: ListNode = ListNode(next=head)
cur: Optional[ListNode] = dummyhead
while cur.next and cur.next.next: #当链表数目为偶数时,cur.next=None,当链表数目为奇数时,cur.next.next=None
temp = cur.next
temp1 = cur.next.next.next
cur.next = cur.next.next
cur.next.next = temp
temp.next = temp1
cur = cur.next.next
return dummyhead.next
1.6 删除链表的倒数第N个节点
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummyhead: ListNode = ListNode(next=head)
slow: ListNode = dummyhead
fast: Optional[ListNode] = dummyhead
# 让fast指针先于slow指针n+1步,这样当fast指针指向None的时候,slow指针正好指向倒数第n个节点的前一个节点
for _ in range(n+1):
fast = fast.next
while fast:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return dummyhead.next
1.7链表相交
class Solution:
def getLength(self, head: ListNode) -> int:
"""
获取链表长度
"""
size: int = 0
while head:
head = head.next
size += 1
return size
def moveForward(self, head: ListNode, steps: int) -> None:
"""
移动链表使之对齐
"""
while steps:
head = head.next
steps -= 1
return head
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
lenA: int = self.getLength(headA)
lenB: int = self.getLength(headB)
if lenA > lenB:
headA = self.moveForward(headA, lenA-lenB)
else:
headB = self.moveForward(headB, lenB-lenA)
while headA and headB:
if headA == headB:
return headA
headA = headA.next
headB = headB.next
return None
1.8 环形链表
面对环形链表,我们如何判断是否有环?这是一个很重要的问题,常见的方法是使用双指针。我们使用一个快指针(快指针每次移动2个节点)和一个慢指针(慢指针每次移动1个节点)这样能保证快指针相对于慢指针每次只多移动一个节点,当快指针追上慢指针的时候,则可以说明该链表一定存在环。当我们发现环之后,我们如何来确定环的入口呢?如下图所示,当快慢指针在环内相遇的时候,快指针走的距离是慢指针走的距离的两倍,则一定有如下等式成立
(
x
+
y
)
×
2
=
x
+
y
+
n
(
y
+
z
)
x
=
(
n
−
1
)
(
y
+
z
)
+
z
(x+y)\times2 = x+y+n(y+z) \\ x = (n-1)(y+z)+z
(x+y)×2=x+y+n(y+z)x=(n−1)(y+z)+z
![Image](https://img-blog.csdnimg.cn/direct/adc2c62e400545eda4970462f3b86535.png)
从上式可以看出x=z
,即我们分别让两个指针index1
、index2
从相遇处和头节点处出发,它们相遇的地方就是环的入口。
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
fast: Optional[ListNode] = head
slow: Optional[ListNode] = head
while fast and fast.next:
fast = fast.next.next # 快指针每次移动两个节点
slow = slow.next # 慢指针每次移动一个节点
if fast == slow:
index1 = fast
index2 = head
while index1 != index2: # 寻找环的入口
index1 = index1.next
index2 = index2.next
return index1
return None
我们还可以将历史访问的节点都保存下来,然后判断是否访问过,这种方法比较原始,但简单。
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
nodelist: List = []
while head:
if head in nodelist:
return head
nodelist.append(head)
head = head.next
return None