Swift 数据结构与算法( 21) 链表 + M_Leetcode707. 设计链表

62 阅读2分钟

概念

  1. 链表的基础概念:了解链表结构,如节点(Node)和指针(next,或在双链表中的prev)。

  2. 链表操作的实现

    • 插入:在链表的头部、尾部、或中间插入一个新节点。
    • 删除:从链表的头部、中间、或尾部删除一个节点。
    • 检索:根据索引查找链表中的节点。
  3. 链表与数组的不同:链表在插入和删除操作时通常更高效,因为它不需要移动元素。但是,链表在访问元素时不如数组高效,因为它不支持随机访问。

  4. 指针和引用的操作:如何使用指针或引用来遍历链表、插入新节点、删除节点等。

  5. 边界和异常情况的处理:例如,当索引超出链表长度时,或当链表为空时,如何处理。

  6. 哑节点(Dummy Node)的使用:哑节点可以简化某些链表操作,特别是插入和删除,因为你不需要为头节点和其他节点编写不同的逻辑。

个人易错点

  1. 未处理特殊情况:在链表操作中,头节点和尾节点的处理通常与中间节点不同。例如,在addAtIndex方法中,当index为0时,应该在头部添加节点,但是在原始代码中这一点被遗漏了。
  2. 遍历的条件设置:在多个函数中,特别是addAtIndexdeleteAtIndex,遍历链表以找到指定位置时,应确保遍历条件正确。例如,为了找到索引前一个节点,应该使用number < index - 1而不是number < index
  3. 边界条件的检查:在执行操作之前,检查索引或节点是否有效非常重要。例如,在get方法中,应检查索引是否小于0,并在deleteAtIndex中检查当前节点的下一个节点是否存在。
  4. 冗余代码:在某些地方,可能会重复执行相同的操作。例如,在addAtIndex中,有多处设置newNode.next的代码,这可能会导致错误或不必要的操作。

为了避免这些错误,在实现链表操作时:

  1. 首先处理特殊情况,如头节点和尾节点。
  2. 画图:在处理链表问题时,画出节点图可以帮助您更好地理解和实现操作。
  3. 测试各种情况:一旦实现了链表操作,测试各种可能的情况,特别是边界情况,以确保代码的正确性。

题目

707. 设计链表

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性: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

 

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndex 和 deleteAtIndex 的次数不超过 2000 。

解题思路🙋🏻‍ ♀️

为什么是 while current != nil && number < index - 1 { number < index - 1, 而不是 index ?

当需要在链表的指定索引位置插入一个新节点或删除一个节点时,我们实际上需要找到该索引前面的节点(也就是索引位置的前一个节点)。这样,我们可以通过修改这个前一个节点的next指针来插入新节点或删除节点。

考虑以下情况:

链表为: 1→2→3→4

如果我们想在索引为2的位置插入一个新节点5,那么链表应该变为: 1→2→5→3→4

注意,为了在索引2处插入新节点,我们实际上需要找到索引1处的节点,然后修改这个节点的next指针,使其指向新节点。

因此,我们需要遍历链表,直到我们到达索引位置的前一个节点。这就是为什么我们在while循环中使用number < index - 1条件的原因。这样,当循环结束时,current会指向索引位置的前一个节点。

为什么 while current?.next != nil && number < index - 1 {< 而不是 <=

使用<而不是<=,主要是为了确保在遍历结束后,current指针指向的是指定索引前一个节点。这样我们可以在该位置插入或删除节点。

为什么这样做呢?考虑以下几点:

  1. 插入操作:如果我们想在指定的索引位置插入一个新节点,我们需要修改前一个节点的next指针,使其指向新节点。为了完成这一操作,我们需要找到前一个节点
  2. 删除操作:如果我们想删除指定索引的节点,我们同样需要修改前一个节点的next指针,使其跳过要删除的节点,直接指向下一个节点。

为了更好地理解,考虑以下示例:

链表为:1→2→3→4

假设我们想在索引2的位置插入新节点5。我们希望链表变为:1→2→5→3→4

为了实现这一操作,我们需要:

  • 找到索引1的节点(也就是值为2的节点)。
  • 修改这个节点的next指针,使其指向新节点5。

如果我们在遍历时使用<=,那么current将会指向索引2的节点(值为3的节点),而不是我们所需要的前一个节点。

因此,我们使用number < index - 1来确保在循环结束后,current指向的是指定索引前一个节点。这样,我们可以方便地进行插入或删除操作。

边界思考🤔

代码

// 定义自定义链表类
class MyLinkedList {

    // 头节点,初始为nil
    private var head: ListNode?
    
    // 初始化函数,设置头节点为nil
    init() {
        head = nil
    }
    
    // 获取链表中下标为index的节点的值。如果下标无效,则返回-1
    func get(_ index: Int) -> Int {
        // 设置当前节点为头节点
        var current = head
        
        // 如果索引小于0,则返回-1
        if index < 0 {
            return -1
        }
        
        var number = 0
        // 遍历链表,直到达到指定的索引或链表末尾
        while current != nil && number < index {
            current = current?.next
            number += 1
        }
        // 返回找到的节点的值,如果节点为nil,则返回-1
        return current?.val ?? -1
    }
    
    // 在链表头部添加一个新节点
    func addAtHead(_ val: Int) {
        // 创建新节点
        let newNode = ListNode(val)
        // 新节点的下一个节点指向原头节点
        newNode.next = head
        // 更新头节点为新节点
        head = newNode
    }
    
    // 在链表尾部添加一个新节点
    func addAtTail(_ val: Int) {
        // 创建新节点
        let newNode = ListNode(val)
        // 设置当前节点为头节点
        var current = head
        
        // 如果链表为空,直接将头节点设置为新节点
        if head == nil {
            head = newNode
            return
        }
        
        // 遍历链表,直到找到最后一个节点
        while current?.next != nil {
            current = current?.next
        }
        // 将最后一个节点的下一个节点设置为新节点
        current?.next = newNode
    }
    
    // 在指定索引添加一个新节点
    func addAtIndex(_ index: Int, _ val: Int) {
        // 创建新节点
        let newNode = ListNode(val)
        // 设置当前节点为头节点
        var current = head
        var number = 0
        
        // 如果索引为0,直接在头部添加新节点
        if index == 0 {
            newNode.next = head
            head = newNode
            return
        }

        // 遍历链表,直到找到索引前一个节点
        while current != nil && number < index - 1 {
            current = current?.next
            number += 1
        }
        
        // 如果找到了该节点,将新节点插入到它后面
        if current != nil {
            newNode.next = current?.next
            current?.next = newNode
        }
    }
    
    // 在指定索引删除节点
    func deleteAtIndex(_ index: Int) {
        // 如果索引为0,直接删除头节点
        if index == 0 {
            head = head?.next
            return
        }

        // 设置当前节点为头节点
        var current = head
        var number = 0
        
        // 遍历链表,直到找到索引前一个节点
        while current != nil && number < index - 1 {
            current = current?.next
            number += 1
        }
        
        // 如果找到了该节点,并且它的下一个节点不为nil,删除它的下一个节点
        if current?.next != nil {
            current?.next = current?.next?.next
        }
    }
}

错题集

func get(_ index: Int) -> Int {
    if index < 0 { return -1 }
    
    var current = head
    var number = 0
    
    // ❌ 错误的部分:您在返回`current`的值之前已经更新了它。
    while current != nil && number <= index {
        current = current?.next
        number += 1
    }
    // ✅ 正确的做法:在更新`current`之前返回它的值。
    while current != nil && number < index {
        current = current?.next
        number += 1
    }
    
    return current?.val ?? -1
}

func addAtIndex(_ index: Int, _ val: Int) {
    let newNode = ListNode(val)
    
    if index == 0 {
        newNode.next = head
        head = newNode
        return
    }
    
    var current = head
    var number = 0
    
    // ❌ 错误的部分:代码逻辑重复和混淆。
    while current != nil && number <= index - 1 {
        if number == index - 1 {
            newNode.next = current?.next
            head = newNode // ❌ 这里不应该更新head。
        }
        newNode.next = current?.next // ❌ 多次连接newNode是不必要的。
        number += 1
    }
    
    // ✅ 正确的做法:在找到正确的位置后只连接一次。
    while current != nil && number < index - 1 {
        current = current?.next
        number += 1
    }
    if current != nil {
        newNode.next = current?.next
        current?.next = newNode
    }
}

func deleteAtIndex(_ index: Int) {
    if index < 0 { return }
    
    // ❌ 错误的部分:当`index`为0时,您没有删除头节点。
    if index == 0 {
        head = head?.next
        return
    }
    
    var current = head
    var number = 0
    
    // ❌ 错误的部分:您的删除逻辑有误。
    while current != nil && number < index {
        if number == index && current?.next != nil {
            current?.next = current?.next?.next
        }
        number += 1
    }
    
    // ✅ 正确的做法:在找到索引前一个节点后执行删除操作。
    while current != nil && number < index - 1 {
        current = current?.next
        number += 1
    }
    if current?.next != nil {
        current?.next = current?.next?.next
    }
}

时空复杂度分析

引用

本系列文章部分概念内容引用 www.hello-algo.com/

解题思路参考了 abuladong 的算法小抄, 代码随想录... 等等

Youtube 博主: huahua 酱, 山景城一姐,