【唐叔学算法】第19天:交换排序-冒泡排序与快速排序的深度解析及Java实现

引言

排序算法是计算机科学中的基础问题,而交换排序作为其中一类经典的排序方法,因其简单直观的思想和易于实现的特点,在初学者中广受欢迎。交换排序的核心思想是通过不断交换相邻元素来达到排序的目的。本文将深入探讨两种典型的交换排序算法:冒泡排序快速排序,分析它们的原理、实现细节、时间复杂度和空间复杂度,并通过Java代码进行具体实现。

冒泡排序:简单却经典的排序算法

算法原理

冒泡排序(Bubble Sort)是一种基础的交换排序算法,其核心思想是通过重复地遍历待排序的序列,依次比较相邻的两个元素,如果它们的顺序错误就交换它们的位置。这样,每一轮遍历都会将当前未排序部分的最大(或最小)元素“冒泡”到序列的末尾。

算法步骤

  1. 从序列的起始位置开始,比较相邻的两个元素。
  2. 如果前一个元素大于后一个元素,则交换它们的位置。
  3. 对序列中的每一对相邻元素重复上述操作,直到序列末尾。
  4. 重复以上步骤,直到没有需要交换的元素为止。

时间复杂度与空间复杂度

  • 时间复杂度
    • 最坏情况:O(n²),当输入序列是逆序时,需要进行n(n-1)/2次比较和交换。
    • 最好情况:O(n),当输入序列已经有序时,只需进行一次遍历即可。
    • 平均情况:O(n²)。
  • 空间复杂度:O(1),冒泡排序是原地排序算法,不需要额外的存储空间。

Java实现

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            boolean swapped = false; // 优化:如果某一轮没有交换,说明已经有序
            for (int j = 0; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    // 交换
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                }
            }
            if (!swapped) {
                break; // 如果没有交换,提前退出
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(arr);
        System.out.println("Sorted array: ");
        for (int i : arr) {
            System.out.print(i + " ");
        }
    }
}

快速排序:高效的分治排序算法

算法原理

快速排序(Quick Sort)是一种基于分治思想的交换排序算法。其核心思想是选择一个“基准元素”(pivot),将序列分为两部分:一部分比基准元素小,另一部分比基准元素大,然后递归地对这两部分进行排序。

算法步骤

  1. 选择一个基准元素(通常选择第一个元素、最后一个元素或中间元素)。
  2. 将序列分为两部分:小于基准元素的部分和大于基准元素的部分。
  3. 对这两部分分别递归地进行快速排序。
  4. 合并结果。

时间复杂度与空间复杂度

  • 时间复杂度
    • 最坏情况:O(n²),当每次选择的基准元素都是序列的最大或最小值时,快速排序会退化为冒泡排序。
    • 最好情况:O(n log n),当每次选择的基准元素都能将序列均匀地分为两部分时。
    • 平均情况:O(n log n)。
  • 空间复杂度:O(log n),快速排序的空间复杂度主要取决于递归调用的栈空间。

Java实现

public class QuickSort {
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int pivotIndex = partition(arr, low, high); // 获取基准元素的位置
            quickSort(arr, low, pivotIndex - 1); // 递归排序左子序列
            quickSort(arr, pivotIndex + 1, high); // 递归排序右子序列
        }
    }

    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high]; // 选择最后一个元素作为基准
        int i = low - 1; // i指向小于基准的元素的位置

        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                // 交换
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }

        // 将基准元素放到正确的位置
        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;

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

    public static void main(String[] args) {
        int[] arr = {10, 7, 8, 9, 1, 5};
        quickSort(arr, 0, arr.length - 1);
        System.out.println("Sorted array: ");
        for (int i : arr) {
            System.out.print(i + " ");
        }
    }
}

对比与总结

冒泡排序 vs 快速排序

特性冒泡排序快速排序
时间复杂度O(n²)平均 O(n log n),最坏 O(n²)
空间复杂度O(1)O(log n)
稳定性稳定不稳定
适用场景小规模数据或基本有序数据大规模数据或随机数据

选择与应用

  • 冒泡排序:虽然时间复杂度较高,但实现简单,适合教学和小规模数据的排序。
  • 快速排序:虽然实现稍复杂,但时间复杂度较低,适合处理大规模数据,是实际应用中最常用的排序算法之一。

通过本文的讲解和Java实现,希望读者能够深入理解冒泡排序和快速排序的原理和实现细节,并在实际编程中根据需求灵活选择合适的排序算法。

如果你是一名专业的java高级架构师,现在你在面试知识,如下是 数据结构算法的面试知识体系结构图 请按照这个体系给出每个知识点的学习理解方便记忆和消化,同时给出每个知识点的高频面试的标准面试答案,结合项目经验给出解释和实战 数据结构算法面试核心知识体系 ├── 一、复杂度分析基础 │ ├── 时间复杂度 │ │ ├── 大O表示法:O(1), O(logn), O(n), O(nlogn), O(n²)等含义? │ │ ├── 最好、最坏、平均时间复杂度分析? │ │ └── 递归算法的时间复杂度分析:主定理? │ ├── 空间复杂度 │ │ ├── 算法运行所需的额外空间?原地操作含义? │ │ ├── 递归调用的空间复杂度(调用栈深度)? │ │ └── 如何权衡时间空间复杂度? │ └── 实际应用 │ ├── 如何根据数据规模选择合适的算法?(10⁵数据不能用O(n²)) │ ├── 常数项优化在实际工程中的意义? │ └── 摊还分析:某些操作的平均代价? ├── 二、数组、链表字符串 │ ├── 数组(Array) │ │ ├── 特点:随机访问O(1),插入删除O(n)? │ │ ├── 双指针技巧:快慢指针、左右指针、滑动窗口? │ │ ├── 前缀和数组:快速计算区间和? │ │ └── 差分数组:快速进行区间增减操作? │ ├── 链表(Linked List) │ │ ├── 单链表、双链表、循环链表区别? │ │ ├── 虚拟头节点(dummy node)的作用? │ │ ├── 常见问题:反转链表、检测环、相交链表、合并有序链表? │ │ └── 链表排序:归并排序实现? │ └── 字符串(String) │ ├── 字符串匹配算法:KMP、Rabin-Karp? │ ├── 回文串问题:中心扩展法、动态规划? │ ├── 字符串操作:翻转、替换、分割? │ └── 不可变字符串的优势?(线程安全、缓存哈希值) ├── 三、栈、队列哈希表 │ ├── 栈(Stack) │ │ ├── LIFO特性,应用场景:函数调用栈、括号匹配? │ │ ├── 单调栈:解决"下一个更大元素"问题? │ │ └── 最小栈:如何O(1)获取栈中最小值? │ ├── 队列(Queue) │ │ ├── FIFO特性,BFS算法基础? │ │ ├── 优先队列(堆):获取最值,Dijkstra算法? │ │ ├── 单调队列:解决滑动窗口最值问题? │ │ └── 双端队列(Deque):实现滑动窗口? │ └── 哈希表(Hash Table) │ ├── 原理:哈希函数、冲突解决(链地址法、开放寻址法)? │ ├── 设计哈希集合、哈希映射? │ ├── 实际应用:缓存(LRU)、快速查找、去重? │ └── 哈希碰撞攻击原理?如何设计好的哈希函数? ├── 四、树形数据结构 │ ├── 二叉树基础 │ │ ├── 遍历方式:前序、中序、后序(递归/迭代)? │ │ ├── 二叉搜索树(BST):性质、查找、插入、删除? │ │ ├── 平衡二叉树:AVL树、红黑树基本概念? │ │ └── 完全二叉树、满二叉树定义? │ ├── 树的变种 │ │ ├── 堆(Heap):大顶堆、小顶堆,优先队列实现? │ │ ├── Trie树(前缀树):字符串前缀匹配? │ │ ├── 线段树:区间查询、区间更新? │ │ └── 树状数组(BIT):单点更新、前缀查询? │ └── 树的应用 │ ├── 最近公共祖先(LCA)问题? │ ├── 二叉树的序列化反序列化? │ ├── 树的直径、高度、路径和问题? │ └── 哈夫曼编码:数据压缩? ├── 五、图论算法 │ ├── 图的表示 │ │ ├── 邻接矩阵 vs 邻接表?空间时间复杂度? │ │ ├── 有向图、无向图、加权图? │ │ └── 图的遍历:BFS、DFS实现? │ ├── 最短路径算法 │ │ ├── Dijkstra算法:非负权图,贪心策略? │ │ ├── Bellman-Ford算法:处理负权边? │ │ ├── Floyd-Warshall算法:多源最短路径? │ │ └── A算法:启发式搜索? │ ├── 最小生成树 │ │ ├── Prim算法:从点开始扩展? │ │ ├── Kruskal算法:按边排序+并查集? │ │ └:适用场景:网络布线、电路设计? │ └── 其他图算法 │ ├── 拓扑排序:有向无环图(DAG),课程安排? │ ├── 并查集(Union-Find):连通分量,路径压缩优化? │ ├── 欧拉路径/回路:一笔画问题? │ └── 强连通分量:Kosaraju或Tarjan算法? ├── 六、排序搜索算法 │ ├── 排序算法 │ │ ├── 比较排序:冒泡、选择、插入、归并、快速、堆排序? │ │ ├── 非比较排序:计数排序、桶排序、基数排序? │ │ ├── 稳定性分析:哪些是稳定排序? │ │ └── 各排序算法的时间/空间复杂度总结? │ ├── 搜索算法 │ │ ├── 二分查找:模板、边界处理、旋转数组搜索? │ │ ├── DFS深度优先:回溯法,排列组合问题? │ │ ├── BFS广度优先:最短路径,层级遍历? │ │ └── 启发式搜索:A算法,估价函数设计? │ └── 实际应用 │ ├── 海量数据排序:外部排序,归并思想? │ ├── 第K大/小元素:快速选择算法O(n)? │ ├── 在排序数组中查找目标值的起始和结束位置? │ └── 寻找旋转排序数组中的最小值? ├── 七、动态规划(DP) │ ├── 基本思想 │ │ ├── 重叠子问题、最优子结构? │ │ ├── 自顶向下(记忆化递归) vs 自底向上(迭代)? │ │ └── 状态定义、状态转移方程、边界条件? │ ├── 经典问题 │ │ ├── 背包问题:0-1背包、完全背包、状态压缩? │ │ ├── 最长公共子序列(LCS)、最长递增子序列(LIS)? │ │ ├── 编辑距离:字符串转换的最小操作数? │ │ ├── 股票买卖问题:多种限制条件? │ │ └── 打家劫舍、零钱兑换、路径问题? │ └── 解题技巧 │ ├── 如何识别DP问题?状态设计技巧? │ ├── 空间优化:滚动数组? │ ├── 输出具体方案而不仅仅是数值? │ └── 数位DP、状压DP、区间DP简介? ├── 八、贪心算法 │ ├── 基本概念 │ │ ├── 贪心选择性质?局部最优导致全局最优? │ │ ├── 动态规划的区别?贪心无法回溯? │ │ └── 如何证明贪心策略的正确性? │ ├── 典型问题 │ │ ├── 区间调度:最多不重叠区间? │ │ ├── 哈夫曼编码:最优前缀码? │ │ ├── 分糖果、跳跃游戏、加油站问题? │ │ ├:贪心+排序:根据某个指标排序后贪心? │ │ └:贪心+优先队列:实时获取最优解? │ └── 应用场景 │ ├── 最小生成树:Prim和Kruskal算法中的贪心思想? │ ├── 最短路径:Dijkstra算法的贪心选择? │ ├── 数据压缩:贪心构造最优编码? │ └── 任务调度:合理安排任务顺序? ├── 九、高级数据结构算法 │ ├── 高级数据结构 │ │ ├── 跳表(Skip List):Redis有序集合实现? │ │ ├── 布隆过滤器(Bloom Filter):判断存在性,可能误判? │ │ ├── LRU缓存:哈希表+双向链表实现? │ │ ├── 一致性哈希:分布式系统数据分片? │ │ └── 倒排索引:搜索引擎核心? │ ├── 数学位运算 │ │ ├── 位操作技巧:判断奇偶、交换数值、找出单独数字? │ │ ├── 素数判断、最大公约数(GCD)、最小公倍数(LCM)? │ │ ├── 快速幂算法:计算a^b % mod? │ │ └── 随机数生成:拒绝采样,水塘抽样? │ └── 高级算法思想 │ ├── 分治算法:归并排序快速排序、最近点对? │ ├── 回溯算法:N皇后、数独、全排列? │ ├── 位运算优化状态表示(状态压缩)? │ ├:扫描线算法:矩形面积并、际线问题? │ └:摩尔投票法:寻找多数元素? └── 十、实战技巧系统设计 ├── 解题方法论 │ ├── 解题步骤:理解题意 -> 分析 -> 选择数据结构 -> 编码 -> 测试? │ ├── 边界条件考虑:空输入、极端值、溢出? │ ├── 代码规范:变量命名、注释、模块化? │ └── 测试用例设计:正常情况、边界情况、错误情况? ├── 系统设计中的应用 │ ├── LRU缓存设计:哈希表+双向链表? │ ├── 排行榜设计:跳表、有序集合? │ ├── 短网址系统:发号器、哈希算法? │ ├:朋友圈设计:并查集管理社交关系? │ └:搜索提示:Trie树实现自动补全? └── 海量数据处理 ├── 分治思想:大数据分解为小数据? ├── 哈希分片:数据均匀分布到多台机器? ├── 位图法:判断存在性,节省空间? ├:堆的应用:Top K问题? └:外部排序:内存不足时如何排序
09-29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唐叔在学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值