周末,上午10点,一阵猛烈的敲门声将我从睡梦中吵醒,打开门一看,原来是表弟来了,突然想到昨晚微信和姨妈聊天中提起的最近表弟学习遇到了困难,身为祖国栋梁的我自然自告奋勇,扛起拯救表弟学业的大梁
班级前五的表弟 : 表哥表哥,最近上了大学之后发现算法题目好难啊,做很久都不会做,感觉自己不是学习的料
我想起了自己当初天天打游戏,从来没有为学习发过愁(因为压根没有好好学习的想法)的自己, 语重心长的对表弟说 : 现在你的水平确实比当初的我弱一些,不过没事,有我在,帮你拿到专业第一的宝座
只见我微微一笑,拿出了珍藏已久的算法秘籍----算法导论
。。。
话不多说,开肝
时间复杂度分析
n: 表示数据规模
O(f(n)): 表示运行算法所需要执行的指令数,和f(n)成正比
class Demo{
public static void main(String[] args){
int a = 2;
int b = 3;
// 这个算法便是O(n)级别的,所需要的指令数就是a * n, f(n) = n
// a 是一个常数,意味着当我们的n无限扩大的时候,影响性能的终究是f(n),常数对算法的影响可以忽略不记
for(int i = 0; i < n; i ++){
a += 3;
b -= 1;
}
}
}
这个时候,拿一张表可能会更加的直观一些
n | 算法A : O(10000 * n) | 算法B : O(10 * n^2) |
---|---|---|
10 | 10 ^ 5 | 10 ^ 3 |
1000 | 10 ^ 7 | 10 ^ 7 |
10^6 | 10 ^ 10 | 10 ^ 13 |
可以看的出来,时间复杂度O衡量的是一个量级的概念,这种量级的概念就是当n突破了一个数量级的时候,时间复杂度高的算法一定比时间复杂度低的算法更加的耗损性能,并且n越高,这种效果越明显
空间复杂度
顾名思义,这个指标是对我们的算法所需内存的大小进行衡量
这个概念理解起来不难,看例子
// 打印函数
void printArr(int* p, int length) {
for (int i = 0; i < length; i++) {
printf("%d ", *p);
p += 1;
}
}
void demo() {
int n = 0;
scanf("%d", &n);
int arr[n]; // 算法所占用的空间和变量n有关系,所以空间复杂度是O(n)级别的,以此类推
for (int i = 0; i < n; i ++) {
arr[i] = i;
}
printArr(arr, n);
};
来一个题
相比关于时间复杂度和算法复杂度的讲解,大家已经耳濡目染,看的腻了,这时候,就来一道算法题,开开荤
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
例如输入 [0, 1,2,0,2, 1, 2, 0, 0], 输出 [0, 0, 0, 0, 1, 1, 2, 2, 2]
普通做法
// 计数排序. 不了解计数排序?没关系,下一篇就是讲解我们大学中学过的各种高排序的
// num 就是一个待排序的数组
void demo(int nums[], int length) {
// 声明一个桶
int bucket[3] = {0};
// 初始化桶
for (int i = 0; i < length; i ++) {
bucket[nums[i]] ++;
}
// 对因为题目要求原地操作,所以需要对nums进行赋值
int count = 0;
for (int i = 0; i < bucket[0]; i ++) {
nums[count ++] = 0;
}
for (int i = 0; i < bucket[1]; i ++) {
nums[count ++] = 1;
}
for (int i = 0; i < bucket[2]; i ++) {
nums[count ++] = 2;
}
printArr(nums, length);
};
眼前一亮的做法. — 三路快排
上面的做法中,我们不仅使用了额外的存储空间,还将nums数组遍历了两次,那么,有没有更优的解法呢?请看大屏幕
void demo(int nums[], int length) {
//0 0 2 1 2
int zero = -1; // 保证闭区间[0..zero]中为元素0
int two = length; // 保证闭区间[two...length-1]为元素1
for (int i = 0; i < two;) { // 当 i 与 two 重合的时候,就代表元素已经大多数有序了
if (nums[i] == 1) {
i ++;
}else if (nums[i] == 0){
swap(&nums[++zero], &nums[i ++]); // 交换两者的位置
}else if (nums[i] == 2){
swap(&nums[--two], &nums[i]);
}
}
printArr(nums, length);
};
以上的做法便是实现了真正的原地操作,是不是非常的 死高音呢
聊聊算法面试
相信有很大一部分人恶补算法的目的是为了算法面试,我还很年轻😊,没有太多的职场经历,但是我可以把以前受前辈老师指导的并且觉得挺有用的话分享给大家
考察算法的面试题目,不要太在乎正确性,更加重要的是他的思考过程;“正确”本身是一个相对的概念,算法面试并不是高考,可以将这个过程当作是在和面试官在一起探讨一个问题的解决方案, 当我们将自己置身于这个环境中时,就会掌握主动性,对于问题的细节和它的应用环境,便会和面试官进行沟通,从而展现出比较高的素质
举个例子,这里有一个面试题目 : 给一组数据进行排序
大多数小伙伴一看到这个题目,顿时热泪盈眶,感觉offer已经到手,随手写一个上学期间熟练无比的快速排序(nlogn),并熟练的将它和其他的算法的比较告诉了面试官,但这就够了吗?
我们忽略了问题的应用环境,这个时候,我们可以和面试官先进行一系列的沟通
- 这组数据具有什么样的特征?
- 有没有可能包含大量重复的元素(如果有,那么三路快排可能会更加的好(后面会介绍))?
- 是否大多数的数据距离它的正确位置很近?换句话说,原数据已经近乎有序了
- 是否需要稳定排序?(如果需要,那么快速排序可能就会不太合适,归并排序可能会更好一点)
- 数据的存储方式
数据是用链表存储的吗?
数据大小是否可以全部装在内存当中?(如果数据量很大的话,可以使用外排序算法)
综上所述,在某一些的场景下,快速排序并不是最优解甚至某些情况快排根本不会适用
小贴士
在日常的开发中,我们无时无刻都在面临着数据结构和算法的使用,这里,我给大家提两条思路:
**算法三部曲: **
- 第一步暴力解法,看看最坏的时间复杂度和空间复杂度
- 无效操作剔除,将你的代码中的无效的计算或者存储优化优化
- 时空转换, 看看有没有合适的数据结构进行替换,从而可以使用空间复杂度来替换时间复杂度
**数据结构三部曲: **
- 判断这些代码使用了哪些操作
- 这些操作中哪些对于时间复杂度的影响最大,最能影响我们的效率
- 哪种数据结构可以优化我们的使用效率
在之后的算法系列博客中,我不仅仅只是将代码写出来,还会和大家一起分享关于问题的所有的思考路径,也欢迎和我一起探讨的小伙伴~