在上一期,我们深入探讨了数组数据结构以及其在编程中的广泛应用,包括了 HashMap、HashTable、ConcurrentHashMap、HashSet、LinkedHashMap 等数据结构。这期,我们将开始介绍链表数据结构以及与之相关的扩展内容。链表是一种基础而强大的数据结构,掌握链表的原理和应用将为您的编程技能增添新的维度。接下来的内容将帮助您更深入地理解链表,以及如何在实际项目中灵活应用它们。
链表(Linked List)是一种常见的线性数据结构,具有以下基本特性:
链式结构:链表是由节点(Node)组成的,每个节点包含两部分:数据域和指针域。数据域用于存储数据,指针域用于指向下一个节点,从而将节点串联在一起,形成链式结构。
动态大小:链表的大小可以动态增加或减少,不像数组一样需要预先分配固定大小的空间。这使得链表更加灵活,能够有效地处理不定数量的元素。
插入和删除效率高:由于链表的动态性,插入和删除元素的效率通常较高。只需改变节点之间的指针,而不需要像数组一样涉及元素的搬迁。
不连续的内存分配:链表中的节点可以分布在内存的不同位置,它们通过指针连接在一起,这与数组内存中连续的存储方式不同。
无需预分配内存空间:链表不需要预先分配连续的内存空间,因此不会出现数组中的“溢出”问题,但会引入一些额外的空间开销用于存储指针。
有单向链表和双向链表:链表可以是单向的,其中每个节点只有一个指向下一个节点的指针,也可以是双向的,其中每个节点同时具有指向下一个节点和上一个节点的指针,使得双向链表在某些操作中更加高效。
不支持随机访问:链表不支持像数组那样的随机访问,要访问链表中的某个元素,需要从头节点开始按顺序遍历。
常见操作:链表的常见操作包括在头部插入节点、在尾部插入节点、在指定位置插入节点、删除节点、查找节点等。
链表是计算机科学中重要且常用的数据结构,特别适用于需要频繁的插入和删除操作的场景,例如实现栈、队列、高级语言中的垃圾回收器等。在学习链表时,通常会深入研究单向链表、双向链表、循环链表等不同类型的链表以及它们的应用。
下面是一个示例代码,演示如何创建一个简单的单向链表以及基本的链表操作,包括插入、删除和遍历节点:
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
class LinkedList {
Node head;
public LinkedList() {
head = null;
}
// 插入节点到链表尾部
public void append(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
return;
}
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
// 删除节点
public void delete(int data) {
if (head == null) {
return;
}
if (head.data == data) {
head = head.next;
return;
}
Node current = head;
while (current.next != null && current.next.data != data) {
current = current.next;
}
if (current.next != null) {
current.next = current.next.next;
}
}
// 遍历链表并打印节点值
public void printList() {
Node current = head;
while (current != null) {
System.out.print(current.data + " ");
current = current.next;
}
System.out.println();
}
}
public class Main {
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
System.out.println("原始链表:");
list.printList();
list.delete(2);
System.out.println("删除节点后的链表:");
list.printList();
}
}
上述代码创建了一个简单的单向链表,包括插入、删除和遍历操作。在这个示例中,我们首先创建了一个链表对象 list
,然后向链表中追加了三个节点。接着,我们删除了节点值为 2 的节点,并打印了链表的最终状态。
链表是一种灵活的数据结构,适用于多种实际开发场景。以下是一些链表的常见使用场景:
实现栈和队列:链表可用于实现栈(先进后出)和队列(先进先出)这两种重要的数据结构。在栈中,节点从链表的头部添加和删除;而在队列中,节点从链表的尾部添加,从头部删除。
缓存实现:链表可用于实现简单的缓存结构,例如 LRU(最近最少使用)缓存策略。当缓存达到容量限制时,最久未使用的数据可以从链表头部删除。
编辑器的撤销功能:许多文本编辑器和图形设计工具使用链表来实现撤销(undo)和重做(redo)功能。每个编辑操作被保存为一个节点,用户可以通过链表的前进和后退来查看和还原编辑历史。
实现高级数据结构:链表是许多高级数据结构的基础,如跳表、图、树(例如二叉搜索树的底层实现 AVL 树)等。链表的灵活性使其成为构建这些复杂数据结构的有力工具。
表达无限数据流:在处理无限数据流时,链表可以很好地表示数据的有限窗口。只需保留链表中的有限数量的节点,以适应可用内存。
实现符号表:链表可用于实现符号表,其中每个节点包含键值对。这在编译器、解析器和数据库中有广泛的应用。
任务调度器:链表可用于管理和调度任务队列,例如操作系统中的任务调度器或异步编程中的任务队列。
多项式运算:链表可以用来表示多项式,其中每个节点包含一个项的系数和指数。这在数学和科学计算中很常见。
游戏开发:链表可以用于游戏中的对象管理,例如管理敌人、子弹、动画等游戏元素。
需要注意的是,虽然链表在某些场景中非常有用,但在其他场景中,数组或其他数据结构可能更适合。选择合适的数据结构取决于具体的问题和性能需求。链表的主要优势在于插入和删除操作的效率,但它在随机访问和空间占用方面可能不如数组。因此,在选择数据结构时,需要综合考虑问题的特性和需求。
链表是计算机科学中重要的数据结构,有许多与之相关的主要知识点。以下是关于链表的主要知识点的讲解:
以下是一个示例代码,展示了一个简单的节点类的定义和用法:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data) {
this.data = data;
this.next = null;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node<T> getNext() {
return next;
}
public void setNext(Node<T> next) {
this.next = next;
}
}
在上面的示例中,我们定义了一个泛型的 Node
类,包含以下成员变量和方法:
data
:用于存储节点的数据。next
:指向下一个节点的引用。使用示例:
public class Main {
public static void main(String[] args) {
// 创建节点
Node<Integer> node1 = new Node<>(10);
Node<Integer> node2 = new Node<>(20);
Node<Integer> node3 = new Node<>(30);
// 构建链表:node1 -> node2 -> node3
node1.setNext(node2);
node2.setNext(node3);
// 遍历链表并打印数据
Node<Integer> current = node1;
while (current != null) {
System.out.println(current.getData());
current = current.getNext();
}
}
}
在上述示例中,我们创建了三个整数类型的节点,并使用 setNext
方法将它们链接在一起,形成一个简单的链表。然后,我们遍历链表并打印每个节点的数据。这是一个基本的节点示例,用于构建链表等数据结构。
以下是一个简单的单向链表(Singly Linked List)的示例代码:
public class SinglyLinkedList<T> {
private Node<T> head;
public SinglyLinkedList() {
this.head = null;
}
// 添加元素到链表的末尾
public void append(T data) {
Node<T> newNode = new Node<>(data);
if (head == null) {
head = newNode;
} else {
Node<T> current = head;
while (current.getNext() != null) {
current = current.getNext();
}
current.setNext(newNode);
}
}
// 删除指定元素
public void delete(T data) {
if (head == null) {
return;
}
if (head.getData().equals(data)) {
head = head.getNext();
return;
}
Node<T> current = head;
while (current.getNext() != null && !current.getNext().getData().equals(data)) {
current = current.getNext();
}
if (current.getNext() != null) {
current.setNext(current.getNext().getNext());
}
}
// 打印链表元素
public void print() {
Node<T> current = head;
while (current != null) {
System.out.print(current.getData() + " -> ");
current = current.getNext();
}
System.out.println("null");
}
}
在上述代码中,我们定义了一个 SinglyLinkedList
类,表示单向链表。该链表包括以下方法:
append(data)
:将元素添加到链表的末尾。delete(data)
:删除指定元素。print()
:打印链表元素。节点(Node
)的定义与之前的示例相同。
使用示例:
public class Main {
public static void main(String[] args) {
SinglyLinkedList<Integer> list = new SinglyLinkedList<>();
list.append(1);
list.append(2);
list.append(3);
list.print(); // 输出: 1 -> 2 -> 3 -> null
list.delete(2);
list.print(); // 输出: 1 -> 3 -> null
}
}
在示例中,我们创建了一个整数类型的单向链表,并演示了如何向链表中添加元素、删除元素以及打印链表的操作。这是一个简单的单向链表示例,用于基本的链表操作。
以下是一个简单的双向链表(Doubly Linked List)的示例代码:
public class DoublyLinkedList<T> {
private Node<T> head;
private Node<T> tail;
public DoublyLinkedList() {
this.head = null;
this.tail = null;
}
// 在链表末尾添加元素
public void append(T data) {
Node<T> newNode = new Node<>(data);
if (head == null) {
head = newNode;
tail = newNode;
} else {
newNode.setPrev(tail);
tail.setNext(newNode);
tail = newNode;
}
}
// 在链表开头添加元素
public void prepend(T data) {
Node<T> newNode = new Node<>(data);
if (head == null) {
head = newNode;
tail = newNode;
} else {
newNode.setNext(head);
head.setPrev(newNode);
head = newNode;
}
}
// 删除指定元素
public void delete(T data) {
Node<T> current = head;
while (current != null) {
if (current.getData().equals(data)) {
if (current.getPrev() != null) {
current.getPrev().setNext(current.getNext());
} else {
head = current.getNext();
}
if (current.getNext() != null) {
current.getNext().setPrev(current.getPrev());
} else {
tail = current.getPrev();
}
return;
}
current = current.getNext();
}
}
// 打印链表元素
public void print() {
Node<T> current = head;
while (current != null) {
System.out.print(current.getData() + " <-> ");
current = current.getNext();
}
System.out.println("null");
}
}
在上述代码中,我们定义了一个 DoublyLinkedList
类,表示双向链表。该链表包括以下方法:
append(data)
:在链表末尾添加元素。prepend(data)
:在链表开头添加元素。delete(data)
:删除指定元素。print()
:打印链表元素。节点(Node
)的定义包括 prev
和 next
引用,用于指向前一个节点和后一个节点。
使用示例:
public class Main {
public static void main(String[] args) {
DoublyLinkedList<Integer> list = new DoublyLinkedList<>();
list.append(1);
list.append(2);
list.append(3);
list.print(); // 输出: 1 <-> 2 <-> 3 <-> null
list.prepend(0);
list.print(); // 输出: 0 <-> 1 <-> 2 <-> 3 <-> null
list.delete(2);
list.print(); // 输出: 0 <-> 1 <-> 3 <-> null
}
}
在示例中,我们创建了一个整数类型的双向链表,并演示了如何向链表中添加元素、删除元素以及打印链表的操作。这是一个简单的双向链表示例,用于基本的链表操作。
以下是一个简单的循环链表(Circular Linked List)的示例代码:
public class CircularLinkedList<T> {
private Node<T> head;
private Node<T> tail;
public CircularLinkedList() {
this.head = null;
this.tail = null;
}
// 在链表末尾添加元素
public void append(T data) {
Node<T> newNode = new Node<>(data);
if (head == null) {
head = newNode;
tail = newNode;
newNode.setNext(newNode); // 将尾节点的 next 指向自身,形成循环
} else {
newNode.setNext(head); // 新节点的 next 指向头节点
tail.setNext(newNode); // 尾节点的 next 指向新节点
tail = newNode; // 更新尾节点
}
}
// 删除指定元素
public void delete(T data) {
if (head == null) {
return;
}
if (head.getData().equals(data)) {
if (head.getNext() == head) { // 如果链表中只有一个节点
head = null;
tail = null;
} else {
tail.setNext(head.getNext()); // 更新尾节点的 next
head = head.getNext(); // 更新头节点
}
return;
}
Node<T> current = head;
while (current.getNext() != head) {
if (current.getNext().getData().equals(data)) {
current.setNext(current.getNext().getNext());
return;
}
current = current.getNext();
}
}
// 打印链表元素
public void print() {
if (head == null) {
System.out.println("null");
return;
}
Node<T> current = head;
do {
System.out.print(current.getData() + " -> ");
current = current.getNext();
} while (current != head);
System.out.println(" (head)");
}
}
在上述代码中,我们定义了一个 CircularLinkedList
类,表示循环链表。该链表包括以下方法:
append(data)
:在链表末尾添加元素,同时确保尾节点的 next
指向头节点,形成循环。delete(data)
:删除指定元素,需要特别处理头节点的删除和链表中只有一个节点的情况。print()
:打印链表元素,从头节点开始遍历,并在头节点处标记 “(head)”。节点(Node
)的定义与之前的示例相同。
使用示例:
public class Main {
public static void main(String[] args) {
CircularLinkedList<Integer> list = new CircularLinkedList<>();
list.append(1);
list.append(2);
list.append(3);
list.print(); // 输出: 1 -> 2 -> 3 -> (head)
list.delete(2);
list.print(); // 输出: 1 -> 3 -> (head)
}
}
在示例中,我们创建了一个整数类型的循环链表,并演示了如何向链表中添加元素、删除元素以及打印链表的操作。这是一个简单的循环链表示例,用于基本的链表操作。
链表的插入和删除:
链表的遍历:
时间复杂度分析:
链表的优点和缺点:
应用场景:
复杂链表结构:
了解这些主要知识点将帮助您深入理解链表的原理、应用和性能特征,从而更好地应用它们解决实际问题。链表是数据结构领域的基础,也是许多高级数据结构的构建模块。
Java中有一些数据类型和类可以被看作是链表的扩展,它们提供了更多的功能和特性,例如双向遍历、随机访问、自动扩容等。以下是一些常见的链表扩展类型:
ArrayList
是 Java 中的一个动态数组实现。它允许在数组末尾高效地添加和删除元素,并且支持随机访问,因此在某些场景下可以被视为链表的扩展。ArrayList
需要预先分配一定的内存空间,并会在需要时自动扩容。以下是 ArrayList
的核心代码讲解,涵盖了初始化、添加元素、删除元素、扩容等关键操作。
首先,让我们看一下 ArrayList
的基本属性和构造方法:
// ArrayList 类的定义
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 存储元素的数组
transient Object[] elementData;
// ArrayList 的大小,即元素的个数
private int size;
// 构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
// ...
}
在上述代码中,ArrayList
使用一个 Object
类型的数组 elementData
来存储元素。默认情况下,elementData
初始化为空数组,容量为 DEFAULT_CAPACITY
(默认为 10)。size
属性表示当前 ArrayList
中的元素个数。
接下来,让我们看一下 ArrayList
的添加元素操作,包括 add
方法和扩容:
// 添加元素到 ArrayList
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e;
return true;
}
// 确保容量足够,如果不够则进行扩容
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容操作
private void grow(int minCapacity) {
// 原容量
int oldCapacity = elementData.length;
// 新容量,扩容为原来的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量仍小于所需容量,新容量则为所需容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 复制元素到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
在上述代码中,add
方法用于将元素添加到 ArrayList
的末尾。如果添加元素后,元素个数超过了当前数组的容量,就会触发扩容操作。扩容操作会创建一个新的数组,并将元素从旧数组复制到新数组中,然后将新数组赋给 elementData
。
最后,让我们看一下 ArrayList
的删除元素操作,包括 remove
方法:
// 删除指定索引位置的元素
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
// 计算需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index+1, elementData, index, numMoved);
}
elementData[--size] = null; // 清空最后一个元素,协助垃圾回收
return oldValue;
}
// 辅助方法,检查索引是否越界
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 辅助方法,获取指定索引位置的元素
E elementData(int index) {
return (E) elementData[index];
}
remove
方法用于删除指定索引位置的元素。首先,它会检查索引是否越界,然后使用 System.arraycopy
将后面的元素向前移动,覆盖被删除的元素。最后,将末尾的元素置为 null
,以协助垃圾回收。
这些是 ArrayList
的核心操作,它们确保了 ArrayList
的高效添加
、删除和扩容能力。ArrayList
是 Java 集合框架中常用的数据结构之一,适用于大多数情况下的动态数组需求。
LinkedList
是 Java 中的双向链表实现。它支持在链表头部和尾部高效地添加和删除元素,同时也支持随机访问,但访问效率相对较低。LinkedList
的双向特性允许双向遍历。以下是 LinkedList
的核心代码讲解,涵盖了初始化、添加元素、删除元素等关键操作。
首先,让我们看一下 LinkedList
的基本属性和构造方法:
// LinkedList 类的定义
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 链表的首节点
transient Node<E> first;
// 链表的尾节点
transient Node<E> last;
// 链表的大小,即元素的个数
transient int size = 0;
// 构造方法
public LinkedList() {
}
// ...
}
在上述代码中,LinkedList
使用 Node
类来表示链表的节点。first
和 last
分别表示链表的首节点和尾节点。size
属性表示当前链表中的元素个数。
接下来,让我们看一下 LinkedList
的添加元素操作,包括 addFirst
、addLast
和 add
方法:
// 在链表头部添加元素
public void addFirst(E e) {
linkFirst(e);
}
// 在链表尾部添加元素
public void addLast(E e) {
linkLast(e);
}
// 在链表尾部添加元素,等效于 addLast
public boolean add(E e) {
linkLast(e);
return true;
}
// 在链表的指定位置添加元素
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
上述代码中,addFirst
方法用于在链表头部添加元素,addLast
方法用于在链表尾部添加元素,而 add
方法用于在链表尾部添加元素,并返回 true
。add
方法还支持在链表的指定位置添加元素。
接下来,让我们看一下 LinkedList
的删除元素操作,包括 removeFirst
、removeLast
和 remove
方法:
// 删除链表头部的元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
// 删除链表尾部的元素
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
// 删除链表中指定的元素
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
// ...
上述代码中,removeFirst
方法用于删除链表头部的元素,removeLast
方法用于删除链表尾部的元素。remove
方法用于删除链表中指定的元素。删除操作会调用 unlink
方法,将节点从链表中断开。
最后,让我们看一下 LinkedList
的遍历操作,包括 get
方法和迭代器:
// 获取链表中指定索引位置的元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
// 返回一个迭代器,支持从前往后遍历
public Iterator<E> iterator() {
return new ListItr(0);
}
// 返回一个迭代器,支持从后往前遍历
public Iterator<E> descendingIterator() {
return new DescendingIterator(size());
}
// ...
get
方法用于获取链表中指定索引位置的元素,迭代器支持从前往后和从后往前遍历链表。
这些是 LinkedList
的核心操作,它们确保了链表的高效添加和删除能力。LinkedList
适用于需要频繁在链表头部或尾部进行添加和删除操作的场景,但在需要随机访问元素的情况下效率相对较低。
Vector
与 ArrayList
类似,也是一个动态数组实现,但它是线程安全的。它提供了与 ArrayList
相似的操作,并且支持自动扩容。Vector
在多线程环境中使用,但通常会因为性能问题而被 ArrayList
或 LinkedList
替代。以下是 Vector
的核心代码讲解,涵盖了初始化、添加元素、删除元素、扩容等关键操作。
首先,让我们看一下 Vector
的基本属性和构造方法:
// Vector 类的定义
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 存储元素的数组
protected Object[] elementData;
// Vector 的大小,即元素的个数
protected int elementCount;
// 构造方法
public Vector() {
this(DEFAULT_CAPACITY);
}
public Vector(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
elementData = new Object[initialCapacity];
}
// ...
}
在上述代码中,Vector
使用一个 Object
类型的数组 elementData
来存储元素。默认情况下,elementData
初始化为指定的容量,容量不足时会触发扩容操作。elementCount
属性表示当前 Vector
中的元素个数。
接下来,让我们看一下 Vector
的添加元素操作,包括 addElement
和 add
方法:
// 向 Vector 添加元素
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
// 向 Vector 尾部添加元素
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
// 辅助方法,确保容量足够
private void ensureCapacityHelper(int minCapacity) {
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容操作
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0)
? capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
上述代码中,addElement
方法用于向 Vector
添加元素,add
方法用于向 Vector
尾部添加元素。如果添加元素后,元素个数超过了当前数组的容量,就会触发扩容操作。扩容操作会创建一个新的数组,并将元素从旧数组复制到新数组中,然后将新数组赋给 elementData
。
接下来,让我们看一下 Vector
的删除元素操作,包括 removeElement
方法:
// 删除指定元素
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}
// 删除指定索引位置的元素
public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount--;
elementData[elementCount] = null;
}
removeElement
方法用于删除指定元素,而 removeElementAt
方法用于删除指定索引位置的元素。删除操作会调用 System.arraycopy
将后面的元素向前移动,覆盖被删除的元素。
最后,让我们看一下 Vector
的遍历操作,包括 get
方法和迭代器:
// 获取指定索引位置的元素
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
// 返回一个迭代器,支持从前往后遍历
public Iterator<E> iterator() {
return listIterator(0);
}
// 返回一个列表迭代器,支持从前往后遍历
public synchronized ListIterator<E> listIterator(int index) {
if (index < 0 || index > elementCount)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
// ...
get
方法用于获取指定索引位置的元素,迭代器支持从前往后遍历 Vector
。
这些是 Vector
的核心操作,它们确保了 Vector
的线程安全和高效添加、删除、扩容能力。Vector
在多线程环境中使用,但通常会因为性能问题而被 ArrayList
或 LinkedList
替代。
Deque
是 Java 中的双端队列(Double-ended Queue)接口,它提供了在队列的两端进行添加和删除操作的能力。LinkedList
实现了 Deque
接口,因此可以用作双向队列。Deque
可以用于模拟栈(先进后出)和队列(先进先出)以及其他需要双向操作的数据结构。Deque
(双端队列)是 Java 中的一种队列数据结构,它可以在队列的两端添加和删除元素,支持先进先出(FIFO)和后进先出(LIFO)等多种操作。Deque
接口有多个实现类,其中最常用的是 ArrayDeque
和 LinkedList
。以下是 Deque
的核心代码讲解,涵盖了初始化、添加元素、删除元素等关键操作。
首先,让我们看一下 Deque
的基本属性和构造方法,以及 ArrayDeque
作为一个实现的示例:
// Deque 接口的定义
public interface Deque<E> extends Queue<E> {
// 添加元素到队列的头部
void addFirst(E e);
// 添加元素到队列的尾部
void addLast(E e);
// 移除并返回队列头部的元素
E removeFirst();
// 移除并返回队列尾部的元素
E removeLast();
// ...
}
// ArrayDeque 类的定义
public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, java.io.Serializable {
// 存储元素的数组
transient Object[] elements;
// 队列的头部索引
transient int head;
// 队列的尾部索引
transient int tail;
// ...
// 构造方法
public ArrayDeque() {
elements = new Object[16];
}
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
// ...
}
在上述代码中,Deque
接口定义了双端队列的基本操作,包括在头部和尾部添加元素、在头部和尾部删除元素等。ArrayDeque
是 Deque
接口的一个实现,使用一个数组 elements
来存储元素。head
和 tail
分别表示队列的头部和尾部索引。
接下来,让我们看一下 Deque
的添加元素操作,包括 addFirst
、addLast
和 offer
方法:
// 添加元素到队列的头部
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
// 添加元素到队列的尾部
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();
}
// 在队列的尾部添加元素,返回是否成功
public boolean offer(E e) {
return addLast(e);
}
addFirst
方法用于添加元素到队列的头部,addLast
方法用于添加元素到队列的尾部,而 offer
方法则是在尾部添加元素并返回是否成功。在添加元素时,如果队列已满,就会触发扩容操作。
接下来,让我们看一下 Deque
的删除元素操作,包括 removeFirst
、removeLast
和 poll
方法:
// 移除并返回队列头部的元素
public E removeFirst() {
E x = pollFirst();
if (x == null)
throw new NoSuchElementException();
return x;
}
// 移除并返回队列尾部的元素
public E removeLast() {
E x = pollLast();
if (x == null)
throw new NoSuchElementException();
return x;
}
// 移除并返回队列头部的元素,返回 null 如果队列为空
public E pollFirst() {
int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// 如果头部元素为空,返回 null
if (result == null)
return null;
// 释放头部元素
elements[h] = null;
head = (h + 1) & (elements.length - 1);
return result;
}
// ...
removeFirst
方法用于移除并返回队列头部的元素,removeLast
方法用于移除并返回队列尾部的元素,而 pollFirst
方法则是移除并返回队列头部的元素,如果队列为空则返回 null
。
这些是 Deque
的核心操作,它们确保了双端队列的高效添加和删除能力。不同的 Deque
实现类可能有不同的性能特性和用途,开发人员可以根据实际需求选择合适的实现类。
CopyOnWriteArrayList
是 Java 中的线程安全的列表实现,它在写操作时进行复制,以保证线程安全。这意味着写操作会创建一个新的底层数组,因此写操作的效率较低。CopyOnWriteArrayList
是 一个线程安全的动态数组实现,它通过在修改操作时创建一个副本(copy)来保证线程安全,因此适用于多线程环境下的读多写少的场景。以下是 CopyOnWriteArrayList
的核心代码讲解,涵盖了初始化、添加元素、删除元素等关键操作。
首先,让我们看一下 CopyOnWriteArrayList
的基本属性和构造方法:
// CopyOnWriteArrayList 类的定义
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 存储元素的数组
private transient volatile Object[] array;
// 构造方法
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
setArray(c.toArray());
}
// ...
}
在上述代码中,CopyOnWriteArrayList
使用一个 Object
类型的数组 array
来存储元素。在默认构造方法中,array
初始化为空数组。在带有 Collection
参数的构造方法中,array
初始化为 Collection
中的元素数组。
接下来,让我们看一下 CopyOnWriteArrayList
的添加元素操作,包括 add
、addAll
和 set
方法:
// 向列表尾部添加元素
public boolean add(E e) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
// 向列表尾部添加元素集合
public boolean addAll(Collection<? extends E> c) {
if (c.isEmpty())
return false;
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + c.size());
int added = 0;
for (E e : c) {
newElements[len + added] = e;
added++;
}
setArray(newElements);
return added > 0;
}
}
// 将指定位置的元素设置为新元素
public E set(int index, E element) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
if (index < 0 || index >= len)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);
Object oldValue = elements[index];
if (oldValue != element) {
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
}
return oldValue;
}
}
上述代码中,add
方法用于向列表尾部添加元素,addAll
方法用于向列表尾部添加元素集合,而 set
方法则是将指定位置的元素设置为新元素。这些添加和修改操作都在同步块内完成,确保了线程安全。
接下来,让我们看一下 CopyOnWriteArrayList
的删除元素操作,包括 remove
和 removeAll
方法:
// 删除指定元素
public boolean remove(Object o) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
int index = indexOf(o, elements, 0, len);
if (index < 0)
return false;
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
setArray(newElements);
}
return true;
}
}
// 删除列表中包含在指定集合中的元素
public boolean removeAll(Collection<?> c) {
if (c.isEmpty())
return false;
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
int newlen = 0;
Object[] tmp = new Object[len];
for (int i = 0; i < len; i++) {
if (!c.contains(elements[i]))
tmp[newlen++] = elements[i];
}
if (newlen == len)
return false;
setArray(Arrays.copyOf(tmp, newlen));
return true;
}
}
remove
方法用于删除指定元素,removeAll
方法用于删除列表中包含在指定集合中的元素。删除操作同样在同步块内完成。
这些是 CopyOnWriteArrayList
的核心操作,它们确保了在多线程环境下的线程安全性。需要注意的是,CopyOnWriteArrayList
的读操作不需要加锁,因此适用于读多写少的场景,但在写入操作较频繁的情况下,性能可能会受到影响。
以下是针对不同的列表实现(ArrayList
、LinkedList
、Vector
、Deque
、CopyOnWriteArrayList
)的使用注意事项:
动态数组:ArrayList
是基于动态数组实现的,因此在添加元素时要注意动态扩容带来的性能开销。如果知道列表的大致大小,可以在初始化时指定初始容量,减少扩容次数。
随机访问快:ArrayList
支持常数时间内的随机访问,但在中间插入或删除元素时会导致后续元素的位移,性能较低。适用于频繁读取但较少修改的场景。
线程安全:ArrayList
不是线程安全的,如果在多线程环境中使用,需要采取额外的同步措施,或者考虑使用线程安全的替代实现。
双向链表:LinkedList
是基于双向链表实现的,支持高效的插入和删除操作,但随机访问性能较差。适用于频繁插入和删除元素的场景。
迭代器:在遍历 LinkedList
时,建议使用迭代器而不是索引。索引访问可能导致遍历性能变得很差。
内存占用:LinkedList
每个节点都需要额外的内存空间存储前后节点的引用,因此在大量数据情况下可能占用较多内存。
线程安全:Vector
是线程安全的,但在多线程环境中,如果需要高性能,可能需要考虑更轻量级的同步机制。
扩容开销:Vector
和 ArrayList
类似,需要进行动态扩容,因此在大量元素插入时,要注意频繁的扩容开销。
性能:由于同步机制,Vector
的性能可能低于 ArrayList
,除非需要线程安全性,否则不建议使用。
双端队列:Deque
支持在队列头部和尾部进行高效的添加和删除操作,是一种灵活的数据结构。
多种实现:Java 提供了多种 Deque
的实现,如 ArrayDeque
和 LinkedList
,可以根据具体需求选择合适的实现。
线程安全:Deque
不是线程安全的,如果需要在多线程环境中使用,需要采取适当的同步措施。
线程安全:CopyOnWriteArrayList
是线程安全的,适用于读多写少的场景。在写入操作较频繁的情况下,性能可能不如其他非线程安全的列表。
内存占用:由于写操作会复制整个数组,因此可能会消耗较多的内存。不适合存储大量数据。
不支持修改操作:在 CopyOnWriteArrayList
上执行修改操作(如 add
、remove
)会抛出 UnsupportedOperationException
异常。只能通过复制一个新的列表来进行修改。
根据实际需求和性能要求,选择合适的列表实现非常重要。不同的列表实现有不同的适用场景和性能特点,需要根据具体情况进行选择。
例如电商场景下,可以根据不同的业务需求选择合适的数据结构,以下是一些示例:
ArrayList:
ArrayList
存储商品信息,以支持快速的随机访问和排序操作。LinkedList:
LinkedList
可以更高效地执行这些操作,同时支持迭代浏览购物车中的商品。Vector:
Vector
,以保持与原有代码的兼容性。Deque:
Deque
可以在队列的头部和尾部进行快速插入和删除。CopyOnWriteArrayList:
CopyOnWriteArrayList
来保证多线程环境下的读取一致性,以防止商品库存数据不一致的问题。需要注意的是,以上示例仅为参考,实际的业务场景可能更加复杂,需要根据具体情况权衡性能、并发性和代码复杂性,选择合适的数据结构。不同的业务模块可能会使用不同的数据结构来满足其需求。
这些数据类型和类提供了不同的性能和线程安全特性,我们可以根据具体的需求选择合适的数据结构。链表适用于需要高效插入和删除操作的情况,而动态数组(如 ArrayList
)适用于需要快速随机访问的情况。同时,双端队列和线程安全的实现可以满足特定的多线程需求。
好的,本期我们的链表数据类型的讲解就到此结束。链表是一种非常重要的数据结构,它为我们提供了灵活性和效率,特别是在插入和删除元素时。希望您能够充分理解链表的概念和操作,以便在编程中更好地应用它们。
在下期,我们将继续深入探讨栈(Stack)数据结构。栈是一种基本的数据结构,它遵循后进先出(Last-In-First-Out,LIFO)的原则,常用于解决许多计算机科学和编程问题。我们将详细介绍栈的工作原理、常见操作以及如何在实际编程中使用它。
谢谢您的关注,下期敬请期待!如果在本期的内容中发现了错误或异常,或者您有任何疑问或建议,请随时留言或私信与我联系。您的反馈对于改进和完善内容非常重要,我将非常感激您的指正。期待与您继续分享有关编程和计算机科学的知识,祝您一切顺利!