算法通关村第一关——链表青铜挑战笔记(Java构建链表、链表增删改查问题处理)

写在前面

这是本人第一次以博客形式记录所学所感,不足之处还请各位道兄赐教。话不多说,我们切入正题。


一、何为链表?

官方解释:链表是一种存储数据元素的逻辑顺序的非连续、非顺序的存储结构,可以节省内存资源和方便操作。链表中的数据呈线性排列,数据的添加和删除都较为方便,但访问比较耗费时间。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 这一看就让人头皮发麻 觉得专业。在这里插入图片描述
链表,其实有点像我们平时看到的班房里被脚铐与链条拷上的“小美”一样各个元素之间相互连接,拥有多个节点/结点(两种叫法均可,以下统一用节点)一环扣一环,且每个节点里存储了指向下一个元素的地址。如图所示:
注意看这个女人叫小美

搞错了,是这个。

所以,正常来说我们只要找到了第一个“小美”后面的“小美”就跑不了,每一个我都可以通过指向她的链条她的找到她,由此可见头节点的重要性。
在这里插入图片描述

二、Java如何构建链表?

1.按面向对象规范建立

代码如下:

public class ListNode{
    private int data;
    private ListNode next;
    public ListNode(int data){
        this.data = data;
    }

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public ListNode getNext() {
        return next;
    }

    public void setNext(ListNode next) {
        this.next = next;
    }
}

data表示节点元素的值,可以理解为“小美”;next指向下一个节点,即连接下个“小美”的链条;ListNode(int data)构造方法,用来初始化链表,在创建链表对象时会调用构造方法来分配内存空间并设置链表的初始状态,就是我们的佛波勒准备班房拷“小美”。
在这里插入图片描述
以上这种方式看起来相对臃肿,我们不妨对其优化一下。

2.按算法题中常用的定义方式

代码如下:

public class ListNode {
    public int data;
    public ListNode next;

    public ListNode(int num) {
        data = num;
        next = null;
    }

    public static void main(String[] args) {
        ListNode listnode=new ListNode(1);
    }
}

让我们写一个完成的程序看一下,代码如下,为方便各位道友捋清思路、传承鱼皮保姆级精神我加了较为详细的注释:

public class ListNode{ // 定义一个名为ListNode的类
    public int data; // 定义一个整型变量data,用于存储节点的值
    public ListNode next; // 定义一个指向下一个节点的指针next
    public ListNode(int num){ // 定义一个构造函数,接收一个整型参数num,用于初始化data和next
        data = num; // 将传入的参数num赋值给data
        next = null; // 将next指针初始化为null,表示当前节点没有指向下一个节点
    }

    public static void main(String[] args) { // main方法,程序的入口
        int[] arr = {1,2,3,4,5,6}; // 定义一个整型数组arr,用于存储要创建链表的元素
        ListNode head = initLinkedList(arr); // 调用initLinkedList方法,传入数组arr,返回链表的头节点head
        System.out.println(head); // 打印链表的头节点head

    }

    private static ListNode initLinkedList(int[] arr) { // 定义一个静态方法initLinkedList,接收一个整型数组arr作为参数,返回一个链表的头节点
        ListNode head = null; // 定义一个指针head,用于指向链表的头节点,初始值为null
        ListNode cur = null; // 定义一个指针cur,用于遍历链表,初始值为null
        for (int i = 0; i < arr.length; i++) { // 遍历数组arr
            ListNode newNode = new ListNode(arr[i]); // 创建一个新的节点newNode,其值为数组arr的第i个元素
            newNode.next = null; // 将新节点的next指针初始化为null,表示当前节点没有指向下一个节点
            if (i == 0) { // 如果当前是第一个节点(即i=0)
                head = newNode; // 将新节点赋值给头节点head
                cur = newNode; // 将新节点赋值给指针cur
            } else { // 如果当前不是第一个节点
                cur.next = newNode; // 将指针cur的next指针指向新节点
                cur = newNode; // 将新节点赋值给指针cur,以便下一次循环时使用
            }
        }
        return head; // 返回链表的头节点head
    }
}

在这里插入图片描述
这里我们打个断点debug一下,光标移动到head按住Alt+F8再点击Evaluate逐个展开:
在这里插入图片描述
可以看到,所有的链表都是从head开始,然后逐个向后访问,形成一个线性结构。

三、链表的增删改查

3.1对链表进行遍历

为什么要先对链表做一个遍历?作用主要有以下几点:

  1. 查找特定元素:通过遍历链表,可以逐个比较节点的值,找到需要查找的元素。

  2. 插入新节点:在遍历链表的过程中,可以根据需要找到合适的位置,将新节点插入到链表中。

  3. 删除节点:通过遍历链表,可以找到需要删除的节点,并进行相应的删除操作。

  4. 更新节点值:在遍历链表的过程中,如果找到了需要更新的节点,可以直接修改节点的值。

  5. 统计链表长度:通过遍历链表,可以计算链表的长度。

总之,遍历链表是实现增删改查等操作的基础,它能够帮助我们访问和操作链表中的每个节点。
遍历链表代码如下:

public static int getListLength(ListNode head){ // 定义一个静态方法getListLength,接收一个ListNode类型的参数head,用于计算链表的长度
        int length = 0; // 定义一个整型变量length,用于存储链表的长度
        ListNode node = head; // 定义一个指针node,用于遍历链表
        while (node != null){ // 遍历链表
            length++; // 每次循环时,length加1,表示当前节点的长度加1
            node = node.next; // 将指针node指向下一个节点
        }
        return length; // 返回链表的长度
    }

3.2链表的插入(头插、中间插、尾插)

链表插入操作中,头插、尾插和中间插分别有以下问题:

  1. 头插:在头插操作中,需要修改原链表的头节点指针。如果原链表为空(即头节点为null),则无法进行头插操作。此外,如果多个线程同时对同一个链表进行头插操作,可能会导致数据竞争和不一致的结果。

  2. 尾插:在尾插操作中,需要遍历整个链表找到最后一个节点,然后将新节点插入到最后一个节点的后面。如果链表非常长,尾插操作的时间复杂度较高。此外,如果链表为空(即头节点为null),则无法进行尾插操作。

  3. 中间插:在中间插操作中,需要遍历链表找到插入位置的前一个节点,然后将新节点插入到该位置。如果插入位置不合法(小于等于0或大于链表长度),则无法进行中间插操作。此外,如果多个线程同时对同一个链表进行中间插操作,可能会导致数据竞争和不一致的结果。

为了处理这些问题,那为了解决以上问题可以采取以下措施:

  1. 对于头插和尾插操作,可以先判断链表是否为空,如果为空则直接返回原链表;如果不为空,再进行相应的插入操作。这样可以确保链表不为空时才能进行插入操作。

  2. 对于头插和中间插操作,可以使用同步机制(如互斥锁)来保证多个线程对同一个链表进行插入操作时的原子性和一致性。具体做法是在插入操作前后分别加锁和解锁,以确保同一时间只有一个线程能够修改链表。

  3. 对于中间插操作,可以在插入前先检查插入位置的合法性,如果不合法则直接返回原链表;如果合法,再进行相应的插入操作。这样可以避免插入位置不合法导致的错误。

以下是具体的代码实现:

public class ListNode {
    int data; 
    ListNode next; 
    ListNode(int num) { 
        data = num;
    }
    // 获取链表长度的方法
    public static int getListLength(ListNode head) {
        int length = 0; // 初始化链表长度为0
        while (head != null) { // 遍历链表,计算节点数量
            length++;
            head = head.next;
        }
        return length; // 返回链表长度
    }

    // 插入节点的方法
    public static ListNode insertNode(ListNode head, ListNode nodeInsert, int position) {
        if (head == null) { // 如果链表为空,直接返回插入的节点
            return nodeInsert;
        }
        int size = getListLength(head); // 获取链表长度
        if (position > size + 1 || position < 1) { // 如果插入位置越界,输出提示信息并返回原链表
            System.out.println("道友,你越界了");
            return head;
        }
        if (position == 1) { // 如果插入位置为1,将插入节点作为新的头节点
            nodeInsert.next = head;
            head = nodeInsert;
            return head;
        }
        ListNode poNode = head; // 从头节点开始遍历链表
        int count = 1;
        while (count < position - 1) { // 找到插入位置的前一个节点
            poNode = poNode.next;
            count++;
        }
        nodeInsert.next = poNode.next; // 将插入节点插入到指定位置
        poNode.next = nodeInsert;
        return head; // 返回修改后的链表头节点
    }

    
}


以上这段代码中,头插法、尾插法和中间插法的实现如下:

  1. 头插法:当position == 1时,将nodeInsert插入到链表头部。
if(position == 1){
    nodeInsert.next = head;
    head = nodeInsert;
    return head;
}
  1. 尾插法:在循环结束后,将nodeInsert插入到链表尾部。
nodeInsert.next = poNode.next;
poNode.next = nodeInsert;
  1. 中间插法:在循环过程中,找到要插入的位置,并将nodeInsert插入到该位置。
ListNode poNode = head;
int count = 1;
while(count < position - 1){
    poNode = poNode.next;
    count++;
}
nodeInsert.next = poNode.next;
poNode.next = nodeInsert;

3.3链表的删除(头删、尾删、中间删)

链表的删除操作,包括头删、尾删和中间删,可能会出现以下问题:

  1. 空链表:如果链表为空,那么就无法进行删除操作。
  2. 只有一个节点的链表:如果链表只有一个节点,那么删除这个节点后,链表就变为空了。
  3. 要删除的节点不存在:如果要删除的节点不在链表中,那么就无法进行删除操作。

处理这些问题的方法如下:

  1. 对于空链表,直接返回即可。
  2. 对于只有一个节点的链表,删除这个节点后,将链表的头指针设为null。
  3. 对于要删除的节点不存在的情况,可以在遍历链表的过程中,一旦找到要删除的节点,就立即将其前一个节点的next指针指向其下一个节点,从而跳过要删除的节点,实现删除操作。

以下是相应的代码实现(这里下意识地分开了,懒得改了,各位道友将就着看):

public class ListNode {
    int data;
    ListNode next;
    ListNode(int num) {
        data = num;
    }
    // 获取链表长度的方法
    public static int getListLength(ListNode head) {
        int length = 0; // 初始化链表长度为0
        while (head != null) { // 遍历链表,计算节点数量
            length++;
            head = head.next;
        }
        return length; // 返回链表长度
    }

    // 头删方法
    public static ListNode deleteHead(ListNode head) {
        if (head == null) { // 如果链表为空,直接返回null
            return null;
        }
        return head.next; // 返回头节点的下一个节点,即删除头节点后的链表
    }

    // 尾删方法
    public static ListNode deleteTail(ListNode head) {
        if (head == null || head.next == null) { // 如果链表为空或只有一个节点,直接返回null
            return null;
        }
        ListNode pre = head; // 定义一个指针pre,指向头节点
        while (pre.next.next != null) { // 遍历链表,找到倒数第二个节点
            pre = pre.next;
        }
        pre.next = null; // 将倒数第二个节点的next指针设为null,即删除尾节点
        return head; // 返回头节点,即删除尾节点后的链表
    }

    // 中间删方法
    public static ListNode deleteMiddle(ListNode head, int position) {
        if (head == null || position < 0) { // 如果链表为空或位置无效,直接返回null
            return null;
        }
        if (position == 0) { // 如果位置为0,调用头删方法
            return deleteHead(head);
        }
        ListNode pre = head; // 定义一个指针pre,指向头节点
        int count = 1; // 定义一个计数器count,用于记录当前位置
        while (count < position && pre.next != null) { // 遍历链表,找到要删除的节点的前一个节点
            pre = pre.next;
            count++;
        }
        if (pre.next == null) { // 如果找不到要删除的节点,直接返回原链表
            return head;
        }
        pre.next = pre.next.next; // 将要删除的节点的next指针设为下一个节点的next指针,即删除该节点
        return head; // 返回头节点,即删除指定位置节点后的链表
    }
}


总结

说实话一开始学的时候感觉还是比较基础、简单,因为以前上学学过,但是到动手自己去码代码的时候还是会有稍许卡顿。说明还是得勤加练习,毕竟脑子会了手还真不一定会。在敲的时候先在顶头把思路捋清楚然后再去码代码。欲速则不达,一步一个脚印夯实基础,加油!

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值