用C实现多态字典

小码虫的公众号

Github地址–源码

my-skiplist-dict

原因

开发中有时需要自己写配置文件,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了。这就是模拟的幂次定律,越大的数出现的概率越小。

参考资料

Redis中的数据结构
10.21学习笔记

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值