排序算法学习整理一(冒泡)
排序算法顾名思义,给元素排序,无论是从小到大也好还是从大到小也罢,都归属于排序,作为一个刚入坑但又在能力上有所欠缺的萌新来说排序算法是简直难以逾越的天坑,我曾经见过一个朋友冒泡排序敲了一周QAQ,勉强敲了出来。我写这些博客的原因,一是学习整理巩固排序算法,二是帮助有需要的朋友们。
我坚信一句话:坚持不能使你成为天才,但足以使你超越常人。
下面言归正传,还记得你第一次写给10个数排序的代码吗,(当初我是用if else写的,捂脸,嗯,脑子是个好东西可惜我没有),是不是当初特别痛恨这个给十个数排序的题目,好不容易写个冒泡,还没过一天,就叫写选择,刚写完选择,结果又说冒泡可以优化......诸如此类的任务,下面呢,我就给大家介绍一下冒泡排序及优化和冒泡小小的变形。
一、冒泡排序(Bubble Sort):
冒泡排序是一种简单的利用交换来完成排序的算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,就像我们肥宅快乐水里的气泡一样。好久没喝了
冒泡算法的步骤:
- 比较相邻的元素。如果前一个元素数比后一个元素大,就交换它们两个。
- 对每一对相邻元素做同样的工作,从开始0和1到结尾的n-1和n。所以,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
下面是代码:
1 void bubble_sort(int arr[], int len) 2 { 3 int i, j; 4 for (i = 0; i < len - 1; i++) //需要进行比较的轮数 5 { 6 for (j = 0; j < len - 1 - i; j++) //每轮需要进行比较的次数, 7 //因为每一轮都有一个最大的数被放到了最后一个,所以第i轮就有i个数不需要排序(这里就存在优化的可能) 8 if (arr[j] > arr[j + 1]) //相邻的两个元素两两进行比较 9 { 10 arr[j] = arr[j] ^ arr[j+1]; 11 arr[j+1] = arr[j] ^ arr[j+1]; 12 arr[j] = arr[j] ^ arr[j+1]; // 交换 13 } 14 } 15 }Bubble Sort
冒泡排序的问题很明显,举个例子
当数组元素为 1 2 3 9 4 5 6 7 8 时:
第一轮:1 2 3 4 5 6 7 8 9;
第二轮:1 2 3 4 5 6 7 8 9;
第三轮:1 2 3 4 5 6 7 8 9;
至此我们可以看出在第一轮的时候数组就已经排好序了,然而冒泡排序仍然在兢兢业业的工作(真是好员工,可惜效率低了点)。
所以我们可尝试着优化一下,在进行交换的时候标记一下,当发数组遍历完都没有交换的情况,也就证明已经排好序了,我们的冒泡排序就可以提前下班了。
优化后的代码如下:
1 void bubble_sort(int arr[], int len) 2 { 3 int i, j; 4 for (i = 0; i < len - 1; i++) //需要进行比较的轮数 5 { 6 int isSorted = 1; //用isSorted进行标记,默认其为已经完成排序,也可以设置为bool型 7 for (j = 0; j < len - 1 - i; j++) //每轮需要进行比较的次数 8 { 9 //因为每一轮都有一个最大的数被放到了最后一个,所以第i轮就有i个数不需要排序 10 if (arr[j] > arr[j + 1]) //相邻的两个元素两两进行比较 11 { 12 isSorted = 0; //进入交换后立刻置0, 表示排序未完成 13 arr[j] = arr[j] ^ arr[j+1]; 14 arr[j+1] = arr[j] ^ arr[j+1]; 15 arr[j] = arr[j] ^ arr[j+1]; //交换 16 } 17 } 18 if (isSorted) //如果一直未交换就代表已经完成了排序,也就是说可以提前下班了 19 { 20 break; 21 } 22 } 23 }
当我们继续测试后会发现冒泡排序的另一个问题
打个比方:
当数组为 33 54 32 21 56 67 78 89 时:
第二个for里
第一轮比较次数:7
第二轮比较次数:7
第三轮比较次数:7
第四轮比较次数:7
程序结束
第一轮的7次是怎么来的呢?
33和54比较,33 < 54,所以不变。
54和32比较,54 > 32,所以54和32换。
33 32 54 21 56 67 78 89
54和21比较,54 > 21,所以54和21换。
33 32 21 54 56 67 78 89
54和56比较,54 < 56, 不变。
56和67比较,56 < 67,不变。
67和78比较, 67 < 78,不变。
78和89比较, 78 < 89, 不变。
第一轮至此结束。
第二轮的6次
33和32比较, 33 > 32, 所以33和32交换。
32 33 21 54 56 67 78 89
33和21比较, 33 > 21,所以33和21交换。
32 21 33 54 56 67 78 89
33和54比较,33 < 54,所以不变。
54和56比较,54 < 56, 不变。
56和67比较,56 < 67,不变。
67和78比较, 67 < 78,不变。
第二轮至此结束。
到这里基本就能看出问题了,这个测试组,从56往后都是有序的,但是冒泡这位员工仍然一丝不苟的执行的他的任务。
从56往后的比较都是没有意义的,所以我们应该考虑如何让程序识别已排好序的子列。大家可以思考一下然后再往后看。
解决方法:我们只需将要再最后一次元素进行交换的位置进行记录,让第二for循环再这个范围内进行比较。
最终优化代码如下(对我来说):
1 void bubble_sort(int arr[], int len) 2 { 3 int i, j; 4 int lastIndex, sortBorder; //用来记录最后一次交换的下标和交换边界 5 sortBorder = len -1; //因为第一轮的时候i = 0,所以第一轮的sortBorder == len - i - 1 6 for (i = 0; i < len - 1; i++) //需要进行比较的轮数 7 { 8 int isSorted = 1; //用isSorted进行标记,默认其为已经完成排序,也可以设置为bool型 9 for (j = 0; j < sortBorder; j++) //每轮需要进行比较的次数 10 { 11 //这里不在由i确定循环边界了,这也是这次优化的核心 12 if (arr[j] > arr[j + 1]) //相邻的两个元素两两进行比较 13 { 14 isSorted = 0; //进入交换后立刻置0, 表示排序未完成 15 lastIndex = j; //记录下最后一次交换的位置 16 arr[j] = arr[j] ^ arr[j+1]; 17 arr[j+1] = arr[j] ^ arr[j+1]; 18 arr[j] = arr[j] ^ arr[j+1]; //交换 19 } 20 } 21 sortBorder = lastIndex; //讲最后一次交换的位置作为下次比较的边界 22 if (isSorted) //如果一直未交换就代表已经完成了排序,也就是说可以提前下班了 23 { 24 break; 25 } 26 } 27 }
当然这也不最优的冒泡优化,但是继续优化就涉及到另一个算法了,所以目前对冒泡这个员工的整改就到这里
二、鸡尾酒排序(CockTailSort):
鸡尾酒排序是冒泡排序的变形,他与冒泡的最大不同在于鸡尾酒排序可以双向同时排序,就在不断搅拌调制鸡尾酒一样,故此得名,有意思的是他还有个特殊的名字叫做快乐小时排序(鬼知道为什么要叫这个名字)。
鸡尾酒排序的原理:
-
找到最小的元素,放到第一位
-
找到最大的元素,放到最后一位。
-
找到第二小的元素,放到第二位
-
找到第二大的元素放到倒数第二位。
以此类推,直到完成排序。
从原理中可以明显看出鸡尾酒排序就是两个冒泡排序同时进行,所以其代码便是双份的冒泡进行略微改动。
至于为什么要两个冒泡同时进行我们来看个例子
现有一个数组其元素为 1 2 3 4 5 6 7 8 0 ,对其进行冒泡排序过程如下:
第一轮 9与0换
1 2 3 4 5 6 7 8 0 9
第二轮 8与0换
1 2 3 4 5 6 7 0 8 9
第三轮 7与0换
1 2 3 4 5 6 0 7 8 9
第四轮 6与0换
1 2 3 4 5 0 6 7 8 9
第五轮 5与0换
1 2 3 4 0 5 6 7 8 9
第六轮 4与0换
1 2 3 0 4 5 6 7 8 9
第七轮 3与0换
1 2 0 3 4 5 6 7 8 9
第八轮 2与0换
1 0 2 3 4 5 6 7 8 9
第九轮 1于0换
0 1 2 3 4 5 6 7 8 9
很明显数组原本从a~a8 都是有序的元素,但是冒泡给他们进行九轮的交换才完成,增加了工作量,降低了工作效率,典型性事倍功半,所以鸡尾酒排序就应运而生。
根据基本冒泡排序的思想可以写出以下代码:
1 void CockTailSort(int *array, int len) 2 { 3 int i, j; 4 5 for (i = 0; i < len/2; i++) 6 { 7 for (j = i; j < len-i-1; j++) 8 { 9 if (array[j] > array[j+1]) 10 { 11 array[j] = array[j] ^ array[j+1]; 12 array[j+1] = array[j] ^ array[j+1]; 13 array[j] = array[j] ^ array[j+1]; 14 } 15 } 16 17 for (j = len-i-1; j > i; j--) 18 { 19 if (array[j] < array[j-1]) 20 { 21 array[j] = array[j] ^ array[j-1]; 22 array[j-1] = array[j] ^ array[j-1]; 23 array[j] = array[j] ^ array[j-1]; 24 } 25 } 26 } 27 }
由于鸡尾酒排序是由双份的冒泡改进而来,所以冒泡的优化都适用于鸡尾酒。
优化后的代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 void CockTailSort(int *, int ); 5 int arr[] = {15, 48, 23, 753, 90, 45, 954, 1009, 1, 15, 9, 3, 4, 1}; 6 int main() 7 { 8 9 //int array[]= {3,4,2,1,5,6,7,8}; 10 int n = sizeof(arr)/sizeof(int); 11 12 CockTailSort(arr , n); 13 14 for (int i = 0; i < n; i++) 15 { 16 printf("%d ", arr[i]); 17 } 18 printf("\n"); 19 return 0; 20 } 21 22 void CockTailSort(int *array, int len) 23 { 24 int i,j; 25 int lastleftIndex = 0; //正向冒泡最后一次交换的下标 26 int lastrightIndex = 0; //反向冒泡最后一次交换的下标 27 int rightBorder = len-1; // 28 int leftBorder = 0; 29 30 for (i = 0; i < len/2; i++) //当你i大于len的一半时就会产生重复检查 31 { //不过后面有IsSorted检查是否完成排序, 32 //这里写成-1也可以,只不过len2更严谨一些 33 int IsSorted = 1; 34 for (j = leftBorder; j < rightBorder; j++) 35 { 36 if (array[j] > array[j+1]) 37 { 38 IsSorted = 0; 39 lastrightIndex = j; 40 array[j] = array[j] ^ array[j+1]; 41 array[j+1] = array[j] ^ array[j+1]; 42 array[j] = array[j] ^ array[j+1]; 43 } 44 } 45 rightBorder = lastrightIndex; //记录自己下次边界,也作为反向冒泡的开始值 46 if (IsSorted) 47 { //检查是否排完序了,无论从哪里break都代表整个数组已经完成排序 48 break; 49 } 50 51 IsSorted = 1; 52 53 for (j = rightBorder; j > leftBorder; j--) 54 { 55 if (array[j] < array[j-1]) 56 { 57 IsSorted = 0; 58 lastleftIndex = j; 59 array[j] = array[j] ^ array[j-1]; 60 array[j-1] = array[j] ^ array[j-1]; 61 array[j] = array[j] ^ array[j-1]; 62 } 63 } 64 leftBorder = lastleftIndex; //同理,记录自己下次边界,也作为正向冒泡的开始值 65 if(IsSorted) 66 { 67 break; 68 } 69 } 70
当然鸡尾酒排序的代码不止这一种,个人水平有限,大家可以自行探索。
冒泡类的排序就说到这里,如果有问题还请大家多多指正。