本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1 概述
查找算法(Searching Algorithms) 一般用来从存储元素的数据结构中检索出所需要查找的元素。根据查找操作的类型,这些算法一般分为两类:
- 顺序查找(Sequential Search) 。其操作过程是,按顺序遍历列表或数组。例如:线性查找(Linear Search)。
- 间隔查找(Interval Search) 。这些算法专门设计用于在有序的数据结构中进行查找。这些类型的查找算法比线性查找更有效,因为它们反复定位搜索结构的中心并将搜索空间分成两半。例如:二分查找(Binary Search)。
2 查找算法汇总
2.1 线性查找(Linear Search)
先看一个简单问题,给定一个由n
个元素组成的数组arr[]
,编写一个函数来查找arr[]
中的给定元素x
,如下:
Input : arr[] = {10, 20, 80, 30, 60, 50,
110, 100, 130, 170}
x = 110;
Output : 6
Element x is present at index 6
Input : arr[] = {10, 20, 80, 30, 60, 50,
110, 100, 130, 170}
x = 175;
Output : -1
Element x is not present in arr[].
可以看出,典型的线性查找过程如下,即:
- 从
arr[]
的最左边的元素开始,逐个比较x
和arr[]
中的每个元素; - 如果
x
与元素匹配,则返回索引值; - 如果
x
与任何元素都不匹配,则返回-1
。
线性查找的时间复杂度为。如下linear_search
,用到了一个for
循环。为了克服最坏情况下的时间复杂度,提供了一个改进型的实现方式linear_search_ex
,其在一次循环中做两次if
判断,但其时间复杂度还是为。
Code
/********************************************************
* Function name :linear_search
* Description : linear search
* Parameter :
* @arr the input array
* @n the length of the input array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int linear_search(int arr[], int n, int x)
{
int i;
for (i = 0; i < n; i++)
if (arr[i] == x)
return i;
return -1;
}
/********************************************************
* Function name :linear_search_ex
* Description : linear search
* Parameter :
* @arr the input array
* @n the length of the input array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int linear_search_ex(int arr[], int n, int x)
{
int left;
int right = n - 1;
// Run loop from 0 to right
for (left = 0; left <= right;)
{
// If search_element is found with
// left variable
if (arr[left] == x)
{
return left;
}
// If search_element is found with
// right variable
if (arr[right] == x)
{
return right;
}
left++;
right--;
}
return -1;
}
2.2 二分查找(Binary Search)
二分查找是一种在有序数组(升序,或降序)中使用的查找算法,其在操作过程中将查找区间不断减半,二分查找的思想是使用数组排序的信息,将时间复杂度降低到。
二分查找的基本步骤如下:
- 从整个数组的中间元素索引(即数组下标)作为查找的起始值;
- 如果中间元素索引所对应的元素等于待查找的值,则返回中间元素索引;
- 或者,如果中间元素索引所对应的元素小于待查找的值,就将间隔缩小到下半部分;
- 否则,将其缩小到上半部分;
- 从第2点重复运行,直到找到相等的值返回元素索引,或返回-1(未找到,查找失败)。
二分查找利用了排序的信息,将时间复杂度降低到。如下分别用迭代和递归实现了二分查找算法。在计算中间索引时,使用了,而不是更简单的。这个是为了防止和过大时,两者之和会越过,产生bug。
Code
/********************************************************
* Function name :binary_search
* Description : binary search
* Parameter :
* @arr the input array
* @l the start index of the array
* @r the end index of the array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int binary_search(int arr[], int l, int r, int x)
{
while (l <= r) {
int m = l + (r - l) / 2;
// Check if x is present at mid
if (arr[m] == x)
return m;
// If x greater, ignore left half
if (arr[m] < x)
l = m + 1;
// If x is smaller, ignore right half
else
r = m - 1;
}
// if we reach here, then element was
// not present
return -1;
}
/********************************************************
* Function name binary_search_recursive
* Description : binary search
* Parameter :
* @arr the input array
* @l the start index of the array
* @r the end index of the array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int binary_search_recursive(int arr[], int l, int r, int x)
{
if (r >= l) {
int mid = l + (r - l) / 2;
// If the element is present at the middle
// itself
if (arr[mid] == x)
return mid;
// If element is smaller than mid, then
// it can only be present in left subarray
if (arr[mid] > x)
return binary_search_recursive(arr, l, mid - 1, x);
// Else the element can only be present
// in right subarray
return binary_search_recursive(arr, mid + 1, r, x);
}
// We reach here when element is not
// present in array
return -1;
}
2.3 跳跃查找算法(Jump Search)
跳跃查找算法(Jump Search) 与二分查找一样,跳跃查找算法也是利用了有序数组特性(升序,或降序)的查找算法。基本思想是通过按固定步长跳跃过某些数组元素,以达到减少查找次数,提高查找效率的目的(和线性查找相比)。例如,假设我们有一个长度为的数组和固定跳跃步长。然后,我们在、、 等处进行查找。一旦我们找到区间,我们就会从索引执行线性查找运算以找到元素。
考虑以下数组:(0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610)
。数组的长度是16
。查找的目标值为55
,假设跳跃的步长为m = 4
。跳跃查找算法的大致步骤如下。
- 从索引
0
跳跃到索引4
; - 从索引
4
跳跃到索引8
; - 由于索引
12
(元素为144
)处的元素大于55
,其将后退一步来到索引8
。 - 从索引
8
执行线性查找以获取元素55
。
那跳跃的最佳步长 是多少呢?
在最坏的情况下,跳跃查找算法必须进行次跳跃,如果最后查找出来的值大于要搜索的元素,将会最多执行次线性查找。因此,最坏情况下的比较总数将是。当时,函数的值最小。 因此,最佳步长为。
跳跃查找算法的时间复杂度为,比线性查找好,但比二分查找差。其代码实现如下:
Code
#include<math.h>
int min(int a, int b){
if(b>a)
return a;
else
return b;
}
/********************************************************
* Function name jump_search
* Description : jump search
* Parameter :
* @arr the input array
* @n the length of the array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int jump_search(int arr[], int n, int x)
{
// Finding block size to be jumped
int step = sqrt(n);
// Finding the block where element is
// present (if it is present)
int prev = 0;
while (arr[min(step, n) - 1] < x)
{
prev = step;
step += sqrt(n);
if (prev >= n)
return -1;
}
// Doing a linear search for x in block
// beginning with prev.
while (arr[prev] < x)
{
prev++;
// If we reached next block or end of
// array, element is not present.
if (prev == min(step, n))
return -1;
}
// If element is found
if (arr[prev] == x)
return prev;
return -1;
}
2.4 插值查找算法(Interpolation Search)
插值查找算法是在二分查找算法的基础上改进得到的一种查找算法。其唯一的区别在于,二分查找总是以所选区域的中间元素作为比较对象;而插值查找算法,采用一个特别的公式来计算出当前区域的比较对象。插值查找算法也是要求所查找数组为有序数组。在有序数组元素呈现均匀分布时,该算法效率比二分查找效率更高;当数组元素不是均匀分布时,其效率不如二分查找。均匀分布很好理解,如数组(0,1,2,3,4,5,6)
就是均匀分布的,其每个元素大小基本呈线性增加。而数组(0,1,10,100,1000,10000)
就不是均匀分布的,前后元素之间相差一个数量级。
前面提到的特别公式也很简单,如下:
其中:
- ,为待查找的有序数组;
- ,待查找的目标元素;
- ,在查找数组中的开始索引;
- ,在查找数组中的结束索引;
- ,当前区域的比较索引(在二分查找中,此值为区域中心索引)。
可以看出,该公式想要表达的意图是,目标元素的值,越靠近哪一端,比较索引就往哪一端偏移。
插值查找算法实现步骤如下:
- 在一个循环中,用上述公式,计算出值;
- 如果和目标值匹配,则返回当前的索引,退出;
- 若目标值小于,计算左子数组的值、否则计算在右子数组的值;
- 重复直到查找到匹配值或子数组减少到零。
插值查找的时间复杂度也是,但对于元素呈均匀分布的查找来说,插值查找的平均性能比二分查找要好得多。下面是算法的实现。
Code
/********************************************************
* Function name interpolation_search
* Description : interpolation search
* Parameter :
* @arr the input array
* @lo the start index of the array
* @hi the end index of the array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int interpolation_search(int arr[], int lo, int hi, int x)
{
int pos;
// Since array is sorted, an element present
// in array must be in range defined by corner
if (lo <= hi && x >= arr[lo] && x <= arr[hi]) {
// Probing the position with keeping
// uniform distribution in mind.
pos = lo
+ (((double)(hi - lo) / (arr[hi] - arr[lo]))
* (x - arr[lo]));
// Condition of target found
if (arr[pos] == x)
return pos;
// If x is larger, x is in right sub array
if (arr[pos] < x)
return interpolation_search(arr, pos + 1, hi, x);
// If x is smaller, x is in left sub array
if (arr[pos] > x)
return interpolation_search(arr, lo, pos - 1, x);
}
return -1;
}
2.5 指数查找算法(Exponential Search)
指数查找算法也是在二分查找的基础上改进的一种算法。不同于二分查找是用减半的形式缩小查找空间,指数查找是以指数速度缩小查找空间。其算法的实现过程为,有一数组,将索引位置为、、、、、处的元素和目标元素进行比较。如果目标元素一直大于或等于上述索引位置的元素,则索引值以指数速度增长,并继续比较下去;如果发现目标元素,比索引位置(必定是的倍数)处的元素小,那值肯定在索引位置区间中。对区间应用二分查找,即可以查找处目标值。
指数查找算法实现一般总结为如下两步:
- 以指数速度,查找出目标元素所在的区间;
- 对步骤1中的区间采用二分查找算法。
指数查找算法的时间复杂度为。对于查找比较稀疏的数组,指数查找算法要优于二分查找算法。指数查找划分的区间更少,查找目标值所在的范围更快。下面是算法的实现。
Code
/********************************************************
* Function name exponential_search
* Description : exponential search
* Parameter :
* @arr the input array
* @n the length of the array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int exponential_search(int arr[], int n, int x)
{
// If x is present at first location itself
if (arr[0] == x)
return 0;
// Find range for binary search by
// repeated doubling
int i = 1;
while (i < n && arr[i] <= x)
i = i * 2;
// Call binary search for the found range.
return binary_search(arr, i / 2,
min(i, n - 1), x);
}
// A recursive binary search function. It returns
// location of x in given array arr[l..r] is
// present, otherwise -1
int binary_search(int arr[], int l, int r, int x)
{
if (r >= l)
{
int mid = l + (r - l) / 2;
// If the element is present at the middle
// itself
if (arr[mid] == x)
return mid;
// If element is smaller than mid, then it
// can only be present n left subarray
if (arr[mid] > x)
return binary_search(arr, l, mid - 1, x);
// Else the element can only be present
// in right subarray
return binary_search(arr, mid + 1, r, x);
}
// We reach here when element is not present
// in array
return -1;
}
2.6 斐波那契查找算法(Fibonacci Search)
斐波那契查找算法和上面介绍的几种算法也是类似的,只不过该算法采用黄金分割来确定查找空间。先来介绍一下斐波那契数列,其数学递归定义如:其中该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618
)。例如数列:0、1、1、2、3、5、8、13、21、34、55、89、144····
就是斐波那契数列的一段。
斐波那契查找算法的过程首先是在斐波那契数列中找一个不小于待查找数组长度的(第个斐波那契数)。由斐波那契数列定义可知,其前面的两个斐波那契数为和。用值和可以将数组分割成两个查找区间,对应的索引范围分别为和,索引区间可能比数组实际长度要长,因为数组长度不可能恰好等于一个斐波那契数。将目标元素和索引范围的最后一个元素(即)进行比较。如果相等,侧查找成功,返回对应索引值;如果小于该元素,则必定在区间内,对该区间应用上述相同的分割方法,并丢弃掉数组剩下的索引区间;如果大于该元素,则必定在区间内,对该区间应用上述相同的分割方法,并丢弃掉数组剩下的索引区间。
斐波那契查找算法的时间复杂度为。时间复杂度和二分查找一样,但是从算法实现中可以看出,斐波那契查找算法只有加减运算,没有除法运算,这也是斐波那契查找的优势。下面是算法的实现。
Code
// Utility function to find minimum of two elements
int min(int x, int y) { return (x <= y) ? x : y; }
/********************************************************
* Function name fibonacci_search
* Description : fibonacci search
* Parameter :
* @arr the input array
* @n the length of the array
* @x search element
* Return :-1 --no found , other -- the index of the element
**********************************************************/
int fibonacci_search(int arr[], int n, int x)
{
/* Initialize fibonacci numbers */
int fibMMm2 = 0; // (m-2)'th Fibonacci No.
int fibMMm1 = 1; // (m-1)'th Fibonacci No.
int fibM = fibMMm2 + fibMMm1; // m'th Fibonacci
/* fibM is going to store the smallest Fibonacci
Number greater than or equal to n */
while (fibM < n) {
fibMMm2 = fibMMm1;
fibMMm1 = fibM;
fibM = fibMMm2 + fibMMm1;
}
// Marks the eliminated range from front
int offset = -1;
/* while there are elements to be inspected. Note that
we compare arr[fibMm2] with x. When fibM becomes 1,
fibMm2 becomes 0 */
while (fibM > 1) {
// Check if fibMm2 is a valid location
int i = min(offset + fibMMm2, n - 1);
/* If x is greater than the value at index fibMm2,
cut the subarray array from offset to i */
if (arr[i] < x) {
fibM = fibMMm1;
fibMMm1 = fibMMm2;
fibMMm2 = fibM - fibMMm1;
offset = i;
}
/* If x is greater than the value at index fibMm2,
cut the subarray after i+1 */
else if (arr[i] > x) {
fibM = fibMMm2;
fibMMm1 = fibMMm1 - fibMMm2;
fibMMm2 = fibM - fibMMm1;
}
/* element found. return index */
else
return i;
}
/* comparing the last element with x */
if (fibMMm1 && arr[offset + 1] == x)
return offset + 1;
/*element not found. return -1 */
return -1;
}
3 总结
- 查找算法一般分为两类:顺序查找(Sequential Search)和间隔查找(Interval Search);
- 间隔查找用在有序的数据结构中,其包括二分查找、跳跃查找、插值查找、指数查找、斐波那契查找;
- 跳跃查找、插值查找、指数查找、斐波那契查找都可以看作是二分查找的改进算法;
- 对于数据结构中元素呈均匀分布的查找来说,插值查找的平均性能比二分查找要好得多;
- 对于数据结构中元素分布比较稀疏的场合,指数查找要优于二分查找算法;
- 斐波那契查找算法只有加减运算,没有除法运算,这是斐波那契查找相对于二分查找的优势。