1.时间复杂度
算法:由专门的人设计出来,应用在特定领域,解决特定问题的处理过程或者处理模型。
两个指标:时间复杂度/空间复杂度
算法最优解:理论上的时间复杂度 + 空间复杂度 + 实际压力测试
1.1 关于时间频度
请看示例:
int aFunc(void) {
printf("Hello, World!\n"); // 需要执行 1 次
return 0; // 需要执行 1 次
}
执行以上代码,需要执行2次运算
再看示例:
int aFunc(int n) {
for(int i = 0; i<n; i++) { // 需要执行 (n + 1) 次
printf("Hello, World!\n"); // 需要执行 n 次
}
return 0; // 需要执行 1 次
}
执行以上代码,需要执行 (n + 1 + n + 1) = 2n + 2 次运算
我们把算法需要执行的运算次数用输入大小n的函数表示,即T(n),也称为时间频度 。
1.2 关于时间复杂度
在前面介绍到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。
为此,我们引入时间复杂度概念。
为了估算算法需要的运行时间和简化算法分析,我们引入时间复杂度的概念。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示。
若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n))
则可以称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
这样用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。
一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。
常见的算法时间复杂度以及效率上的高低顺序:
O(1) 常数阶 < O(logn) 对数阶 < O(n) 线性阶 < O(nlogn) < O(n^2) 平方阶 < O(n^3) < { O(2^n) < O(n!) < O(n^n) }
1.3 根据时间频度求复杂度
当我们拿到算法的执行次数函数 T(n) 之后怎么得到算法的时间复杂度呢?
步骤主要分为以下几步:
1)去掉运行时间中的所有加法常数。
2)在修改后的运行次数函数中,只保留最髙阶项。
3)如果最高阶项存在且不是1,则去除与这个项相乘的常数。
示例一:普通求和算法
#include <stdio.h>
int main()
{
int i, sum = 0, n = 100; /* 执行1次 */
for( i = 1; i <= n; i++) /* 执行 n+1 次 */
{
sum = sum + i; /* 执行n次 */
//printf("%d \n", sum);
}
printf("%d", sum); /* 执行1次 */
分析过程:
1)该算法所用的时间(算法语句执行的总次数)为:
f(n) = 1 + ( n + 1 ) + n + 1 = 2n + 3
2)保留最髙阶项
而当n不断增大,比如我们这次所要计算的不是 1 + 2 + 3 + 4 + … + 100 = ? 而是 1 + 2 + 3 + 4 +… + n = ?
其中 n 是一个十分大的数字,那么由此可见,上述算法的执行总次数(所需时间)会随着 n 的增大而增加,但是在 for 循环以外的语句并不受 n 的规模影响(永远都只执行一次)。
故:上述算法的执行总次数简单的记做: 2n
3)去除与最高项相乘的常数
即2n变为n
所以,算法的时间复杂度为:O(n)
示例2:高斯求和算法
int main()
{
int sum = 0, n = 100; /* 执行1次 */
sum = (1 + n) * n/2; /* 执行1次 */
printf("%d", sum); /* 执行1次 */
}
这个算法的时间复杂度: O(3),但一般记作 O(1)。
从感官上我们就不难看出,从算法的效率上看,O(3) < O(n) 的,所以高斯的算法更快,更优秀。
练习:试着推算下如下算法的时间复杂度:
int main()
{
int i, j, x = 0, sum = 0, n = 100; /* 执行1次 */
for( i = 1; i <= n; i++)
{
sum = sum + i;
//printf("%d \n", sum);
for( j = 1; j <= n; j++)
{
x++; /* 执行n*n次 */
sum = sum + x;
}
}
printf("%d", sum); /* 执行1次 */
}
分析过程:
1)先计算执行总次数:
执行总次数 = 1 + (n + 1) + n*(n + 1) + n*n + (n + 1) + 1 = 2n2 + 3n + 3
2)保留最髙阶项
这里的最高阶是 n 的二次方,所以算式变为:执行总次数 = 2n^2
3)去除与最高项相乘的常数
这里 n 的二次方不是 1 所以要去除这个项的相乘常数,算式变为:执行总次数 = n^2
因此最后我们得到上面那段代码的算法时间复杂度表示为: O( n^2 )
练习:
- 求该方法的时间复杂度
void aFunc(int n)
{
for (int i = 0; i < n; i++)
{
for (int j = i; j < n; j++)
{
printf("Hello World\n");
}
}
}
执行总次数 = (n+1) + n*(n+1) + n*n = 2n^2 + 2n +1 = O(n^2)
- 求该方法的时间复杂度
void aFunc(int n)
{
for (int i = 2; i < n; i++)
{
i *= 2;
printf("%i\n", i);
}
}
执行总次数 = (n+1) + n + n = 3n+1 = O(n)
- 求该方法的时间复杂度
long aFunc(int n)
{
if (n <= 1)
{
return 1;
} else
{
return aFunc(n - 1) + aFunc(n - 2);
}
}
显然运行次数,T(0) = T(1) = 1,同时 T(n) = T(n - 1) + T(n - 2) + 1,这里的1是其中的加法算一次执行。
显然 T(n) =T(n-1)+T(n-2)是一个斐波那契数列,通过归纳证明法可以证明,当n>=1时,T(n)<(5/3)^n,同时当 n > 4 时 T(n) >= (3/2)^n。
所以该方法的时间复杂度可以表示为 O((5/3)^n),简化后为 O(2^n)。
2.常用排序查找算法
2.1 排序算法
2.1.1 冒泡排序
1)实现过程:
从头开始遍历,相邻两个数比较,如果后面比前面小,就交换。然后再从头遍历n遍。实际上是把最大的数排到了后面,后面是已经排好序的。
2)用c实现最优解测试
void bubble_sort(int n,int *a)
{
int i,j,temp;
for(j=0;j<n;j++)
{
for(i=0;i<n-j;i++)
{
if(a[i] > a[i+1])
{
temp = a[i];
a[i] = a[i+1];
a[i+1] = temp;
}
}
}
for(i=0;i<n;i++)
printf("a[%d]=%d\t",i,a[i]);
}
3)计算出时间/空间复杂度,并总结优缺点
2.1.2 选择排序
1)实现过程:
原理:从后面挑出最小的值,放在第一个,依次循环.
过程:标记最小的那个数的下标,与第一个交换,依次循环.
2)用c实现最优解测试
void select(int n,int *a)
{
int i,j,temp,min;
min = a[0];
for(j=0;j<n;j++)
{
for(i=j;i<n;i++)
{
if(min >= a[i])
{
min = a[i];
temp = a[j];
a[j] = a[i];
a[i] = temp;
}
}
}
for(i=0;i<n;i++)
printf("a[%d]=%d\t",i,a[i]);
printf("\n");
}
3)计算出时间/空间复杂度,并总结优缺点
时间复杂度:O(n^2)
优缺点:选择排序比冒泡排序交换次数少
冒泡排序稳定性好,冒泡最好是O(n),选择排序最好最坏都是O(n^2)
稳定性:5 8 5 2 9 第一遍第一个元素5会和2交换,那么原序中两个5相对位置就改变
不稳定性:我们现在所做的拿数据简单的做测试对破坏顺序当然没什么关系。但是如果是那结构体来排序呢?比如一个结构体里面包含一个人的学号和分数,要求在分数相等的情况下再按照学号排序,这个时候顺序就有关系了
2.1.3 插入排序
1)实现过程:
1)从第一个元素开始,该元素可以认为已经被排序
2)取出下一个元素,在已经排序的元素序列中从后向前扫描
3)如果该元素(已排序)大于新元素,将该元素移到下一位置
4)重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5)将新元素插入到该位置中
6)重复步骤2
2)用c实现最优解测试
void insert(int n,int *a)
{
int i,j,temp;
for(j=1;j<n;j++)
{
temp = a[j];
for(i=j-1;i>=0 && temp<a[i];i--)
a[i+1] = a[i];
a[i+1] = temp;
}
}
3)计算出时间/空间复杂度,并总结优缺点
O(n^2)
优点:稳定
缺点:数据多时,比较次数比较多
2.1.4 希尔排序
又称为缩小增量排序,该方法的基本思想是:设待排序元素序列有n个元素,首先取一个整数increment(小于n)作为间隔将全部元素分为increment个子序列,所有距离为increment的元素放在同一个子序列中,在每一个子序列中分别实行直接插入排序。然后缩小间隔increment,重复上述子序列划分和排序工作。直到最后取increment=1,将所有元素放在同一个子序列中排序为止。
由于开始时,increment的取值较大,每个子序列中的元素较少,排序速度较快,到排序后期increment取值逐渐变小,子序列中元素个数逐渐增多,但由于前面工作的基础,大多数元素已经基本有序,所以排序速度仍然很快。
关于希尔排序increment(增量)的取法
增量increment的取法有各种方案。最初shell提出取increment=n/2向下取整,increment=increment/2向下取整,直到increment=1。
但由于直到最后一步,在奇数位置的元素才会与偶数位置的元素进行比较,这样使用这个序列的效率会很低。
后来Knuth提出取increment=n/3向下取整+1.还有人提出都取奇数为好,也有人提出increment互质为好。
应用不同的序列会使希尔排序算法的性能有很大的差异。
举例:
5-排序前的data数组: 10 8 6 20 4 3 22 1 0 15 16
排序前的5个子数组: 10 - - – - 3 – - - – 16
8 - – - - 22
6 – - - – 1
20 - - – - 0
4 - – - - 15
排序后的5个子数组: 3 - - – - 10 – - - – 16
8 - – - – 22
1 – - – -- 6
0 - – -- - 20
4 – -- - – 15
5-排序后、3-排序前 3 8 1 0 4 10 22 6 20 15 16
的的data数组
排序前的3个数组: 3 - - 0 - – 22 - – 15
8 - - 4 – -- 6 – -- 16
1 - - 10 – - 20
排序后的3个数组: 0 - - 3 - – 15 - – 22
4 - - 6 – -- 8 – -- 16
1 - - 10 – - 20
3-排序后、1-排序前 0 4 1 3 6 10 15 8 20 22 16
的data数组
1-排序后的数组: 0 1 3 4 6 8 10 15 16 20 22
时间复杂度:O(n^5/3);
优点:时间复杂度较其他基本排序比较好
缺点:不稳定;不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,
最后其稳定性就会被打乱,所以shell排序是不稳定的
代码实现:
#include<stdio.h>
void shell_sort(int data[],int arrSize)
{
int i,j,hCnt,h;
int increments[20],k;
for(h=1,i=0;h<arrSize;i++)
{
increments[i]=h;
h=3*h+1;
}
for(i--;i>=0;i--)
{
h=increments[i];
for(hCnt=h;hCnt<2*h;hCnt++)
{
for(j=hCnt;j<arrSize;)
{
int tmp=data[j];
k=j;
while(k-h>=0&&tmp<data[k-h])
{
data[k]=data[k-h];
k -=h;
}
data[k]=tmp;
j +=h;
}
}
}
}
int main()
{
int data[11]={10,8,6,20,4,3,22,1,0,15,16};
for(int i=0;i<11;i++)
printf("-%d-",data[i]);
printf("\n");
shell_sort(data,11);
for(int i=0;i<11;i++)
printf("-%d-",data[i]);
printf("\n");
return 0;
}
2.1.5 归并排序
一.思想:分治法
1.每个递归过程涉及三个步骤 :
(1)分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素,最后分解到不能再分解,即子序列只有一个元素
(2) 治理: 对每个子序列分别调用归并排序__MergeSort, 进行递归操作
(3) 合并: 合并两个排好序的子序列,生成排序结果.
2.图解“分解”和“合并”:
(1)偶数个数:

(2)奇数个数:

合并过程:


二.优缺点
优点: 稳定,最坏、平均最好的时间复杂度都是O(nlog2n)
缺点:辅存很大(如下面代码中的临时数组),适合对象排序;
三.参考代码
#include <stdio.h>
#include <stdlib.h>
void mergesort(int *arr,int first,int last) //合并过程
{
int mid = (first + last) / 2; //找到两个子序列的起始下标
int len = last - first + 1;
int i = first;
int j = mid +1;
int * ptr = (int *)malloc(sizeof(int)*(len)); //临时数组
int * tmp = ptr; //记住临时数组的位置
while( i<= mid && j <=last) //当两个数组都有元素时,比较大小,存入临时数组
{
if(arr[i] < arr[j])
*ptr++ = arr[i++];
else
*ptr++ = arr[j++];
}
//因为两个子数组都是已经各自排好序的,所以当一个数组已经全部存入时,说明另一个子数组剩下的元素都是比存入的元素大,所以直接导入剩下的元素
if(i > mid) //把子数组 j剩下的元素存入临时数组
{
while(j <= last)
{
*ptr++ = arr[j++];
}
}
else if(j > last) //把子数组i 剩下的元素存入临时数组
{
while(i <= mid)
{
*ptr++ = arr[i++];
}
}
for(int i = 0; i < len; i++) //把临时数组的值赋给原来的数组
arr[first++] = *tmp++;
}
void sort(int *arr,int first,int last)
{
if(last == first) return; //只有一个元素时
int mid = (first + last) / 2;
sort(arr,first,mid); //整理左半部分
sort(arr,mid+1,last); //整理右半部分
mergesort(arr,first,last); //合并左右两个部分
}
void trvel(int *arr,int n) //打印数组
{
for(int i = 0; i<n; i++)
printf("%d\t", *(arr+i));
printf("\n");
}
int main()
{
int arr[7] = {7,6,9,5,4,2,3};
int arr2[8] = {9,7,3,5,8,4,3,6};
trvel(arr,7);
sort(arr,0,6);
trvel(arr,7);
printf("-------------------------------------------------------------\n");
trvel(arr2,8);
sort(arr2,0,7);
trvel(arr2,8);
return 0;
}
四.时间复杂度
归并的时间复杂度分析:主要考虑两个函数的时间花销,一、数组划分函数 二、有序数组归并函数
前者的时间复杂度为O(n),因为代码中有2个长度为n的循环,所以时间复杂度为O(n);
数组长度为n的归并排序所消耗的时间T[n]:调用前面函数将数组划分为两个部分,那每一个部分排序好所花时间为T[n/2],最后将这两个部分的数组合并成一个有序数组的时间花费为O(n);
公式:T[n]=2T[n/2]+O(n)
用递归树的方法解递归式T[n]=2T[n/2]+O(n)
假设解决最后的子问题用时为常数c,则对于n个待排序记录来说整个问题的规模为cn。
从这个递归树可以看出,第一层时间代价为cn,第二层时间代价为cn/2+cn/2=cn……每一层代价都是cn,总共有logn+1层。所以总的时间代价为cn*(logn+1).时间复杂度是o(nlogn).
///////////////////////////////////////
我们要计算的就是层数:
2 4 8 16
1 1 2 2 4 4 8 8
1 1 2 2 4 4
1 1 2 2
1 1
很明显,
2^1对应(1+1)层
2^2对应(2+1)层
2^3对应(3+1)层
2^4对应(4+1)层
同时,2(n-1)和2n之间对应的层数为(n+1)层
所以令2^x=n(x为层数,n表示多少元素,这里往下取到2的次方整)
x=〖log〗_2 (n)
所以x+1=logn+1
///////////////////////////////////////
最终得出结果为:T[n]=O(nlogn);
因为不管元素在什么情况下都要做出这些步骤,所花销的时间是不变的,所以该算法的最优时间复杂度和最差时间复杂度以及平均复杂度是一致的:O(nlogn)。
2.1.6 快速排序
1)实现过程:
把第1个数设为基数,先设j从后遍历,如果遇到比基数小的,就停下来;设i从头遍历,遇到比基数大的就停下来,然后交换两个数。i,j继续遍历,直到相遇,把基数和相遇点的数交换。然后现在分成了两部分,继续重复以上操作(递归)。
我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数,就是一个用来参照的数。为了方便,就让第一个数6作为基准数。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列。
3 1 2 5 4 6 9 7 10 8
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。利用递归继续分直到结束。
代码实现
void sort(int *a, int left, int right)
{
if(left >= right)/*如果左边索引大于或者等于右边的索引就代表已经整理完成一个组了*/
return ;
int i = left;
int j = right;
int key = a[left];
while(i < j) /*控制在当组内寻找一遍*/
{
while(i < j && key <= a[j])
/*而寻找结束的条件就是,1,找到一个小于或者大于key的数(大于或小于取决于你想升
序还是降序)2,没有符合条件1的,并且i与j的大小没有反转*/
{
j--;/*向前寻找*/
}
a[i] = a[j];
/*找到一个这样的数后就把它赋给前面的被拿走的i的值(如果第一次循环且key是
a[left],那么就是给key)*/
while(i < j && key >= a[i])
/*这是i在当组内向前寻找,同上,不过注意与key的大小关系停止循环和上面相反,
因为排序思想是把数往两边扔,所以左右两边的数大小与key的关系相反*/
{
i++;
}
a[j] = a[i];
}
a[i] = key;/*当在当组内找完一遍以后就把中间数key回归*/
sort(a, left, i - 1);/*最后用同样的方式对分出来的左边的小组进行同上的做法*/
sort(a, i + 1, right);/*用同样的方式对分出来的右边的小组进行同上的做法*/
/*当然最后可能会出现很多分左右,直到每一组的i = j 为止*/
}
最坏 O(n^2)
最好 n*log(n)
空间复杂度:每次递归都需要开辟新的空间存储。
优点:快速
缺点:不稳定。
2.1.7 堆排序
堆是一棵完全二叉树的数组对象。
堆总是满足下列性质:
堆中某个节点的值总是不大于或不小于其父节点的值,根节点最大的堆叫做最大堆或大根堆或大顶堆。
根节点最小的堆叫做最小堆或小根堆或小顶堆。
①构造堆:从最后一个非叶节点进行调整,该节点必须大于其孩子节点,如果不大于则需要交换值
②构造完之后将该根节点与最后个节点位置交换(移到数组的最末尾),并移出树,且要重新进行堆调整
堆根元素(最大数)与堆最后一个数交换后,需再次调整成大根堆,此时是从上往下调整的。
不管是初始大根堆的从下往上调整,还是堆根堆尾元素交换,每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换,交换之后都可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void Swap(int *a,int i,int j)//交换数组中的两个值
{
int tmp;
tmp = a [j];
a[j]=a[i];
a[i]=tmp;
}
//小根堆调整
void MinHeapify(int *a,int n,int node)//小根堆调整
{
int lchild,rchild,min;//定义左右孩子和三者最小元素的下标
lchild=2*node+1;//当前节点的左孩子
rchild=2*node+2;//当前节点的右孩子
if(lchild<n && a[lchild]<a[node])
min=lchild;
else
min=node;
if(rchild<n && a[rchild]<a[min])//判断左右孩子谁最小,并记住下标
min=rchild;
if(min!=node)//再判断最小的是否比当前非叶节点小,如果是则进行交换
{
Swap(a,min,node);//一次堆调整
MinHeapify(a,n,min);//子节点需要重新递归调整
}
}
//构建小根堆
void MinHeapCreate(int *a,int n)
{
int i;
for(i=n/2;i>=0;i--)
{
MinHeapify(a,n,i);//每个非叶节点要进行一次
}
}
//大根堆调整
void MaxHeapify(int *a,int n,int node)
{
int lchild,rchild,max;//定义左右孩子和三者最大元素的下标
lchild=2*node+1;//当前节点的左孩子
rchild=2*node+2;//当前节点的右孩子
if(lchild<n && a[lchild]>a[node])//判断左右孩子谁最大,并记住下标
max=lchild;
else
max=node;
if(rchild<n && a[rchild]>a[max])
max=rchild;
if(max!=node)//再判断最大,是否比当前非叶节点大,如果是则进行交换
{
Swap(a,max,node);
MaxHeapify(a,n,max); //子节点需要重新递归调整
}
}
//构建大根堆
void MaxHeapCreate(int *a, int n)
{
int i;
for(i=n/2;i>=0;i--)
{
MaxHeapify(a,n,i);
}
}
int main()
{
int tmp;
int array[]={6,1,14,7,5,8};
int i,n=sizeof(a)/sizeof(int);//计算数组的长度
for(i=0;i<n;i++)//打印初始数组
{
printf("%d\t",a[i]);
}
printf("\n");
for(i=n;i>1;i--)
{
MinHeapCreate(a,i);
Swap(a,0,i-1);//根节点与最后一个数进行交换
}
printf("MinHeap:\n");
for(i=0;i<n;i++)//输出数组
{
printf("%d\t",a[i]);
}
printf("\n");
for(i=n;i>1;i--)
{
MaxHeapCreate(a,i);
Swap(a,0,i-1);//根节点与最后一个数进行交换
}
printf("MaxHeap;\n");
for(i=0;i<n;i++)//输出数组
{
printf("%d\t",a[i]);
}
return 0;
}
2.1.8 桶排序
桶排序主要就是把数组中的所有元素分为若干个数据块,也就是若干个桶,然后对每个桶里的数据进行排序,最后将所有桶里的数据依次排列.时间复杂度为O(n)
代码展示:
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
int key;
struct node *next;
}KeyNode;
void bucket_sort(int a[],int size,int bucket_size)
{
int i,j;
KeyNode **bucket_table = (KeyNode **)malloc(bucket_size*sizeof(KeyNode*));
for(i=0;i<bucket_size;i++)
{
bucket_table[i] = (KeyNode*)malloc(sizeof(KeyNode));
bucket_table[i]->key = 0;
bucket_table[i]->next = NULL;
}
for(j=0;j<size;j++)
{
KeyNode *node = (KeyNode *)malloc(sizeof(KeyNode));
node->key = a[j];
node->next = NULL;
int index = a[j]/10;
KeyNode *p = bucket_table[index];
if(p->key==0)
{
bucket_table[index]->next = node;
(bucket_table[index]->key)++;
}
else
{
while(p->next != NULL && p->next->key <= node->key)
p = p->next;
node->next = p->next;
p->next = node;
(bucket_table[index]->key)++;
}
}
KeyNode* k = NULL;
for(i=0;i<bucket_size;i++)
{
for(k=bucket_table[i]->next;k!=NULL;k=k->next)
printf("%d ",k->key);
}
printf("\n");
}
int main()
{
int raw[] = {3,45,56,4,5,8,2};
int size = sizeof(raw)/sizeof(int);
bucket_sort(raw,size,10);
}
优点:1、稳定2、大多数情况下是最快的,比快速排序还快
缺点:1、非常浪费存储空间2、排序数据限定,只为整型范围3、只能是整数
2.2 查找算法
2.2.1 二分查找
要求:线性表必须采用顺序存储结构,元素按关键字有序排列
过程:
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
int search(int n,int *a,int key)
{
int first = 1;
int last = n;
int mid;
while(first <= last)
{
mid = (first+last)/2;
if(a[mid] < key)
first = mid+1;
else if(a[mid] > key)
last = mid-1;
else
return mid;
}
}
时间复杂度:
第一次:n/2
第二次:n/(2^2)
第三次:n/(2^3)
…
第m次:n/(2^m)
所以O(n) = log2n;
优点:比较次数少,查找速度快
缺点:要求为有序表,插入必须插在合适的位置
2.2.2 插值查找
打个比方,在英文字典里面查“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,这样也就间接地减少了比较次数。
插值查找是在折半查找的基础上进行优化,将mid的值(low+high)/2修改为
将查找关键字于查找表中的最大最小关键字对比后进行查找。
时间复杂度为O(log n)
int search(int n,int *a,int key)
{
int first = 1;
int last = n;
int mid;
while(first <= last)
{
mid = first + (key-a[first])*(last-first)/(a[last]-a[first]);
if(a[mid] < key)
first = mid+1;
else if(a[mid] > key)
last = mid-1;
else
return mid;
}
}
2.2.3 斐波那契查找
原理:利用斐波那契数列的性质,黄金分割的原理来确定mid的位置。
1.定义
黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为0.618。
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近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,hign]范围内,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个,所以可以递归的应用斐波那契查找
注意:n=F(k)-1, 表中记录的个数为某个斐波那契数小1。
原因:是为了格式上的统一,以方便递归或者循环程序的编写。表中的数据是F(k)-1个,使用mid值进行分割又用掉一个,那么剩下F(k)-2个。正好分给两个子序列,每个子序列的个数分别是F(k-1)-1与F(k-2)-1个,格式上与之前是统一的。不然的话,每个子序列的元素个数有可能是F(k-1),F(k-1)-1,F(k-2),F(k-2)-1个,写程序会非常麻烦。
2.时间复杂度:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。
3.优缺点:如果要查找的记录在右侧,则左边的数据都不用再比较了,不断反复进行下去,对处于当中的大部分数据源,其工作效率要高一些所以尽管斐波那契查找的时间复杂度也为O(logn)。但品均性能上来说,斐波那契查找要优于折半查找,可惜如果是最坏情况,比如这里key=1的时候,那么始终都处于左侧长半区在查找,则查找效率要低于折半查找。
还有一点比较关键,折半查找是进行加法与除法运算(mid={low+high}/2),插值查找进行复杂的四则运算(mid=low+(high-low)*(key-a[low])/(a[high]-a[low])),而斐波那契额查找只是最简单加减运算(mid=low+F[k-1]-1),在海量数据的查找过程中,这种细微的差别可能会影响最终的查找效率。
应该说“顺序表查找”、“折半查找”、“插值查找”这三种查找本质上是分隔点不同,各有优劣,实际开发过程中可根据实际情况中数据的综合特点再做出选择。
demo.c
#include <stdio.h>
#define FIB_MAXSIZE 20
void ProduceFib(int *fib, int size) //创建裴波那契数组,大小为size
{
int i;
fib[0] = 1;
fib[1] = 1;
for (i = 2; i < size; i++)
fib[i] = fib[i - 1] + fib[i - 2];
}
/**
* 斐波那契查找,查找成功返回位序,否则返回-1
* @param data:有序表数组
* @param length:有序表元素个数
* @param searchValue:待查找关键字
*/
int FibonacciSearch(int *data, int length, int searchValue)
{
int low, high, mid, k, i, fib[FIB_MAXSIZE];
low = 0; //记录最小下标
high = length - 1; //记录最大下标
ProduceFib(fib, FIB_MAXSIZE);
k = 0;
// 找到有序表元素个数在斐波那契数列中最接近的最大数列值
while (high > fib[k] - 1)
k++;
// 1 2 3 4 5 6 7 8 9 10 11 若不补齐,想要查找11的话,进行到最后没有值与searchValue
// 比较
// 用最大值补齐有序表
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) //说明所找数据在 low 到 mid-1 之间
{
high = mid - 1;
k = k - 1;
}
if (data[mid] < searchValue) //说明所找数据在 mid+1 到 high 之间
{
low = mid + 1;
k = k - 2;
}
}
return -1; //若没有找到,则返回-1
}
int main()
{
int data[11] = {1,2,3,4,5,6,7,8,9,10,11};
for(int i = 0; i<11;i++)
printf("%d\t",data[i]);
printf("\nenter the data you want to search:\n");
int find ;
scanf("%d",&find);
int index = FibonacciSearch(data, 11, find);
if(index == -1)
{
printf("no found the data %d\n",find);
return -1;
}
else
{
printf("the index of %d is %d\n",find,index);
return 0;
}
}
2.2.4 分块查找
分块查找是折半查找和顺序查找的一种改进方法,折半查找虽然具有很好的性能,但其前提条件时线性表顺序存储而且按照关键码排序,这一前提条件在结点树很大且表元素动态变化时是难以满足的。而顺序查找可以解决表元素动态变化的要求,但查找效率很低。如果既要保持对线性表的查找具有较快的速度,又要能够满足表元素动态变化的要求,则可采用分块查找的方法。
分块查找的速度虽然不如折半查找算法,但比顺序查找算法快得多,同时又不需要对全部节点进行排序。当节点很多且块数很大时,对索引表可以采用折半查找,这样能够进一步提高查找的速度。
分块查找由于只要求索引表是有序的,对块内节点没有排序要求,因此特别适合于节点动态变化的情况。当增加或减少节以及节点的关键码改变时,只需将该节点调整到所在的块即可。在空间复杂性上,分块查找的主要代价是增加了一个辅助数组。
需要注意的是,当节点变化很频繁时,可能会导致块与块之间的节点数相差很大,没写快具有很多节点,而另一些块则可能只有很少节点,这将会导致查找效率的下降。
1,先选取各块中的最大关键字构成一个索引表;
2,查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;
然后,在已确定的块中用顺序法进行查找。
将n个数据元素"按块有序"划分为m块(m ≤ n)。
注意:
每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……。然后使用二分查找及顺序查找。
设表共n个结点,分b块,s=n/b
(分块查找索引表)平均查找长度=Log2(n/s+1)+s/2
(顺序查找索引表)平均查找长度=(S2+2S+n)/(2S)
测试代码
#include <stdio.h>
struct index //定义块的结构
{
int key; //块的关键字
int start; //块的起始值
int end; //块的结束值
}index_table[4]; //定义结构体数组
int block_search(int key,int a[]) //自定义实现分块查找
{
int i,j;
i=1;
while(i<=3 && key>index_table[i].key) //确定在哪个块中
i++;
if(i>3) //大于分得的块数,则返回0
return 0;
j = index_table[i].start; //j等于块范围的起始值
while(j<=index_table[i].end && a[j]!=key) //在确定的块内进行顺序查找
j++;
if(j>index_table[i].end) //如果大于块范围的结束值,则说明没有要査找的数,j置0
j = 0;
return j;
}
int main()
{
int i,j=0,k,key,a[16];
printf("请输入15个数:\n");
for(i=1;i<16;i++)
scanf("%d",&a[i]); //输入由小到大的15个数
for(i=1;i<=3;i++)
{
index_table[i].start = j+1; //确定每个块范围的起始值
j = j+1;
index_table[i].end = j+4; //确定每个块范围的结束值
j = j+4;
index_table[i].key = a[j]; //确定每个块范围中元素的最大值
}
printf("请输入你想査找的元素:\n");
scanf("%d",&key); //输入要查询的数值
k = block_search(key,a); //调用函数进行杳找
if(k!=0)
printf("查找成功,其位置是:%d\n",k); //如果找到该数,则输出其位置
else
printf("查找失败!"); //若未找到,则输出提示信息
return 0;
}
总结优缺点
优点:无需进行大量移动。
缺点:要增加一个索引表的存储空间并对初始化索引表进行排序运算。
适用情况:如果线性既要快速查找又经常动态变化,则可采用分块查找。
时间复杂度 O(logn)
本文详细介绍了算法的时间复杂度,包括时间频度、时间复杂度的概念以及如何根据时间频度求复杂度。文章通过实例展示了如何分析算法执行次数并推算时间复杂度,同时讲解了常见时间复杂度的比较。此外,还提到了几种基础排序算法的时间复杂度及其优缺点。

被折叠的 条评论
为什么被折叠?



