排序算法专题

文章目录

一、排序的基本概念

算法的稳定性
  • 关键字相同的元素经过排序后相对顺序是否会改变
内部排序与外部排序
  • 内部排序:数据都在内存中----关注时间、空间复杂度、稳定性
  • 外部排序:数据太多,无法全部放入内存----还需关注磁盘读取次数

二、插入排序

直接插入排序
  • 每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中
  • 平均时间复杂度:O(n^2)
  • 算法稳定性:稳定
c 复制代码
void InsertSort(int A[],int n){
    int i,j,temp;
    for(i=1;i<n;i++){
        if(A[i]<A[i-1]){
            temp=A[i];
            for(j=i-1;j>=0&&A[j]>temp;--j){
                A[j+1]=A[j]; // 从后往前检查,比当前元素大的右移
            }
            A[j+1]=temp;
        }
    }
}
希尔排序
  • 先追求表中元素部分有序,再逐渐逼近全局有序
  • 仅适用于顺序表
  • 算法过程:
    • 相隔增量d的元素组成同一个子表,对各个子表分别进行直接插入排序,缩小增量d,直到d=1为止
  • 算法稳定性:不稳定
    • 相同关键字划分到不同子表,可能改变相对次序
c 复制代码
void ShellSort(int A[],int n){
    int i,j,temp,d;
    for(d=n/2;d>=1;d=d/2){
        for(i=d;i<n;i++){
            if(A[i]<A[i-d]){
                temp=A[i];
                for(j=i-d;j>=0&&A[j]>temp;j-=d){
                    A[j+d]=A[j]; // 检查前面比当前元素大的右移
                }
                A[j+d]=temp;
            }
        }
    }
}

三、交换排序

冒泡排序
  • 从后往前(或从前往后)两两比较相邻元素的值,若为逆序则交换
  • 平均时间复杂度:O(n^2)
  • 算法稳定性:稳定
c 复制代码
void BubbleSort(int A[],int n){
    int i,j,temp,flag;
    for(i=0;i<n-1;i++){
        flag=false;
        for(j=n-1;j>i;j--){
            if(A[j]<A[j-1]){ //相等不交换保持稳定
                temp=A[j];
                A[j]=A[j-1];
                A[j-1]=temp;
                flag=true;
            }
        }
        if(flag==false)
            return; //本趟遍历后没有发生交换,说明已经有序
    }
}
快速排序
  • 用枢轴元素pivot划分待排序表
  • 算法过程
    • 两个指针lowhigh向中间移动,将小于pivot的元素放到左边,大于pivot的元素放到右边,pivot元素放到最终位置上,再对左右递归
  • 快速排序是所有内部排序算法中平均性能最优的排序算法
  • 空间复杂度:
    • 最好O(log_2n)
    • 最坏O(n)
    • 平均O(log_2n)
  • 平均时间复杂度:O(nlog_2n)
  • 稳定性:不稳定
c 复制代码
int Partition(int A[],int low,int high){
    int pivot=A[low];
    while(low<high){
        while(low<high&&A[high]>=pivot){
            --high;
        }
        A[low]=A[high]; // 比枢轴小的元素移动到左边
        while(low<high&&A[low]<=pivot){
            ++low;
        }
        A[high]=A[low]; // 比枢轴大的元素移动到右边
    }
    A[low]=pivot;
    return low;
}

void Quicksort(int A[],int low,int high){
    if(low<high){ //递归跳出条件
        int pivotpos=Partition(A,low,high);
        Quicksort(A,low,pivotpos-1);
        Quicksort(A,pivotpos+1,high);
    }

}

四、选择排序

简单选择排序
  • 每一趟在待排元素中选取关键字最小(或最大)的元素加入有序子序列
  • 平均时间复杂度:O(n^2)
  • 稳定性:不稳定
c 复制代码
void Selectsort(int A[],int n){
    int i,j,temp,min;
    for(i=0;i<n-1;i++){
        min=i;
        for(j=i+1;j<n;j++){
            if(A[j]<A[min]){
                min=j; //得到最小关键字的索引
            }
        }
        if(min!=i){
            temp=A[min];
            A[min]=A[i];
            A[i]=temp;
        }
    }
}
堆排序
  • 将待排序列整理成大根堆(或小根堆)后进行选择排序
  • 大根堆:根>=左、右
  • 建立大根堆:
    • 在顺序存储的完全二叉树中,非终端节点编号i<=⌊n/2⌋
    • 由后向前检查所有非终端节点,若不满足根>=左、右,将当前节点与更大的一个孩子互换
    • 若元素互换破坏了下一级的堆,则采用相同方法继续向下调整
    • 建堆的时间复杂度:O(n)
  • 输出堆顶元素:最后一个元素替换堆顶
  • 算法过程
    • 首先建立大根堆,不断输出堆顶元素,并将剩余元素序列再次调整为大根堆
  • 堆的插入删除
    • 插入:插入到堆底后"上升"
    • 删除:用堆底元素替换后"下坠"
  • 平均时间复杂度:O(nlog_2n)
  • 稳定性:不稳定
c 复制代码
//建立大根堆
void BuildMaxHeap(int A[],int len){
    int i;
    for(i=len/2;i>0;i--){ //从后往前调整所有非终端节点
        HeadAdjust(A,i,len);
    }
}

//将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len){
    int i;
    A[0]=A[k]; //A[0]暂存防止覆盖
    for(i=2*k;i<=len;i*=2){
        if(i<len&&A[i]<A[i+1]){
            i++; //获取左右孩子中更大的
        }
        if(A[0]>A[i])
            break;
        else{
            A[k]=A[i]; //A[i]调整到双亲结点上
            k=i; //向下继续筛选
        }
    }
    A[k]=A[0]; //注意此时k值已经改变,为该节点最终位置
}

// 堆排序
void HeapSort(int A[],int len){
    BuildMaxHeap(A,len);
    int temp;
    for(int i=len;i>1;i--){
        temp=A[i];
        A[i]=A[1];
        A[1]=temp; //堆顶元素和堆底元素互换
        HeadAdjust(A,1,i-1); //把剩余待排元素整理为大根堆
    }
}

五、归并排序

二路归并排序
  • 将相邻的两个有序表合并成一个
归并排序
  • 将n个待排元素分成两个长度相等的子序列,分别对两个子序列递归调用归并方法,再将两有序子序列二路归并排序
  • 空间复杂度:O(n)
  • 平均时间复杂度:O(nlog_2n)
  • 稳定性:稳定
c 复制代码
int *B=(int *)malloc((n+1)*sizeof(int)); // 辅助数组B

void Merge(int A[],int low,int mid,int high){// mid分割两个待归并序列
    for(int k=low;k<=high;k++){
        B[k]=A[k]; //复制到辅助数组中
    }
    int i,j,k;
    for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
        if(B[i]<=B[j]){
            A[k]=B[i++];
        }else{
            A[k]=B[j++];
        }
    }
    while(i<=mid)
        A[k++]=B[i++];
    while(j<=high){
        A[k++]=B[j++];
    }
}

void MergeSort(int A[],int low,int high){
    if(low<high){
        int mid=(low+high)/2;
        MergeSort(A,low,mid);
        MergeSort(A,mid+1,high);
        Merge(A,low,mid,high); //归并
    }
}

六、基数排序

多关键字排序
  • 按照关键字各位大小排序
  • n:待排元素个数
  • d:单个元素分量的个数
  • rd:基数,每个分量的取值范围
  • 单逻辑关键字:文件中任意一关键字k均由d个分量构成,且每个分量都是单独的关键字
  • 排序方法:
    • 最高位优先法MSD--逐层分割成若干子序列
    • 最低位优先法LSD--不必分成若干子实现排序序列,不通过关键字比较,通过"分配"+"收集"实现排序
链式基数排序
  • 算法过程:
    • 将关键字拆分为d位(或组)
    • 按照各个关键字位权重递增的次序(LSD方法),做d趟分配O(n)和收集O(rd)
  • 空间复杂度:O(rd)
  • 时间复杂度:O(d(n+rd))
  • 稳定性:稳定

七、内部排序算法的比较

  • 各算法性质比较表
排序算法 最好时间复杂度 平均时间复杂度 最坏时间复杂度 空间复杂度(平均) 是否稳定
直接(折半)插入排序 O(n) O(n2) O(n2) O(1) 稳定
希尔排序 - - - O(1) 不稳定
冒泡排序 O(n) O(n2) O(n2) O(1) 稳定
快速排序 O(nlog2n) O(nlog2n) O(n2) O(log2n) 不稳定
简单选择排序 O(n2) O(n2) O(n2) O(1) 不稳定
堆排序 O(nlog2n) O(nlog2n) O(nlog2n) O(1) 不稳定
归并排序 O(nlog2n) O(nlog2n) O(nlog2n) O(n) 稳定
基数排序 O(d(n+rd)) O(d(n+rd)) O(d(n+rd)) O(rd) 稳定
  • 规律总结

    • 每次排序确定一个最终位置:插入排序,冒泡排序,选择排序,快速排序
    • 算法复杂度与初始状态无关:选择排序、堆排序、归并排序、基数排序
    • 比较次数与初始状态无关:选择排序、基数排序
    • 移动次数与初始状态无关:归并排序、基数排序
    • 排序趟数与初始状态有关:快速排序、冒泡排序
  • 根据关键字分布情况选择排序方法

    • n较小,基本有序:直接插入排序
    • n较小,数据项较多,所占存储空间较大:简单选择排序
相关推荐
倒头就睡的小比特16 小时前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!16 小时前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼17 小时前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
m0_5474866618 小时前
《数据结构教程》全套 PPT课件2026
数据结构
旖旎夜光18 小时前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark18 小时前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her119 小时前
并查集-听课笔记
笔记·算法·并查集
码流子19 小时前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven19 小时前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark19 小时前
从算法设计模式看编程思维的抽象能力4
算法