主题
字号
CHAPTER 09 ≈ 17 MIN READ

附录:考前复盘笔记

这一章是我考前最后一晚自己手写的复盘笔记,口吻随意、密度很高——先把八章知识点按章快速过一遍,再记下刷题时反复踩的易错点,然后是几段「不能调库时得默写出来」的算法模板,最后一套带答案的模拟题自测。和前面正文的系统讲解相比,这里更像「考前临门一脚」的速记。

9.1 知识点回顾(按章速过)

9.1.1 数据结构基础知识

malloc是手动在堆上申请分配内存,存活时间是全局的,直到在其他地方free。 注意malloc直接返回指针。

逻辑结构:集合,线性,树形,图状 存储结构:顺序(数组),链式(链表指针),索引(目录),散列(哈希表)

时间复杂度 O(n) 空间复杂度,如果原地就是O(1),额外开了一个一样大的则是O(n)

9.1.2 线性表

一对一线性结构,(a₁, a₂, ..., aₙ)

顺序表(线性表的顺序实现),即为数组:

单链表(线性表的链式实现),即为链表:

循环链表:把单链表最后一个节点的 next 由 NULL 改为指向头结点,整条链就首尾相连成一个环。

双链表:存两个指针(next和piror),前后都可以走

9.1.3 栈与队列

栈:后进先出,一般用数组实现,核心是push, pop, top这三个操作 应用:括号匹配 前后缀表达式和中缀表达式的转换(画二叉树,用前中后序遍历) 后缀表达式求值(用一个栈):从左到右扫描——遇到操作数就入栈;遇到运算符,就弹出栈顶两个操作数做运算,结果再压回栈。扫描结束栈里剩的唯一一个数就是答案。

队列:先进先出,数组实现则需要“循环队列”,防止假溢出,链表实现则不需要。

9.1.4 串和数组

串,就是字符串,如何匹配字符串是一个核心问题 KMP算法

稀疏矩阵:

9.1.5 树与二叉树

看到树,就想到递归

节点的度:有几个孩子

完全二叉树:不需要满,但中间不能有空缺

叶子节点数 n₀ = 度为 2 的节点数 n₂ + 1,即 n₀ = n₂ + 1

二叉树怎么存储?完全二叉树可以用数组,一般二叉树用二叉链表(也就是一堆节点用指针连接)

二叉树怎么遍历?

线索二叉树:对于很稀疏的二叉树,把空着的指针废物利用,左指针为空就指向该节点的中序前驱,如果右指针为空,就指向中序后继。再加两个ltag和rtag区分(0说明是真实的孩子,1说明是线索),方便遍历。

二叉搜索树/二叉排序树(BST):左子树 < 根 < 右子树 查找就是二分查找了,插入也类似, 删除麻烦点:

平衡二叉树(AVL): 防止BST退化,在BST基础上进行的约束 AVL最小高度N(h)? N(h) = N(h-1) + N(h-2) + 1 (左子树高 h-1、右子树高 h-2,再加根自己) 边界:N(0) = 0(空树), N(1) = 1, N(2) = 2 AVL失衡处理办法:

  1. 从插入点往上,找到第一个失衡的节点A
  2. 匹配四种类型做响应操作
    • LL:右旋,A的左孩子上位,LR挂到A左边
    • RR:左旋,A的右孩子上位,RL接到A右边
    • LR:先对L左旋,再对A右旋(让LR上位,L还是L,A变成R)
    • RL:先对R右旋,再对A左旋(让RL上位,R还是R,A变成L) 响应本质是让AVL不管怎么插入,始终在满足BST基础下平衡

哈夫曼树:编码最短的构造 注意每次是从所有里面选两个拼接,包括已有的树,不要一条路走到底。 编码即左0右1, WPL是编码完之后的总位数 注意哈夫曼树不一定唯一(相等权值)但WPL一定唯一且最小

堆:一种特殊的完全二叉树,满足节点和孩子之间的大小关系。 移动删除都是“上浮和下沉”

树转二叉树:左孩子右兄弟

9.1.6 图

怎么存储图?邻接矩阵/邻接表 邻接矩阵的第i行第j列是 i -> j 无向图的邻接矩阵对称 邻接多重表:无向图用,只存一次

怎么遍历图?DFS,BFS。

生成树:用最少的边联通所有顶点的方式,在带权图里面,权值最小的生成树就是最小生成树(MST) 两种算法:

最短路径:

拓扑排序:Kahn算法和DFS,注意结果一般不唯一

9.1.7 查找表

ASL,平均查找长度

顺序查找:最慢,O(n) 二分查找:必须有序,O(log n) 分块查找:最佳分块是O(√n) 树表查找:BST一般O(log n),可能退化O(n),AVL始终O(log n) 哈希表:空间换时间,O(1)

9.1.8 排序

稳定性:若两个元素关键字相等,排序后它们的相对前后位置不变,就叫稳定;若可能被交换顺序,就不稳定

直接插入排序:O(n²) 折半插入排序:O(n²),比较次数变成O(n log n) 希尔排序:gap,最后再插入排序,平均O(n^1.3),第一个突破O(n²)的排序 冒泡排序:O(n²) 快速排序:递归,取基准,双指针,平均 O(n log n),但最坏 O(n²)(当数列基本有序、基准每次都取到最小/最大值时退化)。空间 O(log n)(递归栈)。 选择排序:O(n²) 堆排序:建堆 O(n),之后 n−1 次下沉每次 O(log n),整体 O(n log n),且最好/平均/最坏都是 O(n log n)(性能稳定,无退化) 归并排序:递归,对半分到极致,之后逐渐合并,分 log n 层、每层合并共 O(n),最好/平均/最坏都是 O(n log n)

9.2 刷题小结(易错点速记)

后缀表达式(前中后序遍历)

稀疏矩阵存储(三元组表,十字链表)

三元组表并不创新,只是用三个东西记位置,适合压缩,但查找的适合还是需要遍历,插入删除都不方便,牵一发而动全身。 

十字链表就只需要改指针,方便很多,本质就是用链表而不是矩阵的形式存储,这样可以“跳过”太多空位置,一次性直接指向下一个有信息的元素,当然还是有行和列的概念。 

注意先序/前序 的“前”是指 根

二叉树里面节点的“度”指的是孩子数

哈希冲突里面的处理方法:

任意一颗二叉树,用前序/中序/后序遍历并不改变叶子节点的相对次序关系

归并排序不管数列情况,基本上都是O(n·logn)

关键字序列:折半查找时比较过的元素,按顺序列出来就行

哈夫曼树要记得不是直接走到底,而是每次取最小的两个叠加。

ASL是平均查找长度,分母的n是指一共要找多少个元素。

哈希表:

邻接表的形式是一系列链表,但其实是并列的,如0——2——1——4,其实指的是0分别指向2,1,4,他们是并列而非前后关系,用链表只是为了存储方便。

拓扑序列:每次弹出入度为0的节点,如果结束发现并没有遍历所有节点,说明有环。

拓扑序列通常不唯一。

拓扑排序是一种针对有向图的线性排序方式,核心是该排序严格符合所有有向边的先后顺序。主要有两种实现方式:

BFS(Kahn算法):初始情况下找到一个入度为0的节点,压入栈,消除所有它发出的边,之后弹出并打印,弹出后下一步把所有入度为0的节点都压入,按照顺序(邻接矩阵行顺序)依次弹出并打印,同样,每次弹出一个节点后消除所有它发出的边,有入度为0就压入,之后继续栈内下一个,直到结束。

DFS:对每个节点做DFS,在递归返回时把节点压入栈(即所有后继都处理完才入栈),全部完成后把栈逆序输出,就是拓扑序。若DFS中遇到正在访问的节点说明有环。

顺序存储方式(数组)不止可以用来存线性结构,完全二叉树也可以

9.3 考前突击:手写算法模板(不能调 STL 时手写)

9.3.1 栈(数组实现)

typedef struct {
    int data[MAXSIZE];
    int top;          // 栈顶下标,初始 -1
} Stack;

void init(Stack *s){ s->top = -1; }
int isEmpty(Stack *s){ return s->top == -1; }
int isFull(Stack *s){ return s->top == MAXSIZE - 1; }
int push(Stack *s, int x){
    if(isFull(s)) return 0;
    s->data[++s->top] = x; return 1;
}
int pop(Stack *s, int *x){
    if(isEmpty(s)) return 0;
    *x = s->data[s->top--]; return 1;
}

9.3.2 循环队列(数组实现,牺牲一个单元判满)

typedef struct {
    int data[MAXSIZE];
    int front, rear;  // 初始都为 0
} Queue;

void init(Queue *q){ q->front = q->rear = 0; }
int isEmpty(Queue *q){ return q->front == q->rear; }
int isFull(Queue *q){ return (q->rear + 1) % MAXSIZE == q->front; }
int enqueue(Queue *q, int x){          // 入队
    if(isFull(q)) return 0;
    q->data[q->rear] = x;
    q->rear = (q->rear + 1) % MAXSIZE; return 1;
}
int dequeue(Queue *q, int *x){         // 出队
    if(isEmpty(q)) return 0;
    *x = q->data[q->front];
    q->front = (q->front + 1) % MAXSIZE; return 1;
}
// 队列长度 = (rear - front + MAXSIZE) % MAXSIZE

9.3.3 快速排序(partition + 递归)

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 p = partition(a, low, high);
        quickSort(a, low, p - 1);
        quickSort(a, p + 1, high);
    }
}

9.3.4 归并排序(merge 两个有序段)

void merge(int a[], int tmp[], int l, int mid, int r){
    int i = l, j = mid + 1, k = l;
    while(i <= mid && j <= r)
        tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];  // <= 保证稳定
    while(i <= mid) tmp[k++] = a[i++];
    while(j <= r)   tmp[k++] = a[j++];
    for(k = l; k <= r; k++) a[k] = tmp[k];
}
void mergeSort(int a[], int tmp[], int l, int r){
    if(l < r){
        int mid = (l + r) / 2;
        mergeSort(a, tmp, l, mid);
        mergeSort(a, tmp, mid + 1, r);
        merge(a, tmp, l, mid, r);
    }
}

9.3.5 Dijkstra(伪代码,单源最短路,不能有负权边)

dist[s] = 0, 其余 dist[] = ∞
visited[] = false
重复 n 次:
    在所有 visited=false 的点中,选 dist 最小的 u
    visited[u] = true
    对 u 的每个邻居 v:           // 松弛
        if !visited[v] && dist[u] + w(u,v) < dist[v]:
            dist[v] = dist[u] + w(u,v)
// 堆优化:用小根堆代替"线性扫描找最小 u",O((V+E)logV)

9.3.6 哈希表要点(ASL 计算)

9.4 模拟练习题(含答案)

9.4.1 选择 / 填空(复杂度 + 概念)

1. 一棵完全二叉树有 1000 个节点,叶子节点有多少个?

答案:n₀=n₂+1,n=n₀+n₁+n₂。完全二叉树 n₁=0或1。n=1000偶数→n₁=1。1000=n₀+1+(n₀−1)→n₀=500

2. 含 n 个单元的循环队列,最多能存多少元素?

答案:牺牲一个单元判满,MAXSIZE−1

3. 最坏情况时间复杂度为 O(n log n) 的排序:A.快排 B.堆排序 C.冒泡 D.直接插入

答案B 堆排序(快排最坏 O(n²);归并也满足但不在选项)。

4. 哈希表装填因子 α 越大会怎样?

答案:空间利用率高,但冲突概率大、ASL 变长

5. 邻接表存 n 顶点 e 边的无向图,边结点总数?

答案2e(每条边存两次);有向图为 e。

9.4.2 简答

6. 快排为何不稳定,归并为何稳定?

答案:快排:partition 时基准与远处元素交换,相等元素相对位置可能颠倒→不稳定。归并:merge 时 a[i]<=a[j] 取左段,相等优先取前段→稳定。

7. Prim 与 Kruskal 各适合什么图?

答案:Prim 按顶点扩展,复杂度与顶点相关,适合稠密图;Kruskal 按边排序,与边数相关,适合稀疏图

9.4.3 写中间结果

8. [49,38,65,97,76,13,27] 快排第一趟(基准取首元素49)结果?

答案:基准49归位,左<49右>49:[27,38,13,49,76,97,65]

9. 关键字 {19,14,23,1,68,20,84,27,55,11,10,79},m=13,H(k)=k%13,线性探测。

答案:逐个填:19→6;14→1;23→10;1→1冲突→2;68→3;20→7;84→6冲突→8;27→1,2,3冲突→4;55→3冲突→9;11→11;10→10冲突→11冲突→12;79→1起冲突一路探测到空位(下标0)。 ASL_成功 = 各元素比较次数之和 / 12(考场把表画出来逐个数比较次数)。

10. 先序 ABDECFG,中序 DBEAFCG,求后序。

答案:根A;左子树(先序DBE/中序DBE),右子树(先序CFG/中序FCG)。B左D右E,C左F右G。 后序:DEBFGCA

9.4.4 算法实现(写代码)

11. 用栈判断 ()[]{} 括号匹配。

答案(代码)

int match(char *s){
    Stack st; init(&st); int x;
    for(int i=0; s[i]; i++){
        char c = s[i];
        if(c=='('||c=='['||c=='{') push(&st, c);
        else{
            if(pop(&st,&x)==0) return 0;           // 栈空,右括号多
            if((c==')'&&x!='(')||(c==']'&&x!='[')||(c=='}'&&x!='{'))
                return 0;
        }
    }
    return isEmpty(&st);                            // 栈空才匹配
}

12. 后缀表达式求值步骤。

答案:从左扫描:操作数入栈;遇运算符弹出两个(先弹的是右操作数)运算后压回;扫描完栈顶即答案。

9.4.5 算法设计(思路型,写不出代码就写文字得步骤分)

13. 判断无向图是否连通。

答案:从任一顶点 DFS/BFS,统计访问到的顶点数;== n 则连通,否则不连通。

14. 求二叉树高度。

答案(代码)

int height(Node *t){
    if(!t) return 0;
    int l = height(t->left), r = height(t->right);
    return (l > r ? l : r) + 1;
}

看到树就想递归。