Java实现链表操作 万字肝爆 !链表的图文解析(包含链表OJ练习解析)
Posted 意愿三七
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java实现链表操作 万字肝爆 !链表的图文解析(包含链表OJ练习解析)相关的知识,希望对你有一定的参考价值。
前言:
(温馨提示:)本文字数比较多需要慢慢观看,建议收藏此文有时间慢慢观看,看完此文你会学习到什么是链表,什么是双向链表,单链表的增删查改的基本代码思路和在线OJ题的基本代码思路。
链表的概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
什么意思呢?
我们都知道顺序表是一组数组,在逻辑上,物理上都是连续的
但是链表在逻辑上是连续的,但是在 物理上不一定连续(内存可能连续,可能不连续)
像发哥的金链子一样,后面接着前面串起来。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
- 单向、双向
- 带头、不带头
- 循环、非循环
虽然有这么多的链表的结构,但是我们重点掌握两种:
-
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如 哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
-
无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表
来使用人话说一下什么是链表吧:
大家都吃过糖葫芦,葫芦都是一个接着一个的,长下面这样
那链表呢?
链表是由一个一个 节点构成的 ,每一个节点有两个域一个叫做数值域
,一个叫做next域
data:数值域 里面存储的是数据
next:引用变量 - 存下一个节点的地址
上面的话用下面的图来展示:
从上图发现当前节点的next域存放的都是下一个节点的地址
那么最后那个没有存放下一个的地址叫做什么呢?
其实这个叫做尾巴节点:当这个next节点域为null的时候
有尾巴节点还会有头节点:整个链表当中的第一个节点叫做头节点
我们刚刚写的下面这个就是 不带头非循环的单链表
那么就有人会说了,你刚刚还说上面的这个是头结点啊!!!,怎么说不是带头的呢?? 请听我慢慢道来:
区分带头不带头,我给你画个图就知道了:
红色的那个就是头节点:它里面可以存放一个无效的data。
带头:其实就是标识当前链表的头
- 如果不带头:这个链表的头节点,在随时发生改变,
- 如果带头:这个链表的头节点不会发生改变。
啥是单向?? 往一个方向走就是单向
下图为:单向带头非循环
什么是循环的:最后一个节点的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基础必看,万字肝爆,建议收藏~
十几年老Java咳血推荐:MySQL索引原理失效情况,两万字肝爆,建议收藏!