第08课:哈希索引

索引结构 第8课 / 共25课

📖 课程概述

哈希索引提供了O(1)的等值查找性能,是B+树之外另一种重要的索引结构。数据库中哈希索引分为静态哈希和动态哈希(可扩展哈希/线性哈希)。本课深入实现哈希索引,分析哈希冲突解决策略,并探讨PostgreSQL的哈希索引和MySQL的自适应哈希索引。

本课目标:实现静态哈希和可扩展哈希索引,理解哈希冲突解决策略,分析哈希索引的适用场景。

🔑 哈希索引原理

静态哈希: ┌─────────────────────────────────┐ │ 桶0: [key1→val1] [key5→val5] │ │ 桶1: [key2→val2] │ │ 桶2: [key3→val3] [key6→val6] │ ← 冲突链 │ 桶3: [key4→val4] │ │ │ 桶4: [key7→val7] → 溢出链 │ │ │ ... │ │ 桶N: [keyN→valN] │ └─────────────────────────────────┘ hash(key) % N → 桶号 可扩展哈希(Extendible Hashing): 全局深度=2 目录: 00 → 桶A (局部深度=1): [k4,k8] 01 → 桶B (局部深度=2): [k1,k5] 10 → 桶A (局部深度=1) ← 共享桶 11 → 桶C (局部深度=2): [k3,k7,k11] 桶满时: 局部深度+1,分裂桶,可能翻倍目录

冲突解决策略

策略原理优缺点
链地址法每个桶维护一个链表简单,但链过长时退化为O(n)
开放寻址冲突时探测下一个槽缓存友好,但删除复杂
线性探测h(k)+i简单,但聚集问题
双重哈希h1(k)+i*h2(k)减少聚集,但计算开销大

💻 C语言实现:可扩展哈希索引

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define BUCKET_SIZE   4
#define MAX_DEPTH     16
#define MAX_KEY       32
#define MAX_VAL       64

typedef struct {
    char keys[BUCKET_SIZE][MAX_KEY];
    char values[BUCKET_SIZE][MAX_VAL];
    int  count;
    int  local_depth;
} Bucket;

typedef struct {
    Bucket** directory;    // 目录数组
    int      dir_size;     // 目录大小 = 2^global_depth
    int      global_depth;
    int      num_buckets;
    int      splits;
} ExtHashTable;

uint32_t hash_func(const char* key) {
    uint32_t h = 5381;
    while (*key) h = h * 33 + (unsigned char)*key++;
    return h;
}

ExtHashTable* ext_hash_create() {
    ExtHashTable* ht = calloc(1, sizeof(ExtHashTable));
    ht->global_depth = 1;
    ht->dir_size = 2;
    ht->directory = calloc(ht->dir_size, sizeof(Bucket*));
    // 初始两个桶
    Bucket* b0 = calloc(1, sizeof(Bucket));
    Bucket* b1 = calloc(1, sizeof(Bucket));
    b0->local_depth = 1;
    b1->local_depth = 1;
    ht->directory[0] = b0;
    ht->directory[1] = b1;
    ht->num_buckets = 2;
    printf("[ExtHash] 创建,全局深度=%d\n", ht->global_depth);
    return ht;
}

int bucket_index(ExtHashTable* ht, const char* key) {
    uint32_t h = hash_func(key);
    return h & ((1 << ht->global_depth) - 1);
}

// 分裂桶
void split_bucket(ExtHashTable* ht, int idx) {
    Bucket* old = ht->directory[idx];
    int old_depth = old->local_depth;
    int new_depth = old_depth + 1;

    // 如果局部深度等于全局深度,需要翻倍目录
    if (old_depth == ht->global_depth) {
        int new_size = ht->dir_size * 2;
        Bucket** new_dir = calloc(new_size, sizeof(Bucket*));
        for (int i = 0; i < ht->dir_size; i++) {
            new_dir[i] = ht->directory[i];
            new_dir[i + ht->dir_size] = ht->directory[i];
        }
        free(ht->directory);
        ht->directory = new_dir;
        ht->dir_size = new_size;
        ht->global_depth++;
        printf("  [ExtHash] 目录翻倍,全局深度→%d\n", ht->global_depth);
    }

    // 创建新桶
    Bucket* new_bucket = calloc(1, sizeof(Bucket));
    new_bucket->local_depth = new_depth;
    old->local_depth = new_depth;
    ht->num_buckets++;
    ht->splits++;

    // 重新分配旧桶的记录
    char temp_keys[BUCKET_SIZE][MAX_KEY];
    char temp_vals[BUCKET_SIZE][MAX_VAL];
    int temp_count = old->count;
    memcpy(temp_keys, old->keys, sizeof(temp_keys));
    memcpy(temp_vals, old->values, sizeof(temp_vals));

    old->count = 0;
    int mask = (1 << new_depth) - 1;
    int old_suffix = idx & mask;

    // 更新目录指针
    for (int i = 0; i < ht->dir_size; i++) {
        if (ht->directory[i] == old) {
            int suffix = i & mask;
            if (suffix != old_suffix) {
                ht->directory[i] = new_bucket;
            }
        }
    }

    // 重新插入旧记录
    for (int i = 0; i < temp_count; i++) {
        uint32_t h = hash_func(temp_keys[i]);
        int bi = h & ((1 << ht->global_depth) - 1);
        Bucket* target = ht->directory[bi];
        if (target->count < BUCKET_SIZE) {
            strcpy(target->keys[target->count], temp_keys[i]);
            strcpy(target->values[target->count], temp_vals[i]);
            target->count++;
        }
    }
    printf("  [ExtHash] 桶分裂,新局部深度=%d\n", new_depth);
}

// 插入
void ext_hash_insert(ExtHashTable* ht, const char* key, const char* val) {
    int idx = bucket_index(ht, key);
    Bucket* bucket = ht->directory[idx];

    // 检查重复
    for (int i = 0; i < bucket->count; i++) {
        if (strcmp(bucket->keys[i], key) == 0) {
            strcpy(bucket->values[i], val);
            return;
        }
    }

    if (bucket->count < BUCKET_SIZE) {
        strcpy(bucket->keys[bucket->count], key);
        strcpy(bucket->values[bucket->count], val);
        bucket->count++;
        return;
    }

    // 桶满,分裂
    split_bucket(ht, idx);
    // 重新计算位置
    idx = bucket_index(ht, key);
    bucket = ht->directory[idx];
    if (bucket->count < BUCKET_SIZE) {
        strcpy(bucket->keys[bucket->count], key);
        strcpy(bucket->values[bucket->count], val);
        bucket->count++;
    } else {
        // 递归分裂(极端情况)
        ext_hash_insert(ht, key, val);
    }
}

// 查找
char* ext_hash_search(ExtHashTable* ht, const char* key) {
    int idx = bucket_index(ht, key);
    Bucket* bucket = ht->directory[idx];
    for (int i = 0; i < bucket->count; i++) {
        if (strcmp(bucket->keys[i], key) == 0) {
            printf("  [ExtHash] 找到: %s→%s\n", key, bucket->values[i]);
            return bucket->values[i];
        }
    }
    printf("  [ExtHash] 未找到: %s\n", key);
    return NULL;
}

// 删除
int ext_hash_delete(ExtHashTable* ht, const char* key) {
    int idx = bucket_index(ht, key);
    Bucket* bucket = ht->directory[idx];
    for (int i = 0; i < bucket->count; i++) {
        if (strcmp(bucket->keys[i], key) == 0) {
            // 用最后一个元素替换
            strcpy(bucket->keys[i], bucket->keys[bucket->count - 1]);
            strcpy(bucket->values[i], bucket->values[bucket->count - 1]);
            bucket->count--;
            printf("  [ExtHash] 删除: %s\n", key);
            return 0;
        }
    }
    return -1;
}

void ext_hash_stats(ExtHashTable* ht) {
    printf("\n=== 可扩展哈希统计 ===\n");
    printf("全局深度: %d  目录大小: %d  桶数: %d  分裂次数: %d\n",
           ht->global_depth, ht->dir_size, ht->num_buckets, ht->splits);
    // 统计桶利用率
    int total = 0, used = 0;
    for (int i = 0; i < ht->dir_size; i++) {
        if (i == 0 || ht->directory[i] != ht->directory[i-1]) {
            total += BUCKET_SIZE;
            used += ht->directory[i]->count;
        }
    }
    printf("桶利用率: %.1f%%\n", (double)used / total * 100);
}

int main() {
    printf("╔══════════════════════════════════════╗\n");
    printf("║   可扩展哈希索引                     ║\n");
    printf("╚══════════════════════════════════════╝\n\n");

    ExtHashTable* ht = ext_hash_create();
    const char* data[][2] = {
        {"alice","Beijing"}, {"bob","Shanghai"}, {"charlie","Shenzhen"},
        {"diana","Hangzhou"}, {"eve","Chengdu"}, {"frank","Wuhan"},
        {"grace","Nanjing"}, {"henry","Xi'an"}, {"ivy","Qingdao"},
        {"jack","Xiamen"}, {"kate","Suzhou"}, {"leo","Dalian"},
        {"mia","Changsha"}, {"noah","Zhengzhou"}, {"olivia","Tianjin"},
        {"peter","Harbin"}, {"quinn","Kunming"}, {"rose","Guiyang"},
    };

    printf("--- 插入 ---\n");
    for (int i = 0; i < 18; i++) {
        printf("Insert %s→%s\n", data[i][0], data[i][1]);
        ext_hash_insert(ht, data[i][0], data[i][1]);
    }

    ext_hash_stats(ht);

    printf("\n--- 查找 ---\n");
    ext_hash_search(ht, "alice");
    ext_hash_search(ht, "frank");
    ext_hash_search(ht, "xyz");

    printf("\n--- 删除 ---\n");
    ext_hash_delete(ht, "bob");
    ext_hash_search(ht, "bob");

    printf("\n✅ 可扩展哈希索引运行完成\n");
    return 0;
}

🐍 Python实现:线性哈希

"""
线性哈希(Linear Hashing)实现
与可扩展哈希不同,线性哈希按轮次分裂,不需要目录
"""
import hashlib

class LinearHashTable:
    def __init__(self, bucket_size=4, load_factor=0.75):
        self.bucket_size = bucket_size
        self.load_factor = load_factor
        self.buckets = [[] for _ in range(4)]  # 初始4个桶
        self.level = 0        # 当前轮次
        self.split_ptr = 0    # 分裂指针
        self.num_records = 0
        self.next_split = len(self.buckets)  # 2^level * 2

    def _hash(self, key, level=None):
        if level is None: level = self.level
        h = int(hashlib.md5(key.encode()).hexdigest(), 16)
        n = 2 ** (level + 2)  # 当前桶数基数
        idx = h % n
        if idx < self.split_ptr:
            n = 2 ** (level + 3)  # 已分裂,用下一级
            idx = h % n
        return idx

    def _should_split(self):
        return self.num_records > len(self.buckets) * self.bucket_size * self.load_factor

    def _split_bucket(self):
        old_idx = self.split_ptr
        old_bucket = self.buckets[old_idx]
        # 创建新桶
        new_idx = len(self.buckets)
        self.buckets.append([])

        # 重新分配旧桶记录
        entries = old_bucket[:]
        self.buckets[old_idx] = []
        self.level_changed = False

        for k, v in entries:
            new_pos = self._hash(k, self.level + 1)
            self.buckets[new_pos].append((k, v))

        self.split_ptr += 1
        if self.split_ptr >= 2 ** (self.level + 2):
            self.level += 1
            self.split_ptr = 0
            self.level_changed = True

        print(f"  [LinHash] 分裂桶{old_idx}→新桶{new_idx}, split_ptr={self.split_ptr}, level={self.level}")

    def put(self, key, value):
        idx = self._hash(key)
        bucket = self.buckets[idx]
        # 检查更新
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return
        bucket.append((key, value))
        self.num_records += 1
        if self._should_split():
            self._split_bucket()

    def get(self, key):
        idx = self._hash(key)
        for k, v in self.buckets[idx]:
            if k == key: return v
        return None

    def delete(self, key):
        idx = self._hash(key)
        bucket = self.buckets[idx]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket.pop(i)
                self.num_records -= 1
                return True
        return False

    def stats(self):
        max_len = max(len(b) for b in self.buckets)
        min_len = min(len(b) for b in self.buckets)
        avg_len = self.num_records / len(self.buckets)
        return {
            "buckets": len(self.buckets), "records": self.num_records,
            "level": self.level, "split_ptr": self.split_ptr,
            "max_bucket": max_len, "min_bucket": min_len,
            "avg_bucket": f"{avg_len:.1f}",
            "utilization": f"{self.num_records / (len(self.buckets) * self.bucket_size) * 100:.1f}%"
        }

# 测试
lh = LinearHashTable(bucket_size=4)
names = ["alice","bob","charlie","diana","eve","frank","grace","henry",
         "ivy","jack","kate","leo","mia","noah","olivia","peter",
         "quinn","rose","sam","tina","uma","vera","will","xena"]

for name in names:
    lh.put(name, f"city_{hash(name) % 10}")

print("=== 线性哈希统计 ===")
s = lh.stats()
for k, v in s.items(): print(f"  {k}: {v}")

# 查找
print("\n--- 查找测试 ---")
for name in ["alice", "frank", "xyz"]:
    val = lh.get(name)
    print(f"  {name} → {val}")

# 删除
lh.delete("bob")
print(f"\n删除bob后: {lh.stats()}")
print("✅ 线性哈希实现完成")

🔑 关键概念总结

📝 练习

  1. 为可扩展哈希添加桶合并逻辑,当桶利用率低于25%时合并
  2. 实现Cuckoo Hashing,保证最坏O(1)查找
  3. 对比可扩展哈希和线性哈希在100万次随机插入下的桶利用率
  4. 实现Bloom Filter加速哈希查找的"不存在"判断
#️⃣

🏆 成就解锁:哈希大师

掌握哈希索引,你已理解数据库中O(1)查找的实现原理!

✅ 可扩展哈希 · ✅ 线性哈希 · ✅ 冲突解决策略