算法学习笔记17-经典排序算法

来源:互联网 发布:比尔盖茨编程水平 编辑:程序博客网 时间:2024/06/05 16:58

总结

这里写图片描述

基于比较的排序

1.冒泡排序
冒泡就是相邻两个元素比较,把较大的数放到后面,每一轮比较之后,最尾端的数一定是最大的。
冒泡排序是稳定的。

#python实现def bubble(a):    for i in range(len(a)-1):        for j in range(len(a)-i-1):            if a[j]>a[j+1]:                a[j],a[j+1]=a[j+1],a[j]#C/C++实现void bubble(int*a,int n){    for(int i=0;i<n-1;i++)    {        for(int j=0;j<n-i-1;j++)        {            if(a[j]>a[j+1])            {                int t=a[j];                a[j]=a[j+1];                a[j+1]=t;            }        }    }}

2.直接选择排序
直接选择排序是每一轮选择最小的数放到最前端,每一轮比较之后最前端的数一定是最小的。
直接选择排序是不稳定的。

#python实现def select(a):    for i in range(len(a)-1):        for j in range(i+1,len(a)):            if a[i]>a[j]:                a[i],a[j]=a[j],a[i]#C/C++实现void select(int*a,int n){    for(int i=0;i<n-1;i++)    {        for(int j=i+1;j<n;j++)        {            if(a[i]>a[j])            {                int t=a[i];                a[i]=a[j];                a[j]=t;            }        }    }}

3.快速排序
快速排序是对冒泡排序的一种改进。是稳定的排序。
快速排序用到了递归的思想,先选择第一个数为关键字key,然后用两个索引index(i,j),i指向开头,j指向尾端,先后往前遍历,找到比key小的数,就交换key和a[j]的位置,再从前往后遍历,找到比key大的数就交换key和a[i]的位置。重复上述过程,直到i>=j就结束一个循环。
一轮循环之后,数组被分成了两端,key前面的数都比key小,后面的数都比key大。
然后分别对两段数组做一次quick(),也就是递归,直到不能再分段。

#python实现def quick(a,l,h):    if l>=h:        return    i=l    j=h    k=a[i]    while i<j:        while i<j and a[j]>=k:            j-=1        a[i]=a[j]#从后往前遍历,找到比key小的数就交换key和a[j]的位置        while i<j and a[i]<=k:            i+=1        a[j]=a[i]#从前往后遍历,找到比key大的数就交换key和a[i]的位置    a[i]=k    quick(a,l,i-1)    quick(a,i+1,h)#C/C++实现void quick(int*a,int l,int h){    if(l>=h)        return;    int ft=l,lt=h;    int key=a[ft];    while(ft<lt)    {        while(ft<lt&&a[lt]>=key)            lt--;        a[ft]=a[lt];        while(ft<lt&&a[ft]<=key)            ft++;        a[lt]=a[ft];    }    a[ft]=key;    quick(a,l,ft-1);    quick(a,ft+1,h);}

4.直接插入排序
插入排序是将后面的数插入到前面已经排好序的序列中的适当位置。是稳定的排序。

#python实现def insert(a):    for i in range(1,len(a)):        if a[i-1]>a[i]:            t=a[i]            j=i            while j>0 and a[j-1]>t:                a[j]=a[j-1]                j-=1            a[j]=t#C/C++实现void insert(int*a){    for(int i=1;i<5;i++)    {        if(a[i-1]>a[i])        {            int t=a[i];            int j=i;            while(j>0&&a[j-1]>t)            {                a[j]=a[j-1];                j--;            }            a[j]=t;        }    }}

5.希尔排序
希尔排序(Shell`s sort)是对直接插入排序的一种改进,时间复杂度上得到了优化。它的思想是:先将整个序列分割成若干个子序列(子序列可以有重叠),比如将(R1~R10)分割成五个子序列(R1~R6,R2~R7,R3~R8,R4~R9,R5~R10),分别对各个子序列进行直接插入排序,

#python实现def shell(a):    if len(a)<=1:        return    d=len(a)/2    while d>=1:        for i in range(d+1):            for j in range(i,len(a)-d,d):                for k in range(j,len(a),d):                    if a[j]>a[k]:                        a[j],a[k]=a[k],a[j]        d/=2#C/C++实现void shell(int*a,int n){    if(n<=1||a==NULL)        return;    for(int d=n/2;d>=1;d/=2)    {        for(int i=0;i<=d;i++)        {            for(int j=i;j<n-d;j+=d)            {                for(int k=j;k<n;k+=d)                {                    if(a[j]>a[k])                    {                        int t=a[j];                        a[j]=a[k];                        a[k]=t;                    }                }            }        }    }}

6.归并排序
归并的意思是将两个或者两个以上的有序表合并成一个新的有序表。
2-路归并排序:
初始序列有n个记录,可看成是n个子序列,每个子序列长度为1,然后两两归并,得到n/2个长度为2的子序列。如此重复,直至得到一个长度为n的有序序列。

#python实现def mergesort(a,t,first,middle,last):    i=first    j=middle+1    k=first    while i!=middle+1 and j!=last+1:        if a[i]>a[j]:            t[k]=a[j]            k+=1            j+=1        else:            t[k]=a[i]            k+=1            i+=1    while i!=middle+1:        t[k]=a[i]        k+=1        i+=1    while j!=last+1:        t[k]=a[j]        k+=1        j+=1    for i in range(first,last+1):        a[i]=t[i]def merge(a,t,first,last):    if first<last:        middle=(first+last)//2        merge(a,t,first,middle)        merge(a,t,middle+1,last)        mergesort(a,t,first,middle,last)#C/C++实现void mergesort(int*a,int*t,int first,int middle,int last){    int i=first,j=middle+1,k=first;    while(i!=middle+1&&j!=last+1)    {        if(a[i]>a[j])            t[k++]=a[j++];        else            t[k++]=a[i++];    }    while(i!=middle+1)        t[k++]=a[i++];    while(j!=last+1)        t[k++]=a[j++];    for(i=first;i<=last;i++)        a[i]=t[i];}void merge(int*a,int*t,int first,int last){    int middle;    if(first<last)    {        middle=(first+last)/2;        merge(a,t,first,middle);        merge(a,t,middle+1,last);        mergesort(a,t,first,middle,last);    }}

7.桶排序(Bucket Sort)
桶排序有三个特点:

1,桶排序是稳定的。

2,大多数情况下,桶排序是常见排序里最快的一种,比快速排序还要快。
时间复杂度O(N+C),其中C=N*(logN-logM)。 因为它的实现并不是基于比较实现的,而是基于映射函数实现的。

3,桶排序是最耗空间的一种排序算法。空间复杂度O(N+M)

桶排序的基本思想是将一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。
桶排序的大概流程是这样的:
建立一堆buckets;
遍历原始数组,并将数据放入到各自的buckets当中;
对非空的buckets进行排序;
按照顺序遍历这些buckets并放回到原始数组中即可构成排序后的数组。
最简单的流程就像下面的例子:
桶排序
上述过程的代码实现如下:(这里的桶用链表来实现)

#include <iostream>#include <iomanip>using namespace std;#define NARRAY 8//数组容量8#define NBUCKET 5//桶数量5#define INTERVAL 10//一个桶的范围10struct Node {     int data;      struct Node *next; };void BucketSort(int arr[]);struct Node *InsertionSort(struct Node *list);void print(int arr[]);void printBuckets(struct Node *list);int getBucketIndex(int value);void BucketSort(int arr[]){       int i,j;    struct Node **buckets;      //为每个节点分配内存    buckets = (struct Node **)malloc(sizeof(struct Node*) * NBUCKET);     //初始化桶    for(i = 0; i < NBUCKET;++i) {          buckets[i] = NULL;    }    //将各个数放入桶    for(i = 0; i < NARRAY; ++i) {           struct Node *current;        int pos = getBucketIndex(arr[i]);        current = (struct Node *) malloc(sizeof(struct Node));        current->data = arr[i];         current->next = buckets[pos];          buckets[pos] = current;    }    //查看每个桶的内容    for(i = 0; i < NBUCKET; i++) {        cout << "Bucket[" << i << "] : ";            printBuckets(buckets[i]);        cout << endl;    }    //用插入排序为每个桶排序    for(i = 0; i < NBUCKET; ++i) {          buckets[i] = InsertionSort(buckets[i]);     }    //再次查看每个桶的内容    cout << "-------------" << endl;    cout << "Bucktets after sorted" << endl;    for(i = 0; i < NBUCKET; i++) {        cout << "Bucket[" << i << "] : ";            printBuckets(buckets[i]);        cout << endl;    }    //按顺序将每个桶的内容放回原数组    for(j =0, i = 0; i < NBUCKET; ++i) {            struct Node *node;        node = buckets[i];        while(node) {            arr[j++] = node->data;            node = node->next;        }    }    //释放内存    for(i = 0; i < NBUCKET;++i) {           struct Node *node;        node = buckets[i];        while(node) {            struct Node *tmp;            tmp = node;             node = node->next;             free(tmp);        }    }    free(buckets);     return;}//插入排序struct Node *InsertionSort(struct Node *list){       struct Node *k,*nodeList;    if(list == 0 || list->next == 0) {         return list;     }    nodeList = list;     k = list->next;     nodeList->next = 0;    while(k != 0) {         struct Node *ptr;        if(nodeList->data > k->data)  {             struct Node *tmp;            tmp = k;              k = k->next;             tmp->next = nodeList;            nodeList = tmp;             continue;        }        for(ptr = nodeList; ptr->next != 0; ptr = ptr->next) {            if(ptr->next->data > k->data) break;        }        if(ptr->next!=0){              struct Node *tmp;            tmp = k;              k = k->next;             tmp->next = ptr->next;            ptr->next = tmp;             continue;        }        else{            ptr->next = k;              k = k->next;              ptr->next->next = 0;             continue;        }    }    return nodeList;}int getBucketIndex(int value){    return value/INTERVAL;}void print(int ar[]){       int i;    for(i = 0; i < NARRAY; ++i) {         cout << setw(3) << ar[i];     }    cout << endl;}void printBuckets(struct Node *list){    struct Node *cur = list;    while(cur) {        cout << setw(3) << cur->data;        cur = cur->next;    }}int main(void){       int array[NARRAY] = {29,25,3,49,9,37,21,43};    cout << "初始化数组:" << endl;    print(array);    cout << "-------------" << endl;    BucketSort(array);     cout << "-------------" << endl;    cout << "排序后数组:"  << endl;    print(array);     return 0;}

8.基数排序(Radix Sort)
基数排序也不是基于比较的排序,它是一种多关键字排序,而且它是稳定的,有两种方法:
- 最高位优先MSD (Most Significant Digit First)
- 最低位优先LSD (Least Significant Digit First)
按照MSD排序,是先对最主位关键字排序,必须将序列逐层分割成若干个子序列,对各个子序列分别进行排序,有点类似桶排序。
按照LSD排序,是先对最次位关键字排序,不必分成子序列,每次都是对整个序列排序。
比如一副扑克牌进行排序,有两个关键字,分别是花色和数字:
按照MSD的做法,先按照花色分组,分成四堆,然后每堆按照面值由小到大排序,再按照花色按堆叠起来。
按照LSD的做法,先按照面值由小到大分成13堆(A,2,3~10,J,Q,k),然后由小到大叠起来,再按照花色分成四堆(顺序不变),最后再按花色由小到大叠起来。
比如下面的代码,用MSD方法,实现对一些二位的十进制数进行排序:

#include<iostream>#include<malloc.h>using namespace std;int getdigit(int x,int d)  {       int a[] = {1, 1, 10};     //因为待排数据最大数据也只是两位数,所以在此只需要到十位就满足    return ((x / a[d]) % 10);    //确定桶号}  void  PrintArr(int ar[],int n){    for(int i = 0; i < n; ++i)        cout<<ar[i]<<" ";    cout<<endl;}void msdradix_sort(int arr[],int begin,int end,int d)  {         const int radix = 10;       int count[radix], i, j;     //置空    for(i = 0; i < radix; ++i)       {        count[i] = 0;       }    //分配桶存储空间    int *bucket = (int *) malloc((end-begin+1) * sizeof(int));        //统计各桶需要装的元素的个数      for(i = begin;i <= end; ++i)       {        count[getdigit(arr[i], d)]++;       }    //求出桶的边界索引,count[i]值为第i个桶的右边界索引+1    for(i = 1; i < radix; ++i)       {        count[i] = count[i] + count[i-1];        }    //这里要从右向左扫描,保证排序稳定性     for(i = end;i >= begin; --i)              {            j = getdigit(arr[i], d);      //求出关键码的第d位的数字, 例如:576的第3位是5           bucket[count[j]-1] = arr[i];   //放入对应的桶中,count[j]-1是第j个桶的右边界索引           --count[j];                    //第j个桶放下一个元素的位置(右边界索引+1)       }       //注意:此时count[i]为第i个桶左边界        //从各个桶中收集数据      for(i = begin, j = 0;i <= end; ++i, ++j)      {        arr[i] = bucket[j];     }           //释放存储空间    free(bucket);       //对各桶中数据进行再排序    for(i = 0;i < radix; i++)      {           int p1 = begin + count[i];         //第i个桶的左边界           int p2 = begin + count[i+1]-1;     //第i个桶的右边界           if(p1 < p2 && d > 1)          {            msdradix_sort(arr, p1, p2, d-1);  //对第i个桶递归调用,进行基数排序,数位降 1            }    }  } void  main(){    int  ar[] = {12, 14, 54, 5, 6, 3, 9, 8, 47, 89};    int len = sizeof(ar)/sizeof(int);    cout<<"排序前数据如下:"<<endl;    PrintArr(ar, len);    msdradix_sort(ar, 0, len-1, 2);    cout<<"排序后结果如下:"<<endl;    PrintArr(ar, len);} /*排序前数据如下:12 14 54 5 6 3 9 8 47 89排序后结果如下:3 5 6 8 9 12 14 47 54 89 */

比如下面的代码,用LSD方法,实现对一些三位的十进制数进行排序:

#include<iostream>#include<malloc.h>using namespace std;#define   MAXSIZE   10000int getdigit(int x,int d)  {       int a[] = {1, 1, 10, 100};   //最大三位数,所以这里只要百位就满足了。    return (x/a[d]) % 10;  }  void  PrintArr(int ar[],int n){    for(int i = 0;i < n; ++i)    {        cout<<ar[i]<<" ";    }    cout<<endl;}  void lsdradix_sort(int arr[],int begin,int end,int d)  {        const int radix = 10;       int count[radix], i, j;     int *bucket = (int*)malloc((end-begin+1)*sizeof(int));  //所有桶的空间开辟       //按照分配标准依次进行排序过程    for(int k = 1; k <= d; ++k)      {          //置空        for(i = 0; i < radix; i++)          {            count[i] = 0;                }                       //统计各个桶中所盛数据个数        for(i = begin; i <= end; i++)         {           count[getdigit(arr[i], k)]++;        }        //count[i]表示第i个桶的右边界索引        for(i = 1; i < radix; i++)         {            count[i] = count[i] + count[i-1];        }        //把数据依次装入桶(注意装入时候的分配技巧)        for(i = end;i >= begin; --i)        //这里要从右向左扫描,保证排序稳定性           {                j = getdigit(arr[i], k);        //求出关键码的第k位的数字, 例如:576的第3位是5               bucket[count[j]-1] = arr[i]; //放入对应的桶中,count[j]-1是第j个桶的右边界索引             --count[j];               //对应桶的装入数据索引减一          }         //注意:此时count[i]为第i个桶左边界          //从各个桶中收集数据        for(i = begin,j = 0; i <= end; ++i, ++j)          {            arr[i] = bucket[j];            }            }         free(bucket);   }  void  main(){    int  br[10] = {20, 80, 90, 589, 998, 965, 852, 123, 456, 789};    cout<<"原数据如下:"<<endl;    PrintArr(br,10);    lsdradix_sort(br, 0, 9, 3);    cout<<"排序后数据如下:"<<endl;    PrintArr(br, 10);}/*原数据如下:20 80 90 589 998 965 852 123 456 789排序后数据如下:20 80 90 123 456 589 789 852 965 998*/

9.计数排序
计数排序也不是基于比较的,而且它也是稳定的,它的时间复杂度是O(n+k),计数排序比任何比较排序算法都要快。
计数排序的基本思想是:
假设数序列中小于元素a的个数为n,则直接把a放到第n+1个位置上。当存在几个相同的元素时要做适当的调整,因为不能把所有的元素放到同一个位置上。
具体是这样的:
设被排序的数组为A,排序后存储到B,C为临时数组。所谓计数,首先是通过一个数组C[i]计算大小等于i的元素个数,此过程只需要一次循环遍历就可以;在此基础上,计算小于或者等于i的元素个数,也是一重循环就完成。下一步是关键:逆序循环,从length[A]到1,将A[i]放到B中第C[A[i]]个位置上。原理是:C[A[i]]表示小于等于a[i]的元素个数,正好是A[i]排序后应该在的位置。而且从length[A]到1逆序循环,可以保证相同元素间的相对顺序不变,这也是计数排序稳定性的体现。在数组A有附加属性的时候,稳定性是非常重要的。
比如下面的代码,输入几个各位数字,输出排序后的数:

#include<iostream>using namespace std;int c[10]={0};int r[10]={0};int main(){    int n;    cin>>n;    int *a=new int[n];    for(int i=0;i<n;i++)    {        cin>>a[i];        c[a[i]]++;//记录大小等于a[i]的元素个数    }    for(int i=1;i<10;i++)    {        c[i]+=c[i-1];//计算小于或者等于i的元素个数    }    for(int i=n-1;i>=0;i--)    {        r[--c[a[i]]]=a[i];//将a[i]放到r数组中c[a[i]]-1的位置上    }    for(int i=0;i<n;i++)    {        cout<<r[i]<<endl;//输出排序后的数组r    }    return 0;}

上面代码的具体实现过程如下:
计数排序
9.堆排序(Heap Sort)
堆排序是结合了归并排序和插入排序优点的一种排序算法,它的时间复杂度是O(n log n),空间复杂度却只有O(1)。
堆可以看成一棵完全二叉树。(除了最底层,该树是完全充满的,而且一定是从左到右填充)这里先讲一些二叉树的基本规律:
一棵深度为n的完全二叉树,结点的个数是2^n-1。
一棵含有n个结点的完全二叉树,深度是log_2(n+1)
堆可以分为大顶堆和小顶堆(也叫最大堆和最小堆),大顶堆要求每个孩子结点的值都不大于它们的父结点的值,小顶堆相反。在堆排序中,我们通常使用的是最大堆。
堆可以用一个数组存储,如下图一所示。
这样的存储结构,我们很容易找到一个结点的父结点,左孩子结点,右孩子结点。比如给定一个结点的下标i,那么它的父结点是i/2,左孩子结点是2*i,右孩子结点是2*i+1。如下图二所示。
我们定义一个结点的高度为该结点到叶结点的最长路径。注意这里与上面的深度不同,比如图一两棵树的根结点的高度都是2,但是两棵树的深度都是3。一棵含有n个结点的完全二叉树,它的高度是log_2(n)
通常情况下,初始化的数组并不具有堆的性质,需要通过重新排列数组元素,建成一个堆,如图三所示。
向堆中插入或者删除一个元素,往往需要重新调整,以维护堆的性质,如图四,向堆中插入一个新元素15。图五,删除根结点10之后(删除操作总是发生在根结点的位置),堆的最后一个元素40填补了根结点的位置,然后树被重新调整以维护一棵最小堆。
堆排序
下面正式讲堆排序,其实只有两个步骤:
1. 根据数组建成一个堆
2. 依次取出堆的根结点
因为建堆的时间复杂度是O(n),调整堆的时间复杂度是O(log n),所以总的时间复杂度是O(n log_n)。
注意:堆排序是不稳定的排序。
下面给出代码实现:

#include <stdio.h>//array是待调整的堆数组,i是待调整的数组元素的位置,nlength是数组的长度//本函数功能是:根据数组array构建大根堆void HeapAdjust(int array[],int i,int nLength){    int nChild;    int nTemp;    for(;2*i+1<nLength;i=nChild)    {        //子结点的位置=2*(父结点位置)+1        nChild=2*i+1;        //得到子结点中较大的结点        if(nChild<nLength-1&&array[nChild+1]>array[nChild])++nChild;        //如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点        if(array[i]<array[nChild])        {            nTemp=array[i];            array[i]=array[nChild];            array[nChild]=nTemp;         }        else break; //否则退出循环    }}//堆排序算法void HeapSort(int array[],int length){    int i;    //调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素    //length/2-1是最后一个非叶节点,此处"/"为整除    for(i=length/2-1;i>=0;--i)    HeapAdjust(array,i,length);    //从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素    for(i=length-1;i>0;--i)    {        //把第一个元素和当前的最后一个元素交换,        //保证当前的最后一个位置的元素都是在现在的这个序列之中最大的        array[i]=array[0]^array[i];        array[0]=array[0]^array[i];        array[i]=array[0]^array[i];        //不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值        HeapAdjust(array,0,i);    }}int main(){    int i;    int num[]={9,8,7,6,5,4,3,2,1,0};    HeapSort(num,sizeof(num)/sizeof(int));    for(i=0;i<sizeof(num)/sizeof(int);i++)    {        printf("%d ",num[i]);    }    printf("\nok\n");    return 0;}

最后给出一个完整的堆排序过程:
堆排序