【排序算法】冒泡排序、选择排序、插入排序

简介: 【排序算法】冒泡排序、选择排序、插入排序

冒泡排序

依次比较相邻的两个元素,将比较小的数放在前面,比较大的数放在后面,直到所有元素排列完。

最容易理解的版本

对一个数组的n个整型数据进行n趟排序,每趟排序都尝试将较大值放到数组右侧。
每趟排序比较两个相邻的数据,由于n个数据有n-1个间隔,所以每趟需要比较n-1次。
代码如下:

Java

import java.util.Arrays;

public class Main {
   
   
    public static void main(String[] args) {
   
   
        int[] ints = {
   
   5, 2, 4, 3};
        //比较n趟
        for (int i = 0; i < ints.length; i++) {
   
   
            //每趟比较n-1次
            for (int m = 0; m < ints.length - 1; m++) {
   
   
                //将较大值置于数组右侧
                if (ints[m] > ints[m + 1]) {
   
   
                    int tmp = ints[m];
                    ints[m] = ints[m + 1];
                    ints[m + 1] = tmp;
                }
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

C/C++

#include<stdio.h>
int  main() {
   
   
    int ints[] = {
   
    5,2,4,3 };
    //比较n趟
    for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
   
   
        //每趟比较n-1次
        for (int m = 0; m < sizeof(ints) / sizeof(ints[0]) - 1; m++) {
   
   
            ////将较大值置于数组右侧
            if (ints[m] > ints[m + 1]) {
   
   
                int tmp = ints[m];
                ints[m] = ints[m + 1];
                ints[m + 1] = tmp;
            }
        }
    }
    //检验结果
    for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
   
   
        printf("%d ", ints[i]);
    }
}

进一步优化

上述代码存在优化空间:
在第一趟排序结束后,数组最右端已是当前的最大值5。剩余元素均小于5,后续排序无需再与5进行比较。
在第二趟排序结束后,数组最右侧是4,5,剩余元素均小于4,5,后续排序无需再对4,5进行比较。
即对于内层循环:

  • 在第i趟排序中,只需要比较n-i次(i1开始)。
  • 如果外层循环是从0开始计数的,那么需要每轮需要比较n-1-i次。

对于外层循环,在执行第n-1趟排序时,内层循环只比较了第1个元素和第2个元素。
此时已经排列完成,不需要再执行下一趟排序。
即对于外层循环:

  • 只需要排序n-1趟。
  • 对于给定的数组,n-1的结果也是确定的。因此无论外层循环的计数器从几开始,需要比较的次数都是n-1


上面的例子比较简单,还有一种情况存在优化空间:在第n-1趟排序执行之前就已经排序完成。
这种情况的特征是:在某一趟比较之后,没有发生元素交换。
我们可以:

  1. 定义一个标记flag并初始化为1
  2. 在每趟比较开始前,通过flag检查是否发生元素交换。
  3. 在每趟比较开始时,将flag置为0
  4. 当发生元素交换时,将flag置为1

在第2步中,如果flag值为1,则表明发生交换,继续下一步。
如果flag值为0,则表明没有发生交换,即已经排序完成,结束即可。

  • flag初始值为1,可以正常进入第一趟排序。
  • JavaBoolean类型不能赋值为10,将对应的10改为truefalse即可。

    总结

  • 外层循环控制轮数,总共执行n-1轮。

  • 内层循环控制每轮的比较次数,第i轮比较n-i次(i1开始)。
  • 通过标记flag判断是否已经排序完成。

    C/C++

#include<stdio.h>
int  main() {
   
   
    int ints[] = {
   
    5,2,4,3 };
    //标记完成状态
    char flag = 1;
    //比较n-1趟
    for (int i = 0; i < sizeof(ints) / sizeof(ints[0]) - 1 && flag; i++) {
   
   
        //将标记置为0
        flag = 0;
        //计数器从0开始,每趟比较n-1-i次
        for (int m = 0; m < sizeof(ints) / sizeof(ints[0]) - 1 - i; m++) {
   
   
            //将较大值置于数组右侧
            if (ints[m] > ints[m + 1]) {
   
   
                int tmp = ints[m];
                ints[m] = ints[m + 1];
                ints[m + 1] = tmp;
                //当发生交换时,将flag置为1
                flag = 1;
            }
        }
    }
    //检验结果
    for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
   
   
        printf("%d ", ints[i]);
    }
}

Java

import java.util.Arrays;

public class Main {
   
   
    public static void main(String[] args) {
   
   
        int[] ints = {
   
   5, 2, 4, 3};
        //标记完成状态
        boolean flag = true;
        //比较n-1趟
        for (int i = 0; i < ints.length - 1 && flag; i++) {
   
   
            //将标记置为0
            flag = false;
            //每趟比较n-1次
            for (int m = 0; m < ints.length - 1 - i; m++) {
   
   
                //将较大值置于数组右侧
                if (ints[m] > ints[m + 1]) {
   
   
                    int tmp = ints[m];
                    ints[m] = ints[m + 1];
                    ints[m + 1] = tmp;
                    //当发生交换时,将flag置为1
                    flag = true;
                }
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

需要注意的是,内层循环的结束条件与m无关。

选择排序

分别从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。选择排序是不稳定的排序算法,即对于值相同的数据元素,彼此的前后顺序可能会发生改变。

对比冒泡排序

与冒泡排序不同:

  • 冒泡排序是逐趟选出未排序序列中的最大值,置于右侧。
  • 选择排序是逐趟选出未排序序列中的最小值,置于左侧。
  • 冒泡排序会两两比较相邻元素,将较大值通过多次交换移动到数列右侧,第i趟最多交换n-i次。
  • 选择排序会通过多次比较记录最小元素的下标,在这一趟结束时才发生交换,每趟最多交换1次。

即重复这两个步骤:

  1. 遍历无序序列,记录无序数列的最小值的下标。
  2. 将下标对应的元素与无序数列的最左端元素交换位置。


代码如下:

Java

import java.util.Arrays;

public class Main {
   
   
    public static void main(String[] args) {
   
   
        int[] ints = {
   
   5, 2, 4, 3};
        //用于记录最小值的下标
        int minIndex = 0;
        //控制轮数
        for (int i = 0; i < ints.length - 1; i++) {
   
   
            minIndex = i;
            //在未排序的子序列中找到最小元素的位置
            for (int j = i + 1; j < ints.length; j++) {
   
   
                if (ints[j] < ints[minIndex]) {
   
   
                    //用minIndex记录最小元素的位置
                    minIndex = j;
                }
            }
            //交换元素,将较小值置于左侧
            if (minIndex != i) {
   
   
                int tmp = ints[minIndex];
                ints[minIndex] = ints[i];
                ints[i] = tmp;
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

C/C++

#include<stdio.h>
int  main() {
   
   
    int ints[] = {
   
    5,2,4,3 };
    //标记最小值下标
    char minIndex = 0;
    //比较n-1趟
    for (int i = 0; i < sizeof(ints) / sizeof(ints[0]) - 1; i++) {
   
   
        minIndex = i;
        for (int m = i + 1; m < sizeof(ints) / sizeof(ints[0]); m++) {
   
   
            if (ints[m] < ints[minIndex]) {
   
   
                minIndex = m;
            }
        }
        //交换元素,将较小值置于左侧
        if (minIndex != i) {
   
   
            int tmp = ints[i];
            ints[i] = ints[minIndex];
            ints[minIndex] = tmp;
        }
    }
    //检验结果
    for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
   
   
        printf("%d ", ints[i]);
    }
}

需要注意:

  • 对于n个元素,需要排序的趟数仍然是n-1
  • 不同于冒泡排序,选择排序每趟排序最多只会改变两个元素的位置。不能设置flag检查是否排序完成,也无法通过flag检查。
  • 选择排序需要遍历剩余所有元素,内层循环不能同冒泡循环一样修改右边界。并且要保证能访问到数列的最后一个元素。

    插入排序

    逐步将无序序列中的元素,插入到前面已排好的有序序列的合适位置。

    C/C++

#include<stdio.h>
int  main() {
   
   
    int ints[] = {
   
    5,2,4,3 };
    //遍历数列
    for (int i = 1; i < sizeof(ints) / sizeof(ints[0]); i++) {
   
   
        //定义临时变量存储当前序列取出的值
        int tmp = ints[i];
        int j = 0;
        //寻找合适位置
        for (j = i - 1; j >= 0; j--) {
   
   
            //需要移动的数据向后覆盖
            if (ints[j] > tmp) {
   
   
                ints[j + 1] = ints[j];
            }
            else break;
        }
        if (ints[j + 1] != tmp) {
   
   
            ints[j + 1] = tmp;
        }
    }
    //检验结果
    for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
   
   
        printf("%d ", ints[i]);
    }
}

Java

import java.util.Arrays;

public class Main {
   
   
    public static void main(String[] args) {
   
   
        int[] ints = {
   
   5, 2, 4, 3};
        //遍历数列
        for (int i = 1; i < ints.length; i++) {
   
   
            //定义临时变量存储当前序列取出的值
            int tmp = ints[i];
            int j = 0;
            //寻找合适位置
            for (j = i - 1; j >= 0 && ints[j] > tmp; j--) {
   
   
                ints[j + 1] = ints[j];
            }
            //插入到合适位置
            if (ints[j + 1] != tmp) {
   
   
                ints[j + 1] = tmp;
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

要点如下:

  • 如果数据结构是数组,那么每次插入都需要将插入位置以后的元素向后移动,移动元素会覆盖该元素的下一个元素。会导致ints[i]被覆盖,因此必须要使用临时变量tmp存储每趟排序中的ints[i]的值。
  • 我们需要对数组的每个元素都进行遍历,以保证每个元素都被取出并插入到合适位置。因此外重循环的结束条件为元素个数n而不是n-1
  • 在第一趟插入中,我们将原数列的第1个元素取出作为有序数列,将第2个元素取出作为新元素插入,对应的下标从1开始。虽然结束条件是n,外重循环的次数仍然是n-1
  • 在插入元素时,已经内层循环的结束条件,此时j小于零,或者已经指向合适位置的前一个位置。因此需要对ints[j+1]进行赋值,而非ints[j]

    补充

    交换两个变量的值,除了可以使用第三个变量tmp,也可以使用加减法的方式处理,仅适用于数字类型。

    ints[m] = ints[m] + ints[n];
    ints[n] = ints[m] - ints[n];
    ints[m] = ints[m] - ints[n];
    

    总结

    三种排序方法每趟都只能确保一个数据加入有序数列后仍有序,外层循环执行的趟数都为n-1n即元素个数。但由于计数器开始位置不同,可能为0,也可能为1,或者其它计数方式,不需要死记硬背,只需要保证能执行n-1趟即可。
    对于内层循环,还是由于三种排序方法每趟都只能确保一个数据加入有序数列后仍有序。有序序列每趟排序后长度都在增加,我们不需要对有序序列的元素再取出排序。我们只需要保证能遍历到无序序列中的每一个元素即可。

  • 对于冒泡排序,有序序列默认在右端,左侧为无序序列,我们采取的方式是调整右边界。而内层循环每次从0开始,是为了能够遍历到左侧的无序序列的每一个元素。

  • 对于选择排序,有序序列默认在左端,右侧为无序序列,我们采取的方式是调整左边界。内层循环的计数器初始值随趟数改变而改变,是为了保证每趟都指向无序序列的第一个元素,并能够遍历无序序列的每一个元素。
  • 对于插入排序,有序序列默认在左端,我们需要取出无序序列中的元素之后遍历有序序列,寻找合适位置。由于有序序列是有序的,我们可以选择一个方向,寻找介于两个元素之间的位置插入。在有序序列寻找合适位置时需要考虑数组边界。

对于临时变量的定义,编译器可能存在零优化,如果定义在循环内部,那么可能出现频繁的创建和释放,提高占用时间。并且在插入排序中,如果数据结构是数组,那么数据的移动方式就是向后覆盖,可能导致无序数列的最左端元素被覆盖,我们需要使用临时变量提前保存数据。上面的代码中,为了便于理解,并没有将所有循环内的变量在循环内定义。但出于以上两点原因,建议将变量在循环外定义,在循环内使用

目录
相关文章
|
3月前
|
搜索推荐 Python
利用Python内置函数实现的冒泡排序算法
在上述代码中,`bubble_sort` 函数接受一个列表 `arr` 作为输入。通过两层循环,外层循环控制排序的轮数,内层循环用于比较相邻的元素并进行交换。如果前一个元素大于后一个元素,就将它们交换位置。
152 67
|
4月前
|
搜索推荐
冒泡排序算法
【10月更文挑战第19天】冒泡排序是一种基础的排序算法,虽然在实际应用中可能不是最优的选择,但对于理解排序算法的基本原理和过程具有重要意义。
|
4月前
|
算法 搜索推荐
数据结构与算法学习十一:冒泡排序、选择排序、插入排序
本文介绍了冒泡排序、选择排序和插入排序三种基础排序算法的原理、实现代码和测试结果。
29 0
数据结构与算法学习十一:冒泡排序、选择排序、插入排序
|
4月前
|
搜索推荐 C语言
排序算法--冒泡排序
排序算法--冒泡排序
29 0
|
4月前
|
存储 搜索推荐 算法
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
|
16天前
|
算法 数据安全/隐私保护 计算机视觉
基于Retinex算法的图像去雾matlab仿真
本项目展示了基于Retinex算法的图像去雾技术。完整程序运行效果无水印,使用Matlab2022a开发。核心代码包含详细中文注释和操作步骤视频。Retinex理论由Edwin Land提出,旨在分离图像的光照和反射分量,增强图像对比度、颜色和细节,尤其在雾天条件下表现优异,有效解决图像去雾问题。
|
16天前
|
算法 数据可视化 安全
基于DWA优化算法的机器人路径规划matlab仿真
本项目基于DWA优化算法实现机器人路径规划的MATLAB仿真,适用于动态环境下的自主导航。使用MATLAB2022A版本运行,展示路径规划和预测结果。核心代码通过散点图和轨迹图可视化路径点及预测路径。DWA算法通过定义速度空间、采样候选动作并评估其优劣(目标方向性、障碍物距离、速度一致性),实时调整机器人运动参数,确保安全避障并接近目标。
110 68
|
25天前
|
算法 数据安全/隐私保护
室内障碍物射线追踪算法matlab模拟仿真
### 简介 本项目展示了室内障碍物射线追踪算法在无线通信中的应用。通过Matlab 2022a实现,包含完整程序运行效果(无水印),支持增加发射点和室内墙壁设置。核心代码配有详细中文注释及操作视频。该算法基于几何光学原理,模拟信号在复杂室内环境中的传播路径与强度,涵盖场景建模、射线发射、传播及接收点场强计算等步骤,为无线网络规划提供重要依据。
|
26天前
|
机器学习/深度学习 数据采集 算法
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
本项目基于MATLAB2022a实现时间序列预测,采用CNN-GRU-SAM网络结构。卷积层提取局部特征,GRU层处理长期依赖,自注意力机制捕捉全局特征。完整代码含中文注释和操作视频,运行效果无水印展示。算法通过数据归一化、种群初始化、适应度计算、个体更新等步骤优化网络参数,最终输出预测结果。适用于金融市场、气象预报等领域。
基于GA遗传优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
|
26天前
|
算法
基于龙格库塔算法的锅炉单相受热管建模与matlab数值仿真
本设计基于龙格库塔算法对锅炉单相受热管进行建模与MATLAB数值仿真,简化为喷水减温器和末级过热器组合,考虑均匀传热及静态烟气处理。使用MATLAB2022A版本运行,展示自编与内置四阶龙格库塔法的精度对比及误差分析。模型涉及热传递和流体动力学原理,适用于优化锅炉效率。