Java数据结构之链表(LinkedList)
1. 简介
链表是一种常见的数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的引用。链表具有灵活性和高效的插入和删除操作,因此在很多场景下都比数组更加适用。本文将详细介绍Java中链表的各种类型以及它们的操作和应用。
2. 单链表
2.1 单链表的基本结构
单链表是最简单的链表类型,它由一系列节点组成,每个节点包含数据和指向下一个节点的引用。在Java中,可以使用如下的类定义单链表的节点:
class ListNode {
int data;
ListNode next;
public ListNode(int data) {
this.data = data;
this.next = null;
}
}
单链表的头节点是链表的入口,通常使用一个指向头节点的引用来操作整个链表。以下是单链表的基本操作。
2.2 单链表的插入操作
在单链表中插入一个新节点,通常有三种情况:
2.2.1 在链表头部插入新节点
public void insertAtBeginning(int data) {
ListNode newNode = new ListNode(data);
newNode.next = head;
head = newNode;
}
2.2.2 在链表尾部插入新节点
public void insertAtEnd(int data) {
ListNode newNode = new ListNode(data);
if (head == null) {
head = newNode;
} else {
ListNode current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
}
2.2.3 在指定位置插入新节点
public void insertAtIndex(int data, int index) {
if (index < 0) {
throw new IllegalArgumentException("Invalid index");
}
ListNode newNode = new ListNode(data);
if (index == 0) {
newNode.next = head;
head = newNode;
} else {
ListNode current = head;
int i = 0;
while (i < index - 1 && current != null) {
current = current.next;
i++;
}
if (current == null) {
throw new IndexOutOfBoundsException("Index out of range");
}
newNode.next = current.next;
current.next = newNode;
}
}
2.3 单链表的删除操作
单链表的删除操作通常有以下几种情况:
2.3.1 删除链表头节点
public void deleteAtBeginning() {
if (head != null) {
head = head.next;
}
}
2.3.2 删除链表尾节点
public void deleteAtEnd() {
if (head == null) {
return;
}
if (head.next == null) {
head = null;
return;
}
ListNode current = head;
while (current.next.next != null) {
current = current.next;
}
current.next = null;
}
2.3.3 删除指定位置的节点
public void deleteAtIndex(int index) {
if (index < 0 || head == null) {
return;
}
if (index == 0) {
head = head.next;
return;
}
ListNode current = head;
int i = 0;
while (i < index - 1 && current != null) {
current = current.next;
i++;
}
if (current == null || current.next == null) {
return;
}
current.next = current.next.next;
}
2.4 单链表的查找操作
查找操作用于查找链表中是否存在某个特定的元素,通常实现如下:
public boolean contains(int data) {
ListNode current = head;
while (current != null) {
if (current.data == data) {
return true;
}
current = current.next;
}
return false;
}
2.5 单链表的时间复杂度
- 插入和删除操作的时间复杂度为O(n),其中n是链表的长度。
- 查找操作的时间复杂度也为O(n)。
单链表适用于需要频繁插入和删除元素的场景,但不适用于需要快速随机访问元素的场景。
3. 双链表
3.1 双链表的基本结构
双链表与单链表不同之处在于,每个节点除了包含数据和指向下一个节点的引用外,还包含指向前一个节点的引用。以下是双链表节点的定义:
class DoubleListNode {
int data;
DoubleListNode next;
DoubleListNode prev;
public DoubleListNode(int data) {
this.data = data;
this.next = null;
this.prev = null;
}
}
双链表支持双向遍历
,因此在某些场景下比单链表更加灵活。以下是双链表的基本操作。
3.2 双链表的插入操作
双链表的插入操作与单链表类似,但需要同时更新前后节点的引用。
3.2.1 在链表头部插入新节点
public void insertAtBeginning(int data) {
DoubleListNode newNode = new DoubleListNode(data);
newNode.next = head;
if (head != null) {
head.prev = newNode;
}
head = newNode;
}
3.2.2 在链表尾部插入新节点
public void insertAtEnd(int data) {
DoubleListNode newNode = new DoubleListNode(data);
if (head == null) {
head = newNode;
} else {
DoubleListNode current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
newNode.prev = current;
}
}
3.2.3 在指定位置插入新节点
public void insertAtIndex(int data, int index) {
if (index < 0) {
throw new IllegalArgumentException("Invalid index");
}
DoubleListNode newNode = new DoubleListNode(data);
if (index == 0) {
newNode.next = head;
if (head != null) {
head.prev = newNode;
}
head = newNode;
} else {
DoubleListNode current = head;
int i = 0;
while (i < index - 1 && current != null) {
current = current.next;
i++;
}
if (current == null) {
throw new IndexOutOfBoundsException("Index out of range");
}
newNode.next = current.next;
if (current.next != null) {
current.next.prev = newNode;
}
current.next = newNode;
newNode.prev = current;
}
}
3.3 双链表的删除操作
双链表的删除操作也与单链表类似,需要同时更新前后节点的引用。
3.3.1 删除链表头节点
public void deleteAtBeginning() {
if (head != null) {
head = head.next;
if (head != null) {
head.prev = null;
}
}
}
3.3.2 删除链表尾节点
public void deleteAtEnd() {
if (head == null) {
return;
}
if (head.next == null) {
head = null;
return;
}
DoubleListNode current = head;
while (current.next.next != null) {
current = current.next;
}
current.next = null;
}
3.3.3 删除指定位置的节点
public void deleteAtIndex(int index) {
if (index < 0 || head == null) {
return;
}
if (index == 0) {
head = head.next;
if (head != null) {
head.prev = null;
}
return;
}
DoubleListNode current = head;
int i = 0;
while (i < index - 1 && current != null) {
current = current.next;
i++;
}
if (current == null || current.next == null) {
return;
}
current.next = current.next.next;
if (current.next != null) {
current.next.prev = current;
}
}
3.4 双链表的查找操作
双链表的查找操作与单链表类似,可以从前往后或从后往前遍历链表。
3.4.1 从前往后查找
public boolean contains(int data) {
DoubleListNode current = head;
while (current != null) {
if (current.data == data) {
return true;
}
current = current.next;
}
return false;
}
3.4.2 从后往前查找
public boolean containsReverse(int data) {
DoubleListNode current = head;
while (current != null && current.next != null) {
current = current.next;
}
while (current != null) {
if (current.data == data) {
return true;
}
current = current.prev;
}
return false;
}
3.5 双链表的时间复杂度
- 插入和删除操作的时间复杂度为O(n),其中n是链表的长度。
- 查找操作的时间复杂度仍然为O(n),但从后往前查找的效率更高。
双链表适用于需要双向遍历的场景,以及需要在某些情况下从后往前查找元素的场景。
4. 循环链表
4.1 循环链表的基本结构
循环链表是一种特殊的链表,它的尾节点指向头节点,形成一个闭环。以下是循环链表节点的定义:
class CircularListNode {
int data;
CircularListNode next;
public CircularListNode(int data) {
this.data = data;
this.next = this;
}
}
循环链表通常用于模拟循环队列等数据结构。以下是循环链表的基本操作。
4.2 循环链表的插入操作
循环链表的插入操作与单链表类似,但需要考虑尾节点的特殊情况。
4.2.1 在链表头部插入新节点
public void insertAtBeginning(int data) {
CircularListNode newNode = new CircularListNode(data);
if (head == null) {
head = newNode;
} else {
newNode.next = head.next;
head.next = newNode;
}
}
4.2.2 在链表尾部插入新节点
public void insertAtEnd(int data) {
CircularListNode newNode = new CircularListNode(data);
if (head == null) {
head = newNode;
} else {
newNode.next = head.next;
head.next = newNode;
head = newNode;
}
}
4.2.3 在指定位置插入新节点
public void insertAtIndex(int data, int index) {
if (index < 0) {
throw new IllegalArgumentException("Invalid index");
}
CircularListNode newNode = new CircularListNode(data
);
if (index == 0) {
if (head == null) {
head = newNode;
} else {
newNode.next = head.next;
head.next = newNode;
}
} else {
CircularListNode current = head;
int i = 0;
while (i < index && current != null) {
current = current.next;
i++;
}
if (current == null) {
throw new IndexOutOfBoundsException("Index out of range");
}
newNode.next = current.next;
current.next = newNode;
}
}
4.3 循环链表的删除操作
循环链表的删除操作与单链表类似,但需要特别处理尾节点的引用。
4.3.1 删除链表头节点
public void deleteAtBeginning() {
if (head != null) {
if (head.next == head) {
head = null;
} else {
head.next = head.next.next;
}
}
}
4.3.2 删除链表尾节点
public void deleteAtEnd() {
if (head != null) {
CircularListNode current = head;
while (current.next != head) {
current = current.next;
}
if (current == head) {
head = null;
} else {
current.next = head.next;
head = current;
}
}
}
4.3.3 删除指定位置的节点
public void deleteAtIndex(int index) {
if (index < 0 || head == null) {
return;
}
if (index == 0) {
if (head.next == head) {
head = null;
} else {
head.next = head.next.next;
}
return;
}
CircularListNode current = head;
int i = 0;
while (i < index - 1 && current != null) {
current = current.next;
i++;
}
if (current == null) {
return;
}
current.next = current.next.next;
}
4.4 循环链表的查找操作
循环链表的查找操作与单链表类似,可以从头节点开始遍历。
public boolean contains(int data) {
if (head == null) {
return false;
}
CircularListNode current = head;
do {
if (current.data == data) {
return true;
}
current = current.next;
} while (current != head);
return false;
}
4.5 循环链表的时间复杂度
- 插入和删除操作的时间复杂度为O(n),其中n是链表的长度。
- 查找操作的时间复杂度也为O(n)。
循环链表适用于需要循环操作的场景,如模拟循环队列等。
5. 常见问题
5.1 链表与数组的比较
链表和数组都是常见的数据结构,它们各自有自己的优点和缺点。链表适用于需要频繁插入和删除元素的场景,因为插入和删除操作的时间复杂度为O(1),而数组的时间复杂度为O(n)。但链表的查找操作相对较慢,时间复杂度为O(n),而数组的查找操作时间复杂度为O(1)。因此,在选择数据结构时,需要根据具体的应用场景来决定使用链表还是数组。
5.2 如何避免链表中的内存泄漏?
内存泄漏是一种常见的问题,特别是在使用链表等动态数据结构时。为了避免链表中的内存泄漏,可以采取以下几种措施:
- 确保及时释放不再使用的节点,尤其是在删除节点时。
- 使用弱引用(WeakReference)来引用链表中的节点,以便在不再被引用时能够被垃圾回收。
- 定期检查链表中的节点,识别不再需要的节点并进行释放。
5.3 如何判断链表中是否存在环?
要判断链表中是否存在环,可以使用快慢指针法。具体步骤如下:
- 初始化两个指针,一个慢指针(slow)每次移动一个节点,一个快指针(fast)每次移动两个节点。
- 如果链表中存在环,快指针和慢指针最终会相遇。
- 如果链表中不存在环,快指针将会先到达链表的末尾(即快指针为null)。
以下是Java代码示例:
public 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;
}
6. 总结
本文详细介绍了Java中链表的各种类型,包括单链表、双链表和循环链表,以及它们的基本操作和时间复杂度。链表是一种常见的数据结构,具有灵活的插入和删除操作,适用于不同的应用场景。选择合适的链表类型取决于具体的需求,需要根据应用的特点来做出决策。