赞
踩
本文主要参考《算法通关手册》链表篇
链表(Linked List):一种线性表数据结构。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。简单来说,「链表」 是实现线性表链式存储结构的基础。
虽然列表数据结构要求保持数据项的前后相对位置,但这种前后位置的保持,并不要求数据项依次存放在连续的存储空间。如下图,数据项存放位置并没有规则,但如果在数据项之间建立链接指向,就可以保持其前后相对位置。第一个和最后一个数据项需要显示标记出来,一个是队首,一个是队尾,后面再无数据了。
如上图所示,链表通过将一组任意的存储单元串联在一起。其中,每个数据元素占用若干存储单元的组合称为一个链节点。每个节点包含两个部分:
next
:指出直接后继元素所在链节点的地址。在链表中,数据元素之间的逻辑关系是通过指针来间接反映的。逻辑上相邻的数据元素在物理地址上可能相邻,可也能不相邻。其在物理地址上的表现是随机的。
我们先来简单介绍一下链表结构的优缺点:
优点:存储空间不必事先分配,在需要存储空间的时候可以临时申请,不会造成空间的浪费;一些操作的时间效率远比数组高(插入、移动、删除元素等)。
缺点:不仅数据元素本身的数据信息要占用存储空间,指针也需要占用存储空间,链表结构比数组结构的空间开销大。
接下来先来介绍一下除了单链表之外,链表的其他几种类型。
双向链表(Doubly Linked List):链表的一种,也叫做双链表。它的每个链节点中有两个指针,分别指向直接后继和直接前驱。
从双链表的任意一个节点开始,都可以很方便的访问它的前驱节点和后继节点。
循环链表(Circular linked list):链表的一种。它的最后一个链节点指向头节点,形成一个环。
从循环链表的任何一个节点出发都能找到任何其他节点。
接下来我们以单链表为例,介绍一下链表的基本操作。
数据结构的操作一般涉及到增、删、改、查 4 种情况,链表的操作也基本上是这 4 种情况。我们一起来看一下链表的基本操作。
ListNode
类:链节点类。链表是由链节点通过 next
链接而构成的,所以ListNode
类可使用成员变量 val
表示数据元素的值,使用指针变量 next
表示后继指针。LinkedList
类:链表类 。ListkedList
类中只有一个链节点变量 head
用来表示链表的头节点。我们在创建空链表时,只需要把相应的链表头节点变量设置为空链接(None)即可。上述代码如下:
# 链节点类
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 链表类
class LinkedList:
def __init__(self):
self.head = None
建立线性链表的过程是:据线性表的数据元素动态生成链节点,并依次将其连接到链表中。其做法如下:
1
个数据元素开始依次获取表中的数据元素。1
个链节点的地址。建立一个线性链表的时间复杂度为 O ( n ) O(n) O(n),代码如下:
# 根据 data 初始化一个新链表
def create(self, data):
self.head = ListNode(0)
cur = self.head
for i in range(len(data)):
node = ListNode(data[i])
cur.next = node
cur = cur.next
使用一个可以顺着链表指针移动的指针变量 cur
和一个计数器 count
, cur
从头开始遍历链表直至为空,此时计数器的数值就是链表的长度。此操作时间复杂度为
O
(
n
)
O(n)
O(n),代码如下:
# 获取链表长度
def length(self):
count = 0
cur = self.head
while cur:
count += 1
cur = cur.next
return count
在链表中查找值为 val
的位置:链表不能像数组那样进行随机访问,只能从头节点 head
开始,沿着链表一个一个节点逐一进行查找。如果查找成功,返回被查找节点的地址。否则返回 None
。所以查找元素操作的问时间复杂度也是
O
(
n
)
O(n)
O(n),其代码如下:
# 查找元素
def find(self, val):
cur = self.head
while cur:
if val == cur.val:
return cur
cur = cur.next
return None
链表中插入元素操作分为三种:
1
个链节点之前插入值为 val
的链节点。1
个链节点之后插入值为 val
的链节点。i
个链节点之前插入值为 val
的链节点。接下来我们分别讲解一下。
1. 链表头部插入元素
val
的链节点 node
。node
的 next
指针指向链表的头节点 head
。head
指向 node
。这里后面两步顺序不能反了,否则原先的链表就会丢失。
因为在链表头部插入链节点与链表的长度无关,所以该算法的时间复杂度为 O ( 1 ) O(1) O(1)。其代码如下:
# 头部插入元素
def insertFront(self, val):
node = ListNode(val)
node.next = self.head
self.head = node
2. 尾部插入元素
val
的链节点 node
。cur
指向链表的头节点 head
。next
指针移动 cur
指针,从而遍历链表,直到 cur.next == None
。cur.next
指向将新的链节点 node
。该算法的时间复杂度是 O ( n ) O(n) O(n),代码如下:
# 尾部插入元素
def insertRear(self, val):
node = ListNode(val)
cur = self.head
while cur.next:
cur = cur.next
cur.next = node
3. 中间插入元素
cur
和一个计数器 count
。令 cur
指向链表的头节点,count
初始值赋值为 0
。next
指针遍历链表,指针变量 cur
每指向一个链节点,计数器就做一次计数。count == index - 1
时,说明遍历到了第 index - 1
个链节点,此时停止遍历。val
的链节点 node
。node.next
指向 cur.next
。cur.next
指向 node
。该算法的时间复杂度是 O ( n ) O(n) O(n),代码如下:
# 中间插入元素
def insertInside(self, index, val):
count = 0
cur = self.head
while cur and count < index - 1:
count += 1
cur = cur.next
if not cur:
return 'Error'
node = ListNode(val)
node.next = cur.next
cur.next = node
将链表中第 i
个元素值改为 val
:首先要先遍历到第 i
个链节点,然后直接更改第 i
个链节点的元素值。具体做法如下:
cur
和一个计数器 count
。令 cur
指向链表的头节点,count
初始值赋值为 0
。next
指针遍历链表,指针变量 cur
每指向一个链节点,计数器就做一次计数。count == index
时,说明遍历到了第 index
个链节点,此时停止遍历。cur
的值 val
。该算法的时间复杂度是 O ( n ) O(n) O(n), 代码如下:
# 改变元素
def change(self, index, val):
count = 0
cur = self.head
while cur and count < index:
count += 1
cur = cur.next
if not cur:
return 'Error'
cur.val = val
链表的删除元素操作同样分为三种情况:
1
个链节点。1
个链节点。i
个链节点。接下来我们分别讲解一下。
1. 链表头部删除元素
直接将 self.head
沿着 next
指针向右移动一步即可。
算法的时间复杂度为 O ( 1 ) O(1) O(1),代码如下:
# 链表头部删除元素
def removeFront(self):
if self.head:
self.head = self.head.next
2. 链表尾部删除元素
cur
沿着 next
指针移动到倒数第 2
个链节点。next
指针指向 None
即可。该算法的时间复杂度为 O ( n ) O(n) O(n),代码如下:
# 链表尾部删除元素
def removeRear(self):
if not self.head.next:
return 'Error'
cur = self.head
while cur.next.next:
cur = cur.next
cur.next = None
3. 链表中间删除元素
删除链表中第 i
个元素的算法具体步骤如下:
cur
移动到第 i - 1
个位置的链节点。cur
的 next
指针,指向要第 i
个元素的下一个节点即可。该算法的时间复杂度为 O ( n ) O(n) O(n),代码如下:
# 链表中间删除元素
def removeInside(self, index):
count = 0
cur = self.head
while cur.next and count < index - 1:
count += 1
cur = cur.next
if not cur:
return 'Error'
del_node = cur.next
cur.next = del_node.next
到这里,有关链表的基础知识就介绍完了。下面进行一下总结。
有序表实现:search方法
有序表实现:add方法
代码实现:
class Node: def __init__(self,initdata): self.data = initdata self.next = None def getData(self): return self.data def getNext(self): return self.next def setData(self,newdata): self.data = newdata def setNext(self,newnext): self.next = newnext class OrderedList: def __init__(self): self.head = None def search(self,item): current = self.head found = False stop = False while current != None and not found and not stop: if current.getData() == item: found = True else: if current.getData() > item: stop = True else: current = current.getNext() return found def add(self,item): current = self.head previous = None stop = False while current != None and not stop: if current.getData() > item: # 发现插入位置 stop = True else: previous = current current = current.getNext() temp = Node(item) if previous == None: # 表头插入 temp.setNext(self.head) self.head = temp else: # 表中插入 temp.setNext(current) previous.setNext(temp)
「链表」 是实现线性表的链式存储结构的基础。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。
链表最大的优点在于可以灵活的添加和删除元素。链表进行访问元素、改变元素操作的时间复杂度为 O ( n ) O(n) O(n),进行头部插入、头部删除元素操作的时间复杂度是 O ( 1 ) O(1) O(1),进行尾部插入、尾部删除操作的时间复杂度是 O ( n ) O(n) O(n)。普通情况下进行插入、删除元素操作的时间复杂度为 O ( n ) O(n) O(n)。
Python内置的list数据类型,是基于顺序存储实现的,并进行了优化,其基本操作复杂度如下:
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0707 | 设计链表 | Python | 链表 | 中等 |
0083 | 删除排序链表中的重复元素 | Python | 链表 | 简单 |
0082 | 删除排序链表中的重复元素 II | Python | 链表、双指针 | 中等 |
0206 | 反转链表 | Python | 链表 | 简单 |
0092 | 反转链表 II | Python | 链表 | 中等 |
0025 | K 个一组翻转链表 | Python | 递归、链表 | 困难 |
0203 | 移除链表元素 | Python | 链表 | 简单 |
0328 | 奇偶链表 | Python | 链表 | 中等 |
0234 | 回文链表 | Python | 链表、双指针 | 简单 |
0430 | 扁平化多级双向链表 | Python | 链表 | 中等 |
0138 | 复制带随机指针的链表 | Python | 链表、哈希表 | 中等 |
0061 | 旋转链表 | Python | 链表、双指针 | 中等 |
要求:设计实现一个链表,需要支持以下操作:
get(index)
:获取链表中第 index
个节点的值。如果索引无效,则返回 -1
。addAtHead(val)
:在链表的第一个元素之前添加一个值为 val
的节点。插入后,新节点将成为链表的第一个节点。addAtTail(val)
:将值为 val
的节点追加到链表的最后一个元素。addAtIndex(index, val)
:在链表中的第 index
个节点之前添加值为 val
的节点。如果 index
等于链表的长度,则该节点将附加到链表的末尾。如果 index
大于链表长度,则不会插入节点。如果 index
小于 0
,则在头部插入节点。deleteAtIndex(index)
:如果索引 index
有效,则删除链表中的第 index
个节点。说明:
val
值都在
[
1
,
1000
]
[1, 1000]
[1,1000] 之内。LinkedList
库。示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); // 链表变为 1 -> 2 -> 3
linkedList.get(1); // 返回 2
linkedList.deleteAtIndex(1); // 现在链表是 1-> 3
linkedList.get(1); // 返回 3
新建一个带有 val
值 和 next
指针的链表节点类, 然后按照要求对节点进行操作。
class ListNode: def __init__(self, x): self.val = x self.next = None class MyLinkedList: def __init__(self): """ Initialize your data structure here. """ self.size = 0 self.head = ListNode(0) def get(self, index: int) -> int: """ Get the value of the index-th node in the linked list. If the index is invalid, return -1. """ if index < 0 or index >= self.size: return -1 curr = self.head for _ in range(index + 1): curr = curr.next return curr.val def addAtHead(self, val: int) -> None: """ Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. """ self.addAtIndex(0, val) def addAtTail(self, val: int) -> None: """ Append a node of value val to the last element of the linked list. """ self.addAtIndex(self.size, val) def addAtIndex(self, index: int, val: int) -> None: """ Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. """ if index > self.size: return if index < 0: index = 0 self.size += 1 pre = self.head for _ in range(index): pre = pre.next add_node = ListNode(val) add_node.next = pre.next pre.next = add_node def deleteAtIndex(self, index: int) -> None: """ Delete the index-th node in the linked list, if the index is valid. """ if index < 0 or index >= self.size: return self.size -= 1 pre = self.head for _ in range(index): pre = pre.next pre.next = pre.next.next
时间复杂度:
addAtHead(val)
:
O
(
1
)
O(1)
O(1)。get(index)
、addAtTail(val)
、del eteAtIndex(index)
:
O
(
k
)
O(k)
O(k)。
k
k
k 指的是元素的索引。addAtIndex(index, val)
:
O
(
n
)
O(n)
O(n)。
n
n
n 指的是链表的元素个数。空间复杂度: O ( 1 ) O(1) O(1)。
新建一个带有 val
值和 next
指针、prev
指针的链表节点类,然后按照要求对节点进行操作。
class ListNode: def __init__(self, x): self.val = x self.next = None self.prev = None class MyLinkedList: def __init__(self): """ Initialize your data structure here. """ self.size = 0 self.head = ListNode(0) self.tail = ListNode(0) self.head.next = self.tail self.tail.prev = self.head def get(self, index: int) -> int: """ Get the value of the index-th node in the linked list. If the index is invalid, return -1. """ if index < 0 or index >= self.size: return -1 if index + 1 < self.size - index: curr = self.head for _ in range(index + 1): curr = curr.next else: curr = self.tail for _ in range(self.size - index): curr = curr.prev return curr.val def addAtHead(self, val: int) -> None: """ Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. """ self.addAtIndex(0, val) def addAtTail(self, val: int) -> None: """ Append a node of value val to the last element of the linked list. """ self.addAtIndex(self.size, val) def addAtIndex(self, index: int, val: int) -> None: """ Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. """ if index > self.size: return if index < 0: index = 0 if index < self.size - index: prev = self.head for _ in range(index): prev = prev.next next = prev.next else: next = self.tail for _ in range(self.size - index): next = next.prev prev = next.prev self.size += 1 add_node = ListNode(val) add_node.prev = prev add_node.next = next prev.next = add_node next.prev = add_node def deleteAtIndex(self, index: int) -> None: """ Delete the index-th node in the linked list, if the index is valid. """ if index < 0 or index >= self.size: return if index < self.size - index: prev = self.head for _ in range(index): prev = prev.next next = prev.next.next else: next = self.tail for _ in range(self.size - index - 1): next = next.prev prev = next.prev.prev self.size -= 1 prev.next = next next.prev = prev
addAtHead(val)
、addAtTail(val)
:
O
(
1
)
O(1)
O(1)。get(index)
、addAtIndex(index, val)
、del eteAtIndex(index)
:
O
(
m
i
n
(
k
,
n
−
k
)
)
O(min(k, n - k))
O(min(k,n−k))。
n
n
n 指的是链表的元素个数,
k
k
k 指的是元素的索引。 给定一个链表的头节点 head
和一个值 val
,删除链表中值为 val
的节点,并返回新的链表头节点。
示例:
解题思路:
pre
和 cur
。pre
指向前一节点,cur
指向当前节点。val
的节点时,将 pre
的 next
指针指向当前节点的下一个节点,继续递归遍历。没有遇到则将 pre
指针向后移动一步。cur
,继续遍历。需要注意的是:因为要删除的节点可能包含了头节点,我们可以考虑在遍历之前,新建一个头节点,让其指向原来的头节点。这样,最终如果删除的是头节点,则直接删除原头节点,然后最后返回新建头节点的下一个节点即可。
class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
newHead = ListNode(0, head)
newHead.next = head
pre, cur = newHead, head
while cur:
if cur.val == val:
pre.next = cur.next
else:
pre = cur
cur = cur.next
return newHead.next
或者是:
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
newHead = ListNode(0)
newHead.next = head
cur=newHead
while cur.next:
# 如果cur下一个节点值为val,就将其跳过下一节点,否则cur右移
if cur.next.val==val:
cur.next=cur.next.next
else:
cur=cur.next
return newHead.next
给定一个已排序的链表的头 head
,要求删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
示例:
解题思路
cur
遍历链表,如果cur节点和其下一个节点元素值相等,就跳过下一个节点(cur.next = cur.next.next
)。否则,让 cur
继续向后遍历。head
。class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
if head == None:
return head
cur = head
while cur.next:
if cur.val == cur.next.val:
cur.next = cur.next.next
else:
cur = cur.next
return head
给定一个已排序的链表的头 head
,要求删除原始链表中所有重复数字的节点,只留下不同的数字。返回已排序的链表。
示例:
解题思路:
dummy_head
构造一个指向 head
的指针,使得可以防止从 head
开始就是重复元素。cur
表示链表中当前元素,从哑节点开始遍历。cur
的下一个元素和下下一个元素存在时:
temp
保存下一个元素,并使用 temp
向后遍历,跳过所有重复元素,然后令 cur
的下一个元素指向 temp
的下一个元素,继续向后遍历。cur
向右移动一位,继续向后遍历。cur
的下一个元素或者下下一个元素不存在时,说明已经遍历完,则返回哑节点 dummy_head
的下一个节点作为头节点。# Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def deleteDuplicates(self, head: ListNode) -> ListNode: dummy_head = ListNode(-1) dummy_head.next = head cur = dummy_head while cur.next and cur.next.next: if cur.next.val == cur.next.next.val: temp = cur.next while temp and temp.next and temp.val == temp.next.val: temp = temp.next cur.next = temp.next else: cur = cur.next return dummy_head.next
给定一个单链表的头节点 head
。将该单链表进行反转。可以迭代或递归地反转链表。
示例:
使用两个指针 cur
和 pre
进行迭代。pre
指向 cur
前一个节点位置。初始时,pre
指向 None
,cur
指向 head
。
将 pre
和 cur
的前后指针进行交换,指针更替顺序为:
next
指针保存当前节点 cur
的后一个节点,即 next = cur.next
;cur
的后一节点链接,将 cur
的 next
指针指向前一节点 pre
,即 cur.next = pre
;pre
向前移动一步,移动到 cur
位置,即 pre = cur
;cur
向前移动一步,移动到之前 next
指针保存的位置,即 cur = next
。继续执行第 2 步中的 1、2、3、4。
最后等到 cur
遍历到链表末尾,即 cur == None
,时,pre
所在位置就是反转后链表的头节点,返回新的头节点 pre
。
使用迭代法反转链表的示意图如下所示:
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pre = None
cur = head
while cur != None:
next = cur.next
cur.next = pre
pre = cur
cur = next
return pre
head.next
的位置开始调用递归函数,即将 head.next
为头节点的链表进行反转,并返回该链表的头节点。new_head
。head
和 head.next
的指向关系。也就是将 head.next
的next
指针先指向当前节点 head
,即 head.next.next = head
。head
的 next
指针指向 None
,从而实现从链表尾部开始的局部反转。new_head
。使用递归法反转链表的示意图如下所示:
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if head == None or head.next == None:
return head
new_head = self.reverseList(head.next)
head.next.next = head
head.next = None
return new_head
给定单链表的头指针 head
和两个整数 left
和 right
,其中 left <= right
。要求反转从位置 left
到位置 right
的链表节点,返回反转后的链表 。
n
,且
1
≤
n
≤
500
1 \le n \le 500
1≤n≤500。示例:
在「0206. 反转链表」中我们可以通过迭代、递归两种方法将整个链表反转。而这道题要求对链表的部分区间进行反转。我们可以先遍历到需要反转的链表区间的前一个节点,然后对需要反转的链表区间进行迭代反转。最后再返回头节点即可。
但是需要注意一点,如果需要反转的区间包含了链表的第一个节点,那么我们可以事先创建一个哑节点作为链表初始位置开始遍历,这样就能避免找不到需要反转的链表区间的前一个节点。具体解题步骤如下:
dummy_head
构造一个指向 head
的指针,使得可以从 head
开始遍历。使用 index
记录当前元素的序号。start
,初始赋值为 dummy_head
。然后向右逐步移动到需要反转的区间的前一个节点。cur
和 pre
进行迭代。pre
指向 cur
前一个节点位置,即 pre
指向需要反转节点的前一个节点,cur
指向需要反转的节点。初始时,pre
指向 start
,cur
指向 pre.next
。cur
不为空,且 index
在反转区间内时,将 pre
和 cur
的前后指针进行交换,指针更替顺序为:
next
指针保存当前节点 cur
的后一个节点,即 next = cur.next
;cur
的后一节点链接,将 cur
的 next
指针指向前一节点 pre
,即 cur.next = pre
;pre
向前移动一步,移动到 cur
位置,即 pre = cur
;cur
向前移动一步,移动到之前 next
指针保存的位置,即 cur = next
。index
加 1
。4
步中的 1
、2
、3
、4
、5
步。cur
遍历到链表末尾(即 cur == None
)或者遍历到需要反转区间的末尾时(即 index > right
) 时,将反转区间的头尾节点分别与之前保存的需要反转的区间的前一个节点 reverse_start
相连,即 start.next.next = cur
,start.next = pre
。dummy_head.next
。# Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]: dummy_head=ListNode(-1) dummy_head.next=head # 使用一指针start,初始赋值为 dummy_head。向右遍历到left前一个节点 start=dummy_head index=0 while start.next and index<=left-2: start=start.next index+=1 #print(start.val) #使用两个指针 cur 和 pre分别指向left和left前一个节点,开始反转链表 pre,cur=start,start.next while cur and index<right: next=cur.next cur.next=pre pre=cur cur=next index+=1 # 此时pre是反转后的链表头结点,cur是right下一个节点 #print(pre.val,cur.val) # 如果right是最后一个节点,cur指向None,其没有val属性,print报错 start.next.next=cur start.next=pre return dummy_head.next
方法一的缺点是:如果 left 和 right 的区域很大,恰好是链表的头节点和尾节点时,找到 left 和 right 需要遍历一次,反转它们之间的链表还需要遍历一次,虽然总的时间复杂度为
O
(
n
)
O(n)
O(n),但遍历了链表 2 次,可不可以只遍历一次呢?答案是可以的,如下图所示:
整体思想是:在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。下面的图展示了整个流程。
具体来说,可以使用三个指针变量 pre、curr、next 来记录反转的过程中需要的变量,它们的意义如下:
反转后效果如下:
2. 继续穿针引线
第三步同上:
class Solution: def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode: # 设置 dummyNode 是这一类问题的一般做法 dummy_node = ListNode(-1) dummy_node.next = head pre = dummy_node for _ in range(left - 1): pre = pre.next cur = pre.next for _ in range(right - left): next = cur.next cur.next = next.next next.next = pre.next pre.next = next return dummy_node.next
给定一个链表的头节点 head,判断该链表是否为回文链表。
示例:
利用数组,将链表元素值依次存入,然后判断最终的数组是否是回文数。
class Solution:
def isPalindrome(self, head: ListNode) -> bool:
nodes = []
p1 = head
while p1:
nodes.append(p1.val)
p1 = p1.next
return nodes == nodes[::-1]
在数组双指针中我们已经学习过了双指针的概念。这里再来复习一下。
双指针(Two Pointers):指的是在遍历元素的过程中,使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为「对撞时针」。如果两个指针方向相同,则称为「快慢指针」。如果两个指针分别属于不同的数组 / 链表,则称为「分离双指针」。
而在单链表中,因为遍历节点只能顺着 next
指针方向进行,所以对于链表而言,一般只会用到「快慢指针」和「分离双指针」。其中链表的「快慢指针」又分为「起点不一致的快慢指针」和「步长不一致的快慢指针」。这几种类型的双指针所解决的问题也各不相同,下面我们一一进行讲解。
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0141 | 环形链表 | Python | 链表、双指针 | 简单 |
0142 | 环形链表 II | Python | 链表、双指针 | 中等 |
0160 | 相交链表 | Python | 链表、双指针 | 简单 |
0019 | 删除链表的倒数第 N 个结点 | Python | 链表、双指针 | 中等 |
0876 | 链表的中间结点 | Python | 链表、指针 | 简单 |
剑指 Offer 22 | 链表中倒数第k个节点 | Python | 链表、双指针 | 简单 |
0143 | 重排链表 | Python | 栈、递归、链表、双指针 | 中等 |
0002 | 两数相加 | Python | 递归、链表、数学 | 中等 |
0445 | 两数相加 II | Python | 栈、链表、数学 | 中等 |
两个指针从同一侧开始遍历链表,但是两个指针的起点不一样。 快指针 fast
比慢指针 slow
先走 n
步,直到快指针移动到链表尾端时为止。
求解步骤
slow
、fast
。slow
、fast
都指向链表的头节点,即:slow = head
,fast = head
。n
步。然后再同时向右移动快、慢指针。fast == None
)时跳出循环体。伪代码
slow,fast = head,head
while n:
fast = fast.next
n -= 1
while fast:
fast = fast.next
slow = slow.next
适用范围:主要用于找到链表中倒数第 k 个节点、删除链表倒数第 N 个节点等。
给定一个链表的头节点 head
,要求删除链表的倒数第 n
个节点,并且返回链表的头节点。
sz
,其中
1
≤
s
z
≤
30
1 \le sz \le 30
1≤sz≤30。示例:
解题思路:快慢指针
n
步,然后快慢指针同时右移,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 n
个节点位置。将该位置上的节点删除即可。class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
newHead = ListNode(0, head) # val=0,next=head
fast = head
slow = newHead
while n:
fast = fast.next
n -= 1
while fast:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return newHead.next
两个指针从同一侧开始遍历链表,两个指针的起点一样,但是步长不一致。例如,慢指针 slow
每次走 1
步,快指针 fast
每次走两步。直到快指针移动到链表尾端时为止。
求解步骤
slow
、fast
。slow
、fast
都指向链表的头节点。1
步,即 slow = slow.next
。快指针每次移动 2
步,即 fast = fast.next.next
。fast == None
)时跳出循环体。伪代码
fast = head
slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
适用范围:此算法适合寻找链表的中点、判断和检测链表是否有环、找到两个链表的交点等问题。
3.4.1 题目链接: 876. 链表的中间结点 - 力扣(LeetCode)
给定一个单链表的头节点 head
,返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。
1
和 100
之间。示例1:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL
.示例2:
先遍历一遍链表,统计一下节点个数为 n
,再遍历到 n / 2
的位置,返回中间节点。其代码如下:
class Solution:
def middleNode(self, head: ListNode) -> ListNode:
n = 0
curr = head
while curr:
n += 1
curr = curr.next
k = 0
curr = head
while k < n // 2:
k += 1
curr = curr.next
return curr
使用步长不一致的快慢指针进行一次遍历找到链表的中间节点。具体做法如下:
slow
、fast
。slow
、fast
都指向链表的头节点。1
步,即 slow = slow.next
。快指针每次移动 2
步,即 fast = fast.next.next
。fast == Node
)时跳出循环体,此时 slow
指向链表中间位置。slow
指针。class Solution:
def middleNode(self, head: ListNode) -> ListNode:
fast = head
slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
题目链接: 141. 环形链表 - 力扣(LeetCode)
给定一个链表的头节点 head
,判断链表中是否有环。如果有环则返回 True
,否则返回 False
。
pos
为 -1
或者链表中的一个有效索引。示例:
最简单的思路是遍历所有节点,每次遍历节点之前,使用哈希表判断该节点是否被访问过。如果过就说明存在环,如果没访问过则将该节点添加到哈希表中,继续遍历判断。
class Solution:
def hasCycle(self, head: ListNode) -> bool:
nodeset = set()
while head:
if head in nodeset:
return True
nodeset.add(head)
head = head.next
return False
这种方法类似于在操场跑道跑步。两个人从同一位置同时出发,如果跑道有环(环形跑道),那么快的一方总能追上慢的一方。
基于上边的想法,Floyd 用两个指针,一个慢指针(龟)每次前进一步,快指针(兔)指针每次前进两步(两步或多步效果是等价的)。如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if head == None or head.next == None:
return False
slow = head
fast = head.next
while slow != fast:
if fast == None or fast.next == None:
return False
slow = slow.next
fast = fast.next.next
return True
题目链接::142 环形链表 II
给定一个链表的头节点 head
,判断链表中是否有环,如果有环则返回入环的第一个节点,无环则返回 None
。
pos
的值为 -1
或者链表中的一个有效索引。示例:
遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。借助哈希表可以很方便地实现。
# Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]: se=set() cur=head while cur: if cur not in se: se.add(cur) cur=cur.next else: return cur return None
slow
每次前进一步,快指针 fast
每次前进两步(两步或多步效果是等价的)。ans
,和慢指针一起每次移动一步,两个指针相遇的位置即为入口节点。这是因为:
假设入环位置为 A
,快慢指针在 B
点相遇,则相遇时慢指针走了
a
+
b
a + b
a+b 步,快指针走了
a
+
n
(
b
+
c
)
+
b
a + n(b+c) + b
a+n(b+c)+b 步。
因为快指针总共走的步数是慢指针走的步数的两倍,即 2 ( a + b ) = a + n ( b + c ) + b 2(a + b) = a + n(b + c) + b 2(a+b)=a+n(b+c)+b,所以可以推出: a = c + ( n − 1 ) ( b + c ) a = c + (n-1)(b + c) a=c+(n−1)(b+c)。
我们可以发现:从相遇点到入环点的距离 c c c 加上 n − 1 n-1 n−1 圈的环长 b + c b + c b+c 刚好等于从链表头部到入环点的距离。
# Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]: if head==None or head.next==None: return None else: slow,fast=head,head while fast and fast.next: slow=slow.next fast=fast.next.next if fast==slow: ans=head while ans!=slow: ans,slow=ans.next,slow.next return slow return None
分离双指针:两个指针分别属于不同的链表,两个指针分别在两个链表中移动。
求解步骤
left_1
、left_2
。分别指向两个链表的头节点,即:left_1 = list1
,left_2 = list2
。left_1 = left_1.next
、left_2 = left_2.next
。left_1
指针右移,即 left_1 = left_1.next
。left_2
指针右移,即 left_2 = left_2.next
。伪代码模板
left_1 = list1
left_2 = list2
while left_1 and left_2:
if 一定条件 1:
left_1 = left_1.next
left_2 = left_2.next
elif 一定条件 2:
left_1 = left_1.next
elif 一定条件 3:
left_2 = left_2.next
适用范围:分离双指针一般用于有序链表合并等问题。
给定两个升序链表的头节点 list1
和 list2
,要求将其合并为一个升序链表。
list1
和 list2
均按 非递减顺序 排列示例:
利用分离双指针,具体步骤如下:
dummy_head
构造一个头节点,并使用 cur
指向 dummy_head
用于遍历。list1
和 list2
头节点的值,将较小的头节点加入到合并后的链表中。并向后移动该链表的头节点指针。dummy_dead
的下一个链节点 dummy_head.next
作为合并后有序链表的头节点返回。class Solution: def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: dummy_head = ListNode(-1) cur = dummy_head while list1 and list2: if list1.val <= list2.val: cur.next = list1 list1 = list1.next else: cur.next = list2 list2 = list2.next cur = cur.next # 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可 cur.next = list1 if list1 is not None else list2 return dummy_head.next
题目链接: 160. 相交链表- 力扣(LeetCode)
给定两个链表 listA
、listB
。判断两个链表是否相交,相交则返回起始点,不相交则返回 None
。
示例:
判断两个链表是否相交,可以使用哈希集合存储链表节点。
遍历链表 headA,并将其中的每个节点加入哈希集合中。
遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:
null
# Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]: se=set() cur1=headA cur2=headB while cur1: se.add(cur1) cur1=cur1.next while cur2: if cur2 in se: return cur cur2=cur2.next return None
使用双指针的方法,可以将空间复杂度降至 O ( 1 ) O(1) O(1) :
pA
、pB
,分别从链表 listA
、链表 listB
的头节点开始遍历。pA
为空,将其指向listB
的头结点,继续遍历pB
为空,将其指向listA
的头结点,继续遍历pA
和 pB
指向同一个节点或者都为空时,返回它们指向的节点或者 null
。当两个链表相交时:
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
if headA == None or headB == None:
return None
pA = headA
pB = headB
while pA != pB :
pA = pA.next if pA != None else headB
pB = pB.next if pB != None else headA
return pA
此部分内容请参考《算法通关手册》 链表排序篇。
在数组排序中,常见的排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序等。
而对于链表排序而言,因为链表不支持随机访问,访问链表后面的节点只能依靠 next 指针从头部顺序遍历,所以相对于数组排序问题来说,链表排序问题会更加复杂一点。
希尔排序为什么不适合链表排序?
希尔排序:希尔排序中经常涉及到对序列中第i + gap
的元素进行操作,其中gap
是希尔排序中当前的步长。而链表不支持随机访问的特性,导致这种操作不适合链表,因而希尔排序算法不适合进行链表排序。
为什么不建议使用堆排序?
堆排序:堆排序所使用的最大堆 / 最小堆结构本质上是一棵完全二叉树。而完全二叉树适合采用顺序存储结构(数组)。因为数组存储的完全二叉树可以很方便的通过下标序号来确定父亲节点和孩子节点,并且可以极大限度的节省存储空间。
而链表用在存储完全二叉树的时候,因为不支持随机访问的特性,导致其寻找子节点和父亲节点会比较耗时,如果增加指向父亲节点的变量,又会浪费大量存储空间。所以堆排序算法不适合进行链表排序。
如果一定要对链表进行堆排序,则可以使用额外的数组空间表示堆结构。然后将链表中各个节点的值依次添加入堆结构中,对数组进行堆排序。排序后,再按照堆中元素顺序,依次建立链表节点,构建新的链表并返回新链表头节点。
链表插入排序
1. 算法步骤:
先使用哑节点 dummy_head
构造一个指向 head
的指针,使得可以从 head
开始遍历。
维护 sorted_list
为链表的已排序部分的最后一个节点,初始时,sorted_list = head
。
维护 prev
为插入元素位置的前一个节点,维护 cur
为待插入元素。初始时,prev = head
,cur = head.next
。
比较 sorted_list
和 cur
的节点值。
sorted_list.val <= cur.val
,说明 cur
应该插入到 sorted_list
之后,则将 sorted_list
后移一位。sorted_list.val > cur.val
,说明 cur
应该插入到 head
与 sorted_list
之间。则使用 prev
从 head
开始遍历,直到找到插入 cur
的位置的前一个节点位置。然后将 cur
插入。令 cur = sorted_list.next
,此时 cur
为下一个待插入元素。
重复 4、5 步骤,直到 cur
遍历结束为空。返回 dummy_head
的下一个节点。
2. 实现代码
class Solution: def insertionSort(self, head: ListNode): if not head or not head.next: return head dummy_head = ListNode(-1) dummy_head.next = head sorted_list = head cur = head.next while cur: if sorted_list.val <= cur.val: # 将 cur 插入到 sorted_list 之后 sorted_list = sorted_list.next else: prev = dummy_head while prev.next.val <= cur.val: prev = prev.next # 将 cur 到链表中间 sorted_list.next = cur.next cur.next = prev.next prev.next = cur cur = sorted_list.next return dummy_head.next def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.insertionSort(head)
1. 算法步骤
fast = head.next
、slow = head
,让 fast
每次移动 2
步,slow
移动 1
步,移动到链表末尾,从而找到链表中心链节点,即 slow
。left_head
和 right_head
,并从中心位置将其断开,即 slow.next = None
。dummy_head
构造一个头节点,并使用 cur
指向 dummy_head
用于遍历。left
和 right
的值大小。将较小的头节点加入到合并后的链表中,并向后移动该链表的头节点指针。dummy_dead
的下一个链节点 dummy_head.next
作为合并后的头节点返回。2. 实现代码
class Solution: def merge(self, left, right): # 归并环节 dummy_head = ListNode(-1) cur = dummy_head while left and right: if left.val <= right.val: cur.next = left left = left.next else: cur.next = right right = right.next cur = cur.next if left: cur.next = left elif right: cur.next = right return dummy_head.next def mergeSort(self, head: ListNode): # 分割环节 if not head or not head.next: return head # 快慢指针找到中心链节点 slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next # 断开左右链节点 left_head, right_head = head, slow.next slow.next = None # 归并操作 return self.merge(self.mergeSort(left_head), self.mergeSort(right_head)) def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: return self.mergeSort(head)
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0148 | 排序链表 | Python | 链表、双指针、分治、排序、归并排序 | 中等 |
0021 | 合并两个有序链表 | Python | 递归、链表 | 简单 |
0023 | 合并K个升序链表 | Python | 链表、分治、堆(优先队列)、归并排序 | 困难 |
0147 | 对链表进行插入排序 | Python | 链表、排序 | 中等 |
题目链接:0148. 排序链表
给定链表的头节点 head
,请按照升序排列并返回排序后的链表。
本题使用链表的冒泡排序、选择排序、插入排序、快速排序都会超时。使用桶排序、归并排序、计数排序则可以通过,基数排序只适用于非负数的情况。归并排序代码上面已给出,其它排序算法请参考《算法通关手册》
题目链接: 023 合并K个升序链表
给定一个链表数组,每个链表都已经按照升序排列。要求将所有链表合并到一个升序链表中,返回合并后的链表。
首先,何在
O
(
n
)
O(n)
O(n) 的时间代价以及
O
(
1
)
O(1)
O(1) 的空间代价里完成两个有序链表的合并?为了达到空间代价是
O
(
1
)
O(1)
O(1),我们的宗旨是「原地调整链表元素的next
指针完成合并」,具体参考本文的《3.4.2 合并两个有序链表》。
class Solution: def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: dummy_head = ListNode(-1) cur = dummy_head while list1 and list2: if list1.val <= list2.val: cur.next = list1 list1 = list1.next else: cur.next = list2 list2 = list2.next cur = cur.next # 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可 cur.next = list1 if list1 is not None else list2 return dummy_head.next
我们可以想到一种最朴素的方法:用一个变量 ans
来维护以及合并链表,第 i
次循环把第 i
个链表和 ans
合并,答案保存到 ans
中。
# Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]: if len(lists)==0: return None else: ans=lists[0] for i in lists[1:]: ans=self.mergeTwoLists(ans,i) return ans def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: dummy_head = ListNode(-1) cur = dummy_head while list1 and list2: if list1.val <= list2.val: cur.next = list1 list1 = list1.next else: cur.next = list2 list2 = list2.next cur = cur.next # 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可 cur.next = list1 if list1 is not None else list2 return dummy_head.next
分而治之的思想。将链表数组不断二分,转为规模为二分之一的子问题,然后再进行归并排序。
class Solution: def merge_sort(self, lists: List[ListNode], left: int, right: int) -> ListNode: if left == right: return lists[left] mid = left + (right - left) // 2 node_left = self.merge_sort(lists, left, mid) node_right = self.merge_sort(lists, mid + 1, right) return self.merge(node_left, node_right) def merge(self, a: ListNode, b: ListNode) -> ListNode: root = ListNode(-1) cur = root while a and b: if a.val < b.val: cur.next = a a = a.next else: cur.next = b b = b.next cur = cur.next if a: cur.next = a if b: cur.next = b return root.next def mergeKLists(self, lists: List[ListNode]) -> ListNode: if not lists: return None size = len(lists) return self.merge_sort(lists, 0, size - 1)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。