简介:严蔚敏《数据结构与算法(C语言版)》配套代码实现合集,主要面向计算机专业学生、考研复习者与需要补算法基础的C语言开发者。资源以教材章节为主线,覆盖线性表、栈、队列、二叉树、图、查找、排序、递归与分治等核心主题;每个主题不仅有可编译的C/C++代码,还配有头文件、测试数据和txt运行说明,学习时可直接运行比对结果,适合边读教材边逐段调试。压缩包共416个文件,包括154个cpp、154个c和89个h,另含txt说明、dat测试文件及少量工程辅助文件,整体仅494KB,轻量且目录清晰。已有1607人学习下载。代码把结构体定义、指针操作、递归调用、图论遍历与经典算法流程转化为可运行实例,能帮助读者绕过理解障碍,在复习备考或课程上机时快速定位实现细节,是一份非常实用的配套学习资源。
1. 数据结构与算法 C 语言的代码实现:为什么照着教材敲会翻车,以及怎么救
数据结构与算法 C 语言这套东西,看起来是一门课,真正落到“代码实现”时却最容易翻车。不少同学拿到严蔚敏版《数据结构(C语言版)》,照着书上的顺序表敲进 VSCode,编译直接报错:Status 没定义、ElemType 不存在,算法描述里的“引用参数”在纯 C 语言里也没有对应语法。原因在于教材给的是抽象数据类型的描述,不是能直接运行的源码。这篇笔记把链路拆开讲:怎么配编译环境、怎么补公共骨架、怎么把线性表、树、图和排序查找写成能调试的程序,最后排掉最常见的几个坑。适合正在赶数据结构实验报告、准备数据结构期末复习或考研数据结构的人,按顺序走一遍,后面自己写代码会顺手很多。
2. 编译环境与公共骨架:先让 Status 和 ElemType 立起来
2.1 VSCode 配 GCC:用一条命令把第一个程序跑起来
做数据结构代码实现,最常见的阻碍其实不是算法本身,而是环境。网络上搜“vscode c语言环境配置”能找到大量教程,但很多教程把精力花在 tasks.json、launch.json 的界面配置上,反而把新手绕晕。我一般不建议一开始就用 IDE 的图形化编译按钮,直接在终端用 gcc 编译,既能看到完整报错,也能把编译参数控在自己手里。环境上只要装好 MinGW-w64,并在系统 PATH 里加好 bin 目录即可。
# 编译一个单文件的数据结构程序 gcc -std=c11 -g -Wall -Wextra -fsanitize=address -o dsalgo main.c # 带地址消毒器运行,越界和野指针会直接报错并退出 ./dsalgo # 程序崩了之后,用 gdb 看调用栈 gdb ./dsalgo run bt这里的参数逐个说明:-g生成调试信息,崩了之后 gdb 才能定位到行号;-Wall -Wextra把可疑写法全部警告出来;-fsanitize=address是地址消毒器,能直接检测越界、野指针、栈溢出,对学习数据结构是刚需。之前带过的同学经常遇到“程序跑一会儿就崩,但不知道崩在哪”,用这一条命令跑一遍,哪个函数哪一行越界一目了然,根本不用靠“玄学”猜。 这一阶段坚持单文件工程就好。教材里的代码段都很短,单文件便于交数据结构实验报告;多文件在 IDE 里配置 include 路径反而容易出无关问题。另外,如果在 Windows 控制台里 printf 中文乱码,可以在编译命令里加-fexec-charset=GBK,或者统一让程序只输出 ASCII 字符。
2.2 公共骨架:把 Status、ElemType、OVERFLOW 变成自己的头文件
严蔚敏教材里的算法几乎都依赖一组公共符号:Status返回类型、ElemType元素类型、OK/ERROR/OVERFLOW状态码。新手照抄代码,第一行Status InitList(...)就会报错,因为 C 标准库里根本没有Status这个东西。所以做实现的第一个动作,是写一个公共头文件,把这些符号一次性补全。
/* common.h —— 严蔚敏版代码的公共骨架 */ #include <stdio.h> #include <stdlib.h> typedef int Status; // 函数返回值:OK/ERROR/OVERFLOW typedef int ElemType; // 元素类型,以后换类型只改这一行 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define OVERFLOW -2 // 教材常用的溢出返回码 #define MAXSIZE 100 // 顺序表、队列的默认容量 /* 申请堆内存并检查失败,避免每个函数重复写判空 */ void *emalloc(size_t n) { void *p = malloc(n); if (!p) { perror("malloc failed"); exit(OVERFLOW); } return p; }这段代码把教材里“假定存在”的底层符号补齐了。ElemType默认定义成int,是为了先跑通逻辑;等实验题要求存学生信息或坐标点时,只需要把这一行改成typedef struct {...} ElemType;,其余链表、树、图的代码不用动。注意emalloc封装了内存分配,后面所有动态结构都用它,省去大量重复的if (p == NULL)判断。
有了公共骨架,第一个典型函数就是顺序表初始化。教材里的写法是“构造一个空的线性表 L”,到了 C 语言里就变成给结构体成员分配内存:
typedef struct { ElemType *data; // 存储空间的基址 int length; // 当前长度 int listsize; // 当前分配的容量 } SqList; Status InitList(SqList *L) { L->data = emalloc(MAXSIZE * sizeof(ElemType)); L->length = 0; L->listsize = MAXSIZE; return OK; }这里的函数签名是SqList *L,因为要修改 L 内部的 data、length、listsize 这些成员。注意区分:只修改指针指向的内容,传一重指针;要修改调用者手里的指针变量本身,才需要二级指针。这是严蔚敏版代码实现里最高频的坑,后面专门有一章排查。到这一步,公共骨架立住了,接下来就能放心地写线性表、栈队列这些章节。
3. 线性表到栈队列:把教材描述补成能编译的 C 函数
3.1 顺序表:插入为什么必须从后往前挪
顺序表是数据结构实验报告和期末复习里最常手写的结构,核心操作是插入和删除。教材的ListInsert_Sq第一步是“判断插入位置是否合法”,位置 i 按教材约定是从 1 开始数的,合法范围是1 <= i <= L.length + 1。第二步是判断存储空间是否已满,满了就扩容。第三步是元素移动,这里最容易写反。
Status ListInsert_Sq(SqList *L, int i, ElemType e) { if (i < 1 || i > L->length + 1) // 位置不合法 return ERROR; if (L->length >= L->listsize) { // 容量满,扩容 ElemType *tmp = realloc(L->data, (L->listsize + 10) * sizeof(ElemType)); if (!tmp) return OVERFLOW; // realloc 失败时原数据还在 L->data = tmp; L->listsize += 10; } for (int j = L->length; j >= i; j--) // 从最后一个元素开始往后挪 L->data[j] = L->data[j - 1]; L->data[i - 1] = e; // 数组下标比位置号小 1 L->length++; return OK; }移动元素必须从后往前:先把最后一个元素搬到第 length 个位置,再搬倒数第二个,直到第 i 个元素腾出位置。如果写成for (j = i - 1; j < L->length; j++) L->data[j + 1] = L->data[j];,第 i 个元素会被自己覆盖,后面的元素全部没搬,插入点之后的数据直接丢光。这是血泪经验,务必先移动再写入。realloc也很讲究:一定先用临时变量接返回值,确认非 NULL 再赋给L->data,否则扩容失败返回 NULL 时会直接把原指针覆盖掉,旧数据也跟着丢失。
删除操作是差不多的反向逻辑,但要注意用指针参数把被删的元素带回来,模拟教材里的“引用参数”:
Status ListDelete_Sq(SqList *L, int i, ElemType *e) { if (i < 1 || i > L->length) return ERROR; *e = L->data[i - 1]; // 被删元素通过指针带出 for (int j = i; j < L->length; j++) // 从前往后依次前移 L->data[j - 1] = L->data[j]; L->length--; return OK; }删除的循环条件要留意是j < L->length,最后一次把data[length-1]移到data[length-2],然后 length 减一。这两个函数写对,顺序表章节的实验报告就完成了大半。建议把插入、删除、查找分别封装成Status返回函数,调用处再通过返回值判断成功失败,而不是把一堆逻辑堆在 main 里。
3.2 单链表、顺序栈与循环队列:三个容易翻车的细节
链表部分,严蔚敏教材使用带头结点的单链表,结点结构是数据域加指针域。最容易翻车的点不在建表,而在插入结点的两条语句顺序:
typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; /* 在 p 结点后面插入新结点 s */ s->next = p->next; // 先让 s 指向 p 原来的后继 p->next = s; // 再把 p 的 next 指向 s这两句的顺序一旦反了,先执行p->next = s,再执行s->next = p->next,结果s->next指向它自己,链表从 p 之后就断了。教材上是用图表示的,写代码时看不见图,这个错误最隐蔽。链表插入的另一种常见写法是尾插法,需要维护尾指针;头插法则每次把新结点插在头结点之后,遍历结果会和输入顺序相反,这些细节在数据结构习题集里反复出现。
栈和队列的实现同样依赖“约定”。顺序栈的 top 到底指向哪里,不同教材写法不同,但一个程序里必须从头到尾一致。这里采用严蔚敏教材的约定:top 指向栈顶元素的下一个位置,栈空时 top 为 0。
typedef struct { ElemType *base; int top; // 下一个可写入位置,不是栈顶元素本身 int stacksize; } SqStack; Status Push(SqStack *S, ElemType e) { if (S->top >= S->stacksize) return OVERFLOW; // 栈满 S->base[S->top++] = e; // 先写入,再 top+1 return OK; } Status Pop(SqStack *S, ElemType *e) { if (S->top == 0) return ERROR; // 栈空 *e = S->base[--S->top]; // 先 top-1,再取出 return OK; }如果换一种约定,top 指向当前栈顶元素,那么 push 就要先++top再写入。两种写法都成立,但混用就会出现“入栈三个元素,出栈却丢一个”的怪现象。队列这边,循环队列的队空和队满判断是数据结构期末复习的必考点。用 front 指向队头元素,rear 指向队尾元素的下一个位置,牺牲一个存储单元区分状态:
typedef struct { ElemType *base; int front; // 队头下标 int rear; // 队尾下标的下一个位置 } SqQueue; int QueueEmpty(SqQueue *Q) { return Q->front == Q->rear; } int QueueFull(SqQueue *Q) { return (Q->rear + 1) % MAXSIZE == Q->front; } Status EnQueue(SqQueue *Q, ElemType e) { if (QueueFull(Q)) return ERROR; Q->base[Q->rear] = e; Q->rear = (Q->rear + 1) % MAXSIZE; // 到数组末尾要折回 0 return OK; }队空和队满都是front == rear,所以必须牺牲一个数组单元来区分。初始化时 front 和 rear 都置 0,入队时先写再移动 rear,出队时先取 front 位置的元素再移动 front。这些函数写完之后,树和图章节的遍历代码会大量复用栈和队列,所以这一章值得把每个函数都单独调试到能跑,后面才能直接拿来用。
4. 树、图与查找排序:把教材后三章的代码一次打通
4.1 二叉链表建树与先中后序遍历
二叉树章节的核心是二叉链表结构。教材用递归先序建树,输入字符时以#表示空结点,这是最省事的建树方式。为了方便遍历输出,树结点的数据域直接定义成 char:
typedef struct BiTNode { char data; // 结点数据 struct BiTNode *lchild, *rchild; // 左右孩子指针 } BiTNode, *BiTree; void CreateBiTree(BiTree *T) { char ch; scanf(" %c", &ch); // 前导空格跳过换行符 if (ch == '#') { *T = NULL; } else { *T = (BiTNode *)emalloc(sizeof(BiTNode)); (*T)->data = ch; CreateBiTree(&(*T)->lchild); // 递归建左子树 CreateBiTree(&(*T)->rchild); // 递归建右子树 } }这里CreateBiTree的参数是BiTree *T而不是BiTree T,因为函数里要给*T赋值,改变的是指针本身。scanf(" %c", &ch)前面那个空格用来吃掉上一次输入遗留的换行符,没有它,第二次输入会直接读到换行。先序递归遍历写法最简单,一行访问根,两行递归子树:
void PreOrder(BiTree T) { if (T) { printf("%c ", T->data); // 先访问根 PreOrder(T->lchild); PreOrder(T->rchild); } }中序和后序只是把 printf 的位置挪到两次递归之间或之后。递归写法的优点是直观,但树一深就崩,原因是递归调用占用系统栈。想要稳妥实现,就得把递归改成显式栈。先序遍历的非递归版本是最容易理解的:
void PreOrder_NR(BiTree T) { SqStack S; // 这里需要把栈元素类型改成 BiTree InitStack(&S); BiTree p = T; while (p || !StackEmpty(&S)) { if (p) { printf("%c ", p->data); // 沿左路走,边走边访问 Push(&S, p); p = p->lchild; } else { Pop(&S, &p); // 左路走到底,弹栈转向右子树 p = p->rchild; } } }注意这里的栈里存的是BiTree指针,如果直接复用 3.2 的SqStack,需要把ElemType临时改成BiTree,否则存进去的是截断后的整数。后序非递归遍历要比先序多一个“是否已经访问过右子树”的标记,许多教材用栈里面放二元结构来处理,属于扩展内容,先把先序和中序的非递归写熟就够应付大多数数据结构实验报告。
4.2 邻接表建图与 DFS、BFS
图的代码量明显比线性表大。选型上,边的数量远小于顶点数量平方时用邻接表,矩阵稀疏不浪费空间;边很密时用邻接矩阵更直接。严蔚敏教材里的邻接表结构分成三部分:边表结点、顶点表结点、图结构。
#define MAX_VERTEX_NUM 20 typedef struct ArcNode { // 边表结点 int adjvex; // 该边指向的顶点下标 struct ArcNode *nextarc; // 指向下一条边 } ArcNode; typedef struct VNode { // 顶点表结点 ElemType data; ArcNode *firstarc; // 第一条出边 } VNode, AdjList[MAX_VERTEX_NUM]; typedef struct { AdjList vertices; int vexnum, arcnum; // 顶点数和边数 } ALGraph;建图时用头插法把边插入顶点表,这样代码短,遍历顺序与输入顺序相反,这是一个看起来像 bug 但不是 bug 的现象,别在排查上浪费时间:
Status CreateDG(ALGraph *G) { scanf("%d %d", &G->vexnum, &G->arcnum); for (int i = 0; i < G->vexnum; i++) { scanf("%d", &G->vertices[i].data); G->vertices[i].firstarc = NULL; } for (int k = 0; k < G->arcnum; k++) { int v1, v2; scanf("%d %d", &v1, &v2); // 一条从 v1 到 v2 的边 ArcNode *p = (ArcNode *)emalloc(sizeof(ArcNode)); p->adjvex = v2; p->nextarc = G->vertices[v1].firstarc; // 头插 G->vertices[v1].firstarc = p; } return OK; } int visited[MAX_VERTEX_NUM]; void DFS(ALGraph *G, int v) { visited[v] = 1; printf("%d ", G->vertices[v].data); for (ArcNode *p = G->vertices[v].firstarc; p; p = p->nextarc) { if (!visited[p->adjvex]) DFS(G, p->adjvex); // 递归访问邻接点 } }DFS 递归版本的核心是 visited 标记数组,访问一个顶点后立刻标记,避免回头访问。如果图是非连通图,外面要包一层 for 循环,对每个未访问顶点都调用一次 DFS。BFS 则是把递归换成队列:
void BFS(ALGraph *G, int v) { SqQueue Q; InitQueue(&Q); visited[v] = 1; EnQueue(&Q, v); while (!QueueEmpty(&Q)) { int u; DeQueue(&Q, &u); printf("%d ", G->vertices[u].data); for (ArcNode *p = G->vertices[u].firstarc; p; p = p->nextarc) { int w = p->adjvex; if (!visited[w]) { visited[w] = 1; // 入队前立刻标记 EnQueue(&Q, w); } } } }BFS 里容易忽略的一个细节:一定要在入队时就把 visited 置 1,而不是出队时再标记。否则同一个顶点可能被多个邻居重复入队,队列里出现大量冗余数据,遍历序列也跟着变乱。这个“先标记再入队”的习惯,写图的算法时越早养成越好。考研数据结构经常要求手写邻接表的 DFS 序列,代码跑通之后,建议再在纸上按同样的邻接表手推一遍,两边结果对上才算真会了。
4.3 快速排序、堆排序与折半查找:高频考点的代码框架
排序和查找是教材后段的重头戏,也是数据结构排序算法、查找算法相关检索里最常被问到的部分。这里给三个可以直接跑的框架。先是快速排序的挖坑法:
int Partition(int a[], int low, int high) { int pivot = a[low]; // 把基准先拿出来,形成一个坑 while (low < high) { while (low < high && a[high] >= pivot) high--; a[low] = a[high]; // 右边小于基准的移到左边坑位 while (low < high && a[low] <= pivot) low++; a[high] = a[low]; // 左边大于基准的移到右边坑位 } a[low] = pivot; // 基准落位 return low; } void QuickSort(int a[], int low, int high) { if (low < high) { int pos = Partition(a, low, high); QuickSort(a, low, pos - 1); // 排左半段 QuickSort(a, pos + 1, high); // 排右半段 } }挖坑法里两个内层 while 的判断必须带上low < high,一旦省掉,数组里有重复元素时指针可能直接越界。快速排序在完全有序的数组上会退化到 O(n²),因为每次基准都选在最边上,递归深度接近元素个数,后面避坑章节会再提到。
堆排序在严蔚敏教材里下标从 1 开始,写 C 语言时通常统一成从 0 开始,这样父子关系变成left = root*2 + 1,最后一个非叶子结点是n/2 - 1:
void HeapAdjust(int a[], int root, int n) { int child, tmp = a[root]; for (child = root * 2 + 1; child < n; child = child * 2 + 1) { if (child + 1 < n && a[child + 1] > a[child]) child++; // 取较大的孩子 if (a[child] > tmp) { a[(child - 1) / 2] = a[child]; // 孩子上移 } else { break; } } a[(child - 1) / 2] = tmp; // 原根结点落到最终位置 } void HeapSort(int a[], int n) { for (int i = n / 2 - 1; i >= 0; i--) // 从最后非叶结点向上调整 HeapAdjust(a, i, n); for (int j = n - 1; j > 0; j--) { int t = a[0]; a[0] = a[j]; a[j] = t; // 堆顶最大值换到末尾 HeapAdjust(a, 0, j); // 对剩余部分重新调整 } }堆排序里相等元素的处理也要注意:a[child] > tmp用严格大于,等于时不做交换,这样能避免大量相等元素时发生无意义交换。折半查找的框架则强调循环边界:
int BinarySearch(int *a, int n, int key) { int low = 0, high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; // 防溢出的写法 if (a[mid] == key) return mid; else if (a[mid] < key) low = mid + 1; else high = mid - 1; } return -1; // 没找到 }循环条件是low <= high,一旦写成<,长度为 1 时折半查找直接跳过唯一候选元素。mid最好写成low + (high - low) / 2而不是(low + high) / 2,虽然学习阶段数组长度不大看不出区别,但这是一个值得养成的习惯。这三个框架代码短,建议全部手敲一遍再对照教材描述逐行理解,比堆例题有用得多。
5. 严蔚敏版代码常见的 4 个坑与排查思路
5.1 传参翻车:函数里改了,外面还是旧的
现象:写了InitList(L)调用初始化函数,函数里L->data = emalloc(...)正常执行了,回到 main 里访问L->data还是 NULL,或者一访问就段错误。建二叉树时同样,CreateBiTree(T)之后 T 还是空。
原因:C 语言是值传递,形参是实参的副本。函数拿到的是指针变量的副本,给副本赋值不会影响外面的指针变量。L->data = ...能改到内容是因为通过拷贝进来的指针去访问内存,但L = emalloc(...)改的是形参自己,实参毫发无伤。
解决:记住一条规则:只修改指针指向的内容传一重指针;要修改调用者手里的指针变量,必须传指针的地址,也就是二级指针。
/* 错误写法:形参是副本,改完外面不变 */ void bad(SqList *L) { L = emalloc(sizeof(SqList)); // 只改了形参 } /* 正确写法:二级指针,改的是外面那个指针变量 */ void good(SqList **L) { *L = emalloc(sizeof(SqList)); // *L 就是外面那个 L }排查方法是 gdb 断点进去看形参地址和实参地址是否一致,只要地址不同,就说明传参方式有问题。这一条属于严蔚敏版代码实现里出现频率最高的坑,链表的InitList(LinkList *L)、二叉树的CreateBiTree(BiTree *T)都是这个套路。
5.2 教材下标从 1 开始,C 数组从 0 开始
现象:照搬教材的折半查找算法,发现查询数组第一个元素永远失败;堆排序的代码在 0 基数组上跑,末尾元素总是不参与比较。数据结构习题集里这类边界题一做一个错。
原因:教材为了方便描述,把数组下标从 1 开始,第 1 个元素是a[1]。但 C 语言数组天生从 0 开始,直接照搬时mid计算和边界判断整体偏移了一位。
解决:写代码前先定下标约定,不要边写边改。折半查找统一用 0 基写法,也就是 4.3 给出的low = 0, high = n - 1。如果某些排序算法教材强制 1 基,就使用临时数组:把数据存到arr[1]到arr[n],arr[0]空着不用,代码里所有下标保持为 1 基,这样能和教材描述逐行对上。算法调通后再考虑改成 0 基。最忌讳的是两种约定混在一个程序里,读代码的人迟早被绕晕。
5.3 递归爆栈与快排最坏深度
现象:二叉树高度一千层时,递归先序遍历直接 Segmentation Fault;快速排序遇到一段有序数据,递归版本也崩了,但崩之前小数据量测试一切正常。
原因:递归调用保存在系统栈里,默认栈空间有限,递归深度过大就会溢出。快速排序在有序或逆序数组上,基准每次选在端点,一次只能划分出一个元素,递归深度退化到 O(n),元素一多栈就爆。
解决:先用小数据量确认算法正确,再怀疑栈深度。二叉树遍历改成 5.1 节介绍的非递归栈版本,栈空间用堆内存替换系统栈。快速排序则改基准选取策略,常见做法是取low、mid、high三者的中位数作为 pivot,或者直接用a[low + (high - low) / 2],这样有序数组不会退化。如果程序崩了,用 gdb 的bt看到一长串同名栈帧,基本就是递归过深,别在算法逻辑上耗时间。
5.4 realloc 与扩容:随机崩溃多半在这里
现象:顺序表反复插入几百条数据后,程序在看似无关的地方崩掉;或者 realloc 之后打印旧数据时内容全乱,偶尔还伴随“free(): invalid pointer”报错。
原因:一处是 realloc 失败后直接把 NULL 赋给了原指针,原数据丢失,后续继续使用这个指针就崩;另一处是扩容前没判断容量是否已满,插入越过了已分配的内存边界,破坏堆结构。这种错误有随机性,第一次跑可能正常,数据量一上去就发作,最难排查。
解决:凡是 realloc,一律先放临时变量,确认非 NULL 再赋给原指针:
ElemType *tmp = realloc(L->data, new_size * sizeof(ElemType)); if (!tmp) return OVERFLOW; // 原指针仍然有效,可以继续用 L->data = tmp; L->listsize = new_size;同时把编译命令里的-fsanitize=address打开,越界写入会直接报告发生越界的函数和行号,不需要满代码加打印。顺序表、栈、队列的扩容逻辑全部按这个套路写,能省掉大半随机崩溃的排查时间。
6. 用四组数据验证排序与查找:一个让代码敢改的小技巧
代码跑通一次不代表实现正确。教材上的排序、查找代码最常见的状态是“一组数据恰好通过,换一组数据就翻车”。我拿到一个新的排序实现,第一件事不是去跑大数据,而是先构造四种小数组:完全随机、升序、逆序、全部相同。升序和逆序专门治快速排序的基准选取问题;全部相同专门治堆排序和快排里相等元素处理不当导致的死循环或越界。配合一个单调性校验函数,十几行代码就能把排序函数测出原型。
int check_sorted(int a[], int n) { for (int i = 1; i < n; i++) if (a[i - 1] > a[i]) return 0; // 发现逆序,返回不通过 return 1; } int test_data[4][8] = { {5, 1, 4, 2, 8, 7, 6, 3}, // 随机 {1, 2, 3, 4, 5, 6, 7, 8}, // 升序,快排最怕这个 {8, 7, 6, 5, 4, 3, 2, 1}, // 逆序 {4, 4, 4, 4, 4, 4, 4, 4} // 全部相同 };| 测试数据 | 主要验证点 | 常见翻车现象 |
|---|---|---|
| 随机数组 | 基本正确性 | 局部顺序对,整体不对 |
| 升序数组 | 快速排序最坏情况 | 递归过深或超时 |
| 逆序数组 | 相邻元素反复交换 | 冒泡变慢,快排退化 |
| 相同元素 | 等值处理 | 死循环或越界 |
查找函数的边界测试更简单也更重要:长度为 1 的数组、偶数长度和奇数长度各一组、目标值比最小值还小、比最大值还大、目标值重复出现。这几组跑完,low <= high的边界问题基本都能暴露出来。折半查找常见错误就是长度 1 时循环进不去,或者目标在数组两端时 mid 偏出去。
我自己现在每次给某个数据结构实验报告写排序和查找,都先跑上面这组测试再交,跑通过之后,才敢把函数拿来复用到树、图章节里做辅助逻辑。这个习惯是从一次快排在有序数组上连续两次翻车后养成的,从那以后我再也不信“能跑”两个字,只信“能过边界测试”。希望这个技巧能帮你把教材代码从照着敲变成敢修改,希望帮到你。
本文还有配套的精品资源,点击获取