代码随想录之哈希表篇

106 阅读12分钟

代码随想录之哈希表篇

T242-有效的字母异位词

见LeetCode第242题[有效的字母异位词]

题目描述

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的 字母异位词。

字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次。

示例 1:

输入: s = "anagram", t = "nagaram"
输出: true

我的思路

  • 使用int[26] codebook数组存储每个单词出现的频率
  • 对于s中的每个字符,codebook[c - 'a']++
  • 对于t中的每个字符,codebook[c - 'a']--
  • 最后遍历codebook中每个元素是否等于0,全部等于0则返回true
/**
 * 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的
 * 字母异位词
 * @param s
 * @param t
 * @return
 */
public boolean isAnagram(String s, String t) {
    // 边界条件判断
    if (s == null || t == null || s.length() != t.length()) return false;

    int[] dict = new int[26];
    for (char c : s.toCharArray()) {
        dict[c - 'a']++;
    }
    for (char c : t.toCharArray()) {
        dict[c - 'a']--;
    }

    for (int counts : dict) {
        if (counts != 0) return false;
    }

    return true;
}
  • 时间复杂度:O(m + n),其中m | n分别表示字符串s | t的长度
  • 空间复杂度:O(1),固定长度的数组int[26]用来记录每个字符出现的频率

T49-字母异位词分组

见LeetCode第49题[字母异位词分组]

题目描述

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

我的思路

  • 使用HashMap<String, List<String>>记录结果
  • HashMap的键String通过对字符的词频表压缩而来,即bat -> a1b1t1, teed -> d1e2t1
/**
 * 字符异位词分组
 * @param strs
 * @return
 */
public List<List<String>> groupAnagrams(String[] strs) {
    List<List<String>> res = new ArrayList<>();
    if (strs.length <= 1) {
        res.add(Arrays.asList(strs));
        return res;
    }
    Map<String, List<String>> dict = new HashMap<>();

    for (String str : strs) {
        String code = compress(str);
        if (dict.containsKey(code)) {
            dict.get(code).add(str);
        } else {
            List<String> temp = new ArrayList<>();
            temp.add(str);
            dict.put(code, temp);
        }
    }

    // 遍历 map, 获取结果
    for (Map.Entry<String, List<String>> entry : dict.entrySet()) {
        res.add(entry.getValue());
    }
    return res;
}
  • 时间复杂度为:O(m * n),其中m为字符串数组的长度,n为每个字符串的长度
  • 空间复杂度为:O(m),需要额外的空间存储map

优化点

  • compress()方法就是为了让异位词获得相同的key
  • 可以对异位词str.toCharArray()数组进行排序, 即Arrays.sort(char[]),然后重构异位词String key = new String(char[])作为异位词组的key
  • 这样在LeetCode上效率会高点

PS:LeetCode击败人数,图一乐

T438-找到字符串的所有字母异位词

见力扣第438题[找到字符串的所有字母异位词]

题目描述

给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

示例 1:

输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

我的思路

  • 窗口的大小为p.length
  • 每次窗口滑动之后,都要检查一下,窗口中的字符串是否是p的异位词
  • 窗口和p的字符频率表使用int[26]存储
/**
 * 从 S 中找到 p 的所有字母异位词
 * @param s
 * @param p
 * @return
 */
public List<Integer> findAnagrams(String s, String p) {
    List<Integer> anagrams = new ArrayList<>();
    if (s.length() < p.length()) return anagrams;
    int n = p.length();
    int[] winDict = new int[26];
    int[] pDict = new int[26];

    // pDict 和winDict 初始化
    for (char c : p.toCharArray()) {
        pDict[c - 'a']++;
    }
    int r = 0;
    int l = 0;
    while (r < n) {
        winDict[s.charAt(r++) - 'a']++;
    }
    // 先判断是否是为 异位词
    if (isAnagram(winDict, pDict)) {
        anagrams.add(l);
    }

    // 窗口为一个左闭右开的区间[l, r)
    while (r < s.length()) {
        // 窗口右移
        winDict[s.charAt(r++) - 'a']++;
        winDict[s.charAt(l++) - 'a']--;
        // 判断是否为 异位词
        if (isAnagram(winDict, pDict)) {
            anagrams.add(l);
        }
    }
    return anagrams;

}

/**
 * 判断两个字符数组是否相同
 * @param winDict
 * @param pDict
 * @return
 */
private boolean isAnagram(int[] winDict, int[] pDict) {
    for (int i = 0; i < winDict.length; i++) {
        if (winDict[i] != pDict[i]) {
            return false;
        }
    }
    return true;
}
  • 时间复杂度:O(m + n),遍历了一遍字符串s和字符串t,对于字符频率表的遍历是常数级别的
  • 空间复杂度:O(1),额外的空间为int[]数组,用来存储字符串的字符频率

T349-两个数组的交集

见力扣第349题[两个数组的交集]

题目描述

给定两个数组 nums1nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

思路分析

  • 根据提示,两个数组中数字的范围为[0, 1000],因此可以创建一个boolean[1001]的数组确定nums中的某个数是否出现
  • 先遍历一遍nums1,初始化boolean
  • 遍历一遍nums2,如果boolean[nums2[i]]为真,则将nums[2]存储到临时结果集Set<Integer>
  • 临时结果集去重之后,加入数组,返回
public int[] intersection(int[] nums1, int[] nums2) {
    boolean[] isExist = new boolean[10001];
    Set<Integer> temp = new HashSet<>();
    for (int num : nums1) {
        isExist[num] = true;
    }
    for (int num : nums2) {
        if (isExist[num]) {
            temp.add(num);
        }
    }
    int[] commons = new int[temp.size()];
    int i = 0;
    for (int num : temp) {
        commons[i++] = num;
    }
    return commons;

}
  • 时间复杂度:O(m + n + k),遍历了一遍长度为mnums1数组和长度为nnums2数组以及大小为kset集合
  • 空间复杂度:O(k),额外空间去重集合,去除结果中的重复数据

个人思考:使用isExist数组好像没有多省空间,可能还不如hashMap。但是数组省去了计算哈希值的复杂度。

T202-快乐数

见力扣第202题[快乐数]

题目描述

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

示例 1:

输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

我的思路

  • 如果不是快乐数,就会无限循环,就相当于由节点构成的链表有环
  • 应当定义快慢指针,快指针计算两次,慢指针计算一次
    • 如果慢指针的值为1,直接返回true
    • 如果快慢指针相等:直接返回false
  • 需要定义计算平方和的方法
/**
 * 快乐数
 * @param n
 * @return
 */
public boolean isHappy(int n) {
    if (n == 1) return true;
    int slow = n;
    int fast = squareSum(n);
    while (slow != fast) {
        if (fast == 1) return true;
        slow = squareSum(slow);
        fast = squareSum(fast);
        fast = squareSum(fast);
    }
    return false;
}

/**
 * 计算数字的平方和
 * @param n
 * @return
 */
private int squareSum(int n) {
    int sum = 0;
    int remainder = 0;
    while (n != 0) {
        remainder = n % 10;
        sum += remainder * remainder;
        n = n / 10;
    }
    return sum;
}

时间复杂度:O(n),假设链表的长度为n

空间复杂度:O(1),个别常量用来存储快慢指针

T1-两数之和

见力扣第1题[两数之和]

题目描述

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

我的思路

  • 使用额外HashMap存储数组中的元素
  • 遍历数组,如果map 中不存在target - nums[i],则将nums[i]插入到哈希表中
  • 如果存在的话,返回当前的坐标i以及mapdiff的坐标j
public int[] twoSum(int[] nums, int target) {
    HashMap<Integer, Integer> map = new HashMap<>();
    int[] res = new int[2];
    for (int i = 0; i < nums.length; i++) {
        if (!map.containsKey(target - nums[i])) {
            map.put(nums[i], i);
        } else {
            res[0] = map.get(target - nums[i]);
            res[1] = i;
            return res;
        }
    }
    return res;
}
  • 时间复杂度:O(n),遍历了一遍数组
  • 空间复杂度:O(n),使用额外空间map来帮助判断当前数的另一个加数是否存在

T454-四数相加II

见力扣第454题[四数相加II].

题目描述

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

我的思路[超时]

  • 根据题意可知,要从每一个数组中都要选择一个数,不能不选
  • 可以先将nums3nums[4]中的数进行全排列相加,放入到list1
  • 然后将nums2list1中的数全排列相加,放入到list2
  • 然后将nums1list2中的数全排列相加,如果等于0,计数器count++

上述思路其实算是全排列了,时间复杂度为O(N4)O(N^4)。后续想到了分组,但是如果将nums2, nums3, nums4的结果存储到Map中,时间复杂度还是O(N3)O(N ^ 3)。要两两分组,时间复杂度才能降为O(n2)O(n^2)

正确思路

  • nums1nums2的结果值放到map中,其中mapvalue为和出现的次数。
  • 两层循环遍历nums3nums4,如果map中存在-(num3[i] + nums4[j]),计数器加map中的value
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
    HashMap<Integer, Integer> map = new HashMap<>();
    for (int num1 : nums1) {
        for (int num2 : nums2) {
            int sum = num1 + num2;
            if (!map.containsKey(sum)) {
                map.put(sum, 0);
            }
            map.put(sum, map.get(sum) + 1);
        }
    }

    // 遍历 nums3 和 nums4
    int count = 0;
    for (int num3 : nums3) {
        for (int num4 : nums4) {
            int sum = num3 + num4;
            if (map.containsKey(-sum)) {
                count += map.get(-sum);
            }
        }
    }
    return count;
}

T383-赎金信

见力扣第383题[赎金信]

题目描述

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1:

输入:ransomNote = "aa", magazine = "aab"
输出:false

思路分析

  • 提示说字母全部都是小写的,因此我们同样可以使用数组int[26]来记录每个字符的频次
  • 只有当magazine的字符频次涵盖ransomNote时,返回true
  • covering(ransomNote, magazine)magazine中的每个元素都大于或者等于ransomNote
if (ransomNote.length() >  magazine.length()) return false;

int[] noteDict = new int[26];
for (char c : magazine.toCharArray()) {
    noteDict[c - 'a']++;
}
for (char c : ransomNote.toCharArray()) {
    noteDict[c - 'a']--;
}


for (int j : noteDict) {
    if (j < 0) return false;
}
return true;

时间复杂度:O(n + m),需要遍历字符串

空间复杂度:O(1),额外的静态数组来存储字符串的字符频次

T15-三数之和

见力扣第15题[三数之和]

题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

**注意:**答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1][-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

我的思路

  • 对数组从小到大排序
  • 固定一个数字nums[i],双指针nums[j] + nums[k] 是否为-nums[i]
    • 大于-nums[i]nums[k]左移
    • 小于-nums[i]nums[j]右移
  • 对于ij需要进行两次剪枝避免重复
public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    Arrays.sort(nums);
    if (nums[0] > 0) return res;
    for (int i = 0; i < nums.length - 2; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        int j = i + 1;
        int k = nums.length - 1;
        while (j < k) {
            if (nums[i] + nums[j] + nums[k] == 0) {
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    j++;
                    k--;
                } else {
                    List<Integer> temp = new ArrayList<>();
                    temp.add(nums[i]);
                    temp.add(nums[j]);
                    temp.add(nums[k]);
                    j++;
                    k--;
                    res.add(temp);
                }
            } else if (nums[i] + nums[j] + nums[k] < 0) {
                j++;
            } else {
                k--;
            }
        }
    }
    return res;

}

时间复杂度:O(N2)O(N^2),其中NN是数组的长度

空间复杂度:O(N)O(N),进行排序需要额外的数组保存排序结果

T18-四数之和

见力扣第18题[四数之和]

题目描述

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

我的思路

  • 四数之和,遍历最外层的,相当于内层是三数之和
  • 需要注意去重,避免重复结果,通过剪枝,降低复杂度
  • 最需要注意的是:使用long保存临时和,防止加法溢出
public List<List<Integer>> fourSum(int[] nums, int target) {
    List<List<Integer>> resList = new ArrayList<>();
    Arrays.sort(nums);
    if (nums[0] > 0 && target <= 0) return resList;
    if (nums[nums.length - 1] < 0 && target >= 0) return resList;
    for (int i = 0; i < nums.length - 3; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        if (nums[i] > target && nums[i] >= 0) break;
        long remainder1 = target - nums[i];
        for (int j = i + 1; j < nums.length - 2; j++) {
            if (j > i + 1 && nums[j] == nums[j - 1]) continue;
            long reminder2 = remainder1 - nums[j];
            // 这里可以优化,剪枝处理
            if (nums[i] + nums[j] > target && nums[i] + nums[j] > 0) break;
            
            int l = j + 1;
            int r = nums.length - 1;
            while (l < r) {
                if (nums[l] + nums[r] == reminder2) {
                    List<Integer> temp = new ArrayList<>();
                    temp.add(nums[i]);
                    temp.add(nums[j]);
                    temp.add(nums[l]);
                    temp.add(nums[r]);
                    resList.add(temp);
                    r--;
                    l++;
                    // 剪枝
                    while (r >= 0 && nums[r] == nums[r + 1]) r--;
                    while (l < nums.length && nums[l] == nums[l - 1]) l++;
                } else if (nums[l] + nums[r] < reminder2) {
                    l++;
                } else {
                    r--;
                }
            }
        }
    }
    return resList;
}
  • 时间复杂度:O(N3)O(N^3),排序的时间复杂度为O(NlogN)O(N\log N),遍历四个数的时间复杂度为O(N3)O(N^3)
  • 空间复杂度:O(N)O(N),排序的时候使用到了额外的空间