目录
一、认识链表
1. 链表的定义
链表是由称为节点的独立内存单元通过指针连接形成的线性结构。节点在内存中的地址是不连续的,每个节点都包含存储实际数据的数据域和下一个结点的地址信息的指针域。
2. 链表的优缺点
- 优点:
-
动态内存分配:链表的大小可以在运行时动态调整,不需要预先分配固定大小的内存空间
-
插入和删除效率高:在已知位置插入或删除节点时,时间复杂度为O(1)
- 内存利用率高:链表的内存分配是按需分配的,不会浪费过多的存储空间
- 缺点:
-
随机访问效率低:链表不支持随机访问,访问某个节点需要从头遍历,时间复杂度为 O(n)
-
内存开销大:链表中的节点占用的内存比数组多,因为每个节点有一个简单变量和一个指针变量,指针变量占用4字节内存。
3. 链表的与数组的对比
特点 | 链表 | 数组 |
内存分配 | 动态分配,非连续内存 | 静态分配,连续内存 |
插入/删除效率 | O(1),只需修改指针 | O(n),需要移动元素 |
随机访问效率 | O(n),需要遍历 | O(1),直接通过索引随机访问 |
内存利用率 | 高,按需分配 | 可能存在浪费 |
二、单向链表
单向链表的每个节点包含数据和指向下一个节点的指针。
1. 单向链表的模拟实现
1.1 创建链表
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
class LinkedList {
ListNode head; // 头节点
public LinkedList() {
this.head = null;
}
}
1.2 打印链表
public void printList() {
ListNode current = head;
while (current != null) {
System.out.print(current.val + " -> ");
current = current.next;
}
System.out.println("null");
}
1.3 求链表长度
public int getLength() {
int length = 0;
ListNode current = head;
while (current != null) {
length++;
current = current.next;
}
return length;
}
2. 单向链表的常见方法
2.1 头插法
public void insertAtHead(int val) {
ListNode newNode = new ListNode(val);
newNode.next = head;
head = newNode;
}
2.2 尾插法
public void insertAtTail(int val) {
ListNode newNode = new ListNode(val);
if (head == null) {
head = newNode;
return;
}
ListNode current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
2.3 插入指定位置
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) return;
if (index == 0) {
addFirst(val);
return;
}
ListNode prev = head;
for (int i = 0; i < index-1; i++) {
prev = prev.next;
}
ListNode newNode = new ListNode(val);
newNode.next = prev.next;
prev.next = newNode;
size++;
}
2.4 查找值为 key
的节点
public boolean contains(int key) {
ListNode current = head;
while (current != null) {
if (current.val == key) return true;
current = current.next;
}
return false;
}
2.5 修改值为 key
的节点
public void update(int key, int newVal) {
ListNode current = head;
while (current != null) {
if (current.val == key) {
current.val = newVal;
return;
}
current = current.next;
}
}
2.6 删除值为 key
的第一个节点
public void deleteByKey(int key) {
if (head == null) {
return;
}
if (head.val == key) {
head = head.next;
return;
}
ListNode current = head;
while (current.next != null && current.next.val != key) {
current = current.next;
}
if (current.next != null) {
current.next = current.next.next;
}
}
2.7 删除值为 key
的所有节点
public void deleteAll(int key) {
while (head != null && head.val == key) {
head = head.next;
}
ListNode current = head;
while (current != null && current.next != null) {
if (current.next.val == key) {
current.next = current.next.next;
} else {
current = current.next;
}
}
}
2.8 自动排序添加
// 插入节点时保持链表的有序性
public void insertSorted(int val) {
ListNode newNode = new ListNode(val);
if (head == null || head.val >= val) {
newNode.next = head;
head = newNode;
return;
}
ListNode current = head;
while (current.next != null && current.next.val < val) {
current = current.next;
}
newNode.next = current.next;
current.next = newNode;
}
2.9 反转链表
public void reverseList() {
if (head == null || head.next == null) {
return;
}
ListNode prev = null;
ListNode current = head;
ListNode next = null;
while (current != null) {
next = current.next; // 暂存下一个节点
current.next = prev; // 反转当前节点的指针
prev = current; // 将 prev 向前移动
current = next; // 将 current 向前移动
}
head = prev; // 更新头节点为新的头节点
}
2.10 清空链表
public void clear() {
head = null;
}
三、双向链表
双向链表的每个节点包含两个指针,分别指向前一个节点和后一个节点。
1. 双向链表的模拟实现
class DoublyListNode {
int val;
DoublyListNode prev;
DoublyListNode next;
DoublyListNode(int val) {
this.val = val;
}
}
2. 双向链表的常见方法
2.1 头插法
public void insertAtHead(int val) {
DoublyListNode newNode = new DoublyListNode(val);
if (head == null) {
head = newNode;
tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
2.2 尾插法
public void insertAtTail(int val) {
DoublyListNode newNode = new DoublyListNode(val);
if (tail == null) {
head = newNode;
tail = newNode;
return;
}
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
2.3 插入指定位置
public void insertAtPosition(int val, int position) {
if (position < 0) {
System.out.println("Invalid position");
return;
}
if (position == 0) {
insertAtHead(val);
return;
}
DoublyListNode newNode = new DoublyListNode(val);
DoublyListNode current = head;
for (int i = 0; current != null && i < position - 1; i++) {
current = current.next;
}
if (current == null) {
System.out.println("Position out of bounds");
return;
}
newNode.next = current.next;
newNode.prev = current;
if (current.next != null) {
current.next.prev = newNode;
}
current.next = newNode;
}
2.4 清空链表
public void clear() {
head = null;
tail = null;
}
四、循环链表
循环链表是一种特殊的链表,其尾节点的指针指向头节点,形成一个环形结构。
约瑟夫(Josephu)环问题
约瑟夫环问题描述如下:有 n 个人围成一个圈,从第一个人开始报数,报到 k 的人出圈,然后从下一个人重新开始报数,直到所有人都出圈。我们可以用循环链表来模拟这个问题。
// 创建循环链表
public CircularListNode createCircularList(int n) {
CircularListNode head = new CircularListNode(1);
CircularListNode current = head;
for (int i = 2; i <= n; i++) {
CircularListNode newNode = new CircularListNode(i);
current.next = newNode;
current = newNode;
}
current.next = head; // 形成环
return head;
}
public void josephu(int n, int k) {
CircularListNode head = createCircularList(n);
CircularListNode current = head;
while (current.next != current) {
for (int i = 1; i < k; i++) {
current = current.next;
}
System.out.println("Out: " + current.next.val);
current.next = current.next.next;
current = current.next;
}
System.out.println("Last: " + current.val);
}
五、双向循环链表
双向循环链表的每个节点不仅包含指向下一个节点的指针,还包含指向前一个节点的指针,且头尾节点相互连接,形成一个闭环。
每个节点包含三个部分:两个地址部分(分别指向前后两个节点)+一个数据部分。
六、LinkedList 类
1. 什么是LinkedList
LinkedList 是 Java 标准库中的一个类,既可以作为链表使用,也可以作为队列或栈使用。LinkedList的底层是双向链表结构,支持高效的插入和删除操作。
2. LinkedList的构造方法
import java.util.LinkedList; // 使用前需要引入它
LinkedList<Integer> list = new LinkedList<>();
3. LinkedList的常用方法
-
添加元素:
add(Element)
、add(index, Element)
、addFirst(Element)
、addLast(Element)
-
获取元素:
get(index)
、getFirst()
、getLast()
-
删除元素:
remove(Element)
、removeFirst()
、removeLast()
-
清空链表:
clear()
-
判断是否包含元素:
contains(Element)
4. LinkedList 与 ArrayList 的区别
特性 | ArrayList | LinkedList |
---|---|---|
底层结构 | 数组 | 双向链表 |
访问效率 | 随机访问,效率高 | 顺序访问,效率低 |
插入/删除效率 | 需要移动大量元素,效率低 | 只需修改指针,效率高 |
空间分配 | 预分配固定大小,可能浪费空间 | 动态分配,空间利用率高 |
链表是一种非常灵活而强大的数据结构,在许多算法和程序设计中扮演着重要角色。
本篇全面介绍了数组的相关知识,并结合Java代码示例进行讲解,希望能帮大家更好地掌握链表的理论基础和基本操作。
下期将深入探讨LeetCode上常用的链表练习题,并提供详细的解题思路和代码示例。这些练习题将帮助你巩固链表的知识,提升算法思维和编程能力。
谢谢大家的支持~