几分钟带你解决数据结构问题-------单链表(超详细)
Posted 梦の澜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了几分钟带你解决数据结构问题-------单链表(超详细)相关的知识,希望对你有一定的参考价值。
🌈文章前提
链表在数据结构中占据很重要的地位,本篇博客将从单链表的创建,以及操作单链表实现几种功能展开,下面正式开始但链表的讲解 👇👇👇
📌文章目录:
1.链表的概念及其结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
链表的几种结构:👇
- 单向、带头、循环
- 单向、带头、非循环
- 单向、不带头、循环
- 单向、不带头、非循环
- 双向、带头、循环
- 双向、带头、非循环
- 双向、不带头、循环
- 双向、不带头、非循环
☝️ 一共有 8 中数据结构
本博客主要讨论两种链表:👇
- 单向、不带头、非循环
- 双向、不带头、非循环(后期博客涉及)
❗️ 考试、做项目、笔试题基本都是以上两种链表 ☝️
如果需要存储一组数据:👇
12 23 34 45 56
❓ 用顺序表存储即是用一个数组去进行存储,再进行增删查改等操作
❓ 那如果是用链表进行存储呢 👇
👉 链表是由一个一个叫做结点的东西组成的
如图是单向链表一个结点的图解:👇
我们用实际的数据存储来画图解释一下单向链表 👇
❗️ 注:图中每个结点的地址是随意编写的
❓ 如上图当结点中存储完数据后,每个结点都是独立的,怎样把它们串起来生成一个链表呢
👉 用每一个结点的 next 结点域保存下一个结点的地址,当前结点就可以指向下一个结点,这样一步一步就可以把结点串联起来生成一个链表
👉 最后一个结点由于不知道下一个结点的地址是什么,所以在最后一个结点域中保存的是 null,上图就是一个单链表结构
🔥 我们发现每一个结点在没有串联起来之前都是独立的,所以可以把每一个结点都定义成一个类
👉 head 结点(头结点):存放第一个结点的地址
❓ 那 head.next 存储的是什么地址呢
👉 即下一个结点的地址,并且由于 head 存放的是结点对象的地址,所以由 head.可以访问到结点的 val 域和 next 结点域
❓ 链表最后一个结点叫做什么呢
👉 链表最后一个结点称为尾结点,尾结点有一个特点—>它的尾结点 next 域是一个 null ,如果有一个结点的 next 域为 null 即该结点为尾结点
2.结点的创建
👉 抽象化一个类代表的是一个结点类型
class ListNode {
public int val;//数值域 存放数值
public ListNode next;//next结点域
}
👉 这里的 next 类型为 ListNode 类型,因为结点域存放的是下一个结点的地址,所以 next 类型当然是结点类型(ListNode)
👉构造方法
public ListNode(int val){
this.val = val;
}
在其他方法中实例化一个结点对象,可以生成一个结点:👇
public static void main(String[] args) {
ListNode listNode = new ListNode(1);
}
👉 这样就生成了一个数值域为 1 的一个结点,由于没给它的 next 域进行赋值,所以其 next 域为 null
👉 由于不知道当前结点的下一个结点是哪个,所以在初始化时,next 域不需要进行赋值
当新开辟一个对象时内存示意图:👇
☝️ 以上就是一个结点的创建
❓ 那么头结点如何创建呢 👇
public class MyLinkList {
public ListNode head;//链表的头引用
public static void main(String[] args) {
ListNode listNode = new ListNode(1);
}
}
👉 我们可以看到头结点是在 MyLinkList 链表类中定义的,因为 head 是链表的头,并不是结点的头
❓ 有这样一个问题我们上面创建的是一个单向不带头非循环的链表,那么带头的又是怎样的呢 👇
👉 区别:
- 不带头的链表头结点位置不确定,位置一直在改变
- 带头的链表头结点位置一直是固定的,非常简单,其头结点又被称为傀儡结点,其数值没有任何参考意义
二者没有好坏之分,只是简单与复杂的区别
❓ 科普一下------> 循环的链表又是如何定义的呢
3.单链表的创建
❗️ 我们先不使用头插和尾插这样的方式实现单链表的创建
👉 我们先用穷举法实现单链表的创建
public class MyLinkList {
public ListNode head;//链表的头引用
public void createList(){
ListNode listNode = new ListNode(12);
ListNode listNode1 = new ListNode(23);
ListNode listNode2 = new ListNode(34);
ListNode listNode3 = new ListNode(45);
ListNode listNode4 = new ListNode(56);
}
}
☝️ 在链表类中先定义方法,实例化五个结点对象,这五个结点目前的情况是:👇
❗️ 由于没有给 next 结点域赋值,由于其是引用类型,默认为:null
紧接着上面的代码把结点都连接起来,上一个结点的 next 域存储下一个结点的地址 ,头结点存储第一个结点的地址👇
listNode.next = listNode1;
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
this.head = listNode;
现在的各个结点就链接起来了,生成了一个单链表👇
❓ 那么链表已经有了怎样去遍历链表,拿到这些数据呢。如果是数组可以以0,1,2,3,4……下标对其进行遍历,链表又是什么方法遍历
👉 利用头结点的移动进行单链表的遍历,直到头结点为空
head = head.next
👉 把头结点的下一个结点地址赋值给头结点,再用新的头结点去访问其 val 数值
❓ 思考上述循环遍历的代码是不是有问题☝️
❗️ 代码最后循环的终止条件是 head!=null ,当循环终止时,这时候的 head 指向的是空对象,使得我们不知道 head 导致指向哪里了,这样就导致 head 用过一次就不能再次重复使用了
👉 代码改进:head 不能动,让 head 的替身动即可
ListNode cur = this.head;
☝️ 把 head 保存到的地址赋值给 cur ,即 cur 也可以指向头结点
遍历单链表拿到结点数值:👇
public void display(){
ListNode cur = this.head;
while(cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
}
在测试类中调用并打印:👇
public class TestDemo {
public static void main(String[] args) {
MyLinkList myLinkList = new MyLinkList();
myLinkList.createList();
myLinkList.display();
}
}
12 23 34 45 56
☝️ 这里成功拿到了链表的所有数值
4.单链表功能的实现
功能一:查找是否包括关键字 key(查询链表中的数据) 👇
👉 移动 cur 对每个结点的 val 值进行访问,对比和 key(需要进行查找的数值)是否相同,相同返回 true 当循环结束仍没有返回,就证明没有此数值即返回false ,循环结束的标志是 cur 访问到最后一个结点
代码实现:📌
public boolean contains(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
测试类中调用此方法,传入需要进行查询的数值作为方法的实参,并打印 👇
System.out.println(myLinkList.contains(13));
System.out.println(myLinkList.contains(56));
功能二、求得单链表的长度 👇
👉 利用 cur 去进行遍历,定义一个计数器,循环去计算,循环终止条件是,cur指向 null,即 cur 指向尾结点时结束循环
代码实现:📌
public int size() {
int count = 0;//定义一个计数器
ListNode cur = this.head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
在测试类中调用该方法并打印:👇
System.out.println(myLinkList.size());
功能三、头插法 👇
比如:需要在单链表的头结点处插入如图数据 👇
我们可以看到插入的数据是 1 ,然而插入链表的需要是结点,所以需要拿 1 来构造一个结点 👇
ListNode node = new ListNode(1);
头插法需要对两个地方进行修改:
一、插入元素的 next 结点存储第一个结点的地址
二、头结点存储当前结点的地址
❗️ 在实现插入代码时候,容易出现下图赋值顺序的错误 👇
❗️ 第一种不可以,如果先把 head 指向插入的新元素的地址,这时候 head 存储的地址是 0x444,再 head 赋值给 node.next ,node.next 存储的地址是 0x444,这就相当于把自己指向自己了,代码显然是错误的
❗️ 一定记住绑定位置的时候一定先绑定后面,以防把后面的东西弄丢
❓ 如果插入的时候链表没有元素,链表为空时,怎样插入呢
☝️ 这里空链表用上述的代码实现头插也是完全可以的,效果一样
代码实现:📌
public void addFirst(int data){
ListNode node = new ListNode(data);//创建一个新的结点
node.next = this.head;
this.head = node;
}
在类方法中调用:👇
myLinkList.addFirst(12);
myLinkList.addFirst(23);
myLinkList.addFirst(34);
myLinkList.addFirst(45);
myLinkList.addFirst(56);
56 45 34 23 12
☝️ 这里数值是倒序打印出来的,因为先存储存储第一个数据(12),再把后面的数据依次插在第一个插入元素的前面,所以得到的结果是与输入顺序相反的
功能四、尾插法 👇
❓ 尾插法表示已经有一个链表了,把一个元素放在最后面,如何实现
👉 把链表最后一个结点(尾结点)的结点域保存尾插元素的地址即可,首要任务是寻找链表的最后一个结点
👉 即 cur 所指向的结点是尾结点
❓ 上图所示的代码,是单链表中存在其他结点,那么当链表为空时,怎么实现尾插法呢
❗️ 由于在插入途中会对 cur.next 进行访问,如果是空列表这样就会造成空指针异常,所以尾插法第一次插入必须判断,如果为空,用上图的方法把 node 赋值给 head,然后再进行正常的尾插法操作
public void addLast(int data){
ListNode node = new ListNode(data);
ListNode cur = this.head;
if (this.head == null){
this.head = node;
}
else{
while(cur.next != null){
cur = cur.next;
}
cur.next = node;
}
}
在测试类中调用并打印插入结点后的链表
myLinkList.addLast(1);
myLinkList.addLast(2);
myLinkList.display();
56 45 34 23 12 1 2
功能五、在任意位置插入新元素 👇
👉 先人为的给链表加上一个标号,几号位置
假设我需要在 2 号位置进行插入 👇
需要往 2 号位置插入,就需要找到插入位置的前一个元素,然后进行两步操作
一、node.next = cur.next
二、cur.next = node
☝️ 通过上述两步把插入的结点插入到链表中
代码实现:📌
public ListNode findList(int index){
ListNode cur = this.head;
while(index - 1 != 0){
cur = cur.next;
index--;
}
return cur;
}
public void addIndex(int index, int data){
ListNode node = new ListNode(data);
if (index < 0 || index > size()){
System.out.println("位置不合法");
return;
}
if (index == 0){
addFirst(data);
return;
}
if (index == size()){
addLast(data);
return;
}
ListNode pos = findList(index);
node.next = pos.next;
pos.next = node;
}
👉 先进行判断插入结点的位置合不合法,下标是从 0 开始的没有负数,如果大于元素个数,没有前一个结点没办法插入元素,
📌 如果在下标为 0 的位置进行结点插入,即是头插法,调用头插法的实现方法即可
📌 如果在尾结点进行插入,即是尾插法,调用尾插法的方法即可
📌 在中间进行插入时,调用 findList 方法找到插入位置的前一个结点位置,返回值是一个结点类型,接
收其地址后,进行元素的插入即可完成指定位置的插入
在测试类中调用此方法,并进行打印即可: 👇
myLinkList.addIndex(3,1);
功能六、删除第一次出现关键字为key的节点 👇
❓ 如下图,想要删除链表中的数值为 23 的结点,怎样进行操作
实现的大致思想:👇
👉 循环的条件是 cur 的结点域不为空,当循环终止时也没有找到 key 结点元素,则证明在链表中不存在该元素,无法进行删除
操作
❗️ 由于判断条件是 cur.next.val == key,头结点的元素会被忽略过去,所以头结点需要单独判断
删除步骤:👇
一、判断头结点(head向后移即可)
二、找删除结点的 del 和 cur 位置,找到进行删除操作,找不到即打印该链表不存在此数据
代码实现:👇
public void remove(int key){
if(this.head == null){
System.out.println("单链表为空,不能进行删除操作");
return;
}
if(this.head.val == key){
this.head = this.head.next;
return;
}
ListNode cur = findPos(key);
if (cur == null){
System.out.println("删除的元素在链表中不存在,无法完成删除操作");
return;
}
ListNode del = cur.next;
cur.next = del.next;
}
在测试类中调用此方法并进行打印:👇
myLinkList.remove(56);
myLinkList.remove(23);
功能七、删除所有值为key的节点 👇
❗️ 要求只遍历一遍链表删除所有 key 数值的结点
删除的几种情况:
一、当需要删除的元素结点在链表的中间
cur 代表需要删除的结点,如果 cur 结点的数值刚好等于需要删除的数值,即
prev.next = cur.next;
☝️ 这时候第一个结点和第三个结点就连接起来了,然后 cur 继续向后走,寻找其他数值为 key 的结点
cur = cur.next;
👉 再进行判断目前 cur 的数值是不是 key,如上图的第三个结点数值是 34 ,不是删除的数值,就把目前 cur 的结点地址赋值给 prev
prev = cur;
cur = cur.next;
❗️ 循环的终止条件就是当 cur 遍历整个链表后,当 cur = null 的时候结束循环
二、当删除的元素结点在头和中间时,上方代码依旧适用,只不过头结点没有进行判断,少删除一个结点,等全部删除完毕,再来判断一下头结点,单独删除即可
代码实现: 👇
public ListNode removeAllKey(int key){
ListNode prev = this.head;
ListNode cur = this.head.next;
//防止空指针异常
if(this.head == null){
return null;
}
while(cur != null){
//是需要删除的元素
if (cur.val == key){
prev.next = cur.next;
cur = cur.next;
}
//不是需要删除的元素
else{
prev = cur;
cur = cur.next;
}
}
//最后处理头
if (this.head.val == key){
this.head = this.head.next;
}
//返回删除后链表的头
return this.head;
}
在测试类调用并打印即可:
myLinkList.removeAllKey(23);
功能八、情况链表 👇
👉 把链表的每一个结点都清空,防止内存泄漏
粗暴算法:把 head 置空,之后的每一个结点都没人引用了 👇
head = null;
温柔算法:一个一个结点释放 👇
public void clear(){
while(this.head != null){
ListNode curNext = head.next;
this.head.next = null;
this.head = curNext;
}
}
👆 curNext 向后走,head 不断的置空,再把 curNext 赋给 head ,循环置空即可
五、全部代码👇
单链表的创建及功能实现:👇
/**
* Created with IntelliJ IDEA.
* Description:
* User: Lenovo
* Date: 2021-11-05
* Time: 9:17
*/
class ListNode{
public int val;
public ListNode next;
public ListNode(int val){
this.val = val;
}
}
public class MyLinkList {
public ListNode head;
public void createList(){
ListNode listNode = new ListNode(12);
ListNode listNode1 = new ListNode(23);
ListNode listNode2 = new ListNode(34);
ListNode listNode3 = new ListNode(45);
ListNode listNode4 = new ListNode(56);
listNode.next = listNode1;
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
this.head = listNode;
}
public void display(){
ListNode以上是关于几分钟带你解决数据结构问题-------单链表(超详细)的主要内容,如果未能解决你的问题,请参考以下文章