算法与数据结构(1),List

算法与数据结构(1),List

算法与数据结构(2),Map

算法与数据结构(3),并发结构

前一阵子,遇到一个问题,大概的意思就是说,不使用List集合,实现对象的增加和删除,我之所要写这篇博,是因为我现在仍然不能写出满意的结果,希望你能在看过之后,有所灵感,然后实现它。

本篇,依然从我的知识和思路出发,带大家了解List数据结构。

List类族

可以说三种List均来自AbstractList,而AbstractList又实现了List接口,并继承了AbstractCollection。

ArrayList和Vector底层实现为数组,可以说这两种List内部封装了数组的操作,几乎使用了同样的算法,唯一的区别就是对多线程的支持。ArrayList没有对任何一个方法做线程同步,因此不是线程安全的。Vector中绝大部分方法都做了线程同步,是一种线程安全的实现。因此,ArrayList和Vector的性能特性相差无几,虽然从理论上来说,没有实现线程同步的ArrayList要稍好于Vector,但是我依然查看了很多其他技术文章,得出的结论是,他俩在实际生产环境中的差异并不明显,几乎可以忽略不计。

LinkedList使用了循环双向列表数据结构,由一系列表项连接而成。一个表项总是包括三个部分:元素内容,前驱表项和后驱表项。(为了节省图片宽度,严格意义上的前驱表项应该指向前方,与后驱表项方向相反,在此不做修改。)

LinkedList表项结构

下图展示了一个包含了三个元素的LinkedList,元素之间各个表项的连接关系。无论LinkedList是否为空,链表内部都有一个header表项,它既表示链表的开始,也表示链表的结尾。表项header,的后驱表项表示第一个元素,前驱表项表示链表中最后一个元素。

LinkedList表项关系

由于没能拿到libcore的源码,这里只能贴出JDK的实现,通过比较,个人感觉还是JDK的实现更好一些。

增加元素到列表尾端

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);//确保内部数组有足够的空间
    elementData[size++] = e;         //将元素添加到数组末尾
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    modCount++;                     //修改次数加一
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容到原始容量的1.5倍
    if (newCapacity - minCapacity < 0)                  //如果新容量小于最小需要的,则使用最小需要容量
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)               //如果新容量大于最大数组容量,计算出一个更庞大的新容量
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);//完成扩容,并复制数组
}

当ArrayList对容量的需求超过当前数组大小时,才需要扩容,扩容过程中,会进行大量的数组复制操作,最终调用是本地方法System.arraycopy( ),虽然本地复制效率较高,速度较快,但是,如果ArrayList,内部数组增长过快,频繁的进行扩容,add( )操作还是较慢的,但一般情况我们并不会疯狂的向ArrayList中塞数据,因此,ArrayList.add( ),效率还是不错的。

LinkedList构造函数中初始化了一个header表项,前驱表项和后驱表项均是自己,是一个只有一个元素的,闭合的链表结构。

LinkedList.add( ),将元素添加至链表末端。header元素的前驱表项正是List中最后一个元素,因此将新元素创建出来的同时增加到header之前,就相当于在List最后插入元素。

/**
 * Constructs a new empty instance of {@code LinkedList}.
 */
public LinkedList() {
    voidLink = new Link<E>(null, null, null);
    voidLink.previous = voidLink;
    voidLink.next = voidLink;
}

@Override
public boolean add(E object) {
    return addLastImpl(object);
}

private boolean addLastImpl(E object) {
    Link<E> oldLast = voidLink.previous;
    Link<E> newLink = new Link<E>(object, oldLast, voidLink);
    voidLink.previous = newLink;
    oldLast.next = newLink;
    size++;
    modCount++;
    return true;
}

虽然,LinkedList使用了链表结构,不需要考虑容量的大小,从这一点上说效率是高于ArrayList,然而每次元素的增加都需要新建一个Link对象,并进行赋值操作,如果频繁使用,依然会消耗资源,对效率产生一定影响,在JDK中(SDK中由于没能拿到libcore源码,初始容量未知)ArrayList的初始容量是10,所以绝大情况下的追加操作,ArrayList不需要频繁扩容,效率还是蛮高的。

增加元素到列表任意位置

由于实现不同,ArrayList和LinkedList在这个方法上存在很大差异,由于ArrayList是基于数组实现的,而所谓的数组就是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该位置后的所有元素都要重新排列,因此,效率会相对较低。

/**
 * Inserts the specified element at the specified position in this
 * list. Shifts the element currently at that position (if any) and
 * any subsequent elements to the right (adds one to their indices).
 *
 * @param index   index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,  size - index);
    elementData[index] = element;
    size++;
}

/**
 * A version of rangeCheck used by add and addAll.
 */
private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

可以看到每次插入操作,都会进行一次数组复制。而这个操作在增加元素到List尾端的时候是不存在的。大量的数组操作会导致系统性能低下。并且,插入的元素在List中的位置越靠前,数组充足的开销也越大。所以,使用ArrayList应尽可能的将元素插入List尾端附近,有助于提高该方法的性能。

LinkedList的插入在此时便显出优势,首先判断插入元素位置,如果处于整个List前半段,则从前向后遍历,若其位置处于后半段,则从后向前遍历,找到插入位置的元素Link,进行链表的重新连接。

/**
 * Inserts the specified object into this {@code LinkedList} at the
 * specified location. The object is inserted before any previous element at
 * the specified location. If the location is equal to the size of this
 * {@code LinkedList}, the object is added at the end.
 *
 * @param location the index at which to insert.
 * @param object   the object to add.
 * @throws IndexOutOfBoundsException if {@code location < 0 || location > size()}
 */

@Override
public void add(int location, E object) {
    if (location >= 0 && location <= size) {
        Link<E> link = voidLink;
        if (location < (size / 2)) {
            for (int i = 0; i <= location; i++) {
                link = link.next;
            }
        } else {
            for (int i = size; i > location; i--) {
                link = link.previous;
            }
        }
        Link<E> previous = link.previous;
        Link<E> newLink = new Link<E>(object, previous, link);
        previous.next = newLink;
        link.previous = newLink;
        size++;
        modCount++;
    } else {
        throw new IndexOutOfBoundsException();
    }
}

可见对LinkedList来说,在List尾端插入数据和在任意位置插入数据是一样的。并不会因为插入数据的位置靠前而导致性能的降低。所以,如果在实际生成环境中,需要频繁的在任意位置插入元素,可以考虑用LinkedList代替ArrayList。

删除任意位置元素

对ArrayList来说,remove( )add( )方法是类似的,在任意位置移除元素之后,都要进行数组的复制和重组。

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 *
 * @param index the index of the element to be removed
 * @return the element that was removed from the list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        //将删除元素所在位置,后面的所有元素往前移动一位
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);

    //最后一个位置元素置null
    elementData[--size] = null; // Let gc do its work

    return oldValue;
}

由源码可以看到,在ArrayList的每一次有效的元素删除操作之后,都要进行数组的复制和重组,并将List队列尾端的元素置null,如果删除的元素越靠前,数组重组时的开销就越大,位置越靠后,开销越小。

LinkedList中的remove( )和添加任意位置元素是类似的,首先通过循环找到要删除的元素,如果要删除的元素处于List位置前半段,则从前往后找;若其位置处于后半段,则从后往前找。因此无论要删除较为靠前或者靠后的元素都是非常高效的:但要移除List中间的元素几乎要遍历完半个List,在List拥有大量元素的情况下,效率很低。

/**
 * Removes the object at the specified location from this {@code LinkedList}.
 *
 * @param location the index of the object to remove
 * @return the removed object
 * @throws IndexOutOfBoundsException if {@code location < 0 || location >= size()}
 */
@Override
public E remove(int location) {
    if (location >= 0 && location < size) {
        Link<E> link = voidLink;
        if (location < (size / 2)) {
            for (int i = 0; i <= location; i++) {
                link = link.next;
            }
        } else {
            for (int i = size; i > location; i--) {
                link = link.previous;
            }
        }
        Link<E> previous = link.previous;
        Link<E> next = link.next;
        previous.next = next;
        next.previous = previous;
        size--;
        modCount++;
        return link.data;
    }
    throw new IndexOutOfBoundsException();
}
List遍历

List集合,三种遍历方式。

        List<String> list = null;
        String temp;

        /*迭代器循环*/
        for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
            temo= (String) iterator.next();
        }

        /*ForEach*/
        for (String string : list) {
            temp = string;
        }

        /*for循环,随机访问*/
        for (int i = 0; i < list.size(); i++) {
            temp = list.get(i);
        }

迭代器:ArrayList和LinkedList在迭代器模式中都表现出良好的性能。

ForEach:ArrayList和LinkedList在该遍历模式中效率不及迭代器,通过度娘,找到了ForEach反编译后的样子,性能降低原因是,多余的一步字符串赋值操作。

        /*ForEach反编译解析*/
        for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
            String s = (String) iterator.next();
            String s1 = s;          //多余的操作
        }

fot循环:基于数组的List都实现了RandomAccess接口,如ArrayList和Vector,没有实现的以LinkedList为代表。实现RandomAccess接口的List,当元素数量较多时,通过直接的随机访问比通过迭代的方式,可提升大约10%的性能(谢谢度娘)。如果LinkedList采用随机访问,总是要进行一次遍历查找,虽然通过双向循环链表的特性,将平均查找的次数减半,但是其遍历过程依然会消耗大量cpu资源。

片尾Tip:通过RandomAccess可知道List是否支持快速随机访问。同时,需要记住,如果程序需要使用通过下标对List进行随机访问,尽量不要使用LinkedList,ArrayList和Vector都是不错的选择。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容