LinkedList学习笔记

本博客转自微信公众号:老马说编程;在学习过程中手敲一遍增强记忆

与ArrayList简单比较:

ArrayList 随机访问效率很高,但是插入和删除性能比较低;LinkedList同样实现List接口,它的特点与ArrayList正好相反;
除了实现List接口外,LinkedList还实现了Deque和Queue接口,可以按照队、栈和双端队列的方式进行操作。

用法

构造方法:

  1. 默认构造方法:
public LinkedList()
  1. 接受一个已有Collection
public LinkedList(Collection<? extends E> c)

两种构造方法使用实例:

List<String> list = new LinkedList();
List<String> list2 = new LikedList<>(Arrays.asList(new String[]{"a","b","c"}));

Queue(队列):

前面说到LinkedList实现了队列接口Queue,所谓队列,就跟排队一样,先进先出,在尾部添加元素。

Queue扩展了Collection,它主要有三个操作:
1. 在尾部添加元素:add、offer
2. 查看头部元素:element、peek–返回头部元素,但不改变队列;
3. 删除头部元素:remove、poll–返回头部元素,且从队列中已移除

每个操作都有两个方法,他们的区别是:在队列为空时,element和remove会抛出NoSuchElementException,而peek和poll返回null;在队列为满时,add会抛出异常IllegalStateException,而offer返回false;
简单示例:

Queue<String> queue = new LinkedList<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
while(queue.peek()!=null){
    System.out.println(queue.poll());
    }

输出:

a
b
c

Deque(双端队列):

栈也是一种常用的数据结构,与队列相反,它的特点是先进后出,类似储物箱,放的时候一件一件往上放,拿的时候则只能从上面开始拿。java没有单独的栈接口,栈相关方法包括在了表示双端队列的Deque接口中,主要由是三个方法:

/**
  * 入栈,在头部添加元素,栈的空间可能是有限的,如果满了,push会抛出IllegalStateException
  */
void push(E e)

/**
  * 出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出:NoSuchElementException
  * /
E pop();
/**
  * 查看栈头袁术,不修改栈,如果栈为空,返回null
  * /
E peek();

LinkedList作为栈使用:

扫描二维码关注公众号,回复: 3033757 查看本文章
Deque<String> stack = new LinkedList<>();
stack.push("a");
stack.push("b");
stack.push("C");
while(stack.peek()!=null){
    System.out.println(stack.pop());
    }

输出:

c
b
a
  • java中有一个类Stack,单词意识是:栈,它也实现了栈的一些方法:push、pop、peek等,但它没有实现Deque接口,它是Vector的子类,它增加的这些方法也通过synchronized实现了线程安全。不需要线程安全的情况下,推荐使用LinkedList或ArrayDeque。
  • 栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但尾部只添加、头部只查看和删除。有一个更为通用的操作两端的接口Deque。Deque扩展了 Queue,包括了栈的操作方法,此外,他要有如下更为明确的操作两端的方法:
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 removeLastt();

getxxx/removexxx会抛出异常,peekxxx/pollxxxz会返回null,队列满时,addxxx会抛出异常,offerxxx返回false。

栈和队列 只是 双端队列(Deque) 的特殊情况,它们的方法都可以使用双端队列的方法替代,不过,使用不同的名称和方法,概念上更为清晰。

Deque接口还有一个迭代器方法,可以从后往前遍历:
Iterator<E> descendingIterator();

举个栗子:

Deque<String> deque = new LinkList<>(Arrays.asList(new String[]{"a","b","c"}));
Iterator<String> it = deque.descendingIterator();
while(it.hasNext()){
    System.out.print( it.next() + " ");
}

输出为:
c b a
简单总结下:LinkList的用法是比较简单的,与ArrayList用法类似,支持List接口,只是,LinkedList增加了一个接口Deque,可以把他看做队列,栈,双端队列,方便的在两端进行操作。如果只是用作List,那应该用ArrayList还是LinkList呢?我们需要了解LinkedList的实现原理

实现原理

我们先来看 LinkedList的内部组成,然后分析它的一些主要方法的实现,代码基于java7

内部组成

之前提到过,ArrayList内部是数组,元素在内存是连续存放的,但LinkList不是。LInkList直译就是链表,确切的说,它的内部实现是双向链表,每个元素在内存都是单独存放的,元素之间通过链接连在一起,类似于小朋友之间手拉手一样 。

为了表示链接关系,需要一个节点的概念。节点包括实际的元素,但同时有两个链接,分别指向前一个节点(前驱)和后一个节点(后继)。节点是一个内部类,具体定义为:

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;
    }
}

Node类表示节点,item指向实际的元素,next指向后一个节点,prev指向前一个节点。
LinkedList内部组成就是如下三个实例变量:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

我们暂时忽略transient关键字,size表示链表长度,默认0,first指向头节点,last指向尾节点,初始值都为null。

LinkedList的所有public方法内部操作的都是这三个实例变量,具体如何操作?链接关系如何维护?我们看一些主要方法,先来看add方法。

add方法

add方法代码:

public boolean add(E e){
    linkLast(e);
    return true;
}

主要就是调用了linkLast,它的代码为:

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++;
}

代码的基本步骤如下:
1. 创建一个新节点newNode。l和last指向原来的尾节点,如果原来的链表为空,则为null。代码为:Node<E> newNode = new Node<>(l,e,null);
2. 修改尾节点last,指向新的最后节点newNode。代码为:last = newNode
3. 修改节点前的后向链接,如果原来链表为空,则让头节点指向新节点,否则让前一个节点的next指向新节点。代码为:

if(l == null)
    first = newNode;
else
    l.next = newNode

4.增加链表大小,代码:size++
nodCount++的目的跟ArrayList一样,记录修改次数,便于迭代中间检测结构性变化。
我们通过一些图来进行介绍。比如,代码:

List<String> list = new LinkedList<String>();
list.add("a");
list.add("b");

执行完第一行后,内部结构如图一
图一:这里写图片描述
添加完“a”后,内部结构如图二所示。
图二:
这里写图片描述
添加完“b”后内部结构如图三:
这里写图片描述

可以看出,与ArrayList不同,LinkedList的内存是按需分配的,不需要预先分配多余的内存,添加元素只需分配新元素的空间,然后调节几个链接即可。

根据索引访问元素 get

添加了元素,如何根据缩印访问元素呢?我们看get方法的代码:

public E get(int index){
    checkElementIndex(index);
    return node(index).item;
}

checkElementIndex检查索引位置的有效性,如果无效抛出异常,代码为:

private void checkElementIndex(int index){
    if(!siElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isElementIndex(int index){
    return index >= 0 && index < size; 
}

如果index有效,则调用node方法查找对应节点,其item属性就指向实际元素内容,node方法代码:

Node<E> node(int index){
    if(index<(size >> 1)){ // 二进制移位运算:size>>1 = size/2
        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; 
    }
}

当索引位于前半部分的时候,从头节点开始查找,否则从尾节点开始查找。可以看出,与ArrayList明显不同,ArrayList中数组元素连续存放,可以根据缩印直接定位元素,而在LinkList中,则必须从头或尾顺着链接查找,效率比较低。

4. 根据内容查找元素

我们看下 indexOf的代码:

public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

代码也很简单,从头节点顺着链接往后找,如果要找的是null,则找第一个item为null的节点,否则使用equals方法进行比较。

5. 插入元素

add是在尾部添加元素,如果在头部或中间插入元素可以使用如下方法:
public void add(int index, E element)
代码是:

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

如果index为size, 添加到最后面,一般情况,是插入到index对应节点的前面,调用方法为linkBefore,它的代码为:

void linkBefore(E e,Node<E> succ){
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred,e,succ);
    succ.prev = newNode;
    if(pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

参数succ表示后继节点,变量pred表示前驱节点,目标是在pred和succ中间插入一个节点,插入步骤是:
1. 新建一个newNode,前驱为pred,后继为succ。代码:final Node<E> newNode = new Node<>(pred,e,succ);
2. 让后继的前驱指向新节点。代码succ.prev = newNode;
3. 让前驱的后继指向新节点,如果前驱为空,那么修改头节点指向新节点。代码:

    if(pred == null)
        first = newNode;
    else
        pred.next = newNode;
  1. 增长长度。
    我们通过图示来进行介绍。还是上面的例子,比如添加一个元素:
    list.add(1,”c”)
    内存结构如图所示:
    这里写图片描述
    可以看出,在中间插入元素,LinkedList只需要分配内幕才能,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多额外空间,且移动所有后续元素。

6. 删除元素

我们再来看删除元素,代码:

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

通过node方法找到节点后,调用unlink方法,代码:

E unlink(Node<E> x){
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> 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;
}

删除x节点,基本思路就是让x的前驱和后继直接链接起来,next是x的后继,prev是x的前驱,具体分类两步。
1. 让x的前驱的后继指向x的后继。如果x没有前驱,说明删除的是头节点,则修改头节点指向x的后继。
2. 让x的后继的前驱指向x的前驱。如果x没有后继,说明删除的是尾节点,则修改尾节点指向x的前驱。
通过图式进行说明。还是上面的例子,如果删除一个元素:
list.remove(1);
内存结构如图:

LinkedList特点分析

to be continue …

猜你喜欢

转载自blog.csdn.net/wang_8101/article/details/81176744