@html
查找是各类软件系统中常常用到的操做。查找的效率很是重要,大型的系统尤为如此。java
首先来看一些查找的基本概念和术语。算法
查找表
查找表是由同一类型的数据元素(或记录)构成的集合。因为 “集合” 中的数据元素之间存在着彻底松散的关系,所以查找表是一种很是灵活的数据结构,能够利用其余的数据结构来实现,例如线性表、树表及散列表等。数组
关键字
关键字是数据元素(或记录) 中某个数据项的值,用它能够标识一个数据元素(或记录)。若此关键字 能够惟一地标识一个记录,则称此关键字为主关键字(对不一样的记录,其主关键字均不一样)。反之,称用以识别若千记录的关键字为次关键字。当数据元素只有一个数据项时,其关键字即为该数据元素的值。数据结构
查找
查找是指根据给定的某个值,在查找表中肯定一个其关键字等千给定值的记录或数据元素。若表中存在这样的一个记录, 则称查找成功,此时查找的结果可给出整个记录的信息,或指示该记录在查找表中的位置;若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个 “空” 记录或 “空” 指针。dom
动态查找表和静态查找表
若在查找的同时对表作修改操做(如插入和删除),则相应的表称之为动态查找表,不然称之为静态查找表。换句话说,动态查找表的表结构自己是在查找过程当中动态生成的,即在建立表时,对千给定值, 若表中存在其关键字等于给定值的记录, 则查找成功返回;不然插入关键字等千给定值的记录。函数
平均查找长度
为肯定记录在查找表中的位置,需和给定值进行比较的关键字个数的指望值,称为查找算法,在查找成功时的平均查找长度(Average Search Length, ASL)。post
在查找表的组织方式中,线性表是最简单的一种。性能
在表的组织方式中,线性表是最简单的一种。而顺序查找是线性表查找中最简单的一种。学习
顺序查找的基本思想:从表的一端开始,顺序扫描线性表,依次扫描到的结点关键字和给定的K值相比较,若当前扫描到的结点关键字与 K相等,则查找成功;若扫描结束后,仍未找到关键字等于 K的结点,则查找失败。
順序査找方法既适用于线性表的顺序存储结构,也适用于线性表的链式存储结构。
/** * 顺序查找 * @param data * @param key * @return */ public int linearSearch(int[] data,int key){ //遍历查找 for (int i=0;i<data.length;i++){ //查找到 if (key==data[i]){ return i; } } //没有查找到 return -1; }
成功时的顺序査找的平均査找长度为:
在数据量为n的状况下,线性表的平均查找长度:
(n+……+2+1)/n=(n+1)/2
顺序查找须要从头开始不断地按顺序检查数据,所以在数据量大且目标数据靠后,
或者目标数据不存在时,比较的次数就会更多,也更为耗时。若数据量为 n,线性查找的时间复杂度便为 O(n)。
因此虽然顺序查找比较简单,且对表的结构无任何要求,可是查询效率较低,因此当n较大时不宜采用顺序査找。
二分查找也叫折半查找。
二分查找是一种效率相对较高的线性表查找方式,它要求被查找的线性表是有序的。
二分查找的基本思想是:先肯定待查找记录所在的范围(区间),而后逐步缩小范围直到找到或找不到该记录为止。
具体步骤以下:
/** * 二分查找-非递归 * @param data * @param key * @return */ public int binarySearch(int[] data,int key){ int mid; //置查找区间初值 int left=0; int right=data.length-1; while (left<right){ mid=(left+right)/2; if (key==data[mid]){ //找到待查元素 return mid; }else if (key>data[mid]){ //在右子表查找 left=mid+1; mid=(left+right)/2; }else{ //在左子表查找 right=mid-1; mid=(left+right)/1; } } return -1; //没有查找到待查元素 }
/** * 二分查找-递归 * * @param data * @param left * @param right * @param key * @return */ public int binarySearchRescu(int[] data, int left, int right, int key) { if (left > right) { return -1; } int mid = (left + right) / 2; if (key == data[mid]) { return mid; } else if (key > data[mid]) { //在右子表查找 return binarySearchRescu(data, mid + 1, right, key); } else { return binarySearchRescu(data, left, mid - 1, key); } }
折半查找的时间复杂度为 O(log2n), 折半查找的效率比顺序查找高,但折半查找只适用千有序表,且限于顺序存储结构。
折半查找的优势是:比较次数少,查找效率高。其缺点是:对表结构要求高,只能用于顺序存储的有序表。
若是对无序表进行二分查找,查找前须要排序,而排序自己是一种费时的运算。同时为了保持顺序表的有序性,对有序表进行插入和删除时,平均比较和移动表中一半元素,这也是一种费时的运算。所以,折半查找不适用于数据元素常常变更的线性表。
分块查找又称索引顺序查找。它是把顺序查找和二分査找相结合的一种查找方法,即把线性表分红若干块,块和块之间有序,但每一块内的结点能够无序。
分块查找的基本思想是:肯定被査找的结点所在的块(采用二分查找法)后,对该块中的结点采用顺序查找。
分块査找介于顺序和二分查找之间,其优势是:在表中插入或删除一个记录时,只要找到该记录所属的块,就在该块内进行插入和删除运算。分块査找的主要代价是增长一个辅助数组的存储空间和将初始表分块排序的运算
在重学数据结构(6、树和二叉树) 里,对大量的输进行了详细的描述和实现,因此针对树表的查找,下面只是是作一些简单的描述。
当用线性表做为表的组织形式时,能够用 3 种查找法。其中以二分査找效率最高。但因为二分査找要求表中结点按关键字有序,且不能用链表做存储结构,所以,当表的插入或删除操做频繁时,为维护表的有序性,势必要移动表中不少结点。这种由移动结点引发的额外时间开销,就会抵消二分査找的优势。也就是说,二分查找只适用于静态查找表。若要对动态查找表进行高效率的查找,最好使用二叉排序树。
二叉排序树又称为是二叉查找树或二叉搜索树。二叉排序树是具备下列性质的二叉树:
由 BST性质可得:
(1) 二叉排序树中任一结点x,其左(右)子树中任一结点y若存在)的关键字必小(大)于 x 的关键字。
(2) 二叉排序树中,各结点关键字是唯一的。
须要注意的是在实际应用中,不能保证被查找的数据集中各元素的关键字互不相同,因此可将二叉排序树定义中 BST 性质⑴ 里的“小于”改成“大于等于”,或将 BST性质(2)里的“大于”改成“小于等于”,甚至可同时修改这两个性质。
(3) 按中序遍历该树所获得的中序序列是一个递增有序序列。
二叉树虽然利于查找,可是存在一个最坏的状况——二叉树退化为线性表。
因此就须要就须要引入一些特性来使二叉树保持平衡。
例如最先提出的平衡二叉树AVL树,是具备以下特性的二叉树:
B树也是一种平衡查找树,不过不是二叉树。
B树也称B-树,它是一种多路平衡查找树。
一棵m阶的B树定义以下:
B+树是B树的变体,也是一种多路搜索树。
B+树·和B树有一些共同的特性:
B+树和B树也有一些不同的地方:
在前面学习了关于线性表、树表的查找,这类查找方法都是以关键字的比较为基础的。
在查找过程当中只考虑各元素关键字之间的相对大小,记录在存储结构中的位置和其关键字无直接关系, 其查找时间与表的长度有关,特别是当结点个数不少时,查找时要大量地与无效结点的关键字进行比较,导致查找速度很慢。
若是能在元素的存储位置和其关键字之间创建某种直接关系,那么在进行查找时,就无需作比较或作不多次的比较,按照这种关系直接由关键字找到相应的记录。这就是散列查找法 (HashSearch)的思想,它经过对元素的关键字值进行某种运算,直接求出元素的地址, 即便用关键字到地址的直接转换方法,而不须要反复比较。所以,散列查找法又叫杂凑法或散列法。
下面来看一些散列查找法的术语:
构造散列函数的方法不少,通常来讲,应根据具体问题选用不一样的散列函数,一般要考虑如下因素:
构造一个 “好” 的散列函数应遵循如下两条原则:
下面介绍构造散列函数的几种经常使用方法。
若是事先知道关键字集合, 且每一个关键字的位数比散列表的地址码位数多,每一个关键字由n位数组成,如K1…Kn , 则能够从关键字中提取数字分布比较均匀的若干位做为散列地址。
例如,有80个记录,其关键字为8位十进制数。假设散列表的表长为100, 则可取两位十进制数组成散列地址,选取的原则是分析这80个关键字,使获得的散列地址尽最避免产生冲突。
假设这80个关键字中的一部分以下所列:
对关键字全体的分析中能够发现:第一、2位都是"8 、1", 第3位只可能取3或4, 第4位可能取二、 5或 7,所以这 4 位都不可取。由千中间的 4 位可当作是近乎随机的,所以可取其中任意两位,或取其中两位与另外两位的叠加求和后舍去进位做为散列地址。
数字分析法的适用状况:事先必须明确知道全部的关键字每一位上各类数字的分布状况。
在实际应用中,例如,同一出版社出版的全部图书,其ISBN号的前几位都是相同的,所以,若数据表只包含同一出版社的图书,构造散列函数时能够利用这种数字分析排除ISBN号的前几位数字。
一般在选定散列函数时不必定能知道关键字的所有状况,取其中哪几位也不必定合适,而一个数平方后的中间几位数和数的每一位都相关,若是取关键字平方后的中间几位或其组合做为散列地址,则使随机分布的关键字获得的散列地址也是随机的,具体所取的位数由表长决定。平方取中法是一种较经常使用的构造散列函数的方法。
平方取中法的适用状况:不能事先了解关键字的全部状况,或难千直接从关键字中找到取值较分散的几位。
将关键字分割成位数相同的几部分(最后一部分的位数能够不一样),而后取这几部分的叠加和(舍去进位) 做为散列地址,这种方法称为折叠法。根据数位叠加的方式,能够把折叠法分为移位叠加和边界叠加两种。移位叠加是将分割后每一部分的最低位对齐,而后相加;边界叠加是将两相邻的部分沿边界来回折叠,而后对齐相加。
例如,当散列表长为 1000 时,关键字key = 45387765213, 从左到右按 3 位数一段分割,能够获得 4 个部分:453 、 877 、 652 、 13。分别采用移位叠加和边界叠加,求得散列地址为 995 和914, 以下图 所示。
折叠法的适用状况:适合于散列地址的位数较少,而关键字的位数较多,且难千直接从关键字中找到取值较分散的几位。
假设散列表表长为m, 选择一个不大千m的数p, 用p去除关键字,除后所得余数为散列地址,即
这个方法的关键是选取适当的p, 通常状况下,能够选p为小于表长的最大质数。例如,表长m= 100 , 可取p= 97 。
除留余数法计算简单,适用范围很是广,是最经常使用的构造散列函数的方法。它不只能够对关键字直接取模,也可在折叠、平方取中等运算以后取模,这样可以保证散列地址必定落在散列表的地址空间中。
选取一个随机函数,取关键字的随机函数的值为散列地址。
选择一个 “好” 的散列函数能够在必定程度上减小冲突,但在实际应用中,很难彻底避免发生冲突,因此选择一个有效的处理冲突的方法是散列法的另外一个关键问题。建立散列表和查找散列表都会遇到冲突,两种状况下处理冲突的方法应该一致。
处理冲突的方法与散列表自己的组织形式有关。按组织形式的不一样,一般分两大类:开放地址法和链地址法。
开放地址法的基本思想是:把记录都存储在散列表数组中,当某一记录关键字 key的初始散列地址H0=H(key)发生冲突时,以H0为基础 ,采起合适方法计算获得另外一个地址H1, 若是H1仍然发生冲突,以H1为基础再求下一个地址H2,若H2仍然冲突,再求得H3。依次类推,直至Hk不发生冲突为止,则Hk为该记录在表中的散列地址。
这种方法在寻找 “下一个 “ 空的散列地址时,原来的数组空间对全部的元素都是开放的,因此称为开放地址法。一般把寻找 “下一个 “ 空位的过程称为探测,上述方法可用以下公式表示:
这种探测方法能够将散列表假想成一个循环表,发生冲突时,从冲突地址的下一单元顺序寻找空单元,若是到最后 一个位置也没找到空单元,则回到表头开始继续查找,直到找到一个空位,就把此元素放入此空位中。若是找不到空位,则说明散列表已满,须要进行溢出处理。
例如,散列表的长度为 11, 散列函数 H(key) = key%11, 假设表中已填有关键字分别为 17 、60 、 29 的记录,如图 11 (a) 所示。现有第四个记录,其关键字为 38, 由散列函数获得散列地址为 5, 产生冲突。
若用线性探测法处理时,获得下一个地址 6, 仍冲突;再求下一个地址 7, 仍冲突;直到散列地址为 8 的位置为 “空” 时为止,处理冲突的过程结束,38 填入散列表中序号为 8 的位置,如图11 (b) 所示。
若用二次探测法,散列地址 5 冲突后,获得下一个地址 6, 仍冲突;再求得下一个地址 4 , 无冲突, 38 填入序号为 4 的位置,如图 11 (C), 所示。
若用伪随机探测法,假设产生的伪随机数为 9, 则计算下一个散列地址为(5+9)%11 = 3, 因此38 填入序号为 3 的位置,如图 11 (d) 所示。
从上述线性探测法处理的过程当中能够看到一个现象:当表中 i, i+1, i+2 位置上已填有记录时,下一个散列地址为i、i+ I 、i+2和i+3 的记录都将填入i+3 的位置,这种在处理冲突过程当中发生的两个第一个散列地址不一样的记录争夺同一个后继散列地址的现象称做 “二次汇集"(或称做 “堆积"),即在处理同义词的冲突过程当中又添加了非同义词的冲突。
能够看出,上述三种处理方法各有优缺点。
线性探测法的优势是:只要散列表未填满,总能找到一个不发生冲突的地址。缺点是:会产生 ”二次汇集“ 现象。
而二次探测法和伪随机探测法的优势是:能够避免 “二次汇集“ 现象。缺点也很显然:不能保证必定找到不发生冲突的地址。
链地址法的基本思想是:把具备相同散列地址的记录放在同一个单链表中,称为同义词链表。有 m 个散列地址就有 m 个单链表,同时用数组 HT[0…m-1]存放各个链表的头指针,凡是散列地址为 i 的记录都以结点方式插入到以 HT[i]为头结点的单链表中。
该方法的基本思想就是选择足够大的M,使得全部的链表都尽量的短小,以保证查找的效率。对采用链地址法的哈希实现的查找分为两步,首先是根据散列值找到等一应的链表,而后沿着链表顺序找到相应的键。
散列表上的运算有查找、插入和删除。其中主要是查找,这是由于散列表主要用于快速查找,且插入和删除均要用到査找操做。
接下来创建一个简单的散列表,其散列函数采用上述的除留余数法,处理冲突的方法采用开放定址法下的线性探测法。
public class HashTable { int[] elem; int count; private static final int Nullkey = -32768; public HashTable(int count) { this.count = count; elem = new int[count]; for (int i = 0; i < count; i++) { elem[i] = Nullkey; // 表明位置为空 } } /* * 散列函数 */ public int hash(int key) { return key % count; // 除留余数法 } /* * 插入操做 */ public void insert(int key) { int addr = hash(key); // 求散列地址 while (elem[addr] != Nullkey) { // 位置非空,有冲突 addr = (addr + 1) % count; // 开放地址法的线性探测 } elem[addr] = key; } /* * 查找操做 */ public boolean search(int key) { int addr = hash(key); // 求散列地址 while (elem[addr] != key) { addr = (addr + 1) % count; // 开放地址法的线性探测 if (addr == hash(key) || elem[addr] == Nullkey) { // 循环回到原点或者到了空地址 System.out.println("要查找的记录不存在!"); return false; } } System.out.println("存在记录:" + key + ",位置为:" + addr); return true; } public static void main(String[] args) { int[] arr = {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34}; HashTable aTable = new HashTable(arr.length); for (int a : arr) { aTable.insert(a); } for (int a : arr) { aTable.search(a); } } }
α标志散列表的装满程度。直观地看,α越小,发生冲突的可能性就越小;反之,α越大,表中已填入的记录越多,再填记录时,发生冲突的可能性就越大,则查找时,给定值需与之进行比较的关键字的个数也就越多。
查找是数据处理中常用的一种操做。 这里主要介绍了对查找表的查找 , 查找表实际上仅仅是一个集合,为了提升查找效率,将查找表组织成不一样的数据结构,主要包括3种不一样结构的查找表:线性表、 树表和散列表。
* 二叉排序树的查找过程与折半查找过程相似
* 二叉排序树在形态均匀时性能最好,而形态为单支树时其查找性能则退化为与顺序查找相同,所以,二叉排序树最好是一棵平衡二叉树。 平衡二叉树的平衡调整方法就是确保二叉排序树在任何状况下的深度均为 O(log2n),平衡调整方法分为4种: LL型、RR型、LR型和RL型。
* B-树是一种平衡的多叉查找树,是一种在外存文件系统中经常使用的动态索引技术。 在 B-树上进行查找的过程和二叉排序树相似,是一个顺指针查找结点和在结点内的关键字中查找交叉进行的过程。 为了确保B-树的定义,在B-树中插入一个关键字,可能产生结点的 “分裂", 而删除一个关键字,可能产生结点的 “合并"。
* B+树是一种B-树的变型树,更适合作文件系统的索引。 在B+树上进行随机查找、 插入和删除的过程基本上与B-树相似,但具体实现细节又有所区别。
散列查找法主要研究两方面的问题:如何构造散列函数,以及如何处理冲突。
* 构造散列函数的方法不少,除留余数法是最经常使用的构造散列函数的方法。它不只能够对关键字直接取模, 也可在折叠、平方取中等运算以后取模。
* 处理冲突的方法一般分为两大类:开放地址法和链地址法,两者之间的差异相似千顺序表和单链表的差异。
上一篇:重学数据结构(7、图)
本博客为学习笔记,参考资料以下!
水平有限,不免错漏,欢迎指正!
参考:
【1】:邓俊辉 编著. 《数据结构与算法》
【2】:王世民 等编著 . 《数据结构与算法分析》
【3】: Michael T. Goodrich 等编著.《Data-Structures-and-Algorithms-in-Java-6th-Edition》
【4】:严蔚敏、吴伟民 编著 . 《数据结构》
【5】:程杰 编著 . 《大话数据结构》
【6】:图解:如何理解与实现散列表
【7】:算法图解之散列表
【8】:数据结构与算法-Day17-哈希(散列)表
【9】:Java数据结构与算法解析(十二)——散列表
【10】:【Java】 大话数据结构(13) 查找算法(4) (散列表(哈希表))