1.数组
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗
2.链表
它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。
单链表、双向链表和循环链表:单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。单向链表只有一个方向,结点只有一个后继指针next指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点,用空间换时间的设计思想。
针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1),链表随机访问需要 O(n) 的时间复杂度。
在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:
1.删除结点中“值等于某个给定值”的结点。 单向链表查找O(n)删除O(n),双向链表查找O(n)删除O(1)
2.删除给定指针指向的结点。 单向链表 O(n),双向链表O(1)
数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对CPU 缓存不友好,没办法有效预读。数组的缺点是大小固定,一经声明就要占用整块连续内存空间,链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。
3.栈
后进者先出,先进者后出,这就是典型的“栈”结构。就像一叠盘子。
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,这时我们就应该首选“栈”这种数据结构。
用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。
4.队列
先进者先出。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。
阻塞队列:在队列为空的时候,从队头取数据会被阻塞,如果队列已经满了,那么插入数据的操作就会被阻塞;
线程安全的队列我们叫作并发队列;
实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队
5.散列表
键 --散列函数--> 散列值 存入数组,散列值是下标,键是数组值
散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
业界著名的MD5、SHA、CRC等哈希算法,也无法完全避免这种散列冲突。Java 中 LinkedHashMap 就采用了链表法解决冲突,ThreadLocalMap 是通过线性探测的开放寻址法来解决冲突
Java 中的 HashMap 这样一个工业级的散列表:默认的初始大小是 16,最大装载因子默认是 0.75,每次扩容都会扩容为原来的两倍大小,底层采用链表法来解决冲突,当链表长度太长(默认超过8)时,链表就转换为红黑树,当红黑树结点个数少于 8 个的时候,又会将红黑树转化为链表
6.跳表
这种链表加多级索引的结构,就是跳表
建立了很多索引,空间换时间。查找时间复杂度 logn,插入时间复杂度也是logn,空间复杂度O(n)。
索引存储的只是引用,相比对象很小,所以不必在意索引所占空间。