leetcode刷题(javaScript)——字典哈希表相关场景题总结

在 JavaScript 刷题中,字典(Dictionary)和哈希表(Hash Table)通常用来存储键值对,提供快速的查找、插入和删除操作。它们在很多算法题目中都有广泛的应用,特别是在需要快速查找元素或统计元素出现次数的情况下。

 字典和哈希表出现的场景

以下是一些常见的场景,可以使用字典和哈希表来解决:

  1. 两数之和(Two Sum):给定一个数组和一个目标值,在数组中找到两个数使它们的和等于目标值。
  2. 无重复字符的最长子串(Longest Substring Without Repeating Characters):找到一个字符串中最长的子串,其中没有重复的字符。
  3. 字母异位词分组(Group Anagrams):将给定的字符串数组按照字母异位词分组。
  4. 单词规律(Word Pattern):判断给定的模式字符串是否与给定的单词字符串匹配。

对于新手刷题,以下是一些建议:

  1. 熟悉常见的数据结构和算法:掌握常见的数据结构(如数组、链表、栈、队列、树、图等)和算法(如排序、搜索、动态规划等)是刷题的基础。
  2. 多练习:刷题是一个持续练习的过程,通过不断练习可以提高解题能力和编程技巧。
  3. 理解题目要求:在解题之前,仔细阅读题目要求,确保理解清楚题目的意思和要求。
  4. 注重细节:在编写代码时,注意边界条件、特殊情况和错误处理,确保代码的正确性和健壮性。
  5. 学会利用工具:熟练使用调试工具、在线编程平台和相关资源,可以帮助更快地解决问题和提高效率。

 在js中如何创建哈希表或字典数据结构

在 JavaScript 中,可以使用对象(Object)或 Map 类型来创建哈希表或字典数据结构。下面分别介绍如何使用对象和 Map 类型来实现哈希表或字典:

 使用对象(Object)

在 JavaScript 中,对象可以被用作哈希表或字典数据结构,其中对象的属性(key)可以是字符串或符号,值可以是任意类型。可以通过对象字面量或构造函数来创建对象,然后通过设置属性来存储键值对。

以下是一个简单示例:

// 创建一个空对象作为哈希表
let hashTable = {};

// 存储键值对
hashTable['key1'] = 'value1';
hashTable['key2'] = 'value2';

// 访问值
console.log(hashTable['key1']); // 输出 'value1'

使用 Map 类型

ES6 引入了 Map 类型,它提供了一种更灵活和强大的方式来创建哈希表或字典数据结构。

Map 类型可以存储任意类型的键和值map对象会维护键值对的插入顺序。

而对象的键只能是字符串或符号,且对象中的key不会根据写入顺序遍历。

可以使用 new Map() 来创建一个空的 Map 对象,然后使用 set() 方法来存储键值对。

 以下是一个简单示例:

// 创建一个空的 Map 对象
let hashMap = new Map();

// 存储键值对
hashMap.set('key1', 'value1');
hashMap.set('key2', 'value2');

// 访问值
console.log(hashMap.get('key1')); // 输出 'value1'

使用对象还是Map?

使用对象或 Map 类型来创建哈希表或字典取决于具体的需求和场景。一般来说,如果需要存储简单的键值对且键是字符串类型,可以使用对象;如果需要更灵活的键类型或需要保持键值对的插入顺序,可以使用 Map 类型

 Map对象的常用方法

关于字典或哈希表专项练习中,你必须要掌握以下对Map对象的操作

  1. set(key, value):向 Map 对象中添加一个键值对,如果键已经存在,则更新其对应的值。

  2. get(key):获取指定键对应的值,如果键不存在,则返回 undefined。

  3. has(key):判断 Map 对象中是否包含指定的键,返回一个布尔值。

  4. delete(key):删除 Map 对象中指定键的键值对,返回一个布尔值表示是否删除成功。

  5. clear():清空 Map 对象,删除所有的键值对。

  6. size:获取 Map 对象中键值对的数量。

  7. keys():返回一个包含 Map 对象中所有键的迭代器。

  8. values():返回一个包含 Map 对象中所有值的迭代器。

  9. entries():返回一个包含 Map 对象中所有键值对的迭代器。

数组题型

 1. 两数之和

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

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

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

 思路:这道题,作为leetcode的第一题,已经不是一个简单题了,劝退了很多刷算法的,其实就想做数学题一样,了解解题思想,便可知道怎么下手了。我们看到这道题就想用两个for循环去遍历,直到找到两数和等于target。这种方法时间复杂度高,但是没有空间复杂度。

如何一次遍历就能找到两数之和呢,可以考虑空间换时间。

将前面已经访问的数字和对应的下标存起来,判断后面的元素时,去已经存储的数据中去匹配即可。按照这种思想,看下题解怎么做的吧

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function (nums, target) {
    let map = new Map();
    for (let i = 0; i < nums.length; i++) {
        let num = target - nums[i];
        if (map.has(num)) {
            return [map.get(num), i];
        } else {
            map.set(nums[i], i);
        }
    }
};

通常使用Map的以下几个方法:

set:向哈希表或字典加元素,接收两个参数key:value

get:从哈希表中获取指定key的value值,接收key值,返回value值

has:判断哈希表中是否有key值,接收key值,返回true或false

 217. 存在重复元素

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

 思路:有了上面题的引入,这道题就很简单了。还是将数组存储在字典中,如果比较的元素已经存在字典里,则return true。否则将当前元素放入字典中,这里的set的第二个参数value是没有用的,放什么都行。

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var containsDuplicate = function (nums) {
    let map = new Map();
    for (let i = 0; i < nums.length; i++) {
        if (map.has(nums[i])) {
            return true;
        } else {
            map.set(nums[i], i);
        }
    }
    return false;
};

349. 两个数组的交集

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

 思路:如果不使用ES6的set去重的话。

可以将两个数组中其中一个比如nums1做字典,遍历nums2,返回一个重复的数组。注意nums2也有可能有数据重复

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    let map = new Map();
    let arr = [];
    let first = true;
    nums1.forEach(item => {
        map.set(item, first);
    })
    nums2.forEach(item => {
        if (map.has(item) && map.get(item) === first) {
            arr.push(item);
            map.set(item, !first)
        }
    })
    return arr;

};

 设置用first表示当前元素是首次添加还是非首次添加的

进一步可以用set结合数组的filter过滤方法实现

优化后的代码

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    let arr1 = new Set(nums1);
    let arr2 = new Set(nums2);
    return Array.from(arr2).filter(item => arr1.has(item))
};

如果对数组的方法和ES的新提供的对象熟悉的话就很简单

通过Set方法将nums1和nums2去重。使用Array.from方法将set对象转换为数组对象,这样就可以使用数组自带的过滤方法。是不是很神奇!

 1207. 独一无二的出现次数

给你一个整数数组 arr,请你帮忙统计数组中每个数的出现次数。

如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false

 思路:这道题也不算难,还是用到字典将数字和它出现的次数做存储。关键是如何对这个map字典判重处理。这里给出的代码充分利用了Set对象和Map对象,Set存储的键值具有唯一性可以去重。同时Map和Set对象都提供了size方法判断元素个数。

/**
 * @param {number[]} arr
 * @return {boolean}
 */
var uniqueOccurrences = function (arr) {
    let map = new Map();
    //获取元素出现的次数
    arr.forEach(item => {
        if (map.has(item)) {
            map.set(item, map.get(item) + 1);
        } else {
            map.set(item, 1);
        }
    })
    return map.size === new Set(map.values()).size;
};

 进一步优化,简化判断

/**
 * @param {number[]} arr
 * @return {boolean}
 */
var uniqueOccurrences = function (arr) {
    let map = new Map();
    //获取元素出现的次数
    arr.forEach(item => {
        map.set(item, (map.get(item) || 0) + 1)
    })
    return map.size === new Set(map.values()).size;

 只要将字典map和对字典values去重后的Set对象比较长度即可,如果有重复的Set的size会小与map.size。这道题很精彩

当然可以用Object存储字典

var uniqueOccurrences = function(arr) {
  let count = {};
  for (let item of arr) {
    count[item] = (count[item] || 0) + 1;
  }
  return Object.keys(count).length === new Set(Object.values(count)).size;
};

 219. 存在重复元素 II

给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j]abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false

 思路:跟之前的题比这题应该算简单了。题目主要意思是找最近两个相等元素,看两者的步长是否小与给定值。如果有字典存储key值的话,每次都要更新或新增key为nums[i]的value值。保证重复的value也是最近一次保存的index信息

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {boolean}
 */
var containsNearbyDuplicate = function (nums, k) {
    let map = new Map();
    for (let i = 0; i < nums.length; i++) {
        if (map.has(nums[i]) && (i - map.get(nums[i])) <= k) {
            return true;
        }
        map.set(nums[i], i);
    }
    return false;
};

用i-map.get(nums[i])判断相邻重复元素的差距

49. 字母异位词分组

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

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

思路:考虑用map存储按照sort排序后的key值,val值可以考虑存出现的字母异位数组。但是后面还多了一个遍历map拼接val的过程。这里有个技巧,就是哈希表+数组,在哈希表存储key值,真正的val存在数组中,而数组下标存在val里,这样数组就是最终返回的结果,而过程只是多了一个key到index的映射。

/**
 * @param {string[]} strs
 * @return {string[][]}
 */
var groupAnagrams = function (strs) {
    let arr = [];//用arr存放最终结果
    let map = new Map();//存放唯一str,val是在arr的index
    for (let i = 0; i < strs.length; i++) {
        let str = strs[i].split('').sort().join('');//根据unicode编码排序后返回str
        if (map.has(str)) {
            arr[map.get(str)].push(strs[i]);
        } else {
            arr.push([strs[i]])
            map.set(str, arr.length - 1);
        }
    }
    return arr;
};

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

思路:借助set实现。先将数组利用set去重。之后遍历数组,尝试找序列的起始值。怎么找起始值,如果当前值-1没有,说明以当前值作为序列的起始值没有计算过序列长度。以它为起始值,在set找后续的序列。否则,当前值-1存在,说明当前值参与过序列计算了。不用处理。

每次生成新的序列需要重新计算最大count值

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function (nums) {
    let set = new Set(nums);//构建一个去重set
    let count = 0;
    let maxCount = 0;
    for (let i = 0; i < nums.length; i++) {//遍历一次数组
        //在set中找连续值
        if (!set.has(nums[i] - 1)) {//当前不是某个序列的终点,作为新序列的起点,看是否向后找到更长的连续值
            count = 1;
            let find = nums[i] + 1;
            while (set.has(find)) {//找当前值+1的连续序列
                count++;
                find++;
            }
            maxCount = Math.max(count, maxCount);//取过程中的较大值
        }
    }
    return maxCount;
};

347. 前 K 个高频元素 

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

 思路:可以使用哈希表来统计每个元素出现的频率,并使用桶排序的思想来找出频率前 k 高的元素。首先利用哈希表统计每个元素的频率,然后根据频率构建桶,最后从高频率向低频率遍历桶,找出前 k 高的元素。这个算法的时间复杂度为 O(n)

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function (nums, k) {
    let map = new Map();
    let bucket = [];
    //循环一遍统计出现的频率
    for (let i = 0; i < nums.length; i++) {
        map.set(nums[i], (map.get(nums[i]) || 0) + 1);
    }
    //循环map,初始化桶数据
    for (let [key, value] of map) {
        if (!bucket[value]) {//用出现的频率作为桶的索引地址,相同频率的key放一块
            bucket[value] = [key];
        } else {
            bucket[value].push(key);//key肯定是唯一的
        }
    }
    let result = [];
    for (let i = bucket.length - 1; i > 0 && result.length < k; i--) {
        if(bucket[i]){//从后往前遍历桶,索引会有空的
            result.push(...bucket[i]);
        }
    }
    return result;
};

字符串题型

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串的长度。

 思路:这个题目也是比较经典的笔试题,如何获取无重复的子串呢,涉及到两个指针移动,这里左指针的移动就比较有意思了。首先左指针会移动说明中间出现了重复的,在左指针移动前要将当前的左右指针夹住的长度记一下。

那有重复的左指针怎么移动呢,比如示例1,当右指针到第二个a,左指针指向a的下标是0,左指针往后移动一个。从b开始然后右指针可以继续移动了。所以左指针移动是在左右指针闭合区间内,移动到重复元素的后一个位置上。

那右指针呢,其实不需要单独设置右指针,因为当前循环的index就是右指针啊。这样,一次遍历就能计算所有不重复的子串了。

而重复元素的判断就可以借用字典的思想了。

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function (s) {
    let arr = s.split('');
    let start = 0;
    let map = new Map();
    let maxLength = 0;

    for (let i = 0; i < arr.length; i++) {
        if (map.has(arr[i]) && map.get(arr[i]) >= start) {
            let currentLength = i - start;
            maxLength = Math.max(maxLength, currentLength);
            start = map.get(arr[i]) + 1;
        }
        map.set(arr[i], i);
    }

    // 计算最后一次比较没有重复时的长度
    let currentLength = arr.length - start;
    return Math.max(currentLength, maxLength);
};

这里每次右指针碰到重复值则计算子串长度,但如果最后一次左指针改变后遍历时没有遇到重复的,最后一段的子串的长度就要单独统计。

13. 罗马数字转整数

罗马数字包含以下七种字符: I, V, X, LCD 和 M

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 
  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。

 思路:将所有需要翻译的字符存放在字典中,用object存储,key都是字符。除提到的特殊位置外,将当前值与上一步结果做累加。对于特殊值,主要比较上一个值和当前值关系,是不是符合特殊处理的类型,如果是,累加值要减去上一个值的2倍。思考一下为什么是2倍

因为重复计算了一次,还要参与剪掉当前值

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function (s) {
    let dictionary = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }
    let result = 0;
    let lastStr = "";
    for (let str of s) {
        result += dictionary[str];
        if ((lastStr === 'I' && str === 'V') || (lastStr === 'I' && str === 'X')) {
            result += - 2;
        }
        if ((lastStr === 'X' && str === 'L') || (lastStr === 'X' && str === 'C')) {
            result += - 20;
        }
        if ((lastStr === 'C' && str === 'D') || (lastStr === 'C' && str === 'M')) {
            result += -200;
        }
        lastStr = str;
    }
    return result;
};

 这里比较的是key值,当上一步的key和当前key匹配的时候处理。

更进一步,可以考虑value值的匹配,当上一步的value<当前的value。及小的值出现在了左边,需要对结果进行处理

/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function (s) {
    let dictionary = {
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }
    let result = 0;
    let lastStr = "";
    for (let str of s) {
        result += dictionary[str];
        if (dictionary[lastStr] < dictionary[str]) {
            result -= dictionary[lastStr] * 2;
        }
        lastStr = str;
    }
    return result;
};

12. 整数转罗马数字

罗马数字包含以下七种字符: I, V, X, LCD 和 M

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 
  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给你一个整数,将其转为罗马数字。

思路:用哈希表存储键值对+使用相减法获取每步加入的字符。这里使用map构建哈希表,因为map对象会维护键值对的插入顺序 。为什么要考虑顺序呢?因为相减法会尝试用最大值做减法,用被减数减它,每次相减可获得减数对应的val值。比如3200这个数,3200-1000,得到一个1000,用M表示,剩余2200,是不是还可以用1000减,得到MM,还有1200,在用1000减,得到MMM,剩余200,在尝试从剩余的数值中找比200小的第一个,100。用100减得到MMMC,最后MMMCC

/**
 * @param {number} num
 * @return {string}
 */
var intToRoman = function (num) {
    let map = new Map();
    let carry = 1;
    let str = '';
    //map存储键值对,在遍历的时候顺序和插入的顺序保持一致,大的值放前面
    map.set(1000, 'M');
    map.set(900, 'CM');
    map.set(500, 'D');
    map.set(400, 'CD');
    map.set(100, 'C');
    map.set(90, 'XC');
    map.set(50, 'L');
    map.set(40, 'XL');
    map.set(10, 'X');
    map.set(9, 'IX');
    map.set(5, 'V');
    map.set(4, 'IV');
    map.set(1, 'I');
    //用相减比取余简单一点。一直尝试用map中较高的值做减数
    map.forEach((val, key) => {
        while (num >= key) {//对每个key值如果可以作为减数,一直减,直到不符合,在尝试下一个key
            str += val;
            num -= key;
        }
    })
    return str;
};

169. 多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

思路:这道题有很多种解法,比较容易理解的是字典、哈希表这种方式。将不重复的数字做key,出现次数统计为value,最后在输出value中比n/2大的元素。

/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function (nums) {
    let arr = [];
    let elementMap = new Map();
    for (let num of nums) {
        elementMap.set(num, elementMap.get(num) + 1 || 1);
    }
    elementMap.forEach((value, key) => { 
        if (value > nums.length / 2) {
            arr.push(key);
        }
    })
    return arr;
};

这里不确定题目的意思是有多个大于n/2还是只要一个,就用数组了,但是看了题解,好像只有一个

205. 同构字符串

 给定两个字符串 s 和 t ,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

 思路:这道题要读懂题目,否则容易漏掉判断条件。

总结有两点:

  1. s中作为key的元素,二次出现,t中对应位置的value要与之前的相等
  2. t中做为value的元素,二次出现,其s对应的key也要与之前的相等。

比如下面的示例,当t中的b二次出现时,key是d与之前的b不一样,就要返回false

 

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isIsomorphic = function (s, t) {
    if (s.length != t.length) return false;
    let map = new Map();
    let i = s.length;
    while (i--) {
        //如果s中key存在 且当前的value与map存的不一样返回false
        if (map.has(s[i])) {
            if (map.get(s[i]) != t[i]) {
                return false;
            }
        } else if ([...map.values()].includes(t[i])) {
            //如果key不存在,但是当前value已经在map中出现过了也返回false
            return false;
        }
        map.set(s[i], t[i]);
    }
    return true;
};

通过map的get方法找到value很简单。如何通过value找get呢,这里没有按照这个思想,因为map首先已经存放的是唯一的value,如果value已经存在了则返回false就好了。注意这里的map.values()方法返回的是迭代对象,需要转换为数组,因为数组提供了更多的方法比如includes判断某个元素是否在数组中。

可以用[...迭代对象]也可以用Array.from(对象)来转换

 有上面的题目可以看出,要想熟练掌握字典或哈希表数据结构,需要对ES6的Map对象基本操作铭记与心。

 451. 根据字符出现频率排序

给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。返回 已排序的字符串 。如果有多个答案,返回其中任何一个。

思路:按字符出现的频率对出现的字符进行排序。首先可能想到可以用字典统计字符出现的次数,然后按次数进行排序。但是如果key存的是字符,value存的是次数,在对次数排序的时候不知道key对应的是哪个了。如果将次数改成相同字符组成的子串呢?子串的长度不就是出现的频率了吗

/**
 * @param {string} s
 * @return {string}
 */
var frequencySort = function (s) {
    let map = new Map();
    //遍历字符串,字典key是字符,value是相同字符拼接的子串
    for (let i = 0; i < s.length; i++) {
        map.set(s[i], map.get(s[i]) ? map.get(s[i]) + s[i] : s[i]);
    }
    //拿到values组成的字符串数组
    const values = [...map.values()];
    //根据字符串长度按照从大到小的顺序排序
    values.sort((a, b) => b.length - a.length);
    //返回values的字符串形式
    return values.join("");
};

数字题型

 202. 快乐数

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

「快乐数」 定义为:

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

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

思路:这题要抓题目关键字,“无限循环”,如果程序不知道什么时候跳出来,比如使用递归方式去求解,就会陷入死循环,超出最大调用栈限制。而如果我们知道了,不是快乐数的一定会进入循环,从这个点考虑

进入循环?就是已经出现的数字可以又出现了,看下官方题解里给的示例:

如果这个示例放在题目里就好解决了。我们用字典将过程中计算的结果存起来不就好了吗,如果出现了循环,说明和字典重复了,这个时候就结束循环了,返回false。当然如果过程中出现了1则循环也结束,返回true

/**
 * @param {number} n
 * @return {boolean}
 */
var isHappy = function (n) {
    let map = new Map();
    let result = n;
    while (result) {
        if (getNum(result) === 1) {
            return true;
        } else {
            if (map.has(result)) {
                return false;
            } else {
                map.set(result, 1);
                result = getNum(result);
            }
        }
    }

};

function getNum(num) {
    let result = 0;
    while (num) {
        let n = num % 10;
        result += n * n;
        num = Math.floor(num / 10);
    }
    return result;
}

 这里循环的条件可以用while(result),或者while(true)。只要把结束循环的条件写好就好啦。

另外养成习惯,能抽出来的方法单独写个方法。让主流程简单一些。

矩阵

73. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法

示例 1:

输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]

思路:使用两个set记录行和列等于0的行的下标和列的下标。遍历矩阵,将出现在set中行或列的元素置为0

/**
 * @param {number[][]} matrix
 * @return {void} Do not return anything, modify matrix in-place instead.
 */
var setZeroes = function (matrix) {
    const rows = matrix.length;
    const cols = matrix[0].length;
    let rowSet = new Set();//记录原矩阵有0的行
    let colSet = new Set();//记录原矩阵有0的列
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            if (matrix[i][j] == 0) {
                rowSet.add(i);
                colSet.add(j);
            }
            //判断当前i和j是否在rowSet和colSet里,如果是的话,置为0
        }
    }
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            if (rowSet.has(i) || colSet.has(j)) {
                matrix[i][j] = 0;//判断当前i和j是否在rowSet和colSet里,如果是的话,置为0
            }
        }
    }
};

设计类考察js的构造函数

208. 实现 Trie (前缀树)

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false

 思路:构造函数里通过this定义子类共同使用的属性。这里首先需要一个数组存储所有push的字符串。其次,可以通过Set对象存储已放入数组中的非重复元素。在查找时通过set更方便。


var Trie = function () {
    this.trie = [];//前缀数组
    this.set = new Set();//出现的不重复的字符串集合
};

/** 
 * @param {string} word
 * @return {void}
 */
Trie.prototype.insert = function (word) {
    this.trie.unshift(word);//push入数组
    this.set.add(word);//更新或新增word
};

/** 
 * @param {string} word
 * @return {boolean}
 */
Trie.prototype.search = function (word) {
    if (this.set.has(word)) {//使用set查找
        return true;
    } else {
        return false;
    }
};

/** 
 * @param {string} prefix
 * @return {boolean}
 */
Trie.prototype.startsWith = function (prefix) {
    for(let key of this.set.keys()){//遍历set,通过字符串startsWith方法查找前缀
        if(key.startsWith(prefix)){
            return true;
        }
    }
    return false;
};

/**
 * Your Trie object will be instantiated and called as such:
 * var obj = new Trie()
 * obj.insert(word)
 * var param_2 = obj.search(word)
 * var param_3 = obj.startsWith(prefix)
 */

 380. O(1) 时间插入、删除和获取随机元素

实现RandomizedSet 类:

  • RandomizedSet() 初始化 RandomizedSet 对象
  • bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false
  • bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false
  • int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。

你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1)

思路:看到插入和删除时间复杂度为 O(1) 时,立马联想到使用哈希表,但哈希表无法在随机获取值的时候达到O(1) ,需要使用数组配合。因此同时使用哈希表和数组结构

在插入时,哈希表记录对应值所在的下标,并将值从数组队尾插入;

在删除时,获取值对应的数组下标,并用队尾元素覆盖写入对应下标,且重新给哈希表对应的记录更新下标;

在查找时,只要随机一个下标值访问数组即可


var RandomizedSet = function () {
    this.map = new Map();//存储val和index
    this.arr = [];
};

/** 
 * @param {number} val
 * @return {boolean}
 */
RandomizedSet.prototype.insert = function (val) {
    if (this.map.has(val)) {
        return false;
    } else {
        this.map.set(val, this.arr.length);
        this.arr.push(val);
        return true;
    }
};

/** 
 * @param {number} val
 * @return {boolean}
 */
RandomizedSet.prototype.remove = function (val) {
    if (this.map.has(val)) {
        const index = this.map.get(val);
        this.arr[index] = this.arr[this.arr.length - 1];
        this.map.set(this.arr[index], index);
        this.arr.pop();
        this.map.delete(val);
        return true;
    } else {
        return false;
    }
};

/**
 * @return {number}
 */
RandomizedSet.prototype.getRandom = function () {
    let randomIndex = Math.floor(Math.random() * this.arr.length);
    return this.arr[randomIndex];
};

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * var obj = new RandomizedSet()
 * var param_1 = obj.insert(val)
 * var param_2 = obj.remove(val)
 * var param_3 = obj.getRandom()
 */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三月的一天

你的鼓励将是我前进的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值