Leetcode 算法题题解——判定字符是否唯一

244 阅读5分钟

此题出自《程序员面试宝典》,书中第十章有对此题的解析,但比较简短。

Leetcode 题目链接为:leetcode-cn.com/problems/is… ,感兴趣可以先做一做再来看这篇题解。

对于这道题,思路有很多,不同的方案对应的时间和空间复杂度都不同,本文使用的语言为 JavaScript。

解法 1、哈希表

常规的思路是使用哈希表。

key 用数字类型,value 用布尔值(也可以用数字记录出现次数),存储指定字符是否被访问过的信息。

在遍历字符的过程中,如果发现哈希表存在该字符,说明之前我们遇到过相同的字符,不符合题意,直接返回 false;如果不存在该字符,将其存到哈希表。如果能够走完遍历,符合题意,返回 true。

/**
 * @param {string} astr
 * @return {boolean}
 */
function isUnique(astr) {
  const hashMap = {};
  for (const c of astr) {
    if (hashMap[c]) return false;
    hashMap[c] = true;
  }
  return true;
};

时间复杂度 O(n),空间复杂度 O(n)

解法 2、集合

集合这种数据结构的特点就是 存储的元素不会重复,我们可以将字符串的字符全部塞进集合中,然后对比集合的长度和原来字符串的长度是否相等。

或者可以像解法 1 那样,在遍历过程中往集合添加元素,再看集合的长度是否加一,一旦发现长度没变,说明出现了重复字符,返回 false。

function isUnique(astr) {
  return (new Set(astr)).size === astr.length;
};

集合的底层有各种各样的实现,可能是哈希表,可能是红黑树或跳表,也可能是动态数组。集合的实现不同,空间和时间复杂度也会有一些差别,

总的来说,时间复杂度是 O(n),空间复杂度也是 O(n)

解法 3、原地查找

现在我们加上题目中的限制:不使用额外的数据结构。

所谓额外的数据结构,就是哈希表、数组、集合这些数据结构。换句话来说,时间复杂度需要为 O(1)

遍历字符,然后再从正在遍历的字符下一位遍历,查找是否有和当前字符相同的。

因为使用了双重循环,时间复杂度高了一个数量级,为 O(n^2) 。没有使用额外的数据结构,而是使用提供的字符串,所以空间复杂度是 O(1)

function isUnique(astr) {
  for (let i = 0; i < astr.length; i++) {
    for (let j = i + 1; j < astr.length; j++) {
      if (astr[i] === astr[j]) return false;
    }
  }
  return true;
};

解法 4、使用位操作

但如果面试官告诉你,**字符串只由小写字母组成,你可以如何实现呢?**因为数量有限,我们可以不用哈希表,改用布尔值数组。值得一提的是,Leetcode 中这题提供的测试用例都是小写字母,所以你可以在开头加上这么一句:if (astr.length > 26) return false;

为什么用数组不用哈希表呢?这是因为我们存储映射时,有一条准则:如果缓存的 key 为数字,且范围在 0 到一个指定的常数之间,你就应该用数组而不是哈希表。

因为哈希表的 O(1) 读写时间复杂度是理想条件下的,当多个 key 发生冲突时,时间复杂度会趋向于 O(n),而数组则不会。

回到正题,我们用布尔值数组行不行,不太行,因为算是使用了数据结构。那有没有更小的布尔值数组,有,那就是 用位来实现布尔值数组

位,也就是比特,只有两个值:1 和 0,通常编程语言不提供位的基本类型,这时候我们就可以用 32 位或是 64 位的整数类型来存多个位。我们先看看解法:

function isUnique(astr) {
  let bits = 0;
  for (let i = 0; i < astr.length; i++) {
    const n = 1 << (astr[i].charCodeAt(0) - 'a'.charCodeAt(0));
    if (n & bits) return false;
    bits = bits | n;
  }
  return true;
};

位运算在实际业务开发中其实用的比较少,在底层开发中才比较常见,因为我们业务开发没有必要为了一丁点的性能,而降低代码的可读性。

代码是写给人看的,不是写给机器看的,只是顺便计算机可以执行而已。

下面讲解一下这题需要用到的位运算知识。

判断数字的二进制形式某一位是 0 还是 1

n & (1 << k)

这个对应查询布尔值数组特定索引下的值。下文我将会用二进制来表示数字。

假设我们想要知道一个数字(假设它此时的值为 01010),问它从右往左第 2 位是 0 还是 1(这里最右边的位为第 0 位)。

首先我们需要一个 1 作为起始值,然后对其进行左移( << )2 位,即 1 << 2 得到 00100,然后对其进行按位与( & )操作。按位与操作的特点是,对应的位对比,全 1 为 1,有 0 为 0。计算如下:

    01010
AND 00100
   -------
    00000

结果为 0,说明数字的特定位为 0;否则为 1。

我们对 1 做左移操作,为的是生成只有一个位是 1,其余位都是 0 的二进制,这样不相干的位都会被 & 计算为 0。

这时候特定位的对比就决定了这个结果是否为 0。如果特定位上是 0,结果就是 0,如果是 1,结果就是大于 0 的整数。

给数字二进制形式的某位设置为一

这个对应将布尔值数组特定索引下的值设为 true。

给数字二进制形式的某位设置为 1 的写法为: n | (1 << k)

按位或( | ),特点是有 1 为 1,全 0 为 0。

1 << k 同样是将第 k 位设为 1,其余位设为 0。

0,和 1 或 0 做按位或运算,结果都是后者,这样就可以保证除特定位的其余位保留原样。

而第 k 位为 1,和 0 或 1 做按位或运算,永远都是 1,这样最终就成功将特定位设置为 1 了。

结尾

大概就这些,位运算算是有点偏门的用法,但有些常用的特性还是要掌握的,以后我会说一些其他的位操作相关的算法题。