查找(search) 是指在数据集合中寻找满足某种条件的数据元素的过程。用于查找的数据集合则称为 查找表(search table)。查找表中的数据元素类型是一致的,并且有能够唯一标识出元素的 关键字(keyword)。如果从查找表中找出了关键字等于某个给定值的数据元素,则称为 查找成功,否则称 查找不成功。
通常对查找表有 4 种操作:
查找:在查找表中查看某个特定的记录是否存在
检索:查找某个特定记录的各种属性
插入:将某个不存在的数据元素插入到查找表中
删除:从查找表中删除某个特定元素
如果对查找表只执行前两种操作,则称这类查找表为 静态查找表(static search table)。静态查找表建立以后,就不能再执行插入或删除操作,查找表也不再发生变化。对应的,如果对查找表还要执行后两种操作,则称这类查找表为 动态查找表(dynamic search table)。本章我们要介绍的查找算法都是针对静态查找表的,比如顺序查找、折半查找、分块查找等;而对于动态查找表,往往使用二叉平衡树、B-树或哈希表来处理。
对于各种各样的查找算法,我们要如何比较他们的优劣呢?通常,我们使用 平均查找长度(average search length, ASL) 来衡量查找算法的性能。对于含有 nn 个元素的查找表,定义查找成功的平均查找长度为ASL。考虑到查找不成功的情况,查找算法的平均查找长度应该是查找成功的平均查找长度和查找不成功的平均查找长度之和。通常我们在说平均查找长度时,不考虑查找不成功的情况。
顺序查找(又称线性查找,sequential search),是指在线性表中进行的查找算法。顺序查找算法是最直观的一种查找算法,它从线性表的一端出发,逐个比对关键字是否满足给定的条件。
顺序查找按照查找表中数据的性质,分为对一般的无序线性表的顺序查找和对按关键字有序的线性表的顺序查找。下面我们分别对这两种查找算法进行讲解。
对于一般线性表的查找,基本思想是从线性表的一端开始,逐个比对关键字是否满足给定的条件。若找到某个元素的关键字满足给定的要求,则查找成功;若一直找到线性表的另一端仍未有满足要求的元素,则查找不成功。让我们来一起分析一下在一般线性表上的查找算法的平均查找长度。
对于有 n个元素 a_0, a_1,…,a_{n-1}的查找表,每个元素的查找概率 P_i=\frac{1}{n} 。若每次查找都从第一个元素 a_0开始,则查找第 i 个元素 a_i-1时,需进行 C_i=i+1次比较操作。因此,查找成功的平均查找长度ASL=(n+1)/2
而当查找不成功时,与查找表中各个元素的比较次数为 nn 次,因此查找不成功的平均查找长度为ASLfailed=n
对于有序表的顺序查找,在查找成功时与一般线性表的查找是一样的。而对于查找不成功的情况,无需和表中所有元素都进行比对就可以确认查找不成功,这样能降低查找不成功时的平均查找长度。
具体来说,假设查找表 a_0, a_1, …, a_{n-1}a0 ,a1 ,…,an−1
是从小到大排列的,查找的顺序是从左向右。若待检索的关键字为 key,当查找到第 i个元素时,如果第 i个元素的值大于 key,而之前并没有查找成功时,就可以认为查找不成功了。
很显然,通过这样的优化,我们能将查找不成功的平均查找长度降低。假设对于所有查找不成功的关键字 key,落在 (−∞,a0 ),(a0 ,a1 ),…,(an−2 ,an−1 ),(an−1 ,∞) 这 n+1 个区间的概率是相等 。那么查找不成功的平均查找长度为ASLfailed
1+2+3+…+n+n/n+1
大致是之前查找不成功的平均查找长度 n 的一半,效率提升还是很明显的。当然,这种方法只适用于有序表。
#include <iostream>
#include <cstring>
using namespace std;
class Vector {
private:
int size, length;
int *data;
public:
Vector(int input_size) {
size = input_size;
length = 0;
data = new int[size];
}
~Vector() {
delete[] data;
}
bool insert(int loc, int value) {
if (loc < 0 || loc > length) {
return false;
}
if (length >= size) {
expand();
}
for (int i = length; i > loc; --i) {
data[i] = data[i - 1];
}
data[loc] = value;
length++;
return true;
}
void expand() {
int * old_data = data;
size = size * 2;
data = new int[size];
for (int i = 0; i < length; ++i) {
data[i] = old_data[i];
}
delete[] old_data;
}
int search(int value) {
for(int i=0;i<length;i++){
if(data[i]==value){
return i;
}
else if(data[i]>value){
return -1;
}
}
return -1;
}
bool remove(int index) {
if (index < 0 || index >= length) {
return false;
}
for (int i = index + 1; i < length; ++i) {
data[i - 1] = data[i];
}
length = length - 1;
return true;
}
void print() {
for (int i = 0; i < length; ++i) {
if (i > 0) {
cout << " ";
}
cout << data[i];
}
cout << endl;
}
};
int main() {
Vector a(100);
a.insert(0, 2);
a.insert(1, 4);
a.insert(2, 6);
a.insert(3, 8);
a.insert(4, 10);
cout << a.search(4) << endl;
cout << a.search(5) << endl;
return 0;
}
这个更优的查找算法就是折半查找算法。它的基本流程如下:
首先确定待查关键字在有序(这里我们假设是升序,即从小到大)的查找表中的范围。通常用两个下标来表示范围:left = 0, right = length - 1left=0,right=length−1。然后用给定的关键字和查找表的正中间位置(下标为mid=2left+right)元素的关键字进行比较,若相等,则查找成功;若待查关键字比正中间位置元素的关键字大,则继续对右子表(left = mid + 1)进行折半查找;否则对左子表(right = mid - 1)进行折半查找。
如此重复进行,直到查找成功或范围缩小为空(left > rightleft>right)即查找不成功为止。这个更优的查找算法就是折半查找算法。它的基本流程如下:
首先确定待查关键字在有序(这里我们假设是升序,即从小到大)的查找表中的范围。通常用两个下标来表示范围:left = 0, right = length - 1然后用给定的关键字和查找表的正中间位置(下标为 mid=1/2left+right
)元素的关键字进行比较,若相等,则查找成功;若待查关键字比正中间位置元素的关键字大,则继续对右子表(left = mid + 1)进行折半查找;否则对左子表(right = mid - 1)进行折半查找。
如此重复进行,直到查找成功或范围缩小为空(left > rightleft>right)即查找不成功为止。
下面我们来一起分析一下折半查找算法的平均查找长度 ASLASL。为了直观,我们把有序表折半查找的过程用一颗 二叉判定树(binary decision tree, BDT) 来表示,如下图:
而如果用顺序查找算法,平均查找长度为
折半查找是比顺序查找更高效的查找算法。
无论查找成功还是查找不成功,在有序表中查找某个关键字的过程就是从根节点出发走到该关键字对应结点的路径,而路径的长度就对应着查找长度。与此同时,这个路径长度也对应着该结点在树上的深度。由于树的深度是 \lfloor O(log_2n) \rfloor + 1,所以每次查找不成功的比较次数不会超过 \lfloor O(log_2n) \rfloor + 1
因此,折半查找的时间复杂度为 O(log_2n),明显优于时间复杂度为 O(n) 的顺序查找算法。不过一定要注意,折半查找只适用于关键字有序的顺序表,无序的线性表如果想使用折半查找要先进行排序操作,而链表因为无法随机存取所以没有办法使用折半查找。
#include <iostream>
#include <cstring>
using namespace std;
class Vector {
private:
int size, length;
int *data;
public:
Vector(int input_size) {
size = input_size;
length = 0;
data = new int[size];
}
~Vector() {
delete[] data;
}
bool insert(int loc, int value) {
if (loc < 0 || loc > length) {
return false;
}
if (length >= size) {
expand();
}
for (int i = length; i > loc; --i) {
data[i] = data[i - 1];
}
data[loc] = value;
length++;
return true;
}
void expand() {
int * old_data = data;
size = size * 2;
data = new int[size];
for (int i = 0; i < length; ++i) {
data[i] = old_data[i];
}
delete[] old_data;
}
int search(int value) {
int left=0, right=length-1;
while(left <= right){
int mid=(left+right)/2;
if(data[mid]==value){
return mid;
}
else if(data[mid]<value){
left=mid+1;
}
else{
right=mid-1;
}
}
return -1;
}
bool remove(int index) {
if (index < 0 || index >= length) {
return false;
}
for (int i = index + 1; i < length; ++i) {
data[i - 1] = data[i];
}
length = length - 1;
return true;
}
void print() {
for (int i = 0; i < length; ++i) {
if (i > 0) {
cout << " ";
}
cout << data[i];
}
cout << endl;
}
};
int main() {
Vector a(100);
a.insert(0, 2);
a.insert(1, 4);
a.insert(2, 6);
a.insert(3, 8);
a.insert(4, 10);
cout << a.search(4) << endl;
cout << a.search(5) << endl;
return 0;
}