⭐算法效率
如何衡量一个算法的好坏?
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
⭐时间复杂度
一个程序的运行,依赖于其本身的算法设计以及不同的机器性能。单纯地只是从计算其运算时间的角度来评估一个算法是不合理的。
因此引进了时间复杂度。
🌟时间复杂度
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度,更关注一个算法的时间复杂度。
🐟时间复杂度的概念
一个算法所花费的时间与其中语句的执行次数成正比例。算法中的基本操作的执行次数,为算法的时间复杂度。
🐳例1
// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
Func1执行的基本操作次数
F(N)=N^2+2N+10
但实际上当N非常大时,最高数量级的N^2对F(N)会起到决定性影响。
并且在实际中,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
🐟大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1
取代运行时间中的所有加法常数
。
2、在修改后的运行次数函数中,只保留最高阶项
。
3、如果最高阶项
存在且不是1
,则去除
与这个项目相乘的常数
。得到的结果就是大O阶。
🐳 例2
// 计算Func2的时间复杂度?
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
具体的执行次数F(N)=2N+10
我们只需要抓大头,for循环这条语句是最主要的。
因此Func2的时间复杂度为O(N)
🐳例3
// 计算Func3的时间复杂度?
void Func3(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);
}
Func3函数中,M和N是两个未知数。
当M>>N时,算法的时间复杂度为:O(M)
当N>>M时,算法的时间复杂度为: O(N)
当M=N时,算法的时间复杂度为O(M)或者O(N)
但由于我们不清楚M和N的具体关系,因此这个算法的时间复杂度为:O(M+N)更准确。
🐳例4
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
Func4是可以清楚地知道for语句语句会执行100次,因此这个算法的时间复杂度为O(1)
🐳例5
// 计算Func5的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 1000000; ++ k)
{
++count;
}
printf("%d\n", count);
}
如果这里换成1000,000实际上算法的时间复杂度还是O(1)。
这主要依赖于CPU的强大。CPU的运算速度是十分快的。
这是由于此,对于CPU而言,运行100和1000,1000000 语句,其时间都是一样的。
🐳例6
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
这个库函数表示,在字符串s中查找字符c,返回字符c第一次在字符s中出现的位置,如果未找到字符c,则返回NULL。
这个需要分情况讨论。
比如说,调用这个函数,并传递参数,字符串"hello,world"。
最好的情况是,查找的字符,我们传递的是字符"h",那么这样就只进行了1次查找。
最坏的情况是,查找的字符,我们传递的参数是"d"那么这样就遍历了一次字符数组11次才查找到。或者我们传递的参数是"a",遍历了数组,没有查找到。
平均情况是,11/2=5次
在这种情况中我们关注的是算法最坏的情况。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界) 例如:在一个长度为N数组中搜索一个数据x
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
🐳例7
// 计算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次。
第三次交换为n-3次。
…
第n-1次交换为1次
所以总次数为
为(n-1)+(n-2)+(n-3)+…+1=[(n-1)+1]*(n-1)/2=n(n-1)/2次。
故冒泡算法的时间复杂度为
O(N^2)