目录
首先大家思考下我们写代码是为了干什么?就是为了让计算机去帮助我们做那么多繁杂的计算任务和其他任务,那么我们是不是特别需要关心程序运行花费的时间呢?是的,试想一下如果一项代码需要运行一年才能得出结果,那么只想说"人麻了,毁灭吧世界!"除此之外,运行中的数据需要存储起来对吧,那就要存储设备,我们夸大一点相同的程序,人家只需要100MB就能存储,你的需要1GB存储,那你要多花几十倍的钱,讲解到这里想必大家已经知道我们最需要关心的两个东西了
第一:时间 第二:空间
那么就延伸出来了时间复杂度和空间复杂度两个概念了,开始今天的学习喽,冲冲冲!!!
1->表示算法效率的两个重点知识:时间复杂度和空间复杂度
时间复杂度:主要衡量一个算法的运行速度.先输出结论,平常使用中去关注基本操作的运行次数,其实这个运行次数你就可以直接看做时间复杂度.
空间度复杂度:主要衡量一个算法需要的额外空间. 输出结论, 在使用中去关注定义的变量的个数, 这个定义的变量的次数你就看做空间复杂度. 在计算机发展的早期, 各种元器件价格昂贵, 计算机的存储容量很小, 所以对空间复杂度很是在乎, 但是随着半导体存储芯片产业, 材料科学, 数据存储技术研发研发等产业迅速发展, 计算机的存储容量已经达到了很高的程度, 所以我们如今已经不需要再特别关注一个算法的空间复杂度, 这就是为什么我们大多时候听到的是时间复杂度, 而很少听到空间复杂度的原因.
2->如何表示两种复杂度?使用大O渐进表示法
时间复杂度和空间复杂度一般都使用大O渐进表示法,大O渐进表示法规则:
1、所有常数都用常数1表示
2、只保留最高阶项
3、如果最高阶项存在且不是1,则去除与这个项的系数,得到的结果就是大O阶
3->用例子带大家学会时间复杂度
再次说明:时间复杂度就是基本操作的次数,你就关心基本操作的次数(最后再给大家展示书上的定义)
时间复杂度例一:
#include <iostream> //C++标准头文件
using namespace std;
void func1(int n)
{
int count = 0;
for (int i = 0; i < 100; ++i)
{
count++;
}
}
int main()
{
func1(1);
return 0;
}
跟随珂珂的脚步,首先看下程序怎么运行,进入main函数以后调用func1()函数,传递实参1,那么在func1函数内部会去进行100次count++循环操作.并且不论你传递的实参是多少,都只会执行100次的count++操作,结论:基本操作是count++,进行了100次,换句话说就是func1函数内语句执行的次数不会随着传入变量n的改变而改变,也就是执行的次数为常数次不会变.
得出func1函数的时间复杂度为T(n) = 100;
根据大O的渐进表示法,所有的常数都用常数1来表示,所以,用大O的渐进表示法表示func1函数的时间复杂度为:O(1)
扩展:在LeetCode和牛客网上刷题时,题目要求时间复杂度为O(1),并不是要求函数内部不能有循环,而是要求循环的次数是常数次.
时间复杂度例二:
//计算func2函数的时间复杂度
void func2(int n)
{
//第一个大的for循环
int count = 0;
for (int i = 0; i < 2 * n; ++i)//执行(2*n)次
{
for (int j = 0; j < 2 * n; ++j)//执行(2*n)次
{
count++;
}
}
//第二个大的for循环
for (int i = 0; i < 2 * n; i++)//执行(2*n)次
{
count++;
}
}
有了例一的铺垫,想必大家一眼就看出来了基本操作还是count++,那么接下来就是求出基本操作执行的次数也就是for循环所循环的次数.一共两个大的for循环,第一个大的for循环里边还包含着一个for循环,那么第一个大的for循环里边count++执行的次数就是(2*n)*(2*n) = 4n^2--->,第二个for循环里边count++执行的次数是(2*n).
所以func2函数的时间复杂度为:T(n) = ,根据大O的渐进表示法,只保留最高阶项(即
),去除最高项的系数后(即
),就是最终结果。所以,用大O的渐进表示法表示func2函数的时间复杂度为:O(
) .
时间复杂度例三:
//计算二分查找函数的时间复杂度
//a是传入数组的地址,N是数组元素个数,x是要查找的数
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) / 2);
if (x > a[mid])
begin = mid + 1;
else if (x < a[mid])
end = mid - 1;
else
return mid;
}
return -1;
}
这一题的基本操作就是执行 (int mid = begin + ((end - begin) / 2);)以及分支语句中的一条,但是根据上边两题的经验和大O的渐进表示法规则,小伙伴们已经知道只关注其中的一个即可,因为最后他们都是n的系数,最后都会被舍去,那么也就是求解二分算法执行的次数即while循环执行的次数.
分析:
二分查找算法是对于有序数组来找一个未知数的,二分查找在第一次查找后就会筛选掉一半的数据,第二次查找再次筛选掉一半数据..........,经过多次筛选的结果就是最后只剩一个数据等待筛选(这时两个指针都指向这个数据,具体细节看我算法专栏讲解双指针),我们查找的次数也就是while循环的次数.
证明要查找多少次(其实这是个数学题):
定义数组中的数据总数为N,第一次筛选掉一半的数据,剩下(N/2)个数据,经过第二次筛选掉一半的数据剩下(N/2/2)个数据,经过第三次筛选掉一半的数据剩下(N/2/2/2)个数据................以此类推,最后剩下1个数据,可以列出下列式子:
假如除去了x个2,最后等于1,得,对两边取对数得
,x就是while循环执行的次数也就是基本操作执行的次数,所以根据大O的渐进表示法二分查找算法的时间复杂度为:O(logN),并且以后再表示时间和空间复杂度时
,这时很重要的一点切记切记,后边讲解到二叉树部分你就会知道它的重要性了,现在记住即可!!!
注意:在表示时间复杂度和空间复杂度的时候
时间复杂度例四:
//计算斐波那契函数的时间复杂度
int func3(int N)
{
if (N == 1 || N == 0)
return 1;
else
return func3(N - 1) + func3(N - 2);
}
斐波那契数列:斐波那契数列是指从0和1开始,后面每一项都是前两项的和,即数列的第n个数是第n-1个数和第n-2个数的和。斐波那契数列可以用递归或循环的方式来生成。求一个斐波那契数时,要知道它前两个斐波那契数,相加就可以得出要求的这个;那如果要求第N个斐波那契数呢?需要递归该函数多少次呢?
这其实还是一个数学问题嘻嘻,由2^0加到2^n
这就是等比数列求和就不再多说了,最后结果是,根据大O的渐进表示法
只保留最高阶项:得到斐波那契数列的时间复杂度为O()
注意:递归算法的时间复杂度 = 递归的次数 * 每次递归函数中基本操作的次数!!!
课本上的概念:时间复杂度是描述算法运行时间与输入规模之间关系的度量,它表示算法的执行时间随输入规模增长而增长的速度。一般用大O记号(O)来表示时间复杂度,其中O(f(n))表示在最坏情况下算法的执行时间不会超过f(n)的常数倍,n表示输入规模的大小。通过分析算法的时间复杂度,可以评估算法在处理大规模数据时的效率,并进行比较和选择。
我理解的:算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
3->用例子带大家学会空间复杂度
前边提到了现在存储容量很大,很多时候不用去考虑空间复杂度,并且很多时候为了提高各种效率,我们会采用以空间换时间的做法,这一点请去我的MySQL专栏看索引部分的讲解相信会帮你深度理解它的好处.
再次说明:空间复杂度主要衡量一个算法需要的额外空间. 输出结论, 在使用中去关注定义的变量的个数, 这个定义的变量的次数你就看做空间复杂度.
空间复杂度例一:
//计算冒泡排序函数的空间复杂度
//a是传入待排序的数组地址,N是数组元素个数
void BubbleSort(int* a, int N)
{
for (int i = 0; i < N; i++)
{
int end = 0;
for (int j = 0; j < N - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
end = 1;
}
}
if (end == 0)
break;
}
}
我们要求该函数的空间复杂度那么就要去看使用的变量个数,这个很好看出来,我们只使用了end和tmp两个变量,即O(2),根据大O渐进表示法得到O(1).
空间复杂度例二:
//计算阶乘递归函数的空间复杂度
long long func4(size_t n)
{
return n < 2 ? n : func4(n - 1) * n;
}
我们的形参会根据传递的实参复制一份,也就是调用func4函数多少次就会创建出来多少个n,很好看出来我们的这个func4函数会调用n次,也就是说func4函数的空间复杂度为O(n).
注意:递归算法的空间复杂度通常是递归的深度(也就是递归的层数)
课本上的概念:空间复杂度是指算法在执行过程中所需的额外空间的量度。它主要针对算法运行过程中在内存中所分配的存储空间的大小。通常使用大O符号来表示空间复杂度。
例如,如果一个算法的空间复杂度为O(1),表示该算法所需的额外空间是一个常数值,与输入规模无关。而如果一个算法的空间复杂度为O(n),表示该算法所需的额外空间与输入规模成线性关系。在计算空间复杂度时,通常会考虑算法所使用的变量、数组、堆栈等数据结构所占用的空间,以及递归调用所需的额外空间等因素。
我的理解:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少字节的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
4->专属鼓励师
一遍不会没关系吖,多看几遍,我也是学了好多遍呢,小伙伴们肯定学的又快又好!!!最后希望写的内容对小伙伴们有所帮助,我写的如果有哪里不对的地方请指出来哦!让我们一起进步吖,任何疑问包括心情不好都可以找我聊聊,我很乐意当你的倾听者吖.