sqlite的分词器模块需要对输入的字符串映射为系统中的标示符,其对关键字映射使用了hash算法,其对hash冲突的解决十分巧妙。
1:最常规的解决办法:写一堆判断对每个输入字符串判断是否匹配,如果匹配就映射为系统中的关键字。由于sqlite的关键字有100多个,如果每个字符串进行比较判断,无疑效率很低
2:使用hash算法:
首先构造一个散列函数,该函数计算字符串得到一个hash映射表的入口地址。
在入口地址中存放对应的记录.
由于对不同的字符串进行散列后可能得到一个相同的入口地址,这就要求我们要对冲突进行解决.
2.1以前遇到类似的问题,基本使用了链地址法来解决冲突的问题。例如:
typedef struct map_s map_t;
struct map_s
{
unsigned char *pStr;
unsigned char token;
}
unsigned char getTken(unsigned char *pStr, int len)
{
const unsigned char map[M][N] = {
{{"token11",TK_TOKEN11},...,{"token1n",TK_TOKEN1N}},
....
{{"tokenm1",TK_TOKENM1},...,{NULL,TK_ERR}},
};
hashCode = getHashCode(pStr);
for(int i=0;i<N;i++)
{
if(map[hashCode][i].pStr equals pStr)
return map[hashCode][i].token
}
return TK_ERR;
}
该方法简单容易理解,如果散列函数较好,基本可以寻址复杂度为O(1),但该方法对于没有被映射的位置存在一些空间的浪费。
2.2 sqlite使用了一个字符串缓冲区来包含所有的可能关键字,该缓冲区是被压缩过的。
static const char zText[544] =
"ABORTABLEFTEMPORARYADDATABASELECTHEND"
....
"UNIQUEUSINGVACUUMVALUESVIEWHEREVIRTUAL";
关键字"abort"和"table"公用“ABORTABLE”缓冲区间, 对于每个关键字都存在一个该缓冲区的入口位置,以该入口位置进行字符串比较,判断是否冲突。
static const unsigned short int aOffset[117] = {
0, 4, 7, 10, 10, 14, 19, 21, 26, 27, 32, 34, 36,
....
476, 481, 486, 494, 496, 500, 505, 511, 516, 522, 528
};
hash映射表保存了每个计算的hash值的可能入口位置,如果该入口位置的缓冲区字符串和待转换字符串等同,表明没有冲突,直接获取对应的aCode映射值。
static const unsigned char aHash[127] = {
92, 80, 107, 91, 0, 4, 0, 0, 114, 0, 83, 0, 0,
....
15, 0, 116, 51, 56, 0, 2, 55, 0, 111,
}; static const unsigned char aCode[117] = {
TK_ABORT, TK_TABLE, TK_JOIN_KW, TK_TEMP,
....
TK_WHERE, TK_VIRTUAL,
};
如果出现冲突,使用了“再散列法”来解决,对冲突的地址在aNext重新分配一个没有使用过的入口位置,该入口位置的缓冲区字符串和待转换字符串等同。
static const unsigned char aNext[117] = {
0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0,
....
0, 14, 27, 78, 0, 57, 89, 0, 35, 0, 62, 0,
};
该hash算法设计的比较巧妙,使用了一个压缩的字符串缓冲区结合“再散列法”完美的解决了hash冲突问题。在空间和查找效率上都很优秀。