算法复杂度_数据结构与算法(一)基本概念与算法复杂度

基本概念与算法复杂度


目录: 

  • 什么是数据结构,什么是算法?

  • 算法复杂度

  • 求解时间复杂度

  • 常见时间复杂度


什么是数据结构,什么是算法?

    数据结构(data structure),从字面上来看,是“数据的结构”。

在计算机科学中,数据结构是一种数据组织、管理和存储的格式,它可以帮助我们实现对数据高效的访问和修改。更准确地说,数据结构是数据值的集合,可以体现数据值之间的关系,以及可以对数据进行应用的函数或操作。

数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。

    从逻辑的角度上来看,数据结构可以分为四种,分别是集合结构线性结构线性结构图形结构

    从数据存储的角度上来看,数据结构也可以分为四种,分别是顺序存储结构链式存储结构索引存储结构散列/哈希存储结构

    我个人浅薄地认为,所谓数据结构,就像它的名字,就是“数据的结构”,是数据的组织形式、存储形式。它广泛存在于我们的生活中,是的,我们生活中就存在着大量的数据结构。举个例子,一所高中需要整理所有学生的信息,那么我想,通常而言,这所学校会用EXCEL表格组织学生信息,每一行代表一个学生,其中包含这个学生的基本信息。这种以一行代表一个学生的顺序结构,实际上就是一种类似顺序表的数据结构。

34c8fff2d86b274529b96af6b9128f15.png

    再举个例子,我们需要绘制一个家族的族谱图,那么我们更可能使用图形结构(或树形结构)来组织家族信息,因为这无疑是更高效更直观的组织形式。

7419ee5d789f1a272a789d836cf55760.png

    不同场景下,选择不同的数据结构能带来更高的运行和存储效率,乃至更清晰的逻辑结构。

    算法(Algorithm),或许可以被简单地理解为,解决问题的步骤,或者说解决问题的一系列指令。

算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。

    金字塔以其工程规模之浩大,之复杂闻名于世,其建造者古埃及人在建造金字塔过程中,掌握了基本的几何知识。他们很早就知道如何作出过直线 L 上一点 A 的垂直线的方法。当然,他们可不是拿着直尺画直线,而是借助一根绳子来实现。

b4cf6d39d653f2496ddc7b3cd6920c0b.png

    这个算法的实现需要三人之力!我们将其分为 A,B,C。然后我们需要一段十二等分首尾相连的绳子。AB 之间距离四等分,AC 之间距离三等分,BC 之间距离五等分。AB 与所需做垂线的直线 L 重合,然后,C 将直线拉直,这样沿 AC 连线就能得到垂直于 AB 的直线。

    用算法语言可以描述为:

perpendicular(L, A)
输入: 直线 L 及其上一点 A
输出: 过 A 点且垂直于 L 的直线
1.取一根可十二等分的首尾相连的绳子
2.找来三个帅哥 A B C
3.帅哥 A 与帅哥 B 之间距离四等份绳子
4.帅哥 A 与帅哥 C 之间距离三等份绳子
5.帅哥 B 与帅哥 C 之间距离五等份绳子
6.帅哥 A 和帅哥 B 站在直线 L 上,并拉直两人之间的绳子
7.帅哥 C 把绳子拉直
8.经过帅哥 A 和帅哥 C 绘制一条直线 //此直线即垂直于直线 L 且过 A 点的直线

    这个古埃及人做垂线的方法实际上就是一种算法。

    算法应当具有以下特征:

有穷性(Finiteness)
    算法的有穷性是指算法必须能在执行有限个步骤之后终止。
确切性(Definiteness)
    算法的每一步骤必须有确切的定义。
输入项(Input)
    一个算法有零个或多个输入,以刻画运算对象的初始情况,
    所谓零个输入是指算法本身定出了初始条件。
输出项(Output)
    一个算法有一个或多个输出,以反映对输入数据加工后的结果。
    没有输出的算法是毫无意义的。
可行性(Effectiveness)
    算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,
    即每个计算步骤都可以在有限时间内完成(也称之为有效性)。
/*
正确性(Correctness)
    算法给出的输出应该能够符合由问题本身在事先确定的条件。
*/

算法复杂度

(1)时间频度/算法执行时间  

    在解决一个实际问题时,往往存在多种可行算法,它们能得到相同结果,但在过程中消耗的时间和空间却存在很大区别。那么,如何度量实施一个算法所需的时间呢?影响算法执行时间的因素很多,其中最为关键的是问题规模,或者说输入规模。因此,上面的问题可以转化为:随着输入规模的扩大,算法的执行时间将如何增长?

    特定算法处理规模为  的问题所需的时间可以记作   ,通常而言,我们可以将  理解为算法所需执行指令的个数。但正如聪明的你可能想到的,同样规模为  的输入,算法的执行时间却也可能不同。以冒泡排序为例,输入 12345678 和输入 87654321 所需要的执行时间显然不同,但它们的输入规模却相同。因此,从保守估计的角度出发,  通常选规模  下执行时间最长者

(2)时间复杂度  

    因此,我们现在能在某特定规模  下借助    和    比较两算法 A 和 B 在执行时间上的优劣。但这远不够对这两个算法的优劣下整体判断。因为也许算法 A 在小规模输入中更快,而算法 B 在大规模输入中更高效。

    但我们往往会忽略不同算法解决小规模问题的差异。这是显而易见的,小规模问题本来就无需大量时间计算,不同算法之间的效率差距不明显。而大规模问题中,算法的效率但凡有些微差距,都会对执行时间产生巨大影响。因此,我们应着眼算法处理大规模问题的执行时间。

    那么现在,我们回头看看之前提出的问题:如何度量实施一个算法所需的时间?或者说,随着输入规模的扩大,算法的执行时间将如何增长?

    不难理解,这个问题需要我们关注  增长速度的趋势。

    保守估计,我们主要关注  的渐近上界(增长速度上限)。若存在自然数    和函数    使得对任何   都有:

    那么就说,  给出了  增长速度的一个渐进上界。我们使用大  记号来表示这个渐进上界。

      具有如下两条性质:

1. 对于任一常数  ,有  

2. 对于任意常数  ,有  

    我们称  为算法的渐近时间复杂度,简称时间复杂度

    由于  本身是一种保守估计,因此我们可以这么说,对于规模为  的输入,算法所需的最长的执行时间为  。

(3)求解时间复杂度

求解算法的时间复杂度的具体步骤是:

⑴ 找出算法中的基本语句;

算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。  ⑵ 计算基本语句的执行次数的数量级;  

只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。  

⑶ 用大  记号表示算法的时间性能。  

将基本语句执行次数的数量级放入大  记号中。

如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。例如:

for (i=1; i<=n; i++)
    x++;
for (i=1; i<=n; i++)
   for (j=1; j<=n; j++)
      x++;

第一个for循环的时间复杂度为  ,第二个for循环的时间复杂度为  ,则整个算法的时间复杂度为    。[2]

(4)常见时间复杂度

    常见的算法复杂度有:

常数  
对数  
线性  
线性对数  
多项式  
指数  

    需要说明的是,其中多项式  实际上代指诸如      一类时间复杂度。接下来我们逐个分析上述时间复杂度。

1. 常数  

如果算法的执行时间不随着问题规模  的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是  。[2]

void swap(int& left, int& right) //交换数值{
    left = left + right;
    right = left – right;
    left = left – right;
}

    上面的 swap 算法显然就是一个时间复杂度为  的算法。这是一类非常高效的算法,称为“常数时间复杂度算法”。

2. 对数  

    算法的运行时间若能表示为    ,则它的时间复杂度即为  ,称“对数多项式时间复杂度的算法”。这是一类高效的算法。

int logarithm(int n){
    int output = 1;
    while (output         output *= 2;
    //假设循环执行 m 次,则 m = logn, 显然 O(n) = logn
    return output;
}

    需要注意的是,我们通常忽略对数的底数。

3. 线性  

    凡运行时间可以表示为  的算法称为“线性时间复杂度算法”。这类算法的效率比较高。

 /**********************************************************************
  * Data Structures in C++
  * ISBN: 7-302-33064-6 & 7-302-33065-3 & 7-302-29652-2 & 7-302-26883-3
  * Junhui DENG, deng@tsinghua.edu.cn
  * Computer Science & Technology, Tsinghua University
  * Copyright (c) 2003-2020. All rights reserved.
  **********************************************************************/

int sumI ( int A[], int n )  //数组求和算法(迭代版){
    int sum = 0; //初始化累计器,O(1)
    for ( int i = 0; i //对全部共O(n)个元素,逐一
       sum += A[i]; //累计,O(1)
    return sum; //返回累计值,O(1)
} //O(1) + O(n)*O(1) + O(1) = O(n+2) = O(n)

    本段代码来源见注释。

4. 线性对数  

    简单来说,将对数阶  的代码重复  次,自然就得到了线性对数阶的时间复杂度。这类算法能较好地解决问题。

int logarithm(int n){
    int output = 1;
    for (int m = 0; m //重复 n 次
        while (output             output *= 2;
    return output;
}

5. 多项式  

    运行时间能表示为  的形式,且  为多项式;或者说,  ,其中  ,则称此类算法为“多项式时间复杂度算法”。如果问题存在这个复杂度的算法,则称这个问题可解有解

    下例是冒泡算法,其时间复杂度为  。显然,这是一个“多项式时间复杂度算法”。

/***********************************************************************
 * Data Structures in C++
 * ISBN: 7-302-33064-6 & 7-302-33065-3 & 7-302-29652-2 & 7-302-26883-3
 * Junhui DENG, deng@tsinghua.edu.cn
 * Computer Science & Technology, Tsinghua University
 * Copyright (c) 2003-2020. All rights reserved.
 ***********************************************************************/

void bubblesort1A ( int A[], int n )  //冒泡排序算法{
    bool sorted = false; //整体排序标志,首先假定尚未排序
    while ( !sorted ) 
    { //在尚未确认已全局排序之前,逐趟进行扫描交换
        sorted = true; //假定已经排序
        for ( int i = 1; i         { //自左向右逐对检查当前范围A[0, n)内的各相邻元素
            if ( A[i - 1] > A[i] ) 
            { //一旦A[i - 1]与A[i]逆序,则
                swap ( A[i - 1], A[i] ); //交换之,并
                sorted = false; //因整体排序不能保证,需要清除排序标志
            }
        }
        n--; //至此末元素必然就位,故可以缩短待排序序列的有效长度
    }
} //借助布尔型标志位sorted,可及时提前退出,而不致总是蛮力地做n - 1趟扫描交换

    本段代码来源见注释。

6. 指数  

    运行时间能表示为  的算法称为“指数时间复杂度算法”。通常来说,“指数时间复杂度算法”无法应用于实际问题,其甚至不能被称为算法。

int Fibonacci(int n) // 递归版求斐波那契数列第 n 项的算法{
    if (n <= 0)
        return 0;
    if (n == 1 || n == 2)
        return 1;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

    上面这个递归求斐波那契数列第  项的算法具有  的复杂度。显然,随着  的增大,算法执行时间会急剧膨胀。

7da6041744b229923fd0a693e3d490c0.png

    (上图错了,左下角应该是2和1) 现在来康康这几个时间复杂度的图像:

4b868a801f2ae2e4adcb9fdbddbaaa33.png

(横纵坐标均为64)

a4e86a63b8f3ca1ae5e1d7e6e41d1074.png

(横纵坐标均为4096)

    第二张图中,  和  几乎重合,但  最终会大于  。我们将图像横坐标放大:

c0cad98331a80c9c0257fc555262b11e.png

(横坐标36,纵坐标4096)

    放大横坐标之后,我们可以清晰地看到  迅速超过  并以多项式所不能及的速度向上疯狂生长。

    在问题规模不大时,指数复杂度反而低于多项式复杂度,但随着问题规模的扩大,指数复杂度迅速超过了多项式复杂度。鉴于我们只考虑大规模问题的算法效率,因此可以说指数复杂度远大于多项式复杂度。


    好久不见,你好呀~

    正值寒假的美好时光,我换了个笔名重新回到公众号来同看文章的你分享总结有关数据结构、算法的知识。和上次一样,我希望能把自己学到的知识总结成文章,分享出去。正如费曼学习法所提到的,我相信,能把知识以简洁易懂的形式分享出去,让别人看懂,才是真正学懂了。

    希望和你在接下来的文章中一起进步呀~4fc8af31958c57dd260d62f0a6b5acb6.png



[1] Data Structures in C++ 

     ISBN: 7-302-33064-6 & 7-302-33065-3 & 7-302-29652-2 & 7-302-26883-3
     Junhui DENG, deng@tsinghua.edu.cn
     Computer Science & Technology, Tsinghua University

[2] https://blog.csdn.net/zolalad/article/details/11848739


本系列文章主要意在总结笔者在学习过程中学到的有关数据结构、算法的知识,同大家分享我的见解。水平有限,如您在阅读过程中发现错误之处,欢迎在微信公众号中留言。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值