之前在写过的很多的结构中都实现过搜索功能,但是不论是顺序搜索还是搜索二叉树,由于元素的存储位置和元素各关键码之间没有对应的关系,所以在查找一个元素的时候必须要经过关键码的多次比较。因此搜索的时间复杂度很难能够达到O(1)。
但是如果有一种存储结构,通过某种函数使元素的存储位置与他的关键码之间能够建立一一映射的关系,那么就能够直接地查找到需要地元素,时间复杂度就达到了O(1),那么这种结构就是哈希表。
哈希表
例如:数据集合{10,51,14,33,18,29},哈希函数Hash(key) = key%m(m为内存单元的个数,可以自定义),在这里定义为10
Hash(10)=0 Hash(51)=1 Hash(14)=4 Hash(33)=3 Hash(18)=8 Hash(29)=9
插入元素:根据待插入元素的关键码,按照哈希函数计算出该元素的存储位置,并按照此位置进行存放
搜索元素:对元素的关键码进行相同的计算,把求得的函数值当作元素的存放位置在结构中按照此位置取出元素比较,若关键码相同,则认为搜索成功
哈希函数设计原则:
(1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
(2)哈希函数计算出来的地址能均匀分布在整个空间中
(3)哈希函数应该比较简单
哈希冲突
若在上述哈希表中插入元素13,按照上面的哈希函数,应该插入的位置为下标3号的位置,但此时下标为3的位置已经有了元素,此时就发生了哈希冲突,即不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞
引起哈希冲突的原因:哈希设计不够合理
解决哈希冲突的方法:闭散列和开散列
闭散列
也叫开放地址法,当发生哈希冲突时,若哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到表中的“下一个”空位中去
可以用线性探测的方式来找到下一个空余位置
线性探测的处理:从发生冲突的位置开始,依次继续往后探测,直到找到空位置为止
采用线性探测缺点:
一旦发生哈希冲突,所有冲突连在一起,容易产生数据“堆积”,即,不同关键码占据了可以利用的空位置,使得寻找某关键码的位置需要许多次的比较,导致搜索效率降低
负载因子
散列表的负载因子:a = 填入表中的元素个数 / 散列表的长度
当发生哈希冲突时,不同的关键码占据了可利用的空位置,使得寻找某关键码的位置需要进行多次比较,导致搜索效率降低。
当哈希表长一定时,填入表中的元素越多时,发生冲突的概率就越大,所以要控制插入表中的元素个数。因此引入负载因子。
所以当哈希表的负载因子达到一定的数值时,就不能要考虑扩容来使降低扩容因子,从而使搜索效率提高。
开散列
又称为链地址法(开链法)
开散列:首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个集合称为一个桶,各个桶中的元素通过一个单链表连接起来,各链表的头节点存储在哈希表中
下面是基于线性探测实现哈希表
哈希表的结构
使用数组来实现哈希表的结构,哈希表中的元素类型是键值对,分别用key和value来表示,其中key值表示下标,可以根据key值来寻找对应的value
同时还要
定义一个状态标志位,来记录该位置是未插入的状态还是已插入的状态。在这里定义Empty表示空状态,即可以进行插入操作;用Valid表示有效状态,即该位置已经有元素,不能进行插入操作了;用Delete表示被删改的状态,表示该值被修改
哈希表HashTable中有三个元素,分别是数组、哈希表中元素的个数、哈希函数
6 //我们此处的Hash表希望存储的数据是键值对这样的结构 7 #define HashMaxSize 1000 8 typedef int KeyType; 9 typedef int ValueType; 10 typedef size_t (*HashFunc)(KeyType key); 11 12 typedef enum 13 { 14 Empty,//空状态 15 Valid,//有效状态 16 Delete,//被删改的状态 17 }Stat; 18 19 //这个结构体表示Hash表中的一个元素,这个元素中同时包含了键值对 20 typedef struct HashElem 21 { 22 KeyType key; 23 ValueType value; 24 Stat stat; 25 }HashElem; 26 27 //[0,size)这个区间就不能表示Hash表中有效元素的区间 28 typedef struct HashTable 29 { 30 HashElem data[HashMaxSize]; 31 size_t size; 32 HashFunc func;//这是一个函数指针,指向一个Hash函数 33 }HashTable; 34
1.哈希表的初始化操作
将哈希表中的元素个数置为0,哈希函数为给定的值,将数组中每个元素的状态置为Empty表示可插入状态
11 void HashInit(HashTable* hashtable,HashFunc hash_func) 12 { 13 hashtable->size = 0; 14 hashtable->func = hash_func; 15 size_t i = 0; 16 for(;i < HashMaxSize; ++i) 17 { 18 hashtable->data[i].stat = Empty; 19 } 20 return ; 21 } 22
2.哈希表的销毁操作
将哈希表中元素个数置为0,哈希函数置为空,数组中元素的状态置为Empty
23 void HashDestroy(HashTable* hashtable) 24 { 25 hashtable->size = 0; 26 hashtable->func = NULL; 27 size_t i = 0; 28 for(;i < HashMaxSize; ++i) 29 { 30 hashtable->data[i].stat = Empty; 31 } 32 return ; 33 }
3.哈希表的插入操作
(1)首先根据负载因子来判断哈希表是否能够继续插入元素,若哈希表中元素的个数大于等于哈希表的负载因子上线,则认为不能插入
注:负载因子可以自定义,这里定义为0.8
(2)若可以继续插入元素,根据要插入元素的key来计算要插入位置的下标offset
(3)从offset位置开始线性的往后查找,找到的第一个状态为Empty的元素进行插入操作,将value值插入到该位置,并将状态修改为Valid状态,同时将哈希表的元素个数加1
(4)若发现key相同的元素,则认为插入失败
35 void HashInsert(HashTable* hashtable,KeyType key,ValueType value) 36 { 37 if(hashtable == NULL) 38 { 39 //非法操作 40 return ; 41 } 42 //1.判定hash表是否能继续插入(根据负载因子判定) 43 //此处只是把负载因子定义为0.8 44 if(hashtable->size >= 0.8*HashMaxSize) 45 { 46 //发现当前的hash表已达到负载因子的上限,此时直接插入失败 47 return ; 48 } 49 //2.根据key来计算offset 50 size_t offset = hashtable->func(key); 51 //3.从offset位置开始线性的往后查找,找到第一个状态为empty的元素进行插入 52 while(1) 53 { 54 if(hashtable->data[offset].stat == Empty) 55 { 56 //此时找到合适的位置放置要插入的元素 57 hashtable->data[offset].stat = Valid; 58 hashtable->data[offset].key = key; 59 hashtable->data[offset].value = value; 60 //5.++size 61 ++hashtable->size; 62 return ; 63 } 64 else if(hashtable->data[offset].stat == Valid && hashtable->data[offset].key == key) 65 { 66 //4.若发现key相同的元素,此时认为插入失败 67 //hash表中存在了一个key相同的元素 68 //认为插入失败 69 //若要修改value的值就放开下面这行代码 70 //hashtable->data[offset].value = value; 71 return ; 72 } 73 else 74 { 75 ++offset; 76 if(offset >= HashMaxSize) 77 { 78 offset = 0; 79 } 80 } 81 } 82 return ; 83 }
4.哈希表的查找操作
(1)先对哈希表进行合法性判断还要判断哈希表元素个数是否为0
(2)再根据key来计算出要插入的元素的下标offset
(3)从offset位置开始往后查找,每取到一个元素就与key做比较
a)若找到key相同的值,且状态为Valid状态,认为查找成功,返回1
b)若发现当前的值与value不相等(或者状态为Empty),就继续往后查找
c)若发现当前元素为空(NULL),就认为查找失败
85 int HashFind(HashTable* hashtable,KeyType key,ValueType* value) 86 { 87 if(hashtable == NULL || value == NULL) 88 { 89 return 0; 90 } 91 if(hashtable->size == 0) 92 { 93 //空hash表 94 return 0; 95 } 96 //1.根据key计算出offset 97 size_t offset = hashtable->func(key); 98 //2.从offset开始往后查找,每次取到一个元素,使用key进行比较 99 while(1) 100 { 101 if(hashtable->data[offset].key == key && hashtable->data[offset].stat == Valid) 102 { 103 //找到了 104 //a)找到了key相同的元素,此时直接返回value,认为查找成功 105 *value = hashtable->data[offset].value; 106 return 1; 107 } 108 else if(hashtable->data[offset].stat == Empty) 109 { 110 //查找失败 111 //b)发现当前key不相同,就继续往后查找 112 return 0; 113 } 114 else 115 { 116 //c)若发现当前元素为NULL,认为查找失败 117 ++offset; 118 offset = offset>= HashMaxSize?0:offset; 119 } 120 } 121 return 0; 122 } 1235.哈希表的删除操作
(1)
先对哈希表进行合法性判断还要判断哈希表元素个数是否为0
(2)再根据key来计算出要插入的元素的下标offset
(3)从offset位置开始依次判断当前元素的key值与要删除的key值是否相同
a)若当前的key就是要删除的key值,且当前状态为Valid,删除当前元素即可,并将删除元素标记为Delete状态,同时将哈希表中的元素减1
b)若当前元素为NULL,即当前元素的状态为Empty,表示当前元素查找失败
c)否则,将offset++,线性的往下进行查找
124 void HashRemove(HashTable* hashtable,KeyType key) 125 { 126 if(hashtable == NULL) 127 { 128 return ; 129 } 130 if(hashtable->size == 0) 131 { 132 return ; 133 } 134 //1.根据key计算offset 135 size_t offset = hashtable->func(key); 136 //2.从offset开始依次判断当前元素的key和要删除的key是不是相同 137 while(1) 138 { 139 if(hashtable->data[offset].key == key && hashtable->data[offset].stat == Valid) 140 { 141 //a)若当前的key就是要删除的key,删除当前元素即可,删除元素要引入一个新的状态标记Delete 142 //找到了要删除的元素,要标记成 Delete 143 hashtable->data[offset].stat = Delete; 144 --hashtable->size; 145 return ; 146 } 147 else if(hashtable->data[offset].stat == Empty) 148 { 149 //b)若当前元素为NULL,key在hash表查找删除失败 150 //删除失败 151 return ; 152 } 153 else 154 { 155 //c)剩下的情况++offset,线性的探测下一个元素 156 ++offset; 157 offset = offset >= HashMaxSize?0:offset; 158 } 159 } 160 return ; 161 }