有序表查找

对《大话数据结构》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的选择方法不同,各有优劣。


posted @ 2016-04-26 14:25  Pearl_zju  阅读(473)  评论(0编辑  收藏  举报