一、单向链表基本操作回顾(增删改查)
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 个节点(双指针)
思路:
- p 和 q 都指向头节点
- 首先 p 先往后走 k 步
- 然后 p 和 q 同时往后走,每次一步
- 当 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 NULL3. 创建(创建空双向链表)
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 |
|---|---|
| Linux | GNU make |
| Delphi | make |
| 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 主要包含五个东西:
- 显式规则—— 说明如何生成一个或多个目标文件(明确指出要生成的文件、依赖文件、生成的命令)
- 隐晦规则—— make 有自动推导功能,可以简略书写 Makefile
- 变量定义—— 定义一系列变量(变量都是字符串,类似 C 语言的宏,执行时扩展到引用位置)
- 文件指示—— 三个部分:引用另一个 Makefile(类似 include)、按情况指定有效部分(类似 #if)、定义多行命令
- 注释—— 用
#字符(类似 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.o2. 执行过程
执行make命令时:
- 先在当前目录寻找名为
Makefile或makefile的文件 - 找到后,在当前目录下寻找第一行中的目标文件 main:
- a. 若 main 不存在或过时(main 的时间戳比所依赖文件旧),就去找 main 的依赖文件:main.o module1.o module2.o
- b. 若这些 .o 也没有或过时,跳过第二行命令,定位到第三行
- c. 若 main.o 也没有,于是执行下一行命令生成 main.o,然后定位到下一行,依次类推
- d. 直到生成了目标 main 所有的依赖文件后,回溯到第一行,此时依赖文件都已生成,于是执行第二行的编译命令,最终生成目标文件 main
- 若某个目标是最新的,则跳过它后面的命令
注意:make 只管文件的依赖性。如果找了依赖关系之后,冒号后面的文件还是不存在,make 就直接退出并报错。像
clean这种没有被第一个目标文件直接或间接关联的,不会被自动执行,需要显式执行make clean。
六、结合代码:多文件计算器项目
1. 项目结构
main.c add.c sub.c mul.c div.c add.h sub.h mul.h div.hmain.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);#endif3. 手动编译(不使用 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# 运行./mycalc4. 用 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七、预定义变量
| 变量名 | 默认值 | 含义 |
|---|---|---|
| AR | ar | 静态库文件维护程序的名称 |
| AS | as | 汇编程序的名称 |
| CC | cc | C 编译器的名称 |
| CXX | g++ | C++ 编译器的名称 |
| RM | rm -f | 文件删除程序的名称 |
| ARFLAGS | -ruv | 库文件维护程序的选项 |
| ASFLAGS | (空) | 汇编程序的选项 |
| CFLAGS | -o | C 编译器的选项 |
| CXXFLAGS | (空) | C++ 编译器的选项 |
| LDFLAGS | (空) | 链接器(如 “ld”)参数 |
八、自动变量
| 命令格式 | 含义 |
|---|---|
$* | 不包含扩展名的目标文件名 |
$+ | 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件 |
$< | 第一个依赖文件的名称 |
$? | 所有时间戳比目标文件晚的依赖文件,并以空格分开 |
$@ | 目标文件的完整名称 |
$^ | 所有不重复的依赖文件,以空格分开 |
$% | 如果目标是归档成员,则该变量表示目标的归档成员名称 |
$(@D) | 目标文件的目录部分 |
$(@F) | 目标文件的文件名(含扩展名) |
九、总结
- Makefile 三要素:目标、依赖、命令(命令必须 Tab 开头)
- 工作原理:向下找依赖树,从下往上执行命令
- make 命令:
make自动编译;make clean清理 - 内容五要素:显式规则、隐晦规则、变量定义、文件指示、注释
- 多文件工程:先
-c编译成 .o,再链接成可执行文件