什么是哈希?
将元素的存储位置和该元素的关键码通过某种函数建立一一对应的关系,构造出来的存储结构称之为哈希表,转换时借助的函数称之为哈希函数,在理想情况下,根据关键码搜索元素时可以不经过任何比较,一次性从表中查找到所要搜索的元素
但是在通过哈希函数进行元素存储位置确立的时候会出现,不同元素的关键码通过哈希函数计算出来的存储位置是相同的,这便是哈希冲突
解决哈希冲突有两种方式:
- 开散列
- 闭散列
闭散列
当发生哈希冲突时,如果哈希表还允许做插入,就把该元素放到“下一个”空位置上去,怎么找“下一个”位置,常见的有线性探测和二次探测,这两种方式仅在插入元素发生冲突找“下一个”存储位置有区别
- 线性探测:
while (发生哈希冲突)
{
探测位置 = (当前位置 + 1) % 哈希表容量
}
- 二次探测:
int i = 1;
while (发生哈希冲突)
{
探测位置 = (当前位置 + (i的二次方)) % 哈希表容量
i++;
}
用闭散列的方式解决哈希冲突后会出现一个问题,当需要删除元素时,该元素恰巧导致了哈希冲突,如果直接删除该元素,那么因冲突重新确立存储位置的元素将无法根据哈希函数再次找到
定义一个动态线性表来构造hash表,由于在表中删除元素的时候不能直接删除,所以每个存储空间不止要存放元素的关键码,还需要标记当前该存储位置的状态,需要定义一个枚举常量来表示状态,分三种:未存放、存在有效元素、删除状态;hash函数也以函数指针的形式添加在结构体中
typedef int Key;
typedef int(*HashFunc)(Key, int);
typedef enum
{
EXIST,
DELETE,
EMPTY
} State;
typedef struct
{
Key key;
State state;
} Element;
typedef struct
{
Element *arr;
int size;
int capacity;
HashFunc hashfunc;
} HT;
哈希表的初始化&销毁
根据要求的容量动态开辟空间,别忘了对开辟的每一个空间状态初始值的设置
void HTInit(HT *pHT, int capacity, HashFunc hashFunc)
{
int i = 0;
pHT->size = 0;
pHT->capacity = capacity;
pHT->hashFunc = hashFunc;
Element *tmp = (Element *)malloc(sizeof(Element) * capacity);
assert(tmp);
pHT->arr = tmp;
for (; i<capacity; i++)
{
pHT->arr[i].state = EMPTY;
}
}
void HTDestroy(HT *pHT)
{
pHT->capacity = 0;
pHT->size = 0;
pHT->hashFunc = NULL;
free(pHT->arr);
}
查找
根据元素的关键码和哈希计算出元素的存储位置,判断当前位置的状态和关键码是否都符合,否则根据探测原则持续向后查找,如过遇到的存储位置状态为空表示查找失败
int HTSearch(HT *pHT, Key key)
{
int index = pHT->hashFunc(key, pHT->capacity);
while (pHT->arr[index].state != EMPTY)
{
if (pHT->arr[index].state == EXIST && pHT->arr[index].key == key)
{
return 0;
}
index = (index + 1) % pHT->capacity;
}
return -1;
}
删除
删除其实上也是一个查找的过程,不同的是在进行删除后,哈希表中元素个数减1,删除位置的状态发生变化
int HTDelete(HT *pHT, Key key)
{
int index = pHT->hashFunc(key, pHT->capacity);
while (pHT->arr[index].state != EMPTY)
{
if (pHT->arr[index].state == EXIST && pHT->arr[index].key == key)
{
pHT->arr[index].state = DELETE;
pHT->size--;
return 0;
}
index = (index + 1) % pHT->capacity;
}
return -1;
}
插入
插入元素的时需要考虑一个问题,是否需要扩容,虽然哈希冲突不可避免,但可以使用一些方法来降低哈希冲突
- 控制负载因子:
负载因子 = pHT->size / pHT->capacity
根据经验得知,将负载因子控制在0.8以下,可以有效降低哈希冲突,超过0.8冲突将会呈指数曲线增长
- 优化哈希函数
- 素数表:使用素数表对齐做哈希表的容量
以下采用控制负载因子,降低哈希冲突
所以在插入元素之前先要判断当前的哈希表负载因子,决定需不需要扩容的问题
需要扩容时可以分为以下三步:申请新空间、搬移数据、释放旧空间
扩容后的空间与旧空间容量发生变化,所以在数据搬移的过程中需要对元素的存储位置重新做调整
void IsExpand(HT *pHT)
{
int i = 0;
HT tHT;
Element *tmp = NULL;
int newCapacity = 0;
if (pHT->size * 10 / pHT->capacity < 7)
{
return;
}
newCapacity = pHT->capacity * 2;
HTInit(&tHT, newCapacity, pHT->hashFunc);
for (; i<pHT->capacity; i++)
{
if (pHT->arr[i].state == EXIST)
{
HTInsert(&tHT, pHT->arr[i].key);
}
}
free(pHT->arr);
pHT->arr = tHT.arr;
pHT->capacity = tHT.capacity;
}
插入之前先查找一遍,如果需要插入元素已经存在,不再进行重复插入,如果不存在,状态为EMPTY或者DELETE的位置均可以做插入
int HTInsert(HT *pHT, Key key)
{
int index = 0;
if (!HTSearch(pHT, key))
{
return -1;//元素已经存在
}
IsExpand(pHT);
index = pHT->hashFunc(key, pHT->capacity);
while (pHT->arr[index].state == EXIST)
{
index = (index + 1) % pHT->capacity;
}
pHT->arr[index].key = key;
pHT->arr[index].state = EXIST;
pHT->size++;
return 0;
}
开散列
开散列又称哈希桶,解决哈希冲突的方式是构造一个存放链表头指针的顺序表,顺序表的存储位置是通过哈希函数计算得出,也称为桶号,每条链表都是产生哈希冲突的结点
typedef int Key;
typedef int(*HashFunc)(Key, int);
typedef struct Node
{
Key key;
struct Node* next;
} Node;
typedef struct
{
Node **arr;
int size;
int capacity;
HashFunc hashFunc;
} HB;
初始化&销毁
- 初始化
初始化的时候开辟的空间都需要存放结点地址,故将其全部初始化成NULL
void HBInit(HB *pHB, int capacity, HashFunc hashFunc)
{
int i = 0;
Node **tmp = (Node **)malloc(sizeof(Node *) * capacity);
assert(tmp);
pHB->arr = tmp;
pHB->capacity = capacity;
pHB->size = 0;
pHB->hashFunc = hashFunc;
for (; i<capacity; i++)
{
pHB->arr[i] = NULL;
}
}
- 销毁
动态开辟的空间不止用于存放链表地址的动态顺序表,还有每一条链表的每一个结点,所以应该全部释放
void HBDestroy(HB *pHB)
{
int i = 0;
Node *cur = NULL;
Node *del = NULL;
for (; i<pHB->capacity; i++)
{
cur = pHB->arr[i];
while (cur != NULL)
{
del = cur;
cur = cur->next;
free(del);//删除每条链表的每一个结点
}
}
free(pHB->arr);//删除掉存放链表头指针的动态顺序表
}
查找
用哈希函数计算出桶号后,在每一个桶中去查找元素,实际上是通过遍历链表的方式查找
int HBSearch(HB *pHB, Key key)
{
Node *cur = NULL;
int index = pHB->hashFunc(key, pHB->capacity);
cur = pHB->arr[index];
while (cur != NULL)
{
if (cur->key == key)
{
return 0;
}
cur = cur->next;
}
return -1;
}
删除
删除的时候也是先做查找,当需要删除的元素是链表的第一个结点,直接让桶里的头指针指向链表的第二个结点,如果非第一个结点,就需要借助prev指针来完成链表的删除
int HBDelete(HB *pHB, Key key)
{
Node *cur = NULL;
Node *prev = NULL;
int index = pHB->hashFunc(key, pHB->capacity);
cur = pHB->arr[index];
while (cur != NULL)
{
if (cur->key == key)
{
if (prev == NULL)
{
pHB->arr[index] = cur->next;
}
else
{
prev->next = cur->next;
}
free(cur);
pHB->size--;
return 0;
}
prev = cur;
cur = cur->next;
}
return -1;
}
插入
插入之前同样需要判断是否扩容,扩容是借助另一个扩容后的临时桶来完成,因为需要进行数据的搬移,在搬移过程中由于桶的容量已经扩容,所以通过哈希函数计算出的桶号已经发生变化,数据搬移结束后,释放掉旧桶空间,启用新桶即可
int HBInsert(HB *pHB, Key key);
void IsExpandHB(HB *pHB)
{
int i = 0;
int newCapacity = 0;
Node *cur = NULL;
HB tHB;
if (pHB->size * 10 / pHB->capacity < 9)
{
return;
}
newCapacity = pHB->capacity * 2;
HBInit(&tHB, newCapacity, pHB->hashFunc);
for (; i<pHB->capacity; i++)
{
cur = pHB->arr[i];
while (cur != NULL)
{
HBInsert(&tHB, cur->key);
cur = cur->next;
}
}
HBDestroy(pHB);
pHB->arr = tHB.arr;
pHB->capacity = tHB.capacity;
}
int HBInsert(HB *pHB, Key key)
{
Node *node = NULL;
int index = 0;
if (!HBSearch(pHB, key))
{
return -1;//不再重复插入
}
IsExpandHB(pHB);
index = pHB->hashFunc(key, pHB->capacity);
node = (Node *)malloc(sizeof(Node));
node->key = key;
node->next = pHB->arr[index];
pHB->arr[index] = node;
pHB->size++;
return 0;
}