数据结构

Java数据结构

常见的数据结构

HashMap

迭代方式:

  • keyset
  • entryset

比较:keysetentryset效率低 因为keyset相当于做了两次遍历 一次转化为interacor 一次用keyvalue
entryset把值和value都放在entry中 所以只遍历了一次
使用:

Map map=new HashMap();
Iterator iterator=map.keySet().iterator();
while (iterator.hasNext()){
String key=iterator.next(); //keySet()的遍历方式 相当于遍历两次 效率较低
String value=map.get(key);
}

Iterator> iterator1=map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry=iterator1.next(); //entrySet的遍历方式 效率相对较高
String key=entry.getKey();
String value=entry.getValue();
}

数组vs链表

  • 数组是通过下标找对应元素
  • 链表是通过前后关系的指针找元素

所以数组查找快,增删慢 ,链表查找慢,增删快

Map,List与Set的区别

  • List:允许重复,有序
  • Set:没有重复,无序
  • Map:区别于上述两者,它是单独的接口,并没有实现collection,键值对

补充TreeSet,TreeList,Tree**:自动排序

Java中Map,List与Set的区别

ArrayList vs LinkedList vs HashMap

  • ArrayList是线性表,相当于容量可以动态增长的数组
  • LinkedList是链表,链表随机位置插入、删除数据时比线性表快,遍历比线性表慢
  • HashMap结构的实现原理是将put进来的key-value封装成一个Entry对象存储到一个Entry数组

HashMap vs HashTable

  • 线程安全。HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而HashTable则不行)
  • 效率。由于线程安全问题,单线程下 HashtableHashMap 效率低
  • 顺序。HashMap不能保证随着时间的推移Map中的元素次序是不变的

怎么让HashMap同步?

Map m = Collections.synchronizeMap(hashMap);

JAVA5以上 ConcurrentHashMapHashTable的替代 (即线程安全的)

HashSet vs TreeSet

两者分别默认维护一个HashMapTreeMapHashMap无序,TreeMap可以实现有序
可以看出,Set其实基于Map实现

HashMap vs LinkedHashMap

前者无序,后者有序。如果想保持进入的顺序和取出的顺序一致,优先考虑LinkedHashMap

SparseArray

用来替代key为int的HashMap

对比HashMap

  • 优势
    空间上 去除了hash值的存储空间
    时间上 少了自动装箱的过程 效率提高
  • 劣势
    不适合数据量很大的数据 因为采用的二分查找(O(logn)) 大数据下明显效率低于hash查找(O(1))

并发集合有哪些?

同步集合类

  • Hashtable 已经被ConcurrentHashMap替代
  • Vector
  • 同步集合包装类,Collections.synchronizedMap()和Collections.synchronizedList()

并发集合类

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • CopyOnWriteHashSet

并发集合的实现原理

  • ConcurrentHashMap
    把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段
  • CopyOnWriteArrayList
    允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它
    如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性

ConcurrentHashMap实现原理

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成

  • Segment是一种可重入锁ReentrantLock 结构和HashMap类似 一个Segment里包含一个HashEntry数组 修改HashEntry值时必须获取Segment锁
  • HashEntry则用于存储键值对数据
ConcurrentHashMap结构.png

CopyOnWrite容器

写时复制的容器
往容器添加数据时 不直接添加 而是先将容器copy 复制出一个新的容器 然后往新的容器添加数据 数据添加完成后将原容器引用指向新的容器 这样做的目的:可以并发的读 而不需要加锁
是种读写分离的思想

为什么读的时候不需要加锁?
如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList

public boolean add(T e) {  
    // 写的时候是需要加锁的
    final ReentrantLock lock = this.lock;  
    lock.lock();  
    try {  
  
        Object[] elements = getArray();  
  
        int len = elements.length;  
  
        // 复制出新数组  
        Object[] newElements = Arrays.copyOf(elements, len + 1);  
  
        // 把新元素添加到新数组里  
        newElements[len] = e;  
  
        // 把原数组引用指向新数组  
        setArray(newElements);  
  
        return true;  
  
    } finally {  
        lock.unlock();  
    }  
}
应用场景

读多写少的并发场景 例如白名单 黑名单等

缺点
  • 内存占用
  • 数据一致性
    不能确保实时一致性

HashMap原理

HashMap是数组加链表的方式实现
流程:

HashMap流程.png

HashMap中的Hash计算和碰撞问题

HashMap的hash计算时先计算hashCode(),然后进行二次hash

// 计算二次Hash    
int hash = hash(key.hashCode());

// 通过Hash找数组索引
int i = indexFor(hash, table.length);

为什么要做二次hash?
答:尽量让key尽可能均匀的分配到数组上去 避免造成Hash堆积

/**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1); //其实在length=2^n的情况下 就是h%length取余 分组下经常使用该算法 在hashmap中默认规定数组entry长度为2^n
    }

关于h & (length-1)

由于h & (length-1)相当于取余 那么大概率上会碰撞到同一个数组上 造成hash堆积 对性能造成影响

为了避免这种性能影响 我们利用二次hash来避免这种影响
链地址法提高查找性能:

// 二次hash
static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);  // >>>无符号移位运算符  ^ 异或运算符
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

新:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap的put()解析

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with key, or
     *         null if there was no mapping for key.
     *         (A null return can also indicate that the map
     *         previously associated null with key.)
     */
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value); // ① key为null时单独做处理
        int hash = hash(key.hashCode()); 
        int i = indexFor(hash, table.length); // ② 由二次hash值获取对应数组索引
        for (Entry e = table[i]; e != null; e = e.next) { // ③ 遍历table[i]开头的链表 如果hash和key都相同 则替换为新值 返回旧值
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++; // ④ volatile申明 其他线程可以立即获取到新值 在迭代的时候起到关键作用
        addEntry(hash, key, value, i); // ⑤ 没有找到key和hash都相同的节点 那么就增加新的节点
        return null;
    }

步骤:
① key为null 调用putForNullKey

/**
     * Offloaded version of put for null keys
     */
    private V putForNullKey(V value) {
        for (Entry e = table[0]; e != null; e = e.next) { // 存储到table[0]开头的链表
            if (e.key == null) { // 发现key是null 则替换成新值 并且返回旧值
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0); //  没有发现key为null 则新增节点
        return null;
    }

② 由二次hash值获取对应数组索引
③ 遍历table[i]开头的链表 如果hash和key都相同 则替换为新值 返回旧值
④ volatile申明 其他线程可以立即获取到新值 在迭代的时候起到关键作用

HashIterator() {
            // 每次迭代会将modCount值赋给expectedModCount
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Entry nextEntry() {
        // 这里就是关键 如果不相等则表明其他线程改变了hashmap的结构 那么就抛出异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        current = e;
            return e;
        }

        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }

⑤ 没有找到key和hash都相同的节点 那么就增加新的节点

HashMap的get()解析

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry e = table[indexFor(hash, table.length)]; //由于费线程安全 此循环在扩容时易出现死循环
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

HashMap的size()解析

不是实时计算的 而是每次新增加Entry的时候 size就递增 删除的时候就递减
空间换时间的做法

HashMap的reSize()解析

大小超过临界值时 需要扩容

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) { // 大小超过最大容量就返回
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }
void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

ConcurrentHashMap实现原理

如何解决HashMap的线程安全问题?

  • HashTable 或者Collections.synchronizedMap(hashMap)
    两者都是加锁 对性能有影响
  • ConcurrentHashMap

ConcurrentHashMap原理

采用分段锁的机制 底部是数组+链表的数据结构
包含两个核心静态内部类 Segment HashEntry

  • Segment 继承 ReentrantLock 充当锁 每个Segment 维护对应的散列表的若干个桶
  • HashEntry封装映射表的键值对
  • 每个桶是由若干个HashEntry链接起来的链表
ConcurrentHashMap图解.png

注意 JDK1.8上已经抛弃分段锁的机制 利用CAS+Synchronized来保证并发更新安全

CAS(比较并替换 实现并发算法时常用的技术)
思想:三个参数 当前内存值V 旧的期望值A 即将更新的值B;当V==A时 V修改为B 并返回true 否则直接返回false

HashMap,ArrayMap,SparseArray三者对比

ArrayMap SparseArray 都是Android系统API 用于取代HashMap节省内存

  • ArrayMap 两个数组 mHashes用来保存每一个key的hash值 mArrray大小为mHashes的2倍,依次保存key和value
    关键源码:
mHashes[index] = hash;
mArray[index<<1] = key; //二分查找
mArray[(index<<1)+1] = value;

ArrayMap每存储一条信息 需要保存一个hash值 一个key值 一个value值

空间上 对比HashMap只是少了指向下一个Entry的指针
效率上 查找没有hash快 顺序插入效率高

  • SparseArray 只有key为int时才能使用 注意不是Integer 去除了 自动拆箱 的操作 也是使用二分查找

空间上 不需要Hash值的存储空间 没有next的指针引用
效率上 和ArrayMap基本相同 但是少了拆箱的过程 稍有优势

TreeMap

对查找性能要求不高 对有序性要求比较高的应用场景
是SortedMap的一种实现 key是有序的 基于红黑树实现的 不允许key-value为空

HashTable

通过hash函数将特定的值映射到特定值的数据结构 维护键与值的一一关系

HashSet and HashMap

二者有相同的实现 前者仅仅是对后者做了一层包装(适配器模式) 即 HashSet中有一个HashMap

HashSet的遍历方式:

Set set = new HashSet();
        
        set.add("aaa");
        set.add("bbb");
        set.add("ccc");
 
        //遍历
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());            
        }   
        
        //或者这样
        for (String s:set) {
            System.out.println(s);
        }

HashSet核心代码:

//HashSet是对HashMap的简单包装
public class HashSet
{
    ......
    private transient HashMap map;//HashSet里面有一个HashMap
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<>();
    }
    ......
    public boolean add(E e) {//简单的方法转换
        return map.put(e, PRESENT)==null;  //利用HashMap的key不能重复的原理来实现Set的value不重复
    }
    ......
}

HashSet和HashMap元素不能相同的实现原理

首先 了解一下equals(Object obj)hashcode()两个方法的规范

  • 重写equals(Object obj)必须重写hashcode() 因为需要确保 equals(Object obj)为true hashcode() 一定相等
  • equals(Object obj) 返回false hashcode() 不一定相等

Hash碰撞问题

解决方式:

  • 开放地址
  • 再hash
  • 链地址
  • 公共溢出区

数组和链表的区别

  • 内存存储
    ①数组从栈中分配内存
    ②链表从堆中分配内存
  • 结构
    ①数组必须要事先定义固定的长度
    ②链表动态进行存储分配 查找元素需要遍历整个链表

二叉树的深度优先遍历与广度优先遍历

  • 深度优先遍历采用栈
    对每个可能的访问路径深入到不能再深入为止 每个节点只能访问一次
    可以分为三类:
    ① 先序 根--左子树--右子树
    ② 中序 左子树--根--右子树
    ③ 后序 右子树--根--左子树

  • 广度优先遍历采用队列
    层次遍历 从上往下每一层依次访问;每一层从左往右(或者从右往左) 访问节点 访问完一层进入下一层 直到最后一个节点

Java中堆和栈的区别


  • 用于存储对象(new 创建的对象和数组) 线程共享 虚拟机启动时创建
    数组和变量在没有引用指向的时候会变成垃圾 但是仍然会占用堆内存 后续由垃圾回收器自动回收(Java占用内存大的一个重要原因)


  • 用于存储栈帧 线程私有 随线程启动创建结束而销毁
    函数中定义的基本数据类型的变量 和对象的引用变量 都是在栈内存中 但是实例变量作为对象的一部分保存在堆中
    声明一个变量时 栈中分配这个内存空间 超过这个变量的作用域时 自动释放分配的内存空间

数据结构中的堆栈


  • 优势:
    可以动态分配内存大小
    缺点:
    存取速度慢 因为动态分配内存空间


  • 优势:
    存取的速度都比堆要快,仅次于寄存器
    缺点:
    栈大小以及生命在后期必须确定 缺乏灵活性

你可能感兴趣的:(数据结构)