查找
查找是在查询数据过程中必不可少的一个环节,那么如何来进行查找,以及如何进行高效率的查找,就是接下来要解决的问题。
文章目录
查找的概念
首先提出一些定义,在查找元素的时候,必然是从一堆数据中去找自己想要的数据,那么这种由同一类型的数据元素构成的集合就被称为查找表。对于查找表可执行以下操作:
- 查询某个“特定的”数据元素是否在查找表中;
- 检索某个“特定的”数据元素的各种属性;
- 在查找表中插入一个数据元素;
- 从查找表中删去某个数据元素
根据查找表的操作方式来分类,可以将查找表分为两类:静态查找表和动态查找表。
- 静态查找表:仅作查询和检索的查找表。
- 动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已存在的某个数据元素
在数据表中,应该用一个东西来标识每一个数据元素用以识别该数据,这个东西就叫做关键字,关键字是数据元素(或记录)中某个数据项的值,用以标识(识别)一个数据元素(或记录)。其中关键字也可以进行细分:
- 主关键字:可以识别唯一的一个记录的关键字
- 次关键字:能识别若干记录的关键字
关于关键字这里可以简单举个例子,以全校成绩为例,学号就是每个学生的主关键字,因为一个学号唯一识别一个学生,学院就是次关键字,学院可以识别若干个学生,但是不能唯一识别一个学生。
那么查找是一个什么过程呢,查找就是根据给定的值,在查找表中确定一个其关键字等于给定值的数据元素那么根据查询结果来看分为查询成功和查询不成功,查找成功就是在查找表中查找到指定的记录;查找不成功就是在查找表中没有找到指定记录。
最后还有很关键的一点,就是如何衡量一个查找算法的效率。这里主要参考以下几点:
- 时间复杂度
- 空间复杂度
- 平均查找长度 A S L ASL ASL
这里重点讲一下什么是平均查找长度
A
S
L
ASL
ASL,平定义为确定记录在表中的位置所进行的和关键字比较的次数的平均值,可以使用下面的公式来表示。
A
S
L
=
∑
i
=
1
n
P
i
C
i
ASL=\sum_{i=1}^n{P_iC_i}
ASL=i=1∑nPiCi
其中,
n
n
n为查找表的长度,即表中所含元素。
P
i
P_i
Pi为查找第
i
i
i哥元素的概率(
∑
P
i
=
1
\sum{P_i=1}
∑Pi=1)。
C
i
C_i
Ci是查找第
i
i
i个元素时同给定值
K
K
K比较的次数。
顺序查找
顺序查找算法是查找算法中最简单直接的一种方法,它是属于静态查找表中的一种查找方法。
顺序查找-算法原理
顺序查找有两种实现方式,第一种就是平时常用的那种,从数组的第一个元素开始往后遍历,直到遍历到数组的最后一个元素或者说找到对应的元素才停止。
对于第一种方式,在查找的时候还需要考虑数组是否会越界的问题,那么为了解决这个问题,就采用哨兵的方式来进行查找。一般情况将数组的第一个元素当作哨兵,放置待查询关键字,然后从数组的最后一个元素开始往前查找,这样一来就不用考虑越界的情况,因为数组的第一个元素一定会等于最后一个元素。
顺序查找-算法实现
对于第一种方法,直接采用最简单的 f o r for for循环从第一个遍历到最后一个即可,代码如下所示。
// 顺序查找,不带哨兵
int Sequential_Search(int data[], int n, int key)
{
// 通过for循环查找即可
for (int i = 1; i <= n; i++)
{
// 如果相等则返回下标
if (data[i] == key)
return i;
}
// 没有找到返回0
return 0;
}
对于第二种哨兵法,整体思路和前面相同,都是通过遍历的方法来进行查找,但是减少了数组越界的判断。
// 顺序查找,带哨兵
int Sequential_Search2(int data[], int n, int key)
{
data[0] = key; // 第一个位置当作哨兵
int index = n; // 从后往前进行查找
// 此处省略对数组越界的判断,因为data[0] = key
while (data[index] != key)
{
index--;
}
return index;
}
顺序查找-性能分析
对于顺序表而言,
C
i
=
n
−
i
+
1
C_i=n-i+1
Ci=n−i+1,同时假设是等概率查找,即
P
i
=
1
n
P_i=\frac{1}{n}
Pi=n1,故可以计算得到该顺序表的平均查找长度为
A
S
L
=
n
P
1
+
.
.
.
+
P
n
=
n
(
n
+
1
)
2
ASL=nP_1+...+P_n=\frac{n\left( n+1 \right)}{2}
ASL=nP1+...+Pn=2n(n+1)
同时顺序查找的优点在于其简单且适应面广(对表没有额外要求),但是缺点也很明显,平均查找长度较大,尤其是当
n
n
n很大时,查找效率很低。
折半查找
在平常查找过程中,例如在图书馆找某本书,如果图书馆中的书已经按照书名的首字母拼写摆放好了,那么就可以根据这些顺序来更加快速的找到想要的书籍,而折半查找就是源于这一思想,同时折半查找也是静态查找表中的查找方法。
折半查找-算法原理
首先需要声明的是,折半查找算法是有序表的查找方法,这也就代表在折半查找算法中,静态查找表按关键字大小的次序,有序地存放在顺序表中。
在有序表中,可以通过不断缩小区间的方法来确定要查找的元素的位置,原理类似于数学中的牛顿二分法。每次查找时根据区间的上下界,计算出一个中点值,该中点值的关键字如果大于查找的关键字,则取后半部分区间继续查找,反之则取前半部分区间继续查找,直到找到该记录或区间无法再缩小为止(找不到)。
接下来查看一个查找成功的例子。
以及一个查找不成功的例子。
折半查找-算法实现
具体到代码实现时,只需要将上述步骤设计成流程即可,如下所示。
- n n n个对象从小到大存放在有序顺序表 S T ST ST中, k k k为给定值
- 设 l o w low low、 h i g h high high指向待查元素所在区间的下界、上界,即 l o w = 1 , h i g h = n low=1, high=n low=1,high=n
- 设 m i d mid mid指向待区间的中点,即 m i d = ( l o w + h i g h ) / 2 mid=(low+high)/2 mid=(low+high)/2
- 让
k
k
k与
m
i
d
mid
mid指向的记录比较
若 k = S T [ m i d ] . k e y k=ST[mid].key k=ST[mid].key,查找成功
若 k < S T [ m i d ] . k e y k<ST[mid].key k<ST[mid].key,则 h i g h = m i d − 1 high=mid-1 high=mid−1 [上半区间]
若 k > S T [ m i d ] . k e y k>ST[mid].key k>ST[mid].key,则 l o w = m i d + 1 low=mid+1 low=mid+1 [下半区间] - 重复3,4操作,直至low>high时,查找失败。
// 折半查找
int Binary_Search(int data[], int n, int key)
{
int low = 1; // 低位
int high = n; // 高位
while (low <= high) // 不断缩小区间
{
int mid = (low + high) / 2; // 中值
if (data[mid] > key) // 如果中值大于关键字,则下一次从[low, mid-1]中找
high = mid - 1;
else if (data[mid] < key) // 如果中值小于关键字,则下一次从[mid+1,high]中找
low = mid + 1;
else // 成功找到则直接返回
return mid;
}
// 没有找到返回0
return 0;
}
折半查找-性能分析
在分析折半查找的平均查找长度时,需要借助到二叉树来进行分析,由于每次都是折半的过程,那么就可以将原来的有序序列看做成一个二叉树(中序遍历为有序结果),如下所示。
在具体到查找时,也是按照上述顺序逐层进行查找,因为有 n n n个结点的上述二叉树深度为 [ log 2 n ] + 1 [\log _{2}^{n}]+1 [log2n]+1,故折半查找法在查找过程中比较次数不超过 [ log 2 n ] + 1 [\log _{2}^{n}]+1 [log2n]+1次。上述二叉树也被称为判定树。
设有序表的长度 n = 2 h − 1 n=2^h-1 n=2h−1,则描述折半查找的判定树是深度为 h h h的满二叉树。树中层次为1的结点有1个,层次为 h h h的结点有 2 h − 1 2^{h-1} 2h−1个。
假设表中每个记录的查找概率相等,则查找成功时折半查找的平均查找长度为
A
S
L
=
1
n
∑
i
=
1
n
C
i
=
1
n
[
∑
j
=
1
h
j
⋅
2
j
−
1
]
=
n
+
1
n
ASL=\frac{1}{n}\sum_{i=1}^n{C_i=}\frac{1}{n}\left[ \sum_{j=1}^h{j\cdot 2^{j-1}} \right] =\frac{n+1}{n}
ASL=n1i=1∑nCi=n1[j=1∑hj⋅2j−1]=nn+1
折半查找的优点在于相比于顺序查找,其查找效率很高,特别是在静态查找表的长度很长时,但是缺点也很明显,那就是只能适用于有序表,且数据需要以顺序存储结构存储。
分块查找
分块查找是一种索引顺序表查找方法,是折半查找和顺序查找的简单结合。索引顺序表是将整个表分成几块,块内无序,块间有序,这两个要求的含义具体如下所示。
- 块内无序:同一块内的记录不要求有序。
- 块间有序:后一块中所有记录的关键字均大于前一块中所有记录的最大关键字。
分块查找-算法原理
那么对于索引顺序表(分块有序表)可以将其分为两个表,第一个表就是主表,用数组存放待查记录,每个数据元素至少含有关键字域;第二个就是索引表,每个结点含有最大关键字域和只想本快第一个结点的指针,如下所示。
那么在查找某个元素时,首先通过折半查找的方法来索引块中找到该元素所属的块,之后再通过顺序查找的方式在该块中找到对应的元素。
分块查找-算法实现
分块查找主要就是折半查找和顺序查找的合并版,实现起来没有问题,重点在于如何建立索引表。这里的前提是已经分好了块,但是需要去标注每个块的起始下标,直接通过遍历主表元素即可,从第一个块的最大元素比较起,遇到的第一个比第一个块最大元素还大的元素,就是第二个块的起始元素,以此类推。
// 创建索引表
void Create_Index(int data[], int n, int index[], int indexOrder[])
{
/// <summary>
/// 创建索引表
/// </summary>
/// <param name="data">查找表</param>
/// <param name="n">查找表长度</param>
/// <param name="index">每个块的最大数据</param>
/// <param name="indexOrder">每个块的起始下标</param>
int pos = 1; // 数据从1开始
indexOrder[pos++] = 1; // 第一组起点为1
for (int i = 1; i <= n; i++) // 遍历所有数据
{
if (data[i] > index[pos - 1]) // 遇到第一个比前一个块最大数据大的
{
indexOrder[pos++] = i; // 说明该点是一个新块的起点
}
}
}
这里为了简单起见,块的查找和块内查找都是使用了顺序查找进行实现,代码如下所示。
// 分块索引查找
int Index_Search(int data[], int n, int index[], int k, int indexOrder[], int key)
{
/// <summary>
/// 分块索引查找
/// </summary>
/// <param name="data">查找表</param>
/// <param name="n">查找表长度</param>
/// <param name="index">每个块的最大数据</param>
/// <param name="k">块的个数</param>
/// <param name="indexOrder">每个块的起始下标</param>
/// <param name="key">待查找的关键字</param>
/// <returns></returns>
// 先找数据所属的块
int part = 1; // 从第一块开始查找
int begin = 0; // 数据所属块的起始下标
for (int i = 1; i <= k; i++) // 逐块查找
{
// 如果当前块最大数据大于带查找关键字
// 则说明关键字就在该块中
if (index[i] >= key)
{
part = i;
begin = indexOrder[i];
break;
}
}
int end = n; // 初始化块的尾地址
if (part < k) // 判断是否是最后一个块
end = indexOrder[part + 1] - 1;
// 顺序查找
for (int i = begin; i <= end; i++)
{
if (data[i] == key)
return i;
}
return 0;
}
分块查找-性能分析
对于分块查找,设若将长度为 n n n的表分成 b b b块,每块含 s s s个记录,并设表中每个记录查找概率相等。
用折半查找方法在索引表中查找索引块
A
S
L
块间
≈
l
o
g
2
n
s
+
1
ASL_{\text{块间}}\approx log_{2}^{\frac{\mathrm{n}}{\mathrm{s}+1}}
ASL块间≈log2s+1n
用顺序查找方法在主表对应块中查找记录
A
S
L
块内
≈
s
2
ASL_{\text{块内}}\approx \frac{\mathrm{s}}{2}
ASL块内≈2s
故总的平均查找长度为
A
S
L
≈
l
o
g
2
n
s
+
1
+
s
2
ASL\approx log_{2}^{\frac{\mathrm{n}}{\mathrm{s}+1}}+\frac{\mathrm{s}}{2}
ASL≈log2s+1n+2s