文章目录
前言
在上一篇博客中,我们学习了关于Map和Set的,Tree类型和Hash类型,下面我们学习哈希桶的模拟实现以及有关Map和Set的Oj题
1.哈希桶的模拟实现
这个就是哈希桶的模拟结构图,是由数组和节点,链表组成的。
其中那个扩容是最难的,等会好好进行讲解,现在开干。
1.1.准备工作
先把数组和节点准备好
public class HashBuck {
static class Node{
public int key;
public int val;
public Node next;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
}
public Node[] array = new Node[10];
public int usedSize;
public static double DEFAULT_LOAD_FACTOR = 0.75; // 负载因子
1.2 插入操作
1.第一步,先确定插入的位置(index = key % arr.length)。
2.第二步,判断该位置是否存在相同的key节点,如果存在,那么修改val的值,如果不存在,那么便使用头插法,进行存储
3.第三步,如果达到了负载因子的话,那么进行扩容。
public void put(int key,int val){
int index = key % this.array.length;
Node cur = array[index];
while (cur != null){
if (cur.key == key){
cur.val = val;
return;
}
cur = cur.next;
}
Node node = new Node(key,val);
node.next = array[index];
array[index] = node;
this.usedSize ++ ;
if (doLoadFactor() == DEFAULT_LOAD_FACTOR){
resize();
}
}
private double doLoadFactor() {
return (usedSize*1.0/this.array.length);
}
1.3.扩容操作
扩容并不是简单的数组扩容,而是要把每个节点都放在新的数组(把新的数组长度定为原来的2倍)下标中,例如上面的key = 34 的节点,通过 index = key % newArray.length,得到新的index,再把key = 34 的结点放到新数组指定的位置上。即可
private void resize() {
Node[] newArray = new Node[2*this.array.length];
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while (cur != null){
int index = cur.key % newArray.length;
Node curN = cur.next;
cur.next = newArray[index];
newArray[index] = cur;
cur = curN;
}
}
array = newArray;
}
1.4.获取元素的val值
public int getVal(int key){
int index = key % array.length;
Node cur = array[index];
while (cur != null){
if (cur.key == key){
return cur.val;
}
cur = cur.next;
}
return -1;
}
大家抽空的时候,也可以再写一个关于key和value为泛型的一个哈希桶。
1.5.性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1) 。
1.6.和 java 类集的关系
- HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
- java 中使用的是哈希桶方式解决冲突的
- java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
- java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
2.HashMap源码
3.Oj题
3.1.只出现一次的数字
这道题可以使用 异或(^)来做,但是既然我们学习了Map和Set,那么我们这会就Set来做这道题。
怎么做呢?
因为只有一个数字出现了一次,剩余的数字都出现了两次,
那么第一步,先遍历一遍数组,将数字存在hashset中,如果存在的话,在移除该元素,那么剩下的只有一个元素
第二步,再遍历一遍元素,hashset和数组中,存在相同的元素,那么直接返回即可,如果没有的话,返回-1即可。
class Solution {
public int singleNumber(int[] nums) {
HashSet<Integer> set = new HashSet();
for (int i = 0;i<nums.length;i++){
if (!set.contains(nums[i])){
set.add(nums[i]);
}else{
set.remove(nums[i]);
}
}
for (int i = 0; i < nums.length; i++) {
if (set.contains(nums[i])) {
return nums[i];
}
}
return -1;
}
}
3.2.随机链表的复制
这个是个深拷贝问题,新创建的链表的next指针,random指针指向的还是新的对应值的节点。
这样才是深拷贝,可以使用HashMap,用于存储新节点和老节点,之后再把每个节点的next引用和random引用连接上
class Solution {
public Node copyRandomList(Node head) {
HashMap<Node,Node> map = new HashMap<>();
Node cur = head;
while(cur != null){
Node node = new Node(cur.val);
map.put(cur,node);
cur = cur.next;
}
cur = head;
while (cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
3.3.宝石与石头
这道题比较简单,只需要我们把jewels中的每个字符都存在HashSet中,然后再把stones遍历一遍,如果HashSet中存在stones中的字符,那么count++,最后返回count即可。
class Solution {
public int numJewelsInStones(String jewels, String stones) {
HashSet<Character> set = new HashSet();
int cnt = 0;
for (int i = 0;i<jewels.length();i++){
char ch = jewels.charAt(i);
set.add(ch);
}
for (int j = 0 ;j<stones.length();j++){
char ch = stones.charAt(j);
if (set.contains(ch)){
cnt++;
}
}
return cnt;
}
}
3.4.旧键盘
其中< br/>是换行的意思,
第一步,因为需要返回的是英文大写,并且,只能返回一个,因此将第一句话(a) 和 第二句话(b)都变成大写(toUpperCase()),
第二步,将第二段话(str2)中每个单词都放在setAct(HashSet类型的)中,然后用setBroken(也是HashSet类型的)中,setBroken用于存储在str1中没有出现的单词,并且相同的只能存放一个,然后在打印出来。
import java.util.Scanner;
import java.util.HashSet;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNext()) { // 注意 while 处理多个 case
String a = in.nextLine();
String b = in.nextLine();
func(a,b);
}
}
public static void func(String str1,String str2){
str1 = str1.toUpperCase();
str2 = str2.toUpperCase();
HashSet<Character> setAct = new HashSet<>();
for (int i = 0; i < str2.length(); i++) {
char ch = str2.charAt(i);
setAct.add(ch);
}
HashSet<Character> setBroken = new HashSet<>();
for (int i = 0; i < str1.length(); i++) {
char ch = str1.charAt(i);
if(!setAct.contains(ch) && !setBroken.contains(ch)) {
setBroken.add(ch);
System.out.print(ch);
}
}
}
}
3.5.前K个高频单词
这道题不好做,首先需要使用HashMap来统计每个单词出现的次数,然后还要建立小根堆,后面就是堆排序了。,并且小根堆的比较器还需要重写,因为如果出现次数一样的单词,需要按照字典的先后顺序来排列,那么开干吧。
public List<String> topKFrequent(String[] words, int k) {
//1.遍历words
HashMap<String,Integer> map = new HashMap<>();
for (String word:words){
if (!map.containsKey(word)){
map.put(word,1);
}else {
int val = map.get(word);
map.put(word,val+1);
}
}
// 2.第二步创建小根堆
PriorityQueue<Map.Entry<String,Integer>>minHeap = new PriorityQueue<>(new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
if (o2.getValue().compareTo(o1.getValue())== 0){
return o2.getKey().compareTo(o1.getKey());
}
return o1.getValue().compareTo(o2.getValue());
}
});
//3.遍历map
for(Map.Entry<String,Integer> entry:map.entrySet()){
if (minHeap.size() < k){
minHeap.offer(entry);
}else {
Map.Entry<String,Integer> top = minHeap.peek();
if (top.getValue().compareTo(entry.getValue()) < 0){
minHeap.poll();
minHeap.offer(entry);
}else if (top.getValue().compareTo(entry.getValue()) == 0) {
if (top.getKey().compareTo(entry.getKey())>0){
minHeap.poll();
minHeap.offer(entry);
}
}
}
}
List<String> list = new ArrayList<>();
for(int i = 0;i<k;i++){
Map.Entry<String,Integer> entry = minHeap.poll();
list.add(entry.getKey());
}
Collections.reverse(list);
return list;
}
完