(学习资料来源:维基百科,《算法导论》,《大话数据结构》,《编程珠玑》,《编程珠玑续》,google)
查找(搜索)算法(Search algorithm)
(下面的定义参考自《大话数据结构》)
- 查找表(Search Table)由同一类型的数据元素(或记录)构成的集合。
- 关键字(Key)
是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素。也可以标志一个记录的某个数据项(字段),称为关键码。
主关键字(Primary Key)
此关键字可以唯一地标识一个记录。主关键字所在的数据项称为主关键码。次关键字(Secondary Key)
此关键字可以识别多个数据元素(或记录)。次关键字所在的数据项称为次关键码。查找表分类 静态查找表(Static Search Table):只作查找操作的查找表。
针对静态查找的而使用的数据结构及算法思路:线性表查找:顺序表查找;有序表查找:二分查找、插值查找、斐波那契查找。动态查找表(Dynamic Search Table):在查找过程中还进行插入或删除操作。
针对动态查找而使用的数据结构及算法思路:二叉排序树。其他算法思路:散列表(哈希表Hash Table)。 查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。 附注:在很多参考书籍或文献中,一些文献使用的是“查找”这个词,一些文献使用的是“搜索”这个词,对此我并没有考究,习惯于使用查找而不是搜索。
线性查找(顺序查找)(linear search / sequential search)
Algorithm(http://en.wikipedia.org/wiki/Linear_search)
Linear search or sequential search is a method for finding a particular value in a list that checks each element in sequence until the desired element is found or the list is exhausted.The list need not be ordered.思想:从给定表的第一个元素开始一个一个向下查找,如果有和目标一致的元素,查找成功;如果到最后一个元素仍没有目标元素,则查找失败。
输入:查找表为有序状态或无序状态均可,这里使用的是数组结构,若表含有多个相同的元素,则返回第一个查找到的目标元素(采用顺序遍历和逆序遍历可分别找到最接近数组头和数组尾的目标元素)。
时间复杂度:最坏情况:差找成功时待查找元素数据元素在最后一个位置,时间复杂度为O(n),查找不成功时间复杂度为O(n+1);最好情况:待查找元素数据元素在第一个位置,时间复杂度O(1);平均q情况:时间复杂度为O((n + 1)/2) = O(n)。
空间复杂度:O(1)。
算法分析:适用于数据量较小的情况。
算法实现。注意:这里假定数据在数组中存放时从下标1开始,返回0代表搜索失败,非零代表索引值。
/** * @brief sequence search algorithm * * @param[in] array array input * @param[in] length array length * @param[in] key key value * * @warning the index of the array begin for 1 * * @return the index of the key, return 0 if no fountd */
int sequence_search(int* array, int length, int key)
{
assert(array != NULL && length >= 0);
for (int i = 1; i <= length; i++) {
if (array[i] == key)
return i;
}
return 0;
}
算法优化:如果数组长度足够,数组首位或末位为无效数据,可以设置哨兵(sentinel),去除上面算法中i与length的比较,避免了遍历时每次判断索引值i是否溢出。
/** * @brief sequence search algorithm using a sentinel * * @param[in] array array input * @param[in] length array length * @param[in] key key value * * @warning the index of the array begin for 1 * * @return the index of the key, return 0 if no fountd */
int sequence_search_opt(int* array, int length, int key)
{
assert(array != NULL && length >= 0);
array[0] = key;
int i = length;
while (array[i] != key) i--;
return i;
}
二分查找(折半搜索)(binary search / half-interval search)
Algorithm(https://en.wikipedia.org/wiki/Binary_search_algorithm)
The binary search algorithm begins by comparing the target value to value of the middle element of the sorted array. If the target value is equal to the middle element’s value, the position is returned. If the target value is smaller, the search continues on the lower half of the array, or if the target value is larger, the search continues on the upper half of the array. This process continues until the element is found and its position is returned, or there are no more elements left to search for in the array and a “not found” indicator is returned.思想:算法采用分治思想(divide and conquer algorithm),二分查找从表中间开始查找目标元素。如果找到一致元素,则查找成功。如果中间元素比目标元素小,则仍用二分查找方法查找表的后半部分(表是递增排列的),反之中间元素比目标元素大,则查找表的前半部分。
输入:查找表必须为有序状态(递增排列或递减排列),这里使用的表中数据是递增的,若含有多个相同的目标元素,则无法判断目标元素相对其它相同目标元素的位置。注意:
注意:查找表为数组形式(内存连续)的数据结构;如果是链表(内存不连续)的数据结构二分查找无法使用,即便采用二分查找的思想也达不到时间复杂度O(lgn),因为无法直接定位中间元素(要求O(1))。
时间复杂度:O(lgn)。最坏情况:查找不成功时间复杂度为O(lgn+1)=O(lgn);最好情况:待查找元素数据元素在第一个位置,时间复杂度O(1);平均情况:时间复杂度为O(lgn)。
空间复杂度:O(1)。
算法分析:适用于数据量较大、数据有序的情况。
算法拓展:二分搜索除了用于搜索与目标元素相等的位置外,可进行延伸,稍微修改算法用于搜索大于,大于或等于,小于,小于或等于目标元素的位置。
算法实现。注意:
输入:
这里假设数据在数组中是递增的,若是递减的话要修改算法,数据在数组中起始下标为0。返回值
这里数据在数组中的起始下标为0,若成功搜索目标元素,则返回该元素在数组中的下标,否则返回-1;如果要求数据数组起始下标为1,索引中首尾修改初始化和返回值为0即可。中点位置计算
1.对于unlimited numbers,不需考虑数据溢出,因此可以中点可以用(begin + end) / 2求得。2.对于limited numbers,在计算机中便是如此,需要考虑数据溢出(overflow),因此可以中点可以用begin + (end – begin) / 2求得。
3.求索引中点位置的计算在很多算法中都有用到,比如归并排序,都要考虑上面这个问题。
递归实现(Recursive)
优点:算法简洁,代码量少。
缺点:由于存在递归函数压栈、弹栈的开销,效率不如后一种迭代实现方法。
/**
* @brief search the value in the array of the index
*
* @param[in] array input array
* @param[in] index_begin begin index of input array
* @param[in] index_end end index of input array
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search(TYPE* array, TYPE index_begin, TYPE index_end, TYPE value)
{
assert(array != NULL && index_begin >= -1 && index_begin <= index_end + 1);
if (index_begin > index_end)
return -1;
TYPE middle = index_begin + ((unsigned)(index_end - index_begin) >> 1);
if (array[middle] < value)
return binary_search(array, middle + 1, index_end, value);
else if (array[middle] > value)
return binary_search(array, index_begin, middle - 1, value);
else
return middle;
}
迭代实现(Iterative)
/**
* @brief search the value in the array of the index by binary search method.
*
* @param[in] array input array
* @param[in] count array length
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search(TYPE* array, TYPE count, TYPE value)
{
assert(array != NULL && count >= 0);
TYPE middle;
TYPE index_begin = 0;
TYPE index_end = count - 1;
while (index_begin <= index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin) >> 1);
if (array[middle] == value)
return middle;
else if (array[middle] < value)
index_begin = middle + 1;
else
index_end = middle - 1;
}
return -1;
}
以上两种实现有如下特点
1.查找表中若含有多个相同的目标元素,无法判断目标元素相对其它相同目标元素的位置,即无法使返回值稳定返回排列好的表中第一个或最后一个目标元素的数组下标值。如果要弥补这个缺点:算法需要在搜索到目标元素后再次针对目标元素两端进行搜索,直到找到相同目标元素子序列两端并获得下标值
2.每次迭代或递归过程中比较有三个:第一个是小于,第二个是大于,第三个是等于(算法通过两个条件分支实现)。
>
算法优化:Deferred detection of equality
对比上面两个实现,这个算法有以下两个特点:
这个算法在每次迭代过程中仅有一个条件分支,因此若相同的目标元素比较少且数据量比较大的情况下,这个算法速度稍微高一些。
算法能够返回多个相同目标元素的最小的一个下标值。(稍微修改此算法(条件分支语句和求中点位置公式)可以返回多个相同目标元素的最大的一个数组下标值)
分析:这个实现是在维基百科里面看到的,它针对前两个实现的特点,通过减少分支使二分查找算法在某些数据(特征和规模)下获得更好的运算速度,还能正确寻找到输入具有多个相同目标元素下两端的下标值,不需添加额外算法。
注意:迭代里面,若算法返回多个相同目标元素的最小的一个下标值,求中点的下标值时必须保证:middle < index_end 恒成立,下面算法中求中点位置的公式满足这个要求。
算法实现:返回多个相同目标元素的最小的一个下标值。
/**
* @brief search the value in the array of the minimum index
*
* @param[in] array input array
* @param[in] index_begin begin index of input array
* @param[in] index_end end index of input array
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search(TYPE* array, TYPE index_begin, TYPE index_end, TYPE value)
{
assert(array != NULL);
TYPE middle;
while (index_begin < index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin) >> 1);
if (array[middle] < value)
index_begin = middle + 1;
else
index_end = middle;
}
if (index_end == index_begin && array[index_begin] == value)
return index_begin;
else
return -1;
}
算法实现:返回多个相同目标元素的最大的一个下标值。
/**
* @brief search the value in the array of the maximum index
*
* @param[in] array input array
* @param[in] index_begin begin index of input array
* @param[in] index_end end index of input array
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search(TYPE* array, TYPE index_begin, TYPE index_end, TYPE value)
{
assert(array != NULL);
TYPE middle;
while (index_begin < index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin + 1) >> 1);
if (array[middle] > value)
index_end = middle - 1;
else
index_begin = middle;
}
if (index_end == index_begin && array[index_begin] == value)
return index_begin;
else
return -1;
}
>
拓展
设输入数组为a[0],a[1],a[2] ……数组长度为n,目标元素为X。
下面利用二分搜索法拓展解决以下了4个问题,实现时基于上面的二分搜索实现的第三个实现方法:
(以下的组合可以还可以拓展到利用二分法搜索数组中给定任意范围的区域的两端数组下标值)
重点:
1. 条件分支语句的修改;
2. 数组是否越界判定(目标元素最小下标值为0时,无法找到小于它的数;目标元素最大下标值为数组长度减一时,无法找到大于它的数)
3. 返回索引值:正负1偏移(寻找大于或小于目标元素时的下标时)。
4. 迭代过程中要求:middle < index_end 或 middle > index_begin 恒成立,因此需要稍微修改下求中点坐标的算法实现。
- 寻找小于目标元素的最大数组下标i,a[i] < X
/**
* @brief get the largest index of the value in the array
* that smaller than the given value
*
* @param[in] array input array
* @param[in] count input array length
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search_smaller(TYPE* array, TYPE count, TYPE value)
{
assert(array != NULL && count >= 0);
TYPE middle, index_begin = 0, index_end = count - 1;
while (index_begin < index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin) >> 1);
if (array[middle] < value)
index_begin = middle + 1;
else
index_end = middle;
}
if (index_end == index_begin && array[index_begin] == value && index_begin)
return index_begin - 1;
else
return -1;
}
- 寻找小于等于目标元素的最大数组下标i,a[i] <= X
/**
* @brief get the largest index of the value in the array
* that smaller than or equal to the given value
*
* @param[in] array input array
* @param[in] count input array length
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search_smaller_equal(TYPE* array, TYPE count, TYPE value)
{
assert(array != NULL && count >= 0);
TYPE middle, index_begin = 0, index_end = count - 1;
while (index_begin < index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin + 1) >> 1);
if (array[middle] > value)
index_end = middle - 1;
else
index_begin = middle;
}
if (index_end == index_begin && array[index_begin] == value)
return index_begin;
else
return -1;
}
- 寻找大于目标元素的最小数组下标i,a[i] > X
/**
* @brief get the largest index of the value in the array
* that larger than the given value
*
* @param[in] array input array
* @param[in] count input array length
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search_larger(TYPE* array, TYPE count, TYPE value)
{
assert(array != NULL && count >= 0);
TYPE middle, index_begin = 0, index_end = count - 1;
while (index_begin < index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin + 1) >> 1);
if (array[middle] > value)
index_end = middle - 1;
else
index_begin = middle;
}
if (index_end == index_begin && array[index_begin] == value &&
index_begin != count - 1)
return index_begin + 1;
else
return -1;
}
- 寻找大于等于目标元素的最小数组下标i,a[i] >= X
/**
* @brief get the smallest index of the value in the array
* that larger than or equal to the given value
*
* @param[in] array input array
* @param[in] count input array length
* @param[in] value search value
*
* @warning array index begin from 0
*
* @return index if success, else return -1
*/
TYPE binary_search_larger_equal(TYPE* array, TYPE count, TYPE value)
{
assert(array != NULL && count >= 0);
TYPE middle, index_begin = 0, index_end = count - 1;
while (index_begin < index_end) {
middle = index_begin + ((unsigned)(index_end - index_begin) >> 1);
if (array[middle] < value)
index_begin = middle + 1;
else
index_end = middle;
}
if (index_end == index_begin && array[index_begin] == value)
return index_begin;
else
return -1;
}
>
插值查找(Interpolation search / extrapolation search)
思想:要查找的关键字key与查找表中的最大最小记录的关键字比较后的查找方法,核心在于插值公式。
时间复杂度O(lg(lg(n)))。:最坏情况:查找不成功,数据分布不均匀成指数递增,O(n);最好情况:待查找元素数据元素在第一个位置,时间复杂度O(1);平均情况:时间复杂度为O(lg(lg(n)))。
空间复杂度:O(1)。
与二分查找比较:对与表长比较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找好很多。
算法实现。注意:while循环条件判断中需考虑分母为零;插值公式中先乘后除防止丢失精度;退出循环后需处理分母为零情况。
/**
* @brief interpolation search algorithm
*
* @param[in] array array input
* @param[in] length array length
* @param[in] key key value
*
* @warning the index of the array begin from 0
*
* @return the index of the key, return -1 if no found
*/
int interpolation_search(int* array, int length, int key)
{
assert(array != NULL && length > 0);
int index_begin = 0;
int index_end = length - 1;
int index_mid;
while (array[index_end] != array[index_begin] &&
array[index_begin] <= key &&
array[index_end] >= key) {
index_mid = index_begin + (key - array[index_begin]) *
(index_end - index_begin) / (array[index_end] - array[index_begin]);
if (array[index_mid] == key)
return index_mid;
else if (array[index_mid] < key)
index_begin = index_mid + 1;
else
index_end = index_mid - 1;
}
if (array[index_begin] == key)
return index_begin;
else
return -1;
}
斐波那契查找/黄金分割查找(Fibonacci search)
思想:使用斐波那契数列实现黄金分割查找。
斐波那契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、····,在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。
斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为Fn,完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。时间复杂度O(lgn))。:最坏情况:待查找值位于数组第一个位置,此时效率比二分查找要差;最好情况:待查找元素数据元素在第一个位置,时间复杂度O(1);平均情况:时间复杂度为O(lgn))。
空间复杂度:O(1)。
算法实现:注意:斐波那契索引值计算;数据补充和溢出处理;
int F[10] = {0, 1, 1, 2, 3, 5, 8, 13, 21, 24};
/** * @brief fibonacci search algorithm * * @param[in] array array input * @param[in] length array length * @param[in] key key value * * @warning the index of the array begin from 0 * * @return the index of the key, return -1 if no found */
int fibonacci_search(int* array, int length, int key)
{
assert(array != NULL && length > 0);
int k = 0;
while (length > F[k] - 1)
k++; // get fibonacci index
for (int i = length; i < F[k] - 1; i++)
array[i] = array[length - 1]; // fix the value
int index_begin = 0;
int index_end = length - 1;
int index_mid;
while (index_begin <= index_end) {
index_mid = index_begin + F[k - 1] - 1;
if (array[index_mid] == key) {
if (index_mid <= length - 1)
return index_mid;
else
return -1; // exceed the range because of fixed value
} else if (array[index_mid] < key) {
index_begin = index_mid + 1;
k -= 2; // right half
} else {
index_end = index_mid - 1;
k -= 1; // left half
}
}
return -1;
}
有序表查找比较(二分查找、插值查找、斐波那契查找)
三种查找方法本质上是分隔点的选择不同,各有优劣,实际开发时可根据数据的特点综合考虑再做出选择。