常用算法总结
常用算法总结
前言与准备工作:
动态规划算法、贪心算法、分治算法、回溯法、分支限界法。(此部分内容可以参考博客《五大常用算法》)
排序算法:我的博客已经做出了总结,看《数据结构中常用的排序算法》。
二叉树相关算法
链表相关算法
哈希表相关算法
学习思路 --- 先了解基本的概念,然后找到相关的例题加以巩固即可。
对于基本的概念先摘录下来,知道大概的思想是什么。 对于例题,尽量自己去思考和实践,多实践几次就可以了。
学习任务: 今天7点之前对这些算法的简单题都会做即可。 后面做到了就好。 开始不要钻牛角尖。 切记切记。效率最为重要。
注意:无论如何,看书入门都是最好的途径。
学习方法: 看书,看书,看书! 千万别学习新东西的时候就去看人的博客。打好基础!
正文:
散列表查找(哈希表)概述
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定key的映射f(key),若找到了集合中存在这个记录,那么必定在f(key)的位置上。
我们把这种对应关系f称为散列函数,又称为哈希函数,这块连续存储空间就称为散列表或哈希表。那么关键字对应的记录存储位置我们称为散列地址。
散列技术即使一种存储方法也是一种查找方法,它最适合的求解问题是查找与给定值相等的记录。 但是对于那种同样的关键字有很多记录的情况就不适合了。并且散列表也不适合范围查找。
散列表中的冲突问题
在理想的情况下,不同的关键字通过散列函数(哈希函数)计算得到的地址都是不同的。
但是现实中,很有可能两个不同的关键字通过哈希函数计算得到了相同的地址,这就是冲突。(即key1 !== key2 但是f(key1) === f(key2),这就是冲突了)
散列函数(哈希函数)的构造方法
简单的说哈希函数,就是根据一个关键字找到他的地址的函数。
想要知道有什么构造方法,就得先确定一个原则,怎么样构造得到的哈希函数才是好的呢? 有下面的两个原则:
- 计算简单 --- 即通过关键字查找到对应的地址,这个过程应当简单,如果非常复杂,就没有必要了。
- 散列地址分布均匀 --- 因为分布均匀才能让存储空间合理有效的利用,并且如果分布均匀,那么出现冲突的可能就会很小了。
1. 直接定址法
如统计1990年之后出生的人数,那么关键字(key)自然就是年份了,然后我们把地址设置为 f(key) = key - 1990。如下所示:
地址 | 出生年份 | 人数 |
00 | 1990 | 500万 |
01 | 1991 | 520万 |
02 | 1992 | 560万 |
即地址就是出生年份这个关键字-1990, 然后就可以通过key很快的寻到地址了。
也就是说我们可以通过取关键字的某个线性函数为散列地址(f(key) = a X key + b),这种方法就是直接地址法。
优点: 迅速、方便、不会产生冲突
缺点: 必须连续、 必须实现知道关键字的分布情况、只适合查找表较小且连续的情况。
2. 数字分析法
数字分析法,把位数较多的数字作为关键字,如一个公司的所有工作人员的登记表,每一个人对应的一条记录,由于手机号是不重复的,所以我们可以将手机号作为关键字,然后根据手机号来确定地址,由于前面7位重复的概率比较大,所以我们可以把后面的四位电话号码作为哈希表的地址,这里所做的就是抽取工作,当然也不可避免的会出现冲突。但是我们可以通过将冲突中的一个的地址进行反转、右环位移、甚至前两位和后两位叠加的方法避免冲突即可。
注意:抽取方法是使用关键字的一部分做为散列地址的方法,这就是数字分析法。
3. 平方取中法
即对于关键字1234,求得平方为1522756,取中间的三位227作为散列地址,这就是平方取中法。 这种方法适合于不知道关键字的分布,而位数又不是很大的情况。
4. 折叠法
即将关键字从左到右分割成位数相等的几部分(最后一部分不够也没关系), 然后将这几部分叠加求和, 并按照散列表表长,取后几位作为散列地址。
如关键字是9876543210,那么我们可以将之分为3位,分成四组,987|654|321|0,然后叠加得到 987+654+321+0 = 1962,最后取后三位962作为散列表地址。
这种方法适合于事先不知道关键字的分布,适合关键字位数较多的情况。
5.除留余数法
这种方法是最常用的构造散列函数(哈希函数)的方法。 对于散列表长为m的散列函数公式为:
f(key) = key mod p (p<=m)
注意,这里选择p小于等于m是有原因的,p选不好,也有可能造成冲突,根据前辈们的经验,若散列表的表长为m,那通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
6. 随机数法
选择一个随机数,取关键字的随机函数为它的散列地址。 也就是f (key) = randow(key)。
有同学问了,你说的关键字都是数字,如果关键字是字符串怎么办? ---可以转换为ASCII码或者Unicode码啊。
处理散列冲突的方法
在构建散列表时,难免会有冲突,我们怎么解决冲突呢? 这就是一个问题了。
比如,你买房,已经看好了,准备交钱的时候发现房子已经给卖掉了,但你可以再换一家啊,是不是,只要还有房子就行。于是,开放地址法就是这样的一种思路。
开放地址法:
即一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,那么空的散列地址一定是可以找到的,并将记录存入,公式如下:
fi(key) = ( f(key) + di ) MOD m (di = 1,2,3...m-1)
即本该存入一个位置的记录发现该地址已经被占用了,所以产生了冲突,那么我就存到下一个散列地址中去,这样就成功的避免了冲突。
问题: 如果冲突较多,那么可能一个将要存入的记录需要移动很多次才能到下一个, 而假设他左边就有一个空的,但是我们的规则却一直向右走。这就是 线性探测法。
但是我们如果能让它在两边找位置不是更好吗? 于是新的规则如下:
fi(key) = ( f(key) + di ) MOD m (di = 12,-12,22,-22...q2,-q2, q<= m/2)
这样我们可以看到查找的时候是双向查找的,并且不是连续查找的,这就是二次探测法。好处是不让关键字都聚集在某一块区域,并提高了效率。
还有一种方法是随机探测法, 这种犯法是产生一个随机数列,然后去查找。
再散列函数法:
同样以买房子为例,你之前看房子都在市中心,但市中心没了(冲突了),你还可以去郊区看看啊。
对于我们的散列表而言,我们实现准备多个散列函数。
fi(key) = RHi(key) (i=1,2....k)
其中RHi就是不同的散列函数,这些散列函数是用了什么除留余数、折叠、平方取中都用了的。 每当冲突产生时,我们就换一个散列函数,总有不冲突的。
优点: 可以使得关键字对应的地址不产生聚集。 缺点: 增加了计算的时间。
链地址法:
有冲突我也可以不走啊,直接将所有关键字为同义词的记录存储在一个单链表里就可以了啊,我们称之为同义词子表。 在散列表中只存储所有同义词子表的头指针。
链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会找不到地址的保障,但是在查找时需要遍历单链表,这会带来性能损耗。
公共溢出区法:
这种方法更好理解,即你不是冲突吗? 凡是因为冲突没地去的都跟我走,即为所有冲突的关键字建立一个公共的溢出区来存放。
在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行对比,如果相等,则查找成功;如果不等,则到溢出表中进行顺序查找。 如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
散列表查找实现(重点内容)
首先需要定义一个散列表的结构以及一些相关的常数。其中HashTable就是散列表结构。 结构中的elem是一个动态数组。
#define SUCCESS 1 #define UNSUCCESS 0 #define HASHSIZE 12 #define NULLKEY -32768 typedef struct { int *elem; // 数据元素存储基址,动态分配数组 int count; }HashTable; int m = 0;
已经有了HashTable的定义,我们就可以对这个散列表进行初始化了。
/*初始化散列表*/ Status InitHashTable (HashTable *H) { int i; m = HASHSIZE; H->count = m; // 即哈希表中的长度为12 H->elem = (int *)malloc(m*sizeof(int)); //即给数组elem指针分配一个长度为12的地址 for (i=0;i<m;i++) { H->elem[i]=NULLKEY; // 每一个地址都是NULLKEY,表示这个地址为空,这就是初始化 } return OK; }
为了在插入时计算地址,我们需要定义散列函数,散列函数可以根据不同的情况更改算法:
/*散列函数*/ int Hash(int key) { return key%m; /*除留取余法*/ }
初始化之后,我们可以对散列表进行插入操作。 假设我们插入的关键字集合就是前面的{12,67,56,16,25,37,22,29,15,47,48,34}。
/*插入关键字进入散列表(刚刚只是初始化了)*/ void InsertHash(HashTable *H,int key) { int addr = Hash(key); /*求散列地址*/ while (H->elem[addr] != NULLKEY ) /*如果不为空,则冲突*/ { addr = (addr + 1)%m; /*开放定址法的线性探测*/ } H->elem[addr] = key; /*有空位就插入关键字*/ }
散列表存在之后,我们就可以在需要的时候通过散列表来查找需要的记录了。
/*散列表查找关键字*/ Status SearchHash(HashTable H,int key, int *addr) { *addr = Hash(key); while (H.elem[*addr] != key ) { *addr = (*addr + 1) % m; if (H.elem[*addr]==NULLKEY || *addr == Hash(key)) { return UNSUCCESS; } } return SUCCESS; }