常用
// 比较字符串
strcmp(s1,s2); // 0 表示相等// 快速排序
// 实现结构体排序 qsort
typedef struct{
int id;
int score;
} Student;
// 比较函数,必须返回int
// cont void *a 是通用指针,使用时必须强转
int cmp(const void* a,const void *b){
Student *s1 = (Student*)a;
Student *s2 = (Student*)b;
// 编写排序逻辑
if(s1->score != s2->score){
return s2->score - s1->score;
}else{
return s2->score - s1->score;
}
}
// qsort 调用:数组名,元素个数,每个元素大小,比较函数字符串读取的空格问题
- 现象:用
scanf("%s", str)读带有空格的句子(如 “Hello World”),只会读到 “Hello”。 - 解药:
- C语言用
gets(str)(不安全但考试通常可用) 或者fgets(str, size, stdin)。 - 注意:
fgets会把末尾的回车符\n也读进去,可能需要手动处理掉:str[strlen(str)-1] = '\0';
- C语言用
链表
查找数
// 定义链表节点
typedef struct Node{
int data;
struct Node* next;
} Node;
// 创建链表
Node* CreateList(int n){
Node* head = NULL;
Node* current = NULL;
for(int i = 0; i< n;i++){
int x;
scanf("%d", &x);
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = x;
new_node->next = NULL;
if(i == 0){
head = current = new_node;
}else {
current->next = new_node;
current = current->next;
}
}
return head;
}
// 实现寻找指定数
Node* Search(Node* head,int x){
Node* cur = head;
while(cur != NULL){
if(cur->data == x){
return cur;
}else{
if(cur->next != NULL){
cur = cur->next;
}else{
return NULL;
}
}
}
}
// 释放链表内存
void FreeList(Node* head){
Node* current = head;
while(current != NULL){
Node* temp = current;
current = current->next;
free(temp);
}
}在第 i 个位置插入数
- 没写出来,重写
二级指针的想法,将需插入位置看作一个指针地址,通过修改指针地址的内容,让其指向不同的位置,从而实现插入。
直接让 cur 指向 next 节点,就可以直接修改了。
注意
cur -> next等同于(*cur).next!可以省去一步解引用。
int Insert(Node** head, int i, int x) {
Node** cur = head; // cur 指向“指向当前节点的指针”
int count = 1;
if (i < 1) return 0; // 插入位置非法,返回失败
// 1. 寻找插入位置:移动 i-1 次
// 且要确保我们没有跳过链表末尾
while (*cur != NULL && count < i) {
cur = &((*cur)->next); // 让 cur 指向下一个节点的 next 域
count++;
}
// 2. 检查是否找到了有效位置
// 如果 count < i,说明链表太短了,没到第 i 个位置就结束了
if (count < i) return 0;
// 3. 执行插入
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) return 0; // 内存分配失败
newNode->data = x;
// 关键两步:
newNode->next = *cur; // 新节点指向当前位置的节点
*cur = newNode; // 原本指向当前位置的指针,现在指向新节点
return 1; // 插入成功
}删除节点
核心是二级指针!直接修改节点。
void Remove(Node **head, int i) {
if (i < 1) {
printf("position error\n");
return;
}
int cnt = 1;
Node **cur = head;
while (*cur != NULL) {
if (cnt == i) {
*cur = (*cur)->next;
return;
} else {
cur = &((*cur)->next);
}
cnt++;
}
printf("position error\n");
return;
}
求列表倒数n项
用双指针进行求倒数。
int Find(ListNode *head, int m, int *err) {
int cnt = 0;
*err = 0;
ListNode *slow = head;
ListNode *fast = head;
while (cnt < m && fast != NULL) {
fast = fast->next;
cnt += 1;
}
if (cnt < m) {
*err = 1;
return 0;
}
while (fast != NULL) {
slow = slow->next;
fast = fast->next;
}
return slow->data;
}栈
判断合法序列
// 栈结构体
typedef struct {
int capacity;
int top;
int *data;
} Stack;
// 初始化栈
void init_stack(Stack *stack, int size) {
stack->capacity = size;
stack->top = -1;
stack->data = (int *)malloc(size * sizeof(int));
}
// 判断是否满
int is_full(Stack *stack) { return stack->top + 1 == stack->capacity; }
// 判断是否为空
int is_empty(Stack *stack) { return stack->top == -1; }
// 入栈
int push(Stack *stack, int x) {
if (is_full(stack))
return 0;
stack->data[++stack->top] = x;
return 1;
}
// 出栈
int pop(Stack *stack) {
if (is_empty(stack))
return 0;
stack->top--;
return 1;
}
// 清空栈
void clear(Stack *stack) { stack->top = -1; }
// 销毁栈
void destroy_stack(Stack *stack) {
free(stack->data);
stack->data = NULL;
}
int is_valid_stack_sequence(const char *sequence, int max_capacity) {
int i = 0;
Stack *stack = (Stack *)malloc(sizeof(Stack));
init_stack(stack, max_capacity);
while (sequence[i] != '\0') {
if (sequence[i] == 'S') {
if (push(stack, 1)) {
i++;
} else {
return 0;
}
} else if (sequence[i] == 'X') {
if (pop(stack)) {
i++;
} else {
return 0;
}
} else {
return 0;
}
}
if (is_empty(stack)) {
destroy_stack(stack);
return 1;
} else {
destroy_stack(stack);
return 0;
}
}哈希表
线性探测
注意,当出现冲突的时候,不仅要检查 empty,还需要检查是否有重复键值对!同理,删除以后应该改为 Inactive 而不是 empty,为了防重复!
typedef enum { Empty, Active, Inactive } Status;
typedef struct {
int key;
} Record;
typedef struct {
Record data;
Status status;
} DataNode;
typedef struct {
DataNode *ht; // 散列表数组
int size; // 当前元素个数
int table_size; // 散列表长度
} HashTable;
int Hash(int key, int table_size) { return key % table_size; }
// 线性探查
int SolveColloision(int key, int count) { return 1; }
int SearchHash(HashTable *htable, int key) {
int test = Hash(key, htable->table_size);
if (htable->ht[test].status != Active || htable->ht[test].data.key == key) {
return test;
} else {
int flag = 1;
for (int i = test; i < htable->table_size; i++) {
if (htable->ht[i].status != Active || htable->ht[i].data.key == key) {
return i;
}
}
if (flag) {
for (int i = 0; i < test; i++) {
if (htable->ht[i].status != Active || htable->ht[i].data.key == key) {
return i;
}
}
}
return 0;
}
}
// 插入
void InserHash(HashTable *htable, Record x) {
int p = SearchHash(htable, x.key);
printf("%d ", p);
if (htable->ht[p].status != Active) {
htable->ht[p].data = x;
htable->ht[p].status = Active;
htable->size++;
}
}排序
希尔排序
本质是分组 - 插入排序。实现时,需要不断缩小比较的分组间隙,以达到排序的效果。注意:需要从后向前比较,从前向后循环,就可以默认前面都是有序的了。
void ShellSort(int a[], int l, int r) {
int n = r - l + 1; // 实际参与排序的元素个数
// 1. 确定增量步长 tag
for (int tag = n / 2; tag > 0; tag /= 2) {
// 2. 从第 l + tag 个元素开始,逐个向后处理
for (int i = l + tag; i <= r; i++) {
int temp = a[i];
int j;
// 3. 组内插入排序:j 是在同一组内寻找 temp 应该插入的位置
// j >= l + tag 确保 j-tag 不会越过左边界 l
for (j = i; j >= l + tag && a[j - tag] > temp; j -= tag) {
a[j] = a[j - tag]; // 将前面的大元素后移
}
a[j] = temp; // 插入到正确位置
}
}
}快速排序
核心思想是分治。选数,然后将数组分为大于该数、小于该数的两部分,接着递归处理。
void QuickSort(int a[],int l,int r){
// 递归出口
if (l >= r) return;
int i = l,j = r;
int flag = a[l];
while(i<j){
// 从右往左找第一个小于基准的
while(i<j && a[j] >= flag) j--;
if(i<j) a[i++] = a[j];
// 从左往右找第一个大于基准的
while(i<j && a[i] <= flag) i++;
if(i<j) a[j--] = a[i];
}
a[i] = flag; // 基准数回位
// 递归处理
QuickSort(a, l, i-1);
QuickSort(a, i+1, r);
}归并排序
核心是分治-递归,分成单个区间,然后两两合并。难点在理解递归。
还有非递归的步长式写法。
// from gemini
// 合并两个有序区间 [l, mid] 和 [mid+1, r]
void Merge(int a[], int l, int mid, int r) {
int n = r - l + 1;
int *temp = (int *)malloc(n * sizeof(int)); // 申请辅助空间
int i = l, j = mid + 1, k = 0;
// 两两比较,谁小谁进 temp
while (i <= mid && j <= r) {
if (a[i] <= a[j]) temp[k++] = a[i++];
else temp[k++] = a[j++];
}
// 处理剩余元素
while (i <= mid) temp[k++] = a[i++];
while (j <= r) temp[k++] = a[j++];
// 写回原数组
for (int p = 0; p < n; p++) a[l + p] = temp[p];
free(temp);
}
void MergeSort(int a[], int l, int r) {
if (l >= r) return;
int mid = l + (r - l) / 2;
MergeSort(a, l, mid); // 递归排左边
MergeSort(a, mid + 1, r); // 递归排右边
Merge(a, l, mid, r); // 合并
}插入排序
前半部分是有序的,并始终保持其有序性向后
树
遍历
根据前序序列还原二叉树
需要一个全局变量来进行记录!依次遍历字符串的中(根)、左、右,抓住根节点在最前面即可。
这里是构建而非统计,所以递归的逻辑有些不同,需要先创建当前根节点,然后再递归左右子树。
// 前序遍历的反序列化
Node* PreOrderDeSerialize(char* preorder, int n){
// 递归出口:数组为空、越界、或遇到空节点标识
if(preorder == NULL || k >= n || preorder[k] == '#'){
k++;
return NULL;
}
// 创建当前根节点
Node* root = (Node*)malloc(sizeof(Node));
if(!root) return NULL;
// 填入数据、移动下标;
root->data = preorder[k];
// 递归构造
root->left = PreOrderDeSerialize(preorder, n);
root->right = PreOrderDeSerialize(preorder, n);
return root;
}后序遍历
void PostOrder(Node* root){
if(root != NULL){
PostOrder(root->left);
PostOrder(root->right);
printf("%c ",root->data);
}
}中序遍历
非递归写法,用栈来代替递归,注意左-中-右的结构。
// 非递归中序遍历
void InOrder(Node *tree) {
Node *stack[1000];
int top = -1;
Node *cur = tree;
// 当cur不为空,或栈里还有没处理完的节点时,继续循环
while (cur != NULL || top != -1) {
// 一直到最左,所有节点入栈
while (cur != NULL) {
stack[++top] = cur;
cur = cur->left;
}
// 左边到头,弹出栈顶节点
cur = stack[top--];
printf("%c ", cur->data);
// 去右子树
cur = cur->right;
}
}根据前、中序序列还原二叉树
还是递归构建的逻辑,由于不知道空节点,所以只能找根节点 - 递归左右子树。
// 构建二叉树
BinaryTreeNode* BuildTree(char* preorder, char* inorder,int n){
// 递归出口
if(n <= 0) return NULL;
// 创建根节点
BinaryTreeNode* root = (BinaryTreeNode*)malloc(sizeof(BinaryTreeNode));
root->data = preorder[0];
// 在中序序列中找到根节点位置
int k = 0;
while(k<n && inorder[k] != preorder[0]){
k++;
}
// 递归后件左右子树
root->left = BuildTree(preorder+1, inorder, k);
root->right = BuildTree(preorder+1+k, inorder+k+1, n-k-1);
return root;
}统计
求高度
主要在于要取左右子树中的较大者。
int Height(Node* tree){
if(tree == NULL){
return 0;
}
int left = Height(tree->left);
int right = Height(tree->right);
int height = left > right ? left : right;
return height + 1;
}
// 学一下用 fgets 读取数据
int main() {
char preorder[1000];
if (fgets(preorder, sizeof(preorder), stdin) == NULL) {
// 输入为空
return 0;
}
// 去掉换行符
size_t len = strlen(preorder);
if (len > 0 && preorder[len - 1] == '\n')
preorder[len - 1] = '\n';
if (strlen(preorder) == 0){
printf("0\n");
return 0;
}
k = 0; // 重置全局索引
Node *tree = PreOrderDeSerialize(preorder, strlen(preorder));
PostOrder(tree);
printf("%d\n",Height(tree));
return 0;
}堆
1. 堆的逻辑结构与存储
堆在逻辑上是一棵完全二叉树,但在物理存储上,我们通常使用数组来表示它。
为了方便计算父子节点的下标,习惯上数组的 第 0 个位置不存数据(或存放一个哨兵值),数据从 下标 1 开始存储。
下标关系公式(核心):
对于数组中下标为 的节点:
-
它的父节点下标是: (整数除法)。
-
它的左孩子下标是:。
-
它的右孩子下标是:。
2. 最小堆的插入算法(上浮/Percolate Up)
最小堆的性质是:任意节点的值都小于或等于其左右孩子的值。这意味着根节点永远是整棵树的最小值。
当你向堆中插入一个新数字时,步骤如下:
-
放在末尾:将新元素放到数组当前的最后一个位置(即下标 )。
-
向上比较(上浮):
-
将该元素与其父节点(下标 )比较。
-
如果新元素 < 父节点,则交换它们的位置。
-
重复这个过程,直到该元素大于父节点,或者已经到达根节点(下标 1)。
-
哨兵技巧:在
h[0]存放一个极小值(如 -10001),这样在循环到根节点时,h[i] < h[i/2]自然会失效,可以省去判断i > 1。
3. 路径回溯
打印从下标 到根节点的路径。由于堆是完全二叉树,路径是唯一的。
根据我们之前的下标公式,回溯极其简单:
-
当前节点是 。
-
上一个节点是 。
-
上上个节点是 。
-
以此类推,直到下标变为 1。
示例
typedef struct{
int* data;
int size; // 实际存储
int capacity; // 容量
} MinHeap ;
// 创建最小堆
MinHeap* CreateMinHeap(int capacity){
MinHeap* h = (MinHeap*)malloc(sizeof(MinHeap));
h->capacity = capacity;
h->size = 0;
h->data = (int*)malloc((capacity+1)*sizeof(MinHeap));
return h;
}
// 上调操作
void SiftUp(MinHeap* heap,int i){
int elem = heap->data[i];
while(i > 1 && elem < heap->data[i/2]){
heap->data[i] = heap->data[i/2];
i /= 2;
}
heap->data[i] = elem;
}
// 下调操作
// 需要判断是要去左儿子还是右儿子,并持续下沉
void SiftDown(MinHeap* heap,int i){
int elem = heap->data[i];
int last = heap->size;
while(1){
int child = i*2;
if(child < last && heap->data[child + 1] < heap->data[child])
child++;
if(child > last)
break;
if(heap->data[child] < elem){
heap->data[i] = heap->data[child];
i = child;
}else{
break;
}
heap->data[i] = elem;
}
}
// 插入
void Insert(MinHeap* heap,int x){
if(heap->size == heap->capacity){
printf("错误:堆已满,无法插入。\n");
}
heap->size++;
heap->data[heap->size] = x;
SiftUp(heap,heap->size);
}
// 删除栈顶元素
// 用最后的数进行代替
int ExtractMin(MinHeap* heap){
int minKey = heap->data[1];
heap->data[1] = heap->data[heap->size];
heap->size--;
SiftDown(heap,1);
return minKey;
}
// 查询路径
void Path(MinHeap* h,int i){
while(i>=1){
printf("%d",h->data[i]);
if(i>1){
printf(" ");
}
i /= 2;
}
printf("\n");
} 背包
连续背包
物品是可以分割的,即可以拿走一部分。所以选择的标准是物品的单位价值。
- 将性价比排序,然后拿取,记录比例
注意 qsort、cmd
// 比较函数,用于按价值/重量比排序(降序)
int cmp(const void *a, const void *b) {
Object *obj1 = (Object *)a;
Object *obj2 = (Object *)b;
double r1 = obj1->v / obj1->w;
double r2 = obj2->v / obj2->w;
if (r1 < r2)
return 1;
else if (r1 > r2)
return -1;
else
return 0;
}
double Knapsack(double W, Object s[], double x[], int n){
// 按性价比排序
qsort(s, n, sizeof(Object), cmp);
double totalValue = 0.0; // 背包总价值
double currentW = W; //背包剩余容量;
for(int i = 0;i<n;i++){
// 通过idx找到物品在结果数组中的索引
int original_idx = s[i].idx;
if(s[i].w <= currentW){
x[original_idx] = 1.0;
totalValue += s[i].v;
currentW -= s[i].w;
}else{
if(currentW > 0){
x[original_idx] = currentW / s[i].w;
totalValue += x[original_idx] * s[i].v;
currentW = 0;
}else{
x[original_idx] = 0.0;
}
}
}
return totalValue;
}图
// 深搜、广搜
// DFS
void dfs(int u){
vis[u] = 1;
printf("%d ",u);
int v;
for(v=1;v<=n;v++){
if(G[u][v] == 1 && vis[v] == 0){
dfs(v);
}
}
}
// BFS
void bfs(int start){
// 用队列实现
int q[MAXN];
int front = 0,rear = 0;
q[rear++] = start; // 起点入队
vis[start] = 1;
while(front < rear){
// 出队
int u = q[front++];
printf("%d ",u);
// 遍历邻接点
int v;
for(v=1;v<=n;v++){
if(G[u][v] == 1 && vis[v] == 0){
vis[v] = 1;
q[rear++] = v;
}
}
}
}邻接矩阵的实现
删除节点的逻辑会复杂一点,要考虑清楚,用后面的节点替代前面的节点。然后是图的定义要清晰,包含边、顶点数,以及存储信息等等。
typedef struct {
int n_verts; // 顶点数
int m_edges; // 边数
int edge_matrix[MAX_VERTEX][MAX_VERTEX]; // 邻接矩阵
char *ver_list[MAX_VERTEX]; // 存储顶点信息
int no_edge_value; // 没有边时的权重
int directed; // 1为有向图,0为无向图
} MGraph;
// 初始化图
MGraph *InitGraph(int kMaxVertex, int no_edge_value, int directed) {
MGraph *graph = (MGraph *)malloc(sizeof(MGraph));
graph->n_verts = 0;
graph->m_edges = 0;
graph->no_edge_value = no_edge_value;
graph->directed = directed;
for (int i = 0; i < kMaxVertex; i++) {
graph->ver_list[i] = NULL;
for (int j = 0; j < kMaxVertex; j++) {
graph->edge_matrix[i][j] = no_edge_value;
}
}
return graph;
}
// 本题要求实现
int NumberOfEdges(MGraph *graph){
return graph->m_edges;
}
// 本题要求实现
int NumberOfVerts(MGraph *graph){
return graph->n_verts;
}
// 判断边是否存在 本题要求实现
int ExistEdge(MGraph *graph, int u, int v){
if(u < 0 || u >= graph->n_verts || v < 0 || v >= graph->n_verts) return 0;
return graph->edge_matrix[u][v] != graph->no_edge_value;
}
// 找顶点的第一个邻接点 本题要求实现
int FirstAdjVert(MGraph *graph, int v){
if(v < 0 || v >= graph->n_verts) return -1;
for(int j = 0;j<graph->n_verts;j++){
if(graph->edge_matrix[v][j] != graph->no_edge_value) return j;
}
return -1;
}
// 向图中插入边 本题要求实现
void InsertEdge(MGraph *graph, int u, int v, int weight){
if(u<0 || u>=graph->n_verts || v<0 || v>=graph->n_verts) return;
// 边已存在,不插入
if(ExistEdge(graph, u, v)) return;
graph->edge_matrix[u][v] = weight;
if(graph->directed == 0){
graph->edge_matrix[v][u] = weight;
}
graph->m_edges++;
}
// 从图中删除边 本题要求实现
void RemoveEdge(MGraph *graph, int u, int v){
if(!ExistEdge(graph, u, v)) return;
graph->edge_matrix[u][v] = graph->no_edge_value;
if(graph->directed == 0){
graph->edge_matrix[v][u] = graph->no_edge_value;
}
graph->m_edges--;
}
// 从图中删除顶点及所有邻接边 本题要求实现
void RemoveVert(MGraph *graph, int v){
if(v<0 || v>= graph->n_verts){
printf("error: %d is not exist\n", v);
return;
}
// 统计并扣除与v关联的边数
for(int i = 0;i<graph->n_verts;i++){
if(graph->edge_matrix[v][i] != graph->no_edge_value) graph->edge_matrix[v][i] = graph->m_edges--;
if(graph->directed && graph->edge_matrix[i][v] != graph->no_edge_value) graph->edge_matrix[i][v] = graph->m_edges--;
}
free(graph->ver_list[v]);
// 最后一个节点覆盖当前位置
int last = graph->n_verts - 1;
if(v!=last){
graph->ver_list[v] = graph->ver_list[last];
// 移动行,列
for(int j = 0;j<graph->n_verts;j++){
graph->edge_matrix[v][j] = graph->edge_matrix[last][j];
}
for(int i = 0;i<graph->n_verts;i++){
graph->edge_matrix[i][v] = graph->edge_matrix[i][last];
}
}
graph->ver_list[last] = NULL;
graph->n_verts--;
}
// 构建图
MGraph *BuildGraph() {
int kMaxVertex, no_edge_value, directed;
scanf("%d %d %d", &kMaxVertex, &no_edge_value, &directed);
MGraph *graph = InitGraph(kMaxVertex, no_edge_value, directed);
int n, m;
scanf("%d %d", &n, &m);
char temp[50];
for (int i = 0; i < n; i++) {
scanf("%s", temp);
graph->ver_list[i] = (char *)malloc(strlen(temp) + 1);
strcpy(graph->ver_list[i], temp);
}
graph->n_verts = n;
for (int i = 0; i < m; i++) {
int u, v, weight;
scanf("%d %d %d", &u, &v, &weight);
InsertEdge(graph, u, v, weight);
}
return graph;
}
// 打印图
void PrintGraph(MGraph *g) {
printf("adjacency matrix:\n");
for (int u = 0; u < g->n_verts; u++) {
for (int v = 0; v < g->n_verts; v++) {
printf("%d ", g->edge_matrix[u][v]);
}
printf("\n");
}
printf("vex num = %d, edge num = %d\n", NumberOfVerts(g), NumberOfEdges(g));
}Dijkstra 算法
Dijkstra 是一种求解非负权图单源最短路径的经典算法。它通过不断扩展距离源点最近的“未访问”顶点,来逐步确定到所有点的最短路。
核心步骤:
-
初始化:
-
dist[]:存储源点到各点的最短距离。初始时,源点dist[0] = 0,其余设为无穷大(INF)。 -
path[]:记录路径前驱。初始全为-1。 -
visited[]:记录顶点是否已确定最短路径。初始全为false。
-
-
找最小:在所有
visited == false的顶点中,找一个dist[i]最小的顶点 。 -
标记:将 标记为
visited = true。 -
松弛(Relaxation):遍历 的所有邻接点 。如果通过 抵达 的路径比当前的
dist[v]更短,则更新dist[v]和path[v]。
核心在逐个遍历,标记是否访问。dist[] 标记最短路径,path[] 标记上一个点。
void Dijkstra(MGraph *graph, int s, int path[], int dist[]) {
int visited[MAX_VERTEX] = {0};
int n = graph->n_verts;
// 初始化,源点距离为0,其他已初始化为INF
dist[s] = 0;
// 每次确定一个定点的最短路径,共执行n次
for (int count = 0; count < n; count++) {
// 在所有visited==0的点中,找最小dist
int u = -1;
int min_dist = INF;
for (int i = 0; i < n; i++) {
if (!visited[i] && dist[i] < min_dist) {
min_dist = dist[i];
u = i;
}
}
// 若找不到可达的点,说明剩下的点不联通
if (u == -1)
break;
visited[u] = 1;
// 渗透,继续深入更新邻居
for (int v = 0; v < n; v++) {
// 如果 u->v 有边,且v最短路还没确定
if (!visited[v] && graph->edge_matrix[u][v] != graph->no_edge_value) {
int weight = graph->edge_matrix[u][v];
int new_dist = dist[u] + weight;
// 发现更短的路
if(new_dist < dist[v]){
dist[v] = new_dist;
path[v] = u;
}
else if(new_dist == dist[v] && path[v] != -1){
if(dist[u] < dist[path[v]]){
path[v] = u;
}
}
}
}
}
}Bellman-Ford 算法
Bellman-Ford 的原理基于一个简单的观察:在一个含有 个顶点的图中,任意两点之间的最短路径(如果不含负环)最多包含 条边。
-
操作流程:对图中的所有边进行 轮“松弛”操作。
-
松弛公式:对于每一条边 权重为 ,如果
dist[u] + w < dist[v],则更新dist[v] = dist[u] + w。 -
为什么是 次?:第一轮松弛可以确定最多经过 1 条边的最短路径,第二轮确定最多经过 2 条边的最短路径……以此类推, 轮后可以确定所有点的最短路径。
int BellmanFord(MGraph *graph, int s, int dist[]) {
int n = graph->n_verts;
int no_edge = INF;
// 初始化距离数组
for (int i = 0; i < n; i++) {
dist[i] = INF;
}
dist[s] = 0;
// 进行n-1轮遍历,每一轮尝试通过现有的边来缩短距离
for (int i = 0; i < n - 1; i++) {
int updated = 0;
// 遍历邻接矩阵中所有边
for (int u = 0; u < n; u++) {
// 只有当起点u可达,才有意义
if (dist[u] == INF)
continue;
for (int v = 0; v < n; v++) {
int weight = graph->edge_matrix[u][v];
if (u != v && weight != no_edge) {
dist[v] = dist[u] + weight;
updated = 1;
}
}
}
if (!updated)
break;
}
// 负环检测,若第n轮还能更新,则说明存在负环
for (int u = 0; u < n; u++) {
if (dist[u] == INF)
continue;
for (int v = 0; v < n; v++) {
int weight = graph->edge_matrix[u][v];
if (dist[u] + weight < dist[v]) {
return 0;
}
}
}
return -1;
}Floyd 算法
Floyd 算法的本质是动态规划。它通过不断尝试引入新的“中转点”来优化路径。
- 逻辑描述:假设你想从顶点 到顶点 。
- 初始状态:你只知道 到 的直接距离(如果有边的话)。
- 演进过程:
- 允许经过顶点 中转,看看路径 是否比直接走更短。
- 再允许经过顶点 中转,看看路径是否能更短。
- …直到允许经过图中所有的顶点中转。
- 最终结果:当所有顶点都作为“中转候选人”被考虑过之后, 到 之间留下的就是全局最短路径。
void FloydWarshall(MGraph* graph, int dist[MAX_VERTEX][MAX_VERTEX], int path[MAX_VERTEX][MAX_VERTEX]){
int n = graph->n_verts;
// 初始化dist、path矩阵
for(int i =0;i<n;i++){
for(int j=0;j<n;j++){
dist[i][j] = graph->edge_matrix[i][j];
if(i!=j && graph->edge_matrix[i][j] != INF){
path[i][j] = i;
}else{
path[i][j] = -1;
}
}
}
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
// 若起点到不了中转点,跳过
if(dist[i][k] == INF) continue;
for(int j=0;j<n;j++){
if(dist[k][j] != INF && dist[i][k] + dist[k][j] < dist[i][j]){
dist[i][j] = dist[i][k] + dist[k][j];
path[i][j] = path[k][j];
}
}
}
}
}拓扑排序
就是求实现任务所必须的执行顺序,要按照次序执行。
- 入度:指指向该节点的边的条数。
- 物理意义:在拓扑排序中,一个节点的入度代表了它还有多少个“前置任务”没有完成。
- 逻辑:只有当一个节点的入度为 0 时,它才被认为是“准备就绪”的,可以被处理。
使用队列(Queue)实现的拓扑排序通常遵循以下步骤:
- 统计入度:扫描全图,记录每个节点的初始入度。
- 初始入队:将所有入度为 0 的节点放入队列。
- 循环处理:
- 从队列中弹出一个节点 。
- 计数:记录已处理的节点数量。
- “斩断”边:遍历 指向的所有邻接点 。
- 更新入度:将 的入度减 1(相当于任务 完成了, 的前置条件少了一个)。
- 触发入队:如果减 1 后 的入度变成了 0,则将 入队。
- 结果判定:
- 如果最终处理过的节点数量 等于 图中的总节点数 ,说明拓扑排序成功(图是 DAG)。
- 如果数量 小于 ,说明图中存在环路,无法完成拓扑排序。
int TopSort(Graph* graph, int* top_s){
int n = graph->n_verts;
int in_degree[KMAXVERTEX];//存储入度
int queue[KMAXVERTEX];
int front = 0,rear = 0;
int count = 0; // 记录已排序的节点数
GetInDegree(graph, in_degree);
// 初始入度为0的按序号入队
for(int i=0;i<n;i++){
if(in_degree[i] == 0){
queue[rear++] = i;
}
}
// 循环处理队列
while(front < rear){
int u = queue[front++];
top_s[count++] = u;
// 遍历 u 的所有邻接点
EdgeNode* p = graph->ver_list[u].adj;
while(p!=NULL){
int v = p->dest;
// 模拟入度减少
in_degree[v]--;
// 若入度变为0,入队
if(in_degree[v] == 0){
queue[rear++] = v;
}
p = p->next;
}
}
if(count == n){
return 1;
}else{
return 0;
}
}关键活动
求从起点到终点耗时最长的路径(决定了整个工程的最短工期)。本质是逆拓扑排序,求出必须走过的路径。
第一步:拓扑排序 + 正推求
- 建立邻接表,统计每个节点的入度。
- 将入度为 0 的节点入队列(题目要求使用队列)。
- 初始化所有 。
- BFS流程(拓扑排序):
- 从队列弹出节点 。
- 保存拓扑序列(因为后面逆推需要用到逆拓扑序,可以用一个栈或数组存下来)。
- 遍历 的所有邻接点 :
- 更新
- 的入度减 1,如果减为 0,入队。
第二步:逆向拓扑排序 + 逆推求
- 初始化所有 为汇点(终点)的 值(即 )。
- 即最早、最晚开始时间,判断是否相等.
- 最早开始时间指的是前面所有任务都完成的时间,及最大时间
- 逆序遍历刚才保存的拓扑序列(从终点向起点扫):
- 取出节点 。
- 遍历 的所有邻接点 :
- 更新
- 计算 。
- 计算 。
- 如果 ,输出该边(即为关键路径上的一段)。
// 拓扑排序
int TopSort(LGraph* graph, int* top_s) {
int n = graph->n_verts;
int in_degree[KMAXVERTEX];
GetInDegree(graph, in_degree);
int queue[KMAXVERTEX], front=0, rear=0;
for(int i=0;i<n;i++)
if(in_degree[i]==0) queue[rear++]=i;
int count_v=0;
while(front<rear){
int u=queue[front++];
top_s[count_v++]=u;
EdgeNode* p = graph->ver_list[u].adj;
while(p!=NULL){
int v=p->dest;
in_degree[v]--;
if(in_degree[v]==0) queue[rear++]=v;
p=p->next;
}
}
return count_v==n;
}
// 关键路径分析
int CriticalAnalysis(LGraph* graph) {
int n = graph->n_verts;
int earliest[KMAXVERTEX]={0}, latest[KMAXVERTEX]={0};
int top_s[KMAXVERTEX];
if(!TopSort(graph, top_s)) return 0; // 有环,无法计算
// 求最早发生时间
for(int i=0;i<n;i++){
int u=top_s[i];
EdgeNode* p = graph->ver_list[u].adj;
while(p!=NULL){
int v=p->dest, weight=p->weight;
if(earliest[v]<earliest[u]+weight)
earliest[v]=earliest[u]+weight;
p=p->next;
}
}
// 求完成时间
int completion_time=0;
for(int i=0;i<n;i++)
if(completion_time<earliest[i]) completion_time=earliest[i];
// 初始化最迟发生时间
for(int i=0;i<n;i++) latest[i]=completion_time;
// 求最迟发生时间(逆拓扑)
for(int i=n-1;i>=0;i--){
int u=top_s[i];
EdgeNode* p = graph->ver_list[u].adj;
while(p!=NULL){
int v=p->dest, weight=p->weight;
if(latest[u] > latest[v]-weight)
latest[u] = latest[v]-weight;
p=p->next;
}
}
// 输出关键活动
for(int u=0; u<n; u++){
EdgeNode* p = graph->ver_list[u].adj;
while(p!=NULL){
int v=p->dest, weight=p->weight;
if(earliest[u] == latest[v]-weight)
printf("<%d, %d>\n", u, v);
p=p->next;
}
}
return 1;
}
七桥问题-欧拉回路
一个无向图存在欧拉回路,必须同时满足以下两个条件:
条件一:所有顶点的度数必须是“偶数”
-
为什么?
-
欧拉回路要求“进”每一个节点,必须能“出”这个节点,最后还要回到起点。
-
这意味着,对于任何一个中间节点,有一条路进来(度+1),就必须有一条路出去(度再+1)。
-
进进出出,度数一定是成对出现的(2, 4, 6…)。
-
如果有任何一个节点的度数是奇数(比如 3,进去了2次,出来的只有1次,最后被困在里面了),那么就不可能形成回路。
-
条件二:所有“有边的节点”必须是连通的
-
为什么?
-
如果图分成了两块互不相连的区域(比如左边一个三角形,右边一个正方形),虽然它们的度数可能都是偶数,但你没法从左边走到右边去画完所有边。
-
_注意:如果是一个孤立的点(度为0),它不影响欧拉回路的存在(因为它没有边需要画),我们在检查连通性时通常忽略度为0的点。
-
解法:通过BFS跑一遍,看是否经过了所有节点。
-
// DFS 遍历
void DFSv(MGraphNode* graph, int v) {
graph->visited[v] = 1;
for(int u=0; u<graph->n_verts; u++){
if(graph->edge_matrix[v][u] != graph->no_edge_value && !graph->visited[u])
DFSv(graph, u);
}
}
// 判断欧拉回路
int HasEulerCircuit(MGraphNode* graph) {
// 1. 检查连通性
for(int i=0; i<graph->n_verts; i++)
graph->visited[i] = 0;
DFSv(graph, 0);
for(int i=0; i<graph->n_verts; i++)
if(!graph->visited[i])
return 0;
// 2. 检查所有顶点度是否为偶数
for(int u=0; u<graph->n_verts; u++){
int degree = 0;
for(int v=0; v<graph->n_verts; v++){
if(graph->edge_matrix[u][v] != graph->no_edge_value)
degree++;
}
if(degree % 2 != 0)
return 0;
}
return 1;
}动态规划
计算矩阵的最小相乘次数
void PrintOrder(int i, int j) {
if(i < j){
int k = p[i][j];
printf("(");
PrintOrder(i, k);
printf(")x(");
PrintOrder(k+1, j);
printf(")");
} else {
printf("M%d", i);
}
}
void OptimalMatrixOrdering(int n, int* r) {
for(int i=0;i<n;i++)
m[i][i] = 0; // 只有一个矩阵,不需要乘法
for(int length=1; length<n; length++){ // length = j-i
for(int i=0; i<n-length; i++){
int j = i + length;
m[i][j] = INT_MAX;
for(int k=i; k<j; k++){
int this_m = m[i][k] + m[k+1][j] + r[i]*r[k+1]*r[j+1];
if(this_m < m[i][j]){
m[i][j] = this_m;
p[i][j] = k;
}
}
}
}
}带权活动安排问题
typedef struct {
int start;
int finish;
int weight;
} Activity;
// 用于 qsort 的比较函数,按 finish 升序
int cmpFinish(const void* a, const void* b) {
Activity* aa = (Activity*)a;
Activity* bb = (Activity*)b;
return aa->finish - bb->finish;
}
// 返回最大值函数
int max(int a, int b) {
return a > b ? a : b;
}
// 带权活动选择
int WeightedActivitySelection(Activity* a, int n) {
qsort(a+1, n, sizeof(Activity), cmpFinish); // a[1..n] 排序
int* w = (int*)malloc((n+1)*sizeof(int));
w[0] = 0;
for(int i=1; i<=n; i++){
int k = i-1;
while(k>0){
if(a[k].finish <= a[i].start)
break;
k--;
}
w[i] = max(w[i-1], w[k] + a[i].weight);
}
int result = w[n];
free(w);
return result;
}