查找定义:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
查找算法分类:
1)静态查找和动态查找;
注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
2)无序查找和有序查找。
无序查找:被查找数列有序无序均可; 有序查找:被查找数列必须为有序数列。
平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
Pi:查找表中第i个数据元素的概率。
Ci:找到第i个数据元素时已经比较过的次数。
顺序查找
说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。
基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
复杂度分析: 查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
当查找不成功时,需要n次比较,时间复杂度为O(n);
所以,顺序查找的时间复杂度为O(n)。
优缺点分析:缺点是平均查找长度较大,当n很大时,查找效率较低。优点是:算法简单且适应面广。对表的结构无任何要求,无论记录是否按关键字有序均可应用。
实现源码:
//顺序查找
int SequenceSearch(int[] a, int value, int n)
{
int i;
for (i = 0; i < n; i++)
{
if (a[i] == value)
{
return i;
}
}
return -1;
}
二分查找(折半查找)
说明:元素必须是有序的,如果是无序的则要先进行排序操作。
基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
复杂度分析:最坏情况下,关键词比较次数为,且期望时间复杂度为O(log2(n));
折半查找是一棵二叉排序树,每个根结点的值都大于左子树的所有结点的值,小于右子树所有结点的值。
例如:长度为10的有序表的平均查找长度为:ASL=(1*1+2*2+3*4+4*3)/10=29/10=2.9;
注:折半查找的前提条件是需要有序表顺序存储(即顺序表),对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。
优缺点分析:缺点是只适用于有序表,且限于顺序存储结构。优点是:效率更高。
实现源码:
//二分查找(折半查找),循环实现
int BinarySearch1(int[] a, int value, int n)
{
int low, high, mid;
low = 0;
high = n - 1;
while (low <= high)
{
mid = (low + high) / 2;
if (a[mid] == value)
return mid;
else if (a[mid] > value)
high = mid - 1;
else if (a[mid] < value)
low = mid + 1;
}
return -1;
}
//二分查找,递归实现
int BinarySearch2(int[] a, int value, int low, int high)
{
if (low <= high)
{
int mid = low + (high - low) / 2;
if (a[mid] == value)
return mid;
if (a[mid] > value)
return BinarySearch2(a, value, low, mid - 1);
if (a[mid] < value)
return BinarySearch2(a, value, mid + 1, high);
}
return -1;
}
插值查找
在介绍插值查找之前,首先考虑一个新问题,为什么上述算法一定要是折半,而不是折四分之一或者折更多呢?打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。
同样的,比如要在取值范围1 ~ 10000 之间 100 个元素从小到大均匀分布的数组中查找5, 我们自然会考虑从数组下标较小的开始查找。
经过以上分析,折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:
mid=(low+high)/2, 即mid=low+1/2*(high-low);
通过类比,我们可以将查找的点改进为如下:
mid=low+(key-a[low])/(a[high]-a[low])*(high-low)
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
复杂度分析:查找成功或者失败的时间复杂度均为O(log2(log2(n))) 。
优缺点分析:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
实现源码:
//插值查找
int InsertionSearch(int[] a, int value, int low, int high)
{
if(low <= high)
{
int mid = low+(value-a[low])/(a[high]-a[low])*(high-low);
if(a[mid]==value)
return mid;
if(a[mid]>value)
return InsertionSearch(a, value, low, mid-1);
if(a[mid]<value)
return InsertionSearch(a, value, mid+1, high);
}
return -1;
}
斐波那契查找
基本思想:也是二分查找的一种提升算法,通过运用黄金比例(0.618比)的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。
方法:斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,即n=F(k)-1;
开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种:
1)相等,mid位置的元素即为所求;
2)>,low=mid+1,k -= 2;
说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= Fk-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。
3)<,high=mid-1,k-=1。
说明:low=mid-1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归 的应用斐波那契查找。
复杂度分析:最坏情况下,时间复杂度为 O(log2(n)) ,且其期望复杂度也为 O(log2(n)) 。
原理:斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。原查找列表长度为A,那么我们在Fibo数列中找到一个等于或者刚好大于A的数F[n](这里F[n] >= A), 然后将原来查找表长度扩展为 F[n](以原列表最后一个值进行扩充,最大值扩充). 再对这个新表进行Fibo分割。F[n] = F[n-1] + F[n-2],所以一个长度为F[n] 的查找表分为两个新的查找表,长度分别为F[n-1] 和 F[n-2]。找到要查找的元素在哪一部分,递归。
比如:arr={1,2,3,4,5,6,7,8,9,10,11,12}要对他进行斐波那契查找,查找的值是10。
该数组长度为12, Fibo = {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}, 所以选取13作为新数组长度,这么一来新数组:{1,2,3,4,5,6,7,8,9,10,11,12,12}
F[n] = F[n-1] + F[n-2], F[n] = 13, 所以F[n-1] = 8, F[n-2] = 5. 那么对应的位置是F[6] = F[5] + F[4], 中间值为 F[5] = 8.
找到中间值了,接下来就是进行比较与递归。
优缺点分析:与折半查找相比,斐波那契查找的优点是它只涉及加法和减法运算,而不用除法,而除法比加减法要占用更多的时间,因此,斐波那契查找的运行时间理论上比折半查找小。缺点是引入了斐波拉契数列需要存储的额外空间,牺牲空间换取时间。
实现源码:
/// <summary>
/// 生成斐波那契数列
/// </summary>
/// <param name="fib">指向存储斐波那契数列的数组</param>
/// <param name="size">斐波那契数列长度</param>
void ProduceFib(int[] fib, int size)
{
int i;
fib[0] = 1;
fib[1] = 1;
for (i = 2; i < size; i++)
{
fib[i] = fib[i - 1] + fib[i - 2];
}
}
private const int Maxsize = 20; //斐波那契数组的长度
/// <summary>
/// 斐波那契查找,查找成功返回位序,否则返回-1
/// </summary>
/// <param name="data">有序表数组</param>
/// <param name="searchValue">待查找关键字</param>
/// <returns></returns>
int FibonacciSearch(int[] data, int searchValue)
{
int low = 0;
int length = data.Length;
int high = length - 1;
int mid, k, i;
int[] fib = new int[Maxsize];
ProduceFib(fib, Maxsize);
k = 0;
// 找到有序表元素个数在斐波那契数列中最接近的最大数列值
while (high > fib[k] - 1)
{
k++;
}
// 补齐有序表
for (i = length; i <= fib[k] - 1; i++)
{
data[i] = data[high];
}
while (low <= high)
{
mid = low + fib[k - 1] - 1; // 根据斐波那契数列进行黄金分割
if (data[mid] == searchValue)
{
if (mid <= length - 1)
{
return mid;
}
else
{
// 说明查找得到的数据元素是补全值
return length - 1;
}
}
if (data[mid] > searchValue)
{
high = mid - 1;
k -= 1;
}
if (data[mid] < searchValue)
{
low = mid + 1;
k -= 2;
}
}
return -1;
}
分块查找
要求是顺序表,分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
算法思想:将n个数据元素”按块有序”划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须”按块有序”;即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
算法流程:
1、先选取各块中的最大关键字构成一个索引表;
2、查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;
3、在已确定的块中用顺序法进行查找。
时间复杂度:O(log(m)+N/m)
public struct IndexBlock
{
public int Max;
public int Start;
public int End;
};
private const int BlockCount = 3;
private int BlockSearch()
{
int j = -1;
int k = 0;
int[] a = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
IndexBlock[] indexBlock = new IndexBlock[BlockCount];
for (int i = 0; i < BlockCount; i++)
{
indexBlock[i].Start = j + 1; //确定每个块范围的起始值
j = j + 1;
indexBlock[i].End = j + 4; //确定每个块范围的结束值
j = j + 4;
indexBlock[i].Max = a[j]; //确定每个块范围中元素的最大值
}
k = InternalBlockSearch(12, a, indexBlock);
return k;
}
/// <summary>
/// 分块查找
/// 分块查找要求把一个数据分为若干块,每一块里面的元素可以是无序的,但是块与块之间的元素需要是有序的。
/// (对于一个非递减的数列来说,第i块中的每个元素一定比第i-1块中的任意元素大)
/// </summary>
private static int InternalBlockSearch(int target, int[] a, IndexBlock[] indexBlock)
{
int i = 0;
while (i < BlockCount && target > indexBlock[i].Max)
{
//确定在哪个块中
i++;
}
if (i >= BlockCount)
{
//大于分的块数,则返回-1,找不到该数
return -1;
}
//j等于块范围的起始值
var index = indexBlock[i].Start;
while (index <= indexBlock[i].End && a[index] != target)
{
//在确定的块内进行查找
index++;
}
if (index > indexBlock[i].End)
{
//如果大于块范围的结束值,则说明没有要查找的数,j置为-1
index = -1;
}
return index;
}