算法精学--搜索
好好的整理一下搜索算法,资料参考于hello算法
一、二分查找
一般是要求是有序的,无法对无序数组进行查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 在一个有序的数据中查找一个给定的值,使用左闭右闭区间,找到就返回坐标,没找到返回-1
int binarySearch(vactor<int> &nums, int target){
int l = 0, r = nums.size() - 1;
while(l <= r){
//不要使用(i + j) / 2,可能会超过范围,下面这种写法mid在中间偏左
int mid = l + (r - l) / 2;
if(nums[mid] > target){ //目标在左边
r = mid - 1;
}else if(nums[mid] < target){ //目标在右边
l = mid + 1;
}else return mid;
}
return -1;
}时间复杂度O(logn),空间复杂度O(1)
优点和局限性
- 优点
- 事件效率高
- 无需额外的空间
- 局限性
- 仅适用于有序数据,如果为了二分查找而排序,得不偿失
- 只适用于数组,因为需要跳跃查询
- 在小数据量下,线性查找反而更快,因为只有一次判断,二分查找需要加法,除法,判断等
- 优点
二分查找还有很多变形,比如去查找有重复元素的插入位置等,也就是上面的代码判断相等之后,不要直接返回,要继续缩小范围。这是因为 nums 数组中存在重复元素,binary_search_insertion 的目的是找到第一个等于 target 的元素的数组下标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/* 二分查找插入点(存在重复元素) */
int binarySearchInsertion(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 初始化双闭区间 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) {
i = m + 1; // target 在区间 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在区间 [i, m-1] 中
} else {
j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中
}
}
// 返回插入点 i
return i;
}
二、搜索算法比较
搜索算法可以根据实现思路分为
- 利用遍历的数据结构定位目标,数组、链表、树和图等
- 利用数据组织结构或数据的包含的先验知识,实现高效的元素查找,二分,哈希和二叉搜索树等
查找算法效率的比较
操作 线性搜索 二分查找 树查找 哈希查找 查找元素 O(n) O(logn) O(logn) O(1) 插入元素 O(1) O(n) O(logn) O(1) 删除元素 O(n) O(n) O(logn) O(1) 额外空间 O(n) O(1) O(n) O(1) 数据预处理 / 排序O(nlogn) 建树O(nlogn) 建哈希表O(n) 数据是否有序 无序 有序 有序 无序 各种算法特点,简单说,线性搜索适用于小型或频率更新的数据;二分查找适用于大型、排序的数据;哈希查找适用于对查询效率要求高且无需范围查询的数据;树查找适用于需要维护顺序和支持范围查询的大型动态数据。
- 线性搜索
- 通用性好,无需任何的数据预处理。如果只是需要一次查询,那么其他三种方法的预处理的时间比线性搜索的事件还长
- 适用于体量较小的数据
- 适用于数据更新频率较高的场景,不需要对数据进行任何额外的维护
- 二分查找
- 使用于大数据量的情况,效率表现稳定,最差也是O(logn)
- 数据量还不能过大,因为存储数据需要连续的内存空间
- 不适应于高频的增删数据的场景
- 哈希查找
- 适合对查找性能要求较高的场景,平均事件复杂度为O(1)
- 不适用于有序数据或范围查找的场景,哈希表无法维护数据的有效性
- 对哈希函数和哈希冲突处理策略的依赖较高,具有较大的性能劣化风险
- 不适用于数据量过大的情况,因为哈希表需要额外的空间来最大程度的减少冲突,从而提供良好的查询性能
- 树查询
- 适用于海量的数据,因为树节点在内存中是分散存储的
- 适合需要维护有序数据和范围查找的场景
- 在持续增删节点的过程中,二叉搜索树可能会产生倾斜,事件复杂的劣化至O(n)
- 若使用AVL树或红黑树,则各项操作在O(logn)效率下稳定运行,但是维护平衡的操作会增加额外开销
- 线性搜索