顺序查找
顺序查找又称线性查找,它对顺序表和链表都是适用的。对于顺序表,可通过数组下标递增来顺序扫描每个元素;对于链表,可通过指针 n e x t {\rm next} next 来依次扫描每个元素。顺序查找通常分为对一般的无序线性表的顺序查找和对按关键字有序的线性表的顺序查找。下面分别进行讨论。
1.一般线性表的顺序查找
作为一种最直观的查找方法,其基本思想是从线性表的一端开始,逐个检查关键字是否满足给定的条件。
若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置;
若已经查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败的信息。
下面给岀其算法,主要是为了说明其中引入的“哨兵”的作用。
#include <iostream>
using namespace std;
template<typename T>
struct SSTable {
T *element;
int TableLen;
};
template<typename T>
int Seq_Search(SSTable<T> ST, T key) {
ST.element[0] = key; //哨兵
//从后往前找
int i = ST.TableLen;
while (ST.element[i] != key)
i--;
return i; //若表中不存在关键字为key的元素,将查找到i为0
}
int main() {
SSTable<int> ST;
ST.TableLen = 10;
ST.element = new int[ST.TableLen + 1];
for (int i = 1; i <= ST.TableLen; i++)
ST.element[i] = i;
cout << Seq_Search(ST, 11) << endl;
cout << Seq_Search(ST, 8) << endl;
return 0;
}
运行结果:
0
8
在上述算法中,将ST.element[0]
称为“哨兵”。引入它的目的是使得Seq_Search
内的循环不必判断数组是否会越界,因为满足i==0
时,循环一定会跳出。需要说明的是,在程序中引入“哨兵”并不是这个算法独有的。引入“哨兵”可以避免很多不必要的判断语句,从而提高程序效率。
对于有
n
{\rm n}
n 个元素的表,给定值key与表中第
i
{\rm i}
i 个元素相等,即定位第
i
{\rm i}
i 个元素时,需进行
n
−
i
+
1
{\rm n-i+1}
n−i+1 次关键字的比较,即
C
i
=
n
−
i
+
1
{\rm C_i=n-i+1}
Ci=n−i+1 。查找成功时,顺序查找的平均长度为:
A
S
L
成功
=
∑
i
=
1
n
P
i
(
n
−
i
+
1
)
{\rm ASL_{成功}} =\sum_{i=1}^{n} P_i(n-i+1)
ASL成功=i=1∑nPi(n−i+1)
当每个元素的查找概率相等,即
P
i
=
1
/
n
{\rm P_i=1/n}
Pi=1/n 时,有:
A
S
L
成功
=
∑
i
=
1
n
P
i
(
n
−
i
+
1
)
=
n
+
1
2
{\rm ASL_{成功}} =\sum_{i=1}^{n} P_i(n-i+1)=\frac{n+1}{2}
ASL成功=i=1∑nPi(n−i+1)=2n+1
查找不成功时,与表中各关键字的比较次数显然是
n
+
1
{\rm n+1}
n+1 次,从而顺序查找不成功的平均查找长度为
A
S
L
不成功
=
n
+
1
{\rm ASL_{不成功}=n+1}
ASL不成功=n+1 。
通常,查找表中记录的查找概率并不相等。若能预先得知每个记录的查找概率,则应先对记录的查找概率进行排序,使表中记录按查找概率由大至小重新排列。
综上所述,顺序查找的缺点是当 n {\rm n} n 较大时,平均查找长度较大,效率低;优点是对数据元素的存储没有要求,顺序存储或链式存储皆可。对表中记录的有序性也没有要求,无论记录是否按关键字有序,均可应用。同时还需注意,对线性的链表只能进行顺序查找。
2.有序表的顺序查找
若在查找之前就已经知道表是关键字有序的,则查找失败时可以不用再比较到表的另一端就能返回查找失败的信息,从而降低顺序查找失败的平均查找长度。
假设表 L {\rm L} L 是按关键字从小到大排列的,查找的顺序是从前往后,待查找元素的关键字为key, 当查找到第 i {\rm i} i 个元素时,发现第 i {\rm i} i 个元素对应的关键字小于key,但第 i + 1 {\rm i+1} i+1 个元素对应的关键字大于key,这时就可返回查找失败的信息,因为第 i {\rm i} i 个元素之后的元素的关键字均大于key,所以表中不存在关键字为key的元素。
可以用下图所示的判定树来描述有序线性表的查找过程。树中的圆形结点表示有序线性表中存在的元素;树中的矩形结点称为失败结点(若有 n {\rm n} n 个结点,则相应地有 n + 1 {\rm n+1} n+1 个查找失败结点),它描述的是那些不在表中的数据值的集合。若查找到失败结点,则说明查找不成功。
![image-20220805153316998](https://i-blog.csdnimg.cn/blog_migrate/715292ce529920a1b87e88198bc4a4c9.png)
在有序线性表的顺序查找中,查找成功的平均查找长度和一般线性表的顺序查找一样。查找失败时,查找指针一定走到了某个失败结点。这些失败结点是我们虚构的空结点,实际上是不存在的,所以到达失败结点时所查找的长度等于它上面的一个圆形结点的所在层数。查找不成功的平均查找长度在相等查找概率的情形下为
A
S
L
不成功
=
∑
j
=
1
n
q
j
(
l
j
−
1
)
=
1
+
2
+
⋯
+
n
+
n
n
+
1
=
n
2
+
n
n
+
1
{\rm ASL_{不成功}} =\sum_{j=1}^{n} q_j(l_j-1)=\frac{1+2+ \dots +n+n}{n+1} =\frac{n}{2} +\frac{n}{n+1}
ASL不成功=j=1∑nqj(lj−1)=n+11+2+⋯+n+n=2n+n+1n
式中,
q
j
q_j
qj 是到达第
j
{\rm j}
j 个失败结点的概率,在相等查找概率的情形下,它为
1
/
(
n
+
1
)
{\rm 1/(n+1)}
1/(n+1);
l
i
l_i
li是第
j
{\rm j}
j 个失败结点所在的层数。
注意,有序线性表的顺序查找和后面的折半查找的思想是不一样的,且有序线性表的顺序查找中的线性表可以是链式存储结构。
折半查找
折半查找又称二分查找,它仅适用于有序的顺序表。
折半查找的基本思想:首先将给定值key与表中中间位置的元素比较,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素以外的前半部分或后半部分 (例如,在查找表升序排列时,若给定值key大于中间元素,则所查找的元素只可能在后半部分)。 然后在缩小的范围内继续进行同样的查找,如此重复,直到找到为止,或确定表中没有所需要查找的元素,则查找不成功,返回查找失败的信息。算法如下
#include <iostream>
using namespace std;
template<typename T>
struct SeqList {
T *element;
int TableLen;
};
template<typename T>
int Binary_Search(SeqList<T> L, T key) {
int low = 0, high = L.TableLen - 1, mid;
while (low <= high) {
mid = (low + high) / 2; //取中间位置
if (L.element[mid] == key)
return mid; //查找成功则返回所在位置
else if (L.element[mid] > key)
high = mid - 1; //从前半部分继续查找
else
low = mid + 1; //从后半部分继续查找
}
return -1; //查找失败,返回-1
}
int main() {
SeqList<int> seqList;
seqList.TableLen = 10;
seqList.element = new int[seqList.TableLen];
for (int i = 0; i < seqList.TableLen; i++)
seqList.element[i] = i;
cout << Binary_Search(seqList, 11) << endl;
cout << Binary_Search(seqList, 8) << endl;
return 0;
}
输出结果:
-1
8
例如,已知11个元素的有序表{7, 10, 13, 16, 19, 29, 32, 33, 37, 41, 43}
,指针low
和high
分别指向表的下界和上界,mid
则指向表的中间位置
⌊
(
l
o
w
+
h
i
g
h
)
/
2
⌋
{\rm \left \lfloor (low+high)/2 \right \rfloor }
⌊(low+high)/2⌋,以下说明查找11的过程:
![image-20220805164518140](https://i-blog.csdnimg.cn/blog_migrate/71211e61ff4ac2942c2bb22ee3ff998e.png)
第一次查找,将中间位置元素与key
值比较。因为11<29
,说明待查元素若存在,则必在范围[low,mid-1]
内,令指针high
指向位置mid-1, high=mid-1=5
,重新求得mid = (1 + 5) / 2 = 3
,第二次的查找范围为[1,5]
![image-20220805164806887](https://i-blog.csdnimg.cn/blog_migrate/cedae5bd85bff1356a467e7d32c892eb.png)
第二次查找,同样将中间位置元素与key
值比较。因为11<13
,说明待查元素若存在,则必在范围[low,mid-1]
内,令指针high
指向位置mid-1, high=mid-1=2
,重新求得mid = (l + 2) / 2 = 1
,第三次的查找范围为[1,2]
![image-20220805165407351](https://i-blog.csdnimg.cn/blog_migrate/dbb3af60d02ee334751a27700f0519a1.png)
第三次查找,将中间位置元素与key
值比较。因为11>7
,说明待查元素若存在,则必在范围[mid+1,high]
内。令low=mid+1=2, mid= (2+2)/2=2
,第四次的查找范围为[2,2]
。
![image-20220805165304601](https://i-blog.csdnimg.cn/blog_migrate/401b494553574492dd5c925d840a5d1a.png)
第四次查找,此时子表只含有一个元素,且10!=11
,故表中不存在待查元素。
折半查找的过程可用下图所示的二叉树来描述,称为判定树。树中每个圆形结点表示一个记录,结点中的值为该记录的关键字值;树中最下面的叶结点都是方形的,它表示查找不成功的情况。从判定树可以看出,查找成功时的查找长度为从根结点到目的结点的路径上的结点数,而查找不成功时的查找长度为从根结点到对应失败结点的父结点的路径上的结点数;每个结点值均大于其左子结点值,且均小于于其右子结点值。若有序序列有 n {\rm n} n 个元素,则对应的判定树有 n {\rm n} n 个圆形的非叶结点和 n + 1 {\rm n+1} n+1 个方形的叶结点。显然,判定树是一棵平衡二叉树。
![image-20220805165939610](https://i-blog.csdnimg.cn/blog_migrate/ce04b125b8e497e67fede60fc409f96e.png)
由上述分析可知,用折半查找法查找到给定值的比较次数最多不会超过树的高度。在等概率查找时,查找成功的平均查找长度为
A
S
L
=
1
n
∑
i
=
1
n
l
i
=
1
n
(
1
×
1
+
2
×
2
+
⋯
+
h
×
2
h
−
1
)
=
n
+
1
n
log
2
(
n
+
1
)
−
1
≈
log
2
(
n
+
1
)
−
1
{\rm ASL}=\frac{1}{n} \sum_{i=1}^{n} l_i =\frac{1}{n}(1 \times 1+ 2\times 2 + \dots +h \times 2^{h-1}) =\frac{n+1}{n} \log_{2}{(n+1)} -1 \approx \log_{2}{(n+1)} -1
ASL=n1i=1∑nli=n1(1×1+2×2+⋯+h×2h−1)=nn+1log2(n+1)−1≈log2(n+1)−1
式中,
h
h
h 是树的高度,并且元素个数为
n
n
n 时树高
h
=
⌈
log
2
(
n
+
1
)
⌉
h=\left \lceil \log_{2}{(n+1)} \right \rceil
h=⌈log2(n+1)⌉。所以,折半查找的时间复杂度为
O
(
log
2
n
)
O(\log_{2}{n} )
O(log2n),平均情况下比顺序查找的效率高。
因为折半查找需要方便地定位查找区域,所以它要求线性表必须具有随机存取的特性。因此,该查找法仅适合于顺序存储结构,不适合于链式存储结构,且要求元素按关键字有序排列。