浅谈数据结构-交换排序(冒泡、快速)
交换排序:两两比较待排序的关键字,并交换不满足次序要求的那对数,直到整个表都满足次序要求为止。
冒泡和快速排序是交换排序算法,冒泡排序给人直观感觉是从前开始遍历,将小的元素,慢慢交换到数组前面。快速排序直观感觉是从数组两边开始,以某一值作为分组标准,从顶端和尾端开始慢慢交换。
一、冒泡算法
1、“加火”-冒泡思想
冒泡算法思想:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。从含义讲轻气泡不能在重气泡之下的原则,从下往上扫描数组s:凡扫描到违反本原则的轻气泡,就使其向上"飘浮"。如此反复进行,直到最后任何两个气泡都是轻者在上,重者在下为止。
从算法思想分析,冒泡算法实现比较简单,但从冒泡实现的细节区分,可以初级冒泡排序、一般化冒泡排序、升级版冒泡排序。
2、初级冒泡排序
//低级冒泡排序 void SortAlgorithm::PrimaryBubbleSort(pSqlList plist) { int i,j; for (i =0;i<plist->length;i++) { //数组选择一个,与其他数据比较,如果有小的,就放在i位置,就是获取余下数组中最小的 for(j = i+1;j<plist->length;j++) { if (plist->SqlArray[i] > plist->SqlArray[j]) { swap(plist,i,j); } } } }
上述代码不是严格意义的冒泡排序算法,冒泡算法是循环比较邻接元素大小,如果不符合规则,进行交换。这个算法就是,先从数组选取一个元素,以它为标准,找出最小值。获取最小值后,在获取此最小值。
以上图为了,第一次进行排序后,2竟然在最后面了,这是不高效的。这其实一般人排序算法的理解,没学过排序算法的人,估计就会这一种了。
3 、一般冒泡排序
1 //冒泡排序 2 void SortAlgorithm::bubble_sort(pSqlList pList) 3 { 4 int i,j; 5 6 for (i =0;i<pList->length;i++) 7 { 8 //数组从后面开始遍历,前后两个数比较,将小的往前送 9 //注意数组开始条件,和结束条件 10 for(j = pList->length-1;j>=i;j--) 11 { 12 //注意数组坐标,千万别溢出 13 if (pList->SqlArray[j-1] > pList->SqlArray[j]) 14 { 15 swap(pList,j-1,j); 16 } 17 } 18 } 19 20 }
注意算法是从后向前循环,如果从前向后,起始条件和终止条件要改变下。
算法步骤:
1、比较第1个和第2个数,将小数放前,大数放后。
2、比较第2个和第3个数,将小数放前,大数放后,如此继续,直到比较最后两个数,将小数放前,大数放后。
至此,第一趟结束,将最大的数放在了最后。
3、继续循环操作1、2步骤。其实很简单的操作,改进在于比较前后两个值,遵循一个原则,实现数据有序排列
4、升级版冒泡排序
如果待排序的序列式{2, 1, 3, 4, 5, 6, 7, 8, 9},也就是说,除了第一和第二的关键字需要交换外,别的都已经是正常的顺序了.
//高级冒泡排序 void SortAlgorithm::SeniorBubbleSort(pSqlList pList) { int i,j; int flag = true; printf("开始验证冒泡排序"); for (i =0;i<pList->length&&flag;i++) { //大部分操作和冒泡排序一致,就是添加了判断条件,就是当有数据交换时才执行,没有数据交换,说明数组有序 flag = false; for(j = pList->length-1;j>=i;j--) { //注意数组坐标,千万别溢出 if (pList->SqlArray[j-1] > pList->SqlArray[j]) { flag = true; swap(pList,j-1,j); } } PrintSqlList(pList); } }
代码分析:
1、从尾部9开始冒泡排序,进行第一次冒泡后,序列已经为有序序列{1,2,3,4,5,6,7,8,9}
2、进入第二个循环,flag = false,然后执行内部冒泡排序,发现序列有序,此时flag还是false,所以循环直接跳出。
二、快速排序算法
它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。
然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
1、算法分析
①分解:
在S[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间S[low..pivotpos-1)和S[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。
注意:
划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示为(注意pivot=S[pivotpos]):
S[low..pivotpos-1].keys≤S[pivotpos].key≤S[pivotpos+1..high].keys
其中low≤pivotpos≤high。
②求解:
通过递归调用快速排序对左、右子区间S[low..pivotpos-1]和S[pivotpos+1..high]快速排序。
③组合:
因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。
在当前无序区中选取划分的基准关键字是决定算法性能的关键。所以后续研究人员对快速的提升基本就是如何选取基准关键词。
初始状态为一组无序的数组:50,10,90,30,70,40,80,60,20。
1、数组首位放置基准位,程序中默认选取spList[1] = 50。
2、从数组两边开始排序,大于50的,放在右边,小于50 ,放在左边。
3、经过以上操作步骤后,完成了第一次的排序,得到新的数组:20,10,40,30,50,70,80,60,90,。
新的数组中,以20为分割点,在前5个元素中继续排序,同时以70在后面数组中排序。
2 、核心代码
1 //快速排序 2 void SortAlgorithm::QuickSort(pSqlList pList) 3 { 4 printf("开始验证快速排序"); 5 QuickSort(pList,1,pList->length-1); 6 } 7 //快速排序 8 inline void SortAlgorithm::QuickSort(pSqlList pList,int left,int right) 9 { 10 //快速排序原理是从不停迭代,类似二叉树,分组排序,注意跳出条件 11 //这里是if,如果是while就进入死循环 12 if(left<right) 13 { 14 //数组分组,将组内数组进行交换,让其小的在基数左边,大的基数右边 15 int baseposition = Division(pList,left,right); 16 //继续迭代,将分好的左边小数数组,内部再进行快速排序 17 QuickSort(pList,left,baseposition-1); 18 //继续迭代,将分好的右边大数数组,内部再进行快速排序 19 QuickSort(pList,baseposition+1,right); 20 } 21 printf("快速排序组外排序\n"); 22 PrintSqlList(pList); 23 } 24 25 inline int SortAlgorithm::Division(pSqlList pList,int left,int right) 26 { 27 printf("快速排序组内数组展示"); 28 //以左边数为基准 29 int base = pList->SqlArray[left]; 30 while(left<right) 31 { 32 //判断右边的是否大于基准数,如果右边数小于,放在左边,否则继续向左,遍历,直到找到大于基数的. 33 while(left<right&&pList->SqlArray[right] >= base) 34 { 35 right--; 36 } 37 //找到比base小的数后,将这个元素放在坐标数组中 38 pList->SqlArray[left] = pList->SqlArray[right]; 39 // 从序列左端开始,向右遍历,直到找到大于base的数 40 while(left< right && pList->SqlArray[left] <= base) 41 { 42 left++; 43 } 44 // 从序列左端开始,向右遍历,直到找到大于base的数 45 pList->SqlArray[right] = pList->SqlArray[left]; 46 } 47 48 // /最后将base放到left位置。此时,left位置的左侧数值应该都比left小; 49 // 而left位置的右侧数值应该都比left大。 50 pList-> SqlArray[left] = base; 51 PrintSqlList(pList); 52 return left; 53 }
三、交换排序性能分析
1、冒泡排序复杂度
排序方法 | 时间复杂度
|
空间复杂度 | 稳定性 | 复杂性 | |||
冒泡排序 |
|
O(1) | 稳定 | 简单 | |||
快速排序 |
|
O(Nlog2N)O(2) | 不稳定 | 较复杂 |
1、冒泡复杂度
若文件的初始状态是正序的,一趟扫描即可完成排序。比较次数为n-1,冒泡排序最好时间复杂度为O(N)。
若初始文件是反序的,需要进行 N -1 趟排序。每趟排序要进行 N - i 次关键字的比较(1 ≤ i ≤ N - 1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:1+2+3+….+n-1 = n(n-1)/2,所以最坏时间复杂度为O(N2)。
因此,冒泡排序的平均时间复杂度为O(N2)。
总结起来,其实就是一句话:当数据越接近正序时,冒泡排序性能越好。
2、快速排序复杂度
时间复杂度
当数据有序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差。
而当数据随机分布时,以第一个关键字为基准分为两个子序列,两个子序列的元素个数接近相等,此时执行效率最好。
所以,数据越随机分布时,快速排序性能越好;数据越接近有序,快速排序性能越差。
空间复杂度
快速排序在每次分割的过程中,需要 1 个空间存储基准值。而快速排序的大概需要 Nlog2N次 的分割处理,所以占用空间也是 Nlog2N 个。