嵌入式培训之数据结构学习(四)双向链表、makefile

发布于:2025-05-16 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

一、双向链表

二、双向链表的基本操作

1、创建双向链表

2、头插法插入节点

3、遍历打印链表

4、判断双链表是否为空

5、获取双链表长度

6、尾插法插入节点

7、指定位置插入节点

8、查找(按姓名查找双链表节点)

9、修改链表的指定节点

三、扩展:工程管理工具(makefile)

一、双向链表

双向链表示意图

指针永远指向内存开始的区域

二、双向链表的基本操作

1、创建双向链表

struct DouLinkList* CreateDouLinkList()

 // 定义函数,返回双向链表结构体指针,用于创建双向链表头节点
{
        struct DouLinkList* dl = (struct DouLinkList*) malloc(sizeof(struct         DouLinkList));  // 为双向链表头节点分配内存
        if(NULL == dl)  // 检查内存分配是否失败
        {
        fprintf(stderr,"CreateDouLinkList malloc\n");  // 向标准错误输出内存分配失败信息
        return NULL;  // 分配失败,返回空指针
        }
        dl->head = NULL;  // 初始化头节点的head指针为NULL,表示空链表
        dl->clen = 0;  // 初始化链表长度计数器为0
        return dl;  // 返回创建好的双向链表头节点指针
}

2、头插法插入节点

(1)图解:

第一种情况:第一个插入

第二种情况:

(2)算法实现:

int InsertHeadDouLinkList(struct DouLinkList* dl, struct DATATYPE* data)

 // 定义头部插入函数,参数为链表指针和数据指针,返回操作状态(0成功,非0失败)
{
        struct DouNode * newnode = (struct DouNode*)malloc(sizeof(struct  DouNode));  // 为新节点分配内存
        if(NULL == newnode)  // 检查新节点内存分配是否失败
        {
                fprintf(stderr,"inserthead malloc\n");  // 输出插入节点时内存分配失败的错误信息
                return 1;  // 分配失败,返回错误码1
        }
        memcpy(&newnode->data,data,sizeof(struct DATATYPE));  // 将数据拷贝到新节点的数据域
        newnode->next = NULL;  // 初始化新节点的后继指针为NULL
        newnode->prev = NULL;  // 初始化新节点的前驱指针为NULL
 
        newnode->next = dl->head;  // 新节点的后继指向原链表头节点
        if(dl->head)  // 若原链表非空(存在头节点)
        {
                dl->head->prev = newnode;  // 原头节点的前驱指向新节点
        }
        dl->head = newnode;  // 更新链表头节点为新节点
        dl->clen++;  // 链表长度加1
        return 0;  // 插入成功,返回0
}

3、遍历打印链表

int ShowDouLinkList(struct DouLinkList* dl,DIR dir)  

// 定义遍历函数,参数为链表指针和遍历方向枚举值,返回操作状态(0成功)
{
        struct DouNode* tmp = dl->head;  // 定义临时指针tmp,初始指向链表头节点
        if(FORWADR == dir)  // 若遍历方向为正向(假设FORWADR为正向枚举值)
        {
                while(tmp)  // 正向遍历,直到tmp指向NULL(链表末尾)
                {
                        // 输出节点数据(假设data包含姓名、性别、年龄、分数字段)
                        printf("%s %c %d %d\n",tmp->data.name,tmp->data.sex,tmp->data.age,tmp->data.score);
                        tmp = tmp->next;  // tmp后移至下一个节点
                }
        }
        else if(BACKWADR == dir)  // 若遍历方向为反向(假设BACKWADR为反向枚举值)
        {
                while(tmp->next)  // 先移动到链表尾节点(通过判断next是否为NULL)
                {
                        tmp = tmp->next;
                } // 循环结束后tmp指向尾节点
 
                while(tmp)  // 从尾节点开始反向遍历,直到tmp指向NULL(头节点的前驱)
                {
                        // 输出节点数据(与正向相同)
                        printf("%s %c %d %d\n",tmp->data.name,tmp->data.sex,tmp->data.age,tmp->data.score);
                        tmp = tmp->prev;  // tmp前移至上一个节点
                }
        }
        return 0;  // 遍历完成,返回0
}

4、判断双链表是否为空

int IsEmptyDouLinkList(struct DouLinkList* dl) 
{
    return 0 == dl->clen; // 若链表长度(clen)为0,返回1(真),否则返回0(假)
}

5、获取双链表长度

int GetSizeDouLinkList(struct DouLinkList* dl) 
{
    return dl->clen; // 直接返回链表长度(clen)
}

6、尾插法插入节点

(1)图解

第一种情况(与头插第一种情况类似)

第二种:

(2)算法实现

int InserTailDouLinkList(struct DouLinkList* dl, struct DATATYPE* data)
{
    if (IsEmptyDouLinkList(dl)) // 检查链表是否为空
        {
            return InsertHeadDouLinkList(dl, data); // 空链表时调用头插法插入
        }
    else
        {
            struct DouNode* tmp = dl->head; // 临时指针指向头节点
            while (tmp->next) // 遍历到链表最后一个节点(tmp->next为空时停止)
                {
                    tmp = tmp->next; // 后移指针
                }
            struct DouNode* newnode = malloc(sizeof(struct DouNode)); // 分配新节点内存
            if (newnode == NULL) // 内存分配失败
                {
                    fprintf(stderr, "InserTailDouLinkList malloc\n"); // 输出错误信息到标准错误
                    return 1; // 返回错误码1
                }
            // 初始化新节点
            memcpy(&newnode->data, data, sizeof(struct DATATYPE)); // 复制数据到新节点
            newnode->next = NULL; // 新节点的next指针指向NULL(尾节点特性)
            newnode->prev = NULL; // 初始化为NULL(后续会重新指向前驱节点)

            // 连接链表
            newnode->prev = tmp; // 新节点的前驱指向最后一个节点tmp
            tmp->next = newnode; // tmp的后继指向新节点

            dl->clen++; // 链表长度加1
        }
    return 0; // 插入成功,返回0
}

7、指定位置插入节点

(1)图解:

(2)算法实现:

int InserPosDouLinkList(struct DouLinkList* dl, struct DATATYPE* data, int pos)
{
    int len = GetSizeDouLinkList(dl); // 获取当前链表长度
    if (pos < 0 || pos > len) // 检查位置是否合法(0 ≤ pos ≤ len)
        {
            return 1; // 非法位置,返回错误码1
        }
    if (0 == pos) // 插入位置为头部(pos=0)
        {
            return InsertHeadDouLinkList(dl, data); // 调用头插法插入
        }
    else if (pos == len) // 插入位置为尾部(pos=链表长度)
        {
            return InserTailDouLinkList(dl, data); // 调用尾插法插入
        }
    else // 插入位置在中间(1 ≤ pos ≤ len-1)
        {
            struct DouNode* newnode = malloc(sizeof(struct DouNode)); // 分配新节点内存
            if (newnode == NULL) // 内存分配失败
                {
                    fprintf(stderr, "InserTailDouLinkList malloc\n"); // 输出错误信息
                    return 1; // 返回错误码1
                }
            // 初始化新节点
            memcpy(&newnode->data, data, sizeof(struct DATATYPE)); // 复制数据到新节点
            newnode->next = NULL; // 初始化为NULL(后续会重新指向后继节点)
            newnode->prev = NULL; // 初始化为NULL(后续会重新指向前驱节点)

            struct DouNode* tmp = dl->head; // 临时指针指向头节点
            int i;
            for (i = 0; i < pos - 1; ++i) // 遍历到插入位置的前一个节点(pos-1处)
                {
                    tmp = tmp->next; // 后移指针
                }
                // 双向链表指针重连
                newnode->next = tmp->next; // 新节点的后继指向tmp的原后继
                newnode->prev = tmp; // 新节点的前驱指向tmp
                tmp->next->prev = newnode; // tmp原后继的前驱指向新节点
                tmp->next = newnode; // tmp的后继指向新节点
        }
    return 0; // 插入成功,返回0
}

8、查找(按姓名查找双链表节点)

(1)算法实现:

struct DouNode* FindDouLinkList(struct DouLinkList* dl, char *name) 
{

     if(IsEmptyDouLinkList(dl))
    {
        return NULL;
    }
    struct DouNode* tmp = dl->head; // 定义临时指针tmp,指向链表头节点
    while (tmp) // 遍历链表,直到tmp为NULL(指向空节点时停止)
    {
        if (strcmp(tmp->data.name, name) == 0) // 比较当前节点data中的name字段与目标name
        {
            return tmp; // 若匹配成功,返回当前节点指针
        }
        tmp = tmp->next; // 移动指针到下一个节点
    }
    return NULL; // 遍历结束未找到匹配节点,返回NULL
}

(2)调用:

9、修改链表的指定节点

int ModifyDouLinkList(struct DouLinkList* dl, char *name, struct DATATYPE* data)
{
    // 调用FindDouLinkList函数,根据name在双链表dl中查找节点
    // 将找到的节点指针赋给ret,如果没找到返回NULL
    struct DouNode* ret = FindDouLinkList(dl, name); 
    if (NULL == ret) // 判断是否找到节点
    {
        return 1; // 没找到节点,返回1表示修改失败
    }
    // 使用memcpy函数将data所指向的数据内容,复制到找到节点(ret)的数据部分
    // 复制的长度为struct DATATYPE的大小
    memcpy(&ret->data, data, sizeof(struct DATATYPE)); 
    return 0; // 成功修改节点数据,返回0表示操作成功
}

10、逆序双向链表

int RevertDouLinkList(struct DouLinkList* dl) {
    // 获取链表长度
    int len = GetSizeDouLinkList(dl);
    // 长度小于2无需反转
    if (len < 2) {
        return 1;
    }
    // 定义前驱指针(初始NULL)
    struct DouNode* Prev = NULL;
    // 当前节点指针(从头节点开始)
    struct DouNode* Tmp = dl->head;
    // 下一节点指针(保存当前节点的后继)
    struct DouNode* Next = Tmp->next;
    // 循环反转指针
    while (1) {
        // 当前节点的前驱指向原后继
        Tmp->prev = Next;
        // 当前节点的后继指向原前驱
        Tmp->next = Prev;
        // 前驱指针后移到当前节点
        Prev = Tmp;
        // 当前节点后移到原后继
        Tmp = Next;
        // 若当前节点为空,结束循环
        if (NULL == Tmp) {
            break;
        }
        // 下一节点后移(提前保存原后继的后继)
        Next = Next->next;
    }
    // 头指针指向新的头节点(原尾节点)
    dl->head = Prev;
    // 返回成功标识
    return 0;
}

11、删除链表指定节点

int DeleteDouLinkList(struct DouLinkList* dl, char* name) {
    // 查找指定name的节点
    struct DouNode* tmp = FindDouLinkList(dl, name);
    // 若节点不存在,返回失败
    if (NULL == tmp) {
        return 1;
    }
    // 处理头节点情况
    if (tmp == dl->head) {  
        // 头指针后移
        dl->head = dl->head->next;
        // 若新头节点存在,更新其前驱为NULL
        if (dl->head) {
            dl->head->prev = NULL;
        }
    }
    // 处理尾节点情况(next为空)
    else if (NULL == tmp->next) {  
        // 若当前节点有前驱,断开前驱与当前节点的连接
        if (tmp->prev) {
            tmp->prev->next = NULL;
        }
        // 若无前驱(说明只剩当前节点),置头指针为NULL
        else {
            dl->head = NULL;
        }
    }
    // 处理中间节点情况
    else {
        // 若存在后继节点,更新其后继的前驱指针
        if (tmp->next) tmp->next->prev = tmp->prev;
        // 若存在前驱节点,更新其前驱的后继指针
        if (tmp->prev) tmp->prev->next = tmp->next;
    }
    // 释放目标节点内存
    free(tmp);
    // 链表长度减一
    dl->clen--;
    // 返回成功标识
    return 0;
}

12、销毁链表

int DestroyDouLinkList(struct DouLinkList* dl) {
    // 获取头节点指针
    struct DouNode* tmp = dl->head;
    // 循环释放所有节点
    while (tmp) {
        // 头指针后移到下一个节点
        dl->head = dl->head->next;
        // 释放当前节点内存
        free(tmp);
        // tmp指向新的头节点(继续循环释放)
        tmp = dl->head;
    }
    // 释放链表头结构自身内存
    free(dl);
    // 返回成功标识
    return 0;
}

调用:

三、扩展:工程管理工具(makefile)

1、当三个以上的.c编译时:

(1)vi Makefile(makefile也可,推荐用首字母大写),进入下面的页面进行编写(也可在资源管理器中打开:

出现羊头标志即可)

        版本一:

1、 a.out: main.c ./doulink.c   // 规则1 ,生成可执行文件,其中./a.out为目标,冒号后的为依赖(输入结束回车)

2、        gcc main.c doulink.c  //前面必须为Tab键空格

3、

4、clean:   //可删除目标文件

5、        rm  a.out

        版本二:(推荐使用)

        版本二示例:

(2)make   (编译命令,默认只走第一条规则)

(3)./app (运行,版本一运行命令为 ./a.out)

2、扩展命令:make clean  (删除中间产生的文件)。


网站公告

今日签到

点亮在社区的每一天
去签到