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

前言

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

双链表介绍


与单链表区别

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

  • 对于一个节点,有储存数据的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又需要实实在在指向链表中的真实数据(带头指针就不需要考虑)。所以这时候就新建一个node让head、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;// 为插入的前qu
        for (int i = 0; i < index -1; i++)// 无头节点,index-1位置找到前驱节点
        {
            teampre = teampre.next;
        }

        node<T> team = new node(data);// a c 中插入B 找打a
        team.next = teampre.next;// B.next=c
        teampre.next.pre = team;// c.pre=B
        team.pre = teampre;// 关联a B
        teampre.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为team
        team.next.next.pre=team;//c的前驱变成a
        team.next=team.next.next;//a的后驱变成c
        length--;
    }
 }
   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 stub
    System.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 5
    list.delete(1);
    System.out.println(list.toString() + " length " + list.length());// 55 66 -22 555 length 4
    list.delete(1);

    System.out.println(list.toString() + " length " + list.length());// 55 -22 555 length 3
    list.deletelast();
    System.out.println(list.toString() + " lenth " + list.length());// 55 -22 lenth 2
    list.deletelast();
    System.out.println(list.toString() + " lenth " + list.length());// 55 lenth 1
    list.deletelast();
    System.out.println(list.toString() + " lenth " + list.length());// lenth 0
    System.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
  • 关注回复 数据结构 即可获得精心准备资料一份!

原文地址:https://www.cnblogs.com/bigsai/p/11351153.html

时间: 2024-10-02 19:51:52

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

一张图搞懂分布式大型网站的前世今生

大型的网站都是从小网站一步一步发展过来的,每个阶段都随着访问量的不断上涨面临不同的问题,以下是根据某本书整理出来的网站发展历程及其用到的技术,供大家参考: 一张图搞懂分布式大型网站的前世今生

数据结构与算法系列研究七——图、prim算法、dijkstra算法

图.prim算法.dijkstra算法 1. 图的定义 图(Graph)可以简单表示为G=<V, E>,其中V称为顶点(vertex)集合,E称为边(edge)集合.图论中的图(graph)表示的是顶点之间的邻接关系. (1) 无向图(undirect graph)      E中的每条边不带方向,称为无向图.(2) 有向图(direct graph)      E中的每条边具有方向,称为有向图.(3) 混合图       E中的一些边不带方向, 另一些边带有方向.(4) 图的阶      指

面试官最喜欢问的CAS还不会?怎么和他吹牛?!一文带你搞懂CAS

后端开发中大家肯定遇到过实现一个线程安全的计数器这种需求,根据经验你应该知道我们要在多线程中实现?共享变量?的原子性和可见性问题,于是锁成为一个不可避免的话题,今天我们讨论的是与之对应的无锁 CAS.本文会从怎么来的.是什么.怎么用.原理分析.遇到的问题等不同的角度带你真正搞懂 CAS. 为什么要无锁 我们一想到在多线程下保证安全的方式头一个要拎出来的肯定是锁,不管从硬件.操作系统层面都或多或少在使用锁.锁有什么缺点吗?当然有了,不然 JDK 里为什么出现那么多各式各样的锁,就是因为每一种锁都有

重读《学习JavaScript数据结构与算法-第三版》- 第6章 链表(一)

定场诗 伤情最是晚凉天,憔悴厮人不堪言: 邀酒摧肠三杯醉.寻香惊梦五更寒. 钗头凤斜卿有泪,荼蘼花了我无缘: 小楼寂寞新雨月.也难如钩也难圆. 前言 本章为重读<学习JavaScript数据结构与算法>的系列文章,该章节主要讲述数据结构-链表,以及实现链表的过程和原理. 链表 链表,为什么要有这种数据结构呢?当然,事出必有因! 数组-最常用.最方便的数据结构,But,当我们从数组的起点或中间插入或移动项的成本很高,因为我们需要移动数组元素. 链表,是存储有序的元素集合.链表中的元素在内存中并不

6. C#数据结构与算法 -- 非线性结构(图)

图 图表示点之间的关系,在C#中通过节点对象的集合来表示点(Vertex),用邻接矩阵(adjacency matrix)来表示点之间的关系.下面来看C#实现. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Graph {     //表示点

数据结构与算法(三)-线性表之静态链表

前言:前面介绍的线性表的顺序存储结构和链式存储结构中,都有对对象地引用或指向,也就是编程语言中有引用或者指针,那么在没有引用或指针的语言中,该怎么实现这个的数据结构呢? 一.简介 定义:用数组代替指针或引用来描述单链表,即用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法: 上面的静态链表图有两个数组游标和数据,其中数据数组存储数据,而游标数组存储同下标为数据的下一个数据的下标值,简单模拟一下静态链表遍历的过程: 先查看下标为999的游标数组值:1: 根据游标数组值1,查找下标为1的数据:

一张图搞懂容器所有操作 - 每天5分钟玩转 Docker 容器技术(26)

前面我们已经讨论了容器的各种操作,对容器的生命周期有了大致的理解,下面这张状态机很好地总结了容器各种状态之间是如何转换的. 如果掌握了前面的知识,要看懂这张图应该不难.不过有两点还是需要补充一下: 可以先创建容器,稍后再启动. ① docker create 创建的容器处于 Created 状态.② docker start 将以后台方式启动容器. docker run 命令实际上是 docker create 和 docker start 的组合. 只有当容器的启动进程 退出 时,--rest

4张图搞懂floorMod

在java中floorMod很多人看了官方说明文档仍然不懂,以下用四张图来说明,希望大家能豁然开朗. 原文地址:https://blog.51cto.com/11604910/2372141

一张图搞懂Ajax原理

本文整理在,我的github上.欢迎Star. 原理 说起ajax,就不得不说他背后的核心对象XMLHttpRequest,而说到XMLHttpRequest我觉得,从它的readyState状态说起是最好的切入点. 个人觉得,只要弄清楚了readyState的这几个状态,其实ajax的原理也就算弄清楚了.为了更方便您理解,笔者特意画了一张状态图. 您只需要看懂这张图ajax原理,您就算通关了:并且很难忘记. 首先let xhr = new XMLHttpRequest();,新建一个XMLHt