数据结构+java中常用的集合类

常用的数据结构:

数组:

  • 内存连续的,使用时需要初始化大小;
  • 可以通过下标来查找到数据,所以查询效率很高,时间复杂度O(1)
  • 增删效率比较低,要移动元素或者扩容,时间复杂度O(N)(还要动态扩容,不然会越界)

链表:

  • 对内存空间使用比较灵活,内存不需要连续;
  • 不支持下标查找,所以查询需要顺序遍历,时间复杂度O(n)
  • 增删效率高,最需要操作节点的前后节点的关系,不需要移动元素,时间复杂度O(n)

二叉树:

  • 二分的思想,查询的时间复杂度是O(log n);
  • 某节点的左子树节点值仅包含小于该节点值;
  • 某节点的右子树节点值仅包含大于该节点值;
  • 左右子树每个也必须是二叉查找树
  • 顺序排列

 

问题:普通二叉树可能会不平衡,甚至链化,查询效率不高,所以我们需要采取一些措施

红黑树:拆去顶端优势来达到平衡的目的。自平衡的二叉树(不是绝对平衡)

规则:

  1. 每个节点要么是红色、要么是黑色
  2. 根节点必须是黑色
  3. 每个叶子节点(NULL)是黑色
  4. 每个红色节点的两个子节点必须是黑色的(不存在两个相邻的红色节点)
  5. 任意节点到每个叶子节点的路径都包含相同数量的黑节点(严格来说是黑平衡二叉树)

实现:变色+旋转(左旋、右旋)

  左旋:节点往左旋转,即右节点变为相对根节点,该节点变为右节点的左节点。

  右旋:节点往右旋转,即左节点变为相对根节点,该节点变为右节点的左节点。

 

 

 

 

如下图:由上可知,新节点刚插入肯定是红节点,P是父节点,PP是祖父节点,S叔叔节点

 

 

 

 

具体案例可参照:https://www.cnblogs.com/deusjin/p/14620791.html

B+树:多路平衡二叉树。(Mysql里介绍)

集合

ArrayList

本质是数组,底层是基于数组的,会自动扩容(默认10,此后扩容1.5倍,即加右移一位)。

源码分析:

重要的成员变量:

// 初始容量的默认值
private static final int DEFAULT_CAPACITY = 10;  
// 空的数组,无参构造会初始化一个空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空的数组,无参构造会初始化一个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 集合中存储元素的对象
transient Object[] elementData; 
// 数组的大小
private int size;

无参构造:

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

有参构造:

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) { // 初始化内部数组,创建指定大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) { // 0时,赋值空
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

Add方法:

    public boolean add(E e) {
        // 确定容量、动态扩容数组
        ensureCapacityInternal(size + 1);
        // 元素加到内部数组里面
        elementData[size++] = e;
        return true;
    }

继续看ensureCapacityInterna方法:

    private void ensureCapacityInternal(int minCapacity) {

                   // 数组为空时,返回默认值10

        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

        }

        ensureExplicitCapacity(minCapacity);

    }

继续看ensureExplicitCapacity方法:

继续看grow方法:

 

 

 

 get()方法:

Set()方法:

 

 

 

 

Remove方法

Failfast机制:内部会维护一个modCount,一旦修改了元素,都会加一,每次遍历元素操作会比较modCount,不是预期值,则抛出返回异常,是集合类为了应对并发访问时的原子性,内部结构发生变化的一种防护措施。

LinkedList

是通过双向链表来实现的,它具有双向链表的优缺点

它的顺序访问效率高,随机访问效率低。

增删效率高

Push方法是加到头部,add加到尾部,源码也就是使用了双向链表

节点加到头部:

 

 

 

 

节点加到尾部:

 

Get方法:

 

 

 

 

Set方法:

 

 

 

Vector

       线程安全的,内部实际上是在每个方法上加了synchronized关键字,对性能有比较大的影响。不推荐使用。

         java.util.Collections下的synchronizedList方法,可以转化为同步的集合,在代码中可以灵活使用。synchronizedList内部转化为一个同步集合,实际上是使用了同步代码块来实现的。

 

 

 

HashSet

         实现了Set接口,使用哈希表来实现,实际上内部是通过HashMao来实现的

         存储的数据是无序的,不重复的,允许元素为空。

 

 

 

 

Set方法是将数据保存在内部的HashMap中,key是我们添加的内容,value就是我们定义的一个Object对象:

 

 

 

 

TreeSet

基于TreeMap的NavigableSet实现,使用元素的自然顺序来排序,或者由set提供的Comparator继续排序,具体取决于使用什么构造方法

 

 

 

 

本质是将数据保存在TreeMap中,key是内容,value是一个通用对象。

TreeMap

本质上是红黑树的实现,遵循红黑树的特点。

 

Put方法:

public V put(K key, V value) {

    Entry<K,V> t = root; // 根节点

    if (t == null) { // 根节点是null

        compare(key, key); // type (and possibly null) check



        root = new Entry<>(key, value, null);// 封装node,并设为root

        size = 1;

        modCount++;

        return null;

    }

    int cmp;

    Entry<K,V> parent;  // 父节点

    // split comparator and comparable paths

    Comparator<? super K> cpr = comparator; // 比较器

    if (cpr != null) {

        do {

           // 将root赋值给了parent

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

    }// parent就是我们要插入的节点的父节点

    // e是封装的节点

    Entry<K,V> e = new Entry<>(key, value, parent);

    if (cmp < 0)

        parent.left = e; // 插入的节点在parent左侧

    else

        parent.right = e; // 插入的节点在parent右侧

    fixAfterInsertion(e);  // 实现了红黑树的平衡

    size++;

    modCount++;

    return null;

}

 

 

fixAfterInsertion方法:(参照之前的红黑树列举的情况)

private void fixAfterInsertion(Entry<K,V> x) {

    x.color = RED; // 设置新的节点为红色

        // 循环的条件:添加的节点部位空,不是root节点,父节点为红色

    while (x != null && x != root && x.parent.color == RED) {

        // 父节点是否为祖父节点的左侧

        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {

           // 获取祖父节点节点的右侧,即叔叔节点

            Entry<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 { // 父节点是否为祖父节点的左侧

            Entry<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; // 根节点为黑色

}

 

HashMap

底层:1.7采用数组加链表

           1.8之后采用数据加链表或者数据加红黑树来实现元素的存储的

 

 

 

源码分析:

 常用的成员变量:

 

 

        

HashMap类里面有个Node<K,V>静态内部类,里面包含四个属性:hash,calue,value,next代码如下(主要看有注释的那四行,其他可以忽略)

 

 

接下来看一下put方法的源码:

    public V put(K key, V value) {

        return putVal(hash(key), key, value, false, true);

    }

 

这里的hash(key)是计算出key所对应的hash值,

右移16位的原因,减少哈希碰撞的次数,保证散列分布均匀

继续看putVal()方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

                   boolean evict) {

        Node<K,V>[] tab; // 这里的tab就是table,后三行会赋值,下面我会直接说table而不是tab     

Node<K,V> p; // p就是table[i],后面也会赋值的      

int n, i;

        if ((tab = table) == null || (n = tab.length) == 0)  // table是否为空或者长度为0

            n = (tab = resize()).length;    //满足则调用resize()方法扩容

        if ((p = tab[i = (n - 1) & hash]) == null)   // 计算出索引i,如果table[i] == null

            tab[i] = newNode(hash, key, value, null);  // 直接插入

        else {   // 如果table[i] !=null

            Node<K,V> e; K k;

            if (p.hash == hash &&

                ((k = p.key) == key || (key != null && key.equals(k))))  //判断key是否存在了

                e = p;  //满足则直接覆盖旧值

            else if (p instanceof TreeNode)   // key不存在,继续判断是否table[i]是否是TreeNode(红黑树结构)

                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  //满足则在红黑树中插入键值对

            else {  // 不是红黑树结构          // 开始遍历表,并且插入

                for (int binCount = 0; ; ++binCount) {

                    if ((e = p.next) == null) { 

                        p.next = newNode(hash, key, value, null);

                        if (binCount >= TREEIFY_THRESHOLD - 1) //如果链表长度大于等于8

                            treeifyBin(tab, hash);   // 链表转化为红黑树

                        break;

                    }

                    if (e.hash == hash &&

                        ((k = e.key) == key || (key != null && key.equals(k)))) // 如果链表中存在相同的key,直接覆盖旧值

                        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);  // 这个方法HashMap里面是空的,LinkedHashMap有实现方法,意思就是为了实现顺序插入

        return null;

    }

 

具体分析看下面的图:

 

 

 

接下来看一下gēt方法的源码:

    public V get(Object key) {

        Node<K,V> e;

        return (e = getNode(hash(key), key)) == null ? null : e.value; //指定key 通过hash函数得到key的hash值

    }

 

上面的内部getNode()方法是根据hash值,知道对应的Node,并返回。然后就可以获得到里面的value值了。

4.一些问题:

1.HashMap中的碰撞探测(collision detection)以及碰撞的解决方法:

   当两个对象的hashcode相同时,它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用LinkedList存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在LinkedList中。这两个对象就算hashcode相同,但是它们可能并不相等。 那如何获取这两个对象的值呢?当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,遍历LinkedList直到找到值对象。找到bucket位置之后,会调用keys.equals()方法去找到LinkedList中正确的节点,最终找到要找的值对象使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。

  2.解决 hash 冲突的常见方法

    a. 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

    b. 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。

   c. 再哈希法:即发生冲突时,由其他的函数再计算一次哈希值。

   d. 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。

   HashMap 就是使用链地址法来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。数组中的每一个单元都会指向一个链表,如果发生冲突,就将 put 进来的 K- V 插入到链表的尾部。

 

posted @ 2021-04-06 17:13  black_air  阅读(93)  评论(0编辑  收藏  举报