排序算法的原理及其分类

排序的分类

根据稳定性分类

  排序算法可以分为稳定性排序算法和非稳定性排序算法。它们的定义如下:

  • 稳定排序:如果 a 原本在 b 的前面,且 a == b;排序后, a 仍然在 b 的前面,则为稳定排序。
  • 非稳定排序:如果 a 原本在 b 的前面,且 a == b;排序后, a 可能不在 b 的前面,则为非稳定排序。

其分类如下:

  • 稳定性排序:冒泡排序,插入排序、归并排序、基数排序
  • 不稳定性排序:选择排序、快速排序、希尔排序、堆排序

稳定性的意义
  当排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如:要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得相同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。

根据时间复杂度分类

在这里插入图片描述
O ( n 2 ) {\rm{O}}({n^2}) O(n2):冒泡排序、选择排序、插入排序。
O ( n log ⁡ 2 n ) {\rm{O}}({\rm{n}}{\log _2}n) O(nlog2n):快速排序、希尔排序、归并排序和堆排序。
O ( n ) {\rm{O}}({\rm{n}}) O(n):计数排序,桶排序,基数排序

常见的排序算法的思想:

冒泡排序

  冒泡排序是相邻元素之间的比较和交换,两重循环 O ( n 2 ) {\rm{O}}({n^2}) O(n2);由于如果两个相邻元素相等,是不会交换的,所以这是一种稳定的排序方法。

选择排序

  每个元素都与第一个元素相比,产生交换,两重循环 O ( n 2 ) {\rm{O}}({n^2}) O(n2);举例:5,8,5,2,9,其中2会与5交换,那么原序列中两个5的顺序就被破坏了,所以不是稳定的排序算法。

插入排序

  插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。刚开始这个小序列只包含第一个元素,事件复杂度 O ( n 2 ) {\rm{O}}({n^2}) O(n2)。比较是从这个小序列的末尾开始的。想要插入的元素和小序列的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找它该插入的位置。如果遇见了一个和插入元素相等的,则把插入元素放在这个相等元素的后面,所以相等元素间的顺序没有改变。

快速排序

  快速排序有两个方向,右边的j下标一直往左走,当a[j] >= a[center_index](其中center_index是中枢元素的数组下标,一般取为数组第0个元素),如果j走不动了,将j的值赋给位置i。左边的i下标一直往右走,当a[i] <= a[center_index];如果i走不动了,将i的值赋给位置j。重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱。

归并排序

  归并排序是把排序递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。归并排序是稳定的排序算法,下面代码中temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];这行代码可以保证当左右两部分的值相等的时候,先复制左边的值,这样可以保证值相等的时候两个元素的相对位置不变。

public static void mergeSort(int[] arr) {
    sort(arr, 0, arr.length - 1);
}

public static void sort(int[] arr, int L, int R) {
    if(L == R) {
        return;
    }
    int mid = L + ((R - L) >> 1);
    sort(arr, L, mid);
    sort(arr, mid + 1, R);
    merge(arr, L, mid, R);
}

public static void merge(int[] arr, int L, int mid, int R) {
    int[] temp = new int[R - L + 1];
    int i = 0;
    int p1 = L;
    int p2 = mid + 1;
    // 比较左右两部分的元素,哪个小,把那个元素填入temp中
    while(p1 <= mid && p2 <= R) {
        temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
    }


    // 上面的循环退出后,把剩余的元素依次填入到temp中
    // 以下两个while只有一个会执行
    while(p1 <= mid) {
        temp[i++] = arr[p1++];
    }
    while(p2 <= R) {
        temp[i++] = arr[p2++];
    }
    // 把最终的排序的结果复制给原数组
    for(i = 0; i < temp.length; i++) {
        arr[L + i] = temp[i];
    }
}

基数排序

  基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

   题目:通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},写出代码。

  基本思想:将整数按位数切割成不同的数字,然后按每个位数分别比较。具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。如下图所示:
在这里插入图片描述
代码:

/**
 * 基数排序:C++
 */

#include<iostream>
using namespace std;

/*
 * 获取数组a中最大值
 */
int getMax(int a[], int n)
{
    int i, max;

    max = a[0];
    for (i = 1; i < n; i++)
        if (a[i] > max)
            max = a[i];
    return max;
}

/*
 * 对数组按照"某个位数"进行排序(桶排序)
 * 例如,对于数组a={50, 3, 542, 745, 2014, 154, 63, 616};
 *    (01) 当exp=1表示按照"个位"对数组a进行排序
 *    (02) 当exp=10表示按照"十位"对数组a进行排序
 *    (03) 当exp=100表示按照"百位"对数组a进行排序
 *    ...
 */
void countSort(int a[], int n, int exp)
{
    int output[n];             // 存储"被排序数据"的临时数组
    int i, buckets[10] = {0};

    // 将数据出现的次数存储在buckets[]中
    for (i = 0; i < n; i++)
        buckets[ (a[i]/exp)%10 ]++;

    // 更改buckets[i]。目的是让更改后的buckets[i]的值,是该数据在output[]中的位置。
    for (i = 1; i < 10; i++)
        buckets[i] += buckets[i - 1];

    // 将数据存储到临时数组output[]中
    for (i = n - 1; i >= 0; i--)
    {
        output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
        buckets[ (a[i]/exp)%10 ]--;
    }

    // 将排序好的数据赋值给a[]
    for (i = 0; i < n; i++)
        a[i] = output[i];
}

/*
 * 基数排序
 *
 * 参数说明:
 *     a -- 数组
 *     n -- 数组长度
 */
void radixSort(int a[], int n)
{
    int exp;    // 指数。当对数组按个位进行排序时,exp=1;按十位进行排序时,exp=10;...
    int max = getMax(a, n);    // 数组a中的最大值

    // 从个位开始,对数组a按"指数"进行排序
    for (exp = 1; max/exp > 0; exp *= 10)
        countSort(a, n, exp);
}

int main()
{
    int i;
    int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    radixSort(a, ilen);    // 基数排序
    
    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

希尔排序

  希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比 O ( n 2 ) {\rm{O}}({n^2}) O(n2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
代码:

void shellSort(int arr[],int length) {
   int gap;//间距or增量
   for (gap = length/2; gap>0; gap/=2) { //gap->1 共分几次
       for (int i=gap; i<length; i++) {//从gap个元素开始,直接插入排序
           if (arr[i] < arr[i-gap]) {
               int tmp = arr[i];
               int k = i-gap;
               while (k>=0 && arr[k] > tmp) {
                   arr[k+gap] = arr[k];
                   k-=gap;
               }
               arr[k+gap] = tmp;
           }
       }
   }
}

堆排序

  我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。
   在一个长为n的序列,堆排序的过程:从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
  流程总结如下:

  • 首先将无序数组构造成一个大根堆,即:从第i=1个结点开始,将其与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端。当第i插入的结点找到合适位置后,采用上面的方法重复插入第i=2…n个结点。

  • 固定一个最大值,将剩余的数重新构造成一个大根堆,重复这样的过程。即:将最大数落到堆的末尾进行固定,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,然后继续与下面的孩子进行比较。

实现代码:

    //堆排序
    public static void heapSort(int[] arr) {
        //构造大根堆
        heapInsert(arr);
        int size = arr.length;
        while (size > 1) {
            //固定最大值
            swap(arr, 0, size - 1);
            size--;
            //构造大根堆
            heapify(arr, 0, size);
 
        }
 
    }
 
    //构造大根堆(通过新插入的数上升)
    public static void heapInsert(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            //当前插入的索引
            int currentIndex = i;
            //父结点索引
            int fatherIndex = (currentIndex - 1) / 2;
            //如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
            //然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
            while (arr[currentIndex] > arr[fatherIndex]) {
                //交换当前结点与父结点的值
                swap(arr, currentIndex, fatherIndex);
                //将当前索引指向父索引
                currentIndex = fatherIndex;
                //重新计算当前索引的父索引
                fatherIndex = (currentIndex - 1) / 2;
            }
        }
    }
    //将剩余的数构造成大根堆(通过顶端的数下降)
    public static void heapify(int[] arr, int index, int size) {
        int left = 2 * index + 1;
        int right = 2 * index + 2;
        while (left < size) {
            int largestIndex;
            //判断孩子中较大的值的索引(要确保右孩子在size范围之内)
            if (arr[left] < arr[right] && right < size) {
                largestIndex = right;
            } else {
                largestIndex = left;
            }
            //比较父结点的值与孩子中较大的值,并确定最大值的索引
            if (arr[index] > arr[largestIndex]) {
                largestIndex = index;
            }
            //如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环
            if (index == largestIndex) {
                break;
            }
            //父结点不是最大值,与孩子中较大的值交换
            swap(arr, largestIndex, index);
            //将索引指向孩子中较大的值的索引
            index = largestIndex;
            //重新计算交换之后的孩子的索引
            left = 2 * index + 1;
            right = 2 * index + 2;
        }
 
    }
    //交换数组中两个元素的值
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

其他排序算法:

计数排序

1.适用范围:
  这个算法在n个输入元素中每一个都是0到k的范围的整数,其中k也是整数。当k = O(n)时,排序的时间复杂度为Θ(n)。它的性质也就决定了它的运用范围比较窄,但是对于一个待排序的有负数的数组,我们可以将整个数组的整体加上一个整数,使得整个数组的最小值为0,然后就可以使用这个排序算法了。而且这个算法是稳定的。

2.基本思想
  对一个待排序的元素x,我们可以确定小于等于x的个数i,根据这个个数i,我们就可以把x放到索引i处。那么如何确定小于等于x的个数i呢?
  我们可以专门开辟一个数组c[],然后遍历数组,确定数组a中每个元素中出现的频率,然后就可以确定对于a中每个元素x,小于等于这个元素的个数。然后就可以把元素x放到对应位置了。当然元素x的大小是可能重复的,这样就需要我们对数组c的值访问之后减1,保证和x一样大的元素能放在其前面。

3.运行过程

在这里插入图片描述

i . 对于数组A,我们首先统计每个值的个数,将A的值作为C元素索引,值的个数作为C数组的值。比如对于数组A中的元素2,在数组A中出现了2次,所以c[2] = 2,而元素5出现了1次,所以c[5] = 1。

ii . 至此为止,数组C中已经统计了各个元素的出现次数,那么我们就可以根据各个元素的出现次数,累加出比该元素小的元素个数,更新到数组C中。比如a图中,C[0]=2表示出现0的次数为2,C[1]=0表示出现1的次数为0,那么小于等于1的元素个数为C[0]+C[1]=2,我们把C[1]更新为2,同理C[2]=2表示出现2的次数为2,那么小于等于2的元素个数为C[1]+C[2]=4,继续把C[2]更新为4,以此类推…

iii .到这里,我们得到了存储**小于等于元素的个数 **的数组C。现在我们开始从尾部到头部遍历数组A,比如首先我们看A[7] = 3,然后查找C[3],发现C[3] = 7,说明有7个元素小于等于3。我们首先需要做一步C[3] = C[3] - 1,因为这里虽然有7个元素小于等于3,但是B的索引是从0开始的,而且这样减一可以保证下次再找到一个3,可以放在这个3的前面。然后B[C[3]] = 3,就把第一个3放到了对的位置。后面以此类推,直到遍历完数组B。

iv. 截至到这,我们就获得了一个有序的数组B。

4.代码实现
   我们使用Java来实现这个算法:

    public static void countingSort(int[] a, int[] b, int k){
        int[] c = new int[k+1];//存放0~k
        for(int i = 0; i<a.length; i++)
            c[a[i]] += 1;
        for(int i = 1; i<=k; i++)
            c[i] += c[i-1];
        for(int i = a.length-1; i >= 0; i--){
            c[a[i]] --;
            b[c[a[i]]] = a[i];
        }
    }

简简单单几行代码就实现了计数排序,其中参数a数组表示待排序的数组,b数组表示排序之后的存储数组,k表示a数组中最大的值。

桶排序

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值