主要内容
基本概念
在介绍查找算法前我们先重温几个重要概念:
1)数据:客观事物的符号表示,是所有能输入到计算机中,并能被计算机程序处理的符号的总称。如数学计算中用到的整数和实数,文本编辑中用到的字符串,多媒体程序处理的图形、图像、声音及动画等通过特殊编码定义后的数据。
2)数据元素:数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。数据元素也称记录,用于描述一个完整的对象,该对象可以是一名学生的信息(记录),某一次棋局,图中的某一个顶点等。
3)数据项:组成数据元素的、有独立含义的、不可分割的最小单位。如学生记录中的学号、姓名、性别等都属于数据项。
4)数据结构:相互之间存在一种或多种特定关系的数据元素的集合。数据结构包括逻辑结构(逻辑表示)和存储结构(物理实现)。
5)查找表:由同一类型的数据元素(或记录)构成的集合,可以由线性表、树结构等多种数据结构来实现。
6)关键字:数据元素(或记录)中的某个数据项的值,相当于“数据库”知识中一行记录的主码(PRIMARY KEY)。
typedef struct /*定义数据元素结构体*/
{
KeyType key; /*关键字*/
OtherType Other; /*其他数据项*/
} Element;
typedef struct /*以顺序表结构定义查询表*/
{
/*顺序表借助数组存储数据,因此需要空间基地址base(数组头)和当前长度length(数组长度)*/
Element *base;
int length;
} SSTable; /*SSTable表示顺序查询表*/
7)查找:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。同样可以类比数据库的数据查询。针对不同的数据结构,查找算法也会有所不同。
8)平均查找长度:衡量查找算法性能的一个标准。
线性表的查找
在查找表的数据结构中,线性表是最简单的一种。
顺序查找(Sequential Search)
顾名思义,从查询表的一头顺序往下查找,直至找到含有给定关键字的数据元素。
下面给出顺序查找的两种算法形式:
/*-------First-------*/
int Seq_Search(SSTable t, KeyType key)
{
for(int i = 0; i < length; i++)
{
if(t.base[i].key == key) return i;
}
return 0;
}
/*-------Second-------*/
int Seq_Search(SSTable t, KeyType Key)
{
t.base[0].key = key; /*将头元素设为监视哨*/
for(int i = t.length; t.base[i] != key; i--);
return i;
}
Tips:
1)base是数组头,同时也作数组名,对数组和指针概念不清晰的同学请戳“这里”。
2)在第一种算法形式中,base[]的空间也用来存放数据元素,所以循环是从0开始,到length前结束,共遍历length个位置(但貌似一般习惯在1到length存放数据元素);
3)在第二种算法形式中,我们设置了“监视哨”,“监视哨”的含义可以先不去深入理解,我们先知道它的作用是什么。
设置头元素为监视哨后,每一步循环都只需要判断关键字是否为给定值,而不需要先判断i到哪了,再去判断是否找到给定值。
与此同时,因为设置了监视哨,存储length个元素的位置变成了从1到length。
至于为什么第一种形式可以用从前往后查找,而第二种形式建议用从后往前查找,这就留给大家思考了。
4)虽然在第二种算法形式中减少了判断步骤,但是两种形式的时间复杂度是一样的。不过实践证明,在查询数据很多很多的时候,第二种算法形式的处理时间几乎是第一种的一半。
折半查找(Binary Search)
顾名思义,折半查找每一次查找比较都能使查找范围缩小一半,它是一种效率较高的查找方法。
但是折半查找要求线性表必须采用顺序存储结构,而且表中元素必须按关键字有序排列(有点类似数据库的索引)。
为了标记出查找范围,我们要设置三个变量low、high、middle分别表示查找范围的下界、上界和中间位置(每次查找后更新赋值)。
提前路过的圈毛君:“总是一不留心就把“查找”写成了“查询”(数据库留下的习惯),虽然这两个词意思一样,但无奈博主是强迫症_(:з」∠)_喜欢维持前后文的用词一致性。”
下面给出代码:
int Bin_Search(SSTable t, KeyType key)
{
int low = 1, high = t.length;
while(low <= high)
{
int mid = (low + high) / 2;
if(t.base[mid] == key) return mid;
else if(t.base[mid] < key) low = mid+1;
else high = mid-1;
}
return 0;
}
在每次循环中,low、high、mid中的其中两个值都要作更新,而且这个不断缩小查找范围的过程也很像一个不断深入、细化的树或图的遍历过程,因此我们很容易想到一个词——递归。
下面给出折半查找的递归算法的实现:
int Bin_Search(SSTable t, KeyType key, int low, int high)
{
if(low <= high)
{
int mid = (low + high) / 2;
if(t.base[mid] == key) return mid;
else if(t.base[mid] < key) low = mid + 1;
else high = mid-1;
}
Bin_Search(t, key, low, high);
}
折半查找过程可用二叉树来描述,由此得到的二叉树称为判定树。借助判定树可以很快求出折半查找的平均查找长度。
优点:
查找效率比顺序查找高,时间复杂度为O(log2n)。
缺点:
只适用于顺序存储结构的有序表,所以查找前必须先对表进行排序,而排序本身就是一种费时的运算。而且,对于有序表并不方便进行插入、删除操作,因此折半查找不适用于数据元素经常发生变动的查询表。
分块查找(Blocking Search)
分块查找又称索引顺序查找,后一个名称更能反映查找的特性,这是一种性能介于顺序查找和折半查找之间的查找方法。
在该查找算法中,除了查询表外,还需要为查询表建立一个索引表。
建立索引表的过程分三步:
1)将查询表分块,分成若干个子表,取出每个子表中的第一个数据元素的地址作为索引表中第一个数据元素的其中一个数据项;
2)取出每个子表中的最大关键字作为索引表每个数据元素的关键字;
3)根据关键字对每一块进行排序。
分块查找的过程分两步:
1)通过折半查找确定待查记录所在的块(子表)。如给定关键字27,第一个块的最大关键字为22,第二个块的最大关键字为35,则待查数据位于第二个块。
2)在块中顺序查找。从第二个块中的第一个数据元素开始遍历。
/*--------索引表--------*/
typedef struct
{
KeyType maxkey; /*每一块的最大关键字*/
Element *first; /*每一块的首元素地址*/
} IndexElement;
typedef struct
{
IndexElement *base;
int length;
} Index;
CreatIndex(Index &i);
/*--------分块查询--------*/
/*********分块*********/
DivideBlock(SSTable t, Index &i, int blocknum) /*传递参数包括查询表,索引表的引用,切分块数*/
{
int avg_length = t.length / blocknum;
int j = k = 1;
while(j < t.length)
{
i.base[K].maxkey = Max(t, j, avg_length);
/*Max()函数可以求出查询表t中 base[n](j <= n < j + avg_length)的最大关键字*/
/*若出现 n > t.length 的情况,给予相应处理*/
i.base[k].first = &t.base[j];
i.length++;
j = j + avg_length;
k++;
}
}
/*********查找**********/
int Blo_Search(SSTable t, Index i, Keytype key)
{
/*对索引表折半查找*/
/*对块顺序查找*/
}
优点:
进行插入、删除操作时,只要确定该元素所在的块,就能在块内插入或删除数据元素。由于块内元素无序,插入和删除时不需要大量移动数据元素。
缺点:
需要额外增加索引表的存储空间,并对索引表排序。