707.设计链表

36 阅读12分钟

1.力扣题目链接

(opens new window)

题意:

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

707示例

2.单链表

1)

class ListNode {
    int val; // 定义一个整数型的成员变量 val,用来存储节点的值
    ListNode next;// 定义一个 ListNode 类型的成员变量 next,用来存储指向下一个节点的引用

    ListNode() {
    }// 无参数的构造函数,初始化一个新的节点,val 默认为 0,next 默认为 null

    ListNode(int val) {
        this.val = val;
// 带有整数参数的构造函数,初始化一个新的节点,val 被设置为传入的参数,next 默认为 null
    }

}

这段代码定义了一个名为 ListNode 的类,它代表了单链表中的节点。每个节点都有两个成员变量,val 用于存储节点的值,next 是一个引用,用于指向下一个节点。这个类提供了两个构造函数,一个是无参数的构造函数,用于创建一个值为 0 且没有下一个节点的节点,另一个是带有整数参数的构造函数,用于创建一个指定值的节点,但没有下一个节点。

2)

class MyLinkedList {
    int size;           // 存储链表元素的个数
    ListNode head;      // 虚拟头结点

    // 初始化链表
    public MyLinkedList() {
        size = 0;       // 初始时链表元素个数为0
        head = new ListNode(0); // 创建一个虚拟头结点,值为0,但不包含实际数据
    }

    // 获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
    public int get(int index) {
        // 如果index非法,返回-1
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode currentNode = head; // 从虚拟头结点开始
        // 包含一个虚拟头节点,所以查找第 index+1 个节点
        for (int i = 0; i <= index; i++) {
            currentNode = currentNode.next; // 通过.next属性移动到下一个节点
        }
        return currentNode.val; // 返回第index个节点的值
    }
} 

这段代码定义了一个链表,其中包括初始化链表和获取链表中指定位置节点值的功能。

  • 构造函数 MyLinkedList() 初始化一个空链表,其中 size 初始化为0,而 head 是一个虚拟头结点,值为0,但它并不包含实际的数据,只是用来占据头结点的位置。

  • get(int index) 方法用于获取链表中指定位置 index 的节点值。它首先检查 index 是否合法(即在 0 到 size-1 的范围内),如果不合法,则返回 -1。然后,它通过循环遍历链表,从虚拟头结点开始,找到第 index 个节点,并返回该节点的值。

这是一个基本的链表实现,用于演示链表的基本操作,例如获取节点值。在实际应用中,可以根据需要扩展该链表,添加其他操作如插入、删除等。

index < 0 || index >= size

在这段代码中,index 表示要获取的节点在链表中的位置,而 size 表示链表当前的元素个数。因此,当 index 大于或等于 size 时,表示要获取的节点位置超出了链表的范围,这是一个非法操作。

链表的位置是从 0 开始计数的,所以有效的位置范围是从 0 到 size-1。如果 index 小于 0 或大于等于 size,则意味着要么获取一个负数位置的节点,要么获取一个超出链表长度的位置的节点,这都是不合法的操作,因此在这种情况下,代码返回 -1,表示获取失败。

这种处理方式是为了确保在获取链表中的节点时,不会发生越界访问的情况,从而保证了代码的稳定性和健壮性。如果在实际应用中需要执行其他操作(例如插入、删除等),也应该首先检查 index 是否合法,以避免出现类似的问题。

3)

//在链表最前面插入一个节点,等价于在第0个元素前添加
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }


    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }

        if (index < 0) {
            index = 0;
        }

        size++;

        //找到要插入节点的前驱
        ListNode pred = head;
        for(int i = 0; i < index; i++){
            pred = pred.next;
        }
        
        ListNode toAdd = new ListNode(val);
        toAdd.next = pred.next;
        pred.next = toAdd;
    }

  1. addAtHead(int val) 方法用于在链表最前面插入一个节点,也就是在第 0 个位置之前插入节点。它等价于调用 addAtIndex(0, val) 方法。

  2. addAtTail(int val) 方法用于在链表的最后插入一个节点,也就是在链表的末尾之后插入节点。它等价于调用 addAtIndex(size, val) 方法,其中 size 表示链表的当前长度,所以新节点会添加到末尾的位置。

  3. addAtIndex(int index, int val) 方法用于在指定位置 index 之前插入一个新节点,并将值设置为 val。该方法首先检查 index 是否合法,如果 index 大于链表的长度 size,则不执行任何操作,直接返回。如果 index 小于 0,则将其设为 0,以确保在链表头部插入。然后,它逐步找到要插入节点位置的前驱节点 pred,然后创建一个新的节点 toAdd,将新节点的 next 指向 pred.next,然后将 pred.next 指向新节点 toAdd,从而将新节点插入到链表中。

这些方法允许您以不同的方式向链表中插入节点,包括在头部、尾部和指定位置插入。这些操作是链表常见的基本操作之一,用于构建和修改链表。

 

size++;

        //找到要插入节点的前驱
        ListNode pred = head;
        for(int i = 0; i < index; i++){
            pred = pred.next;
        }
        
        ListNode toAdd = new ListNode(val);
        toAdd.next = pred.next;
        pred.next = toAdd;

这段代码的主要作用是在指定位置 index 之前插入一个新的节点,并将其值设置为 val。下面是这段代码的逐行解释:

  1. size++;:这一行代码增加了链表的 size 变量的值,表示链表中元素的个数增加了一个。这是因为在插入新节点之后,链表的长度会增加。

  2. ListNode pred = head;:这一行代码创建了一个名为 pred 的节点对象,并将其初始化为链表的虚拟头结点 headpred 用于存储要插入节点的前驱节点。

  3. for(int i = 0; i < index; i++) { pred = pred.next; }:这是一个循环,它从虚拟头结点开始,通过 pred 依次遍历链表,直到找到要插入节点位置的前一个节点。循环中的 i 从 0 开始,每次迭代都将 pred 移动到下一个节点,直到 i 的值等于 index,此时 pred 就指向了要插入节点的前驱节点。

  4. ListNode toAdd = new ListNode(val);:这一行代码创建了一个新的节点 toAdd,并将其值初始化为传入的参数 val

  5. toAdd.next = pred.next;:这一行代码将新节点 toAddnext 指向 prednext,也就是指向了要插入节点位置的后继节点。

  6. pred.next = toAdd;:最后,这一行代码将 prednext 指向新节点 toAdd,完成了插入操作。这时,新节点 toAdd 被插入到 predpred.next 之间,链表的结构发生了变化,新节点成为了链表中的一个元素。

通过这些步骤,新节点 toAdd 成功地插入到指定位置 index 之前,完成了链表插入操作。

初始链表: 1 -> 3 -> 5

现在,我们要在索引 1 处(从 0 开始计数)插入一个新节点,值为 2

  1. size++;:原链表的 size 值为 3,表示链表中有 3 个元素。执行这行代码后,size 增加 1,变为 4。

  2. ListNode pred = head;:创建一个名为 pred 的节点,并将其初始化为链表的虚拟头结点 head。此时 pred 指向虚拟头结点。

  3. for(int i = 0; i < index; i++) { pred = pred.next; }:这是一个循环,从虚拟头结点开始,依次移动 pred 指向链表中的节点,直到 i 的值等于 index,也就是 1。在这个过程中,pred 将指向索引 1 之前的节点,也就是节点 1

  4. ListNode toAdd = new ListNode(val);:创建一个新的节点 toAdd,其值初始化为 2

  5. toAdd.next = pred.next;:将新节点 toAddnext 指向 prednext。在我们的例子中,prednext 是节点 3

  6. pred.next = toAdd;:将 prednext 指向新节点 toAdd。这一步相当于将新节点 toAdd 插入到 predpred.next 之间,即在索引 1 处插入节点 2

最终,链表变为:

新链表: 1 -> 2 -> 3 -> 5

通过这段代码,我们成功地在索引 1 处插入了一个值为 2 的新节点,同时更新了链表的 size。这展示了如何使用这段代码在指定位置插入新节点。

4)

//删除第index个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }

        size--;
        if (index == 0) {
            head = head.next;
            return;
        }

        ListNode pred = head;
        for (int i = 0; i < index; i++) {
            pred = pred.next;
        }

        pred.next = pred.next.next;
    }
}

这段代码是用于在链表中删除指定位置 index 处的节点的操作。以下是这段代码的逐行解释:

  1. if (index < 0 || index >= size):这一行代码首先检查 index 是否合法,即是否在链表范围内。如果 index 小于 0 或大于等于链表的长度 size,则表示要删除的节点位置不合法,直接返回,不执行任何操作。

  2. size--;:如果要删除的节点位置合法,首先将链表的 size 减1,表示链表中的元素数量减少了一个。

  3. if (index == 0):这一行代码检查是否要删除的节点位置是链表的头节点,即 index 是否为 0。如果是头节点,就执行下面的操作:

    • head = head.next;:将链表的头结点 head 更新为头结点的下一个节点 head.next,从而跳过了原头节点,实现了删除操作。然后直接返回,操作完成。
  4. 如果 index 不是头节点,那么程序会执行以下操作:

    • ListNode pred = head;:创建一个名为 pred 的节点,并将其初始化为链表的虚拟头结点 head。这个节点 pred 用来找到要删除节点的前驱节点。

    • for (int i = 0; i < index; i++) { pred = pred.next; }:这是一个循环,从虚拟头结点开始,依次移动 pred 指向链表中的节点,直到 i 的值等于 index。在这个过程中,pred 将指向要删除节点位置的前一个节点。

    • pred.next = pred.next.next;:最后,将前驱节点 prednext 指向要删除节点的下一个节点 pred.next.next,从而跳过了要删除的节点,完成删除操作。

通过这段代码,您可以在链表中删除指定位置 index 处的节点,如果 index 合法且不是头节点,它将执行相应的删除操作。这是链表中的基本删除操作之一。

初始链表: 1 -> 2 -> 3 -> 4 -> 5

现在,我们要删除链表中的一些节点。

  1. 删除索引为 2 的节点(值为 3):

    • if (index < 0 || index >= size) 检查 index 是否合法,2 是合法的。

    • size--; 减小链表的 size,变为 4

    • 因为 index 不是头节点,所以程序进入下面的操作:

      • ListNode pred = head; 创建 pred 并初始化为虚拟头结点 head

      • for (int i = 0; i < index; i++) { pred = pred.next; } 在循环中,pred 移动到索引为 2 的前驱节点 pred,即节点 2

      • pred.next = pred.next.next; 将前驱节点 prednext 指向节点 3 的下一个节点,即节点 4,从而删除了节点 3

    最终链表变为:1 -> 2 -> 4 -> 5

  2. 删除索引为 0 的节点(值为 1):

    • if (index < 0 || index >= size) 检查 index 是否合法,0 是合法的。

    • size--; 减小链表的 size,变为 3

    • 因为 index 是头节点,所以程序进入下面的操作:

      • head = head.next; 直接将虚拟头结点 head 更新为下一个节点,即节点 2,从而删除了节点 1

    最终链表变为:2 -> 4 -> 5

  3. 删除索引为 3 的节点(值为 5):

    • if (index < 0 || index >= size) 检查 index 是否合法,3 不合法,因为链表的长度只有 3

    • 由于 index 不合法,不执行任何操作。

最终,通过这段代码,我们可以删除链表中指定位置的节点,确保操作合法并正确更新链表。

3.双链表

1)

//双链表
class ListNode {
    int val;
    ListNode next, prev;

    ListNode() {

    }

    ListNode(int val) {
        this.val = val;
    }
}

这段代码定义了一个名为 ListNode 的类,用于表示双向链表(Doubly Linked List)中的节点。双向链表是一种链表数据结构,每个节点不仅包含一个指向下一个节点的引用(next),还包含一个指向前一个节点的引用(prev),这使得在双向链表中可以双向遍历链表,而不仅仅是单向遍历。

让我逐行解释这段代码:

  • int val;:这是一个整数类型的成员变量,用于存储节点的值。

  • ListNode next, prev;:这两个成员变量分别是指向下一个节点和前一个节点的引用。next 表示指向链表中的下一个节点,而 prev 表示指向链表中的前一个节点。

  • ListNode() { }:这是一个无参数的构造函数,用于创建一个新的节点对象。在这个构造函数中,没有初始化节点的值和引用,默认情况下会被初始化为默认值(对于整数是0,对于引用类型是null)。

  • ListNode(int val) { this.val = val; }:这是一个带有整数参数的构造函数,用于创建一个新的节点对象,并初始化节点的值为传入的整数值。这个构造函数可以用来创建节点并设置初始值。

总之,这段代码定义了一个用于双向链表的节点类 ListNode,它包含了节点的值以及两个引用,允许在链表中双向遍历节点。这种双向链表在某些情况下对于特定的操作非常有用,例如在删除节点时可以更容易地更新前一个节点的 next 引用。

2)

class MyLinkedList {

    //记录链表中元素的数量
    int size;
//记录链表的虚拟头结点和尾结点

    ListNode head, tail;

    public MyLinkedList() {

        //初始化操作
        this size = 0;
        this head = new ListNode(0);
        this tail = new ListNode(0);

        //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
        head.next = tail;
        tail.prev = head;
    }

    public int get(int index) {
        //判断index是否有效
        if (index < 0 || index >= size) {
            return -1;
        }

        ListNode cur = this.head;
        //判断是哪一边遍历时间更短
        if (index >= size / 2) {
            //tail开始
            cur = tail;
            for (int i = 0; i < size - index; i++) {
                cur = cur.prev;
            }
        } else {
            for (int i = 0; i <= index; i++) {
                cur = cur.next;
            }
        }
        return cur.val;
    }
 
  1. 记录链表中元素的数量 size

  2. 记录链表的虚拟头结点 head 和虚拟尾结点 tail。这两个虚拟节点用于简化链表操作,并且它们不存储实际的数据,只是用来占位。

  3. 初始化链表的构造函数 MyLinkedList()

让我逐行解释这段代码的具体含义:

  • int size;:这是一个整数变量,用来记录链表中元素的数量。

  • ListNode head, tail;:这是两个 ListNode 类型的变量,分别表示链表的虚拟头结点和虚拟尾结点。这两个节点并不存储实际数据,只是用来帮助管理链表的结构。

  • 构造函数 MyLinkedList()

    • size 初始化为 0,表示链表中还没有元素。
    • 创建了一个虚拟头结点 head 和一个虚拟尾结点 tail,它们的值都初始化为 0。
    • head.next = tail;tail.prev = head; 这两行代码非常重要,它们将虚拟头结点 headnext 指向虚拟尾结点 tail,同时将虚拟尾结点 tailprev 指向虚拟头结点 head,这样就形成了一个初始状态下只有虚拟头尾结点的链表,确保链表的基本结构正确。
  • public int get(int index) 方法:

    • 首先,它判断输入的 index 是否有效,即是否在链表的合法范围内。
    • 然后,它根据 index 的位置决定从哪一端(头结点还是尾结点)开始遍历链表。这是一个优化措施,如果 index 靠近链表的中间位置,从尾结点开始遍历会更快。
    • 最后,它使用循环遍历链表,找到指定位置 index 处的节点,并返回该节点的值。

总之,这段代码定义了一个包含虚拟头尾结点的链表,通过这种方式,可以在链表的开头和结尾执行添加和删除操作,同时提供了一个 get 方法用于获取指定位置的节点值。这种设计可以提高链表的操作效率。

 public int get(int index) {
        //判断index是否有效
        if (index < 0 || index >= size) {
            return -1;
        }

        ListNode cur = this.head;
        //判断是哪一边遍历时间更短
        if (index >= size / 2) {
            //tail开始
            cur = tail;
            for (int i = 0; i < size - index; i++) {
                cur = cur.prev;
            }
        } else {
            for (int i = 0; i <= index; i++) {
                cur = cur.next;
            }
        }
        return cur.val;
    }

这段代码是 MyLinkedList 类中的 get(int index) 方法,用于获取链表中指定位置 index 处的节点值。让我逐行解释这段代码的功能:

  1. if (index < 0 || index >= size):这一行代码首先检查传入的 index 是否有效,即是否在链表的合法范围内。如果 index 小于 0 或大于等于链表的长度 size,则表示要获取的位置不合法,返回 -1 表示获取失败。

  2. ListNode cur = this.head;:创建一个名为 cur 的节点引用,并将其初始化为链表的虚拟头结点 head。这是开始遍历链表的起始点。

  3. if (index >= size / 2):这一行代码判断从哪一端开始遍历链表会更快。它通过比较 indexsize 的一半来决定。如果 index 大于等于 size 的一半,就从链表的尾结点 tail 开始往前遍历,因为距离尾部更近。这是一种优化,可以减少遍历的次数,提高效率。

  4. cur = tail;:如果从尾部开始遍历,则将 cur 更新为链表的虚拟尾结点 tail,准备从尾部向前遍历。

  5. for (int i = 0; i < size - index; i++) { cur = cur.prev; }:这是一个循环,用于从 cur 开始向前遍历链表。循环的次数等于 size - index,这是因为要走到指定位置 index,需要走 size - index 步,每一步通过 cur.prev 向前移动。

  6. else 分支:如果从头部开始遍历,程序会进入这个分支。

  7. for (int i = 0; i <= index; i++) { cur = cur.next; }:这是一个循环,用于从 cur 开始向后遍历链表。循环的次数等于 index,每一步通过 cur.next 向后移动。

  8. return cur.val;:最后,返回 cur 节点的值,即指定位置 index 处的节点值。

这段代码实现了根据指定位置 index 获取链表中节点值的功能,根据 index 的值,它可以选择从头部或尾部开始遍历链表,以提高效率。如果 index 无效,则返回 -1 表示获取失败。

初始链表: 1 -> 2 -> 3 -> 4 -> 5

现在,让我们使用 get 方法来获取不同位置的节点值。

  1. 获取索引为 2 的节点值:

    javaCopy codeint value = myLinkedList.get(2);
    
    • 首先,检查 index 是否有效,2 是有效的。
    • 根据优化,从尾部开始遍历链表,所以 cur 被更新为虚拟尾结点 tail
    • 然后,进入循环,循环次数为 size - index,即 5 - 2 = 3 次。
    • 在每次循环中,cur 向前移动到前一个节点,最终指向索引为 2 的节点,其值为 3
    • 返回 cur.val,即节点值 3
  2. 获取索引为 0 的节点值:

    javaCopy codeint value = myLinkedList.get(0);
    
    • 首先,检查 index 是否有效,0 是有效的。
    • 根据优化,从头部开始遍历链表,所以 cur 保持为虚拟头结点 head
    • 进入循环,循环次数为 index,即 0 次。
    • 直接返回 cur.val,即节点值 1
  3. 获取索引为 4 的节点值:

    javaCopy codeint value = myLinkedList.get(4);
    
    • 首先,检查 index 是否有效,4 是有效的。
    • 根据优化,从尾部开始遍历链表,所以 cur 被更新为虚拟尾结点 tail
    • 进入循环,循环次数为 size - index,即 5 - 4 = 1 次。
    • 在每次循环中,cur 向前移动到前一个节点,最终指向索引为 4 的节点,其值为 5
    • 返回 cur.val,即节点值 5

通过这些示例,我们可以看到 get 方法根据传入的索引值来获取链表中相应位置的节点值,根据优化策略选择从头部或尾部开始遍历,以提高效率。

3)

public void addAtIndex(int index, int val) {
        //index大于链表长度
        if (index > size) {
            return;
        }
        //index小于0
        if (index < 0) {
            index = 0;
        }

        size++;
        //找到前驱
        ListNode pre = this.head;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        //新建结点
        ListNode newNode = new ListNode(val);
        newNode.next = pre.next;
        pre.next.prev = newNode;
        newNode.prev = pre;
        pre.next = newNode;

    }

这段代码是 MyLinkedList 类中的 addAtIndex(int index, int val) 方法,用于在链表的指定位置 index 处插入一个新节点,并将新节点的值设置为 val。让我逐行解释这段代码的功能:

  1. if (index > size):这一行代码首先检查 index 是否大于链表的长度 size,如果大于链表长度,表示要插入的位置在链表末尾之后,此时直接返回,不执行任何插入操作。

  2. if (index < 0):这一行代码检查 index 是否小于 0,如果小于 0,表示要插入的位置在链表头部之前,将 index 设为 0,以确保在链表头部插入。

  3. size++;:在插入节点之前,先增加链表的 size,表示链表中的元素数量增加了一个。

  4. 找到前驱节点:

    • ListNode pre = this.head;:创建一个名为 pre 的节点引用,并将其初始化为链表的虚拟头结点 head,准备从头部开始遍历找到插入位置的前驱节点。
    • for (int i = 0; i < index; i++) { pre = pre.next; }:通过循环遍历链表,将 pre 移动到指定位置 index 的前驱节点。循环次数为 index,每次移动 pre 到下一个节点,直到找到前驱节点。
  5. 新建结点:

    • ListNode newNode = new ListNode(val);:创建一个新的节点 newNode,并将其值初始化为传入的参数 val,准备将其插入到链表中。
    • newNode.next = pre.next;:将新节点 newNodenext 指向前驱节点 prenext,从而连接新节点到链表中原来在 index 位置的节点之后。
    • pre.next.prev = newNode;:将前驱节点 prenext 节点的 prev 指向新节点 newNode,以建立双向链接,使新节点成为前驱节点 pre 的下一个节点的前驱。
    • newNode.prev = pre;:将新节点 newNodeprev 指向前驱节点 pre,建立新节点和前驱节点之间的双向链接。
    • pre.next = newNode;:最后,将前驱节点 prenext 指向新节点 newNode,完成插入操作。

通过这些步骤,新节点 newNode 成功地插入到了链表的指定位置 index 处,同时更新了链表的 size 和前驱节点的链接,实现了链表的插入操作。

例子

初始链表: 1 -> 2 -> 3

现在,让我们使用 addAtIndex 方法在不同位置插入新节点。

  1. 在索引为 1 的位置插入值为 4 的新节点:

    myLinkedList.addAtIndex(1, 4);
    
    • if (index > size):在这个例子中,index1,小于链表的长度 size(长度为 3),所以不会进入这个条件。

    • if (index < 0):同样,index 不小于 0,所以不会进入这个条件。

    • size++;:链表的 size 增加了,变为 4

    • 找到前驱节点:

      • ListNode pre = this.head;pre 初始化为虚拟头结点 head
      • for (int i = 0; i < index; i++) { pre = pre.next; }:通过循环将 pre 移动到指定位置 index 的前驱节点,即节点 1
    • 新建结点:

      • ListNode newNode = new ListNode(4);:创建新节点 newNode,其值为 4
      • newNode.next = pre.next;:将新节点 newNodenext 指向前驱节点 prenext,即节点 2
      • pre.next.prev = newNode;:将前驱节点 prenext 节点 2prev 指向新节点 newNode,建立双向链接。
      • newNode.prev = pre;:将新节点 newNodeprev 指向前驱节点 pre,建立双向链接。
      • pre.next = newNode;:将前驱节点 prenext 指向新节点 newNode,完成插入操作。

    最终链表变为:1 -> 4 -> 2 -> 3

  2. 在索引为 0 的位置插入值为 0 的新节点:

    myLinkedList.addAtIndex(0, 0);
    
    • 由于 index 等于 0,不会进入 if (index > size)if (index < 0) 条件。

    • size++;:链表的 size 再次增加,变为 5

    • 找到前驱节点:

      • ListNode pre = this.head;pre 仍然初始化为虚拟头结点 head
      • for (int i = 0; i < index; i++) { pre = pre.next; }pre 不需要移动,因为 index 已经是 0
    • 新建结点:

      • ListNode newNode = new ListNode(0);:创建新节点 newNode,其值为 0
      • newNode.next = pre.next;:将新节点 newNodenext 指向前驱节点 prenext,即节点 1
      • pre.next.prev = newNode;:将前驱节点 prenext 节点 1prev 指向新节点 newNode,建立双向链接。
      • newNode.prev = pre;:将新节点 newNodeprev 指向前驱节点 pre,建立双向链接。
      • pre.next = newNode;:将前驱节点 prenext 指向新节点 newNode,完成插入操作。

    最终链表变为:0 -> 1 -> 4 -> 2 -> 3

通过这些示例,我们可以看到 addAtIndex 方法根据传入的索引值在链表中插入新节点,根据索引的位置将新节点连接到相应的前驱节点和后继节点,实现了链表的插入操作。

4)

public void deleteAtIndex(int index) {
        //判断索引是否有效
        if(index<0 || index>=size){
            return;
        }
        //删除操作
        size--;
        ListNode pre = this.head;
        for(int i=0; i<index; i++){
            pre = pre.next;
        }
        pre.next.next.prev = pre;
        pre.next = pre.next.next;
    }

这段代码是 MyLinkedList 类中的 deleteAtIndex(int index) 方法,用于删除链表中指定位置 index 处的节点。让我逐行解释这段代码的功能:

  1. if(index<0 || index>=size):这一行代码首先检查传入的 index 是否有效,即是否在链表的合法范围内。如果 index 小于 0 或大于等于链表的长度 size,表示要删除的位置不合法,直接返回,不执行任何删除操作。

  2. 删除操作:

    • size--;:在删除节点之前,先减少链表的 size,表示链表中的元素数量减少了一个。
  3. 找到前驱节点:

    • ListNode pre = this.head;:创建一个名为 pre 的节点引用,并将其初始化为链表的虚拟头结点 head,准备从头部开始遍历找到要删除节点的前驱节点。
    • for(int i=0; i<index; i++) { pre = pre.next; }:通过循环将 pre 移动到指定位置 index 的前驱节点。循环次数为 index,每次移动 pre 到下一个节点,直到找到前驱节点。
  4. 删除节点:

    • pre.next.next.prev = pre;:将前驱节点 pre 的下一个节点的下一个节点(要删除的节点的下一个节点)的 prev 指向前驱节点 pre,这一步是为了维护双向链接,将删除节点的下一个节点的前驱更新为前驱节点。
    • pre.next = pre.next.next;:将前驱节点 prenext 指向删除节点的下一个节点,从而完成删除操作。

通过这些步骤,链表成功地删除了指定位置 index 处的节点,同时更新了链表的 size 和前驱节点的链接,实现了链表的删除操作。如果 index 无效,则不执行任何删除操作。

举例

初始链表: 0 -> 1 -> 2 -> 3 -> 4

现在,让我们使用 deleteAtIndex 方法在不同位置删除节点。

  1. 删除索引为 2 处的节点:

    javaCopy codemyLinkedList.deleteAtIndex(2);
    
    • if(index<0 || index>=size):在这个例子中,index2,在合法范围内(长度为 5),所以不会进入这个条件。

    • 删除操作:

      • size--;:链表的 size 减少为 4
    • 找到前驱节点:

      • ListNode pre = this.head;pre 初始化为虚拟头结点 head
      • for(int i=0; i<index; i++) { pre = pre.next; }:通过循环将 pre 移动到指定位置 2 的前驱节点,即节点 1
    • 删除节点:

      • pre.next.next.prev = pre;:将前驱节点 pre 的下一个节点的下一个节点(要删除的节点的下一个节点 3)的 prev 指向前驱节点 pre,维护双向链接。
      • pre.next = pre.next.next;:将前驱节点 prenext 指向要删除节点的下一个节点,从而完成删除操作。

    最终链表变为:0 -> 1 -> 3 -> 4

  2. 删除索引为 0 处的节点:

    javaCopy codemyLinkedList.deleteAtIndex(0);
    
    • 同样,不会进入 if(index<0 || index>=size) 条件,因为 index0,在合法范围内。

    • 删除操作:

      • size--;:链表的 size 减少为 3
    • 找到前驱节点:

      • ListNode pre = this.head;pre 初始化为虚拟头结点 head
      • for(int i=0; i<index; i++) { pre = pre.next; }pre 不需要移动,因为 index 已经是 0
    • 删除节点:

      • pre.next.next.prev = pre;:将前驱节点 pre 的下一个节点的下一个节点(要删除的节点的下一个节点 1)的 prev 指向前驱节点 pre,维护双向链接。
      • pre.next = pre.next.next;:将前驱节点 prenext 指向要删除节点的下一个节点,从而完成删除操作。

    最终链表变为:1 -> 3 -> 4

通过这些示例,我们可以看到 deleteAtIndex 方法根据传入的索引值删除链表中的节点,同时更新了链表的 size 和前驱节点的链接,实现了链表的删除操作。