HashMap相关知识
什么是Map
Map是一个接口类,该类没有继承自Collection,该类中存储的是<k,v>结构的键值对,并且k一定是唯一的,不能重复。
Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类
注意:
- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
- Map中存放键值对的Key是唯一的,value是可以重复的。
- 在Map中插入键值对时,key不能为空,否则就会抛NullPointerException异常,但是value可以为空。
- Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
- Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
- Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
Map的两种实例化方式
HashMap
底层数据结构
在jdk1.7之前,HashMap底层是由数组+链表的方式实现的,在jdk1.8之后,改成了数组+链表+红黑树。
哈希表
什么是哈希?
哈希是用来进行高效查找的一种数据结构,该种数据结构是通过某种方式(哈希函数)将元素与其在表格中的存储位置建立一一对应的关系,在查找时就不需要进行遍历,因此可以得到高效的查找。
哈希中可能会存在哈希冲突(碰撞),即不同元素通过相同哈希函数计算出相同的哈希地址。例如:占座
常见哈希函数
- 直接定址法
- 除留余数法
- 折叠法
- 平方取中法
- 随机数法
- 数学分析法
注意:
不论一个哈希函数设计有多精妙,都不能完全解决哈希冲突——哈希函数设计越精妙,产生冲突的概率越低。
解决哈希冲突的方式: - 闭散列:从发生哈希冲突的位置开始,找“下一个”空位置
1)线性探测:从发生哈希冲突的位置开始,逐个挨着依次往后查找,所以必须给每个位置设置状态,EMPTY、EXIST、DELETE
优点:找下一个地址的方式简单
缺点:容易产生数据堆积——原因:找下一个空位置时挨着往后依次查找
2)二次探测
假设第一次计算出的哈希地址H0,第i次探测时,哈希地址为H(i)
H(i)=H0+i2 H(i)=H0-i2
优点:解决了线性探测容易产生数据堆积的问题
缺点:如果空位置比较少,可能需要探测很多次 - 开散列:链地址法 、拉链法
哈希表中没有直接存元素,每个位置将来挂接都是一个链表,即:将发生冲突的元素通过链表的方式组织起来
实现原理
1.HashMap实现了Map接口
2.HashMap的默认初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
3.HashMap的最大容量是230
static final int MAXIMUM_CAPACITY = 1 >> 30
4.HashMap的默认负载因子是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
5.何时链表和红黑树相互转化
若链表中节点个数超过8时,会将链表转化为红黑树;若红黑树中节点小于6时,红黑树退还为链表;如果哈希桶中某条链表的个数超过8,并且桶的个数超过64时才会将链表转换为红黑树,否则直接扩容。
6. HashMap桶中放置的节点—该节点是一个单链表的结构
7.哈希函数
hashFunc(x) = x % capacity;
8.扩容机制
// 每次都是将cap扩展到大于cap最近的2的n次幂
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
9.LinkedHashMap
- LinkedHashMap继承自HashMap,并实现了Map接口
- LinkedHashMap底层使用了哈希桶和双向链表两种结构
- LinkedHashMap需要重写HashMap中的:afterNodeInsertion/afterNodeAcess/afterNodeRemove等方法
- LinkedHashMap使用迭代器访问时,可以保证一个有序的结果,注意此处的有序不是自然有序,而是元素的插入次序。另外,当向哈希表中重复插入某个键的时候,不会影响到原来的有序性。也就是说,假设你插入的键的顺序为1、2、3、4,后来再次插入2,迭代时的顺序还是1、2、3、4,而不会因为后来插入的2变成1、3、4、2
- LinkedHashMap可以作为LRU来使用,但是要重写removeEldestEntry方法。
模拟实现
//哈希桶---> 数组 + 链表实现的---> 可以用来帮助用户快速定位要将元素插入到那个链表来组织链表
//数组中存储的元素实际为元素的引用
public class HashBucket {
public static class Node{
int key;
int value;
Node next;
Node(int key, int value){
this.key = key;
this.value = value;
next = null;
}
}
//哈希桶中的成员数据
Node[] table;
int capacity;//表格的容量--》桶的个数
int size;//有效元素的个数
double loadFact = 0.75;
public HashBucket(int initCap){
//保证哈希桶的容量至少为10
capacity = initCap;
if(initCap < 10){
capacity = 10;
}
table = new Node[capacity];
size = 0;
}
public int put(int key, int value){
//1.通过哈希函数,计算key所在的桶号
int bucketNo = hashFunc(key);
//2.在bucketNo桶中检测key是否存在
//检测方式:遍历链表
Node cur = table[bucketNo];
while(null != cur){
if(cur.key == key){
int oldValue = cur.value;
cur.value = value;
return oldValue;
}
cur = cur.next;
}
//3.key不存在,将key-value插入到哈希桶中
cur = new Node(key, value);
cur.next = table[bucketNo];
table[bucketNo] = cur;
size++;
return value;
}
//将哈希桶中为key的键值对删除
public boolean remove(int key){
//1.通过哈希函数计算key的桶号
int bucketNo = hashFunc(key);
//2.在bucketNo桶中找到key对应的节点,找到后删除
Node cur = table[bucketNo];
Node prev = null;
while(null != cur){
if(cur.key == key){
//找到与key所对应的节点,将其删除
if(null == prev){
//删除的节点刚好是第一个节点
table[bucketNo] = cur.next;
}
else{
//删除其他节点
prev.next = cur.next;
}
--size;
return true;
}
else{
prev = cur;
cur = cur.next;
}
}
return false;
}
public boolean containsKey(int key){
//1.计算key所在的桶号
int bucketNo = hashFunc(key);
//2.在bucketNo桶中找key
Node cur = table[bucketNo];
while(null != cur){
if(cur.key == key){
return true;
}
cur = cur.next;
}
return false;
}
public boolean containsValue(int value){
for(int bucketNo = 0; bucketNo < capacity; bucketNo++){
Node cur = table[bucketNo];
while(null != cur){
if(cur.value == value){
return true;
}
}
}
return false;
}
public int size(){
return size;
}
public boolean empty(){
return 0 == size;
}
private void resize(){
//装载因子超过0.75时进行扩容,按照2倍的方式进行扩容
if(size*10 / capacity > loadFact*10){
int newCap = capacity * 2;
Node[] newTable = new Node[newCap];
//将table中所有的节点搬到newTable中
for(int i = 0; i < capacity; i++){
Node cur = table[i];
//将table中i号桶中所对应链表中所有的节点插入到newTable中
while(null != cur){
table[i] = cur.next;
//将cur节点插入到newTable中
//1.计算cur在newTable中的桶号
int bucketNo = cur.key % newCap;
//2.将cur插入到newTable中
cur.next = newTable[bucketNo];
newTable[bucketNo] = cur;
//取table中i号捅的下一个节点
cur = table[i];
}
}
table = newTable;
capacity = newCap;
}
}
private int hashFunc(int key){
return key % capacity;
}
}
TreeMap
TreeMap是一个Map实现,默认情况下会根据其键的自然顺序对其所有条目进行排序。
底层数据结构
红黑树:一棵二叉搜索树 + 增加节点的颜色限制以及性质约束,即最长路径节点个数一定不会超过最短路径中节点个数的2倍称为平衡树。
红黑树
红黑树是一棵平衡二叉树。
- 每个节点都只能是红色或黑色
- 根节点是黑色
- 每个叶节点(NIL节点、NULL)是黑色的
- 如果一个节点是红的,则它的两个子节点都是黑的,也就是说一条路径上不能出现相邻的两个红色节点
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
实现原理
二叉搜索树:进行中序遍历,可以得到一个有序的序列。
树中最左侧节点一定是最小的节点,最右侧节点一定是最大的。
基本操作
a.查找
在二叉搜索树中找某个节点——最差情况下比较的就是树的高度
b.插入
空树——>直接插入,然后返回
非空:
①在二叉搜索树中找待插入节点的位置,在找的过程中,必须要保存双亲
②插入新节点
c.删除
①找待删除节点在树中的位置,若未找到,直接返回;反之,则继续;
②删除该节点cur
cur有以下几种情况:
1)cur是一个叶子节点
2)cur只有右孩子
3)cur只有左孩子
4)cur左右孩子均存在,该节点不能直接删除,在其子树中找一个替代节点然后再进行删除
模拟实现
public class BSTree {
public static class BSTNode{
BSTNode left = null;
BSTNode right = null;
int val;
public BSTNode(int val){
this.val = val;
}
}
private BSTNode root = null;
boolean contains(int val){
BSTNode cur = root;
while(cur != null){
if(val == cur.val){
return true;
}
else if(val < cur.val){
cur = cur.left;
}
else{
cur = cur.right;
}
}
return false;
}
//将val插入到二叉搜索树中,插入成功返回true,否则返回false
public boolean put(int val){
if(null == root){
root = new BSTNode(val);
return true;
}
BSTNode cur = root;
BSTNode parent = null;
while(cur != null){
parent = cur;
if(val < cur.val){
cur = cur.left;
}
else if(val > cur.val){
cur = cur.right;
}
else{
return false;
}
}
//找到带插入结点的位置 ---》插入新节点
//将新节点插到parent的左侧或右侧
cur = new BSTNode(val);
if(val < parent.val){
parent.left = cur;
}
else{
parent.right = cur;
}
return true;
}
boolean remove(int val){
if(null == root){
return false;
}
//非空--找待删除节点的位置
BSTNode cur = root;
BSTNode parent = null;
while(cur != null){
if(val == cur.val){
break;
}
else if(val < cur.val){
parent = cur;
cur = cur.left;
}
else{
parent = cur;
cur = cur.right;
}
}
//没有找到
if(cur == null){
return false;
}
//已经找到待删除节点在树中的位置
//必须要对cur的孩子节分点分情况
//1.没有孩子
//2.只有左孩子
//3.只有右孩子
//4.左右孩子均存在
if(null == cur.left){
//cur只有右孩子
if(null == parent){
//cur双亲不存在
root = cur.right;
}
else{
//双亲存在
if(cur == parent.left){
parent.left = cur.right;
}
else{
parent.right = cur.right;
}
}
}
else if(null == cur.right){
//cur只有左孩子
if(null == parent){
//双亲不存在
root = cur.left;
}
else{
//双亲存在
if(parent.right == cur){
parent.right = cur.left;
}
else{
parent.left = cur.left;
}
}
}
else{
//cur节点的左右孩子均存在--不能直接删除
//在cur子树中找一个替代节点删除
//方式一:在其右子树中找最小的节点:即最左侧节点
//方式二:在其左子树中找最大的节点:及最右侧节点
BSTNode del = cur.right;
parent = cur;
while(null != del.left){
parent = del;
del = del.left;
}
//替代节点找到
cur.val = del.val;
//删除替代节点
if(del == parent.left){
parent.left = del.right;
}
else{
parent.right = del.right;
}
}
return true;
}
public void inOrder(){
inOrder(root);
}
private void inOrder(BSTNode root){
if(null != root){
inOrder(root.left);
System.out.println(root);
inOrder(root.right);
}
}
//最左侧节点--最小的节点
public int mostLeft(){
if(null == root){
//抛异常--空指针异常
}
BSTNode cur = root;
while(cur.left != null){
cur = cur.left;
}
return cur.val;
}
//最右侧节点
public int mostRight(){
if(null == root){
//抛异常--空指针异常
}
BSTNode cur = root;
while(cur.right != null){
cur = cur.right;
}
return cur.val;
}
}
HashMap与TreeMap的比较
相同点
都存储的是k-v键值对
都实现了Map接口
不同点
TreeMap | HashMap |
---|---|
红黑树 | 哈希桶 |
O(logN) | O(1) |
关于key有序 | 不一定 |
需要key有序 | 不关心key有序,需要更高的查询效率 |