排序
插入排序:直接插入排序,希尔排序
选择排序:简单选择排序,堆排序
内部排序(只使用内存) 交换排序:冒泡排序,快排
归并排序
基数排序
排序
外部排序(内存和外存都是用)
1、直接插入排序
平均复杂度:O(n^2),最好时间O(n),最坏时间O(n^2),较稳定
基本思想:在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
代码实现:
#include <iostream> using namespace std; int main() { int a[]={5,2,6,3,9}; int len=sizeof(a)/sizeof(a[0]); for(int i=1;i<len;++i) { if(a[i]<a[i-1]) { int t=a[i],j; for(j=i-1;j>=0&&a[j]>t;--j) a[j+1]=a[j]; a[j+1]=t; } } for(int i=0;i<len;++i) cout<<a[i]<<" "; cout<<endl; return 0; }
2.折半插入排序
对有序表进行折半查找,其性能优于顺序查找
#include <iostream> using namespace std; int main() { int a[]={5,2,6,3,9}; int len=sizeof(a)/sizeof(a[0]); for(int i=1;i<len;++i) { if(a[i]<a[i-1]) { int t=a[i],low=0,height=i-1; while(low<=height) { int mid=(low+height)/2; if(t<a[mid]) height=mid-1; else low=mid+1; } int j; for(j=i-1;j>=low;--j) a[j+1]=a[j]; a[j+1]=t; } } for(int i=0;i<len;++i) cout<<a[i]<<" "; cout<<endl; return 0; }
3、希尔排序(也称最小增量排序)
时间复杂度:O(n*log(n)),不稳定
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
直接插入排序的的步长是为1的,而希尔排序向前比较是以一定步长向前比较。
#include<iostream> using namespace std; class ShellSort { public: int* shellSort(int* A, int n) //希尔排序 { int gap,i,j,temp; if(n==1) return A; for(gap=n/2;gap>0;gap/=2) { i=gap; while(i<n) { temp=A[i]; j=i-gap; while(j>=0&&A[j]>temp) { A[j+gap]=A[j]; j=j-gap; } A[j+gap]=temp; i++; } } return A; } }; int main() { int arr[]={54,35,48,36,27,12,44,44,8,14,26,17,28}; ShellSort a; a.shellSort(arr,13); for(int i=0;i<13;i++) cout<<arr[i]<<" "; cout<<endl; return 0; }
4、简单选择排序
平均时间复杂度:O(n^2),最好:O(n^2),最坏:O(n^2),不稳定
基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
#include <iostream> using namespace std; void select_sort(int a[],int len) { for(int i=0;i<len-1;++i) //N个数排序,经过N-1次便利后,已经是有序数 { int min=i; for(int j=i+1;j<len;++j) if(a[j]<a[min]) min=j; if(min!=i) { int t=a[min]; a[min]=a[i]; a[i]=t; } } } int main() { int a[]={70,30,40,10,80,20,90,100,75,60,45}; int len=sizeof(a)/sizeof(int); select_sort(a,len); for(int i=0;i<len;++i) cout<<a[i]<<" "; cout<<endl; return 0; }
5、堆排序
平均时间复杂度:n*log(n),最好:nlog(n),最坏:nlog(n),不稳定
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
一般升序采用小顶堆,降序采用大顶堆
堆的定义如下:具有n个元素的序列(h1,h2,...,hn),当且仅当满足(hi>=h2i&&hi>=2i+1)或(hi<=h2i&&hi<=2i+1)(i=1,2,...,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。
堆排序中主要的两个问题:一是按堆的定义建初堆,二是去掉最大元之后重建堆。
初建堆:
将一个任意序列看成是对应的完全二叉树,由于叶结点可以视为单元素的堆,因而可以反复利用上述调整的算法,自底向上逐层把所有子树调整为堆,直到整个完全二叉树为堆。最后一个非叶节点位于n/2(向下取整)个位置,n为二叉树结点数目,因此,筛选从n/2(向下取整)个结点开始,逐层向上倒退,知道根节点。
调整堆:
这是为了保持堆的特性而做的一个操作。对某一个节点为根的子树做堆调整,其实就是将该根节点进行“下沉”操作(具体是通过和子节点交换完成的),一直下沉到合适的位置,使得刚才的子树满足堆的性质。
例如对最大堆的堆调整我们会这么做:
1、在对应的数组元素A[i], 左孩子A[LEFT(i)], 和右孩子A[RIGHT(i)]中找到最大的那一个,将其下标存储在largest中。
2、如果A[i]已经就是最大的元素,则程序直接结束。
3、否则,i的某个子结点为最大的元素,将A[largest]与A[i]交换。
4、再从交换的子节点开始,重复1,2,3步,直至叶子节点,算完成一次堆调整。
这里需要提一下的是,一般做一次堆调整的时间复杂度为log(n)。
堆排序:
数组储存成堆的形式之后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n-2]交换,再对A[0…n-3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Heap_sort { public: void build_heap(vector<int> &v); void adjust_heap(vector<int> &v,vector<int>::iterator i,vector<int>::iterator dis); void sort_heap(vector<int> &v); vector<int>::iterator left_child(vector<int> &v,vector<int>::iterator i); vector<int>::iterator right_child(vector<int> &v,vector<int>::iterator i); }; //获取左节点下标 vector<int>::iterator Heap_sort::left_child(vector<int> &v,vector<int>::iterator i) { advance(i,distance(v.begin(),i)); return i; } //获取右节点下标 vector<int>::iterator Heap_sort::right_child(vector<int> &v,vector<int>::iterator i) { advance(i,distance(v.begin(),i)+1); return i; } //调整堆 void Heap_sort::adjust_heap(vector<int> &v,vector<int>::iterator i,vector<int>::iterator dis) { vector<int>::iterator l=left_child(v,i); vector<int>::iterator r=right_child(v,i); vector<int>::iterator largest=v.begin(); if((*i)<(*l)&&(l<dis)) largest=l; else largest=i; if((*largest)<(*r)&&(r<dis)) largest=r; //建的是大顶锥 if(largest!=i) { swap(*largest,*i); adjust_heap(v,largest,dis); } } //初建堆 void Heap_sort::build_heap(vector<int> &v) { for(vector<int>::iterator i=v.begin()+v.size()/2-1;i!=--v.begin();--i) adjust_heap(v,i,v.end()); } //堆排序 void Heap_sort::sort_heap(vector<int> &v) { build_heap(v);//堆排序之前应该先初建堆 for(auto i=--v.end();i!=--v.begin();--i) { swap(*i,*(v.begin()));//交换第一个元素(最大的)与最后一个元素 adjust_heap(v,v.begin(),i);//交换完之后第一个元素不是最大的,要调整堆使第一个元素最大 } } int main() { vector<int> v{70,30,40,10,80,20,90,100,75,60,45};//看做完全二叉树 Heap_sort hs;//创建堆排序的对象 hs.sort_heap(v); for(auto i:v) cout<<i<<" "; cout<<endl; return 0; }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
#include<iostream> using namespace std; /* 对于具有n个节点的完全二叉树,如果按照从上(根节点)到下(叶节点)和从左到右的顺序对二叉树中的所有节点从0开始到n-1进行编号,则对于任意的下标为k的节点,有: 如果k=0,则它是根节点,它没有父节点;如果k>0,则它的父节点的下标为[(i-1)/2]; 如果2k+1 <= n-1,则下标为k的节点的左子结点的下标为2k+1;否则,下标为k的节点没有左子结点. 如果2k+2 <= n-1,则下标为k的节点的右子节点的下标为2k+2;否则,下标为k的节点没有右子节点 */ class HeapSort { public: int* heapSort(int* A, int n) //堆排序 { int i,temp; for(i=n/2-1;i>=0;--i) HeapAdjust(A,i,n-1); for(i=n-1;i>0;i--) { temp=A[0]; A[0]=A[i]; A[i]=temp; HeapAdjust(A,0,i-1); } return A; } void HeapAdjust(int *A,int s,int m) //已知A[s,...,m]中记录的关键字除A[s]之外均满足堆的定义,本函数调整A[s] //的关键字,使A[s,...,m]成为一个大顶堆(对其中记录的关键字而言) { int j,rc=A[s]; for(j=2*s+1;j<=m;j=2*j+1) { if(j<m&&A[j]<A[j+1]) j++; if(rc>A[j]) break; A[s]=A[j]; s=j; } A[s]=rc; } }; int main() { int arr[]={54,35,48,36,27,12,44,44,8,14,26,17,28}; HeapSort a; a.heapSort(arr,13); for(int i=0;i<13;i++) cout<<arr[i]<<" "; cout<<endl; return 0; }
6、冒泡排序
平均时间复杂度:o(n^2),最好O(n),最坏:o(n^2),稳定
基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v{70,30,40,10,80,20,90,100,75,60,45}; bool flag=true; //n个数只需要n-1次排序找出n-1个大的,剩下的一个自动排序 for(int i=0;i<v.size()-1;++i) { flag=false; for(auto j=begin(v);j!=--v.end()-i;++j) if(*j>*(j+1)) { swap(*j,*(j+1)); flag=true; } } for(auto i:v) cout<<i<<" "; cout<<endl; return 0; }
算法评价
从冒泡的原理上,我们可以知道,从前向后进行循环遍历交换和从后向前进行循环遍历交换的代价和逻辑是一致的。
在单向冒泡排序算法中,存在着一个著名的“乌龟问题”---假设我们需要将序列A按照升序序列排序。序列中的较小的数字又大量存在于序列的尾部,这样会让小数字在向前移动得很缓慢。
而在排序过程中,又主要是这个过程耗费了大量时间。关于具体的实例在下面的“双向冒泡排序”算法中体现。
双向冒泡排序算法步骤
- 比较相邻两个元素的大小。如果前一个元素比后一个元素大,则两元素位置交换
- 对数组中所有元素的组合进行第1步的比较
- 奇数趟时从左向右进行比较和交换
- 偶数趟时从右向左进行比较和交换
- 当从左端开始遍历的指针与从右端开始遍历的指针相遇时,排序结束
//双向冒泡排序 #include <iostream> #include <algorithm> using namespace std; void per_sort(int a[],int len,int per_index) { for(int i=per_index+1;i<len;++i) if(a[i]<a[per_index]) swap(a[i],a[per_index]); } void back_sort(int a[],int len,int back_index) { for(int i=back_index-1;i>=0;--i) if(a[i]>a[back_index]) swap(a[i],a[back_index]); } void two_way_bubble_sort(int a[],int len) { int per_index=0,back_index=len-1; while(per_index<back_index) { per_sort(a,len,per_index); ++per_index; if(per_index>back_index) break; back_sort(a,len,back_index); --back_index; } } int main() { int a[]={70,30,40,10,80,20,90,100,75,60,45}; int length=sizeof(a)/sizeof(int); two_way_bubble_sort(a,length); for(int i=0;i<length;++i) cout<<a[i]<<" "; cout<<endl; return 0; }
7、快速排序
平均时间复杂度:O(n*log(n)),最好:O(nlog(n)),最坏:O(n^2),不稳定
基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。
假设待划分的序列为a[left],a[left+1]......a[right],首先将基准记录a[left]移致变量x中,使a[left]相当于空单元,然后反复进行如下两个扫描过程,知道left和right相遇。
1.left从右向左扫描,直到a[right]<x,将a[right]移至空单元a[left]中,此时a[right]相当于空单元
2.left从左向右扫描,直到a[left]>x,将a[left]移到空单元a[right],此时a[left]相当于空单元。
当left和right相遇时,a[left]或a[right]相当于空单元,且a[left]左边均比它小,右边均比它大,最后将基准记录移至a[left]中,完成了一次划分,再对a[left]的左子表和右子表进行同样的划分。
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Quick_sort { public: void quick_sort(vector<int> &v,vector<int>::iterator it1,vector<int>::iterator it2); private: vector<int>::iterator quick_pass(vector<int> &v,vector<int>::iterator it1,vector<int>::iterator it2); }; vector<int>::iterator Quick_sort::quick_pass(vector<int> &v,vector<int>::iterator it1,vector<int>::iterator it2) { int t=*it1; while(it1<it2) { while(t<=*it2&&it2>it1) --it2; if(it1<it2) *it1++=*it2; while(t>*it1&&it1<it2) ++it1; if(it1<it2) *it2--=*it1; } *it1=t; return it1; } void Quick_sort::quick_sort(vector<int> &v,vector<int>::iterator it1,vector<int>::iterator it2) { if(it1<it2) { auto mid=quick_pass(v,it1,it2); quick_sort(v,it1,mid-1); quick_sort(v,mid+1,it2); } } int main() { vector<int> v{70,30,40,10,80}; Quick_sort qs; qs.quick_sort(v,v.begin(),--v.end()); for(auto i:v) cout<<i<<" "; cout<<endl; return 0; }
8、归并排序
平均时间复杂度:O(n*log(n)),最好:O(nlog(n)),最坏:O(nlog(n)),稳定
基本排序:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
算法思想:
假设初始序列含有n个记录,首先将这n个记录看成n个有序的子序列,每个子序列的长度为1,然后两两归并,得到N/2(向上取整)个长度为2(n为奇数时,最后一个序列的长度为1)的有序子序列
在此基础上,在对长度为2的有序子序列进行两两归并,得到若干个长度为4的子序列,重复,知道得到长度为n的有序序列为止。
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Merge_sort { public: void merge_sort(vector<int> &v,vector<int>::iterator low,vector<int>::iterator heigh); private: void merge(vector<int> &v,vector<int>::iterator low,vector<int>::iterator mid,vector<int>::iterator heigh,vector<int> &tmp); }; void Merge_sort::merge(vector<int> &v,vector<int>::iterator low,vector<int>::iterator mid,vector<int>::iterator heigh,vector<int> &tmp) { vector<int>::iterator i=low,j=mid+1; while(i<=mid&&j<=heigh) { if(*i<=*j) { tmp.push_back(*i); ++i; } else { tmp.push_back(*j); ++j; } } while(i<=mid) { tmp.push_back(*i); ++i; } while(j<=heigh) { tmp.push_back(*j); ++j; } for(auto i=tmp.begin(),k=low+distance(tmp.begin(),i);i!=tmp.end();++i,k=low+distance(tmp.begin(),i)) *k=*i; return; } void Merge_sort::merge_sort(vector<int> &v,vector<int>::iterator low,vector<int>::iterator heigh) { vector<int> tmp; if(low<heigh) { auto mid=low+distance(low,heigh)/2; merge_sort(v,low,mid); merge_sort(v,mid+1,heigh); merge(v,low,mid,heigh,tmp); } return; } int main() { vector<int> v{70,30,40,10,80,20,90,100,75,60,45}; Merge_sort ms; ms.merge_sort(v,v.begin(),--v.end()); for(auto i:v) cout<<i<<" "; cout<<endl; return 0; }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
#include<iostream> using namespace std; class MergeSort { public: int* mergeSort(int* A, int n) //归并排序 { Msort(A,A,0,n-1); return A; } void Msort(int *A,int *B,int s,int t) //将A[s,...,t]归并排序为B[s,...,t] { if(s==t) B[s]=A[s]; else { int m=(s+t)/2; int C[100]={0}; Msort(A,C,s,m); Msort(A,C,m+1,t); Merge(C,B,s,m,t); } } void Merge(int *A,int *B,int i,int m,int n) //将有序的A[i,...,m]和A[m+1,...,n]归并为有序的B[i,...,n] { int j,k; for(j=m+1,k=i;i<=m&&j<=n;k++) { if(A[i]<A[j]) B[k]=A[i++]; else B[k]=A[j++]; } if(i<=m) while(i<=m) B[k++]=A[i++]; if(j<=n) while(j<=n) B[k++]=A[j++]; } }; int main() { int arr[]={54,35,48,36,27,12,44,44,8,14,26,17,28}; MergeSort a; a.mergeSort(arr,13); for(int i=0;i<13;i++) cout<<arr[i]<<" "; cout<<endl; return 0; }
9、基数排序
平均时间复杂度:O(n),最好:O(n),最坏:O(n),稳定
基本思想:
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
简介:
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
整个算法过程描述如下:
1、将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
2、从最低位开始,依次进行一次排序。
3、这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的时间复杂度是 O(k•n),其中n是排序元素个数,k是数字位数。
注意这不是说这个时间复杂度一定优于O(n·log(n)),因为k的大小一般会受到n的影响。 以排序n个不同整数来举例,假定这些整数以B为底,这样每位数都有B个不同的数字,k就一定不小于logB(n)。由于有B个不同的数字,所以就需要B个不同的桶,在每一轮比较的时候都需要平均n·log2(B) 次比较来把整数放到合适的桶中去,所以就有:
k 大于或等于 logB(n)
每一轮(平均)需要 n·log2(B) 次比较
所以,基数排序的平均时间T就是:
T ≥ logB(n)·n·log2(B) = log2(n)·logB(2)·n·log2(B) = log2(n)·n·logB(2)·log2(B) = n·log2(n)
所以和比较排序相似,基数排序需要的比较次数:T ≥ n·log2(n)。 故其时间复杂度为 Ω(n·log2(n)) = Ω(n·log n) 。
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Radix_sort { public: void radix_sort(vector<int> &v); private: void count_sort(vector<int> &v,int exp); }; void Radix_sort::count_sort(vector<int> &v,int exp) { vector<int> tmp(10,0),ot(v.size(),0); for(int i=0;i<v.size();++i) ++(tmp.at((v.at(i)/exp)%10)); for(int i=1;i<10;++i) tmp.at(i)+=tmp.at(i-1); for(int i=v.size()-1;i>-1;--i) ot.at(--(tmp.at(v.at(i)/exp%10)))=v.at(i); copy(ot.begin(),ot.end(),v.begin()); } void Radix_sort::radix_sort(vector<int> &v) { int Max=*max_element(v.begin(),v.end()); for(int exp=1;Max/exp>0;exp*=10) count_sort(v,exp); } int main() { vector<int> v{70,30,40,10,80,20,90,100,75,60,45}; Radix_sort rs; rs.radix_sort(v); for(auto i:v) cout<<i<<" "; cout<<endl; return 0; }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
#include<iostream> using namespace std; #define N 10 class RadixSort { public: int* radixSort(int* A, int n,int radix) //基数排序:radix为关键字最高位数 { int temp[10][N]={0},order[10]={0}; int m=(int)pow((double)10,radix-1),base=1; while(base<=m) { int i,k; for(i=0;i<n;i++) { int lsd=(A[i]/base)%10; temp[lsd][order[lsd]]=A[i]; order[lsd]++; } for(i=0,k=0;i<10;i++) { if(order[i]) { int j; for(j=0;j<order[i];j++,k++) A[k]=temp[i][j]; } order[i]=0; } base*=10; } return A; } }; int main() { int arr[]={5412,351,4821,362,127,12,441,414,8,1499,2226,5717,268}; RadixSort a; a.radixSort(arr,13,4); for(int i=0;i<13;i++) cout<<arr[i]<<" "; cout<<endl; return 0; }
10、桶排序
平均时间复杂度:O(n),最好:O(n),最坏:O(n),不稳定
算法简介
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
桶排序是稳定的,且在大多数情况下常见排序里最快的一种,比快排还要快,缺点是非常耗空间,基本上是最耗空间的一种排序算法,而且只能在某些情形下使用。
算法描述和分析
桶排序具体算法描述如下:
1、设置一个定量的数组当作空桶子。
2、寻访串行,并且把项目一个一个放到对应的桶子去。
3、对每个不是空的桶子进行排序。
4、从不是空的桶子里把项目再放回原来的串行中。
桶排序最好情况下使用线性时间O(n),很显然桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为 其它部分的时间复杂度都为O(n);很显然,桶划分的越小,各个桶之间的数据越少,排 序所用的时间也会越少。但相应的空间消耗就会增大。
可以证明,即使选用插入排序作为桶内排序的方法,桶排序的平均时间复杂度为线性。其空间复杂度也为线性。
#include <iostream> #include <algorithm> #include <vector> #include <cstdlib> #include <ctime> using namespace std; class Rand { public: int operator() () { //srand((unsigned)time(NULL)); return rand()%(100-1)+1; } }; class Bucket_sort { public: void bucket_sort(vector<int> &v); }; void Bucket_sort::bucket_sort(vector<int> &v) { //1.设置10个桶 vector<vector<int> > tmp(10,vector<int>(distance(v.begin(),v.end()))); //vector<int> count(10,0);//桶 //2.获得数据的平均值 double avg=(*max_element(v.begin(),v.end())-*min_element(v.begin(),v.end())+1)/10.0; //cout<<avg<<endl; for(auto it=v.begin();it!=v.end();++it) { //3.确定桶号 int num=(*it-*min_element(v.begin(),v.end()))/avg; //cout<<num<<endl; //4.把数据装入桶中 if(tmp[num][0]) { //此桶中有数据,用直接插入排序 int len=1; for(int i=1;i<10;++i) if(tmp[num][i]) ++len; else break; //int t=*it; int j; for(j=len-1;j>=0;--j) { if(*it<tmp[num][j]) tmp[num][j+1]=tmp[num][j]; else break; } tmp[num][j+1]=*it; } else//桶中无数据可以直接插入 tmp[num][0]=*it; } //把桶中的数据按顺序放到原来的数组 v.clear(); for(int i=0;i<10;++i) { if(tmp[i][0])//如果桶中有数据 for(int j=0;j<10&&tmp[i][j];++j) v.push_back(tmp[i][j]); } } int main() { vector<int> arr(10); generate(arr.begin(),arr.end(),Rand()); cout<<" 产生的随机数:"<<endl; for(auto i:arr) cout<<i<<" "; cout<<endl; cout<<" 排序后的数字:"<<endl; Bucket_sort bs; bs.bucket_sort(arr); for(auto i:arr) cout<<i<<" "; cout<<endl; return 0; }
11、计数排序
算法简介
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i+Min的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。
算法的步骤如下:
1、找出待排序的数组中最大和最小的元素
2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3、对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4、反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。计数排序是一种以空间换时间的排序算法,并且只适用于待排序列中所有的数较为集中时,比如一组序列中的数据为0 1 2 3 4 999;就得开辟1000个辅助空间。
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Count_sort { public: void count_sort(vector<int> &v); }; void Count_sort::count_sort(vector<int> &v) { int Min=*min_element(v.begin(),v.end()); int Max=*max_element(v.begin(),v.end()); vector<int> count(Max-Min+1);//数据范围 for(int i=0;i<v.size();++i)//把数据存入计数数组,计算每个下表的数字个数 ++(count.at(v.at(i)-Min)); for(int i=0,index=0;i<Max-Min+1;++i) while((count.at(i))--) v.at(index++)=i+Min; } int main() { vector<int> v{5,6,3,8,4,5,2,6,7,6,3,9}; Count_sort cs; cs.count_sort(v); for(auto i:v) cout<<i<<" "; cout<<endl; return 0; }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
#include<iostream> #include<cstdlib> using namespace std; class CountingSort { public: int* countingSort(int* A, int n) //¼ÆÊýÅÅÐò { int i,j,min,max; for(i=1,min=max=A[0];i<n;i++) { if(A[i]<=min) min=A[i]; if(A[i]>max) max=A[i]; } int *counts=(int *)calloc(max-min+1,sizeof(int)); if(!counts) exit(-1); for(i=0;i<n;i++) counts[A[i]-min]++; for(i=0,j=0;i<max-min+1;i++) while(counts[i]) { A[j]=i+min; counts[i]--; j++; } free(counts); counts=NULL; return A; } }; int main() { int arr[]={54,35,48,36,27,12,44,44,8,14,26,17,28}; CountingSort a; a.countingSort(arr,13); for(int i=0;i<13;i++) cout<<arr[i]<<" "; cout<<endl; return 0; }
分析一下8种排序算法的稳定性
(1)直接插入排序:一般插入排序,比较是从有序序列的最后一个元素开始,如果比它大则直接插入在其后面,否则一直往前比。如果找到一个和插入元素相等的,那么就插入到这个相等元素的后面。插入排序是稳定的。
(2)希尔排序:希尔排序是按照不同步长对元素进行插入排序,一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,稳定性就会被破坏,所以希尔排序不稳定。
(3)简单选择排序:在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。光说可能有点模糊,来看个小实例:858410,第一遍扫描,第1个元素8会和4交换,那么原序列中2个8的相对前后顺序和原序列不一致了,所以选择排序不稳定。
(4)堆排序:堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, ...这些父节点选择元素时,有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,所以堆排序并不稳定。
(5)冒泡排序:由前面的内容可知,冒泡排序是相邻的两个元素比较,交换也发生在这两个元素之间,如果两个元素相等,不用交换。所以冒泡排序稳定。
(6)快速排序:在中枢元素和序列中一个元素交换的时候,很有可能把前面的元素的稳定性打乱。还是看一个小实例:6 4 4 5 4 7 8 9,第一趟排序,中枢元素6和第三个4交换就会把元素4的原序列破坏,所以快速排序不稳定。
(7)归并排序:在分解的子列中,有1个或2个元素时,1个元素不会交换,2个元素如果大小相等也不会交换。在序列合并的过程中,如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,所以,归并排序也是稳定的。
(8)基数排序:是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
查找
一:基于线性表的查找
顺序查找的基本思想:
从表的一端开始,顺序扫描表,依次将扫描到的结点关键字和给定值(假定为a)相比较,若当前结点关键字与a相等,则查找成功;若扫描结束后,仍未找到关键字等于a的结点,则查找失败。
说白了就是,从头到尾,一个一个地比,找着相同的就成功,找不到就失败。很明显的缺点就是查找效率低。
适用于线性表的顺序存储结构和链式存储结构。
计算平均查找长度。
例如上表,查找1,需要1次,查找2需要2次,依次往下推,可知查找16需要16次,
可以看出,我们只要将这些查找次数求和(我们初中学的,上底加下底乘以高除以2),然后除以结点数,即为平均查找长度。
设n=节点数
平均查找长度=(n+1)/2
#include <iostream> using namespace std; int order_search(int a[],int t,int len) { for(int i=0;i<len;++i) if(a[i]==t) return i+1; return 0; } int main() { int a[]={70,30,40,10,80,20,90,100,75,60,45}; int length=sizeof(a)/sizeof(int); cout<<"请输入待查找的关键字:"; int t; cin>>t; int result=order_search(a,t,length); if(result) cout<<"查找成功,元素的位置在:"<<result<<endl; else cout<<"查找失败,要查找的元素不在表中."<<endl; return 0; }
二分法查找(折半查找)的基本思想:
前提:
(1)确定该区间的中点位置:mid=(low+high)/2
min代表区间中间的结点的位置,low代表区间最左结点位置,high代表区间最右结点位置
(2)将待查a值与结点mid的关键字(下面用R[mid].key)比较,若相等,则查找成功,否则确定新的查找区间:
如果R[mid].key>a,则由表的有序性可知,R[mid].key右侧的值都大于a,所以等于a的关键字如果存在,必然在R[mid].key左边的表中。这时high=mid-1
如果R[mid].key<a,则等于a的关键字如果存在,必然在R[mid].key右边的表中。这时low=mid
如果R[mid].key=a,则查找成功。
(3)下一次查找针对新的查找区间,重复步骤(1)和(2)
(4)在查找过程中,low逐步增加,high逐步减少,如果high<low,则查找失败。
平均查找长度=Log2(n+1)-1
注:虽然二分法查找的效率高,但是要将表按关键字排序。而排序本身是一种很费时的运算,所以二分法比较适用于顺序存储结构。为保持表的有序性,在顺序结构中插入和删除都必须移动大量的结点。因此,二分查找特别适用于那种一经建立就很少改动而又经常需要查找的线性表。
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Quick_sort { public: void quick_sort(vector<int> &v,vector<int>::iterator low,vector<int>::iterator heigh); private: vector<int>::iterator quick_pass(vector<int> &v,vector<int>::iterator low,vector<int>::iterator heigh); }; vector<int>::iterator Quick_sort::quick_pass(vector<int> &v,vector<int>::iterator low,vector<int>::iterator heigh) { int t=*low;//此时low相当于空的 while(low<heigh) { while(low<heigh&&t<=(*heigh))//从右向左找第一个比t小的 --heigh; if(low<heigh) *low++=*heigh;//把height移到low,此时height相当于空的 while(low<heigh&&t>(*low))//从左向右找第一个比t大的 ++low; if(low<heigh) *heigh++=*low; } *low=t; return low; } void Quick_sort::quick_sort(vector<int> &v,vector<int>::iterator low,vector<int>::iterator heigh) { if(low<heigh) { vector<int>::iterator mid=quick_pass(v,low,heigh); quick_sort(v,low,mid-1); quick_sort(v,mid+1,heigh); } return; } vector<int>::const_iterator binary_search(const vector<int> &v,int num) { vector<int>::const_iterator low=v.begin(); vector<int>::const_iterator heigh=v.end(); while(low<=heigh) { vector<int>::const_iterator mid=low+distance(low,heigh)/2; if(num<*mid) heigh=mid-1; else if(num>*mid) low=mid+1; else return low; } } int main() { vector<int> v{3,6,52,4,8,9}; Quick_sort qs; qs.quick_sort(v,v.begin(),--v.end()); for(auto i:v) cout<<i<<" "; cout<<endl; cout<<*binary_search(v,6)<<endl; return 0; }
分块查找的基本思想:
二分查找表使分块有序的线性表和索引表(抽取各块中的最大关键字及其起始位置构成索引表)组成,由于表是分块有序的,所以索引表是一个递增有序表,因此采用顺序或二分查找索引表,以确定待查结点在哪一块,由于块内无序,只能用顺序查找。
1.将待查找关键字k与索引表中的关键字进行比较,已确定待查找记录所在的块,可用折半查找或顺序查找
2.进一步用顺序查找,在相应的块内查找关键字为k的元素。
#include <iostream> #include <vector> #include <algorithm> using namespace std; struct block { int start,key; }block[3]; typedef struct block B; bool cmp(const B &x,const B &y) { if(x.key<=y.key) return true; else return false; } int block_search(vector<int> v,int num) { //分为三块,计算每个块内有多少元素 int step=ceil(v.size()/3.0);//其实也就是块的长度,最后一个元素的下标 for(int i=0,j=0;i<3;++i)//3块 { block[i].start=j; if(j+step<v.size())//最后一个块内的元素不一定等于setp,防止越界 j+=step; else j=v.size(); block[i].key=*max_element(v.begin()+block[i].start,v.begin()+j);//key为每个块内的最大的元素 } sort(block,block+3,cmp); //找到第一个key小于num的block int i=0; for(;block[i].key<num&&i<3;++i); int j=block[i].start; for(;j<(block[i].start+step)&&((v.at(j))!=num);++j); if(j>=block[i].start+step) { cerr<<" 要查找的数不存在"<<endl; return -1; } return v.at(j); } int main() { vector<int> v{70,30,40,10,80,20,90,100,75,60,45}; cout<<block_search(v,80)<<endl; return 0; }
二:计算式查找法
哈希法
根据关键字(key)而直接访问在内存储存位置,在元素的关键字 k 与元素的存储位置 p 之间建立一个对应的关系H,则有p=H(k),这种关系H,就是哈希函数,在创建哈希表时,将关键字 k 的元素直接存入对应的地址 H(k )里;以后要查找关键字为 k 的元素时,通过哈希函数H(k)计算出相应的存储位置,就可以直接找到,这也就是计算式查找!
构造哈希函数的原则是:①函数本身便于计算;②计算出来的地址分布均匀,即对任一关键字k,f(k) 对应不同地址的概率相等,目的是尽可能减少冲突。
1. 数字分析法
如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。例如,有80个记录,关键字为8位十进制整数d1d2d3…d7d8,如哈希表长取100,则哈希表的地址空间为:00~99。假设经过分析,各关键字中 d4和d7的取值分布较均匀,则哈希函数为:h(key)=h(d1d2d3…d7d8)=d4d7。例如,h(81346532)=43,h(81301367)=06。相反,假设经过分析,各关键字中 d1和d8的取值分布极不均匀, d1 都等于5,d8 都等于2,此时,如果哈希函数为:h(key)=h(d1d2d3…d7d8)=d1d8,则所有关键字的地址码都是52,显然不可取。
2. 平方取中法
当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如图8.23所示。
在哈希表上进行查找的过程和哈希造表的过程基本一致。给定K值,根据造表时设定的哈希函数求得哈希地址,若表中此位置没有记录,则查找不成功;否则比较关键字,若何给定值相等,则查找成功;否则根据处理冲突的方法寻找“下一地址”,知道哈希表中某个位置为空或者表中所填记录的关键字等于给定值时为止。
3. 分段叠加法
这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。具体方法有折叠法与移位法。移位法是将分割后的每部分低位对齐相加,折叠法是从一端向另一端沿分割界来回折叠(奇数段为正序,偶数段为倒序),然后将各段相加。例如:key=12360324711202065,哈希表长度为1000,则应把关键字分成3位一段,在此舍去最低的两位65,分别进行移位叠加和折叠叠加,求得哈希地址为105和907,如图8.24所示。
4. 除留余数法
假设哈希表长为m,p为小于等于m的最大素数,则哈希函数为
h(k)=k % p ,其中%为模p取余运算。
例如,已知待散列元素为(18,75,60,43,54,90,46),表长m=10,p=7,则有
h(18)=18 % 7=4 h(75)=75 % 7=5 h(60)=60 % 7=4
h(43)=43 % 7=1 h(54)=54 % 7=5 h(90)=90 % 7=6
h(46)=46 % 7=4
此时冲突较多。为减少冲突,可取较大的m值和p值,如m=p=13,结果如下:
h(18)=18 % 13=5 h(75)=75 % 13=10 h(60)=60 % 13=8
h(43)=43 % 13=4 h(54)=54 % 13=2 h(90)=90 % 13=12
h(46)=46 % 13=7
5. 伪随机数法
采用一个伪随机函数做哈希函数,即h(key)=random(key)。
在实际应用中,应根据具体情况,灵活采用不同的方法,并用实际数据测试它的性能,以便做出正确判定。通常应考虑以下五个因素 :
l 计算哈希函数所需时间 (简单)。
l 关键字的长度。
l 哈希表大小。
l 关键字分布情况。
l 记录查找频率
处理冲突的办法
1. 开放定址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:
Hi=(H(key)+di)% m i=1,2,…,n
其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:
l 线性探测再散列
dii=1,2,3,…,m-1
这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
l 二次探测再散列
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
l 伪随机探测再散列
di=伪随机数序列。
具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。
例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元,参图8.26 (a)。如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元,参图8.26 (b)。如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元,参图8.26 (c)。
从上述例子可以看出,线性探测再散列容易产生“二次聚集”,即在处理同义词的冲突时又导致非同义词的冲突。例如,当表中i, i+1 ,i+2三个单元已满时,下一个哈希地址为i, 或i+1 ,或i+2,或i+3的元素,都将填入i+3这同一个单元,而这四个元素并非同义词。线性探测再散列的优点是:只要哈希表不满,就一定能找到一个不冲突的哈希地址,而二次探测再散列和伪随机探测再散列则不一定。
2. 再哈希法
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
3. 链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
例如,已知一组关键字(32,40,36,53,16,46,71,27,42,24,49,64),哈希表长度为13,哈希函数为:H(key)= key % 13,则用链地址法处理冲突的结果如图8.27所示:
本例的平均查找长度 ASL=(1*7+2*4+3*1)=1.5
4、建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
基于线性表和基于树的查找方法相比
这两种结构在记录时,相对位置是随机的,与记录的关键字没有直接关系,在查找时,是基于与关键字的比较,查找的效率依赖于查找过程中比较的次数,所以相比而言哈希法的查找效率会快很多
#include <iostream> #include <vector> #include <vector> using namespace std; typedef struct Hash { int key; struct Hash *next; Hash() { key=0; next=nullptr; } }Hash; Hash *insert_hash(Hash *head,const int &key) { Hash *t=new Hash(); t->key=key; Hash *p0=head,*p1=nullptr; if(!head) { head=t; t->next=nullptr; } else { while((p0->key<t->key)&&(p0->next!=nullptr)) { p1=p0; p0=p0->next; } if(t->key<=p0->key)//插到当前节点的前面 { if(p0==head)//且当前节点是第一个节点 { head=t; head->next=p0; } else { p1->next=t; t->next=p0; } } else//插入到当前节点的后面 { p0->next=t; t->next=nullptr; } } return head; } pair<int,int> hash_search(Hash *h,int key) { int j=0; Hash *t=h; while(t) { if(t->key==key) break; ++j; t=t->next; } if(!t) return make_pair(-1,-1); else return make_pair(-1,j); } int main() { vector<Hash*> h(100); vector<int> v{70,30,40,10,48,24,90,100,75,60,45}; for(int i=0;i<h.size();++i) h.at(i)=nullptr; for(int i=0;i<v.size();++i) { int t=v.at(i)%(v.size()+1); h.at(t)=insert_hash(h.at(t),v.at(i)); } for(int i=0;i<v.size();++i) { Hash *t=h.at(i); while(t) { cout<<t->key<<" "; t=t->next; } cout<<endl; } pair<int,int> p; for(int i=0;i<v.size();++i) { p=hash_search(h.at(i),75); if(p.second!=-1) { p.first=i; break; } } cout<<"row:"<<p.first<<" col:"<<p.second<<endl; return 0; }