目录
一. 算法的复杂度
1. 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。
2. 因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
3. 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
4. 在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
二. 时间复杂度
1. 概念
1. 时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。
2. 一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。
3. 但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。
4. 一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模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); }
Func1 执行的基本操作次数 :F(N) = N*N + 2*N + 10
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法:O(N*N)。
2. 大O的渐进表示法
1. 用常数1取代运行时间中的所有加法常数。
2. 在修改后的运行次数函数中,只保留最高阶项。
3. 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
另外有些算法的时间复杂度存在最好、平均和最坏三种情况,在实际中一般情况关注的是算法的最坏运行情况。
3. 实践练习
3.1 练习1
计算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); }
答:O(N),可以去掉系数。
3.2 练习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); }
答:O(M+N),因为这里M与N并没有关联关系,所以都不能省略。
3.3 练习3
计算Func4的时间复杂度?
void Func4(int N) { int count = 0; for (int k = 0; k < 100; ++ k) { ++count; } printf("%d\n", count); }
答:O(1),这里实际执行了一百次,是常数次,用O(1)代表常数次。
3.4 练习4
计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n-1; while (begin < end) { int mid = begin + ((end-begin)>>1); if (a[mid] < x) begin = mid+1; else if (a[mid] > x) end = mid; else return mid; } return -1; }
答:O(logN),以二为底但是被省略了,微分查找的思想就是每次找完缩小一半区间,N/2/2/2/2.../2 = 1,假设有x次,那么N = 2^x,x = log2N(2是底数)。
3.5 练习5
计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N) { if(0 == N) return 1; return Fac(N-1)*N; }
答:O(N),因为N一直到0才返回1,所以一共调用了N+1次,每次执行1次,把所有加起来。
三. 空间复杂度
1. 概念
1. 空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用额外存储空间大小的量度 。
2. 空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
3. 空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
4. 注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
2. 实践练习
2.1 练习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; } }
答:O(1),因为只有常数个变量。
2.2 练习2
计算Fibonacci的空间复杂度?
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; }
答:O(N),因为额外开了n个空间。
2.3 练习3
计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N) { if(N == 0) return 1; return Fac(N-1)*N; }
答:O(N),每次调用是常数级别的空间复杂度,一共调用N次,所以将所有加起来。
2.4 练习4
计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
答:O(N),因为递归的调用不是同时展开的,是先往第一个深处走,走到低再返回。空间可以重复利用。
四. 编程题练习
1. 消失的数字
思路:异或,将0到n全部异或,再和数组所有元素异或,得出剩下那个数。
int missingNumber(int* nums, int numsSize){ int ret = 0, i; for(i=0; i<numsSize; i++) ret ^= nums[i] ^ i; ret ^= i; return ret; }
2. 轮转数组
思路:三次逆置
void reverse(int* nums, int left, int right) { while(left < right) { int tmp = nums[left]; nums[left++] = nums[right]; nums[right--] = tmp; } } void rotate(int* nums, int numsSize, int k) { int key = numsSize - k%numsSize; reverse(nums, key, numsSize-1); reverse(nums, 0, key-1); reverse(nums, 0, numsSize-1); }
思路2:用空间换时间,借用一个数组交换内容。
void rotate(int* nums, int numsSize, int k) { k %= numsSize; int* tmp = (int*)malloc(sizeof(int)*numsSize); memcpy(tmp, nums+(numsSize-k), sizeof(int)*(k)); memcpy(tmp+k, nums, sizeof(int)*(numsSize-k)); memcpy(nums, tmp, sizeof(int)*numsSize); free(tmp); }