当前位置: 首页 > article >正文

Java-数据结构-链表-高频面试题(2)

第一题:分隔链表

📚 思路提示:这题看似需要将很多的结点进行移动转换,但其实用不着这么麻烦。该题的要求是"要我们将小于x的结点都放在左边,大于等于x的结点都放在右边",那么就说明左边是一类,右边是一类,它们都可以通过某种条件筛选出对应的结点。

所以我们就可以新建两个链表,一个用于存储"值小于x的结点",另一个用于存储"值大于等于x的结点",最后再将新表尾的next置为null,返回新表头就好了!

⭐ 图解

📖 代码示例

class Solution {
    public ListNode partition(ListNode pHead, int x) {
        ListNode left = new ListNode();
        ListNode right = new ListNode();
        ListNode head = left;
        ListNode end = right;
        while(pHead != null){
            if(pHead.val < x){
                left.next = pHead;
                left = left.next;
            }else{
                right.next = pHead;
                right = right.next;
            }
            pHead = pHead.next;
        }
        right.next = null;
        left.next = end.next;
        return head.next;
    }
}

第二题:合并两个有序链表

📚 思路提示:这题还是比较简单的,思路就和上一道题大同小异~

这题与"分隔链表"有一个共同点就是,都可以创建新链表,通过将题中已给的链表进行遍历,然后将符合条件的结点插入新链表,直到将所有结点都插入新链表~就能够得出我们想要的答案了

而这题显而易见,我们只需要在两者都不为空的前提下遍历两个有序链表,对两个结点的val进行比较,将val小的结点插入新链表,然后继续遍历~最后就能得到两者结合的升序链表啦~

⭐ 图解

📖 代码示例

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null){
            return list2;
        }
        if(list2 == null){
            return list1;
        }
        ListNode node = new ListNode();
        ListNode list = node;
        while(list1 != null && list2 != null){
            if(list1.val > list2.val){
                list.next = list2;
                list2 = list2.next;
            }else if(list1.val <= list2.val){
                list.next = list1;
                list1 = list1.next;
            }
            list = list.next;
        }
        if(list1 == null){
                list.next = list2;
        }if(list2 == null){
                list.next = list1;
        }
        return node.next;
    }
}

第三题:合并 K 个升序链表

📚 思路提示:此题稍微复杂,并且方法繁多,但是我们上道题已经搞清楚了最核心的基础操作

合并两个升序链表

而随之我们就需要围绕着这八个字开始解题了~其实最简单的暴力解法就非常简单了,就是将这个链表数组全部遍历,依次将第一个链表和第二个链表结合成有序链表传回,再用此链表与第三个链表结合成有序链表传回...以此类推,就能解决该题

没错,但是太暴力了!!并且如果这么解题,也对不起它困难的难度评级,那么让我们先考虑一下为何它的时间复杂度如此之高

⭐ 图解(1)

从大小为3的链表数组或许看不出什么,但是它的时间损耗是"从链表出现"到"链表数组遍历结束"之间重复进行并且一直存在的,而且它遍历数组需要经历n次(链表数组大小)操作,则第一个和第二个链表需要参与排列n次,第三个需要参与n - 1次...所以当链表数组特别大的时候,这个损耗便不能再忽略。于是吸取这个问题的教训,加以思考:

时间复杂度过高的原因是"链表参与的后续运算过多,排列次数多影响该问题"。

排序好的链表参与后续运算我们是无法改变的,我们只能尽量减少这种损耗,那不妨我们从排列次数入手,我们现在的排列效率为一次只排列第一对儿,这样导致我们大多数的数据都卡在第一个列表,并且参与n多次计算。那此时我们可以将链表数组进行一直分组的操作,同时将被分到一组的链表合并,以此递归,这样就解决了排列次数过多和数据参与多余计算过多的问题了:

(采用分治的方法,通过不断递归取 "mid" 的方法,将 k 个链表分为2组、4组、8组...直到分为 k/2 组(每组两个链表)跳出递归,然后开始不断结合链表,将 k 个链表结合为 k/2 个链表,再变成 k/4 、k/8...直到变成一整个链表,返回该链表)

⭐ 图解(2)

📖 代码示例

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode list = grouping(lists,0,lists.length - 1);
        return list;
    }
    public ListNode grouping(ListNode[] lists,int l,int r){
        if(l == r){
            return lists[l];
        }
        if(l > r){
            return null;
        }
        int mid = (l + r) >> 1;
        return Merge(grouping(lists,l,mid) ,grouping(lists,mid + 1,r));
    }
    public ListNode Merge(ListNode cur1,ListNode cur2){
        if(cur1 == null){
            return cur2;
        }
        if(cur2 == null){
            return cur1;
        }
        ListNode head = new ListNode();
        ListNode cur = head;
        while(cur1 != null && cur2 != null){
            if(cur1.val < cur2.val){
                head.next = cur1;
                head = head.next;
                cur1 = cur1.next;
            }else {
                head.next = cur2;
                head = head.next;
                cur2 = cur2.next;
            }
        }
        if(cur1 == null){
            head.next = cur2;
        }else if(cur2 == null){
            head.next = cur1;
        }
        return cur.next;
    }
}

第四题:两数相加

📚 思路提示该题的核心也是同时遍历两个链表,将对应单位的和对10取余后插入新链表~并且要记得将计算后结果大于等于10的情况,要在下一位进位哦~,还要记得将进位数置0。

也就是说,如果某一时刻,取出的两个数字分别为 a 和 b ,进位值为 num,那么这位的数字就是 (a + b + num) % 10 ,新的进位制为 (a + b + num) / 10。

(注意:每位数字都是按照 逆序 的方式存储的,即 2->4->3 = 342)

📖 代码示例

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode cur = new ListNode();
        ListNode head = cur;
        //代表进位数
        int num = 0;
        while(l1 != null || l2 != null){
            int x = 0;
            int y = 0;
            if(l1 != null){
                x = l1.val;
            }
            if(l2 != null){
                y = l2.val;
            }
            int sum = x + y + num;
            //进位数重置
            num = 0;
            if(sum < 10){
                cur.next = new ListNode(sum);
            }else {
                cur.next = new ListNode(sum % 10);
                num++;
            }
            cur = cur.next;
            if(l1 != null){
                l1 = l1.next;
            }
            if(l2 != null){
                l2 = l2.next;
            }
        }
        //防止最后进位
        if(num == 1){
            cur.next = new ListNode(num);
        }
        return head.next;
    }
}

第五题:删除链表倒数第 N 个结点

📚 思路提示想要学会删除链表倒数第N个结点,我们首先得要能够找到链表的倒数第N个结点,以及知道如何删除链表中的一个结点

找到链表的倒数第N个结点:

这个思路其实并不难,既然已经给定你N了,那么就很好找了。

比如此时我们想找到链表中的倒数第2个结点(N = 2),那我们先定义两个结点 cur1 = head 和 cur2 = head,然后我们先让 cur1 走 (N - 1)步,也就是1步,然后两结点同时往后走,等到 cur1 走到最后一个结点时,cur2 此时的位置就是倒数第2个结点了~

⭐ 图解(1)

删除链表中的一个结点:

这点并不难,我们在之前模拟实现单链表的时候就写过类似的函数,直接看图~

⭐ 图解(2)

那么将这两个小问题结合起来就能够轻松解决这个问题了:

⭐ 图解(3)

📖 代码示例

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode cur0 = head;
        ListNode cur1 = head;
        while(n-- > 0){
            cur0 = cur0.next;
            //代表删除第一个数据
            if(cur0 == null){
                return head.next;
            }
        }
        //找到倒数第N个结点的前一个结点
        while(cur0.next != null){
            cur0 = cur0.next;
            cur1 = cur1.next;
        }
        //跳过倒数第N个结点
        cur1.next = cur1.next.next;
        return head;
    }
}

第六题:两两交换链表中的节点

📚 思路提示:题目看似简单,其实还是有些弯绕的。看到这题的时候或许大家想弄两个结点,然后将 cur2.next = cur1,cur1.next = cur3...等等步骤,但实际上,直接通过第一个和第二个结点就开始对整个链表进行操作,会有很多潜在的问题:

📕 交换 cur1,cur2 时,还需要创建cur3,cur4用来帮助cur1,cur2交换以及向后移动,而从第一个和第二个结点开始,就有可能出现形如:1->2->3->4 ==》2->1->4->3 的情况,也就代表我们的第一个结点要指向第四个结点,因此可能越界访问

📕 从 cur1和cur2开始,无法应对链表长度为2的情况,因为只有链表长度 >= 3,才能够进入循环

所以虽然说,我们可以对"访问第四个结点是否越界"使用条件语句解决,也可以单独将"链表长度为2"的情况解决,但却显得太麻烦了~所以我们换一个思路:

以上问题的出现是因为"访问越界""进入循环的链表长度 >= 3"那我们不妨设计一个 cur0 在 cur1 前面,不再使用 cur1 去访问 cur4,而是通过 cur0 先去调换 cur1 和 cur2 的位置,然后进入下次交换时,"1"已经变成了第二个结点,那么此时再用它去与第四个结点相连,便解决了"访问越界"的问题。

而同样的,因为我们将起始点往回退了一步,这样链表长度为2的链表也能够进入我们的循环进行结点交换了~

⭐ 图解

📖 代码示例

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode cur0 = new ListNode(0,head);
        ListNode cur1 = head;
        ListNode Head = cur0;
        while(cur1 != null && cur1.next != null){
            ListNode cur2 = cur1.next;
            ListNode cur3 = cur2.next;
            //交换 1 和 2
            cur0.next = cur2;
            cur2.next = cur1;
            cur1.next = cur3;
            //移动 cur0 和 cur1 
            cur0 = cur1;//第0位 移动到 第2位                 
            cur1 = cur3;//第1位 移动到 第3位
        }
        return Head.next;
    }
}

左边是改良过的,右边是之前的,可以看出代码量和整洁程度差的不是一星半点~

第七题:排序链表

📚 思路提示:还记得我们上面做了一道"合并K个升序链表"的题嘛?那个题和这个题还是有些相似之处的,或许说我们可以用近乎一样的思路去解决它~

首先,我们知道,想通过创建新链表,用迭代的方法将两个链表组合成一个新的有序链表,就要求两个链表也必须是有序链表。

再回看我们这道题,看起来杂乱无章,根本不是有序链表。但我们看到的仅仅是"4->2->1->3"不是有序链表,不妨让我们再仔细看看:将"4->2->1->3"分为"4->2","1->3",然后再将"4->2","1->3"分为"4","2","1","3",这回我们再看~每个链表只有一个结点,这样就能称得上是有序了吧~

接着这个思路,我们就要知道如何将链表从中隔断,没错,就是上一篇练习题中所学习过的"快慢指针"方法,我们可以创建一个"从中隔断链表"的函数,然后通过"归并排序"的方法,将其分成一个个结点,最后再通过"合并两个有序链表"的函数得到一个完整的有序链表~

⭐ 图解

📖 代码示例

class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        // 找到中间节点
        ListNode mid = getMid(head);
        ListNode right = mid.next;
        mid.next = null;
        // 递归排序左右子链表
        ListNode left = sortList(head);
        right = sortList(right);
        // 合并两个有序链表
        return merge(left, right);
    }

    private ListNode getMid(ListNode head) {
        ListNode slow = head;
        ListNode fast = head.next;
        while (fast!= null && fast.next!= null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    private ListNode merge(ListNode cur1, ListNode cur2) {
        ListNode Head = new ListNode();
        ListNode pHead = Head;
        while (cur1!= null && cur2!= null) {
            if (cur1.val < cur2.val) {
                Head.next = cur1;
                cur1 = cur1.next;
            } else {
                Head.next = cur2;
                cur2 = cur2.next;
            }
            Head = Head.next;
        }
        if (cur1!= null) {
            Head.next = cur1;
        } else {
            Head.next = cur2;
        }
        return pHead.next;
    }
}

那么这次关于链表的练习题就为大家分享到这里啦,作者能力有限,如果有讲得不清晰或者不正确的地方,还请大家在评论区多多指出,我也会虚心学习的!那我们下次再见哦~


http://www.kler.cn/a/473262.html

相关文章:

  • Goldendb数据库dbtool命令介绍
  • web服务器架构,websocket
  • 使用高云小蜜蜂GW1N-2实现MIPI到LVDS(DVP)转换案例分享
  • Ollama + Openwebui 本地部署大型模型与交互式可视化聊天
  • 自动化脚本本地可执行但是Jenkins上各种报错怎么解决
  • Linux(上):基本知识篇
  • 常用的AT命令,用于查看不同类型的网络信息
  • SQLite 调试与性能优化指南
  • 去掉el-table中自带的边框线
  • 我的前端面试笔记(React篇)
  • NRF24L01模块STM32通信-通信初始化
  • js适配器模式
  • 《Spring Framework实战》3:概览
  • Hybrid A*算法-KinodynamicAstar::estimateHeuristic
  • LLM 大语言模型学习记录
  • js可不使用document直接根据id获取id元素
  • 无人机培训机构模拟考试系统技术详解
  • 让生命科学数据为数字时代服务
  • ATmega328P是一款基于AVR架构的高性能、低功耗8位微控制器
  • ajax与json