【考查目标】 1.掌握数据结构的基本概念、基本原理和基本方法。
2.掌握数据的逻辑结构、存储结构及基本操作的实现,能够对算法进行基本的时间复杂度与空间复杂度的分析。
3.能够运用数据结构基本原理和方法进行问题的分析与求解,具备采用C或C++语言设计与实现算法的能力。
一、基本概念
(一)数据结构的基本概念
数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合,以及定义在该集合上的一组操作。其核心在于研究数据的逻辑结构、存储结构(物理结构)及其运算。
逻辑结构:描述数据元素之间的逻辑关系,独立于计算机。主要分为:
线性结构:数据元素之间存在一对一的线性关系,如线性表、栈、队列、串、数组。
非线性结构:包括树形结构(一对多,如树、二叉树)和图状结构/网状结构(多对多,如图)。
集合结构:数据元素间除"同属一个集合"外无其他关系。
存储结构:逻辑结构在计算机中的物理实现(映像)。主要包括:
顺序存储:逻辑上相邻的元素存储在物理位置也相邻的存储单元中。优点是可随机存取,存储密度高;缺点是插入、删除操作可能需要移动大量元素,且需预先分配连续空间。
链式存储:不要求逻辑上相邻的元素在物理位置上也相邻,通过指针(链)表示元素间的逻辑关系。优点是插入、删除灵活,空间利用率高;缺点是存储密度较低,且不可随机存取。
索引存储:除建立存储结点外,还附加建立索引表(由关键字和地址组成)来指示结点存储位置。
散列存储(哈希存储):根据结点的关键字直接计算出该结点的存储地址。
数据的运算:施加在数据上的操作,如插入、删除、查找、排序、修改等。运算的定义依赖于逻辑结构,而运算的具体实现则依赖于存储结构。
重难点解析:
逻辑结构与存储结构的区别与联系:逻辑结构是面向问题的抽象模型,存储结构是面向计算机的具体实现。同一种逻辑结构可以采用不同的存储结构,其运算效率也会不同。这是理解数据结构灵活性与复杂性的基础。
算法效率与存储结构的关系:算法的设计与效率分析必须紧密结合所选的存储结构。例如,在顺序表(顺序存储)中按索引访问元素的时间复杂度为O(1),而在链表(链式存储)中则为O(n);但链表的插入删除操作在已知位置时通常更高效。
(二)算法的基本概念
算法是对特定问题求解步骤的一种描述,是指令的有限序列。一个算法必须具备五个重要特性:有穷性、确定性、可行性、输入(零个或多个)和输出(一个或多个)。
算法设计目标:正确性、可读性、健壮性、高效率与低存储量需求(即时间效率高和空间效率高)。
算法效率的度量:
时间复杂度:衡量算法执行时间随问题规模增长而增长的趋势。通常关注最坏情况时间复杂度和平均情况时间复杂度,使用大O记法表示。常见复杂度有O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(2ⁿ)等。
空间复杂度:衡量算法执行过程中所需辅助存储空间随问题规模增长而增长的趋势。
重难点解析:
时间复杂度分析:是408考研的核心考点。关键在于找出基本操作执行次数与问题规模n之间的函数关系,并忽略低阶项和常数系数,保留最高阶项。需熟练掌握递归算法(如分治法)的时间复杂度推导(常涉及主定理或递归树法),以及循环嵌套、顺序结构等常见模式的分析。
空间复杂度分析:需注意算法本身占用的空间(如代码)和算法运行中使用的辅助空间。对于递归算法,递归栈的深度是空间复杂度分析的关键。
算法与程序的区别:算法是解决问题的步骤描述,可以用自然语言、伪代码、流程图等表示,独立于具体的编程语言;程序是算法在特定编程语言上的具体实现。
二、线性表
(一) 线性表的基本概念
线性表是具有相同数据类型的 n (n ≥ 0) 个数据元素的有限序列。通常表示为 L = (a₁, a₂, ..., aᵢ, ..., aₙ)。
核心特性:
有限性:元素个数有限。
序列性:元素之间存在严格的顺序关系,每个元素有且仅有一个直接前驱和一个直接后继(除首尾元素外)。
同类型:所有元素属于同一数据对象。
基本操作:初始化、销毁、判空、求长度、按值查找、按位查找、插入、删除、遍历等。
重难点解析:
逻辑结构 vs. 物理结构:线性表是一种逻辑结构,其具体实现依赖于顺序存储或链式存储等物理结构。这是理解线性表所有变体(如栈、队列)的基础。
"位序"概念:线性表中元素的位序从1开始,而数组下标通常从0开始。在实现和解题时,必须严格区分,这是408选择题的常见陷阱。
(二) 线性表的实现
- 顺序存储(顺序表)
用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的元素在物理位置上也相邻。
实现方式:
静态分配:使用定长数组,容量固定。
动态分配:使用指针和malloc/new动态申请内存,容量可扩。
核心特点:
随机访问:通过首地址和元素序号(下标)可在O(1)时间内找到指定元素。
存储密度高:结点只存储数据元素本身。
插入/删除效率低:平均需要移动约一半的元素,时间复杂度为O(n)。
重难点与408真题考点:
插入操作:在顺序表L的第i (1 ≤ i ≤ L.length+1) 个位置插入新元素e。
必须将第i个元素及之后的所有元素后移。
移动次数 = n - i + 1。
平均时间复杂度:O(n)。
易错点:需先判断插入位置i的合法性,并检查表是否已满。
删除操作:删除顺序表L中第i (1 ≤ i ≤ L.length) 个位置的元素。
必须将第i+1个元素及之后的所有元素前移。
移动次数 = n - i。
平均时间复杂度:O(n)。
动态扩容:当空间不足时,需申请一个更大的新数组,将原数组元素复制过去,再释放原数组。这是一个耗时的操作,虽然均摊时间复杂度可能仍为O(1),但单次扩容开销大。
- 链式存储(链表)
用一组任意的存储单元存储线性表的数据元素,通过指针来表示元素间的逻辑关系。
单链表结点结构:数据域 | 指针域。
核心特点:
非随机访问:查找第i个元素需要从头指针开始顺序查找,时间复杂度为O(n)。
存储密度较低:结点需额外存储指针。
插入/删除效率高:在已知结点前驱/后继的情况下,仅需修改指针,时间复杂度为O(1)。
主要类型:
单链表:每个结点包含数据和指向下一个结点的指针。
双链表:每个结点包含指向前驱和后继的指针,支持双向遍历。
循环链表:尾结点指针指向头结点,形成环。
静态链表:借助数组描述链式结构,指针域存放的是数组下标。
重难点与408真题考点:
头结点 vs. 头指针:
头指针:指向链表第一个结点的指针,是链表的必要标识。
头结点:在第一个数据结点之前附加的一个结点,其数据域可不存信息(或存表长等)。引入头结点可以统一空表和非空表的操作,简化插入/删除首元结点的代码逻辑。
插入与删除操作:
单链表后插:给定结点*p,在其后插入新结点*s。
c
Copy Code
s->next = p->next;
p->next = s;
单链表前插(经典方法):给定结点*p,在其前插入新结点*s。通常需要找到*p的前驱结点*q,然后对*q进行后插,时间复杂度O(n)。
单链表前插(高效技巧):在*p后插入新结点*s,然后交换p和s的数据域。这样可以在O(1)时间内实现"前插"效果,是重要技巧。
双链表插入/删除:操作涉及前驱和后继两个指针的修改,必须注意修改顺序,防止断链。典型顺序:先处理新结点的指针,再处理原结点的指针。
链表判空条件:
带头结点的单链表:L->next == NULL
不带头结点的单链表:L == NULL
带头结点的循环单链表:L->next == L
特殊题型:
逆置链表:常考算法,需熟练掌握头插法或三指针法。
合并有序链表:归并思想的应用。
寻找公共结点/环的入口:快慢指针法的经典应用。
(三) 线性表的应用
线性表作为最基础的结构,其思想和实现是其他复杂结构的基础。
栈和队列:可以视为操作受限的线性表。
栈是后进先出(LIFO)的线性表,插入和删除仅在表尾进行。
队列是先进先出(FIFO)的线性表,插入在队尾,删除在队头。
它们都可以用顺序存储(顺序栈/循环队列)或链式存储(链栈/链队)实现。
多项式运算:可以用线性表(顺序或链式)存储多项式的系数和指数,实现相加、相乘等运算。
内存管理:操作系统中的空闲内存块管理,常使用链表(如空闲链表)进行组织。
应用选择原则:
频繁查找,少增删 → 顺序表。
频繁在首尾增删 → 可考虑带头结点的单链表或双链表。
频繁在任意位置插入删除 → 链表。
无法预估表长 → 链表。
需要快速随机访问 → 顺序表。
综合重难点总结:
时间复杂度的权衡:顺序表"以空间换时间"(随机访问),链表"以时间换空间"(灵活存储)。这是所有选择题和分析题的核心出发点。
边界条件处理:无论是顺序表的插入删除(判断i的合法性、表满表空),还是链表的操作(头结点、尾结点、空链表),边界条件的处理是代码题和算法题的绝对重点和易错点。
408典型综合题:常要求对比顺序表和链表在特定场景下的性能,或要求手写一个完整的链表操作算法(如逆置、合并、去重),并分析其时间复杂度。务必注重代码的鲁棒性和完整性。
三、栈、队列和数组
(一) 栈和队列的基本概念
栈是一种后进先出的线性表,限定仅在表尾进行插入和删除操作。表尾称为栈顶,表头称为栈底。基本操作包括:初始化、判空、进栈、出栈、读栈顶元素等。
队列是一种先进先出的线性表,限定在表尾插入,在表头删除。表尾称为队尾,表头称为队头。基本操作包括:初始化、判空、入队、出队、读队头元素等。
重难点解析:
操作受限性:栈和队列是操作受限的线性表。这种限制使得它们在某些特定场景下(如函数调用、任务调度)比普通线性表更高效、更安全。
卡特兰数:n个不同元素进栈,其出栈序列的总数为卡特兰数 C(2n, n)/(n+1)。这是栈的核心性质,也是408选择题的经典考点。
输入/输出序列合法性判断:给定一个入栈序列,判断某个出栈序列是否可能。解题核心是模拟:依次处理出栈序列,检查当前期望出栈的元素是否在栈顶,若不在则按入栈序列顺序压栈,若压完所有元素后仍无法匹配则序列非法。
(二) 栈和队列的顺序存储结构
- 顺序栈
利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时设一个指针指示当前栈顶元素的位置。
实现:通常用数组data[MaxSize]和整型变量top(栈顶指针)实现。
栈空条件:top == -1(若栈顶指针指向栈顶元素)。
栈满条件:top == MaxSize - 1。
进栈操作:data[++top] = x;
出栈操作:x = data[top--];
- 循环队列
为解决顺序队列"假溢出"问题(队头有空闲空间但队尾指针已到数组末尾)而设计。将顺序队列臆造为一个环状空间。
实现:数组data[MaxSize],两个整型变量front(队头指针)和rear(队尾指针)。
队空条件:front == rear。
队满条件:(rear + 1) % MaxSize == front。此方法会牺牲一个存储单元以区分队空和队满。
入队操作:rear = (rear + 1) % MaxSize; data[rear] = x;
出队操作:front = (front + 1) % MaxSize; x = data[front];
重难点与408真题考点:
栈的共享:两个栈共享一个一维数组空间。将两个栈的栈底分别设置在数组的两端,栈顶向中间延伸。仅当两个栈顶指针相邻时才栈满。这种结构能更有效地利用存储空间。
循环队列的判空与判满:这是最大的难点和考点。除了上述牺牲一个单元的方法,还可以:
增设一个表示元素个数的数据成员size。
增设一个tag标志位(0表示最近一次操作是出队,1表示入队),当front == rear时,若tag==0则为空,tag==1则为满。
必须熟练掌握牺牲单元法的指针移动和条件判断。
队列元素个数计算:在循环队列中,(rear - front + MaxSize) % MaxSize。
(三) 栈和队列的链式存储结构
- 链栈
采用单链表实现,规定所有操作在单链表的表头进行。通常不设头结点,Lhead指针即为栈顶指针。
栈空条件:Lhead == NULL。
优点:便于多个栈共享存储空间,且不存在栈满上溢的情况。
- 链队
采用带有头结点的单链表实现。队头指针front指向头结点,队尾指针rear指向最后一个结点。
队空条件:front == rear(即front->next == NULL)。
优点:不存在队满问题(内存允许的情况下)。
重难点解析:
链栈与顺序栈的选择:链栈的优点是动态扩容,但每个元素需要额外指针空间。顺序栈访问快,但容量固定。
链队与循环队列的选择:链队动态性好,循环队列静态分配、访问效率高。在无法预估数据量时宜用链队。
(四) 多维数组的存储
多维数组在内存中通常采用行优先或列优先的方式映射到一维线性地址空间。
行优先存储:先行后列。如C、Pascal语言。
列优先存储:先列后行。如FORTRAN语言。
数组元素地址计算(以行优先为例):
对于m×n的二维数组A[0..m-1][0..n-1],设每个元素占L个字节,首地址为LOC,则A[i][j]的地址为:
LOC + (i * n + j) * L
重难点解析:
计算通式:对于n维数组A[d₁][d₂]...[dₙ],行优先下,A[i₁][i₂]...[iₙ]的地址为:
LOC + (i₁*d₂*d₃*...*dₙ + i₂*d₃*...*dₙ + ... + iₙ₋₁*dₙ + iₙ) * L
必须理解并会推导此公式,这是计算特殊矩阵压缩存储地址的基础。
(五) 特殊矩阵的压缩存储
压缩存储的目标是:为多个值相同的元素只分配一个存储空间,对零元素不分配空间。
- 对称矩阵
存储下三角(或上三角)区和主对角线元素。对于n×n矩阵,需存储n(n+1)/2个元素。
下三角按行优先存储时,a[i][j](i≥j)在一维数组B中的下标k为:
k = i*(i-1)/2 + j-1 (数组下标从1开始)
k = i*(i+1)/2 + j (数组下标从0开始,且i, j从0开始)
- 三角矩阵
与对称矩阵类似,但需额外一个存储单元存放常数区(上三角或下三角的常量)。
- 三对角矩阵(带状矩阵)
所有非零元素集中在主对角线及其相邻两条对角线上。对于n×n矩阵,需存储3n-2个元素。
按行优先将非零元素存入B[3n-2]中,a[i][j]在B中的位置k为:
k = 2i + j (有多种等价公式,需注意i, j的起始值)
- 稀疏矩阵
非零元素个数远少于矩阵元素总数。存储方法有:
三元组顺序表:每个非零元用(行标,列标,值)表示,按行优先存储。
十字链表:每个非零元结点包含行、列、值、以及指向同行和同列下一个非零元的指针。便于矩阵的加法、乘法等运算。
重难点与408真题考点:
下标变换公式的推导与应用:这是核心考点。必须掌握从矩阵下标(i, j)到压缩数组下标k的映射关系,以及反向映射。考题常要求计算特定元素的存储位置或根据存储位置反推矩阵下标。
稀疏矩阵存储方式的选择:三元组表节省空间但失去随机存取特性;十字链表适合矩阵运算但结构复杂。需根据问题需求(如转置、乘法)选择合适结构。
(六) 栈、队列和数组的应用
栈的应用:
函数调用/递归:系统栈用于保存调用点的返回地址、局部变量、参数等。
括号匹配:遇到左括号入栈,遇到右括号检查栈顶是否匹配。
表达式求值:中缀转后缀(或直接求值),需要操作数栈和运算符栈。
迷宫求解:回溯法,栈记录路径。
浏览器的前进后退:使用两个栈实现。
队列的应用:
层次遍历:二叉树的层次遍历、图的广度优先搜索。
缓冲区:操作系统中的进程就绪队列、打印队列。
资源分配:CPU时间片轮转调度。
消息队列:系统间异步通信。
数组的应用:
矩阵运算:科学计算的基础。
哈希表:直接寻址表。
字符串:字符数组存储。
静态查找表:有序数组的二分查找。
综合重难点总结:
栈在表达式求值中的核心作用:中缀表达式转后缀表达式的算法(运算符栈的使用规则),以及后缀表达式的求值算法(操作数栈的使用),是408算法题的经典题型,必须熟练掌握流程。
循环队列的灵活运用:不仅是数据结构,其"环形缓冲区"的思想在操作系统、网络通信中广泛应用。
压缩存储的思维:不仅为了节省空间,更重要的是理解数据的内在规律(对称性、稀疏性),并设计高效的存取方法。这是从"存储"上升到"设计"的关键一步。
四、树与二叉树
(一) 树的基本概念
树是n (n≥0) 个结点的有限集合。当 n=0 时,称为空树;对于任意一棵非空树:
有且仅有一个特定的称为根的结点。
当 n>1 时,其余结点可分为 m (m>0) 个互不相交的有限集合,每个集合本身又是一棵树,称为根的子树。
重要术语:结点的度、树的度、叶子结点、分支结点、孩子、双亲、兄弟、祖先、子孙、层次、深度(高度)、有序树、无序树、路径、路径长度、森林。
重难点解析:
树与线性结构的根本区别:线性结构中每个结点至多一个直接前驱和一个直接后继;树中每个结点有且仅有一个直接前驱(双亲),但可以有零个或多个直接后继(孩子),呈现一对多的关系。
度为 m 的树与 m 叉树的区别:这是核心易错点。
度为 m 的树:任意结点的度 ≤ m,且至少有一个结点的度等于 m,同时要求树非空。
m 叉树:任意结点的度 ≤ m,可以是空树。
(二) 二叉树
- 二叉树的定义及其主要特征
二叉树是每个结点最多有两棵子树(即度 ≤ 2),且子树有左右之分(有序)的树结构。
主要性质(408 常考推导与计算):
第 i 层最多有 2^{i-1} 个结点 (i≥1)。
深度为 k 的二叉树最多有 2^{k} - 1 个结点 (k≥1)。
任意二叉树,若叶子结点数为 n₀,度为 2 的结点数为 n₂,则 n₀ = n₂ + 1。
具有 n 个结点的完全二叉树的深度为 ⌊log₂n⌋ + 1。
完全二叉树的顺序存储中,对于结点 i:
左孩子为 2i(若 2i ≤ n)。
右孩子为 2i+1(若 2i+1 ≤ n)。
双亲为 ⌊i/2⌋(若 i>1)。
特殊二叉树:
满二叉树:深度为 k 且有 2^k - 1 个结点。
完全二叉树:除最后一层外,其余层都是满的,且最后一层结点从左到右连续排列。
- 二叉树的顺序存储结构和链式存储结构
顺序存储:用一组连续的存储单元,按完全二叉树的结点编号顺序依次存储。仅适用于完全二叉树或接近完全的二叉树,否则空间浪费严重。
链式存储(二叉链表):每个结点包含数据域、左孩子指针、右孩子指针。n 个结点的二叉链表有 n+1 个空指针域。
- 二叉树的遍历
遍历是按某条搜索路径访问每个结点一次且仅一次。四种基本方式:
先序遍历:根 → 左 → 右
中序遍历:左 → 根 → 右
后序遍历:左 → 右 → 根
层序遍历:从上到下、从左到右逐层访问
重难点与 408 真题考点:
遍历序列的互推:已知中序序列和另一种序列(先序、后序、层序之一),可以唯一确定一棵二叉树。若只有先序和后序,则无法唯一确定(除非是满二叉树或给出其他条件)。
递归与非递归实现:必须掌握三种遍历的递归算法(简单)和非递归算法(重点,尤其是中序)。非递归遍历需借助栈来模拟递归调用。
线索二叉树(见下)
- 线索二叉树的基本概念和构造
利用二叉链表中的空指针域,使其指向该结点在某种遍历序列中的前驱或后继。这种附加的指针称为"线索"。
目的:加快查找结点前驱和后继的速度,无需递归或栈即可遍历。
结构:在结点中增加两个标志域 ltag 和 rtag。当 tag == 0 时,指针指向孩子;当 tag == 1 时,指针指向线索(前驱/后继)。
构造:对二叉树进行某种次序的遍历,在遍历过程中修改空指针,添加线索。通常有先序、中序、后序线索二叉树。以中序线索化最为常见。
遍历:对于中序线索二叉树,找后继的规则是:
若 rtag == 1,则后继即右指针所指。
若 rtag == 0,则后继是其右子树的最左下结点。
重难点解析:
线索化的实质:是对二叉树进行一次遍历,在访问结点时处理其前驱和后继关系。理解线索化过程的关键在于设置一个全局变量 pre 始终指向刚刚访问过的结点,当前结点 p 的前驱就是 pre,而 pre 的后继是 p。
前驱/后继的确定:在不同次序的线索树中,找特定结点的前驱和后继的规则不同,容易混淆。需结合遍历次序的特点来记忆。
(三) 树、森林
- 树的存储结构
双亲表示法:用一组连续空间存储结点,每个结点包含数据域和指向其双亲下标的指针。易于找双亲,但找孩子需遍历。
孩子表示法:每个结点的孩子结点用单链表链接。易于找孩子,但找双亲困难。
孩子兄弟表示法(二叉链表表示法):每个结点包含数据域、指向第一个孩子的指针、指向下一个兄弟的指针。此法是实现树与二叉树转换的桥梁。
- 森林与二叉树的转换
基于孩子兄弟表示法建立一一对应关系。转换是唯一的。
树 → 二叉树:每个结点的左指针指向其第一个孩子,右指针指向其下一个兄弟。"左孩子,右兄弟"。
森林 → 二叉树:将森林中每棵树先转为二叉树,然后将第二棵树的根作为第一棵树根的右兄弟(即第一棵二叉树根的右孩子),依此类推。
二叉树 → 森林:是上述过程的逆过程。
- 树和森林的遍历
树的遍历:
先根遍历:访问根 → 依次先根遍历每棵子树。序列与对应二叉树的先序序列相同。
后根遍历:依次后根遍历每棵子树 → 访问根。序列与对应二叉树的中序序列相同。
森林的遍历:
先序遍历森林:访问第一棵树的根 → 先序遍历第一棵树根的子树森林 → 先序遍历剩余树构成的森林。序列与对应二叉树的先序序列相同。
中序遍历森林:中序遍历第一棵树的子树森林 → 访问第一棵树的根 → 中序遍历剩余树构成的森林。序列与对应二叉树的中序序列相同。
重难点解析:
对应关系:树/森林的遍历与二叉树遍历的对应关系(先根对先序,后根对中序)必须牢记,是转换与应用的基础。
(四) 树与二叉树的应用
- 哈夫曼树和哈夫曼编码
哈夫曼树(最优二叉树):带权路径长度(WPL)最小的二叉树。
构造算法(贪心):
将 n 个权值看作 n 棵仅含根结点的二叉树,构成森林 F。
从 F 中选取两棵根结点权值最小的树作为左右子树构造新树,新树根权值为其和。
将新树加入 F,删除原来两棵树。
重复 2、3,直到 F 中只剩一棵树。
特点:没有度为 1 的结点;n 个叶子结点的哈夫曼树共有 2n-1 个结点。
哈夫曼编码:用于数据压缩。前缀编码(任一字符的编码都不是另一字符编码的前缀)。编码长度等于该字符在哈夫曼树中的路径长度。
重难点与 408 真题考点:
计算 WPL:必须熟练。WPL = Σ(叶子权值 × 路径长度)。
构造与验证:给定一组权值,能画出哈夫曼树,写出各字符编码,并计算 WPL。选择题常考查哈夫曼树形态不唯一,但 WPL 唯一最小。
前缀编码的判断:给定一组编码,判断是否为前缀编码(构造一棵二叉判定树,看是否所有字符都在叶子结点)。
- 并查集及其应用
用于管理一系列不相交的集合,支持两种操作:
查找:确定某个元素属于哪个集合(通常返回根的代表元素)。
合并:将两个集合合并为一个。
实现:通常用双亲表示法的树。用数组 S[] 表示,S[i] 表示元素 i 的双亲,根结点的双亲为 -1 或自身。
优化:
查找路径压缩:查找时,将路径上所有结点的父指针直接指向根。
按秩(高度)合并:将矮树合并到高树上,防止树退化成链。
应用:判断无向图的连通分量、Kruskal 最小生成树算法中判断边是否构成环、社交网络的朋友圈问题等。
重难点解析:
数组表示法的理解:S[i] = j 表示 j 是 i 的父结点;S[i] = -1 或 i 表示 i 是根。并查集操作的核心就是不断向上找根。
性能分析:优化后的并查集,单次操作的平均时间复杂度可视为常数级 O(α(n)),其中 α(n) 是增长极慢的阿克曼函数反函数。
- 堆及其应用
堆是一种完全二叉树,且满足:每个结点的值都大于等于(大顶堆)或小于等于(小顶堆)其孩子结点的值。
存储:由于是完全二叉树,通常用数组顺序存储。
基本操作:
向下调整:从某个结点开始,与其孩子比较,若不满足堆性质则交换,并继续向下调整。时间复杂度 O(log n)。
向上调整:从某个叶子结点开始,与其父结点比较,若不满足则交换,并继续向上调整。时间复杂度 O(log n)。
建堆:从最后一个非叶子结点开始,向前依次对每个结点进行向下调整。时间复杂度 O(n)。
插入:将新元素放末尾,然后向上调整。
删除堆顶:用最后一个元素替换堆顶,然后对堆顶进行向下调整。
应用:
堆排序:O(n log n) 的不稳定排序。步骤:建堆 → 反复交换堆顶与末尾元素并调整。
优先级队列:最高(或最低)优先级的元素始终在堆顶,可用于任务调度、合并 k 个有序链表等。
重难点与 408 真题考点:
建堆的复杂度分析:O(n) 是重要结论,需理解其推导(错位相减法或级数求和)。
堆的插入删除序列:给定一个堆,插入或删除一个元素后,判断新堆的状态,或选择正确的调整步骤。
堆排序的过程模拟:需完整掌握每趟排序后堆的变化。
五、图
(一) 图的基本概念
图 G 由顶点集 V 和边集 E 组成,记为 G=(V, E)。其中,V 是有限非空集合;E 是顶点间关系(边)的集合,可以是空集。
重要术语与分类:
有向图 vs. 无向图:边是否有方向。
简单图 vs. 多重图:简单图不存在重复边和顶点到自身的边(环)。
完全图:无向完全图有 n(n-1)/2 条边;有向完全图有 n(n-1) 条边。
子图:顶点和边都是原图的子集。
连通、连通图、连通分量(无向图):
顶点间有路径则连通。
连通图:任意两顶点均连通。
连通分量:极大连通子图。
强连通、强连通图、强连通分量(有向图):
顶点间双向有路径则强连通。
强连通图:任意一对顶点都强连通。
强连通分量:极大强连通子图。
生成树、生成森林:连通图的生成树是包含全部顶点的极小连通子图(n个顶点,n-1条边)。非连通图的生成森林由各连通分量的生成树组成。
度、入度、出度:
无向图:顶点 v 的度 TD(v) 是以 v 为端点的边数。
有向图:入度 ID(v) 是以 v 为终点的有向边数;出度 OD(v) 是以 v 为起点的有向边数。TD(v) = ID(v) + OD(v)。
边的权和网:带权图称为网。
稠密图 vs. 稀疏图:通常以 |E| < |V|log|V| 作为稀疏图的粗略判定。
重难点与408真题考点:
度的性质:
无向图:所有顶点的度之和等于边数的 2倍。
有向图:所有顶点的入度之和 = 出度之和 = 边数。
任意图,奇度顶点的个数必为偶数。
连通性相关结论:
对于无向连通图,边数 ≥ 顶点数 - 1。
删除某个顶点或边后可能影响连通性,相关计算是选择题常考点。
n个顶点的图,可能有多少种形态? 这是对图基本概念的综合性考察。
(二) 图的存储及基本操作
- 邻接矩阵法
用一个一维数组存储顶点信息,一个二维数组(邻接矩阵)存储边(或弧)的信息。
无向图:邻接矩阵是对称矩阵,第 i 行(或列)非零元素个数为顶点 i 的度。
有向图:第 i 行非零元素个数为顶点 i 的出度;第 i 列非零元素个数为顶点 i 的入度。
带权图(网):矩阵元素 A[i][j] 为权值 w_{ij};若不存在边,则为 ∞。
特点:
空间复杂度:O(|V|²),适合存储稠密图。
优点:便于判断顶点间是否有边、求顶点的度。
缺点:增删顶点操作代价高;稀疏图空间浪费大。
- 邻接表法
为每个顶点建立一个单链表,链表中结点表示依附于该顶点的边(对于有向图,通常表示以该顶点为尾的弧)。
顶点表:数组存储,包含数据域和指向第一条边的指针。
边表:链表结点包含邻接点域(指示该边指向的顶点)、权值域(可选)和下一条边指针域。
特点:
空间复杂度:无向图 O(|V|+2|E|),有向图 O(|V|+|E|),适合存储稀疏图。
优点:节省空间;便于增删顶点和边。
缺点:不便于判断两顶点间是否有边;求有向图顶点的入度需要遍历整个表(可构造逆邻接表解决)。
- 邻接多重表、十字链表
十字链表:用于存储有向图。结合了邻接表和逆邻接表。边结点同时被链入出边链表和入边链表,便于同时求入度和出度。
邻接多重表:用于存储无向图。每条边用一个结点表示,该结点同时链接在两个顶点的边链表中。解决邻接表在无向图中同一条边存储两次的问题,便于边的删除等操作。
重难点解析:
存储结构的选择:根据图是稠密还是稀疏、需要频繁进行何种操作(查边、增删顶点、遍历等)来选择。这是综合应用题的基础。
结构理解:十字链表和邻接多重表是优化特定操作的链式存储,需理解其结点结构设计如何解决邻接表的不足。
(三) 图的遍历
从图中某一顶点出发,访问图中所有顶点,且每个顶点仅访问一次。
- 深度优先搜索
类似树的先根遍历。从起始顶点 v 出发,访问 v,然后从 v 的未被访问的邻接点出发深度优先遍历图,直至所有与 v 连通的顶点被访问;若此时仍有顶点未被访问,则另选一个未访问顶点作为起点重复过程。
实现:递归或显式栈。
空间复杂度:来自递归栈或辅助栈,最坏 O(|V|)。
时间复杂度:
邻接矩阵:O(|V|²)。
邻接表:O(|V|+|E|)。
应用:判断图的连通性、求连通分量、判断是否有环(无向图)、拓扑排序等。
- 广度优先搜索
类似树的层次遍历。从起始顶点 v 出发,访问 v,然后依次访问 v 的各个未被访问的邻接点,再按这些邻接点被访问的先后次序依次访问它们的邻接点,直至所有与 v 连通的顶点被访问。
实现:队列。
空间复杂度:来自辅助队列,最坏 O(|V|)。
时间复杂度:同 DFS。
应用:求无权图的单源最短路径、Dijkstra算法的原型。
重难点与408真题考点:
遍历序列的不唯一性:由于顶点邻接点访问顺序不固定,DFS和BFS序列可能不唯一。但给定存储结构和起始点,序列通常唯一。
基于遍历的算法设计:如何利用DFS/BFS的框架解决具体问题(如判断连通性、是否有环、求两点间路径等)是代码题重点。
遍历生成树/森林:在遍历过程中,将经过的边连接起来,会形成一棵深度优先生成树或广度优先生成树。对非连通图,则得到生成森林。
(四) 图的基本应用
- 最小(代价)生成树
在带权连通无向图中,权值之和最小的生成树。
Prim算法(普里姆):
思想:从某个顶点开始构建生成树,每次将代价最小的新顶点加入生成树,直至所有顶点都加入。
实现:需要两个辅助数组:lowcost[] 记录当前生成树到其他顶点的最小权值;adjvex[] 记录最小权值对应的顶点。
时间复杂度:O(|V|²),适合稠密图。
Kruskal算法(克鲁斯卡尔):
思想:按权值递增顺序选择边,若该边连接的两个顶点属于不同的连通分量,则加入生成树,否则舍弃。
实现:需要并查集来判断两个顶点是否属于同一连通分量。
时间复杂度:O(|E|log|E|),主要来自边排序,适合稀疏图。
重难点:两种算法的思想、步骤、适用场景对比。掌握手动模拟过程。Prim算法与Dijkstra算法在形式上相似,但核心不同(Prim是离生成树集合最近,Dijkstra是离源点最近)。
- 最短路径
Dijkstra算法(迪杰斯特拉):求单源最短路径,边权非负。
思想:贪心。设置两个顶点集合 S 和 U。S 包含已确定最短路径的顶点,U 包含未确定的。每次从 U 中选出距离源点 v 最短的顶点 k 加入 S,并更新 v 到 U 中所有顶点的距离。
时间复杂度:O(|V|²)。使用优先队列(最小堆)可优化至 O(|V|+|E|log|V|)。
特点:不适用于有负权边的图。
Floyd算法(弗洛伊德):求各顶点对之间的最短路径。
思想:动态规划。引入中转顶点,逐步优化最短路径。
核心递推:A^{(k)}[i][j] = min{A^{(k-1)}[i][j], A^{(k-1)}[i][k] + A^{(k-1)}[k][j]}。
时间复杂度:O(|V|³)。
特点:允许边权为负,但不能有负权回路。
重难点与408真题考点:
Dijkstra算法的过程模拟:必须熟练掌握每一步距离数组的更新过程,并能画出最短路径树。这是选择题和综合题的高频考点。
Floyd算法的矩阵序列:给定邻接矩阵,要求写出每一轮迭代后的矩阵。理解 k 作为中转点的含义是关键。
算法对比:单源 vs. 全源;能否处理负权边。
- 拓扑排序
针对有向无环图的顶点的一种线性排序,使得对于任何有向边 u→v,u 在排序中都出现在 v 之前。
算法步骤(基于队列):
计算所有顶点的入度,将入度为 0 的顶点入队。
出队一个顶点并输出,将其所有邻接点的入度减 1。若某邻接点入度变为 0,则入队。
重复步骤 2,直到队列为空。
若输出的顶点数小于图中顶点数,则说明图中存在环。
时间复杂度:O(|V|+|E|)。
应用:课程安排、任务调度、编译顺序。
重难点:理解拓扑排序不唯一。掌握手动求解拓扑序列的过程,并能判断给定序列是否为合法拓扑序列。
- 关键路径
在带权有向无环图中,从源点到汇点的最长路径称为关键路径。关键路径上的活动称为关键活动,关键活动延迟会导致整个项目延迟。
相关概念:事件(顶点)、活动(边)、活动持续时间(边权)。
关键参数:
事件 v_k 的最早发生时间 ve(k):从源点到 v_k 的最长路径长度。
事件 v_k 的最迟发生时间 vl(k):不推迟整个工期的前提下,该事件最迟必须发生的时间。
活动 a_i 的最早开始时间 e(i):等于该活动弧尾事件的最早发生时间。
活动 a_i 的最迟开始时间 l(i):等于该活动弧头事件的最迟发生时间减去活动持续时间。
活动 a_i 的时间余量 d(i):d(i) = l(i) - e(i)。
关键活动:d(i) = 0 的活动。由所有关键活动构成的路径即关键路径。
求解步骤:
从源点出发,按拓扑排序求所有事件的 ve。
从汇点出发,按逆拓扑排序求所有事件的 vl。
根据 ve 和 vl 求每个活动的 e 和 l。
找出 e(i) = l(i) 的活动,即为关键活动。
重难点与408真题考点:
手工计算关键路径:这是必考的综合题。必须清晰写出 ve、vl、e、l、d 的表格,并找出关键活动和关键路径。
理解关键路径的意义:缩短关键活动可以缩短工期;非关键活动有松弛时间。注意,关键路径可能不止一条,加快所有关键路径上的公共活动才能缩短工期。
与拓扑排序的关系:求 ve 需要正向拓扑序列,求 vl 需要反向拓扑序列。
六、查找
(一) 查找的基本概念
查找是在数据集合中寻找满足某种条件的元素的过程。数据集合称为查找表。
静态查找表:仅进行查找操作,不修改表。
动态查找表:查找过程中可能插入或删除元素。
关键字:标识数据元素的数据项。
平均查找长度:衡量查找算法效率的主要指标,定义为所有查找过程中进行的关键字比较次数的期望值。
成功平均查找长度:ASL_success = Σ(P_i * C_i),其中 P_i 为查找第 i 个元素的概率,C_i 为找到该元素所需的比较次数。
不成功平均查找长度:查找失败时的平均比较次数。
重难点:理解 ASL 的计算是评估所有查找算法性能的核心。对于不同结构的查找表(无序、有序、树形、散列),ASL 的计算方法不同。
(二) 顺序查找法
又称线性查找,从表的一端开始,依次将元素关键字与给定值比较。
适用性:无序线性表。
实现:简单循环。
效率分析(设表长为 n):
成功 ASL:查找概率相等时,ASL_success = (n+1)/2。
失败 ASL:ASL_fail = n+1(通常假设需比较完整个表才确认失败)。
优化:设置哨兵(将待查值放在表头或表尾作为哨兵),可减少每次循环的越界判断。
重难点与408考点:
对有序表的顺序查找:查找失败时不一定需要比较完所有元素,当遇到第一个大于(或小于)给定值的元素时即可判定失败。此时失败 ASL 降低。
查找概率不等时的优化:按查找概率从高到低排列元素,可降低成功 ASL。
(三) 分块查找法
又称索引顺序查找,是顺序查找和折半查找的折中。
结构:将查找表分成若干块,块内元素可以无序,但块间有序(即后一块中所有元素的关键字均大于前一块的最大关键字)。另建一个索引表,索引表中每个元素包含各块的最大关键字和该块的起始地址。
过程:先在索引表中确定待查记录所在的块(可顺序或折半查找),再在块内顺序查找。
效率分析:设表长 n,均匀分为 b 块,每块 s 个元素(n = b * s)。
若对索引表采用顺序查找,则 ASL = (b+1)/2 + (s+1)/2。当 s = √n 时,ASL 取最小值 √n + 1。
若对索引表采用折半查找,则 ASL ≈ log₂(b+1) - 1 + (s+1)/2。
重难点:理解分块查找对数据"块内无序、块间有序"的要求,以及 ASL 受索引表和块内查找双重影响的特点。
(四) 折半查找法
又称二分查找,要求查找表必须采用顺序存储结构,且元素按关键字有序排列。
算法思想:将给定值 k 与中间元素的关键字比较,若相等则成功;若 k 较小,则在左半区继续查找;若 k 较大,则在右半区继续查找。
实现:需要 low、high、mid 三个指针。
判定树:描述折半查找过程的二叉树。树中每个结点对应一个 mid 位置。判定树一定是平衡的二叉排序树。
效率分析(表长 n):
成功 ASL:查找概率相等时,ASL ≈ log₂(n+1) - 1(时间复杂度 O(log n))。
失败 ASL:同样为 O(log n)。
局限性:仅适用于顺序存储的有序表,插入删除困难。
重难点与408真题考点:
判定树的构建与性质:
有 n 个结点的判定树,其树高 h = ⌈log₂(n+1)⌉。
比较次数不超过树高。
给定一个有序序列,必须能画出其对应的判定树,并计算成功/失败 ASL。这是选择题和综合题的常考点。
查找过程模拟:给定序列和关键字,能逐步写出 low、high、mid 的变化过程。
(五) 树形查找
- 二叉搜索树
又称二叉排序树,它或者是一棵空树,或者是具有下列性质的二叉树:
若左子树不空,则左子树上所有结点的值均小于其根结点的值。
若右子树不空,则右子树上所有结点的值均大于其根结点的值。
左、右子树也分别为二叉排序树。
查找:从根开始,比较关键字,小则左查,大则右查。
插入:查找失败的位置即为插入位置。
删除:分三种情况处理(叶子结点、只有一棵子树、有两棵子树)。删除有两棵子树的结点时,通常用其直接前驱(左子树最大)或直接后继(右子树最小)来替代。
效率:平均查找长度取决于树形。最坏情况(树退化成单支树)下,ASL = (n+1)/2,与顺序查找相同。最好情况(树平衡)下,ASL ≈ log₂n。
重难点:BST 的删除操作是重点和难点,必须熟练掌握三种情况的处理逻辑,尤其是用前驱/后继替代后,要递归地删除那个前驱或后继结点。
- 平衡二叉树
为避免 BST 退化成链表,引入平衡因子(BF)的概念。平衡二叉树是一种特殊的 BST,其中每个结点的左右子树高度差的绝对值不超过 1。
平衡因子:BF = 左子树高 - 右子树高,取值范围为 {-1, 0, 1}。
失衡与调整:在插入或删除结点后,可能导致某个结点的 |BF| > 1,此时需要旋转调整以恢复平衡。
四种旋转类型:
LL 型(右单旋):在 A 的左孩子(L)的左子树(L) 插入导致 A 失衡。
RR 型(左单旋):在 A 的右孩子(R)的右子树(R) 插入导致 A 失衡。
LR 型(先左后右双旋):在 A 的左孩子(L)的右子树(R) 插入导致 A 失衡。
RL 型(先右后左双旋):在 A 的右孩子(R)的左子树(L) 插入导致 A 失衡。
插入调整步骤:1) 插入;2) 从插入点向上找第一个失衡结点 A;3) 确定失衡类型并旋转。
高度与结点数关系:设深度为 h 的 AVL 树最少有 N_h 个结点,则 N_h = N_{h-1} + N_{h-2} + 1(类似斐波那契数列),可推导出 h ≈ O(log n)。
重难点与408真题考点:
旋转操作的手动模拟:这是必考的核心。给定插入序列,要求画出每一步的 AVL 树,并在失衡时正确判断类型并旋转。必须清晰记忆四种类型的判别方法和旋转步骤。
删除操作:AVL 树的删除更复杂,删除后可能引起多个祖先结点失衡,需要从删除点的父节点开始向上回溯调整。考研中通常只要求掌握插入调整。
- 红黑树
一种近似平衡的二叉搜索树,通过对结点着色和规则约束,确保没有一条路径会比其他路径长出两倍,从而保证最坏情况下也是高效的。
五大性质:
每个结点是红色或黑色。
根结点是黑色。
叶子结点(NIL 空结点)是黑色。
红色结点的两个子结点都是黑色(即不存在两个连续的红色结点)。
从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点(黑高相同)。
与 AVL 树对比:
平衡标准:AVL 严格平衡(|BF|≤1);红黑树是相对平衡(确保最长路径不超过最短路径的两倍)。
插入删除效率:红黑树的插入删除所需的旋转操作更少(通常不超过3次),性能更稳定。
适用场景:AVL 适合查找密集型;红黑树适合插入删除频繁的场景,如许多语言(Java, C++)的 map/set 底层实现。
重难点:对于 408 考研,红黑树通常不作为代码题要求,但需理解其定义、性质和与 AVL 树的对比。选择题可能考查性质判断或简单插入后的颜色调整方向。
(六) B树及其基本操作、B+树的基本概念
B树
一种多路平衡查找树,专为磁盘等外存设备设计,能有效减少磁盘 I/O 次数。
定义:一棵 m 阶 B 树(m≥3)或为空树,或满足:
每个结点至多有 m 棵子树(m-1 个关键字)。
除根结点外,其他非叶结点至少有 ⌈m/2⌉ 棵子树(即至少 ⌈m/2⌉ - 1 个关键字)。
根结点若非叶子,则至少有两棵子树。
所有叶子结点出现在同一层,且不带信息(可视为查找失败的结点)。
非叶结点结构:(n, P₀, K₁, P₁, K₂, ..., K_n, P_n),其中 K 为关键字且递增,P 为指向子树的指针,且 P_{i-1} 所指子树中所有关键字均小于 K_i,P_i 所指子树中所有关键字均大于 K_i。
核心操作:
查找:多路版的二叉查找。
插入:先查找插入位置(必在某个叶子结点)。若插入后该结点关键字数超过 m-1,则需分裂:将中间关键字上移至父结点,原结点分裂成两个。
删除:更复杂。若删除后结点关键字数少于 ⌈m/2⌉ - 1,则需合并(向兄弟借或与兄弟合并)。
高度与性能:含 N 个关键字的 m 阶 B 树,其高度 h 满足:log_m(N+1) ≤ h ≤ log_{⌈m/2⌉}((N+1)/2) + 1。查找、插入、删除的磁盘 I/O 次数均为 O(h)。
B+树
B 树的变体,更适用于数据库索引和文件系统。
与 B 树的主要区别:
关键字与记录:B 树非叶结点存放关键字及其对应记录的地址;B+树非叶结点仅起索引作用,所有记录都存放在叶子结点中,且叶子结点按关键字大小顺序链接。
关键字重复:B 树关键字不重复;B+树非叶结点的关键字也会出现在叶子结点中(是叶子结点中最大或最小的关键字)。
子树指针数:m 阶 B 树结点有 m 棵子树;m 阶 B+树结点有 m 个关键字和 m 棵子树。
优点:
查询效率更稳定(任何查找都走到叶子层)。
便于范围查询(叶子结点链表)。
磁盘读写代价更低(内部结点更"瘦",可容纳更多关键字)。
重难点与408真题考点:
B 树的插入删除与结点调整:这是最核心的难点。必须掌握分裂和合并的详细规则与过程。给定一个 B 树和一系列插入/删除操作,要求画出每一步的结果,是综合大题的重要题型。
B 树阶数与关键字数的关系:对于 m 阶 B 树,根结点关键字数 1 ≤ n ≤ m-1;非根非叶结点关键字数 ⌈m/2⌉ - 1 ≤ n ≤ m-1。叶子结点(不含失败结点)同样满足此约束。
B+树与 B 树的对比:理解两者在结构、操作和适用场景上的差异,是选择题的常见考点。
(七) 散列(Hash)表
散列表(哈希表)是一种通过散列函数将关键字映射到表中特定位置进行访问的数据结构,其核心目标是实现平均情况下接近 O(1) 的查找、插入和删除效率。
- 基本概念
散列函数:将关键字映射到存储地址的函数,记为 Hash(key) = addr。
冲突:不同的关键字映射到同一散列地址的现象,即 key1 ≠ key2,但 Hash(key1) = Hash(key2)。
同义词:发生冲突的关键字互称为同义词。 - 散列函数的构造方法
设计目标是计算简单、分布均匀。常用方法包括:
直接定址法:H(key) = a * key + b。简单、无冲突,但仅适用于关键字分布基本连续的情况。
除留余数法(最常用):H(key) = key % p。其中,p 通常取不大于表长 m 但最接近或等于 m 的质数,以减少冲突。
数字分析法:选取关键字中分布均匀的若干位作为散列地址。适用于已知关键字集合。
平方取中法:取关键字平方值的中间几位作为散列地址。适用于关键字的各位取值不够均匀。
折叠法:将关键字分割成位数相同的几部分,然后取它们的叠加和作为散列地址。
3. 处理冲突的方法
(1) 开放定址法
当冲突发生时,系统地在散列表中寻找另一个空单元(探测序列)存放记录。通用的再散列函数为:
H_i = (H(key) + d_i) % m,其中 i = 0, 1, 2, ..., k (k ≤ m-1),m 为表长,d_i 为增量序列。
线性探测法:d_i = 1, 2, 3, ..., m-1。易产生"聚集"(堆积)现象,大大降低查找效率。
平方探测法(二次探测法):d_i = 1², -1², 2², -2², ..., ±k² (k ≤ m/2)。可避免"聚集",但要求表长 m 必须是满足 4k+3 的质数。
双散列法:d_i = i * Hash₂(key)。使用两个散列函数,冲突时以第二个散列函数的结果作为步长。
伪随机序列法:d_i 为伪随机数序列。
注意:采用开放定址法时,不能物理删除元素,否则会截断探测路径,只能做删除标记。这会导致表看起来"满"但实际不满,可能需要定期维护。
(2) 拉链法(链地址法)
将所有散列地址相同的记录存储在一个单链表中,散列表的每个单元存储链表头指针。
优点:无堆积现象;易于删除;适合表长不确定的情况。
缺点:指针需要额外空间;若链表过长,性能会下降。
4. 性能分析
散列表的性能取决于三个因素:散列函数、处理冲突的方法和装填因子 α。
装填因子:α = 表中记录数 n / 散列表长度 m。α 越大,表越满,发生冲突的可能性越高,是查找效率的直接影响因素。
平均查找长度:
成功 ASL:查找表中已有元素的平均比较次数。
不成功 ASL:查找表中不存在的元素,直到遇到空单元(开放定址法)或链表结尾(拉链法)的平均比较次数。
效率对比:在等概率查找下,拉链法通常优于线性探测法,而平方探测法性能介于两者之间。
重难点与408真题考点:
ASL的计算:给定散列函数、冲突处理方法、关键字序列和表长,要求计算成功和不成功的 ASL。这是必考题型。计算时需清晰列出每个关键字的比较次数。
构造与模拟:手动模拟给定关键字序列的插入过程,画出最终的散列表形态(尤其是拉链法的链表结构)。
不同冲突处理方法的对比:理解线性探测的聚集问题、平方探测对表长的特殊要求、拉链法的优缺点。
(八) 字符串模式匹配
字符串模式匹配是在主串 S 中定位子串 T(模式串)的过程。
- 简单的模式匹配算法(暴力匹配/BF算法)
从主串 S 的第一个字符起,与模式串 T 的第一个字符比较。若相等,则继续比较后续字符;若不等,则主串回溯到本次匹配起始位置的下一个字符,模式串回溯到开头,重新比较。
时间复杂度:最坏 O(m*n),其中 m 为主串长,n 为模式串长。
2. 改进的模式匹配算法(KMP算法)
KMP 算法利用已匹配的部分信息,通过一个 next 数组,使模式串 T 在匹配失败时能够向右滑动一段有效距离,而主串指针 i 绝不回溯。
核心概念:
前缀:除最后一个字符外,字符串的所有头部子串。
后缀:除第一个字符外,字符串的所有尾部子串。
部分匹配值(PM):字符串的前缀和后缀的最长公共元素长度。
next 数组:next[j] 表示当模式串中第 j 个字符与主串失配时,模式串需要跳转到哪个位置(新的 j) 继续与主串当前字符比较。next[1] 通常为 0(表示主串指针 i 后移一位)。
next 数组的求法(手算步骤):
next[1] = 0。
设 next[j] = k,则 next[j+1] 的计算为:
若 T[k] == T[j],则 next[j+1] = k + 1。
若 T[k] != T[j] 且 k != 0,则令 k = next[k],循环比较。
若 k == 0,则 next[j+1] = 1。
KMP 匹配过程:当 S[i] != T[j] 时,令 j = next[j],i 不变,继续比较。
时间复杂度:O(m+n)。主要时间花在求 next 数组上。
重难点与408真题考点:
next 数组的手动求解:这是核心中的核心。必须熟练掌握上述递推规则,能根据给定的模式串快速写出 next 数组。选择题常考 next 数组的某个值。
next 数组的优化(nextval 数组):当 T[j] == T[next[j]] 时,一次跳转后必然再次失配,因此可以递归地将 next[j] 修改为 next[next[j]],得到 nextval 数组,使匹配更高效。必须掌握 nextval 的求法。
匹配过程模拟:给定主串和模式串,能逐步写出 KMP 算法的匹配过程,包括 i、j 的变化和 next 数组的使用。
(九) 查找算法的分析及应用
- 各类查找算法的对比与分析
查找算法 数据结构要求 时间复杂度(平均) 时间复杂度(最坏) 优点 缺点
顺序查找 无序/有序线性表 O(n) O(n) 简单,对存储无要求 效率低
折半查找 有序顺序表 O(log n) O(log n) 效率高 要求有序且顺序存储,插入删除困难
分块查找 块内无序、块间有序 索引顺序: O(√n) - 动态结构,插入删除易 需额外索引空间,有退化风险
二叉搜索树 二叉排序树 O(log n) O(n) 结构简单,适合动态查找 性能不稳定,可能退化成链
平衡二叉树 AVL树 O(log n) O(log n) 稳定高效,查找性能最好 插入删除调整复杂,开销大
散列查找 散列表 O(1) O(n) 理想情况下效率极高 冲突影响性能,需好散列函数 - 综合应用与选择原则
静态查找表(数据不变):若关键字有序,首选折半查找;若无序,可考虑顺序查找或先排序再折半。
动态查找表(频繁插入删除):
若对查找性能要求极高,且插入删除不特别频繁,可选平衡二叉树(AVL)。
若插入删除非常频繁,且对性能有综合要求,红黑树是更佳选择(如系统级Map/Set实现)。
若关键字范围已知且分布较集中,可考虑散列表,追求平均O(1)时间。
外存查找(文件系统、数据库):数据量巨大,必须减少磁盘I/O。B树/B+树是多路平衡树,树矮,磁盘访问次数少,是索引的标准结构。
字符串匹配:
一次性的简单匹配可用 BF 算法。
主串很大、模式串固定且需多次匹配,或对效率要求高时,必须使用 KMP 算法。
重难点与408真题考点:
场景化算法选择:给出一组数据特征和操作需求(如"学生学号查询,学号范围已知,需频繁插入删除"),要求选择最合适的查找结构并说明理由。这需要综合对比各类结构的优缺点。
性能理论分析:结合装填因子分析散列表的ASL变化趋势;根据B树的阶数估算其高度和查找次数;分析不同输入序列下BST的形态和ASL。
七、排序
排序是将一组任意序列的数据元素按特定关键字重新排列成有序序列的过程,是计算机科学中最基础、应用最广泛的算法之一。本章将系统阐述各类排序算法,并结合考研真题深度剖析其原理、实现与性能。
(一) 排序的基本概念
排序定义:将一组记录按关键字递增或递减的次序重新排列。
排序算法的稳定性:若待排序表中存在两个关键字相等的记录 R_i 和 R_j,排序后它们的相对次序保持不变,则称该排序算法是稳定的;否则为不稳定。
内部排序与外部排序:
内部排序:排序期间所有待排序记录都存放在内存中。
外部排序:排序期间记录太多,无法全部放入内存,需要在内存和外存之间进行多次数据交换。
排序算法的性能评价:主要从时间复杂度、空间复杂度和稳定性三个方面衡量。时间复杂度通常关注比较次数和移动(交换)次数。
重难点:理解稳定性的概念是分析比较不同排序算法的关键前提,也是408选择题的常见考点。需明确算法稳定与否取决于其具体实现,而非算法类别本身。
(二) 直接插入排序
基本思想:将待排序的记录逐个插入到前面已经排好序的有序序列中,直到全部记录插入完成。
过程模拟:初始时将第一个记录视为有序序列,从第二个记录开始,将其与前面有序序列从后向前比较,找到合适位置插入,原位置及之后的记录后移。
性能分析:
空间复杂度:O(1),仅使用常数个辅助单元。
时间复杂度:
最好情况(初始有序):比较次数 n-1,移动次数 0,时间复杂度 O(n)。
最坏情况(初始逆序):比较和移动次数均为 O(n²)。
平均时间复杂度:O(n²)。
稳定性:稳定。因为是从后向前比较,遇到相等元素时停止移动,不会改变相对次序。
适用性:适用于基本有序或数据量较小的序列。
重难点与408考点:掌握手动模拟插入过程,并能准确计算特定序列下的比较和移动次数。
(三) 折半插入排序
基本思想:在直接插入排序的基础上进行优化。寻找插入位置时,对前面已排好序的有序子序列使用折半查找,从而减少比较次数。
与直接插入排序的区别:仅减少了比较次数,记录的移动次数并未减少。因为找到插入位置后,仍需将插入位置之后的元素后移。
性能分析:
时间复杂度:平均比较次数降为 O(n log n),但移动次数仍为 O(n²),因此平均时间复杂度仍为 O(n²)。
稳定性:稳定。
重难点:理解折半插入排序只是优化了"查找"插入点的过程,并未优化"移动"过程,因此其时间复杂度阶数未变。
(四) 起泡排序
基本思想:从后往前(或从前往后)两两比较相邻元素的值,若为逆序则交换,直到序列比较完。这样每一趟会将一个最小(或最大)元素"冒泡"到其最终位置。
优化:设置标志位 flag,若某一趟未发生交换,说明序列已有序,可提前结束排序。
性能分析:
空间复杂度:O(1)。
时间复杂度:
最好情况(初始有序):一趟比较 n-1 次,无交换,时间复杂度 O(n)。
最坏情况(初始逆序):需 n-1 趟,比较和交换次数均为 O(n²)。
平均时间复杂度:O(n²)。
稳定性:稳定。因为只有相邻元素逆序时才交换,相等时不交换。
重难点:掌握每趟排序后序列的变化,以及优化后提前终止的条件。
(五) 简单选择排序
基本思想:第 i 趟排序在待排序序列 L[i..n] 中选择关键字最小(或最大)的记录,与第 i 个位置的记录交换。
性能分析:
空间复杂度:O(1)。
时间复杂度:无论序列初始状态如何,都必须进行 n-1 趟选择,总比较次数为 n(n-1)/2,即 O(n²)。移动次数较少,最好为 0,最坏为 3(n-1)。
稳定性:不稳定。例如序列 (2, 2*, 1),第一趟选择最小元素 1 与第一个 2 交换,导致两个 2 的相对次序改变。
重难点:理解其不稳定的原因,并与直接插入排序、冒泡排序进行对比。
(六) 希尔排序
基本思想:将待排序表分割成若干形如 L[i, i+d, i+2d, ..., i+kd] 的"子表",对各个子表分别进行直接插入排序。然后缩小增量 d,重复上述过程,直到增量 d=1,即对整个表进行一次直接插入排序。
核心:通过增量序列将相距较远的元素先进行粗略排序,使得序列逐步趋于基本有序,从而减少直接插入排序的工作量。
性能分析:
空间复杂度:O(1)。
时间复杂度:依赖于增量序列的选择,分析复杂。当增量序列为 {n/2, n/4, ..., 1}(希尔增量)时,最坏时间复杂度为 O(n²)。使用某些优化增量(如Hibbard增量)可达到 O(n^{1.5})。
稳定性:不稳定。由于是跳跃式移动,可能改变相同关键字的相对次序。
重难点与408考点:理解希尔排序是插入排序的改进,其"分组插入"的思想。给定增量序列和初始序列,能手动模拟每一趟排序后各子表及总表的状态。
(七) 快速排序
基本思想:基于分治法。任取一个元素 pivot(通常取首、尾或中间元素)作为枢轴,通过一趟排序将待排序表划分为独立的两部分 L[1..k-1] 和 L[k+1..n],使得左边所有元素 ≤ pivot,右边所有元素 ≥ pivot。pivot 则放在其最终位置 L[k] 上。然后递归地对两个子表重复上述过程。
划分过程(Partition):是算法的核心,需熟练掌握。常用"挖坑填数"或"指针交换"法。
性能分析:
空间复杂度:递归栈的深度。平均 O(log n),最坏(有序或逆序)O(n)。
时间复杂度:
最好/平均情况:每次划分均匀,递归树平衡,时间复杂度 O(n log n)。
最坏情况:每次划分极不均匀(如初始有序),递归树退化成链,时间复杂度 O(n²)。
稳定性:不稳定。在划分过程中,跨区域的交换可能破坏稳定性。
优化:随机选取枢轴、三数取中法选取枢轴,以避免最坏情况。
重难点与408真题考点:
手动模拟递归排序过程:这是必考题型。需写出每一趟划分后的序列状态及枢轴的最终位置。
时间复杂度分析:结合具体序列分析快排的性能,理解其性能依赖于枢轴的选择和划分的均衡性。
(八) 堆排序
基本思想:利用堆这种数据结构进行排序。首先将待排序序列构造成一个大顶堆(升序)或小顶堆(降序)。此时,堆顶元素即为最大(或最小)值。将其与堆尾元素交换,然后将剩余 n-1 个元素重新调整成堆,如此反复,直到有序。
关键操作:
建堆:从最后一个非叶结点开始,自底向上、自右向左进行向下调整。时间复杂度 O(n)。
调整堆:交换堆顶与堆尾元素后,对新的堆顶进行一次向下调整。时间复杂度 O(log n)。
性能分析:
空间复杂度:O(1)。
时间复杂度:建堆 O(n) + 每次调整 O(log n),共调整 n-1 次,因此总时间复杂度为 O(n log n)。且最好、最坏、平均情况均为 O(n log n)。
稳定性:不稳定。例如序列 (1, 2*, 2),建大顶堆后交换,会导致 2* 和 2 的相对次序改变。
重难点与408真题考点:
建堆与调整堆的手动模拟:给定序列,要求画出初始建堆后的完全二叉树形态,并逐步写出每趟排序(交换堆顶与堆尾、调整)后的序列状态。
堆排序与堆插入删除的区别:堆排序是"原地"排序,通过交换和调整实现;而优先队列的插入删除涉及堆结构的动态维护。
(九) 二路归并排序
基本思想:基于分治法。将长度为 n 的序列视为 n 个长度为 1 的有序子表,然后两两归并,得到 ⌈n/2⌉ 个长度为 2 或 1 的有序子表;再两两归并,如此重复,直到合并成一个长度为 n 的有序表为止。
核心操作:合并两个有序表。需要与待排序序列等大小的辅助数组。
性能分析:
空间复杂度:O(n),主要来自辅助数组。
时间复杂度:每趟归并时间复杂度为 O(n),共需进行 ⌈log₂n⌉ 趟归并,因此总时间复杂度为 O(n log n)。
稳定性:稳定。关键在于合并操作中,当遇到相等元素时,优先将前一子表的元素放入辅助数组。
重难点:掌握手动模拟归并排序的过程,理解其"先分解、后合并"的递归思想,以及需要额外 O(n) 辅助空间的特点。
(十) 基数排序
基本思想:不基于比较,而是基于关键字各位的大小进行排序。分为最高位优先和最低位优先两种方法。通常使用 LSD(最低位优先)法。
过程:假设关键字是 d 位 r 进制数。建立 r 个队列。从最低位开始,依次按关键字当前位的值将记录分配到这 r 个队列中,然后按队列顺序收集记录,形成按当前位有序的新序列。重复此过程,直到最高位。
性能分析:
空间复杂度:需要 r 个队列,每个队列最多 n 个元素,因此空间复杂度为 O(n+r)。
时间复杂度:进行 d 趟分配和收集,每趟分配 O(n),收集 O(r),因此总时间复杂度为 O(d(n+r))。
稳定性:稳定。因为分配和收集过程是队列操作,先进先出,保证了相同关键字的相对次序。
重难点与408考点:给定一组多位数或字符串,要求手动模拟 LSD 基数排序的每一趟分配和收集过程。理解其"非比较"和"多关键字"排序的特性。
(十一) 外部排序
问题背景:待排序文件过大,无法一次性装入内存。
基本过程:
生成初始归并段:根据内存工作区大小,每次读入一部分记录,用内部排序方法(如快排、堆排)排好序后,写回外存,形成一个有序的子文件(归并段)。
多路归并:将多个归并段逐趟归并,最终得到完整的有序文件。
核心优化------减少磁盘I/O次数:
多路平衡归并:增加归并路数 k,可以减少归并趟数 S = ⌈log_k r⌉(r 为初始归并段数),从而减少 I/O。但 k 增大会增加内部归并的比较次数。
败者树:一种树形选择排序结构,可在 k 个归并段中选出最小元素时,将关键字比较次数从 k-1 降至 ⌈log₂k⌉,从而支持更大的 k 而不显著增加比较开销。
置换-选择排序:用于生成更长的初始归并段,从而减少初始归并段数量 r,间接减少归并趟数。
最佳归并树:考虑初始归并段长度不等的情况,构造一棵哈夫曼树作为归并顺序,可以使总的 I/O 次数最少。
重难点与408真题考点:
计算磁盘I/O次数:给定缓冲区大小、记录数、磁盘块大小等参数,计算总读写次数。
败者树的工作原理:理解败者树如何维护和更新,能模拟其调整过程。
最佳归并树的构造:给定各归并段长度,能构造哈夫曼树形式的最佳归并树,并计算带权路径长度(即总的读写记录数)。
(十二) 排序算法的分析和应用
- 综合对比
排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 适用场景
直接插入 O(n²) O(n²) O(1) 稳定 小规模或基本有序
折半插入 O(n²) O(n²) O(1) 稳定 同直接插入,比较略少
起泡排序 O(n²) O(n²) O(1) 稳定 教学或小规模
简单选择 O(n²) O(n²) O(1) 不稳定 移动次数少,但不稳定
希尔排序 O(n^{1.3}) O(n²) O(1) 不稳定 中等规模,不稳定
快速排序 O(n log n) O(n²) O(log n) 不稳定 大规模,内部排序首选
堆排序 O(n log n) O(n log n) O(1) 不稳定 大规模,对最坏情况有要求
归并排序 O(n log n) O(n log n) O(n) 稳定 大规模,要求稳定,可外排
基数排序 O(d(n+r)) O(d(n+r)) O(n+r) 稳定 多关键字,位数少且范围小
- 应用选择原则
数据规模小或基本有序:直接插入排序简单有效。
数据规模中等,对稳定性无要求:希尔排序是较好的选择。
数据规模大,追求平均性能:快速排序是通用首选,但需注意其最坏情况。
数据规模大,且要求最坏情况性能保证:堆排序。
数据规模大,且要求稳定排序:归并排序,但需要额外空间。
数据为多关键字结构(如日期、字符串):基数排序。
数据量极大,内存无法容纳:必须使用外部排序,核心是多路归并和减少I/O。
重难点:能够根据具体问题描述(数据特征、规模、稳定性要求、空间限制等),综合对比上表,选择最合适的排序算法并阐述理由。这是408综合应用题的重要考查形式。