作者简介:苏勇,资深软件工程师。本文选自:拉勾教育《300分钟搞定数据结构与算法》
你好,我是你的算法老师苏勇,欢迎来到今天的学习。在前两节课里,我们学习了面试中常用的以及几个较为复杂的数据结构,它们是学好算法的基石,只有把它们的性质牢牢掌握了,才能在接下来的课程里游刃有余。
算法学习其实是一种学习和提高思维能力的过程。无论是在面试还是实际的工作和生活中,我们都会碰见一些从没遇到过的问题,通常,我们会按照已经拥有的经验来推敲它们,我们也会罗列出各种方案、讨论它们的可行性、风险性,最后选择最佳的解决办法。
这跟我们学习算法的方法是一样的。当面试官提出一个你从来没见过的问题时,不要惊慌,先想想最直观的解法,虽然在大多数情况下,它不会是最佳的办法,但是它能帮助你打通思路,理清解决问题的各个步骤,然后往往我们所需要解决的核心就是对某个步骤进行优化。例如,回顾一下我们在讲前缀树的例题时,我们正是为了要提高匹配字符串的速度才借用了前缀树的。
另外,天下的算法实在太多,我们不可能有足够的时间和精力去一一准备,因此在接下来的五节课里,我把一些在面试中最常考的,也是最核心的算法一一介绍给你,希望你能好好地去学习和消化。
我相信你对排序算法一定不会陌生,也能列举出好几种不同的排序算法,然而在面试准备中,你真正需要熟练掌握的排序算法应该是以下几种:
- 基本的排序算法
冒泡排序(Bubble Sort)
插入排序(Insertion Sort)
- 常考的排序算法
归并排序(Merge Sort)
快速排序(Quick Sort)
拓扑排序(Topological Sort)
- 其他排序算法
堆排序(Heap Sort)
桶排序(Bucket Sort)
在这些排序算法中,冒泡排序和插入排序是最基础的,不要小看了它们,因为这两种算法的思想简单、直接,面试官有时候就喜欢拿它们来考察你的基础知识,并且看看你能不能快速地写出没有bug的代码。在面试中,归并排序、快速排序和拓扑排序的思想是解决绝大部分涉及排序问题的关键,我们将在这节课里重点介绍它们。至于堆排序和桶排序,大家有时间的话一定要看看,尤其是桶排序,在一定的场合中(例如知道所有元素出现的范围时),它能在线性的时间复杂度里解决战斗,掌握好它的解题思想能开阔解题思路,由于时间关系,我们在这里就不对它们进行深入研究了。
下面就让我们对冒泡排序法进行探讨。
冒泡排序(Bubble Sort)
首先让我们来看看冒泡排序的基本思想。
基本思想
给定一个数组,我们把数组里的元素通通倒入到水池中,这些元素将通过相互之间的比较,按照大小顺序一个一个地像气泡一样浮出水面。具体的实现方法就是:每一轮,从杂乱无章的数组头部开始,每两个元素比较大小并进行交换,直到这一轮当中最大或最小的元素被放置在数组的尾部,然后不断地重复这个过程,直到所有元素都排好位置。
可以看到,元素相互比较的过程就是冒泡排序的核心操作,让我们通过一道题目来加深对它的理解吧。
例题分析
给定数组 [2, 1, 7, 9, 5, 8],要求按照从左到右、从小到大的顺序进行排序。我们可以从左到右依次冒泡,把较大的数往右边挪动即可。
首先指针指向第一个数,比较第一个数和第二个数的大小,由于2比1大,所以两两交换,[1,2, 7, 9, 5, 8]。
接下来指针往前移动一步,比较2和7,由于2比7小,两者保持不动,[1, 2, 7, 9, 5, 8]。到目前为止,7是最大的那个数。
指针继续往前移动,比较7和9,由于7比9小,两者保持不动,[1, 2, 7, 9, 5, 8]。现在,9变成了最大的那个数。
再往后,比较9和5,很明显,9比5大,交换它们的位置,[1, 2, 7, 5, 9, 8]。
最后,比较9和8,9比8大,交换它们的位置,[1, 2, 7, 5, 8, 9]。经过第一轮的两两比较,9这个最大的数就像冒泡一样冒到了数组的最后面。
接下来进行第二轮的比较,我们把指针重新指向第一个元素,重复上面的操作,最后,数组变成了:[1, 2, 5, 7, 8, 9]。
可以看到此时数组已经排好序了。但是我们怎么让算法知道呢?我们可以在进行新一轮的比较中,判断一下在上一轮比较的过程中有没有发生两两交换,如果一次交换都没有发生,就证明其实数组已经排好序了。
代码示例
我们来看看冒泡排序的代码。
void sort(int[] nums) {
boolean hasChange = true;
for (int i = 0; i < nums.length - 1 && hasChange; i++) {
hasChange = false;
for (int j = 0; j < nums.length - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
hasChange = true;
}
}
}
}
首先我们定义了一个布尔变量hasChange,用来标记每轮遍历中是否发生了交换。
- 在每轮遍历开始的时候,将hasChange设置为false。
- 接下来,进行两两比较,如果发现当前的数比下一个数还大,那么就交换这两个数,同时记录一下有交换发生。
- 不断地两两交换,直到在这一轮,把最大的数放置到数组的最末端。
算法分析
下面让我们来分析一下冒泡排序算法的空间和时间复杂度。假设数组的元素个数是n,由于在整个排序的过程中,我们是直接在给定的数组里面进行元素的两两交换,空间复杂度是O(1)。至于时间复杂度,我们看看以下几种情况:
- 给定的数组按照顺序已经排好
在这种情况下,我们只需要进行n - 1次的比较,两两交换次数为0,时间复杂度是O(n),这是最好的情况。
- 给定的数组按照逆序排列
在这种情况下,我们需要进行n(n - 1) / 2次比较,时间复杂度是O(n^2),这是最坏的情况。
- 给定的数组杂乱无章
在这种情况下,平均时间复杂度是O(n^2)
由此可见,冒泡排序的时间复杂度是O(n^2),但它是一种稳定的排序算法,所谓稳定,也就是说如果数组里两个相等的数,那么排序前后这两个相等的数的相对位置保持不变。
下一节课,我们将深入学习递归算法和回溯算法,它们在算法面试中出现的概率是最高的。好,我们下节课再见!关注我的公号:IT技术思维,回复:123,可以免费获得大厂面试真题哦~
版权声明:本文版权归属拉勾教育及该专栏作者,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表,违者必究。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。