数据结构知识点和代码实现总结(C语言实现)

文章目录

  • 数据结构知识点与代码实现总结
    • 目录
    • [第一章 线性结构](#第一章 线性结构)
    • [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]] 取值,又能用下标判断是否滑出窗口。

算法流程(每轮严格三步):

  1. 维护单调性 :while (front <= rear && a[i] >= a[q[rear]]) rear--;------用 >= 把相等元素也弹出
  2. 当前下标入队 :q[++rear] = i;------这行必须在 while 之外(源码注释特意强调)
  3. 移除滑出窗口的队头 :while (q[front] <= i - k) front++;
  4. 窗口成形后输出 :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。每结点存一个区间和。

区间查询三步式:

  1. 完全覆盖 → 直接返回 tree[node]
  2. 无交集 → 返回幺元(加法单位元 0)
  3. 部分覆盖 → 下传递归左右孩子

实现要点:

  • 四参数递归签名 (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 三要素:

  1. 更新孩子的 tree(乘以区间长度)
  2. 叠加孩子的 lazy(用 += 而非 =)
  3. 清空自己的 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]。

算法三步:

  1. 按权值 qsort 升序
  2. 依次遍历每条边,Union(from,to) 成功才入选
  3. 选够 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 为 0
  • freeNode 是递归后序释放整棵子树,是 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 个章节

相关推荐
小灰灰搞电子2 小时前
C语言 <time.h> 头文件详解
c语言·time.h
C++ 老炮儿的技术栈3 小时前
sizeof操作符
c语言·c++·人工智能·mfc·c
wuminyu4 小时前
ForkJoinPool内部WorkQueue的Lock-Free数组操作以及并发任务窃取原理剖析
java·linux·c语言·jvm·c++
code2cat4 小时前
Java进阶篇之AtomicMarkableReference:引用还在,标记已经改变
java·开发语言·cas·并发编程·逻辑删除
CappuccinoRose4 小时前
FormData数据处理
开发语言·前端·javascript·表单数据
她说彩礼65万4 小时前
C语言 堆区和栈区
java·linux·c语言
Zguigo4 小时前
【CUDA8】第一个CUDA C++ Kernel --vector add
java·开发语言·c++
-Marks-5 小时前
【C++编程】STL容器(二)--- vector底层模拟实现(常用接口实现 | 扩容机制 | 深浅拷贝 | 迭代器失效)
开发语言·c++·vector·内存管理·迭代器失效·vector底层模拟实现
Shadow(⊙o⊙)5 小时前
C++11并发支持库
开发语言·c++