有序顺序表的查找——整理好的物品怎么快速找到呢?

99 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情


书籍被摆放在书架之中,书架被一个个标好了序号,整齐的排列在一起。走进这图书馆之中,寻找着心怡的书籍,当然可以从头走到尾一个个找下去,可是也许有其他更好的方法呢?

这是来自线性表(顺序表、链表)的简单方法,基于其下标和顺序性,查找着键值为给定值的元素或结点


1.二分查找

或许书籍已经按照价格大小排列整齐,而预算有限,书籍太多,找到一本价格合适的书籍,从头走到尾或许不太合适,直接走到书架的中间部分,比较书籍的价格和自己的预算价格,就能知道自己想要的价格区间该去哪里找了

1.算法1-1
功能:给定具有n个元素的顺序表sl,确定是否存在键值为k的元素
算法步骤
(1)定义两个整型变量leftright,分别表示当前子区间的下标区间的左端点和右端点,初始值为0和n-1
(2)如果left>right,当前子表为空,查找失败,结束;否则计算当前子表下标的中间点mid=(left+right)/2,并比较sl[mid]k的大小,分为以下3种情况:

  1. 如果sl[mid]=k,查找成功,结束
  2. 如果sl[mid]>k,则k只能出现在左半区间,令right=mid-1,进入第(2)步
  3. 如果sl[mid]<k,则k只能出现在右半区间,令left=mid+1,进入第(2)步

2.示例与代码
给定有序顺序表sl={3,4,6,8,10,12,14,16,18,20}中查找键值为14的元素下标

#include<iostream>
#include<vector>
using namespace std;
//二分查找,查找顺序表sl的下标区间中是否存在值为k的元素,存在返回下标,否则返回-1
int binary_search(vector<int>sl, int left, int right, int k) {
	int mid;
	if (left > right)return -1;//查找失败
	mid = (right + left) >> 1;//下标区间的中间位置,等价于(right+left)/2
	if (sl[mid] == k)return mid;//查找成功
	else if (sl[mid]>k)return binary_search(sl, left, mid - 1, k);//转到左半区间查找
	else return binary_search(sl, mid + 1, right, k);//转到右半区间查找
}
//二分查找的非递归实现,查找顺序表sl的下标区间中是否存在值为k的元素,存在返回下标,否则返回-1
int binary_search_no(vector<int>sl, int k) {
	int left = 0, right = sl.size() - 1, mid;
	while (left <= right) {
		mid = (right + left) >> 1;//下标区间的中间位置,等价于(right+left)/2
		if (sl[mid] == k)return mid;//查找成功
		else if (sl[mid]>k)right = mid - 1;//转到左半区查找
		else left = mid + 1;//转到右半区查找
	}
	return -1;
}
int main() {
	vector<int>sl = { 3,4,6,8,10,12,14,16,18,20 };
	cout << binary_search(sl, 0, sl.size() - 1, 14) << endl;
	cout << binary_search_no(sl, 14) << endl;
}

二分查找的时间复杂度为O(logn),空间复杂度为O(1);
缺点是元素必须有序,只能用于有序顺序表,不能用于有序链表

2.插值查找

书籍不全是按照价格摆放的,大多时候书籍按照书名的首字母进行摆放,此时走到中间书架去查找显然不合适,按照书籍的首字母查找到对应的区间才是更快的方法

1.插值查找
基于有序表的查找方法,根据查找键值与键值表中最大键值与最小键值的计算结果决定查找方向,即将二分查找中的分割点的选择改进为自适应选择
2.算法1-2
功能:给定具有n个元素的顺序表sl,确定是否存在键值为k的元素
算法步骤
(1)定义两个整型变量left和right,分别表示当前子区间的下标区间的左端点和右端点,初始值为0和n-1
(2)计算mid=left+(rightleft)ksl[left]sl[right]sl[left]mid=left+(right-left)·\frac{k-sl[left]}{sl[right]-sl[left]}
(3)如果left>right,当前子表为空,查找失败,结束;否则计算当前子表下标的中间点mid=(left+right)/2,并比较sl[mid]与k的大小,分为以下3种情况:

  1. 如果sl[mid]=k,查找成功,结束
  2. 如果sl[mid]>k,则k只能出现在左半区间,令right=mid-1,进入第(2)步
  3. 如果sl[mid]<k,则k只能出现在右半区间,令left=mid+1,进入第(2)步

2.示例与代码
给定有序顺序表sl={3,4,6,8,10,12,14,16,18,20}中查找键值为14的元素下标

#include<iostream>
#include<vector>
using namespace std;
//插值查找的非递归实现,查找顺序表sl中是否存在元素的值为k
int interpolation_search(vector<int>sl, int k) {
	int left = 0, right = sl.size() - 1, mid;
	while (left <= right) {
		mid = left + (right - left) * (k - sl[left]) / (sl[right] - sl[left]);
		if (mid < 0)return -1;//k<sl[left]查找失败
		if (sl[mid] == k)return mid;//查找成功
		else if (sl[mid] > k)right = mid - 1;//转到左半区间
		else left = mid + 1;//转到右半区间
	}
	return -1;
}
int main() {
	vector<int>sl = { 3,4,6,8,10,12,14,16,18,20 };
	cout << interpolation_search(sl, 14) << endl;
}

3.优缺点
插值查找在键值分布均匀的情况下效率比二分查找高,在键值分布不均匀如sl={2,4,20,38,49,100}的情况下,查找效率低于二分查找

3.STL中的有序表查找算法

1.四种有序表查找方法
1.binary_search(begin,end,k):判断k在[begin,end)区间是否存在,存在返回ture,否则返回false
2.lower_bound(begin,end,k):返回区间[begin,end)中的第一个大于等于k的迭代器,如果区间中所有元素都小于k,返回end
3.upper_bound(begin,end,k):返回区间[begin,end)中的第一个大于k的迭代器,如果区间中所有元素都小于等于k,返回end
4.equal_range(begin,end,k):返回一个pair型变量,第一个元素为lower_bound的返回结果,第二个元素为upper_bound的返回结果,可以用来查[begin,end)中k出现的次数

注意begin和end都为迭代器
头文件为<algorithm>

2.应用
1.线性容器(vector,list,deque和数组)只能应用全局函数
2.关联容器(map,multimap,set,multiset)可以应用全局函数binary_search,但必须通过成员函数调用其他三个函数,此时函数的参数只有一个k
3.示例与代码

#include<iostream>
#include<vector>
#include<set>
#include<algorithm>
using namespace std;

int main() {
	vector<int>ve = { 2,4,6,6,6,6,6,8,10,12 };
	if (binary_search(ve.begin(), ve.end(), 2))cout << "存在" << endl;
	else cout << "不存在" << endl;
	vector<int>::iterator it = lower_bound(ve.begin(), ve.end(), 6);
	if (it != ve.end())cout << *it << endl;//输出结果为6
	int i = lower_bound(ve.begin(), ve.end(), 6) - ve.begin();
	if (i < ve.size())cout << i << ' ' << ve[i] << endl;//输出结果为2 6
	i = upper_bound(ve.begin(), ve.end(), 6) - ve.begin();
	if (i < ve.size())cout << i << ' ' << ve[i] << endl;//输出结果为7 8
	pair<vector<int>::iterator, vector<int>::iterator>pi;
	pi = equal_range(ve.begin(), ve.end(), 6);
	cout << pi.second - pi.first << endl;//输出结果为5
	set<int>se = { 2,6,4,10,8 };
	set<int>::iterator it1 = se.lower_bound(5);
	if (it1 != se.end())cout << *it1 << endl;//输出结果为6
}