常见的排序算法

常见的排序算法包括:

冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序和堆排序。本文默认讨论由小到大排序。

排序算法最好时间最差时间平均时间空间是否稳定
冒泡排序O(n)或者O(n^2)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
希尔排序O(n)O(n(log n)^2)依据步长变化O(1)不稳定
归并排序O(nlog n)O(nlog n)O(nlog n)O(n)稳定
快速排序O(nlog n)O(n^2)O(nlog n)O(log n~n)不稳定
堆排序O(nlog n)O(nlog n)O(nlog n)O(1)不稳定

排序算法的稳定性的概念:假设两个相等的数ri=rj,在排序之前rirj的前面,经过排序之后ri还是在rj的前面,那么该排序算法就是稳定的,否则就是不稳定的。

1.冒泡排序

冒泡排序是最简单的一种排序算法。
逆序对的概念:如果i<j,但是在A[i] > A[j],那么称A[i],A[j]为一组逆序对,如果i+1=j,也称之为相邻的逆序对
冒泡排序的思路很简单:存在相邻的逆序对,则交换相邻的两个元素,每一轮比较交换之后,必然相对的最后一个元素就绪。
一般冒泡排序的空间复杂度为O(1),最好的时间复杂度为O(n^2),平均时间复杂度为O(n^2),最坏的时间复杂度也为O(n^2)
稳定性取决于相等的相邻元素是否交换,由于是相邻的逆序对才交换,所以这种情况下的冒泡排序算法是稳定的。
具体代码如下:

//Bubble sort
//average:O(n^2);
//best:O(n^2);
//worst:O(n^2);
//space complication:O(1);
//stable sort
void BubbleSort(int A[], int n){
    for(int i = 0; i < n-1; ++i){
        for(int j = 0; j < n-i-1; ++j){
            if(A[j] > A[j+1])   //>= 则冒泡排序不稳定
            std::swap(A[j], A[j+1]);
        }
    }
}

冒泡排序还可以加以优化,使得最好的时间复杂度为O(n),其余无法改变。优化的手段就是添加一个全局的排序标志,一旦这个标志显示有序,那么整体就有序。
最好的情况发生在最开始就是整体有序的,经过一轮比较就直接完成。
具体代码如下:

//same
//best:O(n)
void BubbleSort(int A[], int n) {
    bool sorted = false;    //全局有序标志
    while(!sorted) {
        sorted = true;  //假定已经有序
        for(int i = 0; i < n - 1; ++i) {
            if(A[i] > A[i + 1]) {
                std::swap(A[i], A[i + 1]);
                sorted = false;     //清楚全局有序标志
            }
        }
        --n;    //相对末尾元素就位
    }
}

2.选择排序

选择排序的思路:每一次找到最大元素的下标,找到后将其与最后一个元素交换,这样每次也使得最后一个元素就绪。
选择排序的空间复杂度为O(1),最好的时间复杂度为O(n^2),平均时间复杂度为O(n^2),最坏的时间复杂度也为O(n^2)。并且是不稳定的,不稳定的原因就在于那个交换的操作。
代码如下:

//Selection sort
//average:O(n^2);
//best:O(n^2);
//worst:O(n^2);
//space complication:O(1);
//unstable sort
void SelectionSort(int A[], int n) {
    for(int i = n - 1; i >= 0; --i) {
        int max_index = i;
        for(int j = i; j >= 0; --j) {
            if(A[j] > A[max_index]) {
                max_index = j;
            }
        }
        std::swap(A[max_index], A[i]);//这一步会导致不稳定
    }
}

3.插入排序

插入排序的思路:和我们平常打桥牌一样,每摸到一张牌就将其插入到合适的位置,为了给插入的元素腾位置,需要将其余的所有元素在插入之前往后移动一个位置。
选择排序的空间复杂度为O(1),最好的时间复杂度为O(n),平均时间复杂度为O(n^2),最坏的时间复杂度也为O(n^2)。插入排序最好的时间复杂度发生在一开始就有序的情况。
插入排序是稳定的。
具体代码如下:

//Insertion sort
//average:O(n^2);
//best:O(n);
//worst:O(n^2);
//space complication:O(1);
//stable sort
//适用于数据量较小的;
//在STL的sort算法和stdlib的qsort算法,将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)
void InsertionSort(int A[], int n) {
    for(int i = 1; i < n; ++i){
        int get = A[i];     //手牌
        int j = i - 1;
        //将get插入到A[0],A[1],...,A[i-1]中
        while(j >=0 && A[j] > get){
            A[j+1] = A[j];  //往右边移
            --j;
        }
        A[j+1] = get;
    }
}

4.希尔排序

希尔排序是一种基于插入排序的快速的排序算法。

    //Shell sort
    //average:依据步长不同而不同;
    //best:O(n);
    //worst:O(n(log n)^2);
    //space complication:O(1);
    //unstable sort
    void ShellSort(int A[], int n){
        int h = 1;
        while(h < n/3)
            h = 3 * h + 1; //步长序列:1,4,13,40...
        while(h >= 1){
            for(int i = h; i < n; i++){
                int get = A[i];
                int j = i - h;
                //将get插入到A[i-h],A[i-2h]...中
                while(j >= 0 && A[j] > get){
                    A[j + h] = A[j];
                    j -= h;
                }
                A[j+h] = get;
            }
            h /= 3; //减小步长
        }
    }

5.归并排序

//Merge sort
    //average:O(nlog n);
    //best:O(nlog n);
    //worst:O(nlog n);
    //space complication:O(n);
    //stable sort
    void MergeSort(int A[], int lo, int hi){
        //bound A[lo, hi];
        if(lo >= hi)
            return;
        int mid = lo + (hi - lo)/2;
        MergeSort(A, lo, mid);
        MergeSort(A, mid + 1, hi);
        Merge(A, lo, mid, hi);
    }

    void Merge(int A[], int lo, int mid, int hi){
        int len = hi - lo + 1;
        int *temp = new int[len];
        int i = lo, j = mid + 1, index = 0;
        while(i <= mid && j <= hi)
            temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];
        while(i <= mid)
            temp[index++] = A[i++];
        while(j <= hi)
            temp[index++] = A[j++];
        for(int k = 0; k < len; ++k)
            A[lo++] = temp[k];
        delete []temp;
    }

6.快速排序

//Quick sort
    //average:O(nlog n);
    //best:O(nlog n);
    //worst:O(n^2);
    //space complication:O(log n)~O(n);
    //unstable sort
    void QuickSort(int A[], int lo, int hi){
        if(lo >= hi)
            return;
        int pivot_index = Partition(A, lo, hi);
        QuickSort(A, lo, pivot_index - 1);
        QuickSort(A, pivot_index + 1, hi);
    }

    int Partition(int A[], int lo, int hi){
        int pivot = A[hi];  //选择最后一个元素作为基准
        int tail = lo - 1;  //tail为小于基准的子数组的最后一个元素索引
        for(int i = lo; i < hi; ++i){
            if(A[i] <= pivot)
                std::swap(A[++tail], A[i]);  //将小于基准的元素放到子数组末尾
        }
        std::swap(A[tail + 1], A[hi]);   //将基准放置在子数组末尾
        return tail + 1;    //返回基准的索引
    }

7.堆排序

//Heap sort
    //average:O(nlog n);
    //best:O(nlog n);
    //worst:O(nlog n);
    //space complication:O(1);
    //unstable sort
    void HeapSort(int A[], int n){
        int heap_size = BuildHeap(A, n);
        while(heap_size > 1){
            std::swap(A[0], A[--heap_size]); //最后一个元素和当前最大的堆顶元素互换,并将堆的规模减1
            Heapify(A, 0, heap_size);
        }
    }

    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){
            std::swap(A[i], A[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;
    }

非比较排序

计数排序(CountSort)

计数排序主要针对于一定范围的内的整数排序,时间复杂度为:O(n + k),其中k为整数的范围,计数牺牲空间来换取时间。
步骤:
1. 找到最大最小元素记为max, min,计算范围k = max - min + 1,申请一个临时数组A,其长度为k
2. 统计待排序数组中值为i的元素出现的次数,存入数组A的第i - min项。
3. 遍历计数数组A,反向填充原数组。

举个例子:原数组B=[2,1,-1,4,2]max=4,min=-1,k=6,则A=[1,0,1,2,0,1],分别表示-1,0,1,2,3,4在原数组中出现的个数。
遍历A,反向填充:

index = 0
i = 0, A[0] = 1, B[index++] = i + min = -1, --A[0], judge A[0] == 0, pass
i = 1, A[1] = 0, judge A[0] == 0, pass
i = 2, A[2] = 1, B[index++] = i + min = 1, --A[2], judge A[2] == 0, pass
i = 3, A[3] = 2, B[index++] = 3 + min = 2, --A[3], judge A[3] == 0, continue
                 B[index++] = 3 + min = 2, --A[3], judge A[3] == 0, pass
i = 4, A[4] = 0, judge A[4] == 0, pass
i = 5, A[5] = 1, B[index++] = 4 + min = 4, --A[5], judge A[5] == 0, pass 

具体代码如下:

//CountSort
//Time: O(n + k)
template <typename RandomItWithTypeInt>
void CountSort(RandomItWithTypeInt first, RandomItWithTypeInt last) {
  if (last < first) throw "Interval is wrong";
  int maxElem = *max_element(first, last);
  int minElem = *min_element(first, last);
  vector<int> count_array(maxElem - minElem + 1, 0);
  for (auto iter = first; iter != last; ++iter) {
    ++count_array[*iter];
  }
    auto iter = first;
  for (int i = 0; i <= maxElem - minElem; ++i) {
        while (count_array[i]) {
          *first++ =  i + minElem;
          --count_array[i];
        }
  }
  return;
}

基数排序(RadixSort)

基数排序通常适用于非负整数,是一种分配排序,通过数某一位的将其分配至某一个桶,再重排重新分配,时间复杂度为:O(k*n),其中k为最大位数。
步骤:
1. 计算最大位数,假设为k。
2. 声明一个临时数组记录待排序数组temp,和m个桶bucket,通常m=10,从最低位开始,按位放入相应的bucket,重复这个操作。
3. 将temp放回原数组。

举个例子:原数组A=[11,10,8,279,9],首先计算最大位数是279的3位,即k=3

//按各位
temp: 11,10,8,279,9
[0, 1,  2, 3, 4, 5, 6, 7, 8, 9]
 10 11                    8 279
                              9
//按十位
temp:10,11,8,279,9
[0, 1,  2, 3, 4, 5, 6, 7, 8, 9]
 8 10                 279   
 9 11      
//按百位
temp:10,11,8,279,9
[0, 1,  2, 3, 4, 5, 6, 7, 8, 9]
 8      279   
 9
 10
 11
//放回元素组
B = [8,9,10,11,279]

代码如下:

template <typename RandomItWithTypeUnsigned> 
void RadixSort(RandomItWithTypeUnsigned first, RandomItWithTypeUnsigned last) {
  if (last < first) throw "Interval is wrong";
  int max_digits = 1;
  int digit = 10;
  for (auto iter = first; iter != last; ++iter) {
    if (*iter >= digit) {
      digit *= 10;
      ++max_digits;
    }
  }
  int radix = 1, max_index = last - first;
  vector<int> temp(first, last);
  for (int i = 0; i < 10; ++i) {
    vector<int> count(10);
    for (auto iter = first; iter != last; ++iter) {
      int k = (*iter / radix) % 10;
      ++count[k];
    }
    for (int j = 1; j < 10; ++j) {
      count[j] += count[j - 1];
    }
    for (int n = max_index - 1; n >= 0; --n) {
      int k = (*(first + n) / radix) % 10;
      temp[count[k] - 1] = *(first + n);
      --count[k];
    }
    for (auto i = 0; i < max_index; ++i) {
      *(first + i) = temp[i];
    }
    radix *= 10;
  }
  return;
}

总结

稳定的排序算法只有归并、插入、选择排序以及非比较排序,非比较排序的算法时间复杂度通常可以达到O(N),不受比较排序O(NlogN)的影响,但是限制也比较明显。

主要参考了排序算法总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值