别只知道暴力循环!我从用户名校验功能中领悟到的高效字符集判断法(1684. 统计一致字符串的数目)

别只知道暴力循环!我从用户名校验功能中领悟到的高效字符集判断法 😎

大家好,日常开发中,我们经常会遇到一些看似不起眼,却能成为性能瓶颈的小模块。今天,我想和大家分享一个我亲身经历的故事,关于如何从一个简单的“用户名合法性校验”功能,一步步挖掘出三种不同层次的算法解法,最终get到性能优化的精髓。

我遇到了什么问题

事情是这样的,我正在负责一个新项目的用户注册模块。产品经理(PM)跑过来对我说:“为了系统的安全和统一,我们规定新注册的用户名,只能包含我们指定的一组字符。这组字符未来可能会在后台调整。”

听起来是个小需求嘛,不就是个字符串校验?我当时想。

PM接着补充道:“后台会有个功能,可以批量导入一批用户名,我们需要快速地筛选出所有合法的用户名。”

这个场景就变得有意思了。我需要一个规则集allowed 字符串)去校验一大批待测字符串(words 数组)。如果每次校验一个单词,都去遍历一次规则集,当单词数量和长度上来后,性能开销会非常大。

这让我想起了 LeetCode 上的这道题,简直就是对我这个场景的完美抽象:

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

给你一个由不同字符组成的字符串 allowed 和一个字符串数组 words 。如果一个字符串的每一个字符都在 allowed 中,就称这个字符串是 一致字符串 。请你返回 words 数组中 一致字符串 的数目。

在动手编码前,我们先来仔细品味一下题目的“提示”:

  • 1 <= words.length <= 10^4, 1 <= words[i].length <= 10: 单词数量不少,但每个单词都不长。这告诉我们,算法的效率重点应该放在如何快速判断“单个字符”是否合法上,而不是纠结于单词长度。
  • 1 <= allowed.length <= 26, allowed 中的字符 互不相同, 只包含小写英文字母: 这是最重要的三条信息! 它把问题从一个通用的字符串匹配,降维到了一个固定大小(26个字母)的集合问题上。这是我们施展优化“魔法”的关键。

好了,背景介绍完毕,让我们看看我是如何用三套“组合拳”来解决这个问题的。

我是如何用算法“组合拳”解决的

解法一:哈希集合(HashSet),万能的“存在性”检查器

这是我脑海里冒出的第一个想法,也是最符合直觉的解法。要反复检查一个字符在不在 allowed 里,如果每次都用 String.contains(),效率太低。更好的办法是把 allowed 的字符先存到一个查询飞快的数据结构里。HashSet 就是为此而生的!

先把所有“合法”的字符都请进一个 HashSet,它就像一个VIP俱乐部的门禁系统。然后,对于每一个待校验的用户名,我们检查它的每个字符是不是这个俱乐部的会员。

import java.util.HashSet;
import java.util.Set;/** 思路:使用哈希集合。将 allowed 字符串的字符存入 HashSet,以实现 O(1) 的快速查找。* 然后遍历每个单词,检查其所有字符是否都在 HashSet 中。* 时间复杂度:O(L + S),其中 L 是 allowed 的长度,S 是 words 中所有字符的总数。* 空间复杂度:O(L),由于 L<=26,可视为 O(1)。*/
class Solution {public int countConsistentStrings(String allowed, String[] words) {// 1. 预处理,构建“VIP俱乐部”// 为什么用 HashSet?因为它提供了平均时间复杂度为 O(1) 的 contains() 方法,// 对于需要频繁检查“某元素是否存在于集合中”的场景,它是最理想的选择。// 比起 List 的 O(L) 查找,简直是降维打击。Set<Character> allowedSet = new HashSet<>();for (char c : allowed.toCharArray()) {allowedSet.add(c);}int consistentCount = 0;// 2. 遍历所有待校验的用户名for (String word : words) {boolean isConsistent = true;// 3. 检查用户名的每个字符for (char c : word.toCharArray()) {// 如果在“VIP俱乐部”里查不到这个字符,说明是非法用户,直接标记并“拉黑”if (!allowedSet.contains(c)) {isConsistent = false;break; // 重要的优化:一旦发现不合法的,立即停止对当前单词的检查!}}// 如果这个用户名所有字符都通过了检查,计数器加一if (isConsistent) {consistentCount++;}}return consistentCount;}
}

复杂度

时间复杂度:O(L + S)。L 是 allowed 的长度,S 是 words 中所有字符的总数。构建 HashSet 的时间是 O(L)。之后遍历所有单词的所有字符,总字符数为 S,每次查询是 O(1),所以检查部分是 O(S)。总计 O(L + S)。

空间复杂度:O(L)。HashSet 需要存储 allowed 中的字符。因为题目限制 L <= 26,所以空间复杂度可以认为是常数级别的 O(1)。

这个解法又稳又准,代码清晰,足以应对大多数场景。但当我看到提示里的“26个小写字母”时,一个念头闪过:我真的需要 HashSet 这个“重型武器”吗?有没有更轻量、更快的办法?😉

解法二:布尔数组,小范围数据的“降维打击”

HashSet 很好,但它内部有哈希计算、冲突处理等机制,有一定开销。既然我们已经知道所有字符都在’a’到’z’这个小范围内,就可以用一个简单的数组来创建一个更快的“门禁系统”。

我们可以创建一个长度为26的布尔数组,每个位置对应一个字母。array[0] 代表 ‘a’,array[1] 代表 ‘b’,以此类推。如果 allowed 里有 ‘c’,我们就把 array[2] 设为 true。检查时,只需要看对应位置是不是 true 就行了,这是最纯粹的 O(1) 操作!

/** 思路:使用布尔数组模拟哈希。利用字符集限定在26个小写字母的特性,* 创建一个长度为26的布尔数组作为查找表,比 HashSet 更高效。* 时间复杂度:O(L + S),但常数时间更小。* 空间复杂度:O(1),数组大小固定为26。*/
class Solution {public int countConsistentStrings(String allowed, String[] words) {// 1. 预处理,构建一个超快的“字母通行证”// 为什么用 boolean 数组?因为当键的范围已知且很小时(26个字母),// 数组提供了最快的直接寻址访问(真正的 O(1)),避免了 HashSet 的哈希计算和潜在冲突开销。// c - 'a' 这个操作是精髓,它能巧妙地把字符映射到 0-25 的索引上。boolean[] isAllowed = new boolean[26];for (char c : allowed.toCharArray()) {isAllowed[c - 'a'] = true;}int consistentCount = 0;outerLoop: // 使用标签可以更清晰地跳出外层循环,直接检查下一个单词for (String word : words) {for (char c : word.toCharArray()) {// 如果“通行证”上这个字母没盖章,说明不合法if (!isAllowed[c - 'a']) {continue outerLoop; // 直接跳到下一个单词的检查}}// 如果一个单词的内层循环能正常走完,说明它是合法的consistentCount++;}return consistentCount;}
}

复杂度

时间复杂度:O(L + S)。分析同上。但由于数组访问是直接内存寻址,它的实际运行速度通常会比 HashSet 快。

空间复杂度:O(1)。isAllowed 数组的大小是固定的26,不随输入规模变化,是纯粹的常数空间。

这个解法是我当时“恍然大悟”的瞬间!它让我明白了,充分利用题目给定的约束条件,是算法优化的不二法门。这个方法已经非常高效了,但我还想挑战一下自己,有没有办法把空间再压榨一下?

解法三:位运算(Bitmask),高手的极简主义

布尔数组 [true, true, false, ...] 本质上不就是一串二进制 110... 吗?既然如此,我为什么不直接用一个整数来表示这26个状态位呢?一个 int 类型有32位,足够放下26个字母的“通行证”了!

这就是位运算的魅力。我们可以用一个整数的二进制位来代表集合。第0位是1,代表’a’合法;第1位是1,代表’b’合法……

  • 添加字符:用“按位或 |”操作。
  • 检查字符:用“按位与 &”操作。
/** 思路:位运算。将 allowed 字符串表示为一个位掩码(bitmask),然后检查* 每个单词的每个字符对应的位是否在该掩码中为1。这是空间和时间效率都极高的做法。* 时间复杂度:O(L + S)。* 空间复杂度:O(1)。*/
class Solution {public int countConsistentStrings(String allowed, String[] words) {// 1. 预处理,构建一个整数版的“通行证”// 为什么用 int 做位掩码?因为它紧凑、高效。26个字母的状态刚好可以用一个32位整数表示。// `1 << (c - 'a')` 生成一个只有 c 对应位是 1 的数字(例如 c='b', 结果是二进制的10)。// `|=` (按位或赋值) 将这个位设置为1,而不影响其他位。int allowedMask = 0;for (char c : allowed.toCharArray()) {allowedMask |= (1 << (c - 'a'));}int consistentCount = 0;outerLoop:for (String word : words) {for (char c : word.toCharArray()) {// 检查字符 c 的“通行证”位是否为1// `(allowedMask & (1 << (c - 'a')))` 会提取出 c 对应的位。// 如果结果是 0,说明 allowedMask 在这一位上是0,即字符不合法。if ((allowedMask & (1 << (c - 'a'))) == 0) {continue outerLoop; // 验证失败,检查下一个单词}}consistentCount++;}return consistentCount;}
}

复杂度

时间复杂度:O(L + S)。分析同上。位运算直接在CPU层面执行,通常是所有解法中实际运行最快的。

空间复杂度:O(1)。只用了一个额外的 int 变量,空间利用达到了极致。

这个解法虽然代码看起来有点“黑话”,但它展示了对计算机底层工作原理的深刻理解。在追求极致性能的场景下,位运算是你的屠龙宝刀!

举一反三:这个思想还能用在哪?

这个“预处理查询集”的思想在开发中无处不在:

  • API网关:校验请求参数是否在允许的枚举值范围内。
  • 文件上传:检查上传文件的后缀名是否在白名单中(如 .jpg, .png, .gif)。
  • 敏感词过滤:将敏感词库预处理成高效的查找结构(如Trie树),快速判断文本中是否包含敏感词。

练练手,更熟练

如果你对这类问题产生了兴趣,不妨挑战一下这些“兄弟”题目,它们都能用上类似的思路:

  • 771. 宝石与石头:本题的简化版,绝佳的入门练习。
  • 242. 有效的字母异位词:同样使用数组作为哈希表来统计字符频率。
  • 389. 找不同:位运算解法在此题中大放异彩。

希望我这次从真实项目到算法优化的心路历程能给你带来一些启发。记住,优雅的解决方案,往往源于对问题本质和约束条件的深刻洞察。下次再遇到类似问题,别只知道暴力循环啦!😉

解法对比

特性解法一 (HashSet)解法二 (布尔数组)解法三 (位运算)
核心思想通用哈希集合查找数组直接寻址模拟哈希整数位表示集合
代码简洁度高,API直观易用中,逻辑清晰低,需要理解位运算
执行效率高效非常高效(优于HashSet)极致高效(硬件级优化)
时间复杂度O(L + S)O(L + S)O(L + S)
空间复杂度O(L) (可视为O(1))O(1) (固定26)O(1) (固定1个int)
普适性,可用于任何类型元素,仅限于键可映射为连续整数,仅限于键空间极小

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/90380.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/90380.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣面试150题--在排序数组中查找元素的第一个和最后一个位置

Day 85 题目描述思路 当 nums[mid] < target 时&#xff0c;说明目标值在右侧&#xff0c;移动左指针 left mid 1 当 nums[mid] > target 时&#xff0c;说明目标值可能在当前位置或左侧&#xff0c;移动右指针 right mid - 1 循环结束后&#xff0c;left 指针会指向第…

C++实战:人脸识别7大核心实例

计算机视觉实例应用 基于C++的人脸识别实例 以下是一些基于C++的人脸识别实例的示例和实现方法,涵盖了多种技术和库的应用。这些例子可以帮助开发者快速上手并实现人脸识别功能。 OpenCV 基础人脸检测 使用OpenCV的预训练模型进行人脸检测是入门级示例。OpenCV自带Haar级联…

Uniapp中使用vue3语法

在setup语法糖中调用uniapp的页面生命周期 <script setup>import { onShow } from "dcloudio/uni-app"onShow(() > {//hanlder...}) </script>vue2混入在vue3中建议使用组合式API 新建baseHook.js import { ref } from "vue"; export fu…

C++vector(2)

2.vector深度剖析及模拟实现 2.1std::vector的核心框架接口的模拟实现bit::vector vector的模拟实现 2.2 使用memcpy拷贝问题 假设模拟实现的vector中的reserve接口中&#xff0c;使用memcpy进行的拷贝&#xff0c;以下代码会发生什么问题&#xff1f; int main() {gxl::ve…

IPSec VPN -- 野蛮模式

一、野蛮模式简介野蛮模式VPN是指IPsec VPN中IKE协商采用野蛮模式&#xff08;Aggressive Mode&#xff09;的虚拟专用网络。它是IKE第一阶段协商的一种方式&#xff0c;与主模式相对&#xff0c;具有协商速度快但安全性稍低的特点。以下是具体介绍&#xff1a;1、工作原理&…

rk3588开发板使用硬件编码处理视频

开发板默认下载的ffmpeg是通用版&#xff0c;无法调用rk3588的硬件编码器&#xff0c;视频编码效率低。 nyanmisaka开发了用于jellyfin的ffmpeg&#xff0c;支持rk3588硬件编码器&#xff0c;编译方法&#xff1a; https://github.com/nyanmisaka/ffmpeg-rockchip/wiki/Compil…

`neutron router-gateway-set` 操作失败的可能原因及解决方案

根据提供的错误信息和搜索结果&#xff0c;neutron router-gateway-set 操作失败的可能原因及解决方案如下&#xff1a;一、常见错误原因数据库字符集配置问题&#xff08;中文名支持&#xff09; 表现&#xff1a;若路由器名称包含中文字符&#xff0c;可能因数据库字符集非UT…

(一)ZooKeeper 发展历史

✨博客主页&#xff1a; https://blog.csdn.net/m0_63815035?typeblog &#x1f497;《博客内容》&#xff1a;.NET、Java.测试开发、Python、Android、Go、Node、Android前端小程序等相关领域知识 &#x1f4e2;博客专栏&#xff1a; https://blog.csdn.net/m0_63815035/cat…

OpenCV快速入门之CV宝典

文章目录OpenCV的基础应用一、OpenCV简介&#xff1a;1.1 OpenCV 优势1.2 OpenCV-Python二、环境安装2.1 环境导入三、图像表示3.1 颜色空间&#xff08;Color Space&#xff09;3.2 具体说明3.3 图像在计算机中的表示四、基本图像操作4.1 创建窗口**1. 核心窗口行为控制**cv.W…

LangChain4j 两种类型API

LangChain4j operates on two levels of abstraction: &#xfeff;LangChain4j 提供了两种类型API抽象Low level. At this level, you have the most freedom and access to all the low-level components such as ChatModel, UserMessage, AiMessage, EmbeddingStore, Embedd…

CLI 与 IDE 编码代理比较:提升开发效率的两种路径

引言 在当今快速发展的软件开发领域&#xff0c;人工智能编码助手已成为开发者工具箱中不可或缺的一部分。根据行业报告&#xff0c;使用AI编码助手可以将开发速度提高55%以上&#xff0c;同时显著提升代码质量。目前市场上主要有两种类型的编码代理&#xff1a;集成在IDE中的代…

【STM32】FreeRTOS 任务的创建(二)

这篇文章在于 详细解释 FreeRTOS 中任务的创建过程&#xff0c;包括任务创建的本质过程、API 详解、两种创建方式&#xff08;动态/静态&#xff09;、任务函数规范、常见错误及实践建议。 这里参照&#xff1a;RTOS官方文档&#xff1a;https://www.freertos.org/zh-cn-cmn-s…

软考 系统架构设计师系列知识点之面向服务架构设计理论与实践(9)

接前一篇文章:软考 系统架构设计师系列知识点之面向服务架构设计理论与实践(8) 所属章节: 第15章. 面向服务架构设计理论与实践 第3节 SOA的参考架构 15.3 SOA的参考架构 IBM的Websphere业务集成参考架构(如图15-2所示,以下简称参考架构)是典型的以服务为中心的企业集…

分区域材料设计:主承重区 / 次承重区 / 足弓区的弹性参数与刺激强度匹配

你是否总在为足部酸痛、膝盖不适或腰背僵硬烦恼&#xff1f;穿了昂贵的缓震跑鞋&#xff0c;用了定制矫形器&#xff0c;问题却反复出现&#xff1f;今天&#xff0c;我们要颠覆一个流传百年的“常识”——脚不是脆弱的“需要被保护的对象”&#xff0c;而是被错误的设计“惯坏…

使用Qt下QAudioOutput播放声音

导读本项目目的是使用QAudioOutput播放声音 &#xff0c;音频数据来源为ffmpeg解码后的音频数据。Qt音频播放类说明 QAudioFormatQAudioFormat是Qt多媒体框架中用于定义音频格式的核心类&#xff0c;用于设置音频数据的参数&#xff0c;确保与硬件设备兼容。其主要功能和参数如…

日语学习-日语知识点小记-构建基础-JLPT-N3阶段(9):ようなN

日语学习-日语知识点小记-构建基础-JLPT-N3阶段&#xff08;9&#xff09;&#xff1a;ようなN 1、前言&#xff08;1&#xff09;情况说明&#xff08;2&#xff09;工程师的信仰2、知识点&#xff08;&#xff11;&#xff09;复习&#xff08;&#xff12;&#xff09;复习&…

洛谷P1512 伊甸园日历游戏

一开始&#xff0c;我发现有“必胜策略”&#xff0c;就知道是博弈论&#xff0c;然后看了两种操作&#xff08;月份1和天数1&#xff09;&#xff0c;于是想到用记忆化搜索找出所有的可能性 &#xff0c;但不知道怎么判断当前是否为先手必胜/必败态&#xff0c;使用了TJ方法后…

Kafka——消费者组到底是什么?

引言在分布式系统中&#xff0c;消息中间件的核心价值在于高效地连接生产者与消费者&#xff0c;实现数据的可靠传递。然而&#xff0c;传统消息引擎面临一个两难困境&#xff1a;如何在“消息不重复消费”与“系统可扩展性”之间找到平衡&#xff1f;点对点模型&#xff08;如…

新mac电脑软件安装指南(前端开发用)

1. 下载git 未下载git直接下载homebrew也会提示你下载git 2. 下载homebrew 介绍&#xff1a; Homebrew 是 macOS 和 Linux 系统的开源包管理器‌&#xff0c;通过命令行实现软件的快速安装、更新和管理&#xff0c;极大简化了开发者及普通用户的工作流程。 命令&#xff1a;…

【HarmonyOS】ArkUI 布局与容器组件

目录前言一、线性布局(Column/Row)1.先布局后内容2.元素在主轴上的排列方式3.元素在交叉轴上的排列方式二、层叠布局(Stack)1.开发布局2.对齐方式三、弹性布局(Flex)四、创建列表(List)五、创建轮播(Swiper)1.基本用法2.常用属性3.样式自定义六、选项卡Tabs1.基本用法2.常用属性…