复杂度常见题型详解
学数据结构一定要画图!!!!学会画图!!!!!
通过这一篇博客《新手小白初学者初步了解数据结构第一篇》我们已经知道了如今已经不需要再特别关注一个算法的空间复杂度。因此本篇博客将取少量空间复杂度题型讲解,重点主要放在时间复杂度上。在看这个博客之前强烈建议大家先去看一下上面的博客,因为本篇博客会涉及很多知识点都来自于上篇,在这里不再多做讲述。
在讲复杂度之前,先预习一下推导大O阶渐近法。因为时间复杂度和空间复杂度都可以用。
推导大O阶
1.用常数1取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。
时间复杂度
// 请计算一下Func1基本操作执行了多少次?
void Func1(int N)
{
int count = 0;
//执行 2 * N
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
//执行 10次
while (M--)
{
++count;
}
printf("%d\n", count);
}
因此,上面的Func1总执行 T(n)=2 * N + 10
根据推导大O阶第三条,如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。
所以Func1时间复杂度为O(N)。
// 请计算一下Func2基本操作执行了多少次?
void Func2(int N)
{
int count = 0;
// 执行 N*N 次
// 外for每执行一次,内for也会执行一次
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
// 执行 2*N 次
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
//执行 10次
while (M--)
{
++count;
}
printf("%d\n", count);
}
因此,上面的Func2总执行 T(n)=N2 + 2 * N + 10
又因为我们之前讲过,像此类公式只需要保留n的最高次项,也就是保留n的次方数最大的那一项。因为随着n的增大,后面项的增长远远不及n的最高次项大,所以直接省略低次项,接着最高次项系数也要去掉。
所以Func2时间复杂度为O(N2)。
// 计算Func3的时间复杂度
void Func3(int N, int M)
{
int count = 0;
// 执行 M 次
for (int k = 0; k < M; ++k)
{
++count;
}
// 执行 N 次
for (int k = 0; k < N; ++k)
{
++count;
}
printf("%d\n", count);
}
上面的Func3时间复杂度为 O(M+N)。
为什么不是O(1)呢?因为M和N都是未知数
假设给了条件: M远大于N,那么Func3时间复杂度为 O(M),若M和N差不多大,那么Func3时间复杂度为 O(M) 或 O(N)
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
// 执行 100 次
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
看过上一篇博客的,再不济看过本篇博客推导大O阶的已经知道Func4时间复杂度为 O(1)。
因为:用常数1取代运行时间中的所有加法常数。
// 计算strchr的时间复杂度?
const char* strchr(const char* str, char character)
{
while (*str != '\0')
{
if (*str == character)
return str;
++str;
}
return NULL;
}
这个编程是给了你一组字符串str,再给你一个指定的字符ch,让你在字符串中寻找指定的字符
例如我有一组字符串str,长度为11,如下图
很明显,如果我给的字符 ch = a 那时间复杂度就为 O(1)
如果我给的字符 ch = o 那时间复杂度就为 O(11)
那假设字符串长度为N呢?那最坏的情况是不是可以考虑到时间复杂度为O(N)呢?
那如果我给的字符 ch = h 呢?那时间复杂度就是O(11/2)
同样,假设字符串长度为N,是不是也可以考虑到有O(N/2)的存在呢?
这个题说明了什么问题?说明有些时间复杂度是要分情况的,如下
最好时间复杂度,指的是算法计算量可能达到的最小值。也就是任意输入规模的最小运行次数(上界)。
最坏时间复杂度,指的是算法计算量可能达到的最大值。也就是任意输入规模的最大运行次数(上界)。
平均时间复杂度,是指算法在所有可能情况下,按照输入实例以等概率出现时,算法计算量的加权平均值。
当算法同时存在这三种的情况下,一般是优先最坏时间复杂度。
// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]); //前一个数字和后一个数字相互比较
exchange = 1;
}
}
if (exchange == 0)
break;
}
这个实际上是一个冒泡排序,要想知道冒泡排序的时间复杂度就要先搞清楚什么是冒泡排序?
相邻两个数两两相比较,前一个数字大于后一个数字则交换,否则进入下一组比较,以此类推,直到比较完毕。
第一次冒泡 :N
第二次冒泡 :N-1
第三次冒泡 :N-2
…….
第一次冒泡 :1
以此来看,冒泡排序实际也是一个等差数列,而等差数列公式:(首项 + 尾项) * 项数 / 2,因此冒泡排序次数为 (N + 1) * N / 2
所以,冒泡排序的最坏时间复杂度为 O(N2)。
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int low = 0;
int high = n;
while (low < high)
{
int mid = low + ((high - low) >> 1);
if (a[mid] < x)
low = mid + 1;
else if (a[mid] > x)
high = mid;
else
return mid;
}
return -1;
}
这个实际上是一个二分查找,要想知道二分查找的时间复杂度就要先搞清楚什么是二分查找?
当静态查找表的关键字有序时,可以用二分查找来实现
二分查找也称折半查找,它是一种效率较高的查找方法。
key 值与中间元素比较,相等则成功;当 key 大则比较右半边,key 小则比较左半边。
分别用 low 和 high 来表示当前查找区间的下界和上界, mid 为区间的中间位置
由此可见,二分查找最好的情况就是时间复杂度为 O(1)。
那最坏的情况下呢?譬如上图,假设找了 X 次,就是 1 * 2 * 2 * 2* 2 …= N,那就是2X = N,X=log2N,在算法中一般写成logN,因此,二分查找最坏情况下的时间复杂度为O(logN)。
// 计算阶乘递归Factorial的时间复杂度 N!
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N - 1) * N;
}
这个其实就是一个递归阶乘,N * ( N-1) * (N-2) * (N-3) * …… * 1
若 N = 5 时,如下图:
通过计算分析发现基本操作递归了N次,时间复杂度为O(N)。
// 计算斐波那契递归Fib的时间复杂度
long long Fib(size_t N)
{
if (N < 3) //当N<3时,斐波那契数为1
return 1;
return Fib(N - 1) + Fib(N - 2);
}
在计算斐波那契递归Fib的时间复杂度之前,我们先了解一下什么是斐波那契数列:
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”。 斐波那契数列指的是这样一个数列: 0,1,1,2,3,5,8,13,21,34,55,89,144,……
这个就跟一个细胞分裂一样,是一个等比数列
由此可见,斐波那契递归Fib的时间复杂度进行估算为 O(2N)。
空间复杂度
时间复杂度是累计的,空间复杂度不是累计
// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end) //1
{
int exchange = 0; //2
for (size_t i = 1; i < end; ++i) //3
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]); //前一个数字和后一个数字相互比较
exchange = 1;
}
}
if (exchange == 0)
break;
}
空间复杂度为 O(1)
这个开辟了三个空间,用大O渐进表示法就是O(1)。
//计算Fibonacci的空间复杂度?(循环)
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n) //1
{
if (n == 0)
return NULL;
// O(N+6)
long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long)); //2
fibArray[0] = 0; //3
fibArray[1] = 1; //4
for (int i = 2; i <= n; ++i) //5
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
根据大O渐进表示法,Fibonacci的空间复杂度为O(N)
// 计算阶乘递归Fac的空间复杂度
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
每次调用都开辟一段空间,然后开辟N个空间,所以空间复杂度为O(N)
这里值得注意的是返回的时候会销毁栈帧。
到此为止了,再强调一遍**学数据结构一定要画图!!!!学会画图!!!!!**