算法学习 Day03 链表基础1

96 阅读4分钟

203. 移除链表元素

文章讲解

视频讲解

题目:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

image.png

解题思路

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        if head is None:
            return head

        dummy = ListNode(-1, next=head)
        p = dummy

        # 删除一个节点需要它的前驱
        while p.next is not None:
            if p.next.val == val: # 注意:判断p.next.val节点的值(p=dummy时,p.next是第一个节点)
                p.next = p.next.next
            else:
                p = p.next
            
        return dummy.next

总结

这道题需要我们仔细思考链表的操作细节,尤其是对头节点和中间节点的处理。使用虚拟头节点可以简化代码,并统一处理头节点被删除的情况。

707. 设计链表

文章讲解

视频讲解

题目:

实现 `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` 的节点。

解题思路

实现单向链表,即每个节点仅存储本身的值和后继节点。除此之外,我们还需要一个虚拟节点dummy作为头节点,和一个 size 参数保存有效节点数。额外维护一个tail节点,方便删除尾部节点。

  • 时间复杂度:插入删除O(1),查找O(n)
  • 空间复杂度:O(1)
class ListNode:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next

class MyLinkedList:
    def __init__(self):
        self.dummy = ListNode(-1)
        self.size = 0
        self.tail = self.dummy

    def get(self, index: int) -> int:
        if index < 0 or index >= self.size:
            return -1

        current = self.dummy.next
        for i in range(index):
            current = current.next
        return current.val

    def addAtHead(self, val: int) -> None:
        new_node = ListNode(val, self.dummy.next)
        self.dummy.next = new_node
        if self.size == 0:
            self.tail = new_node
        self.size += 1

    def addAtTail(self, val: int) -> None:
        new_node = ListNode(val)
        self.tail.next = new_node
        self.tail = new_node
        self.size += 1

    def addAtIndex(self, index: int, val: int) -> None:
        ''' 插入节点到idx之前 '''
        if index<0 or index>self.size: # =size时插入到末尾
            return 
            
        # 找到idx的前驱    
        cur = self.dummy
        for i in range(index):
            cur = cur.next
        
        # 插入节点
        node = ListNode(val, cur.next)
        cur.next = node
        
        # 处理tail和size
        if index == self.size:
            self.tail = node
        self.size += 1
        
    def deleteAtIndex(self, index: int) -> None:
        ''' 删除第idx个节点 '''
        if index<0 or index>= self.size:
            return 
            
        # 找到idx的前驱    
        cur = self.dummy
        for i in range(index):
            cur = cur.next
        
        # 删除节点
        cur.next = cur.next.next 
            
        # 处理tail和size
        if index == self.size-1:
            self.tail = cur
        self.size -= 1
        

总结

这道题的主要难点和知识点可以总结为以下几点:

  • 1 虚拟头节点的使用: 使用虚拟头节点可以简化链表的操作,避免对头节点的特殊处理。

  • 2 维护 sizetail 指针: 通过维护链表的大小 size 和尾部指针 tail,可以方便地进行插入和删除操作。

  • 3 定位目标节点的前驱节点: 在插入和删除时,需要先找到目标节点的前驱节点,然后进行相应的操作。这需要遍历链表,时间复杂度与操作的位置有关。

  • 4 边界条件的处理: 需要仔细考虑一些边界条件,比如插入时 index 等于链表长度的情况,或者删除时 index 超出链表长度的情况。

206. 反转链表

文章讲解

视频讲解

题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

image.png

解题思路1: 递归

首先确保有两个节点;然后按照函数定义反转第二个开始的节点,并返回last节点作为反转链表的头节点;最后把head作为尾节点,接在second节点的后面。

  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 确保两个节点以上
        if head is None or head.next is None:
            return head
        
        # 反转第二个节点开始的链表
        last_node = self.reverseList(head.next)
        # 第二个节点变成倒数第二个,指向head
        head.next.next = head
        head.next = None

        return last_node
        

解题思路2:迭代(双指针)

使用双指针挨个反转相邻的两个节点。

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head is None or head.next is None:
            return head
        prev = None
        cur = head
        while cur is not None:
            nxt = cur.next
            cur.next = prev
            prev= cur
            cur = nxt

        # 注意:cur迭代到None了,此时prev在last node位置
        return prev

总结

两种解法各有优缺点:

  • 递归解法代码更加简洁易懂,但需要额外的栈空间,在链表很长的情况下可能会导致栈溢出。
  • 迭代解法不需要额外的栈空间,但代码相对复杂一些。