Github地址–源码
原因
开发中有时需要自己写配置文件,ini这种配置文件格式,在python中有configparser模块,使用很方便,但是在C中,没有一个很好的库可以用。
INI文件的格式很简单,最基本的三个要素是:parameters,sections 和 comments。
[owner]
name=John Doe
organization=Acme Products
[database]
server=192.0.2.42
port=143
file = "acme payroll.dat"
有个C库iniparser,能够解析使用ini配置文件。iniparser库其实是基于一个简单字典来实现的,源码就4个文件。
dictionary.h:实现简单的字典,使用数组实现的
dictionary.c
iniparser.h:基于字典实现的ini解析库
iniparser.c
但是里边的字典是使用数组实现的,带来的问题是,不停的动态增删情况下,这个字典有可能会持续增长,前边释放的字典,还在占用着数组的空间,字典元素少的时候还行,如果内容比较多,很多已经无效的元素占着非常多的空间。
那我们来实现一个新的字典。
多态字典
设计
1、支持多态,能够适应各种自定义的场合
2、使用哈希跳表实现索引
3、使用链表来解决哈希冲突的问题
跳表字典关键结构体
struct dictEntry; // 字典KV结构体,其中存放键-值信息。key存放key信息变量,使用void *来支撑多态的实现。v使用枚举,来支持几种类型的值。
struct skipNode; // 跳表节点,存放键计算出来的哈希值信息。hashKey是key计算出来的一个hash值。pEntry后跟着hashKey相同的dictEntry链表。
struct skipList; // 跳表结构体
struct dictEntry; // 字典KV结构体,其中存放键-值信息
struct skipNode; // 跳表节点,存放键计算出来的哈希值信息
struct skipList; // 跳表结构体
/**
跳表节点,哈希值节点
*/
typedef struct skipNode
{
uint32_t high; // node height
uint32_t count; // dictEntry list num
hashKeyType hashKey; // hash key
struct dictEntry * pEntry; // pEntry list pointer
struct skipNode * next[0]; // 后继指针数组,柔性数组 可实现结构体的变长
} skipNode;
/**
跳表结构体
*/
typedef struct skipList
{
int max_lev; // max level
int level; // 当前使用的最大层数
skipNode * head; // skipNode链表头节点
uint32_t count; // skipNode数量
/*
全局缓存数组,不再额外申请内存了
在搜寻指定Key的过程中,保存每一层level的最后一个节点的位置
用在删除和插入节点时,对涉及到的节点的指针更新
*/
skipNode * last[MAX_LEVEL];
} skipList;
/**
字典KV结构体
key 属性保存着键值对中的键, 而 v 属性则保存着键值对中的值,
其中键值对的值可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。
*/
typedef struct dictEntry {
//键
void * key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 指向下个哈希表节点,形成链表
} dictEntry;
/** 字典结构体 */
typedef struct dict {
dictType *type; // 用于支撑多态字典
void *privdata; // 私有数据
skipList * sl; // 跳表
uint32_t count; // entry数量
} dict;
多态的实现
c语言支持的数据类型,没有python、C++等语言多,没有垃圾回收等机制。
想要支持多态,需要让使用者能够自己实现一些函数。
多态的实现参考redis多态。
typedef struct dictType {
// 计算哈希值的函数
uint64_t (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 设置值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
// 打印键的函数,调试用
//void (*keyPrint)(void *privdata, const void *key);
// 打印值的函数,调试用
//void (*valPrint)(void *privdata, const void *obj);
} dictType;
skiplist的接口
跳表需要能够支撑以下几个接口就能满足字典的实现了。
skipList * create_skiplist(void); //创建一个跳表
skipNode *skiplist_find(skipList *sl, hashKeyType hashKey); //查找一个node,失败返回null
skipNode *skiplist_find_or_insert(skipList *sl, hashKeyType hashKey); //找一个node,找不到则添加一个新的node
void skiplist_delete(skipList *sl, hashKeyType hashKey); //在字典实现中用不到,
字典接口
字典需要支持以下几个接口:
dict *dict_new(dictType *type, void *privDataPtr); //新建一个字典dict
dictEntry * dict_add(dict * d, void * key, void * val); //字典添加一个Key-Value,如果已存在,则修改该Key的Value信息
dictEntry * dict_find(dict * d, void * key); //查找一个dictEntry
int dict_delete(dict * d, void * key); //删除一个Key
void dict_free(dict *d); //释放一个字典dict
哈希冲突
跳表的每个skipNode都存有键计算出来的哈希值,使用链表方式来解决键哈希冲突的问题。
pEntry作为一个链表的指针,链表中每个dictEntry存放真正的键值key-value信息,
随机分层–模拟幂次定律
用来决定每个跳表skipNode的层级数量。
/**
* @brief random_level.
*
* @param void
*
* @return level num, 1 2 3....
*/
static int random_level(void)
{
int level = 1;
const double p = 0.5;
while ((random() & 0xffff) < (0xffff * p))
{
level++;
}
return level > MAX_LEVEL ? MAX_LEVEL : level;
}
level初始化为1,然后,如果持续满足条件:((random() & 0xffff) < (0xffff * p))的话,则level+=1。最终调整level的值,使其小于MAX_LEVEL。
理解该算法的核心,就是要理解满足条件:((random() & 0xffff) < (0xffff * p))的概率是多少?
random()&0xFFFF形成的数,均匀分布在区间[0,0xFFFF]上,那么这个数小于(p * 0xFFFF)的概率是多少呢?
自然就是p,代码中是0.5。因此,最终返回level为1的概率是0.5,返回level为2的概率为0.5 * 0.5,返回level为3的概率为0.5 * 0.5 * 0.5…
如果p是0.25,则最终返回level为1的概率是0.75,返回level为2的概率为0.25 * 0.75,返回level为3的概率为0.25 * 0.25 * 0.75…
如果返回level为k的概率为x,则返回level为k+1的概率为0.25x,换句话说,如果k层的结点数是x,那么k+1层就是0.25x了。这就是模拟的幂次定律,越大的数出现的概率越小。