考研数据结构考点——第一章绪论

第一章:绪论

一、基本术语、逻辑结构、存储结构

  • 数据: 能被输入到计算机并被程序识别和处理的符号集合,包括数值、字符及其他能表示信息的符号。

  • 数据元素: 数据元素是数据的基本单位,通常作为一个整体考虑,如一个人的信息记录。

  • 数据项: 数据项是构成数据元素的最小不可分割单位,反映了数据的基本属性。

  • 数据对象: 具有相同性质的数据元素的集合,是数据的子集。

  • 抽象数据类型:

    • 定义: 抽象数据类型是一种高层次的逻辑概念,由数据对象、数据关系和一组操作组成,屏蔽了底层的实现细节。
    • 特点: ADTs专注于数据逻辑特性,而不关心其具体实现方式。
  • 数据结构:

    • 定义: 数据结构是指数据元素之间的关系,可以通过不同方式来存储和操作。

    • 三要素: 包括数据的逻辑结构、存储结构和基本操作。

  • 数据的逻辑结构:

    • 分类:线性结构—一对一、树形结构—一对多、图形结构—多对多、集合—是否属于同一集合。
    • 串是受限线性表:特殊性体现在数据元素是一个字符,即存储内容受限。
    • 栈和队列是受限线性表:操作受限。
    • 广义表:是一种层次结构,不是线性结构。
    • 多维数组:不是线性结构。
  • 存储结构:

    • 分类:顺序存储结构、链式存储结构、索引存储结构、散列(Hash)存储结构。
    • 顺序存储结构:将逻辑相邻的结点存储在物理位置相邻的存储单元中,该方法得到的存储结构上顺序存储结构,更具体一般是由数组表示。
      • 优点:可以实现随机存取(也可以顺序存取)、每个元素占用最少的空间。
      • 缺点:只能使用一整块相邻的存储单元,可能产生较多外部碎片。
    • 链式存储结构:不要求逻辑相邻的结点存储在物理位置相邻的存储单元中,该方法得到的存储结构是链式存储结构,更具体一般是由指针表示。
      • 优点:不会出现碎片现象。
      • 缺点:会因为指针而占用额外的存储空间。只能顺序存取。
      • 存储密度 = 数据本身占用的存储量/整个结构占用的存储量,链式存储方法存储量额外的指针域,所以存储密度不如顺序存储方法高。
    • 索引存储结构:存储结点时,额外存储地址,形式如 <关键字,地址>。
      • 优点:检索速度快。
      • 缺点:增加了附加的索引表,会占用更多空间;增、删数据时要修改索引表,会花费更多的时间。
    • 散列(Hash)存储结构:根据结点的关键字计算出结点的存储地址,形如 location = Hash(key)。
      • 优点:检索、增加、删除结点操作很快。
      • 缺点:散列函数不好的情况下,会出现元素存储单元冲突,解决冲突会增加时空开销。
  • 存取结构:

    • 分类:随机存取、非随机存取(顺序存取)。
    • 随机存取:存取第N个数据时,不需要访问前(N-1)个数据,直接就可以对第N个数据操作。
    • 非随机存取:存取第N个数据时,必须先访问前(N-1)个数据。

二、时空复杂度

  • 算法的定义

    • 算法是解决特定问题的一组有序步骤的描述,通常以一系列明确的指令形式表示,每条指令描述一个或多个操作。算法不仅要明确每一步的操作内容,还要能够在有限步骤内完成目标任务。
    • 简单来说,算法就是指导计算机处理问题的详细过程,它描述了从输入到输出的每一个必要步骤。
  • 算法的五个特性

    • 有穷性:算法必须在有限的步骤内结束,不能是无限执行的,否则不能算作一个有效的算法。

    • 确定性:算法的每一步都有明确的定义,在相同条件下每次执行都应产生相同的结果,不会产生歧义。

    • 可行性:算法的每一步都应该是可执行的,即算法能够在实际环境中实现,所有操作都能通过有限的资源在有限的时间内完成。

    • 输入:算法可以有0个或多个输入,以便算法处理的起点。输入数据决定了算法执行的具体内容。

    • 输出:算法必须至少有一个输出,输出是算法执行的结果。无论算法多么复杂,其目的都是为了得到有效的结果。

  • 算法的评价标准

    • 正确性:算法必须能够在所有合法输入下生成正确的输出,正确性是评价算法的首要标准。

    • 可读性:一个良好的算法应具备良好的结构,代码应清晰明了,便于他人理解和维护。

    • 健壮性:即使面对非法输入或异常情况,算法也应能合理处理或优雅地终止,而不导致程序崩溃。

    • 高效率与低存储量:算法的效率体现在时间复杂度上,指算法在解决问题时需要的时间;低存储量指的是算法执行过程中占用的空间。理想的算法应在时间和空间上达到平衡。

  • 算法效率的度量方法

    • 事后统计方法:通过实际运行算法,记录算法的执行时间和占用的存储空间。这种方法可以得到精确的结果,但只能应用于已实现的算法,且会受到硬件和环境的影响。

    • 事前分析估算方法:在算法实现之前,通过理论分析算法的执行次数和空间使用量,估算出时间复杂度和空间复杂度。这种方法的优势在于它可以在算法设计阶段评估其效率,不依赖具体的硬件环境。

  • 算法复杂度计算

    • 关注点

      • 在计算算法复杂度时,我们主要关注的是算法随着输入规模 (n) 增长时,执行次数或消耗的资源(时间或空间)的增长速度。我们只关心算法的量级,也就是当输入规模变大时,哪个因素对算法的执行时间或占用空间影响最大。

      • 复杂度的计算是为了衡量算法的性能表现,尤其是在处理大规模数据时,计算复杂度能够帮助我们预估算法的执行效率。

    • 算法时间复杂度标记

      • 时间复杂度的常见表示是 $ T(n) = O(f(n))$ ,其中 $ f(n) $ 表示随问题规模 n n n 增长,算法所有语句执行的总次数(即频度的总和)。

      • 为简化表示,我们通常只保留影响最大的项,也就是增长速度最快的那一项,去掉其他增长较慢的项和所有常数系数。这种表示法使我们可以直观地了解算法在处理大规模数据时的执行效率。

      • 例如,如果一个算法执行次数为 3 n 2 + 5 n + 2 3n^2 + 5n + 2 3n2+5n+2 ,我们保留最高次项 n 2 n^2 n2 ,忽略系数和低次项,最终时间复杂度为 O ( n 2 ) O(n^2) O(n2)

    • 常见的时间复杂度大小关系

      • 常见的时间复杂度按从小到大的顺序排列如下:

        O ( 1 ) ≤ O ( log ⁡ n ) ≤ O ( n ) ≤ O ( n log ⁡ n ) ≤ O ( n 2 ) ≤ O ( n 3 ) ≤ O ( 2 n ) ≤ O ( n ! ) ≤ O ( n n ) O(1) \leq O(\log n) \leq O(n) \leq O(n \log n) \leq O(n^2) \leq O(n^3) \leq O(2^n) \leq O(n!) \leq O(n^n) O(1)O(logn)O(n)O(nlogn)O(n2)O(n3)O(2n)O(n!)O(nn)

        O ( 1 ) O(1) O(1):常数时间复杂度,算法的执行时间不依赖于输入规模。例如,访问数组中的某个元素。

        O ( log ⁡ n ) O(\log n) O(logn):对数时间复杂度,通常出现在通过分治策略缩小问题规模的算法中,例如二分查找。

        O ( n ) O(n) O(n):线性时间复杂度,算法的执行时间随着输入规模成线性增长。例如,遍历数组中的每个元素。

        O ( n log ⁡ n ) O(n \log n) O(nlogn):常见于高效的排序算法,如归并排序和快速排序。

        O ( n 2 ) O(n^2) O(n2):平方时间复杂度,通常出现在嵌套循环的算法中,例如冒泡排序。

        O ( n 3 ) O(n^3) O(n3):立方时间复杂度,较为复杂的多重嵌套循环或矩阵运算算法中出现。

        O ( 2 n ) O(2^n) O(2n):指数时间复杂度,常见于穷举搜索或递归的算法中,如解决“汉诺塔问题”。

        O ( n ! ) O(n!) O(n!):阶乘时间复杂度,通常出现在排列问题或组合问题中,如解决“旅行商问题”。

        O ( n n ) O(n^n) O(nn):极高的复杂度,通常出现在组合爆炸问题中。

    • 影响时间复杂度的因素

      • 问题规模 n n n:问题规模是影响算法时间复杂度的最关键因素。随着输入规模 n n n 增大,算法执行时间也会随之变化。不同算法对输入规模的敏感程度不同,因此不同的复杂度级别会表现出显著差异。

      • 输入数据的性质

        • 初始状态:一些算法的性能可能会受到输入数据初始状态的影响。例如,排序算法在处理已排序数据时,性能可能会显著提升(如插入排序)。
        • 输入数据的分布:某些算法依赖输入数据的某些特征,数据的分布可能会影响算法的执行效率。例如,快速排序在数据分布不均时,最坏情况可能会退化为 O ( n 2 ) O(n^2) O(n2)
  • **空间复杂度:**指算法在执行过程中所需的存储空间。它不仅包括输入数据本身占用的空间,还包括算法在运行过程中动态申请的额外存储空间。空间复杂度的计算与时间复杂度类似,也用大O符号来表示。

    • 使用malloc申请空间的函数:当使用 malloc 或其他动态内存分配函数申请空间时,申请的规模 g ( n ) g(n) g(n) 对应的空间复杂度就是 O ( g ( n ) ) O(g(n)) O(g(n)),这里的 g ( n ) g(n) g(n)malloc 的参数,表示申请的空间大小。例如,如果申请大小为 n n n 的数组,空间复杂度为 O ( n ) O(n) O(n)

    • 使用递归调用的算法:递归算法的空间复杂度不仅要考虑函数调用本身占用的栈空间,还需要考虑递归调用的深度和每次递归调用额外申请的空间。

      • 递归空间复杂度计算公式为: O ( c ( n ) × s ( n ) ) O(c(n) \times s(n)) O(c(n)×s(n)),其中:
        • c ( n ) c(n) c(n) 表示递归调用的深度,通常与问题规模 n n n 有关。
        • s ( n ) s(n) s(n) 表示每次递归调用所消耗的空间大小。
      • 例如,对于一个简单的递归函数,递归深度为 n n n,每次递归调用只消耗常量空间,则空间复杂度为 O ( n ) O(n) O(n)
    • 原地工作::原地工作的算法指的是不需要额外申请与输入规模成比例的存储空间,所需的辅助空间为常量,即 O ( 1 ) O(1) O(1)。这并不是说算法不占用任何空间,而是它的额外空间开销是常数,和输入规模无关。例如,快速排序在不使用额外数组的情况下被称为原地排序算法,其空间复杂度为 O ( log ⁡ 2 n ) O(\log_{2}n) O(log2n),因为递归调用栈的深度为 log ⁡ 2 n \log_{2} n log2n

    • 其他情况:在算法执行过程中,若新建了数组或其他数据结构(如栈或队列),需要考虑这些结构所占用的空间。比如:

      • 归并排序中,需要额外的数组来合并两个子数组,因此其空间复杂度为 O ( n ) O(n) O(n),因为需要一个大小为 n n n 的辅助数组。
      • 在某些算法中,如广度优先遍历,可能会动态申请栈或队列,这些结构会影响算法的空间复杂度,通常与问题规模相关。
  • 归纳法:

    在算法复杂度分析中,归纳法是分析递归算法的一种常用方法,它通过递归的逻辑结构来推导时间复杂度和空间复杂度。

    • 递归类算法: 在递归算法中,通过递归公式(即 T ( n ) T(n) T(n) 公式)来表达问题规模 n n n 的变化,从而推导出时间复杂度或空间复杂度。递归复杂度的计算可以表示为 递归深度 乘以 单次调用的复杂度,即: T ( n ) = 递归深度 × 单次调用的复杂度 T(n) = \text{递归深度} \times \text{单次调用的复杂度} T(n)=递归深度×单次调用的复杂度

    • **递归式的书写:**递归算法的时间复杂度和空间复杂度都可以通过递归式来表达。递归式通常包含两个部分:

      • 结束条件:表示递归终止时的操作次数。例如,问题规模为1时的执行次数。

      • 递归调用的复杂度:包括递归调用时问题规模的变化和每次调用的操作次数。

      递归式的一般形式为: T ( n ) = 结束条件时的执行次数 + 递归调用的规模变化 + 本次调用的复杂度 T(n) = \text{结束条件时的执行次数} + \text{递归调用的规模变化} + \text{本次调用的复杂度} T(n)=结束条件时的执行次数+递归调用的规模变化+本次调用的复杂度

      其中,本次调用复杂度 表示当前递归调用中不涉及递归部分的操作,例如合并、比较或其他操作。计算本次调用复杂度时,我们需要忽略递归调用部分,专注于剩余部分的时间消耗。

    • 非递归类算法:对于非递归算法,我们通过控制变量来进行复杂度分析。非递归算法往往通过循环、计数等方式控制算法的执行次数。

      • 确定变量的变化范围:分析变量如何变化,例如循环的次数。

      • 计数每个操作的执行次数:计算每个步骤中执行操作的次数,进而确定总的执行次数。

      • 求总和:通过计算所有操作的执行次数总和,得到算法的复杂度。

    • 通用公式: T ( n ) = a ⋅ T ( n b ) + O ( n d ) T(n) = a \cdot T\left(\frac{n}{b}\right) + O(n^d) T(n)=aT(bn)+O(nd)

      其中:

      a a a:每次递归生成的子问题数。

      b b b:每次递归缩小的问题规模,即原问题缩小到 1 / b 1/b 1/b

      O ( n d ) O(n^d) O(nd):表示分解和合并子问题所需的时间复杂度。

      • 情况一: log ⁡ b ( a ) > d \log_b(a) > d logb(a)>d:复杂度为: O ( n log ⁡ b ( a ) ) O(n^{\log_b(a)}) O(nlogb(a))

        递归生成的子问题数量快速增长,因此递归调用次数主导整个算法复杂度。

      • 情况二: l o g b ( a ) = d log_b(a) = d logb(a)=d:复杂度为: O ( n d ⋅ log ⁡ n ) O(n^d \cdot \log n) O(ndlogn)

        递归生成的子问题数量与每次操作的复杂度平衡,因此最终复杂度是 n d log ⁡ n n^d \log n ndlogn

      • 情况三: l o g b ( a ) < d log_b(a) < d logb(a)<d:复杂度为: O ( n d ) O(n^d) O(nd)

        分解问题的时间复杂度主导整个算法,递归调用的次数相对较少。

  • t t t

    t t t是一种用于推导算法复杂度的方法,尤其适用于涉及复杂循环条件的算法。它的核心思路是通过假设算法在执行了 t t t 次后结束,然后将循环次数 t t t 与问题规模 n n n 建立数学关系,最终将 t t t n n n 表示,从而推导出时间复杂度。

    • 方法步骤:

      • 假设循环执行 t t t 次后结束:从循环条件入手,设循环在执行了 t t t 次后结束,找出循环条件中的变量在每次循环后的变化规律。

      • 建立 t t t n n n 之间的关系:分析问题规模 n n n 与执行的次数 t t t 的变化关系,建立一个方程来表示这两者之间的联系。

      • 解出 t t t 并用 n n n 表示:通过解方程将 t t t 表示为 n n n 的函数,得到复杂度。

    • 适用场景:

      t t t适合那些循环条件中变量的变化较为复杂的情况。这些变化往往不是简单的线性变化(如每次加1或减1),而是指数变化、倍数变化或其他更复杂的情况。此时,传统的计数法难以直接得出循环执行次数,通过设 t t t 法可以有效分析。

    示例:复杂变量变化的情况:

    假设我们有一个算法,每次循环时,变量 n n n 的变化为 n = n / 3 n = n / 3 n=n/3,我们可以使用设 t t t 法分析它的复杂度。

    算法描述

    while (n > 1) {
        n = n / 3;
        // 其他操作
    }
    

    步骤

    1. 设 ( t ) 次结束:假设经过 ( t ) 次循环,( n ) 从原始规模变为1。

    2. 建立关系:每次循环,( n ) 除以3,因此可以建立如下方程:

      n 3 t = 1 \frac{n}{3^t} = 1 3tn=1

    3. 解出 ( t ):通过方程解出 ( t ):

      t = log ⁡ 3 ( n ) t = \log_3(n) t=log3(n)

    4. 时间复杂度:因此,该算法的时间复杂度为 O ( log ⁡ 3 ( n ) ) O(\log_3(n)) O(log3(n))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

全栈ing小甘

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

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

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

打赏作者

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

抵扣说明:

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

余额充值