数据结构与算法-常用数据结构必知必会

757 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

ArrayList

  • 内部维护elementData对象数组
  • 通过size记录elementData中实际存在的集合数量
  • 插入删除效率低、查找效率高

add

插入操作先判断数组是否需要扩容,然后下标size加1后对数组对应下标赋值。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

若是在指定下标插入对象,操作前同样需要判断数组是否需要扩容,然后调动Native方法System.arraycopy进行数据拷贝,最后在指定下标赋值对象。

public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    /// 对数组进行扩容,扩容长度为+1
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    /// 目标数组的index起始位置开始拷贝,目标对象起始位置在index + 1,克隆长度为index下标之后数据的长度。此过程相当于将数组index开始数据向后移位腾出index下标位置。
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    /// 最后是在index下标赋值插入对象。                 
    elementData[index] = element;
    size++;
}

remove

移除操作还是通过arraycopy对数组进行拷贝操作将移除的对象覆盖并减小数组长度。

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)
    /// 同样调用arraycopy方法将下标后的数组向前拷贝。等同于后面的数组向前移动一个位置。
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //size大小减1同时数组原先最后的下标值置为null
    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;
}

trimToSize

通过删除方法其实可以知道数组虽然删除了对象但之前通过add方法扩容数组大小实际上并没有真的变小,只是通过size标记记录当前数组中存在的对象数量。事实上需要调用此方法通过Arrays.copyOf重新创建符合实际大小的数组。

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

LinkedList

  • 双向链表集合
  • 插入效率高、查找效率低

add

LinkedList 简单插入对象无需遍历链表,只要找到表尾关联表尾的后继即可。

public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    /// 获取表尾节点临时变量
    final Node<E> l = last;
    /// 创建插入值的节点对象
    final Node<E> newNode = new Node<>(l, e, null);
    /// 表尾赋值为插入值
    last = newNode;
    /// 如果表尾为空,则表头就是插入值
    if (l == null)
        first = newNode;
    else
    /// 否则表尾的后继赋值为插入值
        l.next = newNode;
    size++;
    modCount++;
}

若是指定下标插入对象则需要先遍历链表找到插入点关联插入值。

public void add(int index, E element) {
    checkPositionIndex(index);
    /// 若插入值正好在表尾,同上add方法
    if (index == size)
        linkLast(element);
    else
    /// 若插入对象在链表之中先通过node方法获取插入点
        linkBefore(element, node(index));
}
/// 遍历链表找到插入值的后继
Node<E> node(int index) {
    // assert isElementIndex(index);
    /// size >> 1 截取链表一半,减小循环长度
    /// 插入点下标小于 size一半从表头开始遍历
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
    /// 插入点下标大于 size一半从表尾开始遍历
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
/// 前驱关联
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    /// 插入点的前驱临时变量
    final Node<E> pred = succ.prev;
    /// 创建插入对象Node
    final Node<E> newNode = new Node<>(pred, e, succ);
    /// 插入点前驱赋值为插入对象
    succ.prev = newNode;
    /// 原前驱为空则表头就是插入对象
    if (pred == null)
        first = newNode;
    else
    /// 否则前驱后继赋值为插入对象
        pred.next = newNode;
    size++;
    modCount++;
}

remove

从链表中删除对象需要遍历链表查找需要删除的对象。

  • 若删除对象object为null则遍历删除链表中出现的第一个null对象。
  • 若链表存在equals为相同对象则删除。
public boolean remove(Object o) {
    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;
}

unlink删除操作先获取item前驱和后继,判断是否为链表中首尾将前驱和后继进行拼接,将item从链表中移除。

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next; // 后继
        final Node<E> prev = x.prev; // 前驱
        // 如果前驱为空,说明删除的item为表头。first直接赋值上item的后继。
        if (prev == null) { 
            first = next;
        } else { 
        // 不为表头数据,则把后继赋值给前驱的后继,将item对象从链表断开。
            prev.next = next;
            x.prev = null;
        }
        // 后继为空,说明删除的item为表尾。last直接赋值上item的前驱。
        if (next == null) {
            last = prev;
        } else {
        // 不为表尾数据,则把前驱赋值给后继的前驱。
            next.prev = prev;
            x.next = null;
        }
        // 删除对象置空
        x.item = null;
        size--;
        modCount++;
        return element;
}

set

重新赋值将原先只设置为新的。同样通过上述提到的node获取节点对象替换item。

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

CopyOnWriteArrayList

  • 所有对数组数据操作都带synchronized
  • CopyOnWriteArrayList线程安全是ArrayList升级版
  • 总体来看CopyOnWriteArrayList操作都带悲观锁效率并不高

add

插入操作带synchronized锁,通过copyOf操作对数组长度扩容最后赋值。

public boolean add(E e) {
    synchronized (lock) {
        /// 获取数组集合临时遍历
        Object[] elements = getArray();
        int len = elements.length;
        ///拷贝数组,扩充数组长度加1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        /// 在数组末尾插入新值。
        newElements[len] = e;
        /// 重新给数组集合赋值。
        setArray(newElements);
        return true;
    }
}

指定下标位置赋值操作,同样还是通过arraycopy对数组进行拷贝和移位操作。但加锁同时有进行两次拷贝效率并不是特别高。

public void add(int index, E element) {
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException(outOfBounds(index, len));
        Object[] newElements;
        int numMoved = len - index;
        ///若在数组尾部添加同上操作
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
        ///先拷贝下标前的数组数据,再拷贝下标后的数组数据。相当于数组长度加1 移位数组腾出index下标位置
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        /// 下标index位置赋值
        newElements[index] = element;
        setArray(newElements);
    }
}

remove

public E remove(int index) {
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        /// 如果移除的是数组最后一个对象,直接通过copyOf缩小数组长度删除对象。
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
        /// 否则先创建加1的数组然后对index之外的数据进行拷贝。
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    }
}

移除对象操作同样和之前ArrayList类似,先遍历数组找到对象下标然后通过下标删除方法移除对象。

public boolean remove(Object o) {
  Object[] snapshot = getArray();
  int index = indexOf(o, snapshot, 0, snapshot.length);
  return (index < 0) ? false : remove(o, snapshot, index);
}

HashMap

  • 存储形式以由数组+链表构成,外加红黑树存储结构,无序存储
  • 数组会存在扩容的情况,若size大于threshold时执行resize()
  • 是非线程安全
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

put

public V put(K key, V value) {
    /// key入参先进行hash算法得出hash值
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfA
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /// 若数组对应值为空 直接赋值
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    /// 若
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.e
            e = p;
        /// 红黑树结构按照红黑树插入方式
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab,
        else {
        /// 
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, n
                    if (binCount >= TREEIFY_THRESHOLD - 
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null 
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

LinkedHashMap

  • 继承HashMap,采用LinkedHashMapEntry继承HashMap.Node是双向链表形态
  • LinkedHashMap是有序存储通过双向链表将每个成员对象首尾相连,HashMap则是无序存储
  • 同样是非线程安全
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
        LinkedHashMapEntry<K,V> before, after;
        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
}

HashTable

  • HashTable操作是线程安全的,所有操作使用Synchronized。
  • 和HashMap一样通过数组+单链表形式存储数据。
  • 若数组下标不足存储hash值时调用rehash()进行数组扩充。 操作带悲观锁synchronized
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }
    // Makes sure the key is not already in the hashtable.
    HashtableEntry<?,?> tab[] = table;
    ///计算出hash值
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    ///获取对应下标对象
    HashtableEntry<K,V> entry = (HashtableEntry<K,V>)tab[inde
    ///循环单链表取到entry为空时赋值
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    addEntry(hash, key, value, index);
    return null;
}
///添加entry到数组中,若大小超出闸值就执行rehash()扩容
private void addEntry(int var1, K var2, V var3, int var4) {
    ++this.modCount;
    Hashtable.Entry[] var5 = this.table;
    if (this.count >= this.threshold) {
        this.rehash();
        var5 = this.table;
        var1 = var2.hashCode();
        var4 = (var1 & 2147483647) % var5.length;
    }

    Hashtable.Entry var6 = var5[var4];
    var5[var4] = new Hashtable.Entry(var1, var2, var3, var6);
    ++this.count;
}

ConcurrentHashMap

  • ConcurrentHashMap是分段锁,可解决效率问题
  • 可以当作是HashMap高级版,HashTable升级高效版

SparseArrayCompat

HashSet

  • HashSet是HashMap的包装类。
  • 存放不重复数据对象,存储数据无序。
  • 不支持下标寻址查找对象,只能通过Iterator迭代器遍历集合。

add

HashSet将数据存储在Key上,value使用同一个PRESENT空对象。

 private static final Object PRESENT = new Object();
 public boolean add(E e) {
     return map.put(e, PRESENT)==null;
 }

Iterator

HashSet只能通过迭代器获取每个成员对象并且成员对象排序为无序。

public Iterator<E> iterator() {
 return map.keySet().iterator();
}

因此HashSet实质上是使用HashMap做嫁衣实现存储不重复数据的集合。

LinkedHashSet

  • 继承HashSet,实例化的是LinkedHashMap所以插入数据有序。
  • 拥有HashSet特点但同时数据存储有序。

可能在看LinkedHashSet类文件代码时很难察觉它的不同,明明没有实现有序插入的代码呀。看LinkedHashSet构造函数会发现在HashSet内部构造方法中实现了map实例化为LinkedHashMap。这也只有是在LinkedHashSet时才会实例化LinkedHashMap。

public LinkedHashSet(int var1, float var2) {
     super(var1, var2, true);
}
HashSet(int var1, float var2, boolean var3) {
        this.map = new LinkedHashMap(var1, var2);
}

TreeMap

  • TreeMap是以key-Value存储的红黑树数据结构。
  • 每个节点都有他的左子节点,右子节点以及父节点,除了根节点只有左子节点和右子节点。
  • TreeMap通过比较器比较Key值来计算出Value在树中的位置并且Key是唯一的。

put

 public V put(K key, V value) {
        TreeMapEntry<K,V> t = root;
        ///根结点不存在则创建根节点
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new TreeMapEntry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        TreeMapEntry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        // 若存在比较器则通过比较
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        /// 插入
        TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

由于TreeMap是红黑树,节点添加中重要的一点是在插入节点后需要对红黑树进行平衡操作。

    private void fixAfterInsertion(TreeMapEntry<K,V> x) {
        x.color = RED;

        while (x != null && x != root && x.parent.color == RED) {
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                TreeMapEntry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }

get

获取节点值的过程主要通过比较器计算比较Key值大小找到节点存储位置。

public V get(Object key) {
        TreeMapEntry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
}
final TreeMapEntry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    /// 若存在比较器则通过比较器获取节点
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
    /// 若Key是比较器则通过Key遍历比较获取节点值
        Comparable<? super K> k = (Comparable<? super K>) key;
    TreeMapEntry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}
/// 比较器遍历比较与Key为比较器过程相同
final TreeMapEntry<K,V> getEntryUsingComparator(Object key) {
    @SuppressWarnings("unchecked")
        K k = (K) key;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        TreeMapEntry<K,V> p = root;
        while (p != null) {
            int cmp = cpr.compare(k, p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
    }
    return null;
}

TreeMap详解

TreeSet

  • 内部使用NavigableMap<E,Object>存储成员对象
  • 和HashSet一样存储不重复的数据
  • 非线程安全

add

add方法和HashSet相同将存储对象保存在map的key上,不同的是TreeSet使用的是NavigableMap

public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}

TreeSet详解