一、排序定义及算法概述
二、排序算法
1、冒泡排序(Bubble Sort,稳定)
2、选择排序(SelectSort,不稳定)
3、插入排序(Insertion Sort,稳定)
4、希尔排序(Shell Sort,不稳定)
5、归并排序(Merge Sort,稳定)
6、快速排序(Quick Sort,不稳定)
7、堆排序(Heap Sort,不稳定)
8、计数排序(Counting Sort,稳定)
9、桶排序(Bucket Sort,稳定)
10、基数排序(Radix Sort,稳定)
一、排序定义及算法概述
排序:通过重新排列表中的元素,使表中的元素满足按关键字有序的过程。为了查找方便,通常希望计算机中的表是按关键字有序的。
比较类排序:通过比较来决定元素间的相对次序。
非比较类排序:不通过比较来决定元素间的相对次序,可以线性时间运行。
算法的稳定性:若待排序表中有两个元素R和R,若使用某一排序算法排序后, R仍然在R的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的。算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法的性质进行描述。如果待排序表中的关键字不允许重复,则排序结果是唯一的,那么选择排序算法时的稳定与否就无关紧要。’
时间复杂度:是指算法中基本操作的执行次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
算法复杂度
二、排序算法
1、直接插入排序(InsertSort,稳定)
插入排序是一种简单直观的排序方法,其基本思想是每次将一个待排序的记录按其关键字大
小插入前面已排好序的子序列,直到全部记录插入完成。
算法描述:
1.将第一个数当作有序序列,后面的数字当作无序序列;
2.从第二个数开始往前面依次比较,如果该数大于新数则将该数移到新数的下一个位置,小于则向前寻找,直到找到比该数小于或者等于的新元素的后面位置并插入;
3.将排列好的序列认定为有序序列,重复上述操作
算法优化:折半插入排序,①从前面的有序子表中查找出待插入元素应该被插入的位置;②给插入位置腾出空间,将待插入元素复制到表中的插入位置
动图演示
代码实现
void InsertSort(ElemType A[],int n)
{
int i,j;
for(i=2;i<=n;i++) //依次将A[2]~A[n]插入前面已排序序列
{
if(A[i]<A[i-1]) { //若A[i]关键码小于其前驱,将A[i]插入有序表
A[0]=A[i] ; //复制为哨兵,A[0]不存放元素
for(j=i-1;A[0]<A[j];--j) //从后往前查找待插入位置
A[j+1]=A[j]; //向后挪位
A[j+1]=A[0]; //复制到插入位置
}
}
}
void InsertSort(ElemType A[],int n){ //优化:折半插入
int i,j,low,high,mid;
for(i=2;i<=n;i++){ //依次将A[2]~A[n]插入前面的已排序序列
A[0]=A[i]; //将A[i]暂存到A[0]
low=1;high=i-1; //设置折半查找的范围
while(low<=high){ //折半查找(默认递增有序)
mid= (low+high)/2; //取中间点
if (A[mid]>A[0]) high=mid-1; //查找左半子表
else low=mid+1; //查找右半子表
}
for(j=i-1;j>=high+1;--j)
A[j+1]=A[j]; //统一后移元素,空出插入位置
A[high+1]=A[0]; //插入操作
}
}
2、希尔排序(Shell Sort,不稳定)
希尔排序本质上是对插入排序的优化,希尔排序又叫缩小增量排序,本质还是插入排序,更适用于基本有序和数据量不大的排序表。每次排序都会按照增量大小将表分成多组列表进行插入排序
算法描述:
1.初始增量gap为n/2,根据增量大小将待排序列分成多组序列
2.每组序列按照简单插入排序进行排列
3.增量缩减为初始增量的gap/2,继续1-2操作
动图演示
代码实现
void ShellSort(ElemType A[],int n) {
//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
for (gap=n/2;gap>=1;gap=gap/2) //步长变化
{
for(i=gap+1;i<=n;++i)
{
if(A[i]<A[i-gap]){ //需将A[i]插入有序增量子表
A[0]=A[i] ; //暂存在A[0]
for (j=i-gap;j>0&&A[0]<A[j];j-=gap)
A[j+gap]=A[j]; //记录后移,查找插入的位置
A[j+gap]=A[0];//插入
}//if
}
}
}
3、冒泡排序(Bubble Sort,稳定)
冒泡排序是交换排序,通过从后往前(或从前往后)两两进行比较相邻元素的值,若为逆序则交换,直到序列比较完成。如第一趟冒泡是将最小的值放置在第一个位置,第二趟是第二小的值放在第二。。。依此类推
算法思想
1.从后往前倒数第一个值开始第一趟排序,比较相邻元素,小值向前移,最后最小值放在第一位
2.第二趟开始,从倒数第二个值往前比较,小值向前移,值放在第二位
3.直到最终实现序列有序
算法优化1:在每次循环完成之后进行判断本趟排序是否进行交换,若为交换则直接判定为序列有序,直接跳出循环
算法优化2:向前排序过程中每次记录最后交换位置,也就是下一趟进行排序的第一个位置,下一趟排序直接从最后到该位置就行比较交换即可
动画演示(动画为从前往后排序)![](https://img-blog.csdnimg.cn/f593d193d4a743c38b127d67c97439b5.gif)
代码实现(代码为从后往前排序)
void BubbleSort(ElemType A[],int n){
for(i=0;i<n-1;i++)
{
flag= false; //表示本趟冒泡是否发生交换的标志 优化1
for(j=n-1;j>i;j--){ //一趟冒泡过程
if(A[j-1]>A[j]){ //若为逆序
swap(A[j-1],A[j]); //交换
flag=true;
}
}
if (flag==false)
return; //本趟遍历后没有发生交换,说明表已经有序
}
//优化二:如果前半部分无序后半部分有序,可以每次记录最后交换的位置
int firstpos=0;
for(i=0;i<n-1;i++)
{
int k=firstpos;//记录本趟交换时最后进行交换的位置
flag= false;
for(j=n-1;j>k;j--){ //一趟冒泡过程
if(A[j-1]>A[j]){ //若为逆序
swap(A[j-1],A[j]); //交换
flag=true;
firstpos=j;
}
}
if (flag==false)
return;
}
}
4、快速排序(Quick Sort,不稳定)
快速排序是基于分治法的:在待排序表去一个元素pivot作为枢轴(一般取首元素),通过第一趟排序将排序表划分为两个部分,前面的小于该枢轴,后面的大于枢轴,这是一趟快速排序;然后重复将子表进行上述操作,直到排序完成
算法思想:
1.分解:以 pivot为基准将 a[low,high]划分为三段a[low,pivot-1],a[pivot] 和 a[pivot+1,high],使得a[low,p-1] 中任何一个元素小于等于a[pivot],而 a[p+1,high] 中任何一个元素大于等于 a[pivot]
2.递归求解:通过递归调用快速排序算法分别对a[low,pivot-1]和 a[pivot+1,high]进行排序
3.合并:由于对a[low,pivot-1]和 a[pivot+1,high]的排序是就地进行的,所以在a[low,pivot-1]和 a[pivot+1,high]都已排好序后,不需要执行任何计算,a[low: high] 就已经排好序了。
算法优化1之基准选择: (1)本文代码给的基准为固定基准,在排序列表基本有序的情况下一直选择第一个数为基准,则会出现每次排序都是后面代排序列比基准大,相当于冒泡排序。 (2)随机基准Random(a, low, high):基准选的最差的概率1/n,最好也是1/n,但是数组元素越多,随机数算法的效果越好,不容易出现最坏的情况 (3)三数取中:即每次基准都从[第一个元素,最中间元素,最后一个元素]中选择一个最中间元素,就可以避免待排序数组基本有序的情况,选取的基准没有随机性,处理效果最好
算法优化2: (1)序列长度达到一定大小时,使用插入排序 (2)尾递归优化 (3)聚集相同元素:在一次分割结束后,将与本次基准相等的元素聚集在一起,再分割时,不再对聚集过的元素进行分割。具体过程①在划分过程中将与基准值相等的元素放入数组两端,②划分结束后,再将两端的元素移到基准值周围。 (4)多线程处理快排
算法优化详细请参考:(12条消息) 快速排序的4种优化_快速排序优化_Tyler_Zx的博客-CSDN博客
动画演示
代码实现
int Partition(ElemType A[],int 1ow, int high){ //一趟划分
ElemType pivot=A[low]; //将 当前表中第一个元素设为枢轴,对表进行划分
while (low<high) { //循环跳出条件
while (1ow<high&&A [high]>=pivot) --high;
A[low]=A[high] ; //将比枢轴小的元素移动到左端
while (1ow<high&&A[low]<=pivot) ++low;
A[high]=A[low] ; //将比枢轴大的元素移动到右
}
A[low]=pivot; //枢轴元素存放到最终位置
return low; //返回存放区期的最终位置
}
void QuickSort(ElemType A[],int low,int high) {
if (low<high) {//递归跳出的条件
//Partition()就是划分操作,将表A[low...high]划分为满足上述条件的两个子表
int pivotpos=Partition(A,low,high); //划分
QuickSort(A, low,pivotpos-1); //依次对两个子表进行递归排序
QuickSort(A, pivotpos+1,high);
}
5、简单选择排序(SelectSort,不稳定)
选择排序是通过比较选择出最小的值,再将最小值和第一个值进行交换
算法思想:
1.第一趟:从左到右进行比较,将最小值记录在min中,如果最小值不等于初始位置则交换俩值
2.第二趟:从第二个值开始执行上述操作
3.直到序列有序即可
算法优化:每次选出最小值和最大值并放入对应位置
动画演示
代码实现
void SelectSort(ElemType A[],int n){
for(i=0;i<n-1;i++){ //一共进行n-1趟
min=i; //记录最小元素位置,
for(j=i+1;j<n;j++) //在A[i...n-1]中选择最小的元素
if(A[j]<A[min]) min=j; //更新最小元素位置
if (min!=i) swap(A[1],A[min]); //封装的swap()函数共移动元素3次
}
}
//优化:同时将最大最小值进行选择排序
void SelecSort(ElemType A[],int n){
int left=0;
int right=n-1;
while(left<right)
{
int min=left;
int max=right;
for(int i=left;i<=right;i++)
{
if(arr[i]<arr[min])
min=i;
if(arr[i]>arr[max])
max=i;
}
swap(arr[max],arr[right]);
if(min==right)//当最大值在最小位置,最小值在最大位置时,更换一次就成功了
min=max; //此时直接将min指向最小值所在的位置,此时后续交换实际无效
swap(arr[min],arr[left]);
left++;
right--;
}
}
6、堆排序(HeapSort,不稳定)
堆的定义如下,n个关键字序列L[...n]称为堆,当且仅当该序列满足:
①L(i)>=L(2i)且L(i)>=L(2i+1) 或②L(i)<=L(2i) 且L(i)<=L(2i+1) (1si≤L Ln/2」)
可以将该一维数组视为一棵完全二叉树,满足条件①的堆称为大根堆(大顶堆),满足条件②的堆称为小根堆( 小顶堆);大根堆的最大值在顶端,小根堆的最小值在顶端
算法思想:
首先将存放在L[1..n]中的n个元素建成初始堆,(大顶堆为例)堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶 此时根结点已不满足大顶堆的性质,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素 如此重复,直到堆中仅剩一个元素为止。
关键是构造初始堆。n个结点的完全二叉树,最后一个结点是第Ln/2」个结点的孩子。对第Ln/2」个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对各结点(Ln/2」-1~1)为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。
动画演示
代码实现
//建立大根堆
void BuildMaxHeap (ElemType A[],int len) {
for(int i=len/2;i>0;i--) //从i=[n/2]~1,反复调整堆
HeadAdjust (A,i,len);
}
//大根堆调整
void HeadAdjust(ElemType A[],int k,int len) {
//函数HeadAdjust将元素k为根的子树进行调整
A[0]=A[k]; //A[0]暂存子树的根结点
for(i=2*k;i<=len;i*=2){ //沿key较大的子结点向下筛选
if(i<len&&A[i]<A[i+1])
i++; //取key较大的子结点的下标
if(A[0]>=A[i]) break; //筛选结束
else {
A[k]=A[i]; //将A[i]调整到双亲结点上
k=i; //修改k值,以便继续向下筛选
}
}
A[k]=A[0] ; //被筛选结点的值放入最终位置
}
//下面是堆排序算法:
void HeapSort(ElemType A[],int len) {
BuildMaxHeap(A,len); //初始建堆
for(i=len;i>1;i--){ //n-1趟的交换和建堆过程
Swap(A[i],A[1]); //输出堆顶元素(和堆底元素交换)
HeadAdjust(A,1,i-1); //调整,把剩余的i-1个元素整理成堆
}
}
7、归并排序(MergeSort,稳定)
“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。假定待排序表含有n个记录,则可将其视为n个有序的子表,每个子表的长度为I,然后两两归并,得到「n/21个长度为2或1的有序表;继续两两.....如此重复,直到合并成一个长度为n的有序表为止,这种排序方法称为2路归并排序。
算法思想
分解:将含有n个元素的待排序表分成各含n/2 个元素的子表,采用2路归并排序算法对两个子表递归地进行排序。
合并:合并两个已排序的子表得到排序结果。
动画演示
代码实现
ElemType *B=(ElemType*)malloc((n+1)*sizeof(ElemType)); //辅助数组B
void Merge(ElemType A[],int low,int mid,int high){
//表A的两段A[1ow..mid]和A[mid+..high]各自有序,将它们合并成一个有序表
for(int k=low;k<=high;k++)
B[k]=A[k] ; //将A中所有元素复制到B中
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(B[i]<=B[j]) //比较B的左右两段中的元素
A[k]=B[i++]; //将较小值复制到A中
else
A[k]=B[j++] ;
}//for
while(i<=mid) A[k++]=B[i++]; //若第一个表未检测完, 复制
while(j<=high) A[k++]=B[j++];. //若第二个表未检测完,复制
}
void MergeSort(ElemType A[],int low,int high){
if(1ow<high){
int mid=(low+high)/2;
MergeSort(A,low,mid); //对左侧子序列进行递归排序
MergeSort(A,mid+1,high); //对右侧于序列进行递月排序
Merge (A,low,mid,high); //归并
}//if
}
8、计数排序(CountingSort,稳定)
计数排序的原理:统计数组中每个数出现的次数,数组中每个数排序后的位置,就是比它小的数的出现次数累加和,只能适用于数值型的数组
动画演示
代码实现
void countingSort(int a[],n) {
for(int min=0,int max=0,int i=0;i<n;i++){//先找出数组中的最大值与最小值
if(a[i]<min) min=a[i];
if(a[i]>max) max=a[i];
}
int count[max+1]; //初始化统计数组,统计各元素出现的次数
Index=0; //辅助数组读取数字进行排序
countLen=max+1;
for(int i=0;i<n;i++) { //将各数的数量存储在bucket数组
count[a[i]]++; //每次计数加1
}
for(int j=0;j<countLen;j++){//将count中的数按照大小全部提出进行排列
while(count[j]>0){
a[Index++]=j;
count[j]--;
}
}
}
9、桶排序(BucketSort,稳定)
桶排序(箱排序)是一种基于分治思想、效率很高的排序算法,理想情况时间复杂度为O(n),工作的原理是将数组分到有限数量的桶里,每个桶再个别排序(可用其他排序算法或递归使用桶序),最后依次把各个桶中的记录列出成有序序列。
动画演示
代码实现
void bucketSort(vector<int>& arr){
//初始化桶,桶的个数及开辟空间
int min=0,max=0;
for (int i=1;i<n;i++){//计算数组的最大最小值
if(a[i]>a[max]) max=i;
if(a[i]<a[min]) min=i;
}
int amax=arr[max],amin=arr[min];
int count=(amax-amin)/n+1;//根据得到的最大最小值算出桶数
vector<vector<int>> bucket(count,vector<int>());//开辟桶空间
//将元素映射到每个桶中,按照值区间均匀分布的思想进行映射
for (int i = 0; i < n; i++) {
int k = (arr[i]-amin)/ n;//映射到需要放在哪一个桶
bucket[k].push_back(arr[i]);//放入桶中
}
arr.clear();//清空数组空间
//桶内插入排序,然后插入拼接
for (int i=0;i<count;i++){
insertSort(bucket[i]);
arr.insert(arr.end(),bucket[i].begin(),bucket[i].end());
}
}
//插入排序:将无序序列插入到有序序列中,先记录插入位置最后再插入
void insertSort(vector<int> arr){
int n=arr.size();
for(int i=1;i<n;i++) //第一个当作有序序列,从第二个开始
{
int pre=i-1; //记录当前有序序列最后一个
int cur=arr[i]; //记录当前需要进行插入的数字
while(pre>=0 && cur<arr[pre]){//先移动后插入
arr[pre+1]=arr[pre];
pre--;
}
arr[pre+1]=cur;
}
}
10、基数排序(Radix Sort,稳定)
基数排序是一种很特别的排序方法,它不基于比较和移动进行排序,而基于关键字各位的大小进行排序。基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。基数排序是桶排序的扩展,速度快,占用内存很大
为实现多关键字排序,通常有两种方法:第一种是最高位优先法,按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列。第二种是最低位优先(LSD) 法,按关键字权重递增依次进行排序,最后形成一个有序序列
算法思想:
如图片所示,第一趟:先进行个位分配,按照顺序依次将其放入个位数字对应的队列中,按照先进先出进行收集,先从下往上依次取出;第二趟:进行十位上分配,从前往后依次放入对应的队列,进行收集,从下往上取出,最终得到排序序列
动画演示
代码实现
void radixSort(int arr[],int n){
//计算最大值的位数
int max=a[0]
for(i=0;i<n;i++)//求数组最大值
if(a[i]>a[0]) max=a[i];
int digit=0;
int base=1;
while (max/base>0){
digit++;
base *= 10;
}
//需要排序的轮次数就是位数
base = 1;
for (int i=0;i<digit;i++){
//统计出现次数
int bucket[10];//设置10个桶0~9
for(int j=0;j<n;j++)
bucket[(arr[j]/base)%10]++;
//使用累加数组
for(intj=1;j<10;j++)
bucket[j]+=bucket[j-1];
//根据累加数组 反向重建数组
vector<int> temp(n);
for (int k = n - 1; k >= 0; k--) {
temp[--bucket[(arr[k] / base) % 10]] = arr[k];
}
//将排好序的元素覆盖到原数组
arr.assign(temp.begin(), temp.end());
base *= 10;
}
}
图片参考:十大经典排序算法(动图演示)
代码参考:王道数据结构
桶排序基数排序代码来自(8条消息) 十大排序算法及优化 ( C++简洁实现)_c++ 桶排序优化_阿祖_in_coding的博客-CSDN博客