线性表及ArrayList/LinkedList源码分析总结

一.线性表

定义:零个或者多个元素的有限序列。
也就是说它得满足以下几个条件:
  ①该序列的数据元素是有限的。
  ②如果元素有多个,除了第一个和最后一个元素之外,每个元素只有一个前驱元素和一个后驱元素
  ③第一元素没有前驱元素,最后一个元素没有后继元素。
  ④序列中的元素数据类型相同
则这样的数据结构为线性结构。在复杂的线性表中,一个数据元素(对象)可以由若干个数据项组成,组成一张数据表,类似于数据库。

二.线性表的抽象数据类型
1.相关概念

抽象数据类型(abstract data type,ADT)是带有一组操作的一组对象的集合。这句话表明,线性表的抽闲数据类型主要包括两个东西:数据集合和数据集合上的操作集合
①数据集合:我们假设型如a0,a1,a2,...an-1的表,我们说这个表的大小是N,我们将大小为0的标称为空表
②集合上的操作:一般常用的操作包括,查找,插入,删除,判断是否为空等等。

2.线性表的顺序储存结构

线性表的顺序储存结构,指的是用一段地址连续的储存单元依次储存线性表的数据元素。我们可以用一维数组实现顺序储存结构,并且上述对表的所有操作都可由数组实现。但是一维数组有一个很大缺点就是它得长度是固定的,也就是我们创建的额时候就声明的长度,一旦我们要存储的数据超出了这个长度,我们就不得不手动的创建一个更长的新数组并将原来数组中的数据拷贝过去。

    int [] arr = new int[10]
    ......
    int [] newArr = new int[arr.length*2]
    for(int i=0;i<arr;i++){
        newArr[i] = arr[i];
    }
    arr = new Arr;

显然这样做非常的不明智,因此java中的ArrayList就应用而生了。ArrayList也就是传说中的动态数组,也就是我们可以随着我们数据的添加动态增长的数组。
  实际上不管是简单数组也好,动态数组也好,在具体的操作中都存在这样的问题:
  ①如果我们在线性表的第i个位置插入/删除一个元素,那么我们需要怎么做呢?首先我们得从最后一个元素开始遍历,到第i个位置,分辨将他们向后/前移动一个位置在i位置处将要插入/删除的元素进行相应的插入/删除操作整体的表长加/减1.
  ②如果我们在线性表的第一个位置插入/删除一个元素,那么整个表的所有元素都得向后/向前移动一个单位,那么此时操作的时间复杂度为O(N);如果我们在线性表的最末位置进行上面两种操作,那么对应的时间复杂度为O(1)——综合来看,在线性表中插入或者删除一个元素的平均时间复杂度为O(N/2)。
  总结一下,线性表的缺点——插入和删除操作需要移动大量的元素;造成内存空间的"碎片化"。这里有些童鞋就说了,ArrayList是一个线性表吧,我再ArrayList中添加/删除一个元素直接调用add()/remove()方法就行了啊,也是一步到位啊——这样想就不对了,如果我们看ArrayList的源码就会发现,实际上他内部也是通过数组来实现的,remove()/add()操作也要通过上面说的一系列步骤才能完成,只不过做了封装让我们用起来爽。之后我们会通过源码分析ArrayList等的内部的实现方式。
  当然了,优缺点就会有优点——由于顺序储存结构的元素数目,元素相对位置都确定的,那么我们在存取确定位置的元素数据的时候就比较方便,直接返回就行了。

3.线性表的链式储存结构

上面我们说过,顺序结构的最大不足是插入和删除时需要移动大量元素;造成内存空间的"碎片化。那么造成这中缺点的原因是什么呢?原因就在于这个线性表中相邻两元素之间的储存位置也就有相邻关系,也就是说元素的位置是相对固定的,这样就造成了"牵一发而动全身"的尴尬局面;同时,什么是"碎片化"呢?就是说上述顺序结构中,以数组为例来说,如果我们先在内存中申请一个10位长度的数组,然后隔3个位置放一个5位长度的数组,这个时候如果再来一个8位长度的数组,那么显然不能放在前两个数组之间(他们之间只有三个空位),那只能另找地方,中间的这三个位置就空下了,久而久之类似的事情多了就发生了"碎片化"的现象。

(1)简单链表

为了解决上述两个问题,我们前辈的科学家们就发明了伟大的"链式储存结构",也就是传说中的链表(Linked List)。链表的结构有两大特点:
  ①用一组任意的储存单元储存线性表的数据元素,这组储存单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存中任意的未被占用的位置
  ②在上面的顺序数据结构中,每个数据元素只要储存数据信息就可以了,但是在链表中,由于数据元素之间的位置不是固定的,因此为了保证数据元素之间能相互找到前后的位置,每个数据元素不仅需要保存自己的数据信息,还需要保存指向下一个指针位置的信息

简单链表.png

如图,我们画出了简单链表的示意图。链表由一系列结点组成,这些结点不必再内存中相连,每个结点含有表元素(数据域)到包含该元素后继结点的链(link)(指针域)
  对于一个线性表来说,总得有头有尾,链表也不例外。我们把第一个链表储存的位置叫做"头指针",整个链表的存取就是从头指针开始的。之后的每一个结点,其实就是上一个结点的指针域指向的位置。最后一个结点由于没有后继结点,因此它的指针域为NULL。
  有时我们会在第一个指针前面加上一个头结点,头结点的数据域可以不表示任何数值,也可以储存链表长度等公共信息,头结点的指针域储存指向第一个结点的位置的信息。
  需要注意的是,头结点不是链表的必要要素,但是有了头结点,在对第一个结点之前的位置添加/删除元素时,就与其他元素的操作统一了。
(1.1)简单链表的读取
  在上面的线性表的顺序储存结构中,我们知道它的一个优点就是存取确定位置的元素比较的方便。但是对于链式储存结构来说,假设我们要读取i位置上的元素信息,但是由于我们事先并不知道i元素到底在哪里,所以我们只能从头开始找,知道找到第i个元素为止。由于这个算法的时间复杂度取决于i的位置,最坏的情况就是O(n),即元素在末尾。
  由于单链表没有定义表长,所以我们没有办法使用for循环来查找。因此这里查找算法的核心思想是"工作指针后移",也就是当前的元素查完之后,移动到下一个位置,检查下一个位置的元素,以此循环,直到找到第i个位置的元素。
  可以看到,这是链表结构的一个缺点,对于确定位置的元素查找平均时间复杂度为O(N/2)。
(1.2)简单链表的插入与删除

  当然了,有缺点就有优点,我们说过链表解决了顺序表中"插入和删除时需要移动大量元素"的缺点。也就是说,链表中插入删除元素不需要移动其他的元素,那么链表是怎么做到的呢?

单链表的删除与插入.png

我们用“.next”表示一个节点的后继指针,X.next = Y表示将X的后继指针指向Y节点。这里不得不说一下,由于Java中没有指针的概念,而是引用(关于指针和引用的区别我们这里不做过多的说明,他们本质上都是指向储存器中的一块内存地址)。在java语言描述的链表中,上面所说的“指针域”更准确的说是“引用域”,也就是说,这里的x.next实际上是X的下一个节点x->next元素的引用。说的更直白一点,我们可以简单的理解为x.next = x->next,就像我们经常做的Button mbutton = new Button();一样,在实际操作中我们处理mbutton这个引用实际上就是在处理new Button()对象。
  我们先说删除的做法,如上图所示,我们假设一个x为链表节点,他的前一个结点为x->prev,后一个结点为x->next,我们用x.next表示他的后继引用。现在我们要删除x结点,需要怎么做呢?实际上很简单,直接让前一个结点元素的后继引用指向x的下一个节点元素(向后跳过x)就可以了x->prev.next = x.next
  同理插入一个节点呢?首先我们把Node的后继节点Next的变成P的后继节点,接着将Node的后继引用指向P,用代码表示就是:P.next = Node.next; Node.next = P;。解释一下这两句代码,P.next = Node.next;实际上就是用Node.next的引用覆盖了P.next的引用,P.next的引用本来是一个空引用(刚开始还没插入链表不指向任何节点),而Node.next本来是指向Next节点的,这一步操作完之后实际上P.next这个引用就指向了Next节点;这个时候Node.next = P;这句代码中,我们将Node.next这个引用重新赋值,也就是指向了P这个节点,这样整个插入过程就完成了。
  为什么我们啰里啰嗦的为两句代码解释了一堆内容呢?就是要强调,上面两个步骤顺序是不能颠倒的!为什么呢?我们不妨颠倒顺序看一下——我们首先进行Node.next = P;这一步,开始的时候,P的引用域是空的(或者指向无关的地址),此时如果我们进行了这一步,那么Node.next这个引用就指向了P节点,这个时候我们再进行P.next = Node.next这一步就相当于P.next = p,p.next引用域指向自己,这没有任何意义。

(2)双链表(LinkedList)

上面我们说了简单链表的各种事项,但是在实际的运用中,为了我们的链表更加灵活(比如既可以工作指针后移向后查找,也可以指针向前移动查询),我们运用更多的是双向链表,即每个节点持有前面节点和后面节点的引用。java的双向链表通过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;
        }
    }

Java双链表中的结点是通过一个静态内部类定义。一个结点包含自身元素(item),该节点对后一个节点的引用(next),该节点对前一个节点的引用(prev)
(2.1)双链表的删除

双链表的删除.png

如图,我们假设在一个双向链表中有一个节点X,他的前继节点是prev,后继节点是next.现在我们展示删除节点X的源码(sources/ansroid-24/java/util/LinkedList):

    public boolean remove(Object o) {         //删除分为两种情况,一种是删链表中的null元素,一种是删正常元素
        if (o == null) {    //删除的具体操作没什么区别,都是从第一个开始,向后移动工作指针,直到找到符合条件的
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            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) {   //上面是找元素,这个方法是真正删除元素
        final E element = x.item;   //x.item表示当前的x节点
        final Node<E> next = x.next;    //x.next表示x后继引用,next同
        final Node<E> prev = x.prev;    //x.prev是x的前继引用,prev同
        ......

        if (prev == null) { //如果prev为null,则表示x为第一个结点,此时删除x的做法只需要将x的
            first = next;   //下一个节点设为第一个节点即可,first表示链表的第一节点。
        } else {
     ①      prev.next = next;   //否则的话,x为普通节点。那么只要将x的前一个结点(prev)的后继引用指向x的下一个 
            x.prev = null;      //节点就行了,也就是(向后)跳过了x节点。x的前继引用删除,断开与前面元素的联系。
        }

        if (next == null) {
            last = prev;    //如果x节点后继无人,说明他是最后一个节点。直接把前一个结点作为链表的最后一个节点就行
        } else {
     ②      next.prev = prev;   //否则x为普通节点,将x的下一个节点的前继引用指向x的前一个节点,也就是(向前)跳过x.   
            x.next = null;  //x的后继引用删除,断了x的与后面元素的联系
        }

        x.item = null;  //删除x自身
        size--;     //链表长度减1
        modCount++;
        return element;
    }

我们在在上面的源码中标出了删除一个元素所需要的两个步骤,即prev节点中原本指向X节点的后继引用,现在向后越过X,直接指向next节点①(prev.next = next;)next节点中原本指向X节点的前继引用,现在向前越过X节点,直接指向prev节点。;然后就是将X的前继引用和后继引用都设置为null,断了他与前后节点之间的联系;最后将X节点本身置为null,方便内存的释放。
(2.2)双链表的插入

双链表的添加.png

这里我们选取较为容易的,在指定位置添加一个元素的add方法分析(sources/ansroid-24/java/util/LinkedList):

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)  //size表示整个链表的长度,如果指定的索引等于链表的长度,那么就把这个元素添加到链表末尾
            linkLast(element);
        else        //否则执行linkBefore()方法,添加到末尾之前的元素前面
            linkBefore(element, node(index));
    }

这里我们看一下这个node(index)方法:

    /**
     * Returns the (non-null) Node at the specified element index.返回指定索引处的非空元素
     */
    Node<E> node(int index) {
        if (index < (size >> 1)) {  //size >> 1,表示二进制size右移一位,相当于size除以2
            Node<E> x = first;
            for (int i = 0; i < index; i++) //如果指定的索引值小于size的一般,那么从第一个元素开始,
                x = x.next;         //指针向后移动查找,一直到指定的索引处
            return x;
        } else {            //else,从最后一个元素开始,向前查找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    /**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {    //e为我们要插入的元素,succ为我们要插入索引处的元素

        final Node<E> pred = succ.prev;     //先将succ的前继引用(或者前一个结点的元素)保存在pred变量中
        final Node<E> newNode = new Node<>(pred, e, succ);  //创建一个新节点,也就是我们要插入的这个元素
                                    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值