题目:给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。

示例 1:

// 输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
// 输出: 16 
// 解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。

示例 2:

// 输入: words = ["a","ab","abc","d","cd","bcd","abcd"]
// 输出: 4 
// 解释: 这两个单词为 "ab", "cd"。

示例 3:

// 输入: words = ["a","aa","aaa","aaaa"]
// 输出: 0 
// 解释: 不存在这样的两个单词。

提示:

  • 2 <= words.length <= 1000
  • 1 <= words[i].length <= 1000
  • words[i] 仅包含小写字母

注意:本题与LeetCode 318 题相同。

思路分析

本题需要注意的有两点,即单词长度达到最大,每个单词之间不能出现重复的字母。如果使用暴力循环来解答的话,还是不复杂的。如下:

/**
 * @param {string[]} words
 * @return {number}
 */
var maxProduct = function(words) {
    const l = words.length;
    let res = 0;
    for(let i = 0;i < l;i++){
        for(let j = i + 1;j < l;j++){
            // 判断两个单词如果没有出现重复字符,则求最大的单词乘积
            if(!hasSameLetter(words[i],words[j])){
                res = Math.max(words[i].length * words[j].length,res);
            }
        }
    }
};
var hasSameLetter = function(wordA,wordB){
    for(const letter of wordA){
        if(wordB.indexOf(letter) !== -1){
            return true;
        }
    }
    return false;
}

以上算法的时间复杂度和空间复杂度分析如下:

  • 时间复杂度:O(n ^ 2 * m ^ 2),其中 n 是数组的长度,m为单词的长度。
  • 空间复杂度:O(1),只用了一个辅助变量。

暴力解法,我们可以看到时间复杂度还是很高的,那么我们来考虑一下如何优化,首先需要优化的是hasSameLetter方法,通常这种情况,我们就可以考虑位运算来优化。由于英文字母有26个,因此我们可以使用一个26位的二进制数来代表每一个字母,例如从右到左可以是a ~ z。如下所示:

// z...cba。

也就是说,当某个位上出现了该字母,那么就是1,没有出现就是0,比如abcd,其中a出现了1次,那么对应的第一位上就应该是数字1。根据按位与(&)运算的性质,我们不难得出当两个单词的二进制表示执行按位与的结果,如果等于0的话,那么这两个单词是一定不重复的。因为按位与的结果就是只有当2个操作数的每一个位上的数字都是1的时候,执行结果才是1。现在假设我们"a"的位已知,我们求其它字母的位,我们是可以使用其它字母的ascii - "a"的ascii码得出来的,但是这里我们也可以使用左移操作符来得出来。我们来看如果单词是a,表示如下:

// 0 0 0 ... 1 a的表示
// =>
// b的表示
// 0 0 0 ... 1 0 => 相当于将a左移1位,然后再求按位或。即等价b = b | 1 << a
// 简写为b |= 1 << a

因此,我们可以分别用2个变量来统计出两个单词之间每一个字母的位,然后再求出按位与即可。因此以上的代码我们就可以修改如下:

/**
 * @param {string[]} words
 * @return {number}
 */
var maxProduct = function(words) {
    const l = words.length;
    let res = 0;
    for(let i = 0;i < l;i++){
        for(let j = i + 1;j < l;j++){
            // 判断两个单词如果没有出现重复字符,则求最大的单词乘积
            if(!hasSameLetter(words[i],words[j])){
                res = Math.max(words[i].length * words[j].length,res);
            }
        }
    }
};
var hasSameLetter = function(wordA,wordB){
    let x = 0,y = 0;
    for(const letter of wordA){
        // 字符串的charCodeAt方法可以得到字母的ascii码值
        x |= 1 << (letter.charCodeAt() - "a".charCodeAt());
    }
    for(const letter of wordB){
        // 字符串的charCodeAt方法可以得到字母的ascii码值
        y |= 1 << (letter.charCodeAt() - "a".charCodeAt());
    }
    // 按位操作符优先级低因此需要加括号
    return (x & y) !== 0;
}

接下来,我们来优化嵌套循环里的代码。我们可以采用哈希表法,用一个哈希表来存储每一个单词的长度,最后我们遍历这个哈希表,求出最大长度即可。在这里,我们可以使用JavaScript的Map数据结构来存储。

/**
 * @param {string[]} words
 * @return {number}
 */
var maxProduct = function(words) {
    let map = new Map(),
        n = words.length,
        res = 0;
    const asciiA = "a".charCodeAt();
    for(let i = 0;i < n;i++){
        let temp = 0;
        for(const c of words[i]){
            temp |= 1 << (c.charCodeAt() - asciiA);
        }
        // 根据temp来设置单词的长度
        if(map.has(temp)){
            // 设置单词长度的最大值
            map.set(temp,Math.max(words[i].length,map.get(temp)));
        }else{
            map.set(temp,words[i].length);
        }
    }
    for(const keyA of map.keys()){
        for(const keyB of map.keys()){
            // 按位与操作为0,则代表单词字符不重复
             if((keyA & keyB) === 0){
                 res = Math.max(res,map.get(keyA) * map.get(keyB));
             }
        }
    }
    return res;
};

以上算法的时间复杂度和空间复杂度分析如下:

  • 时间复杂度:O((m + n) * n),其中 m 是查找单词中字母的次数,n是遍历哈希表的次数。
  • 空间复杂度:O(n)。哈希映射中包含最多 n 个元素,即需要的空间为 O(n)。
Apple

Apple

Graduated in Computer Science and Engineering, but currently working with GNU/Linux infrastructure and in the spare time I'm an Open Source programmer (Python and C), a drawer and author in the YINGJUE Blog.


Comments