排序算法

在面试中经常会遇到排序算法的问题,尤其以快速排序、归并排序和堆排序。最近闲下来,把常用的排序算法做一个整理,对自己进行总结,以期对他人有点帮助。

各类算法总结

排序方法平均时间复杂度最差时间复杂度最优时间复杂度空间复杂度算法稳定性
插入排序 O(n2) O(n2) O(n) O(1) 稳定
希尔排序与步长有关与步长有关与步长有关 O(1) 不稳定
冒泡排序 O(n2) O(n2) O(n) O(1) 稳定
快速排序 O(nlgn) O(n2) O(nlgn) O(lgn) ~ O(n) 不稳定
选择排序 O(n2) O(n2) O(n2) O(1) 不稳定
堆排序 O(nlgn) O(nlgn) O(nlgn) O(1) 不稳定
归并排序 O(nlgn) O(nlgn) O(nlgn) O(n) 稳定
计数排序 O(n+k) O(n+k) O(n+k) O(n+k) 不是比较算法
基数排序 O(d(n+k)) O(d(n+k)) O(d(n+k)) O(n+k) 稳定
桶排序 O(n+k) O(n2) O(n) O(nk) 不是比较算法

插入排序

最早拥有排序概念的机器出现在1901至1904年间由Hollerith发明出使用基数排序法的分类机,此机器包括打孔、制表等功能。

直接插入排序

时间复杂度:平均为 O(n2) ,最优为 O(n) ,最差为 O(n2)
空间复杂度: O(1)
算法:

输入:n个数的一个序列 {a1,a2,,an}
输出:输入数列的一个排列 {a1,a2,,an} ,满足 a1a2an

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

代码实现:

// 插入排序:data[]为插入序列,start,end为待排列的起始下标
void InsertSort(int data[], int start, int end)
{
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    for (int i = start+1; i <= end; i++)
    {
        int key = data[i];
        int j = i-1;
        while (j >= start && data[j] > key)
        {
            data[j+1] = data[j];
            j--;
        }// while
        data[j+1] = key;
    }// for
}// InsertSort
折半插入排序

在上述算法中,可以采用二分查找法来减少比较操作的次数。该算法是插入排序的一个变种,称为折半插入排序
空间复杂度上,折半插入排序所需附加的存储空间和直接插入排序相同。从时间上比较,折半插入排序仅减少了关键字间的比较次数,而记录的移动次数不变。因此时间复杂度与直接插入排序相同。

代码实现:

// 折半插入排序
void BInsertSort(int data[], int start, int end)
{
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    for (int i = start+1; i <= end; i++)
    {
        int key = data[i];
        int low = start, high = i-1;
        while (low <= high)
        {// 在data[low...high]中折半查找有序插入的位置
            int m = (low + high)/2;
            if (key < data[m])
            {// 在低半区
                high = m - 1;
            }
            else
            {// 在高半区
                low = m + 1;
            }
        }// while

        for (int j = i-1; j >= high+1; j--)
        {// 记录后移
            data[j+1] = data[j];
        }
        data[high+1] = key;
    }// for
}// BInsertSort
希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。基于插入排序的以下两点而提出的改进方法:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

基本思想:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。

希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及到一些数学上尚未解决的难题。步长序列为 n/2i 时的最差时间复杂度是 O(n2) ;步长序列为 2k1 时,最差时间复杂度为 O(n3/2) ;步长序列为 2i3j ,最差时间复杂度为 O(nlg2n) 。已知的最好的步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,…),该序列的项来自 9×4i9×2i+1 2i+2×(2i+23)+1 这两个算式1。这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

另一个在大数组中表现优异的步长序列是(斐波那契数列除去0和1将剩余的数以黄金分区比的两倍的幂进行运算得到的数列):(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713,…)。

增量序列可以有各种取法,但注意应使增量序列中的值没有除1之外的公因子,且最后一个增量必须等于1。

代码实现:

// 希尔排序
void ShellInsert(int data[], int start, int end, int dk)
{
    if (start >= end || dk < 1)
    {// 若start小于end或步长小于1,结束
        return;
    }// if

    for (int i = start+dk; i <= end; i++)
    {
        if (data[i] < data[i-dk])
        {// 需要将data[i]插入到有序增量子表中
            int key = data[i];
            int j;
            for (j = i-dk; j >= start && key < data[j]; j -= dk)
            {// 记录后移,查找插入位置
                data[j+dk] = data[j];
            }// for
            data[j+dk] = key;
        }// if
    }// for
}// ShellInsert

void ShellSort(int data[], int start, int end)
{// 步长序列从二分之一长度开始减少
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    int len = end - start + 1;
    for (int dk = len >> 1; dk > 0; dk >>= 1)
    {
        ShellInsert(data, start, end, dk);
    }// for
}// ShellSort

冒泡排序

冒泡排序是一种简单的排序算法。重复地走访过要排序的序列,若他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
时间复杂度:平均为 O(n2) ,最优为 O(n) ,最差为 O(n2)
空间复杂度: O(1)
算法:

输入:n个数的一个序列 {a1,a2,,an}
输出:输入数列的一个排列 {a1,a2,,an} ,满足 a1a2an

代码实现:

// 冒泡排序
void BubbleSort(int data[], int start, int end)
{
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if
    int flag = 0;
    for (int i = 1; i <= end-start; i++)
    {
        flag = 0;
        for (int j = start; j <= end-i; j++)
        {
            if (data[j] > data[j+1])
            {
                data[j] ^= data[j+1];
                data[j+1] ^= data[j];
                data[j] ^= data[j+1];
                flag++;
            }
        }// for
        if (flag == 0)
        {
            break;
        }
    }// for
}// BubbleSort

代码中,标志位flag统计每轮相邻元素比较发生交换的次数,若没有发生交换,则意味着序列已经排序完成,不需要继续,因此在最好情况下时间复杂度为 O(n) ,若没有该标志位,则冒泡排序算法的时间复杂度总为 O(n2)
此外,还可以记录每轮相邻元素比较中最后一次交换发生的位置,该位置之后的序列已经完成排序,可以不再参与比较。不过,该优化方法不会降低时间复杂度的数量级。

快速排序

快速排序算法是由C. A. R. Hoare(东尼霍尔,Charles Antony Richard Hoare)在1960年提出的。是对冒泡排序的一种改进。基本思想是通过一趟排序将待排记录分割成独立的两部分,其中一部分元素均比另一部分元素小,则分别对剩下的元素继续排序以达到整个序列有序。
时间复杂度:最好和平均为 O(nlgn) ,最差情况下为 O(n2)
空间复杂度: O(lgn)

实现代码:

// 快排
int Partition(int A[], int start, int end)
{
    if (start >= end)
    {
        return start;
    }// if

    // 以第一个元素作为主元
    int key = A[start];
    while (start < end)
    {
        while (start < end && A[end] > key)
        {
            end--;
        }// while
        A[start] = A[end];

        while (start < end && A[start] < key)
        {
            start++;
        }// while
        A[end] = A[start];
    }// while
    A[start] = key;
    return start;
}// Partition

void QuickSort(int A[], int start, int end)
{
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    int k = Partition(A, start, end);
    QuickSort(A, start, k-1);
    QuickSort(A, k+1, end);
}// QuickSort

上述代码实现中以第一个元素作为主元(pivot element),并围绕它来划分数组,下面代码是以最后一个元素作为主元划分数组。

代码实现:

int Partition2(int A[], int start, int end)
{
    if (start >= end)
    {
        return start;
    }// if

    // 以最后一个元素作为主元
    int key = A[end];
    int i = start - 1, j = start;
    while (j <= end - 1)
    {
        if (A[j] <= key)
        {
            i++;
            int flag = A[i];
            A[i] = A[j];
            A[j] = flag;
        }// if
        j++;
    }// while

    // 交换A[i+1]和A[end]
    A[end] = A[i+1];
    A[i+1] = key;
    return i+1;
}// Partition2

函数QuickSort()采用分治的思想实现。Partition()和Partition2()的复杂度均为 O(n) ,即对元素遍历一次进行n-1 次比较。在最差情况下时,划分产生的两个子问题分别包含n-1个和0个元素,算法运行时间 T(n)=T(n1)+O(n)=O(n2) 。最好情况是分成两个同样大小的子问题,运行时间 T(n)=2T(n/2)+O(n)=O(nlgn) 。快速排序的平均情况接近于其最好情况而非最坏情况。

空间复杂度为递归调用的次数,即二叉树的深度为 O(lgn) 的。

在讨论快速排序平均情况性能时,前提假设是输入数据的所有排序都是等概率的。但实际工作中,这个假设并不是总是成立的。可以在算法中引入随机性,从而使得算法对于所有数据获得较好的期望性能。

将上述算法改成随机QS改动不大,只要将随机选出的元素与期望的主元元素交换位置即可。
代码实现:

int Random_Partition(int A[], int start, int end)
{// 本处已交换第一个元素为主元为例,也可以选择交换最后一个元素,调用Partition2()函数
    int i = start + rand()%(end - start);
    int flag = A[start];
    A[start] = A[i];
    A[i] = flag;
    return Partition(A, start, end);
}// Rand_Partition

快速排序的平均情况下性能与随机快排算法的期望性能时一致的,故通过分析随机快速排序算法的期望时间在计算快速排序的平均情况下时间代价。
本处引入调和级数第n个部分和 Hn=nk=11klnn+O(1) 。设 Pij 表示元素 Ai,Aj 发生比较的概率,即其中一个元素曾作为主元,不妨设 i<j ,则 E(Pij)=2ji+1
RQ的比较次数期望为

E(T)=i=1nj=i+1nPiji=1nk=1ni+12k2i=1nk=1n1k=2nHn

即RQ的时间付代价期望为 O(nlgn) ,即快速排序算法平均情况下的时间复杂度。

选择排序

选择排序的基本思想是每次从n-i+1(i=1,2,…,n-1)个元素中选取关键字最小的元素作为有序队列中的第 i 个元素。
算法输入和输出:

输入:n个数的一个序列 {a1,a2,,an}
输出:输入数列的一个排列 {a1,a2,,an} ,满足 a1a2an

简单选择排序

时间复杂度: O(n2)
空间复杂度: O(1)
代码实现:

// 简单选择排序
void SelectSort(int data[], int start, int end)
{
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    for (int i = start; i <= end; i++)
    {
        int j = i, min_Key_Flag = i;
        for (j = i; j <= end; j++)
        {
            if (data[j] < data[min_Key_Flag])
            {
                min_Key_Flag = j;
            }// if
        }// for

        if (i != min_Key_Flag)
        {
            data[i] ^= data[min_Key_Flag] ^= data[i] ^= data[min_Key_Flag];
        }// if
    }// for
}// SelectSort

在上述代码中,每次从n-i+1(i=1,2,…,n-1) 个值中选出最小值,要进行n-i次比较。因此可以从改进简单排序算法比较次数出发,改进算法。
在n个关键字中选出最小值,至少进行n-1次比较,然而,继续在剩余的n-1个元素中选取最小值,可以利用前一次比较所得的信息来减少比赛次数。例如体育比赛中的锦标赛。在8名运动员中决出前3名至多需要11场而不是7+6+5=18 场比赛(前提是若乙胜丙,甲胜乙,则认为甲必能胜丙,即传递性)。按照锦标赛的思想可得到树形选择排序

堆排序

1964年由J. W. J. Willians提出。堆算法是不稳定算法。每次将前 i(i=1,2,…,n-1) 个待排元素构建最大堆,然后将堆顶元素与第 i 个元素交换,继续构建前 i-1个元素的最大堆,后面的元素排序完成,直到所有的元素均完成排序。
时间复杂度:在最差和最好情况下均为 O(nlgn)
空间复杂度: O(1)

在构建最大堆时,要考虑堆的起始节点编号。
若以0为起始编号,则节点 i 的
- 父节点 i 的左子节点编号为 (2×i+1)
- 父节点 i 的右子节点编号为 (2×i+2)
- 子节点 i 的父节点的编号为 (i1)/2

若以1为起始编号,则节点 i 的
- 父节点 i 的左子节点编号为 (2×i)
- 父节点 i 的右子节点编号为 (2×i+1)
- 子节点 i 的父节点的编号为 i/2

代码实现:

// 堆排序
inline int Parent(int i){return (i-1)/2;}
inline int Left(int i){return 2*i+1;}
inline int Right(int i){return 2*i+2;}

void Max_Heapify(int A[], int i, int end)
{// 维护堆的性质,假设左右子树都是最大堆,仅将i节点下降到合适位置
    int l = Left(i);
    int r = Right(i);
    int largest = i;    // 最大默认当前节点

    if (l <= end && A[l] > A[i])
    {// 比较左子节点
        largest = l;
    }// if
    if (r <= end && A[r] > A[largest])
    {// 比较右子节点
        largest = r;
    }// if

    if (largest != i)
    {
        int flag = A[i];
        A[i] = A[largest];
        A[largest] = flag;
        Max_Heapify(A, largest, end);
    }// if
}// Max_Heapify

void Build_Maxheap(int A[], int end)
{// 构建最大堆,A[0,...,end]
    for (int i = (end-1)/2; i >= 0; i--)
    {
        Max_Heapify(A, i, end);
    }// for
}// Build_Maxheap

void HeapSort(int A[], int end)
{
    Build_Maxheap(A, end);
    for (int i = end; i >= 1; i--)
    {
        int flag = A[0];
        A[0] = A[i];
        A[i] = flag;
        Max_Heapify(A, 0, i-1);
    }// for
}// HeapSort

本处代码实现是以0为起始编号的。其中Max_Heapify()函数是维护最大堆性质的重要过程。在调用该函数时,假定根节点为Left(i)和Right(i)的二叉树都是最大堆。但经过交换后,A[i]可能小于其孩子,违背了最大堆的性质,通过逐级下降的方式使得A[i]下降到合适位置,从而保证根节点为i的二叉树保持最大堆的性质。在该情况下最差时间复杂度是 O(lg(endi+1)) 的,即对n个元素的二叉树,时间复杂度是 O(lgn) 的。
初始化时Build_Maxheap()函数构建最大堆,从编号最后一个节点的父节点开始调用Max_Heapify()函数。在高度为h的节点上调用Max_Heapify()的代价是 O(lgn) 的,故Build_Maxheap()的总代价可以表示为

h=0lgnn2h+1O(h)=O(nh=0lgnh2h)=O(nh=0h2h)=O(n)

而每次将堆最后一个元素和堆顶元素交换并将堆长度减少一,时间复杂度是 O(lgn) 的。故堆排序的时间复杂度是 O(n)+(n1)O(lgn)=O(nlgn)

归并排序

归并的含义是将两个或两个以上的有序表组成一个新的有序表。归并排序是建立在归并操作上的一种有效的排序算法。1945年由约翰·冯·诺伊曼首次提出。该算法是分治法(Divide and Conquer)的一个典型应用,且各层分治递归可以同时进行。

代码实现:

// 归并排序
void Merge(int A[], int p, int q, int r)
{// 将已经排序完成的A[p,...,q]和A[q+1,...,r]归并
    int n1 = q - p + 1;
    int n2 = r - q;
    int *A1 = new int[n1];
    int *A2 = new int[n2];
    int i = 0, j = 0;

    for (i = 0; i < n1; i++)
    {
        A1[i] = A[p+i];
    }// for
    for (i = 0; i < n2; i++)
    {
        A2[i] = A[q+1+i];
    }// for

    i = 0;
    j = 0;
    while (i < n1 && j < n2)
    {
        if (A1[i] < A2[j])
        {
            A[p+i+j] = A1[i];
            i++;
        }
        else
        {
            A[p+i+j] = A2[j];
            j++;
        }
    }// while

    while (i < n1)
    {
        A[p+i+j] = A1[i];
        i++;
    }// while

    while (j < n2)
    {
        A[p+i+j] = A2[j];
        j++;
    }// while

    delete[] A1;
    delete[] A2;
}// Merge

void MergeSort(int A[], int start, int end)
{
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    int q = start + (end-start)/2;
    MergeSort(A, start, q);
    MergeSort(A, q + 1, end);
    Merge(A, start, q, end);
}// MergeSort

归并排序的运行时间 T(n)=2T(n/2)+n=O(nlgn) ,运行中每次需要归并两个有序序列总长的空间复杂度即最大为 n ,以及栈递归需要的空间 lgn ,故归并排序的空间复杂度为 O(n)
以上代码为2-路归并排序,在内部排序中较少采用归并排序。

计数排序

前面介绍的所有算法,在排序的是最终结果中,各元素的次序依赖于他们之间的比较,称为比较排序。对包含n个元素的输入序列来说,任何比较排序在最差情况下都要经过 O(nlgn) 次比较操作[^2]。这是由全序集的模糊代数结构所导致的。从该意义上讲,归并排序和堆排序是渐进最优的,任何已知的比较排序算法最多在常数上由于二者。
计数排序、基数排序和桶排序是通过运算而不是比较来确定排序顺序的,时间复杂度是线性的。因此比较排序 nlgn 的下界对三者不适用。
计数排序假设n个输入元素中的每一个都是在0~k之间的一个整数,其中k为整数。当 k=O(n) 时,排序的运行时间是 O(n) 的。
时间复杂度: O(n+k)
空间复杂度: O(n+k)
算法:

输入:n个数的一个序列 {a1,a2,,an} 以及k值,对任意元素 ai[0,k]
输出:输入数列的一个排列 {a1,a2,,an} ,满足 a1a2an

代码实现:

// 计数排序
void CountSort(int A[], int n, int k)
{// 对A[]中n个元素(0~k)进行排序
    int* c = new int[k+1];
    int* B = new int[n];
    // 初始化计数数组
    for (int i = 0; i <= k; i++)
        c[i] = 0;
    // 计算i值出现的次数
    for (int i = 0; i < n; i++)
        c[A[i]]++;
    // 统计小于等于i的数目
    for (int i = 1; i <= k; i++)
        c[i] += c[i-1];
    // 将元素放到指定位置
    for (int i = n; i > 0; i--)
        B[--c[A[i - 1]]] = A[i - 1];
    // 保存到原数组中
    for (int i = 0; i < n; i++)
        A[i] = B[i];
    // 释放内存空间
    delete[] B;
    delete[] c;
}// CountSort

计数排序在上述代码中是稳定的:具有相同值的元素在输出数组中的相对次序与他们在输入数组中的相对次序相同。第四个for循环的倒置循环是稳定性的保证。
算法的时间复杂度为 O(k)+O(n)=O(n+k) ,即在 k=O(n) 时,该算法的时间复杂度与输入元素数目成线性相关。
空间复杂度为 O(n+k)

基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。
基数排序依据排序方式的不同有两种:LSD(Least significant digital)和MSD(Most significant digital)。LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

给定n个数d位数,其中每一个数位有k个可能的取值。如果RadixSort使用的稳定排序算法耗时 Θ(n+k) ,那么它就可以在 Θ(d(n+k)) 时间内将这些数排序好。

LSD代码实现:

// LSD排序算法
int getDigit(int x, int d)
{// 返回x第d位置的数值,输入为正整数
    if (x <= 0)
    {
        return 0;
    }// if

    int d_10 = 1;
    while (d--)
    {
        d_10 *= 10;
    }// while

    return (x%d_10)*10/d_10;
}// getDigit
void RadixSort_LSD(int A[], int start, int end, int d)
{// k值为10,d为输入数列最大数的位数
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    const int k = 10;
    int* c = new int[k];
    int* B = new int[end-start+1];

    for (int iii = 1; iii <= d; iii++)
    {
        // 初始化计数数组
        for (int i = 0; i < k; i++)
            c[i] = 0;
        // 计算iii位置上i值出现的次数
        for (int i = start; i <= end; i++)
            c[getDigit(A[i],iii)]++;
        // 统计小于等于i的数目
        for (int i = 1; i < k; i++)
            c[i] += c[i-1];
        // 将元素放到指定位置
        for (int i = end; i >= start; i--)
            B[--c[getDigit(A[i],iii)]] = A[i];
        // 保存到原数组中
        for (int i = start; i <= end; i++)
            A[i] = B[i-start];
    }// for

    // 释放内存空间
    delete[] B;
    delete[] c;
}// RadixSort_LSD

MSD算法和LSD算法略有区别:LSD从低位到高位按该位置上的数值对数字进行排列,而MSD是从高位开始分到不同的桶中之后,再递归地对桶里面的元素进行排序。

MSD代码实现:

int getDigit(int x, int d)
{// 返回x第d位置的数值,输入为正整数
    if (x <= 0)
    {
        return 0;
    }// if

    int d_10 = 1;
    while (d--)
    {
        d_10 *= 10;
    }// while

    return (x%d_10)*10/d_10;
}// getDigit
void RadixSort_MSD(int A[], int start, int end, int d)
{// d为输入列最大数位数
    if (start >= end || start < 0 || end <= 0)
    {
        return;
    }// if

    const int k = 10;
    int* B = new int[end-start+1];
    int* c = new int[k+1];      // 在位置k处记录总数,用来标记最后一个元素的边界
    for (int i = 0; i <= k; i++)
        c[i] = 0;
    for (int i = start; i <= end; i++)
        c[getDigit(A[i],d)]++;
    for (int i = 1; i <= k; i++)
        c[i] += c[i-1];
    for (int i = end; i >= start; i--)
        B[--c[getDigit(A[i],d)]] = A[i];
    for (int i = start; i <= end; i++)
        A[i] = B[i-start];

    delete[] B;

    for (int i = 0; i < k; i++)
    {
        int start_ = start + c[i];      // 第i桶中的左边界
        int end_ = start + c[i+1] - 1;  // 第i桶中的右边界
        if (d > 1)
        {
            RadixSort_MSD(A, start_, end_, d-1);
        }// if
    }// for

    delete[] c;
}// RadixSort_MSD

关于基数排序的性能优化,可以参考该博客:http://blog.csdn.net/yutianzuijin/article/details/22876017

对MSD和LSD进行复杂度分析,LSD时间复杂度清晰,为 O(d(n+k)) ,MSD的时间复杂度,假设数字总是平均分布在各个桶中的

T(n)=kT(n/k)+O(n+k)=i=0dkiO(n/ki+k)

从公式角度看,时间代价应该是高于LSD的(数量级相同,系数更大)。

桶排序

桶排序也叫箱排序,原理是将数组分到有限数量的桶子里。每个桶再进行排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间 O(n)

桶排序以下列程序进行

1.设置一个定量的数组当作空桶子。
2.寻访序列,并且把项目一个一个放到对应的桶子去。
3.对每个不是空的桶子进行排序。
4.从不是空的桶子里把项目再放回原来的序列中。

从上述描述中,会发现桶排序与上述的非比较算法都有一定相似,尤其是MSD,差别在于计数排序和基数排序一般要求类型为一定区间上的整形数据(即k的取值是一定的),而桶排序可以针对于浮点型,或者说是k值不确定的情况,但桶的数目是一定的。

相关性知识说明

时间复杂度符号

O(f(n)) 表示渐进上界是 f(n) ,即 O(g(n))={f(n)):c,n0, 使得对于所有的 nn0 , 0f(n)cg(n)}
Ω(f(n)) 表示渐进下界是 f(n) ,即 Ω(g(n))={f(n)):c,n0, 使得对于所有的 nn0 , 0cg(n)f(n)}
Θ(f(n)) 表示 O(f(n)) Ω(f(n)) 同时成立
f(x) 表示x的多项式函数。

两个变量交换

在上述交换排序中,出现交换排序的部分可分为三种形式。本处对交换算法进行简单的补充说明。

借助临时变量

如代码所示:

void swap(int &a, int &b)
{// 交换a和b的数值
    int flag = a;
    a = b;
    b = flag;
}// swap

该方法较为简单,一目了然。

算术运算
void swap(int &a, int &b)
{
    a = b - a;
    b = b - a;
    a = b + a;
}

可以借助于数轴来理解该方法。在这里不妨设b>a,

第一个等式使得a保存a到b的距离d
第二个等式b减去a到b的距离d,b保存的为a原来的值
第三个等式b加上a到b的距离d,即将a赋值为b原来位置的值

此外,在上述方法改变下,可以通过取a,b变量的地址进行算术运算交换二者指向的地址而实现交换,该方法可以实现对不同类型元素的交换(地址总是可以表示为整形的)。

异或法
void swap(int &a, int &b)
{// 交换a和b的值
    if (a == b)
        return;
    a ^= b;
    b ^= a;
    a ^= b;
}

该代码的连写形式 a^= b ^= a ^= b。异或法的原理是一个数对另一个数字进行两次异或之后,数值不变,在上述情况下,可以发现a异或了一次b一次自身,所以变成了b的值,b与此同理。但要注意a,b不能是相同的地址。若是一个数的话,如a ^= a ^= a ^= a,是对自身做了偶数次异或,因此会变成0。

总述

在上面的算术运算异或法中,大家发现可以在不借助于临时变量的情况下,实现元素交换,节省了空间。但在实际中,尤其在现代机器上,这些技巧也会存在缺点。
算术运算对于+和-操作,均有值溢出的可能性。
异或法并不会提高算法执行的性能(上面的代码中进行的只是为了测试)。详细情况可以看下面的博客,通过编译之后的汇编代码可以清楚的看到采用这些技巧之后,性能的下降。
- 用异或来交换变量是错误的——陈硕: http://blog.csdn.net/solstice/article/details/5166912

参考


  1. Pratt, V. Shellsort and sorting networks (Outstanding dissertations in the computer sciences). Garland. 1979. ISBN 0-824-04406-1. (This was originally presented as the author’s Ph.D. thesis, Stanford University, 1971) http://faculty.simpson.edu/lydia.sinapova/www/cmsc250/LN250_Weiss/L12-ShellSort.htm#increments
    [^2]:Cormen, Thomas H.; Leiserson, Charles E., Rivest, Ronald L., Stein, Clifford (2009) [1990]. Introduction to Algorithms (3rd ed.). MIT Press and McGraw-Hill. pp. 191–193. ISBN 0-262-03384-4.[算法导论第3版中文版8.1]
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值