Java实现链表操作 万字肝爆 !链表的图文解析(包含链表OJ练习解析)

Posted 意愿三七

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java实现链表操作 万字肝爆 !链表的图文解析(包含链表OJ练习解析)相关的知识,希望对你有一定的参考价值。

前言:

(温馨提示:)本文字数比较多需要慢慢观看,建议收藏此文有时间慢慢观看,看完此文你会学习到什么是链表,什么是双向链表,单链表的增删查改的基本代码思路和在线OJ题的基本代码思路。


链表的概念及结构

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。

什么意思呢?

我们都知道顺序表是一组数组,在逻辑上,物理上都是连续的

但是链表在逻辑上是连续的,但是在 物理上不一定连续(内存可能连续,可能不连续)

像发哥的金链子一样,后面接着前面串起来。


实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

  • 单向、双向
  • 带头、不带头
  • 循环、非循环

虽然有这么多的链表的结构,但是我们重点掌握两种:

  • 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如 哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

  • 无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表

来使用人话说一下什么是链表吧:

大家都吃过糖葫芦,葫芦都是一个接着一个的,长下面这样


那链表呢?

链表是由一个一个 节点构成的 ,每一个节点有两个域一个叫做数值域一个叫做next域
data:数值域 里面存储的是数据
next:引用变量 - 存下一个节点的地址
上面的话用下面的图来展示:


从上图发现当前节点的next域存放的都是下一个节点的地址

那么最后那个没有存放下一个的地址叫做什么呢?

其实这个叫做尾巴节点:当这个next节点域为null的时候

有尾巴节点还会有头节点:整个链表当中的第一个节点叫做头节点

我们刚刚写的下面这个就是 不带头非循环的单链表

那么就有人会说了,你刚刚还说上面的这个是头结点啊!!!,怎么说不是带头的呢?? 请听我慢慢道来:

区分带头不带头,我给你画个图就知道了:


红色的那个就是头节点:它里面可以存放一个无效的data。

带头:其实就是标识当前链表的头

  1. 如果不带头:这个链表的头节点,在随时发生改变,
  2. 如果带头:这个链表的头节点不会发生改变。

啥是单向?? 往一个方向走就是单向

下图为:单向带头非循环

什么是循环的:最后一个节点的next域引用 第一个节点的地址 ,必须是第一个,不可以是第二个,不然不叫循环了
单向 不带头 循环 【下面的图】


单链表的实现

上面知道了链表大概是什么意思,现在我们准备使用代码来实现一下:

我们把整链表抽象为一个类:

把节点抽象为一个类:这个类里面包含data 字段 和next字段

代码如下:(为了方便 这些类就写到一个class文件里面)

先抽象出来节点类:

下面是解释:

为什么要给data搞个构造方法,因为如果不设置,那么我们不知道节点的next域要存什么地址,为什么不给next赋值呢? 因为next 是一个引用类型,默认是null,这就是我们的节点类 。

而且当我们实例化对象也发方便:
Node node = new Node(5);

在抽象出来整个链表的类:

对于链表,我们还需要一个属性 head,
这东西是一个引用,指向当前链表的头
因为当前是不带头的,头一直发生改变
要一个来一直标识单链表的头结点

接下来我要使用一种穷举的方式创建一个链表,为什么说这句话, 我怕你们说我low

代码如下:创建一个链表

上面代码什么意思呢?看我慢慢道来:
我们知道链表是 当前节点后面跟着一个节点

node1.next = node2;

可以看见node2的地址是0x456 ,所以为了成为链表node1的next要被node2赋值,这样才是一个链表。

node2.next = node3; 原理也是一样,只不过是我们的node3 没有引用,因为它是最后一个节点,后面没有下一个节点了,而且它是引用类型默认就是null了,所以不需要写node3的代码。


this.head = node1;

目前的头是node1 ,使用head指向node1所指的对象,13就是head

MyLinkedList 全部代码:


//节点类
class Node{
   public  int data;  //数据域
   public Node next;  //引用类型

   public Node(int data){
       this.data = data;
   }
}

//链表
public class MyLinkedList {

    public Node head;  //标识单链表的头节点

	//穷举的方式创建链表 当然有点low ,此处为了好理解
    public void createList(){  //创建链表
        Node node1 = new Node(13);
        Node node2 = new Node(2);
        Node node3 = new Node(5);

        node1.next =node2;
        node2.next =node3;
        this.head = node1;
    }
}

一、实现链表的函数操作

1、实现链表的打印函数

如果head不等于null,那么就打印数据,然后让当前head,等于下一个head.next

public void show(){   //打印链表
        while(this.head!=null){
            System.out.print(this.head.data+" ");
            this.head=this.head.next;
        }
    }

那为什么不可以head.next !=null
因为head.next 等于空那么最后一个数据就没有打印出来了

不过由此可以推出:

如果把整个链表 遍历完成 那么head==null

如果只是想找到链表最后一个节点 那么head.next==null

但是大家发现一个问题了没,就是head一直在动,那么head又没有意义了,不是头节点,所以要一个小弟来代替它,我们把代码优化一下,定义了一个小弟:

这就是比较完善的代码了


2、实现得到单链表的长度函数

原理很简单让cur一直遍历,如果cur不等于空 ,count++就好啦

public int size(){ //求Node 长度
        Node cur =this.head;
        int count =0 ;

            while(cur!=null){
                count++;
                cur=cur.next;
            }
            return count;

    }

3、查找是否包含关键字key是否在单链表当中

原理很简单让cur一直遍历,如果cur的data 等key 就return true

public boolean contains(int key){//查找是否包含关键字key是否在单链表当中
        Node cur= this.head;
        while (cur!=null){
            if (cur.data == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

4、链表头插法

什么叫做头插法:

一个新节点,要插到第一个节点前面:把14这个节点插到最前面去,next是下一个结点的地址。head 要改变成为新的node

上面所说可以将代码这样写:
node.next =head;
head = node;
这样第一个14的next域就是下一个元素了
然后在把head引用node;


但是我们不可以
head = node;
node.next =head;
把他们顺序颠倒
这样变成自己引用自己了: 所以在插入一个节点的时候,一定要先绑后面


还有一个情况就是head是null,当前链表没有任何结点,插入的是第一个节点
那么代码直接: head = node;

public void addFirst(int data){
        Node node = new Node(data);
        node.next = this.head;
        this.head = node;
    }

可以使用头插法加数据,不需要使用以前的穷举了
因为是头插法最后一个后插入到第一个去,所以打印是 5 3 1

5、链表尾插法

什么叫做尾插法:

一个新节点,要插到最后节点后面:

逻辑实现:找到最后一个节点,然后把最后节点的next域变成新节点的地址,在上面我们说过,想找最后一个节点,只需要判断head.next == null,就是尾节点了

代码的实现:

public void addLast(int data){  //尾插法
       Node node = new Node(data);   //新节点
        if (this.head ==null){    //如果是head是null  那么是第一次 直接 head = node
          this.head = node;
        }else{
            Node cur = this.head;     //定义一个cur 代替当做head 
            while (cur.next!=null){   //找到最后一个节点
                cur = cur.next;     //如果不是最后节点 一直往后走
            }
            cur.next =node;			//当前节点的next 域指向 新节点
        }

    }

6、任意位置插入,第一个数据节点为0号下标

什么意思呢? 我们需要把新的节点 插入目标节点的后面(下面的红色是新节点)

具体实现思路:
如果想实现变成链表,我们必须得把 当前节点的next域 变成 后面一个节点的地址
然后把前一个的next域 变成 新节点的地址,这就是 核心思路!!

我们先把代码放出来,依次解析:

    public Node searchPrev(int index){
        Node cur = this.head;
        int count = 0;
        while (count != index -1){  //给个条件 找到前一个就退出
            cur = cur.next; //继续找的条件
            count++;
        }
        return cur;  // 返回前面的一个
    }

    public void addIndex(int index , int data){  //任意位置插入,第一个数据节点为0号下标
            if(index<0 || index >size() ){  //判断需要放入的位置是否合法
                System.out.println("下标不合法");
                return;
            }
            //头插法
            if(index == 0){
                addFirst(data);
            }
            //尾插法
            if (index ==size()){
                addLast(data);
            }

           Node cur= searchPrev(index);
            //新节点的插入 核心代码
           Node node = new Node(data);
           node.next = cur.next;
           cur.next =node;
    }

大家可以看见上面有 两个方法,我们先来看一下searchPrev() 这个函数

searchPrev(): 找到需要插入的前一个节点的方法;
为什么需要这个方法呢?

因为我们的这个链表不是循环的链表,如果我们找到的不是插入的前一个节点那么我们就不知道前一个节点的next域是多少,因此不成立链表的实现,所以不可以。

好了现在来解析一下这个searchPrev()函数:

 public Node searchPrev(int index){  //需要插入的下标
        Node cur = this.head;   //定义一个cur 代替head
        int count = 0;         //计数
        while (count != index -1){  
//给个条件 count不等于index-1 就进入,换句话说就是找到index-1 就退出 
            cur = cur.next; //继续找的条件
            count++;
        }
        //退出了 当前cur就是 index -1
        return cur;  // 返回前面的一个
    }

以上就是这个函数的意思,那我们看看下一个函数addIndex();

public void addIndex(int index , int data){  //任意位置插入,第一个数据节点为0号下标
            if(index<0 || index >size() ){  //判断需要放入的位置是否合法
                System.out.println("下标不合法");
                return;
            }
            //头插法
            if(index == 0){    //如果index等于0 就是头插法
                addFirst(data);
            }
            //尾插法
            if (index ==size()){  //size是个函数 上面写过,
            //如果index等于size 就是全部长度,也就是尾插法
                addLast(data);
            }

           Node cur= searchPrev(index);   //前一个节点
           
            //新节点的插入 核心代码  
           Node node = new Node(data);  //这个是新节点
           //核心代码
           node.next = cur.next; 
           cur.next =node;
    }

怎么理解核心代码呢?
看下面的图:可以看出 现在cur的next 等于node2

那么 node.next = cur.next; 这个代码的意思就是把 0x789给了新节点

就意味着是新节点 指向刚刚那个0x789 node2

那么竟然新节点已经指向node2 那 cur 是不是应该指向 新节点呢?
所以 cur.next =node;

所以这才是一个完美的链表插入!!!


7、删除指定的节点

删除结点原理就是让那个节点不被引用,这样就会被JVM自动回收(当没有被引用时会被自动回收),我们可以让被删除的节点不被引用,让被删除前面的节点引用被删除后面的那个节点,这样删除的节点在中间没有被引用就会自己被回收,达到删除的效果。

代码送上:

    public Node searchPrevNode(int val){
        Node cur = this.head;

        while (cur.next!=null){
            if (cur.next.data==val){
                return cur;
            }
            cur = cur.next;

        }
        return null;
    }

    public void remove(int val){ //删除第一次出现的关键字为key的节点
            if (this.head == null){
                return;
            }
            if (this.head.data==val){
                this.head =this.head.next;
                return;
            }
            Node cur = searchPrevNode(val);
            if (cur==null){
                System.out.println("没有要删除的节点");
                return;
            }
            Node del = cur.next;
            cur.next =del.next;
    }

让我们依次解析:searchPrevNode();

public Node searchPrevNode(int val){
        Node cur = this.head;   //定义一个head

        while (cur.next!=null){//循环节点
            if (cur.next.data==val){
                //如果下一个结点的值 等于要删除的值 那么就返回前一个值
                return cur;
            }
            cur = cur.next;     //让条件继续


        }
        return null; //找不到返回null
    }

上面代码核心是: 为什么要找到前面的节点?

if (cur.next.data==val){
    //如果下一个结点的值 等于要删除的值 那么就返回前一个值
	 return cur;
}

上面我们说过,找到要删除结点的 前面节点,让前面的节点不要引用被删除的节点,就可以达到删除的效果


解析:remove()

public void remove(int val){ //删除第一次出现的关键字为key的节点
            if (this.head == null){   //如果没有节点要删除
                return;
            }
            if (this.head.data==val){     //如果删除第一个结点
                this.head =this.head.next;  //让当前结点被下一个节点引用
                return;
            }
            Node cur = searchPrevNode(val);
            if (cur==null){ //上面函数可能返回null
                System.out.println("没有要删除的节点");
                return;
            }
            Node del = cur.next;    //被删除节点 就是cur的next域
            cur.next =del.next;     //让cur的next等于del.next
                                    //del.next 是要删除的节点下一个地址
    }

来看看较难理解的代码:

if (this.head.data==val){     //如果删除第一个结点
    this.head =this.head.next;  //让当前结点被下一个节点引用
    return;
}


head.next == 0x456 ,所以head指向0x456

二:

   Node del = cur.next;    //被删除节点 就是cur的next域
   cur.next =del.next;     //让cur的next等于del.next
     //del.next 是要删除的节点下一个地址

8、删除全部指定的节点

删除全部指定的结点也是和删除差不多一样的原理,把要删除的那个节点不被引用即可:
比如下面要删除 【2】 这个结点:下面的做法就是让prev的next域等于 cur的next域,这样prev的next域,就没有引用cur ,而是引用cur.next域(就是【5】这个节点)

public void removeAllKey(int key){  //删除给定的所有值
        if(this.head==null)return;      //判断如果head是空 证明没有节点,直接return
        Node prev = this.head;          //定义一个前驱 方便操作
        Node cur = this.head.next;      //定义一个cur 从第二个节点开始
        while (cur!=null){              //条件是cur不等于空 就进去执行代码
            if(cur.data == key){        //如果当前cur等于要删除的
                prev.next=cur.next;     //把前驱的引用指向第二个节点的next域(也就是cur的下一个结点)
                cur = cur.next;         //cur往后走一步
            }else{                      //如果不是要删除的节点
                prev=cur;               //让前驱等于下一个
                cur = cur.next;         //让cur等于下一个
            }
        }

        if (this.head.data==key){       //最后判断第一个是不是要删除的节点
            this.head = this.head.next; //当前的head引用下一个
        }
    }

9、清空全部节点

清空节点原理很简单:就是让当前的节点的next不引用后面的域,然后依次置空即可, 当然我们可以暴力点直接head等于null,那么这样后面的节点依次就没有被引用,就会被JVM回收掉。下面这图就是介绍上面所说的:

 public void clear(){//删除所有节点
        while (this.head!=null){  //当不等于空进入
            Node curNext = this.head.next;  //把下一个地址保存
            this.head.next = null;          //把下一个的地址置空
            this.head = curNext;            //把当前节点不被引用
        }
    }

那么我们怎么证明已经把它给"干掉了呢"?
1、打个断点调试起来

2.打开cdm 输入 jps 查看当前java进程
然后输入:

jmap - histo:live 4548

(jmap是JDK自带的工具软件,主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节)

意思是:这些东西会查出很多信息,打印到你的cmd中 但是这样不方便 我们可以重新重定向一下,把查出信息放在d盘下123123.txt 文件 方便阅读

jmap -histo:live 4548>d:\\123123.txt

注意图上的箭头所指向的要是一样 比如我上面是4548 live后面也是4548,然后按回车,发现它在一闪一闪,我们去idea执行下一步操作

然后去找到文件打开即可:

ctrl+f 找一下 节点 (我的节点叫做Node)这里是有4个

我的程序里面也刚刚好

以上是关于Java实现链表操作 万字肝爆 !链表的图文解析(包含链表OJ练习解析)的主要内容,如果未能解决你的问题,请参考以下文章

1小时0基础带你Git入门,保姆式教学,万字肝爆! 建议收藏~

一篇博文:带你 gulp入门 0基础必看,万字肝爆,建议收藏~

C语言万字肝爆!建议收藏!深度剖析数据在内存中的存储

十几年老Java咳血推荐:MySQL索引原理失效情况,两万字肝爆,建议收藏!

一篇博文:带你TypeScript入门,两万字肝爆,建议收藏!

一篇博文:带你TypeScript入门,两万字肝爆,建议收藏!