排序

来源:互联网 发布:java jlabel 链接 编辑:程序博客网 时间:2024/04/28 22:05

排序算法总结

1. 概述

  • 定义

    排序是将一个数据元素记录的任意序列,重新排成一个按关键字有序的序列。

  • 稳定性

    标准:对于两个大小相同的元素,排序前位置领先的元素在排序后依然领先,则这种排序算法
    稳定的 ;反之,若可能使排序后两者的位置交换,则这种算法就是不稳定的

    判断:说某种排序算法是稳定的,意思是说存在一种代码实现,使之满足以上标准;说某种排
    序算法不是稳定的,意思是说,该算法的任何一种实现形式,都存在某组特殊的关键字,使之
    不稳定。

  • 分类

    按排序过程中的不同原则来分,可以分为 交换排序、选择排序、插入排序、归并排序和计数排序等五类。

    按时间复杂度来分,可以分为简单排序(时间复杂度为O (n^2))、先进排序(时间复杂度为O (nlogn))和
    基数排序(时间复杂度为O (dn))三类。

2. 交换排序

2.1 冒泡排序(Bubble Sort)

  • 思想
    首先将记录的第一个数据和第二个数据比较,若为逆序则将两个数据的位置交换,然后比较第二个和第三个数据。
    以此类推,直到第n-1和第n个数据比较完,完成一趟冒泡排序,此时最大值位于最后一个位置处。进行完n-1趟排序
    或者在一趟排序过程中没有发生数据交换,则排序完成。
  • 伪代码
do  swapped = false  for i = 1 to indexOfLastUnsortedElement    if leftElement > rightElement      swap(leftElement, rightElement)      swapped = true; swapCounter++while swapped
  • 代码
void bubble_sort(int *a, int n){    swapped = 1;    while(swapped)    {        int i,j,temp;        swapped = 0;        for(i=0;i<n-1-j;i++)        {            if(a[i]>a[i+1])            {                temp = a[i];                a[i]=a[i+1];                a[i+1] = temp;                swapped = 1;            }        }        j++;    }}
void bubble_sort(int *a, int n){    int i, j, temp,swapped;    for(i = 0; i < n-1; i++)    {        swapped =0;        for(j = 0; j < n-1-i; j++)        {            if(a[j] > a[j+1])            {                temp = a[j];                a[j] = a[j+1];                a[j+1] = temp;                swapped =1;            }        }        if(swapped==0) break;    }}
  • 分析
    交换次数较少,最少为0,比较次数多,为n(n-1)/2,时间复杂度为O (n^2)。

2.2 快速排序(Quick Sort)

  • 思想

    快速排序是对冒泡排序的一种改进,也是交换排序的一种。快速排序首先选择一个基数(通常选择序列的第一个数据),
    然后以这个基数为标准将序列分成两个部分,其中一部分中的数据全部大于该基数,另一部分全部小于该基数。然后
    将分成的两个部分再分别进行以上的排序处理,从而使整个序列有序。一般用递归来实现。

  • 代码

///////////////////////快速排序//查找位置int find_pos(int *a, int low, int high){    int val = a[low];    while(low < high)    {        while(low < high && a[high] >= val)        {//大于移动,小于则赋值,降序则相反            high--;        }        a[low] = a[high];        while(low < high && a[low] <= val)        {//小于移动,大于则赋值,降序则相反            low++;        }        a[high] = a[low];    }//终止while循环之后low和high一定是相等的    //high可以改为low    a[low] = val;    return low;}//low:第一个元素下标//high:最后一个元素下标void quick_sort(int *a, int low, int high){    if(low < high)  //长度大于1    {        int pos = find_pos(a, low, high);  //将序列一分为二        quick_sort(a, low, pos-1);     //对低位序列快速排序        quick_sort(a, pos+1, high);  //对高位序列快速排序    }}
  • 分析
    快速排序平均时间复杂度为O (nlogn),最坏情况(逆序)的时间复杂度同冒泡法一样为O (n^2)。
    快速排序是对冒泡排序的改进,冒泡排序是对每个相邻的元素进行比较,快速排序是对所有的数据同基准元素进行比较,空间跨度更大,比较次数和交换次数减少。同时,用到了二分的思想,降低的时间复杂度。
    快速排序在同等数量级knlogn的排序算法中常数因子k最小,是平均时间最少的一种排序算法。
    平均空间复杂度为O (log2n +1),最坏情况为O(n),此时栈的深度为n。

3. 选择排序

3.1 简单选择排序(Simple Selection Sort)

  • 思想

    令i从1到n-1,进行n-1趟选择排序,每一趟都从未进行排序的n-i+1个数据中找出最小的数据,然后和第i个数据进行交换,完成排序。

  • 伪代码

repeat (numOfElements - 1) times{  set the first unsorted element as the minimum  for each of the unsorted elements  {    if element < currentMinimum      set element as new minimum  }  swap minimum with first unsorted position}
  • 代码
void select_sort(int *a, int n){    int i, j, k, temp;    for(i = 0; i < n-1; i++)    {        k = i;        for(j = i+1; j < n; j++)        {            if(a[k] > a[j])            {                k = j;            }        }        if(i != k)        {            temp = a[i];            a[i] = a[k];            a[k] = temp;        }    }}
  • 分析
    交换次数较少,最少为0,最大为3(n-1),比较次数多,为n(n-1)/2,时间复杂度为O (n^2)。

3.2 堆排序(Heap Sort)

  • 回顾补充

    先定义一下二叉堆,二叉堆是完全二叉树或者是近似完全二叉树,且满足:
    1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
    2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
    当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:
    这里写图片描述
    堆的存储: 一般用数组来表示堆,i节点的父节点下标为(i-1)/2,它的左右子节点的下标为2*i+1和2*i+2。
    这里写图片描述

  • 思想

    堆顶是序列的最小值(或最大值),输出堆顶后,将其余的元素重新排列成堆,可以得到序列的次小值,
    然后将次小值输出,如此反复执行,便能得到一个有序序列。
    所以,堆排序要做的就是输出堆顶和重新排列一个堆。

  • 代码
//  从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2 void RebuildMinHeap(int *a,int i, int n){    int j,temp;    temp =a[i];    j=2*i+1;    while(j<n)    {        if(j+1<n && a[j+1]<a[j]) //在左右孩子中找最小的             j++;        if(a[j]<temp)    //如果孩子节点比父节点小,则将孩子节点的值赋值给父节点,并继续往下走,         {               //如果上述赋值破坏了原有的堆结构,则重建。            a[i]=a[j];            i=j;            j=2*i+1;        }         else  break;    }    a[i]=temp;        //最后,将开始的调整的顶节点的值放到合适的位置上}// 堆排序//1.建立堆化数组;//2.取出堆的顶点,把最后一位放到顶点位置;//3.重新建立堆化数组,重复步骤2,直到所有数据都被取出;void heap_sort (int *a,int n) {    int i,j,temp;    for(i=n/2 -1;i>=0;i--)  //对于叶子节点来说,可以认为它已经是一个合法的堆了,    {                       //所以从第一个非叶子节点n/2 -1开始重建堆。        RebuildMinHeap(a,i,n); //建立堆化数组    }    for(i=n-1;i>=1;i--)    //数组中第一个数据已经是最小值,先将该值取出,最后一个数据放到堆顶,    {                     //然后,重新建立堆化数组。        temp= a[0];        a[0]=a[i];        a[i]=temp;        RebuildMinHeap(a,0,i);    }}//* 注意 使用最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。
  • 分析
    由于每次重新恢复堆的时间复杂度为O(logn),共n- 1次重新恢复堆操作,再加上前面建立堆时n / 2次向>下调整,每次调整时间复杂度也为O(logn)。二次操作时间相加还是O(nlogn)。故堆排序的时间复杂度>为时间复杂度为O (nlogn)。

4. 插入排序

4.1 直接插入排序 (Straight Insertion Sort)

  • 思想

    最简单的排序方法,将一个元素插入到已经排好序的有序表中,得到一个新的、元素加1的有序表。
    将表中的第一个元素看做是已经有序的表,然后将后面的元素依次插入到该表中。
    具体做法是,将第一个元素看做是已经有序的,从第二个到第n个元素依次进行排序,排序的元素
    先和已经排好序的表中的最后一个元素开始比较,若大于最后一个元素则插入到最后一个元素的后
    一位,若小于最后一个元素,则最后一个元素后移一位,然后和倒数第二个元素比较,依次类推,
    最终将排序的元素插入到有序列表中。然后再进行下一个元素的排序。

  • 伪代码

mark first element as sortedfor each unsorted element  'extract' the element  for i = lastSortedIndex to 0    if currentSortedElement > extractedElement      move sorted element to the right by 1    else: insert extracted element
  • 代码
void insert_sort(int *a, int n){    int i, j, temp;    for(i = 1; i < n; i++)    {        temp = a[i];        for(j = i-1; j >= 0 && a[j] > temp; j--)        {            a[j+1] = a[j];//将前面的值往后移一位        }        a[j+1] = temp; //待排序元素放在a[j]的后面    }}
  • 分析
    需要的辅助空间为1,比较和交换次数约为(n^2)/4,时间复杂度为O (n^2)。

4.2 希尔排序 (Shells Sort)

  • 思想

    先将整个待排序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,
    再对全体记录进行一次直接插入排序。子序列的分割是将相隔某个“增量”的记录组成一个序列。增量
    的选取较复杂。若增量选取为:n,n/2,n/4,n/8···2,1 则称之为折半插入排序。所以说折半插入排序是希
    尔排序的一种,下面以折半插入排序为例进行介绍。

  • 代码

void shell_sort(int *a, int n)  //希尔排序(折半插入排序){    int i, j, flag, temp;    int gap = n;    while(gap > 1)    {        gap = gap/2; //增量缩小,每次减半(折半)        do        {            flag = 0;            //n-gap是控制上限不让越界            for(i = 0; i < n-gap; i++)            {                j = i + gap; //相邻间隔的前后值进行比较                if(a[i] > a[j])                {                    temp = a[i];                    a[i] = a[j];                    a[j] = temp;                    flag = 1;                }            }        }while(flag != 0);    }}
  • 分析
    需要的辅助空间和直接插入排序相同,比较次数较直接插入排序减少了,但交换次数不变,故约时间复杂度仍为O (n^2)。若增量选取的好,希尔排序的时间复杂度可以降为O (n^3/2)。

5. 归并排序 (Merge Sort)

  • 思想

    归并排序建立在归并操作上的一种有效的排序算法。该算法是采用分治法。
    首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
    解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?
    可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。

  • 代码

//合并两个有序数列//从first到mid为第一个序列,mid+1到last为第二个序列void mergearray(int *a,int first,int mid,int last,int *temp){    int i=first,j= mid +1;    int n=mid,  m= last;    int k=0;    while(i<=n && j<=m)    {        if(a[i]<=a[j])            temp[k++]=a[i++];        else            temp[k++]=a[j++];    }    while(i<=n)    {        temp[k++]=a[i++];    }    while(j<=m)    {        temp[k++]=a[j++];    }    for(i=0;i<k;i++)    {        a[first+i]=temp[i]; //将缓存到temp中的有序序列转移到a数组中    }}//归并排序void merge_sort(int *a,int first,int last,int *temp){    if(first < last)    {        int mid =(first + last)/2;        merge_sort(a,first,mid,temp);  //左侧序列有序        merge_sort(a,mid+1,last,temp); //右侧序列有序        mergearray(a,first,mid,last,temp); //两个有序序列合并    }}
  • 分析
    将数列分开成小数列一共要logn步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(n),故时间复杂度一共为O (nlogn)。
    辅助空间为n。
    是一种稳定的排序算法(快速排序和堆排序都是不稳定的)。

6. 计数排序 (Count sort)

  • 思想

    计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
    算法的步骤如下:

    1. 找出待排序的数组中最大和最小的元素
    2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
    3. 对所有的计数累加(从C中的位置为1的元素开始,每一项和前一项相加)
    4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
  • 代码

    void count_sort(int *a,int n){ int i,j,num,max=-INF,min=INF; for(i=0;i<n;i++)   //找最大值最小值 {     if(a[i]>max)     {         max=a[i];     }     if(a[i]<min)     {         min=a[i];     } } num=max-min+1;  //计算C数组的大小 int c[num]; for(i=0;i<num;i++) //初始化C数组 {     c[i]=0; } for(i=0;i<n;i++)  //填充C数组 {     c[a[i]-min]++; } j=0; i=0; while(i<n && j<num) //从C数组中取值出来 {     while(i<n && c[j]!=0)     {         a[i]=j+min;         c[j]--;         i++;     }     j++; }}
  • 分析

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。时间复杂度为O (n)。 是一种稳定排序算法。

7. 总结

比较以上几种排序算法,有以下结果:

排序方法 平均情况 最好情况 最坏情况 辅助空间 稳定性 冒泡排序 O (n^2) O (n) O (n^2) O (1) 稳定 简单选择排序 O (n^2) O (n^2) O (n^2) O (1) 稳定 直接插入排序 O (n^2) O (n) O (n^2) O (1) 稳定 希尔排序 O (nlogn)~ O (n^2) O (n^1.3) O (n^2) O (1) 不稳定 堆排序 O (nlogn) O (nlogn) O (nlogn) O (1) 不稳定 归并排序 O (nlogn) O (nlogn) O (nlogn) O (n) 稳定 快速排序 O (nlogn) O (nlogn) O (n^2) O (logn)~ O (n) 不稳定

以上就是对排序算法的一个小的总结,还有一些算法没有涉及到,如果以后涉及到了,再做补充。

参考:
1. 《数据结构-C语言版》(严蔚敏,吴伟民版);
2. http://blog.csdn.net/jnu_simba/article/details/9705111?utm_source=tuicool&utm_medium=referral
3. http://blog.csdn.net/morewindows/article/details/6709644
4. http://blog.csdn.net/morewindows/article/details/6678165/

0 0