☰
数据结构笔记三 —— 单向链表进阶 · 双向链表·Makefile
2026/10/20 11:18:24 网站建设 项目流程

一、单向链表基本操作回顾(增删改查)

1. 增(插入)

头插(每次插到头节点后面):

voidlinklist_insert_head(node_t*phead,data_tdata){node_t*pNew=malloc(sizeof(node_t));pNew->data=data;pNew->next=phead->next;// 新节点指向原首节点phead->next=pNew;// 头节点指向新节点}

尾插(每次插到尾节点后面):

voidlinklist_insert_tail(node_t*phead,data_tdata){node_t*pNew=malloc(sizeof(node_t));pNew->data=data;pNew->next=NULL;node_t*p=phead;while(p->next!=NULL)// 找到尾节点p=p->next;p->next=pNew;}

2. 删(删除)

头删:

intlinklist_delete_head(node_t*phead){node_t*p=phead->next;phead->next=p->next;free(p);p=NULL;return0;}

尾删:

intlinklist_delete_tail(node_t*phead){node_t*p=phead;while(p->next->next!=NULL)// 让 p 停在尾节点前一个p=p->next;free(p->next);p->next=NULL;return0;}

指定删(按 key 删):

intlinklist_delete_key(node_t*phead,intkey){node_t*p=phead;node_t*del=NULL;while(p->next!=NULL){del=p->next;if(p->next->data==key){p->next=del->next;// 断开free(del);// 释放return0;}p=p->next;}return-1;}

3. 改(先找到 然后 修改数据)

node_t*linklist_update(node_t*phead,data_told,data_tnew){node_t*p=phead->next;while(p!=NULL){if(p->data==old){p->data=new;returnp;}p=p->next;}returnNULL;}

4. 查(找)

node_t*linklist_find_key(node_t*phead,data_tkey){node_t*p=phead->next;while(p!=NULL){if(p->data==key)returnp;p=p->next;}returnNULL;}

5. 小结

单向基本操作: 创建 销毁(注意悬空指针,用二级指针 *pphead=NULL) 增:头插 / 尾插 删:头删 / 尾删 / 指定删 改:先找到,然后修改数据 查:找

二、单向链表笔试面试进阶

1. 找到链表的中间节点(快慢指针)

思路:

  • 思路1:先走到长度的一半
  • 思路2(推荐):快慢指针—— 快指针一次走两步,慢指针一次走一步

奇数个元素(head->1-2-3-4-5-NULL):快指针走到p==NULL时,慢指针在中间 3。

偶数个元素(head->1-2-3-4-5-6-NULL):快指针走到p->next==NULL时,慢指针在中偏右一个。

node_t*linklist_find_mid(node_t*phead){//1.判断有效性if(phead==NULL||is_empty(phead)==1)returnNULL;//2.创建 p 和 q 指向头节点node_t*p=phead->next;// 快指针node_t*q=phead->next;// 慢指针//3.快慢指针往后走,走到 p->next==NULL 或 p==NULL 结束while(p!=NULL&&p->next!=NULL){p=p->next->next;// 快指针走两步q=q->next;// 慢指针走一步}returnq;// 慢指针位置即中间节点}

2. 找到链表的倒数第 k 个节点(双指针)

思路:

  1. p 和 q 都指向头节点
  2. 首先 p 先往后走 k 步
  3. 然后 p 和 q 同时往后走,每次一步
  4. 当 p 走到 NULL 时,q 所在位置就是倒数第 k 个节点
node_t*linklist_find_behind_key(node_t*phead,intk){//1.判断有效if(phead==NULL||is_empty(phead)==1)returnNULL;//2.p 和 q 指向头节点node_t*p=phead;node_t*q=phead;//3.p 先往后走 k 步(防越界)inti=0;while(i<k&&p!=NULL){p=p->next;++i;}if(i<k)// k 超出链表长度returnNULL;//4.p 和 q 同时走,p 到 NULL 时 q 是倒数第 k 个while(p!=NULL){p=p->next;q=q->next;}returnq;}

3. 判断链表是否有环(快慢指针追击)

思路:快慢指针,快指针每轮比慢指针多走 1 步(相对速度差 1),有环则最终一定追上(相遇);无环则快指针先走到 NULL。

intlinklist_is_cirlc(node_t*phead){//1.判断有效if(phead==NULL||is_empty(phead)==1)return0;//2.p 和 q 指向头节点node_t*p=phead;// 快指针node_t*q=phead;// 慢指针//3.p 一次走两步,q 一次走一步//4.结束条件while(p!=NULL&&p->next!=NULL){p=p->next->next;q=q->next;if(p==q)// p==q 表示有环 → 返回 1return1;}// p!=NULL && p->next!=NULL 结束 → 返回 0(无环)return0;}

4. 链表的逆序(倒置)

原始:head -> 1 -> 2 -> 3 -> 4 -> NULL
逆序后:head -> 4 -> 3 -> 2 -> 1 -> NULL

实现思路(头插法):

1. p = phead->next; // p 此时相当于是一个无头链表的首节点 2. phead->next = NULL; // 相当于是一个空链表 3. 通过 p 进行头插: (1) q = p->next; (2) p->next = phead->next; (3) phead->next = p; (4) p = q; // p 最终走到了 NULL --- 循环结束
intlinklist_reverse(node_t*phead){//1.判断是否有效if(phead==NULL||phead->next->next==NULL||is_empty(phead)==1)return-1;// 空链表或只有一个有效节点node_t*p=phead->next;phead->next=NULL;while(p!=NULL){node_t*q=p->next;p->next=phead->next;phead->next=p;p=q;}return0;}

数据结构中的原则:一般能动指针,就不动数据。


三、单向链表排序

1. 冒泡排序

思想:相邻两个元素比较,小的放前,大的放后。

数组版:

for(i=len-1;i>1;--i)// 趟数{for(j=0;j<i;++j)// 一趟比较过程{if(a[j]>a[j+1])// 相邻两个数两两相比{交换;}}}

链表版思路:

node_t*end=NULL;// 专门一个指针记录每一趟结束的位置for(p=phead->next;p->next!=NULL;p=p->next)// 外层控制趟数{q=phead->next;// 首节点while(q->next!=end){if(q->data>q->next->data){交换;}q=q->next;}end=q;// 记录这一趟结束的位置}

2. 插入排序

思想:把链表划分为有序区和无序区,将无序区中的数据插入到有序区中。

步骤:1.拿数据 → 2.找位置 → 3.插入

// 单向链表插入排序// 1.拿数据node_t*p_temp=phead->next->next;// 指向首节点的后一个节点(无序区第一个)phead->next->next=NULL;// 把整个链表拆分为 有序区 和 无序区node_t*curr=p_temp;// 当前要插入的节点while(curr!=NULL){p_temp=p_temp->next;// 保存下一个待插入的节点// 2.找位置node_t*p=phead;// 负责动的指针// 结束条件:要么走到末尾,要么后一个值不小于要插入的值while(p->next!=NULL&&p->next->data<curr->data){p=p->next;}// 循环结束,说明此时 p 后面就是要插入的位置// 3.插入curr->next=p->next;p->next=curr;curr=p_temp;}

注意:因为单向链表方向单一的特性,要插入时,必须知道待插入位置的前一个节点。另外,p_temp整个节点要被修改,修改前应该先保存其地址,不然后面的节点就断了、找不到了。

3. 指针方式交换两个节点

一次交换:

(1)prev->next=curr->next;(2)curr->next=next->next;(3)next->next=curr;

往后走:

(4)prev=curr;(5)curr=next;(6)next=curr->next;

用好指针的两个要点:

1. 清楚 p 本身在哪里 2. p 现在指向哪里

四、笔试考点汇总

笔试 a. 中间节点 b. 倒数第 k 个 c. 判断有环 d. 逆序 e. 排序 数据结构 --- 能动指针 不动数据

五、双向链表

1. 节点结构

双向链表每个节点有三个部分:数据域 + 前驱指针(prev)+ 后继指针(next)。

typedefintdata_t;typedefstructdnode{data_tdata;// 数据域structdnode*prev;// 前驱指针:指向前一个节点structdnode*next;// 后继指针:指向下一个节点}dnode_t;

2. 特点

  • 既能向后走(next),也能向前走(prev)
  • 找前驱节点不用遍历,直接p->prev,比单链表灵活
  • 代价:每个节点多一个指针,占内存更多(空间换时间)
  • 头节点:prev为 NULL;尾节点:next为 NULL
head -> [prev][data][next] <-> [prev][data][next] <-> ... -> NULL NULL

3. 创建(创建空双向链表)

dnode_t*dlist_create(void){dnode_t*head=malloc(sizeof(dnode_t));head->prev=NULL;head->next=NULL;// 空链表:只有头节点,next 为 NULLreturnhead;}

4. 头插

intdlist_insert_head(dnode_t*head,data_tdata){dnode_t*pNew=malloc(sizeof(dnode_t));pNew->data=data;pNew->prev=head;// 1.pNew 的前驱指向头节点pNew->next=head->next;// 2.pNew 的后继指向原首节点if(head->next!=NULL)// 3.若原首节点存在,其前驱指向 pNewhead->next->prev=pNew;head->next=pNew;// 4.头节点后继指向 pNewreturn0;}

六、Makefile

一、什么是 Makefile

1. 定义

makefile 文件保存了编译器和连接器的参数选项,还表述了所有源文件之间的关系(源文件需要的包含文件、可执行文件要求的目标文件模块及库等)。

make 程序首先读取 makefile 文件,然后激活编译器、汇编器、资源编译器和连接器产生最后的输出(通常是可执行文件)。

2. 作用 —— 自动化编译

makefile 带来的好处就是**“自动化编译”**:一旦写好,只需要一个make命令,整个工程完全自动编译,极大提高软件开发效率。

3. make 是什么

make 是一个命令工具,是一个解释 makefile 中指令的命令工具:

环境对应的 make
LinuxGNU make
Delphimake
Visual C++nmake

会不会写 makefile,从一个侧面说明一个人是否具备完成大型工程的能力。


二、Makefile 基本规则(三要素)

1. 三要素:目标 / 依赖 / 命令

目标 (mycalc) 依赖 (add.c sub.c mul.c div.c) 命令 (gcc add.c sub.c mul.c div.c -o mycalc)

2. 规则格式

目标: 依赖 命令(第二行为一个 TAB 缩进)

重要:makefile 中的命令必须以 Tab 键开始,不能用空格代替。

3. 结合计算器项目示例

mycalc: add.c sub.c mul.c div.c main.c gcc add.c sub.c mul.c div.c main.c -o mycalc

三、Makefile 工作原理

1. 依赖树(向下检索、向上执行)

mycalc(终极目标) ├── main.o ← main.c ├── add.o ← add.c ├── sub.o ← sub.c ├── mul.o ← mul.c └── div.o ← div.c
  • Makefile 向下检索,构建出一颗关系依赖树(向下寻找依赖)
  • 从下向上执行命令,生成最终目标文件(向上执行命令)

2. 生成目标 / 更新目标流程

生成目标:

检查依赖条件 ├── 依赖条件存在 → 通过命令生成目标 └── 依赖条件不存在 → 寻找新规则来生成依赖条件 (依赖条件作为子目标,向下寻找并反馈至生成目标)

更新目标:

检查更新 → 检查多个依赖 └── 依赖比目标文件新 → 更新目标文件

四、Makefile 里有什么(内容五要素)

Makefile 主要包含五个东西:

  1. 显式规则—— 说明如何生成一个或多个目标文件(明确指出要生成的文件、依赖文件、生成的命令)
  2. 隐晦规则—— make 有自动推导功能,可以简略书写 Makefile
  3. 变量定义—— 定义一系列变量(变量都是字符串,类似 C 语言的宏,执行时扩展到引用位置)
  4. 文件指示—— 三个部分:引用另一个 Makefile(类似 include)、按情况指定有效部分(类似 #if)、定义多行命令
  5. 注释—— 用#字符(类似 C 的//),要写#可用\#转义

五、make 如何解释执行 makefile

1. 简单 makefile 示例

main: main.o module1.o module2.o gcc main.o module1.o module2.o -o main main.o: main.c head1.h head2.h com_head.h gcc -c main.c -o main.o module1.o: module1.c head1.h gcc -c module1.c -o module1.o module2.o: module2.c head2.h gcc -c module2.c -o module2.o

2. 执行过程

执行make命令时:

  1. 先在当前目录寻找名为Makefile或makefile的文件
  2. 找到后,在当前目录下寻找第一行中的目标文件 main:
    • a. 若 main 不存在或过时(main 的时间戳比所依赖文件旧),就去找 main 的依赖文件:main.o module1.o module2.o
    • b. 若这些 .o 也没有或过时,跳过第二行命令,定位到第三行
    • c. 若 main.o 也没有,于是执行下一行命令生成 main.o,然后定位到下一行,依次类推
    • d. 直到生成了目标 main 所有的依赖文件后,回溯到第一行,此时依赖文件都已生成,于是执行第二行的编译命令,最终生成目标文件 main
  3. 若某个目标是最新的,则跳过它后面的命令

注意:make 只管文件的依赖性。如果找了依赖关系之后,冒号后面的文件还是不存在,make 就直接退出并报错。像clean这种没有被第一个目标文件直接或间接关联的,不会被自动执行,需要显式执行make clean。


六、结合代码:多文件计算器项目

1. 项目结构

main.c add.c sub.c mul.c div.c add.h sub.h mul.h div.h

main.c 调用 add/sub/mul/div 四个函数实现加减乘除计算器。

2. 各源文件代码

main.c:

#include<stdio.h>#include"add.h"#include"sub.h"#include"mul.h"#include"div.h"intmain(intargc,constchar*argv[]){inta=10;intb=20;printf("add=%d ",add(a,b));printf("sub=%d ",sub(a,b));printf("mul=%d ",mul(a,b));printf("div=%d ",div(a,b));return0;}

add.c:

#include"add.h"intadd(inta,intb){intsum=0;sum=a+b;returnsum;//return a + b;}

sub.c:

#include"sub.h"intsub(inta,intb){returna-b;}

mul.c:

#include"mul.h"intmul(inta,intb){returna*b;}

div.c:

#include"div.h"intdiv(inta,intb){returna/b;}

add.h(其余头文件结构相同):

#ifndef_ADD_H_#define_ADD_H_intadd(inta,intb);#endif

3. 手动编译(不使用 Makefile)

# 先编译每个 .c 生成 .ogcc-cmain.c-omain.o gcc-cadd.c-oadd.o gcc-csub.c-osub.o gcc-cmul.c-omul.o gcc-cdiv.c-odiv.o# 再链接生成可执行文件gcc main.o add.o sub.o mul.o div.o-omycalc# 运行./mycalc

4. 用 Makefile 管理编译

mycalc: add.o sub.o mul.o div.o main.o gcc add.o sub.o mul.o div.o main.o -o mycalc main.o: main.c add.h sub.h mul.h div.h gcc -c main.c -o main.o add.o: add.c add.h gcc -c add.c -o add.o sub.o: sub.c sub.h gcc -c sub.c -o sub.o mul.o: mul.c mul.h gcc -c mul.c -o mul.o div.o: div.c div.h gcc -c div.c -o div.o clean: rm -f *.o mycalc

使用时:

make# 编译整个工程makeclean# 清除目标文件和可执行文件

5. 用变量和自动变量优化

CC = gcc CFLAGS = -Wall -g mycalc: main.o add.o sub.o mul.o div.o $(CC) $^ -o $@ main.o: main.c add.h sub.h mul.h div.h $(CC) $(CFLAGS) -c $< -o $@ add.o: add.c add.h $(CC) $(CFLAGS) -c $< -o $@ sub.o: sub.c sub.h $(CC) $(CFLAGS) -c $< -o $@ mul.o: mul.c mul.h $(CC) $(CFLAGS) -c $< -o $@ div.o: div.c div.h $(CC) $(CFLAGS) -c $< -o $@ clean: $(RM) *.o mycalc

七、预定义变量

变量名默认值含义
ARar静态库文件维护程序的名称
ASas汇编程序的名称
CCccC 编译器的名称
CXXg++C++ 编译器的名称
RMrm -f文件删除程序的名称
ARFLAGS-ruv库文件维护程序的选项
ASFLAGS(空)汇编程序的选项
CFLAGS-oC 编译器的选项
CXXFLAGS(空)C++ 编译器的选项
LDFLAGS(空)链接器(如 “ld”)参数

八、自动变量

命令格式含义
$*不包含扩展名的目标文件名
$+所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
$<第一个依赖文件的名称
$?所有时间戳比目标文件晚的依赖文件,并以空格分开
$@目标文件的完整名称
$^所有不重复的依赖文件,以空格分开
$%如果目标是归档成员,则该变量表示目标的归档成员名称
$(@D)目标文件的目录部分
$(@F)目标文件的文件名(含扩展名)

九、总结

  • Makefile 三要素:目标、依赖、命令(命令必须 Tab 开头)
  • 工作原理:向下找依赖树,从下往上执行命令
  • make 命令:make自动编译;make clean清理
  • 内容五要素:显式规则、隐晦规则、变量定义、文件指示、注释
  • 多文件工程:先-c编译成 .o,再链接成可执行文件

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询