1935.可以输入的最大单词数

目标

键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。

给你一个由若干单词组成的字符串 text ,单词间由单个空格组成(不含前导和尾随空格);另有一个字符串 brokenLetters ,由所有已损坏的不同字母键组成,返回你可以使用此键盘完全输入的 text 中单词的数目。

示例 1:

输入:text = "hello world", brokenLetters = "ad"
输出:1
解释:无法输入 "world" ,因为字母键 'd' 已损坏。

示例 2:

输入:text = "leet code", brokenLetters = "lt"
输出:1
解释:无法输入 "leet" ,因为字母键 'l' 和 't' 已损坏。

示例 3:

输入:text = "leet code", brokenLetters = "e"
输出:0
解释:无法输入任何单词,因为字母键 'e' 已损坏。

说明:

  • 1 <= text.length <= 10^4
  • 0 <= brokenLetters.length <= 26
  • text 由若干用单个空格分隔的单词组成,且不含任何前导和尾随空格
  • 每个单词仅由小写英文字母组成
  • brokenLetters 由 互不相同 的小写英文字母组成

思路

有一个字符串,其中的单词由空格分隔,现在键盘有一些按键不能正常工作,返回能够正常输入的单词个数。

使用哈希表记录不正常的按键,分割单词,判断单词是否包含不正常字符。

代码


/**
 * @date 2025-09-15 8:46
 */
public class CanBeTypedWords1935 {

    public int canBeTypedWords(String text, String brokenLetters) {
        Set<Character> brokenSet = new HashSet<>();
        for (char c : brokenLetters.toCharArray()) {
            brokenSet.add(c);
        }
        String[] words = text.split(" ");
        int res = words.length;
        for (String word : words) {
            for (char c : word.toCharArray()) {
                if (brokenSet.contains(c)){
                    res--;
                    break;
                }
            }
        }
        return res;
    }
}

性能

966.元音拼写检查器

目标

在给定单词列表 wordlist 的情况下,我们希望实现一个拼写检查器,将查询单词转换为正确的单词。

对于给定的查询单词 query,拼写检查器将会处理两类拼写错误:

  • 大小写:如果查询匹配单词列表中的某个单词(不区分大小写),则返回的正确单词与单词列表中的大小写相同。
    • 例如:wordlist = ["yellow"], query = "YellOw": correct = "yellow"
    • 例如:wordlist = ["Yellow"], query = "yellow": correct = "Yellow"
    • 例如:wordlist = ["yellow"], query = "yellow": correct = "yellow"
  • 元音错误:如果在将查询单词中的元音 ('a', 'e', 'i', 'o', 'u') 分别替换为任何元音后,能与单词列表中的单词匹配(不区分大小写),则返回的正确单词与单词列表中的匹配项大小写相同。
    • 例如:wordlist = ["YellOw"], query = "yollow": correct = "YellOw"
    • 例如:wordlist = ["YellOw"], query = "yeellow": correct = "" (无匹配项)
    • 例如:wordlist = ["YellOw"], query = "yllw": correct = "" (无匹配项)

此外,拼写检查器还按照以下优先级规则操作:

  • 当查询完全匹配单词列表中的某个单词(区分大小写)时,应返回相同的单词。
  • 当查询匹配到大小写问题的单词时,您应该返回单词列表中的第一个这样的匹配项。
  • 当查询匹配到元音错误的单词时,您应该返回单词列表中的第一个这样的匹配项。
  • 如果该查询在单词列表中没有匹配项,则应返回空字符串。

给出一些查询 queries,返回一个单词列表 answer,其中 answer[i] 是由查询 query = queries[i] 得到的正确单词。

示例 1:

输入:wordlist = ["KiTe","kite","hare","Hare"], queries = ["kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"]
输出:["kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"]

示例 2:

输入:wordlist = ["yellow"], queries = ["YellOw"]
输出:["yellow"]

说明:

  • 1 <= wordlist.length, queries.length <= 5000
  • 1 <= wordlist[i].length, queries[i].length <= 7
  • wordlist[i] 和 queries[i] 只包含英文字母

思路

有一个单词列表 wordlist,将输入单词 query 按照下面规则转换为列表中的单词:

  • 如果 query 在 wordlist 中直接返回
  • 如果 query 忽略大小写后在 wordlist 中返回第一个匹配的单词
  • 如果 query 忽略大小并且其中的元音替换为任意元音后在 wordlist 中返回第一个匹配的单词
  • 如果以上都没有匹配返回空串

依题意模拟即可。创建三个哈希表,key 分别为 原单词、忽略大小写后的单词以及 将元音字母替换为通配符后的单词。

代码


/**
 * @date 2025-09-14 21:25
 */
public class Spellchecker966 {

    public String[] spellchecker(String[] wordlist, String[] queries) {
        Set<String> originSet = new HashSet<>();
        Map<String, List<String>> caseMap = new HashMap<>();
        Map<String, List<String>> vowelMap = new HashMap<>();
        for (String word : wordlist) {
            originSet.add(word);
            String key = word.toLowerCase();
            caseMap.putIfAbsent(key, new ArrayList<>());
            caseMap.get(key).add(word);
            String vowelKey = key.replaceAll("[aeiouAEIOU]", "*");
            vowelMap.putIfAbsent(vowelKey, new ArrayList<>());
            vowelMap.get(vowelKey).add(word);
        }
        for (int i = 0; i < queries.length; i++) {
            String query = queries[i];
            if (originSet.contains(query)) {
                continue;
            }
            String key = query.toLowerCase();
            List<String> caseList = caseMap.get(key);
            if (caseList != null && caseList.size() > 0) {
                queries[i] = caseList.get(0);
                continue;
            }
            List<String> vowelList = vowelMap.get(key.replaceAll("[aeiouAEIOU]", "*"));
            if (vowelList != null && vowelList.size() > 0) {
                queries[i] = vowelList.get(0);
                continue;
            }
            queries[i] = "";
        }
        return queries;
    }

}

性能

3487.删除后的最大子数组元素和

目标

给你一个整数数组 nums 。

你可以从数组 nums 中删除任意数量的元素,但不能将其变为 空 数组。执行删除操作后,选出 nums 中满足下述条件的一个子数组:

  • 子数组中的所有元素 互不相同 。
  • 最大化 子数组的元素和。

返回子数组的 最大元素和 。

子数组 是数组的一个连续、非空 的元素序列。

示例 1:

输入:nums = [1,2,3,4,5]
输出:15
解释:
不删除任何元素,选中整个数组得到最大元素和。

示例 2:

输入:nums = [1,1,0,1,1]
输出:1
解释:
删除元素 nums[0] == 1、nums[1] == 1、nums[2] == 0 和 nums[3] == 1 。选中整个数组 [1] 得到最大元素和。

示例 3:

输入:nums = [1,2,-1,-2,1,0,-1]
输出:3
解释:
删除元素 nums[2] == -1 和 nums[3] == -2 ,从 [1, 2, 1, 0, -1] 中选中子数组 [2, 1] 以获得最大元素和。

说明:

  • 1 <= nums.length <= 100
  • -100 <= nums[i] <= 100

思路

有一个数组 nums,允许删除其中任意元素得到一个 非空 数组,使得最终数组中 不包含重复元素 并且所有 元素和最大

为了使和最大,我们应该删掉所有的负数,同时对剩余元素去重累加求和。需要注意如果数组中全是负数,需要保留最大元素。

代码


/**
 * @date 2025-07-25 8:55
 */
public class MaxSum3487 {

    public int maxSum(int[] nums) {
        int sum = Arrays.stream(nums).distinct().filter(x -> x > 0).sum();
        if (sum == 0){
            OptionalInt max = Arrays.stream(nums).max();
            return max.isPresent() ? max.getAsInt() : sum;
        }
        return sum;
    }

}

性能

1128.等价多米诺骨牌对的数量

目标

给你一组多米诺骨牌 dominoes 。

形式上,dominoes[i] = [a, b] 与 dominoes[j] = [c, d] 等价 当且仅当 (a == c 且 b == d) 或者 (a == d 且 b == c) 。即一张骨牌可以通过旋转 0 度或 180 度得到另一张多米诺骨牌。

在 0 <= i < j < dominoes.length 的前提下,找出满足 dominoes[i] 和 dominoes[j] 等价的骨牌对 (i, j) 的数量。

示例 1:

输入:dominoes = [[1,2],[2,1],[3,4],[5,6]]
输出:1

示例 2:

输入:dominoes = [[1,2],[1,2],[1,1],[1,2],[2,2]]
输出:3

说明:

  • 1 <= dominoes.length <= 4 * 10^4
  • dominoes[i].length == 2
  • 1 <= dominoes[i][j] <= 9

思路

计算二维数组中相同元素组成的下标对 (i, j) 个数,这里相同元素指 (dominoes[i][0] == dominoes[j][0] && dominoes[i][1] == dominoes[j][1]) || (dominoes[i][0] == dominoes[j][1] && dominoes[i][1] == dominoes[j][0])

使用哈希表记录相同元素的出现次数,key 可以使用 dominoes[i][0] << 4 | dominoes[i][1]dominoes[i][1] << 4 | dominoes[i][0] 表示。

代码


/**
 * @date 2025-05-04 17:38
 */
public class NumEquivDominoPairs1128 {

    public int numEquivDominoPairs(int[][] dominoes) {
        Map<Integer, Integer> map = new HashMap<>();
        int res = 0;
        for (int[] dominoe : dominoes) {
            int key = dominoe[0] << 4 | dominoe[1];
            res += map.getOrDefault(key, 0);
            map.merge(key, 1, Integer::sum);
            if (dominoe[0] != dominoe[1]) {
                map.merge(dominoe[1] << 4 | dominoe[0], 1, Integer::sum);
            }
        }
        return res;
    }

}

性能

781.森林中的兔子

目标

森林中有未知数量的兔子。提问其中若干只兔子 "还有多少只兔子与你(指被提问的兔子)颜色相同?" ,将答案收集到一个整数数组 answers 中,其中 answers[i] 是第 i 只兔子的回答。

给你数组 answers ,返回森林中兔子的最少数量。

示例 1:

输入:answers = [1,1,2]
输出:5
解释:
两只回答了 "1" 的兔子可能有相同的颜色,设为红色。 
之后回答了 "2" 的兔子不会是红色,否则他们的回答会相互矛盾。
设回答了 "2" 的兔子为蓝色。 
此外,森林中还应有另外 2 只蓝色兔子的回答没有包含在数组中。 
因此森林中兔子的最少数量是 5 只:3 只回答的和 2 只没有回答的。

示例 2:

输入:answers = [10,10,10]
输出:11

说明:

  • 1 <= answers.length <= 1000
  • 0 <= answers[i] < 1000

思路

对森林中的兔子提问 有多少只兔子与你颜色相同answers[i] 表示下标为 i 的兔子的回答,问森林中最少有多少只兔子。

假设被提问的兔子的颜色都不相同,那么兔子最少有 sum(answers[i]) + n,即被提问的兔子个数 n 加上与它们颜色相同的兔子个数。

如果被提问的兔子的颜色相同,那么它们的回答一定相同。可以根据回答进行分组,如果分组个数 groupCnt 超过了颜色个数 colorCnt,说明是其它颜色。只需要分组后对 groupCnt / colorCnt 向上取整,然后乘以 colorCnt 即可,即 ⌈groupCnt / colorCnt⌉ * colorCnt

代码


/**
 * @date 2025-04-20 14:09
 */
public class NumRabbits781 {

    public int numRabbits(int[] answers) {
        int res = 0;
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : answers) {
            map.merge(num, 1, Integer::sum);
        }
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            Integer colorCnt = entry.getKey() + 1;
            Integer groupCnt = entry.getValue();
            int colors = (groupCnt + colorCnt - 1) / colorCnt;
            res += colors * colorCnt;
        }
        return res;
    }
}

性能

2364.统计坏数对的数目

目标

给你一个下标从 0 开始的整数数组 nums 。如果 i < j 且 j - i != nums[j] - nums[i] ,那么我们称 (i, j) 是一个 坏数对 。

请你返回 nums 中 坏数对 的总数目。

示例 1:

输入:nums = [4,1,3,3]
输出:5
解释:数对 (0, 1) 是坏数对,因为 1 - 0 != 1 - 4 。
数对 (0, 2) 是坏数对,因为 2 - 0 != 3 - 4, 2 != -1 。
数对 (0, 3) 是坏数对,因为 3 - 0 != 3 - 4, 3 != -1 。
数对 (1, 2) 是坏数对,因为 2 - 1 != 3 - 1, 1 != 2 。
数对 (2, 3) 是坏数对,因为 3 - 2 != 3 - 3, 1 != 0 。
总共有 5 个坏数对,所以我们返回 5 。

示例 2:

输入:nums = [1,2,3,4,5]
输出:0
解释:没有坏数对。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9

思路

求数组中有多少坏数对,定义坏数对 (i, j) 满足 i < j && j - i != nums[j] - nums[i]

将题目中的条件转化一下,j - nums[j] != i - nums[i],可以维护一个 下标 - 元素值 数组。问题变为求当前下标前有多少个元素与当前元素 不同。对元素值计数,使用当前元素下标(表示 i 前的元素个数)减去当前元素值在前面的出现次数即可。

代码


/**
 * @date 2025-04-18 8:52
 */
public class CountBadPairs2364 {

    public long countBadPairs(int[] nums) {
        int n = nums.length;
        long res = 0L;
        Map<Integer, Long> cnt = new HashMap<>();
        for (int i = 0; i < n; i++) {
            res += i - cnt.merge(i - nums[i], 1L, Long::sum) + 1;
        }
        return res;
    }
}

性能

2176.统计数组中相等且可以被整除的数对

目标

给你一个下标从 0 开始长度为 n 的整数数组 nums 和一个整数 k ,请你返回满足 0 <= i < j < n ,nums[i] == nums[j] 且 (i * j) 能被 k 整除的数对 (i, j) 的 数目 。

示例 1:

输入:nums = [3,1,2,2,2,1,3], k = 2
输出:4
解释:
总共有 4 对数符合所有要求:
- nums[0] == nums[6] 且 0 * 6 == 0 ,能被 2 整除。
- nums[2] == nums[3] 且 2 * 3 == 6 ,能被 2 整除。
- nums[2] == nums[4] 且 2 * 4 == 8 ,能被 2 整除。
- nums[3] == nums[4] 且 3 * 4 == 12 ,能被 2 整除。

示例 2:

输入:nums = [1,2,3,4], k = 1
输出:0
解释:由于数组中没有重复数值,所以没有数对 (i,j) 符合所有要求。

说明:

  • 1 <= nums.length <= 100
  • 1 <= nums[i], k <= 100

思路

找出数组中下标不同的数对,使数对元素值相同且下标的乘积能够被 k 整除,返回满足要求的数对个数。

根据题意模拟即可。

有网友提出了使用原数组原地保存后面一个相同元素的下标,组成一个元素值相同的链表,避免重复判断不相等的元素。

也有网友提出了 O(nlogn) 的解法,与当前元素 nums[j] 组成合法数对需要满足 i < j,并且 nums[i] == nums[j] && (i * j) % k == 0nums[i] 必须提供因子 k2 = k / gcd(k, j),问题转化为查找 j 前,包含因子 k2 且与当前元素值相同的元素个数。提前预处理每一个数的因子列表,遍历当前值的因子列表,将每一个因子与当前值组成 key 并计数。

代码

/**
 * @date 2025-04-17 0:09
 */
public class CountPairs2176 {

    public int countPairs(int[] nums, int k) {
        int n = nums.length;
        int res = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (nums[i] == nums[j] && i * j % k == 0){
                    res++;
                }
            }
        }
        return res;
    }
}

性能

2716.最小化字符串长度

目标

给你一个下标从 0 开始的字符串 s ,重复执行下述操作 任意 次:

  • 在字符串中选出一个下标 i ,并使 c 为字符串下标 i 处的字符。并在 i 左侧(如果有)和 右侧(如果有)各 删除 一个距离 i 最近 的字符 c 。

请你通过执行上述操作任意次,使 s 的长度 最小化 。

返回一个表示 最小化 字符串的长度的整数。

示例 1:

输入:s = "aaabc"
输出:3
解释:在这个示例中,s 等于 "aaabc" 。我们可以选择位于下标 1 处的字符 'a' 开始。接着删除下标 1 左侧最近的那个 'a'(位于下标 0)以及下标 1 右侧最近的那个 'a'(位于下标 2)。执行操作后,字符串变为 "abc" 。继续对字符串执行任何操作都不会改变其长度。因此,最小化字符串的长度是 3 。

示例 2:

输入:s = "cbbd"
输出:3
解释:我们可以选择位于下标 1 处的字符 'b' 开始。下标 1 左侧不存在字符 'b' ,但右侧存在一个字符 'b'(位于下标 2),所以会删除位于下标 2 的字符 'b' 。执行操作后,字符串变为 "cbd" 。继续对字符串执行任何操作都不会改变其长度。因此,最小化字符串的长度是 3 。

示例 3:

输入:s = "dddaaa"
输出:2
解释:我们可以选择位于下标 1 处的字符 'd' 开始。接着删除下标 1 左侧最近的那个 'd'(位于下标 0)以及下标 1 右侧最近的那个 'd'(位于下标 2)。执行操作后,字符串变为 "daaa" 。继续对新字符串执行操作,可以选择位于下标 2 的字符 'a' 。接着删除下标 2 左侧最近的那个 'a'(位于下标 1)以及下标 2 右侧最近的那个 'a'(位于下标 3)。执行操作后,字符串变为 "da" 。继续对字符串执行任何操作都不会改变其长度。因此,最小化字符串的长度是 2 。

说明:

  • 1 <= s.length <= 100
  • s 仅由小写英文字母组成

思路

每次操作可以从字符串 s 中任选一个字符 c,同时删除其左侧与右侧距离最近的相同字符。求执行操作任意次后字符串的最小长度。

由于选中的字符不会被删除,本质是返回字符串中不同字符的个数。

代码


/**
 * @date 2025-03-28 0:20
 */
public class MinimizedStringLength2716 {

    /**
     * 位运算,将出现过的字符保存到 mask 中
     */
    public int minimizedStringLength_v1(String s) {
        int n = s.length();
        int mask = 0;
        for (int i = 0; i < n; i++) {
            mask |= 1 << (s.charAt(i) - 'a');
        }
        return Integer.bitCount(mask);
    }

    public int minimizedStringLength(String s) {
        int n = s.length();
        Set<Character> set = new HashSet<>();
        for (int i = 0; i < n; i++) {
            set.add(s.charAt(i));
        }
        return set.size();
    }
}

性能

2610.转换二维数组

目标

给你一个整数数组 nums 。请你创建一个满足以下条件的二维数组:

  • 二维数组应该 只 包含数组 nums 中的元素。
  • 二维数组中的每一行都包含 不同 的整数。
  • 二维数组的行数应尽可能 少 。

返回结果数组。如果存在多种答案,则返回其中任何一种。

请注意,二维数组的每一行上可以存在不同数量的元素。

示例 1:

输入:nums = [1,3,4,1,2,3,1]
输出:[[1,3,4,2],[1,3],[1]]
解释:根据题目要求可以创建包含以下几行元素的二维数组:
- 1,3,4,2
- 1,3
- 1
nums 中的所有元素都有用到,并且每一行都由不同的整数组成,所以这是一个符合题目要求的答案。
可以证明无法创建少于三行且符合题目要求的二维数组。

示例 2:

输入:nums = [1,2,3,4]
输出:[[4,3,2,1]]
解释:nums 中的所有元素都不同,所以我们可以将其全部保存在二维数组中的第一行。

说明:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= nums.length

思路

将数组转化为二维数组,要求每一行元素没有重复,并且行数尽可能地少。

使用哈希表对相同元素计数,将所有 key 放入一行并将所有 key 的计数减一,如果计数减为 0 则删除 key,直到哈希表为空,时间复杂度为 O(n)

也可以将哈希表改为数组相当于计数排序。

代码


/**
 * @date 2025-03-19 0:21
 */
public class FindMatrix2610 {

    public List<List<Integer>> findMatrix(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.merge(num, 1, Integer::sum);
        }
        List<List<Integer>> res = new ArrayList<>();
        while (!map.isEmpty()) {
            res.add(new ArrayList<>());
            List<Integer> list = res.get(res.size() - 1);
            Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<Integer, Integer> entry = iterator.next();
                list.add(entry.getKey());
                entry.setValue(entry.getValue() - 1);
                if (entry.getValue() == 0) {
                    iterator.remove();
                }
            }
        }
        return res;
    }

}

性能

350.两个数组的交集II

目标

给你两个整数数组 nums1 和 nums2,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。

示例 1:

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

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]

说明:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 1000

进阶:

  • 如果给定的数组已经排好序呢?你将如何优化你的算法?
  • 如果 nums1 的大小比 nums2 小,哪种方法更优?
  • 如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

思路

求两个数组 nums1nums2 的交集(不考虑元素顺序),比如 a b c be b d 的交集为 b

使用哈希表对数组 nums1 的元素值计数,遍历 nums2 获取对应元素在 nums1 中的数量,如果大于0,将元素加入列表,并将数量减一。

如果已经排好序可以直接使用双指针,每次比较移动元素值小的指针。

交集的最大元素不会超过两个数组长度的最小值,因此初始化时可以取长度较小的数组进行计数。

如果 nums2 存储在磁盘上,根据上面的讨论,我们对 nums1 计数,每次从磁盘读取一部分数据进行判断即可。

代码


/**
 * @date 2025-01-30 21:37
 */
public class Intersect350 {

    public int[] intersect_v2(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length) {
            intersect_v2(nums2, nums1);
        }
        Arrays.sort(nums1);
        Arrays.sort(nums2);
        int[] res = new int[nums1.length];
        int j = 0;
        int index = 0;
        for (int i = 0; i < nums2.length && j < nums1.length; i++) {
            while (j < nums1.length && nums2[i] > nums1[j]) {
                j++;
            }
            if (j == nums1.length) {
                break;
            }
            if (nums2[i] == nums1[j]) {
                res[index++] = nums1[j];
                j++;
            }
        }

        return Arrays.copyOfRange(res, 0, index);
    }

    public int[] intersect_v1(int[] nums1, int[] nums2) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int i : nums1) {
            cnt.merge(i, 1, Integer::sum);
        }
        List<Integer> list = new ArrayList<>();
        for (int i : nums2) {
            int value = cnt.getOrDefault(i, 0) - 1;
            if (value >= 0) {
                list.add(i);
                cnt.put(i, value);
            }
        }
        return list.stream().mapToInt(i -> i).toArray();
    }

}

性能