背景介绍
最近使用Python开发项目为主,当使用到字典时感觉非常方便实用。那么好奇心就驱使我要搞清楚字典是怎么实现的。为了真正的搞清楚字典的实现就不得不使用C语言来实现一遍,为此我查了一些资料现在总结一下。
字典简述
字典也被称为关联数组,还称为哈希数组等。实现的原理一般是有一个键值对,通过键可以索引到值。很多工具都使用这种方式保存数据,例如redis/memcached/mongo等。所以键是唯一的,要是实现字典的快速查询肯定不能使用字符串遍历对比的方式实现。那样实现的字典会非常非常的慢。我们都知道在数组中使用下标索引可以快速的得到对应值,所以我们需要做的就是怎样把键计算出一个唯一值并且这个值要唯一还要在我们的数组索引值范围呢。举例子说,当一个键为hello
的时候,我的数组最大索引是1024
这个范围呢!我们的键值对可以有1024
对。那么如果按照ascii
值对hello
进行一个简单的加法计算我们会得到什么呢?下面运行一段c
程序看看。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int sum_ascii(char * key){
unsigned long sum = 0;
unsigned long i;
for(i = 0; i < strlen(key);i++){
sum+= key[i];
}
return sum;
}
int main(int argc, char * argv[]){
int key_id = sum_ascii("hello");
printf("The key id %d
", key_id);
return 0;
}
看到这个532
你会觉得正好,至少没有超过1024
这个索引范围,可以用了。不要着急,我们继续将hello
改成hellohashtable
看看会得到多少值。
糟糕了!超出索引范围了,你可能会想到两种解决方案。
*1,增加数组大小。
*2,写一篇说明书,说明你的这个键不能超过多少个字符。
这两个方案显然是无法让使用者开心的,所以我们需要解决掉。我们可以使用下面的方式获取绝对不会超过索引范围的值,猜猜什么办法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_ARRAY 1024
int sum_ascii(char * key){
unsigned long sum = 0;
unsigned long i;
for(i = 0; i < strlen(key);i++){
sum+= key[i];
}
return sum;
}
int residue(int size){
return size % SIZE_ARRAY;
}
int main(int argc, char * argv[]){
int key_id = sum_ascii("hellohashtable");
key_id = residue(key_id);
printf("The key id %d
", key_id);
return 0;
}
现在好了,我们不会超出索引范围了。那么问题又来了,我们的计算值很明显非常容易出现冲突,下面举一个例子吧。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_ARRAY 1024
int sum_ascii(char * key){
unsigned long sum = 0;
unsigned long i;
for(i = 0; i < strlen(key);i++){
sum+= key[i];
}
return sum;
}
int residue(int size){
return size % SIZE_ARRAY;
}
int main(int argc, char * argv[]){
char * key1 = "hellolandpack";
char * key2 = "hellohellohellohelloak6";
int key1_id = sum_ascii(key1);
int key2_id = sum_ascii(key2);
key1_id = residue(key1_id);
key2_id = residue(key2_id);
printf("The key[%s] id %d
", key1,key1_id);
printf("The key[%s] id %d
", key2,key2_id);
return 0;
}
可以看到,我们的key
显然是不同的,但是却生成了同样的一个索引值。这就会导致数据冲突了。下面换一种方式计算键值。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_ARRAY 1024
int sum_ascii(char * key){
unsigned long sum = 0;
unsigned long i;
for(i = 0; i < strlen(key);i++){
//sum+= key[i];
sum = sum << 8;
sum += key[i];
}
return sum;
}
int residue(int size){
return size % SIZE_ARRAY;
}
int main(int argc, char * argv[]){
char * key1 = "hellolandpack";
char * key2 = "hellohellohellohelloak6";
int key1_id = sum_ascii(key1);
int key2_id = sum_ascii(key2);
key1_id = residue(key1_id);
key2_id = residue(key2_id);
printf("The key[%s] id %d
", key1,key1_id);
printf("The key[%s] id %d
", key2,key2_id);
return 0;
}
难道这样就不会生成重复的键值吗?不可能哈,来看看下面这个测试。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_ARRAY 1024
int sum_ascii(char * key){
unsigned long sum = 0;
unsigned long i;
for(i = 0; i < strlen(key);i++){
//sum+= key[i];
sum = sum << 8;
sum += key[i];
}
return sum;
}
int residue(int size){
return size % SIZE_ARRAY;
}
int main(int argc, char * argv[]){
//char * key1 = "hellolandpack";
//char * key2 = "hellohellohellohelloak6";
char * key1 = "ddddd";
char * key2 = "dddd";
int key1_id = sum_ascii(key1);
int key2_id = sum_ascii(key2);
key1_id = residue(key1_id);
key2_id = residue(key2_id);
printf("The key[%s] id %d
", key1,key1_id);
printf("The key[%s] id %d
", key2,key2_id);
return 0;
}
好了,问题又来了!我们又遇到问题了。这个和之前的算法一样无法解决唯一键值问题。那么干嘛还要引入这个呢?来再看看原来的算法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_ARRAY 1024
int sum_ascii(char * key){
unsigned long sum = 0;
unsigned long i;
for(i = 0; i < strlen(key);i++){
//sum+= key[i];
//sum = sum << 8;
sum += key[i];
}
return sum;
}
int residue(int size){
return size % SIZE_ARRAY;
}
int main(int argc, char * argv[]){
//char * key1 = "hellolandpack";
//char * key2 = "hellohellohellohelloak6";
char * key1 = "hello";
char * key2 = "olleh";
int key1_id = sum_ascii(key1);
int key2_id = sum_ascii(key2);
key1_id = residue(key1_id);
key2_id = residue(key2_id);
printf("The key[%s] id %d
", key1,key1_id);
printf("The key[%s] id %d
", key2,key2_id);
return 0;
}
好了!可以看到。对于同一组字符的不同顺序它居然认为是同一个键,显然是错误的。而增加了向右移位处理后我们可以得到如下的结果。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_ARRAY 1024
int sum_ascii(char * key){
unsigned long sum = 0;
unsigned long i;
for(i = 0; i < strlen(key);i++){
//sum+= key[i];
sum = sum << 8;
sum += key[i];
}
return sum;
}
int residue(int size){
return size % SIZE_ARRAY;
}
int main(int argc, char * argv[]){
//char * key1 = "hellolandpack";
//char * key2 = "hellohellohellohelloak6";
char * key1 = "hello";
char * key2 = "olleh";
int key1_id = sum_ascii(key1);
int key2_id = sum_ascii(key2);
key1_id = residue(key1_id);
key2_id = residue(key2_id);
printf("The key[%s] id %d
", key1,key1_id);
printf("The key[%s] id %d
", key2,key2_id);
return 0;
}
总结
现在大概了解了一下设计字典最大的难点就是怎样去容纳各种不同的键值,并且产生一个唯一的索引。下一章我将会具体实现一个字典,并且在最后我会将其编译为一个python
包。
如果你在使用字典或者键值数据库有什么有趣的发现就快快分享分享吧~~