• Java LinkedList小记


    1. 基本用法

      LinkedList实现了List、Deque、Queue接口,可以按照队列、栈和双端队列的方式进行操作。LinkedList有两个构造方法,一个是默认构造,另一个接受Collection:

    public LinkedList()
    public LinkedList(Collection<? extends E> c)

      可以按照List操作:

    List<Integer> list = new LinkedList<>();
    List<Integer> list1 = new LinkedList<>(Arrays.asList(2, 3, 4, 5));

      LinkedList还实现了队列接口Queue,队列的特点是先进先出,在尾部添加数据,在头部删除数据,其接口定义为:

    public interface Queue<E> extends Collection<E> {
        // 在尾部添加元素
        boolean add(E e);
        // 在尾部添加元素
        boolean offer(E e);
        // 返回头部元素,并且从队列中删除
        E remove();
        // 返回头部元素,并且从队列中删除
        E poll();
        // 返回头部元素,但不改变队列
        E element();
        // 返回头部元素,但不改变队列
        E peek();
    }

      Queue接口扩展了Collection,主要有三种操作,在尾部添加数据(add、offer)、查看头部元素(element、peek)和删除头部元素(remove、poll)。每种操作都有两种形式,区别在于特殊情况的处理不同。特殊情况是指当队列为空或者为满时,为空就是没有元素数据,为满是指队列有长度大小限制,而且已经占满了。LinkedList的实现中,队列长度没有限制,但是其他的Queue的实现可能有。在队列为空时,remove和element会抛出异常NoSuchElementException,而poll和peek返回null;在队列为满时,add会抛出IllegalStateException,而offer只是返回false。

      把LinkedList当做Queue使用:

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

      栈是一种和队列特点相反的数据结构,它的特点是先进后出,后进先出。Java中没有单独的栈接口,栈的相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:

        // 入栈
        void push(E e);
        // 出栈
        E pop();
        // 查看
        E peek();

      push表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push会抛出IllegalStateException;pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空会抛出NoSuchElementException;peek查看栈头部元素,不修改栈,如果栈为空,返回特殊值null。使用方法如下:

            Deque<Integer> stack = new LinkedList<>();
            stack.push(1);
            stack.push(2);
            stack.push(3);
            while (stack.peek() != null) {
                System.out.println(stack.pop());
            }
            /**
             * output:
             * 3
             * 2
             * 1
             */

      Java中还有一个Stack类,就是栈的意思,它也实现了栈的一些方法,如push、pop、peek等,但它没有实现Deque接口,他是Vector的子类它增加的这些方法也通过synchronized实现了线程安全。由于Vector和Stack内部使用了大量的syncronized做同步操作,效率比较低,已经过时了,具体就不学习了。

      栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但只在尾部添加、头部只查看和删除元素。有一个更为通用的操作两端的接口Deque。接口定义如下:

    public interface Deque<E> extends Queue<E> {
        void addFirst(E e);
        void addLast(E e);
        boolean offerFirst(E e);
        boolean offerLast(E e);
        E removeFirst();
        E removeLast();
        E pollFirst();
        E pollLast();
        E getFirst();
        E getLast();
        E peekFirst();
        E peekLast();
        //删除第一次出现的指定元素(从头到尾遍历)
        boolean removeFirstOccurrence(Object o);
        //删除最后次出现的指定元素(从头到尾遍历)
        boolean removeLastOccurrence(Object o);
        boolean add(E e);
        boolean offer(E e);
        E remove();
        E poll();
        E element();
        E peek();
        void push(E e);
        E pop();
        boolean remove(Object o);
        // 队列是否包含指定元素
        boolean contains(Object o);
        public int size();
        Iterator<E> iterator();
        // 从后往前遍历的迭代器
        Iterator<E> descendingIterator();
    }

      根据方法名很容易知道作用,稍微不太清晰的做了注释,descendingIterator()方法示例如下:

            Deque<String> deque = new LinkedList<>(Arrays.asList("a", "b", "c", "d"));
            Iterator<String> it = deque.descendingIterator();
            while (it.hasNext()) {
                System.out.print(it.next() + " ");
            }
            /**
             * output:
             * d c b a 
             */

      下面看下实现原理。

    2. 原理

      先来看下LinkedList的内部组成,再分析一些主要方法的实现,代码基于JDK8

    2.1内部组成

      我们知道,ArrayList内部是数组,元素在内存中是连续存放的,基于索引的访问效率非常高,但LinkedList不是。LinkedList的内部实现是双向链表,每个元素在内存中都是单独存放的,元素之间通过链接连接在一起。为了表示链接关系,需要一个节点的概念。节点包括实际的元素,但同时有两个链接,分别指向前一个(前驱)和后一个节点(后继)。节点是一个内部类:

        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;

      size表示链表长度,默认为0,first指向头节点,last指向尾节点,初始值都是null。LinkedList的所有public方法内部操作的就是这三个实例变量,来看下具体方法:

    2.2 add方法

        public boolean add(E e) {
            linkLast(e);
            return true;
        }
        
            void linkLast(E e) {
            // 将尾节点赋给l变量
            final Node<E> l = last;
            // 新建节点,将l赋给新建节点的pre前驱节点,e为当前节点的元素值,新建节点没有后继节点,所以为null
            final Node<E> newNode = new Node<>(l, e, null);
            // 将新建节点赋给尾节点
            last = newNode;
            // 如果尾节点不存在,就将新建节点作为头结点赋给first实例变量
            if (l == null)
                first = newNode;
            // 如若尾节点存在,就将新建节点作为尾节点的后继节点赋给l.next
            else
                l.next = newNode;
            // 链表长度加1
            size++;
            // 修改次数加1
            modCount++;
        }

      代码的基本步骤见代码中注释,modCount变量用来记录修改次数,便于在迭代中检测结构性变化。我们根据图示来理解下。比如如下代码:

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

      

      当新建list对象后内部结构如图一,头结点和尾节点都是null;当添加“a”后内部结构如图二,size加1,头结点和尾节点都指向同一个Node节点;当添加完“b”后内部结构如图三所示。

    2.3 根据索引访问元素

      来看下get方法:

        public E get(int index) {
            // 检查索引位置的有效性,若无效,抛出异常
            checkElementIndex(index);
            // 索引有效,执行node方法,查找指定索引位置的元素并返回
            return node(index).item;
        }
        private void checkElementIndex(int index) {
            if (!isElementIndex(index))
                // 抛出未受检异常,索引越界异常
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
        private boolean isElementIndex(int index) {
            return index >= 0 && index < size;
        }
        Node<E> node(int index) {
            // 若索引位置在前半部分,则从头结点开始查找(右移一位相当于除以2),若找到返回节点
            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;
            }
        }

      与ArrayList不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头到尾顺着连接查找,效率比较低。

    2.4 按内容查找元素

      看下indexOf的代码:

        public int indexOf(Object o) {
            int index = 0;
            // 查找元素为null时
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null)
                        return index;
                    index++;
                }
            // 查找元素不为null时
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item))
                        return index;
                    index++;
                }
            }
            // 买找到指定元素返回-1
            return -1;
        }

      代码比较简单,有两种情况,都是从头节点开始找,见代码注释。

    2.5 插入元素

      add是在尾部添加元素,如果在头部或者中间插入元素可以使用如下重载方法:

        public void add(int index, E element) {
            checkPositionIndex(index);
            // 这就是在尾部添加元素
            if (index == size)
                linkLast(element);
            // 主要看这个
            else
                linkBefore(element, node(index));
        }
        void linkBefore(E e, Node<E> succ) {
            // succ不为空,就把succ的前驱节点赋给pred
            final Node<E> pred = succ.prev;
            // 新建节点,将pred指定为新建节点的前驱节点,succ为后继节点
            final Node<E> newNode = new Node<>(pred, e, succ);
            // 将后的前驱指向新建节点
            succ.prev = newNode;
            // 将前驱的后继指向新建节点,若前驱为空,修改头结点指向新节点
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            // 增加长度
            size++;
            modCount++;
        }

      下面通过图示来加深理解,比如添加一个元素

        list.add(1, "c");

      

      可以看出,在中间插入元素,LinkedList只需按需分配内存,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多的额外空间,且移动、复制所有后继元素。

    2.6 删除元素

      再来看看删除元素的代码:

        public E remove(int index) {
            // 同上检查索引是否有效
            checkElementIndex(index);
            // node方法先查找节点,再执行unlink删除指定节点
            return unlink(node(index));
        }
        E unlink(Node<E> x) {
            // x节点不为空,取得节点元素值
            final E element = x.item;
            // 取得前驱节点
            final Node<E> next = x.next;
            // 取得后继节点
            final Node<E> prev = x.prev;
            // 指定前驱的后继为x的后继(不在指向x),若前驱为空,修改头节点指向x的后继
            if (prev == null) {
                first = next;
            } else {
                prev.next = next;
                x.prev = null;
            }
            // 指定后继的前驱为x的前驱(不再指向x),若后继为空,修改尾节点指向x的前驱
            if (next == null) {
                last = prev;
            } else {
                next.prev = prev;
                x.next = null;
            }
            // x节点元素值设为null,便于垃圾回收
            x.item = null;
            // 链表长度减1
            size--;
            // 修改次数加1
            modCount++;
            // 返回删除的节点值
            return element;
        }

      分析逻辑见代码注释,基本思路就是让x的前驱和后继直接链接起来,再把x的前驱、后继节点、item都设置为null,便于垃圾回收。下面通过图示加深理解,比如删除一个元素:

    list.remove(1);

    3. LinkedList特点总结

      用法上LinkedList是一个List,有序有重复元素,也实现了Deque接口,可以作为队列、栈和双端队列使用。实现原理上,LinkedList内部是一个双向链表,并维护了长度、头结点和尾节点。有如下特点:

      1. 按需分配空间,不需要预先分配很多空间。

      2. 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。

      3. 不管列表是否已排序,只要按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。

    ---------- I love three things in this world. Sun, moon and you. Sun for morning, moon for night , and you forever .

  • 相关阅读:
    Vue 备
    mac 下如何建立vue-cli项目
    24,25-request对象
    nodejs 备忘
    nodejs中mysql断线重连
    创建node.js,blog
    Mac 升级node与npm
    js 弹出层,以及在javascript里定义层样式
    js 光标选中 操作
    js 捕获型事件
  • 原文地址:https://www.cnblogs.com/rookiek/p/10909530.html
Copyright © 2020-2023  润新知