LinkedList是Java中的一种双向链表数据结构实现类,它实现了List
和Deque
接口。
LinkedList的特点主要包括以下几点:
Deque
接口的实现类,LinkedList可以被用作队列(先进先出)和栈(后进先出)的数据结构,可以使用addFirst、removeFirst
等方法模拟栈操作,使用addLast、removeFirst
等方法模拟队列操作。总体而言,LinkedList适用于频繁地插入和删除元素的场景,但对于随机访问元素的需求不是很高时,可以考虑使用它。
优点:
缺点:
综上所述,LinkedList 适合在需要频繁进行插入和删除操作,但是不需要频繁随机访问元素的场景下使用。如果需要随机访问或者占用空间比较重要时,可以考虑使用其他数据结构,比如 ArrayList
。
在Java中,LinkedList的实现是通过一个双向链表来实现的。在LinkedList中,每个节点都有一个指向前一个节点和一个指向后一个节点的引用。
在LinkedList中,Node节点结构体可以被定义如下:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(E element, Node<E> next, Node<E> prev) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
上面的代码定义了一个私有静态内部类Node,该类包含了一个泛型元素item
,以及两个Node
类型的引用next
和prev
,分别表示指向下一个节点和上一个节点的引用。此外,Node类还有一个带有三个参数的构造函数,用于初始化节点的prev、item
和next
属性。
通过这样的设计,LinkedList可以通过Node
节点来构建双向链表结构,实现了在任意位置进行节点的插入和删除操作。
Java中的LinkedList实现了双向链表是因为双向链表具有以下优点:
O(1)
时间复杂度的前驱和后继节点访问操作,而单向链表则需要O(n)
时间复杂度的遍历操作才能找到前驱节点。因此,双向链表在实现一些需要频繁插入和删除操作的场景下,比单向链表更加适用。Java中的LinkedList就是基于双向链表实现的,所以它能够高效地支持插入、删除等操作,同时也提供了迭代器和列表迭代器等许多方便的操作。
在Java中,LinkedList类实现了链表的基本操作,包括增加(添加)、删除、修改和查询等功能。下面是一些常用的方法:
LinkedList类还提供了其他一些方法用于获取链表的大小、清空链表、判断链表是否为空等。
在Java中,你可以使用以下几种方式对LinkedList进行遍历:
1.使用迭代器(Iterator)进行遍历:
LinkedList<String> linkedList = new LinkedList<>();
// 假设已经向linkedList中添加了元素
Iterator<String> iterator = linkedList.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
// 对element进行处理
}
2.使用增强型for循环(foreach)进行遍历:
LinkedList<String> linkedList = new LinkedList<>();
// 假设已经向linkedList中添加了元素
for (String element : linkedList) {
// 对element进行处理
}
这两种方式都可以用来遍历LinkedList中的元素,你可以根据实际情况选择其中一种来进行遍历操作。
/**
* 默认构造函数,它不带任何参数,用来创建一个空的 LinkedList 对象
*/
public LinkedList() {
}
/**
* 带有参数的构造函数,它接受一个类型为 Collection 的参数 c
*/
public LinkedList(Collection<? extends E> c) {
// 调用无参构造函数
this();
// 将参数 c 中的所有元素添加到新创建的 LinkedList 对象中
addAll(c);
}
/**
* LinkedList 类中的 addAll 方法的定义
*/
public boolean addAll(Collection<? extends E> c) {
// size表示为当前 LinkedList 中的元素数量
// 调用另一个名为 addAll 的方法来将参数 c 中的所有元素添加到 LinkedList 对象中
return addAll(size, c);
}
/**
* 用于将指定集合中的元素插入到指定位置
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 检查索引的有效性,确保索引在范围内
checkPositionIndex(index);
// 将集合 c 转换为数组,并将其赋值给对象数组 a
Object[] a = c.toArray();
// 获取新添加元素的数量
int numNew = a.length;
// 如果新添加元素的数量为 0,则直接返回 false
if (numNew == 0)
return false;
// 定义两个节点,pred 表示当前节点的前一个节点,succ 表示当前节点的后一个节点
LinkedList.Node<E> pred, succ;
// 根据插入位置确定 succ 和 pred 的值
// 如果插入位置在末尾,则将 succ 设置为 null,将 pred 设置为最后一个节点
if (index == size) {
succ = null;
pred = last;
} else {
// 否则,通过 node(index) 方法找到指定位置的节点,并将其设置为 succ,同时将其前一个节点设置为 pred
succ = node(index);
pred = succ.prev;
}
// 遍历数组 a 中的元素
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 将每个元素转换为泛型类型 E,并创建一个新的节点 newNode,该节点的前一个节点为 pred,值为 e,后一个节点为 null
LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, null);
// 如果 pred 为 null,则将 newNode 设置为链表的第一个节点
if (pred == null)
first = newNode;
else
// 否则,将 pred 的下一个节点设置为 newNode
pred.next = newNode;
// 将 pred 更新为 newNode
pred = newNode;
}
// 根据 succ 是否为 null 来确定插入后的链表结构
// 如果 succ 为 null,则表示插入位置在末尾,将 pred 设置为最后一个节点
if (succ == null) {
last = pred;
} else {
// 否则,将 pred 的下一个节点设置为 succ,并将 succ 的前一个节点设置为 pred
pred.next = succ;
succ.prev = pred;
}
// 更新链表的大小和修改计数器
size += numNew;
modCount++;
// 返回 true,表示添加操作成功完成
return true;
}
/**
* 用于向链表末尾添加一个元素 e
*/
public boolean add(E e) {
// 将元素 e 添加到链表的末尾
linkLast(e);
// 添加操作成功
return true;
}
/**
* 向链表末尾添加一个元素的操作
*/
void linkLast(E e) {
// 创建一个名为 l 的局部变量,用于保存当前链表的最后一个节点的引用,通过访问 last 字段获取最后一个节点的引用
final LinkedList.Node<E> l = last;
// 创建一个新的节点 newNode,并将其初始化为一个具有前驱节点为 l、元素为 e、后继节点为 null 的节点
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
// 将链表的 last 指针指向新的节点 newNode,使其成为最后一个节点
last = newNode;
// 如果 l 为空(即链表为空),则将链表的 first 指针指向新的节点 newNode,否则将 l 节点的后继节点指向新的节点 newNode
if (l == null)
first = newNode;
else
l.next = newNode;
// 增加链表的大小,将链表中元素的数量加1
size++;
// 增加修改计数器 modCount 的值,用于追踪链表结构的修改次数
modCount++;
}
/**
* 用于从 LinkedList 中删除指定的元素
*/
public boolean remove(Object o) {
// 判断传入的参数 o 是否为 null
// 如果 o 为 null,则说明要删除的元素是 null 值
if (o == null) {
// 因此需要遍历 LinkedList 中的所有节点并查找 item 属性为 null 的节点
// 循环体中的变量 x 表示当前正在遍历的节点,初始化为 first(即头节点),在每次迭代后更新为下一个节点,直到遍历完整个 LinkedList
for (LinkedList.Node<E> x = first; x != null; x = x.next) {
// 如果当前节点的 item 属性为 null(在 o 为 null 的情况下),则调用辅助方法 unlink 删除该节点并返回 true
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 否则需要遍历所有节点并查找 item 属性等于 o 的节点
for (LinkedList.Node<E> x = first; x != null; x = x.next) {
// 如果当前节点的 item 属性等于 o(在 o 不为 null 的情况下),则调用辅助方法 unlink 删除该节点并返回 true
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
/**
* 这是一个辅助方法,用于删除指定节点 x
*/
E unlink(LinkedList.Node<E> x) {
// assert x != null;
// 保存被删除节点的元素值
final E element = x.item;
// 存储被删除节点的前驱和后继节点
final LinkedList.Node<E> next = x.next;
final LinkedList.Node<E> prev = x.prev;
// 如果被删除节点是头节点,则将头指针指向其后继节点
if (prev == null) {
first = next;
} else {
// 否则更新被删除节点的前驱节点的 next 属性为其后继节点,并将被删除节点的 prev 属性设为 null
prev.next = next;
x.prev = null;
}
// 如果被删除节点是尾节点,则将尾指针指向其前驱节点
if (next == null) {
last = prev;
} else {
// 否则更新被删除节点的后继节点的 prev 属性为其前驱节点,并将被删除节点的 next 属性设为 null
next.prev = prev;
x.next = null;
}
// 将被删除节点的 item 属性设为 null
x.item = null;
// 更新 LinkedList 的元素数量和修改次数
size--;
modCount++;
// 返回被删除节点的元素值
return element;
}
/**
* 用于获取指定索引处的元素
*/
public E get(int index) {
// 检查索引的有效性,然后调用另一个私有辅助方法 node 来获取对应索引处的节点
checkElementIndex(index);
// 返回该节点的元素值
return node(index).item;
}
/**
* 检查指定索引是否在有效范围内
*/
private void checkElementIndex(int index) {
// 调用 isElementIndex 方法来判断索引是否在有效范围内
if (!isElementIndex(index))
// 如果不在,则抛出 IndexOutOfBoundsException 异常,异常消息由 outOfBoundsMsg 方法返回
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 判断指定索引是否在有效范围内
*/
private boolean isElementIndex(int index) {
// 如果索引大于等于 0 并且小于 size,则返回 true;否则返回 false
return index >= 0 && index < size;
}
/**
* 用于将指定索引(index)位置的元素替换为新的元素,并返回被替换掉的旧元素
*/
public E set(int index, E element) {
// 用于将指定索引(index)位置的元素替换为新的元素,并返回被替换掉的旧元素
checkElementIndex(index);
// 获取指定索引位置上的节点
LinkedList.Node<E> x = node(index);
// 将指定索引位置上的节点的元素值赋给变量 oldVal,即记录旧元素的值
E oldVal = x.item;
// 将指定索引位置上的节点的元素值替换为新的元素值 element
x.item = element;
// 返回被替换掉的旧元素值
return oldVal;
}
/**
* 检查指定索引是否在有效范围内
*/
private void checkElementIndex(int index) {
// 调用 isElementIndex 方法来判断索引是否在有效范围内
if (!isElementIndex(index))
// 如果不在,则抛出 IndexOutOfBoundsException 异常,异常消息由 outOfBoundsMsg 方法返回
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 判断指定索引是否在有效范围内
*/
private boolean isElementIndex(int index) {
// 如果索引大于等于 0 并且小于 size,则返回 true;否则返回 false
return index >= 0 && index < size;
}
/**
* 用于清空整个链表的内容,并释放相关的内存空间
*/
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// 解释了清除所有节点之间链接的操作虽然“不必要”,但有两个原因:
// - helps a generational GC if the discarded nodes inhabit
// 帮助分代垃圾回收(generational GC)
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
// 和确保释放内存即使存在可达的迭代器
// 循环体中的变量 x 初始化为头节点 first,然后在每次迭代中更新为下一个节点,直到遍历完整个链表
for (LinkedList.Node<E> x = first; x != null; ) {
// 用于保存当前节点 x 的后继节点的引用,以防止在清空当前节点之后丢失对后继节点的引用
LinkedList.Node<E> next = x.next;
// 将当前节点 x 的元素值、前驱节点和后继节点都设置为 null,从而切断当前节点与前后节点的关联
x.item = null;
x.next = null;
x.prev = null;
// 将 x 更新为下一个节点,以便进行下一次循环迭代
x = next;
}
// 清空头节点和尾节点的引用,使整个链表为空
first = last = null;
// 将链表的大小设置为 0,表示链表中不再包含任何元素
size = 0;
// 更新修改次数计数器,用于在迭代过程中检测并发修改
modCount++;
}
/**
* 用于查找指定元素在链表中第一次出现的位置(索引)
*/
public int indexOf(Object o) {
// 初始化索引变量为 0,表示从头节点开始查找
int index = 0;
// 如果要查找的元素 o 是 null,则执行下面的 for 循环,按顺序遍历链表,查找第一个元素值为 null 的节点
if (o == null) {
// 变量 x 初始化为头节点 first,然后在每次迭代中更新为下一个节点,直到遍历完整个链表
for (LinkedList.Node<E> x = first; x != null; x = x.next) {
// 判断当前节点 x 的元素值是否为 null,如果是,则说明找到了要查找的元素,返回当前索引 index 表示该元素在链表中的位置
if (x.item == null)
return index;
// 如果当前节点不是要查找的元素,则将索引变量 index 加 1,继续向下查找
index++;
}
} else {
// 如果要查找的元素 o 不是 null,则执行下面的 for 循环,按顺序遍历链表,查找第一个元素值与 o 相等的节点
for (LinkedList.Node<E> x = first; x != null; x = x.next) {
// 判断当前节点 x 的元素值是否与要查找的元素 o 相等,如果是,则说明找到了要查找的元素,返回当前索引 index 表示该元素在链表中的位置
if (o.equals(x.item))
return index;
// 如果当前节点不是要查找的元素,则将索引变量 index 加 1,继续向下查找
index++;
}
}
// 如果整个链表都遍历完了还没有找到要查找的元素,则返回 -1,表示该元素不存在于链表中
return -1;
}
LinkedList在Java中适用于以下场景:
O(1)
,因此适合在需要频繁执行这些操作的场景下使用。Queue
和Deque
接口,可以作为队列或双端队列来使用。需要注意的是,在某些特定的场景下,可能需要根据具体的需求进行性能测试和选择,以确定使用LinkedList是否能够带来性能上的提升。
LinkedList和ArrayList是Java中两种常见的集合实现类,它们具有一些不同的特点和适用场景。
LinkedList的特点:
ArrayList的特点:
综上所述,选择LinkedList还是ArrayList取决于具体的使用场景和需求:
需要注意的是,在多线程环境下,LinkedList和ArrayList都不是线程安全的,如果需要在多线程环境下使用,需要进行适当的同步处理或使用线程安全的集合类。
在使用 Java 中的 LinkedList 时,有一些需要注意的事项,包括但不限于以下几点:
综上所述,使用 LinkedList 时需要根据具体的场景和需求进行权衡,特别是在涉及到插入、删除、遍历和空间占用等方面需要特别留意。
LRU(Least Recently Used)
算法是一种缓存置换策略,它根据数据最近被访问的时间来决定哪些数据会被保留,哪些数据会被淘汰。在 Java 中,可以通过 LinkedList
和 HashMap
来实现 LRU 缓存算法。
具体实现步骤如下:
1.定义一个双向链表和一个哈希表,用于存储缓存数据和快速定位数据。
private class CacheNode {
private String key;
private Object value;
private CacheNode prev;
private CacheNode next;
public CacheNode(String key, Object value) {
this.key = key;
this.value = value;
}
}
private Map<String, CacheNode> cacheMap;
private CacheNode head;
private CacheNode tail;
private int capacity;
2.在构造函数中初始化双向链表和哈希表,并设置缓存容量。
public LRUCache(int capacity) {
this.capacity = capacity;
cacheMap = new HashMap<>(capacity);
head = new CacheNode(null, null);
tail = new CacheNode(null, null);
head.next = tail;
tail.prev = head;
}
3.实现 get 方法,每次获取数据时,将数据移到链表头部,并更新哈希表中的位置信息。
public Object get(String key) {
CacheNode node = cacheMap.get(key);
if (node == null) {
return null;
}
// 将节点移动到链表头部
moveToHead(node);
return node.value;
}
private void moveToHead(CacheNode node) {
// 先将节点从原有位置删除
removeNode(node);
// 将节点插入到链表头部
insertNodeAtHead(node);
}
private void removeNode(CacheNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void insertNodeAtHead(CacheNode node) {
node.next = head.next;
node.next.prev = node;
head.next = node;
node.prev = head;
}
4.实现 put 方法,每次存储数据时,如果缓存已满,则淘汰链表尾部的数据,并在哈希表中删除相应的位置信息。
public void put(String key, Object value) {
CacheNode node = cacheMap.get(key);
if (node != null) {
// 如果键已经存在,则更新值,并移到链表头部
node.value = value;
moveToHead(node);
} else {
// 如果键不存在,则插入新节点到链表头部,并添加位置信息到哈希表
node = new CacheNode(key, value);
cacheMap.put(key, node);
insertNodeAtHead(node);
if (cacheMap.size() > capacity) {
// 如果缓存已满,则淘汰链表尾部的节点,并删除相应位置信息
CacheNode removedNode = removeNodeAtTail();
cacheMap.remove(removedNode.key);
}
}
}
private CacheNode removeNodeAtTail() {
CacheNode removedNode = tail.prev;
removeNode(removedNode);
return removedNode;
}
这样,一个基于 LinkedList 的 LRU 缓存算法就实现了。
可以通过如下代码进行简单的测试:
LRUCache cache = new LRUCache(2);
cache.put("A", 1);
cache.put("B", 2);
System.out.println(cache.get("A")); // 输出 1
cache.put("C", 3);
System.out.println(cache.get("B")); // 输出 null
盈若安好,便是晴天