文章目录
- 数据结构知识点与代码实现总结
-
- 目录
- [第一章 线性结构](#第一章 线性结构)
- [TL;DR 快速导读](#TL;DR 快速导读)
- [第一章 线性结构](#第一章 线性结构)
-
- [1.1 顺序表(array_impl/sqlist.c)](#1.1 顺序表(array_impl/sqlist.c))
- [1.2 单链表(list_impl/singly_linked_list.c)](#1.2 单链表(list_impl/singly_linked_list.c))
- [1.3 双向链表(list_impl/doubly_linked_list.c)](#1.3 双向链表(list_impl/doubly_linked_list.c))
- [1.4 全局地址索引单链表(list_impl/linked_list_global.c)](#1.4 全局地址索引单链表(list_impl/linked_list_global.c))
- [1.5 四种线性结构对比](#1.5 四种线性结构对比)
- [第二章 栈与队列](#第二章 栈与队列)
-
- [2.1 顺序栈(array_impl/stack.c)](#2.1 顺序栈(array_impl/stack.c))
- [2.2 链式栈(list_impl/stack.c)](#2.2 链式栈(list_impl/stack.c))
- [2.3 循环队列(array_impl/circular_queue.c)](#2.3 循环队列(array_impl/circular_queue.c))
- [2.4 链式队列(list_impl/queue.c)](#2.4 链式队列(list_impl/queue.c))
- [2.5 单调队列(array_impl/monotonic_queue.c)](#2.5 单调队列(array_impl/monotonic_queue.c))
- [第三章 哈希表](#第三章 哈希表)
-
- [3.1 拉链法哈希表(hash/hash_chaining.c)](#3.1 拉链法哈希表(hash/hash_chaining.c))
- [3.2 开放寻址法哈希表(hash/hash_open_addressing.c)](#3.2 开放寻址法哈希表(hash/hash_open_addressing.c))
- [3.3 两种哈希策略对照](#3.3 两种哈希策略对照)
- [第四章 堆与优先队列](#第四章 堆与优先队列)
-
- [4.1 大根堆(heap/priority_queue.c)](#4.1 大根堆(heap/priority_queue.c))
- [第五章 树结构](#第五章 树结构)
-
- [5.1 二叉树(tree/binary_tree.c)](#5.1 二叉树(tree/binary_tree.c))
- [5.2 二叉搜索树(tree/bst.c)](#5.2 二叉搜索树(tree/bst.c))
- [5.3 树状数组(tree/fenwicktree.c)](#5.3 树状数组(tree/fenwicktree.c))
- [5.4 线段树(单点更新,tree/segment_tree_point.c)](#5.4 线段树(单点更新,tree/segment_tree_point.c))
- [5.5 懒标记线段树(区间加 + 区间求和,tree/segment_tree_lazy.c)](#5.5 懒标记线段树(区间加 + 区间求和,tree/segment_tree_lazy.c))
- [5.6 三种"区间"结构对照](#5.6 三种“区间”结构对照)
- [第六章 图论算法](#第六章 图论算法)
-
- [6.1 图的遍历:DFS 与 BFS](#6.1 图的遍历:DFS 与 BFS)
- [6.2 最小生成树:Prim(graph/mst/prim.c)](#6.2 最小生成树:Prim(graph/mst/prim.c))
- [6.3 最小生成树:Kruskal(graph/mst/kruskal.c)](#6.3 最小生成树:Kruskal(graph/mst/kruskal.c))
- [6.4 最短路:Dijkstra(graph/shortest_path/dijkstra.c)](#6.4 最短路:Dijkstra(graph/shortest_path/dijkstra.c))
- [6.5 最短路:Bellman-Ford(graph/shortest_path/bellman_ford.c)](#6.5 最短路:Bellman-Ford(graph/shortest_path/bellman_ford.c))
- [6.6 最短路:SPFA(graph/shortest_path/spfa.c)](#6.6 最短路:SPFA(graph/shortest_path/spfa.c))
- [6.7 最短路:Floyd(graph/shortest_path/floyd.c)](#6.7 最短路:Floyd(graph/shortest_path/floyd.c))
- [6.8 四个最短路算法总对照](#6.8 四个最短路算法总对照)
- [第七章 字符串结构:Trie](#第七章 字符串结构:Trie)
-
- [7.1 pass / end 双计数器设计](#7.1 pass / end 双计数器设计)
- [7.2 静态数组实现(trie/trie_array.c)](#7.2 静态数组实现(trie/trie_array.c))
- [7.3 指针 + 定长数组路(trie/trie_class_array.c)](#7.3 指针 + 定长数组路(trie/trie_class_array.c))
- [7.4 指针 + 哈希路(trie/trie_class_hash.c)](#7.4 指针 + 哈希路(trie/trie_class_hash.c))
- [7.5 三种 Trie 实现对照](#7.5 三种 Trie 实现对照)
- [第八章 位运算结构](#第八章 位运算结构)
-
- [8.1 位集 Bitset(bitset/bitset.c)](#8.1 位集 Bitset(bitset/bitset.c))
- [8.2 布隆过滤器(bitset/bloom_filter.c)](#8.2 布隆过滤器(bitset/bloom_filter.c))
- [附录 C 语言实战技巧与缺陷清单](#附录 C 语言实战技巧与缺陷清单)
-
- [A.1 反复出现的 C 语言技巧](#A.1 反复出现的 C 语言技巧)
- [A.2 代码中的真实缺陷清单](#A.2 代码中的真实缺陷清单)
- [A.3 源码文件总览](#A.3 源码文件总览)
数据结构知识点与代码实现总结
目录
| 章节 | 内容 |
|---|---|
| [第一章 线性结构](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 |
| [第二章 栈与队列](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | 顺序栈、链栈、循环队列、链式队列、单调队列 |
| [第三章 哈希表](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | 拉链法、开放寻址法(线性探测 + 墓碑删除) |
| [第四章 堆与优先队列](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | 大根堆的上浮与下沉 |
| [第五章 树结构](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | 二叉树、BST、树状数组、线段树、懒标记线段树 |
| [第六章 图论算法](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd |
| [第七章 字符串结构:Trie](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | Trie 的三种实现 |
| [第八章 位运算结构](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | 位集 Bitset、布隆过滤器 |
| [附录](#章节 内容 第一章 线性结构 顺序表、单/双链表、链式栈与队列、循环队列、单调队列 第二章 栈与队列 顺序栈、链栈、循环队列、链式队列、单调队列 第三章 哈希表 拉链法、开放寻址法(线性探测 + 墓碑删除) 第四章 堆与优先队列 大根堆的上浮与下沉 第五章 树结构 二叉树、BST、树状数组、线段树、懒标记线段树 第六章 图论算法 DFS、BFS、Prim、Kruskal、Dijkstra、Bellman-Ford、SPFA、Floyd 第七章 字符串结构:Trie Trie 的三种实现 第八章 位运算结构 位集 Bitset、布隆过滤器 附录 C 语言实战技巧与真实缺陷清单) | C 语言实战技巧与真实缺陷清单 |
第一章 线性结构
TL;DR 快速导读
这是一篇以 C 语言源码为核心的数据结构复习笔记,覆盖 8 大章节、31 个源文件、约 3800 行代码。如果你时间有限,按下面方式快速上手:
- 只想快速过一遍:先看每节的「核心数据结构」与「时间复杂度」两行,跳过代码块;重点章节的「对照表」(1.5、3.3、5.6、6.8、7.5)是精华浓缩。
- 准备面试/笔试:优先掌握第一章线性结构、第二章栈与队列、第六章图论算法------这三章是出题重灾区;配合文末「常见面试题与易错点清单」自查。
- 想动手跑代码 :每个
.c文件都自带main测试,可直接编译运行;建议先跑通顺序表、单链表、循环队列、Dijkstra 四个代表。 - 特别提醒 :文中标注了多处真实缺陷 (如单链表
deletesameelemtype的 use-after-free、二叉树LevelOrder的编译错误),阅读时留意⚠️与附录 A.2,这些正是面试官爱挖的坑。
一句话总结:数据结构 = 存储模型 + 操作复杂度 + 边界处理。本文的价值在于把「教科书理论」和「真实 C 代码」一一对应,让你既懂原理、又能写出健壮的实现。
第一章 线性结构
1.1 顺序表(array_impl/sqlist.c)
核心数据结构 :「结构体 + 堆上动态数组」模型。data 是指向 malloc 连续数组的指针,length 是当前有效元素个数。对外接口用 1-based 下标,内部一律 pos-1 访问。
实现要点:
- 插入位置的合法区间是
[1, length],不是[1, length+1]------ 空表无法在"位置 1"插入,是真实存在的边界缺陷 - 插入搬移必须从后往前 (
for (i = length-1; i >= pos-1; i--)),删除搬移从前往后 - 删除时先取值
*e = data[pos-1]再搬移,顺序颠倒会导致被删元素已被覆盖 - 销毁用二级指针
destroy(seqlist** L),free 后置 NULL 防野指针 main中sizeof(list->data)打印的是指针大小(8 字节)而非数组长度
时间复杂度: 随机访问 O(1);尾部追加 O(1);插入/删除 O(n),平均移动 n/2 个元素;查找 O(n)
c
#include <stdio.h>
#include<stdlib.h>
//线性的顺续表;
#define maxsize 100
typedef int elemtype;
typedef struct
{
elemtype* data;
int length;
}seqlist;
seqlist* initlist()
{
seqlist* L = (seqlist*)malloc(sizeof(seqlist));
if (L == NULL)
{
printf("空表\n");
return NULL;
}
L->data = (elemtype*)malloc(maxsize * sizeof(elemtype));
if (L->data == NULL)
{
free(L);
L = NULL;
printf("分配失败\n");
return NULL;
}
L->length = 0;
return L;
}
int appendelem(seqlist* L, elemtype e)//weibujia;
{
if (L == NULL)
{
printf("空表\n");
return 0;
}
if (L->length >= maxsize)
{
return 0;
}
L->data[L->length] = e;
L->length++;
return 1;
}
void listelem(seqlist* L)
{
if (L == NULL)
{
printf("空表\n");
return;
}
for (int i = 0;i < L->length;i++)
{
printf("%d ", L->data[i]);
}
printf("\n");
}
int insertelem(seqlist* L, int pos, elemtype e)
{
if (L == NULL)
{
printf("空表\n");
return 0;
}
if (L->length >= maxsize)
{
return 0;
}
if (pos<1 || pos>L->length)
{
return 0;
}
else{
for (int i = L->length - 1;i >= pos - 1;i--)
{
L->data[i + 1] = L->data[i];
}
L->data[pos - 1] = e;
L->length++;
}
return 1;
}
int deleteelem(seqlist* L, int pos, elemtype* e)
{
if (L == NULL)
{
printf("空表\n");
return 0;
}
if (pos < 1 || pos >L->length)
{
printf("删除位置非法\n");
return 0;
}
else
{
*e = L->data[pos - 1];
for (int i = pos;i < L->length;i++)
{
L->data[i - 1] = L->data[i];
}
}
L->length--;
return 1;
}
int findelem(seqlist* L, elemtype e)//firstfind;
{
if (L == NULL)
{
printf("空表\n");
return 0;
}
for (int i = 0;i < L->length;i++)
{
if (L->data[i] == e)
{
return i + 1;
}
}
return 0;
}
void destroy(seqlist** L)
{
if (L == NULL || *L==NULL)
{
return;
}
free((*L)->data);
(*L)->data = NULL;
free(*L);
*L = NULL;
}
int main()
{
seqlist* list = initlist();
printf("%zu\n", sizeof(list->data));
printf("%d\n", list->length);
appendelem(list, 4);
appendelem(list, 8);
appendelem(list, 64);
appendelem(list, 97);
listelem(list);
insertelem(list, 1, 6);
listelem(list);
elemtype deldata;
deleteelem(list, 2, &deldata);
printf("%d", deldata);
printf("\n");
listelem(list);
printf("%d\n", findelem(list, 6));
destroy(&list);
return 0;
}
1.2 单链表(list_impl/singly_linked_list.c)
核心数据结构 :带头节点的单链表(哨兵头节点),且头节点的 data 被复用为链表长度计数器 ,因此 len(L) 是 O(1)。
关键设计:
- 哨兵头节点消除边界特判:
pos=1时"找前驱"循环体一次不执行,p恰好是头节点 - 插入合法区间
[1, length+1](可插末尾),删除区间[1, length],两者差 1 - 指针操作顺序:插入必须先接后链 (
q->next = p->next; p->next = q;) inserttail先调get_tail再 malloc,尾插 n 次整体 O(n²)
⚠️ 代码中发现的真实缺陷(
deletesameelemtype函数):
free(q); q = NULL;写在 while 循环体内部 ------ 第一次迭代后就释放了辅助数组,后续所有*(q+index)访问都是 use-after-free- 辅助数组按
L->data+1分配,却用abs(p->data)作下标 ------ 数据一大就越界- 函数内
L->data没有同步递减,去重后长度字段与实际节点数不一致
时间复杂度: len O(1);头插 O(1);尾插/查找前驱 O(n)
c
#include<stdio.h>
#include<stdlib.h>
typedef int elemtype;
typedef struct node
{
elemtype data;//数据长度
struct node* next;
}Node;
Node* initlist()
{
Node* head = (Node*)malloc(sizeof(Node));
if (head == NULL)
{
printf("分配失败\n");
return NULL;
}
head->data = 0;
head->next = NULL;
return head;
}
void inserthead(Node* L, elemtype e)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
Node* p = (Node*)malloc(sizeof(Node));
if (p == NULL)
{
printf("分配失败\n");
return;
}
p->data = e;
p->next = L->next;
L->next = p;
L->data++;
}
Node* get_tail(Node* L)
{
if (L == NULL)
{
printf("空链表\n");
return NULL;
}
Node* p = L;
while (p->next != NULL)
{
p = p->next;
}
return p;
}
Node* inserttail(Node* L, elemtype e)
{
if (L == NULL)
{
printf("空链表\n");
return NULL;
}
Node* tail = get_tail(L);
Node* p = (Node*)malloc(sizeof(Node));
if (p == NULL)
{
printf("分配失败\n");
return NULL;
}
p->data = e;
p->next = NULL;
tail->next = p;
L->data++;
return p;
}
void listNode(Node* L)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
Node* p = L->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
int len(Node* L)
{
if (L == NULL)
{
printf("空链表\n");
return -1;
}
return L->data;
}
void insertelemtype(Node* L, elemtype pos, elemtype e)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
int length = len(L);
if (pos<1 || pos>length + 1)
{
printf("插入位置非法\n");
return;
}
Node* p = L;
int i = 0;
while (i < pos - 1)//找前驱
{
p = p->next;
i++;
}
Node* q = (Node*)malloc(sizeof(Node));
if (q == NULL)
{
printf("分配失败\n");
return;
}
q->data = e;
q->next = p->next;
p->next = q;
L->data++;
}
void deleteelemtype(Node* L, elemtype pos)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
int length = len(L);
if (pos<1 || pos>length)
{
printf("删除位置非法\n");
return;
}
Node* p = L;
int i = 0;
while (i < pos - 1)//找前驱
{
p = p->next;
i++;
}
Node* q = p->next;
p->next = q->next;
L->data--;
free(q);
q = NULL;
}
void freelist(Node* L)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
Node* p = L->next;
while (p != NULL)
{
Node* q = p->next;
free(p);
p = q;
}
free(L);
L = NULL;
}
void deletesameelemtype(Node* L)//简易的hash
{
if (L == NULL)
{
printf("空链表\n");
return;
}
Node* p = L->next;
int index;
int* q = (int*)malloc((L->data + 1) * sizeof(int));
if (q == NULL)
{
printf("分配失败\n");
return;
}
for (int i = 0;i < L->data + 1;i++)
{
*(q + i) = 0;
}
while (p != NULL)
{
index = abs(p->data);
if (*(q + index) == 0)
{
*(q + index) = 1;
p = p->next;
}
else
{
Node* temp = p->next;
p->next = temp->next;
free(temp);
temp = NULL;
}
free(q);
q = NULL;
}
}
//void reverselist(Node* L)
//{
// Node* first = NULL;
// Node* second = L->next;
// Node* third;
// while (second != NULL)
// {
// third = second->next;
// second->next = first;
// first = second;
// second = third;
// }
// Node* hd = initlist();
// hd->next = first;
//}
//int iscycle(Node* L)//快慢指针
//{
// Node* fast = L;
// Node* slow = L;
// while (fast != NULL && fast->next != NULL)
// {
// fast = fast->next->next;
// slow = slow->next;
// if (fast == slow)
// {
// return 1;
// }
//
// }
// return 0;
//}
//Node* findbegin(Node* L)
//{
// Node* fast = L;
// Node* slow = L;
// while (fast != NULL && fast->next != NULL)
// {
// fast = fast->next->next;
// slow = slow->next;
// Node* p = fast;
// int count = 1;
// if (fast == slow)
// {
// while (p->next != slow)
// {
// count++;
// }
// }
// fast = L;
// slow = L;
// for (int i = 0;i < count;i++)
// {
// fast = fast->next;
// }
// while (fast != slow)
// {
// fast = fast->next;
// slow = slow->next;
// }
// return slow;
// }
// return NULL;
//}
// 测试主函数:验证所有链表操作功能
int main()
{
// 1. 初始化链表
Node* L = initlist();
if (L == NULL) return 1; // 初始化失败直接退出程序
printf("=== 初始化后 ===\n");
printf("链表长度:%d\n", len(L)); // 输出0
listNode(L); // 输出"链表无有效数据"
// 2. 头插法插入元素(1、2、3)
printf("\n=== 头插1、2、3后 ===\n");
inserthead(L, 3);
inserthead(L, 2);
inserthead(L, 1);
printf("链表长度:%d\n", len(L)); // 输出3
listNode(L); // 输出"1 2 3"
Node* tail1= get_tail(L);
// 3. 尾插法插入元素(4、5)
printf("\n=== 尾插4、5后 ===\n");
inserttail(L, 4);
inserttail(L, 5);
printf("链表长度:%d\n", len(L)); // 输出5
listNode(L); // 输出"1 2 3 4 5"
// 4. 指定位置插入(位置3插入99)
printf("\n=== 在位置3插入99后 ===\n");
insertelemtype(L, 3, 99);
printf("链表长度:%d\n", len(L)); // 输出6
listNode(L); // 输出"1 2 99 3 4 5"
/*Node* tail2 = get_tail(L);
tail2->next = tail1;
Node* p = findbegin(L);
printf("%d\n", p->data);*/
/*int k;
scanf("%d", &k);
Node* p = L->next;
for (int i = 0;i <L->data - k;i++)
{
p = p->next;
}
printf("%d\n", p->data);
Node* fast = L->next;
Node* slow = L->next;
for (int i = 0;i < k;i++)
{
fast = fast->next;
}
while (fast != NULL)
{
fast = fast->next;
slow = slow->next;
}
printf("%d\n", slow->data);*/
//5. 非法位置插入测试(位置10)
printf("\n=== 非法位置(10)插入测试 ===\n");
insertelemtype(L, 10, 100); // 提示"插入位置非法"
// 6. 指定位置删除(位置3的99)
printf("\n=== 删除位置3的元素后 ===\n");
deleteelemtype(L, 3);
printf("链表长度:%d\n", len(L)); // 输出5
listNode(L); // 输出"1 2 3 4 5"
// 7. 非法位置删除测试(位置0)
printf("\n=== 非法位置(0)删除测试 ===\n");
deleteelemtype(L, 0); // 提示"删除位置非法"
// 8. 释放链表并验证
printf("\n=== 释放链表后 ===\n");
freelist(L);
L = NULL; // 手动置空,避免野指针
listNode(L); // 输出"空链表,无法遍历"
return 0;
}
1.3 双向链表(list_impl/doubly_linked_list.c)
核心数据结构 :带头节点的双向链表,同样用头节点 data 存长度。头节点 pre 恒为 NULL,尾节点 next 恒为 NULL(非循环结构)。
实现要点:
- 头插四步维护:
p->pre = L; p->next = L->next; if (L->next != NULL) L->next->pre = p; L->next = p; - 第 3 步的
if判空是双向链表头插进空表时的必需保护,否则解引用 NULL - 删除时
if (q->next != NULL) q->next->pre = p;------ 删除尾节点时跳过。这是双向链表最易错的地方 reverseListNode逆序遍历(走pre指针)是独有的"自检"手段:漏改任何pre都会立刻暴露freelist(Node** L)用二级指针,真正把调用方的指针置空,比单链表版freelist(Node*)更严谨
时间复杂度: len O(1);头插 O(1);尾插 O(n);已知节点指针时删除该节点 O(1)(单链表做不到)
c
#include<stdio.h>
#include<stdlib.h> // 必须包含,否则malloc/free报错
typedef int elemtype;
// 双向链表节点结构(带头节点)
typedef struct node {
elemtype data; // 头节点:存储链表长度;普通节点:存储数据
struct node* next; // 后继指针
struct node* pre; // 前驱指针
} Node;
// 1. 初始化双向链表(带头节点)
Node* initlist() {
Node* head = (Node*)malloc(sizeof(Node));
if (head == NULL) {
printf("头节点分配失败\n");
return NULL;
}
head->data = 0; // 头节点存储链表长度
head->next = NULL;
head->pre = NULL; // 头节点无前驱
return head;
}
// 2. 获取链表长度
int len(Node* L) {
if (L == NULL) {
printf("空链表\n");
return -1;
}
return L->data;
}
// 3. 头插法插入元素
void inserthead(Node* L, elemtype e) {
if (L == NULL) {
printf("空链表\n");
return;
}
Node* p = (Node*)malloc(sizeof(Node));
if (p == NULL) {
printf("节点分配失败\n");
return;
}
p->data = e;
p->pre = L; // 新节点前驱指向头节点
p->next = L->next; // 新节点后继指向原第一个节点
if (L->next != NULL) { // 若原链表非空,原第一个节点前驱指向新节点
L->next->pre = p;
}
L->next = p; // 头节点后继指向新节点
L->data++; // 长度+1
}
// 4. 获取链表尾节点
Node* get_tail(Node* L) {
if (L == NULL) {
printf("空链表\n");
return NULL;
}
Node* p = L;
while (p->next != NULL) {
p = p->next;
}
return p;
}
// 5. 尾插法插入元素
Node* inserttail(Node* L, elemtype e) {
if (L == NULL) {
printf("空链表\n");
return NULL;
}
Node* tail = get_tail(L);
Node* p = (Node*)malloc(sizeof(Node));
if (p == NULL) {
printf("节点分配失败\n");
return NULL;
}
p->data = e;
p->pre = tail; // 新节点前驱指向尾节点
p->next = NULL; // 新节点后继为空
tail->next = p; // 原尾节点后继指向新节点
L->data++; // 长度+1
return p;
}
// 6. 指定位置插入元素(pos从1开始)
void insertelemtype(Node* L, elemtype pos, elemtype e) {
if (L == NULL) {
printf("空链表\n");
return;
}
int length = len(L);
if (pos < 1 || pos > length + 1) {
printf("插入位置非法\n");
return;
}
Node* p = L;
int i = 0;
while (i < pos - 1) { // 找插入位置的前驱节点
p = p->next;
i++;
}
Node* q = (Node*)malloc(sizeof(Node));
if (q == NULL) {
printf("节点分配失败\n");
return;
}
q->data = e;
q->pre = p; // 新节点前驱指向p
q->next = p->next; // 新节点后继指向p的后继
// 关键:处理边界(p->next可能为空)
if (p->next != NULL) {
p->next->pre = q; // 原后继节点的前驱指向新节点
}
p->next = q; // p的后继指向新节点
L->data++; // 长度+1
}
// 7. 指定位置删除元素
void deleteelemtype(Node* L, elemtype pos) {
if (L == NULL) {
printf("空链表\n");
return;
}
int length = len(L);
if (pos < 1 || pos > length) {
printf("删除位置非法\n");
return;
}
Node* p = L;
int i = 0;
while (i < pos - 1) { // 找删除位置的前驱节点
p = p->next;
i++;
}
Node* q = p->next; // q是待删除节点
// 关键:处理边界(q->next可能为空)
if (q->next != NULL) {
q->next->pre = p; // 待删节点的后继节点前驱指向p
}
p->next = q->next; // p的后继指向待删节点的后继
L->data--; // 长度-1
free(q); // 释放节点内存
q = NULL; // 避免野指针
}
// 8. 遍历双向链表(正序)
void listNode(Node* L) {
if (L == NULL) {
printf("空链表\n");
return;
}
Node* p = L->next;
if (p == NULL) {
printf("链表无有效数据\n");
return;
}
printf("正序遍历:");
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 9. 反向遍历双向链表(验证pre指针)
void reverseListNode(Node* L) {
if (L == NULL) {
printf("空链表\n");
return;
}
Node* tail = get_tail(L);
if (tail == L) { // 无有效节点
printf("链表无有效数据\n");
return;
}
printf("逆序遍历:");
while (tail != L) {
printf("%d ", tail->data);
tail = tail->pre;
}
printf("\n");
}
// 10. 释放双向链表内存
void freelist(Node**L) {
if (L == NULL || *L==NULL) {
printf("空链表\n");
return;
}
Node* p = (*L)->next;
while (p != NULL) {
Node* q = p->next;
free(p);
p = q;
}
free(*L);
*L = NULL;
}
// 测试主函数
int main() {
// 1. 初始化链表
Node* L = initlist();
if (L == NULL) return 1;
printf("=== 初始化后 ===\n");
printf("链表长度:%d\n", len(L));
listNode(L);
// 2. 头插法插入
printf("\n=== 头插1、2、3后 ===\n");
inserthead(L, 3);
inserthead(L, 2);
inserthead(L, 1);
listNode(L);
reverseListNode(L);
printf("链表长度:%d\n", len(L));
// 3. 尾插法插入
printf("\n=== 尾插4、5后 ===\n");
inserttail(L, 4);
inserttail(L, 5);
listNode(L);
reverseListNode(L);
printf("链表长度:%d\n", len(L));
// 4. 指定位置插入
printf("\n=== 位置3插入99后 ===\n");
insertelemtype(L, 3, 99);
listNode(L);
reverseListNode(L);
printf("链表长度:%d\n", len(L));
// 5. 指定位置删除
printf("\n=== 删除位置3的元素后 ===\n");
deleteelemtype(L, 3);
listNode(L);
reverseListNode(L);
printf("链表长度:%d\n", len(L));
// 6. 释放链表
freelist(&L);
return 0;
}
1.4 全局地址索引单链表(list_impl/linked_list_global.c)
核心数据结构 :顺序无关的单链表 + 全局"插入序号 → 结点地址"映射表 addr[]。addr[k] 是第 k 个被插入结点的地址,与它在链表中的实际位置无关。
关键技巧:
addr[]指针数组把"按插入序号定位"从 O(n) 降到 O(1),用 800 KB 空间换时间idx只在插入时自增、删除时不自减 ------ 序号是"插入时间戳",永不回收重排deleteAfterK用k==0特判删除头结点的后继,把哨兵当作"第 0 号虚拟节点",消除首元素删除的特判scanf(" %c", &op)中%c前的空格用于跳过换行,是竞赛输入处理的必备技巧- 被删除结点的
addr[k]成为悬垂指针(use-after-free 隐患),题面保证不会引用已删除序号
时间复杂度: 头插/指定位置插入/删除均 O(1);全部 m 条指令 O(m)
c
#include<stdio.h>
#include<stdlib.h>
typedef int elemtype;
typedef struct node
{
elemtype data;
struct node* next;
}Node;
// 新增:存储每个插入数的结点地址,下标对应插入序号(1~100000)
Node* addr[100010];
// 新增:全局插入序号,每插入一个数+1
int idx = 0;
// 初始化链表(带头结点,无需存储长度)
Node* initlist()
{
Node* head = (Node*)malloc(sizeof(Node));
if (head == NULL)
{
printf("分配失败\n");
return NULL;
}
head->next = NULL;
return head;
}
// 修改:头插操作 + 记录插入序号和结点地址
void inserthead(Node* L, elemtype e)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
Node* p = (Node*)malloc(sizeof(Node));
if (p == NULL)
{
printf("分配失败\n");
return;
}
p->data = e;
p->next = L->next;
L->next = p;
// 新增:序号+1,记录当前结点地址
addr[++idx] = p;
}
// 新增:在第k个插入的数后面插入e
void insertAfterK(Node* L, int k, elemtype e)
{
if (L == NULL || k > idx)
{
return;
}
// 直接通过addr[k]找到第k个插入的数的结点
Node* p = addr[k];
Node* q = (Node*)malloc(sizeof(Node));
q->data = e;
q->next = p->next;
p->next = q;
// 新增:序号+1,记录新结点地址
addr[++idx] = q;
}
// 新增:删除第k个插入的数后面的数(k=0删除头结点)
void deleteAfterK(Node* L, int k)
{
if (L == NULL)
{
return;
}
Node* p;
if (k == 0)
{
// k=0时,前驱是头结点
p = L;
}
else
{
// 直接通过addr[k]找到第k个插入的数的结点
p = addr[k];
}
// 题目保证操作合法,无需判断p->next是否为空
Node* q = p->next;
p->next = q->next;
free(q);
}
// 原遍历函数,无需修改
void listNode(Node* L)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
Node* p = L->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 原释放函数,无需修改
void freelist(Node* L)
{
if (L == NULL)
{
printf("空链表\n");
return;
}
Node* p = L->next;
while (p != NULL)
{
Node* q = p->next;
free(p);
p = q;
}
free(L);
L = NULL;
}
// 修改main函数,适配题目操作逻辑
int main()
{
int m;
scanf("%d", &m);
Node* L = initlist();
if (L == NULL)
{
return 1;
}
for (int i = 0; i < m; i++)
{
char op;
// 空格跳过换行/空格,避免读取错误
scanf(" %c", &op);
if (op == 'H')
{
int x;
scanf("%d", &x);
inserthead(L, x);
}
else if (op == 'D')
{
int k;
scanf("%d", &k);
deleteAfterK(L, k);
}
else if (op == 'I')
{
int k, x;
scanf("%d %d", &k, &x);
insertAfterK(L, k, x);
}
}
listNode(L);
freelist(L);
return 0;
}
1.5 四种线性结构对比
| 维度 | 顺序表 | 单链表 | 双向链表 | 全局地址索引单链表 |
|---|---|---|---|---|
| 插入 | 尾部 O(1),中间/头部 O(n)(需搬移元素) | 头插 O(1),指定位置 O(n)(需找前驱) | 头插 O(1),指定位置 O(n)(需找前驱) | 任意位置 O(1)(通过 addr[] 直接定位) |
| 删除 | 尾部 O(1),中间/头部 O(n)(需搬移元素) | 头删 O(1),指定位置 O(n)(需找前驱) | 已知节点指针时 O(1),否则 O(n) | 任意位置 O(1)(通过 addr[] 直接定位) |
| 查找 | 按下标随机访问 O(1);按值查找 O(n) | 按值查找 O(n);无随机访问 | 按值查找 O(n);无随机访问 | 按插入序号定位 O(1);按值查找 O(n) |
| 空间开销 | 连续内存,无指针开销;但需预分配容量,可能浪费 | 每节点多 1 个指针(8 字节),无容量浪费 | 每节点多 2 个指针(16 字节),无容量浪费 | 每节点多 1 个指针 + 全局 addr[] 数组(约 800 KB 固定开销) |
| 适用场景 | 频繁随机访问、元素数量可预估、尾部增删为主 | 频繁头部增删、内存碎片敏感、无需随机访问 | 需要双向遍历、频繁删除已知节点 | 大量"按插入序号"定位操作、以时间换空间的特殊需求 |
简要说明:
- 顺序表 的核心优势是随机访问 O(1) ,代价是插入/删除需要搬移元素,且容量固定(
maxsize=100)可能造成空间浪费或溢出。 - 单链表 用指针换取了插入/删除的灵活性(头插 O(1)),但无法随机访问,且删除已知节点仍需 O(n) 找前驱。
- 双向链表 在单链表基础上增加
pre指针,换来已知节点删除 O(1) 和逆序遍历能力,代价是每节点多 8 字节空间。 - 全局地址索引单链表 是本文档的特色实现:用全局
addr[]映射表把"按插入序号定位"从 O(n) 降到 O(1),本质是用约 800 KB 空间换时间,适合插入/删除/定位都频繁且序号有意义的场景。
第二章 栈与队列
2.1 顺序栈(array_impl/stack.c)
核心数据结构 :动态数组 + 栈顶下标 top。top == -1 表示空栈,top 指向当前栈顶元素本身,有效元素个数 = top + 1。
实现要点:
IsEmpty三态返回值(-1/0/1),消费时用if (IsEmpty(s) == 1)精确匹配 1,使 -1(指针为空)不被误判为空栈- 入栈顺序不可颠倒:先
s->top++再s->data[s->top] = e - 出栈顺序同样关键:先
*e = s->data[s->top]再s->top-- - 判满用
top >= MAXSIZE - 1,而非top == MAXSIZE(top是元素下标) GetTop与Pop的差异就是"读 vs 读+删",是栈 ADT 的两个独立操作
时间复杂度: Push / Pop / GetTop / IsEmpty 全部 O(1)
c
#include <stdio.h>
#include <stdlib.h>
// 栈的最大容量
#define MAXSIZE 100
// 栈中元素的类型
typedef int ElemType;
// 顺序栈的结构体定义(动态数组实现)
typedef struct {
ElemType *data; // 存储栈元素的动态数组
int top; // 栈顶指针(初始为-1,指向栈顶元素)
} Stack;
/**
* @brief 初始化栈
* @return Stack* 初始化成功返回栈指针,失败返回NULL
*/
Stack* InitStack() {
// 分配栈结构体内存
Stack *s = (Stack *)malloc(sizeof(Stack));
if (s == NULL) {
printf("栈结构体内存分配失败!\n");
return NULL;
}
// 分配栈元素数组内存
s->data = (ElemType *)malloc(sizeof(ElemType) * MAXSIZE);
if (s->data == NULL) {
printf("栈元素数组内存分配失败!\n");
free(s); // 避免内存泄漏
return NULL;
}
s->top = -1; // 空栈标识
return s;
}
/**
* @brief 判断栈是否为空
* @param s 栈指针(需非空)
* @return 1-空栈,0-非空栈
*/
int IsEmpty(Stack *s) {
if (s == NULL) {
printf("错误:栈指针为空!\n");
return -1; // 异常标识
}
return s->top == -1 ? 1 : 0;
}
/**
* @brief 入栈操作
* @param s 栈指针(需非空)
* @param e 待入栈的元素
* @return 1-成功,0-失败(栈满/参数错误)
*/
int Push(Stack *s, ElemType e) {
// 参数合法性检查
if (s == NULL) {
printf("错误:栈指针为空!\n");
return 0;
}
// 栈满检查
if (s->top >= MAXSIZE - 1) {
printf("错误:栈已满,无法入栈!\n");
return 0;
}
s->top++; // 栈顶指针上移
s->data[s->top] = e; // 元素入栈
return 1;
}
/**
* @brief 出栈操作
* @param s 栈指针(需非空)
* @param e 用于接收出栈元素的指针(需非空)
* @return 1-成功,0-失败(栈空/参数错误)
*/
int Pop(Stack *s, ElemType *e) {
// 参数合法性检查
if (s == NULL || e == NULL) {
printf("错误:栈指针或元素接收指针为空!\n");
return 0;
}
// 栈空检查
if (IsEmpty(s) == 1) {
printf("错误:栈为空,无法出栈!\n");
return 0;
}
*e = s->data[s->top]; // 取出栈顶元素
s->top--; // 栈顶指针下移
return 1;
}
/**
* @brief 获取栈顶元素(不出栈)
* @param s 栈指针(需非空)
* @param e 用于接收栈顶元素的指针(需非空)
* @return 1-成功,0-失败(栈空/参数错误)
*/
int GetTop(Stack *s, ElemType *e) {
// 参数合法性检查
if (s == NULL || e == NULL) {
printf("错误:栈指针或元素接收指针为空!\n");
return 0;
}
// 栈空检查
if (IsEmpty(s) == 1) {
printf("错误:栈为空,无栈顶元素!\n");
return 0;
}
*e = s->data[s->top]; // 仅读取栈顶元素,不修改栈顶指针
return 1;
}
/**
* @brief 销毁栈(释放所有动态内存)
* @param s 栈指针的地址(需非空)
*/
void DestroyStack(Stack **s) {
if (s == NULL || *s == NULL) {
return; // 无需销毁
}
// 先释放元素数组内存
free((*s)->data);
(*s)->data = NULL;
// 再释放栈结构体内存
free(*s);
*s = NULL; // 避免野指针
}
// 测试示例
int main() {
Stack *s = InitStack();
if (s == NULL) {
return -1;
}
// 测试入栈
ElemType e;
Push(s, 10);
Push(s, 20);
Push(s, 30);
// 测试获取栈顶
if (GetTop(s, &e)) {
printf("栈顶元素:%d\n", e); // 输出:30
}
// 测试出栈
if (Pop(s, &e)) {
printf("出栈元素:%d\n", e); // 输出:30
}
if (Pop(s, &e)) {
printf("出栈元素:%d\n", e); // 输出:20
}
// 测试栈空
printf("栈是否为空:%d\n", IsEmpty(s)); // 输出:0(非空)
Pop(s, &e); // 出栈10
printf("栈是否为空:%d\n", IsEmpty(s)); // 输出:1(空)
// 销毁栈
DestroyStack(&s);
return 0;
}
2.2 链式栈(list_impl/stack.c)
核心数据结构 :单链表 + 哨兵头节点。栈顶 = s->next,push 即"哨兵后头插",pop 即"删除哨兵后继"。无需容量字段、无固定上限。
实现要点:
- 头插法 = 压栈:
p->next = s->next; s->next = p;与单链表的inserthead完全同构。 pop三步:q = s->next; *e = q->data; s->next = q->next; free(q);------判空必须在取q->data之前。- 源码注释中保留了真实调试记录 :作者最初误写
if (s->next == NULL) return 0;,把空栈当作不可入栈。链式栈没有容量上限,任何时刻都必须允许入栈。 isempty中的printf已被注释掉,体现了"判断函数不应有副作用"的原则。
时间复杂度: push / pop / isempty 均 O(1);空间按需增长,无溢出风险
c
#include <stdio.h>
#include <stdlib.h>
// 单链表实现栈(带哨兵头节点)
typedef int elemtype;
typedef struct stack {
elemtype data;
struct stack* next;
} Stack;
/**
* 初始化栈:创建哨兵头节点(头节点不存实际数据,仅作占位)
* @return 哨兵头节点指针,失败返回NULL
*/
Stack* initStack() {
Stack* s = (Stack*)malloc(sizeof(Stack));
if (s == NULL) { // 补充内存分配失败检查
printf("头节点分配失败!\n");
return NULL;
}
s->data = 0; // 头节点数据域无意义,仅占位
s->next = NULL; // 初始空栈:头节点next为NULL
return s;
}
/**
* 判断栈是否为空(基于哨兵头节点)
* @param s 哨兵头节点指针
* @return 1-空栈,0-非空(修正原返回值语义)
*/
int isempty(Stack* s) {
if (s == NULL) { // 补充空指针检查
printf("错误:栈头节点为空!\n");
return -1;
}
if (s->next == NULL) {
// printf("空栈\n"); // 建议把打印剥离,函数仅做判断
return 1; // 空栈返回1(符合常规语义)
} else {
return 0; // 非空返回0
}
}
/**
* 压栈(头插法:新节点插在哨兵头节点后,即栈顶)
* @param s 哨兵头节点指针
* @param e 待入栈元素
* @return 1-成功,0-失败
*/
int push(Stack* s, elemtype e) {
if (s == NULL) { // 空指针检查
printf("错误:栈头节点为空!\n");
return 0;
}
// 原代码错误:s->next==NULL是空栈,空栈反而不能入栈?这是逻辑颠倒
// 空栈时更要允许入栈,删除这个错误判空
Stack* p = (Stack*)malloc(sizeof(Stack));
if (p == NULL) {
printf("分配失败\n");
return 0;
}
p->data = e;
p->next = s->next; // 新节点指向原栈顶(头节点的下一个节点)
s->next = p; // 头节点指向新节点,新节点成为新栈顶
return 1;
}
/**
* 弹栈(仅操作栈顶:哨兵头节点的下一个节点)
* @param s 哨兵头节点指针
* @param e 接收弹栈元素的指针
* @return 1-成功,0-失败
*/
int pop(Stack* s, elemtype* e) {
// 空指针+接收指针检查
if (s == NULL || e == NULL) {
printf("错误:栈头节点或接收指针为空!\n");
return 0;
}
// 空栈检查
if (isempty(s) == 1) {
printf("空栈,无法弹栈!\n");
return 0;
}
Stack* q = s->next; // 暂存栈顶节点(头节点的下一个节点)
*e = q->data; // 取出栈顶数据
s->next = q->next; // 头节点指向原栈顶的下一个节点,更新栈顶
free(q); // 释放原栈顶节点
q = NULL;
return 1;
}
// 测试示例
int main() {
Stack* s = initStack();
if (s == NULL) return -1;
elemtype e;
// 压栈:10→20→30
push(s, 10);
push(s, 20);
push(s, 30);
// 弹栈:30→20→10
pop(s, &e); printf("弹栈元素:%d\n", e); // 30
pop(s, &e); printf("弹栈元素:%d\n", e); // 20
pop(s, &e); printf("弹栈元素:%d\n", e); // 10
pop(s, &e); // 空栈弹栈,提示错误
// 销毁栈(补充销毁逻辑,避免内存泄漏)
Stack* p = s;
while (p != NULL) {
Stack* temp = p;
p = p->next;
free(temp);
}
return 0;
}
2.3 循环队列(array_impl/circular_queue.c)
核心数据结构 :环形数组 + 双下标。front 指向队头元素,rear 指向队尾元素的下一个空位,通过 % maxsize 实现环形回绕。
实现要点:
- 判空条件:
front == rear - 判满采用**"牺牲一个存储单元"法**:
(rear + 1) % maxsize == front。队满时仍有一个空位,实际最多存maxsize-1个元素 - 三种判满方案:① 牺牲一个单元(本文件)② 增设
size计数 ③ 增设tag标志 dequeue返回值有歧义:空队列返回 0,但 0 也可能是合法元素(作者已在注释中自行指出)------ 返回"值"的接口无法表达错误,这正是顺序栈用状态码+出参的原因- 元素个数公式:
(rear - front + maxsize) % maxsize
时间复杂度: enqueue / dequeue / isempty 均 O(1)
c
#include <stdio.h>
#include <stdlib.h>
#define maxsize 100
typedef int elemtype;
typedef struct
{
elemtype* data;
int front; // 队头指针(指向队头元素)
int rear; // 队尾指针(指向队尾元素的下一个位置)
} Queue;
// 初始化队列(动态分配内存)
Queue* initQueue()
{
Queue* Q = (Queue*)malloc(sizeof(Queue));
// 增加内存分配失败的判断(避免空指针)
if (Q == NULL) {
printf("队列结构体内存分配失败!\n");
return NULL;
}
Q->data = (int*)malloc(maxsize * sizeof(int));
if (Q->data == NULL) {
printf("队列数据数组内存分配失败!\n");
free(Q); // 释放已分配的结构体,避免内存泄漏
return NULL;
}
Q->front = 0;
Q->rear = 0;
return Q;
}
// 判断队列是否为空:空返回1,非空返回0
int isempty(Queue* Q)
{
// 增加空指针判断
if (Q == NULL) {
printf("队列未初始化!\n");
return -1; // 用-1标记异常
}
if (Q->front == Q->rear)
{
printf("空队列\n");
return 1;
}
else
{
return 0;
}
}
// 出队操作:返回出队元素,空队列返回0(注意:元素0会有歧义)
elemtype dequeue(Queue* Q)
{
if (Q == NULL) {
printf("队列未初始化,无法出队!\n");
return -1; // 用-1标记异常(假设元素不会是-1)
}
if (Q->front == Q->rear)
{
printf("空队列\n");
return 0;
}
elemtype e = Q->data[Q->front];
Q->front = (Q->front + 1) % maxsize;
return e;
}
// 入队操作:成功返回1,队列满返回0
int enqueue(Queue* Q, elemtype e)
{
if (Q == NULL) {
printf("队列未初始化,无法入队!\n");
return 0;
}
if ((Q->rear + 1) % maxsize == Q->front)
{
printf("队列满了,元素 %d 入队失败!\n", e);
return 0;
}
Q->data[Q->rear] = e;
Q->rear = (Q->rear + 1) % maxsize;
printf("元素 %d 入队成功\n", e);
return 1;
}
// 新增:销毁队列(释放所有动态内存,避免内存泄漏)
void destroyQueue(Queue** Q) {
if (*Q == NULL) return;
free((*Q)->data); // 释放数据数组
free(*Q); // 释放队列结构体
*Q = NULL; // 置空,避免野指针
printf("队列已销毁\n");
}
// 测试主函数:验证队列的入队、出队、判空功能
int main() {
// 1. 初始化队列
Queue* Q = initQueue();
if (Q == NULL) {
return -1; // 初始化失败,退出程序
}
// 2. 初始状态:判空
printf("=== 初始状态 ===\n");
isempty(Q);
// 3. 入队测试
printf("\n=== 入队操作 ===\n");
enqueue(Q, 10);
enqueue(Q, 20);
enqueue(Q, 30);
// 4. 再次判空(非空)
printf("\n=== 入队后判空 ===\n");
isempty(Q);
// 5. 出队测试
printf("\n=== 出队操作 ===\n");
printf("出队元素:%d\n", dequeue(Q));
printf("出队元素:%d\n", dequeue(Q));
printf("出队元素:%d\n", dequeue(Q));
// 6. 空队列出队
printf("\n=== 空队列出队 ===\n");
dequeue(Q);
// 7. 销毁队列
printf("\n=== 销毁队列 ===\n");
destroyQueue(&Q);
return 0;
}
2.4 链式队列(list_impl/queue.c)
核心数据结构 :双层结构体------Node 是链表结点,Queue 内含 front 与 rear 两个指针。front 指向哨兵头节点,rear 指向最后一个数据节点。
实现要点:
- 链式队列必须同时维护
front和rear:只有front则入队 O(n),只有rear则出队 O(n)。 - 入队只改
rear:q->rear->next = node; q->rear = node;------因有rear指针,入队是 O(1)。 - 出队操作的是
front->next而非front,因为front是哨兵、不存数据。 - 最易错的一步 :出队最后一个元素时必须重置
rear = front,否则rear指向已 free 的节点,下次入队就是 use-after-free。 dequeue用出参elemtype *e而非返回值,规避了循环队列"返回 0 无法区分空队与元素 0"的歧义
时间复杂度: enqueue / dequeue 均 O(1);空间 O(n) 动态增长,无需预估容量
c
#include<stdio.h>
#include<stdlib.h>
// 定义队列元素类型
typedef int elemtype;
// 定义链表节点结构
typedef struct Node
{
elemtype data;
struct Node* next;
} Node;
// 定义队列结构(包含队头、队尾指针)
typedef struct
{
Node* front; // 队头指针(指向头节点,非第一个元素)
Node* rear; // 队尾指针(指向最后一个元素节点)
} Queue;
// 初始化队列(创建头节点,队头队尾指向头节点)
Queue* initQueue()
{
Queue* q = (Queue*)malloc(sizeof(Queue));
if (q == NULL) { // 内存分配失败判断
printf("队列结构体内存分配失败!\n");
exit(1);
}
Node* node = (Node*)malloc(sizeof(Node));
if (node == NULL) { // 头节点内存分配失败判断
printf("头节点内存分配失败!\n");
free(q); // 释放已分配的队列结构体
exit(1);
}
node->data = 0; // 头节点数据域无意义,仅作占位
node->next = NULL;
q->front = node;
q->rear = node;
return q;
}
// 判断队列是否为空(队头队尾指向同一节点即为空)
int isempty(Queue* q)
{
if (q == NULL) { // 空指针保护
printf("队列指针为空!\n");
return -1;
}
return q->front == q->rear ? 1 : 0;
}
// 入队操作(尾插法)
void enqueue(Queue* q, elemtype e)
{
if (q == NULL) {
printf("队列指针为空,无法入队!\n");
return;
}
Node* node = (Node*)malloc(sizeof(Node));
if (node == NULL) {
printf("入队节点内存分配失败!\n");
return;
}
node->data = e;
node->next = NULL;
q->rear->next = node; // 原队尾节点指向新节点
q->rear = node; // 队尾指针移动到新节点
}
// 出队操作(移除队头元素,通过指针返回元素值)
void dequeue(Queue* q, elemtype *e)
{
if (q == NULL || e == NULL) {
printf("队列指针或返回值指针为空,无法出队!\n");
return;
}
if (isempty(q)) { // 队空判断
printf("队列为空,无法出队!\n");
return;
}
Node* node = q->front->next; // 指向第一个元素节点
*e = node->data; // 取出元素值
q->front->next = node->next; // 头节点指向第二个元素节点
// 若出队的是最后一个元素,需将队尾指针重置为头节点
if (q->rear == node) {
q->rear = q->front;
}
free(node); // 释放出队节点内存
node = NULL; // 避免野指针
}
// 测试函数(验证队列功能)
int main()
{
Queue* q = initQueue();
elemtype val;
// 测试判空
printf("队列是否为空:%d(1=空,0=非空)\n", isempty(q));
// 入队
enqueue(q, 10);
enqueue(q, 20);
enqueue(q, 30);
printf("入队 10、20、30 后,队列是否为空:%d\n", isempty(q));
// 出队
dequeue(q, &val);
printf("出队元素:%d\n", val);
dequeue(q, &val);
printf("出队元素:%d\n", val);
// 再次判空
printf("出队两次后,队列是否为空:%d\n", isempty(q));
// 继续出队(测试队空时出队)
dequeue(q, &val);
printf("出队元素:%d\n", val);
dequeue(q, &val); // 队空时出队,会提示错误
return 0;
}
2.5 单调队列(array_impl/monotonic_queue.c)
核心数据结构 :数组模拟双端队列,q[] 存的是元素下标 而非数值------既能通过 a[q[front]] 取值,又能用下标判断是否滑出窗口。
算法流程(每轮严格三步):
- 维护单调性 :
while (front <= rear && a[i] >= a[q[rear]]) rear--;------用>=把相等元素也弹出 - 当前下标入队 :
q[++rear] = i;------这行必须在 while 之外(源码注释特意强调) - 移除滑出窗口的队头 :
while (q[front] <= i - k) front++; - 窗口成形后输出 :
if (i >= k - 1) printf("%d ", a[q[front]]);
正确性核心论证: 若 a[i] >= a[q[rear]],则 a[q[rear]] 既比 a[i] 小、又比 a[i] 先过期,永远不可能再成为任何窗口的最大值------这就是可以弹出队尾的依据。
时间复杂度: 整体 O(n) 。虽然外层 for 有 n 次、内层有 while,但每个下标最多入队一次、被弹出两次,总操作 ≤ 2n------均摊分析的经典案例
c
#include <stdio.h>
#include <stdlib.h>
#define MAXN 1000005
int a[MAXN], q[MAXN]; // q存下标,模拟双端队列
int front = 0, rear = -1;
void slidingWindowMax(int n, int k) {
int i;
for (i = 0; i < n; i++) { // 这个for循环包裹所有三步逻辑
// 1. 维护单调性:队尾≤当前值则弹出(清理不符合条件的队尾元素)
while (front <= rear && a[i] >= a[q[rear]]) {
rear--;
}
// 清理完成后,将当前元素下标入队尾(这行要在while循环外面)
q[++rear] = i;
// 8 3
// 1 3 - 1 - 3 5 3 6 7
// 2. 移除窗口外的队头(判断队头下标是否超出当前窗口左边界)
while (q[front] <= i - k) {
front++;
}
// 3. 窗口形成后输出结果(当i>=k-1时,窗口才完整形成)
if (i >= k - 1) {
printf("%d ", a[q[front]]);
}
}
}
int main() {
int n, k, i;
scanf("%d%d", &n, &k);
for (i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
slidingWindowMax(n, k);
return 0;
}
第三章 哈希表
两个文件实现了同一 ADT(int→int 映射)的两种冲突解决策略。两者的
isPrime/nextPrime/LOAD_FACTOR 0.75/ 初始容量 31 / 协议式 main 完全一致,是绝佳的对照材料。
3.1 拉链法哈希表(hash/hash_chaining.c)
核心数据结构 :Node 是链表结点(key-value-next),HashMap 内含 table(桶数组)、size(已存元素个数,不是桶数 )、capacity(桶数)。
| 函数 | 作用 | 复杂度 |
|---|---|---|
isPrime / nextPrime |
试除法判质数 / 向上找质数(跳过偶数,用 (long long)i*i 防溢出) |
O(√n) |
hash |
(key % cap + cap) % cap ------ 同余原理处理负数 key |
O(1) |
resize |
扩容到 nextPrime(capacity*2),复用原结点头插重挂 |
均摊 O(1) |
put |
size >= capacity*0.75 先扩容;已存在则只覆盖 value |
均摊 O(1) |
get / containsKey |
遍历对应链 | 均摊 O(1) |
containsValue |
必须扫描整张表所有桶与链 | O(n) |
removeKey |
二级指针 Node **pp 删除,同时覆盖删头结点与删中间结点 |
均摊 O(1) |
实现要点:
- 扩容容量取
capacity*2再向上取质数,模质数优于模合数(减少公共因子造成的聚集) - rehash 复用原结点 + 头插,不 malloc,无内存泄漏也无额外拷贝开销
removeKey的二级指针技巧是全文件最值得记的 C 惯用法- 接口歧义:
get/remove用 -1 表示不存在,与"值本身就是 -1"的合法数据冲突 - 装载因子 α = n/m 决定性能:α 过大链变长,需动态扩容 + rehash
时间复杂度: 理想情况下 put/get/remove/containsKey 均摊 O(1),最坏 O(n)(全冲突退化成链)
c
// hash_chaining.c ------ 哈希表:拉链法(链地址法)
// 风格对齐 Java 版 HashMap:类封装、size/capacity/LOAD_FACTOR、质数扩容
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LOAD_FACTOR 0.75
/* ---------- 链表节点 ---------- */
typedef struct Node {
int key;
int value;
struct Node *next;
} Node;
/* ---------- HashMap 结构 ---------- */
typedef struct {
Node **table; // 桶数组,每个桶是一条链表
int size; // 存储的元素个数
int capacity; // 总容量(桶的数量)
} HashMap;
/* ---------- 判断是否为质数 ---------- */
static int isPrime(int n) {
if (n < 2) return 0;
if (n % 2 == 0) return n == 2;
for (int i = 3; (long long)i * i <= n; i += 2) {
if (n % i == 0) return 0;
}
return 1;
}
/* ---------- 下一个质数(用于扩容) ---------- */
static int nextPrime(int n) {
if (n <= 2) return 2;
if (n % 2 == 0) n++;
while (!isPrime(n)) n += 2;
return n;
}
/* ---------- 初始化 ---------- */
void hashMapInit(HashMap *m) {
m->capacity = 31; // 初始容量为 31,一个质数
m->table = (Node **)calloc(m->capacity, sizeof(Node *));
m->size = 0;
}
/* ---------- 释放整张表 ---------- */
void hashMapFree(HashMap *m) {
for (int i = 0; i < m->capacity; i++) {
Node *p = m->table[i];
while (p) {
Node *nxt = p->next;
free(p);
p = nxt;
}
}
free(m->table);
m->table = NULL;
m->size = m->capacity = 0;
}
/* ---------- 哈希函数:同余原理,处理负数 key ---------- */
static int hash(HashMap *m, int key) {
return (key % m->capacity + m->capacity) % m->capacity;
}
/* ---------- 扩容 ---------- */
static void resize(HashMap *m, int newCapacity) {
int newCap = nextPrime(newCapacity); // 扩容为一个质数
// 保存旧表
int oldCap = m->capacity;
Node **oldTable = m->table;
// 创建新表
m->capacity = newCap;
m->table = (Node **)calloc(m->capacity, sizeof(Node *));
m->size = 0;
// 把旧表所有有效节点重新插入
for (int i = 0; i < oldCap; i++) {
Node *p = oldTable[i];
while (p) {
Node *nxt = p->next;
int index = hash(m, p->key);
// 头插法
p->next = m->table[index];
m->table[index] = p;
m->size++;
p = nxt;
}
}
free(oldTable);
}
/* ---------- 插入元素 ---------- */
void put(HashMap *m, int key, int value) {
if (m->size >= m->capacity * LOAD_FACTOR) { // 容量已满,则扩容
resize(m, m->capacity * 2);
}
int index = hash(m, key);
Node *p = m->table[index];
// 如果 key 已存在,覆盖 value
while (p) {
if (p->key == key) {
p->value = value;
return;
}
p = p->next;
}
// 头插新节点
Node *node = (Node *)malloc(sizeof(Node));
node->key = key;
node->value = value;
node->next = m->table[index];
m->table[index] = node;
m->size++;
}
/* ---------- 查询元素 ---------- */
int get(HashMap *m, int key) {
int index = hash(m, key);
Node *p = m->table[index];
while (p) {
if (p->key == key) return p->value;
p = p->next;
}
return -1; // 不存在返回 -1
}
/* ---------- 是否包含某个键 ---------- */
int containsKey(HashMap *m, int key) {
int index = hash(m, key);
Node *p = m->table[index];
while (p) {
if (p->key == key) return 1;
p = p->next;
}
return 0;
}
/* ---------- 是否包含某个 value ---------- */
int containsValue(HashMap *m, int value) {
for (int i = 0; i < m->capacity; i++) {
Node *p = m->table[i];
while (p) {
if (p->value == value) return 1;
p = p->next;
}
}
return 0;
}
/* ---------- 删除元素 ---------- */
int removeKey(HashMap *m, int key) {
int index = hash(m, key);
Node **pp = &m->table[index];
while (*pp) {
if ((*pp)->key == key) {
Node *del = *pp;
int res = del->value;
*pp = del->next;
free(del);
m->size--;
return res;
}
pp = &(*pp)->next;
}
return -1; // 不存在返回 -1
}
/* ---------- 打印哈希表 ---------- */
void print(HashMap *m) {
printf("{");
int first = 1;
for (int i = 0; i < m->capacity; i++) {
Node *p = m->table[i];
while (p) {
if (!first) printf(", ");
printf("%d=%d", p->key, p->value);
first = 0;
p = p->next;
}
}
printf("}\n");
}
/* ---------- 读取一行(替代 Java 的 BufferedReader.readLine) ---------- */
static int readLine(char *buf, int size) {
if (!fgets(buf, size, stdin)) return 0;
return 1;
}
/* ---------- 主函数测试 ---------- */
int main(void) {
char line[256];
if (!readLine(line, sizeof(line))) return 0;
int n = atoi(line);
HashMap hm;
hashMapInit(&hm);
for (int i = 0; i < n; i++) {
if (!readLine(line, sizeof(line))) break;
char op[32];
sscanf(line, "%31s", op);
// 跳过第一个 token,指向后面的参数
char *p = line;
while (*p && *p != ' ' && *p != '\t') p++;
while (*p == ' ' || *p == '\t') p++;
if (strcmp(op, "put") == 0) {
int k, v;
sscanf(p, "%d%d", &k, &v);
put(&hm, k, v);
} else if (strcmp(op, "get") == 0) {
int k;
sscanf(p, "%d", &k);
printf("%d\n", get(&hm, k));
} else if (strcmp(op, "containsKey") == 0) {
int k;
sscanf(p, "%d", &k);
printf("%d\n", containsKey(&hm, k));
} else if (strcmp(op, "containsValue") == 0) {
int v;
sscanf(p, "%d", &v);
printf("%d\n", containsValue(&hm, v));
} else if (strcmp(op, "remove") == 0) {
int k;
sscanf(p, "%d", &k);
printf("%d\n", removeKey(&hm, k));
} else if (strcmp(op, "size") == 0) {
printf("%d\n", hm.size);
} else if (strcmp(op, "print") == 0) {
print(&hm);
}
}
hashMapFree(&hm);
return 0;
}
3.2 开放寻址法哈希表(hash/hash_open_addressing.c)
核心数据结构 :三个平行数组 + 一个状态数组。int *hash 是状态数组:0 = 空(从未使用)、1 = 占用、2 = 已删除(墓碑 tombstone) ;int *keys / *values 分别存键与值。
★ 本文件最关键的理论点:为什么删除必须用墓碑而不能直接置 0?
- 线性探测的查找会在遇到"空位 0"时提前终止 。若把中间元素直接置 0,会切断后续同簇元素的探测链,导致它们永远查不到
- 墓碑(状态 2)告诉查找"这里曾有人,继续往后找",从而保住探测链的完整性
- 墓碑的副作用:删除会留下"假占用",使探测路径变长。但只要
size增加就会触发扩容,rehash 时天然清掉所有墓碑
核心探测逻辑:
c
static int find(HashMap *m, int key) {
int index = (key % m->capacity + m->capacity) % m->capacity; // 同余原理,处理负数
int count = 0; // 记录探测次数,防止哈希表满时死循环
while (m->hash[index] != 0 && m->keys[index] != key && count < m->capacity) {
index = (index + 1) % m->capacity; // 线性探测到下一个位置
count++;
}
return index;
}
实现要点:
find既是查找函数也是插入定位函数 ------ 与二叉搜索树中search_bst复用为插入定位是同一设计思想- 返回下标的三种语义:命中(状态 1 且 key 相等)、空位(0)、墓碑(2)。调用方必须检查状态才能判定是否命中
count < capacity是防死循环的保险resize只迁移状态为 1 的元素,墓碑被自然丢弃 ------ rehash 顺带清理墓碑的好处- 三种探测方式:线性探测(易一次聚集)、二次探测(易二次聚集)、双重散列(最均匀)
时间复杂度: 理想情况均摊 O(1),最坏 O(n)。线性探测的平均探测次数约 ½(1 + 1/(1-α))
空间优势: 只有三个数组、无指针,缓存友好 ------ 这是开放寻址相对拉链法的主要优势
c
// hash_open_addressing.c ------ 开放寻址法(线性探测 + 墓碑删除 + 质数扩容)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LOAD_FACTOR 0.75
typedef struct {
int *hash; // 0-空无数据,1-占用,2-已删除状态
int *keys; // 存键值
int *values; // 存 value 值
int size; // 存储的元素个数
int capacity; // 总容量
} HashMap;
/* ---------- 判断是否为质数 ---------- */
static int isPrime(int n) {
if (n < 2) return 0;
if (n % 2 == 0) return n == 2;
for (int i = 3; (long long)i * i <= n; i += 2) {
if (n % i == 0) return 0;
}
return 1;
}
/* ---------- 下一个质数(用于扩容) ---------- */
static int nextPrime(int n) {
if (n <= 2) return 2;
if (n % 2 == 0) n++;
while (!isPrime(n)) n += 2;
return n;
}
/* ---------- 初始化 ---------- */
void hashMapInit(HashMap *m) {
m->capacity = 31; // 初始容量为 31,一个质数
m->hash = (int *)calloc(m->capacity, sizeof(int));
m->keys = (int *)calloc(m->capacity, sizeof(int));
m->values = (int *)calloc(m->capacity, sizeof(int));
m->size = 0;
}
/* ---------- 释放 ---------- */
void hashMapFree(HashMap *m) {
free(m->hash);
free(m->keys);
free(m->values);
m->hash = m->keys = m->values = NULL;
m->size = m->capacity = 0;
}
/* ---------- 查找元素位置下标(核心探测逻辑) ---------- */
static int find(HashMap *m, int key) {
int index = (key % m->capacity + m->capacity) % m->capacity; // 同余原理,处理负数
int count = 0; // 记录探测次数,防止哈希表满时死循环
while (m->hash[index] != 0 && m->keys[index] != key && count < m->capacity) {
index = (index + 1) % m->capacity; // 线性探测到下一个位置
count++;
}
return index;
}
/* ---------- 扩容 ---------- */
static void resize(HashMap *m, int newCapacity) {
int newCap = nextPrime(newCapacity); // 扩容为一个质数
// 保存旧的表
int oldCap = m->capacity;
int *oldHash = m->hash;
int *oldKeys = m->keys;
int *oldVals = m->values;
// 创建新表
m->capacity = newCap;
m->hash = (int *)calloc(m->capacity, sizeof(int));
m->keys = (int *)calloc(m->capacity, sizeof(int));
m->values = (int *)calloc(m->capacity, sizeof(int));
m->size = 0;
// 把旧表有效元素重新插入
for (int i = 0; i < oldCap; i++) {
if (oldHash[i] == 1) {
// 这里直接调用 put 逻辑,但 put 可能再次触发 resize,所以手动实现
int key = oldKeys[i];
int val = oldVals[i];
int index = find(m, key);
if (m->hash[index] == 0 || m->hash[index] == 2) {
m->hash[index] = 1;
m->keys[index] = key;
m->values[index] = val;
m->size++;
} else {
m->values[index] = val;
}
}
}
free(oldHash);
free(oldKeys);
free(oldVals);
}
/* ---------- 插入元素 ---------- */
void put(HashMap *m, int key, int value) {
if (m->size >= m->capacity * LOAD_FACTOR) { // 容量已满,则扩容
resize(m, m->capacity * 2);
}
int index = find(m, key);
if (m->hash[index] == 0 || m->hash[index] == 2) { // 空位或已删除位
m->hash[index] = 1;
m->keys[index] = key;
m->values[index] = value;
m->size++;
} else { // 已存在,覆盖
m->values[index] = value;
}
}
/* ---------- 查询元素 ---------- */
int get(HashMap *m, int key) {
int index = find(m, key);
if (m->hash[index] == 1) return m->values[index];
return -1;
}
/* ---------- 是否包含某个键 ---------- */
int containsKey(HashMap *m, int key) {
int index = find(m, key);
return m->hash[index] == 1;
}
/* ---------- 是否包含某个 value ---------- */
int containsValue(HashMap *m, int value) {
for (int i = 0; i < m->capacity; i++) {
if (m->hash[i] == 1 && m->values[i] == value) return 1;
}
return 0;
}
/* ---------- 删除元素 ---------- */
int removeKey(HashMap *m, int key) {
int index = find(m, key);
if (m->hash[index] != 1) return -1;
int res = m->values[index];
m->hash[index] = 2; // 标记为已删除
m->size--;
return res;
}
/* ---------- 打印哈希表 ---------- */
void print(HashMap *m) {
printf("{");
int first = 1;
for (int i = 0; i < m->capacity; i++) {
if (m->hash[i] == 1) {
if (!first) printf(", ");
printf("%d=%d", m->keys[i], m->values[i]);
first = 0;
}
}
printf("}\n");
}
/* ---------- 读取一行(替代 Java 的 BufferedReader.readLine) ---------- */
static int readLine(char *buf, int size) {
if (!fgets(buf, size, stdin)) return 0;
return 1;
}
/* ---------- 主函数测试 ---------- */
int main(void) {
char line[256];
if (!readLine(line, sizeof(line))) return 0;
int n = atoi(line);
HashMap hm;
hashMapInit(&hm);
for (int i = 0; i < n; i++) {
if (!readLine(line, sizeof(line))) break;
char op[32];
// 解析第一个 token
int pos = 0;
sscanf(line, "%31s", op);
// 找第一个空格后面
char *p = line;
while (*p && *p != ' ' && *p != '\t') p++;
while (*p == ' ' || *p == '\t') p++;
if (strcmp(op, "put") == 0) {
int k, v;
sscanf(p, "%d%d", &k, &v);
put(&hm, k, v);
} else if (strcmp(op, "get") == 0) {
int k;
sscanf(p, "%d", &k);
printf("%d\n", get(&hm, k));
} else if (strcmp(op, "containsKey") == 0) {
int k;
sscanf(p, "%d", &k);
printf("%d\n", containsKey(&hm, k));
} else if (strcmp(op, "containsValue") == 0) {
int v;
sscanf(p, "%d", &v);
printf("%d\n", containsValue(&hm, v));
} else if (strcmp(op, "remove") == 0) {
int k;
sscanf(p, "%d", &k);
printf("%d\n", removeKey(&hm, k));
} else if (strcmp(op, "size") == 0) {
printf("%d\n", hm.size);
} else if (strcmp(op, "print") == 0) {
print(&hm);
}
(void)pos;
}
hashMapFree(&hm);
return 0;
}
3.3 两种哈希策略对照
| 维度 | 拉链法(hash_chaining) | 开放寻址法(hash_open_addressing) |
|---|---|---|
| 冲突解决 | 桶 + 链表(链地址法) | 线性探测 + 环形回绕 |
| 删除方式 | 直接摘链 + free | 置墓碑(状态 2),不能直接置 0 |
| 存储布局 | 桶数组 + 链表结点(指针跳转) | 三个平行数组(连续内存,缓存友好) |
| 装载因子容忍度 | 较高,α 可接近 1 | 较低,α=0.75 时探测已明显变长 |
| 空间开销 | 每元素一个结点 + 指针 | 无指针,但需状态数组 |
| 最大元素数 | 仅受内存限制 | 受 capacity 限制,满表需扩容 |
第四章 堆与优先队列
4.1 大根堆(heap/priority_queue.c)
核心数据结构 :用数组隐式表示的完全二叉树。PriorityQueue 含 heap(动态数组)、size(有效长度)、capacity。下标关系:父 (index - 1) / 2、左孩子 index * 2 + 1、右孩子 index * 2 + 2(0-based)。
上浮与下沉的分工:
| 操作 | 触发场景 | 恢复方向 | 过程 |
|---|---|---|---|
insert |
在数组末尾追加新元素 | 向上(sift up) | parent = (index - 1) / 2,父 < 子就交换并继续上浮,否则 break |
poll |
把末尾元素搬到堆顶后删除堆顶 | 向下(sift down) | 先在两个孩子里挑较大者,若自己最大则 break,否则交换后继续下沉 |
★ 本文件最值得记录的实现细节:异或交换的陷阱
swap用异或实现(a^=b; b^=a; a^=b;)省了一个临时变量,但自交换会清零- 代码用
if (index == parent) return;特判规避 ------ 这是面试与实战中的经典坑
其他实现要点:
- 下沉时选"较大的孩子"再比较是大根堆的正确写法,注意两次都要判
下标 < size(右孩子可能不存在) poll用exit(1)处理空堆,简单粗暴但不可恢复;peek完全没有空检查insert容量满时静默丢弃 ------ 不扩堆、不报错、不返回状态码,作为库函数是语义缺陷- 建堆方式是 n 次
insert,即 O(n log n) 的逐个插入建堆,而不是 O(n) 的 Floyd 自底向上下沉建堆 main用char line[1 << 20](1 MB 行缓冲)整行读入,再用手写扫描器配合strtol双指针解析- 反复取堆顶即得到降序序列 ------ 这就是堆排序思想
时间复杂度: insert O(log n);poll O(log n);peek/isEmpty/pqSize O(1);连续 poll 全部元素 = 堆排序 O(n log n)
c
// priority_queue.c ------ 大根堆(优先队列)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int *heap; // 堆数组
int size; // 堆的有效长度
int capacity;// 堆的容量
} PriorityQueue;
/* ---------- 异或交换 ---------- */
void swap(int *heap, int index, int parent) {
if (index == parent) return; // 异或交换自身会变 0,必须特判
heap[index] = heap[index] ^ heap[parent];
heap[parent] = heap[index] ^ heap[parent];
heap[index] = heap[index] ^ heap[parent];
}
/* ---------- 初始化 ---------- */
void pqInit(PriorityQueue *pq, int capacity) {
pq->heap = (int *)malloc(sizeof(int) * capacity);
pq->size = 0;
pq->capacity = capacity;
}
/* ---------- 释放 ---------- */
void pqFree(PriorityQueue *pq) {
free(pq->heap);
pq->heap = NULL;
pq->size = pq->capacity = 0;
}
/* ---------- 插入 → 向上调整 ---------- */
void insert(PriorityQueue *pq, int value) {
if (pq->size >= pq->capacity) { // 容量已满,直接返回
return;
}
pq->heap[pq->size] = value; // 新元素放末尾
pq->size++;
int index = pq->size - 1;
while (index > 0) {
int parent = (index - 1) / 2;
// 大根堆:父 < 子 就交换(小根堆改成 >)
if (pq->heap[parent] < pq->heap[index]) {
swap(pq->heap, index, parent);
index = parent;
} else {
break;
}
}
}
/* ---------- 弹出堆顶 → 向下调整 ---------- */
int poll(PriorityQueue *pq) {
if (pq->size == 0) {
fprintf(stderr, "队列为空!\n");
exit(1);
}
int result = pq->heap[0]; // 堆顶是最大值
pq->heap[0] = pq->heap[pq->size - 1];// 最后一个元素放到堆顶
pq->size--;
int index = 0;
while (1) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int maxIdx = index;
// 大根堆:找最大的孩子(小根堆改成 <)
if (left < pq->size && pq->heap[left] > pq->heap[maxIdx]) maxIdx = left;
if (right < pq->size && pq->heap[right] > pq->heap[maxIdx]) maxIdx = right;
if (maxIdx == index) break; // 自己最大,结束
swap(pq->heap, index, maxIdx);
index = maxIdx;
}
return result;
}
/* ---------- 判断是否为空 ---------- */
int isEmpty(PriorityQueue *pq) {
return pq->size == 0;
}
/* ---------- 返回堆顶元素,不删除 ---------- */
int peek(PriorityQueue *pq) {
return pq->heap[0];
}
/* ---------- 返回堆中元素个数 ---------- */
int pqSize(PriorityQueue *pq) {
return pq->size;
}
/* ---------- 打印 ---------- */
void print(PriorityQueue *pq) {
for (int i = 0; i < pq->size; i++) {
printf("%d ", pq->heap[i]);
}
printf("\n");
}
/* ---------- 主函数 ---------- */
int main(void) {
char line[1 << 20]; // 用于读一整行(n 个数)
if (!fgets(line, sizeof(line), stdin)) return 0;
int n = atoi(line);
if (n <= 0) return 0;
PriorityQueue pq;
pqInit(&pq, n);
// 第二行读入 n 个数
if (!fgets(line, sizeof(line), stdin)) {
pqFree(&pq);
return 0;
}
char *p = line;
for (int i = 0; i < n; i++) {
while (*p == ' ' || *p == '\t') p++;
if (!*p) break;
int x = (int)strtol(p, &p, 10);
insert(&pq, x);
}
// 不断弹出堆顶,输出序列
while (!isEmpty(&pq)) {
printf("%d ", poll(&pq));
}
printf("\n");
pqFree(&pq);
return 0;
}
第五章 树结构
5.1 二叉树(tree/binary_tree.c)
核心数据结构 :二叉链表 ------ treeNode 含 data、lchild、rchild,再用 typedef treeNode* bitree 把"节点指针"提升为树类型名。全局 str[] 是扩展先序序列(# 为空指针占位符),idx 是全局读取游标。
实现要点:
- 建树的递归返回值靠二级指针 传出:
createtree(bitree* T),C 里想修改指针本身必须传T** - 扩展先序序列(带空标记)唯一确定一棵二叉树 ------ 这是建树的理论基础
- 三种遍历结构完全同构,仅
printf位置不同:先序(根-左-右)、中序(左-根-右)、后序(左-右-根) - 层序遍历用数组模拟队列,先根入队,循环条件
front < rear,出队后判空再把左右孩子入队 idx是全局变量,main里必须idx = 0复位,否则重复调用建树会越界读str
⚠️ 代码中的真实瑕疵
LevelOrder函数使用的是BiTree类型名,但本文件只定义了bitree(小写 b)。这是命名不一致导致的编译错误,该函数在当前状态下无法通过编译。
时间复杂度: 建树 O(n);三种深度优先遍历 O(n);层序遍历 O(n)。空间:递归栈 O(h),最坏单链树 O(n)。
c
#include<stdio.h>//二叉树
#include<stdlib.h>
#include<windows.h> // 关键:解决system("pause")编译报错,必须加!
typedef char elemtype;
typedef struct treeNode
{
elemtype data;
struct treeNode* lchild;
struct treeNode* rchild;
}treeNode;
typedef treeNode* bitree;
char str[] = "ABDH#K#E##CFI###G#J##";
int idx = 0;
void createtree(bitree* T)
{
elemtype ch;
ch = str[idx++];
if (ch == '#' || ch=='\0')
{
*T = NULL;
}
else
{
*T = (struct treeNode*)malloc(sizeof(treeNode));
(*T)->data = ch;
createtree(&((*T)->lchild));
createtree(&((*T)->rchild));
}
}
void preorder(bitree T)//前序遍历
{
if (T == NULL)
{
return;
}
printf("%c ", T->data);
preorder(T->lchild);
preorder(T->rchild);
}
void inorder(bitree T)//中序遍历
{
if (T == NULL)
{
return;
}
inorder(T->lchild);
printf("%c ", T->data);
inorder(T->rchild);
}
void postorder(bitree T)//后序遍历
{
if (T == NULL)
{
return;
}
postorder(T->lchild);
postorder(T->rchild);
printf("%c ", T->data);
}
void LevelOrder(BiTree T)
{
if (T == NULL) return;
BiTree q[1005];
int front = 0, rear = 0;
q[rear++] = T;
while (front < rear)
{
BiTree p = q[front++];
// 直接输出,不计数、不判断
printf("%c ", p->data);
if (p->lchild) q[rear++] = p->lchild;
if (p->rchild) q[rear++] = p->rchild;
}
}
int main()
{
bitree T;
idx = 0;
createtree(&T);
preorder(T);
printf("\n");
inorder(T);
printf("\n");
postorder(T);
printf("\n");
system("pause"); // 防闪退,现在不会报错了
return 0;
}
5.2 二叉搜索树(tree/bst.c)
核心数据结构 :与二叉树相同的二叉链表,但数据为 int,用 -1 代替 # 作空结点标记。树中每结点满足 BST 性质:左子树全部 < 根 < 右子树全部。
数据观察: 全局数组 arr[] 构造出的是有序 BST 形态,其中序序列为 30, 39, 49, 53, 55, 70, 75, 80, 95, 98------恰好是递增的,验证了"BST 中序遍历得递增序列"这一定理。
实现要点:
- 查找复用为插入定位是本文件精髓:一个递归同时完成"判重"和"找出父结点",省去第二趟比较
search_bst未命中时*pos返回的是"查找失败时最后访问的结点",即插入位置的父亲insert_bst里传的是*T而不是T(源码注释明确写"修正:传 *T 而非 T"),这是曾经的类型错配 bug 修复痕迹- 插入只用叶子插入法:新结点必为叶子,不改变已有结点位置,因此不破坏 BST 性质
- 性能取决于树高:有序插入会退化为 h = n,操作变成 O(n)------这正是 AVL / 红黑树 / Treap 等平衡树存在的动机
时间复杂度: 查找/插入均 O(h);理想情况 h ≈ log n,退化情况 h = n
c
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
typedef int elemtype;
typedef struct treeNode
{
elemtype data;
struct treeNode* lchild;
struct treeNode* rchild;
}treeNode;
typedef treeNode* bitree;
// 用于创建二叉树的数组(-1表示空节点)
int arr[] = { 70,55,49,30,-1,39,-1,-1,53,-1,-1,-1,80,75,-1,-1,98,95,-1,-1,-1 };
int idx = 0;
// 恢复被注释的创建二叉树函数
void createtree(bitree* T)
{
elemtype ch;
ch = arr[idx++];
if (ch == -1)
{
*T = NULL;
}
else
{
*T = (treeNode*)malloc(sizeof(treeNode));
(*T)->data = ch;
(*T)->lchild = NULL; // 初始化左右孩子,避免野指针
(*T)->rchild = NULL;
createtree(&(*T)->lchild);
createtree(&(*T)->rchild);
}
}
// 二叉搜索树查找函数
int search_bst(bitree T, int value, bitree parent, bitree* pos) {
if (T == NULL) {
*pos = parent;
return 0;
}
if (T->data == value) {
*pos = T;
return 1;
}
else if (T->data > value) {
return search_bst(T->lchild, value, T, pos);
}
else {
return search_bst(T->rchild, value, T, pos);
}
}
// 二叉搜索树插入函数(修正核心错误)
int insert_bst(bitree* T, int value) {
bitree parent = NULL, pos; // 初始化parent为NULL,避免野指针
bitree curr;
int status = search_bst(*T, value, parent, &pos); // 修正:传*T而非T
if (status == 0) {
curr = (bitree)malloc(sizeof(treeNode));
curr->data = value;
curr->lchild = NULL;
curr->rchild = NULL;
if (pos == NULL) { // 树为空时,新节点作为根节点
*T = curr;
}
else if (value < pos->data) { // 小于父节点,插左孩子
pos->lchild = curr;
}
else { // 大于父节点,插右孩子
pos->rchild = curr;
}
return 1;
}
else {
return 0;
}
}
// 前序遍历
void preorder(bitree T)
{
if (T == NULL)
{
return;
}
printf("%d ", T->data);
preorder(T->lchild);
preorder(T->rchild);
}
int main()
{
bitree T = NULL; // 初始化根节点,避免野指针
idx = 0;
createtree(&T); // 创建二叉树
bitree searchT = NULL;
// 查找值为53的节点,parent初始为NULL
int searchResult = search_bst(T, 53, NULL, &searchT);
printf("二叉树前序遍历结果:");
preorder(T);
printf("\n");
// 增加判空逻辑,避免空指针访问
if (searchResult && searchT != NULL) {
printf("找到的节点值:%d\n", searchT->data);
} else {
printf("未找到值为53的节点\n");
}
system("pause"); // 暂停查看结果
return 0;
}
5.3 树状数组(tree/fenwicktree.c)
核心数据结构 :一维数组 ll Tree[MAXN] 隐式表示树,不使用任何结构体、不显式存孩子,树形关系完全由下标的二进制编码决定。
核心三函数:
c
static int lowbit(int x) {
return x & (-x); // 取最低位 1 所代表的幂
}
static void add(int x, ll v) {
while (x <= N) { Tree[x] += v; x += lowbit(x); } // 向上跳父结点
}
static ll query(int x) {
ll res = 0;
while (x > 0) { res += Tree[x]; x -= lowbit(x); } // 向下剥离最低位
return res;
}
实现要点:
- 1-based 下标是硬约束 :
query终止条件是x > 0,若用 0-based 会死循环(lowbit(0) == 0) - 两个循环方向相反是核心对称性:update 加 lowbit 向上、query 减 lowbit 向下,各恰好 O(log n) 次
Tree[x]的语义:管理区间(x - lowbit(x), x]的和 。例如Tree[6]管[5,6],Tree[8]管[1,8]- 区间和 =
query(r) - query(l-1),这是树状数组相比线段树最大的便利 - 不支持区间加 / 区间最值(需差分 BIT 或线段树)
- 空间 O(n) 对线段树的 O(4n),常数更小
时间复杂度: add O(log n);query O(log n);空间 O(n)
c
//
// Created by Administrator on 2026/10/6.
//
//和前面的思路也一样
#include <stdio.h>
typedef long long ll;
#define MAXN 100005
ll Tree[MAXN];
int N;
static int lowbit(int x) {
return x & (-x);
}
static void add(int x, ll v) {
while (x <= N) {
Tree[x] += v;
x += lowbit(x);
}
}
static ll query(int x) {
ll res = 0;
while (x > 0) {
res += Tree[x];
x -= lowbit(x);
}
return res;
}
int main() {
int n, m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
ll num;
scanf("%lld", &num);
add(i, num);
}
while (m--) {
int op;
scanf("%d", &op);
if (op == 1) {
int i;
ll x;
scanf("%d %lld", &i, &x);
add(i, x);
} else {
int l, r;
scanf("%d %d", &l, &r);
printf("%lld\n", query(r) - query(l - 1));
}
}
return 0;
}
5.4 线段树(单点更新,tree/segment_tree_point.c)
核心数据结构 :数组式(堆式)线段树,int tree[MAXN << 2] 即 4n 大小。用完全二叉树编号法:结点 node 的左孩子 node<<1、右孩子 (node<<1)|1,根固定为 1。每结点存一个区间和。
区间查询三步式:
- 完全覆盖 → 直接返回
tree[node] - 无交集 → 返回幺元(加法单位元 0)
- 部分覆盖 → 下传递归左右孩子
实现要点:
- 四参数递归签名
(node, start, end, 目标区间)是线段树通用模板,mid = (start+end) >> 1 - 无交集剪枝靠"返回值 0"实现,而非先判断再进入 ------ 换成区间 max 就要返回
-INF - 单点更新路径唯一 :
if (index <= mid)走左else走右,只递归一侧,天然 O(log n) - 建树用 n 次单点
update(O(n log n)),而不是经典的build递归 O(n) 4n是必要上界(编号最坏可达 4n-1),少开会越界写- 元素须满足结合律(不必可逆),这点强于树状数组的前缀差
时间复杂度: 单点更新 O(log n);区间查询 O(log n);空间 O(4n)
c
// segment_tree_point.c ------ 单点更新 + 区间查询(对应你原来的 C++ 版)
#include <stdio.h>
#include <string.h>
#define MAXN 100005
int tree[MAXN << 2]; // 4 * n 空间
int n;
void update(int node, int start, int end, int index, int value) {
if (start == end) {
tree[node] = value;
} else {
int mid = (start + end) >> 1;
if (index <= mid) {
update(node << 1, start, mid, index, value);
} else {
update((node << 1) | 1, mid + 1, end, index, value);
}
tree[node] = tree[node << 1] + tree[(node << 1) | 1];
}
}
int query(int node, int start, int end, int left, int right) {
if (left > end || right < start) {
return 0;
}
if (left <= start && right >= end) {
return tree[node];
}
int mid = (start + end) >> 1;
return query(node << 1, start, mid, left, right)
+ query((node << 1) | 1, mid + 1, end, left, right);
}
int main(void) {
int q;
while (scanf("%d%d", &n, &q) != EOF) {
memset(tree, 0, sizeof(int) * (4 * n + 4));
// 建树:读 n 个数依次单点赋值
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
update(1, 1, n, i, x);
}
while (q--) {
int op, a, b;
scanf("%d%d%d", &op, &a, &b);
if (op == 1) {
update(1, 1, n, a, b); // 单点赋值 a = b
} else {
printf("%d\n", query(1, 1, n, a, b));
}
}
}
return 0;
}
5.5 懒标记线段树(区间加 + 区间求和,tree/segment_tree_lazy.c)
核心数据结构 :线段树 + 懒标记 。两个并列数组:tree[MAXN<<2](区间和)与 lazy[MAXN<<2](该区间尚未下传给孩子的"每元素增量")。注意 lazy 的语义是单位增量,不是区间总增量。
★ pushDown 三要素:
- 更新孩子的
tree(乘以区间长度) - 叠加孩子的
lazy(用+=而非=) - 清空自己的
lazy
★ pushDown 调用时机只有两处: update 和 query 在访问孩子之前。这是懒标记最容易漏的地方------查询时若不下推,会读到孩子尚未更新的旧值。
c
void pushDown(int node, int start, int end) {
if (lazy[node] == 0) return; // 零标记优化
int mid = (start + end) >> 1;
int lc = node << 1;
int rc = (node << 1) | 1;
tree[lc] += lazy[node] * (mid - start + 1); // 左孩子长度 = mid-start+1
lazy[lc] += lazy[node];
tree[rc] += lazy[node] * (end - mid); // 右孩子长度 = end-mid
lazy[rc] += lazy[node];
lazy[node] = 0; // 清空父节点懒标记
}
易错核心: 区间长度因子必须写对------左长 mid-start+1、右长 end-mid(不是 mid-start)。写错会让求和结果整体偏小或偏大。
时间复杂度: 区间加 O(log n)(amortized:完全覆盖的结点 O(log n) 个,被"拦住"不再下探);空间 O(4n)
c
//
// Created by Administrator on 2026/10/6.
//
// segment_tree_lazy.c ------ 区间加 + 区间求和(带懒标记)
#include <stdio.h>
#include <string.h>
#define MAXN 100005
#define ll long long
ll tree[MAXN << 2]; // 区间和
ll lazy[MAXN << 2]; // 懒标记:该区间每个元素累加的值
int n;
/* 下推懒标记:把父节点的 lazy 传给两个子节点 */
void pushDown(int node, int start, int end) {
if (lazy[node] == 0) return;
int mid = (start + end) >> 1;
int lc = node << 1;
int rc = (node << 1) | 1;
// 左子节点
tree[lc] += lazy[node] * (mid - start + 1);
lazy[lc] += lazy[node];
// 右子节点
tree[rc] += lazy[node] * (end - mid);
lazy[rc] += lazy[node];
// 清空父节点懒标记
lazy[node] = 0;
}
/* 区间加:把 [left, right] 中每个元素 + value */
void update(int node, int start, int end, int left, int right, ll value) {
if (left <= start && end <= right) {
// 当前区间被完全覆盖,直接打懒标记
tree[node] += value * (end - start + 1);
lazy[node] += value;
return;
}
pushDown(node, start, end); // 访问子区间前先下推
int mid = (start + end) >> 1;
if (left <= mid) update(node << 1, start, mid, left, right, value);
if (right > mid) update((node << 1) | 1, mid + 1, end, left, right, value);
tree[node] = tree[node << 1] + tree[(node << 1) | 1];
}
/* 区间查询:返回 [left, right] 的和 */
ll query(int node, int start, int end, int left, int right) {
if (left <= start && end <= right) {
return tree[node];
}
pushDown(node, start, end); // 访问子区间前先下推
int mid = (start + end) >> 1;
ll ans = 0;
if (left <= mid) ans += query(node << 1, start, mid, left, right);
if (right > mid) ans += query((node << 1) | 1, mid + 1, end, left, right);
return ans;
}
int main(void) {
int q;
while (scanf("%d%d", &n, &q) != EOF) {
memset(tree, 0, sizeof(ll) * (4 * n + 4));
memset(lazy, 0, sizeof(ll) * (4 * n + 4));
// 建树:读 n 个数依次单点加
for (int i = 1; i <= n; i++) {
ll x;
scanf("%lld", &x);
update(1, 1, n, i, i, x);
}
while (q--) {
int op, l, r;
scanf("%d%d%d", &op, &l, &r);
if (op == 1) {
ll v;
scanf("%lld", &v);
update(1, 1, n, l, r, v); // 区间 [l,r] 每个数 +v
} else {
printf("%lld\n", query(1, 1, n, l, r));
}
}
}
return 0;
}
5.6 三种"区间"结构对照
| 维度 | 树状数组 | 线段树(单点) | 线段树(懒标记) |
|---|---|---|---|
| 支持操作 | 点更新 + 前缀和 | 点更新 + 任意区间查询 | 区间更新 + 任意区间查询 |
| 区间修改 | 不支持 | 不支持(需 O(n)) | 支持,O(log n) |
| 附加空间 | 无 | 无 | lazy 数组(4n) |
| 空间 | O(n) | O(4n) | O(4n) |
| 常数 | 极小 | 中等 | 较大 |
| 元素要求 | 可差分(如加法) | 可合并 + 结合律 | 可合并 + 结合律 + 标记可叠加 |
| 适用场景 | 单点改 + 区间和 | 点的修改 + 区间询问 | 区间加减 + 区间询问 |
第六章 图论算法
涵盖图的两种存储(邻接矩阵 / 链式前向星)、两种遍历(DFS / BFS)、最小生成树(Prim / Kruskal)、四个最短路算法。
6.1 图的遍历:DFS 与 BFS
核心数据结构 :Mat_Graph 结构体------vertex[100] 顶点表、arc[100][100] 邻接矩阵、vertex_num、edge_num。全局 visited[100] 做访问标记。
DFS(graph/dfs/dfs.c):
- 判定条件
arc[i][j] == 1 && visited[j] == 0,按 j 从小到大扫描,访问顺序由顶点编号决定 - 对称化循环
arc[j][i] = arc[i][j]在全 i、j 范围执行只写了一半有效数据,因矩阵已全 0 且只填上三角,结果正确但写法不严谨 visited是全局数组,多次调用必须手动重置,否则第二次什么都不输出- 起点固定为 0,非连通图只会遍历到含顶点 0 的连通分量,没有外层补漏循环
- DFS 的递归本质即栈(系统函数调用栈)
BFS(graph/dfs/bfs.c):
- 与 DFS 的关键差异 :入队时就置
visited[j] = 1并立即打印,而非出队时标记------这是 BFS 不重复入队的核心技巧 - 手写顺序队列,判空
front != rear,没有判满、没有取模,靠MAXSIZE = 100与顶点数 9 保证不溢出 - BFS 的层序性质与"边权全 1 时最短路径"等价;队列 FIFO 保证"先发现先扩展"
时间复杂度: 邻接矩阵下 DFS / BFS 均为 O(V²);空间 O(V²)。若改用邻接表则为 O(V+E)
c
#include<stdio.h>
//深度优先 dfs
typedef char Vertextype;
typedef int Edgetype;
#define MAXSIZE 100
typedef struct
{
Vertextype vertex[MAXSIZE];//顶点
Edgetype arc[MAXSIZE][MAXSIZE];//二维数组
int vertex_num;//顶点的数量
int edge_num;//边的数量
}Mat_Graph;//矩阵的图
int visited[MAXSIZE];//记录被访问的顶点
void creat_graph(Mat_Graph* G)//创造图
{
G->vertex_num = 9;
G->edge_num = 15;
G->vertex[0] = 'A';
G->vertex[1] = 'B';
G->vertex[2] = 'C';
G->vertex[3] = 'D';
G->vertex[4] = 'E';
G->vertex[5] = 'F';
G->vertex[6] = 'G';
G->vertex[7] = 'H';
G->vertex[8] = 'I';
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[i][j] = 0;
}
}
// A-B A-F
G->arc[0][1] = 1;
G->arc[0][5] = 1;
// B-C B-I B-G
G->arc[1][2] = 1;
G->arc[1][8] = 1;
G->arc[1][6] = 1;
// C-D C-I
G->arc[2][3] = 1;
G->arc[2][8] = 1;
// D-E D-H D-G D-I
G->arc[3][4] = 1;
G->arc[3][7] = 1;
G->arc[3][6] = 1;
G->arc[3][8] = 1;
// E-F E-H
G->arc[4][5] = 1;
G->arc[4][7] = 1;
// F-G
G->arc[5][6] = 1;
// G-H
G->arc[6][7] = 1;
//对称
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[j][i] = G->arc[i][j];
}
}
}
void dfs(Mat_Graph* G, int i)
{
visited[i] = 1;
printf("%c ", G->vertex[i]);
for (int j = 0; j < G->vertex_num; j++)
{
if (G->arc[i][j] == 1 && visited[j] == 0)
{
dfs(G, j);
}
}
}
int main()
{
Mat_Graph G;
creat_graph(&G);
for (int i = 0; i < G.vertex_num; i++)
{
visited[i] = 0;
}
dfs(&G, 0);
return 0;
}
c
#include<stdio.h>
//广度优先 bfs
typedef char Vertextype;
typedef int Edgetype;
#define MAXSIZE 100
typedef struct
{
Vertextype vertex[MAXSIZE];//顶点
Edgetype arc[MAXSIZE][MAXSIZE];//二维数组
int vertex_num;//顶点的数量
int edge_num;//边的数量
}Mat_Graph;//矩阵的图
int visited[MAXSIZE];//记录被访问的顶点
int front = 0;
int rear = 0;
int queue[MAXSIZE];
void creat_graph(Mat_Graph* G)//创造图
{
G->vertex_num = 9;
G->edge_num = 15;
G->vertex[0] = 'A';
G->vertex[1] = 'B';
G->vertex[2] = 'C';
G->vertex[3] = 'D';
G->vertex[4] = 'E';
G->vertex[5] = 'F';
G->vertex[6] = 'G';
G->vertex[7] = 'H';
G->vertex[8] = 'I';
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[i][j] = 0;
}
}
// A-B A-F
G->arc[0][1] = 1;
G->arc[0][5] = 1;
// B-C B-I B-G
G->arc[1][2] = 1;
G->arc[1][8] = 1;
G->arc[1][6] = 1;
// C-D C-I
G->arc[2][3] = 1;
G->arc[2][8] = 1;
// D-E D-H D-G D-I
G->arc[3][4] = 1;
G->arc[3][7] = 1;
G->arc[3][6] = 1;
G->arc[3][8] = 1;
// E-F E-H
G->arc[4][5] = 1;
G->arc[4][7] = 1;
// F-G
G->arc[5][6] = 1;
// G-H
G->arc[6][7] = 1;
//对称
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[j][i] = G->arc[i][j];
}
}
}
void bfs(Mat_Graph* G)
{
int i = 0;
visited[i] = 1;
printf("%c ", G->vertex[i]);
queue[rear] = i;
rear++;
while (front != rear)
{
i = queue[front];
front++;
for (int j = 0; j < G->vertex_num; j++)
{
if (G->arc[i][j] == 1 && visited[j] == 0)
{
visited[j] = 1;
printf("%c ", G->vertex[j]);
queue[rear] = j;
rear++;
}
}
}
}
int main()
{
Mat_Graph G;
creat_graph(&G);
for (int i = 0; i < G.vertex_num; i++)
{
visited[i] = 0;
}
bfs(&G);
return 0;
}
6.2 最小生成树:Prim(graph/mst/prim.c)
核心数据结构 :链式前向星邻接表(Edge{to,w,next} + head[] 头插法链)+ 手写小根堆(HeapNode{cost,node})+ char set[MAXN] 标记顶点是否已进生成树。
算法流程: 起点固定为 1 号点 → 把 head[1] 所有边入堆 → 循环弹堆顶最小边 → 若 set[next] == 0 则加入生成树(cnt++、set[next] = 1、ans += cost)并把新点所有邻边继续压堆;若已在 set 中则丢弃(会成环)。
实现要点:
- 这是**"边驱动"的 Prim**:堆里存边而非"点到树的最小距离",同一点可能多次入堆,靠
set去重 ------ 等价于懒删除 - Prim 是点扩展贪心 (每次取横切边中最小者),Kruskal 是边扩展贪心
- 连通性判定:
cnt == n输出ans,否则输出orz(图不连通,无生成树) - 链式前向星 = 数组模拟链表,比
vector更省内存且更适合竞赛 - 正确性依据:最小生成树的切割性质 与环性质
时间复杂度: 建图 O(M);每条边入堆最多一次,堆操作 O(log M),总 O(M log M);空间 O(N + M)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXN 5005
#define MAXM 200005
/* ---------- 邻接表 ---------- */
typedef struct Edge {
int to;
int w;
int next;
} Edge;
Edge edges[MAXM * 2]; // 无向图,边要存两遍
int head[MAXN]; // 每个点的第一条边下标
int edgeCnt = 0;
void addEdge(int u, int v, int w) {
edges[edgeCnt].to = v;
edges[edgeCnt].w = w;
edges[edgeCnt].next = head[u];
head[u] = edgeCnt++;
}
/* ---------- 小根堆(按权值排序) ---------- */
typedef struct {
int cost; // 权值
int node; // 目标点
} HeapNode;
HeapNode heap[MAXM * 2];
int heapSize = 0;
void heapPush(int cost, int node) {
int i = heapSize++;
heap[i].cost = cost;
heap[i].node = node;
// 上浮
while (i > 0) {
int p = (i - 1) / 2;
if (heap[p].cost <= heap[i].cost) break;
HeapNode tmp = heap[p];
heap[p] = heap[i];
heap[i] = tmp;
i = p;
}
}
HeapNode heapPop(void) {
HeapNode top = heap[0];
heap[0] = heap[--heapSize];
// 下沉
int i = 0;
while (1) {
int l = i * 2 + 1;
int r = i * 2 + 2;
int smallest = i;
if (l < heapSize && heap[l].cost < heap[smallest].cost) smallest = l;
if (r < heapSize && heap[r].cost < heap[smallest].cost) smallest = r;
if (smallest == i) break;
HeapNode tmp = heap[i];
heap[i] = heap[smallest];
heap[smallest] = tmp;
i = smallest;
}
return top;
}
int heapEmpty(void) {
return heapSize == 0;
}
/* ---------- 主函数 ---------- */
int main() {
int n, m;
// 应对多组测试数据
while (scanf("%d", &n) != EOF) {
// 【1. 建图】邻接表:mst[u] 存的是 {v, w}
memset(head, -1, sizeof(head));
edgeCnt = 0;
heapSize = 0;
scanf("%d", &m);
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
// 【无向图】必须双向加边
addEdge(u, v, w);
addEdge(v, u, w);
}
// 【2. Prim 核心数据结构】
// 小根堆:存放所有"已选中点伸出去"的边,按权值从小到大排序
// 结构体里把 cost 放前面,方便比较
// 【3. 初始化】
// 随便选一个点开始(这里选 1),把它的所有边扔进小根堆
for (int e = head[1]; e != -1; e = edges[e].next) {
heapPush(edges[e].w, edges[e].to); // {cost, next}
}
char set[MAXN]; // set 记录节点是否已经加入生成树
memset(set, 0, sizeof(set));
int cnt = 1; // 已经选中的节点数(因为从1开始,所以初始化为1)
set[1] = 1; // 标记起点 1 已加入
int ans = 0; // 记录最小生成树的总权值
// 【4. 贪心选边】
while (!heapEmpty()) {
HeapNode edge = heapPop(); // 每次弹出权值最小的边(贪心策略)
int cost = edge.cost; // 这条边的权重
int next = edge.node; // 这条边指向的目标点
// 如果这个目标点还没被选过(不在 set 中)
if (!set[next]) {
cnt++; // 选中节点数 +1
set[next] = 1; // 标记已加入
ans += cost; // 累加权值
// 【核心步骤】:新点加入了,把它所有的邻边也扔进堆里
for (int e = head[next]; e != -1; e = edges[e].next) {
heapPush(edges[e].w, edges[e].to); // {cost, next}
}
}
// 如果目标点已经在 set 中,说明该边会成环,直接舍弃
}
// 【5. 判断连通性并输出】
if (cnt == n) {
printf("%d\n", ans);
} else {
printf("orz\n");
}
}
return 0;
}
6.3 最小生成树:Kruskal(graph/mst/kruskal.c)
核心数据结构 :边集数组 int edges[MAXM][3](二维数组代替结构体,索引 0/1/2 分别是 u/v/w)+ 并查集 int father[MAXN]。
算法三步:
- 按权值
qsort升序 - 依次遍历每条边,
Union(from,to)成功才入选 - 选够
n-1条即break(提前退出优化)
c
int find(int x) {
if (x != father[x]) { // 如果 x 不是根节点
father[x] = find(father[x]); // 递归查找根节点,并压缩路径
}
return father[x]; // 返回根节点
}
int Union(int x, int y) {
int px = find(x); // 找到 x 的根节点
int py = find(y); // 找到 y 的根节点
if (px != py) { // 如果根节点不同,说明不在同一集合
father[px] = py; // 将 x 的集合挂到 y 的集合下(简单合并)
return 1; // 合并成功,这条边可以加入生成树
}
return 0; // 根相同,合并失败(加入这条边会形成环)
}
实现要点:
- 路径压缩是 Kruskal 效率的关键;本文件省略了按秩/按 size 合并,最坏情况会让树退化成链
- 重边、自环由并查集自然处理(自环 find 相同必然返回 0)
qsort(edges, m, sizeof(edges[0]), cmp):sizeof(edges[0])是一整行 3 个 int 的字节数,正是二维数组行排序的正确写法- Kruskal 本质是图形拟阵上的贪心;MST 边数恒为 n-1 是判定连通性的依据
时间复杂度: 排序 O(M log M);并查集近似 O(α(N));总 O(M log M);空间 O(N+M)
c
//kruskal 算法
#include <stdio.h>
#include <stdlib.h>
#define MAXN 5001
#define MAXM 200001
int n, m; // n = 节点数(村庄数),m = 边数(可修建的公路数)
int father[MAXN]; // 并查集:father[i] 表示节点 i 的父节点
int edges[MAXM][3]; // 边集:edges[i] = {起点, 终点, 权值}
void init() {
for (int i = 1; i <= n; i++) {
father[i] = i; // 每个节点都是自己的根节点
}
}
int find(int x) {
if (x != father[x]) { // 如果 x 不是根节点
father[x] = find(father[x]); // 递归查找根节点,并压缩路径
}
return father[x]; // 返回根节点
}
int Union(int x, int y) {
int px = find(x); // 找到 x 的根节点
int py = find(y); // 找到 y 的根节点
if (px != py) { // 如果根节点不同,说明不在同一集合
father[px] = py; // 将 x 的集合挂到 y 的集合下(简单合并)
return 1; // 合并成功,这条边可以加入生成树
}
return 0; // 根相同,合并失败(加入这条边会形成环)
}
int cmp(const void *a, const void *b) {
int *x = (int *)a;
int *y = (int *)b;
return x[2] - y[2];
}
int main() {
// ---- 读取 n 和 m ----
scanf("%d%d", &n, &m);
init();
// ---- 读取 m 条边 ----
for (int i = 0; i < m; i++) {
scanf("%d%d%d", &edges[i][0], &edges[i][1], &edges[i][2]);
}
// ---- Kruskal 算法第一步:按权值升序排序 ----
// 贪心思想:优先选择成本最小的边
qsort(edges, m, sizeof(edges[0]), cmp);
// ---- Kruskal 算法第二步:从小到大遍历边 ----
int ans = 0; // 记录最小生成树的总权值(最低成本)
int cnt = 0; // 记录已选入生成树的边数
for (int i = 0; i < m; i++) {
int from = edges[i][0]; // 起点
int to = edges[i][1]; // 终点
int w = edges[i][2]; // 权值
// 如果这条边连接的两个节点不在同一集合(不会形成环)
if (Union(from, to)) {
cnt++; // 选中边数 +1
ans += w; // 累加成本
// 优化:提前退出
// 最小生成树只需要 n-1 条边,选够了就不用再遍历了
if (cnt == n - 1) {
break;
}
}
}
// ---- 输出结果 ----
// 如果选中的边数 == n-1,说明所有节点连通,输出最低成本
// 否则输出 "orz"(表示图不连通,无法实现村村通)
if (cnt == n - 1) {
printf("%d\n", ans);
} else {
printf("orz\n");
}
return 0;
}
6.4 最短路:Dijkstra(graph/shortest_path/dijkstra.c)
核心数据结构 :链式前向星 + 手写小根堆(比较键是 dist)+ int dist[] 最短路估计 + int vis[] 是否已确定最短路。
核心流程: 源点入堆 → 弹 dist 最小点 → if(vis[u]) continue(懒删除 ,跳过过期堆项)→ vis[u]=1 → 遍历出边松弛。
c
while (!heapEmpty()) {
HeapNode cur = heapPop();
int u = cur.u;
if (vis[u]) continue; // 懒删除
vis[u] = 1;
for (int e = head[u]; e != -1; e = edges[e].next) {
int v = edges[e].to, w = edges[e].w;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
heapPush(dist[v], v);
}
}
}
★ 工程技巧:
INF = 0x3f3f3f3f的妙处
- 既够大(约 1.06e9),又能保证
INF + INF不溢出 int(两倍 2.12e9 < INT_MAX 2.147e9)memset(dist, 0x3f, sizeof(int)*(n+1))按 0x3f 逐字节填充正好得到 0x3f3f3f3f- 只清 n+1 个 int(而非清满 MAXN)是多组数据的效率考虑
实现要点:
- 松弛成功才入堆,堆中可能存在同点多个历史版本,靠
vis过滤 ------ 替代堆内 decrease-key 的经典写法 - 不支持负权边(贪心前提会被破坏)
- 堆优化把 O(V²) 的朴素 Dijkstra 降到 O((N+M) log M),在 10 万点规模下朴素版会 TLE
时间复杂度: O((N+M) log M);空间 O(N+M)
c
// dijkstra.c ------ 单源最短路(非负权)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXN 100005
#define MAXM 200005
#define INF 0x3f3f3f3f
typedef struct Edge {
int to, w, next;
} Edge;
Edge edges[MAXM * 2];
int head[MAXN], edgeCnt;
void addEdge(int u, int v, int w) {
edges[edgeCnt] = (Edge){v, w, head[u]};
head[u] = edgeCnt++;
}
/* ---------- 小根堆 ---------- */
typedef struct { int dist, u; } HeapNode;
HeapNode heap[MAXM * 2];
int heapSize;
void heapPush(int dist, int u) {
int i = heapSize++;
heap[i] = (HeapNode){dist, u};
while (i > 0) {
int p = (i - 1) / 2;
if (heap[p].dist <= heap[i].dist) break;
HeapNode t = heap[p]; heap[p] = heap[i]; heap[i] = t;
i = p;
}
}
HeapNode heapPop(void) {
HeapNode top = heap[0];
heap[0] = heap[--heapSize];
int i = 0;
while (1) {
int l = i * 2 + 1, r = i * 2 + 2, s = i;
if (l < heapSize && heap[l].dist < heap[s].dist) s = l;
if (r < heapSize && heap[r].dist < heap[s].dist) s = r;
if (s == i) break;
HeapNode t = heap[i]; heap[i] = heap[s]; heap[s] = t;
i = s;
}
return top;
}
int heapEmpty(void) { return heapSize == 0; }
/* ---------- Dijkstra ---------- */
int dist[MAXN];
int vis[MAXN];
void dijkstra(int n, int s) {
memset(dist, 0x3f, sizeof(int) * (n + 1));
memset(vis, 0, sizeof(int) * (n + 1));
heapSize = 0;
dist[s] = 0;
heapPush(0, s);
while (!heapEmpty()) {
HeapNode cur = heapPop();
int u = cur.u;
if (vis[u]) continue; // 懒删除
vis[u] = 1;
for (int e = head[u]; e != -1; e = edges[e].next) {
int v = edges[e].to, w = edges[e].w;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
heapPush(dist[v], v);
}
}
}
}
int main(void) {
int n, m;
while (scanf("%d%d", &n, &m) != EOF) {
memset(head, -1, sizeof(int) * (n + 1));
edgeCnt = 0;
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w);
addEdge(v, u, w); // 无向图;有向图删掉这行
}
dijkstra(n, 1);
for (int i = 1; i <= n; i++) {
if (dist[i] == INF) printf("INF ");
else printf("%d ", dist[i]);
}
printf("\n");
}
return 0;
}
6.5 最短路:Bellman-Ford(graph/shortest_path/bellman_ford.c)
核心数据结构 :只有边集数组 Edge{int u,v,w}。没有邻接表/邻接矩阵 ------ Bellman-Ford 只需能枚举所有边。
算法流程: 核心循环 n-1 轮 ,每轮遍历全部 m 条边 做松弛。每轮用 updated 标记是否发生松弛,一整轮无松弛则 break(图已收敛)。主循环后再扫一遍全部边,若仍能松弛则存在源点可达的负环。
理论依据: 「最多 n-1 条边组成最短路」这一长度上界是轮数的理论依据;第 n 轮仍能松弛 ⟺ 存在负环。
实现要点:
- 松弛前先判
dist[u]!=INF,避免"用不可达点去松弛"以及 INF 参与加法溢出 - 只有有向边语义(无向图的负权边天然构成负环,代码未做双向加边)
- 未做路径记录(没有
pre数组),只求距离值
时间复杂度: O(N·M);负环检测额外 O(M);空间 O(N+M)
c
//
// Created by Administrator on 2026/10/6.
//
// bellman_ford.c ------ 单源最短路,支持负权,可判负环
#include <stdio.h>
#include <string.h>
#define MAXN 1005
#define MAXM 20005
#define INF 0x3f3f3f3f
typedef struct { int u, v, w; } Edge;
Edge edges[MAXM];
int dist[MAXN];
/* 返回 1 表示存在从源点可达的负环 */
int bellmanFord(int n, int m, int s) {
memset(dist, 0x3f, sizeof(int) * (n + 1));
dist[s] = 0;
for (int i = 1; i <= n - 1; i++) {
int updated = 0;
for (int j = 0; j < m; j++) {
int u = edges[j].u, v = edges[j].v, w = edges[j].w;
if (dist[u] != INF && dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
updated = 1;
}
}
if (!updated) break; // 提前退出
}
// 第 n 轮还能松弛 → 有负环
for (int j = 0; j < m; j++) {
int u = edges[j].u, v = edges[j].v, w = edges[j].w;
if (dist[u] != INF && dist[v] > dist[u] + w) return 1;
}
return 0;
}
int main(void) {
int n, m, s;
while (scanf("%d%d%d", &n, &m, &s) != EOF) {
for (int i = 0; i < m; i++) {
scanf("%d%d%d", &edges[i].u, &edges[i].v, &edges[i].w);
}
if (bellmanFord(n, m, s)) {
printf("存在负环\n");
} else {
for (int i = 1; i <= n; i++) {
if (dist[i] == INF) printf("INF ");
else printf("%d ", dist[i]);
}
printf("\n");
}
}
return 0;
}
6.6 最短路:SPFA(graph/shortest_path/spfa.c)
核心数据结构 :链式前向星 + dist[] + inQueue[](是否在队中)+ cnt[](入队次数,判负环)+ 手写顺序队列。
核心流程: 源点入队 → 取队头出队 → 遍历出边松弛 → 只有松弛成功且 v 不在队中才入队 → 入队时 if(++cnt[v] >= n) 判负环。
★ 与 Dijkstra 的关键对比
- SPFA 没有
vis数组 (不"确定"最短路,允许反复改进),松弛条件也不判vis- SPFA = Bellman-Ford + 队列优化,只有"dist 变小"的点才有资格继续传播
inQueue去重是 SPFA 的关键;负环判定:入队次数 ≥ n- SPFA 平均 O(k·M) 很快,但最坏 O(N·M);在稠密/构造数据下会退化,这是竞赛中被 Dijkstra 取代的原因
实现要点:
- 队列用线性数组 +
qHead<qTail判空,没有取模,靠MAXN*10容量兜底 - 出队时只清
inQueue,不清cnt------cnt是累计入队次数 - 此处没有判
dist[u]!=INF(能入队的点 dist 必已被赋值,故安全)
时间复杂度: 平均 O(k·M),最坏 O(N·M);空间 O(N+M)
c
//
// Created by Administrator on 2026/10/6.
//
// spfa.c ------ 单源最短路,支持负权,可判负环
#include <stdio.h>
#include <string.h>
#define MAXN 100005
#define MAXM 200005
#define INF 0x3f3f3f3f
typedef struct Edge {
int to, w, next;
} Edge;
Edge edges[MAXM * 2];
int head[MAXN], edgeCnt;
void addEdge(int u, int v, int w) {
edges[edgeCnt] = (Edge){v, w, head[u]};
head[u] = edgeCnt++;
}
int dist[MAXN];
int inQueue[MAXN];
int cnt[MAXN]; // 入队次数,>=n 判定负环
int queue[MAXN * 10];
int qHead, qTail;
/* 返回 1 表示存在负环 */
int spfa(int n, int s) {
memset(dist, 0x3f, sizeof(int) * (n + 1));
memset(inQueue, 0, sizeof(int) * (n + 1));
memset(cnt, 0, sizeof(int) * (n + 1));
qHead = qTail = 0;
dist[s] = 0;
queue[qTail++] = s;
inQueue[s] = 1;
cnt[s] = 1;
while (qHead < qTail) {
int u = queue[qHead++];
inQueue[u] = 0;
for (int e = head[u]; e != -1; e = edges[e].next) {
int v = edges[e].to, w = edges[e].w;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
if (!inQueue[v]) {
queue[qTail++] = v;
inQueue[v] = 1;
if (++cnt[v] >= n) return 1; // 负环
}
}
}
}
return 0;
}
int main(void) {
int n, m, s;
while (scanf("%d%d%d", &n, &m, &s) != EOF) {
memset(head, -1, sizeof(int) * (n + 1));
edgeCnt = 0;
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w);
// addEdge(v, u, w); // 无向图加这一行
}
if (spfa(n, s)) {
printf("存在负环\n");
} else {
for (int i = 1; i <= n; i++) {
if (dist[i] == INF) printf("INF ");
else printf("%d ", dist[i]);
}
printf("\n");
}
}
return 0;
}
6.7 最短路:Floyd(graph/shortest_path/floyd.c)
核心数据结构 :二维邻接矩阵 int dist[MAXN][MAXN](MAXN = 505,约 1 MB 全局数组),dist[i][j] 既是初始边权也直接充当结果矩阵。
DP 状态转移: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]),k 必须是最外层循环(Floyd 正确性关键,写成内层即错)。
c
void floyd(int n) {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
if (dist[i][k] == INF) continue; // 剪枝
for (int j = 1; j <= n; j++) {
if (dist[k][j] == INF) continue;
if (dist[i][j] > dist[i][k] + dist[k][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
}
实现要点:
- 两处剪枝既避免无效松弛,也防止 INF + INF 溢出
- 初始化
dist[i][j] = (i == j) ? 0 : INF,对角线为 0 - 重边处理:读入时
if (w < dist[u][v])保留最小边 - 本质是阶段 DP :
k表示"允许经过编号 ≤ k 的中转点",正确性依赖 k 在最外层的阶段不变式 - 能处理负权边但不能有负环 (负环会让
dist[i][i] < 0) - 优势是一次求全源,劣势是 O(N³) 与 O(N²) 空间
时间复杂度: O(N³);空间 O(N²);适合 N ≤ 500。
c
//
// Created by Administrator on 2026/10/6.
//
// floyd.c ------ 多源最短路,任意两点间
#include <stdio.h>
#include <string.h>
#define MAXN 505
#define INF 0x3f3f3f3f
int dist[MAXN][MAXN];
void floyd(int n) {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
if (dist[i][k] == INF) continue; // 剪枝
for (int j = 1; j <= n; j++) {
if (dist[k][j] == INF) continue;
if (dist[i][j] > dist[i][k] + dist[k][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
}
int main(void) {
int n, m;
while (scanf("%d%d", &n, &m) != EOF) {
// 初始化
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dist[i][j] = (i == j) ? 0 : INF;
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
if (w < dist[u][v]) { // 去重边,保留最小
dist[u][v] = w;
dist[v][u] = w; // 无向图;有向图删掉这行
}
}
floyd(n);
// 输出任意两点最短距离(示例:输出 1 到 n)
int s = 1, t = n;
if (dist[s][t] == INF) printf("不可达\n");
else printf("%d\n", dist[s][t]);
// 如需完整矩阵:
// for (int i = 1; i <= n; i++) {
// for (int j = 1; j <= n; j++)
// printf(dist[i][j] == INF ? "INF " : "%d ", dist[i][j]);
// printf("\n");
// }
}
return 0;
}
6.8 四个最短路算法总对照
| 算法 | 适用场景 | 负权 | 负环检测 | 时间复杂度 | 核心数据结构 |
|---|---|---|---|---|---|
| Dijkstra | 单源、非负权 | ❌ | 不适用 | O((N+M)log M) | 链式前向星 + 小根堆 + vis 懒删除 |
| Bellman-Ford | 单源、可负权 | ✅ | 支持(第 n 轮仍松弛) | O(N·M) | 边集数组 |
| SPFA | 单源、可负权 | ✅ | 支持(入队次数 ≥ n) | 平均 O(kM),最坏 O(NM) | 链式前向星 + 队列 + inQueue |
| Floyd | 多源(全源) | ✅ | 不支持(但可检测) | O(N³) | 二维邻接矩阵 |
共同点: 四个文件的
INF常量完全一致,都是0x3f3f3f3f。
第七章 字符串结构:Trie
三个文件实现了同一 ADT(前缀树)的三种底层"路"表示------静态数组、指针 + 定长数组、指针 + 哈希表。三者的
pass/end双计数器语义完全一致,是"抽象接口固定、底层表示可变"的绝佳案例。
7.1 pass / end 双计数器设计
| 计数器 | 含义 | 对应查询 |
|---|---|---|
pass[i] |
经过节点 i 的单词数 | 以某前缀开头的单词数量(前缀计数) |
end[i] |
以节点 i 结尾的单词数 | 某单词精确出现的次数(支持重复单词) |
关键细节: insert 中根节点的 pass 也要 ++ (循环外先执行),这是 prefixNumber 与 deleteWord 对称性的前提。
7.2 静态数组实现(trie/trie_array.c)
核心数据结构 :int tree[MAXN][ALPHA](MAXN = 150001,ALPHA = 26)转移表,tree[u][c] 表示节点 u 沿字符 c 走到的子节点编号,0 表示无路 。根节点固定为 1 号。
实现要点:
- 字符到下标:
path = word[i] - 'a',只支持小写字母 deleteWord的剪枝逻辑:沿路径--pass[nxt],一旦pass[nxt]==0说明该子树不再被使用,直接tree[cur][path]=0断开并 return ------ 不回收到内存池,只断开引用clear末尾把cnt置 0 而build置 1,两组之间必须紧跟 build,否则根编号不合法(代码中的真实陷阱)- 空间换时间:
tree数组约 150001×26×4B ≈ 15.6 MB - 数组版用 0 作空指针哨兵、根固定为 1,避免指针开销
时间复杂度: 插入/查询/前缀计数/删除均 O(L)(L 为串长)。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXN 150001
#define ALPHA 26
int tree[MAXN][ALPHA]; // 转移边:tree[u][c] = 子节点编号
int pass[MAXN]; // 经过该节点的单词数
int end[MAXN]; // 以该节点结尾的单词数
int cnt; // 节点计数器(1 号节点为根)
/* ---------- 建树 / 清空 ---------- */
void build(void) {
cnt = 1;
memset(tree, 0, sizeof(tree));
memset(pass, 0, sizeof(pass));
memset(end, 0, sizeof(end));
}
/* ---------- 插入 ---------- */
void insert(const char *word) {
int cur = 1;
pass[cur]++;
for (int i = 0; word[i]; i++) {
int path = word[i] - 'a';
if (tree[cur][path] == 0) {
tree[cur][path] = ++cnt;
}
cur = tree[cur][path];
pass[cur]++;
}
end[cur]++;
}
/* ---------- 查询某个单词出现了几次 ---------- */
int search(const char *word) {
int cur = 1;
for (int i = 0; word[i]; i++) {
int path = word[i] - 'a';
if (tree[cur][path] == 0) {
return 0;
}
cur = tree[cur][path];
}
return end[cur];
}
/* ---------- 查询以 pre 为前缀的单词数 ---------- */
int prefixNumber(const char *pre) {
int cur = 1;
for (int i = 0; pre[i]; i++) {
int path = pre[i] - 'a';
if (tree[cur][path] == 0) {
return 0;
}
cur = tree[cur][path];
}
return pass[cur];
}
/* ---------- 删除一个单词 ---------- */
void deleteWord(const char *word) {
if (search(word) > 0) {
int cur = 1;
pass[cur]--; // 根节点的 pass 也要减
for (int i = 0; word[i]; i++) {
int path = word[i] - 'a';
int nxt = tree[cur][path];
if (--pass[nxt] == 0) {
tree[cur][path] = 0; // 剪枝
return;
}
cur = nxt;
}
end[cur]--;
}
}
/* ---------- 清空(比 build 更轻量,只清用过的节点) ---------- */
void clear(void) {
for (int i = 1; i <= cnt; i++) {
memset(tree[i], 0, sizeof(tree[i]));
end[i] = 0;
pass[i] = 0;
}
cnt = 0;
}
/* ---------- 主函数 ---------- */
int main(void) {
char line[512];
char word[256];
while (fgets(line, sizeof(line), stdin)) {
int m = atoi(line);
if (m <= 0) continue;
build();
for (int i = 0; i < m; i++) {
if (!fgets(line, sizeof(line), stdin)) break;
int op;
if (sscanf(line, "%d %255s", &op, word) != 2) continue;
if (op == 1) {
insert(word);
} else if (op == 2) {
deleteWord(word);
} else if (op == 3) {
printf("%s\n", search(word) > 0 ? "YES" : "NO");
} else if (op == 4) {
printf("%d\n", prefixNumber(word));
}
}
clear();
}
return 0;
}
7.3 指针 + 定长数组路(trie/trie_class_array.c)
核心数据结构 :typedef struct TrieNode { int pass; int end; struct TrieNode *nexts[26]; } TrieNode; ------ 每个节点持 26 个指针 。外层用 Trie { TrieNode *root; } 封装根。
实现要点:
insert中惰性建点 :if(!node->nexts[path]) node->nexts[path] = newNode();------ 只分配真正需要的分支(对比数组版预先占满二维表)newNode用calloc(1, sizeof(TrieNode))零初始化,保证nexts全 NULL、pass/end为 0freeNode是递归后序释放整棵子树,是 C 里最容易漏内存的地方erase剪枝时真的把整棵子树 free 掉,比数组版仅断开引用更彻底- 空间代价:每节点 26×8 = 208 字节指针开销,远大于数组版的 26×4 = 104 字节
时间复杂度: 四操作均 O(L) ;trieFree O(节点数)
c
// trie_class_array.c ------ 类 + 数组路实现前缀树
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ALPHA 26
typedef struct TrieNode {
int pass;
int end;
struct TrieNode *nexts[ALPHA];
} TrieNode;
static TrieNode *newNode(void) {
return (TrieNode *)calloc(1, sizeof(TrieNode));
}
static void freeNode(TrieNode *node) {
if (!node) return;
for (int i = 0; i < ALPHA; i++) freeNode(node->nexts[i]);
free(node);
}
typedef struct { TrieNode *root; } Trie;
void trieInit(Trie *t) { t->root = newNode(); }
void trieFree(Trie *t) { freeNode(t->root); t->root = NULL; }
void insert(Trie *t, const char *word) {
TrieNode *node = t->root;
node->pass++;
for (int i = 0; word[i]; i++) {
int path = word[i] - 'a';
if (!node->nexts[path]) node->nexts[path] = newNode();
node = node->nexts[path];
node->pass++;
}
node->end++;
}
int countWordsEqualTo(Trie *t, const char *word) {
TrieNode *node = t->root;
for (int i = 0; word[i]; i++) {
int path = word[i] - 'a';
if (!node->nexts[path]) return 0;
node = node->nexts[path];
}
return node->end;
}
int countWordsStartingWith(Trie *t, const char *pre) {
TrieNode *node = t->root;
for (int i = 0; pre[i]; i++) {
int path = pre[i] - 'a';
if (!node->nexts[path]) return 0;
node = node->nexts[path];
}
return node->pass;
}
void erase(Trie *t, const char *word) {
if (countWordsEqualTo(t, word) > 0) {
TrieNode *node = t->root;
node->pass--;
for (int i = 0; word[i]; i++) {
int path = word[i] - 'a';
TrieNode *nxt = node->nexts[path];
if (--nxt->pass == 0) {
freeNode(nxt);
node->nexts[path] = NULL;
return;
}
node = nxt;
}
node->end--;
}
}
int main(void) {
Trie trie;
trieInit(&trie);
char line[512], word[256];
if (!fgets(line, sizeof(line), stdin)) return 0;
int n = atoi(line);
for (int i = 0; i < n; i++) {
if (!fgets(line, sizeof(line), stdin)) break;
int op;
if (sscanf(line, "%d %255s", &op, word) != 2) continue;
if (op == 1) insert(&trie, word);
else if (op == 2) erase(&trie, word);
else if (op == 3) printf("%d\n", countWordsEqualTo(&trie, word));
else if (op == 4) printf("%d\n", countWordsStartingWith(&trie, word));
}
trieFree(&trie);
return 0;
}
7.4 指针 + 哈希路(trie/trie_class_hash.c)
核心数据结构 :HashEntry { int key; struct TrieNode *value; struct HashEntry *next; } 是拉链法桶节点;TrieNode { int pass; int end; HashEntry *buckets[128]; } ------ 每个 Trie 节点自带一张 128 桶的哈希表。
实现要点:
- 字符键统一用
(unsigned char)word[i]转成 0~255 的整数 ------ 比- 'a'更通用,可支持大写、数字、标点甚至 UTF-8 字节 - 每个节点独立开一张 128 桶哈希表是最独特的设计:等价于"用哈希表替代定长数组路",只有实际出现的字符才占内存
- 代价是每节点固定 128×8 = 1KB 的桶数组开销,对稀疏字符集(中文/大量标点)才划算
mapRemove用二级指针HashEntry **pp遍历,能优雅地删头结点mapPut命中已存在键时只更新 value,避免键重复导致的内存泄漏hashFunc用((key % 128) + 128) % 128处理负数取模
时间复杂度: 单次操作 O(L) 期望;最坏(全冲突)O(L·k)
c
// trie_class_hash.c ------ 类 + 哈希路实现前缀树
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HASH_BUCKETS 128
typedef struct HashEntry {
int key;
struct TrieNode *value;
struct HashEntry *next;
} HashEntry;
typedef struct TrieNode {
int pass;
int end;
HashEntry *buckets[HASH_BUCKETS];
} TrieNode;
static int hashFunc(int key) {
return ((key % HASH_BUCKETS) + HASH_BUCKETS) % HASH_BUCKETS;
}
static TrieNode *mapGet(TrieNode *node, int key) {
int h = hashFunc(key);
for (HashEntry *e = node->buckets[h]; e; e = e->next)
if (e->key == key) return e->value;
return NULL;
}
static void mapPut(TrieNode *node, int key, TrieNode *value) {
int h = hashFunc(key);
for (HashEntry *e = node->buckets[h]; e; e = e->next)
if (e->key == key) { e->value = value; return; }
HashEntry *ne = (HashEntry *)malloc(sizeof(HashEntry));
ne->key = key; ne->value = value;
ne->next = node->buckets[h];
node->buckets[h] = ne;
}
static void mapRemove(TrieNode *node, int key) {
int h = hashFunc(key);
HashEntry **pp = &node->buckets[h];
while (*pp) {
if ((*pp)->key == key) {
HashEntry *del = *pp;
*pp = del->next;
free(del);
return;
}
pp = &(*pp)->next;
}
}
static TrieNode *newNode(void) {
return (TrieNode *)calloc(1, sizeof(TrieNode));
}
static void freeNode(TrieNode *node) {
if (!node) return;
for (int i = 0; i < HASH_BUCKETS; i++) {
HashEntry *e = node->buckets[i];
while (e) {
HashEntry *nxt = e->next;
freeNode(e->value);
free(e);
e = nxt;
}
}
free(node);
}
typedef struct { TrieNode *root; } Trie;
void trieInit(Trie *t) { t->root = newNode(); }
void trieFree(Trie *t) { freeNode(t->root); t->root = NULL; }
void insert(Trie *t, const char *word) {
TrieNode *node = t->root;
node->pass++;
for (int i = 0; word[i]; i++) {
int path = (unsigned char)word[i];
TrieNode *nxt = mapGet(node, path);
if (!nxt) { nxt = newNode(); mapPut(node, path, nxt); }
node = nxt;
node->pass++;
}
node->end++;
}
int countWordsEqualTo(Trie *t, const char *word) {
TrieNode *node = t->root;
for (int i = 0; word[i]; i++) {
TrieNode *nxt = mapGet(node, (unsigned char)word[i]);
if (!nxt) return 0;
node = nxt;
}
return node->end;
}
int countWordsStartingWith(Trie *t, const char *pre) {
TrieNode *node = t->root;
for (int i = 0; pre[i]; i++) {
TrieNode *nxt = mapGet(node, (unsigned char)pre[i]);
if (!nxt) return 0;
node = nxt;
}
return node->pass;
}
void erase(Trie *t, const char *word) {
if (countWordsEqualTo(t, word) > 0) {
TrieNode *node = t->root;
node->pass--;
for (int i = 0; word[i]; i++) {
int path = (unsigned char)word[i];
TrieNode *nxt = mapGet(node, path);
if (--nxt->pass == 0) {
mapRemove(node, path);
freeNode(nxt);
return;
}
node = nxt;
}
node->end--;
}
}
int main(void) {
Trie trie;
trieInit(&trie);
char line[512], word[256];
if (!fgets(line, sizeof(line), stdin)) return 0;
int n = atoi(line);
for (int i = 0; i < n; i++) {
if (!fgets(line, sizeof(line), stdin)) break;
int op;
if (sscanf(line, "%d %255s", &op, word) != 2) continue;
if (op == 1) insert(&trie, word);
else if (op == 2) erase(&trie, word);
else if (op == 3) printf("%d\n", countWordsEqualTo(&trie, word));
else if (op == 4) printf("%d\n", countWordsStartingWith(&trie, word));
}
trieFree(&trie);
return 0;
}
7.5 三种 Trie 实现对照
| 维度 | trie_array | trie_class_array | trie_class_hash |
|---|---|---|---|
| 路表示 | int tree[N][26] 静态二维数组 |
TrieNode *nexts[26] 指针数组 |
HashEntry *buckets[128] 哈希桶 |
| 节点分配 | 预分配整块二维数组 | calloc 惰性建点 | calloc 惰性建点 |
| 字符集 | 仅小写字母 a-z | 仅小写字母(按 26 算) | 任意字符(unsigned char 0~255) |
| 每节点开销 | 26×4 = 104 字节 | 26×8 = 208 字节 + 分配开销 | 128×8 = 1KB + entry 开销 |
| 删除回收 | 只断开引用,不回收 | 递归 freeNode 真正释放 | 递归 freeNode 真正释放 |
| 内存管理 | 无 malloc/free | 需 trieFree 递归释放 | 需 trieFree 递归释放 |
| 适用场景 | 小写字母、追求速度 | 小写字母、要动态增删 | 大字符集/稀疏字符 |
第八章 位运算结构
8.1 位集 Bitset(bitset/bitset.c)
核心数据结构 :typedef struct { int *bits; int words; } Bitset; ------ bits 是 int 数组,每个 int 承载 32 个比特 ;words = (n+31)/32 是 word 数(向上取整)。
位运算四件套:
| 操作 | 函数 | 位运算写法 |
|---|---|---|
| 置位 | add(b, x) |
`bits[x/32] |
| 清位 | removeBit(b, x) |
bits[x/32] &= ~(1 << (x%32)) |
| 测试 | contains(b, x) |
(bits[x/32] & (1 << (x%32))) != 0 |
| 翻转 | reverseBit(b, x) |
bits[x/32] ^= (1 << (x%32)) |
实现要点:
- 定位公式:
index = x/32定位字,bit = x%32定位位内偏移 calloc保证初始全 0(位集语义要求"默认不存在")size统计 1 的个数时用__builtin_popcount((unsigned int)bits[i])------ 转 unsigned 是必要的,对负数符号位行为不友好- 空间相比 bool 数组节省 32 倍;n 位只需 n/32 个 int
- 位运算作为集合操作:
|并、&交、^对称差、~补
时间复杂度: add/remove/contains/reverse 均 O(1) ;size O(words) = O(n/32)
c
// bitset.c ------ 位集(按 Java 版 1:1 移植,int 数组 + 32 位一个 word)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int *bits; // 位数组,每个 int 存 32 位
int words; // int 的个数,= (n + 31) / 32
} Bitset;
/* ---------- 构造函数:对应 Java 的 Bitset(int n) ---------- */
void bitsetInit(Bitset *b, int n) {
b->words = (n + 31) / 32; // 向上取整:(a+b-1)/b
b->bits = (int *)calloc(b->words, sizeof(int));
}
/* ---------- 析构函数:释放 ---------- */
void bitsetFree(Bitset *b) {
free(b->bits);
b->bits = NULL;
b->words = 0;
}
/* ---------- add:第 x 位置 1 ---------- */
void add(Bitset *b, int x) {
int index = x / 32;
int bit = x % 32;
b->bits[index] |= (1 << bit);
}
/* ---------- remove:第 x 位置 0 ---------- */
void removeBit(Bitset *b, int x) {
int index = x / 32;
int bit = x % 32;
b->bits[index] &= ~(1 << bit);
}
/* ---------- contains:第 x 位是否为 1 ---------- */
int contains(Bitset *b, int x) {
int index = x / 32;
int bit = x % 32;
return (b->bits[index] & (1 << bit)) != 0;
}
/* ---------- reverse:第 x 位翻转 ---------- */
void reverseBit(Bitset *b, int x) {
int index = x / 32;
int bit = x % 32;
b->bits[index] ^= (1 << bit);
}
/* ---------- size:1 的总个数(对应 Java 的 Integer.bitCount) ---------- */
int size(Bitset *b) {
int count = 0;
for (int i = 0; i < b->words; i++) {
// GCC / Clang 内置函数,等价于 Integer.bitCount
count += __builtin_popcount((unsigned int)b->bits[i]);
}
return count;
}
/* ---------- 打印(调试用) ---------- */
void print(Bitset *b, int n) {
printf("[");
for (int i = 0; i < n; i++) {
printf("%d", contains(b, i));
if (i != n - 1) printf(", ");
}
printf("]\n");
}
/* ---------- 主函数测试 ---------- */
int main(void) {
int n, q;
if (scanf("%d%d", &n, &q) != 2) return 0;
Bitset bs;
bitsetInit(&bs, n);
while (q--) {
char op[16];
int x;
scanf("%s", op);
if (strcmp(op, "add") == 0) {
scanf("%d", &x);
add(&bs, x);
} else if (strcmp(op, "remove") == 0) {
scanf("%d", &x);
removeBit(&bs, x);
} else if (strcmp(op, "contains") == 0) {
scanf("%d", &x);
printf("%d\n", contains(&bs, x));
} else if (strcmp(op, "reverse") == 0) {
scanf("%d", &x);
reverseBit(&bs, x);
} else if (strcmp(op, "size") == 0) {
printf("%d\n", size(&bs));
} else if (strcmp(op, "print") == 0) {
print(&bs, n);
}
}
bitsetFree(&bs);
return 0;
}
8.2 布隆过滤器(bitset/bloom_filter.c)
核心数据结构 :typedef struct { unsigned long long *bits; int bitSize; int words; } BloomFilter; ------ 位图每元素 64 位 (区别于 Bitset 的 32 位 int);words = (size+63)/64。
双哈希设计: hash1 用 BASE1=131、MOD1=1000000007LL;hash2 用 BASE2=13331、MOD2=1000000009LL。都是多项式滚动哈希 h = h*base + c,最后 % bitSize 映射到位下标。
核心操作: bloomAdd 把两个哈希位都置 1;bloomContains 要求两个哈希位都为 1 才返回真(逻辑与)。
★ 布隆过滤器的标准语义
- 假阳性(false positive) :
bloomContains返回 1 只能说"可能存在",实际可能不存在- 不存在假阴性:如果返回 0,那一定不存在
- 不支持删除:因为一个位被多个元素共享,清位会影响其他元素
- 误判率与位数组大小 m、元素数 n、哈希个数 k 三者相关
实现要点:
1ULL << (i & 63)使用 unsigned long long 字面量,避免 32 位移位溢出/符号位 UB ------ 64 位位图必须注意的细节i >> 6与i & 63是 /64 与 %64 的位运算等价形式,比除法取模更快- 大质数用
LL后缀保证 long long 运算(h*BASE1会超过 32 位)
时间复杂度: bloomAdd / bloomContains 均 O(L);空间约 100000 位 = 12.5 KB
c
// bloom_filter.c ------ 双哈希布隆过滤器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BASE1 131
#define BASE2 13331
#define MOD1 1000000007LL
#define MOD2 1000000009LL
typedef struct {
unsigned long long *bits; // 位图,每个元素 64 位
int bitSize; // 位图总位数
int words; // 需要多少个 64 位
} BloomFilter;
/* ---------- 构造函数:对应 Java 的 BloomFilter(int size) ---------- */
void bloomInit(BloomFilter *bf, int size) {
bf->bitSize = size;
bf->words = (size + 63) / 64; // 向上取整
bf->bits = (unsigned long long *)calloc(bf->words, sizeof(unsigned long long));
}
/* ---------- 析构函数 ---------- */
void bloomFree(BloomFilter *bf) {
free(bf->bits);
bf->bits = NULL;
bf->words = bf->bitSize = 0;
}
/* ---------- hash1:BASE1 / MOD1 的多项式滚动哈希 ---------- */
static long long hash1(const BloomFilter *bf, const char *s) {
long long h = 0;
for (int i = 0; s[i]; i++) {
h = (h * BASE1 + (unsigned char)s[i]) % MOD1;
}
return h % bf->bitSize;
}
/* ---------- hash2:BASE2 / MOD2 的多项式滚动哈希 ---------- */
static long long hash2(const BloomFilter *bf, const char *s) {
long long h = 0;
for (int i = 0; s[i]; i++) {
h = (h * BASE2 + (unsigned char)s[i]) % MOD2;
}
return h % bf->bitSize;
}
/* ---------- setBit:把第 idx 位置 1 ---------- */
static void setBit(BloomFilter *bf, long long idx) {
int i = (int)idx;
bf->bits[i >> 6] |= (1ULL << (i & 63)); // i / 64 定位,i % 64 置位
}
/* ---------- getBit:判断第 idx 位是否为 1 ---------- */
static int getBit(const BloomFilter *bf, long long idx) {
int i = (int)idx;
return (bf->bits[i >> 6] & (1ULL << (i & 63))) != 0;
}
/* ---------- add:两个哈希位都置 1 ---------- */
void bloomAdd(BloomFilter *bf, const char *s) {
setBit(bf, hash1(bf, s));
setBit(bf, hash2(bf, s));
}
/* ---------- contains:两个哈希位都为 1 才返回真 ---------- */
int bloomContains(const BloomFilter *bf, const char *s) {
return getBit(bf, hash1(bf, s)) && getBit(bf, hash2(bf, s));
}
/* ---------- 主函数测试 ---------- */
int main(void) {
BloomFilter bf;
bloomInit(&bf, 100000); // 对应 Java 里的 new BloomFilter(100000)
char line[512];
if (!fgets(line, sizeof(line), stdin)) return 0;
int n = atoi(line);
for (int i = 0; i < n; i++) {
if (!fgets(line, sizeof(line), stdin)) break;
char op[32], s[256];
if (sscanf(line, "%31s %255s", op, s) != 2) continue;
if (strcmp(op, "add") == 0) {
bloomAdd(&bf, s);
} else if (strcmp(op, "query") == 0) {
printf("%d\n", bloomContains(&bf, s));
}
}
bloomFree(&bf);
return 0;
}
附录 C 语言实战技巧与缺陷清单
A.1 反复出现的 C 语言技巧
| 技巧 | 所在文件 | 说明 |
|---|---|---|
| 二级指针传参 | sqlist / stack / queue / doubly_linked_list / trie_class_hash | 要修改调用方的指针本身,必须传 T**。反例:singly_linked_list.c 的 freelist(Node*) 里 L=NULL 是无效语句 |
| 哨兵头节点 | 全部 6 个链式结构文件 | 消除头插/首删的空表特判,统一代码路径 |
| 头节点存 length | singly_linked_list / doubly_linked_list | 换取 O(1) 的 len(),代价是元素类型被污染 |
(long long)i*i 防溢出 |
hash_chaining / hash_open_addressing | 试除法判质数时避免 i*i 在 int 下溢出 |
(x % m + m) % m 处理负数 |
hash 系列 / trie_class_hash | C 的 % 对负数结果为负,必须做同余修正 |
%31s 限宽 |
hash / trie 系列 | sscanf 读取字符串时限制宽度,防止缓冲区溢出 |
0x3f3f3f3f 作无穷大 |
dijkstra / bellman_ford / spfa / floyd | 够大且 INF+INF 不溢出 int;memset 按 0x3f 填充正好得到它 |
1ULL 字面量 |
bloom_filter | 64 位移位运算必须用 unsigned long long 字面量避免 UB |
__builtin_popcount |
bitset | GCC/Clang 内建函数统计 1 的个数 |
| 异或交换 + 自交换特判 | priority_queue | 异或交换省临时变量,但自交换会清零,必须特判 index==parent |
strtol 双指针解析 |
priority_queue | 利用 strtol 第二参数返回结束位置,边解析边推进指针 |
scanf(" %c") 前导空格 |
linked_list_global | %c 前的空格跳过换行/空白,解析"字符+数字"混合指令流 |
| 1MB 行缓冲 | priority_queue | char line[1<<20] 应对"一行塞下 n 个数"的输入格式 |
A.2 代码中的真实缺陷清单
| 文件 | 问题 | 性质 |
|---|---|---|
list_impl/singly_linked_list.c |
deletesameelemtype 中 free(q); q=NULL; 写在 while 循环体内,导致后续访问 use-after-free |
🔴 严重 bug |
list_impl/singly_linked_list.c |
辅助数组按 L->data+1 分配,却用 abs(p->data) 作下标,数据大时越界 |
🔴 严重 bug |
list_impl/singly_linked_list.c |
deletesameelemtype 中 L->data 未同步递减 |
🟠 逻辑错误 |
list_impl/singly_linked_list.c |
freelist(Node* L) 中 L=NULL 只改形参,对调用方无效 |
🟡 无效语句 |
tree/binary_tree.c |
LevelOrder 使用未定义的 BiTree 类型(本文件只有 bitree) |
🔴 编译错误 |
array_impl/sqlist.c |
插入合法区间为 [1,length],导致空表无法在位置 1 插入 |
🟠 边界缺陷 |
array_impl/circular_queue.c |
dequeue 返回值 0 与合法元素 0 存在歧义 |
🟡 接口歧义 |
array_impl/circular_queue.c |
destroyQueue 中 if(*Q==NULL) 未先判 Q 本身是否为 NULL |
🟠 潜在崩溃 |
list_impl/linked_list_global.c |
insertAfterK 漏检 malloc 返回值;未检查 k<0;被删结点 addr[k] 成悬垂指针 |
🟠 健壮性缺陷 |
hash/hash_chaining.c |
get/remove 用 -1 表示不存在,与合法值 -1 冲突 |
🟡 接口歧义 |
hash/hash_open_addressing.c |
main 中残留未使用的 int pos 变量 |
🟡 冗余代码 |
heap/priority_queue.c |
insert 容量满时静默丢弃;poll 空堆直接 exit(1);peek 无空检查 |
🟠 健壮性缺陷 |
graph/shortest_path/floyd.c |
注释掉的打印代码使用 printf(运行时格式串) |
🟡 代码瑕疵 |
graph/dfs/dfs.c |
对称化循环在全范围执行,只写了一半有效数据 | 🟡 可读性 |
A.3 源码文件总览
| 分类 | 文件 | 行数 | 核心内容 |
|---|---|---|---|
| 线性结构 | array_impl/sqlist.c |
153 | 顺序表(动态数组) |
| 线性结构 | array_impl/stack.c |
165 | 顺序栈 |
| 线性结构 | array_impl/circular_queue.c |
133 | 循环队列(牺牲单元判满) |
| 线性结构 | array_impl/monotonic_queue.c |
39 | 单调队列(滑动窗口最大值) |
| 线性结构 | list_impl/singly_linked_list.c |
347 | 单链表(哨兵 + 长度计数) |
| 线性结构 | list_impl/doubly_linked_list.c |
248 | 双向链表 |
| 线性结构 | list_impl/linked_list_global.c |
165 | 全局地址索引单链表 |
| 线性结构 | list_impl/stack.c |
120 | 链式栈 |
| 线性结构 | list_impl/queue.c |
124 | 链式队列 |
| 哈希表 | hash/hash_chaining.c |
251 | 拉链法 + 质数扩容 |
| 哈希表 | hash/hash_open_addressing.c |
224 | 开放寻址 + 墓碑删除 |
| 堆 | heap/priority_queue.c |
138 | 大根堆(上浮/下沉) |
| 树 | tree/binary_tree.c |
97 | 二叉树(三种遍历 + 层序) |
| 树 | tree/bst.c |
118 | 二叉搜索树 |
| 树 | tree/fenwicktree.c |
50 | 树状数组(lowbit) |
| 树 | tree/segment_tree_point.c |
59 | 线段树(单点更新) |
| 树 | tree/segment_tree_lazy.c |
94 | 线段树(懒标记区间加) |
| 图 | graph/dfs/dfs.c |
103 | 深度优先遍历 |
| 图 | graph/dfs/bfs.c |
115 | 广度优先遍历 |
| 图 | graph/mst/prim.c |
144 | Prim 最小生成树(堆优化) |
| 图 | graph/mst/kruskal.c |
87 | Kruskal 最小生成树(并查集) |
| 图 | graph/shortest_path/dijkstra.c |
105 | Dijkstra(堆优化 + 懒删除) |
| 图 | graph/shortest_path/bellman_ford.c |
59 | Bellman-Ford(负环检测) |
| 图 | graph/shortest_path/spfa.c |
84 | SPFA(队列优化) |
| 图 | graph/shortest_path/floyd.c |
59 | Floyd 全源最短路 |
| Trie | trie/trie_array.c |
122 | 静态数组实现 |
| Trie | trie/trie_class_array.c |
96 | 指针 + 定长数组路 |
| Trie | trie/trie_class_hash.c |
146 | 指针 + 哈希路 |
| 位运算 | bitset/bitset.c |
106 | 位集(32 位 word) |
| 位运算 | bitset/bloom_filter.c |
96 | 布隆过滤器(双哈希) |
共 31 个 C 语言源文件 · 3847 行代码 · 8 个章节