【Java数据结构-链表】常见链表OJ算法题

前言

本篇博客用来记录链表中间常见的OJ算法题,所有的解题思路都采用时间复杂度最小即O(n)来解答,一道题解法有很多,希望本篇博客对你有所启发。

1.删除链表中等于给定值 val 的所有节点。

OJ链接-删除链表中等于给定值 val 的所有节点

在这里插入图片描述
题目要求将所有出现的val值删除,删除节点就是修改前一个节点的next值为需要删除节点的下一个节点即可,那我们怎么去拿到前一个值呢?我们常见的方法就是定义一个prev指针,用来记录当前节点的前一个节点,当前节点用来对比val值是否相同,如果val相同,前一个prev节点的next进行修改,之后循环遍历到最后就可以完成所有的删除操作。这种方法之遍历了一遍,时间复杂度:O(n)

    public ListNode removeElements(ListNode head, int val) {
        if(head == null){
            //先判断head是否为空,如果为空,直接发挥空指针head,如果不判断,后续代码会报错
            return head;
        }
        ListNode cur = head.next;//定义当前指针cur指向头节点下一个
        ListNode prev = head;//定义cur前一个节点指向第一个头
        //cur要走到最后为空,才遍历完整
        while(cur != null){
            if(cur.val == val){
                //如果cur的val等于val,让prev的next指向cur的next(删除cur指向的节点)
                prev.next = cur.next;
                //之后cur往后走一步
            }else{
                //如果不相等,prev往后走一步
                prev = prev.next;
                //cur往后走一步
            }
            //if和else里面都要cur往后走一步
            cur = cur.next;
        }
        //但是这个程序有一个漏洞,我们没有去判断第一个head的val是否就是val,如果是,
        //直接改变head的指针,往后走一步即可,如果少了这个判断,就会考虑不周
        if(head.val == val){
            head = head.next;
        }
        //最后返回head头节点
        return head;

这里需要注意,最后的head判断是否是val是十分有必要的,如果不去判断,就会忽略头节点是val的情况,就通过不了测试用例,如下图
在这里插入图片描述
当我们修改之后,就涵盖了所有的情况…
在这里插入图片描述

2.反转一个单链表

OJ链接-反转一个单链表
在这里插入图片描述
本题要求逆置链表所有节点,这其实是个头疼的问题,我们知道逆置,就直接把每个节点的next改成上一个节点的地址就行,但是链表只能从前往后进行遍历,不能往回走,并且我们要做到时间复杂度为O(n),只遍历一遍,该怎么办呢?竟然不能往后走,那我们记录一下走过来前一个节点的地址不就好了!我们直接让当前的cur节点的next指向prev指的前一个节点,不就完成了逆置吗?但是又有一个问题:cur的next改变了,那我们怎么继续往后遍历?如果你写成cur = cur.next;就是错的!我们上一步已经把cur的next改成了前一个的地址~为了解决这个问题,我们就在定义一个指着指向cur的下一个节点呗,就叫做curNext,当我们修改完cur的next指向prev之后,我们只需要让cur = curNext就能继续往后走了

public ListNode reverseList(ListNode head) {
        //首先判断head是否为空,如果为空直接返回head空指针
        if(head == null){
            return head;
        }
        //如果不判断head是否为空,这里就会对head空指针解引用调用next报错
        ListNode cur = head.next;//定义cut指向当前需要修改方向的节点
        //逆置之后,原本的头节点就变成了末尾,
        //如果放到最后修改next为空,需要整个遍历一遍,所以我们刚开始就可以把他置空
        head.next = null;
        //循环条件:当前的cur节点指向空,表示循环完毕
        while(cur != null){
            //定义cur下一个节点,我们放在while循环里面定义,就能很好规避
            //如果cur==null的情况,也就是只有一个节点的情况
            ListNode curNext = cur.next;
            //修改当前cut.next指针指向头节点
            cur.next = head;
            //头节点更新,变成新链接上去的cur
            head = cur;
            //cur节点更新成curNext节点
            cur = curNext;
            //curNext节点,通过循环更新到下一个
        }
        //最后返回新的头节点
        return head;

    }

运行结果如下
在这里插入图片描述

3.链表的中间节点

OJ链接-链表的中间节点
在这里插入图片描述
这道题需要你返回链表的中间节点,如果是奇数个,直接返回中间即可,如果是偶数个,返回中间节点的第二个节点。最简单的思路就是通过遍历整个链表统计一下有多少个节点,之后直接除以2,再去遍历到特定的位置拿到中间节点,但是时间复杂度高了…我们有没有办法,在时间复杂度只有O(n)的前提下,只遍历一遍,就能拿到中间节点呢?是有办法的,这道题要涉及到一个算法思想——快慢指针
快慢指针:定义一个fast指针,再定义一个slow指针,我们让fast指针一次走两个节点,我们让slow指针一次只走一个节点,这样他们走的路程相比,快的比慢的多一倍,最后我们只要返回slow位置即为中间节点。

在这里插入图片描述

public ListNode middleNode(ListNode head) {
        //定义指着fast和slow,开始都指向head位置
        ListNode fast = head;
        ListNode slow = head;
        //循环结束条件,只要fast指针在奇数个情况下,指向最后一个位置和
        //fast指针在偶数个情况下,指向null位置,都要结束循环
        //注意:这里的循环条件不能互换位置,因为fast是有可能为null的
        //      偶数情况下fast就是null,所以我们要先判断fast!=null,如果成立
        //      就不会判断后面的fast.next!=null的情况
        while(fast != null && fast.next != null){
            //fast指针走两步
            fast = fast.next.next;
            //slow指着你走一步
            slow = slow.next;
        }
        //返回slow位置的节点
        return slow;
    }

运行结果如下:
在这里插入图片描述

4.返回链表的倒数第K个节点

OJ链接-返回链表倒数第K个节点
在这里插入图片描述
这题是个烧脑的问题哦,我们明明知道链表是不可能往回进行遍历的,但是本题就是要我们返回倒数第K个值,并且同样的需求,时间复杂度O(n),该怎么解题呢?
这道题也是一个快慢指针的问题!我们定义一个fast指针,让他先走k-1次,指向k-1次的节点不要动,之后再定义一个slow指针,从head出发,接着让fast和head一起走,当fast走到最后节点,这个时候slow指向的节点就是倒数第K个节点。我们画个图来看看
在这里插入图片描述
那他的原理是什么呢?其实就是我们自己去设置了一个不变长度的尺子,当我们拿着这个尺子平移,他之间的差值也是不会改变的,而之间的差值就是k-1,我么也可以设置成k个差值,只不过要让fast指向null再停止也是一样的!

public int kthToLast(ListNode head, int k) {
        //定义两个指针fast和slow指向head
        ListNode fast = head;
        ListNode slow = head;
        //让fast先走到k-1的位置
        for(int i = 0;i<k-1;i++){
            fast = fast.next;
        }
        //让fast和slow一起走,直到fast走到最后一个节点
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        //返回slow下标的val
        return slow.val;
    }

运行结果如下:
在这里插入图片描述
但是,这样一个解法必须有一个前提,那就是k必须要合法!虽然本题里面说了,不考虑k的合法性,但是你实际去使用的时候,我就是向判断k的合法性,会存在k不合法情况,该怎么判断呢?会有人想,加一个条件if(k<0 || k>size()){return -1;}按理来说是没错,但是size()这个函数该怎么写?并且目前时间复杂度已经是O(n)了,你要求链表的长度就必须要再去遍历,所以我们再加一个要求,那就是不能去遍历链表!你该怎么办呢?
如果不去判断,当你的k输入大于链表长度的值,代码就会报错,我们手动输入k = 6
在这里插入图片描述
怎么去改?其实也很简单
在这里插入图片描述
我们再次输入k = 6,就可以通过测试用例在这里插入图片描述
正常去提交也是没有问题的
在这里插入图片描述

5.合并两个有序链表

OJ链接-合并两个有序链表在这里插入图片描述
本题让我们合并两个有序的链表,返回合并之后链表的头节点,思路很简单,因为都是有序的,我们可以分别去遍历每一个链表的val,拿到val去对比,如果是小的,那就把它拿出来,接着对比下一个,把下一个小的再连接到上一个对比后拿出来小的节点后面,就这样一直对比到一个链表结束,直到某一个链表都走完,之后把另一个剩余的节点,都连接到最后即可,因为剩余的肯定都是大于之前所有的节点的。这样时间复杂度就是走完了两条链表O(m+n)

public ListNode mergeTwoLists(ListNode head1, ListNode head2) {
        //我们申请一个新的节点用来当火车头,之后里面放-1,用来往后面挂小的节点
        ListNode headNode = new ListNode(-1);
        //让cur在遍历走动,不然最后就找不到头了
        ListNode cur = headNode;
        //只要两个链表都不为null,就表示没有走到最后
        while(head1 != null && head2 != null){
            if(head1.val < head2.val){
                //如果小于,就让小的给到cur的后面,之后head1往后走
                cur.next = head1;
                head1 = head1.next;
                //cur往后走,因为两种情况都要往后走,所以写在循环外面
            }else{
                cur.next = head2;
                head2 = head2.next;
            }
            cur = cur.next;
        }
        //判断哪一个不为空,直接让不为空的连接到cur的后面
        if(head1 != null){
            cur.next = head1;
        }else{
            cur.next = head2;
        }
        //最后返回的是头节点的下一个节点,头节点是我们自己申请用来
        //悬挂的节点,没有实际意义
        return headNode.next;
    }

运行结果如下:
在这里插入图片描述

6.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。

OJ链接-编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。
在这里插入图片描述
本道题是牛客网上的一道题,这道题其实并不难,但是他没有给测试用例,再加上很容易忽略其中的情况,导致无法通过全部的测试用例,所以通过率并不高,都没有超过20%。题目要求,小于x的要放到前面,并且不能改变原本就存在的顺序,我们看下面的图:
在这里插入图片描述
其实思路并不难,我们只要把他分成两个组,一个用来记录小于x的用prevStart做为头节点,prevEnd作为尾节点,一个用来记录大于x的用posStart作为头节点,posEnd做为尾节点,之后把他们拼接到一起就可以了。
在这里插入图片描述

public ListNode partition(ListNode pHead, int x) {
        //先判断给的pHead是否为空,如果为空直接返回pHead空指针
        //否则后续会报错
        if(pHead == null){
            return pHead;
        }
        //定义四个指针,分别去记录两头两尾
        ListNode prevStart = null;
        ListNode prevEnd = null;
        ListNode posStart = null;
        ListNode posEnd = null;
        //循环结束条件就是当pHead遍历完整结束
        while(pHead != null){
            //小于x的放一起
            if(pHead.val < x){
                //需要判断是否是第一个
                if(prevStart == null){
                    //如果是第一个直接头尾相等等于pHead
                    prevStart = prevEnd = pHead;
                }else{
                    //如果不是第一个,进行尾插
                    prevEnd.next = pHead;
                    prevEnd = prevEnd.next;
                }
            }else{
                //大于x的放一起
                if(posStart == null){
                    //如果是第一个直接头尾相等等于pHead
                    posEnd = posStart = pHead;
                }else{
                    //如果不是第一个,进行尾插
                    posEnd.next = pHead;
                    posEnd = posEnd.next;
                }
            }
            //每一次进行完pHead都要往后走,所以放在了外面
            pHead = pHead.next; 
        }
        //接下来要连接两个链表,但是一定要考虑到!
        //如果都小于x或者都大于x的情况,如果不考虑本题就过不了

        //如果小于x的链表都为空了, 直接返回大于x的链表
        if(prevStart == null){
            return posStart;
        }
        //小于x的链表不为空
        //连接prevEnd和posStart
        prevEnd.next = posStart;
        if(posEnd!=null){
            //这里需要注意!大于x的链表最后的next不一定就是null,
            //我们要手动给他置空
            posEnd.next = null;
        }
        //最后返回头节点
        return prevStart;
        
    }

运行结果如下:
在这里插入图片描述

7.链表的回文结构

OJ链接-链表的回文结构
在这里插入图片描述
本题需要你判断链表是否是回文结构,回文结构就是,正这读和反着读,都是一样的,并且,要求时间复杂度为O(n),空间复杂度为O(1),就表示不能创建其他的结构,只能再原来的链表上面进行修改!单链表的性质就是只能从前往后,这个应该怎么做呢?我们前面接触了,反转链表的算法,我们可以尝试,反转后面的一部分链表,之后让指针从头和尾巴依次去比较,只要两个指针重合了,就表示这个链表是回文结构。

public boolean chkPalindrome(ListNode head) {
        // write code here
        if(head == null){
            return true;
        }
        //先找到中间节点
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        //此时的slow就是中间节点
        //开始进行反转,要反转cur节点,这个节点是slow的下一个
        ListNode cur = slow.next;
        while(cur != null){
            ListNode curN = cur.next;
            cur.next = slow;
            slow = cur;
            cur = curN;
        }
        //反转完毕
        //再定义指针headNode指向头节点,让headNode和slow同时走
        //相遇就是回文
        ListNode headNode = head;
        //只要headNode和slow没有相遇,就一直循环
        while(headNode!=slow){
            //如果他们val不相等,直接return false
            if(headNode.val!=slow.val){
                return false;
            }
            //这里很细节!我们正常不加这个条件,无法排除偶数个情况,
            //偶数个情况会一直循环,他们无法相遇
            if(headNode.next == slow){
                return true;
             }
            //只要是相等的就都走一步
            headNode = headNode.next;
            slow = slow.next;
        }
        //都走完return true
        return true;
    }

这里我们需要注意,当节点个数是偶数个的时候,因为我们改变完指向之后,第一个slow指向的next是没有改变的,还是指向后一个节点的地址,我们改变的只是slow后面的一个节点的next,让他指向了slow,所以,偶数个情况下,我们画图会知道,他们顺着next走不可能相遇,因为后面已经成环了!但是奇数情况下,不会发生这个问题
在这里插入图片描述

所以我们需要单独去判断偶数个情况,我们就加入了一个if条件,加的位置也很重要,只有当前面的val都相等,情况下,再去判断只要满足,当前节点的下一个是slow节点,就可以表示他们俩相遇了!
在这里插入图片描述
如果我们不添加if条件,就会出错
在这里插入图片描述
添加完之后,代码逻辑正确,运行成功!
在这里插入图片描述

8.输入两个链表,找出它们的第一个公共结点

OJ链接-输入两个链表,找出他们的第一个公共节点
在这里插入图片描述
如何找到他们的公共节点?我们知道,这两个链表后续相同公用的部分,长度肯定是一样的,长度不一样的地方,就是相交之前的部分,那我们,提前让长的链表,走一定的长度,之后只要剩余的长度相,接着走下去,就一定能够相遇,此时的相遇节点就是我们要找的节点那么要提前走多少个节点呢?没错就是差值,所以我们先求出两个链表的差值,之后让长的走差值个节点,再一起走,直到相遇返回任意一个节点即可。

 //size()方法用来求出每一个链表的长度
    public int size(ListNode head){
        ListNode cur = head;
        int count = 0;
        while(cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        //首先求出两个链表的差值
        int sizeA = size(headA);
        int sizeB = size(headB);
        int len = sizeA - sizeB; 
        if(len <= 0){
            //sizeB长
            len = -len;
            //让B链表走len个节点
            for(int i = 0;i<len;i++){
                headB = headB.next;
            }
        }else{
            //sizeA长
            //让A走len个节点
            for(int i = 0;i<len;i++){
                headA = headA.next;
            }
        }
        //之后两个链表一起走,直到相遇,就是相交节点
        //这里要考虑到,如果两个链表都没有相交,那他会一直走下去
        //就会报错,所以加条件判断前提他们都不能null,如果为null跳出
        //我们就只要判断一个长度即可,因为长的链表起始位置是他们的差值
        //所以走的长度都是相等的,只要一个为null,另一个也就是null
        while(headA != headB && headA != null){
            headA = headA.next;
            headB = headB.next;
        }
        //判断如果其中一个是null,就说明没有交点
        if(headA == null){
            return null;
        }
        //这里就说明是走到交点相遇了
        return headA; 
    }

代码运行结果如下:
在这里插入图片描述

9.给定一个链表,判断链表中是否有环

在这里插入图片描述
本题需要判断是否有环,乍一看真不太好有思路,其实这道题也是一个经典的快慢指针问题!我们想啊,一个fast快指针,一个slow慢指针,正常情况下是不可能相遇的,可是成环了就会相遇!
但是快指针fast应该多快?慢指针slow应该多慢?其实是有说法的,我们一般fast为2,slow为1,我们一般都让他们相差为1,如果不为1,就会存在永远不会相交的情况,我们来看下面的图
在这里插入图片描述
在这种情况下,即使成环了slow和fast完美错过!
我们fast和slow相差为1,每次的走一步,fast和slow之间的差距就缩小了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;
    }

运行结果如下:
在这里插入图片描述

10.给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL

OJ链接-给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
在这里插入图片描述

这道题十分有趣,可以把它看作是一个追击问题,我们看下面的题解
在这里插入图片描述
所以一样是用到了快慢指针,我们就只需要让他们相遇之后,让一个指针从头开始,之后再让指针从相遇开始一直走下去,他俩相遇的地方就是环的入口节点

 public ListNode detectCycle(ListNode head) {
        //定义fast和slow快慢指针
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                //相遇成环
                break;
            }
        }
        //跳出循环一种情况是fast指针走到了结尾,那就表示没相遇,无环
        if(fast == null || fast.next == null){
            return null;
        }
        //另外的情况就是相遇了跳出,定义startNode节点从头开始走
        ListNode startNode = head;
        while(startNode != slow){
            //只要不相遇就一直走
            startNode = startNode.next;
            slow = slow.next;
        }
        //相遇之后 返回任意一个节点位置,就是环的入口节点
        return startNode;
    }

运行结果如下:
在这里插入图片描述

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值