目录
哈希表
定义节点类
根据hash码获取value
向hash表存入新key value,如果key重复,则更新value
根据hash码删除,返回删除的value
关于resize()一些问题的解答
冲突测试
MurmurHash
设计思考
练习
Leetcode01
Leetcode03
Leetcode49
Leetcode217
Leetcode136
Leetcode242
Leetcode387
Leetcode819
哈希表
查找数据红黑树B树是对数时间,但是哈希表的速度更快O(1),增删查改

定义节点类
static class Entry{
        int hash;//哈希码
        Object key;//键
        Object value;//值
        Entry next;
        public Entry(int hash, Object key, Object value) {
            this.hash = hash;
            this.key = key;
            this.value = value;
        }
    }
    
    //数组存链表头指针即可
    Entry[] table = new Entry[16];//选择2^n
    int size=0;//元素个数根据hash码获取value
 /*求模运算替换为位运算
        - 前提:数组长度是2的n次方
        - hash % 数组长度 等价于 hash & (数组长度-1)
     */
    // 根据hash码获取value
    Object get(int hash,Object key){
        int idx = hash & (table.length-1);
        if(table[idx] == null){
            return null;
        }
        Entry p = table[idx];
        while(p!=null){
            if(p.key.equals(key)){
                return p.value;
            }
            p=p.next;
        }
        return null;
    }
向hash表存入新key value,如果key重复,则更新value
// 向hash表存入新key value,如果key重复,则更新value
    void put(int hash,Object key,Object value){
        int idx = hash & (table.length-1);
        if(table[idx] == null){
            table[idx] = new Entry(hash,key,value);
        }else{
            Entry p = table[idx];
            while(true){
                if(p.key.equals(key)){
                    p.value = value;//更新
                    return;
                }
                if(p.next==null){
                    break;
                }
                p=p.next;
            }
            p.next = new Entry(hash,key,value);//新增
        }
        size++;
    }根据hash码删除,返回删除的value
 // 根据hash码删除,返回删除的value
    Object remove(int hash,Object key){
        int idx = hash&(table.length-1);
        if(table[idx]==null){
            return null;
        }
        Entry p = table[idx];
        Entry prev = null;
        while(p!=null){
            if(p.key.equals(key)){
                //找到了删除 -- 参考单向链表删除
                if(prev==null){//删的是链表头元素
                    table[idx]=p.next;
                }else{
                    prev.next = p.next;
                }
                size--;
                return p.value;
            }
            prev=p;
            p=p.next;
        }
        return null;
    }
什么时候对哈希表扩容合适呢?
这里引入一个名词:负载因子 load factor(a) = n/m, [n:元素个数 m:数组长度]
这个load factor不宜过大也不宜太小,在Java中合适取值为3/4,这是一个经验值.




扩容后我们需要将数据转移到新数组中,要重新计算索引

   float loadFactor = 0.75f;// 0.75 * 16 = 12(阈值)超过就扩容
    int threshold = (int)(loadFactor*table.length);
    /*求模运算替换为位运算
        - 前提:数组长度是2的n次方
        - hash % 数组长度 等价于 hash & (数组长度-1)
     */
    // 根据hash码获取value
    Object get(int hash,Object key){
        int idx = hash & (table.length-1);
        if(table[idx] == null){
            return null;
        }
        Entry p = table[idx];
        while(p!=null){
            if(p.key.equals(key)){
                return p.value;
            }
            p=p.next;
        }
        return null;
    }
    // 向hash表存入新key value,如果key重复,则更新value
    void put(int hash,Object key,Object value){
        int idx = hash & (table.length-1);
        if(table[idx] == null){
            table[idx] = new Entry(hash,key,value);
        }else{
            Entry p = table[idx];
            while(true){
                if(p.key.equals(key)){
                    p.value = value;//更新
                    return;
                }
                if(p.next==null){
                    break;
                }
                p=p.next;
            }
            p.next = new Entry(hash,key,value);//新增
        }
        size++;
        if(size>threshold){
            resize();
        }
    }
    private void resize(){
        Entry[] newTable = new Entry[table.length<<1];
        for(int i = 0;i<table.length;i++){
            Entry p = table[i];//拿到每个链表头
            if(p!=null){
                //拆分链表,移动到新数组
                /*
                    拆分规律
                    * 一个链表最多拆成两个
                    * hash & table.length == 0 的一组
                    * hash & table.length != 0 的一组
                                               p
                    0->8->16->24->32->40->48->null
                                a
                    0->16->32->48->null
                           b
                    8->24->40->null
                 */
                Entry a = null;
                Entry b = null;
                Entry aHead = null;
                Entry bHead = null;
                while(p!=null){
                    if((p.hash & table.length)==0){
                        if(a!=null){
                            a.next = p;
                        }else{
                            aHead =p;
                        }
                        //分配到a
                        a = p;
                    }else{
                        if(b!=null){
                            b.next = p;
                        }else{
                            bHead = p;
                        }
                        //分配到b
                        b = p;
                    }
                    p=p.next;
                }
                // 规律: a 链表保持索引位置不变, b 链表索引位置+table.length
                if(a!=null){
                    a.next = null;
                    newTable[i] = aHead;
                }
                if(b!=null){
                    b.next = null;
                    newTable[i+table.length] = bHead;
                }
            }
        }
        table = newTable;
        threshold=(int)(loadFactor*table.length);
    }
关于resize()一些问题的解答
/*
        为什么计算索引位置用式子:hash & (数组长度-1)  等价于 hash % 数组长度
        解释: %10看后一位   %100看后两位
            30%2==0
            0011110 % 0000010 = 0000000 看后1位
       4    0011110 % 0000100 = 0000010 看后2位
            0011110 % 0001000 = 0000110 ...
            0011110 % 0010000 = 0001110 ...
            0011110 % 0100000 = 0011110  ...
         文字表达: 10进制中去除以 10,100,1000时,余数就是被除数的后1,2,3位
                    10^1  10^2  10^3
                  2进制中去除以10,100,1000时,余数也是被除数的后1,2,3位
                        2^1  2^2   2^3
                  :任何一个数字跟 0按位与都等于0 跟1按位与能保留该位
        为什么旧链表会拆分成两条,一条hash & 旧数组长度==0 另一条!=0
        解释:
        旧数组长度换算成二进制后,其中的1就是我们要检查的倒数第几位
              旧数组长度 8 二进制 => 1000 检查倒数第4位
              旧数组长度 16 二进制 => 10000 检查倒数第5位
            hash & 旧数组长度,就是用来检查扩容前后索引位置(余数) 会不会变
        为什么拆分后的两条链表,一条原索引不变,另一个是原索引+旧数组长度
        解释:跟上面那个问题一样  二进制第i位 只能是0或者1
        它们都有个共同的前提:数组长度是2的n次方
     */
public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Object obj = new Object();
            System.out.println(obj.hashCode());
/*
1922154895
883049899
2093176254
1854731462
317574433
885284298
1389133897
1534030866
664223387
824909230
*/
            }
        Object obj1 = new Object();
        for (int i = 0; i < 10; i++) {
            System.out.println(obj1.hashCode());//同一对象的hashCode值是一样的
        }
    }  public Object get(Object key){
        int hash = getHash(key);
        return get(hash,key);
    }
    public void put(Object key,Object value){
        int hash = getHash(key);
        put(hash,key,value);
    }
    public Object remove(Object key){
        int hash = getHash(key);
        return remove(hash,key);
    }
    private static int getHash(Object key) {
        int hash = key.hashCode();
        return hash;
    }但是更多时候我们是不想用Object父类给我们提供的hashCode方法的,比如字符串类
  public static void main(String[] args) {
        String s1 = "abc";
        String s2 = new String("abc");
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());//结果发现两个哈希码一样 说明父类的hashcode被重写了
        //那我们来看看字符串的哈希码如何生成的
        //原则:值相同的字符串生成相同的hash码,尽量让值不同的字符串生成不同的hash码
        /*
        对于 abc a*100 + b*10 + c
        对应 bac b*100 + a*10 + c
         */
        int hash=0;
        for (int i = 0; i < s1.length(); i++) {
            char c = s1.charAt(i);
            System.out.println((int)c);
//            hash= hash*10 + c;//这个10 换成31就是底层内部的实现了
//            hash = hash*31+c;
            hash = (hash<<5)-hash+c; //这样效率比乘法更高
        }
        System.out.println(hash);
    }冲突测试
检测链表长度
 public void print(){
        int[] sums = new int[table.length];
        for(int i = 0;i<table.length;i++){
            Entry p = table[i];
            while(p!=null){
                sums[i]++;
                p = p.next;
            }
        }
        System.out.println(Arrays.toString(sums));
    }
    public static void main(String[] args) {
        HashTable table = new HashTable();
        for (int i = 0; i < 20; i++) {
            Object obj = new Object();
            table.put(obj,obj);
        }
        table.print();
    }
当然数量可能还是有点少
    public void print(){
        int[] sums = new int[table.length];
        for(int i = 0;i<table.length;i++){
            Entry p = table[i];
            while(p!=null){
                sums[i]++;
                p = p.next;
            }
        }
//        System.out.println(Arrays.toString(sums));
        Map<Integer, Long> collect = Arrays.stream(sums).boxed().
                collect(Collectors.groupingBy(e -> e, Collectors.counting()));
        System.out.println(collect);
    }
    public static void main(String[] args) {
        HashTable table = new HashTable();
        for (int i = 0; i < 200000; i++) {
            Object obj = new Object();
            table.put(obj,obj);
        }
        table.print();
    }
如果对字符串呢?
    public void print(){
        int[] sums = new int[table.length];
        for(int i = 0;i<table.length;i++){
            Entry p = table[i];
            while(p!=null){
                sums[i]++;
                p = p.next;
            }
        }
//        System.out.println(Arrays.toString(sums));
        Map<Integer, Long> collect = Arrays.stream(sums).boxed().
                collect(Collectors.groupingBy(e -> e, Collectors.counting()));
        System.out.println(collect);
    }
    public static void main(String[] args) throws IOException {
        HashTable table = new HashTable();
        List<String> strings = Files.readAllLines(Path.of("C:\\大一学习\\Java\\YJY\\learning\\src\\Tree\\a.txt"));
        for (String string : strings) {
            table.put(string,string);
        }
        table.print();
    }
}


MurmurHash
第二代哈希算法

- 引入google guava 依赖
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>



对比:

设计思考

1.Java内部HashTable 就是运用头插法 HashMap 在1.7版本也是头插法,后来因为在多线程下会导致死循环问题所以改为尾插法
2.

在我们自己的实现中:
源代码中通过 hash^(hash>>>16) 这样就能够避免上面产生的问题
不过这是有前提的 是要在数组容量是2^n次方下,如果不是就可以不用
3. ==> 2^n 按位与 拆分链表 高低位异或
Java中的HashTable 就不是用2^n次方 初始容量是11 是一个质数 求模算余数分布性更好 因此也不需要高低位异或
4.防患于未然 有的人造攻击的哈希数组,一旦造成冲突会导致服务器性能下降,这种做法是一种保底的做法 一般都是6到头了 除非恶意
练习
Leetcode01
1. 两数之和 - 力扣(LeetCode)
import java.util.HashMap;
/**
 * <h3>两数之和</h3>
 * 给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回他们的数组下标
 * 注意:<strong>只会存在一个有效答案</strong>
 */
public class Leetcode01 {
    /*
        [(2,0),(6,1),]
        输入: nums = [2,7,11,15],target = 9
        输出: [0,1]
        解释: 因为 nums[0] + nums[1] == 9,返回[0,1]
        输入: nums = [3,3],target = 6
        输出: [1,2]
        输入: nums = [3,3],target = 6
        输出: [0,1]
        思路:
        1.循环遍历数组,拿到每个数字 X
        2.以 target-x作为key到hash表查找
            1) 若没找到,将x作为key,它的索引作为value放入hash表
            2) 若找到了,返回x和它配对数的索引即可
     */
    public int[] twoSum(int[] nums,int target){
        HashMap<Integer,Integer>map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int x = nums[i];
            int y = target-x;
            if(map.containsKey(y)){
                return new int[]{i,map.get(y)};
            }else{
                map.put(x,i);
            }
        }
        return null;
    }
}
Leetcode03
3. 无重复字符的最长子串 - 力扣(LeetCode)
import java.util.HashMap;
/**
 * <h2>无重复字符的最长子串</h2>
 * <p>1.给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度.</p>
 * <p>2.s由英文字母,数字,符号和空格组成</p>
 */
public class Leetcode03 {
    public int lengthOfLongestSubstring(String s){
        HashMap<Character,Integer> map = new HashMap<>();//key返回比较大用hashMap 但是这道题目仅仅是128个字符可以用数组
        int begin = 0;
        int maxLength = 0;
        for(int end = 0;end<s.length();end++){
            char ch = s.charAt(end);
            if (map.containsKey(ch)) {//重复时,调整begin
                begin = Math.max(begin,map.get(ch) + 1);//防止begin回退
                map.put(ch,end);
            }else{//没有重复
                map.put(ch,end);
            }
//            System.out.println(s.substring(begin,end+1));
            maxLength = Math.max(maxLength,end - begin +1);
        }
        return maxLength;
    }
    public static void main(String[] args){
//        System.out.println(new Leetcode03().lengthOfLongestSubstring("abcabcbb"));
        Leetcode03 e02 = new Leetcode03();
//        System.out.println(e02.lengthOfLongestSubstring("abcabcbb"));
        System.out.println(e02.lengthOfLongestSubstring("abba"));
        /*
            [(a,0),(b,2)]
             b
               e
            abba
         */
         /*
            a
            ab
            abc
            bca
            cab
            abc
            cb
            b
          */
        /*
            给定一个字符串 s ,请你找出其中不含重复字符的最长子串的长度
            abcabcbb   3
            a
            ab
            abc
            bca
            cab
            abc
            cb
            b
            bbbbbb      1
            b
            pwwkew      3
            p
            pw
            w
            wk
            wke
            kew
            [(a,3),(b,7),(c,5)]
                 b
                   e
            abcabcbb
        要点:
            1.用 begin 和 end 表示子串开始和结束位置
            2.用hash表检查重复字符
            3.从左到右查看每一个字符,如果:
                - 没遇到重复字符,调整end
                - 遇到重复的字符,调整begin
                - 将当前字符放入hash表
            4.end - begin + 1 是当前子串长度
         */
    }
}
运用了滑动窗口的思路
滑动窗口做题思路-CSDN博客
Leetcode49
49. 字母异位词分组 - 力扣(LeetCode)
import java.util.*;
/**
 * 字母异位次分组
 */
public class Leetcode49 {
    /*
        思路:
        1.遍历字符串数组,每个字符串中的字符重新排序后作为key
        2.所谓分组,其实就是准备一个集合,把这些单词加入到key相同的集合中
        3.返回分组结果
     */
    public List<List<String>> groupAnagrams(String[] strs){  // 6ms
        HashMap<String, List<String>>map = new HashMap<>();
        for (String str : strs) {
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String key = new String(chars);//字符数组->字符串
            List<String> list = map.computeIfAbsent(key, k -> new ArrayList<>());
            //computeIfAbsent原理:先看key在map中是否存在,如果缺失(absent)就会创建一个新的集合放到集合中
            //如果不是absent那就不会创建  然后把list集合作为结果进行返回
            list.add(str);
        }
        return new ArrayList<>(map.values());//map.values就是分的组
    }
    public static void main(String[] args) {
        // ab  ba
        // eat eta tae tea ate aet
        String[] strs = {"eat","tea","tan","ate","nat","bat"};
        // eat tea ate => aet
        // tan nat     => ant
        // bat         => abt
        // [(aet,{eat,tea}), (ant,{tan})]
        List<List<String>>lists = new Leetcode49().groupAnagrams(strs);
        System.out.println(lists);
    }
}
computeIfAbsent()那一段可以这么写: 性能上是差不多的只是看起来更简洁
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        HashMap<String,List<String>>map = new HashMap<>();
        for(String str:strs){
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String key = new String(chars);
            List<String>list = map.get(key);
            if(list ==null){
                list = new ArrayList<>();
                map.put(key,list);
            }
            list.add(str);
        }
        return new ArrayList<>(map.values());
    }
}第二种方法:效率更高 5ms
  static class ArrayKey {//一个整数数组做一个封装
        int[] key = new int[26];
        
        //alt+insert ==>生成 基于数组的equals 和 hashCode
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;//同一对象
            if (o == null || getClass() != o.getClass()) return false;
            ArrayKey arrayKey = (ArrayKey) o;
            return Arrays.equals(key, arrayKey.key);//比较内容
        }
        @Override
        public int hashCode() {
            return Arrays.hashCode(key);
        }
        //将字符串变成整数数组
        public ArrayKey(String str){
            for (int i = 0; i < str.length(); i++) {
                char ch = str.charAt(i);// 'a'97-97= 0(映射到索引0) 'b'98-97=1  'a'
                key[ch-97]++;
            }
        }
    }
    /*
    bitmap 位图思想
        题目有说明:strs[i] 仅包含小写字母
        key = [2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0]  26
        key = [2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0]  26
        "eaat","teaa"
     */
    public List<List<String>> groupAnagrams(String[] strs){
        HashMap<ArrayKey,List<String>>map = new HashMap<>();
        for (String str : strs) {
            ArrayKey key = new ArrayKey(str);
            List<String> list = map.computeIfAbsent(key, k -> new ArrayList<>());
            list.add(str);
        }
        return new ArrayList<>(map.values());
    }
Leetcode217
217. 存在重复元素 - 力扣(LeetCode)
    /*
    [1,2,3,1]
    [1,2,3] => true
     */
    public boolean containsDuplicate1(int[] nums) {//11ms
        HashMap<Integer,Integer> map = new HashMap<>();
        for(int key:nums){
            if(map.containsKey(key)){
                //找到重复
                return true;
            }
            map.put(key,key);//一次循环调用put 和 containsKey
        }
        return false;
    }
    public boolean containsDuplicate(int[] nums){//5ms
        HashSet<Integer>set = new HashSet<>();
        for(int key:nums){
            if(!set.add(key)){//一次循环调用一次put
                return true;
            }
        }
        return false;
    }


通过分析我们可以来这样修改我们的代码:6ms
  public boolean containsDuplicate(int[] nums) {
        HashMap<Integer,Object>map = new HashMap<>(nums.length*2);
        Object value = new Object();
        for(int key:nums){
            Object put = map.put(key,value);
            if(put!=null){
                return true;
            }
        }
        return false;
    }Leetcode136
136. 只出现一次的数字 - 力扣(LeetCode)

import java.util.HashSet;
/**
 * 找出出现一次的数字
 * 除了某个元素只出现一次以外,其余每个元素均出现两次
 */
public class Leetcode136 {
    /*
        输入:nums = [2,2,1]
        输出:1
        HashSet
        [2,] 遇到重复的就把第一个拿出来 最后集合中唯一的元素就是只出现一次的答案
        输入:nums = [4,1,2,1,2]
        输出4
        [4]
思路一:
        1.准备一个Set 集合,逐一放入数组元素
        2.遇到重复的,则删除
        3.最后留下来的,就是那个没有重复的数字
     */
    public int singleNumber1(int[] nums){//效率低
        HashSet<Integer> set = new HashSet<>();
        for(int num:nums){
            if (!set.add(num)) {
                set.remove(num);
            }
        }
        return set.toArray(new Integer[0])[0];
    }
    /*
    思路二:
    1.任何相同的数字异或,结果都是0
    2.任何数字与0异或,结果都是数字本身
     */
    public int singleNumber(int[] nums){
        int num = nums[0];
        for(int i=1;i<nums.length;i++){
            num = num^nums[i];
        }
        return num;
    }
    public static void main(String[] args) {
        Leetcode136 e06 = new Leetcode136();
        System.out.println(e06.singleNumber(new int[]{2, 2, 1}));
        System.out.println(e06.singleNumber(new int[]{4, 1, 2, 1, 2}));
    }
}

Leetcode242
242. 有效的字母异位词 - 力扣(LeetCode)
import java.util.Arrays;
public class Leetcode242 {
    /*
          输入:s = "anagram", t = "nagaram"
          输出:true
          1.拿到字符数组,排序后比较字符数组
          2.字符打散放入 int[26] 比较数组
    */
    public boolean isAnagram(String s,String t){
        return Arrays.equals(getKey(s),getKey(t));
    }
    private int[] getKey1(String s){//3ms
        int[] array = new int[26];
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i); //'a' - 97 = 0
            array[ch-97]++;
        }
        return array;
    }
    private int[] getKey(String s){//1ms
        int[] array = new int[26];
        char[] chars = s.toCharArray();
        for(char ch:chars){
            array[ch-97]++;
        }
        return array;
    }
}
Leetcode387
387. 字符串中的第一个唯一字符 - 力扣(LeetCode)
public class Leetcode387 {
    public int firstUniqChar(String s) {
        int[] array = new int[26];
        char[] chars = s.toCharArray();
        for (char ch : chars) {
            array[ch - 97]++;
        }
        for (int i = 0; i < chars.length; i++) {
            char ch = chars[i];
            if(array[ch-97]==1){
                return i;
            }
        }
        return -1;
    }
}
Leetcode819
819. 最常见的单词 - 力扣(LeetCode)
这种方法14ms 非常不理想
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class Leetcode819 {
    /*
        1. 将Paragraph截取为一个个单词
        2.将单词加入map集合,单词本身作为key,出现次数作为value,避免禁用词加入
        3.在map集合中找到value最大的,返回它对应的key即可
     */
    public String mostCommonWord(String paragraph,String[] banned){
        //1
        String[] split = paragraph.toLowerCase().split("[^A-Za-z]+");
        Set<String> set = Set.of(banned);//Java9以上版本 转成set集合
        HashMap<String,Integer>map = new HashMap<>();
        for (String key : split) {
            if(!set.contains(key)){//如果set中包含了key 就不加入 不包含再加入
                map.compute(key,(k,v)->v==null?1:v+1);
            }
//            System.out.println(key);
//            Integer value = map.get(key);
//            if(value==null){
//                value=0;
//            }
//            map.put(key,value+1);
//            map.compute(key,(k,v)->v==null?1:v+1);//第一次放入1 不是第一次就加1 跟上面4行等效
        }
//        System.out.println(map);
        Optional<Map.Entry<String, Integer>> max = map.entrySet().stream().max(Map.Entry.comparingByValue());
        System.out.println(max);
        return max.map(Map.Entry::getKey).orElse(null);
    }
}
改进1:
  public String mostCommonWord(String paragraph,String[] banned){//12ms
        //1
        String[] split = paragraph.toLowerCase().split("[^A-Za-z]+");
        Set<String> set = Set.of(banned);//Java9以上版本 转成set集合
        HashMap<String,Integer>map = new HashMap<>();
        for (String key : split) {
            if(!set.contains(key)){//如果set中包含了key 就不加入 不包含再加入
                map.compute(key,(k,v)->v==null?1:v+1);
            }
        }
        int max = 0;
        String maxKey = null;
        for (Map.Entry<String, Integer> e : map.entrySet()) {
            Integer value = e.getValue();
            if(value>max){
                max= value;
                maxKey = e.getKey();
            }
        }
        return maxKey;
    }import com.sun.jdi.event.StepEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class Leetcode819 {
    /*
        1. 将Paragraph截取为一个个单词
        2.将单词加入map集合,单词本身作为key,出现次数作为value,避免禁用词加入
        3.在map集合中找到value最大的,返回它对应的key即可
     */
    public String mostCommonWord(String paragraph,String[] banned){// 4ms
//        String[] split = paragraph.toLowerCase().split("[^A-Za-z]+");
        Set<String>set = Set.of(banned);
        HashMap<String,Integer>map = new HashMap<>();
        char[] chars = paragraph.toLowerCase().toCharArray();
        StringBuilder sb =new StringBuilder();
        for (char ch : chars) {
            if(ch>='a'&&ch<='z'){
                sb.append(ch);
            }else{
                String key = sb.toString();
                if(!set.contains(key)){
                    map.compute(key,(k,v)->v==null?1:v+1);
                }
//                sb = new StringBuilder();
                sb.setLength(0);//不创建置为0
            }
        }
        if(sb.length()>0){
            String key = sb.toString();
            if(!set.contains(key)){
                map.compute(key,(k,v)->v==null?1:v+1);
            }
        }
        System.out.println(map);
        int max = 0;
        String maxKey = null;
        for (Map.Entry<String, Integer> e : map.entrySet()) {
            Integer value = e.getValue();
            if(value>max){
                max= value;
                maxKey = e.getKey();
            }
        }
        return maxKey;
    }
}



















