目录
Map 和 set 是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。
在搜索的过程中国,一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型有两种:
1. 纯 key 模型 (在英文字典中查找英文单词)
2. Key-Value 模型 (统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>)
而Map中存储的就是key-value的键值对,Set中只存储了Key。
一、 Map
Map是一个接口,并且由上图可以看出来,Map并没有继承自 Collection。
1、 Map 的常用方法
方法 | 解释 |
V get (Object key) | 返回 key 对应的 value |
V getOrDefault (Object key, V defaultValue) | 返回 key 对应的 value,若 key 不存在,返回默认值 |
V put (K key, V value) | 设置 key 对应 的 value |
V remove (Obgect key) | 删除 key 对应的映射关系 |
Set <K> keySet ( ) | 返回所有 key 的不重复集合 (返回值是一个 set) |
Collection < V > values ( ) | 返回所有 value 的可重复集合 |
Set < Map.Entry < K, V >> entrySet ( ) | 返回所有的 key-value 映射关系 |
boolean containsKey (Object key) | 判断是否包含 key |
boolean containsValue (Object value) | 判断是否包含 value |
在使用时需要注意:
(1) Map 是一个接口,因此我们使用这些方法时,要通过 HashMap 或者 TreeMap 来使用。
(2) Map 中存放的键值对的 key 是唯一的,value 是可以重复的。(key相同时,存放value会进行覆盖)。
(3) Map 中的 key 可以全部分离出来,存储在 Set 中;(key 不能重复)
(4) Map 中的 value 可以全部分离出来,存储在 Collection 的任何一个子集合中。(value 可以重复)
(5) Map 中的 key 不能直接修改。如果要进行修改,必须先删除,然后再重新插入。
2、 Map.Entry< K, V >
Map.Entry< K, V > 是 Map 内部实现的用来存放 < key, value >键值对 的 内部类。正因为他存的是 Map 中的键值对,所以我们可以根据其内部的方法来对 key 或 value 进行获取。
方法 | 解释 |
K getKey ( ) | 返回 entry 中的 key |
V getValue ( ) | 返回 entry 中的 value |
V setValue (V value) | 将键值对中的 value 替换为指定的 value |
我们可以这样通俗的认为:每一个 entry 都存储了一个键值对
而对于我们上面的Map 中的方法 Set < Map.Entry < K, V >> entrySet ( ) 获取映射关系,返回值是一个Set,而 Set 中存储的对象,是一个个键值对。
3、 TreeMap 和 HashMap
TreeMap 和 HashMap 的最本质区别是底层结构的实现: TreeMap是红黑树,而HashMap是哈希桶。
Map | TreeMap | HashMap |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找 时间复杂度 | O( log(N) ) | O(1) |
是否有序 | 关于 key 有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找 区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较 与 覆写 | key 必须能够比较 否则会抛出ClassCastException异常 | 自定义类需要 覆写 equals 方法 和 hashCode 方法 |
应用场景 | 需要 key 有序的场景下 | key 是否有序不关心,关心是否需要更高的时间性能 |
由上面的比较可以发现,HashMap 增删查改 的时间复杂度是 O(1),是非常高效的。因为他在插入元素的过程中是通过哈希函数来进行计算然后插入的。因此在查找元素时,不会对所有元素进行遍历,而是再通过这个哈希函数来找到要查找的元素。
因此我们通常使用的是 HashMap,并且如果传入的 key-value 中的类型是自定义类型的话,一定要重写 equals 方法和 hashCode 方法。
4、ConcurrentHashMap(多线程 推荐)
① ConcurrentHashMap 是线程安全的 HashMap。
② ConcurrentHashMap 减少了锁冲突,就让锁加到每个链表的头结点上(锁桶);
③ ConcurrentHashMap 只是针对 写操作 加锁了,读操作并没有加锁,而只是使用;
④ ConcurrentHashMap 中更广泛的使用CAS,进一步提高效率,(比如维护 size 操作);
⑤ ConcurrentHashMap 针对扩容,进行了巧妙的化整为零:
对于HashTable 来说,只要你这次 put 触发了扩容,就会一口气搬运完,就会导致这次的 put 非常卡顿。
对于CocurrentHashMap,每次操作只搬运一点点,通过多次操作完成整个搬运过程。同时维护一个新的 HashMap 和一个 旧的HashMap,查找的时候即需要查旧的也要查新的,而插入的时候就只插入到新的HashMap 中。直到全部搬运完在销毁旧的。
二、 Set
Set 与 Map 主要的不同之处有两点:① Set 是继承自 Collection 的接口类;② Set 中只存储了 key。
1、 Set 的常用方法
方法 | 解释 |
boolean add (E e) | 添加元素,但重复的元素不会被添加成功 |
void clear ( ) | 清空集合 |
boolean contains (Object o) | 判断 o 是否在集合中 |
Iterator < E > iterato ( ) | 返回一个迭代器 |
boolean remove (Object o) | 删除集合中的 o |
int size ( ) | 返回 set 中元素的个数 |
boolean isEmpty ( ) | 检测 set 是否为空, 空返回true,否则返回 false |
Object [] toArray ( ) | 将 set 中的元素转换为 数组 返回 |
boolean containsAll (Collection<?> c) | 检测 集合 c 中的元素是否在 set 总全部存在, 是返回 true,否则返回 false |
boolean addAll (Collection < ? extrnds E > c) | 将集合 c 中的元素添加到 set 中,可以达到去重效果 |
在使用时要注意:
(1) Set 最大的功能:去重。
(2) Set 是继承自 Collection 的一个接口类。使用这些方法需要使用实现这一接口的类:TreeSet 和 HashSet(常用类)。还有一个类 LinkedHashSet,它是在 HashSet的基础上维护了一个双向链表来记录元素的插入顺序的。(HashSet 中 存储时无序的)
(3) Set 的底层是使用 Map 来实现的,其使用 key 与 Object 的一个默认对象,作为键值对插入到 Map 中的。
(4) Set 中 的 key 不能修改,要修改,就得先删除原来的,再重新插入。
(5) Set 中不能插入 null。
2、 TreeSet 和 HashSet
Set | TreeSet | HashSet |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找 时间复杂度 | O( log(N) ) | O(1) |
是否有序 | 关于 key 有序 | 不一定有序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找 区别 | 按照红黑树的特性来进行插入和删除 | 1、先计算 key 哈希地址 2、再进行插入和删除 |
比较 与 覆写 | key 必须能比较 否则会抛出 ClassCastException异常 | 自定义类型需要覆写 equals 方法 和 hashCode 方法 |
应用场景 | 需要 key 有序的场景下 | key 不要求有序,关心更高的时间性能的情况 |
与 Map 类似,我们更常用的是 HashSet。
三、 搜索树(二叉排序树)
搜索树又叫二叉排序树,它是一棵树,并且具有以下性质:
- 若它的左子树不为空,则左子树上所有结点的值都小于根结点的值;
- 若它的右子树不为空,则右子树上所有结点的值都大于根结点的值;
- 它的左右子树也分别为二叉搜索树。
如图所示就是一个二叉搜索树。
1、 自己实现内部方法
首先定义一个结点类:
//结点
class Node {
public int value;
public Node left;
public Node right;
public Node(int val, Node left, Node right) {
this.value = val;
this.left = left;
this.right = right;
}
public Node(int val) {
this.value = val;
this.left = left;
this.right = right;
}
}
(1)查找
查找操作较为简单,因为二叉搜索树本质上是一棵树,所以我们只需要遍历树中的每个结点,判断其值是否和目标值相等即可。
//查找
public boolean search(int key) {
Node cur = root;
while(cur != null) {
if (cur.value < key) {
cur = cur.right;
} else if(cur.value > key) {
cur = cur.left;
} else {
return true;
}
}
return false;
}
(2)插入
思路:考虑树是否为 空树,若为空树,直接插入即可;若不是空树,进行接下来的操作。比较 目标元素值 和 当前结点值 的大小,若目标值 小于 当前结点值,当前结点指向当前结点的左孩子进行判断;当目标值 大于 当前结点值,当前结点指向当前结点的右孩子继续进行判断;当目标值 等于 当前结点值时,是不合法的。一直这样循环判断。
判断到最后当前结点指向空了,此时要插入,没有指向其父母结点的变量。因此我们要定义一个parent 结点,指向当前结点的父母结点,每更新一次指向当前结点的引用值,就更新一个parent的值。
最后根据目标元素和parent引用指向的结点的大小比较,确定插到左孩子还是右孩子上。
因此:对于二叉查找树来说,插入的元素一定在叶子结点上。
- cur 和 parent 来找到 value 需要存储的位置
- praent.value 和 value 比较大小,确定格式在左边还是在右边进行插入
//插入
//对于二叉搜索树来说,插入的数据一定在叶子结点
public boolean insert(int val) {
if(root == null) {
root = new Node(val);
return true;
}
//树不为空
Node p = root;
Node cur = root;
while(cur != null) {
if (val > cur.value) {
p = cur;
cur = cur.right;
} else if (val < cur.value) {
p = cur;
cur = cur.left;
} else {
//不能插入相同的数据
return false;
}
}
Node node = new Node(val);
if (val > p.value) {
p.right = node;
} else {
p.left = node;
}
return true;
}
(3)删除
删除的操作较为复杂,需要考虑很多种情况。假设要删除的结点为 cur,其双亲结点为 parent。
① 当 cur.left == null 时,即要删除的结点没有左孩子时:
a、 cur == root,则 root = cur.right
b、 cur != root,cur == parent.left,则 parent.left = cur.right
c、 cur != root,cur == parent.right,则 parent.right = cur.right
② 当 cur.right == null 时,即要删除的结点没有右孩子时:
a、 cur == root,则 root = cur.left
b、 cur != root,cur == parent.left,则 parent.left = cur.left
c、 cur != root,cur == parent.right,则 parent.right = cur.left
③ 当 cur.left != null && cur.right != null 时,即要删除的结点既有左孩子又有右孩子时:
使用替换法: 寻找替罪羊,在 cur 的左子树中找最大值 或者 在右子树中找最小值。找到之后将其元素值和 cur 的值进行交换,然后删除这个被交换的元素结点即可。(删除后的一系列调整操作还是上面的这些情况)
注意:在找最大值(最小的)的过程中,还需要一个 tar 引用以及 tarp引用,tar 指向当前结点,tarp 指向其双亲结点。因此最后还需要判断 tar 是 tarp 的左孩子还是右孩子,然后再进行引用的转换。
//删除
//通过两个方法
//cur 为要删除的结点
//比较麻烦:三种情况
public boolean remove(int key) {
Node cur = root;
Node p = null;
while(cur != null) {
if (cur.value == key) {
removeNode(cur,p);
return true;
} else if(key > cur.value) {
p = cur;
cur = cur.right;
} else {
p = cur;
cur = cur.left;
}
}
return false;
}
public void removeNode(Node cur, Node p) {
if (cur.left == null) {
if (cur == root) {
root = cur.right;
} else if (cur == p.left) {
p.left = cur.right;
} else {
//cur == p.right
p.right = cur.right;
}
} else if (cur.right == null) {
if (cur == root) {
root = cur.left;
} else if (cur == p.left) {
p.left = cur.left;
} else {
//cur === p.right
p.right = cur.left;
}
} else {
//cur 既有左子树又有右子树
//找替罪羊替换:
//在左子树中找最大值,或者在右子树中找最小值(肯定是叶子)
//这里找的是右子树(right)的最大值(left)
Node tar = cur.right;
Node tarp = cur;
while(tar.left != null) {
tarp = tar;
tar = tar.left;
}
//循环结束,此时 tar 指向目标结点(tar 的左子树为空)
//替换
cur.value = tar.value;
if (tarp.left == tar) {
//说明 tar 是 tarp 的左孩子
tarp.left = tar.right;
} else {
//tar 是 tarp 的右孩子
tarp.right = tar.right;
}
}
}
(4)整体代码
//结点
class Node {
public int value;
public Node left;
public Node right;
public Node(int val, Node left, Node right) {
this.value = val;
this.left = left;
this.right = right;
}
public Node(int val) {
this.value = val;
this.left = left;
this.right = right;
}
@Override
public String toString() {
return "value: " + value;
}
}
public class BinarySearchTree {
public Node root = null;
//查找
public boolean search(int key) {
Node cur = root;
while(cur != null) {
if (cur.value < key) {
cur = cur.right;
} else if(cur.value > key) {
cur = cur.left;
} else {
return true;
}
}
return false;
}
//插入
//对于二叉搜索树来说,插入的数据一定在叶子结点
public boolean insert(int val) {
if(root == null) {
root = new Node(val);
return true;
}
//树不为空
Node p = root;
Node cur = root;
while(cur != null) {
if (val > cur.value) {
p = cur;
cur = cur.right;
} else if (val < cur.value) {
p = cur;
cur = cur.left;
} else {
//不能插入相同的数据
return false;
}
}
Node node = new Node(val);
if (val > p.value) {
p.right = node;
} else {
p.left = node;
}
return true;
}
//删除
//通过两个方法
//cur 为要删除的结点
//比较麻烦:三种情况
public boolean remove(int key) {
Node cur = root;
Node p = null;
while(cur != null) {
if (cur.value == key) {
removeNode(cur,p);
return true;
} else if(key > cur.value) {
p = cur;
cur = cur.right;
} else {
p = cur;
cur = cur.left;
}
}
return false;
}
public void removeNode(Node cur, Node p) {
if (cur.left == null) {
if (cur == root) {
root = cur.right;
} else if (cur == p.left) {
p.left = cur.right;
} else {
//cur == p.right
p.right = cur.right;
}
} else if (cur.right == null) {
if (cur == root) {
root = cur.left;
} else if (cur == p.left) {
p.left = cur.left;
} else {
//cur === p.right
p.right = cur.left;
}
} else {
//cur 既有左子树又有右子树
//找替罪羊替换:
//在左子树中找最大值,或者在右子树中找最小值(肯定是叶子)
//这里找的是右子树(right)的最大值(left)
Node tar = cur.right;
Node tarp = cur;
while(tar.left != null) {
tarp = tar;
tar = tar.left;
}
//循环结束,此时 tar 指向目标结点(tar 的左子树为空)
//替换
cur.value = tar.value;
if (tarp.left == tar) {
//说明 tar 是 tarp 的左孩子
tarp.left = tar.right;
} else {
//tar 是 tarp 的右孩子
tarp.right = tar.right;
}
}
}
//中序遍历
public void inOrder(Node root) {
if (root == null) {
return;
}
inOrder(root.left);
System.out.print(root.value + " ");
inOrder(root.right);
}
//测试
public static void main(String[] args) {
int[] arr = {23,4,76,9,46,98,1,6};
BinarySearchTree binarySearchTree = new BinarySearchTree();
for (int i : arr) {
binarySearchTree.insert(i);
}
binarySearchTree.inOrder(binarySearchTree.root);
System.out.println("查找:");
System.out.println(binarySearchTree.search(4));
System.out.println(binarySearchTree.search(5));
System.out.println("删除:");
System.out.println(binarySearchTree.remove(4));
System.out.println(binarySearchTree.remove(10));
}
}
2、 性能分析
因为插入和删除操作都必须先进性查找,因此查找效率就代表的二叉搜索树中各个操作的性能。而对于有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,比较次数越多。
- 最优情况下,二叉搜索树为 完全二叉树,其平均次数为:log(N)
- 最差情况下,二叉搜索树 退化成 单支树,其平均次数为:N/2 (性能大大降低)
因此,对于 TreeMap 和 TreeSet 来说,实际上是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础上+颜色以及红黑树性质。
四、哈希表
引入哈希表的概念:
之前学习的顺序结构以及平衡树中,元素和其存储位置没有任何对应关系,因此我们在查找某个元素时,就得经过大量的关键字的比较。这样的查找时间复杂度就为 O(N),平衡树中就为树的高度,即 O( log(N) )。可以得出搜索的效率取决于关键字比较的次数。
因此我们想要提高查找的效率,就想可以不经过任何比较,一次性就可以找到我们要找的元素。
为此,如果构造一种存储结构,通过某种函数将元素和其存储位置之间建立一一对应的联系,根据这样的关系进行插入元素。当我们要查找这个元素时,再通过这个函数就可以不经过任何关键字比较,就可以找到这个元素了。效率将会大大提高。
这样的方法即为 哈希方法(散列方法),哈希方法中使用的转换函数 为 哈希函数(散列函数),构造出来的结构 为 哈希表(散列表)。
注意:这里的哈希函数并不是唯一确定的,而是人为设置的。
1、冲突
下面用一个例子来解释什么叫冲突。
假设我们有一个集合:{2,5,7,10,6,15},并且人为设置的哈希函数:hash( key ) = key / capacity ;capacity 为存储元素底层空间大小(这里为数组长度)。
冲突:不同关键字通过相同的哈希函数计算出相同的哈希地址,这种现象称为 哈希冲突(哈希碰撞)。
2、冲突 —— 避免
冲突的发生时必然的,我们能做的只有尽量降低冲突率。有两个方面原因引起哈希冲突:
- 1、哈希函数设计
- 2、负载因子
(1)哈希函数设计
引起哈希冲突的一个原因就是:哈希函数的设计不够合理,计算出来的哈希地址重复过多。
哈希函数的设计原则:
- 哈希函数的定义域必须包含需要存储的全部关键码。
- 哈希函数计算出来的地址能均匀的分布在整个空间。
- 哈希函数应该比较简单。
常见的哈希函数:
- 1、直接定值法:直接取关键字任意的某个 线性函数 为散列地址。优点:简单、均匀分布;缺点:需要事先知道关键字的分布场景;使用场景:查找比较小且连续的情况。
- 2、除留余数法:设散列表中允许的地址数为m,取一个不大于 m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash( key ) = key % p (p <= m),将关键字转换成哈希地址。
- 3、平方取中法、折叠法、随机数法、数学分析法........
(2)调节负载因子
散列表的负载因子 α : α = 散列表中的元素个数 / 散列表的长度 。
因此,我们正常的负载因子大概在 0.75,当负载因子过大时,我们就需要扩容了。
3、 冲突 —— 解决
解决哈希冲突的两种常见方法:闭散列 和 开散列。
(1)闭散列
闭散列 也称为 开放地址法:当发生哈希冲突时,若哈希表没有被装满,就将 key 存放到冲突位置的下一个空位置中。有两种找空位置的方法:线性探测、二次探测。
- 线性探测:从发生冲突的位置开始,一次向后探测,直到找到下一个空位置为止。
- 二次探测:找下一个空位置的方法为:Hi = (H0 + i²)% m,其中 i = 1,2,3......,H0是通过散列函数 Hash( x ) 对元素的关键码 key进行计算得到的值,m是表的大小。
- 注意:在搜索时可以不考虑表装满的情况,但是在插入时必须确保表的 装载因子 a 不超过0.5,如果超过必须考虑扩容。
(2)开散列(哈希桶)
开散列又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表连接起来,个链表的头结点存储在哈希表中。
类似这种情况:
开散列中每个桶中放的都是发生哈徐冲突的元素。可以认为是把一个大集合中的搜索问题转化为在小集合中做搜索。
当冲突还没有办法解决时,我们就需要继续转化:
- 每个桶背后都是另一个哈希表
- 每个桶背后都是一棵搜索树(红黑树)...
4、* 自己实现一个哈希桶 *
要实现 插入以及查找两个方法,以及考虑负载因子、扩容的情况:
(1)key 是可比较的类型(int)
//自己实现哈希表
//实现 插入和查找两个方法
public class HashBuck {
//通过内部类来定义结点
public class Node {
public int key;
public int value;
public Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
this.next = null;
}
}
private Node[] array;
//记录元素个数
private int usedSize;
//标准负载因子
private static final double DEFAULT_LOAD_FACTOR = 0.75;
//构造方法
//哈希表的初始容量人为设置为 10
public HashBuck() {
this.array = new Node[10];
}
//put方法:插入 key-value键值对
//思路:
//1.找到 key 的位置
//2.遍历链表,判断是否已经有 key 了,若有则进行覆盖
//3.没有 key 值,头插法(或者尾插法),将结点插入
//4.元素插入成功之后,检查当前散列表的负载因子,若超过就进行扩容
public void put(int key,int value){
//1.找位置
//我们这里自己设置的散列函数
int index = key % this.array.length;
Node node = new Node(key, value);
//2.遍历链表,判断
Node cur = array[index];
while(cur != null) {
if (cur.key == key) {
cur.value = value;
return;
}
cur = cur.next;
}
//3.头插法
node.next = cur;
array[index] = node;
this.usedSize++;
//4.判断负载因子:
if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
resize();
}
}
//求负载因子
public double loadFactor() {
return 1.0 * usedSize/array.length;
}
//扩容
//将数组长度翻倍,并且对其中的元素的散列地址重新计算插入
public void resize() {
Node[] newArray = new Node[array.length * 2];
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while(cur != null) {
int index = cur.key % newArray.length;
//此时需要将 cur 转移到新数组中:头插/尾插的形式插入到新的数组对应的下标的链表中
//但是要考虑:如果 cur 的后面还有元素怎么办
//如果一块转移过去的话,很可能后面这个元素的散列地址和 cur 元素的散列地址不同
//因此我们需要将 cur.next 再进行存储
Node curNext = cur.next;
//头插
cur.next = newArray[index];
newArray[index] = cur;
//头插结束,将 cur 指回原来的 cur.next
cur = cur.next;
}
}
array = newArray;
}
//get方法:通过 key 的值获取 value 值
//1.找到 key 所在的位置
//2.遍历该位置下的所有链表,看是否有 key值,若没有则返回 -1
public int get(int key) {
int index = key % this.array.length;
Node cur = this.array[index];
while(cur != null) {
if (cur.key == key) {
return cur.value;
}
cur = cur.next;
}
return -1;
}
//测试
public static void main(String[] args) {
HashBuck hashBuck = new HashBuck();
hashBuck.put(1,6);
hashBuck.put(2,7);
hashBuck.put(5,8);
hashBuck.put(6,9);
hashBuck.put(9,1067);
hashBuck.put(7,18);
hashBuck.put(8,16);
hashBuck.put(10,109);
hashBuck.put(12,100);
hashBuck.put(11,19);
System.out.println(hashBuck.get(11));
}
}
(2)key 是自定义类的对象(person)
大致思路方法和上面类似,最主要的区别在于 put 方法 和 get 方法中,元素的处理方面。因此这里只写了二者不同的地方,负载因子和扩容都是一样的。
put 方法:
- 1. 找到 key 的存放下标
- 之前因为 key 是int类型,可以直接使用函数进行计算得到散列地址,但是我们这里的 key 是个泛型。就以自定义类 person 举例,并不能直接通过计算得到散列地址,因此我们要通过在person类中重写 hashCode方法来获得一个 hash 数值,然后再进行散列函数的计算得到散列地址。
- 2. 遍历该下标下的所有链表结点,判断是否有 key,若有则进行替换,若没有则进行头插。
- 在这个过程中,我们要不断的进行比较,因此我们的自定义类型也要具备比较能力,因此还要在自定义类中重写 equals方法。
get 方法类似
//定义person类
//person 类中只有一个 ID。当 ID相同时,我们认为是同一个人
class Person{
private String ID;
public Person(String ID) {
this.ID = ID;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return ID == person.ID;
}
@Override
public int hashCode() {
return Objects.hash(ID);
}
@Override
public String toString() {
return "Person{" +
"ID=" + ID +
'}';
}
}
//写一个 泛型 的哈希桶
public class HashBuck2< K, V > {
//Node类定义为内部类
static class Node<K, V> {
public K key;
public V value;
public Node<K, V> next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
//开始定义哈希桶的属性
//数组以及存放元素个数
public Node<K,V>[] array =(Node<K,V>[]) new Node[10];
public int uesdSized;
//put 方法
//1. 找到 key 的存放下标
//之前因为 key 是int类型,可以直接使用函数进行计算得到散列地址,但是我们这里的 key 是个泛型
//就以自定义类 person 举例,并不能直接通过计算得到散列地址,
// 因此我们要通过在person类中重写 hashCode方法来获得一个 hash数值,然后再进行散列函数的计算得到散列地址。
//2. 遍历该下标下的所有链表结点,判断是否有 key,若有则进行替换,若没有则进行头插
//在这个过程中,我们要不断的进行比较,因此我们的自定义类型也要具备比较能力,因此还要在自定义类中重写 equals方法。
public void put(K key, V value) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur = array[index];
while(cur != null) {
if (cur.key.equals(key)) {
cur.value = value;
return;
}
cur = cur.next;
}
//循环结束,说明没有 key 这个值,进行头插法
Node<K,V> node = new Node<>(key, value);
array[index] = node;
node.next = array[index];
//头插成功,存入哈希桶的元素个数+1
this.uesdSized++;
}
//get 方法
//和 put 方法类似
//1. 找到key的位置
//2. 遍历对应位置下的链表,如果有 key 值,就返回,没有 key 值返回 null
public V get(K key) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur = array[index];
while (cur != null) {
if (cur.key.equals(key)) {
return cur.value;
}
cur = cur.next;
}
return null;
}
//测试
public static void main(String[] args) {
Person person1 = new Person("123");
Person person2 = new Person("123");
HashBuck2<Person,String> hashBuck2 = new HashBuck2<>();
hashBuck2.put(person1,"我");
System.out.println(hashBuck2.get(person2));
}
}
五、HashMap 和 HashSet 中的哈希表
HashMap 和 HashSet 即是利用哈希桶实现的 Map 和 Set。在 HashMap 的源码内部: