数据结构与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. 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<String> 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<String> list) {
        Iterator<String> it = list.iterator();  
        while (it.hasNext()) {  
            String s = it.next();  
            if (s.equals("b")) {  
                it.remove();  
            }  
        }  
    }
  1. 倒序for循环:解决了前移忽视元素的情况
public static void remove14(List<String> 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))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343