1. 链表的基本概念
- 节点(Node):链表的基本组成单元。每个节点包含数据和指向下一个节点的指针。
- 头节点(Head):指向链表的第一个节点,通常用于定位整个链表。
- 尾节点(Tail) :指向链表的最后一个节点,尾节点的指针域通常为
null
(即不再有下一个节点)。 - 空链表(Empty List) :链表中没有任何节点时,头指针为
null
。
2. 链表的类型
-
单向链表(Singly Linked List):
- 每个节点只包含一个指向下一个节点的引用(即单向引用)。
- 特点:只能从头节点遍历到尾节点,不能逆向遍历。
节点结构:
class Node {
int data;
Node next;
}
-
双向链表(Doubly Linked List):
- 每个节点包含两个引用:一个指向下一个节点,一个指向前一个节点。
- 特点:可以在链表中双向遍历,操作更灵活,但每个节点占用更多内存。
节点结构:
class Node {
int data;
Node next;
Node prev;
}
-
循环链表(Circular Linked List):
- 最后一个节点的指针域指向链表的头节点,从而形成一个环。
- 单向循环链表和双向循环链表都有,适用于一些特殊场景,如任务调度。
3. 链表的常见操作
(1) 遍历
遍历链表就是从头节点开始,依次访问每个节点,直到 null
(对于单向链表)或循环到头节点(对于循环链表)。
Node current = head;
while (current != null) {
System.out.println(current.data);
current = current.next;
}
(2) 插入
- 头部插入:将新节点插入链表的头部。时间复杂度为 O(1)。
- 尾部插入:将新节点插入链表的尾部。对于单向链表,时间复杂度为 O(n)(需要遍历到尾部),但如果有指向尾节点的指针,复杂度为 O(1)。
- 中间插入:将新节点插入到链表中的任意位置。时间复杂度为 O(n)(需要找到插入点)。
// 头部插入示例
Node newNode = new Node(data);
newNode.next = head;
head = newNode;
(3) 删除
- 删除头节点:直接将头指针指向第二个节点,时间复杂度为 O(1)。
- 删除尾节点 :需要遍历到倒数第二个节点并将其
next
设置为null
。时间复杂度为 O(n)。 - 删除中间节点 :找到待删除节点的前一个节点,将前一个节点的
next
指针指向待删除节点的下一个节点,时间复杂度为 O(n)。
// 删除头节点示例
if (head != null) {
head = head.next;
}
(4) 查找
- 按值查找:从头节点开始遍历链表,找到值匹配的节点。时间复杂度为 O(n)。
- 按位置查找:从头节点遍历链表,直到到达指定位置的节点。时间复杂度为 O(n)。
4. 链表的优缺点
优点:
- 动态大小:链表可以在运行时动态分配和释放内存,不需要提前定义大小,适合频繁插入和删除的场景。
- 高效的插入和删除:链表在进行插入和删除操作时,只需要调整指针,不需要像数组一样移动元素,效率较高,时间复杂度为 O(1)(头部插入/删除)或 O(n)(中间、尾部插入/删除)。
缺点:
- 随机访问性能差:由于链表不是连续存储的,不能通过索引直接访问某个节点,只能通过遍历链表找到目标节点。时间复杂度为 O(n)。
- 额外的内存开销 :链表中的每个节点都需要存储指针信息(单向链表存储
next
,双向链表还需要存储prev
),因此相对于数组有额外的内存开销。
5. 链表的常见应用
- 实现队列和栈:链表可以很方便地实现队列(FIFO)和栈(LIFO),因为它支持在头部或尾部进行快速的插入和删除操作。
- 哈希表中的拉链法:当哈希冲突发生时,可以使用链表将同一个哈希值对应的多个元素串联起来。
- 图的邻接表:在图的表示中,邻接表通常使用链表来存储每个顶点的邻接顶点。
- LRU缓存机制:使用双向链表与哈希表结合,可以实现高效的 LRU(Least Recently Used)缓存。
6. 链表的常见面试题
-
反转链表:要求将单向链表反转,使得原来的头节点变为尾节点,尾节点变为头节点。
public Node reverseList(Node head) {
Node prev = null;
Node current = head;
while (current != null) {
Node next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
-
链表中环的检测:使用"快慢指针"方法,快指针一次走两步,慢指针一次走一步。如果链表有环,快慢指针会在某一点相遇。
public boolean hasCycle(Node head) {
if (head == null || head.next == null) return false;
Node slow = head;
Node fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) return false;
slow = slow.next;
fast = fast.next.next;
}
return true;
}
-
合并两个有序链表:将两个已经排序的链表合并为一个新的有序链表。
public Node mergeTwoLists(Node l1, Node l2) {
Node dummy = new Node(0);
Node current = dummy;
while (l1 != null && l2 != null) {
if (l1.data <= l2.data) {
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;
}
7. 链表与其他数据结构的比较
- 与数组 :
- 插入/删除:链表比数组高效。链表的插入和删除操作在 O(1) 时间内完成,而数组需要 O(n)。
- 访问效率:数组支持 O(1) 的随机访问,而链表只能通过 O(n) 的遍历找到某个元素。
- 与栈/队列 :
- 链表可以很好地实现栈和队列的功能,通过头部插入、尾部删除等操作实现先进先出或后进先出。