🥳🥳🥳 茫茫人海千千万万,感谢这一刻你看到了我的文章,感谢观赏,大家好呀,我是最爱吃鱼罐头,大家可以叫鱼罐头呦~🥳🥳🥳
如果你觉得这个【重启人生计划】对你也有一定的帮助,加入本专栏,开启新的训练计划,漫长成长路,千锤百炼,终飞升巅峰!无水文,不废话,唯有日以继日,终踏顶峰! ✨✨欢迎订阅本专栏✨✨
❤️❤️❤️ 最后,希望我的这篇文章能对你的有所帮助! 愿自己还有你在未来的日子,保持学习,保持进步,保持热爱,奔赴山海! ❤️❤️❤️
序言
大家好,我是最爱吃鱼罐头,距离离职已经过去一个月了,目前进度为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;
}
}
最后注意虚拟头结点:
链表的一大问题就是操作当前结点必须要找前一个结点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个结点了。
每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题。而且很多链表的题目中,都大多数需要用到虚拟头结点。
环形链表
这道题也是快慢指针的典型应用,其实也上道题的区别,在于这个链表是有环的,大体步骤一致,主要思路:通过快慢指针,快指针每次移动两步,而慢指针每次移动一步,只要链表有环,终究会相遇的。
主要步骤
- 定义快慢指针,两个指针开始都指向head结点;
- 只要快指针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
在环形链表的基础上,也就是如果确定链表有环,如何找到这个环的入口呢? 此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
确定环入口的方式
-
首先我们确定链表是否有环,这个确定方式,在环形链表中已经知道。
-
当有环存在时,我们假设快慢指针在 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的长度。
-
最终,我们可以根据上面的推论,当快慢指针相遇之后,我们重新定义一个指针(慢指针,后面就是快指针)从链表的头部开始,每次移动一个结点,快指针也同时一次移动一个结点,这两个指针最终的相遇点就是环的入口点。
主要步骤
- 定义快慢指针,两个指针开始都指向head结点;
- 只要快指针fast不为空,或者fast的下一个结点不为空,就可以开始循环遍历:
- 快指针fast移动两步,即fast = fast.next.next;
- 慢指针slow移动一步,即slow = slow.next;
- 只要fast和slow相遇,代表循环结束,有环。
- 无环就返回null,有环就下面继续:
- 确定有环后,也确定了相遇的结点,重新定义慢指针,慢指针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;
}
}
相交链表
对比环形链表,这道题是两个链表了,并且两个链表之前有相交的现象,那么我们如何确定相交的地方呢?
确定相交的方式
-
我们可以确定在相交的地方后面的大小为c,是一致的,而链表1相交之前的大小为a,链表2相交之前的大小b;
-
可以确定链表1的大小为a + c = 5,而链表2的大小为b + c= 6;
-
两者的区别是不是就是相交前的大小的差距,必须消除两个链表的长度差,即2和3的差距,比如说,链表1移动2步就到末尾,而链表2只能移动到2的位置。那此时将链表1的指针移动链表2的头结点,而链表2在移动到末尾后也转移到链表1的头结点,此时的距离大小是不是:链表1为2 + 3, 链表2为3 + 2,这样就消除两个链表的长度差;
-
我们构建两个指针分别指向两个链表的头结点,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时,代表两者相遇,走的距离完全一样后,遇到了相交处。
主要步骤
- 定义两个指针,分别指向两个链表的头结点,a = headA, b = headB,接着依次往后遍历;
- 如果 a 到了末尾,则 a = headB 继续往后遍历;
- 如果 b 到了末尾,则 b = headA 继续往后遍历;
- 通过切换链表的方式,消除两个链表的长度差,如此就找到了两个链表的相交处。
代码实现:
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 | 表示符合查询条件的数据百分比 |
Extra | SQL执行的额外信息 |
具体需要关注的字段有:
type:
执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法
,又称"访问类型”,其中的type
列就表明了这个访问方法是啥,是较为重要的一个指标。比如,看到type
列的值是ref
,表明MySQL即将使用ref
访问方法来执行对s1
表的查询。
完整的访问方法如下: system
, const
, eq_ref
, ref
, fulltext
, ref_or_null
,index_merge
, unique_subquery
, index_subquery
, range
, index
, ALL
。
结果值从最好到最坏依次是:
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. 主从同步延迟
主从同步延迟的原因:
- 主库的从库太多,主库需要将 binlog 日志传输给多个从库,导致复制延迟。
- 在从库执行的 SQL 中存在慢查询语句,会导致整体复制进程的延迟。
- 如果主库的读写压力过大,会导致主库处理 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上了。
乘风破浪会有时 直挂云帆济沧海
希望从明天开始,一起加油努力吧,成就更好的自己。
🥂 虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!✨✨✨
💟 感谢各位看到这里!愿你韶华不负,青春无悔!让我们一起加油吧! 🌼🌼🌼
💖 学到这里,今天的世界打烊了,晚安!🌙🌙🌙