一.时间复杂度和空间复杂度
在基本的算法里面,一个好的算法不仅要有正确性、可读性、健壮性,还要有效率性,那么衡量一个算法的效率性就是用时间复杂度和空间复杂度来衡量。
在学习时,我们已经知道,判断一个算法的时间复杂度和空间复杂度的优劣只看它们的基本式级数。如对于时间复杂度来说,执行一次和执行10000次每什么区别,只要是常数次,次数不随数据的规模改变而改变,时间复杂度就是O(1);对于空间复杂度来说,只要在程序执行过程中,开辟的额外辅助空间是常数,不随数据规模的变化而改变,那么空间复杂度就是O(1)
在如今的设备水平下,空间往往已经不成为主要考虑因素,因为空间已经什么充裕,我们更注重时间效率。所以在设计算法时,对时间要求特别严格的情况下,可以通过牺牲空间来换取时间。
二.效率性练习
1.旋转比对子字符问题
题目来源https://leetcode.cn/problems/rotate-string/description/

通过几个方法能感受到效率的明显区别
方法1:暴力移动比对
题目说比较两个数组经过若干次旋转后有没有机会相等,我们就先假设它相等,然后去考虑相等需要的条件
- 假设经过K次循环后,s与goal相等。
- 如果s与goal相等,两个数组的长度一定相等。
- 如果s与goal相等,最多循环strlen(s)次。
那么我们就旋转一次,就对比一次

思考,应该如何移动
假设数组s如图(不要在意例子的元素类项,这里只是举例子,不同的类项赋值方式可能有所不同)

我们这样执行,每次移动都先把最后一项临时存储起来,然后从后往前,把前一项的值赋给后一项,最后把最后一项的值赋给第一项,就完成了一次移动,既然这样,我们用一次函数来包装它
void Move(char* s) {
int len = strlen(s);
char temp = s[len - 1];
for (int i = len - 1; i - 1 >= 0; i--) {
s[i] = s[i - 1];
}
s[0] = temp;
}
接下来就是每对比一次,如果不等,就移动一次
void Move(char* s) {
int len = strlen(s);
char temp = s[len - 1];
for (int i = len - 1; i - 1 >= 0; i--) {
s[i] = s[i - 1];
}
s[0] = temp;
}
bool rotateString(char* s, char* goal) {
// 先求两个数组的长度
int len1 = strlen(s);
int len2 = strlen(goal);
// 判断长度是否相等
if (len1 != len2)
return false;
bool OK;
// 逐一比对,移动一次比对一次
for (int i = 0; i < len1; i++) {
OK = true;
for (int j = 0; j < len2; j++) {
if (s[j] != goal[j]) {
OK = false;
break;
}
}
if(OK == true)
return true;
Move(s);
}
return false;
}
测试查看

但是这种暴力移动一次对比一次的方法,时间复杂度为O(N^2),并且代码量比较大。我们来看另一种方法
方法2:角标取模法(自己取的名字)
假设有一个数m模上一个非零数n,m%n的取值范围为(0~n-1);同样的,我们求出S的长度之后,假设移动了i位,原来的j位置经过移动后的位置就为(i+j)%len。
理解上面的原理后,我们本意不是移动,而是从不同的位置开始访问S,并且每次都能实现循环访问,这样,只需要判断在循环访问的过程中是否有相等的情况。

虽然时间复杂度依然为O(n^2),但是代码的明显干净不少。那有没有时间复杂度更优的呢?肯定是有的,我们来看下个方法
方法3:
在本文最初位置时我们就提到,当今硬件已经很强大,空间已经不是问题,我们更注重时间效率,这个时候我们可以通过牺牲一定的空间来换取时间。

我们都知道strcpy、strcat和strstr的时间复杂度均为O(n),所以这个算法的时间复杂度为O(n)
这里我们开辟了一个元素个数为2len1+1的数组,空间复杂度为O(n),使得我们能够更方便去比较两个数组是否相等,这就是一种利用空间换取时间。