算法刷题记录-Hash(LeetCode)

187. Repeated DNA Sequences

思路1 滑窗+字符串hash

数据范围只有 1 0 5 10^5 105 ,一个朴素的想法是:从左到右处理字符串 s,使用滑动窗口得到每个以 s[i] 为结尾且长度为 10 的子串,同时使用哈希表记录每个子串的出现次数,如果该子串出现次数超过一次,则加入答案。

为了防止相同的子串被重复添加到答案,而又不使用常数较大的 Set 结构。我们可以规定:当且仅当该子串在之前出现过一次(加上本次,当前出现次数为两次)时,将子串加入答案。

    public List<String> findRepeatedDnaSequences(String s) {
        int end=10;
        HashMap<String,Boolean> map=new HashMap<>();
        List<String> res=new ArrayList<>();
        while (end<=s.length()){
            String sub=s.substring(end-10,end);
            if (map.containsKey(sub)&& !map.get(sub)){
                res.add(sub);
                map.put(sub,true);
            }
            if (!map.containsKey(sub)){
                map.put(sub,false);
            }
            end++;
        }
        return res;
    }

*思路2 整数hash

子串长度为 101010,因此上述解法的计算量为 1 0 6 10^6 106

若题目给定的子串长度大于 100 时,加上生成子串和哈希表本身常数操作,那么计算量将超过 1 0 7 10^7 107 ,会 TLE。

因此一个能够做到严格 O ( n ) O(n) O(n) 的做法是使用「字符串哈希 + 前缀和」。

具体做法为,我们使用一个与字符串 sss 等长的哈希数组h[],以及次方数组p[]

由字符串预处理得到这样的哈希数组和次方数组复杂度为 O ( n ) O(n) O(n)。当我们需要计算子串 s[i...j] 的哈希值,只需要利用前缀和思想 h [ j ] − h [ i − 1 ] ∗ p [ j − i + 1 ] h[j]−h[i−1]∗p[j−i+1] h[j]h[i1]p[ji+1] 即可在 O ( 1 ) O(1) O(1) 时间内得出哈希值(与子串长度无关)。

到这里,还有一个小小的细节需要注意:如果我们期望做到严格 O ( n ) O(n) O(n),进行计数的「哈希表」就不能是以 String 作为 key,只能使用 Integer(也就是 hash 结果本身)作为 key。因为 Java 中的 String 的 hashCode 实现是会对字符串进行遍历的,这样哈希计数过程仍与长度有关,而 Integer 的 hashCode 就是该值本身,这是与长度无关的

代码2

关于h[i]的递推形式证明:

对于 h[i]=h[i-1]*PRIME+s.charAt(i-1);
(Prime简写为P)
h [ 0 ] = 0 h[0]=0 h[0]=0
h [ 1 ] = h [ 0 ] ∗ P + s 0 = s [ 0 ] h[1]=h[0]*P+s_0=s[0] h[1]=h[0]P+s0=s[0]
h [ 2 ] = h [ 1 ] ∗ P + s 1 = s 0 ∗ P + s 1 h[2]=h[1]*P+s_1=s_0*P+s_1 h[2]=h[1]P+s1=s0P+s1
h [ 3 ] = h [ 2 ] ∗ P + s 2 = s 0 ∗ P 2 + s 1 ∗ P + s 2 h[3]=h[2]*P+s_2=s_0*P^2+s_1*P+s_2 h[3]=h[2]P+s2=s0P2+s1P+s2
. . . ... ...
h [ j ] = h [ j − 1 ] ] ∗ P + s j − 1 = s 0 ∗ P j − 1 + s 1 ∗ P j − 2 + . . . + s j − 1 h[j]=h[j-1]]*P+s_{j-1}=s_0*P^{j-1}+s_1*P^{j-2}+...+s_{j-1} h[j]=h[j1]]P+sj1=s0Pj1+s1Pj2+...+sj1
对于hash=h[j]-h[i-1]*p[j-i+1]:
h [ i ] = s 0 ∗ P i − 1 + s 1 ∗ p i − 2 + . . . + s i − 1 h[i]=s_0*P^{i-1}+s_1*p^{i-2}+...+s_{i-1} h[i]=s0Pi1+s1pi2+...+si1
h [ i + 10 ] = s 0 ∗ P i + 9 + s 1 ∗ p i − 2 + . . . + s i + 9 h[i+10]=s_0*P^{i+9}+s_1*p^{i-2}+...+s_{i+9} h[i+10]=s0Pi+9+s1pi2+...+si+9
h a s h = h [ i + 10 ] − h [ i ] ∗ P 10 = s i ∗ P 9 + s i + 1 ∗ P 8 + . . . + s i + 9 hash=h[i+10]-h[i]*P^{10}=s_{i}*P^9+s_{i+1}*P^8+...+s_{i+9} hash=h[i+10]h[i]P10=siP9+si+1P8+...+si+9

code
class Solution {
    public static Integer N= (int) (1e5+10);
    public static Integer PRIME= 131313;
    public List<String> findRepeatedDnaSequences(String s) {
        int n=s.length();
        List<String> ans=new ArrayList<>();
        int[] h=new int[N];
        int[] p=new int[N];
        p[0]=1;
        for (int i = 1; i <= n; i++) {
            h[i]=h[i-1]*PRIME+s.charAt(i-1);
            p[i]=p[i-1]*PRIME;
        }
        Map<Integer,Integer>map=new HashMap<>();
        for (int i = 1; i+10-1 <=n ; i++) {
            int j=i+10-1;
            int hash=h[j]-h[i-1]*p[j-i+1];
            int cnt=map.getOrDefault(hash,0);
            if (cnt==1){
                ans.add(s.substring(i-1,i+10-1));
            }
            map.merge(hash,1,Integer::sum);
        }
        return ans;
    }
}

820. Short Encoding of Words

思路

Hashset有个特点remove的元素不在set里面的话,是删除不了什么东西的。例如题目中的样例,time me bell,删除ime 的话是什么都不会发生的。利用这一点,我们可以把每个string元素从第一位开始从set中删除。

代码

    public int minimumLengthEncoding(String[] words) {
        Set<String> set = new HashSet<>(Arrays.asList(words));
        for (String word : words) {
            for (int i = 1; i < word.length(); i++) {
                set.remove(word.substring(i));
            }
        }
        System.out.println(set);
        int ans = 0;
        for (String word : set) {
            ans += word.length() + 1;
        }
        return ans;
    }

869. Reordered Power of 2

思路

预先将 1 < = k < = 1 0 9 1 <= k <= 10^9 1<=k<=109范围内2的幂的字符串表示的长度存入HashMap中,并将其具体值存入HashSet中。将新输入的数字转化为字符串,首先判断hashmap中是否存在长度为k的2的幂,若无则一定不可以重组为2的幂。若存在,则对长度等于字符串的2的幂的每个字符串逐个比较字符数量,若一致则存在返回ture。

代码

class Solution {
    static HashMap<Integer, HashSet<String>> map=new HashMap<>();
    static  {
        int base=1;
        while (base<1000000000){
            int digits=String.valueOf(base).length();
            if (!map.containsKey(digits)){
                map.put(digits,new HashSet<>());
                map.get(digits).add(String.valueOf(base));
            }
            else{
                map.get(digits).add(String.valueOf(base));
            }
            base*=2;
        }
    }
    public boolean reorderedPowerOf2(int n) {

        char[] digits=String.valueOf(n).toCharArray();
        int[] cnts=new int[10];
        for (char ch:digits){
            cnts[ch-'0']++;
        }
        if (!map.containsKey(digits.length)){
            return false;
        }
        continue2: for (String key:map.get(digits.length)){
            int[] cnts_cpy=new int[10];
            System.arraycopy(cnts,0,cnts_cpy,0,10);
            for (char ch:key.toCharArray()){
                cnts_cpy[ch-'0']--;
                if (cnts_cpy[ch-'0']<0){
                    continue continue2;
                }
            }
            return true;
        }
        return false;
    }
}

890. Find and Replace Pattern

思路

创建两个HashMap 实现1对1双射

代码

class Solution {
    public List<String> findAndReplacePattern(String[] words, String _pattern) {
        List<String> res=new ArrayList<>();
        char[] pattern=_pattern.toCharArray();
        continue2: for (String word:words){
            HashMap<Character,Character> mapping=new HashMap<>();
            HashMap<Character,Character> reverseMapping=new HashMap<>();
            for (int i=0;i<word.length();i++){
                if (!mapping.containsKey(word.charAt(i))&&!reverseMapping.containsKey(pattern[i])){
                    mapping.put(word.charAt(i),pattern[i]);
                    reverseMapping.put(pattern[i],word.charAt(i));
                }
                else if (mapping.containsKey(word.charAt(i))&& mapping.get(word.charAt(i))==pattern[i]){
                    continue;
                }
                else{
                    continue continue2;
                }
            }
            res.add(word);
        }
        return res;
    }
}

911. Online Election

思路

首先定义counter,用于保存当前时间t的票型,定义curr_idx为历史最高得票者、定义curr_cnt为历史最高票数。
定义TreeMaptimeSeries用于存储在不同时间下的领先者。
对时间从1-n进行遍历,在counter中对当前的票数以及最高者进行判断:

  • 若当前票数大于历史最高票curr_cnt对其进行更新。
  • 若当前最高票者票数大于或等于curr_cnt。判断是否与历史最高票者是否一致,不一致则在timeSeries插入最高票数为其的时间戳(t,curr_idx)。
  • 对于任意输入t,通过timeSeries.get(timeSeries.floorKey(t)方法获取最接近t的最高票人即可。

代码

public class TopVotedCandidate {

    TreeMap<Integer,Integer> timeSeries=new TreeMap<>();
    HashMap<Integer,Integer> counter=new HashMap<>();
    public TopVotedCandidate(int[] persons, int[] times) {
        init(persons,times);
    }
    public void init(int[] persons, int[] times){
        int curr_idx=-1;
        int curr_cnt=0;
        for (int i = 0; i < times.length; i++) {
            counter.merge(persons[i],1,Integer::sum);
            if (counter.get(persons[i])==curr_cnt){
                if (curr_idx!=persons[i]){
                    curr_idx=persons[i];
                    timeSeries.put(times[i],curr_idx);
                }
            }
            if (counter.get(persons[i])>curr_cnt){
                curr_cnt=counter.get(persons[i]);
                if (curr_idx!=persons[i]){
                    curr_idx=persons[i];
                    timeSeries.put(times[i],curr_idx);
                }
            }
        }
    }

    public int q(int t) {
        return timeSeries.get(timeSeries.floorKey(t));
    }
}

954. Array of Doubled Pairs

思路 排序+hashmap

首先对数组从小到大排序,然后遍历每一个数字val
对于val,我们进行如下考虑

  1. 如果val大于0
    • 如果hashmap中val不存在,则我们要寻找2*val是否存在,因此将hashmap[2*val]+1;
    • 如果hashmap中val存在,则我们要寻找2*val存在,因此将hashmap[2val]-1且如果hashmap[2val]为0则移除。
  2. 如果val小于0
    • 如果hashmap中val不存在,则我们要寻找val/2是否存在,因此将hashmap[val/2]+1;
    • 如果hashmap中val存在,则我们要寻找val/2存在,因此将hashmap[val/2]-1且如果hashmap[2*val]为0则移除。
      最终如果hashmap为空则说明所有元素配对。

代码

class Solution {
public:
    bool canReorderDoubled(vector<int>& arr) {
        std::sort(arr.begin(), arr.end());
        unordered_map<double,int> set1;
        for (int val:arr) {
            if (set1.count(val)){
                set1[val]--;
                if (set1[val]==0){
                    set1.erase(val);
                }
            }
            else{
                if (val<0){
                    set1[(double )val/2]++;
                }
                else{
                    set1[val*2]++;
                }
            }
        }
        return set1.empty();
    }
};

973. K Closest Points to Origin

思路

使用TreeMap或者ProiorQueue均可,计算距离,存入结构中。最后取前k个。

代码

    public int[][] kClosest(int[][] points, int k) {
        TreeMap<int[],Integer> map=new TreeMap<>((o1,o2)->{
            int first_val = o1[0]*o1[0]+o1[1]*o1[1];
            int second_val=o2[0]*o2[0]+o2[1]*o2[1];
            if (first_val==second_val){
                if (o1[0]!=o2[0]){
                    return o1[0]-o2[0];
                }
                return o1[1]-o2[1];
            }
            return Integer.compare(first_val,second_val);
        });
        for (var key:points){
            map.merge(key,1,Integer::sum);
        }
        int[][] ans=new int[k][2];
        for (int i = 0; i <k ; i++) {
            ans[i]=map.firstKey();
            map.merge(map.firstKey(),-1,Integer::sum);
            if (map.get(map.firstKey())==0){
                map.remove(map.firstKey());
            }
        }
        return ans;
    }

974. Subarray Sums Divisible by K

思路

对于数组nums从0到n,我们统计每一个nums[0]到nums[i[的和sum。我们知道,如果想要子数组的和被k整除,则和除以k的余数必然相等。例如子数组 n u m s = { 4 , 5 , 0 , − 2 , − 3 , 1 } nums=\{4,5,0,-2,-3,1\} nums={4,5,0,2,3,1}
s u m 0 = 4 sum_0=4 sum0=4 s u m 1 = 9 % 5 = 4 sum_1=9\%5=4 sum1=9%5=4 s u m 2 = 9 % 5 = 4 sum_2=9\%5=4 sum2=9%5=4, s u m 4 = 9 % 5 = 4 sum_4=9\%5=4 sum4=9%5=4因此对于 { 0 , 1 , 2 , 4 } \{0,1,2,4\} {0,1,2,4}任意为开始,任意为结束的子数组(开始坐标<结束坐标),必然可以被k整除。使用求和公式求和即可。
因此整体采用hashMap存储不同的和出现的次数,然后对每一个出现的次数使用求和公式计算。

代码

    public int subarraysDivByK(int[] nums, int k) {
        HashMap<Integer, Integer> sums = new HashMap<>();
        int curr_sum = 0;
        sums.put(0, 1);
        int ans = 0;

        for (int num : nums) {
            curr_sum += num;
            curr_sum %= k;
            curr_sum+=k;
            curr_sum %= k;
            sums.merge(curr_sum, 1, Integer::sum);
        }

        for (var pair : sums.entrySet()) {
            int n = pair.getValue();
            ans += (n) * (n - 1) / 2;
        }
        return ans;
    }

981. Time Based Key-Value Store

思路

以时间为key,设计二维HashMap。

代码

public class TimeMap {
    HashMap<String, TreeMap<Integer,String>> map=new HashMap<>();
    public TimeMap() {

    }

    public void set(String key, String value, int timestamp) {
        if (!map.containsKey(key)) {
            map.put(key, new TreeMap<>());
        }
        map.get(key).put(timestamp,value);
    }

    public String get(String key, int timestamp) {
        if (!map.containsKey(key)){
            return "";
        }
        var res=map.get(key).floorKey(timestamp);
        if (res==null){
            return "";
        }
        return map.get(key).get(res);
    }
}

*1001. Grid Illumination

思路 Hash(注意斜向的处理)

棋盘大小的数据范围为 n n = 1 0 9 n = 10^9 n=109 ,硬模拟「亮灯」的操作必然会 TLE,而 lampsqueries 数据范围为 20000 是一个较为常见的突破口。

由于点亮每一盏灯,可以使得当前 行、列 和 对角线 的位置被照亮,行列可直接使用棋盘坐标的 (x,y)(x, y)(x,y) 来代指,而对角线则可以使用「截距」来进行代指,即使用x+yx−y 进行代指。

分别使用四个「哈希表」rowcolleftright 来记录 行、列 和 对角线 的点亮情况(key 为线编号,value 为点亮次数)。

这样我们可以在 O ( 1 ) O(1) O(1) 的复杂度处理掉所有的 lamps[i],某个位置被照亮的充要条件为:「当前位置所在行被点亮」或「当前位置所在列被点亮」或「当前位置所处的对角线被点亮」

同时,由于每次查询后要进行「灭灯」操作(注意:灭灯只能灭有灯的位置,而不是灭有光的位置 🤣),因此我们还需要另外记录每个灯的位置,可以使用利用二维转一维的技巧进行编号: i d x = x ∗ n + y idx=x∗n+y idx=xn+yi,并使用 HashSet 进行记录(忽略重复的 lamps[i])。

由于询问次数最多为 20000,因此直接在查询完成后模拟「灭灯」即可(访问自身及相邻格子,共 999 个),计算量为 2∗1052 * 10^52∗10
5
以内,可以接受。若某个位置存在灯,将其从 HashSet 中移除,并更新对应线的点亮情况。

代码

class Solution {
    static int[][] dirs = new int[][]{{0, 0}, {0, -1}, {0, 1}, {-1, 0}, {-1, -1}, {-1, 1}, {1, 0}, {1, -1}, {1, 1}};
    static final long N = 20001;
    HashMap<Integer, Integer> rows = new HashMap<>();
    HashMap<Integer, Integer> columns = new HashMap<>();
    HashMap<Integer, Integer> left = new HashMap<>();
    HashMap<Integer, Integer> right = new HashMap<>();
    HashSet<Long> set = new HashSet<>();

    public int[] gridIllumination(int n, int[][] lamps, int[][] queries) {
        for (var lamp : lamps) {
            int r = lamp[0];
            int c = lamp[1];
            int sum = r + c;
            int diff = r - c;
            if (set.contains(r * N + c)) {
                continue;
            }
            rows.merge(r, 1, Integer::sum);
            columns.merge(c, 1, Integer::sum);
            left.merge(sum, 1, Integer::sum);
            right.merge(diff, 1, Integer::sum);
            set.add(r * N + c);
        }
        int m = queries.length;
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            int[] q = queries[i];
            int r = q[0];
            int c = q[1];
            int sum = r + c;
            int diff = r - c;
            if (rows.containsKey(r)||columns.containsKey(c)||left.containsKey(sum)||right.containsKey(diff)){
                ans[i]=1;
            }
            for(var d:dirs){
                int next_r = r + d[0], next_c = c + d[1];
                int next_sum = next_r + next_c, next_diff = next_r - next_c;
                if (next_r < 0 || next_r >= n || next_c < 0 || next_c >= n) continue;
                if (set.contains(next_r * N + next_c)) {
                    set.remove(next_r * N + next_c);
                    if (rows.get(next_r)==1){
                        rows.remove(next_r);
                    }
                    else{
                        rows.merge(next_r,-1,Integer::sum);
                    }
                    if (columns.get(next_c)==1){
                        columns.remove(next_c);
                    }
                    else{
                        columns.merge(next_c,-1,Integer::sum);
                    }
                    if (left.get(next_sum)==1){
                        left.remove(next_sum);
                    }
                    else{
                        left.merge(next_sum,-1,Integer::sum);
                    }
                    if (right.get(next_diff)==1){
                        right.remove(next_diff);
                    }
                    else{
                        right.merge(next_diff,-1,Integer::sum);
                    }
                }
            }
        }
        return ans;
    }

}

1010. Pairs of Songs With Total Durations Divisible by 60

思路 hash

想要两个值的和为60的整数倍,则这两个值分别对60取余得值得和应为60。例如:
80 + 220 = 300 % 60 = 0 80+220=300\%60=0 80+220=300%60=0
= 80 % 60 + 220 % 60 =80\%60+220\%60 =80%60+220%60
= 20 + 40 =20+40 =20+40
而对于非0以及30的情形,只要在相加=60的两个集合 s 1 , s 2 s1,s2 s1,s2中各取一个元素,即可组成一个有效组合。因此方案数= ∣ s 1 ∣ ∗ ∣ s 2 ∣ |s1|*|s2| s1∣s2∣。对于0以及30,因为可以组成组合的元素在本集合 s s s中,方案数= ( n 2 ) \binom{n}{2} (2n)。最终将所有方案数相加即可。

代码

class Solution {
    TreeMap<Integer,Long> map=new TreeMap<>();
    public int numPairsDivisibleBy60(int[] time) {
        for (int val:time){
            map.merge(val%60, 1L,Long::sum);
        }
        Integer upper=map.floorKey(30);
        if (upper==null){
            return 0;
        }
        long ans=0;
        for (int key:map.keySet()){
            if (key>30){
                return (int) ans;
            }
            if (key==0){
                ans+=combinations(map.get(key));
            }
            else if (key==30){
                ans+=combinations(map.get(key));
                break;
            }
            else {
                ans+=map.get(key)*(map.getOrDefault(60 - key, 0L));
            }
        }
        return (int) ans;
    }
    public static long combinations(long n){
        return (n)*(n-1)/2;
    }
}

思路 三Hash

创建两个HashMap和一个HashSet、其中两个HashMap分别是
HashMap<Integer,HashSet<String>> mapper: key 为每个字符串的长度,Value为长度为key的字符串的集合
HashMap<String,Integer> counter:key为字符串,Value为前面连续字符串的数量
,HashSet为
HashSet<String> curr:当前循环中的字符串集合(后续说明)
定义函数
check1DigitDiff(String a,String b):确认a和b是不是只差一个字符
我们知道,如果

代码

class Solution {
    public int longestStrChain(String[] words) {
        HashMap<String,Integer> counter=new HashMap<>();
        HashSet<String> curr=new HashSet<>();
        HashMap<Integer,HashSet<String>> mapper=new HashMap<>();
        for (String word:words){
            if (!mapper.containsKey(word.length())){
                mapper.put(word.length(),new HashSet<>());
            }
            mapper.get(word.length()).add(word);
        }
        int curr_len=1;
        while (curr_len<=16){
            if (!mapper.containsKey(curr_len)){
                curr.clear();
                curr_len++;
                continue;
            }
            if (curr.isEmpty()){
                for(String word:mapper.get(curr_len)){
                    counter.put(word,1);
                    curr.add(word);
                }
            }
            else{
                for(String word:mapper.get(curr_len)){
                    counter.put(word,1);
                    for (String prev:curr){
                        if (check1DigitDiff(prev,word)){
                            counter.put(word,Math.max(counter.get(prev)+1,counter.get(word)));
                        }
                    }
                }
                curr.clear();
                curr.addAll(mapper.get(curr_len));
            }
            curr_len++;
        }
        int ans=1;
        for ( var pair:counter.entrySet()){
            ans=Math.max(pair.getValue(),ans);
        }
        return ans;
    }
    public static boolean check1DigitDiff(String a,String b){
        boolean used=false;
        int b_idx=0;
        for (int i = 0; i < a.length(); i++) {
            if (a.charAt(i)==b.charAt(b_idx)){
                b_idx++;
                continue;
            }
            if (a.charAt(i)!=b.charAt(b_idx)&&!used){
                used=true;
                b_idx++;
                i--;
            }
            else{
                return false;
            }
        }
        return true;
    }

}

2829 Determine the Minimum Sum of a k-avoiding Array

思路

采取hashset的方式,从小到大逐渐遍历每一个数,并将k关于起的互补数即 a + b = k a+b=k a+b=k置入结果中。直到放满n个元素则结果数+1。

代码

    public int minimumSum(int n, int k) {
        HashSet<Integer> set=new HashSet<>();
        int sum=0;
        int elements=0;
        int base=1;
        while (elements<n){
            if (!set.contains(base)){
                set.add(k-base);
                sum+=base;
                elements++;
            }
            base++;
        }
        return sum;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值