集合

集合

1.为什么数组索引从0开始呢?假如从1开始不行吗?

  • 数组的索引从0开始相对于从1开始性能更高;

  • 因为在获取元素的时候,"内存地址"="数组的首地址"+"索引" * "数据类型的大小";

  • 假设内存地址为n,数组的首地址为a,索引为b,数据类型大小为c,那么寻址公式就是n=a+b*c;

  • 如果数组的索引从0开始,索引就刚好对应b这个变量,而如果是从1开始,b就等于索引-1,这样就需要加一次减法操作,对于CPU而言就多了一次指令,所以数组索引从0开始相对于从1开始性能更高。

2.操作数组的时间复杂度(查找)

分三种情况:

● 第一种是知道下标,可以直接通过索引访问,所以时间复杂度为O(1);

● 第二种是不知道下标,且数组没有排序,就需要遍历数组,所以时间复杂度为O(n);

● 第三种是不知道下标,但是数组排序了,可以使用二分查找,时间复杂度为O(log n)。

3.操作数组的时间复杂度(插入、删除)

分两种情况:

● 第一种是操作中间的元素,不管是插入还是删除,都需要后面的元素往后或者往前移一位,所以时间复杂度为O(n);

● 第二种是操作最后一位元素,可以直接插入或删除,所以时间复杂度为O(1);

● 所以最好的情况下是O(1),最坏的情况是O(n),平均情况下是O(n)。

4.List相关

(1).如何实现数组和List之间的转换

● 数组转List可以使用JDK中Java.util.Arrays工具类的asList方法

● List转数组,可以使用List的toArray方法。不传参数可以返回Object数组,传入初始化长度的数组对象可以返回对象数组。

(2).用Arrays.asList转换List之后,如果修改了数组内容,list受影响吗?

● 会受影响;因为他的底层使用的是Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装,最终指向的是同一个内存地址。

(3).List用toArray转数组后,如果修改了List内容,数组受影响吗?

● 不受影响;因为调用toArray以后,在底层它是进行了数组的拷贝,所以不受影响。

5.ArrayList和LinkedList的区别是什么?(高频)

数据结构:

● 底层的数据结构不一样,ArrayList是动态数组实现的,而LinkedList是双向链表实现的

查询:

● ArrayList按照下标查询的时间复杂度是O(1),而LinkedList不支持下标查询;

● 不知道下标的情况下ArrayList需要遍历,LinkedList也需要遍历,所以时间复杂度都是O(n);

新增和删除:

● ArrayList尾部插入和删除的时间复杂度是O(1),其它部分删除需要挪动数组,时间复杂度是O(n);

● 而LinkedList头和尾结点增删的时间复杂度是O(1),其它需要遍历,时间复杂度是O(n);

内存空间占用:

● ArrayList底层是数组,内存是连续的,更节省内存;

● 而LinkedList是双向链表,需要存储数据和两个指针,更占用内存;

线程安全:

● 这俩线程都是不安全的;

● 如果需要保证线程安全,有两种方案:

1.在方法内使用,局部变量是线程安全的;

2.使用线程安全的ArrayList和LinkedList,但这样性能差相对差。

6.HashMap相关

(1).HashMap的实现原理(高频)

● HashMap底层使用hash表数据结构,就是数组和链表或者红黑树;

● 在存储的时候,如果出现hash值相同的key,会有两种情况:

1.key相同,会覆盖原来的值;

2.key不相同,会把当前的key-value放入链表或红黑树里。

● 拿的时候,会直接找到Hash值对应的下标,通过判断key是否相同来找到对应的值。

(2).HashMap的JDK1.7和JDK1.8有什么区别?(高频)

● JDK1.8之前用的是拉链法,就是将数组和链表结合,没有使用红黑树;

● JDK1.8之后不仅用了数组和链表,还有红黑树,当链表长度大于8而且数组长度达到64的时候,会把链表转化为红黑树,减少搜索时间。在扩容resize()的时候,如果红黑树的结点数小于等于临界值6个,就会退化成链表。

(3).HashMap的put方法的具体流程(高频)

● JDK1.8之前用的是拉链法,就是将数组和链表结合,没有使用红黑树;

● JDK1.8之后不仅用了数组和链表,还有红黑树,当链表长度大于8而且数组长度达到64的时候,会把链表转化为红黑树,减少搜索时间。在扩容resize()的时候,如果红黑树的结点数小于等于临界值6个,就会退化成链表。

6.3HashMap的put方法的具体流程(高频)

● 在执行put方法的时候,首先会判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)

● 是就会根据键值key计算hash值得到数组索引

  1. 判断table[i]==null,条件成立,直接新建节点添加
  2. 如果table[i]==null ,不成立

● 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value

● 不相同则判断table[i] 是否为红黑树,如果是红黑树,就会直接在树中插入键值对

● 如果不是红黑树就遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value

● 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。

HashMap的扩容机制?

● 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)每次扩容的时候,都是扩容之前容量的2倍;扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中,没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置如果是红黑树,走红黑树的添加,如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

hashMap的寻址算法?

● 这个哈希方法首先计算出key的hashCode值,然后通过这个hash值右移16位后的二进制进行按位异或运算得到最后的hash值。在putValue的方法中,计算数组下标的时候使用hash值与数组长度取模得到存储数据下标的位置,hashmap为了性能更好,并没有直接采用取模的方式,而是使用了数组长度-1 得到一个值,用这个值按位与运算hash值,最终得到数组的位置

(4).HashMap的扩容机制

● 在添加元素或者是初始化的时候会调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了数组长度的0.75倍才会扩容;

● 每次扩容的时候,都是扩容之前容量的2倍;

● 扩容之后,会创建一个数组,需要把老数组中的数据挪动道新的数组中,有三种情况

1.没有hash冲突的结点,会直接使用e.hash & (newCap - 1)计算数组的索引位置

2.如果是红黑树,走红黑树的添加

3.如果是链表,需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,这个元素要么停留在原始位置,要么移动到原始位置 + 增加的数组大小这个位置上。

(5).为什么HashMap的数组长度一定是2的次幂?

● 这样计算索引效率更高,因为如果是2的n次幂可以用位与运算代替取模;

● 扩容时重新计算索引效率更高,hash & oldCap == 0 的元素留在原来的位置,否则新位置 = 旧位置 + oldCap。

(6).HashMap在1.7情况下的多线程死循环问题

● 在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环

● 比如说,现在有两个线程

线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入

线程二:也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB, 扩容后的顺序是BA,线程二执行结束。

线程一:继续执行的时候就会出现死循环的问题。

线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。当然,JDK8将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题。

end
Java

评论区

暂无评论