1 链表理论基础
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。链表的入口节点称为链表的头结点也就是head。
链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
1.1 链表的类型
- 单链表:指针域只能指向下一个节点。
- 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。因此,双链表既可以向前查询也可以向后查询。
- 循环链表:链表首尾相连,可以用来解决约瑟夫环问题。
1.2 链表的定义
class ListNode:
def __init__(self, val = 0, next = None):
self.val = val
self.next = next
1.3 链表的操作
- 删除节点
- 添加节点
1.4 性能分析
2 移除链表元素
题目:给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
思路
如果删除的是头结点该怎么办呢?
- 直接使用原来的链表来进行删除操作
只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点;但因为在单链表中移除头结点和移除其他节点的操作方式不一样,需要单独写一段逻辑来处理移除头结点的情况。
- 设置一个虚拟头结点在进行删除操作
可以设置一个虚拟头结点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
class Solution: def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]: dummy_head = ListNode(next = head) # 创建虚拟头部节点以简化删除过程 curr = dummy_head while curr.next: # 遍历列表并删除值为val的节点 if curr.next.val == val: # 下一个节点为val curr.next = curr.next.next else: curr = curr.next
return dummy_head.next
3 设计链表
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
思路
实现单向链表,即每个节点仅存储本身的值和后继节点。除此之外,我们还需要一个虚拟节点作为头节点,和一个 size 参数保存有效节点数。如下图所示。
class ListNode: def __init__(self, val = 0, next = None): self.val = val self.next = next
class MyLinkedList:
def __init__(self): self.dummy_head = ListNode() # 虚拟头节点 self.size = 0 def get(self, index: int) -> int: if index < 0 or index >= self.size: return -1 curr = self.dummy_head.next for i in range(index): curr = curr.next return curr.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: curr = self.dummy_head while curr.next: curr = curr.next curr.next = ListNode(val) self.size += 1 def addAtIndex(self, index: int, val: int) -> None: if index < 0 or index > self.size: return curr = self.dummy_head for i in range(index): curr = curr.next curr.next = ListNode(val, curr.next) self.size += 1 def deleteAtIndex(self, index: int) -> None: if index < 0 or index >= self.size: return curr = self.dummy_head for i in range(index): curr = curr.next curr.next = curr.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)
4 反转链表
题目:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = []
输出:[]
提示:
- 链表中节点的数目范围是
[0, 5000]
5000 <= Node.val <= 5000
思路
只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
- 双指针法
- 递归法
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
cur = head
pre = None
while cur:
temp = cur.next # 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur.next = pre # 反转
# 更新pre、cur指针
pre = cur
cur = temp
return pre
时间复杂度: O(n),空间复杂度: O(1)
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
return self.reverse(head, None)
def reverse(self, cur: ListNode, pre: ListNode) -> ListNode:
if cur == None:
return pre
temp = cur.next
cur.next = pre
return self.reverse(temp, cur)
时间复杂度: O(n),空间复杂度: O(n)
5 两两交换链表中的节点
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
思路
- 虚拟头节点
- 递归法
class Solution: def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]: dummy_head = ListNode(next = head) curr = dummy_head
while curr.next and curr.next.next: first = curr.next second = first.next tmp = second.next curr.next = second second.next = first first.next = tmp curr = first return dummy_head.next
时间复杂度:O(n),空间复杂度:O(1)
class Solution: def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]: if head is None or head.next is None: return head
first = head second = head.next next = second.next second.next = first first.next = self.swapPairs(next) # 将以next为head的后续链表两两交换 return second
时间复杂度:O(n),空间复杂度:O(1)
6 删除链表的倒数第N个节点
题目:给你一个链表,删除链表的倒数第 n
**个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
思路
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy_head = ListNode(next = head)
fast = slow = dummy_head
for i in range(n):
fast = fast.next
while fast.next:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return dummy_head.next
时间复杂度: O(n),空间复杂度: O(1)
7 链表相交
题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
思路
简单来说,就是求两个链表交点节点的指针。注意,交点不是数值相等,而是指针相等。如果有交点,那么右端必定共享相同节点指针。可以求出两个链表的长度并求差值,然后让curA移动到和curB 末尾对齐的位置(即右对齐)。
此时就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。否则循环退出返回空指针。
class Solution: def get_size(self, head: ListNode) -> int: size = 0 while head: head = head.next size += 1 return size
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: # 如果相交,右对齐后从右往左必定有相同元素 lenA = self.get_size(headA) lenB = self.get_size(headB) currA, currB = headA, headB # 保证A是最长链表 if lenB > lenA: lenA, lenB = lenB, lenA currA, currB = currB, currA # 右对齐 diff = lenA - lenB for _ in range(diff): currA = currA.next while currA: # 遍历curA 和 curB,遇到相同则直接返回 if currA == currB: return currA currA = currA.next currB = currB.next return None
8 环形链表**
题目:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。不允许修改给定的链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
示例 2:
输入:head = [1], pos = -1
输出:返回 null
思路
- 快慢指针法
- 判断链表是否有环
- 如果有环,如何找到这个环的入口
- 集合法
主要考察两个知识点:
判断链表是否有环
使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
如果有环,如何找到这个环的入口**
假设从头结点到环形入口节点的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点的节点数为y。 从相遇节点再到环形入口节点节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数。
因为fast指针一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:(x + y) * 2 = x + y + n (y + z)
要找环形的入口,那么要求的是x:x = n (y + z) - y
,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
拿n为1的情况举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。此时公式化解为 x = z
,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是环形入口的节点。
n如果大于1,就是fast指针在环形转n圈之后才遇到 slow指针。这种情况和n为1的时候效果是一样的,只不过index1 指针在环里多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
class Solution: def detectCycle(self, head: ListNode) -> ListNode: slow = head fast = head
while fast and fast.next: slow = slow.next fast = fast.next.next # If there is a cycle, the slow and fast pointers will eventually meet if slow == fast: # Move one of the pointers back to the start of the list slow = head while slow != fast: slow = slow.next fast = fast.next return slow # If there is no cycle, return None return None
时间复杂度: O(n),空间复杂度: O(1)
class Solution: def detectCycle(self, head: ListNode) -> ListNode: visited = set()
while head: if head in visited: return head visited.add(head) head = head.next return None