考研学习
解题方法数据结构

数据结构第 1 章:绪论

408 数据结构「绪论」复习:核心知识点、真题题型、通用解法与易错点。

Last updated on

本章依据本站 408 真题 → 数据结构 → 第 1 章 绪论,以及 408 模拟选择题 → 数据结构 → 第 1 章 绪论 整理。本站本章真题一共整理了 2011、2012、2014、2017、2019、2022、2023 七道,其中前六道都是复杂度,第七道考顺序存储的操作复杂度;模拟题则进一步补充了算法特性、递归空间复杂度、双指针摊还分析、二叉树递归等变式。(Any Docs)

内容按照 408 实际命题方式 组织,而不是机械罗列教材概念。

一、本章在 408 中到底重要在哪里?

从本站现有真题看,本章的命题重心非常鲜明:

考法真题情况重要程度
程序时间复杂度2011、2012、2014、2017、2019、2022★★★★★
顺序存储操作复杂度2023★★★
空间复杂度真题页暂未单独出现,模拟题多次覆盖★★★★
算法基本特征模拟题出现★★
数据结构基本概念、逻辑结构/存储结构本页真题暂未直接出现★★

所以这一章真正要形成的能力不是“背诵大 O 定义”,而是:

看到任意一小段程序,能够迅速找到决定执行次数的变量,再把执行次数转化成关于问题规模的数量级。

很多学生后面学树、图、查找、排序时复杂度总出错,其实根源都在这一章。


二、必须掌握的基础概念

1. 什么叫数据结构?

可以把“数据结构”理解成两件事的组合:

数据之间有什么关系 + 数据在计算机里怎么放。

例如学生信息按照学号排成一个序列:

逻辑上,它们是“一前一后”的线性关系。

至于实际存储,可以连续放:

A B C D E

也可以通过指针连起来:

A → B → C → D → E

因此要区分:

层面问什么
逻辑结构数据元素之间是什么关系?
存储结构这种关系在计算机内存中如何实现?

典型逻辑结构包括集合结构、线性结构、树形结构和图状结构。

最容易混淆的一点是:

逻辑结构相同,不代表存储结构相同。

同一个线性表既可以采用顺序存储,也可以采用链式存储。


三、题型一:算法基本概念判断

这是本章最简单的一类题,考到基本不能丢。

网站模拟题:

王道·卷一 Q01

题意:下面哪一个不是算法的主要特征?

A. 有穷性 B. 可行性 C. 确定性 D. 可读性

答案:D。(Any Docs)

算法通常强调:

有穷性、确定性、可行性、输入和输出。

“可读性”当然是好程序的重要性质,但算法不是因为“不可读”就不是算法。

这里有一个特别容易出的陷阱:

程序可以无限执行,但算法必须有穷。

例如一个操作系统:

while (1) {
    process();
}

程序当然允许一直运行。

但如果若声称它是“求数组最大值的算法”,却永远不结束,那它就不是一个正确的求最大值算法。


四、时间复杂度的真正含义

设某算法的基本操作执行次数为:

其中问题规模为:

时间复杂度并不是计算“程序运行了多少秒”,而是研究:

时,执行次数增长得有多快。

例如:

真正决定增长速度的是:

因此写为:

408 选择题通常写成:

虽然严格数学意义上大 O 是上界,但考试中“时间复杂度为 O(...)”一般是在寻找最紧的常见数量级。


五、复杂度比较必须非常熟

常见复杂度从小到大:

其中最需要建立直觉的是:

通常来自“每轮乘一个常数或者除一个常数”。

而:

通常来自:

或:


六、题型二:变量每次乘常数——对数复杂度

这是第一章最高频模板之一。

真题:2011 年 408

网站题目可以概括为:

x = 2;

while (x < n / 2)
    x = 2 * x;

问时间复杂度。本站答案为:

(Any Docs)

怎么分析?

不要盯着 while 猜。

应该问:

执行 k 次以后 x 变成多少?

初始:

一次以后:

两次:

因此 k 次后:

循环大约在:

时停止。

两边取对数:

所以:


模拟题:王道·卷三 Q01

题目变为:

i = 2;

while (i < n / 3)
    i = i * 3;

网站答案仍然是:

(Any Docs)

为什么底数从 2 变成 3,复杂度没变?

因为:

而:

只是一个常数。

所以复杂度分析中:


模拟题:王道·卷四 Q01

题意:

i = n * n;

while (i != 1)
    i = i / 2;

每一次缩小一半。

执行 k 次以后数量级为:

停止条件大致是:

所以:

于是:

因此:

(Any Docs)

一句话模板

凡看到:

i *= c;

或者:

i /= c;

且:

第一反应就是:


七、题型三:普通递归——看递归深度和每层工作量

2012 年 408 真题

网站题目是递归求阶乘:

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

    return n * fact(n - 1);
}

问时间复杂度。

本站答案:

(Any Docs)

为什么不是:

也不是指数级?

递归调用过程:

fact(n)
fact(n-1)
fact(n-2)
...
fact(1)

一共有大约:

层。

可以写递推式:

展开:

继续:

所以:


这道题更重要的隐藏考点:空间复杂度

虽然真题问的是时间复杂度,但必须同时知道:

原因不是总共进行了多少次递归,而是:

某一时刻同时存在多少层栈帧。

递归调用过程中:

fact(n)
 ↓
fact(n-1)
 ↓
fact(n-2)
 ↓
...
 ↓
fact(1)

最深时同时存在约 n 层。

因此递归空间:

这个思想会直接用于后面的树和图。


八、题型四:两层循环不能看到两层就写平方

这是 408 极其爱挖的坑。

2014 年真题

网站题目:

count = 0;

for (k = 1; k <= n; k *= 2)
    for (j = 1; j <= n; j++)
        count++;

本站答案:

(Any Docs)

外层:

1,2,4,8,...

所以执行:

次。

而每一次外循环中,内层固定执行:

次。

因此总次数:

也就是:


什么时候可以直接相乘?

只有类似这种真正“独立”的循环:

for (...) {
    for (...) {
        ...
    }
}

外层每执行一次,内层都执行同一个数量级。

才可以大致:

但如果内层次数随外层变量变化,就必须求和。

这是本章最重要的原则之一。


九、题型五:累计量达到 n——建立方程

2017 年 408 真题

网站题目:

int func(int n) {
    int i = 0, sum = 0;

    while (sum < n)
        sum += ++i;

    return i;
}

答案:

(Any Docs)

第一次循环:

第二次:

第三次:

执行 i 次:

利用等差数列:

停止时约满足:

看最高次项:

所以:

因此:


以后看到这种题怎么办?

例如:

s = 0;

for (i = 1; s < n; i++)
    s += i;

不要去模拟。

直接建立:

然后:

得到:


十、题型六:循环变量满足平方条件——直接解不等式

2019 年真题

题目:

x = 0;

while (n >= (x + 1) * (x + 1))
    x = x + 1;

网站答案:

(Any Docs)

循环能够继续的条件:

因此:

变量每次只增加 1,所以循环次数:


2017 和 2019 本质其实是一道题

2017:

推出:

2019:

直接出现。

最终都得到:

所以不要把真题记成“两道孤立的题”。

应该记成统一模型:

当循环终止条件最终转化成“循环次数的平方约等于 n”,复杂度就是平方根级。


十一、题型七:内层执行次数随外层变化——必须求和

这是 2022 真题最值得掌握的技巧。

2022 年 408 真题

题目:

int sum = 0;

for (int i = 1; i < n; i *= 2)
    for (int j = 0; j < i; j++)
        sum++;

网站答案:

而不是:

(Any Docs)

很多人会说:

外层:

内层最大:

所以:

这是错误的。

因为不同轮的内层执行次数不同。

i 的取值:

1
2
4
8
...

总次数应该是:

这是等比数列。

若最后一项数量级为 n,则:

因此:


十二、这是复杂度题最重要的升级思想:求和而不是机械相乘

遇到:

for (...) {
    for (...) {
    }
}

应问:

第 k 次外层循环,内层究竟执行几次?

于是总复杂度其实是:

2014 真题对应:

一共:

项,所以:

而 2022 真题对应:

所以:

两道题摆在一起学,价值非常高。


十三、模拟强化:不是所有双重循环都是平方复杂度

王道·卷一 Q02

网站模拟题大意:

for (i = 1; i <= n; i++) {
    for (j = 2 * i; j <= n; j++) {
        y += i * j;
    }
}

答案:

(Any Docs)

注意内层只有:

的时候才运行。

也就是:

固定 i 时,内层次数约为:

总次数:

展开后,本质仍然是平方数量级:

这道题训练的是:

内外层相关时,先写出“第 i 轮执行多少次”,再求和。


十四、题型八:双指针——两个循环也可以是线性复杂度

本站里的 竟成·模拟一-01 非常值得做。

题目使用左右指针,大致逻辑为:

int cnt = 0, left = 0;

for (int right = 0; right < n; right++) {
    if (nums[right] == 1)
        cnt++;

    while (left <= right && cnt >= k) {
        if (nums[left] == 1)
            cnt--;

        left++;
    }
}

问时间复杂度。

答案:

(Any Docs)

表面上是:

for
    while

但绝对不能直接写:

因为 right 从左到右最多移动:

次。

而 left 从头到尾也只会向右走,不会回退,所以最多:

次。

总移动次数至多数量级:

因此:

这就是摊还分析的基本思想。

以后看到:

“双指针都只单调移动、不回退”

第一反应应该是:

而不是:

这也是后面字符串、链表、数组算法中非常实用的规律。


十五、题型九:空间复杂度——千万不要数总调用次数

网站模拟题在这一点上补得很好。

竟成·模拟二-01:斐波那契

题目比较:

int f1(int n) {
    if (n == 1) return 1;
    if (n == 0) return 0;
    return f1(n - 1) + f1(n - 2);
}

和使用长度约为 n 的数组进行动态规划的 f2。

问两者空间复杂度。

网站答案:

(Any Docs)

最容易犯的错误:

因为递归树的结点数量大约指数增长,就认为空间复杂度是:

错。

例如:

                 f(n)
              /        \
          f(n-1)       f(n-2)
          / ...

左右子树不是全部同时存在于调用栈中。

真正决定递归空间的是:

最长的一条递归调用链。

最长链:

f(n)
f(n-1)
f(n-2)
...
f(1)

所以最大深度:

空间:

虽然它的时间复杂度确实是指数级。

因此一定分清:


十六、题型十:二分递归——时间可能是 n,空间却是 log n

竟成·模拟七-01

网站题目:

int fun(int *arr, int m, int n) {
    if (m == n)
        return 1;

    int mid = (m + n) / 2;

    return fun(arr, m, mid)
         + fun(arr, mid + 1, n);
}

问空间复杂度。

答案:

(Any Docs)

每层区间长度减半:

若递归深度为 k:

所以:

得到:

因此空间:


但它的时间复杂度是多少?

这个地方特别适合拓展。

每个区间最终都要处理。

递推式:

递归树:

1 个结点
2 个结点
4 个结点
8 个结点
...

总节点数量:

所以:

最终:

这是408很喜欢的组合。


十七、模拟强化:树递归的复杂度

本站 王道·卷二 Q01 给出了这样一个函数:

int maxFunc(TreeNode* root) {
    if (root == NULL)
        return 0;

    return max(
        maxFunc(root->left),
        maxFunc(root->right)
    ) + 1;
}

设二叉树有 n 个结点,问时间复杂度。

答案:

(Any Docs)

非常多人会想:

“二叉树高度是 log n,所以复杂度是 log n。”

错。

树高只说明最长递归链。

但这个函数为了求高度,需要访问所有结点。

如果左右子树结点数分别为:

则:

因为:

所以每个结点仅处理一次:

这里再次体现:

递归深度决定空间;访问节点总数更常决定时间。


十八、题型十一:顺序存储为什么能 O(1) 随机访问?

2023 年 408 真题

网站题意:

长度为 n 的有序顺序表,下面哪个操作平均时间复杂度可以达到:

A. 按指定值查找元素 B. 插入指定值元素 C. 删除第 i 个元素 D. 获取第 i 个元素

答案:D。(Any Docs)

这是第一章和第二章“线性表”的连接题。


为什么第 i 个元素可以直接得到?

顺序表连续存储。

假设首元素地址:

每个元素占 L 个字节。

那么:

只需要一次乘法和一次加法即可定位。

无论:

还是:

都是固定次数计算。

因此:

这就是所谓的:

随机访问 Random Access。


十九、为什么另外三个不是 O(1)?

按值查找

因为题目说明“有序”,所以可以折半查找:

但仍不是:

插入

即使利用二分查找快速找到插入位置,后面的元素仍然可能需要整体后移:

1 3 5 7 9

插入 4

1 3 _ 5 7 9

移动元素平均为线性数量级:

所以整个插入仍为:

这是408非常容易出的陷阱:

查找插入位置快,不代表插入本身快。

删除第 i 个元素

删除后后续元素需要前移:

1 2 3 4 5
      ↑删除

1 2 4 5

平均:


二十、一个非常重要的表:顺序表常见复杂度

操作时间复杂度
已知下标访问元素O(1)
已知下标修改元素O(1)
无序表按值查找O(n)
有序表折半查找O(log n)
插入O(n)
删除O(n)

这个表以后学第二章仍然会反复出现。


二十一、一个容易出错的特殊模拟题:i 每次平方

网站 竟成·模拟五-01:

int sum = 0;

for (int i = 2; i <= n; i *= i)
    for (int j = 1; j <= i; j++)
        sum++;

i 的变化:

2
4
16
256
65536
...

也就是:

外层次数大约只有:

但千万不能得出:

因为内层每轮执行 i 次。

总执行次数:

序列增长非常快,总和由最后的大项主导。

网站在给定选项中选择:

(Any Docs)

不过这里补充一个严格性问题:

由于 i 只能取:

对于任意给定 n,最后一个 i 并不一定与 n 同阶,因此把它在所有 n 上直接写成严格的:

并不严谨。

但:

确实成立,而且是题目提供选项中的最小标准上界,所以按这道模拟选择题应选 B。

这个区别值得知道,不需要为408常规题过度纠结。


二十二、复杂度题真正通用的“六步法”

以后拿到陌生程序,不要靠感觉。

第一步:找基本操作

通常是最内层语句,例如:

count++;

真正的问题变成:

它总共执行多少次?

第二步:找控制变量

观察:

i++
i += k
i *= 2
i /= 2
sum += i
i *= i

这决定循环次数模型。

第三步:写第 k 次后的变量

例如:

i *= 2

得到:

例如:

i++

得到:

第四步:利用终止条件列方程

例如:

于是:

或者:

于是:

第五步:多层循环判断“乘还是求和”

若每轮固定执行 n 次:

可以写乘法。

若每轮不同:

必须求和。

第六步:删掉常数和低阶项

例如:

最终:

这套方法几乎可以吃掉本章全部真题。


二十三、观察到程序时可以直接做的“模式匹配”

程序特征第一反应
i++ 跑到 nO(n)
i += c 跑到 nO(n)
i *= cO(log n)
i /= cO(log n)
两层均跑 n 次O(n²)
一层 n,一层 log nO(n log n)
累加 1+2+…+k 达到 nO(√n)
判断 k² ≤ nO(√n)
1+2+4+…+nO(n)
两个单调双指针通常 O(n)
递归参数每次减 1栈深通常 O(n)
递归规模每次减半栈深通常 O(log n)

这一张表建议真正记住。


二十四、最容易错的五种思维

错误一:看到两层循环就写平方

2022 真题直接反例:

不是:


错误二:只看循环次数,不看循环体成本

外层:

并不说明整个程序:

因为里面可能还有大量工作。


错误三:递归调用很多,所以空间也很多

错误。

空间看的是:

同一时刻存在的最大递归深度。

例如斐波那契递归:

时间很大,但空间只是:


错误四:二叉树高度 log n,所以遍历也是 log n

错误。

平衡树高度可以是:

但如果需要访问全部 n 个结点:


错误五:能二分找到位置,所以插入也是 log n

错误。

顺序表:

查找位置:

移动元素:

总复杂度由大项决定:


二十五、补充一道很像未来 408 的综合变式

考虑:

int count = 0;

for (int i = 1; i <= n; i *= 2) {
    for (int j = 0; j < i; j++)
        count++;

    for (int k = 0; k < n; k++)
        count++;
}

问复杂度。

很多人可能看到第一部分:

于是答:

但是别忘了第二个循环。

外层执行:

次。

而第二个内循环每次执行:

次。

因此这部分:

总复杂度:

保留高阶项:

这类题就是把 2014 和 2022 两道真题揉在一起,非常符合408变式风格。


二十六、再预测一道空间复杂度题

int fun(int n) {
    if (n <= 1)
        return 1;

    return fun(n / 2) + fun(n / 2);
}

问时间和空间复杂度。

很多人会认为两个都是:

实际上不是。

递归树高度:

所以空间复杂度:

但每层节点数:

最后一层约 n 个节点。

总节点数:

因此:

所以答案:

这正是本站模拟题中“二叉树递归 + 二分递归空间”两种思想的融合。


二十七、本章最后应该形成什么能力?

按照本站的真题分布,第 1 章不值得花大量时间背长篇概念,而应该形成下面这条完整思考链:

程序
 ↓
基本操作
 ↓
循环变量如何变化
 ↓
执行 k 次后变量是多少
 ↓
利用终止条件求 k
 ↓
若有嵌套,判断相乘还是求和
 ↓
若有递归,分别考虑:
    总调用量 → 时间
    最大递归深度 → 空间
 ↓
得到渐近复杂度

尤其建议把六道真题组成三组来记:

真题组合本质
2011 + 模拟乘 3 / 除 2倍增倍减 → 对数
2017 + 2019平方达到 n → 平方根
2014 + 2022嵌套循环:机械相乘 vs 按轮求和

而 2023 真题 则单独记住顺序存储的核心优势:

综合本站的真题与模拟题,第 1 章最值得投入时间的排序就是:复杂度求和模型 > 递归时间/空间区分 > 倍增倍减循环 > 顺序表随机访问 > 算法概念判断 > 其余绪论概念。 网站本身也明确建议研究408时采用“知识点 → 真题 → 命题方式总结 → 模拟题补充”的路径,而本章的真题分布恰好非常适合这样学。(Any Docs)

本站:408 真题·数据结构第 1 章绪论 本站:408 模拟选择题·数据结构第 1 章绪论

On this page

一、本章在 408 中到底重要在哪里?二、必须掌握的基础概念1. 什么叫数据结构?三、题型一:算法基本概念判断四、时间复杂度的真正含义五、复杂度比较必须非常熟六、题型二:变量每次乘常数——对数复杂度真题:2011 年 408怎么分析?模拟题:王道·卷三 Q01模拟题:王道·卷四 Q01一句话模板七、题型三:普通递归——看递归深度和每层工作量2012 年 408 真题这道题更重要的隐藏考点:空间复杂度八、题型四:两层循环不能看到两层就写平方2014 年真题什么时候可以直接相乘?九、题型五:累计量达到 n——建立方程2017 年 408 真题以后看到这种题怎么办?十、题型六:循环变量满足平方条件——直接解不等式2019 年真题2017 和 2019 本质其实是一道题十一、题型七:内层执行次数随外层变化——必须求和2022 年 408 真题十二、这是复杂度题最重要的升级思想:求和而不是机械相乘十三、模拟强化:不是所有双重循环都是平方复杂度王道·卷一 Q02十四、题型八:双指针——两个循环也可以是线性复杂度十五、题型九:空间复杂度——千万不要数总调用次数竟成·模拟二-01:斐波那契十六、题型十:二分递归——时间可能是 n,空间却是 log n竟成·模拟七-01但它的时间复杂度是多少?十七、模拟强化:树递归的复杂度十八、题型十一:顺序存储为什么能 O(1) 随机访问?2023 年 408 真题为什么第 i 个元素可以直接得到?十九、为什么另外三个不是 O(1)?按值查找插入删除第 i 个元素二十、一个非常重要的表:顺序表常见复杂度二十一、一个容易出错的特殊模拟题:i 每次平方二十二、复杂度题真正通用的“六步法”第一步:找基本操作第二步:找控制变量第三步:写第 k 次后的变量第四步:利用终止条件列方程第五步:多层循环判断“乘还是求和”第六步:删掉常数和低阶项二十三、观察到程序时可以直接做的“模式匹配”二十四、最容易错的五种思维错误一:看到两层循环就写平方错误二:只看循环次数,不看循环体成本错误三:递归调用很多,所以空间也很多错误四:二叉树高度 log n,所以遍历也是 log n错误五:能二分找到位置,所以插入也是 log n二十五、补充一道很像未来 408 的综合变式二十六、再预测一道空间复杂度题二十七、本章最后应该形成什么能力?