程序员必备的十大技能(进阶版)之高阶数据结构与算法(四)

简介: 教程来源 http://qcycj.cn/ 本节介绍海量数据与字符串匹配核心算法:布隆过滤器(高效判存、允许误报)、倒排索引(支撑搜索引擎的词→文档映射)、KMP(线性单模匹配)及AC自动机(O(n)多模匹配)。兼顾原理、代码实现与典型场景,适用于缓存穿透防护、URL去重、全文检索与敏感词识别等工业级应用。

五、海量数据处理算法

当数据量超过单机内存时,需要特殊的算法策略。

5.1 布隆过滤器(Bloom Filter)
布隆过滤器用于判断一个元素是否可能存在于集合中(允许假阳性,不允许假阴性)。

import java.util.BitSet;

public class BloomFilter {
    private BitSet bitSet;
    private int bitSize;
    private int numHashFunctions;

    public BloomFilter(int expectedInsertions, double falsePositiveRate) {
        // 计算最优位数组大小
        this.bitSize = optimalBitSize(expectedInsertions, falsePositiveRate);
        // 计算最优哈希函数数量
        this.numHashFunctions = optimalNumHashFunctions(expectedInsertions, bitSize);
        this.bitSet = new BitSet(bitSize);
    }

    // 位数组大小公式:m = -n * ln(p) / (ln(2))^2
    private int optimalBitSize(int n, double p) {
        return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }

    // 哈希函数数量公式:k = m/n * ln(2)
    private int optimalNumHashFunctions(int n, int m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }

    // 计算多个哈希值(采用双重哈希法)
    private int[] getHashValues(String value) {
        int[] hashes = new int[numHashFunctions];
        int hash1 = value.hashCode();
        int hash2 = murmurHash(value);  // 另一个独立哈希函数

        for (int i = 0; i < numHashFunctions; i++) {
            hashes[i] = (hash1 + i * hash2) & 0x7fffffff % bitSize;
        }
        return hashes;
    }

    private int murmurHash(String value) {
        // 简化的MurmurHash,实际应用中应使用完整实现
        return value.hashCode() ^ (value.hashCode() >>> 16);
    }

    public void add(String value) {
        for (int hash : getHashValues(value)) {
            bitSet.set(hash);
        }
    }

    public boolean mightContain(String value) {
        for (int hash : getHashValues(value)) {
            if (!bitSet.get(hash)) {
                return false;  // 一定不存在
            }
        }
        return true;  // 可能存在
    }
}

应用场景:

防止缓存穿透(查询不存在的数据)

爬虫URL去重

黑名单过滤

5.2 倒排索引(Inverted Index)
搜索引擎的核心数据结构。

class InvertedIndex {
    // 词项 -> 文档ID列表(带位置信息)
    private Map<String, List<Posting>> index = new HashMap<>();

    // 文档ID -> 文档内容
    private Map<Integer, String> documents = new HashMap<>();
    private int nextDocId = 0;

    // 文章项(文档ID + 词频 + 位置列表)
    static class Posting {
        int docId;
        int frequency;
        List<Integer> positions;

        Posting(int docId) {
            this.docId = docId;
            this.frequency = 0;
            this.positions = new ArrayList<>();
        }

        void addPosition(int position) {
            frequency++;
            positions.add(position);
        }
    }

    // 添加文档
    public void addDocument(String content) {
        int docId = nextDocId++;
        documents.put(docId, content);

        // 分词(简化版)
        String[] words = content.toLowerCase().split("\\W+");

        for (int i = 0; i < words.length; i++) {
            String word = words[i];
            index.computeIfAbsent(word, k -> new ArrayList<>());

            // 查找或创建该文档的Posting
            List<Posting> postings = index.get(word);
            Posting targetPosting = null;
            for (Posting p : postings) {
                if (p.docId == docId) {
                    targetPosting = p;
                    break;
                }
            }
            if (targetPosting == null) {
                targetPosting = new Posting(docId);
                postings.add(targetPosting);
            }
            targetPosting.addPosition(i);
        }
    }

    // 布尔查询:AND
    public List<Integer> andQuery(String word1, String word2) {
        List<Integer> result = new ArrayList<>();
        List<Posting> postings1 = index.getOrDefault(word1, Collections.emptyList());
        List<Posting> postings2 = index.getOrDefault(word2, Collections.emptyList());

        int i = 0, j = 0;
        while (i < postings1.size() && j < postings2.size()) {
            int doc1 = postings1.get(i).docId;
            int doc2 = postings2.get(j).docId;

            if (doc1 == doc2) {
                result.add(doc1);
                i++;
                j++;
            } else if (doc1 < doc2) {
                i++;
            } else {
                j++;
            }
        }
        return result;
    }

    // 短语查询
    public List<Integer> phraseQuery(String phrase) {
        String[] words = phrase.toLowerCase().split("\\W+");
        if (words.length == 1) {
            List<Posting> postings = index.getOrDefault(words[0], Collections.emptyList());
            return postings.stream().map(p -> p.docId).collect(Collectors.toList());
        }

        // 获取第一个词的postings作为候选集
        List<Posting> firstPostings = index.getOrDefault(words[0], Collections.emptyList());
        List<Integer> result = new ArrayList<>();

        for (Posting p : firstPostings) {
            int docId = p.docId;
            List<Integer> positions = p.positions;

            // 检查后续词是否在文档中出现且位置连续
            boolean phraseFound = true;
            for (int i = 1; i < words.length; i++) {
                List<Posting> currentPostings = index.getOrDefault(words[i], Collections.emptyList());
                Posting current = null;
                for (Posting cp : currentPostings) {
                    if (cp.docId == docId) {
                        current = cp;
                        break;
                    }
                }
                if (current == null) {
                    phraseFound = false;
                    break;
                }

                // 检查位置关系
                boolean hasContiguous = false;
                for (int pos : positions) {
                    int targetPos = pos + i;
                    if (current.positions.contains(targetPos)) {
                        hasContiguous = true;
                        break;
                    }
                }
                if (!hasContiguous) {
                    phraseFound = false;
                    break;
                }
            }
            if (phraseFound) {
                result.add(docId);
            }
        }
        return result;
    }

    // TF-IDF评分
    public Map<Integer, Double> searchWithTfIdf(String query) {
        String[] terms = query.toLowerCase().split("\\W+");
        int totalDocs = documents.size();

        Map<Integer, Double> scores = new HashMap<>();

        for (String term : terms) {
            List<Posting> postings = index.getOrDefault(term, Collections.emptyList());
            // IDF = log(N / df)
            double idf = Math.log((double) totalDocs / (postings.size() + 1));

            for (Posting p : postings) {
                // TF(简化:词频/文档总词数)
                double tf = (double) p.frequency / documents.get(p.docId).split("\\W+").length;
                scores.merge(p.docId, tf * idf, Double::sum);
            }
        }

        return scores.entrySet().stream()
            .sorted(Map.Entry.<Integer, Double>comparingByValue().reversed())
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, 
                    (e1, e2) -> e1, LinkedHashMap::new));
    }
}

六、字符串匹配算法

6.1 KMP算法(Knuth-Morris-Pratt)
KMP通过前缀函数避免重复比较,时间复杂度O(n+m)。

public class KMP {

    // 构建部分匹配表(next数组/前缀函数)
    private static int[] buildPrefixTable(String pattern) {
        int m = pattern.length();
        int[] next = new int[m];
        next[0] = 0;  // 第一个字符没有真前缀

        int j = 0;  // 前缀长度
        for (int i = 1; i < m; i++) {
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }
            if (pattern.charAt(i) == pattern.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

    // KMP搜索
    public static List<Integer> search(String text, String pattern) {
        List<Integer> matches = new ArrayList<>();
        if (pattern.isEmpty()) return matches;

        int[] next = buildPrefixTable(pattern);
        int n = text.length();
        int m = pattern.length();

        int j = 0;  // 模式串指针
        for (int i = 0; i < n; i++) {
            while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];
            }
            if (text.charAt(i) == pattern.charAt(j)) {
                j++;
            }
            if (j == m) {
                matches.add(i - m + 1);
                j = next[j - 1];  // 允许重叠匹配
            }
        }
        return matches;
    }

    // 可视化前缀表构建
    public static void printPrefixTable(String pattern) {
        int[] next = buildPrefixTable(pattern);
        System.out.println("Pattern: " + pattern);
        System.out.print("Prefix:  ");
        for (int i = 0; i < pattern.length(); i++) {
            System.out.printf("%-3d", next[i]);
        }
        System.out.println();
    }
}

6.2 多模式匹配——AC自动机(Aho-Corasick)
AC自动机结合了Trie树和KMP的思想,可以在O(n)时间内匹配多个模式串。

class AhoCorasickNode {
    AhoCorasickNode[] children = new AhoCorasickNode[26];
    AhoCorasickNode fail;      // 失败指针
    List<String> outputs;      // 该节点对应的模式串
    int depth;

    AhoCorasickNode() {
        outputs = new ArrayList<>();
        depth = 0;
    }
}

public class AhoCorasick {
    private AhoCorasickNode root;

    public AhoCorasick() {
        root = new AhoCorasickNode();
    }

    // 插入模式串
    public void insert(String pattern) {
        AhoCorasickNode node = root;
        for (char c : pattern.toCharArray()) {
            int idx = c - 'a';
            if (node.children[idx] == null) {
                node.children[idx] = new AhoCorasickNode();
                node.children[idx].depth = node.depth + 1;
            }
            node = node.children[idx];
        }
        node.outputs.add(pattern);
    }

    // 构建失败指针(BFS)
    public void buildFailureLinks() {
        Queue<AhoCorasickNode> queue = new LinkedList<>();

        // 第一层节点的fail指向root
        for (int i = 0; i < 26; i++) {
            if (root.children[i] != null) {
                root.children[i].fail = root;
                queue.offer(root.children[i]);
            }
        }

        while (!queue.isEmpty()) {
            AhoCorasickNode current = queue.poll();

            for (int i = 0; i < 26; i++) {
                AhoCorasickNode child = current.children[i];
                if (child != null) {
                    AhoCorasickNode failNode = current.fail;
                    // 寻找失败节点的对应子节点
                    while (failNode != null && failNode.children[i] == null) {
                        failNode = failNode.fail;
                    }
                    child.fail = (failNode == null) ? root : failNode.children[i];

                    // 合并失败节点的输出
                    child.outputs.addAll(child.fail.outputs);
                    queue.offer(child);
                }
            }
        }
    }

    // 搜索文本中所有模式串
    public Map<String, List<Integer>> search(String text) {
        Map<String, List<Integer>> results = new HashMap<>();
        AhoCorasickNode node = root;

        for (int pos = 0; pos < text.length(); pos++) {
            char c = text.charAt(pos);
            int idx = c - 'a';

            // 沿着失败指针跳转,直到找到匹配的子节点
            while (node != root && node.children[idx] == null) {
                node = node.fail;
            }

            if (node.children[idx] != null) {
                node = node.children[idx];
            }

            // 输出所有匹配的模式串
            for (String pattern : node.outputs) {
                int startPos = pos - pattern.length() + 1;
                results.computeIfAbsent(pattern, k -> new ArrayList<>()).add(startPos);
            }
        }

        return results;
    }
}

// 使用示例
// AhoCorasick ac = new AhoCorasick();
// ac.insert("he");
// ac.insert("she");
// ac.insert("his");
// ac.insert("hers");
// ac.buildFailureLinks();
// Map<String, List<Integer>> matches = ac.search("ushers");
// 输出:他匹配到 "she" 在位置1,"hers" 在位置2,"he" 在位置2

来源:
http://oieaw.cn/

相关文章
|
23天前
|
人工智能 自然语言处理 测试技术
《现有Python脚本快速封装OpenClaw Skill指南》
本文针对开发者硬盘中大量闲置Python脚本调用繁琐、复用受限的普遍问题,深入解析OpenClaw Skill体系的底层运行逻辑,澄清“需重写代码”的常见认知误区。文章详细阐述无侵入式封装的完整三步流程,涵盖脚本最小化预处理、语义化描述文件编写、全场景本地验证的关键细节,拆解单一职责、业务逻辑分离等核心设计原则,分享状态保持、多轮对话支持及跨Skill协同的进阶技巧,为开发者提供可直接落地的实战指南,揭示Skill体系重构代码复用方式的深层意义与生态价值。
150 0
|
23天前
|
人工智能 开发工具 开发者
终端里跑 3D 老鼠,桌面窗口成摆锤;AI 大佬新公司估值百亿起
上周技术圈的信息挺杂,但有几条线索值得放在一起看。 一边,AI 产品继续往具体工作流里走:Claude Code 开始支持 Agent View,OpenAI 把 Codex 带到移动端;另一边,开发者社区继续整活:有人给 Claude Code 做实体旋钮,有人做 Claude 用量桌面仪表盘,还有人把终端做成能显示 3D 老鼠的玩具。
207 1
终端里跑 3D 老鼠,桌面窗口成摆锤;AI 大佬新公司估值百亿起
|
23天前
|
存储 安全 Java
【Java基础】泛型:泛型擦除、通配符、上下界限定(附《思维导图》+《面试高频考点清单》)
本文系统梳理了Java泛型的核心知识体系,主要内容包括: 泛型概述:介绍了泛型的定义、本质和三大优势(类型安全、代码复用、可读性),以及泛型类、接口和方法的三种使用形式。 泛型擦除:深入解析了Java泛型实现的核心机制,包括擦除规则(无界类型擦除为Object,有界类型擦除为第一个边界类型)、擦除带来的问题(如无法使用instanceof、创建泛型数组等)及其解决方案。 泛型通配符:详细讲解了三种通配符类型(无界通配符、上界通配符和下界通配符)的语法、语义和使用场景。
|
23天前
|
机器学习/深度学习 人工智能 算法
用好 Codex Goal,关键就这三步
Codex 新增 /goal 命令,支持目标驱动的Agent式循环:设定可量化目标(如“运行时间降20%且测试全通过”)、构建短反馈闭环、用PLAN/EXPERIMENTS等Markdown文件持久化记忆。三要素缺一不可,方能真正释放长任务自动化潜力。
673 1
用好 Codex Goal,关键就这三步
|
1天前
|
消息中间件 人工智能 Apache
|
1天前
|
存储 人工智能 运维
千亿级 AI 搜索的效能实战:从混合检索到 Agentic RAG 的三年实战
本文为2026 Elastic中国大会演讲实录,直击千亿级AI搜索三大挑战:搜索融合(关键词+向量+稀疏检索原生一体)、极致效能(冷热分层、硬件降级、自研FalconSeek引擎)与Agentic RAG演进(结构化知识图谱+智能体自主推理),揭示企业级AI搜索从“能用”到“好用”再到“自进化”的实战路径。
259 8
|
1天前
|
存储 缓存 Java
程序员必备的十大技能(进阶版)之底层计算机原理(二)
教程来源 http://bncne.cn/ 本文详解计算机内存层次结构与指令集原理:从寄存器到硬盘的存储金字塔,剖析缓存行、伪共享、内存一致性模型及屏障机制;涵盖x86-64汇编基础、C/Java代码映射、JNI内联汇编调用,助力高性能编程优化。
|
23天前
|
供应链 安全 测试技术
如何选择CNAS与CMA双资质渗透测试机构?
数字化转型下,企业需通过专业渗透测试应对复杂网络威胁。选择具备CNAS与CMA双资质的机构,可满足等保测评、合规审查及平台入驻要求,其报告具法律效力。服务覆盖Web、移动APP、PC软件及API,融合自动化与人工深度测试,严格遵循OWASP、PTES等国际标准,提供漏洞诊断、风险还原与闭环修复。
|
23天前
|
Kubernetes 数据安全/隐私保护 容器
【架构实战】Helm Chart应用部署最佳实践
Helm是Kubernetes的包管理器: 核心概念: Chart:应用包 Repository:Chart仓库 Release:部署实例
98 2
|
23天前
|
程序员 开发工具 git
初级程序员必备的十大技能之规范编码与团队协作(三)
教程来源 http://qcycj.cn/ 本节系统阐述高效团队协作核心实践:从精准提问、高效会议、知识共享到冲突化解,并配套自动化工具链(Prettier/ESLint/Husky/Commitlint/GitHub Actions),全面提升研发协同质量与工程规范性。