java数据结构进阶篇

java数据结构进阶

先简单总结常见list、set和map,list和set都集成于Collection 集合,list有序集,能存储相同元素,set无序集,不能存储相同元素,map键值对方式存储

list下面有ArrayList、LinkedList,前者底层是以数组方式存储,后者链表方式;浅谈几个问题
增删改查效率问题
对于增删操作,前者操作比较麻烦,删除或者增加后数组会进行移位调制,后者方便,删除后直接前后修改链表指向即可;改查操作则相反,因为数组存储底层的位置是连续的查询相对较快,后者慢些;但是Linkedlist功能相对完全,提供了许多api,可以把它当作队列、堆栈进行使用;
扩容问题
ArrayList是一个动态数组,每次增加数据先判断是否需要扩容,需要就扩大其1.5倍,过程先创建新的数组,在把旧数组的元素全部copy到新数组;而linkedlist则无需扩容,直接申请内存即可

set常用的是hashset,与hashMap类似,相当与hashmap的key值存储,无序集合,扩容时有一个加载因子,当存储数据为总数组的加载因子倍,就扩容一倍;比如,加载因子0.75,当存储量大于初始容量的0.75倍就进行扩容

HashMap与ArrayMap

HashMap: 默认是一个16位的数组加横向链表,每次put元素时,会先hash算法计算key的hash值,在对数组长度求余,确定数组中的位置,如果数组的链表为空,就直接new一个Entry(hash,key,value,nextEntry)的对象装入,如果链表不为空,就出现hash碰撞,解决的办法就是先遍历单链表的整个链表,并比较hash和key值是否有相等的元素,有就直接更新,没有就new一个新的Entry添加在链表尾;除此之外,当存储的size超过总容量*加载因子倍,就会对map扩容2倍,在重新hash将旧map的数组迁移到新map中去;看源码更方便理解:

//1 put
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//2 hash算法加移位操作  减少hash冲突
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//3 加入value
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //检查hash数组是否需要扩容resize
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //与操作,确定当前值在数组中的index   
        //如果index为空直接插入 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //index不为空    
        else {
            Node<K,V> e; K k;
            //如果和index处的entry相等,替换
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果链表长度大于8了,已经成为treeNode红黑树结构,红黑树结构去插入  
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //小于8,单链表结构,依次遍历链表,有相等的替换,没有就加入尾巴    
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        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;
    }

HashMap在JDK1.8及以后的版本中引入了红黑树结构,若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

__ArrayMap:__使用两个一维数组进行存储,一个存放key的hash值,从小到大排列,一个存放key和value值,每次添加和删除有可能导致数组的元素移动,但是内存使用量较小,适合移动端,每次查找时在hash的数组里面采用二分法查找

为什么ArrayMap内存使用量相对于HashMap小?

  1. HashMap的Node节点大于存储ArrayMap的单个节点
  2. 扩若HashMap的2倍大于ArrayMap,ArrayMap默认只扩大4个
  3. 而且HashMap扩若后会对所有元素重新hash,计算元素的位置
  4. ArrayMap还有缩容机制,删除元素后,小于阈值就进行缩容处理
    但是查找、增加、删除操作ArrayMap效率较低,arraymap二分法,hashmap算法计算确定index

HashMap的碰撞问题?
put时大量的hash值求余后确定到同一个数组index就出现了hash碰撞,HashMap解决的办法:
5. 对key的hash值进行移位求余操作,将高16位右移16位于地位相亦或
6. hashmap上述介绍的key值比较

HashMap从jdk1.7到1.8为什么把链表从头插改为尾插?
最直接的原因就是为了解决链表循环问题;怎么出现以及解决的呢?
两个线程同时添加元素的,并且刚好遇到map需要扩容,需要重新hash确定数组中的位置;如果扩容前某个链表为10-2-null,扩容后,在移动链表后,正确的结果应该是2-10-null,但是两个线程同时进入都处理为10-null,也都取到2节点,这时其中一个线程先处理完结果为2-10-null,然后第二个线程也把2处理了,变为2-10,然后遍历2的next,发现是10(线程1执行的结果),然后把10的next指向2,再次遍历10的next为null,链表移动结束,现在的结果是2-10,但是2和10是双向链表,查找时变为死循环

可以参考:https://juejin.im/post/5ba457a25188255c7b168023

Map的线程同步问题

HashMap是线程同步不安全的;当然也可以用

Collections.synchronizedMap(Map<K,V> m);

使其同步安全;除此之外还有一个HashTable结构是线程安全的,用法和HashMap一样,只是同步安全的结果造成访问效率较低,同一时间只允许一个线程访问,并且HashTable在put和get都做了同步操作;为了使效率更高,我们可以使用更高级的ConcurrentHashMap,如下

ConcurrentHashMap

和HashMap结构一样,仍然是数组加链表segement、HashEntry,数组变为Segment;存取原理也和HashMap一样,通过HashCode值定位数组位置,在比较hash、key是否相等等条件,也有大于8转为红黑树结构;

ConcurrentHashMap的锁机制

首先ConcurrentHashMap的HashEntry中:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;		//volalite保证读取的是变量真实内存存放值,而不是缓存
        volatile Node<K,V> next;
}

val的属性是volatile,保证读取的是变量真实内存存放值,而不是缓存;所以在读取get的时候不必要进行同步操作;
而Segment继承于ReentrantLock重入锁;从而在数组上每个Segment独享自己的锁lock;不与其他Segment去争抢锁,保证了并发的效率,每个链表独享自己数组index的锁lock

static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

以上是jdk1.7的操作;在jdk1.8又做了相应的改进;改进主要有以下几点:

  • 链表结构改为大于8变为红黑书结构,提高访问效率O(log n)
  • 分段锁改为CAS+synchronized来插入数据
    1.8的put数据主要是:
    a. 通过HashCode定位在数组中的位置;
    b. 如果数组中位置为空,则CAS写入值
    c. 如果数组不为空,则synchronized(数组位置处的对象)来保证同步
    大致代码如下,省略非关键代码:
 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //初始化容量
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //没有hash冲突情况,跳出循环    
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果当前位置为-1,需要进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                //....链表遍历插入
            }
        }
        addCount(1L, binCount);
        return null;
    }

 private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
        	//CAS写入数据
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

CAS(compare and swap)比较交换,
do{
备份旧数据get_old;
基于旧数据构造新数据new_value;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
CAS即去内存地址取出的值与旧值相同,就用新数据更新;否则就不做任何操作;此操作为原子性操作,无需锁,提高性能

红黑树

首先他是一个二叉树,二叉树的特性:

  • 一个节点如果他的左叶子节点非空,那么左叶子节点上所有的值均小于当前节点
  • 如果他的右叶子节点非空,那么他的右叶子节点上的值均大于当前节点
  • 他的左右叶子节点下的也要满足以上两个特性

红黑树 在上面的基础上,为每个节点增加一种属性,非红即黑,并且要满足以下特性

  • 根节点为黑色
  • 如果一个节点为红色,那么他的左右子节点为黑色
  • 树尾,最终的叶子节点都是黑色
  • 任意一个节点到树尾的路径上黑色节点数是一样的

java数据结构进阶篇_第1张图片
对红黑树进行查找时,由于自身的特性,效率是比较高的,在进行插入、修改元素时还需要对树的结构重新调整,上色
TreeMap 就是以红黑树为基础封装的一个类,以下是其基本使用方法

private void testTreeMap(){
        TreeMap<String, String> map = new TreeMap<>(new MyCompartor());
        map.put("as", "dslkf");
        map.put("re", "dslertekf");
        map.put("awers", "dslkf");
        map.put("ahjgklis", "dslkdfgdfdgf");
        map.put("ass3s", "dslkf");
        map.put("a1232432s", "dslk3435f");
        map.put("a78s", "dsl3452kf");
        map.put("87as", "dsl34kf");
        map.put("aasdas", "dsl242342kf");
        map.put("as67", "dslk2423f");
        map.put("a1221312s", "dsl242kf");
        map.put("as546", "dsl5678kf");
        map.put("a12s", "dslkfghfghsf");
        map.put("a111111s", "dfgdgslkf");

        for(String key : map.keySet()){
            Log.i("j_tag", "key " + key + " value " + map.get(key));
        }
    }

    private class MyCompartor implements Comparator<String>{
        @Override
        public int compare(String o1, String o2) {
            return o1.compareTo(o2);
        }
    }

进阶

ArrayBlockingQueue

阻塞队列:先进先出、线程安全,多用于消费者-生产者模式

PriorityBlockingQueue

一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。

Vector

用数组的方式存储数据,用法等同于ArrayList,但是线程安全

java集合进行比较,获取,查找时为什么要重写hashCode和equas方法?

严格来说集合中使用了hash算法存储的要重写hashCode(hashset/hashmap/hashtable),非hashCode结构则不需要(arrayList/linkedList);并且在比较时要hashCode和equals必须同时相等时才能说明两个元素相等

从hashCode和equals两个方法单独来说,不同的对象都有可能分别相等,两者一起比较加强了比较的相等逻辑;按照正常集合比较逻辑来说,就是遍历整个集合,一个一个比较,但是这种太过于麻烦,而hashmap结构key值的存储位置和hashCode很密切,通过hashCode可以快速定位key的位置,然后在比较,提高比较效率;

而ArrayList这种结构,存储位置是数组结构,只能遍历所有一个个比较,可以不用重写hashCode,重写equals即可

List集合indexof(null)

indexof查找元素在集合List中的位置,因为List允许插入空元素,所以也可以查找null空元素;
所以返回值如果能在集合中查找到的话就返回index,也包含null,如果已经插入null,也返回null处的位置
另一种,查找不到元素,统一返回错误码-1

你可能感兴趣的:(JAVA)