常见的集合及数据机构
集合分为Collection和Map两个接口。Collection用于存放单个元素,Map存放k-v键值对。
Collection下面有List、Set、Queue三个子接口。
List存放有序、可重复元素。
Set存放无序、不可重复元素。
Queue有排队规则,存放有序、可重复元素。
List
- Arraylist: Object[] 数组
- Vector: Object[] 数组
- LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
- HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet:LinkedHashSet 是 HashSet 的子类,内部是通过 LinkedHashMap 来实现
- TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
Queue
- PriorityQueue:Object[] 数组来实现二叉堆
- ArrayQueue:Object[] 数组 + 双指针
Map
- HashMap: JDK1.8之前,由数组+链表实现,通过拉链法(链表)解决哈希冲突。 JDK1.8之后,由数组+链表+红黑树实现。链表长度大于8,则把链表转换成红黑树(转红黑树之前会先判断数组长度小于64,则先对数组扩容)
- LinkedHashMap:继承自HashMap,数组+链表+红黑树+双向链表,保持键值对的插入顺序。
- Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树),比HashMap多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力
ArrayList和Array的区别?
1、数组长度:ArrayList基于动态数组,有扩容机制,长度可变。Array在初始化时指定长度,不可变。
2、操作元素:ArrayList提供了内置api如add remove来操作元素。Array只能通过下标方式访问元素。
3、存储元素的类型:ArrayList只能存对象类型(Null也可以但尽量不要),Array可以是任意类型。
4、泛型:ArrayList支持泛型,Array不支持。
ArrayList扩容机制
ArrayList底层由动态数组实现。
初始化时,ArrayList里是一个空数组。当添加第一个元素时,扩容返回最小长度10。添加到第11个元素时,进行第二次扩容。
当前的数组长度小于最小容量时进行扩容,默认每次扩容时扩容新的容量会是原容量的1.5倍。新容量=旧容量右移一位(相当于除于2)+旧容量。如果还是不够,则新数组长度为最小容量。
扩容时调用Array.copyOf方法,把旧数组中的内容复制到新数组当中。
ArrayList和LinkedList的异同?
1、线程安全:ArrayList都是同步的,都不保证线程安全。
2、底层数据结构:ArrayList底层是用Object[]数组存,LinkedList底层是双向链表。
3、插入或删除元素时的时间复杂度:
ArrayList:
头部或指定位置插入或删除:要把元素依次往后移,时间复杂度o(n)
尾部插入或删除:o(1)
LinkedList:
头部或尾部插入删除:直接修改前后指针,o(1)
指定位置插入删除:先移动到指定位置,o(n),再修改指针
4、内存空间
ArrayList空间占用主要是要预留空间,LinkedList空间占用主要是要存前驱和后继指针。
HashMap和HashTable的区别?
1、线程安全:hashMap线程不安全,hashTable内部用synchronized修饰方法保证安全(但效率低)。
2、对Null的支持:HashMap支持Null键,但只能有一个,支持多个Null值;hashTable不支持Null的键或者值,报空指针异常。
3、初始化和扩容机制不同:
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,
Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
HashMap的长度为什么是2的整数次幂?
HashMap 通过 key 的 hashcode 经过扰动函数(HashMap的hash方法)处理过后得到 hash 值,然后通过 (数组长度 - 1) & hash (按位与运算)判断当前元素存放的位置。
如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
hash值的范围是很大的,不能在内存中直接存一个40亿长度的数组,可以用取模的方法来取对应下标。在数组长度为2的整数次幂时,hash%length==hash&(length-1),位运算的效率比余数更高。
HashMap为什么线程不安全?(待。。。)
JDK1.7之前,并发进行扩容时,
ConcurrentHashMap怎么实现线程安全?
JDK1.7之前,由segment数组+HashEntry数组+链表实现。
每个segment对应一个类似HashMap的结构,HashEntry可扩容,由链表解决哈希冲突。segment数组不可扩容,长度16,即最多支持16个线程操作,每次操作时给segment加锁。
JDK1.8之后,由node数组+链表/红黑树实现。
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,不会影响其他 Node 的读写,效率大幅提升。
解决hash冲突的方法?
- 开放定址法 ,当关键字key的哈希地址p =H(key)出现冲突时,以p为基础,再次计算hash,p1=H(p),若p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。缺点是占空间大,且删除节点时只能做标记,不能真正删除。
- 再哈希法 ,提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值。虽然不易堆积,但增加了计算时间。
- 拉链法 ,发生哈希冲突时,把冲突节点用链表连接。拉链法适用于经常进行插入和删除的情况。(HashMap用的方法)
- 建立公共溢出区,将哈希表分为基本表和溢出表两部分,和基本表发生冲突的元素,填入溢出表。