排序

排序算法总结

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 sorted

for 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.youkuaiyun.com/jnu_simba/article/details/9705111?utm_source=tuicool&utm_medium=referral
3. http://blog.youkuaiyun.com/morewindows/article/details/6709644
4. http://blog.youkuaiyun.com/morewindows/article/details/6678165/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值