zoukankan      html  css  js  c++  java
  • 字符串之全文索引

        字符串,我现在正在写的就是一个字符串。我们的源代码就是一个字符串,计算机科学里面,一大部分问题都是字符串处理的问题。比如,编译器,就是一个字符串处理程序。还有,搜索引擎,也在处理一个字符串问题。数据库,最难处理的还是字符串部分。索引,一般是一种预处理的中间程序。在我们写代码的时候,往往需要对一个对象进行预处理。这个预处理时间可能比较长,但是,处理完了以后,就能很快的多次的在上面进行查询。比如,你要在一组数里面进行查找,可能先要进行排序,这样速度就会快一些, 排序可以看做是建立索引的一个过程。

        字符串的全文索引,怎么样才能非常的省空间,查找速度也还可以,我这里介绍一种数据结构,叫做后缀数组。

        概念不多说,我就在例子中说明什么东西是后缀数组吧。

    step 1. 所有的后缀:

    string a = ‘aabbaa’;

    找到所有的后缀:

    0 aabbaa

    1 abbaa

    2 bbaa

    3 baa

    4 aa

    5 a

         

          step 2. 对所有的后缀进行排序:

    0 (5)a

    1 (4)aa

    2 (0)aabbaa

    3 (1)abbaa

    4 (3)baa

    5 (2)bbaa

    小括号里面的就是原来的索引值。

    排序后的这个数组就叫做后缀数组。

    下面这个程序,我想让大家更加感性的认识一下后缀数组是什么东西:

    #include <stdio.h>
    #include <stdlib.h>
    //5M
    #define MAX_LEN 1024 * 1024 * 5
    char str[MAX_LEN + 1], *suffix_array[MAX_LEN + 32];
    int readstr();
    int cmpnum, charcmpnum;
    int pstrcmp(const void *a, const void *b);
     
    int main()
    {
        int n, i;
        n = readstr();
        printf("string = %s\n", str);
        printf("All Suffix:\n");
        for (i = 0; i < n; i++)
        {
            suffix_array[i] = str + i;
            printf("%d %s\n", i, suffix_array[i]);
        }
        qsort(suffix_array, n , sizeof(char *), pstrcmp);
        printf("Suffix Array:\n");
        for (i = 0; i < n; i++)
        {
            printf("%d (%d) %s\n", i, suffix_array[i] - str, suffix_array[i]);
        }
        return 0;
    }
     
    int readstr()
    {
        int ch;
        int n = 0;
        while ((ch = getchar()) != EOF)
        {
            str[n++] = (char)ch;
            if (n >= MAX_LEN) {
                break;
            }
        }
        while (str[n-1] == '\r' || str[n-1] == '\n')
        {
            n--;
        }
        str[n] = 0;
        return n;
    }
     
    int pstrcmp(const void *a, const void *b)
    {
        unsigned char *p = *((unsigned char **)a), *q = *((unsigned char **)b);
        unsigned char c1 , c2;
        cmpnum++;
        do {
            c1 = (unsigned char)*p++;
            c2 = (unsigned char)*q++;
            charcmpnum++;
            if (c1 == '\0') {
                return c1 - c2;
            }
        } while (c1 == c2);
        return c1 - c2;
    }

    运行这个程序的方法是输入一个字符串,然后输入EOF 字符串

    比如输入 aabbaa[回车][ctrl+z][回车] 的结果是

    image

    后缀数组就是将所有的字符串后缀进行排序,注意,代码空间复杂度,每一个后缀只是保存了一个指针,并没有复制整个字符串。

    啰嗦了半天,到底这个东西怎么做全文索引呢?你看非常简单的代码,核心的代码就几行,你肯定觉得这个东西没有用。编程珠玑的 第15章 字符串 有关于这个话题的讨论,大家可以去看看。介绍算法不是我写这篇博客的目的,我想写一些超越算法了一些东西,这篇只是开一个头。

    这个后缀数组的所有前缀就是所有的substring(子串)。用过数据库的人可能知道数据库里面的 like 查询 查前缀(like prefix% )要比查后缀(like %suffix )或者任意的查询(like %query%)快很多.原因就是数据库里面是按照字母顺序存储的,这样前缀查询可以二分查找,速度就快了。后缀数组的原理也是这样。一个查询问题转换为一个前缀查询的问题,但是转换的方法却是通过后缀, 很有老庄哲学的韵味。

    下面的程序是读入一个很长的字符串,我测试的是一本23万个英语单词的字典。先随机从这本英文字典里面抽取了5000个单词,然后,把正本字典看做一个字符串,在这本字典里面进行查询,分别用系统自带的 strstr (kMP算法) 和 我们的全文索引(后缀数组进行查询) 看看性能会差多少:

    #define _CRT_SECURE_NO_WARNINGS
     
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <time.h>
    #include <windows.h>
     
    //5M
    #define MAX_LEN 1024 * 1024 * 5
     
    //一个单词的最大长度
    #define MAX_WORD 255
    #define MAX_DICT 500000
     
    //路径的最大长度
    #ifndef MAX_PATH
        #define MAX_PATH 256
    #endif
     
    #define TEST_NUM 5000
    //函数列表
    static int read_str();
    static int read_dict(const char * filepath);
    static int pstrcmp(const void *a, const void *b);
    static char * dirname(const char *path, int count);
    static int range_rand(int min, int max);
    static int prefixcmp(const void *a, const void *b);
    static void test_full_index();
    static void test_strstr();
     
    //全局变量
    char str[MAX_LEN + 1]; //原始字符串
    char *suffix_array[MAX_LEN + 32]; //后缀数组
    char *dict[MAX_DICT]; //测试字典
    int  cmpnum, charcmpnum; //以后可能用来测试性能的计数
    char *query[TEST_NUM];  //查询表达式
    int dictn, strn;
     
    int main(int argc, char *argv[])
    {
        int i;
        if (argc < 2) {
            printf("usage: %s dict_path", argv[0]);
            exit(0);
        }
        //读取字典,构建测试查询
        printf("dict path is: %s\n", argv[1]);
        dictn = read_dict(argv[1]);
        for (i = 0; i < TEST_NUM; i++)
        {
            query[i] = dict[range_rand(0, dictn)];
        }
        //最好free掉dict的内存,对于这样简单的程序,没有多少必要,程序结束以后,自动释放。
     
        //创建后缀数组, 从stdin读取
        strn = read_str();
        for (i = 0; i < strn; i++)
        {
            suffix_array[i] = str + i;
        }
        qsort(suffix_array, strn, sizeof(char *), pstrcmp);
        //测试通过后缀数组建立的索引进行全文查找
        test_full_index();
        //测试通过普通子串查询, 这些算法一般用KMP算法。
        test_strstr();
        return 0;
    }
     
    static void test_full_index()
    {
        int i , t, nofind = 0, find = 0; 
        t = clock();
        for (i = 0; i < TEST_NUM; i++)
        {
            char **index;
            index = (char **)bsearch(query[i], suffix_array, strn, sizeof(char *), prefixcmp);
            if (index == NULL) {
                nofind++;
            } else {
                find++;
            }
        }
        printf("full index, find : %d , not find: %d, cost: %d ms\n", find, nofind, clock() - t);
    }
     
    static void test_strstr()
    {
        int i , t, nofind = 0, find = 0; 
        t = clock();
        for (i = 0; i < TEST_NUM; i++)
        {
            char *index;
            index = strstr(str, query[i]);
            if (index == NULL) {
                nofind++;
            } else {
                find++;
            }
        }
        printf("strstr, find : %d , not find: %d, cost: %d ms\n", find, nofind, clock() - t);
    }
     
    static int read_str()
    {
        int ch;
        int n = 0;
        while ((ch = getchar()) != EOF)
        {
            str[n++] = (char)ch;
            if (n >= MAX_LEN) {
                break;
            }
        }
        while (str[n-1] == '\r' || str[n-1] == '\n')
        {
            n--;
        }
        str[n] = 0;
        return n;
    }
     
    static int read_dict(const char * filepath)
    {
        char buffer[MAX_WORD];
        FILE *fp = fopen(filepath, "r");
        int n = 0;
     
        if (fp == NULL) return 0;
        while (fscanf(fp, "%s", buffer) != EOF)
        {
            int word_len = strlen(buffer) + 1;
            if (n >= MAX_DICT) {
                break;
            }
            dict[n++] = (char *)malloc(word_len);
            memcpy(dict[n-1], buffer, word_len);
        }
        return n;
    }
     
    static int pstrcmp(const void *a, const void *b)
    {
        unsigned char *p = *((unsigned char **)a), *q = *((unsigned char **)b);
        unsigned char c1 , c2;
        cmpnum++;
        do {
            c1 = (unsigned char)*p++;
            c2 = (unsigned char)*q++;
            charcmpnum++;
            if (c1 == '\0') {
                return c1 - c2;
            }
        } while (c1 == c2);
        return c1 - c2;
    }
     
    static int prefixcmp(const void *a, const void *b)
    {
        unsigned char *p = (unsigned char *)a, *q = *(unsigned char **)b;
        unsigned char c1 , c2;
        do {
            c1 = (unsigned char)*p++;
            c2 = (unsigned char)*q++;
            if (c1 == '\0') {
                return 0; //match
            }
        } while (c1 == c2);
        return c1 - c2;
    }
     
    static int range_rand(int min, int max)
    {
        double r = 0;
        int    i;
        double mul = 1;
        for (i = 0; i < 3; i++)
        {
            mul *= 0.0001;
            r += (rand() % 10000) * mul;
        }
        //0 - 1 中的一个随机数
        return (int)(r * (max - min)) + min;
    }

    后面的一些小函数大概比较多,其实主要看main函数就可以了。

    命令行运行:suffix_array dict.txt < dict.txt , 用了一个文件重定向到stdin,附件中有这本测试字典。

    测试结果是:

    image

    可以发现,性能差的挺多了,有1000倍。如果字符串更加的长,差别会更加的大。

    这篇博客还只是个引子,实际上,理论上来说,我们采用qsort的方法来排序后缀数组性能比较低。但是,实际用起来这几乎是最好的方法。这是一个典型的一行顶一万行的例子。后缀数组的应用也不仅仅是做全文索引这样一种功能,欲知详情,请关注下一篇博客:字符串之后缀数组倍增算法。

  • 相关阅读:
    android29
    android28
    android27
    android26
    Dynamics CRM2011 MspInstallAction failed when installing an Update Rollup
    Dynamics CRM Import Solution Attribute Display Name description is null or empty
    The service cannot be activated because it does not support ASP.NET compatibility
    IIS部署WCF报 无法读取配置节“protocolMapping”,因为它缺少节声明
    Unable to access the IIS metabase.You do not have sufficient privilege
    LM算法与非线性最小二乘问题
  • 原文地址:https://www.cnblogs.com/niniwzw/p/2265137.html
Copyright © 2011-2022 走看看