算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
2.1 时间复杂度的概念
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个
函数(数学中带有未知表达式的函数),它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
计算一下这个算法的时间复杂度
// 请计算一下Func1基本操作执行了多少次?void Func1(int N) { int count = 0;//两层循环嵌套外循环执行n次,内循环执行n次,整体计算就是N*N的执行次数for (int i = 0; i < N ; ++ i) { for (int j = 0; j < N ; ++ j) { ++count; }}//2 * N的执行次数for (int k = 0; k < 2 * N ; ++ k){ ++count;}//常数项10int M = 10;while (M--) { ++count; } printf("%d\n", count);}
精确的时间复杂度是N ^ 2 + 2 * N + 10
大O的渐进表示法时间复杂度是O(N ^ 2)
分析:
1、两层循环嵌套外循环执行n次,内循环执行n次,整体计算就是N*N 的执行次数
2、2 * N的执行次数
3、常数项10
根据前面的大o渐进表示法规则,所以最后只保留那项对执行次数影响最大的那一项,时间复杂度就是O(N ^ 2)
计算一下这个算法的时间复杂度
// 计算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 * n + 10
采用大O渐进表示法规则后时间复杂度是O(N)
如果最高阶项存在且不是1,则去除与这个项目相乘的常数,那保留的就会是O(N)
计算一下这个算法的时间复杂度
// 计算Func3的时间复杂度?void Func3(int N, int M){ //执行M次 int count = 0; for (int k = 0; k < M; ++ k) { ++count; } //执行N次 for (int k = 0; k < N ; ++ k) { ++count; } printf("%d\n", count);}
时间复杂度:O(M + N)
假设:
M远大于N --> O(M)
N远大于M --> O(N)
M和N一样大 --> O(M) / O(N)
还是取影响最大的那一项,如果并没有说明M和N的大小关系,那么时间复杂度就是O(M + N)
计算一下这个算法的时间复杂度
// 计算Func4的时间复杂度?void Func4(int N) { int count = 0; for (int k = 0; k < 100; ++ k) { ++count; } printf("%d\n", count);}
时间复杂度:O(1)
循环执行的次数是常数次,常数取1
计算一下这个算法的时间复杂度
// 计算strchr的时间复杂度?const char * strchr ( const char * str, int character ){ while(*str) { if(*str == character) return *str; else str++; }}
时间复杂度:O(N)
最坏情况:遍历到最后才找到或者字符串中压根就没有,O(N)
平均情况:O(N / 2) , 忽略系数项是O(N)
最好情况:一次就找到了,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; } }
精确的时间复杂度:O(N * (N-1) / 2)
冒泡排序的思想是:假设给你N个元素,让你排升序,对N个元素排升序,只需要执行N - 1趟,两两相比较,每趟交换都会将最大的那个元素换到最后面去,这样子一来最大的数就都集中在后面,不就相当于每排序一趟后就少比较一个元素嘛,因为后面已经是最大的了不需要再交换。
通过公式展开后采用大O渐进表示法规则后,时间复杂度:O(N ^ 2)
计算一下这个算法的时间复杂度
// 计算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; }
先来理解二分查找的思想:
二分查找是通过下标来搜索对应的元素值,前提是这个数组是有序的,通过确定中间位置划分左右区间,如果val小于mid那么目标值排除了出现在右边的情况,继续再左边区间查找,确定中间下标,通过中间下标划分左右区间,直到找到,否则一直重复规则直到循环结束,理解了二分查找的思想我们能想到的几种情况
最坏情况:O(log N)
每次确定中间下标,划分左右区间的时候都会除以2,那么
N / 2
N / 4
N / 8
.
.
.
1 //等于1的时候找到了
当找到了,要想知道它的执行次数的时候,通过它的展开就能知道
1 * 2 * 2 * 2 * 2 = N
2 ^ x = N
x = log N (以2为底,N的对数)
平均情况:不考虑
最好情况:O(1)
计算一下这个算法的时间复杂度
// 计算阶乘递归Factorial的时间复杂度?long long Factorial(size_t N) { return N < 2 ? N : Factorial(N-1)*N; }
时间复杂度:O(N)
这个算法,递归的开始条件是当N >= 2,它的结束条件是N < 2,当条件成立的时候会一直递归下去,直达条件为假递归终止才会将结果返回,递归的深度决定的是算法的时间复杂度,
N - 1
N - 2
N - 3
.
.
2
1 //递归结束,递归的层数是N
计算一下这个算法的时间复杂度
// 计算斐波那契递归Fibonacci的时间复杂度?long long Fibonacci(size_t N) { return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);}
时间复杂度:O(2 ^ N)
经典的递归分治算法,读者可以将它看成一个完全二叉树,在它递归层层展开的时候开辟的栈帧看作是一个结点,每创建一个结点都是一次函数调用,那么在脑海中想象出来的场景是这样的,可以看到它的调用次数是 Fib(N) = 2 ^0 + 2 ^1 + 2 ^ 2 + … + 2 ^(n -1) - x
时间复杂度:O(2 ^ N)
计算一下这个算法的空间复杂度
// 计算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),常数个额外的变量
计算一下这个算法的空间复杂度
// 计算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),malloc了N+1个long long大小的空间
计算一下这个算法的空间复杂度
long long Factorial(size_t N) { return N < 2 ? N : Factorial(N-1)*N; }
空间复杂度O(N),之前说时间复杂度的时候讲这个例子是每调用一次这个函数就会开辟一次栈帧,调用了N次
long long Fib(size_t N){ if(N < 3) return else return Fib(N - 1) + Fib(N - 2); }
空间复杂度是O(N)
再之前讲时间复杂度的时候用这个例子,说的是将它看成一个完全二叉树,在它递归层层展开的时候开辟的栈帧看作是一个结点,每创建一个结点都是一次函数调用,当然计算时间复杂度符合这个特性,因为时间是不可复用的,而如果以空间复杂度的角度来计算的话,结果却并不是这样子,因为空间是可以复用的,每次递归调用,当Fib(N-1)递归完了,就会将内存归还给操作系统,进而才递归Fib(N - 2),所以空间复杂度是O(N)
17.04. 消失的数字
链接: [link](javascript:void(0)).
题目描述:
数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
//实现思路四:int missingNumber(int* nums, int numsSize){ int m = 0; int i = 0; while(i < numsSize) { m ^= i; m ^= nums[i++]; } m ^= numsSize; return m;}
旋转数组
链接: [link](javascript:void(0)).
题目描述:
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数
实现思路:
数组元素整体逆置,【0,numsSize - 1】
左区间逆置,【0,k - 1】
右区间逆置,【k,numsize - 1】
//逆置数组void reverse(int* nums1,int *nums2){ while(nums1 < nums2) { int tmp = *nums1; *nums1 = *nums2; *nums2 = tmp; ++nums1; --nums2; } }void rotate(int* nums, int numsSize, int k){ if(k >= numsSize) k %= numsSize; reverse(nums, nums + numsSize - 1); reverse(nums,nums + k - 1); reverse(nums + k,nums + numsSize - 1); }
整体逆置,【0,numsSize - 1】
> 左区间逆置,【0,k - 1】
> 右区间逆置,【k,numsize - 1】
//逆置数组void reverse(int* nums1,int *nums2){ while(nums1 < nums2) { int tmp = *nums1; *nums1 = *nums2; nums2 = tmp; ++nums1; --nums2; } }void rotate(int nums, int numsSize, int k){ if(k >= numsSize) k %= numsSize; reverse(nums, nums + numsSize - 1); reverse(nums,nums + k - 1); reverse(nums + k,nums + numsSize - 1); }