集合等数据结构-01

一、顺序结构

1.如何判断链表中有一个环

https://blog.csdn.net/mucaoyx/article/details/81395782

2. ArrayList和LinkedList区别

  • 首先,他们的底层数据结构不同。ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
  • 由于底层数据结构不同,他们所适用的场景也不同,Araylist更适合随机查找,Linkedlist更适合删除和添加,查询、添加、删除的时间复杂度不同3
  • .另外Arraylist和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以Linkedlist还可以当做队列来使用
  • ArrayList需要扩容机制int newCapacity = oldCapacity + (oldCapacity >> 1);新的长度为1.5倍

3.说一下HashMap的Put方法先说HashMap的Put方法的大体流程:

  • 根据Key通过哈希算法与与运算得出数组下标
  • 如果数组下标位置元素为空,则将key和value封装为Entry对象((JDK1.7中是Entry对象,JDK1.8中是Node对象)并放入该位置
  • 如果数组下标位置元素不为空,则要分情况讨论
    • a.如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
    • b.如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node
      • i如果是红黑树Node,则将key和value封装为一个红黑树节点并(便利同时)添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key ,如果存在则更新value
      • iüi如果此位置上的Node对象是链表节点,则将key和value封装为一个铠表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前Kkey,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果超过了8,那么则会将该链表转成红黑树
    • l.将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法

二、树

2.0 二叉查找树

二叉查找树(BST)具备什么特性呢?

1.左子树上所有结点的值均小于或等于它的根结点的值。

2.右子树上所有结点的值均大于或等于它的根结点的值。

3.左、右子树也分别为二叉排序树。

利用二分查找思想,查找次数最大为树的深度。

但他也有劣势:如果元素递增,会转变为链表

为了解决二叉查找树多次插入后不平衡问题,出现了红黑树。

2.1 红黑树的特性和优势

关于红黑树很容易看懂:https://blog.csdn.net/qq_36610462/article/details/83277524

特性

红黑树是一种自平衡的二叉查找树

1.节点是红色或黑色。

2.根节点是黑色。

3.每个叶子节点都是黑色的空节点(NIL节点)。

4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

插入过程

  1. 在树中搜索插入点
  2. * 新结点将替代某个已经存在的空结点,并且将拥有两个作为子结点的空结点
  3. * 新结点标记为红色,其父结点的颜色根据红黑树的定义确定;如果需要,对树作调整

优势

查找次数最大为树的深度

  • 自平衡。红黑树从根到叶子的最长路径不会超过最短路径的2倍,解决了二叉查找树容易不平衡的缺陷(在某些情况下会退化成一个线性结构),提高了读取性能(树越平衡,读取性能就越好)。
  • 虽然AVL(二叉查找树)树具有更高的读取性能(因为平衡性更好),但是当插入或删除节点时,AVL树要复杂很多,红黑树在插入或删除节点方面具有更高的效率

劣势

  • 内部插入新节点时,所做的操作更复杂,时间复杂度更高,查找复杂度:因此查找是O(lg(n))

  • 数据存储不是连续的,在基于磁盘的I/O中,与连续块读取相比,寻找成本较高。

2.2 红黑树在什么情况下需要变色,什么情况下需要旋转

左旋转:以某个结点作为支点,其右子树变为支点的父节点,右子树的左节点变为支点的右节点

右旋转:以某一个节点作为节点,其位置被左节点替代,左节点的右子节点变为员节点的左子节点。

在红黑二叉树中插入节点或删除节点后,如果破坏了红黑树的规则(也就是上述的特性,主要是插入节点的父节点是红色。具体如下代码区),则需要对修改后的红黑树进行调整,使其重新符合红黑树的规则。

首先是变色(往往需要多次变色,一次改变一个节点的颜色),

当变色无能为力时,就使用旋转,

旋转一次之后,然后再继续多次变色,

如此反复循环,直到修改后的红黑树重新符合规则。

/*火
    *插入后修复红黑树平衡的方法
    |---情景1:红黑树为空树,将跟节点染色为黑色
    |—--情景2:插入节点的key已经存在,不需要处理
    |---情景3:插入节点的父节点为黑色,因为你所插入的路径,黑色节点没有变化,所以红黑树依然平衡,所以不需要处理。
    情景4需要咱们去处理
    |---情景4:插入节点的父节点为红色
        |---情录4.1:叔叔节点存在,并且为红色(父-叔双红),将爸爸和叔叔染色为黑色,将爷爷染色为红色,并且再以爷爷节点为当前节点进行下一轮处理
        |—--情景4.2:叔叔节点不存在,或者为黑色,父节点为爷爷节点的左子树
            |---情景4.2.1:插入节点为其父节点的左子节点(LL情况)﹐将爸爸染色为黑色,将爷爷染色为红色,然后以爷爷节点右旋,就完成了
            |---情景4.2.2:插入节点为其父节点的右子节点(LR情况)﹐
                    以爸爸节点进行一次左旋,得到LL双红的情景(4.2.1),然后指定爸爸节点为当前节点进行下一轮处理
        l---情景4.3:叔叔节点不存在,或者为黑色,父节点为爷爷节点的右子树
            |---情景4.3.1:插入节点为其父节点的右子节点(RR情况),将爸爸染色为黑色,将爷爷染色为红色,然后以爷爷节点左旋,就完成
            |---情景4.3.2:插入节点为其父节点的左子节点(RL情况),
                    以爸爸节点进行一次右旋,得到RR双红的情景(4.3.1),然后指定爸爸节点为当前节点进行下一轮处理
    */

2.3 红黑树使用场景、什么地方使用到了它

JDK的TreeMap和TreeSet底层的数据结构就是使用红黑树,JDK8中HashMap也是使用了红黑树。

2.4 手写红黑树

  1. 创建RBTree,定义颜色
  2. 创建RBNode
  3. 辅助方法定义:parentOf(node),isRed(node),isBlock(node),setRed(node),setBlock(node),inOrderPrint()
  4. 左旋定义方法:leftRotate(node)
  5. 右旋方法定义:rightRotate(node)
  6. 公开插入接口方法定义:insert(K key,V value)
  7. 内部插入接口方法定义:insert(RBNode node)
  8. 修正插入导致红黑树失衡的方法定义:insertFixUp(RBNode node)
  9. 测试红黑树正确性

使用hash作为索引

使用二叉树或红黑树作为索引

缺点;
无论是二叉树还是红黑树,都会因为树的深度过深而造成io次数变
多,影响数据读取的效率

B-树

自平衡的树

B-树中所有结点中孩子结点个数的最大值成为B-树的阶,通常用m表示,从查找效率考虑,一般要求m>=3。一棵m阶B-树或者是一棵空树,或者是满足以下条件的m叉树。(m一般有分块大小决定)

  • 每个结点最多有m个分支(子树);而最少分支数要看是否为根结点,如果是根结点且不是叶子结点,则至少要有两个分支,非根非叶结点至少有ceil(m/2)个分支,这里ceil代表向上取整

  • 每个中间节点都包含1到m-1个数据

  • 如果一个结点有n-1个关键字,那么该结点有n个分支。这n-1个关键字按照递增顺序排列

  • 所有的叶子结点都位于同一层。

  • 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

漫画说B-树:https://www.sohu.com/a/154640931_478315

B-树也可以用作索引,但是当数据量很大,或是每条数据占用内存很大(一个块一般4k),会增加树深度,增加I/O,降低效率

MongoDB使用B-树作为索引

B+树

  • B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别
  • B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;(不保存数据,只对数据进行一个分区)
  • B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
  • B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
  • 有k个子树的中间节点包含有k个元素(B树中是k-1个元素)
  • 中间结点的元素是子树的最大或最小值

  • B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;

2、B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;

3、B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
4、B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。

B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。

三、集合介绍

在这里插入图片描述

1. Set

  • HashSet: 基于 HashMap 实现的,底层采用 HashMap 来保存元素(不保证有序,唯一)
  • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
  • TreeSet: 红黑树,自平衡的排序二叉树(可实现自然排序,例如 a-z)

2. Map

在这里插入图片描述

  • HashMap作为编程的首选项,速度最快;JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 存储元素的主体,链表则是主要为了解决哈希冲突而存在的,即 “拉链法” 解决冲突。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(哈希表对键进行散列,Map结构即映射表存放键值对)
  • LinkedHashMap :LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得键值对的插入顺序以及访问顺序等逻辑可以得以实现。取“键值对”的顺序是其插入的顺序,速度比HashMap慢一点,但是遍历迭代的速度更快;
  • TreeMap 基于红黑树的实现(平衡二叉排序树),所得到的结果可以经过自定义的排序类进行排序,含有获取子树的方法;
  • ConcurrentHashMap 线程安全的Map;
  • Hashtable: 数组 + 链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的

四、扩容

相比于之前的版本,JDK.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前款组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

五、面试题

1. List 和Set区别以及底层数据结构

在这里插入图片描述

2. 为何 Map 接口不继承 Collection 接口?

map是键值对,而Colleciton是数组,数据结构不同,包含的操作不同,继承了完全没有好处,违反了接口隔离原则,所以不能继承。

3. 为什么HashMap的长度为2的n次幂

https://blog.csdn.net/sidihuo/article/details/78489820

5.为何 Collection 不从 Cloneable 和 Serializable 接口继承?

Collection 接口指定一组对象,对象即为它的元素。

如何维护这些元素由 Collection 的具体实现决定。例如,一些如 List 的 Collection 实现允许重复的元素,而其它的如 Set 就不允许。
很多 Collection 实现有一个公有的 clone 方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为 Collection 是一个抽象表现,重要的是实现。
当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制,特定的实现应该决定它是否可以被克隆和序列化

6.Collection 和 Collections 的区别?

Collection ,是集合类的上级接口,继承与他的接口主要有 Set 和List 。
Collections ,是针对集合类的一个工具类,它提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

7. Collections和Comparable关系

Comparator 强行对某个对象collection进行整体排序的比较函数,可以将Comparator传递给Collections.sort或Arrays.sort。

由于Collections中的sort()是对int类型进行排序的,所以当我们需要对一些不仅仅是int类型的对象进行排序时,可以使其重写Comparable,返回0、-1、1,以此来实现排序。

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestList {
    public static void main(String[] args) {
        List<Stu> list = new ArrayList<>();
        list.add(new Stu(12,"小红"));
        list.add(new Stu(13,"小黑"));
        list.add(new Stu(11,"小白"));
        list.forEach(stu -> {
            System.out.println(stu);
        });

        Collections.sort(list);
        list.forEach(stu -> {
            System.out.println(stu);
        });
    }

    static class Stu implements Serializable, Comparable{
        int age;
        String name;

        @Override
        public String toString() {
            return "Stu{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }

        public Stu (int age, String name) {
            this.age = age;
            this.name = name;
        }

        @Override
        public int compareTo(Object o) {
            Stu stu = (Stu)o;
            if(this.age>stu.age) {
                return 1;
            }
            if(this.age<stu.age) {
                return -1;
            }
            return 0;
        }
    }
}

8. 对迭代器(Interator)

Iterator 提供了遍历及操作集合元素的接口,而 Collection接口实现 Iterable 接口,也就是说,每个集合都通过实现Iterable 接口中 iterator() 方法返回 Iterator 接口的实例,然后对集合的元素进行迭代操作。

有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过 Iterator接口中的 remove() 方法进行删除,同理想要增加元素,就可以使用 ListIterator 的 add 方法 (ListIterator 拥有 add、set、remove 方法,Iterator 拥有 remove 方法)

9. Iterator 和 ListIterator 的区别是什么?

  • Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历 List。
  • ListIterator 实现了 Iterator 接口,并包含其他的功能。比如:增加元素,替换元素,获取前一个和后一个元素的索引等等。
  • Iterator只能使用 next() 顺序的向后遍历,ListIterator则向前 previous()和向后 next() 遍历都可以
    • 还有一个额外的功能,ListIterator可以使用 nextIndex() 和previousIndex() 取得当前游标位置的前后index位置,Iterator没有此功能

可参考:Java - Iterator和ListIterator

10. HashTable

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

11. HashMap和HashTable区别

  • Hashtable 继承 Dictionary ,HashMap 继承的是 的 AbstractMap。

    • public class HashMap<K,V> extends AbstractMap<K,V>
          implements Map<K,V>, Cloneable, Serializable {
      
  • 2、HashMap 去掉了 Hashtable 的 contains 方法,但是加上了 containsValue 和 containsKey 方法。

  • 3、HashMap 允许空键值,而 Hashtable 不允许。

  • 4、HashTable 是同步的,而 HashMap 是非同步的,效率上比 HashTable 要高。也因此,HashMap 更适合于单线程环境,而 HashTable 适合于多线程环境。

  • 5、HashMap 的迭代器(Iterator)是 fail-fast 迭代器,HashTable的 enumerator 迭代器不是 fail-fast 的。

  • 6、HashTable 中数组默认大小是 11 ,扩容方法是 old * 2 + 1 ,HashMap 默认大小是 16 ,扩容每次为 2 的指数大小。
    一般现在不建议用 HashTable 。主要原因是两点:

一是,HashTable 是遗留类,内部实现很多没优化和冗余。
二是,即使在多线程环境下,现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用 Hashtable **

12. ConcurrentHashMap

12.2 ConcurrentHashMap的分段锁和使用场景

12.3 concurrentHashmap 在1.7和1.8的区别

12.4 ConcurrentHashMap扩容机制

  • 1.7版本
    • 1.7版本的ConcurrentHashMap是基于Segment分段实现的
    • 每个Segment相对于一个小型的HashMap
    • 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
    • 先生成新的数组,然后转移元素到新数组中
    • 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
  • 1.8版本
    • 1.1.8版本的ConcurrentHashMap不再基于Segment实现
    • 2.当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
    • 3.如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容(先判断链表长度是否大于等于8,如果大于等于,判断数组长度是否小于64,如果小于先扩容数组长度,如等于64,则对链表进行红黑树的转换)
    • ConcurrentHashMap是支持多个线程同时扩容的
    • 扩容之前也先生成一个新的数组
    • 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

12.5 ConcurrentHashMap同步的实现原理

13 HashMap 和 ConcurrentHashMap 的区别?

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable 

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

ConcurrentHashMap 是线程安全的 HashMap 的实现。主要区别如下:

concurrentHashMap对整个集合进行分割分段,然后在每一个分段上使用lock锁进行保护,并且仅对修改操作加锁。相对于HashTable的synchronized来说锁粒度更精细。s
HashMap没有锁,不是线程安全的。当然可以使用Collections的synchronizedMap(Map<K,V> m) 来获取线程安全,但这个方法也是在对象上加锁,多线程中对其对象的所有操作都需要获得锁,因此性能非常低

参考:https://blog.csdn.net/weixin_43718267/article/details/89419899

14. ConcurrentHashMap 和 Hashtable 的区别

HashTable 虽然也满足线程安全,但是类似 Vector, 是一个Java遗留类,基本不做使用。想保证线程安全,可以考虑使用 ConcurrentHashMap。

数据结构:JDK 1.7 中,ConcurrentHashMap 底层采用分段数组 + 链表实现,在 JDK 1.8 中,ConcurrentHashMap 中的数据结构与 HashMap 一致,都是数组 + 链表或红黑树。而 Hashtable 采用的是数组 + 链表的形式(数组为主体,链表用来解决哈希冲突)

线程安全:ConcurrentHashMap 在 JDK 1.7 的时候,有一个分段锁的概念,也就是对整个数组进行分割开来(这就是 Segment 的概念),每一把锁,只负责整个锁分段中的一部分,而如果多线程访问不同数据段的数据,锁的竞争也就不存在了,访问并发率也因此提高。而在 JDK 1.8 的时候,直接用 Node 数组 + 链表或红黑树实现,通过 synchronized(JDK 1.6 后优化了很多) 和 CAS 进行并发的控制。Hashtable 就是用一把锁 synchronized 来保证线程安全,效率不是很高,多线程下,很可能会陷入阻塞轮询状态。

  • 注:虽然 JDK 1.8 的源码中还能看到 Segment ,但是主要也只是为了兼容旧版本了

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ConcurrentHashMap 版本差异和实现原理有这个:https://blog.csdn.net/jaryle/article/details/92824994

15. jdk1.7到jdk1.8 HashMap发生了什么变化?

  • 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
  • 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾播法,因为1.8中插入key和nvalue时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
  • 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源

16. Array 和 ArrayList 有何区别?什么时候更适合用 Array?

Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象。
Array 是指定大小的,而 ArrayList 大小是固定的,可自动扩容。
Array 没有提供 ArrayList 那么多功能,比如 addAll、removeAll 和 iterator 等。
尽管 ArrayList 明显是更好的选择,但也有些时候 Array 比较好用,比如下面的三种情况。

1、如果列表的大小已经指定,大部分情况下是存储和遍历它们
2、对于遍历基本数据类型,尽管 Collections 使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢。
3、如果你要使用多维数组,使用 [][] 比 List 会方便

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值