一、linkedlist的整体结构

1.1、linkedlist的继承关系

  • public class linkedlist<e> extends abstractsequentiallist <e> implements list<e>, deque<e>
  • linkedlist具备abstractsequentiallist的特点:abstractsequentiallist 只支持按次序访问,而不像 abstractlist 那样支持随机访问
  • linkedlist具备list的特点
  • linkedlist具备deque的特点:deque是一个线性collection,支持在两端插入和移除元素

1.2、linkedlist的结构

//元素个数
transient int size = 0;
//第一个元素指针
transient node<e> first;
//最后一个元素指针
transient node<e> last;
//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;
    }
}

1.2.1 node的结构

linkedlist结构

linkedlist特点

1.linkedlist是通过双链表去实现的。

2.linkedlist不存在容量不足的问题,因为是链表。

3.linkedlist实现了deque,而deque接口定义了在双端队列两端访问元素的方法,所以linkedlist可以作为fifo(先进先出)的队列;linkedlist可以作为lifo(后进先出)的栈

 二、源码分析

2.1、添加元素

//添加元素
public boolean add(e e) {
    //默认调用,尾部添加元素的方法
    linklast(e);
    return true;
}
//尾部添加元素
void linklast(e e) {
    //记录当前尾部元素
    final node<e> l = last;
    //创建一个新的node节点 ,prev是当前的尾节点,next指向null
    final node<e> newnode = new node<>(l, e, null);
    //将last设置为新节点
    last = newnode;
    //判断当前尾部节点是否为null
    if (l == null)
        //当前尾部节点为null,就挂到头结点上
        first = newnode;
    else
        //当前尾部节点不为null,就将新建的node挂到当前last节点的next指针上
        l.next = newnode;
    //元素的个数+1
    size++;
    //linkedlist修改记录+1
    modcount++;
}

新增元素add()方法默认是尾部追加,核心就是将新建的node节点追加到当前last节点的next指针上 ,伪代码:

node newnode=new node();
newnode.prev=last;
last.next=newnode;
last=newnode;
last.next=null;

addfirst:首部追加

public void addfirst(e e) {
    linkfirst(e);
}
//头部追加
private void linkfirst(e e) {
    //记录当前首部元素
    final node<e> f = first;
    //新建一个node节点
    final node<e> newnode = new node<>(null, e, f);
    //首部元素指向新建的节点
    first = newnode;
    //判断当前首部指针是否为null
    if (f == null)
        //当前首部指针为null,就把新建的节点挂到last指针上
        last = newnode;
    else
        //当前首部指针不为null,就把新建的节点挂到,当前first指针指向元素的prev指针上
        f.prev = newnode;
    //元素个数+1
    size++;
    //linkedlist修改记录+1
    modcount++;
}

首部追加的逻辑与尾部追加基本相同,伪代码:

node newnode=new node();
newnode.next=first;
first.prev=newnode;
first=newnode;
first.prev=null;(也可以:newnode.prev=null)

指定位置添加元素:add(int index, e element):

public void add(int index, e element) {
    //检查要插入的位置是否合法
    checkpositionindex(index);
    //如要插入的位置在最后,直接调用linklast()
    if (index == size)
        linklast(element);
    else
        //如要插入的位置不在最后,就先查找再插入
        linkbefore(element, node(index));
}
 
//查找要插入元素的位置
node<e> node(int index) {
    // assert iselementindex(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;
    }
}
//将新建的元素插入到查找的元素前面
void linkbefore(e e, node<e> succ) {
    // assert succ != null;
    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++;
}

linkedlist是一个双向链表,他只记录了头部和尾部位置,如果我们要指定位置插入,他会这么做:

1.先遍历查找出要插入的元素位置,然后再插入;查找方式是根据 index < (size >> 1) 判断结果,决定是从头遍历,还是从尾部遍历,这种遍历方式类似于二分查找(只在第一层循环二分)

2.新建一个node节点,插入到查找出来的元素的前面

由此可知为何链表对随机位置读写是不合适的;他的时间复杂度=o(n/2) ,如果n很大,我们一般就认为他的时间复杂度=o(n)

2.2、删除元素

//重写list的remove()
public boolean remove(object o) {
    if (o == null) {
        //如果要删除的元素null元素,从头开始查找这个null元素
        for (node<e> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
         //如果要删除的元素不null元素,从头开始查找这个非null元素
        for (node<e> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
//执行删除逻辑,实质就是打断改元素与链表的引用关系
e unlink(node<e> x) {
    // assert x != null;
    //记录改元素的值,实际作用就是做返回值
    final e element = x.item;
    //记录当前元素的下一个节点
    final node<e> next = x.next;
    //记录当前元素的上一个节点
    final node<e> prev = x.prev;
    //判断 x->prev 节点是否为null,为null就是删除头结点 
    if (prev == null) {
        first = next;
    } else {
        //将 x->prev节点的next指针指向x节点的下一个节点
        prev.next = next;
        //将 x->prev 指针,设置为null(断开引用关系)
        x.prev = null;
    }
     //判断 x->next 节点是否为null,为null就是删尾部结点 
    if (next == null) {
        last = prev;
    } else {
        //将x->next节点的prev指针指向x->prev
        next.prev = prev;
        //将 x->next指针,设置为null(断开引用关系)
        x.next = null;
    }
    //将x的值设置为null
    x.item = null;
    //集合大小-1
    size--;
    //集合的修改记录-1
    modcount++;
    return element;
}

这里我们看到linkedlist重写了list的remove方法,整个删除逻辑也是先查找再删除,时间复杂度o(n),如果是删除首部元素时间复杂度=o(1),若要删除尾部元素请使用removelast( ) 

  • linkedlis删除首部元素:removefirst()
  • linkedlis删除尾部元素:removelast()
  • linkedlis首部出队:pollfirst( ) ,队列的特点
  • linkedlit尾部出队:polllast( ),队列的特点

2.3、迭代器

iterator迭代器只能是从头往尾迭代,而linkedlist是双向链表,他还可以从尾往头部迭代,java提供了一个新的迭代器接口:

public interface listiterator<e> extends iterator<e>{
    //判断是否存在下一个元素
    boolean hasnext();
    //获取下一个元素
    e next();
    //判断是否还有前一个元素
    boolean hasprevious();
    //获取前一个元素
    e previous();
}

linkedlist实现该接口:

private class listitr implements listiterator<e> {
    private node<e> lastreturned;//上一次next() 或者 previous()的元素
    private node<e> next;//lastreturned->next 指向的元素
    private int nextindex;//下一个元素的位置
    private int expectedmodcount = modcount;
}

linkedlist从前往后遍历:

//是否存在下一个元素
public boolean hasnext() {
    return nextindex < size;
}
public e next() {
    //检查集合的版本
    checkforcomodification();
    if (!hasnext())
        throw new nosuchelementexception();
    //lastreturned赋值上次next
    lastreturned = next;
    //next赋值为上次next->next
    next = next.next;
    //下一个元素的位置
    nextindex++;
    return lastreturned.item;
}

linkedlist从后往前遍历:

//判断是否到头了
public boolean hasprevious() {
    return nextindex > 0;
}
//从尾部往头部取数据
public e previous() {
    checkforcomodification();
    if (!hasprevious())
        throw new nosuchelementexception();
    // next==null:第一次遍历取尾节点(last),或者上一次遍历时把尾节点删除掉了
    // next!=null:已经发生过遍历了,直接取前一个节点即可(next.prev)
    lastreturned = next = (next == null) ? last : next.prev;
    //遍历的指针-1
    nextindex--;
    return lastreturned.item;
}

迭代器删除元素:

public void remove() {
    checkforcomodification();
    // lastreturned 是本次迭代需要删除的值
    // lastreturned==null则调用者没有主动执行过 next() 或者 previos(),二直接调remove()
    // lastreturned!=null,是在上次执行 next() 或者 previos()方法时赋的值
    if (lastreturned == null)
        throw new illegalstateexception();
    //保存将当前要删除节点的下一个节点(如果是从尾往头遍历,该值永远是null)
    node<e> lastnext = lastreturned.next;
    //删除当前节点
    unlink(lastreturned);
 
    // next == lastreturned:从尾到头递归顺序,并且是第一次迭代,并且要删除最后一个元素的情况下,
    // previous() 方法里面设置了 lastreturned = next = last,所以 next 和 lastreturned会相等
    if (next == lastreturned)
        next = lastnext;
    else
        nextindex--;
    lastreturned = null;
    expectedmodcount++;
}

三、总结

linkedlist底层数据结构是双向链表,所以他更适合顺序操作,由于他继承了deque接口,同时他具有队列的性质,非线程安全的集合

到此这篇关于java基础之容器linkedlist的文章就介绍到这了,更多相关java容器linkedlist内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!