链表算法详解(Java 实现):从基础到高频面试题

 在计算机科学的浩瀚领域中,数据结构如同构建程序大厦的基石,而链表(Linked List)作为经典的线性数据结构,更是算法学习道路上的关键一站。相较于数组,链表通过节点间的引用实现动态存储,在插入和删除操作上具备得天独厚的优势,时间复杂度可达\(O(1)\)。本文将基于 Java 语言,深入剖析链表的核心操作,并结合 LeetCode 平台上的高频面试题,给出详细的 Java 实现方案,助力读者夯实数据结构基础,提升算法解题能力。

一、链表基础实现

1.1 节点定义

在 Java 中,实现链表的第一步是定义链表节点类。每个节点包含两个关键部分:数据域val用于存储节点的值,引用域next用于指向下一个节点。通过这种链式连接,多个节点串联成链表结构。

class ListNode {
    int val;
    ListNode next;

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

上述代码中,提供了三种构造函数,方便在不同场景下创建节点,例如初始化单个节点或构建包含多个节点的链表片段。

1.2 链表类型对比

链表根据节点引用的不同形式,可分为单链表、双向链表和循环链表。以下是它们在 Java 实现中的特点及时间复杂度优势对比:

类型Java 实现特点时间复杂度优势典型应用场景
单链表每个节点仅维护一个next引用,指向下一个节点插入 / 删除操作时间复杂度为\(O(1)\),高效修改节点连接关系简单数据存储与顺序遍历场景,如栈和队列的底层实现
双向链表每个节点同时维护prevnext引用,分别指向前驱和后继节点双向遍历时间复杂度为\(O(1)\),支持从后向前遍历操作频繁需要反向遍历的场景,如文本编辑器的撤销 / 重做功能
循环链表尾节点的next引用指向头节点,形成环形结构适用于环形数据处理,如循环队列、资源循环分配操作系统中的任务调度、游戏中的循环场景管理

不同类型的链表适用于不同的应用场景,开发者可根据具体需求选择合适的链表结构。

二、链表核心操作

2.1 遍历链表

遍历链表是最基本的操作之一,通过从头节点开始,依次访问每个节点的数据。

void traverse(ListNode head) {
    ListNode current = head;
    while (current != null) {
        System.out.print(current.val + " -> ");
        current = current.next;
    }
    System.out.println("NULL");
}

在上述代码中,使用一个临时指针current从链表头节点开始,每次循环将current移动到下一个节点,直到遍历完整个链表。

2.2 头部插入

在链表头部插入新节点是一种常见操作,该操作能快速改变链表的结构。

ListNode insertAtHead(ListNode head, int val) {
    ListNode newNode = new ListNode(val);
    newNode.next = head;
    return newNode; // 返回新的头节点
}

上述代码创建一个新节点newNode,将其next指向原链表头节点head,然后返回新节点作为新的头节点,从而实现头部插入。

2.3 删除节点

删除链表中指定值的节点时,为了统一处理头节点删除的情况,通常会引入虚拟头节点。

ListNode deleteNode(ListNode head, int target) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode prev = dummy;

    while (prev.next != null) {
        if (prev.next.val == target) {
            prev.next = prev.next.next;
            break;
        }
        prev = prev.next;
    }
    return dummy.next;
}

代码中创建虚拟头节点dummy,将其next指向原链表头节点head。通过遍历链表,找到值为target的节点,将其前驱节点的next直接指向该节点的后继节点,实现删除操作,最后返回新的链表头节点。

三、高频面试题精讲

3.1 反转链表(LeetCode 206)

迭代法

ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode current = head;

    while (current != null) {
        ListNode nextTemp = current.next;
        current.next = prev;
        prev = current;
        current = nextTemp;
    }
    return prev;
}

迭代法通过三个指针prevcurrentnextTemp,在遍历链表的过程中,不断改变节点的next指向,从而实现链表反转。

递归法

ListNode reverseListRecursive(ListNode head) {
    if (head == null || head.next == null) return head;

    ListNode newHead = reverseListRecursive(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
}

递归法利用递归调用,先递归到链表尾部,然后逐步将节点反转,通过巧妙地处理节点间的引用关系实现反转。

3.2 环形链表检测(LeetCode 141)

boolean hasCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

该方法使用快慢指针,慢指针每次移动一个节点,快指针每次移动两个节点。如果链表存在环,快指针最终会追上慢指针,从而检测出环形链表。

3.3 合并两个有序链表(LeetCode 21)

ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(-1);
    ListNode current = dummy;

    while (l1 != null && l2 != null) {
        if (l1.val < l2.val) {
            current.next = l1;
            l1 = l1.next;
        } else {
            current.next = l2;
            l2 = l2.next;
        }
        current = current.next;
    }

    current.next = (l1 != null) ? l1 : l2;
    return dummy.next;
}

通过创建虚拟头节点dummy,在遍历两个有序链表l1l2的过程中,比较节点值大小,将较小值的节点依次连接到结果链表中,最后处理剩余节点,返回合并后的有序链表。

四、链表操作注意事项

  1. 虚拟头节点:在处理链表的插入、删除操作时,引入虚拟头节点可以避免对头节点的特殊处理,统一代码逻辑,降低出错概率。
  2. 指针丢失:在修改节点的next引用时,务必先保存后续节点的引用,防止链表断裂,导致部分节点无法访问。
  3. 尾节点处理:进行插入、删除等操作后,要注意更新尾节点的next引用,确保链表结构的完整性。
  4. 并发修改:Java 中对象引用机制在多线程环境下可能导致链表结构被意外修改,需要合理使用同步机制或并发数据结构。

五、性能优化技巧

  • 快慢指针法找中间节点(LeetCode 876):通过快慢指针,慢指针每次移动一个节点,快指针每次移动两个节点,当快指针到达链表尾部时,慢指针所在位置即为链表中间节点,可用于寻找链表中点、判断链表长度奇偶性等场景。
  • 哈希表辅助检测重复节点(LeetCode 142):在处理涉及重复节点检测、节点唯一标识等问题时,借助哈希表可以快速判断节点是否已出现,提高算法效率。
  • 递归栈空间优化(LeetCode 25):对于复杂的递归操作,如 K 个一组反转链表,可通过迭代方式或优化递归逻辑,减少递归深度,降低栈空间消耗。
  • 多指针协同操作(LeetCode 19):在删除链表倒数第 N 个节点等问题中,使用多个指针协同工作,能够更高效地定位和处理目标节点。

结语

链表操作是 Java 开发者算法能力的重要体现。深入理解链表节点的引用机制,熟练运用双指针、递归等技巧,是解决链表相关问题的关键。通过本文对链表基础操作和高频面试题的讲解,相信读者对链表算法有了更清晰的认识。建议读者结合 LeetCode 平台上的相关题目进行针对性练习,在实践中不断提升自己的算法水平。

推荐练习

如果在学习过程中有任何疑问或见解,欢迎在评论区留言讨论,让我们共同学习,共同进步!

/* * 基于链表实现树结构 */ package dsa; public class TreeLinkedList implements Tree { private Object element;//树根节点 private TreeLinkedList parent, firstChild, nextSibling;//父亲、长子及最大的弟弟 //(单节点树)构造方法 public TreeLinkedList() { this(null, null, null, null); } //构造方法 public TreeLinkedList(Object e, TreeLinkedList p, TreeLinkedList c, TreeLinkedList s) { element = e; parent = p; firstChild = c; nextSibling = s; } /*---------- Tree接口中各方法的实现 ----------*/ //返回当前节点中存放的对象 public Object getElem() { return element; } //将对象obj存入当前节点,并返回此前的内容 public Object setElem(Object obj) { Object bak = element; element = obj; return bak; } //返回当前节点的父节点;对于根节点,返回null public TreeLinkedList getParent() { return parent; } //返回当前节点的长子;若没有孩子,则返回null public TreeLinkedList getFirstChild() { return firstChild; } //返回当前节点的最大弟弟;若没有弟弟,则返回null public TreeLinkedList getNextSibling() { return nextSibling; } //返回当前节点后代元素的数目,即以当前节点为根的子树的规模 public int getSize() { int size = 1;//当前节点也是自己的后代 TreeLinkedList subtree = firstChild;//从长子开始 while (null != subtree) {//依次 size += subtree.getSize();//累加 subtree = subtree.getNextSibling();//所有孩子的后代数目 } return size;//即可得到当前节点的后代总数 } //返回当前节点的高度 public int getHeight() { int height = -1; TreeLinkedList subtree = firstChild;//从长子开始 while (null != subtree) {//依次 height = Math.max(height, subtree.getHeight());//在所有孩子中取最大高度 subtree = subtree.getNextSibling(); } return height+1;//即可得到当前节点的高度 } //返回当前节点的深度 public int getDepth() { int depth = 0; TreeLinkedList p = parent;//从父亲开始 while (null != p) {//依次 depth++; p = p.getParent();//访问各个真祖先 } return depth;//真祖先的数目,即为当前节点的深度 } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值