算法相关概念讲解和时间复杂度,空间复杂度的简单分析

1.算法的概念

    在数学和计算机中,算法 ( a l g o r i t h m ) (algorithm) (algorithm)是指一个被定义好的、计算机可实施的有限步骤或次序,常用于计算、数据处理和自动推理。算法是有效方法,包含一系列定义清晰的指令,并可于有限的时间及空间内清楚地表述出来。

    我们通常用时间复杂度空间复杂度来衡量一个算法的优劣,而同一个问题可以用具有不同时间复杂度和空间复杂度的算法来求解。

    算法具有以下五个重要特征。

  1. 输入项:一个算法有0个或多个输入。
  2. 输出项:一个算法有1个或多个输出。
  3. 确定性:算法的每一个步骤必须有确定的定义。即语句的描述不能存在二义性,对于每一种情况,需要执行的动作都应该是严格的、清晰的。
  4. 有穷性:算法必须能在执行有限步之后终止,即在执行有限步后自动结束,而不会出现有限循环,并且每一步都在规定的时间内完成。
  5. 可行性:算法执行的任何步骤都是可以被分解为基本的、可执行的操作步骤,即算法的每一条运算原则上都能精确地执行。

    其中,C++入门需要学习的算法有模拟(高精度),排序,枚举,递推递归,贪心,二分,搜索,简单动态规划等。

2.算法的描述

    :以下内容可能会触及到没有学过的知识,但是没太大关系,只需要大概了解一下主体内容就行。

    算法可采用多种语言来描述,各种描述语言在对问题的描述能力方面存在一定的差异。常用的方式有自然语言流程图伪代码等。不管用哪种描述方式表达算法,其描述的结果必须满足算法的五个特征。

2.1.自然语言描述

    算法的自然语言描述,指的是用日常使用的语言来描述解决问题的具体步骤。

    用自然语言描述算法的优点是通俗易懂,当算法中的操作步骤都是顺序执行时,比较直观,容易理解。缺点是如果算法中包含了分支结构和循环结构,并且操作步骤较多时,容易引起歧义,且描述得不够清晰

2.2.流程图描述

    流程图是以特定的图形符号加上说明来表示算法的图。流程图用一些图框来表示各种类型的操作,在框内写出各个步骤,然后用带箭头的线将它们连接起来,以表示算法执行的先后顺序

    使用流程图来描述算法,其优点是可以使读者更加清楚地了解整个算法的完整操作过程,有助于在工作过程中及时对算法进行修改。但流程图有其约定的符号,绘制时需要根据其符号进行搭建,绘制过程比较繁琐

    具体可以参考这一篇文章

2.3.伪代码描述

    伪代码是一种非正式的用于描述模块结构图的语言。使用伪代码的目的是使被描述的算法可以容易地以任何一种编程语言实现。因此,伪代码必须结构清晰代码简单可读性好。伪代码介于自然语言与编程语言之间,以编程语言的书写形式说明算法功能。使用伪代码,你用拘泥于具体实现,相比程序语言它更类似自然语言,可以将整个算法运行过程的结构用接近自然语言的形式描述出来。

    使用伪代码描述算法没有严格的语法限制,书写格式比较自由,只要把意思表达清楚就可以了,它更侧重于对算法本身的描述。在伪代码描述中,表述关键词的语句一般用英文单词,其他语句可以用英文语句,也可以用中文语句。

    以下是一个冒泡排序的伪代码示例。

flag<-0
for i<-1 to n-1 do
    if flag为真 then do
        break
    for j<-1 to n-i do
        if a(j)>a(j+1) then do
            swap a(j),a(j+1)
            flag<-1

3.时间复杂度概念、简单分析及相关例题

    一个程序在写出来之前是无法准确估计实际运行时间的。但是几乎所有算法竞赛的任务都会告知输入数据的规模和运行时间限制,这就允许选手通过分析算法的时间复杂度(Time complexity),从而实现估计能否在限定的时间能运行完程序。

    真的有这么神奇吗?别急,让我们不妨先来看一道例题。

3.1.例题.最大子段和

传送门 - 洛谷

题目描述

给出一个长度为 n n n 的序列 a a a,选出其中连续且非空的一段使得这段和最大。

输入格式

第一行是一个整数,表示序列的长度 n n n

第二行有 n n n 个整数,第 i i i 个整数表示序列的第 i i i 个数字 a i a_i ai

输出格式

输出一行一个整数表示答案。

输入输出样例

输入 #1
7
2 -4 3 -1 2 -4 3
输出 #1
4

提示

样例 1 解释

选取 [ 3 , 5 ] [3, 5] [3,5] 子段 { 3 , − 1 , 2 } \{3, -1, 2\} {3,1,2},其和为 4 4 4

数据规模与约定
  • 对于 40 % 40\% 40% 的数据,保证 n ≤ 2 × 1 0 3 n \leq 2 \times 10^3 n2×103
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 2 × 1 0 5 1 \leq n \leq 2 \times 10^5 1n2×105 − 1 0 4 ≤ a i ≤ 1 0 4 -10^4 \leq a_i \leq 10^4 104ai104

思路1.直接枚举

    最直接的思路就是双重循环,枚举某一个区间的所有数并进行累加。代码如下:

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n,a[200010],sum,ans=INT_MIN;//用ans记录当前答案,sum记录当前区间的和
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++)//枚举起点
        for(int j=0;i+j<=n;j++)//枚举长度
        {
        	sum=0;//归0
        	for(int k=0;k<=j;k++)//累加
                sum+=a[i+k];
            ans=max(ans,sum);//更新答案
		}
    cout<<ans;
    return 0;
}

    那么,这个程序运行需要花多久时间呢?这主要还跟输入规模 n n n有关。我们可以根据这份代码估计这个程序运行的次数

    这段代码中, n n n比较大时,sum+=a[i+k];会运行很多次。对于给定的 n n n,我们可以定义一个计数器cnt,每运行一次这个语句就cnt++,最后再输出。

    当然,我们也可以使用数学的方法来估算。假设给定的 n n n 3 3 3。设时间复杂度为 T ( n ) T(n) T(n),最外层的循环变量i x x x时的运行总次数为为 S x S_x Sx,可以得出:
T ( 3 ) = S 1 + S 2 + S 3 = ( 1 + 2 + 3 ) + ( 1 + 2 ) + 1 = ( 1 + 3 ) ∗ 3 2 + ( 1 + 2 ) ∗ 2 2 + ( 1 + 1 ) ∗ 2 2 ( 等差数列求和公式 ) = 1 2 ( 4 ∗ 3 + 3 ∗ 2 + 2 ∗ 1 ) = 1 2 [ ( 3 2 + 3 ) + ( 2 2 + 2 ) + ( 1 2 + 1 ) ] = 1 2 [ ( 1 2 + 2 2 + 3 2 ) + ( 1 + 2 + 3 ) ] = 1 2 [ 1 6 ∗ 3 ( 3 + 1 ) ( 2 ∗ 3 + 1 ) + ( 1 + 2 + 3 ) ] ( 平方和公式,其中 n = 3 ) = 1 2 [ 1 6 ∗ 3 ( 3 + 1 ) ( 2 ∗ 3 + 1 ) + ( 1 + 3 ) ∗ 3 2 ] ( 等差数列求和公式 ) \begin{align*} T(3)&=S_1+S_2+S_3\\ &=(1+2+3)+(1+2)+1\\ &=\frac{(1+3)*3}2+\frac{(1+2)*2}2+\frac{(1+1)*2}2(等差数列求和公式)\\ &=\frac1 2(4*3+3*2+2*1)\\ &=\frac1 2[(3^2+3)+(2^2+2)+(1^2+1)]\\ &=\frac1 2[(1^2+2^2+3^2)+(1+2+3)]\\ &=\frac1 2[\frac1 6*3(3+1)(2*3+1)+(1+2+3)](平方和公式,其中n=3)\\ &=\frac1 2[\frac1 6*3(3+1)(2*3+1)+\frac{(1+3)*3}2](等差数列求和公式) \end{align*} T(3)=S1+S2+S3=(1+2+3)+(1+2)+1=2(1+3)3+2(1+2)2+2(1+1)2(等差数列求和公式)=21(43+32+21)=21[(32+3)+(22+2)+(12+1)]=21[(12+22+32)+(1+2+3)]=21[613(3+1)(23+1)+(1+2+3)](平方和公式,其中n=3)=21[613(3+1)(23+1)+2(1+3)3](等差数列求和公式)
    从一般到特殊,我们可以得出一般情况下输入规模为 n n n时此程序的运行次数的代数式。
T ( n ) = S 1 + S 2 + S 3 + . . . + S n = 1 2 [ ( n + 1 ) 2 + n 2 + ( n − 1 ) 2 + . . . 1 2 + 1 ] = 1 2 [ ( 1 2 + 2 2 + 3 2 + . . . + n 2 ) + ( 1 + 2 + 3 + . . . + n ) ] = 1 2 [ 1 6 n ( n + 1 ) ( 2 n + 1 ) + ( 1 + 2 + 3 ) ] ( 平方和公式 ) = 1 2 [ 1 6 n ( n + 1 ) ( 2 n + 1 ) + ( 1 + n ) ∗ n 2 ] ( 等差数列求和公式 ) = 1 6 ( n 3 + 3 n 2 + 2 n ) \begin{align*} T(n)&=S_1+S_2+S_3+...+S_n\\ &=\frac1 2[(n+1)^2+n^2+(n-1)^2+...1^2+1]\\ &=\frac1 2[(1^2+2^2+3^2+...+n^2)+(1+2+3+...+n)]\\ &=\frac1 2[\frac1 6n(n+1)(2n+1)+(1+2+3)](平方和公式)\\ &=\frac1 2[\frac1 6n(n+1)(2n+1)+\frac{(1+n)*n}2](等差数列求和公式)\\ &=\frac 1 6(n^3+3n^2+2n) \end{align*} T(n)=S1+S2+S3+...+Sn=21[(n+1)2+n2+(n1)2+...12+1]=21[(12+22+32+...+n2)+(1+2+3+...+n)]=21[61n(n+1)(2n+1)+(1+2+3)](平方和公式)=21[61n(n+1)(2n+1)+2(1+n)n](等差数列求和公式)=61(n3+3n2+2n)
    其中, n 3 n^3 n3对这个式子起主导作用,可以说这个程序的时间复杂度就是 O ( n 3 ) O(n^3) O(n3)

    一般的家用计算机一秒可以运行千万到数亿( 1 0 8 10^8 108)。代入这道题的数据范围,得到 n 3 = 4 ∗ 1 0 10 n^3=4*10^{10} n3=41010,不能通过所有的测试点。
在这里插入图片描述

思路2.动态规划

    这道题的正解是动态规划。这里暂时不需要知道动态规划是什么,直接给出代码。

#include<bits/stdc++.h>
using namespace std;
#define MAXN 200010
int main()
{
	int dp[MAXN],a[MAXN],n,ans=INT_MIN;
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    dp[1]=a[1];//初始状态 
    for(int i=2;i<=n;i++)
    {
    	dp[i]=max(a[i],dp[i-1]+a[i]);//状态转移方程 
    	ans=max(ans,dp[i]);
	}
    cout<<ans;
	return 0;
}

    这里我们只需要分析时间复杂度就行了。大多数情况下,我们都不需要使用复杂的数学公式进行推导,而是只需要得出一个大概的式子。例如上面的代码,主要部分就是这个代码段:

for(int i=2;i<=n;i++)
{
    dp[i]=max(a[i],dp[i-1]+a[i]);//状态转移方程 
    ans=max(ans,dp[i]);
}

    可以看出,这个循环一共执行了 n − 1 n-1 n1次。那么它的时间复杂度大概就是 O ( n ) O(n) O(n)

3.2.不同量级的时间复杂度总结和归纳

    在编写程序时,我们通常会使用估算的方法来计算某个算法的时间复杂度。有的时候,为了严谨,也会使用数学公式进行推导。

    在编写程序时,我们要根据题目给出的数据范围,合理编写不同时间复杂度的程序。下表提供了常见复杂度与数据范围。

复杂度常见范围冒险范围非专业术语
O ( 1 ) O(1) O(1) 1 0 30 10^{30} 1030 ∞ \infty 常数阶
O ( n ) O(n) O(n) 1 0 7 10^7 107 1 0 8 10^8 108常数阶
O ( n log ⁡ n ) O(n\log n) O(nlogn) 3 ∗ 1 0 5 3*10^5 3105 1 0 6 10^6 106\
O ( n log ⁡ log ⁡ n ) O(n\log\log n) O(nloglogn) 1 0 5 10^5 105 1 0 6 10^6 106\
O ( n 2 ) O(n^2) O(n2) 5 ∗ 1 0 3 5*10^3 5103 1 0 4 10^4 104平方阶
O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn) 1 0 3 10^3 103 3 ∗ 1 0 3 3*10^3 3103\
O ( n 3 ) O(n^3) O(n3) 100 100 100 400 400 400立方阶
O ( 2 n ) O(2^n) O(2n) 20 20 20 25 25 25指数阶
O ( n ∗ 2 n ) O(n*2^n) O(n2n) 15 15 15 20 20 20\
O ( n ! ) O(n!) O(n!) 10 10 10 10 10 10\

    下图提供了不同时间复杂度的运算次数随数据规模增长趋势。
在这里插入图片描述
    有的时候,我们并不能直接编写出满分程序。这个时候,我们可以尝试编写非完美算法

  1. 完成较小数据范围的高复杂度算法;
  2. 解决部分特殊情况;
  3. 使用近似算法,比如随机算法,启发式搜索,模拟退火等。

4.空间复杂度概念、简单分析及相关例题

    空间复杂度是评估算法的另一个重要因素。不过在比赛时,给出的栈空间,即空间限制十分充裕。这就导致了空间复杂度相比来说就没有时间复杂度那么重要。通常情况下,如果程序中使用了数组空间复杂度就是 O ( n ) O(n) O(n),否则就是 O ( 1 ) O(1) O(1)

    注意:在使用递归函数,以及 S T L STL STL中的容器时,也有可能造成栈空间溢出,需要注意。

4.1.例题

    试分析下面算法的空间复杂度。

    注意:MAXNMAXM分别为nm的数据范围。

#include <iostream>
using namespace std;
#define MAXN 110
#define MAXM 1010
int a[MAXN][MAXM], n, m, cnt;
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j=1; j <= m; j++)
            cin >> a[i][j];
    for (int i = 1; i <= n ; i++)
        for (int j = 1 ; j <= m; j++)
            if(a[i][j] == 10)
                cnt++;
    cout << cnt;
    return 0;
}

    解析:本题中的空间开销主要来源于数组。所以主要关注数组占用的内存。其占用内存为n*m,故该算法的空间复杂度为 O ( n m ) O(nm) O(nm)

4.2.注意事项

    在代码运行的过程中,开辟数组,设立STL容器,运行递归函数,都会占用较大内存。特别是设立STL容器,需要小心题目的数据范围。

5.练习题目和答案

5.1.题目

    试分析下面每个算法的时间复杂度和空间复杂度(递归函数带来的开销不需要计算):
1.

int accumulation(int n) {
    if (n == 1)
        return n;
    else
        return n + accumulation(n - 1);
}
bool is_prime(int num) {
    if (num < 2)
        return false;
    for (int i = 1; i * i <= num; i++)
        if (num % i == 0)
            return false;
    return true;
}
#include <iostream>
using namespace std;
int n;
long long f[15] = {1, 1};
int main() {
    cin >> n;
    for (int i = 2; i <= n; i++)
        f[i] = f[i - 1] * i;
    cout << f[n] <<endl;
    return 0;
}
#include <iostream>
#include <cstring>

using namespace std;

#define MAXN 1010
int n, primes[MAXN], pcnt;
bool is_prime[MAXN];

int main() {
    cin >> n;
    memset(is_prime, 1, sizeof(is_prime));
    is_prime[0] = is_prime[1] = 0;
    for (int i = 2; i <= n; i++) {
        if (is_prime[i])
            primes[pcnt++] = i;
        for (int j = 0;  j < pcnt && i * primes[j] <= n;  j++) {
            is_prime[i * primes[j]] = 0;
            if (i % primes[j] == 0)
                break;
        }
    }
    for (int i = 0; i < pcnt; i++)
	    cout << primes[i] << endl; 
    return 0;
}
#include<iostream>

using namespace std;

int n, k;

int solve1() {
    int l = 0, r = n;
    while (l < r) {
        int mid = (l + r) / 2;
        if (mid * mid <= n) l = mid + 1;
        else r = mid;
    }
    return l - 1;
}

double solve2(double x) {
    if(x == 0) return x;
    for (int i = 0; i < k; i++)
        x = (x + n / x) / 2;
    return x;
}

int main() {
    cin >> n >> k;
    double ans = solve2(solve1());
    cout<< ans << ' ' << (ans * ans == n) << endl;
    return 0;
}

5.2.答案

题号/类别时间复杂度空间复杂度
1 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)
2 O ( n ) O(\sqrt n) O(n ) O ( 1 ) O(1) O(1)
3 O ( n ) O(n) O(n) O ( n ) O(n) O(n)
4 O ( n ) O(n) O(n) O ( n ) O(n) O(n)
5 O ( log ⁡ n + k ) O(\log n+k) O(logn+k) O ( 1 ) O(1) O(1)

解析:

  1. 可以转换成以下等价的迭代形式;
for (int i = 1; i <= n; i++)
    sum += i;
return sum;

    故时间复杂度为 O ( n ) O(n) O(n)
2. 主要分析下列循环,i*i<=num可以化成i<=sqrt(num)

for (int i = 1; i * i <= num; i++)
  1. 主要分析下列循环。
for (int i = 2; i <= n; i++)
  1. 主要分析两个循环。

    第一个,循环次数为 n n n

for (int i = 2; i <= n; i++)

    第二个,由于质数越到后面越稀疏,所以时间复杂度接近常数。

for (int j = 0;  j < pcnt && i * primes[j] <= n;  j++)

    合并得: O ( n ∗ 1 ) = O ( n ) O(n*1)=O(n) O(n1)=O(n)
5. 第一个函数,每次查找序列长度减半,时间复杂度为 O ( log ⁡ 2 n ) O(\log_2 n) O(log2n),简称 O ( log ⁡ n ) O(\log n) O(logn)

int solve1() {
    int l = 0, r = n;
    while (l < r) {
        int mid = (l + r) / 2;
        if (mid * mid <= n) l = mid + 1;
        else r = mid;
    }
    return l - 1;
}

    第二个函数,时间复杂度为 O ( k ) O(k) O(k)

for (int i = 0; i < k; i++)

    合并得: O ( log ⁡ n + k ) O(\log n+k) O(logn+k)

6.小知识

    你知道时间复杂度和空间复杂度中的 O O O是怎么来的吗?

‌    时间复杂度和空间复杂度中的的 O O O是由德国数论学家保罗·巴赫曼 P a u l   B a c h m a n n Paul\space Bachmann Paul Bachmann)在其 1892 1892 1892年的著作《解析数论》( A n a l y t i s c h e   Z a h l e n t h e o r i e Analytische\space Zahlentheorie Analytische Zahlentheorie)中首先引入的。‌

    时间复杂度是计算机科学中用于描述算法运行时间随输入数据规模增长的趋势的一种方法。大 O O O表示法,即 T = O ( f ( n ) ) T=O(f(n)) T=O(f(n))。尽管大 O O O表示法最初是由数论学家提出的,但在计算机科学领域,它成为了评估算法性能的重要指标之一‌

喜欢就订阅此专辑吧!

【蓝胖子编程教育简介】
蓝胖子编程教育,是一家面向青少年的编程教育平台。平台为全国青少年提供最专业的编程教育服务,包括提供最新最详细的编程相关资讯、最专业的竞赛指导、最合理的课程规划等。本平台利用趣味性和互动性强的教学方式,旨在激发孩子们对编程的兴趣,培养他们的逻辑思维能力和创造力,让孩子们在轻松愉快的氛围中掌握编程知识,为未来科技人才的培养奠定坚实基础。

欢迎扫码关注蓝胖子编程教育
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝胖子教编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值