神的规范:排序算法(二):简单选择排序

来源:互联网 发布:宁波seo入门教程 编辑:程序博客网 时间:2024/05/22 07:53

写在前面:
排序又称为分类,它是数据处理中经常用到的一种重要运算。
虽然并未列入世界最伟大的几大算法之一,但毫无疑问,在各行各业的各个时期排序都是作为奠基者般的存在为程序所调用,也为编程者所敬仰。只是,也许正是因为它与我们息息相关,以至于我们竟然时常忽略它的存在。
事实上我们生活中无时无刻不在做排序:考试成绩排名,按身高、年龄、能力高低去评判他人,划分任务处理的优先级,等等·······
上一篇博客我们讨论了简单插入排序算法,而今天我们进入“简单选择排序”的学习,一步步去探索排序算法如何一步步在反思中前进,在山重水复处柳暗花明。

为简单起见,我们采用顺序存储的结构存放所排序的数据元素序列。
虽然简单选择排序与简单插入排序都是“简单的排序”,但是我们好奇的是:它们的思想和实现有什么区别呢?
我们已经知道,所谓简单插入排序,就是将所有待排序元素分为有序无序两个部分,每次取出无序部分的第一个元素,通过数据比较,插入到有序序列的合适位置,依次类推直到所有数据有序,它是一个稳定的算法。
而简单选择排序,重在“选择”二字,怎么个选法呢?
大家试想一下公司(比如谷歌)是如何选择招聘优秀员工的?
在一群(无序的)应聘者中,首先把表现最好(排序码最小)的员工挑出来(形成有序部分的第一个元素),然后在剩下的所有员工中再选出表现最后的(放入到有序部分的第二个位置),以此类推直到选出所有公司需要的职员。
相信这一流程非常容易理解,因为它很符合人类的思维(但有缺点,缺点后面详述)。
我们将简单选择排序的思想规范化描述一下:第一趟排序是在无序的{R1,R2,…Rn}中按排序码选出最小的元素,将它与R1交换;第二趟排序是在无序的{R2,R3,…Rn}中选出最小的元素,将它与R2交换;而第i趟排序时{R1,R2,…R(i-1)}已排好序,在当前无序的{Ri,…Rn}中再选出最小的元素,将它与Ri交换,使{R1,…Ri}有序。值得注意的是,i-1趟排序后,整个数据元素就是递增有序的(为什么?)
我们可以给出一个简单选择排序的C语言描述(假设待排序元素为double型数据):

void selectsort1(double a[],int n) {    int i,j;    for (i = 0;i < n-1;i++) {        double small = a[i];   //Assume that a[i] is the smallest        for (j = i+1;j < n;j++) {            if (a[j] < small) {   //Swap if a[j] is smaller                double temp = a[j];                a[j] = small;                small = temp;            }        }        a[i] = small;  //It is the ith smallest element    }}

以上代码经测试(此处略)实现了递增排序,但是我们注意到有一个问题:它可能要多次交换数据,即a[j]与small,而它并没有对我们的排序结果(根据这一排序算法)做出实际贡献。这势必会导致整体执行效率的低下。那么有没有改良的方案呢?
有。我们考虑不移动数据,只把排序码做个变化,代码如下:

void selectsort2(double a[],int n) {    int i,j,small;    for (i = 0;i < n-1;i++) {        small = i;        for (j = i+1;j < n;j++) {            if (a[j] < a[small])                 small = j;        }        if (i != small) {  //only needs n-1 at most            double temp = a[small];            a[small] = a[i];            a[i] = temp;        }    }}

这样一来,交换数据的动作在最坏情况下也就只需n-1个,大大减少了交换所需的开销。
简单评估一下简单选择排序:
函数selectsort中外层for循环需执行n-1次,而对于找出排序码最小的数据元素的内层for循环,最坏情况下需要执行n-i次数据比较,因此简单选择排序最坏情况下需要执行n-1+n-2+…1 = n(n-1)/2次比较,算法效率为O[n^2](注意,与简单插入排序效率等级相同)。

如此一来,我们有了一个疑问,一个是简单选择排序,一个是简单插入排序,这两种“简单排序”谁更快呢?
有人会想,应该是一样快吧,毕竟算法效率都一样。
非也非也,是骡子是马,牵出来遛遛。我们用代码进行测试(包括针对第一种不完善的简单选择排序的测试),测试代码如下:

#include "stdio.h"#include "stdlib.h"#include "time.h"void print(double a[],int n);void selectsort1(double a[],int n);void selectsort2(double a[],int n);void insertsort(double a[],int n);#define NUM 20000int main(int argc, char* argv[]){    clock_t t_s,t_e;    double t;    double array1[NUM];    srand(time(NULL));    for (int i = 0;i < NUM;i++)        array1[i] = rand();    double array2[NUM];    for (i = 0;i < NUM;i++)        array2[i] = array1[i];    double array3[NUM];    for (i = 0;i < NUM;i++)        array3[i] = array1[i];    printf("Before sort,elements are:\n");//  print(array,NUM);    printf("After selectsort1,elements are:\n");    t_s = clock();    selectsort1(array1,NUM);    t_e = clock();    t = (t_e - t_s)/(double)CLOCKS_PER_SEC;//  print(array1,NUM);    printf("Time used:%f s\n",t);    printf("After selectsort2,elements are:\n");    t_s = clock();    selectsort2(array2,NUM);    t_e = clock();    t = (t_e - t_s)/(double)CLOCKS_PER_SEC;//  print(array2,NUM);    printf("Time used:%f s\n",t);    printf("After insertsort,elements are:\n");    t_s = clock();    insertsort(array3,NUM);    t_e = clock();    t = (t_e - t_s)/(double)CLOCKS_PER_SEC;//  print(array3,NUM);    printf("Time used:%f s\n",t);    return 0;}void print(double a[],int n) {    for(int i = 0;i < n;i++) {        printf("%f\t",a[i]);    }    printf("\n");}void selectsort1(double a[],int n) {    int i,j;    for (i = 0;i < n-1;i++) {        double small = a[i];   //Assume that a[i] is the smallest        for (j = i+1;j < n;j++) {            if (a[j] < small) {   //Swap if a[j] is smaller                double temp = a[j];                a[j] = small;                small = temp;            }        }        a[i] = small;  //It is the ith smallest element    }}void selectsort2(double a[],int n) {    int i,j,small;    for (i = 0;i < n-1;i++) {        small = i;        for (j = i+1;j < n;j++) {            if (a[j] < a[small])                 small = j;        }        if (i != small) {            double temp = a[small];            a[small] = a[i];            a[i] = temp;        }    }}void insertsort(double a[],int n) {    int i,j;    for (i = 0;i < n-1;i++) {        double temp = a[i+1];        j = i;        while(j > -1 && temp < a[j]) {            a[j+1] = a[j];            //move bigger number to a bigger location            j--;                    //Search downside for number to move        }        a[j+1] = temp;  //Insert element temp    }}

测试大量(代码中是20000,可更改)数据的结果如下图(不妨多测几次,排除干扰):
这里写图片描述
这里写图片描述
这里写图片描述

三次测试中,我们发现:对于相同的数据样本,简单插入排序的效果每次都好于简单选择排序。这说明,实际上简单插入排序算法平均效率最高(而不是最坏情况下效率最高,我们有理由相信,最坏情况下简单选择排序与简单插入排序算法效率相等或相近)。

至于为什么,一个直觉的答案是:简单插入排序是稳定的,而简单选择排序则不稳定(为什么?)。

行文至此,对于简单选择排序也可以告一段落了,下一篇我们来讨论一个同一维度的竞争者——冒泡排序。当然,我们还要比比三者之中,谁是简单排序算法之王。

To be continued…

1 0