结论:冒泡排序的循环迭代次数等于所有元素的排序距离中的最大值,其中排序距离的定义为:排序后该元素的下标减去当前该元素的下标,值可正可负。即,冒泡排序的时间复杂度为 Θ ( 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)),效率远低于冒泡排序。