常见的排序算法

总结一下常见的排序算法。
排序分内排序和外排序。
内排序 :指在排序期间数据对象全部存放在内存的排序。
外排序 :指在排序期间全部对象个数太多,不能同时存放在内存,必须根据排序过程的要求,不断在内、外存之间移动的排序。
内排序的方法有许多种,按所用策略不同,可归纳为五类:插入排序、选择排序、交换排序、归并排序、分配排序和计数排序。
插入排序 主要包括 直接插入排序,折半插入排序 希尔排序 两种;
选择排序 主要包括 直接选择排序 堆排序;
交换排序 主要包括 冒泡排序 快速排序;
归并排序 主要包括 二路归并 (常用的归并排序)和 自然归并。
分配排序 主要包括 箱排序 基数排序。
计数排序就一种。
稳定排序 :假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。
其中 冒泡,插入,基数,归并属于稳定排序 ; 选择,快速,希尔,堆属于不稳定排序。 
下面是对这些常见排序算法的介绍。未来测试代码的正确性,我们采取了随机生成10个序列,然后先使用C++STL中给出的排序算法 sort 来得到一个正确的排序,然后再使用我们的方法进行排序得到结果,通过对比这两者的结果来验证我们代码的正确性。

测试代码如下:

void sort_test( void (*_sort)( int*, int)){
     const  int N= 10;
     int orig[N];
     int standard[N];
     int arr[N];
    srand(time( 0));
     for( int j= 0;j< 15;j++){
         for( int i= 0;i<N;i++)
            orig[i]=rand()% 100;//随机生成序列
        cout<< " bef: ";
        print(orig,N);

        copy(orig,orig+N,standard);
        sort(standard,standard+N);//利用sort函数进行排序
        cout<< " std: ";
        print(standard,N);

        copy(orig,orig+N,arr);
        _sort(arr,N);//采用我们的方法进行排序
        cout<< " aft: ";
        print(arr,N);
        if(equal(standard,standard+N,arr))//测试我们的方法是否正确
            printf( " %sOK%s\n ",green,normal);
         else
            printf( " %sNO%s\n ",red,normal);
    }
}

 其中参数是要测试的方法,void (*_sort)(int*,int)是排序方法的指针,我们所有的排序方法都写成这种形式。

1. 直接插入排序  

 直接插入排序(straight insertion sort)的作法是:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。

  第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从后向前扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。

  直接插入排序属于稳定的排序,时间复杂性为o(n^2),空间复杂度为O(1)。

  直接插入排序是由两层嵌套循环组成的。外层循环标识并决定待比较的数值。 内层循环为待比较数值确定其最终位置。直接插入排序是将待比较的数值与它的前一个数值进行比较,所以外层循环是从第二个数值开始的。当前一数值比待比较数 值大的情况下继续循环比较,直到找到比待比较数值小的并将待比较数值置入其后一位置,结束该次循环。(从小到大)

值得注意的是,我们必需用一个存储空间来保存当前待比较的数值,因为当一趟比较完成时,我们要将待比较数值置入比它小的数值的后一位。插入排序类似玩牌时整理手中纸牌的过程。

代码如下:

void insert_sort( int a[], int n)
{
    _FUNC;
     for( int i= 1;i<n;i++) {
         int t=a[i];
         int j;
         for(j=i- 1;j>= 0&&a[j]>t;j--) {
                a[j+ 1]=a[j];
        }
        a[j+ 1]=t;
        print(a,n);
    }

}

测试结果如下:

2. 折半插入排序

  折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。

    折半插入排序算法的具体操作为:在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为 a[high],则轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新 的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成 立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。

  折半插入排序算法是一种稳定的排序算法,比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。

代码如下:

void binary_insert_sort( int a[], int n){
     for( int i= 1;i<n;i++){
         int low= 0;
         int high=i- 1;
         int t=a[i];
         int mid;
         while(low<=high){
            mid=(low+high)/ 2;
             if(t<a[mid])
                high=mid- 1;
             else
                low=mid+ 1;
        }
         for( int j=i;j>mid;j--)
            a[j]=a[j- 1];
        a[low]=t;

    }
}

 

 测试结果如下:

3. 希尔排序

希尔排序(Shell Sort)又叫做缩小增量排序(diminishing increment sort),是一种很优秀的排序法,算法本身不难理解,也很容易实现,而且它的速度很快。 

基本思想:

  先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插入 排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1), 即所有记录放在同一组中进行直接插入排序为止。

  该方法实质上是一种分组插入方法。插入排序(Insertion Sort)的一个重要的特点是,如果原始数据的大部分元素已经排序,那么插入排序的速度很快(因为需要移动的元素很少)。从这个事实我们可以想到,如果原 始数据只有很少元素,那么排序的速度也很快。--希尔排序就是基于这两点对插入排序作出了改进。

 下图是希尔排序的一种实现方式:

 该图对应的实现方式如下:
void shell_sort( int a[], int n)
{
    _FUNC;
     int gap=n/ 2;
     bool flag= true;
     while(gap> 1||flag)
    {
        flag= false;
         for( int i= 0;i+gap<n;i++)
             if(a[i]>a[i+gap])
            {
                swap(a[i],a[i+gap]);
                flag= true;
            }
        print(a,n);
         if(gap> 1)
            gap/= 2;
    }

}

 测试结果如下:

另一种实现方式:

void shell_sort2( int a[], int n){
//     _FUNC;
     int gap=n/ 2;
     while(gap> 0){
         for( int i=gap;i<n;i++){
             int t=a[i];
             int j;
             for(j=i-gap;j>= 0&&a[j]>t;j-=gap)
                a[j+gap]=a[j];
            a[j+gap]=t;
        }
        gap/= 2;
    }
}

 

4. 直接选择排序

  排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。时间复杂度是O(n^2)

代码如下:

void select_sort( int a[], int n)
{
     for( int i= 0;i<n- 1;i++)
    {
         int min=a[i];
         int index=i;
         for( int j=i+ 1;j<n;j++)
             if(a[j]<min)
            {
                min=a[j];
                index=j;
            }
        swap(a[i],a[index]);
    }
}

 这个是最基本的:从中找出最小的然后和第一个数交换,再从第2到n-1中找出最小的和第二个数交换

 方法二:

void select_sort2( int a[], int n)
{
    _FUNC;
     for( int i=n- 1;i> 0;i--){
         for( int j= 0;j<i;j++)
             if(a[j]>a[i])
                swap(a[j],a[i]);
    }

}

 这儿感觉形式上有点类似下面的冒泡排序。

方法三:

这是对方法二的改进,判断过程中是否有交换发生,如果没有交换,说明已经完成排序了。

void select_sort3( int a[], int n)
{
    _FUNC;
     bool flag= true;
     for( int i=n- 1;i> 0&&flag;i--){
        flag= false;
         for( int j= 0;j<i;j++)
             if(a[j]>a[i])
                swap(a[j],a[i]),flag= true;
        print(a,n);
    }

}

 

5. 堆排序

  我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

堆排序的代码如下: 

void adjust( int b[], int m, int n){
//     int *b=a-1;
     int j=m;
     int k= 2*m;
     while(k<=n){
         if(k<n&&b[k]<b[k+ 1])
            k++;
         if(b[j]<b[k])
            swap(b[j],b[k]);
        j=k;
        k*= 2;
    }
}
void heap_sort( int a[], int n){
    _FUNC;
     int *b=a- 1;
     for( int i=n/ 2;i>= 1;i--)
        adjust(b,i,n);
     for( int i=n- 1;i>= 1;i--){
        swap(b[ 1],b[i+ 1]);
        adjust(b, 1,i);
    }
}

  需要注意的是,如果使用数组表示堆的话,要从下标1开始,而不是从0开始。所以,这儿采用了一个技巧,让int*b=a-1;这样的话b[1]就相当于对原数组从0开始似的,即a[0]。

6. 冒泡排序

 冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不用交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。

代码如下:

void bubble_sort( int a[], int n)
{
    _FUNC;
     for( int i=n- 1;i> 0;i--)
         for( int j= 0;j<i;j++)
             if(a[j]>a[j+ 1])
                swap(a[j],a[j+ 1]);

}

  下面的方法是加入了是否已经排好序的判断。  

void bubble_sort2( int a[], int n)
{
     bool flag= true;
     for( int i=n- 1;i> 0&&flag;i--){
        flag= false;
         for( int j= 0;j<i;j++)
             if(a[j]>a[j+ 1])
                swap(a[j],a[j+ 1]),flag= true;
    }

}

 

7. 快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然 后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

你可能感兴趣的:(排序算法)