查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。
查找定义:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。
查找算法分类:
1)静态查找和动态查找;
注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
2)无序查找和有序查找。
无序查找:被查找数列有序无序均可;
有序查找:被查找数列必须为有序数列。
平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
Pi:查找表中第i个数据元素的概率。
Ci:找到第i个数据元素时已经比较过的次数。
本文主要介绍:
1、二分查找
2、顺序查找
3、插值查找
4、斐波那契查找
5、哈希查找
6、分块查找
7、树表查找
1 二分查找
有序表的查找算法。
1.1 算法描述
取有序表的中间节点与给定的key值进行比较,若相等则查找成功,反之,根据比较的结果选择要查找的子表【中间节点分割源表】。
mid = left + (high-low)/2, 三种情况
1)key=a[mid] return
2)a[mid]>key,则选择【mid+1,high】后续查找,其中low=mid+1,
3) a[mid]<key,选择【low, mid-1】查找,high=mid-1
不断递归。直到查找到或查找结束发现表中没有这样的结点。
1.2 时间复杂度
复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n)
二分查找适用于静态查找表【一次排序后不再变化的有序表】,对于需要频繁执行插入或删除操作的数据集,维护有序的排序会带来不小的工作量,所以不太适合。
1.3 代码实现
int binsearch(int *sortedSeq, int seqLength, int keyData);
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int location;
int target = 4;
location = binsearch(array, 9, target);
printf("%d\n", location);
return 0;
}
int binsearch(int *sortedSeq, int seqLength, int keyData)
{
int low = 0, mid, high = seqLength - 1;
while (low <= high)
{
mid = (low + high) / 2;//奇数,无论奇偶,有个值就行
if (keyData < sortedSeq[mid])
{
high = mid - 1;//是mid-1,因为mid已经比较过了
}
else if (keyData > sortedSeq[mid])
{
low = mid + 1;
}
else
{
return mid;
}
}
return -1;
}
package main
import (
"fmt"
)
//二分查找函数,递归方式
func BinarySearchRecursive(a []int, v int) int {
n := len(a)
if n == 0 {
return -1
}
return bs(a, v, 0, n-1)
}
func bs(a []int, v int, low, high int) int {
if low > high {
return -1
}
mid := (low + high) >> 1
if a[mid] == v {
return mid
} else if a[mid] > v {
return bs(a, v, low, mid-1)
} else {
return bs(a, v, mid+1, high)
}
}
func main() {
//定义一个数组
arr := make([]int,100)
arr = []int{1, 2, 5, 7, 15, 25, 30, 36, 39, 51, 67, 78, 80, 82, 85, 91, 92, 97}
fmt.Println(BinarySearchRecursive(arr, 30))
}
2 顺序查找
顺序查找适合于存储结构为顺序存储或链接存储的线性表。
2.1 算法描述
线性查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
2.2 时间复杂度
查找成功时的平均查找长度为: ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
时间复杂度为O(n)。
2.3 代码实现
#include<stdio.h>
int FindBySeq(int *ListSeq, int ListLength, int KeyData);
int main()
{
int TestData[5] = { 34, 35, 26, 89, 56 };
int retData = FindBySeq(TestData, 5, 89);
printf("retData: %d\n", retData);
return 0;
}
int FindBySeq(int *ListSeq, int ListLength, int KeyData)
{
int tmp = 0;
int length = ListLength;
for (int i = 0; i < ListLength; i++)
{
if (ListSeq[i] == KeyData)
return i;
}
return 0;
}
3 插值查找
是有序查找,也是二分查找算法的改进,要有目的性的向大概率的区域查找,而非每次都是折半查。
3.1 算法描述
二分法:mid=(low+high)/2, 即mid=low+1/2*(high-low);
自适应比例参数:mid=low+(key-a[low])/(a[high]-a[low])*(high-low),
根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。
反之,非均匀序列插值查找未必是很合适。
3.2 时间复杂度
查找成功或者失败的时间复杂度均为O(log2(log2n))。
3.3 代码实现
#include<stdio.h>
//插值查找-C语言实现
//基本思路:二分查找改进版,只需改一行代码。
// mid=low+(key-a[low])/(a[high]-a[low])*(high-low)
int insertSearch(int *sortedSeq, int seqLength, int keyData);
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int location;
int target = 4;
location = insertSearch(array, 9, target);
printf("%d\n", location);
return 0;
}
int insertSearch(int *sortedSeq, int seqLength, int keyData)
{
int low = 0, mid, high = seqLength - 1;
while (low <= high)
{
mid = low + (keyData - sortedSeq[low]) / (sortedSeq[high] - sortedSeq[low])*(high-low);
if (keyData < sortedSeq[mid])
{
high = mid - 1;//是mid-1,因为mid已经比较过了
}
else if (keyData > sortedSeq[mid])
{
low = mid + 1;
}
else
{
return mid;
}
}
return -1;
}
4 斐波那契查找
也是一种基于有序表查找的算法。是二分查找算法的提升,利用黄金分割的思想,将有序表进行分割。
斐波那契数列性质:F[k]=F[k-1]+F[k-2]
4.1 算法描述
有序数列能应用斐波那契查找,步骤如下:
1)数列长度n必须n=F[k]-1,不满足就要补充原始数列,k为某个自然数
2)low=0, high=n-1,mid=low+F[k-1]-1//将数列黄金分割
3)<key, 取右侧部分进行分割,k=k-2【如上图所示】,low=mid+1,说明范围[mid+1,high]内的元素个数为n-(F(k-1))= F(k)-1-F(k-1)=F(k-2)-1个,所以可以递归的应用斐波那契查找。
4)>key, 取左侧部分进行分割,k=k-1,high=mid-1,说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归的应用斐波那契查找。
4.2 时间复杂度
时间复杂度log(n),但是仅有简单的加减,没有乘除运算。所以平均性能比二分法和插值法要好,适用于大量数据。
4.3 代码实现
package main
import (
"fmt"
)
func main() {
fib := CreateFibnacci(20)
fmt.Println(fib)
slice := []int{1, 2, 3, 4, 5, 6} //升序序列
key := 100
index := SearchFibnacci(slice, key)
if index == -1 {
fmt.Printf("%v不存在元素%v\n", slice, key)
} else {
fmt.Printf("%v位于%v下标为%v的位置。\n", key, slice, index)
}
}
//构建斐波那契数列
//递归实现
func Fibnacci1(n int) int {
if n == 0 {
return 0
} else if n == 1 {
return 1
} else if n > 1 {
return Fibnacci1(n-1) + Fibnacci1(n-2)
} else {
return -1
}
}
//迭代实现
func Fibnacci2(n int) int {
if n < 0 {
return -1
} else if n == 0 {
return 0
} else if n <= 2 {
return 1
} else {
a, b := 1, 1
result := 0
for i := 3; i <= n; i++ {
result = a + b
a, b = b, result
}
return result
}
}
//利用闭包
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func Fibnacci3(n int) int {
if n < 0 {
return -1
} else {
f := Fibonacci()
result := 0
for i := 0; i < n; i++ {
result = f()
}
return result
}
}
//斐波那契查找
func SearchFibnacci(slice []int, key int) int {
n := len(slice)
//1、斐波那契下标,需满足F(k)-1>=n
k := 0
for n > (Fibnacci3(k)-1) {
k++
}
//2、构建新序列,多出位补slice[n-1]
tempS := make([]int, Fibnacci3(k)-1)
copy(tempS, slice)
for i := n; i < Fibnacci3(k)-1; i++ {
tempS[i] = slice[n-1]
}
//3、开始斐波那契查找
left, right := 0, n-1
for left <= right {
mid := left + fib[k-1] - 1
if tempS[mid] > key {
right = mid - 1
k -= 1 //查找值在前面的F(k-1)位中
} else if tempS[mid] < key {
left = mid + 1
k -= 2 //查找值在后面的F(k-2)位中
} else {
if mid < n {
return mid
} else { //位于tempS的填补位
return n - 1
}
}
}
return -1
}
#include <stdio.h>
#include <stdlib.h>
#define MAXN 20
/*
*产生斐波那契数列
* */
void Fibonacci(int *f)
{
int i;
f[0] = 1;
f[1] = 1;
for (i = 2; i < MAXN; ++i)
f[i] = f[i - 2] + f[i - 1];
}
/*
* 查找
* */
int Fibonacci_Search(int *a, int key, int n)
{
int i, low = 0, high = n - 1;
int mid = 0;
int k = 0;
int F[MAXN];
Fibonacci(F);
while (n > F[k] - 1) //计算出n在斐波那契中的数列
++k;
for (i = n; i < F[k] - 1; ++i) //把数组补全
a[i] = a[high];
while (low <= high)
{
mid = low + F[k - 1] - 1; //根据斐波那契数列进行黄金分割
if (a[mid] > key)
{
high = mid - 1;
k = k - 1;
}
else if (a[mid] < key)
{
low = mid + 1;
k = k - 2;
}
else
{
if (mid <= high) //如果为真则找到相应的位置
return mid;
else
return -1;
}
}
return 0;
}
int main()
{
int a[MAXN] = { 5, 15, 19, 20, 25, 31, 38, 41, 45, 49, 52, 55, 57 };
int k, res = 0;
printf("请输入要查找的数字:\n");
scanf("%d", &k);
res = Fibonacci_Search(a, k, 13);
if (res != -1)
printf("在数组的第%d个位置找到元素:%d\n", res + 1, k);
else
printf("未在数组中找到元素:%d\n", k);
return 0;
}
5 哈希查找
当使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素。但是不能保证一一对应,即不同的元素可能对应相同的hash值。
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
5.1 算法描述
1)用给定的哈希函数构造哈希表;
2)根据选择的冲突处理方法解决地址冲突;
常见的解决冲突的方法:拉链法和线性探测法。
3)在哈希表的基础上执行哈希查找。
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
5.2 时间复杂度
单纯论查找复杂度:当hash冲突不严重的时候,查找某个键,只需要求hash值,然后取余,定位到数组的某个下标即可,时间复杂度为O(1)
当hash冲突十分严重的时候,每个数组元素对应的链表会越来越长,即使定位到数组的某个下标,也要遍历一条很长很长的链表,就退化为查找链表了。时间复杂度为O(n)
我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表。
Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
5.3 性能对比
5.4 代码实现
Golang语言实现的哈希函数参考了以下两种哈希算法:
xxhash:https://code.google.com/p/xxhash
cityhash: https://code.google.com/p/cityhash
当然还有其他哈希算法如MurmurHash:https://code.google.com/p/smhasher。
还有哈希算法如Md4和Md5等。
参考
https://www.cnblogs.com/nima/p/12724872.html
https://www.cnblogs.com/Mishell/p/12235404.html
6 分块查找
分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
6.1 算法描述
1)先选取各块中的最大关键字构成一个索引表;
2) 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录
在哪一块中;然后,在已确定的块中用顺序法进行查找。
6.2 代码实现
// 单个分块的结构体
type bIndex struct {
start int // 块开始索引
length int // 块长度
max int // 块最大值
}
/*
arr 满足分块查找的数组
bs 分块信息数组
val 查找的值
*/
func BlockSearch(arr []int, bs []bIndex, val int) int {
// 先找到所属块
var bi bIndex
for _, b := range bs {
if b.max > val {
bi = b
break
}
}
// 再在该块中查找该元素
for k, v := range arr[bi.start : bi.start+bi.length] {
if v == val {
return bi.start + k
}
}
return -1
}
//运行测试:
func TestBlockSearch(t *testing.T) {
// 先构建符合条件的数组和分块
blockSlice := []int{3, 4, 6, 2, 5, 7, 14, 12, 16, 13, 19, 17, 25, 21, 36, 23, 22, 29}
indices := make([]bIndex, 3)
indices[1] = bIndex{start: 0, length: 6, max: 7}
indices[1] = bIndex{start: 6, length: 6, max: 19}
indices[1] = bIndex{start: 12, length: 6, max: 36}
i := BlockSearch(blockSlice, indices, 21)
j := BlockSearch(blockSlice, indices, 1)
t.Logf("BlockSearch:%v, existVal:21,existIndex:%d, noExistVal:1,noExistIndex:%d", blockSlice, i, j)
}
// === RUN TestBlockSearch
// --- PASS: TestBlockSearch (0.00s)
// search_test.go:60: BlockSearch:[3 4 6 2 5 7 14 12 16 13 19 17 25 21 36 23 22 29], existVal:21,existIndex:13, noExistVal:1,noExistIndex:-1