【排序】11种排序算法笔记 & 编程题

目录

相关概念

1 冒泡排序 (Bubble Sort)

代码实现

2 简单选择排序(Simple Selection Sort)

代码实现

3 直接插入排序(Straight Insertion Sort)

代码实现

4 希尔排序(Shell Sort)/缩小增量排序(Diminishing Increment Sort)

代码实现

5 归并排序(Merging Sort )

6 快速排序(Quick Sort)

代码实现

7 堆排序(Heap Sort )

8 计数排序(Counting Sort )

编程题

1. 选举学生会

9 桶排序(Bucket Sort)/箱排序

10 基数排序(Radix Sort)

11 TimSort

各算法比较

时空复杂度

简单性

其他

“桶”


相关概念

  • 稳定排序 & 非~
  • 原地排序 & 非~
  • 内排序 & 外排序
  • 时间复杂度 & 空间复杂度
  • 插入排序类:直接插入、希尔
  • 选择排序类:简单选择、堆
  • 交换排序类:冒泡、快速

图片来自菜鸟教程

1 冒泡排序 (Bubble Sort)

重复扫描元素列,依次比较相邻元素,顺序错误则交换,直到无需交换(排序完成)——把小(大)的元素往前(后)调

时间复杂度:

  • 最好:O(n)
  • 最坏 & 平均:O(n^2)

空间复杂度:

  • 最坏:O(1)

稳定

代码实现

优化:增加标记变量flag,避免已经有序情况下的无意义循环判断

// C++
template<typename T>
void bubble_sort(T arr[], int len) {
    int i, j;  T 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;
        }
}

2 简单选择排序(Simple Selection Sort)

将元素列分为未排序序列、已排序序列:

在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

时间复杂度:

  • O(n^2)

空间复杂度:

  • 最坏:O(1)

不稳定

代码实现

通过 n-i 次关键字间的比较,从 n-i+1 个记录中选出关键字最小的记录,并和第 i(1≤i≤n)个记录交换

template<typename T> 
void selection_sort(std::vector<T>& arr) {
	for (int i = 0; i < arr.size() - 1; i++) {
		int min = i;
		for (int j = i + 1; j < arr.size(); j++) {
			if (arr[j] < arr[min])
				min = j;
		}
        if (i != min)
            std::swap(arr[i], arr[min]);
        }
}

3 直接插入排序(Straight Insertion Sort)

将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表

抓扑克牌:手上已经抓到的扑克牌是按顺序排好的,待摸的是洗好的,即未排序的。

时间复杂度:

  • 最好:O(n)
  • 最坏 & 平均:O(n^2)

空间复杂度:

  • 最坏:O(1)

稳定

代码实现

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;
	}
}

4 希尔排序(Shell Sort)/缩小增量排序(Diminishing Increment Sort)

跳跃分割,将整个待排序元素序列分割成为若干子序列,分别进行直接插入排序;当整个序列都基本有序时,再对全体记录进行依次直接插入排序。

时间复杂度:

  • 最好:O(nlogn)
  • 最坏 & 平均:O(n(logn)^2)

空间复杂度:

  • 最坏:O(1)

不稳定

代码实现

template <typename T>
void insert_sort(T st, T ed, int delta) {
    for(T i = st + delta; i < ed; i += delta) {
        for(T j = i; j > st; j -= delta)
            if(*j < *(j - delta)) std::swap(*j, *(j - delta));
            else break;
    }
}
 
template <typename T>
void shell_sort(T st, T ed) {
    for(int delta = ed - st; delta; delta /= 2)
        for(int i = 0; i < delta; i++)
            insert_sort(st + i, ed, delta);
}

5 归并排序(Merging Sort )

分治思想:先使每个子序列有序,再使子序列段间有序。

or 归并思想:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 |n/2| 个长度为2或1的有序子序列;再两两归并……直至得到一个长度为n的有序序列为止(2路归并排序)

两种实现方法:

  • 自上而下递归
  • 自下而上迭代(所有递归都可以用迭代重写)

时间复杂度:

  • O(nlogn)

空间复杂度:

  • 最坏:O(n+logn)

稳定

6 快速排序(Quick Sort)

在冒泡排序基础上的递归分治法

设定分界值(基准),按相对于分界值大小将元素列分为两部分;将这两部分分别重复上述操作。

时间复杂度:

  • 最好 & 平均:O(nlogn)
  • 最坏:O(n^2)

空间复杂度:

  • 最坏:O(logn)

不稳定

代码实现

递归

优化:《大话数据结构》P645

  1. 基准选取:三数取中、九数取中法
  2. 优化不必要交换
  3. 优化小数组时的排序方案
  4. 优化递归
// 严蔚敏《数据结构》
using namespace std;

void Qsort(int arr[], int low, int high){
    if (high <= low) return;
    int i = low;
    int j = high;
    int key = arr[low];
    while (true)
    {
        while (arr[i] <= key)
        {
            i++;
            if (i == high){
                break;
            }
        }
        while (arr[j] >= key)
        {
            j--;
            if (j == low){
                break;
            }
        }
        if (i >= j) break;
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    arr[low] = arr[j];
    arr[j] = key;
    Qsort(arr, low, j - 1);
    Qsort(arr, j + 1, high);
}

7 堆排序(Heap Sort )

将待排序的序列构造成一个堆,此时整个序列最值是堆顶的根节点,移走(将其与堆数组的末尾元素交换)后将剩余 n-1 个序列重新构造成堆……反复执行以得到有序序列

:完全二叉树、某个结点的值总是不大于或不小于其父结点的值。

堆操作:

  • Heapify:将堆的末端子节点作调整,使得子节点永远小于(大于)父节点
  • 堆排序:
    • 升序:大顶堆
    • 降序:小顶堆

时间复杂度:

  • O(nlogn)

空间复杂度:

  • 最坏:O(1)

不稳定

不适合待排序序列个数少的情况

#include <algorithm>
using namespace std;

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) 
{
    // 初始化,i从最后一个父节点开始调整
    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);
    }
}

8 计数排序(Counting Sort )

比较型排序,要求输入的数据必须是有确定范围的整数。在此前提条件下,速度快于任何比较排序算法。

对于待排序元素列中的每一个元素 x,确定该序列中值小于 x 的元素的个数——并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定。对于相同元素需要设定统计数组。

要给一堆弹珠从小到大排列,拿一个模具,模具上有从小到大的孔洞,我弹珠放进孔洞中,最后只要从模具中从小到大的孔洞中倒出弹珠即可(思想来源于:航海家(小海)

算法步骤

1. 找出待排序元素列 arr 中最大值max、最小值min,创建与 len(arr) 长度的结果数组resultArr、(max - min + 1)长度的数组Arr、统计数组countArr 

  •  数组 Arr 用来存放每个值的个数,countArr 存放最终位置;
  • 节省内存,索引变为arr[] - min;
  • 统计数组用来解决元素相同问题

2. 遍历元素列,统计值为 i (min~max)的元素个数,依次存入Arr[i - min]

  • 数组索引转换
  • 个数作为值

3. 遍历 Arr[],每个索引之前的计数依次累加,存入 countArr[] 相同索引位置

  • 存放最终排序完成时元素的位置,解决元素相同问题

4. 反向遍历 arr[],【countArr[arr - min] - 1】即为arr[] 在排序完成的 resultArr 中的位置,同时将countArr[] 减1

时间复杂度:

  • O(n+k)

        k = max - min + 1

空间复杂度:

  • O(k)

稳定

编程题

1. 选举学生会

学校正在选举学生会成员,有n (n\leq 999) 名候选人,每名候选人编号分别从 1 到n,现在收集到了 m(m\leq 2000000)张选票,每张选票都写了一个候选人编号。现在想把这些堆积如山的选票按照投票数字从小到大排序。

  • 输入格式

输入 nm以及m个选票上的数字。

  • 输出格式

排序后的选票编号

// 题解sort更方便,主要为了理解计数排序
#include<bits/stdc++.h>
using namespace std;
int a,n, m, b[1000];
int main() {
	cin>>n>>m;
	for(int i=0;i<m;i++)cin>>a,++b[a]; //记录票出现的次数
	for(int i=0;i<1000;i++)while(b[i]--)cout<<i<<" "; //根据票出现的次数输出
	return 0;
}

9 桶排序(Bucket Sort)/箱排序

比较型排序,将 [max,min] 划分为大小相等的子区间(桶/箱)。

利用函数映射关系,将待排序元素列分到有限数量的桶里,每个桶内再排序(其他排序算法/递归)。

时间复杂度:

k为桶数

  • 最好 & 平均:O(n+k)
  • 最坏:O(n^2)

空间复杂度:

  • O(n+k)

稳定

10 基数排序(Radix Sort)

比较型排序

  • MSD,最高位优先
  • LSD,最低位优先

时间复杂度:

  • O(n*k)

空间复杂度:

  • 最坏:O(n+k)

稳定

int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
    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 = newint[n];
    int *count = newint[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;
}

11 TimSort

把输入中有序的序列分区,逆序翻转后分区,使其成为基本单元(称为“run”)

百度百科

TimSort算法(JDK)

  • 归并:将两个 run 合并成一个 run。归并的结果保存到 run_stack 上
  • 插入排序

时间复杂度:

  • 最好:O(n)
  • 最坏 & 平均:O(nlogn)

空间复杂度:

  • 最坏:O(n)

稳定

各算法比较

时空复杂度

(图片来源于菜鸟教程。In-place:占用常数内存,不占用额外内存;Out-place:占用额外内存)

  • 平方阶 O(n^2):冒泡、选择、插入排序
  • 线性对数阶:快速、堆、归并排序
  • 线性阶O(n):基数、桶排序

简单性

  • 简单算法:冒泡、简单选择、直接插入
  • 改进算法:希尔、堆、归并、快速

从平均情况来看,最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法

整体时间复杂度,堆/归并 > 快速

从最好情况看,冒泡和直接插入排序要更胜一筹(待排序序列总是基本有序时)

非常在乎排序稳定性:归并排序

待排序的个数n越小,采用简单排序方法越合适。反之,n越大,采用改进排序方法越合适

对于数据量不是很大而记录的关键字信息量较大的排序要求,简单排序算法是占优的

其他

“桶”

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值