排序(插入排序、选择排序、交换排序、二路归并排序、基数排序、外部排序)
一、插入排序
1. 直接插入排序
算法思想:每趟排序将一个待排序的关键字按照其值大小插入到已经安排好的部分有序序列的适当位置上,直到所有待排关键字都被插入到有序序列中为止。
最好情况:初始序列已经有序
- 时间复杂度:O(n)
最坏情况:初始序列逆序
- 执行次数:\(\frac{n(n-1)}{2}\)
- 时间复杂度:O(\(n^{2}\));
平均时间复杂度:O(\(n^{2}\))
空间复杂度:O(1)
稳定性:稳定
代码
void InsertSort(int R[],int n){
int i,j;
int temp;
for(int i=1;i<n;i++){
temp=R[i];
j=i-1;
while(j>=0&&temp<R[j]){
R[j+1]=R[j];
j--;
}
R[j+1]=temp;
}
}
2. 折半插入排序
算法思想,用二分法寻找插入位置,在查找方面大大减少时间,移动方面同直接插入排序
最好情况:初始序列已经有序
- 时间复杂度:O(\(nlog{_{2}}^{n}\))
最坏情况:初始序列逆序
- 执行次数 :\(\frac{n(n-1)}{2}\)
- 时间复杂度:O(\(n^{2}\))
平均时间复杂度:O(\(n^{2}\))
空间复杂度:O(1)
稳定性:稳定
代码
void BBSort(int arr[],int n){
int low,high,mid,place;
for(int i=2;i<=n;i++){
low=1;
high=i-1;
arr[0]=arr[i];
while(low<=high){
mid=(low+high)/2;
if(arr[0]<arr[mid])high=mid-1;
else low=mid+1;
}
place=low;
for(int j=i-1;j>place;j--){
arr[j+1]=arr[j];
}
arr[place]=arr[0];
}
}
3. 希尔排序
算法思想:也叫做缩小增量排序,将待排序序列按某种规则分成几个子序列,分别对这几个子序列进行直接插入排序,规则体现在于增量的选取,如果增量是1,就是直接插入排序。
希尔排序的时间复杂度与增量的选取有关。
先追求表中元素部分有序,再逐渐逼近全局变量
1)希尔自己提出的
n/2,n/4…n/2^k…2,1,即每次将增量除以2并且向下取整。
此时时间复杂度O(n^2)
2)帕佩尔诺夫 斯塔舍维奇:
2^k+1,…65,33,17,9,5,3,1
此时时间复杂度大约为O(n^1.5)
增量选取的注意:
1)增量序列最后一个一定为1。
2)增量序列中的值应尽量没有除1之外的公因子
空间复杂度:O(1).
稳定性:不稳定
二、交换类排序
1. 冒泡排序
算法思想:遍历序列,第一个和第二个比较,如果比第二个大,则交换位置,再与第三个比较,这样一趟之后最大的元素被换至最后一位,再进行多次循环,直至序列有序。
最坏情况:元素初始逆序,
- 基本操作执行次数为n(n-1)/2
- 时间复杂度O(n^2)
最好情况:初始序列有序,不发生交换,此时时间复杂度O(n)
平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
代码
void BubbleSort(int R[],int n){
int i,j,flag,temp;
for(int i=n-1;i>=1;i--){
flag=0;
for(j=1;j<=i;j++){
if(R[j-1]>R[j]){
temp=R[j];
R[j]=R[j-1];
R[j-1]=temp;
flag=1;
}
}
if(flag=0)return;//没有关键字交换则证明已经有序
}
}
2. 快速排序
算法思想:每一趟选择当前子序列的一个关键字作为枢轴,子序列中比枢轴小的移到左边,比枢轴大的移到枢轴的右边,每一趟会得到一组更短的子序列,作为下一趟的初始序列。
最坏情况:初始序列逆序,时间复杂度O(\(n^{2}\))
最好情况:初始序列无序,时间复杂度O(\(nlog_{2}^{n}\))
平均时间复杂度: O(\(nlog_{2}^{n}\))
平均情况下是所有排序中最快的,待排序序列越有序,算法效率越低
空间复杂度:O(\(log_{2}^{n}\))
稳定性:不稳定
//划分函数
void partition(int arr[],int,low,int high,int &i){
int j,temp;
i=low;j=high;temp=arr[i];
while(i<j){
while(i<j&&arr[j]>=temp)j--;
if(i<j){
arr[i]=arr[j];
i++;
}
while(i<j&&arr[i]<temp)i++;
if(i<j){
arr[j]=arr[i];
j--;
}
}
arr[i]=temp;
}
//递归快速排序
void quickSort(int arr[],int low,int high){
int i;
if(low<high){
partition(arr,low,high,i);
quickSort(arr,low,i-1);
quickSort(arr,i+1,high);
}
}
//非递归快速排序(手动实现系统栈的功能)
void quickSortNonrecursion(int arr[],int n){
int i,low,high;
int stack[maxSize][2],top=-1;
low =0;high=n-1;
stack[top][0]=low;
stack[top][1]=high;
while(top>=0){
low=stack[top][0];
high=stack[top][1];
top--;
partition(arr,low,high,i);
if(low<high){
top++;
stack[top][0]=low;//左边部分的上下限
stack[top][1]=i-1;
top++;
stack[top][0]=i+1;//右边部分的上下限
stack[top][1]=high;
}
}
}
三、选择类排序
1. 简单选择排序
算法思想:通过从头至尾扫描序列,选出最小的一个关键字,和第一个关键字交换,直到最终有序
执行次数:n(n-1)/2(固定)
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定(链表为载体的简单选择排序是稳定的)
void SelectSort(int R[],int n){
int i,j,k;
int temp;
for(i=0;i<n;i++){//找出无序序列中最小的一个
k=i;
for(j=i+1;j<n;j++){
if(R[k]>R[j])k=j;
}
temp=R[i];
R[i]=R[k];
R[k]=temp;
}
}
2. 堆排序
算法思想:将序列不断的调整为堆,不断的调整,使得不符合堆定义的完全二叉树变成符合堆定义的
完全二叉树,每次调整完成交换第一个与最后一个元素,再对前n-1个元素重新调整,直至序列有序。
时间复杂度:O(\(nlog_{2}^{n}\))
最好最坏情况时间复杂度均为O(\(nlog_{2}^{n}\))
空间复杂度:O(1)
堆排序在所有时间复杂度为O(log2n)的排序算法中空间复杂度最低,适用于关键字很多的情况,从多少个关键字中选出几个最大(最小)的关键字
稳定性:不稳定
void Shift(int R[],int low,int high){
//在[low,high]范围内对low位置上的节点进行调整
//在Low位置上的节点要调整到比其左右孩子都大 则调整结束
int i=low,j=2*i;
int temp=R[i];//j是i的左孩子节点
while(j<=high){
if(j<high&&R[j]<R[j+1])j++;//右孩子较大 指向右孩子
if(temp<R[j]){
//孩子大于双亲,将孩子R[j]调整到双亲的位置上
R[i]=R[j]
//继续向下调整
i=j;
j=2*i;
}
else break;
}
R[i]=temp;
}
void heapSort(int R[],int n){
int i;
int temp;
for(int i=n/2;i>=1;i--)Shift(R,i,n);//建立初始堆
for(int i=n;i>=2;i--){
//换出根节点中关键字,放入最终位置
temp=R[1];
R[i]=R[i];
R[i]=temp;
Shift(R,1,i-1);//继续调整
}
}
四、归并类排序
1. 二路归并排序
算法思想:先将整个序列分成两段,分别对每一段进行归并排序,将得到两个有序序列,再将这两个序列归并成一个序列
时间复杂度:最好最坏情况下均为O(nlog2n),时间复杂度与初始序列无关
空间复杂度:O(n)
稳定性:稳定
const int maxn = 100;
void merge(int A[], int L1, int R1, int L2, int R2) {
//将数组A的[L1,R1][L2,R2]合并成有序区间(L2为R1+1)
int i = L1, j = L2;
int temp[maxn], index = 0;
while (i <= R1 && j <= R2) {
if (A[i] <= A[j])temp[index++] = A[i++];
else temp[index++] = A[j++];
}
while (i <= R1)temp[index++] = A[i++];
while (j <= R2)temp[index++] = A[j++];
for (int i = 0; i < index; i++) {
A[L1 + i] = temp[i];//将合并后的数组赋值会A
}
}
void mergeSort(int A[], int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(A, left, mid);
mergeSort(A, mid + 1, right);
merge(A, left, mid, mid + 1, right);
}
}
void mergeSort(int A[],int n) {//非递归
/*
令步长step的初值为2,将数组中每step个元素作为一组,内部排序,将左step/2与右step/2个元素进行合并。
(若元素个数不超过step/2则不操作),再令step*2,继续上面的操作,直至step/2超过n(只有一组)
//具体用的时候要考虑数组下标是从0开始还是从1开始
*/
for (int step = 2; step / 2 <= n; step *= 2) {
for (int i = 1; i <= n; i += step) {
int mid = i + step / 2 - 1;
if (mid + 1 <= n) {//右区间存在则合并
merge(A, i, mid, mid + 1, min(i + step - 1, n));
}
}
}
}
void mergeSort(int A[]) {
for (int step = 2; step / 2 <= n; step *= 2) {
//如果只需要归并排序每一步的结果,可以对子区间调sort函数
for (int i = 1; i <= n; i += step) {
sort(A+ i, A + min(i + step, n+1));
}
}
}
2. 基数排序
时间复杂度:O(d(n+rd)),空间复杂度 O(rd)
n为关键字个数,d为关键字位数,rd为关键字基的个数(构成关键字的符号)
适合关键字很多,关键字的取值范围很小
稳定性:稳定
总结
- 时间复杂度
O(nlog2n):快速排序,希尔排序,归并排序,堆排序
快排在最坏情况下是O(n^2),其余均为O(lnlog2n)
O(n^2):直接插入排序,折半插入排序,简单选择排序,冒泡排序
直接插入排序与冒泡排序在最好情况下为O(n),其余一样 - 空间复杂度
快速排序O(log2n)(基于栈),归并排序O(n),基数排序O(rd),其余均为O(1) - 稳定性
不稳定:快速排序,希尔排序,简单选择排序,堆排序
(以链表为载体的简单选择排序是稳定的)
其余稳定 - 每经过一次排序,能保证一个关键字到达最终位置:交换类(冒泡,快排),选择类(简单选择,堆)
- 排序算法的关键字比较次数与原始序列无关:简单选择排序,折半插入排序
- 排序趟数与原始序列有关:交换类排序(冒泡,快排)
比较次数与序列初态无关的算法是:二路归并排序、简单选择排序、基数排序、二分法插入
比较次数与列初态有关的算法是:快速排序、直接插入排序、冒泡排序、堆排序、希尔排序
排序趟数与列初态无关的算法是:直接插入排序、折半插入排序、希尔排序简单选择排序、归并排序、基数排序
排序趟数与序列初态有关的算法是:冒泡排序、快速排序
时间复杂度与序列初态无关的算法是:直接选择排序
不能保证每趟排序至少有一个元素放在其最终位置上:shell排序、归并排序
平均性能而言,最好的内部排序方法是:快速排序
最后一趟开始前,所有元素都可能不在最终位置上:插入排序