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

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

双链表中的结点

双向链表中的结点有两个指针域,一个指向直接前趋,一个指向直接后继。(链表中第一个结点的前趋结点为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. ClientDataSet + DataSetProvider + FDQuery 的bug

    ClientDataSet + DataSetProvider  +FDQuery 有 bug ClientDataSet + DataSetProvider  +ADOQuery正常. Client ...

  2. DAY10-MYSQL存储引擎

    一 什么是存储引擎 mysql中建立的库===>文件夹 库中建立的表===>文件 现实生活中我们用来存储数据的文件有不同的类型,每种文件类型对应各自不同的处理机制:比如处理文本用txt类型 ...

  3. 使用Ping命令解析主机名解析出来的是IPv6

    如果你经常使用ping命令,并身处局域网,那么你肯定会有这样一个疑问:Ping计算机名为何是IPv6地址? 问这个问题的人很少见,大多都是对网络知识稍有了解的人,所以才会闻到关于ping的问题,而且在 ...

  4. js分页demo

    纯js实现分页   原理:所有数据已加载好,js通过遍历部分显示,实现分页效果 html代码 <html> <head> <meta charset='utf-8'> ...

  5. JAVA基础知识总结6(面向对象特征之一:多态)

    多 态:函数本身就具备多态性,某一种事物有不同的具体的体现. 体现:父类引用或者接口的引用指向了自己的子类对象. Animal a = new Cat(); 多态的好处:提高了程序的扩展性. 多态的弊 ...

  6. [codevs1159]最大全0子矩阵(悬线法)

    解题关键:悬线法模板题.注意此模板用到了滚动数组. #include<cstdio> #include<cstring> #include<algorithm> # ...

  7. 100722E The Bookcase

    传送门 题目大意 给你一些书的高度和宽度,有一个一列三行书柜,要求放进去书后,三行书柜的高的和乘以书柜的宽度最小.问这个值最小是多少. 分析 我们可以先将所有书按照高度降序排好,这样对于每一层只要放过 ...

  8. cakephp数据库配置

  9. js获取指定小时日期格式化

    不得不感叹一下,聪明的程序员写的代码真是让人惊奇 找了一圈格式化代码的方式,下面的这个使用了一个 slice 函数,真是厉害 https://stackoverflow.com/questions/4 ...

  10. Spring JDBCTemplate配置使用

    一.开发环境 Windows 10 IntelliJ IDEA 2016.1 旗舰版 JDK1.8 二.项目和数据库结构 项目结构: 数据库(MySQL 5.5.39): /* Navicat MyS ...