排序算法有很多种,如冒泡排序、插入排序、堆排序、归并排序、希尔排序、快速排序等,也体会到了它们之间的不同时间复杂度。但是之前一直没有想明白,快速排序虽然是将一个数组切分成两个数组,但是为何就要快一点呢?比较笨,一时转不过来,就记住它们的时间复杂度吧,反正也验证过,没有错的。
今天突然又想到这个问题,尝试了用一万个元素的数组分别比较了冒泡排序和快速排序,发现他们的比较次数大概遵循某些规律。
冒泡排序每次只排序一个元素(通常是最末尾的),因此一万个元素第一次需要比较 9999
次,第二个元素需要比较 9998
个,以此类推,总共需要比较 1+2+3+...+9999
次。
快速排序的伪代码:
quick_sort(n){ //数组长度为n
定位标杆 //比较n-1次
quick_sort(n/2) //递归快排前n/2个元素
quick_sort(n/2) //递归快排后n/2个元素
}
快速排序第一次也需要比较 9999
次(将标杆定位时),但是因为其将数组分成两部分(把它们看作相等的两部分),一次快速排序分别递归调用两次快速排序,因此递归调用的两次快速排序在定位标杆时,都需要 4999
次的比较,故其规律是 1*9999+2*4999+4*2999+...+10000*1
(大概)。
翻开《算法导论》第三版101页,可见快速排序的递推式为:T(n)=max(T(q) + T(n-q-1)) +O(n)
,q为切分长度,如果每次切分都刚好切分成两半,则 q==n-q-1
, T(q)==T(n-q-1)
,则简化为 T(n)=2*T(n/2)+O(n)
。换一下加法项的位置,T(n)=O(n)+2*T(n/2)
,不正是上面的规律吗?第一次比较 9999
次,因此 T(10000)=9999+2*T(5000)
,而 T(5000)=4999+2*T(2500)
。因此 T(10000)=9999+2*(4999+2*T(2500))
,即 T(10000)=1*9999+2*4999+4*T(2500)
,最终得到上述分析出的规律。
注意,下面的分析都是近似。冒泡排序为 1
到 9999
的累加共 10000*5000==5000,0000
,而快速排序为每一项都是 9999
,近似为 10000
,但是项数少很多,按照2次幂的增长,2^13==8192
,2^14=16384
,因此从 2^0==1
到 2^14==16384
最多15项,因此大约 10000*15==15,0000
。冒泡排序比较次数是快速排序比较次数的约 333
倍。
写了个程序验证,下面是运行截图:
从图中可知,10000
个元素的数组,冒泡排序的比较次数与分析的 5000,0000
十分接近。快速排序比分析的 15,0000
多一些,但是大体相近,应该是数组长度小于20时采用了插入排序,以及数组长度切分不均匀导致的。
由此可见,快速排序比冒泡排序快在比较次数少很多次。
程序代码:
#include "stdafx.h"
#include <iostream>
#include <chrono>
using namespace std;
//注意,只记录了元素的比较次数,[大概] 地说明两种排序方法分别进行了多少次比较
//元素赋值等没有考虑,但我想应该不影响最终结果
int b_count = 0; //统计冒泡排序中元素比较次数
int q_count = 0; //统计快速排序中元素比较次数
//冒泡排序
void bubble_sort(int* arr, int len) {
for (int i = len - 1; i > 0; --i) {
for (int j = 0; j < i; ++j) {
++b_count; //++b_count
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
//插入排序,当快速排序中子数组长度小于20时调用
void insertion_sort(int* arr, int beg, int end) {
for (int i = beg + 1; i <= end; ++i) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
++q_count; //++q_count
arr[j + 1] = arr[j];
--j;
}
arr[j + 1] = key;
}
}
//划分函数--快速排序中使用
int partition(int* arr, int beg, int end) {
//三数中值法,可避免快排的最坏情况
int mid = (beg + end) / 2;
int max_index = beg;
if (arr[mid] > arr[max_index]) max_index = mid; //++q_count;
if (arr[end] > arr[max_index]) max_index = end; //++q_count;
int index1 = max_index == beg ? mid : beg;
int index2 = max_index == beg || max_index == mid ? end : mid;
if (arr[index1] < arr[index2]) index1 = index2; //++q_count;
q_count += 3; //统一对q_count进行递增
int temp = arr[max_index];
arr[max_index] = arr[index1];
arr[index1] = temp;
int index = beg - 1;
int key = arr[end - 1];
for (int i = beg; i <= end; ++i) {
++q_count; //++q_count
if (arr[i] < key) {
++index;
int temp = arr[index];
arr[index] = arr[i];
arr[i] = temp;
}
}
++index;
arr[end - 1] = arr[index];
arr[index] = key;
return index;
}
//快速排序的主算法体,被quick_sort调用
void q_sort(int* arr, int beg, int end) {
if (beg >= end) return;
if (end - beg > 20) { //当子数组元素个数大于20时递归进行快排
int p = partition(arr, beg, end);
q_sort(arr, beg, p - 1);
q_sort(arr, p + 1, end);
}
else { //进行插入排序
insertion_sort(arr, beg, end);
}
}
//快速排序
void quick_sort(int* arr, int len) {
q_sort(arr, 0, len - 1);
}
int main() {
const int LENGTH = 10000;
int arr1[LENGTH]; //用于冒泡排序
int arr2[LENGTH]; //用于快速排序
cout << "数组长度:" << LENGTH << endl;
//随机生成数组元素
auto start_time = chrono::system_clock::now(); //开始计时
for (int i = 0; i < LENGTH; ++i) {
int r = rand();
arr1[i] = r;
arr2[i] = r;
}
auto end_time = chrono::system_clock::now(); //结束计时
cout << "随机生成10000个int型元素的数组耗时: " << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << "ms" << endl;
//冒泡排序
start_time = chrono::system_clock::now(); //开始计时
bubble_sort(arr1, LENGTH);
end_time = chrono::system_clock::now(); //结束计时
cout << "冒泡排序耗时: " << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << "ms" << endl;
//快速排序
start_time = chrono::system_clock::now(); //开始计时
quick_sort(arr2, LENGTH);
end_time = chrono::system_clock::now(); //结束计时
cout << "快速排序耗时: " << chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count() << "ms" << endl;
//输出比较次数
cout << "冒泡排序元素比较次数: " << b_count << endl;
cout << "快速排序元素比较次数: " << q_count << endl;
system("pause");
}
OVER