双向链表与通用型容器

一、双向链表基本概念

双向链表是一种常见的数据结构,每个节点包含两个指针:一个指向前一个节点(prev),一个指向后一个节点(next)。与单向链表相比,双向链表支持双向遍历,操作更为灵活,尤其适合需要频繁前后遍历的场景。

核心特点

  • 每个节点包含数据域和两个指针域
  • 支持从头到尾或从尾到头的双向遍历
  • 实际应用中常采用循环结构(首尾相连)

二、双向链表设计与实现

1. 节点设计

typedef struct node
{
    int data;               // 数据域
    struct node* prev_p;    // 前驱指针
    struct node* next_p;    // 后继指针
} node_t, *node_p;

2. 初始化空链表(头节点)

node_p DLINK_LIST_InitHeadNode(void)
{
    node_p p = malloc(sizeof(node_t));
    if (p != NULL) {
        p->prev_p = NULL;
        p->next_p = NULL;
        return p;
    }
    return NULL;
}

关键点

  • 头节点不存储实际数据
  • 初始时两个指针均指向NULL
  • 头节点用于简化链表操作

3. 初始化数据节点

node_p DLINK_LIST_InitDataNode(int data)
{
    node_p p = malloc(sizeof(node_t));
    if (p != NULL) {
        p->data = data;
        p->prev_p = NULL;
        p->next_p = NULL;
        return p;
    }
    return NULL;
}

4. 判断链表是否为空

bool DLINK_LIST_IfEmpty(node_p head_node)
{
    return (head_node->prev_p == NULL) && (head_node->next_p == NULL);
}

5. 插入操作

头插法

void DLINK_LIST_HeadInsertDataNode(node_p head_node, node_p new_node)
{
    if (DLINK_LIST_IfEmpty(head_node)) {
        new_node->prev_p = head_node;
        new_node->next_p = NULL;
        head_node->next_p = new_node;
    } else {
        new_node->prev_p = head_node;
        new_node->next_p = head_node->next_p;
        head_node->next_p->prev_p = new_node;
        head_node->next_p = new_node;
    }
}

尾插法

void DLINK_LIST_TailInsertDataNode(node_p head_node, node_p new_node)
{
    node_p tmp_p = head_node;
    while (tmp_p->next_p != NULL) {
        tmp_p = tmp_p->next_p;
    }
    
    new_node->prev_p = tmp_p;
    new_node->next_p = NULL;
    tmp_p->next_p = new_node;
}

6. 遍历链表

int DLINK_LIST_ShowListData(node_p head_node)
{
    if (DLINK_LIST_IfEmpty(head_node))
        return -1;
    
    node_p tmp_p = head_node->next_p;
    int i = 0;
    
    printf("======================链表数据======================\n");
    while (tmp_p != NULL) {
        printf("节点%d: 数据 = %d\n", i, tmp_p->data);
        tmp_p = tmp_p->next_p;
        i++;
    }
    printf("====================================================\n");
    
    return 0;
}

7. 删除操作

int DLINK_LIST_DelDataNode(node_p head_node, int data)
{
    if (DLINK_LIST_IfEmpty(head_node))
        return -1;
    
    node_p tmp_p = head_node;
    while (tmp_p->next_p != NULL) {
        if (tmp_p->next_p->data == data) {
            node_p del_node = tmp_p->next_p;
            tmp_p->next_p = del_node->next_p;
            
            if (del_node->next_p != NULL)
                del_node->next_p->prev_p = tmp_p;
            
            free(del_node);
            return 0;
        }
        tmp_p = tmp_p->next_p;
    }
    
    return -1;
}

8. 修改操作

int DLINK_LIST_ChangeNodeData(node_p head_node, int data, int change_data)
{
    if (DLINK_LIST_IfEmpty(head_node))
        return -1;
    
    node_p tmp_p = head_node->next_p;
    while (tmp_p != NULL) {
        if (tmp_p->data == data) {
            tmp_p->data = change_data;
            return 0;
        }
        tmp_p = tmp_p->next_p;
    }
    
    return -1;
}

9. 销毁链表

void DLINK_LIST_UnInit(node_p head_node)
{
    if (DLINK_LIST_IfEmpty(head_node)) {
        free(head_node);
        return;
    }
    
    node_p tmp_p = head_node->next_p;
    node_p next_p;
    
    while (tmp_p != NULL) {
        next_p = tmp_p->next_p;
        free(tmp_p);
        tmp_p = next_p;
    }
    
    free(head_node);
}

三、通用型容器设计

1. 基本概念

通用型容器是指不关心存储数据的具体类型,只关注数据间的逻辑关系和相应操作的数据结构。它提供了一套可以处理任何数据类型的通用API。

实现方式

  • 让用户提供数据类型(C++ STL方式)
  • 将数据从容器中剥离(Linux内核链表方式)

在C语言中,通常采用第二种方式,通过宏定义实现类型切换。

2. 通用型节点设计

// 通过宏定义选择数据类型
#define STU_DATA 1
#define MED_DATA 0

#if STU_DATA
    #define DATATYPE stu_t
#elif MED_DATA
    #define DATATYPE med_t
#endif

typedef DATATYPE datatype;

typedef struct node
{
    datatype data;          // 通用数据域
    struct node* prev_p;    // 前驱指针
    struct node* next_p;    // 后继指针
} node_t, *node_p;

3. 双向循环链表特点

  • 头节点的prev和next指针都指向自己
  • 判断空链表:head_node->prev_p == head_node && head_node->next_p == head_node
  • 遍历时以再次遇到头节点为结束条件

4. 通用型容器操作特点

  • 插入、删除、遍历操作与普通双向链表类似
  • 数据比较和操作需要用户自定义函数
  • 通过宏切换支持多种数据类型

四、实战应用建议

1. 学习重点

  • 理解指针操作和内存管理
  • 掌握双向链表的插入、删除原理
  • 学会通用型容器的设计思想

2. 常见错误

  • 忘记处理边界条件(空链表、首尾节点)
  • 内存泄漏(未正确释放节点)
  • 指针操作错误(造成链表断裂)

3. 调试技巧

  • 画图辅助理解指针变化
  • 使用调试器逐步跟踪指针操作
  • 添加打印语句输出链表状态

五、总结

双向链表是基础数据结构中的重要组成部分,相比单向链表提供了更大的灵活性。通用型容器设计则体现了数据结构设计的抽象思维,通过将数据与结构分离,实现了代码的复用和扩展性。

关键记忆点

  1. 双向链表每个节点有两个指针,分别指向前后节点
  2. 头节点不存储数据,用于简化操作
  3. 插入删除操作需要仔细处理指针指向
  4. 通用容器通过数据类型抽象实现代码复用
  5. 循环链表通过头节点自我指向实现循环特性

掌握双向链表和通用型容器的设计与实现,不仅能够加深对数据结构的理解,也为学习更复杂的数据结构和算法打下坚实基础。


练习题

  1. 实现一个双向循环链表,支持基本操作
  2. 使用通用型容器实现医疗挂号管理系统
  3. 比较双向链表与单向链表的性能差异
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值