数据结构--双向链表

1. 概念

对链表而言,双向均可遍历是最方便的,另外首尾相连循环遍历也可大大增加链表操作的便捷性。因此,双向循环链表,是在实际运用中是最常见的链表形态。

2. 基本操作

与普通的链表完全一致,双向循环链表虽然指针较多,但逻辑是完全一样。基本的操作包括:

  1. 节点设计
  2. 初始化空链表
  3. 增删节点
  4. 链表遍历
  5. 销毁链表

3. 节点设计

双向链表的节点只是比单向链表多了一个前向指针。示例代码如下所示:

typedef struct node
{
    // 以整型数据为例
    int data; 

    // 指向相邻的节点的双向指针
    struct node * prev;
    struct node * next;
}node;

3. 初始化

所谓初始化,就是构建一条不含有效节点的空链表。
以带头结点的双向循环链表为例,初始化后,其状态如下图所示:

在初始空链表的情况下,链表只有一个头结点,下面是初始化示例代码:

node * initList()
{
    // 申请头结点
    node * head = malloc(sizeof(node));

    // 让首尾互相指向
    // 不需要对头结点的 data 做任何操作
    if(head != NULL)
    {
        head->prev = head;
        head->next = head;
    }

    return head;
}

4. 插入节点

与单链表类似,也可以对双链表中的任意节点进行增删操作,常见的有所谓的头插法、尾插法等,即:将新节点插入到链表的首部或者尾部,示例代码是:

  • 头插法:将新节点插入到链表的头部
// 将新节点new,插入到链表的首部
void insertHead(node *head, node *new)
{
    new->prev = head;
    new->next = head->next;

    head->next->prev = new;
    head->next = new;
}
  • 尾插法:将新节点插入到链表的尾部
// 将新节点new,插入到链表的尾部
void insertTail(node *head, node *new)
{
    new->prev = head->prev;
    new->next = head;

    head->prev->next = new;
    head->prev= new;
}

5. 剔除节点

注意,从链表中将一个节点剔除出去,并不意味着要释放节点的内容。当然,我们经常在剔除了一个节点之后,紧接着的动作往往是释放它,但是将“剔除”与“释放”两个动作分开,是最基本的函数封装的原则,因为它们虽然常常连在一起使用,但它们之间并无必然联系,例如:当我们要移动一个节点的时候,实质上就是将“剔除”和“插入”的动作连起来,此时就不能释放该节点了。

在双向链表中剔除指定节点的示例代码如下:

// 将指定节点p从链表中剔除
void remove(node *head, node *p)
{
    p->prev->next = p->next;
    p->next->prev = p->prev;

    p->prev = NULL;
    p->next = NULL;
}

6. 链表的遍历

对于双向循环链表,路径可以是向后遍历,也可以向前遍历。

下面是向前遍历、向后遍历的示例代码,假设遍历每个节点并将其整数数据输出:

// 向前遍历
void listForEachPrev(node * head)
{
    if(isEmpty(head))
        return;

    for(node * tmp=head->prev; tmp!=head; tmp=tmp->prev)
    {
        printf("%d\n", tmp->data);
    }
}

// 向后遍历
void listForEach(node * head)
{
    if(isEmpty(head))
        return;

    for(node * tmp=head->next; tmp!=head; tmp=tmp->next)
    {
        printf("%d\n", tmp->data);
    }
}

7. 销毁链表

由于链表中的各个节点被离散地分布在各个随机的内存空间,因此销毁链表必须遍历每一个节点,释放每一个节点。

注意:
销毁链表时,遍历节点要注意不能弄丢相邻节点的指针

示例代码如下:

void destroy(node * head)
{
    if(isEmpty(head))
        return;

    node *n;
    for(node *tmp = head->next; tmp!=NULL; tmp=n)
    {
        n = tmp->next;
        free(tmp);
    }
}

8. 适用场合

经过单链表、双链表的学习,可以总结链表的适用场合:

  • 适合用于节点数目不固定,动态变化较大的场合
  • 适合用于节点需要频繁插入、删除的场合
  • 适合用于对节点查找效率不十分敏感的场合

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我不吃辣。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值