代码随想录训练营 Day3打卡 链表part01 203.移除链表元素 707.设计链表 206.反转链表

代码随想录训练营 Day3打卡 链表part01

一、 链表理论基础

链表基础

链表是一种动态数据结构,由一系列节点(Node)组成,每个节点包含两部分:数据和指向下一个节点的引用(或指针)。链表的种类主要有以下几种:

单向链表(Singly Linked List):每个节点只包含一个指向下一个节点的引用。
双向链表(Doubly Linked List):每个节点包含两个引用,一个指向下一个节点,另一个指向前一个节点。
循环链表(Circular Linked List):链表的最后一个节点指向第一个节点,形成一个环。

单向链表的实现示例(Python)

class Node:
    def __init__(self, data=None):
        """
        初始化节点。
        :param data: 节点存储的数据
        """
        self.data = data  # 节点的数据
        self.next = None  # 指向下一个节点的指针,初始为 None

class SinglyLinkedList:
    def __init__(self):
        """
        初始化单向链表。
        """
        self.head = None  # 链表的头节点,初始为 None

    def append(self, data):
        """
        向链表末尾添加新节点。
        :param data: 新节点存储的数据
        """
        new_node = Node(data)  # 创建新节点
        if not self.head:
            # 如果链表为空,将新节点设为头节点
            self.head = new_node
            return
        last = self.head
        # 遍历链表,找到最后一个节点
        while last.next:
            last = last.next
        last.next = new_node  # 将最后一个节点的 next 指针指向新节点

    def display(self):
        """
        打印链表中的所有节点。
        """
        current = self.head
        while current:
            print(current.data, end=" -> ")  # 打印当前节点的数据
            current = current.next  # 移动到下一个节点
        print("None")  # 打印链表的结尾

# 示例用法
linked_list = SinglyLinkedList()
linked_list.append(1)  # 向链表添加节点,数据为 1
linked_list.append(2)  # 向链表添加节点,数据为 2
linked_list.append(3)  # 向链表添加节点,数据为 3
linked_list.display()  # 输出链表中的所有节点: 1 -> 2 -> 3 -> None

数组和链表的区别

内存分配:
数组:内存连续分配,元素在内存中紧密排列。
链表:内存非连续分配,节点可以分散在内存中的任意位置,通过指针连接。
存取时间:
数组:支持随机访问,可以通过索引在 O(1) 时间内访问任意元素。
链表:不支持随机访问,访问元素需要从头节点开始遍历,平均时间复杂度为 O(n)。
插入和删除:
数组:插入和删除操作需要移动元素,时间复杂度为 O(n)。
链表:插入和删除操作只需改变指针,时间复杂度为 O(1),但需要找到插入或删除位置,平均时间复杂度为 O(n)。
内存利用:
数组:需要在声明时确定大小,可能会造成内存浪费或不足。
链表:动态分配内存,根据需要增长或缩小,不会造成内存浪费。
空间开销:
数组:除了存储数据本身外,没有额外的空间开销。
链表:每个节点需要额外存储指针信息,有额外的空间开销。
再把链表的特性和数组的特性进行一个对比,如图所示:
在这里插入图片描述

二、 移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
在这里插入图片描述
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]

在这里插入图片描述

版本一 虚拟头节点法

思路讲解:

  • 虚拟头节点技巧:为了简化删除操作,特别是当头节点需要被删除时的处理,引入一个虚拟头节点。这个节点的next指针指向原来的链表头,这样做的目的是使得删除操作始终可以从链表的第一个有效节点开始考虑,而不用担心修改头节点指针导致链表丢失。
  • 遍历与删除:通过一个循环遍历整个链表,当前指针current始终指向待检查节点的前一个节点。如果发现current.next.val等于要删除的值val,就通过current.next
    = current.next.next来删除该节点(实际上是覆盖掉,原节点因为失去引用会被垃圾回收)。
  • 处理完成:遍历完成后,返回dummy_head.next作为新链表的头节点,此时链表中所有等于val的节点已经被成功移除。
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        # 创建一个虚拟头节点,其next指针指向原链表的头节点。
        dummy_head = ListNode(next=head)
        
        # 初始化一个指针`current`指向虚拟头节点,用于遍历链表。
        current = dummy_head
        
        # 开始遍历链表,直到`current.next`为空,即遍历完所有有效节点。
        while current.next:
            # 如果当前节点的下一个节点的值等于`val`,则跳过下一个节点,
            # 实现删除操作,即修改当前节点的next指针指向下一个节点的next。
            if current.next.val == val:
                current.next = current.next.next
            else:
                # 如果当前节点的下一个节点不需要被删除,则移动到下一个节点继续检查。
                current = current.next
                
        return dummy_head.next

版本二 直接使用原链表

思路讲解:

  • 不使用虚拟头节点的情况下,直接操作原始的head可能会导致一些特殊情况下处理不当,特别是当链表的头节点就是要删除的值时。
    因此,在循环开始前,我们先检查头节点是否需要被删除,如果头节点的值等于val,就将head指针直接移到下一个节点,相当于删除了原头节点。
  • 然后,通过一个循环遍历链表,对于每一个节点,检查其下一个节点的值是否等于val,如果是,则当前节点的next指针跳过下一个节点,直接指向再下一个节点,从而实现删除操作。
  • 最后,返回处理后的head作为新的链表头节点。这样就避免了因直接删除头节点而丢失链表引用的问题。
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        # 初始化一个指针作为当前节点,同时处理头节点可能就需要被删除的情况
        while head and head.val == val:
            head = head.next  # 删除头节点,直接将head指向下下一个节点
        
        current = head  # 将current指针置于处理后的head位置,开始遍历
        
        # 遍历列表并删除值为val的节点
        while current and current.next:
            if current.next.val == val:
                current.next = current.next.next  # 删除当前节点的下一个节点
            else:
                current = current.next  # 移动到下一个节点继续检查
        
        return head  # 返回处理后的链表头节点

力扣题目链接
题目文章讲解
题目视频讲解

三、 设计链表

你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始
实现 MyLinkedList 类:
MyLinkedList() 初始化 MyLinkedList 对象。
int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。
示例:
输入
[“MyLinkedList”, “addAtHead”, “addAtTail”, “addAtIndex”, “get”, “deleteAtIndex”, “get”]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]
解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
myLinkedList.get(1); // 返回 3

删除链表节点:
在这里插入图片描述

添加链表节点:
在这里插入图片描述
为了方便操作链表,我们可以使用一个虚拟头节点(dummy head)。这样可以简化在链表头部进行插入和删除操作的逻辑。下面我们提供两个版本的链表实现:单链表和双链表。

版本一 单链表法

实现思路
节点类(ListNode):定义一个包含值和下一个节点指针的节点类。
链表类(MyLinkedList):

  • 使用一个虚拟头节点,初始化时设置链表大小为0。
  • 实现获取、在头部添加、在尾部添加、在指定位置添加和删除节点的方法。
# 定义单链表节点类
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val  # 节点存储的值
        self.next = next  # 指向下一个节点的指针

# 定义单链表类
class MyLinkedList:
    def __init__(self):
        self.dummy_head = ListNode()  # 虚拟头节点
        self.size = 0  # 链表大小初始化为0

    # 获取链表中第index个节点的值
    def get(self, index: int) -> int:
        if index < 0 or index >= self.size:  # 检查索引范围
            return -1
        
        current = self.dummy_head.next  # 从虚拟头节点的下一个节点开始
        for i in range(index):  # 遍历到第index个节点
            current = current.next
            
        return current.val

    # 在链表头部添加一个节点
    def addAtHead(self, val: int) -> None:
        self.dummy_head.next = ListNode(val, self.dummy_head.next)  # 新节点指向旧头节点
        self.size += 1  # 链表大小加1

    # 在链表尾部添加一个节点
    def addAtTail(self, val: int) -> None:
        current = self.dummy_head
        while current.next:  # 遍历到链表末尾
            current = current.next
        current.next = ListNode(val)  # 新节点插入到链表末尾
        self.size += 1  # 链表大小加1

    # 在链表指定位置添加一个节点
    def addAtIndex(self, index: int, val: int) -> None:
        if index < 0 or index > self.size:  # 检查索引范围
            return
        
        current = self.dummy_head
        for i in range(index):  # 遍历到指定位置的前一个节点
            current = current.next
        current.next = ListNode(val, current.next)  # 新节点插入到指定位置
        self.size += 1  # 链表大小加1

    # 删除链表中指定位置的节点
    def deleteAtIndex(self, index: int) -> None:
        if index < 0 or index >= self.size:  # 检查索引范围
            return
        
        current = self.dummy_head
        for i in range(index):  # 遍历到指定位置的前一个节点
            current = current.next
        current.next = current.next.next  # 删除指定位置的节点
        self.size -= 1  # 链表大小减1

# 示例用法
obj = MyLinkedList()
obj.addAtHead(1)
obj.addAtTail(2)
obj.addAtIndex(1, 3)
print(obj.get(1))  # 输出: 3
obj.deleteAtIndex(1)
print(obj.get(1))  # 输出: 2

版本二 双链表法

实现思路
节点类(ListNode):定义一个包含值、前一个节点指针和下一个节点指针的节点类。
链表类(MyLinkedList):

  • 初始化时设置链表头和尾为None,大小为0。
  • 实现获取、在头部添加、在尾部添加、在指定位置添加和删除节点的方法。

双链表的实现中通常不需要虚拟头节点(dummy head)。因为双链表本身已经可以通过头节点(head)和尾节点(tail)方便地进行操作。

# 定义双链表节点类
class ListNode:
    def __init__(self, val=0, prev=None, next=None):
        self.val = val  # 节点存储的值
        self.prev = prev  # 指向前一个节点的指针
        self.next = next  # 指向下一个节点的指针

# 定义双链表类
class MyLinkedList:
    def __init__(self):
        self.head = None  # 链表头节点
        self.tail = None  # 链表尾节点
        self.size = 0  # 链表大小初始化为0

    # 获取链表中第index个节点的值
    def get(self, index: int) -> int:
        if index < 0 or index >= self.size:  # 检查索引范围
            return -1
        
        if index < self.size // 2:  # 如果索引在前半部分
            current = self.head
            for i in range(index):  # 从头节点开始遍历
                current = current.next
        else:  # 如果索引在后半部分
            current = self.tail
            for i in range(self.size - index - 1):  # 从尾节点开始遍历
                current = current.prev
                
        return current.val

    # 在链表头部添加一个节点
    def addAtHead(self, val: int) -> None:
        new_node = ListNode(val, None, self.head)  # 创建新节点
        if self.head:  # 如果头节点存在
            self.head.prev = new_node
        else:  # 如果头节点不存在,说明链表为空
            self.tail = new_node
        self.head = new_node  # 更新头节点
        self.size += 1  # 链表大小加1

    # 在链表尾部添加一个节点
    def addAtTail(self, val: int) -> None:
        new_node = ListNode(val, self.tail, None)  # 创建新节点
        if self.tail:  # 如果尾节点存在
            self.tail.next = new_node
        else:  # 如果尾节点不存在,说明链表为空
            self.head = new_node
        self.tail = new_node  # 更新尾节点
        self.size += 1  # 链表大小加1

    # 在链表指定位置添加一个节点
    def addAtIndex(self, index: int, val: int) -> None:
        if index < 0 or index > self.size:  # 检查索引范围
            return
        
        if index == 0:  # 在头部插入
            self.addAtHead(val)
        elif index == self.size:  # 在尾部插入
            self.addAtTail(val)
        else:
            if index < self.size // 2:  # 如果索引在前半部分
                current = self.head
                for i in range(index - 1):  # 从头节点开始遍历
                    current = current.next
            else:  # 如果索引在后半部分
                current = self.tail
                for i in range(self.size - index):  # 从尾节点开始遍历
                    current = current.prev
            new_node = ListNode(val, current, current.next)  # 创建新节点
            current.next.prev = new_node  # 更新指针
            current.next = new_node  # 更新指针
            self.size += 1  # 链表大小加1

    # 删除链表中指定位置的节点
    def deleteAtIndex(self, index: int) -> None:
        if index < 0 or index >= self.size:  # 检查索引范围
            return
        
        if index == 0:  # 删除头节点
            self.head = self.head.next
            if self.head:
                self.head.prev = None
            else:  # 链表为空时更新尾节点
                self.tail = None
        elif index == self.size - 1:  # 删除尾节点
            self.tail = self.tail.prev
            if self.tail:
                self.tail.next = None
            else:  # 链表为空时更新头节点
                self.head = None
        else:
            if index < self.size // 2:  # 如果索引在前半部分
                current = self.head
                for i in range(index):  # 从头节点开始遍历
                    current = current.next
            else:  # 如果索引在后半部分
                current = self.tail
                for i in range(self.size - index - 1):  # 从尾节点开始遍历
                    current = current.prev
            current.prev.next = current.next  # 更新指针
            current.next.prev = current.prev  # 更新指针
        self.size -= 1  # 链表大小减1

# 示例用法
obj = MyLinkedList()
obj.addAtHead(1)
obj.addAtTail(2)
obj.addAtIndex(1, 3)
print(obj.get(1))  # 输出: 3
obj.deleteAtIndex(1)
print(obj.get(1))  # 输出: 2

力扣题目链接
题目文章讲解
题目视频讲解

四、 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]

我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur)
在这里插入图片描述

版本一 双指针法
  1. 首先,初始化两个指针:cur指针指向链表的头节点,而pre指针则初始化为null。这样设置是为了在反转过程中追踪当前处理的节点及其前一个节点。
  2. 接着,进入反转环节。为了在更改当前节点cur的下一个节点的指向时不受影响,需要事先用一个临时变量tmp来存储cur->next,即待反转到cur之前的那个节点。
  3. 保存cur->next到tmp是关键步骤,因为随后我们将修改cur->next,使其指向前一个节点pre,完成首节点的反转操作。
  4. 随后的过程通过循环进行,每次迭代都会将cur和pre指针向前推进一步,即pre变为当前的cur,cur变为原本的cur->next(即现在的tmp),持续这一过程直至遍历完整个链表。
  5. 当cur指针到达链表尾部(指向null)时,循环自然终止,表明链表反转完成。此时,最初的头节点已变成末尾节点,而pre指针则指向了新的头节点。
  6. 最终,函数返回pre指针,因为它现在指向的是已完全反转的链表的起始位置。
# 单链表节点的定义
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val  # 节点的值
        self.next = next  # 指向下一个节点的链接

# 解决方案类,包含反转链表的方法
class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        # 初始化两个指针,cur 指向当前处理的节点,初始时为链表头节点
        cur = head   
        # pre 指向当前节点的前一个节点,初始时为 None
        pre = None
        
        # 当 cur 不为空时,继续循环
        while cur:
            # 保存 cur 的下一个节点,因为接下来要改变 cur 的 next 指向
            temp = cur.next 
            # 反转操作:将 cur 的 next 指向其前一个节点 pre
            cur.next = pre 
            # 更新两个指针的位置,pre 移动到 cur 的位置,cur 移动到原 cur 的下一个节点(即 temp)
            pre = cur
            cur = temp
            
        # 当循环结束时,cur 已经为空,pre 指向新链表的头部
        return pre
        
版本二 递归法

递归法实现链表反转的核心思想与双指针方法一致,只是表达形式更为抽象。递归的关键在于定义基本情况(base case)和递归情况。

  • 基本情况(Base Case):当链表走到尽头,即cur == None时,返回新的头节点pre,因为此时pre实际上是指向原链表最后一个节点,反转后它将成为新链表的头节点。
  • 递归情况(Recursive Case):对于非空的cur节点,执行以下步骤:
    – 保存cur的下一个节点到temp。
    – 将cur的next指针改为指向前一个节点pre,完成一次节点反转。
    – 递归调用reverse函数,传入temp作为新的cur(即将处理的下一个节点),以及当前的cur作为新的pre(因为经过这次递归调用,当前的cur将会变成其后继节点的前驱)。
# 定义链表节点类
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    # 主函数,用于启动递归过程
    def reverseList(self, head: ListNode) -> ListNode:
        # 调用辅助函数进行递归反转,初始时pre为None
        return self.reverse(head, None)
    
    # 辅助递归函数,负责实际的反转操作
    def reverse(self, cur: ListNode, pre: ListNode) -> ListNode:
        # 基本情况:如果当前节点cur为空,表示已经到达链表尾部,
        # 此时pre指向的就是新的头节点,所以直接返回pre
        if cur == None:
            return pre
        
        # 保存当前节点的下一个节点,为下一步反转做准备
        temp = cur.next
        
        # 反转当前节点的指向,使其指向前一个节点pre
        cur.next = pre
        
        # 递归调用,处理剩余链表部分,同时更新pre和cur的角色
        # cur的下一个节点(原cur.next,现为temp)成为新的"当前节点"
        # 当前节点cur成为新节点的"前一个节点"
        return self.reverse(temp, cur)
        

力扣题目链接
题目文章讲解
题目视频讲解

  • 21
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值