C++ 位运算 高频面试考点 力扣 01.01 判断字符是否唯一 题解 每日一题
文章目录
- 题目解析
- 为什么这道题值得你花几分钟的时间看完?
- 可以做出这道题的方法
- 核心解法详解
- 哈希表
- 数组模拟哈希(空间优化)
- 位运算(最优,无额外数据结构)
- 核心原理(位图思想)
- 关键位运算操作
- 代码实现(O(n))
- 时间复杂度和空间复杂度的分析
- 其他方法 (对比参考)
- 暴力遍历(O(n²))
- 排序+相邻判断(O(n logn))
- 面试避坑指南
- 思考题
- 总结
- 下题预告
题目解析
题目链接:力扣 01.01 判断字符是否唯一
题目描述:
示例 1:
输入: s = “leetcode”
输出: false
解释:字符串中包含重复的 ‘e’ 和 ‘t’,故返回 false。
示例 2:
输入: s = “abc”
输出: true
解释:字符串中所有字符均不重复,故返回 true。
限制:
- 0 <= len(s) <= 100
- s[i] 仅包含小写字母
- 如果你不使用额外的数据结构,会很加分。
为什么这道题值得你花几分钟的时间看完?
它不是一道普通的算法题,而是面试高频“开胃题”。难度不高,价值却堪称“算法思维试金石”——它用极简的题干,藏住了“从直观到最优”的完整优化链路,能帮你快速复盘算法设计的核心逻辑。
面试官考这道题,从来不是“做出来就行”。他们真正想通过这道题看清你的能力底色,尤其关注三点:
- 能否说出3种及以上不同解法,展现思维广度;
- 能否精准分析每种解法的时间/空间复杂度,体现基础功底;
- 能否结合“仅小写字母”的题目限制,给出“无额外数据结构”的最优解,彰显问题拆解与优化能力。
接下来,我们将从“基础解法”到“进阶最优解”逐步拆解。其中会重点用到位运算技巧——这正是面试中的“隐形加分项”,用好它往往能给面试官留下“算法功底扎实”的第一印象。
如果对位运算的细节有些生疏,建议先结合我的这篇总结复习:位运算 常见方法总结 算法练习。这篇博客可以帮刚接触算法的朋友可以用它捋顺基础,避免后续思路脱节;已有积累的伙伴能借它快速唤醒核心技巧记忆;至于算法大佬,直接往下读即可——当然若您愿意在评论区分享建议,我会万分感激,文中梳理的场景化用法也可供您随手一阅,为后续最优解的推导多添一份顺畅严谨。
可以做出这道题的方法
这道题的解法可按“空间优化程度”分为三个层级,从“直观易想到”到“极致优化”,具体对比如下表:
解法 | 核心逻辑 | 时间复杂度 | 空间复杂度 | 适用场景 | 优势与不足 |
---|---|---|---|---|---|
暴力遍历 | 双层循环,每个字符与后续所有字符对比,存在重复则返回false | O(n²) | O(1) | 数组长度极小(n<10) | 无额外空间,但时间效率极低 |
排序+相邻判断 | 先排序(重复字符会相邻),再遍历对比相邻字符是否相同 | O(n logn) | O(logn) | 对空间要求较低,可接受排序开销 | 实现简单,但依赖排序,修改原字符串 |
哈希表 | 遍历字符存入哈希表,存入前判断是否已存在,存在则返回false | O(n) | O(26)≈O(1) | 追求时间效率,允许少量空间 | 时间最优,但需额外哈希表结构 |
数组模拟哈希 | 用26位布尔数组替代哈希表,对应26个小写字母,标记字符是否出现 | O(n) | O(26)≈O(1) | 已知字符范围(仅小写字母) | 空间更紧凑,比哈希表更贴合题目限制 |
位运算(最优) | 用一个整数的26个二进制位作为“位图”,标记字符是否出现,无额外数据结构 | O(n) | O(1) | 面试加分项,追求极致空间优化 | 无任何额外数据结构,空间效率拉满 |
我将在时间复杂度最优的情况下按“空间优化递进”的顺序讲解,重点聚焦数组模拟哈希和位运算两种核心解法,最后补充基础解法作为对比。
核心解法详解
哈希表
核心思路
- 初始化一个哈希表(如C++的
unordered_set<char>
); - 遍历字符串
s
中的每个字符ch
:- 若
ch
已在哈希表中,说明存在重复,返回false
; - 若
ch
不在哈希表中,将其插入哈希表;
- 若
- 遍历结束未发现重复,返回
true
。
代码实现(O(n))
#include <unordered_set>
using namespace std;class Solution {
public:bool isUnique(string astr) {unordered_set<char> char_set;for (char ch : astr) {// 若字符已存在,返回falseif (char_set.count(ch)) {return false;}// 字符不存在,插入哈希表char_set.insert(ch);}return true;}
};
数组模拟哈希(空间优化)
哈希表虽高效,但仍属于“额外数据结构”,且存在一定的底层实现开销。由于字符范围固定为“26个小写字母”,我们可以用一个长度为26的布尔数组替代哈希表,进一步压缩空间。
核心思路
- 字符映射:将小写字母
ch
映射为数组下标——index = ch - 'a'
(如’a’→0,'b’→1,…,'z’→25); - 初始化布尔数组
used[26]
,全部置为false
(表示字符未出现); - 遍历字符串
s
中的每个字符ch
:- 计算映射下标
index
; - 若
used[index]
为true
,说明字符已出现,返回false
; - 若
used[index]
为false
,将其置为true
;
- 计算映射下标
- 遍历结束未发现重复,返回
true
。
代码实现(O(n))
class Solution {
public:bool isUnique(string astr) {bool used[26] = {false}; // 26个小写字母对应26个位置for (char ch : astr) {int index = ch - 'a'; // 字符转下标if (used[index]) {return false; // 已出现过,返回false}used[index] = true; // 标记为已出现}return true;}
};
优势
- 空间更紧凑:布尔数组仅占用26个字节(哈希表需存储链表/红黑树结构,开销更大);
- 访问更快:数组下标访问为O(1),比哈希表的哈希计算+冲突处理更高效。
位运算(最优,无额外数据结构)
这是面试中的“加分解法”——利用一个32位整数的二进制位作为“位图”,完全避免额外数据结构,空间复杂度达到理论最优O(1)。
核心原理(位图思想)
32位整数有32个二进制位,而我们仅需26个位即可表示26个小写字母:
- 用整数
map
的第i
位(从右数)表示第i
个小写字母是否出现(如第0位→’a’,第1位→’b’,…); - 二进制位为
0
:字符未出现;二进制位为1
:字符已出现。
如图中第三个👇
关键位运算操作
1. 判断字符是否已出现(位图第i位是否为1)
核心公式:
(n >> i) & 1
(n代表位图,i代表目标位,详细推导可参考位运算 常见方法总结 算法练习)
- 第一步移位聚焦:将位图
n
右移i
位(n >> i
),把要判断的第i
位移到二进制数的最右侧,其余高位自动舍弃; - 第二步与运算验证:用移位结果与
1
做“与运算”(& 1
)。由于1
的二进制只有最右侧为1,仅当目标位原本为1时,结果才会是1(表示字符已出现),否则为0(表示未出现)。
示例:若n=0b101
(对应’a’和’c’已标记),判断’c’(对应位i=2
):
0b101 >> 2 = 0b1
,0b1 & 1 = 1
→ 字符已出现。
2. 标记字符为已出现(将位图第i位改为1)
核心公式:
n = n | (1 << i)
(n代表位图,i代表目标位,详细推导可参考位运算 常见方法总结 算法练习)
- 第一步构建掩码:将
1
左移i
位(1 << i
),得到一个“仅第i位为1、其余位全为0”的二进制数(即掩码); - 第二步或运算标记:用位图
n
与掩码做“或运算”(n | 掩码
)。或运算的特性是“有1则1”,能在不改变其他位的前提下,强制将第i位改为1,实现字符标记。
示例:若n=0b101
,标记’d’(对应位i=3
):
1 << 3 = 0b1000
,0b101 | 0b1000 = 0b1101
→ 'd’已成功标记。
代码实现(O(n))
class Solution {
public:bool isUnique(string astr) {int map = 0; // 32位整数,作为26位位图for (char ch : astr) {int i = ch - 'a'; // 字符映射为下标(0-25)// 判断第i位是否为1(字符已出现)if ((map >> i) & 1) {return false;}// 将第i位改为1(标记字符已出现)map |= (1 << i);}return true;}
};
代码解释:
map = 0
:初始位图全为0(所有字符未出现);i = ch - 'a'
:核心映射逻辑,将字符转为0-25的整数;(map >> i) & 1
:核心判断逻辑,快速检测字符是否重复;map |= (1 << i)
:核心标记逻辑,更新位图状态。
时间复杂度和空间复杂度的分析
解法 | 时间复杂度 | 空间复杂度 | 关键原因 |
---|---|---|---|
哈希表 | O(n) | O(1) | 遍历字符串1次,哈希表最多存26个字符 |
数组模拟哈希 | O(n) | O(1) | 遍历字符串1次,数组长度固定为26 |
位运算 | O(n) | O(1) | 遍历字符串1次,仅用1个整数,无任何额外数据结构 |
其他方法 (对比参考)
暴力遍历(O(n²))
最直观但效率最低的解法,适合理解“重复判断”的本质逻辑。
核心思路
双层循环:外层循环遍历每个字符astr[i]
,内层循环遍历astr[i]
之后的所有字符astr[j]
(j > i),若astr[i] == astr[j]
则返回false
。
代码实现
class Solution {
public:bool isUnique(string astr) {int n = astr.size();for (int i = 0; i < n; ++i) {for (int j = i + 1; j < n; ++j) {if (astr[i] == astr[j]) {return false;}}}return true;}
};
缺点:
时间复杂度O(n²),当n=100时需执行近5000次比较,效率远低于O(n)解法。
排序+相邻判断(O(n logn))
利用“排序后重复字符相邻”的特性,将双层循环优化为单层循环。
核心思路
- 对字符串进行排序(如快速排序,时间复杂度O(n logn));
- 遍历排序后的字符串,对比相邻字符是否相同,若相同则返回
false
。
代码实现
#include <algorithm>
using namespace std;class Solution {
public:bool isUnique(string astr) {// 排序字符串(重复字符会相邻)sort(astr.begin(), astr.end());int n = astr.size();for (int i = 1; i < n; ++i) {// 相邻字符相同,返回falseif (astr[i] == astr[i-1]) {return false;}}return true;}
};
注意
排序会修改原字符串(若不允许修改,需额外复制一份,空间复杂度变为O(n)),且时间复杂度依赖排序算法,不如哈希/位运算高效。
面试避坑指南
- 用排序解法时,若题目要求“不修改原字符串”,需先复制副本(如
string sorted_str = astr
),否则会因修改输入数据扣分; - 位运算解法中,
i
的计算必须是ch - 'a'
(而非直接用ch
),否则会因字符ASCII值超出26位范围,导致位图逻辑错误; - 哈希表解法中,优先选择
unordered_set
(平均O(1)访问)而非set
(O(logn)访问),避免不必要的性能损耗。
思考题
如果用“异或运算”替代“与/或+移位”,能否实现同样的逻辑?下一题“丢失的数字”会带你解锁异或的更多用法~
总结
这道题的本质是“重复检测”,解法的优化路径清晰体现了“时间与空间的权衡”以及“利用题目限制做定制化优化”的算法思想:
- 若追求“最简单实现”:选暴力遍历或排序+相邻判断;
- 若追求“时间最优”:选哈希表或数组模拟哈希;
- 若追求“面试加分”:必选位运算(无额外数据结构,空间极致优化)。
核心考点回顾:
- 字符映射:
ch - 'a'
是处理小写字母的常用技巧; - 位图思想:用二进制位标记状态,是嵌入式、底层开发中的常用优化手段;
- 复杂度分析:能清晰说明每种解法的优劣,是面试的核心考察点。
下题预告
下一篇博客将讲解力扣 268. 丢失的数字,这道题与今天的“位运算解法”一脉相承,同样可以用“异或特性”实现最优解,同时还能巩固“高斯求和”“哈希表”等多种思路。