1. 双向循环链表的定义和特点
在计算机科学中,链表(Linked List)是一种常见的数据结构,由若干个节点组成,每个节点包含一个值和一个指针,指向下一个节点。
与单向链表不同,双向链表(Doubly Linked List)每个节点还有一个指向前驱节点的指针。因此,我们可以从任意一个节点开始,向前或向后遍历整个链表。
在双向循环链表(Doubly Circular Linked List)中,链表的尾部指针也指向头部,这样就形成了一个循环结构。这种数据结构的好处是,我们可以像遍历数组一样遍历链表,只需要不断地往后移动指针,直到回到起始位置即可。
双向循环链表的主要优点是在插入和删除结点时具有较好的灵活性和效率。由于每个结点都包含其前驱节点和后继节点的引用,我们可以很方便地在链表中间插入或删除结点。
2. 双向循环链表的实现
public class DoublyCircularLinkedList<T> {
private Node<T> head; // 头结点指针
private int size; // 链表大小
/**
* 结点类,包含数据、前驱节点和后继节点三个属性
*
* @param <T> 链表中存储的元素类型
*/
private static class Node<T> {
T data; // 数据
Node<T> prev; // 前驱节点
Node<T> next; // 后继节点
Node(T data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
/**
* 构造函数,初始化链表为空
*/
public DoublyCircularLinkedList() {
size = 0;
}
/**
* 添加新结点到链表尾部
*
* @param data 新节点的数据
*/
public void add(T data) {
Node<T> newNode = new Node<>(data);
if (head == null) { // 如果链表为空,则将头结点指针设置为新结点,并形成一个自环
head = newNode;
head.prev = head;
head.next = head;
} else { // 否则,将新节点插入到末尾
Node<T> tail = head.prev; // 获取尾结点
tail.next = newNode; // 尾结点的后继节点指向新节点
newNode.prev = tail; // 新节点的前驱节点指向尾结点
newNode.next = head; // 新节点的后继节点指向头结点
head.prev = newNode; // 头结点的前驱节点指向新节点
}
size++; // 增加链表大小
}
/**
* 获取链表中指定位置的元素
*
* @param index 元素的位置,从0开始计数
* @return 链表中对应位置的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出该异常
*/
public T get(int index) throws IndexOutOfBoundsException {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds!");
}
Node<T> current = head;
for (int i = 0; i < index; i++) { // 遍历链表,找到指定位置的结点
current = current.next;
}
return current.data; // 返回结点的数据
}
/**
* 删除链表中指定位置的元素
*
* @param index 元素的位置,从0开始计数
* @return 被删除的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出该异常
*/
public T remove(int index) throws IndexOutOfBoundsException {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds!");
}
Node<T> current = head;
for (int i = 0; i < index;
i++) { // 遍历链表,找到指定位置的结点
current = current.next;
}
if (size == 1) { // 如果链表只有一个结点,则将头结点指针设置为null
head = null;
} else {
// 否则,更新前驱和后继节点的引用关系
current.prev.next = current.next;
current.next.prev = current.prev;
if (current == head) {
// 如果要删除的是头结点,则需要更新头结点指针
head = current.next;
}
}
size--;
// 减少链表大小
current.prev = null;
//清空被删除结点的引用,释放内存
current.next = null;
return current.data; // 返回被删除的结点的数据
}
/**
* 检查链表是否为空
*
* @return 如果链表为空,则返回 true,否则返回 false。
*/
public boolean isEmpty() {
return head == null;
}
/**
* 返回链表中元素的个数
*
* @return 链表中元素的个数
*/
public int size() {
return size;
}
/**
* 打印链表中所有结点的数据
*/
public void printList() {
if (head
== null) { // 如果链表为空,则抛出异常
throw new IllegalStateException("List is empty.");
}
Node<T> current = head;
do {
// 从头结点开始遍历链表,直到回到头结点为止
System.out.print(current.data + " ");
// 打印结点的数据
current = current.next; // 将当前结点指针后移
} while (current != head);
System.out.println();
}
/**
* 清空链表并释放内存
*/
public void clear() {
while (head
!= null) { // 遍历链表,逐一删除结点
Node<T> temp = head;
head = head.next;
temp.prev = null;
temp.next = null;
}
size = 0; // 将链表大小置为0
}
}
3. 双向循环链表的常见操作及其实现
在双向循环链表中,常见的操作包括:
- 在链表尾部添加一个新节点
- 获取指定位置的节点
- 删除指定位置的节点
- 判断链表是否为空
- 获取链表中元素的个数
- 打印链表中所有节点的数据
下面将分别介绍这些操作的具体实现。
3.1 在链表尾部添加一个新节点
向双向循环链表尾部添加一个新节点的过程如下:
- 如果链表为空,则将头结点指针设置为新节点,并形成一个自环。
- 否则,找到链表中的最后一个节点,即头结点的前驱节点。然后将新节点插入到末尾,使得新节点成为新的尾节点。
该操作的实现代码如下:
在 public void add(T data) {
Node<T> newNode = new Node<>(data);
if (head == null) { // 如果链表为空,则将头结点指针设置为新结点,并形成一个自环
head = newNode;
head.prev = head;
head.next = head;
} else { // 否则,将新节点插入到末尾
Node<T> tail = head.prev; // 获取尾结点
tail.next = newNode; // 尾结点的后继节点指向新节点
newNode.prev = tail; // 新节点的前驱节点指向尾结点
newNode.next = head; // 新节点的后继节点指向头结点
head.prev = newNode; // 头结点的前驱节点指向新节点
}
size++; // 增加链表大小
}
3.2 获取指定位置的节点
获取双向循环链表中指定位置的节点的过程如下:
- 遍历链表,直到找到指定位置的节点。
- 返回该节点的数据。
该操作的实现代码如下:
public T get(int index) throws IndexOutOfBoundsException {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds!");
}
Node<T> current = head;
for (int i = 0; i < index; i++) { // 遍历链表,找到指定位置的结点
current = current.next;
}
return current.data; // 返回结点的数据
}
3.3 删除指定位置的节点
删除双向循环链表中指定位置的节点的过程如下:
遍历链表,直到找到指定位置的节点。
如果链表只有一个节点,则将头结点指针设置为null。
否则,更新被删除节点的前驱节点和后继节点的引用关系。如果被删除的是头结点,则还需要更新头结点的指针。
减少链表大小。
清空被删除节点的引用,释放内存。
返回被删除节点的数据。
该操作的实现代码如下
public T remove(int index) throws IndexOutOfBoundsException {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index out of bounds!");
}
Node<T> current = head;
for (int i = 0; i < index; i++) { // 遍历链表,找到指定位置的结点
current = current.next;
}
if (size == 1) { // 如果链表只有一个结点,则将头结点指针设置为null
head = null;
} else { // 否则,更新前驱和后继节点的引用关系
current.prev.next = current.next;
current.next.prev = current.prev;
if (current == head) { // 如果要删除的是头结点,则需要更新头结点指针
head = current.next;
}
}
size--; // 减少链表大小
current.prev = null; // 清空被删除结点的引用,释放内存
current.next = null;
return current.data; // 返回被删除的结点的数据
}
总结
- 双向循环列表可以用于实现循环队列、双端队列等数据结构,也可以用于实现LRU缓存淘汰算法。
- 在双向循环列表中,所有节点都相互连接,形成一个环状结构。
- 双向循环列表支持在任意位置插入和删除节点,并且这些操作的时间复杂度为O(1)。
- 与单向链表相比,双向循环列表的优点是可以从后往前遍历,缺点是需要更多的内存来存储额外的指针。
- 双向循环列表的实现方式比较简单,需要定义一个Node类来表示每个节点,并提供插入、删除、查找等基本操作。
- 双向循环列表可以使用迭代器来遍历节点,也可以使用foreach循环来遍历节点。