【定量分析】冒泡排序运行时间与输入数组元素排列之间的关系

结论:冒泡排序的循环迭代次数等于所有元素的排序距离中的最大值,其中排序距离的定义为:排序后该元素的下标减去当前该元素的下标,值可正可负。即,冒泡排序的时间复杂度为 Θ ( n m ) \Theta(nm) Θ(nm),其中n为数组规模,m为最大排序距离。

众所周知,冒泡排序的时间复杂度为 Θ ( n 2 ) \Theta(n^2) Θ(n2),并且优化后的冒泡排序随着输入数组“无序度”的增加,运行时间也会增加,但这个无序度究竟该如何准确描述?本文将探讨这个问题。

首先给出冒泡排序的算法:

for (int i = 0; i < len - 1; ++i) {
    bool flag = 1;
    for (int j = len - 1; j > i; --j) {
        if (v[j - 1] > v[j]) {
            flag = 0;
            swap(v[j - 1], v[j]);
        }
    }
    if (flag) break;
}

在这里,我们使用循环迭代次数作为运行时间的量度。程序的结束位置只有i=len或者flag=1两处。当程序因为i=len而终止时,循环迭代次数为len-1,但是,当程序因flag被置为1时中断,此时的运行时间该如何判断?

给出一个例子,对于数组【2,5,1,3,4】进行排序:

在这里插入图片描述

可以看出,在这里制约排序发展的关键路径是5的后移操作。由于5每次只能后移一个位置,而总共要后移3个位置,因此运行时间是3次迭代。

我们给出类似的猜想如下,冒泡排序的循环迭代次数等于所有元素的排序距离中的最大值,其中排序距离的定义为:排序后该元素的下标减去当前该元素的下标,值可正可负。

在这里插入图片描述
先进行一次简单的验证,判断该猜想是否可以证伪。

#include <iostream>
#include <vector>
#include <ctime>
using namespace std;

int main() {
    // 随机生成一个数组
    int size = 1000;
    srand(time(NULL));
    vector<int> v;
    for (int i = 0; i < size; ++i) {
        v.push_back(rand() % size);
    }
    // 计算dist = max(sorted(v).index(v[i]) - i)
    int dist = 0;
    for (int i = 0; i < size; ++i) {
        // k是排序后v[i]的下标
        int k = 0;
        for (int j = 0; j < i; ++j) {
            if (v[j] <= v[i]) {
                k++;
            }
        }
        for (int j = i + 1; j < size; ++j) {
            if (v[j] < v[i]) {
                k++;
            }
        }
        // k即为sorted(v).index(v[i])
        dist = max(dist, k - i);
    }
    cout << "dist:" << dist << "\n";
    // 冒泡排序, 并计算总迭代次数
    int iter = -1;  // 最后一次迭代前数组已经有序, 因此减一
    for (int i = 0; i < size - 1; ++i) {
        iter++;
        bool flag = 1;
        for (int j = size - 1; j > i; --j) {
            if (v[j - 1] > v[j]) {
                flag = 0;
                swap(v[j - 1], v[j]);
            }
        }
        if (flag) {
            break;
        }
    }
    cout << "iter:" << iter << "\n";
    // 观察结果判断二者是否相等
}

在多次试验后,发现循环迭代次数始终等于元素最大排序距离。因此,我们现在需要对该结论进行正确性证明。

证明:
(1)每次迭代,数组的元素最大排序距离减一。

设数组中具有最大排序距离的元素为e,其下标为k,排序距离为d,那么,d一定为正整数,且比e大的元素数量不超过len-k-d。在一次迭代的内循环当中,保持有v[j]v[j...len-1]中最小值的性质,因此,当j-1 = k时,v[j] < v[k]。所以swap一定会运行,e会后移一位,排序距离减一。同理,所有排序距离为正的元素的排序距离均减一。因此,最大排序距离减一。

    for (int j = len - 1; j > i; --j) {
        if (v[j - 1] > v[j]) {
            swap(v[j - 1], v[j]);
        }
    }

(2)当数组的最大排序距离为1时,只用一次迭代就可以排好序。

仍然考虑一次迭代的内循环代码,利用数学归纳法,设循环不变量为:v[j+1...len-1]为数组的最大的len-j-1个元素的升序排列
初始:当j=len-1时,v[j+1...len-1]为空,显然成立。
保持:在某一次迭代当中,即j更新为j-1,将v[j]纳入已归位部分的过程中,由于最大排序距离为1,因此v[j-1...len-1]中元素包含了数组的最大len-j-2个元素,所以,v[j-1]或者v[j]中的较大者将被放置在v[j]的位置。因此,迭代结束时,v[j...len-1]为数组的最大的len-j个元素的升序排列。
终止:当j == i时,内循环终止,此时,v[i+1...len-1]为数组的最大的len-i-1个元素的升序排列,而v[0...i]显然为数组的最小的i+1个元素的升序排列,证明结束。
在这里插入图片描述

考虑这一结论的应用:对于一个已排序的、规模很大的数组,发生轻微颠簸后,每个元素都或左移或右移若干位,那么此时采用冒泡排序的时间就应当是 Θ ( n m ) \Theta(nm) Θ(nm),m是个常数,表示最大排序距离,而快排和插排等排序方法的时间复杂度为 Θ ( n l g ( n ) ) \Theta(nlg(n)) Θ(nlg(n)),效率远低于冒泡排序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值