深入理解链表:从基础概念到经典算法
目录
什么是链表?
链表的基本结构
节点(Node)
链表的关键术语
常见的链表类型
1. 单链表
2. 双链表
3. 循环链表
4. 带虚拟头节点的链表
链表的基本操作
1. 创建链表
2. 遍历链表
3. 插入节点
4. 删除节点
5. 查找节点
经典链表算法题解析
1. 反转链表
2. 检测链表中的环
3. 找到链表的中间节点
5. 链表相交
链表的优缺点及应用场景
优点
缺点
应用场景
总结
什么是链表?
链表是一种常见的线性数据结构,它通过节点之间的引用(或指针)连接形成链式结构。与数组不同,链表中的元素在内存中不是连续存储的,这使得链表在插入和删除操作上具有独特的优势。
想象一下日常生活中的锁链:每一环(节点)都包含了具体内容(数据)和与另一环的连接(指针),这就是链表的直观体现。
链表的基本结构
节点(Node)
链表的基本组成单位是节点,在 Java 中通常定义为:
public class ListNode {// 数据域:存储节点的值int val;// 指针域:指向后继节点的引用ListNode next;// 构造方法ListNode(int val) {this.val = val;this.next = null; // 初始化为null,表示没有后继节点}}
每个节点包含两部分:
- 数据域(val):存储具体的数据
- 指针域(next):存储下一个节点的引用(地址)
链表的关键术语
- 头节点(Head):链表的第一个节点,是访问整个链表的入口
- 尾节点(Tail):链表的最后一个节点,其 next 指针为 null
- 空链表:头节点为 null 的链表
- 节点长度:链表中节点的个数
常见的链表类型
1. 单链表
单链表是最基础的链表结构,每个节点只有一个指针指向后继节点:
head -> 1 -> 2 -> 3 -> null
2. 双链表
双链表的每个节点有两个指针,分别指向前驱节点和后继节点:
public class DoubleListNode {int val;DoubleListNode prev; // 指向前驱节点DoubleListNode next; // 指向后继节点DoubleListNode(int val) {this.val = val;this.prev = null;this.next = null;}}
结构示意图:
null <- 1 <-> 2 <-> 3 -> null
双链表的优势是可以双向遍历,方便某些操作,但会增加内存开销。
3. 循环链表
循环链表的尾节点 next 指针不指向 null,而是指向头节点,形成一个闭环:
head -> 1 -> 2 -> 3 -> head
循环链表适合需要反复循环处理的场景,如约瑟夫环问题。
4. 带虚拟头节点的链表
虚拟头节点(Dummy Head)是一个不存储实际数据的节点,其 next 指向真正的头节点:
dummy -> head -> 1 -> 2 -> 3 -> null
这种结构的优势是可以统一各种操作的代码逻辑,避免对头节点进行特殊处理。
链表的基本操作
1. 创建链表
public class DoubleListNode {int val;DoubleListNode prev; // 指向前驱节点DoubleListNode next; // 指向后继节点DoubleListNode(int val) {this.val = val;this.prev = null;this.next = null;}}
2. 遍历链表
// 遍历链表并打印所有元素public static void traverse(ListNode head) {ListNode current = head;while (current != null) {System.out.print(current.val + " ");current = current.next; // 移动到下一个节点}System.out.println();}
遍历是链表操作的基础,时间复杂度为 O (n)。
3. 插入节点
插入操作需要注意指针的修改顺序,避免节点丢失:
// 在指定节点后插入新节点public static void insertAfter(ListNode node, int val) {if (node == null) return;ListNode newNode = new ListNode(val);newNode.next = node.next; // 新节点指向原节点的后继node.next = newNode; // 原节点指向新节点}// 在链表头部插入新节点public static ListNode insertAtHead(ListNode head, int val) {ListNode newNode = new ListNode(val);newNode.next = head; // 新节点指向原头节点return newNode; // 新节点成为新的头节点}
4. 删除节点
// 删除指定节点的后继节点public static void deleteAfter(ListNode node) {if (node == null || node.next == null) return;// 跳过要删除的节点node.next = node.next.next;}// 删除头节点public static ListNode deleteHead(ListNode head) {if (head == null) return null;return head.next; // 第二个节点成为新的头节点}
5. 查找节点
// 根据值查找节点public static ListNode findNode(ListNode head, int target) {ListNode current = head;while (current != null) {if (current.val == target) {return current;}current = current.next;}return null; // 未找到}
经典链表算法题解析
1. 反转链表
反转链表是最经典的链表算法题之一,有迭代和递归两种解法:
// 迭代法反转链表public static 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; // prev成为新的头节点}// 递归法反转链表public static 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;}
2. 检测链表中的环
使用弗洛伊德的快慢指针算法(Floyd's Tortoise and Hare Algorithm):
public static boolean hasCycle(ListNode head) {if (head == null || head.next == null) {return false;}ListNode slow = head; // 慢指针,每次走一步ListNode fast = head.next; // 快指针,每次走两步while (slow != fast) {// 快指针到达尾部,无环if (fast == null || fast.next == null) {return false;}slow = slow.next;fast = fast.next.next;}// 快慢指针相遇,有环return true;}
3. 找到链表的中间节点
同样可以使用快慢指针:
public static ListNode findMiddleNode(ListNode head) {if (head == null) {return null;}ListNode slow = head;ListNode fast = head;// 快指针走两步,慢指针走一步// 当快指针到达尾部时,慢指针刚好在中间while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;}return slow;}4. 合并两个有序链表public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {// 创建虚拟头节点,简化操作ListNode dummy = new ListNode(0);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;}
5. 链表相交
判断两个链表是否相交并找到交点:
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {if (headA == null || headB == null) {return null;}ListNode a = headA;ListNode b = headB;// 当a和b不相等时继续遍历// 如果相交,最终会在交点相遇// 如果不相交,最终都会为nullwhile (a != b) {// 到达链表末尾则切换到另一个链表a = (a == null) ? headB : a.next;b = (b == null) ? headA : b.next;}return a;}
链表的优缺点及应用场景
优点
- 动态大小:不需要预先分配固定大小的内存
- 高效插入删除:在已知前驱节点的情况下,插入删除操作时间复杂度为 O (1)
- 内存利用率高:只使用必要的内存空间
缺点
- 随机访问效率低:访问第 k 个元素需要 O (k) 时间
- 额外内存开销:需要存储指针信息
- 不适合缓存:节点在内存中不连续,缓存命中率低
应用场景
- 频繁进行插入删除操作的场景
- 不确定数据总量的场景
- 实现其他数据结构,如栈、队列、哈希表的拉链法等
- 操作系统中的内存管理
- 浏览器的历史记录(双向链表)
总结
链表是一种灵活高效的数据结构,掌握链表的操作是理解更复杂数据结构和算法的基础。学习链表的关键在于:
- 理解节点之间的引用关系
- 掌握指针操作的技巧
- 注意处理边界条件(空链表、单节点链表等)
- 学会利用快慢指针等特殊技巧解决问题