• 字符串之全文索引


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

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

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

    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的方法来排序后缀数组性能比较低。但是,实际用起来这几乎是最好的方法。这是一个典型的一行顶一万行的例子。后缀数组的应用也不仅仅是做全文索引这样一种功能,欲知详情,请关注下一篇博客:字符串之后缀数组倍增算法。

  • 相关阅读:
    雅虎、网易ajax标签导航效果的实现
    仿淘宝网站的导航标签效果!
    FLASH2007展望
    "运行代码”功能整理发布
    获取远程文件保存为本地文件(精简实用)
    整理JS+FLASH幻灯片播放图片脚本代码
    解决IE更新对FLASH产生影响
    模仿combox(select)控件
    0209.Domino R8.0.x升级指南
    Lotus Domino 中的高级 SMTP 设置Notes.ini相关参数
  • 原文地址:https://www.cnblogs.com/niniwzw/p/2265137.html
Copyright © 2020-2023  润新知