位运算 + 滚动哈希实现轻量文本搜索引擎

本节目标

位运算妙用:用一个整数的每一个 bit 表示"某元素是否在集合",适合小规模去重/状态压缩。

// 运行环境:Node.js 14+
// 保存为 dsa-l26.js,执行:node dsa-l26.js

// 用位掩码表示集合:第 i 位=1 表示元素 i 在集合内
function setOps() {
  let A = 0, B = 0
  for (const i of [0, 2, 4]) A |= (1 << i) // 加入元素
  for (const i of [1, 2, 3]) B |= (1 << i)
  const union = A | B          // 并集
  const inter = A & B          // 交集
  const diff = A & ~B          // 差集(在 A 不在 B)
  return { union, inter, diff }
}
console.log(setOps()) // { union: 31, inter: 4, diff: 21 }
// 解释:A={0,2,4}=bits 10101=21, B={1,2,3}=bits 01110=14
// union=31(11111), inter=4(00100=元素2), diff=21(10101=元素0,2,4)

// 两个集合相似度(Jaccard):交集大小 / 并集大小
function jaccard(a, b) {
  const inter = (a & b).toString(2).split('').filter(c => c === '1').length
  const union = (a | b).toString(2).split('').filter(c => c === '1').length
  return union ? inter / union : 0
}
console.log(jaccard(21, 14).toFixed(2)) // 0.20(交集1个/并集5个)

滚动哈希(Rabin-Karp):把字符串当作"_base 进制数"求模,窗口滑动时 O(1) 更新哈希,高效多模式/子串检索。

// 在文本 txt 中找出所有出现模式 pat 的起始下标
function rabinKarp(txt, pat) {
  const base = 131, mod = 1e9 + 7
  const m = pat.length, n = txt.length
  if (m > n) return []
  const pow = (() => { let p = 1; for (let i = 0; i < m - 1; i++) p = (p * base) % mod; return p })()
  const hash = s => { let h = 0; for (const ch of s) h = (h * base + ch.charCodeAt(0)) % mod; return h }
  const ph = hash(pat)
  let th = hash(txt.slice(0, m))
  const res = []
  if (th === ph && txt.slice(0, m) === pat) res.push(0)
  for (let i = m; i < n; i++) {
    // 滑出 txt[i-m],滑入 txt[i]
    th = (th - txt.charCodeAt(i - m) * pow % mod + mod) % mod
    th = (th * base + txt.charCodeAt(i)) % mod
    if (th === ph && txt.slice(i - m + 1, i + 1) === pat) res.push(i - m + 1)
  }
  return res
}
console.log(rabinKarp('abracadabra', 'abra')) // [0, 7]

名词解释

课后练习

  1. 位掩码 A | (1 << i) 为什么能"加入元素 i"?
    • 答案:1 << i 是第 i 位为 1 的掩码,| 或运算把 A 的第 i 位置 1,其余位不变,正好表示"集合中加入 i"。
  2. rabinKarp 里为什么哈希相等后还要 txt.slice(...) === pat 再确认?
    • 答案:哈希可能因取模发生"哈希冲突"(不同串同余),所以哈希相等只是疑似匹配,必须用真实子串比对一次排除冲突,保证正确。

总结

这一节是整门课的"收官彩蛋"——它把前面零散的算法(位运算、哈希、字符串)缝合成一个能用的小工具,恰好说明"高级功能=基础算法的组合"。位运算最迷人之处是用一个整数的 32 个 bit 就能表示一个集合,状态压缩在棋盘/子集枚举里威力巨大;而滚动哈希则展示了"如何把字符串比较从 O(模式长) 降成 O(1) 均摊",是多模式串检索、 plagiarism 检测、DNA 匹配的基石。我想强调 Rabin-Karp 里那个常被忽略的细节:哈希相等不等于真的相等,必须"再比一次"排除冲突——这是所有哈希类算法的通用纪律。整门 DSA 走到这里,希望你体会到的不是"我又背了俩算法",而是"我能用这些零件搭出真正有用的东西"。算法学习的最终目的,从来不是刷题,而是拥有的这套"把复杂问题拆成标准零件"的思维肌肉。