前言:有了C语言的基础知识,但对于一个代码我们可以用算法和数据结构优化,如何优化我们后续再聊。本章我们需要学习衡量优化的标准是什么?我们知道,对于一个程序,衡量其好坏主要有他运行的时间和占用的空间两部分,下面我们来仔细的讨论一下。
1、算法效率
如何衡量一个程序的好坏呢?我们下面来看一段C代码:
long long Fib(int N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
2、时间复杂度
2.1时间复杂度概念
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);
}
计算一下++count语句计算了多少次?其实也就是简单的循环的累加,
答案:f(N)=N^2+2*N+10
N=10时,f(N)=130
N=100时,f(N)=10210
N=1000时,f(N)=1002010
我们发现,主导f(N)的主导值取决于最大指数的项。
2.2大O的渐进表示法
2.3时间复杂度例题
1、我们先看上面出现过的一段代码:
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);
}
这里我们知道了count次数统计为f(N)=N^2+2*N+10,次数的决定效果最大的项是N^2,那么用大O方法表示该段代码的时间复杂度就是O(N^2)
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);
}
count统计的次数是2*N+10,我们知道了用大O表示法要去掉常数项10了,那么
时间复杂度是O(2*N)还是其他的呢?
其实他的时间复杂度是O(N),虽然次数统计的决定项是2*N,但是到决定他数量级的其实只是N的一次方,这里可以忽略系数,所以是O(N).
3、我们继续看例子:
// 计算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(1),那么真的正确吗?
其实这题有两个变量M,N,他们对于时间复杂度的影响是同一级别的,并且M,N的值是不一定的
所以这道题的时间复杂度是O(M+N)
4、继续看起来
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
计算Func4的复杂度。这道题我们明确的知道了循环循环了100次,是一个准确的常数次,所以根据规定,这道题的时间复杂度是O(1)
5、而我们继续往下看一个库函数:
const char* strchr(const char* str, int character);
如果之前没见过这个库函数,这里我们简略讲解一下
通过查询,这个函数的作用是在一个字符串中查找一个字符,若查找成功则返回这个字符所在的地址,如果找不到返回NULL。
那么这个函数的时间复杂度如何?查找是常数次,是O(1)?查找的平均次数理论上是N/2,是O(N/2)?还是其他情况?
其实,根据上面的规定,我们对于这段代码,考虑的是他的最坏情况,正确的标准答案是O(N)!!!
总结:如果一个算法,随着输入不同,时间复杂度不同,我们往往取一个悲观预期。
6、下面我们看一下大家耳熟能详的冒泡排序:
//计算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;
}
}
这道题目我们还是从循环次数来着手,并考虑最坏的情况(这里引用了exchange变量,如果有序程序就会停止,但我们考虑悲观预期):
第一次我们比较并交换N-1个数
第二我们比较并交换N-2个数
.......
最后一次我们比较并交换1个数
累加就是(N-1)+(N-2)+(N-3)+....+2+1
高斯公式计算也就是N*(N-1)/2
他的最高指数项是N^2
因此时间复杂度为O(N^2)
7、我们再来看一下二分查找的时间复杂度:
计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n ;
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(N),或许有的人考虑悲观预期,但是又不会算;有的人不假思索答一个O(N/2)。那么这段代码我们该如何思考呢?
其实我们首先确实要考虑悲观情况,最悲观的情况其实就是这个数找不到,那么我们查找完要经过多少次呢?
我们设找了x次,然后假设我们已经找到了最后一个数的时候,我们反过来想,也就是展开x次,所以x个2相乘等于N,x=log^2 N,因此时间复杂度是O(log^2 N)
可见效率非常高
8、计算一下阶乘的时间复杂度:
计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
下面我们看图解
其实也就是执行了N次递归,所以时间复杂度是O(N).
9、计算一下斐波那契数列的时间复杂度:
请看以下代码:
计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
这里我们画出过程图解,最后一次大致计算了2^N个(右枝比左枝少几项),也就是用了2^N的时间,所以这里的最大数量级就是2^N,可以理解为2^N最主要决定这个函数运行时间,时间复杂度就是O(2^N)。
这道题目的时间复杂度太大,所以这个递归的算法除了简易,实际上用处不大,N=50时计算所花费的时间已经不可想象了,指数式的爆炸增长。
3、空间复杂度
有了上面的学习,那么介绍空间复杂度就容易多了。现在随着硬件的升级,大部分题目其实对空间要求不严格了,而更多地采用空间换时间的做法,ok,小提一句,我们下面开始介绍空间复杂度即应用。
3.1空间复杂度概念
3.2空间复杂度例题
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(2^N),其实是不对的。我们来深入详解一下。
对于空间复杂度,我们先看的就是空间了,我们看这道题申请了什么空间,除了变量本身申请的空间外,额外申请的空间就是end和i的空间,常数个空间,因此空间复杂度就是O(1)。
这里大家要学会与时间复杂度区别开。
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));//O(N)
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
这道题我们可以看到一个malloc开辟了n+1个空间,所以数量级是n
因此空间复杂度是O(N)。
但是有的斐波那契额数列的函数并没有malloc开辟空间
计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
我们来借用一下下面的图:
我们知道递归是不断调用自己,调用Fib(5)直到最后符合条件返回值,这道题其实调用到最后是五个栈帧空间,调用后销毁返回,返回后的空间数总是比返回前小(并且空间是可以重复利用的,可以覆盖在之前销毁的空间上),这就知道了空间最大利用的时候是5.
以此类推,空间复杂度为O(N)。
小结:时间不可以重复利用,但是空间可以。
以上就是空间复杂度和时间复杂度的介绍,例题包含了很多种特殊情况,干货满满哦。
我们总结一下,有O(1),O(N),O(N^2),O(2^N)等等种情况,理想可用的代码通常是O(1),O(N)等复杂度小的,而O(2^N)这类指数式增长的对实际操作来说,没有太大的意义,所以我们在学习完本章后,要学会优化算法!
4、复杂度的OJ练习
数组nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
示例 1:
输入:[3,0,1] 输出:2
这道题我们最基本的想法是排序然后用条件语句比对,但是最基本的排序和比对时间复杂度太大,有什么能历遍一遍的方法呢?
我们进一步思考,我么是不是可以创建一个数组,先初始化成0,然后历遍数组把数值为n的数放在a[n]中,然后看数组中还是为初始化0的是哪一个(假设为a[k]),那么少的数就是哪一个(k)。这个方法确实有进步,但是还是完成不了O(N)的时间限制。
这里我们介绍一种比较巧妙的方法:
首先明确三点:1、任何数和本身进行异或运算都为0.因为异或运算是每个比特位进行,相同为0,相异为1。
2、0和任何数异或都还是那个数本身,还是异或特点推出来的。
3、异或运算有交换律,比如1^2^3=2^3^1
纳闷这道题目就可以利用异或的特点,先初始化一个0和这0到numsSize个数异或,再和数组中的数异或,得出的值就是缺少的值!
请看代码:
int missingNumber(int* nums, int numsSize)
{
int num = 0;
int i = 0;
for (i = 0; i <= numsSize; i++)
{
num ^= i;
}
for (i = 0; i < numsSize; i++)
{
num ^= *nums;
nums++;
}
return num;
}
题目二:
力扣https://leetcode.cn/problems/rotate-array/给你一个数组,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
来源:力扣(LeetCode)
- 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
- 你可以使用空间复杂度为
O(1)
的 原地 算法解决这个问题吗?
题解:这道题,我们先想,用最基本的数据结构可以先把最后一个数保存出来,然后前面的数移到后面,再把保存的最后一个数放进第一个位置,右移几次就实行多少次以上的步骤,这一方法可行,空间复杂度可行。
第二种思路是在开辟一块数组空间,然后按要求存放,空间复杂度太大,不可取。
第三种思路是大神发现的,我们来讲解一下:
右移k个数,我们先把前n-k个数倒置,再把后k个数倒置,然后整体倒置,就是右移k个数后得到的结果,是不是很巧妙呢?这里的倒置函数也非常好实现。请看代码:
void reverse(int *arr,int left, int right)
{
int tmp = 0;
while (left < right)
{
tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
void rotate(int* nums, int numsSize, int k)
{
if(k> numsSize)
k = k % numsSize;
reverse(nums, 0,numsSize-k- 1);
reverse(nums , numsSize - k , numsSize - 1);
reverse(nums, 0,numsSize - 1);
}
以上就是时间复杂度和空间复杂度详解,还望大家支持一下!谢谢!