内排序算法

news2024/9/23 17:17:53

排序算法是面试中常见的问题,不同算法的时间复杂度、稳定性和适用场景各不相同。按照数据量和存储方式可以将排序算法分为 内排序(Internal Sorting)和 外排序(External Sorting)。

内排序是指对所有待排序的数据都可以一次性加载到内存中进行排序。这意味着所有的数据都可以直接在计算机的内存中操作,无需借助外部存储设备(如硬盘)。内排序算法的设计可以更加灵活,因为内存操作速度远远高于磁盘操作速度。常见的内排序算法包括快速排序、归并排序、插入排序、冒泡排序等。

外排序是指对大量数据进行排序时,由于数据无法一次性加载到内存中,需要借助外部存储设备进行排序。通常在外排序中,数据会被分成若干个小块,每次只处理其中一部分数据,然后将部分排序好的数据存储回外部存储设备,再进行合并等操作。外排序算法需要考虑数据的分块、读写外部存储的效率,以及合并有序数据等问题。常见的外排序算法有多路归并排序、置换选择排序等。

本文介绍的都是常见的内排序算法:插入排序、冒泡排序、选择排序、希尔排序、归并排序、快速排序、堆排序、桶排序。

目录

  • 1. 插入排序
  • 2. 冒泡排序
  • 3. 选择排序
  • 4. 希尔排序
  • 5. 归并排序
  • 6. 快速排序
  • 7. 堆排序
  • 8. 桶排序

1. 插入排序

插入排序的核心是将待排序的元素逐个插入到已经排好序的序列中,以构建有序的输出序列:

void inssort(int A[],int n){
    for(int i=1;i<n;i++){
        for(int j=i;j>0;j--){
            if(A[j]<A[j-1]){        //A[j]<A[j-1](此处小于即排在前面)
                swap(A,j,j-1);
            }
            else{
                break;              //A[j]已达到正确位置(时间优化)
            }
        }
    }
}

插入排序的平均时间复杂度为 O(n2),最好情况为 O(n),最坏情况为 O(n2),适用于小规模的数据集或者已经基本有序的数据。插入排序的空间复杂度为 O(1),是一种稳定排序算法。

2. 冒泡排序

冒泡排序的核心思想是通过比较相邻的元素并交换位置,逐渐将最小的元素 “冒泡” 到正确的位置。冒泡排序的过程类似于冒泡泡沫在水中升起的过程,较大的元素会逐渐向序列的开头 “冒泡”:

void bubsort(int A[],int n){
    for(int i=0;i<n-1;i++){
        for(int j=n-1;j>i;j--){
            if(A[j]<A[j-1]){
                swap(A,j,j-1);
            }
        }
    }
}

冒泡排序的平均时间复杂度为 O(n2),最好情况和最坏情况都为 O(n2)。冒泡排序的空间复杂度为 O(1),是一种稳定排序算法。

3. 选择排序

选择排序的核心思想是在未排序序列中选择最小的元素,然后将它与未排序序列的起始位置交换,使得已排序序列逐步增长:

void selsort(int A[],int n){
    for(int i=0;i<n-1;i++){
        int index=i;
        for(int j=n-1;j>i;j--){
            if(A[j]<A[index]){
                index=j;
            }
        }
        swap(A,i,index);
    }
}

选择排序的平均时间复杂度为 O(n2),最好情况和最坏情况都为 O(n2)。选择排序的空间复杂度为 O(1),是一种稳定排序算法。

4. 希尔排序

希尔排序利用了插入排序最佳时间代价特性,在不相邻的记录之间进行交换和比较。核心思想是将整个序列分成多个较小的子序列来逐步减少序列的无序度,从而在最后一步进行一次插入排序,达到提高排序速度的效果。过程如图:
在这里插入图片描述

void inssort2(int A[],int n,int incr){      //n为传入数组长度,incr为数组内元素间隔
    for(int i=incr;i<n;i+=incr){            //incr相当于第1位元素,+incr相当于间隔
        for(int j=i;(j>=incr)&&(A[j]<A[j-incr]);j-=incr){
            int tmp=A[j];
            A[j]=A[j-incr];
            A[j-incr]=tmp;
            cnt1++;
        }
    }
}
void shellsort(int A[],int n){
    for(int i=n/2;i>2;i/=2){            //i表示组数或者间隔
        for(int j=0;j<i;j++){           //j表示有i组数据时从0到i遍历
            inssort2(&A[j],n-j,i);      //对第j组数据进行插入排序(起始位置,长度,间隔)
        //第j组数据在A中下标:j,j+i,j+2i...用inssort2中传入参数表示为:&A[j],&A[j]+i,&A[j]+2i...
        }
    }
    inssort2(A,n,1);                    //对整个数组插入排序
}

希尔排序在现实中没有对应的直观解释,也无法证明其时间复杂度,一般认为平均时间复杂度为 O(n1.5),最好情况为 O(nlogn),最坏情况为 O(n2)。希尔排序的空间复杂度为 O(1),是一种不稳定排序算法。

5. 归并排序

归并排序的核心思想是 “二分+合并”,即将一个未排序的数组分割成两个子数组,递归地对这两个子数组排好序后再合并为一整个有序的数组:

void mergesort(int A[],int tmp[],int left,int right){	//left表示A[0]下标,right表示A[n-1]下标
    if(left==right) return;
    int mid=(left+right)/2;
    mergesort(A,tmp,left,mid);      //向下递归,二分集合并排序
    mergesort(A,tmp,mid+1,right);
    for(int i=left;i<=right;i++){
        tmp[i]=A[i];
    }
    int i1=left,i2=mid+1;           //i1,i2表示两个待合并数组(各自都已排序)首个未排序元素下标
    for(int curr=left;curr<=right;curr++){
        if(curr==mid+1){            //left数组已完成合并
            A[curr]=tmp[i2++];
        }
        else if(i2>right){          //right数组已完成合并
            A[curr]=tmp[i1++];
        }
        else if(tmp[i1]<tmp[i2]){
            A[curr]=tmp[i1++];
        }
        else{
            A[curr]=tmp[i2++];
        }
    }
}

归并排序的平均时间复杂度为 O(nlogn),最好情况和最坏情况也都为 O(nlogn)。归并排序的空间复杂度为 O(n),是一种稳定排序算法。

6. 快速排序

快速排序的核心思想是选择一个轴值 pivot,将数组分为小于轴值的部分和大于轴值的部分,然后递归地对这两部分进行排序。轴值的选择会影响算法的效率,一般选取数组的中间点作为轴值。为了设计方便,会将轴值置于数组末端,待排好序后再移动到合适位置:

//partition函数将数组的l+1~r-1划分为两个以轴值为分界的部分,并返回轴值的下标(实际上该位元素与最后一位交换后才是真正的轴值下标)
int partition(int A[],int l,int r,int& pivot){  //l,r为A[]的左右范围,pivot为轴值
    do{
        while(A[++l]<pivot);                    //跳过满足小于轴值的元素,终止在大于轴值的地方
        while((l<r)&&(A[--r]>pivot));
        int tmp=A[r];
        A[r]=A[l];
        A[l]=tmp;
    }while(l<r);
    return l;
}

void qsort(int A[],int i,int j){
    if(i>=j)    return;
    int pivotindex=(i+j)/2;                 //选取轴值
    int pivot=A[pivotindex];
    A[pivotindex]=A[j];						//将轴值与末尾元素互换
    A[j]=pivot;
    pivotindex=partition(A,i-1,j,A[j]);     //将l+1~r-1按轴值分部,并返回待与轴值交换元素下标
    pivot=A[j];                             //轴值还在末尾等待交换
    A[j]=A[pivotindex];
    A[pivotindex]=pivot;
    qsort(A,i,pivotindex-1);
    qsort(A,pivotindex+1,j);
}

快速排序是迄今为止所有内排序算法中在平均情况下最快的一种,当每个轴值都把数组分成相等的两个部分时,整个算法的时间代价是 Θ \Theta Θ(nlogn)。快速排序的平均时间复杂度为 O(nlogn),最好情况为 O(nlogn),最坏情况为 O(n2)。快速排序的空间复杂度为 O(logn),是一种不稳定排序算法。

7. 堆排序

堆排序是基于堆数据结构的排序算法,它利用最小堆的性质来实现对数组的排序:

class heap{
private:
    int* Heap;
    int maxsize;
    int n;
    void siftdown(int pos){
        while(!isLeaf(pos)){
            int j=leftchild(pos);       //下面j表示的是lc和rc中较小值的下标
            int rc=rightchild(pos);
            if((rc<n)&&(Heap[rc]<Heap[j])){
                j=rc;
            }
            if(Heap[pos]<Heap[j]){
                return;
            }
            int tmp=Heap[pos];
            Heap[pos]=Heap[j];
            Heap[j]=tmp;
            pos=j;
        }
    }
public:
    heap(int* h,int num,int max){
        Heap=h;
        n=num;
        maxsize=max;
        buildHeap();
    }
    int size(){
        return n;
    }
    bool isLeaf(int pos){
        return (pos>=n/2)&&(pos<n);
    }
    int leftchild(int pos){
        return 2*pos+1;
    }
    int rightchild(int pos){
        return 2*pos+2;
    }
    int parent(int pos){
        return (pos-1)/2;
    }
    void buildHeap(){
        for(int i=n/2-1;i>=0;i--){
            siftdown(i);
        }
    }
    void insert(const int& it){
        int curr=n++;
        Heap[curr]=it;
        while((curr!=0)&&(Heap[curr]<Heap[parent(curr)])){
            int tmp=Heap[curr];
            Heap[curr]=Heap[parent(curr)];
            Heap[parent(curr)]=tmp;
            curr=parent(curr);
        }
    }
    int removefirst(){
        n--;
        int tmp=Heap[0];
        Heap[0]=Heap[n];
        Heap[n]=tmp;
        if(n!=0){
            siftdown(0);
        }
        return Heap[n];
    }
    int remove(int pos){
        if(pos==(n-1)){
            n--;
        }
        else{
            n--;
            int tmp=Heap[pos];
            Heap[pos]=Heap[n];
            Heap[n]=tmp;
            while((pos!=0)&&(Heap[pos]<Heap[parent(pos)])){
                int tmp=Heap[pos];
                Heap[pos]=Heap[parent(pos)];
                Heap[parent(pos)]=tmp;
                pos=parent(pos);
            }
            if(n!=0){
                siftdown(pos);
            }
        }
        return Heap[n];
    }
};
void heapsort(int A[],int n){
    int minval;
    heap H(A,n,n);
    for(int i=0;i<n;i++){
        minval=H.removefirst();
    }
}

堆排序的平均时间复杂度为 O(nlogn),最好情况和最坏情况也都为 O(nlogn),但比快速排序要慢一个常数因子。堆排序在实际使用时适用于查找第 k 大小的元素,或者用于外排序。堆排序的空间复杂度为 O(1),是一种不稳定排序算法。

8. 桶排序

桶排序的核心思想是将数组按数值范围放入若干个桶,每个桶对应一定范围的数据,然后对每个桶中的数据使用其他排序算法或递归方式进行排序,最后将所有桶中的数据按顺序合并得到排序结果:

void inssort(int A[],int n){
    for(int i=1;i<n;i++){
        for(int j=i;j>0;j--){
            if(A[j]<A[j-1]){
            	int tmp=A[j];
            	A[j]=A[j-1];
            	A[j-1]=tmp;
            }
            else{
                break;
            }
        }
    }
}

void bucketSort(int A[],int n,int numBuckets){
    //创建桶
    int buckets[numBuckets][n]; 	//二维数组,每行表示一个桶
    int bucketSizes[numBuckets]; 	//每个桶中元素的数量
    //初始化桶的大小
    for(int i=0;i<numBuckets;i++){
        bucketSizes[i]=0;
    }
    //确定数组元素范围
	int maxA=-999,minA=999;
	for(int i=0;i<n;i++){
		if(A[i]>maxA)	maxA=A[i];
		if(A[i]<minA)	minA=A[i];
	} 
    //将元素放入桶中
    for(int i=0;i<n;i++){
    	int bucketGap=ceil((maxA-minA)/(float)numBuckets);
        int bucketIndex=(A[i]-minA)/bucketGap;
        buckets[bucketIndex][bucketSizes[bucketIndex]++]=A[i];
    }
    //对每个桶中的元素进行插入排序
    for(int i=0;i<numBuckets;i++){
        inssort(buckets[i],bucketSizes[i]);
    }
    // 合并桶中的元素
    int index=0;
    for(int i=0;i<numBuckets;i++){
        for(int j=0;j<bucketSizes[i];j++){
            A[index++]=buckets[i][j];
        }
    }
}

桶排序的平均时间复杂度为 O(n2),最好情况和最坏情况也都为 O(n2),但比插入排序要快一个常数因子。桶排序适用于在已知输入数据的范围内进行排序,若数据分布不均匀也会影响效率。桶排序的空间复杂度为 O(n2),是一种稳定排序算法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1079793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

wifi管理软件 WiFi Signal mac中文介绍

WiFi Signal mac是一款WiFi信号强度监测工具&#xff0c;它可以帮助用户实时监测WiFi信号的强度、频率、噪声等信息&#xff0c;并提供详细的图表和统计数据。 WiFi Signal可以自动扫描附近的WiFi网络&#xff0c;并显示它们的信号强度和频率。用户可以通过WiFi Signal来找到最…

Android斩首行动——应用层开发Framework必知必会

前言 相信做应用层业务开发的同学&#xff0c;都跟我一样&#xff0c;对Framework”深恶痛绝“。确实如此&#xff0c;如果平日里都在做应用层的开发&#xff0c;那么基本上我们很少会去碰Framework的知识。但生活所迫&#xff0c;面试总是逃不过这一关的&#xff0c;所以作为…

第二证券:A股公司首批三季报出炉 柏楚电子、平煤股份业绩一增一减

10月10日晚&#xff0c;柏楚电子、平煤股份拉开了A股公司三季报发表序幕。来自激光切开控制体系赛道的柏楚电子&#xff0c;前三季度营收、净利润均完结较大崎岖增加&#xff1b;焦煤龙头企业平煤股份&#xff0c;受煤价跌落连累成果&#xff0c;前三季度营收、净利润均有所下降…

Java架构师缓存性能优化

目录 1 缓存的负载策略2 缓存的序列化问题3 缓存命中率低4 缓存对数据库高并发访问5 缓存数据刷新的策略6 何时写缓存7 批量数据来更新缓存8 缓存数据过期的策略9 缓存数据如何恢复10 缓存数据如何迁移11 缓存冷启动和缓存预热1 缓存的负载策略 如果说我们在缓存架构设计当中啊…

优思学院|八大浪费深度剖析

在工作流程中消除浪费是精益思想的目标。在深入探讨八大浪费之前&#xff0c;了解浪费的定义至关重要。浪费是指工作流程中的任何行动或步骤&#xff0c;这些行动或步骤不为客户增加价值。换句话说&#xff0c;浪费是客户不愿意为其付费的任何过程。 最初的七大浪费&#xff0…

第83步 时间序列建模实战:Catboost回归建模

基于WIN10的64位系统演示 一、写在前面 这一期&#xff0c;我们介绍Catboost回归。 同样&#xff0c;这里使用这个数据&#xff1a; 《PLoS One》2015年一篇题目为《Comparison of Two Hybrid Models for Forecasting the Incidence of Hemorrhagic Fever with Renal Syndr…

Nerf 学习笔记

Nerf 学习笔记 Step 1&#xff1a;相机 Rays 行进(ray marching)Step 2&#xff1a;收集查询点Step 3&#xff1a;将查询点投射到高维空间(位置编码)Step 4&#xff1a;神经网络推理和体渲染神经网络推理体渲染计算损失 Reference: 搞懂神经辐射场 Neural Radiance Fields (Ne…

如何在一个传统的html中,引入vueJs并使用vue复制组件?

如何在一个传统的html中&#xff0c;引入vueJs并使用vue复制组件&#xff1f; 1.1 引言1.2 背景1.3 解决方案1.3.1 解决方案一&#xff1a;直接使用clipboard(不推荐仅供参考学习)1.3.2 解决方案二&#xff1a;封装指令js库后使用 (推荐) 1.1 引言 这篇博文主要分享如何在一个…

Springboot给每个接口设置traceId,并添加到返回结果中

原理 slf4j有个MDC的类&#xff0c;是ThreadLocal的实现&#xff0c;保存在这里的变量会绑定到某个请求线程&#xff0c;于是在该请求的线程里的日志代码都可以使用设入的变量。 实现 一、引入依赖 这个是可选项&#xff0c;用于生成唯一uid&#xff0c;我人懒&#xff0c…

一文带你了解 Linux 的 Cache 与 Buffer

目录 前言一、Cache二、Buffer三、Linux 系统中的 Cache 与 Buffer总结 前言 内存的作用是什么&#xff1f;简单的理解&#xff0c;内存的存在是为了解决高速传输设备与低速传输设备之间数据传输速度不和谐而设立的中间层&#xff08;学过计算机网络的应该都知道&#xff0c;这…

【实战】kubeadmin安装kubernetes集群

文章目录 前言服务器介绍准备工作设置服务器静态ip修改host关闭防火墙和swap修改所需的内核参数 部署步骤安装containerd安装cri工具&#xff08;效果等同于docker&#xff09; 安装kubernetes集群安装网络插件flannel安装可视化面板kuboard&#xff08;可选&#xff09; 下期预…

42. QT中开发Android配置QFtp功能时遇到的编译问题

1. 说明 此问题仅适用在QT中开发Android程序时&#xff0c;需要适用QFtp功能的情况。一般情况下&#xff0c;如果开发的是Windows或者Linux系统下的程序&#xff0c;可能不会出现该问题。 2. 问题 【Android】在将QFtp的相关代码文件加入到项目中后&#xff0c;编译项目时会…

sql server判断两个集合字符串是否存在交集

sql server判断字符串A101;A102和字符串A102;A103是否存在交集 我们编写两个函数&#xff1a; 1&#xff09;函数fn_split将字符串拆分成集合 create function [dbo].[fn_split](inputstr varchar(8000), seprator varchar(10)) returns temp table (Result varchar(200)) a…

TCP/IP(七)TCP的连接管理(四)全连接

一 全连接队列 nginx listen 参数backlog的意义 nginx配置文件中listen后面的backlog配置 ① TCP全连接队列概念 全连接队列: 也称 accept 队列 ② 查看应用程序的 TCP 全连接队列大小 实验1&#xff1a; ss 命令查看 LISTEN状态下 Recv-Q/Send-Q 含义附加&#xff1a;…

2785323-77-3,MAL-Alkyne,双功能连接试剂Alkyne maleimide

炔烃马来酰亚胺&#xff0c;Alkyne maleimide,MAL-Alkyne是一种非常有用的双功能连接试剂&#xff0c;可以在生物分子中发挥重要的作用。它的马来酰亚胺基团可以与生物分子中的硫醇基团反应&#xff0c;形成共价键&#xff0c;从而将生物分子与炔烃连接起来。这种连接方式在生物…

React的类式组件和函数式组件之间有什么区别?

React 中的类组件和函数组件是两种不同的组件编写方式&#xff0c;它们之间有一些区别。 语法和写法&#xff1a;类组件是使用类的语法进行定义的&#xff0c;它继承自 React.Component 类&#xff0c;并且需要实现 render() 方法来返回组件的 JSX。函数组件是使用函数的语法进…

Linux|qtcreator编译可执行程序双击运行

qt GUI window移植到linux参见&#xff1a;VS|vs2017跨平台编译linux&&CConsole&&QtGUI 参考&#xff1a;QtCreator修改项目的生成目录 文章目录 双击.pro文件&#xff0c;点击configureproject构建项目切换到release模式下双击打开pro文件&#xff0c;修改依赖…

嵌入式Linux裸机开发(六)EPIT 定时器

系列文章目录 文章目录 系列文章目录前言介绍配置过程 前言 前面学的快崩溃了&#xff0c;这也太底层了&#xff0c;感觉学好至少得坚持一整年&#xff0c;我决定这节先把EPIT学了&#xff0c;下面把常见三种通信大概学一下&#xff0c;直接跳过其他的先学移植了&#xff0c;有…

项目平台——项目首页设计(五)

这里写目录标题 一、页面成果图展示二、滚动条组件的使用三、页面设计1、需要4个盒子2、项目名称样式设计3、对基本信息、bug统计、执行记录进行样式设计4、基本信息5、bug统计 四、echarts使用1、安装2、折线图的使用 一、页面成果图展示 二、滚动条组件的使用 当内容超过屏幕…