最近在做数据结构课程设计,所以总结一下常见的排序方法,其他不常见的排序以后更新。为了便于发现问题,下面的每一个代码段都会输出每一趟排序的结果,默认排序是升序
插入排序
- 插入排序的基本思想是每一趟排序,将一个待排序的记录按关键字大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
- 这种排序方法有点类似于打扑克抓牌,每次抓一张牌就把他放在合适的位置上。
- 按照查找的不同方法插入排序可以有多种,下面举三个不同方法的例子
直接插入排序
将数组从前到后遍历,找到一个位置不对的就把它排到正确的方向上去
- 排序思路很简单
- 复杂度分析:寻找元素位置需要遍历数组,这是O(n),找到位置插入也是O(n),所以总的时间复杂度是O(n2),在比较的时候只需要记录一个元素,所以空间复杂度是O(1)
- 稳定排序
template<class ElemType>
void insert_sort(vector<ElemType> &vs){
int len = vs.size();
for(int i=1;i<len;i++){
if(vs[i]<vs[i-1]){
ElemType tmp = vs[i];
for(int j=0;j<i;j++){
if(tmp<vs[j]){
for(int k=i;k>j;k--){
vs[k] = vs[k-1];
}
vs[j] = tmp;
break;
}
}
}
bool flag = true;
for(int i=0;i<len;i++){
cout<<vs[i]<<" ";
if(i&&vs[i]<vs[i-1]) flag = false;
}cout<<endl;
if(flag) return;
}
}
折半插入排序
- 直接插入排序查找待排序元素的位置时候是从前往后依次寻找,折半插入排序在寻找的过程中采用二分查找,
- 复杂度分析:查找是O(log2n),插入是O(n),这里有一个误区,错误地认为折半插入时间复杂度是nlogn,但是看代码就可以发现其实应该是O(n2),因为我们还有一个遍历的过程,也是O(n)的,查找和插入是并列的关系,所以应该取较大的时间复杂度O(n),所以总的时间复杂度是O(n2),空间复杂度同上O(1)
- 稳定排序
template<class ElemType>
int FIND(int l,int r,vector<ElemType> vs,ElemType num){
while(l<=r){
int mid = (l+r)/2;
if(num < vs[mid]) r = mid - 1;
else l = mid + 1;
}
return l;
}
template<class ElemType>
void Binsert_sort(vector<ElemType> &vs){
int len = vs.size();
for(int i=1;i<len;i++){
if(vs[i]<vs[i-1]){
ElemType tmp = vs[i];
int j = FIND(0,i,vs,tmp);
for(int k=i;k>j;k--){
vs[k] = vs[k-1];
}
vs[j] = tmp;
}
bool flag = true;
for(int i=0;i<len;i++){
cout<<vs[i]<<" ";
if(i&&vs[i]<vs[i-1]) flag = false;
}cout<<endl;
if(flag) return;
}
}
希尔排序
- 也是插入排序的一种但是这种方法显得没那么简单,也更加的具有研究价值。
- 理解排序要点,这是一种跨越式的排序,首先规定步长,根据步长去排序,如下图
这是以4为步长的一次排序,每次只需要排好红色部分即可,这样可以达到一个相对有序的结果,这样逐渐减小步长,最后到1,可以实现排序。 - 这种排序方法高级在哪里呢?主要是前面的大步跨越式排序已经使得数组基本有序,所以最后步长为1的时候排序是很快的,因为插入排序对付基本有序的数组效率差不多是O(n)
template<class ElemType>
void ShellSort(vector<ElemType> &vs,int dk){//dk表示步长
int len = vs.size();
for(int i=dk;i<len;i++){
if(vs[i]<vs[i-dk]){
ElemType tmp = vs[i];
int j;
for(j=i-dk;j>=0&&tmp<vs[j];j-=dk){
vs[j+dk] = vs[j];
}
vs[j+dk] = tmp;
}
}
}
template<class ElemType>
void Shell_insert_sort(vector<ElemType> &vs,int dlta[],int t){
int len = vs.size();
for(int i=0;i<t;i++){
ShellSort(vs,dlta[i]);
cout<<dlta[i]<<endl;
for(int i=0;i<len;i++) cout<<vs[i]<<' ';
cout<<endl;
}
}
- 时间复杂度大约是O(n3/2),这涉及到一些尚未解决的难题,故不做讨论,只用了一个中间变量,所以空间复杂度仍然为O(1)
- 跳跃式移动导致排序不稳定
交换排序
冒泡排序
最开始接触C++的时候首先了解的就是冒泡排序,基本思想是从前往后遍历数组,只要发现一个大的就往后挪,这样一次起泡,最大的被挪到最后,这样起泡最多n-1次就可以将整个数组排好序
- 简单的优化就是加一个flag,如果一次起泡没有移动元素就说明已经排好序了不需要再排
template<class ElemType>
void SWAP(ElemType &x,ElemType &y){
ElemType temp;
temp = x;
x = y;
y = temp;
}
template<class ElemType>
void BubbleSort(vector<ElemType> &vs){
int len = vs.size();
bool flag;
for(int i=1;i<len;i++){
flag = true;
for(int j=1;j<len;j++){
if(vs[j]<vs[j-1]){
flag = false;
SWAP(vs[j],vs[j-1]);
}
}
if(flag) return;
for(int i=0;i<len;i++) cout<<vs[i]<<" ";
cout<<endl;
}
}
- 时间复杂度:两次for,O(n2),SWAP操作还有flag,占两个空间,空间复杂度同样为O(1)
- 稳定排序
快速排序
- 这是最基本最重要最普遍最常用的排序方法,algorithm库里的sort就是基于快速排序,再经过优化设计而成的,效率很高
- 需要注意的是快速排序是基于冒泡排序改进而成,冒泡排序是对相邻的两个元素进行比较交换,而快速排序是对不相邻的两个元素进行比较交换,从而一次操作就可以减少多个逆序,效率也大大提高
- 冒泡排序是每一次都排好最大的那一个元素,快速排序每一次都排好现在正在排的元素
基本思想是这样的:首先找一个枢轴元素,方便起见就选第一个,分别从前往后和从后往前寻找元素,从前往后找元素,找到一个比它大的就和它交换位置,直到找不到这样的元素;再从后往前找,找到一个比它小的就和它交换位置,这样枢轴元素就会排到正确的位置上,接着对枢轴两侧递归排序,形成一颗递归树,这颗树高度范围是log2n到n的。
template<class ElemType>
int Partition(vector<ElemType> &vs,int l,int r){
ElemType value = vs[l];
while(l<r){
while(l<r&&value<=vs[r]) r--;
swap(vs[l],vs[r]);
// vs[l] = vs[r];
while(l<r&&value>=vs[l]) l++;
swap(vs[l],vs[r]);
// vs[r] = vs[l];
}int len = vs.size();
cout<<l<<' '<<value<<endl;
// vs[l] = value;
for(int i=0;i<len;i++) cout<<vs[i]<<" ";
cout<<endl;
return l;
}
template<class ElemType>
void quicksort(vector<ElemType> &vs,int l,int r){
int pivoloc;
if(l<r){
pivoloc = Partition(vs,l,r);
quicksort(vs,l,pivoloc-1);
quicksort(vs,pivoloc+1,r);
}
}
- 时间复杂度:选元素是O(n)的,递归排序是O(log2n),总的时间复杂度是O(nlog2n),空间复杂度:由于递归树的高度是log2n到n,所以空间复杂度是O(log2n)到O(n)
- 非顺次移动导致排序不稳定
选择排序
简单选择排序
简单选择排序,顾名思义选择的方法很简单,每次选择当前最小的放在前面,通过遍历数组得到此最小值,而后将数组元素向后挪一位得到排序结果。
template<class ElemType>
void SimpleSelectSort(vector<ElemType> &vs){
int len = vs.size();
for(int i=0;i<len-1;i++){
int k = i;
for(int j=i+1;j<len;j++){
if(vs[j]<vs[k]){
k = j;
}
}
ElemType tmp = vs[k];
vs[k] = vs[i];
vs[i] = tmp;
for(int j=0;j<len;j++){
cout<<vs[j]<<' ';
}cout<<endl;
}
}
- 时间复杂度:O(n2),空间复杂度:O(1)
- 方法是稳定的排序方法,但是通过交换破坏了次序导致不稳定,能够写出稳定排序的算法
堆排序
这是基于完全二叉树的一种排序,所谓完全二叉树,就是一颗二叉树每一层的节点都要从左往右依次分布,满足父亲节点下标 * 2 = 左孩子节点下标,父亲节点下标 * 2+1 = 右孩子节点下标,如下图
- 二叉树用数组存储而不是链表
- 这里涉及到大根堆和小根堆的概念,顾名思义,大根堆就是根节点元素要最大,小根堆就是根节点元素要最小,简而言之看最顶上元素是不是最大的,是最大就是大根堆,小根堆同理
- 那么排序过程是怎样的呢?首先建树,接着,调整成大根堆,把所有非终端节点都要调成以它为根的子树的元素最大值,也就是说要把最大孩子节点和父亲节点交换位置,这样一层一层调节直到最上端的根节点的值最大,将它和最后一个节点交换位置,这样最后一个节点就是最大值了,忽略这个节点,继续上述操作,直到排好序。
- 所以,大根堆排出的应该是升序,小根堆排出的是降序。
template<class ElemType>
void SWAP(ElemType &x,ElemType &y){
ElemType temp;
temp = x;
x = y;
y = temp;
}
template<class ElemType>
void HeapASCAdJust(vector<ElemType> &vs,int s,int m){//大根堆
ElemType tmp = vs[s];
for(int j = 2*s+1;j < m;j=2*j+1){
if(j<m-1&&vs[j]<vs[j+1]) ++j;
if(tmp>=vs[j]) break;
vs[s] = vs[j];
s = j;
}
vs[s] = tmp;
}
template<class ElemType>
void HeapDASCAdJust(vector<ElemType> &vs,int s,int m){//小根堆
ElemType tmp = vs[s];
for(int j = 2*s+1;j<m;j = 2*j+1){
if(j<m-1&&vs[j]>vs[j+1]) ++j;
if(tmp<=vs[j]) break;
vs[s] = vs[j];
s = j;
}
vs[s] = tmp;
}
template<class ElemType>
void CreatHeap(vector<ElemType> &vs,int n){
int len = vs.size();
for(int i = len/2-1;i>=0;i--){
if(n == 1) HeapASCAdJust(vs,i,len);
if(n == 2) HeapDASCAdJust(vs,i,len);
}
}
template<class ElemType>
void Heapsort(vector<ElemType> &vs,int n){
CreatHeap(vs,n);
int len = vs.size();
for(int j = 0;j<len;j++) cout<<vs[j]<<" ";
cout<<endl;
for(int i = len-1;i > 0;i--){
SWAP(vs[i],vs[0]);
if(n == 1) HeapASCAdJust(vs,0,i);
if(n == 2) HeapDASCAdJust(vs,0,i);
for(int j=0;j<len;j++) cout<<vs[j]<<' ';
cout<<endl;
}
}
- 时间复杂度:选择节点是O(n),排序是O(log2n),所以时间复杂度与是O(log2n),空间复杂度O(1)
- 不稳定排序
- 记录较多的时候比较高效
归并排序
这里讲的是二路归并
- 归并排序第一次两个两个一组,将他们变有序,之后四个四个一组,再次归并,再之后八个八个一组,以此类推,图解如下
- 在合并的时候如果发现有一组空了,那么就把剩下的那组所有元素都加在后面
template<class ElemType>
void Merge(vector<ElemType> &a,int l,int mid,int r){
int i = l,j = mid + 1,k = 0;
ElemType *Data = new ElemType[r-l+1];
while(i<=mid&&j<=r){
if(a[i]<=a[j]) Data[k++] = a[i++];
else Data[k++] = a[j++];
}
while(i<=mid) Data[k++] = a[i++];
while(j<=r) Data[k++] = a[j++];
k = 0;
for(int i=l;i<=r;i++) a[i] = Data[k++];
delete [] Data;
}
// template<class ElemType>
// void mergesort(vector<ElemType> &vs,int l,int r){
// int mid = (l+r)/2;
// if(l<r){
// mergesort(vs,l,mid);
// mergesort(vs,mid+1,r);
// Merge(vs,l,mid,r);
// }
// if(r == vs.size()-1){
// for(int i=0;i<r;i++) cout<<vs[i]<<" ";
// cout<<endl;
// }
// }
template<class ElemType>
void MSort(vector<ElemType> &A){
int SIZE = 1;
int low,high,mid;
int len = A.size();
while(SIZE<len){
low = 0;
while(low + SIZE < len){
mid = low + SIZE-1;
high = mid + SIZE;
if(high>len - 1) high = len - 1;
Merge(A,low,mid,high);
low = high + 1;
}
SIZE*=2;
for(int i=0;i<len;i++){
cout<<A[i]<<" ";
if(i==len-1) cout<<endl;
}
}
}
- 这里有递归和非递归两种写法,得到的最终结果都一样,但是想打印中间结果递归不太容易故改为非递归打印,递归写起来更加容易
- 时间复杂度:O(nlog2n),空间复杂度:O(n)
- 稳定排序