经典排序算法

来源:互联网 发布:淘宝首页在线制作 编辑:程序博客网 时间:2024/06/05 14:09

经典的八大算法总结

排序的分类可以分为两种:

  1. 内排序
    在排序过程中,全部记录存放在内存,则称为内排序
  2. 外排序
    如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。

内排序可以分为以下几类:

  (1)、插入排序:直接插入排序、二分法插入排序、希尔排序。

  (2)、选择排序:简单选择排序、堆排序。

  (3)、交换排序:冒泡排序、快速排序。

  (4)、归并排序

  (5)、基数排序

  如图:
  这里写图片描述


各种排序算法的时间复杂度和空间复杂度


这里写图片描述

交换排序

1、冒泡排序(Bubble Sort)
冒泡排序是一种极其简单的排序算法,它重复地走访过要排序的元素,依次比较相邻两个元素,如果他们的顺序错误就把他们调换过来,直到没有元素再需要交换,排序完成。这个算法的名字由来是因为越小(或越大)的元素会经由交换慢慢“浮”到数列的顶端。
  冒泡排序算法的运作如下:
1、比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3、针对所有的元素重复以上的步骤,除去n-m(n指参与排序的元素个数,m指当前所执行的是第几次排序)。
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

示例:
以数组{ 9, 6, 3, 1, 4, 8, 2, 3 }为例执行冒泡排序

C语言版:

#include <stdio.h>void Swap(int A[], int i, int j){    int temp = A[i];    A[i] = A[j];    A[j] = temp;}void BubbleSort(int A[], int n){    for (int j = 0; j < n - 1; j++)         // 每次最大元素就像气泡一样"浮"到数组的最后    {        for (int i = 0; i < n - 1 - j; i++) // 依次比较相邻的两个元素,使较大的那个向后移        {            if (A[i] > A[i + 1])            // 如果条件改成A[i] >= A[i + 1],则变为不稳定的排序算法            {                Swap(A, i, i + 1);            }        }    }}int main(){    int A[] = { 9, 6, 3, 1, 4, 8, 2, 3 };    // 从小到大冒泡排序    int n = sizeof(A) / sizeof(int);         //计算数组的长度    BubbleSort(A, n);    printf("冒泡排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}

java 版本:

package com.sort;public class BubbleSort {    public static void main(String[] args) {        int[] a={ 9, 6, 3, 1, 4, 8, 2, 3 };        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //冒泡排序        for (int i = 0; i < a.length; i++) {            for(int j = 0; j<a.length-i-1; j++){                //这里-i主要是每遍历一次都把最大的i个数沉到最底下去了                if(a[j]>a[j+1]){                    int temp = a[j];                    a[j] = a[j+1];                    a[j+1] = temp;                }            }        }        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }}

排序实现过程如图:
这里写图片描述

分析:
冒泡排序简单,但对于元素很多的情况下,冒泡排序是基本没什么效率的。

定向冒泡排序(冒泡排序算法的改进)


此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。

C语言版:

#include <stdio.h>void Swap(int A[], int i, int j){    int temp = A[i];    A[i] = A[j];    A[j] = temp;}void CocktailSort(int A[], int n){    int left = 0;                            // 初始化边界    int right = n - 1;    while (left < right)    {        for (int i = left; i < right; i++)   // 前半轮,将最大元素放到后面        {            if (A[i] > A[i + 1])            {                Swap(A, i, i + 1);            }        }        right--;        for (int i = right; i > left; i--)   // 后半轮,将最小元素放到前面        {            if (A[i - 1] > A[i])            {                Swap(A, i - 1, i);            }        }        left++;    }}int main(){    int A[] = { 9, 6, 3, 1, 4, 8, 2, 3 };   // 从小到大定向冒泡排序    int n = sizeof(A) / sizeof(int);    CocktailSort(A, n);    printf("定向冒泡排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}

java版:

package com.sort;//稳定的排序public class BubbleSort {    public static void main(String[] args) {        int[] a={ 9, 6, 3, 1, 4, 8, 2, 3 };        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }       CocktailSort(a);        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }    //定向冒泡排序    void CocktailSort(int A[]){    int left = 0;                            // 初始化边界    int right = a.length-1;    while (left < right)    {        for (int i = left; i < right; i++)   // 前半轮,将最大元素放到后面        {            if (A[i] > A[i + 1])            {                Swap(A, i, i + 1);            }        }        right--;        for (int i = right; i > left; i--)   // 后半轮,将最小元素放到前面        {            if (A[i - 1] > A[i])            {                Swap(A, i - 1, i);            }        }        left++;    }}}


实现过程如图:

这里写图片描述

分析:
它比冒泡排序稍微好一点的效能,但在乱数序列的状态下,它和冒泡排序效率同样很低

2、快速排序

基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。

快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:
1、从序列中挑出一个元素,作为”基准”(pivot).
2、把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
3、对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

示例:

C语言版:

#include <stdio.h>void Swap(int A[], int i, int j){    int temp = A[i];    A[i] = A[j];    A[j] = temp;}int Partition(int A[], int left, int right)  // 划分函数{    int pivot = A[right];               // 这里每次都选择最后一个元素作为基准    int tail = left - 1;                // tail为小于基准的子数组最后一个元素的索引    for (int i = left; i < right; i++)  // 遍历基准以外的其他元素    {        if (A[i] <= pivot)              // 把小于等于基准的元素放到前一个子数组末尾        {            Swap(A, ++tail, i);        }    }    Swap(A, tail + 1, right);           // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组                                        // 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法    return tail + 1;                    // 返回基准的索引}void QuickSort(int A[], int left, int right){    if (left >= right)        return;    int pivot_index = Partition(A, left, right); // 基准的索引    QuickSort(A, left, pivot_index - 1);    QuickSort(A, pivot_index + 1, right);}int main(){    int A[] = { 9, 6, 8, 5, 3, 6, 7, 3, 5 }; // 从小到大快速排序    int n = sizeof(A) / sizeof(int);    QuickSort(A, 0, n - 1);    printf("快速排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}


java版:

package com.sort;//不稳定public class QuickSort {    public static void main(String[] args) {        int[] a={ 9, 6, 8, 5, 3, 6, 7, 3, 5 };        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //快速排序        quick(a);        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }    private static void quick(int[] a) {        if(a.length>0){            quickSort(a,0,a.length-1);        }    }    private static void quickSort(int[] a, int low, int high) {        if(low<high){ //如果不加这个判断递归会无法退出导致堆栈溢出异常            int middle = getMiddle(a,low,high);            quickSort(a, 0, middle-1);            quickSort(a, middle+1, high);        }    }    private static int getMiddle(int[] a, int low, int high) {        int temp = a[low];//基准元素        while(low<high){            //找到比基准元素小的元素位置            while(low<high && a[high]>=temp){                high--;            }            a[low] = a[high];             while(low<high && a[low]<=temp){                low++;            }            a[high] = a[low];        }        a[low] = temp;        return low;    }}

排序过程:
这里写图片描述

分析:
1、快速排序是不稳定的排序。

 2、快速排序的时间复杂度为O(nlogn)。

 2、当n较大时使用快排比较好,当序列基本有序时用快排反而不好。

选择排序:

1、简单选择排序:
基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。

简单选择排序与冒泡排序的区别:
冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;而选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。

示例:
C语言版:

#include <stdio.h>void Swap(int A[], int i, int j){    int temp = A[i];    A[i] = A[j];    A[j] = temp;}void SelectionSort(int A[], int n){    for (int i = 0; i < n - 1; i++)         // i为已排序序列的末尾    {        int min = i;        for (int j = i + 1; j < n; j++)     // 未排序序列        {            if (A[j] < A[min])              // 找出未排序序列中的最小值            {                min = j;            }        }        if (min != i)        {            Swap(A, min, i);    // 放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法        }    }}int main(){    int A[] = { 9, 6, 4, 8, 9, 3, 2, 4, 5, 7 }; // 从小到大选择排序    int n = sizeof(A) / sizeof(int);    SelectionSort(A, n);    printf("选择排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}

java版

package com.sort;public class SelectionSort {    public static void main(String[] args) {        int[] a={ 9, 6, 4, 8, 9, 3, 2, 4, 5, 7 };        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //简单的选择排序        for (int i = 0; i < a.length; i++) {            int min = a[i];            int n=i; //最小数的索引            for(int j=i+1;j<a.length;j++){                if(a[j]<min){  //找出最小的数                    min = a[j];                    n = j;                }            }            a[n] = a[i];            a[i] = min;        }        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }}

排序过程图:
这里写图片描述

2、堆排序

1、基本思想:
  堆排序是一种树形选择排序,是对直接选择排序的有效改进。
  堆的定义下:具有n个元素的序列 (h1,h2,…,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,…,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二 叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。
  思想:初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个 堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对 它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
  
示例:
C语言版:

#include <stdio.h>void Swap(int A[], int i, int j){    int temp = A[i];    A[i] = A[j];    A[j] = temp;}void Heapify(int A[], int i, int size)  // 从A[i]向下进行堆调整{    int left_child = 2 * i + 1;         // 左孩子索引    int right_child = 2 * i + 2;        // 右孩子索引    int max = i;                        // 选出当前结点与其左右孩子三者之中的最大值    if (left_child < size && A[left_child] > A[max])        max = left_child;    if (right_child < size && A[right_child] > A[max])        max = right_child;    if (max != i)    {        Swap(A, i, max);                // 把当前结点和它的最大(直接)子节点进行交换        Heapify(A, max, size);          // 递归调用,继续从当前结点向下进行堆调整    }}int BuildHeap(int A[], int n)           // 建堆,时间复杂度O(n){    int heap_size = n;    for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整        Heapify(A, i, heap_size);    return heap_size;}void HeapSort(int A[], int n){    int heap_size = BuildHeap(A, n);    // 建立一个最大堆    while (heap_size > 1)           // 堆(无序区)元素个数大于1,未完成排序    {        // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素        // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法        Swap(A, 0, --heap_size);        Heapify(A, 0, heap_size);     // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)    }}int main(){    int A[] = { 9, 5, 7, 4, 6, 2, 8, 3, 8 };// 从小到大堆排序    int n = sizeof(A) / sizeof(int);    HeapSort(A, n);    printf("堆排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}

java版:

package com.sort;import java.util.Arrays;public class HeapSort {    public static void main(String[] args) {        int[] a={ 9, 5, 7, 4, 6, 2, 8, 3, 8 };        int arrayLength=a.length;          //循环建堆          for(int i=0;i<arrayLength-1;i++){              //建堆              buildMaxHeap(a,arrayLength-1-i);              //交换堆顶和最后一个元素              swap(a,0,arrayLength-1-i);              System.out.println(Arrays.toString(a));          }      }    //对data数组从0到lastIndex建大顶堆    public static void buildMaxHeap(int[] data, int lastIndex){         //从lastIndex处节点(最后一个节点)的父节点开始         for(int i=(lastIndex-1)/2;i>=0;i--){            //k保存正在判断的节点             int k=i;            //如果当前k节点的子节点存在              while(k*2+1<=lastIndex){                //k节点的左子节点的索引                 int biggerIndex=2*k+1;                //如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在                if(biggerIndex<lastIndex){                      //若果右子节点的值较大                      if(data[biggerIndex]<data[biggerIndex+1]){                          //biggerIndex总是记录较大子节点的索引                          biggerIndex++;                      }                  }                  //如果k节点的值小于其较大的子节点的值                  if(data[k]<data[biggerIndex]){                      //交换他们                      swap(data,k,biggerIndex);                      //将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值                      k=biggerIndex;                  }else{                      break;                  }              }        }    }    //交换    private static void swap(int[] data, int i, int j) {          int tmp=data[i];          data[i]=data[j];          data[j]=tmp;      } }

排序过程图:
这里写图片描述

分析:
堆排序也是一种不稳定的排序算法。
  堆排序优于简单选择排序的原因:
  1、直接选择排序中,为了从R[1..n]中选出关键字最小的记录,必须进行n-1次比较,然后在R[2..n]中选出关键字最小的记录,又需要做n-2次比较。事实上,后面的n-2次比较中,有许多比较可能在前面的n-1次比较中已经做过,但由于前一趟排序时未保留这些比较结果,所以后一趟排序时又重复执行了这些比较操作。
  2、堆排序可通过树形结构保存部分比较结果,可减少比较次数。
  3、堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

插入排序:
思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置,直到全部插入排序完为止。
关键问题:在前面已经排好序的序列中找到合适的插入位置。
1、直接插入排序(从后往前找到合适的插入位置)

基本思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止。
具体算法描述如下:

1、从第一个元素开始,该元素可以认为已经被排序
2、取出下一个元素,在已经排序的元素序列中从后向前扫描
3、如果该元素(已排序)大于新元素,将该元素移到下一位置
4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5、将新元素插入到该位置后
6、重复步骤2~5

示例:
C语言版:

#include <stdio.h>void InsertionSort(int A[], int n){    for (int i = 1; i < n; i++)         // 类似抓扑克牌排序    {        int get = A[i];                 // 右手抓到一张扑克牌        int j = i - 1;                  // 拿在左手上的牌总是排序好的        while (j >= 0 && A[j] > get)    // 将抓到的牌与手牌从右向左进行比较        {            A[j + 1] = A[j];            // 如果该手牌比抓到的牌大,就将其右移            j--;        }        A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)    }}int main(){    int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 从小到大插入排序    int n = sizeof(A) / sizeof(int);    InsertionSort(A, n);    printf("插入排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}

java版:

package com.sort;public class InsertionSort {    public static void main(String[] args) {        int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //直接插入排序        for (int i = 1; i < a.length; i++) {            //待插入元素            int temp = a[i];            int j;            /*for (j = i-1; j>=0 && a[j]>temp; j--) {                //将大于temp的往后移动一位                a[j+1] = a[j];            }*/            for (j = i-1; j>=0; j--) {                //将大于temp的往后移动一位                if(a[j]>temp){                    a[j+1] = a[j];                }else{                    break;                }            }            a[j+1] = temp;        }        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }}

排序过程:
这里写图片描述

2、希尔排序(按二分法找到合适位置插入)
基本思想:二分法插入排序的思想和直接插入一样,只是找合适的插入位置的方式不同,这里是按二分法找到合适的位置,可以减少比较的次数

示例:
C语言版本:

#include <stdio.h>void InsertionSortDichotomy(int A[], int n){    for (int i = 1; i < n; i++)    {        int get = A[i];                    // 右手抓到一张扑克牌        int left = 0;                    // 拿在左手上的牌总是排序好的,所以可以用二分法        int right = i - 1;                // 手牌左右边界进行初始化        while (left <= right)            // 采用二分法定位新牌的位置        {            int mid = (left + right) / 2;            if (A[mid] > get)                right = mid - 1;            else                left = mid + 1;        }        for (int j = i - 1; j >= left; j--)    // 将欲插入新牌位置右边的牌整体向右移动一个单位        {            A[j + 1] = A[j];        }        A[left] = get;                    // 将抓到的牌插入手牌    }}int main(){    int A[] = {49,38,65,97,176,213,227,49,78,34,12,164,11,18,1};// 从小到大二分插入排序    int n = sizeof(A) / sizeof(int);    InsertionSortDichotomy(A, n);    printf("二分插入排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}

java版本:

package com.sort;public class InsertionSortDichotomy {    public static void main(String[] args) {        int[] a={49,38,65,97,176,213,227,49,78,34,12,164,11,18,1};        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //二分插入排序        sort(a);        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }    private static void sort(int[] a) {        for (int i = 0; i < a.length; i++) {            int temp = a[i];            int left = 0;            int right = i-1;            int mid = 0;            while(left<=right){                mid = (left+right)/2;                if(temp<a[mid]){                    right = mid-1;                }else{                    left = mid+1;                }            }            for (int j = i-1; j >= left; j--) {                a[j+1] = a[j];            }            if(left != i){                a[left] = temp;            }        }    }}

分析:
当然,二分法插入排序也是稳定的。
  二分插入排序的比较次数与待排序记录的初始状态无关,仅依赖于记录的个数。当n较大时,比直接插入排序的最大比较次数少得多。但大于直接插入排序的最小比较次数。算法的移动次数与直接插入排序算法的相同,最坏的情况为n2/2,最好的情况为n,平均移动次数为O(n2)。

3、希尔排序
1、基本思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2

#include <stdio.h>  void ShellSort(int A[], int n){    int h = 0;    while (h <= n)                          // 生成初始增量    {        h = 3 * h + 1;    }    while (h >= 1)    {        for (int i = h; i < n; i++)        {            int j = i - h;            int get = A[i];            while (j >= 0 && A[j] > get)            {                A[j + h] = A[j];                j = j - h;            }            A[j + h] = get;        }        h = (h - 1) / 3;                    // 递减增量    }}int main(){    int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大希尔排序    int n = sizeof(A) / sizeof(int);    ShellSort(A, n);    printf("希尔排序结果:");    for (int i = 0; i < n; i++)    {        printf("%d ", A[i]);    }    printf("\n");    return 0;}

java版:

package com.sort;//不稳定public class ShellSort {    public static void main(String[] args) {        int[] a={ 5, 2, 9, 4, 7, 6, 1, 3, 8 };        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //希尔排序        int d = a.length;        while(true){            d = d / 2;            for(int x=0;x<d;x++){                for(int i=x+d;i<a.length;i=i+d){                    int temp = a[i];                    int j;                    for(j=i-d;j>=0&&a[j]>temp;j=j-d){                        a[j+d] = a[j];                    }                    a[j+d] = temp;                }            }            if(d == 1){                break;            }        }        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }}

排序过程:
这里写图片描述

分析:
希尔排序是不稳定的排序算法,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。

归并排序
基本思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),1945年由冯·诺伊曼首次提出。

  归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

  归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:

1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4、重复步骤3直到某一指针到达序列尾
5、将另一序列剩下的所有元素直接复制到合并序列尾

示例
C语言版:

#include <stdio.h>#include <limits.h>void Merge(int A[], int left, int mid, int right)// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]{    int len = right - left + 1;    int *temp = new int[len];       // 辅助空间O(n)    int index = 0;    int i = left;                   // 前一数组的起始元素    int j = mid + 1;                // 后一数组的起始元素    while (i <= mid && j <= right)    {        temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];  // 带等号保证归并排序的稳定性    }    while (i <= mid)    {        temp[index++] = A[i++];    }    while (j <= right)    {        temp[index++] = A[j++];    }    for (int k = 0; k < len; k++)    {        A[left++] = temp[k];    }}void MergeSortRecursion(int A[], int left, int right)    // 递归实现的归并排序(自顶向下){    if (left == right)    // 当待排序的序列长度为1时,递归开始回溯,进行merge操作        return;    int mid = (left + right) / 2;    MergeSortRecursion(A, left, mid);    MergeSortRecursion(A, mid + 1, right);    Merge(A, left, mid, right);}void MergeSortIteration(int A[], int len)    // 非递归(迭代)实现的归并排序(自底向上){    int left, mid, right;// 子数组索引,前一个为A[left...mid],后一个子数组为A[mid+1...right]    for (int i = 1; i < len; i *= 2)        // 子数组的大小i初始为1,每轮翻倍    {        left = 0;        while (left + i < len)              // 后一个子数组存在(需要归并)        {            mid = left + i - 1;            right = mid + i < len ? mid + i : len - 1;// 后一个子数组大小可能不够            Merge(A, left, mid, right);            left = right + 1;               // 前一个子数组索引向后移动        }    }}int main(){    int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 };      // 从小到大归并排序    int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };    int n1 = sizeof(A1) / sizeof(int);    int n2 = sizeof(A2) / sizeof(int);    MergeSortRecursion(A1, 0, n1 - 1);          // 递归实现    MergeSortIteration(A2, n2);                 // 非递归实现    printf("递归实现的归并排序结果:");    for (int i = 0; i < n1; i++)    {        printf("%d ", A1[i]);    }    printf("\n");    printf("非递归实现的归并排序结果:");    for (int i = 0; i < n2; i++)    {        printf("%d ", A2[i]);    }    printf("\n");    return 0;}

Java版:

package com.sort;//稳定public class MergeSortRecursion {    public static void main(String[] args) {        int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8};        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //归并排序        mergeSort(a,0,a.length-1);        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }    private static void mergeSort(int[] a, int left, int right) {        if(left<right){            int middle = (left+right)/2;            //对左边进行递归            mergeSort(a, left, middle);            //对右边进行递归            mergeSort(a, middle+1, right);            //合并            merge(a,left,middle,right);        }    }    private static void merge(int[] a, int left, int middle, int right) {        int[] tmpArr = new int[a.length];        int mid = middle+1; //右边的起始位置        int tmp = left;        int third = left;        while(left<=middle && mid<=right){            //从两个数组中选取较小的数放入中间数组            if(a[left]<=a[mid]){                tmpArr[third++] = a[left++];            }else{                tmpArr[third++] = a[mid++];            }        }        //将剩余的部分放入中间数组        while(left<=middle){            tmpArr[third++] = a[left++];        }        while(mid<=right){            tmpArr[third++] = a[mid++];        }        //将中间数组复制回原数组        while(tmp<=right){            a[tmp] = tmpArr[tmp++];        }    }}

排序过程:
这里写图片描述

分析:

基数排序
基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

示例:
java版:

package com.sort;import java.util.ArrayList;import java.util.List;//稳定public class CardinalSort {    public static void main(String[] args) {        int[] a={49,38,65,97,176,213,227,49,78,34,12,164,11,18,1};        System.out.println("排序之前:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }        //基数排序        sort(a);        System.out.println();        System.out.println("排序之后:");        for (int i = 0; i < a.length; i++) {            System.out.print(a[i]+" ");        }    }    private static void sort(int[] array) {        //找到最大数,确定要排序几趟        int max = 0;        for (int i = 0; i < array.length; i++) {            if(max<array[i]){                max = array[i];            }        }        //判断位数        int times = 0;        while(max>0){            max = max/10;            times++;        }        //建立十个队列        List<ArrayList> queue = new ArrayList<ArrayList>();        for (int i = 0; i < 10; i++) {            ArrayList queue1 = new ArrayList();            queue.add(queue1);        }        //进行times次分配和收集        for (int i = 0; i < times; i++) {            //分配            for (int j = 0; j < array.length; j++) {                int x = array[j]%(int)Math.pow(10, i+1)/(int)Math.pow(10, i);                ArrayList queue2 = queue.get(x);                queue2.add(array[j]);                queue.set(x,queue2);            }            //收集            int count = 0;            for (int j = 0; j < 10; j++) {                while(queue.get(j).size()>0){                    ArrayList<Integer> queue3 = queue.get(j);                    array[count] = queue3.get(0);                    queue3.remove(0);                    count++;                }            }        }    }}

分析:
基数排序是稳定的排序算法。
  基数排序的时间复杂度为O(d(n+r)),d为位数,r为基数。

总结:

一、稳定性:

  稳定:冒泡排序、插入排序、归并排序和基数排序

  不稳定:选择排序、快速排序、希尔排序、堆排序

二、平均时间复杂度

  O(n^2):直接插入排序,简单选择排序,冒泡排序。

  在数据规模较小时(9W内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为O(n^2)的算法基本上是相邻元素进行比较,基本上都是稳定的。

  O(nlogn):快速排序,归并排序,希尔排序,堆排序。

  其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。

三、排序算法的选择

  1.数据规模较小

  (1)待排序列基本序的情况下,可以选择直接插入排序;

  (2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡

  2.数据规模不是很大

  (1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出log(N)的额外空间。

  (2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序

  3.数据规模很大

  (1)对稳定性有求,则可考虑归并排序。

 (1)对稳定性没有要求,则可考虑用堆排序。

  4.序列初始基本有序(正序),宜用直接插入,冒泡

原创粉丝点击