O(n^2)时间复杂度下各排序算法效率比较
最近学习了排序算法,有了一些浅显的理解,文章参考了刘宇波老师的讲解,Mark下来分享给大家,也算记录下本阶段的学习成果,望各位大牛多多指教。
首先是为什么要用算法,顾名思义算法是优化复杂计算的方法,可以对比下nlogn和n^2级别算法间的差异。
nlogn和n^2复杂度算法效率对比
显而易见,在基数n较小的情况下,差异无从体现。但当数量级达到一定数量后,效果的差别就极其巨大了,这就好比LOL开局你刚买个树叉对面已然6神装一样的差距,这还打毛?GG好吧。同理,在目前的大数据时代非常有必要对算法进行一些有效的利用和优化,不然,拿什么和对手闹?
回归正题,既然nlogn的效率更高,那为何我们还需要n^级别的算法呢?在于以下几点:
1. 易于实现,编码简单。
2. 一些特殊情况下,简单排序算法更有效。
3. 作为子过程,改进更复杂的排序算法。
扯了半天,现在进入正题: O(n^2) 级别排序算法
主要包括以下几种:
1.选择排序 2.插入排序 3.冒泡排序 4.希尔排序
这里只实现前三种方法进行对比,希尔排序请各位自行尝试。
首先我们需要搭建基本的测试环境(封装测试函数)用以后面的效率对比。
说明:这里我们假定一个名词:随机复杂度
高->相对顺序程度低(如165423) 低->相对顺序程度高(如123465)
namespace SortTestHelper {
//生成有n个元素的随机数组,每个元素的随机范围为[rangeL,rangeR]
int* generateRandomArray(int n, int rangeL, int rangeR)
{
assert(rangeL <= rangeR);
int *arr = new int[n];
srand(time(NULL)); //ctime
for (int i = 0; i < n; i++)
arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
return arr;
}
//生成有n个元素的随机数组,且近乎有序,n:元素个数,swapTimes:交换次数
int * generateRandomNearArr(int n, int swapTimes)
{
int * arr = new int[n];
for (int i = 0; i < n; ++i)
arr[i] = i;
srand(time(NULL));
for (int i = 0; i < swapTimes; ++i)
{
int randomX = rand() % n;
int randomY = rand() % n;
swap(arr[randomX], arr[randomY]);
}
return arr;
}
//复制整形数组(用于测试多个算法效率)
int * copyArr(int a[], int n)
{
int * arr = new int[n];
copy(a, a + n, arr); //a是头指针,a+n是尾指针
return arr;
}
//检测排序是否成功
template<typename T>
bool isSort(T arr[], int n)
{
for (int i = 0; i < n-1; ++i)
{
if (arr[i] > arr[i + 1])
return false;
}
return true;
}
//测试排序方法效率
template <typename T>
void testSort(string name, void(*sort)(T[], int), T arr[], int n)
{
clock_t startTime = clock();
sort(arr, n);
clock_t endTime = clock();
assert(isSort(arr, n));
cout << name << " efficiency:" << double(endTime - startTime) / CLOCKS_PER_SEC <<" s"<< endl;
return;
}
}
1 ) 选择排序
原理:简单举例,一组数 12345876 需要升序排列,O(n^2)时间复杂度可以理解为需要两次for循环(即每个数需要操作n次,n个数即n^2),第一个for 循环遍历所有数,第二个for循环遍历对比得到当前未排序数中的最小值,将最小值交换到当前位置(本例为最左边未排序位置),这样每次循环都将未排序数中的最小值放到左边,依次遍历即可完成选择排序,代码如下:
//选择排序
template <typename T>
void selectionSort(T arr[], int n)
{
for (int i = 0; i < n; ++i)
{
//寻找[i,n)区间里的最小值
int minIndex = i; //最小值下标
for (int j = i + 1; j < n; ++j)
if (arr[j]<arr[minIndex])
minIndex = j; //始终指向最小值
swap(arr[i], arr[minIndex]);
}
}
2)插入排序
原理:依然两层for循环,第一层循环遍历所有数,第二层循环对比当前值(arr[j])和前值(arr[j-1])的大小,如arr[j]
//插入排序(未优化)
template <typename T>
void insertSort(T arr[], int n)
{
for (int i = 1; i < n; ++i)
{
for (int j = i; j > 0&& arr[j] < arr[j - 1]; --j) //二层循环可以提前结束,
swap(arr[j], arr[j - 1]);
}
return;
}
由于二层循环可以提前终止(arr[j]>arr[j-1]),所以理论上插入排序是要优于选择排序的,但实际测试结果并不理想,插入排序反而慢于选择排序。
主因:我们处理插入排序时使用了swap函数,实际上交换操作所消耗的时间要远远大于比较,swap相当于进行了3次赋值,这使得插入排序并没有达到其应有的效率,需要进一步优化。
优化:将3次赋值变成一次赋值,请大家参照代码自行脑补~
//插入排序(优化)
template <typename T>
void insertSort(T arr[], int n)
{
for (int i = 1; i < n; ++i)
{
//寻找元素arr[i]合适的插入位置
T e = arr[i];
//j保存元素e应该插入的位置
int j;
for (j = i; j > 0 && arr[j - 1]>e; --j) //二层循环可以提前结束,
{
arr[j] = arr[j - 1];
}
arr[j] = e;
}
}
再次对比优化后的效率提高显著。
之后我们再对随机复杂度低的数组排序对比:
发现插入排序速度优势非常明显,可以想见,在一些场景下(如系统日志,个别地方出现错乱但整体有序),当排序数组近乎有序时,插入排序将升级成O(n)级别的排序算法,具体可以在复杂算法中做为一个子方法进行优化,这也是O(n^2)级别算法存在的价值。
3 )冒泡排序
原理:冒泡排序实现起来比较简单,也是入门最常见的一种算法,在此不过多赘述,就是把最大(小)值每轮循环冒泡到最右(左)边,依次循环完成排序,由于其交换次数较多,所以在随机复杂度高的情况下效率比选择排序还低,在大数据量下的使用价值不是很高(也可优化)。发现插入排序速度优势非常明显,可以想见,在一些场景下(如系统日志,个别地方出现错乱但整体有序),当排序数组近乎有序时,插入排序将升级成O(n)级别的排序算法,具体可以在复杂算法中做为一个子方法进行优化,这也是O(n^2)级别算法存在的价值。
template <typename T>
void BubbleSort(T arr, int n)
{
//第一次循环表示循环次数
for (int i = 1; i < n; i++)
{
//第二次循环将最大值冒泡到右边
for (int j = 0; j < n - i; j++)
{
if (arr[j] > arr[j + 1])
swap(arr[j], arr[j + 1]);
}
}
}
三种O(n^2)算法效率对比:
随机复杂度高:
随机复杂度低:
可见,这三种排序方法中插入排序综合性能最优,且在随机复杂度低的情况下效率可以接近O(n),其它两种排序方法在效率上不占优,但能提供更多思路上的延伸以完成更加复杂的算法。
这样我们就简单的完成了O(n^2)级别排序算法的对比,下章我们将继续进行 nlog(n) 级别的排序算法比较。