八大排序算法总结

排序算法稳定性

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

插入排序

直接插入排序

算法

直接插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。

设数组为a[0…n-1]。

  1. 初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1

  2. 将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。

  3. i++并重复第二步直到i==n-1。排序完成。

将搜索和数据后移这二个步骤合并。即每次a[i]先和前面一个数据a[i-1]比较,如果a[i] > a[i-1]说明a[0…i]也是有序的,无须调整。否则就令j=i-1,temp=a[i]。然后一边将数据a[j]向后移动一边向前搜索,当有数据a[j]<a[i]时停止并将temp放到a[j + 1]处。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void Insertsort2(int a[], int n)
{
    int i, j;
    for (i = 1; i < n; i++)
        if (a[i] < a[i - 1])
        {
            int temp = a[i];
            for (j = i - 1; j >= 0 && a[j] > temp; j--)
                a[j + 1] = a[j];
            a[j + 1] = temp;
        }
}

再对将a[j]插入到前面a[0…j-1]的有序区间所用的方法进行改写,用数据交换代替数据后移。如果a[j]前一个数据a[j-1] > a[j],就交换a[j]和a[j-1],再j–直到a[j-1] <= a[j]。这样也可以实现将一个新数据新并入到有序区间。

1
2
3
4
5
6
7
void Insertsort3(int a[], int n)
{
    int i, j;
    for (i = 1; i < n; i++)
        for (j = i - 1; j >= 0 && a[j] > a[j + 1]; j--)
            Swap(a[j], a[j + 1]);
}

时间复杂度

  1. 最好的情况是已经是正序的序列,只需比较(n-1)次,时间复杂度为O(n)。

  2. 最坏的情况是倒序的序列,要比较n(n-1)/2次,时间复杂度为O(n^2 )。

  3. 平均的话要比较时间复杂度为O(n^2 )

空间复杂度

只需要2个指针,和数据量无关,所以空间复杂度为O(1)

稳定性

插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,也就是第一个元素(默认它有序)。

比较是从有序序列的末尾开始,也就是把待插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面。

否则一直往前找直到找到它该插入的位置。如果遇见一个与插入元素相等的,那么把待插入的元素放在相等元素的后面。

所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序仍是排好序后的顺序,所以插入排序是稳定的。

希尔排序

算法

希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。

该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。

随着步长逐渐减小,所分成的组包含的记录越来越多,当步长的值减小到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。

以n=10的一个数组49, 38, 65, 97, 26, 13, 27, 49, 55, 4为例

1A,1B,2A,2B等为分组标记,数字相同的表示在同一组,大写字母表示是该组的第几个元素, 每次对同一组的数据进行直接插入排序。即分成了五组(49, 13) (38, 27) (65, 49) (97, 55) (26, 4)这样每组排序后就变成了(13, 49) (27, 38) (49, 65) (55, 97) (4, 26),下同。

以第二次排序为例,原来是每次从1A到1E,从2A到2E,可以改成从1B开始,先和1A比较,然后取2B与2A比较,再取1C与前面自己组内的数据比较…….。这种每次从数组第gap个元素开始,每个元素与自己组内的数据进行直接插入排序显然也是正确的。这种方法可以简化代码。

1
2
3
4
5
6
7
8
void shellsort3(int a[], int n)
{
    int i, j, gap;
    for (gap = n/3 + 1; gap > 0; gap = gap / 3 + 1)  //确定gap
        for (i = gap; i < n; i++)  //扫描数组,枚举比较元素
            for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap)//扫描之前的同组元素
                Swap(a[j], a[j + gap]);
}

附注:最初Shell提出取gap=n/2,gap=gap/2,直到gap=1。后来Knuth提出取gap=gap/3+1,另外还有人认为gap都去奇数比较好,也有人说让各个gap值互质为好。另外,由上面的实例可以看出:该算法是不稳定的算法.

时间复杂度

步长的选择是希尔排序的重要部分。影响时间复杂度。

空间复杂度

只需要2个指针和1个gap变量,和数据量无关,所以空间复杂度为O(1)

稳定性

希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;

当元素基本有序时,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(N^2)好一些。

由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,

但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。

所以shell排序是不稳定的排序算法。

选择排序

直接选择排序

算法

直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接插入排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后。

设数组为a[0…n-1]。

  1. 初始时,数组全为无序区为a[0..n-1]。令i=0

  2. 在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0…i]就形成了一个有序区。

  3. i++并重复第二步直到i==n-1。排序完成。

直接选择排序无疑是最容易实现的,下面给出代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void Selectsort(int a[], int n)
{
    int i, j, nMinIndex;
    for (i = 0; i < n; i++)
    {
        nMinIndex = i; //找最小元素的位置
        for (j = i + 1; j < n; j++)
            if (a[j] < a[nMinIndex])
                nMinIndex = j;

        Swap(a[i], a[nMinIndex]); //将这个元素放到无序区的开头
    }
}

时间复杂度

比较次数与关键字的初始状态无关,最好和最坏情况下都为O(n^2)

空间复杂度

只需要nMinIndex和i,j,与数据量无关,所以是O(1)

稳定性

选择排序即是给每个位置选择待排序元素中当前最小的元素。比如给第一个位置选择最小的,在剩余元素里面给第二个位置选择次小的,

依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。

那么,在一趟选择时,如果当前锁定元素比后面一个元素大,而后面较小的那个元素又出现在一个与当前锁定元素相等的元素后面,那么交换后位置顺序显然改变了。

举个例子:序列5 8 5 2 9, 我们知道第一趟选择第1个元素5会与2进行交换,那么原序列中两个5的相对先后顺序也就被破坏了。

所以选择排序不是一个稳定的排序算法。

堆排序

堆给人的感觉是一个二叉树,但是其本质是一种数组对象,因为对堆进行操作的时候将堆视为一颗完全二叉树,树中每个节点与数组中的存放该节点值的那个元素对应。所以堆又称为二叉堆,堆与完全二叉树的对应关系如下图所示:

通常给定节点i,可以根据其在数组中的位置求出该节点的左右孩子节点和父节点,一般采用宏或者内联函数实现。

1
2
3
4
5
6
7
8
9
inline int parent(int idx) {//父节点
    return (idx >> 1);
}
inline int left(int idx) {//左孩子
    return (idx << 1) + 1;
}
inline int right(int idx) {//右孩子
    return (idx << 1) + 2;
}

根据节点数值满足的条件,可以将分为最大堆和最小堆:

  1. 最大堆的特性是:除了根节点以外的每个节点i,有A[PARENT(i)] >= A[i],

  2. 最小堆的特性是:除了根节点以外的每个节点i,有A[PARENT(i)] >=A[i]。

把堆看成一个棵树,有如下的特性:

  1. 含有n个元素的堆的高度是lgn。

  2. 当用数组表示存储了n个元素的堆时,叶子节点的下标是n/2+1,n/2+2,……,n。

  3. 在最大堆中,最大元素该子树的根上;在最小堆中,最小元素在该子树的根上。

算法

维护最大堆(MAX-HEAPIFY)

堆的关键操作是如何保持堆的特有性质,给定一个节点i,要保证以i为根的子树满足堆性质。以最大堆为例,以递归形式的保持最大堆性的操作过程MAX-HEAPIFY。先从看一个例子,操作过程如下图所示:

从图中可以看出,在节点i=2时,不满足最大堆的要求,需要进行调整,选择节点2的左右孩子中最大一个进行交换,然后检查交换后的节点i=4是否满足最大堆的要求,从图看出不满足,接着进行调整,直到没有交换为止。这是一个下沉的过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void max_heapify(vector<int>& nums, int idx,int n) {//检查i节点是否满足性质
    int largest = idx;//largest保持最大节点的下标
    int l = left(idx), r = right(idx);
    //注意左右孩子保证不越界
    if (l < n && nums[l] > nums[largest]) largest = l;
    if (r < n && nums[r] > nums[largest]) largest = r;
    if (largest != idx) {//需要进行维护
        swap(nums[idx], nums[largest]);
        max_heapify(nums, largest,n);//递归向下调整
    }
}

此操作可以用递推来表示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void max_heapify(vector<int>& nums, int idx,int n) {//检查i节点是否满足性质
    int largest = idx;//largest保持最大节点的下标
    int l=left(idx);//先求出左孩子的位置,方便判断是否到达下沉终点。
    while(l<n){//仍有孩子节点
        int r = right(idx);
        //注意左右孩子保证不越界
        if (l < n && nums[l] > nums[largest]) largest = l;
        if (r < n && nums[r] > nums[largest]) largest = r;
        if (largest == idx) {//不需要进行维护
            break;
        }
        swap(nums[idx], nums[largest]);
        idx=largest;
        l=left(idx);
    }
}

建立最大堆(BUILD-MAX-HEAP)

建立最大堆的过程是自底向上地调用最大堆调整程序将一个数组A[1…..N]变成一个最大堆。将数组视为一颗完全二叉树,从其最后一个非叶子节点(n/2)开始调整。调整过程如下图所示:

1
2
3
4
5
6
void build_max_heap(vector<int>& nums) {
    heap_size = nums.size();
    for (int i = (heap_size >> 1) - 1; i >= 0; i--)
    //(heap_size >> 1) - 1最后一个父节点
        max_heapify(nums, i);
}

最大堆排序(HEAPSORT)

堆排序算法过程为:先调用创建堆函数将输入数组A[1…n]造成一个最大堆,使得最大的值存放在数组第一个位置A[1],然后用数组最后一个位置元素与第一个位置进行交换,并将堆的大小减少1,并调用最大堆调整函数从第一个位置调整最大堆。

1
2
3
4
5
6
7
8
9
void heapsort(vector<int>& nums, int k) {
    build_max_heap(nums);
    int n=nums.size();
    for (int i = 0; i < k; i++) {
        swap(nums[0], nums[n-1]);
        n--;
        max_heapify(nums, 0,n);
    }
}

最大堆的插入(MAX-HEAP-INSERT)

每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据并入到有序区间中,这是一个上溯过程(FIX_UP).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int maxheapinsert(int *heap, int n, int num)
{
    int i, j;
    heap[n] = num;//num插入堆尾
    i = n;
    j = parent(i);//j指向i的父结点

    //注意不要漏掉i!=0的条件。因为必须保证i有父结点j。j>=0并不能保证i!=0。
    //如果没有此条件,当i=0时,j=0,若heap[0]<num,程序就会陷入死循环。
    while (j >= 0 && i != 0)
    {
        if (heap[j] >= num)//当i的父节点比num大的时候,i就是num该放的位置
            break;
        heap[i] = heap[j];//类似插入排序,将j位的元素换到i位。
        i = j;
        j = (i - 1) / 2;
    }
    heap[i] = num;
    return 0;
}

可以进行和插入排序相似的代码优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int maxheapinsert(int *heap, int n, int num)
{
    int i, j;
    heap[n] = num;//num插入堆尾
    i = n;
    j = parent(i);//j指向i的父结点
    for (int j = (i - 1) / 2;
            (j >= 0 && i != 0)&& a[i] < a[j];
            i = j, j = (i - 1) / 2)
        Swap(a[i], a[j]);
}

最大堆的删除(HEAP-EXTRACT-MAX)

按定义,堆中每次都只能删除第0个数据。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。相当于从根结点将一个数据的“下沉”过程。

1
2
3
4
5
6
7
8
int heapextractmax(int *heap, int n)
{
//使用堆尾元素直接覆盖堆顶元素。
heap[0] = heap[n - 1];
//从堆顶到堆尾(此时堆中只有n-1个元素)进行堆调整。
max_heapify(nums, i,n-1);
return 0;
}

时间复杂度

堆排序的时间复杂度,主要在初始化堆过程和每次选取最大数后重新建堆的过程;

  1. MAX-HEAPIFY过程时间复杂度:O(lgn)

  2. BUILD-MAX-HEAP过程时间复杂度:O(n)

    推算过程:

    首先要理解怎么计算这个堆化过程所消耗的时间,可以直接画图去理解;

    假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;

    那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要比较的次数,如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;

    S = 2^(k-2) 1 + 2^(k-3)2…..+2(k-2)+2^(0)(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;

    这个等式求解,我想高中已经会了:等式左右乘上2,然后和原来的等式相减,就变成了:

    S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ….. + 2 - (k-1)

    除最后一项外,就是一个等比数列了,直接用求和公式:S = { a1[ 1- (q^n) ] } / (1-q);

    S = 2^k -k -1;又因为k为完全二叉树的深度,所以 (2^k) <= n < (2^k -1 ),总之可以认为:k = logn (实际计算得到应该是 log(n+1) < k <= logn );

    综上所述得到:S = n - longn -1,所以时间复杂度为:O(n)

  3. HEAPSORT时间复杂度:O(nlogn)

    推算过程:

    循环 n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn - logn ;

综上所述:堆排序的时间复杂度为:O(nlogn),并且与关键字的初始状态无关。

空间复杂度

因为堆排序是就地排序,而且所有的操作都可以用递推表示不需要递归栈,所以空间复杂度为常数:O(1)

稳定性

我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。

在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。

但当为n/2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。

有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。

所以,堆排序不是稳定的排序算法。

交换排序

冒泡排序

算法

冒泡排序是非常容易理解和实现,,以从小到大排序举例: 设数组长度为N。

1.比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换。

2.这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1个位置。

3.N=N-1,如果N不为0就重复前面二步,否则排序完成。

按照定义很容易写出代码:

1
2
3
4
5
6
7
8
9
//冒泡排序1
void BubbleSort1(int a[], int n)
{
   int i, j;
   for (i = 0; i < n; i++)
          for (j = 1; j < n - i; j++)
                 if (a[j - 1] > a[j])
                        Swap(a[j - 1], a[j]);
}

下面对其进行优化,设置一个标志,如果这一趟发生了交换,则为true,否则为false。明显如果有一趟没有发生交换,说明排序已经完成。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//冒泡排序2
void BubbleSort2(int a[], int n)
{
       int j, k;
       bool flag;

       k = n;
       flag = true;
       while (flag)
       {
              flag = false;
              for (j = 1; j < k; j++)
                     if (a[j - 1] > a[j])
                     {
                            Swap(a[j - 1], a[j]);
                            flag = true;
                     }
              k--;
       }
}

再做进一步的优化。如果有100个数的数组,仅前面10个无序,后面90个都已排好序且都大于前面10个数字,那么在第一趟遍历后,最后发生交换的位置必定小于10,且这个位置之后的数据必定已经有序了,记录下这位置,第二次只要从数组头部遍历到这个位置就可以了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//冒泡排序3
void BubbleSort3(int a[], int n)
{
    int j, k;
    int flag;

    flag = n;
    while (flag > 0)
    {
        k = flag;
        flag = 0;
        for (j = 1; j < k; j++)
            if (a[j - 1] > a[j])
            {
                Swap(a[j - 1], a[j]);
                flag = j;
            }
    }
}

冒泡排序毕竟是一种效率低下的排序方法,在数据规模很小时,可以采用。数据规模比较大时,最好用其它排序方法。

时间复杂度

  1. 最坏的情况是反序序列,要比较n(n-1)/2次,时间复杂度为O(n^2 ),

  2. 最好的情况是正序,只进行(n-1)次比较,不需要移动,时间复杂度为O(n)

  3. 平均的时间复杂度为O(n^2 )

空间复杂度

辅助空间和数据量无关,所以空间复杂度为O(1)

稳定性

冒泡排序就是把小的元素往前调(或者把大的元素往后调)。注意是相邻的两个元素进行比较,而且是否需要交换也发生在这两个元素之间。

所以,如果两个元素相等,我想你是不会再无聊地把它们俩再交换一下。

如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个元素相邻起来,最终也不会交换它俩的位置,所以相同元素经过排序后顺序并没有改变。

所以冒泡排序是一种稳定排序算法。

快速排序

算法

该方法的基本思想是:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。 3.再对左右区间重复第二步,直到各区间只有一个数。

虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我对快速排序作了进一步的说明:挖坑填数+分治法:

  1. i =L; j = R; 将基准数挖出形成第一个坑a[i]。

  2. j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。

  3. i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。

  4. 再重复执行2,3二步,直到i==j,将基准数填入a[i]中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void quick_sort(int s[], int l, int r)
{
    if (l < r)//partion步骤
    {
        int i = l, j = r, x = s[l];  //s[l]即s[i]就是第一个坑
        while (i < j)
        {
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
                j--;
            if(i < j)    //将s[j]填到s[i]中,s[j]就形成了一个新的坑
                s[i++] = s[j];

            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
                i++;
            if(i < j)   //将s[i]填到s[j]中,s[i]就形成了一个新的坑
                s[j--] = s[i];
        }
        s[i] = x;
        quick_sort(s, l, i - 1); // 递归调用
        quick_sort(s, i + 1, r);
    }
}

时间复杂度

在最优情况下,Partition每次都划分得很均匀,第一次Partiation对整个数组扫描一遍,需要时间为T(n)然后,获得的枢轴将数组一分为二,那么各自还需要T(n/2)的时间获得的枢轴将数组一分为二。

在最坏的情况下,待排序的序列为正序或者逆序,每次枢轴都是取最大或者最小。每次划分只得到一个比上一次划分少一个记录的子序列,另一个为空。。此时需要执行n‐1次递归调用,且第i次划分需要经过n‐i次关键字的比较才能找到第i个记录,也就是枢轴的位置。总计比较n(n-1)/2次,时间复杂度为O(n^2 )

根据平均情况来说是O(nlogn),因为在数据分布等概率的情况下对于单个数据来说在logn次移动后就会被放到正确的位置上了。

空间复杂度

快速排序在对序列的操作过程中只需花费常数级的空间。空间复杂度S(1)。 但需要注意递归栈上需要花费最少logn 最多n的空间。所以平均空间复杂度为O(logn)

稳定性

快速排序有两个方向,左边的i下标一直往右走(当条件a[i] <= a[center_index]时),其中center_index是中枢元素的数组下标,一般取为数组第0个元素。

而右边的j下标一直往左走(当a[j] > a[center_index]时)。

如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。交换a[j]和a[center_index],完成一趟快速排序。

在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11

现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱。

所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。

归并排序

算法

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void MemeryArray(int a[], int n, int b[], int m, int c[])
{
    int i, j, k;

    i = j = k = 0;
    while (i < n && j < m)
    {
        if (a[i] < b[j])
            c[k++] = a[i++];
        else
            c[k++] = b[j++];
    }

    while (i < n)
        c[k++] = a[i++];

    while (j < m)
        c[k++] = b[j++];
}

可以看出合并有序数列的效率是比较高的,可以达到O(n)。

解决了上面的合并有序数列问题,再来看归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序了?

可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//将有二个有序数列a[first...mid]和a[mid...last]合并。
void mergearray(int a[], int first, int mid, int last, int temp[])
{
    int i = first, j = mid + 1;
    int m = mid,   n = last;
    int k = 0;

    while (i <= m && j <= n)
    {
        if (a[i] <= a[j])
            temp[k++] = a[i++];
        else
            temp[k++] = a[j++];
    }

    while (i <= m)
        temp[k++] = a[i++];

    while (j <= n)
        temp[k++] = a[j++];

    for (i = 0; i < k; i++)
        a[first + i] = temp[i];
}
void mergesort(int a[], int first, int last, int temp[])
{
    if (first < last)
    {
        int mid = (first + last) / 2;
        mergesort(a, first, mid, temp);    //左边有序
        mergesort(a, mid + 1, last, temp); //右边有序
        mergearray(a, first, mid, last, temp); //再将二个有序数列合并
    }
}

bool MergeSort(int a[], int n)
{
    int *p = new int[n];
    if (p == NULL)
        return false;
    mergesort(a, 0, n - 1, p);
    delete[] p;
    return true;
}

注:有的书上是在mergearray()合并有序数列时分配临时数组,但是过多的new操作会非常费时。因此作了下小小的变化。只在MergeSort()中new一个临时数组。后面的操作都共用这一个临时数组。

时间复杂度

比较和移动次数没有好坏之分,时间复杂度为O(n*logn)

空间复杂度

归并排序每次merge都要用到一个辅助表,长度与待排序的表长度相同,递归栈花费空间是O(log2n),如果在递归过程中建立辅助表,那么在递归结束后会释放辅助表所占的空间,不会增大递归过程中的花费空间。

因而空间复杂度还是O(n)。

稳定性

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),

然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。

可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。

那么,在短的有序序列合并的过程中,稳定是是否受到破坏?

没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。

所以,归并排序也是稳定的排序算法。

基数排序

算法

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

桶排序

基本思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。

简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。

基数排序

基数排序(以整形为例),将整形10进制按每位拆分,然后从低位到高位依次比较各个位。主要分为两个过程:

分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中(比如53,个位为3,则放入3号桶中) 收集,再将放置在0~9号桶中的数据按顺序放到数组中 重复(1)(2)过程,从个位到最高位(比如32位无符号整形最大数4294967296,最高位10位)

以【521 310 72 373 15 546 385 856 187 147】序列为例,具体细节如下图所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int GetNumInPos(int num,int pos)  //找到num的从低到高的第pos位的数据
{
    int temp = 1;
    for (int i = 0; i < pos - 1; i++)
        temp *= 10;

    return (num / temp) % 10;
}

#define RADIX_10 10    //整型排序
#define KEYNUM_31 10     //关键字个数,这里为整型位数
void RadixSort(int* pDataArray, int iDataNum)
{
    int *radixArrays[RADIX_10];    //分别为0~9的序列空间
    for (int i = 0; i < 10; i++)
    {
        radixArrays[i] = (int *)malloc(sizeof(int) * (iDataNum + 1));
        radixArrays[i][0] = 0;    //index为0处记录这组数据的个数
    }

    for (int pos = 1; pos <= KEYNUM_31; pos++)    //从个位开始到31位
    {
        for (int i = 0; i < iDataNum; i++)    //分配过程
        {
            int num = GetNumInPos(pDataArray[i], pos);
            int index = ++radixArrays[num][0];
            radixArrays[num][index] = pDataArray[i];
        }

        for (int i = 0, j =0; i < RADIX_10; i++)    //收集
        {
            for (int k = 1; k <= radixArrays[i][0]; k++)
                pDataArray[j++] = radixArrays[i][k];
            radixArrays[i][0] = 0;    //复位
        }
    }
}

LSD和MSD

再思考一个问题:既然我们可以从最低位到最高位进行如此的分配收集,那么是否可以由最高位到最低位依次操作呢? 答案是完全可以的。

基于两种不同的排序顺序,我们将基数排序分为LSD(Least significant digital)或MSD(Most significant digital),

LSD的排序方式由数值的最右边(低位)开始,而MSD则相反,由数值的最左边(高位)开始。

注意一点:LSD的基数排序适用于位数少的数列,如果位数多的话,使用MSD的效率会比较好。

MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。

我们把扑克牌的排序看成由花色和面值两个数据项组成的主关键字排序。

要求如下:

花色顺序:梅花<方块<红心<黑桃

面值顺序:2<3<4<…<10<J<Q<K<A

那么,若要将一副扑克牌排成下列次序:

梅花2,…,梅花A,方块2,…,方块A,红心2,…,红心A,黑桃2,…,黑桃A。

有两种排序方法:

<1>先按花色分成四堆,把各堆收集起来;然后对每堆按面值由小到大排列,再按花色从小到大按堆收叠起来。—-称为”最高位优先”(MSD)法。 <2>先按面值由小到大排列成13堆,然后从小到大收集起来;再按花色不同分成四堆,最后顺序收集起来。—-称为”最低位优先”(LSD)法。

(1)MSD法实现

最高位优先法通常是一个递归的过程:

<1>先根据最高位关键码K1排序,得到若干对象组,对象组中每个对象都有相同关键码K1。

<2>再分别对每组中对象根据关键码K2进行排序,按K2值的不同,再分成若干个更小的子组,每个子组中的对象具有相同的K1和K2值。

<3>依此重复,直到对关键码Kd完成排序为止。

<4> 最后,把所有子组中的对象依次连接起来,就得到一个有序的对象序列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include<iostream>
#include<malloc.h>
using namespace std;
int getdigit(int x,int d)
{
    int a[] = {1, 1, 10};     //因为待排数据最大数据也只是两位数,所以在此只需要到十位就满足
    return ((x / a[d]) % 10);    //确定桶号
}
void  PrintArr(int ar[],int n)
{
    for(int i = 0; i < n; ++i)
        cout<<ar[i]<<" ";
    cout<<endl;
}
void msdradix_sort(int arr[],int begin,int end,int d)
{
    const int radix = 10;
    int count[radix], i, j;
    //置空
    for(i = 0; i < radix; ++i)
    {
        count[i] = 0;
    }
    //分配桶存储空间
    int *bucket = (int *) malloc((end-begin+1) * sizeof(int));
    //统计各桶需要装的元素的个数
    for(i = begin;i <= end; ++i)
    {
        count[getdigit(arr[i], d)]++;
    }
    //求出桶的边界索引,count[i]值为第i个桶的右边界索引+1
    for(i = 1; i < radix; ++i)
    {
        count[i] = count[i] + count[i-1];
    }
    //这里要从右向左扫描,保证排序稳定性
    for(i = end;i >= begin; --i)
    {
        j = getdigit(arr[i], d);      //求出关键码的第d位的数字, 例如:576的第3位是5
        bucket[count[j]-1] = arr[i];   //放入对应的桶中,count[j]-1是第j个桶的右边界索引
        --count[j];                    //第j个桶放下一个元素的位置(右边界索引+1)
    }
    //注意:此时count[i]为第i个桶左边界
    //从各个桶中收集数据
    for(i = begin, j = 0;i <= end; ++i, ++j)
    {
        arr[i] = bucket[j];
    }
    //释放存储空间
    free(bucket);
    //对各桶中数据进行再排序
    for(i = 0;i < radix; i++)
    {
        int p1 = begin + count[i];         //第i个桶的左边界
        int p2 = begin + count[i+1]-1;     //第i个桶的右边界
        if(p1 < p2 && d > 1)
        {
            msdradix_sort(arr, p1, p2, d-1);  //对第i个桶递归调用,进行基数排序,数位降 1
        }
    }
}
void  main()
{
    int  ar[] = {12, 14, 54, 5, 6, 3, 9, 8, 47, 89};
    int len = sizeof(ar)/sizeof(int);
    cout<<"排序前数据如下:"<<endl;
    PrintArr(ar, len);
    msdradix_sort(ar, 0, len-1, 2);
    cout<<"排序后结果如下:"<<endl;
    PrintArr(ar, len);
}
/*
排序前数据如下:
12 14 54 5 6 3 9 8 47 89
排序后结果如下:
3 5 6 8 9 12 14 47 54 89
 */

(2)LSD法实现

最低位优先法首先依据最低位关键码Kd对所有对象进行一趟排序,

再依据次低位关键码Kd-1对上一趟排序的结果再排序,

依次重复,直到依据关键码K1最后一趟排序完成,就可以得到一个有序的序列。

使用这种排序方法对每一个关键码进行排序时,不需要再分组,而是整个对象组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include<iostream>
#include<malloc.h>
using namespace std;
#define   MAXSIZE   10000
int getdigit(int x,int d)
{
    int a[] = {1, 1, 10, 100};   //最大三位数,所以这里只要百位就满足了。
    return (x/a[d]) % 10;
}
void  PrintArr(int ar[],int n)
{
    for(int i = 0;i < n; ++i)
    {
        cout<<ar[i]<<" ";
    }
    cout<<endl;
}
void lsdradix_sort(int arr[],int begin,int end,int d)
{
    const int radix = 10;
    int count[radix], i, j;
    int *bucket = (int*)malloc((end-begin+1)*sizeof(int));  //所有桶的空间开辟

    //按照分配标准依次进行排序过程
    for(int k = 1; k <= d; ++k)
    {
        //置空
        for(i = 0; i < radix; i++)
        {
            count[i] = 0;
        }
        //统计各个桶中所盛数据个数
        for(i = begin; i <= end; i++)
        {
           count[getdigit(arr[i], k)]++;
        }
        //count[i]表示第i个桶的右边界索引
        for(i = 1; i < radix; i++)
        {
            count[i] = count[i] + count[i-1];
        }
        //把数据依次装入桶(注意装入时候的分配技巧)
        for(i = end;i >= begin; --i)        //这里要从右向左扫描,保证排序稳定性
        {
            j = getdigit(arr[i], k);        //求出关键码的第k位的数字, 例如:576的第3位是5
            bucket[count[j]-1] = arr[i]; //放入对应的桶中,count[j]-1是第j个桶的右边界索引
            --count[j];               //对应桶的装入数据索引减一
        }
        //注意:此时count[i]为第i个桶左边界

        //从各个桶中收集数据
        for(i = begin,j = 0; i <= end; ++i, ++j)
        {
            arr[i] = bucket[j];
        }
    }
    free(bucket);
}
void  main()
{
    int  br[10] = {20, 80, 90, 589, 998, 965, 852, 123, 456, 789};
    cout<<"原数据如下:"<<endl;
    PrintArr(br,10);
    lsdradix_sort(br, 0, 9, 3);
    cout<<"排序后数据如下:"<<endl;
    PrintArr(br, 10);
}
/*
原数据如下:
20 80 90 589 998 965 852 123 456 789
排序后数据如下:
20 80 90 123 456 589 789 852 965 998
*/

时间复杂度

该算法所花的时间基本是在把元素分配到桶里和把元素从桶里串起来;把元素分配到桶里:循环 n 次;

设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集

空间复杂度

该算法的空间复杂度就是在分配元素时,使用的桶空间;所以空间复杂度为:O(rd + n)

稳定性

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。

有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序结果就是高优先级高的在前,高优先级相同的情况下低优先级高的在前。

基数排序基于分别排序,分别收集,相等的元素一直在一个桶内,相对位置不会变化。所以其是稳定的排序算法。

参考: http://blog.csdn.net/MoreWindows/article/category/859207. http://www.cnblogs.com/Anker/archive/2013/01/23/2873422.html