文章目录
前言
(1)在本章的学习此前,需要复习前一章的内容,动手敲一遍解题。上一章讲的是冒泡排序算法,我在回顾的时候重新敲了一遍重新,就发了一些问题,改Bug改了一个下午,虽然时间有点久,毕竟是刚开始学,但是我也更深刻的理解了冒泡排序的算法原理,也对它有了更深的研究,也明白了学算法一定要把算法的原理学透,同时自己也要思考这个算法还有没有其他方法可以实现。
(2)上一节的冒泡排序可以说是我们学习的第一个真正的算法,并且解决了桶排序浪费空间的问题,但是在算法的执行效率上牺牲了很多,它的时间复杂度达到了O(N^2)。那有没有既不浪费空间,又可以快一点的排序算法呢?那就是"快速排序"。
本章的学习目标:
(1)回顾冒泡排序的一个小问题
(2)学习快速排序原理,递归
(3)会用快速排序联系实际生活,解决编程问题
复习冒泡排序时遇到的问题
(1)我们昨天写的冒泡排序是从数组的第1位开始用的,我自己习惯从数组的第0位开始用,所以在我自己编写代码的时候,按照冒泡排序的思路写完编译,我发现有问题。
(2)最后通过自己演算了一遍计算过程,发现从第0位开始写冒泡排序,代码会有点不一样,花时间研究了一遍,这让我更加理解了冒泡排序。
(2)所以说,问题是自己发现的,解决了自己的问题,会掌握得更透彻。
从数组的第0位开始使用数组
#include <stdio.h>
int main()
{
int a[100], i, j, t, n;
scanf("%d", &n);
for (i = 0; i < n; i++)
scanf("%d", &a[i]);
for(i = 0;i < n - 1;i++)
{
for (j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;
}
}
}
for (i = 0; i < n; i++)
printf("%d ",a[i]);
return 0;
}
对比昨天的代码,从数组的第1位开始使用数组
#include <stdio.h>
int main()
{
int a[100], i, j, t, n;
scanf("%d", &n);
for (i = 1; i <= n; i++)
scanf("%d", &a[i]);
//冒泡排序的核心部分
for (i = 1; i <= n - 1; i++)
{
for (j = 1; j <= n - i; j++)
{
//a[j] > a[j + 1]------从小到大排序;a[j] < a[j + 1]------从大到小排序
if (a[j] > a[j + 1]) //若a[j] > a[j + 1],则a[j] 与 a[j + 1]调换位置,即大的放数组后面,故实现从小到大排序
{
t = a[j]; a[j] = a[j + 1]; a[j + 1] = t;
}
}
}
//数组内的数据已排好序,这一步按顺序打印出数组内的数值
for (i = 1; i <= n; i++) //若i = 1; 则i <= n;要加等号
printf("%d ", a[i]);
return 0;
}
可以发现,冒泡排序的核心部分代码的第三行代码有些不一样。
快速排序
(1)每次排序的时候设置一个基准点,将小于基准点的数全部放到基准点的左边,将大于基准点的数全部放到基准点的右边。这样每次交换的时候就不会像冒泡排序一样只能在相邻的数之间进行交换,交换的距离就大得多了。因此次数就少了,速度自然就提高了。
(2)快速排序之所以比较快,是因为相比冒泡排序,每次交换都是跳跃式的。
(3)当然在最坏的情况下,仍可能是相邻的两个数进行了交换。
(4)因此,快速排序的最差时间复杂度和冒泡排序是一样的,都是O(N^2),它的平均时间复杂度是O(NlogN)。
(5)其实冒泡排序是基于一种叫做"二分"的思想,我们后面还会遇到"二分"的思想。
#include <stdio.h>
/*****************
*a[101]用于存放要排序的数组
* n是要排序的个数
*****************/
int a[101], n; //全局变量,这两个变量需要在子函数中使用
/*****************
* 函数名:quicksort
* 形参:int left, int right
* 返回值:无
* 作用:快速排序算法
void quicksort(int left, int right) // left一直都是1
{
int i, j, t, temp;
if (left > right)
return;
temp = a[left]; //temp中存的就是基准数
i = left;
j = right;
while (i != j) //相遇后跳出循环
{
//顺序很重要,要先从右往左找
while (a[j] >= temp && i < j)
j--;
//再从左往右找
while (a[i] <= temp && i < j)
i++;
//交换两个数再数组中的位置
if (i < j) //当哨兵i和哨兵j没有相遇时
{
t = a[i];
a[i] = a[j];
a[j] = t;
}
}
//最终将基准数归位
//若相遇,一定是在一个小于基准数的数相遇,将相遇时的数作为基准数进行下一轮的"探测"
a[left] = a[i];
a[i] = temp;
quicksort(left,i-1); //继续处理左边的,这里是一个递归的过程
quicksort(i + 1, right); //继续处理右边的,这里是一个递归的过程
}
int main()
{
int i, j, t;
//读入数据
scanf("%d",&n);
for (i = 1; i <= n; i++)
scanf("%d",&a[i]);
quicksort(1,n); //快速排序调用
//输出排序后的结果
for (i = 1; i <= n; i++)
printf("%d ",a[i]);
return 0;
}
大致思路就是
开始找,找到了就换,换完了再找再换,直到排序结束。
提问,为什么快速排序,要从边开始找元素?
- 快速排序我们每次递归要达成的效果是在基准数左侧的数都比基准数小,在基准数右侧的数都比基准数大
- 那我们再来看,左右指针是如何运动的:左指针向右运动,直到碰见比基准数大的数停下,右指针向左运动,直到碰见比基准数小的数停下
- 问题就出现在这里,我们最后是要把相遇位置处的数和基准位置处的数字相交换,而我们一直都把第一个数字作为我们的基准数(也就是该区域的最左侧),所以我们要保证交换来的数字比基准数要小
- 那么只有先进行右指针的运动,才可以保证在相遇处的数字小于基准数。这是一个:一边先动,才相遇的过程。为确保相遇的时候所在数比基准数小,所以,先从右边开始找元素