LinkedList实现分析(二)——常用操作

2019-08-23  本文已影响0人  alexwu59

上一篇文章LinkedList实现分析(一)介绍了LinkedList中的一些重要属性和构造方法,下面我们将详细介绍一下LinkedList提高的常用方法的实现原理

元素添加

add(E e)方法

往LinkedList添加元素,LinkedList提供了多重方式,首先介绍add方法,下面我们使用一个简单的实例代码来分析一下add方法的实现原理:

List<String> myList = new LinkedList();//1
myList.add("a"); //2
myList.add("b"); //3
myList.add("c"); //4

执行第一行代码后创建了一个空的myList对象,此时myList如下图所示:


myList.png

然后执行第2行代码myList.add("a"),把字符串"a"添加到myList中,通过debug方式进入add方法内部:

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

add又调用linkLast方法,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++;
 }

在linkLast内部,此时e="a"。首先把last,此时myList是新创建的对象,last=null,赋值给一个临时变量 l,这里的做法是用来保存last的值,然后用元素e创建一个新的Node对象,这里是往myList的尾部添加元素,因此创建的Node节点的构造方法最后一个参数是null,而第一个参数表示的是插入元素e的前一个元素指针,因此需要传入l,因为在整个LinkedList中last属性表示的最后一个元素。当创建好包含e的newNode节点的时候,此时newNode就应该是myList的最后一个节点,也是第一个节点(l==null),因此把newNode赋值给last和first,然后把表示myList大学的size进行加一,最后对modCount加一,记录了修改次数。执行完第2行代码后,myList的状态入下图所示:


myList.png

当示例代码执行第3行代码的时,同样会执行linkLast,此时e=“b”。在进入linkLast方法的时候,last的值已经不在是null,而是刚刚创建的包含了“a”的Node节点对象,同样把last先用一个临时变量l保存起来,然后创建一个包含“b”的Node节点,这个时候该node节点的前一个节点是“a”所在的节点,因此构造函数 new Node<>(l, e, null),把新创建的newNode作为整个myList的最后一个节点: last = newNode,而由于l不为null,也就是last不为null,所以需要把新创建的节点放到myList中最后一个节点的后面: l.next = newNode,最后修改size和modCount的值,完成“b”元素的添加。此时myList的状态如下图所示:


myList.png

然后示例代码执行到第4行,此时myList已经有2个node,根据上图可知last指向了“b”node,然创建“c”的node对象,让“c”的prev执行“b”,然后myList中的last指向“c”,“b”的next执行“c”,执行完后myList的状态如下图所示:


myList.png

add(int index, E element)

该方法是在指定的索引的位置插入一个元素,index值指定的索引位置,element是需要插入的元素信息。这里还是以一个示例为基础进行对 add(int index, E element) 源码的分析,示例代码如下:

List<String> myList = new LinkedList();//1
myList.add("a"); //2
myList.add(0,"b")//3

当示例代码执行完第2部的时候,myList的状态如下图所示:


myList.png

然后执行第3行代码,具体 add(int index, E element) 的源码如下:

 public void add(int index, E element) {
        checkPositionIndex(index);//index校验
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

首先是对传入的index进行位置校验,具体实现是要求index不能小于0,不能大于myList当前的大小。根据示例代码,执行到第3行的时候,此时size=1,index=0,因此程序执行linkBefore方法,linkBefore是第一参数是新添加的元素element,第二个参数表示element需要被添加到myList中哪个元素的后面。这里传入是位置索引,因此需要调用node(index)方法找到mylist中index对应的node对象,node方法实现如下:

Node<E> node(int 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;
 }

这里源码中使用了一个技巧,通过判断传入的index与整个list的大小(size)的1/2进行比较,如果index<size/2,那么对myList进行正序遍历,如果index>=size/2,那么对myList进行倒序遍历。这里需要大家注意的是源码中使用<font color=red>右移操作获取size的1/2</font>,这个技巧希望大家可以掌握:

在java中,数字左移表示乘2,数字右移除2
由于当前myList的size=1,右移之后是0,index=0,因此执行else的语句,从last指向的node开始倒序遍历,此时myList只有一个index=0所指向的元素就是last的值,因此把last返回。node方法执行完之后,开始执行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++;
 }

linkBefore方法就是一个双向列表的插入操作,首先把succ的前驱节点prev保存到临时变量pred,使用传入的e(根据示例代码,此时e=“b”)创建一个新的Node对象:newNode,由于是在succ节点的前面插入新元素"b",因此newNode的前驱点是之前succ节点的前驱节点,newNode的后节点就是succ节点本身:final Node<E> newNode = new Node<>(pred, e, succ),此时myList的状态如下图所示:


image.png

然后执行 succ.prev = newNode,是把原来succ指向的前驱节点改成了newNode,因为newNode插入了succ节点前面,因此succ的prev就是newNode,指向完的myList状态如下:


image.png
然后是判断pred是否为null,在myList的当前状态中, succ.prev是null,pred也就是null,所以把first指向newNode,也就是在myList的头结点succ之前添加一个新元素newNode,即 first = newNode;然后对size和modCount加一,执行完最后myList的状态如下图:
image.png

addAll(Collection<? extends E> c)

下面介绍一下使用集合对象给LinkedList添加元素,addAll有两个重载方法:

 public boolean addAll(Collection<? extends E> c)
 public boolean addAll(int index, Collection<? extends E> c) 

在源码中,addAll(Collection<? extends E> c)实际是调用了addAll(int index, Collection<? extends E> c) :

 public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
}

因此这里只介绍后面一个addAll的实现:

public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;
        //下面的代码是查询需要插入位置的前驱和后继节点
        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }
       
        //把a中的元素创建为一个列表
        //并且让pred指向a的最后一个节点
        for (Object o : a) {
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }
        //如果没有后继节点,那么c集合中最后一个节点就是
        //整个list的最后节点
        if (succ == null) {
            last = pred;
        } else { 
           //如果在list找到插入的前驱和后继节点
           //把c中的最后一个节点的后继节点指向succ
           //把后继节点的前驱节点指向c中的最后一个节点
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

addAll方法内部,首先还是判断index的值是否合法,然后对传入的Collection对象c进行验证, 接着在源码中定义了两个变量:

Node<E> pred, succ;

pred表示需要插入c的前驱节点,succ表示c的后继节点。当index=size表示是把c添加到LinkedList的尾部,因此执行succ=null和pred=last;否则调用 node(index)方法,找到后继节点succ,然后把当前后继节点的前驱节点赋值给pred。

找到所插入前驱和后继节点之后,开始循环遍历c,把c中的每一个元素创建一个Node对象,c中的前一个元素都是后一个元素的前驱节点,把c建立为一个链表。然后把c的链表插入的LinkedList中,具体见上面源码的注释,完成链表的连接操作只会,对size和modCount进行加1操作。

其他添加元素的方法

下面介绍一个往list头插入元素的方法:

public void addFirst(E e) {
        linkFirst(e);
 }
private void linkFirst(E e) {
       //保存第一个元素
        final Node<E> f = first;
       //头插法,因此newNode的前驱节点是null,后继节点是f
        final Node<E> newNode = new Node<>(null, e, f);
        //让新创建节点当整个list的头节点
        first = newNode;
        if (f == null)
            //如果f=null,表示当前list没有一个元素,因此需要给last=newNode
            last = newNode;
        else
           //把之前的first接的前驱节点换成新创建的newNode
            f.prev = newNode;
        size++;
        modCount++;
 }

头插入的原理就是在整个list中,找到一个元素first,把first的前驱节点换成新插入的元素,然后把新传入元素的后继节点指向之前的头节点,具体实现见上面源码和注释

LinkedList提供了offer(E e),offerFirst(E e), offerLast(E e),addFirst(E e) ,addLast(E e),push(E e) 往list中添加一个元素的方法,这些方法的底层都是基于上面介绍的原理实现的:头插和尾插。这里就不多做说明。

获取元素

LinkedList获取元素提供多种方法:

//根据索引获取元素
 public E get(int index) 
//获取头元素
 public E peek() 
//获取头元素,并且把头元素从list中删除
 public E poll() 
//获取头元素,并且把头元素从list中删除
 public E pop() 

下面追个介绍这些方法的具体实现:

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

由上面源码可知,get操作底层是调用的node方法,node方法参考上面的介绍。
下面是peek,peek操作是获取头元素,因此只需要获取到LinkedList的first所执行的元素就可以了。

public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
 }

下面介绍poll():

public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
 }

private E unlinkFirst(Node<E> f) {
        //保存f的具体值
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        //f的后继节点赋值给LinkedList的first
        first = next;
        //next==null表示要删除的节点是list的最后一个节点
        if (next == null)
            last = null;
        else
            //表示删除f之后的list列表中,头结点的prev应该为null,而之前
            //prev是执行f的
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

由上面源码可知,当调用poll的时候,会把LinkedList的头元素返回,然后把头元素从LinkedList中删除。
下面看一下pop方法:

public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

pop方法的底层也是调用unlinkFirst方法完成头结点的删除操作。

To Array的操作

LinkedList提供了两种方法完成list到数组的转换:

public Object[] toArray() 
public <T> T[] toArray(T[] a) 

第一个方法,是把list转为Object类型的数组,具体是实现如下:

public Object[] toArray() {
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
 }

该方法的实现原理比较简单,首先创建一个大小与list相同的object类型数组,然后从头到尾遍历list的元素,把每个元素放到创建的数组中,因为数组对象是Object类型,因此list中的任何元素都可以直接放入到数组中,最后把数组返回。
在T[] toArray(T[] a) 方法中,需要传递一个T类型的数组对象,这样可以按照T类型返回T类型数组:

public <T> T[] toArray(T[] a) {
        if (a.length < size)
            a = (T[])java.lang.reflect.Array.newInstance(
                                a.getClass().getComponentType(), size);
        int i = 0;
        Object[] result = a;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;

        if (a.length > size)
            a[size] = null;
        return a;
    }

首先是判断传入的数组a的大小是否大于或者等于list的大小,如果小于,那么利用反射机制重新创建一个数组,此时参数a和toArray的数组就不是同一个数组了,如果a的大小大于size的值,假设数组a={"1","2","3","4"},LinkedList的值是:{"a","b"},那么调用toArray方法之后,a的值变为:{"a","b",null,"4"}。"3"变为null是因为

 if (a.length > size)
        a[size] = null;

今天的分享到这里,谢谢!

上一篇下一篇

猜你喜欢

热点阅读