LinkeList是基于双向链表的实现,同时它也实现了Queue队列,Deque栈的实现,使得我们可以进行双向队列的实现,上一节java集合之ArrayList解析中说过ArrayList查询效率比较高,添加和删除集合中的元素效率比较低,而LinkList正好相反.
LinkList组织结构图
LinkList继承自AbstractSequentialList(ArrayList继承AbstractList)同时实现了Serializable和Cloneable标记接口,相比于ArrayList缺少了RandomAccess,说明进行随机访问效率比较低,多了Deque及Queue可以进行栈和队列的操作.
队列Queue
public interface Queue<E> extends Collection<E> {
boolean add(E var1);
boolean offer(E var1);
E remove();
E poll();
E element();
E peek();
}
LinkList实现了队列Queue,Queue扩展了Collection,队列的特点是先进先出,在尾部添加数据,在头部删除数据.
- 使用add,offer在尾部添加数据
- 使用remove,poll获取头部数据,并删除头部数据,会改变队列结构
- 使用element,peek仅仅是获取头部数据,不会改变队列结构
每种操作都有两种形式,它们的区别是什么?区别就在于针对队列特殊情况的处理上,所谓队列的特殊情况即队列满和队列空.
当队列满的时候执行add操作会抛出IllegalStateException,而offer只会返回false.
当队列空的时候执行remove和element会抛出NoSuchElementException,而poll和peek会返回null。
LinkList队列没有长度的限制,其他的特定队列是有长度的。
LinkList作为队列使用:
Queue<Integer> queue=new LinkedList<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
while (queue.peek()!=null){
System.out.println(queue.poll());//输出1>2>3
}
}
Deque双端队列
栈和队列都可以进行双端操作,只是栈只能操作头部,而队列可以操作头部和尾部,在尾部添加数据,在头部删除数据.
LinkList实现了栈,栈在开发中很常见,栈的特点是先进后出.Deque继承了队列Queue,它同样是个接口。
public interface Deque<E> extends Queue<E> {
boolean add(E var1);
boolean offer(E var1);
E remove();
E poll();
E element();
E peek();
void push(E var1);
E pop();
int size();
Iterator<E> iterator();
Iterator<E> descendingIterator();
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
boolean offerFirst(E e);
boolean offerLast(E e);
E peekFirst();
E peekLast();
E pollFirst();
E pollLast();
E removeFirst();
E removeLast();
}
Deque栈扩展了队列,实现入栈和出栈操作。
push表示入栈,在头部添加数据,如果这时栈满,报IllegalStateException异常.
pop表示出栈,返回头部数据,并且从栈中删除,如果栈是空的,报NoSuchElementException异常
peek表示获取栈中头部数据,并返回头部数据,不改变栈结构,若栈是空的,就返回null.
xxxFirst操作头部,xxxLast操作尾部。与队列类似,每种操作有两种形式,区别也是在队列为空或满时,处理不同。为空时,getXXX/removeXXX会抛出异常,而peekXXX/pollXXX会返回null。队列满时,addXXX会抛出异常,offerXXX只是返回false。
栈和队列只是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过,使用不同的名称和方法,概念上更为清晰
Iterator descendingIterator();迭代器方法,可以从后往前遍历
LinkList作为栈使用
Deque<Integer> deque=new LinkedList<>();
deque.push(1);
deque.push(2);
deque.push(3);
while (deque.peek()!=null){
System.out.println(deque.pop());//输出3>2>1
}
在java中Stack类也用于表示栈,具体的实现细节大家可以自己看看源码。LinkedList的用法是比较简单的,与ArrayList用法类似,支持List接口,只是,LinkedList增加了一个接口Deque,可以把它看做队列、栈、双端队列,方便的在两端进行操作。
LinkList源码分析:
ArrayList内部的实现是基于动态数组,数据元素在内存中是连续存放的.而LinkList是基于双向链表实现的,在物理上链表中的数据元素在内存中的形态是非连续的,它是靠链表的前后节点Node指向进行关联的,下面是双链表的结构图.
LinkList中的内部类Node
Node类表示节点,item表示当前的数据元素,prev指向前一个节点,next指向下一个节点.
First节点也叫头节点,它的prev是null
Last节点又叫尾节点,它的next是null.
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkList中的成员变量,分别代表如下含义:
size表示链表的长度即数据元素的个数
first代表头节点,last代表尾节点,默认都是null
LinkList的所有操作都是围绕上面这三个成员变量展开的。
transient int size;
transient LinkedList.Node<E> first;
transient LinkedList.Node<E> last;
LinkList的add操作
public boolean add(E e) {
this.linkLast(e);
return true;
}
(1)在执行add操作的时候调用了添加数据元素e到链表尾部的方法linkLast,并返回true.
void linkLast(E e) {
final Node l = last;
final Node newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
(2)得到当前链表的尾节点并赋值给节点l
(3)创建一个新节点newNode,将节点l设置为它的前驱节点,数据元素e为其数据,null为后继节点。
(4)将newNode节点设置为last节点.
(5)如果l为null,表明当前列表是空链表,这时设置头节点first为newNode否则执行第6步。
(6)设置头节点的next节点为newNode
(7)让size加1,modCount加1,modCount的作用代表修改次数与ArrayList相同。
LinkList的remove操作
public E remove(int index) {
this.checkElementIndex(index);
return this.unlink(this.node(index));
}
(1)首先调用checkElementIndex方法检查索引是不是合法,如果不合法抛出IndexOutOfBoundsException异常,否则执行第2步。
(2)调用unlink(node(index))方法,其中node(index)表示根据索引查询链表中的node元素.
Node node(int index) {
if (index < (size >> 1)) {
Node x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
(3)如果索引index
E unlink(Node x) {
final E element = x.item;
final Node next = x.next;
final Node prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
(5)删除node节点x的基本思路是就是把x节点的前驱节点与后继节点直接关联起来,然后修改x.item=null以及size进行减1,modCount加1操作。
至此LinkList的add与remove操作源码分析完了,其他的get/size等方法也就可以很容易的看懂了,由于时间的关系这里不再赘述了。
LinkList的总结
- LinkedList内部是用双向链表实现的,维护了长度、头节点和尾节点
- 按需分配空间,不需要预先分配很多空间
- 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2).
- 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
- 在两端添加、删除元素的效率很高,为O(1)。
- 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。