算法学习笔记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;}
最后给出一个完整的堆排序过程:
- 算法学习笔记17-经典排序算法
- 经典排序算法归纳笔记
- 【算法学习笔记】-排序算法
- 排序算法学习笔记
- 排序算法学习笔记
- 算法学习笔记-排序
- 排序算法学习笔记
- 排序算法学习笔记
- 排序算法学习笔记
- 排序算法学习笔记
- 经典算法学习:排序之冒泡排序
- 经典算法学习:排序之选择排序
- 经典算法学习:排序之插入排序
- 经典算法学习:排序之归并排序
- 经典算法学习:排序之快速排序
- 经典排序算法归纳笔记(1)
- 经典排序算法归纳笔记(1)
- 经典排序算法归纳笔记(2)
- Base64位图片上传和解密
- 简单页面(C标签)+java后台+数据库,对前篇文章的改进
- SSH项目整合-简单在线订单系统
- 小白算法练习 简单背包专题003 完全背包 hdu lanqiao 包子凑数 dp
- 归档命令(4)——gzip
- 算法学习笔记17-经典排序算法
- ProjectBySwift-02-CustomFont
- struct和typedef struct分析
- js 定时器如何立即关闭
- 【挖坑】在阿里云上部署Web项目(学生9块9一个月!)
- jQuery源码学习笔记(05)
- 数据结构排序基础算法总结(C++版)
- 使用python如何实现森另算法?
- 真正理解线程上下文类加载器(多案例分析)