之前接触到的链表都只有一个指针,指向直接后继,整个链表只能单方向从表头访问到表尾,这种结构的链表统称为 “单向链表”或“单链表”。

如果算法中需要频繁地找某结点的前趋结点,单链表的解决方式是遍历整个链表,增加算法的时间复杂度,影响整体效率。
为了快速便捷地解决这类问题,在单向链表的基础上,给各个结点额外配备一个指针变量,用于指向每个结点的直接前趋元素。这样的链表被称为“双向链表”或者“双链表”。

双链表中的结点

双向链表中的结点有两个指针域,一个指向直接前趋,一个指向直接后继。(链表中第一个结点的前趋结点为NULL,最后一个结点的后继结点为NULL)

图1 双向链表中的结点

结点的具体构成:

typedef struct line
{
  struct line *prior;   //指向直接前趋
  int data;
  struct line *next;    //指向直接后继
}line;

创建双向链表并初始化

双向链表创建的过程中,每一个结点需要初始化数据域和两个指针域,一个指向直接前趋结点,另一个指向直接后继结点。

例如,创建一个双向链表line(1,2,3):

图2 双向表(1,2,3)

实现代码:

line* initLine(line *head)
{
  head = (line*)malloc(sizeof(line));  //创建链表第一个结点(首元结点)
  head->prior = NULL;
  head->next = NULL;
  head->data = ;
  line *list = head;
  for (int i=; i<=; i++)
  {
    //创建并初始化一个新结点
    line *body = (line*)malloc(sizeof(line));
    body->prior = NULL;
    body->next = NULL;
    body->data = i;
    list->next = body;//直接前趋结点的next指针指向新结点
    body->prior = list;//新结点指向直接前趋结点
    list = list->next;
  }
  return head;
}

双向链表中插入结点

比如在(1,2,3)中插入一个结点 4,变成(1,4,2,3)。

实现效果图:

图3 插入结点4
在双向链表中插入数据时,首先完成图 3 中标注为 1 的两步操作,然后完成标注为 2 的两步操作;反之,如果先完成 2,就无法通过头指针访问结点 2,需要额外增设指针,虽然能实现,但较前一种麻烦。

实现代码:

line *insertLine(line * head, int data, int add)
{
  //新建数据域为data的结点
  line * temp = (line*)malloc(sizeof(line));
  temp->data = data;
  temp->prior = NULL;
  temp->next = NULL;
  //插入到链表头,要特殊考虑
  if (add == )
  {
    temp->next = head;
    head->prior = temp;
    head = temp;
  }
  else
  {
    line *body = head;
    //找到要插入位置的前一个结点
    for (int i=; i<add-; i++)
    {
      body = body->next;
    }
    //判断条件为真,说明插入位置为链表尾
    if (body->next == NULL)
    {
      body->next = temp;
      temp->prior=body;
    }
    else
    {
      body->next->prior = temp;
      temp->next = body->next;
      body->next = temp;
      temp->prior = body;
    }
  }
  return head;
}

双向链表中删除节点

双链表删除结点时,直接遍历链表,找到要删除的结点,然后利用该结点的两个指针域完成删除操作。

例如,在(1,4,2,3)中删除结点 2:

//删除结点的函数,data为要删除结点的数据域的值
line *delLine(line *head, int data)
{
  line *temp = head;
  //遍历链表
  while (temp)
  {
    //判断当前结点中数据域和data是否相等,若相等,摘除该结点
    if (temp->data == data)
    {
      temp->prior->next = temp->next;
      temp->next->prior = temp->prior;
      free(temp);
      return head;
    }
    temp = temp->next;
  }
  printf("链表中无该数据元素");
  return head;
}

双向链表中的查找和更改操作

双向链表的查找操作和单链表的实现方法完全一样,从链表的头结点或者首元结点开始遍历,这里不做过多解释。

更改链表中某结点的数据域的操作是在查找的基础上完成的。通过遍历找到存储有该数据元素的结点后,直接更改其数据域就可以。

本节的完整代码

#include <stdio.h>
#include <stdlib.h>
typedef struct line
{
  struct line *prior;
  int data;
  struct line *next;
}line;
line *initLine(line *head);
line *insertLine(line *head, int data, int add);
line *delLine(line *head, int data);
void display(line *head);
int main()
{
  line *head = NULL;
  head=initLine(head);
  head=insertLine(head, , );
  display(head);
  head=delLine(head, );
  display(head);
  return ;
}
line *initLine(line * head)
{
  head = (line*)malloc(sizeof(line));
  head->prior = NULL;
  head->next = NULL;
  head->data = ;
  line *list = head;
  for (int i=; i<=; i++)
  {
    line *body = (line*)malloc(sizeof(line));
    body->prior = NULL;
    body->next = NULL;
    body->data = i;
    list->next = body;
    body->prior = list;
    list = list->next;
  }
  return head;
}
line *insertLine(line *head, int data, int add)
{
  //新建数据域为data的结点
  line *temp = (line*)malloc(sizeof(line));
  temp->data = data;
  temp->prior = NULL;
  temp->next = NULL;
  //插入到链表头,要特殊考虑
  if (add == )
  {
    temp->next = head;
    head->prior = temp;
    head = temp;
  }
  else
  {
    line *body = head;
    //找到要插入位置的前一个结点
    for (int i=; i<add-; i++)
    {
      body = body->next;
    }
    //判断条件为真,说明插入位置为链表尾
    if (body->next == NULL)
    {
      body->next = temp;
      temp->prior = body;
    }
    else
    {
      body->next->prior = temp;
      temp->next = body->next;
      body->next = temp;
      temp->prior = body;
    }
  }
  return head;
}
line *delLine(line *head, int data)
{
  line *temp = head;
  //遍历链表
  while (temp)
  {
    //判断当前结点中数据域和data是否相等,若相等,摘除该结点
    if (temp->data == data)
    {
      temp->prior->next = temp->next;
      temp->next->prior = temp->prior;
      free(temp);
      return head;
    }
    temp = temp->next;
  }
  printf("链表中无该数据元素");
  return head;
}
//输出链表的功能函数
void display(line *head)
{
  line *temp = head;
  while (temp)
  {
    if (temp->next == NULL)
    {
      printf("%d\n",temp->data);
    }
    else
    {
      printf("%d->",temp->data);
    }
    temp=temp->next;
  }
} 总结
双向链表和单链表唯一的不同在于结构中多了一个指向直接前趋的指针,其他完全一样。如果问题中需要频繁的调取当前结点的前趋结点,那使用双向链表的数据结构为最佳方案。

补:双向链表和循环链表的结合体

约瑟夫环问题其实还可以这样玩:如果顺时针报数,有人出列后,顺时针找出出列位置的下一个人,开始反方向(也就是逆时针)报数,有人出列后,逆时针找出出列位置的下一个人,开始顺时针报数。依次重复,直至最后一个出列。
例如,还是从编号为 3 的开始数,数到 2 的人出列:

图4 约瑟夫环

新玩法的出列顺序为:
首先顺时针转,4 数 2,所以 4 出列;
顺时针找到下一个人为 5,开始逆时针转,3 数 2,所以 3 出列;
逆时针找到下一个人为 2,开始顺时针转,5 数 2,所以 5 出列;
顺时针找到下一个人为 1,开始逆时针转,2 数 2,所以 2 出列;
最后只剩下 1,所以 1 自己出列。

对于新的约瑟夫环问题,需要将循环链表和双向链表结合使用,组成:双向循环链表。

有兴趣的可以尝试编码解决新的约瑟夫环问题。

数据结构8: 双向链表(双向循环链表)的建立及C语言实现的更多相关文章

  1. 【C语言教程】“双向循环链表”学习总结和C语言代码实现!

    双向循环链表 定义 双向循环链表和它名字的表意一样,就是把双向链表的两头连接,使其成为了一个环状链表.只需要将表中最后一个节点的next指针指向头节点,头节点的prior指针指向尾节点,链表就能成环儿 ...

  2. JS数据结构第三篇---双向链表和循环链表之约瑟夫问题

    一.双向链表 在上文<JS数据结构第二篇---链表>中描述的是单向链表.单向链表是指每个节点都存有指向下一个节点的地址,双向链表则是在单向链表的基础上,给每个节点增加一个指向上一个节点的地 ...

  3. 双向链表、双向循环链表的JS实现

    关于链表简介.单链表.单向循环链表.JS中的使用以及扩充方法:  单链表.循环链表的JS实现 关于四种链表的完整封装: https://github.com/zhuwq585/Data-Structu ...

  4. 1.Go语言copy函数、sort排序、双向链表、list操作和双向循环链表

    1.1.copy函数 通过copy函数可以把一个切片内容复制到另一个切片中 (1)把长切片拷贝到短切片中 package main import "fmt" func main() ...

  5. 1.Go-copy函数、sort排序、双向链表、list操作和双向循环链表

    1.1.copy函数 通过copy函数可以把一个切片内容复制到另一个切片中 (1)把长切片拷贝到短切片中 ? 1 2 3 4 5 6 7 8 9 10 11 12 package main   imp ...

  6. java与数据结构(4)---java实现双向循环链表

    线性表之链式存储结构双向循环链表 双向循环链表:每个结点包含了数据.直接前驱地址指针和直接后驱地址指针,头结点的直接前驱指向尾结点,尾结点的直接后驱指向头结点,头尾相连构成一个可正可反的圆环.可以形象 ...

  7. java数据结构-06双向循环链表

    双向循环链表跟单向链表一样,都是头尾相连,不过单向是尾指向头,双向是头尾互相指,可以从前往后查,也可以从后往前查 无头结点的双向循环链表 public class CircleLinkedList&l ...

  8. C++实现双向循环链表

    本次博文是关于利用C++模板的方式实现的双向循环链表以及双向循环链表的基本操作,在之前的博文C++语言实现双向链表中,已经给大家分析了双向循环链表的结构,并以图示的方式给大家解释了双向循环链表的基本操 ...

  9. C语言通用双向循环链表操作函数集

    说明 相比Linux内核链表宿主结构可有多个链表结构的优点,本函数集侧重封装性和易用性,而灵活性和效率有所降低.     可基于该函数集方便地构造栈或队列集.     本函数集暂未考虑并发保护. 一  ...

随机推荐

  1. SpringMVC---依赖注入与面向切面

    1.依赖注入与面向切面 1.1.出现背景 ——如何简化java开发? 其中很重要的一点是“组件化”. ——如何更好的“组件化”? 松耦合,以及尽可能的让组件专注于本身. ——Spring框架的目的也只 ...

  2. ajax跨域请求-jsonp

    1. 同源策略 ajax之所以需要“跨域”,罪魁祸首就是浏览器的同源策略.即,一个页面的ajax只能获取这个页面相同源或者相同域的数据. 如何叫“同源”或者“同域”呢?——协议.域名.端口号都必须相同 ...

  3. hadoop报错java.io.IOException: Incorrect configuration: namenode address dfs.namenode.servicerpc-address or dfs.namenode.rpc-address is not configured

    不多说,直接上干货! 问题详情 问题排查 spark@master:~/app/hadoop$ sbin/start-all.sh This script is Deprecated. Instead ...

  4. 关于大数据领域各个组件打包部署到集群运行的总结(含手动和maven)(博主推荐)

    对于这里的打包,总结: (1)     最简单的,也是为了适应公司里,还是要用maven,当然spark那边sbt,maven都可以.但是maven居多. Eclipse/MyEclipse下如何Ma ...

  5. eclipse配置hadoop location的端口号

    在eclipse下配置hadoop location的时候 hadoop端口号应该与conf文件夹下的core-site.xml以及mapred-site.xml保持一致 前者对应dfs master ...

  6. 向vsftp服务器上传文件报“550 Permission denied”错误的解决办法

    上传文件: ftp> mput db.iso 550 Permission denied 原因:vsftp默认配置不允许上传文件. 解决:修改/etc/vsftpd.conf 将“write_e ...

  7. (内存地址hashcode与对象内容hashcode)分析== 和 equal()方法

    ==.equals()和hashCode()字符串测试 1.hashCode() 是根据 内容 来产生hash值的 2.System.identityHashCode() 是根据 内存地址 来产生ha ...

  8. 开发工具 idea 激活方法

    1. 到网站 http://idea.lanyus.com/ 获取注册码. 2.填入下面的license server: http://intellij.mandroid.cn/ http://ide ...

  9. SQL server2008无法收缩日志

    SQL server2008无法收缩日志,错误信息为: 1:由于最小日志空间要求,无法收缩日志文件 2:无法收缩日志文件 2 (XXX_log),因为该文件结尾的逻辑日志文件正在使用 描述: 用的是网 ...

  10. Entity Framework Tutorial Basics(29):Stored Procedure in Entity Framework

    Stored Procedure in Entity Framework: Entity Framework has the ability to automatically build native ...