赞
踩
本篇文章主要记录学习几种经典的排序算法:桶排序、冒泡排序、快速排序,并且使用C语言来简单的完成这几种排序算法的实例
排序算法是计算机科学中最基本的算法之一,它是一种将一组数据按照特定顺序排列的方法。排序算法的作用是将一个无序的数据集合按照某个规则重新排列,使得这些数据具有一定的有序性质。排序算法可以用于很多应用场景,比如搜索、统计、查找等。
排序算法的基本思想是:通过对数据元素的比较和交换来达到排序的目的。
排序算法的效率通常使用时间复杂度表示,即算法执行所需的时间与数据集大小之间的关系。时间复杂度越小,算法的执行效率越高。因此,针对不同的数据集大小和数据特征,选择适当的排序算法可以帮助我们更高效地处理数据。
介绍:
桶排序最早由美国计算机科学家 Myron H. Tribus 和 Edward C. Voudouris 在1950年代提出。后来,桶排序被计算机科学家们广泛研究和运用,并且被证明是一种简单而高效的排序算法,特别适用于处理大量数据。
实际上,桶排序是一种非常古老的排序方法,它源于人类社会中对物品的分类和组织。在古代,人们就已经使用各种方式将物品按照大小、颜色、形状等规则分类,这些分类方法可以看作是桶排序的前身。随着计算机技术的发展,桶排序也逐渐被引入到计算机科学中,成为了一种重要的排序算法。
一个问题:
假设现在有一个小组考试,CC小组里一共有6个人,分别是Tom、Jack、 Thomas、 Jackson、 Allen、 Mandy,他们的考试成绩满分为10分,成绩依次是9、4、10、9、7、2。现在问题来了如何将这些成绩从大到小的来排序呢?
首先我们研究一下这个问题可以知道,总分是:10分,分数依次为:9、4、10、9、7、2。现在需要我们将分数从大到小来排序。那我们现在假设现在10个桶,每个桶代表了不同的分数。现在有6个人每个人手里有着不同的标签。不同的标签也就代表着不同的分数。现在我们只需要将这些不同的标签放进相对应的桶里面就可以了。
那我们现在就可以想象把我们C语言中的数组比作一个个桶数组的下表也就代表着每个桶对应的不同的分数。那么我们就只需要通过键盘录入不同的成绩,然后拿着这些不同的成绩依次丢入数组中,最后将装有分数的数组下标输出也就代表了分数的输出。
现在我们使用C语言来实现:
#include <stdio.h> // a[i] --> 分数的次数 // i --> 分数 int main(){ int a[11],i,j,t,n; for (i=0;i<=10;i++) { a[i] = 0; // 将所有分数出现的次数初始化为0 } printf("-------------\n"); printf("请输入需要写入1-10个人的成绩个数:"); scanf("%d",&n); for (i=1;i<=n;i++) // 循环读入n个成绩 { printf("输入%d号成绩:",i);scanf("%d",&t); // 输入分数 a[t]++; // 将此分数的次数加一 } printf("------------\n"); printf("成绩从小到大排序:\n"); for (i=10;i>=0;i--) // 循环十一次从0遍历到10 { for(j=1;j<=a[i];j++) //当j的值小于分数的次数的时候打印此分数,j的值不能为0,因为分数出现的次数最小就是0 { printf("%d\n",i); } } return 0; }
分析该程序可以看出:我们首先定义了一个11长度大小的数组,然后将数组的所有元素初始化为0,目的就是让初始时的人还没有得过分,然后我们用户写入了1-10个成绩,然后循环然用户输入这些成绩,并且将输入的成绩在数组对应的下标的值中加一代表该成绩出现了的次数。然后就是输出成绩了。
输出成绩是这样的,我们首先定义第一层for循环,目的是遍历数组的每一个分数对应的次数。然后定义第二层for循环,目的是通过依次比较分数出现的此时是否小于数组中的分数的次数,如果小于,则输出该分数,也就是数组的下标,否则就直接下一轮循环遍历下一个分数的次数的比较了。具体的依次比较就比如下图:
实现的效果如下:
可以看到,将我们输入的成绩从大到小的排序了。那么我们有没有想过几个问题:第一,我们的分数是从大到小的排序了,我们的学生姓名好像却没有排序,或者说用这套桶排序好像没有办法将姓名和分数一起排序;第二,如果最大分数有多大我们的数组就要定义一个多大的数组,是不是空间资源占用的太大了。可以看到桶排序的缺陷很严重的。那么接下来我们来尝试另一种排序算法以此来解决问题。。。
介绍:
什么是冒泡排序算法?冒泡排序算法就是将相邻的元素两两比较,根据比较结果交换位置,从而实现排序。冒泡排序的名字来源于排序过程中较小或较大的元素会像气泡一样逐渐“浮起”或“沉下”,从而达到排序的目的。
冒泡排序最早出现于1956年,当时由IBM大型机操作系统的主要开发者之一——R.W. Dorsey提出。不过当时它还没有得到广泛的重视和应用。后来,在1962年,科学家Cocke和Nishimoto又独立发明了冒泡排序,并对其进行了深入研究和改进。由于他们的贡献,冒泡排序才逐渐被广泛应用于计算机领域,并成为了一种经典的排序算法。
实例介绍
我们先来做一个冒泡排序的实例。继续接着之前桶排序的实例,我们让9、4、10、9、7、2从大到小依次排列。
第一轮:
1、 首先我们比较第一个数与第二个数的大小,发现9 > 4,那么我们跳过。
2、 接下来我们比较第二个数与第三个数的大小,发现4 < 10,那么我们将它们互换一下位置。
3、 我们比较第三个数与第四个数的大小,发现4 < 9,那么我们将它们互换一下位置。
4、 我们比较第四个数与第五个数的大小,发现7 > 4,那么我们跳过。
5、我们比较第五个数与第六个数的大小,发现4 > 2,那么我们跳过。
第二轮:
1、我们接着像第一轮的比较,发现9 < 10,那么我们将它们互换位置。
可以发现,现在我们的成绩已经是一个从大到小的排列状态,但其实我们的排列还没有排完,我们后面还有6-轮次-1次的排列,并且还有6-1次的排列,这次为什么我们排列的这么快?原因只是我们的数据巧合罢了。。。
具体的排列顺序应该是:
可以看到,就像冒泡一样的把小的数移到了最右边。
那么我们可以总结一下,【每大次的排序总共(轮)=数据数量(个数)-1】,【每小次的排序=个数-该轮数-1】
那么这个时候我们就可以来编写相对应的C语言代码来实现快速排序:
#include <stdio.h> #define STU_SIZE 20 //存储学生的空间大小 // 定义名字、分数结构体 struct students_fraction { char name[26]; int frc; }; int main(){ int i,j,get_stu; // get_stu写入学生的个数 printf("-------当前结构体的容量可存储:%d个学生的姓名与成绩\n\n",STU_SIZE); struct students_fraction nam_num[STU_SIZE],temp; //fraction_nam个人信息结构体共STU_SIZE个人的空间,temp临时个人信息结构体存储变量 // 录入数据 printf("需要写入多少人的数据:"); scanf("%d",&get_stu); printf("请输入%d个姓名对应的成绩:\n",get_stu); for (i = 0; i < get_stu; i++){ printf("请输入第%d个学生的姓名与成绩:",i+1); scanf("%s %d",nam_num[i].name,&nam_num[i].frc); } // 打印数据 for (i = 0; i < get_stu; i++){ printf("%s-->%d,已录入!\n",nam_num[i].name,nam_num[i].frc); } printf("\n-------开始冒泡排序:-------\n"); // 冒泡排序,一共(get_stu)-1趟 for (i = 0; i < get_stu-1; i++){ //每一趟的换位 一共换(get_stu)-i-1次,也就是get_stu减去1再减去第多少趟的次数 for (j = 0; j < get_stu - i - 1; j++){ if(nam_num[j].frc < nam_num[j+1].frc){ temp = nam_num[j]; nam_num[j] = nam_num[j+1]; nam_num[j+1] = temp; } } } for (i = 0; i < get_stu; i++){ printf("第%d名:%s,成绩--->%d\n",i+1,nam_num[i].name,nam_num[i].frc); } return 0; }
实现的效果如下:
从上述代码不难看出,我们首先是定义了一个students_fraction
结构体,结构体中有学生的姓名字段和分数字段,然后我们定义了一个个人信息结构体变量nam_num[STU_SIZE]来存储学生的姓名和成绩以及一个后面会用到的临时结构体变量。之后我们让用户输入写入的信息个数循环让用户输入姓名和成绩,然后采用冒泡排序算法来显示数据信息,算法是这样的:外层的for循环用来表示每一趟,一共需要走get_stu-1
趟;内层循环用来表示每一趟需要的排列次数,一共需要排get_stu-i-1
(i表示当前的趟数)次;在换位中我们是这样换的:如果该学生是A1下一个学生是A2,如果A1的成绩小于A2的成绩,就把A1的信息先放入一个临时的结构体中存储把A2的信息覆盖掉A1的信息,然后把A1的信息覆盖掉A2原始的位置信息,这样就达到了一个交换的效果。而且姓名和跟随着成绩一同进行了排序。
冒泡排序的动画演示:
冒泡排序.mp4
插入内容
从我们实现的效果来看,冒泡排序似乎真正的解决了我们眼前的问题,分数和姓名都一起的进行了排序,桶排序的空间问题也得到了很好的解决。
但其实这种排序算法还是存在着一些缺点和不足。
我们再来回顾我们写的的核心排序算法的代码:
我们用语句频度来表示一条语句的执行次数,这是一个嵌套循环结构,外层循环执行了get_stu-1
次,内层循环执行次数则是不固定的,因为它的执行次数是由前面的i
的取值所决定的。
内层循环的执行次数可以这样计算:第一趟时,i = 0
,内层循环执行了get_stu - i - 1
次;第二趟时,i = 1
,内层循环执行了get_stu - i - 1
次;以此类推,最后一趟时,i = get_stu - 2
,内层循环执行了1
次。因此,内层循环的总执行次数为:
(get_stu - 1) + (get_stu - 2) + ... + 1 = (get_stu - 1) * get_stu / 2
对于内部的条件语句if
,它的执行次数取决于内层循环中满足条件的情况,具体次数需要根据数据分析。但是我们可以看到,在最好情况下,即nam_num
已经有序的情况下,条件语句一次也不会执行;在最坏情况下,即nam_num
完全逆序的情况下,条件语句会执行内层循环的所有次数。
因此,对于给出的代码,其整体的语句频度为:
T(n) = (get_stu - 1) * get_stu / 2 * T(if)
其中T(if)
表示条件语句的平均执行次数。
n
的某个函数,用T(n)
表示。对于某个辅助函数f(n)
(f(n)是正整数n的函数),若存在一个正的常数M,使lim{n->∞}[T(n)/f(n)=M]
,则称f(n)是T(n)的同数量级函数,记为T(n)=O(f(n))
,称O(f(n))为算法的时间的复杂度。那么对于我们上面的代码,我们令get_stu = n
,T(if) = O(1~n)
,O(1~n)表示判断语句在最好的情况下执行的次数与最坏情况下执行的次数,则T(n) = (n-1) * n / 2 * O(1~n)
,为了方便我们令O1~n) = 1
,则T(n) = (n-1) * n / 2
,那么我们不难得出辅助函数f(n) = n^2,因为:
此时的正的常数M=1/2
,因此我们的冒泡排序的时间复杂度是O(n^2)
,对比一下之前的桶排序,它的时间复杂度是O(n)
,当然在具体的情况下具体的时间复杂度与具体的排序算法有关。我们可以看出冒泡排序的时间复杂度要高的多。
假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序只需要 0.1 秒,而冒泡排序则需要1千万秒!
冒泡排序在1956年就有人开始研究,之后有很多人都尝试过对冒泡排序进行改进,但结果却令人失望。如 Donald E. Knuth(中文名为高德纳,1974年图灵奖获得者)所说:“冒泡排序除了它迷人的名字和导致了某些有趣的理论问题这一事实之外,似乎没有什么值得推荐的。”
既然冒泡排序的算法这么差,那么我们再换一种算法,我们要求它在前两种算法的基础上更加优秀。
介绍:
什么是快速排序?快速排序算法是一种基于分治思想的排序算法,也是目前被广泛应用的最快的排序算法之一。其基本思路是选择一个枢轴元素,将待排序序列划分为两个子序列,其中一个子序列中所有元素都比枢轴元素小,而另一个子序列中所有元素都比枢轴元素大,然后分别对这两个子序列递归进行排序。
快速排序算法由英国计算机科学家Tony Hoare于1960年发明。当时,Hoare正在为Atlas电子计算机编写排序算法,并试图找到一种比归并排序更快的排序算法。他最初的想法是使用三向划分来处理重复元素,但这种方法没有取得理想的效果。后来,他发现用一个元素作为枢轴元素,将序列划分为两个子序列,然后递归地对子序列进行排序,可以获得良好的性能。快速排序算法的提出,极大地推动了计算机科学和算法领域的发展。
实例介绍:
我们首先给定一些数,还是我们之前的那些:9、4、10、9、7、2,这次我们从小到大来排一遍。
我们在介绍的时候说了快速排序的基本思路就是选择一个枢轴元素,我们把它暂且称为基点。基点可以是这些数里面的任意一个数。然后我们用i、j指针指向这些数的最左边和最右边。
我们取9
为基点(由于该数中有两个9我们把基点称为基点9
),定义i
指向最左边[0]、定义j
指向最右边[5],如下图所示:
1、我们先让最右边的指针开始向左边移动,也就是j,直到遇到比基点数小的数就停下,这里我们可以发现j指向2时就停下了,这个时候我们把基点数与j所指向的数进行交换,也就是基点9和2进行互换位置。
2、j已经移动过了,这一次我们让i移动,i向左移动,直到遇到比基点数大的数就停下来,我们可以发现当i指向10的时候i就停下来了,此时我们把基点数与i进行交换,也就是把10和基点9进行互换位置。
3、接下来还是一样的道理,我们这次移动j,将j向左移动直到遇到小于基点数的数,这个数也就是7,然后把7与基点9互换位置。
4、可以发现,数据已经是从小到大的排序了。当其实我们的指针移动还没有结束。i还是会继续向又移动,于是指针i就会和指针j重合,这时以重合点为中心,将两端的数据分开,再对每一段数据进行向前面的找基点定两个指针来排序,排序完再合并,就是最终的结果了。
我们通过如下的C语言代码来实现:
#include <stdio.h> void quicksort(int arr[], int left, int right); int partition(int arr[], int left, int right); void quicksort(int arr[], int left, int right){ if (left >= right) return; // 子序列长度为0或1时直接返回 int pivot = partition(arr, left, right); // 划分子序列并得到基点元素位置 quicksort(arr, left, pivot - 1); // 对左边子序列递归排序 quicksort(arr, pivot + 1, right); // 对右边子序列递归排序 } int partition(int arr[], int left, int right){ int pivot = arr[left]; // 选择第一个元素作为基点元素 int i = left, j = right; while (i < j){ while (i < j && arr[j] > pivot) j--; // 从右向左遍历找到小于等于pivot的元素 if (i < j) arr[i++] = arr[j]; // 将小于等于pivot的元素移到左边 while (i < j && arr[i] <= pivot) i++; // 从左向右遍历找到大于pivot的元素 if (i < j) arr[j--] = arr[i]; // 将大于pivot的元素移到右边 } arr[i] = pivot; // 将基点元素放到正确的位置 return i; // 返回基点元素的位置 } int main(){ int arr[6]; int a; for (a = 0; a < 6; a++){ printf("请输入第%d个数:", a + 1); scanf("%d", &arr[a]); } printf("\n---数据已写入---\n"); printf("[0]\t[1]\t[2]\t[3]\t[4]\t[5]\n"); for (a = 0; a < 6; a++){ printf(" %d\t", arr[a]); } quicksort(arr, 0, 5); // 对整个序列进行快速排序 printf("\n---排序结果---\n"); printf("[0]\t[1]\t[2]\t[3]\t[4]\t[5]\n"); for (a = 0; a < 6; a++){ printf(" %d\t", arr[a]); } }
运行效果截图:
快速排序的动画演示:
快速排序.mp4
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。