数据结构绪论

1.1 数据结构的基本概念

数据、数据元素、数据项、数据对象

  • 数据:信息的载体,能被计算机识别、存储和处理的符号总称。例如整数、字符、声音、图像等。
  • 数据元素:数据的基本单位,在程序中作为一个整体处理。例如学生表中的一条学生记录是一个数据元素。
  • 数据项:数据元素由若干数据项组成,数据项是数据的最小单位。例如学生记录中的学号、姓名、性别等。
  • 数据对象:性质相同的数据元素的集合,是数据的一个子集。例如所有学生的集合是一个数据对象。

举例:在"学生信息管理系统"中,全体学生的信息是数据,每个学生的记录是一个数据元素,学号、姓名、年龄是数据项,所有学生的集合是数据对象。

数据结构

数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。简单来说,数据结构 = 数据 + 结构,研究的是如何组织和存储数据。

1.2 数据结构三要素

数据结构包含三个要素:逻辑结构、存储结构、数据运算。

逻辑结构

逻辑结构是指数据元素之间的逻辑关系,与计算机的存储无关。分为四类:

  1. 集合结构:数据元素之间没有明确的关系,仅属于同一个集合。
  2. 线性结构:数据元素之间存在一对一的关系,如线性表、栈、队列。
  3. 树形结构:数据元素之间存在一对多的关系,如树、二叉树。
  4. 图形结构:数据元素之间存在多对多的关系,如图。

用数学语言描述:逻辑结构可以用二元组 (D, R) 表示,其中 D 是数据元素的集合,R 是 D 上的关系集合。

存储结构(物理结构)

存储结构是指数据在计算机内存中的存储方式。主要有四种:

存储方式 描述 优点 缺点
顺序存储 用连续的存储单元依次存储 随机存取方便 插入删除需移动大量元素
链式存储 用任意存储单元存储,通过指针关联 插入删除方便 存储密度低,需额外空间存指针
索引存储 建立索引表,索引项指向数据存放位置 查找速度快 需额外空间维护索引表
散列存储 根据关键字通过散列函数计算存储位置 查找速度极快 可能产生冲突,不易处理范围查询

数据运算

数据运算即对数据施加的操作,包括:

  • 基本操作:创建、销毁、查找、插入、删除、修改等。
  • 运算的定义:在逻辑结构上定义,指明功能。
  • 运算的实现:在存储结构上实现,指明具体步骤。

📌 408考点提示:常以选择题考查数据结构三要素的区别,以及逻辑结构与存储结构的对应关系。例如:给出一种结构问属于哪种逻辑结构,或给出存储方式问属于哪种存储结构。

1.3 数据类型与抽象数据类型

数据类型

数据类型是一个值的集合和定义在这个值集上的一组操作的总称。例如 C 语言中的 int 类型,包括整数集合和加减乘除等操作。

抽象数据类型(ADT)

ADT 是一个数据模型以及定义在该模型上的一组操作。ADT 的定义只关心"做什么",不关心"怎么做",实现了封装和信息隐藏。

ADT 的定义格式:

ADT 抽象数据类型名 {
  数据对象:数据对象的定义
  数据关系:数据关系的定义
  基本操作:操作的定义
} ADT 抽象数据类型名

现实类比:把 ADT 想象成"自动售货机"。你只关心投币、选商品、取货(操作),不需要知道售货机内部如何识别硬币、如何传送商品(实现细节)。

1.4 算法与算法评价

算法的定义

算法是对特定问题求解步骤的一种描述,是指令的有限序列。算法必须满足五个特性:

特性 说明
有穷性 算法必须在有限步后结束
确定性 每条指令无歧义,相同输入产生相同输出
可行性 每条指令都可以通过已有操作实现
输入 有零个或多个输入
输出 有一个或多个输出

算法的评判标准

  1. 正确性:算法能正确解决问题。
  2. 可读性:算法易于理解。
  3. 健壮性:能处理异常输入。
  4. 效率与低存储量需求:时间效率高、空间占用少。

💡 记忆技巧:五个特性记作"有确可输出",其中"有"=有穷性、"确"=确定性、"可"=可行性、"输"=输入、"出"=输出。

1.5 时间复杂度分析

大O表示法

时间复杂度是算法中基本操作执行次数的数量级,用大O表示法描述。T(n) = O(f(n)) 表示当 n 趋于无穷大时,T(n) 的上界是 f(n) 的常数倍。

常见的时间复杂度(按增长速度升序排列):

复杂度 名称 示例
O(1) 常数阶 数组随机访问
O(log n) 对数阶 二分查找
O(n) 线性阶 顺序查找
O(n log n) 线性对数阶 快速排序、归并排序
O(n²) 平方阶 冒泡排序、选择排序
O(n³) 立方阶 矩阵乘法(朴素)
O(2ⁿ) 指数阶 斐波那契数列递归
O(n!) 阶乘阶 旅行商问题(暴力法)

时间复杂度分析方法

  1. 找基本操作:找到最内层的循环操作。
  2. 分析执行次数:计算基本操作的执行次数。
  3. 取数量级:忽略低阶项和常数系数,取最高阶。

示例1:单层循环

for (i = 0; i < n; i++) {
    a[i] = 0;  // 基本操作
}

基本操作执行 n 次,T(n) = O(n)。

示例2:双层循环

for (i = 0; i < n; i++) {
    for (j = 0; j < n; j++) {
        a[i][j] = 0;  // 基本操作
    }
}

基本操作执行 n×n = n² 次,T(n) = O(n²)。

示例3:对数阶

for (i = 1; i < n; i = i * 2) {
    // 基本操作
}

设执行 k 次后条件不满足,则 2^k ≥ n,k = log₂n,T(n) = O(log n)。

示例4:递归函数的时间复杂度

int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}

T(n) = T(n-1) + T(n-2) + O(1),解为 T(n) = O(2ⁿ)。

三种复杂度分析

  • 最坏时间复杂度:算法在最坏情况下的时间复杂度(常用)。
  • 平均时间复杂度:所有可能输入等概率出现时的时间复杂度。
  • 最好时间复杂度:算法在最理想情况下的时间复杂度(极少使用)。

1.6 空间复杂度分析

空间复杂度 S(n) 表示算法运行时所需的存储空间数量级。

要点:

  • 包括输入数据占用空间、程序代码占用空间、辅助变量占用空间。
  • 通常只考虑额外空间(除输入数据外的空间)。
  • 原地工作(in-place)算法的空间复杂度为 O(1)。

示例1:常量空间

int sum(int a[], int n) {
    int s = 0;  // 辅助变量
    for (int i = 0; i < n; i++) s += a[i];
    return s;
}

辅助空间为常数个变量,S(n) = O(1)。

示例2:线性空间

int* copy(int a[], int n) {
    int *b = (int*)malloc(n * sizeof(int));
    for (int i = 0; i < n; i++) b[i] = a[i];
    return b;
}

辅助数组 b 占用 n 个 int 空间,S(n) = O(n)。

示例3:递归的空间复杂度

int fact(int n) {
    if (n <= 1) return 1;
    return n * fact(n-1);
}

递归深度为 n,每层需一个栈帧,S(n) = O(n)。

复杂度的对比与理解

为了正确分析算法复杂度,需要记住以下几个原则:

  • 常数系数不影响数量级:O(2n) = O(n),O(100n²) = O(n²)
  • 低阶项可以忽略:O(n² + n) = O(n²)
  • 嵌套循环:内外层循环次数相乘
  • 并列循环:各段复杂度取最大值
  • 递归算法:需要建立递推关系式求解

常见递推关系式的求解:

  • T(n) = T(n-1) + O(1) → O(n)
  • T(n) = T(n-1) + O(n) → O(n²)
  • T(n) = 2T(n/2) + O(n) → O(n log n)(归并排序)
  • T(n) = T(n/2) + O(1) → O(log n)(二分查找)
  • T(n) = T(n-1) + T(n-2) + O(1) → O(2ⁿ)

常用复杂度大小比较

O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(n³) < O(2ⁿ) < O(n!)

在 n 较大时,不同复杂度的差距非常显著。例如 n=10⁶ 时:

  • O(log n) ≈ 20 次操作
  • O(n) ≈ 10⁶ 次操作
  • O(n²) ≈ 10¹² 次操作(不可接受)

📌 408考点提示:时间复杂度是必考内容,常见题型包括:

  • 分析循环嵌套的时间复杂度(注意循环变量的变化规律)
  • 分析递归函数的时间复杂度(常见递推式:T(n) = T(n-1) + O(n) → O(n²))
  • 比较不同算法的时间复杂度
  • 给代码段选择正确的时间复杂度

⚠️ 易错点:

  • 不要遗漏循环条件中的变量变化,如 i = i * 2 是 O(log n),不是 O(n)。
  • 多层循环嵌套并不一定就是 O(n²),如内层循环次数变化的情况。
  • 递归算法的空间复杂度要考虑递归调用栈的深度。

1.7 题型示例

例题1:分析以下代码的时间复杂度。

int i = 1, k = 0;
while (i < n) {
    k++;
    i = i + 2;
}

解:每次循环 i 增加 2,设循环执行 m 次后 i ≥ n,则 1 + 2m ≥ n,m ≥ (n-1)/2。所以 T(n) = O(n)。

例题2:分析以下代码的时间复杂度。

for (i = 0; i < n; i++) {
    for (j = i; j < n; j++) {
        a[i][j] = 0;
    }
}

解:i=0 时内层执行 n 次,i=1 时执行 n-1 次,...,i=n-1 时执行 1 次。 总次数 = n + (n-1) + ... + 1 = n(n+1)/2,T(n) = O(n²)。

例题3:分析以下代码的时间复杂度。

int func(int n) {
    if (n <= 1) return 1;
    return func(n-1) + func(n-2);
}

解:设 T(n) 为 func(n) 的执行时间,则 T(n) = T(n-1) + T(n-2) + O(1)。 递归展开类似于斐波那契数列,是指数增长。T(n) = O(2ⁿ)。

例题4:分析以下代码的时间复杂度。

int i = 1;
while (i <= n) {
    for (int j = 1; j <= i; j++) {
        // 基本操作
    }
    i = i * 2;
}

解:外层循环 i 以指数增长(1, 2, 4, 8, ...),执行 k 次后 2^(k-1) ≤ n,k = log₂n + 1。 内层循环次数随 i 变化:i=1 时 1 次,i=2 时 2 次,i=4 时 4 次,... 总次数 = 1 + 2 + 4 + ... + 2^(k-1) = 2^k - 1 ≤ 2n - 1 T(n) = O(n)。

复杂度的实际意义

理解复杂度有助于在实际编程中选择合适的数据结构和算法。例如:

  • 当 n = 1000 时,O(n) 需要 1000 次操作,而 O(n²) 需要 1,000,000 次操作——差距悬殊。
  • 这就是为什么大规模数据处理中,O(n log n) 的排序算法(快排、归并)优于 O(n²) 的排序算法(冒泡、选择)。
  • 指数级 O(2ⁿ) 算法在 n > 30 时几乎不可用,需要寻找多项式时间的近似算法。

题型归纳

408 中复杂度分析题有几种常见的套路模式:

模式1:线性搜索型——单层循环,循环变量步长为 1

for (i = 0; i < n; i++) { ... }

T(n) = O(n)

模式2:搜索范围减半型——循环变量每次翻倍或减半

for (i = 1; i < n; i = i * 2) { ... }
for (i = n; i > 0; i = i / 2) { ... }

T(n) = O(log n)

模式3:矩形遍历型——双层循环,内外层都到 n T(n) = O(n²)

模式4:三角遍历型——双层循环,内层起始随外层变化

for (i = 0; i < n; i++)
    for (j = i; j < n; j++) { ... }

T(n) = O(n²)

模式5:分治递归型——T(n) = aT(n/b) + f(n),套用主定理

主定理(Master Theorem):T(n) = aT(n/b) + f(n),其中 a ≥ 1,b > 1:

  • 若 f(n) = O(n^(log_b(a)-ε)),则 T(n) = Θ(n^log_b(a))
  • 若 f(n) = Θ(n^log_b(a)),则 T(n) = Θ(n^log_b(a) × log n)
  • 若 f(n) = Ω(n^(log_b(a)+ε)),且 af(n/b) ≤ cf(n),则 T(n) = Θ(f(n))

例题5:分析以下代码的时间复杂度。

for (i = 0; i < n; i++) {
    for (j = 0; j < n; j++) {
        for (k = 0; k < n; k++) {
            c[i][j] += a[i][k] * b[k][j];
        }
    }
}

解:三重循环嵌套,每层执行 n 次。总次数 = n × n × n = n³。T(n) = O(n³)。

本章总结

数据结构的三要素是贯穿全书的线索,时间/空间复杂度分析是所有算法学习的基础。408考试中,复杂度分析几乎每年都会出现,务必熟练掌握。理解"逻辑结构决定功能,存储结构决定效率"这一核心思想,对后续学习至关重要。