二分查找
int SeqListBinary_Search(int* arr,size_t search_data,size_t size){
int left = 0;
int right =size-1;
int mid;
while(left<=right) {
mid = left+((right-left)>>2);
if(arr[mid]<search_data) {
left = mid + 1;
}
else if(arr[mid]>search_data) {
right = mid -1;
}
else
return mid;
}
return -1;
}
我们在长度为N 的数组里面不断折半查找
第一次查找长度变为 : N/2 N/2^1
第二次查找长度变为 : N/2/2 N/2^2
第二次查找长度变为 : N/2/2/2 N/2^3
…
第K次查找的长度变为: N/2/2/2/…/2 N/2^K
如图
假设我们找到最后一个数据才找到这个元素这时候长度变为了 1,这时候查找的次数为K(注意这时候为最坏的查找情况
就是最后一次才查到)
N/2^K = 1
那么长度和我们的查找K次的关系为 N = 2^K
那么他们之间的关系就是我们熟悉的对数函数关系
那么他的时间复杂度为 O(log N)
递归求阶乘
long long Fac(size_t N) {
if(N == 1) {
return 1;
}
else return Fac(N-1)*N;
}
在我们运行函数时候我们不断递归调用,如何看其时间复杂度
我们只需要计算其单次递归函数中语句的执行次数*递归函数的总次数
在这个函数中 单次递归函数时间复杂度是一个常数 O(1),我们递归调用了 N+1 次 那么他的时间复杂度为O(N)
函数总执行次数为 O(1)*O(N)
综合为O (N)
递归斐波那契
long long Fib(size_t N) {
if(N<2) {
return 1;
}
else Fib(N-1)+Fib(N-2);
}
这个程序初看起来很容易让人看懂,但是这个程序如果值数过大那么程序可能跑一天也跑不出来结果
为什么呢,比如我们要计算Fib(5),那么要计算Fib(5),就需要计算Fib(4)和Fib(3),分开要计算Fib(4)和
Fib(3),依次类推,分推下来,或许数值很小的时候我们可以分推出来,但是当数值过大的时候,
也就是说每个分支都要自己计算数本身到1的斐波那契数列,这样就增加了庞大且冗杂的运算量,
如下图
总结
图中数字代表第5个斐波那契数,它的每一步计算都被分成计算前两个斐波那契数,以此类推。那么这就形成了一颗二叉树,
虽然不是满二叉树,但是我们分析的是最坏时间复杂度,而且只要估算出来递归次数随N增长的趋势即可,
故可以近似将它看成满二叉树,其中的节点数就是计算的次数,也就是复杂度,由公式:节点数=2h-1(h为树的高度)可得O(2n)
如何优化?下面我们会接着说
第二部分
分析递归斐波那契数列的:时间、空间复杂度,并对其进行优化,伪递归优化—>循环优化
三种方式来实现
递归 代码一:
long long Fib(size_t N) {
if(N<3) {
return 1;
}
else Fib(N-1)+Fib(N-2);
}
已经分析过 时间复杂度为O(2^n);
前面已经分析过如何计算递归函数的时空复杂度
我们只需要计算其单次递归函数中语句的执行次数*递归函数的总次数
我们可以从图中看出在我们最大的递归调用次数也不过3次,开辟了4个内存空间, 如 1,2,3 步
调用了fib(2)之后,会立即释放掉,再第四步调用fib(1),fib(1)返回1 之后空间又会立即销毁掉剩余三个内存空间
所以我们虽然在不断的调用,但是开辟的内存空间还是一个常数值,所以空间复杂度
为O(1)
总结:
整个程序执行过程中,最多占用4个函数栈帧空间的大小,设一个函数栈帧空间为C
因此可得知当n=5时,该程序空间复杂度为O(4C)=>O(1)
当求第n个斐波那契数时,程序空间复杂度为O(n-1)C (n是常数)=>O(1)
循环的方法 代码二:
long Fib2(int N) {
int first = 1;
int second = 1;
int last;
int i;
if(N<3) {
return 1;
}
else {
for(i = 3;i<=N;i++) {
last = first+second;
first = second;
second = last;
}
}
return second;
}
循环的方式我们看其时间复杂度为O(N);
空间复杂度为O(1),只定义了常数个变量
第三种方法 尾递归
何为尾递归:
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。
当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
代码如下 :
long Fib3(int first,int second,int N) {
if(N<3) {
return 1;
}
if(N == 3) {
return first+second;
}
else
return Fib3(second,first+second,N-1);
}
时间复杂度为O(N)
空间复杂度在没有优化的时候是O(N),因为还是不断递归调用
如果优化的话,那么平台会把他转为一段循环 那么只是不断在开辟了一个内存单元上做函数调用
所以空间复杂度为O(1);
三段代码比较通俗易懂,和尾递归比较起来,迭代法(程序二)因为不用开辟栈空间,所以这三种方法比较起来迭代法是效率最高的。
我们在解决实际问题的时候,根据实际要求选择合适的方法。