文章目录
- 研究对象一:数据间逻辑关系
- 研究对象二:数据的存储结构(或物理结构)
- [研究对象三:运算结构 相关的算法操作](#研究对象三:运算结构 相关的算法操作)
- 一维数组
- 链表
- 栈
- 队列
- 树与二叉树
- List接口
- [链表 LinkedList](#链表 LinkedList)
- Map接口
- LinkedHashMap
- Set接口
- HashMap相关
研究对象一:数据间逻辑关系
数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构
和物理结构
以及它们之间相互关系,并对这种结构定义相应的运算
,目的是加快程序的执行速度、减少内存占用的空间。
数据的逻辑结构指反映数据元素之间的逻辑关系,而与数据的存储无关,是独立于计算机的
- 集合结构 :数据结构中的元素之间除了"
同属一个集合
" 的相互关系外,别无其他关系。集合元素之间没有逻辑关系。 - 线性结构 :数据结构中的元素存在
一对一
的相互关系。比如:排队。结构中必须存在唯一的首元素和唯一的尾元素。体现为:一维数组、链表、栈(先进后出 后进先出)、队列(先进先出 后进后出) - 树形结构 :数据结构中的元素存在
一对多
的相互关系。比如:家谱、文件系统、组织架构 - 图形结构 :数据结构中的元素存在
多对多
的相互关系。比如:全国铁路网、地铁图
研究对象二:数据的存储结构(或物理结构)
数据的物理结构/存储结构:包括数据元素的表示
和关系的表示
。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。
结构1:顺序结构
-
顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素。
-
优点: 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现随机访问。
-
缺点: 必须静态分配连续空间,内存空间的利用率比较低。插入或删除可能需要移动大量元素,效率比较低
结构2:链式结构
- 不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。
- 优点:不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点。插入或删除元素时,不需要移动大量的元素。
- 缺点:需要额外的空间来表达数据之间的逻辑关系,不支持下标访问和随机访问。
结构3:索引结构
- 除建立存储节点信息外,还建立附加的
索引表
来记录每个元素节点的地址。索引表由若干索引项组成。索引项的一般形式是:(关键字,地址)。 - 优点:用节点的索引号来确定结点存储地址,检索速度快。
- 缺点: 增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修改索引表,因而会花费较多的时间。
结构4:散列结构
- 根据元素的关键字直接计算出该元素的存储地址,又称为Hash存储。(key-value)
- 优点:检索、增加和删除结点的操作都很快。
- 缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。
开发中,我们更习惯上如下的方式理解存储结构:
线性表(一对一关系): 一维数组、单向链表、双向链表、栈、队列
树(一对多关系):各种树。比如:二叉树、B+树
图(多对多关系)
哈希表:比如:HashMap、HashSet
研究对象三:运算结构 相关的算法操作
施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
- 分配资源,建立结构,释放资源
- 插入和删除
- 获取和遍历
- 修改和排序
常见数据结构包含四种 树 图 线性表 散列表
树 :{
二叉树:[平衡二叉树 二叉查找树 平衡二叉树 平衡二叉查找树 :[AVL树 红黑树] 完全二叉树 满二叉树]
多路查找树 :[ B树 B+树 2-3树 2-3-4树 ]
堆 :[ 小顶堆 大顶堆 优先级队列 斐波那契堆 二项堆]
其他 :[树状数组 线段树]
}
图 :{
图的存储 :[邻接矩阵 邻接表]
有向图
无向图
联通图
遍历:[广度优先遍历 深度优先遍历]
最小生成树
路径:[最短路径 关键路径]
}
线性表:{
数组
链表:[单向链表 双向链表 循环链表 双向循环链表]
栈:[顺序栈 链式栈]
队列 :[普通队列 双端队列 阻塞队列 并发队列 阻塞并发队列]
}
散列表:{
散列函数 冲突解决:[链表法 开发寻址 其他] 动态扩容 位图
}
一维数组
数组特点 在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型
java
//只声明了类型和长度
数据类型[] 数组名称 = new 数据类型[数组长度];
//声明了类型,初始化赋值,大小由元素个数决定
数据类型[] 数组名称 = {数组元素1,数组元素2,......}
- 申请内存:一次申请一大段连续的空间,一旦申请到了,内存就固定了。
- 不能动态扩展(初始化给大了,浪费;给小了,不够用),插入快,删除和查找慢。
- 存储特点:所有数据存储在这个连续的空间中,数组中的每一个元素都是一个具体的数据(或对象),所有数据都紧密排布,不能有间隔。
链表
-
逻辑结构:线性结构 链表中的基本单位是:节点(Node)
-
物理结构:不要求连续的存储空间
-
存储特点:链表由一系列结点node(链表中每一个元素称为结点)组成,结点可以在代码执行过程中动态创建。每个结点包括两个部分:一个是存储数据元素的
数据域
,另一个是存储下一个结点地址的指针域
。
自定义单向链表
java
//单向链表
class Node{
Object data;
Node next;
public Node(Object data){
this.data = data;
}
}
//创建对象:
Node node1 = new Node("AA");
Node node2 = new Node("BB");
node1.next = node2;
java
public class Node {
// 存储的数据
Object data;
// 下一个节点的内存地址
Node next;
public Node(){
}
public Node(Object data, Node next){
this.data = data;
this.next = next;
}
}
// 双链表示例
class DoubleNode {
int value;
DoubleNode prev;
DoubleNode next;
public DoubleNode(int value) {
this.value = value;
this.prev = null;
this.next = null;
}
}
// 循环链表示例
class CircularNode {
int value;
CircularNode next;
public CircularNode(int value) {
this.value = value;
this.next = null;
}
}
java
//双向链表
class Node{
Node prev;
Object data;
Node next;
public Node(Object data){
this.data = data;
}
public Node(Node prev,Object data,Node next){
this.prev = prev;
this.data = data;
this.next = next;
}
}
//创建对象:
Node node1 = new Node(null,"AA",null);
Node node2 = new Node(node1,"BB",null);
Node node3 = new Node(node2,"CC",null);
node1.next = node2;
node2.next = node3;
栈
常见存储结构之:栈(stack、先进后出、first in last out、FILO、LIFO)
属于抽象数据类型(ADT)
可以使用数组或链表来构建
-
栈(Stack)又称为堆栈或堆叠,是限制仅在表的一端进行插入和删除运算的线性表。
-
栈按照
先进后出(FILO,first in last out)
的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶。每次删除(退栈)的总是删除当前栈中最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能删除。 -
核心类库中的栈结构有Stack和LinkedList。
- Stack就是顺序栈,它是Vector的子类。
- LinkedList是链式栈。
-
体现栈结构的操作方法:
- peek()方法:查看栈顶元素,不弹出
- pop()方法:弹出栈
- push(E e)方法:压入栈
- clear() 清栈
- size() 大小
- empty() 判断是否是空
- search(Object obj) 查找元素出现位置
-
时间复杂度:
- 索引:
O(n)
- 搜索:
O(n)
- 插入:
O(1)
- 移除:
O(1)
- 索引:
栈常见使用场景 二叉树 森林遍历 CPU中断处理 图形深度优先查找法(DFS) 递归调用(斐波那契数列 汉诺塔问题) 子程序的调用
java
//数组实现栈
class Stack{
Object[] values;
int size;//记录存储的元素的个数
public Stack(int length){
values = new Object[length];
}
//入栈
public void push(Object ele){
if(size >= values.length){
throw new RuntimeException("栈空间已满,入栈失败");
}
values[size] = ele;
size++;
}
//出栈
public Object pop(){
if(size <= 0){
throw new RuntimeException("栈空间已空,出栈失败");
}
Object obj = values[size - 1];
values[size - 1] = null;
size--;
return obj;
}
}
队列
-
队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表。
-
队列是逻辑结构,其物理结构可以是数组,也可以是链表。
-
队列的修改原则:队列的修改是依
先进先出(FIFO)的原则
进行的。新来的成员总是加入队尾(即不允许"加塞"),每次离开的成员总是队列头上的(不允许中途离队),即当前"最老的"成员离队。(先进先出)
常见存储结构之:队列(queue、先进先出、first in first out、FIFO)
属于抽象数据类型(ADT)
可以使用数组或链表来构建
java
//数组实现队列
class Queue{
Object[] values;
int size;//记录存储的元素的个数
public Queue(int length){
values = new Object[length];
}
public void add(Object ele){ //添加
if(size >= values.length){
throw new RuntimeException("队列已满,添加失败");
}
values[size] = ele;
size++;
}
public Object get(){ //获取
if(size <= 0){
throw new RuntimeException("队列已空,获取失败");
}
Object obj = values[0];
//数据前移
for(int i = 0;i < size - 1;i++){
values[i] = values[i + 1];
}
//最后一个元素置空
vlaues[size - 1] = null;
size--;
return obj;
}
}
树与二叉树
结点
:树中的数据元素都称之为结点
根节点
:最上面的结点称之为根,一颗树只有一个根且由根发展而来,从另外一个角度来说,每个结点都可以认为是其子树的根
父节点
:结点的上层结点
子节点
:节点的下层结点
兄弟节点
:具有相同父节点的结点称为兄弟节点
结点的度数
:每个结点所拥有的子树的个数称之为结点的度
树叶
:度数为0的结点,也叫作终端结点
非终端节点(或分支节点)
:树叶以外的节点,或度数不为0的节点。
树的深度(或高度)
:树中结点的最大层次数
结点的层数
:从根节点到树中某结点所经路径上的分支树称为该结点的层数,根节点的层数规定为1,其余结点的层数等于其父亲结点的层数+1
同代
:在同一棵树中具有相同层数的节点
二叉树
二叉树(Binary tree)是树形结构的一个重要类型。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。许多实际问题抽象出来的数据结构往往是二叉树形式,二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
二叉树遍历
-
前序遍历:中左右(根左右)
即先访问根结点,再前序遍历左子树,最后再前序遍历右子 树。前序遍历运算访问二叉树各结点是以根、左、右的顺序进行访问的。
-
中序遍历:左中右(左根右)
即先中前序遍历左子树,然后再访问根结点,最后再中序遍 历右子树。中序遍历运算访问二叉树各结点是以左、根、右的顺序进行访问的。
-
后序遍历:左右中(左右根)
即先后序遍历左子树,然后再后序遍历右子树,最后访问根 结点。后序遍历运算访问二叉树各结点是以左、右、根的顺序进行访问的。
java
class TreeNode{
TreeNode left;
Object data;
TreeNode right;
public TreeNode(Object data){
this.data = data;
}
public TreeNode(TreeNode left,Object data,TreeNode right){
this.left = left;
this.data = data;
this.right = right;
}
}
//创建对象:
TreeNode node1 = new TreeNode(null,"AA",null);
TreeNode leftNode = new TreeNode(null,"BB",null);
TreeNode rightNode = new TreeNode(null,"CC",null);
node1.left = leftNode;
node1.right = rightNode;
java
class TreeNode{
TreeNode parent;
TreeNode left;
Object data;
TreeNode right;
public TreeNode(Object data){
this.data = data;
}
public TreeNode(TreeNode left,Object data,TreeNode right){
this.left = left;
this.data = data;
this.right = right;
}
public TreeNode(TreeNode parent,TreeNode left,Object data,TreeNode right){
this.parent = parent;
this.left = left;
this.data = data;
this.right = right;
}
}
创建对象:
TreeNode node1 = new TreeNode(null,null,"AA",null);
TreeNode leftNode = new TreeNode(node1,null,"BB",null);
TreeNode rightNode = new TreeNode(node1,null,"CC",null);
node1.left = leftNode;
node1.right = rightNode;
经典二叉树
1、满二叉树
: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 第n层的结点数是2的n-1次方,总的结点个数是2的n次方-1
2、完全二叉树
: 叶结点只能出现在最底层的两层,且最底层叶结点均处于次底层叶结点的左侧。
3、二叉排序/查找/搜索树
:即为BST (binary search/sort tree)。满足如下性质:
(1)若它的左子树不为空,则左子树上所有结点的值均小于它的根节点的值;
(2)若它的右子树上所有结点的值均大于它的根节点的值;
(3)它的左、右子树也分别为二叉排序/查找/搜索树。
对二叉查找树进行中序遍历,得到有序集合。便于检索。
4、平衡二叉树
:(Self-balancing binary search tree,AVL)首先是二叉排序树,此外具有以下性质:
(1)它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
(2)并且左右两个子树也都是一棵平衡二叉树
(3)不要求非叶节点都有两个子结点
平衡二叉树的目的是为了减少二叉查找树的层次,提高查找速度。平衡二叉树的常用实现有红黑树、AVL、替罪羊树、Treap、伸展树等。
6、红黑树
:即Red-Black Tree。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,它是在 1972 年由 Rudolf Bayer 发明的。红黑树是复杂的,但它的操作有着良好的最坏情况运行时间
,并且在实践中是高效的
:它可以在 O(log n)时间内做查找,插入和删除, 这里的 n 是树中元素的数目。
红黑树的特性:
-
每个节点是红色或者黑色
-
根节点是黑色
-
每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点)
-
每个红色节点的两个子节点都是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
-
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(确保没有一条路径会比其他路径长出2倍)
当我们插入或删除节点时,可能会破坏已有的红黑树,使得它不满足以上5个要求,那么此时就需要进行处理,使得它继续满足以上的5个要求:
1、recolor
:将某个节点变红或变黑
2、rotation
:将红黑树某些结点分支进行旋转(左旋或右旋)
红黑树可以通过红色节点和黑色节点尽可能的保证二叉树的平衡。主要是用它来存储有序的数据,它的时间复杂度是O(logN),效率非常之高。
List接口
- List集合所有的元素是以一种
线性方式
进行存储的,例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。 - 它是一个元素
存取有序
的集合。即元素的存入顺序和取出顺序有保证。 - 它是一个
带有索引
的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。 - 集合中可以有
重复
的元素,通过元素的equals方法,来比较是否为重复的元素。
List集合关心元素是否有序,而不关心是否重复,请大家记住这个原则。例如"张三"可以领取两个号。
List接口的主要实现类
- ArrayList:动态数组
- Vector:动态数组
- LinkedList:双向链表
- Stack:栈
动态数组ArrayList与Vector
ArrayList与Vector的区别
它们的底层物理结构都是数组,我们称为动态数组。
- ArrayList是新版的动态数组,线程不安全,效率高,Vector是旧版的动态数组,线程安全,效率低。
- 动态数组的扩容机制不同,ArrayList默认扩容为原来的1.5倍,Vector默认扩容增加为原来的2倍。
- 数组的初始化容量,如果在构建ArrayList与Vector的集合对象时,没有显式指定初始化容量,那么Vector的内部数组的初始容量默认为10,而ArrayList在JDK 6.0 及之前的版本也是10,JDK8.0 之后的版本ArrayList初始化为长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。原因:
- 用的时候,再创建数组,避免浪费。因为很多方法的返回值是ArrayList类型,需要返回一个ArrayList的对象,例如:后期从数据库查询对象的方法,返回值很多就是ArrayList。有可能你要查询的数据不存在,要么返回null,要么返回一个没有元素的ArrayList对象。
ArrayList部分源码分析
java
//属性
transient Object[] elementData;
private int size;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //初始化为空数组
}
//方法:add()相关方法
public boolean add(E e) {
//查看当前数组是否够多存一个元素
ensureCapacityInternal(size + 1); // Increments modCount!!
//存入新元素到[size]位置,然后size自增1
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果当前数组还是空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//那么minCapacity取DEFAULT_CAPACITY与minCapacity的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//查看是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //修改次数加1
//如果需要的最小容量比当前数组的长度大,即当前数组不够存,就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //当前数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //新数组容量是旧数组容量的1.5倍
//看旧数组的1.5倍是否够
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//看旧数组的1.5倍是否超过最大数组限制
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//复制一个新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
链表 LinkedList
LinkedList的特点:
实现了List接口,存储有序的、可以重复的数据
底层使用双向链表存储
线程不安全的
Java中有双链表的实现:LinkedList,它是List接口的实现类。
LinkedList是一个双向链表
因为LinkedList使用的是双向链表,不需要考虑扩容问题。
链表与动态数组的区别
动态数组底层的物理结构是数组,因此根据索引访问的效率非常高。但是非末尾位置的插入和删除效率不高,因为涉及到移动元素。另外添加操作时涉及到扩容问题,就会增加时空消耗。
链表底层的物理结构是链表,因此根据索引访问的效率不高,即查找元素慢。但是插入和删除不需要移动元素,只需要修改前后元素的指向关系即可,所以插入、删除元素快。而且链表的添加不会涉及到扩容问题。
java
//LinkedList内部声明:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
启示与开发建议
-
Vector基本不使用了。
-
ArrayList底层使用数组结构,查找和添加(尾部添加)操作效率高,时间复杂度为O(1)
删除和插入操作效率低,时间复杂度为O(n)
LinkedList底层使用双向链表结构,删除和插入操作效率高,时间复杂度为O(1)
查找和添加(尾部添加)操作效率高,时间复杂度为O(n) (有可能添加操作是O(1))
-
在选择了ArrayList的前提下,new ArrayList() : 底层创建长度为10的数组。
new ArrayList(int capacity):底层创建指定capacity长度的数组。
如果开发中,大体确认数组的长度,则推荐使用ArrayList(int capacity)这个构造器,避免了底层的扩容、复制数组的操作。
Map接口
哈希表的物理结构
HashMap和Hashtable底层都是哈希表(也称散列表),其中维护了一个长度为2的幂次方的Entry类型的数组table,数组的每一个索引位置被称为一个桶(bucket),你添加的映射关系(key,value)最终都被封装为一个Map.Entry类型的对象,放到某个table[index]桶中。
使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。
HashMap
- HashMap中元素的特点
HashMap中的所有的key彼此之间是不可重复的、无序的。所有的key就构成一个Set集合。--->key所的类要重写hashCode()和equals()
HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成一个Collection集合。--->value所在的类要重写equals()
HashMap中的一个key-value,就构成了一个entry。
HashMap中的所有的entry彼此之间是不可重复的、无序的。所有的entry就构成了一个Set集
java
// 在底层创建了长度为16的Entry[] table的数组
HashMap map = new HashMap();
map.put(key1,value1);
map.get(key1);
map.remove(key1);
HashMap源码解析
2.1 jdk7中创建对象和添加数据过程(以JDK1.7.0_07为例说明):
//创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16];
HashMap<String,Integer> map = new HashMap<>();
...
map.put("AA",78); //"AA"和78封装到一个Entry对象中,考虑将此对象添加到table数组中。
添加/修改的过程:
将(key1,value1)添加到当前的map中:
首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2。
哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置i。
1.1 如果此索引位置i的数组上没有元素,则(key1,value1)添加成功。 ---->情况1
1.2 如果此索引位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2 --->哈希冲突
2.1 如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功。 ---->情况2
2.2 如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals()。要调用key1所在类的equals(),将key2作为参数传递进去。
3.1 调用equals(),返回false: 则(key1,value1)添加成功。 ---->情况3
3.2 调用equals(),返回true: 则认为key1和key2是相同的。默认情况下,value1替换原有的value2。
说明:情况1:将(key1,value1)存放到数组的索引i的位置
情况2,情况3:(key1,value1)元素与现有的(key2,value2)构成单向链表结构,(key1,value1)指向(key2,value2)
随着不断的添加元素,在满足如下的条件的情况下,会考虑扩容:
(size >= threshold) && (null != table[i])
当元素的个数达到临界值(-> 数组的长度 * 加载因子)时,就考虑扩容。默认的临界值 = 16 * 0.75 --> 12.
默认扩容为原来的2倍。
HashMap 在 jdk8与jdk7的不同之处(以jdk1.8.0_271为例):
① 在jdk8中,当我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key,value)时,进行判断,
如果发现table尚未初始化,则对数组进行初始化。
② 在jdk8中,HashMap底层定义了Node内部类,替换jdk7中的Entry内部类。意味着,我们创建的数组是Node[]
③ 在jdk8中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标i中。如果此时角标i位置上有
元素。在jdk7中是将新的(key,value)指向已有的旧的元素(头插法),而在jdk8中是旧的元素指向新的
(key,value)元素(尾插法)。 "七上八下"
④ jdk7:数组+单向链表
jk8:数组+单向链表 + 红黑树
什么时候会使用单向链表变为红黑树:如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上
的多个元素改为使用红黑树的结构进行存储。(为什么修改呢?红黑树进行put()/get()/remove()
操作的时间复杂度为O(logn),比单向链表的时间复杂度O(n)的好。性能更高。
什么时候会使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构退化为单向链表。
属性/字段:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表
//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64
transient Node<K,V>[] table; //数组
transient int size; //记录有效映射关系的对数,也是Entry对象的个数
int threshold; //阈值,当size达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率
Entry
key-value被封装为HashMap.Entry类型,而这个类型实现了Map.Entry接口。
java
public class HashMap<K,V>{
transient Entry<K,V>[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//略
}
}
Node
key-value被封装为HashMap.Node类型或HashMap.TreeNode类型,它俩都直接或间接的实现了Map.Entry接口。
存储到table数组的可能是Node结点对象,也可能是TreeNode结点对象,它们也是Map.Entry接口的实现类。即table[index]下的映射关系可能串起来一个链表或一棵红黑树。
LinkedHashMap
LinkedHashMap 与 HashMap 的关系:
LinkedHashMap 是 HashMap的子类。
LinkedHashMap在HashMap使用的数组+单向链表+红黑树的基础上,又增加了一对双向链表,记录添加的(key,value)的
先后顺序。便于我们遍历所有的key-value。
源码内部定义的Entry 底层结构:LinkedHashMap内部定义了一个Entry
java
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap重写了HashMap的如下方法:
java
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
HashSet和LinkedHashSet的源码分析
HashSet底层使用的是HashMap
LinkedHashSet底层使用的是LinkedHashMap
Set接口
Set集合与Map集合的关系 Set的内部实现其实是一个Map,Set中的元素,存储在HashMap的key中。即HashSet的内部实现是一个HashMap,TreeSet的内部实现是一个TreeMap,LinkedHashSet的内部实现是一个LinkedHashMap。
Set 和 Map 在 Java 中是两种不同的集合类型
Set 存储的是不重复的元素集合,没有键值对的概念,类似于数学中的集合。而 Map 则是存储键值对的数据结构,每个键是唯一的,并且对应一个值。 Set 中的元素没有顺序,可以用来保证数据的唯一性。Map 中的键也保证唯一,但它的键值对有特定的意义,键用于查找对应的值。
虽然它们本质不同,但 Map 中的实现类如 HashMap,它的内部会用到 Set 来存储键的集合(keySet),这样可以避免重复的键。简单来说,Set 和 Map 是互补的,Set 用于存储单个元素,而 Map 用于存储成对的元素。在处理关联数据时,Map 尤其有用。
HashMap相关
说说你理解的哈希算法
hash算法是一种可以从任何数据中提取出其"指纹"的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的"对象"产生的hash code永远是一样的。
Entry中的hash属性为什么不直接使用key的hashCode()返回值呢?
不管是JDK1.7还是JDK1.8中,都不是直接用key的hashCode值直接与table.length-1计算求下标的,而是先对key的hashCode值进行了一个运算,JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
将hashCode值的高位二进制与低位二进制值进行了异或,然高位二进制参与到index的计算中。
为什么要hashCode值的二进制的高位参与到index计算呢?
因为一个HashMap的table数组一般不会特别大,至少在不断扩容之前,那么table.length-1的大部分高位都是0,直接用hashCode和table.length-1进行&运算的话,就会导致总是只有最低的几位是有效的,那么就算你的hashCode()实现的再好也难以避免发生碰撞,这时让高位参与进来的意义就体现出来了。它对hashcode的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。
HashMap是如何决定某个key-value存在哪个桶的呢?
因为hash值是一个整数,而数组的长度也是一个整数,有两种思路:
①hash 值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算效率没有位运算符&高。
②hash 值 & (table.length-1),任何数 & (table.length-1)的结果也一定在[0, table.length-1]范围。
为什么要保持table数组一直是2的n次幂呢?
因为如果数组的长度为2的n次幂,那么table.length-1的二进制就是一个高位全是0,低位全是1的数字,这样才能保证每一个下标位置都有机会被用到。
解决[index]冲突问题
虽然从设计hashCode()到上面HashMap的hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的hashCode值相同,或者hashCode值就算不同,通过hash()函数计算后,得到的index也会存在大量的相同,因此key分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?
JDK1.8之间使用:数组+链表的结构。
JDK1.8之后使用:数组+链表/红黑树的结构
为什么JDK1.8会出现红黑树和链表共存呢?
因为当冲突比较严重时,table[index]下面的链表就会很长,那么会导致查找效率大大降低,而如果此时选用二叉树可以大大提高查询效率。
但是二叉树的结构又过于复杂,占用内存也较多,如果结点个数比较少的时候,那么选择链表反而更简单。所以会出现红黑树和链表共存。
加载因子的值大小有什么关系?
如果太大,threshold就会很大,那么如果冲突比较严重的话,就会导致table[index]下面的结点个数很多,影响效率。
如果太小,threshold就会很小,那么数组扩容的频率就会提高,数组的使用率也会降低,那么会造成空间的浪费。
什么时候树化?什么时候反树化?
java
static final int TREEIFY_THRESHOLD = 8;//树化阈值
static final int UNTREEIFY_THRESHOLD = 6;//反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;//最小树化容量
-
当某table[index]下的链表的结点个数达到8,并且table.length>=64,那么如果新Entry对象还添加到该table[index]中,那么就会将table[index]的链表进行树化。
-
当某table[index]下的红黑树结点个数少于6个,此时,
-
当继续删除table[index]下的树结点,最后这个根结点的左右结点有null,或根结点的左结点的左结点为null,会反树化
-
当重新添加新的映射关系到map中,导致了map重新扩容了,这个时候如果table[index]下面还是小于等于6的个数,那么会反树化
key-value中的key是否可以修改?
key-value存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的key-value,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。key值不能修改
这个规则也同样适用于LinkedHashMap、HashSet、LinkedHashSet、Hashtable等所有散列存储结构的集合。
JDK1.7中HashMap的循环链表是怎么回事?如何解决?
避免HashMap发生死循环的常用解决方案:
- 多线程环境下,使用线程安全的ConcurrentHashMap替代HashMap,推荐
- 多线程环境下,使用synchronized或Lock加锁,但会影响性能,不推荐
- 多线程环境下,使用线程安全的Hashtable替代,性能低,不推荐
HashMap死循环只会发生在JDK1.7版本中,主要原因:头插法+链表+多线程并发+扩容。
在JDK1.8中,HashMap改用尾插法,解决了链表死循环的问题。