目录
【快速排序(Quick Sort)】(不稳定)
【希尔排序(Shell Sort)】(不稳定)
【堆排序(Heap Sort)】(不稳定)
【前言】
所有代码段都以升序为例,数组下标从0开始。排序的稳定性即:任意两个相等的数据,排序前后的相对位置不发生变化。
【冒泡排序(Bubble Sort)】
它重复地访问过要排序的元素序列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。访问元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经完成排序。
因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
for(int i=0;i<n-1;i++)
for(int j=0;j<n-1-i;j++)
if(a[j]>a[j+1]) swap(a[j],a[j+1]);
排序过程如下:
原序列:6 8 1 4 5 3 7 2
第一次:6 1 4 5 3 7 2 8
第两次:1 4 5 3 6 2 7 8
第三次:1 4 3 5 2 6 7 8
第四次:1 3 4 2 5 6 7 8
第五次:1 3 2 4 5 6 7 8
第六次:1 2 3 4 5 6 7 8
第七次:1 2 3 4 5 6 7 8
我们会发现,每一次排序之后至少有一个元素会到最终位置上,就是每次排序的元素中的最大元素。
小优化:当在一次排序过程中交换次数为0时,说明已经有序,可提前结束冒泡排序。
冒泡排序是稳定的排序。最好时间复杂度为O(n),最差时间复杂度为O(n^2),平均时间复杂度为O(n^2)。
【快速排序(Quick Sort)】
快速排序的基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序分为数组划分和递归排序两个步骤。
1.数组划分
选取一个基值,将数组分为大于基值以及小于基值两部分,并返回基值所在位置以利用于递归划分。
对数组a,设需要划分的一段为a[p]~a[r],我们期望得到一个q,其中p<=q<=r,使得a[p]~a[q-1] <=a[q]<= a[q+1]~a[r],这个时候原先的一段数组被分成了三部分。我们可以设基值x为这段数组的第一个元素a[p]。然后令i=p+1,j=r。当a[j]>=x时,j--;当a[j]<x时,我们需要将这个元素放到小于基值的一边,于是开始比较a[i] : 当a[i]<=x时,i++;当a[i]>x时,交换此时a[i]与a[j]的元素。判断直到i==j时结束,交换基准值与a[i]。这一部分算法复杂度为o(n) 。
2.递归排序
在对整个数组进行了划分后,我们将数组分成了两部分,一部分比基值小,一部分比基值大,并且我们知道了基值所在的位置,因此只需对划分出来的两部分进行递归排序即可。
排序过程如下:
原序列: 6 8 1 4 5 3 7 2
以6为基准:3 2 1 4 5 6 7 8 //交换2与8,最后基准值:交换3与6
以3为基准:1 2 3 4 5 6 7 8 //交换3与1
以1为基准:1 2 3 4 5 6 7 8 //无需交换
设基值为这段数组的第一个元素a[p]时进行快速排序:
#include <stdio.h>
#define maxn 105
int a[maxn],n;
void swap(int x,int y)
{
int t=a[x];
a[x]=a[y];
a[y]=t;
}
void quick_sort(int left,int right)
{
if(left>right) return;
int temp=a[left]; //存入基准数
int l=left,r=right;
while(l!=r)
{
//寻找右边小于基准数的数跟左边大于基准数的数做交换
while(a[r]>=temp&&l<r) r--;
while(a[l]<=temp&&l<r) l++;
if(l<r) swap(a[l],a[r]);
}
//将基准数归位
a[left]=a[l];
a[l]=temp;
quick_sort(left,l-1); //继续处理左边
quick_sort(l+1,right); //继续处理右边
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
quick_sort(1,n);
for(int i=1;i<=n;i++) printf(i==n?"%d\n":"%d ",a[i]);
return 0;
}
对于分治算法,当每次划分时,若都能分成两个等长的子序列,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。除了取序列的第一个或最后一个元素作为基准,还可以随机选取基准和三数取中选择基准。
小优化:当待排序序列的长度分割到一定大小后,使用插入排序。
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排。
快速排序是不稳定的排序方法。最差时间复杂度为O(n^2),平均时间复杂度为O(nlogn)。
【插入排序(Insert Sort)】
插入排序的基本操作就是将一个元素插入到已经排好序的有序序列中,从而得到一个新的、个数加一的有序序列。类似打扑克时的按大小整理扑克的步骤,每步将一个待排序的元素按其关键值的大小插入前面的有序序列中的适当的位置上(有序序列中,该适当位置的后面位置的元素,都往后移动一位,腾出该适当位置),直到全部插入完为止。
for(int i=1;i<n;i++)
{
int j=i-1,t=a[i];
while(a[j]>t&&j>=0) //大于位置i的元素往后移动一位
{
a[j+1]=a[j];
j--;
}
a[j+1]=t;
}
排序过程如下:
原序列:6 8 1 4 5 3 7 2
第一次:6 8 1 4 5 3 7 2
第两次:1 6 8 4 5 3 7 2
第三次:1 4 6 8 5 3 7 2
第四次:1 4 5 6 8 3 7 2
第五次:1 3 4 5 6 8 7 2
第六次:1 3 4 5 6 7 8 2
第七次:1 2 3 4 5 6 7 8
插入排序是稳定的排序。最好时间复杂度为O(n),最差时间复杂度为O(n^2),平均时间复杂度为O(n^2)。
【希尔排序(Shell Sort)】
希尔排序是插入排序的一种,又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版。
希尔排序是把整个数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个分组恰被分成一组,算法便终止。
for(int k=n/2;k>0;k/=2)
{
for(int i=k;i<n;i++)
{
int j=i;
if(j-k>=0&&a[j]<a[j-k])
swap(a[j],a[j-k]),j-=k;
}
}
排序过程如下:
原序列:6 8 1 4 5 3 7 2
第一次:5 3 1 2 6 8 7 4 //增量为4
第两次:1 2 5 3 6 4 7 8 //增量为2
第三次:1 2 3 5 4 6 7 8 //增量为1
希尔排序是非稳定的排序。对希尔排序的时间复杂度分析很困难,在特定情况下可以准确的估算排序码的比较次数和元素移动的次数,但要想弄清楚排序码比较次数和元素移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。
【选择排序(Selection Sort)】
选择排序是一种简单直观的排序算法,它的工作原理是每一次从待排序的元素中选出最小(或最大)的一个元素,与已排序序列的后一个位置进行交换,使得已排序的序列长度+1,重复操作直到没有元素待排序。
for(int i=0;i<n-1;i++)
{
int t=i;
for(int j=i+1;j<n;j++)
if(a[j]<a[t]) t=j;
swap(a[i],a[t]);
}
排序过程如下:
原序列:6 8 1 4 5 3 7 2
第1次: 1 8 6 4 5 3 7 2
第2次: 1 2 6 4 5 3 7 8
第3次: 1 2 3 4 5 6 7 8
第4次: 1 2 3 4 5 6 7 8
第5次: 1 2 3 4 5 6 7 8
第6次: 1 2 3 4 5 6 7 8
第7次: 1 2 3 4 5 6 7 8
选择排序是不稳定的排序方法。最差时间复杂度为O(n^2),平均时间复杂度为O(n^2)。
【堆排序(Heap Sort)】
堆排序的算法是选择排序的优化版本。堆排序在寻找最小值(或最大值)的过程中使用了堆这种数据结构,提高了效率。堆是一个近似二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
堆是什么?如何建堆?堆如何插入删除和调整?传送门:二叉树和堆(理论)
那么我们要如何实现堆排序呢?下面以升序排序为例讲解。
算法一:
将需要排序的序列建堆,调整成最小堆。
每次删除并输出堆顶结点的值即最小值,再将最后一个数放置于堆顶结点的位置,最小堆的大小-1,向下调整成最小堆。重复上述步骤直到堆为空。
这样得到的输出序列即是升序。
算法二:
将需要排序的序列建堆,调整成最大堆。
每次把堆顶结点的值即最大值与最后一个结点交换位置,最大堆的大小-1,向下调整成最大堆。重复上述步骤直到结束。
最后层序遍历输出堆排序后的堆,即为升序序列。
下面为算法二的代码,以升序堆排序为例:
#include <stdio.h>
#define maxn 105
int h[maxn];
int n;
//交换函数
void swap(int x,int y)
{
int t=h[x];
h[x]=h[y];
h[y]=t;
}
//向下比较调整成最大堆
void siftdown(int pos,int num)
{
int t,flag=0; //flag用来标记是否需要继续向下调整
while(!flag)
{
int t=pos; //用t记录父结点和左右儿子中值较大的结点编号
if(pos*2<=num&&h[t]<h[pos*2]) t=pos*2;
if(pos*2+1<=num&&h[t]<h[pos*2+1]) t=pos*2+1;
//如果最大的结点不是父结点
if(t!=pos)
{
swap(t,pos);
pos=t;
}
else flag=1;
}
}
void create()
{
//从最后一个非叶结点到第1个结点依次进行向下调整
for(int i=n/2;i>=1;i--)
siftdown(i,n);
}
//堆排序(升序)
void heapSort()
{
create(); //建堆
int num=n;
for(int i=n;i>1;i--)
{
swap(1,i); //交换最大值与最后一个数
num--;
siftdown(1,num); //前num个数调整成最大堆
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
heapSort();
for(int i=1;i<=n;i++) printf(i==n?"%d\n":"%d ",h[i]);
return 0;
}
堆排序是不稳定的排序。最差时间复杂度为O(nlogn),平均时间复杂度为O(nlogn)。
【归并排序(Merge Sort)】
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。
首先考虑如何将二个有序数列合并。这个非常简单,只要比较两个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果其中一个数列为空,那直接将另一个数列的数据依次取出即可。
而归并排序的基本思路就是将数组依次分成2组A、B,如果这2组组内的数据都是有序的,那么就可以将这2组数据进行如上操作排序。那么如何让这2组数据有序呢?可以将A、B组各自再分成2组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组已经达到了有序,然后再合并相邻的2个小组就可以了。这样通过先递归分解数列,再合并数列就完成了归并排序。
#include <stdio.h>
#define maxn 105
void solve(int *a,int l,int mid,int r) //治:哪边小先取哪边的元素
{
int b[maxn];
int i=l,j=mid+1,k=l;
while(i<=mid&&j<=r)
{
if(a[i]<a[j]) b[k++]=a[i++];
else b[k++]=a[j++];
}
while(i<=mid) b[k++]=a[i++];
while(j<=r) b[k++]=a[j++];
for(i=l;i<=r;i++) a[i]=b[i];
}
void Merge_Sort(int *a,int l,int r) //分:递归分解再合并
{
if(l<r)
{
int mid=(l+r)/2;
Merge_Sort(a,l,mid);
Merge_Sort(a,mid+1,r);
solve(a,l,mid,r);
}
}
main()
{
int n,a[maxn]; scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
Merge_Sort(a,0,n-1);
for(int i=0;i<n;i++) printf(i==n-1?"%d\n":"%d ",a[i]);
}
归并排序是稳定的排序。最差时间复杂度为O(nlogn),平均时间复杂度为O(nlogn)。
【基数排序(Radix Sort)】
要讲基数排序,首先我们先来讲讲桶排序(Bucket Sort)。
假设我们有 N 个学生,他们的成绩是0到100之间的整数(于是最多有 M = 101 个不同的成绩),如何在线性时间内将学生的成绩排序输出?
我们可以用一个大小为101的数组a,a[i]存放成绩为i的学生人数,最后遍历输出即可。时间复杂度T(N, M) = O( M+N )。
那么当M>>N时,桶排序显然不再适用,那么基数排序就出现了。
基数排序属于"分配式排序"(distribution sort),又称"桶子法",顾名思义,它是通过元素的部分关键码,将要排序的元素分配至某些"桶"中,藉以达到排序的作用。
基数排序分为两类:第一类是最低位优先法,简称LSD法,即先从最低位开始排序,再对次低位进行排序,直到对最高位排序后得到一个有序序列。第二类是最高位优先法,简称MSD法,先从最高位开始排序,再对次高位进行排序,直到对最低位排序后得到一个有序序列。
当对给定序列进行次位优先(LSD)排序时,过程如下:
原序列: 64,8,216,512,27,729,0,1,343,125
桶子: 0 1 2 3 4 5 6 7 8 9
按个位: 0,1,512,343,64,125,216,27,8,729
按十位: 0,512,125 343 64
1,216, 27,
8 729
按百位: 0 125 216 343 512 729
1
8
27
64
最终序列:0 1 8 27 64 125 216 343 512 729
基数排序是稳定的排序方法。P为最大位数,N为数的数目,B为桶子的大小,最差时间复杂度为O(P(N+B)),平均时间复杂度为O(P(N+B))。