数据结构期末复习:排序算法

图片来源toptal
本文用几种排序算法演示对一个大小为20的数组排序,主要参考课本《C++数据结构与算法(第四版)》,数组下标默认从0开始。


插入排序

时间复杂度 O(n2)
空间复杂度 O(1)
先对前 i 个数排好序,对于第i+1个数,不断将前面的数后移 1 位,直到找到合适的位置插进去。
这里写图片描述

# include <iostream>
using namespace std;
void insertion_sort(int a[], int n)
{
    for(int i=1; i<n; ++i)
    {
        int j, tmp = a[i];
        for(j=i; j>0 && a[j-1]>tmp; --j)
            a[j] = a[j-1];
        a[j] = tmp;
    }
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    insertion_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

选择排序

时间复杂度O(n2)
空间复杂度 O(1)
将第 i 个数分别和第[i+1,n1]个数比较,将最小的数交换到 i 处。
这里写图片描述

# include <iostream>
using namespace std;
void selection_sort(int a[], int n)
{
    for(int i=0; i<n; ++i)
    {
        int tmp = i;
        for(int j=i+1; j<n; ++j)
            if(a[j] < a[tmp])
                tmp = j;
        swap(a[tmp], a[i]);
    }
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    selection_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

冒泡排序

时间复杂度O(n2)
空间复杂度 O(1)
从后往前比较相邻两位,不断将较小值交换到前面。
这里写图片描述

# include <iostream>
using namespace std;
void bubble_sort(int a[], int n)
{
    for(int i=0; i<n; ++i)
        for(int j=n-1; j>i; --j)
            if(a[j-1] > a[j])
                swap(a[j-1], a[j]);
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    bubble_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

希尔排序

时间复杂度下界 O(nlog2n) ,与增量序列的选取有关
空间复杂度 O(1)
一般采用插入排序,算是插入排序的升级版,先将数组分成几个子数组,对相隔较远的元素进行插入排序,再对相隔较近的元素进行插入排序,通过广泛的研究,这个间隔为代码中的 increments 数组比较合适。
这里写图片描述

# include <iostream>
using namespace std;
int increments[23]={1};
void shell_sort(int a[], int n)
{
    for(int i=1; i<n; ++i) increments[i] = 3*increments[i-1] + 1;//设置增量值
    for(int i=n-1; i>=0; --i)
    {
        int h = increments[i];//当前增量值
        for(int j=h; j<2*h; ++j)//h到2h的位置都要跑一遍
        {
            for(int k=j; k<n; k+=h)
            {
                int tmp = a[k];
                int pos = k;
                while(pos-h >= 0 && a[pos-h] > tmp)//类似插入排序
                {
                    a[pos] = a[pos-h];
                    pos -= h;
                }
                a[pos] = tmp;
            }
        }
    }
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    shell_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

堆排序

时间复杂度 O(nlogn)
空间复杂度 O(1)
首先介绍大根堆:
    1. 每一个节点的值都大于等于它儿子的值。
    2. 该二叉树完全平衡,即最后一层叶子结点位于最左侧位置。
于是堆顶元素一定是最大的,利用此性质每次将堆顶元素交换到数组的最后(类似选择排序的相反版本),然后恢复堆,重复这一过程实现对数组的升序排序。
用数组实现堆参考课本 P215
这里写图片描述

# include <iostream>
using namespace std;
void movedown(int a[], int left, int right)//将根元素沿树向下移动
{
    int big_son = 2*left+1;
    while(big_son <= right)
    {
        if(big_son<right && a[big_son] < a[big_son+1]) ++big_son;//找出较大的那个儿子
        if(a[left] < a[big_son])//如果爸爸比儿子小
        {
            swap(a[left], a[big_son]);//就执行交换
            left = big_son;//然后继续更新儿子
            big_son = left*2+1;
        }
        else break;//否则可以退出了
    }
}
void heap_sort(int a[], int n)
{
    for(int i=n/2-1; i>=0; --i)//建堆
        movedown(a, i, n-1);
    for(int i=n-1; i>=0; --i)
    {
        swap(a[0], a[i]);//将最值放到末尾
        movedown(a, 0, i-1);//恢复堆,即重新将最值放到堆顶部
    }
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    heap_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

快速排序

时间复杂度 O(nlogn)
空间复杂度 O(log2n)
将数组分为两个子数组,设置一个基准值,使得左边数组小于等于该基准值,右边数组大于等于该基准值,对两个子数组递归此过程实现升序排序。
课本中快排之前进行预处理将最大值放到数组末尾,原因就是课本的代码首先将基准值换到数组的首位,最后再换到正确的位置,那么第一次快排时如果最大值刚好被换到首位,low指针就会永无止境地加下去导致数组越界,因此该预处理是必要的。
这里写图片描述

# include <iostream>
using namespace std;
void qucik_sort(int a[], int left, int right)
{
    swap(a[left], a[(left+right)/2]);//先将基准元素放到前面,防止它来回移动(swap(a[low],a[up])),结束再放回正确的位置。
    int low = left+1, up = right, bound = a[left];
    while(low <= up)
    {
        while(a[low] < bound) ++low;
        while(a[up] > bound) --up;
        if(low < up)
        {
            swap(a[low], a[up]);//将左边>=基准值和右边<=基准值的数字交换
            ++low;--up;
        }
        else break;
    }
    swap(a[up], a[left]);//将基准值放回去正确位置,显然可以是up所在位置
    if(left < up-1) qucik_sort(a, left, up-1);//递归对左子数组排序,up为分界点,a[up]不用排
    if(right > up+1) qucik_sort(a, up+1, right);//递归对右子数组排序,up为分界点
}
void quick_sort(int a[], int n)
{
    if(n < 2) return;
    int imax = 0;
    for(int i=1; i<n; ++i)//预处理,将最大的元素调到数组最后,否则第一次调用快排可能会使low指针越界
        if(a[i] > a[imax])
            imax = i;
    swap(a[n-1], a[imax]);
    qucik_sort(a, 0, n-2);
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    quick_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

归并排序

时间复杂度 O(nlogn)
空间复杂度 O(n)
将一个数组拆成两个子数组,分别排序后再合并在一起,这是一个递归的过程。因为两个子数组已经有序,合并操作可以比较快的完成。
这里写图片描述

# include <iostream>
using namespace std;
int temp[20];//临时数组
void merge(int a[], int left, int right)
{
    int cnt = 0, mid = (left+right)/2;
    int l=left, r=mid+1;//l和r分别是左子数组的开头和右子数组的开头
    while(l<=mid && r<=right)//如果两个子数组均有元素
    {
        if(a[l] < a[r]) temp[cnt++] = a[l++];//按升序放到temp数组
        else temp[cnt++] = a[r++];//按升序放到temp数组
    }
    while(l <= mid) temp[cnt++] = a[l++];//将左子数组剩余元素放到temp数组,当然本循环不会和下面的循环同时出现
    while(r <= right) temp[cnt++] = a[r++];//将右子数组剩余元素放到temp数组,当然本循环不会和上面的循环同时出现
    cnt = 0;
    for(int i=left; i<=right; ++i)//更新整个数组
        a[i] = temp[cnt++];
}
void merge_sort(int a[], int left, int right)
{
    if(left >= right) return;
    int mid = (left+right)/2;
    merge_sort(a, left, mid);//对左子数组排序
    merge_sort(a, mid+1, right);//对右子树组排序
    merge(a, left, right);//合并两个子数组,因为他们已经别排序了,合并操作只需O(right-left+1)复杂度
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    merge_sort(a, 0, n-1);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

基数排序

复杂度 O(nlog(r)m) r 为所采取的基数,m为堆数
比较神奇的算法,平常我们判断两个数的大小是先从最高位开始判断的,最高位相同就按剩下的低位数判断。假如我们只按高位排序,有没有办法使得低位也自动排好序呢?基数排序就是这个原理,它从最低位开始,对每一个独立的数位进行排序,具体就是放到一个二维队列里面(维度一般为10,因为单个数位只有0到9),队列是线性结构,按顺序放进去必然能保持数据的有序性,所以同一个队列里面的数值永远都是“有序”的,根据这个性质就能实现升序排序了。

# include <iostream>
# include <queue>
using namespace std;
void radix_sort(int a[], int n)
{
    int digits = 2, radix = 10;//digits是最长那个数字的长度,radix是单个数位的范围。
    queue<int>q[radix];
    for(int i=0, fac=1; i<digits; ++i,fac*=10)
    {
        for(int j=0; j<n; ++j)
            q[(a[j]/fac)%10].push(a[j]);//按当前数位的大小进队
        for(int j=0,cnt=0; j<radix; ++j)//从小到大更新原数组
        {
            while(!q[j].empty())
            {
                a[cnt++] = q[j].front();
                q[j].pop();
            }
        }
    }
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    radix_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}

计数排序

复杂度 O(n+k) k 是整数范围
比较巧妙的思路,当k不是特别大时效率很高。算法用到两个数组 count tmp count[i] 记录i这个数出现了几次,然后 count 数组最后要求一次前缀和,那么此时 count[i] 就表示i这个数在n个数中排第几位了,接下来就好办了, tmp 数组是临时存放结果的数组。

# include <iostream>
using namespace std;
int count[100], tmp[100];
void counting_sort(int a[], int n)
{
    int biggest = 0;//最大的数
    for(int i=0; i<n; ++i) biggest = max(biggest, a[i]);
    for(int i=0; i<n; ++i) ++count[a[i]];
    for(int i=1; i<=biggest; ++i) count[i] += count[i-1];//前缀和
    for(int i=0; i<n; ++i)
    {
        tmp[count[a[i]]-1] = a[i];
        --count[a[i]];
    }
    for(int i=0; i<n; ++i)
        a[i] = tmp[i];
}
int main()
{
    int a[20] = {42,76,17,1,45,23,98,22,77,83,58,41,64,74,4,96,22,84,45,40};
    int n = 20;
    counting_sort(a, n);
    for(int i=0; i<n; ++i)
        cout << a[i] << " ";
    return 0;
}
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值