赞
踩
作者:旧梦拾遗186
专栏:数据结构成长日记
每日励志:
如果有一天,你的努力配得上你的梦想,那么你的梦想也绝对不会辜负你的努力。
前言:
小编带大家来学习数据结构中的复杂度问题。
目录
如何衡量一个算法的好坏呢?比如对于以下斐波那契数列
long long Fib(int N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?
算法效率分析分为两种: 第一种是时间效率,第二种是空间效率 。时间效率被称为时间复杂度,而空间效率 被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度 , 而空间复杂度主要衡量一个算法所需要 的额外空间 ,在计算机发展的早期,计算机的存储容量小。所以对空间复杂度很是在乎。但是经过计算机 行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法 的空间复杂度。
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个 函数(数学的带未知数的函数表达式) ,它定量描述了该算法的运行时间。 一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。但是我们需要每个算法都上机测试吗? 是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例, 算法中的基本操作的执行次数,为算法 的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
举个例子:(不能纯粹的数循环,要看具体的算法逻辑,进行计算)
// 请计算一下Func1中++count语句总共执行了多少次? void Func1(int N) { int count = 0; for (int i = 0; i < N ; ++ i) { for (int j = 0; j < N ; ++ j) { ++count; } } for (int k = 0; k < 2 * N ; ++ k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }计算次数的时间复杂度函数:n*n+2*n+10;
Func1 执行的基本操作次数 :N = 10 F(N) = 130N = 100 F(N) = 10210N = 1000 F(N) = 1002010实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而 只需要大概执行次数 , 那么这 里我们使用大O的渐进表示法。
大 O 符号( Big O notation ):是用于描述函数渐进行为的数学符号。推导大 O 阶方法:1 、用常数 1 取代运行时间中的所有加法常数。2 、在修改后的运行次数函数中,只保留最高阶项。3 、如果最高阶项存在且不是 1 ,则去除与这个项目相乘的常数。得到的结果就是大 O 阶。使用大 O 的渐进表示法以后, Func1 的时间复杂度为:N = 10 F(N) = 100N = 100 F(N) = 10000N = 1000 F(N) = 1000000通过上面我们会发现大 O 的渐进表示法 去掉了那些对结果影响不大的项 ,简洁明了的表示出了执行次数。另外有些算法的时间复杂度存在最好、平均和最坏情况:最坏情况:任意输入规模的最大运行次数 ( 上界 )平均情况:任意输入规模的期望运行次数最好情况:任意输入规模的最小运行次数 ( 下界 )例如:在一个长度为 N 数组中搜索一个数据 x最好情况: 1 次找到最坏情况: N 次找到平均情况: N/2 次找到在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为 O(N)
// 计算Func2的时间复杂度? void Func2(int N) { int count = 0; for (int k = 0; k < 2 * N ; ++ k) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }
实例2:
- // 计算Func3的时间复杂度?
- void Func3(int N, int M)
- {
- int count = 0;
- for (int k = 0; k < M; ++ k)
- {
- ++count;
- }
- for (int k = 0; k < N ; ++ k)
- {
- ++count;
- }
- printf("%d\n", count);
- }
实例3:
- // 计算Func4的时间复杂度?
- void Func4(int N)
- {
- int count = 0;
- for (int k = 0; k < 100; ++ k)
- {
- ++count;
- }
- printf("%d\n", count);
- }
实例4:
// 计算strchr的时间复杂度? const char * strchr ( const char * str, int character );
另外有些算法的时间复杂度存在最好、平均和最坏情况:最坏情况:任意输入规模的最大运行次数 ( 上界 )平均情况:任意输入规模的期望运行次数最好情况:任意输入规模的最小运行次数 ( 下界 )4. 实例4基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)
实例5:
// 计算BubbleSort的时间复杂度? void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }这是一个冒泡排序算法。我们需要注意,计算时间复杂度时不能只看循环,这种做法是肤浅的、错误的。我们应该关注算法的内核。就拿这个冒泡算法来说,我们通过图解来表达清楚冒泡排序的核心:
可以看到,最好的情况就是执行 N 次(不是 1 次原因是即使不进行元素交换,此算法也会对数组遍历检查)。最差的情况就是计算 1、2、3……N-2、N-1 这个等差数列之和,即 (N-1)*N/2 ,那么要表示时间复杂度,就要用大 O 的渐进表示法,即去除影响不大的项,O(N^2) 。
实例6:
- // 计算BinarySearch的时间复杂度?
- int BinarySearch(int* a, int n, int x) {
- assert(a);
- int begin = 0;
- int end = n-1;
- // [begin, end]:begin和end是左闭右闭区间,因此有=号
- while (begin <= end)
- {
- int mid = begin + ((end-begin)>>1);
- if (a[mid] < x)
- begin = mid+1;
- else if (a[mid] > x)
- end = mid-1;
- else
- return mid;
- }
- return -1; }
二分查找,具体逻辑如图
实例7:
- // 计算阶乘递归Fac的时间复杂度?
- long long Fac(size_t N) {
- if(0 == N)
- return 1;
-
- return Fac(N-1)*N; }
实例8:
// 计算斐波那契递归Fib的时间复杂度? long long Fib(size_t N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
斐波那契数列算法在现实生活中,没什么用,因为其时间复杂度过大。
实例答案及分析:1. 实例 1 基本操作执行了 2N+10 次,通过推导大 O 阶方法知道,时间复杂度为 O(N)2. 实例 2 基本操作执行了 M+N 次,有两个未知数 M 和 N ,时间复杂度为 O(N+M)3. 实例 3 基本操作执行了 10 次,通过推导大 O 阶方法,时间复杂度为 O(1)4. 实例 4 基本操作执行最好 1 次,最坏 N 次,时间复杂度一般看最坏,时间复杂度为 O(N)5. 实例 5 基本操作执行最好 N 次,最坏执行了 (N*(N+1)/2 次,通过推导大 O 阶方法 + 时间复杂度一般看最坏,时间复杂度为 O(N^2)6. 实例 6 基本操作执行最好 1 次,最坏 O(logN) 次,时间复杂度为 O(logN) ps : logN 在算法分析中表示是底数为 2 ,对数为 N 。有些地方会写成 lgN 。(建议通过折纸查找的方式讲解 logN 是怎么计算出来的)7. 实例 7 通过计算分析发现基本操作递归了 N 次,时间复杂度为 O(N) 。8. 实例 8 通过计算分析发现基本操作递归了 2^N 次,时间复杂度为 O(2^N) 。(建议画图递归栈帧的二叉树讲解)
空间复杂度也是一个数学表达式,是对一个算法在运行过程中 临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少 bytes 的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用 大 O 渐进表示法 。注意: 函数运行时所需要的栈空间 ( 存储参数、局部变量、一些寄存器信息等 ) 在编译期间已经确定好了,因 此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
实例1:
// 计算BubbleSort的空间复杂度? void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
实例2:
// 计算Fibonacci的空间复杂度? // 返回斐波那契数列的前n项 long long* Fibonacci(size_t n) { if(n==0) return NULL; long long * fibArray = (long long *)malloc((n+1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; ++i) { fibArray[i] = fibArray[i - 1] + fibArray [i - 2]; } return fibArray; }
实例3:
// 计算阶乘递归Fac的空间复杂度? long long Fac(size_t N) { if(N == 0) return 1; return Fac(N-1)*N; }
实例答案及分析:
1. 实例 1 使用了常数个额外空间,所以空间复杂度为 O(1)2. 实例 2 动态开辟了 N 个空间,空间复杂度为 O(N)3. 实例 3 递归调用了 N 次,开辟了 N 个栈帧,每个栈帧使用了常数个空间。空间复杂度为 O(N)
3.1 消失的数字OJ链接解题思路:所有数相加和减去当前数和就是那个所缺的数
int missingNumber(int* nums, int numsSize){ int sum = 0; for(int i = 0;i<numsSize;i++) { sum+=nums[i]; } int ret = 0; for(int i = 0;i<=numsSize;i++) { ret+=i; } return ret-sum; }
思路一:存放最右边的数,一次右移一个,需要移动K%numsSize次
void rotate(int* nums, int numsSize, int k){ if (k > numsSize) { k %= numsSize; } for (int i = 0; i < k; i++) { int tmp = nums[numsSize - 1]; for (int j = numsSize - 1; j >= 1; j--) { nums[j] = nums[j - 1]; } nums[0] = tmp; } }但是 可以发现运行超时:
思路二:开辟一个新的数组存放需要移动的数,在把剩余的数存放到新的数组中,最后把新数组中的数字赋值给原来的数组
void rotate(int* nums, int numsSize, int k) { if (k >= numsSize) { k %= numsSize; } int* arr = (int*)malloc(sizeof(int) * (numsSize+1)); for (int i = 0; i <k; i++) { arr[k-i-1] = nums[numsSize - i-1]; } for (int i = k; i < numsSize; i++) { arr[i] = nums[i-k]; } for(int i = 0;i<numsSize;i++) { nums[i] = arr[i]; } }思路三:前n-k个逆置,后k个逆置,整体逆置
void right_move(int arr[], int left, int right) { while (left < right) { int tmp = arr[left]; arr[left] = arr[right]; arr[right] = tmp; ++left; --right; } } void rotate(int* nums, int numsSize, int k) { if (k >= numsSize) { k %= numsSize; } right_move(nums, 0, numsSize - 1 - k); right_move(nums, numsSize - k, numsSize - 1); right_move(nums, 0, numsSize - 1); }
结语:
每个人的成长都是能力和想要得到的东西,不断匹配的过程,当你的才华和欲望不匹配时,你就该静下心来学习了,如果小编的总结能对你有所帮助,希望小伙伴们三连加关注哦,你的支持是小编创作的最大动力。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。