📚最快最简单的排序算法——桶排序
🔐举个栗子:
期末考试完了老师要将同学们的分数按照从高到低排序。小哼的班上只有5个同学,这5个同学分别考了5分、3分、5分、2分和8分,哎考得真是惨不忍睹(满分是10分)。接下来将分数进行从大到小排序,排序后是85532。你有没有什么好方法编写一段程序,让计算机随机读入5个数然后将这5个数从大到小输出?
💡分析
首先我们需要申请一个大小为11的数组int a[11]。OK,现在你已经有了11个变量,编号从a[0] ~ a[10]。刚开始的时候,我们将a[0] ~ a[10]都初始化为0,表示这些分数还都没有人得过。例如 a[0]等于0就表示目前还没有人得过0分,同理a[1]等于0就表示目前还没有人得过1分……a[10]等于0就表示目前还没有人得过10分。
下面开始处理每一个人的分数,第一个人的分数是5分,我们就将相对应的a[5]的值在原来的基础增加1,即将a[5]的值从0改为1,表示5分出现过了一次。
同理第二个人的分数为3,我们就把相对应的 a[3]的值在原来的基础上增加1,即将a[3]的值从0改为1,表示3分出现过了一次。
注意此时第三个人的分数也是5分,所以 a[5]的值需要在此基础上再增加1,即将a[5]的值从1改为2,表示5分出现过了两次。
按照刚才的方法处理第四个和第五个人的分数。最终结果就是下面这个图啦。
你发现没有,a[0]~a[10]中的数值其实就是0分到10分每个分数出现的次数。接下来,我们只需要将出现过的分数打印出来就可以了,出现几次就打印几次,具体如下。
a[0]为0,表示“O”没有出现过,不打印。
a[1]为0,表示“1”没有出现过,不打印。
a[2]为1,表示“2”出现过1次,打印 2。
a[3]为1,表示“3”出现过1次,打印 3。
a[4]为0,表示“4”没有出现过,不打印。
a[5]为2,表示“5”出现过2次,打印5 5。
a[6]为0,表示“6”没有出现过,不打印。
a[7]为0,表示“7”没有出现过,不打印。
a[8]为1,表示“8”出现过1次,打印 8。
a[9]为0,表示“9”没有出现过,不打印。
a[10]为0,表示“10”没有出现过,不打印。
最终屏幕输出“23558”。
🔑Java代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int a[] =new int[11];
int k;
for(int i=0;i<=10;i++){
a[i]=0; //初始化为0
}
for(int i=0;i<5;i++){ //读入五个数
k=sc.nextInt(); //把每一个数读到变量k中
a[k]++; //进行计数
}
for(int i=10;i>=0;i--){ //依次判断a[10]-a[0](题目要求从大到小输出)
for(int j=i;a[j]>0;a[j]--){ //出现几次就打印几次
System.out.print(i+" ");
}
}
}
}
输入:
5 3 5 2 8
输出:
8 5 5 3 2
📒时间复杂度
该算法的时间复杂度是O(m+n+m+n)即O(2*(m+n))。我们在说时间复杂度的时候可以忽略较小的常数,最终桶排序的时间复杂度为O(m+n)。还有一点,在表示时间复杂度的时候,n和m通常用大写字母即 O(M+N)。
📒说明
这是一个非常快的排序算法。桶排序从1956年就开始被使用,该算法的基本思想是由E.J.Issac和R.C.Singleton提出来的。其实这并不是真正的桶排序算法,真正的桶排序算法要比这个更加复杂。我们目前学习的简化版桶排序算法,简化版的桶排序不仅仅有上一节所遗留的问题,更要命的是:它非常浪费空间!例如需要排序数的范围是0 ~ 2100000000之间,那你则需要申请2100000001个变量,也就是说要写成int a[2100000001]。因为我们需要用2100000001个“桶”来存储0~2100000000之间每一数出现的次数。
📒问题的引出:
简化版桶排序算法本质上还不能算是一个真正意义上的排序算法。为什么呢?例如遇到下面这个例子就没辙了。
现在分别有5个人的名字和分数: huhu 5分、 haha 3分、xixi 5分、hengheng 2分和gaoshou8分。请按照分数从高到低,输出他们的名字。即应该输出gaoshou. huhu, xixi、 haha、henghengo发现问题了没有?如果使用我们刚才简化版的桶排序算法仅仅是把分数进行了排序。最终输出的也仅仅是分数,但没有对人本身进行排序。也就是说,我们现在并不知道排序后的分数原本对应着哪一个人!这该怎么办呢?不要着急,请看下面——冒泡排序。
📚冒泡排序
💡基本思想
冒泡排序的基本思想是:每次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。
💡思想分析
🔐举个栗子:
将12 35 99 18 76这5个数进行从大到小的排序。
既然是从大到小排序,也就是说越小的越靠后,你是不是觉得我在说废话,但是这句话很关键(∩_∩)。
🌱第一趟:
首先比较第1位和第2位的大小,现在第1位是12,第2位是35。发现12比35要小,因为我们希望越小越靠后嘛,因此需要交换这两个数的位置。交换之后这5个数的顺序是:
按照刚才的方法,继续比较第2位和第3位的大小,第2位是12,第3位是99。12比99要小,因此需要交换这两个数的位置。交换之后这5个数的顺序是:
根据刚才的规则,继续比较第3位和第4位的大小,如果第3位比第4位小,则交换位置。交换之后这5个数的顺序是:
最后,比较第4位和第5位。4次比较之后5个数的顺序是:
经过4次比较后我们发现最小的一个数已经就位(已经在最后一位,请注意12这个数的移动过程),是不是很神奇。现在再来回忆一下刚才比较的过程。每次都是比较相邻的两个数,如果后面的数比前面的数大,则交换这两个数的位置。一直比较下去直到最后两个数比较完毕后,最小的数就在最后一个了。就如同是一个气泡,一步一步往后“翻滚”,直到最后一位。所以这个排序的方法有一个很好听的名字“冒泡排序”。
下面我们将继续重复刚才的过程,将剩下的4个数一一归位。
🌱第二趟:
现在开始“第二趟”,目标是将第2小的数归位。首先还是先比较第1位和第2位,如果第1位比第2位小,则交换位置。交换之后这5个数的顺序是:
接下来你应该都会了,依次比较第⒉位和第3位,第3位和第4位。注意此时已经不需要再比较第4位和第5位。因为在第一趟结束后已经可以确定第5位上放的是最小的了。第二趟结束之后这5个数的顺序是:
🌱第三趟:
第三趟之后这5个数的顺序是:
🌱第四趟:
现在到了最后一趟“第四趟”。有的同学又要问了,这不是已经排好了吗?还要继续?当然,这里纯属巧合,你若用别的数试一试可能就不是了。
💡冒泡排序的原理
📝每一趟只能确定将一个数归位。即第一趟只能确定将末位上的数(即第5位)归位,第二趟只能将倒数第2位上的数(即第4位)归位,第三趟只能将倒数第3位上的数(即第3位)归位,而现在前面还有两个位置上的数没有归位,因此我们仍然需要进行“第四趟”。
“第四趟”只需要比较第1位和第2位的大小。因为后面三个位置上的数归位了
📝现在第1位是99,第2位是76,无需交换。这5个数的顺序不变仍然是99 76 35 18 12。到此排序完美结束了,5个数已经有4个数归位,那最后一个数也只能放在第1位了。
⭐️总结
如果有 n 个数进行排序,只需将n-1个数归位,也就是说要进行n-1趟操作。而“每一趟”都需要从第 1 位开始进行相邻两个数的比较,将较小的一个数放在后面,比较完毕后向后挪一位继续比较下面两个相邻数的大小,重复此步骤,直到最后一个尚未归位的数,已经归位的数则无需再进行比较(已经归位的数你还比较个啥?,浪费表情)。
🔑Java代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int[] a=new int[100];
int n=sc.nextInt(); //输入一个数n,表示接下来有n个数
int t;
for(int i=1;i<=n;i++){ //循环读入n个数到数组a中
a[i]=sc.nextInt();
}
//冒泡排序的核心部分
for(int i=1;i<=n-1;i++){ //n个数排序,只用进行n-1趟
for(int j=1;j<=n-i;j++){ //从第1位开始比较直到最后一个尚未归位的数,
// 想一想为什么到n-i就可以了。
if(a[j]<a[j+1]){ //比较大小并交换
t=a[j];
a[j]=a[j+1];
a[j+1]=t;
}
}
}
for(int i=1;i<=n;i++){ //输出结果
System.out.print(a[i]+" ");
}
}
}
运行结果:
输入:
10
1 2 3 4 5 6 7 8 9 10
输出:
10 9 8 7 6 5 4 3 2 1
现在我们就可以来解决之前桶排序留下的问题了
现在分别有5个人的名字和分数: huhu 5分、 haha 3分、xixi 5分、hengheng 2分和gaoshou8分。请按照分数从高到低,输出他们的名字。
🔑代码如下:
import java.util.Scanner;
class Student{
String name;
int score;
}
public class Main {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
Student[] s=new Student[100];
for (int i = 0; i < 100; i++){
s[i]=new Student(); //注意这一步,初始化每个数组
}
Student s2;
int n=sc.nextInt();
for(int i=0;i<n;i++){
s[i].name=sc.next();
s[i].score=sc.nextInt();
}
for(int i=0;i<n-1;i++){
for(int j=0;j<n-i;j++){
if(s[j].score<s[j+1].score){
s2=s[j];
s[j]=s[j+1];
s[j+1]=s2;
}
}
}
for(int i=0;i<n;i++){
System.out.println(s[i].name);
}
}
}
运行结果:
输入:
5
huhu 5
haha 3
xixi 5
hengheng 2
gaoshou 8
输出:
gaoshou
huhu
xixi
haha
hengheng
📒时间复杂度
冒泡排序的核心部分是双重嵌套循环。不难看出冒泡排序的时间复杂度是 O(N*N)。这是一个非常高的时间复杂度。
📚快速排序
💡基本思想
快速排序(Quick Sort)的基本思想是:
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
💡思想分析
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这 10 个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,这就是一个用来参照的数,待会儿你就知道它用来做啥了)。为了方便,就让第一个数 6 作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在 6 的右边,比基准数小的数放在 6 的左边,类似下面这种排列。
方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于 6 的数,再从左往右找一个大于 6 的数,然后交换它们。这里可以用两个变量 i 和 j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵 i”和“哨兵 j”。刚开始的时候让哨兵 i 指向序列的最左边(即 i=1),指向数字 6。让哨兵 j 指向序列的最右边(即 j=10),指向数字 8。
首先哨兵 j 开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵 j 先出动,
这一点非常重要(请自己想一想为什么)。哨兵 j 一步一步地向左挪动(即 j- -),直到找到一个小于 6 的数停下来。接下来哨兵 i 再一步一步向右挪动(即 i++),直到找到一个大于 6的数停下来。最后哨兵 j 停在了数字 5 面前,哨兵 i 停在了数字 7 面前。
现在交换哨兵 i 和哨兵 j 所指向的元素的值。交换之后的序列如下:
到此,第一次交换结束。接下来哨兵 j 继续向左挪动(再次友情提醒,每次必须是哨兵
j 先出发)。他发现了 4(比基准数 6 要小,满足要求)之后停了下来。哨兵 i 也继续向右挪动,他发现了 9(比基准数 6 要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:
第二次交换结束,“探测”继续。哨兵 j 继续向左挪动,他发现了 3(比基准数 6 要小,
满足要求)之后又停了下来。哨兵 i 继续向右移动,糟啦!此时哨兵 i 和哨兵 j 相遇了,哨兵 i 和哨兵 j 都走到 3 面前。说明此时“探测”结束。我们将基准数 6 和 3 进行交换。交换之后的序列如下:
到此第一轮“探测”真正结束。此时以基准数 6 为分界点,6 左边的数都小于等于 6,6右边的数都大于等于 6。回顾一下刚才的过程,其实哨兵 j 的使命就是要找小于基准数的数,而哨兵 i 的使命就是要找大于基准数的数,直到 i 和 j 碰头为止。
现在基准数 6 已经归位,它正好处在序列的第 6 位。此时我们已经将原来的序列,以 6 为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列,因为 6 左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理 6 左边和右边的序列即可。
🔑代码如下:
import java.util.Scanner;
public class 快速排序 {
//定义全局变量,这两个变量需要在子函数中使用
static int[] a=new int[101];
static int n;
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
for(int i=1;i<=n;i++){
a[i]=sc.nextInt();
}
quicksort(1,n); //快速排序调用
for(int i=1;i<=n;i++){
System.out.print(a[i]+" ");
}
}
private static void quicksort(int left, int right) {
int i,j,t,temp;//temp中存的就是基准数
if(left>right){
return;
}
temp=a[left];
i=left;
j=right;
while(i!=j){
//顺序很重要,要先从右往左找一个小于 temp 的数,如果没找到j--
while(a[j]>=temp && i<j){
j--;
}
//再从左往右找一个大于 temp 的数,如果没找到i++
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); ;//继续处理右边的,这里是一个递归的过程
}
}
运行结果:
输入:
10
6 1 2 7 9 3 4 5 10 8
输出:
1 2 3 4 5 6 7 8 9 10
📒时间复杂度
快速排序的最差时间复杂度和冒泡排序是一样的,都是 O(N*N),它的平均时间复杂度为 O (NlogN)。
本文参考了《啊哈!算法》