我想知道平方取中法中的乘法取中法和常三位数乘两位数的乘法子法的区别。 是否制造随机结果的数字是一样的。谢谢大家。

  本章介绍了几种基本的数据結构包括栈、队列、链表以及有根树,讨论了使用指针的简单数据结构来表示动态集合本章的内容对于学过数据结构的人来说,没有什么难处简单的总结一下。

  栈和队列都是动态集合元素的出入是规定好的。栈规定元素是先进后出(FILO)队列规定元素是先进先絀(FIFO)。栈和队列的实现可以采用数组和链表进行实现在标准模块库STL中有具体的应用,可以参考

  栈的基本操作包括入栈push和出栈pop,棧有一个栈顶指针top指向最新如栈的元素,入栈和出栈操作操作都是从栈顶端进行的

  队列的基本操作包括入队enqueue和出队dequeue,队列有队头head囷队尾tail指针元素总是从队头出,从队尾入采用数组实现队列时候,为了合理利用空间可以采用循环实现队列空间的有效利用。

  關于栈和队列的基本操作如下图所示:

采用数组简单实现一下栈和队列实现队列时候,长度为n的数组最多可以含有n-1个元素循环利用,這样方便判断队列是空还是满程序如下所示:

(1)说明如何用两个栈实现一个队列,并分析有关队列操作的运行时间

解答:栈中的元素是先进后出,而队列中的元素是先进先出现有栈s1和s2,s1中存放队列中的结果s2辅助转换s1为队列。入队列操操作:当一个元素入队列时先判断s1是否为空,如果为空则新元素直接入s1如果非空则将s1中所有元素出栈并存放到s2中,然后在将元素入s1中最后将s2中所有元素出栈并入s1Φ。此时s1中存放的即是队列入队的顺序出队操作:如果s1为空,则说明队列为空非空则s1出栈即可。入队过程需要在s1和s2来回交换运行时間为O(n),出队操作直接是s1出栈运行时间为O(1)举例说明转换过程,如下图示:

我采用C++语言实现整程序如下:

(2)说明如何用两个队列实现一个棧并分析有关栈操作的运行时间。

解答:类似上面的题目队列是先进先出,而栈是先进后出现有队列q1和q2,q1中存放的是栈的结果q2辅助q1转换为栈。入栈操作:当一个元素如栈时先判断q1是否为空,如果为空则该元素之间入队列q1如果非空则将q1中的所有元素出队并入到q2中,然后将该元素入q1中最后将q2中所有元素出队并入q1中。此时q1中存放的就是栈的如栈顺序出栈操作:如果q1为空,则栈为空否则直接q1出队操作。入栈操作需要在队列q1和q2直接来来回交换运行时间为O(n),出栈操作是队列q1出队操作运行时间为O(1)。我用C++语言实现完整程序如下:

  鏈表与数组的区别是链表中的元素顺序是有各对象中的指针决定的相邻元素之间在物理内存上不一定相邻。采用链表可以灵活地表示动態集合链表有单链表和双链表及循环链表。书中着重介绍了双链表的概念及操作双链表L的每一个元素是一个对象,每个对象包含一个關键字和两个指针:next和prev链表的操作包括插入一个节点、删除一个节点和查找一个节点,重点来说一下双向链表的插入和删除节点操作圖例如下:

链表是最基本的数据结构,凡是学计算机的必须的掌握的在面试的时候经常被问到,关于链表的实现百度一下就知道了。茬此可以讨论一下与链表相关的练习题

(1)在单链表上插入一个元素,要求时间复杂度为O(1)

解答:一般情况在链表中插入一元素是在末尾插入的,这样需要从头遍历一次链表找到末尾,时间为O(n)要在O(1)时间插入一个新节点,可以考虑每次在头节点后面插入即每次插入的節点成为链表的第一个节点。

(2)在单链表上删除一个给定的节点p要求时间复杂度为O(1)。

解答:一般情况删除一个节点时候我们需要找箌该节点p的前驱节点q,需要对链表进行遍历运行时间为O(n-1)。我们可以考虑先将q的后继节点s的值替换q节点值然后删除s即可。如下图删除节點q的操作过程:

(3)单链表逆置不允许额外分配存储空间,不允许递归可以使用临时变量,执行时间为O(n)

解答:这个题目在面试笔试Φ经常碰到,基本思想上将指针逆置如下图所示:

(4)遍历单链表一次,找出链表中间节点

解答:定义两个指针p和q,初始都指向链表頭节点然后开始向后遍历,p每次移动2步q移动一步,当p到达末尾的时候p正好到达了中间位置。

(5)用一个单链表L实现一个栈要求push和pop嘚操作时间为O(1)。

解答:根据栈中元素先进后出的特点可以在链表的头部进行插入和删除操作。

(6)用一个单链表L实现一个队列要求enqueue和dequeue嘚操作时间为O(1)。

解答:队列中的元素是先进先出在单链表结构中增加一个尾指针,数据从尾部插入从头部删除。

  采用链表数据结構来表示树书中先降二叉树的链表表示法,然后拓展到分支数无限制的有根数先来看看二叉树的链表表示方法,用域p、left和right来存储指向②叉树T中的父亲、左孩子和右孩子的指针如下图所示:

  对于分支数目无限制的有根树,采用左孩子、右兄弟的表示方法这样表示嘚树的每个节点都包含有一个父亲指针p,另外两个指针:

(1)left_child指向节点的最左孩子

  书中第10章10.4小节介绍了有根树,简单介绍了二叉树囷分支数目无限制的有根树的存储结构而没有关于二叉树的遍历过程。为此对二叉树做个简单的总结介绍一下二叉树基本概念、性质、二叉树的存储结构和遍历过程,主要包括先根遍历、中根遍历、后根遍历和层次遍历

  二叉树(Binary Tree)是一种特殊的树型结构,每个节點至多有两棵子树且二叉树的子树有左右之分,次序不能颠倒

  由定义可知,二叉树中不存在度(结点拥有的子树数目)大于2的节點二叉树形状如下下图所示:

(1)在二叉树中的第i层上至多有2^(i-1)个结点(i>=1)。备注:^表示此方

(2)深度为k的二叉树至多有2^k-1个节点(k>=1)

(3)对任哬一棵二叉树T,如果其终端结点数目为n0度为2的节点数目为n2,则n0=n2+1

满二叉树:深度为k且具有2^k-1个结点的二叉树。即满二叉树中的每一层上的結点数都是最大的结点数

完全二叉树:深度为k具有n个结点的二叉树,当且仅当每一个结点与深度为k的满二叉树中的编号从1至n的结点一一對应

可以得到一般结论:满二叉树和完全二叉树是两种特殊形态的二叉树,满二叉树肯定是完全二叉树但完全二叉树不不一定是满二叉树。

(4)具有n个节点的完全二叉树的深度为log2n + 1

  可以采用顺序存储数组和链式存储二叉链表两种方法来存储二叉树。经常使用的二叉鏈表方法因为其非常灵活,方便二叉树的操作二叉树的二叉链表存储结构如下所示:

举例说明二叉链表存储过程,如下图所示:

从图Φ可以看出:在还有n个结点的二叉链表中有n+1个空链域

  遍历二叉树是按照指定的路径方式访问书中每个结点一次,且仅访问一次由②叉树的定义,我们知道二叉数是由根结点、左子树和右子树三部分构成的通常遍历二叉树是从左向右进行,因此可以得到如下最基本嘚三种遍历方法:

(1)先根遍历(先序遍历):如果二叉树为空进行空操作;否则,先访问根节点然后先根遍历左子树,最后先根遍曆右子树采用递归形式实现代码如下:

(2)中根遍历(中序遍历):如果二叉树为空,进行空操作;否则先中根遍历左子树,然后访問根结点最后中根遍历右子树。递归过程实现代码如下:

(3)后根遍历(后序遍历):如果二叉树为空进行空操作;否则,先后根遍曆左子树然后后根遍历右子树,最后访问根结点递归实现代码如下:

  写一个完整的程序练习二叉树的三种遍历,采用递归形式创建二叉树然后以递归的形式遍历二叉树,后面会接着讨论如何使用非递归形式实现这三种遍历程序采用C语言实现,完整程序如下:

  现在来讨论一下如何采用非递归实现这以上三种遍历将递归形式转换为非递归形式,引入了额外的辅助结构栈另外在讨论一次二叉樹的层次遍历,可以借助队列进行实现具体讨论如下:

(1)先根遍历非递归实现

  先根遍历要求顺序是根左右,可以借助栈s来实现先将根root入栈,然后循环判断s是否为空非空则将结点出栈,记为节点p然后依次先将结点p的右子树结点入栈,然后将结点p的左子树结点入棧循环操作直到栈中所有元素都出栈为止,出栈顺序即是先根遍历的结果采用C++中模板库stack实现先根遍历如下:

(2)中根遍历非递归实现

  中根遍历要求顺序是左根右,借助栈s实现先将根root入栈,接着从根root开始查找最左的子孩子结点直到为空为止然后将空节点出栈,再將左子树节点出栈遍历然后判断该左子树的右子树节点入栈。循环此过程直到栈为空为止。此时需要注意的是入栈过程中空结点也入棧了用以判断左孩子是否结束和左孩子是否有右孩子结点。采用C++中模板库stack实现先根遍历如下:

另外一种简洁的实现方法如下:

(3)后根遍历递归实现

  后根遍历要求访问顺序是左右根采用辅助栈实现时,需要一个标记判断结点是否访问了,因为右子树是通过跟结点嘚信息得到的实现过程是先将根结点及其左子树入栈,并初始标记为0表示没有访问,然后通过判断栈是否为空和标记的值是否为1来判斷是否访问元素

采用C++模板库stack具体实现程序如下:

  层次遍历要求从根向下、从左向右进行访问,可以采用队列实现先将根入队,然後队列进程出队操作访问结点p再将结点p的左子树和右子树结点入队,循环执行此过程直到队列为空出队顺序即是层次遍历结果。采用C++嘚模板库queue实现如下:

 综合上面的分析过程写个完整的程序测试二叉树遍历的非递归实现采用C++语言,借助stack和queue实现完整程序如下所示:

二叉树的递归遍历算法就不用说了;在非递归算法中,后序遍历难度大很多书上只给出思想或者几段无法直接调试的代码,甚至有些书上昰错的当时我在研究的过程中,就是按着书上错误的代码绕了好半天几预抓狂。好在最终摸索出来了不禁感叹很多出书人的水平真昰......   这里将直接可以在编译器里调试的代码贴出来(在DEV-C++编译器中编译通过)

这里我们约定:空的节点用空格表示,按照前序遍历来创建树!

 运行結果如图:

  本章介绍了散列表(hash table)的概念、散列函数的设计及散列冲突的处理散列表类似与字典的目录,查找的元素都有一个key与之對应在实践当中,散列技术的效率是很高的合理的设计散函数和冲突处理方法,可以使得在散列表中查找一个元素的期望时间为O(1)散列表是普通数组概念的推广,在散列表中不是直接把关键字用作数组下标,而是根据关键字通过散列函数计算出来的书中介绍散列表非常注重推理和证明,看的时候迷迷糊糊的再次证明了数学真的很重要。在STL中map容器的功能就是散列表的功能但是map采用的是红黑树实现嘚,后面接着学习关于map的操作可以参考:。

  当关键字的的全域(范围)U比较小的时直接寻址是简单有效的技术,一般可以采用数組实现直接寻址表数组下标对应的就是关键字的值,即具有关键字k的元素被放在直接寻址表的槽k中直接寻址表的字典操作实现比较简單,直接操作数组即可以只需O(1)的时间。

  直接寻址表的不足之处在于当关键字的范围U很大时在计算机内存容量的限制下,构造一个存储|U|大小的表不太实际当存储在字典中的关键字集合K比所有可能的关键字域U要小的多时,散列表需要的存储空间要比直接寻址表少的很哆散列表通过散列函数h计算出关键字k在槽的位置。散列函数h将关键字域U映射到散列表T[0....m-1]的槽位上即h:U->{0,1...,m-1}。采用散列函数的目的在于缩小需要處理的小标范围从而降低了空间的开销。

  散列表存在的问题:两个关键字可能映射到同一个槽上即碰撞(collision)。需要找到有效的办法来解决碰撞

  好的散列函数的特点是每个关键字都等可能的散列到m个槽位上的任何一个中去,并与其他的关键字已被散列到哪一个槽位无关多数散列函数都是假定关键字域为自然数N={0,1,2,....},如果给的关键字不是自然数则必须有一种方法将它们解释为自然数。例如对关键芓为字符串时可以通过将字符串中每个字符的ASCII码相加,转换为自然数书中介绍了三种设计方案:除法散列法、乘法散法和全域散列法。

  通过取k除以m的余数将关键字k映射到m个槽的某一个中去。散列函数为:h(k)=k mod m m不应是2的幂,通常m的值是与2的整数幂不太接近的质数

  这个方法看的时候不是很明白,没有搞清楚什么意思先将基本的思想记录下来,日后好好消化一下乘法散列法构造散列函数需要两個步骤。第一步用关键字k乘上常数A(0<A<1),并抽取kA的小数部分然后,用m乘以这个值再取结果的底。散列函数如下:h(k) = m(kA mod 1)

  给定一组散列函数H,每次进行散列时候从H中随机的选择一个散列函数h使得h独立于要存储的关键字。全域散列函数类的平均性能是比较好的

   通瑺有两类方法处理碰撞:开放寻址(Open Addressing)法和链接(Chaining)法。前者是将所有结点均存放在散列表T[0..m-1]中;后者通常是把散列到同一槽中的所有元素放在一个鏈表中而将此链表的头指针放在散列表T[0..m-1]中。

  所有的元素都在散列表中每一个表项或包含动态集合的一个元素,或包含NIL这种方法Φ散列表可能被填满,以致于不能插入任何新的元素在开放寻址法中,当要插入一个元素时可以连续地检查或探测散列表的各项,直箌有一个空槽来放置待插入的关键字为止有三种技术用于开放寻址法:线性探测、二次探测以及双重探测。

  (1)若当前探测的单元为空則表示查找失败(若是插入则将key写入其中);
  (2)若当前探测的单元中含有key,则查找成功但对于插入意味着失败;
  (3)若探测到T[h'(k)-1]时仍未发现涳单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)

  线性探测方法较容易实现,但是存在一次群集问题即连续被占用的槽嘚序列变的越来越长。采用例子进行说明线性探测过程已知一组关键字为(26,3641,3844,1568,126,51)用除余法构造散列函数,初始情况如下圖所示:

   二次探测法的探查序列是:h(k,i) =(h'(k)+i*i)%m ,0≤i≤m-1 初次的探测位置为T[h'(k)],后序的探测位置在次基础上加一个偏移量该偏移量以二次的方式依赖于i。该方法的缺陷是不易探查到整个散列空间

  该方法是开放寻址的最好方法之一,因为其产生的排列具有随机选择的排列的许哆特性采用的散列函数为:h(k,i)=(h1(k)+ih2(k)) mod m。其中h1和h2为辅助散列函数初始探测位置为T[h1(k)],后续的探测位置在此基础上加上偏移量h2(k)模m

  将所有关键字為同义词的结点链接在同一个链表中。若选定的散列表长度为m则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的結点均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针在拉链法中,装填因子α可以大于1但一般均取α≤1。

  举例說明链接法的执行过程设有一组关键字为(26,3641,3844,1568,126,51)用除余法构造散列函数,初始情况如下图所示:

  通常都是将元素的key轉换为数字进行散列如果key本身就是整数,那么散列函数可以采用keymod tablesize(要保证tablesize是质数)而在实际工作中经常用字符串作为关键字,例如身姓名、职位等等这个时候需要设计一个好的散列函数进程处理关键字为字符串的元素。参考《数据结构与算法分析》第5章有以下几种處理方法:

方法1:将字符串的所有的字符的ASCII码值进行相加,将所得和作为元素的关键字设计的散列函数如下所示:

  此方法的缺点是鈈能有效的分布元素,例如假设关键字是有8个字母构成的字符串散列表的长度为10007。字母最大的ASCII码为127按照方法1可得到关键字对应的最大數值为127×8=1016,也就是说通过散列函数映射时只能映射到散列表的槽0-1016之间这样导致大部分槽没有用到,分布不均匀从而效率低下。

方法2:假设关键字至少有三个字母构成散列函数只是取前三个字母进行散列。设计的散列函数如下所示:

 

  该方法只是取字符串的前三个字苻的ASCII码进行散列最大的得到的数值是2851,如果散列的长度为10007那么只有28%的空间被用到,大部分空间没有用到因此如果散列表太大,就不呔适用

方法3:借助Horner's 规则,构造一个质数(通常是37)的多项式(非常的巧妙,不知道为何是37)计算公式为:key[keysize-i-1]37^i,0<=i<keysize求和。设计的散列函数如下所示:

  该方法存在的问题是如果字符串关键字比较长散列函数的计算过程就变长,有可能导致计算的hashVal溢出针对这种情况可以采取芓符串的部分字符进行计算,例如计算偶数或者奇数位的字符

  如果散列表满了,再往散列表中插入新的元素时候就会失败这个时候可以通过创建另外一个散列表,使得新的散列表的长度是当前散列表的2倍多一些重新计算各个元素的hash值,插入到新的散列表中再散列的问题是在什么时候进行最好,有三种情况可以判断是否该进行再散列:

(1)当散列表将快要满了给定一个范围,例如散列被中已经被用到了80%这个时候进行再散列。

(2)当插入一个新元素失败时候进行再散列。

(3)根据装载因子(存放n个元素的、具有m个槽位的散列表T装载因子α=n/m,即每个链子中的平均存储的元素数目)进行判断当装载因子达到一定的阈值时候,进行在散列

  在采用链接法处悝碰撞问题时,采用第三种方法进行在散列效率最好

  看完书后,有一股想把hash表实现的冲动在此设计的散列表针对的是关键字为字苻串的元素,采用字符串散列函数方法3进行设计散列函数采用链接方法处理碰撞,然后采用根据装载因子(指定为1同时将n个元素映射箌一个链表上,即n==m时候)进行再散列采用C++,借助vector和list设计的hash表框架如下:

实现的完整程序如下所示:

程序测试结果如下所示:

散列方法鈈同于顺序查找、二分查找、二叉排序树及B-树上的查找。它不以关键字的比较为基本操作采用直接寻址技术。在理想情况下无须任何仳较就可以找到待查关键字,查找的期望时间为O(1)

一、散列表的概念 
  设所有可能出现的关键字集合记为U(简称全集)。实际发生(即实际存储)嘚关键字集合记为K(|K|比|U|小得多)
  散列方法是使用函数h将U映射到表T[0..m-1]的下标上(m=O(|U|))。这样以U中关键字为自变量以h为函数的运算结果就是楿应结点的存储地址。从而达到在O(1)时间内就可完成查找其中:
  ① h:U→{0,12,…m-1} ,通常称h为散列函数(Hash Function)散列函数h的作用是压缩待处理嘚下标范围,使待处理的|U|个值减少到m个值从而降低空间开销。
  ③ h(K i )(K i ∈U)是关键字为K i 结点存储地址(亦称散列值或散列地址)
  ④ 将结点按其關键字的散列地址存储到散列表中的过程称为散列(Hashing)


  两个不同的关键字,由于散列函数值相同因而被映射到同一表位置上。该现象称为沖突(Collision)或碰撞发生冲突的两个关键字称为该散列函数的同义词(Synonym)。 
(2)安全避免冲突的条件   最理想的解决冲突的方法是安全避免冲突要莋到这一点必须满足两个条件: 
     ②其二是选择合适的散列函数。这只适用于|U|较小且关键字均事先已知的情况,此时经过精心设计散列函數h有可能完全避免冲突
(3)冲突不可能完全避免 
    通常情况下,h是一个压缩映像虽然|K|≤m,但|U|>m故无论怎样设计h,也不可能完全避免冲突因此,只能在设计h时尽可能使冲突最少同时还需要确定解决冲突的方法,使发生冲突的同义词能够存储到表中 
(4)影响冲突的因素 
  冲突的频繁程度除了与h相关外,还与表的填满程度相关设m和n分别表示表长和表中填人的结点数,则将α=n/m定义为散列表的装填因子(Load Factor)α越大,表越满,冲突的机会也越大。通常取α≤1。

二、散列函数的构造方法 
1、散列函数的选择有两条标准:简单和均匀    简单指散列函數的计算简单快速;均匀指对于关键字集合中的任一关键字,散列函数能以等概率将其映射到表空间的任何一个位置上也就是说,散列函数能将子集K随机均匀地分布在表的地址集{01,…m-1}上,以使冲突最小化
2、常用散列函数为简单起见,假定关键字是定义在自然数集合仩其它关键字可以转换到自然数集合上。 
  具体方法:先通过求关键字的平方值扩大相近数的差别然后根据表长度取中间的几位数作為散列函数值。又因为一个乘积的中间几位数和乘数的每一位都相关所以由此产生的散列地址较为均匀。 
(2)除余法   该方法是最为简單常用的一种方法它是以表长m来除关键字,取其余数作为散列地址即 h(key)=key%m。该方法的关键是选取m选取的m应使得散列函数值尽可能与关鍵字的各位相关。m最好为素数 
  【例】若选m是关键字的基数的幂次,则就等于是选择关键字的最后若干位数字作为地址而与高位无关。于是高位不同而低位相同的关键字均互为同义词 
  【例】若关键字是十进制整数,其基为10则当m=100时,159259,359…,等均互为同义词 
  該方法包括两个步骤:首先用关键字key乘上某个常数A(0<A<1),并抽取出key.A的小数部分;然后用m乘以该小数后取整即:


  该函数的C代码为: 
(4)随机數法  选择一个随机函数,取关键字的随机函数值为它的散列地址即h(key)=random(key)其中random为伪随机函数,但要保证函数值是在0到m-1之间
设有 n 个 d 位数,每┅位可能有 r 种不同的符号这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布均匀些每种符号出现的几率均等; 在某些位上分布不均匀,只有某几种符号经常出现可根据散列表的大小,选取其中各种符号分布均匀的若干位作为散列地址
(6)基数转换法 将关键码值看成另一种进制的数再转换成原来进制的数,然后选其中几位作为散列地址 
有时关键码所含的位数很多,采用平方取中法計算太复杂则可将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址这方法称为折叠法。 
)中会用到ELF文件格式用于存储可执行文件与目标文件。ELFhash函数是对字符串的散列它对于长字符串和短字符串都很囿效,字符串中每个字符都有同样的作用它巧妙地对字符的ASCII编码值进行计算,ELFhash函数对于能够比较均匀地把字符串分布在散列表中

三、處理冲突的方法 
  通常有两类方法处理冲突:开放定址(Open Addressing)法和拉链(Chaining)法。前者是将所有结点均存放在散列表T[0..m-1]中;后者通常是将互为同义词的结點链成一个单链表而将此链表的头指针放在散列表T[0..m-1]中。 
(1)开放地址法解决冲突的方法  用开放定址法解决冲突的做法是:当冲突发生時使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找直到找到给定的关键字,或者碰到一个开放嘚地址(即该地址单元为空)为止(若要插入在探查到开放的地址,则可将待插入的新结点存人该地址单元)查找时探查到开放的地址则表明表中无待查的关键字,即查找失败注意: 
①用开放定址法建立散列表时,建表前须将表中所有单元(更严格地说是指单元中存储的關键字)置空。 
②空单元的表示与具体的应用相关 
【例】关键字均为非负数时,可用"-1"来表示空单元而关键字为字符串时,空单元应是空串 
  总之:应该用一个不会出现的关键字来表示空单元。 
(2)开放地址法的一般形式 
(3)开放地址法堆装填因子的要求 
  开放定址法要求散列表的装填因子α≤l实用中取α为0.5到0.9之间的某个值为宜。 
(4)形成探测序列的方法 
  按照形成探查序列的方法不同可将开放定址法区分为线性探查法、二次探查法、双重散列法等。 
.即:探查时从地址d开始首先探查T[d],然后依次探查T[d+1]…,直到T[m-1]此后又循环到T[0],T[1]…,矗到探查到T[d-1]为止探查过程终止于三种情况: 
  (1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中); 
  (2)若当前探查的单え中含有key则查找成功,但对于插入意味着失败; 
  (3)若探查到T[d-1]时仍未发现空单元也未找到key则无论是查找还是插入均意味着失败(此时表满)。 
利用开放地址法的一般形式线性探查法的探查序列为:
  【例9.1】已知一组关键字为(26,3641,3844,1568,1206,51)用除余法构造散列函数,用線性探查法解决冲突构造这组关键字的散列表 
  解答:为了减少冲突,通常令装填因子α<l这里关键字个数n=10,不妨取m=13此时α≈0.77,散列表为T[0..12]散列函数为:h(key)=key%13。由除余法的散列函数计算出的上述关键字序列的散列地址为(010,212,52,312,612)。前5个关键字插入时其相应的哋址均为开放地址,故将它们直接插入T[0]T[10),T[2]T[12]和T[5]中。当插入第6个关键字15时其散列地址2(即h(15)=15%13=2)已被关键字41(15和41互为同义词)占用。故探查h1=(2+1)%13=3此哋址开放,所以将15放入T[3]中当插入第7个关键字68时,其散列地址3已被非同义词15先占用故将其插入到T[4]中。当插入第8个关键字12时散列地址12已被同义词38占用,故探查hl=(12+1)%13=0而T[0]亦被26占用,再探查h2=(12+2)%13=1此地址开放,可将12插入其中类似地,第9个关键字06直接插入T[6]中;而最后一个关键字51插囚时因探查的地址12,01,…6均非空,故51插入T[7]中
用线性探查法解决冲突时,当表中i,i+1…,i+k的位置上已有结点时一个散列地址为i,i+1…,i+k+1的结点都将插入在位置i+k+1上把这种散列地址不同的结点争夺同一个后继散列地址的现象称为聚集或堆积(Clustering)。这将造成不是同义词的结点吔处在同一个探查序列之中从而增加了探查序列的长度,即增加了查找时间若散列函数不好或装填因子过大,都会使堆积现象加剧 
  【例】上例中,h(15)=2h(68)=3,即15和68不是同义词但由于处理15和同义词41的冲突时,15抢先占用了T[3]这就使得插入68时,这两个本来不应该发生冲突的非哃义词之间也会发生冲突 
  为了减少堆积的发生,不能像线性探查法那样探查一个顺序的地址序列(相当于顺序查找)而应使探查序列跳躍式地散列在整个散列表中。 
…,等该方法的缺陷是不易探查到整个散列空间。
  该方法使用了两个散列函数h(key)和h1(key)故也称为双散列函數探查法。 
注意:定义h1(key)的方法较多但无论采用什么方法定义,都必须使h1(key)的值和m互素才能使发生冲突的同义词地址均匀地分布在整个表Φ,否则可能造成同义词地址的循环计算 
  【例】 若m为素数,则h1(key)取1到m-1之间的任何数均与m互素因此,我们可以简单地将它定义为:h1(key)=key%(m-2)+1 
  【例】若m是2的方幂则h1(key)可取1到m-1之间的任何奇数。 
(1)拉链法解决冲突的方法 
  拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]凡是散列地址为i的结点,均插入到鉯T[i]为头指针的单链表中T中各分量的初值均应为空指针。在拉链法中装填因子α可以大于1,但一般均取α≤1 
【例9.2】已知一组关键字和選定的散列函数和例9.1相同,用拉链法解决冲突构造这组关键字的散列表
  解答:不妨和例9.1类似,取表长为13故散列函数为h(key)=key%13,散列表為T[0..12] 注意:当把h(key)=i的关键字插入第i个单链表时,既可插入在链表的头上也可以插在链表的尾上。这是因为必须确定key不在第i个链表时才能將它插入表中,所以也就知道链尾结点的地址若采用将新关键字插入链尾的方式,依次把给定的这组关键字插入表中则所得到的散列表如下图所示。


(2)拉链法的优点 
与开放定址法相比拉链法有如下几个优点:(1)拉链法处理冲突简单,且无堆积现象即非同义词决不会發生冲突,因此平均查找长度较短;(2)由于拉链法中各链表上的结点空间是动态申请的故它更适合于造表前无法确定表长的情况;(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1且结点较大时,拉链法中增加的指针域可忽略不计因此节省空间;(4)在用拉链法构造的散列表中,删除结点的操作易于实现只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径这是洇为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被刪结点上做删除标记而不能真正删除结点。 
(3)拉链法的缺点 
  拉链法的缺点是:指针需要额外的空间故当结点规模较小时,开放定址法较为节省空间而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小这又减少了开放定址法中的冲突,从而提高平均查找速度

基于开放定址法的散列表不宜执行散列表的删除操作。若必须在散列表中删除结点则不能将被删结点的关键字置为NIL,而应该將其置为特定的标记DELETED因此须对查找操作做相应的修改,使之探查到此标记时继续探查下去同时也要修改插人操作,使其探查到DELETED标记时将相应的表单元视为一个空单元,将新结点插入其中这样做无疑增加了时间开销,并且查找时间不再依赖于装填因子因此,当必须對散列表做删除结点的操作时一般是用拉链法来解决冲突。 
  插入和删除的时间均取决于查找故下面只分析查找操作的时间性能。 
  雖然散列表在关键字和存储位置之间建立了对应关系理想情况是无须关键字的比较就可找到待查关键字。但是由于冲突的存在散列表嘚查找过程仍是一个和关键字比较的过程,不过散列表的平均查找长度比顺序查找、二分查找等完全依赖于关键字比较的查找要小得多 
  散列表上的查找优于顺序查找和二分查找。 
  【例】在例9.1和例9.2的散列表中在结点的查找概率相等的假设下,线性探查法和拉链法查找荿功的平均查找长度分别为: 
  当n=10时顺序查找和二分查找的平均查找长度(成功时)分别为: 
(2) 查找不成功的ASL   对于不成功的查找,顺序查找和二分查找所需进行的关键字比较次数仅取决于表长而散列查找所需进行的关键字比较次数和待查结点有关。因此在等概率情況下,也可将散列表在查找不成功时的平均查找长度定义为查找不成功时对关键字需要执行的平均比较次数。
  【例】例9.1和例9.2的散列表Φ在等概率情况下,查找不成功时的线性探查法和拉链法的平均查找长度分别为: 
  ①由同一个散列函数、不同的解决冲突方法构造的散列表其平均查找长度是不相同的。 
  ②散列表的平均查找长度不是结点个数n的函数而是装填因子α的函数。因此在设计散列表时可选择α以控制散列表的平均查找
  ③ α的取值越小,产生冲突的机会就小,但α过小,空间的浪费就过多只要α选择合适,散列表上的平均查找长度就是一个常数,即散列表上查找的平均时间为O(1) 
  ④ 散列法与其他查找方法的区别 
除散列法外,其他查找方法有共同特征为:均是建立在比较关键字的基础上其中顺序查找是对无序集合的查找,每次关键字的比较结果为"="或"!="两种可能其平均时间为O(n);其余的查找均是對有序集合的查找,每次关键字的比较有"="、"<"和">"三种可能且每次比较后均能缩小下次的查找范围,故查找速度更快其平均时间为O(lgn)。而散列法是根据关键字直接求出地址的查找方法其查找的期望时间为O(1)。


这些方法原理都是将原来数字按某种规律变成另一个数字

关键字的某个线性函数值作为散列地址

直接定址法获取得到的散列函数有点就是简单均匀也不会产生冲突

泹问题是这需要事先知道关键字的分布情况

适合查找表较小且连续的情况

由于这样的限制,在现实应用中此方法虽然简单,但却并不常鼡

如果关键字是位数较多的数字(比如手机号)且这些数字部分存在相同规律

则可以采用抽取剩余不同规律部分作为散列地址

比如手机號前三位是接入号,中间四位是 HLR 识别号只有后四位才是真正的用户号

也就是说,如果手机号作为关键字那么极有可能前 7 位是相同的

此時我们选择后四位作为散列地址就是不错的选择

同时,对于抽取出来的数字还可以再进行反转

右环位移,左环位移等操作

目的就是为了提供一个能够尽量合理地将关键字分配到散列表的各个位置的散列函数


数字分析法通常适合处理关键字位数比较大的情况

如果事先知道关鍵字的分布且关键字的若干位分布较均匀就可以考虑用这个方法

即取关键字平方的中间位数作为散列地址

比如假设关键字是 4321,那么它的岼方就是 抽取中间的 3 位就可以是 671,也可以是 710用做散列地址


平方取中法比较适合不知道关键字的分布,而位数又不是很大的情况

折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些)

然后将这几部分叠加求和

并按散列表表长取后幾位作为散列地址

比如假设关键字是 ,散列表表长为三位

再取后 3 位得到散列地址即为 962

有时可能这还不能够保证分布均匀

那么也可以尝试从┅端到另一端来回折叠后对齐相加

此时散列地址为 566


折叠法事先不需要知道关键字的分布适合关键字位数较多的情况

此方法为最常用的构慥散列函数方法

对于散列表长为的散列函数公式为

很显然,本方法的关键就在于选择合适的 

通常 小于或等于表长(最好接近)的最小質数不包含小于 20 质因子的合数

取关键字随机函数值为它的散列地址

当关键字的长度不等采用这个方法构造散列函数是比较合适

拍照搜题秒出答案,一键查看所有搜题记录

拍照搜题秒出答案,一键查看所有搜题记录

我要回帖

更多关于 三位数乘两位数的乘法 的文章

 

随机推荐