排序—时间复杂度为O(n2)的三种排序算法

1 如何评价、分析一个排序算法?

很多语言、数据库都已经封装了关于排序算法的实现代码。所以我们学习排序算法目的更多的不是为了去实现这些代码,而是灵活的应用这些算法和解决更为复杂的问题,所以更重要的是学会如何评价、分析一个排序算法并在合适的场景下正确使用。

分析一个排序算法,主要从以下3个方面入手:

1.1 排序算法的执行效率

1)最好情况、最坏情况和平均情况时间复杂度

待排序数据的有序度对排序算法的执行效率有很大影响,所以分析时要区分这三种时间复杂度。除了时间复杂度分析,还要知道最好、最坏情况复杂度对应的要排序的原始数据是什么样的。

2)时间复杂度的系数、常数和低阶

时间复杂度反映的是算法执行时间随数据规模变大的一个增长趋势,平时分析时往往忽略系数、常数和低阶。但如果我们排序的数据规模很小,在对同一阶时间复杂度的排序算法比较时,就要把它们考虑进来。

3)比较次数和交换(移动)次数

内排序算法中,主要进行比较和交换(移动)两项操作,所以高效的内排序算法应该具有尽可能少的比较次数和交换次数。

1.2 排序算法的内存消耗

也就是分析算法的空间复杂度。这里还有一个概念—原地排序,指的是空间复杂度为O(1)的排序算法。

1.3 稳定性

如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,那么这种排序算法叫做稳定的排序算法;如果前后顺序发生变化,那么对应的排序算法就是不稳定的排序算法。

在实际的排序应用中,往往不是对单一关键值进行排序,而是要求排序结果对所有的关键值都有序。所以,稳定的排序算法往往适用场景更广。

 

2 三种时间复杂度为O(n2)的排序算法

2.1 冒泡排序

2.1.1 原理

两两比较相邻元素是否有序,如果逆序则交换两个元素,直到没有逆序的数据元素为止。每次冒泡都会至少让一个元素移动到它应该在的位置。

2.1.2 实现

void BubbleSort(int *pData, int n)    //冒泡排序
{
    int temp = 0;
    bool orderlyFlag = false;    //序列是否有序标志

    for (int i = 0; i < n && !orderlyFlag; ++i)    //执行n次冒泡
    {
        orderlyFlag = true;
        for (int j = 0; j < n - 1 - i; ++j)    //注意循环终止条件
        {
            if (pData[j] > pData[j + 1])    //逆序
            {
                orderlyFlag = false;    
                temp = pData[j];
                pData[j] = pData[j + 1];
                pData[j + 1] = temp;
            }
        }
    }
}

测试结果

2.1.3 算法分析

1)时间复杂度

最好情况时间复杂度:当待排序列已有序时,只需一次冒泡即可。时间复杂度为O(n);

最坏情况时间复杂度:当待排序列完全逆序时,需要n次冒泡。时间复杂度为O(n2);

平均情况时间复杂度:当待排序列完全逆序时,逆序度为n * (n - 1) / 2。只有当交换逆序对时才会才会使得有序,取中间逆序度n * (n - 1) / 4,那么就要进行n * (n - 1) /4次交换,而比较的次数大于交换次数,所以平均情况时间复杂度为O(n2)。

2)空间复杂度

只借助了一个临时变量temp,所以空间复杂度为O(1)。

3)稳定性

该算法中只有交换操作会改变数据元素的顺序,只要我们在数据元素值相等时不交换数据元素,那么算法就是稳定的。

4)比较和交换的次数

交换操作的执行次数与逆序度相等,比较操作的执行次数大于等于逆序度小于等于n * (n - 1) / 2。

2.2 插入排序

2.2.1 原理

将待排序序列分为已排序区间和未排序区间,开始时已排序区间只有一个数据元素也就是序列的第一个元素,将未排序区间中的数据元素插入已排序区间中同时保持已排序区间的有序,直到未排序区间没有数据元素。

2.2.2 实现

void InsertSort(int *pData, int n)    //插入排序
{
    int temp = 0, i, j;

    for (i = 1; i < n; ++i)    //未排序区间
    {
        if (pData[i] < pData[i - 1])    //逆序
        {
            temp = pData[i];
            for (j = i - 1; pData[j] > temp; --j)    //搬移数据元素
                pData[j + 1] = pData[j];
            pData[j + 1] = temp;    //插入数据
        }
    }
}

测试结果:

2.2.3 算法分析

1)时间复杂度

最好情况时间复杂度:当待排序列已有序时,只需遍历一次即可完成排序。时间复杂度为O(n);

最坏情况时间复杂度:当待排序列完全逆序时,需要进行n-1次数据搬移和插入操作。时间复杂度为O(n2);

平均情况时间复杂度:与冒泡法的分析过程一样,平均情况时间复杂度为O(n2)。

2)空间复杂度

排序过程中只需要一个临时变量存储待插入数据,空间复杂度为O(1)。

3)稳定性

插入排序过程中只有插入操作会改变数据元素的相对位置,只要元素大小比较时相等情况下不进行插入操作,插入排序算法就是稳定的。

4)比较操作和数据搬移操作执行次数

数据搬移操作执行次数和逆序度相同。比较操作次数大于等于逆序度,小于等于n * (n - 1) / 2。

2.3 选择排序

2.3.1 原理

选择排序的原理类似于插入排序都分为已排序区间和未排序区间,选择排序的已排序区间初始大小为零,每次从未排序区间取关键值最大(或最小)的数据元素放在已排序区间的后一个位置,直到未排序区间没有数据元素则完成排序。

2.3.2 实现

void SelectSort(int *pData, int n)    //选择排序
{
    int i, j, min, temp;

    for (i = 0; i < n; ++i)    //未排序区间
    {
        min = i;    //最小值下标
        for (j = i + 1; j < n; ++j)
        {
            if (pData[min] > pData[j])    //逆序
                min = j;    //保存当前较小值下标
        }
        if (i != min)    //如果不是最小值,交换元素
        {
            temp = pData[i];
            pData[i] = pData[min];
            pData[min] = temp;
        }
    }
}

测试结果:

2.3.3 算法分析

1)时间复杂度

不管是已有序序列还是完全逆序序列,都要进行n次遍历无序区间操作,时间复杂度为O(n2)。

2)空间复杂度

排序过程中只需要保存每次遍历无序区间最小值的下标和第i个元素的数值,所以空间复杂度为O(1)。

3)稳定性

选择排序算法中改变数据元素相对位置的操作为交换操作,当第i次中第i个数据元素不为当前无序区间最小值时则和最小值交换数据元素。当有重复元素时,就有可能发生相对位置改变。例如5,3,4,5,1第一次选择操作后为1,3,4,5,5,此时两个5的相对位置已经改变。所以选择排序算法不是稳定的。

4)比较操作和交换操作的执行次数

比较操作执行次数为n * (n - 1) / 2,交换操作执行次数小于等于n-1。

2.4 三种算法之间的比较

1)一般待排序列长度n较小时,我们选择这三种排序算法;

2)当排序要求稳定时,一般选择插入排序,因为相同的情况下,移动数据比交换数据执行速度快;

3)当数据元素信息量较大时,可以考虑用选择排序,因为它交换操作执行次数最少。

 

该篇博客是自己的学习博客,水平有限,如果有哪里理解不对的地方,希望大家可以指正!

posted @ 2019-04-23 22:08  zpchya  阅读(5947)  评论(0编辑  收藏  举报