【集合学习--LinkedList】

学习内容:

  1. 概述
  2. LinkedList集合的主要结构
  3. LinkedList集合的添加操作
  4. LinkedList集合的移除操作
  5. LinkedList集合的查找操作
  6. LinkedList集合的栈工作特性
  7. LinkedList集合和ArrayList的对比

学习产出:

概述

在这里插入图片描述
学习的最后一种主要的List集合,学习的一种Queue集合。
LinkedList同时具有List集合和Queue集合的基本特征

LinkedList集合的主要结构

LinkedList集合的主要结构是双向链表,不要求节点具有连续的内存存储地址,每一个节点都使用一个LinkedList.Node类的对象进行描述
在这里插入图片描述item属性:当前Node节点上存储的具体数据对象
next属性:当前节点指向的下一个节点
prev属性:当前节点指向的上一个节点
在当前结构中,双向链表的头节点的prev和尾节点next为空


LinkedList内部采用first记录头节点,使用last属性记录尾节点,使用size属性记录当前长度

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
}

由上面的源码我们可以知道
1,当双向链表没有任何数据对象的时候,first和last一定为空
2,当只有一个数据对象的时候,first和last属性一定指向同一个节点
3,只有有一个对象,first和last就不可能为空


LinkedList集合的添加操作

从jdk1.8开始,LinkedList有三个用在链表不同位置添加新节点方法,linkFirst(E),linkLast(E),linkBefore(E,Node),但是这三个方法都不是public修饰发方法,这些方法要通过add(E),addLast(E),addFirst(E)等方法进行封装,才能提供服务


linkFirst(E)方法

向链表头部添加一个Node节点,并且调整first属性的指向位置

private void linkFirst(E e) {
		//记录first属性中信息
        final Node<E> f = first;
        //创建一个新节点,prev为空,next为first
        final Node<E> newNode = new Node<>(null, e, f);
        //变更fist
        first = newNode;
        //如果原来的first是空的,则表明原链表中没有数据,添加的第一个数据
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        //链表长度加一
        size++;
        //操作次数加一
        modCount++;
    }
linkLast(E)方法

向链表尾部添加一个Node节点,并且调整last属性的指向位置

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++;
    }
linkBefore(E)方法

在指定节点前的索引栏位上插入一个新节点,需要注意的是LinkedList集合的操作逻辑保证了srcc入参一定是不为空的

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集合的移除操作

复制移除的三个方法方法unlinkFirst(Node),unlinkList(Node),unlink(Node),同样这三个方法也是通过包装对外提供服务removeFirst(),removeLast(),remove(Object)


unlinkFirst(Node)方法

移除头节点,并且设置它的后续节点为头节点

private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        //记录头节点的值
        final E element = f.item;
        //记录头节点的下一个节点
        final Node<E> next = f.next;
        //清楚节点中的值
        f.item = null;
        f.next = null; // help GC
        //设置头节点为第二个节点
        first = next;
        //如果第二个节点为空,则说明只有一个元素让尾节点也为空
        if (next == null)
            last = null;
        else
        //让新头节点的prev为空
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

unlinkList(Node)方法与这个类似

unlink(Node)方法
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;
        //条件成立则说明是头节点
        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;
    }

LinkedList集合的查找操作

在调用linkBefore(E,Node)方法插入新节点时,和unlink(Node)方法时,都先要找到操作的这个节点。
双向链表查询指定索引位置的方式,就是从头节点或者尾节点开始遍历具体的实现如下

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

node(int ) 方法在addAll(int,Collection),get(int),set(int,E),add(int,E),remove(int)方法等读和写的时候都被使用。
有一些方法则比较特殊indexOf(Object),lastIndexOf(Object),remove(Object)
下面是indexOf(Object)的源码

//该方法是从头节点开始查询(如果是o为空,则是查询值为空的节点,如果不是为空,则是查询用equals方法比较相等的节点)
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;
    }

LinkedList集合的栈工作特性

LinkedList既实现了List,又实现了Deque,Deque具有传统队列的特殊定义,支持队列的普遍操作(如出队入队)。而Deque是双端队列,它支持从队列的两端独立检索和添加节点,因此Deque既支持后进先出(LIFO)又支持先进先出(FIFO)
涉及的一些双端队列操作如下

方法名方法说明
addFirst(E)头部新增一个节点,与Push(E)方法相同
addLast(E)尾部新增一个节点
offerFirst(E)作用与addFirst(E)相同
offerLast(E)与addLast(E)相同
getFirst()返回当前双向链表中的头节点,没有节点会抛异常
getLast()返回当前双向链表的尾节点,没有节点会抛异常
peekFirst()返回头节点,没有返回null
peekLast()返回尾节点,没有返回null
push(E)作为栈结构,向栈顶添加一个节点。实际上就是双向链表头部设置一个新节点
pop()作为栈结构,移除栈顶节点,并返回,没有会抛异常。实际上就是调用的removeFirst()
removeFirst()移除双向链表的头节点
removeLast()移除双向链表尾部节点
pollFirst()移除双向链表头部节点,没有返回null
pollLast()移除双向链表尾部节点,没有返回null

LinkedList集合和ArrayList的对比

两种集合写的操作性能比较
  • ArrayList

    • 当使用add(E)方法在数组尾部添加新数据对象,数组都有多余容量,此时时间复杂度为O(1)
    • 当使用add(E)方法在数组尾部添加新数据对象,数组没有多余容量,此时数组要先扩容,在添加,会有较多的时间消耗
    • 极端情况,在使用add(int ,E)方法在第一个位置添加数据,且此时数组没有多余容量,则要先扩容,然后当前数组所有数据对象整体向后移动一个位置,此时耗时最多
  • LinkedList

    • 在头尾增加数据使用addFirst(E),addLast(E),add(E),方法没有查询的消耗,时间复杂度为O(1)
    • 不是在头尾进行添加操作,则需要先找到正确的索引位置,此时时间复杂度为O(n)
两种集合读的操作性能比较

ArrayList无论是读哪个位置的数据,时间复杂度都为O(1),对性能的消耗都是相同的,因为ArrayList支持随机访问。

LinkedList的如果不是头尾的读,那么就存在一个查询的过程,尽管做了优化,根据索引位置确定是从前往后还是从后往前,最慢的是集合中间的元素。时间复杂度为O(n)

不同遍历方式对LinkedList集合遍历的意义

有多种方式对LinkedList进行遍历,可以使用for(;😉,for(😃,stream()方法,Iterator迭代器等多种方式。

 LinkedList<String> list=new LinkedList<>();
        //第一种,传统for循环
        //每一次get(i)都需要从头到尾或者从尾到头遍历一次
        //时间复杂度为O(n2)
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
        }
        //第二种
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            String next = iterator.next();
        }
        //第三种
        list.forEach(i ->{
            
        });
        //第四种
        for(String s:list){
            
        }
        //第五种
        list.stream().forEach(i->{
            
        });

第2,3,4,5本质都是迭代器方式,每一次查询能从上一次查询的位置继续进行,而不是每次从头节点或者尾节点进行查询。
下面是for(:)进行遍历时传统迭代器ListItr的实现(部分)

private class ListItr implements ListIterator<E> {
		//最后一次返回的节点
        private Node<E> lastReturned;
        //下一次要返回的节点
        private Node<E> next;
        //指向下一个要返回的索引位
        private int nextIndex;
        //操作次数数器
        private int expectedModCount = modCount;
        //创建迭代器时必须传入一个开始索引位置
        //ListItr类会在指定的索引位置上添加一个变量引用
        ListItr(int index) {
            // assert isPositionIndex(index);
            next = (index == size) ? null : node(index);
            nextIndex = index;
        }
        //如果要查询的索引位置大于或者等于当前集合大小,则表示没有下一个要查询的节点了
        public boolean hasNext() {
            return nextIndex < size;
        }
        //用于查询遍历下一个节点
        public E next() {
            checkForComodification();
            if (!hasNext())
                throw new NoSuchElementException();
            //设置最后一次返回的节点为当前节点
            lastReturned = next;
            //next变量指向下一个节点
            next = next.next;
            //记录下一个索引位置的变量
            nextIndex++;
            //返回当前遍历的节点信息
            return lastReturned.item;
        }
        //如果成立,则说明在遍历时,双向链表的结构发生了变化
 final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
在什么场景中推荐使用LinkedList

1,集合的写规模远远大于读操作规模,并且这种写并不是在集合尾部进行的。
2,满足上一条的情况下,需要对集合进行栈结构性质的操作(单纯的栈结构操作使用ArrayDeque)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值