散列

以下为学习笔记

散列的插入、删除、查找时间为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)达到装填因子(散列表中元素个数/散列表的大小)时开始

posted @ 2020-01-12 11:49  江雨牧  阅读(286)  评论(0)    收藏  举报