您当前的位置:首页 > 电脑百科 > 程序开发 > 算法

开启算法之路,还原题目,用debug调试搞懂每一道题

时间:2021-02-26 09:58:19  来源:  作者:

文章简述

在本次的文章中,按照个人的刷题计划,会分享关于链表的 3 道简单级别的算法题(可是依然感觉不简单)

但是不要紧,从本篇文章开始分享的算法题个人都会把关于这道题的全部代码写出来,并用debug的形式,分解每一步来整理出来。

通过还原题目场景,用 debug 调试的方式去分析,印象更加深刻些。

本篇文章中共有 3 道题目。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

一,合并两个有序链表

开启算法之路,还原题目,用debug调试搞懂每一道题

 

1.1 题目分析

看到这道题的时候,第一反应就是先将两个链表合并,然后再排序。嗯。。。不用想,绝对的暴力写法。

或者是循环两个链表,然后两两相比较,就像:

for(){
  for(){
    if(){}
  }
}

好吧,其实这道题精华在于可以使用递归,这个。。。来个草图简单描述下。

第一步:

两个链表的首节点进行比较

开启算法之路,还原题目,用debug调试搞懂每一道题

 

两个节点相等,则使 L2 链表【1】,和 L1 链表的【2】进行比较

注意:

L1节点【1】和L2节点【1】比较完成后,需要修改1.next指针,以指向它的下个节点。

第二步:

现在我们获取到了 L2 链表【1】,那它的 next 指向谁?也就是 L2 链表【1】去和 L1 链表的【2】进行比较。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

比较完成后,L2 链表【1】的 next 就指向了 L1 链表【2】,接着以此类推。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

L2 链表【3】去和 L1 链表【4】比较。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

最后 L1 链表【4】和 L2 链表【4】比较。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

全部比较完成后,整个链表就已经排序完成了。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

递归的方式就在于,两两节点进行比较,当有一个链表为null时,表示其中一个链表已经遍历完成,那就需要终止递归,并将比较结果进行返回。

可能只是单纯的画图并不好理解,下面用代码 debug 的方式去分析,还请耐心往下看。

 

1.2 代码分析

按照题意需要先创建 2 个单链表,具体的创建方式可以参考本人的第一篇文章《手写单链表基础之增,删,查!附赠一道链表题》。不多说,先初始化节点对象。

class ListNode {
    /**
     * 数据域
     */
    int val;
    /**
     * 下一个节点指针
     */
    ListNode next;

    ListNode() {
    }
    ListNode(int val) {
        this.val = val;
    }
    ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
    @Override
    public String toString() {
        return "ListNode{" +
                "val=" + val +
                '}';
    }
}

定义新增链表方法。

 public ListNode add(ListNode node) {
        // 1,定义辅助指针
        ListNode temp = nodeHead;
        // 2,首先判断当前节点是否为最后节点
        if (null == temp.next) {
            // 当前节点为最后节点
            temp.next = node;
            return nodeHead;
        }
        // 3,循环遍历节点,如果当前节点的下个节点不为空,表示还有后续节点
        while (null != temp.next) {
            // 否则将指针后移
            temp = temp.next;
        }
        // 4,遍历结束,将最后节点的指针指向新添加的节点
        temp.next = node;
        return nodeHead.next;
    }

接着创建 2 个链表。

 /**
         * 定义L1链表
         */
        ListNode l11 = new ListNode(1);
        ListNode l12 = new ListNode(2);
        ListNode l13 = new ListNode(4);

        MergeLinkedList l1 = new MergeLinkedList();
        l1.add(l11);
        l1.add(l12);
        /**
         * 返回新增完的L1链表
         */
        ListNode add1 = l1.add(l13);

        /**
         * 定义L2链表
         */
        ListNode l21 = new ListNode(1);
        ListNode l22 = new ListNode(3);
        ListNode l23 = new ListNode(4);

        MergeLinkedList l2 = new MergeLinkedList();
        l2.add(l21);
        l2.add(l22);
        /**
         * 返回L2链表
         */
        ListNode add2 = l2.add(l23);

我们先把上述图中使用递归的代码贴出来。

 public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        /**
         * 如果L1链表为null,终止递归
         */
        if (l1 == null) {
            return l2;
        }
        if (l2 == null) {
            return l1;
        }
        /**
         * 按照图中的描述,两两比较链表的节点
         */
        if (l1.val <= l2.val) {
            /**
             * L1的节点比L2的小,按照图中就是需要比较L1链表的下个节点
             * l1.next 就是指当比较出节点大小后,需要修改指针的指引,将整个链表全部串联起来
             */
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            /**
             * 同理,与上个if判断一样
             */
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

1.3 debug 调试

1.3.1 L1 链表已经创建完成,同理 L2 也被创建完成。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

1.3.2 比较两个链表的首节点

开启算法之路,还原题目,用debug调试搞懂每一道题

 

注意:

l1.next是指向递归方法的,也就是上图中我们描述的l1链表【1】指向了l2链表【1】,但是L2链表【1】又指向谁?开始进入递归

1.3.3 如上图,开始比较 L2 链表【1】与 L1 链表的【2】

开启算法之路,还原题目,用debug调试搞懂每一道题

 

1.3.4 比较 L1 链表【2】与 L2 链表【3】

开启算法之路,还原题目,用debug调试搞懂每一道题

 

1.3.5 后面操作是一样的,下面就直接展示最后两个节点比较

开启算法之路,还原题目,用debug调试搞懂每一道题

 

这里已经到最后两个节点的比较。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

这个时候 L1 链表先遍历完成,需要终止递归,返回 L2 链表。为什么返回 L2 链表?直接看图。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

因为最后一步比较的是 L1 链表【4】和 L2 链表【4】,也就是说 L2 链表【4】是最后的节点,如果返回 L1 链表,那 L2 链表【4】就会被丢弃,可参考上面图解的最后一张。

重点来了!!!

重点来了!!!

重点来了!!!

L1 链表已经遍历完成,开始触发递归将比较的结果逐次返回。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

这是不是我们最后 L1 链表【4】和 L2 链表【4】比较的那一步,是不是很明显,l1.next 指向了 l1 的节点【4】,而 L1 节点也就是它最后的节点【4】,和我们那上面图解中最后的结论一样。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

再接着执行下一步返回。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

L2 链表的【3】指向了 L1 链表的【4】

同理,按照之前递归的结果以此返回就可以了,那我们来看下最终的排序结果。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

二,删除排序链表中的重复元素

开启算法之路,还原题目,用debug调试搞懂每一道题

 

2.1 题目分析

初次看这道题好像挺简单的,这不就是个人之前写的第一篇文章里面,删除链表节点吗!

仔细审题其实这道题要更简单些,因为题中已说明是一个排序链表,因此我们只需要将当前节点与下一个节点进行比较,如果相等则直接修改 next 指针即可。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

2.1 代码分析

同样是链表的定义,与上面第一题中的创建是一样的,只不过我们是需要再重新创建一个单链表。

				ListNode l1 = new ListNode(1);
        ListNode l2 = new ListNode(1);
        ListNode l3 = new ListNode(3);
        ListNode l4 = new ListNode(4);
        ListNode l5 = new ListNode(4);
        ListNode l6 = new ListNode(5);

        NodeFun nodeFun = new NodeFun();
        nodeFun.add(l1);
        nodeFun.add(l2);
        nodeFun.add(l3);
        nodeFun.add(l4);
        nodeFun.add(l5);
        ListNode listNode = nodeFun.add(l6);

创建完成后,接着看去重复的代码。

public ListNode deleteDuplicates(ListNode head) {
        /**
         * 定义辅助指针
         */
        ListNode temp = head;
        /**
         * 判断当前节点和下一个节点不能为空,因为是需要将当前节点和下一个节点进行比较的
         */
        while (temp != null && temp.next != null) {
            /**
             * 如果节点值相同
             */
            if (temp.val == temp.next.val) {
                /**
                 * 表示当前节点与下一个节点的值相同,则移动指针
                 */
                temp.next = temp.next.next;
            } else {
                /**
                 * 必须移动指针,否则会产生死循环
                 */
                temp = temp.next;
            }
        }
        return head;
    }
}

2.2 debug 调试

开启算法之路,还原题目,用debug调试搞懂每一道题

 

2.2.1 按照初始化的链表,应该是首节点【1】和第二个节点【1】进行比较。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

不用说两个节点是相等的,那下一步进入 if 判断,就是修改指针的指向。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

此时第二个节点【1】已经没有被 next 指引了,就会被 GC 回收掉。

2.2.2 下一步就是节点【1】和节点【3】进行比较

两个节点不相等,进入 else 将辅助指针移动到下个节点。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

那么剩下的节点判断也都是一样的,我们最后看下打印的结果。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

三,环形链表

开启算法之路,还原题目,用debug调试搞懂每一道题

 

3.1 题目分析

如果这个链表里面有环,其中一个节点必然是被指针指引了一次或者多次(如果有多个环的话)。因此个人当时简单的做法就是遍历链表,把遍历过的节点对象保存到 HashSet 中,每遍历下一个节点时去 HashSet 中比对,存在就表示有环。

而这道题没有设置过多的要求,只要有环返回 boolean 就好。

还有一种巧妙的写法,使用快慢指针的思想。

这种方式大致意思就是说,快慢指针比作龟兔赛跑,兔子跑的快,如果存在环那么兔子就会比乌龟先跑进环中。那么它们就会在某个节点上相遇,相遇了也就说明链表是有环的。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

那么,你们问题是不是来了?这不公平啊,【兔子】本来就比【乌龟】跑的快,那咋兔子还先跑了。

试想,如果它俩都在一个节点上跑,那它们从开始不就是相遇了,因为我们我们是设定如果在一个节点上相遇,表示链表是有环的。所以,这不是“不打自招“了!

比赛开始,这【兔子大哥】有点猛啊,一下跑两个节点。

开启算法之路,还原题目,用debug调试搞懂每一道题

 


开启算法之路,还原题目,用debug调试搞懂每一道题

 

果然,有情人终成眷属,它们相遇了。

3.2 代码分析

这次创建链表的时候,就不能单纯是个单链表了,还得加个环。

 

 		ListNode l1 = new ListNode(3);
        ListNode l2 = new ListNode(2);
        ListNode l3 = new ListNode(0);
        ListNode l4 = new ListNode(-4);

        /**
         * 给主角加个环
         */
        l4.next = l2;

        NodeFun nodeFun = new NodeFun();
        nodeFun.add(l1);
        nodeFun.add(l2);
        nodeFun.add(l3);
        ListNode listNode = nodeFun.add(l4);

那就一起来找环吧。

public boolean hasCycle(ListNode head) {
        ListNode temp = head;
        if(null == head){
            // 为空表示没有环
            return false;
        }
        // 1,set集合保存遍历过的节点,如果新的节点已经在set中,表示存在环
        // 2,使用快慢指针的思想
        // 定义慢指针
        ListNode slow = head;
        // 定义快指针
        ListNode fast = head.next;
        // 循环,只要2个指针不重合,就一直循环
        while(slow != fast){
            // 如果2个指针都到达尾节点,表示没有环
            if(fast == null || fast.next == null){
                return false;
            }
            // 否则就移动指针
            slow = slow.next;
            fast = fast.next.next;
        }
        return true;
    }

3.3 debug 调试

所以,尴尬的事情来了,这玩意 debug 不了啊。如果存在环,那么 while 循环是不会进来的。

那就直接看下结果吧。

开启算法之路,还原题目,用debug调试搞懂每一道题

 

如果把环去掉就是?

开启算法之路,还原题目,用debug调试搞懂每一道题

 

那还用猜?没有光环了肯定。。。

作者:奋进的小样

原文链接:https://www.cnblogs.com/fenjyang/p/14426665.html



Tags:算法   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
前言Kafka 中有很多延时操作,比如对于耗时的网络请求(比如 Produce 是等待 ISR 副本复制成功)会被封装成 DelayOperation 进行延迟处理操作,防止阻塞 Kafka请求处理线程。Kafka...【详细内容】
2021-12-27  Tags: 算法  点击:(1)  评论:(0)  加入收藏
分稀疏重建和稠密重建两类:稀疏重建:使用RGB相机SLAMOrb-slam,Orb-slam2,orb-slam3:工程地址在: http://webdiis.unizar.es/~raulmur/orbslam/ DSO(Direct Sparse Odometry)因为...【详细内容】
2021-12-23  Tags: 算法  点击:(7)  评论:(0)  加入收藏
一、什么是冒泡排序1.1、文字描述冒泡排序是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地...【详细内容】
2021-12-15  Tags: 算法  点击:(16)  评论:(0)  加入收藏
前面文章在谈论分布式唯一ID生成的时候,有提到雪花算法,这一次,我们详细点讲解,只讲它。SnowFlake算法据国家大气研究中心的查尔斯&middot;奈特称,一般的雪花大约由10^19个水分子...【详细内容】
2021-11-17  Tags: 算法  点击:(24)  评论:(0)  加入收藏
基于算法的业务或者说AI的应用在这几年发展得很快。但是,在实际应用的场景中,我们经常会遇到一些非常奇怪的偏差现象。例如,Facebook将黑人标记为灵长类动物、城市图像识别系统...【详细内容】
2021-11-08  Tags: 算法  点击:(32)  评论:(0)  加入收藏
随着注册制的加速推进,新股越来越多,截止到今天A股上市公司的总数高达4500余家,A股一直就是重融资,轻投资的市场,而上市公司发行可转债这种再融资的(圈钱方式)是最能让普通投资者接...【详细内容】
2021-11-05  Tags: 算法  点击:(97)  评论:(0)  加入收藏
导读:在大数据时代,对复杂数据结构中的各数据项进行有效的排序和查找的能力非常重要,因为很多现代算法都需要用到它。在为数据恰当选择排序和查找策略时,需要根据数据的规模和类型进行判断。尽管不同策略最终得到的结果完...【详细内容】
2021-11-04  Tags: 算法  点击:(37)  评论:(0)  加入收藏
这是我在网上找的资源的一个总结,会先给出一个我看了觉得还行的关于算法的讲解,再配上实现的代码: Original author: Bill_Hoo Original Address: http://blog.sina.com.cn/s/bl...【详细内容】
2021-11-04  Tags: 算法  点击:(36)  评论:(0)  加入收藏
每个人都有过这样的经历:打开手机准备回消息或打电话,一看到微信图标右上方的小红点,于是忍不住先打开微信;看完微信,不知不觉又被另一个App牵引,直到关闭手机屏幕才发现自己早已...【详细内容】
2021-11-03  Tags: 算法  点击:(30)  评论:(0)  加入收藏
文丨互联网怪盗团在互联网行业,尤其是在投资人心目中,往往存在一种“算法迷信”或曰“技术迷信”:某公司的广告变现做得好,一定是因为有算法;某公司的云计算业务开展的好,也是因为...【详细内容】
2021-11-03  Tags: 算法  点击:(25)  评论:(0)  加入收藏
▌简易百科推荐
前言Kafka 中有很多延时操作,比如对于耗时的网络请求(比如 Produce 是等待 ISR 副本复制成功)会被封装成 DelayOperation 进行延迟处理操作,防止阻塞 Kafka请求处理线程。Kafka...【详细内容】
2021-12-27  Java技术那些事    Tags:时间轮   点击:(1)  评论:(0)  加入收藏
博雯 发自 凹非寺量子位 报道 | 公众号 QbitAI在炼丹过程中,为了减少训练所需资源,MLer有时会将大型复杂的大模型“蒸馏”为较小的模型,同时还要保证与压缩前相当的结果。这就...【详细内容】
2021-12-24  量子位    Tags:蒸馏法   点击:(9)  评论:(0)  加入收藏
分稀疏重建和稠密重建两类:稀疏重建:使用RGB相机SLAMOrb-slam,Orb-slam2,orb-slam3:工程地址在: http://webdiis.unizar.es/~raulmur/orbslam/ DSO(Direct Sparse Odometry)因为...【详细内容】
2021-12-23  老师明明可以靠颜值    Tags:算法   点击:(7)  评论:(0)  加入收藏
1. 基本概念希尔排序又叫递减增量排序算法,它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的;希尔排序是一种不稳定的排序算法...【详细内容】
2021-12-22  青石野草    Tags:希尔排序   点击:(6)  评论:(0)  加入收藏
ROP是一种技巧,我们对execve函数进行拼凑来进行system /bin/sh。栈迁移的特征是溢出0x10个字符,在本次getshell中,还碰到了如何利用printf函数来进行canary的泄露。ROP+栈迁移...【详细内容】
2021-12-15  星云博创    Tags:栈迁移   点击:(19)  评论:(0)  加入收藏
一、什么是冒泡排序1.1、文字描述冒泡排序是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地...【详细内容】
2021-12-15    晓掌柜丶韶华  Tags:排序算法   点击:(16)  评论:(0)  加入收藏
在了解golang的map之前,我们需要了解哈希这个概念。哈希表,又称散列表(Hash table),是根据键(key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算出一个键值的函数,将...【详细内容】
2021-12-07  一棵梧桐木    Tags:哈希表   点击:(13)  评论:(0)  加入收藏
前面文章在谈论分布式唯一ID生成的时候,有提到雪花算法,这一次,我们详细点讲解,只讲它。SnowFlake算法据国家大气研究中心的查尔斯&middot;奈特称,一般的雪花大约由10^19个水分子...【详细内容】
2021-11-17  小心程序猿QAQ    Tags:雪花算法   点击:(24)  评论:(0)  加入收藏
导读:在大数据时代,对复杂数据结构中的各数据项进行有效的排序和查找的能力非常重要,因为很多现代算法都需要用到它。在为数据恰当选择排序和查找策略时,需要根据数据的规模和类型进行判断。尽管不同策略最终得到的结果完...【详细内容】
2021-11-04  华章科技    Tags:排序算法   点击:(37)  评论:(0)  加入收藏
这是我在网上找的资源的一个总结,会先给出一个我看了觉得还行的关于算法的讲解,再配上实现的代码: Original author: Bill_Hoo Original Address: http://blog.sina.com.cn/s/bl...【详细内容】
2021-11-04  有AI野心的电工和码农    Tags: KMP算法   点击:(36)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条