数据结构和算法(第四章链表)

本文详细介绍了链表的基本概念和实现,包括单链表和双向链表的存储、添加、修改、删除操作,以及单向环形链表在约瑟夫环问题中的应用。通过实例分析和代码示例,帮助读者掌握链表的使用技巧和解决问题的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、链表介绍

1.链表在内存中的存储(真实存储)

链表:
1,链表以节点的方式来存储,是链式存储
2,每个节点包含一个date域,next域指向下一个节点
3,链表的各个节点不一定是连续存储
4,链表分带头节点的链表和不带头节点的链表,根据实际需求来确定

在这里插入图片描述

2.单链表(带头节点)逻辑结构

在这里插入图片描述

二、单向链表的实现

1.不考虑编号顺序时,添加英雄分析

在这里插入图片描述

2.不考虑编号顺序时,添加英雄代码

代码如下(示例):

//定义 SingleLinkedList管理HeroNode节点对象
class SingleLinkedList{
    //先创建一个头节点,作用就是表示单链表的头,头节点不动,不存放具体的数据
    private HeroNode head=new HeroNode(0,"","");
    /**
     * 添加节点到单向链表:(当不考虑编号顺序时)
     * 1,找到当前链表的最后节点
     * 2,将最后这个节点的next指向新的节点
     */
      public void add(HeroNode heroNode){
        //1,找到当前链表的最后节点
        //因为head节点不能动,所以我们需要一个辅助遍历temp,从头开始遍历
        HeroNode temp=head;
        //遍历链表,找到最后
        while(true){
            //如果链表为空,表明已经找到了最后
            if(temp.next==null){
                break;
            }
            //如果没有找到最后,就将temp后移
            temp=temp.next;
        }
        //当退出while循环时,temp就指向链表的最后,将最后这个节点的next执行那个新的节点heroNode
       temp.next=heroNode;
    }
    //显示链表,遍历
    public void list(){
        //判断链表是否为空
        if(head.next==null){
            System.out.println("链表为空!");
            return;
        }
        //因为头节点不能动,所以我们需要一个辅助变量来遍历
        HeroNode temp=head.next;
        while(true){
            //判断链表是否到最后
            if(temp==null){
                return;
            }
            //输出节点的信息,并将辅助变量后移
            System.out.println(temp);
            temp=temp.next;
        }
    }
}

//定义HeroNode,每一个HeroNode的对象,就是一个节点
class HeroNode{
    public int no;//英雄编号
    public String name;//英雄名字
    public String nickname;//英雄昵称
    public HeroNode next;//指向下一个节点

    @Override
    public String toString() {
        return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
    }

    //构造器,构造一个节点
    public HeroNode(int no,String name,String nickname){
        this.no=no;
        this.name=name;
        this.nickname=nickname;
        //为了显示节点,重写toString方法
    }
}

3.考虑编号顺序时,添加英雄分析

在这里插入图片描述

4.考虑编号顺序时,添加英雄代码

代码如下(示例):

public void addByOrder(HeroNode heroNode){
        //1,首先找到新添加节点的位置,是通过辅助变量(指针),通过遍历来完成
        //因为head节点不能动,所以我们需要一个辅助遍历temp,从头开始遍历
        HeroNode temp=head;
        //因为是单链表,所以我们找的temp是位于添加位置的前一个节点,否则插入不了
        boolean flag=false;//flag标志添加的编号是否存在,默认为false
        //while循环来进行遍历,找到新添加节点的位置
        while(true){
            if(temp.next==null){
                //说明temp已经在链表的最后
                break;
            }
            if(temp.next.no>heroNode.no){
                //位置找到,就在temp的后面插入
                //该值小于temp.next.no,说明该值就在temp和temp.next之间,直接插入到它们之间即可
                break;
            }else if(temp.next.no==heroNode.no){
                //说明希望添加的heroNode的编号已经存在了
                flag = true;//说明编号存在
                break;
            }
            //将temp后移,遍历当前链表,继续找看是否有符合上述条件的
            temp=temp.next;
        }
        //判断flag的值
        if(flag){
            //不能添加,说明编号存在
            System.out.printf("准备插入的英雄的编号%d已经存在了,不能加入\n",heroNode.no);
        }else{
            //2.插入到链表中, temp 的后面,将该编号插入到temp和temp.next之间
            heroNode.next=temp.next;
            temp.next=heroNode;
        }
    }

5.单链表的修改

修改节点的信息,根据no编号来修改,即no编号不能改,no编号改相当于添加数据
1.找到需要修改的节点,根据no编号
2,根据newHeroNode的no来修改即可
代码如下(示例):

public void update(HeroNode newHeroNode){
        //判断是否为空
        if(head.next==null){
            System.out.println("链表为空~");
        }
        //1.找到需要修改的节点,根据no编号
        //定义一个辅助变量
        HeroNode temp=head.next;
        boolean flag=false;//表示是否找到该节点
        while(true){
            if(temp==null){
                //表示已经遍历完链表
                break;
            }
            if(temp.no==newHeroNode.no){
                //找到了该节点
                flag=true;
                break;
            }
            //如果没有找到最后,就将temp后移,遍历
            temp=temp.next;
        }
        //2,根据newHeroNode的no来修改即可
        //根据flag判断是否找到要修改的节点
        if(flag){
            temp.name=newHeroNode.name;
            temp.nickname=newHeroNode.nickname;
        }else{
            //没有找到
            System.out.printf("没有找到编号%d的节点,不能修改\n",newHeroNode.no);
        }
    }


6.单链表的删除

删除节点思路
分析:head头节点不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
1,先找到需要删除的这个节点的前一个节点temp
说明我们在比较时,是temp.next.no和需要删除的节点的no进行比较
2,temp.next=temp.next.next;
说明:被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收

代码如下(示例):


public void delete(int no){
        //1,先找到需要删除的这个节点的前一个节点temp
        //定义一个辅助变量
        HeroNode temp=head.next;
        boolean flag=false;//表示是否找到待删除节点
        while(true){
            if(temp==null){
                //表示已经到了链表的最后
                break;
            }
            if(temp.next.no==no){
                //找到了待删除节点的前一个节点temp
                flag=true;
                break;
            }
            //如果没有找到最后,就将temp后移,遍历
            temp=temp.next;
        }
        //2,根据newHeroNode的no来修改即可
        //根据flag判断是否找到要修改的节点
        if(flag){
            //找到了,进行删除
            temp.next=temp.next.next;
        }else{
            //没有找到
            System.out.printf("要删除的节点%d不存在",no);
        }
    }

7.单链表小练-问题1:求单链表中有效节点的个数

分析:
方法:获取到单链表有效节点的个数(如果是带头节点的链表,需要不统计头节点)

代码如下(示例):

public static int getLength(HeroNode head){
        //如果链表为空
        if(head.next==null){
            return 0;
        }
        //定义一个计数变量length来统计来年表有效节点的个数
        int length=0;
        //因为头节点不能动,所以我们需要一个辅助变量来遍历
        //不统计头节点head.next
        HeroNode temp=head.next;
        while(temp!=null){
            //没有到最后,遍历链表,得到每一个节点length++
            length++;
            //将temp后移
            temp=temp.next;
        }
        return length;
    }


8.单链表小练-问题2:查找单链表中的倒数第k个节点

(1)分析:倒数第k个节点即就是寻找第:(单链表有效结点的个数-k)个节点,返回这个节点即可
* 1,编写一个方法,来接收头节点和索引
* 2,遍历链表,找到(单链表有效结点的个数-k)个节点,并将其返回即可
(2)代码如下(示例):


public static HeroNode findLastIndexNode(HeroNode head,int index){
        //先判断链表是否为空,为空的话,返回一个null
        if(head.next==null){
            return null;
        }
        //第一个遍历得到链表的长度
        int size=getLength(head);
        //先做一个index的校验
        if(index<=0||index>size){
            return null;
        }
        //2,遍历链表,找到(单链表有效结点的个数-k)个节点,并将其返回即可
        //该节点的索引就为:getLength(head)-index
        //因为头节点不能动,所以我们需要一个辅助变量来遍历
        //不统计头节点head.next
        HeroNode temp=head.next;
        //遍历链表得到第getLength(head)-index个节点
        for(int i=0;i<getLength(head)-index;i++){
            //没有到该节点,将temp后移
            temp=temp.next;
        }
        //返回该节点
        return temp;
    }

(3)力扣小练-快慢指针解决链表中倒数第k个节点
https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/

9.单链表小练-问题3:单链表的反转(头插法)

(1)#思路
1,遍历单链表拿出每一个节点
2,创建一个新的链表头reverseHead=new HeroNode();
3.将原单链表中拿出的节点,添加到新的链表头reverseHead中的最前端
接着让temp指向原头的下一个节点
4,再让原来的链表头指向新的链表头的下一个节点
在这里插入图片描述

(2)#代码

代码如下(示例):


public static void reverseList(HeroNode head){
        //如果当前链表为空,或者只有一个节点,直接返回即可
        if(head.next==null||head.next.next==null){
            return;
        }
        // 2,创建一个新的链表头reverseHead
        HeroNode reverseHead=new HeroNode(0,"","");
        //1,遍历单链表拿出每一个节点
        //因为头节点不能动,所以我们需要一个辅助变量来遍历原来的链表
        HeroNode temp=head.next;
        HeroNode next = null;// 指向当前节点[temp]的下一个节点
        while(temp!=null){
            //拿走了temp节点,而没有记录下一节点,单链表就断开了,无法进行下面拿原单链表中的节点
            next = temp.next;//先暂时保存原头的下一个节点,因为后面需要使用
            //将摘下来的节点放在新的链表和它下一个节点之间
            //将temp的下一个节点指向新的链表头的最前端
            temp.next=reverseHead.next;
            //将temp连接到新的链表头上
            reverseHead.next=temp;
            //让next后移,继续遍历原头后面的节点
            temp=next;
        }
        //4,再让原来的链表头指向新的链表头的下一个节点
        //将head.Next指向新的链表头,实现单链表的反转
        head.next=reverseHead.next;
    }

(3)力扣小练
https://leetcode-cn.com/problems/reverse-linked-list/submissions/
代码如下(示例):

 //采用头插法
    public ListNode reverseList(ListNode head) {
        //创建一个新的链表头
        ListNode reverseHead=null;
        ListNode temp=head;
        ListNode next=null;//用来保存当前节点的下一个节点
        while(temp!=null){
            next=temp.next;//先保存当前节点的下一个节点
            temp.next=reverseHead;
            reverseHead=temp;
            //让temp后移,继续遍历原链表的下一个节点
            temp=next;
        }
        return reverseHead;
    }


10.单链表小练-问题4:从尾到头打印单链表

(1)分析:
方式一:先将单链表进行反转操作,然后再遍历即可(会破坏原来单链表的结构)
方式二:利用栈(Stack)这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
1,创建一个栈stack
2,遍历单链表得到每一个节点,将每一个节点添加到栈中(入栈)
3,遍历栈,出栈

(2)代码如下(示例):

public static void reversePrint(HeroNode head){
        //如果是空链表不能打印
        if(head.next==null){
            return;
        }
        //1,创建一个栈stack
        Stack<HeroNode> stack=new Stack<HeroNode>();
        // 2,遍历单链表得到每一个节点,将每一个节点添加到栈中(入栈)
        //因为头节点不能动,所以需要一个辅助变量来遍历链表
        HeroNode temp=head.next;
        //入栈
        while(temp!=null){
            //2,遍历单链表得到每一个节点,将每一个节点添加到栈中(入栈)
            stack.add(temp);
            //将temp后移
            temp=temp.next;
        }
        //出栈
        while(stack.size()>0){
            System.out.println(stack.pop());
        }
    }




(3)力扣小练
https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/submissions/

11.单链表小练-问题5:合并两个有序的单链表(力扣)

https://leetcode-cn.com/problems/merge-two-sorted-lists/submissions/

三、双向链表

1.双向链表相对于单向链表的优点

1,单向链表,查找的方向只能是一个方向,而双向链表可以向前或向后查找
2,单向链表不能自我删除,需要借助辅助节点,而双向链表,则可以自我删除,所以前面单链表删除节点,总是找到temp,temp是待删除节点的前一个节点

2.双向链表增删改除思路分析

在这里插入图片描述

2.双向链表增删改除代码

(1)双向链表的添加分析
双向链表的添加(默认添加到双向链表的最后)
1,先找到双向链表的最后这个节点
2,temp.next=newHeroNode
3,newHeroNode.pre=temp

(1)双向链表的添加代码如下(示例):

//添加一个节点到双向链表的最后
    public void add(HeroNode2 heroNode) {
        //因为head头节点不能动,因此我们需要一个辅助遍历temp
        HeroNode2 temp = head;
        //遍历链表,找到链表最后位置
        while (true) {
            //如果到链表最后,就退出
            if (temp.next == null) {
                break;
            }
            //如果没到最后,就将temp后移
            temp = temp.next;
        }
        //当退出while循环时,temp就指向链表的最后
        //形成一个双向来年表
        temp.next = heroNode;
        heroNode.pre = temp;
    }



(2)双向链表的修改分析

修改一个节点的内容,和单链表一样
(2)双向链表的修改代码如下(示例):

    public void update(HeroNode2 newHeroNode) {
       //判断链表是否为空
        if(newHeroNode==null){
            System.out.println("链表为空~");
            return;
        }
        //定义一个辅助变量
        HeroNode2 temp=head.next;
        //找到需要修改的节点,根据其no编号
        //表示是否找到该节点
        boolean flag=false;
        while(true){
            if(temp==null){
                //已经遍历完该链表
                break;
            }
            if(temp.no==newHeroNode.no){
                //找到需要修改的节点
                flag=true;
                break;
            }
            //将temp后移
            temp=temp.next;
        }
        //根据flag判断是否找到要修改的节点
        if(flag){
            temp.name=newHeroNode.name;
            temp.nickname=newHeroNode.nickname;
        }else{
            //没有找到
            System.out.printf("没有找到编号为%d的节点,不能修改\n",newHeroNode.no);
        }
    }


(3)双向链表的删除分析
双向链表的删除
1,因为是双向链表,因此我们可实现自我删除某个节点
2,直接找到要删除的这个节点,比如temp
3,temp.pre.next=temp.next
4,temp.next.pre=temp.pre
(3)双向链表的删除代码如下(示例):

public void delete(int no) {
        //判断当前链表是否为空
        if(head.next==null){
            System.out.println("链表为空,不能删除~");
            return;
        }
        //辅助变量
        HeroNode2 temp=head.next;
        boolean flag=false;//标记变量,标记是否找到待删除节点
        while(true){
            //已经到链表的最后
            if(temp==null){
                break;
            }
            if(temp.no==no){
                //找到待删除节点
                flag=true;
                break;
            }
            //将temp后移,进行遍历
            temp=temp.next;
        }
        //判断flag
        if(flag){
            temp.pre.next=temp.next;
            /*
            这里代码有问题:
               如果是最后一个节点,就不需要执行下面这句话,否则就会出现空指针异常
               是最后一个的话, temp.next==null
             */
            if(temp.next!=null) {
                temp.next.pre = temp.pre;
            }
        }else{
            System.out.printf("要删除的节点%d不存在\n",no);
        }
    }


(4)添加节点到双向链表(当考虑编号顺序时)分析
1,首先找到新添加节点的位置,是通过辅助变量(指针),通过遍历来完成
2. 将该英雄编号插入到temp和temp.next之间
(4)添加节点到双向链表(当考虑编号顺序时)的代码如下(示例):

public void addByOrder(HeroNode2 heroNode){
        //1,首先找到新添加节点的位置,是通过辅助变量(指针),通过遍历来完成
        //因为head节点不能动,所以我们需要一个辅助遍历temp,从头开始遍历
        HeroNode2 temp=head;
        //因为是单链表,所以我们找的temp是位于添加位置的前一个节点,否则插入不了
        boolean flag=false;//flag标志添加的编号是否存在,默认为false
        //while循环来进行遍历,找到新添加节点的位置
        while(true){
            if(temp.next==null){
                //说明temp已经在链表的最后
                break;
            }
            if(temp.next.no>heroNode.no){
                //位置找到,就在temp的后面插入
                //该值小于temp.next.no,说明该值就在temp和temp.next之间,直接插入到它们之间即可
                break;
            }else if(temp.next.no==heroNode.no){
                //说明希望添加的heroNode的编号已经存在了
                flag = true;//说明编号存在
                break;
            }
            //将temp后移,遍历当前链表,继续找看是否有符合上述条件的
            temp=temp.next;
        }
        //判断flag的值
        if(flag){
            //不能添加,说明编号存在
            System.out.printf("准备插入的英雄的编号%d已经存在了,不能加入\n",heroNode.no);
        }else if(temp.next!=null){
            //待添加的节点不存在,且temp不是指向双链表的最后一个节点,将该编号插入到temp和temp.next之间(画个图即可)
            //使用一个变量来保存temp.next
            HeroNode2 count=temp.next;
            heroNode.next=count;
            count.pre=heroNode;
            temp.next=heroNode;
            heroNode.pre=temp;

        }else{
            //待添加的节点不存在,且temp指向双链表的最后一个节点
            temp.next=heroNode;
            heroNode.next=null;
            heroNode.pre=temp;
        }
    }


4.双链表小练习(也可使用单链表)

https://leetcode-cn.com/problems/design-linked-list/submissions/

四、单向环形链表(Josepfu约瑟夫环问题)

1.单向环形链表应用场景

在这里插入图片描述

2.约瑟夫问题示意图

在这里插入图片描述

3.构建一个单向环形链表思路

在这里插入图片描述

4.构建一个单向环形链表代码

代码如下(示例):

//创建一个单向的环形链表
class CircleSingleLinkedList{
    //创建一个first节点,当前没有编号
    private Boy first=null;
    //添加小孩节点,构成一个环形的链表
    /**
     * 构建一个单向环形链表的思路
     * 1,先创建第一个节点,让first指向该节点,并形成环形
     * 2,后面当我们每创建一个新的节点,就把该节点加入到已有的环形链表中即可
     * @param nums 代表环形链表有几个小孩
     */
    public void addBoy(int nums){
        //nums做一个数据校验
        if(nums<1){
            System.out.println("nums的值不正确~");
            return;
        }
        //辅助指针,帮助构建环形链表,因为first头指针不能动
        Boy curBoy=null;
        //根据for,将nums个小孩添加到单向环形链表中,因为已经创建了一个null的first指针,如果for从0开始会出现空指针异常
        for(int i=1;i<=nums;i++){
            //根据编号创建小孩节点,,第几个小孩
            Boy boy=new Boy(i);
            if(i==1){
                //如果是第一个小孩,单独考虑,形成环状
                first=boy;//让first节点指向boy
                first.setNext(first);//让first指向first构成一个环状
                curBoy=first;//让curBoy指向first节点,first不能动
            }else{
                curBoy.setNext(boy);//先让curBoy指向boy节点
                boy.setNext(first);//再让boy节点next指向first节点
                curBoy=boy;//让curBoy指向boy节点,因为后面可能还有节点
            }
        }
    }
    /**
     * 遍历环形链表
     * 1,先让一个辅助指针(变量)curBoy,指向first节点
     * 2,然后通过一个while循环遍历该环形链表即可,curBoy.next=first结束
     */
    public void showBoy(){
        //判断链表是否为空
        if(first==null){
            System.out.println("没有任何小孩~~");
            return;
        }
        //因为first不能动,所以我们需要一个辅助指针来帮助我们进行遍历
        Boy curBoy=first;
        while(true){
            System.out.printf("小孩的编号%d\n",curBoy.getNo());
            if(curBoy.getNext()==first){
                //说明已遍历完毕
                break;
            }
            //将curBoy后移
            curBoy=curBoy.getNext();
        }
    }
}

5.约瑟夫问题思路(小孩出圈顺序思路)

在这里插入图片描述

6.约瑟夫问题代码

代码如下(示例):

public void countBoy(int startNo,int countNum,int nums){
        //先对数据进行校验
        if(first==null||startNo<1 ||startNo>nums){
            System.out.println("输入参数有误,请重新输入~");
            return;
        }
        //1,需要创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
        Boy helper=first;
        while(true){
            if(helper.getNext()==first){
                //说明helper已经指向最后小孩节点
                break;
            }
            //helper没指向环形链表的最后那个节点,将helper后移
            helper=helper.getNext();
        }
        //2,先让first和helper移动到开始数的的地方(当小孩报数前)
        for(int i=0;i<startNo-1;i++){
            first=first.getNext();
            helper=helper.getNext();
        }
        //3,当小孩报数时,让first和helper指针同时移动(countNum-1)次, 然后出圈
        //这是一个循环操作,直到圈中只剩下一个节点
        while(true){
            if(helper==first){
                //圈中只剩下一个节点
                break;
            }
            //当小孩报数时,让first和helper指针同时移动(countNum-1)次
            for(int i=0;i<countNum-1;i++){
                first=first.getNext();
                helper=helper.getNext();
            }
            //这时first指向的节点就是要出圈的小孩节点
            System.out.printf("编号为%d的小孩出圈\n",first.getNo());
            //这时就可以将first指向的小孩节点出圈 *(1)first=first.getNext() (2)helper.setNext(first);
            first=first.getNext();
            helper.setNext(first);
        }
        System.out.printf("最后留在圈中的小孩编号为:%d\n",first.getNo());
    }


7.单向环形链表(Josepfu约瑟夫环问题)小练力扣(数学方法-反推)

(1)数学方法-反推(详解)
https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/javajie-jue-yue-se-fu-huan-wen-ti-gao-su-ni-wei-sh/
(2)反推力扣
https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/
(3)结论
//反推的过程,就是 (当前index + m) % 上一轮剩余数字的个数。
(4)代码

class Solution {
    //数学方法-反推
    //https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/javajie-jue-yue-se-fu-huan-wen-ti-gao-su-ni-wei-sh/
    public int lastRemaining(int n, int m) {
        int result=0;
        for(int i=2;i<=n;i++){
            result=(result+m)%i;
        }
        return result;
    }
}

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值