【数据结构】第一站:复杂度
创始人
2025-05-28 12:54:09
0

目录

一、算法效率

1.如何衡量一个算法的好坏

2.算法的复杂度

二、时间复杂度

1.时间复杂度概念

2.大O渐进表示法

3.关于时间复杂度的一些计算

三、空间复杂度

四、常见复杂度对比

五、两道经典的题目

1.消失的数字

2.轮转数组


一、算法效率

1.如何衡量一个算法的好坏

对于一个算法,我们想要衡量他的效率

我们有两个方向可以去衡量,时间和空间

但是要注意,这里的时间并不是绝对的时间。因为对于同一个算法,可能会由于硬件设施的不同而导致运行时间的不同。因此不能简单的将绝对时间作为算法效率的衡量标准。对于时间而言,我们衡量的是算法的大概的执行次数

2.算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

二、时间复杂度

1.时间复杂度概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度

如下面的例子

// 请计算一下Func1中++count语句总共执行了多少次?
#include
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);
}

对于这个函数而言,我们不难得出他的运行次数为f(N)=N²+2N+10

但是这个过于繁琐了。我们不需要那么精确,我们抓大头,当n很大的时候,影响最大的一项就是我们最需要关注的一项。即最高次

所以这个运行次数我们简记为N²

为此我们也引出了大O渐进表示法,这个函数的大O渐进表示法为O(N²)

2.大O渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

如上题的大O渐进表示法结果为O(N²)

3.关于时间复杂度的一些计算

例一:

// 计算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);
}

显然精确的次数为2N+10,但是大O渐进表示形式就为O(N)了

例二:

// 计算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);
}

精确次数为M+N次

所以大O渐进表示形式为O(M+N),如果M>>N,那么可以近似为O(M),反之近似为O(N)

例三:

// 计算Func4的时间复杂度?
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++k){++count;}printf("%d\n", count);
}

精确次数为100次

他的大O渐进表示为O(1),注意这里的1并不是一次,而是常数次

例四:

// 计算strchr的时间复杂度?
const char* strchr(const char* str, int character);

这个其实是一个库函数,他的作用是从字符串中找出对应的字符,他的实现是通过依次遍历来完成的

既然是通过遍历来寻找,那么我们就发现,他有三种特殊情况:最好、最坏和平均

他的最好次数为:O(1),即只需要一次就找到了

最坏情况是:O(N),也就是最后一次才找到

平均情况为:N/2次,即O(N)

对于出现最好最坏的情况,我们一般取最坏情况

例五:

// 计算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;}
}

这个也是一种冒牌排序算法,但是要注意这个冒泡排序与我们之前写的有一点不同,我们加了一个break来进行控制,这样的话

对于最好的情况,也就是顺序的情况,他的次数为O(N),因为他有一个exchange来进行控制,如果他是顺序,那么第一次循环的时候,就会由于exchange没有被赋为1,导致外层循环直接被跳出,仅仅只有内层循环执行了N-1次,所以他的时间复杂度为O(N)

最坏情况:也就是彻底的逆序,那么就需要执行N-1 +N-2 +.....+1,即O(N²)

所以他的时间复杂度为O(N²)

但是假如说没有了这个exchange来控制的话,那么无论最好还是最坏都是O(N²)

例六:

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{assert(a);int begin = 0;int end = n - 1;// [begin, end]:begin和end是左闭右闭区间,因此有=号while (begin <= end){int mid = begin + ((end - begin) >> 1);if (a[mid] < x)begin = mid + 1;else if (a[mid] > x)end = mid - 1;elsereturn mid;}return -1;
}

这是一个二分查找

最好情况当然是O(1)了,一次就找到了

最坏情况就是O(logN)次,原因是,二分查找是每次缩小一半去查找的,最后的一个区间长度是1。因此我们逆着来思考,1*2*2....*2=N。就是我们的方程,每查找一次就乘以2,即方程又可化为2^X=N,所以X=log(2,N),意思是以2 为底

为了方便简写,我们一般默认以2为底时可以省略这个底数

例七:

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{if (0 == N)return 1;return Fac(N - 1) * N;
}

这道题从栈帧的角度去理解,他开辟了多少次函数的栈帧,显然调用F(N)就需要调用F(N-1).......一直调用到F(0),所以时间复杂度为O(N)

例八:

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{if (N < 3)return 1;return Fib(N - 1) + Fib(N - 2);
}

如上图所示,他是一个不断递归调用的过程,那么就需要画出他的图解。第一层是2^0次方,第二层是2^1次方次......,当然后面的圆圈处又一些会提前结束,但是我们可以忽略不记。近似认为是补全的。根据等比数列求和可解得时间复杂度为O(2^N)

三、空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定

例一:

// 计算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;}
}

对于这个冒泡排序,他所额外占用的变量数是3个,所以空间复杂度为O(1)

例二:

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{if (n == 0)return NULL;long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));fibArray[0] = 0;fibArray[1] = 1;for (int i = 2; i <= n; ++i){fibArray[i] = fibArray[i - 1] + fibArray[i - 2];}return fibArray;
}

显然空间复杂度为O(N)

例三:

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{if(N == 0)return 1;return Fac(N-1)*N;
}

 

 如上图所示,是这个函数的栈帧图,显然是O(N)

例四:

// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{if (N < 3)return 1;return Fib(N - 1) + Fib(N - 2);
}

对于这个斐波那契算法的空间复杂度,其实很容易陷入误区,误以为是O(2^N),其实不然。他的空间复杂度是O(N)

这是因为当函数递归到最后一层的时候,又会进行销毁这一层的栈帧,将空间还给操作系统,然后操作系统又将空间给了下一个栈帧。他的栈帧调用图如下所示

 他始终只有这O(N)的空间在反复利用

四、常见复杂度对比

如下是一些常见的时间复杂度

 

 

五、两道经典的题目

1.消失的数字

题目链接:力扣

法1:排序加二分查找

排序加二分查找

这种方法的时间复杂度其实是不满足条件的

法2:异或

将整个数组进行异或

然后将1到n与这个数组进行异或,最终的结果就是缺失的数字

int missingNumber(int* nums, int numsSize){int val=0;int i=0;for(i=0;i
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

法3:等差数列公式

先将整个数组求和,然后将从1~n进行求和

两个和相减即可

2.轮转数组

题目链接:力扣

法1:依次移动每个元素

先写一个可以轮转一次的函数,然后依次轮转k次

时间复杂度为O(K*N)

最坏情况需要轮转N-1次,时间复杂度为O(N²)

当然这种方法的时间复杂度太大了,对于这道题而言是无法通过的

法2:三次逆置

先逆置前n-k个数据

然后逆置后k个数据

最后整体逆置

void reverse(int* a, int begin, int end)
{while (begin < end){int tmp = a[begin];a[begin] = a[end];a[end] = tmp;begin++;end--;}
}
void rotate(int* nums, int numsSize, int k) {if (k >= numsSize){k = k % numsSize;}reverse(nums, 0, numsSize - 1 - k);reverse(nums, numsSize - k, numsSize - 1);reverse(nums, 0, numsSize - 1);
}

他的时间复杂度是O(N)

空间复杂度是O(1)

法3:以空间换时间

这种方法就是将后k个数据拷贝到一个新的数组中

然后将前n-k个数据拷贝到新数组的后面

最后在将新数组的数据整体拷贝到新的数组中

void rotate(int* nums, int numsSize, int k){k=k%numsSize;int* a=(int*)malloc(sizeof(int)*numsSize);memcpy(a,nums+numsSize-k,k*sizeof(int));memcpy(a+k,nums,(numsSize-k)*sizeof(int));memcpy(nums,a,numsSize*sizeof(int));free(a);
}

时间复杂度和空间复杂度都是O(N)


好了本期内容就到这里

如果对你有帮助的话,不要忘记点赞加收藏哦!!!

相关内容

热门资讯

教育名著读书心得 教育名著读书心得(通用29篇)  当我们积累了新的体会时,就十分有必须要写一篇心得体会,这样能够给人...
洛阳实习报告 洛阳实习报告  一段充实而忙碌的实习生活结束了,相信你积累了不少实习心得,让我们一起来学习写实习报告...
《小英雄雨来》读书笔记 《小英雄雨来》读书笔记45篇  读完一本书以后,大家心中一定是萌生了不少心得,何不写一篇读书笔记记录...
团支部个人心得体会 团支部个人心得体会范文(精选5篇)  当我们积累了新的体会时,不如来好好地做个总结,写一篇心得体会,...
劳动节活动心得体会 劳动节活动心得体会(通用5篇)  当在某些事情上我们有很深的体会时,可用写心得体会的方式将其记录下来...
护理学专业的心得体会 护理学专业的心得体会(通用13篇)  在平日里,心中难免会有一些新的想法,马上将其记录下来,这样有利...
工作心得体会感悟 工作心得体会感悟(通用18篇)  从某件事情上得到收获以后,往往会写一篇心得体会,如此就可以提升我们...
采购课程培训心得体会 采购课程培训心得体会范文(通用13篇)  我们心里有一些收获后,可以通过写心得体会的方式将其记录下来...
疫情期间做社区志愿服务心得 疫情期间做社区志愿服务心得  有了一些收获以后,写一篇心得体会,记录下来,这样可以帮助我们分析出现问...
被隔离人员心得体会 被隔离人员心得体会  我们有一些启发后,将其记录在心得体会里,让自己铭记于心,这么做可以让我们不断思...
从优秀到卓越读书心得 从优秀到卓越读书心得(通用18篇)  当阅读了一本名著后,大家心中一定有不少感悟,是时候静下心来好好...
小学生读书心得体会 小学生读书心得体会范文(精选10篇)  当我们受到启发,对生活有了新的感悟时,不妨将其写成一篇心得体...
教师学习心得体会 实用的教师学习心得体会(通用15篇)  我们得到了一些心得体会以后,往往会写一篇心得体会,它可以帮助...
档案管理的心得体会 档案管理的心得体会(精选11篇)  我们得到了一些心得体会以后,可以记录在心得体会中,这样我们就可以...
超市社会实践心得体会 超市社会实践心得体会范文(精选14篇)  我们在一些事情上受到启发后,可以寻思将其写进心得体会中,这...
保密工作心得 保密工作心得保密工作心得通过学习中央领导关于保密工作重要指示精神,了解到了保密工作在我们工作中的重要...
作风建设心得体会 作风建设心得体会(精选16篇)  当我们经过反思,有了新的启发时,可以记录在心得体会中,这样可以不断...
参加辅导员培训班心得 参加辅导员培训班心得范文  参加了咱们学校本学期举办的辅导员培训班后,我有所体会,获益匪浅。  很高...
解读《中学教师专业标准》心得 解读《中学教师专业标准》心得  《中学教师专业标准》的基本理念是:  (一)学生为本:尊重中学学生权...
爱岗敬业心得体会 爱岗敬业心得体会(精选12篇)  当我们经过反思,有了新的启发时,可以记录在心得体会中,这样能够培养...