你们好,这里是《齐姐聊数据结构》系列之大集合。html
话很少说,直接上图:java
Java 集合,也称做容器,主要是由两大接口 (Interface)
派生出来的: Collection 和 Map
git
顾名思义,容器就是用来存放数据的。程序员
那么这两大接口的不一样之处在于:github
就是单身狗放 Collection 里面,couple 就放 Map 里。(因此你属于哪里?面试
学习这些集合框架,我认为有 4 个目标:算法
关于 Map,以前那篇 HashMap 的文章已经讲的很是透彻详尽了,因此本文再也不赘述。若是还没看过那篇文章的小伙伴,快去公众号内回复「HashMap」看文章吧~api
先来看最上层的 Collection.数组
Collection 里还定义了不少方法,这些方法也都会继承到各个子接口和实现类里,而这些 API 的使用也是平常工做和面试常见常考的,因此咱们先来看下这些方法。安全
操做集合,无非就是「增删改查」四大类,也叫 CRUD
:
Create, Read, Update, and Delete.
那我也把这些 API 分为这四大类:
功能 | 方法 |
---|---|
增 | add()/addAll() |
删 | remove()/ removeAll() |
改 | Collection Interface 里没有 |
查 | contains()/ containsAll() |
其余 | isEmpty()/size()/toArray() |
下面具体来看:
boolean add(E e);
add()
方法传入的数据类型必须是 Object,因此当写入基本数据类型的时候,会作自动装箱 auto-boxing 和自动拆箱 unboxing。
还有另一个方法 addAll()
,能够把另外一个集合里的元素加到此集合中。
boolean addAll(Collection<? extends E> c);
boolean remove(Object o);
remove()
是删除的指定元素。
那和 addAll()
对应的,
天然就有removeAll()
,就是把集合 B 中的全部元素都删掉。
boolean removeAll(Collection<?> c);
Collection Interface 里并无直接改元素的操做,反正删和增就能够完成改了嘛!
boolean contains(Object o);
boolean containsAll(Collection<?> c);
boolean isEmpty();
int size();
Object[] toArray();
以上就是 Collection 中经常使用的 API 了。
在接口里都定义好了,子类不要也得要。
固然子类也会作一些本身的实现,这样就有了不一样的数据结构。
那咱们一个个来看。
List 最大的特色就是:有序
,可重复
。
看官网说的:
An ordered collection (also known as a sequence).Unlike sets, lists typically allow duplicate elements.
这一下把 Set 的特色也说出来了,和 List 彻底相反,Set 是 无序
,不重复
的。
List 的实现方式有 LinkedList 和 ArrayList 两种,那面试时最常问的就是这两个数据结构如何选择。
对于这类选择问题:
一是考虑数据结构是否能完成须要的功能;
若是都能完成,二是考虑哪一种更高效。
(万事都是如此啊。
那具体来看这两个 classes 的 API 和它们的时间复杂度:
功能 | 方法 | ArrayList | LinkedList |
---|---|---|---|
增 | add(E e) | O(1) | O(1) |
增 | add(int index, E e) | O(n) | O(n) |
删 | remove(int index) | O(n) | O(n) |
删 | remove(E e) | O(n) | O(n) |
改 | set(int index, E e) | O(1) | O(n) |
查 | get(int index) | O(1) | O(n) |
稍微解释几个:
add(E e)
是在尾巴上加元素,虽然 ArrayList 可能会有扩容的状况出现,可是均摊复杂度(amortized time complexity)仍是 O(1) 的。
add(int index, E e)
是在特定的位置上加元素,LinkedList 须要先找到这个位置,再加上这个元素,虽然单纯的「加」这个动做是 O(1) 的,可是要找到这个位置仍是 O(n) 的。(这个有的人就认为是 O(1),和面试官解释清楚就好了,拒绝扛精。
remove(int index)
是 remove 这个 index 上的元素,因此
remove(E e)
是 remove 见到的第一个这个元素,那么
那形成时间复杂度的区别的缘由是什么呢?
答:
这个特色形成了在数组里能够经过下标用 O(1) 的时间拿到任何位置的数,而链表则作不到,只能从头开始逐个遍历。
也就是说在「改查」这两个功能上,由于数组可以随机访问,因此 ArrayList 的效率高。
那「增删」呢?
若是不考虑找到这个元素的时间,
数组由于物理上的连续性,当要增删元素时,在尾部还好,可是其余地方就会致使后续元素都要移动,因此效率较低;而链表则能够轻松的断开和下一个元素的链接,直接插入新元素或者移除旧元素。
可是呢,实际上你不能不考虑找到元素的时间啊。。。并且若是是在尾部操做,数据量大时 ArrayList 会更快的。
因此说:
那做为 List 的最后一个知识点,咱们来聊一下 Vector。这也是一个年龄暴露帖,用过的都是大佬。
那 Vector 和 ArrayList 同样,也是继承自 java.util.AbstractList<E>,底层也是用数组来实现的。
可是如今已经被弃用了,由于...它加了太多的 synchronized!
任何好处都是有代价的,线程安全的成本就是效率低,在某些系统里很容易成为瓶颈,因此如今你们再也不在数据结构的层面加 synchronized,而是把这个任务转移给咱们程序员==
那么面试常问题:Vector 和 ArrayList 的区别是什么,只答出来这个还还不太全面。
来看 stack overflow 上的高票回答:
一是刚才已经说过的线程安全问题;
二是扩容时扩多少的区别。
这个得看看源码:
这是 ArrayList 的扩容实现,这个算术右移操做是把这个数的二进制往右移动一位,最左边补符号位,可是由于容量没有负数,因此仍是补 0.
那右移一位的效果就是除以 2,那么定义的新容量就是原容量的 1.5 倍。
不了解这个右移操做符的小伙伴,公众号内回复「二进制」快复习一下吧~
再来看 Vector 的:
由于一般 capacityIncrement 咱们并不定义,因此默认状况下它是扩容两倍。
答出来这两点,就确定没问题了。
Queue 是一端进另外一端出的线性数据结构;而 Deque 是两端均可以进出的。
Java 中的 这个 Queue 接口稍微有点坑,通常来讲队列的语义都是先进先出(FIFO)的。
可是这里有个例外,就是 PriorityQueue,也叫 heap,并不按照进去的时间顺序出来,而是按照规定的优先级出去,而且它的操做并非 O(1) 的,时间复杂度的计算稍微有点复杂,咱们以后单独开一篇来说。
那 Queue 的方法官网都总结好了,它有两组 API,基本功能是同样的,可是呢:
功能 | 抛异常 | 返回值 |
---|---|---|
增 | add(e) | offer(e) |
删 | remove() | poll() |
瞧 | element() | peek() |
为何会抛异常呢?
那 add(e) 怎么会抛异常呢?
有些 Queue 它会有容量的限制,好比 BlockingQueue,那若是已经达到了它最大的容量且不会扩容的,就会抛异常;但若是 offer(e),就会 return false.
那怎么选择呢?:
Deque 是两端均可以进出的,那天然是有针对 First 端的操做和对 Last 端的操做,那每端都有两组,一组抛异常,一组返回特殊值:
功能 | 抛异常 | 返回值 |
---|---|---|
增 | addFirst(e)/ addLast(e) | offerFirst(e)/ offerLast(e) |
删 | removeFirst()/ removeLast() | pollFirst()/ pollLast() |
瞧 | getFirst()/ getLast() | peekFirst()/ peekLast() |
使用时同理,要用就用同一组。
Queue 和 Deque 的这些 API 都是 O(1) 的时间复杂度,准确来讲是均摊时间复杂度。
它们的实现类有这三个:
因此说,
咱们一个个来看。
在实现普通队列时,如何选择用 LinkedList 仍是 ArrayDeque 呢?
来看一下 StackOverflow 上的高票回答:
总结来讲就是推荐使用 ArrayDeque,由于效率高,而 LinkedList 还会有其余的额外开销(overhead)。
那 ArrayDeque 和 LinkedList 的区别有哪些呢?
仍是在刚才的同一个问题下,这是我认为总结的最好的:
因此,只要不是必需要存 null 值,就选择 ArrayDeque 吧!
那若是是一个很资深的面试官问你,什么状况下你要选择用 LinkedList 呢?
为了版本兼容的问题,实际工做中咱们不得不作一些妥协。。
那最后一个问题,就是关于 Stack 了。
Stack 在语义上是 先进先出(LIFO) 的线性数据结构。
有不少高频面试题都是要用到栈的,好比接水问题,虽然最优解是用双指针,可是用栈是最直观的解法也是须要了解的,以后有机会再专门写吧。
那在 Java 中是怎么实现栈的呢?
虽然 Java 中有 Stack 这个类,可是呢,官方文档都说不让用了!
缘由也很简单,由于 Vector 已通过被弃用了,而 Stack 是继承 Vector 的。
那么想实现 Stack 的语义,就用 ArrayDeque 吧:
Deque<Integer> stack = new ArrayDeque<>();
最后一个 Set,刚才已经说过了 Set 的特定是无序
,不重复
的。
就和数学里学的「集合」的概念一致。
Set 的经常使用实现类有三个:
HashSet: 采用 Hashmap 的 key 来储存元素,主要特色是无序的,基本操做都是 O(1) 的时间复杂度,很快。
LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特色就是既拥有了 O(1) 的时间复杂度,又可以保留插入的顺序。
TreeSet: 采用红黑树结构,特色是能够有序,能够用天然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。
那每一个 Set 的底层实现其实就是对应的 Map:
数值放在 map 中的 key 上,value 上放了个 PRESENT,是一个静态的 Object,至关于 place holder,每一个 key 都指向这个 object。
那么具体的实现原理、增删改查四种操做,以及哈希冲突、hashCode()/equals() 等问题都在 HashMap 那篇文章里讲过了,这里就不赘述了,没有看过的小伙伴能够在公众号后台回复「HashMap」获取文章哦~
再回到开篇的这张图,有没有清楚了一些呢?
每一个数据结构下面其实都有不少内容,好比 PriorityQueue 也就是堆,齐姐以前也专门写过文章讲解它的相关操做,好比颇有名的 heapify()
的过程为何是 O(n)
的等面试常问题,感兴趣的小伙伴在公众号后台回复「堆」获取文章吧~
若是你喜欢这篇文章,记得给我点赞留言哦~大家的支持和承认,就是我创做的最大动力,咱们下篇文章见!
我是小齐,纽约程序媛,终生学习者,天天晚上 9 点,云自习室里不见不散!
更多干货文章见个人 Github: https://github.com/xiaoqi6666...