常见的排序算法分析

简介: 排序是将一组数据元素序列重新排列,使得数据元素序列按某个数据项(关键字)有序。不管是在数据处理中,还是别的应用场景中,对数据的排序都是很有必要的。对于任意的数据元素序列,若排序前后所有相同关键字的相对位置不变,则称该排序算法为稳定的排序方法,否则为不稳定的排序方法。例如对下面数据做排序操作:3, 2, 3, 4 -> 2, 3, 3, 4带下划线的3的位置发生了变化,则该排序方法是不稳定的。


一、概述



排序是将一组数据元素序列重新排列,使得数据元素序列按某个数据项(关键字)有序。不管是在数据处理中,还是别的应用场景中,对数据的排序都是很有必要的。对于任意的数据元素序列,若排序前后所有相同关键字的相对位置不变,则称该排序算法为稳定的排序方法,否则为不稳定的排序方法。例如对下面数据做排序操作:

3, 2, 3, 4   ->   2, 3, 3, 4

带下划线的3的位置发生了变化,则该排序方法是不稳定的。


二、冒泡排序



冒泡排序是将相邻两个关键字对比,若为 a1>a2 则进行交换,然后依次比较后面的数据,直到最后,每一趟冒泡排序,都会确定最大的关键字记录并放至最后。假设我们有一组数据 4,6,3,5,1,2,对它进行冒泡排序过程如下:

微信图片1111.png

微信图片11111.gif

算法实现代码如下:

public static int[] bubbleSort(int[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        for (int j = 0; j < a.length - i - 1; j++) {
            if (a[j] > a[j + 1]) {
                int tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
            }
        }
    }
    return a;
}

根据上面代码可以计算出时间复杂度如下:

(n-1) + (n-2) + ...  + (n-i) + ...  + 1 = n(n-1)/2

所以时间复杂度为 O(n^2),只使用一个临时变量,空间复杂度为 O(1)


三、选择排序



选择排序是从无序的序列中选择关键字最小或最大的记录,并将它加入到有序的子序列中,如果我们每次选择最小的记录,每进行一趟排序,就会确定最小的关键字并放至最前。假设我们有一组数据 4,6,3,5,1,2,对它进行选择排序过程如下:微信图片2222.png

微信图片22222.gif算法实现代码如下:

public static int[] selectSort(int[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        for (int j = i + 1; j < a.length; j++) {
            if (a[i] > a[j]) {
                int tmp = a[j];
                a[j] = a[i];
                a[i] = tmp;
            }
        }
    }
    return a;
}

根据上面代码可以计算出时间复杂度如下:

(n-1) + (n-2) + ...  + (n-i) + ...  + 1 = n(n-1)/2

所以时间复杂度为 O(n^2),只使用一个临时变量,空间复杂度为 O(1)


四、插入排序



插入排序是将无序的数据序列中的一个或几个记录插入到有序子序列中,从而增加有序子序列的长度。每一趟排序都会确定最小的关键字并放至最前。假设我们有一组数据 4,6,3,5,1,2,对它进行插入排序过程如下:

微信图片3333.png

微信图片33333.gif

算法实现代码如下:

public static int[] insertSort(int[] a) {
    int tmp;
    for (int i = 1; i < a.length; i++) {
        int j = i - 1;
        tmp = a[i];
        for (; j >= 0 && tmp < a[j]; j--) {
            a[j + 1] = a[j];
        }
        a[j + 1] = tmp;
    }
    return a;
}

根据上面代码可以计算出时间复杂度如下:

n + (n-1) + (n-2) + ...  + (n-i) + ...  + 2 = (n+2)(n-1)/2

所以时间复杂度为 O(n^2),只使用一个临时变量,空间复杂度为 O(1)


五、归并排序



归并排序是将两个或两个以上的有序表组合成一个新的有序表,我们在进行归并排序时会将数据元素序列,按照 n/2 的长度递归拆分,然后再两两合并,每一个合并的操作都只是对于两个有序表进行合并,直至得到一个长度为 n 的有序序列为止。假设我们有一组数据 4,6,3,5,1,2,对它进行归并排序过程如下:

微信图片4444.png

微信图片44444.gif

算法实现代码如下:

public static int[] mergeSort(int[] a, int l, int r) {
    if (l < r) {
        int m = (l + r) / 2;
        mergeSort(a, l, m);
        mergeSort(a, m + 1, r);
        merge(a, l, m, r);
    }
    return a;
}
private static void merge(int[] a, int l, int m, int r) {
    int n1 = m - l + 1;
    int n2 = r - m;
    int[] L = new int[n1];
    int[] R = new int[n2];
    System.arraycopy(a, l, L, 0, n1);
    System.arraycopy(a, m + 1, R, 0, n2);
    int i = 0, j = 0, k = l;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            a[k++] = L[i++];
        } else {
            a[k++] = R[j++];
        }
    }
    while (i < n1) {
        a[k++] = L[i++];
    }
    while (j < n2) {
        a[k++] = R[j++];
    }
}

根据上面代码可以得出,每一趟归并的时间复杂度为 O(n),总共需要归并 log2n 趟,因而总的时间复杂度为 O(n*log n),在归并的过程中,需要一个与数据表等长的数组空间,因此,空间复杂度为 O(n)


六、快速排序



快速排序是实际使用中已知的最快的算法,快速排序在数据元素中选择一个数据元素为枢纽,然后把比它小的放前面,比它大的放后面,这样分成两堆,然后对两个小堆做同样的操作,以此类推,直到不能分为止,最后返回最终排序结果,快速排序的设计,关键在于枢纽的选择。假设我们有一组数据 4,6,3,5,1,2,选择第一个关键字作为枢纽,对它进行快速排序过程如下:

微信图片5555.png

微信图片55555.gif

算法实现代码如下:

public static int[] quicklySort(int[] a, int l, int r) {
    if (l >= r) {
        return a;
    }
    int i = l, j = r, k = a[l];
    while (i != j) {
        while (i < j && a[j] > k) {
            j--;
        }
        a[i] = a[j];
        while (i < j && a[i] <= k) {
            i++;
        }
        a[j] = a[i];
        a[i] = k;
    }
    quicklySort(a, l, j - 1);
    quicklySort(a, j + 1, r);
    return a;
}

如果每次总是选到中间值作为枢纽,那快速排序可以达到最好的情况,时间复杂度为 O(n*log n),如果每次总是选到最小或最大值作为枢纽,最坏情况时间复杂度为 O(n^2)。对应的空间复杂度最坏情况为 O(n),平均情况为 O(log n)

读者可以去网上找资料,了解快速排序中确定枢纽位置的相关算法。

七、总结



五种排序算法对比

算法 冒泡排序 选择排序 插入排序 归并排序 快速排序
稳定性 稳定 不稳定 稳定 稳定 不稳定
平均时间复杂度 O(n^2) O(n^2) O(n^2) O(n*log n) O(n*log n)
最坏时间复杂度 O(n^2) O(n^2) O(n^2) O(n*log n) O(n^2)
空间复杂度 O(1) O(1) O(1)


递归与分治策略


前面讲到的归并排序和快速排序,都使用到了分治策略,为了解决一个规模较大的问题,我们可以将其分解成两个子问题,并借助递归分别得到它们的解,然后将子问题的解合并成原问题的解,这就是分治策略。我们把一个规模大的问题,分解成 k 个子问题,对这 k 个子问题分别求解,如果子问题的规模仍然不够小,则再划分为 k 个子问题,如此递归的进行下去,直到问题的规模足够小,很容易求出其解为止。微信图片6666.png

目录
相关文章
|
2月前
|
机器学习/深度学习 算法 搜索推荐
从理论到实践,Python算法复杂度分析一站式教程,助你轻松驾驭大数据挑战!
【10月更文挑战第4天】在大数据时代,算法效率至关重要。本文从理论入手,介绍时间复杂度和空间复杂度两个核心概念,并通过冒泡排序和快速排序的Python实现详细分析其复杂度。冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1);快速排序平均时间复杂度为O(n log n),空间复杂度为O(log n)。文章还介绍了算法选择、分而治之及空间换时间等优化策略,帮助你在大数据挑战中游刃有余。
65 4
|
4月前
|
数据采集 机器学习/深度学习 算法
|
4月前
|
人工智能 算法 BI
第一周算法设计与分析 D : 两面包夹芝士
这篇文章介绍了解决算法问题"两面包夹芝士"的方法,通过找出两个数组中的最大最小值,计算这两个值之间的整数个数,包括特判不存在整数的情况。
|
2月前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
2月前
|
算法
PID算法原理分析
【10月更文挑战第12天】PID控制方法从提出至今已有百余年历史,其由于结构简单、易于实现、鲁棒性好、可靠性高等特点,在机电、冶金、机械、化工等行业中应用广泛。
|
3月前
|
算法 搜索推荐 开发者
别再让复杂度拖你后腿!Python 算法设计与分析实战,教你如何精准评估与优化!
在 Python 编程中,算法的性能至关重要。本文将带您深入了解算法复杂度的概念,包括时间复杂度和空间复杂度。通过具体的例子,如冒泡排序算法 (`O(n^2)` 时间复杂度,`O(1)` 空间复杂度),我们将展示如何评估算法的性能。同时,我们还会介绍如何优化算法,例如使用 Python 的内置函数 `max` 来提高查找最大值的效率,或利用哈希表将查找时间从 `O(n)` 降至 `O(1)`。此外,还将介绍使用 `timeit` 模块等工具来评估算法性能的方法。通过不断实践,您将能更高效地优化 Python 程序。
63 4
|
3月前
|
算法 程序员 Python
程序员必看!Python复杂度分析全攻略,让你的算法设计既快又省内存!
在编程领域,Python以简洁的语法和强大的库支持成为众多程序员的首选语言。然而,性能优化仍是挑战。本文将带你深入了解Python算法的复杂度分析,从时间与空间复杂度入手,分享四大最佳实践:选择合适算法、优化实现、利用Python特性减少空间消耗及定期评估调整,助你写出高效且节省内存的代码,轻松应对各种编程挑战。
52 1
|
2月前
|
算法
PID算法原理分析及优化
【10月更文挑战第6天】PID控制方法从提出至今已有百余年历史,其由于结构简单、易于实现、鲁棒性好、可靠性高等特点,在机电、冶金、机械、化工等行业中应用广泛。
|
3月前
|
算法 数据可视化
基于SSA奇异谱分析算法的时间序列趋势线提取matlab仿真
奇异谱分析(SSA)是一种基于奇异值分解(SVD)和轨迹矩阵的非线性、非参数时间序列分析方法,适用于提取趋势、周期性和噪声成分。本项目使用MATLAB 2022a版本实现从强干扰序列中提取趋势线,并通过可视化展示了原时间序列与提取的趋势分量。代码实现了滑动窗口下的奇异值分解和分组重构,适用于非线性和非平稳时间序列分析。此方法在气候变化、金融市场和生物医学信号处理等领域有广泛应用。
161 19
|
4月前
|
算法
算法设计与分析作业
这篇文章是关于算法设计与分析的作业,其中包含了两个算法实现:一个是使用分治算法实现的十进制大整数相乘(包括加法、减法和乘法函数),并进行了正确性和健壮性测试;另一个是使用快速排序思想实现的分治查找第K小元素的程序,并分析了其平均和最坏时间复杂度。
算法设计与分析作业