一、是什么
链表是一种线性数据结构,由一系列节点(Node)组成,每个节点包含两个部分:数据部分(存储数据的值)和指针部分(存储下一个节点的地址)。链表的第一个节点称为头节点(Head),最后一个节点的指针指向null
,表示链表的结束。
链表的基本类型
- 单链表(Singly Linked List):每个节点只有一个指针,指向下一个节点。
- 双链表(Doubly Linked List):每个节点有两个指针,一个指向下一个节点,一个指向前一个节点。
- 循环链表(Circular Linked List):链表的最后一个节点指向链表的头节点,形成一个环。
链表的优势
相比于其他数据结构(如数组),链表有以下几个优势:
-
动态大小:链表的大小可以动态调整,不需要像数组那样在创建时指定大小。因此,当数据量增加或减少时,链表不会像数组那样导致内存浪费或需要重新分配内存。
-
插入和删除操作快:链表的插入和删除操作只需要修改指针,时间复杂度为
O(1)
。相较于数组,插入或删除元素时需要移动大量元素,链表在这方面更高效。 -
节省内存:链表不需要预留额外的内存空间。对于大小不确定的场景,链表可以节省空间。
链表的劣势
-
随机访问速度慢:链表不支持下标访问,需要从头节点开始遍历,时间复杂度为
O(n)
,因此查找速度较慢。 -
内存开销大:由于每个节点都需要存储指针信息,链表的内存开销比数组大。
-
不支持缓存:由于链表节点不连续存储,链表不太适合缓存友好的应用(比如数组那样的连续内存块)。
链表通常用于需要频繁插入和删除操作的数据结构中,如实现队列(Queue)、栈(Stack)等。它特别适合用来管理动态数据。
注:数组是一块连续的内存块(为了直接访问的高效性和内存管理的便利性)。在内存中,数组的元素是紧挨着存储的,中间不允许有空白或跳跃。因为数组的这一特性,我们可以通过索引直接访问数组的任意元素,时间复杂度为 O(1)
如果需要一个能在中间插入或删除元素的数据结构,可以使用链表或动态数组。
动态数组(如 ArrayList
):
在内部仍然使用数组,但会在内存不足时自动扩展,且扩展时新数组还是连续的内存。它的随机访问性能与数组相同O(1),但在插入和删除操作时可能会有较大的开销。
二、为什么
链表是一种重要的数据结构,它在很多场景下比其他数据结构(如数组)更合适。以下是链表的几个主要用途和需要链表的原因:
1. 动态数据管理
链表的大小是动态的,可以根据需要增加或减少节点。这对于那些数据量不确定或频繁变化的场景非常有用。例如,内存管理中的空闲块链表、浏览器历史记录、音乐播放列表等需要频繁插入或删除元素的场景,链表能够灵活处理数据。
2. 高效的插入和删除操作
在链表中,插入和删除操作非常高效,时间复杂度为O(1)
。对于数组,如果在中间插入或删除元素,可能需要移动大量数据,时间复杂度为O(n)
。因此,当需要频繁插入或删除元素时,链表是一个更好的选择。例如:
- 实现栈和队列:链表可以用来高效地实现栈和队列的数据结构,支持在头部或尾部的快速插入和删除。
- 实时数据流处理:在处理数据流时,链表可以高效地添加和移除数据。
3. 节省内存和避免内存浪费
链表不会像数组那样需要在创建时就分配固定的内存空间,因此它避免了因数据量不确定导致的内存浪费。例如:
- 处理大文件或大数据:链表可以逐步读取数据并添加到结构中,而不需要一次性分配大量内存。
- 实现动态内存分配器:链表用于操作系统中的内存管理器(如堆)来动态分配和释放内存。
4. 灵活的数据存储
链表可以轻松实现各种高级数据结构(如双向链表、循环链表、跳跃表等),这些结构可以进一步优化特定类型的操作。例如:
- 图(Graph)和树(Tree)结构:链表可以用来表示图中的邻接表和树结构中的子节点。
5. 方便的扩展和连接操作
链表可以轻松合并两个链表而不需要移动数据。链表的拆分和合并操作可以在O(1)
时间内完成,这在许多算法中(如快速排序中的链表版本)是很有用的。
6. 适用于链式存储的算法和应用
某些算法的设计思路和应用场景更适合链式存储。例如:
- LRU缓存机制:链表可以用来实现缓存淘汰策略(如LRU缓存),因为它允许快速地移动、删除和插入元素。
总结来说,链表的优势在于其动态性和插入/删除效率,因此在数据频繁变化、大小不确定以及需要灵活的内存管理和数据结构设计时,链表是一种非常有用的选择。
三、怎么办
链表的实现通常是通过节点类(Node)和链表类(LinkedList)来完成的。以下是链表的基本实现和一些常见的算法题考查方式。
链表的基本实现
以单链表(Singly Linked List)为例,说明链表的实现方法:
1. 单链表的基本实现
定义节点类(ListNode
)
每个节点包含数据部分和指向下一个节点的指针部分。
class ListNode {
int value; // 数据部分
ListNode next; // 指向下一个节点的指针
// 构造函数:初始化对象,特点:名与类同,无返回类型,new的时候自动调用
ListNode(int value) {
this.value = value;
//Java中没有像C/C++那样的指针操作,而是使用引用来指向对象的内存地址。引用与指针相似,但更加安全
//引用是一个对象的内存地址的“句柄”。你不能直接操作这个地址,引用只是一个用来访问对象的安全指针
//this是 Java 中的一个关键字,表示对当前对象的引用。它在类的方法和构造函数中非常有用,帮助区分成员变量和局部变量,或者在需要引用当前对象的情况下使用。
//在 ListNode 类中,next 的数据类型是 ListNode,属于 引用类型
this.next = null;
}
}
定义链表类(LinkedList
)
链表类支持常见的链表操作,如插入、删除、查找等。
class LinkedList {
ListNode head; // 链表的头节点
// 构造函数初始化链表为空
public LinkedList() {
this.head = null;
}
// 在链表末尾添加一个节点
public void append(int value) {
ListNode newNode = new ListNode(value);
if (head == null) { // 如果链表为空,新的节点就是头节点
head = newNode;
} else { // 遍历链表找到最后一个节点
ListNode current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
}
// 打印链表的所有元素
public void printList() {
ListNode current = head;
while (current != null) {
System.out.print(current.value + " -> ");
current = current.next;
}
System.out.println("null");
}
}
使用上述代码,可以创建一个链表,并向其中添加节点和打印节点内容:
public class Main {
public static void main(String[] args) {
LinkedList ll = new LinkedList();
ll.append(1);
ll.append(2);
ll.append(3);
ll.printList(); // 输出: 1 -> 2 -> 3 -> null
}
}
2. 链表的常见算法题
(1) 反转链表
反转链表是最常见的链表问题。它要求将链表的节点顺序反转。
public ListNode reverseList(ListNode head) {
ListNode prev = null; // 前一个节点
ListNode current = head; // 当前节点
while (current != null) {
ListNode nextNode = current.next; // 保存下一个节点
current.next = prev; // 反转当前节点的指向
prev = current; // 更新 prev
current = nextNode; // 更新 current
}
return prev; // 新的头节点
}
(2) 寻找链表的中间节点
使用快慢指针法找到链表的中间节点。
public ListNode middleNode(ListNode head) {
ListNode slow = head; // 慢指针
ListNode fast = head; // 快指针
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // 慢指针到达中间节点
}
(3) 合并两个有序链表
将两个升序链表合并为一个新的升序链表。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0); // 创建一个虚拟头节点
ListNode current = dummy;
//链表值决定current的next是什么,节点后移
while (l1 != null && l2 != null) {
if (l1.value < l2.value) {
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;
}
(4) 删除链表的倒数第N个节点
删除链表中的倒数第N
个节点。
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode first = dummy;
ListNode second = dummy;
// 先让第一个指针走 n+1 步
for (int i = 0; i <= n; i++) {
first = first.next;
}
//f始终比s多走n+1,f到结尾(结尾本身就是null的,所以不存在指到虚空去了),则s隔f有n+1这么远,再往前就隔n个那么远
//即f指向的下一个为倒数第n个节点
// 同时移动两个指针,直到第一个指针到达末尾
while (first != null) {
first = first.next;
second = second.next;
}
// 删除倒数第 N 个节点
second.next = second.next.next;
return dummy.next;
}
(5) 判断链表是否有环
使用快慢指针判断链表中是否存在环。
public 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;
fast = fast.next.next;
if (slow == fast) { // 快慢指针相遇,说明有环
return true;
}
}
return false; // 没有环
}
总结
在Java中,链表的实现和算法题考察与其他语言类似,都是通过节点类和链表类的组合来实现各种操作。在编程面试中,链表题目主要考察指针操作、链表结构的理解和掌握。熟悉基本的链表操作和算法,可以帮助应对各种链表相关的算法题。