Leetcode算法整理(1)——哈希表

106 阅读21分钟
1. 两数之和 leetcode.cn/problems/tw…

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回答案。

var twoSum = (nums, target) => {
    // 创建一个对象来存储出现过的数字和对应的索引
    const prevNums = {};

    for (let i = 0; i < nums.length; i++) {         // 遍历元素   
        const curNum = nums[i];                     // 当前元素   
        const targetNum = target - curNum;          // 满足要求的目标元素   
        const targetNumIndex = prevNums[targetNum]; // 在prevNums中获取目标元素的索引
        if (targetNumIndex !== undefined) {         // 如果存在,直接返回 [目标元素的索引, 当前索引]
            return [targetNumIndex, i];
        } else {                                    // 如果不存在,说明之前没出现过目标元素
            prevNums[curNum] = i;                   // 存入当前的元素和对应的索引
        }
    }
};

function twoSum(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        const targetNum = target - nums[i];
        if (map.has(targetNum)) {
            return [map.get(targetNum), i];
        }
        map.set(nums[i], i);  // 等同于 else {map.set(nums[i], i);}
    }
    // 如果没有找到,返回空数组
    return [];
}
// 测试函数
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]
console.log(twoSum([3, 2, 4], 6)); // [1, 2]
console.log(twoSum([3, 3], 6)); // [0, 1]

const 声明的特性: 使用 const 声明的变量在声明时必须初始化,并且在其作用域内不能重新赋值。 每次迭代创建一个新的块作用域,const 变量在每个新的作用域内都是新的,因此可以在每次迭代中被重新声明和赋值。

块作用域: for 循环中的每次迭代都会创建一个新的块作用域。在每个块作用域内,const 声明的变量都是新的变量,与前一次迭代中的变量无关。

避免意外修改: 使用 const 可以确保 complement 在每次迭代中不会被意外重新赋值,从而增强代码的安全性和可读性。

20. 有效的括号 leetcode.cn/problems/va…

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 每个右括号都有一个对应的相同类型的左括号。

是一种后进先出的数据结构,非常适合用于处理括号匹配问题。具体思路是:

  • 遇到左括号时,将其压入栈中。
  • 遇到右括号时,检查栈顶元素是否为对应的左括号。如果是,则将栈顶元素弹出;否则,字符串无效。
  • 在遍历完字符串后,如果栈为空,则字符串有效;否则,字符串无效。
// 方法1:栈 + Map
var isValid = function(s) {
    const stack = [];
    const map = new Map([
        ['(', ')'],
        ['{', '}'],
        ['[', ']']
    ]);

    for (const char of s) {
        if (map.has(char)) {
            // 如果是左括号,将其对应的右括号压入栈中
            stack.push(map.get(char));
        } else {
            // 如果是右括号,检查栈顶元素是否匹配
            if (stack.pop() !== char) {
                return false;
            }
        }
    }

    // 如果栈为空,表示所有括号都匹配
    return stack.length === 0;
}

// 方法2:栈 + 对象哈希
function isValid(s) {
    const stack = [];
    const map = {
        '(': ')',
        '{': '}',
        '[': ']'
    };

    for (const char of s) {
        if (map[char]) {
            // 如果是左括号,将其对应的右括号压入栈中
            stack.push(map[char]);
        } else {
            // 如果是右括号,检查栈顶元素是否匹配
            if (stack.pop() !== char) {
                return false;
            }
        }
    }

    // 如果栈为空,表示所有括号都匹配
    return stack.length === 0;
}
// 测试函数
console.log(isValid("()")); // true
console.log(isValid("()[]{}")); // true
console.log(isValid("(]")); // false
console.log(isValid("([)]")); // false
console.log(isValid("{[]}")); // true
169. 多数元素 leetcode.cn/problems/ma…

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。

方法1: Boyer-Moore 投票算法

  1. 候选人选择
    1)初始化一个计数器 count 为 0 和一个候选人 candidate 为 null。
    2)遍历数组 nums 中的每个元素 num:
     如果 count 为 0,将 candidate 设置为当前元素 num。
     如果当前元素 num 等于 candidate,将 count 增加 1;否则,将 count 减少 1。
    3)遍历结束后,candidate 就是多数元素。
  2. 候选人验证
    在选择候选人之后,我们需要验证这个候选人是否确实是多数元素。
    在实际应用中,这一步通常是隐含的,因为题目保证了多数元素的存在。
    但如果没有这个保证,可通过再次遍历数组来确认候选人的出现次数。

 通过一个具体例子来详细解释这个算法的执行过程。
  假设输入数组是 [2, 2, 1, 1, 1, 2, 2],目标是找到多数元素。
  初始化:count = 0, candidate = null
  遍历数组:
   第一个元素是 2,因为 count = 0,所以设置 candidate = 2,count = 1
   第二个元素是 2,因为 candidate = 2,所以 count = 2
   第三个元素是 1,因为 candidate ≠ 1,所以 count = 1
   第四个元素是 1,因为 candidate ≠ 1,所以 count = 0
   第五个元素是 1,因为 count = 0,所以设置 candidate = 1,count = 1
   第六个元素是 2,因为 candidate ≠ 2,所以 count = 0
   第七个元素是 2,因为 count = 0,所以设置 candidate = 2,count = 1
   最后的候选人是 2。

function majorityElement(nums) {
    let count = 0;
    let candidate = null;

    for (const num of nums) {
        if (count === 0) {
            candidate = num;
        }
        count += (num === candidate) ? 1 : -1;
    }

    return candidate;
}

function majorityElement(nums) {
    let count = 0;
    let candidate = null;

    // 第一次遍历:选择候选人
    for (let num of nums) {
        if (count === 0) {
            candidate = num;
        }
        count += (num === candidate) ? 1 : -1;
    }

    // 第二次遍历:验证候选人(如果题目没有保证多数元素存在的话)
    count = 0;
    for (let num of nums) {
        if (num === candidate) {
            count++;
        }
    }

    if (count > Math.floor(nums.length / 2)) {
        return candidate;
    } else {
        throw new Error("No majority element found");
    }
}

方法2:哈希Map

var majorityElement = function(nums) {
    const map = new Map();
    for (const num of nums) {
        if (map.has(num)) {
            map.set(num, map.get(num) + 1);
        } else {
            map.set(num, 1);
        }
        
        // map.set(num, (map.get(num) || 0) + 1);
        // 在处理非布尔值时,|| 返回第一个真值或最后一个假值。map.get(ch) || 0 是一个逻辑或运算符表达式。
        // 它的意思是,如果 map.get(ch) 的值为 undefined(表示字符 ch 还没有出现过),则使用 0 作为默认值;否则,使用 map.get(ch) 的值。
    }
    for (const [key, value] of map) {
        if (value > nums.length / 2) {  // 在逻辑上,更严谨的做法是使用 Math.floor(nums.length / 2),以确保得到的是整数部分。
            return key;
        }
    }
    // return null; // 这是一个保险措施,理论上不会走到这里,因为题目假设数组总是存在多数元素
    // 如果没有找到,抛出错误
    throw new Error("No majority element found");
};

方法3:排序
如果一个元素是多数元素,那么它在排序后的数组中必然位于数组的中间位置。

var majorityElement = function(nums) {
    nums.sort((a, b) => a - b);
    // 返回 nums 数组中间位置的元素,使用 Math.floor() 函数向下取整确保得到的是中间位置的整数索引
    return nums[Math.floor(nums.length / 2)];  // Math.floor() 函数总是返回小于等于一个给定数字的最大整数。
};
// 测试函数
console.log(majorityElement([3, 2, 3])); // 3
console.log(majorityElement([2, 2, 1, 1, 1, 2, 2])); // 2
console.log(majorityElement([1])); // 1
448. 找到所有数组中消失的数字 leetcode.cn/problems/fi…

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

方法1:哈希Map

var findDisappearedNumbers = function(nums) {
    const map = new Map();
    nums.forEach(num => map.set(num, true));  // 使用Map记录每个数字的存在
    const res = [];
    for (let i = 1; i <= nums.length; i++) {
        if (!map.has(i)) {
            res.push(i);  // 如果数字i不在Map中,加入结果数组
        }
    }
    return res;
}

方法2:哈希Set

var findDisappearedNumbers = function(nums) {
    const set = new Set(nums);
    const res = [];
    for (let i = 1; i <= nums.length; i++) {
        if (!set.has(i)) {
            res.push(i);
        }
    }
    return res;
};

方法3:数组哈希

function findDisappearedNumbers(nums) {
    const n = nums.length;
    const present = new Array(n).fill(false);

    // 标记出现的数字
    for (const num of nums) {
        present[num - 1] = true;
    }

    // 找到所有没有出现的数字
    const result = [];
    for (let i = 0; i < n; i++) {
        if (!present[i]) {
            result.push(i + 1);
        }
    }

    return result;
}
// 测试函数
console.log(findDisappearedNumbers([4, 3, 2, 7, 8, 2, 3, 1])); // [5, 6]
console.log(findDisappearedNumbers([1, 1])); // [2]
console.log(findDisappearedNumbers([1, 2, 2, 4])); // [3]
  1. 存在重复元素 leetcode.cn/problems/co…

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。

方法1:哈希Map

var containsDuplicate = function(nums) {
    const map = new Map();
    for (const num of nums) {
        if (map.has(num)) {
            return true;
        } else {
            map.set(num, 1);
        }
    }
    return false;
};

方法2:哈希Set

var containsDuplicate = function(nums) {
    const seen = new Set();
    for (const num of nums) {
        if (seen.has(num)) {
            return true; // 如果已经存在这个数字,说明有重复,返回true
        }
        seen.add(num); // 否则,将这个数字加入Set中
    }
    return false; // 循环结束没有发现重复,返回false
}
// 测试函数
console.log(containsDuplicate([1, 2, 3, 1])); // true
console.log(containsDuplicate([1, 2, 3, 4])); // false
console.log(containsDuplicate([1, 1, 1, 3, 3, 4, 3, 2, 4, 2])); // true
383. 赎金信 leetcode.cn/problems/ra…

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。如果可以,返回 true ;否则返回 false 。magazine 中的每个字符只能在 ransomNote 中使用一次。

方法1:数组哈希

var canConstruct = function(ransomNote, magazine) {
    // 检查ransomNote的长度是否大于magazine的长度,如果是,则ransomNote无法通过magazine构造,直接返回false
    if (ransomNote.length > magazine.length) {
        return false;
    }
    
    // 创建一个长度为26的数组,用于统计magazine中每个字母的出现次数,数组下标对应26个小写字母
    const count = new Array(26).fill(0);
    
    // 遍历magazine中的每个字符,并统计出现次数
    for (const ch of magazine) {
        idx = ch.charCodeAt() - 'a'.charCodeAt();  // 根据字符的ASCII码减去'a'的ASCII码,得到字符在数组中的索引
        count[idx]++; // 增加该索引处的计数
    }
    
    // 遍历ransomNote中的每个字符,并检查是否可以从magazine中构造
    for (const ch of ransomNote) {
        idx = ch.charCodeAt() - 'a'.charCodeAt();  // 根据字符的ASCII码减去'a'的ASCII码,得到字符在数组中的索引
        count[idx]--; // 减少该索引处的计数
        
        // 使用字符'ch'之后的判断
        // 如果减少计数后小于0,说明magazine中不存在足够的字符以构造ransomNote,返回false
        if (count[idx] < 0) {
            return false;
        }
    }
    
    // 如果ransomNote可以从magazine中构造,则返回true
    return true;
};

当我们将字符的ASCII码减去'a'的ASCII码时,我们实际上是将字符转换为其在字母表中的相对位置。 例如,如果我们将字符'a'的ASCII码(97)减去字符'a'的ASCII码(97),结果为0;这意味着字符'a'在字母表中的位置是0。 类似地,字符'b'减去字符'a'的结果为1,字符'c'减去字符'a'的结果为2,以此类推,直到字符'z'减去字符'a'的结果为25。

由于我们的目的是统计每个字母在magazine字符串中出现的次数,我们可以使用这个相对位置作为数组的索引。 这样一来,数组的索引0代表字母'a',索引1代表字母'b',以此类推,直到索引25代表字母'z'。 因此,我们可以将magazine字符串中的每个字符映射到对应的数组位置,并增加该位置处的计数。这样做的好处是,我们可以使用一个固定长度的数组来统计26个小写字母的出现次数,而不需要使用对象或Map来保存计数,这样可以提高代码的效率和简洁性。

方法2:对象哈希

function canConstruct(ransomNote, magazine) {
    // 创建一个对象来存储 magazine 中每个字符的计数
    const charCount = {};

    // 遍历 magazine ,统计每个字符的出现次数
    for (const ch of magazine) {
        if (charCount[ch]) {
            charCount[ch]++;
        } else {
            charCount[ch] = 1;
        }
    }

    // 遍历 ransomNote ,检查每个字符是否可以在 magazine 中找到
    for (const ch of ransomNote) {
        // 使用字符'ch'之前的判断
        if (!charCount[ch] || charCount[ch] === 0) {
            return false; // 如果字符不存在或已用完,返回 false
        }
        charCount[ch]--; // 使用一个字符
    }

    return true; // 所有字符都能找到,返回 true
}
// 测试函数
console.log(canConstruct("a", "b")); // false
console.log(canConstruct("aa", "ab")); // false
console.log(canConstruct("aa", "aab")); // true
389. 找不同 leetcode.cn/problems/fi…

给定两个字符串 s 和 t ,它们只包含小写字母。字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。请找出在 t 中被添加的字母。

方法1:数组哈希

var findTheDifference = function(s, t) {
    if (s.length === t.length) {
        return false;
    }
    
    const cnt = new Array(26).fill(0);
    const base = 'a'.charCodeAt(0);

    // 先遍历字母数比较少的
    for (const ch of s) {
        cnt[ch.charCodeAt(0) - base]++;
    }

    // 再遍历字母数比较多的
    for (const ch of t) {
        cnt[ch.charCodeAt(0) - base]--;
        // 找到计数值为 -1 的索引,该索引对应的字母就是多余的字母。
        if (cnt[ch.charCodeAt(0) - base] < 0) {  // 或cnt[ch.charCodeAt() - base] === -1
            return ch;
        }
    }

    return ' ';
};

function findTheDifference(s, t) {
    const count = new Array(26).fill(0);
    const base = 'a'.charCodeAt(0);

    // 遍历字母数多的字符串 t,增加每个字母对应的计数。
    for (const char of t) {
        count[char.charCodeAt(0) - base]++;
    }

    // 遍历字母数少的字符串 s,减少每个字母对应的计数。
    for (const char of s) {
        count[char.charCodeAt(0) - base]--;
    }

    // 遍历计数数组,找到计数值为 1 的索引,该索引对应的字母就是多余的字母。
    for (let i = 0; i < 26; i++) {
        if (count[i] === 1) {
            return String.fromCharCode(i + base);
        }
    }

    return '';
}

方法2:异或运算,时间复杂度为 O(n)
利用异或运算的自反性(a ^ a = 0)来消除成对出现的字符,剩下的结果就是多出的那个字符。 异或运算(XOR)具有以下几个重要性质,这些性质使得异或运算非常适合用来处理找出不同元素的问题。

  • 交换律:a ^ b = b ^ a
  • 结合律:a ^ (b ^ c) = (a ^ b) ^ c
  • 自反性:a ^ a = 0
  • 与零的运算:a ^ 0 = a
    例子解释
     假设 s = "abcd" 和 t = "abcde"。
     1) 初始化 charCode = 0。
     2) 遍历 s:
      charCode ^= 'a'.charCodeAt(0) -> charCode = 97
      charCode ^= 'b'.charCodeAt(0) -> charCode = 97 ^ 98
      charCode ^= 'c'.charCodeAt(0) -> charCode = (97 ^ 98) ^ 99
      charCode ^= 'd'.charCodeAt(0) -> charCode = ((97 ^ 98) ^ 99) ^ 100
     3) 遍历 t:
      charCode ^= 'a'.charCodeAt(0) -> charCode = (((97 ^ 98) ^ 99) ^ 100) ^ 97
      charCode ^= 'b'.charCodeAt(0) -> charCode = ((((97 ^ 98) ^ 99) ^ 100) ^ 97) ^ 98
      charCode ^= 'c'.charCodeAt(0) -> charCode = (((((97 ^ 98) ^ 99) ^ 100) ^ 97) ^ 98) ^ 99
      charCode ^= 'd'.charCodeAt(0) -> charCode = ((((((97 ^ 98) ^ 99) ^ 100) ^ 97) ^ 98) ^ 99) ^ 100
      charCode ^= 'e'.charCodeAt(0) -> charCode = (((((((97 ^ 98) ^ 99) ^ 100) ^ 97) ^ 98) ^ 99) ^ 100) ^ 101
      通过异或运算,相同的字符会被抵消(因为 a ^ a = 0,所以 97 ^ 97 = 0),最终只会剩下多出的字符 e 的 ASCII 码。
     4) 最终 charCode 的值是 101,对应的字符是 e,所以返回 e。
function findTheDifference(s, t) {
    let charCode = 0;

    for (const ch of s) {
        charCode ^= ch.charCodeAt(0);
    }

    for (const ch of t) {
        charCode ^= ch.charCodeAt(0);
    }

    return String.fromCharCode(charCode);
}
// 测试函数
console.log(findTheDifference("abcd", "abcde")); // "e"
console.log(findTheDifference("a", "aa")); // "a"
console.log(findTheDifference("", "y")); // "y"
console.log(findTheDifference("ae", "aea")); // "a"
409. 最长回文串 leetcode.cn/problems/lo…

给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的 回文串的长度。在构造过程中,请注意 区分大小写 。比如 "Aa" 不能当做一个回文字符串。

贪心算法
 1) 使用一个哈希表(对象)来记录每个字符的出现次数。
 2) 遍历哈希表,计算能够添加到回文串中的字符数:
  如果字符出现偶数次,则可以全部添加到回文串中。
  如果字符出现奇数次,则可以添加 count - 1 个字符到回文串中,并且可以使用一个字符作为回文串的中心字符。
 3) 最后,如果有中心字符,将其长度加一。

方法1:哈希Map

var longestPalindrome = function(s) {
    const map = new Map();

    // 统计各字符数量
    for (let i = 0; i < s.length; i++) {
        // charAt(index): 返回指定索引处的字符。它返回的是一个字符串,表示指定位置的字符。
        ch = s.charAt(i);
        if (map.has(ch)) {
            map.set(ch, map.get(ch) + 1);
        } else {
            map.set(ch, 1);
        }
    }

    // for (const ch of s) {
    //     if (map.has(ch)) {
    //         map.set(ch, map.get(ch) + 1);
    //     } else {
    //         map.set(ch, 1);
    //     }
    // }

    // 统计构造回文串的最大长度
    let even = 0, odd = 0;
    for (const [ch, count] of map) {
        // 将当前字符出现次数向下取偶数,并计入 res
        even += count - count % 2; // 如果是奇数,减去1;
        // 若当前字符出现次数为奇数,则将 odd 置 1
        if (count % 2 === 1) odd = 1;
    }
    return even + odd;
};

方法2:对象哈希

function longestPalindrome(s) {
    const charCount = {};
    
    // 统计每个字符的出现次数
    for (const ch of s) {
        if (charCount[ch]) {
            charCount[ch]++;
        } else {
            charCount[ch] = 1;
        }
    }
    
    let length = 0;
    let hasOdd = false;
    
    // 遍历字符计数
    for (const count of Object.values(charCount)) {
        if (count % 2 === 0) {
            length += count;
        } else {
            length += count - 1;
            hasOdd = true;
        }
    }
    
    // 如果有奇数次出现的字符,可以放一个在中心位置
    if (hasOdd) {
        length++;
    }
    
    return length;
}
// 测试函数
console.log(longestPalindrome("abccccdd")); // 7
console.log(longestPalindrome("a")); // 1
console.log(longestPalindrome("Aa")); // 1
console.log(longestPalindrome("AaBbCcDd")); // 1
console.log(longestPalindrome("AaBbCcDdD")); // 3
268. 丢失的数字 leetcode.cn/problems/mi…

给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

方法1:排序
时间复杂度:O(nlog⁡n);空间复杂度:O(log⁡n)。

var missingNumber = function(nums) {
    nums.sort((a, b) => a - b);
    const n = nums.length;
    for (let i = 0; i < n; i++) {
        if (nums[i] !== i) {
            return i;
        }
    }
    return n;
};

方法2:数学-高斯求和公式
时间复杂度:O(n);空间复杂度:O(1)。
0到n的总和 = n(n+1)/2 ,求出这个总和,然后减去数组中所有数的和,差值就是缺失的那个数。

var missingNumber = function(nums) {
    const n = nums.length;
    const total = n * (n + 1) / 2
    let arrSum = 0;
    for (let i = 0; i < n; i++) {
        arrSum += nums[i];
    }
    // const arraySum = nums.reduce((acc, num) => acc + num, 0);
    return total - arrSum;
};

方法3:Set
时间复杂度:O(n);空间复杂度:O(n)。

var missingNumber = function(nums) {
    const n = nums.length;
    const set = new Set(nums);
    for (let i = 0; i <= n; i++) {
        if (!set.has(i)) {
            return i;
        }
    }
};
// 测试
const nums = [3, 0, 1];
console.log(missingNumber(nums));  // 输出 2
575. 分糖果 leetcode.cn/problems/di…

Alice 有 n 枚糖,其中第 i 枚糖的类型为 candyType[i] 。Alice 注意到她的体重正在增长,所以前去拜访了一位医生。 医生建议 Alice 要少摄入糖分,只吃掉她所有糖的 n / 2 即可(n 是一个偶数)。Alice 非常喜欢这些糖,她想要在遵循医生建议的情况下,尽可能吃到最多不同种类的糖。 给你一个长度为 n 的整数数组 candyType ,返回: Alice 在仅吃掉 n / 2 枚糖的情况下,可以吃到糖的 最多 种类数。

var distributeCandies = function(candyType) {
    const set = new Set(candyType);  // 用 Set 存储不同种类的糖
    const n = candyType.length;
    if (set.size <= n / 2) {
        return set.size;
    }
    return n / 2;  // Alice 可以吃的最多糖数
};
771. 宝石与石头 leetcode.cn/problems/je…

给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。 stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。 字母区分大小写,因此 "a" 和 "A" 是不同类型的石头。

方法1:哈希Map

var numJewelsInStones = function(jewels, stones) {
    const map = new Map();
    for (const s of stones) {
        if (map.has(s)) {
            map.set(s, counter.get(s) + 1);
        } else {
            map.set(s, 1);
        }
    }

    let count = 0;
    for (const j of jewels) {
        if (map.has(j)) {
            count += map.get(j);
        }
    }
    return count;
};

方法2:哈希Set

function numJewelsInStones(jewels, stones) {
    const jewelSet = new Set(jewels);  // 使用 Set 存储宝石类型
    let count = 0;
    
    for (const stone of stones) {
        if (jewelSet.has(stone)) {
            count++;
        }
    }
    
    return count;
}
// 测试
const jewels = "aA";
const stones = "aAAbbbb";
console.log(numJewelsInStones(jewels, stones));  // 输出 3
349. 两个数组的交集 leetcode.cn/problems/in…

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

方法1:哈希Set

function intersection(nums1, nums2) {
    // 将两个数组转换为集合
    const set1 = new Set(nums1);  // set1 = {1, 2}  // set1 = {4, 9, 5}
    const set2 = new Set(nums2);  // set2 = {2}     // set2 = {9, 4, 8}

    // 使用过滤器来找到交集
    const result = [...set1].filter(item => set2.has(item));

    return result;
}

方法2:哈希Map

function intersection(nums1, nums2) {
    // 创建两个 Map 来存储每个数组中的元素及其计数
    const map1 = new Map();
    const map2 = new Map();

    // 遍历 nums1,将每个元素存储在 map1 中
    for (const num of nums1) {
        if (!map1.has(num)) {
            map1.set(num, 1);
        }
    }  // map1 = {['1', 1], ['2', 1]}   // map1 = {['4', 1], ['9', 1], ['5', 1]}

    // 遍历 nums2,将每个元素存储在 map2 中
    for (const num of nums2) {
        if (!map2.has(num)) {
            map2.set(num, 1);
        }
    }  // map2 = {['2', 1]}             // map2 = {['9', 1], ['4', 1], ['8', 1]}

    // 找到 map1 和 map2 的交集
    const result = [];
    // [] 用于解构赋值(destructuring assignment)。解构赋值允许我们从数组或对象中提取值,并将其赋值给变量。
    for (const [key] of map1) {  // const key of map1.keys()
        if (map2.has(key)) {
            result.push(key);
        }
    }
    return result;

    const res = new Set();
    for (const num of nums2) {
        if (map1.has(num)) {
            res.add(num);
        }
    }
    return [...res];
}
// 测试函数
console.log(intersection([1, 2, 2, 1], [2, 2])); // [2]
console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])); // [4, 9]
350. 两个数组的交集 II leetcode.cn/problems/in…

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

function intersect(nums1, nums2) {
    const map = new Map();
    const result = [];

    // 记录 nums1 中每个元素的出现次数
    for (const num of nums1) {
        if (map.has(num)) {
            map.set(num, map.get(num) + 1);
        } else {
            map.set(num, 1);
        }
    }

    // 遍历 nums2,找到交集元素并考虑出现次数
    for (const num of nums2) {
        if (map.has(num) && map.get(num) > 0) {
            result.push(num);
            map.set(num, map.get(num) - 1);
        }
    }

    return result;
}
// 测试
const nums1 = [4, 9, 5];
const nums2 = [9, 4, 9, 8, 4];
console.log(intersect(nums1, nums2));  // 输出 [4, 9]

补充知识

使用Map对象
const myMap = new Map();

const keyString = "a string";
const keyObj = {};
const keyFunc = function () {};

// 添加键
myMap.set(keyString, "和键'a string'关联的值");
myMap.set(keyObj, "和键 keyObj 关联的值");
myMap.set(keyFunc, "和键 keyFunc 关联的值");

console.log(myMap.size); // 3

// 读取值
console.log(myMap.get(keyString)); // "和键'a string'关联的值"
console.log(myMap.get(keyObj)); // "和键 keyObj 关联的值"
console.log(myMap.get(keyFunc)); // "和键 keyFunc 关联的值"

console.log(myMap.get("a string")); // "和键'a string'关联的值",因为 keyString === 'a string'
console.log(myMap.get({})); // undefined,因为 keyObj !== {}
console.log(myMap.get(function () {})); // undefined,因为 keyFunc !== function () {}
使用 for...of 迭代 Map
// Map 可以使用 for...of 循环来实现迭代:
const myMap1 = new Map();
myMap1.set(0, "zero");
myMap1.set(1, "one");

for (const [key, value] of myMap1) {
  console.log(`${key} = ${value}`);
}
// 0 = zero
// 1 = one

for (const key of myMap1.keys()) {
  console.log(key);
}
// 0
// 1

for (const value of myMap1.values()) {
  console.log(value);
}
// zero
// one

for (const [key, value] of myMap1.entries()) {
  console.log(`${key} = ${value}`);
}
// 0 = zero
// 1 = one
使用 forEach() 迭代 Map
// Map 也可以通过 forEach() 方法迭代:
myMap.forEach((value, key) => {
  console.log(`${key} = ${value}`);
});
// 0 = zero
// 1 = one
Map 与数组对象的关系
const kvArray = [
  ["key1", "value1"],
  ["key2", "value2"],
];

// 使用常规的 Map 构造函数可以将一个二维的键值对数组转换成一个 Map 对象
const myMap2 = new Map(kvArray);

console.log(myMap2.get("key1")); // "value1"

// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维的键值对数组
console.log(Array.from(myMap2)); // 输出和 kvArray 相同的数组

// 更简洁的方法来做如上同样的事情,使用展开运算符
console.log([...myMap2]);

// 或者在键或者值的迭代器上使用 Array.from,进而得到只含有键或者值的数组
console.log(Array.from(myMap2.keys())); // 输出 ["key1", "key2"]
复制或合并 Map
// Map 能像数组一样被复制:
const original = new Map([[1, "one"]]);

const clone = new Map(original);

console.log(clone.get(1)); // one
console.log(original === clone); // false. 浅比较 不为同一个对象的引用
使用Set对象
const mySet1 = new Set();

mySet1.add(1); // Set(1) { 1 }
mySet1.add(5); // Set(2) { 1, 5 }
mySet1.add(5); // Set(2) { 1, 5 }
mySet1.add("some text"); // Set(3) { 1, 5, 'some text' }
const o = { a: 1, b: 2 };
mySet1.add(o);

mySet1.add({ a: 1, b: 2 }); // o 是不同对象的引用,所以这是可以的

mySet1.has(1); // true
mySet1.has(3); // false,因为并未将 3 添加到集合中
mySet1.has(5); // true
mySet1.has(Math.sqrt(25)); // true
mySet1.has("Some Text".toLowerCase()); // true
mySet1.has(o); // true

mySet1.size; // 5

mySet1.delete(5); // 从集合中移除 5
mySet1.has(5); // false,5 已从集合中移除

mySet1.size; // 4,因为我们刚刚移除了一个值

mySet1.add(5); // Set(5) { 1, 'some text', {...}, {...}, 5 }——先前删除的元素会作为新的元素被添加,不会保留删除前的原始位置

console.log(mySet1); // Set(5) { 1, "some text", {…}, {…}, 5 }
迭代Set
for (const item of mySet1) {
    console.log(item);
}
// 1、"some text"、{ "a": 1, "b": 2 }、{ "a": 1, "b": 2 }、5

for (const item of mySet1.keys()) {
    console.log(item);
}
// 1、"some text"、{ "a": 1, "b": 2 }、{ "a": 1, "b": 2 }、5

for (const item of mySet1.values()) {
    console.log(item);
}
// 1、"some text"、{ "a": 1, "b": 2 }、{ "a": 1, "b": 2 }、5

// 键和值是相同的
for (const [key, value] of mySet1.entries()) {
    console.log(key);
}
// 1、"some text"、{ "a": 1, "b": 2 }、{ "a": 1, "b": 2 }、5

// 使用 Array.from 将 Set 对象转换为数组对象
const myArr = Array.from(mySet1); // [1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}, 5]

// 在 Set 和 Array 之间转换
const mySet2 = new Set([1, 2, 3, 4]);
console.log(mySet2.size); // 4
console.log([...mySet2]); // [1, 2, 3, 4]

// 可以通过如下代码模拟求交集
const intersection = new Set([...mySet1].filter((x) => mySet2.has(x)));

// 可以通过如下代码模拟求差集
const difference = new Set([...mySet1].filter((x) => !mySet2.has(x)));

// 使用 forEach() 迭代集合中的条目
mySet2.forEach((value) => {
    console.log(value);
});
// 1  // 2  // 3  // 4