数据结构(三十八)静态查找表(顺序查找、二分查找、插值查找、斐波那契查找、线性索引查找)
静态查找表有5种查找方法:顺序查找、二分查找、插值查找、斐波那契查找、线性索引查找
一、顺序查找
1.顺序查找的定义
顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
2.顺序表查找算法的实现
(1)顺序查找算法的实现
// 顺序查找,a为数组,key为要查找的关键字,查找成功则返回i,否则返回-1 private static int seqSearch(int[] a, int key) { for (int i = 0; i < a.length; i++) { if (a[i] == key) { return i; } } return -1; }
(2)带监视哨的顺序查找算法
// 有哨兵顺序查找数组a中从a[1]到数组末尾的key,无哨兵每次循环都需要对i是否越界,即是否小于a.length做判断。 // 设置一个哨兵可以解决不需要每次让i与a.length作比较。 // 返回-1说明查找失败,注意: 只能从数组下标为1的位置开始查找 private static int seqSearchWithGuard(int[] a, int key) { int i = a.length - 1; // 设置循环从数组尾部开始 a[0] = key; // 设置a[0]为关键字值,称之为"哨兵" while (a[i] != key) { i--; } if (i>0) { return i; } else { return -1; } }
3.顺序查找算法的时间复杂度
对于顺序查找算法来说,查找成功最好的情况就是在第一个位置就找到了,算法时间复杂度为O(1),最坏的情况是在最后一位才找到,需要n次比较,时间复杂度为O(n),需要n+1次比较,时间复杂度为O(n),由于关键字在任意一位置的概率是相等的,所以平均查找次数为(n+1)/2,所以最终时间复杂度还是O(n)。当n很大时,查找效率极为低下,对一些小型数据的查找时,这种查找方式是可以适用的。
二、二分查找
1.二分查找的定义
二分查找(Binary Search),又称为折半查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。二分查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
2.二分查找的实现
// 二分查找:数组a中所有数据中包含key的数组下标,如果没有,返回-1 private static int binarySearch(int[] a, int key) { int low = 0; // 定义最低下标为记录首位 int high = a.length-1; // 定义最高下标为记录末位 while (low<=high) { int mid = (low + high)/2; // 折半得到中间记录的下标 if (key < a[mid]) { // 若查找值比中值小 high = mid - 1; // 最高下标调整到中间下标小一位 } else if (key > a[mid]) { // 若查找值比中指大 low = mid + 1; // 最低下标调整到中间下标大一位 } else { return mid; // 若相等则说明中间记录的下标即为查找到的值 } } return -1; }
3.二分查找算法的时间复杂度
二分查找等于是把静态有序查找分成了两棵子树,即查找结果只需要找其中的一半数据记录即可,等于工作量少了一半,然后继续折半查找,效率当然非常高了。
根据二叉树的性质4,即“具有n个结点的完全二叉树的深度为【Log2n】+1”,可以得到二分查找最坏情况下查找到关键字或查找失败的次数是【Log2n】+1,最好的情况当然是1次了,因此二分查找的时间复杂度为O(Logn),显然远远好于顺序查找的O(n)时间复杂度了。
三、插值查找
1.插值查找的定义
在基于二分查找算法的基础上,考虑到这么一种情况:要在取值范围0~10000之间100个元素从小到大均匀分布的数组中查找5,自然会考虑从数组下标比较小的位置开始查询。
插值查找(Interpolation Search)是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法,其核心就在于插值的计算公式:(high-low)/(a[high]-a[low])
2.插值查找算法的实现
// 插值查找:数组a中所有数据中包含key的数组下标,如果没有,返回-1 private static int interpolationSearch(int[] a, int key) { int low = 0; // 定义最低下标为记录首位 int high = a.length-1; // 定义最高下标为记录末位 while (low<=high) { int mid = low + (high-low)*(key-a[low])/(a[high]-a[low]);// 插值计算公式 if (key < a[mid]) { // 若查找值比中值小 high = mid - 1; // 最高下标调整到中间下标小一位 } else if (key > a[mid]) { // 若查找值比中指大 low = mid + 1; // 最低下标调整到中间下标大一位 } else { return mid; // 若相等则说明中间记录的下标即为查找到的值 } } return -1; }
3.插值查找的时间复杂度
从时间复杂度来看,插值查找和二分查找的时间复杂度一样都是O(LOGn),但对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法平均性能比二分查找要好得多。反之,如果数组中分布极端不均匀的数据,用插值查找也未必是很合适的选择。
四、斐波那契查找
1.斐波那契查找算法的定义
除了插值查找外,还有另外一种有序查找,斐波那契(Fibonacci Search)利用了黄金分割原理也可以解决二分查找算法的问题。
斐波那契算法的核心在于:在给定了斐波那契数列的情况下:
- 若key=a[mid]时,则查找成功
- 若key<a[mid]时,则新范围是第low个到第mid-1个,此时范围个数为F[k-1]-1个
- 若key>a[mid]时,则新范围是第mid+1个到第high个,此时范围个数为F[k-2]-1个
2.斐波那契查找算法的实现
private static int FibonacciSearch(int[] a, int key) { // 创建一个斐波那契数列的数组 int[] F = new int[100]; // 斐波那契数列 F[0]=0; F[1]=1; for (int i = 2; i < F.length; i++) { F[i] = F[i-1] + F[i-2]; } // 斐波那契算法准备工作 int low = 0; // 定义最低下标为记录首位 int high = a.length-1; // 定义最高下标为记录末位 int k = 0; int[] a_temp = new int[F.length]; System.arraycopy(a, 0, a_temp, 0, a.length); // 将数组a复制到a_temp数组中,因为要保证a_temp的长度和F数组长度一致 while (a.length - 1 > F[k] - 1) { // 计算要查找的最大下标位于斐波那契数列中的位置 k++; } for (int i = a.length -1; i < F[k] - 1; i++) { // 将不满的数值补全 a_temp[i] = a_temp[a.length - 1]; } // 斐波那契算法开始,可以结合二分查找算法对比着理解 while (low<=high) { int mid = low + F[k-1] - 1; // 计算当前分隔的下标 if (key < a_temp[mid]) { // 若查找值比中值小 high = mid - 1; // 最高下标调整到中间下标小一位 k = k - 1; // 斐波那契数下标减一位 } else if (key > a_temp[mid]) { // 若查找值比中指大 low = mid + 1; // 最低下标调整到中间下标大一位 k = k - 2; // 斐波那契数下标减二位 } else { if (mid <= a.length - 1) { // 若相等或小于数组下标最大值则说明mid即为查找到的位置 return mid; } else { // 若min>数组下标最大值则说明是补全值,返回数组中最后一位 return a.length - 1; } } } return -1; }
3.斐波那契算法的时间复杂度
如果要查找的记录在右侧,则左侧的数据都不用再判断了,不断反复进行下去,对处于当中的大部分数据,其工作效率要高一些。所以尽管斐波那契查找算法的时间复杂度也为O(LOGn),但就平均性能来说,斐波那契查找要由于二分查找。但是,如果是最坏情况,比如key=1,那么始终都处于左侧长半区查找,则查找效率要低于二分查找。
五、线性索引查找
1.索引的定义
顺序查找和二分查找都是基于有序的基础上,但事实上,很多数据集可能增长非常快,例如一些服务器的日志信息记录可能是海量数据,要保证记录全部是按照当中的某个关键字有序,其时间代价是非常高昂的,所以这种数据通常都是按先后顺序存储。对于这种表,使用索引可以快速查找到需要的数据。
索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。索引技术使大型数据库以及磁盘文件的一种重要技术。
索引按照结构可分为线性索引、树形索引和多级索引。
2.线性索引的定义
线性索引就是将索引项集合组织为线性结构,也称为索引表。线性索引包括稠密索引、分块索引和倒排索引。
3.稠密索引
稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项。对于稠密索引的索引表来说,索引项一定是按照关键码有序的排列。索引项有序也就意味着,查找关键字时,可以用到二分、插值、斐波那契等有序查找算法,大大提高了效率。
例如,如果要查找关键字是18的记录,如果直接从右侧的数据表中查找,只能顺序查找,需要查找6次才可以查到结果。但是如果从左侧的索引表中查找,只需两次二分查找就可以得到18对应的指针,最终查找到结果。
如果数据集非常大,那也就意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降了。
4.分块索引
稠密索引因为索引项与数据集的记录个数相同,所以空间代价很大。为了减少索引项的个数,可以对数据集进行分块,使其分块有序,然后再对每一块建立一个索引项,从而减少索引项的个数。对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。
(1)分块有序,是把数据集的记录分成若干块,并且这些块需要满足两个条件
- 块内无序:即每一块内的记录不要求有序。
- 块间有序:例如,第二块所有记录的关键字均要大于第一块中所有记录的关键字,第三块所有记录的关键字均要大于第二块中所有记录的关键字...
(2)分块索引的索引项结构分为三个数据项:
- 最大关键码,它存储每一块中的最大关键字,这样的好处就是可以使得在它之后的下一块中的最小关键字也能比这一块最大的关键字要大
- 存储了块中的记录个数,以便于循环时使用
- 用于指向块首元素的指针,便于开始对这一块中记录进行遍历
(3)在分块索引表中查找,需要分两步进行:
- 首先查找关键字所在的块。由于分块索引表是块间有序的,因此很容易利用二分、插值等算法得到结果。例如,如果要查找36这个记录,首先采用顺序查找或者二分查找将36依次和每一块的最大关键码进行比较,很容易得得到key=36是在第二块
- 根据块首指针找到相应的块,并在块中顺序查找关键码。由于块中是无序的,因此只能采用顺序查找的方法在对应的块中利用快长循环从而顺序查找到要查的关键码。
(4)分块索引的时间复杂度
设n个记录的数据集被平均分成m块,每个块中有t条记录,显然n=mxt,接着Lb为查找索引表的平均查找长度,由于最好与最差等概,所以Lb的平均长度为(m+1)/2。又Lw为块中查找记录的平均查找长度,同理Lw的平均查找长度为(t+1)/2。这样来说,分块索引查找的平均查找长度为:Lb+Lw=(m+1)/2 + (t+1)/2 = (n/t + t)/2 + t
从推导可以看出,最佳情况就是分的块数m和每个块中的记录数t相等,此时n=mxt=t^2,即平均查找长度=t+1=根号下n再加上1,因此分块索引的时间复杂度为O(根号n)。分块索引的效率比顺序查找的O(n)是提高了不少,但是与折半查找的O(logn)还有不小的差距。因此在确定块的过程中,由于块间有序,所以可以应用 二分、插值手段来提高效率。
5.倒排索引
例如,现在有两篇文章1和2,上面的表格显示了每个不同的单词分别出现在哪篇文章中。假如要搜索“book”这个关键字,首先在这张单词表中有序查找“book”,找到后将它对应的文章编号1和2的文章地址返回,由于单词表是有序的,查找效率很高,返回的知识文章的编号,所以整体速度非常快。
(1)倒排索引的索引项的通用结构:次关键码(英文单词) + 记录号表(文章编号)
(2)倒排索引的定义:使用次关键码和记录号表来查找的方法叫倒排索引。其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。倒排索引源于实际应用中需要根据属性(或字段、次关键码)的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因此称为倒排索引。
(3)倒排索引的优缺点:优点就是查找记录非常快,基本等于生成索引表后,查找时都不用去读取记录,就可以得到结果。缺点是记录号表不定长,维护比较困难,插入和删除操作都需要作相应的处理。
六、静态查找算法的C语言代码实现
01静态查找_Search.c#include "stdio.h" #include "stdlib.h" #include "io.h" #include "math.h" #include "time.h" #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 100 /* 存储空间初始分配量 */ typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */ int F[100]; /* 斐波那契数列 */ /* 无哨兵顺序查找,a为数组,n为要查找的数组个数,key为要查找的关键字 */ int Sequential_Search(int *a,int n,int key) { int i; for(i=1;i<=n;i++) { if (a[i]==key) return i; } return 0; } /* 有哨兵顺序查找 */ int Sequential_Search2(int *a,int n,int key) { int i; a[0]=key; i=n; while(a[i]!=key) { i--; } return i; } /* 折半查找 */ int Binary_Search(int *a,int n,int key) { int low,high,mid; low=1; /* 定义最低下标为记录首位 */ high=n; /* 定义最高下标为记录末位 */ while(low<=high) { mid=(low+high)/2; /* 折半 */ if (key<a[mid]) /* 若查找值比中值小 */ high=mid-1; /* 最高下标调整到中位下标小一位 */ else if (key>a[mid])/* 若查找值比中值大 */ low=mid+1; /* 最低下标调整到中位下标大一位 */ else { return mid; /* 若相等则说明mid即为查找到的位置 */ } } return 0; } /* 插值查找 */ int Interpolation_Search(int *a,int n,int key) { int low,high,mid; low=1; /* 定义最低下标为记录首位 */ high=n; /* 定义最高下标为记录末位 */ while(low<=high) { mid=low+ (high-low)*(key-a[low])/(a[high]-a[low]); /* 插值 */ if (key<a[mid]) /* 若查找值比插值小 */ high=mid-1; /* 最高下标调整到插值下标小一位 */ else if (key>a[mid])/* 若查找值比插值大 */ low=mid+1; /* 最低下标调整到插值下标大一位 */ else return mid; /* 若相等则说明mid即为查找到的位置 */ } return 0; } /* 斐波那契查找 */ int Fibonacci_Search(int *a,int n,int key) { int low,high,mid,i,k=0; low=1; /* 定义最低下标为记录首位 */ high=n; /* 定义最高下标为记录末位 */ while(n>F[k]-1) k++; for (i=n;i<F[k]-1;i++) a[i]=a[n]; while(low<=high) { mid=low+F[k-1]-1; if (key<a[mid]) { high=mid-1; k=k-1; } else if (key>a[mid]) { low=mid+1; k=k-2; } else { if (mid<=n) return mid; /* 若相等则说明mid即为查找到的位置 */ else return n; } } return 0; } int main(void) { int a[MAXSIZE+1],i,result; int arr[MAXSIZE]={0,1,16,24,35,47,59,62,73,88,99}; for(i=0;i<=MAXSIZE;i++) { a[i]=i; } result=Sequential_Search(a,MAXSIZE,MAXSIZE); printf("Sequential_Search:%d \n",result); result=Sequential_Search2(a,MAXSIZE,1); printf("Sequential_Search2:%d \n",result); result=Binary_Search(arr,10,62); printf("Binary_Search:%d \n",result); result=Interpolation_Search(arr,10,62); printf("Interpolation_Search:%d \n",result); F[0]=0; F[1]=1; for(i = 2;i < 100;i++) { F[i] = F[i-1] + F[i-2]; } result=Fibonacci_Search(arr,10,62); printf("Fibonacci_Search:%d \n",result); return 0; }
七、静态查找表算法的Java语言代码实现
实现类:
package bigjun.iplab.staticSearch; public class StaticSearch { private static final int MAXSIZE = 100; // 存储空间初始分配量 // 顺序查找,a为数组,key为要查找的关键字,查找成功则返回i,否则返回-1 private static int seqSearch(int[] a, int key) { for (int i = 0; i < a.length; i++) { if (a[i] == key) { return i; } } return -1; } // 有哨兵顺序查找数组a中从a[1]到数组末尾的key,无哨兵每次循环都需要对i是否越界,即是否小于a.length做判断。 // 设置一个哨兵可以解决不需要每次让i与a.length作比较。 // 返回-1说明查找失败,注意: 只能从数组下标为1的位置开始查找 private static int seqSearchWithGuard(int[] a, int key) { int i = a.length - 1; // 设置循环从数组尾部开始 a[0] = key; // 设置a[0]为关键字值,称之为"哨兵" while (a[i] != key) { i--; } if (i>0) { return i; } else { return -1; } } // 二分查找:数组a中所有数据中包含key的数组下标,如果没有,返回-1 private static int binarySearch(int[] a, int key) { int low = 0; // 定义最低下标为记录首位 int high = a.length-1; // 定义最高下标为记录末位 while (low<=high) { int mid = (low + high)/2; // 折半得到中间记录的下标 if (key < a[mid]) { // 若查找值比中值小 high = mid - 1; // 最高下标调整到中间下标小一位 } else if (key > a[mid]) { // 若查找值比中指大 low = mid + 1; // 最低下标调整到中间下标大一位 } else { return mid; // 若相等则说明中间记录的下标即为查找到的值 } } return -1; } // 插值查找:数组a中所有数据中包含key的数组下标,如果没有,返回-1 private static int interpolationSearch(int[] a, int key) { int low = 0; // 定义最低下标为记录首位 int high = a.length-1; // 定义最高下标为记录末位 while (low<=high) { int mid = low + (high-low)*(key-a[low])/(a[high]-a[low]);// 插值计算公式 if (key < a[mid]) { // 若查找值比中值小 high = mid - 1; // 最高下标调整到中间下标小一位 } else if (key > a[mid]) { // 若查找值比中指大 low = mid + 1; // 最低下标调整到中间下标大一位 } else { return mid; // 若相等则说明中间记录的下标即为查找到的值 } } return -1; } private static int FibonacciSearch(int[] a, int key) { // 创建一个斐波那契数列的数组 int[] F = new int[100]; // 斐波那契数列 F[0]=0; F[1]=1; for (int i = 2; i < F.length; i++) { F[i] = F[i-1] + F[i-2]; } // 斐波那契算法准备工作 int low = 0; // 定义最低下标为记录首位 int high = a.length-1; // 定义最高下标为记录末位 int k = 0; int[] a_temp = new int[F.length]; System.arraycopy(a, 0, a_temp, 0, a.length); // 将数组a复制到a_temp数组中,因为要保证a_temp的长度和F数组长度一致 while (a.length - 1 > F[k] - 1) { // 计算要查找的最大下标位于斐波那契数列中的位置 k++; } for (int i = a.length -1; i < F[k] - 1; i++) { // 将不满的数值补全 a_temp[i] = a_temp[a.length - 1]; } // 斐波那契算法开始,可以结合二分查找算法对比着理解 while (low<=high) { int mid = low + F[k-1] - 1; // 计算当前分隔的下标 if (key < a_temp[mid]) { // 若查找值比中值小 high = mid - 1; // 最高下标调整到中间下标小一位 k = k - 1; // 斐波那契数下标减一位 } else if (key > a_temp[mid]) { // 若查找值比中指大 low = mid + 1; // 最低下标调整到中间下标大一位 k = k - 2; // 斐波那契数下标减二位 } else { if (mid <= a.length - 1) { // 若相等或小于数组下标最大值则说明mid即为查找到的位置 return mid; } else { // 若min>数组下标最大值则说明是补全值,返回数组中最后一位 return a.length - 1; } } } return -1; } private static int[] createArray() { int[] a = new int[MAXSIZE + 1]; for (int i = 0; i <= MAXSIZE; i++) { a[i] = i; } return a; } public static void main(String[] args) { int[] a = createArray(); System.out.println("创建了一个数组长度为" + a.length + ",a[0]=" + a[0] + ",a[100]=" + a[100] + "的数组" ); System.out.println("顺序查找100: " + seqSearch(a, MAXSIZE)); System.out.println("顺序查找0: " + seqSearch(a, 0)); System.out.println("带监视哨的顺序查找0: " +seqSearchWithGuard(a, 0)); System.out.println("带监视哨的顺序查找1: " +seqSearchWithGuard(a, 1)); int[] array = {0,1,16,24,35,47,59,62,73,88,99}; System.out.println("二分查找62: " +binarySearch(array, 62)); System.out.println("插值查找62: " +interpolationSearch(array, 62)); System.out.println("斐波那契查找62: " +FibonacciSearch(array, 62)); System.out.println("斐波那契查找99: " +FibonacciSearch(array, 99)); System.out.println("斐波那契查找0: " +FibonacciSearch(array, 0)); System.out.println("斐波那契查找100: " +FibonacciSearch(array, 100)); } }
输出:
创建了一个数组长度为101,a[0]=0,a[100]=100的数组 顺序查找100: 100 顺序查找0: 0 带监视哨的顺序查找0: -1 带监视哨的顺序查找1: 1 二分查找62: 7 插值查找62: 7 斐波那契查找62: 7 斐波那契查找99: 10 斐波那契查找0: 0 斐波那契查找100: -1