当前位置: 首页 > ds >正文

位运算的巧思:以一道简单题看高效算法的设计精髓

位运算的巧思:以一道简单题看高效算法的设计精髓

问题的提出:哪些字符串是“一致”的?

力扣1684. 统计一致字符串的数目

假设我们有一个允许使用的字符集合 allowed,以及一串待检查的字符串数组 words。我们的目标是统计 words 中有多少个字符串,它们的所有字符都包含在 allowed 集合中。例如:

allowed = "ab", words = ["ad", "bd", "aaab", "baa", "badab"]

在这个例子中,只有 "aaab""baa" 里的字符都属于 "ab",因此答案是 2。

最直观的思路,可能是对 allowed 构建一个哈希集合(Set),然后遍历 words 中的每一个字符串,再遍历字符串中的每一个字符,去哈希集合中查找。如果发现任何一个字符不在哈希集合里,则该字符串不符合要求。

// 传统哈希集合解法示意
Set<Character> allowedChars = new HashSet<>();
for (char c : allowed.toCharArray()) {allowedChars.add(c);
}int count = 0;
for (String word : words) {boolean consistent = true;for (char c : word.toCharArray()) {if (!allowedChars.contains(c)) {consistent = false;break;}}if (consistent) {count++;}
}

这个方法是正确且易于理解的。它的时间复杂度是 O(m + n * k),其中 m 是 allowed 的长度,n 是 words 数组的长度,k 是平均字符串长度。对于适中的数据量,这个方法足够了。但有没有更“酷”一点、效率更高一点的方法呢?

位运算的闪光点:用一个整数表示一个字符集

注意到英文字母只有 26 个,这个特性非常适合使用位运算来表示字符集。我们可以用一个整数的 26 个最低位来分别对应字母 ‘a’ 到 ‘z’。如果某个位是 1,就表示对应的字母存在于字符集中;如果位是 0,则表示不存在。

例如:

  • 字符集 {“a”} 可以用二进制 ...0001 (十进制 1) 表示 (‘a’ 对应第 0 位)
  • 字符集 {“b”} 可以用二进制 ...0010 (十进制 2) 表示 (‘b’ 对应第 1 位)
  • 字符集 {“a”, “b”} 可以用二进制 ...0011 (十进制 3) 表示 (‘a’ 和 ‘b’ 对应位都为 1)
  • 字符集 {“a”, “c”} 可以用二进制 ...0101 (十进制 5) 表示 (‘a’ 和 ‘c’ 对应位都为 1)

将一个字符添加到集合中,就相当于将对应位置的位设为 1,这可以通过按位或 (|) 操作实现:mask |= (1 << (c - 'a'))。这里的 (c - 'a') 计算出字符 c 相对于 ‘a’ 的偏移量,即它对应的位数。1 << (c - 'a') 生成一个只有对应位是 1 的整数。

代码解析:巧妙的子集判断

基于这个思想,我们可以对 allowed 字符串构建一个“允许字符掩码” mask。然后,遍历 words 中的每一个字符串 word,也为其构建一个“当前字符串掩码” mask1

问题的关键就转化为:如何判断 word 的字符集是 allowed 字符集的子集?也就是说,word 中出现的任何字符,在 allowed 中都必须出现。用位掩码来表达就是:mask1 中为 1 的位,在 mask 中也必须为 1。

考虑代码中的这一行判断:

if ((mask1 | mask) == mask) {res++;
}

这句代码乍看有点绕,为什么是 (mask1 | mask) == mask 而不是直接比较 mask1 == mask 呢?

原因在于我们判断的是子集关系,而不是完全相等。

让我们用上面的例子来理解:

  • allowed = "ab", mask = ...0011 (十进制 3)
  • word = "a", mask1 = ...0001 (十进制 1)

如果直接比较 mask1 == mask (1 == 3),显然是 false,这不符合我们的要求,因为 “a” 是由 “ab” 中的字符组成的。

现在看 (mask1 | mask)

  • mask1 | mask(...0001 | ...0011)
  • 按位或的结果是 ...0011 (十进制 3)

此时 (mask1 | mask) == mask (3 == 3) 成立,判断正确!

再举一个不符合要求的例子:

  • allowed = "ab", mask = ...0011 (十进制 3)
  • word = "ac", mask1 = ...0101 (十进制 5)

计算 (mask1 | mask)

  • mask1 | mask(...0101 | ...0011)
  • 按位或的结果是 ...0111 (十进制 7)

此时 (mask1 | mask) == mask (7 == 3) 不成立,判断正确,因为 “ac” 包含了不在 “ab” 中的字符 ‘c’。

结论:

mask1 | mask 的结果是包含 mask1mask 中所有为 1 的位。只有当 mask1 中所有为 1 的位在 mask 中也为 1 时(即 mask1mask 的子集),按位或操作并不会引入新的为 1 的位,结果才会和 mask 完全相同。这就是 (mask1 | mask) == mask 判断子集关系的精髓所在。

算法效率分析

使用位运算的方案:

  1. 构建 allowed 的掩码:时间复杂度 O(m)
  2. 遍历 words 数组:n 次循环
  3. 在每次循环中,构建 word 的掩码:时间复杂度 O(k)
  4. 位运算判断:时间复杂度 O(1)

总的时间复杂度为 O(m + n * k),与哈希集合方案看起来相同。然而,位运算在硬件层面执行得非常快,常数时间 O(1) 的操作实际上比哈希集合的查找(平均 O(1),但有额外的计算和内存开销)要快得多。尤其是在字符串长度 k 相对较小的情况下,位运算的优势更明显。

实际应用场景的联想

位运算的这种巧妙应用不仅仅局限于这道算法题。很多需要表示和操作小型集合、状态标志的场景,都可以考虑使用位运算来提升效率和代码简洁度。比如:

  • 权限管理:用一个整数的各个位表示用户拥有的不同权限,通过位运算快速判断用户是否拥有某项或某组权限。
  • 状态标志:在游戏开发或系统编程中,用一个整数的不同位表示对象的不同状态(如“活动”、“可见”、“已锁定”等),通过位运算快速检查和修改状态。
  • 集合操作:对于元素数量有限的小集合,位运算可以高效地实现集合的并集、交集、差集、子集判断等操作。

总结

通过这道简单的算法题,我们看到了位运算在解决特定问题时的强大能力。它不仅可以将复杂的逻辑用简洁的代码表达,更能利用底层硬件的特性实现高效计算。当我们面对问题时,跳出固有的思维模式,思考数据的特性(比如字符数量有限),并尝试运用计算机基础知识(如位运算),往往能发现意想不到的优化空间,写出更具性能优势的代码。位运算就像一个隐藏的宝藏,等待我们在合适的场景下挖掘和利用。

http://www.xdnf.cn/news/5759.html

相关文章:

  • 可视化图解算法37:序列化二叉树-II
  • MCP与GitHub的集成:开发者的“自动化流水线”革命
  • ThreadLocal 详解
  • 2025年阿里云ACP大数据分析师认证模拟试题(附答案解析)
  • JVM对象分配与程序崩溃排查
  • Git的基本操作
  • Jupyter-AI Pandas-AI本地使用功能优化
  • 识别人脸人名,只是窗口的中文乱码待解决
  • 数据库实验报告 SQL SERVER 2008的基本操作 1
  • 调出事件查看器界面的4种方法
  • 从规划到完善,原型标注图全流程设计
  • 国产化芯片ZCC3790--同步升降压控制器的全新选择, 替代LT3790
  • 接口和抽象类的区别
  • uniapp-商城-54-后台 新增商品1
  • A Survey of Learning from Rewards:从训练到应用的全面剖析
  • 计算机网络|| 路由器和交换机的配置
  • 运用数组和矩阵对数据进行存取和运算——NumPy模块 之四
  • Excel表的导入与导出
  • RAGFlow 初步尝试 (01)
  • 基于HTTP头部字段的SQL注入:SQLi-labs第17-20关
  • OpenCV4.8 开发实战系列专栏之 49 二值图像分析 -轮廓外接矩形
  • 我用Deepseek + 亮数据爬虫神器 1小时做出輿情分析器
  • 一文了解JavaScript对象
  • Kotlin与Ktor构建Android后端API
  • RWA开发全解析:技术架构、合规路径与未来趋势
  • Matlab 汽车制动纵向动力学模型和PID控制
  • Webpack中Compiler详解以及自定义loader和plugin详解
  • 用python清除PDF文件中的水印(Adobe Acrobat 无法删除)
  • 机架式服务器是什么?机架式/塔式/刀片式三大服务器类型区别与选型全解析
  • vue3+flask+sqlite前后端项目实战