LeetCode练习三:链表

news/2024/4/19 14:27:28/文章来源:https://blog.csdn.net/qq_56591814/article/details/129119049

文章目录

    • 一、链表基础
      • 1.1 无序表(UnorderedList)
        • 1.1.2 双向链表
        • 1.1.3 循环链表
      • 1.2 链表的基本操作
        • 1.2.1 定义链表结构
        • 1.2.2 建立线性链表
        • 1.2.3 求线性链表的长度
        • 1.2.4 查找元素
        • 1.2.5 插入元素
        • 1.2.6 改变元素
        • 1.2.7 删除元素
      • 1.3 有序表OrderedList
      • 1.4 链表总结
    • 二、 链表基础题目
      • 2.1 设计链表(707)
        • 2.1.1单链表
        • 2.1.2 双链表
      • 2.2 移除链表元素(203)
      • 2.3 删除排序链表中的重复元素(83)
      • 2.4 删除排序链表中的重复元素 (82)
      • 2.5 反转链表
        • 2.5.1 迭代
        • 2.5.2 递归
      • 2.6 反转链表二(92)
        • 2.6.1 迭代
        • 2.6.2 头插法,一次遍历(官方题解)
      • 2.7 回文链表
    • 三、链表双指针
      • 3.1 双指针简介
      • 3.2 起点不一致的快慢指针
        • 3.2.1 算法简介
        • 3.2.2 删除链表的倒数第 N 个结点
      • 3.3 步长不一致的快慢指针
        • 3.3.1 算法原理
        • 3.3.2 求链表的中间结点
          • 3.3.2.1 单指针
          • 3.3.2.2 快慢指针
        • 3.3.3 判断链表中是否含有环
          • 3.3.3.1 哈希表
          • 3.3.3.2 快慢指针(Floyd 判圈算法)
        • 3.3.4 求环形链表入环位置
          • 3.3.4.1 哈希表
          • 3.3.4.2 快慢指针(Floyd 判圈算法)
      • 3.4 分离双指针
        • 3.4.1 算法原理
        • 3.4.2 合并两个有序链表
        • 3.4.3 相交链表
          • 3.4.3.1 哈希表
          • 3.4.3.2 分离双指针
    • 四、链表排序
      • 4.1 基础知识
      • 4.2 链表插入排序
      • 4.3 链表归并排序
      • 4.4 链表排序题目
        • 4.4.1 排序链表
        • 4.4.2 合并K个升序链表
          • 4.4.2.1 顺序合并
          • 4.4.2.2 分治合并

本文主要参考《算法通关手册》链表篇

一、链表基础

1.1 无序表(UnorderedList)

   链表(Linked List):一种线性表数据结构。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。简单来说,「链表」 是实现线性表链式存储结构的基础。

  虽然列表数据结构要求保持数据项的前后相对位置,但这种前后位置的保持,并不要求数据项依次存放在连续的存储空间。如下图,数据项存放位置并没有规则,但如果在数据项之间建立链接指向,就可以保持其前后相对位置。第一个和最后一个数据项需要显示标记出来,一个是队首,一个是队尾,后面再无数据了。

  • 插入删除元素时需要移动表中元素,而不是修改指针,顺序表也是随机存取数据。
    在这里插入图片描述

如上图所示,链表通过将一组任意的存储单元串联在一起。其中,每个数据元素占用若干存储单元的组合称为一个链节点。每个节点包含两个部分:

  • 元素值
  • 后继指针 next:指出直接后继元素所在链节点的地址。

在链表中,数据元素之间的逻辑关系是通过指针来间接反映的。逻辑上相邻的数据元素在物理地址上可能相邻,可也能不相邻。其在物理地址上的表现是随机的。

我们先来简单介绍一下链表结构的优缺点:

  • 优点:存储空间不必事先分配,在需要存储空间的时候可以临时申请,不会造成空间的浪费;一些操作的时间效率远比数组高(插入、移动、删除元素等)。

  • 缺点:不仅数据元素本身的数据信息要占用存储空间,指针也需要占用存储空间,链表结构比数组结构的空间开销大。

接下来先来介绍一下除了单链表之外,链表的其他几种类型。

1.1.2 双向链表

双向链表(Doubly Linked List):链表的一种,也叫做双链表。它的每个链节点中有两个指针,分别指向直接后继和直接前驱。

从双链表的任意一个节点开始,都可以很方便的访问它的前驱节点和后继节点。

1.1.3 循环链表

循环链表(Circular linked list):链表的一种。它的最后一个链节点指向头节点,形成一个环。

从循环链表的任何一个节点出发都能找到任何其他节点。

接下来我们以单链表为例,介绍一下链表的基本操作。

1.2 链表的基本操作

  数据结构的操作一般涉及到增、删、改、查 4 种情况,链表的操作也基本上是这 4 种情况。我们一起来看一下链表的基本操作。

1.2.1 定义链表结构

  • ListNode 类:链节点类。链表是由链节点通过 next 链接而构成的,所以ListNode 类可使用成员变量 val 表示数据元素的值,使用指针变量 next 表示后继指针。
  • LinkedList 类:链表类 。ListkedList 类中只有一个链节点变量 head 用来表示链表的头节点。我们在创建空链表时,只需要把相应的链表头节点变量设置为空链接(None)即可。

上述代码如下:

# 链节点类
class ListNode:def __init__(self, val=0, next=None):self.val = valself.next = next# 链表类
class LinkedList:def __init__(self):self.head = None

1.2.2 建立线性链表

建立线性链表的过程是:据线性表的数据元素动态生成链节点,并依次将其连接到链表中。其做法如下:

  1. 从所给线性表的第 1 个数据元素开始依次获取表中的数据元素。
  2. 每获取一个数据元素,就为该数据元素生成一个新节点,将新节点插入到链表的尾部。
  3. 插入完毕之后返回第 1 个链节点的地址。

建立一个线性链表的时间复杂度为 O(n)O(n)O(n),代码如下:

# 根据 data 初始化一个新链表
def create(self, data):self.head = ListNode(0)cur = self.headfor i in range(len(data)):node = ListNode(data[i])cur.next = nodecur = cur.next

1.2.3 求线性链表的长度

  使用一个可以顺着链表指针移动的指针变量 cur 和一个计数器 count cur 从头开始遍历链表直至为空,此时计数器的数值就是链表的长度。此操作时间复杂度为 O(n)O(n)O(n),代码如下:

# 获取链表长度
def length(self):count = 0cur = self.headwhile cur:count += 1cur = cur.next return count

1.2.4 查找元素

  在链表中查找值为 val 的位置:链表不能像数组那样进行随机访问,只能从头节点 head 开始,沿着链表一个一个节点逐一进行查找。如果查找成功,返回被查找节点的地址。否则返回 None。所以查找元素操作的问时间复杂度也是 O(n)O(n)O(n),其代码如下:

# 查找元素
def find(self, val):cur = self.headwhile cur:if val == cur.val:return curcur = cur.nextreturn None

1.2.5 插入元素

链表中插入元素操作分为三种:

  • 链表头部插入元素:在链表第 1 个链节点之前插入值为 val 的链节点。
  • 链表尾部插入元素:在链表最后 1 个链节点之后插入值为 val 的链节点。
  • 链表中间插入元素:在链表第 i 个链节点之前插入值为 val 的链节点。

接下来我们分别讲解一下。

1. 链表头部插入元素

  1. 先创建一个值为 val 的链节点 node
  2. 然后将 nodenext 指针指向链表的头节点 head
  3. 再将链表的头节点 head 指向 node

这里后面两步顺序不能反了,否则原先的链表就会丢失。

  因为在链表头部插入链节点与链表的长度无关,所以该算法的时间复杂度为 O(1)O(1)O(1)。其代码如下:

# 头部插入元素
def insertFront(self, val):node = ListNode(val)node.next = self.headself.head = node

2. 尾部插入元素

  1. 先创建一个值为 val 的链节点 node
  2. 使用指针 cur 指向链表的头节点 head
  3. 通过链节点的 next 指针移动 cur 指针,从而遍历链表,直到 cur.next == None
  4. cur.next 指向将新的链节点 node

  该算法的时间复杂度是 O(n)O(n)O(n),代码如下:

# 尾部插入元素
def insertRear(self, val):node = ListNode(val)cur = self.headwhile cur.next:cur = cur.nextcur.next = node

3. 中间插入元素

  1. 使用指针变量 cur 和一个计数器 count。令 cur 指向链表的头节点,count 初始值赋值为 0
  2. 沿着链节点的 next 指针遍历链表,指针变量 cur 每指向一个链节点,计数器就做一次计数。
  3. count == index - 1 时,说明遍历到了第 index - 1 个链节点,此时停止遍历。
  4. 创建一个值为 val 的链节点 node
  5. node.next 指向 cur.next
  6. 然后令 cur.next 指向 node

  该算法的时间复杂度是 O(n)O(n)O(n),代码如下:

# 中间插入元素
def insertInside(self, index, val):count = 0cur = self.headwhile cur and count < index - 1:count += 1cur = cur.nextif not cur:return 'Error'node = ListNode(val)node.next = cur.nextcur.next = node

1.2.6 改变元素

  将链表中第 i 个元素值改为 val:首先要先遍历到第 i 个链节点,然后直接更改第 i 个链节点的元素值。具体做法如下:

  1. 使用指针变量 cur 和一个计数器 count。令 cur 指向链表的头节点,count 初始值赋值为 0
  2. 沿着链节点的 next 指针遍历链表,指针变量 cur 每指向一个链节点,计数器就做一次计数。
  3. count == index 时,说明遍历到了第 index 个链节点,此时停止遍历。
  4. 直接更改 cur 的值 val

  该算法的时间复杂度是 O(n)O(n)O(n), 代码如下:

# 改变元素
def change(self, index, val):count = 0cur = self.headwhile cur and count < index:count += 1cur = cur.nextif not cur:return 'Error'cur.val = val

1.2.7 删除元素

链表的删除元素操作同样分为三种情况:

  • 链表头部删除元素:删除链表的第 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.headwhile cur.next.next:cur = cur.nextcur.next = None

3. 链表中间删除元素

删除链表中第 i 个元素的算法具体步骤如下:

  1. 先使用指针变量 cur 移动到第 i - 1 个位置的链节点。
  2. 然后将 curnext 指针,指向要第 i 个元素的下一个节点即可。

该算法的时间复杂度为 O(n)O(n)O(n),代码如下:

# 链表中间删除元素
def removeInside(self, index):count = 0cur = self.headwhile cur.next and count < index - 1:count += 1cur = cur.nextif not cur:return 'Error'del_node = cur.nextcur.next = del_node.next

到这里,有关链表的基础知识就介绍完了。下面进行一下总结。

1.3 有序表OrderedList

在这里插入图片描述

  • 在实现有序表时,需要记住数据项的相对位置,取决于他们之间的“大小”比较,Node定义相同,OrderedList也设置一个head来保存链表表头的引用。
  • 对于isEmpty(),size(),remove()方法与节点次序无关,其实现与UnorderedList相同。
  • search和add方法需要修改。无序表中,需要search的数据项不存在,则需要搜索整个链表。而有序表中,可以利用链表有序排列的特点,节省搜索时间。

有序表实现:search方法

  • 在无序表的search中,如果需要查找的数据项不存在,则会搜遍整个链表,直到表尾。
  • 对于有序表来说,可以利用链表节点有序排列的特性,来为search节省不存在数据项的查找时间。一旦当前节点的数据项大于所要查找的数据项,则说明链表后面已经不可能再有要查找的数据项,可以直接返回False。

有序表实现:add方法

  • add方法必须保证加入的数据项添加在合适的位置,以维护整个链表的有序性。
  • 从头找到第一个比添加项大的数据项,将添加项插到该数据项前面。
  • 跟remove方法类似,引入一个previous,跟随当前节点current。

代码实现

class Node:def __init__(self,initdata):self.data = initdataself.next = Nonedef getData(self):return self.datadef getNext(self):return self.nextdef setData(self,newdata):self.data = newdatadef setNext(self,newnext):self.next = newnextclass OrderedList:def __init__(self):self.head = Nonedef search(self,item):current = self.headfound = Falsestop = Falsewhile current != None and not found and not stop:if current.getData() == item:found = Trueelse:if current.getData() > item:stop = Trueelse:current = current.getNext()return founddef add(self,item):current = self.headprevious = Nonestop = Falsewhile current != None and not stop:if current.getData() > item: 		# 发现插入位置stop = Trueelse:previous = currentcurrent = current.getNext()temp = Node(item)if previous == None:  				   # 表头插入temp.setNext(self.head)self.head = tempelse:									# 表中插入temp.setNext(current)previous.setNext(temp)

1.4 链表总结

  「链表」 是实现线性表的链式存储结构的基础。它使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。

  链表最大的优点在于可以灵活的添加和删除元素。链表进行访问元素、改变元素操作的时间复杂度为 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删除排序链表中的重复元素 IIPython链表、双指针中等
0206反转链表Python链表简单
0092反转链表 II Python链表中等
0025K 个一组翻转链表Python递归、链表困难
0203移除链表元素Python链表简单
0328奇偶链表Python链表中等
0234回文链表Python链表、双指针简单
0430扁平化多级双向链表Python链表中等
0138复制带随机指针的链表Python链表、哈希表中等
0061旋转链表Python链表、双指针中等

2.1 设计链表(707)

要求:设计实现一个链表,需要支持以下操作:

  • 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] 之内。
  • 操作次数将在 [1,1000][1, 1000][1,1000] 之内。
  • 请不要使用内置的 LinkedList 库。

示例

  • 示例 1:
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

2.1.1单链表

新建一个带有 val 值 和 next 指针的链表节点类, 然后按照要求对节点进行操作。

class ListNode:def __init__(self, x):self.val = xself.next = Noneclass MyLinkedList:def __init__(self):"""Initialize your data structure here."""self.size = 0self.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 -1curr = self.headfor _ in range(index + 1):curr = curr.nextreturn curr.valdef 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:returnif index < 0:index = 0self.size += 1pre = self.headfor _ in range(index):pre = pre.nextadd_node = ListNode(val)add_node.next = pre.nextpre.next = add_nodedef 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:returnself.size -= 1pre = self.headfor _ in range(index):pre = pre.nextpre.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)kkk 指的是元素的索引。
    • addAtIndex(index, val)O(n)O(n)O(n)nnn 指的是链表的元素个数。
  • 空间复杂度O(1)O(1)O(1)

2.1.2 双链表

新建一个带有 val 值和 next 指针、prev 指针的链表节点类,然后按照要求对节点进行操作。

class ListNode:def __init__(self, x):self.val = xself.next = Noneself.prev = Noneclass MyLinkedList:def __init__(self):"""Initialize your data structure here."""self.size = 0self.head = ListNode(0)self.tail = ListNode(0)self.head.next = self.tailself.tail.prev = self.headdef 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 -1if index + 1 < self.size - index:curr = self.headfor _ in range(index + 1):curr = curr.nextelse:curr = self.tailfor _ in range(self.size - index):curr = curr.prevreturn curr.valdef 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:returnif index < 0:index = 0if index < self.size - index:prev = self.headfor _ in range(index):prev = prev.nextnext = prev.nextelse:next = self.tailfor _ in range(self.size - index):next = next.prevprev = next.prevself.size += 1add_node = ListNode(val)add_node.prev = prevadd_node.next = nextprev.next = add_nodenext.prev = add_nodedef 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:returnif index < self.size - index:prev = self.headfor _ in range(index):prev = prev.nextnext = prev.next.nextelse:next = self.tailfor _ in range(self.size - index - 1):next = next.prevprev = next.prev.prevself.size -= 1prev.next = nextnext.prev = prev
  • 时间复杂度
    • addAtHead(val)addAtTail(val)O(1)O(1)O(1)
    • get(index)addAtIndex(index, val)del eteAtIndex(index)O(min(k,n−k))O(min(k, n - k))O(min(k,nk))nnn 指的是链表的元素个数,kkk 指的是元素的索引。
  • 空间复杂度O(1)O(1)O(1)

2.2 移除链表元素(203)

  给定一个链表的头节点 head 和一个值 val,删除链表中值为 val 的节点,并返回新的链表头节点。

  • 列表中的节点数目在范围 [0,104][0, 10^4][0,104]

示例

  • 输入:head = [1,2,6,3,4,5,6], val = 6
  • 输出:[1,2,3,4,5]


解题思路:

  • 使用两个指针 precurpre 指向前一节点,cur 指向当前节点。
  • 从前向后遍历链表,遇到值为 val 的节点时,将 prenext 指针指向当前节点的下一个节点,继续递归遍历。没有遇到则将 pre 指针向后移动一步。
  • 向右移动 cur,继续遍历。

  需要注意的是:因为要删除的节点可能包含了头节点,我们可以考虑在遍历之前,新建一个头节点,让其指向原来的头节点。这样,最终如果删除的是头节点,则直接删除原头节点,然后最后返回新建头节点的下一个节点即可。

class Solution:def removeElements(self, head: ListNode, val: int) -> ListNode:newHead = ListNode(0, head)newHead.next = headpre, cur = newHead, headwhile cur:if cur.val == val:pre.next = cur.nextelse:pre = curcur = cur.nextreturn newHead.next

或者是:

class Solution:def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:newHead = ListNode(0)newHead.next = head              cur=newHeadwhile cur.next:# 如果cur下一个节点值为val,就将其跳过下一节点,否则cur右移if cur.next.val==val:cur.next=cur.next.nextelse:cur=cur.nextreturn newHead.next

2.3 删除排序链表中的重复元素(83)

给定一个已排序的链表的头 head,要求删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。

  • 链表中节点数目在范围 [0,300][0, 300][0,300] 内。
  • −100≤Node.val≤100-100 \le Node.val \le 100100Node.val100
  • 题目数据保证链表已经按升序排列。

示例

  • 输入:head = [1,1,2,3,3]
  • 输出:[1,2,3]

解题思路

  • 使用指针 cur 遍历链表,如果cur节点和其下一个节点元素值相等,就跳过下一个节点(cur.next = cur.next.next)。否则,让 cur 继续向后遍历。
  • 遍历完之后返回头节点 head
class Solution:def deleteDuplicates(self, head: ListNode) -> ListNode:if head == None:return headcur = headwhile cur.next:if cur.val == cur.next.val:cur.next = cur.next.nextelse:cur = cur.nextreturn head

2.4 删除排序链表中的重复元素 (82)

  给定一个已排序的链表的头 head,要求删除原始链表中所有重复数字的节点,只留下不同的数字。返回已排序的链表。

  • 链表中节点数目在范围 [0,300][0, 300][0,300] 内。
  • −100≤Node.val≤100-100 \le Node.val \le 100100Node.val100
  • 题目数据保证链表已经按升序排列。

示例

  • 输入:head = [1,1,2,3,3,4,4,5]
  • 输出:[2,5]

解题思路:

  • 先使用哑节点 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 = headcur = dummy_headwhile cur.next and cur.next.next:if cur.next.val == cur.next.next.val:temp = cur.nextwhile temp and temp.next and temp.val == temp.next.val:temp = temp.nextcur.next = temp.nextelse:cur = cur.nextreturn dummy_head.next

2.5 反转链表

给定一个单链表的头节点 head。将该单链表进行反转。可以迭代或递归地反转链表。

  • 链表中节点的数目范围是 [0,5000][0, 5000][0,5000]
  • −5000≤Node.val≤5000-5000 \le Node.val \le 50005000Node.val5000

示例

  • 输入:head = [1,2,3,4,5]
  • 输出:[5,4,3,2,1]
  • 解释:翻转前 1->2->3->4->5->NULL,反转后 5->4->3->2->1->NULL

2.5.1 迭代

  1. 使用两个指针 curpre 进行迭代。pre 指向 cur 前一个节点位置。初始时,pre 指向 Nonecur 指向 head

  2. precur 的前后指针进行交换,指针更替顺序为:

    1. 使用 next 指针保存当前节点 cur 的后一个节点,即 next = cur.next
    2. 断开当前节点 cur 的后一节点链接,将 curnext 指针指向前一节点 pre,即 cur.next = pre
    3. pre 向前移动一步,移动到 cur 位置,即 pre = cur
    4. cur 向前移动一步,移动到之前 next 指针保存的位置,即 cur = next
  3. 继续执行第 2 步中的 1、2、3、4。

  4. 最后等到 cur 遍历到链表末尾,即 cur == None,时,pre 所在位置就是反转后链表的头节点,返回新的头节点 pre

使用迭代法反转链表的示意图如下所示:

class Solution:def reverseList(self, head: ListNode) -> ListNode:pre = Nonecur = headwhile cur != None:next = cur.nextcur.next = prepre = curcur = nextreturn pre
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(1)O(1)O(1)

2.5.2 递归

  1. 首先定义递归函数含义为:将链表反转,并返回反转后的头节点。
  2. 然后从 head.next 的位置开始调用递归函数,即将 head.next 为头节点的链表进行反转,并返回该链表的头节点。
  3. 递归到链表的最后一个节点,将其作为最终的头节点,即为 new_head
  4. 在每次递归函数返回的过程中,改变 headhead.next 的指向关系。也就是将 head.nextnext 指针先指向当前节点 head,即 head.next.next = head
  5. 然后让当前节点 headnext 指针指向 None,从而实现从链表尾部开始的局部反转。
  6. 当递归从末尾开始顺着递归栈的退出,从而将整个链表进行反转。
  7. 最后返回反转后的链表头节点 new_head

使用递归法反转链表的示意图如下所示:

class Solution:def reverseList(self, head: ListNode) -> ListNode:if head == None or head.next == None:return headnew_head = self.reverseList(head.next)head.next.next = headhead.next = Nonereturn new_head
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(n)O(n)O(n)。最多需要 nnn 层栈空间。

2.6 反转链表二(92)

  给定单链表的头指针 head 和两个整数 leftright ,其中 left <= right。要求反转从位置 left 到位置 right 的链表节点,返回反转后的链表 。

  • 链表中节点数目为 n,且1≤n≤5001 \le n \le 5001n500
  • −500≤Node.val≤500-500 \le Node.val \le 500500Node.val500
  • 1≤left≤right≤n1 \le left \le right \le n1leftrightn

示例

  • 输入:head = [1,2,3,4,5], left = 2, right = 4
  • 输出:[1,4,3,2,5]

2.6.1 迭代

  在「0206. 反转链表」中我们可以通过迭代、递归两种方法将整个链表反转。而这道题要求对链表的部分区间进行反转。我们可以先遍历到需要反转的链表区间的前一个节点,然后对需要反转的链表区间进行迭代反转。最后再返回头节点即可。

  但是需要注意一点,如果需要反转的区间包含了链表的第一个节点,那么我们可以事先创建一个哑节点作为链表初始位置开始遍历,这样就能避免找不到需要反转的链表区间的前一个节点。具体解题步骤如下:

  1. 先使用哑节点 dummy_head 构造一个指向 head 的指针,使得可以从 head 开始遍历。使用 index 记录当前元素的序号。
  2. 我们使用一个指针 start,初始赋值为 dummy_head。然后向右逐步移动到需要反转的区间的前一个节点。
  3. 然后再使用两个指针 curpre 进行迭代。pre 指向 cur 前一个节点位置,即 pre 指向需要反转节点的前一个节点,cur 指向需要反转的节点。初始时,pre 指向 startcur 指向 pre.next
  4. 当当前节点 cur 不为空,且 index 在反转区间内时,将 precur 的前后指针进行交换,指针更替顺序为:
    1. 使用 next 指针保存当前节点 cur 的后一个节点,即 next = cur.next
    2. 断开当前节点 cur 的后一节点链接,将 curnext 指针指向前一节点 pre,即 cur.next = pre
    3. pre 向前移动一步,移动到 cur 位置,即 pre = cur
    4. cur 向前移动一步,移动到之前 next 指针保存的位置,即 cur = next
    5. 然后令 index1
  5. 继续执行第 4 步中的 12345 步。
  6. 最后等到 cur 遍历到链表末尾(即 cur == None)或者遍历到需要反转区间的末尾时(即 index > right) 时,将反转区间的头尾节点分别与之前保存的需要反转的区间的前一个节点 reverse_start 相连,即 start.next.next = curstart.next = pre
  7. 最后返回新的头节点 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_headindex=0while start.next and index<=left-2:start=start.nextindex+=1#print(start.val)#使用两个指针 cur 和 pre分别指向left和left前一个节点,开始反转链表pre,cur=start,start.nextwhile cur and index<right:next=cur.nextcur.next=prepre=curcur=nextindex+=1# 此时pre是反转后的链表头结点,cur是right下一个节点#print(pre.val,cur.val) # 如果right是最后一个节点,cur指向None,其没有val属性,print报错start.next.next=curstart.next=prereturn dummy_head.next

2.6.2 头插法,一次遍历(官方题解)

  方法一的缺点是:如果 left 和 right 的区域很大,恰好是链表的头节点和尾节点时,找到 left 和 right 需要遍历一次,反转它们之间的链表还需要遍历一次,虽然总的时间复杂度为 O(n)O(n)O(n),但遍历了链表 2 次,可不可以只遍历一次呢?答案是可以的,如下图所示:
在这里插入图片描述
  整体思想是:在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。下面的图展示了整个流程。
在这里插入图片描述
具体来说,可以使用三个指针变量 pre、curr、next 来记录反转的过程中需要的变量,它们的意义如下:

  • curr:指向待反转区域的第一个节点 left;
  • next:永远指向 curr 的下一个节点,循环过程中,curr 变化以后 next 会变化;
  • pre:永远指向待反转区域的第一个节点 left 的前一个节点,在循环过程中不变。
  1. 反转链表(同206题)
    • 先将 curr 的下一个节点记录为 next;
    • 执行操作 ①:把 curr 的下一个节点指向 next 的下一个节点;
    • 执行操作 ②:把 next 的下一个节点指向 pre 的下一个节点;
    • 执行操作 ③:把 pre 的下一个节点指向 next。

在这里插入图片描述
反转后效果如下:
在这里插入图片描述
2. 继续穿针引线
在这里插入图片描述
在这里插入图片描述
第三步同上:
在这里插入图片描述
在这里插入图片描述

class Solution:def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode:# 设置 dummyNode 是这一类问题的一般做法dummy_node = ListNode(-1)dummy_node.next = headpre = dummy_nodefor _ in range(left - 1):pre = pre.nextcur = pre.nextfor _ in range(right - left):next = cur.nextcur.next = next.nextnext.next = pre.nextpre.next = nextreturn dummy_node.next

2.7 回文链表

给定一个链表的头节点 head,判断该链表是否为回文链表。
示例:

  • 输入:head = [1,2,2,1]
  • 输出:True

利用数组,将链表元素值依次存入,然后判断最终的数组是否是回文数。

class Solution:def isPalindrome(self, head: ListNode) -> bool:nodes = []p1 = headwhile p1:nodes.append(p1.val)p1 = p1.nextreturn nodes == nodes[::-1]

三、链表双指针

3.1 双指针简介

在数组双指针中我们已经学习过了双指针的概念。这里再来复习一下。

   双指针(Two Pointers):指的是在遍历元素的过程中,使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为「对撞时针」。如果两个指针方向相同,则称为「快慢指针」。如果两个指针分别属于不同的数组 / 链表,则称为「分离双指针」

  而在单链表中,因为遍历节点只能顺着 next 指针方向进行,所以对于链表而言,一般只会用到「快慢指针」和「分离双指针」。其中链表的「快慢指针」又分为「起点不一致的快慢指针」「步长不一致的快慢指针」。这几种类型的双指针所解决的问题也各不相同,下面我们一一进行讲解。

题号标题题解标签难度
0141环形链表Python链表、双指针简单
0142环形链表 IIPython链表、双指针中等
0160相交链表Python链表、双指针简单
0019删除链表的倒数第 N 个结点Python链表、双指针中等
0876链表的中间结点Python链表、指针简单
剑指 Offer 22链表中倒数第k个节点Python链表、双指针简单
0143重排链表Python栈、递归、链表、双指针中等
0002两数相加Python递归、链表、数学中等
0445两数相加 IIPython栈、链表、数学中等

3.2 起点不一致的快慢指针

3.2.1 算法简介

  两个指针从同一侧开始遍历链表,但是两个指针的起点不一样。 快指针 fast 比慢指针 slow 先走 n 步,直到快指针移动到链表尾端时为止。

求解步骤

  1. 使用两个指针 slowfastslowfast 都指向链表的头节点,即:slow = headfast = head
  2. 先将快指针向右移动 n 步。然后再同时向右移动快、慢指针。
  3. 等到快指针移动到链表尾部(即 fast == None)时跳出循环体。

伪代码

slow,fast = head,headwhile n:fast = fast.nextn -= 1while fast:fast = fast.nextslow = slow.next

适用范围:主要用于找到链表中倒数第 k 个节点、删除链表倒数第 N 个节点等

3.2.2 删除链表的倒数第 N 个结点

题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

给定一个链表的头节点 head,要求删除链表的倒数第 n 个节点,并且返回链表的头节点。

  • 要求使用一次遍历实现。
  • 链表中结点的数目为 sz,其中 1≤sz≤301 \le sz \le 301sz30
  • 0≤Node.val≤1000 \le Node.val \le 1000Node.val100
  • 1≤n≤sz1 \le n \le sz1nsz

示例

  • 输入:head = [1,2,3,4,5], n = 2
  • 输出:[1,2,3,5]

解题思路:快慢指针

  • 常规思路是遍历一遍链表,求出链表长度,再遍历一遍到对应位置,删除该位置上的节点。
  • 使用快慢指针可以实现一次遍历得到结果。思路就是让快指针先走 n 步,然后快慢指针同时右移,这样等快指针遍历到链表尾部的时候,慢指针就刚好遍历到了倒数第 n 个节点位置。将该位置上的节点删除即可。
  • 要删除的节点可能包含了头节点。我们可以考虑在遍历之前,新建一个头节点,让其指向原来的头节点。这样,最终如果删除的是头节点,则删除原头节点即可。返回结果的时候,可以直接返回新建头节点的下一位节点。
class Solution:def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:newHead = ListNode(0, head) # val=0,next=headfast = headslow = newHeadwhile n:fast = fast.nextn -= 1while fast:fast = fast.nextslow = slow.nextslow.next = slow.next.nextreturn newHead.next
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(1)O(1)O(1)

3.3 步长不一致的快慢指针

3.3.1 算法原理

  两个指针从同一侧开始遍历链表,两个指针的起点一样,但是步长不一致。例如,慢指针 slow 每次走 1 步,快指针 fast 每次走两步。直到快指针移动到链表尾端时为止。

求解步骤

  1. 使用两个指针 slowfastslowfast 都指向链表的头节点。
  2. 在循环体中将快、慢指针同时向右移动,但是快、慢指针的移动步长不一致。比如将慢指针每次移动 1 步,即 slow = slow.next。快指针每次移动 2 步,即 fast = fast.next.next
  3. 等到快指针移动到链表尾部(即 fast == None)时跳出循环体。

伪代码

fast = head
slow = headwhile fast and fast.next:slow = slow.nextfast = fast.next.next

适用范围:此算法适合寻找链表的中点、判断和检测链表是否有环、找到两个链表的交点等问题。

3.3.2 求链表的中间结点

3.4.1 题目链接: 876. 链表的中间结点 - 力扣(LeetCode)

给定一个单链表的头节点 head,返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。

  • 给定链表的结点数介于 1100 之间。

示例1

  • 输入:[1,2,3,4,5]
  • 输出:此列表中的结点 3 (序列化形式:[3,4,5])
  • 解释:返回的结点值为 3 。
  • 注意,我们返回了一个 ListNode 类型的对象 ans,这样:
    ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

示例2

  • 输入:[1,2,3,4,5,6]
  • 输出:此列表中的结点 4 (序列化形式:[4,5,6])
  • 解释:由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
3.3.2.1 单指针

  先遍历一遍链表,统计一下节点个数为 n,再遍历到 n / 2 的位置,返回中间节点。其代码如下:

class Solution:def middleNode(self, head: ListNode) -> ListNode:n = 0curr = headwhile curr:n += 1curr = curr.nextk = 0curr = headwhile k < n // 2:k += 1curr = curr.nextreturn curr
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(1)O(1)O(1)
3.3.2.2 快慢指针

使用步长不一致的快慢指针进行一次遍历找到链表的中间节点。具体做法如下:

  1. 使用两个指针 slowfastslowfast 都指向链表的头节点。
  2. 在循环体中将快、慢指针同时向右移动。其中慢指针每次移动 1 步,即 slow = slow.next。快指针每次移动 2 步,即 fast = fast.next.next
  3. 等到快指针移动到链表尾部(即 fast == Node)时跳出循环体,此时 slow 指向链表中间位置。
  4. 返回 slow 指针。
class Solution:def middleNode(self, head: ListNode) -> ListNode:fast = headslow = headwhile fast and fast.next:slow = slow.nextfast = fast.next.nextreturn slow
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(1)O(1)O(1)

3.3.3 判断链表中是否含有环

题目链接: 141. 环形链表 - 力扣(LeetCode)

  给定一个链表的头节点 head,判断链表中是否有环。如果有环则返回 True,否则返回 False

  • 链表中节点的数目范围是 [0,104][0, 10^4][0,104]
  • −105≤Node.val≤105-10^5 \le Node.val \le 10^5105Node.val105
  • pos-1 或者链表中的一个有效索引。

示例

  • 输入:head = [3,2,0,-4], pos = 1
  • 输出:True
  • 解释:链表中有一个环,其尾部连接到第二个节点。
3.3.3.1 哈希表

  最简单的思路是遍历所有节点,每次遍历节点之前,使用哈希表判断该节点是否被访问过。如果过就说明存在环,如果没访问过则将该节点添加到哈希表中,继续遍历判断。

class Solution:def hasCycle(self, head: ListNode) -> bool:nodeset = set()while head:if head in nodeset:return Truenodeset.add(head)head = head.nextreturn False
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(n)O(n)O(n)
3.3.3.2 快慢指针(Floyd 判圈算法)

  这种方法类似于在操场跑道跑步。两个人从同一位置同时出发,如果跑道有环(环形跑道),那么快的一方总能追上慢的一方。

  基于上边的想法,Floyd 用两个指针,一个慢指针(龟)每次前进一步,快指针(兔)指针每次前进两步(两步或多步效果是等价的)。如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。

class Solution:def hasCycle(self, head: ListNode) -> bool:if head == None or head.next == None:return Falseslow = headfast = head.nextwhile slow != fast:if fast == None or fast.next == None:return Falseslow = slow.nextfast = fast.next.nextreturn True
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(1)O(1)O(1)

3.3.4 求环形链表入环位置

题目链接::142 环形链表 II

  给定一个链表的头节点 head,判断链表中是否有环,如果有环则返回入环的第一个节点,无环则返回 None

  • 链表中节点的数目范围在范围 [0,104][0, 10^4][0,104] 内。
  • −105≤Node.val≤105-10^5 \le Node.val \le 10^5105Node.val105
  • pos 的值为 -1 或者链表中的一个有效索引。

示例

  • 输入:head = [3,2,0,-4], pos = 1
  • 输出:返回索引为 1 的链表节点
  • 解释:链表中有一个环,其尾部连接到第二个节点。
3.3.4.1 哈希表

  遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。借助哈希表可以很方便地实现。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:se=set()cur=headwhile cur:if cur not in se:se.add(cur)cur=cur.nextelse:return curreturn None
3.3.4.2 快慢指针(Floyd 判圈算法)
  1. 利用两个指针,一个慢指针 slow 每次前进一步,快指针 fast 每次前进两步(两步或多步效果是等价的)。
  2. 如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环。
  3. 否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。
  4. 如果有环,则再定义一个指针 ans,和慢指针一起每次移动一步,两个指针相遇的位置即为入口节点。

这是因为:

  • 假设入环位置为 A,快慢指针在 B 点相遇,则相遇时慢指针走了 a+ba + ba+b 步,快指针走了 a+n(b+c)+ba + n(b+c) + ba+n(b+c)+b 步。

  • 因为快指针总共走的步数是慢指针走的步数的两倍,即 2(a+b)=a+n(b+c)+b2(a + b) = a + n(b + c) + b2(a+b)=a+n(b+c)+b,所以可以推出:a=c+(n−1)(b+c)a = c + (n-1)(b + c)a=c+(n1)(b+c)

  • 我们可以发现:从相遇点到入环点的距离 ccc 加上 n−1n-1n1 圈的环长 b+cb + cb+c 刚好等于从链表头部到入环点的距离。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:       if head==None or head.next==None:return Noneelse:            slow,fast=head,head                       while fast and fast.next:                                                                               slow=slow.nextfast=fast.next.nextif fast==slow: ans=headwhile ans!=slow:ans,slow=ans.next,slow.nextreturn slowreturn None
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(1)O(1)O(1)

3.4 分离双指针

3.4.1 算法原理

分离双指针:两个指针分别属于不同的链表,两个指针分别在两个链表中移动。

求解步骤

  1. 使用两个指针 left_1left_2。分别指向两个链表的头节点,即:left_1 = list1left_2 = list2
  2. 当满足一定条件时,两个指针同时右移,即 left_1 = left_1.nextleft_2 = left_2.next
  3. 当满足另外一定条件时,将 left_1 指针右移,即 left_1 = left_1.next
  4. 当满足其他一定条件时,将 left_2 指针右移,即 left_2 = left_2.next
  5. 当其中一个链表遍历完时或者满足其他特殊条件时跳出循环体。

伪代码模板

left_1 = list1
left_2 = list2while left_1 and left_2:if 一定条件 1:left_1 = left_1.nextleft_2 = left_2.nextelif 一定条件 2:left_1 = left_1.nextelif 一定条件 3:left_2 = left_2.next

适用范围:分离双指针一般用于有序链表合并等问题。

3.4.2 合并两个有序链表

题目链接: 21. 合并两个有序链表 - 力扣(LeetCode)

给定两个升序链表的头节点 list1list2,要求将其合并为一个升序链表。

  • 两个链表的节点数目范围是 [0,50][0, 50][0,50]
  • −100≤Node.val≤100-100 \le Node.val \le 100100Node.val100
  • list1list2 均按 非递减顺序 排列

示例

  • 输入:list1 = [1,2,4], list2 = [1,3,4]
  • 输出:[1,1,2,3,4,4]

利用分离双指针,具体步骤如下:

  1. 使用哑节点 dummy_head 构造一个头节点,并使用 cur 指向 dummy_head 用于遍历。
  2. 然后判断 list1list2 头节点的值,将较小的头节点加入到合并后的链表中。并向后移动该链表的头节点指针。
  3. 然后重复上一步操作,直到两个链表中出现链表为空的情况。
  4. 将剩余链表链接到合并后的链表中。
  5. 将哑节点 dummy_dead 的下一个链节点 dummy_head.next 作为合并后有序链表的头节点返回。
class Solution:def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:dummy_head = ListNode(-1)cur = dummy_headwhile list1 and list2:if list1.val <= list2.val:cur.next = list1list1 = list1.nextelse:cur.next = list2list2 = list2.nextcur = cur.next# 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可cur.next = list1 if list1 is not None else list2return dummy_head.next
  • 时间复杂度O(n)O(n)O(n)
  • 空间复杂度O(1)O(1)O(1)

3.4.3 相交链表

题目链接: 160. 相交链表- 力扣(LeetCode)

  给定两个链表 listAlistB。判断两个链表是否相交,相交则返回起始点,不相交则返回 None
示例:

  • 输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
  • 输出:Intersected at ‘8’
  • 解释:
    • 相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
    • 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
    • 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

3.4.3.1 哈希表

判断两个链表是否相交,可以使用哈希集合存储链表节点。

  1. 遍历链表 headA,并将其中的每个节点加入哈希集合中。

  2. 遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:

    • 如果当前节点不在哈希集合中,则继续遍历下一个节点;
    • 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
    • 如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:se=set()cur1=headAcur2=headBwhile cur1:se.add(cur1)cur1=cur1.nextwhile cur2:if cur2 in se:return cur                          cur2=cur2.nextreturn None
3.4.3.2 分离双指针

使用双指针的方法,可以将空间复杂度降至 O(1)O(1)O(1)

  • 使用两个指针 pApB,分别从链表 listA、链表 listB 的头节点开始遍历。
  • 直到pA 为空,将其指向listB的头结点,继续遍历
  • 直到pB 为空,将其指向listA的头结点,继续遍历
  • 当指针 pApB 指向同一个节点或者都为空时,返回它们指向的节点或者 null

当两个链表相交时:
在这里插入图片描述

class Solution:def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:if headA == None or headB == None:return NonepA = headApB = headBwhile pA != pB :pA = pA.next if pA != None else headBpB = pB.next if pB != None else headAreturn pA    

四、链表排序

  此部分内容请参考《算法通关手册》 链表排序篇。

4.1 基础知识

  在数组排序中,常见的排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序等。
  而对于链表排序而言,因为链表不支持随机访问,访问链表后面的节点只能依靠 next 指针从头部顺序遍历,所以相对于数组排序问题来说,链表排序问题会更加复杂一点。

  • 适合链表的排序算法:冒泡排序选择排序插入排序归并排序快速排序计数排序桶排序基数排序
  • 重点掌握:链表插入排序链表归并排序
  • 不适合链表的排序算法:希尔排序
  • 可以用于链表排序但不建议使用的排序算法:堆排序

希尔排序为什么不适合链表排序?
  希尔排序:希尔排序中经常涉及到对序列中第 i + gap 的元素进行操作,其中 gap 是希尔排序中当前的步长。而链表不支持随机访问的特性,导致这种操作不适合链表,因而希尔排序算法不适合进行链表排序。

为什么不建议使用堆排序?
  堆排序:堆排序所使用的最大堆 / 最小堆结构本质上是一棵完全二叉树。而完全二叉树适合采用顺序存储结构(数组)。因为数组存储的完全二叉树可以很方便的通过下标序号来确定父亲节点和孩子节点,并且可以极大限度的节省存储空间。
  而链表用在存储完全二叉树的时候,因为不支持随机访问的特性,导致其寻找子节点和父亲节点会比较耗时,如果增加指向父亲节点的变量,又会浪费大量存储空间。所以堆排序算法不适合进行链表排序。
  如果一定要对链表进行堆排序,则可以使用额外的数组空间表示堆结构。然后将链表中各个节点的值依次添加入堆结构中,对数组进行堆排序。排序后,再按照堆中元素顺序,依次建立链表节点,构建新的链表并返回新链表头节点。

链表插入排序

4.2 链表插入排序

1. 算法步骤

  1. 先使用哑节点 dummy_head 构造一个指向 head 的指针,使得可以从 head 开始遍历。

  2. 维护 sorted_list 为链表的已排序部分的最后一个节点,初始时,sorted_list = head

  3. 维护 prev 为插入元素位置的前一个节点,维护 cur 为待插入元素。初始时,prev = headcur = head.next

  4. 比较 sorted_listcur 的节点值。

    • 如果 sorted_list.val <= cur.val,说明 cur 应该插入到 sorted_list 之后,则将 sorted_list 后移一位。
    • 如果 sorted_list.val > cur.val,说明 cur 应该插入到 headsorted_list 之间。则使用 prevhead 开始遍历,直到找到插入 cur 的位置的前一个节点位置。然后将 cur 插入。
  5. cur = sorted_list.next,此时 cur 为下一个待插入元素。

  6. 重复 4、5 步骤,直到 cur 遍历结束为空。返回 dummy_head 的下一个节点。

2. 实现代码

class Solution:def insertionSort(self, head: ListNode):if not head or not head.next:return headdummy_head = ListNode(-1)dummy_head.next = headsorted_list = headcur = head.next while cur:if sorted_list.val <= cur.val:# 将 cur 插入到 sorted_list 之后sorted_list = sorted_list.next else:prev = dummy_headwhile prev.next.val <= cur.val:prev = prev.next# 将 cur 到链表中间sorted_list.next = cur.nextcur.next = prev.nextprev.next = curcur = sorted_list.next return dummy_head.nextdef sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:return self.insertionSort(head)
  • 时间复杂度O(n2)O(n^2)O(n2)
  • 空间复杂度O(1)O(1)O(1)

4.3 链表归并排序

1. 算法步骤

  1. 分割环节:找到链表中心链节点,从中心节点将链表断开,并递归进行分割。
    1. 使用快慢指针 fast = head.nextslow = head,让 fast 每次移动 2 步,slow 移动 1 步,移动到链表末尾,从而找到链表中心链节点,即 slow
    2. 从中心位置将链表从中心位置分为左右两个链表 left_headright_head,并从中心位置将其断开,即 slow.next = None
    3. 对左右两个链表分别进行递归分割,直到每个链表中只包含一个链节点。
  2. 归并环节:将递归后的链表进行两两归并,完成一遍后每个子链表长度加倍。重复进行归并操作,直到得到完整的链表。
    1. 使用哑节点 dummy_head 构造一个头节点,并使用 cur 指向 dummy_head 用于遍历。
    2. 比较两个链表头节点 leftright 的值大小。将较小的头节点加入到合并后的链表中,并向后移动该链表的头节点指针。
    3. 然后重复上一步操作,直到两个链表中出现链表为空的情况。
    4. 将剩余链表插入到合并后的链表中。
    5. 将哑节点 dummy_dead 的下一个链节点 dummy_head.next 作为合并后的头节点返回。

2. 实现代码

class Solution:def merge(self, left, right):# 归并环节dummy_head = ListNode(-1)cur = dummy_headwhile left and right:if left.val <= right.val:cur.next = leftleft = left.nextelse:cur.next = rightright = right.nextcur = cur.nextif left:cur.next = leftelif right:cur.next = rightreturn dummy_head.nextdef mergeSort(self, head: ListNode):# 分割环节if not head or not head.next:return head# 快慢指针找到中心链节点slow, fast = head, head.nextwhile 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)
  • 时间复杂度O(n×log⁡2n)O(n \times \log_2n)O(n×log2n)
  • 空间复杂度O(1)O(1)O(1)

4.4 链表排序题目

题号标题题解标签难度
0148排序链表Python链表、双指针、分治、排序、归并排序中等
0021合并两个有序链表Python递归、链表简单
0023合并K个升序链表Python链表、分治、堆(优先队列)、归并排序困难
0147对链表进行插入排序Python链表、排序中等

4.4.1 排序链表

题目链接:0148. 排序链表

给定链表的头节点 head,请按照升序排列并返回排序后的链表。

  • 链表中节点的数目在范围 [0,5∗104][0, 5 * 10^4][0,5104] 内。
  • −105≤Node.val≤105-10^5 \le Node.val \le 10^5105Node.val105

  本题使用链表的冒泡排序、选择排序、插入排序、快速排序都会超时。使用桶排序、归并排序、计数排序则可以通过,基数排序只适用于非负数的情况。归并排序代码上面已给出,其它排序算法请参考《算法通关手册》

4.4.2 合并K个升序链表

题目链接: 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_headwhile list1 and list2:if list1.val <= list2.val:cur.next = list1list1 = list1.nextelse:cur.next = list2list2 = list2.nextcur = cur.next# 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可cur.next = list1 if list1 is not None else list2return dummy_head.next
4.4.2.1 顺序合并

  我们可以想到一种最朴素的方法:用一个变量 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 ansdef mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:dummy_head = ListNode(-1)cur = dummy_headwhile list1 and list2:if list1.val <= list2.val:cur.next = list1list1 = list1.nextelse:cur.next = list2list2 = list2.nextcur = cur.next# 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可cur.next = list1 if list1 is not None else list2return dummy_head.next    
4.4.2.2 分治合并

  分而治之的思想。将链表数组不断二分,转为规模为二分之一的子问题,然后再进行归并排序。
在这里插入图片描述

class Solution:def merge_sort(self, lists: List[ListNode], left: int, right: int) -> ListNode:if left == right:return lists[left]mid = left + (right - left) // 2node_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 = rootwhile a and b:if a.val < b.val:cur.next = aa = a.nextelse:cur.next = bb = b.nextcur = cur.nextif a:cur.next = aif b:cur.next = breturn root.nextdef mergeKLists(self, lists: List[ListNode]) -> ListNode:if not lists:return Nonesize = len(lists)return self.merge_sort(lists, 0, size - 1)

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_74032.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

39-Golang中的接口

Golang中的接口基本介绍基本语法注意事项和细节案例实现对Hero结构体切片的排序&#xff1a;sort.Sort(data Interface)实现接口和继承之间的比较区别基本介绍 interface类型可以定义一组方法&#xff0c;但是这些不需要实现。并且interface不能包含任何变量。到某个自定义类型…

直接在ide启动mitmproxy监听,脱离命令行启动,懒人福音

前言 本文解决了只能通过命令行启动 mitmproxy 的痛点。 在使用 mitmproxy 时候存在这样一个问题&#xff0c;就是每次启动它时候都需要通过命令行启动。 加上最近有位读者向我提问&#xff08;以前也有读者提问该问题&#xff09;&#xff1a;不通过命令行如何启动 mitmproxy监…

XML调用 CAPL Test Function

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

阿里限量出产Elasticsearch学习手册,确定不心动?

前言只有光头才能变强。不知道大家的公司用Elasticsearch多不多&#xff0c;反正我公司的是有在用的。平时听同事们聊天肯定避免不了不认识的技术栈&#xff0c;例如说&#xff1a;把数据放在引擎&#xff0c;从引擎取出数据等等。如果对引擎不了解的同学&#xff0c;就压根听不…

九龙证券|阿里+鸿蒙+人工智能+元宇宙概念热度爆棚,“会说话的猫”亮了!

近一周组织调研个股数量有240多只&#xff0c;汤姆猫成为调研组织数量最多的股票。 证券时报数据宝统计&#xff0c;近一周组织调研公司数量有240多家。从调研组织类型来看&#xff0c;证券公司调研相对最广泛&#xff0c;调研230多家公司。 “会说话的猫”亮了 汤姆猫成为近…

Flink高手之路1一Flink的简介

文章目录一、Flink简介1. Fink的引入2.Flink简介3.支持的编程语言4.Flink的特性5.Flink四大基石6.批处理和流处理二、Flink的架构1.Flink的角色2.编程模型一、Flink简介 1. Fink的引入 大数据的计算引擎&#xff0c;发展过程有四个阶段 第一代&#xff1a;Hadoop的MapReduce…

二叉搜索树中的众数Java解法

给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root &#xff0c;找出并返回 BST 中的所有 众数&#xff08;即&#xff0c;出现频率最高的元素&#xff09;。 如果树中有不止一个众数&#xff0c;可以按 任意顺序 返回。 假定 BST 满足如下定义&#xf…

【Web逆向】万方数据平台正文的逆向分析(上篇--加密发送请求)—— 逆向protobuf

【Web逆向】万方数据平台正文的逆向分析&#xff08;上篇--加密发送请求&#xff09;—— 逆向protobuf声明一、了解protobuf协议&#xff1a;二、前期准备&#xff1a;二、目标网站&#xff1a;三、开始分析&#xff1a;我们一句句分析&#xff1a;先for循环部分&#xff1a;后…

【算法】最短路算法

&#x1f600;大家好&#xff0c;我是白晨&#xff0c;一个不是很能熬夜&#x1f62b;&#xff0c;但是也想日更的人✈。如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下&#x1f440;白晨吧&#xff01;你的支持就是我最大的动力&#xff01;&#x1f4…

电子技术——输出阶类型

电子技术——输出阶类型 输出阶作为放大器的最后一阶&#xff0c;其必须有较低的阻抗来保证较小的增益损失。作为放大器的最后一阶&#xff0c;输出阶需要处理大信号类型&#xff0c;因此小信号估计模型不适用于输出阶。尽管如此&#xff0c;输出阶的线性也非常重要。实际上&a…

为什么要用线程池?

1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 2.提高响应速度。当任务到达时&#xff0c;任务可以不需要的等到线程创建就能立即执行。 3.提高线程的可管理性。线程是稀缺资源&#xff0c;如果无限制的创建&#xff0c;不仅会消耗系统资源&#…

Python实现贝叶斯优化器(Bayes_opt)优化支持向量机回归模型(SVR算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。1.项目背景贝叶斯优化器 (BayesianOptimization) 是一种黑盒子优化器&#xff0c;用来寻找最优参数。贝叶斯优化器是…

AI_News周刊:第三期

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 2023.02.20—2023.02.25 News 1.OpenAI 现在正在帮助可口可乐改善其营销和运营 2023 年 2 月 21 日——贝恩公司今天宣布与 OpenAI 建立全球服务联盟&#xff0c;OpenAI 是人工智能系统 ChatGPT、DA…

java Spring JdbcTemplate配合mysql实现数据库表数据添加

本文为 java Spring JdbcTemplate 准备工作的续文 如果您还没有大家好JdbcTemplate 的基础环境 可以先查看前文 首先 之前数据库我们已经弄好了 然后 我们在下面创建一个表 我这里叫 user_list 每一个数据库表 要对应一个实体类 这里 我们打开上一文搭建的项目环境 src下创建…

【华为OD机试模拟题】用 C++ 实现 - 英文输入法(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 分积木(2023.Q1) 【华为OD机试模拟题】用 C++ 实现 - 吃火锅(2023.Q1) 【华为OD机试模拟题】用 C++ 实现 - RSA 加密算法(2023.Q1) 【华为OD机试模拟题】用 C++ 实现 - 构成的正方形数量(2023.Q1) 【华为OD机试模拟…

【原创】java+swing+mysql生肖星座查询系统设计与实现

今天我们来开发一个比较有趣的系统&#xff0c;根据生日查询生肖星座&#xff0c;输入生日&#xff0c;系统根据这个日期自动计算出生肖和星座信息反馈到界面。我们还是使用javaswingmysql去实现这样的一个系统。 功能分析&#xff1a; 生肖星座查询系统&#xff0c;顾名思义…

【CSS】CSS 层叠样式表 ① ( 简介 | CSS 引入方式 - 内联样式 | 内联样式语法 | 内联样式缺点 )

文章目录一、CSS 层叠样式表二、CSS 引入方式 - 内联样式1、内联样式语法2、内联样式缺点3、内联样式代码示例① 核心代码示例② 完整代码示例③ 执行结果一、CSS 层叠样式表 CSS 全称 Cascading Style Sheets , 层叠样式表 ; 作用如下 : 设置 HTML 页面 文本内容 的 字体 , 颜…

【华为OD机试模拟题】用 C++ 实现 - 最少停车数(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

绝对让你明明白白,脚把脚带你盯着 I2C 时序图将 I2C 程序给扣出来(基于STM32的模拟I2C)

目录前言一、关于STM32 I/O端口位的基本结构讲解二、模拟I2C编写前的需知道的知识1、I2C简介2、根据时序编写模拟I2C程序重要的两点Ⅰ、主机发送数据给从机时的时序控制Ⅱ、主机接收来自从机的数据时的时序控制Ⅲ、完整的I2C时序图&#xff08;按写程序的思想分割时序&#xff…

2023年湖北住建厅七大员建筑八大员怎么报考?启程别

2023年湖北住建厅七大员建筑八大员怎么报考&#xff1f;启程别 建筑施工企业关键技术岗位人员可以叫七大员也可以叫八大员&#xff0c;施工现场专业人员&#xff0c;从事相关岗位人员都应该持证上岗。 为什么有的叫七大员&#xff1f;有的叫八大员呢&#xff1f;甚至还有五大员…