目录

  • 前言
  • 双链表介绍
    • 与单链表区别
    • 结构的设计
  • 具体方法的解析
    • 初始化
    • 增加
      • 空表插入:
      • 头插入:
      • 尾插入:
      • 编号插入:
    • 删除
      • 单节点删除:
      • 头删除:
      • 尾删除:
      • 普通删除:
  • 代码与测试
  • 总结与感悟

前言

前面讲过线性表中顺序表和链表的实现和性质。但是在数据结构与算法中,双向链表无论在考察还是运用中都占有很大的比例,笔者旨在通过本文与读者一起学习分享双链表相关知识。

双链表介绍


与单链表区别

逻辑上没有区别。他们均是完成线性表的内容。主要的区别是结构上的构造有所区别。
对于单链表:

  • 对于一个节点,有储存数据的data。和next后驱节点(指针)。也就是这个单链表想要一些遍历的操作都得通过前节点—>后节点

对于双链表:

  • 对于一个节点,有些和单链表一样有存储数据的data,指向后方的next(指针)。它拥有单链表的所有操作和内容。但是他还有一个前驱节点pre(指针)。

结构的设计

  • 对于双链表的结构,上图也很清楚的。以前设计的单链表是带头节点的。带头节点可以方面首位的插入和删除。而这次我们抱着学习的态度搞清链表故该双链表是不带头节点的.
  • 同时,以前的单链表是不带尾节点的,这次我们带上尾节点tail。这样我们就接触了几乎所有类型啦!遇到啥也不怕了。

所以我们构造的这个双链表的的性质:

  • 不带头节点、带尾指针(tail)、双向链表。

对于node节点:

class node<T> {T data;node<T> pre;node<T> next;public node() {}public node(T data) {this.data = data;}
}

对于链表:

public class doubleList<T> {private node<T> head;// 头节点private node<T> tail;// 尾节点private int length;//各种方法
}

具体方法的解析

  • 其实对于一个链表主要的操作还是增删。增闪的话都需要考虑是否带头节点。头插尾插中间插。并且还要考虑其中的一些细节处理。指针的运算。防止链表崩掉。因为这些操作如果不当往往会对链表的结构和证悟性带来致命的打击。而像查找那些逻辑稍微简单。也很容易排查错误。

初始化

  • 我们知道一个双链表在最初的时候它的数据肯定是为null的。那么对于这个不带头节点的双链表而言。它的head始终指向第一个真实有效的数据。tail也是如此。那么在最初没数据的时候当然要head=null,并且tail=head。(tail和head需要在一个链上)。
public doubleList() {head = null;tail = head;length = 0;}

增加

空表插入:

  • 对于空链表来说。增加第一个元素可以特殊考虑。因为在链表为空的时候headtail均为null。但head和tail又需要实实在在指向链表中的真实数据(带头指针就不需要考虑)。所以这时候就新建一个nodehead、tail等于它
node<T> teamNode = new node(data);
if (isEmpty()) {head = teamNode;tail = teamNode;
}

头插入:

对于头插入来说。步骤很简单,只需考虑head节点的变化。

  1. 新建插入节点node
  2. head前驱指向node
  3. node后驱指向head
  4. head指向node。(这时候head只是表示第二个节点,而head需要表示第一个节点故重新赋值)

尾插入:

对于尾插入来说。只需考虑尾节点tail节点的变化。

  1. 新建插入节点node
  2. node前驱指向tail
  3. tail后驱指向node
  4. tail指向node。(这时候tail只是表示倒数第二个节点,而tail需要表示最后节点故重新赋值等于node即可)

编号插入:

对于编号插入来说。要考虑查找和插入两部,而插入既和head无关也和tail无关。

  1. 新建插入节点node
  2. 找到欲插入node的前一个节点pre。和后一个节点after
  3. node后驱指向after,after前驱指向node(次时node和后面节点的关联已经完成,但是和前面处理分离状态)
  4. pre后驱指向node。node前驱指向pre(此时完毕)

整个流程的动态图为:

删除

单节点删除:

无论头删还是尾删,遇到单节点删除的需要将链表从新初始化!

if (length == 1)// 只有一个元素
{head = null;tail = head;length--;
}

头删除:

头删除需要注意的就是删除不为空时候头删除只和head节点有关

大致分为:

  1. head节点的后驱节点前驱节点改为null。(head后面本指向head但是要删除第一个先让后面那个和head断绝关系)
  2. head节点指向head.next.(这样head就指向我们需要的第一个节点了。如果有需要处理内存的语言就可以把第一个被孤立的节点删除了)

尾删除:

尾删除需要注意的就是删除不为空时候尾删除只和tail节点有关。记得在普通链表中,我们删除尾节点需要找到尾节点的前驱节点。需要遍历整个表。而双向链表可以直接从尾节点遍历到前面。

删除的时tail所在位置的点。也就是tail所在节点要断绝和双链表的关系。

  1. tail.pre.next=null尾节点的前一个节点(pre)的后驱节点等于null
  2. tail=tail.pre尾节点指向它的前驱节点,此时尾节点由于步骤1next已经为null。完成删除

普通删除:

普通删除需要重点掌握,因为前两个删除都是普通删除的一个特例而已。(普通删除要确保不是头删除和尾删除)

  1. 找打将删除节点的前驱节点team(team.next是要删除的节点)
  2. team.next.next.pre=team.(欲被删除节点的后一个节点的前驱指向team,双向链表需要处理pre和next。这步处理了pre)
  3. team.next=team.next.next;此时team.next也跳过被删除节点。
  4. 完成删除

    整个流程的动态图为:

代码与测试


代码:

package LinerList;/** 不带头节点的*/
public class doubleList<T> {class node<T> {T data;node<T> pre;node<T> next;public node() {}public node(T data) {this.data = data;}
}private node<T> head;// 头节点
private node<T> tail;// 尾节点
private int length;public doubleList() {head = null;tail = head;length = 0;
}boolean isEmpty() {return length == 0 ? true : false;
}void addfirst(T data) {node<T> teamNode = new node(data);if (isEmpty()) {head = teamNode;tail = teamNode;} else {teamNode.next = head;head = teamNode;}length++;
}void add(T data)// 尾节点插入
{node<T> teamNode = new node(data);if (isEmpty()) {head = teamNode;tail = teamNode;} else {tail.next = teamNode;teamNode.pre=tail;tail = teamNode;}length++;
}int length(){return length;}
T getElum(int index)//为了简单统一从头找
{node<T> team=head;for(int i=0;i<index;i++)//不带头节点  遍历次数-1{team=team.next;}return team.data;
}
void add(int index, T data)// 编号插入
{if (index == 0) {addfirst(data);} else if (index == length) {add(data);} else {// 重头戏node teampre = head;// 为插入的前qufor (int i = 0; i < index -1; i++)// 无头节点,index-1位置找到前驱节点{teampre = teampre.next;}node<T> team = new node(data);// a c 中插入B 找打ateam.next = teampre.next;// B.next=cteampre.next.pre = team;// c.pre=Bteam.pre = teampre;// 关联a Bteampre.next = team;length++;}
}
void deletefirst()// 头部删除
{if (length == 1)// 只有一个元素{head = null;tail = head;length--;} else {head = head.next;length--;}
}void deletelast() {if(length==1){head=null;tail=head;length--;}else {tail.pre.next=null;tail=tail.pre;length--;}
}void delete(int index){if(index==0)deletefirst();else if (index==length-1) {deletelast();}else {//删除 为了理解统一从头找那个节点  node<T>team=head;for(int i=0;i<index-1;i++){team=team.next;}//team 此时为要删除的前节点  a  c   插入B  a为teamteam.next.next.pre=team;//c的前驱变成ateam.next=team.next.next;//a的后驱变成clength--;}}void set(int index,T data){node<T>team=head;for(int i=0;i<index-1;i++){team=team.next;}team.data=data;}
@Override
public String toString() {node<T> team = head;String vaString = "";while (team != null) {vaString += team.data + " ";team = team.next;}return vaString;
}
}

测试:

package LinerList;public class test {public static void main(String[] args) throws Exception {// TODO Auto-generated method stubSystem.out.println("线性表测试:");doubleList<Integer> list = new doubleList<Integer>();list.add(66);list.addfirst(55);list.add(1, 101);list.add(-22);list.add(555);list.addfirst(9999);System.out.println(list.toString() + " lenth " + list.length());// 9999 55 101 66 -22 555// System.out.println(list.getElum(0)+" "+list.getElum(2)+" "+list.getElum(4));list.deletefirst();System.out.println(list.toString() + " lenth " + list.length());// 55 101 66 -22 555 lenth 5list.delete(1);System.out.println(list.toString() + " length " + list.length());// 55 66 -22 555 length 4list.delete(1);System.out.println(list.toString() + " length " + list.length());// 55 -22 555 length 3list.deletelast();System.out.println(list.toString() + " lenth " + list.length());// 55 -22 lenth 2list.deletelast();System.out.println(list.toString() + " lenth " + list.length());// 55 lenth 1list.deletelast();System.out.println(list.toString() + " lenth " + list.length());// lenth 0System.err.println("欢迎关注公众号:bigsai");}}

结果图

总结与感悟

插入、删除顺序问题

  • 很多人其实不清楚插入、删除正确的顺序是什么。其实这点没有必然的顺序,要根据题意所给的条件完成相同的结果即可!
  • 还有就是你可能会搞不清一堆next.next这些问题。这时候建议你画个图。你也可以先建一个节点,用变量名完成操作,可能会更容易一些。比如删除操作,你找到pre节点(删除前的节点)。你可以node delete=pre.next,node next=delete.next。这样你直接操作pre。delete。next三个节点会更简单。
  • 但是很多题目只给你一个node。你这时候要分析next(pre)。改变顺序。因为只有一个节点,你改变next(pre)很可能导致你遍历不到那个节点。所以这种情况要好好思考(可以参考笔者的代码实现)。
  • 至于有些语言需要删除内存的。别忘记删除。(java大法好)

其他操作问题:

  • 对于其他操作,相比增删要容易理解,可以参考代码理解。
  • 双向链表可以对很多操作进行优化。这里只是突出实现并没有写的太多。比如查找时候可以根据长度判断这个链表从头查找还是从尾查找

另外,代码写的可能不是太好,链表也没考虑线程安全问题。算法效率可能不太优。如果有什么改进或者漏洞还请大佬指出!

最后(last but not least)

  • 喜欢的感觉可以的烦请大家动动小手关注一下把,关注后回复: 数据结构 有精心准备的系列。个人公众号交流:bigsai
  • 欢迎交友!

数据结构与算法—一文多图搞懂双链表相关推荐

  1. 一文多图搞懂KITTI数据集下载及解析

    转载自一文多图搞懂KITTI数据集下载及解析-阿里云开发者社区 KITTI Dataset 1.图片下载:点击下载:https://s3.eu-central-1.amazonaws.com/avg- ...

  2. 分别用邻接矩阵和邻接表实现图的深度优先遍历和广度优先遍历_数据结构与算法:三十张图弄懂「图的两种遍历方式」...

    原创: 进击的HelloWorld1 引言遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次. 在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点 ...

  3. 深度优先遍历访问的边集合_数据结构与算法: 三十张图弄懂「图的两种遍历方式」...

    1 引言 遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次. 在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点出发,按照一定的访问规则,依次访 ...

  4. 一文多图搞懂KITTI检测数据集下载使用(附网盘链接)

    文章目录 前言 1 下载 2 说明 2.1 devkit_object 2.2 data_object_calib 2.3 data_object_label_2 2.4 data_object_im ...

  5. 【数据结构与算法】一篇文章彻底搞懂二分查找(思路图解+代码优化)两种实现方式,递归与非递归

    1.二分查找是什么? 二分查找也称折半查找,是一种效率较高的查找方式.但是,二分查找要求线性表为顺序存储结构且表按关键字有序排列. 时间复杂度:O(log2n) 2.二分查找的思路分析 便于叙述,笔者 ...

  6. 一文多图搞定制作自己的VOC数据集+使用yolov4训练自己的数据集+封装video测试脚本(基于ubuntu)

    一文多图搞定制作自己的VOC数据集+使用yolov4训练自己的数据集+封装video测试脚本(基于ubuntu) 制作VOC数据集 标注自己的数据集 整理数据集路径格式 训练数据集 环境 在Linux ...

  7. 数据结构与算法(C++)– 图(Graph)

    数据结构与算法(C++)– 图(Graph) 1.图的基础概念 定义:一个图G=(V, E)由顶点(vertex)的集V和边(edge)的集E组成. 边(edge):一对点即为一条边(v, w),其中 ...

  8. RPC框架:一文带你搞懂RPC

    RPC是什么(GPT答) ChatGPT回答: RPC(Remote Procedure Call)是一种分布式应用程序的编程模型,允许程序在不同的计算机上运行.它以一种透明的方式,将一个程序的函数调 ...

  9. RPC框架:从原理到选型,一文带你搞懂RPC

    大家好,我是华仔,RPC系列的文章是我去年写的,当时写的比较散,现在重新进行整理.对于想学习RPC框架的同学,通过这篇文章,让你知其然并知其所以然,便于以后技术选型,下面是文章内容目录: RPC 什么 ...

最新文章

  1. 纪念逝去的岁月——C/C++选择排序
  2. Oracle三种循环:for,while,do...while(PL/SQL)
  3. la3401解码板_拆解SONY ST-V702收音头 更换调频收音板+频偏调整
  4. Bailian2760 数字三角形【DP】
  5. tsm linux文件备份命令,IBM TSM简单使用文档
  6. 《剑指offer》面试题23——从上往下打印二叉树
  7. python股票回测源码_股票量化交易回测框架pyalgotrade源码阅读(一)
  8. Android应用的persistent属性
  9. Windows10正式版为什么没有休眠选项?
  10. python 7-1 输出星期名缩写 (10分)
  11. catgroup linux_linux中/etc/group文件详解
  12. 【网络相关】curl可以访问浏览器打不开,无法访问此网站,ERR_UNSAFE_PORT。10080端口
  13. 当你提出的分手被当真,应该如何去挽回
  14. Win10如何批量修改文件名,实现向后加固定的数字,001.jpg——999.jpg
  15. python爬虫之 爬取案例网页ajax请求的数据
  16. 玉雕工作室php,吴春强玉雕大师—吴春强玉雕工作室
  17. 企业研发流程演进之路
  18. android 电路模拟器,仿真电路模拟器(专业版)
  19. 单片机C语言应用100例(第二版)光盘资料 作者王东峰 陈圆圆 郭向阳
  20. VNC安装常见问题与处理

热门文章

  1. 石墨烯区块链(6)开发实例
  2. QT 32位程序Debug模式发布问题
  3. 密码技术--对称加密算法及Go语言应用
  4. 【颜值打分小程序】最火爆的“颜值测试”,做还是不做?(疯狂打call)
  5. [FF-A]-01-Introduction
  6. optee中mutex的实现方式
  7. [trustzone]-ARM trustzone技术下常见的软件框图
  8. win32线程学习总结(临界区,互斥体,事件,信号量)
  9. linux route命令删除多余路由
  10. 某CMSV1.0代码审计