每天5分钟用C#学习数据结构(32)查找 Part 3

【基础知识】|作者 / Edison Zhou

从本篇开始,我们开始讨论基于哈希技术的查找方法。哈希(散列)技术既是一种存储方法,也是一种查找方法。然而它与线性表、树、图等结构不同的是,前面几种结构,数据元素之间都存在某种逻辑关系,可以用连线图示表示出来,而哈希技术的记录之间不存在什么逻辑关系,它只与关键字有关联。因此,哈希主要是面向查找的存储结构。哈希技术最适合的求解问题是查找与给定值相等的记录。

本篇为哈希技术的基础概念部分,阅读时间大约为5min+。

1哈希定义的引入

这里首先看一个场景:在大多数情况下,数组中的索引并不具有实际的意义,它仅仅表示一个元素在数组中的位置而已,当需要查找某个元素时,往往会使用有实际意义的字段。例如下面一段代码,它使用学生的学号来查找学生的地址。

(1)学生实体类定义

public class StudentInf{    public string Number { get; set; }    public string Address { get; set; }    public StudentInfo(string number, string address)    {        Number = number;        Address = address;    }}

(2)通过索引遍历查找

static StudentInfo[] InitialStudents()

{   

  StudentInfo[] arrStudent = {       

    new StudentInfo("200807001","四川达州"),       

    new StudentInfo("200807002","四川成都"),       

    new StudentInfo("200807003","山东青岛"),       

      new StudentInfo("200807004","河南郑州"),     

      new StudentInfo("200807005","江苏徐州") 

};   

return arrStudent;}

static void NormalSearch(StudentInfo[] arrStudent, string searchNumber)

{   

  bool isFind = false;   

  foreach (var student in arrStudent)   

  {     

    if (student.Number == searchNumber)       

    {         

      isFind = true;           

      Console.WriteLine("Search successfully!{0} address:{1}", searchNumber, student.Address);       

    }   

  }   

  if (!isFind)    {     

    Console.WriteLine("Search {0} failed!", searchNumber);   

  }

}

static void Main(string[] args)

{   

  StudentInfo[] arrStudent = InitialStudents();   

  // 01.普通数组遍历查找   

  NormalSearch(arrStudent, "200807005");   

  Console.ReadKey();

}

运行结果如下图所示,可以看到圆满完成了查找任务。

但是,如果查找的记录位于数组的最后或者根本就不存在,仍然需要遍历整个数组。当数组非常巨大时,还以这样的方式查找将会消耗较多的时间。是否有一种方法可以通过学号关键字就能直接地定位到相应的记录?

(3)改写查找方式为哈希查找

通过观察学号记录与索引的对应关系,学号的后三位数组恰好是一组有序数列,如果把每个学生的学号后三位数组抽取出来并减去1,结果刚好可以与数组的索引号一一对应。于是,我们可以将上例改写为如下方式:

static int GetHashCode(string number)

{   

  string index = number.Substring(6);   

  return Convert.ToInt32(index) - 1;

}

static void HashSearch(StudentInfo[] arrStudent, string searchNumber)

{   

  Console.WriteLine("{0} address:{1}", searchNumber, arrStudent[GetHashCode(searchNumber)].Address);

}

static void Main(string[] args)

{   

  StudentInfo[] arrStudent = InitialStudents();   

  HashSearch(arrStudent, "200807005"); 

  HashSearch(arrStudent, "200807001"); 

  Console.ReadKey();

}

可以看出,通过封装GetHashCode()方法,实现了学号与数组索引的一一对应关系,在查找中直接定位到了索引号,避免了遍历操作,从而提高了查询效率,从原来的O(n)提高到了O(1),运行结果如下图所示:

上例中的学号是不重复的,它可以唯一标识学生集合中的每一条记录,这样的字段就被称为key(关键字)。而在记录存储地址和它的关键字之间建立一个确定的对应关系h,使得每个关键字和一个唯一的存储位置相对应。在查找时,只需要根据这个对应关系h,就可以找到所需关键字及其对应的记录,这种查找方式就被称为哈希查找,关键字和存储位置的对应关系可以用函数表示为:

h(key)=存储地址

2构造哈希函数的方法

构造哈希函数的目标在于使哈希地址尽可能均匀地分布在连续的内存单元地址上,以减少发生冲突的可能性,同时使计算尽可能简单以达到尽可能高的时间效率,这里主要看看两个构造哈希函数的方法。

(1)直接地址法

直接地址法取关键字的某个线性函数值为哈希地址,即h(key)=key 或 h(key)=a*key+b

其中,a、b均为常数,这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用

(2)除留余数法

除留余数法采用取模运算(%)把关键字除以某个不大于哈希表表长的整数得到的余数作为哈希地址,它也是最常用的构造哈希函数的方法,其形式为:h(key)=key%p

本方法的关键就在于选择合适的p,p如果选得不好,就可能会容易产生同义词。

PS:根据前辈们的经验,若哈希表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

3解决哈希冲突的方法

(1)闭散列法

闭散列法时把所有的元素都存储在哈希表数组中,当发生冲突时,在冲突位置的附近寻找可存放记录的空单元。寻找“下一个”空位的过程则称为探测。上述方法可用如下公式表示为:

其中,h(key)为哈希函数,m为哈希表长度,di为递增的序列。根据di的不同,又可以分为几种探测方法:线性探测法、二次探测法以及双重散列法。

(2)开散列法

开散列法的常见形式是将所有关键字为同义词的记录存储在一个单链表中。我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。对于关键字集合{12,67,56,16,25,37,22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法,可得到如下图所示的结构,此时,已经不存在什么冲突换址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。

该方法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链表的性能损耗。在.NET中,链表的各个元素分散于托管堆各处,也会给GC垃圾回收带来压力,影响程序的性能。

4小结

本篇介绍了哈希技术的基本概念及为何引入哈希技术,构造哈希函数的方法以及如何解决哈希冲突。下一篇,我们会了解.NET BCL中的HashTable、Dictionary与SortedDictionary(其内部是红黑树数据结构实现)。

5参考资料

程杰,《大话数据结构》

陈广,《数据结构(C#语言描述)》

段恩泽,《数据结构(C#语言版)》

许两会,《.NET集合类的研究-有序集合》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354