重启人生计划-我和我

🥳🥳🥳 茫茫人海千千万万,感谢这一刻你看到了我的文章,感谢观赏,大家好呀,我是最爱吃鱼罐头,大家可以叫鱼罐头呦~🥳🥳🥳

如果你觉得这个【重启人生计划】对你也有一定的帮助,加入本专栏,开启新的训练计划,漫长成长路,千锤百炼,终飞升巅峰!无水文,不废话,唯有日以继日,终踏顶峰! ✨✨欢迎订阅本专栏✨✨

❤️❤️❤️ 最后,希望我的这篇文章能对你的有所帮助! 愿自己还有你在未来的日子,保持学习,保持进步,保持热爱,奔赴山海! ❤️❤️❤️

🔥【重启人生计划】第零章序·大梦初醒🔥

🔥【重启人生计划】第壹章序·明确目标🔥

🔥【重启人生计划】第贰章序·勇敢者先行🔥

🔥【重启人生计划】第叁章序·拒绝内耗🔥

🔥【重启人生计划】第肆章序·积蓄星火🔥

🔥【重启人生计划】第伍章序·积蓄星火🔥

序言

大家好,我是最爱吃鱼罐头,距离离职已经过去一个月了,目前进度为5,打算重新找工作倒计时25天,当然这其中也会去投递面试。

我爱我的所有,微不足道的部分,爱我所有平凡日子的总和。

今日回顾

今天在学习过程中,遇到几个难点,第一,链表花费的时间比较长,因为我做了一些详细的图解,加上自己的理解,从早上一直干到下午才弄完,感觉后续不能这么搞了,除非你们觉得这个图解或者解题思路不错的话,我就会继续做下去。

然后MySQL比较高级的题目还是有点不理解,晚上和明天都会看下视频,找下资料去补充下。

算法回顾

链表的中间结点

链表的中间结点 📍

这道题的关键,其实跟昨天的删除链表的倒数第 N 个结点 📍类似,关键是使用快慢指针,使用两个指针,一开始都位于链表的头结点,slow指针一次只走 1 步,fast指针一次只走 2 步,一个在前,一个在后,同时走。这样当fast指针走完的时候,slow指针就来到了链表的中间位置。

代码实现:

package com.ygt.day6;

import com.ygt.day4.ListNode;

/**
 * 876. 链表的中间结点
 * https://leetcode.cn/problems/middle-of-the-linked-list/description/
 * 给你单链表的头结点 head ,请你找出并返回链表的中间结点。
 * 如果有两个中间结点,则返回第二个中间结点。
 * 输入:head = [1,2,3,4,5]
 * 输出:[3,4,5]
 * 解释:链表只有一个中间结点,值为 3 。
 *
 * @author ygt
 * @since 2024/8/16
 */
public class MiddleNode {
    public static void main(String[] args) {
        ListNode node5 = new ListNode(5);
        ListNode node4 = new ListNode(4, node5);
        ListNode node3 = new ListNode(3, node4);
        ListNode node2 = new ListNode(2, node3);
        ListNode node = new ListNode(1, node2);

        // 打印查看当前效果
        ListNode.print(node);

        ListNode listNode = new MiddleNode().middleNode(node);
        System.out.println();

        // 打印查看当前效果
        ListNode.print(listNode);
    }

    public ListNode middleNode(ListNode head) {
        // 1. 一开始都位于链表的头结点
        ListNode slow = head, fast = head;

        // 2. slow指针一次只走 1 步,fast指针一次只走 2 步
        // 这样肯定是fast先到链表的尾部
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        // 返回慢结点就是中间结点了。
        return slow;
    }
}

最后注意虚拟头结点:

链表的一大问题就是操作当前结点必须要找前一个结点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个结点了。

每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题。而且很多链表的题目中,都大多数需要用到虚拟头结点。

环形链表

环形链表 📍

环形链表-1723775536933.jpg

这道题也是快慢指针的典型应用,其实也上道题的区别,在于这个链表是有环的,大体步骤一致,主要思路:通过快慢指针,快指针每次移动两步,而慢指针每次移动一步,只要链表有环,终究会相遇的。

主要步骤

  1. 定义快慢指针,两个指针开始都指向head结点;
  2. 只要快指针fast不为空,或者fast的下一个结点不为空,就可以开始循环遍历:
    • 快指针fast移动两步,即fast = fast.next.next;
    • 慢指针slow移动一步,即slow = slow.next;
    • 只要fast和slow相遇,代表循环结束,有环。

图解

我们根据步骤画出一个大概的图解过程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

动图图解

为了更方便查看图解过程,做了个动画:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码实现:

package com.ygt.day6;

import com.ygt.day4.ListNode;

/**
 * 141. 环形链表
 * https://leetcode.cn/problems/linked-list-cycle/description/
 * 给你一个链表的头节点 head ,判断链表中是否有环。
 * 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
 * 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
 * 如果链表中存在环 ,则返回 true 。 否则,返回 false 。
 * 输入:head = [3,2,0,-4], pos = 1
 * 输出:true
 * 解释:链表中有一个环,其尾部连接到第二个节点。
 * @author ygt
 * @since 2024/8/16
 */
public class HasCycle {
    public static void main(String[] args) {
        ListNode node5 = new ListNode(5);
        ListNode node4 = new ListNode(4, node5);
        ListNode node3 = new ListNode(3, node4);
        ListNode node2 = new ListNode(2, node3);
        ListNode node = new ListNode(1, node2);

        // 形成环
//        node5.next = node2;

        System.out.println(new HasCycle().hasCycle(node));
    }

    public boolean hasCycle(ListNode head) {
        // 主要思路:
        // 通过快慢指针,快指针每次移动两步,而慢指针每次移动一步,只要链表有环,终究会相遇的。

        // 定义快慢指针,一开始为head头结点的位置
        ListNode fast = head, slow = head;

        // 如果能找到末尾null的位置,代表着这个链表是无环的,退出循环即可,返回false
        while(fast != null && fast.next != null) {
            // 快指针走两步,而慢指针走一步
            fast = fast.next.next;
            slow = slow.next;
            // 如果相等,代表两个相遇,代表有环
            if(fast == slow) {
                return true;
            }
        }

        return false;
    }
}

环形链表 II

环形链表 II 📍

在环形链表的基础上,也就是如果确定链表有环,如何找到这个环的入口呢? 此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

确定环入口的方式

  1. 首先我们确定链表是否有环,这个确定方式,在环形链表中已经知道。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 当有环存在时,我们假设快慢指针在 d 点相遇。a 代表入环之前的长度,b 代表慢指针进入环后又走了b的长度,c 代表环余下的长度。指针的指向是顺时针方向:

    • 如果快指针和慢指针在 d 点相遇,此时快指针比慢指针多走了 n 圈,也就是 n*(b+c) 的长度;
    • 此时快指针走过的距离是 a+n*(b+c)+b,慢指针走过的距离是 a+b;
    • 因为快指针每次走两步,慢指针每次走一步,所以快指针走过的距离永远是慢指针的两倍,所以 a+n*(b+c)+b=2*(a+b);
    • 上述公式可以推导出 a = (n-1)*(b+c)+c,也就是a的长度是恰好是 n-1 圈环的长度加上c的长度。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 最终,我们可以根据上面的推论,当快慢指针相遇之后,我们重新定义一个指针(慢指针,后面就是快指针)从链表的头部开始,每次移动一个结点,快指针也同时一次移动一个结点,这两个指针最终的相遇点就是环的入口点。

主要步骤

  1. 定义快慢指针,两个指针开始都指向head结点;
  2. 只要快指针fast不为空,或者fast的下一个结点不为空,就可以开始循环遍历:
    • 快指针fast移动两步,即fast = fast.next.next;
    • 慢指针slow移动一步,即slow = slow.next;
    • 只要fast和slow相遇,代表循环结束,有环。
  3. 无环就返回null,有环就下面继续:
  4. 确定有环后,也确定了相遇的结点,重新定义慢指针,慢指针slow指向head结点,快指针fast不变:
    • 快指针fast和慢指针slow同样移动一步,即fast = fast.next;slow = slow.next;
    • 只要fast和slow相遇,找到环的入口处,结束返回当前结点即可。

图解

我们根据步骤画出一个大概的图解过程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

动图图解

为了更方便查看图解过程,做了个动画:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码实现:

package com.ygt.day6;

import com.ygt.day4.ListNode;

/**
 * 142. 环形链表 II
 * https://leetcode.cn/problems/linked-list-cycle-ii/description/
 * 给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
 * 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
 * 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
 * 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
 * 不允许修改 链表。
 * 输入:head = [3,2,0,-4], pos = 1
 * 输出:返回索引为 1 的链表节点
 * 解释:链表中有一个环,其尾部连接到第二个节点。
 * @author ygt
 * @since 2024/8/16
 */
public class DetectCycle {
    public static void main(String[] args) {
        ListNode node5 = new ListNode(5);
        ListNode node4 = new ListNode(4, node5);
        ListNode node3 = new ListNode(3, node4);
        ListNode node2 = new ListNode(2, node3);
        ListNode node = new ListNode(1, node2);

        node5.next = node2;

        // 这个有环的打印不了一点。
        System.out.println(new DetectCycle().detectCycle(node).val);
    }

    public ListNode detectCycle(ListNode head) {
        // 主要思路:
        // 通过快慢指针,快指针每次移动两步,而慢指针每次移动一步,确定环后,再重新定义指针,以同样的速度移动指针,来确定这个环的入口

        // 定义快慢指针,一开始为head头结点的位置
        ListNode fast, slow;
        fast = slow = head;

        // 如果能找到末尾null的位置,代表着这个链表是无环的,退出循环即可,返回null即可。
        while(fast != null && fast.next != null) {
            // 快指针走两步,而慢指针走一步
            fast = fast.next.next;
            slow = slow.next;
            // 如果相等,代表两个相遇,代表有环
            if(fast == slow) {
                // 有环后,代表寻找有环的过程的循环结束,可以退出循环在外面编写,也可以在这里编写
                // 这里确定环的入口,重新定义slow指针
                slow = head;

                // 两个指针以同样的速度移动,两个相遇就代表找到环的入口
                while (slow != fast) {
                    fast = fast.next;
                    slow = slow.next;
                }

                // 退出循环,找到环的入口
                return slow;
            }
        }
        return null;
    }
}

相交链表

相交链表📍

对比环形链表,这道题是两个链表了,并且两个链表之前有相交的现象,那么我们如何确定相交的地方呢?

确定相交的方式

  1. 我们可以确定在相交的地方后面的大小为c,是一致的,而链表1相交之前的大小为a,链表2相交之前的大小b;

  2. 可以确定链表1的大小为a + c = 5,而链表2的大小为b + c= 6;

  3. 两者的区别是不是就是相交前的大小的差距,必须消除两个链表的长度差,即2和3的差距,比如说,链表1移动2步就到末尾,而链表2只能移动到2的位置。那此时将链表1的指针移动链表2的头结点,而链表2在移动到末尾后也转移到链表1的头结点,此时的距离大小是不是:链表1为2 + 3, 链表2为3 + 2,这样就消除两个链表的长度差;

  4. 我们构建两个指针分别指向两个链表的头结点,a指针和b指针,依次往后遍历,直到某一方遇到null,就切换到对方链表上继续移动;

    • a = headA, a = a.next;
    • b = headB, b = b.next;
    • 此时a遇到null,即a = null ==> a = headB,a = a.next;
    • 而b也遇到null,即b = null ==> b = headA, b = b.next;
    • 一旦 a == b时,代表两者相遇,走的距离完全一样后,遇到了相交处。

主要步骤

  1. 定义两个指针,分别指向两个链表的头结点,a = headA, b = headB,接着依次往后遍历;
  2. 如果 a 到了末尾,则 a = headB 继续往后遍历;
  3. 如果 b 到了末尾,则 b = headA 继续往后遍历;
  4. 通过切换链表的方式,消除两个链表的长度差,如此就找到了两个链表的相交处。

代码实现:

package com.ygt.day6;

import com.ygt.day4.ListNode;

/**
 * 160. 相交链表
 * https://leetcode.cn/problems/intersection-of-two-linked-lists/description/
 * 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
 * 图示两个链表在节点 c1 开始相交:
 * 题目数据 保证 整个链式结构中不存在环。
 * 注意,函数返回结果后,链表必须 保持其原始结构 。
 * 自定义评测:
 * 评测系统 的输入如下(你设计的程序 不适用 此输入):
 * intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
 * listA - 第一个链表
 * listB - 第二个链表
 * skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
 * skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数
 * 评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,
 * 那么你的解决方案将被 视作正确答案 。
 * 输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
 * 输出:Intersected at '8'
 * 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
 * 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
 * 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
 * — 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。
 * 换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
 * @author ygt
 * @since 2024/8/16
 */
public class GetIntersectionNode {
    public static void main(String[] args) {
        ListNode node6 = new ListNode(4);
        ListNode node5 = new ListNode(4, node6);
        ListNode node4 = new ListNode(8, node5);
        ListNode node3 = new ListNode(1, node4);
        ListNode node2 = new ListNode(6, node3);
        ListNode node = new ListNode(5, node2);

        ListNode node22 = new ListNode(1, node4);
        ListNode node11 = new ListNode(4, node22);

        System.out.println("相交的结点值:" + new GetIntersectionNode().getIntersectionNode(node, node11).val);
    }


    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 判断条件
        if(headA == null || headB == null) {
            return null;
        }

        // 定义两个指针,分别指向两个链表的头结点
        ListNode a = headA;
        ListNode b = headB;

        // 依次往后遍历 直到遇到两个结点相等
        while (a != b) {
            // 1. 如果 a 到了末尾,则 a = headB 继续往后遍历;
            a = a == null ? headB : a.next;
            // 2. 如果 b 到了末尾,则 b = headA 继续往后遍历;
            b = b == null ? headA : b.next;
        }

        return a;
    }
}

小结算法

今天的算法是有点难度,得多思考下,才能做出来,当然大神的你无需耗费更多的精神就做出来啦。

明日内容

基础面试题

下面的题目的答案是基于自己的理解和思考去编写出来的,也希望大家如果看到了,可以根据自己的理解去转换为自己的答案。

当然很多思考也有参考别人的成分,但是自己能讲述出来就是最棒的。

这里有一篇阿里的mysql面试题

18. Explain

Explain语句返回列的各列含义:

列名含义
id每个select都有一个对应的id号,并且是从1开始自增的
select_type查询语句执行的查询操作类型(simple、primary、union)
table表名
partitions表分区情况
type查询所用的访问类型
possible_keys可能用到的索引
key实际查询用到的索引
key_len所使用到的索引长度
ref使用到索引时,与索引进行等值匹配的列或者常量
rows预计扫描的行数(索引行数或者表记录行数)
filtered表示符合查询条件的数据百分比
ExtraSQL执行的额外信息

具体需要关注的字段有:

type:

执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法,又称"访问类型”,其中的type列就表明了这个访问方法是啥,是较为重要的一个指标。比如,看到type列的值是ref,表明MySQL即将使用ref访问方法来执行对s1表的查询。

完整的访问方法如下: systemconsteq_refreffulltextref_or_nullindex_mergeunique_subqueryindex_subqueryrangeindexALL

结果值从最好到最坏依次是:

system > const > eq_ref > ref >

fulltext > ref_or_null > index_merge >unique_subquery > index_subquery > range >

index > ALL

SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。(阿里巴巴
开发手册要求)

possible_keys、key、key_len:

  • possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些。一般查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。
  • key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引。
  • key_len:实际使用到的索引长度(即:字节数),在联合索引里面,命中一次key_len加一次长度。越长代表精度越高,效果越好

rows、filtered:

  • rows:预估的需要读取的记录条数;
  • filtered :filtered 的值指返回结果的行占需要读到的行(rows 列的值)的百分比。

Extra :

Extra列是用来说明一些额外信息的,包含不适合在其他列中显示但十分重要的额外信息。我们可以通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询语句

  • Using where:当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息。

  • Using index: 当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,在Extra列将会提示该额外信息。

  • Using index condition:有些搜索条件中虽然出现了索引列,但却不能使用到索引

  • Using join buffer (Block Nested Loop) 没有索引的字段进行表关联。在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫join buffer的内存块来加快查询速度,也就是我们所讲的基于块的嵌套循环算法

19. 主从复制原理

  • 在主服务器上,所有修改数据的语句(如 INSERT、UPDATE、DELETE)会被记录到bin log中;
  • 主服务器创建一个log dump线程向从服务器推送bin log;
  • 从服务器连接到主服务器的时候,会创建一个IO线程接收bin log,并记录到relay log中继日志中;
  • 从服务器上有一个 SQL 线程会读取中继日志,并在本地数据库上执行,从而保证主从数据的同步。

20. 主从同步延迟

主从同步延迟的原因:

  1. 主库的从库太多,主库需要将 binlog 日志传输给多个从库,导致复制延迟。
  2. 在从库执行的 SQL 中存在慢查询语句,会导致整体复制进程的延迟。
  3. 如果主库的读写压力过大,会导致主库处理 binlog 的速度减慢,进而影响复制延迟。

缓存记录写key法:

在cache里记录哪些记录发生过的写请求,来路由读主库还是读从库

异步复制:

在异步复制中,主库执行完操作后,写入binlog日志后,就返回客户端,这一动作就结束了,并不会验证从库有没有收到,完不完整,所以这样可能会造成数据的不一致。

半同步复制:

当主库每提交一个事务后,不会立即返回,而是等待其中一个从库接收到Binlog并成功写入Relay-log中才返回客户端,通过一份在主库的Binlog,另一份在其中一个从库的Relay-log,可以保证了数据的安全性和一致性。

全同步复制:

指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响

21. binlog记录格式

MySQL的Binlog有三种录入格式,分别是Statement格式、Row格式和Mixed格式。它们的主要区别如下:
1Statement格式:

  • 将SQL语句本身记录到Binlog中。
  • 记录的是在主库上执行的SQL语句,从库通过解析并执行相同的SQL来达到复制的目的。
  • 简单、易读,节省存储空间。
  • 但是,在某些情况下,由于执行计划或函数等因素的影响,相同的SQL语句在主从库上执行结果可能不一致,导致复制错误。

【2】Row格式:

  • 记录被修改的每一行数据的变化。
  • 不记录具体的SQL语句,而是记录每行数据的变动情况,如插入、删除、更新操作前后的值。
  • 保证了复制的准确性,不受SQL语句执行结果的差异影响,适用于任何情况。
  • 但是,相比Statement格式,Row格式会占用更多的存储空间。

【3】Mixed格式:

  • Statement格式和Row格式的结合,MySQL自动选择适合的格式。
  • 大多数情况下使用Statement格式进行记录,但对于无法保证安全复制的情况,如使用非确定性函数、触发器等,会自动切换到Row格式进行记录。
  • 结合了两种格式的优势,既减少了存储空间的占用,又保证了复制的准确性。

我们需要根据实际需求和应用场景,选择适合的Binlog录入格式非常重要。

  • Statement格式适用于简单的SQL语句,对存储空间要求较高;
  • Row格式适用于需要精确复制的场景;
  • Mixed格式是综合考虑两种格式的优势而出现的折中方案。

算法

在有链表的基础上进行链表的算法题,可以事半功倍。

需要有链表以及双指针的基础。

🌸 完结

最后,相关算法的代码也上传到gitee或者github上了。

乘风破浪会有时 直挂云帆济沧海

希望从明天开始,一起加油努力吧,成就更好的自己。

🥂 虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!✨✨✨

💟 感谢各位看到这里!愿你韶华不负,青春无悔!让我们一起加油吧! 🌼🌼🌼

💖 学到这里,今天的世界打烊了,晚安!🌙🌙🌙

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最爱吃鱼罐头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值