今天分享的是三种排序算法, 在面试, 实际编程中经常会碰到和使用到的, 我会带领大家从分析排序算法技巧上以及代码实现上全面理解这一知识点的掌握.
一, 如何分析一个「排序算法」
1. 执行效率
1 最好, 最坏, 平均时间复杂度
在分析算法的好坏时, 要分别说出最好, 最坏, 平均时间复杂度的同时, 也要说出最好, 最坏时间复杂度对应排序的原始数据是什么样的.
2 复杂度系数, 常数, 低阶
时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势, 它表示的时候会忽略系数, 常数, 低阶 , 小规模数据除外.
3 比较次数和移动次数
基于比较的排序算法, 在分析算法效率时, 我们要考虑到元素的比较和元素的移动.
2. 内存消耗
算法的内存消耗可以通过空间复杂度来衡量, 排序算法也不例外. 我们引用一个名词叫做「原地排序」, 就是指特定空间复杂度是 O(1) 的排序算法.
3. 稳定性
如果待排序的序列中存在值相等的元素, 经过排序之后, 相等元素之间原有的先后顺序不变, 就叫做「稳定排序」.
二, 冒泡排序
1, 算法思想
每次冒泡对相邻的两个元素进行比较, 看是否满足大小关系, 不满足就进行互换, 一次冒泡会让至少一个元素移动到它应该在的位置. 有 n 个数据, 需要重复 n 次.
2, 算法优化
当某次冒泡过程已经没有数据交换时, 说明已经达到完全有序, 不用再执行后续的冒泡操作.
3, 代码实现
- // 冒泡排序, a 表示数组, n 表示数组大小
- public void bubbleSort(int[] a, int n) {
- if (n <= 1) return;
- for (int i = 0; i <n; ++i) {
- // 提前退出冒泡循环的标志位
- boolean flag = false;
- for (int j = 0; j <n - i - 1; ++j) {
- if (a[j]> a[j+1]) { // 交换
- int tmp = a[j];
- a[j] = a[j+1];
- a[j+1] = tmp;
- flag = true; // 表示有数据交换
- }
- }
- if (!flag) break; // 没有数据交换, 提前退出
- }
- }
4, 问题思考
1 是否为原地排序
冒泡的过程只涉及相邻数据的交换操作, 只需要常量级的临时空间, 所以它的空间复杂度为 O(1), 是一个原地排序算法.
2 是否为稳定排序
在冒泡排序中, 只有交换才可以改变两个元素的前后顺序. 为了保证冒泡排序算法的稳定性, 当有相邻的两个元素大小相等的时候, 我们不做交换, 相同大小的数据在排序前后不会改变顺序, 所以冒泡排序是稳定的排序算法.
3 最好, 最坏以及平均时间复杂度
最好的情况是数据已经排好序, 我们只进行一次冒泡排序就可以了, 最好时间复杂度为 O(n) . 最坏的情况是, 要排序的数据刚好是倒序排列的, 我们只进行 n 此冒泡操作, 所以最坏的时间复杂度为 O(n²), 平均时间复杂度为 O(n²).
三, 插入排序 (Insertion Sort)
1, 算法思想
我们将元素分为两个区间, 未排序区间和已排序区间. 我们要做的就是在未排序区间取出元素与已排序区间元素进行比较插入到适当位置, 以此类推, 直到未排序区间元素为空为止 (顺序为从后向前比较).
2, 代码实现
- // 插入排序, a 表示数组, n 表示数组大小 (从小到大进行排序)
- public void insertionSort(int[] a, int n) {
- // 如果数组大小为 1 直接返回
- if (n <= 1) return;
- // 否则进行插入排序
- for (int i = 1; i <n; ++i) {
- int value = a[i];
- int j = i - 1;
- // 查找插入的位置
- for (; j>= 0; --j) {
- if (a[j]> value) {
- a[j+1] = a[j]; // 数据移动
- } else {
- break;
- }
- }
- a[j+1] = value; // 插入数据
- }
- }
3, 问题思考
1 是否为原地排序?
答: 插入排序的运算并不需要额外的存储空间, 所以空间复杂度是 O(1), 是一个原地排序算法.
2 是否为稳定排序?
答: 在插入排序中, 对于值相同的元素, 我们会将后边出现的元素插入到前边出现的元素的后边, 所以插入排序是稳定排序.
3 最好, 最坏, 平均时间复杂度?
答:
最好的情况就是数据元素已经排好序, 最好的时间复杂度为 O(1) ,
如果数组是倒序的, 每次插入都相当于在数组的第一个位置插入新的数据, 需要移动大量的数据, 最坏的时间复杂度是 O(n²).
我们在数组中插入数据的平均时间复杂度为 O(n), 对于插入排序来说我们每次就相当于数组插入一个新的数据, 循环执行 n 次插入数据, 所以平均时间复杂度为 O(n²).
四, 选择排序
1, 算法思想
和插入排序有点相似, 将在未排序期间寻找到最小的数据, 并将其放到已排好区间的元素的尾部.
2, 问题思考
1 是否为原地排序
因为, 数组中的两个元素需要相互交换, 需要用一个变量来存储交换值, 选择排序的空间复杂度为 O(1), 所以, 是一种原地排序算法.
2 是否为稳定排序
选择排序每次都要找到剩余未排序元素的最小值, 并和前边的元素交换位置, 这样破坏了稳定性. 所以说, 选择排序是一种不稳定的排序算法.
3 最好, 最坏以及平均时间复杂度
选择排序的最好情况就是已经是一组有序数据, 最好的时间复杂度为 O(1), 最坏的情况就是 O(n²). 平均时间复杂度就是 O(n²).
3, 代码实现
- // 选择排序, a 表示数组, n 表示数组大小
- public static void selectionSort(int[] a, int n) {
- if (n <= 1) return;
- for (int i = 0; i <n; ++i) {
- // 查找最小值
- int minIndex = i;
- int minValue = a[i];
- for (int j = i; j <n; ++j) {
- if (a[j] < minValue) {
- minValue = a[j];
- for (int i = 0; i < n - 1; ++i) {
- // 查找最小值
- int minIndex = i;
- for (int j = i + 1; j < n; ++j) {
- if (a[j] < a[minIndex]) {
- minIndex = j;
- }
- }
- if (minIndex == i)
- continue;
- // 交换
- int tmp = a[i];
- a[i] = a[minIndex];
- a[minIndex] = tmp;
- }
- }
五, 实际应用中为什么插入排序应用最为广泛
冒泡排序不管怎么优化, 元素交换的次数是一个固定值, 是原始数据的逆序度. 插入排序是同样的, 不管怎么优化, 元素移动的次数也等于原始数据的逆序度.
从代码实现上来看, 冒泡排序的数据交换要比插入排序的数据移动要复杂, 冒泡排序需要 3 个赋值操作, 而插入排序只需要 1 个.
1 冒泡排序中数据的交换操作:
- if (a[j]> a[j+1]) { // 交换
- int tmp = a[j];
- a[j] = a[j+1];
- a[j+1] = tmp;
- flag = true;
- }
9 插入排序中数据的移动操作:
- if (a[j]> value) {
- a[j+1] = a[j]; // 数据移动
- } else {
- break;
- }
有兴趣的小伙伴可以编几个数据自己测试一下.
虽然冒泡排序和插入排序在在时间复杂度上是一样的, 都是 O(n²), 我们希望把性能优化做到极致, 首选插入排序.
六, 重点掌握
今天重点掌握的内容是三种排序的「分析方法」, 不必要死记硬背. 另一个方面就是实际应用中用到最多就是「插入排序」.
来源: https://yq.aliyun.com/articles/669203