集合学习
1. 集合介绍
相对于数据来说,集合的大小是不固定的。
1.1 问题一:List,Set,Queue和Map的区别?
- List:存储的元素是有顺序的。可重复的。
- Queue:存储的元素是有顺序的。可重复的。按照FIFO排队规则确定顺序。
- Set:存储的元素是无序的,不可重复的。
- Map:元素为键值对,键key的信息是无序的,不可重复的。
2. List(Stack,ArrayList,LinkedList)
2.1 Stack(Vector的实现类)
- 底层数据结构:Object[] 数组
- List古老的实现类,线程是安全的,目前用的少。
2.2 ArrayList
- 底层数据结构:Object[] 数组
- 元素特点:有序(默认元素插入顺序),可重复,有索引,线程不安全。
2.3 LinkedList
- 底层数据结构:双向链表
- 元素特点:有序(默认元素插入顺序),可重复,有索引。
- 常常使用ListedList作为栈和队列的结构。
2.4 LinkedList和ArrayList的五点区别?
- 线程安全:两者都是不同步的,也不保证线程安全,但是Vector能够保证。
- 底层数据结构:ArrayList的底层为数组,LinkedList的底层为链表
- 时间复杂度:LinkedList更加方便插入和删除
- 快速随机访问 :ArrayList更加方便进行随机访问。
- 内存占用:链表结构的LinkedList更加占用内存。
- 扩容:数组结构的ArrayList需要扩容,而LinkedList不用。
- 一般情况下,均有有限使用ArrayList.
2.5 List常用的API
//1)新建ArrayList和LinedList对象
List<Integer> data1=new ArrayList<>();
List<Integer> data2=new LinkedList<>();
//2)增删改查
data1.add(2) //增加:在list集合最后位置添加元素2
data1.add(1,2) //增加:在索引1的位置插入元素2
data1.remove(2) //删除:把索引2位置的元素删除
data1.set(1,5) //吸怪:把索引1位置的元素修改为5
data.get(1) //查看:查看索引1位置元素的值
//3)常规操作
data1.clear() //清空集合
data1.contains(2) //判断某个对象中是否包含在集合中,返回true或者false
data1.isEmpty() //判断集合是否为空
data1.toArray() //把集合元素转换为数组存储
2.6 RandomAccess接口
1)ArrayList实现了RandomAccess接口,但是LinkedList没有实现。
2)RandomAccess其实没有具体的内容,主要是标志是否支持快速随机访问。
3)数据结构的ArrayList天然支持快速随机访问
2.7 ArrayList的扩容机制是什么?
- ArrayList有三种构造器,每种构造器有不同的扩容机制
1) 无参构造器,无参构造
2)有参构造器,传容量构造
3) 有参构造器,传列表构造 - ArrayList的扩容机制
1)第一种情况:当ArrayList的容量为0时,此时添加元素的话,需要扩容,三种构造方法创建的
(1) 无参构造,创建ArrayList后容量为0,添加第一个元素后,容量变为10,此后若需要扩容,则正常扩容。
(2) 传容量构造,当参数为0时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。
(3) 传列表构造,当列表为空时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。
2)第二种情况,当ArrayList的容量大于0,并且ArrayList是满的时,此时添加元素的话,进行正常扩容,每次扩容到原来的1.5倍(新=旧+旧/2)。
2.8 List集合如何遍历?
方法一:for循环遍历
List<Integer> list=new ArrayList<>(Arrays.asList(1,2,3,4,5));
//方法一:循环遍历
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
方法二:foreach遍历
List<Integer> list=new ArrayList<>(Arrays.asList(1,2,3,4,5));
for(Integer i: list){
System.out.println(i);
}
2.9 List集合中元素的顺序如何指定?
以整型集合为例
- 初始顺序为元素添加进去的顺序
- Collections.sort()排序:默认升序
List<Integer> list=new ArrayList<>(Arrays.asList(3,5,2,4,1));
Collections.sort(list);
System.out.print("默认升序:");
for(Integer i: list) System.out.print(i+" ");
System.out.println();
- Collections.reverse()排序:默认降序
List<Integer> list=new ArrayList<>(Arrays.asList(3,5,2,4,1));
Collections.reverse(list);
System.out.print("修改为降序:");
for(Integer i: list) System.out.print(i+" ");
System.out.println();
- Collections.sort()重写Comperator类的compare方法排序:自定义
//4)自定义排序为降序
List<Integer> list1=new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7));
Collections.sort(list1,(a,b)->b-a);
System.out.print("自定义排序为降序:");
for(Integer i: list1) System.out.print(i+" ");
System.out.println();
- 在类中重写方法CompareTo(常用于对Map集合进行操作)
3. Queue(ArrayDeque,LinkedList和PriorityQueue)
3.0 Deque与Queue
- Queue:单端队列,准守先进先去的原则。
- Deque:双端队列,准守先进先出的原则。
- 函数方法两种类型,针对出现异常的情况:
1)抛出异常的方法:add,remove,element/addfirst,removefirst,getfirst
2)返回特殊值的方法:offer,poll,peek/offerfirst,pollfirst,peekfirst - Deque还有函数方法:push和poll,能够用来模拟栈。
3.1 Deque->ArrayDeque
- 底层数据结构:Object[ ] 数组+双指针
3.2 Deque->LinkedList
- LinkedList作为栈的操作:
//LinkedList作为一个栈的使用
//1)新建栈对象
Deque<Integer> stack=new LinkedList<>();
//2)栈的压入操作
stack.addLast(2);
stack.addLast(3);
stack.addLast(1);
System.out.println(stack);
//3)栈的弹出操作(使用弹出操作之前,一定要判断栈中还有元素)
stack.removeLast();
stack.removeLast();
//4)查看栈头操作(保证栈中元素非空)
System.out.println(stack.getLast());
- LinkedList作为队列的操作:
//LinkedList作为一个队列的使用
//1)新建队列对象
Deque<Integer> queue=new LinkedList<>();
//2)入队操作
queue.addLast(2);
queue.addLast(3);
queue.addLast(1);
//3)出队操作(一定要保证队列元素非空)
queue.removeFirst();
queue.removeFirst();
//4)查看操作(一定要保证队列元素非空)
System.out.println(queue.getFirst());
3.3 PriorityQueue(重点容易忘)
- 底层数据结构:Object[ ] 数组来实现二叉堆(二叉树)
- 可以用来实现小顶堆和大顶堆(代码要重写一遍了)
- 在面试中可能更多的会出现在手撕算法的时候,典型例题包括堆排序、求第K大的数、带权图的遍历等,所以需要会熟练使用才行。
堆定义:
- 大顶堆:一种特殊的完全二叉树,任意一个节点都要比左右孩子节点大。
- 小顶堆:一种特殊的完全二叉树,任意一个节点都要比左右孩子节点小。
//方法一:实现小顶堆的操作(输出第K小的数)
//PriorityQueue实现类能够实现堆排序操作(默认是小顶堆)
PriorityQueue<Integer> minHeap=new PriorityQueue<>(Arrays.asList(5,9,8,2,4));
int size= minHeap.size();
for(int i=0;i<size;i++){
System.out.println("按从小到大顺序输出"+minHeap.poll());
}
//方法二:实现大顶堆的操作
//PriorityQueue实现类能够实现大顶堆,需要重写Comperator类的compare方法
PriorityQueue<Integer> maxHeap=new PriorityQueue<>(new Comparator<Integer>(){
@Override
public int compare(Integer a,Integer b){
return b-a;
}
});
//使用Lambda表达式简化大顶堆重写操作
PriorityQueue<Integer> maxHeap=new PriorityQueue<>((a,b)->b-a);
特点:
- PriorityQueue本质上还是一个队列,所以有入队(queue.offer()),出队(queue.poll())
和查看(queue.peek())操作。 - 大顶堆与小顶堆的入队和出队操作的时间复杂度均为O(log(n))。
- 堆的队列是线程安全的,不允许出现null.
3.4 ArrayDeque和LinkedList的区别
- 相同点:两者都实现了Deque接口,两者都有队列的功能。
- 不同点:数据结构:ArrayDeque是基于可变长数组和链表组成,LinkedList是基于双向量链表组成。
- ArrayDeque插入需要扩容,而LinkedList不需要扩容,但是需要申请新的空间,均摊性能相比更慢。
- 综合来说,ArrayDeque比LinkedList性能更加好一点。
4. Set(HashSet,LinkedHashSet,TreeSet)
4.0 HashSet、LinkedHashSet 和 TreeSet的异同?
- 相同点:都是Set接口的实现类,都能够保证数据不重复,并且都不是线程安全的。
- 不同点:底层数据结构不同,HashSet是基于HashMap(数组+链表),LinkedHashSet是基于LinkedListMap(数组+链表,多了一个双向链表)和TreeSet是基于红黑树实现。
- 使用场景:HashSet:不需要保证元素插入和取出的顺序; LinkedHashSet:保证元素的插入和去除顺序满足FIFO的顺序 ;TreeSet:常用于对元素需要进行自定义排序的时候。
4.1 HashSet
- 底层数据结构:基于HashMap实现,底层采用HashMap保存元素。
- 元素性质:无序,唯一
4.2 LinkedHashSet
- 底层数据结构:是HashSet的子类,基于LinkedHashMap实现
4.3 TreeSet
- 底层数据结构:红黑树实现(自平衡的排序二叉树实现)
- 元素性质:有序,唯一
5. Map(HashMap,LinkedHashMap,TreeMap)
5.1 HashMap
- 底层数据结构:由数组+链表组成,数组是主体,链表是为了防止哈希冲突(拉链法)
5.2 LinkedHashMap
- 底层数据结构:是HashMap的子类,也是由数组+链表组成,额外添加了一条双向链表。
5.3 TreeMap
- 底层数据结构:红黑树实现(自平衡的排序二叉树实现)
5.4 Map集合的遍历方式(重点容易忘)
- 在for循环中,使用entrySet()遍历Map集合
Map<Integer,String> map=new TreeMap<>();
map.put(1,"a");
map.put(2,"b");
map.put(3,"c");
System.out.println(map);
//Map遍历方式一
for(Map.Entry<Integer,String> ma: map.entrySet()){
System.out.print(ma.getKey()+"="+ma.getValue());
System.out.print(" ");
}
System.out.println();
//输出
{1=a, 2=b, 3=c}
1=a 2=b 3=c
- 使用foreach循环遍历map.Key和map.Value
Map<Integer,String> map=new TreeMap<>();
map.put(1,"a");
map.put(2,"b");
map.put(3,"c");
System.out.println(map);
//Map遍历方式二(可以分别遍历键和值)
for(Integer i:map.keySet()){
System.out.print(i+" ");
}
for(String j:map.values()){
System.out.print(j+" ");
}
//输出
1 2 3 a b c
6. Set与Map之间的比较
6.1 Hashtable与HashMap的异同
- 线程安全:HashMap是非线程安全的,内部方法没有经过synchronized修饰。(如果想保证线程安全,就用ConcurrentHashMap )
- 效率:HashMap比Hashtable效率高,Hashtable几乎不用了。
- Null key 和Null value的支持:HashMap可以存有一个 Null key和多个Null value。
- 初始容量大小和每次扩充容量大小的不同 :HashMap默认初始化大小为16,每次扩容变为原来的两倍;HashMap也可使用自定义的初始容量值(必须是2的幂次方),扩容也会扩为2的幂次方。(HashMap 总是使用 2 的幂作为哈希表的大小)
- 底层数据结构:HashMap的数据结构为数组+链表(链表是为了解决hash冲突,使用拉链法)。JDK1.8之后,当数组长度大于64,链表长度大于阈值(默认为8时),会自动将链表转变为红黑树,以减少搜索是时间。
6.2 HashMap 和 HashSet 的异同
- HashSet的底层是使用HashMap实现的,都是数组+链表的组合。
- HashSet除了几个特殊的功能之后,其他主要功能都是调用HashMap的方法。
6.3 HashMap 和 TreeMap 的异同
- HashMap和TreeMap都是Map的实现类,但是TreeMap是基于红黑树实现的,HashMap是基于数组+链表实现的
- TreeMap额外多实现了两个接口:NavigableMap和SortedMap 接口。
- NavigableMap:使得TreeMap有在集合内实现搜索的能力:
public NavigableMap<K, V> descendingMap() 返回一个降序排列的Map
public NavigableSet<K> descendingKeySet() 返回一个降序排列的由键名组成的Set
- SortedMap :使得TreeMap有在集合内元素根据键的大小排序的能力[一定要学会比较器的重写】
//匿名内部类,重写compare方法
TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
@Override
public int compare(Person person1, Person person2) {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
}
});
//匿名内部类,重写compare方法,使用Lambda表达式进行简化。
TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
});
- 综上:TreeMap多了两个功能:集合内元素搜索和集合元素按照键的值进行指定排序。
6.4 HashSet 如何检查重复
- 添加的元素计算hashcode,如果不相等,那么就不存在重复情况,直接加上去。
- 如果hashcode相等,那么就用调用equals方法来检查,当hashcode相同时,对象是否真的相同。
6.5 HashMap 的底层实现
HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列
- 得到 hash 值:HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值.
- 得到存放位置:通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度)。
- 判断是覆盖还是拉链法添加:当前位置存在元素的话,判断该元素与要存入的元素的 hash 值以及 key 是否相同,相同就覆盖,不相同就使用拉链法解决冲突。
- “拉链法” :将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。(尾插法),其他解决哈希冲突方法有开放地址法。hashmap头插法在多线程情况下会形成链表环,陷入死循环。
6.6 HashMap 的长度为什么是 2 的幂次方
6.7 HashMap 多线程操作导致死循环问题
6.8 HashMap 有哪几种常见的遍历方式?
6.9 ConcurrentHashMap 和 Hashtable 的区别
6.10 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
7. Collections工具类
- 排序
- void reverse(List list) //反转集合
- void sort(List list) //按照升序进行排列
- void sort(List list,Comparator c) //自定义排序规则(按照键的值)
- void swap(List list,int i,int j) //交换集合两个索引位置的元素
- 查找和替换操作
//使用二分查找集合中的元素,首先集合必须是有序的,返回索引值。
int binarySearch(List list, Object key)
8. Java集合使用的注意事项
8.1 集合判空
- 判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式。
- isEmpty() 方法时间复杂度恒为O(1),size()==0不一定。后者更加稳定
8.2 集合转 Map
- 在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。(不太理解)
8.3 集合的遍历
- 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式
- Iterator是一个迭代器,每个集合都可以得到一个迭代器,然后迭代器中有remove()的方法。
- 使用普通的for循环进行遍历,可以直接删除元素。
- 如果并发操作,需要对 Iterator 对象加锁。
8.4 集合去重
集合去重,可以使用Set集合,避免使用List集合,然后又用contains方法去判断。
// Set 去重代码示例:list集合去重。
public static <T> Set<T> removeDuplicateBySet(List<T> data) {
if (CollectionUtils.isEmpty(data)) {
return new HashSet<>();
}
return new HashSet<>(data);
}
区别:
- HashSet集合中的contains使用哈希值查找,找一个数时间复杂度为O(1),查找n个数,总时间复杂度为O(n).
- List集合中的contains使用遍历查找,找一个数时间复杂度为O(n),查找n个数,总时间复杂度为O(n^2).
8.5 集合转变为数组:toArray()
//1)toArray():集合转变为数组
//注意:在使用toArray()时候,返回的是一个Object[]数组,
// 需要在括号里添加类型:new String[0],只是声明返回值类型的作用。
String[] s=list.toArray(new String[0]);
System.out.println("s="+Arrays.toString(s));
8.6 数组转变为集合:Arrays.toList()
//1)Arrays.asList:数组转变为集合
//注意:一定不能直接赋值给list集合,需要转化为ArrayList
//List<String> list=Arrays.asList(string); 这个操作会使得list无法进行修改。
String[] string={"2","3","a","b"};
List<String> list=new ArrayList<>(Arrays.asList(string));
System.out.println("list="+list);
主要参考文章
https://javaguide.cn/