哈希表之一初步原理了解

考虑如下场景:小明住在芳华小区,芳华小区中有很多幢房子,每个房子有十几二十层,每层有4个住户;

现在小红要去找小明玩耍,现在假设小红只知道小明住在芳华小区,但是不知道住在哪一幢,哪一层和哪一户,

那么小红要怎么才能找到小明呢?

那么毫无疑问,小红只有在芳华小区中一家一家地敲门问小明住在哪儿?

(此时不允许小红叫一个收破烂的拿着喇叭在楼下喊:"小明,额想你!")

这个时候,如果小红有芳华小区的所有住户的明细的话,就可以从明细上找到小明住的地方,然后根据这个地方在芳华小区中找到小明,

而不需要敲遍小区所有住户的门。

 

同样,现在内存中有一数组,我们需要判断在数据中是否存在某个元素的时候,如果没有其他额外的信息,唯一的办法就是遍历这个数组,

因为这个时候我们只有这个这个数组的下标可用。

 

那么联想到上面小红找小明的例子,其实有类似之处,数组中的元素和下标现在是没什么联系的;不能根据元素值来定位到元素如果存在

在数组中的时候应该在哪个位置,要是我们想小红一样,也有一个"住户的明细"的话,任意给一个元素,我们根据元素的值找到其对应

在数组中的下标的值,然后直接根据这个下标的值直接去访问数组,便可以知道这个元素是否存在了。

 

现在出现了两个需要注意的东西:

(1)"住户的明细"

(2)找到

"住户的明细"其实从数学上来说可以看做一个对应关系,一个映射,也就是现在要讨论的哈希表。这个表记录了元素和其在数组中下标的

对应关系,待会我们就会对某个数组做一张这样的表;

"找到",其实就是根据元素的值计算出其在数组中的下标(位置)。

 

 

哈希表中有个两需要解决的问题:

(1)找到一个尽可能将元素分散的放到数组中去,不要让不同的元素都分到同一个位置去了

(2)万一确实通过hash函数计算之后分到了同一个地方,我们得有相应的措施来处理这种情况

 

现在有如下几个数:1,2,4,5,7,8,10,11;现在为了方便查找,我们可以这样放

 

这里的数据放置的规则很简单:

1%7=1,放到数组的第1个位置;

2%7=2,放到数组的第2个位置;

......

11%7=4,放到数组的第4个位置;

这里元素的值对7求余就是所选择的hash函数。如果我们要找数组中是否存在某个数x,可以用x%7求得一个数,

这个数就是在这个数组中应该存在的位置,此时直接通过这个位置定位数组中的这个元素,就可以很快的判断这个数在数组中

存不存,而不需要一个一个遍历数组。

图中可以看到1和8对7求余的余数是相同的,导致了它们被放到了同一个位置上,这就是属于冲突的情况。

哈希表中解决冲突的情况有两种方法:

一种方法就是如果冲突了的话就纵向的添加冲突的元素,如图中那样,先用hash算出元素应该出现的坑,然后在通过链表进行普通查找;

另一种方法就是继续横向的添加,具体怎么添加还没摸清楚~,反正hash基本原理就是这样的。

 

哈希表的如果建的好的话,查找元素的时间复杂度是O(n)哦,典型的时间换取空间。

 

有助于理解的图:

 

 

 

 下面用C来写一个简单的hash表:

 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 
 4 #define LIST_LENGTH 100
 5 
 6 typedef struct Item Item;
 7 
 8 struct Item {     // 用链表来解决冲突
 9   int value;
10   int index;
11   Item * next;
12 };
13 
14 typedef struct List {
15   Item *head;
16   Item *end;
17 } List;
18 
19 void insertItem(List *list, Item *item) {
20 
21   int key = (unsigned int)(item->value) % LIST_LENGTH; // 键的计算方法
22   // 判断list中是否存在元素
23   if(!list[key].head) {    // 这个槽之前没有元素
24     list[key].head = item;
25     list[key].end = item;
26   } else {                 // 已经有元素放到了这个位置
27     list[key].end->next = item;
28     list[key].end = item;
29   }
30 }
31 
32 int searchValue(List *list, int value) {
33   // 计算出元素的键值
34   int key = (unsigned int)(value) % LIST_LENGTH;
35   Item *p = list[key].head;
36 
37   while(p) {
38     if(value == p->value) {
39       return p->index+1; // 万一元素在数组中的索引就是在0处, 直接返回0的话和后面返回的0冲突了
40     }
41     p = p->next;
42   }
43   return 0;
44 }
45 
46 int main(void) {
47 
48   int array[] = {2, 3, 5, 7, 8, 9, -3, -8, 22, 13};
49   int num = 10;              // 数组元素的个数
50   int i = 0;
51   List list[LIST_LENGTH];    // 直接分配了这么一个结构体数组存放链表和key的对应关系
52   memset(list, 0, sizeof(List) * LIST_LENGTH);
53 
54   Item item[10];             // 直接分配了一个结构体数组存放元素
55   memset(item, 0, sizeof(Item) * num);
56 
57   // 遍历数组, 先将元素组装为一个结构体元素, 你可以看做面向对象编程中的对象
58   for(i=0; i<num; i++) {
59     item[i].value = array[i];
60     item[i].index = i;
61     insertItem(list, &item[i]);   // 把对象的引用放到hash表中
62   }
63 
64   int test_arr[] = {-34, 2, 5, 42, -32, 12, 6, -8};
65   int num2 = 8;
66 
67   printf("数组元素为:");
68   for(i=0; i<num; i++) {
69     printf("%d ", array[i]);
70   }
71   printf("\n");
72   printf("测试数组元素为:");
73   for(i=0; i<num2; i++) {
74     printf("%d ", test_arr[i]);
75   }
76   printf("\n\n");
77 
78   for(i=0; i<num2; i++) {
79     int position = searchValue(list, test_arr[i]);
80     if(position) {
81       printf("数组array中存在元素%d, 下标为%d\n", test_arr[i], position-1);
82     } else {
83       printf("数组array中不存在元素%d\n", test_arr[i]);
84     }
85   }
86 
87   return 0;
88 }

后续会关注的有:

1. 哈希表的变种,不过目前也就知道又一个一致性hash,这个实在memcached这个缓存框架中会用到;

2. java中其实已经实现了哈希表这种数据结构,用起来很方便,到时候可以跟一下源码;

3. java中的equals方法和hashcode方法的关系是什么样的,以及这两个方法应该如何去写才比较好;

================================================2017年7月1日

我们在查找的时候,其实是和元素的值进行比较;

顺序找到的时候,比较的结果为"="与"!="两种;

在二分查找、二叉树排序查找与B-树查找时,比较的结果为"<","="和">"这3种;

以上查找的效率依赖于查找过程中所进行的比较次数

理想情况下不需要经过任何比较,一次存取便能得到所查的记录,那就必须在记录的存储位置和他的关键字之间建立一个确定的

对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。 

 

哈希函数很灵活,能够把关键字经过哈希函数一运算,然后将得到的哈希值落在表长的允许范围之内;

哈希表的关键字和下标位置的对应必须能够随机访问,感觉应该是只有数组才可以满足这个;

不同的关键字哈希之后可能得到同一哈希地址:

但是呢

这种现象就是冲突,肯定是冲突越少越好了,但是有时候有不能完全避免。

为什么一定会有冲突?

C语言中可以对源程序的标识符建立一张哈希表,假设哈希表存的是所有关键字集合包括所有的可能关键字,

标识符的个数肯定是很多很多的,排列组合算一下就知道估计又是10的多少次方,但是我们在程序中实际用到的

标识符没这么多,顶天了设置为1000,但是这1000个可能存储的标识符就是多种多样的,这样hash存储实际

上就是一个压缩映像,所以不可避免的产生冲突。

这样来说:把一万个球放到一千个盒子中,肯定又一个盒子中的球会多于两个;实际上有可能我们只是把一万个球

中的500个放到这1000个盒子中,但是这500个是可以随便从那一万个中选的,虽然500<1000,但是仍旧可能产生冲突。

 结论:在构造哈希表的时候不仅要设定一个好的哈希函数,还有设定一个好的方法来处理冲突。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值