JAVA数据结构与算法 - 基础:链表

07、数据结构与算法 - 基础:链表

一、为什么需要链表

在计算机内存中,数据的组织方式直接决定了操作效率。数组(Array)是我们最早接触的线性结构,它凭借连续内存分配实现了 O(1) 的随机访问。但数组也有先天不足:创建时必须预先声明容量,扩容需要整体拷贝;中间插入或删除元素时,需要移动大量数据。这些场景下,数组的 O(N) 时间复杂度就显得捉襟见肘。

链表(Linked List)采用截然不同的设计思路:每个数据单元(节点)在内存中离散分布,通过指针(引用)串联形成逻辑上的线性序列。这种"以空间换灵活"的策略,使得插入和删除操作可以在 O(1) 时间内完成,代价是丧失了随机访问能力。

下表直观对比了两种结构的核心差异:

操作 数组 链表
随机访问 O(1) O(N)
头部插入 O(N) O(1)
尾部插入(有尾指针) O(1) O(1)
中间插入 O(N) O(N)(查找耗时)
删除 O(N) O(1)(已知位置)
内存占用 纯数据 数据 + 指针开销
缓存友好

二、单向链表的完整设计与实现

单向链表(Singly Linked List)是最基础的链表形态,每个节点持有数据域和指向后继节点的引用。下面给出一个支持泛型、包含完整增删改查功能的工业级实现。

2.1 节点定义与链表骨架

java 复制代码
/**
 * 单向链表 - 泛型实现
 * 支持头部插入、尾部插入、指定位置插入、指定位置删除、查找、反转、判空等操作
 */
public class SinglyLinkedList<T> {

    /* ========== 节点内部类 ========== */
    private static class Node<T> {
        T data;          // 节点存储的数据
        Node<T> next;    // 指向后继节点的引用

        Node(T data) {
            this.data = data;
            this.next = null;
        }
    }

    private Node<T> head;   // 链表头指针
    private int size;       // 链表元素个数

    public SinglyLinkedList() {
        this.head = null;
        this.size = 0;
    }

    /* ==================== 基本操作 ==================== */

    /** 获取链表长度 */
    public int size() {
        return size;
    }

    /** 判断链表是否为空 */
    public boolean isEmpty() {
        return size == 0;
    }

    /** 头插法:将新节点插入链表头部 */
    public void addFirst(T data) {
        Node<T> newNode = new Node<>(data);
        newNode.next = head;
        head = newNode;
        size++;
    }

    /** 尾插法:将新节点插入链表尾部 */
    public void addLast(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
        } else {
            Node<T> cur = head;
            while (cur.next != null) {
                cur = cur.next;
            }
            cur.next = newNode;
        }
        size++;
    }

    /** 在指定索引处插入元素(索引从 0 开始) */
    public void add(int index, T data) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("索引越界: " + index);
        }
        if (index == 0) {
            addFirst(data);
            return;
        }
        Node<T> newNode = new Node<>(data);
        Node<T> prev = head;
        for (int i = 0; i < index - 1; i++) {
            prev = prev.next;
        }
        newNode.next = prev.next;
        prev.next = newNode;
        size++;
    }

    /** 删除头部节点并返回其数据 */
    public T removeFirst() {
        if (head == null) {
            throw new RuntimeException("链表为空,无法删除");
        }
        T removed = head.data;
        head = head.next;
        size--;
        return removed;
    }

    /** 删除指定索引处的节点 */
    public T remove(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("索引越界: " + index);
        }
        if (index == 0) {
            return removeFirst();
        }
        Node<T> prev = head;
        for (int i = 0; i < index - 1; i++) {
            prev = prev.next;
        }
        T removed = prev.next.data;
        prev.next = prev.next.next;
        size--;
        return removed;
    }

    /** 按值查找,返回首次出现的索引,未找到返回 -1 */
    public int indexOf(T data) {
        Node<T> cur = head;
        int idx = 0;
        while (cur != null) {
            if (cur.data == null ? data == null : cur.data.equals(data)) {
                return idx;
            }
            cur = cur.next;
            idx++;
        }
        return -1;
    }

    /** 获取指定索引的元素 */
    public T get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("索引越界: " + index);
        }
        Node<T> cur = head;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        return cur.data;
    }

    /** 打印链表内容 */
    @Override
    public String toString() {
        if (head == null) return "[]";
        StringBuilder sb = new StringBuilder("[");
        Node<T> cur = head;
        while (cur != null) {
            sb.append(cur.data);
            if (cur.next != null) sb.append(" -> ");
            cur = cur.next;
        }
        sb.append("]");
        return sb.toString();
    }

    // ========== main 方法用于验证 ==========
    public static void main(String[] args) {
        SinglyLinkedList<Integer> list = new SinglyLinkedList<>();

        // 测试尾部插入
        list.addLast(10);
        list.addLast(20);
        list.addLast(30);
        System.out.println("尾插后: " + list);          // [10 -> 20 -> 30]

        // 测试头部插入
        list.addFirst(5);
        System.out.println("头插后: " + list);          // [5 -> 10 -> 20 -> 30]

        // 测试指定位置插入
        list.add(2, 15);
        System.out.println("索引2插入15后: " + list);   // [5 -> 10 -> 15 -> 20 -> 30]

        // 测试删除
        list.removeFirst();
        System.out.println("删头后: " + list);           // [10 -> 15 -> 20 -> 30]

        list.remove(1);
        System.out.println("删索引1后: " + list);        // [10 -> 20 -> 30]

        // 测试查找
        System.out.println("20的索引: " + list.indexOf(20));  // 1
        System.out.println("100的索引: " + list.indexOf(100)); // -1

        // 测试获取
        System.out.println("索引0元素: " + list.get(0));  // 10
        System.out.println("链表大小: " + list.size());    // 3
    }
}

2.2 时间复杂度分析

  • addFirst:O(1),直接修改头指针。
  • addLast:O(N),需要遍历到尾部。若维护尾指针可优化至 O(1)。
  • add(index):O(N),查找目标位置的前驱。
  • removeFirst:O(1)。
  • remove(index):O(N)。
  • indexOf / get:O(N),无法随机访问。

三、链表反转算法

链表反转是面试中的高频考点,存在迭代法和递归法两种经典解法。

3.1 迭代法反转

迭代法的核心思想是"三指针原地翻转":维护 prev(已翻转部分的头)、cur(当前待处理节点)、next(暂存下一个节点),逐步将每个节点的 next 指针反向。

java 复制代码
/**
 * 单向链表反转 - 迭代实现
 * 时间复杂度 O(N),空间复杂度 O(1)
 */
public void reverseIterative() {
    Node<T> prev = null;
    Node<T> cur = head;
    while (cur != null) {
        Node<T> next = cur.next;   // 暂存后继
        cur.next = prev;            // 翻转指针
        prev = cur;                 // prev 前进
        cur = next;                 // cur 前进
    }
    head = prev;  // 新头节点
}

执行过程模拟(链表 1→2→3→null):

轮次 prev cur next 操作后链表状态
初始 null 1 - 1→2→3→null
第1轮 1 2 2 null←1 2→3→null
第2轮 2 3 3 null←1←2 3→null
第3轮 3 null null null←1←2←3

3.2 递归法反转

递归法的本质是利用函数调用栈保存上下文,先递归到最后一个节点,然后在回溯过程中逐层翻转指针。

java 复制代码
/**
 * 单向链表反转 - 递归实现
 * 时间复杂度 O(N),空间复杂度 O(N)(递归栈深度)
 */
public void reverseRecursive() {
    head = reverseRecursiveHelper(head);
}

private Node<T> reverseRecursiveHelper(Node<T> node) {
    if (node == null || node.next == null) {
        return node;  // 到达尾节点,尾节点将成为新头
    }
    Node<T> newHead = reverseRecursiveHelper(node.next);
    node.next.next = node;  // 后继节点指回当前节点
    node.next = null;       // 断开当前节点的原指向
    return newHead;         // 始终返回新头节点
}

四、双向链表

单向链表只能正序遍历,无法回退。双向链表在每个节点中额外存储前驱指针,使得双方向遍历成为可能,同时使得尾部删除从 O(N) 降至 O(1)。

java 复制代码
/**
 * 双向链表 - 泛型实现
 * 支持头尾双向操作、双向遍历
 */
public class DoublyLinkedList<T> {

    private static class Node<T> {
        T data;
        Node<T> prev;   // 前驱指针
        Node<T> next;   // 后继指针

        Node(T data) {
            this.data = data;
        }
    }

    private Node<T> head;   // 头哨兵(简化边界处理)
    private Node<T> tail;   // 尾哨兵
    private int size;

    public DoublyLinkedList() {
        head = new Node<>(null);  // 头哨兵不存数据
        tail = new Node<>(null);  // 尾哨兵不存数据
        head.next = tail;
        tail.prev = head;
        size = 0;
    }

    public int size() { return size; }
    public boolean isEmpty() { return size == 0; }

    /** 头部插入 */
    public void addFirst(T data) {
        Node<T> newNode = new Node<>(data);
        Node<T> first = head.next;
        newNode.prev = head;
        newNode.next = first;
        head.next = newNode;
        first.prev = newNode;
        size++;
    }

    /** 尾部插入 */
    public void addLast(T data) {
        Node<T> newNode = new Node<>(data);
        Node<T> last = tail.prev;
        newNode.prev = last;
        newNode.next = tail;
        last.next = newNode;
        tail.prev = newNode;
        size++;
    }

    /** 头部删除 */
    public T removeFirst() {
        if (isEmpty()) throw new RuntimeException("链表为空");
        Node<T> first = head.next;
        head.next = first.next;
        first.next.prev = head;
        size--;
        return first.data;
    }

    /** 尾部删除 */
    public T removeLast() {
        if (isEmpty()) throw new RuntimeException("链表为空");
        Node<T> last = tail.prev;
        tail.prev = last.prev;
        last.prev.next = tail;
        size--;
        return last.data;
    }

    /** 正向遍历打印 */
    public String forwardString() {
        StringBuilder sb = new StringBuilder("[");
        Node<T> cur = head.next;
        while (cur != tail) {
            sb.append(cur.data);
            if (cur.next != tail) sb.append(" <-> ");
            cur = cur.next;
        }
        sb.append("]");
        return sb.toString();
    }

    /** 反向遍历打印 */
    public String backwardString() {
        StringBuilder sb = new StringBuilder("[");
        Node<T> cur = tail.prev;
        while (cur != head) {
            sb.append(cur.data);
            if (cur.prev != head) sb.append(" <-> ");
            cur = cur.prev;
        }
        sb.append("]");
        return sb.toString();
    }

    public static void main(String[] args) {
        DoublyLinkedList<String> dll = new DoublyLinkedList<>();
        dll.addLast("A");
        dll.addLast("B");
        dll.addLast("C");
        dll.addFirst("0");

        System.out.println("正向: " + dll.forwardString());   // [0 <-> A <-> B <-> C]
        System.out.println("反向: " + dll.backwardString());  // [C <-> B <-> A <-> 0]

        dll.removeFirst();
        dll.removeLast();
        System.out.println("删头删尾后: " + dll.forwardString()); // [A <-> B]
        System.out.println("大小: " + dll.size());                // 2
    }
}

五、合并两个有序链表

这是 LeetCode 经典题目(#21),要求将两个升序链表合并为一个新的升序链表。核心思路是双指针比较,逐个连接较小的节点。

java 复制代码
/**
 * 合并两个有序单向链表(升序)
 * 时间复杂度 O(M+N),空间复杂度 O(1)
 */
public static SinglyLinkedList<Integer> mergeSorted(
        SinglyLinkedList<Integer> list1,
        SinglyLinkedList<Integer> list2) {

    // 使用内部节点直接操作,这里简化:先将两个链表数据取出再构建
    // 实际工程中应操作 Node 引用
    return null; // 此处仅说明算法思路
}

// ---- 更工业级的独立工具类 ----
public class MergeSortedLists {

    static class ListNode {
        int val;
        ListNode next;
        ListNode(int val) { this.val = val; }
    }

    /**
     * 迭代法合并两个有序链表
     * 使用虚拟头节点简化边界处理
     */
    public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);  // 虚拟头节点
        ListNode tail = dummy;

        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                tail.next = l1;
                l1 = l1.next;
            } else {
                tail.next = l2;
                l2 = l2.next;
            }
            tail = tail.next;
        }
        // 连接剩余部分
        tail.next = (l1 != null) ? l1 : l2;
        return dummy.next;
    }

    /** 递归法合并(更简洁但可能有栈溢出风险) */
    public static ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        if (l1.val <= l2.val) {
            l1.next = mergeTwoListsRecursive(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoListsRecursive(l1, l2.next);
            return l2;
        }
    }

    /** 辅助方法:从数组构建链表 */
    public static ListNode buildList(int[] arr) {
        ListNode dummy = new ListNode(-1);
        ListNode tail = dummy;
        for (int v : arr) {
            tail.next = new ListNode(v);
            tail = tail.next;
        }
        return dummy.next;
    }

    /** 辅助方法:打印链表 */
    public static void printList(ListNode head) {
        StringBuilder sb = new StringBuilder("[");
        while (head != null) {
            sb.append(head.val);
            if (head.next != null) sb.append(" -> ");
            head = head.next;
        }
        sb.append("]");
        System.out.println(sb);
    }

    public static void main(String[] args) {
        ListNode l1 = buildList(new int[]{1, 3, 5, 7});
        ListNode l2 = buildList(new int[]{2, 4, 6, 8});

        System.out.print("链表1: "); printList(l1);
        System.out.print("链表2: "); printList(l2);

        // 迭代法合并
        ListNode merged = mergeTwoLists(buildList(new int[]{1,3,5,7}),
                                         buildList(new int[]{2,4,6,8}));
        System.out.print("迭代合并: "); printList(merged);

        // 递归法合并
        ListNode merged2 = mergeTwoListsRecursive(buildList(new int[]{1,3,5,7}),
                                                   buildList(new int[]{2,4,6,8}));
        System.out.print("递归合并: "); printList(merged2);

        // 边界测试:空链表
        ListNode emptyMerged = mergeTwoLists(null, new ListNode(42));
        System.out.print("合并空链表: "); printList(emptyMerged);
    }
}

六、环形链表检测(Floyd 判圈算法)

环形链表检测是另一道高频考题(LeetCode #141、#142)。Floyd 算法(龟兔赛跑算法)使用快慢两个指针,快指针每次走两步,慢指针每次走一步。若无环,快指针先到达 null;若有环,两指针最终会在环内相遇。

java 复制代码
/**
 * 环形链表检测与环入口查找
 */
public class CycleDetection {

    static class ListNode {
        int val;
        ListNode next;
        ListNode(int val) { this.val = val; }
    }

    /**
     * 判断链表是否有环
     * 快慢指针法,时间复杂度 O(N),空间复杂度 O(1)
     */
    public static boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) return false;
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;       // 慢指针每次走 1 步
            fast = fast.next.next;  // 快指针每次走 2 步
            if (slow == fast) {
                return true;        // 相遇说明有环
            }
        }
        return false;               // 快指针到达终点,无环
    }

    /**
     * 找出环的入口节点(LeetCode #142)
     * 原理:相遇点到环入口的距离 = 头节点到环入口的距离
     */
    public static ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) return null;
        ListNode slow = head;
        ListNode fast = head;
        // 第一步:确认有环并找到相遇点
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                // 第二步:从头节点和相遇点同时出发,再次相遇处即为环入口
                ListNode ptr = head;
                while (ptr != slow) {
                    ptr = ptr.next;
                    slow = slow.next;
                }
                return ptr;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        // 构建带环链表:  1 -> 2 -> 3 -> 4 -> 5
        //                          ^         |
        //                          |_________|
        ListNode n1 = new ListNode(1);
        ListNode n2 = new ListNode(2);
        ListNode n3 = new ListNode(3);
        ListNode n4 = new ListNode(4);
        ListNode n5 = new ListNode(5);
        n1.next = n2; n2.next = n3; n3.next = n4; n4.next = n5;
        n5.next = n3;  // 形成环,入口在 n3

        System.out.println("是否有环: " + hasCycle(n1));           // true
        ListNode entry = detectCycle(n1);
        System.out.println("环入口值: " + (entry != null ? entry.val : "null")); // 3

        // 测试无环链表
        ListNode noCycle = new ListNode(10);
        noCycle.next = new ListNode(20);
        noCycle.next.next = new ListNode(30);
        System.out.println("无环链表检测: " + hasCycle(noCycle));   // false
        System.out.println("无环链表入口: " + detectCycle(noCycle)); // null
    }
}

Floyd 算法数学原理简述:

设链表头到环入口距离为 a,环入口到相遇点距离为 b,相遇点到环入口距离为 c(环周长 = b + c)。相遇时:慢指针走了 a + b 步,快指针走了 a + b + k(b + c) 步。由于快指针速度是慢指针的两倍,有 2(a+b) = a+b+k(b+c),化简得 a = (k-1)(b+c) + c,说明从头节点到环入口的距离 a,等于从相遇点沿环走 (k-1) 圈再加 c 的距离。当 k=1 时,a = c,即从头节点和相遇点同时出发,刚好在环入口相遇。

七、单向循环链表

循环链表的尾节点不指向 null,而是指向头节点,形成闭环。适合处理环形调度问题(如约瑟夫环)。

链表类型 尾节点指向 典型场景
普通单链表 null 栈、队列基础结构
带头尾指针的双向链表 null(哨兵模式) Java LinkedList
单向循环链表 头节点 约瑟夫问题、轮询调度
双向循环链表 头节点 LRU 缓存、音乐播放器

八、常见面试题与解题策略

问题 核心思路 难度
反转链表 三指针迭代 / 递归 ★★☆
合并有序链表 双指针逐个比较 + 虚拟头 ★★☆
环形链表检测 Floyd 快慢指针 ★★☆
查找倒数第 K 个节点 双指针间隔 K 步同步移动 ★★☆
求两个链表的交点 消除长度差后同步遍历 ★★★
回文链表判断 快慢指针找中点 + 反转后半段 ★★★
K 个一组反转链表 分段处理 + 递归衔接 ★★★★

九、总结

链表通过"指针串联"的方式换取了插入删除的 O(1) 效率,代价是无法随机访问。在实际工程中,Java 的 LinkedList 底层即基于双向链表实现;Linux 内核大量使用双向循环链表管理进程、内存等资源;Redis 的列表(List)底层采用双向链表;LRU 缓存淘汰算法也以双向链表 + 哈希表作为核心结构。理解链表的节点模型、指针操作和边界处理,是掌握更高级数据结构(如图、树)的前提。

相关推荐
日月云棠3 小时前
JAVA数据结构与算法 - 基础:栈 (Stack) 深度解析
java·后端
xiguolangzi3 小时前
java使用Map映射遍历方法
java·后端
日月云棠3 小时前
JAVA数据结构与算法 - 基础:队列 (Queue) 全方位解析
java·后端
JAVA面经实录9174 小时前
Java集合大全终极手册(一)
java·开发语言
IT策士4 小时前
Django 从 0 到 1 打造完整电商平台:为什么用 Django 做电商?
后端·python·django
Cosolar4 小时前
吃透 Spring Cloud Gateway:基于 Spring Boot 3 的核心原理、企业级实战与避坑指南
java·spring cloud·架构
千里马-horse4 小时前
gRPC -- Java 基础教程
java·开发语言·grpc
甲方大人请饶命4 小时前
Java-面向对象进阶(qqbb知识点)
java·开发语言
ChoSeitaku4 小时前
07_static_JavaBean_继承_super/this
java·开发语言