数据结构与Java集合类

1. 二叉树、BST、AVL、B树、B+树、红黑树:节点存储方式、时间复杂度、特点

  1. 二叉树:节点存值
    1. 遍历方式:前(根左右)、中(左根右)、后(左右根)
    2. 时间复杂度
      1. 查找、插入、删除都是On
    3. 容易形成单向链表
  2. BST:节点存值,节点值按照左根右从小到大排序,中序遍历为递增
    1. 时间复杂度
      1. 查找、插入、删除都是On
  3. AVL:节点存值,左右子树高度不超过1
    1. 时间复杂度
      1. 查找、插入、删除都是Ologn
  4. B树:节点可以存m-1个值,叶子节点位于同一层
    1. 时间复杂度:O(logn),二分查找比较节点的值
  5. B+树:节点可以存m个值,叶子节点存数据且升序,非叶子节点存数据索引。
    1. 查找效率稳定,数据必定在叶子节点
    2. 叶子节点数据通过指针指向连接,形成升序链表。
    3. 时间复杂度:O(logn)
  6. 红黑树:节点红黑相间,根节点为黑,从任一节点出发到叶子节点都有相同数量的黑节点
    1. 红黑树不是严格的AVL树,查询效率可能比AVL树低,但是插入操作进行的旋转次数比AVL树要少,至多3次。
    2. 适合频繁插入和删除且数据量大的情况
    3. 时间复杂度:同AVL

2. 数组和链表:内存存储、时间复杂度、优缺点、使用场景

  1. 数组:在内存中连续存储,通过索引来随机获取元素,查询快增删慢。
    1. 时间复杂度:查询O1,删除On,头插On,尾插O1
    2. 大小固定,扩容要新建数组,类型单一
    3. 适用于数据量少且查询较多
  2. 链表:在内存中非连续存储,只能顺序访问,查询慢增删快
    1. 时间复杂度:查询On,删除O1,头插On,尾插O1
    2. 无初始容量,无上限,但内存消耗大,因为存在大量的指针
    3. 适合数据量少增删多

3. 集合类:List接口、Set接口、Queue接口、Map。从数据结构以及特点来讲

  1. List
    1. ArrayList:数组,查询快增删慢,线程不安全
    2. LinkedList:双向链表,查询慢增删快,线程不安全
    3. Vector:线程安全版的ArrayList
  2. Set
    1. TreeSet:红黑树,有序且唯一,底层是treemap,compareTo实现唯一,比较器实现有序
    2. HashSet:哈希表,无序且唯一,底层是hashmap,compareTo实现唯一
    3. LinkedHashSet:双向链表维护HashSet集合
  3. Queue
    1. LinkedList
    2. PriorityQueue:优先队列,大根堆小根堆
  4. Map
    1. HashMap:数组+链表+红黑树,key无序且唯一,通过hashcode和equals实现唯一
    2. TreeMap:红黑树,key有序且唯一,compareTo实现唯一,比较器实现有序
    3. LinkedHashMap:双向链表+HashMap,有序的HashMap
    4. HashTable:数组+链表,线程安全
    5. concurrentHashMap:线程安全的HashMap。

4. HashMap概述:数据结构、初始容量、扩容、树化、时间复杂度、为什么线程不安全。好文:https://zhuanlan.zhihu.com/p/76735726

1. 1.8之前

  1. 数据结构:数组+链表,数组存KV键值对,查询时间复杂度:On,最好O1。存储时间复杂度O1。
  2. 初始容量:16,加载因子0.75。扩容2倍。当threshold == 加载因子 * 当前数组长度时,扩容
  3. 节点存入方式:头插法,多线程先可能出现节点翻转时导致循环链表
  4. 数组是用来确定桶的位置,利用key的hash值与数组长度-1取模得到. 链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置通过头插法形成一条链表。

2. 1.8之后

  1. 数据结构:数组+链表+红黑树,当链表长度大于等于8时,树化。存储时间复杂度O1,查询时间复杂度降为Ologn
  2. 初始容量不变,扩容也不变。但是扩容会重新计算长度和阈值。
  3. 节点存入方式:尾插法,解决了环形链表问题,但是多线程下可能会发生数据覆盖,依旧是线程不安全
  4. hashmap的rehash采用的是位运算函数

5. HashMap的put机制:1.8前后

  1. 1.8之前:
    1. 先判断数组是否为空,如果为空,新建一个数组,长度为16
    2. 如果key为null,存入table[0],遍历table[0]数组下的链表,通过hashcode和equals判断是否存在相同的key,如果是,替换,否则,存入链表头
    3. 如果key不为null,将key的hashcode传入indexFor,计算出index位置,遍历table[index]下的链表,通过hashcode和equals判断是否存在相同的key,如果是,替换value。
    4. 如果key不重复,addEntry添加节点到链表头,判断是否需要扩容
  2. 1.8之后:调用putVal方法
    1. 先判断数组是否为空,如果为空,新建一个数组,长度为16
    2. 根据key的hashcode和数组长度-1进行与运算得到index位置,如果table[index]位置下没有Node节点,将key和value封装成Node节点存入。如果第一个节点的hash和key相同,替换value值
    3. 否则先提取节点,如果节点是树节点,调用树方法将key和value存入树中。如果是链表节点,如果遍历链表的过程中通过hashcode和equals没有发现相同的key,将key和value封装成Node节点,存入链表尾部。如果发现相同key,替换value。判断链表长度是否大于等于8,如果是,进行树化。
    4. 判断数组是否需要扩容

6. HashMap的get机制:1.8前后

  1. 1.8之前
    1. 先判断数组是否为空,如果为空,直接返回null
    2. 判断key是否为null,如果为null,调用getForNullKey查找table[0]数组,遍历链表,通过hashcode和equals查找key,找到,返回value,否则返回null
    3. 如果key不为null,调用getForEntry,将key的hashcode传入indexfor计算得到index,遍历table[index]下的链表,通过hashcode和equals查找key,找到返回value,否则返回null
  2. 1.8之后
    1. 调用getNode
      1. 如果数组为空,返回null
      2. 如果数组不为空,查找第一个Node节点是否与key相同,如果相同,直接返回
      3. 如果节点是树节点,调用getTreeNode查找
      4. 如果节点是链表节点,遍历链表,通过hashcode和equals查找key,找到返回value,否则返回null

7. HashMap的resize机制:1.8

  1. 1.8之前的resize
    1. 在put的addEntry中判断是否需要扩容,先判断数组长度是否大于threshold阈值,如果是,调用resize扩容
    2. resize先提取旧数组容量,判断旧容量是否已经到达MAXVALUE,如果是,将threshold设置为Integer.MAX_VALUE,不再扩容,直接返回。否则新建一个数组,长度为原来的两倍,调用transfer进行转移
    3. transfer遍历旧数组元素,调用rehash判断是否需要重新进行hash,调用indexFor计算index,头插法将节点转移到新数组中
  2. 1.8之后要重新规划长度和阈值,会重新排序
    1. 重新规划长度和阈值
      1. 最大容量:先判断旧容量是否到达最大容量,如果是,将threshold设置为Integer.MAX_VALUE,不再扩容,返回
      2. 两倍扩容:如果当前容量的两倍小于最大容量并且当前容量大于16,当前容量扩大两倍
      3. threshold:如果都不满足且threshold大于0,将threshold设置为容量
      4. 初始16和12:否则将长度设置为16,threshold为12
    2. 重新排序节点
      1. 如果节点为null,不处理
      2. 如果是树节点,调用split方法处理。
      3. 如果是链表节点:将链表拆分为hash值超出旧容量和未超出旧容量的情况。如果hash计算结果为0,原位置,不处理。否则将节点存入到新下标,新下标 = 旧容量+旧下标

8. 线程安全问题:环形链表和数据覆盖

  1. 1.8之前:
    1. 环形链表:transfer采用头插法,多线程下可能出现节点翻转指向同一个位置
  2. 1.8之后
    1. 尾插法可能导致数据覆盖。

9. HashMap的初始容量为什么是2的n次方:防止数组越界、hash分别更均匀、转换为与运算

  1. 防止数组越界,因为数组index位置是通过key的hash值跟数组的长度-1进行与运算得到的,如果数组长度为2的n次方,那么长度-1的二进制就是11111...的数,与运算,最大的结果也不会发生数组越界
  2. 为了使hash分布更均匀,长度-1的二进制位1111....,这种二进制数进行与运算时,出现重复数的概率低,hash冲突发生概率低
  3. 将取模运算转换为与运算,效率更高

10. hash冲突如何解决(hash碰撞是指两个不同的对象计算出同一个hashcode。hash冲突是指hashmap中桶位置重叠),hash函数类型:开放定址法、rehash、拉链法

  1. 开放定址法:线性探测,当发生hash冲突时,从该hash值的地址往下搜索,直到找到一个不冲突的hash值为止
  2. rehash:定义多个hash函数,当发生hash冲突时,轮番调用函数重新进行计算,直到不发生冲突为止。常见的hash函数有:位运算(HashMap)、乘法(String)、加法
  3. 拉链法:头插和尾插
  4. 数据结构与Java集合类_第1张图片
    image

    。紫色的为数组,绿色的为链表

11. HashTable如何解决线程安全:put和get方法加synchronized、初始容量、扩容、key-value

  1. hashtable的初始容量为11,扩容2倍+1。key和value都不能为null
  2. 通过在put和get方法加synchronized实现线程安全

12. ConcurrentHashMap如何解决线程安全问题:1.8前后

  1. hashtable主要的问题就是对整个put方法加锁,并发低。
  2. concurrentHashMap在1.8之前通过Segment分段锁 + Entry节点实现,每个分段锁维护一段数组,多个线程可以同时访问不同的分段锁,并发提高
  3. concurrentHashMap在1.8之后通过Cas + synchronized + Node节点实现,锁住Node节点,锁粒度小。当数组index位置下没有元素时,通过CAS方式存入节点

13. ConcurrentHashMap的put机制:1.8版本

  1. 先判断key和value是否为null,concurrentHashMap的key和value不能为null,如果为null,break
  2. 计算key的hash值,确定index位置。
  3. while true循环,因为可能会发生扩容和初始化数组
    1. 提取节点
    2. 如果数组为空,创建一个数组
    3. 如果数组不为空,但是table[index]位置下没有元素,将key和value封装成Node节点,CAS方式存入
    4. 如果正在发生扩容,table数组提取扩容后返回的数组
    5. 对节点上锁
      1. 遍历链表,如果在链表中找到相同的key,替换value,如果没有找到,尾插法存入链表
      2. 如果节点是树节点,putTreeVal存入
      3. 判断链表长度是否大于等于8,如果是,进行树化
  4. 容量+1,判断数组是否需要扩容

14. ConcurrentHashMap的get机制要加synchronized吗:Node节点加volatile,线程间可见

  1. 因为Node节点的value和Node节点.next都是使用volatile修饰,多线程下修改value或者增删Node节点都是线程间可见的

15. ArrayList概述:数据结构、初始容量、扩容、时间复杂度、线程不安全

  1. 数据结构:数组
  2. 初始容量:10
  3. 扩容:1.5倍,通过Arrays.copyOf()复制到新数组
  4. 线程不安全
  5. 时间复杂度:查询O1,删除On,头插On,尾插O1

16. ArrayList的grow:创建新数组,1.5倍,通过Arrays.copyOf()复制,底层调用了System.arraycopy(),扩容开销大

  1. add时先判断容量,如果容量不够,调用grow进行扩容
  2. grow会创建一个新数组,长度为1.5倍,使用Arrays.copyOf转移元素。底层调用System.arraycopy(),开销大

17. ArrayList的add:先判断是否越界,判断是否需要扩容,调用System.arraycopy()添加

  1. 先判断index是否越界,如果是,抛出越界异常
  2. 判断数组是否需要扩容
  3. 调用System.arraycopy()添加

18. ArrayList的remove:先判断是否越界、提取数组、调用fastRemove()、调用System.arraycopy()删除

  1. 先检查index是否越界,如果越界,抛出越界异常
  2. 提取数组对象
  3. 调用fastRemove,调用System.arraycopy()删除,返回删除后的数组对象

19. 如何解决ArrayList线程不安全问题:使用Vector、Collections.synchronizedList()、原子类CopyOnWriteArrayList

  1. 使用Vector,效率低
  2. 使用Collection接口的线程安全方法Collections.synchronizedList()
  3. 使用原子类CopyOnWriteArrayList,使用synchronized实现

20. Vector和ArrayList有什么区别,Vector如何解决线程不安全:就是一个线程安全的ArrayList,通过在add和get方法加synchronized解决,锁粒度大,效率低

  1. Vector就是一个线程安全的ArrayList,初始容量10,扩容2倍,也是通过Arrays.copyOf()
  2. 通过对add和get方法加synchronized实现线程安全。

21. ArrayList和LinkedList比较:数据结构、初始容量扩容、时间复杂度、线程安全、100W数据插入(头擦和尾插)

  1. 数据结构:
    1. ArrayList:数组,可以通过索引随机访问
    2. LinkedList:链表,只能顺序访问
  2. 初始容量:
    1. ArrayList初始容量10,扩容1.5倍
    2. LinkedList无
  3. 时间复杂度:
    1. ArrayList
      1. 查询O1
      2. 删除On
      3. 头插On
      4. 尾插O1
    2. LinkedList
      1. 查询On
      2. 删除O1
      3. 头插On
      4. 尾插O1
  4. 都是线程不安全
  5. 存100W数据
    1. 头插:LinkedList完胜,因为ArrayList的add和grow都要调用System.arraycopy()进行转移元素
    2. 尾插:ArrayList完胜,且效率越来越高。因为ArrayList需要扩容,前期扩容频繁,但是后期扩容越来越少。

22. ArrayList的删除方法:使用Iterator方式、倒序遍历、remove方法

  1. 正序for循环:元素前移后忽略元素,不可取
    public static void remove(ArrayList list) {
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            if (s.equals("b")) {
                list.remove(s);
            }
        }
    }
  1. foreach循环:不能用
  2. Iterator迭代器:正确方式
    public static void remove3(ArrayList list) {
        Iterator it = list.iterator();  
        while (it.hasNext()) {  
            String s = it.next();  
            if (s.equals("b")) {  
                it.remove();  
            }  
        }  
    }
  1. 倒序for循环:解决了前移忽视元素的情况
public static void remove14(List list, String target){
        for(int i = list.size() - 1; i >= 0; i--){
            String item = list.get(i);
            if(target.equals(item)){
                // List 删除元素的逻辑是将目标元素之后的元素往前移一个索引位置,最后一个元素置为 null,同时 size - 1
                list.remove(item);
            }
        }
        print(list);
    }
  1. 使用remove方法
    1. 按照索引删除:List.remove(index)
    2. 按照value值删除:List.remove(Integer.valueOf(value))

你可能感兴趣的:(数据结构与Java集合类)