【君义精讲】排序算法

一、概念

1. 排序的定义

一般定义:将一组无序的数据元素调整为有序的数据元素。
数学定义:假设含n个数据元素的序列为 R 1 , R 2 , . . . , R n {R_1,R_2,...,R_n} R1R2...Rn,其相应的关键字序列为 K 1 , K 2 , . . . , K n {K_1,K_2,...,K_n} K1K2...Kn,这些关键字相互之间可以进行比较,即在它们之间存在这样的关系: K p 1 ≤ K p 2 ≤ . . . ≤ K p n K_{p1}\le K_{p2}\le...\le K_{pn} Kp1Kp2...Kpn,按此固有关系将上式记录序列重新排序为: { R p 1 , R p 2 , . . . , R p n } \{R_{p1}, R_{p2}, ... ,R_{pn}\} {Rp1,Rp2,...,Rpn}的操作称为排序。

2. 排序的前提条件

首先,要排序的数据元素必须类型相同。
其次,数据元素之间是可以比较的,即只要给定两个元素,就可以说出谁“大”谁“小”,当然,很多情况下“大”与“小”是人为定义的。

例:
整数或小数比较:可以直接比较数值大小。
字母比较:字母在前面的被认为更小,字母在后面的被认为更大。如’a’比’z’小。
字符串比较:按字典序比较,字典序在前的被认为更小,字典序在后面的被认为更大。
数对比较:数对的第一个数字更大的数对更大,数对第一个数字相同时,第二个数字更大的数对更大。

第三,要明确排成升序还是降序。
严格意义下,有序序列的顺序有以下4种:

  • 升序序列:后面的数据元素大于前面的数据元素。
  • 不降序列:后面的数据元素大于等于前面的数据元素。
  • 不升序列:后面的数据元素小于等于前面的数据元素。
  • 降序序列:后面的数据元素小于前面的数据元素。

以下讨论时,使用非严格叙述,即用升序表示升序序列及不降序列,用降序表示不升序列及降序序列。

3. 内部排序和外部排序:
  1. 内部排序:整个排序过程不需要访问外存便能完成。
  2. 外部排序:待排序的数据元素数量很大,整个序列的排序过程需要借助外存才能完成。

本文只介绍内部排序。

4. 排序中的关键操作
  1. 比较:任意两个数据元素通过比较确定先后顺序。
    根据元素间比较规则写出元素比较的表达式或函数,表达式的值必须是布尔值,表示第一个元素是否比第二个元素小。
  2. 交换:两个数据元素之间数据值发生了交换。
    常用到STL中的swap函数,可以实现任意类型变量值的交换。其实现为:
template<class T>
void Swap(T &a, T &b)//STL中的函数为swap(),这里为了加以区分,写为Swap()
{
    T t = a;
    a = b;
    b = t;
}
5. 排序算法的稳定性

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,那么这个排序算法就是稳定的。
即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

5. 对排序的评价
  1. 时间复杂度:关键性能差异体现在比较和交换的次数
  2. 辅助空间复杂度:为完成排序操作需要的额外的存储空间的大小
    本身存储数据的存储空间不算在内,只考虑由于选择这一排序算法带来的额外空间开销。
6. 排序图示及动画

这个已经有很多人做过了,本文不再给出相关图示及动画。排序动画可以在这个网站看:
visualgo.net排序动画

二、排序算法

n为待排序的元素个数。
本文以该题为例题:洛谷 P1177 【模板】快速排序
复杂度为 O ( n 2 ) O(n^2) O(n2)的排序算法只能通过该题的一个测试点。
计数排序无法完成该题。
希尔排序、基数排序以及多数复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)的排序算法可以完全通过该题所有测试点。
该题几个测试点测试了极端情况,比如序列一开始就是有序的,或都是同一个值。一些排序算法在极端情况下复杂度会退化为 O ( n 2 ) O(n^2) O(n2),因而可能有几个点过不了。
本文只给出下标从1开始的数组的排序算法代码。下标从0开始的数组的排序算法代码与之类似,不再赘述。
本文只给出升序排序算法代码,降序排序一般只需要将比较符号修改一下即可实现,不再赘述。

1. 直接选择排序

  • 基本思想:每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在待排序的数列的最前面,直到全部待排序的数据元素排完。
    第1次在下标1~n中选择最小值,与下标1的元素进行交换。
    第2次在下标2~n中选择最小值,与下标2的元素进行交换。
    第i次在下标i~n中选择最小值,与下标i的元素进行交换。
    第n-1次,在下标n-1~n中选择最小值,与下标n-1的元素进行交换。
  • 复杂度:时间复杂度: O ( n 2 ) O(n^2) O(n2)    额外空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:不稳定
  • 示例:
    粗体数字为当前要替换的位置,斜体数字为已经确定的有序序列

8 3 2 6 4 从8到末尾的范围内,选择最小值2与8交换
2 3 8 6 4 从3到末尾的范围内,选择最小值3与3交换(实际不变)
2 3 8 6 4 从8到末尾的范围内,选择最小值4与8交换
2 3 4 6 8 从6到末尾的范围内,选择最小值6与6交换(不变)
2 3 4 6 8 所有数字都已确定位置

  • 代码:
    写法1:找出最小值而后交换
#include <bits/stdc++.h>
using namespace std;
int main()
{
    int a[100005], n, mi;
    cin >> n;
    for(int i = 1; i <= n; ++i)//输入数据 
        cin >> a[i];
    for(int i = 1; i <= n-1; ++i)//直接选择排序 升序排序 
    {
        mi = i;//mi:从i到n的最小值的下标 
        for(int j = i+1; j <= n; ++j)
            if(a[j] < a[mi])//如为 > 即可实现降序排序 
                mi = j;
        swap(a[i], a[mi]);
    }
    for(int i = 1; i <= n; ++i)//输出数据 
        cout << a[i] << ' ';
    return 0;
}

写法2:不断和第i位置交换

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int a[100005], n;
    cin >> n;
    for(int i = 1; i <= n; ++i)//输入数据 
        cin >> a[i];
    for(int i = 1; i <= n-1; ++i)//直接选择排序 升序排序 
        for(int j = i+1; j <= n; ++j)
            if(a[j] < a[i])//如为 > 即可实现降序排序
                swap(a[i], a[j]);
    for(int i = 1; i <= n; ++i)//输出数据 
        cout << a[i] << ' ';
    return 0;
}

2. 冒泡排序

  • 基本思想:重复地访问要排序的元素序列,依次比较两个相邻的元素,如果两元素的顺序与预期顺序不符,就将二者交换。
    每趟冒泡后,最大(最小)的元素会经由交换“浮”到序列右端(或左端)。多次冒泡后,即可完成排序。
  • 复杂度:时间复杂度: O ( n 2 ) O(n^2) O(n2)    额外空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:稳定
  • 示例
    粗体数字为要比较的两个数字,斜体数字为已经确定位置的数字

第1趟冒泡
8 3 2 6 4
3 8 2 6 4
3 2 8 6 4
3 2 6 8 4
3 2 6 4 8
第2趟冒泡
3 2 6 4 8
2 3 6 4 8
2 3 6 4 8
2 3 4 6 8
第3趟冒泡
2 3 4 6 8
2 3 4 6 8
2 3 4 6 8
第4趟冒泡
2 3 4 6 8
2 3 4 6 8
最后,第一个数字的位置自然被确定
2 3 4 6 8

  • 代码:
    写法1:易记版本:i从1到n-1,j从1到n-i
#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n, a[100005];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    for(int i = 1; i <= n-1; ++i)//i:需要冒泡n-1次。
        for(int j = 1; j <= n-i; ++j)//j: 要交换的数对中第一个数的位置。最后一次比较的数对的第一个数字位于n-i位置。
            if(a[j] > a[j+1])
                swap(a[j], a[j+1]);
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

写法2:符合概念的写法

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n, a[100005];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
	for(int i = n; i >= 2; --i)//i:此次冒泡,最大值最后落在的位置。
	    for(int j = 1; j <= i - 1; ++j)//j: 要交换的数对中第一个数的位置
	        if(a[j] > a[j + 1])
	            swap(a[j], a[j + 1]);
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

写法3:将最小值冒泡到前面的写法

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n, a[100005];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    for(int i = 1; i <= n - 1; ++i)//i:从后向前冒泡,最小值所在位置 
        for(int j = n; j >= i + 1; --j)//j:比较数对第二个数的位置 
            if(a[j] < a[j - 1])
                swap(a[j], a[j - 1]);     
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

3. 插入排序

  • 基本思想:插入排序是指在待排序的元素中,假设前面n-1个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,使得插入后这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程。
  • 复杂度:时间复杂度: O ( n 2 ) O(n^2) O(n2)    额外空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:稳定
  • 示例
    粗体数字为当前正在插入的数字,斜体数字为已经构造好的有序序列
    每次将正在插入的数字和其前面的数字比较

第1个数字自然构成一个有序序列
8 3 2 6 4
插入第2个数字
8 3 2 6 4
3 8 2 6 4
插入第3个数字
3 8 2 6 4
3 2 8 6 4
2 3 8 6 4
插入第4个数字
2 3 8 6 4
2 3 6 8 4
插入第5个数字
2 3 6 8 4
2 3 6 4 8
2 3 4 6 8
2 3 4 6 8

  • 代码
    写法1:不断交换元素
#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n, a[100005];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    for(int i = 2; i <= n; ++i)//将第i个数字插入有序序列
    {
        for(int j = i; j > 1; j--)//j指向待插入数字
        {
            if(a[j] < a[j - 1])
                swap(a[j], a[j - 1]);
            else
                break;
        }
    }
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

写法2:用一个变量保存要插入的值

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n, a[100005];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    for(int i = 2; i <= n; ++i)//将第i个数字插入有序序列
    {
        int j, num = a[i];//num:待插入数字 
        for(j = i; j > 1; j--)//j指向待插入数字的位置
        {
            if(num < a[j - 1])
                a[j] = a[j - 1];
            else
                break;
        }
        a[j] = num;
    } 
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

写法3:可以一边输入一边做插入排序(常用)

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

4. 希尔排序

  • 基本思想:希尔排序是把数据元素按下标的一定增量分组,对每组使用直接插入排序算法排序。随着增量逐渐减少,每组包含的元素越来越多,当增量减至 1 时,整个数组就已经完成排序。
  • 复杂度:时间复杂度: O ( n 1.3 ) ∼ O ( n 2 ) O(n^{1.3}) \sim O(n^2) O(n1.3)O(n2)    额外空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:不稳定
  • 示例:

初始增量n/2,为3
8 3 2 6 4 1  8与6一组,3与4一组,2与1一组,分别做插入排序,结果为:6 3 1 8 4 2
增量除以2,为1
6 3 1 8 4 2    做插入排序,结果为:1 2 3 4 6 8

  • 代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n, a[100005];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    for(int g = n/2; g > 0; g /= 2)//g:增量
    {
        for(int i = g+1; i <= n; ++i)
        {
            for(int j = i; j > g; j-=g)
            {
                if(a[j] < a[j-g])
                    swap(a[j], a[j-g]);
                else
                    break;
            }
        }
    } 
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

5. 计数排序

  • 基本概念:
    散列思想:通过散列函数把数据值对应到一个数组下标
    桶排序:将数据通过散列函数分配到有限数量的有序的桶里,每个桶内分别排序,最后将各个桶中的数据合并。
    计数排序:是一种特殊的桶排序。桶的个数是可能出现的数据种类数。
    计数数组:数组下标为数据值,数组的值表示数据个数。即a[i]表示数字i出现的个数。
    计数排序只适用于对范围有限的整数进行排序。
  • 复杂度:
    数据个数为n,数据范围为k
    时间复杂度: O ( n + k ) O(n+k) O(n+k)
    额外空间复杂度: O ( k ) O(k) O(k)
  • 稳定性:稳定
  • 示例:

待排序数据:1, 5, 3, 1, 3, 2
输入数据并统计后,计数数组c为

下标12345
21201

遍历计数数组,将i输出c[i]次,为:1 1 2 3 3 5

  • 代码
    用计数排序做不了P1177,这里用计数排序完成的问题为:
    给定n个1~1000范围内的整数( n ≤ 1 0 4 n \le 10^4 n104),输出其升序序列。
#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n, a, c[1005] = {};
    cin >> n;
    for(int i = 1; i <= n; ++i)
    {
        cin >> a;
        c[a]++;
    }
    for(int i = 1; i <= 1000; ++i)
    {
        for(int j = 1; j <= c[i]; ++j)
            cout << i << ' ';
    }
    return 0;
}

6. 基数排序

  • 基本概念:基数排序是一种整数排序算法,其原理是将整数按每个位数分别比较。相当于多趟桶排序,适合位数有限的整数排序。
  • 复杂度:
    d是数字位数,n是数字个数,r是基数(进制)
    时间复杂度: O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))
    额外空间复杂度: O ( r + n ) O(r+n) O(r+n)
  • 稳定性:稳定
  • 示例:

待排序数字:53, 542, 3, 63, 14, 214
第一趟按个位排序后:542, 53, 3, 63, 14, 214
第二趟按十位排序后:3, 14, 214, 542, 53, 63
第三趟按百位排序后:3, 14, 53, 63, 214, 542

  • 代码:
#include <bits/stdc++.h>
using namespace std;
#define N 100005
int countBit(int x)//输出整数x的位数 
{
    return floor(log10(x)) + 1;
}
int main()
{
    int a[N], temp[N], n, mxBit = 0, buk[15];//mxBit:数字中的最大位数 
    cin >> n;
    for(int i = 1; i <= n; ++i)
    {
        cin >> a[i];
        mxBit = max(mxBit, countBit(a[i]));
    }
    for(int b = 1, rad = 1; b <= mxBit; ++b, rad *= 10)//每趟针对第b位进行桶排序,位权rad 
    {
        memset(buk, 0, sizeof(buk));//桶清零 
        for(int i = 1; i <= n; ++i)//按第b位加入桶中计数 buk[i]表示第b位为i的数字数量 
            buk[a[i]/rad%10]++;
        for(int i = 1; i <= 9; ++i)//buk[i]的概念变为第b位为i的数字的最大下标 
            buk[i] += buk[i-1];
        for(int i = n; i >= 1; --i)//临时数组temp的buk[k]位置添加数字a[i],buk[k]作为下一次添加数字的下标,应该减1。由大向小遍历。 
        {
            int k = a[i]/rad%10; 
            temp[buk[k]--] = a[i];
        }
        for(int i = 1; i <= n; ++i)
            a[i] = temp[i]; 
    }
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

7. 归并排序

  • 基本概念:
  1. 合并有序序列
    给定两个长为a、b的有序序列,合并成长为a+b的有序序列。
  2. 归并排序对序列的元素进行逐层折半分组,然后从最小分组开始合并有序序列,逐层进行,最后得到整个序列的排序结果。
  • 复杂度:
    时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    空间复杂度: O ( n ) O(n) O(n)
  • 稳定性:稳定
  • 示例:

待排序数字序列 1 5 6 3 2 7 9 8
先进行折半分组,对于分组后的每一组,再进行折半分组,直到每组只有1个元素
1 5 6 3 | 2 7 9 8
1 5 | 6 3 | 2 7 | 9 8
1 | 5 | 6 | 3 | 2 | 7 | 9 | 8
依次对于每次分开的两组合并有序序列
1 5 | 3 6 | 2 7 | 8 9
1 3 5 6 | 2 7 8 9
1 2 3 5 6 7 8 9

  • 代码
#include<bits/stdc++.h>
using namespace std;
#define N 100005 
int a[N], t[N];//t:临时数组 
void mergeSort(int l, int r)//a数组下标l到r进行归并排序 
{
    if(l >= r)
        return;
    int mid = (l + r)/2;
    mergeSort(l, mid);
    mergeSort(mid+1, r);
    int ti = l, li = l, ri = mid+1;//li第一个段数组中的下标 ri第二段数组中的下标 ti临时数组中的下标
    while(li <= mid && ri <= r)
    {
        if(a[li] < a[ri])
            t[ti++] = a[li++];
        else
            t[ti++] = a[ri++];
    }
    while(li <= mid)//如果其中一段数组都填入t了,那么将另一段数组剩下的所有数字都填入t
        t[ti++] = a[li++];
    while(ri <= r)
        t[ti++] = a[ri++];
    for(int i = l; i <= r; ++i) 
        a[i] = t[i];
}
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    mergeSort(1, n);
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

8. 快速排序

  • 基本概念:
    通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小。分别对这两部分记录继续进行快速排序,以达到整个序列有序。

  • 复杂度:
    时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    额外空间复杂度: O ( 1 ) O(1) O(1)

  • 稳定性:不稳定

  • 代码

#include<bits/stdc++.h>
using namespace std;
#define N 100005 
int a[N];
void quickSort(int l, int r)
{  
    if(l >= r)
        return;
    int i = l, j = r;
    int mid = a[(l+r)/2];//将当前序列在中间位置的数定义为分隔数
    while(i <= j)//注意这里不能少了等号
    {
        while(a[i] < mid) 
            i++;  //在左半部分寻找大于等于中间数的数
        while(a[j] > mid)
            j--;    //在右半部分寻找小于等于中间数的数
        if(i <= j) //若找到一组与排序目标不一致的数对
        {                               
            swap(a[i], a[j]);//则交换它们 
            i++;
            j--;
        }
    }
    quickSort(l, j);//递归搜索左右区间
    quickSort(i, r);
}
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    quickSort(1, n);
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}

9. 堆排序

  • 基本概念:堆是一种基于二叉树的数据结构,借助堆可以在 O ( l o g n ) O(logn) O(logn)时间复杂度下获取n个元素中的最值。
  • 复杂度:
    时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    额外空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:不稳定

实现堆排序有两种方法
方法1:似于选择排序,构造小顶堆,每次取出所有元素中的最小值,并删除堆顶。循环n次即可获得有序序列。

#include <bits/stdc++.h>
using namespace std;
#define N 100005
int heap[N], n;
//b是否比a高级。堆顶是最高级的元素 
bool isPrior(int a, int b)
{
    return a > b;//a > b:小顶堆, a < b:大顶堆 
} 
//第i结点上移 
void shiftUp(int i)
{
    if(i == 1)//p是根结点 
        return;
    if(isPrior(heap[i/2], heap[i]))
    {
        swap(heap[i], heap[i/2]);
        shiftUp(i/2);
    }
}
//第i结点下沉 
void shiftDown(int i)
{
    if(i > n/2)//如果i是叶子结点
        return; 
    int sel;//选择交换的结点位置 
    if(2*i+1 <= n && isPrior(heap[2*i],heap[2*i+1]))//有右孩子且右孩子值更大 
        sel = 2*i + 1;
    else//没有右孩子或左孩子值更大 
        sel = 2*i; 
    if(isPrior(heap[i],heap[sel]))
    {
        swap(heap[sel], heap[i]);
        shiftDown(sel); 
    }
}
//建堆 
void buildHeap()
{
    for(int i = n/2; i >= 1; --i)
        shiftDown(i); 
}
//插入元素 
void insert(int val)
{
    heap[++n] = val;
    shiftUp(n); 
}
//删除元素 
void del()
{
    swap(heap[1], heap[n]);
    n--;
    shiftDown(1); 
}
//获取堆顶 
int top()
{
    return heap[1];
}
int main()
{
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> heap[i];
    buildHeap();
    while(n > 0)//堆不空 
    {
        cout << top() << ' ';
        del();
    }
    return 0;
}

方法2:构造大顶堆

  1. 首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
  2. 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
  3. 将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
#include <bits/stdc++.h>
using namespace std;
#define N 100005
int heap[N], n;
//b是否比a高级。堆顶是最高级的元素 
bool isPrior(int a, int b)
{
    return a < b;//a > b:小顶堆, a < b:大顶堆 
} 
//第i结点上移 
void shiftUp(int i)
{
    if(i == 1)//p是根结点 
        return;
    if(isPrior(heap[i/2], heap[i]))
    {
        swap(heap[i], heap[i/2]);
        shiftUp(i/2);
    }
}
//第i结点下沉 
void shiftDown(int i)
{
    if(i > n/2)//如果i是叶子结点
        return; 
    int sel;//选择交换的结点位置 
    if(2*i+1 <= n && isPrior(heap[2*i],heap[2*i+1]))//有右孩子且右孩子值更大 
        sel = 2*i + 1;
    else//没有右孩子或左孩子值更大 
        sel = 2*i; 
    if(isPrior(heap[i],heap[sel]))
    {
        swap(heap[sel], heap[i]);
        shiftDown(sel); 
    }
}
//建堆 
void buildHeap()
{
    for(int i = n/2; i >= 1; --i)
        shiftDown(i); 
}
//插入元素 
void insert(int val)
{
    heap[++n] = val;
    shiftUp(n); 
}
//删除元素 
void del()
{
    swap(heap[1], heap[n]);
    n--;
    shiftDown(1); 
}
//获取堆顶 
int top()
{
    return heap[1];
}
//堆排序 
void heapSort()
{
    int tot = n;
    for(int i = 1; i <= tot - 1; ++i)
        del();
    for(int i = 1; i <= tot; ++i)
        cout << heap[i] << ' ';
}
int main()
{
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> heap[i];
    buildHeap();
    heapSort();
    return 0;
}

10. 二叉树排序

  • 基本概念
    二叉排序树或者是一颗空树,或者是具有如下性质的二叉树:
  1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 它的 左、右子树又分别为二叉排序树 。

二叉排序树插入操作:如果要插入的值比当前结点的值大,则看右孩子。若要查找的值比当前结点的值小,则看左孩子。如果要查找的值与当前结点的值相等,则该数值出现的次数加1。如果找到空结点,那么就将其插入到这个位置。
每次插入操作复杂度: O ( l o g n ) O(logn) O(logn)

输入n个数字,将每个数字插入到二叉排序树中,对二叉排序树做中序遍历,即可得到有序序列。

  • 复杂度:
    时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    额外空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:稳定
  • 代码
#include <bits/stdc++.h>
using namespace std;
#define N 100005
struct Node
{
    int val, ct, left, right;//val:值 ct:个数 
};
Node node[N];
int p, root;
void ins(int r, int val)
{
    if(node[r].val == val)
        node[r].ct++;
    else if(node[r].val < val)
    {
        if(node[r].right == 0)
        {
            node[++p].val = val;
            node[p].ct = 1;
            node[r].right = p;
        }
        else
            ins(node[r].right, val);
    }
    else if(node[r].val > val)
    {
        if(node[r].left == 0)
        {
            node[++p].val = val;
            node[p].ct = 1;
            node[r].left = p;
        }
        else
            ins(node[r].left, val);
    }
} 
void midOrder(int r)//中序遍历 
{
    if(r == 0)
        return;
    midOrder(node[r].left);
    for(int i = 1; i <= node[r].ct; ++i) 
        cout << node[r].val << ' ';
    midOrder(node[r].right);
}
int main()
{
    int n, v;
    cin >> n;
    cin >> v;
    node[++p].val = v;
    node[p].ct = 1;
    root = p;//树根 
    for(int i = 2; i <= n; ++i)
    {
        cin >> v;
        ins(root, v);
    }
    midOrder(root);
    return 0;
}

11. 排序总结

在这里插入图片描述

三、STL中的排序函数

1. sort函数

内部原理是快速排序,不稳定,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
sort(起始元素指针/迭代器,最后一个元素的指针/迭代器的后一个位置, 比较函数);
对数组a下标1到n进行排序
sort(a+1,a+1+n, cmp);
对STL容器a进行排序
sort(a.begin(), a.end(), cmp);
若不写比较函数cmp,默认为升序排序。如果对结构体变量进行排序,也可以通过重载小于号运算符来指定排序规则。
比较函数的写法:传入两个数组中元素类型的量,返回传入第一个参数排在前面的条件

bool cmp(int a, int b)
{
    return a < b;//a < b:升序 a > b:降序
}

bool cmp(const int &a, const int &b)
{
    return a < b;//a < b:升序 a > b:降序
}

解决:P1177 【模板】快速排序

  • 用数组保存数据
#include <bits/stdc++.h>
using namespace std;
#define N 100005
bool cmp(const int &a, const int &b)
{
    return a < b;
}
int main()
{
    int n, a[N];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    sort(a+1, a+1+n, cmp);//或sort(a+1, a+1+n) 
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}
  • 用vector保存数据
#include <bits/stdc++.h>
using namespace std;
#define N 100005
bool cmp(const int &a, const int &b)
{
    return a < b;
}
int main()
{
    vector<int> vec;
    int n, a;
    cin >> n;
    for(int i = 1; i <= n; ++i)
    {
        cin >> a;
        vec.push_back(a);
    }
    sort(vec.begin(), vec.end(), cmp); 
    for(int i = 0; i < vec.size(); ++i)
        cout << vec[i] << ' ';
    return 0;
}
2. stable_sort函数

内部原理是归并排序,稳定,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
stable_sort(起始元素指针/迭代器,最后一个元素的指针/迭代器的后一个位置, 比较函数);
对数组a下标1到n进行排序
stable_sort(a+1,a+1+n, cmp);
对STL容器a进行排序
stable_sort(a.begin(), a.end(), cmp);
若不写比较函数cmp,默认为升序排序。
比较函数的写法:传入两个数组中元素类型的量,返回传入第一个参数排在前面的条件

bool cmp(int a, int b)
{
    return a < b;//a < b:升序 a > b:降序
}

bool cmp(const int &a, const int &b)
{
    return a < b;//a < b:升序 a > b:降序
}

解决:P1177 【模板】快速排序

  • 用数组保存数据
#include <bits/stdc++.h>
using namespace std;
#define N 100005
bool cmp(const int &a, const int &b)
{
    return a < b;
}
int main()
{
    int n, a[N];
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];
    stable_sort(a+1, a+1+n, cmp);//或sort(a+1, a+1+n) 
    for(int i = 1; i <= n; ++i)
        cout << a[i] << ' ';
    return 0;
}
  • 用vector保存数据
#include <bits/stdc++.h>
using namespace std;
#define N 100005
bool cmp(const int &a, const int &b)
{
    return a < b;
}
int main()
{
    vector<int> vec;
    int n, a;
    cin >> n;
    for(int i = 1; i <= n; ++i)
    {
        cin >> a;
        vec.push_back(a);
    }
    stable_sort(vec.begin(), vec.end(), cmp); 
    for(int i = 0; i < vec.size(); ++i)
        cout << vec[i] << ' ';
    return 0;
}
3. 使用优先队列来实现堆排序

stl中优先队列类:priority_queue,内部原理是堆。
priority_queue<元素类型, 容器, 比较规则> pq;
例:保存int类型元素的大顶堆
priority_queue<int, vector<int>, less<int> > pq;
例:保存int类型元素的小顶堆
priority_queue<int, vector<int>, greater<int> > pq;
其中less<int>greater<int>是仿函数,其形式大概为:

template <class T>
bool Less(T a, T b)
{
	return a < b;
}
template <class T>
bool Greater(T a, T b)
{
	return a > b;
}

优先队列仿函数返回的是b优先的条件,所以传less<int>是大顶堆,传greater<int>是小顶堆。
借助优先队列,可以实现本文堆排序中第一种方式的堆排序

#include <bits/stdc++.h>
using namespace std;
int n;
priority_queue<int, vector<int>, greater<int> > pq;//greater仿函数为:a>b,小顶堆 做升序排序 
int main()
{
    int a;
    cin >> n; 
    for(int i = 1; i <= n; ++i)
    {
        cin >> a;
        pq.push(a);
    }
    for(int i = 1; i <= n; ++i)
    {
        cout << pq.top() << ' ';
        pq.pop();
    }
    return 0;
}
4. 使用multiset实现二叉树排序

multiset内部原理是红黑树,红黑树是一种二叉排序树。
multiset<元素类型, 比较规则> pq;

  • 如想让更小的数值排在前面,也就是做升序排序,需要使用less仿函数。
    multiset<int, less<int> > pq;
  • 如想让更大的数值排在前面,也就是做降序排序,需要使用greater仿函数。
    multiset<int, greater<int> > pq;
#include <bits/stdc++.h>
using namespace std;
int main()
{
	multiset<int> ms;
	int n, a;
	cin >> n;
	for(int i = 1; i <= n; ++i)
	{
		cin >> a;
		ms.insert(a);
	}
	for(int v : ms)
		cout << v << ' ';
	/*或使用迭代器遍历
	for(multiset<int>::iterator it = ms.begin(); it != ms.end(); ++it)
	    cout << *it << ' ';*/
	return 0;
}
  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值