散列
以下为学习笔记
散列的插入、删除、查找时间为O(1),因为其不是像树一样通过比较来进行上面的操作,而是直接进行。
理想的散列表结构:一个包含有关键字的具有固定大小的数组。
表的大小记为TableSize,index从0到TableSize-1。
散列函数
(1)散列函数:每个关键字通过散列函数映射到0~TableSize-1这个范围的某个数,并且关键字被放到散列表适当的单元中。
(2)理想散列函数的要求:两个不同的关键字被映射到不同的单元;在TableSize大小的散列表里均匀地分配关键字。
(3)如果输入的关键字为整数,则一般合理的方法是返回“key mod TableSize”的结果;
通常,关键字是字符串,一种方法是把字符串中字符的ASCII码值加起来,再对TableSize求余。
冲突
(1)冲突:当一个元素插入散列表时,其插入的单元已经存在另一个元素时,就产生冲突,冲突需要消除。
(2)解决冲突的常用方法:分离链接法和开放定址法
分离链接法
其做法是将散列到同一个单元的所有元素保留到一个表中(即建立个链表保存这些冲突的元素),为了方便,这些表都有表头。
代码如下:
1 #include <iostream> 2 using namespace std; 3 4 struct ListNode//链表结点 5 { 6 int data;//数据 7 struct ListNode* next;//next指针 8 }; 9 typedef struct ListNode* list; 10 typedef struct ListNode* position; 11 struct HashTable 12 { 13 int TableSize;//散列表大小 14 list TheLists;//定义一个指针指向散列表数组 15 }; 16 typedef struct HashTable* Hash; 17 18 //散列函数,返回该关键字在散列表的位置 19 unsigned int hash_fun(unsigned int key, int tablesize) 20 { 21 return key%tablesize; 22 } 23 //初始化散列表 24 Hash initialize_table(int tablesize) 25 { 26 Hash H; 27 H = (Hash)malloc(sizeof(HashTable)); 28 H->TableSize = tablesize; 29 H->TheLists = (list)malloc(sizeof(ListNode)*tablesize);//new ListNode[tablesize]; 30 for (int i = 0; i < tablesize; i++) 31 (H->TheLists+i)->next = NULL; 32 return H; 33 } 34 //释放散列表 35 void destroy_table(Hash H) 36 { 37 position p, tmp,p1=H->TheLists; 38 for (int i = 0; i < H->TableSize; i++) 39 { 40 p = H->TheLists->next; 41 while (p != NULL) 42 { 43 tmp = p->next; 44 free(p); 45 p = tmp; 46 } 47 H->TheLists++; 48 } 49 free(p1); 50 } 51 //找数据 52 position find(Hash H, int data) 53 { 54 position p; 55 list L; 56 L = H->TheLists + hash_fun(data, H->TableSize);//先找到所属的哪个链表 57 p = L->next; 58 while (p != NULL && p->data != data)//然后在所属链表里找 59 p = p->next; 60 return p;//返回指针,没有找到则为NULL 61 } 62 //放数据 63 void insert_data(Hash H,int data[],int n) 64 { 65 position pos,p,tmp; list L; 66 for (int i = 0; i < n; i++) 67 { 68 pos = find(H, data[i]); 69 if (pos == NULL)//散列表里没有这个数据 70 { 71 L = H->TheLists + hash_fun(data[i], H->TableSize);//先找到所属的那个链表 72 p = (position)malloc(sizeof(ListNode)); 73 tmp = L->next; 74 L->next = p; 75 p->data = data[i]; 76 p->next = tmp; 77 } 78 //若已经有这个数据则什么也不干 79 } 80 } 81 //扫描散列表 82 void scan(Hash H) 83 { 84 position p; list L; 85 for (int i = 0; i < H->TableSize; i++) 86 { 87 L = H->TheLists + i; 88 p = L->next; 89 while (p != NULL) 90 { 91 cout << p->data << " "; 92 p = p->next; 93 } 94 } 95 } 96 int main() 97 { 98 Hash H; 99 int tablesize = 10; 100 int data[] = { 0,1,4,9,16,25,36,49,64,81 }; 101 H = initialize_table(tablesize); 102 insert_data(H, data, 10); 103 scan(H); 104 destroy_table(H); 105 system("pause"); 106 return 0; 107 }
运行结果:

开放定址法
分离链接法缺点是需要指针,且给新单元分配地址需要时间,这使得算法的速度较慢。开放定址法,在发生冲突时尝试在原来的散列表里选择空的单元(而不是建新的空间),直到找到空的单元。单元的选择依据(Hash(X)+F(i))%TableSize,i=0,1,2,3……(遇到冲突时i才加1,没冲突时则不再加),Hash(X)为散列函数,即开始时映射出的关键字在散列表的位置。根据F(i)的不同可以分为线性探测法、平方探测法、双散列。
1)线性探测法
F(i)=i。
只要表够大,总能找到一个空间,但是如此花费的时间是较多的;即使表较空,元素占据的单元也会形成一些区块,称为一次聚集,这使得散列到区块的关键字要多次选单元才能解决冲突。
代码如下:
1 #include <iostream> 2 using namespace std; 3 4 enum KindOfEntry { Legitimate, Empty, Deleted };//表示散列表某单元是存在数据、空的还是删除了(懒惰删除) 5 struct HashNode//散列表单元 6 { 7 int data; 8 KindOfEntry info; 9 }; 10 typedef struct HashNode* position; 11 struct HashTable//记录散列表的信息 12 { 13 int TableSize; 14 HashNode* TheCells; 15 }; 16 typedef HashTable* Hash; 17 18 //散列函数 19 unsigned int fun_hash(unsigned int key, int tablesize, int i) 20 { 21 return (key % tablesize + i) % tablesize; 22 } 23 //散列表初始化 24 Hash initialize_hash(int tablesize) 25 { 26 Hash H; 27 H = (Hash)malloc(sizeof(HashTable)); 28 H->TableSize = tablesize;//初始化散列表大小 29 H->TheCells = (position)malloc(sizeof(HashNode)*tablesize); 30 for (int i = 0; i < tablesize; i++) 31 { 32 (H->TheCells + i)->info = Empty;//散列表的单元全部初始化为空 33 (H->TheCells + i)->data = -1;//初始化里面数据为-1,表示没有填进数据 34 } 35 return H; 36 } 37 //查找能插入的位置 38 position find(Hash H, int data) 39 { 40 unsigned int current_pos,i=0; 41 current_pos = data % H->TableSize; 42 while ((H->TheCells + current_pos)->data != data && (H->TheCells + current_pos)->info != Empty) 43 { 44 current_pos++; 45 if (current_pos >= H->TableSize) 46 current_pos =0; 47 } 48 return H->TheCells + current_pos; 49 } 50 //插入 51 void insert(Hash H, int data[],int n) 52 { 53 for (int i = 0; i < n; i++) 54 { 55 position p = find(H, data[i]); 56 if (p->info != Legitimate) 57 { 58 p->data = data[i]; 59 p->info = Legitimate; 60 } 61 } 62 } 63 //扫描 64 void scan(Hash H) 65 { 66 for (int i = 0; i < H->TableSize; i++) 67 { 68 if ((H->TheCells + i)->info != Deleted) 69 cout << (H->TheCells + i)->data << " "; 70 else 71 cout << -1 << " "; 72 } 73 } 74 //懒惰删除 75 void lazy_delete(Hash H, int data) 76 { 77 int i = 0; 78 while ((H->TheCells + i)->data != data) 79 i++; 80 (H->TheCells + i)->info = Deleted; 81 } 82 int main() 83 { 84 Hash H; 85 int data[] = { 89,18,49,58,69 }; 86 H = initialize_hash(10); 87 insert(H, data, 5); 88 scan(H); 89 cout << endl; 90 lazy_delete(H, 89); 91 scan(H); 92 system("pause"); 93 return 0; 94 }
运行结果:

2)平方探测法
F(i)=i^2。
消除线性探测的一次聚集问题
一旦散列表被填了超过一半,当表的大小不是素数时甚至在表被填满一般之前,就不能保证一次找到一个空的单元了。
定理:使用平方探测,且表的大小时素数,那么当表至少有一般是空的时候,总能插入一个新的元素。
3)双散列
F(i)=i*hash2(X)。hash2(X)是新的散列函数,一般hash2(X)=R-(X%R)
再散列
当散列表快要填满时,操作的运行时间会变长,且insert操作会失败,此时一种解决方法是建立另外一个大约两倍大的表,扫描整个原始散列表,(使用新的散列函数)映射到到新表中。
可以在以下情况时开始再散列:
1)表满一半时即开始再散列
2)插入失败时开始
3)达到装填因子(散列表中元素个数/散列表的大小)时开始

浙公网安备 33010602011771号