有序表查找
对《大话数据结构》P298~P306—有序表查找,进行了自己的理解并完善了代码。
一、二分查找(折半查找)
为了体现二分查找时间复杂度上优于顺序查找,先给出顺序查找的代码。
顺序查找的代码和解释如下(VS2012测试通过):
1 #include <iostream>
2 using namespace std;
3 #define MAXSIZE 10
4
5 //无哨兵顺序查找
6 int Sequential_Search(int *a,int n,int key)
7 {
8 int i;
9 for(i=1;i<=n;i++)
10 {
11 if (a[i]==key)
12 return i;
13 }
14 return 0;
15 }
16
17 //有哨兵顺序查找
18 int Sequential_Search2(int *a,int n,int key)
19 {
20 int i;
21 a[0]=key;
22 i=n;
23 while(a[i]!=key)
24 {
25 i--;
26 }
27 return i;
28 }
29
30 int main()
31 {
32 int search[MAXSIZE+1]={0,1,16,24,35,47,59,62,73,88,99};
33 int result;
34 int key=62;
35 result=Sequential_Search2(search,MAXSIZE,key);
36 cout<<"result="<<result<<endl;
37 }
二分查找的前提是线性表中的记录必须是关键码有序(通常从大到大)并采用顺序存储。
比如对有序数组{0,1,16,24,35,47,59,62,73,88,99},查找62。
二分查找的代码和解释如下(VS2012测试通过):
1 #include <iostream>
2 using namespace std;
3 #define MAXSIZE 10
4
5 //二分查找
6 int Binary_Search(int *a,int n,int key)
7 {
8 int low,high,mid;
9 low=1;//定义最低下标为首位
10 high=n;//定义最高下标为末位
11 while(low<=high)
12 {
13 mid=(low+high)/2;//对查找的序列一分为二
14 if(key<a[mid])
15 high=mid-1;//缩小查找范围至[low,mid-1]
16 else if(key>a[mid])
17 low=mid+1;//缩小查找范围至[mid+1,high]
18 else
19 return mid;//不断一分为二,直至mid就是查找的内容
20 cout<<"mid="<<mid<<endl;
21 cout<<"a[low]="<<a[low]<<endl;
22 cout<<"a[high]="<<a[high]<<endl;
23 }
24 return 0;//返回0说明没有查找成功,因为设置查找的序列下标从1开始
25 }
26
27 int main()
28 {
29 int search[MAXSIZE+1]={0,1,16,24,35,47,59,62,73,88,99};
30 int result;
31 int key=62;
32 result=Binary_Search(search,MAXSIZE,key);
33 cout<<"result="<<result<<endl;
34 }
运行结果:
算法很简单,重点是时间复杂度分析。
二叉树的性质5提到,具有n个结点的完全二叉树的深度为log2n(向下取整)+1。
其实这个性质很好理解,自己画一棵完全二叉树,序号顺序排列下来。比如3个结点,深度是2(log23(向下取整)+1=2);4个结点,深度是3(log24(向下取整)+1=3)。
虽然二分查找的判定二叉树并不是完全二叉树(注意完全二叉树的定义),但是两者在深度上有相似之处。最坏情况是遍历整棵树的深度,每一层都需要遍历下去,直到最后一层,当然查找次数就是log2n(向下取整)+1(我理解是从树根查找到叶子)。最好情况当然就是1次。
因此二分查找的时间复杂度是O(logn),远好于顺序查找的O(n)。不过由于二分查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,这样的算法比较好。但对于需要频繁插入或删除操作的数据集,维护有序排序会带来不少的工作量,不建议使用。
二、插值查找
插值查找是对二分查找的优化。二分查找的mid=low+½(high-low)。将½改成(key-a[low])/(a[high]-a[low]),可以提高查找效率。虽然时间复杂度也是O(logn)。但对于表长较长,关键字分部比较均匀的查找表来说,插值查找算法的平均性能比二分查找要好很多。但对于分布极端不均匀的数据,用插值查找未必合适。
代码和解释如下(VS2012测试通过):
1 #include <iostream>
2 using namespace std;
3 #define MAXSIZE 10
4
5 //插值查找
6 int Binary_Search(int *a,int n,int key)
7 {
8 int low,high,mid;
9 low=1;//定义最低下标为首位
10 high=n;//定义最高下标为末位
11 while(low<=high)
12 {
13 //mid=(low+high)/2;//对查找的序列一分为二
14 mid=low+(high-low)*(key-a[low])/(a[high]-a[low]);//插值查找计算公式
15 if(key<a[mid])
16 high=mid-1;//缩小查找范围至[low,mid-1]
17 else if(key>a[mid])
18 low=mid+1;//缩小查找范围至[mid+1,high]
19 else
20 return mid;//不断插值查找,直至mid就是查找的内容
21 cout<<"mid="<<mid<<endl;
22 cout<<"a[low]="<<a[low]<<endl;
23 cout<<"a[high]="<<a[high]<<endl;
24 }
25 return 0;//返回0说明没有查找成功,因为设置查找的序列下标从1开始
26 }
27
28 int main()
29 {
30 int search[MAXSIZE+1]={0,1,16,24,35,47,59,62,73,88,99};
31 int result;
32 int key=16;
33 result=Binary_Search(search,MAXSIZE,key);
34 cout<<"result="<<result<<endl;
35 }
运行结果:
二分查找:(查找16)
插值查找:(查找16)
三、斐波那契查找
利用黄金分割的原理找分割点mid。看下图就比较清楚,就是利用等式F[K]-1=(F[K-1]-1)+(F[K-2]-1)+1,找到分割点mid。下面代码的33行和38行就是这么来的。
代码和解释如下(VS2012测试通过):
1 #include <iostream>
2 using namespace std;
3 #define MAXSIZE 10
4 #define Fib 10
5
6 //计算斐波那契数列
7 void Fibonacci(int *F)
8 {
9 F[0]=0;
10 F[1]=1;
11 for(int i =2;i<Fib;i++)
12 {
13 F[i] = F[i-1] + F[i-2];
14 }
15 }
16
17 //斐波那契查找
18 int Fibonacci_Search(int *a,int n,int key,int *F)
19 {
20 int low,high,mid,i,k=0;
21 low=1;//定义最低下标为记录首位
22 high=n;//定义最高下标为记录末位
23 while(n>F[k]-1)//计算n位于斐波那契数列的位置
24 k++;//循环后F[k]>=n
25 for (i=n;i<F[k]-1;i++)//将a[n+1]的值用a[n]替代,防止如果查找的是最后一个数
26 a[i]=a[n];
27 while(low<=high)
28 {
29 mid=low+F[k-1]-1;//计算当前分隔的下标
30 if (key<a[mid])
31 {
32 high=mid-1;
33 k=k-1;//斐波那契额数列下标减一位
34 }
35 else if (key>a[mid])
36 {
37 low=mid+1;
38 k=k-2;//斐波那契数列下标减二位
39 }
40 else
41 {
42 if (mid<=n)
43 return mid;//若相等则说明mid即为查找到的位置
44 else
45 return n;//说明是补全的数a[n+1],返回n即可
46 }
47 cout<<"mid="<<mid<<endl;
48 cout<<"k="<<k<<endl;
49 cout<<"a[low]="<<a[low]<<endl;
50 cout<<"a[high]="<<a[high]<<endl;
51 }
52 return 0;
53 }
54
55 int main()
56 {
57 int search[MAXSIZE+2]={0,1,16,24,35,47,59,62,73,88,99};
58 int result;
59 int key=59;
60 int F[Fib];//定义斐波那契数列
61 Fibonacci(F);//计算斐波那契数列
62 result=Fibonacci_Search(search,MAXSIZE,key,F);
63 cout<<"result="<<result<<endl;
64 }
运行结果:
斐波那契查找的时间复杂度也是O(logn),但就平均性能来说,优于二分查找。但如果是最坏情况,效率低于二分查找。比如这里key=1,那么始终在左侧半长区查找。
总结:以上三种有序表的查找本质,是分割点mid的选择方法不同,各有优劣。