时间复杂度
在算法的分析中,语句的执行次数T(n)是一个关于n(问题规模)的一个函数。分析n的变化引起T(n)的改变,进而得到T(n)的数量级,也就是时间频率。如果存在某一个辅助函数f(n),当n趋于无穷大时,T(n)/f(n)的值为一个不为0的常数,有T(n)=O(f(n)),这就是算法的渐进时间复杂度,也就是我们常说的时间复杂度。
大O表示法:用O(f(n))来体现时间复杂度的方法被称作大O表示法;
大O推导法:
O(1)叫做常数阶;O(n)叫做线性阶;O(n^2)叫做平方阶。
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。
举一个简单的例子
int i;
for(i=0;i<n;i++)//该语句的复杂度为O(n)
{
cout<<i; //该语句的复杂度为O(1);
}
这段代码的时间复杂度为O(n),其中“cout<<i”的执行次数为1,是常量阶,所以它的复杂度为O(1);“for(i=0;i<n;i++)”的执行次数是n,是线性阶,所以它的复杂度为O(n)。整段代码的复杂度为O(1*n),也就是O(n).
常见的时间复杂度:
常数阶O(1),
如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,
其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
对数阶O(log2 n),
线性阶O(n),
线性对数阶O(n log2 n),
平方阶O(n^2),
立方阶O(n^3)
k次方阶O(n^K),
指数阶O(2^n)。
其他时间复杂度都会随着n的变化慢慢变大,算法开销也越来越大 。
常见的时间复杂度所损耗时间排序:
O(1)<O(log n)<O(n)<O(nlogn)<O(n ^2)<O(n ^3)<O(2 ^n)<O(n!)<O(n ^n)
空间复杂度
空间复杂度指的是一个程序在执行时,所占有的临时内存空间大小:S(n)=O(f(n));n是问题的规模,f(n)为语句关于n所占据的内存的函数。
举个简单的例子:
交换两个变量的值,它需要定义一个临时的变量,这就造成了空间复杂度,因为是常量阶,所以它的空间复杂度为O(1).
int i = 10, j = 100;//两个需要交换的值
//交换变量的值
int temp=i;//定义临时变量
i=j;
j=temp;
提到空间复杂度,就需要提一下递归了,在进行递归的算法的时候,每一次的递归都会使用临时的变量保存递归的信息,所以递归的方法很消耗内存,也就是空间复杂度相对较高。如果递归次数过多,会造成内存超载,无法计算得到需要的数据。所以,当循环的次数过多的时候,尽量不要使用递归方法。
插入排序
原理分析
将一个记录插入到已排好序的序列中,从而得到一个新的有序序列
将序列的第一个数据看成是一个有序的子序列,然后从第二个记录逐个向该有序的子序列进行有序的插入,直至整个序列有序
代码实现
#include<iostream>
using namespace std;
void printArray(int *arr, int len)
{
for (int i = 0; i < len; ++i)
{
cout << arr[i] << " ";
}
cout << endl;
}
void InsertSort(int *arr, int len)
{
for (int i = 1; i < len; ++i)
{
if (arr[i] > arr[i - 1])
{
int temp = arr[i];
int j = i - 1;
for (; j >= 0 && temp > arr[j]; j--)
{
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
}
}
int main()
{
int arr[] = { 4,5,8,9,1,2 };
int len = sizeof(arr) / sizeof(int);//通过字节计算数组大小
printArray(arr, len);
InsertSort(arr, len);
printArray(arr, len);
return 0;
}
复杂度计算
1、当初始序列为正序时,只需要外循环n-1次,每次进行一次比较,无需移动元素。此时比较次数(C min)和移动次数(M min)达到最小值。
C min=n-1;
M min=0;
此时时间复杂度为O(n).
2、初始序列为反序时,需要外循环n-1次,每次排序中待插入的元素都要和[0,i-1]中的i个元素进行比较且要将这i个元素后移i次,此时比较次数和移动次数达到最大值。
C min=1+2+3+4+……+n-1=(n-1)n/2;
M min=1+2+3+……+n-1=(n-1)n/2;
此时时间复杂度为O(n^2).
3、在直接插入排序中只使用了i,j,tmp这三个辅助元素,与问题规模无关,空间复杂度为O(1)
冒泡排序
概念及思路
重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,故称为"冒泡排序"。
代码实现
#include<iostream>
using namespace std;
void BubbleSort(int *a, int size)
{
for (int i = 0; i < size; i++)//外循环,循环每个元素
{
for (int j = 1; j < size - i; j++)//内循环进行元素的两两比较
{
if (a[j] < a[j - 1])//判断相邻元素并进行交换
{
int temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
}
}
}
}
int main()
{
int a[10] = { 2, 7, 34, 54, 12, 5, 19, 33, 88, 23 };
cout << "原来的数组为:" << endl;
for (int i = 0; i < 10; i++)
{
cout << a[i] << " ";
}
cout << endl;
BubbleSort(a, 10);
cout << "冒泡排序后的数组为:" << endl;
for (int i = 0; i < 10; i++)
{
cout << a[i] << " ";
}
return 0;
}
时间复杂度
外循环和内循环以及判断和交换元素的时间开销。
最优的情况也就是开始就已经排序好序了,那么就可以不用交换元素了,由于外层循环为n,内层所需要循环比较的次数为(n-1)、(n-2)…1由等差数列求和得时间花销为:[ n(n-1) ] / 2;所以最优的情况时间复杂度为:O( n^2 )。
最差的情况也就是开始的时候元素是逆序的,那么每一次排序都要交换两个元素,则时间花销为:[ 3n(n-1) ] / 2;(其中比上面最优的情况所花的时间就是在于交换元素的三个步骤);所以最差的情况下时间复杂度为:O( n^2 );
空间复杂度
冒泡排序的辅助变量空间仅仅是一个临时变量,并且不会随着排序规模的扩大而进行改变,所以空间复杂度为O(1)。
选择排序
void Efferve()
{
int m[5] = { 12, 8, 6, 9, 10 };
int max = m[0];
for (int i = 0; i < 4; i++)
{
for (int j = i; j < 4; j++)
{
if (m[j] < m[j + 1])
{
max = m[j + 1];
m[j + 1] = m[j];
m[j] = max;
}
}
}
}
上面的代码可以看出选择排序套用了两个循环如下:
for (int i = 0; i < 4; i++)
{
for (int j = i; j < 4; j++)
{
}
}
当i=0下面循环4次,每次i+1下面循环都执行次4-i次,因此上述循环次数为T=(4-1))+ (4 -2)+(4 - 3)+ 1;
T=[4*(4-1)]/2次
哪当N个数进行排序时,将进行T=[N*(N-1)]/2次,根据计算方法保留最高次N ^2,因此选择排序的时间复杂度为O(N ^2);
因为排序中始终只用到了数组大小的空间,为常数,因此空间复杂度为O(1)。
快速排序
基本思路
在一个数组中,找一个数为基准数,将这个数中所有比基准数大的数放在该数的右边,比基准数小的数放在该数的左边。
例如"6 1 2 7 9 3 4 5 10 8"这个数组
以6作为基准数,将比6小的数放在6的组左边,比6大的数放在6的右边
得到:3 1 2 5 4 6 9 7 10 8
可以看出6的左边的数都比6小,而右边的数都比6大,此时6已经归位
具体步骤为:
- 先找一个基准数(一般为第一个),然后从右边开始向左找,找到第一个小于基准值的数,然后从左开始找,找到第一个小于基准值的数,然后进行交换,一直到左边和右边相遇时,则将基准数与找到的位置进行交换
- 以基准数为分界线,划分为左右2个数组,再以步骤1进行递归
- 当递归的数组中无法再继续递归时,循序则结束,此时的数组则已经完成了排序
代码实现
void QuickSort(int *n, int left, int right)
{
if (left > right)
return ;
int temp = n[left]; //temp中存的数为基准数
int i = left, j = right;
int t;
while (i != j)
{
//一定要先从右向左找
while (i < j && n[j] >= temp)
j--;
while (i < j && n[i] <= temp)
i++;
if (i < j)
{
t = n[i];
n[i] = n[j];
n[j] = t;
}
}
n[left] = n[i];
n[i] = temp;
QuickSort(n, left, i - 1); //处理左边的数
QuickSort(n, i + 1, right); //处理右边的数
}
复杂度计算
空间复杂度:logn
主要是由于递归造成的栈空间的使用,
最好的情况下其树的深度为:log2(n)
空间复杂度为 O(logn)
而最坏的情况下:需要n-1次调用,每2个数都需要交换,此时退化为冒泡排序
空间复杂度为 O(n)
平均时间复杂度为:O(logn)
时间复杂度:O(nlogn)
由于快速排序用到了递归调用,因此计算其时间复杂度也需要用到递归算法计算
递归算法的时间复杂度公式:T[n] = aT[n/b] + f(n)
**
最优情况下时间复杂度
快速排序最优的情况就是每一次取到的元素都刚好平分整个数组
此时的时间复杂度公式则为:T[n] = 2T[n/2] + f(n);T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间
第一次递归:
T[n] = 2T[n/2] + n;
第二次递归:
令 n = n/2
= 2^2 T[ n/ (2^2) ] + 2n
第三次递归:
令:n = n/(2^2)
= 2^3 T[ n/ (2^3) ] + 3n
…
第m次递归:
令:n = n/( 2^(m-1) )
= 2^m T[1] + mn
当最后平分的不能再平分时,也就是说把公式一直往下跌倒,到最后得到T[1]时,说明这个公式已经迭代完了(T[1]是常量了)。
得到:T[n/ (2^m) ] = T[1] ===>> n = 2^m ====>> m = logn;
T[n] = 2^m T[1] + mn ;其中m = logn;
T[n] = 2^(logn)T[1]+lognn = n +nlogn
又因为当n >= 2时:nlogn >= n (也就是logn > 1),所以取后面的 nlogn;
综上所述:快速排序最优的情况下时间复杂度为:O( nlogn )
最差情况下时间复杂度
最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序)
此时的时间复杂度为:T[n] = n * (n-1) = n^2 + n;
综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )
平均时间复杂度
快速排序的平均时间复杂度也是:O(nlogn)
参考:
https://blog.csdn.net/not_in_mountain/article/details/77976743
本博文由小队成员(Wu_0526,qq_49032326,ly1196324806,SamGeren,qq_39124199)共同编写