C++算法:排序之一(插入、冒泡、快速排序)

news2024/12/26 22:49:14

C++算法:排序

排序之一(插入、冒泡、快速排序)

文章目录

  • C++算法:排序
  • 前言
  • 一、十大排序法性能
  • 二、各算法实现
    • 1、插入排序
    • 2、冒泡排序
    • 3、快速排序
  • 原创文章,未经许可,严禁转载


前言

排序算法很多,一直有十大经典的说法。实际工作中除了个别有争议的排序算法,各有各擅长的领域,不能因为选择排序又慢又简单就小看它,有时候还真是非它不可。也不能因为桶排序、计数排序是理论上时间复杂度最小的就觉得能包圆所有排序工作。


一、十大排序法性能

这里用一张表格来说明各种排序算法的性能比较:

算法名称最好时间复杂度平均时间复杂度最坏时间复杂度空间复杂度稳定性
插入排序O(n)O(n2)O(n2)O(1)
冒泡排序O(n)O(n2)O(n2)O(1)
快速排序O(NlogN)O(NlogN)O(n2)O(logN)
归并排序O(NlogN)O(NlogN)O(NlogN)O(n)
希尔排序O(NlogN)O(NlogN)O(NlogN)O(1)
选择排序O(n2)O(n2)O(n2)O(1)
堆排序O(NlogN)O(NlogN)O(NlogN)O(1)
-----------------------------------------------------------------------------------------------------------
计数排序O(n)O(n)O(n)O(n)
桶排序O(n)O(n)O(n)O(n)
基数排序O(n*K)O(n*K)O(n*K)O(n)

以上隔开的前七个算法是比较排序,后三个是非比较排序。

  • 比较排序是通过比较元素之间的次序来确定它们在最终结果中的位置,简单说就是比大小了。
  • 非比较排序则是通过确定每个元素之前应该有多少个元素来进行排序的。
  • 比较排序的优点是适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
  • 非比较排序时间复杂度低,但由于非比较排序需要占用空间来确定唯一位置,所以对数据规模和数据分布有一定的要求。

二、各算法实现

以下动图来源本站此文,顺便说一下,这篇文章写得很好。我都想直接转的,可惜是JAVA实现,与笔者主题不符。就借动图来一用,具体动图来源不可考。反正我是从此文借来,如有侵权、请留言指正。

1、插入排序

在这里插入图片描述
插入排序在对小规模数据进行排序时很常用。它有点 类似于我们斗地主抓牌的过程,第一张扑克牌拿到手后,之后每抓到一张都按大小放置,抓完了牌就自然是有序的。

代码如下(示例):

#include <iostream>
#include <vector>

using namespace std;

void insert_sort(vector<int> &vec){
    int n = vec.size();
    int curr;                          //当前要排的
    for (int i=1; i<n; i++){
        curr = vec[i];         
        int j = i - 1;                //另一个下标指针,指向被比较的
        while (j>=0 && curr<vec[j]){
            vec[j+1] = vec[j];        //符合条件,就将当前替换为前一个,因为当前已备份到curr
            j--;                      //一直向前比较
        }
        vec[j+1] = curr;             //将当前放入合适的位置,j经过循环会停在第一个小于curr的元素上
    }    
}

int main(){
    vector<int> vec = {6, 5, 3, 1, 8, 7, 2, 4};
    insert_sort(vec);
    for (auto it=vec.begin(); it!=vec.end(); it++){
        cout << *it << " ";
    }
    return 0;
}

代码使用了图中同样的数据,使用了STL中的vector,对链表也是可行的,不过要用迭代器,不能直接用[]运算。通过遍历数组中的每个元素,将当前元素与前面的元素进行比较并交换位置,直到当前元素大于前面的元素为止。这样可以保证在当前元素之前的所有元素都是有序的。最终,整个数组都将变为有序。

2、冒泡排序

在这里插入图片描述
冒泡排序(也叫气泡排序(bubble sort),名字来源于水中的大气泡总是在小气泡之上)很好理解,前一个与后一个比较大小,小的放前,大的放后。如此一个个比过去,多来几遍自然就排序完成了。当某一次排序没有产生交换操作,那就可以认定排序完成。这个排序算法和插入排序一样,比较适合小规模的数据,并且在数据基本有序的时候是非常好用的,实现简单,效率很高。

代码如下(示例):

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void bubble_sort(vector<int> &vec){
    int n = vec.size();
    int i = 0, flag;                    //i 是循环计数,flag用于表示有没有交换动作
    do{
        flag = 0;                       //先认为没有交换
        for (int j=0; j<n-i-1; j++){
            if (vec[j] > vec[j+1]){
                swap(vec[j], vec[j+1]);    //交换大小元素
                flag = 1;                 //产生了交换动作
            } 
        }    
    } while (++i < n && flag);            //没有交换动作或循环次数能保证有序就停止了
}

int main(){
    vector<int> vec = {2, 11, 10, 5, 4, 13, 9, 7, 8, 1, 12, 3, 6, 15, 14};
    bubble_sort(vec);
    for (auto it=vec.begin(); it!=vec.end(); it++){
        cout << *it << " ";
    }
    return 0;
}

上面的代码中引入了algorithm中的swap函数来实现元素交换。当然也可以自己写一个,这个很容易。实现过程看注释也就明白了,特别的,在排序函数最后的while条件中,++i < n 可以改成++i < n-1,当然实际情况是很少真需要 n-1 次的,因为flag这个条件会在排序成功后停止循环。只有最小值是最后一个元素的情况才会循环 n-1 次(因为最小值元素需要n-1次才能跑到最前面)。

另外冒泡排序有个很有名的改良版本,叫鸡尾酒排序(也叫双向冒泡排序),看名字也就知道怎么改良了,事实上它和冒泡排序的时间、空间复杂度是一样的,更适合于基本有序的情况而已。

3、快速排序

在这里插入图片描述
前面两种排序算法都只适合小规模数据,且平均时间复杂度很高,通常适用于特定情况。而快速排序是一种高效的排序算法,它采用了分治的思想,适用于较大规模数据的情况。这个排序法在实践中使用得很多,也是实践证明最有效的排序法,很多IT大厂面试时往往会要求写一个快速排序算法,本文也重点写了这个算法。其基本步骤如下:

  • 从数列中挑出一个元素作为基准元素,也称之为划界元素。
  • 将所有比基准元素小的元素放到基准元素的左边,所有比基准元素大的元素放到基准元素的右边。
  • 对基准元素左右两边的子序列递归执行第1步和第2步,直到序列中所有元素都有序。

快速排序的平均时间复杂度为O(NlogN),最坏情况下的时间复杂度为O(n^2),但这种情况并不常见。快速排序通常明显比其他O(NlogN)算法更快,因为它的内部循环可以在大部分架构上非常高效地实现。既然采用了递归算法,那就有可能产生递归深度太深的情况,如果数据规模太大也是不适用的。对于C++标准库内置的sort来说,在这种情况下会转用堆排序。

代码如下(示例):

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;
//以最后元素划界的写法
void quick_sort_b(vector<int> &vec, int left, int right) {
    if (left < right) {                          
        int base = vec[right];                     //以最后元素划界
        int i = left - 1;                          //前元素下标指针
        for (int j = left; j <= right - 1; j++){   // j 为后元素下标指针
            if (vec[j] <= base) {                 
                i++;                               
                swap(vec[i], vec[j]);         //循环交换,实际效果是小于划界的都到前面去了
            }
        }
        swap(vec[i + 1], vec[right]);      //把划界元素放到中间,当前i是小于划界的,i+1是大的
        int pi = i + 1;                    //递归是要排除划界的
        quick_sort_b(vec, left, pi - 1);
        quick_sort_b(vec, pi + 1, right);
    }
}

//以最左元素划界的写法
void quick_sort_f(vector<int> &vec, int left, int right) {
    if (left < right) {
        int base = vec[left];                    //以最左元素划界
        int i = left;                            //前元素下标指针
        for (int j = left+1; j <= right; j++){   // j 为后元素下标指针
            if (vec[j] <= base) {
                i++;
                swap(vec[i], vec[j]);        //循环交换,实际效果是小于划界的都到前面去了
            }
        }
        swap(vec[i], vec[left]);          //把划界元素放到中间,当前i是小于划界的
        int pi = i + 1;                   //递归前排除划界元素
        quick_sort_f(vec, left, pi-1);
        quick_sort_f(vec, pi+1, right);
    }
}

int main(){
    vector<int> vec = {3, 1, 2, 4, 9, 6};
    //quick_sort_b(vec, 0, vec.size()-1);
    quick_sort_f(vec, 0, vec.size()-1);
    for (auto it=vec.begin(); it!=vec.end(); it++){
        cout << *it << " ";
    }
    return 0;
}

快速排序的基准元素的选择对算法的性能有很大影响。理想情况下,基准元素应该是序列中的中位数,这样可以将序列平均分成两部分,从而最大限度地减少递归调用的次数。但是,在实际应用中,寻找序列的中位数可能会很耗时,因此通常采用一些近似方法来选择基准元素。

一种常用的方法是固定位置选取基准值,即每次都选择序列的第一个元素或最后一个元素作为基准元素。这种方法实现简单,但是当序列本身就是有序或接近有序时,会导致快速排序的性能下降。

另一种方法是随机选取基准值,即每次从序列中随机选择一个元素作为基准元素。这种方法可以有效避免快速排序在特殊数据下的性能下降。

还有一种方法是三数取中法,即从序列的头、尾和中间三个位置分别取出一个元素,然后比较它们的大小,选择中间值作为基准元素。这种方法相对于固定位置选取基准值和随机选取基准值来说,更能保证快速排序的稳定性。

本文只写了以最前和最后的元素划界的方法,其实以中间元素划界只要找出中间元素,然后把这元素交换到最后去,再以最后元素划界的方法运行就行了,同样的选三元素取中间值也可以这么办。在实际使用中,往往采取只剩少数元素时直接用插入排序的办法。


未完待续…

原创文章,未经许可,严禁转载

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

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

相关文章

chatgpt赋能python:Python备份一个列表:最简单的方式和最佳实践

Python备份一个列表&#xff1a;最简单的方式和最佳实践 在Python编程中&#xff0c;经常需要将数据存储在列表中。但是&#xff0c;由于数据的重要性&#xff0c;我们需要确保数据不会丢失或损坏。因此&#xff0c;备份列表是我们需要考虑的一件事情。在这篇文章中&#xff0…

chatgpt赋能python:Python实现文件夹备份:让你的数据永不丢失

Python实现文件夹备份&#xff1a;让你的数据永不丢失 数据备份对于每个人都非常重要。如果你有很多个人或工作文件保存在计算机上&#xff0c;那么定期备份可以保证你的数据不会因为计算机出现故障而丢失。Python作为一种强大的编程语言&#xff0c;可以帮助你轻松地实现文件…

Linux开发工具gcc/g++篇

文章目录 &#x1f347;0. 前言&#x1f348;1. 背景知识&#x1f349;2. gcc/g使用&#x1f34a;2.1 预处理操作&#x1f34b;去注释&#x1f34b;头文件展开&#x1f34b;条件编译 & 宏展开 &#x1f34a;2.2 编译操作&#x1f34a;2.3 汇编操作&#x1f34a;2.4 链接 &a…

chatgpt赋能python:Python多段分段函数的介绍

Python多段分段函数的介绍 在Python编程中&#xff0c;有许多种不同类型的函数&#xff0c;其中之一是多段分段函数。多段分段函数的特点在于&#xff0c;在输入域上&#xff0c;函数定义被划分为不同的段&#xff0c;每个段都求值并返回结果。在本文中&#xff0c;我们将深入…

Java性能权威指南-总结5

Java性能权威指南-总结5 垃圾收集入门垃圾收集概述分代垃圾收集器 垃圾收集入门 很多时候没有机会重写代码&#xff0c;又面临需要提高Java应用性能的压力&#xff0c;这种情况下对垃圾收集器的调优就变得至关重要。 现代JVM的类型繁多&#xff0c;最主流的四个垃圾收集器分别…

使用RP2040自制的树莓派pico—— [2/100] HelloWorld! 和 点亮LED

使用RP2040自制的树莓派pico—— [2/100] HelloWorld! 和 点亮LED 开发环境HelloWorld!闪烁 LED 灯代码 由于比较简单就放在一起写了 开发环境 软件&#xff1a;Thonny HelloWorld! 要想使串口打印HelloWorld&#xff01; 只需要一行代码 print("HelloWorld!")保…

c++与c中多组输入的使用

我们现在看看c中多组输入的使用 int main() {int a;//1while (~scanf("%d", &a)){}//2while (scanf("%d", &a) ! EOF){}return 0; } 这两个是等同的 我们需要知道的是scanf的返回值是成功读取的个数&#xff0c;我们来验证一下 我们可以看到&am…

chatgpt赋能python:Python在Mac上的运行方法

Python在Mac上的运行方法 如果你是一名使用Mac系统的Python开发人员&#xff0c;你肯定希望能够尽可能方便地运行Python。幸运的是&#xff0c;Mac系统已经预先安装了Python&#xff0c;但是你可能需要对其进行配置&#xff0c;以便更好地管理Python模块和环境。 检查Python版…

chatgpt赋能python:Python地区分析:如何使用Python进行地理数据分析

Python地区分析&#xff1a;如何使用Python进行地理数据分析 简介 Python是一种广泛使用的编程语言&#xff0c;它提供了许多强大的工具来处理大量数据。其中包括地理数据&#xff0c;地理数据是指地球表面的空间信息。Python中有一些强大的地图库&#xff0c;包括Folium和Ba…

chatgpt赋能python:Python的均值计算公式

Python的均值计算公式 在数据分析和机器学习方面&#xff0c;计算均值是非常常见的操作。Python提供了一些内置函数和库来计算均值。本文将介绍Python中常用的均值计算公式。 1. 算术均值 算术均值&#xff08;Arithmetic Mean&#xff09;是最常见的均值计算方法。Python中…

解决高并发

目录 1.4 对比单体系统、分布式系统和微服务系统 1.4.1 单体系统之痛 1、什么是单体系统 2、单体系统面临的问题 1.4.2 高并发系统之分布式架构 1.4.3 高并发系统之微服务架构 1.4 对比单体系统、分布式系统和微服务系统 接下来从企业真实场景出发&#xff0c;对比单体系统…

ROS:服务端Server的编程实现

目录 一、服务模型二、创建功能包三、创建代码并编译运行&#xff08;C&#xff09;3.1步骤3.2创建服务端Server代码3.3编译3.4运行 一、服务模型 Server端本身是进行模拟海龟运动的命令端&#xff0c;它的实现是通过给海龟发送速度&#xff08;Twist&#xff09;的指令&#x…

【Android Framework系列】第1章 Handler消息传递机制

1 Handler简介 Handler是一套Android的消息传递机制&#xff0c;Handler主要用于同进程的线程间通信。而Binder/Socket用于进程间通信。 2 Handler运行机制 Handler运行主要涉及到四个类&#xff1a;Handler、Looper、Message、MessageQueue Handler&#xff1a;消息处理器&…

chatgpt赋能python:Python文件备份的重要性和应用

Python文件备份的重要性和应用 在现代企业和个人用户中&#xff0c;数据备份是一项至关重要的工作&#xff0c;以防止数据丢失或损坏。当涉及到计算机数据时&#xff0c;文件备份是一项基本需求。文件备份还可以用于保护文件&#xff0c;以防它们被病毒、恶意软件或未经授权的…

法规标准-UN R158标准解读

UN R158是做什么的&#xff1f; UN R158全名为针对驾驶员识别车辆后方弱势道路使用者&#xff0c;联合国对倒车系统和机动车的统一规定&#xff0c;该法规涉及批准倒车和机动车辆的装置&#xff0c;主要为保证倒车时避免碰撞&#xff0c;方便驾驶员观察了解车辆后部人员和物体…

dSPACE一览(暂存)

1. SCALEXIO - dSPACE 2. dSPACE仿真流程介绍&#xff08;dSPACE软件介绍、仿真演示、自动化API接口使用&#xff09;_云溪溪儿的博客-CSDN博客 目录 硬件 板卡 软件 VEOS 应用领域 全面总线仿真 高效集成多种建模方法 硬件 板卡 SCALEXIO FPGA Subsystems DS6601 FPG…

STM32通过esp8266连接WiFi接入MQTT服务器

上文我们讲到如何搭建本地MQTT服务器&#xff0c;现在介绍如何通过stm32连接MQTT 一.首先我们初始化esp8266这里我们使用的是USART4与其通信代码如下 void UART4_Init(uint32_t bound) {GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;RCC_APB1…

高通 Camera HAL3:添加一条PipeLine

一.概述 添加一条PipeLine实现两路Raw进&#xff0c;一路Raw出 二.简介 要添加的PipeLine&#xff1a;SWMFMergeRawTwo2One 包含1个memcpy node&#xff0c;这个node用于将2个raw buffer input输入 变为 1个raw buffer output输出 三.添加 3.1 在相应的Usecase下添加一个p…

Spring Cloud Alibaba - Nacos源码分析(一)

目录 一、源码 1、为什么要分析源码 2、看源码的方法 二、Nacos服务注册与发现源码剖析 1、Nacos核心功能点 2、Nacos服务端/客户端原理 2.1、nacos-example 2.2、Nacos-Client测试类 3、项目中实例客户端注册 一、源码 1、为什么要分析源码 1. 提升技术功底&#x…

NB使用MQTT连接格物平台

内容简介&#xff1a; 本文主要记述了怎么使用NB-IoT模块&#xff0c;采用MQTT协议连接联通的格物平台&#xff0c;并且实现单属性和多属性数据的上报。 1 创建产品 打开格物平台&#xff0c;进行注册登录&#xff1b;之后点击页面的控制台&#xff0c;进入设备管理引擎&#x…