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

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

双链表中的结点

双向链表中的结点有两个指针域,一个指向直接前趋,一个指向直接后继。(链表中第一个结点的前趋结点为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. PHP函数(六)-匿名函数(闭包函数)

    匿名函数能够临时创建一个没有名称的函数,常用作回调函数参数的值 <?php $test = function($a){ echo "Hello,".$a; }; $test( ...

  2. 1106SQLserver基础--变量、运算符的使用,if...else,while语句

    数据库---变量(对数据库中的数据没有任何影响) 作用:临时存储数据的作用,起一个衔接的作用,为了方便理解存储过程. 例:Declare @hello varchar(20) Set @hello=’ ...

  3. form(去掉关闭按钮,禁止调整大小)

    禁止Form窗口调整大小方法:FormBorderStyle 设为FixedSingle: 不能使用最大化窗口: MaximuzeBox 设为False: 不能使用最小化窗口:   MinimizeB ...

  4. 【转载】你知道 Linux 内核是如何构建的吗?

    内核的根 Makefile 负责构建两个主要的文件:vmlinux (内核镜像可执行文件)和模块文件.内核的 Makefile 从定义如下变量开始: VERSION = PATCHLEVEL = SU ...

  5. xcode编写c/c++静态库使用系统头文件问题

    c/c++编写的静态库中有引用ios系统头文件比如: #include <EGL/egl.h> 在xcode编译的时候需要设置静态库程序: Build Settings-Header Se ...

  6. IE6中浮动双边距bug

    想要创建出漂亮的网页设计, 除了要认真学习每一个html和CSS代码之外,不可能不去了解一下臭名昭著的IE6和更早的那些IE浏览器的坏脾气,因为你本来写出的规规矩矩的代码, 漂亮的设计就此就要完成了, ...

  7. Ajax入门(二)Ajax函数封装

    如果看了的我上一篇博客<Ajax入门(一)从0开始到一次成功的GET请求>的话,肯定知道我们已经完成了一个简单的get请求函数了.如下: 1234567891011121314151617 ...

  8. [poj3686]The Windy's(费用流)

    题目大意: 解题关键:指派问题,待更. #include<cstdio> #include<cstring> #include<algorithm> #includ ...

  9. 算法Sedgewick第四版-第1章基础-009一链表与数组的比较及其他数据结构

    1. 2.

  10. 高性能MySQL笔记-第5章Indexing for High Performance-003索引的作用

    一. 1. 1). Indexes reduce the amount of data the server has to examine.2). Indexes help the server av ...