暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Java面试一天一题(day 7面试题:聊聊LinkedList的底层结构)

架构狂人 2021-06-29
705

    之前谈过ArrayList的扩容,它是一个动态数组,插入删除元素都需要移动元素,扩容则需要进行数组拷贝,今天我们来聊聊真正动态的数据结构



    LinkedList,顾名思义,链表存储数据的容器称为结点,每个结点之间用一个或者两个链串起来形成一种无需扩容的结构。这样一种数据结构,无需处理固定容量,但是也因此牺牲了其随机访问的效率,不能通过索引查找,需要遍历到对应位置进行插入或者删除


1 链表


1.1 什么是链表


链表是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,有一系列结点(地址)组成,结点可以动态生成


百度百科中定义的单链表:(指针变量可以不用管)

1.1 链表的作用

将数据按照一定顺序串联存储在结点中,允许在任意位置插入和删除结点,且无需处理固定容量,做到动态添加和删除

1.2 单向链表



一个结点中包含数据和下一个节点的地址,尾节点没有下一个结点,所以指向null。访问某个节点只能从头节点开始查找,然后依次往后遍历

单向循环链表:单链表基础上,尾结点next指向头结点



1.3 双向链表


双向链表的每个结点包含以下数据:上一个结点的地址,自身结点数据,下一个结点的地址.尾结点没有下一个结点,所以指向null。这样的结构,比如我拿到链表中间的一个节点,即可以往前遍历,也可以往后遍历,这是双向链表的优势

双向循环链表的尾结点的下一个结点是头结点,头结点的上一个节点是尾结点

2 LinkedList各个版本对比



(1) JDK1.6使用的是一个带有头结点的双向循环链表,头结点不存储实际数据

(2) JDK1.7之后使用的是不带头结点的普通的双向链表,增加两个节点指针first、last分别指向首尾节点,相比之前节约了head结点的部分内存,其他并无太大差别


3 LinkedList重要属性及方法

    //记录链表中节点的个数
    transient int size = 0;
    //记录链表中第一个节点
    transient Node<E> first;
    //记录链表中最后一个节点
    transient Node<E> last;


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


    LinkedList继承自Deque,所以需要支持removeFirst、removeLast等一系列操作,因此需要First、Last记录首尾结点



    3.1 添加元素


    流程图:

      public boolean add(E e) {
      linkLast(e);
      return true;
      }
      void linkLast(E e) {
      //获取最后一个节点
      final Node<E> l = last;
      //这里创建出一个新的节点,它的前一个节点指向l,要保存的数据即传入进来的数据,后一个节点为null
      final Node<E> newNode = new Node<>(l, e, null);
      //接着将这个新节点当做链表的最后一个节点保存起来
      last = newNode;
      //如果刚开始最后一个节点为null,说明是第一次添加
      if (l == null)
      first = newNode;
      else
      //否则将刚开始最后一个节点的下一个节点的引用指向新创建的节点
      l.next = newNode;
      size++;
      modCount++;
      }


      3.2 获取元素


        public E get(int index) {
        //参数合法检验
        checkElementIndex(index);
        return node(index).item;
        }
        //取该节点保存的值并返回
        Node<E> node(int index) {
        //index如果小于链表大小的一半,则从头遍历
        if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
        x = x.next;
        return x;
        } else {
        //index如果大于等于链表大小的一半,则从尾部遍历
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
        x = x.prev;
        return x;
        }
        }



        3.3 删除元素



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


          E unlink(Node<E> x) {
          //分别保存当前要删除节点的值、前置节点、后置节点
          final E element = x.item;
          final Node<E> next = x.next;
          final Node<E> prev = x.prev;

          //前置节点为null,说明当前节点为首节点
          if (prev == null) {
          first = next;
          } else {
          //前置节点不为null,将前置节点的next后移
          prev.next = next;
          x.prev = null; //(1)
          }

          //后置节点为null,说明当前节点为尾节点
          if (next == null) {
          last = prev;
          } else {
          //后置节点不为null,将后置节点的prev指向前置节点
          next.prev = prev;
          x.next = null; //(2)
          }
          x.item = null; //(3)
          size--;
          modCount++;
          return element;
          }


          注意删除元素(1)、(2)、(3)处,置空避免内存占用考虑垃圾回收

          4 LinkedList和ArrayList的区别

          (1)内存位置比较
          ArrayLst基于动态数组,所以内存上是连续的,而LinkedList基于链表,内存上不需要保持连续。

          (2)插入速度比较
          经常听到LinkedList插入比ArrayLst快。这种说法不准确。LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Node的引用地址,而ArrayList做插入、删除的时候,慢在数组元素的arrCopy,快在寻址。

          如果待插入的元素位置在数据结构的前半段尤其是非常靠前时,ArrayList需要拷贝大量的元素,显然LinkedList会更快

          如果带插入元素位置在数据结构后半段尤其是非常靠后,ArrayList需要拷贝的元素个数会越来越少,所以速度也会提升,甚至超过LinkedList

          4 LinkedList 遍历的探索

          一般遍历LinkedList时最好不要使用普通for循环,而是使用迭代器代替,这是为什么呢?
              

            //以下是for循环遍历
            for(int i =0;i<list.size();i++){
            list.get(i);
            }
            //迭代器遍历
            Iterator<String> it = list.listIterator();
            while(it.hasNext()){
            System.out.print(it.next()+" ");
            }


            从上面代码可见,iterator相比for循环少了list.get(i),调用一次get(i)时间复杂度为O(n),嵌套一次for循环就是O(n²),而在iterator中因为next的存在get当前元素不需要时间所以循环下来应该是O(n)

            5 小结

            (1)承自 AbstractSequentialList 接口,同时了还实现了 Deque, Queue 接口,可以用来模拟堆栈,队列,双端队列结构

            (2)LinkedList从1.7开始采用不带头结点的双向链表结构,相比1.6去掉了头结点

            (3)由于结点只存储数据域和前驱、后继结点的地址,和元素值无关,因而可以插入空值,同时允许重复插入

            (4)LinkedList默认通过尾插法新增元素,也可以通过addFirst的头插法方式,两者都需要判断是否第一次添加元素

            (5)相比基于动态数组的ArrayList,LinkedList随机访问的性能不如ArrayList,通过链表的方式换来优秀的增删效率


            (6)LinkedList结构对内存要求低,不需要大块连续内存来满足扩容,能够自动动态地消耗内存,容量变小时会自动释放曾经占用的内存,ArrayList则需要手动trim

            文章转载自架构狂人,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

            评论