算法-数组实战【设计链表】中等

设计链表

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

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,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 的节点。

示例:

scss 复制代码
输入
["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 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndexdeleteAtIndex 的次数不超过 2000

解答

使用双向链表实现 MyLinkedList 类的 Java 代码。代码设计使用虚拟头节点 (dummyHead) 和虚拟尾节点 (dummyTail) 来简化边界操作(如头插和尾插),并通过维护 size 变量记录链表长度以提高索引效率。

ini 复制代码
java
复制
public class MyLinkedList {
    // 双向链表节点定义
    private class ListNode {
        int val;
        ListNode prev;
        ListNode next;
        ListNode() {}
        ListNode(int val) { this.val = val; }
        ListNode(int val, ListNode prev, ListNode next) {
            this.val = val;
            this.prev = prev;
            this.next = next;
        }
    }

    private ListNode dummyHead;  // 虚拟头节点
    private ListNode dummyTail;  // 虚拟尾节点
    private int size;            // 链表实际长度(不含虚拟节点)

    // 初始化链表
    public MyLinkedList() {
        dummyHead = new ListNode();
        dummyTail = new ListNode();
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
        size = 0;
    }

    // 获取指定索引的节点值(索引无效时返回 -1)
    public int get(int index) {
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode curr = getNode(index); // 获取目标节点
        return curr.val;
    }

    // 在头部插入节点
    public void addAtHead(int val) {
        addAtIndex(0, val); // 复用 addAtIndex 实现
    }

    // 在尾部插入节点
    public void addAtTail(int val) {
        addAtIndex(size, val); // 复用 addAtIndex 实现
    }

    // 在指定索引插入节点
    public void addAtIndex(int index, int val) {
        if (index < 0) index = 0;     // 负数索引视为头部插入
        if (index > size) return;     // 超过长度则忽略

        // 插入位置的目标节点(新节点将插入在其之前)
        ListNode targetNode;
        if (index == size) {
            targetNode = dummyTail;   // 插在尾部:目标节点是虚拟尾节点
        } else {
            targetNode = getNode(index); // 中间插入:获取当前索引节点
        }

        // 创建新节点并链接
        ListNode newNode = new ListNode(val);
        ListNode prevNode = targetNode.prev;
        // 连接新节点与前驱
        prevNode.next = newNode;
        newNode.prev = prevNode;
        // 连接新节点与后继
        newNode.next = targetNode;
        targetNode.prev = newNode;
        size++; // 更新链表长度
    }

    // 删除指定索引节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;

        ListNode targetNode = getNode(index); // 获取目标节点
        // 断开目标节点的连接
        ListNode prevNode = targetNode.prev;
        ListNode nextNode = targetNode.next;
        prevNode.next = nextNode;
        nextNode.prev = prevNode;
        // 清空引用(帮助GC)
        targetNode.prev = null;
        targetNode.next = null;
        size--; // 更新链表长度
    }

    // 辅助方法:获取指定索引的节点(优化遍历:根据位置选择方向)
    private ListNode getNode(int index) {
        if (index < size / 2) { 
            // 从前向后遍历
            ListNode curr = dummyHead.next;
            for (int i = 0; i < index; i++) {
                curr = curr.next;
            }
            return curr;
        } else {
            // 从后向前遍历
            ListNode curr = dummyTail.prev;
            for (int i = size - 1; i > index; i--) {
                curr = curr.prev;
            }
            return curr;
        }
    }
}

代码说明:

  1. 节点定义​:

    • ListNode 类包含值 (val) 和前驱/后继指针 (prev, next)。
  2. 虚拟头尾节点​:

    • dummyHeaddummyTail 始终存在但不存储实际数据,确保头/尾插入统一处理。
    • 初始化时二者互连,形成空链表结构。
  3. 核心方法​:

    • ​**get(int index)**​:

      • 检查索引有效性,无效返回 -1
      • 调用 getNode(index) 获取目标节点值。
    • ​**addAtHead(int val) / addAtTail(int val)**​:

      • 复用 addAtIndex(0, val)addAtIndex(size, val) 实现。
    • ​**addAtIndex(int index, int val)**​:

      • 处理负数索引(视为头插)。
      • 若索引超过 size 则忽略。
      • 获取目标节点(若索引为 size 则目标为虚拟尾节点)。
      • 创建新节点并链接到目标节点和其前驱之间。
    • ​**deleteAtIndex(int index)**​:

      • 检查索引有效性,无效则返回。
      • 获取目标节点,断开其与前后节点的连接,并更新链表长度。
  4. ​**辅助方法 getNode(int index)**​:

    • 根据索引位置优化遍历方向:

      • 索引在前半部分:从 dummyHead.next 向后遍历。
      • 索引在后半部分:从 dummyTail.prev 向前遍历。

特点:

  • 时间复杂度​:

    • addAtHead/addAtTail:O(1)。
    • get/addAtIndex/deleteAtIndex:平均 O(n/2)(通过双向遍历优化)。
  • 空间复杂度​:O(n)(存储 n 个节点)。

  • 简化边界​:虚拟头尾节点避免处理空链表、单节点链表等边界情况。

相关推荐
车队老哥记录生活1 分钟前
【MPC】模型预测控制笔记 (4):约束输出反馈MPC
笔记·算法
wen__xvn37 分钟前
基础数据结构第03天:顺序表(实战篇)
数据结构·c++·算法
聚客AI1 小时前
💡 Transformer数据管道:自定义Dataset类+智能批处理最佳实践
数据结构·人工智能·llm
迪小莫学AI1 小时前
【力扣每日一题】划分数组并满足最大差限制
算法·leetcode·职场和发展
爱喝茶的小茶1 小时前
模拟/思维
算法
SimonKing1 小时前
集合的处理:JDK和Guava孰强孰弱?
java·后端·算法
爱装代码的小瓶子1 小时前
字符操作函数续上
android·c语言·开发语言·数据结构·算法
码破苍穹ovo2 小时前
回溯----5.括号生成
java·数据结构·力扣·递归
千楼2 小时前
LeetCode 3090. 每个字符最多出现两次的最长子字符串
算法
人类发明了工具2 小时前
【强化学习】PPO(Proximal Policy Optimization,近端策略优化)算法
人工智能·算法·机器学习·ppo·近端策略优化算法