前言:
大家好,这是0基础学算法的第一课,当然这个0基础并不是我们常规意义上的0基础,你需要掌握一点语法知识即可学习此课程,夸张点说会敲“Hello World”就行/doge。当然这是一个系列课,我将由简到深的为大家讲解一些算法知识,从排序到搜索以及更多,也希望这个系列可以给你带来帮助。算法学习是个很难的过程,希望你可以在此过程中坚持下来,加油!
附:系列中出现较难的知识点或者语法知识我都会作讲解,如果还有什么不明白的知识可以在下方评论,本人看到后会立即回复滴。
知识讲解:
下面直接开始我们今天所要讲解的知识——快速排序。
快速排序重要性:
快速排序是我们在编程技术中十分常见的一种排序方式,其由于排序效率在同为O(N*logN)的几种排序方法中效率较高,所以经常会被采用,再加上处理快速排序时使用的分治法思想十分实用,这就导致了很多的软件公司(腾讯,微软等知名IT公司)在笔试面试的时候也会很喜欢考这个,还有我们在程序方面的考试(软考等)和一些比赛(PAT,蓝桥杯等)还有考研中都会经常出现他的身影。
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。我们在实现快速排序的时候会采用一种分治的策略,也成为分治法(Divide-and-ConquerMethod)。
分治法的基本思想:
分治法可以通俗的解释为:把一片领土分解,分解为若干块小部分,然后一块块地占领征服,被分解的可以是不同的政治派别或是其他什么,然后让他们彼此异化。
实现步骤:
在我们今天学习的算法中,我们将其分为以下的三个步骤:
- 1.先从数列中取出一个数作为基准数。(确认边界点)
- 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。(重新调整区间)
- 3.再对左右区间重复第二步,直到各区间只有一个数。(递归)
看完上面你也不妨看出我将其简化为:确认边界点-重新调整区间-进行递归。三个步骤去进行实现。在这三个步骤中,要注意的一点是:在进行确认边界点的时候我们要考虑确定的边界点是左边界?右边界?中点?随机?的同时也要考虑取哪个点更方便我们后续的操作。
如上文所说,我们在分区选点的时候会面临着四种选择,在这里我们选择取中间点为我们的分界点,在这里会有人问为什么不取其他的点呢,先说随机点,这个就相对来说比较麻烦,然后左右两个端点为什么不建议取呢,主要是左右两个端点与后面开区间时会有边界值无法通过的现象,我们在举出特殊例子时是可以看出的,在这里我就不为大家列举了,这个知识相对来说没那么重要,我们只需记得取中间点为分界点即可。
在经过第一步的思考后我们在进行分区的操作是也如下图所示:
到这里我们距离实现快速排序也只差了一步,也就是我们的递归,这个递归就是我们就是不断分界,分到最小单元。(当然这个现在看不懂没关系,到后面的代码展示环节时会出现)
举例:
我为大家举一个实例来帮助大家理解。以一个数组作为示例,取区间中间数数为基准数(标红)。
数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
82 | 24 | 88 | 71 | 85 | 94 | 73 | 90 | 86 | 93 |
然后我们将左右两个端点的下标记住,
首先从左边下标开始向右一个一个移动,在移动前要判断当前数字是否>=标记数字,如果没有就继续移动,知道移动到为止。(就比如这个例子,0,1对应的数字都不行,当下标移动到2时停止)
然后我们再对右边下标开始移动并判断其对应数字是否<标记数字,直到移动到为止。(就比如这个例子,9,8对应的数字都不行,当下标移动到7的时停止。
(此时数组下标对应就是这样的)
之后的我们就将左右两个元素交换,交换后继续重复上述操作,直到两个指针相遇停止,也就是如下操作:
交换位置:
数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
82 | 24 | 73 | 71 | 85 | 94 | 88 | 90 | 86 | 93 |
之后继续移动直到相遇:
相遇的时候对应下标为:4 。这个时候明显可见我们数组中0-4的数字都是<=85的,而5-9的数字都是>85的。
所以我们就将数组一分为二(并不是真正意义上的分开),0-4分开进行排序,5-9分开进行排序。
排序的过程也就跟我们上面所描述的一样。
数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
82 | 24 | 73 | 71 | 85 | 94 | 88 | 90 | 86 | 93 |
相信聪明的各位已经看出来了,这里的一分为二就是在同一个数组上分开两次进行上述方法的交换。交换的具体过程我就不细写啦,结果如下:
数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
71 | 24 | 73 | 82 | 85 | 86 | 88 | 90 | 94 | 93 |
然后继续套娃,分开进行:
数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
71 | 24 | 73 | 82 | 85 | 86 | 88 | 90 | 94 | 93 |
数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
24 | 71 | 73 | 82 | 85 | 86 | 88 | 90 | 93 | 94 |
显而易见到这里我们的排序就完成啦,也成功的对这个数组进行了一次排序。
思路:
思路就是对上面步骤以及例子的总结。我之所以放到例子后面附上主要是希望大家通过看例子可以加深对这些的记忆。在这里我直接为大家附上图,希望大家更容易理解一些。
思路一:
思路一就是我们的暴力解法,直接开两个数组,然后找出分界点,将比分界点小以及大的数字分别放入两个数组,最后再归还为原数组。
思路二:
在这里思路二也就是我们上面举例子用的方法,也是我们今天主讲的思路,就是用双指针进行不断靠近。
快速排序实现:
在这里快速排序的实现我就为大家简化为下面几步:
第一步:开一个数组
第二步:输入元素个数
第三步:将每个元素输入
第四步:调用模板
非常的通俗易懂是吧,在这里大家都想知道这个模板是什么,所以我们的重头戏就来了!
模板:
模板的实现我给大家已画图的方式列出,字丑希望大家不要介意。
在这里你可能会疑惑,哎?这不就是刚才讲的那些内容吗,为什么还要再说一遍呢?
我之所以在这里再提到一遍,主要是因为在我们学算法刷题时,不是说只学那个方法就可以了,我们有很多要考虑的东西,有的边缘值了,范围了都需要我们考虑。就比如刚才模板中的1个数不用排序的情况,如果我只给你讲解了前面的知识,你可能会理解快速排序,但是你自己实现起来会有很大的难度,有很多是你很难想象的到的错误,所以大家不要嫌我讲的繁琐。/抱拳
模板的代码实现:
void quick_sort(int q[], int l, int r) { if (l >= r) return; int i = l - 1 ; int j = r + 1 ; int x = q[l + r >> 1] ; while (i < j) { do i ++ ; while (q[i] < x); do j -- ; while (q[j] > x); if (i < j) { swap(q[i], q[j]); } } quick_sort(q, l, j), quick_sort(q, j + 1, r); }
好了,到了这里肯定有hxd会问:l+r>>1是什么意思。我在这直接为大家讲一下:
这里>> 就是二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
简单点来说 x>>1等价于x/2 。
为了方便大家学习,我直接把这两个公式给大家附上,希望大家可以看一下:
· x >> n 等同于 x / (2^n) ,x << n 等同于 x * (2^n)
实战
到这里大家对快速排序已经了解的差不多了,下面看几个简单的题目来巩固一下吧。
第一题:快速排序
这个题目很简单就是对一段长为n的数组,对其进行从小到大的排序后并输出以可。
输入格式:
输入共两行,第一行包含整数 n。 第二行包含 数列中的n 个整数。
输出格式:
输出排序后的结果
数据范围:
1≤n≤1000001≤n≤100000
输入样例:
5 7 2 3 8 10
输出样例:
2 3 7 8 10
讲解:
这就是一个经典的快速排序题,当然你也可以用其他方法,不过我们今天学习的是快排,那就希望大家可以使用快速排序写,这个题目目的很明确,就是进行排序就行,所以我们就在主函数中对他需要的数字进行输入,然后调用我们之前所说的模板就行,大家可以先自己尝试以下,如果解决不了就看一下我下面的AC代码,代码也有讲解,希望你看过之后可以知道为啥自己没写出来的原因。
AC代码:
#include <iostream> using namespace std ; const int N = 100010 ; int a[N] ; void quick_sort(int a[], int l, int r) { if(l>=r) return ; //数组中所含元素为一个或零个时返回 int i = l-1 ; int j = r+1 ; int x = a[l+r>>1] ; /* 这里对左边下标减一以及右边下标加一是为了后面do while语句的执行 因为刚开始都要i++ j-- 所以我们在开始定义的时候就直接先减去, 等后面第一步i++ j--时正好可以从左右两端坐标开始 也不耽误循环的进行 */ while(i<j) //两指针相遇就退出循环 { do i++ ; while(a[i] < x) ; //指针的移动 do j-- ; while(a[j] > x) ; //指针的移动 if(i<j) swap(a[i], a[j]) ; //交换 } quick_sort(a, l, j) ; //递归进行 quick_sort(a, j+1, r) ; } int main() { //按照题目输入 int n ; cin >> n ; for(int i=0 ; i<n; i++) { cin >> a[i] ; } quick_sort(a, 0, n-1) ; //调用模板 for(int i=0; i<n; i++) { cout << a[i] << " " ; } return 0 ; }
第二题:求第 k 小的数
题目描述
输入 n(1 <= n < 5000000)个数字 ai(1 <= ai < 10^9),输出这些数字的第 k 小的数。最小的数是第 0 小。
输入格式
无
输出格式
无
样例
样例输入
5 1 4 3 2 1 5
样例输出
2
讲解:
这个题就是一个小变式了,其实百变不离其总,也是要先对其进行排序,然后从前往后寻找就可以啦。
AC代码:
#include <iostream> using namespace std ; const int N = 5000010 ; int a[N] ; void quick_sort(int a[], int l, int r) { int x = a[l+r>>1] ; int i = l-1 ; int j = r+1 ; if(l>=r) return ; while(i<j) { do i++ ; while(x>a[i]) ; do j-- ; while(x<a[j]) ; if(i<j) swap(a[i],a[j]) ; } quick_sort(a,l,j) ; quick_sort(a,j+1,r) ; } int main() { int n, k ; cin >> n >> k ; for(int i=0; i<n; i++) { cin >> a[i] ; } quick_sort(a, 0, n-1) ; cout << a[k] << endl ; return 0 ; }
显而易见,这个代码也就是第一题代码的一个小变形,把输出变一下就行,所以就不做讲解了。
结尾
好的,到这里我们今天的算法学习就到了尾声,不过大家以后一点要多复习,不让会遗忘的而且模板也要多敲几遍,加深自己的印象。
我的0基础快速学算法将会持续更新,并且会逐步的去将那些算法知识点给大家做一个讲解,并把所以的讲解放到我算法知识的专栏之中,如果大家感兴趣的话可以关注一下我和我的专栏,十分感谢。