数据结构优化

ArrayList

Java集合:ArrayList详解

基本属性

    private static final int DEFAULT_CAPACITY = 10; //初始化大小

    private static final Object[] EMPTY_ELEMENTDATA = {}; //空数组

    transient Object[] elementData; //存放元素的数组

    private int size;  //数组当前的元素个数

    public ArrayList(int initialCapacity) { //指定长度的构造方法
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    public ArrayList() { //不带参数,默认长度的构造函数
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

如图所示:


image.png

get方法

    public E get(int index) {
        if (index >= size)  //检查是否越界
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        return (E) elementData[index];  // 直接根据index返回对应位置的元素(底层elementData是个数组)
    }

由于底层是数组实现的,判断index是否越界,然后直接返回对应索引位置的元素即可。

set方法

    public E set(int index, E element) {
        if (index >= size)  //检查是否越界
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        E oldValue = (E) elementData[index];  // 根据index获取指定位置的元素
        elementData[index] = element;  // 用传入的element替换index位置的元素
        return oldValue;   // 返回index位置原来的元素
    }

add方法

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 将modCount+1,并校验添加元素后是否需要扩容
        elementData[size++] = e;  // 在数组尾部添加元素,并将size+1
        return true;
    }

    public void add(int index, E element) {
        if (index > size || index < 0)  // 校验索引是否越界
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // 将modCount+1,并校验添加元素后是否需要扩容
        System.arraycopy(elementData, index, elementData, index + 1,  // 将index位置及之后的所有元素向右移动一个位置(为要添加的元素腾出1个位置)
                         size - index);
        elementData[index] = element;  // index位置设置为element元素
        size++;  // 元素数量+1
    }

remove方法

    public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

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

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

clear方法

    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

扩容

上文add方法在添加元素之前会先调用ensureCapacityInternal方法,主要是有两个目的:1.如果没初始化则进行初始化;2.校验添加元素后是否需要扩容。

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {  //判断是否是null数组
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);  //设置为初始容量
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;  //修改次数+1

        if (minCapacity - elementData.length > 0) //如果添加元素后大小超过容量,则扩容
            grow(minCapacity);  //扩容
    }

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;  //数组允许的最大长度

    private void grow(int minCapacity) {  //数组扩容
        // overflow-conscious code
        int oldCapacity = elementData.length;  //老的容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);  //新容量 = 老容量 + 老容量 / 2
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)  // 如果新容量比最大允许容量大
            newCapacity = hugeCapacity(minCapacity);  // 则调用hugeCapacity方法设置一个合适的容量
        // 将原数组元素拷贝到一个容量为newCapacity的新数组(使用System.arraycopy),
        // 并且将elementData设置为新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

面试遇见的问题

arraylist可以设置容量,arraylist不设置容量时,默认的容量是10。设置容量为10,和默认的有区别吗?

设置了容量之后,直接初始化了对应长度的空间。而默认的没有,在add之后会再初始化。无论是否设置长度都会进行扩容。

LinkedList

Java集合: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;
        }
    }

add方法

    public boolean add(E e) {
        linkLast(e);  //调用linkLast方法,将节点添加到尾部
        return true;
    }
    
    public void add(int index, E element) {
        checkPositionIndex(index);  //

        if (index == size)  //如果索引为size,即将element插入链表尾部
            linkLast(element);
        else  // 否则,将element插入原index位置节点的前面,即:将element插入index位置,将原index位置节点移到index+1的位置
            linkBefore(element, node(index));
    }

linkLast方法

    void linkLast(E e) {  //将e放到尾节点
        final Node<E> l = last;  //l设置为尾节点
        final Node<E> newNode = new Node<>(l, e, null); //新建一个节点,前节点为l,下个节点为null
        last = newNode;  //尾节点指向新建的node
        if (l == null)  //如果l为null,代表最初链表为null。则first为新建的node
            first = newNode;
        else  //如果l不为null。则l的next设置为newNode
            l.next = newNode;
        size++;
        modCount++;
    }

linkBefore方法

    void linkBefore(E e, Node<E> succ) {  //将e插入succ节点之前
        // assert succ != null;
        final Node<E> pred = succ.prev;  //pre为succ前一个节点
        final Node<E> newNode = new Node<>(pred, e, succ); //使用e创建一个新的节点newNode,其中prev属性为pred节点,next属性为succ节点
        succ.prev = newNode;  //将succ的prev设置为newNode
        if (pred == null)  //如果pred为null,则首节点为newNode
            first = newNode;
        else  //否则,pred的下一个节点是newNode
            pred.next = newNode;
        size++;
        modCount++;
    }

get方法

    public E get(int index) {
        checkElementIndex(index);  //检查index是否越界
        return node(index).item;
    }

    Node<E> node(int index) {
        if (index < (size >> 1)) {  //如果index小于size/2,从first往后开始查找
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {  //否则从last开始往前开始查找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

set方法

    public E set(int index, E element) {  //替换index位置节点的值为element
        checkElementIndex(index);  //检查index是否越界
        Node<E> x = node(index);  //获取index的目标节点
        E oldVal = x.item;  //保存旧值
        x.item = element;  //当x的值修改为element
        return oldVal;  //返回之前的值
    }

remove方法

    public boolean remove(Object o) {
        if (o == null) {  //如果o为空, 则遍历链表寻找item属性为空的节点, 并调用unlink方法将该节点移除
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {  //如果o不为空, 则遍历链表寻找item属性跟o相同的节点, 并调用unlink方法将该节点移除
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

    public E remove(int index) {  // 移除index位置的节点
        checkElementIndex(index);  // 检查index是否越界
        return unlink(node(index));  // 移除index位置的节点
    }

    public E remove() {
        return removeFirst();  //移除第一个节点
    }

clear方法

    public void clear() {  //清除链表的所有节点
        for (Node<E> x = first; x != null; ) {  //从头开始遍历所有节点,属性清空
            Node<E> next = x.next;
            x.item = null;
            x.next = null;
            x.prev = null;
            x = next;
        }
        first = last = null;  //头尾节点都设为null
        size = 0;
        modCount++;
    }

unlink方法

    E unlink(Node<E> x) {  //移除链表上的x节点
        // assert x != null;
        final E element = x.item;  //x节点的值
        final Node<E> next = x.next;  //下一个节点
        final Node<E> prev = x.prev;  //上一个节点

        if (prev == null) {  //如果上一个节点为null,则x为第一个节点
            first = next;  //将first移动到x.next节点即可
        } else {
            prev.next = next;  //x不是首节点,则前节点的next为x的下一个节点,跳过x
            x.prev = null;  //x的前节点为null
        }

        if (next == null) {  //如果next为null,则x为尾节点
            last = prev;  //尾节点指向x的前节点
        } else {
            next.prev = prev;  //x不是尾节点,x的后节点的pre为x的前节点,跳过x
            x.next = null;  //x的后节点为null
        }

        x.item = null;  //将x的值清空
        size--;
        modCount++;
        return element;
    }
image.png

ArrayList 和 LinkedList 的区别

  • ArrayList 底层基于动态数组实现,LinkedList 底层基于链表实现。
  • 对于按 index 索引数据(get/set方法):ArrayList 通过 index 直接定位到数组对应位置的节点,而 LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上 ArrayList 优于 LinkedList。
  • 对于随机插入和删除:ArrayList 需要移动目标节点后面的节点(使用System.arraycopy 方法移动节点),而 LinkedList 只需修改目标节点前后节点的 next 或 prev 属性即可,因此在效率上 LinkedList 优于 ArrayList。
  • 对于顺序插入和删除:由于 ArrayList 不需要移动节点,因此在效率上比 LinkedList 更好。这也是为什么在实际使用中 ArrayList 更多,因为大部分情况下我们的使用都是顺序插入。

HashMap

总结

史上最详细的 JDK 1.8 HashMap 源码解析
面试阿里,HashMap 这一篇就够了

以上两篇文章已经说的很清楚了,做一下简单的整理。

  • HashMap是基于Map接口实现的一种键值对<key,value>的存储结构,允许null值,同时非有序,非同步(非线程安全)。
  • HashMap的底层实现是数组 + 链表 + 红黑树(JDK1.8增加了红黑树部分)。
  • 它存储和查找数据时,是根据键key的hashCode的值计算出具体的存储位置。
  • HashMap最多只允许一条记录的键key为null。
  • HashMap增删改查等常规操作都有不错的执行效率,是 ArrayList 和 LinkedList 等数据结构的一种折中实现。

HashSet

基本属性

    private transient HashMap<E,Object> map;  //内部含有一个map

    private static final Object PRESENT = new Object();  //虚拟对象,用于作为value放在map里面

总结

  1. HashSet 内部使用 HashMap 的 key 存储元素,以此来保证元素不重复;
  2. HashSet 是无序的,因为 HashMap 的 key 是无序的;
  3. HashSet 中允许有一个 null 元素,因为 HashMap 允许 key 为 null;
  4. HashSet 是非线程安全的;
  5. HashSet 是没有 get()方法的;

SparseArray

基本属性

    private static final Object DELETED = new Object();
    private boolean mGarbage = false;

    private int[] mKeys;  //key的数组
    private Object[] mValues;  //value的数组
    private int mSize;  //大小

构造函数

    public SparseArray() {
        this(10);  //不带参数,初始化默认容量为10的集合
    }

    public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }

get函数

    public E get(int key) {
        return get(key, null);  //找不到则返回为null
    }

    @SuppressWarnings("unchecked")
    public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);  //数组查询

        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;  //返回默认值参数
        } else {
            return (E) mValues[i];  //返回对应的value
        }
    }

ArrayMap

ArrayMap对象的数据储存格式如图所示:

mHashes是一个记录所有key的hashcode值组成的数组,是从小到大的排序方式;
mArray是一个记录着key-value键值对所组成的数组,是mHashes大小的2倍;

image.png

深度解读ArrayMap优势与缺陷

深入剖析 Android中的 ArrayMap

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

推荐阅读更多精彩内容