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

来源:互联网 发布:php跟java的区别 商城 编辑:程序博客网 时间:2024/06/07 15:17

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


插入排序

复杂度O(n2)
先对前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)
将第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)
从后往前比较相邻两位,不断将较小值交换到前面。
这里写图片描述

# 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),与增量序列的选取有关
一般采用插入排序,算是插入排序的升级版,先将数组分成几个子数组,对相隔较远的元素进行插入排序,再对相隔较近的元素进行插入排序,通过广泛的研究,这个间隔为代码中的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(nlgn)
首先介绍大根堆:
    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(nlgn)
将数组分为两个子数组,设置一个基准值,使得左边数组小于等于该基准值,右边数组大于等于该基准值,对两个子数组递归此过程实现升序排序。
课本中快排之前进行预处理将最大值放到数组末尾,原因就是课本的代码首先将基准值换到数组的首位,最后再换到正确的位置,那么第一次快排时如果最大值刚好被换到首位,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(nlgn)
将一个数组拆成两个子数组,分别排序后再合并在一起,这是一个递归的过程。因为两个子数组已经有序,合并操作可以比较快的完成。
这里写图片描述

# 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不是特别大时效率很高。算法用到两个数组counttmpcount[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;}