Java数据结构-单链表

前提

顺序表的优缺点:

优点:
😔 连续的物理空间,支持根据下标随机访问

缺点:
😔插入数据,空间不足时要扩容,会造成一定的资源浪费
😔头插或中间位置插入元素时需要挪动元素,效率比较低

基于顺序表的缺点,就出现了链表.

1.单链表概念

链表是物理存储结构上非连续,非顺序的存储结构,整个链表通过对各个结点地址的链式存储来实现.链表由结点组成.

链表类似于火车,由一节节的车厢通过钩子连接起来.

那么问题来了,这些结点之间是怎么联系起来的呢?
其实是通过引用所指向的地址连接起来的.
也就是每一个结点都分为两部分,分别是数值域next域(地址域)
结点是一个引用类型

那么存储的地址是谁的地址?
其实是下一个结点的地址

我们用有四个结点的链表来说明:这四个结点之间的关系是这样的

在这里插入图片描述
我们发现:
😔next域里面存储的就是下一个结点的引用变量(地址)
😔第四个结点的next域为null,说明没有下一个结点了

我们知道链表是由结点组成的,那么结点是怎么组成的呢?

从上面那张图片我们知道:

每一个结点都是一个独立的个体,那我们可以把它抽象成一个内部类,并放到单链表这个类里面,这样的话我们就可以在单链表这个类里面使用结点这个类.

2.单链表的创建

public class MyLinkedList {
    listNode head=null;//声明链表中的头结点
    //创建单链表结构,将链表中的每一个结点定义成一个内部类
    public class listNode{
        public int val;//结点的数值域
        public listNode next;//结点的next域,存储下一个结点的地址,两个结点之间通过next关联
        public listNode(int val){//构造方法,给新的结点赋值,同时next默认为null
            this.val=val;
        }
    }
}

补充:static修饰结点类也可以,表示结点类脱离MyLinkedList也可以单独存在.

链表创建好之后,就有一些对链表的基本操作方法:

2.1 链表的初始化

那怎么初始化呢?

也就是创建一些结点,并在每一个结点中都把下一个结点的地址存储起来,让结点之间关联起来.

public listNode listInit(){
        Scanner sc=new Scanner(System.in);
        System.out.println("输入链表长度");
        int n=sc.nextInt();
        System.out.println("输入链表第1个元素的值");
        int firstVal=sc.nextInt();
        this.head=new listNode(firstVal);//创建链表的第一个结点,将链表的头结点引用head指向第一个结点
        listNode cur=this.head;//创建cur结点去完成对链表的初始化,因为头结点head很重要,不能直接使用,否则会丢失链表
        for(int i=1;i<n;i++){
            System.out.println("请输入链表第"+(i+1)+"个元素的值");
            int val=sc.nextInt();
            listNode node=new listNode(val);
            cur.next=node;//当前结点的next存放的就是下一个结点的引用变量node,node存储的就是下一个结点的地址
            cur=node;//将当前结点移动到下一结点位置
        }
        return this.head;//返回链表头结点
    }

😀
对于结点之间的连接,用到的就是cur这个结点引用变量:

当cur引用也指向第一个结点之后,cur.next就代表第一个结点的next域,只要next域存储了下一个结点的地址,这两个结点就连接起来了.

😀
从上面的代码我们也可以知道:

node引用是指向新建的结点的,也就是node里面存储的是第二个结点的地址,所以当cur.next=node,这两个结点就被连接起来了.

😔
那如果再来一个结点呢?该怎么连接?

我们只需要让cur=cur.next,cur.next=node即可

当cur=cur.next之后,那么引用变量cur所引用的就变为了第二个结点,那么cur.next=node,也就是把第三个结点的地址存储到了第二个结点的next域中.

😔
有人会问,为什么需要再定义一个结点的引用变量cur呢?用head结点引用指向的不断改变来连接各个结点不可以吗?

是可以的,但是如果用head结点来连接其他结点的话,我们还能找到head吗?还能找到这个链表吗?

我们来看个例子:为什么不能直接使用头结点head!

💗我们先手动创建一个链表:

public class MyLinkedList {
    class listNode{
        public int val;
        public listNode next;
        public listNode(int val){
            this.val=val;
        }
    }
    public listNode head;
    //手动创建链表
    public void createLinkedList(){
        listNode node1=new listNode(12);
        listNode node2=new listNode(23);
        listNode node3=new listNode(34);
        listNode node4=new listNode(45);
        node1.next=node2;
        node2.next=node3;
        node3.next=node4;
        head=node1;
    }
    //遍历链表
    public void display(){
        while(head!=null){
            System.out.print(head.val+" ");
            head=head.next;
        }
    }
}

💗然后我们在测试类Test中对这个链表进行打印:

public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList=new MyLinkedList();
        myLinkedList.createLinkedList();
        myLinkedList.display();
        System.out.println();
        System.out.println("=====================");
        myLinkedList.display();
    }
}

💗最后,我们得到的结果是:

12 23 34 45 
=====================

通过这个例子,我们发现,利用head结点进行遍历打印,第一次遍历可以成功地把所有结点打印出来,但是,第二次打印这个链表的时候却没有结果.这是因为第一次打印结束之后,我们丢失了head这个结点,head结点为null了.而每一次的打印都必须先找到head结点才可以.

2.2 链表的打印

public void linkedListPrint(){
        listNode cur=this.head;//创建cur引用变量去完成遍历(因为头节点不能直接使用,会丢失从而导致链表的丢失)
        while(cur!=null){
            System.out.println(cur.val+" ");
            cur=cur.next;//打印完当前结点,就指向下一结点,完成下一结点的打印
        }
        System.out.println();
    }

2.3 获取链表的长度

//时间复杂度为O(N)
public int getSize(){
        listNode cur=this.head;
        int l=0;
        while (cur!=null){
            l++;
            cur=cur.next;
        }
        return l;
    }

2.4 判断链表是否为空

// 为空就抛出异常,终止程序
    private void isEmpty() { // 判断链表是否为空,只是该类中使用,所有
        if (head == null) {
            System.out.println("该链表为空!!!");
            throw new NullPointerException();
            // 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
            //异常一旦抛出,其后的代码就不会执行,相当于就直接return了
        }
    }

以上操作就可以得到一个基本的链表了,接下来来看对链表的一些操作

3.链表的基本操作

😀在链表头部插入元素

//时间复杂度O(1)
public void addHead(int data) {
        listNode node=new listNode(data);//新插入的结点node
        node.next=this.head;//🧐
        this.head=node;//🧐
    }

注意:🧐这两行代码的顺序很重要,不要弄反!

😃尾插法

//时间复杂度O(N),找尾巴的过程
public void addLast(int val){
        listNode node=new listNode(val);
        if(this.head==null){
            this.head=node;//当head为空,直接让head指向node即可
            return;
        }else {
            listNode cur=this.head;
            while(cur.next!=null){
                cur=cur.next;
            }
            cur.next=node;
        }
    }

链表的头插,尾插都不需要挪动元素.而且链表的插入只是修改指向

😀指定下标插入元素

public void addIndex(int pos,int data){
        if(pos<0||pos>this.getSize()){//pos==getSize也是可以的,这样相当于在尾部插入结点
            System.out.println();
        }
        listNode node=new listNode(data);//要插入到新的结点
        //如果是头插
        if(pos==0){//当插入的是头结点时
            addHead(data);
            return;
        }
        //如果是其他位置插入
        else {
            listNode cur=this.head;
            for(int i=0;i<pos-1;i++){//这种情况包含了尾插结点
                cur=cur.next;//通过循环,让cur指向要新增下标的前一个结点
            }
            node.next=cur.next;//先将node结点指向下一个结点,再让cur指向node结点
            cur.next=node;
        }
    }

😍第二种写法:

public void addPos(int pos,int val){
        if(pos<0||pos>this.getSize()){//pos==getSize也是可以的,这样相当于在尾部插入结点
            System.out.println();
        }
        listNode node=new listNode(val);
        if(pos==0){
            this.head=node;
        } else if (pos==getSize()) {
            addLast(val);
        }else {
            listNode cur=this.head;
            for(int i=0;i<pos-1;i++){
                cur=cur.next;
            }
            node.next=cur.next;
            cur.next=node;
        }
    }

有一个需要注意的点:

node.next=cur.next;
cur.next=node;

这两步的顺序很重要!为什么呢?我们来看!

比如我们想在下标为2的地方插入新的结点node,那么首先我们就要通过循环找到要插入的的结点(也就是下标为2的结点)的前一个结点.

然后呢❓
容易错的地方就在于,很多人会认为,让下标为1的结点的next指向node,让node指向下标为2的结点不就可以了吗.

但这样是不对的!这种错误思路的代码是

cur.next=node;
node.next=❓

我们发现❓处不知道应该填写什么.是因为结点2原本是通过结点1来找到的,但是当执行cur.next=node之后,我们丢失了结点2,所以到了node.next=结点2时,我们无法知道结点2在哪里.

所以说,我们要特别注意!

😀删除头结点

public void deleteHead(){
        if(ifEmpty()){//检查是否为空
            System.out.println("链表为空,无法删除");
            return;
        }
        if(head.next!=null){//如果head结点不是链表唯一的结点,就直接将head结点的下一节点变为新的head结点,原来的head结点自动被系统回收
            this.head=this.head.next;
        }
        else {
            this.head=null;//当链表只有一个元素的时候
        }
    }

😀删除指定下标的结点

public void deleteIndex(int pos){
        if (pos<0||pos>=this.getSize()){//注意此时pos不能等于getSize,因为getSize位置没有结点,结点下标最大到getSize-1的地方
            System.out.println("下标不合理,删除失败");
            return;
        }
        if (pos==0){//pos==0,相当于删除的是头结点
            deleteHead();
            return;
        }
        listNode cur=this.head;//创建一个cur引用去完成循环
        for (int i=1;i<pos-1;i++){//要让cur指向要删除的结点的前一个结点
            cur=cur.next;
        }
        cur.next=cur.next.next;
    }

思路就是:我们要删除一个结点,本质上就是要让其他结点不指向它,也就是其他结点的next域不存储被删除结点的引用.

😀删除所有数值为key的元素

public void deleteAllKey(int key){
        if (ifEmpty()){
            System.out.println("链表为空,删除失败");//检查是否为空
        }
        while(this.head!=null&&this.head.val==key){//删除头结点
            deleteHead();
            if(this.head==null){//当头结点为空,直接返回,否则会有空指针异常
                return;
            }
        }
        listNode cur=this.head;
        while(cur.next!=null){
            if(cur.next.val==key){
                cur.next=cur.next.next;//包含删除尾结点的情况
            }else {
                cur=cur.next;
            }
        }
    }

😀查找元素key是否在链表中

public boolean searchKey(int key){
        listNode cur=this.head;
        while (cur!=null){
            if (cur.val==key){
                return true;
            }
            else {
                cur=cur.next;
            }
        }
        return false;
    }

😀清空链表

public void clear(){
        this.head=null;//直接让head结点为null
    }

注意:如果链表中的结点的val为引用类型的话,则用equals比较,而不是

😀总的来说:插入结点,就是让cur走到要插入位置的前一步.删除结点,也是让cur走到要删除位置的前一步

总代码

public class MyLinkedList {
    listNode head=null;//声明链表中的头结点
    //创建单链表结构,将链表中的每一个结点定义成一个内部类
    public class listNode{
        public int val;//结点的数值域
        public listNode next;//结点的next域,存储下一个结点的地址,两个结点之间通过next关联
        public listNode(int val){//构造方法,给新的结点赋值,同时next默认为null
            this.val=val;
        }
    }
    //链表的初始化
    public listNode listInit(){
        Scanner sc=new Scanner(System.in);
        System.out.println("输入链表长度");
        int n=sc.nextInt();
        System.out.println("输入链表第1个元素的值");
        int firstVal=sc.nextInt();
        this.head=new listNode(firstVal);//创建链表的第一个结点,将链表的头结点引用head指向第一个结点
        listNode cur=this.head;//创建cur结点去完成对链表的初始化,因为头结点head很重要,不能直接使用,否则会丢失链表
        for(int i=1;i<n;i++){
            System.out.println("请输入链表第"+(i+1)+"个元素的值");
            int val=sc.nextInt();
            listNode node=new listNode(val);
            cur.next=node;//当前结点的next存放的就是下一个结点的引用变量node,node存储的就是下一个结点的地址
            cur=node;//将当前结点移动到下一结点位置
        }
        return this.head;//返回链表头结点
    }
    //链表的打印
    public void linkedListPrint(){
        listNode cur=this.head;//创建cur引用变量去完成遍历(因为头节点不能直接使用,会丢失从而导致链表的丢失)
        while(cur!=null){
            System.out.print(cur.val+" ");
            cur=cur.next;//打印完当前结点,就指向下一结点,完成下一结点的打印
        }
        System.out.println();
    }
    //获取链表长度
    public int getSize(){
        listNode cur=this.head;
        int l=0;
        while (cur!=null){
            l++;
            cur=cur.next;
        }
        return l;
    }
    //判断链表是否为空
    private boolean ifEmpty() { // 判断链表是否为空,只是该类中使用,所有
        if (head == null) {
            System.out.println("该链表为空!!!");
            return true;
        }
        return false;
    }
    //在链表头部插入元素
    public void addHead(int data) {
        listNode node=new listNode(data);//新插入的结点node
        node.next=this.head;
        this.head=node;
    }
    //指定下标插入元素
    public void addIndex(int pos,int data){
        if(pos<0||pos>this.getSize()){//pos==getSize也是可以的,这样相当于在尾部插入结点
            System.out.println("pos位置不合法,插入失败");
            return;
        }
        listNode node=new listNode(data);//要插入到新的结点
        //如果是头插
        if(pos==0){//当插入的是头结点时
            addHead(data);
            return;
        }
        //如果是其他位置插入
        else {
            listNode cur=this.head;
            for(int i=0;i<pos-1;i++){//这种情况包含了尾插结点
                cur=cur.next;//通过循环,让cur指向要新增下标的前一个结点
            }
            node.next=cur.next;//先将node结点指向下一个结点,再让cur指向node结点
            cur.next=node;
        }
    }
    //删除头结点
    public void deleteHead(){
        if(ifEmpty()){//检查是否为空
            System.out.println("链表为空,无法删除");
            return;
        }
        if(head.next!=null){//如果head结点不是链表唯一的结点,就直接将head结点的下一节点变为新的head结点,原来的head结点自动被系统回收
            this.head=this.head.next;
        }
        else {
            this.head=null;//当链表只有一个元素的时候
        }
    }
    //删除指定下标的结点
    public void deleteIndex(int pos){
        if (pos<0||pos>=this.getSize()){//注意此时pos不能等于getSize,因为getSize位置没有结点,结点下标最大到getSize-1的地方
            System.out.println("下标不合理,删除失败");
            return;
        }
        if (pos==0){//pos==0,相当于删除的是头结点
            deleteHead();
            return;
        }
        listNode cur=this.head;//创建一个cur引用去完成循环
        for (int i=1;i<pos-1;i++){//要让cur指向要删除的结点的前一个结点
            cur=cur.next;
        }
        cur.next=cur.next.next;
    }
    //找到关键字key的前驱结点
    public listNode searchPrev(int key){
        if(this.head==null){//一个结点都没有
            return null;
        }
        listNode cur=this.head;
        while(cur.next!=null){//当cur.next==null时说明没有要删除的结点
            if(cur.next.val==key){
                return cur;//cur是要删除的key的前驱结点
            }
            cur=cur.next;
        }
        return null;//没有要删除的结点
    }

    //删除链表中第一个数值为key的元素
    public void deleteKey(int key){
        if(head.val==key){
            head=head.next;
            return;
        }
        listNode node=searchPrev(key);
        if(node==null){
            return;
        }
        listNode del=node.next;//要删除的结点
        node.next=del.next;
    }
    //删除链表所有数值为key的结点
    public void deleteAllKey(int key){
        if (ifEmpty()){
            System.out.println("链表为空,删除失败");//检查是否为空
        }
        while(this.head!=null&&this.head.val==key){//删除头结点
            deleteHead();
            if(this.head==null){//当头结点为空,直接返回,否则会有空指针异常
                return;
            }
        }
        listNode cur=this.head;
        while(cur.next!=null){
            if(cur.next.val==key){
                cur.next=cur.next.next;//包含删除尾结点的情况
            }else {
                cur=cur.next;
            }
        }
    }
    //查找元素key是否在链表中
    public boolean searchKey(int key){
        listNode cur=this.head;
        while (cur!=null){
            if (cur.val==key){
                return true;
            }
            else {
                cur=cur.next;
            }
        }
        return false;
    }
}

测试代码

public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.listInit();
        System.out.println("链表初始化后.第一次打印");
        myLinkedList.linkedListPrint();
        System.out.println("==========================");
        myLinkedList.addHead(5);
        System.out.println("头插元素5之后,第二次打印");
        myLinkedList.linkedListPrint();
        System.out.println("==========================");
        myLinkedList.addIndex(2, 9);
        System.out.println("在下标1处插入元素9之后,第三次打印");
        myLinkedList.linkedListPrint();
        System.out.println("==========================");
        myLinkedList.deleteIndex(3);
        System.out.println("删除下标为3的元素后,第四次打印");
        myLinkedList.linkedListPrint();
        System.out.println("==========================");
        myLinkedList.deleteHead();
        System.out.println("删除头结点后,第五次打印");
        myLinkedList.linkedListPrint();
        System.out.println("===========================");
        myLinkedList.deleteAllKey(2);
        System.out.println("删除所有值为2的元素后,第六次打印");
        myLinkedList.linkedListPrint();
        System.out.println("===========================");
        System.out.println("输出链表的长度");
        System.out.println(myLinkedList.getSize());
    }
}

结果

输入链表长度
4
输入链表第1个元素的值
1
请输入链表第2个元素的值
2
请输入链表第3个元素的值
3
请输入链表第4个元素的值
4
链表初始化后.第一次打印
1 2 3 4 
==========================
头插元素5之后,第二次打印
5 1 2 3 4 
==========================
在下标1处插入元素9之后,第三次打印
5 1 9 2 3 4 
==========================
删除下标为3的元素后,第四次打印
5 1 2 3 4 
==========================
删除头结点后,第五次打印
1 2 3 4 
===========================
删除所有值为2的元素后,第六次打印
1 3 4 
===========================
输出链表的长度
3

4. 链表面试题

😎反转一个单链表 OJ链接

大体思路:将第一个结点之后的元素都用头插法,插入到链表头部

代码:

//空间复杂度要求O(1)
    //时间复杂度要求O(N)
    //利用遍历,头插法
    public listNode reverse(){
        if(this.head==null){
            return null;
        }
        if(this.head.next==null){
            return this.head;
        }
        listNode cur=head.next;
        head.next=null;
        while (cur!=null){
            listNode curNext=cur.next;
            cur.next=head;
            head=cur;
            cur=curNext;
        }
        return head;
    }

😎链表的中间结点 OJ链接

大体思路:快慢指针,也就是创建一个fast引用,一个slow引用,先指向head结点,fast每次走两步,slow每次走一步,fast的速度是slow的两倍,同样的路程,当fast走到最后,slow的位置刚好就是中间位置.

奇数个结点的时候,fast.next==null,就找到了中间结点.偶数个结点的时候,fast为空,就找到了.

代码:

public listNode middleNode(listNode head){
        listNode fast=this.head;
        listNode slow=this.head;
        while(fast!=null&&fast.next!=null){//这一步很重要!顺序一定先是fast!=null,然后是fast.next!=null
            //而不能先是fast.next!=null,然后是fast!=null,这样会发生空指针异常
            fast=fast.next.next;
            slow=slow.next;
        }
        return slow;
    }

😎返回倒数第k个结点 OJ链接

方法一:

大体思路:最后一个结点与倒数第k个结点差k-1步.先让fast走k-1步,然后fast走一步,slow走一步,直到fast.next为null的时候.此时slow所指的就是倒数第k个结点.

代码:

public listNode kthToLast2(listNode head,int k){
        if(k<=0||k>getSize()){
            return null;
        }
        if(head==null){
            return null;
        }
        listNode fast=this.head;
        listNode slow=this.head;
        while(k-1!=0){//让fast先走k-1步
            fast=fast.next;
            k--;
            if(fast==null){
                return null;
            }
        }
        while (fast.next!=null){//fast走一步,slow走一步
            fast=fast.next;
            slow=slow.next;
        }
        return slow;
    }

方法二:

大体思路:倒数第k个结点,就往后走getSize()-k步,此时就是要找的结点

代码:

public listNode kthToLast(listNode head,int k){
        listNode cur=this.head;
        if(k<1||k>this.getSize()){
            System.out.println("k输出不正确,无法找到");
        }else {
            if(this.head==null){
                return null;
            } else if (this.head.next==null) {
                return this.head;
            }else {
                for(int i=0;i<getSize()-k;i++){
                    cur=cur.next;
                }
            }
        }
        return cur;
    }

😎合并两个有序链表 OJ链接

大体思路:双指针遍历两个链表,比较结点大小关系,两个指针交替前进,直到遍历结束.

重要的一点:引入伪头结点,因为刚开始合并链表并没有结点,所以无法将结点添加到合并链表中.

解决方案:就是引入一个结点作为合并链表的伪头结点,将其他结点添加到这个伪结点之后.

需要注意的地方:当两个链表不一样长,那么就会跳出while循环,跳出之后,直接将没有遍历完的链表添加到合并链表后面,因为这两个链表本身就是有序的.

代码:

public listNode mergeTwoLists(listNode list1, listNode list2) {
        listNode newHead=new listNode(0);
        listNode tmp=newHead;
        while(list1!=null&&list2!=null){
            if(list1.val<list2.val){
                tmp.next=list1;
                list1=list1.next;
                tmp=tmp.next;
            }else {
                tmp.next=list2;
                list2=list2.next;
                tmp=tmp.next;
            }
        }
        tmp.next=(list1==null?list2:list1);
        return newHead.next;
    }

😎回文链表 OJ链接

大体思路:找到中间结点,然后将中间结点之后的结点翻转,然后双指针进行判断.

需要注意的地方:偶数个结点和奇数个结点的不同处理

代码:

public boolean isPalindrome(listNode head){
        listNode fast=head;
        listNode slow=head;
        if(head==null){
            return false;
        }
        if(head.next==null){
            return true;
        }
        //1.找中间结点
        while (fast!=null&&fast.next!=null){//这两个判断缺一不可,而且顺序很重要,否则会报空指针异常
            fast=fast.next.next;
            slow=slow.next;
        }
        listNode cur=slow.next;
        //2.翻转中间结点后的结点
        while(cur!=null){
            listNode curNext=cur.next;
            cur.next=slow;
            slow=cur;
            cur=curNext;
        }
        //3.双指针判断
        while(head!=slow){
            if(head.val!=slow.val){
                return false;
            }
            if(head.next==slow){//当结点个数为偶数个,需要增加这个判断
                return true;
            }
        }
        return true;
    }

😎链表分割 OJ链接

大体思路:遍历完这个链表之后,我们会得到两个链表,一个是数据小于x的链表small,一个是数据大于x的链表large,最后我们只需要将small链表的尾结点指向large链表的头结点即可.

注意:
关键的一点在于我们要为两个链表分别创建一个傀儡结点,分别是smallHead和largeHead,用傀儡结点的next去指向两个链表的头结点.

为什么要这样做呢?

为了更方便的处理头结点为空的边界情况,在遍历结束后,small要指向它的链表的结尾,large要指向它的链表的结尾.

最后不要忘了,将large.next设置为null,是因为当前这个结点是复用的原来链表的结点,这个结点的next可能指向下一个不属于large链表里的结点,那么我们就要通过large.next=null来切断这个引用.

代码:

class Solution {
    public ListNode partition(ListNode head, int x) {
       ListNode small=new ListNode(0);
       ListNode smallHead=small;
       ListNode large=new ListNode(0);
       ListNode largeHead=large;
       ListNode cur=head;
       while(cur!=null){
           if(cur.val<x){
               small.next=cur;
               small=small.next;
           }else{
               large.next=cur;
               large=large.next;
           }
           cur=cur.next;
       }
       large.next=null;
       small.next=largeHead.next;
       return smallHead.next;
    }
}

😎相交链表 OJ链接

大体思路:采用双指针,设这两个链表的第一个公共结点为node,两个链表的头结点分别为headA和headB,设headA链表的结点数为a,headB链表的结点数为b,两个链表的相交部分的结点数为c.

那么就有,headA到node前,有a-c个结点.headB到node前,有b-c个结点.

创建两个结点引用变量A和B,分别指向headA和headB.

然后让A先遍历headA指向的链表,再遍历headB指向的链表,当A走到node时,总共走了a+(b-c)步.
B先遍历headB指向的链表,再遍历headA指向的链表,B走到node时,总共走了b+(a-c)步.

a+(b-c)=b+(a-c),此时A和B重合.

有两种情况:

😁(1)两个链表有公共尾部:也就是c>0,A和B同时指向node
😁(2)没有公共尾部:也就是
c=0,那么A和B同时指向null

所以,最后返回A即可.

代码:

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode A=headA;
        ListNode B=headB;
        while(A!=B){
            A=(A!=null?A.next:headB);
            B=(B!=null?B.next:headA);
        }
        return A;
    }

在这里插入图片描述

😎判断链表中是否有环 OJ链接

大体思路:利用快慢指针.让快指针走两步,慢指针走一步

那么就出现一个问题:为什么非得是快指针走两步,慢指针走一步呢?可不可以快指针走三步,四步,N步…,慢指针走一步?

原因是快指针走三步,慢指针走一步这两个指针可能永远都不会相遇.因为有环,而且存在速度差,所以是追及相遇问题.

假设链表带环,那么两个指针最后都会进入环,快指针先进环,慢指针后进环,可能就和快指针相遇了,最差情况下两个指针之间的距离正好是环的长度,此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况.因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指针的,也就是相遇.

我们来举个例子说明一下为什么不能是快指针走三步,慢指针走一步:

假设:我们让快指针每次走三步,慢指针每次走一步,那么肯定是**快指针先进环,慢指针后进环.**假设这两个指针的位置如图所示:

在这里插入图片描述

此时按照三倍的速度差来移动,那这两个指针是永远不会相遇的.

只有快指针每次走两步,慢指针每次走一步才可以,因为环的最小长度为1,即使套圈了,这两个指针也在相同的位置.

代码:

public boolean hasCycle(ListNode head) {
        ListNode fast=head;
        ListNode slow=head;
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(fast==slow){
                return true;
            }
        }
        return false;
    }

😎环形链表二 OJ链接

大体思路:

在这里插入图片描述

从起始结点开始,fast每次走两步,slow每次走一步,那么在环中相遇后,由于fast的速度是slow的2倍,所以fast走的路程也是slow的2倍.

fast走的路程是:X+C+(C-Y)
slow走的路程是:X+(C-Y)

所以得到表达式:X+C+(C-Y)=2*(X+(C-Y))
最后得出XY==

所以在得出相遇结点后,让fast或者slow回到起始结点,以相同的速度走(每次走一步),那么再次相遇的结点就是入口结点.

代码:

public ListNode detectCycle(ListNode head) {
        ListNode fast=head;
        ListNode slow=head;
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(fast==slow){
                break;
            }
        }
        if(fast==null||fast.next==null){
            return null;
        }
        slow=head;
        while(fast!=slow){
            fast=fast.next;
            slow=slow.next;
        }
        return fast;
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值