返回分类:全部文章 >> 基础知识
返回上级:算法 - 查找与排序 (Searching and Sorting)
本文将用C++实现通用模板斐波那契查找算法,复制代码直接可使用。
在查看本文之前,需要一些程序语言的基础。
还需要熟悉 算法 - 查找 - 二分查找 (Binary Search) 。
斐波那契查找,是二分查找的一个变种。其时间复杂度 O(log2n) 。
斐波那契数列,又称黄金分割数列, F = { 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , . . . } F = \{ 1, 1, 2, 3, 5, 8, 13, 21, 34, ... \} F={1,1,2,3,5,8,13,21,34,...} 数学表达式:
F 0 = 1 F 1 = 1 F 2 = F 0 + F 1 ⋮ F i = F i − 2 + F i − 1 ( i ≥ 2 ) \begin{aligned} F_0 &= 1 \\ F_1 &= 1 \\ F_2 &= F_0 + F_1 \\ & \vdots \\ F_i &= F_{i-2} + F_{i-1} \quad (i \geq 2) \end{aligned} F0F1F2Fi=1=1=F0+F1⋮=Fi−2+Fi−1(i≥2)
之所以它又称为黄金分割数列,是因为它的前一项与后一项的比值随着数字数量的增多逐渐逼近黄金分割比值0.618。
所以斐波那契查找改变了二分查找中原有的中值 mid 的求解方式,其 mid 不再代表中值,而是代表了黄金分割点:
m i d = l e f t + F b l o c k − 1 − 1 mid = left + F_{block - 1} - 1 mid=left+Fblock−1−1
二分查找,以一半元素来分割数组;
斐波那契查找,以黄金分割点来分割数组。
假设表中有 n 个元素,查找过程为取区间中间元素的下标 mid ,对 mid 的关键字与给定值的关键字比较:
(1)如果与给定关键字相同,则查找成功,返回在表中的位置;
(2)如果给定关键字大,向右查找并减小2个斐波那契区间;
(3)如果给定关键字小,向左查找并减小1个斐波那契区间;
(4)重复过程,直到找到关键字(成功)或区间为空集(失败)。
通常情况下:
返回值,代表下标;
返回-1,代表没有找到关键字;
举例,假设有集合 e0=2, e1=3, e2=7, e3=14, e4=22, e5=33, e6=55, e7=75, e8=89, e9=123 。
查找元素44
的过程:
已知:
size = 10
;left = 0
、right = 9
。则初始化的斐波那契数列为 F i b = { 1 , 1 , 2 , 3 , 5 , 8 , 13 } Fib = \{ 1, 1, 2, 3, 5, 8, 13 \} Fib={1,1,2,3,5,8,13} ,即斐波那契数列最后一位要比size - 1
大。
此时查找第一步为:此时刚初始化完,斐波那契数列最后一个区间为第6个区间,即区间(8, 13);第8个数字为当前黄金分割点,则第8个数字下标为下标7
,检测的数字为 e7 ;以8分割数组后,左区间为(1, 8)。
b l o c k 0 = 6 m i d 0 = l e f t 0 + F i b [ b l o c k 0 − 1 ] − 1 = 0 + 8 − 1 = 7 i n d e x 0 = min ( m i d 0 , s i z e − 1 ) = 7 ∵ ( e = 44 ) < ( E [ i n d e x 0 ] = 75 ) (小于,去左区间) ∴ l e f t 1 = l e f t 0 = 0 r i g h t 1 = m i d 0 − 1 = 6 b l o c k 1 = b l o c k 0 − 1 = 5 \begin{array}{rl} block_0 &= 6 \\ mid_0 &= left_0 + Fib[block_0 - 1] - 1 \\ &= 0 + 8 - 1 \\ &= 7 \\ index_0 &= \min(mid_0, size - 1) \\ &= 7 \\ \because & (e=44) < (E[index_0]=75) \quad \text{(小于,去左区间)} \\ \therefore & left_1 = left_0 = 0 \\ & right_1 = mid_0 - 1 = 6 \\ & block_1 = block_0 - 1 = 5 \end{array} block0mid0index0∵∴=6=left0+Fib[block0−1]−1=0+8−1=7=min(mid0,size−1)=7(e=44)<(E[index0]=75)(小于,去左区间)left1=left0=0right1=mid0−1=6block1=block0−1=5
此时查找第二步为:同第一步,黄金分割点为第5个数字下标为4
。
m i d 1 = l e f t 1 + F i b [ b l o c k 1 − 1 ] − 1 = 0 + 5 − 1 = 4 i n d e x 1 = min ( m i d 1 , s i z e − 1 ) = 4 ∵ ( e = 44 ) > ( E [ i n d e x 1 ] = 22 ) (大于,去右区间) ∴ l e f t 2 = m i d 1 + 1 = 5 r i g h t 2 = r i g h t 1 = 6 b l o c k 2 = b l o c k 1 − 2 = 3 (注意:区间是从left开始算,不是从0) \begin{array}{rl} mid_1 &= left_1 + Fib[block_1 - 1] - 1 \\ &= 0 + 5 - 1 \\ &= 4 \\ index_1 &= \min(mid_1, size - 1) \\ &= 4 \\ \because & (e=44) > (E[index_1]=22) \quad \text{(大于,去右区间)}\\ \therefore & left_2 = mid_1 + 1 = 5 \\ & right_2 = right_1 = 6 \\ & block_2 = block_1 - 2 = 3 \\ & \text{(注意:区间是从left开始算,不是从0)} \end{array} mid1index1∵∴=left1+Fib[block1−1]−1=0+5−1=4=min(mid1,size−1)=4(e=44)>(E[index1]=22)(大于,去右区间)left2=mid1+1=5right2=right1=6block2=block1−2=3(注意:区间是从left开始算,不是从0)
上一步结束,还剩 { 33 55 } ,此时查找第三步为:黄金分割点为第2个数字55
,它的下标为6
。
m i d 2 = l e f t 2 + F i b [ b l o c k 2 − 1 ] − 1 = 5 + 2 − 1 = 6 i n d e x 2 = min ( m i d 2 , s i z e − 1 ) = 6 ∵ ( e = 44 ) < ( E [ i n d e x 2 ] = 55 ) (小于,去左区间) ∴ l e f t 3 = l e f t 2 = 5 r i g h t 3 = m i d 2 − 1 = 5 b l o c k 3 = b l o c k 2 − 1 = 2 \begin{array}{rl} mid_2 &= left_2 + Fib[block_2 - 1] - 1 \\ &= 5 + 2 - 1 \\ &= 6 \\ index_2 &= \min(mid_2, size - 1) \\ &= 6 \\ \because & (e=44) < (E[index_2]=55) \quad \text{(小于,去左区间)} \\ \therefore & left_3 = left_2 = 5 \\ & right_3 = mid_2 - 1 = 5 \\ & block_3 = block_2 - 1 = 2 \end{array} mid2index2∵∴=left2+Fib[block2−1]−1=5+2−1=6=min(mid2,size−1)=6(e=44)<(E[index2]=55)(小于,去左区间)left3=left2=5right3=mid2−1=5block3=block2−1=2
上一步结束,还剩 { 33 } ,此时查找第四步为:黄金分割点为第1个数字33
,它的下标为5
。
m i d 3 = l e f t 3 + F i b [ b l o c k 3 − 1 ] − 1 = 5 + 1 − 1 = 5 i n d e x 3 = min ( m i d 3 , s i z e − 1 ) = 5 ∵ ( e = 44 ) > ( E [ i n d e x 3 ] = 33 ) (大于,去右区间) ∴ l e f t 4 = m i d 3 + 1 = 6 r i g h t 4 = r i g h t 3 = 5 b l o c k 4 = b l o c k 3 − 2 = 0 ∵ l e f t 4 > r i g h t 4 ∴ b r e a k (退出循环,查找失败) \begin{array}{rl} mid_3 &= left_3 + Fib[block_3 - 1] - 1 \\ &= 5 + 1 - 1 \\ &= 5 \\ index_3 &= \min(mid_3, size - 1) \\ &= 5 \\ \because & (e=44) > (E[index_3]=33) \quad \text{(大于,去右区间)} \\ \therefore & left_4 = mid_3 + 1 = 6 \\ & right_4 = right_3 = 5 \\ & block_4 = block_3 - 2 = 0 \\ \because & left_4 > right_4 \\ \therefore & break \quad \text{(退出循环,查找失败)} \end{array} mid3index3∵∴∵∴=left3+Fib[block3−1]−1=5+1−1=5=min(mid3,size−1)=5(e=44)>(E[index3]=33)(大于,去右区间)left4=mid3+1=6right4=right3=5block4=block3−2=0left4>right4break(退出循环,查找失败)
之后的程序,我们以数组列表形式描述。
注意:代码全部使用std::vector
作为数组列表,如果你用指针数组MyType*
,还需传入数组大小size
。
一般举例中,查找最基本的元素本身就是整型。
基本思路,循环整张表即可。
// Author: https://blog.csdn.net/DarkRabbit
// Fibonacci Search
// 整型有序表 - 斐波那契查找
// params:
// list: 查找的有序表
// element: 查找的元素
// return:
// int: 找到的数字
template<typename T>
int FibonacciSearch(const std::vector<T>& list,
const T& element)
{
if (pEqual == nullptr || list.empty())
{
return -1;
}
int size = list.size(); // 列表长度
std::vector<int> fib(2, 1); // 斐波那契数列
int fibBlock = 1;
while (size > fib[fibBlock] - 1) // 一边构造数列,一边计算区间的位置
{
fibBlock++;
if (fibBlock == fib.size())
{
fib.push_back(fib[fibBlock - 1] + fib[fibBlock - 2]);
}
}
// 二分过程
int left = 0;
int right = size - 1;
int mid;
int index;
while (left <= right)
{
mid = left + fib[fibBlock - 1] - 1;
index = mid >= size ? size - 1 : mid;
if (element > list[index])
{
left = mid + 1;
fibBlock -= 2;
}
else if (element < list[index])
{
right = mid - 1;
fibBlock -= 1;
}
else
{
return index;
}
}
return -1;
}
在实际应用中,通常情况下,列表存储的都是一些数据(结构体或类),它们都包含唯一标识(即关键字Key)。
我们一般不会将它们的关键字重新建立一个列表,再去查找。
这在C++中通常用模板 (template) 来解决,其它语言多数用泛型 (Genericity) 来解决。
我们的要求仅仅是用结构中的关键字进行比较,即我们只关心关键字而不关心这个数据的类型。这样使用自定义类型也不怕了,所以可以使用一个函数指针传入比较方法,在函数中自定义比较。
我们规定此函数指针的结果:
返回值大于0,则给定关键字大,向右查找并减小2个斐波那契区间;
返回值等于0,成功找到,则返回结构;
返回值小于0,则给定关键字小,向左查找并减小1个斐波那契区间;
我们接下来改造成模板函数:
// Author: https://blog.csdn.net/DarkRabbit
// Fibonacci Search
// 模板有序表 - 斐波那契查找
// params:
// list: 查找的有序表
// element: 查找的元素
// pEqual: 判断查找标准,
// >0 查找的值比斐波那契值大
// =0 找到了
// <0 查找的值比斐波那契值小
// return:
// int: 找到的数字
template<typename T>
int FibonacciSearch(const std::vector<T>& list,
const T& element,
int (*pEqual)(const T&, const T&))
{
if (pEqual == nullptr || list.empty())
{
return -1;
}
int size = list.size(); // 列表长度
std::vector<int> fib(2, 1); // 斐波那契数列
int fibBlock = 1;
while (size > fib[fibBlock] - 1) // 一边构造数列,一边计算列表在数列的位置
{
fibBlock++;
if (fibBlock == fib.size())
{
fib.push_back(fib[fibBlock - 1] + fib[fibBlock - 2]);
}
}
// 二分过程
int left = 0;
int right = size - 1;
int isEqual;
int mid;
int index;
while (left <= right)
{
mid = left + fib[fibBlock - 1] - 1;
index = mid >= size ? size - 1 : mid;
isEqual = (*pEqual)(element, list[index]);
if (isEqual > 0)
{
left = mid + 1;
fibBlock -= 2;
}
else if (isEqual < 0)
{
right = mid - 1;
fibBlock -= 1;
}
else
{
return index;
}
}
return -1;
}
有了模板函数后,我们之前的整型函数可以进行修改,直接调用模板函数即可。
我们在这里直接传入 Lambda 表达式:
// Author: https://blog.csdn.net/DarkRabbit
// Fibonacci Search
// 整型有序表 - 斐波那契查找
// params:
// list: 查找的有序表
// element: 查找的元素
// return:
// int: 找到的数字
int FibonacciSearch(const std::vector<int>& list,
const int& element)
{
return FibonacciSearch<int>(list, element,
[](const int& x, const int& y)->int
{
return x - y;
});
}
类似的,自定义类型调用:
// Author: https://blog.csdn.net/DarkRabbit
// Fibonacci Search
#include
#include
#include
using namespace std;
struct MyElement
{
int key;
string data;
};
int MyFibonacciCompare(const MyElement& x,
const MyElement& y)
{
return x.key - y.key;
}
int main()
{
vector<MyElement> list; // 列表
MyElement tofind; // 需要查找的元素
// TODO 省略初始化列表和元素的过程
int index = FibonacciSearch<MyElement>(list, tofind,
[](const MyElement& x, const MyElement& y)->int
{
return x.key - y.key;
});
// 以上调用方法等同于
// int index = FibonacciSearch(list,
// tofind,
// MyFibonacciCompare);
if (index != -1)
{
// do something
cout << "找到了下标:" << index << endl;
}
return 0;
}