使用Trie树实现的MAP

最近看到一种基于Tire树的map结构,其键值类型为string类型,查找速度很快。文章[1]中分析了这种‘TrieMap’原理,对比了其和std::map,std::unordered_map的查找速度。基于Trie树和文章中提到的TrieMap设计,进行实践,给出自己构造的TrieMap以供参考学习。

Trie树,又称单词查找树、字典树。是一种哈希树的变种,是一种用于快速检索的多叉树结构。Trie树查找的时间复杂度在O(n),实际上这是以消耗更多的内存空间为代价的。以本文构造的Trie树为例,每个节点都包含了128个(因为我们只接受ASCII码在0-127的之间的字符组成的字符串作为键值)子节点。这其实造成了极大的空间浪费,但是带来的好处是:可以使用直接定址的下标访问来判断某个节点是否为空。比如TrieNode *ptr指向的是某个节点,其含有128个子节点,那么对于string str = “hello”;可以直接用ptr->pChild[str[i]]来访问或判断该节点的状态。此外,作为一种树形数据结构,相邻兄弟节点可以存储在一个数组中,但父节点和子节点之间的连接还是通过链表的结构。
本文要讲的TrieMap,其本质不过是在Trie树的节点中增加了一块空间,用于存储对于的Value值,但这也带来了进一步的空间浪费,以test:test123这个键值对为例,前三个节点t->e->s实际上都分配了用于存储Value的空间,但只有t->e->s->t的最后一个节点t存储了对于的键值‘test123’。当插入test:test123后,从空间的角度来看,树的第一层共有128个节点,其中第117个节点t不为空,其余均为空;第二层共有128个节点,其中只有t对应的第二层中e的位置不为空,其余均为空…但是当再次插入新的键值对task:tasker时,第一层t节点位置与键值test共有存储位置,但是第二层会增加128个节点,却仍然只有a对应的存储位置不为空,这也是Trie树的特点,搜索引擎系统用于文本词频统计的原理也基于此。如果键值有公共的前缀,可以节省内存开销。
TrieMap的键值需为字符串类型,或是可以转换成字符串类型,存在一定的局限性。但通过模板类设计的TrieNode节点,可以保证Key对应的Value支持较为宽广的类型。TrieMap对外的接口中,提供了:
(1).用于实例化对象的构造函数;
(2).用于建立TrieMap结构的Insert()函数;
(3).用于查找键值对的函数Find()
(4).用于判断TrieMap是否为空的Empty()函数
其中建立和查找TrieMap的键可为c-style的字符串,以及string类型,实际上代码中只是将string类型统一转换为c-style的字符串。
下面给出实现,并使用CPPUNIT进行测试。

//triemap.h
#ifndef _TRIE_MAP_201612
#define _TRIE_MAP_201612
#include <string.h>
#include <string>
#include <stdlib.h> 
using namespace std;
//ASCII码0-127,不包括扩展的ASCII码,对应每个节点的叶子节点个数
const int NODESIZE = 128;

//节点的数据结构
template <class T>
class TrieNode
{
public:
    TrieNode()
    {
        isLeaf = false;
        ::memset(this->pChild, 0, NODESIZE * sizeof(TrieNode*));
    }
public:
    T value;//节点可以存储的数据类型
    bool isLeaf;//标记该节点是否是叶子节点
    TrieNode* pChild[NODESIZE];//每个节点的子节点
};

template<class T>
class TrieMap
{
public:
    //constructor
    TrieMap();
    //deconstructor
    ~TrieMap();
    //向Tire树中插入新节点
    void Insert(const char *key, const T &value);
    //向Tire树中插入新节点--string
    void Insert(const string &key, const T &value);
    //查找
    bool Find(const char *key,T &value);
    //查找--string
    bool Find(const string &key,T &value);
    //
    bool Empty()const;
private:
    //创建新的TrieNode节点
    TrieNode<T> *newTrieNode();
    //用于删除某节点及其所有的子节点
    void Delete(TrieNode<T> *&ptr);
private:
    TrieNode<T> *root;//Trie树的根节点
};
template<class T>
TrieMap<T>::TrieMap()
{
    this->root = new TrieNode<T>;
    if( NULL == this->root )
        exit(-1);
}
template<class T>
TrieMap<T>::~TrieMap()
{
    if(this->root != NULL)
        this->Delete(root);
}

template<class T>
void TrieMap<T>::Insert(const char *key, const T &value)
{
    TrieNode<T> *ptr = this->root;
    int idx;
    int len = strlen(key);
    for(int i = 0 ; i <  len ;++i)
    {
        idx = key[i];
        //该子节点不存在
        if(ptr->pChild[idx] == NULL)
        {
            ptr->pChild[idx] = this->newTrieNode();
        }
        //指向该节点,继续遍历
        ptr = ptr->pChild[idx];
    }
    ptr->isLeaf = true;
    ptr->value = value;
}

template<class T>
void TrieMap<T>::Insert(const string &key, const T &value)
{
    this->Insert(key.c_str(),value);
}

template<class T>
bool TrieMap<T>::Find(const char *key, T &value)
{
    TrieNode<T> *ptr = this->root;
    int idx;
    int len = strlen(key);
    int i;
    for( i = 0 ; i < len ;++i)
    {
        idx = key[i];
        //输入的字符串,不在普通ASCII码范围
        if(idx < 0 || idx >= NODESIZE)
            return false;
        //节点为空,可能是未找到,也可能是遍历结束了
        if(ptr->pChild[idx] == NULL)
        {
            break;
        }
        //找到该节点,继续遍历 
        else
        {
            ptr = ptr->pChild[idx];
        }
    }
    if(i == len && ptr->isLeaf)
    {
        value = ptr->value;
        return true;
    }
    return false;
}

template<class T>
bool TrieMap<T>::Find(const string &key,T &value)
{
    return this->Find(key.c_str(), value);
}
template<class T>
bool TrieMap<T>::Empty()const
{
    bool flag = true;
    for(int i = 0 ;i < NODESIZE ;++i)
    {
        if(this->root->pChild[i] != NULL)
        {
            flag = false;
            return flag;
        }       
    }
    return flag;
}
template<class T>
TrieNode<T>* TrieMap<T>::newTrieNode()
{
    TrieNode<T> *ptr = new TrieNode<T>();
    if(NULL == ptr)
    {
        exit(-1);
    }
    return ptr;
}

template<class T>
void TrieMap<T>::Delete(TrieNode<T> *&ptr)
{
    if(NULL == ptr)
        return;
    for(int i = 0 ;i < NODESIZE ;++i)
    {
        if(NULL != ptr->pChild[i] )
        {
            this->Delete(ptr->pChild[i]);
        }
    }
    if(NULL != ptr)
    {
        delete ptr;
        ptr = NULL;
    }
}
#endif
//main.cpp
#include <iostream>
#include "triemap.h"
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>

class myTest: public CppUnit::TestFixture 
{
public:
    void  checkInsert()
    {
        triemap.Insert("root","123456");
        bool flag = triemap.Empty();
        CPPUNIT_ASSERT_MESSAGE("TrieMap not Empty!", (!flag) );
    }

    void checkFind() 
    {
        triemap.Insert("admin","admin123");
        triemap.Insert("tester","tester");
        string value;
        triemap.Find("admin",value);
        int len =  value.length();
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Check Find:", len, 8);
    }

    void checkFindNull() 
    {
        string value;
        bool flag = triemap.Find("test",value);
        CPPUNIT_ASSERT_MESSAGE("Could't find test!", (!flag) );
    }

    CPPUNIT_TEST_SUITE( myTest );
    CPPUNIT_TEST( checkInsert );
    CPPUNIT_TEST( checkFind );
    CPPUNIT_TEST( checkFindNull );
    CPPUNIT_TEST_SUITE_END();
private:
    TrieMap<string> triemap;
};

int main ()
{
    //使用辅助宏注册测试套件
    CPPUNIT_TEST_SUITE_REGISTRATION ( myTest );
    CppUnit::Test *test =  CppUnit::TestFactoryRegistry::getRegistry().makeTest();
    CppUnit::TextTestRunner runner;
    runner.addTest(test);
    //运行测试,自动显示进度和结果
    runner.run("",true);
    return 0;
}

在示例中,我们插入了键值对tester:tester,但是没有插入键为test的键值对,虽然test为tester的前缀,在Trie树中共用前缀节点,但是由于在TrieNode节点中用布尔变量isLeaf来标记节点的类型,所以即使test为tester的前缀,也无法查找到,这是符合MAP的定义的,也是与Trie树不同之处。
运行结果:
[root@localhost map]# ./run

OK (3 tests)

[1].Trie实践:一种比哈希表更快的数据结构.http://blog.csdn.net/stevenkylelee/article/details/38343985
[2].Trie树:应用于统计和排序.http://blog.csdn.net/hguisu/article/details/8131559

    原文作者:Trie树
    原文地址: https://blog.csdn.net/cjf_wei/article/details/53561109
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞