Skip to main content

哈希表

快乐数

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

「快乐数」 定义为:

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

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

示例 1:

输入:n = 19
输出:true
解释:
1² + 9² = 82
8² + 2² = 68
6² + 8² = 100
1² + 0² + 02 = 1

我的解法

var isHappy = function (n) {
let deep = 0
while (n !== 1 && deep < 10) {
const digits = n.toString().split('')

n = 0
digits.forEach(num => {
n += Math.pow(num, 2)
})

deep++
}

return n === 1
}

这个解法其实很粗糙,通过肯定是能通过的,但有几个很明显的问题。

  1. 首先在数位分离我采用 n.toString().split('') ,不过这是借用到了 js 的特性,要是换其他语言肯定不行。
  2. 重复计算,比如 19 第一次计算 1² + 9² = 82,第二次计算 8² + 2² = 68,第三次计算 6² + 8² = 100 ...,这里的 8² 就可以将其结果存起来,避免重复计算。
  3. 因为存在某些数(如 2)会无限循环,所以需要设置一个深度限制 deep,不然会死循环。

改进

在数位分离上可以依次使用 num % 10 来得到 个十百...位, 通过下方代码则可以得到所有位上的数字。

function getDigist(nums) {
let digits = []
while (nums > 0) {
digits.unshift(nums % 10)
nums = Math.floor(nums / 10)
}

return digits
}

getDigist(1234)

而要避免重复运算最简单就是使用哈希集合,将计算过的结果存起来。

const cache = {}

digits.forEach(num => {
if (!cache[num]) {
cache[num] = Math.pow(num, 2)
}

n += cache[num]
})

然而现在还有一个问题,就是如何处理 deep 的问题,由于官方题解中对于这部分给出分析,这里便不赘述了。如果一个数不是快乐数,那么它一定存在一个循环,可以将生成循环链的数字存入哈希集合中来判断是否处于无限循环中。最终代码如下

var isHappy = function (n) {
function getDigist(nums) {
let digits = []
while (nums > 0) {
digits.unshift(nums % 10)
nums = Math.floor(nums / 10)
}
return digits
}

const cache = {}
const set = new Set()

while (n !== 1 && !set.has(n)) {
const digits = getDigist(n)
set.add(n)

n = 0
digits.forEach(num => {
if (!cache[num]) {
cache[num] = Math.pow(num, 2)
}

n += cache[num]
})
}

return n === 1
}

最长连续序列

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

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

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4

示例 2:

输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

我的解法

  1. 对数组去重并排序
  2. 判断 nums[i] === nums[i+1] - 1,为真则缓存当前最长序列的长度
  3. 不符合条件则重新计算当前最长序列的长度,取 maxLen 和 currentLen 最大值作为返回值
var longestConsecutive = function (nums) {
if (nums.length === 0) return 0

nums = [...new Set(nums)]
nums = nums.sort((a, b) => a - b)

const n = nums.length

let maxLen = 0
let currentLen = 0

for (let i = 0; i < n - 1; i++) {
if (nums[i] === nums[i + 1] - 1) {
currentLen++
} else {
maxLen = Math.max(maxLen, currentLen)
currentLen = 0
}
}

maxLen = Math.max(maxLen, currentLen) + 1

return maxLen
}

哈希表

var longestConsecutive = function (nums) {
let numSet = new Set(nums)

let maxLen = 0

for (let num of numSet) {
if (!numSet.has(num - 1)) {
let currentNum = num
let currentLen = 1

while (numSet.has(currentNum + 1)) {
currentNum++
currentLen++
}

maxLen = Math.max(maxLen, currentLen)
}
}

return maxLen
}

解释: 只有当一个数是连续序列的第一个数的情况下才会进入内层循环 if (!numSet.has(num - 1)),然后在内层循环中匹配连续序列中的数,因此数组中的每个数只会进入内层循环一次。

单词规律

给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。

这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。

示例1:

输入: pattern = "abba", s = "dog cat cat dog"
输出: true

示例 2:

输入:pattern = "abba", s = "dog cat cat fish"
输出: false

示例 3:

输入: pattern = "aaaa", s = "dog cat cat dog"
输出: false

我的解法

var wordPattern = function (pattern, s) {
// 提取 pattern 的索引 { a: [0,3], b:[1,2] }
const rules = {}
pattern.split('').forEach((p, i) => {
rules[p] ||= []
rules[p].push(i)
})

const words = s.match(/[a-z]+\b/g)

for (let value of Object.values(rules)) {
const group = new Set()

for (let v of value) {
if (group.size === 0) {
group.add(words[v])
}

if (!group.has(words[v])) {
return false
}

group.add(words[v])
}
}
return true
};

而对于下列数据会返回 true,显然是不符合条件的。

pattern = "abba"

s = "dog dog dog dog"

原因就在于 ab 要不同,因此就需要改进验证组 group。

于是就想到既然提取 pattern 的索引,不如也提取 words 索引,然后判断两者值

var wordPattern = function (pattern, s) {
function convertArray(arr) {
const result = [];
let charMap = {};

for (let i = 0; i < arr.length; i++) {
const char = arr[i];
if (charMap[char] === undefined) {
charMap[char] = [i];
} else {
charMap[char].push(i);
}
}

for (const char in charMap) {
result.push(charMap[char]);
}

return result;
}

// "abba" => [[0, 3], [1, 2]]
const patternValues = convertArray(pattern)
// "dog cat cat dog" => [[0, 3], [1, 2]]
const wordValues = convertArray(s.match(/[a-z]+\b/g))

for (let i in patternValues) {
if (JSON.stringify(patternValues[i]) !== JSON.stringify(wordValues[i])) {
return false
}
}

return true
};

然而我没想到,但有字母为 constructor 时,wordsRules[w] 就会变成 wordsRules[constructor] 相当于下图所示。

image-20230921010005890

不过也好解决,因为这里的 key a,b,dog,cat 事实上我们都没用到。只需要把{ a: [0,3], b: [1,2] } 转成 [[0,3], [1,2]] 即可。

然而我发现 将 "abba" 转成 [[0,3], [1,2]] 在不借助 Object.values() 情况下是一件很困难的事情。于是既然 {}[constructor] 不行,那么我就用 Map.get() 来解决便可。贴上 convertArray 函数代码

function convertArray(arr) {
const result = [];
const charMap = new Map();

for (let i = 0; i < arr.length; i++) {
const char = arr[i];
if (charMap.has(char)) {
charMap.get(char).push(i);
} else {
charMap.set(char, [i]);
}
}

charMap.forEach((indices) => result.push(indices));

return result;
}

很显然,上述的解法缺陷很多,这里就不一一列举了,直接来看正确答案。

正确答案

var wordPattern = function (pattern, s) {
const word2ch = new Map();
const ch2word = new Map();
const words = s.split(' ');
if (pattern.length !== words.length) {
return false;
}

for (const [i, word] of words.entries()) {
const ch = pattern[i];
if (word2ch.has(word) && word2ch.get(word) != ch || ch2word.has(ch) && ch2word.get(ch) !== word) {
return false;
}
word2ch.set(word, ch);
ch2word.set(ch, word);
}
return true;
};Ï

要判断一个集合与另一个集合是否相同的关系叫「双射」,利用 两个哈希集合的 key 与 value 来判断便可。

同构字符串

有了上题解题思路,这题就容易许多了。

var isIsomorphic = function(s, t) {
const s2t = {};
const t2s = {};

for (let i = 0; i < s.length; ++i) {
const x = s[i], y = t[i];
if ((s2t[x] && s2t[x] !== y) || (t2s[y] && t2s[y] !== x)) {
return false;
}
s2t[x] = y;
t2s[y] = x;
}
return true;
};