
简介《数据结构》是计算机科学的核心课程这份由山东大学课堂内容整理而成的PDF讲义面向计算机专业学生、考研复习者及自学数据结构的学习者重点解决对基本概念、逻辑结构与存储结构、算法分析等基础知识的系统梳理。资源为1个PDF文件压缩包仅324KB内容精炼便于随时查阅与打印。文件围绕绪论与线性表两大部分展开绪论部分系统讲解数据、数据元素、数据项、数据对象等基本术语集合/线性/树形/网状四种逻辑结构顺序、链式、散列、索引四种存储方式并详细说明算法的五个重要特性、描述方式以及时间复杂度和空间复杂度的分析方法线性表部分则覆盖其定义、基本操作着重对比顺序存储与链式存储的实现原理、插入删除等操作的平均效率差异帮助读者建立从逻辑设计到物理实现再到算法评估的完整认知。目前已有113人学习下载适合作为课堂笔记补充、考前速记或第一轮入门复习的手册型资料。1. 山东大学-数据结构.pdf一份PDF背后的复习路线之争不少考研党的网盘里都躺着一份“山东大学-数据结构.pdf”它是考研数据结构这一科的高频复习资料把线性表、栈与队列、树与二叉树、图、排序查找这些核心考点收拢成一条主线。它能解决的核心问题就一个让目标明确的人——考山东大学计算机相关专业或者想用山大自命题风格练手的人——把有限的复习时间花在真正会考的地方。但这里有个反直觉的结论光靠把这本PDF从头翻到尾、在上面划重点是考不过的。山大自命题数据结构最拉分的是算法设计题要求手写完整且能运行的C代码PDF里大量类C伪代码如果不亲手调通上了考场连链表反转这样基础的操作都可能写岔。这份资料真正有用的打开方式是带着“能不能跑起来”的标准把它读薄。2. 先把“数据结构”的范围圈死山大考点与408考纲的差异拿到PDF先别急着顺着章节目录往下读。数据结构这门课内容看着就八章但不同考试对深度的要求差别非常大。山大计算机相关专业的考研专业课常见路线是自命题数据结构不考操作系统、计算机组成原理和网络这跟统考408的思路完全不同。408的数据结构只是四科之一考查范围广但单点深度有限山大自命题把数据结构单独拎出来考意味着可以在这一门上砸更多时间也意味着算法题考查得更细、更偏代码实现。这会导致一个很现实的问题如果你前期是照着408的思路复习数据结构的比如花大量时间刷选择题技巧、背时间复杂度结论转到山大风格时就会明显不适应。山大的大题经常要求你直接写一个完整的函数包括结构体定义、参数设计、边界条件处理而不是选一个正确选项。我见过不少同学做408真题时选择题正确率很高一上来自命题的算法设计题就卡壳根因就是复习重心放错了。2.1 山大自命题与408的差异复习深度的风向标先把两类考试的差异摆成一张对比表方便你定位自己的复习坐标系。对比维度408统考数据结构山大自命题数据结构考试范围数据结构占四科之一覆盖大纲全部章节数据结构单科范围相对集中题型构成选择题 大题含一道算法设计选择/填空 简答 算法设计题算法题风格代码量适中偏重思路代码量更大常要求完整可运行的函数高频重点概念、复杂度、基础算法链表操作、树与二叉树、图遍历、排序复习策略技巧优先刷选择题提分快代码优先手写实现能力决定上限这张表不是我编出来的应试玄学而是两类考试近几年出题风格的共性总结。408的选择题占比高很多知识点靠“认得”就能拿分自命题学校则更愿意在算法设计题上拉开区分度因为代码题无法蒙会写就是会写不会写就是空白。结论很简单如果你目标是山大复习重心必须从“看懂”转向“能默写”。从时间分配上看我一般会建议把一半以上的复习时间压在线性表、栈与队列、二叉树、图这四块上因为它们的代码题密度最高。排序和查找作为第二梯队要熟练掌握每一趟的过程和代码框架串、数组与广义表放到最后理解核心概念、会做选择题即可。2.2 用大纲倒推PDF重点哪些章节精读、哪些只做选择题拿到PDF后第一件事是动手把目录抄到一张A4纸上然后对照一轮真题把考察频率标注出来。这个过程花不了一下午但它会直接决定你后面几个月的节奏避免在低频章节上浪费大力气。我自己的分法是三层。第一层精读精练线性表、栈与队列、树与二叉树、图。这四个章节不能只做题必须做到能独立写出完整代码、能默写核心算法、能答出边界条件。第二层会做题查找重点是折半查找、二叉排序树、哈希表构造与冲突处理排序重点是快排、归并、堆排的实现和每趟过程稳定性与复杂度要张口就来。第三层扫读串、数组与广义表。这两章考察重心在概念题比如KMP的next数组计算、特殊矩阵的压缩存储推导不值得花大量时间写完整串匹配代码。为什么这样分拿串举例。算法设计题里几乎不会让你单独写一个KMP匹配函数它最多作为一道选择题或简答题出现让你手工算next数组。数组与广义表和它类似重点在地址计算和压缩映射你花一晚上把稀疏矩阵三元组表完整实现一遍考试用到的概率很低。图却完全相反邻接矩阵和邻接表的建图代码、DFS、BFS、拓扑排序、最小生成树、最短路径每一个都可能被改编成大题。还有个更实操的验证方法面对PDF里每一章的章末习题只看“算法设计题”一栏。如果一道题你拿起笔10分钟内写不出完整函数框架说明这一章的代码能力没过关回头练到能默写为止。这个标准比“我好像看懂了”靠谱得多因为考场上你只有一支笔和一张答题卡。3. 按“代码优先”的顺序过一遍C语言版数据结构的最小可运行方案判断数据结构复习是否到位的唯一标准是你能不能在编译器里把代码跑通而不是能不能看懂书上的伪代码。考研手写代码最稳妥的载体是C语言王道、严蔚敏《数据结构C语言版》以及大话数据结构里的代码框架基本都是C或类C。用Python、Java刷题手感虽然好但手写代码上考场时C的指针操作和结构体定义才是阅卷老师最熟悉的答案形态。所以这一章我按数据结构考研最常见的顺序给你一套从环境到核心数据结构的“最小可运行方案”。每个代码块都直接复制就能编译关键函数后我会解释设计原因和参数含义。3.1 环境准备Win/Mac 上用 GCC 跑通第一个 C 程序不管是Windows、Mac还是Linux第一步都是在命令行里确认C编译器可用。Windows装MinGW-w64或者装Visual Studio时勾选“使用C的桌面开发”里的MSVC都行Mac自带clangLinux自带gcc。装好后先跑一个最小程序# 把下面三行存成 hello.c然后依次执行 gcc -g -Wall -o hello hello.c ./hello # 程序里只需要一个 main 函数printf(hello\n); return 0;这里几个参数说明一下。-g是把调试信息写进可执行文件后续用gdb或printf定位段错误、野指针时断点信息才能对上行号-Wall把常见警告全部打开比如变量未使用、隐式类型转换这些警告能帮你在早期拦住很多隐蔽问题。我一般调试数据结构代码不依赖复杂IDE而是直接在代码里插入printf打印关键变量配合编译器警告逐个击破所以环境越轻越好。3.2 带头结点的单链表从初始化到删除结点的全套代码链表是所有代码题的基础也是翻车重灾区。下面的代码把带头结点单链表从初始化到按值删除完整实现一遍。#include stdio.h #include stdlib.h typedef struct LNode { int data; // 数据域 struct LNode *next; // 指针域指向下一个结点 } LNode, *LinkList; // 初始化建立头结点next 置空 void initList(LinkList *L) { *L (LNode *)malloc(sizeof(LNode)); if (*L NULL) exit(1); // 分配失败直接退出 (*L)-next NULL; } // 头插法新结点插到头结点之后 void insertHead(LinkList L, int x) { LNode *s (LNode *)malloc(sizeof(LNode)); s-data x; s-next L-next; // 顺序一先让新结点指向旧首元结点 L-next s; // 顺序二再让头结点指向新结点 } // 按值删除第一个出现的结点删除成功返回 1 int deleteByValue(LinkList L, int x) { LNode *pre L, *p L-next; while (p ! NULL p-data ! x) { pre p; p p-next; } if (p NULL) return 0; // 没找到 pre-next p-next; // 先接前驱跳到后继 free(p); // 后断释放被删结点 return 1; } void printList(LinkList L) { LNode *p L-next; while (p ! NULL) { printf(%d , p-data); p p-next; } printf(\n); } int main() { LinkList L; initList(L); insertHead(L, 3); insertHead(L, 2); insertHead(L, 1); printList(L); // 输出1 2 3 deleteByValue(L, 2); printList(L); // 输出1 3 return 0; }逻辑说明为什么坚持带头结点因为空表和非空表的插入删除操作能统一不用在main里写一堆if判断“是不是空表”。头结点的data域闲置只当作哨兵用换来的是代码逻辑大幅简化。删除函数里必须维护一个pre前驱指针因为单链表只有后继指针要删p就得先拿到它的前驱再把pre-next指向p-next最后free(p)这个“先接后断”的顺序不能反。参数说明initList需要传LinkList*也就是指针的指针因为malloc出来的头结点地址要写回到调用者的L变量里insertHead和deleteByValue直接传头指针就行因为它们只修改结点内部的next指向不修改头指针本身。main函数里测试的输出顺序验证了头插法的特性后插入的元素排在前面。3.3 栈与队列数组实现与链表实现各写一遍栈和队列是后续二叉树层序遍历、表达式求值、图遍历的基础建议数组实现和链表实现都亲手写一遍。先看顺序栈#define MaxSize 100 typedef struct { int data[MaxSize]; int top; // 栈顶下标空栈时为 -1 } SqStack; // 入栈栈满返回 0成功返回 1 int push(SqStack *s, int x) { if (s-top MaxSize - 1) return 0; s-data[s-top] x; // 先移动 top再存数据 return 1; } // 出栈栈空返回 0成功返回 1 int pop(SqStack *s, int *x) { if (s-top -1) return 0; *x s-data[s-top--]; // 先取数据再减小 top return 1; }参数说明top初始为-1表示栈里没有元素入栈执行top出栈执行top--top始终指向栈顶元素的位置。仔细对比push和pop里top移动的先后一个先加后存一个先取后减这个细节是后续手写代码最常见的丢分点。再看循环队列它比栈多一个“牺牲一个存储单元”的经典设计#define MaxSize 10 typedef struct { int data[MaxSize]; int front, rear; // front 指向队头rear 指向队尾的下一个位置 } SqQueue; // 入队队满返回 0成功返回 1 int enQueue(SqQueue *q, int x) { if ((q-rear 1) % MaxSize q-front) return 0; // 队满判断 q-data[q-rear] x; q-rear (q-rear 1) % MaxSize; // 尾巴循环后移 return 1; } // 出队队空返回 0成功返回 1 int deQueue(SqQueue *q, int *x) { if (q-front q-rear) return 0; // 队空判断 *x q-data[q-front]; q-front (q-front 1) % MaxSize; return 1; }为什么循环队列必须空一个位置因为队空和队满都可能是frontrear如果不空一个位置就无法区分这两种状态。队空条件是frontrear队满条件是(rear1)%MaxSizefront这个取模操作保证了front和rear在数组范围内环形移动。后期做二叉树层序遍历时辅助队列用的就是这个结构所以现在把它调通后面直接复用。4. 排序算法与树的实现把PDF里的伪代码变成能跑的C代码排序和树是算法大题的高发区也是PDF里伪代码密度最高的地方。伪代码不是不能跑而是省略了大量实现细节——递归边界、指针判空、返回值处理、中间变量初始化。这一章把快排和二叉树的核心代码补齐到可直接运行的程度。4.1 快速排序的基准选取为什么PDF版本会栈溢出很多PDF版本里的快排基准直接取a[low]。这个写法简洁但有个致命问题对已经有序的数组每次划分只移除一个元素递归深度达到n栈直接溢出。用“三数取中”可以缓解退化成O(n²)的情况。#include stdio.h // 三数取中把 low、mid、high 三个位置的元素排好序返回 mid 的下标 int medianOfThree(int a[], int low, int high) { int mid low (high - low) / 2; if (a[low] a[mid]) { int t a[low]; a[low] a[mid]; a[mid] t; } if (a[low] a[high]) { int t a[low]; a[low] a[high]; a[high] t; } if (a[mid] a[high]) { int t a[mid]; a[mid] a[high]; a[high] t; } return mid; } // 划分挖坑填数法返回基准元素的最终位置 int partition(int a[], int low, int high) { int idx medianOfThree(a, low, high); int pivot a[idx]; // 把基准换到 low 位置腾出 a[low] 当坑 int t a[low]; a[low] a[idx]; a[idx] t; while (low high) { while (low high a[high] pivot) high--; a[low] a[high]; // 右边小于 pivot 的填到左边坑 while (low high a[low] pivot) low; a[high] a[low]; // 左边大于 pivot 的填到右边坑 } a[low] pivot; // 基准归位 return low; } // 递归边界必须写 low high否则死循环或栈溢出 void quickSort(int a[], int low, int high) { if (low high) return; int pos partition(a, low, high); quickSort(a, low, pos - 1); // 左半闭区间 quickSort(a, pos 1, high); // 右半闭区间 }逻辑说明partition里内层的两个while都要带上low high条件否则high和low会互相穿过导致越界访问。基准先挖走相当于数组里默认留下一个坑先从右往左找小于pivot的值填坑再从左往右找大于pivot的值填坑最后把pivot放回low和high相遇的位置。参数说明low和high是闭区间下标递归边界low high表示当前区间没有元素或只有一个元素必须放在函数第一行。4.2 二叉树层序遍历与递归非递归互换二叉树这块PDF里画图很清晰但代码往往只给递归版本。考试要求你写非递归版本时很多人就卡住了。这里用数组模拟栈和队列避免手写链表结构引入额外变量。#include stdio.h #include stdlib.h typedef struct BiTNode { int data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; // 用数组队列保存结点地址容量 100 练习题足够 typedef struct { BiTree data[100]; int front, rear; } Queue; // 层序遍历从上到下、从左到右 void levelOrder(BiTree root) { if (root NULL) return; Queue q { .front 0, .rear 0 }; q.data[q.rear] root; // 根结点先入队 while (q.front ! q.rear) { BiTree cur q.data[q.front]; // 出队一个 printf(%d , cur-data); if (cur-lchild) q.data[q.rear] cur-lchild; if (cur-rchild) q.data[q.rear] cur-rchild; } } // 非递归中序遍历数组栈模拟系统调用栈 void inOrderNoRec(BiTree root) { BiTree stack[100]; int top -1; BiTree p root; while (p ! NULL || top ! -1) { while (p ! NULL) { // 一路向左全部压栈 stack[top] p; p p-lchild; } if (top ! -1) { p stack[top--]; // 左走到底出栈访问 printf(%d , p-data); p p-rchild; // 转向右子树 } } }逻辑说明层序遍历的核心是“出队一个、访问、左右孩子入队”队列保证每一层从左到右再逐层向下。这里用线性数组队列front和rear直接后移不需要循环取模因为二叉树层序最多同时入队的结点数不会超出数组容量练习题100个结点绰绰有余。非递归中序的外层循环条件“p ! NULL || top ! -1”覆盖了两种情况要么当前结点不为空需要继续往左要么栈里有等待访问的结点。参数说明递归转非递归的通用思路是用栈保存“还没处理完的上下文”每次把左链全部压栈弹出一个访问再转右子树这个模式吃透后前序、后序、DFS都能套。5. 避坑山大版数据结构复习中最常见的6个翻车点这章写点血泪经验。数据结构复习的坑很集中基本都是代码能力没跟上、边界条件没处理导致的。每条我按“现象→原因→解决”写你对照自查能省下大量无效调试时间。5.1 现象PDF里的代码抄下来编译报错报错信息看不懂原因资料里大量代码是类C伪代码省略了#include、结构体定义、返回值类型甚至省略了malloc的头文件stdlib.h。解决抄代码时先把头文件和typedef补全再逐个函数编译。如果报“未声明的标识符”优先检查函数用到的结构体是否定义在前面如果报“不可达代码”多半是if或while后面直接跟了return括号范围盖住了后续逻辑。5.2 现象链表题一写就断链打印出来元素少一半原因指针修改顺序反了。典型错误是插入时先把前驱的next指向新结点再让新结点的next指向原来的后继结果原后继彻底丢失或者删除时先free(p)再想拿p-next直接读到野指针。解决画图。每个操作先画出“从哪个箭头断开、从哪个箭头接上”再落代码。插入口诀是“先接后断”删除口诀是“先跳过后继再释放当前结点”。5.3 现象二叉树递归代码思路对一运行栈溢出或空指针崩溃原因递归边界写错。常见坑是拿叶子结点当终止条件写成if (p-lchild NULL p-rchild NULL)就return这会让空指针传进函数后继续访问p-lchild。解决所有二叉树递归函数第一行固定写if (p NULL) return;先保证空指针安全再写业务逻辑。这个习惯能挡住八成以上二叉树代码崩溃。5.4 现象图算法看得懂真让写完整代码就懵不知道从哪下手原因图的存储结构没单独练。邻接矩阵和邻接表的建图代码PDF里通常只给示意片段很多同学跳过建图直接看DFS、BFS结果读懂了遍历过程但写不出函数签名。解决先花一个下午把邻接矩阵的“建图DFSBFS”完整敲一遍再换成邻接表敲一遍。注意函数签名要写全比如void DFS(int v, int visited[])把visited数组显式传进去不要依赖全局变量。5.5 现象排序的稳定性、复杂度记混代码和结论对不上号原因只看不跑没有建立“代码过程”和“结论”的联系。快排为什么不稳定因为partition里基准会和远处的元素交换相同元素的相对顺序可能被打乱——这个结论不亲手跟踪一趟划分过程很难内化。解决写一个随机数组生成器把冒泡、快排、归并每趟排序后的数组print出来观察相同值的相对位置变化。跑三次稳定性结论自然就记住了比死背口诀可靠。5.6 现象真题算法题看着会写模拟考一紧张就写岔指针满天飞原因平时用IDE写代码自动补全和编译纠错把问题提前挡住了手写代码的手感没建立起来。考场上是白纸黑字没有编译器提示。解决复习后期每天抽20分钟在纸上默写一个核心算法比如链表原地反转、快排、二叉树非递归中序遍历默写完再对照PDF里的框架逐行核对。坚持两周手写速度和心态都会明显变化。6. 复习后期怎么把PDF用出“后悔药”效果真题复盘与算法默写前期按章节过代码后期PDF的角色要变它不再是一本从头读到尾的书而是一本查漏字典。不用等学完所有章节再开始真题我自己的节奏是第二轮复习就穿插真题每做一套都把涉及的知识点在PDF目录旁边做一个记号练到后期一眼就能看出哪些章节是重灾区。建议把下面几个高频算法整理成一张默写清单每天挑一个限时完成算法建议限时检查重点带头结点链表原地反转15分钟三指针移动顺序二叉树非递归中序遍历20分钟栈的进出时机快速排序含三数取中15分钟递归边界与划分二叉树层序遍历10分钟队列判空条件DFS / BFS邻接矩阵版20分钟visited数组传递真题复盘时不要只看对错。把错题映射到PDF里的具体知识点一道拓扑排序大题做错了回到图这一章把入度表更新和队列结构的关系重新画一遍一道链表逆置综合题卡住回到线性表章节把头插法代码再默写一遍。这样做两套真题PDF的目录就会被你标出一张“高频考点热力图”后续冲刺就只看这些地方。我自己当年最亏的事就是只顾着“看懂”。看视频、看资料都觉得简单一合上书写链表反转纸上画了三遍才把指针顺序理清。后来改成每天早饭后默写一个算法坚持两周手感和心态都稳了。数据结构这门课别人讲一万遍不如自己跑通一遍希望帮到你。本文还有配套的精品资源点击获取