哈希 - O(1)的摊销复杂度 - 搜索与哈希(上)

        这次老师给我们讲了哈希,但因为时间的缘故,没有写完。现在正好有些时间,便在此补上这篇博客。
搜索算法

        如果给出一个序列,要求在这个序列中寻找一些元素,那你会怎么做呢?如果允许一些预先操作,你又会怎么做,使得你的方法稳定、高效?

        第一种方法是顺序搜索。顺序搜索,就是从某个元素开始,按某个顺序,遍历整个序列,如果找到,则返回找到,反之返回找不到。这种方法的优点,第一是简便,第二是通用。这种方法地球人都能想到,而且在不允许任何预先处理的情况下使用,这是唯一的办法。

       样例代码给出如下(注:仅给出核心部分,已过测试

const int MaxLen=10001;
int array[MaxLen];
bool sequential_search(const int& begin,const int& end,const int& value){
for (int i=begin;i<end;++i){
if (array[i]==value){
return true;
}
}
return false;
}

        很简单就可以看出其的时间复杂度为O(n)。

        第二种方法是二分查找。这种方法需要排序的预先操作,比如给出一个从小到大的排序好的序列S,一个元素K,在这个序列S中以二分查找的形式,查找这个元素K,具体过程可以概括为:

1.找出序列S的中点的元素E,E将整个序列S分成前半部与后半部两部分;

2.如果K=E,则返回找到;

3.如果K<E,则在前半部递归这个过程予以查找;

4.如果K>E,则在后半部递归这个过程予以查找;

5.如果查找都没有成功,则返回找不到。

        根据分析,每层的时间复杂度为O(1),一共有log2n层。所以,可以看出整的时间复杂度是为O(log2n)。但是二分查找有一个显著的缺点:当寻找次数过小时,预先的排序操作会成为一个巨大的包袱,当然,保证的输入除外。如果查找的次数太小,比如说只有一两次,那最快的快速排序的排序速度也只有O(nlog2n),总的来说是O(log2n+log2n),远远大于O(n)的线性时间复杂度。不过二分查找的最大优势还是在的,一般而言,二分搜索的效率还是很高的。
        样例代码给出如下(注:仅核心代码,已过测试):

const int MaxLen=10001;
int array[MaxLen];
int binary_search(int begin,int end,int value){
if (array[begin]!=value && begin==end){
return -1;
}
if (array[begin]==value && begin==end){
return begin;
}
int mid=(begin+end)/2;
if (array[mid]==value){
return mid;
}
if (array[mid]<value){
return binary_search(begin,mid-1,value);
}
if (array[mid]>value){
return binary_search(mid+1,end,value);
}
}

但是,有很多时候,我们需要一个最高效的算法。比如说,在编译器的内部,要大量的查询一些程序中的字符串,如果不够高效,那编译器,特别是那些商业的编译器就肯定卖不出去。再比如搜索引擎,在一些时候,也需要用到一些这样的搜索算法,再比如数据库,等等。下面让我们请出这篇博文的主角:哈希算法。
哈希算法,又称散列算法,能大大提高搜索的效率。它的主要工作是将一个数字映射到一个表格的某个地方。打一个比喻,哈希就像那些公司前台的接待人员,直接将领导的电话记住。而哈希,就是将每一个元素的位置记住,就是我们不去找某个东西,而是将它的位置算出来。那么,有哪些方法来求出哈希值呢?我们需要一个传说中的哈希函数,在这里设这个哈希函数为H(x)。
直接取值法
直接取值法,就是直接以当前元素的值来决定它的位置。化成函数就是 H(x)=x。这种方法的好处是不可能冲突,除非两个元素一模一样。而且这样甚至能够保证在哈希表里面的元素有序,就像计数排序一样。
但是这种方法也有缺点,当x的取值太大的时候,耗费的空间同时也会很大。举个例子,如果有3个数:3,6814246421,1654654614874213,那光是这三个数,就已经耗费了巨大的内存空间了。
除法哈希
既然直接取值会耗费很大的内存空间,那我们可以模一下这个变量,一般来说,模一个数组长度,就是不错的选择。这样既可以刚刚好放下这些数据,又不会耗费太多的空间。化成函数就是H(x)=x%m。但是这样就会出现冲突。所谓冲突,就是指两个取值不一样的数,它们在哈希后得出的值相同,映射到了同一个位置。也就是说,a!=b,但H(a)==H(b)。在这里我们先不讨论冲突。那怎样尽量避免冲突呢?答案就是:模一个素数!可以证明,当H(x)定义中x%m的m的因数越多,则冲突的概率就越大。不过,其实最好的方法还是增大表格的大小,这样相应的,x%m的取值也会更为多样化。
位运算哈希
除法哈希的缺点之一,是容易冲突,而且有的时候甚至还不与整一个数相关。下面我就介绍一种位运算哈希,这种哈希主要运用乘法,而且多是位运算,速度较快。同时,除法哈希要求数组长度最好是一个素数,但在计算机中,我们更喜欢让数组长度为2的幂数,这样就不会浪费空间。确切地说,就是利用位运算,充分的混合元素。举个例子,ELFHash就是一个很好的实现。

unsigned int ELFhash(char *key)
{
unsigned long h=0;
while(*key)
{
h=(h<<4)+*key++;
   unsigned long g=h&0Xf0000000L;
   if(g) h^=g>>24;
   h&=~g;
   }
  return h%MOD;
}


乘法哈希
最后介绍一种最实用且最容易记的哈希算法。这种哈希函数叫做乘法哈希。其原理就是将原数看做一个n进制的数在转换回十进制。这种哈希算法的典型实现有BKDRHash。理解起来很容易,也是奥赛中经常用到的算法,一般来说冲突率非常小。

顺带附上BKDRHash的核心代码(已过测试):

unsigned int BKDRHash(char *key){
unsigned int seed=131;
unsigned int hash=0;

while(*key)
{
hash = hash * seed + (*key++);
}
return hash%MOD;
}



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值