常用的数据结构分析

常用的数据结构
ArrayList:底层由数组实现,所以它在内存中是一块连续的空间

/**
两种情况,一种是直接在添加,那么它是添加在数组的最后,这个时候只需要让size++的元素填充为e
*/
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
/**
在指定的位置插入,System.arraycopy首先会将当前index到数组最后的元素后移拷贝一份,再将插入的元素插入index
*/
    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

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

所以在add的时候如果是发生在中间的情况下,那么会发生一次数据拷贝

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

remove的操作同理,取出index坐标下的对象,然后通过数据拷贝将index+1到末尾的数据向前拷贝一位,并将数组中最后一位置为null,等待GC。

        public E get(int index) {
            if (index < 0 || index >= this.size)
              throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
            return (E) ArrayList.this.elementData[offset + index];
        }

而因为ArrayList在内存中是一个连续的内存空间,所以当它声明后它的地址是已知的,如果在内部存入一个Object的对象(4个字节)那么只需要在这个地址上加4就能查询到对应的数据。
所以ArrayList在进行查询,和直接添加时效率高,但是在进行中间插入以及删除时会因为要进行一次数据拷贝,效率不高。
与ArrayList通常进行比较的是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;
        }
    }

链表结构带来的好处是,在进行数据插入和删除时,只需要修改节点的前驱和后驱指向就可以实现,并不需要进行数据的拷贝,但是如果要进行数据查询的话

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

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    //返回指定下标的对象,get的实现也是通过这个方法来进行,所以我们没有办法通过第一个节点知道第10节点的对象,必须要进行循环查询。
    Node<E> node(int index) {
        // assert isElementIndex(index);

        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;
        }
    }
    //在succ之前插入一个元素,就是把succ的前驱节点设置给e,pred的后驱指向e,succ的前驱指向e,这样就完成了数据插入的操作。删除反之。
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

所以LinkedList是一种增删效率快,但是查询慢的数据结构。
那么有没有一种数据结构是同时具有两者优点的,HashMap
HashMap在1.7之前后之后的版本是不同的实现
之前的HashMap结构是由数组➕链表构成的
之后的HashMap是由数组➕链表➕红黑树构成的

hash碰撞的出现,以及解决方法?
HashMap的默认大小-》16

  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

HashMap什么时候扩容?由什么决定的?

长度为2的次幂的意义?
减小碰撞的可能
例子:
Length1 =10
Length2= 16

H1 110 = 6
H2 111 = 7

L1 :9 = 1001
L2 :15 = 1111

分别求模
110
1001 0000

111
1001 0001

110
1111 0110

111
1111 0111
可以发现非2次幂的情况下,中间两位为0,那么它就没有计算的意义,只有高位和低位有决定作用,而在2次幂的情况下,都为1那么都参与运算,减少了hashcode的冲突可能。

加载因子:

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

阙值:0.75*16(length) = 12,就是如果是默认长度的情况下,如果数组长度》=12的话,那么就扩容。

扩容的意义:均匀分布,减小冲突

Android特有的数据结构:
SparseArray:双数组分别存储key和value,但key必须为int类型的数据,通过二分查找的方式去查询key的位置。
ArrayMap:

        mHashes[index] = hash;
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;

通过key的hash值查找在mHashes中的下标位置,并将key和value存入对应规则的indx下标对应的位置。
这里解决了SparseArray的key只能为int类型的限制,也是通过两个数组实现,两个数组分别存储的是key的Hash值,另外一个数组存储的是key和value,所以mArray的长度是mHashes的2倍,在mArray的下标也是2倍为key,2倍加1为value。
ArrayMap是如何解决Hash碰撞的:

int indexOf(Object key, int hash) {
        final int N = mSize;

        // Important fast case: if nothing is in here, nothing to look for.
        if (N == 0) {
            return ~0;
        }

        int index = binarySearchHashes(mHashes, N, hash);

        // If the hash code wasn't found, then we have no entry for this key.
        if (index < 0) {
            return index;
        }

        // If the key at the returned index matches, that's what we want.
        //通过hash获取一个下标后,在实际存储key和value的数组中拿出来跟实际的key比较一下,
        //如果一样说明就是我们需要查找的下标位置
        if (key.equals(mArray[index<<1])) {
            return index;
        }

        // Search for a matching key after the index.
        int end;
        //因为如果出现hash碰撞的情况下,会将mHashes中存储的index加1直到没有冲突为止
        //上一步获取到的key和实际的key不一样,就是出现了hash碰撞,那么在以这个index+1的起点向右查找,因为hash发生了碰撞,那么在一个范围内存储的hash是一样的,我们只需要在这个一样hash范围内查找
        for (end = index + 1; end < N && mHashes[end] == hash; end++) {
            if (key.equals(mArray[end << 1])) return end;
        }
        //向前查找
        // Search for a matching key before the index.
        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
            if (key.equals(mArray[i << 1])) return i;
        }

        // Key not found -- return negative value indicating where a
        // new entry for this key should go.  We use the end of the
        // hash chain to reduce the number of array entries that will
        // need to be copied when inserting.
        return ~end;
    }

所以相对于HashMap,这两者都是优化了内存空间的使用,因为HashMap是采用空间换时间的策略,只要到达75%的容量就会翻倍扩容,哪怕是只存储一个数据。

位运算:

  • <<: 左移运算符号: num<<1 ,相当于num乘以2
  • : 右移运算符号: num>>1 ,相当于num除以2

  • :无符号右移,忽略符号位,空位都以0补齐