每日算法前传-常用排序算法

221 阅读10分钟

常用排序算法介绍

1 直接插入排序

插入排序算法是基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。这种比较是从该有序序列的最末端开始执行,即要插入序列中的元素最先和有序序列中最大的元素比较,若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int a[]= {12,4,132,55,46,232,789,1,0,98,523,666};//随机数组
    int n = sizeof(a)/sizeof(a[0]);//获取数组大小
    int i,j,k;
 //在要排序的一组数中,假定前n-1个数已经排好序,现将第n个数插到前面的有序数列中,
 //使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
    for(i = 0; i < n-1; i ++) {
        for(j = i+1; j > 0; j --)
            if(a[j] < a[j-1]) {
                k = a[j-1];
                a[j-1] = a[j];
                a[j] = k;
            } else
                break;
    }
    for(i = 0; i < n; i ++)//输出排序后的结果
        printf("%d ",a[i]);
    return 0;
}
//运行结果如下:
//0 1 4 12 46 55 98 132 232 523 666 789

2 希尔排序

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

#include<stdio.h>
#include<math.h>
 
#define MAXNUM 10
 
void main()
{
    void shellSort(int array[],int n,int t);//t为排序趟数
    int array[MAXNUM],i;
    for(i=0;i<MAXNUM;i++)
        scanf("%d",&array[i]);
    shellSort(array,MAXNUM,(int)(log(MAXNUM+1)/log(2)));//排序趟数应为log2(n+1)的整数部分
    for(i=0;i<MAXNUM;i++)
        printf("%d ",array[i]);
    printf("\n");
}
 
//根据当前增量进行插入排序
void shellInsert(int array[],int n,int dk)
{
    int i,j,temp;
    for(i=dk;i<n;i++)//分别向每组的有序区域插入
    {
        temp=array[i];
        for(j=i-dk;(j>=i%dk)&&array[j]>temp;j-=dk)//比较与记录后移同时进行
            array[j+dk]=array[j];
        if(j!=i-dk)
            array[j+dk]=temp;//插入
    }
}
 
//计算Hibbard增量
int dkHibbard(int t,int k)
{
    return (int)(pow(2,t-k+1)-1);
}
 
//希尔排序
void shellSort(int array[],int n,int t)
{
    void shellInsert(int array[],int n,int dk);
    int i;
    for(i=1;i<=t;i++)
        shellInsert(array,n,dkHibbard(t,i));
}
 
//此写法便于理解,实际应用时应将上述三个函数写成一个函数

3 直接选择排序

选择排序算法的基本思路是为每一个位置选择当前最小的元素。

首先从第1个位置开始对全部元素进行选择,选出全部元素中最小的给该位置,再对第2个位置进行选择,在剩余元素中选择最小的给该位置即可;以此类推,重复进行“最小元素”的选择,直至完成第(n-1)个位置的元素选择,则第n个位置就只剩唯一的最大元素,此时不需再进行选择。使用这种排序时,要注意其中一个不同于冒泡法的细节。举例说明:序列58539.我们知道第一遍选择第1个元素“5”会和元素“3”交换,那么原序列中的两个相同元素“5”之间的前后相对顺序就发生了改变。因此,我们说选择排序不是稳定的排序算法,它在计算过程中会破坏稳定性。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int a[]= {12,4,132,55,46,232,789,1,0,98,523,666};//随机数组
    int n = sizeof(a)/sizeof(a[0]);//获取数组大小
    int i,j,k;
        //第一次遍历n-1个数,找到最小的数值与第一个元素交换
        //第二次遍历n-2个数,找到最小的数值与第二个元素交换
        // 以此类推
        //第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
    for(i = 0; i < n-1; i ++) {
        for(j = i+1; j < n; j ++) {
             if(a[i] > a[j]) {//从小到大排序
                    k = a[i];
                    a[i] = a[j];
                    a[j] = k;
             }
         }
    }
    for(i = 0; i < n; i ++)//输出排序后的结果
        printf("%d ",a[i]);
    return 0;
}
//运行结果如下:
//0 1 4 12 46 55 98 132 232 523 666 789

4 堆排序

堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

  • 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  • 创建最大堆(Build Max Heap):将堆中的所有数据重新排序
  • 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

#include <stdio.h>
#include <stdlib.h>
int arr[]= {12,4,132,55,46,232,789,1,0,98,523,666};//随机数组
int n = sizeof(arr)/sizeof(arr[0]);//获取数组大小
void adjustHeap(int i, int lef) {
    int temp=arr[i];
    for(int k=i*2+1; k<lef; k=k*2+1) { //从i结点的左子结点开始,也就是2i+1处开始
        if(k+1<lef&&arr[k]<arr[k+1]) {
            k++;
        }
        if(arr[k]>temp) { //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
            arr[i]=arr[k];
            i=k;
        } else {
            break;
        }
    }
    arr[i]=temp;//将temp值放到最终的位置
}
void swap(int a, int b) {
    int temp=arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}
void heapsort() {
    // 1、构建大顶堆
    for(int i = n/2-1; i>=0; i--) {
        //从第一个非叶子节点从下至上,从右至左调整结构
        adjustHeap(i,n);
    }
    //2、调整堆结构+交换堆顶元素与末尾元素
    for(int j=n-1; j>0; j--) {
        swap(0,j);//将堆顶元素与末尾元素进行交换
        adjustHeap(0, j);//重新对堆进行调整
    }
}
int main() {
    int i;
    heapsort();
    for(i = 0; i < n; i ++)
        printf("%d ",arr[i]);
    return 0;
}
//运行结果如下:
//0 1 4 12 46 55 98 132 232 523 666 789

5 冒泡排序

冒泡排序算法是把较小的元素往前调或者把较大的元素往后调。

这种方法主要是通过对相邻两个元素进行大小的比较,根据比较结果和算法规则对该二元素的位置进行交换,这样逐个依次进行比较和交换,就能达到排序目的。冒泡排序的基本思想是,首先将第1个和第2个记录的关键字比较大小,如果是逆序的,就将这两个记录进行交换,再对第2个和第3个记录的关键字进行比较,依次类推,重复进行上述计算,直至完成第(n一1)个和第n个记录的关键字之间的比较,此后,再按照上述过程进行第2次、第3次排序,直至整个序列有序为止。排序过程中要特别注意的是,当相邻两个元素大小一致时,这一步操作就不需要交换位置,因此也说明冒泡排序是一种严格的稳定排序算法,它不改变序列中相同元素之间的相对位置关系。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int a[]= {12,4,132,55,46,232,789,1,0,98,523,666};//随机数组
    int n = sizeof(a)/sizeof(a[0]);//获取数组大小
    int i,j,k;
    //比较相邻的两个数据,如果第二个数小,就交换位置。从后向前两两比较,一直到比较最前两个数据。
        for(i = 1; i < n; i ++) {
            for(j = 0; j < n-1; j ++) {
                if(a[j] > a[j+1]) {//从小到大排序
                    k = a[j];
                    a[j] = a[j+1];
                    a[j+1] = k;
                }
            }
        }  
    for(i = 0; i < n; i ++)//输出排序后的结果
        printf("%d ",a[i]);
    return 0;
}
//运行结果如下:
//0 1 4 12 46 55 98 132 232 523 666 789

6 快速排序

快速排序的基本思想是:通过一趟排序算法把所需要排序的序列的元素分割成两大块,其中,一部分的元素都要小于或等于另外一部分的序列元素,然后仍根据该种方法对划分后的这两块序列的元素分别再次实行快速排序算法,排序实现的整个过程可以是递归的来进行调用,最终能够实现将所需排序的无序序列元素变为一个有序的序列。

#include <stdio.h>
#include <stdlib.h>
//先从数列中取出一个数作为key值
//将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边
//对左右两个小数列重复第二步,直至各区间只有1个数。
void quickSort(int a[],int l,int r) {
    if(l>=r)
        return;
    int i = l;
    int j = r;
    int key = a[l];//选择第一个数为key
    while(i<j) {
        while(i<j && a[j]>=key)//从右向左找第一个小于key的值
            j--;
        if(i<j) {
            a[i] = a[j];
            i++;
        }
        while(i<j && a[i]<key)//从左向右找第一个大于key的值
            i++;
        if(i<j) {
            a[j] = a[i];
            j--;
        }
    }
    a[i] = key;
    quickSort(a, l, i-1);//继续排左部分,递归调用
    quickSort(a, i+1, r);//继续排右部分,递归调用
}
int main() {
    int a[]= {12,4,132,55,46,232,789,1,0,98,523,666};//随机数组
    int i,n = sizeof(a)/sizeof(a[0]);//获取数组大小
    quickSort(a,0,n-1);//快速排序函数入口
    for(i = 0; i < n; i ++)//输出排序后的结果
        printf("%d ",a[i]);
    return 0;
}
//运行结果如下:
//0 1 4 12 46 55 98 132 232 523 666 789

7 归并排序

归并排序算法就是把序列递归划分成为一个个短序列,以其中只有1个元素的直接序列或者只有2个元素的序列作为短序列的递归出口,再将全部有序的短序列按照一定的规则进行排序为长序列。归并排序融合了分治策略,即将含有n个记录的初始序列中的每个记录均视为长度为1的子序列,再将这n个子序列两两合并得到n/2个长度为2(当凡为奇数时会出现长度为l的情况)的有序子序列;将上述步骤重复操作,直至得到1个长度为n的有序长序列。需要注意的是,在进行元素比较和交换时,若两个元素大小相等则不必刻意交换位置,因此该算法不会破坏序列的稳定性,即归并排序也是稳定的排序算法。

归并排序原理

归并排序具体工作原理如下(假设序列共有n个元素):

  • 将序列每相邻两个数字进行归并操作(merge),形成floor(n/2+n%2)个序列,排序后每个序列包含两个元素

  • 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素

  • 重复步骤2,直到所有元素排序完毕

#include <stdlib.h>
#include <stdio.h>
 
void Merge(int sourceArr[],int tempArr[], int startIndex, int midIndex, int endIndex)
{
    int i = startIndex, j=midIndex+1, k = startIndex;
    while(i!=midIndex+1 && j!=endIndex+1)
    {
        if(sourceArr[i] > sourceArr[j])
            tempArr[k++] = sourceArr[j++];
        else
            tempArr[k++] = sourceArr[i++];
    }
    while(i != midIndex+1)
        tempArr[k++] = sourceArr[i++];
    while(j != endIndex+1)
        tempArr[k++] = sourceArr[j++];
    for(i=startIndex; i<=endIndex; i++)
        sourceArr[i] = tempArr[i];
}
 
//内部使用递归
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
    int midIndex;
    if(startIndex < endIndex)
    {
        midIndex = startIndex + (endIndex-startIndex) / 2;//避免溢出int
        MergeSort(sourceArr, tempArr, startIndex, midIndex);
        MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
        Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
    }
}
 
int main(int argc, char * argv[])
{
    int a[8] = {50, 10, 20, 30, 70, 40, 80, 60};
    int i, b[8];
    MergeSort(a, b, 0, 7);
    for(i=0; i<8; i++)
        printf("%d ", a[i]);
    printf("\n");
    return 0;
}

8 基数排序

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。

#include<math.h>
testBS()
{
    inta[] = {2, 343, 342, 1, 123, 43, 4343, 433, 687, 654, 3};
    int *a_p = a;
    //计算数组长度
    intsize = sizeof(a) / sizeof(int);
    //基数排序
    bucketSort3(a_p, size);
    //打印排序后结果
    inti;
    for(i = 0; i < size; i++)
    {
        printf("%d\n", a[i]);
    }
    intt;
    scanf("%d", t);
}
//基数排序
voidbucketSort3(int *p, intn)
{
    //获取数组中的最大数
    intmaxNum = findMaxNum(p, n);
    //获取最大数的位数,次数也是再分配的次数。
    intloopTimes = getLoopTimes(maxNum);
    inti;
    //对每一位进行桶分配
    for(i = 1; i <= loopTimes; i++)
    {
        sort2(p, n, i);
    }
}
//获取数字的位数
intgetLoopTimes(intnum)
{
    intcount = 1;
    inttemp = num / 10;
    while(temp != 0)
    {
        count++;
        temp = temp / 10;
    }
    returncount;
}
//查询数组中的最大数
intfindMaxNum(int *p, intn)
{
    inti;
    intmax = 0;
    for(i = 0; i < n; i++)
    {
        if(*(p + i) > max)
        {
            max = *(p + i);
        }
    }
    returnmax;
}
//将数字分配到各自的桶中,然后按照桶的顺序输出排序结果
voidsort2(int *p, intn, intloop)
{
    //建立一组桶此处的20是预设的根据实际数情况修改
    intbuckets[10][20] = {};
    //求桶的index的除数
    //如798个位桶index=(798/1)%10=8
    //十位桶index=(798/10)%10=9
    //百位桶index=(798/100)%10=7
    //tempNum为上式中的1、10、100
    inttempNum = (int)pow(10, loop - 1);
    inti, j;
    for(i = 0; i < n; i++)
    {
        introw_index = (*(p + i) / tempNum) % 10;
        for(j = 0; j < 20; j++)
        {
            if(buckets[row_index][j] == NULL)
            {
                buckets[row_index][j] = *(p + i);
                break;
            }
        }
    }
    //将桶中的数,倒回到原有数组中
    intk = 0;
    for(i = 0; i < 10; i++)
    {
        for(j = 0; j < 20; j++)
        {
            if(buckets[i][j] != NULL)
            {
                *(p + k) = buckets[i][j];
                buckets[i][j] = NULL;
                k++;
            }
        }
    }
}

常用排序算法时间复杂度和空间复杂度表:

排序方法时间复杂度(平均)时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性复杂性
直接插入排序O(n2)O(n2)O(n)O(1)稳定简单
希尔排序O(nlog2n)O(n2)O(n1.3)O(1)不稳定较复杂
直接选择排序O(n2)O(n2)O(n2)O(1)不稳定简单
堆排序O(nlog2n)O(nlog2n)O(nlog2n)O(1)不稳定较复杂
冒泡排序O(n2)O(n2)O(n)O(1)稳定简单
快速排序O(nlog2n)O(n2)O(nlog2n)O(nlog2n)不稳定较复杂
归并排序O(nlog2n)O(nlog2n)O(nlog2n)O(n)稳定较复杂
基数排序O(d(n+r))O(d(n+r))O(d(n+r))O(n+r)稳定较复杂

文献参考: