排序排序的概念及其运用和选择排序
- 7. 排序
- 7.1 排序的概念及其运用
- 7.2 选择排序算法——直接选择排序
- 选择排序基本思想:
- 直接选择排序
- 选择排序原理
- 参考程序
- 如何交换数据
- 直接选择排序的特性总结:
7. 排序
7.1 排序的概念及其运用
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]==r[j]
,且r[i]
在r[j]
之前,而在排序后的序列中,r[i]
仍在r[j]
之前,则称这种排序算法是稳定的;否则称为不稳定的。
举个例子,同一张试卷,分数相同的情况下,谁先交卷,谁排在前面。
或者总分相同,谁的语文成绩更好,谁排在前。此时对这种特殊需求,排序算法的稳定性就很有必要。
内部排序:数据元素全部放在内存中的排序。(下文也称内排序)
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。(下文也称外排序)
例如 1 G = = 102 4 3 b y t e 1G==1024^3 byte 1G==10243byte,一个int型整数4byte,100亿( 1 0 10 10^{10} 1010)个int型整数占用空间为37G左右,针对这100亿个数据的排序已无法用常见的排序算法去操作。
排序的算法先考虑可行性,再考虑效率。
排序要掌握的点:
- 不同排序算法的时间复杂度和空间复杂度。
- 排序的思想。
- 排序的实现。
排序主要学习这几种:
如果是参加算法竞赛,则只需要学习堆排序、快速排序和归并排序即可,遇到竞赛题除非有特殊需求,否则一律用库函数sort解决。
7.2 选择排序算法——直接选择排序
选择排序基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序
选择排序原理
-
在元素集合
array[i]
–array[n-1]
中选择关键码最大(小)的数据元素。 -
若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
-
在剩余的
array[i]
–array[n-2]
(array[i+1]
–array[n-1]
)集合中,重复上述步骤,直到集合剩余1个元素。
排序动图示例:
参考程序
以升序排序为例。
根据原理分析,得到的未优化选择排序:
//选择排序,因为传递的是指针,要另外传递元素个数
void selectSort(int b[],int n) {int i=0,j=0,k=0;for(i=0;i<n;i++) {k=i;//每次更新第一个(或最后一个)元素的位置 for(j=i+1;j<n;j++)if(b[j]<b[k])//遍历寻找最小值k=j;if(k!=i) {//找到的最小值没有放在第一个位置int temp=b[k];b[k]=b[i];b[i]=temp;}}
}
尝试优化:我们在每次遍历数据时,同时找到最大值和最小值的位置并分别放在开头和结尾,还能再减少一半的整体遍历次数。这个思路需要两个起到指针作用的变量分别标记开头和结尾。
这个思路很像双指针,但并不是,只是用到了双指针的思路进行优化。
在实现时我们先看一组样例:
int a[]={7,4,3,6,5,2,3};
我们发现,最大值7和开头位置重合。若按照思路进行枚举,则交换最小值到开头时,数据变化:
(7) 4 3 6 5 (2) 3 ==> (2) 4 3 6 5 (7) 3
此时2莫名其妙的被判断成最大值和3进行交换。
(2) 4 3 6 5 (7) 3 ==> 3 4 3 6 5 (7) (2)
到这里可以看出,排序没能使数组有序。
解决方法:优化方案会进行两次数据交换,在第一次交换时判断找到的最值是否与开头、结尾重叠,重叠的话进行修正即可。
优化后的选择排序参考程序:
//经优化的选择排序,每次同时选出最小的和最大的
void selectSortplus(int* a, int n) {int begin = 0, end = n - 1;//起标记作用的两个变量while (begin < end) {//能不能加等于,取决于交换数据时用的方法int maxi = begin, mini = begin;for (int i = begin; i <= end; i++) {if (a[i] > a[maxi])maxi = i;if (a[i] < a[mini])mini = i;}Swap(&a[begin], &a[mini]);if (begin == maxi)//处理begin和maxi重叠时的情况maxi = mini;Swap(&a[end], &a[maxi]);++begin;//双指针思路--end;}
}
如何交换数据
在优化的选择排序中,有提到为啥这句能不能用<=
取决于交换数据的方法。
程序设计语言进行交换数据的方法有3种:
- 设置中间变量
这个是最基础、最常用的方法。但会额外开一个临时变量的空间。若交换的数据比较庞大的话可能需要将庞大的数据分成若干份分别进行交换。
void Swap(Datatype* a,Datatype* b){int tmp = *a;*a = *b;*b = tmp;
}
- 三次异或
我们知道,c语言有一个按位异或操作符^
,而异或操作的真值表:
尽管按位异或是针对二进制位的操作,但如果是两个相同的数据进行异或操作,结果是0。例如:3^3==0
的结果为真,因为两个3在内存中存储的二进制形式完全相同,这就使得每一个bit位进行异或时的结果都是0,得到的补码也是0,对应的真实数据自然也是0。
利用这个特点,我们也可以设计出一种不用额外申请空间的数据交换方式:
void Swap(Datatype* a,Datatype* b){Datatype aa=*a,bb=*b;//先进行演示,后进行优化//交换a、b的数据*a=aa^bb^aa;*b=aa^bb^bb;
}
其中aa^bb
我们可以用另一个变量进行存储:
void Swap(Datatype* a,Datatype* b){Datatype aa=*a,bb=*b;//先进行演示,后进行优化//交换a、b的数据Datatype tmp=aa^bb;*a=tmp^aa;*b=tmp^bb;
}
我们将tmp更换成aa也没关系,因为此时bb还没发生变化,即使aa的值变成了aa^bb
,我们还可以再通过aa^bb^bb
的方式重新拿回aa的数据。
void Swap(Datatype* a,Datatype* b){Datatype aa=*a,bb=*b;aa=aa^bb;*b=aa^bb;*a=aa^*b;//运行到这里时,*b已经变成了*a的样子
}
第3行:此时aa存储的数据是*a^*b
第4行:此时bb还未发生改变,aa存储的数据是
*a^*b
,因此=右边的表达式相当于是*a^*b^*b
。*b
已经变成了*a
的样子。第5行:
*b
的内容已经变成了*a
的数据,而aa存储的数据依旧是*a^*b
,所以*a
变成了*b
。
到这里,其实aa和bb作为中间变量的功能已经可以被取代。所以我们得到了一个新的交换数据的方式:
void Swap(Datatype* a,Datatype* b){*a=*a^*b;*b=*a^*b;*a=*a^*b;
}
第2行:
*a
自己作为中间变量存储*a^*b
的数据第3行:运行完这一句后,
*b
存储了*a
的数据第4行:
*a
存储的数据依旧是*a^*b
的值,而*b
存储的是*a
的数据,所以*a
存储的数据也变成了*b
的值。
例如:
void f00(){int a=3,b=4;printf("%d %d\n",a,b);a=a^b;b=a^b;a=a^b;printf("%d %d\n",a,b);
}
输出:
3 4
4 3
当两个变量的值相等时,第一次异或的值为0,此后再用0参与异或时的值是1,而不是我们想要的值。所以这种数据交换的方式不能用于两个变量一样的情况。而且这种变量也不需要交换数据。
3. 作差
利用数值都有大小的特点,我们可以通过作差得到两数差的部分,再将这部分填到另一部分,这样也能做到交换数据的目的:
void f01(int a,int b){a=a-b;//获得差值b=a+b;//差值填给aa=b-a;//b已变成a,砍掉差值变成b
}
这种交换方式不能用于a和b都代表同一空间的情况,否则数据会丢失(这也是为什么while(begin<end)
不加等号的原因,但没多少人会用这种方式进行数据交换,加不加取决于个人爱好)。此外不清楚比较规则的自定义类型也不能用这种数据交换方式。
例如:
void f02(int x){int* a=&x,*b=&x;printf("%d\n",*a,*b);*a=*a-*b;*b=*a+*b;*a=*b-*a;printf("%d\n",*a,*b);
}
输出:
6 6
0 0
直接选择排序的特性总结:
-
直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
-
时间复杂度: O ( N 2 ) O(N^2) O(N2)(两层循环)
-
空间复杂度: O ( 1 ) O(1) O(1)
-
稳定性:不稳定(要与当前的第一个和最后一个数据进行数据交换,即使是相同的数据也不知道会被移动到哪里)。
例如这个样例:[5,5,3]
,第一次选择最小数的3时候,不可避免的和第1个5进行交换,这时数组变成了[3,5,5]
。
我们还可以通过下列代码测试稳定性。只要找到值相同但次序不同的数的位置即可发现算法是否稳定。
#include<stdio.h>typedef struct Num {int x;int i;
}Num;
typedef Num DataType;void Swap(DataType* a, DataType* b) {DataType t = *a;*a = *b;*b = t;
}void selectSort(DataType b[],int n) {int i=0,j=0,k=0;for(i=0;i<n;i++) {k=i;//每次更新第一个(或最后一个)元素的位置 for(j=i+1;j<n;j++)if(b[j].x<b[k].x)//遍历寻找最小值k=j;if(k!=i) {//找到的最小值没有放在第一个位置DataType temp=b[k];b[k]=b[i];b[i]=temp;}}
}//经优化的选择排序,每次同时选出最小的和最大的
void selectSortplus(DataType* a, int n) {int begin = 0, end = n - 1;//起标记作用的两个变量while (begin < end) {//能不能加等于,取决于交换数据时用的方法int maxi = begin, mini = begin;int i=0;for (i = begin; i <= end; i++) {if (a[i].x > a[maxi].x)maxi = i;if (a[i].x < a[mini].x)mini = i;}Swap(&a[begin], &a[mini]);if (begin == maxi)//处理begin和maxi重叠时的情况maxi = mini;Swap(&a[end], &a[maxi]);++begin;//双指针思路--end;}
}void f() {srand((size_t)time(0));//随机数的种子Num a[30] = { {0,0} },b[30] = { {0,0} };int i=0;for (i = 0; i < 30; i++) {a[i].x = rand()%100+1;a[i].i = i + 1;b[i] = a[i];}for (i = 0; i < 30; i++)printf("%d %d,",a[i].x,a[i].i);printf("\n\n");selectSort(a, 30);selectSortplus(b, 30);for (i = 0; i < 30; i++)printf("%d %d,", a[i].x, a[i].i);printf("\n\n");for (i = 0; i < 30; i++)printf("%d %d,", b[i].x, b[i].i);
}int main(){f();return 0;
}