在“匠人的算法课I”中我们详细介绍了单链表的创建、插入结点、删除结点等基本操作。单链表的优点是插入结点和删除结点的时间复杂度较低,不需要像数组那样批量移动数据,但是单链表也存在着明显的不足,那就是每次定位一个结点都需要从单链表的头部开始向后遍历,而且遍历的方向永远是单向的(从前往后)。这就导致如果不知道要插入或删除的结点的前一个结点的指针,就需要从链表头开始向后遍历链表,因此时间复杂度仍为O(n),这样链表相较于数组的优势就不明显了。所以在实际应用中,人们更常使用的是改进的链表结构,例如循环链表,双向链表或者是双向循环链表。本讲我们来介绍双向链表。

顾名思义双向链表就是指针朝前后两个方向都能自由移动的链表,这是双向链表与单链表最本质的区别。为了实现指针可以朝前后两个方向自由移动,在链表结点中需要设置两个指针,一个指针指向后继结点,一个指针指向前驱结点。如图1所示。

图1 双向链表的结构

从图1可以看出,双向链表与单链表的根本区别就在于每个结点中多出了一个指向前驱结点的指针域,通过这个指针域中保存的地址就可以方便地访问到该结点的前驱结点,从而使得访问结点更加灵活方便。

双向链表的定义

双向链表是由链表结点构成的,因此在定义双向链表之前首先要定义双向链表的结点。

class Node {int data;Node next;Node prev;public Node(int data) {this.data = data;next = null;prev = null;}
}

在Node类中包含3个成员变量:data为整型变量,它是该结点中的数据域,可用来存放一个整数;next是Node类型的变量(引用类型变量),next中保存该结点后继结点的指针;prev也是Node类型的变量(引用类型变量),prev中保存该结点前驱结点的指针。

接下来可以定义双向链表类。

public class MyDoubleLinkedList {Node head;int length;public boolean insertNode(int data, int index) {//在双向链表的index位置上插入一个结点,结点中的数据为data}public boolean deleteNode(int index) {//删除双向链表中index位置上的结点}
}
    MyDoubleLinkedList是我们定义的双向链表类,该类包含两个成员变量:head是Node类型的成员,它是这个双向链表的头指针,用来指向双向链表的第一个结点;length是一个整型变量,用来记录双向链表中结点的个数。同时在MyDoubleLinkedList类中还定义了两个成员方法用来对双向链表进行操作。方法insertNode(int data, int index)的作用是在双向链表的index位置上插入一个结点,结点中的数据为data。方法deleteNode(int index)的作用是删除双向链表中index位置上的结点。
双向链表的操作

最基本的操作包括向双向链表中插入结点,从双向链表中删除结点。除此之外,还可以根据需要定义其他的操作方法。在这里主要介绍插入结点和删除结点的方法,这两个方法中包含了双向链表的最基本操作,所以最具有代表性。

向双向链表中插入结点

前面已经给出了向双向链表中插入结点方法的定义,如下:

public boolean insertNode(int data, int index)

该方法的作用是在双向链表的index位置上插入一个元素为整型变量data的结点。这里需要声明,所谓在双向链表的index位置上插入结点是指新插入的结点最终将位于链表的第index位置上,index是从1开始计算的。例如在一个包含3个结点的双向链表的第3个位置上插入结点,最终该双向链表的形态如图2所示。

图2 向双向链表的第3个位置上插入结点

很显然,如果链表的长度为length,那么插入一个结点后其长度将变为length+1。因此新插入的结点只能位于链表的1(包含)到length+1(包含)的位置上,所以index的取值范围只能是从1(包含)到length+1(包含)之间的值,不在这个范围内的index值都是非法的。

如何在双向链表的第index位置上插入结点呢?有的同学可能会被双向链表结点中的prev和next指针搞晕,其实只要按照下面的步骤进行,是很容易完成这个操作的。

(1)首先通过一个循环操作找到要插入位置的前一个结点,并用指针curNode指向这个结点。例如要想在包含3个结点的双向链表中的第3个位置上插入结点,就要将curNode指向该双向链表的第2个结点。如图3所示。

图3 将curNode指向第二个结点

(2)创建一个新的结点,然后用指针node指向该结点。如图4所示。

图4 创建新的结点并用node指向该结点

(3)修改4个指针域实现结点的插入:curNode.next.prev,node.next,node.prev,curNode.next。如图5所示。

图5 向双向链表中插入结点(修改4个指针域的内容)

需要注意的是,当插入的新结点位于双向链表的第一个位置(index=1)或者最后一个位置(index=length+1)时需要做特殊处理,此时需要修改的指针域要少一些,大家可结合代码自行研究。

下面给出向双向链表的index位置上插入结点的Java代码实现。

 public boolean insertNode(int data, int index) {if (index < 1 || index > ( length + 1 )) {//插入结点的位置非法,直接返回falseSystem.out.println("insert node fail");return false;}if (index == 1) {//在第一个位置插入结点if (head == null) {//此时链表中没有结点head = new Node(data);} else {Node node = new Node(data);head.prev = node;node.next = head;head = node;}} else {Node curNode = head;for (int i=1; i<index-1; i++) {//curNode最终指向要插入位置的前一个结点curNode = curNode.next;}//在curNode的后面插入结点Node node = new Node(data);if (curNode.next != null) {curNode.next.prev = node;node.next = curNode.next.prev;node.prev = curNode;curNode.next = node;  } else {//curNode为最后一个结点,也就是在链表//的最后位置上插入结点curNode.next = node;node.prev = curNode;}}length++;return true;}

从双向链表中删除结点

从双向链表中删除结点的方法定义如下:

public boolean deleteNode(int index)

该方法的作用是删除双向链表中第index个结点。与插入结点类似,删除第index结点后,第index位置上的结点将被原来第index+1位置上结点(如果存在的话)所代替,同时链表的长度length减1。如果在原双向链表中第index位置上的结点为最后的结点,那么删除该结点后第index位置将为空,同时链表的长度length减1。

如果链表的长度为length,那么从该链表中删除结点的位置index的取值只能是1(包含)到length(包含),不在这个范围内的index值都是非法的。

如何从双向链表中删除第index位置上的结点呢?可以按照以下的步骤进行:

(1)首先通过一个循环操作找到要删除的结点,并用指针curNode指向这个结点。例如要想在包含3个结点的双向链表中的第2个位置上插入结点,就要将curNode指向该双向链表的第2个结点。如图6所示。

图6 将curNode指向第二个结点

(2)修改curNode结点的前驱结点的next指针curNode.prev.next和后继结点的prev指针curNode.next.prev,从而删除curNode结点。如图7所示。

图7 删除curNode结点的过程(修改2个指针)

完成如图7所示的操作后,curNode的前驱结点的next域将指向curNode的后继结点;curNode结点的后继结点的prev域将指向curNode的前驱结点,从而完成curNode结点的删除。为了删除的更加彻底,我们还可以将curNode结点的next和prev都置为null,这样curNode就变成了一个孤立的结点等待系统回收,这样做会更加安全。

如果要删除的结点是双向链表的第一个结点(index=1),或者是双向链表的最后一个结点(index=length),则需要修改的指针域要少一些,大家可结合代码自行研究。

当然我们也可以参照单链表删除结点的方法,将curNode指向要删除结点的前一个结点上,这里并没有一定之规。这也正体现出双向链表相较于单链表的优势所在,它可以任意沿着前后两个方向移动指针,从而操作起来更加方便。

下面给出删除双向链表第index位置上的结点的Java代码实现。

  public boolean deleteNode(int index) {if (index<1 || index > length) {System.out.println("delete node fail");return false;}if (index == 1) {head = head.next;head.prev.next=null;head.prev = null;length--;return true;}Node curNode = head;for (int i=1; i<index; i++) {//curNode最终指向要删除的那个结点curNode = curNode.next;}if(curNode.next != null) {Node prevNode = curNode.prev;Node nextNode = curNode.next;prevNode.next = nextNode;nextNode.prev = prevNode;curNode.next = null;curNode.prev = null;} else {curNode.prev.next = null;curNode.prev = null;}length--;return true;  }

想要算法面试类精选文章,可关注我的微信公众号  @算法匠人

关注“算法匠人”微信公众号,共享“匠人的算法课”,我们一起提高进步。


--  算法匠人作品展示  --

向大家推荐《算法大爆炸:面试通关步步为营》一书。这本书是一本既可以帮助读者筑牢数据结构和算法基础,同时又能帮助读者提升职场竞争实力的书籍。

双向链表的定义及基本操作相关推荐

  1. 数据结构(C语言)中双向链表的定义及基本操作

    一.双向链表的定义及基本操作  单链表中的结点只有一个指向其后继的指针,使得单链表要访问某个结点的前驱结点时,只能从头开始遍历,而双向链表的话 可以直接访问结点的后继结点和前驱结点. 双链表的结点中有 ...

  2. python链表怎么定义_Python数据结构之双向链表的定义与使用方法示例

    本文实例讲述了Python数据结构之双向链表的定义与使用方法.分享给大家供大家参考,具体如下: 和单链表类似,只不过是增加了一个指向前面一个元素的指针而已. 示意图: python 实现代码: #!/ ...

  3. 队列的定义及其基本操作

    文章目录 1.队列的定义 2.顺序队列及其操作 2.1 队列的顺序存储结构 2.2 顺序队列上的操作 (1)创建队列 (2)插入 (3)删除 3.循环队列及其操作 3.1 循环队列的存储结构 3.2 ...

  4. 数据结构-栈(Stack)-定义与基本操作

    数据结构-栈(Stack)-定义与基本操作 一. 顺序栈(Sequential Stack) 1. 定义 2. 基本操作 2.1 初始化 2.2 进栈 2.3 出栈 2.4 取栈顶元素 2.5 判空 ...

  5. 数据结构(C语言)——线性表(定义,基本操作)

    数据结构(C语言)--线性表(定义,基本操作) 一. 线性表的定义 二.线性表的基本操作 什么时候要传入引用"==&=="----对参数的修改结果需要"==带回来 ...

  6. 数据结构(C语言)顺序表的定义及基本操作

    顺序表的定义及基本操作 一.数据结构:顺序表的定义,创建,基本运算,实现 二.全部代码 定义顺序表 #include<stdio.h> #include<malloc.h> # ...

  7. 数据结构之数组定义及基本操作(转)

    数据结构之数组定义及基本操作数据结构中最基本的一个结构就是线性结构,而线性结构又分为连续存储结构和离散存储结构.所谓的连续存储结构其实就是数组.数组本质其实也是数据的一种存储方式,既然有了数据的存储, ...

  8. 2.1 线性表的定义和基本操作

    目录 思维导图 线性表的定义 线性表的基本操作 思维导图 数据结构的三要素:逻辑结构.数据的运算.存储结构. 线性表的定义 线性表的基本操作

  9. (王道408考研数据结构)第四章串-第一节:串的定义和基本操作及存储结构

    文章目录 一:串基本概念 (1)串的定义 (2)相关术语 (3)串的基本操作 二:串的比较 三:字符集编码 四:串的存储结构 (1)串的顺序存储 (2)串的链式存储 一:串基本概念 (1)串的定义 串 ...

最新文章

  1. nacos linux启动_微服务系列之Nacos配置中心之一:Nacos介绍与安装
  2. android获取小程序音频时长,微信小程序获取音频时长与实时获取播放进度
  3. 计算机完成逻辑运算的原理,计算机组成原理2.5.1逻辑运算.ppt
  4. WPF中查看PDF文件 - 基于开源的MoonPdfPanel (无需安装任何PDF阅读器)问题汇总
  5. 同等质量下那种图片格式小_最实用的Window小工具合集,总有一款适合你!
  6. Linux中b设备是什么,linux-将新设备添加到b??trfs卷中,但是可用...
  7. MVC自学系列之四(MVC模型-Models)
  8. 2019春第一课程设计报告
  9. 理光m2554进入维修_理光DX2432C,基士得耶6201供墨检测代码,看完马上解决代码故障...
  10. c# word文档与二进制数据的相互转换
  11. 【专家访谈】测试专家 - 陈林钧 访谈记录整理汇总
  12. Flash互动网站设计学习-Flash发展历史
  13. python系列3—顺序结构和分支结构
  14. lua的演进 lua的历史
  15. 【机器人学导论】 第二章.串联机器人
  16. 【土旦】vue项目中 使用 pako.js 解密 gzip加密字符串
  17. php 浏览器唯一标识符,在PHP中获取唯一的Web浏览器ID
  18. Weight the Tree CodeForces - 1646D
  19. 软件设计师教程(十三)计算机系统知识-软件系统分析与设计
  20. c语言中的汉诺塔问题详解

热门文章

  1. 深入理解计算机系统:hello程序的编译过程,如何用命令编译出hello.i、hello.s、hello.o的文件
  2. PYNQ-Z2点亮led灯
  3. Python批量将文件按序号重命名
  4. Linux rename命令
  5. 洛谷:梦中的统计(P1554)
  6. Dask Bag 应用
  7. File not found: ' .dcu' 的解决办法
  8. php迭代器实例,php设计模式之迭代器模式实例分析【星际争霸游戏案例】
  9. 关于中国软件发展的讨论沙龙
  10. Hive连接产生笛卡尔集. FAILED: ParseException line 1:18 Failed to recognize predicate 'a'. Failed rule: 'kwIn