数据结构-双向链表与多文件工程整合之makefile基础

  • Makefile文件 记录:源代码之间依赖关系、编译命令、怎么链接、目标文件名。
  • make命令 / 工具程序 执行逻辑:读取当前目录 Makefile,自动分析哪些文件需要重新编译,执行里面的指令

3. Makefile 基础语法格式

makefile

复制代码
目标: 依赖文件
	命令    # ⚠️ 前面必须是【Tab键】,不能空格!

makefile中有许多预定义变量,这些变量具有特殊的含义,可在 makefile 中直接使用。

$@ 目标名

$< 依赖文件列表中的第一个文件

$^ 依赖文件列表中除去重复文件的部分

示例:

  1. 自定义变量展开:`CC=gcc`、`SRC=main.c dlinklist.c`、`TARGET=dlinklist`、`CFLAGS= -Wall -g`,所有`$(变量名)`会被对应内容直接替换。

  2. 规则头部 `(TARGET):(SRC)` 展开为 `dlinklist:main.c dlinklist.c`,声明可执行文件依赖两份源码。

  3. 编译命令 `(CC) (SRC) -o @ (CFLAGS)` 逐个替换:`(CC)\`替换为gcc,\`(SRC)`替换源文件列表,内置变量`@\`代表当前目标dlinklist,\`(CFLAGS)`替换编译参数。

  4. 全部替换完成,最终真实执行命令:`gcc main.c dlinklist.c -o dlinklist -Wall -g`,输入make即可运行编译。

  5. 执行`make clean`,`rm $(TARGET)`展开为`rm dlinklist`,完成删除可执行文件操作。

    CC=gcc
    SRC=main.c dlinklist.c
    TARGET=dlinklist
    CFLAGS= -Wall -g

    (TARGET):(SRC)
    (CC) (SRC) -o @ (CFLAGS)

    clean:
    rm $(TARGET)

双向链表核心知识点

单向链表:只有 后继指针 next,只能向后遍历; 双向链表:每个节点拥有两个指针

复制代码
typedef struct Node
{
    data_t data;          // 数据域
    struct Node *prev;    // 前驱指针,指向前一个节点
    struct Node *next;    // 后继指针,指向后一个节点
}node_t;
  • prev:指上一个节点
  • next:指下一个节点 优势:可以向前、向后双向遍历;已知某节点,直接删除该节点不需要从头查找前驱。 缺点:每个节点多占用一个指针内存。

双向链表头结点创建

  1. 函数启动,定义指针变量 head,调用 malloc 申请一块大小与 node_t 结构体相等的堆内存,把分配到的内存地址存入 head。

  2. 判断 head 的值是否等于 NULL,如果内存申请失败,打印 malloc fail 提示语句,随后返回 NULL,结束当前函数。

  3. 若内存分配成功,将头结点内部 next 指针赋值为 NULL,再将头结点内部 prev 指针赋值为 NULL。

  4. 全部赋值操作完成,函数返回头结点指针 head。

    node_t * dlinklist_create(void)
    {
    node_t * head = malloc(sizeof(node_t));
    if(head == NULL)
    {
    printf("malloc fail\n");
    return NULL;
    }
    //头结点的上下指针均赋值为0
    head->next = NULL;
    head->prev = NULL;

    复制代码
     return head;

    }

双向链表头结点插入新节点

  1. 判断头结点指针是否为空,为空直接退出;

  2. 动态申请内存创建新节点,内存分配失败打印提示并返回;

  3. 将传入数据存入新节点的数据域;

  4. 新节点后继指针指向头结点原本的后继节点;

  5. 新节点前驱指针指向头结点;

  6. 更新头结点后继指针,使其指向新节点;

  7. 判断新节点后继不为空,链表原本存在有效结点,将原首节点的前驱指针指向新节点;

  8. 函数执行完毕,完成双向链表头插。

    void dlinklist_insert_head(node_t *head,data_t data)
    {
    if(head==NULL)
    {
    return ;
    }
    //创建新节点
    node_t *pNew = malloc(sizeof(node_t));
    if(pNew==NULL)
    {
    printf("malloc fail");
    return ;
    }

    复制代码
     //新节点写入数据
     pNew->data = data;
    
     //新节点获取头结点的next
     pNew->next = head->next;
    
     //新节点指向头结点
     pNew->prev = head;
    
     //头结点的next指向新节点成链
     head->next = pNew;
     
     //如果链表非空节点,新节点的next指向首节点地址
     if(pNew->next!=NULL)
     {
     	pNew->next->prev = pNew;
     }

    }

双向链表尾结点插入新节点

  1. 判断头结点指针是否为空,为空直接退出;

  2. 动态申请内存创建新节点;

  3. 若内存分配失败打印提示并结束函数;

  4. 将传入数据存入新节点数据域;

  5. 使用指针 p 从 head 开始向后遍历,找到链表最后一个节点;

  6. 新节点后继赋值为 NULL;新节点前驱指向找到的尾节点 p;

  7. 将尾节点 p 的后继指向新节点,完成双向链表尾插。

    void dlinklist_insert_tail(node_t *head,data_t data)
    {
    if(head==NULL)
    {
    return ;
    }
    //创建新节点
    node_t *pNew = malloc(sizeof(node_t));
    if(pNew==NULL)
    {
    printf("malloc fail\n");
    }
    //新节点填充数据
    pNew->data = data;

    复制代码
     if(pNew==NULL)
     {
     	printf("malloc fail");
     	return ;
     }
     //寻找尾节点
     node_t *p = head;
     while(p->next!=NULL)
     {
     	p = p->next;
     }
     //链接新节点
     pNew->next = p->next;
     pNew->prev = p;
     p->next = pNew;

    }

双向链表按数据内容查找地址

  1. 判断头结点指针是否为 NULL,指针非法直接返回 NULL;

  2. 判断链表是否为空链表,为空打印提示信息并返回 NULL;

  3. 遍历指针 p 指向第一个有效节点;

  4. while 循环遍历链表所有有效节点;

  5. 如果当前节点数据与目标 data 相等,直接返回当前节点地址;

  6. 未匹配成功,p 向后移动继续循环;

  7. 循环结束代表遍历完整链表无匹配项,返回 NULL。

    node_t *dlinklist_find_key(node_t *head,data_t data)
    {
    //判断链表地址合法且非空链表

    复制代码
     if(head == NULL)
     {
     	return NULL;
     }else if(is_empty(head)==0)
     {
     	printf("linklist empty\n");
     	return NULL;
     }
     //从首节点开始寻找
     node_t *p = head->next;
    
     //遍历到尾节点为止
     while(p!=NULL)
     {
     	//找到对应数据节点返回节点地址
     	if(p->data == data)
     	{
     		return p;
     	}
     	//没找到对应节点一直向后遍历
     	p = p->next;
     }
    
     //找完整个链表也没找到匹配节点则返回空地址
     return NULL;	

    }

双向链表从头结点后删除节点

  1. 判断头结点指针是否为 NULL,指针非法直接函数返回;

  2. 判断链表为空,打印提示信息并直接返回;

  3. 指针 p 保存第一个有效节点地址;

  4. 修改头结点后继指针,指向原首节点的下一个节点;

  5. 如果被删节点后方还有有效节点,修改后继节点前驱指针指向头结点;

  6. 释放被删除节点内存,完成双向链表头结点删除。

    void dlinklist_delete_head(node_t *head)
    {
    //判断链表地址合法且非空链表

    复制代码
     if(head == NULL)
     {
     	return ;
     }else if(is_empty(head)==0)
     {
     	printf("linklist empty\n");
     	return ;
     }
    
     node_t *p = head->next;
     
     head->next = p->next;
     if(p->next!=NULL)
     	p->next->prev = head;
    
     free(p);

    }

双向链表从尾节点删除节点

  1. 判断头结点指针是否为 NULL,指针非法直接函数返回;

  2. 判断链表为空,打印提示信息并直接返回;

  3. 遍历指针 p 指向第一个有效节点;

  4. 循环向后移动 p,直到 p 指向链表尾部节点;

  5. 将尾节点前驱节点的后继指向 NULL,断开尾节点链接;

  6. 释放尾节点内存,完成双向链表尾删除节点。

    void dlinklist_delete_tail(node_t *head)
    {
    //判断链表地址合法且非空链表
    if(head == NULL)
    {
    return ;
    }else if(is_empty(head)==0)
    {
    printf("linklist empty\n");
    return ;
    }

    复制代码
     node_t *p = head->next;
     
     while(p->next!=NULL)
     {
     	p = p->next;
     }
    
     p->prev->next = p->next;
     
     free(p);

    }

双向链表根据数据查找删除对应节点

  1. 判断头结点指针是否为 NULL,指针非法直接函数返回;

  2. 判断链表为空,打印提示信息并直接返回;

  3. 遍历指针 p 指向第一个有效节点;

  4. while 循环遍历链表所有有效节点;

  5. 如果当前节点数据与目标 key 相等: 将前驱节点后继指向当前节点的后继; 如果存在后继节点,更新后继节点的前驱; 释放当前节点内存,函数结束;

  6. 未匹配成功,p 向后移动继续循环;

  7. 遍历完整链表无匹配项,直接返回。

    void dlinklist_delete_key(node_t *head,data_t key)
    {
    //判断链表地址合法且非空链表

    复制代码
     if(head == NULL)
     {
     	return ;
     }else if(is_empty(head)==0)
     {
     	printf("linklist empty\n");
     	return ;
     }
     //从首节点开始寻找
     node_t *p = head->next;
    
     //遍历到尾节点为止
     while(p!=NULL)
     {
     	//找到对应数据节点返回节点地址
     	if(p->data ==  key)
     	{
     		p->prev->next = p->next;
     		if(p->next!=NULL)
     		{
     			p->next->prev = p->prev;	
     		}
     		return ;
     	}
     	//没找到对应节点一直向后遍历
     	p = p->next;
     }
     //找完整个链表也没找到匹配节点则返回空地址

    return ;
    }

双向链表销毁

  1. 判断二级指针 head 是否为 NULL,参数非法直接返回;

  2. 判断链表为空,打印提示,释放头结点内存,外部指针置空后函数返回;

  3. 遍历指针 p 指向第一个有效节点;

  4. 循环遍历所有有效节点: 使用 temp 保存当前待释放节点; p 先向后移动保存下一个节点地址; 释放 temp 指向的有效节点;

  5. 所有有效节点全部释放完成后,释放头结点;

  6. 将外部传入的链表头指针置为 NULL,消除野指针。

    void dlinklist_destroy(node_t **head)
    {
    //判断链表地址合法且非空链表

    复制代码
     if(head == NULL)
     {
     	return ;
     }else if(is_empty(*head)==0)
     {
     	printf("linklist empty\n");
     	free(*head);
     	*head = NULL;
     	return ;
     }
    
     //获取首节点
     node_t *p = (*head)->next;
     while(p!=NULL)
     {
     	//中间变量存储当前需要释放的节点
     	node_t *temp = p;
     	//后移遍历链表节点
     	p = p->next;
     	//释放当前位置节点
     	free(temp);
     }
     //释放头结点地址并将指针指向NULL防止悬空
     free(*head);
     *head = NULL;

    }

相关推荐
神明不懂浪漫3 小时前
【第五章】队列
开发语言·数据结构·经验分享·笔记·算法
Doraemomo4 小时前
数据结构-内存调试工具
数据结构
神明不懂浪漫4 小时前
【第六章】二叉树
开发语言·数据结构·经验分享·笔记·算法
Mem0rin4 小时前
前缀和:中心下标与数组乘积
数据结构·算法
Lazionr4 小时前
list容器详解——双向链表的封装与使用
数据结构·c++·链表·list
漂流瓶jz5 小时前
UVA-12174 Shuffle的播放记录 题解答案代码 算法竞赛入门经典第二版
数据结构·c++·算法·图论·滑动窗口·算法竞赛入门经典·uva
lingran__5 小时前
C++ 高阶数据结构:红黑树万字详解|完整原理推导 + 插入实现 + 完整性校验【STL 底层】
数据结构·c++·面试·红黑树·二叉搜索树·平衡二叉树·stl底层
moonsims6 小时前
低空量子无人机
前端·数据结构
love_muming6 小时前
二叉树操作全解析:从递归到层序遍历
java·数据结构·算法·二叉树