容器--LinkedList
一、前言
上一篇我们介绍了List的重要实现之一ArrayList, 在大多数情况下,我们写代码时会直接使用到ArrayList,因为其在随机访问的优势是其它List无法比拟的。除了ArrayList之外, JDK还提供了一种List实现,即LinkedList, 这个是基于链表来实现的,虽然我们平时用的不多,但它却天生是一个功能完备的双向队列,也可以直接当作stack来使用, 所以有些时候还是能有其用武之地的。
本文将对这个类的实现机制进行介绍。
二、实现原理
和ArrayList类似,LinkedList里的大部分方法都比较简单,所以我们没有必要每个方法都分析,而是重点介绍一些能体现其实现机制的内容。
1)数据如何存储?
LinkedList使用的是链表的数据结构进行存储,在数据结构中我们知道,链表结构不同于数组,其在物理上的存储空间是不连续的,每个节点表示一个数据对象,节点与节点之间通过引用变量进行关联,一般分为单向链表和双向链表。LinkedList采用的是双向链表的方式。具体来说,LinkedList通过以下字段或类来进行构造。
size: 这个表示LinkedList里数组的个数,初始为0
first:链表的表头元素,初始为null, Node<E> 类型
last: 表尾元素,初始为null, Node<E> 类型
Node:用于表示节点的内部类,定义如下:
private static class Node<E> {
E item; //元素
Node<E> next; //后继
Node<E> prev; //前驱
//三个参数的顺序,前驱,元素,后继
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
可见每个节点都有前驱和后继,这样就构造成了一个双向链表,注意这个链表并不是闭环的,也就是first和last之间没有引用关系。那么针对链表的所有操作,实际上都可以理解为是对部分节点(或所有)的遍历及前后引用的修改操作。理解了链表的操作,也就容易理解LinkedList了。
和ArrayList不一样,在新增元素的时候,LinkedList并不需要扩容。
2)如何实现get(index)?
对于ArrayList来说,实现get(index)的时间复杂度为O(1),而LinkedList则需要O(n),因为需要对链表进行遍历,不过由于是双向链表,所以在实现时做了点小小的优化,即先判断index是在前半部分,还是后半部分,以决定是从表头遍历还是表尾遍历。实现如下:
Node<E> node(int index) {
// assert isElementIndex(index);
//判断 index 在 整个列表中的位置来决定是从头遍历还是从尾遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
因为每个元素都是有其前后的指针的,所以,只要能定位到某个位置的元素,再在该位置做任何操作都比较方便了。所以,在LinkedList对于所有按index来操作的方法(比如add,remove,set,get等),都是先通过node方法来定位到元素,然后再做相应的操作。
3)如何实现对队列的支持?
LinkedList的一个重要特点就是实现了双向队列的接口(Deque), 当然Deque继承自Queue,所以单向队列自然也是支持,另外还提供了实现一个栈所需要的方法,比如push, pop,所以也相当于是实现了栈的功能。根据队列和栈的特点,我们可以认为它们是简化以及具有一些设计限制的链表,所以可以直接把链表作为队列和栈来使用。
当然,用ArrayList也能实现栈和队列,但随着队列的出队,入队,栈的出栈,入栈,ArrayList底层的数组大小可能会不断增长,而可能大部分空间都没有利用到,这需要一种更巧妙的实现方式去处理,总的来说较为复杂。
但由于队列和栈只会在链表的两端进行操作,不涉及到元素的遍历,这刚好避开了链表的不足,而利用到了链表的优势,所以,我们一般用链表来实现这样的数据结构。
对于LinkedList来说,无论是在表头还是表尾操作都是非常简单的事,仅举一例如下:
public boolean offer(E e) {
return add(e); //队尾入队
}
public boolean add(E e) {
linkLast(e); // 将元素添加到列表最后
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
上面的逻辑中惟一需要注意的就是我们要正确地将newNode关联到链表中,以及更新表尾的引用。
另外,在JDK中对于Queue的定义中,实际上存取元素都分为多个版本,抛异常和不抛出异常,有的时候容易将其混得不清楚,这里对比如下:
| 方法功能 | 异常版本 | 非异常版本 | 说明 |
| 队尾入队 | add | offer |
仅对于有容量限制的队列来说,如果队列已满,则执行add会抛出异常, 而offer直接返回false |
| 队头出队 | remove | poll | 当队列为空时,remove抛出异常,而poll返回null |
| 获取队尾元素(不出队) | element | peek | 当队列为空时,element抛出异常,而peek返回null |
刚好我们举了offer的例子,上面可以看到offer的实现和add是完全一样的,这是因为LinkedList并没有对容量的大小做出限制,这种情况下,这两者的实现方式是一样的。
异常版本也不难记,它们的首字母刚好组成单词(are),后续当我们学习到阻塞队列时,这几个方法还有对应的阻塞版本。
三、总结
对于链表来说,我们重点要记住的,就是其对于链表结构的定义,以及其对于节点操作的主要步骤,另外就是要了解其是如何实现队列的功能的。
容器--LinkedList的更多相关文章
- List容器——LinkedList及常用API,实现栈和队列
LinkedList及常用API ① LinkedList----链表 ② LinkedList类扩展AbstractSequentialList并实现List接口 ③ LinkedLis ...
- List容器-LinkedList链表
LinkedList--链表 特点: 删除,增加 用LinkedList性能高 层次查找不适合 查询用ArrayList 数组下标查找 插入和删除慢缺点是要做移位操作 总结:Link ...
- 容器LinkedList原理(学习)
一.概述 数据结构和ArrayList有本质不同,LinkedList 是基于链表实现,它的插入和删除操作比 ArrayList 更加高效,基于链表的,所以随机访问的效率要比 ArrayList 差. ...
- java-生产者消费者模式
经常会有公司叫我们手撕代码,比如网易,阿里,那我们是不是该掌握下呢.下面这段代码来自<现代操作系统>进程与线程P49页. public class ProducerConsumer { p ...
- ch11 持有对象
Java集合的基本类型:List.Set.Queue.Map 使用容器时若未指定泛型参数ArrayList apples=new ArrayList();,则容器中所有元素都为Object类型,使用时 ...
- 单线程和多线程处理1W条数据对比代码
package study.interview; import java.util.ArrayList; import java.util.HashMap; import java.util.Link ...
- JAVA基础--IO输入输出(File使用)17
一. File中的方法 1. 列举方法 /* * 获取指定目录以及子目录下的所有文件和文件夹 */ public class ListFilesDemo { public static void m ...
- 【Java中级】(四)多线程
线程的概念 进程和线程的主要差别在于它们是不同的操作系统资源管理方式.进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径.线程有自己的堆栈和局 ...
- java中常见的内存泄露的例子
JAVA 中的内存泄露 Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露. Java中的内存泄露与C++中的表现有所不同. 在C++ ...
随机推荐
- Atitit 知识图谱解决方案:提供完整知识体系架构的搜索与知识结果overview
Atitit 知识图谱解决方案:提供完整知识体系架构的搜索与知识结果overview 知识图谱的表示和在搜索中的展1 提升Google搜索效果3 1.找到最想要的信息.3 2.提供最全面的摘要.4 ...
- AIX下如何根据端口号查找相应的进程
1. $ netstat -Aan |grep 8080 f1000e0002321bb8 tcp 0 0 *.8080 *.* LISTEN 2. $ rmsock f1000e0002321bb8 ...
- JavaScript禁用页面刷新
JavaScript禁用页面刷新代码如下: //禁用F5刷新 document.onkeydown = function () { if (event.keyCode == 116) { event. ...
- 设置国内的centos 7 yum源
本人在帝都,觉得首都在线(yun-idc.com)的yum源速度不错,故选它了.具体操作方法是将以下文件保存到 /etc/yum.repos.d/yun-idc.repo 即可. [base] nam ...
- 百度地图JavaScript API覆盖物旋转时出现偏移
在项目中,调用百度地图JavaScript API,做覆盖物的旋转再添加到地图上,结果出现偏移了. 调试过程中的效果图: 发现图片的旋转并不是按车子的中心来的,而是之外的一个点.最后发现犯了一个很细节 ...
- Android自定义EditText去除边框并添加下划线
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo ...
- .NET 三层架构的简单规划
今天心血来潮简单看了下petshop4.0的源代码,他就是用三层架构来实现的.现在简单的做下总结. 首先我们先看下petshop的三层架构. 1 WEB 表示层 2 Model 业务实体 3 BLL ...
- 如何解决Android SDK中离线文档打开慢的问题
原文:http://blog.csdn.net/hansel/article/details/39268511 Android SDK中的离线文档虽然都是本地文件,但是有很多Javascript, C ...
- 实现在GET请求下调用WCF服务时传递对象(复合类型)参数
WCF实现RESETFUL架构很容易,说白了,就是使WCF能够响应HTTP请求并返回所需的资源,如果有人不知道如何实现WCF支持HTTP请求的,可参见我之前的文章<实现jquery.ajax及原 ...
- Emit学习(4) - Dapper解析之数据对象映射(一)
感觉好久没有写博客了, 这几天有点小忙, 接下来会更忙, 索性就先写一篇吧. 后面估计会有更长的一段时间不会更新博客了. 废话不多说, 先上菜. 一.示例 1. 先建类, 类的名称与读取的表名并没有什 ...