10 排序算法:冒泡排序与快速排序(算法原理、算法实现、时间和空间复杂度分析)

目录

1 十大常见的排序算法

1.1 算法的稳定性

2 冒泡排序

2.1 算法原理

2.2 算法实现

2.3 时间空间复杂度分析

2.3.1 时间复杂度分析

2.3.2 空间复杂度分析

3 快速排序

3.1 算法原理

3.1.1 排序思想

3.1.2 递归过程

3.2 示例

3.2.1 示例 1

3.2.2 示例 2

3.2.3 示例 3

3.3 算法实现

3.4 时间空间复杂度分析

3.4.1 时间复杂度分析

3.4.2 空间复杂度分析


1 十大常见的排序算法

        排序算法众多,实现方式各异,其时间复杂度、空间复杂度和稳定性各不相同,如下图所示:

1.1 算法的稳定性

  • 稳定排序算法:如果一个排序算法在排序前后,相等的元素之间的相对顺序保持不变,那么这个算法是稳定的
  • 不稳定排序算法:如果一个排序算法在排序前后,相等的元素之间的相对顺序可能发生改变,那么这个算法是不稳定的

        假设有一个学生记录列表,每个记录包含学生的姓名和成绩:

[("Alice", 85), ("Bob", 90), ("Charlie", 85), ("David", 90)]

按成绩升序排序:

  • 稳定排序算法

    • 排序结果:[("Alice", 85), ("Charlie", 85), ("Bob", 90), ("David", 90)]
    • 相同成绩的学生(85 分的 Alice 和 Charlie,90 分的 Bob 和 David)保持了原来的顺序
  • 不稳定排序算法

    • 排序结果:[("Charlie", 85), ("Alice", 85), ("David", 90), ("Bob", 90)]
    • 相同成绩的学生的顺序可能发生变化。

2 冒泡排序

2.1 算法原理

        冒泡排序是一种直观且易于理解的排序算法,其基本思想是通过不断地交换相邻的、顺序错误的元素,逐步将较大的元素(如果是升序排列的话)像水泡一样 “浮” 到序列的末尾。具体步骤如下:

        1. 比较相邻元素:从数组的第一个元素开始,逐一比较相邻的两个元素。

        2. 交换位置:如果前一个元素大于后一个元素(对于升序排序而言),则交换这两个元素的位置。如果是降序排序,则当发现前一个元素小于后一个元素时进行交换。

        3. 一趟遍历完成:经过一次完整的遍历后,数组中最大的元素(升序时)或最小的元素(降序时)会被移动到数组的最后一个位置。

        4. 重复遍历与交换:在排除了已排序的最后一个元素后,再次从头开始重复第 1 步和第 2 步的操作,直到整个数组中的所有元素都被正确地排序。

        5. 循环遍历直至完全排序:整个过程会不断重复,每完成一次遍历就使得更多的元素处于正确的位置,直到没有更多的交换发生,即整个数组被完全排序为止。

        通过上述步骤,冒泡排序能够有效地对一组数字进行排序,尽管它的效率在大规模数据集上不如更高级的排序算法,如快速排序或归并排序,但对于小规模数据集来说,冒泡排序仍然是一个很好的选择

        如下图所示:

2.2 算法实现

#include <stdio.h>

// 冒泡排序函数
void bubbleSort(int arr[], int size)
{
    // 外层循环控制遍历次数(n-1)次
    for (int i = 0; i < size - 1; i++)
    {
        // 内层循环进行相邻元素的比较和交换 (n-1-i)次
        for (int j = 0; j < size - i - 1; j++)
        {
            // 如果前一个元素大于后一个元素,则交换
            if (arr[j] > arr[j + 1])
            {
                // 交换 arr[j] 和 arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main()
{
    // 定义一个待排序的数组
    int arr[] = {3, 1, 5, 4, 2};
    int size = sizeof(arr) / sizeof(arr[0]); // 计算数组长度

    // 输出原始数组
    printf("原始数组: ");
    for (int i = 0; i < size; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 调用冒泡排序函数
    bubbleSort(arr, size); // 传递数组名即传递数组首元素的地址

    // 输出排序后的数组
    printf("排序后的数组: ");
    for (int i = 0; i < size; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

        输出结果如下所示: 

        下面是优化后的冒泡排序:

#include <stdio.h>

// 冒泡排序函数
void bubbleSort(int arr[], int size)
{
    // 外层循环控制遍历次数 (n-1)次
    for (int i = 0; i < size - 1; i++)
    {
        // 设置标志位,用于检测本轮是否有元素交换
        int swapped = 0;

        // 内层循环进行相邻元素的比较和交换 (n-1-i)次
        for (int j = 0; j < size - i - 1; j++)
        {
            // 如果前一个元素大于后一个元素,则交换
            if (arr[j] > arr[j + 1])
            {
                // 交换 arr[j] 和 arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;

                // 标记发生了交换
                swapped = 1;
            }
        }

        // 如果没有发生任何交换,说明数组已经有序,提前退出,避免不必要的遍历。
        if (!swapped)
        {
            break;
        }
    }
}

优化点解释:

        设置标志位 swapped

  • 在每次内层循环开始时,设置一个标志位 swapped 为 0。
  • 如果在内层循环中发生了元素交换,将 swapped 设置为 1。
  • 内层循环结束后,检查 swapped 是否仍为 0。如果是,则说明数组已经有序,可以提前退出外层循环,避免不必要的遍历

注意:

        即使外层循环 i < size - 1 不小心写成了 i < size,内层循环 j < size - i - 1 不小心写成了 j < size - 1,最终的结果也不会错。这是因为在这些情况下,虽然会进行一些多余的比较和交换,但不会影响最终的排序结果。

  1. 外层循环 i < size

    • 正确的外层循环条件是 i < size - 1,这样可以确保在最后一趟遍历时,只剩下最后一个元素,而这个元素已经是有序的。
    • 如果写成 i < size,外层循环会多进行一次遍历,即进行 size 次遍历。
    • 在第 size 次遍历时,内层循环 j < size - 1 会比较所有相邻的元素,但由于数组已经排序完成,不会有元素交换发生。因此,这次遍历是多余的,但不会影响最终的排序结果。
  2. 内层循环 j < size - 1

    • 正确的内层循环条件是 j < size - i - 1,这样可以确保每次遍历只比较未排序部分的元素。
    • 如果写成 j < size - 1,内层循环会比较所有相邻的元素,包括已经排序好的部分。
    • 由于已经排序好的部分不会再发生交换,这些多余的比较不会影响最终的排序结果。

        尽管这些错误不会影响最终的排序结果,但会导致不必要的比较和交换,从而降低算法的效率。因此,建议使用正确的循环条件以提高性能。

2.3 时间空间复杂度分析

2.3.1 时间复杂度分析

  • 最坏情况(Worst Case)

    • 当输入数组是完全逆序的时,冒泡排序需要进行最多的比较和交换操作
    • 每次内层循环都需要进行 n−1 次比较和最多 n−1 次交换
    • 因此,总的时间复杂度为 O(n^2)
  • 最好情况(Best Case)

    • 当输入数组已经是有序的时,冒泡排序只需要进行一次完整的遍历,而且不会进行任何交换
    • 使用优化后的冒泡排序(带有标志位 swapped),可以在第一次遍历后就提前结束。
    • 因此,总的时间复杂度为 O(n)
  • 平均情况(Average Case)

    • 对于随机分布的数组,冒泡排序的时间复杂度通常也是 O(n^2)
    • 这是因为在大多数情况下,数组不会完全有序,也不会完全逆序,但仍然需要多次比较和交换。

2.3.2 空间复杂度分析

  • 原地排序
    • 冒泡排序是一种原地排序算法,即它只需要常数级的额外存储空间。
    • 主要的额外空间用于临时变量(如交换元素时使用的 temp 变量)。
    • 因此,空间复杂度为 O(1)

3 快速排序

3.1 算法原理

        快速排序(Quick Sort)是由图灵奖获得者 Tony Hoare 发明的一种高效排序算法,被列为 20 世纪十大算法之。快速排序以其卓越的性能和简洁的实现方式,在实际应用中非常广泛,尤其是在处理大规模数据时表现尤为出色。快速排序的时间复杂度为 O(n log n)通常比其他同样具有 O(n log ⁡n) 时间复杂度的排序算法更快

3.1.1 排序思想

        快速排序的核心思想是 “分治法”,即通过递归的方式将大问题分解成小问题来解决。具体步骤如下:

        1. 选择基准(Pivot)从数列中挑选一个元素作为 “基准”(pivot)。选择基准的方法有多种,常见的包括选择第一个元素、最后一个元素、中间元素或随机选择一个元素。

        2. 分区(Partition)重新排列数列,使得所有比基准值小的元素都摆放在基准的前面,所有比基准值大的元素都摆放在基准的后面。相同的元素可以任意放置。在这个分区操作结束之后,基准元素会处于数列的中间位置,这个位置称为基准的最终位置。

        3. 递归排序递归地对基准左侧的子数列和右侧的子数列进行快速排序。递归的最底层情况是子数列的大小为零或一,这时子数列已经自然排序好了。

3.1.2 递归过程

  • 递归的最底层当子数列的大小为零或一时,递归结束。这是因为单个元素或空数列自然是有序的。

  • 递归的迭代在每次递归调用中,快速排序都会将一个元素放到其最终位置。因此,随着递归的进行,越来越多的元素会被正确排序,最终整个数列将变得有序。

3.2 示例

3.2.1 示例 1

        假设有一个数列 [3, 6, 8, 10, 1, 2, 1],我们选择第一个元素 3 作为基准:

1. 分区

  • 重新排列数列,使得所有比 3 小的元素在左边,所有比 3 大的元素在右边。结果可能是 [1, 2, 1, 3, 10, 6, 8]

2. 递归排序

  • 对基准左侧的子数列 [1, 2, 1] 进行快速排序。
  • 对基准右侧的子数列 [10, 6, 8] 进行快速排序。

3. 递归结束

  • 当子数列的大小为零或一时,递归结束。

        通过上述步骤,快速排序能够高效地对数列进行排序。尽管快速排序在最坏情况下(例如数列已经有序或完全逆序)的时间复杂度退化为 O(n^2),但在实际应用中,通过合理选择基准和优化分区操作,快速排序通常能保持其优秀的性能。

3.2.2 示例 2

3.2.3 示例 3

        第 1 轮操作:

        第 2 轮操作:

3.3 算法实现

#include <stdio.h>


// 子排序函数,实现快速排序的核心逻辑
void subSort(int arr[], int start, int end)
{
    if (start < end)
    {
        // 选择基准元素,这里选择数组的第一个元素
        int base = arr[start];
        int low = start;
        int high = end + 1;

        // 分区操作
        while (1)
        {
            // 从左向右查找大于基准的元素
            while (low < end && arr[++low] <= base)
                ;

            // 从右向左查找小于基准的元素
            while (high > start && arr[--high] >= base)
                ;

            // 如果找到了需要交换的元素
            if (low < high)
            {
                // 交换两个位置的元素
                int temp = arr[low];
                arr[low] = arr[high];
                arr[high] = temp;
            }
            else
            {
                // 分区结束,跳出循环
                break;
            }
        }

        // 交换基准元素和 high 位置的元素
        int temp1 = arr[start];
        arr[start] = arr[high];
        arr[high] = temp1;

        // 递归调用子排序函数,对基准左侧和右侧的子数组进行排序
        subSort(arr, start, high - 1);
        subSort(arr, high + 1, end);
    }
}

// 快速排序主函数
void quickSort(int arr[], int size)
{
    // 调用子排序函数,从数组的第一个元素到最后一个元素
    subSort(arr, 0, size - 1);
}

// 打印数组
void print(int arr[], int size)
{
    for (int i = 0; i < size; i++)
    {
        printf("%d  ", arr[i]);
    }
    printf("\n");
}

int main()
{
    // 定义一个待排序的数组
    int arr[] = {9, -16, 40, 23, -30, -49, 25, 21, 30};
    int length = sizeof(arr) / sizeof(int); // 计算数组长度

    // 输出排序前的数组
    printf("排序前的数组: ");
    print(arr, length);

    // 调用快速排序函数
    quickSort(arr, length);

    // 输出排序后的数组
    printf("排序后的数组: ");
    print(arr, length);


    return 0;
}

        输出结果如下所示:

        另一种实现方法:

#include <stdio.h>

// 函数声明
void quickSort(int arr[], int low, int high);
int partition(int arr[], int low, int high);

// 主函数
int main()
{
    int arr[] = {10, 7, 8, 9, 1, 5, -13, -20, -19, -50};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("原始数组: \n");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }

    // 调用快速排序函数
    quickSort(arr, 0, n - 1);

    printf("\n排序后的数组: \n");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", arr[i]);
    }

    return 0;
}

// 快速排序函数
void quickSort(int arr[], int low, int high)
{
    if (low < high)
    {
        // pi 是分区操作后的基准元素索引
        int pi = partition(arr, low, high);

        // 分别对基准元素左边和右边的子数组进行递归排序
        quickSort(arr, low, pi - 1);  // 排序左子数组
        quickSort(arr, pi + 1, high); // 排序右子数组
    }
}

// 分区函数
int partition(int arr[], int low, int high)
{
    int pivot = arr[high]; // 选择最后一个元素作为基准
    int i = (low - 1);     // i 指向较小元素的索引

    for (int j = low; j <= high - 1; j++)
    {
        // 如果当前元素小于或等于基准
        if (arr[j] <= pivot)
        {
            i++; // 增加较小元素的索引
            // 交换 arr[i] 和 arr[j]
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    // 交换 arr[i+1] 和 arr[high] (或 pivot)
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;

    return (i + 1); // 返回基准元素的最终位置
}

        输出结果如下所示:

3.4 时间空间复杂度分析

3.4.1 时间复杂度分析

  • 最坏情况(Worst Case)

    • 当输入数组已经有序或完全逆序时,每次划分只能将数组分成一个元素和剩下的元素,导致递归深度为 n。
    • 每次划分需要 O(n) 的时间,因此总的时间复杂度为 O(n^2)
  • 最好情况(Best Case)

    • 当每次划分都能将数组均匀分成两个相等的部分时,递归深度为 log n。
    • 每次划分需要 O(n) 的时间,因此总的时间复杂度为 O(n log n)
  • 平均情况(Average Case)

    • 对于随机分布的数组,快速排序的平均时间复杂度也是 O(n log n)。
    • 这是因为在大多数情况下,数组的划分接近均匀。

3.4.2 空间复杂度分析

  • 递归栈空间

    • 快速排序是一个递归算法,递归调用的深度决定了所需的空间。
    • 在最坏情况下,递归深度为 n,因此空间复杂度为 O(n)。
    • 在最好和平均情况下,递归深度为 log⁡ n,因此空间复杂度为 O(log n)
  • 辅助空间

    • 快速排序是原地排序算法,除了递归栈空间外,只需要常数级的额外存储空间。
    • 因此,辅助空间复杂度为 O(1)。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RHCE--网络服务

第一章 例行性工作 1、单一执行的例行性工作&#xff08;at&#xff09; 1.1 查看at命令 at的黑名单&#xff08;deny&#xff09;、白名单&#xff08;allow&#xff09;&#xff1b;两个文件若都不存在则只有root用户能使用 at工作调度对应的系统服务 atd&#xff1a;at的…

N9305高品质mp3音频语音芯片ic在早教故事机的应用方案

随着人们对教育的重视程度不断提高&#xff0c;儿童早教机已经成为了很多家庭的教育必备品。N9305音乐芯片在早教故事机中的应用&#xff0c;不仅为孩子们带来了丰富多彩的故事世界&#xff0c;还以其卓越的音质表现和功能&#xff0c;进一步提升了早教体验。 九芯电子N9305高品…

单片机——ADC采样

1、什么是ADC采样&#xff1f; ADC是指将模拟信号转换成数字信号的过程。通俗理解ADC采样就是采集电路中的电压&#xff0c;通过数值的方式表现出来。以STM32F103系列为例&#xff0c;它可以反应0~4095&#xff0c;换句话说&#xff0c;它采集的电压数值上表现为0~4095&#xf…

前端文件流导出

1、前端代码 ​ /** 导出 */ const handleExport async () > {let config {responseType: blob,headers: {Content-Type: application/json,},};const res await getTargetExport(config);const blob new Blob([res]);const fileName PK目标跟进导出列表.xls;const li…

WEB前端作业1

<!DOCTYPE html> <html><head><meta charset"utf-8"><title>用户注册页面</title></head><style type"text/css">#center{text-align: center;background-color: #e9e9e9;}tr td,th{border:1px solid whi…

linux线程 | 同步与互斥 | 互斥(下)

前言&#xff1a;本篇文章主要讲述linux线程的互斥的知识。 讲解流程为先讲解锁的工作原理&#xff0c; 再自己封装一下锁并且使用一下。 做完这些就要输出一堆理论性的东西&#xff0c; 但博主会总结两条结论&#xff01;&#xff01;最后就是讲一下死锁。 那么&#xff0c; 废…

Java-多线程2

什么是线程&#xff1f; 线程是 cpu调度和执行的单位。 多个线程共享进程的堆和方法区资源&#xff0c;但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。 如何实现线程 继承Thread类 实现步骤&#xff1a; 创建自定义类&#xff0c;继承Thread类 重写run方法 创建自定…

深度学习面试笔试之循环神经网络(RNN)、门控循环单元(GRU)、长短期记忆(LSTM)

深度学习面试笔试之循环神经网络RNN、门控循环单元GRU、长短期记忆LSTM 循环神经网络(RNN)1. 什么是RNN1.1 RNN的应用1.2 为什么有了CNN&#xff0c;还要RNN?1.3 RNN的网络结构1.4 双向RNN1.5 BPTT算法 2. 其它类型的RNN3. CNN与RNN的区别4. 为什么RNN 训练的时候Loss波动很大…

aws(学习笔记第七课) 私有子网使用NAT服务器

aws(学习笔记第七课) AWS的私有子网使用NAT服务器 学习内容&#xff1a; AWS的私有子网使用NAT服务器 1. AWS的私有子网使用NAT服务器 在上面的例子的网络构成图中&#xff0c;可能会发现一个问题。就是Private Subnet的Apache server无法访问互联网。比如&#xff0c;当需要…

MySQL【知识改变命运】10

联合查询 0.前言1.联合查询在MySQL里面的原理2.练习一个完整的联合查询2.1.构造练习案例数据2.2 案例&#xff1a;⼀个完整的联合查询的过程2.2.1. 确定参与查询的表&#xff0c;学⽣表和班级表2.2.2. 确定连接条件&#xff0c;student表中的class_id与class表中id列的值相等2.…

Win11右键默认显示更多选项

Win11默认显示 想要效果 解决方案1 先按住Shift键&#xff0c;再按右键试试。 解决方案2 1.启动命令行&#xff0c;输入命令 reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve2.显示操作成功完成&#…

2024java高频面试之JVM-第二弹

什么是 STW Java 中「「Stop-The-World机制简称 STW」」 &#xff0c;是在执行垃圾收集算法时&#xff0c;Java 应用程序的其他所有线程都被挂起&#xff08;除了垃圾收集帮助器之外&#xff09;。「Java 中一种全局暂停现象&#xff0c;全局停顿」&#xff0c;所有 Java 代码…

子比美化 – WP添加网站翻译功能 | 实现国际化多语言[js翻译]

前言 本教程适用于子比主题&#xff0c;其它程序或主题请自行适配&#xff01;&#xff01;&#xff01; 图片展示 目前支持五种语言 教程开始 首先在后台自定义CSS代码中添加以下代码 .ignore:hover{color:var(--theme-color);transition:color .2s,transform .3s;}#tran…

怎么通过docker搭建一个mqtt服务器

由于debug需要排查mqtt的连接问题&#xff0c;为了方便&#xff0c;自己在云服务器上搭建一个mqtt服务器。 文中涉及的IP是虚构的IP&#xff0c;请替换成自己云服务器的IP&#xff0c;如有雷同&#xff0c;纯属巧合。 大致分为三部分&#xff1a; 一、安装docker 二、安装m…

cisco网络安全技术第3章测试及考试

测试 使用本地数据库保护设备访问&#xff08;通过使用 AAA 中央服务器来解决&#xff09;有什么缺点&#xff1f; 试题 1选择一项&#xff1a; 必须在每个设备上本地配置用户帐户&#xff0c;是一种不可扩展的身份验证解决方案。 请参见图示。AAA 状态消息的哪一部分可帮助…

<Project-11 Calculator> 计算器 0.2 工时计算器 WorkHours Calculator HTTP + JS

灵感 给工人发工资是按小时计算的&#xff0c;每次都要上网&#xff0c;我比较喜欢用 Hours Calculator &#xff0c;也喜欢它的其它的功能&#xff0c; 做个类似的。 我以为是 Python&#xff0c;结果在学 javascript 看 HTML&#xff0c;页面的基础还停留在 Frontpage 2000…

Cloudlog delete_oqrs_line 未授权SQL注入漏洞复现

0x01 产品简介 Cloudlog 是一个自托管的 PHP 应用程序,可让您在任何地方记录您的业余无线电联系人。使用PHP和MySQL构建的基于Web的业余无线电记录应用程序支持从HF到微波的一般站记录任务 0x02 漏洞概述 Cloudlog delete_oqrs_line 接口存在未授权SQL注入漏洞,未经身份验…

Marin说PCB之GMSL2 的Layout走线的注意事项

昨天有一位铁粉私信问我能不能讲解一下GMSL走线的一些注意事项啥的&#xff0c;我说当等我从以色列出差回来就给你更新一下这个&#xff0c;当然后来又很多的热心的粉丝提出很多的想法&#xff0c;我会一一给大家解答分享的&#xff0c;本期文章主要先给大家分享一下美信的手册…

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json 简介 pickle 模块 json 模块 pickle VS json 简介 什么叫序列化&#xff1f; 序列化指的是将对象转换为可以在网络上传输或者存储到文件系统中的字节流的过程。序列化使得对象可以被保存、传输和恢复&#…

机器学习与神经网络:科技的星辰大海

前提 近日&#xff0c;2024年诺贝尔物理学奖颁发给了机器学习与神经网络领域的研究者&#xff0c;这是历史上首次出现这样的情况。这项奖项原本只授予对自然现象和物质的物理学研究作出重大贡献的科学家&#xff0c;如今却将全球范围内对机器学习和神经网络的研究和开发作为了一…