目录
数组
数组的另一个特点,是在内存中顺序存储,因此可以很好地实现逻辑上的顺序 表。
数组在内存中的顺序存储,具体是什么样子呢?
内存是由一个个连续的内存单元组成的,每一个内存单元都有自己的地址。在这些内存单元中,有些被其他数据占用了,有些是空闲的。
数组中的每一个元素,都存储在小小的内存单元中,并且元素之间紧密排列, 既不能打乱元素的存储顺序,也不能跳过某个存储单元进行存储。
数组的插入和删除操作,时间复杂度分别是多少?
先说说插入操作,数组扩容的时间复杂度是O(n),插入并移动元素的时间复杂度也是O(n),综合起来插入操作的时间复杂度是O(n)。至于删除操作,只涉及元素的移动,时间复杂度也是O(n)。
数组的优势和劣势?
数组拥有非常高效的随机访问能力,只要给出下标,就 可以用常量时间找到对应元素。有一种高效查找元素的算法叫作二分查找, 就是利用了数组的这个优势。
至于数组的劣势,体现在插入和删除元素方面。由于数组元素连续紧密地存储在内存中,插入、删除元素都会导致大量元素被迫移动,影响效率。
数组所适合的是读操作多、写操作少的场景
链表
单向链表
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。
单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部 分是指向下一个节点的指针next。
链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指 针指向空。
与数组按照下标来随机寻找元素不同,对于链表的其中一个节点A,我们只能根 据节点A的next指针来找到该节点的下一个节点B,再根据节点B的next指针找到下 一个节点C……
双向链表
双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有data和next指 针,还拥有指向前置节点的prev指针。
如果说数组在内存中的存储方式是顺序存储,那么链表在内存中的存储方式则是随机存储。
什么叫随机存储呢?
数组的内存分配方式,数组在内存中占用了连续完整的存储空间。而链表则采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。
图中的箭头代表链表节点的next指针。
链表的插入和删除操作,时间复杂度分别是多少?
如果不考虑插入、删除操作之前查找元素的过程,只考虑纯粹的插入和删除操作,时间复杂度都是O(1)。
数组VS链表
数组的优势在于能够快速定位元素,对于读操作多、写操作少的场景来说,用数组更合适一些。
链表的优势在于能够灵活地进行插入和删除操作,如果需要在尾部频繁插入、删除元素,用链表更合适一些。
栈
栈(stack)是一种线性数据结构,它就像一个上图所示的放入乒乓球的圆筒容器,栈中的元素只能先进后出(First In Last Out,简称FILO)。最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶(top)。
栈这种数据结构既可以用数组来实现,也可以用链表来实现。
栈的数组实现如下
栈的链表实现如下
入栈操作
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶。
出栈操作
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶。
队列
队列中的元素只能先进先出(First In First Out,简称FIFO)。队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)。
与栈类似,队列这种数据结构既可以用数组来实现,也可以用链表来实现。
用数组实现时,为了入队操作的方便,把队尾位置规定为最后入队元素的下一个位置。
队列的数组实现如下
队列的链表实现如下
入队
入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放入元素,新元素的下一个位置将会成为新的队尾。
出队
出队操作(dequeue)就是把元素移出队列,只允许在队头一侧移出元素,出队元素的后一个元素将会成为新的队头。
散列表
散列表也叫作哈希表(hash table),这种数据结构提供了键(Key)和值 (Value)的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时 间复杂度接近于O(1)。
哈希函数
- 通过某种方式,把Key和 数组下标进行转换。这个中转站就叫作哈希函数。通过哈希函数,我们可以把字符串或其他类型的Key,转化成数组的下标 index。
散列表的读写操作
写操作
写操作(put)写操作就是在散列表中插入新的键值对(在JDK中叫作Entry)。
如调用hashMap.put(“002931”,“王五”),意思是插入一组Key为002931、Value为王五的键值对。具体该怎么做呢?
- 第1步,通过哈希函数,把Key转化成数组下标5。
- 第2步,如果数组下标5对应的位置没有元素,就把这个Entry填充到数组下标5
但是,由于数组的长度是有限的,当插入的Entry越来越多时,不同的Key通过 哈希函数获得的下标有可能是相同的。例如002936这个Key对应的数组下标是2; 002947这个Key对应的数组下标也是2。
这种情况,就叫作哈希冲突。
解决哈希冲突的方法主要有两种,一种是开放寻址法,一种是链表法。
解决哈希冲突 - 开放寻址法
开放寻址法: 开放寻址法的原理很简单,当一个Key通过哈希函数获得对应的数组下标已被占 用时,我们可以“另谋高就”,寻找下一个空档位置。(在Java中,ThreadLocal所使用的就是开放寻址法。)
解决哈希冲突 - 链表法
当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可。
读操作
例如调用 hashMap.get(“002936”),意思是查找Key为002936的Entry在散列 表中所对应的值。 具体该怎么做呢?下面以链表法为例来讲一下。
- 第1步,通过哈希函数,把Key转化成数组下标2。
- 第2步,找到数组下标2所对应的元素,如果这个元素的Key是002936,那么就 找到了;如果这个Key不是002936也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。
在上图中,首先查到的节点Entry6的Key是002947,和待查找的Key 002936不 符。接着定位到链表下一个节点Entry1,发现Entry1的Key 002936正是我们要寻 找的,所以返回Entry1的Value即可。
扩容(resize)
当经过多次元素插入,散列表达到一定饱和度时,Key映射位置发生冲突的概率 会逐渐提高。这样一来,大量元素拥挤在相同的数组下标位置,形成很长的链表, 对后续插入操作和查询操作的性能都有很大影响。
对于JDK中的散列表实现类HashMap来说,其扩容
- 1.扩容,创建一个新的Entry空数组,长度是原数组的2倍。
- 2.重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中。为什 么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。
关于HashMap的实现,JDK 8和以前的版本有着很大的不同。当 多个Entry被Hash到同一个数组下标位置时,为了提升插入和查找的效率,HashMap 会把Entry的链表转化为红黑树这种数据结构。建议读者把两个版本的实现都认真地 看一看,这会让你受益匪浅。
二叉树
二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每 个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩 子节点。
满二叉树
一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层 级上,那么这个树就是满二叉树。
完全二叉树
满二叉树要求所有分支都是满的; 而完全二叉树只需保证最后一个节点之前的节点都齐全即可。
二叉查找树
二叉查找树要求左子树小于父节点,右子树 大于父节点,正是这样保证了二叉树的有序性
二叉堆
二叉堆本质上是一种完全二叉树,它分为两个类型: 最大堆、最小堆。
- 最大堆
最大堆的任何一个父节点的值,都大于或等于它左、右孩子 节点的值。
- 最小堆
最小堆的任何一个父节点的值,都小于或等于它左、右孩子 节点的值。
最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆 的堆顶是整个堆中的最小元素。
优先队列
优先队列不再遵循先入先出的原则,而是分为两种情况。
- 最大优先队列,无论入队顺序如何,都是当前最大的元素优先出队
- 最小优先队列,无论入队顺序如何,都是当前最小的元素优先出队