排序算法,最全的10大排序算法详解(Sort Algorithm)

排序算法,最全的10大排序算法详解(Sort Algorithm)

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。

也就说,把一串数通过一通操作变成有序的一串数,这个操作就要用到排序算法。

下面是十大经典排序算法。

(1)冒泡排序;

(2)选择排序;

(3)插入排序;;

(4)希尔排序;

(5)归并排序;;

(6)快速排序;

(7)基数排序;

(8)堆排序;

(9)计数排序;

(10)桶排序。

我将会采用自己的方法和别人的各种讲法汇总起来讲解

排序算法分类

十种常见排序算法可以分为两大类:

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等

排序算法稳定性

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面

需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。

img

img

时间复杂度(time complexity)

计算机科学中,算法时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。例如,如果一个算法对于任何大小为 n (必须比 n0 大)的输入,它至多需要 5n3 + 3n 的时间运行完毕,那么它的渐近时间复杂度是 O(n3)。

为了计算时间复杂度,我们通常会估计算法的操作单元数量,每个单元运行的时间都是相同的。因此,总运行时间和算法的操作单元数量最多相差一个常量系数。

相同大小的不同输入值仍可能造成算法的运行时间不同,因此我们通常使用算法的最坏情况复杂度,记为 T*(*n*) ,定义为任何大小的输入 n 所需的最大运行时间。另一种较少使用的方法是平均情况复杂度,通常有特别指定才会使用。时间复杂度可以用函数 T(n) 的自然特性加以分类,举例来说,有着 T(n) = O(n) 的算法被称作“线性时间算法”;而 T(n) = O(M*n) 和 M**n= O(T(n)) ,其中 Mn > 1 的算法被称作“指数时间算法”。

1#时间复杂度的意义

究竟什么是时间复杂度呢?让我们来想象一个场景:某一天,小灰和大黄同时加入了一个公司…

640?wx_fmt=jpeg

一天过后,小灰和大黄各自交付了代码,两端代码实现的功能都差不多。大黄的代码运行一次要花100毫秒,内存占用5MB。小灰的代码运行一次要花100秒,内存占用500MB。于是…

640?wx_fmt=jpeg

640?wx_fmt=jpeg

由此可见,衡量代码的好坏,包括两个非常重要的指标:

1.运行时间;

2.占用空间。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

2#基本操作执行次数

关于代码的基本操作执行次数,我们用四个生活中的场景,来做一下比喻:

**场景1:**给小灰一条长10寸的面包,小灰每3天吃掉1寸,那么吃掉整个面包需要几天?

640?wx_fmt=jpeg

答案自然是 3 X 10 = 30天。

如果面包的长度是 N 寸呢?

此时吃掉整个面包,需要 3 X n = 3n 天。

如果用一个函数来表达这个相对时间,可以记作 T(n) = 3n。

**场景2:**给小灰一条长16寸的面包,小灰每5天吃掉面包剩余长度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸…那么小灰把面包吃得只剩下1寸,需要多少天呢?

这个问题翻译一下,就是数字16不断地除以2,除几次以后的结果等于1?这里要涉及到数学当中的对数,以2位底,16的对数,可以简写为log16。

因此,把面包吃得只剩下1寸,需要 5 X log16 = 5 X 4 = 20 天。

如果面包的长度是 N 寸呢?

需要 5 X logn = 5logn天,记作 T(n) = 5logn。


**场景3:**给小灰一条长10寸的面包和一个鸡腿,小灰每2天吃掉一个鸡腿。那么小灰吃掉整个鸡腿需要多少天呢?

640?wx_fmt=jpeg

答案自然是2天。因为只说是吃掉鸡腿,和10寸的面包没有关系 。

如果面包的长度是 N 寸呢?

无论面包有多长,吃掉鸡腿的时间仍然是2天,记作 T(n) = 2。

**场景4:**给小灰一条长10寸的面包,小灰吃掉第一个一寸需要1天时间,吃掉第二个一寸需要2天时间,吃掉第三个一寸需要3天时间…每多吃一寸,所花的时间也多一天。那么小灰吃掉整个面包需要多少天呢?

答案是从1累加到10的总和,也就是55天。

如果面包的长度是 N 寸呢?

此时吃掉整个面包,需要 1+2+3+…+ n-1 + n = (1+n)*n/2 = 0.5n^2 + 0.5n。

记作 T(n) = 0.5n^2 + 0.5n。

640?wx_fmt=jpeg

上面所讲的是吃东西所花费的相对时间,这一思想同样适用于对程序基本操作执行次数的统计。刚才的四个场景,分别对应了程序中最常见的四种执行方式:

如何推导出时间复杂度呢?有如下几个原则:

  1. 如果运行时间是常数量级,用常数1表示;
  2. 只保留时间函数中的最高阶项;
  3. 如果最高阶项存在,则省去最高阶项前面的系数。

3#让我们回头看看刚才的四个场景。

场景1:

T(n) = 3n

最高阶项为3n,省去系数3,转化的时间复杂度为:

T(n) = O(n)

640?wx_fmt=png

场景2:

T(n) = 5logn

最高阶项为5logn,省去系数5,转化的时间复杂度为:

T(n) = O(logn)

640?wx_fmt=png

场景3:

T(n) = 2

只有常数量级,转化的时间复杂度为:

T(n) = O(1)

640?wx_fmt=png

场景4:

T(n) = 0.5n^2 + 0.5n

最高阶项为0.5n^2,省去系数0.5,转化的时间复杂度为:

T(n) = O(n^2)

640?wx_fmt=png

这四种时间复杂度究竟谁用时更长,谁节省时间呢?稍微思考一下就可以得出结论:

O(1)< O(logn)< O(n)< O(n^2)

在编程的世界中有着各种各样的算法,除了上述的四个场景,还有许多不同形式的时间复杂度,比如:

O(nlogn), O(n^3), O(m*n),O(2^n),O(n!)

遨游在代码的海洋里,我们会陆续遇到上述时间复杂度的算法。

640?wx_fmt=png

对数(log、logarithm)

在数学中,对数是对求幂的逆运算,正如除法是乘法的倒数,反之亦然。 这意味着一个数字的对数是必须产生另一个固定数字(基数)的指数。 在简单的情况下,乘数中的对数计数因子。更一般来说,乘幂允许将任何正实数提高到任何实际功率,总是产生正的结果,因此可以对于b不等于1的任何两个正实数b和x计算对数。

如果a的x次方等于N(a>0,且a≠1),那么数x叫做以a为底N的对数(logarithm),记作x=log_a N。其中,a叫做对数的底数,N叫做真数

:23是8,指数为3,底数为2,结果是8。那么以2为底8的对数就是求指数3。

对数数轴与天文数字

对数数轴

我们要是想把为 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IExzVVJ3-1629094072579)(https://www.zhihu.com/equation?tex=%5Ctimes+2)] 坐标系继续画下去是困难的,因为指数增长太快了(指数级增长):

img

尽量缩小才画到对数为5的地方,我相信你已经快看不清了,如果画到对数为100的地方,地球都摆不下这个长度。

我们可以保持对数值等距离摆放,这就是对数坐标系:

img

是不是可以摆下更多的对数了?

天文数字

对数是将数轴进行强力的缩放,再大的数字都经不起对数缩放,如果我选用10为底的话,一亿这么大的数字,在对数数轴上也不过是8。这对于天文学里的天文数字简直是强有力的武器。

要是不进行缩放的话,地球和太阳是不可能同框的:

img

1# 基数排序(radix sort)

时间复杂度:O (nlog®m)

首先,基数排序的名字和计数排序很像,而且原理都是基于桶的原理,但是略有不同,不要记混了。

通俗解释:输入的n个数,对于这n个数,对照每一位比较,每一位的数字只能是0~9,所以最多有10个桶,最后排序到最后一个位,逐个排列。(好吧,总觉得解释的听不懂)

科学解释(但我一开始看不懂):基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

看不懂文字,就看图!

https://visualgo.net/zh/sorting,这里是排序算法集合,很多排序算法的演示都有。visualgo主站还有其他数据结构的演示,比如链表。

在这里插入图片描述

行了,上代码

Java

public class RadixSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxDigit = getMaxDigit(arr);
        return radixSort(arr, maxDigit);
    }

    /**
     * 获取最高位数
     */
    private int getMaxDigit(int[] arr) {
        int maxValue = getMaxValue(arr);
        return getNumLenght(maxValue);
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

    protected int getNumLenght(long num) {
        if (num == 0) {
            return 1;
        }
        int lenght = 0;
        for (long temp = num; temp != 0; temp /= 10) {
            lenght++;
        }
        return lenght;
    }

    private int[] radixSort(int[] arr, int maxDigit) {
        int mod = 10;
        int dev = 1;

        for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
            // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
            int[][] counter = new int[mod * 2][0];

            for (int j = 0; j < arr.length; j++) {
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }

            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
        }

        return arr;
    }

    /**
     * 自动扩容,并保存数据
     *
     * @param arr
     * @param value
     */
    private int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }
}

C++

int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
    int maxData = data[0];              ///< 最大数
    /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
    for (int i = 1; i < n; ++i)
    {
        if (maxData < data[i])
            maxData = data[i];
    }
    int d = 1;
    int p = 10;
    while (maxData >= p)
    {
        //p *= 10; // Maybe overflow
        maxData /= 10;
        ++d;
    }
    return d;
/*    int d = 1; //保存最大的位数
    int p = 10;
    for(int i = 0; i < n; ++i)
    {
        while(data[i] >= p)
        {
            p *= 10;
            ++d;
        }
    }
    return d;*/
}
void radixsort(int data[], int n) //基数排序
{
    int d = maxbit(data, n);
    int *tmp = new int[n];
    int *count = new int[10]; //计数器
    int i, j, k;
    int radix = 1;
    for(i = 1; i <= d; i++) //进行d次排序
    {
        for(j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for(j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for(j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for(j = 0; j < n; j++) //将临时数组的内容复制到data中
            data[j] = tmp[j];
        radix = radix * 10;
    }
    delete []tmp;
    delete []count;
}

2# 冒泡排序(Bubble Sort)

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来

算法描述:
1.比较相邻两个数据如果。第一个比第二个大,就交换两个数
2对每一个相邻的数做同样1的工作,这样从开始一队到结尾一队在最后的数就是最大的数
3.针对所有元素上面的操作,除了最后一个
4.重复1~3步骤,知道顺序完成。

  1. 原理:比较两个相邻的元素,将值大的元素交换到右边

  2. 思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。

(1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。

(2)比较第2和第3个数,将小数 放在前面,大数放在后面。

(3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成

(4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。

(5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。

(6)依次类推,每一趟比较次数减少依次

img

C语言:

#include <stdio.h>
void bubble_sort(int arr[], int len) {
        int i, j, temp;
        for (i = 0; i < len - 1; i++)
                for (j = 0; j < len - 1 - i; j++)
                        if (arr[j] > arr[j + 1]) {
                                temp = arr[j];
                                arr[j] = arr[j + 1];
                                arr[j + 1] = temp;
                        }
}
int main() {
        int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
        int len = (int) sizeof(arr) / sizeof(*arr);
        bubble_sort(arr, len);
        int i;
        for (i = 0; i < len; i++)
                printf("%d ", arr[i]);
        return 0;
}

C++中可以把交换部分变成swap函数,swap(arr[i], arr[j]);

我们只需要记住

for (int i=0;i<n-1;i++)
	for (int j=n-1;j>i;j--)
		if (a[j]>a[j-1]) swap(a[j],a[j-1]);

稍微加入改进

for (int i = 0 ;i<n;i++) {
    bool flag = 1 ;
    for (int j = n-1;j>i;j--){
        if (a[j]>a[j-1]) {
            swap(a[j],a[j-1]) ;
            flag =0 ;
        }
    }
    if (flag) break; // 中途结束算法
}

在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;
int a[10000001] ;
int main(){
	int n , sum = 0 ;
	cin>>n ;
	for (int i = 1;i<= n;i++) 
		cin>> a[i] ;
	for (int i = 1 ;i<n;i++)
		for (int j =1;j<=n-i;j++)
			if (a[j] > a[j+1]) sum ++ ;
	cout<< sum <<endl;
	return 0 ;
}

3# 希尔排序(shellSort)

看懂了插入排序再来,对不起编排顺序有误。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

基本思想

先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2 =1( d t    d t − 1 {d_t}\ \ {d_{t-1}} dt  dt1

该方法实质上是一种分组插入方法

比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。

一般的初次取序列的一半为增量,以后每次减半,直到增量为1。

给定实例的shell排序的排序过程

假设待排序文件有10个记录,其关键字分别是:

49,38,65,97,76,13,27,49,55,04。

增量序列的取值依次为:

5,2,1

希尔排序

img

#include <stdio.h>
int shsort(int s[], int n)    /* 自定义函数 shsort()*/
{
    int i,j,d;
    d=n/2;    /*确定固定增虽值*/
    while(d>=1)
    {
        for(i=d+1;i<=n;i++)    /*数组下标从d+1开始进行直接插入排序*/
        {
            s[0]=s[i];    /*设置监视哨*/
            j=i-d;    /*确定要进行比较的元素的最右边位置*/
            while((j>0)&&(s[0]<s[j]))
            {
                s[j+d]=s[j];    /*数据右移*/
                j=j-d;    /*向左移d个位置V*/
            }
            s[j + d]=s[0];    /*在确定的位罝插入s[i]*/
        }
        d = d/2;    /*增里变为原来的一半*/
    }
return 0;
}

int main()
{
    int a[11],i;    /*定义数组及变量为基本整型*/
    printf("请输入 10 个数据:\n");
    for(i=1;i<=10;i++)
    scanf("%d",&a[i]);    /*从键盘中输入10个数据*/
    shsort(a, 10);    /* 调用 shsort()函数*/
    printf("排序后的顺序是:\n");
    for(i=1;i<=10;i++)
    printf("%5d",a[i]);    /*输出排序后的数组*/
    printf("\n");
    return 0;
}

请输入 10 个数据:

69 56 12 136 3 55 46 99 88 25
排序后的顺序是:
  3  12  25  46  55  56  69  88  99 136

4# 快速排序(quickSort)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

当然,我们可以使用algorithm库自带的sort函数,这是c++给你提供的最方便最快捷的排序函数,可以排序数组和动态数组。例如

bool cmp(int a, int b) // 数组类型适当更改
{
    return a > b; // 重载从大到小
}

vector<int>a;
int t;
while (cin >> t)
    a.push_back(t);
sort(a.begin(), a.end()); // 默认从小到大排序
sort(a.begin(), a.end(), cmp); // 改为从大到小
int array[5] = {6, 7, 2, 3, 5};
sort(array, array + 5); // 从小到大
sort(array, array + 5, cmp); // 同理,改为从大到小

我这个伪代码写得很匆忙,不理解可以百度一下,看看别人的详细代码。

sort函数大概就是这个意思,但是真正快速排序的原理是什么呢?我们要自己写出代码才能明白。,

基本思想:
通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。

然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

详细的图解往往比大堆的文字更有说明力,所以直接上图:

img

上图中,演示了快速排序的处理过程:

初始状态为一组无序的数组:2、4、5、1、3。

经过以上操作步骤后,完成了第一次的排序,得到新的数组:1、2、5、4、3。

新的数组中,以2为分割点,左边都是比2小的数,右边都是比2大的数。

因为2已经在数组中找到了合适的位置,所以不用再动。

2左边的数组只有一个元素1,所以显然不用再排序,位置也被确定。(注:这种情况时,left指针和right指针显然是重合的。因此在代码中,我们可以通过设置判定条件left必须小于right,如果不满足,则不用排序了)。

而对于2右边的数组5、4、3,设置left指向5,right指向3,开始继续重复图中的一、二、三、四步骤,对新的数组进行排序。

在这里插入图片描述

核心代码:

public int division(int[] list, int left, int right) {
    // 以最左边的数(left)为基准
    int base = list[left];
    while (left < right) {
        // 从序列右端开始,向左遍历,直到找到小于base的数
        while (left < right && list[right] >= base)
            right--;
        // 找到了比base小的元素,将这个元素放到最左边的位置
        list[left] = list[right];

        // 从序列左端开始,向右遍历,直到找到大于base的数
        while (left < right && list[left] <= base)
            left++;
        // 找到了比base大的元素,将这个元素放到最右边的位置
        list[right] = list[left];
    }

    // 最后将base放到left位置。此时,left位置的左侧数值应该都比left小;
    // 而left位置的右侧数值应该都比left大。
    list[left] = base;
    return left;
}

private void quickSort(int[] list, int left, int right){

    // 左下标一定小于右下标,否则就越界了
    if (left < right) {
        // 对数组进行分割,取出下次分割的基准标号
        int base = division(list, left, right);

        System.out.format("base = %d:\t", list[base]);
        printPart(list, left, right);

        // 对“基准标号“左侧的一组数值进行递归的切割,以至于将这些数值完整的排序
        quickSort(list, left, base - 1);

        // 对“基准标号“右侧的一组数值进行递归的切割,以至于将这些数值完整的排序
        quickSort(list, base + 1, right);
    }
}

C++:

#include <stdio.h>
#include <stdlib.h>

void display(int* array, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

int getStandard(int array[], int i, int j) {
    // 基准数据
    int key = array[i];
    while (i < j) {
        // 因为默认基准是从左边开始,所以从右边开始比较
        // 当队尾的元素大于等于基准数据 时,就一直向前挪动 j 指针
        while (i < j && array[j] >= key) {
            j--;
        }
        // 当找到比 array[i] 小的时,就把后面的值 array[j] 赋给它
        if (i < j) {
            array[i] = array[j];
        }
        // 当队首元素小于等于基准数据 时,就一直向后挪动 i 指针
        while (i < j && array[i] <= key) {
            i++;
        }
        // 当找到比 array[j] 大的时,就把前面的值 array[i] 赋给它
        if (i < j) {
            array[j] = array[i];
        }
    }
    // 跳出循环时 i 和 j 相等,此时的 i 或 j 就是 key 的正确索引位置
    // 把基准数据赋给正确位置
    array[i] = key;
    return i;
}

void QuickSort(int array[], int low, int high) {
    // 开始默认基准为 low
    if (low < high) {
        // 分段位置下标
        int standard = getStandard(array, low, high);
        // 递归调用排序
        // 左边排序
        QuickSort(array, low, standard - 1);
        // 右边排序
        QuickSort(array, standard + 1, high);
    }
}

// 合并到一起快速排序
// void QuickSort(int array[], int low, int high) {
//     if (low < high) {
//         int i   = low;
//         int j   = high;
//         int key = array[i];
//         while (i < j) {
//             while (i < j && array[j] >= key) {
//                 j--;
//             }
//             if (i < j) {
//                 array[i] = array[j];
//             }
//             while (i < j && array[i] <= key) {
//                 i++;
//             }
//             if (i < j) {
//                 array[j] = array[i];
//             }
//         }
//         array[i] = key;
//         QuickSort(array, low, i - 1);
//         QuickSort(array, i + 1, high);
//     }
// }

int main() {
    int array[] = {49, 38, 65, 97, 76, 13, 27, 49, 10};
    int size    = sizeof(array) / sizeof(int);

    // 打印数据
    printf("%d \n", size);
    QuickSort(array, 0, size - 1);
    display(array, size);

    // int size      = 20;
    // int array[20] = {0};                 // 数组初始化
    // for (int i = 0; i < 10; i++) {       // 数组个数
    //     for (int j = 0; j < size; j++) { // 数组大小
    //         array[j] = rand() % 1000;    // 随机生成数大小 0~999
    //     }
    //     printf("原来的数组:");
    //     display(array, size);
    //     QuickSort(array, 0, size - 1);
    //     printf("排序后数组:");
    //     display(array, size);
    //     printf("\n");
    // }

    return 0;
}

#include<iostream>
using namespace std;
int n,a[1000001];
void qsort(int l,int r)//应用二分思想
{
    int mid=a[(l+r)/2];//中间数
    int i=l,j=r;
    do{
        while(a[i]<mid) i++;//查找左半部分比中间数大的数
        while(a[j]>mid) j--;//查找右半部分比中间数小的数
        if(i<=j)//如果有一组不满足排序条件(左小右大)的数
        {
            swap(a[i],a[j]);//交换
            i++;
            j--;
        }
    }while(i<=j);//这里注意要有=
    if(l<j) qsort(l,j);//递归搜索左半部分
    if(i<r) qsort(i,r);//递归搜索右半部分
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    qsort(1,n);
    for(int i=1;i<=n;i++) cout<<a[i]<<" ";
}

在这里插入图片描述
在这里插入图片描述

快速排序算法的性能

img

时间复杂度

当数据有序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差。

而当数据随机分布时,以第一个关键字为基准分为两个子序列,两个子序列的元素个数接近相等,此时执行效率最好。

所以,数据越随机分布时,快速排序性能越好;数据越接近有序,快速排序性能越差。

空间复杂度

快速排序在每次分割的过程中,需要 1 个空间存储基准值。而快速排序的大概需要 Nlog2N次的分割处理,所以占用空间也是 Nlog2N 个。

算法稳定性

在快速排序中,相等元素可能会因为分区而交换顺序,所以它是不稳定的算法。

时间复杂度:

最好: O ( n l o g 2 n ) O(n log_{2} n) O(nlog2n)
最坏: O ( n 2 ) O(n^2) O(n2)
平均: O ( n l o g 2 n ) O(n log_{2} n) O(nlog2n)
空间复杂度: O ( n l o g 2 n ) O(n log_{2} n) O(nlog2n)

在这里插入图片描述

5# 堆排序(heapSort)

简介

堆排序(英语:Heapsort)是指利用 二叉堆 这种数据结构所设计的一种排序算法。堆排序的适用数据结构为数组。

工作原理

本质是建立在堆上的选择排序。

排序过程
首先建立大顶堆,然后将堆顶的元素取出,作为最大值,与数组尾部的元素交换,并维持残余堆的性质;

之后将堆顶的元素取出,作为次大值,与数组倒数第二位元素交换,并维持残余堆的性质;

以此类推,在第n-1 次操作后,整个数组就完成了排序。

在数组上建立二叉堆

从根节点开始,依次将每一层的节点排列在数组里。

于是有数组中下标为 i 的节点,对应的父结点、左子结点和右子结点如下:

iParent(i) = (i - 1) / 2;
iLeftChild(i) = 2 * i + 1;
iRightChild(i) = 2 * i + 2;

算法评估
稳定性:不稳定,如同选择排序
速度:O(nlogn),算是高效算法
空间:由于可以在输入数组上建立堆,所以这是一个原地算法。

// C++ Version
void sift_down(int arr[], int start, int end) {
  // 计算父结点和子结点的下标
  int parent = start;
  int child = parent * 2 + 1;
  while (child <= end) {  // 子结点下标在范围内才做比较
    // 先比较两个子结点大小,选择最大的
    if (child + 1 <= end && arr[child] < arr[child + 1]) child++;
    // 如果父结点比子结点大,代表调整完毕,直接跳出函数
    if (arr[parent] >= arr[child])
      return;
    else {  // 否则交换父子内容,子结点再和孙结点比较
      swap(arr[parent], arr[child]);
      parent = child;
      child = parent * 2 + 1;
    }
  }
}

void heap_sort(int arr[], int len) {
  // 从最后一个节点的父节点开始 sift down 以完成堆化 (heapify)
  for (int i = (len - 1 - 1) / 2; i >= 0; i--) sift_down(arr, i, len - 1);
  // 先将第一个元素和已经排好的元素前一位做交换,再重新调整(刚调整的元素之前的元素),直到排序完毕
  for (int i = len - 1; i > 0; i--) {
    swap(arr[0], arr[i]);
    sift_down(arr, 0, i - 1);
  }
}

6# 归并排序(mergeSort)

归并排序(英语:Merge sort,或 mergesort),是创建在归并操作上的一种有效的排序算法。1945年由约翰·冯·诺伊曼首次提出。

该算法是采用分治法( Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
在这里插入图片描述

  • 分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
  • 再来看看 治 阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将【45,7,8】和【1,2,3,6】
    两个已经有序的子序列,合并为最终序列【1,2,3,4,5,6,7,8】,来看下实现步骤

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
伪代码
在这里插入图片描述

归并排序评估:

  • 最好最坏平均时间复杂度nlogn
  • 空间复杂度高O(n)
  • 是高效算法中唯一“稳定”的排序算法
  • 较少用于内部排序,较多用于外部排序

模板:

// C++ Version
void merge(int ll, int rr) {
  // 用来把 a 数组 [ll, rr - 1] 这一区间的数排序。 t
  // 数组是临时存放有序的版本用的。
  if (rr - ll <= 1) return;
  int mid = ll + (rr - ll >> 1);
  merge(ll, mid);
  merge(mid, rr);
  int p = ll, q = mid, s = ll;
  while (s < rr) {
    if (p >= mid || (q < rr && a[p] > a[q])) {
      t[s++] = a[q++];
      // ans += mid - p;
    } else
      t[s++] = a[p++];
  }
  for (int i = ll; i < rr; ++i) a[i] = t[i];
}
// 关键点在于一次性创建数组,避免在每次递归调用时创建,以避免内存分配的耗时。

我们发现去掉代码中的注释,ans中保存的就是逆序对的数量

7# 插入排序(Insertion Sort)

基本思想:每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。

img

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

img

程序在设计的时候应该注意一些点:每次要定一个基准数,逐个比较,覆盖的时候要留有备份。

C++函数代码:

void insertion_sort(int arr[],int len){
        for(int i=1;i<len;i++){
                int key=arr[i]; // 标兵
                int j=i-1;
                while((j>=0) && (key<arr[j])){ // 后移操作
                        arr[j+1]=arr[j];
                        j--;
                }
                arr[j+1]=key; // 最终赋值标兵
        }
}

C#函数代码:

public static void InsertSort(int[] array)
{
    for(int i = 1;i < array.length;i++)
    {
        int temp = array[i];
        for(int j = i - 1;j >= 0;j--)
        {
            if(array[j] > temp)
            {
                array[j + 1] = array[j];
                array[j] = temp;
            }
            else
                break;
        }
    }
}

程序需要,把这个函数复制,传递对应参数。
在这里插入图片描述

在插入排序中,我们可以用二分法来优化,从而更快确定数字
在这里插入图片描述
在这里插入图片描述

8# 选择排序(Selection sort)

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

这是首批的O(n2)算法,如下是算法步骤。

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

C 语言

void swap(int *a,int *b) 
{
  int temp = *a;
  *a = *b;
  *b = temp;
}
void selection_sort(int arr[], int len)
{
  int i,j;
  for (i = 0 ; i < len - 1 ; i++)
  {
        int min = i;
        for (j = i + 1; j < len; j++)   
            if (arr[j] < arr[min])   
                min = j;   
        swap(&arr[min], &arr[i]);   
  }
}

注:这种排序算法非常的**慢!慢!慢!**初学者可以使用这种算法,进阶后绝对不要使用这种算法,至少也要用上插入排序、桶排序之类的,除非排序时n<10000,或者简单的做个测试的时候使用。
在这里插入图片描述

9# 计数排序(CountingSort)

评估
稳定性:稳定
速度:O(n+m),m代表待排序数据的值域大小

工作原理:
计数排序的工作原理是使用一个额外的数组 C,其中第 i个元素是待排序数组 A 中值等于 i的元素的个数,然后根据数组 C来将 A中的元素排到正确的位置。

它的工作过程分为三个步骤:

  1. 计算每个数出现了几次;
  2. 求出每个数出现次数的 前缀和;
  3. 利用出现次数的前缀和,从右至左计算每个数的排名。

在这里插入图片描述

// C++ Version
const int N = 100010;
const int W = 100010;

int n, w, a[N], cnt[W], b[N];

void counting_sort() {
  memset(cnt, 0, sizeof(cnt));
  for (int i = 1; i <= n; ++i) ++cnt[a[i]];
  for (int i = 1; i <= w; ++i) cnt[i] += cnt[i - 1];
  for (int i = n; i >= 1; --i) b[cnt[a[i]]--] = a[i];
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10# 桶排序(bucketSort)

在这里插入图片描述

桶排序评估:
速度:O(n)
空间复杂度:需要建立一个容量为输入数字中的最大数+1的数组,所以空间复杂度超高,所以输入数字最好不要超过10000000

// C++ Version
#include <iostream>
#include <cstring>
using namespace std;
#define N 100010
int a[N];
int n, t;
int main()
{
	memset(a, 0, sizeof(a));
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		cin >> t;
		a[t]++;
	}
	// 从小到大,要从大到小只需要改一下for循环开头
	// for (int i = N - 1; i >= 0; i--)
	for(int i = 0; i < N; i++)
	{
		for (int j = 0; j < a[i]; j++)
			cout << i << " " ;
	}
	cout << endl;
	return 0;
}

PS#桶排序&计数排序&基数排序

基数排序有两种方法:

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

参考资料

参考书目

  • 《信息学奥赛课课通》
  • 《信息学奥赛一本通,C++篇》
  • 《啊哈!算法》
  • 《数据结构与算法分析,C语言描述》[美]马克·艾伦·维斯 著

参考程序

我将7种常用排序放集合在了一个程序里,你只需要在文件的同目录下创建一个RandintModel.txt,其中的输入格式如下

n   (一共n个数,至少大于1000,要不然测不出速度,当然你可以自己写一个随机数生成程序)
n个数

输出的是排序时间和一个文件Lastlog.txt,里面是最后一次的排序结果,不要用notepad打开,会卡死。(建议使用notepad++)

// SortingSpeed.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>
#include <windows.h>
#include <cstdio>
#include <fstream>
#include <vector>
#include <array>
#include <algorithm>
#include <numeric>
//numeric_limits
using namespace std;
DWORD time_start, time_end;
int a[100010], n;

void max_heapify(int arr[], int start, int end) {
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) {
        if (son + 1 <= end && arr[son] < arr[son + 1])
            son++;
        if (arr[dad] > arr[son])
            return;
        else {
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heap_sort(int arr[], int len) {
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    for (int i = len - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}
typedef struct _Range {
    int start, end;
} Range;

Range new_Range(int s, int e) {
    Range r;
    r.start = s;
    r.end = e;
    return r;
}

void swap(int* x, int* y) {
    int t = *x;
    *x = *y;
    *y = t;
}

void quick_sort(int arr[], const int len) {
    if (len <= 0)
        return;
    Range* r = new Range[len];
    int p = 0;
    r[p++] = new_Range(0, len - 1);
    while (p) {
        Range range = r[--p];
        if (range.start >= range.end)
            continue;
        int mid = arr[(range.start + range.end) / 2];
        int left = range.start, right = range.end;
        do {
            while (arr[left] < mid) ++left;
            while (arr[right] > mid) --right;
            if (left <= right) {
                swap(&arr[left], &arr[right]);
                left++;
                right--;
            }
        } while (left <= right);
        if (range.start < right) r[p++] = new_Range(range.start, right);
        if (range.end > left) r[p++] = new_Range(left, range.end);
    }
}
//void Merge(vector<int>& Array, int front, int mid, int end) {
//    // preconditions:
//    // Array[front...mid] is sorted
//    // Array[mid+1 ... end] is sorted
//    // Copy Array[front ... mid] to LeftSubArray
//    // Copy Array[mid+1 ... end] to RightSubArray
//    vector<int> LeftSubArray(Array.begin() + front, Array.begin() + mid + 1);
//    vector<int> RightSubArray(Array.begin() + mid + 1, Array.begin() + end + 1);
//    int idxLeft = 0, idxRight = 0;
//    LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max());
//    RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max());
//    // Pick min of LeftSubArray[idxLeft] and RightSubArray[idxRight], and put into Array[i]
//    for (int i = front; i <= end; i++) {
//        if (LeftSubArray[idxLeft] < RightSubArray[idxRight]) {
//            Array[i] = LeftSubArray[idxLeft];
//            idxLeft++;
//        }
//        else {
//            Array[i] = RightSubArray[idxRight];
//            idxRight++;
//        }
//    }
//}
//
//void MergeSort_Rec(vector<int>& Array, int front, int end) {
//    if (front >= end)
//        return;
//    int mid = (front + end) / 2;
//    MergeSort_Rec(Array, front, mid);
//    MergeSort_Rec(Array, mid + 1, end);
//    Merge(Array, front, mid, end);
//}
void insertion_sort(int arr[], int len) {
    for (int i = 1; i < len; i++) {
        int key = arr[i];
        int j = i - 1;
        while ((j >= 0) && (key < arr[j])) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
    int maxData = data[0];              ///< 最大数
    /// 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
    for (int i = 1; i < n; ++i)
    {
        if (maxData < data[i])
            maxData = data[i];
    }
    int d = 1;
    int p = 10;
    while (maxData >= p)
    {
        //p *= 10; // Maybe overflow
        maxData /= 10;
        ++d;
    }
    return d;
    /*    int d = 1; //保存最大的位数
        int p = 10;
        for(int i = 0; i < n; ++i)
        {
            while(data[i] >= p)
            {
                p *= 10;
                ++d;
            }
        }
        return d;*/
}
void radixsort(int data[], int n) //基数排序
{
    int d = maxbit(data, n);
    int* tmp = new int[n];
    int* count = new int[10]; //计数器
    int i, j, k;
    int radix = 1;
    for (i = 1; i <= d; i++) //进行d次排序
    {
        for (j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for (j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for (j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for (j = 0; j < n; j++) //将临时数组的内容复制到data中
            data[j] = tmp[j];
        radix = radix * 10;
    }
    delete[]tmp;
    delete[]count;
}
void selection_sort(int arr[], int len)
{
    int i, j;
    for (i = 0; i < len - 1; i++)
    {
        for (j = i + 1; j < len; j++)
            if (arr[j] < arr[i])
                swap(arr[j], arr[i]);    
    }
}
void shell_sort(int arr[], int len) {
    int gap, i, j;
    int temp;
    for (gap = len >> 1; gap > 0; gap >>= 1)
        for (i = gap; i < len; i++) {
            temp = arr[i];
            for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
                arr[j + gap] = arr[j];
            arr[j + gap] = temp;
        }
}
void bubble_sort(int arr[], int len)
{
    int i, j, temp;
    for (i = 0; i < len - 1; i++)
        for (j = 0; j < len - 1 - i; j++)
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
}
void menu()
{
    printf("Sorting Algorithm\n------------------\n");
    printf("1.希尔排序\n");
    printf("2.冒泡排序\n");
    printf("3.选择排序\n");
    printf("4.基数排序\n");
    printf("5.插入排序\n");
    printf("6.堆排序\n");
    printf("7.快速排序\n");
    // new
    printf("--Choose Number--\n------------------\n");
}
void game()
{
    menu();
    int choose;
    cin >> choose;
    time_start = GetTickCount();
    if (choose == 1)
        shell_sort(a, n);
    else if (choose == 2)
        bubble_sort(a, n);
    else if (choose == 3)
        selection_sort(a, n);
    else if (choose == 4)
        radixsort(a, n);
    else if (choose == 5)
        insertion_sort(a, n);
    else if (choose == 6)
        heap_sort(a, n);
    else if (choose == 7)
        quick_sort(a, n);
    // new
    time_end = GetTickCount();
    cout << "Time = " << (time_end - time_start) << "ms\n ";
}
int main()
{
    ifstream fin("RandintModel.txt");
    ofstream fout("Lastlog.txt");
    fin >> n;
    for (int i = 0; i < n; i++)
        fin >> a[i];
    game();
    printf("-----排序结果-----\n");
    for (int i = 0; i < n; i++)
    {
        fout << a[i] << " ";
    } fout << endl;

    std::cout << "Hello World!\n";
    return 0;
}

参考习题

P1097 [NOIP2007 提高组] 统计数字

P1093 [NOIP2007 普及组] 奖学金

P1059 [NOIP2006 普及组] 明明的随机数

P1094 [NOIP2007 普及组] 纪念品分组

P1161 开灯

洛谷中选择排序算法题目可以找到更多题目。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值