链表作为计算机科学中最基础的数据结构之一,在算法面试和实际开发中都有着广泛应用。与数组不同,链表通过指针将零散的内存块串联起来,每个节点包含数据和指向下一个节点的指针。这种非连续存储特性使得链表在插入和删除操作上具有O(1)时间复杂度优势,但也带来了随机访问效率低下的问题。
链表的核心在于节点间的指针链接。单链表每个节点包含:
这种结构决定了链表操作的特殊性:
提示:在实际工程中,链表常用于实现LRU缓存、哈希表冲突解决等场景。其动态扩容的特性使其特别适合数据量变化频繁的场景。
原始材料中反复提到的"虚拟头节点"(dummy node)技术是链表操作的核心技巧。它的价值在于:
java复制// 创建虚拟头节点的典型实现
ListNode dummy = new ListNode(-1); // 数据域值无关紧要
dummy.next = head; // 连接原链表
直接操作时需要区分头节点和其他节点:
java复制// 处理头节点
while (head != null && head.val == target) {
head = head.next;
}
// 处理非头节点
ListNode current = head;
while (current != null && current.next != null) {
if (current.next.val == target) {
current.next = current.next.next; // 跳过目标节点
} else {
current = current.next; // 仅当不删除时才移动指针
}
}
注意事项:移动指针的条件是关键。只有在不删除节点时才移动current指针,否则会导致跳过连续的目标节点。
虚拟头节点方案显著简化了代码逻辑:
java复制ListNode dummy = new ListNode(-1, head);
ListNode current = dummy;
while (current.next != null) {
if (current.next.val == target) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return dummy.next; // 注意返回的是dummy.next而非head
完整链表实现需要考虑的边界条件:
java复制public int get(int index) {
if (index < 0 || index >= size) return -1;
ListNode current = head;
for (int i = 0; i < index; i++) {
current = current.next;
}
return current.val;
}
java复制public void addAtHead(int val) {
ListNode newNode = new ListNode(val);
newNode.next = head;
head = newNode;
size++;
if (tail == null) tail = head; // 更新尾指针
}
java复制public void addAtTail(int val) {
ListNode newNode = new ListNode(val);
if (tail == null) {
head = tail = newNode;
} else {
tail.next = newNode;
tail = newNode;
}
size++;
}
实操心得:维护tail指针可以优化尾部插入性能,但需要确保在删除操作时正确更新tail指针,否则容易产生指针错乱。
java复制public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode nextTemp = current.next; // 临时保存下一个节点
current.next = prev; // 反转指针
prev = current; // 前移prev
current = nextTemp; // 前移current
}
return prev; // 新头节点
}
时间复杂度分析:每个节点只被访问一次,因此时间复杂度为O(n)。空间上只使用了常数个额外指针,空间复杂度O(1)。
java复制public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head; // 反转指针
head.next = null; // 断开原指针
return newHead;
}
注意事项:递归解法虽然简洁,但在处理超长链表时可能导致栈溢出。实际工程中更推荐使用迭代解法。
关键点在于指针操作的顺序和终止条件判断:
java复制public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(-1, head);
ListNode current = dummy;
while (current.next != null && current.next.next != null) {
ListNode first = current.next;
ListNode second = current.next.next;
// 执行交换
first.next = second.next;
second.next = first;
current.next = second;
// 移动current指针
current = first;
}
return dummy.next;
}
常见错误:
快慢指针法的经典应用:
java复制public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1, head);
ListNode fast = dummy, slow = dummy;
// 快指针先走n+1步
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
// 同步移动直到快指针到末尾
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 删除目标节点
slow.next = slow.next.next;
return dummy.next;
}
时间复杂度分析:快指针最多遍历链表两次,时间复杂度为O(L)(L为链表长度),空间复杂度O(1)。
java复制public boolean hasCycle(ListNode head) {
if (head == null) return false;
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
设:
根据速度关系有:
2(x + y) = x + y + n(y + z)
=> x = (n-1)(y+z) + z
当n=1时,x=z,这意味着:
java复制public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 相遇
ListNode index1 = head;
ListNode index2 = slow;
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1; // 环入口
}
}
return null;
}
实操心得:在实际面试中,能够清晰解释这个数学推导过程往往比写出代码更重要。建议理解并记忆x=z这个关键结论。