LeetCode练习三:链表

news2024/11/18 19:47:17

文章目录

    • 一、链表基础
      • 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 = val
        self.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.head
    for i in range(len(data)):
        node = ListNode(data[i])
        cur.next = node
        cur = cur.next

1.2.3 求线性链表的长度

  使用一个可以顺着链表指针移动的指针变量 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

1.2.4 查找元素

  在链表中查找值为 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.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.head
    self.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.head
    while cur.next:
        cur = cur.next
    cur.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 = 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

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 = 0
    cur = self.head
    while cur and count < index:
        count += 1
        cur = cur.next
        
    if 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.head
    while cur.next.next:
        cur = cur.next
    cur.next = None

3. 链表中间删除元素

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

  1. 先使用指针变量 cur 移动到第 i - 1 个位置的链节点。
  2. 然后将 curnext 指针,指向要第 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

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

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 = 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)

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 = 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)

2.1.2 双链表

新建一个带有 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,nk)) n n n 指的是链表的元素个数, k k k 指的是元素的索引。
  • 空间复杂度 O ( 1 ) O(1) O(1)

2.2 移除链表元素(203)

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

  • 列表中的节点数目在范围 [ 0 , 1 0 4 ] [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 = 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

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

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

  • 链表中节点数目在范围 [ 0 , 300 ] [0, 300] [0,300] 内。
  • − 100 ≤ N o d e . v a l ≤ 100 -100 \le Node.val \le 100 100Node.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 head

        cur = head
        while cur.next:
            if cur.val == cur.next.val:
                cur.next = cur.next.next
            else:
                cur = cur.next
        return head

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

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

  • 链表中节点数目在范围 [ 0 , 300 ] [0, 300] [0,300] 内。
  • − 100 ≤ N o d e . v a l ≤ 100 -100 \le Node.val \le 100 100Node.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 = 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

2.5 反转链表

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

  • 链表中节点的数目范围是 [ 0 , 5000 ] [0, 5000] [0,5000]
  • − 5000 ≤ N o d e . v a l ≤ 5000 -5000 \le Node.val \le 5000 5000Node.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 = None
        cur = head
        while cur != None:
            next = cur.next
            cur.next = pre
            pre = cur
            cur = next
        return 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 head
        new_head = self.reverseList(head.next)
        head.next.next = head
        head.next = None
        return new_head
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( n ) O(n) O(n)。最多需要 n n n 层栈空间。

2.6 反转链表二(92)

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

  • 链表中节点数目为 n,且 1 ≤ n ≤ 500 1 \le n \le 500 1n500
  • − 500 ≤ N o d e . v a l ≤ 500 -500 \le Node.val \le 500 500Node.val500
  • 1 ≤ l e f t ≤ r i g h t ≤ n 1 \le left \le right \le n 1leftrightn

示例

  • 输入: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_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

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 = 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

2.7 回文链表

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

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

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

class Solution:
    def isPalindrome(self, head: ListNode) -> bool:
        nodes = []
        p1 = head
        while p1:
            nodes.append(p1.val)
            p1 = p1.next
        return 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,head

while n:
    fast = fast.next
    n -= 1
    
while fast:
    fast = fast.next
    slow = slow.next

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

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

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

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

  • 要求使用一次遍历实现。
  • 链表中结点的数目为 sz,其中 1 ≤ s z ≤ 30 1 \le sz \le 30 1sz30
  • 0 ≤ N o d e . v a l ≤ 100 0 \le Node.val \le 100 0Node.val100
  • 1 ≤ n ≤ s z 1 \le n \le sz 1nsz

示例

  • 输入: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=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
  • 时间复杂度 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 = head

while fast and fast.next:
    slow = slow.next
    fast = 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 = 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
  • 时间复杂度 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 = head
        slow = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        return slow
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

3.3.3 判断链表中是否含有环

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

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

  • 链表中节点的数目范围是 [ 0 , 1 0 4 ] [0, 10^4] [0,104]
  • − 1 0 5 ≤ N o d e . v a l ≤ 1 0 5 -10^5 \le Node.val \le 10^5 105Node.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 True
            nodeset.add(head)
            head = head.next
        return 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 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
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

3.3.4 求环形链表入环位置

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

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

  • 链表中节点的数目范围在范围 [ 0 , 1 0 4 ] [0, 10^4] [0,104] 内。
  • − 1 0 5 ≤ N o d e . v a l ≤ 1 0 5 -10^5 \le Node.val \le 10^5 105Node.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 = 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
3.3.4.2 快慢指针(Floyd 判圈算法)
  1. 利用两个指针,一个慢指针 slow 每次前进一步,快指针 fast 每次前进两步(两步或多步效果是等价的)。
  2. 如果两个指针在链表头节点以外的某一节点相遇(即相等)了,那么说明链表有环。
  3. 否则,如果(快指针)到达了某个没有后继指针的节点时,那么说明没环。
  4. 如果有环,则再定义一个指针 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+(n1)(b+c)

  • 我们可以发现:从相遇点到入环点的距离 c c c 加上 n − 1 n-1 n1 圈的环长 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
  • 时间复杂度 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 = 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

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

3.4.2 合并两个有序链表

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

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

  • 两个链表的节点数目范围是 [ 0 , 50 ] [0, 50] [0,50]
  • − 100 ≤ N o d e . v a l ≤ 100 -100 \le Node.val \le 100 100Node.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_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
  • 时间复杂度 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 = 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
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 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    

四、链表排序

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

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 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)
  • 时间复杂度 O ( n 2 ) 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_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)
  • 时间复杂度 O ( n × log ⁡ 2 n ) 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 ∗ 1 0 4 ] [0, 5 * 10^4] [0,5104] 内。
  • − 1 0 5 ≤ N o d e . v a l ≤ 1 0 5 -10^5 \le Node.val \le 10^5 105Node.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_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
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 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    
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) // 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)

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis之消息队列实现

文章目录秒杀场景采用消息队列实现List实现消息队列PubSub&#xff08;发布订阅&#xff09;实现消息队列基于Stream实现消息队列消费者组实践总结秒杀问题是非常重要且比较难实现的&#xff0c;如果不进行架构的优化的话&#xff0c;直接访问会给业务系统造成很大的压力… 秒杀…

【Linux】system V共享内存 | 消息队列 | 信号量

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《学会Linux》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;system V共…

go进阶(2) -深入理解Channel实现原理

Go的并发模型已经在https://guisu.blog.csdn.net/article/details/129107148 详细说明。 1、channel使用详解 1、channel概述 Go的CSP并发模型&#xff0c;是通过goroutine和channel来实现的。 channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲&#xf…

通用信息抽取技术UIE产业案例解析,Prompt 范式落地经验分享!

想了解用户的评价究竟是“真心夸赞”还是“阴阳怪气”&#xff1f;想快速从多角色多事件的繁杂信息中剥茧抽丝提取核心内容&#xff1f;想通过聚合相似事件准确地归纳出特征标签&#xff1f;……想了解UIE技术在产业中的实战落地经验&#xff1f;通用信息抽取技术 UIE 产业案例…

FPGA基础知识

FPGA是在PAL、PLA和CPLD等可编程器件的基础上进一步发展起来的一种更复杂的可编程逻辑器件。它是ASIC领域中的一种半定制电路&#xff0c;既解决了定制电路的不足&#xff0c;又克服了原有可编程器件门电路有限的缺点。 由于FPGA需要被反复烧写&#xff0c;它实现组合逻辑的基…

【强化学习】强化学习数学基础:贝尔曼公式

强化学习数学基础&#xff1a;贝尔曼公式强化学习的数学原理课程总览贝尔曼公式&#xff08;Bellman Equation&#xff09;一个示例状态值贝尔曼公式&#xff1a;推导过程贝尔曼公式&#xff1a;矩阵-向量形式&#xff08;Matrix-vector form&#xff09;贝尔曼公式&#xff1a…

(四)应变度量

本文主要内容包括&#xff1a;1. Hill 应变度量 与 Seth 应变度量2. Hill -Seth 应变度量的 Lagrange 描述2.1. Green-Lagrange 应变张量2.2. 物质 Biot 应变张量/工程应变2.3. 右 Henkey 应变张量/Lagrange 型对数应变2.4. Piola 应变张量3. Hill -Seth 应变度量的 Euler 描述…

Tesla都使用什么编程语言?

作者 | 初光 出品 | 车端 备注 | 转载请阅读文中版权声明 知圈 | 进“汽车电子与AutoSAR开发”群&#xff0c;请加微“cloud2sunshine” 总目录链接>> AutoSAR入门和实战系列总目录 带着对更美好未来的愿景&#xff0c;特斯拉不仅成为有史以来最有价值的汽车公司&…

乐友商城学习笔记(五)

什么是Nginx Nginx是一个高性能的web和反向代理服务器 作为web服务器作为负载均衡服务器作为邮件代理服务 树组件的用法 跨域问题 跨域&#xff1a;浏览器对javastript的同源策略的限制。 以下情况都属于跨域&#xff1a; 域名不同域名相同&#xff0c;端口不同二级域名不…

Python每日一练(20230225)

目录 1. 整数反转 2. 求最大公约数和最小公倍数 最大公约数 最小公倍数 3. 单词搜索 II 附录&#xff1a; DFS 深度优先搜索算法 BFS 广度优先搜索算法 BFS 和 DFS 的区别 1. 整数反转 给你一个 32 位的有符号整数 x &#xff0c;返回将 x 中的数字部分反转后的结果。…

大型旋转设备滑动轴承X、Y测点振动值说明(转载的)

滑动轴承支撑的大型旋转设备&#xff0c;绝大部分的故障都表现为不平衡引起的1倍频振动&#xff0c;诊断故障原因要根据振动随转速、负荷、温度、时间的变化情况来具体判断。滑动轴承设备的诊断主要依据电涡流传感器测量轴和轴瓦间的相对振动&#xff0c;判断转子相关的各种问题…

基于SpringBoot的共享汽车管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…

Orin安装ssh、vnc教程

文章目录一&#xff1a;ssh远程终端的配置PC的配置MobaXterm的下载二&#xff1a;VNC Viewer远程图形界面终端配置&#xff1a;PC配置&#xff1a;一&#xff1a;ssh远程 终端的配置 1.ifconfig查看终端ip地址 其中的eth是网口&#xff0c;我们需要看的是wlan0下的inet&#…

5M1270ZT144A5N CPLD 980MC 6.2NS 144TQFP /5M1270ZT144C5N

【产品介绍】MAX V系列低成本和低功耗CPLD提供更大的密度和每占地面积的I/O。MAX V器件的密度从40到2210个逻辑元件(32到1700个等效宏单元)和多达271个I/O&#xff0c;为I/O扩展、总线和协议桥接、电源监控和控制、FPGA配置和模拟IC接口等应用提供可编程解决方案。MAX V器件具有…

MYSQL 索引失效的十个场景(二)

六、对索引列运算&#xff08;如&#xff0c;、-、*、/、%等&#xff09;&#xff0c;索引失效 CREATE TABLE student (id bigint(20) NOT NULL AUTO_INCREMENT,name varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,score decimal(10,2) DEFAULT NULL,subject varchar(…

【华为OD机试模拟题】用 C++ 实现 - 绘图机器(2023.Q1)

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

【Leedcode】环形链表必备的面试题和证明题(附图解)

环形链表必备的面试题和证明题&#xff08;附图解&#xff09; 文章目录环形链表必备的面试题和证明题&#xff08;附图解&#xff09;前言一、第一题1.题目2.思路3.代码4.延伸问题(1)证明题一&#xff1a;(2)证明题二&#xff1a;二、第二题1.题目2.思路延伸的证明题总结前言 …

【网络原理8】HTTP请求篇

在上一篇文章当中&#xff0c;我们也提到了什么是HTTP。 每一个HTTP请求&#xff0c;都会对应一个HTTP响应。 下面这一篇文章&#xff0c;将聊一下HTTP请求的一些内容 目录 一、URL 第一部分&#xff1a;协议名称 第二部分:认证信息(新的版本已经没有了) 第三部分&#xf…

这款 Python 调试神器推荐收藏

大家好&#xff0c;对于每个程序开发者来说&#xff0c;调试几乎是必备技能。 代码写到一半卡住了&#xff0c;不知道这个函数执行完的返回结果是怎样的&#xff1f;调试一下看看 代码运行到一半报错了&#xff0c;什么情况&#xff1f;怎么跟预期的不一样&#xff1f;调试一…

【教学类-10-03】python单线程下载哔哩哔哩网址(中间字母不同,前面后面相同)的视频

背景需求&#xff1a;最近测试以前的多线程&#xff08;同时下载5个视频&#xff09;&#xff0c;结果30个视频只下到了3个&#xff0c;于是把“单个下载&#xff08;单线程下载&#xff09;”的一个代码进行拓展研究。前一篇介绍了网址尾数递增的遍历程序&#xff0c;本篇介绍…