首页 > 基础算法 > 排序 > 你必须要知道的8种基本排序方法
2014
01-03

你必须要知道的8种基本排序方法

1.选择排序

定义:首先,选出数组中最小的元素,将它与数组中第一个元素交换。然后找出次小的元素,并将它与数组中第二个元素交换。按照这种方法一直进行下去,直到整个数组排完序。

交换次数:N-1

缺点:运行时间对文件已有序的部分依赖较少,从文件中选出最小元素的每一遍操作过程,并没有给出下一遍要找的最小元素的位置的相关消息。例如,该程序对已排好序的文件或各元素都相同的元素文件与对随机排列的文件排序所花的时间基本相同。

适用性:对于元素比较大,关键字又比较小的文件,应该选择该方法,而其他算法移动数据的步数都比选择排序更多。

sc(source code):

template <typename T, typename Compare>
void SelectSort(vector<T> & arr, Compare cp)
{
    Display(arr, "before SelectSort:");
    for (size_t i=0; i<arr.size(); ++i) {
        size_t m = i;
        for (size_t j=i+1; j<arr.size(); ++j) {
            if ( !cp(arr[m], arr[j]) ) {
                m = j;
            }
        }
        if (m != i) {
            swap(arr[i], arr[m]);
        }

        Display(arr, i);
    }
}

2.插入排序(摘自:http://zh.wikipedia.org/wiki/%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F

插入排序(Insertion
Sort
的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

算法描述:

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

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

如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找排序

算法复杂度:

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。

sc

template <typename T, typename Compare>
void InsertSort(vector<T>& arr, Compare cp)
{
    Display(arr, "before InsertSort:");
    for(size_t i=1; i<arr.size(); ++i) {
        T temp = arr[i];
        for(size_t j=i ; j>0 && !cp(temp, arr[j-1]) ; --j) {
            arr[j] = arr[j-1];
            arr[j-1] = temp;
            Display(arr, "----");
        }
        Display(arr, i);
    }
}

3.冒泡排序

定义:遍历文件,如果紧邻的两个元素大小顺序不对,就将两者交换,重复这样的操作直到整个文件排好序。

小结:

1)选择排序使用大概次比较操作和N次交换操作。

2)在平均情况下,插入排序执行大约次比较操作和次交换(移动)操作,而在最坏情况下需要两倍的数量。

3)在平均情况和最坏情况下,冒泡排序执行大约次比较操作和次交换操作。

4)对于逆序数[文件中一对顺序不对的元素称为一个逆序]是常数级的文件,插入排序和冒泡排序所要进行的比较和交换操作的数目的是输入的线性函数。

5)如果文件中固有部分的元素的逆序数超过常数级,插入排序所执行的比较和交换操作仍是线性的。

6)对于数据项较大,关键字较小的文件,选择排序的运行时间是线性的。

4.希尔排序(http://zh.wikipedia.org/wiki/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F

希尔排序,也称递减增量排序算法,是插入排序的一种高速而稳定的改进版本。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,
    效率高, 即可以达到线性排序的效率
  • 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位

算法实现

希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。

假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n2)的排序(冒泡排序插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。

一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i
+= step_size
而不是i++)。

例如,假设有这样一组数[
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10
]
,如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5行的表中来更好地描述算法,这样他们就应该看起来是这样:

13 14 94 33 82

25 59 94 65 23

45 27 73 25 39

10

然后我们对每列进行排序:

10 14 73 25 23

13 27 94 33 39

25 59 94 65 82

45

当我们以单行来读取数据时我们得到:[
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
].
这时10已经移至正确位置了,然后再以3为步长进行排序:

10 14 73

25 23 13

27 94 33

39 25 59

94 65 82

45

排序之后变为:

10 14 13

25 23 33

27 25 59

39 65 73

45 94 82

94

最后以1步长进行排序(此时就是简单的插入排序了)。

步长序列

步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。

Donald Shell
最初建议步长选择为n/2并且对步长取半直到步长达到1。虽然这样取可以比O(n2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。
可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。

已知的最好步长序列由Marcin
Ciura
设计(141023571323017011750,…)
这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长序列的希尔排序比插入排序堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

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

sc

template <typename T, typename Compare>
void ShellSort(vector<T>& arr, Compare cp)
{
    Display(arr, "before ShellSort");

    int gap = 0;
    while (gap <= arr.size()) {
        gap = gap * 3 + 1;
    }
    T temp;
    while (gap > 0) {
        for (int i=gap; i<arr.size(); ++i) {
            int j = i - gap;
            temp = arr[i];
            while (( j>=0 ) && ( cp(arr[j], temp) )) {
                arr[j+gap] = arr[j];
                j -= gap;
            }
            arr[j + gap] = temp;
        }
        gap = (gap - 1) / 3;
        Display(arr, gap);
    }
    Display(arr, "end sort");
}

5.快速排序

我之前的一篇随笔:快速排序

快速排序算法是一种分治排序算法。它将数组划分为两个部分,然后分别对两个部分进行排序。划分的准确位置取决于输入文件中元素的初始位置。关键在于划分过程,它重排元素,使得以下三个条件成立:

对于某一ia[i]在数组的最终位置上;

a[1],…a[i-1]中的元素都比a[i]小;

a[i+1],…,a[r]中的元素都比a[i]大。

我们通过划分来完成排序,然后递归地应用该方法处理子文件。

递归实现:

sc:

int Partition(vector<int> &  arr, int left, int right)
{   
    int i = left - 1;
    int j = right;
    int v = arr[right];

    for (;;) {
        while (arr[++i] < v) ;

        while (arr[--j] > v) {
            if (j == left) {
                break;
            }
        }

        if (i >= j) {
            break;
        }

        swap(arr[i], arr[j]);
    }

    swap(arr[i], arr[right]);
    return i;
}

void QuickSort(vector<int> & arr, int left, int right)
{
    if (right <= left) return ;

    int i = Partition(arr,  left, right);
    QuickSort(arr, left, i-1);
    QuickSort(arr, i+1, right);
}

非递归实现:

sc:

int Partition(vector<int> &  arr, int left, int right)
{   
    int i = left - 1;
    int j = right;
    int v = arr[right];

    for (;;) {
        while (arr[++i] < v) ;

        while (arr[--j] > v) {
            if (j == left) {
                break;
            }
        }

        if (i >= j) {
            break;
        }

        swap(arr[i], arr[j]);
    }

    swap(arr[i], arr[right]);
    return i;
}

void QuickSort(vector<int> & arr, int left, int right)
{
    stack< pair<int, int> > sk;
    sk.push(make_pair<int, int>(left, right));

    while (!sk.empty()) {
        pair<int, int> lr = sk.top();
        sk.pop();

        if (lr.first >= lr.second) continue;

        int i = Partition(arr, lr.first, lr.second);
        if (i-lr.first > lr.second-i) {
            sk.push(make_pair<int, int>(lr.first, i-1));
            sk.push(make_pair<int, int>(i+1, lr.second));
        }
        else {
            sk.push(make_pair<int, int>(i+1, lr.second));
            sk.push(make_pair<int, int>(lr.first, i-1));
        }
    }

}

快速排序的性能特征:

1)快速排序最坏情况下使用大约次比较。

2)快速排序平均情况下使用大约2NlgN次比较。

3)如果两个子文件的较小者首先是排好序的,那么在使用快速排序对N个元素进行排序时,栈中元素不会超过lgN个。

6.归并排序

我之前的一篇随笔:递归与分治策略中的归并排序。

7.堆排序(参见堆积排序

8.基数排序(参见基数排序

亦可参见基数排序

附上述代码中缺失Display方法代码:

template <typename T>
void Display(const vector<T> & arr, size_t count)
{
    cout << count << ": ";
    copy (arr.begin(), arr.end(), ostream_iterator<T>(cout, " "));
    cout << endl;
}

template <typename T>
void Display(const vector<T> & arr, string prompt=string())
{
    cout << prompt << " ";
    copy (arr.begin(), arr.end(), ostream_iterator<T>(cout, " "));
    cout << endl;
}

文章参考资料:

《算法:C语言实现》

/********************************************************/
 * 本文由chinazhangjie整理,转载请注明博客链接,谢谢!
 * 时间:2011.07.29
/*******************************************************/

转自:http://www.cnblogs.com/chinazhangjie/archive/2011/07/29/2121200.html


  1. 很高兴你会喜欢这个网站。目前还没有一个开发团队,网站是我一个人在维护,都是用的开源系统,也没有太多需要开发的部分,主要是内容整理。非常感谢你的关注。

  2. 这道题这里的解法最坏情况似乎应该是指数的。回溯的时候
    O(n) = O(n-1) + O(n-2) + ….
    O(n-1) = O(n-2) + O(n-3)+ …
    O(n) – O(n-1) = O(n-1)
    O(n) = 2O(n-1)