时间复杂度和空间复杂度
- 算法效率
- 时间复杂度
- 空间复杂度
- 表示方法(大O的线性表示)
- 举例说明
- 时间复杂度举例说明
- 空间复杂度举例说明
- 冒泡排序的时间和空间复杂度
- 递归情况下的时间和空间复杂度
- 两个例子
算法效率
算法(Algorithn)是指用来操作数据,解决程序问题的一组方法。对于同一个问题使用不同的算法,结果或许相同,但所消耗的资源和时间会有所不同。这里通过时间和空间二个维度去考虑算法效率。
时间复杂度
一个算法所花费的时间与语句的执行次数成正比例,一个算法中语句的执行次数称为时间频度,用T(n)表示,n称为问题的规模。假设某个辅助函数 f ( n ) f(n) f(n),当n趋近于无穷大时, T ( n ) / f ( n ) T(n)/f(n) T(n)/f(n)的极限值为不为零的常数,记作 T ( n ) = O ( f ( n )) T(n)=O(f(n)) T(n)=O(f(n)),称 O ( f ( n )) O(f(n)) O(f(n))为算法的渐进时间复杂度,简称为时间复杂度。
空间复杂度
空间复杂度是一个算法在运行过程中临时占用存储空间大小的一个量度。空间复杂度算的是变量的个数,包括数组,还有动态内存函数开辟出来的空间。
随着计算机行业的发展,计算机的存储容量已经达到了很高的程度,所以,我们更多关注的是时间复杂度。
表示方法(大O的线性表示)
规则:
1 用常数1取代所有加法常量,即所有常数项的执行的复杂度为 O ( 1 ) O(1) O(1)
2 在运行次数的函数中,只保留最高阶
3 如果最高阶项存在且不是1,则去掉与这个项目相乘的常数,得到的结果就是大O阶
值得注意的是:
一些算法存在最好,最坏和平均的情况。
例如在一个长度为N(这里的N是一个未知量)的数组中去搜索一个数据x,最好的情况:1次;最坏的情况:N次,平均:N/2次。
在实际中我们只关注算法的最坏运行情况作为时间复杂度或者空间复杂度。
举例说明
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count1;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count2;
}
int M = 10;
while (M--)
{
++count3;
}
printf("%d\n", count);
}
上述代码中,++count1
语句被执行了
N
2
N^2
N2次,++count2
执行了
2
∗
N
2*N
2∗N次,++count3
被执行了10次,还包括int count = 0;
, int M = 10;
等有限常数个语句,可以看出最高阶项为
‘
N
2
‘
`N^2`
‘N2‘,所以时间复杂度为
O
(
N
2
)
O(N^2)
O(N2)
时间复杂度举例说明
1
// 计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N; ++k)
{
++count1;
}
int M = 10;
while (M--)
{
++count2;
}
printf("%d\n", count);
}
++count1;语句被执行2*N次,++count2;语句被执行次数为10次,还有其它有限的常数语句被执行,取其最高阶2N,根据大O规则,时间复杂度为O(N)
2
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
++count的执行次数为100次,还有其余常量个语句,根据规则,其时间复杂度为O(1)
3
void Func(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++k)
{
++count;
}
for (int k = 0; k < N; ++k)
{
++count;
}
printf("%d\n", count);
}
上述的M,N都是未知量,所以时间复杂度为O(M+N)。如果题目有条件:如果M>>N,那么可以认为该复杂度为O(M),如果M<<N,那么可以认为该复杂度为O(N),M和N差不多大,既可以认为是O(M),也可以认为是O(N)
4
// 计算strchr的时间复杂度?
const char* strchr(const char* str, int character);
str函数用于查找字符串中的一个字符,并返回它在字符串的位置。这里我们默认数组或者字符串的长度为N,查找一个字符,最好的情况是一个就能找到,最坏的情况是查找N次。根据规则,时间复杂度取其最坏的情况,为O(N)
5
二分查找法的时间复杂度
int BinarySearch(int* a,int n,int x)
{
assert(a);
int begin = 0;
int end = n - 1;
while (begin<=end)
{
int mid=begin+((end-begin)>>1)//相当于除以2
if (a[mid]<x)
{
begin = mid + 1;
}
else if (a[mid]>x)
{
end = mid - 1;
}
else
{
return a[mid];
}
}
return -1;
}
最好的情况,查找一次就可以找到,最坏的情况可以如下分析:
我们需要从N份中拿出一份,如下图所示
假设查找次数为x,则N/2/2…=1,
X
=
l
o
g
2
N
X=log_2N
X=log2N,所以其时间复杂度为O(
l
o
g
2
N
log_2N
log2N),有时也会写成logN或者lgN。
空间复杂度举例说明
1
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
上述只创建了i,j,m,3个变量,为有限个空间,所以空间复杂度为O(1)
2
int arr[n]= {0}
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
上述一个数组arr有n个变量,占用空间大小为n,还有一个变量i占用为1,根据规则,其空间复杂度为O(n)
冒泡排序的时间和空间复杂度
void BubbleSort(int* a,int n)
{
for (int j=0;j<n;j++)
{
for (int i = 1; i < n - j; i++)
{
int exchange = 0;
if (a[i-1]>a[i])
{
Swap(a + i - 1, a + i);
exchange = 1;
}
}
if (0==exchange)
{
break;
}
}
}
可以观察到int exchange = 0;,Swap(a + i - 1, a + i);,exchange = 1;这3个语句的执行量级为n^2,为最高阶项,所以时间复杂度为O( n 2 n^2 n2).其定义的变量有exchange,i,j,包括Swap函数中的临时变量temp,为有限个变量,所以其空间复杂度为O(1)
递归情况下的时间和空间复杂度
1
// 计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
如果N<2,成立,则返回N,否则返回
F
a
c
t
o
r
i
a
l
(
N
−
1
)
∗
N
Factorial(N-1)*N
Factorial(N−1)∗N;
最好的情况是N<2成立,则返回N;最坏的情况函数一直调用
Factorial(N-1)
Factorial(N-2)
Factorial(N-3)
......
Factorial(3)
Factorial(2)
Factorial(1)
当调用到Factorial(1),函数再回归。这种情况下时间复杂度考虑的是函数的调用次数,为N次,所以时间复杂度为O(N)。每一次函数调用都会开辟栈帧,所以这里开辟的空间也是N,空间复杂度为O(N)
2
// 计算斐波那契递归Fibonacci的时间复杂度?
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
如果N<2,成立,则返回N,否则返回Fibonacci(N-1)+Fibonacci(N-2);
最好的情况是N<2成立,则返回N;最坏的情况函数一直调用
可以算出函数的总共调用次数为
2
0
+
2
1
+
.
.
.
+
2
n
−
1
2^0+2^1+...+2^{n-1}
20+21+...+2n−1,其量级为
2
n
2^n
2n,所以时间复杂度为O(2^N).空间复杂度,共需要开辟N次函数栈帧,所以空间复杂度为O(N)
一般情况下递归调用的空间复杂度为其调用的深度
两个例子
对于上述,如果依次拿着0到n的数字对比,那么需要两层for循环,它的时间复杂度就为O(n^2),就无法在O(n)的时间内完成。
为此,我们可以考虑以下两种方法:
1 异或。根据两个相同的数字异或为0,并且异或具有交换率,因此可以将0-n内的数字和缺少一个的0-n的数字异或,其结果就是缺少的值。
异或,将0-n内的数字与缺少一个数字的0-n异或
int val=0;
for(int i=0;i<numsSize;i++)
{
val^=nums[i];
}
for(int i=0;i<=numsSize;i++)
{
val^=i;
}
return val;
它的时间复杂度为O(n)
2 直接套用公式,1-n的数字加和是一定的。
// numSize默认为缺失后的数组的大小,正常情况下数组大小为numSize+1
int tollal=((numsSize+1)*(0+numsSize))/2;
for(int i=0;i<numsSize;i++)
{
tollal=tollal-nums[i];
}
return tollal;
它的时间复杂度也为O(n)
思路1:整体反转,需要轮转的前k个元素反转,其余的全部元素反转。代码如下:
void swap(int* m,int* n)
{
int temp=*m;
*m=*n;
*n=temp;
}
void reverse(int* p,int length)
{
int* left=p;
int *right=p+length-1;
while(left<right)
{
//交换值
swap(left,right);
left++;
right--;
}
}
k=k%numsSize;
reverse(nums,numsSize);
reverse(nums,k);
reverse(nums+k,numsSize-k);
时间复杂度是O(n)。整体逆置就是需要进行n/2次操作,前n-k次逆置和后k次逆置,也是需要进行n/2次操作。都是n的数量级
空间复杂度是O(1)。我们看到变量的创建都是有限个。
思路2:利用memcpy函数整体改变数组的顺序。
k=k%numsSize;
int* temp=(int*)malloc(sizeof(int)*numsSize);
memcpy(temp,nums+numsSize-k,sizeof(int)*(k));
memcpy(temp+k,nums,sizeof(int)*(numsSize-k));
memcpy(nums,temp,sizeof(int)*(numsSize));
free(temp);
temp=NULL;
时间复杂度为O(n),虽然表面看起来,是有限次的语句,但是,memcpy内部也是一个一个进行copy的,需要进行n的数量级的次数
空间复杂度为O(n),开辟了n个空间。