1.ArrayList与LinkedList
1)是否保证线程安全:
ArrayList与LinkedList都是不同步的,都不保证线程安全
2)底层数据结构:
ArrayList底层使用object数组
LinkedList底层使用双向链表数据结构
3)插入和删除是否受元素位置的影响:
ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响
LinkedList采用链表存储,所以插入和删除元素的时间复杂度不受元素位置的影响
4)是否支持快速随机访问
ArrayList支持高效的的随机元素访问
LinkedList不支持高效的随机元素访问
快速随机访问:通过元素的序号快速获取元素对象
5)内存空间占用
ArrayList的空间浪费主要体现在list列表结尾会预留一定的容量空间
LinkedList的空间浪费体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)
RandomAccess:
public interface RandomAccess{}
//标识实现这个接口的类具有随机访问功能
ArrayList实现了RandomAccess接口,LinkedList没有实现
ArrayList底层是数组,数组天然支持随机访问,时间复杂度为O(1),所以称为快速随机访问
LinkedList底层是链表,链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为O(n),所以不支持快速随机访问
list的遍历方式选择:
实现了RandomAccess接口的list,优先选择普通for循环,其次是foreach(底层大多数是iterator实现的)
未实现RandomAccess接口的list,优先选择iterator遍历,大size的数据不要用普通for循环
补充:数组和链表的常用操作时间复杂度
无序数组
操作 | 时间复杂度 |
---|---|
查询 | O(1) |
插入(空间充足) | O(1) |
插入(空间不足) | O(n)+O(1)=O(n) |
删除(末尾元素) | O(1) |
删除(非末尾元素且元素个数>1) | O(1)+O(n)=O(n) |
1.查询:通过index直接定位,即O(1)
2.插入分为以下两种情况:
2.1 空间充足:无序数组不需要考虑插入位置,直接插入到末尾,即O(1)
2.2 空间不足:如果空间不足,则需要将整个数组移动到另一个空间,再添加元素,即O(n)
3.删除分为以下两种情况:
3.1 删除末尾元素:删除末尾不需要考虑移动元素位置,即O(1)
3.2 非末尾元素且元素个数>1:删除操作O(1)+移动元素位置O(n)=O(n)
有序数组
操作 | 时间复杂度 |
---|---|
查询 | O(1) |
插入(基于二分查找法) | O(log2n)+O(n)=O(n) |
插入(基于顺序查找法) | O(n)+O(n)=O(n) |
删除(末尾元素) | O(1) |
删除(非末尾元素且元素个数>1) | O(1)+O(n)=O(n) |
1.查询:通过index直接定位,故时间复杂度为O(1)
2.插入有序数组的插入需要考虑插入位置,即分下列两种情况查询插入位置:
2.1 二分查找法
二分查找法的时间复杂度为O(log2n),移动元素位置的时间复杂度为O(n),即O(log2n)+O(n)=O(n)
2.2 顺序查找法
顺序查找法查找的时间复杂度为O(n),移动元素位置的时间复杂度为O(n),即O(n)+O(n)=O(n)
ps:如果数组本身无序,还需要进行排序的话,使用冒泡排序复杂度达到o(n^2)
3.删除分为以下两种情况:
3.1 删除末尾元素:删除末尾不需要考虑移动元素位置,即O(1)
3.2 非末尾元素且元素个数>1:删除操作O(1)+移动元素位置O(n)=O(n)
链表(单向链表)
有序链表(失去了链表插入快的特性)
操作 | 时间复杂度 |
---|---|
查询 | O(n) |
插入 | O(n) |
删除 | O(n)+O(1)=O(n) |
1.查询:依次比较,最好情况下第一个就是即o(1),最坏情况o(n),取最坏情况o(n)
2.插入:有序链表的插入需要先查询到该插入的位置,且链表不能使用二分查找,即o(n)
ps:可以考虑使用跳跃表来达到o(logn)的时间复杂度,但是其空间复杂度达到了o(n),也是接受的
3.删除:和插入一样,需要先去找到要删除的元素,查询元素位置O(n)+删除操作O(1)=O(n)
无序链表
操作 | 时间复杂度 |
---|---|
查询 | O(n) |
插入 | O(1) |
删除 | O(n)+O(1)=O(n) |
1.查询:同有序链表查询,取最坏情况o(n)
2.插入:不需要考虑插入位置,即o(1)
3.删除:同有序链表一样,即查询元素位置O(n)+删除操作O(1)=O(n)
2.ArrayList与Vector
vector类的所有方法都是同步的,可以由两个线程安全访问一个vector对象
一个线程访问vector,代码要在同步操作上耗费大量时间
ArrayList是不同步的,所以不需要保证线程安全时建议使用ArrayList
3.HashMap的底层实现
JDK1.8之前:
hashmap的底层是数组和链表结合在一起使用,也称链表散列
hashmap通过key的hashcode经过扰动函数
处理后得到hash值,然后通过(n-1)& hash
判断当前元素存放的位置,这里n代表数组的长度,如果当前位置存在元素,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同直接覆盖,不相同就通过拉链法
解决冲突
扰动函数
:hashmapde的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashcode()方法,即使用扰动函数减少碰撞
拉链法
:将链表与数组相结合,即创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
JDK1.7
的hashmap的hash方法:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK1.8
的hashmap的hash方法:
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK1.8之后:
JDK1.8之后在解决哈希冲突有了较大变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
4.HashMap的长度?2的幂次方
为了能让hashmap存取高效,尽量减少碰撞,也就是尽量把数据分配均匀
Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的
一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的
用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标
数组下标的计算方法是(n - 1) & hash
(n代表数组长度)也就解释了HashMap 的长度为什么是2的幂次方
取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作
也就是说hash%length==hash&(length-1)的前提是length 是2的n 次方
并且采用二进制位操作&,相对于%能够提高运算效率
5.HashSet与HashMap
hashset底层基于hashmap实现
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
HashMap使用键(key)来计算HashCode | HashSet使用成员对象来计算HashCode值,对于两个对象来说hashcode可能相同,所以equals()方法来判断对象的相等性 |
HashMap相对于HashSet较快,因为使用唯一的键获取对象 | HashSet相对于HashSet较慢 |
6.ConcurrentHashMap线程安全的具体实现方式/底层具体实现
JDK1.7
:
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个数据时,其他段的数据也能被其他线程访问
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成
Segment
实现了ReentrantLock,所以Segment
是一种可重入锁,扮演锁的角色
static class Segment<K,V> extends ReentrantLock implements Serializable {}
HashEntry
用于存储键值对数据
一个ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和HashMap类似,是一种数组和链表结构
一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素
每个 Segment 守护着一个HashEntry数组里的元素
当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁
JDK1.8
:
ConcurrentHashMap取消
了Segment分段锁
,采用CAS
和synchronized
来保证并发安全
数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树
synchronized锁定
当前链表或红黑二叉树的首节点
,这样只要hash不冲突,就不会产生并发,效率又提升N倍
7.集合框架底层数据和结构
Collection
1)List
Arraylist:Object数组
Vector:Object数组
LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
2)Set
HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的
有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树
3)Map
HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在
JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap: 红黑树(自平衡的排序二叉树)