利用索引树去统计单词出现频率

一个统计词频的程序,利用多路树实现

– 一个统计词频的程序,利用多路树实现 –

一种最撇的统计单词的方法就是挨个对比

当然这种方法显然是不可行的,因为假设有n不同的单词,那么单词的对比次数就是 \[ f(n) = 0 \times (n) + 1 \times (n - 1) + 2 \times (n - 2) \dots (n-1) \times 1 + n \times 0 = \sum_{n}^{0}i(n - i) \]

而且这个只是基本的单词的对比 两个单词的比较又不一样,假设\(k_{i}\)表示第\(i\)个单词包含的字母数 所以总的对比次数就是 \[h(n) = \sum_{n}^{0}i(n - 1)k_{i}k_{n - i}\] 复杂度过大 利用树就可以极大的减小复杂度。 算法的思想就是,输入一个单词,然后将这个单词的每一个字母作为树的一个子项,之后不断的去生成这个树。 然后如果发现这个单词已经存在,那么就是将这个单词的数量加1,如果这个单词不存在,那么在树中就会查找失败,然后创建就可以了。

因为树是动态创建的,所以可以起到剪枝作用,而且存储也只是占最小的空间。

先看效果,先是一篇普通的英文文章: 出处:http://www.duwenzhang.com/wenzhang/yingyuwenzhang/20130519/255870.html My father was a self-taught mandolin player. He was one of the best string instrument players in our town. He could not read music, but if he heard a tune a few times, he could play it. When he was younger, he was a member of a small country music band. They would play at local dances and on a few occasions would play for the local radio station. He often told us how he had auditioned and earned a position in a band that featured Patsy Cline as their lead singer. He told the family that after ……..(文章太长,不全部粘贴了,占空间) 最后的统计结果就是:

注意红圈中的,就是出现次数很多的,像was就出现了25次,he出现了50次。 这篇文章大概是一千词左右,在我自己的电脑上几乎就是瞬时刷出来的,说明效率还是不错的。 下面就是具体的实现细节: 树的设计很重要,用一个动态生成的索引树去存储。 所谓的索引树,就是单词前面的字母就是树的根节点,例如is 那么i就是根节点,s就是子节点, 那么一个存了is,would,was的索引树 ,就会是下面的样子 查找is时就是先对比i,然后对比s就将is查出来了。 如果来了一个新单词,例如he,那么树在第一层对比的时候就会发现h这个字母没有,那么他就是自己再创建一个h的节点 接着在h节点的子节点中没有找到e这个子项,那么他又会自己创建一个e子项 这样树的自己学习功能就实现了,接下来的事情只要给树不停的喂数据就可以了。 一下是具体的实现细节: 因为树节点有多少个子节点是不确定,所以这个树的子节点,要可以动态的增加和减少。 因此树的节点,应该就是一个节点的数据,和保存子节点指针的链表组成。

#ifndef _STUDYTREENODE_H_
#define _STUDYTREENODE_H_

#include "seqlist.h"

// Author:Q
// Date:2013/10/11
// Describe:学习树的节点,其实就是一个多路树的节点

template <class Data>
class StudyTreeNode
{
public:
    StudyTreeNode();
    StudyTreeNode(Data data);
    StudyTreeNode(Data data,StudyTreeNode<Data* > next);
    ~StudyTreeNode();

    int        m_iCount;
    Data    m_dData;
    SeqList<StudyTreeNode<Data>* >    m_listNext;
};

#endif

template <class Data>
StudyTreeNode<Data>::StudyTreeNode()
{
    m_iCount = 0;
}

template <class Data>
StudyTreeNode<Data>::StudyTreeNode(Data data)
{
    m_iCount = 0;
    m_dData = data;
}

template <class Data>
StudyTreeNode<Data>::StudyTreeNode(Data data,StudyTreeNode<Data* > next)
{
    m_iCount     = 0;
    m_dData     = data;
    m_listNext     = next;
}

template <class Data>
StudyTreeNode<Data>::~StudyTreeNode()
{
    m_listNext.clearList();
}

这个树中多了一个参数int m_iCount;这个参数的目的就是为了统计单词出现了多少次。 然后就是最重要的部分,树的设计,一下就是树的声明。

#ifndef _STUDYTREE_H_

#define _STUDYTREE_H_

// Author:Q // Date:2013/10/11 // Describe:一个自我学习的树,可以自己扩展节点 // 只需要给他输入数据即可,他会自己生成树

#include "studytreenode.h"

template <class Data>
class StudyTree
{
public:
    StudyTree();
    ~StudyTree();

    void checkData(Data* list,int num);

    void preOrder();
    void postOrder();

    void levelOrder();

    void release();

    void printfTreeCnt();
    void printfResult();

    int getDepth();

protected:
    void checkData(Data* list,int num,int pos,StudyTreeNode<Data>* &node);

    void preOrder(    StudyTreeNode<Data>* node);
    void postOrder(    StudyTreeNode<Data>* node);

    void levelOrder(StudyTreeNode<Data>* node);

    void release(StudyTreeNode<Data>* &node);

    void printfTreeCnt(StudyTreeNode<Data>* node);
    void printfResult(StudyTreeNode<Data>*     node,char* outlist,int num);

    int getDepth(StudyTreeNode<Data>* node,int pos,int &depth);
protected:
    StudyTreeNode<Data>* m_ndHead;
};

#endif

其中最重要的函数就是 void checkData(Data list,int num); void checkData(Data list,int num,int pos,StudyTreeNode* &node); 这两个函数就是生产树的核心代码 上面的函数声明为public,是让用户调用的,下面的函数声明为protected,实际的递归生成函数

这里着重讲void checkData(Data list,int num,int pos,StudyTreeNode &node);函数

第一个参数就是实际的单词数据,一个char 类型(不明白为什么Data 变成char*? 这种无聊的问题不要问,自己看代码)

template <class Data>
void StudyTree<Data>::checkData(Data* list,int num)
{
    int pos = 0;
    checkData(list,num,pos,m_ndHead);
}

template <class Data>
void StudyTree<Data>::checkData(Data* list,int num,int pos,StudyTreeNode<Data>* &node)
{
    if (node == NULL)
    {
        node = new StudyTreeNode<Data>(list[pos]);
        // std::cout << "\nCreate Data = " << list[pos];
    }


    if (num != pos)
    {
        int mark = -1;

        int i;
        for (i = 0;i != node ->m_listNext.getNum();i ++)
        {
            if (node ->m_listNext[i] ->m_dData == list[pos])
            {
                mark = i;
                break;
            }
        }

        if (mark == -1)        // 未发现这个元素,重新创建
        {
            mark = node ->m_listNext.getNum();
            StudyTreeNode<Data>* newnode = new StudyTreeNode<Data>(list[pos]);
            // std::cout << "\nCreate Data = " << list[pos];
            node ->m_listNext.insert(newnode);
        }

        checkData(list,num,pos + 1,node ->m_listNext[mark]);
    }
    else
    {
        node ->m_iCount ++;
    }
}

我觉的这段代码还是没什么好说的,意思很明显。 喂了数据之后,树就生成了。而且因为树是运行过程中生成的,所以本身就带有剪枝的效果。

然后说说如何打印出结果; 之前不是在树的节点中加了一个计数的m_iCount 么,通过判断,只有这个值为0的时候(那么就表示这个是一个单词的结尾了)就将结果输出。然后输出这个值,这样只要将树按照类似于中序遍历的方式,就可以将所有的结果输出了。

template <class Data>
void StudyTree<Data>::printfResult()
{
    int length         = getDepth();
    char* templist     = new char [length];
    int num            = 0;

    int i;
    for (i = 0;i != m_ndHead ->m_listNext.getNum();i ++)
    {
        printfResult(m_ndHead ->m_listNext[i],templist,num);
    }

    delete [] templist;
}

template <class Data>
void StudyTree<Data>::printfResult(StudyTreeNode<Data>* node,char* outlist,int num)
{
    outlist[num] = node ->m_dData;
    num ++;

    int i;
    if (node ->m_iCount == 0)
    {
        for (i = 0;i != node ->m_listNext.getNum();i ++)
        {
            printfResult(node ->m_listNext[i],outlist,num);
        }
    }
    else
    {
        for (i = 0;i != num;i ++)
        {
            std::cout << outlist[i];
        }

        std::cout << ":" << node ->m_iCount << " ";

        for (i = 0;i != node ->m_listNext.getNum();i ++)
        {
            printfResult(node ->m_listNext[i],outlist,num);
        }
    }
}

这里用一个char* 的缓冲,去保存过程,只有当计数器不为0的时候才输出。

下面就是一个测试的demo了

#include "studytree.h"
#include <fstream>
#include <string>

using namespace std;


int main()
{
    StudyTree<char> stytree;

    fstream infile("data.txt");
    string str;

    while (infile >> str)
    {
        int length = str.length();
        char* tmplist = new char [length];

        int j;
        for (j = 0;j != length;j ++)
        {
            tmplist[j] = str[j];
        }

        stytree.checkData(tmplist,length);
    }

    infile.close();

    stytree.printfResult();

    return 0;
}

data.txt保存的就是文章中所有的单词。

这个就是数据的前期处理。

下面就介绍下怎么把刚刚那篇文章处理成这个样子。 先是一篇普普通通的文章,直接从网上copy下来的。 第一步是去除标点。方法很多,我为了复习下lex的使用,写了个简单的正则表达式。 [,.‘“:;?-] {printf(“ “);}

然后数据就会变得没有标点了。 下面就是要把单词大小写统一,然后一行一个单词。

#include <iostream>
#include <string>
#include <fstream>
#include "dos.h"

using namespace std;

int main(int argc,char* argv[])
{
    ifstream infile(argv[1]);        // 读取字符串
    ofstream outfile(argv[2]);        // 写入字符串

    string str;

    int sub = 'a' - 'A';

    while (infile >> str)
    {
        string pstr = "";

        int i;
        for (i = 0;i != str.length();i ++)
        {
            if (str[i] <= 'z' && str[i] >= 'a')
            {
                pstr += str[i];
            }
            else if (str[i] <= 'Z' && str[i] >= 'A')
            {
                pstr += char(str[i] + sub);
            }
            else
            {
            }
        }

        outfile << pstr << "\n";
    }

    infile.close();
    outfile.close();
    return 0;
}

这样一来,数据就会变得符合标准了。