文章目录
1 引入 认识时间复杂度
常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的指标。常用O (读作big O)来表示。具体来说,在常数操作数量的表达式中, 只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为 f ( N ) f(N) f(N),那么时间复杂度为 O ( f ( N ) ) O(f(N)) O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。
2 补充 对数器
用来判定算法是否正确,笔试的时候不要裸奔去考,可以准备对数器,像是堆,数组,二叉树等的随机样本发生器,以快速检测出错的地方。
对数器可以说是验证算法是否正确的一种方式。尤其是在笔试的时候,用贪心算法写出的程序,暂时无法用数学公式严格推导证明,只能通过大量的数据集验证算法的正确性。而大量的数据集当中要包括各种情况,各个方面都要考虑到,对我们自己来说,有时会考虑不周,而且又是在时间紧迫的情况下。所以对数器就派上了用场。
- 有一个你想测试的算法a【自己实现的排序算法】
- 实现一个绝对正确但复杂度高的算法b【使用#include中的sort函数】
- 实现一个随机样本产生器【下面实现中函数randomArrayGenerator】
- 实现比对算法a和b的方法,判断两个算法得出的结果是否相等【下面实现中函数isEqual】
- 多次(100000+)比对a和b来验证a是否正确【主函数中设置循环即可】
- 如果有样本出错,则打印出来分析
- 当对此对比测试都正确时,可以基本判断算法a正确
其中要注意的几点:
- 要测试的算法a是时间复杂度比较低的算法,而算法b唯一要求就是保证正确,而不用管复杂度的高低
- 随机产生的样本大小要小,这里说的是样本的大小而不是样本的个数。因为出错时,小样本方便分析。
- 随机产生的样本个数要多,100000+ 只要大量随机产生的样本才可能覆盖所有的情况。
- 算法b也无法保证完全的正确,在不断出错调试的过程中,也可以不断完善b,最终达到a和b都正确的完美结果。
/**
随机生成随机个元素随机数字的数组
**/
vector<int> randomArrayGenerator(int maxSize,int maxValue)
{
default_random_engine e;
uniform_real_distribution<double> u(0, 1); //随机数分布对象
double random = u(e);
int length = int((maxSize+1)*random*10);
vector<int> res;
for(int i=0;i<length;i++)
{
double randomval = u(e);
res.push_back(int(randomval*(maxValue+1)));
}
return res;
}
/**
比较两个数组是否相等
**/
bool isEqual(vector<int> arr1,vector<int> arr2)
{
if(arr1.size() != arr2.size())
return false;
else
{
for(int i=0;i<arr1.size();i++)
{
if(arr1[i] != arr2[i])
return false;
}
}
return true;
}
/**
复杂度很高,但是很简单的对比方法,用来对比所用方法的准确性
可以使用#include<algorithm>中的sort函数
**/
3 排序算法分析
关于排序的总结,这个网址的博主总结的很棒,而且设计了动画演示,对于理解有很大的帮助,以下仅作为我个人对排序算法的理解与总结,以及C++实现。
3.1 基于比较的排序
3.1.1 冒泡排序
该算法称为冒泡排序是很形象的,质量较大的物体浮起来是需要更大的浮力的,所以每一次循环就会将最重【最大】的数沉下去,最终最轻【最小】的数浮在最上面。
该算法在一次循环中,比较相邻的元素,如果前面的数大,就交换两个元素【最后的排序结果是从小到大】,因此每次循环只排好一个位置上的数,但是每次可以少排最后一个位置的数,因为已经排好了。
时间复杂度 O ( N 2 ) O(N^2) O(N2),额外空间复杂度 O ( 1 ) O(1) O(1)
/**首先实现一个交换函数**/
void Swap(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
/**【实现的时候出现的问题】
一开始传入的是数组原型,在运行时会复制一个vector,然后交换操作也在复制的vector中进行,
传出时并没有对原数组进行修改,所以测试结果时数组并没有排序。
要想改变原数组,需要传入数组的引用,对数组进行修改
**/
void bubbleSort(vector<int> &arr)
{
if(arr.size() < 2)
return;
int length = arr.size();
for (int e = length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i + 1]) {
Swap(&arr[i],&arr[i+1]);
}
}
}
}
3.1.2 选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法。
【实际上,冒泡排序也是每次比较找到最大【小】的数放在最后,但是选择排序的效果应该是比冒泡排序好的,两者的差别在于每次比较数据大小之后是否进行交换,冒泡排序每次都进行交换,而选择排序则是记录最大值的索引,最后进行一次交换。】
时间复杂度: O ( N 2 ) O(N^2) O(N2),额外空间复杂度 O ( 1 ) O(1) O(1)
void selectionSort(vector<int> &arr)
{
if(arr.size() < 2)
return;
int length = arr.size();
int minIndex;
for(int i=0;i<length;i++)
{
minIndex = i;
for(int j=i+1;j<length;j++)
{
minIndex = (arr[j]<arr[minIndex])?j:minIndex;
}
Swap(&arr[i],&arr[minIndex]);
}
}
3.1.3 插入排序
一个新的值插入到哪个位置比较合适。构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
时间复杂度最好情况: O ( N ) O(N) O(N)
最差情况: O ( N 2 ) O(N^2) O(N2)与数据状况有关
/**
每一次循环和后面一位比较大小,满足要求则交换,若交换再和前面的位进行比较。
**/
void insertSort(vector<int> &arr)
{
if(arr.size() < 2)
return;
int length = arr.size();
for(int i=0;i<length;i++)
{
for(int j=i+1;j > 0;j--)
{
if(arr[j-1]>arr[j])
Swap(&arr[j-1],&arr[j]);
}
}
}
3.1.4 归并排序
归并排序之前首先看一下递归调用,课堂上学习到的都是递归就是自己调用自己,很玄乎。实际上递归函数中,是系统在帮忙压栈,当前函数跑到了第几行,以及当前所有的变量和信息都压入系统的栈中,然后继续跑下次调用函数的过程,如果不符合递归结束条件,继续压入栈中。【栈,先进后出,所以,递归函数是倒着跑回去。】
任何递归行为都可以改成非递归。
分析复杂度,估计递归行为的通式:master公式,子问题的样本来量需要是一样的才可以使用该公式
T ( n ) = a T ( n b ) + O ( n d ) T(n)=a T\left(\frac{n}{b}\right)+O\left(n^{d}\right) T(n)=aT(bn)+O(nd