各位看官早安午安晚安呀
如果您觉得这篇文章对您有帮助的话
欢迎您一键三连,小编尽全力做到更好
欢迎您分享给更多人哦
今天我们来学习Map/Set的底层实现
目录
问题一:hash会出现负数?数组越界
一:什么是二叉搜索树?
1.1:创建一个二叉搜索树
1.2.移除一个节点:
1.3.二叉搜索树的缺陷:
1.4.引出map和set以及和红黑树之间的关系
二:关于搜索我们一般用到的方式
2.1.静态查找(就只进行查找不进行其他的操作)
2.2.但是现实中的查找大多数都是动态查找:
2.3 模型
三:Map
3.2.Map常用方法说明
3.3.关于Map方法的注意事项
四:Set(去重很好)
4.2.Set注意事项
五:我们日常的比较
六:哈希表
6.1:哈希冲突
6.2:哈希冲突不可避免
6.2.1:冲突-避免-哈希函数设计
6.2.2. 冲突-避免-负载因子调节(重点掌握)
6.3:哈希冲突解决:
6.3.1.闭散列
6.3.2:开散列/哈希桶(特别重要,很好用)
6.4:泛型写法
一定要重写equals方法!!!
6.5:总结:性能分析
7. OJ练习
7.1:只出现一次的数字
7.2:随机链表的复制
7.3:宝石与石头
7.4:坏键盘打字
7.5:输出前K个高频单词
问题一:hash会出现负数?数组越界
问题二:为什么会死循环?
问题三:第一个练习:万一 一个数字出现了三次呢?
一:什么是二叉搜索树?
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
1.1:创建一个二叉搜索树
public class BinarySearchTree {static class TreeNode {public int val;public TreeNode left;public TreeNode right;public TreeNode(int val) {this.val = val;}}public TreeNode root;public boolean search(int key){TreeNode cur = root;if(root == null){return false;}while(cur != null){if(cur.val > key){cur = cur.left;} else if (cur.val <key) {cur = cur.right;}else{return true;}}return false;}public boolean insert(int key){TreeNode node = new TreeNode(key);if(root == null){// 如果根就是的话,直接添加了,然后return true;root = node;return true;}TreeNode cur = root;TreeNode parent = cur;//赋值while(cur != null){if(cur.val < key){parent = cur; //这一步一定要提前cur = cur.right;} else if (cur.val >key) {parent = cur;cur = cur.left;}else{return false;}}if(parent.val < key){ //你就说为什么cur == null了把它小于的话(cur在该添加的位置上面等于了null)parent.right = node;}else{parent.left = node;}return true;}
1.2.移除一个节点:
要思考的问题:
代码实现:
public void remove(int val){TreeNode cur = root;//要移除这个节点,首先我要先遍历二叉搜索树找到这个节点TreeNode parent = null;if(root == null){return;}while(cur != null)if(cur.val < val) { parent = cur;cur = cur.right;}else if(cur.val > val){parent = cur;cur = cur.left;}else{//要删除的节点,找到了removeTreeNode(cur,parent);}}private void removeTreeNode(TreeNode cur,TreeNode parent) {if(cur.left == null){if(cur.val > parent.val){//这一步就纯纯的是判读那cur走到了parent的左边还是右边,parent.right = cur.right;//你这一点都不知道,我滴妈你让parent的左边接力还是右边?}else{parent.left = cur.right;}}else if(cur.right == null){ //cur的右边空了if(cur.val > parent.val){//和上面一样parent.right = cur.left;}else{parent.left = cur.left;}}else{ // cur 的两边都不为空TreeNode target = cur.right; //准备TreeNode parentTarget = null;while(target.left != null){parentTarget = target;target = target.left;}cur.val = target.val;if(parentTarget != null){parentTarget.left = target.right;}else{cur.right = target.right;}}}
1.3.二叉搜索树的缺陷:
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有 n 个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
- 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:
- 最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2(这个时候效率已经没了)
1.4.引出map和set以及和红黑树之间的关系
二:关于搜索我们一般用到的方式
2.1.静态查找(就只进行查找不进行其他的操作)
1. 直接遍历,时间复杂度为 O(N) ,元素如果比较多效率会非常慢2. 二分查找,时间复杂度为log(N), 但搜索前必须要求序列是有序的
2.2.但是现实中的查找大多数都是动态查找:
1. 根据姓名查询考试成绩2. 通讯录,即根据姓名查询联系方式3. 不重复集合,即需要先搜索关键字是否已经在集合中
2.3 模型
比如:有一个英文词典,快速查找一个单词是否在词典中,快速查找某个名字在不在通讯录中
比如:统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数: < 单词,单词出现的次数 >
三:Map
Map官方文档
Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对(这个键值对往大了点说就是一个内部类)(譬如树里面的节点里面存储了左右孩子节点),并且K一定是唯一的,不能重复(那这个<K,V>)
3.1.关于Map.Entry<K, V>(很重要)
首先Entry<K,V>是Map接口里面的一个内部接口),自然,TreeMap里面的Entry<K,V>这个类也是实现了Entry这个内部接口
如图:TreeMap里面的Entry<K,V>这个类也是实现了Entry这个内部接口
关于Map.Entry<K,V>(在Map里面的内部接口)这个类就相当于二叉树里面的节点
总的来说:Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式
注意:Map.Entry<K,V>并没有提供设置Key的方法
3.2.Map常用方法说明
解释:在 Java 中,当你尝试将一个对象赋值给一个基本数据类型时,实际上会发生自动拆箱(unboxing)操作,但这之前还有一个更关键的检查点:空值检查。
在这个例子中,
map.get("lihua")
返回一个Integer
对象(或者null
,如果键"lihua"
不存在)。但是,当你尝试将这个Integer
对象赋值给一个int
类型的变量val
时,Java 编译器会插入一个自动拆箱操作。然而,在自动拆箱之前,Java 运行时环境会检查
Integer
对象是否为null
。如果对象是null
,那么尝试进行拆箱操作(即将Integer
转换为int
)是不合法的,因为基本数据类型int
不能是null
。在这种情况下,Java 不会抛出类型转换异常(ClassCastException
),而是会抛出一个空指针异常(NullPointerException
)。类型转换异常(
ClassCastException
)通常发生在尝试将一个对象强制转换为不兼容类型的对象时。但在这个例子中,问题不在于类型的不兼容,而在于尝试将一个空引用(null
)用作基本数据类型的值。
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;public class Test {public static void main(String[] args) {Map<String,Integer> map = new TreeMap<>();
//1.put 和 get(Object key) 返回 key 对应的 valuemap.put("zhangsan",18);map.put("zhangsan",20);//会进行更新,key值不会重复map.put("lisi",18);map.put("wangWu",50);//int val = map.get("lihua");返回的是一个空指针异常而不是类型转换异常,很有讲究的//这个时候就可以用这个方法map.getOrDefault("lihua",20);// 没有key的话就相当于一次put,(key是lihua)(val是20)
//3.(keyset):Set<K> keySet() 返回所有 key 的不重复集合Set<String> set = map.keySet(); //经过排序的从大到小for (String s: set) {System.out.print(s + " ");}//或者System.out.println(set);
//4:Set<K> keySet() 返回所有 key 的不重复集合Collection<Integer> collection = map.values();//也是有顺序的,一般都是按照key的进行排序System.out.println(collection);System.out.println("==============");
//5:Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系Set<Map.Entry<String,Integer>> entries = map.entrySet();for (Map.Entry<String,Integer> entry:entries) {System.out.println("key:" + entry.getKey() + " val:" + entry.getValue());}//或者直接打印也行System.out.println(entries);System.out.println("===========");}
结果:
3.3.关于Map方法的注意事项
- 1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
- 2. Map中存放键值对的Key是唯一的,value是可以重复的
- 3. 在TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常(在我看来放入TreeMap的key需要进行比较利用compareTo方法,(这个时候null调用compareTo方法就会报空指针异常)所以不能插入key为空的情况)(TreeMap是基于红黑树实现的,它要求所有的key都必须实现Compare接口,或者通过构造函数传入Comparator。value可以为空。但是HashMap的key和value都可以为空。
- 4. Map中的Key可以全部分离出来,存储到Set中(Set<K> keySet())来进行访问(因为Key不能重复)。
- 5. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
- 6. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
四:Set(去重很好)
Set的官方文档
4.2.Set注意事项
注意:
- 1. Set是继承自Collection的一个接口类
- 2. Set中只存储了key,并且要求key一定要唯一
- 3. TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
- 4. Set最大的功能就是对集合中的元素进行去重
- 5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础
- 上维护了一个双向链表来记录元素的插入次序。
- 6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
- 7. TreeSet中不能插入null的key(和TreeMap的解释一样,都是要进行比较的),HashSet可以。
五:我们日常的比较
函数:数学函数!!!给一个x就能得出一个y,时间复杂度就是O(1)
至此,我们成功引入哈希表!!!
六:哈希表
插入元素:我们通过对关键码(Key)的运算,计算出该元素存储的位置,然后直接放在该位置
搜索元素:对元素的Key进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称为哈希表 (Hash Table)( 或者称散列表 )例如:数据集合 {1 , 7 , 6 , 4 , 5 , 9} ;哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小
但我再加一个为44的元素呢?结果就是完蛋,和4这个元素你俩撞上了
6.1:哈希冲突
上述的两个元素撞上了,就是你俩通过一个哈希函数然后计算出的哈希值一模一样,导致你俩放的位置一样,这样就是大名鼎鼎的——哈希冲突
具体解释:
对于两个数据元素的关键字 和 (i != ) ,有k i != kj ,但有: Hash( ki) == Hash(kj) ,即: 不同关键字通过相同哈 希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 。把具有不同关键码而具有相同哈希地址的数据元素称为 “ 同义词 ”
6.2:哈希冲突不可避免
- 设计合理的哈希函数
- 扩容(和负载因子有关):底层数组尽量可以根据放入元素的多少进行扩容(底层容量改变了,冲突也会尽量少一点)
6.2.1:冲突-避免-哈希函数设计
1. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间(我是数组我要连续(*^▽^*))
2. 哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数应该比较简单
1. 直接定制法 --( 常用 )取关键字的某个线性函数为散列地址: Hash ( Key ) = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况面试题:只出现一次 的字符2. 除留余数法 --( 常用 )设散列表中允许的地址数为m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:Hash(key) = key% p(p<=m), 将关键码转换成哈希地址3. 平方取中法 --( 了解 )假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 位 227 作为哈希地址; 再比如关键字为 4321 ,对它平方就是18671041 ,抽取中间的 3 位 671( 或 710) 作为哈希地址 平方取中法比较适合:不知道关键字的分 布,而位数又不是很大的情况4. 折叠法 --( 了解 )折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况5. 随机数法 --( 了解 )选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中 random 为随机数函数。通常应用于关键字长度不等时采用此法6. 数学分析法 --( 了解 )设有 n 个 d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转( 如1234改成 4321) 、右环位移 ( 如 1234 改成 4123) 、左环移位、前两数与后两数叠加 ( 如 1234 改成 12+34=46) 等方法。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均 匀的情况注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
6.2.2. 冲突-避免-负载因子调节(重点掌握)
6.3:哈希冲突解决:
一般是这两种方法
- 二次探测和线性探测(闭散列): 让你们两个冲突的元素,第一个放在计算出来的位置,然后再给另一个元素再设计一个函数进行放置(也称之为二次探测), 不进行二次探测的话,也可以把另外一个元素进行线性探测(既然这个位置已经有人了,那我就放在下一个空的位置(反正小编不想这么干))
- 数组 + 链表(开散列/哈希桶)(小编很喜欢这个):既然你俩计算出来相同的位置了,你俩都别委屈了,都来吧(
)
等等方法………………(接下俩小编为大家讲解这几个方法)
6.3.1.闭散列
我们来想想
线性探测的缺点: 线性探测会导致产生冲突的数据堆积在一块,这与其找下一个空位置有关系(一个一个挨着找,这种情况很正常)而二次探测可以避免这个问题
解决二:二次探测:
6.3.2:开散列/哈希桶(特别重要,很好用)
开散列/哈希桶(数组 + 链表)
虽然这样可行,但是万一冲突的元素太多了怎么办呢?(我还不是要遍历链表?)
好好好,那我就把你链表变成二叉树()
这个思想就是不断地把(大集合的搜索问题转化为小集合的搜索问题)
6.3.3:开散列/哈希桶的实现
// key-value 模型
public class HashBucket {public static class Node {private int val;private int key;public Node next;//这里一定要注意,你如果在这里上来就构造了一个根节点的public Node(int key, int val) {this.key = key;this.val = val;}}private static final double LOAD_FACTOR = 0.75;//负载因子//根本不需要根节点,我底层是数组进行实现的,要你没有用//public Node root;public Node[] array = new Node[5]; //我这个就是HsahMap的底层实现,开散列:数组 + 链表private int usedSize; //一般只要一个数据结构底层运用了数组,一般都会有usedSize去记录有效元素public boolean search(int key) { //这个要判断val吗?int index = key % array.length;Node cur = array[index];// 我直接计算出这个key应该在的位置,然后在数组的这个位置进行查找while (cur != null) {if (cur.key == key) {return true;}cur = cur.next;}return false;}public void put(int key, int val) {int index = key % array.length;Node cur = array[index];while (cur != null) { // 先遍历一遍这个链表,判读这个key在不在里面,如果在的话,就更新val值if (cur.key == key) {cur.val = val;}cur = cur.next;}//走到这里说明不在这里,直接进行头插Node node = new Node(key,val);node.next = array[index];array[index] = node;usedSize++;if(usedSize * 1.0/ array.length > LOAD_FACTOR){ //这里要乘 1.0revise(array);}}private void revise(Node[] array) { //这里要是有参数的话,下面的array,一定要加this啊!!!!!!!!!!!//首先让数字进行扩容//最后再返回数组Node[] tmpArray = Arrays.copyOf(array,2*array.length);//先遍历一遍原来的数组,再在每个数组元素上遍历链表for (int i = 0; i < array.length; i++) {Node cur = array[i];while(cur != null){//计算出新的下标,再进行头插int newIndex = cur.key % tmpArray.length;//这个节点就是cur其实没必要再多定义了/*Node node = new Node(cur.key,cur.val);node.next = tmpArray[newIndex];tmpArray[newIndex] = node;cur = cur.next;
*///你们是一样的道理Node curNext = cur.next;cur.next = tmpArray[newIndex];tmpArray[newIndex] = cur;cur =curNext;}}array = tmpArray;// 这里一定要this啊!!!!!!!!!!!!!// 除非上面没有参数}public int get(int key){int index = key % array.length;Node cur = array[index];// 我直接计算出这个key应该在的位置,然后在数组的这个位置进行查找while (cur != null) {if (cur.key == key) {return cur.val;}cur = cur.next;}return -1;}
}
Test(测试)
public class Test {public static void main(String[] args) {HashBucket hashBucket = new HashBucket();hashBucket.put(1,1);hashBucket.put(2,2);hashBucket.put(3,3);hashBucket.put(4,4);hashBucket.put(5,5);//就这里i = 1下标出问题了System.out.println("===============");System.out.println(hashBucket.get(1));System.out.println(hashBucket.get(2));System.out.println(hashBucket.get(5));System.out.println(hashBucket.get(9));}
}
我这里给大家提出一个问题,如果我上述有revise(array)并且后面array = tmpArray,(我也没加this,为什么最后会死循环呢?)
大家可以去运行一下,为什么最后会死循环?
6.4:泛型写法
class Person{String name;public Person(String name) {this.name = name;}//一定要重写equals方法,因为我要看看你一开始在不在里面,相同名字的lihua@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(name, person.name);}//这个也是@Overridepublic int hashCode() {return Objects.hash(name);}
}public class HashBucket1 <K,V>{public class Node<K,V>{ //为什么不能是静态的?(静态的不依赖于类,所以说我根本不知道K,V是谁?private K key;private V val;private Node<K,V> next;//这里一定要注意,你如果在这里上来就构造了一个根节点的public Node( K key, V val) {this.key = key;this.val = val;}}private Node[] array ;private static final double LOAD_FACTOR = 0.75;//负载因子public HashBucket1() {this.array = new Node[10];}private int usedSize;public void put(K key, V val){int hash = key.hashCode();//还能是负的?int index = Math.abs(hash % array.length);Node<K,V> cur = array[index];//先找一遍看有没有,没有直接头插法while(cur != null){if(cur.key.equals(key)){ //所以说这个key一定要重写equals方法cur.val = val;return;}cur = cur.next;}// 到这里就肯定没有了,没有直接头插法Node<K,V> node = new Node<>(key,val);node.next = array[index];array[index] = node;usedSize++;// 判断负载因子大不大if(usedSize*1.0 / array.length > LOAD_FACTOR){revise();}}private void revise() {Node<K,V> [] tmpArray = Arrays.copyOf(array,2*array.length);for (int i = 0; i < array.length; i++) {Node<K,V> cur = array[i];while(cur != null){Node<K,V> curNext = cur.next;//找到新下标直接进行插入int newIndex = Math.abs(cur.hashCode() % tmpArray.length);cur.next = tmpArray[newIndex];tmpArray[newIndex] = cur;cur = curNext;}}this.array = tmpArray;}public V get(K key){int index = key.hashCode() % array.length;Node<K,V> cur = array[index];while(cur != null){if(cur.key == key){return cur.val;}cur = cur.next;}return null;}public boolean search(K key){int index = key.hashCode() % array.length;Node<K,V> cur = array[index];while(cur != null){if(cur.key == key){return true;}cur = cur.next;}return false;}
}
Test测试
public class TestHashBucket1 {public static void main(String[] args) {Person person = new Person("zhangSan");Person person1 = new Person("zhangSan");Person person2 = new Person("zhangSan");HashBucket1<Person,Integer> hashBucket1= new HashBucket1();//这个key一定要重写equals方法(不然俺咋判断,我写的哈希表里面有没有这个元素hashBucket1.put(person,18);hashBucket1.put(person1,18);hashBucket1.put(person2,18);System.out.println("============");}
}
一定要重写equals方法!!!
写了:
没写:
6.5:总结:性能分析
- HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
- java 中使用的是哈希桶方式解决冲突的
- java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值, 必须重写 hashCode 和 equals 方 法。而且要做到 equals 相等的对象, hashCode 一定是一致的。
7. OJ练习
7.1:只出现一次的数字
只出现一次的数字
方法一:我们没学哈希表之前(直接全部异或就好了)
方法二:
class Solution {public int singleNumber(int[] nums) {Set<Integer> set = new HashSet<>();
//一边进去,相同的出来for(int i = 0; i < nums.length; i++){if(set.contains(nums[i])){set.remove(nums[i]);}else{set.add(nums[i]);}}
//如果包含就是你了(只出现过一次)for(int i = 0; i < nums.length; i++){if(set.contains(nums[i])){return nums[i];}}return -1;}
}
7.2:随机链表的复制
随机链表的复制
import java.util.Map;
class Solution {public Node copyRandomList(Node head) {Node cur = head;Map<Node,Node> map = new HashMap<>();while(cur != null){Node node = new Node(cur.val);map.put(cur,node); //帮下面的链接上,通过HashMapcur = cur.next;//node = node.next; 这样写肯定不对,因为我每次都要new一个Node对象,就链接不上了}cur = head;while(cur != null){map.get(cur).random = map.get(cur.random);map.get(cur).next = map.get(cur.next);cur = cur.next;}return map.get(head);}
}
7.3:宝石与石头
宝石与石头
class Solution {public int numJewelsInStones(String jewels, String stones) {Set<Character> set = new HashSet<>();for(int i = 0; i<jewels.length(); i++){set.add(jewels.charAt(i)); // 是add而不是put}int count = 0;for(int i = 0; i<stones.length(); i++){char ch = stones.charAt(i);if(set.contains(ch)){count++;}}return count;}
}
7.4:坏键盘打字
坏键盘打字
public static void find(String s1,String act){Set<Character> set = new HashSet<>();for (char ch: act.toUpperCase().toCharArray()) {set.add(ch);}Set<Character> set2 = new HashSet<>();for (char ch: s1.toUpperCase().toCharArray()) {if(! set.contains(ch)){set2.add(ch);}}for (Object o:set2.toArray()) {System.out.println(o);}}public static void main(String[] args) {String s1 = "abc def 7 abc";String s2 = "abc";find(s1,s2);}
需要掌握,String的toUpperCase()(转换大写),toCharArray()(转换成字符数组)
以及Set的toArray()将里面的元素转换成数组
7.5:输出前K个高频单词
输出前K个高频单词
public List<String> topKFrequent(String[] words, int k) {// 一个哈希表,统计所有单词出现的个数Map<String,Integer> map = new HashMap<>();for(String word : words){if( !map.containsKey(word)){map.put(word,1);}else{int count = map.get(word);map.put(word,++count);}}//建立小根堆,重写比较方法// 这里的Entry<>记得前面加上Map,因为他是Map里面的内部类,所有的都要加PriorityQueue<Map.Entry<String,Integer>> minHeap = new PriorityQueue<>(new Comparator<Map.Entry<String, Integer>>() {@Overridepublic int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {if((o1.getValue() - o2.getValue()) == 0 ){return o2.getKey().compareTo(o1.getKey());}return o2.getValue() - o1.getValue();}});for (Map.Entry<String,Integer> entry : map.entrySet()) {Map.Entry<String,Integer> top = minHeap.peek();if(minHeap.size() <= k){minHeap.offer(entry);}else{// 如果你俩的出现的频率相同,让字母的ASCII码值进行比较大的进来if(top.getValue().equals(entry.getValue())){if(top.getKey().compareTo(entry.getKey()) > 0){minHeap.poll();minHeap.offer(entry);}}else{// 下一个元素的val大,让下一个元素进来if(entry.getValue().compareTo(top.getValue()) > 0){minHeap.poll();minHeap.offer(entry);}}}}List<String> ret = new ArrayList<>();for (int i = 0; i < k; i++) {Map.Entry<String,Integer> top = minHeap.poll();ret.add(top.getKey());}Collections.reverse(ret);return ret;}
上述就是哈希碰撞攻防战——深入浅出Map/Set的底层实现
的全部内容了,哈希碰撞还有很多我们的未解之谜,让未来的我们一起去解决吧~~~
能看到这里相信您一定对小编的文章有了一定的认可。
有什么问题欢迎各位大佬指出
欢迎各位大佬评论区留言修正~~