news 2026/10/20 8:46:03

《数据结构(C语言版)》实战:从抽象类型到可运行代码的避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《数据结构(C语言版)》实战:从抽象类型到可运行代码的避坑指南

简介:这份资源是严蔚敏、吴伟民编著的《数据结构(C语言版)》PDF电子书,面向计算机专业学生、考研备考者以及需要夯实算法与数据结构基础的开发者,可用于课程学习、期末复习与考研专业课备考。压缩包内共1个PDF文件,整体约29.15MB,内容为完整教材正文,涵盖线性表、栈与队列、串、树与二叉树、图、查找、排序等经典章节,配合C语言伪代码描述算法,便于边学边实现。目前已有4048人学习下载,说明其在数据结构学习群体中认可度较高。读者可借助该书系统梳理数据结构的逻辑结构与存储结构,理解各类算法的设计思路与时间空间复杂度分析,并对照C语言代码加深对指针、结构体等语法的运用,适合作为长期查阅的案头参考书。

1. 为什么今天还有人翻这本《数据结构(C语言版)》

如果你正在准备考研专业课、补计算机基础,或者带新人做 C 语言项目,大概率会撞上这本《数据结构(C语言版)》。它最反直觉的地方在于:书里几乎没有能直接跑起来的完整工程代码,伪代码和类 C 描述混着写,指针、结构体、Status返回值满天飞,可它偏偏是国内数据结构教学里绕不开的一本。原因不复杂——它把线性表、栈、队列、串、树、图、查找、排序这些结构的逻辑关系、存储方式和基本操作讲得足够系统,考试和面试的底层考点几乎都能在里面找到影子。

但“能看懂”和“能写出来”是两回事。很多人翻完线性表觉得懂了,一上手写单链表插入就指针乱飞;看完二叉树遍历觉得会了,真去写非递归版本又卡在栈的模拟上。这篇笔记不打算复述书里的定义,而是按一线开发的习惯,把这本书里最值得动手的部分拆成可复现的路径:先讲清楚为什么用 C 描述数据结构、怎么把书里的抽象类型定义翻译成能编译的代码、哪些参数和边界最容易翻车,最后给一套自己验证掌握程度的方法。适合正在啃这本书的学生,也适合想用 C 把基础结构重新夯实一遍的开发者。

2. 把书里的抽象数据类型翻译成能编译的 C 代码

2.1 先理解Status、ElemType和结构体三件套

书里大量出现Status、ElemType、OK、ERROR这些名字,新手容易以为它们是 C 语言关键字,其实全是作者为了教学统一而做的类型别名。常见做法是在每个结构实现的开头用宏和typedef把它们落地,这样书上的函数签名才能直接抄进.c文件。

/* ds_common.h —— 把书里的抽象约定固定下来 */ #ifndef DS_COMMON_H #define DS_COMMON_H #define OK 1 #define ERROR 0 #define OVERFLOW -2 #define TRUE 1 #define FALSE 0 typedef int Status; /* 函数返回状态,对应书里的 Status */ typedef int ElemType; /* 元素类型,实际项目里可换成结构体 */ #endif

这段头文件的作用是把书里“约定俗成”的东西变成编译器认识的符号。Status用int是常见做法,因为书里只用到OK、ERROR、OVERFLOW三个值,没必要上枚举。ElemType用int是为了先跑通逻辑,等你确认插入、删除、遍历都对了,再把它换成struct Student之类的真实类型。注意OVERFLOW给的是负数,这是书里的习惯,你在自己项目里可以改成-1,但别和ERROR的0混了,否则if (result)判断会出错。

2.2 线性表:从SqList到可运行的插入删除

书里线性表分顺序存储和链式存储两套。顺序表SqList的结构定义是ElemType *elem; int length; int listsize;,很多人抄完结构体就不知道listsize干嘛用。它其实是当前已分配容量,插入时先判断length >= listsize,满了就realloc扩容。下面这段是可以直接编译运行的顺序表插入与删除。

/* sqlist.c —— 顺序表最小可运行实现 */ #include <stdio.h> #include <stdlib.h> #include "ds_common.h" #define LIST_INIT_SIZE 10 #define LISTINCREMENT 5 typedef struct { ElemType *elem; int length; int listsize; } SqList; Status InitList(SqList *L) { L->elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType)); if (!L->elem) return OVERFLOW; L->length = 0; L->listsize = LIST_INIT_SIZE; return OK; } /* 在 1-based 位置 i 插入 e,书里位置从 1 开始 */ Status ListInsert(SqList *L, int i, ElemType e) { if (i < 1 || i > L->length + 1) return ERROR; if (L->length >= L->listsize) { ElemType *newbase = (ElemType *)realloc( L->elem, (L->listsize + LISTINCREMENT) * sizeof(ElemType)); if (!newbase) return OVERFLOW; L->elem = newbase; L->listsize += LISTINCREMENT; } ElemType *q = &(L->elem[i - 1]); for (ElemType *p = &(L->elem[L->length - 1]); p >= q; --p) *(p + 1) = *p; *q = e; ++L->length; return OK; } Status ListDelete(SqList *L, int i, ElemType *e) { if (i < 1 || i > L->length) return ERROR; ElemType *p = &(L->elem[i - 1]); *e = *p; ElemType *q = L->elem + L->length - 1; for (++p; p <= q; ++p) *(p - 1) = *p; --L->length; return OK; } int main(void) { SqList L; InitList(&L); for (int i = 1; i <= 12; i++) ListInsert(&L, i, i * 10); ElemType out; ListDelete(&L, 3, &out); printf("deleted=%d length=%d\n", out, L.length); for (int i = 0; i < L.length; i++) printf("%d ", L.elem[i]); free(L.elem); return 0; }

逻辑上要盯住三件事。第一,书里位置i是 1-based,代码里访问数组要减 1,这是最常见的下标翻车点。第二,插入时移动方向必须从后往前,删除时从前往后,方向反了会把数据覆盖掉。第三,realloc之后必须用返回值更新L->elem,不能直接写L->elem = realloc(L->elem, ...)然后不检查,失败时原指针会丢。参数上LIST_INIT_SIZE和LISTINCREMENT可以按场景调,考试里常考“插入 n 个元素平均移动多少次”,扩容增量越小,均摊代价越高。

2.3 链表:书里的LNode和指针操作怎么不写飞

单链表是这本书里第一个真正考验指针的地方。书上的LinkList就是LNode *,插入删除靠s->next = p->next; p->next = s;这类语句。很多人背得下来,但自己写ListInsert时忘了传二级指针,头插法改不了头指针。下面给一个带头结点的单链表插入和删除,重点看LinkList *L这个二级指针。

/* linklist.c —— 带头结点单链表 */ #include <stdio.h> #include <stdlib.h> #include "ds_common.h" typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; Status InitList(LinkList *L) { *L = (LNode *)malloc(sizeof(LNode)); if (!*L) return OVERFLOW; (*L)->next = NULL; return OK; } /* 在第 i 个位置前插入,i 从 1 开始 */ Status ListInsert(LinkList *L, int i, ElemType e) { LNode *p = *L; int j = 0; while (p && j < i - 1) { p = p->next; ++j; } if (!p || j > i - 1) return ERROR; LNode *s = (LNode *)malloc(sizeof(LNode)); if (!s) return OVERFLOW; s->data = e; s->next = p->next; p->next = s; return OK; } Status ListDelete(LinkList *L, int i, ElemType *e) { LNode *p = *L; int j = 0; while (p->next && j < i - 1) { p = p->next; ++j; } if (!p->next || j > i - 1) return ERROR; LNode *q = p->next; p->next = q->next; *e = q->data; free(q); return OK; }

这段代码里LinkList *L是二级指针,因为要修改头指针本身。书里有时写LinkList L然后直接操作,那是默认头结点已经存在、只改next的情况。如果你在main里写LinkList L; InitList(&L);,传的就是&L,类型是LinkList *。另一个坑是while条件里的j < i - 1,插入和删除的边界不同:插入允许i = length + 1,删除不允许,所以删除循环里要先判断p->next。这些边界在考试里就是选择题的常客,在工程里就是空指针崩溃的来源。

3. 栈、队列和串:书里最容易“看懂但写不对”的三块

3.1 顺序栈和链栈的top到底指哪里

书里顺序栈的top有两种约定:一种指向栈顶元素,一种指向栈顶的下一个空位。严蔚敏版通常用后者,top == base表示空栈。这个约定直接影响Push和Pop的写法,抄代码前先确认你用的是哪套。

/* sqstack.c —— 顺序栈,top 指向栈顶下一个空位 */ #include <stdlib.h> #include "ds_common.h" #define STACK_INIT_SIZE 20 #define STACKINCREMENT 5 typedef struct { ElemType *base; ElemType *top; int stacksize; } SqStack; Status InitStack(SqStack *S) { S->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType)); if (!S->base) return OVERFLOW; S->top = S->base; S->stacksize = STACK_INIT_SIZE; return OK; } Status Push(SqStack *S, ElemType e) { if (S->top - S->base >= S->stacksize) { S->base = (ElemType *)realloc( S->base, (S->stacksize + STACKINCREMENT) * sizeof(ElemType)); if (!S->base) return OVERFLOW; S->top = S->base + S->stacksize; S->stacksize += STACKINCREMENT; } *S->top++ = e; return OK; } Status Pop(SqStack *S, ElemType *e) { if (S->top == S->base) return ERROR; *e = *--S->top; return OK; }

关键在realloc之后要重新计算S->top,因为base变了,原来的偏移量不能直接沿用。常见错误是只更新base和stacksize,忘了top,结果下一次Push写到非法地址。链栈则相反,top就是栈顶指针,Push相当于头插,Pop相当于删除第一个结点,不需要扩容判断,但每次操作都要malloc和free,参数上要权衡时间与空间。

3.2 循环队列的front、rear和“假溢出”

队列是这本书里第一个让人意识到“顺序存储不一定好用”的结构。普通顺序队列出队后front后移,前面空间浪费,书里叫假溢出。循环队列用取模解决,但front == rear既可能是空也可能是满,所以通常牺牲一个存储单元,或者加一个tag或size字段。下面用牺牲一个单元的做法。

/* sqqueue.c —— 循环队列,牺牲一个单元区分空满 */ #include "ds_common.h" #define MAXQSIZE 100 typedef struct { ElemType *base; int front; int rear; } SqQueue; Status InitQueue(SqQueue *Q) { Q->base = (ElemType *)malloc(MAXQSIZE * sizeof(ElemType)); if (!Q->base) return OVERFLOW; Q->front = Q->rear = 0; return OK; } int QueueLength(SqQueue Q) { return (Q.rear - Q.front + MAXQSIZE) % MAXQSIZE; } Status EnQueue(SqQueue *Q, ElemType e) { if ((Q->rear + 1) % MAXQSIZE == Q->front) return ERROR; Q->base[Q->rear] = e; Q->rear = (Q->rear + 1) % MAXQSIZE; return OK; } Status DeQueue(SqQueue *Q, ElemType *e) { if (Q->front == Q->rear) return ERROR; *e = Q->base[Q->front]; Q->front = (Q->front + 1) % MAXQSIZE; return OK; }

QueueLength里加MAXQSIZE是为了处理rear < front的情况,这是取模运算的常见修正。EnQueue判断满的条件是(rear + 1) % MAXQSIZE == front,不是rear == front。如果你用size字段记录元素个数,满的条件就是size == MAXQSIZE,空的条件是size == 0,代码更直观但多维护一个变量。考试里两种都考,工程里我更倾向加size,因为少一次取模判断,可读性也好。

3.3 串的模式匹配:从 BF 到 KMP 的next数组

书里串这一章的重点是模式匹配,BF 算法好懂但效率低,KMP 的next数组是很多人第一遍看不懂的地方。next[j]的含义是:模式串第j个字符失配时,下一步用模式串的第next[j]个字符继续匹配。手算next有个常用口诀:找最长相等前后缀,然后加 1。下面给一个可运行的 KMP 实现。

/* kmp.c —— KMP 模式匹配,next 数组 1-based */ #include <stdio.h> #include <string.h> void GetNext(const char *T, int *next) { int i = 1, j = 0; next[1] = 0; while (i < (int)strlen(T)) { if (j == 0 || T[i - 1] == T[j - 1]) { ++i; ++j; next[i] = j; } else { j = next[j]; } } } int IndexKMP(const char *S, const char *T, int pos) { int i = pos, j = 1; int next[100]; GetNext(T, next); while (i <= (int)strlen(S) && j <= (int)strlen(T)) { if (j == 0 || S[i - 1] == T[j - 1]) { ++i; ++j; } else { j = next[j]; } } if (j > (int)strlen(T)) return i - (int)strlen(T); return 0; } int main(void) { const char *S = "ababcabcacbab"; const char *T = "abcac"; printf("pos=%d\n", IndexKMP(S, T, 1)); return 0; }

这段代码里next数组是 1-based,next[1] = 0是书里的约定。GetNext里j == 0表示已经退到无前缀可退,直接前进。IndexKMP返回的是 1-based 位置,找不到返回 0。常见翻车点是字符串下标和next下标混用:S和T是 0-based 的 C 字符串,但算法逻辑是 1-based,所以访问时统一减 1。如果你把next写成 0-based,next[0] = -1,逻辑也能跑,但和书里对不上,考试写答案时容易扣分。

4. 树与图:递归能写,非递归怎么落地

4.1 二叉树的二叉链表定义与三种递归遍历

二叉树是这本书里承上启下的结构,前面线性结构的思想在这里第一次分叉。书里用BiTNode和BiTree定义二叉链表,遍历分先序、中序、后序。递归版本几乎和定义一样直白,但很多人写完不知道Visit函数该放哪。

/* bitree.c —— 二叉链表与递归遍历 */ #include <stdio.h> #include <stdlib.h> #include "ds_common.h" typedef struct BiTNode { ElemType data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; Status CreateBiTree(BiTree *T) { ElemType ch; scanf("%d", &ch); if (ch == -1) { /* -1 表示空结点 */ *T = NULL; } else { *T = (BiTNode *)malloc(sizeof(BiTNode)); if (!*T) return OVERFLOW; (*T)->data = ch; CreateBiTree(&(*T)->lchild); CreateBiTree(&(*T)->rchild); } return OK; } void PreOrder(BiTree T) { if (T) { printf("%d ", T->data); PreOrder(T->lchild); PreOrder(T->rchild); } } void InOrder(BiTree T) { if (T) { InOrder(T->lchild); printf("%d ", T->data); InOrder(T->rchild); } }

CreateBiTree用-1表示空结点,这是书里常见的先序建树方式。输入序列必须和先序顺序一致,比如1 2 -1 -1 3 -1 -1表示根 1,左孩子 2,右孩子 3。递归遍历的Visit位置决定遍历顺序:先序在两次递归前,中序在中间,后序在两次递归后。参数上BiTree T是指针,递归调用时传T->lchild即可,不需要二级指针,因为不修改指针本身。

4.2 非递归中序遍历:用栈模拟递归的“黑匣子”

递归遍历好写,但面试和考试常要求非递归。中序非递归的核心是:一路向左压栈,直到空,然后弹栈访问,再转向右子树。这个过程把递归的“黑匣子”拆开了,也是理解栈与递归关系的最好例子。

/* inorder_nonrec.c —— 非递归中序遍历 */ #include <stdio.h> #include "ds_common.h" #define MAX_STACK 100 void InOrderNonRec(BiTree T) { BiTree stack[MAX_STACK]; int top = 0; BiTree p = T; while (p || top > 0) { while (p) { stack[top++] = p; p = p->lchild; } if (top > 0) { p = stack[--top]; printf("%d ", p->data); p = p->rchild; } } }

这里用数组模拟栈,top指向栈顶下一个空位。外层while的条件是p || top > 0,缺一不可:p不为空表示还有左子树没走完,top > 0表示栈里还有祖先没访问。内层while一路向左,把路径上的结点全压栈。弹栈后访问,然后转向右子树。如果你把p = p->rchild写在弹栈之前,就会漏掉右子树。这个逻辑和递归版本对照看,能直接看出系统栈里存了什么。

4.3 图的邻接矩阵与邻接表:存储结构怎么选

图这一章书里先讲邻接矩阵再讲邻接表,很多人学完不知道什么时候用哪个。邻接矩阵适合稠密图,判断两点是否相邻是 O(1),但空间是 O(n²);邻接表适合稀疏图,空间是 O(n+e),但判断相邻要遍历链表。下面给邻接表的定义和 DFS。

/* graph_adjlist.c —— 邻接表与 DFS */ #include <stdio.h> #include <stdlib.h> #include "ds_common.h" #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; int visited[MAX_VERTEX_NUM]; void DFS(ALGraph *G, int v) { visited[v] = TRUE; 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); } }

ArcNode存邻接点和下一条边,VNode存顶点数据和第一条边。visited数组是全局的,DFS 前要清零。递归 DFS 的深度等于图的直径,稀疏图里没问题,稠密图里可能爆栈,工程里常用显式栈改非递归。参数上MAX_VERTEX_NUM是顶点上限,实际项目里可以动态分配AdjList,但书里为了教学固定成数组。

5. 查找与排序:书里的算法怎么在本地验证

5.1 折半查找的边界:low <= high还是low < high

折半查找本身不难,难在边界。书里用low、high、mid三个变量,循环条件是low <= high,mid = (low + high) / 2。如果写成low < high,最后一个元素会漏查。下面给一个可运行的版本,并打印每次比较。

/* binsearch.c —— 折半查找,带比较次数统计 */ #include <stdio.h> int BinSearch(const int *a, int n, int key, int *cnt) { int low = 0, high = n - 1, mid; *cnt = 0; while (low <= high) { mid = (low + high) / 2; (*cnt)++; if (a[mid] == key) return mid; else if (a[mid] < key) low = mid + 1; else high = mid - 1; } return -1; } int main(void) { int a[] = {5, 13, 19, 21, 37, 56, 64, 75, 80, 88, 92}; int cnt, pos = BinSearch(a, 11, 56, &cnt); printf("pos=%d cnt=%d\n", pos, cnt); return 0; }

low <= high保证区间为空时才退出。mid用(low + high) / 2在low和high都很大时可能溢出,工程里写成low + (high - low) / 2。折半查找要求表有序,且是顺序存储,链表上没法 O(1) 定位中间结点,所以书里强调它只适用于顺序表。

5.2 快速排序的Partition:一趟划分到底做了什么

快速排序是这本书里最常被考的排序算法,核心在Partition函数。书里用pivotkey存枢轴,然后左右交替扫描。很多人背得下代码,但说不清一趟划分后枢轴的位置意味着什么。下面给一个可运行的版本,并打印每趟结果。

/* quicksort.c —— 快速排序与一趟划分 */ #include <stdio.h> int Partition(int *a, int low, int high) { int pivotkey = a[low]; while (low < high) { while (low < high && a[high] >= pivotkey) --high; a[low] = a[high]; while (low < high && a[low] <= pivotkey) ++low; a[high] = a[low]; } a[low] = pivotkey; return low; } void QSort(int *a, int low, int high) { if (low < high) { int pivotloc = Partition(a, low, high); QSort(a, low, pivotloc - 1); QSort(a, pivotloc + 1, high); } } int main(void) { int a[] = {49, 38, 65, 97, 76, 13, 27, 49}; QSort(a, 0, 7); for (int i = 0; i < 8; i++) printf("%d ", a[i]); return 0; }

Partition里先把a[low]存到pivotkey,然后从右往左找比枢轴小的,填到左边;再从左往右找比枢轴大的,填到右边。循环结束时low == high,把pivotkey放回去。这个“挖坑填数”的过程比书上的交换写法更直观,结果一样。参数上low和high是闭区间,递归时pivotloc - 1和pivotloc + 1跳过枢轴。快排平均 O(n log n),最坏 O(n²),枢轴选第一个元素时对有序数组最不利,工程里常用随机化或三数取中。

5.3 堆排序:HeapAdjust的下沉逻辑

堆排序是书里排序章节的难点,HeapAdjust负责把以某个结点为根的子树调整成大顶堆。核心是拿根和左右孩子比,如果孩子更大就交换,然后继续往下沉。下面给一个可运行版本。

/* heapsort.c —— 堆排序 */ #include <stdio.h> void HeapAdjust(int *a, int s, int m) { int rc = a[s]; for (int j = 2 * s; j <= m; j *= 2) { if (j < m && a[j] < a[j + 1]) ++j; if (rc >= a[j]) break; a[s] = a[j]; s = j; } a[s] = rc; } void HeapSort(int *a, int n) { for (int i = n / 2; i >= 1; --i) HeapAdjust(a, i, n); for (int i = n; i > 1; --i) { int tmp = a[1]; a[1] = a[i]; a[i] = tmp; HeapAdjust(a, 1, i - 1); } } int main(void) { int a[] = {0, 49, 38, 65, 97, 76, 13, 27, 49}; HeapSort(a, 8); for (int i = 1; i <= 8; i++) printf("%d ", a[i]); return 0; }

这里数组下标从 1 开始,a[0]不用,是为了让2*s和2*s+1直接对应左右孩子。HeapAdjust里j *= 2是往下沉一层,j < m判断右孩子是否存在,rc >= a[j]表示已经满足大顶堆,直接退出。建堆从n/2开始,因为叶子结点本身已经是堆。排序时每次把堆顶和最后一个元素交换,然后调整前i-1个元素。参数上n是元素个数,不是数组长度,传错会越界。

6. 避坑与排查:这本书配套练习里最常见的 5 个翻车点

6.1 现象:编译报unknown type name 'Status',原因:头文件没包含或宏没定义

这是新手第一个翻车点。书里的代码片段默认Status、OK、ERROR已经定义,但你自己建工程时没有ds_common.h,编译器不认识。解决方法是把第 2 章里的公共头文件放到每个.c同目录,并在开头#include "ds_common.h"。如果还报错,检查头文件里有没有#ifndef保护,以及编译命令有没有加-I.。

6.2 现象:单链表插入后遍历死循环,原因:s->next指向了自己

头插法里如果写成s->next = p; p->next = s;,就会形成环。正确顺序是先s->next = p->next;再p->next = s;。排查方法是插入后打印p->next和s->next的地址,看是否相等。如果已经成环,遍历时用while (p && p != head)加一个步数上限,避免死循环。

6.3 现象:循环队列EnQueue后QueueLength为负,原因:rear和front没取模

QueueLength里如果直接写rear - front,当rear < front时结果是负数。解决方法是加MAXQSIZE再取模,即(rear - front + MAXQSIZE) % MAXQSIZE。另一个坑是EnQueue里忘了rear = (rear + 1) % MAXQSIZE,导致rear越界。排查时打印front、rear和QueueLength,看三者是否自洽。

6.4 现象:KMP 的next数组手算和代码结果不一致,原因:下标基准不同

书里next是 1-based,next[1] = 0;有些教材是 0-based,next[0] = -1。两种都对,但混用就会错。解决方法是先确认你参考的版本,然后在代码注释里写清楚。手算时用“最长相等前后缀加 1”验证,比如T = "abcac",next[1]=0,next[2]=1,next[3]=1,next[4]=1,next[5]=2。如果代码输出不同,检查i和j的初始值。

6.5 现象:堆排序结果不对,原因:数组下标从 0 开始但算法按 1 开始

HeapAdjust里2*s和2*s+1是 1-based 的左右孩子。如果你把数组从 0 开始存,根是a[0],左孩子是a[1],右孩子是a[2],公式变成2*s+1和2*s+2。解决方法是统一基准:要么a[0]空着不用,要么把公式改成 0-based。排查时打印建堆后的数组,看是否满足a[i] >= a[2*i]和a[i] >= a[2*i+1]。

7. 用“手写 + 对拍”验证你是否真的掌握了这本书

7.1 手写替代抄写:每个结构只写接口和边界

这本书的代码量不小,但真正要练的是接口和边界。我的习惯是:每个结构先写头文件,声明Init、Insert、Delete、Traverse四个函数,然后不看书实现一遍。写完再对照书里的伪代码,重点看边界条件:插入位置i的范围、空表判断、满表扩容、删除后指针处理。如果某个边界写错,就在旁边用注释标出来,下次复习只看注释。

7.2 对拍:用暴力版本验证你的实现

对拍是工程里验证算法最有效的方法。以快速排序为例,写一个main随机生成 1000 个整数,一份用你的QSort,一份用qsort,排序后逐元素比较。如果结果不一致,打印原始数组和两个结果,定位到具体元素。链表、栈、队列也可以用类似方法:用数组模拟一个“标准答案”,和你的链表实现对比每次操作后的状态。

/* fuzz_quicksort.c —— 对拍快速排序 */ #include <stdio.h> #include <stdlib.h> #include <string.h> int cmp(const void *a, const void *b) { return (*(int *)a - *(int *)b); } void QSort(int *a, int low, int high); int main(void) { srand(42); for (int t = 0; t < 1000; t++) { int n = rand() % 100 + 1; int *a = (int *)malloc(n * sizeof(int)); int *b = (int *)malloc(n * sizeof(int)); for (int i = 0; i < n; i++) { a[i] = rand() % 1000; b[i] = a[i]; } QSort(a, 0, n - 1); qsort(b, n, sizeof(int), cmp); if (memcmp(a, b, n * sizeof(int)) != 0) { printf("mismatch at test %d\n", t); for (int i = 0; i < n; i++) printf("%d ", a[i]); printf("\n"); for (int i = 0; i < n; i++) printf("%d ", b[i]); printf("\n"); free(a); free(b); return 1; } free(a); free(b); } printf("all passed\n"); return 0; }

这段对拍代码里srand(42)固定随机种子,保证每次运行结果可复现。memcmp比较两个数组是否完全一致,不一致就打印。QSort的声明放在前面,实现用第 5 章的版本。对拍通过不代表算法最优,但能保证逻辑正确。我一般会跑 1000 组,每组长度 1 到 100,覆盖空数组、单元素、重复元素、有序、逆序这些边界。

7.3 用考试真题反向验证:只看题眼,不背答案

这本书的配套练习和历年真题是最好的验证材料。我的做法是:拿到一道题,先看它考哪个结构、哪个操作、哪个边界,然后自己写代码或伪代码,最后对照答案。如果答案用了不同的存储结构,比如你用链表、答案用顺序表,就对比两者的时间空间复杂度,想清楚为什么答案选那个。这个过程比刷十遍书更能暴露盲区。比如“判断链表是否有环”这种题,书里没有直接答案,但用快慢指针就能解,考的是你对链表指针操作的理解。

7.4 一个具体技巧:把Status返回值当断言用

书里所有操作都返回Status,很多人写完就扔了。我的习惯是在main里用assert或if检查每个返回值,比如if (ListInsert(&L, i, e) != OK) { printf("insert failed at %d\n", i); return 1; }。这样一旦边界写错,程序立刻停在出错位置,而不是继续跑出一堆错误结果。这个习惯在调试 KMP、堆排序这种逻辑复杂的算法时特别有用,相当于给自己留了后悔药。

我自己的教训是:第一遍看这本书时贪快,线性表、栈、队列的代码都是抄一遍就过,结果到树和图的时候完全写不出来。后来逼自己每个结构手写三遍,第一遍对着书,第二遍合上书,第三遍改参数和边界,才真正把指针和递归理顺。如果你也在啃这本书,别急着往后翻,把每个结构的Init、Insert、Delete、Traverse写熟,后面的树、图、查找、排序都是这些基本操作的组合。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 22:22:55

Java爬虫实战:从技术选型到分布式架构的工程指南

1. 我为什么用Java写爬虫&#xff0c;而不是Python说到爬虫&#xff0c;很多人下意识就想到Python。这个问题我几乎每次分享都会被问到&#xff0c;尤其在团队内部做技术选型的时候。我的答案其实很简单&#xff1a;爬虫的核心从来不是语言&#xff0c;而是抓取、解析、存储和调…

作者头像 李华
网站建设 2026/10/16 14:25:26

高防IP实战调优:DDoS清洗策略、CC防护参数与黑洞排障全解析

业务上线第三天&#xff0c;凌晨两点半&#xff0c;监控大屏的流量曲线突然从 200Mbps 直接飙到 400Gbps。机房交换机告警、运营商封堵通知、客户投诉电话几乎同时砸过来。那一夜我盯着高防 IP 的拦截日志看了整整六个小时&#xff0c;也第一次真正体会到大流量 DDoS 攻击下&am…

作者头像 李华
网站建设 2026/10/16 5:40:48

SpringBoot2+Vue3宠物商城系统:前后端分离实战全解析

前后端分离项目写了几年&#xff0c;技术栈审美会变得异常挑剔。2020年以后我接到的商城类项目需求&#xff0c;十条里有七八条是同一套配方&#xff1a;SpringBoot2 写后端、Vue3 写前端、MyBatis-Plus 管数据访问、MySQL8 做存储。这个 Java Web 宠物商城系统项目就是很典型的…

作者头像 李华
网站建设 2026/10/20 0:45:52

NVIDIA驱动安装失败与CUDA不可用的系统级原因解析

1. 为什么显卡驱动装不上、一更新就黑屏、CUDA跑不起来&#xff1f;这不是玄学&#xff0c;是可复现的系统级问题你是不是也遇到过这些场景&#xff1a;刚装好Ubuntu&#xff0c;nvidia-smi报“NVIDIA-SMI has failed”&#xff0c;连驱动都没加载&#xff1b;或者Windows下更新…

作者头像 李华
网站建设 2026/10/15 10:33:44

LSTM-GPR混合模型实现新能源汽车销量概率预测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华