C 语言实现 HashMap

哈希表是一种十分重要的数据结构,在很多应用场景下都有用到,本文会对哈希表原理进行简单的剖析,并使用C语言实现一个完整的HashMap。

文中有一些宏可以参考:基本宏

什么是HashMap?

存储方式主要有两种线性存储链式存储,常见的线性存储例如数组,常见的链式存储如链表、二叉树等。哈希表的存储主干为线性存储,这也是它在理想状态(无冲突)下时间复杂度为 O(1) 的关键所在。普通线性存储的存储内容与索引地址之间没有任何的联系,只能通过索引地址推算出存储内容,不能从存储内容推算出索引地址,是一个单向不可逆的过程,而 HashMap 存储的是一个 <key, value> 的键值对,通过key和索引地址建立了一层关系,这层关系称之为哈希函数(或散列函数),这样既可以通过key推算出索引地址,也可以通过推算出的索引地址直接定位到键值对,这是一个双向可逆的过程。需要注意的一点是 HashMap并不直接暴露出键值对的索引地址,但是可以通过哈希函数推算出 HashCode,其实 HashCode 就是真实的索引地址。

定义键值对结构

code c
typedef struct entry {
    void * key;             // 键
    void * value;           // 值
    struct entry * next;    // 冲突链表
}*Entry;
#define newEntry() NEW(struct entry)
#define newEntryList(length) (Entry)malloc(length * sizeof(struct entry))

我也希望世界上存在一种函数,称为完美哈希函数,但我找寻了这么久依旧不见它的身影。如果两个不同的 key 值得到了一个相同的 HashCode,这种情况称之为哈希冲突,一个好的哈希函数很大程度上决定了哈希表的性能,不存在一种适合所有哈希表的哈希函数,在很多特定的情景下,需要有针对性的设计哈希函数才能达到理想的效果。当然啦,还是有一些优秀的哈希函数可以应对大多数情况的,对于性能要求不是很高的场景用这些就可以了。使用 HashMap 的时候难免会发生冲突,常用的方法主要分为两类:再散列法链地址法。再散列法就是发生冲突时使用另一个哈希函数重新推算,一直到不冲突为止,这种方法有时候会造成数据堆积,就是元素本来的 HashCode 被其它元素再散列的 HashCode 占用,被迫再散列,如此恶性循环。链地址法就是在冲突的位置建立一个链表,将冲突值放在链表中,检索的时候需要额外的遍历冲突链表,本文采用的就是链地址法。

定义HashMap结构体

HashMap 结构的存储本体是一个数组,建立一个 Entry 数组作为存储空间,然后根据传入的 key 计算出 HashCode,当做数组的索引存入数据,读取的时候通过计算出的 HashCode 可以在数组中直接取出值。

size 是当前存储键值对的数量,而 listSize 是当前数组的大小,仔细观察键值对结构会发现,数组的每一项其实都是冲突链表的头节点。因为冲突的存在,就有可能导致 size 大于 listSize,当 size 大于 listSize 的时候一定发生了冲突(思考下为什么?),这时候就会扩容,详细内容会在后面提到。

在结构体中我还放了一些常用的方法,之所以这样做是因为C语言本身并没有类的概念,为了便于内部封装(经常会有一个方法调用另一个方法的时候),可以让用户自定义一个方法而不影响其它方法的调用。举个简单的例子,put方法中调用了 hashCode 函数,如果想自定义一个 hashCode 方法,迫不得已还要再实现一个 put 方法,哪怕 put 中只改了一行代码。

结构体定义如下:

code c
// 哈希结构
typedef struct hashMap *HashMap;
#define newHashMap() NEW(struct hashMap)

// 哈希函数类型
typedef int(*HashCode)(HashMap, void * key);

// 判等函数类型
typedef Boolean(*Equal)(void * key1, void * key2);

// 添加键函数类型
typedef void(*Put)(HashMap hashMap, void * key, void * value);

// 获取键对应值的函数类型
typedef void * (*Get)(HashMap hashMap, void * key);

// 删除键的函数类型
typedef Boolean(*Remove)(HashMap hashMap, void * key);

// 清空Map的函数类型
typedef void(*Clear)(HashMap hashMap);

// 判断键值是否存在的函数类型
typedef Boolean(*Exists)(HashMap hashMap, void * key);

typedef struct hashMap {
    int size;           // 当前大小
    int listSize;       // 有效空间大小
    HashCode hashCode;  // 哈希函数
    Equal equal;        // 判等函数
    Entry list;         // 存储区域
    Put put;            // 添加键的函数
    Get get;            // 获取键对应值的函数
    Remove remove;      // 删除键
    Clear clear;        // 清空Map
    Exists exists;      // 判断键是否存在
    Boolean autoAssign;	// 设定是否根据当前数据量动态调整内存大小,默认开启
}*HashMap;

// 默认哈希函数
static int defaultHashCode(HashMap hashMap, void * key);

// 默认判断键值是否相等
static Boolean defaultEqual(void * key1, void * key2);

// 默认添加键值对
static void defaultPut(HashMap hashMap, void * key, void * value);

// 默认获取键对应值
static void * defaultGet(HashMap hashMap, void * key);

// 默认删除键
static Boolean defaultRemove(HashMap hashMap, void * key);

// 默认判断键是否存在
static Boolean defaultExists(HashMap hashMap, void * key);

// 默认清空Map
static void defaultClear(HashMap hashMap);

// 创建一个哈希结构
HashMap createHashMap(HashCode hashCode, Equal equal);

// 重新构建
static void resetHashMap(HashMap hashMap, int listSize);

HashMap 的所有属性方法都有一个默认的实现,创建 HashMap 时可以指定哈希函数和判等函数(用于比较两个 key 是否相等),传入 NULL 时将使用默认函数。这些函数都被设置为了 static,在文件外不可访问。

哈希函数

哈希函数是哈希表的重中之重,我从网上直接找来一个,感觉比自己写的靠谱些:

code c
int defaultHashCode(HashMap hashMap, let key) {
    string k = (string)key;
    unsigned long h = 0;
    while (*k) {
        h = (h << 4) + *k++;
        unsigned long g = h & 0xF0000000L;
        if (g) {
            h ^= g >> 24;
        }
        h &= ~g;
    }
    return h % hashMap->listSize;
}

key 的类型为 void *,是一个任意类型,HashMap 本身也没有规定 key 值一定是 string 类型,上面的哈希函数只针对 string 类型,可以根据实际需要替换成其他。

put函数

用于在哈希表中存入一个键值对,首先先推算出 HashCode,然后判断该地址是否已经有数据,如果已有的 key 值和存入的 key 值相同,改变 value 即可,否则为冲突,需要挂到冲突链尾部,该地址没有数据时直接存储。实现如下:

code c
void resetHashMap(HashMap hashMap, int listSize) {

    if (listSize < 8) return;

    // 键值对临时存储空间
    Entry tempList = newEntryList(hashMap->size);

    HashMapIterator iterator = createHashMapIterator(hashMap);
    int length = hashMap->size;
    for (int index = 0; hasNextHashMapIterator(iterator); index++) {
        // 迭代取出所有键值对
        iterator = nextHashMapIterator(iterator);
        tempList[index].key = iterator->entry->key;
        tempList[index].value = iterator->entry->value;
        tempList[index].next = NULL;
    }
    freeHashMapIterator(&iterator);

    // 清除原有键值对数据
    hashMap->size = 0;
    for (int i = 0; i < hashMap->listSize; i++) {
        Entry current = &hashMap->list[i];
        current->key = NULL;
        current->value = NULL;
        if (current->next != NULL) {
            while (current->next != NULL) {
                Entry temp = current->next->next;
                free(current->next);
                current->next = temp;
            }
        }
    }

    // 更改内存大小
    hashMap->listSize = listSize;
    Entry relist = (Entry)realloc(hashMap->list, hashMap->listSize * sizeof(struct entry));
    if (relist != NULL) {
        hashMap->list = relist;
        relist = NULL;
    }

    // 初始化数据
    for (int i = 0; i < hashMap->listSize; i++) {
        hashMap->list[i].key = NULL;
        hashMap->list[i].value = NULL;
        hashMap->list[i].next = NULL;
    }

    // 将所有键值对重新写入内存
    for (int i = 0; i < length; i++) {
        hashMap->put(hashMap, tempList[i].key, tempList[i].value);
    }
    free(tempList);
}

void defaultPut(HashMap hashMap, let key, let value) {
    if (hashMap->autoAssign && hashMap->size >= hashMap->listSize) {

        // 内存扩充至原来的两倍
        // *注: 扩充时考虑的是当前存储元素数量与存储空间的大小关系,而不是存储空间是否已经存满,
        // 例如: 存储空间为10,存入了10个键值对,但是全部冲突了,所以存储空间空着9个,其余的全部挂在一个上面,
        // 这样检索的时候和遍历查询没有什么区别了,可以简单这样理解,当我存入第11个键值对的时候一定会发生冲突,
        // 这是由哈希函数本身的特性(取模)决定的,冲突就会导致检索变慢,所以这时候扩充存储空间,对原有键值对进行
        // 再次散列,会把冲突的数据再次分散开,加快索引定位速度。
        resetHashMap(hashMap, hashMap->listSize * 2);
    }

    int index = hashMap->hashCode(hashMap, key);
    if (hashMap->list[index].key == NULL) {
        hashMap->size++;
        // 该地址为空时直接存储
        hashMap->list[index].key = key;
        hashMap->list[index].value = value;
    }
    else {

        Entry current = &hashMap->list[index];
        while (current != NULL) {
            if (hashMap->equal(key, current->key)) {
                // 对于键值已经存在的直接覆盖
                current->value = value;
                return;
            }
            current = current->next;
        };

        // 发生冲突则创建节点挂到相应位置的next上
        Entry entry = newEntry();
        entry->key = key;
        entry->value = value;
        entry->next = hashMap->list[index].next;
        hashMap->list[index].next = entry;
        hashMap->size++;
    }
}

put 函数还有一个重要的功能,当 size 大于 listSize 时要主动扩容,这个判定条件看似有些不合理,当 size 大于 listSize 的时候可能因为冲突的存在,数组并没有存满,这时候就扩容不是浪费存储空间吗?事实确实如此,但这其实是为了加快检索速度一种妥协的办法,上文提到过,当 size 大于 listSize 时一定会发生冲突,因为哈希函数为了不越界,都会将计算出的 HashCode 进行取余操作,这就导致 HashCode 的个数一共就 listSize 个,超过这个个数就一定会冲突,冲突的越多,检索速度就越向 O(n) 靠拢,为了保证索引速度消耗一定的空间还是比较划算的,扩容时直接将容量变为了当前的两倍,这是考虑到扩容时需要将所有重新计算所有元素的 HashCode,较为消耗时间,所以应该尽量的减少扩容次数。

其它函数

其它的函数没有什么关键点,直接放出代码实现:

code c
let defaultGet(HashMap hashMap, let key) {
    int index = hashMap->hashCode(hashMap, key);
    Entry entry = &hashMap->list[index];
    while (entry->key != NULL && !hashMap->equal(entry->key, key)) {
        entry = entry->next;
    }
    return entry->value;
}

Boolean defaultRemove(HashMap hashMap, let key) {
    int index = hashMap->hashCode(hashMap, key);
    Entry entry = &hashMap->list[index];
    if (entry->key == NULL) {
        return False;
    }
    Boolean result = False;
    if (hashMap->equal(entry->key, key)) {
        hashMap->size--;
        if (entry->next != NULL) {
            Entry temp = entry->next;
            entry->key = temp->key;
            entry->value = temp->value;
            entry->next = temp->next;
            free(temp);
        }
        else {
            entry->key = entry->value = NULL;
        }
        result = True;
    }
    else {
        Entry p = entry;
        entry = entry->next;
        while (entry != NULL) {
            if (hashMap->equal(entry->key, key)) {
                hashMap->size--;
                p->next = entry->next;
                free(entry);
                result = True;
                break;
            }
            p = entry;
            entry = entry->next;
        };
    }

    // 如果空间占用不足一半,则释放多余内存
    if (result && hashMap->autoAssign &&  hashMap->size < hashMap->listSize / 2) {
        resetHashMap(hashMap, hashMap->listSize / 2);
    }
    return result;
}

Boolean defaultExists(HashMap hashMap, let key) {
    int index = hashMap->hashCode(hashMap, key);
    Entry entry = &hashMap->list[index];
    if (entry->key == NULL) {
        return False;
    }
    if (hashMap->equal(entry->key, key)) {
        return True;
    }
    if (entry->next != NULL) {
        do {
            if (hashMap->equal(entry->key, key)) {
                return True;
            }
            entry = entry->next;

        } while (entry != NULL);
        return False;
    }
    else {
        return False;
    }
}

void defaultClear(HashMap hashMap) {
    for (int i = 0; i < hashMap->listSize; i++) {
        // 释放冲突值内存
        Entry entry = hashMap->list[i].next;
        while (entry != NULL) {
            Entry next = entry->next;
            free(entry);
            entry = next;
        }
        hashMap->list[i].next = NULL;
    }
    // 释放存储空间
    free(hashMap->list);
    hashMap->list = NULL;
    hashMap->size = -1;
    hashMap->listSize = 0;
}

HashMap createHashMap(HashCode hashCode, Equal equal) {
    HashMap hashMap = newHashMap();
    hashMap->size = 0;
    hashMap->listSize = 8;
    hashMap->hashCode = hashCode == NULL ? defaultHashCode : hashCode;
    hashMap->equal = equal == NULL ? defaultEqual : equal;
    hashMap->exists = defaultExists;
    hashMap->get = defaultGet;
    hashMap->put = defaultPut;
    hashMap->remove = defaultRemove;
    hashMap->clear = defaultClear;
    hashMap->autoAssign = True; 
    // 起始分配8个内存空间,溢出时会自动扩充
    hashMap->list = newEntryList(hashMap->listSize);
    Entry p = hashMap->list;
    for (int i = 0; i < hashMap->listSize; i++) {
        p[i].key = p[i].value = p[i].next = NULL;
    }
    return hashMap;
}

Iterator接口

Iterator 接口提供了遍历 HashMap 结构的方法,基本定义如下:

code c
// 迭代器结构
typedef struct hashMapIterator {
    Entry entry;    // 迭代器当前指向
    int count;      // 迭代次数
    int hashCode;   // 键值对的哈希值
    HashMap hashMap;
}*HashMapIterator;

#define newHashMapIterator() NEW(struct hashMapIterator)

// 创建一个哈希结构
HashMap createHashMap(HashCode hashCode, Equal equal);

// 创建哈希结构迭代器
HashMapIterator createHashMapIterator(HashMap hashMap);

// 迭代器是否有下一个
Boolean hasNextHashMapIterator(HashMapIterator iterator);

// 迭代到下一次
HashMapIterator nextHashMapIterator(HashMapIterator iterator);

// 释放迭代器内存
void freeHashMapIterator(HashMapIterator * iterator);

实现如下:

code c
HashMapIterator createHashMapIterator(HashMap hashMap) {
    HashMapIterator iterator = newHashMapIterator();
    iterator->hashMap = hashMap;
    iterator->count = 0;
    iterator->hashCode = -1;
    iterator->entry = NULL;
    return iterator;
}

Boolean hasNextHashMapIterator(HashMapIterator iterator) {
    return iterator->count < iterator->hashMap->size ? True : False;
}

HashMapIterator nextHashMapIterator(HashMapIterator iterator) {
    if (hasNextHashMapIterator(iterator)) {
        if (iterator->entry != NULL && iterator->entry->next != NULL) {
            iterator->count++;
            iterator->entry = iterator->entry->next;
            return iterator;
        }
        while (++iterator->hashCode < iterator->hashMap->listSize) {
            Entry entry = &iterator->hashMap->list[iterator->hashCode];
            if (entry->key != NULL) {
                iterator->count++;
                iterator->entry = entry;
                break;
            }
        }
    }
    return iterator;
}

void freeHashMapIterator(HashMapIterator * iterator) {
    free(*iterator);
    *iterator = NULL;
}

使用测试

code c
#define Put(map, key, value) map->put(map, (void *)key, (void *)value);
#define Get(map, key) (char *)map->get(map, (void *)key)
#define Remove(map, key) map->remove(map, (void *)key)
#define Existe(map, key) map->exists(map, (void *)key)

int main() {

    HashMap map = createHashMap(NULL, NULL);
    Put(map, "asdfasdf", "asdfasdfds");
    Put(map, "sasdasd", "asdfasdfds");
    Put(map, "asdhfgh", "asdfasdfds");
    Put(map, "4545", "asdfasdfds");
    Put(map, "asdfaasdasdsdf", "asdfasdfds");
    Put(map, "asdasg", "asdfasdfds");
    Put(map, "qweqeqwe", "asdfasdfds");

    printf("key: 4545, exists: %s\n", Existe(map, "4545") ? "true" : "false");
    printf("4545: %s\n", Get(map, "4545"));
    printf("remove 4545 %s\n", Remove(map, "4545") ? "true" : "false");
    printf("remove 4545 %s\n", Remove(map, "4545") ? "true" : "false");
    printf("key: 4545, exists: %s\n", Existe(map, "4545") ? "true" : "false");

    HashMapIterator iterator = createHashMapIterator(map);
    while (hasNextHashMapIterator(iterator)) {
        iterator = nextHashMapIterator(iterator);
        printf("{ key: %s, key: %s, hashcode: %d }\n",
            (char *)iterator->entry->key, (char *)iterator->entry->value, iterator->hashCode);
    }
    map->clear(map);
    freeHashMapIterator(&iterator);

    return 0;
}

运行结果:

key: 4545, exists: true
4545: asdfasdfds
remove 4545 true
remove 4545 false
key: 4545, exists: false
{ key: asdfasdf, key: asdfasdfds, hashcode: 2 }
{ key: asdhfgh, key: asdfasdfds, hashcode: 2 }
{ key: sasdasd, key: asdfasdfds, hashcode: 2 }
{ key: asdfaasdasdsdf, key: asdfasdfds, hashcode: 6 }
{ key: asdasg, key: asdfasdfds, hashcode: 7 }
{ key: qweqeqwe, key: asdfasdfds, hashcode: 9 }