Logo
Section 1 / 5

是否唯一

我们将讨论三种判断字符串中所有字符是否唯一的办法。每种方法在时间和空间复杂度方面各有优缺点。

导入

是否唯一

我们将讨论三种判断字符串中所有字符是否唯一的办法。每种方法在时间和空间复杂度方面各有优缺点。

是否唯一

指令

创建一个函数,判断字符串中的所有字符是否都唯一。该函数需区分大小写,即同时包含 'a' 和 'A' 的字符串仍可通过测试。

输入:字符串
输出:布尔值

示例

1isUnique('abcdef'); // -> true 2isUnique('89%df#$^a&x'); // -> true 3isUnique('abcAdef'); // -> true 4isUnique('abcaef'); // -> false

我们立即意识到,必须处理字符串中的每一个字符。此算法的最佳时间复杂度为线性,即 O(n)。这是无法避免的。


1function isUnique(str) { 2 // 你的代码在此 3}

解法 1

这是最直接、暴力的解法。

1function isUnique(str) { 2 for(let i = 0; i < str.length; i++) { 3 if(str.lastIndexOf(str[i]) !== i) { 4 return false; 5 } 6 } 7 8 return true; 9}

工作原理

我们遍历整个字符串。对于每个字符,检查其最后一次出现的位置是否与当前循环索引相同。

如果检查失败,说明该字符在后续位置再次出现,可立即返回 false。若遍历完成,说明所有字符均唯一,返回 true。

时间复杂度

外层循环遍历字符串中每个字符,初始复杂度为 O(n)。

内层 str.lastIndexOf() 从后往前扫描字符串。当处理第一个字符时,若所有字符唯一,则需遍历整个字符串。

随着循环推进,lastIndexOf 需要扫描的字符数逐渐减少。平均而言,每次调用需扫描字符串一半长度,因此 lastIndexOf 的时间复杂度为 O(1/2 * n),去掉常数后为 O(n)。

由于 lastIndexOf 嵌套在循环内部,时间复杂度相乘,最终结果为:

O(n²)

空间复杂度

无论字符串多长,函数中仅使用一个变量 i,因此空间复杂度为:

O(1)

这是可达到的最佳空间复杂度。

但时间复杂度仍有优化空间。


解法 2

1function isUnique(str) { 2 const chars = str.split('').sort(); 3 4 for(let i = 1; i < chars.length; i++) { 5 if(chars[i] === chars[i - 1]) { 6 return false; 7 } 8 } 9 10 return true; 11}

工作原理

将字符串中每个字符放入数组并排序。排序后,重复字符会相邻排列。

然后遍历排序后的数组,检查当前字符是否与前一个字符相同。若相同则返回 false。若遍历完成,说明所有字符唯一,返回 true。

时间复杂度

排序算法的时间复杂度约为 O(n * log(n))。循环部分为 O(n)。

两者独立执行,总时间复杂度为 O(n + n * log(n))。去掉低阶项后为:

O(n * log(n))

这是最终的时间复杂度。

空间复杂度

需要存储所有字符到数组中,因此空间复杂度为:

O(n)

我们可以做得更好。


解法 3

1function isUnique(str) { 2 const chars = {}; 3 4 for(let i = 0; i < str.length; i++) { 5 const thisChar = str[i]; 6 7 if(chars[thisChar]) { 8 return false; 9 } 10 11 chars[thisChar] = true; 12 } 13 14 return true; 15}

工作原理

在循环中逐个处理字符,并将其存入对象中,以记录已遇到的字符。

每处理一个字符,检查对象中是否已存在该字符。若存在,返回 false;否则将其加入对象,继续下一次迭代。遍历完成后,说明所有字符唯一,返回 true。

时间复杂度

外层循环为 O(n)。

循环内部的操作均为 O(1):无嵌套循环或类似循环的函数。JavaScript 中对象的插入和查找通常为 O(1),因为它们本质上类似于哈希表。

因此,最终时间复杂度为:

O(n)

空间复杂度

我们为每个遇到的字符创建一条记录,因此空间复杂度为:

O(n)

使用的空间与输入大小成正比。


优化改进

我们可以对上述解法进行微调,使其更优雅。不再使用对象,而是使用 ES2015 新增的数据结构——Set。

Set 与对象类似,用于存储项目并稍后检索。

Set 的主要优势在于:它只能存储唯一的值。尝试添加重复项会被静默忽略,只保留唯一值。

此外,Set 支持快速插入和查找。

1function isUnique(str) { 2 const chars = new Set(); 3 4 for(let i = 0; i < str.length; i++) { 5 const thisChar = str[i]; 6 7 if(chars.has(thisChar)) { 8 return false; 9 } 10 11 chars.add(thisChar); 12 } 13 14 return true; 15}

我认为这个版本更优雅,因为它使用了 Set 这一数据结构,正是其设计用途。方法名 has 和 add 含义清晰,整体代码更易理解。


令人惊讶的是,Set 还能让我们将代码简化为一行。这是“巧妙”的解法,但效率略低于解法 3。

解法 4

1function isUnique(str) { 2 return new Set(str).size === str.length; 3}

如果所有字符唯一,集合的大小等于字符串长度;若有重复字符,集合大小会小于字符串长度,因为重复项被自动忽略。

虽然此解法的时间和空间复杂度也都是 O(n),但通常比解法 3 稍慢。因为解法 3 在发现重复字符时可以提前终止循环,而此解法必须遍历完整个字符串才能得出结果。


数组支持

值得一提的是,解法 4 以及解法 3 的两个版本(使用对象的版本和使用 Set 的版本)不仅适用于字符串,也适用于数组。不信?可以自己动手验证一下。

1function isUnique(str) { 2 return new Set(str).size === str.length; 3}

字符串和数组都是可迭代对象,Set 对它们的处理方式一致。


总结

暴力解法

第一种解法是大多数人最先想到的,简单直接。若你能识别出时间复杂度并进一步优化,便能在面试中脱颖而出。

第二种解法提升了时间效率,但牺牲了空间。

理想解法

第三种解法展示了合适数据结构如何显著降低时间复杂度。通过引入对象或 Set,我们将原本 O(n * log(n)) 的解法优化为 O(n),提升明显。

尽管第四种解法巧妙且简洁,但效率不如第三种。不过在面试中,它仍是极佳的答案。

代码扩展性

最后一个解法同时适用于字符串和数组,说明我们的解决方案很容易适配其他输入类型。在本课程的许多问题中,只需做少量调整即可复用。

掌握这些解题技巧才是核心目标。一旦掌握,你就能将其应用于各种问题。

关键收获

记住:采用不同策略可以显著改善时间或空间复杂度。排序使时间复杂度大幅下降。

合理使用数据结构也能提升效率。尝试用对象、数组、Set、栈或队列来存储数据,看看是否能打开新的解决思路。

简介

扁平化数组