diff --git a/README.md b/README.md index 6b8b44b..4beca2f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# 《后端技术总结》 + +# 《大厂面试指北》 最佳阅读地址:http://notfound9.github.io/interviewGuide/ @@ -6,18 +7,13 @@ Github项目主页:https://github.com/NotFound9/interviewGuide 作者博客地址:https://juejin.im/user/5b370a42e51d4558ce5eb969 - ## 为什么要做这个开源项目? -之前喜欢在空闲时看一些技术书籍,但是发现看完后,即便每一章都写了读书笔记,看到一些实际的问题时,自己还是一脸茫然,所以我认为学习一项技术分为三个阶段: - -1.看过入门教程,会用API。 +之前在业余时间阅读技术书籍时,发现只阅读而不产出,这样收效甚微。所以就在网上找了很多常见的技术问题,根据自己的技术积累,查阅书籍,阅读文档和博客等资料,尝试着用自己的话去写了很多原创解答,最终整理开源到Github。一方面是便于自己复习巩固,一方面是将这些自己写的解答开源出来分享给大家,希望可以帮助到大家,也欢迎大家一起来完善这个项目,为开源做贡献。 -2.看过相关的技术书籍,了解一部分原理。 - -3.能够根据了解的原理对实际的问题进行分析,解决问题,这样才能对技术有较为深入的理解,在工作中遇到复杂问题时,才能解决。 +
+ -所以我发起了这个项目,一方面是便于自己复习巩固,一方面是将这些自己写的解答开源出来分享给大家,希望可以帮助到大家,也欢迎大家一起来完善这个项目,为开源做贡献。 ## 目录 @@ -36,6 +32,8 @@ Github项目主页:https://github.com/NotFound9/interviewGuide - [10.Java中的内部类是怎么样的?](docs/JavaBasic.md#Java中的内部类是怎么样的?) - [11.Java中的注解是什么?](docs/JavaBasic.md#Java中的注解是什么?) - [12.为什么hashCode()和equal()方法要一起重写?](docs/JavaBasic.md#为什么hashCode()和equal()方法要一起重写?) + - [13.Java中有哪些数据类型?](docs/JavaBasic.md#Java中有哪些数据类型?) + - [14.包装类型和基本类型的区别是什么?](docs/JavaBasic.md#包装类型和基本类型的区别是什么?) * 容器 - [ArrayList和LinkedList](docs/ArrayList.md) - [1.ArrayList与LinkedList的区别是什么?](docs/ArrayList.md#ArrayList与LinkedList的区别是什么?) @@ -59,19 +57,19 @@ Github项目主页:https://github.com/NotFound9/interviewGuide - [3.Java中单例有哪些写法?](docs/JavaMultiThread.md#Java中单例有哪些写法?) - [4.Java中创建线程有哪些方式?](docs/JavaMultiThread.md#Java中创建线程有哪些方式?) - [5.如何解决序列化时可以创建出单例对象的问题?](docs/JavaMultiThread.md#如何解决序列化时可以创建出单例对象的问题?) - - [6.悲观锁和乐观锁是什么?](docs/JavaMultiThread.md#悲观锁和乐观锁是什么?) - - [7.volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?](docs/JavaMultiThread.md#volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?) - - [8.Java中线程的状态是怎么样的?](docs/JavaMultiThread.md#Java中线程的状态是怎么样的?) - - [9.wait(),join(),sleep()方法有什么作用?](docs/JavaMultiThread.md#wait(),join(),sleep()方法有什么作用?) - - [10.Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?](docs/JavaMultiThread.md#Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?) - - [11.谈一谈你对线程中断的理解?](docs/JavaMultiThread.md#谈一谈你对线程中断的理解?) - - [12.线程间怎么通信?](docs/JavaMultiThread.md#线程间怎么通信?) - - [13.怎么实现实现一个生产者消费者?](docs/JavaMultiThread.md#怎么实现实现一个生产者消费者?) - - [14.谈一谈你对线程池的理解?](docs/JavaMultiThread.md#谈一谈你对线程池的理解?) - - [15.线程池有哪些状态?](docs/JavaMultiThread.md#线程池有哪些状态?) + - [6.volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?](docs/JavaMultiThread.md#volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?) + - [7.Java中线程的状态是怎么样的?](docs/JavaMultiThread.md#Java中线程的状态是怎么样的?) + - [8.wait(),join(),sleep()方法有什么作用?](docs/JavaMultiThread.md#wait(),join(),sleep()方法有什么作用?) + - [9.Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?](docs/JavaMultiThread.md#Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?) + - [10.谈一谈你对线程中断的理解?](docs/JavaMultiThread.md#谈一谈你对线程中断的理解?) + - [11.线程间怎么通信?](docs/JavaMultiThread.md#线程间怎么通信?) + - [12.怎么实现实现一个生产者消费者?](docs/JavaMultiThread.md#怎么实现实现一个生产者消费者?) + - [13.谈一谈你对线程池的理解?](docs/JavaMultiThread.md#谈一谈你对线程池的理解?) + - [14.线程池有哪些状态?](docs/JavaMultiThread.md#线程池有哪些状态?) - [锁相关](docs/Lock.md) - [1.sychronize的实现原理是怎么样的?](docs/Lock.md#sychronize的实现原理是怎么样的?) - [2.AbstractQueuedSynchronizer(缩写为AQS)是什么?](docs/Lock.md#AbstractQueuedSynchronizer(缩写为AQS)是什么?) + - [3.悲观锁和乐观锁是什么?](docs/Lock.md#悲观锁和乐观锁是什么?) * Redis - [基础](docs/RedisBasic.md) - [1.Redis是什么?](docs/RedisBasic.md#Redis是什么?) @@ -103,8 +101,11 @@ Github项目主页:https://github.com/NotFound9/interviewGuide - [2.Redis中哨兵是什么?](docs/RedisUserful.md#Redis中哨兵是什么?) - [3.客户端是怎么接入哨兵系统的?](docs/RedisUserful.md#客户端是怎么接入哨兵系统的?) - [4.Redis哨兵系统是怎么实现自动故障转移的?](docs/RedisUserful.md#Redis哨兵系统是怎么实现自动故障转移的?) + - [5.谈一谈你对Redis Cluster的理解?](docs/RedisUserful.md#谈一谈你对RedisCluster的理解?) + - [6.RedisCluster是怎么实现数据分片的?](docs/RedisUserful.md#RedisCluster是怎么实现数据分片的?) + - [7.RedisCluster是怎么做故障转移和发现的?](docs/RedisUserful.md#RedisCluster是怎么做故障转移和发现的?) * MySQL - - [面试题解答](docs/MySQLNote.md) + - [基础](docs/MySQLNote.md) - [1.一条MySQL更新语句的执行过程是什么样的?](docs/MySQLNote.md#一条MySQL更新语句的执行过程是什么样的?) - [2.脏页是什么?](docs/MySQLNote.md#脏页是什么?) - [3.Checkpoint是什么?](docs/MySQLNote.md#Checkpoint是什么?) @@ -135,24 +136,30 @@ Github项目主页:https://github.com/NotFound9/interviewGuide - [28.MySQL的join的实现是怎么样的?](docs/MySQLNote.md#MySQL的join的实现是怎么样的?) - [慢查询优化实践](docs/MySQLWork.md) * JVM - - [面试题解答](docs/JavaJVM.md) + - [基础](docs/JavaJVM.md) - [1.Java内存区域怎么划分的?](docs/JavaJVM.md#Java内存区域怎么划分的?) - [2.Java中对象的创建过程是怎么样的?](docs/JavaJVM.md#Java中对象的创建过程是怎么样的?) - [3.Java对象的内存布局是怎么样的?](docs/JavaJVM.md#Java对象的内存布局是怎么样的?) - [4.垃圾回收有哪些特点?](docs/JavaJVM.md#垃圾回收有哪些特点?) - [5.在垃圾回收机制中,对象在内存中的状态有哪几种?](docs/JavaJVM.md#在垃圾回收机制中,对象在内存中的状态有哪几种?) - [6.对象的强引用,软引用,弱引用和虚引用的区别是什么?](docs/JavaJVM.md#对象的强引用,软引用,弱引用和虚引用的区别是什么?) - - [7.垃圾回收算法有哪些?](docs/JavaJVM.md#垃圾回收算法有哪些?) - - [8.Minor GC和Full GC是什么?](docs/JavaJVM.md#MinorGC和FullGC是什么?) - - [9.如何确定一个对象可以回收?](docs/JavaJVM.md#如何确定一个对象是否可以被回收?) - - [10.目前通常使用的是什么垃圾收集器?](docs/JavaJVM.md#目前通常使用的是什么垃圾收集器?) - - [11.双亲委派机制是什么?](docs/JavaJVM.md#双亲委派机制是什么?) - - [12.怎么自定义一个类加载器?](docs/JavaJVM.md#怎么自定义一个类加载器?) + - [7.双亲委派机制是什么?](docs/JavaJVM.md#双亲委派机制是什么?) + - [8.怎么自定义一个类加载器?](docs/JavaJVM.md#怎么自定义一个类加载器?) + - [9.垃圾回收算法有哪些?](docs/JavaJVM.md#垃圾回收算法有哪些?) + - [10.Minor GC和Full GC是什么?](docs/JavaJVM.md#MinorGC和FullGC是什么?) + - [11.如何确定一个对象可以回收?](docs/JavaJVM.md#如何确定一个对象是否可以被回收?) + - [12.目前通常使用的是什么垃圾收集器?](docs/JavaJVM.md#目前通常使用的是什么垃圾收集器?) - [Kafka](docs/Kafka.md) - [ZooKeeper](docs/ZooKeeper.md) - [HTTP](docs/HTTP.md) -- [《剑指Offer》解题思考](docs/CodingInterviews.md) -- [大厂面试系列](docs/BATInterview.md) +- [Spring](docs/Spring.md) +- [Nginx](docs/Nginx.md) +- [系统设计](docs/SystemDesign.md) +* 算法 + - [《剑指Offer》解题思考](docs/CodingInterviews.md) + - [《LeetCode热门100题》解题思考(上)](docs/LeetCode.md) + - [《LeetCode热门100题》解题思考(下)](docs/LeetCode1.md) +- [大厂面试公众号文章系列](docs/BATInterview.md) - [【大厂面试01期】高并发场景下,如何保证缓存与数据库一致性?](https://mp.weixin.qq.com/s/hwMpAVZ1_p8gLfPAzA8X9w) - [【大厂面试02期】Redis过期key是怎么样清理的?](https://mp.weixin.qq.com/s/J_nOPKS17Uax2zGrZsE8ZA) - [【大厂面试03期】MySQL是怎么解决幻读问题的?](https://mp.weixin.qq.com/s/8D6EmZM3m6RiSk0-N5YCww) @@ -176,13 +183,8 @@ Github项目主页:https://github.com/NotFound9/interviewGuide 我平时比较喜欢看书,写技术文章,也比较喜欢讨论技术。这是我的[掘金主页](https://juejin.im/user/5b370a42e51d4558ce5eb969),希望大家可以关注一下,谢谢了!大家如果有事需要联系我,或者想进技术群,一起讨论技术,也可以扫描[主页中我的微信二维码](http://notfound9.github.io/interviewGuide/#/)加我,谢谢了! -
- - -
- ## 关于转载 如果你需要转载本仓库的一些文章到自己的博客的话,记得注明原文地址就可以了。 diff --git a/_sidebar.md b/_sidebar.md index 4d162b3..bd331c5 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -12,15 +12,21 @@ - [持久化(AOF和RDB)](docs/RedisStore.md) - [高可用(主从切换和哨兵机制)](docs/RedisUserful.md) * MySQL - - [面试题解答](docs/MySQLNote.md) + - [基础](docs/MySQLNote.md) - [慢查询优化实践](docs/MySQLWork.md) * JVM - - [面试题解答](docs/JavaJVM.md) + - [基础](docs/JavaJVM.md) - [Kafka](docs/Kafka.md) - [ZooKeeper](docs/ZooKeeper.md) - [HTTP](docs/HTTP.md) -- [大厂面试系列](docs/BATInterview.md) -- [《剑指Offer》解题思考](docs/CodingInterviews.md) +- [Spring](docs/Spring.md) +- [Nginx](docs/Nginx.md) +- [系统设计](docs/SystemDesign.md) +* 算法 + - [《剑指Offer》解题思考](docs/CodingInterviews.md) + - [《LeetCode热门100题》解题思考(上)](docs/LeetCode.md) + - [《LeetCode热门100题》解题思考(下)](docs/LeetCode1.md) +- [大厂面试公众号文章系列](docs/BATInterview.md) * 读书笔记 - [《Redis设计与实现》读书笔记 上](docs/RedisBook1.md) - [《Redis设计与实现》读书笔记 下](docs/RedisBook2.md) diff --git a/docs/ArrayList.md b/docs/ArrayList.md index 4635e0a..d2717bb 100644 --- a/docs/ArrayList.md +++ b/docs/ArrayList.md @@ -1,10 +1,11 @@ (PS:建了一个技术微信群,可以自由地讨论技术,工作和生活,也会分享一些我自己在看的技术资料,不定时发放红包福利,欢迎大家扫[首页里面的二维码](README.md)进群,希望和大家一起学习进步!大家如果想一起为这个项目做贡献的话,也可以进群大家聊一聊) -下面是主要是自己看了很多Java容器类相关的博客,以及很多面经中涉及到的Java容器相关的面试题后,自己全部手写的解答,也花了一些流程图,之后会继续更新这一部分。 +下面是主要是自己看了很多Java容器类相关的博客,以及很多面经中涉及到的Java容器相关的面试题后,自己全部手写的解答,也画了一些流程图,之后会继续更新这一部分。 #### [1.ArrayList与LinkedList的区别是什么?](#ArrayList与LinkedList的区别是什么?) #### [2.怎么使ArrayList,LinkedList变成线程安全的呢?](#怎么使ArrayList,LinkedList变成线程安全的呢?) + #### [3.ArrayList遍历时删除元素有哪些方法?](#ArrayList遍历时删除元素有哪些方法?) #### [4.ConcurrentModificationException是什么?](#ConcurrentModificationException是什么?) @@ -16,9 +17,9 @@ #### 1.底层使用的数据结构 -* Arraylist 底层使用的是Object数组,初始化时就会指向的会是一个static修饰的空数组,数组长度一开始为**0**,插入第一个元素时数组长度会初始化为**10**,之后每次数组空间不够进行扩容时都是增加为原来的**1.5倍**。ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) +* ArrayList 底层使用的是**Object数组**,初始化时就会指向的会是一个static修饰的空数组,数组长度一开始为**0**,插入第一个元素时数组长度会初始化为**10**,之后每次数组空间不够进行扩容时都是增加为原来的**1.5倍**。ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间(为了避免添加元素时,数组空间不够频繁申请内存),而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放后继指针next和前驱指针pre以及数据) -* LinkedList 底层使用的是双向链表数据结构,每个节点保存了指向前驱节点和后继结点的指针。初始化时,不执行任何操作,添加第一个元素时,再去构造链表中的节点。 +* LinkedList 底层使用的数据结构是**双向链表**,每个节点保存了指向前驱节点和后继结点的指针。初始化时,不执行任何操作,添加第一个元素时,再去构造链表中的节点。 #### 2.是否保证线程安全: @@ -26,7 +27,7 @@ 因为ArrayList的插入元素的方法就是裸奔的,直接将原数组index及后面的元素拷贝到index+1及后面的位置上,然后将index位置设置为插入的值,并发修改时保证不了数据安全性,所以也不允许并发修改,一旦检测到并发修改,会抛出ConcurrentModificationException异常。 -``` +```java //ArrayList的插入元素的方法 public void add(int index, E element) { rangeCheckForAdd(index); @@ -38,29 +39,27 @@ public void add(int index, E element) { } ``` - - #### 3.插入和删除的复杂度: -* ArrayList 采用数组存储,元素的物理存储地址是连续的,支持以O(1)的时间复杂度对元素快速访问。插入和删除元素后,需要将后面的元素进行移动,所以插入和删除元素的时间复杂度受元素位置的影响。复杂度是 O(n), +* ArrayList 采用数组存储,元素的物理存储地址是连续的,支持以O(1)的时间复杂度对元素快速访问。插入和删除元素后,需要将后面的元素进行移动,所以插入和删除元素的时间复杂度受元素位置的影响。复杂度是 O(n), * LinkedList 采用链表存储,所以不能快速随机访问。所以首尾插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)(如果是插入到中间位置还需要考虑寻找插入位置的时间复杂度)。而数组为近似 O(n)。 #### 4.继承树 -* ArrayList继承于AbstractList抽象类,实现了List, RandomAccess, Cloneable, java.io.Serializable接口 。 -* LinkedList继承自AbstractSequentialList 实现List, Deque, Cloneable, java.io.Serializable接口。 +* ArrayList继承于AbstractList抽象类,实现了**List, RandomAccess, Cloneable, java.io.Serializable**接口 。 +* LinkedList继承自AbstractSequentialList 实现**List, Deque, Cloneable, java.io.Serializable**接口。 -AbstractSequentialList是AbstractList类的子类,实现了根据下标来访问元素的一些方法,主要是通过listIterator遍历获取特定元素。 +**AbstractSequentialList**是AbstractList类的子类,实现了根据下标来访问元素的一些方法,主要是通过listIterator遍历获取特定元素。 -List接口代表的是有序结合,与Set相反,List的元素是按照移动的顺序进行排列。 +**List接口**代表的是有序结合,与Set相反,List的元素是按照移动的顺序进行排列。 -Cloneable接口代表类会重新父类Object的clone()方法,支持对实例对象的clone操作。 +**Cloneable接口**代表类会重新父类Object的clone()方法,支持对实例对象的clone操作。 -java.io.Serializable接口代表类支持序列化。 +**java.io.Serializable**接口代表类支持序列化。 -RandomAccess是一个标示性接口,代表ArrayList支持快速访问,而LinkedList不支持。 +**RandomAccess**是一个标示性接口,代表ArrayList支持快速访问,而LinkedList不支持。 -Deque接口是双端队列的意思,代表LinkedList支持两端元素插入和移除。 +**Deque**接口是双端队列的意思,代表LinkedList支持两端元素插入和移除。 ### 怎么使ArrayList,LinkedList变成线程安全的呢? @@ -70,7 +69,7 @@ SynchronizedList是一个线程安全的包装类。继承于SynchronizedCollect 使用方法如下 -``` +```java LinkedList linkedList = new LinkedList(); //调用Collections的synchronizedList方法,传入一个linkedList,会返回一个SynchronizedList实例对象 List synchronizedList = Collections.synchronizedList(linkedList); @@ -85,23 +84,24 @@ List synchronizedRandomAccessList = Collections.synchronizedList(linke SynchronizedList类的部分代码如下: ```java - static class SynchronizedList +static class SynchronizedList extends SynchronizedCollection implements List { final List list;//源list - final Object mutex; + final Object mutex; SynchronizedCollection(Collection c) { this.c = Objects.requireNonNull(c); mutex = this;//mutex就是SynchronizedList实例自己,作为同步锁使用 } - - public E get(int index) { + + public E get(int index) { synchronized (mutex) { 是父类中的成员变量,在父类中会将list赋值给mutex return list.get(index); } } + public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } @@ -112,7 +112,7 @@ SynchronizedList类的部分代码如下: CopyOnWriteArrayList跟ArrayList类似,都是实现了List接口,只不过它的父类是Object,而不是AbstractList。CopyOnWriteArrayList与ArrayList的不同在于, -1.内部持有一个ReentrantLock类型的lock成员变量, +##### 1.内部持有一个ReentrantLock类型的lock锁,用于控制并发访问 ```java final transient ReentrantLock lock = new ReentrantLock(); @@ -120,7 +120,7 @@ CopyOnWriteArrayList跟ArrayList类似,都是实现了List接口,只不过 在对数组进行修改的方法中,都会先获取lock,获取成功才能进行修改,修改完释放锁,保证每次只允许一个线程对数组进行修改。 -2.CopyOnWriteArrayList内部用于存储元素的Object数组使用volatile +##### 2.使用volatile修饰Object数组,使得变量具备内存可见性 ```java //CopyOnWriteArrayList @@ -130,14 +130,16 @@ CopyOnWriteArrayList跟ArrayList类似,都是实现了List接口,只不过 private transient Object[] elementData;//transient ``` -可以看到区别主要在于CopyOnWriteArrayList的Object是使用volatile来修饰的,volatile可以使变量具备内存可见性,一个线程在工作内存中对变量进行修改后,会立即更新到物理内存,并且使得其他线程中的这个变量缓存失效,其他线程在读取会去物理内存中读取最新的值。(volatile修饰的是指向数组的引用变量,所以对数组添加元素,删除元素不会改变引用,所以为了保证内存可见性,CopyOnWriteArrayList.add()方法在添加元素时,都是复制出一个新数组,进行修改操作后,再设置到就数组上) +可以看到区别主要在于CopyOnWriteArrayList的Object是使用volatile来修饰的,volatile可以使变量具备内存可见性,一个线程在工作内存中对变量进行修改后,会立即更新到物理内存,并且使得其他线程中的这个变量缓存失效,其他线程在读取会去物理内存中读取最新的值。(volatile修饰的是指向数组的引用变量,所以对数组添加元素,删除元素不会改变引用,只有对数组变量array重新赋值才会改变。所以为了保证内存可见性,CopyOnWriteArrayList.add()方法在添加元素时,都是复制出一个新数组,进行修改操作后,再设置到就数组上) + +注意事项:Object数组都使用transient修饰是**因为transient修饰的属性不会参与序列化**,ArrayList通过实现writeObject()和readObject()方法来自定义了序列化方法(基于反序列化时节约空间考虑,如果用默认的序列方法,源elementData数组长度为100,实际只有10个元素,反序列化时也会分配长度为100的数组,造成内存浪费。) -注意事项:Object数组都使用transient修饰是因为transient修饰的属性不会参与序列化,ArrayList通过实现writeObject()和readObject()方法来自定义了序列化方法(基于反序列化时节约空间考虑,如果用默认的序列方法,源elementData数组长度为100,实际只有10个元素,反序列化时也会分配长度为100的数组,造成内存浪费。) +**下面是CopyOnWriteArrayList的add()方法:** ```java public boolean add(E e) { final ReentrantLock lock = this.lock; - //1. 使用Lock,保证写线程在同一时刻只有一个 + //1. 使用Lock,保证写线程在同一时刻只有一个 lock.lock(); try { //2. 获取旧数组引用 @@ -158,8 +160,12 @@ public boolean add(E e) { #### SynchronizedList和CopyOnWriteArrayList优缺点 +##### SynchronizedList:读写都加锁 + SynchronizedList是通过对读写方法使用synchronized修饰来实现同步的,即便只是多个线程在读数据,也不能进行,如果是读比较多的场景下,会性能不高,所以适合读写均匀的情况。 +##### CopyOnWriteArrayList:读不加锁,写加锁 + 而CopyOnWriteArrayList是读写分离的,只对写操作加锁,但是每次写操作(添加和删除元素等)时都会复制出一个新数组,完成修改后,然后将新数组设置到旧数组的引用上,所以在写比较多的情况下,会有很大的性能开销,所以适合读比较多的应用场景。 ### ArrayList遍历时删除元素有哪些方法? @@ -333,7 +339,7 @@ private void fastRemove(int index) { } ``` -而当删除完元素后,进行下一次循环时,会调用下面源码中Itr.next()方法获取下一个元素,会调用checkForComodification()方法对ArrayList进行校验,判断在遍历ArrayList是否已经被修改,由于之前对modCount+1,而expectedModCount还是初始化时ArrayList.Itr对象时赋的值,所以会不相等,然后抛出ConcurrentModificationException异常。 +而当删除完元素后,进行下一次循环时,会调用下面源码中Itr.next()方法获取下一个元素,会调用checkForComodification()方法对ArrayList进行校验,判断在遍历ArrayList是否已经被修改,由于之前对modCount+1,而**Iterator中的expectedModCount**还是初始化时ArrayList.Itr对象时赋的值,所以会不相等,然后抛出ConcurrentModificationException异常。 ##### 那么有什么办法可以让expectedModCount及时更新呢? @@ -371,7 +377,7 @@ public void tranverse() { **Iterator的源代码** -``` +```java private class Itr implements Iterator { int cursor; // 游标 int lastRet = -1; // index of last element returned; -1 if no such @@ -441,7 +447,7 @@ Exception in thread "main" java.util.ConcurrentModificationException 第4种方法其实是第3种方法在编译后的代码,所以第四种写法也会抛出ConcurrentModificationException异常。这种需要注意的是,每次调用iterator的next()方法,会导致游标向右移动,从而达到遍历的目的。所以在单次循环中不能多次调用next()方法,不然会导致每次循环时跳过一些元素,我在一些博客里面看到了一些错误的写法,比如这一篇[《在ArrayList的循环中删除元素,会不会出现问题?》](https://juejin.im/post/5b92844a6fb9a05d290ed46c)文章中: -![image-20200101124822998](/Users/ruiwendaier/Library/Application Support/typora-user-images/image-20200101124822998.png) +![image-20200101124822998](../static/image-20200101124822998.png) 先调用iterator.next()获取元素,与elem进行比较,如果相等,再调用list.remove(iterator.next());来移除元素,这个时候的iterator.next()其实已经不是与elem相等的元素了,而是后一个元素了,我们可以写个demo来测试一下 diff --git a/docs/BATInterview.md b/docs/BATInterview.md index 5cc5cac..70e0bfa 100644 --- a/docs/BATInterview.md +++ b/docs/BATInterview.md @@ -6,13 +6,9 @@ ![](http://notfound9.github.io/interviewGuide/static/wdsfsdfsmaster.png) -
- - -
## 目录: diff --git a/docs/CodingInterviews.md b/docs/CodingInterviews.md index 99b6cd1..bc294f1 100644 --- a/docs/CodingInterviews.md +++ b/docs/CodingInterviews.md @@ -4,15 +4,11 @@ 下面是主要是自己刷完《剑指Offer》上题后写的题解,有点乱,主要是给自己复习看的,之后有空了会进行整理。 #### [题003 二维数组中的查找](#题003) - #### [题004 替换空格](#题004) #### [题005从尾到头打印链表](#题005) - #### [题006重建二叉树](#题006) - #### [题007两个栈实现队列](#题007) - #### [题008旋转数组](#题008) #### [题009斐波那契数列](#题009) @@ -23,41 +19,61 @@ #### [题012调整数组顺序使奇数排在前面](#题012) #### [题013链表的倒数第K个结点](#题013) #### [题014反转链表](#题014) + #### [题015 合并链表](#题015) #### [题016判断一个二叉树是否是另一个二叉树的子结构](#题016) + #### [题017二叉树的镜像](#题017) #### [题018顺时针打印矩形](#题018) + #### [题019包含min函数的栈](#题019) #### [题020 栈的压入、弹出序列](#题020) + #### [题021 从上往下打印二叉树](#题021) + #### [题022 判断是否是二叉搜索树的后序遍历](#题022) + #### [题023 二叉树中和为某一值的路径](#题023) + #### [题024 复杂链表的复制](#题024) #### [题025 二叉搜索树与双向链表](#题025) #### [题026字符串的排列](#题026) + #### [题027数组中出现的次数超过一半的数字](#题027) #### [题028最小的k个数](#题028) #### [题029连续子数组的最大和](#题029) #### [题030从1到n中出现的整数中1出现的次数](#题030) + #### [题031把数组排成最小的数](#题031) + #### [题032返回第N个丑数](#题032) + #### [题033 第一个只出现一次的字符](#题033) + #### [题034 数组中的逆序对](#题034) + #### [题035 两个的链表的第一个公共节点](#题035) + #### [题036 数字在排序数组中出现的次数](#题036) + #### [题037 二叉树的深度](#题037) + #### [题038 判断是否是平衡二叉树](#题038) #### [题039 数组中只出现一次的数组](#题039) + #### [题040 和为S的连续正数序列](#题040) #### [题041 和为S的两个数字](#题041) + #### [题042左旋转字符串](#题042) #### [题043 翻转单词的序列](#题043) #### [题044 扑克牌顺子](#题044) #### [题045 圆圈中最后剩下的数字](#题045) #### [题046 求1+2+…+n](#题046) #### [题047 不用加减乘除做加法](#题047) + #### [题048 将字符串转换为整数](#题048) #### [题049 数组中重复的数字](#题049) + #### [题050 构建乘积数组](#题050) #### [题053 字符流中第一个不重复的字符](#题053) #### [题054 链表中环的入口节点](#题054) @@ -67,18 +83,22 @@ #### [题058 按之字形顺序打印二叉树](#题058) #### [题059 把二叉树打印成多行](#题059) #### [题060序列化二叉树](#题060) + #### [题061 二叉搜索树的第K小的节点](#题061) #### [题062 数据流的中位数](#题062) + #### [题063 滑动窗口的最大值](#题063) #### [题064 矩阵中的路径](#题064) #### [题065机器人的运动范围](#题065) -#### [题066剪绳子](#题066) +#### [题066剪绳子](#题066) ### 题003 二维数组中的查找 ##### 题目内容: +![image-20201202111714727](../static/image-20201202111714727.png) + 在一个二维[数组](https://cuijiahua.com/blog/tag/数组/)中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维[数组](https://cuijiahua.com/blog/tag/数组/)和一个整数,判断数组中是否含有该整数。 如果在一个二维数组中找到数字7,则返回true,如果没有找到,则返回false。 @@ -150,8 +170,6 @@ } ``` - - ## 题005 从尾到头打印链表 输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。 @@ -163,22 +181,7 @@ - 不为空,那么递归调用printListFromTailToHead方法来获取后面的节点反序生成的ArrayList,然后添加当前的节点的值,然后返回arrayList。 - 为空,那么说明当前节点是链表尾部节点,直接创建一个ArrayList,然后添加当前节点的值,然后返回arrayList。 -```java -ArrayList printListFromTailToHead(ListNode listNode) { - if(listNode == null) { return new ArrayList(); } - ArrayList arrayList; - ListNode nextNode = listNode.next; - if (nextNode!=null) { - arrayList = printListFromTailToHead(nextNode); - arrayList.add(listNode.val); - } else { - arrayList = new ArrayList<>(); - arrayList.add(listNode.val); - } - return arrayList; -} -``` -或者是这样写,其实原理就是先递归遍历,然后再打印,这样链表打印的顺序就是逆序的了。 +其实原理就是先递归遍历,然后再打印,这样链表打印的顺序就是逆序的了。 ```java ArrayList list = new ArrayList(); public ArrayList printListFromTailToHead(ListNode listNode) { @@ -224,6 +227,7 @@ public ArrayList printListFromTailToHead(ListNode listNode) { int leftLength = i - inStart;//左子树长度 treeNode.left = reConstructBinaryTree(pre, preStart + 1, preStart+leftLength, in, inStart, i-1); treeNode.right = reConstructBinaryTree(pre, preStart +leftLength+1, preEnd, in, i+1, inEnd); + break; } } return treeNode; @@ -239,11 +243,9 @@ public ArrayList printListFromTailToHead(ListNode listNode) { ```java Stack stack1 = new Stack(); Stack stack2 = new Stack(); - public void push(Integer number) { stack1.push(number); } - public Integer pop() { if (stack2.size()>0) { return stack2.pop(); @@ -262,8 +264,6 @@ public ArrayList printListFromTailToHead(ListNode listNode) { 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 - - ```java int minNumberInRotateArray(int[] array) { if (array[0]=array[start]){ + if ( array[mid]>=array[start]){//左半部分是递增的,那么就去掉左半部分 start = mid; - } else if(array[mid]<=array[end]) { + } else if(array[mid]<=array[end]) {//右半部分是递增的,那么就去掉右半部分 end = mid; } } @@ -312,7 +312,7 @@ int minNumberInRotateArray(int[] array) { 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39 - +##### 递归解法 f(n) =f(n-1)+f(n-2) @@ -331,6 +331,7 @@ int Fibonacci(int n) { } ``` + ## 题010求某个数的二进制格式下1的个数 输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。 @@ -358,13 +359,13 @@ int Fibonacci(int n) { ```java public int NumberOf1(int n) { int count = 0; - int flag = 1; + int bit = 1; int times =0; while(times<32) { - int value = flag&n; - if(value != 0) {count++;} + //不为0说明这一个二进制位为1, + if((bit&n) != 0) {count++;} times++; - flag = flag<<1; + bit = bit<<1; } return count; } @@ -424,9 +425,11 @@ double Power(double base ,int exponent) { 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。 +解题思路: + 如果可以使用额外的内存空间,可以对数组遍历两遍,一遍将奇数取出,存放在额外的数组中去,一遍把剩下的偶数存放到额外的数组中去。 -如果不能使用额外的内存空间,就是查找奇数,然后与前面的元素互换,一直到替换到最后一个奇数的后面,有点像是冒泡排序 +如果不能使用额外的内存空间,就是查找奇数,然后与前面的元素互换,一直到替换到最后一个奇数的后面,有点像是冒泡排序。(因为不能改变相对位置,所以不能用快排) 冒泡排序是其实是交换,从头开始,依次判断两个相邻的元素,将更大的元素向右交换,遍历一次后可以将当前序列最大的元素交换到最后面去,下次遍历就不用管最后一个元素。 @@ -458,14 +461,12 @@ ListNode FindKthToTail(ListNode head, int k) { return null; } ListNode secondNode = head; - for (int i=0 ; i < k-1 ; i++) {//向前走k-1步 if (secondNode.next==null) {//链表长度不足k个 return null; } secondNode = secondNode.next; } - ListNode firstNode = head; while (secondNode.next != null) {//一直遍历到secondNode成为最后一个节点 secondNode = secondNode.next; @@ -755,41 +756,35 @@ public int min() { 所以可以对压入顺序A进行遍历,判断A压入的元素是否是出栈顺序B最前面的元素, * 如果不是,那么说明只是把元素压入栈tempStack,现在还没有出栈 -* 如果是,那么现在元素可以出栈了,将元素先压入tempStack,然后对B继续向后遍历,并且与tempStack的栈顶元素进行判断,是的话就出栈,知道tempStack的元素与B中遍历到的元素不相等,那么停止出栈,继续之前的循环。 +* 如果是,那么现在元素可以出栈了,将元素先压入tempStack,然后对B继续向后遍历,继续之前的循环。 循环结束后,继续对B继续向后遍历,并且与tempStack的栈顶元素进行判断,是的话就出栈,知道tempStack的元素与B中遍历到的元素不相等,那么说明B与A对应不上。 ```java - public boolean IsPopOrder(int [] pushA,int [] popA) { - Stack tempStack = new Stack(); - if (popA.length != pushA.length|| pushA == null || popA == null){ +public static boolean IsPopOrder1(int [] pushA,int [] popB) { + if (pushA==null||popB==null) { return false; } + Stack stack = new Stack<>(); int j = 0; + //先根据入栈序列,往栈中压入数据 for (int i = 0; i < pushA.length; i++) { - if (pushA[i] == popA[j]) { - tempStack.push(pushA[i]); - - for (; j < popA.length; j++) { - if (popA[j] == tempStack.peek()) { - tempStack.pop(); - } else { - break; - } - } - } else { - tempStack.push(pushA[i]); + //如果当前栈顶元素跟出栈序列当前遍历的元素一样,那么进行出栈处理 + while (stack.size()>0 && j0 && j PrintFromTopToBottom(TreeNode root) { 也可以使用递归实现,深度遍历递归实现 -``` +```java ArrayList list = new ArrayList(); void deepTranverse(TreeNode node) { if(node!=null) { @@ -900,13 +895,14 @@ public static boolean VerifySquenceOfBST(int[] sequence, int start, int end) { } else if(rightChildIndex== start) {//说明全部位于右边子树 return VerifySquenceOfBST(sequence,start,end-1); } + //继续校验 return VerifySquenceOfBST(sequence,start,rightChildIndex-1) && VerifySquenceOfBST(sequence,rightChildIndex, end-1); } ``` ## 题023 二叉树中和为某一值的路径 -输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的 list 中,数组长度大的数组靠前) +输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的 list 中,数组长度大的数组靠前。) 就是递归调用每个节点的左右子树,然后将节点值相加,如果节点值和为某个预期值,并且该节点为叶子节点,那么这条路径就是要找的路径。 @@ -961,7 +957,6 @@ public RandomListNode Clone(RandomListNode pHead) } - //设置新链表的特殊指针 RandomListNode oldCurrentNode= pHead; RandomListNode newCurrentNode; @@ -999,6 +994,7 @@ public RandomListNode Clone(RandomListNode pHead) ## 题025 二叉搜索树与双向链表 + 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 二叉搜索树的中序遍历的结果就是递增的序列,所以递归实现二叉搜索树的中序遍历 @@ -1031,80 +1027,45 @@ public RandomListNode Clone(RandomListNode pHead) 输入描述:输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。 -对于一个字符串的序列,可以看成第一个字符+剩下字符的排列,每次从后面序列中取一个不重复的字符,与第一个字符交换,然后递归地对后面的字符调用函数进行排列。 - -```java -public ArrayList Permutation(String str) { - ArrayList arrayList = new ArrayList<>(); - Permutation(arrayList, str,0,str.length()-1); - return arrayList; -} - -public void Permutation(ArrayList arrayList, String str, int start,int end) { +##### 回溯算法 - if (str == null || str.length() == 0|| start>end) { - return; - } - if (end == start) {//当前只有一个元素 - arrayList.add(str); - return; - } - HashSet set = new HashSet(); - StringBuffer stringBuffer = new StringBuffer(str); - for (int i = start ;i <= end; i++) { - Character currentChar = stringBuffer.charAt(i); - if (set.contains(currentChar)==false) { - set.add(currentChar); - //元素交换 - Character temp = currentChar; - stringBuffer.setCharAt(i,stringBuffer.charAt(start)); - stringBuffer.setCharAt(start,temp); - - //这种写法调用完毕后不用交换,因为传过去的string不会改变当前的stringBuffer - Permutation(arrayList, stringBuffer.toString(), start +1, end); - - } - } -} -``` +对于一个字符串的序列,可以看成第一个字符+剩下字符的排列,每次从后面序列中取一个不重复的字符,与第一个字符交换,然后递归地对后面的字符调用函数进行排列。 +1.遍历字符串,每次取一个不同的字符作为首字符,然后对后面剩余的字符串递归调用方法,对后面的字符串进行排列。 +2.在递归的尽头,也就是当前子字符串只有1个元素,会将所有元素添加到数组。 -``` - public ArrayList Permutation(String string) { - char[] charArray = string.toCharArray(); +```java + public ArrayList Permutation1(String str) { ArrayList list = new ArrayList(); - PermutationHelper(charArray,0,list); - Collections.sort(list); - + if (str==null||str.length()==0) { + return list; + } + Permutation(list,str,0,str.length()-1); return list; } - - public void PermutationHelper(char[] charArray,int start,ArrayList list) { - if (charArray == null || charArray.length == 0) { - return; - } - if(start == charArray.length-1) {//最后一个元素 - list.add(String.valueOf(charArray)); + public void Permutation(ArrayList list,String str,int start,int end) { + StringBuffer stringBuffer = new StringBuffer(str); + HashSet set = new HashSet<>(); + //递归到最后一层,此时只有一个字符了,肯定是排在第一个,将当前字符串添加到list + if (start == end) { + list.add(str); return; } - HashSet set = new HashSet(); - for(int i = start;i GetLeastNumbers_Solution(int [] input, int k) { @@ -1188,13 +1149,13 @@ public ArrayList GetLeastNumbers_Solution(int [] input, int k) { ## 题029连续子数组的最大和 -例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1) +例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第1个开始,到第4个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1) 使用动态规划的方法来进行思考 f(n) 有两种取值 -* 当f(n-1)<=0时,取array[n] +* 当f(n-1)<=0时,取array[n],从这个元素重新开始 * 当f(n-1)>0时,取f(n-1)+array[n] @@ -1204,19 +1165,19 @@ f(n) 有两种取值 if(array==null || array.length==0) { return 0; } - int max=array[0]; int currentSum = array[0]; + int maxSum = currentSum; for (int i = 1; i < array.length; i++) { - if (currentSum<0) { + if (currentSum<0) {//前面的和是负数,就直接丢弃 currentSum = array[i]; } else { currentSum = currentSum + array[i]; } - if (currentSum>max) { - max = currentSum; + if (currentSum>maxSum) { + maxSum = currentSum; } } - return max; + return maxSum; } ``` @@ -1226,7 +1187,7 @@ f(n) 有两种取值 解法一,就是对1到n进行遍历,对每个数统计该数1出现的次数,统计时用这个数x%10,判断个位数是否为1,然后用x=x/10的结果继续%10来进行判断个位数为1,一直到x=0,统计到x包含1的个数,这样的话,一共有N个数,每个数计算的时间复杂度log10 N,总时间复杂度是N*log10 (N)也就是Nlog(N) -解法二:还是对1到n进行遍历,对每个数统计该数1出现的次数,将每个数转换为字符串,判断字符串包含字符"1"的个数,但是将数字转换为字符串的这个过程,由于使用了StringBuffer的append()方法,然后使用了Integer的getChars方法,复杂度还是Log100 (N),所以总复杂度还是Nlog(N) +解法二:还是对1到n进行遍历,对每个数统计该数1出现的次数,将每个数转换为字符串,判断字符串包含字符"1"的个数,但是将数字转换为字符串的这个过程,由于使用了StringBuffer的append()方法,然后使用了Integer的getChars方法,复杂度还是Log10 (N),所以总复杂度还是Nlog(N) ```java public int NumberOf1Between1AndN_Solution(int n) { @@ -1293,7 +1254,9 @@ public Boolean compare(int a, int b) { 把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 -第一个丑数是1,除了1以外,其他丑数都是2,3,5之间相乘得到的,也就是丑数的因子都是2,3,5, +1 2 3 5 + +第一个丑数是1,除了1以外,其他丑数都是2,3,5之间相乘得到的,也就是丑数的因子都是2,3,4,5,6, 第一种解决方案就是从1开始对所有整数遍历,将每个数一直除以2,3,5,看能否除尽,能除尽代表是丑数,一直得到第N个丑数。 @@ -1311,13 +1274,12 @@ public int GetUglyNumber_Solution(int index) { int temp5 = array[index5]*5; int minTemp = temp2 < temp3 ? temp2 : temp3; minTemp = minTemp < temp5 ? minTemp : temp5; - if (temp2 == minTemp) { index2++; } if (temp3 == minTemp) { //可能存在一个丑数可以由多种丑数相乘得到, - // 例如6可以是2*2*2,也可以是2*3,所以这里的三个if需要分开判断赋值 + // 例如12可以是6*2,也可以是4*3,所以这里的三个if需要分开判断赋值 index3++; } if (temp5 == minTemp) { @@ -1369,7 +1331,7 @@ public int GetUglyNumber_Solution(int index) { 对于%100的数据,size<=2*10^5 -逆序对就是前面的数比后面的数大,就是一个逆序对,可以使用归并排序,每次组合并时,右边的组的数比左边的数小时,会出现逆序对,逆序对的个数为左边的组的数组元素个数。 +逆序对就是前面的数比后面的数大,就是一个逆序对,可以使用归并排序,每次组合并时,右边的组的数比左边的数小时,会出现逆序对,逆序对的个数为当前左边的组的元素个数。 ```java public int InversePairs(int [] array) { @@ -1420,7 +1382,7 @@ public int inversePairsort2(int[] array, int left, int right, int[] temp) { } ``` -思路就是先递归对数组进行分组,一直到每个组只有一个元素,然后每个组按大小进行合并,形成一个新的组。每次合并的时间复杂度是N,大概需要合并log(N)次,所以总时间复杂度是 Nlog(N)这样写简单是简单,就是空间复杂度太高了,每次创建新数组,空间复杂度是Nlog(N) +思路就是先递归对数组进行分组,一直到每个组只有一个元素,然后每个组按大小进行合并,形成一个新的组。每次合并的时间复杂度是N,大概需要合并log(N)次,所以总时间复杂度是 Nlog(N),这样写简单是简单,就是空间复杂度太高了,每次创建新数组,空间复杂度是Nlog(N) ```java public static int[] sort(int[] array,int start, int end) { @@ -1655,7 +1617,7 @@ public int IsBalanced_Solution_Depth(TreeNode root) { int right = IsBalanced_Solution_Depth(root.right); if (left!=-1 && right!= -1) { int temp = left-right; - if (temp<=1&& temp>=-1) { + if (temp<=1 && temp>=-1) { return left > right ? left + 1 : right + 1; } } @@ -1763,8 +1725,15 @@ public ArrayList> FindContinuousSequence(int sum) { 汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! +输入字符串S=”abcXYZdef”,要输出“XYZdefabc”,其实就是将前n个字符移动到字符串末尾,直接移动的话很容易实现,不管是直接使用添加字符串的方法,还是直接使用字符串截取的方法,这里是一种新方法,就是将整个字符串翻转,再将length-n的字符翻转,再将后n个字符翻转,这样得到的也是前n个字符移动到末尾的结果。 + +原字符串: abcXYZdef + +整个字符翻转: fedZYXcba + +前length-n个字符翻转: XYZdefcba -输入字符串S=”abcXYZdef”,要输出“XYZdefabc”,其实就是将前n个字符移动到字符串末尾,直接移动的话很容易实现,不管是直接使用添加字符串的方法,还是直接使用字符串截取的方法,这里是一种新方法,就是将前n个字符翻转,再将前n以后的字符翻转,然后对整个字符串翻转,这样得到的也是前n个字符移动到末尾的结果。 +后n个字符翻转得到最终结果:XYZdefabc ```java public String LeftRotateString(String str,int n) { @@ -1894,13 +1863,23 @@ public static void qsort(int[] array, int start ,int end) { 就是约瑟夫环问题 -n为1时,f(n,m)为0 +假设一开始有n个数,每个人的编号为0到n-1,让数到m的人出局,假设出局的人的编号为k,那么k=(m-1)%n; +在n个人中间报数时,每个人的编号是 +0 1 2 ... k k+1 k+2 ... n-1 +当k出局以后,在n-1个人中报数时,每个人的编号是重新从k+1开始计数,原来的编号就映射为下面这样了(原来k+1变成了0,原来的n-1变成了n-1-(k+1)) +n-k-1 ... 0 1 ... n-1 -(k+1) +所以假设从n-1报数时的编号到n个人报数时的编号存在一个映射关系 +假设f(n)代表n个人报数编号 +f(n) = (f(n-1)+k+1)%n = (f(n-1)+ (m-1)%n + 1)%n = +(f(n-1) +m)%n -n>1时, +n为1时,只有一个数,也就是最后剩下的数,所以f(1)为0 -f(n,m)=(f(n-1,m)+m)%n +n>1时,f(n)=(f(n-1)+m)%n -``` +所以就是按照这个公式从2计算到n,得到f(n) + +```java public int LastRemaining_Solution(int n, int m) { if (n<1|| m<1) { return -1; @@ -1917,7 +1896,9 @@ public int LastRemaining_Solution(int n, int m) { 求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。 +解题思路: +因为不能使用for循环,肯定是需要使用递归来进行累加,但是由于不能使用if和三元判断语句,所以结束递归不是特别方便,所以这里是利用了&&语句,当n>0时,后面的语句才会执行,同时利用了赋值语句的值可以用于判断的特性,sum+=Sum_Solution(n-1))>0 ``` public int Sum_Solution(int n) { @@ -1931,13 +1912,15 @@ public int Sum_Solution(int n) { 写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 -就是A^B,A异或B的结果其实就是A,B二进制位中不相同的结果,也就是A+B,但是不进位的相加结果,A+B进位的结果等于(A&B)<<1,所以在循环中一直用A^B+A&B,一直到A&B等于0。 +就是A^B,A异或B的结果其实就是A,B二进制位中不相同的结果,也就是A+B,但是不进位的相加结果,A+B进位的那一部分结果等于(A&B)<<1,所以在循环中一直用A^B+A&B,一直到A&B等于0。 A+B = A^B + A&B<<1 -如果A&B为0的话也就不需要进行进位了,此时就等于A+B = A^B +如果A&B为0的话也就不需要进行进位了,此时就等于A+B = A^B,否则就继续递归调用方法,对A^B + A&B<<1 进行相加 + +递归写法 ```java public int Add(int num1,int num2) { @@ -1948,7 +1931,7 @@ public int Add(int num1,int num2) { } ``` - +for循环写法 ```java public int Add(int num1,int num2) { @@ -1972,9 +1955,9 @@ public int Add(int num1,int num2) { -2147483648 至 2147483647 --2的31次方 2的31次方-1 +-2的31次方 2的31次方 -1 -``` +```java public static int StrToInt(String str) { if (str==null || str.length()==0) { return 0; @@ -2041,7 +2024,7 @@ public boolean duplicate(int numbers[],int length,int [] duplication) { ## 题050 构建乘积数组 -``` +```java 就是B[i] = A[0]A[1]...A[i-1] A[i+1]...*A[n-1],通过拆分成两部分, C[i] = A[0]A[1]...A[i-1] D[i] = A[i+1]...*A[n-1] @@ -2053,12 +2036,14 @@ public int[] multiply(int[] A) { int[] d = new int[A.length]; c[0] = 1; int result1 = c[0]; + //构建数组C for (int i = 1; i < c.length; i++) { result1 = result1 * A[i-1]; c[i] = result1; } d[d.length-1] = 1; int result2 = 1; + //构建数组D for (int i = d.length-2; i >=0; i--) { result2 = result2 * A[i+1]; d[i] = result2; @@ -2074,8 +2059,8 @@ public int[] multiply(int[] A) { 就是使用一个数组来记录字符出现的次数。 -``` -StringBuffer str = new StringBuffer(); +```java + StringBuffer str = new StringBuffer(); int[] table = new int[256];//记录出现次数,0代表0次,1代表1次,2代表2次及2次以上 public void Insert(char ch) { @@ -2086,15 +2071,15 @@ StringBuffer str = new StringBuffer(); } public char FirstAppearingOnce() - { - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if (table[c] == 1) { - return c; - } - } - return '#'; +{ + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (table[c] == 1) { + return c; + } } + return '#'; +} ``` ## 题054 链表中环的入口节点 @@ -2104,13 +2089,13 @@ public char FirstAppearingOnce() 一种方法是遍历整个链表,将每个节点添加到HashSet中,判断是否在HashSet中出现过,第一个重复的节点就是环的入口节点。 -另一种解决方法是,假设存在环,环的长度为x,第一个指针先走x步,然后第二个指针从链表头结点出发,两个指针一起走,当第而个指针刚好走到环入口时,第一个指针正好在环中走了一圈,也在环的入口,此时的节点就是环的的入口节点, +另一种解决方法是,假设存在环,环的长度为x,第一个指针先走x步,然后第二个指针从链表头结点出发,两个指针一起走,当第二个指针刚好走到环入口时,第一个指针正好在环中走了一圈,也在环的入口,此时的节点就是环的的入口节点, 怎么得到环的长度呢,就是一个指针每次走2步,一个指针每次走一步,他们相遇时的节点肯定就是在环中的某个节点,然后这个节点在环中遍历一圈,回到原点,就可以得到环的长度count。 -两个指针从头出发,第一个指针先走count步,然后两个指针每次都只走一步,相遇的地方就是环的入口, +两个指针从头出发,第一个指针先走count步,然后两个指针每次都只走一步,相遇的地方就是环的入口。 -``` +```java public ListNode EntryNodeOfLoop(ListNode pHead) { if (pHead == null || pHead.next==null) { @@ -2160,7 +2145,7 @@ public ListNode EntryNodeOfLoop(ListNode pHead) 使用hashSet的解法 -``` +```java public ListNode EntryNodeOfLoop1(ListNode pHead) { if (pHead==null) { @@ -2209,7 +2194,6 @@ public ListNode deleteDuplication(ListNode pHead) ListNode ourHead = new ListNode(0); ourHead.next = pHead; - int temp = pHead.val; ListNode preNode = ourHead; ListNode currentNode = ourHead.next; @@ -2244,23 +2228,23 @@ public ListNode deleteDuplication(ListNode pHead) 所以当前节点的中序遍历中的下一个节点是 -1.有右子树 +1.当前节点有右子树 ​ 右子树有左节点,一直向下遍历,找到最左的叶子节点。 ​ 右子树没有左节点,就是右子树节点。 -2.没有右子树 +2.当前节点没有右子树 ​ 没有父节点,那么没有下一个节点。 - 有父节点 + 这个节点有父节点 -​ 父节点是左子树,直接返回父节点。 +​ 这个节点父节点是属于左边分支的,直接返回父节点。 -​ 父节点是右子树,一直向上遍历,直到找到一个父节点,他是祖先节点是左节点的,找到就返回祖先节点,找不到就返回空。 +​ 这个节点父节点是属于右边分支的,一直向上遍历,直到找到一个父节点,他是祖先节点是左节点的,找到就返回祖先节点,找不到就返回空。 -``` +```java public TreeLinkNode GetNext(TreeLinkNode pNode) { //这个节点有右子树 @@ -2307,7 +2291,7 @@ public TreeLinkNode GetNext(TreeLinkNode pNode) 假设有另外一种遍历是根节点,右子树,左子树,如果二叉树是对称,那么这两种遍历的结果是一样的,所以使用递归来进行两种遍历,然后在过程中判断两种遍历结果是否一样。 -``` +```java boolean isSymmetrical(TreeNode pRoot) { return isSymmetrical(pRoot,pRoot); @@ -2345,7 +2329,7 @@ public ArrayList> Print(TreeNode pRoot) { int flag = 0;//代表当前遍历的是奇数层还是偶数层。区别在于添加子节点的顺序。 stack1.add(pRoot); while ((flag == 0 && stack1.size()>0) || (flag == 1 && stack2.size()>0)) { - if (flag==0) { + if (flag==0) {//代表是偶数层 ArrayList array = new ArrayList(); while (stack1.size()>0) { TreeNode node = stack1.pop(); @@ -2426,7 +2410,7 @@ public ArrayList> Print(TreeNode pRoot) { 就是使用一个队列queue,一开始将根节点加入queue,并且加入一个null元素到队列中作为标志元素,用来分割每一层,标志这一层的节点都在标志元素的前面。然后对queue中元素出列,每个进行打印,直到出列的元素是null,表示这一层已经结束了,如果queue中还有元素,那么在后面加入null标志元素分割,并且进行换行,打印下一行,如果queue中没有元素就结束循环 -``` +```java ArrayList> Print(TreeNode pRoot) { ArrayList> arrayLists= new ArrayList>(); if (pRoot == null) return arrayLists; @@ -2455,9 +2439,9 @@ ArrayList> Print(TreeNode pRoot) { 就是递归遍历每一个节点,遍历时传入深度depth,将节点加入到ArrayList中特定深度对应的数组中去。 -这种方法也可以用来进行二叉树深度遍历,遍历完之后将嵌套数组拆分成单层的数组。 +这种方法也可以用来进行二叉树先序遍历,遍历完之后将嵌套数组拆分成单层的数组。 -``` +```java ArrayList> Print2(TreeNode pRoot) { ArrayList> arrayLists = new ArrayList>(); if (pRoot==null)return arrayLists; @@ -2494,7 +2478,7 @@ void find(TreeNode pRoot, int depth, ArrayList> arrayLists) { 4.重复步骤3,直到队列元素个数为空。 -``` +```java String Serialize(TreeNode root) { StringBuffer stringBuffer = new StringBuffer(); if (root == null) {return stringBuffer.toString();} @@ -2567,7 +2551,7 @@ Integer convert(String str) { 由于前序遍历是先左子树,根节点,右子树的顺序,所以前序遍历的结果,就是二叉搜索树中元素按递增顺序排列的结果,所以按照前序遍历到第K个元素就是第K小的节点。 -``` +```java Integer index = 0; TreeNode kNode = null; @@ -2599,7 +2583,7 @@ void find(TreeNode node, Integer k) { -``` +```java ArrayList arrayList = new ArrayList(); public void Insert(Integer num) { @@ -2633,7 +2617,7 @@ void find(TreeNode node, Integer k) { 解法一就是维护一个排序好的数组,数组就是当前滑动窗口排序好的结果,每次滑动时将值插入排序好的队列,并且将过期的值删除, -``` +```java public ArrayList maxInWindows1(int[] num, int size) { ArrayList arrayList = new ArrayList(); ArrayList sortList = new ArrayList(size); @@ -2688,7 +2672,7 @@ ArrayList removeValueIntoSorted(ArrayList sortList, int curren 因为这些元素的index比A都小,而且值也比A小,不可能再成为最大值了, 然后判断队列头结点的最大值是否过期,过期的话也删除 -``` +```java public ArrayList maxInWindows(int[] num, int size) { ArrayList arrayList = new ArrayList(); @@ -2727,7 +2711,7 @@ public ArrayList maxInWindows(int[] num, int size) { 就是递归去判断就行了。 -``` +```java public boolean hasPath(char[] matrix, int rows, int cols, char[] str) { boolean[] flag = new boolean[matrix.length]; @@ -2754,7 +2738,7 @@ boolean judge(char[] matrix, int rows, int cols, int i, int j, char[] str, int c return false; } if (charIndex==str.length-1) { return true;} - + //用来记录这个位置是否在路径中 flag[index]=true; if (judge(matrix,rows,cols,i+1, j, str, charIndex+1, flag) ||judge(matrix,rows,cols,i-1, j, str, charIndex+1, flag) @@ -2782,7 +2766,7 @@ boolean judge(char[] matrix, int rows, int cols, int i, int j, char[] str, int c 就是递归去求解,判断每个节点的上,下,左,右节点是否满足需求。 -``` +```java public int movingCount(int threshold, int rows, int cols) { if (rows<0||cols<0||threshold<0) {return 0;} @@ -2862,17 +2846,13 @@ public int cutRope(int target) { } ``` - - - - # 基础篇 ## 二分查找 普通的二分查找 -``` +```java public int findByHalf(int[] array, int target) { if (array==null||array.length==0) { return -1; @@ -2895,7 +2875,7 @@ public int findByHalf(int[] array, int target) { 二分查找-左边界版本,只要改等于时的判断以及后面的越界判断 -``` +```java public static void main(String[] args) { Test002 test002 = new Test002(); int[] array = new int[]{1,2,2,2,2,4,5,6,7}; @@ -2923,195 +2903,3 @@ public int findByHalf(int[] array, int target) { //如果有左边界就返回左边界,没有时,此时的left应该是最接近左边界,并且大于左边界的数的位置 ``` - - -## LRU算法 - -LRU其实就是Last Recent Used,就是最近使用淘汰策略,所以当空间满了时,就根据最近使用时间来删除。一般是使用一个双向链表来实现,同时为了快速访问节点,会使用一个HashMap来存储键值映射关系。(需要注意的是,为了在内存满时删除最后一个节点时,可以以O(1)时间复杂度从HashMap中删掉这个键值对,每个节点除了存储value以外,还需要存储key)。 - -#### 添加新元素的过程: - -首先我们增加两个方法,remove()方法用于删除一个节点,addNewNodeToHead()代表添加一个节点到头部。 - -1.判断节点是否已经存在于链表中,是的话,找出节点, - -1.1更新value, - -1.2调用remove()方法删除节点, - -1.3调用addNewNodeToHead()将节点添加到链表头部。 - -2.节点不存在于链表中,那么判断链表长度是否超出限制, - -2.1是的话remove(lastNode.key) - -2.2创建一个新节点,调用addNewNodeToHead()将节点添加到链表头部。 - - - -remove()方法的细节,主要是更新node的前后节点的next或pre指针,以及更新后需要判断删除的节点是否是headNode或者lastNode,是的话同时需要更新headNode或者lastNode。 - -addNewNodeToHead()方法细节,主要是要先判断head是否为null,是的话说明链表为空,需要将headNode和lastNode都设置为node,不为null就执行添加操作,将headNode.pre设置为node,node的next设置为headNode,headNode=node; - - - -```java -//双向链表 -public static class ListNode { - String key; - Integer value; - ListNode pre = null; - ListNode next = null; - ListNode(String key, Integer value) { - this.key = key; - this.value = value; - } -} -ListNode headNode; -ListNode lastNode; -int limit=4; -HashMap hashMap = new HashMap(); -public void put(String key, Integer val) { - ListNode existNode = hashMap.get(key); - if (existNode!=null) {//有老的节点,只是更新值,先从链表移除,然后从头部添加 - existNode.value=val; - remove(key); - addNewNodeToHead(existNode); - } else { - //达到限制,先删除尾节点 - if (hashMap.size() == limit) { remove(lastNode.key); } - ListNode newNode = new ListNode(key,val); - addNewNodeToHead(newNode); - } -} -public ListNode get(String key) { - - ListNode node = hashMap.get(key); - if(node == null) { - return null; - } - remove(node.key); - addNewNodeToHead(node); - return node; -} -public void remove(String key) { - ListNode deleteNode = hashMap.get(key); - hashMap.remove(key); - ListNode preNode = deleteNode.pre; - ListNode nextNode = deleteNode.next; - //删除操作需要更新pre节点的next指针和next节点的pre指针,以及更新head和last - if (preNode!=null) { preNode.next = nextNode; } - if (nextNode!=null) { nextNode.pre = preNode; } - if (headNode == deleteNode) { headNode = nextNode; } - if (lastNode == deleteNode) { lastNode = preNode; } -} -private void addNewNodeToHead(ListNode node) { - hashMap.put(node.key,node); - if (headNode==null||lastNode==null) { - headNode = node; - lastNode = node; - return; - } - headNode.pre = node; - node.next = headNode; - headNode = node; -} -``` - -##### 使用LinkedHashMap实现的算法 - -使用LinkedHashMap实现LRU算法, - -* LinkedHashMap默认的accessOrder为false,也就是会按照插入顺序排序, - 所以在插入新的键值对时,总是添加在队列尾部, - 如果是访问已存在的键值对,或者是put操作的键值对已存在,那么需要将键值对先移除再添加。 -* 如果是将accessOrder设置为true,get已有键值对时就不需要删除key了,会自动调整顺序,put方法需要在添加或者更新键值对后调用LinkedHashMap#get()访问key,调整顺序。 - -```java -//accessOrder为false,按照插入顺序排序的写法 -public static class LRUCache { - int capacity; - Map map; - public LRUCache(int capacity) { - this.capacity = capacity; - map = new LinkedHashMap<>(); - } - public int get(int key) { - if (!map.containsKey(key)) { - return -1; - } - //先删除旧的位置,再放入新位置 - Integer value = map.remove(key); - map.put(key, value); - return value; - } - - public void put(int key, int value) { - if (map.containsKey(key)) { - map.remove(key); - map.put(key, value); - return; - } - map.put(key, value); - //超出capacity,删除最久没用的,利用迭代器,删除第一个 - if (map.size() > capacity) { - map.remove(map.keySet().iterator().next()); - } - } -} -``` -accessOrder为true,按照访问顺序排序的实现方法 -```java - -public static class LRUCache2 { - int capacity; - LinkedHashMap linkedHashMap; - LRUCache2(int capacity) { - this.capacity = capacity; - //如果要修改accessOrder只能使用这种构造器方法来创建LinkedHashMap - linkedHashMap = new LinkedHashMap(16,0.75f,true); - } - public int get(int key) { - Integer value = linkedHashMap.get(key); - return value == null ? -1 : value; - } - public void put(int key, int val) { - linkedHashMap.put(key, val); - if (linkedHashMap.size() > capacity) { - linkedHashMap.remove(linkedHashMap.keySet().iterator().next()); - } - } - //通过调用get()方法访问key来调整顺序 - linkedHashMap.get(key); -} -``` - -### 【面试算法题】阿拉伯数字转化为中文读法 - -例如我们要将10100转换为中文,总体流程就是先拿10100/1个亿,发现结果为0,说明不会包含亿这个数量级,然后10100/1万,得到结果result为1,余数remain为100,说明包含万这个数量级,我们的结果肯定是等于 "result的中文表示"+单位"万"+"余数的中文表示",所以就对问题进行了分解,f(n) = f(n/数量级)+数量级单位+f(n%数量级) - -```java - static String[] nameArray1 = {"","一","二","三","四","五","六","七","八","九"}; - static String[] nameArray2 = {"","十","百","千","万","亿"}; - static int[] intArray = {1,10,100,1000,10000,100000000}; - - public static String numToChinese(int num) { - for (int i = intArray.length-1; i >= 0; i--) { - int part1 = num/intArray[i]; - int part2 = num%intArray[i]; - if (i==0) {//到个位了 - return nameArray1[part1]; - } - if (part1>0) { - //整除部分,例如10100,整除部分就是十 - String left = numToChinese(part1); - //整除部分的单位,例如10100,整除部分的单位就是万 - String unitString = nameArray2[i]; - //余数部分,例如10100,余数部分就是一百 - String right = numToChinese(part2); - return left + unitString + right; - } - } - return ""; - } -``` \ No newline at end of file diff --git a/docs/Coding_Array.md b/docs/Coding_Array.md new file mode 100644 index 0000000..940ecc3 --- /dev/null +++ b/docs/Coding_Array.md @@ -0,0 +1,1236 @@ +(PS:扫描[首页里面的二维码](README.md)进群,分享我自己在看的技术资料给大家,希望和大家一起学习进步!) + +## 数组专题 + +### 剑指Offer部分 + +#### [题003 二维数组中的查找](#题003) + +#### [题008旋转数组](#题008) + +#### [题012调整数组顺序使奇数排在前面](#题012) + +#### [题018顺时针打印矩形](#题018) + +#### [题027数组中出现的次数超过一半的数字](#题027) + +#### [题028最小的k个数](#题028) + +#### [题029连续子数组的最大和](#题029) + +#### [题030从1到n中出现的整数中1出现的次数](#题030) + +#### [题031把数组排成最小的数](#题031) + +#### [题032返回第N个丑数](#题032) + +#### [题034 数组中的逆序对](#题034) + +#### [题036 数字在排序数组中出现的次数](#题036) + +#### [题039 数组中只出现一次的数组](#题039) + +#### [题040 和为S的连续正数序列](#题040) + +#### [题041 和为S的两个数字](#题041) + +#### [题044 扑克牌顺子](#题044) + +#### [题049 数组中重复的数字](#题049) + +#### [题050 构建乘积数组](#题050) + +#### [题062 数据流的中位数](#题062) + +#### [题063 滑动窗口的最大值](#题063) + +#### [题064 矩阵中的路径](#题064) + +### 题003 二维数组中的查找 + +##### 题目内容: + +![image-20201202111714727](../static/image-20201202111714727.png) + +在一个二维[数组](https://cuijiahua.com/blog/tag/数组/)中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维[数组](https://cuijiahua.com/blog/tag/数组/)和一个整数,判断数组中是否含有该整数。 + +如果在一个二维数组中找到数字7,则返回true,如果没有找到,则返回false。 + +##### 思路 + +就是从右上角开始遍历,假设要查找的数为A,当前遍历的数为B,B的特点是B所在行里面最大的数,也是B所在列最小的数,如果遍历的数BA,那么B所在的列可以排除(比B都大)。 + +##### 代码: + +```java + static boolean find(int target, int [][] array) { + int rowLength = array.length;//总行数 + int colLength = array[0].length;//总列数 + + int currentRow = 0;//起始遍历位置是右上角,行号为0 + int currentCol = colLength - 1;//起始遍历位置是右上角,列号为最大值 + while (currentRow = 0) {//防止超出边界 + if (array[currentRow][currentCol] == target) { + return true; + } else if (array[currentRow][currentCol] > target) {//比要找的数大,那么排除更大的数,也就是排除这一列 + currentCol--; + } else {//比要找的数小,那么排除更小的数,也就是排除这一行 + currentRow++; + } + } + return false; +} +``` + +##### 总结 + +注意这个currentRow = 0判断条件,防止越界。 + +## 题008旋转数组 + +把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 + +##### 解题思路: + +旋转数组其实就是一个递增的数组,整体移动了一下元素,类似3,4,5,1,2这种。要查找最小的元素,可以遍历一遍数组,复杂度为O(N),这样就太暴力了,因为这个旋转数组其实是有规律的,可以根据左边界,右边界,中间值来判断最小值的位置 + +* 左边界<=中间值 说明左边界到中间值这一段是递增的,也就是最小值不处于这一段。这样可以排除掉这一段,然后去另一段里面遍历查找。 + +* 中间值<=右边界 说明中间值到右边界这一段是递增的,也就是最小值不处于这一段。这样可以排除掉这一段,然后去另一段里面查找。 + + 一直排除到最后,右边界下标-左边界下标==1时,说明左边界是最大值,右边界是最小值,此时整个循环结束。 + +* 特殊情况 左边界== 中间值==右边界 说明无法判断最小值位于哪里,只能从左边界到右边界进行遍历然后获得最小值。 + +```java +int minNumberInRotateArray(int[] array) { + if (array[0]= array[end]为基础的,一旦右边界值>左边界值,说明现在的顺序就是完全递增的,那么就返回左边界。 + while (array[start] >= array[end]) { + System.out.println(start+"======"+mid+"====="+end); + if (end-start == 1) { + return array[end]; + } + mid = (end + start)/2; + if (array[start] == array[mid] && array[start] == array[end]) {//左边界,中间值,右边界相等 + int min = array[start]; + for (int i = start+1; i <=end ; i++) { + if (array[i]< min) { + min = array[i]; + } + } + return min; + } + if ( array[mid]>=array[start]){ + start = mid; + } else if(array[mid]<=array[end]) { + end = mid; + } + } + return array[mid]; + } +``` + +## 题012调整数组顺序使奇数排在前面 + +输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。 + +##### 使用额外的内存空间的解法 + +如果可以使用额外的内存空间,可以对数组遍历两遍,一遍将奇数取出,存放在额外的数组中去,一遍把剩下的偶数存放到额外的数组中去。 + +##### 类似冒泡排序的解法 + +如果不能使用额外的内存空间,就是查找到第一个奇数,然后与前面的偶数元素整体往后挪一位,将该奇数设置到前面第一个偶数的位置,有点像是冒泡排序,复杂度其实是O(n^2)。(由于本题需要保证奇数与奇数的顺序,偶数与偶数的顺序,所以需要使用这种类似于冒泡排序的算法,如果不保证顺序,找到奇数后就于第一个偶数交换位置就行,复杂度是O(N)) + +冒泡排序是其实是交换,从头开始,依次判断两个相邻的元素,将更大的元素向右交换,遍历一次后可以将当前序列最大的元素交换到最后面去,下次遍历就不用管最后一个元素。 + +```java + public static void reOrderArray1(int [] array) { + if (array==null || array.length==0) { + return; + } + int saveIndex=0;//第一个偶数的位置,用于存后面找到的第一个奇数 + for (int i = 0; i < array.length; i++) { + if (array[i]%2==1) {//找到奇数 + int temp = array[i];//将奇数保存 + //将奇数前面的数都往后挪 + for (int j = i; j >saveIndex; j--) { + array[j] = array[j-1]; + } + array[saveIndex] = temp; + saveIndex++; + } + } + } +``` + +## 题018 顺时针打印矩形 + +输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: +1 2 3 4 +5 6 7 8 +9 10 11 12 +13 14 15 16 +则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10. + +就是将矩形外面一圈打印完,外面打印时可以分为四块,上面,右边,下面,左边。然后递归打印剩下的部分,当矩阵是正常情况下时,这么打印是没有问题,如果矩阵只有一个元素,只有一行,只有一列,这么打印就会有问题,所以我们将特殊情况单独列出来了。 + +```java + public ArrayList printMatrix2(int [][] matrix) { + ArrayList arrayList = new ArrayList(); + printMatrix2(arrayList,matrix,0,matrix.length-1,0,matrix[0].length-1); + return arrayList; + } + public ArrayList printMatrix2(ArrayList arrayList, int [][] matrix,int rowStart,int rowEnd,int colStart,int colEnd) { + if (rowStart>rowEnd||colStart>colEnd) { + return arrayList; + } + //我们采取的策略是按照左闭右开区间进行打印,也就是[rowStart,roWend),对于每一边打印时, + // 只包含左边界的值,不包含右边界的值 + // 比如二维矩阵为 + // 1 2 3 + // 4 5 6 + // 7 8 9 + // 打印顺序是先打印 + // 1,2 + //3,6 + //9,8 + //7,4 + //4 + //这种打印方法的缺点是只有一个元素,只有一行,只有一列时,打印就会有问题,所以我们将这些情况单独列出来。 + //特殊情况1 只有一个元素 + if (rowStart == rowEnd && colStart == colEnd) { + arrayList.add(matrix[rowStart][colEnd]); + return arrayList; + } + //特殊情况2 只有一列, + if (colStart == colEnd) { + for (int i = rowStart; i <=rowEnd ; i++) { + arrayList.add(matrix[i][colStart]); + } + return arrayList; + } + //特殊情况2 只有一行 + if (rowStart == rowEnd) { + for (int i = colStart; i <= colEnd; i++) { + arrayList.add(matrix[rowStart][i]); + } + return arrayList; + } + //打印上边 + for (int i = colStart;icolStart;i--) { + arrayList.add(matrix[rowEnd][i]); + } + //打印左边 + for (int i = rowEnd;i>rowStart;i--) { + arrayList.add(matrix[i][colStart]); + } + printMatrix2(arrayList,matrix,rowStart+1,rowEnd-1,colStart+1,colEnd-1); + return arrayList; + } +``` + +##### 解法二: + +```java +public ArrayList printMatrix(int [][] matrix) { + if(matrix==null) { + return null; + } + ArrayList arrayList = new ArrayList(); + return printMatrix(arrayList, matrix, 0, matrix.length-1,0,matrix[0].length -1); + } + + public ArrayList printMatrix(ArrayList arrayList, int [][] matrix,int rowStart, int rowEnd, int colStart, int colEnd) { + if (rowStart> rowEnd || colStart>colEnd) { + return arrayList; + } + for (int i = colStart; i <=colEnd;i++) { + arrayList.add(matrix[rowStart][i]); + } + for (int i = rowStart+1; i <=rowEnd-1;i++) { + arrayList.add(matrix[i][colEnd]); + } + for (int i = colEnd; i >=colStart&&rowEnd>rowStart;i--) {//要加rowEnd>rowStart判断,不然对于单行情况会重复打印 + arrayList.add(matrix[rowEnd][i]); + } + for (int i = rowEnd-1; i >=rowStart+1&& colStart < colEnd;i--) {//要加rowEnd>rowStart判断,不然对于单列情况会重复打印 + arrayList.add(matrix[i][colStart]); + } + printMatrix(arrayList, matrix,rowStart+1,rowEnd-1, colStart+1,colEnd-1); + return arrayList; + } +``` + +## 题027数组中出现的次数超过一半的数字 + +数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出 2 。如果不存在则输出 0 。 + +#### 解题思路: + +##### 只适用于本题的特殊解法 + +就是一个数组,假设包含一个超过次数一半的元素,那么去除掉两个不相等的元素后,剩下的数组中,这个元素还是会出现次数超过一半。 + +(原理就是每次排除两个不相等的元素,最后剩下的一个元素,或者两个元素一定是次数超过一半的这个数字。) + +```java +public int MoreThanHalfNum_Solution(int [] array) { + if (array==null||array.length==0) { + return 0; + } + if (array.length==1) {//只有一个元素,直接返回 + return array[0]; + } + int result = array[0]; + int times = 1; + for (int i = 1; i < array.length; i++) { + if (times == 0) { + times = 1; + result = array[i]; + } else if (array[i] == result) {//相同就是times+1 + times++; + } else {//不相同就将times-1,进行抵消 + times--; + } + } + //下面就是判断这个数字是否满足条件,因为也有可能存在1,1,2,2,3这种数组,最后导致result是3,但是3其实不是超过一半的元素,所以需要重新遍历判断。 + int statTimes = 0; + for (int i = 0; i < array.length; i++) { + if (array[i] == result) { + statTimes++; + } + } + if (statTimes>array.length/2) { + return result; + } + return 0; +} +``` + +##### 快排解法 + +另外一种思路就是假设该数组存在数量超过一半的元素A,并且数组排好序,那么元素A一定是数组的中位数,A的下标一定是n/2,所以此题可以转换为找出数组第n/2小的元素,找出元素A后,然后对数组进行遍历,判断A的出现次数是否超过了一半。但是这种解法的最坏时间复杂度是O(N^2); + +```java +//[1,2,3,2,4,2,5,2,3] + public int MoreThanHalfNum_Solution1(int[] array) { + if (array==null||array.length==0) { + return 0; + } + if (array.length==1) { + return array[0]; + } + int value = quickSort(array,0,array.length-1,array.length/2); + int times = 0; + for (int i = 0; i < array.length; i++) { + if (array[i] == value) { + times++; + } + if (times>array.length/2) { + return value; + } + } + return 0; + } + //寻找第K小的元素 + public int quickSort(int[] array,int start ,int end,int K) { + if (start>=end) { + return array[start]; + } + int base = array[start]; + int i = start; + int j = end; + while (ibase&&j>i) {j--;} + while (array[i]<=base&&iK) {//第K小的元素在左边 + return quickSort(array,start, i-1,K); + } else {//第K小的元素在右边 + return quickSort(array,i+1,end,K); + } + } +``` + +## 题028最小的k个数 + +输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。 + +##### 插入排序普通版 + +其实就是插入排序,只不过只是对k个数进行插入排序,复杂度O(N^2) + +```java +public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList arrayList = new ArrayList(); + if(input==null || input.length==0 ||input.length arrayList.get(arrayList.size()-1)) {//子数组个数达到了K,并且当前数比子数组最后一个数大 + continue; + } else if (input[i] < arrayList.get(arrayList.size()-1)) { + arrayList.remove(arrayList.size()-1); + arrayList.add(input[i]); + } + + //将最后一个元素移动合适的位置 + for (int j = arrayList.size()-1; j > 0 ; j--) { + if (arrayList.get(j) < arrayList.get(j-1)) { + int temp = arrayList.get(j); + arrayList.set(j, arrayList.get(j-1)); + arrayList.set(j-1, temp); + } + } + + } + return arrayList; + } +``` + +##### 解法二 插入排序优化版(根据二分查找插入的位置) + +时间复杂度N*log(N) + +```java +public ArrayList GetLeastNumbers_Solution1(int [] input, int k) { + ArrayList arrayList = new ArrayList<>(); + if (input==null||input.lengthk) { + arrayList.remove(arrayList.size()-1); + } + } + return arrayList; +} +//使用二分查找插入的位置 +void addNewElement(ArrayList arrayList, int target) { + if (target<=arrayList.get(0)) { + arrayList.add(0,target); + } else if (target>= arrayList.get(arrayList.size()-1)) { + arrayList.add(target); + } else {//使用二分查找,查找左边界,就是左边界右边的值就是>=target的 + int left = 0; + int right = arrayList.size(); + while (left target) { + //因为是寻找左边界,此时middle可能是需要的下标 + right = middle; + } + } + arrayList.add(left,target); + + } +} +``` + +##### 解法三 堆排 + +```java +public ArrayList GetLeastNumbers_Solution2(int [] input, int k) { + ArrayList arrayList = new ArrayList<>(); + if (input == null || input.length==0|| input.length=0 ; i--) { + adjustHeap(input,i,k); + } + for (int i = k; i < input.length; i++) { + if (input[i] < input[0]) { + swap(input,0,i); + adjustHeap(input,0,k); + } + } + for (int i = k-1; i >=0; i--) { + arrayList.add(input[i]); + } + return arrayList; +} +void adjustHeap(int[] input, int i ,int length) { + while (2*i+1 input[left] ? right:left; + maxIndex = input[i] > input[maxIndex] ? i : maxIndex; + if (maxIndex==i) { + break; + } else if (maxIndex == left) { + swap(input, i, left); + i = left; + } else if (maxIndex == right) { + swap(input, i, right); + i = right; + } + } else { + if (input[left] > input[i]) { + swap(input,i,left); + i = left; + } else { + break; + } + } + } +} +void swap(int[] input, int i, int j) { + int temp = input[i]; + input[i] = input[j]; + input[j] = temp; +} +``` + +##### 快排解法 + +平均时间复杂度是O(N),最坏的时间复杂度是O(N^2)。 + +最坏的时间复杂度出现的情况是当数组是以排好序的数组,每次取的基准元素都比数组中其他元素小,这样就每次分割就只能分割出1个元素,每次分割时间复杂度为O(N),需要分割K次,当K趋近与N时,复杂度就是O(N^2)。 + +```java +public ArrayList GetLeastNumbers_Solution3(int[] input, int k) { + ArrayList arrayList = new ArrayList(); + if (input==null||input.length=end) { + return; + } + int base = array[start]; + int i = start; + int j = end; + while (ibase&&j>i) {j--;} + while (array[i] <= base &&j>i) {i++;} + if (iK-1) { + quickSort(array,start,i-1,K); + } +} +``` + +## 题029连续子数组的最大和 + +例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和(子向量的长度至少是1) + +##### 解法一 + +```java + public int FindGreatestSumOfSubArray(int[] array) { + if(array==null || array.length==0) { + return 0; + } + int max=array[0];//最大的连续子数组和 + int currentSum = array[0];//前面的节点当前的累计值 + for (int i = 1; i < array.length; i++) { + if (currentSum<0) {//累计值<0,直接丢掉 + currentSum = array[i]; + } else {//累计值>0,不管现在的值是否为正数,都需要累加,不然就断了 + currentSum = currentSum + array[i]; + } + //超过最大值,保存到max + max = currentSum>max ? currentSum : max; + } + return max; + } +``` + +##### 动态规划的解法 + +使用动态规划的方法来进行思考 + +f(n) 有两种取值 + +* 当f(n-1)<=0时,取array[n] + +* 当f(n-1)>0时,取f(n-1)+array[n] + + 所以可以使用动态规划来解这道题。 + +## 题030从1到n中出现的整数中1出现的次数 + +求出1~13的整数中 1 出现的次数,并算出 100~1300 的整数中1出现的次数?为此他特别数了一下 1~13 中包含1的数字有 1、10、11、12、13 因此共出现 6 次,但是对于后面问题他就没辙了。ACMer 希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。 + +##### 解法一 + +就是对1到n进行遍历,对每个数统计该数1出现的次数,统计时用这个数x%10,判断个位数是否为1,然后用x=x/10的结果继续%10来进行判断个位数为1,一直到x=0,统计到x包含1的个数,这样的话,一共有N个数,每个数计算的时间复杂度log10 N,总时间复杂度是N*log10 (N)也就是Nlog(N) + +```java +public int NumberOf1Between1AndN_Solution2(int n) { + if (n<=0) {return 0;} + int count = 0; + for (int i = 1; i < n; i++) { + while (i>0) { + if (i%10==1) { + count++; + } + i=i/10; + } + } + return 0; +} +``` + +##### 解法二 + +还是对1到n进行遍历,对每个数统计该数1出现的次数,将每个数转换为字符串,判断字符串包含字符"1"的个数,但是将数字转换为字符串的这个过程,由于使用了StringBuffer的append()方法,然后使用了Integer的getChars方法,复杂度还是Log100 (N),所以总复杂度还是Nlog(N)。 + +##### 解法三 + +这种解法就是自己去找数学规律了 + +```java +public int NumberOf1Between1AndN_Solution(int n) { + int count = 0; + for (int i = 1; i <= n; i *= 10) { + int a = n / i,b = n % i; + //之所以补8,是因为当百位为0,则a/10==(a+8)/10, + //当百位>=2,补8会产生进位位,效果等同于(a/10+1) + count += (a + 8) / 10 * i + ((a % 10 == 1) ? b + 1 : 0); + } + return count; +} +``` + +## 题031把数组排成最小的数 + +输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。 + +##### 解题思路 + +本题其实就是对数组中的元素进行排序,只不过元素之间比较的不是值大小,而是字典序的大小,是以AB和BA的大小来决定的,所以对A和B进行拼接成,AB和BA,通过字符串比较,判断AB和BA的大小,AB>BA。这里主体主要使用冒泡排序 + +```java +public String PrintMinNumber(int [] numbers) { + if(numbers.length==1) { + return new StringBuffer().append(numbers[0]).toString(); + } + StringBuffer stringBuffer = new StringBuffer(); + for (int i = numbers.length; i > 0; i--) { + for (int j = 1; j < i ; j++) { + if (compare(numbers[j-1], numbers[j])) {//numbers[j]更大,需要交换到后面去 + int temp = numbers[j]; + numbers[j] = numbers[j-1]; + numbers[j-1] = temp; + } + } + } + for (int i = 0; i < numbers.length; i++) { + stringBuffer.append(numbers[i]); + } + return stringBuffer.toString(); +} +//判断a和b的字典序的大小的秘诀是,拼接两个字符串ab,ba,判断两个字符串,前面的字符大小 +public Boolean compare(int a, int b) { + String first = new StringBuffer().append(a).append(b).toString(); + String second = new StringBuffer().append(b).append(a).toString(); + for (int i = 0; i < first.length(); i++) { + Character char1 = first.charAt(i); + Character char2 = second.charAt(i); + if (char1.equals(char2)) { + continue; + } else if (char1 > char2) { + return true; + } else { + return false; + } + } + return true; +} +``` + +## 题032返回第N个丑数 + +把只包含质因子2、3和5的数称作丑数。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 + +第一个丑数是1,除了1以外,其他丑数都是2,3,5之间相乘得到的,也就是丑数的因子都是2,3,5, + +第一种解决方案就是从1开始对所有整数遍历,将每个数一直除以2,3,5,看能否除尽,能除尽代表是丑数,一直得到第N个丑数。 + +第二种解决方案就是用一个数组来存丑数,将2,3,5当前对应的最小丑数乘以2,3,5,取最小值,作为最新的丑数,一直计算到第N个丑数。 + +使用index2,index3,index5记录上一次乘了2,3,5的数的最小值 + +```java +public int GetUglyNumber_Solution(int index) { + if (index == 0) return 0; + int[] array = new int[index]; + array[0] = 1;//最小的丑数是1 + int index2 =0 ,index3 = 0, index5 = 0;//分别代表上一次乘了2,3,5的index + for (int i = 1; i< index;i++){ + int temp2 = array[index2]*2; + int temp3 = array[index3]*3; + int temp5 = array[index5]*5; + int minTemp = temp2 < temp3 ? temp2 : temp3; + minTemp = minTemp < temp5 ? minTemp : temp5; + if (temp2 == minTemp) { + index2++; + } + if (temp3 == minTemp) { + //可能存在一个丑数可以由多种丑数相乘得到, + // 例如6可以是2*2*2,也可以是2*3,所以这里的三个if需要分开判断赋值 + index3++; + } + if (temp5 == minTemp) { + index5++; + } + array[i] = minTemp; + } + return array[index-1]; +} +``` + + +## 题034 数组中的逆序对 + +在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007 + +输入描述: 题目保证输入的数组中没有的相同的数字 + +数据范围: + + 对于%50的数据,size<=10^4 + + 对于%75的数据,size<=10^5 + + 对于%100的数据,size<=2*10^5 + +逆序对就是前面的数比后面的数大,就是一个逆序对,可以使用归并排序,每次组合并时,右边的组的数比左边的数小时,会出现逆序对,逆序对的个数为左边的组的数组元素个数。 + +举例:假设数组是[9,8 ,5,4 ,11,7,6,23] + +分成四组,分别是 + +A组9,8 + +B组5,4 + +C组 11,7 + +D组6,23 + +那么对于5来说,它的逆序对其实是等于9在B组内的逆序对,5大于5,也就是1,然后是5对于A组,C组和D组的逆序对,所以可以进行归并排序来进行分组,然后进行数组归并,每次合并时,右边的组的数比左边的数小时,会出现逆序对,逆序对的个数为左边的组的数组元素个数。 + +```java +public int InversePairs(int [] array) { + if (array==null||array.length==0) {return 0;} + int[] tempArray = new int[array.length]; + return mergeSort(array,0,array.length-1,tempArray); + } + + int mergeSort(int[] array,int start,int end,int[] tempArray) { + int count = 0; + if (start>=end) { + return 0; + } + int middle = start + (end - start)/2; + //对左半部分,右半部分进行分组,然后进行合并 + count += mergeSort(array,start,middle,tempArray); + count += mergeSort(array,middle+1,end,tempArray); + //进行合并 + int i = start; + int j = middle+1; + int currentIndex = start; + while (i<=middle&&j<=end) { + if (array[j]1000000007?count%1000000007:count; + tempArray[currentIndex++] = array[j++]; + } else { + tempArray[currentIndex++] = array[i++]; + } + } + while (i<=middle) { + tempArray[currentIndex++] = array[i++]; + } + while (j<=end) { + tempArray[currentIndex++] = array[j++]; + } + //将临时数组的值拷贝到原数组 + for (int k = start; k <= end; k++) { + array[k] = tempArray[k]; + } + return count; + } +``` + +## 题036 数字在排序数组中出现的次数 + +统计一个数字在排序数组中出现的次数。 + +正常的二分查找是这样的,找不到时就会返回-1,所以此题可以计算K+0.5应该插入的位置减去 K-0.5应该插入的位置,就得到K的个数了 + +```java + public int GetNumberOfK1(int [] array , int k) { + if (array==null||array.length==0) { + return 0; + } + return left_bound(array,k+0.5) - left_bound(array,k-0.5); + } +//查找左边界,也就是>=taeget的元素的最小下标 + int left_bound(int[] array, double target) { + if (array==null||array.length == 0) return -1; + int left = 0; + int right = array.length-1; // 注意 + while (left<=right) { + int middle = left + (right -left)/2; + if (array[middle] == target) { + right = middle-1; + } else if(array[middle] < target) { + left = middle+1; + } else if(array[middle]>target) { + right = middle-1; + } + } +// if (left>=array.length) { +// return -1; +// } + return left; + } +``` + +## 题039 数组中只出现一次的数 + +一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。 + +数组中有两个数字只出现了一次,其他数字都出现了两次, + +因为A异或A的结果是0,所以对数组遍历异或后的结果result,出现两次的数字异或结果为0, 所以result其实是两个只出现一次的数字B和C的异或结果,并且因为B,C不相等,所以result肯定也不等于0,result肯定有一位是1,在这一位上,肯定B,C中一个为1,一个为0,所以可以根据这一位将数组分成两个子数组,这样每个子数组只会包含一些出现过两次的数字和B,C中的一个,所以对两个子数组异或只会的结果就可以得到B和C。 + +```java + + public void FindNumsAppearOnce2(int [] array,int num1[] , int num2[]) { + if (array == null || array.length<=1) { + return; + } + int result = 0; + for (int i = 0; i < array.length; i++) { + //由于相同的数异或后会变成0,result其实是只出现一次的元素a和元素b的异或结果a^b + result = result ^ array[i]; + } + //bit通过跟a,b的异或结果result进行&元素,得到a,b不相同的第一个位置bit位 + int bit = 1; + while (bit<=result) { + if ((result&bit) > 0) { + break; + } else{ + bit = bit<<1; + } + } + int a=0; + int b=0; + //在bit位进行分组,bit位为1的为一组,一起异或,bit位为0的为一组,一起异或,其中a,b肯定是在不同组 + for (int i = 0; i < array.length; i++) { + if ((array[i]&bit)==0) { + a = a^array[i]; + } else { + b = b^array[i]; + } + } + num1[0] = a; + num2[0] = b; + } + +``` + +## 题040 和为S的连续正数序列 + +输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序。 + +##### 滑动窗口解法 + +就是使用滑动窗口来实现,当两个下标相差1时,计算的和还比sum大,这个时候会进行low++,会使得low==high,跳出循环,例如sum是100,那么在low=high=51时跳出循环 + +```java +public ArrayList> FindContinuousSequence(int sum) { + ArrayList arrayList = new ArrayList>(); + int low=1,high=2; + while (low(); + for (int k =low;k<=high;k++) { + tempArrayList.add(k); + } + arrayList.add(tempArrayList); + low++; + } else if (currentSum FindNumbersWithSum(int [] array, int sum) { + ArrayList arrayList = new ArrayList(); + int low = 0; + int high = array.length-1; + + while (low=1 && numbers[i]!=0&&numbers[i-1]!=0) { + if (numbers[i] == numbers[i-1]) { + return false; + } + needKing += numbers[i] - numbers[i-1]-1; + } + } + if (needKing>count) { + return false; + } + return true; +} + +//快排写法 +public static void qsort(int[] array, int start ,int end) { + if (start>=end) {return;} + int threhold = array[start];//阀值 + int i = start; + int j = end; + while (i < j) { + while (array[j] >= threhold && i < j) j--;//从右端找的应该需要写在前面 + while (array[i] <= threhold && i < j) i++; + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + //将现在最中间的数与最左端的数(也就是阀值)交换 + array[start] = array[i]; + array[i] = threhold; + qsort(array, start, i - 1); + qsort(array, i + 1, end); + +} +``` + +## 题049 数组中重复的数字 + +在一个长度为n的数组里的所有数字都在0到 n-1 的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 + +解法一就是在遍历时将每个元素添加到hashSet,通过判断hashset中是否包含当前元素,来判断是否重复,由于这个长度为n数组中元素的取值范围是0-n-1,所以可以使用一个长度为n的数组array来代替hashSet记录元素是否出现,例如x出现了,将数组array[x]设置为1。 + +解法二就是将当前数组作为标记数组,每次遍历到下标为i的元素时,将array[array[i]]与当前元素交换,并且将array[array[i]]设置为-1,代表已经这个元素是重复元素,然后i- -,继续遍历交换后的这个元素。 + +```java +public boolean duplicate(int numbers[],int length,int [] duplication) { + if (numbers == null || numbers.length==0) { + return false; + } + for (int i=0;i=0; i--) { + result2 = result2 * A[i+1]; + d[i] = result2; + } + for (int i = 0; i < b.length; i++) { + b[i] = c[i]*d[i]; + } + return b; +} +``` + +## 题062 数据流的中位数 + +如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用 `Insert()` 方法读取数据流,使用 `GetMedian()` 方法获取当前读取数据的中位数。 + +可以使用插入排序来实现,就是每次插入元素,将元素与排好序的元素逐一比较,插入已排序后的数组,返回中位数时直接根据下标来获取 + +```java + ArrayList arrayList = new ArrayList(); + + public void Insert(Integer num) { + if (arrayList.size()==0) { + arrayList.add(num); + } else { + for (int i = arrayList.size()-1; i >=0 ; i--) { + if (arrayList.get(i)=num && i==0) {//插入的元素比数组中所有元素都小的情况 + arrayList.add(0,num); + } + } + } + } + + public Double GetMedian() { + if (arrayList.size()%2==1) { + return arrayList.get(arrayList.size()/2).doubleValue() ; + } else { + return (arrayList.get(arrayList.size()/2-1).doubleValue() + arrayList.get(arrayList.size()/2).doubleValue())/2; + } + } +``` + +## 题063 滑动窗口的最大值 + +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组`{2,3,4,2,6,2,5,1}`及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为`{4,4,6,6,6,5}`; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: `{[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}`。 + +解题思路 + +思想主要是插入排序的思想,就是遍历数组,每次往双端队列后面插入元素,并且可以把比队尾比当前元素小的数都移除掉。插入到特定位置,队列是按顺序排序的,如果队头元素的下标超出了滑动窗口,就可以把它移除,一直到队头元素没有过期,这样就是这个此时的最大元素了。 + +其实插入排序的复杂度是O(N^2),因为需要对N个元素插入到排序好的序列,而每次插入的复杂度都是N,本题由于从队列尾部进行插入会将那些比较小的数移除掉,所以每次插入的时间复杂度是一个常数,总时间复杂度是O(N), + +具体步骤: + +##### 添加初始元素 + +1.遍历数组中的每个元素,如果队列为空,直接添加元素(考虑到有时候窗口大小是1和数组长度也是1,所以此时不能调用continue结束本次循环) + +##### 移除队尾比当前值小的元素 + +2.将当前元素num[i]从队列尾部进行插入,将那些比num[i]小的元素移除掉,因为num[i]的下标比它们大,值也比它们大,它们不可能成为最大值了 + +##### 添加当前值到队列尾部 + +3.比当前值小的数都从队尾移除完之后,再将当前值添加到队列尾部 + +##### 移除队首过期元素,添加当前窗口最大值到数组 + +4.计算滑动窗口左下标windowLeft,对队列头部元素下标进行判断,如果小于窗口左下标,说明过期了,需要移除,判断windowLeft是否>=0,满足说明才到计算滑动窗口最大值的时机,才会添加当前队列最大值到数组。 + +```java +public ArrayList maxInWindows2(int[] num, int size) { + ArrayList arrayList = new ArrayList<>(); + if (num==null||num.length==0||num.length queue = new LinkedList(); + for (int i = 0; i < num.length; i++) { + int currentValue = num[i]; + if (queue.size()==0) {//1.添加第一个元素 + queue.add(i); + } + //2.当前元素比队尾元素大,移除队尾元素 + while (queue.size() > 0 &¤tValue >= num[queue.getLast()]) { + queue.removeLast(); + } + queue.add(i); + int windowLeft = i - size + 1;//滑动窗口最左边的下标 + //3.顶部移除过期队列头部元素 + if (windowLeft>queue.getFirst()) { + queue.removeFirst(); + } + if (windowLeft>=0) {//进入第一个滑动窗口 + arrayList.add(num[queue.getFirst()]); + } + } + return arrayList; + } +``` + +## 题064 矩阵中的路径 + +请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 `a b c e s f c s a d e e` 这样的 `3 X 4` 矩阵中包含一条字符串`"bcced"`的路径,但是矩阵中不包含`"abcb"`路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 + +判断一个字符串在字符矩阵中是否一条符号条件的路径 + +就是递归去判断就行了。 + +```java +public boolean hasPath(char[] matrix, int rows, int cols, char[] str) +{ + boolean[] flag = new boolean[matrix.length]; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (matrix[i*cols + j] == str[0]) { + if (judge(matrix,rows,cols,i, j, str, 0, flag)) { + return true; + } + } + } + } + return false; +} + +//flag 标识这个节点是否已经在路径中了 + + +boolean judge(char[] matrix, int rows, int cols, int i, int j, char[] str, int charIndex,boolean[] flag) { + + + int index = i*cols + j; + if (i<0 || j<0 || i>=rows || j>=cols || matrix[index] != str[charIndex] || flag[index] == true) { + return false; + } + if (charIndex==str.length-1) { return true;} + + flag[index]=true; + if (judge(matrix,rows,cols,i+1, j, str, charIndex+1, flag) + ||judge(matrix,rows,cols,i-1, j, str, charIndex+1, flag) + ||judge(matrix,rows,cols,i, j+1, str, charIndex+1, flag) + ||judge(matrix,rows,cols,i, j-1, str, charIndex+1, flag)) { + return true; + } + + flag[index]=false; + return false; +} +``` diff --git a/docs/Coding_BinaryTree.md b/docs/Coding_BinaryTree.md new file mode 100644 index 0000000..357f0c1 --- /dev/null +++ b/docs/Coding_BinaryTree.md @@ -0,0 +1,765 @@ + +(PS:扫描[首页里面的二维码](README.md)进群,分享我自己在看的技术资料给大家,希望和大家一起学习进步!) + +## 二叉树专题 +### 剑指Offer部分 +#### [题006重建二叉树](#题006) +#### [题016判断一个二叉树是否是另一个二叉树的子结构](#题016) +#### [题017二叉树的镜像](#题017) +#### [题021 从上往下打印二叉树](#题021) +#### [题022 判断是否是二叉搜索树的后序遍历](#题022) + +#### [题023 二叉树中和为某一值的路径](#题023) +#### [题025 二叉搜索树与双向链表](#题025) + +#### [题037 二叉树的深度](#题037) +#### [题038 判断是否是平衡二叉树](#题038) +#### [题056 二叉树的下一个节点](#题056) +#### [题057 对称的二叉树](#题057) +#### [题058 按之字形顺序打印二叉树](#题058) + +#### [题059 把二叉树打印成多行](#题059) +#### [题060序列化二叉树](#题060) +#### [题061 二叉搜索树的第K小的节点](#题061) + +## 题006重建二叉树 + +输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 + +![image-20201206164927849](../static/image-20201206164927849.png) + +前序遍历结果和中序遍历结果: + +![image-20200624204944577](../static/image-20200624204944577.png) + +前序遍历结果分布是二叉树根节点,左子树,右子树。 + +中序遍历结果分布是左子树,二叉树根节点,右子树。 + +所以根据前序遍历结果的第一个元素获取到根节点,然后根据根节点把中序遍历结果分为两半,得到左子树的中序遍历结果,然后根据左子树的长度可以去前序遍历结果中分离出左子树的前序遍历结果,右子树也是如此,所以可以递归得构造出整个二叉树。 + +```java + public static TreeNode reConstructBinaryTree(int[] pre, int[] in) { + return reConstructBinaryTree(pre, 0, pre.length-1, in, 0, in.length-1); + } + + public static TreeNode reConstructBinaryTree(int[] pre, int preStart, int preEnd, int[] in, int inStart, int inEnd) { + if (preStart > preEnd || inStart > inEnd) { + return null; + } + TreeNode treeNode = new TreeNode(pre[preStart]); + for (int i = inStart; i <= inEnd; i++) { + if (in[i] == pre[preStart]) { + int leftLength = i - inStart;//左子树长度 + treeNode.left = reConstructBinaryTree(pre, preStart + 1, preStart+leftLength, in, inStart, i-1); + treeNode.right = reConstructBinaryTree(pre, preStart +leftLength+1, preEnd, in, i+1, inEnd); + } + } + return treeNode; + } +``` + +## 题016 判断一个二叉树是否是另一个二叉树的子结构 + +输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) + +有一个root为空,就返回false,然后判断根节点是否相等, + +* 相等,那么对根节点进行递归,判断子树根节点是否为NULL,是返回true,判断父树根节点是否为NULL,是返回false,然后对左右节点进行判断 +* 不相等,直接对左子树递归调用判断,是false,继续对右子树进行判断。 + +```java +//判断当前root2是否是root1及其子树的子结构 +public boolean HasSubtree(TreeNode root1,TreeNode root2) { + boolean result = false; + if (root1 != null && root2 != null) { + if (root1.val == root2.val) { + result = judgeTheTree(root1, root2); + //如果在这里就直接return result就会跳过对后面左右子树的判断 + } + if (result == false) { + result = HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2); + } + } + return result; +} +//判断 +boolean judgeTheTree(TreeNode root1, TreeNode root2) { + if (root2 == null) {return true;} + if (root1 == null) {return false;} + if (root1.val == root2.val) { + return judgeTheTree(root1.left, root2.left) && judgeTheTree(root1.right, root2.right); + } + return false; +} +``` + + + +## 题017 二叉树的镜像 + +操作给定的二叉树,将其变换为源二叉树的镜像。 + +就是翻转二叉树,将二叉树的左右子树进行交换。 + +```java +public void Mirror(TreeNode root) { + if (root != null) { + TreeNode tempNode = root.left; + root.left = root.right; + root.right = tempNode; + Mirror(root.left); + Mirror(root.right); + } +} +``` + +## 题021 从上往下打印二叉树 + +从上往下打印出二叉树的每个节点,同层节点从左至右打印。 + +##### 宽度优先遍历 + +其实就是二叉树的宽度优先遍历,一般就是通过队列来实现,将根节点添加到队列中,然后对队列进行循环,每次从队列取出一个元素,添加到ArrayList中去,然后将左,右子节点添加到队列中去,然后继续循环,一直到队列中取不到元素。(Java中可以使用LinkedList的add(),remove()方法来实现队列。 + +```java +public ArrayList PrintFromTopToBottom(TreeNode root) { + ArrayList arrayList = new ArrayList(); + if (root==null) { + return arrayList; + } + //这里用LinkedList比较好,因为它的底层是基于链表实现的,在队列取出队头元素时,不后面的元素不需要前移。 + LinkedList queue = new LinkedList(); + queue.add(root); + while (queue.size()>0) { + TreeNode treeNode = queue.remove(0); + arrayList.add(treeNode.val); + if (treeNode.left!=null) queue.add(treeNode.left); + if (treeNode.right!=null) queue.add(treeNode.right); + } + return arrayList; +} +``` + +##### 深度优先遍历 + +一般是使用栈来实现,一开始将 + +1.根节点加入栈, + +2.将栈顶元素出栈,打印这个节点,然后将它的右子节点入栈,将其左节点入栈 + +3.重复2操作,一直到栈中元素为空。 + +也可以使用递归实现 + +```java +//使用递归实现深度遍历 +ArrayList list = new ArrayList(); +void deepTranverse(TreeNode node) { + if(node!=null) { + list.add(node); + deepTranverse(node.left); + deepTranverse(node.right); + } +} +//使用栈的实现深度遍历 +void deepTranverse(TreeNode node) { + Stack stack=new Stack(); + List list=new ArrayList(); + if(root==null) + return list; + //压入根节点 + stack.push(root); + //然后就循环取出和压入节点,直到栈为空,结束循环 + while (!stack.isEmpty()){ + TreeNode t=stack.pop(); + if(t.right!=null) + stack.push(t.right); + if(t.left!=null) + stack.push(t.left); + list.add(t.val); + } + return list; +} +``` + +## 题022 判断是否是二叉搜索树的后序遍历 + +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出 Yes ,否则输出 No 。假设输入的数组的任意两个数字都互不相同。 + +![image-20201205191448743](../static/image-20201205191448743.png) + +二叉搜索树的特点在于,左子树所有节点<根节点<右子树所有节点,后续遍历的遍历顺序是左子树,右子树,根节点,所以取出数组最后一个元素,也就是根节点,然后遍历序列,发现第一个比根节点大的数之后,这个是也就是临界点,后面的数肯定也需要比根节点大,否则不是后续遍历,遍历完成后。 + +* 没有临界点(也就是第一个右子树的节点),全部都是左子树,递归调用判断, +* 临界点等于第一个元素,全部都是右子树,递归调用判断, +* 其他情况,将序列分为左子树,右子树,递归调用判断, + +(遍历时如果只有一个元素,那么直接返回正确,肯定满足要求) + +```java +public static boolean VerifySquenceOfBST(int [] sequence) { + if (sequence == null || sequence.length ==0) { + return false; + } + return VerifySquenceOfBST(sequence,0,sequence.length-1); +} + +public static boolean VerifySquenceOfBST(int[] sequence, int start, int end) { + if (start==end) { + return true; + } + Integer rightChildIndex = null; + for (int i = start;i sequence[end]&&rightChildIndex==null) { + rightChildIndex = i; + } + if (rightChildIndex!=null&&sequence[i] < sequence[end]) {//右子树有更小的元素 + return false; + } + } + if(rightChildIndex==null) {//说明全部位于左子树 + return VerifySquenceOfBST(sequence,start,end-1); + } else if(rightChildIndex== start) {//说明全部位于右边子树 + return VerifySquenceOfBST(sequence,start,end-1); + } + return VerifySquenceOfBST(sequence,start,rightChildIndex-1) && VerifySquenceOfBST(sequence,rightChildIndex, end-1); +} +``` + +## 题023 二叉树中和为某一值的路径 + +输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的 list 中,数组长度大的数组靠前) + +就是递归调用每个节点的左右子树,然后将节点值相加,如果节点值和为某个预期值,并且该节点为叶子节点,那么这条路径就是要找的路径。 + +```java +public ArrayList> FindPath(TreeNode root, int target) { + ArrayList> arrayContainer = new ArrayList>(); + if (root == null) { + return arrayContainer; + } + judgeIfIsTarget(arrayContainer, new ArrayList(),root,target,0); + return arrayContainer; +} + +public void judgeIfIsTarget(ArrayList> arrayContainer, ArrayList currentArrayList, TreeNode root, int target,int sum) { + if (root == null) {//根节点为空,直接结束这条路径的遍历 + return ; + } + currentArrayList.add(root.val); + sum = sum+root.val; + if (sum == target && root.left == null && root.right == null) {//当前路径满足要求就将路径添加到数组中去 + ArrayList copyArrayList = new ArrayList<>(currentArrayList); + arrayContainer.add(copyArrayList); + } else { + //判断左边节点的路径是否满足需求 + judgeIfIsTarget(arrayContainer, new ArrayList<>(currentArrayList), root.left, target,sum); + //判断右边界的路径是否满足需求 + judgeIfIsTarget(arrayContainer, new ArrayList<>(currentArrayList), root.right, target,sum); + } +} +``` + +另外一种写法 + +```java +public ArrayList> FindPath(TreeNode root, int target) { + ArrayList> allLists = new ArrayList>(); + if (root==null) { + return allLists; + } + ArrayList currentArray = new ArrayList(); + judgeIfIsTarget(allLists,root,target,currentArray,0); + return allLists; + } +public void judgeIfIsTarget(ArrayList> allList,TreeNode root, int target,ArrayListcurrentArray, int currentValue) { + currentValue += root.val; + if (currentValue > target) {//大于目标值,这条路径不可能是了 + return; + } else if (currentValue == target && root.left == null && root.right == null) {//必须要是根节点 + currentArray.add(root.val); + ArrayList newArray = new ArrayList<>(currentArray); + allList.add(newArray); + } else {//小于target + currentArray.add(root.val); + ArrayList newArray = new ArrayList<>(currentArray); + if (root.left!=null) {//遍历左子树 + judgeIfIsTarget(allList,root.left,target,newArray,currentValue); + } + if (root.right!=null) {//遍历右子树 + judgeIfIsTarget(allList,root.right,target,newArray,currentValue); + } + } + } +``` + + + +## 题025 二叉搜索树与双向链表 + +输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 + +二叉搜索树的中序遍历的结果就是递增的序列,所以递归实现二叉搜索树的中序遍历 + +```java + TreeNode head = null;//主要记录双向链表头结点 + TreeNode lastNode = null;//主要记录中序遍历时,上一次遍历的节点 + public TreeNode Convert(TreeNode pRootOfTree) { + if (pRootOfTree == null) { + return null; + } + Convert(pRootOfTree.left); + if (head == null) {//这里相当于是把第一次执行Convert方法的元素设置为链表头结点,也就是中序遍历第一个位置的节点,也就最左边的叶子节点。 + head = pRootOfTree; + } + if (lastNode != null) {//中序遍历时,假设存在上一个遍历的节点,将上一个节点与这个节点进行关联 + lastNode.right = pRootOfTree; + pRootOfTree.left = lastNode; + } + //完成对当前节点的遍历,将当前设置为lastNode。 + lastNode = pRootOfTree; + Convert(pRootOfTree.right); + return head; + } +``` + + +## 题037 二叉树的深度 + +输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 + +递归来遍历就好了 + +```java + public int TreeDepth(TreeNode root) { + if (root==null) { + return 0; + } + int left = TreeDepth(root.left); + int right = TreeDepth(root.right); + return left > right ? left+1 : right+1; + } +``` + +## 题038 判断是否是平衡二叉树 + +平衡二叉树的特点就是**任意节点的左右子树高度差的绝对值都小于等于1**, + +也可以先根据上面计算二叉树深度的算法先计算左右高度差,然后再去减,判断当前节点是否满足平衡二叉树的要求,然后再去对左子节点,和右子节点做同样的操作,但是这样的问题在于会对节点多次重复遍历。如果是把顺序调换一下,先去分别计算左右子节点的最大高度,过程中,发现不符合平衡二叉树的要求时,直接返回-1,这样就直接结束了,否则返回最大高度。 + +```java +public boolean IsBalanced_Solution1(TreeNode root) { + if (root==null) { + return true; + } + return fetchDepthIfSatisfy(root) != -1;//如果正常返回深度,则是平衡的,否则根节点子树中存在不平衡 + } + +//如果满足条件就返回当前节点的深度,当子树中存在节点不符合平衡二叉树会返回-1 +Integer fetchDepthIfSatisfy(TreeNode root) { + if (root == null) { + return 0; + } + Integer leftDepth = fetchDepthIfSatisfy(root.left); + Integer rightDepth = fetchDepthIfSatisfy(root.right); + if (leftDepth==-1 || rightDepth==-1) {//代表子树中有节点不满足条件 + return -1; + } + if (leftDepth - rightDepth > 1 || leftDepth - rightDepth < -1) {//代表当前节点不满足条件 + return -1; + } + return leftDepth> rightDepth? leftDepth+1:rightDepth+1; + } + +``` + +## 题056 二叉树的下一个节点 + +给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 + +主要就是分情况讨论 + +中序遍历就是左子树,根节点,右子树 + +所以当前节点的中序遍历中的下一个节点是 + +1.有右子树 + +​ 1.1右子树有左节点,一直向下遍历,找到最左的叶子节点。 + +​ 1.2右子树没有左节点,就是右子树节点。 + +2.没有右子树 + +​ 2.1没有父节点,那么没有下一个节点。 + + 2.2有父节点 + +​ 2.3当前节点是父节点的左子树,直接返回父节点。 + +​ 2.4当前节点是父节点的右子树,一直向上遍历,直到找到一个父节点,他是祖先节点的左子树节点的,找到就返回祖先节点,找不到就返回空。 + +```java +public TreeLinkNode GetNext(TreeLinkNode pNode) +{ + //这个节点有右子树 + if (pNode.right != null) { + TreeLinkNode right = pNode.right; + if (right.left==null) {//右子树没有左节点 + return right; + } else { + TreeLinkNode leftNode = right.left; + while (leftNode.left!= null) {//右子树有左节点 + leftNode = leftNode.left; + } + return leftNode; + } + } else {//这个节点没有右子树,那么就去找父节点 + TreeLinkNode father = pNode.next; + if (father == null) {//父节点为空 + return null; + } else if(father.left == pNode) {//父节点不为空,该节点为父节点的左子树 + return father; + } else {//父节点不为空,该节点为父节点的右子树 + while (father.next!=null) { + TreeLinkNode grandFather = father.next; + if (grandFather.left == father) { + return grandFather; + } else { + father = grandFather; + continue; + } + } + } + } + return null; +} +``` + +## 题057 对称的二叉树 + +请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 + + + +前序遍历是根节点,左子树,右子树 + +假设有另外一种遍历是根节点,右子树,左子树,如果二叉树是对称,那么这两种遍历的结果是一样的,所以使用递归来进行两种遍历,然后在过程中判断两种遍历结果是否一样。 + +```java +boolean isSymmetrical(TreeNode pRoot) +{ + return isSymmetrical(pRoot,pRoot); +} + +boolean isSymmetrical(TreeNode leftRoot,TreeNode rightRoot) +{ + + if (leftRoot == null || rightRoot == null) { + return (leftRoot == null) & (rightRoot == null); + } + if (leftRoot.val!=rightRoot.val) { + return false; + } else { + return isSymmetrical(leftRoot.left, rightRoot.right) + & isSymmetrical(leftRoot.right, rightRoot.left); + } +} +``` + +##### 另外一种写法 + +```java +boolean isSymmetrical1(TreeNode pRoot) { + if (pRoot == null) { + return true; + } + return isSymmetrical1(pRoot.left,pRoot.right); +} +boolean isSymmetrical1(TreeNode leftNode,TreeNode rightNode) { + if (leftNode==null && rightNode==null) {//都为null + return true; + } + if (leftNode==null && rightNode!=null) {//其中一个为null + return false; + } + if (leftNode!=null && rightNode==null) {//其中一个为null + return false; + } + if (leftNode.val==rightNode.val) {//都不为null,判断左右节点是否对称 + return isSymmetrical1(leftNode.left,rightNode.right) && isSymmetrical1(leftNode.right,rightNode.left); + } + return false; +} +``` + + + +## 题058 按之字形顺序打印二叉树 + +就是使用两个栈,stack1存放奇数层的节点,stack2存放偶数层的节点,一开始将根节点加入奇数层的栈,开始遍历, + +当前处于奇数层时,每次对stack1出栈,将出栈的节点的值打印,然后依次将节点的左子节点,右子节点加入到stack2,一直到stack1的全部元素出栈。 + +当前出于偶数层时,每次对stack2出栈,将出栈的节点的值打印,然后依次将节点的右子节点,左子节点加入大屏stack2,一直到stack2的全部元素出栈。 + +```java +public ArrayList> Print(TreeNode pRoot) { + ArrayList> arrayLists = new ArrayList>(); + if (pRoot==null) return arrayLists; + Stack stack1 = new Stack();//存放奇数层的栈 + Stack stack2 = new Stack();//存放偶数层的栈 + int flag = 0;//代表当前遍历的是奇数层还是偶数层。区别在于添加子节点的顺序。 + stack1.add(pRoot); + while ((flag == 0 && stack1.size()>0) || (flag == 1 && stack2.size()>0)) { + if (flag==0) { + ArrayList array = new ArrayList(); + while (stack1.size()>0) { + TreeNode node = stack1.pop(); + array.add(node.val); + //flag0是奇数层,子节点从左往右添加到栈中, + if (node.left!=null) stack2.push(node.left); + if (node.right!=null) stack2.push(node.right); + } + arrayLists.add(array); + flag = 1; + } else { + ArrayList array = new ArrayList(); + while (stack2.size()>0) { + TreeNode node = stack2.pop(); + array.add(node.val); + //flag1是偶数层,子节点从右往左添加到栈中 + if (node.right!=null) stack1.push(node.right); + if (node.left!=null) stack1.push(node.left); + } + arrayLists.add(array); + flag = 0; + } + } + return arrayLists; +} +``` +这是另外一种写法。 +```java +public ArrayList> Print(TreeNode pRoot) { + ArrayList> list = new ArrayList>(); + Stack otherStack = new Stack(); + Stack currentStack = new Stack(); + if(pRoot == null) { + return list; + } + currentStack.push(pRoot); + int addChildFromRightFlag = 0;// + while(currentStack.size()>0 || otherStack.size()>0) { + ArrayList array = new ArrayList(); + while(currentStack.size()>0) { + TreeNode node = currentStack.pop(); + array.add(node.val); + if(addChildFromRightFlag == 0) {//根据层数的不同,决定从右边还是左边添加节点。 + if(node.left!=null) { + otherStack.add(node.left); + } + if(node.right!=null) { + otherStack.add(node.right); + } + } else { + if(node.right!=null) { + otherStack.add(node.right); + } + if(node.left!=null) { + otherStack.add(node.left); + } + } + } + list.add(array); + addChildFromRightFlag = addChildFromRightFlag == 0 ? 1:0; + currentStack = otherStack; + otherStack = new Stack(); + } + return list; + } +``` + + + +## 题059 把二叉树打印成多行 + +宽度优先遍历的话,就是使用一个队列来实现,这里需要每一层的节点在一行打印,其实就是宽度优先遍历时需要区分每一层。 + +##### 使用两个队列的解法 + +可以使用两个队列,队列queue1存放奇数层节点,队列queue2存放偶数层节点,一开始将根节点加到队列queue1,然后对queue1所有元素按顺序出列,每次将元素的左右子节点添加到偶数队列queue2中,知道queue1队列元素全部出列,然后对queue2队列重复queue1的操作,直到queue1,queue2的元素都为空。 + +##### 使用一个队列的解法(使用空节点分割) + +就是使用一个队列queue,一开始将根节点加入queue,并且加入一个null元素到队列中作为标志元素,用来分割每一层,标志这一层的节点都在标志元素的前面。然后对queue中元素出列,每个进行打印,直到出列的元素是null,表示这一层已经结束了,如果queue中还有元素,那么在后面加入null标志元素分割,并且进行换行,打印下一行,如果queue中没有元素就结束循环 + +```java +ArrayList> Print(TreeNode pRoot) { + ArrayList> arrayLists= new ArrayList>(); + if (pRoot == null) return arrayLists; + ArrayList queue = new ArrayList<>(); + queue.add(pRoot); + queue.add(null);//每一层结束时,添加标志节点 + ArrayList tempArrayList = new ArrayList<>(); + while (queue.size()>0) { + TreeNode treeNode = queue.remove(0); + if (treeNode==null) {//null是标志节点,说明这一层已经打印结束了 + arrayLists.add(tempArrayList); + tempArrayList = new ArrayList<>(); + if (queue.size() == 0) {break;}//如果队列里没有元素了,就结束循环 + else { queue.add(null); }//如果队列里还有元素就继续添加标志节点用于分割 + } else { + tempArrayList.add(treeNode.val); + if (treeNode.left!=null) queue.add(treeNode.left); + if (treeNode.right!=null) queue.add(treeNode.right); + } + } + return arrayLists; +} +``` + +递归的解法 + +就是递归遍历每一个节点,遍历时传入深度depth,将节点加入到ArrayList中特定深度对应的数组中去。(但需要注意的是,这种方式其实是深度遍历的先序遍历,所以在添加节点到数组中时的顺序不是某一层的节点添加完毕后,才添加下一层的,所以如果是需要当时打印的话就不行,这种方法其实是将某一层的节点添加到专门存这一层的数组中,后续全部遍历完毕后,打印每个数组) + +这种方法也可以用来进行二叉树深度遍历,遍历完之后将嵌套数组拆分成单层的数组。 + +```java +ArrayList> Print2(TreeNode pRoot) { + ArrayList> arrayLists = new ArrayList>(); + if (pRoot==null)return arrayLists; + find(pRoot,1,arrayLists); + return arrayLists; +} + +void find(TreeNode pRoot, int depth, ArrayList> arrayLists) { + if (arrayLists.size()< depth) {//还没有存放这一层节点的数组 + arrayLists.add(new ArrayList()); + } + ArrayList tempArrayList = arrayLists.get(depth-1); + tempArrayList.add(pRoot.val); + if (pRoot.left!=null) find(pRoot.left,depth+1,arrayLists); + if (pRoot.right!=null) find(pRoot.right,depth+1,arrayLists); +} +``` + +## 题060序列化二叉树 + +请实现两个函数,分别用来序列化和反序列化二叉树 + + + +序列化的过程就是二叉树宽度遍历的过程,null元素也会加入到序列化的字符串中去 + +反序列化就是调用String.split()方法 + +1.将字符串先转换为String[]数组的过程, + +2.然后也是去数组取出第一个字符串firstStr,创建一个根节点treeNode,将firstStr转换为Integer类型,赋值给treeNode,然后将treeNode添加到一个队列中去。 + +3.每次从队列取出一个元素node,依次从String[]数组中取两个值,转换为Intger类型,作为node的左,右子节点,如果左,右子节点的值不为null,那么就将左右子节点添加到队列中去。 + +4.重复步骤3,直到队列元素个数为空。 + +```java +String Serialize(TreeNode root) { + StringBuffer stringBuffer = new StringBuffer(); + if (root == null) {return stringBuffer.toString();} + ArrayList queue = new ArrayList(); + queue.add(root); + while (queue.size()>0) { + TreeNode node = queue.remove(0); + if (node == null) { + stringBuffer.append("#!"); + } else { + stringBuffer.append(node.val+"!"); + queue.add(node.left); + queue.add(node.right); + } + } + return stringBuffer.toString(); +} + +TreeNode Deserialize(String str) { + if (str == null || str.length() == 0) {return null;} + String[] array = str.split("!"); + + Integer rootValue = convert(array[0]); + if (rootValue == null) {return null;} + + TreeNode rootNode = new TreeNode(rootValue); + ArrayList queue = new ArrayList(); + queue.add(rootNode); + int currentIndex = 1; + while (queue.size()>0 && currentIndex左子树所有节点,根节点<右子树所有节点 + +由于前序遍历是先左子树,根节点,右子树的顺序,所以前序遍历的结果,就是二叉搜索树中元素按递增顺序排列的结果,所以按照前序遍历到第K个元素就是第K小的节点。 + +```java +Integer index = 0; +TreeNode kNode = null; + +TreeNode KthNode(TreeNode pRoot, int k) +{ + find(pRoot,k); + return kNode; +} + +void find(TreeNode node, Integer k) { + if (node == null) {return;} + find(node.left, k); + index++; + if (index == k) { + kNode = node; + } else { + find(node.right, k); + } +} +``` + + + diff --git a/docs/Coding_LinkedList.md b/docs/Coding_LinkedList.md new file mode 100644 index 0000000..18d95b3 --- /dev/null +++ b/docs/Coding_LinkedList.md @@ -0,0 +1,475 @@ + +(PS:扫描[首页里面的二维码](README.md)进群,分享我自己在看的技术资料给大家,希望和大家一起学习进步!) + +## 链表专题 +一开始是刷剑指offer,后面是刷LeetCode,发现就是题目都是相对比较零散,不是按照链表,二叉树,数组等等这些Tag来分的,所以不是特别方便自己复习,所以自己在复习时就把刷过的题按照Tag重新刷了一遍。 + +### 剑指Offer部分 +#### [题005从尾到头打印链表](#题005) +#### [题013链表的倒数第K个结点](#题013) +#### [题014反转链表](#题014) +#### [题015 合并链表](#题015) + +#### [题024 复杂链表的复制](#题024) +#### [题035 两个的链表的第一个公共节点](#题035) + +#### [题054 链表中环的入口节点](#题054) +#### [题055 删除链表中重复的节点](#题055) + +## 题005 从尾到头打印链表 + +输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。 + +#### 解题思路(递归遍历链表) + +首先通过开始的判断,来排除链表为空的情况,直接返回空数组,链表不为空,取下一个节点,判断下一个节点是否为空, + +- 不为空,那么递归调用printListFromTailToHead方法来获取后面的节点反序生成的ArrayList,然后添加当前的节点的值,然后返回arrayList。 +- 为空,那么说明当前节点是链表尾部节点,直接创建一个ArrayList,然后添加当前节点的值,然后返回arrayList。 + +```java +ArrayList printListFromTailToHead(ListNode listNode) { + if(listNode == null) { return new ArrayList(); } + ArrayList arrayList; + ListNode nextNode = listNode.next; + if (nextNode!=null) { + arrayList = printListFromTailToHead(nextNode); + arrayList.add(listNode.val); + } else { + arrayList = new ArrayList<>(); + arrayList.add(listNode.val); + } + return arrayList; +} +``` +或者是这样写,其实原理就是先递归遍历,然后再打印,这样链表打印的顺序就是逆序的了。下面这么写,通过把ArrayList设置为一个全局变量,更加便于理解。 +```java +ArrayList list = new ArrayList(); +public ArrayList printListFromTailToHead(ListNode listNode) { + if(listNode == null ){ + return list; + } + printListFromTailToHead(listNode.next); + list.add(listNode.val); + return list; +} +``` + + +## 题013链表的倒数第K个结点 + +输入一个链表,输出该链表中倒数第k个结点。 + +#### 解题思路(快慢指针) +一个指针A先向前走k-1步,然后一个指针B指向头结点,A,B同时往后面走,直到A成为最后一个节点。 + +```java +ListNode FindKthToTail(ListNode head, int k) { + if (head==null || k <= 0) {//空链表,或者k小于等于0 + return null; + } + ListNode secondNode = head; + + for (int i=0 ; i < k-1 ; i++) {//向前走k-1步 + if (secondNode.next==null) {//链表长度不足k个 + return null; + } + secondNode = secondNode.next; + } + + ListNode firstNode = head; + while (secondNode.next != null) {//一直遍历到secondNode成为最后一个节点 + secondNode = secondNode.next; + firstNode = firstNode.next; + } + return firstNode; +} +``` +还有一种方法更加简洁一些,就是让快指针quickNode走到为null,此时head就是倒数第K个节点。 +```java +ListNode findLastK(ListNode head,int k) { + if (head==null || k < 1) {//空链表,或者k小于等于0 + return null; + } + ListNode quickNode = head; + for (int i = 0; i < k; i++) { + if (quickNode == null) { + return null; + } else { + quickNode = quickNode.next; + } + } + while (quickNode!=null) { + quickNode = quickNode.next; + head=head.next; + } + return head; + } +``` +## 题014反转链表 + +输入一个链表,反转链表后,输出新链表的表头。 + +A = head + +B = head.next + +head = null;//特别注意需要将原本的头结点置为null,否则原来的头结点的next会引用原来的第二个节点,形成一个环。 + +上一个节点A,当前节点B,下一个节点C,让 + +C = B.next; + +B.next = A; + +A = B; + +B = C; + +一直到B为null,此时A为最后一个节点. + +```java +public static ListNode ReverseList(ListNode head) { + + if (head == null) return null;//链表为空 + if (head.next==null) return head;//链表只有一个节点 + ListNode lastNode = head; + ListNode currentNode = head.next; + head.next = null;//将原来的头结点指向null + + while (currentNode != null) {//一直到currentNode是最好一个节点 + ListNode saveNextNode = currentNode.next; + currentNode.next = lastNode; + lastNode = currentNode; + currentNode = saveNextNode; + } + return lastNode; +} +``` + +这种解法好理解一点,就是使用first,second,three保存三个连续的节点,依次后移动 + +```java +public ListNode findLastNode(ListNode node) { + if(node==null ||node.next ==null) { + return node; + } + //至少有两个节点 + ListNode first = node; + ListNode second = node.next; + ListNode three = second.next; + first.next = null; + while(second!=null) { + second.next = first; + first = second; + second = three; + if (three == null){break}; + else { + three = three.next; + } + } + return first; +} +``` +还有一种是递归的写法 +```java +ListNode reverseNode(ListNode head) { + ListNode lastNode = reverseNode2(head); + head.next = null;//把老的链表头结点的next指针设置为null,不然会形成环 + return lastNode; + } + //递归遍历到末尾 +ListNode reverseNode2(ListNode head) { + if (head.next==null) { + return head; + } + ListNode lastNode = reverseNode2(head.next); + head.next.next = head; + return lastNode; +} + +``` + +## 题015 合并链表 + +输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 + +遍历写法: + +```java + ListNode merge(ListNode head1, ListNode head2) { + //有一个链表为空,就将另一个链表返回 + if (head1==null) { return head2;} + if (head2==null) { return head2;} + //创建preNode作为新链表头结点前面的节点 + ListNode preHead = new ListNode(-1); + //currentNode作为起始节点 + ListNode currentNode = preHead; + while (head1!=null&&head2!=null) { + if (head1.vallength2) { + length1--; + node1=node1.next; + } else { + length2--; + node2=node2.next; + } + } + while (node1!=node2) { + node1 = node1.next; + node2 = node2.next; + } + if (node1==node2) { + return node1; + } + return null; + } +``` + +## 题054 链表中环的入口节点 + +给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。 + + +一种方法是遍历整个链表,将每个节点添加到HashSet中,判断是否在HashSet中出现过,第一个重复的节点就是环的入口节点。 + +另一种解决方法是,假设存在环,环的长度为x,第一个指针先走x步,然后第二个指针从链表头结点出发,两个指针一起走,当第而个指针刚好走到环入口时,第一个指针正好在环中走了一圈,也在环的入口,此时的节点就是环的的入口节点, + +怎么得到环的长度呢,就是一个指针每次走2步,一个指针每次走一步,他们相遇时的节点肯定就是在环中的某个节点,然后这个节点在环中遍历一圈,回到原点,就可以得到环的长度count。 + +两个指针从头出发,第一个指针先走count步,然后两个指针每次都只走一步,相遇的地方就是环的入口, + +``` +public ListNode EntryNodeOfLoop(ListNode pHead) +{ + if (pHead == null || pHead.next==null) { + return null; + } + //计算环的长度 + ListNode slowNode = pHead.next; + ListNode quickNode = slowNode.next; + ListNode nodeInLoop = null;//获取环上的某个节点 + while (quickNode!=null && slowNode!= null) { + if (quickNode == slowNode) { + nodeInLoop = quickNode; + break; + } + slowNode = slowNode.next; + quickNode = quickNode.next; + if (quickNode!= null) { + quickNode=quickNode.next; + } + } + if (nodeInLoop == null) {//说明没有环 + return null; + } + //根据环上的某个节点来计算环的长度count + ListNode tempNode = nodeInLoop; + int count = 1;//将当前计算计算在内 + while (tempNode.next!=nodeInLoop) { + tempNode = tempNode.next; + count++; + } + //从链表头结点出发,第一个指针先走count步,然后两个指针每次只走一步,相遇的地方就是环的入口, + // 然后第一个指针和第二个指针一起走,当第二个指针刚好走了x步到环入口时, + // 第一个指针正好走了x+count步,在环中走了一圈,也在环的入口, + quickNode = pHead; + for (int i = 0; i < count; i++) { + quickNode = quickNode.next; + } + + slowNode = pHead; + while (quickNode!=slowNode) { + quickNode = quickNode.next; + slowNode=slowNode.next; + } + return slowNode; +} +``` + +使用hashSet的解法 + +``` +public ListNode EntryNodeOfLoop1(ListNode pHead) +{ + if (pHead==null) { + return null; + } + HashSet set = new HashSet(); + ListNode node = pHead; + while (node !=null) { + if (set.contains(node)) { + return node; + } else { + set.add(node); + } + node= node.next; + } + return null; +} +``` + +## 题055 删除链表中重复的节点 + +在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5 + +解法: + +就先创建一个我们自己的节点ourHead, + +ourHead.next= head, + +pre = ourHead + +currentNode = pre.next + +然后currentNode开始向后遍历,每次拿当前节点与后一个节点值比较 + +相等,那么就遍历找到一个不相等的点,然后将pre节点指向这个不相等的节点,currentNode = pre.next + +不相等,那么就直接让pre和currentNode向后移动一步。 + +```java +public ListNode deleteDuplication(ListNode pHead) +{ + if (pHead == null || pHead.next == null) { + return pHead; + } + + ListNode ourHead = new ListNode(0); + ourHead.next = pHead; + int temp = pHead.val; + ListNode preNode = ourHead; + ListNode currentNode = ourHead.next; + + while (currentNode!=null) {//往后遍历 + if (currentNode.next!=null && currentNode.val == currentNode.next.val) {//如果当前节点与下一个节点相等,就找到一个与当前节点不相等的节点,然后把中间多出来的这些相等的节点都删除掉 + ListNode tempNode = currentNode.next; + //找到第一个不相等的节点 + while (tempNode!=null) { + if(tempNode.val == currentNode.val) { tempNode = tempNode.next; } + else { break; } + } + preNode.next = tempNode; + currentNode = preNode.next; + } else {//如果当前节点与下一个节点相等,就跳过,遍历下一个节点 + preNode = preNode.next; + currentNode = currentNode.next; + } + } + return ourHead.next; +} +``` \ No newline at end of file diff --git a/docs/Git.md b/docs/Git.md new file mode 100644 index 0000000..83ed882 --- /dev/null +++ b/docs/Git.md @@ -0,0 +1,126 @@ +##### git命令使用 + +`git branch dev` 创建一个名称为dev的分支 + +`git checkout dev`切换到dev分支上 + + +`git checkout -b dev ` 从当前分支上创建出一个新分支dev + + +`git merge dev` 假设当前为master分支,执行这条命令后会将dev分支合并到当前master分支,合并完成后,master分支和dev分支会变成同一个分支 + +`git:(dev) git rebase master` 假设当前为dev分支,git rebase master命令会将当dev的更改进行回退,回退到与main分支交汇的地方,将这些更改暂存到rebase目录,然后将master上面的提交拿过来,让dev分支更新到master分支那样,然后再把之前暂存的更改应用上,中间如果出现冲突,需要解决冲突,解决完冲突后,使用git add 将更新添加上,然后使用git rebase --continue继续合并。如果中间需要中断合并那么可以使用git rebase —abort。 + +在rebase完成后,我们dev分支已经是最新了,但是master上还是老的代码,我们需要使用git checkout master 切换到master分支上,然后使用git rebase dev将dev上面的更改移动到master上来,然后push到远程。 +![image-20210603172535748](../static/image-20210603172535748.png) + +`git checkout HEAD^` 将当前head指向上1次提交 + +`git checkout HEAD~3` 将当前head指向上3次提交 + +`git reset HEAD~1` 回滚到上一次提交 + +`git reset HEAD` 生成一个新的提交,用于将上一个提交的更改撤销掉,等价于`git reset HEAD~1` + +`git cherry-pick C3 C4 C7`将其他分支上的C3提交,C4提交,C7提交拿到这个分支上面来 + +`git rebase -i HEAD~3` 合并最近的两次提交为一次提交记录,执行这个命令后会进入vim界面,然后会出现3次提交的记录 + +```java +pick 115e825 queryCreditsMonth +pick 4cedfe6 queryCreditsMonth +pick b3dccfd nickname +``` + +有一下操作符: +pick:保留该commit(缩写:p) +reword:保留该commit,但我需要修改该commit的注释(缩写:r) +edit:保留该commit, 但我要停下来修改该提交(不仅仅修改注释)(缩写:e) +squash:将该commit和前一个commit合并(缩写:s) +fixup:将该commit和前一个commit合并,但我不要保留该提交的注释信息(缩写:f) +exec:执行shell命令(缩写:x) +drop:我要丢弃该commit(缩写:d) + +咱们如果要把三次提交合并成一个就改成这样 + +``` +pick 115e825 queryCreditsMonth +s 4cedfe6 queryCreditsMonth +s b3dccfd nickname +``` + +然后保存退出,然后会进入填写commit说明的地方,我们直接保存就好了,这样就完成了,会生成一个新的commit + +```java +commit b3dccfd2c2173fa0a6358de604b6541c8c6c644a (HEAD -> feature-dev) +Date: Fri May 7 16:29:22 2021 +0800 + + nickname + + nickname + + change credits +``` + + + +`git commit -amend`可以修改最后一次提交的commit说明 + + + + + + + +##### rebase出现了冲突怎么办? + +假设master上跟我们改了同一行,其实就会造成冲突 +例如下面这个就是我们需要解决的冲突,<<<<< HEAD 到====== 之间为其他分支上别人的更改,======到>>>>>>> change credits之间为我们自己在dev分支上的更改,我们需要解决冲突,然后使用git add对文件进行提交 +```java + @Override + public List creditsMonthsQuery(Integer months, long year) { +<<<<<<< HEAD + List creditsMonth = creditsMonthsMapper.creditsMonthsQuery(months, year); + for(CreditsMonthsUser creditsMonthsUser : creditsMonth){ + String nickname = WechatUtil.decodeNickName(creditsMonthsUser.getNickname()); + creditsMonthsUser.setNickname(nickname); + } + return creditsMonth; +======= + Integer a = 100; + return creditsMonthsMapper.creditsMonthsQuery(months, year); +>>>>>>> change credits + } + +``` + +##### 日常开发流程应该是怎么样的呢? +1. git fetch origin dev 拉取远程的dev分支到本地,本地也会创建出dev分支 + +2.git checkout -b feature-dev 我们自己从dev分支创建出一个feature-dev分支,用于我们自己开发 + +3.我们自己在feature-dev开发了一些功能后,进行提交时,可能其他人往dev分支上提交了更新,我们需要将更新拿到本地 + +4. + +git checkout dev 切到dev分支 +git pull origin dev 将其他人在远程dev上的提交拉到本地 + +5. + +git checkout feature-dev + +git rebase dev 将dev上的更改应用到我们的feature-dev分支 + +然后可能会出现冲突,我们对冲突进行合并, + +然后对修改后的文件使用git add +文件名 进行添加, + +添加完成后使用git rebase --continue就可以继续,然后合并完成(如果需要中断rebase操作可以使用git rebase --abort) + +6.git checkout feature-dev + +git rebase feature-dev 就可以把feature-dev上合并好的更改拿到dev分支上 + +7. git push origin dev 就可以将dev分支上的更改提交到远程 \ No newline at end of file diff --git a/docs/HTTP.md b/docs/HTTP.md index a435f04..fbd2a92 100644 --- a/docs/HTTP.md +++ b/docs/HTTP.md @@ -5,14 +5,13 @@ #### [3. TCP三次握手和四次挥手是怎么样的?](#TCP三次握手和四次挥手是怎么样的?) #### [4.TCP怎么保证可靠性的?](#TCP怎么保证可靠性的?) #### [5.TCP拥塞控制怎么实现?](#TCP拥塞控制怎么实现?) -#### [6.close_wait 太多怎么处理?为什么会出现这种情况?](#close_wait 太多怎么处理?为什么会出现这种情况?) - +#### [6.close_wait太多怎么处理?](#close_wait太多怎么处理?) ### HTTPS建立连接的过程是怎么样的? ![img](../static/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3p1b3lpZ2VoYWl6ZWk=,size_16,color_FFFFFF,t_70.png) -在发起连接之前,服务器会将自己的公钥发给CA证书机构,CA证书机构会用自己的私钥对服务器的公钥加密,生成CA证书给服务器,服务器存储后供之后建立安全连接使用。 +在发起连接之前,服务器会向证书机构申请SSL证书,流程是服务器将自己的公钥发给CA证书机构,CA证书机构会用自己的私钥对服务器的公钥加密,生成SSL证书给服务器,服务器将SSL证书存储后供之后建立安全连接使用。 1.客户端发起请求,TCP三次握手,跟服务器建立连接。 如上图所示,在第 ② 步时服务器发送了一个SSL证书给客户端,SSL 证书中包含的具体内容有: @@ -46,8 +45,6 @@ 假设没有CA,那么如果服务器返回的包含公钥的包被hack截取,然后hack也生成一对公私钥,他将自己的公钥发给客户端。hack得到客户端数据后,解密,然后再通过服务器的公钥加密发给服务器,这样数据就被hack获取。 有了CA后,客户端根据内置的CA根证书,很容易识别出hack的公钥不合法,或者说hack的证书不合法。 - - ### HTTP的缓存策略是怎么样的? HTTP 缓存主要分为强缓存和对比缓存两种,从优先级上看,强缓存大于对比缓存。 @@ -155,7 +152,7 @@ TCP主要提供了**检验和**、**序列号/确认应答**、**超时重传** ### 流量控制是怎么实现的? -因为发送端在发送的数据包的序号必须小于最大的滑动窗口值,所以当发送的数据包过多,导致接收端的缓冲区写满时,接收端会通知给客户端将滑动窗口设置为更小的值,减少发送的量,达到一个流量控制的效果。 +因为滑动窗口设置得太大或太小都不易于数据传输,所以是根据接收端的反馈,发送端可以对滑动窗口大小进行动态调整的。发送端在发送的数据包的序号必须小于最大的滑动窗口值,所以当发送的数据包过多,导致接收端的缓冲区写满时,接收端会通知给客户端将滑动窗口设置为更小的值,减少发送的量,达到一个流量控制的效果。 ### TCP拥塞控制怎么实现? @@ -177,6 +174,10 @@ TCP连接刚建立,一点一点地提速,试探一下网络的承受能力 一旦认定网络拥塞,就会将慢启动阀值ssthresh设置为发生拥塞时的窗口cwnd大小的一半。 +PS:快重传是什么? + +假设发送方发送了1,2,3,4个数据包,假设接收方收到1,2数据包,并且发送了ACK确认,没有收到3,但是收到了4。根据可靠性传输原理,由于没有收到3,即便接受到了4,它也是一个失序报文,接收方不能对它进行确认。只能等发送方在等待3的ACK的回应时间超过2MSL后,进行重发。但是在这里为了让发送方快速知道哪些数据报文丢失了,接收方在收到3时就会给他返回2的ACK,一旦收到3个重复的ACK回应,也就是2的ACK,发送方就会意识到数据包3丢了,就会进行快速重传,重发报文3。 + **快速恢复算法** 发生网络拥塞后,慢启动阀值ssthresh设置为发生拥塞时的窗口cwnd大小的一半后, @@ -185,14 +186,24 @@ TCP连接刚建立,一点一点地提速,试探一下网络的承受能力 如果现在的TCP Reno算发,会将cwnd窗口设置为新的ssthresh值的大小,后续开始进入拥塞避免算法的流程,对cwnd窗口进入线性增长的状态。 -### close_wait 太多怎么处理?为什么会出现这种情况? - -close_wait 主要在TCP四次挥手时,被动关闭方给主动关闭方返回ACK应答后,由于自身还需要给主动方传输数据,所以会进入到close_wait状态,直到不需要给主动方发数据了,才会去给主动关闭方发送FIN包,同时进入LAST_ACK状态。 +### close_wait太多怎么处理? -一般如果我们使用JDBC去操作数据库,如果在完成操作后不关闭与数据库的连接,就会造成数据库连接不会关闭,进入close_wait状态。 +close_wait 主要在TCP四次挥手时,服务端给客户端返回ACK应答后,由于自身还需要给客户端传输数据,所以会进入到close_wait状态,直到不需要给客户端发数据了,才会去给客户端发送FIN包,同时进入LAST_ACK状态。(被动关闭的一方没有及时发出 FIN 包就会导致自己一直处于close_wait状态。) tcp_keepalive_time默认是2个小时,也就是TCP空闲连接可以存活2个小时,在close_wait状态下,可以把这个时间调小,减少处于close_wait连接的数量 +### time_wait太多是怎么造成的? +首先time_wait状态存在的意义是可以有效地终止TCP连接,因为主动关闭方发生ACK给被动关闭方后,需要等待2MSL的时间(MSL指的是报文最大有效存活时间,在linux下是60s),在这个时间内,如果没有收到被动关闭方重发的FIN包,就说明连接关闭完成了。 +在高并发短连接的业务场景下,由于短连接的传输数据+业务处理的时间很短,所以服务器处理完请求就会立即主动关闭连接,并且进入TIME_WAITING状态,而端口处于有个0~65535的范围中,除去系统占用的,总的数量有限。所以持续的到达一定量的高并发短连接,会使服务器因端口资源不足而拒绝为一部分请求服务。 +可以通过修改TCP的默认配置来改善这个问题。 +``` +net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; +net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; +net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 +net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间 +``` +https://www.cnblogs.com/dadonggg/p/8778318.html +https://segmentfault.com/a/1190000019292140 ### HTTP/2 有哪些新特性? #### 1.二进制传输 HTTP/2传输数据量的大幅减少,主要有两个原因:以二进制方式传输和Header 压缩。我们先来介绍二进制传输,HTTP/2 采用二进制格式传输数据,而非HTTP/1.x 里纯文本形式的报文 ,二进制协议解析起来更高效。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。 @@ -214,9 +225,13 @@ HTTP/2并没有使用传统的压缩算法,而是开发了专门的"HPACK” #### 3.多路复用 在 HTTP/2 中引入了多路复用的技术。多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也接更容易实现全速传输,毕竟新开一个 TCP 连接都需要慢慢提升传输速度。 +https://my.oschina.net/u/4331678/blog/3628959 + #### 4.Server Push + HTTP2还在一定程度上改变了传统的“请求-应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟,这被称为"服务器推送"( Server Push,也叫 Cache push) #### 5.提高安全性 + 出于兼容的考虑,HTTP/2延续了HTTP/1的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。 但由于HTTPS已经是大势所趋,而且主流的浏览器Chrome、Firefox等都公开宣布只支持加密的HTTP/2,所以“事实上”的HTTP/2是加密的。也就是说,互联网上通常所能见到的HTTP/2都是使用"https”协议名,跑在TLS上面。HTTP/2协议定义了两个字符串标识符:“h2"表示加密的HTTP/2,“h2c”表示明文的HTTP/2。 @@ -295,9 +310,11 @@ HTTP响应报文由状态行、响应头、空行和响应内容4个部分构成 200 OK: 表示客户端请求成功 +304 Not Modified:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。 + 400 Bad Request: 表示客户端请求有语法错误,不能被服务器端解析 -401 Unauthonzed: 表示请求未经授权,该状态码必须与WWW-Authenticate报文头一起使用 +401 Unauthorized: 表示请求未经授权,该状态码必须与WWW-Authenticate报文头一起使用 404 Not Found:请求的资源不存在,例如输入了错误的url @@ -333,3 +350,10 @@ Connection: 连接方式。 ![img](../static/640-4554190.jpeg) +##### HTTP状态码502,503,504各自代表着什么? + +502是指网关(一般是Nginx)从后端服务器接受到了无效的响应结果。通常是我们的后端服务器挂了之类的。 + +503是请求过载,就是请求超过了Nginx限流设置的阀值,就会返回503服务不可用。 + +504是指网关(一般是Nginx)从后端服务器接受的响应超时了。 \ No newline at end of file diff --git a/docs/HashMap.md b/docs/HashMap.md index fc724d4..95ae725 100644 --- a/docs/HashMap.md +++ b/docs/HashMap.md @@ -40,19 +40,19 @@ static final int hash(Object key) { 根据(n - 1) & hash计算得到插入的数组下标i,然后进行判断 -##### table[i]==null +##### 3.1.数组为空(table[i]==null) 那么说明当前数组下标下,没有hash冲突的元素,直接新建节点添加。 -##### table[i].hash == hash &&(table[i]== key || (key != null && key.equals(table[i].key))) +##### 3.2.等于下标首元素,table[i].hash == hash &&(table[i]== key || (key != null && key.equals(table[i].key))) 判断table[i]的首个元素是否和key一样,如果相同直接更新value。 -##### table[i] instanceof TreeNode +##### 3.3.数组下标存的是红黑树,table[i] instanceof TreeNode 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对。 -##### 其他情况 +##### 3.4.数组下标存的是链表 上面的判断条件都不满足,说明table[i]存储的是一个链表,那么遍历链表,判断是否存在已有元素的key与插入键值对的key相等,如果是,那么更新value,如果没有,那么在链表末尾插入一个新节点。插入之后判断链表长度是否大于8,大于8的话把链表转换为红黑树。 @@ -66,14 +66,16 @@ static final int hash(Object key) { final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; - // tab为空则创建 + // 1.tab为空则创建 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; - // 计算index,并对null做处理 - // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + // 2.计算index,并对null做处理 + // 3.插入元素 + //(n - 1) & hash 确定元素存放在哪个数组下标下 + //下标没有元素,新生成结点放入中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); - // 桶中已经存在元素 + // 下标中已经存在元素 else { Node e; K k; // 节点key存在,直接覆盖value @@ -150,27 +152,27 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 根据key计算hash值(计算结果跟HashMap是一致的,写法不同)。 #### 3.进入for循环,插入或更新元素 -* ##### 3.1 tab==null || tab.length==0, +* ##### 3.1 如果 tab==null || tab.length==0,说明数组未初始化 - 说明当前tab还没有初始化。 + 说明当前数组tab还没有初始化。 - 那么调用initTable()方法初始化tab。(在initTable方法中,为了控制只有一个线程对table进行初始化,当前线程会通过CAS操作对SIZECTL变量赋值为-1,如果赋值成功,线程才能初始化table,否则会调用Thread.yield()方法让出时间片)。 + 那么调用initTable()方法初始化tab。(在initTable方法中,为了控制只有一个线程对table进行初始化,当前线程会通过**CAS操作对SIZECTL变量赋值为-1**,如果赋值成功,线程才能初始化table,否则会调用Thread.yield()方法让出时间片)。 -* ##### 3.2 f ==null +* ##### 3.2 如果f ==null,说明当前下标没有哈希冲突的键值对 - (Node f根据hash值计算得到数组下标,下标存储的元素,f可能是null,链表头节点,红黑树根节点或迁移标志节点ForwardNode) + 说明当前数组下标还没有哈希冲突的键值对。 - 说明当前位置还没有哈希冲突的键值对。 + Node f是根据key的hash值计算得到数组下标,下标存储的元素,f可能是null,也有可能是链表头节点,红黑树根节点或迁移标志节点ForwardNode) - 那么根据key和value创建一个Node,使用CAS操作设置在当前数组下标下,并且break出for循环。 + 那么根据key和value创建一个Node,使用**CAS操作设置在当前数组下标下**,并且break出for循环。 -* ##### 3.3 f != null && f.hash = -1 +* ##### 3.3 如果f != null && f.hash = -1,说明存储的是标志节点,表示在扩容 说明ConcurrentHashMap正在在扩容,当前的节点f是一个标志节点,当前下标存储的hash冲突的元素已经迁移了。 那么当前线程会调用helpTransfer()方法来辅助扩容,扩容完成后会将tab指向新的table,然后继续执行for循环。 -* ##### 3.4 除上面三种以外情况 +* ##### 3.4 除上面三种以外情况,说明是下标存储链表或者是红黑树 说明f节点是一个链表的头结点或者是红黑树的根节点,那么对f加sychronize同步锁,然后进行以下判断: @@ -212,31 +214,32 @@ treeifyBin方法的源码: MIN_TREEIFY_CAPACITY是64 调用addCount()对当前数组长度加1,在addCount()方法中,会判断当前元素个数是否超过sizeCtl(扩容阈值,总长度*0.75),如果是,那么会进行扩容,如果正处于扩容过程中,当前线程会辅助扩容。 +**ConcurrentHashMap源码:** -ConcurrentHashMap源码: ```java final V putVal(K key, V value, boolean onlyIfAbsent) { + //ConcurrentHashMap不允许key和value为null,否则抛出异常 if (key == null || value == null) throw new NullPointerException(); - int hash = spread(key.hashCode()); + int hash = spread(key.hashCode());//计算hash值 int binCount = 0; - for (Node[] tab = table;;) { + for (Node[] tab = table;;) {//进入循环 Node f; int n, i, fh; - if (tab == null || (n = tab.length) == 0) - tab = initTable(); + if (tab == null || (n = tab.length) == 0)//数组如果为空 + tab = initTable();//初始化数组 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, - new Node(hash, key, value, null))) - break; // no lock when adding to empty bin + new Node(hash, key, value, null)))//如果发现此数组下标下没有哈希冲突的元素,就直接使用CAS操作将Node设置到此下标下 + break; } - else if ((fh = f.hash) == MOVED) - tab = helpTransfer(tab, f); - else { + else if ((fh = f.hash) == MOVED)//代表当前下标的头结点是标识节点,代表数组在扩容 + tab = helpTransfer(tab, f);//协助扩容 + else {//这种是普遍情况,存的是链表或者红黑树,进行插入 V oldVal = null; - synchronized (f) { + synchronized (f) {//加同步锁,避免多线程进行插入 if (tabAt(tab, i) == f) { - if (fh >= 0) { + if (fh >= 0) {//头结点hash值大于0,此数组下标下代表存的是一个链表 binCount = 1; - for (Node e = f;; ++binCount) { + for (Node e = f;; ++binCount) {//遍历链表 K ek; if (e.hash == hash && ((ek = e.key) == key || @@ -247,14 +250,14 @@ final V putVal(K key, V value, boolean onlyIfAbsent) { break; } Node pred = e; - if ((e = e.next) == null) { + if ((e = e.next) == null) {//键值对是新添加的,在链表尾部插入新节点 pred.next = new Node(hash, key, value, null); break; } } } - else if (f instanceof TreeBin) { + else if (f instanceof TreeBin) {//下标下存的是红黑树 Node p; binCount = 2; if ((p = ((TreeBin)f).putTreeVal(hash, key, @@ -267,7 +270,7 @@ final V putVal(K key, V value, boolean onlyIfAbsent) { } } if (binCount != 0) { - if (binCount >= TREEIFY_THRESHOLD) + if (binCount >= TREEIFY_THRESHOLD)//链表长度>=8,转换为红黑树 treeifyBin(tab, i); if (oldVal != null) return oldVal; @@ -287,23 +290,23 @@ final V putVal(K key, V value, boolean onlyIfAbsent) { #### 1.底层数据结构 ```java - transient Node[] table; //HashMap - - transient volatile Node[] table;//ConcurrentHashMap - - private transient Entry<?,?>[] table;//HashTable +transient Node[] table; //HashMap + +transient volatile Node[] table;//ConcurrentHashMap + +private transient Entry<?,?>[] table;//HashTable ``` #### HashMap=数组+链表+红黑树 HashMap的底层数据结构是一个数组+链表+红黑树,数组的每个元素存储是一个链表的头结点,链表中存储了一组哈希值冲突的键值对,通过链地址法来解决哈希冲突的。为了避免链表长度过长,影响查找元素的效率,当链表的长度>8时,会将链表转换为红黑树,链表的长度<6时,将红黑树转换为链表(但是红黑树转换为链表的时机不是在删除链表时,而是在扩容时,发现红黑树分解后的两个链表<6,就按链表处理,否则就建立两个小的红黑树,设置到扩容后的位置)。之所以临界点为8是因为红黑树的查找时间复杂度为logN,链表的平均时间查找复杂度为N/2,当N为8时,logN为3,是小于N/2的,正好可以通过转换为红黑树减少查找的时间复杂度。 -#### Hashtable=数组+链表 -Hashtable底层数据结构跟HashMap一致,底层数据结构是一个数组+链表,也是通过链地址法来解决冲突,只是链表过长时,不会转换为红黑树来减少查找时的时间复杂度。Hashtable属于历史遗留类,实际开发中很少使用。 - #### ConcurrentHashMap=数组+链表+红黑树 ConcurrentHashMap底层数据结构跟HashMap一致,底层数据结构是一个数组+链表+红黑树。只不过使用了volatile来进行修饰它的属性,来保证内存可见性(一个线程修改了这些属性后,会使得其他线程中对于该属性的缓存失效,以便下次读取时取最新的值)。 +#### Hashtable=数组+链表 +Hashtable底层数据结构跟HashMap一致,底层数据结构是一个数组+链表,也是通过链地址法来解决冲突,只是链表过长时,不会转换为红黑树来减少查找时的时间复杂度。Hashtable属于历史遗留类,实际开发中很少使用。 + #### 2.线程安全 ##### HashMap 非线程安全 @@ -342,9 +345,9 @@ static final boolean casTabAt(Node[] tab, int i, Node f = tabAt(tab, i = (n - 1) & hash)); synchronized (f) {//f就是数组下标存储的元素 if (tabAt(tab, i) == f) { - if (fh >= 0) { + if (fh >= 0) {//当前下标存的是链表 binCount = 1; - for (Node e = f;; ++binCount) { + for (Node e = f;; ++binCount) {//遍历链表 K ek; if (e.hash == hash && ((ek = e.key) == key || @@ -362,7 +365,7 @@ synchronized (f) {//f就是数组下标存储的元素 } } } - else if (f instanceof TreeBin) { + else if (f instanceof TreeBin) {//当前下标存的是红黑树 Node p; binCount = 2; if ((p = ((TreeBin)f).putTreeVal(hash, key, @@ -379,13 +382,13 @@ synchronized (f) {//f就是数组下标存储的元素 #### 3.执行效率 -因为HashMap是非线程安全的,执行效率会高一些,其次是ConcurrentHashMap,因为HashTable在进行修改和访问时是对整个HashTable加synchronized锁,所以效率最低。 +因为HashMap是非线程安全的,执行效率会高一些,其次是ConcurrentHashMap,因为HashTable在进行修改和访问时是对整个HashTable加synchronized锁,多线程访问时,同一时间点,只有一个线程可以访问或者添加键值对,所以效率最低。 #### 4.是否允许null值出现 -HashMap的key和null都可以为null,如果key为null,那么计算的hash值会是0,最终计算得到的数组下标也会是0,所以key为null的键值对会存储在数组中的首元素的链表中。value为null的键值对也能正常插入,跟普通键值对插入过程一致。 +HashMap的key和value都可以为null,如果key为null,那么计算的hash值会是0,最终计算得到的数组下标也会是0,所以key为null的键值对会存储在数组中的首元素的链表中。value为null的键值对也能正常插入,跟普通键值对插入过程一致。 -``` +```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); @@ -402,7 +405,7 @@ public synchronized V put(K key, V value) { } Entry<?,?> tab[] = table; int hash = key.hashCode(); - ...其他代码 + //...其他代码 } ``` @@ -420,14 +423,14 @@ final V putVal(K key, V value, boolean onlyIfAbsent) { ##### 不指定初始容量 如果不指定初始容量,HashMap和ConcurrentHashMap默认会是16,HashTable的容量默认会是11。 -##### 不指定初始容量 -如果制定了初始容量,HashMap和ConcurrentHashMap的容量会是比初始容量稍微大一些的2的幂次方大小,HashTable会使用初始容量, +##### 指定初始容量 +如果指定了初始容量,HashMap和ConcurrentHashMap的容量会是比初始容量稍微大一些的2的幂次方大小,HashTable会使用初始容量, ##### 扩容 -扩容时,HashMap和ConcurrentHashMap扩容时会是原来长度的两倍,HashTable则是2倍加上1. +扩容时,如果远长度是N,HashMap和ConcurrentHashMap扩容时会是2N,HashTable则是2N+1。 #### 6.hash值计算 -HashTable会扩容为2n+1,HashTable之所以容量取11,及扩容时是是2n+1,是为了保证 HashTable的长度是一个素数,因为数组的下标是用key的hashCode与数组的长度取模进行计算得到的,而当数组的长度是素数时,可以保证计算得到的数组下标分布得更加均匀,可以看看这篇文章http://zhaox.github.io/algorithm/2015/06/29/hash +HashTable会扩容为2N+1,HashTable之所以容量取11,及扩容时是是2N+1,是为了保证 HashTable的长度是一个素数,因为数组的下标是用key的hashCode与数组的长度取模进行计算得到的,而当数组的长度是素数时,可以保证计算得到的数组下标分布得更加均匀,可以看看这篇文章http://zhaox.github.io/algorithm/2015/06/29/hash ```java public synchronized V put(K key, V value) { @@ -440,7 +443,7 @@ public synchronized V put(K key, V value) { } ``` -HashMap和ConcurrentHashMap的hash值都是通过将key的hashCode()高16位与低16位进行异或运算(这样可以保留高位的特征,避免一些key的hashCode高位不同,低位相同,造成hash冲突),得到hash值,然后将hash&(n-1)计算得到数组下标。(n为数组的长度,因为当n为2的整数次幂时,hash mod n的结果在数学上等于hash&(n-1),而且计算机进行&运算更快,所以这也是HashMap的长度总是设置为2的整数次幂的原因) +HashMap和ConcurrentHashMap的hash值都是通过将key的hashCode()高16位与低16位进行异或运算(这样可以保留高位的特征,避免一些key的hashCode高位不同,低位相同,造成hash冲突),得到hash值,然后将hash&(n-1)计算得到数组下标。(n为数组的长度,因为当n为2的整数次幂时,hash mod n的结果在数学上等于hash&(n-1),而计算机进行&运算更快,所以这也是HashMap的长度总是设置为2的整数次幂的原因) ```java //HashMap计算hash值的方法 @@ -452,11 +455,8 @@ static int hash(Object key) { static int spread(int h) {//h是对象的hashCode return (h ^ (h >>> 16)) & HASH_BITS;// HASH_BITS = 0x7fffffff; } - ``` - - ### HashMap扩容后是否需要rehash? 在JDK1.8以后,不需要rehash,因为键值对的Hash值主要是根据key的hashCode()的高16位与低16位进行异或计算后得到,根据hash%length,计算得到数组的下标index,因为length是2的整数次幂,当扩容后length变为原来的两倍时,hash%(2*length)的计算结果结果差别在于第length位的值是1还是0,如果是0,那么在新数组中的index与旧数组的一直,如果是1,在新数组中的index会是旧数组中的数组中的index+length。 @@ -492,7 +492,7 @@ table = newTab; 这一步就很有意思,也是HashMap是非线程安全的表现之一,因为此时newTab还是一个空数组,如果有其他线程访问HashMap,根据key去newTab中找键值对,会返回null。实际上可能key是有对应的键值对的,只不过键值对都保存在旧table中,还没有迁移过来。 -(与之相反,HashTable在解决扩容时其他线程访问的问题,是通过对大部分方法使用sychronized关键字修饰,也就是某个线程在执行扩容方法时,会对HashTable对象加锁,其他线程无法访问HashTable。ConcurrentHashMap在解决扩容时其他线程访问的问题,是通过设置ForwardingNode标识节点来解决的,扩容时,某个线程对数组中某个下标下所有Hash冲突的元素进行迁移时,那么会将数组下标的数组元素设置为一个标识节点ForwardingNode,之后其他线程在访问时,如果发现key的hash值映射的数组下标对应是一个标识节点ForwardingNode(ForwardingNode继承于普通Node,区别字啊呀这个节点的hash值会设置为-1,并且会多一个指向扩容过程中新tab的指针nextTable),那么会根据ForwardingNode中的nextTable变量,去新的tab中查找元素。(如果是添加新的键值对时发现是ForwardingNode,当前线程会进行辅助扩容或阻塞等待,扩容完成后去新数组中更新或插入元素) +(与之相反,HashTable在解决扩容时其他线程访问的问题,是通过对大部分方法使用sychronized关键字修饰,也就是某个线程在执行扩容方法时,会对HashTable对象加锁,其他线程无法访问HashTable。ConcurrentHashMap在解决扩容时其他线程访问的问题,是通过设置**ForwardingNode标识节点**来解决的,扩容时,某个线程对数组中某个下标下所有Hash冲突的元素进行迁移时,那么会将数组下标的数组元素设置为一个**标识节点ForwardingNode**,之后其他线程在访问时,如果发现key的hash值映射的数组下标对应是一个**标识节点ForwardingNode**(ForwardingNode继承于普通Node,区别字啊呀这个节点的hash值会设置为-1,并且会多一个指向扩容过程中新tab的指针nextTable),那么会根据ForwardingNode中的nextTable变量,去新的tab中查找元素。(如果是添加新的键值对时发现是ForwardingNode,当前线程会进行辅助扩容或阻塞等待,扩容完成后去新数组中更新或插入元素) #### 迁移元素 @@ -502,7 +502,7 @@ table = newTab; hash%length=hash&(length-1) ``` -而因为length是2的N次幂,length-1在二进制中其实是N-1个1。例如: +而因为length是2的N次幂,length-1在二进制中其实是N个1。例如: length为16,length用2进制表示是10000, @@ -583,8 +583,7 @@ static final class CounterCell { 因为HashMap是非线程安全的,默认单线程环境中使用,如果get(key)为null,可以通过containsKey(key) 方法来判断这个key的value为null,还是不存在这个key,而ConcurrentHashMap,HashTable是线程安全的, -在多线程操作时,因为get(key)和containsKey(key)两个操作和在一起不是一个原子性操作, -可能在执行中间,有其他线程修改了数据,所以无法区分value为null还是不存在key。 +在多线程操作时,因为get(key)和containsKey(key)两个操作和在一起不是一个原子性操作,可能在containsKey(key)时发现存在这个键值对,但是get(key)时,有其他线程删除了键值对,导致get(key)返回的Node是null,然后执行方法时抛出异常。所以无法区分value为null还是不存在key。 至于ConcurrentHashMap,HashTable的key不能为null,主要是设计者的设计意图。 ### HashSet和HashMap的区别? @@ -799,8 +798,9 @@ abstract class HashIterator { ### 谈一谈你对LinkedHashMap的理解? -LinkedHashMap是HashMap的子类,与HashMap的实现基本一致,只是说所有的节点都有一个before和after指针,根据插入顺序形成一个双向链表。所以可以根据插入顺序进行遍历,默认accessOrder是false,也就是按照插入顺序来排序的,map.keySet().iterator().next()第一个元素是最早插入的元素的key。LinkedHashMap可以用来实现LRU算法。(accessOrder为true,会按照访问顺序来排序。) -LRU算法实现: +LinkedHashMap是HashMap的子类,与HashMap的实现基本一致,只是说在HashMap的基础上做了一些扩展,所有的节点都有一个before指针和after指针,根据插入顺序形成一个**双向链表**。默认accessOrder是false,也就是按照**插入顺序**来排序的,每次新插入的元素都是插入到链表的末尾。map.keySet().iterator().next()第一个元素是最早插入的元素的key。LinkedHashMap可以用来实现LRU算法。(accessOrder为true,会按照访问顺序来排序。) + +![img](../static/249993-20161215143120620-1544337380-20201130113344624.png)LRU算法实现: ```java //使用LinkedHashMap实现LRU算法(accessOrder为false的实现方式) @@ -827,13 +827,17 @@ public class LRUCache{ map.put(key, value); return; } - map.put(key, value); //超出capacity,删除最久没用的,利用迭代器,删除第一个 if (map.size() > capacity) { map.remove(map.keySet().iterator().next()); } + map.put(key, value); } } +``` +**下面是另外一种实现方法:** + +```java //使用LinkedHashMap实现LRU算法(accessOrder为true的实现方式) //如果是将accessOrder设置为true,get和put已有键值对时就不需要删除key了 public static class LRUCache2 { @@ -924,4 +928,82 @@ void afterNodeAccess(Node e) { // move node to last } } } -``` \ No newline at end of file +``` + +### JDK1.7的HashMap链表成环是怎么造成的? + +这是JDK1.7里面HashMap扩容相关的代码: + +```java +void transfer(Entry[] newTable, boolean rehash) { + int newCapacity = newTable.length; + for (Entry e : table) {//遍历原数组的每一个下标 + while(null != e) { + //将数组下标下所有元素一个一个迁移到新数组,使用头插法 + Entry next = e.next; + if (rehash) { + e.hash = null == e.key ? 0 : hash(e.key); + } + int i = indexFor(e.hash, newCapacity); + e.next = newTable[i]; + newTable[i] = e; + e = next; + } + } +} +``` + +JDK1.7版本的HashMap中,两个线程同时添加一个新的键值对,然后同时触发扩容时,两个线程都会进行扩容,就会造成前一个线程将某个数组下标的元素迁移过去后,另一个线程又进行迁移。假设原数组下标下有node1->node2->null,这样一个链表,线程A和线程B都在迁移,都拿到了node1,假设线程A先执行,将两个节点迁移到新的数组,假设node1和node2在新数组还是在同一下标下,那么迁移后的链表是node2->node1->null,此时如果线程B还在迁移,拿到node1又迁移,会让node1-next=node,从而让node1和node2形成环。 + +##### 那么JDK1.8版本的HashMap是怎么解决的呢? + +首先迁移时不是拿到一个键值对就迁移一个了,而是对一个数组下标下的链表进行遍历,根据hash值的不同,分成两个链表,然后将两个链表分别设置到新的数组的下标下。 + +```java +if (oldTab != null) { + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } +``` + + + +https://zhuanlan.zhihu.com/p/111501405 \ No newline at end of file diff --git a/docs/JavaBasic.md b/docs/JavaBasic.md index a7126b7..195c533 100644 --- a/docs/JavaBasic.md +++ b/docs/JavaBasic.md @@ -25,10 +25,14 @@ #### [12.为什么hashCode()和equal()方法要一起重写?](#为什么hashCode()和equal()方法要一起重写?) +#### [13.Java中有哪些数据类型?](#Java中有哪些数据类型?) + +#### [14.包装类型和基本类型的区别是什么?](#包装类型和基本类型的区别是什么?) + ### Java中的多态是什么? -**多态**指的是相同类型的变量在调用通一个方法时呈现出多种**不同的行为特征**。而造成这一现象的原因在于Java中的变量有两个类型: +**多态**指的是相同类型的变量在调用同一个方法时呈现出多种**不同的行为特征**。而造成这一现象的原因在于Java中的变量有两个类型: * 编译时类型,由声明变量时的类型决定。 @@ -169,7 +173,7 @@ final修饰方法时,有两个作用: * 修饰变量 - final修饰的变量一旦初始化,就不能被修改。当final修饰实例变量时,可以在在定义变量时赋初值,也可以在动态代码块,或构造器中赋初值。(只能赋值一次)。 + final修饰的变量一旦初始化,就不能被修改。当final修饰实例变量时,可以在在定义变量时赋初值,也可以在定义时不赋值,在动态代码块,或构造器中赋初值。(只能赋值一次)。 如果final变量在声明时就指定了初始值,并且在编译时可以确定下来,那么在编译时,final变量本质上会变成一个宏变量,所有用到该变量的地方都直接替换成该变量的值。 @@ -269,7 +273,7 @@ String是一个不可变类,任何对String改变都是会产生一个新的St 当频繁对字符串进行修改时,使用String会生成一些临时对象,多一些附加操作,执行效率降低。 -``` +```java stringA = StringA + "2"; //实际上等价于 { @@ -287,7 +291,7 @@ StringBuffer的读写方法都使用了synchronized修饰,同一时间只有 ### Object类有哪些自带方法? -#### registerNatives +#### registerNatives() 首先Object类有一个本地方法registerNatives(),会在类加载时调用,主要是将Object类中的一些本地方法绑定到指定的函数中去,便于之后调用。例如将hashCode和clone本地方法绑定到JVM_IHashCode和JVM_IHashCode函数。 @@ -304,7 +308,7 @@ StringBuffer的读写方法都使用了synchronized修饰,同一时间只有 [Object类中的registerNatives方法的作用深入介绍](https://blog.csdn.net/Saintyyu/article/details/90452826) -#### getClass +#### getClass() **getClass()**方法会返回对象的**运行时**类。 @@ -330,7 +334,7 @@ StringBuffer的读写方法都使用了synchronized修饰,同一时间只有 ```java class1 is class com.test.Son ``` -##### Classs.forName +##### Classs.forName() 与**getClass()**类似的方法还有两个,一个是Class类中的**forName()**方法,也是在**运行时**,根据传入的类名去加载类,然后返回与类关联的Class对象。也正是因为是动态加载,在编译时可以没有这个类,也不会对类名进行检验,所以有可能抛出ClassNotFoundException异常。 ```java @@ -341,7 +345,7 @@ public static Class forName(String className) } ``` 可以自己尝试运行一下这段代码 -``` +```java Class class2 = null; try { class2 = Class.forName("com.test.Son");//如果实际不存在com.test.Son这个类,那么会抛出ClassNotFoundException异常 @@ -351,12 +355,13 @@ public static Class forName(String className) } ``` -``` +```java class2 is class com.test.Son ``` ##### 类名+.class + 还有一种方式是使用**类名+.class**来获取类关联的Class对象,与上面两种方法不同之处在于,它返回的是编译时类型,也就是在编译时,类加载器将类加载到JVM中,然后初始化一个Class对象返回。 -``` +```java Class class3 = Son.class; System.out.println(class3); ``` @@ -367,19 +372,20 @@ class2 is class com.test.Son PS:无论是使用哪种方式来获取类关联的Class对象,类都是只会加载一次,如果获取Class对象时,不会重复加载类。 -### 为什么hashCode()和equal()方法要一起重写? -#### hashCode()和equal()方法 +### 为什么hashCode()和equals()方法要一起重写? +#### hashCode()和equals()方法 可以看到Obejct类中的源码如下,可以看到equals()方法的默认实现是判断两个对象的内存地址是否相同来决定返回结果。 -``` +```java public native int hashCode(); - public boolean equals(Object obj) { + + public boolean equals(Object obj) { return (this == obj); } ``` -网上很多博客说hashCode的默认实现是返回内存地址,其实不对,以OpenJDK为例,hashCode的默认计算方法有5种,有返回随机数的,有返回内存地址,具体采用哪一种计算方法取决于运行时库和JVM的具体实现。 +网上很多博客说hashCode的默认实现是返回内存地址,其实不对,以OpenJDK为例,hashCode的默认计算方法有5种,有返回随机数的,有返回内存地址,具体采用哪一种计算方法取决于运行时库和JVM的具体实现。(可以理解Object类hashCode默认实现是对所有对象返回的值都不一样。) 感兴趣的朋友可以看看这篇博客 @@ -393,17 +399,17 @@ PS:无论是使用哪种方式来获取类关联的Class对象,类都是只会 * 快速判断对象是否不相等 - 因为两个对象hashCode相等,调用equal()方法的结果不一定为true, + 因为两个对象hashCode相等,调用equals()方法的结果不一定为true, - 因为两个对象调用equal()方法相等,hashCode一定相等。 + 因为两个对象调用equals()方法相等,hashCode一定相等。 所以hashCode不相等可以作为两个对象不相等的快速判断条件。 - 在往HashMap中添加一个键值对时,计算得到数组下标后,会遍历数组下标下存储的链表中,拿key的hashCode与每个节点的hashCode进行比较,相等时,才调用equal()方法进行继续调用,节约时间。(在一些类的equal()方法的自定义实现中也会对hashCode进行判断)。 + 在往HashMap中添加一个键值对时,计算得到数组下标后,会遍历数组下标下存储的链表中,拿key的hashCode与每个节点的hashCode进行比较,相等时,才调用equals()方法进行继续调用,节约时间。(在一些类的equal()方法的自定义实现中也会对hashCode进行判断)。 -##### 假如只重写hashCode()方法(结果:HashMap无法保证去重) +##### 假如只重写hashCode()方法(结果:HashMap可以存在两个内存地址不相同,但是相等的对象,无法保证去重) -此时equal()方法的实现是默认实现,也就是当两个对象的内存地址相等时,equal()方法才返回true,假设两个键值对,它们的key类型都是TestObject,的值都是test,但是由于是使用new String()创建而成的字符串对象,key1和key2的内存地址不相等,所以key1==key2的结果会是false,TestObject的equals()方法默认实现是判断两个对象的内存地址,所以 key1.equals(key2)也会是false, 所以两个键值对可以重复地添加到hashMap中去。 +此时equals()方法的实现是默认实现,也就是当两个对象的内存地址相等时,equals()方法才返回true,假设两个键值对,它们的key类型都是TestObject,的值都是test,但是由于是使用new String()创建而成的字符串对象,key1和key2的内存地址不相等,所以key1==key2的结果会是false,TestObject的equals()方法默认实现是判断两个对象的内存地址,所以 key1.equals(key2)也会是false, 所以两个键值对可以重复地添加到hashMap中去。 ```java public class TestObject { @@ -443,7 +449,7 @@ HashMap是 com.test.TestObject@1=value2} ``` -##### 假如只重写equals()方法(结果:HashMap无法保证去重) +##### 假如只重写equals()方法(结果:相同的对象hashCode不同,从而映射到不同下标下,HashMap无法保证去重) 假设只equals()方法,hashCode方法会是默认实现,具体的计算方法取决于JVM,可能会导致两个相等的对象,它们的hashCode却不相同,从而计算得到的数组下标不相同,存储到hashMap中不同数组下标下的链表中,也会导致HashMap中存在重复元素。 @@ -489,15 +495,15 @@ HashMap是 #### clone()方法 -clone方法会创建并返回当前对象的副本。副本与原对象的区别在于它们相等,但是存储在不同的内存位置中。 +clone()方法会创建并返回当前对象的副本。副本与原对象的区别在于它们相等,但是存储在不同的内存位置中。 ```java protected native Object clone() throws CloneNotSupportedException; ``` -要调用clone方法必须实现Cloneable接口,否则调用默认的Object类的clone方法会抛出CloneNotSupportedException异常。默认clone()方法返回的对象是浅拷贝的。 +要调用clone()方法必须实现Cloneable接口,否则调用默认的Object类的clone方法会抛出CloneNotSupportedException异常。默认clone()方法返回的对象是浅拷贝的。 -#### toString方法 +#### toString()方法 返回类名+@+hashCode的16进制字符串 @@ -509,22 +515,22 @@ public String toString() { #### wait()方法和notify()方法 -``` +```java //timeout是超时时间,也就是等待的最大毫秒数,如果为0,代表会一直等待下去 public final native void wait(long timeout) throws InterruptedException; public final native void notify(); public final native void notifyAll(); ``` -##### wait +##### wait() wait()方法可以让当前线程放弃对象的监视器(可以简单认为监视器就是一个锁),进入等待队列,进行等待,直到其他线程调用notify()或者notifyAll()后(或者过了超时时间),线程才会从等待队列,移动到同步队列,再次获得对象的监视器后才能继续执行。 -##### notify +##### notify() notify()可以唤醒等待队列中的某一个线程,线程被唤醒后会从等待队列移动到同步队列,线程再次获得对象的监视器后才能继续执行。(然后调用notify()方法的线程会继续执行,在同步块中执行完毕后,会释放对象的监视器。) -##### notifyAll +##### notifyAll() notifyAll()方法与notify()方法类似,只是会将等待队列中的所有线程唤醒。 @@ -579,7 +585,7 @@ public 只允许在所有地方访问。 ##### 注意事项: -如果某个类Father一个方法A是没有使用访问修饰符,那么子类Son如果是在其他包中,不能调用这个方法A。但是如果方法A是使用protected修饰的,那么在子类中可以调用。(但是不能是使用父类去调用,就是不能在子类中去创建父类对象,然后用父类对象去调用。) +如果某个类Father一个方法A是没有使用访问修饰符,那么子类Son如果是在其他包中,不能调用这个方法A。但是如果方法A是使用protected修饰的,那么在子类中可以调用。(但是不能使用父类去调用,就是不能在子类中去创建父类对象,然后用父类对象去调用。) 具体可以看看下面这个例子: @@ -618,7 +624,7 @@ public class Test { } } ``` -1.如果没有自定义构造器,系统会提供一个默认的无参数构造器。如果提供了自定义的构造器,系统就不会提供默认的无参数构造器。(也就是不能直接调用new Test()来创建一个对象了,除非自己自己自定义一个无参数构造器)。 +1.如果没有自定义构造器,系统会提供一个默认的无参数构造器。如果提供了自定义的构造器,系统就不会提供默认的无参数构造器。(也就是不能直接调用new Test()来创建一个对象了,除非自己自定义一个无参数构造器)。 2.在上面的代码中,其实在构造器Test(String str)调用之前,系统已经分配好空间,创建一个对象,然后执行构造器Test(String str)方法对对象进行初始化,然后返回。 @@ -628,11 +634,11 @@ public class Test { ### Java中的内部类是怎么样的? -内部类分为静态内部类和非静态内部类。静态内部类是与外部类相关的,而非静态内部类外部类的实例对象相关的。 +内部类分为静态内部类和非静态内部类。静态内部类是与外部类相关的,而非静态内部类是与外部类的实例对象相关的。 ##### 静态内部类 -静态内部类一般使用public static修饰,也可以使用private static使用,那样只能在外部内内部使用。在外部类以外的地方使用静态类时,需要带上外部类的包名,例如创建一个静态内部类对象: +静态内部类一般使用public static修饰,也可以使用private static使用,那样只能在外部类内部使用。在外部类以外的地方使用静态类时,需要带上外部类的包名,例如创建一个静态内部类对象: ```java OutClass.InnerClass object = new OutClass.InnerClass(); @@ -640,7 +646,7 @@ OutClass.InnerClass object = new OutClass.InnerClass(); ##### 非静态内部类 -非静态内部类是跟外部类的实例对象对象绑定在一起的。外部类一般是由两种访问修饰符default(只能包内访问),public(所有位置可以访问)。而非静态内部类又private,default,protected,public四种访问修饰符。因为必须跟外部类的实例对象对象绑定在一起,所有非静态内部类不能有静态方法,静态成员变量,静态初始化块,在外面创建一个非静态内部类对象: +非静态内部类是跟外部类的实例对象绑定在一起的。外部类一般是由两种访问修饰符default(只能包内访问),public(所有位置可以访问)。而非静态内部类有private,default,protected,public四种访问修饰符。因为必须跟外部类的实例对象绑定在一起,所以非静态内部类不能有静态方法,静态成员变量,静态初始化块,在外面创建一个非静态内部类对象: ```java OutClass out = new OutClass(); @@ -649,7 +655,7 @@ OutClass.InnerClass object = out.new InnerClass(); ### Java中的注解是什么? Java中的注解其实是继承于annotation接口的一个接口,根据@Retention修饰的作用范围,注解可以是作用于源码层面,字节码文件层面,运行时层面。 -``` +```java // 如果@Retention的值是RetentionPolicy.Source 那么在编译时注解就失效了,不会编译进class文件 // 如果@Retention的值是RetentionPolicy.CLASS 那么会编译进class文件,但是JVM加载class文件时就会丢弃这个注解,在运行时注解就失效了 // 如果@Retention的值是RetentionPolicy.RUNTIME 在运行时注解同样有效 @@ -657,7 +663,7 @@ Java中的注解其实是继承于annotation接口的一个接口,根据@Reten 根据@Retention的值,注解主要分为编译时扫描和运行时扫描,例如@Override注解是作用于源码层面的,只是在编译时,编译器会去检验method是否是真正重写了父类的某个方法。 -``` +```java @Override public void method() { @@ -681,14 +687,42 @@ Throwable的子类为Error和Exception Exception的子类为RuntimeException异常和RuntimeException以外的异常(例如IOException)。 -主要分为Error,RuntimeException异常和RuntimeException以外的异常。 +主要分为Error,RuntimeException类及其子类的非受检异常和非受检异常以外的受检异常。 Error就是一些程序处理不了的错误,代表JVM出现了一些错误,应用程序无法处理。例如当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。 -RuntimeException异常就是应用程序运行时,可能会抛出的异常。这些异常是不检查异常,编译时Java编译器不会去检查,不会强制程序员添加处理异常的代码。程序中可以选择捕获处理,也可以不处理。如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等。 +RuntimeException异常就是应用程序运行时,可能会抛出的异常。这些异常是非受检异常,编译时Java编译器不会去检查,不会强制程序员添加处理异常的代码。程序中可以选择捕获处理,也可以不处理。如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等。 -RuntimeException以外的异常可以认为是编译时异常,从程序语法角度讲是必须进行处理的异常,编译时编译器就会要求有相关的异常捕获处理的代码逻辑。如IOException、SQLException。 +非受检异常以外的异常可以认为是受检异常,从程序语法角度讲是必须进行处理的异常,编译时编译器就会要求有相关的异常捕获处理的代码逻辑。如IOException、SQLException,FileNotFoundException。 ##### PS: -@Transaction默认检测异常为RuntimeException及其子类,如果有其他异常需要回滚事务的需要自己手动配置,例如:@Transactional(rollbackFor = Exception.class) \ No newline at end of file +@Transaction默认检测异常为RuntimeException及其子类,如果有其他异常需要回滚事务的需要自己手动配置,例如:@Transactional(rollbackFor = Exception.class) + +### Java中有哪些基本数据类型? + + +| 类型 | 字节数 | 取值范围 | +| ------- | -------------------------------------------------- | ------------------------------------------------------------ | +| byte | 1字节 | -128~127,也就是-2的7次方到2的7次方减1 | +| short | 2字节 | -32768~32767,就是-2的15次方到2的15次方减1 | +| int | 4字节 | -2147483648~2147483647,-2的31次方到2的31次方减1,换算成十进制应该是有10位,也就是十亿的数量级。 | +| long | 8字节 | -2的63次方到2的64次方,换算成十进制的数,是有19位。 | +| boolean | 理论上1字节就可以满足需求,为了内存对齐一般是4字节 | | +| float | 4字节 | | +| double | 8字节 | | +| char | 2字节 | C语言的char类型其实是1字节,使用anscil编码,取值范围是0~127,有一个2进制位作为数据校验位。Java使用的是unicode编码,16个二进制位。取值范围会大一些,所以用的2字节,可以存汉字。 | + +### 包装类型和基本类型的区别是什么? + +最主要的区别是包装类型是对象,拥有字段和方法,可以很方便地调用一些基本的方法,初始值是null,而且可以使用null代表空值,而基本数据类型只能使用0来代表初始值。其次是基本数据类型是直接存储在栈中,而包装类型是一个对象,对象的引用变量是存储在栈中,存储了对象在堆中的地址,对象的数据是存在堆中。 + +### JDK8 Stream与普通for循环的区别? + +对于简单操作,比如最简单的遍历,Stream串行api性能明显差于显示迭代,但并行的Stream API能够发挥多核特性。 + +元素为整型时,普通for循环耗时:串行api耗时:双核CPU并行Stream API=1:2:1 + +元素为字符串型时,普通for循环耗时:串行api耗时:双核CPU并行Stream API=1:1.5:0.72。 + +https://blog.csdn.net/zhenghongcs/article/details/104305798 \ No newline at end of file diff --git a/docs/JavaJVM.md b/docs/JavaJVM.md index 786d4a4..fbf2eec 100644 --- a/docs/JavaJVM.md +++ b/docs/JavaJVM.md @@ -9,12 +9,13 @@ #### [4.垃圾回收有哪些特点?](#垃圾回收有哪些特点?) #### [5.在垃圾回收机制中,对象在内存中的状态有哪几种?](#在垃圾回收机制中,对象在内存中的状态有哪几种?) #### [6.对象的强引用,软引用,弱引用和虚引用的区别是什么?](#对象的强引用,软引用,弱引用和虚引用的区别是什么?) -#### [7.垃圾回收算法有哪些?](#垃圾回收算法有哪些?) -#### [8.Minor GC和Full GC是什么?](#MinorGC和FullGC是什么?) -#### [9.如何确定一个对象可以回收?](#如何确定一个对象是否可以被回收?) -#### [10.目前通常使用的是什么垃圾收集器?](#目前通常使用的是什么垃圾收集器?) -#### [11.双亲委派机制是什么?](#双亲委派机制是什么?) -#### [12.怎么自定义一个类加载器?](#怎么自定义一个类加载器?) +#### [7.双亲委派机制是什么?](#双亲委派机制是什么?) +#### [8.怎么自定义一个类加载器?](#怎么自定义一个类加载器?) +#### [9.垃圾回收算法有哪些?](#垃圾回收算法有哪些?) +#### [10.Minor GC和Full GC是什么?](#MinorGC和FullGC是什么?) +#### [11.如何确定一个对象可以回收?](#如何确定一个对象是否可以被回收?) +#### [12.目前通常使用的是什么垃圾收集器?](#目前通常使用的是什么垃圾收集器?) + ### Java内存区域怎么划分的? 运行时数据区域包含以下五个区域:程序计数器,Java虚拟机栈,本地方法栈,堆,方法区(其中前三个区域各线程私有,相互独立,后面两个区域所有线程共享) @@ -41,16 +42,14 @@ 堆存储了几乎所有对象实例和数组,是被所有线程进行共享的区域。在逻辑上是连续的,在物理上可以是不连续的内存空间(在存储一些类似于数组的这种大对象时,基于简单和性能考虑会使用连续的内存空间)。 #### 方法区 -存储了被虚拟机加载的类型信息,常量,静态变量等数据,在JDK8以后,存储在元空间中(以前是存储在堆中的永久代中,JDK8以后已经没有永久代了)。 +存储了被虚拟机加载的**类型信息**,**常量**,**静态变量**等数据,在JDK8以后,存储在**方法区的元空间**中(以前是存储在堆中的永久代中,JDK8以后已经没有永久代了)。 -运行时常量池是方法区的一部分,会存储各种字面量和符号引用。具备动态性,运行时也可以添加新的常量入池(例如调用String的intern()方法时,如果常量池没有相应的字符串,会将它添加到常量池)。 +**运行时常量池**是方法区的一部分,会存储各种字面量和符号引用。具备动态性,运行时也可以添加新的常量入池(例如调用String的intern()方法时,如果常量池没有相应的字符串,会将它添加到常量池)。 ### 直接内存区(不属于虚拟机运行时数据区) 直接内存区不属于虚拟机运行时数据区的一部分。它指的是使用Native方法直接分配堆外内存,然后通过Java堆中的DirectByteBuffer来对内存的引用进行操作(可以避免Java堆与Native堆之间的数据复制,提升性能)。 - - ### Java中对象的创建过程是怎么样的? 这是网上看到的一张流程图: @@ -104,11 +103,11 @@ ##### 添加CAS锁 - 对内存分配的操作进行同步处理,添加CAS锁,配上失败重试的方式来保证原子性。(默认使用这种方式)。 +对内存分配的操作进行同步处理,添加CAS锁,配上失败重试的方式来保证原子性。(默认使用这种方式)。 ##### 预先给各线程分配TLAB - 预先在Java堆中给各个线程分配一块TLAB(本地线程缓冲区)内存,每个线程先在各自的缓冲区中分配内存,使用完了再通过第一种添加CAS锁的方式来分配内存。(是否启动取决于-XX:+/-UseTLAB参数)。 +预先在Java堆中给各个线程分配一块TLAB(本地线程缓冲区)内存,每个线程先在各自的缓冲区中分配内存,使用完了再通过第一种添加CAS锁的方式来分配内存。(是否启动取决于-XX:+/-UseTLAB参数)。 ### Java对象的内存布局是怎么样的? @@ -120,13 +119,13 @@ #### 对象头 -对象头主要包含对象自身的运行时数据(也就是图中Mark Word),类型指针(图中的Class Pointer,指向对象所属的类)。如果对象是数组,还需要包含数组长度(否则无法确定数组对象的大小)。 +对象头主要包含对象自身的**运行时数据**(也就是图中Mark Word),**类型指针**(图中的Class Pointer,指向对象所属的类)。如果对象是数组,还需要包含数组长度(否则无法确定数组对象的大小)。 **Mark Word**:存储对象自身的运行时数据,例如hashCode,GC分代年龄,锁状态标志,线程持有的锁等等。在32位系统占4字节,在64位系统中占8字节。 ![在这里插入图片描述](../static/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI5NDY4NTcz,size_16,color_FFFFFF,t_70.jpeg) - **Class Pointer**:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节。 + **Class Pointer**:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在开启了指针压缩时,占4字节。(默认是开启的) **Length**:如果是数组对象,还有一个保存数组长度的空间,占4个字节。 @@ -138,7 +137,9 @@ 因为HotSpot虚拟机的自动内存管理系统要求对象起始地址是8字节的整数倍,所以任何对象的大小必须是8字节的整数倍,而对象头部分一般是8字节的倍数,如果实力数据部分不是8字节的整数倍,需要对齐填充来补全。 +### 如何计算一个对象在内存中占多少个字节? +由于默认是开启了指针压缩的,现在普遍的机器位数都是64位,如果对象是普通对象,非数组类型,那么就是对象头部分就是12字节(类型指针4字节,Mark Word 8字节),假设这个对象只有一个Integer变量,那么在实例数据部分就是一个引用变量的空间4字节,总共是16字节,由于正好是8的倍数,就不需要进行对齐填充了。 ### 垃圾回收有哪些特点? @@ -153,15 +154,15 @@ ### 在垃圾回收机制中,对象在内存中的状态有哪几种? -1.可达状态 +1.**可达状态** 有一个及以上的变量引用着对象。 -2.可恢复状态 +2.**可恢复状态** 已经没有变量引用对象了,但是还没有被调用finalize()方法。系统在回收前会调用finalize()方法,如果在执行finalize()方法时,重新让一个变量引用了对象,那么对象会变成可达状态,否则会变成不可达状态。 -3.不可达状态 +3.**不可达状态** 执行finalize()方法后,对象还是没有被其他变量引用,那么对象就变成了不可达状态。 @@ -177,93 +178,300 @@ ##### 弱引用 -引用级别比软引用低,对于只有软引用的对象,不管内存是否足够, 都可能会被系统回收。 +引用级别比软引用低,对于只有软引用的对象,不管内存是否足够,在垃圾回收时, 都可能会被系统回收。 ##### 虚引用 虚引用主要用于跟踪对象被垃圾回收的状态,在垃圾回收时可以收到一个通知。 -### JVM 内存区域分布是什么样的?gc 发生在哪些部分? +### 双亲委派机制是什么? -Java虚拟机的内存区域主要分为虚拟机栈,本地方法栈,程序计数器,堆,方法区。垃圾回收主要是对堆中的对象进行回收,方法区里面也会进行一些垃圾回收,但是毕竟少,主要是针对**常量池的回收** 和 **对类型的卸载**,判定一个常量是否是“废弃常量”比较简单,不被引用就行了,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”: +![img](../static/4491294-8edc15f60a58bd0b.png) -1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例; +就是类加载器一共有三种: -2.加载该类的ClassLoader已经被回收; +**启动类加载器**:主要是在加载JAVA_HOME/lib目录下的特定名称jar包,例如rt.jar包,像java.lang就在这个jar包中。 -3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 +**扩展类加载器**:主要是加载JAVA_HOME/lib/ext目录下的具备通用性的类库。 -##### 虚拟机栈 +**应用程序类加载器**:加载用户类路径下所有的类库,也就是程序中默认的类加载器。 -是Java方法执行的内存模型,主要是用于方法执行的,每次执行方法时就会创建一个栈帧来压入栈中,每个栈帧存储了方法调用需要的数据,例如局部变量表,操作数栈等。 +工作流程: -局部变量表主要是存储方法的参数和创建的局部变量(基本类型或者引用类型),它的大小在编译时就固定了,方法有一个Code属性记录了需要的局部变量表的大小。 +除启动类加载器以外,所有类加载器都有自己的父类加载器,类加载器收到一个类加载请求时,首先会判断类是否已经加载过了,没有的话会调用父类加载器的的loadClass方法,将请求委派为父加载器,当父类加载器无法完成类加载请求时,子加载器才尝试去加载这个类。 +目的是为了保证每个类只加载一次,并且是由特定的类加载器进行加载(都是首先让启动类来进行加载)。 -操作数栈是一个栈,主要用来做算术运算及调用其他方法时存储传参,调用其他方法时,当前方法栈帧的操作数栈会跟其他方法的局部变量表有一定的重叠,主要便于共享这些传递参数,节约内存空间。(最大深度也是编译时就计算好的。) +```java +public abstract class ClassLoader { + ... + public Class<?> loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); + } + protected Class<?> loadClass(String name, boolean resolve) + throws ClassNotFoundException { + //使用synchronized加锁,保证不会重复加载 + synchronized (getClassLoadingLock(name)) { + //判断这个类是否已加载 + Class<?> c = findLoadedClass(name); + if (c == null) { + ... + try { + if (parent != null) {//有父类加载器,让父类加载器先尝试加载 + c = parent.loadClass(name, false); + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { + } + //使用当前类加载器进行类加载 + if (c == null) { + ... + c = findClass(name); + // do some stats + ... + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + protected Class<?> findClass(String name) throws ClassNotFoundException { + throw new ClassNotFoundException(name); + } + ... +} +``` -##### 本地方法栈 +### 怎么自定义一个类加载器? -调用native方法时的栈 +加载一个类时,一般是调用类加载器的loadClass()方法来加载一个类,loadClass()方法的工作流程如下: -##### 程序计数器 +1.先调用findLoadedClass(className)来获取这个类,判断类是否已加载。 -跟虚拟机栈,本地方法栈一样,程序计数器也是线程私有的,主要用来记录当前线程执行的字节码指令的行号,字节码解释器通过改变这个计数器来选取下一条需要执行的字节码指令。 +2.如果未加载,如果父类加载器不为空,调用父类加载器的loadClass()来加载这个类,父类加载器为空,就调用父类加载器加载这个类。 -##### 堆 +3.父类加载器加载失败,那么调用该类加载器findClass(className)方法来加载这个类。 -用来存储对象和数据的区域,被所有线程共享,在物理上可以是不连续的。 +所以我们一般自定义类加载器都是**继承ClassLoader,重写findClass()方法**,来实现类加载,这样不会违背双亲委派类加载机制,也可以通过重写loadClass()方法进行类加载,但是这样会违背双亲委派类加载机制。 -##### 方法区 +```java +public class DelegationClassLoader extends ClassLoader { + private String classpath; -方法区也是被线程共享的,主要存放类型信息,常量,静态变量,方法去中有一个运行时常量池。 + public DelegationClassLoader(String classpath, ClassLoader parent) { + super(parent); + this.classpath = classpath; + } -Java中的类在编译后会生成class文件,class文件除了包含变量,方法,接口信息外还包含一个常量池表,用于存放编译时生成的各种字面量和符号引用,在类加载后,字符流常量池会被存放在方法区中的运行时常量池中,运行期间也会可以将新的常量添加到运行时常量池,例如对String的实例调用intern方法。 + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + InputStream is = null; + try { + String classFilePath = this.classpath + name.replace(".", "/") + ".class"; + is = new FileInputStream(classFilePath); + byte[] buf = new byte[is.available()]; + is.read(buf); + return defineClass(name, buf, 0, buf.length); + } catch (IOException e) { + throw new ClassNotFoundException(name); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + throw new IOError(e); + } + } + } + } -### 对象的创建过程是怎么样的? + public static void main(String[] args) + throws ClassNotFoundException, IllegalAccessException, InstantiationException, + MalformedURLException { + sun.applet.Main main1 = new sun.applet.Main(); -#### 1.类加载检查 + DelegationClassLoader cl = new DelegationClassLoader("java-study/target/classes/", + getSystemClassLoader()); + String name = "sun.applet.Main"; + Class<?> clz = cl.loadClass(name); + Object main2 = clz.newInstance(); + + System.out.println("main1 class: " + main1.getClass()); + System.out.println("main2 class: " + main2.getClass()); + System.out.println("main1 classloader: " + main1.getClass().getClassLoader()); + System.out.println("main2 classloader: " + main2.getClass().getClassLoader()); + ClassLoader itrCl = cl; + while (itrCl != null) { + System.out.println(itrCl); + itrCl = itrCl.getParent(); + } + } +} +``` -首先在代码里面创建对象使用的new关键字在编译时会编译成new字节码指令,然后Java虚拟机在执行这条指令时,首先会根据类名去方法区中的运行时常量池查找类的符号引用,检查**符号引用**对应的类是否已经加载,如果没有加载,那么进行类加载。如果已经加载了,那么进行内存分配。 +### 类加载的过程是什么样的? -#### 2.内存分配 +#### 类加载器 -Java虚拟机会从堆中申请一块大小确定的内存(因为类加载时,创建一个此类的实例对象的所需的内存大小就确定了),并且初始化为零值,根据采用的垃圾收集器的不同,内存分配方式有两种: +类加载器是 Java 运行时环境(Java Runtime Environment)的一部分,负责动态加载 Java 类到 Java 虚拟机的内存空间中。**类通常是按需加载,即第一次使用该类时才加载。** 由于有了类加载器,Java 运行时系统不需要知道文件与文件系统。每个 Java 类必须由某个类加载器装入到内存。 -##### 指针碰撞 +![jvm_classloader_architecture](../static/jvm_classlaoder_architecture.svg) -一些垃圾回收算法,回收后的可用内存是规整,只需要移动分界点的指针就可以内存分配。 +类装载器除了要定位和导入二进制 class 文件外,还必须负责验证被导入类的正确性,为变量分配初始化内存,以及帮助解析符号引用。这些动作必须严格按一下顺序完成: -##### 空闲列表 +1. **装载**:查找并装载类型的二进制数据。 +2. **链接**:执行验证、准备以及解析(可选) - + -**验证**:确保被导入类型的正确性 + -**准备**:为类变量分配内存,并将其初始化为默认值。 + - **解析**:把类型中的符号引用转换为直接引用。 +3. **初始化**:把类变量初始化为正确的初始值。 -一些垃圾回收算法,回收后的可用内存是零散的,与已使用的内存是相互交错的,此时需要用一个列表来记录这些空闲的内存,分配内存时找一块足够的内存使用。(一般来说,使用标记-清除的垃圾算法是不规整的) +#### 装载 -#### 3.对象初始化(虚拟机层面) +##### 类加载器分类 + +在Java虚拟机中存在多个类装载器,Java应用程序可以使用两种类装载器: + +- **Bootstrap ClassLoader**:此装载器是 Java 虚拟机实现的一部分。由原生代码(如C语言)编写,不继承自 `java.lang.ClassLoader` 。负责加载核心 Java 库,启动类装载器通常使用某种默认的方式从本地磁盘中加载类,包括 Java API。 +- **Extention Classloader**:用来在`/jre/lib/ext` ,或 `java.ext.dirs` 中指明的目录中加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。 +- **Application Classloader**:根据 Java应用程序的类路径( `java.class.path` 或 `CLASSPATH` 环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 `ClassLoader.getSystemClassLoader()` 来获取它。 +- **自定义类加载器**:可以通过继承 `java.lang.ClassLoader` 类的方式实现自己的类加载器,以满足一些特殊的需求而不需要完全了解 Java 虚拟机的类加载的细节。 + +##### 全盘负责双亲委托机制 + +在一个 JVM 系统中,至少有 3 种类加载器,那么这些类加载器如何配合工作?在 JVM 种类加载器通过 **全盘负责双亲委托机制** 来协调类加载器。 + +- **全盘负责**:指当一个 `ClassLoader` 装载一个类的时,除非显式地使用另一个 `ClassLoader` ,该类所依赖及引用的类也由这个 `ClassLoader` 载入。 +- **双亲委托机制**:指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。 -Java虚拟机层面会对对象做一些初始化操作, 将对象的一些信息存储到Obeject header。 +全盘负责双亲委托机制只是 Java 推荐的机制,并不是强制的机制。实现自己的类加载器时,如果想保持双亲委派模型,就应该重写 `findClass(name)` 方法;如果想破坏双亲委派模型,可以重写 `loadClass(name)` 方法。 + +##### 装载入口 -##### 4.对象初始化(代码层面) +所有Java虚拟机实现必须在每个类或接口首次主动使用时初始化。以下六种情况符合主动使用的要求: -在Java程序层面,对对象进行初始化,在构造一个类的实例对象时,遵循的原则是先静后动,先变量,后代码块构造器,先父后子。在Java程序层面会依次进行以下操作: +- 当创建某个类的新实例时(new、反射、克隆、序列化) +- 调用某个类的静态方法 +- 使用某个类或接口的静态字段,或对该字段赋值(用final修饰的静态字段除外,它被初始化为一个编译时常量表达式) +- 当调用Java API的某些反射方法时。 +- 初始化某个类的子类时。 +- 当虚拟机启动时被标明为启动类的类。 + +除以上六种情况,所有其他使用Java类型的方式都是被动的,它们不会导致Java类型的初始化。 +当类是被动引用时,不会触发初始化: + +1.通过子类去调用父类的静态变量,不会触发子类的初始化,只会触发包含这个静态变量的类初始化,例如执行这样的代码`SubClass.fatherStaticValue`只会触发FatherClass的初始化,不会触发SubClass的初始化,因为fatherStaticValue是FatherClass的变量 -- 初始化父类的静态变量(如果是首次使用此类) +2.通过数组定义类引用类,SuperClass[] array = new SuperClass[10]; -- 初始化子类的静态变量(如果是首次使用此类) +不会触发SuperClass类的初始化,但是执行字节码指令newarray会触发另外一个类[Lorg.fenixsoft.classloading.SuperClass的初始化,这个类继承于Object类,是一个包装类,里面包含了访问数组的所有方法, -- 执行父类的静态代码块(如果是首次使用此类) +3.只引用类的常量不会触发初始化,因为常量在编译阶段进入常量池 -- 执行子类的静态代码块(如果是首次使用此类) +```java +class SuperClass { + public static final String str = "hello"; +} -- 初始化父类的实例变量 +//引用常量编译时会直接存入常量池 +System.out.println(SuperClass.str); -- 初始化子类的实例变量 +``` -- 执行父类的普通代码块 +对于接口来说,只有在某个接口声明的非常量字段被使用时,该接口才会初始化,而不会因为事先这个接口的子接口或类要初始化而被初始化。 -- 执行子类的普通代码块 +**父类需要在子类初始化之前被初始化**。当实现了接口的类被初始化的时候,不需要初始化父接口。然而,当实现了父接口的子类(或者是扩展了父接口的子接口)被装载时,父接口也要被装载。(只是被装载,没有初始化) -- 执行父类的构造器 +#### 验证 -- 执行子类的构造器 +确认装载后的类型符合Java语言的语义,并且不会危及虚拟机的完整性。 + +- **装载时验证**:检查二进制数据以确保数据全部是预期格式、确保除 Object 之外的每个类都有父类、确保该类的所有父类都已经被装载。 +- **正式验证阶段**:检查 final 类不能有子类、确保 final 方法不被覆盖、确保在类型和超类型之间没有不兼容的方法声明(比如拥有两个名字相同的方法,参数在数量、顺序、类型上都相同,但返回类型不同)。 +- **符号引用的验证**:当虚拟机搜寻一个被符号引用的元素(类型、字段或方法)时,必须首先确认该元素存在。如果虚拟机发现元素存在,则必须进一步检查引用类型有访问该元素的权限。 + +#### 准备 + +在准备阶段,Java虚拟机为类变量分配内存,**设置默认初始值**。但在到到初始化阶段之前,类变量都没有被初始化为真正的初始值。 + +| 类型 | 默认值 | +| --------- | -------- | +| int | 0 | +| long | 0L | +| short | (short)0 | +| char | ’\u0000’ | +| byte | (byte)0 | +| blooean | false | +| float | 0.0f | +| double | 0.0d | +| reference | null | + +#### 解析 + +https://www.zhihu.com/question/30300585 + +解析的过程就是在类型的常量池总寻找类、接口、字段和方法的符号引用,**把这些符号引用替换为直接引用的过程**。 + +- `类或接口的解析`:判断所要转化成的直接引用是数组类型,还是普通的对象类型的引用,从而进行不同的解析。 +- `字段解析`:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束, + +#### 初始化 + +**所有的类变量(即静态量)初始化语句和类型的静态初始化器都被Java编译器收集在一起,放到一个特殊的方法中,这个步骤就是初始化类静态变量和执行静态代码块。** 对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化方法。在类和接口的 class 文件中,这个方法被称为``。 + +1. 如果存在直接父类,且直接父类没有被初始化,先初始化直接父类。 +2. 如果类存在一个类初始化方法,执行此方法。 + +这个步骤是递归执行的,即第一个初始化的类一定是`Object`。 + +**Java虚拟机必须确保初始化过程被正确地同步。** 如果多个线程需要初始化一个类,仅仅允许一个线程来进行初始化,其他线程需等待。 + +> 这个特性可以用来写单例模式。 + +##### Clinit 方法 + +- 对于静态变量和静态初始化语句来说:执行的顺序和它们在类或接口中出现的顺序有关。 +- **并非所有的类都需要在它们的class文件中拥有()方法,** 如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有`()`方法。如果类声明了类变量,但没有明确的使用类变量初始化语句或者静态代码块来初始化它们,也不会有`()`方法。如果类仅包含静态`final`常量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有`()`方法。**只有那些需要执行Java代码来赋值的类才会有()** +- `final`常量:Java虚拟机在使用它们的任何类的常量池或字节码中直接存放的是它们表示的常量值。 + + +### JVM 内存区域分布是什么样的?gc 发生在哪些部分? + +Java虚拟机的内存区域主要分为**虚拟机栈**,**本地方法栈**,**程序计数器**,堆,**方法区**。垃圾回收主要是对**堆**中的对象进行回收,方法区里面也会进行一些垃圾回收,但是方法区中的内存回收出现的频率会比较低,主要是针对**常量池的回收** 和 **对类型的卸载**,判定一个常量是否是“废弃常量”比较简单,不被引用就行了,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”: + +1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例; + +2.加载该类的ClassLoader已经被回收; + +3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法; + +##### 虚拟机栈 + +是Java方法执行的内存模型,主要是用于方法执行的,每次执行方法时就会创建一个栈帧来压入栈中,每个栈帧存储了方法调用需要的数据,例如局部变量表,操作数栈等。 + +局部变量表主要是存储方法的参数和创建的局部变量(基本类型或者引用类型),它的大小在编译时就固定了,方法有一个Code属性记录了需要的局部变量表的大小。 + +操作数栈是一个栈,主要用来做算术运算及调用其他方法时存储传参,调用其他方法时,当前方法栈帧的操作数栈会跟其他方法的局部变量表有一定的重叠,主要便于共享这些传递参数,节约内存空间。(最大深度也是编译时就计算好的。) + +##### 本地方法栈 + +线程在调用native方法时使用的栈。 + +##### 程序计数器 + +跟虚拟机栈,本地方法栈一样,程序计数器也是线程私有的,主要用来记录当前线程执行的字节码指令的行号,字节码解释器通过改变这个计数器来选取下一条需要执行的字节码指令。 + +##### 堆 + +用来存储对象和数据的区域,被所有线程共享,在物理上可以是不连续的。 + +##### 方法区 + +方法区也是被线程共享的,主要存放类型信息,常量,静态变量,方法去中有一个运行时常量池。 + +Java中的类在编译后会生成class文件,class文件除了包含变量,方法,接口信息外还包含一个常量池表,用于存放编译时生成的各种字面量和符号引用,在类加载后,字符流常量池会被存放在方法区中的运行时常量池中,运行期间也会可以将新的常量添加到运行时常量池,例如对String的实例调用intern方法。 ### 垃圾回收算法有哪些? @@ -271,7 +479,7 @@ Java虚拟机层面会对对象做一些初始化操作, 将对象的一些信 ![1581500802565](../static/1581500802565.jpg) -#### 标记-清除算法 +#### 标记-清除算法(一般用于老年代) 就是对要回收的对象进行标记,标记完成后统一回收。(CMS垃圾收集器使用这种) @@ -285,7 +493,7 @@ Java虚拟机层面会对对象做一些初始化操作, 将对象的一些信 内存碎片化,会产生大量不连续的内存碎片。(内存碎片只能通过使用分区空闲分配链表来分配内存) -#### 标记-复制算法 +#### 标记-复制算法(一般用于新生代) 就是将内存分为两块,每次只用其中一块,垃圾回收时将存活对象,拷贝到另一块内存。(serial new,parallel new和parallel scanvage垃圾收集器) @@ -301,15 +509,15 @@ Java虚拟机层面会对对象做一些初始化操作, 将对象的一些信 解决方案是新生代的内存配比是Eden:From Survivor: To Survivor = 8比1比1 -每次使用时,Eden用来分配新的对象,From Survivor存放上次垃圾回收存活的对象,只使用Eden和From Survivor的空间,To Survivor是空的,垃圾回收时将存活对象拷贝到To Survivor,当空间不够时,从老年代进行分配担保。 +每次使用时,Eden用来分配新的对象,From Survivor存放上次垃圾回收后存活的对象,只使用Eden和From Survivor的空间,To Survivor是空的,垃圾回收时将存活对象拷贝到To Survivor,当空间不够时,从老年代进行分配担保。 -#### 标记-整理算法 +#### 标记-整理算法(一般用于老年代) -就是让存活对象往内存空间一端移动,然后直接清理掉边界以外的内存。(parallel Old和Serial old收集器就是采用该算法进行回收的) +标记-整理算法跟标记-清除算法适用的场景是一样的,都是用于老年代,也就是存活对象比较多的情况。标记-整理算法的流程就是让存活对象往内存空间一端移动,然后直接清理掉边界以外的内存。(parallel Old和Serial old收集器就是采用该算法进行回收的) **吞吐量高** -移动时内存操作会比较复杂,需要移动存活对象并且更新所有对象的引用,会是一种比较重的操作,但是如果不移动的话,会有内存碎片,内存分配时效率会变低,所以由于内存分配的频率会比垃圾回收的频率高很多,所以从吞吐量方面看,标记-整理法高于标记-清除法,所以强调高吞吐量的Parallel Scavenge收集器是采用标记整理法。 +移动时内存操作会比较复杂,需要移动存活对象并且更新所有对象的引用,会是一种比较重的操作,但是如果不移动的话,会有内存碎片,内存分配时效率会变低,所以由于内存分配的频率会比垃圾回收的频率高很多,所以从吞吐量方面看,标记-整理法高于标记-清除法。 **延迟高** @@ -325,9 +533,10 @@ Java虚拟机层面会对对象做一些初始化操作, 将对象的一些信 新生代存活率低,使用标记-复制算法。新生代发生的垃圾收集交Minor GC,发生频率较高 -老年代存活率高,使用标记-清除算法,或者标记-整理算法。(一般是多数时间采用标记-清除算法,内存碎片化程度较高时,使用标记-整理算法收集一次)。老年代内存满时会触发Major GC(Full GC),一般触发的频率比较低。 +老年代存活率高,使用标记-清除算法,或者标记-整理算法。(CMS垃圾收集器一般是多数时间采用标记-清除算法,内存碎片化程度较高时,使用标记-整理算法收集一次)。老年代内存满时会触发Major GC(Full GC),一般触发的频率比较低。 #### 如何确定一个对象是否可以被回收? + 有两种算法,一种是引用计数法,可以记录每个对象被引用的数量来确定,当被引用的数量为0时,代表可以回收。 一种是可达性分析法。就是判断对象的引用链是否可达来确定对象是否可以回收。就是把所有对象之间的引用关系看成是一个图,通过从一些GC Roots对象作为起点,根据这些对象的引用关系一直向下搜索,走过的路径就是引用链,当所有的GCRoots对象的引用链都到达不了这个对象,说明这个对象不可达,可以回收。 GCRoots对象一般是当前肯定不会被回收的对象,一般是虚拟机栈中局部变量表中的对象,方法区的类静态变量引用的对象,方法区常量引用的对象,本地方法栈中Native方法引用的对象。 @@ -358,19 +567,19 @@ To Survivor是用于下次Minor GC时存放存活的对象。 1) 对象优先在Eden分配 -当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。 +当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块空闲的Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。 2) 大对象直接进入老年代 所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组,为了避免大对象在Eden和两个Survivor区之间进行来回复制,所以当对象超过-XX:+PrintTenuringDistribution参数设置的大小时,直接从老年代分配 -3) 长期存活的对象将进入老年代。 +3) 长期存活的对象将进入老年代 当对象在新生代中经历过一定次数(XX:MaxTenuringThreshold参数设置的次数,默认为15)的Minor GC后,就会被晋升到老年代中。 -4) 动态对象年龄判定。 +4) 动态对象年龄判定 -为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 +为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中某个年龄所有对象大小的总和>Survivor空间的50%,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 ### MinorGC和FullGC是什么? @@ -379,13 +588,15 @@ Minor GC:对新生代进行回收,不会影响到年老代。因为新生代 Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满和System.gc()被显式调用等。 ### 触发Minor GC的条件有哪些? + 1.为新对象分配内存时,新生代的Eden区空间不足。 新生代回收日志: -``` +```java 2020-05-12T16:15:10.736+0800: 7.803: [GC (Allocation Failure) 2020-05-12T16:15:10.736+0800: 7.803: [ParNew: 838912K->22016K(943744K), 0.0982676 secs] 838912K->22016K(1992320K), 0.0983280 secs] [Times: user=0.19 sys=0.01, real=0.10 secs] ``` ### 触发Full GC的条件有哪些? + 主要分为三种: #### 1.system.gc() @@ -398,24 +609,24 @@ Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年 #### 3.老年代空间不足 而老年代空间不足又有很多种情况: -3.1 Promotion Failed 老年代存放不下晋升对象 +**3.1 Minor GC后,老年代存放不下晋升对象** 在进行 MinorGC 时, Survivor Space 放不下存活的对象,此时会让这些对象晋升,只能将它们放入老年代,而此时老年代也放不下时造成的。 还有一些情况也会导致新生代对象晋升,例如存活对象经历的垃圾回收次数超过一定次数(XX:MaxTenuringThreshold参数设置的次数,默认为15),那么会导致晋升, 或者在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 -3.2 Concurrent Mode Failure +**3.2 Concurrent Mode Failure** 在执行 CMS GC 的过程中,同时有对象要放入老年代,而此时老年代空间不足造成的。 -3.3 历次晋升的对象平均大小>老年代的剩余空间 +**3.3 历次晋升的对象平均大小>老年代的剩余空间** 这是一个较为复杂的触发情况, HotSpot为了避免由于新生代对象晋升到老年代导致老年代空间不足的现象, 在进行 Minor GC时,做了一个判断,如果之前统计所得到的 MinorGC 晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发 Full GC。 -3.4 老年代空间不足以为大对象分配内存 +**3.4 老年代空间不足以为大对象分配内存** 因为超过阀值(-XX:+PrintTenuringDistribution参数设置的大小时)的大对象,会直接分配到老年代,如果老年代空间不足,会触发Full GC。 ### 垃圾收集器有哪些? -一般老年代使用的就是标记-整理,或者标记-清除+标记-整理结合(例如CMS) +一般老年代使用的就是**标记-整理**,或者**标记-清除**+标记-整理结合(例如CMS) -新生代就是标记-复制算法 +新生代就是**标记-复制**算法 | 垃圾收集器 | 特点 | 算法 | 适用内存区域 | | ------------------------------------------- | ---------------------------------------------------- | ------------------- | -------------- | @@ -451,11 +662,11 @@ ParNew收集器是Serial收集器的多线程并行版本,在进行垃圾收 #### Parallel Scanvenge收集器(吞吐量优先收集器) -也支持多线程收集,它的目标是达到一个可控制的吞吐量,就是运行用户代码的时间/CPU消耗的总时间的比值。高吞吐量可以最高效率地利用处理器资源,尽快完成程序运算任务,适合不需要太多的交互分析任务。不支持并发收集,进行垃圾回收时会暂停用户线程,使用多个垃圾回收线程进行垃圾回收。 +也支持多线程收集,它的目标是达到一个可控制的吞吐量,就是运行用户代码的时间/(运行用户代码的时间+垃圾回收的时间)比值。高吞吐量可以最高效率地利用处理器资源,尽快完成程序运算任务,适合不需要太多的交互分析任务。不支持并发收集,进行垃圾回收时会暂停用户线程,使用多个垃圾回收线程进行垃圾回收。 #### Parallel Old收集器 -是Parallel Scanvenge老年代版本,支持多线程收集,使用标记整理法实现的, +是Parallel Scanvenge老年代版本,支持多线程收集,使用标记整理法实现的。 ![image-20200228191619466](../static/image-20200228191619466.png) @@ -467,44 +678,54 @@ CMS英文是Concurrent Mark Sweep,是基于标记-清除法+标记-整理算 ![image-20200228195544758](../static/image-20200228195544758.png) -#### 初始标记 +#### 1.初始标记 标记那些GC Roots可以直接关联到的对象。 -#### 并发标记(可以与用户线程并发执行) +#### 2.并发标记(可以与用户线程并发执行) 从GC Roots能关联到的对象直接向下遍历整个对象图,耗时较长,但是可以与用户线程并发执行。 整个标记过程是采用三色标记法来实现的: -白色:还没有遍历的对象。 +**白色**:代表还没有遍历的对象。 + +**灰色**:代表已遍历到的对象,但是它直接引用的对象还没有遍历完。 + +**黑色**:代表已遍历到的对象,它直接引用的对象也都已经遍历完。 + +##### 执行流程: -灰色:已遍历到的对象,但是它直接引用的对象还没有遍历完。 +1.先将所有对象放到白色集合,然后将GC Roots能关联的对象添加到灰色集合。 -黑色:已遍历到的对象,它直接引用的对象也都已经遍历完。 +2.对灰色集合中对象进行遍历,假设从灰色集合中取出的对象是A,将对象A引用的所有对象添加到灰色集合,全部添加完毕后,将A添加到黑色集合。 -##### 对于多标的情况,会生成浮动垃圾 +3.重复第2步,直到灰色集合为空。 + +##### 三色标记法的一些问题: + +##### 1.对于多标的情况,会生成浮动垃圾 ![img](../static/7779607-7a5ce353116237e2.png) 对于这种多标的情况,对象E/F/G是“应该”被回收的。然而因为E已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存。 这部分本应该回收 但是 没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。 另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。 -##### 对于引用变动后导致漏标的情况,使用原始快照法会保存变动前的情况 +##### 2.对于引用变动后导致漏标情况的处理 ![](../static/7779607-dab8f35ecb417433.png) ##### 增量更新法 -对于这种漏标的情况,CMS垃圾收集器使用的是增量更新法,就是将引用变化后的引用情况进行记录,然后之后进行标记。也就是当E->G变成了E->null,D->G,会对D->G进行记录,用于在重新标记阶段对这种情况进行处理。 +对于这种漏标的情况,CMS垃圾收集器使用的是增量更新法,就是将引用变化后的引用情况进行记录,然后之后进行标记。也就是当E->G变成了E->null,D->G,会对更改后的引用D->G进行记录,用于在重新标记阶段对这种情况进行处理。 ##### 原始快照法 就是对于这种E->G,然后改成D->G,正常来说,应该可能会漏掉,因为D已经是黑色对象了,就不会遍历G了,G1垃圾收集器对这种情况的处理是保存原始快照,就是在并发标记过程中,引用的变动,都会对变动前的引用情况进行记录,会按照变动前的引用情况进行标记,也就是即便E->G变成了E->null,D->G变化了,还是会记录E->G的引用情况,用于在重新标记阶段对这种情况进行处理。 -#### 重新标记 +#### 3.重新标记 由于标记时,用户线程在运行,并发标记阶段存在一些标记变动的情况,这一阶段就是修正这些记录。(CMS采用增量更新算法来解决,主要是将更改后的引用关系记录,之后增量更新) -#### 并发清除 +#### 4.并发清除 清除标记阶段判断的已经死亡的对象,由于不需要移动存活对象,这个阶段也可以与用户线程并发执行。 @@ -512,7 +733,7 @@ CMS英文是Concurrent Mark Sweep,是基于标记-清除法+标记-整理算 目标是在延迟可控(用户设定的延迟时间)的情况下获得尽可能高的吞吐量。 -JDK9以前,服务端模式默认的收集器是Parallel Scavenge+Parallel Old,JDK9之后,默认收集器是G1。G1不按照新生代,老年代进行划分,而是将Java堆划分为多个大小相等的独立Region,每一个Region可以根据需要,扮演新生代的Eden空间,Survivor空间,老年代Old空间和用于分配大对象的Humongous区。回收思路是G1持续跟踪各个Region的回收价值(回收可释放的空间和回收所需时间),然后维护一个优先级列表,在用户设定的最大收集停顿时间内,优先回收那些价值大的Region。 +JDK9以前,服务端模式默认的收集器是Parallel Scavenge+Serial Old。JDK9及之后,默认收集器是G1。G1不按照新生代,老年代进行划分,而是将Java堆划分为多个大小相等的独立Region,每一个Region可以根据需要,扮演新生代的Eden空间,Survivor空间,老年代Old空间和用于分配大对象的Humongous区。回收思路是G1持续跟踪各个Region的回收价值(回收可释放的空间和回收所需时间),然后维护一个优先级列表,在用户设定的最大收集停顿时间内,优先回收那些价值大的Region。 JDK 8 和9中,Region的大小是通过(最大堆大小+最小堆大小)的平均值/2048,一般是需要在1到32M之间。G1认为2048是比较理想的Region数量 @@ -541,13 +762,15 @@ Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。 ##### Region如何解决跨代指针? -因为老年代old区也会存在对新生代Eden区的引用,如果只是为了收集Eden区而对整个老年代进行扫描,那样开销太大了,所以G1其实会将每个Region分为很多个区,每个区有一个下标,当这个区有对象被其他Region引用时,那么CardTable对应下标下值为0,然后使用一个Rset来存储其他Region对当前Region的引用,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。对跨代引用的扫描,只需要扫描RSet就行了。 +因为老年代old区也会存在对新生代Eden区的引用,如果只是为了收集Eden区而对整个老年代进行扫描,那样开销太大了,所以G1其实会将每个Region分为很多个区,每个区有一个下标,当这个区有对象被其他Region引用时,那么CardTable对应下标下值为0,然后使用一个**Rset来存储老年代Region对当前这个新生代Region的引用**,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。对跨代引用的扫描,只需要扫描RSet就行了。 在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。 但在G1中,并没有使用point-out(就是记录当前Region对其他Region中对象的引用),这是由于一个Region太小,Region数量太多,如果是用point-out的话,如果需要计算一个Region的可回收的对象数量,需要把所有Region都是扫描一遍会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些Region引用了当前Region中的对象。这样只需要将当前Region中这些对象当做初始标记时的根对象来扫描就可以扫描出因为有跨代引用需要存活的对象,避免了无效的扫描。 -由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代的Region对新生代的这个Region之间的引用即可。 +##### 由于新生代有多个,那么我们需要在新生代之间记录引用吗? + +这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录**老年代的Region对新生代的这个Region之间的引用**即可。 需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。CardTable通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未引用。当一个地址空间有引用时,这个地址空间对应的数组索引的值被标记为"0",即标记为脏引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table集合(每个线程对应一个Hash Table,主要是为了减少多线程并发更新RSet的竞争),每个哈希表的Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。 @@ -555,17 +778,6 @@ Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。 ![img](../static/2579123-e0b8898d895aee05.png) -- 阶段1:根扫描 - 表态和本地对象被扫描 -- 阶段2:更新RS - 处理dirty card队列更新RS -- 阶段3:处理RS - 检测从新生代指向老年代的对象 -- 阶段4:对象拷贝 - 拷贝存活的对象到survivor/old区域 -- 阶段5:处理引用队列 - 软引用、弱引用、虚引用处理 - **G1 MixGC** MixGC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。Young GC回收是把新生代活着的对象都拷贝到Survivor的特定区域(Survivor to),剩下的Eden和Survivor from就可以全部回收清理了。那么,mixed GC就是把一部分老年区的region加到Eden和Survivor from的后面,合起来称为collection set, 就是将被回收的集合,下次mixed GC evacuation把他们所有都一并清理。选old region的顺序是垃圾多的(存活对象少)优先。 @@ -606,13 +818,25 @@ MixGC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫 https://www.jianshu.com/p/aef0f4765098 +### 三色标记法,引用计数, Mark-Sweep法(标记清除法)三者的区别? + +三色标记法可以与用户线程并发执行。 + +引用计数法主要没有办法解决循环引用的问题。 + +标记清除法主要流程就是一开始认为所有对象是0,然后从GC Roots对象开始向下遍历,**递归地**遍历所有对象,将能遍历到的对象标记为1。然后遍历结束后,标记为0的对象被认为是可以被销毁的。主要问题在于标记过程需要Stop the world,也就是需要停止用户线程,只有标记线程能运行。 + +扩展阅读: + +https://zhuanlan.zhihu.com/p/87790329 + ### 垃圾收集器相关的参数 --XX:+UseSerialGC, 虚拟机运行在Client 模式下的默认值,打开此开关后,使用Serial + Serial Old 的收集器组合进行内存回收 +-XX:+UseSerialGC,虚拟机运行在Client 模式下的默认值,打开此开关后,使用Serial + Serial Old 的收集器组合进行内存回收 --XX:+UseConcMarkSweepGC,打开此开关后,使用ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为CMS 收集器出现Concurrent Mode Failure失败后的后备收集器使用。(我们的线上服务用的都是这个) +-XX:+UseConcMarkSweepGC,打开此开关后,使用**ParNew + CMS + Serial Old** 的收集器组合进行内存回收。Serial Old 收集器将作为CMS 收集器出现Concurrent Mode Failure失败后的后备收集器使用。(我们的线上服务用的都是这个) --XX:+UseParallelGC,虚拟机运行在Server 模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收。 +-XX:+UseParallelGC,虚拟机运行在Server 模式下的默认值,打开此开关后,使用**Parallel Scavenge + Serial Old**(PS MarkSweep)的收集器组合进行内存回收。 -XX:+UseParallelOldGC,打开此开关后,使用Parallel Scavenge + Parallel Old 的收集器组合进行内存回收。 @@ -620,16 +844,20 @@ https://www.jianshu.com/p/aef0f4765098 -XX:+UseParNewGC,在JDK1.8被废弃,在JDK1.7还可以使用。打开此开关后,使用ParNew + Serial Old 的收集器组合进行内存回收 +参考链接: + +https://www.pianshen.com/article/7390335728/ + ### 目前通常使用的是什么垃圾收集器? ##### 怎么查询当前JVM使用的垃圾收集器? 使用这个命令可以查询当前使用的垃圾收集器 -java -XX:+PrintCommandLineFlags -version, +`java -XX:+PrintCommandLineFlags -version` 另外这个命令可以查询到更加详细的信息 -java -XX:+PrintFlagsFinal -version | grep GC +`java -XX:+PrintFlagsFinal -version | grep GC` 我们在IDEA中启动的一个Springboot的项目,默认使用的垃圾收集器参数是 -XX:+UseParallelGC @@ -642,408 +870,182 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode) ``` ##### Parallel Scavenge+Serial Old -JDK8默认情况下服务端模式下JVM垃圾回收参数是-XX:+UseParallelGC参数,也就是会使用Parallel Scavenge+Serial Old的收集器组合,进行内存回收。 +JDK8默认情况下服务端模式下JVM垃圾回收参数是-XX:+UseParallelGC参数,也就是会使用**Parallel Scavenge+Serial Old**的收集器组合,进行内存回收。 ##### ParNew+CMS -但是一般如果我们的后端应用不是那种需要进行大量计算的应用,基于低延迟的考虑,可以考虑使用-XX:+UseConcMarkSweepGC进行垃圾收集,这种配置下会使用ParNew来收集新生代内存,CMS垃圾回收器收集老年代内存。 - -##### G1 - -在JDK9时,默认的垃圾收集器是G1收集器,也可以使用-XX:+UseG1GC参数来启动G1垃圾收集器。 - - - -![img](../static/519126-20180623154635076-953076776.png) - - - -### 容器的内存和 jvm 的内存有什么关系?参数怎么配置? -在JDK8以后,JVM增加了容器感知功能,就是如果不显示指定-Xmx2048m 最大堆内存大小, -Xms2048m最小堆内存大小,会取容器所在的物理机的内存的25%作为最大堆内存大小, -也可以通过这几个参数来设置堆内存占容器内存的比例 --XX:MinRAMPercentage --XX:MaxRAMPercentage --XX:InitialRAMPercentage - -如何大体估算java进程使用的内存呢? - -Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss] - --Xss128k:设置每个线程的堆栈大小.JDK5.0以后每个线程的栈大小为1M。 - --Xms 堆内存的初始大小,默认为物理内存的1/64 --Xmx 堆内存的最大大小,默认为物理内存的1/4 --Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn - --Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。 - -### 怎么获取 dump 文件?怎么分析? - -1. 启动时配置,出现OOM问题时自动生成 -JVM启动时增加两个参数: - - -``` -//出现 OOME 时生成堆 dump: --XX:+HeapDumpOnOutOfMemoryError -//生成堆文件地址: --XX:HeapDumpPath=/home/liuke/jvmlogs/ -``` - -2.执行jmap命令立即生成 -发现程序异常前通过执行指令,直接生成当前JVM的dmp文件,6214是指JVM的进程号 - -``` -jmap -dump:format=b,file=/home/admin/logs/heap.hprof 6214 -``` - -获得heap.hprof以后,执行`jvisualvm`命令打开使用Java自带的工具Java VisualVM来打开heap.hprof文件,就可以分析你的Java线程里面对象占用堆内存的情况了 - -由于第一种方式是一种事后方式,需要等待当前JVM出现问题后才能生成dmp文件,实时性不高,第二种方式在执行时,JVM是暂停服务的,所以对**线上的运行会产生影响**。所以建议第一种方式。 - -### gc日志怎么看? - -这是一条Minor GC的回收日志 - -``` -2020-05-07T16:28:02.845+0800: 78210.469: [GC (Allocation Failure) 2020-05-07T16:28:02.845+0800: 78210.469: [ParNew: 68553K->466K(76672K), 0.0221963 secs] 131148K->63062K(2088640K), 0.0223082 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] -``` - -**GC (Allocation Failure)** - -代表Eden区分配内存失败触发Minor GC。 -**2020-05-07T16:28:02.845+0800: 78210.469** - -是发生的时间。 -**68553K->466K(76672K)** - -代表垃圾回收前新生代使用内存是68MB,剩余0.4MB,总内存是76MB。 -**0.0221963 secs** - -是垃圾回收耗时。 -**131148K->63062K(2088640K)** - -代表堆区回收前使用131MB,63MB,总内存是2088MB。 - -**[Times: user=0.02 sys=0.00, real=0.02 secs]** - -用户态耗时0.02s,内核态耗时0s,总耗时0.02s - -### cpu 使用率特别高,怎么排查?通用方法?定位代码?cpu高的原因? - -[CPU飙高,频繁GC,怎么排查?](https://mp.weixin.qq.com/s?src=11×tamp=1589109647&ver=2330&signature=LegOQuRkl7V9kFT6JT3Kg-9X4VYN40OmTyVRlSOFLppbFy*PTUWPAb2iyCFqz-ka9RRCje4fXGxS3sqw1z3JYQ3MRxzuI-UTLtlx-fV8VA8CLDWOFFfIuIVwrAeUdDHb&new=1) - -[jstack命令:教你如何排查多线程问题](https://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&mid=2247484624&idx=1&sn=a907100b51aca8bd651aebdb9aca532f&chksm=eb5381e6dc2408f092159c453a452c2781d374ca43f0984dde87688de8a1f8d9196dfd747124&scene=21#wechat_redirect) -``` - jstat -gcutil 29530 1000 10 - 垃圾回收信息统计,29530是pid,1000是每1秒打印一次最新信息,10是最多打印10次 -``` - - -### 怎么排查CPU占用率过高的问题? -1.首先使用`top`命令查看CPU占用率高的进程的pid。 -``` -top - 15:10:32 up 523 days, 3:47, 1 user, load average: 0.00, 0.01, 0.05 -Tasks: 95 total, 1 running, 94 sleeping, 0 stopped, 0 zombie -%Cpu(s): 1.7 us, 0.5 sy, 0.0 ni, 95.7 id, 2.2 wa, 0.0 hi, 0.0 si, 0.0 st -KiB Mem : 16267904 total, 6940648 free, 2025316 used, 7301940 buff/cache -KiB Swap: 16777212 total, 16776604 free, 608 used. 13312484 avail Mem - - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND -14103 hadoop 20 0 2832812 203724 18392 S 3.7 1.3 977:08.04 java -14010 hadoop 20 0 2897344 285392 18660 S 0.3 1.8 513:30.49 java -14284 hadoop 20 0 3052556 340436 18636 S 0.3 2.1 1584:47 java -14393 hadoop 20 0 2912460 504112 18632 S 0.3 3.1 506:43.68 java - 1 root 20 0 190676 3404 2084 S 0.0 0.0 4:31.47 systemd - 2 root 20 0 0 0 0 S 0.0 0.0 0:04.77 kthreadd - 3 root 20 0 0 0 0 S 0.0 0.0 0:10.16 ksoftirqd/0 -``` -2.使用`top -Hp 进程id`获得该进程下各个线程的CPU占用情况,找到占用率最高的线程的pid2, -使用`printf "%x\n" pid2`命令将pid2转换为16进制的数number。 -``` -top - 15:11:01 up 523 days, 3:48, 1 user, load average: 0.00, 0.01, 0.05 -Threads: 69 total, 0 running, 69 sleeping, 0 stopped, 0 zombie -%Cpu(s): 12.8 us, 0.1 sy, 0.0 ni, 87.0 id, 0.1 wa, 0.0 hi, 0.0 si, 0.0 st -KiB Mem : 16267904 total, 6941352 free, 2024612 used, 7301940 buff/cache -KiB Swap: 16777212 total, 16776604 free, 608 used. 13313188 avail Mem - - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND -14393 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:00.01 java -14411 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:01.95 java -14412 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:16.18 java -14413 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:12.79 java -14414 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 8:09.10 java -``` -3.使用`jstack pid`获得进程下各线程的堆栈信息,nid=0xnumber的线程即为占用率高的线程,查看它是在执行什么操作。(`jstack 5521 | grep -20 0x1596`可以获得堆栈信息中,会打印匹配到0x1596的上下20行的信息。) - -例如这个线程是在执行垃圾回收: -``` -"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f338c01f000 nid=0x1593 runnable -``` - -##### JVM相关的异常 - -#### 1.stackoverflow - -这种就是栈的空间不足,就会抛出这个异常,一般是递归执行一个方法时,执行方法深度太深时出现。Java执行一个方法时,会创建一个栈帧来存放局部变量表,操作数栈,如果分配栈帧时,栈空间不足,那么就会抛出这个异常。 - -(栈空间可以设置-Xss参数实现,默认为1M,如果参数) - -### 双亲委派机制是什么? - -![img](../static/4491294-8edc15f60a58bd0b.png) - -就是类加载器一共有三种: - -**启动类加载器**:主要是在加载JAVA_HOME/lib目录下的特定名称jar包,例如rt.jar包,像java.lang就在这个jar包中。 - -**扩展加载器**:主要是加载JAVA_HOME/lib/ext目录下的具备通用性的类库。 - -**应用程序加载器**:加载用户类路径下所有的类库,也就是程序中默认的类加载器。 - -工作流程: - -除启动类加载器以外,所有类加载器都有自己的父类加载器,类加载器收到一个类加载请求时,首先会判断类是否已经加载过了,没有的话会调用父类加载器的的loadClass方法,将请求委派为父加载器,当父类加载器无法完成类加载请求时,子加载器才尝试去加载这个类。 -目的是为了保证每个类只加载一次,并且是由特定的类加载器进行加载(都是首先让启动类来进行加载)。 - -```java -public abstract class ClassLoader { - ... - public Class<?> loadClass(String name) throws ClassNotFoundException { - return loadClass(name, false); - } - protected Class<?> loadClass(String name, boolean resolve) - throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - Class<?> c = findLoadedClass(name); - if (c == null) { - ... - try { - if (parent != null) { - c = parent.loadClass(name, false); - } else { - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { - } - - if (c == null) { - ... - c = findClass(name); - // do some stats - ... - } - } - if (resolve) { - resolveClass(c); - } - return c; - } - } - protected Class<?> findClass(String name) throws ClassNotFoundException { - throw new ClassNotFoundException(name); - } - ... -} -``` - -### 怎么自定义一个类加载器? - -加载一个类时,一般是调用类加载器的loadClass()方法来加载一个类,loadClass()方法的工作流程如下: - -1.先调用findLoadedClass(className)来获取这个类,判断类是否已加载。 - -2.如果未加载,如果父类加载器不为空,调用父类加载器的loadClass()来加载这个类,父类加载器为空,就调用父类加载器加载这个类。 +但是一般如果我们的后端应用不是那种需要进行大量计算的应用,基于低延迟的考虑,可以考虑使用-XX:+UseConcMarkSweepGC进行垃圾收集,这种配置下会使用ParNew来收集新生代内存,CMS垃圾回收器收集老年代内存。 -3.父类加载器加载失败,那么调用该类加载器findClass(className)方法来加载这个类。 +##### G1 -所以我们我们一般自定义类加载器都是继承ClassLoader,来重新findClass()方法,来实现类加载。 +在JDK9时,默认的垃圾收集器是G1收集器,也可以使用-XX:+UseG1GC参数来启动G1垃圾收集器。 -```java -public class DelegationClassLoader extends ClassLoader { - private String classpath; - public DelegationClassLoader(String classpath, ClassLoader parent) { - super(parent); - this.classpath = classpath; - } - @Override - protected Class<?> findClass(String name) throws ClassNotFoundException { - InputStream is = null; - try { - String classFilePath = this.classpath + name.replace(".", "/") + ".class"; - is = new FileInputStream(classFilePath); - byte[] buf = new byte[is.available()]; - is.read(buf); - return defineClass(name, buf, 0, buf.length); - } catch (IOException e) { - throw new ClassNotFoundException(name); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - throw new IOError(e); - } - } - } - } +![img](../static/519126-20180623154635076-953076776.png) - public static void main(String[] args) - throws ClassNotFoundException, IllegalAccessException, InstantiationException, - MalformedURLException { - sun.applet.Main main1 = new sun.applet.Main(); +### 容器的内存和 jvm 的内存有什么关系?参数怎么配置? - DelegationClassLoader cl = new DelegationClassLoader("java-study/target/classes/", - getSystemClassLoader()); - String name = "sun.applet.Main"; - Class<?> clz = cl.loadClass(name); - Object main2 = clz.newInstance(); +一般在使用容器部署Java应用时,一般为了充分利用物理机的资源,会在物理机上部署多个容器应用,然后对每个容器设置最大内存的限制,但是JVM的最大堆默认值一般取得的物理机最大内存的25%,一旦应用内存超出容器的最大内存限制,容器就会把应用进程kill掉,然后重启。为了解决这个问题,有3种解决方案: - System.out.println("main1 class: " + main1.getClass()); - System.out.println("main2 class: " + main2.getClass()); - System.out.println("main1 classloader: " + main1.getClass().getClassLoader()); - System.out.println("main2 classloader: " + main2.getClass().getClassLoader()); - ClassLoader itrCl = cl; - while (itrCl != null) { - System.out.println(itrCl); - itrCl = itrCl.getParent(); - } - } -} -``` +1.在应用的JVM参数中添加-Xmx 最大堆内存的大小,可以设置为容器最大内存限制的75%。一旦你在修改了容器的最大内存限制,每个应用的JVM参数-Xmx 也许需要同步进行修改。 -### 类加载的过程是什么样的? +2.就是添加这几个参数可以让Java应用感知容器的内存限制,从而在设置最大堆内存大小时,根据容器的内存限制进行设置。 +`-XX:+UnlockExperimentalVMOptions +-XX:+UseCGroupMemoryLimitForHeap +-XX:MaxRAMFraction=2` -#### 类加载器 +下面是MaxRAMFraction取不同的值时,最大堆内存与容器最大内存限制的比例。考虑到除了内存中除了最大堆内存以外,还有方法区,线程栈等需要需要占用内存,所以MaxRAMFraction一般至少取2会比较合适。如果取值为1,在最大堆内存占满时,可能Java应用占用的总内存会超过容器最大内存限制。 -类加载器是 Java 运行时环境(Java Runtime Environment)的一部分,负责动态加载 Java 类到 Java 虚拟机的内存空间中。**类通常是按需加载,即第一次使用该类时才加载。** 由于有了类加载器,Java 运行时系统不需要知道文件与文件系统。每个 Java 类必须由某个类加载器装入到内存。 -![jvm_classloader_architecture](../static/jvm_classlaoder_architecture.svg) +![image-20210207145307777](../static/image-20210207145307777.png) -类装载器除了要定位和导入二进制 class 文件外,还必须负责验证被导入类的正确性,为变量分配初始化内存,以及帮助解析符号引用。这些动作必须严格按一下顺序完成: +3.在JDK8以后,JVM增加了容器感知功能,就是如果不显示指定-Xmx2048m 最大堆内存大小, -Xms2048m最小堆内存大小,会取容器所在的物理机的内存的25%作为最大堆内存大小,也可以通过这几个参数来设置堆内存占容器内存的比例 +-XX:MinRAMPercentage 最小堆内存大小占容器内存限制的比例 +-XX:MaxRAMPercentage 最大堆内存大小占容器内存限制的比例 +-XX:InitialRAMPercentage 初始堆内存大小占容器内存限制的比例 -1. **装载**:查找并装载类型的二进制数据。 -2. **链接**:执行验证、准备以及解析(可选) - - -**验证**:确保被导入类型的正确性 - -**准备**:为类变量分配内存,并将其初始化为默认值。 - - **解析**:把类型中的符号引用转换为直接引用。 -3. **初始化**:把类变量初始化为正确的初始值。 +### 如何大体估算java进程使用的内存呢? -#### 装载 +Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss] -##### 类加载器分类 +-Xss128k: 设置每个线程的堆栈大小.JDK5.0以后默认每个线程的栈大小为1M。 -在Java虚拟机中存在多个类装载器,Java应用程序可以使用两种类装载器: +-Xms 堆内存的初始大小,默认为物理内存的1/64 +-Xmx 堆内存的最大大小,默认为物理内存的1/4 +-Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn -- **Bootstrap ClassLoader**:此装载器是 Java 虚拟机实现的一部分。由原生代码(如C语言)编写,不继承自 `java.lang.ClassLoader` 。负责加载核心 Java 库,启动类装载器通常使用某种默认的方式从本地磁盘中加载类,包括 Java API。 -- **Extention Classloader**:用来在`/jre/lib/ext` ,或 `java.ext.dirs` 中指明的目录中加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。 -- **Application Classloader**:根据 Java应用程序的类路径( `java.class.path` 或 `CLASSPATH` 环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 `ClassLoader.getSystemClassLoader()` 来获取它。 -- **自定义类加载器**:可以通过继承 `java.lang.ClassLoader` 类的方式实现自己的类加载器,以满足一些特殊的需求而不需要完全了解 Java 虚拟机的类加载的细节。 +-Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。 -##### 全盘负责双亲委托机制 +### 怎么获取 dump 文件?怎么分析? -在一个 JVM 系统中,至少有 3 种类加载器,那么这些类加载器如何配合工作?在 JVM 种类加载器通过 **全盘负责双亲委托机制** 来协调类加载器。 +1.启动时配置,出现OOM问题时自动生成 +JVM启动时增加两个参数: -- **全盘负责**:指当一个 `ClassLoader` 装载一个类的时,除非显式地使用另一个 `ClassLoader` ,该类所依赖及引用的类也由这个 `ClassLoader` 载入。 -- **双亲委托机制**:指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。 -全盘负责双亲委托机制只是 Java 推荐的机制,并不是强制的机制。实现自己的类加载器时,如果想保持双亲委派模型,就应该重写 `findClass(name)` 方法;如果想破坏双亲委派模型,可以重写 `loadClass(name)` 方法。 +```java +//出现 OOME 时生成堆 dump: +-XX:+HeapDumpOnOutOfMemoryError +//生成堆文件地址: +-XX:HeapDumpPath=/home/liuke/jvmlogs/ +``` -##### 装载入口 +2.执行jmap命令立即生成 +发现程序异常前通过执行指令,直接生成当前JVM的dmp文件,6214是指JVM的进程号 -所有Java虚拟机实现必须在每个类或接口首次主动使用时初始化。以下六种情况符合主动使用的要求: +```java +jmap -dump:format=b,file=/home/admin/logs/heap.hprof 6214 +``` -- 当创建某个类的新实例时(new、反射、克隆、序列化) -- 调用某个类的静态方法 -- 使用某个类或接口的静态字段,或对该字段赋值(用final修饰的静态字段除外,它被初始化为一个编译时常量表达式) -- 当调用Java API的某些反射方法时。 -- 初始化某个类的子类时。 -- 当虚拟机启动时被标明为启动类的类。 +获得heap.hprof以后,执行`jvisualvm`命令打开使用Java自带的工具Java VisualVM来打开heap.hprof文件,就可以分析你的Java线程里面对象占用堆内存的情况了 -除以上六种情况,所有其他使用Java类型的方式都是被动的,它们不会导致Java类型的初始化。 -当类是被动引用时,不会触发初始化: +由于第一种方式是一种事后方式,需要等待当前JVM出现问题后才能生成dmp文件,实时性不高,第二种方式在执行时,JVM是暂停服务的,所以对**线上的运行会产生影响**。所以建议第一种方式。 -1.通过子类去调用父类的静态变量,不会触发子类的初始化,只会触发包含这个静态变量的类初始化,例如执行这样的代码`SubClass.fatherStaticValue`只会触发FatherClass的初始化,不会触发SubClass的初始化,因为fatherStaticValue是FatherClass的变量 +### gc日志怎么看? -2.通过数组定义类引用类,SuperClass[] array = new SuperClass[10]; +可以在启动Java应用的命令中添加这些参数,指定生成垃圾收集的日志的路径,可以记录垃圾收集的情况。 -不会触发SuperClass类的初始化,但是执行字节码指令newarray会触发另外一个类[Lorg.fenixsoft.classloading.SuperClass的初始化,这个类继承于Object类,是一个包装类,里面包含了访问数组的所有方法, +`-XX:+PrintGCDetails +-XX:+PrintGCDateStamps +-Xloggc:/var/log/gc.log +` -3.只引用类的常量不会触发初始化,因为常量在编译阶段进入常量池 +这是一条Minor GC的回收日志(一般GC (Allocation Failure)代表Minor GC,老年代垃圾回收一般打印的是Full GC (Metadata GC Threshold) ) ```java -class SuperClass { - public static final String str = "hello"; -} +2020-05-07T16:28:02.845+0800: 78210.469: [GC (Allocation Failure) 2020-05-07T16:28:02.845+0800: 78210.469: [ParNew: 68553K->466K(76672K), 0.0221963 secs] 131148K->63062K(2088640K), 0.0223082 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] +``` -//引用常量编译时会直接存入常量池 -System.out.println(SuperClass.str); +**GC (Allocation Failure)** -``` +代表Eden区分配内存失败触发Minor GC。 +**2020-05-07T16:28:02.845+0800: 78210.469** -对于接口来说,只有在某个接口声明的非常量字段被使用时,该接口才会初始化,而不会因为事先这个接口的子接口或类要初始化而被初始化。 +是发生的时间。 +**68553K->466K(76672K)** -**父类需要在子类初始化之前被初始化**。当实现了接口的类被初始化的时候,不需要初始化父接口。然而,当实现了父接口的子类(或者是扩展了父接口的子接口)被装载时,父接口也要被装载。(只是被装载,没有初始化) +代表垃圾回收前新生代使用内存是68MB,剩余0.4MB,总内存是76MB。 +**0.0221963 secs** -#### 验证 +是垃圾回收耗时。 +**131148K->63062K(2088640K)** -确认装载后的类型符合Java语言的语义,并且不会危及虚拟机的完整性。 +代表堆区回收前使用131MB,63MB,总内存是2088MB。 -- **装载时验证**:检查二进制数据以确保数据全部是预期格式、确保除 Object 之外的每个类都有父类、确保该类的所有父类都已经被装载。 -- **正式验证阶段**:检查 final 类不能有子类、确保 final 方法不被覆盖、确保在类型和超类型之间没有不兼容的方法声明(比如拥有两个名字相同的方法,参数在数量、顺序、类型上都相同,但返回类型不同)。 -- **符号引用的验证**:当虚拟机搜寻一个被符号引用的元素(类型、字段或方法)时,必须首先确认该元素存在。如果虚拟机发现元素存在,则必须进一步检查引用类型有访问该元素的权限。 +**[Times: user=0.02 sys=0.00, real=0.02 secs]** -#### 准备 +用户态耗时0.02s,内核态耗时0s,总耗时0.02s -在准备阶段,Java虚拟机为类变量分配内存,**设置默认初始值**。但在到到初始化阶段之前,类变量都没有被初始化为真正的初始值。 + PS:有一个网站,可以对上传GC.log的日志进行分析,解析日志文件,统计出垃圾收集总占用的时间,以及新生代,老年代的内存使用峰值,https://gceasy.io/ -| 类型 | 默认值 | -| --------- | -------- | -| int | 0 | -| long | 0L | -| short | (short)0 | -| char | ’\u0000’ | -| byte | (byte)0 | -| blooean | false | -| float | 0.0f | -| double | 0.0d | -| reference | null | +![image-20210208162512025](../static/image-20210208162512025.png) -#### 解析 +### cpu 使用率特别高,怎么排查?通用方法?定位代码?cpu高的原因? -https://www.zhihu.com/question/30300585 +[CPU飙高,频繁GC,怎么排查?](https://mp.weixin.qq.com/s?src=11×tamp=1589109647&ver=2330&signature=LegOQuRkl7V9kFT6JT3Kg-9X4VYN40OmTyVRlSOFLppbFy*PTUWPAb2iyCFqz-ka9RRCje4fXGxS3sqw1z3JYQ3MRxzuI-UTLtlx-fV8VA8CLDWOFFfIuIVwrAeUdDHb&new=1) -解析的过程就是在类型的常量池总寻找类、接口、字段和方法的符号引用,**把这些符号引用替换为直接引用的过程**。 +[jstack命令:教你如何排查多线程问题](https://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&mid=2247484624&idx=1&sn=a907100b51aca8bd651aebdb9aca532f&chksm=eb5381e6dc2408f092159c453a452c2781d374ca43f0984dde87688de8a1f8d9196dfd747124&scene=21#wechat_redirect) +``` + jstat -gcutil 29530 1000 10 + 垃圾回收信息统计,29530是pid,1000是每1秒打印一次最新信息,10是最多打印10次 +``` -- `类或接口的解析`:判断所要转化成的直接引用是数组类型,还是普通的对象类型的引用,从而进行不同的解析。 -- `字段解析`:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束, -#### 初始化 +### 怎么排查CPU占用率过高的问题? +1.首先使用`top`命令查看CPU占用率高的进程的pid。 +``` +top - 15:10:32 up 523 days, 3:47, 1 user, load average: 0.00, 0.01, 0.05 +Tasks: 95 total, 1 running, 94 sleeping, 0 stopped, 0 zombie +%Cpu(s): 1.7 us, 0.5 sy, 0.0 ni, 95.7 id, 2.2 wa, 0.0 hi, 0.0 si, 0.0 st +KiB Mem : 16267904 total, 6940648 free, 2025316 used, 7301940 buff/cache +KiB Swap: 16777212 total, 16776604 free, 608 used. 13312484 avail Mem -**所有的类变量(即静态量)初始化语句和类型的静态初始化器都被Java编译器收集在一起,放到一个特殊的方法中,这个步骤就是初始化类静态变量和执行静态代码块。** 对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化方法。在类和接口的 class 文件中,这个方法被称为``。 + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND +14103 hadoop 20 0 2832812 203724 18392 S 3.7 1.3 977:08.04 java +14010 hadoop 20 0 2897344 285392 18660 S 0.3 1.8 513:30.49 java +14284 hadoop 20 0 3052556 340436 18636 S 0.3 2.1 1584:47 java +14393 hadoop 20 0 2912460 504112 18632 S 0.3 3.1 506:43.68 java + 1 root 20 0 190676 3404 2084 S 0.0 0.0 4:31.47 systemd + 2 root 20 0 0 0 0 S 0.0 0.0 0:04.77 kthreadd + 3 root 20 0 0 0 0 S 0.0 0.0 0:10.16 ksoftirqd/0 +``` +2.使用`top -Hp 进程id`获得该进程下各个线程的CPU占用情况,找到占用率最高的线程的pid2, +使用`printf "%x\n" pid2`命令将pid2转换为16进制的数number。 -1. 如果存在直接父类,且直接父类没有被初始化,先初始化直接父类。 -2. 如果类存在一个类初始化方法,执行此方法。 +``` +top - 15:11:01 up 523 days, 3:48, 1 user, load average: 0.00, 0.01, 0.05 +Threads: 69 total, 0 running, 69 sleeping, 0 stopped, 0 zombie +%Cpu(s): 12.8 us, 0.1 sy, 0.0 ni, 87.0 id, 0.1 wa, 0.0 hi, 0.0 si, 0.0 st +KiB Mem : 16267904 total, 6941352 free, 2024612 used, 7301940 buff/cache +KiB Swap: 16777212 total, 16776604 free, 608 used. 13313188 avail Mem -这个步骤是递归执行的,即第一个初始化的类一定是`Object`。 + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND +14393 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:00.01 java +14411 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:01.95 java +14412 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:16.18 java +14413 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 0:12.79 java +14414 hadoop 20 0 2912460 504112 18632 S 0.0 3.1 8:09.10 java +``` +3.使用`jstack pid`获得进程下各线程的堆栈信息,nid=0xnumber的线程即为占用率高的线程,查看它是在执行什么操作。(`jstack 5521 | grep -20 0x1596`可以获得堆栈信息中,会打印匹配到0x1596的上下20行的信息。) -**Java虚拟机必须确保初始化过程被正确地同步。** 如果多个线程需要初始化一个类,仅仅允许一个线程来进行初始化,其他线程需等待。 +例如这个线程是在执行垃圾回收: +``` +"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f338c01f000 nid=0x1593 runnable +``` -> 这个特性可以用来写单例模式。 +##### JVM相关的异常 -##### Clinit 方法 +#### 1.stackoverflow -- 对于静态变量和静态初始化语句来说:执行的顺序和它们在类或接口中出现的顺序有关。 -- **并非所有的类都需要在它们的class文件中拥有()方法,** 如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有`()`方法。如果类声明了类变量,但没有明确的使用类变量初始化语句或者静态代码块来初始化它们,也不会有`()`方法。如果类仅包含静态`final`常量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有`()`方法。**只有那些需要执行Java代码来赋值的类才会有()** -- `final`常量:Java虚拟机在使用它们的任何类的常量池或字节码中直接存放的是它们表示的常量值。 +这种就是栈的空间不足,就会抛出这个异常,一般是递归执行一个方法时,执行方法深度太深时出现。Java执行一个方法时,会创建一个栈帧来存放局部变量表,操作数栈,如果分配栈帧时,栈空间不足,那么就会抛出这个异常。 + +(栈空间可以设置-Xss参数实现,默认为1M,如果参数) ### JVM调优有哪些工具? #### jstat -jstat可以打印出当前JVM运行的各种状态信息,例如新生代内存使用情况,老年代内存使用情况,Minor GC发生总次数,总耗时,Full GC发生总次数,总耗时。 +jstat可以打印出当前JVM运行的各种状态信息,例如新生代内存使用情况,老年代内存使用情况,以及垃圾回收的时间。Minor GC发生总次数,总耗时,Full GC发生总次数,总耗时。(jmap -heap命令也可以打印出堆中各个分区的内存使用情况,但是不能定时监测,持续打印。例如每1s打印当前的堆中各个分区的内存使用情况,一直打印100次。) ``` //5828是java进程id,1000是打印间隔,每1000毫秒打印一次,100是总共打印100次 jstat -gc 5828 1000 100 @@ -1063,9 +1065,9 @@ jstat -gc 5828 1000 100 `S1U` 新生代中第二个survivor(幸存区)目前已使用空间 (字节) -`EC` 新生代中Eden(伊甸园)的总容量 (字节) +`EC` 新生代中Eden区的总容量 (字节) -`EU` 新生代中Eden(伊甸园)目前已使用空间 (字节) +`EU` 新生代中Eden区目前已使用空间 (字节) `OC` 老年代的总容量 (字节) @@ -1102,8 +1104,6 @@ jstat -gc 5828 1000 100 ![image-20200731152050340](../static/image-20200731152050340.png) - - #### jstack jstack可以生成当前JVM的线程快照,也就是当前每个线程当前的状态及正在执行的方法,锁相关的信息。`jstack -l 进程id `,-l代表除了堆栈信息外,还会打印锁的附加信息。jstack还会检测出死锁信息。一般可以用于定位线程长时间停顿,线程间死锁等问题。 @@ -1158,24 +1158,17 @@ public static void main(String[] args) { #### jmap -一般可以生成当前堆栈快照。使用 jmap -heap可以打印出当前各个分区的内存使用情况,使用`jmap -dump:format=b,file=dump.hprof 进程id`可以生成当前的堆栈快照,堆快照和对象统计信息,对生成的堆快照进行分析,可以分析堆中对象所占用内存的情况,检查大对象等。执行`jvisualvm`命令打开使用Java自带的工具Java VisualVM来打开堆栈快照文件,进行分析。可以用于排查内存溢出,内存泄露问题。 - -也可以配置启动时的JVM参数,让发送内存溢出时,自动生成堆栈快照文件。 +##### jmap -heap -``` -//出现 OOM 时生成堆 dump: --XX:+HeapDumpOnOutOfMemoryError -//生成堆文件地址: --XX:HeapDumpPath=/home/liuke/jvmlogs/ -``` +这个命令可以生成当前堆栈快照。使用 `jmap -heap 进程id`可以打印出当前堆各分区内存使用情况的情况,新生代(Eden区,To Survivor区,From Survivor区),老年代区的内存使用情况。 -*查看内存使用情况* +使用jmap -heap查看内存使用情况的案例 ![image-20200726173723112](../static/image-20200726173723112.png) +##### jmap -histo - - **jmap -histo**打印出当前堆中的对象统计信息,包括类名,每个类的实例数量,总占用内存大小。 + **jmap -histo 进程id** 打印出当前堆中的对象统计信息,包括类名,每个类的实例数量,总占用内存大小。 ```java instances列:表示当前类有多少个实例。 @@ -1187,11 +1180,26 @@ class name列:表示的就是当前类的名称,class name 对于基本数 ![image-20200726174009407](../static/image-20200726174009407.png) + + +##### jmap -dump + +使用`jmap -dump:format=b,file=dump.hprof 进程id`可以生成当前的堆栈快照,堆快照和对象统计信息,对生成的堆快照进行分析,可以分析堆中对象所占用内存的情况,检查大对象等。执行`jvisualvm`命令打开使用Java自带的工具**Java VisualVM**来打开堆栈快照文件,进行分析。可以用于排查内存溢出,内存泄露问题。在**Java VisualVM**里面可以看到每个类的实例对象占用的内存大小,以及持有这个对象的实例所在的类等等信息。 + +也可以配置启动时的JVM参数,让发送内存溢出时,自动生成堆栈快照文件。 + +```java +//出现 OOM 时生成堆 dump: +-XX:+HeapDumpOnOutOfMemoryError +//生成堆文件地址: +-XX:HeapDumpPath=/home/liuke/jvmlogs/ +``` + 使用**jmap -dump:format=b,file=/存放路径/heapdump.hprof 进程id**就可以得到堆转储文件,然后执行jvisualvm命令就可以打开JDK自带的jvisualvm软件。 例如在这个例子中会造成OOM问题,通过生成heapdump.hprof文件,可以使用jvisualvm查看造成OOM问题的具体代码位置。 -``` +```java public class Test018 { ArrayList arrayList = new ArrayList(); @@ -1262,4 +1270,29 @@ public class Test018 { ![img](../static/640-20200726210041919.jpeg) -![img](../static/640-20200726210058837.jpeg) \ No newline at end of file +![img](../static/640-20200726210058837.jpeg) + +### happens-before指的是什么? +happens-before主要是为保证Java多线程操作时的有序性和可见性,防止了编译器重排序对程序结果的影响。 +因为当一个变量出现一写多读,多读多写的情况时,就是多个线程读这个变量,然后至少一个线程写这个变量,因为编译器在编译时会对指令进行重排序,如果没有happens-before原则对重排序进行一些约束,就有可能造成程序执行不正确的情况。 + +具体有8条规则: +1.**程序次序性规则**:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。一段代码在单线程中执行的结果是有序的。(只在单线程有效,一写多读时,写的变量如果是没有使用volatile修饰时,是没法保证其他线程读到的变量是最新的值) + +2.**锁定规则**:一个锁的解锁操作总是要在加锁操作之前。 + +3.**volatile规则**:如果一个写操作去写一个volatile变量,一个读操作去读volatile变量,那么写操作一定是在读操作之前。 + +4.**传递规则**:操作A happens before 操作B, B happens before C,那么A一定是happens before C的。 + +5.**线程启动规则**:线程A执行过程中修一些共享变量,然后再调用threadB.start()方法来启动线程B,那么A对那些变量的修改对线程B一定是可见的。 + +6.**线程终结规则**:线程A执行过程中调用了threadB.join()方法来等待线程B执行完毕,那么线程B在执行过程中对共享变量的修改,在join()方法返回后,对线程A可见。 + +7.**线程中断规则**:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 + +8.**对象终结规则**:一个对象的初始化完成先行发生于他的finalize()方法的开始; + +https://www.cnblogs.com/chenssy/p/6393321.html + +http://ifeve.com/java-%e4%bd%bf%e7%94%a8-happen-before-%e8%a7%84%e5%88%99%e5%ae%9e%e7%8e%b0%e5%85%b1%e4%ba%ab%e5%8f%98%e9%87%8f%e7%9a%84%e5%90%8c%e6%ad%a5%e6%93%8d%e4%bd%9c/#more-38824 \ No newline at end of file diff --git a/docs/JavaMultiThread.md b/docs/JavaMultiThread.md index 9b2ce2c..ba3743f 100644 --- a/docs/JavaMultiThread.md +++ b/docs/JavaMultiThread.md @@ -3,21 +3,22 @@ #### [1.进程与线程的区别是什么?](#进程与线程的区别是什么?) #### [2.进程间如何通信?](#进程间如何通信?) + #### [3.Java中单例有哪些写法?](#Java中单例有哪些写法?) #### [4.Java中创建线程有哪些方式?](#Java中创建线程有哪些方式?) #### [5.如何解决序列化时可以创建出单例对象的问题?](#如何解决序列化时可以创建出单例对象的问题?) -#### [6.悲观锁和乐观锁是什么?](#悲观锁和乐观锁是什么?) -#### [7.volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?](#volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?) -#### [8.Java中线程的状态是怎么样的?](#Java中线程的状态是怎么样的?) -#### [9.wait(),join(),sleep()方法有什么作用?](#wait(),join(),sleep()方法有什么作用?) -#### [10.Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?](#Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?) -#### [11.谈一谈你对线程中断的理解?](#谈一谈你对线程中断的理解?) -#### [12.线程间怎么通信?](#线程间怎么通信?) -#### [13.怎么实现实现一个生产者消费者?](#怎么实现实现一个生产者消费者?) -#### [14.谈一谈你对线程池的理解?](#谈一谈你对线程池的理解?) -#### [15.线程池有哪些状态?](#线程池有哪些状态?) +#### [6.volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?](#volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性?) +#### [7.Java中线程的状态是怎么样的?](#Java中线程的状态是怎么样的?) +#### [8.wait(),join(),sleep()方法有什么作用?](#wait(),join(),sleep()方法有什么作用?) +#### [9.Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?](#Thread.sleep(),Object.wait(),LockSupport.park()有什么区别?) +#### [10.谈一谈你对线程中断的理解?](#谈一谈你对线程中断的理解?) +#### [11.线程间怎么通信?](#线程间怎么通信?) +#### [12.怎么实现实现一个生产者消费者?](#怎么实现实现一个生产者消费者?) +#### [13.谈一谈你对线程池的理解?](#谈一谈你对线程池的理解?) +#### [14.线程池有哪些状态?](#线程池有哪些状态?) ### 进程与线程的区别是什么? + #### 批处理操作系统 **批处理操作系统**就是把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。 @@ -50,7 +51,19 @@ Linux系统会给每个进程分配4G的虚拟地址空间(0到3G是User地址 #### 动态性 -程序是一个静态的指令集合,而进程是正在操作系统中运行的指令集合,进程有自己的生命周期和各种不同的状态。(五态模型一般指的是新建态(创建一个进程),就绪态(已经获取到资源,准备好了,进入运行队列,一旦获得时间片可以立即执行),阻塞态(运行过程中等待获取其他资源,I/O请求等),终止态(进程被杀死了))。 +程序是一个静态的指令集合,而进程是正在操作系统中运行的指令集合,进程有自己的生命周期和各种不同的状态。 + +五态模型一般指的是: + +**新建态**(创建一个进程) + +**就绪态**(已经获取到资源,准备好了,进入运行队列,一旦获得时间片可以立即执行) + +**运行态**(获取到了时间片,执行程序) + +**阻塞态**(运行过程中等待获取其他资源,I/O请求等) + +**终止态**(进程被杀死了) #### 并发性 @@ -99,7 +112,7 @@ class CustomThread extends Thread { } void run() { System.out.println(Thread.currentThread().getName()+"线程调用了run()方法"); - for (int j = 0; j < 20; j++) { + for (int j = 0; j < 5; j++) { System.out.println(Thread.currentThread().getName()+"线程--j是"+j); } System.out.println("run()方法执行完毕!"); @@ -113,26 +126,10 @@ class CustomThread extends Thread { main线程调用了main方法 Thread-0线程调用了run()方法 Thread-0线程--j是0 -Thread-0线程--j是1 +main线程--i是1 Thread-0线程--j是2 Thread-0线程--j是3 Thread-0线程--j是4 -Thread-0线程--j是5 -Thread-0线程--j是6 -Thread-0线程--j是7 -Thread-0线程--j是8 -Thread-0线程--j是9 -Thread-0线程--j是10 -Thread-0线程--j是11 -Thread-0线程--j是12 -Thread-0线程--j是13 -Thread-0线程--j是14 -main线程--i是1 -Thread-0线程--j是15 -Thread-0线程--j是16 -Thread-0线程--j是17 -Thread-0线程--j是18 -Thread-0线程--j是19 run()方法执行完毕! main()方法执行完毕! ``` @@ -176,7 +173,7 @@ public synchronized void start() { ##### 注意事项: -start()方法中判断threadStatus是否为0,是判断当前线程是否新建态,0是代表新建态(上图中的源码注释里面有提到),而不是就绪态,因为Java中,就绪态和运行态是合并在一起的,(Thread的state为RUNNABLE时(也就是threadStatus为4时),代表线程为就绪态或运行态)。执行start()方法的线程还不是JVM新建的线程,所以不是就绪态。有一些技术文章把这里弄错了,例如这一篇[《深入浅出线程Thread类的start()方法和run()方法》](https://juejin.im/post/5b09274af265da0de25759d5) +start()方法中判断threadStatus是否为0,是判断当前线程是否新建态,0是代表新建态(上图中的源码注释里面有提到),而不是就绪态,因为Java的Thread类中,Thread的Runnable状态包括了线程的就绪态和运行态,(Thread的state为RUNNABLE时(也就是threadStatus为4时),代表线程为就绪态或运行态)。执行start()方法的线程还不是JVM新建的线程,所以不是就绪态。有一些技术文章把这里弄错了,例如这一篇[《深入浅出线程Thread类的start()方法和run()方法》](https://juejin.im/post/5b09274af265da0de25759d5) ![image-20200105144031591](../static/image-20200105144031591.png) @@ -186,7 +183,7 @@ start()方法中判断threadStatus是否为0,是判断当前线程是否新建 #### 第二种 实现Runnable接口 -这种方式就是创建一个类Target,实现Runnable接口的Run方法,然后将Target类的实例对象作为Thread的构造器入参target,实际的线程对象还是Thread实例,只不过线程Thread与线程执行体(Target类的run方法)分离了,耦合度更低一些。 +这种方式就是创建一个类(例如下面代码中的Target类),实现Runnable接口的Run方法,然后将Target类的实例对象作为Thread的构造器入参target,实际的线程对象还是Thread实例,只不过线程Thread与线程执行体(Target类的run方法)分离了,耦合度更低一些。 ```java class ThreadTarget implements Runnable { @@ -208,7 +205,7 @@ class ThreadTarget implements Runnable { ##### 原理 -之所以有这种实现方法,是因为Thread类的run方法中会判断成员变量target是否为空,不为空就会调用target类的run方法。 +之所以有这种实现方法,是因为Thread类的run()方法中会判断成员变量target是否为空,不为空就会调用target类的run方法。 ```java private Runnable target; @@ -262,17 +259,15 @@ thread.start() #### 第三种 实现Callable接口 -创建一个类CallableTarget,实现Callable接口,实现带有返回值的call()方法,以CallableTarget实例对象作为创建FutureTask对象的参数,FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承于Runnable, Future接口,所以FutureTask对象可以作为创建Thread对象的入参,创建Thread对象,然后调用start方法。 +Runnable接口中的run()方法是没有返回值,如果我们需要执行的任务带返回值就不能使用Runnable接口。创建一个类CallableTarget,实现Callable接口,实现带有**返回值的call()方法**,然后根据CallableTarget创建一个任务FutureTask,然后根据FutureTask来创建一个线程Thread,调用Thread的start方法可以执行任务。 ```java public class CallableTarget implements Callable { - public Integer call() throws InterruptedException { System.out.println(Thread.currentThread().getName()+"线程执行了call方法"); Thread.sleep(5000); return 1; } - public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println(Thread.currentThread().getName()+"线程执行了main方法"); CallableTarget callableTarget = new CallableTarget(); @@ -286,34 +281,151 @@ public class CallableTarget implements Callable { } ``` -Callable接口的源码 +原理就是Thread类默认的run()方法实现是会去调用自身实例变量target的run()方法,(target就是我们构造Thread传入的FutureTask),而FutureTask的run方法中就会调用Callable接口的实例的call()方法。 ```java -@FunctionalInterface -public interface Callable { - V call() throws Exception; +//Thread类的run方法实现 +@Override +public void run() { + if (target != null) { + //这里target就是我们在创建Thread时传入的FutureTask实例变量 + target.run(); + } +} +//FutureTask类的run方法实现 +public void run() { + if (state != NEW || + !UNSAFE.compareAndSwapObject(this, runnerOffset, + null, Thread.currentThread())) + return; + try { + Callable c = callable; + if (c != null && state == NEW) { + V result; + boolean ran; + try { + //在这里会调用Callable实例的call方法 + result = c.call(); + ran = true; + } catch (Throwable ex) { + result = null; + ran = false; + setException(ex); + } + if (ran) + set(result); + } + } finally { + // runner must be non-null until state is settled to + // prevent concurrent calls to run() + runner = null; + // state must be re-read after nulling runner to prevent + // leaked interrupts + int s = state; + if (s >= INTERRUPTING) + handlePossibleCancellationInterrupt(s); + } } ``` -RunnableFuture接口的源码 +### Java中的Runnable、Callable、Future、FutureTask的区别和联系? +最原始的通过新建线程执行任务的方法就是我们去新建一个类,继承Thread,然后去重写run()方法,但是这样限制太大了,Java也不支持多继承。所以有了Runnable。 +##### Runnable +Runnable是一个接口,只需要新建一个类实现这个接口,然后重写run方法,将该类的实例作为创建Thread的入参,线程运行时就会调用该实例的run方法。 ```java +@FunctionalInterfacepublic interface Runnable { + public abstract void run(); +} +``` + +Thread.start()方法->Thread.run()方法->target.run()方法 + +##### Callable + +Callable跟Runnable类似,也是一个接口。只不过它的call方法有返回值,可以供程序接收任务执行的结果。 + +```java +@FunctionalInterfacepublic interface Callable { + V call() throws Exception; +} +``` + +##### Future + +Future也是一个接口,Future就像是一个管理的容器一样,进一步对Runable和Callable的实例进行封装,定义了一些方法。取消任务的cancel()方法,查询任务是否完成的isDone()方法,获取执行结果的get()方法,带有超时时间来获取执行结果的get()方法。 + +```java +public interface Future { + //mayInterruptIfRunning代表是否强制中断 + //为true,如果任务已经执行,那么会调用Thread.interrupt()方法设置中断标识 + //为false,如果任务已经执行,就只会将任务状态标记为取消,而不会去设置中断标识 + boolean cancel(boolean mayInterruptIfRunning); + boolean isCancelled(); + boolean isDone(); + V get() throws InterruptedException, ExecutionException; + V get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException; +} +``` + +##### FutureTask + +因为Future只是一个接口,并不能实例化,可以认为FutureTask就是Future接口的实现类,FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承Runnable接口和Future接口。 + +```java +public class FutureTask implements RunnableFuture { +... +} + public interface RunnableFuture extends Runnable, Future { void run(); } ``` +##### 使用案例 +使用时,Runnable实现类的实例可以作为Thread的入参使用,而Callable只能使用FutureTask进行封装使用。 + +```java +//Runnable配合Thread进行使用 +Thread threadA = new Thread(new Runnable() { + @Override + public void run() { + //任务的代码 + } + }); + +//Callable使用FutureTask封装后,配合线程池进行使用 +ExecutorService pool = Executors.newSingleThreadExecutor(); +FutureTask task = new FutureTask(new Callable() { + @Override + public Object call() throws Exception { + //任务的代码 + return null; + } +}); +pool.submit(task); + +//Runnable使用FutureTask封装后,配合线程池进行使用 +FutureTask task1 = new FutureTask(new Runnable() { + @Override + public void run() { + //任务的代码 + } +}); +pool.submit(task1); +``` ### Java中单例有哪些写法? 正确并且可以做到延迟加载的写法其实就是三种: -使用volatile修饰变量并且双重校验的写法。 +1.使用volatile修饰变量并且双重校验的写法来实现。 -使用静态内部类来实现(类A有一个静态内部类B,类B有一个静态变量instance,类A的getInstance()方法会返回类B的静态变量instance,因为只有调用getInstance()方法时才会加载静态内部类B,这种写法缺点是不能传参。) +2.使用静态内部类来实现(类A有一个静态内部类B,类B有一个静态变量instance,类A的getInstance()方法会返回类B的静态变量instance,因为只有调用getInstance()方法时才会加载静态内部类B,这种写法缺点是不能传参。) -使用枚举来实现() +3.使用枚举来实现 #### 第1种 不加锁(裸奔写法) @@ -386,7 +498,7 @@ instance = new Singleton(); 3.将分配好的内存地址设置给instance引用。 -但是编译器会对指令进行重排序,只能保证单线程执行时结果不会变化,也就是可能第3步会在第2步之前执行,某个线程A刚好执行完第3步,正在执行第2步时,此时如果有其他线程B进入if (instance == null)判断,会发现instance不为null,然后将instance返回,但是实际上instance还没有完成初始化,线程B会访问到一个未初始化完成的instance对象。 +但是编译器会对指令进行重排序,只能保证单线程执行时结果不会变化,也就是可能第3步会在第2步之前执行,某个线程A刚好执行完第3步,正在执行第2步时,此时如果有其他线程B进入if (instance == null)判断,会发现instance不为null,然后将instance返回,但是实际上instance还没有完成初始化,线程B会访问到一个未初始化完成的instance对象。所以需要像第5种解法一样使用volatile来修饰变量,防止重排序。 #### 第5种 基于 volatile 的双重检查锁定的解决方案 @@ -420,13 +532,15 @@ volatile修饰的变量在编译后,会多出一个lock前缀指令,lock前 #### 第6种 - 使用静态内部类来实现 ```java - private static class Signleton { - private static Signleton instance = new Signleton(); +class Test { + public static Signleton getInstance() { + return Signleton.instance ; // 只有调用getInstance()方法时,才会引用到静态内部类Signleton,从而会触发Signleton类的instance变量的初始化,以此实现懒加载的目的。 } - public static Signleton getInstance() { - return Signleton.instance ; // 这里将导致 Signleton 类被初始化 + private static class Signleton { + private static Signleton instance = new Signleton(); } +} ``` 因为JVM底层通过加锁实现,保证一个类只会被加载一次,多个线程在对类进行初始化时,只有一个线程会获得锁,然后对类进行初始化,其他线程会阻塞等待。所以可以使用上面的代码来保证instance只会被初始化一次,这种写法的问题在于创建单例时不能传参。 @@ -457,103 +571,6 @@ public class Singleton implements java.io.Serializable { ``` 通过实现readResolve方法,ObjectInputStream实例对象在调用readObject()方法进行反序列化时,就会判断相应的类是否实现了readResolve()方法,如果实现了,就会调用readResolve()方法返回一个对象作为反序列化的结果,而不是去创建一个新的对象。 - - -### 悲观锁和乐观锁是什么? - -##### 悲观锁 - -就是假定在每次取数据的时候会修改这个数据,所以在取数据的时候就会进行加锁,这样其他调用者就不能取数据,阻塞等待,一直到获取到锁。Java中的同步锁sychronized和ReentrantLock就是悲观锁思想的实现。 - -##### 乐观锁 - -就是假定在每次取数据时不会修改这个数据,所以在取数据的时候不会加锁,只有在真正修改数据时才加锁。Java中的atomic原子变量类就是乐观锁的实现。 - -##### 区别 - -##### 悲观锁适合多写的场景, - -乐观锁适合多读的场景,这样只有读写冲突会发生的比较少,减少加锁的性能开销。但是如果是多写的场景,这样会导致上层应用一直重试,增加性能开销。 - - - -### 乐观锁的实现 - -#### 版本号机制 - -使用版本号来实现,对数据加上一个版本号,代表修改次数,每次修改后+1,修改数据时判断数据的版本号跟之前取的是否一致,一致才修改,不一致就重试,直到更新成功。 - -#### CAS操作 - -就是在更新数据时会传入之前取的值,在内存中判断当前内存中的值跟之前的值是否一致,一致再更新,(比较和更新都是在一个原子操作中)。 - -##### ABA问题 - -但是没法解决ABA的问题,就是其他调用方对数据修改成其他值后又改回原来的值。AtomicStampedReference的compareAndSet会先判断对象的引用是否相同,相同才进行CAS更新。实现原理主要是AtomicStampedReference会保存之前对象的的引用,及一个修改版本号,只有当引用和版本号都相等的情况下,才会进行CAS更新操作。 - -##### 循环时间长开销大 - -自旋CAS操作如果不成功就一直循环执行直到成功,如果长时间不成功,会给CPU带来非常大的执行开销 - -##### CAS 只对单个共享变量有效 - -多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作 - -#### Java的原子类 - -原子类一共有以下四种 - -- 1.基本类型: AtomicInteger, AtomicLong, AtomicBoolean ; -- 2.数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ; -- 3.引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ; -- 4.对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。 - -##### AtomicInteger - -主要是对Integer的封装,提供了一些原子性的操作,因为如果是使用Integer来完成i=i+1;操作,在内存中是三个步骤,先将从内存中取出i,放到寄存器中,然后将寄存器中的值与1相加,然后将结果写入内存,一共是三个步骤,所以不是原子性的,并发时会造成数据不一致的问题。 - -主要实现原理是AtomicInteger类有一个unsafe属性,可以通过unsafe来调用Unsafe类的一些原子性的方法Unsafe.compareAndSwapInt来实现原子性的加减运算。 - -其次是使用volatile来修饰value属性,保证一个内存可见性 - -``` -//compareAndSwapInt有四个参数,第一个是待运算的对象,第二个是对象中用于运算的属性的偏移量,第三个是期望值,第四个是更新的值。 -unsafe.compareAndSwapInt(this, valueOffset, expect, update) -``` - -``` -public class AtomicInteger extends Number implements java.io.Serializable { - private static final long serialVersionUID = 6214790243416807050L; - - // setup to use Unsafe.compareAndSwapInt for updates - private static final Unsafe unsafe = Unsafe.getUnsafe(); - private static final long valueOffset; - - static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } - } - - private volatile int value;//使用volatiole来保证value的内存可见性 -} - -``` - -在Unsafe类中,compareAndSwapInt和getAndAddInt的区别在于,getAndAddInt会一直重试直到成功,compareAndSwapInt如果更新失败,只会返回false - -``` -public final int getAndAddInt(Object var1, long var2, int var4) { - int var5; - do { - var5 = this.getIntVolatile(var1, var2); - } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); - - return var5; -} -``` - ### volatile关键字有什么用?怎么理解可见性,一般什么场景去用可见性? 当线程进行一个volatile变量的写操作时,JIT编译器生成的汇编指令会在写操作的指令后面加上一个“lock”指令。 @@ -566,18 +583,33 @@ instance = new Singleton(); // instance是volatile变量 “lock”有三个作用: 1.将当前CPU缓存行的数据会写回到系统内存。 + 2.这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效。 3.确保指令重排序时,内存屏障前的指令不会排到后面去,内存屏障后的指令不会排到前面去。 -可见性可以理解为一个线程的写操作可以立即被其他线程得知。为了提高CPU处理速度,CPU一般不直接与内存进行通信,而是将系统内存的数据读到内部缓存,再进行操作,对于普通的变量,修改完不知道何时会更新到系统内存。但是如果是对volatile修饰的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据立即写回到系统内存。但是即便写回到系统内存,其他CPU中的缓存行数据还是旧的,为了保证数据一致性,其他CPU会嗅探在总线上传播的数据来检查自己的缓存行的值是否过期,当CPU发现缓存行对应的内存地址被修改,那么就会将当前缓存行设置为无效,下次当CPU对这个缓存行上 的数据进行修改时,会重新从系统内存中把数据读到处理器缓存 里。 +可见性可以理解为一个线程的写操作可以立即被其他线程得知。为了提高CPU处理速度,CPU一般不直接与内存进行通信,而是将系统内存的数据读到内部缓存,再进行操作。对于普通的变量,修改完不知道何时会更新到系统内存。但是如果是对volatile修饰的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据立即写回到系统内存。但是即便写回到系统内存,其他CPU中的缓存行数据还是旧的,为了保证数据一致性,其他CPU会嗅探在总线上传播的数据来检查自己的缓存行的值是否过期,当CPU发现缓存行对应的内存地址被修改,那么就会将当前缓存行设置为无效,下次当CPU对这个缓存行上的数据进行修改时,会重新从系统内存中把数据读到处理器缓存里。 ##### 使用场景 ##### 读写锁 -如果需要实现一个读写锁,每次只能一个线程去写数据,但是有多个线程来读数据,就synchronize同步锁来对set方法加锁,get方法不加锁, 使用volatile来修饰变量,保证内存可见性,不然多个线程可能会在变量修改后还读到一个旧值。 +如果需要实现一个读写锁,每次只能一个线程去写数据,但是有多个线程来读数据,就synchronized同步锁来对set方法加锁,get方法不加锁, 使用volatile来修饰变量,保证内存可见性,不然多个线程可能会在变量修改后还读到一个旧值。 + +```java +volatile Integer a; +//可以实现一写多读的场景,保证并发修改数据时的正确。 +set(Integer c) { + synchronized(this.a) { + this.a = c; + + } +} +get() { + return a; +} +``` ##### 状态位 @@ -594,16 +626,16 @@ instance = new Singleton(); // instance是volatile变量 所以传统的操作系统线程一般有以下状态 -1. 新建状态: +1. **新建状态**: 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。 -2. 就绪状态: +2. **就绪状态**: 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。 -3. 运行状态: +3. **运行状态:** 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 -4. 阻塞状态: +4. **阻塞状态:** 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种: @@ -611,7 +643,7 @@ instance = new Singleton(); // instance是volatile变量 - 同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。 - 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。 -5. 死亡状态: +5. **死亡状态:** 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。 但是Java中Thread对象的状态划分跟传统的操作系统线程状态有一些区别。 @@ -643,7 +675,7 @@ public enum State { #### BLOCKED 阻塞态 -阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。 +阻塞状态。线程没有申请到synchronize同步锁,就会处于阻塞状态,等待锁的释放以进入同步区。 #### WAITING 等待态 @@ -709,7 +741,7 @@ join()方法是Thread类的一个实例方法。它的作用是让当前线程 实现原理是join()方法本身是一个sychronized修饰的方法,也就是调用join()这个方法需要先获取threadA的锁,获得锁之后再调用wait()方法来进行等待,一直到threadA执行完成后,threadA会调用notify_all()方法,唤醒所有等待的线程,当前线程才会结束等待。 -``` +```java Thread threadA = new Thread(); threadA.join(); ``` @@ -761,21 +793,18 @@ static void ensure_join(JavaThread* thread) { thread->clear_pending_exception(); - java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED); - + java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED); java_lang_Thread::set_thread(threadObj(), NULL); - //同志们看到了没,别的不用看,就看这一句 //thread就是当前线程,是啥?就是刚才例子中说的threadA线程 lock.notify_all(thread); - thread->clear_pending_exception(); } ``` #### sleep()方法是什么? -sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。:**sleep方法是不会释放当前的锁的,而wait方法会。** +sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。:**sleep方法是不会释放当前线程持有的锁,而wait方法会。** sleep与wait方法的区别: @@ -809,7 +838,7 @@ https://blog.csdn.net/u013332124/article/details/84647915 ### 谈一谈你对线程中断的理解? -在Java中认为,一个线程不应该由其他线程来强制中断或者停止,所以一些会强制中断线程的方法Thread.stop, Thread.suspend都已经废弃了。所以一般是通过调用thread.interrupt();方法来设置线程的中断标识, +在Java中认为,一个线程不应该由其他线程来强制中断或者停止,所以一些会强制中断线程的方法Thread.stop(), Thread.suspend()方法都已经废弃了。所以一般是通过调用thread.interrupt();方法来设置线程的中断标识, 1.这样如果线程是处于阻塞状态,会抛出InterruptedException异常,代码可以进行捕获,进行一些处理。(例如Object#wait、Thread#sleep、BlockingQueue#put、BlockingQueue#take。其中BlockingQueue主要调用conditon.await()方法进行等待,底层通过LockSupport.park()实现) @@ -823,9 +852,167 @@ thread.isInterrupted() //会返回当前的线程中断状态,并且重置线程的中断标识,将中断标识设置为false thread.interrupted() ``` + +### 线程执行的任务可以终止吗? + +##### 1.设置中断 + +FutureTask提供了cancel(boolean mayInterruptIfRunning)方法来取消任务,并且 + +如果入参为false,如果任务已经在执行,那么任务就不会被取消。 + +如果入参为true,如果任务已经在执行,那么会调用Thread的interrupt()方法来设置线程的中断标识,如果线程处于阻塞状态,会抛出InterruptedException异常,如果正常状态只是设置标志位,修改interrupted变量的值。所以如果要取消任务只能在任务内部中调用thread.isInterrupted()方法获取当前线程的中断状态,自行取消。 + +##### 2.线程的stop方法 + +线程的stop()方法可以让线程停止执行,释放所有的锁,抛出ThreadDeath这种Error。但是在释放锁之后,没有办法让受这些锁保护的资源,对象处于一个安全,一致的状态。(例如有一个变量a,本来的值是0,你的线程任务是将a++后然后再进行a--。正常情况下任务执行完之后,其他线程取到这个变量a的值应该是0,但是如果之前调用了Thread.stop方法时,正好是在a++之后,那么变量a就会是1,这样其他线程取到的a就是出于不一致的状态。) + +### 让线程顺序执行有哪些方法? + +##### 1.主线程Join + +就是调用threadA.start()方法让线程A先执行,然后主线程调用threadA.join()方法,然后主线程进入TIME_WAITING状态,直到threadA执行结束后,主线程才能继续往下执行,执行线程B的任务。(join方法的底层实现其实是调用了threadA的wait()方法,当线程A执行完毕后,会自动调用notifyAll()方法唤醒所有线程。) + +示例代码如下: + +```java +Thread threadA = new Thread(new Runnable() { + @Override + public void run() { + //执行threadA的任务 + } +}); +Thread threadB= new Thread(new Runnable() { + @Override + public void run() { + //执行threadB的任务 + } +}); +//执行线程A任务 +threadA.start(); +//主线程进行等待 +threadA.join(); +//执行线程B的任务 +threadB.start(); +``` + +##### 子线程Join + +就是让线程B的任务在执行时,调用threadA.join()方法,这样就只有等线程A的任务执行完成后,才会执行线程B。 + +```java + Thread threadA = new Thread(new Runnable() { + @Override + public void run() { + //执行threadA的任务 + } + }); + Thread threadB= new Thread(new Runnable() { + @Override + public void run() { + //子线程进行等待,知道threadA任务执行完毕 + threadA.join(); + //执行threadB的任务 + } + }); + //执行线程A任务 + threadA.start(); + //执行线程B的任务 + threadB.start(); +``` +##### 单线程池法 + +就是使用Executors.newSingleThreadExecutor()这个线程池,这个线程池的特点就是只有一个执行线程,可以保证任务按顺序执行。 + +```java +ExecutorService pool = Executors.newSingleThreadExecutor(); +//提交任务A +executorService.submit(taskA); +//提交任务B +executorService.submit(taskB); +``` + +##### 等待通知法(wait和notify) + +就是在线程B中调用Object.waiting()方法进行等待,线程A执行完毕后调用Object.notify()方法进行唤醒。(这种方法有两个缺点,一个是Object.waiting()和notify()方法必须在同步代码块中调用,第二个是如果线程A执行过快,先调用了object.notify()方法,就会导致线程B后面一直得不到唤醒。) + +```java + final Object object = new Object(); + Thread threadA = new Thread(new Runnable() { + @Override + public void run() { + //执行threadA的任务 + synchronized(object) { + object.notify(); + } + } + }); + Thread threadB= new Thread(new Runnable() { + @Override + public void run() { + synchronized(object) { + //子线程进行等待,知道threadA任务执行完毕 + object.wait(); + //执行threadB的任务 + } + } + }); +``` + +##### 等待通知法(await和singal) + +具体实现就是Reentrantlock可以创建出一个Condition实例queue,可以认为是一个等待队列,线程B调用queue.await()就会进行等待,直到线程A执行完毕调用queue.signal()来唤醒线程B。 + +```java + final ReentrantLock lock = new ReentrantLock(); + final Condition queue1 = lock.newCondition(); + final Object object = new Object(); + final Thread threadA = new Thread(new Runnable() { + @Override + public void run() { + //执行threadA的任务 + lock.lock(); + try { + //唤醒线程B的任务 + queue1.signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("执行了任务A2"); + lock.unlock(); + } + }); + final Thread threadB= new Thread(new Runnable() { + @Override + public void run() { + lock.lock(); + //子线程进行等待,知道threadA任务执行完毕 + try { + queue1.await(); + System.out.println("执行了任务B2"); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + //执行threadB的任务 + lock.unlock(); + } + }); + threadA.start(); + threadB.start(); +``` + + + +参考链接: + +http://cnblogs.com/wenjunwei/p/10573289.html + ### 线程间怎么通信? -1.通过sychronized锁来进行同步,让一次只能一个线程来执行。 +#### 1.synchronized锁 + +通过synchronized锁来进行同步,让一次只能一个线程来执行。 #### 2.等待/通知机制 @@ -875,7 +1062,7 @@ synchronized(对象) { while(条件不满足) { 在代码清单4-12所示的例子中,创建了printThread,它用来接受main线程的输入,任何 main线程的输入均通过PipedWriter写入,而printThread在另一端通过PipedReader将内容读出并打印。 代码清单4-12 Piped.java -``` +```java public class Piped { public static void main(String[] args) throws Exception { PipedWriter out = new PipedWriter(); @@ -918,7 +1105,7 @@ Thread.join()的使用如果一个线程A执行了thread.join()语句,当前 ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这 个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个 线程上的一个值。 -``` +```java public class Profiler { // 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次 private static final ThreadLocal TIME_THREADLOCAL = new ThreadLocal() { @@ -945,7 +1132,40 @@ Profiler可以被复用在方法调用耗时统计的功能上,在方法的入 ### 怎么实现实现一个生产者消费者? #### 1.使用Object.wait()和Object.notify()实现 -使用queue作为一个队列,存放数据,并且充当锁,每次只能同时存在一个线程来生产或者消费数据,一旦队列容量>10,就进入waiting状态,一旦成功往队列添加数据,那么就唤醒所有线程(主要是生产者线程起来消费)。生产者消费时一旦发现队列容量==0,也会主动进入waiting状态。 +使用queue作为一个队列,存放数据,并且使用Synchronized同步锁,每次只能同时存在一个线程来生产或者消费数据, + +生成线程发现队列容量>10,生产者线程就进入waiting状态,一旦成功往队列添加数据,那么就唤醒所有线程(主要是生产者线程起来消费)。 + +消费线程消费时,发现队列容量==0,也会主动进入waiting状态。 + +伪代码如下: + +```java +LinkedList queue = new LinkedList<>(); +void produce(Integer value) { + synchronized(queue) {//加锁控制,保证同一时间点,只能有一个线程生成或者消费 + while(queue.size()>10) { + queue.waiting(); + } + queue.add(value); + //唤醒消费者线程 + queue.notifyAll(); + } +} +Integer consumer() { + synchronized(queue) {//加锁控制,保证同一时间点,只能有一个线程生成或者消费 + while(queue.size()==0) { + queue.waiting(); + } + Integer value = queue.poll(); + //唤醒生产者线程 + queue.notifyAll(); + return value; + } +} +``` + +完整代码如下: ```java public static void main(String[] args) { @@ -1026,7 +1246,7 @@ private static class Producer { 调用Object.wait()方法可以让线程进入等待状态,被添加到Object的monitor监视器的等待队列中,Object.notifyAll()可以唤醒monitor监视器等待队列中的所有线程。 -而调用lock的newCondition()方法,可以返回一个ConditionObject实例对象,每个ConditionObject包含一个链表,存储等待队列。可以认为一个ReentrantLock有一个同步队列(存放没有获得锁的线程),和多个等待队列(存放调用await()方法的线程)。使用Condition.singal()和Condition.singalAll()可以更加精准的唤醒线程,也就是唤醒的都是这个Condition对应的等待队列里面的线程,而Object.notify()和Object.notifyAll()只能唤醒唯一的等待队列中的线程。 +而调用lock的newCondition()方法,可以返回一个ConditionObject实例对象,每个ConditionObject包含一个链表,存储等待队列。可以认为一个ReentrantLock有一个同步队列(存放没有获得锁的线程),和多个等待队列(存放调用await()方法的线程)。使用Condition.singal()和Condition.singalAll()可以更加**精准的唤醒线程**,也就是唤醒的都是这个Condition对应的等待队列里面的线程,而Object.notify()和Object.notifyAll()只能唤醒等待队列中的所有的线程。 ```java ReentrantLock lock = new ReentrantLock(); @@ -1164,7 +1384,8 @@ private static class Producer { ```java public class BlockQueueRepository extends AbstractRepository implements Repository { - public BlockQueueRepository() { + public BlockQueueRepository(int cap) { + //cap代表队列的最大容量 products = new LinkedBlockingQueue<>(cap); } @@ -1212,43 +1433,74 @@ public class BlockQueueRepository extends AbstractRepository implements Re #### 线程池有哪些参数? -##### corePoolSize 核心线程数 +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory) { + this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, + threadFactory, defaultHandler); + } +``` + +##### 1.corePoolSize 核心线程数 + +该线程池中**核心线程数最大值**,添加任务时,即便有空闲线程,只要当前线程池线程数())); } ``` +一句话总结就是:**单线程池,等待队列无限长。** +创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。主要是通过将核心线程数和最大线程数都设置为1来实现。 ##### newCachedThreadPool可缓存线程池 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。但是由于最大线程数设置的是Integer.MAX_VALUE,存在内存溢出的风险。 +一句话总结就是:**最大线程数无限大,线程超时被回收** + ```java public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, @@ -1305,9 +1562,9 @@ public static ExecutorService newCachedThreadPool() { 1. 提交任务进线程池。 2. 因为**corePoolSize**为0的关系,不创建核心线程,线程池最大为Integer.MAX_VALUE。 -3. 尝试将任务添加到**SynchronousQueue**队列。 -4. 如果SynchronousQueue入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue拉取任务并在当前线程执行。 -5. 如果SynchronousQueue已有任务在等待,入列操作将会阻塞。 +3. 尝试将任务添加到**SynchronousQueue**队列。(需要注意的是**SynchronousQueue**本身不存储任务,只是将添加任务的线程加入一个栈中,进行阻塞等待,然后线程池中的线程空闲时,会从栈中取出线程,取出线程携带的任务,进行执行。) +4. 如果**SynchronousQueue**入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue拉取任务并在当前线程执行。 +5. 如果**SynchronousQueue**已有任务在等待,入列操作将会阻塞。 当需要执行很多**短时间**的任务时,CacheThreadPool的线程复用率比较高, 会显著的**提高性能**。而且线程60s后会回收,意味着即使没有任务进来,CacheThreadPool并不会占用很多资源。 @@ -1315,6 +1572,8 @@ public static ExecutorService newCachedThreadPool() { 创建一个定时执行的线程池,主要是通过DelayedWorkQueue来实现(该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素)。支持定时及周期性任务执行。但是由于最大线程数设置的是Integer.MAX_VALUE,存在内存溢出的风险。 +一句话总结就是:线程数无限大,定时执行。 + ```java public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { @@ -1323,13 +1582,23 @@ public ScheduledThreadPoolExecutor(int corePoolSize, } ``` +##### 为什么不建议大家使用Executors的四种线程池呢? + +主要是newFixedThreadPool和newSingleThreadExecutor的等待队列是LinkedBlockingQueue,长度是Integer.MAX_VALUE,,可以认为是无限大的,如果创建的任务特别多,可能会造成内存溢出。而newCachedThreadPool和newScheduledThreadPool的最大线程数是Integer.MAX_VALUE,如果创建的任务过多,可能会导致创建的线程过多,从而导致内存溢出。 + +扩展资料: + +[Java线程池实现原理及其在美团业务中的实践](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html) + +[SynchronousQueue实现原理](https://zhuanlan.zhihu.com/p/29227508) + ### 线程池有哪些状态? 线程池生命周期: - **RUNNING**:表示线程池处于运行状态,这时候的线程池可以接受任务和处理任务。值是-1, -- **SHUTDOWN **:表示线程池不接受新任务,但仍然可以处理队列中的任务,二进制值是0。调用showdown()方法会进入到SHUTDOWN状态。 +- **SHUTDOWN**:表示线程池不接受新任务,但仍然可以处理队列中的任务,二进制值是0。调用showdown()方法会进入到SHUTDOWN状态。 - **STOP**:表示线程池不接受新任务,也不处理队列中的任务,同时中断正在执行任务的线程,值是1。调用showdownNow()方法会进入到STOP状态。 @@ -1339,6 +1608,233 @@ public ScheduledThreadPoolExecutor(int corePoolSize, ![img](../static/640-20200728210136673.jpeg) -参考链接: +### 怎么根据业务场景确定线程池的参数corePoolSize和maximumPoolSize? + +#### 方法一 计算密集型任务 +因为是计算密集型任务,可以理解为每个任务在执行期间基本没有IO操作,全部都在CPU时间片中执行。所以可以理解为CPU就是满载的,CPU利用率就是100%,其实线程数等于CPU数就可以的,但是由于需要考虑到计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,此时应该需要有一个“额外”的空闲线程来获得时间片,然后执行,可以确保在这种情况下CPU周期不会中断工作,充分利用CPU。 +```java +最佳线程数=CPU的数量+1 +``` +#### 方法二 IO密集型任务 +这种任务在执行时,需要进行一些IO操作,所以为了充分利用CPU,应该在线程进行IO操作时,就让出时间片,CPU进行上下文切换,执行其他线程的任务,保证CPU利用率尽可能达到100%。 + +如果任务有50%的时间需要CPU执行状态,其他时间进行IO操作,则程序所需线程数为CPU数量的1除以0.5,也就是2倍。如果任务有20%的时时间需要CPU执行,其他时间需要进行IO操作,最佳线程数也就是1除以0.2,也就是CPU数的5倍。 +所以公式为 + +```java +最佳线程数 = CPU数量/(每个任务中需要CPU执行的时间的比例) += CPU数量/(CPU运行时间/任务执行总时间)=CPU数量/(CPU运行时间/(CPU运行时间+IO操作时间)) +所以最终公式为 +最佳线程数/CPU数量 = CPU运行时间/(CPU运行时间+IO操作时间) +``` +##### 不足 +但是在实际线上运行的环境中,每个任务执行的时间是各不相同的,而且我们其实是不太方便去监测每个任务执行时需要的CPU执行时间,IO操作时间的,所以这种方法只是一种理论。 + +#### 方法三 动态化线程池 +这种其实是美团他们做的一个线程池监测平台,主要把任务分成两种, +##### 追求响应时间的任务 +一种是追求响应时间的任务,例如使用线程池对发起多个网络请求,然后对结果进行计算。 这种任务的最大线程数需要设置大一点,然后队列使用同步队列,队列中不缓存任务,任务来了就会被执行。判断线程池资源不够用时,一般是发现活跃线程数/最大线程数>阀值(默认是0.8)时,或者是线程池抛出的RejectedExecut异常次数达到阀值,就会进行告警。然后程序员收到告警后,动态发送修改核心线程数,最大线程数,队列相关的指令,服务器进行动态修改。 + +##### 追求高吞吐量的任务 + +假设说需要定期自动化生成一些报表,不需要考虑响应时间,只是希望如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。 +这种就是使用有界队列,对任务进行缓存,然后线程进行并发执行。判断线程池资源不够用时,一般是发现等待队列中的任务数量/等待队列的长度>阀值(默认是0.8)时,或者是线程池抛出的RejectedExecut异常次数达到阀值,就会进行告警。然后程序员收到告警后,动态发送修改核心线程数,最大线程数,队列相关的指令,服务器进行动态修改。 + +ThreadPoolExecutor提供了如下几个public的setter方法 + +![image-20210119104549770](../static/image-20210119104549770.png) + +调用corePoolSize方法之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。(总得来说就是,多退少补的策略) + +**对于新corePoolSize<当前工作线程数的情况:** + +说明有多余的worker线程,此时会向当前idle状态的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收。 + +**对于新corePoolSize>当前工作线程数且队列中有任务的情况:** + +如果当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务。 + +setCorePoolSize的方法的执行流程入下图所示: + + + +![图20 setCorePoolSize方法执行流程](../static/9379fe1666818237f842138812bf63bd85645.png) + +扩展资料: +[Java并发(八)计算线程池最佳线程数](https://www.cnblogs.com/jpfss/p/11016169.html) + +### ThreadLocal是什么?怎么避免内存泄露? + +从字面意思上,ThreadLocal会被理解为线程本地存储,就是对于代码中的一个变量,每个线程拥有这个变量的一个副本,访问和修改它时都是对副本进行操作。 +##### 使用场景: +ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。(例如:方法直接调用时传递的变量过多,为了代码简洁性,可以使用ThreadLocal,在前一个方法中,将变量进行存储,后一个方法中取,进行使用。) +```java +public class A { + // 每个线程本地副本初始化 + private static ThreadLocal threadLocal = new ThreadLocal <>(). withInitial (() -> new UserData ()); + public static void setUser (UserLogin user){ + if (user == null ) + return ; + UserData userData = threadLocal.get(); + userData. setUserLogin (user); + } + public static UserLogin getUser (){ + return threadLocal.get(). getUserLogin (); + } +} +``` + +##### 实现原理 + +就是每个Thread有一个ThreadLocalMap,类似于HashMap,当调用ThreadLocal#set()方法进行存值时,实际上是先获取到当前的线程,然后获取线程的map,是一个ThreadLocalMap类型,然后会在这个map中添加一个新的键值对,key就是我们ThreadLocal变量的地址,value就是我们存的值。ThreadLocalMap与HashMap不同的时,解决HashMap使用的是**开放定址法**,也就是当发现hashCode计算得到数组下标已经存储了元素后,会继续往后找,直到找到一个空的数组下标,存储键值对。 + +```java +//ThreadLocal实例的赋值方法 +public void set(T value) { + //获取当前线程 + Thread t = Thread.currentThread(); + //获取线程对应的Map + ThreadLocalMap map = getMap(t); + //将值存入线程特有的Map中 + if (map != null) + //key为this就是当前ThreadLocal引用变量的地址 + //value就是我们要存储的值 + map.set(this, value); + else + createMap(t, value); +} +ThreadLocalMap getMap(Thread t) { + //线程的threadLocals实例变量就是Map + return t.threadLocals; +} +``` + +##### ThreadLocal中的Entry的key使用了弱引用,为什么使用弱引用? + +![thread](../static/thread.png) + +首先在上面类A的代码中,类A中有一个ThreadLocal类型的变量 + +它们的引用链如下: + +```java +ThreadLocal变量所在的类的实例(代码中A的实例)->ThreadLocal +执行代码的线程->线程独有的ThreadLocalMap->引用的key就是ThreadLocal +``` + +可以看到ThreadLocal变量不仅被所在的类A的实例所引用,还被执行的线程所引用, + +1.如果使用强引用,也就是线程对ThreadLocal变量是强引用,那么即便实例A被回收了,只要线程还没有被回收,线程的ThreadLocalMap还会引用这个key(也就是这个ThreadLocal遍历),导致这个key 没有被回收,造成内存泄露。 + +2.如果使用弱引用,不会影响key的回收,也就是不会影响引用了ThreadLocal的实例对象的回收。 + +但是即便使用弱引用,ThreadLocalMap对value的引用是强引用(一边value是局部变量,也不能用弱引用,那样在用到的时候就会被),但是value依然不会被回收,会造成内存泄露。 + +通常来说,value回收的时机有两个: + +1.我们在用完ThreadLocal后,应该遵循规范手动调用ThreadLocal#remove()对键值对value释放,这样可以使value被回收。 + +2.此线程在其他对象中使用ThreadLocal对线程ThreadLocalMap进行set()和get()时,由于需要进行开放定址法进行探测,会对沿途过期的键值对(就是key为null的键值对)进行清除。以及set()方法触发的cleanSomeSlots()方法对过期键值对进行清除。 + +[《一篇文章,从源码深入详解ThreadLocal内存泄漏问题》](https://www.jianshu.com/p/dde92ec37bd1) + +### Random类取随机数的原理是什么? + +首先在初始化Random实例的时候就会根据当前的时间戳生成一个种子数seed。 + +```java +public Random() { + this(seedUniquifier() ^ System.nanoTime()); + } + + private static long seedUniquifier() { + // L'Ecuyer, "Tables of Linear Congruential Generators of + // Different Sizes and Good Lattice Structure", 1999 + for (;;) { + long current = seedUniquifier.get(); + long next = current * 181783497276652981L; + if (seedUniquifier.compareAndSet(current, next)) + return next; + } + } +``` + +然后每次取随机数时是拿seed乘以一个固定值multiplier,作为随机数。 + +```java + protected int next(int bits) { + long oldseed, nextseed; + AtomicLong seed = this.seed; + do { + oldseed = seed.get(); + nextseed = (oldseed * multiplier + addend) & mask; + } while (!seed.compareAndSet(oldseed, nextseed)); + return (int)(nextseed >>> (48 - bits)); + } +``` + +但是这样的话,多线程并发使用Math.random取随机数时,同一个时间点取到的随机数一样的概率会比较大。所以可以使用ThreadLocalRandom.current().nextInt()方法去取随机数。每个线程第一次调用ThreadLocalRandomd的current()方法时,会为这个线程生成一个线程独立的种子数seed,这样多线程并发读取随机数时,可以保证取到的随机数都是不一样的。 + +```java +public static ThreadLocalRandom current() { + //判断这个线程是否生成种子 + if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) + localInit(); + return instance; + } + +//为这个线程生成一个种子seed,并且将种子seed,和线程已生成种子的标志 存储到Unsafe类中 +static final void localInit() { + int p = probeGenerator.addAndGet(PROBE_INCREMENT); + int probe = (p == 0) ? 1 : p; // skip 0 + long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); + Thread t = Thread.currentThread(); + UNSAFE.putLong(t, SEED, seed); + UNSAFE.putInt(t, PROBE, probe); + } + +//获取随机数,根据当前种子计算随机数 +public int nextInt(int origin, int bound) { + if (origin >= bound) + throw new IllegalArgumentException(BadRange); + return internalNextInt(origin, bound); + } + +final int internalNextInt(int origin, int bound) { + int r = mix32(nextSeed()); + if (origin < bound) { + int n = bound - origin, m = n - 1; + if ((n & m) == 0) + r = (r & m) + origin; + else if (n > 0) { + for (int u = r >>> 1; + u + m - (r = u % n) < 0; + u = mix32(nextSeed()) >>> 1) + ; + r += origin; + } + else { + while (r < origin || r >= bound) + r = mix32(nextSeed()); + } + } + return r; + } +``` + +### 僵尸进程,孤儿进程,守护进程是什么? + +僵尸进程:通常来说,使用fork()系统调用从一个父进程创建出一个子进程,子进程退出,是需要父进程调用wait()或者是waitpid()函数来回收子进程的资源,如果父进程没有调用,子进程的信息就会一直在内存中,而不会被回收,变成僵尸进程。 + +孤儿进程:就是父进程先退出了,它的子进程会被init进程接管,由它来收集子进程的状态。(init进程是内核启动时,创建出来的进程,是一个以root身份运行的普通用户进程,是永远不会停止的。) + +守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免将在执行的过程中的信息打印在终端上,并且进程也不会被任何终端所产生的终端信息所打断。 + +### BlockingQueue的原理是怎么样的? + +https://www.cnblogs.com/tjudzj/p/4454490.html + +### 进程间通信的方式 + +https://network.51cto.com/art/201911/606827.htm?mobile -https://mp.weixin.qq.com/s?src=11×tamp=1595941110&ver=2487&signature=i8CGBfTlDi4SaG5SSOWYJo-Sgb*bauWAv8MEMYqWQMy4lFBQwjTY4*99R2-8PhC4WtBc4uBy-m3IveQ9a0RlQn53unVD6Xalfl2r30*IbwAdK7CPlbW6-8icKhG4OjKE&new=1 diff --git a/docs/Kafka.md b/docs/Kafka.md index c344ce1..f3a4529 100644 --- a/docs/Kafka.md +++ b/docs/Kafka.md @@ -6,13 +6,21 @@ 分发机制: -* 发的消息指定了分区就发到特定分区下 +* 1.发的消息指定了分区就发到特定分区下 -* 指定了key,就根据murmur2 哈希算法对key计算得到一个哈希值,将哈希值与分区数量取余,得到分区。 +* 2.指定了key,就根据murmur2 哈希算法对key计算得到一个哈希值,将哈希值与分区数量取余,得到分区。 -* 没有指定分区,也没有指定key,那么就根据一个自增计数与分区数取余得到分区,这样可以让消息分发在每个分区更加均匀。 +* 3.没有指定分区,也没有指定key,那么就根据一个自增计数与分区数取余得到分区,这样可以让消息分发在每个分区更加均匀。 -3.每个分区就是一个目录,目录名是topic+分区编号在收到消息后会将消息写入到日志文件中,如果一个分区的消息都有存放在一个日志文件中,那么文件会比较大,查询时会比较慢,而且也不便于之后删除旧的消息。所以每个分区对应一个segement,每个segement的名称是上一个segement最后一条消息的offset,一个segement有两个文件,一个是.index文件,记录了每个offset的消息数据在log文件中的偏移量用于查询特定offset的消息。一个是.log文件,实际存储每个消息数据,每条消息数据大小不一,每条消息数据包含offset,消息体大小,消息体等等内容。查的时候根据offset先去index文件找到偏移量,然后去log文件中读。 +3.每个分区就是一个目录,目录名是topic+分区编号,在收到消息后会将消息写入到日志文件中,如果一个分区的消息都有存放在一个日志文件中,那么文件会比较大,查询时会比较慢,而且也不便于之后删除旧的消息。所以每个分区对应多个大小相等的segment文件,每个segment的名称是上一个segment最后一条消息的offset,一个segment有两个文件,一个是.index文件,记录了消息的offset及这条消息数据在log文件中的偏移量。一个是.log文件,实际存储每个消息数据,每条消息数据大小不一,每条消息数据包含offset,消息体大小,消息体等等内容。查的时候根据offset先去index文件找到偏移量,然后去log文件中读。 + +(具体的segment切分有很多个触发条件: + +当log文件>log.segment.bytes时切分,默认是1G。 + +或者是segment文件中最早的消息距离现在的时间>log.roll.ms配置的时间,默认是7天。 + +或者是索引文件index>log.index.size.max.bytes的大小,默认是10M。) 4.分区leader将消息存储到日志文件中后还不能算是写成功,会把消息同步给所有follower,当follower同步好消息之后就会给leader发ack,leader收到所有follower返回的ack之后,这条才算是写成功,然后才会给生产者返回写成功。(依据ACK配置来决定多少follower同步成功才算生产者发送消息成功) @@ -22,7 +30,7 @@ 1.replication.factor>=2,也就是一个分区至少会有两个副本。 -2.min.insync.replicas默认是1,leader至少要有一个follow跟自己保持联系没有掉线。 +2.min.insync.replicas默认是1,leader至少要有一个follow跟自己保持联系没有掉线。(这个配置只有在ack为all或者-1时有用,也就是ack为all也只是要求生产者发送的消息,被leader以及ISR集合里面的从节点接收到,就算所有节点都接收到了。) 3.一般设置了ack=all就不会丢数据。因为会保证所有的follower都收到消息,才算broker接收成功,默认ack=1。 @@ -38,8 +46,6 @@ **-1** 也就是all,producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。 - - ### 怎么防止Kafka 丢数据? 这块比较常见的一个场景,就是 `Kafka` 某个 `broker` 宕机,然后重新选举 `partition` 的 `leader` 。大家想想,要是此时其他的 `follower` 刚好还有些数据没有同步,结果此时 `leader` 挂了,然后选举某个 `follower` 成 `leader` 之后,不就少了一些数据?这就丢了一些数据啊。 @@ -49,23 +55,17 @@ - 给 `topic` 设置 `replication.factor` 参数:这个值必须大于 1,要求每个 `partition` 必须有 **至少** 2 个副本。 - 在 `Kafka` 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1,这个是 **要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系**,没掉队,这样才能确保 `leader` 挂了还有一个 `follower` 吧。 - 在 `producer` 端设置 `acks=all`:这个是要求每条数据,**必须是写入所有 replica 之后,才能认为是写成功了**。 -- 在 `producer` 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是要求 **一旦写入失败,就无限重试**,卡在这里了。 +- 在 `producer` 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是要求**一旦写入失败,就无限重试**,卡在这里了。 -这样配置之后,至少在 Kafka `broker` 端就可以保证在 `leader` 所在 `broker` 发生故障,进行 `leader` 切换时,数据不会丢失。 +这样配置之后,至少在Kafka `broker` 端就可以保证在`leader` 所在 `broker` 发生故障,进行`leader` 切换时,数据不会丢失。 ### 生产者会不会弄丢数据? -如果按照上述的思路设置了 `acks=all`,一定不会丢,要求是,你的 `leader` 接收到消息,所有的 `follower` 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者可以自动不断的重试,重试无限次。 +如果按照上述的思路设置了`acks=all`,一定不会丢,要求是,你的 `leader` 接收到消息,所有的`follower` 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者可以自动不断的重试,重试无限次。 #### 怎么实现 Exactly-Once? -就是0.11版本之前,kafka只能支持At most once和At least once - -在0.11之后增加了对幂等的支持,就是在建立连接时,给每个生产者初始化时生成一个pid,然后这个生产者发的消息都会带有一个pid,sequenceNumber,每个分区也会存这个生产者当前最大的sequencenumber,如果这个消息的sequenceNumber比缓存的sequenceNumber大才处理。但是如果生产者挂掉后,会重新生成生产者id也会出现有数据重复的现象;所以幂等性解决在单次会话的单个分区的数据重复,但是在分区间或者跨会话的是数据重复的是无法解决的。所以可以消费者去做去重。 - -#### 幂等性发送 - -上文提到,实现`Exactly Once`的一种方法是让下游系统具有幂等处理特性,而在Kafka Stream中,Kafka Producer本身就是“下游”系统,因此如果能让Producer具有幂等处理特性,那就可以让Kafka Stream在一定程度上支持`Exactly once`语义。 +##### 生产端幂等性发送 为了实现Producer的幂等语义,Kafka引入了`Producer ID`(即`PID`)和`Sequence Number`。每个新的Producer在初始化的时候会被分配一个唯一的PID,该PID对用户完全透明而不会暴露给用户。 @@ -79,7 +79,14 @@ 上述设计解决了0.11.0.0之前版本中的两个问题: - Broker保存消息后,发送ACK前宕机,Producer认为消息未发送成功并重试,造成数据重复 -- 前一条消息发送失败,后一条消息发送成功,前一条消息重试后成功,造成数据乱序 + +- 前一条消息发送失败,后一条消息发送成功,前一条消息重试后成功,造成数据乱序。 + + http://www.jasongj.com/kafka/transaction/ + +##### 消费端幂等性 + +只能自己从业务层面保证重复消费的幂等性,例如引入版本号机制。 #### 事务性保证 @@ -129,5 +136,118 @@ #### ISR是什么? -ISR(in-sync replica) 就是 Kafka 为某个分区维护的一组同步集合,即每个分区都有自己的一个 ISR 集合,处于 ISR 集合中的副本,意味着 follower 副本与 leader 副本保持同步状态,只有处于 ISR 集合中的副本才有资格被选举为 leader。follower从leader同步数据有一些延迟(延迟时间replica.lag.time.max.ms),一旦超过延迟时间,就会把这个这个follower从ISR列表中移除。被移除的followe会从leader复制数据进行追赶,一旦追赶上又可以重新进入ISR列表。一条 Kafka 消息,只有被 ISR 中的副本都接收到,才被视为“已同步”状态。这跟 zk 的同步机制不一样,zk 只需要超过半数节点写入,就可被视为已写入成功。 +ISR(in-sync replica) 就是 Kafka 为某个分区维护的一组同步集合,即每个分区都有自己的一个 ISR 集合,就是从分区的从节点中找出一些节点加入到ISR集合(min.insync.replicas这个参数设定ISR中的最小副本数是多少,默认值为1)。处于 ISR 集合中的副本,意味着 follower 副本与 leader 副本保持同步状态,只有处于 ISR 集合中的副本才有资格被选举为 leader。follower从leader同步数据有一些延迟(延迟时间replica.lag.time.max.ms),一旦超过延迟时间,就会把这个这个follower从ISR列表中移除。被移除的followe会从leader复制数据进行追赶,一旦追赶上又可以重新进入ISR列表。一条 Kafka 消息,只有被 ISR 中的副本都接收到,才被视为“已同步”状态。这跟 zk 的同步机制不一样,zk 只需要超过半数节点写入,就可被视为已写入成功。 + +### 什么是零拷贝技术? +传统的IO接口像read和write系统调用,在执行过程中都是涉及到数据拷贝操作的,比如调用read()接口去读取一个文件时,首先需要将CPU由用户切换成内核态,然后把文件从磁盘读取到 + +#### read()和write() + +![图片](../static/640) + +**read()系统调用的步骤:** +1.会涉及到到一次用户态到内核态的切换,然后会发出 sys_read()系统调用,从文件读取数据。(一次上下文切换) +2.磁盘控制器会使用DMA技术将磁盘文件拷贝到内核内存空间的缓冲区。(一次DMA拷贝) +3.CPU会将数据从内核内存空间的缓冲区拷贝到用户进程内存空间的缓冲区。(一次CPU拷贝) +4.然后read()系统调用返回后,会进行内核态往用户态的切换,这样用户程序进程就可以修改数据了。(一次上下文切换) + +**write()系统调用的步骤:** +1.首先会涉及CPU从用户态切换到内核态,然后会将数据从用户程序的内存空间拷贝到内核内存空间中的Socket缓冲区。(一次上下文切换,一次CPU拷贝) +2.网卡会使用DMA技术,将数据从内核内存空间中的缓冲区拷贝到网卡。(一次DMA拷贝) +3.write()调用完成后会从内核态切换到用户态。(一次上下文切换) + +#### 2.MMAP和write() + +![图片](../static/640-20210326173442637) + +##### mmap + +1.CPU从用户态切换到内核态,磁盘控制器使用DMA技术将数据从磁盘拷贝到内核的内存空间。不会将数据拷贝到用户程序的内存空间,而是将一块物理内存让用户进程的空间与内核空间进行共享,将内核中的这部分内存空间映射到用户进程的内存空间,从而让用户进程可以直接访问这部分内存。(一次上下文切换,一次DMA拷贝) + +2.mmap调用完毕后,CPU会从内核态切换到用户态。(一次上下文切换) + +mmap相比于read()系统调用还是会有2次上下文切换,但是可以减少一次CPU拷贝,因为数据是存在内核的内存空间中。 + +##### write + +1.首先CPU从用户态切换到内核态,然后把数据从内核的内存空间拷贝到内核中Socket缓冲区。(一次上下文切换,一次CPU拷贝) + +2.网卡使用DMA技术,将数据从Socket缓冲区拷贝到网卡。发送完毕后,从内核态切换为用户态。(一次上下文切换,一次DMA拷贝) + +https://mp.weixin.qq.com/s/xDZ9NnyUZSoR9npuMLdpWA +https://blog.csdn.net/choumu8867/article/details/100658332 + +#### sendfile + +这种方式只能用于发送文件,不能修改文件,在Kakfa发送消息给消费者时有用到。 + +![图片](../static/640-20210326191349615) + +读取时: + +1.首先CPU从用户态切换成内核态,然后磁盘控制器使用DMA技术将文件从磁盘拷贝到内核空间的缓冲区中。 + +(一次上下文切换,一次DMA拷贝) + +发送时: + +2.早期的版本是将数据从内核空间中的缓存区拷贝到内核空间的Socket缓冲区,在Linux 2.4以后,是只需要将数据在内核空间的文件数据缓存中的位置和偏移量写入到Socket缓存中,然后网卡直接从Socket缓存中读取文件的位置和偏移量,使用DMA技术拷贝到网卡。发送完毕后,从内核态切换为用户态。 + +(一次上下文切换,一次DMA拷贝。) + +##### 总结: + +传统read()和write()方案:数据拷贝了4次,CPU上下文切换了很多次 + +mmap和write()方案:数据拷贝了3次,会减少一次CPU拷贝,上下文切换了4次。(可以减少1次CPU拷贝) + +sendfile方案:数据拷贝了2次,上下文切换了2次。但是用户进程不能修改数据。(可以减少2次CPU拷贝,至少2次上下文切换) + +### Kafka刷盘时机是怎么样的? +log.flush.interval.messages 最大刷盘消息数量 +log.flush.interval.interval.ms 最大刷盘时间间隔 +log.flush.scheduler.interval.ms 定期刷盘间隔 +可以通过设置 最大刷盘消息数量 和 最大刷盘时间间隔 来控制fsync系统调用的时间,但是Kafka不推荐去设置这些参数,希望让操作系统来决定刷盘的时机,这样可以支持更高的吞吐量。而且Kafka保证可用性是通过多副本来实现的,一个机器挂掉了就会选举副本作为leader。 +### Kafka什么时候进行rebalance? +1.topic下分区的数量增加了或者减少了。(这个一般是我们手动触发的) + +2.消费者的数量发生了改变,例如新增加了消费者或者有消费者挂掉了。 +Kafka有一个session.timeout.ms,最大会话超时时间,最长是10s。就是如果broker与消费者之间的心跳包超过10s还没有收到回应,就会认为消费者掉线了。以及还有一个max.poll.interval.ms参数,消费者两次去broker拉取消息的间隔,默认是5分钟。如果消费者两次拉取消息的间隔超过了5分钟,就会认为消费者掉线了。 + +一旦发生rebalance了,有可能会导致重复消费的问题,就是消费者A拉取了100条消息,消费时间超过了5分钟,被broker认定下线,就会进行rebalance,把这个分区分配给其他消费者消费,其他消费者就会进行重复消费。 + +怎么解决rebalance带来的重复消费问题呢? + +1.可以减少每批消息的处理时间,让每条消息的处理时间减少,或者是修改max.poll.records,减小每次拉取消息的数量。 + +2.可以自行在MySQL或者Redis里面存储每个分区消费的offset,然后消费者去一个新的分区拉取消息时先去读取上次消费的offset。 + +3.为消息分配一个唯一的消息id,通过消息id来判定是否重复消费了。 + +##### kafka 1.1的优化 + +新版本新增了**group.initial.rebalance.delay.ms**参数。空消费组接受到成员加入请求时,不立即转化到PreparingRebalance状态来开启reblance。当时间超过**group.initial.rebalance.delay.ms**后,再把group状态改为PreparingRebalance(开启reblance),这样可以避免服务启动时,consumer陆续加入引起的频繁Rebalance。 + +##### Kafka2.3对reblance的优化 + +但对于运行过程中,consumer超时或重启引起的reblance则无法避免,其中一个原因就是,consumer重启后,它的身份标识会变。简单说就是Kafka不确认新加入的consumer是否是之前挂掉的那个。 + +在Kafka2.0中引入了静态成员ID,使得consumer重新加入时,可以保持旧的标识,这样Kafka就知道之前挂掉的consumer又恢复了,从而不需要Reblance。这样做的好处有两个: + +1. 降低了Kafka Reblance的频率 +2. 即使发生Reblance,Kafka尽量让其他consumer保持原有的partition,减少了重分配引来的耗时、幂等等问题 + +https://blog.csdn.net/weixin_37968613/article/details/104607012 + +https://blog.csdn.net/z69183787/article/details/105138782 + +https://zhuanlan.zhihu.com/p/87577979 + +https://www.cnblogs.com/runnerjack/p/12108132.html + +### kafka的选举机制 + +https://blog.csdn.net/qq_37142346/article/details/91349100 + +https://honeypps.com/mq/kafka-basic-knowledge-of-selection/ diff --git a/docs/LeetCode.md b/docs/LeetCode.md index 3ef2b64..5b5c42a 100644 --- a/docs/LeetCode.md +++ b/docs/LeetCode.md @@ -1,9 +1,57 @@ -## LeetCode 热门100题题解 - -#### [1.两数之和](#两数之和) +## LeetCode 热门100题-题解(上) + +##### 主要是记录自己刷题的过程,也方便自己复习 + +##### [第1题-两数之和](#第1题-两数之和) +##### [第206题-反转链表](#第206题-反转链表) + +##### [第2题-两数相加](#第2题-两数相加) +##### [第3题-无重复字符的最长子串](#第3题-无重复字符的最长子串) +##### [第20题-有效的括号](#第20题-有效的括号) +##### [第5题-最长回文子串](#第5题-最长回文子串) +##### [第19题-删除链表的倒数第N个节点](#第19题-删除链表的倒数第N个节点) +##### [第121题-买卖股票的最佳时机](#第121题-买卖股票的最佳时机) +##### [第70题-爬楼梯](#第70题-爬楼梯) +##### [第53题-最大子序和](#第53题-最大子序和) +##### [第21题-合并两个有序链表](#第21题-合并两个有序链表) +##### [第283题-移动零](#第283题-移动零) +##### [第34题-在排序数组中查找元素的第一个和最后一个位置](#第34题-在排序数组中查找元素的第一个和最后一个位置) +##### [第11题-盛最多水的容器](#第11题-盛最多水的容器) +##### [第17题-电话号码的字母组合](#第17题-电话号码的字母组合) +##### [第15题-三数之和](#第15题-三数之和) +##### [第141题-环形链表](#第141题-环形链表) +##### [第104题-二叉树的最大深度](#第104题-二叉树的最大深度) +##### [第22题-括号生成](#第22题-括号生成) +##### [第102题-二叉树的层序遍历](#第102题-二叉树的层序遍历) +##### [第198题-打家劫舍](#第198题-打家劫舍) +##### [第46题-全排列](#第46题-全排列) +##### [第55题-跳跃游戏](#第55题-跳跃游戏) +##### [第62题-不同路径](#第62题-不同路径) +##### [第56题-合并区间](#第56题-合并区间) +##### [第169题-多数元素](#第169题-多数元素) +##### [第101题-对称二叉树](#第101题-对称二叉树) +##### [第136题-只出现一次的数字](#第136题-只出现一次的数字) +##### [第23题-合并K个升序链表](#第23题-合并K个升序链表) +##### [第94题-二叉树的中序遍历](#第94题-二叉树的中序遍历) +##### [第64题-最小路径和](#第64题-最小路径和) +##### [第215题- 数组中的第K个最大元素](#第215题-数组中的第K个最大元素) + +##### [第234题- 回文链表](#第234题-回文链表) +##### [第200题-岛屿数量](#第200题-岛屿数量) +##### [第48题-旋转图像](#第48题-旋转图像) +##### [第98题-验证二叉搜索树](#第98题-验证二叉搜索树) +##### [第78题-子集](#第78题-子集) +##### [第75题-颜色分类](#第75题-颜色分类) +##### [第39题-组合总和](#第39题-组合总和) +##### [第226题-翻转二叉树](#第226题-翻转二叉树) +##### [第31题-下一个排列](#第31题-下一个排列) +##### [第322题-零钱兑换](#第322题-零钱兑换) +##### [第300题-最长递增子序列](#第300题-最长递增子序列) + +### 第1题-两数之和 #### 题目描述 - +题目详情:https://leetcode-cn.com/problems/two-sum/ 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 @@ -40,7 +88,8 @@ class Solution { } ``` -### [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/) +### 第206题-反转链表 +题目详情:https://leetcode-cn.com/problems/reverse-linked-list/ #### 题目描述: @@ -59,22 +108,17 @@ class Solution { #### 循环的解法: ```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode(int x) { val = x; } - * } - */ class Solution { public ListNode reverseList(ListNode head) { if(head==null||head.next==null){return head;} ListNode preNode = head; ListNode currentNode = head.next; + //将原来头结点的next指针设置为null head.next = null; while(currentNode!=null) { + //保存指向下一个节点的指针 ListNode saveNode = currentNode.next; + //将当前节点的next指向前一个节点 currentNode.next = preNode; preNode = currentNode; currentNode = saveNode; @@ -104,8 +148,8 @@ public static ListNode reverseList(ListNode node) { return newHead; } ``` - -### [3.无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) +### 第3题-无重复字符的最长子串 +题目详情:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/ #### 题目描述: @@ -160,12 +204,12 @@ public int lengthOfLongestSubstring(String s) { return max; } ``` - -### [2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/) +### 第2题-两数相加 +题目详情:https://leetcode-cn.com/problems/add-two-numbers/ #### 题目详情: -给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。 +给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照逆序的方式存储的,并且它们的每个节点只能存储 一位 数字。 如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 @@ -209,7 +253,8 @@ public ListNode addTwoNumbers(ListNode l1, ListNode l2) { if(carryFlag == 0) {//没有进位,直接将剩余链表接过来 currentNode.next = l1.next == null ? l2.next : l1.next; break; - } else {//有进位就建新节点 + } else { + //两个链表都到末尾了,并且有进位就建新节点 if(l1.next == null && l2.next == null) { currentNode.next = new ListNode(1); break; @@ -229,7 +274,8 @@ public ListNode addTwoNumbers(ListNode l1, ListNode l2) { } ``` -### [20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses/) +### 第20题-有效的括号 +题目详情:https://leetcode-cn.com/problems/valid-parentheses/ #### 题目描述 @@ -266,7 +312,11 @@ public ListNode addTwoNumbers(ListNode l1, ListNode l2) { #### 解题思路 -就是遍历字符串,字符属于左括号就添加到栈中,属于右括号就判断是否属于与栈顶元素对应,是的话可以将栈顶出栈,不是的话就说明不匹配,返回 false。遍历完成需要判断栈的长度是否为0,不为0代表还存在没有匹配上的左括号,不满足要求。 +就是遍历字符串, + +字符属于左括号就添加到栈中, + +字符属于右括号就判断是否属于与栈顶元素对应,是的话可以将栈顶出栈,不是的话就说明不匹配,返回 false。遍历完成需要判断栈的长度是否为0,不为0代表还存在没有匹配上的左括号,不满足要求。 ```java class Solution { @@ -295,8 +345,8 @@ class Solution { ``` - -### [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) +### 第5题-最长回文子串 +题目详情:https://leetcode-cn.com/problems/longest-palindromic-substring/ #### 题目详情 @@ -336,11 +386,12 @@ class Solution { char[] array = s.toCharArray(); String maxString = String.valueOf(array[0]); for(int i = 1;i=输入值的元素下标),所以本题可以通过找出target-0.5查找左边界,得到target的最左边的值,同时通过找出target+0.5查找出第一个大于目标值的元素下标,然后-1得到taget最右边的值。(当然也需要考虑taget不存在的情况) + +```java +public int[] searchRange(int[] nums, int target) { + int[] array = new int[2]; + array[0]=-1; + array[1]=-1; + if (nums==null||nums.length==0) { + return array; + } + int left = findLeftBound(nums,target-0.5); + int right = findLeftBound(nums,target+0.5); + if (left==-1) {//nums不存在这个target值 + return array; + } + if (right==-1) {//taget值可能是数组最后一个元素 + right = nums.length-1; + } else {//right是第一个大于target的值,减一得到target的右边界 + right = right -1; + } + //如果相等,那么返回下标 + if (nums[left] == target && nums[right] == target) { + array[0] = left; + array[1] = right; + return array; + } + return array; +} +//查找target值的左边界(也就是第一个>=target的元素下标) +int findLeftBound(int[] nums,double target) { + int left = 0; + int right = nums.length-1; + while (left<=right) { + int mid = left+(right-left)/2; + if (nums[mid] == target) { + right=mid-1; + } else if (nums[mid]>target) { + right = mid-1; + } else if (nums[mid]=nums.length) { + return -1; + } else { + return left; + } +} +``` +### 第11题-盛最多水的容器 + +给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + +说明:你不能倾斜容器。 +示例 1: + +![img](../static/question_11.jpg)输入:[1,8,6,2,5,4,8,3,7] +输出:49 +解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 + +##### 解题思路 + +我们取两个指针从数组的两端往内遍历,i从数组头部出发,j从数组尾部出发。每次计算最大面积,并且移动高度较小的那个端点。 + +对于两个端点i和j来说,容纳水的面积是是等于(j-i)*min(height[i],height[j]),假设height[i]是两者之间较小的那一个,那么面积等于(j-i)*height[i],假设i不移动,j向左移动,这样宽度j-i会减少,而height[j]即便变大也不会使得面积变大,因为面积是由宽度乘以两者中较小的高度决定的,所以此时的面积对于i这个端点来说,已经是最大的面积,我们可以右移高度较小的端点i。 + +```java +public int maxArea(int[] height) { + if (height==null||height.length==0) {return 0;} + int left =0; + int right = height.length-1; + int maxArea = 0; + while (left area ? maxArea : area; + left++; + } else { + int area = height[right] * (right-left); + maxArea = maxArea > area ? maxArea : area; + right--; + } + } + return maxArea; + } +``` +### 第17题-电话号码的字母组合 +给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 +给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 + +示例: + +输入:"23" +输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. + +##### 解题思路: + "" + "a" "b" "c" + "d" "e" "f" "d" "e" "f" "d" "e" "f" + "ad" "ae" "af" "bd" "be" "bf" "cd" "ce" "cf" +其实可以认为这是一个多叉树,根节点到叶子节点的路径就是每一种字符的组合,然后我们可以通过宽度遍历的方式来得到根节点到叶子节点路径。 + + +```java +public List letterCombinations(String digits) { + List list = new ArrayList(); + if (digits==null || digits.length() ==0) { + return list; + } + LinkedList queue = new LinkedList<>(); + // 空字符串是作为多叉树的根节点 + queue.add(""); + //下面是多叉树的宽度遍历的过程 + for (int i = 0; i < digits.length(); i++) { + String[] array = convert(digits.charAt(i)); + //当queue.getFirst().length() == i+1是说明是本次循环添加的值,那么不应该加进来 + while (queue.size()>0&&queue.getFirst().length()<=i) { + String firstValue = queue.removeFirst(); + //拼接后添加到最后面j + for (int j = 0; j < array.length; j++) { + String temp = firstValue+array[j]; + queue.add(temp); + } + } + } + return queue; + } + String[] convert(Character character) { + String[] list = new String[4]; + if (character == '2') { list = new String[]{"a","b","c"}; } + if (character == '3') { list = new String[]{"d","e","f"}; } + if (character == '4') { list = new String[]{"g","h","i"}; } + if (character == '5') { list = new String[]{"j","k","l"}; } + if (character == '6') { list = new String[]{"m","n","o"}; } + if (character == '7') { list = new String[]{"p","q","r","s"}; } + if (character == '8') { list = new String[]{"t","u","v"}; } + if (character == '9') { list = new String[]{"w","x","y","z"}; } + return list; + } +``` + +下面是另外一种解法,主要是多叉树的深度遍历的解法: + +```java +public List letterCombinations(String digits) { + List arrayList = new ArrayList<>(); + if (digits==null||digits.length()==0) { + return arrayList; + } + tranverse(digits,"",0,arrayList); + return arrayList; +} +//递归进行深度遍历 +public void tranverse(String digits,String currentString,int start,List arrayList) { + //下一层已经没有节点时,直接添加字符串 + if (start>=digits.length()) { + arrayList.add(currentString); + return; + } + //convert方法就不重复列出来了,在上面的解法中有相关的实现 + String[] array = convert(digits.charAt(start)); + //对每一个子节点进行继续递归遍历 + for (int i = 0; i < array.length; i++) { + String temp = currentString + array[i]; + tranverse(digits,temp,start+1,arrayList); + } +} +``` + +### 第15题-三数之和 + +给你一个包含 *n* 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 *a,b,c ,*使得 *a + b + c =* 0 ?请你找出所有满足条件且不重复的三元组。 + +**注意:**答案中不可以包含重复的三元组。 + +**示例:** + +```java +给定数组 nums = [-1, 0, 1, 2, -1, -4], + +满足要求的三元组集合为: +[ + [-1, 0, 1], + [-1, -1, 2] +] +``` + +##### 解题思路 +两数之和的是通过两个指针从首尾两端向中间移动来解决的,三数之和相当于是基于两数之和来解答的,相当于对数组进行遍历,对于每个数调用两数之和的函数获得结果,大致思路如下: +```java +for(int i=0;i> totalList = new ArrayList<>(); + public List> threeSum(int[] nums) { + //先对数组进行排序 + Arrays.sort(nums); + for (int i = 0; i < nums.length; i++) { + //说明上次循环已经添加了 + if(i>=1 && nums[i] == nums[i-1]) { + continue; + } + int target = 0 - nums[i]; + //这里不是从0开始遍历,而是只是对后面的元素遍历,防止三元组重复 + twoSum(nums, i+1, target); + } + return totalList; + } + + public void twoSum(int[] nums,int start, int target) { + int i = start,j=nums.length-1; + while (istart && nums[i] == nums[i-1]){ + i++; + continue; + } else if (sum == target) { + List list = new ArrayList<>(); + list.add(0-target); + list.add(nums[i]); + list.add(nums[j]); + totalList.add(list); + i++; + j--; + } else if (sum > target) { + j--; + } else if(sum rightDepth ? leftDepth+ 1: rightDepth+1; + } +``` + +## 第22题-括号生成 +数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 + +示例: + +输入:n = 3 +输出:[ + "((()))", 000111 + "(()())", 001011 + "(())()", 001101 + "()(())", 010011 + "()()()" 010101 + ] + +##### 解题思路 + +其实这一题也是一个回朔的解法,就是可以把每个"("和")"看成是一个二叉树的节点,从根节点到叶子节点的路径组成了每种括号组合。例如n=3时,二叉树如下: + +二叉树第1层 "" + +第2层 ( ) + +​ ( ) ( ) + +( ) ( ) ( ) ( ) + +每次就使用回朔的方法进行深度遍历,每当发现当前的路径不符合要求时,进行剪枝,回退到上一层,遍历其他路径。(本题中剪枝的要求是左括号数量>n,或者右括号数量>n,或者右括号数量>左括号。) + +```java + public List generateParenthesis(int n) { + List totalList = new ArrayList(); + if(n<=0) { + return totalList; + } + LinkedList stack = new LinkedList(); + //使用回朔算法进行遍历 + generateParenthesis(n,0,0,stack,totalList); + return totalList; + } + + public void generateParenthesis(int n,int left,int right,LinkedList stack,List totalList) { + //不满足要求,进行剪枝,回退去遍历其他节点 + if(left>n || right>n || right>left) {return;} + if(left==n&&right==n) {//正好匹配上了,将栈中所有值转换为 + StringBuffer str = new StringBuffer(); + for(int i = 0;i < stack.size(); i++) { + str.append(stack.get(i)); + } + totalList.add(str.toString()); + } + //往左边遍历 + stack.add('('); + generateParenthesis(n,left+1,right,stack,totalList); + //回朔 + stack.removeLast(); + //往右边遍历 + stack.add(')'); + generateParenthesis(n,left,right+1,stack,totalList); + //回朔 + stack.removeLast(); + } +``` + +PS:回朔算法的框架 + +``` +List result = new ArrayList<>(); +void backtrack(路径, 选择列表stack): + if 超出范围 return + + if 满足结束条件: + result.add(路径) + return + + for 选择 in 选择列表: + 做选择 stack.add(当前元素); + backtrack(路径, 选择列表) + 撤销选择 stack.remove(当前元素); +``` +### 第42题-接雨水 + +给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 + +示例 1: + +![img](../static/rainwatertrap.png) + +输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] +输出:6 +解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 + +##### 解题思路 + +就是对于每个柱子来说,它这个位置顶上可以容纳的水,其实是等于柱子左边柱子的最大值leftMax,右边柱子的最大值rightMax,两者中的较小值min,水容量=min-height[i]。所以先统计出每个柱子左边的最大值和右边的最大值,然后就可以计算水容量了。 + +```java +public int trap(int[] height) { + if (height==null||height.length==0){return 0;} + int[][] dp = new int[height.length][2]; + int leftMax = height[0]; + //统计每个柱子左边的最大值 + for (int i = 1; i < height.length; i++) { + leftMax = leftMax > height[i] ? leftMax : height[i]; + dp[i][0] = leftMax; + } + int rightMax = height[height.length-1]; + //统计每个柱子右边的最大值 + for (int i = height.length-2; i >=0 ; i--) { + rightMax = rightMax > height[i] ? rightMax : height[i]; + dp[i][1] = rightMax; + } + int area = 0; + //统计每个柱子可以存的水 + for (int i = 1; i <= height.length-2; i++) { + int min = dp[i][0] < dp[i][1] ? dp[i][0] : dp[i][1]; + area += min-height[i]; + } + return area; + } +``` + + + + +### 第102题-二叉树的层序遍历 + +给你一个二叉树,请你返回其按 **层序遍历** 得到的节点值。 (即逐层地,从左到右访问所有节点)。 + +示例: + +``` +二叉树:[3,9,20,null,null,15,7], + + 3 + / \ + 9 20 + / \ + 15 7 +返回其层次遍历结果: + +[ + [3], + [9,20], + [15,7] +] +``` + +##### 解题思路 + +其实就是二叉树的宽度优先遍历,只不过返回结果,是每一层的节点存在同一个数组中 + +```java +public List> levelOrder(TreeNode root) { + List totalList = new ArrayList<>(); + if(root==null){ + return totalList; + } + LinkedList currentQueue = new LinkedList(); + LinkedList nextQueue = new LinkedList(); + currentQueue.add(root); + while(currentQueue.size()>0) { + List list = new ArrayList(); + while(currentQueue.size()>0) { + TreeNode node = currentQueue.removeFirst(); + if(node != null) { + list.add(node.val); + if(node.left!=null) { + nextQueue.add(node.left); + } + if(node.right!=null) { + nextQueue.add(node.right); + } + } + } + totalList.add(list); + currentQueue = nextQueue; + nextQueue = new LinkedList(); + } + return totalList; + } +``` + + + +### 第198题-打家劫舍 +你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 + +给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 + +示例 1: + +输入:[1,2,3,1] +输出:4 +解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 +  偷窃到的最高金额 = 1 + 3 = 4 。 + +##### 解题思路: + +其实跟斐波拉契数列很像,这道题中其实如果要计算nums数组的前n个元素的最高金额的话,使用f(n)来代替。 + +f(0)=nums[0]; + +f(1)=nums[1]>nums[0]?nums[1]:nums[0];//也就是num[0]和nums[1]之间的最大值。 + +状态转移方程如下: + +f(n) = f(n-1) > f(n-2)+nums[n] ? f(n-1) :f(n-2)+nums[n] + +```java +Integer[] saveTable; + public int rob(int[] nums) { + if(nums==null|| nums.length == 0) { + return 0; + } + if(nums.length==1) { + return nums[0]; + } + saveTable = new Integer[nums.length]; + int value1 = maxRob(nums,nums.length-1); + int value2 = maxRob(nums,nums.length-2); + return value1 > value2 ? value1 : value2; + } + + public int maxRob(int[] nums,int n) { + if(saveTable[n]!=null) + { + return saveTable[n]; + } + int max = 0; + if(n==0) { + max = nums[0]; + } else if(n==1) { + max = nums[1]>nums[0]?nums[1]:nums[0]; + } else if(n-2>=0) { + int value1 = maxRob(nums,n-1); + int value2 = maxRob(nums,n-2)+nums[n]; + max = value1 > value2 ? value1 : value2; + } + saveTable[n] = max; + return max; + } +``` + +### 第46题-全排列 + +给定一个 **没有重复** 数字的序列,返回其所有可能的全排列。 +**示例:** + +``` +输入: [1,2,3] +输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + +##### 递归解法 + +假设你需要计算[1,2,3]的全排列结果,其实等于 + +1为首元素,[2,3]的全排列结果, + +2为首元素,[1,3]的全排列结果, + +3首元素,[1,2]的全排列结果 + +```java + public List> permute(int[] nums) { + List> totalList = new ArrayList>(); + if (nums == null|| nums.length==0){ + return totalList; + } + tranverse(nums,0,totalList); + return totalList; + } + //遍历 + public void tranverse(int[] nums,int start,List> totalList) { + if (start>=nums.length) {//说明递归到最后了,将所有元素添加到list + List list = new ArrayList(); + for (int i = 0; i < nums.length; i++) { + list.add(nums[i]); + } + totalList.add(list); + return; + } + //遍历将后面的元素取出,与首元素交换,然后对子串递归,因为对于[1,2,3]而言,所有排序结果是等于1为首元素,后面子数组的结果+2为首元素,后面子数组的结果+3为首元素,后面子数组的结果 + for (int i = start; i < nums.length; i++) { + swap(nums,start,i); + tranverse(nums,start+1,totalList); + swap(nums,i,start); + } + } + void swap(int[] nums,int a,int b) { + int temp = nums[a]; + nums[a] = nums[b]; + nums[b] = temp; + } +``` + +##### 回朔解法 + +回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优[搜索](https://baike.baidu.com/item/搜索/2791632)法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。这道题中其实就是用回朔方法对多叉树进行一次遍历,每次到叶子节点后,发现没有元素了就退回。 + +![6111](../static/6111.jpeg) + +```java +List> totalList = new ArrayList>(); +LinkedList stack = new LinkedList(); +HashSet set = new HashSet(); +public List> permute1(int[] nums) { + if (nums==null||nums.length==0) { + return totalList; + } + permute1(nums); + return totalList; +} +public void permute1(int[] nums) { + if (stack.size()==nums.length) {//排列完毕了 + LinkedList newList = new LinkedList<>(stack); + totalList.add(newList); + return; + } + for (int i = 0; i < nums.length; i++) { + if (set.contains(nums[i])) {//包含说明此元素在前面出现过了 + continue; + } + //在剩余元素中找到一个stack中未出现的,然后添加到stack + stack.add(nums[i]); + set.add(nums[i]); + permute1(nums,stack,totalList); + //回撤 + stack.removeLast(); + set.remove(nums[i]); + } +} +``` + +### 第55题-跳跃游戏 + +给定一个非负整数数组,你最初位于数组的第一个位置。 + +数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个位置。 + +示例 1: + +输入: [2,3,1,1,4] +输出: true +解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 + +##### 解题思路 + +这个其实也是通过回朔法去判断每个下标能否到达最后一步 + +```java + //这个数组主要用于记录那些不能到达最后一个元素的数组下标,减少冗余计算 + Boolean[] recordArray; + public boolean canJump(int[] nums) { + if(nums==null||nums.length==0) {return false;} + recordArray = new Boolean[nums.length]; + return canJump(nums,0); + } + + public boolean canJump(int[] nums,int start) { + //当前start已经处于最后一步了,或者是当前数组下标加上数字超过最后一个元素了 + if(start >= nums.length-1 || start+nums[start] >= nums.length-1) { + return true; + } + //已经对于改已经有记录结果,不用重复计算 + if(recordArray[start]!=null) { + return recordArray[start]; + } + int end = start+nums[start]; + //计算[start+1,end]之间的元素,是否有可以到达最后一步的 + for(int i = start+1;i<=end;i++) { + if(canJump(nums,i)) { + return true; + } + } + recordArray[start] = false; + return false; + } +``` + +##### 贪心解法 + +```java +public boolean canJump(int[] nums) { + if(nums==null||nums.length==0) {return false;} + //maxDepth代表可以抵达的最远距离 + int maxDepth = nums[0]; + for(int i=1;i<=maxDepth && i< nums.length;i++){ + //当前元素的可抵达距离超过maxDepth,进行更新 + if(nums[i]+i>maxDepth){ + maxDepth = nums[i]+i; + } + } + return maxDepth>= nums.length-1; + } +``` + +### 第62题-不同路径 + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 + +问总共有多少条不同的路径? 右6 下2 C 2 8 + + + +![img](../static/robot_maze.png) + +输入:m = 3, n = 2 +输出:3 +解释: +从左上角开始,总共有 3 条路径可以到达右下角。 +1. 向右 -> 向右 -> 向下 +2. 向右 -> 向下 -> 向右 +3. 向下 -> 向右 -> 向右 + +##### 解题思路 + +其实就是C(m-1,m-1+n-1),其实总共要走m-1+n-1步,其中有m-1步是向右的,n-1步是向下的,所以其实是一个组合问题,相当于在m-1+n-1步中找出m-1步的组合数。 + +```java +public int uniquePaths(int m, int n) { + int rightStep = m-1; + int downStep = n-1; + int min = rightStep0) { + allTimes = allTimes*sum; + innerTimes = innerTimes *min; + min--; + sum--; + } + return (int) (allTimes/innerTimes); + } +``` +### 第56题-合并区间 + +给出一个区间的集合,请合并所有重叠的区间。 + +示例 1: +``` +输入: intervals = [[1,3],[2,6],[8,10],[15,18]] +输出: [[1,6],[8,10],[15,18]] +解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]。 +``` + +##### 解题思路 + +就是先根据左边界进行排序,排序完之后进行进行区间合并,合并的判断规则就是当前区间的左边界是否在上一个区间内。 + +```java +public int[][] merge(int[][] intervals) { + if (intervals==null||intervals.length<=1) { + return intervals; + } + //先进行排序 + quickSort(intervals,0,intervals.length-1); + int lastIndex = 0; + for (int i = 1; i < intervals.length; i++) { + if (intervals[i][0] >= intervals[lastIndex][0] + && intervals[i][0] <= intervals[lastIndex][1]) { + //如果当前区间的左边界处于上一个区间中间,说明可以被合并 + intervals[lastIndex][1] = intervals[lastIndex][1] > intervals[i][1] + ? intervals[lastIndex][1] : intervals[i][1]; + } else {//不能被合并 + lastIndex++; + intervals[lastIndex][0] = intervals[i][0]; + intervals[lastIndex][1] = intervals[i][1]; + } + } + //对数组进拷贝 + int[][] result = new int[lastIndex+1][2]; + for (int i = 0; i <= lastIndex ; i++) { + result[i][0] = intervals[i][0]; + result[i][1] = intervals[i][1]; + } + return result; + } + //快排,按照每个区间的左边界进行排序 + void quickSort(int[][] array,int start,int end) { + if (start>=end){return;} + int i = start; + int j = end; + int base = array[start][0]; + while (i base && j>i) {j--;} + while (array[i][0]<=base&&j>i) {i++;} + swap(array,i,j); + } + swap(array,start, i); ; + quickSort(array,start,i-1); + quickSort(array,i+1,end); + } + //交换元素 + void swap(int[][] array, int i,int j) { + int temp_0 = array[j][0]; + int temp_1 = array[j][1]; + array[j][0] = array[i][0]; + array[j][1] = array[i][1]; + array[i][0] = temp_0; + array[i][1] = temp_1; + } +``` +### 第169题-多数元素 +给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于n/2 的元素。 + +你可以假设数组是非空的,并且给定的数组总是存在多数元素。 + +示例 1: +``` +输入: [3,2,3] +输出: 3 +``` +##### 解题思路 +这个多数元素一定是数组的中位数,所以可以转换为寻找数组的中位数,也就是寻找第nums.length/2小的元素,也就转换为Top K问题了,所以使用快排解决。 +```java +public int majorityElement(int[] nums) { + if (nums==null||nums.length==0) { return 0; } + return quickSort(nums,nums.length/2,0,nums.length-1); + } + int quickSort(int[] nums,int k,int start,int end) { + if (start>=end) { + return nums[start]; + } + int base = nums[start]; + int i = start; + int j = end; + while (ibase&&ik) { + return quickSort(nums,k,start,i-1); + } else { + return quickSort(nums,k,i+1,end); + } + } +``` +### 第101题-对称二叉树 + +给定一个二叉树,检查它是否是镜像对称的。 + +例如,二叉树 [1,2,2,3,4,4,3] 是对称的。 + + 1 + / \ + 2 2 + / \ / \ + 3 4 4 3 +##### 解题思路 + +```java +public boolean isSymmetric(TreeNode root) { + if (root == null) { + return true; + } + return isSymmetric(root.left, root.right); +} +public boolean isSymmetric(TreeNode left, TreeNode right) { + if (left == null && right == null) {//都为null + return true; + } + if ((left == null && right != null) || (left != null && right == null)) {//其中一个为null + return false; + } + if (left.val != right.val) {//都不为null但是值不相等 + return false; + } + //判断子节点是否相等 + return isSymmetric(left.left, right.right) && isSymmetric(left.right, right.left); +} +``` +### 第33题-搜索旋转排序数组 +升序排列的整数数组 nums 在预先未知的某个点上进行了旋转(例如, [0,1,2,4,5,6,7] 经旋转后可能变为 [4,5,6,7,0,1,2] )。 + +请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。 +示例 1: + +输入:nums = [4,5,6,7,0,1,2], target = 0 +输出:4 +##### 解题思路 +还是按照二分搜索来进行搜索,只是多一步判断,如果nums[mid]nums[mid] && target<=nums[right]) { + left = mid+1; + } else { + right = mid-1; + } + } else {//说明旋转点在右边,这是我们根据左边来判断 + if(target>=nums[left] && target < nums[mid]) { + right = mid-1; + } else { + left = mid+1; + } + } + } + return -1; + } +``` +### 第136题-只出现一次的数字 + +给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 + +说明: + +你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗? + +示例 1: + +输入: [2,2,1] +输出: 1 + +##### 解题思路 + +```java +public int singleNumber(int[] nums) { + int value =0; + for(int i = 0;i4->5, + 1->3->4, + 2->6 +] +将它们合并到一个有序链表中得到。 +1->1->2->3->4->4->5->6 + +##### 解题思路 +就是对K个链表的头结点建立一个小顶堆,每次取堆顶元素出来,放到新链表的末尾。然后对堆进行调整,每次调整复杂度为logK,总时间复杂度是N*LogK. +```java +public ListNode mergeKLists(ListNode[] lists) { + if(lists==null||lists.length==0) {return null;} + ListNode preHead = new ListNode(-1); + ListNode currentNode = preHead; + ArrayList arrayList = new ArrayList<>(); + //过滤lists中为null的元素,然后将不为null的元素添加到arrayList中去 + // (测试用例中有很多为null的用例) + for (int i = 0; i < lists.length; i++) { + if (lists[i] != null) { + arrayList.add(lists[i]); + } + } + if (arrayList.size()==0) { + return null; + } + //建立小顶堆 + for (int i = arrayList.size()/2-1; i >= 0; i--) { + adjustHeap(arrayList,i,arrayList.size()); + } + while (arrayList.size()>0) { + if (arrayList.get(0) == null) {//这个链表到最后一个节点了,从小顶堆中移除 + swap(arrayList,0,arrayList.size()-1); + arrayList.remove(arrayList.size()-1); + continue; + } + adjustHeap(arrayList,0,arrayList.size()); + + ListNode node = arrayList.get(0); + currentNode.next = node; + currentNode = currentNode.next; + arrayList.set(0,node.next); + } + return preHead.next; + } + + void adjustHeap(ArrayList lists, int i, int length) { + while (2*i+1 lists,int a, int b) { + ListNode temp = lists.get(a); + lists.set(a,lists.get(b)); + lists.set(b,temp); + } +``` + +### 第94题-二叉树的中序遍历 + +给定一个二叉树的根节点 `root` ,返回它的 **中序** 遍历。 + +**示例 1:** + +![img](../static/inorder_1.jpg) + +``` +输入:root = [1,null,2,3] +输出:[1,3,2] +``` + +##### 解题思路 + +递归解法 + +```java +List list = new ArrayList(); +public List inorderTraversal(TreeNode root) { + if(root==null){return list;} + inorderTraversal(root.left); + list.add(root.val); + inorderTraversal(root.right); + return list; +} +``` + +栈解法 + +就是遍历到每个节点时,先把这个节点的右节点right添加到栈,再把这个节点添加到栈,再把这个节点的左节点left添加到栈。再把这个节点的left和right指针设置为null,代表这个节点的左右子节点已经被访问到了,下次遍历到这个节点时可以直接添加到列表中。 + +```java +public List inorderTraversal1(TreeNode root) { + List list = new ArrayList(); + if (root==null){return list;} + Stack stack = new Stack<>(); + stack.add(root); + while (stack.size()>0) { + TreeNode node = stack.pop(); + if (node.left == null && node.right==null) { + list.add(node.val); + continue; + } + if (node.right!=null) { + stack.push(node.right); + //这里将right置为null主要防止父节点多次遍历,也可以使用一个HashSet来记录那些已经添加子节点的父节点。 + node.right =null; + } + stack.push(node); + if (node.left!=null) { + stack.push(node.left); + //这里将left置为null主要防止父节点多次遍历,也可以使用一个HashSet来记录那些已经添加子节点的父节点。 + node.left=null; + } + } + return list; +} +``` + +### 第64题-最小路径和 + +给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 + +说明:每次只能向下或者向右移动一步。 + +示例 1: + +![img](../static/minpath.jpg) + +``` +输入:grid = [[1,3,1],[1,5,1],[4,2,1]] +输出:7 +解释:因为路径 1→3→1→1→1 的总和最小。 +``` + +##### 解题思路 + +其实跟斐波拉契数列的题很相似 + +```java +int[][] cache; +public int minPathSum(int[][] grid) { + if (grid==null||grid[0]==null) {return 0;} + cache = new int[grid.length][grid[0].length]; + return minPathSum(grid,0,0); +} + +public int minPathSum(int[][] grid, int row,int col) { + //缓存中已有这一步到右下角的数据,直接从缓存中取值 + if (cache[row][col]!=0) { + return cache[row][col]; + } + int max_row = grid.length-1; + int max_col = grid[0].length-1; + int path = 0; + if (row == max_row && col == max_col) {//已经处于右下角 + path = grid[row][col]; + } else if (row == max_row) {//当前处于最下面的一行,只能往右边走 + path = grid[row][col] + minPathSum(grid,row,col+1); + } else if (col == max_col) {//当前处于最右边的一行,只能往下边走 + path = grid[row][col] + minPathSum(grid,row+1,col); + } else { + //往下走 + int down = grid[row][col] + minPathSum(grid, row, col + 1); + int right = grid[row][col] + minPathSum(grid, row + 1, col); + path = down < right ? down : right; + } + cache[row][col] = path; + return path; +} +``` +### 第215题-数组中的第K个最大元素 + +在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 + +示例 1: + +输入: [3,2,1,5,6,4] 和 k = 2 +输出: 5 + +##### 解题思路 + +就是Top K问题,这里可以使用堆排进行排序,需要注意的是,大顶堆建堆结束后,每次将堆顶元素移动到数组末尾,然后继续对剩下的0到i-1范围内的元素进行调整,所以是adjustHeap(nums,0,i); +```java +public int findKthLargest(int[] nums, int k) { + for (int i = nums.length/2-1; i >=0 ; i--) { + //对整个数组看成一个对,进行大顶堆调整 + adjustHeap(nums,i,nums.length); + } + for (int i = nums.length-1; i > 0; i--) { + swap(nums,0,i); + //对0到i-1范围内的元素看成一个堆,进行大顶堆调整 + if (i==nums.length-k) { + break; + } + adjustHeap(nums,0,i); + } + return nums[nums.length - k]; + } + void adjustHeap(int[] nums, int i,int currentLength) { + //左子节点存在 + while (2*i+1 nums[left]) {//右节点也存在,并且大于左节点 + if (nums[right] > nums[i]) {//右节点大 + swap(nums,i,right); + i = right; + } else {//根节点最大 + break; + } + } else {//右节点不存在,或者右节点比左节点小 + if (nums[left] > nums[i]) { + swap(nums,i,left); + i = left; + } else { + break; + } + } + } + } +``` + +### 第234题-回文链表 +请判断一个链表是否为回文链表。 + +示例 1: + +输入: 1->2 +输出: false +示例 2: + +输入: 1->2->2->1 +输出: true +##### 解题思路 +就是用一个快慢指针,找到链表的中位数节点,然后对后半部分链表进行反转,然后分别从原链表头部和尾部开始遍历判断。 +```java + public boolean isPalindrome(ListNode head) { + ListNode slow = head; + ListNode quick = head; + while (quick != null) { + quick = quick.next; + if (quick == null) { + break; + } + quick = quick.next; + slow = slow.next; + } + //此时的slow要么是中位数节点, + // 中位数有两个时,就是靠前的那个中位数节点 + //总结点数为奇数 1->2->3->2->1 slow为3 + //总结点数为偶数数 1->2->2->1 slow为第一个2 + //对后面的节点进行翻转 + ListNode preNode = slow; + ListNode currentNode = slow.next; + while (currentNode!=null) { + ListNode tempNode = currentNode.next; + currentNode.next = preNode; + preNode = currentNode; + currentNode = tempNode; + } + slow.next = null; + ListNode otherHead = preNode; + while (head!=null && otherHead!=null) { + if ((head==null&&otherHead!=null) + || (head!=null&&otherHead==null)) { + return false; + } + if (head.val!=otherHead.val) { + return false; + } + head=head.next; + otherHead = otherHead.next; + } + return true; + } +``` + +### 第200题-岛屿数量 +给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 + +岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 +此外,你可以假设该网格的四条边均被水包围。 +示例 1: + +输入:grid = [ + ["1","1","1","1","0"], + ["1","1","0","1","0"], + ["1","1","0","0","0"], + ["0","0","0","0","0"] +] +输出:1 + +#### 解题思路 +因为每个岛屿中所有的1之间是可以相互抵达的,只要你找到岛屿中的一个1,然后向四周进行遍历,判断是1继续向四周扩散,可以抵达这个岛屿所有的节点,所以通过infect函数对每个未被遍历的岛屿点进行扩散,判断这个点是1就将它设置为2,代表已遍历,然后继续扩散。这样就可以统计出岛屿数量。 +```java +public int numIslands(char[][] grid) { + if (grid==null||grid[0]==null){return 0;} + int num =0; + for (int i = 0; i < grid.length; i++) { + for (int j = 0; j < grid[i].length; j++) { + char c = grid[i][j]; + if (c == '1') {//如果是未感染的陆地,那么使用infect函数向四周扩散 + infect(grid,i,j); + num++; + } + } + } + return num; + } + //向四周未被遍历的陆地进行扩散 + void infect(char[][] grid,int i,int j) { + if (i < 0 || i >= grid.length || j < 0 || j >= grid[i].length) { + return; + } + if (grid[i][j] == '1') { + grid[i][j]= '2'; + infect(grid,i-1,j); + infect(grid,i+1,j); + infect(grid,i,j-1); + infect(grid,i,j+1); + } + } +``` +### 第48题-旋转图像 + +给定一个 n × n 的二维矩阵表示一个图像。 +将图像顺时针旋转 90 度。 +说明: +你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。 +示例 1: +给定 matrix = +[ + [1,2,3], + [4,5,6], + [7,8,9] +], + +原地旋转输入矩阵,使其变为: +[ + [7,4,1], + [8,5,2], + [9,6,3] +] +##### 解题思路 +这个题就是你可以每次只旋转外圈元素,然后旋转完毕后把内圈元素看成一个新的矩阵,继续对矩阵进行旋转。在对外圈元素进行旋转时,我们只需要先将最上方一条边的元素与右方元素交换,再与下方元交换,在于左方元素交换,这样最好就旋转成功了。 + +例如: + [1,2,3], + [4,5,6], + [7,8,9] + 对于这个矩阵来说,我们只旋转外圈元素,也就是1,2,3,6,9,8,7,4,1。然后把里面的元素,也就是5看成一个新的矩阵,继续对外圈元素进行旋转,直到最后矩阵只剩下一个元素。 + +如何对外圈元素进行旋转呢? +我们对最上面的一条边进行遍历,也就是[1,3)进行遍历,例如一开始的元素是1,将左上角的元素1与右上角的元素3交换,此时左上角元素为3,再将左上角的3与右下角的9交换,此时左上角为9,再将左上角的9与左下角的7进行交换,这样对于四个角的元素来说,就完成了旋转,然后继续遍历,对2,6,8,4四个元素按照相同的方法进行旋转。 +```java +public void rotate(int[][] matrix) { + if (matrix == null || matrix[0] == null) { + return; + } + rotate(matrix,0,matrix.length-1,0,matrix[0].length-1); + } + //对矩阵进行旋转 + public void rotate(int[][] matrix,int rowStart,int rowEnd,int colStart,int colEnd) { + if (rowStart>=rowEnd || colStart>= colEnd) { + return; + } + //进行交换 + for (int j = colStart; j < colEnd; j++) { + //当前j从原点走了几步 + int race = j-colStart; + //左上角与右上角元素交换 + swap(matrix,rowStart,j,rowStart+race,colEnd); + //左上角与右下角元素交换 + swap(matrix,rowStart,j,rowEnd,colEnd-race); + //左上角与左下角元素交换 + swap(matrix,rowStart,j,rowEnd-race,colStart); + } + //然后对内圈元素进行旋转 + rotate(matrix,rowStart+1,rowEnd-1,colStart+1,colEnd-1); + } + + void swap(int[][] matrix,int i,int j,int other_i,int other_j) { + int temp = matrix[i][j]; + matrix[i][j] = matrix[other_i][other_j]; + matrix[other_i][other_j] = temp; + } +``` + +### 第98题-验证二叉搜索树 + +给定一个二叉树,判断其是否是一个有效的二叉搜索树。 + +假设一个二叉搜索树具有如下特征: + +节点的左子树只包含小于当前节点的数。 +节点的右子树只包含大于当前节点的数。 +所有左子树和右子树自身必须也是二叉搜索树。 +示例 1: + +输入: + 2 + / \ + 1 3 +输出: true + +##### 解题思路 + +二叉搜索树的中序遍历结果就是一个排序好的序列,所以我们可以对二叉树进行中序遍历 + +,判断当前的遍历节点值是否大于上一个节点值。 + +```java +Integer lastValue = null; +public boolean isValidBST(TreeNode root) { + if (root==null) {return true;} + Boolean leftResult = isValidBST(root.left); + if (leftResult==false){return false;} + if (lastValue==null){ + lastValue=root.val; + } else if (lastValue>=root.val) { + return false; + } else if (lastValue> subsets(int[] nums) { + List> totalList = new ArrayList>(); + int size = (int)Math.pow(2,nums.length); + for (int i = 0; i < size; i++) { + List list = new ArrayList<>(); + for (int j = 0; j < nums.length; j++) { + //j是元素在数组中的位置,通过判断i的第j个二进制位是否为0,来决定是否添加这个元素 + int result = i & (1<> totalList = new ArrayList>(); + public List> combinationSum(int[] candidates, int target) { + combinationSum(candidates,target,candidates.length-1,new LinkedList()); + return totalList; + } + + public void combinationSum(int[] candidates, + int target, int end, + LinkedList stack) { + if (target==0) { + List copyList = new ArrayList<>(stack); + totalList.add(copyList); + return; + } else if (target<0||end<0) { + return; + } + + for (int i = 0; i*candidates[end] <= target ; i++) { + for (int j = 0; j < i; j++) {//添加j个当前元素,也就是假设子集和中有j个candidates[end]元素 + stack.add(candidates[end]); + } + combinationSum(candidates,target-i*candidates[end],end-1,stack); + for (int j = 0; j < i; j++) {//移除j个当前元素 + stack.removeLast(); + } + } + } +``` + +### 第226题-翻转二叉树 + +翻转一棵二叉树。 + +示例: + + 输入: + 4 + / \ + 2 7 + / \ / \ + 1 3 6 9 + 输出: + 4 + / \ + 7 2 + / \ / \ + 9 6 3 1 + +##### 解题思路 + +```java +public TreeNode invertTree(TreeNode root) { + if (root==null) {return null;} + TreeNode temp = root.left; + root.left = root.right; + root.right = temp; + invertTree(root.left); + invertTree(root.right); + return root; +} +``` + +## 第31题-下一个排列 + +实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。 + +如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。 + +必须 原地 修改,只允许使用额外常数空间。  + +示例 1: + +输入:nums = [1,2,3] +输出:[1,3,2] + +##### 解题思路 +本题其实就是提升数字的字典序,并且要提升的幅度最小。就是从后面往前找到第一个nums[i-1]0 ; i--) { + + if (nums[i-1] nums[i-1]) { + min = j; + } + } + //进行交换 + int temp = nums[i-1]; + nums[i-1] = nums[min]; + nums[min] = temp; + flag=1; + //然后再对后面的元素进行排序 + Arrays.sort(nums,i,nums.length); + break; + } + } + //如果现在的数组就是字典序最大的,那么就排序,得到字典序最小的进行返回。 + if (flag==0) { + Arrays.sort(nums); + } + } +``` + + +### 第322题-零钱兑换 + +题目详情:(https://leetcode-cn.com/problems/coin-change/) +给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 + +你可以认为每种硬币的数量是无限的。 + +示例 1: + +输入:coins = [1, 2, 5], amount = 11 +输出:3 +解释:11 = 5 + 5 + 1 + +##### 递归解法 + +```java + Integer[] array; + public int coinChange(int[] coins, int amount) { + if (array == null) {//初始化缓存数组 + array = new Integer[amount+1]; + } + if (amount == 0) { + return 0; + } else if (amount < 0) { + return -1; + } else if(array[amount]!=null) {//缓存数组中有值,直接返回 + return array[amount]; + } + Integer minNum = null; + for (int i = 0; i < coins.length; i++) {//遍历计算最大值 + int remainNum = coinChange(coins, amount - coins[i]); + if (remainNum == -1) { + continue; + } else if(minNum==null || remainNum + 1 < minNum ) { + minNum = remainNum + 1; + } + } + array[amount] = minNum; + return minNum == null ? -1 : minNum; + } +``` + +##### 动态规划解法 + +1.找到Base Case + +金额为0时,需要返回的硬币数是0。f(0)=0 + +2.确定状态 + +也就是原问题和子问题中会变化的变量。这个问题中的变量就是金额会变化,硬币面额是确定的,数量是无限。 + +3.确定选择 + +也就是导致「状态」产生变化的行为,这个题里面的选择就是在凑金额的时候,硬币面额的选择, + +4.确定对应的`dp`函数/数组 + +这里会有一个递归的 `dp` 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就这个题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量,每个目标金额的最少硬币数量=min(目标金额-硬币面额后的金额所需最少硬币数),所以我们可以这样定义 `dp` 函数: + +![coin](../static/coin.png) + +搞清楚上面这几个关键点,解法的伪码就可以写出来了: + +```python +# 伪码框架 +def coinChange(coins: List[int], amount: int): + # 定义:要凑出金额 n,至少要 dp(n) 个硬币 + def dp(n): + # 做选择,选择需要硬币最少的那个结果 + for coin in coins: + res = min(res, 1 + dp(n - coin)) + return res + # 题目要求的最终结果是 dp(amount) + return dp(amount) +``` + +```java +//动态规划解法 +public int coinChange1(int[] coins, int amount) { + int[] array = new int[amount+1]; + for (int i = 1; i <= amount; i++) { + Integer minNum=null; + for (int j = 0; j < coins.length; j++) { + int remain = i - coins[j]; + if (remain < 0 || array[remain]==-1) { + continue; + } + //此时肯定有值 + if (minNum==null || array[remain]+1 < minNum ) { + minNum = array[remain]+1; + } + } + array[i] = minNum == null ? -1 : minNum; + } + return array[amount]; +} +``` + +### 第300题-最长递增子序列 + +给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 + +子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 + + +示例 1: + +输入:nums = [10,9,2,5,3,7,101,18] +输出:4 +解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。 + +##### 解题思路 +使用一个数组dp[i]来记录包含元素i时,最大递增子序列长度,所以状态转移方程为 +dp[i] = max(1,dp[k]+1); k=0; j--) { + if (nums[j] < nums[i] && dp[j] + 1 > max) { + max = dp[j] + 1; + } + } + dp[i] = max; + totalMax = max > totalMax ? max : totalMax; + } + return totalMax; + } + +``` + +时间复杂度为O(NlogN)的解法,就是使用一个数组sizeArray记录元素对应的最长子序列长度,sizeArray[i]代表sizeArray[i]这个元素组成的最长子序列的长度为i+1 + +```java +public int anotherLengthOfLIS(int[] nums) { + if (nums== null||nums.length==0) { + return 0; + } + //sizeArray[i]代表sizeArray[i]这个元素组成的最长子序列的长度为i+1 + int[] sizeArray = new int[nums.length]; + sizeArray[0] = nums[0];//第一个元素的最长子序列长度为1 + int maxSize = 0; + for (int i = 1; i < nums.length; i++) { + if (nums[i]>sizeArray[maxSize]) {//如果当前元素比最长子序列的尾部元素大 + maxSize++; + sizeArray[maxSize] = nums[i]; + } else if (nums[i]==sizeArray[maxSize]) {//等于尾部元素,那么不用更新 + continue; + } else {//小于尾部元素进行更新,在sizeArray中进行二分查找 + int left = 0; + int right = maxSize; + while (left < right) { + int mid = (left+right)/2; + if (sizeArray[mid] < nums[i]) { + left = mid+1; + } else { + right = mid; + } + } + //最后left的位置肯定就是需要进行插入的位置 + //left左边的元素都比nums[i]小 + sizeArray[left]=nums[i]; + } + } + return maxSize+1; +} +``` \ No newline at end of file diff --git a/docs/LeetCode1.md b/docs/LeetCode1.md new file mode 100644 index 0000000..bb25d1a --- /dev/null +++ b/docs/LeetCode1.md @@ -0,0 +1,2371 @@ +## LeetCode 热门100题-题解(下) + +##### 主要是记录自己刷题的过程,也方便自己复习 + +##### [第155题-最小栈](#第155题-最小栈) +##### [第160题-相交链表](#第160题-相交链表) + +##### [第142题-环形链表II](#第142题-环形链表II) +##### [第739题-每日温度](#第739题-每日温度) + +##### [第347题-前K个高频元素](#第347题-前K个高频元素) + +##### [第49题-字母异位词分组](#第49题-字母异位词分组) +##### [第32题-最长有效括号](#第32题-最长有效括号) +##### [第543题-二叉树的直径](#第543题-二叉树的直径) +##### [第79题-单词搜索](#第79题-单词搜索) +##### [第96题-不同的二叉搜索树](#第96题-不同的二叉搜索树) +##### [第239题-滑动窗口最大值](#第239题-滑动窗口最大值) +##### [第146题-LRU缓存机制](#第146题-LRU缓存机制) +##### [第236题-二叉树的最近公共祖先](#第236题-二叉树的最近公共祖先) +##### [第114题-二叉树展开为链表](#第114题-二叉树展开为链表) +##### [第84题-柱状图中最大的矩形](#第84题-柱状图中最大的矩形) +##### [第148题-排序链表](#第148题-排序链表) +##### [第617题-合并二叉树](#第617题-合并二叉树) +##### [第287题-寻找重复数](#第287题-寻找重复数) +##### [第152题-乘积最大子数组](#第152题-乘积最大子数组) +##### [第72题-编辑距离](#第72题-编辑距离) +##### [第139题-单词拆分](#第139题-单词拆分) +##### [第76题-最小覆盖子串](#第76题-最小覆盖子串) +##### [第124题-二叉树中的最大路径和](#第124题-二叉树中的最大路径和) +##### [第461题-汉明距离](#第461题-汉明距离) +##### [第128题-最长连续序列](#第128题-最长连续序列) +##### [第647题-回文子串](#第647题-回文子串) +##### [第337题-打家劫舍III](#第337题-打家劫舍III) +##### [第238题-除自身以外数组的乘积](#第238题-除自身以外数组的乘积) +##### [第207题-课程表](#第207题-课程表) +##### [第309题-最佳买卖股票时机含冷冻期](#第309题-最佳买卖股票时机含冷冻期) +##### [第416题-分割等和子集](#第416题-分割等和子集) +##### [第560题-和为K的子数组](#第560题-和为K的子数组) +##### [第448题-找到所有数组中消失的数字](#第448题-找到所有数组中消失的数字) +##### [第437题-路径总和III](#第437题-路径总和III) +##### [第338题-比特位计数](#第338题-比特位计数) +##### [第406题-根据身高重建队列](#第406题-根据身高重建队列) +##### [第538题-把二叉搜索树转换为累加树](#第538题-把二叉搜索树转换为累加树) +##### [第297题-二叉树的序列化与反序列化](#第297题-二叉树的序列化与反序列化) +##### [第438题-找到字符串中所有字母异位词](#第438题-找到字符串中所有字母异位词) +##### [第240题-搜索二维矩阵II](#第240题-搜索二维矩阵II) +##### [第494题-目标和](#第494题-目标和) +##### [第621题-任务调度器](#第621题-任务调度器) +##### [第581题-最短无序连续子数组](#第581题-最短无序连续子数组) + + +### 第155题-最小栈 +设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 + +push(x) —— 将元素 x 推入栈中。 +pop() —— 删除栈顶的元素。 +top() —— 获取栈顶元素。 +getMin() —— 检索栈中的最小元素。 + + +示例: + +输入: +["MinStack","push","push","push","getMin","pop","top","getMin"] +[[],[-2],[0],[-3],[],[],[],[]] + +输出: +[null,null,null,null,-3,null,0,-2] + +解释: +MinStack minStack = new MinStack(); +minStack.push(-2); +minStack.push(0); +minStack.push(-3); +minStack.getMin(); --> 返回 -3. +minStack.pop(); +minStack.top(); --> 返回 0. +minStack.getMin(); --> 返回 -2. + +##### 解题思路 +就是用一个最小栈来记录最小值 +```java +Stack stack; + Stack minStack; + /** initialize your data structure here. */ + public MinStack() { + stack = new Stack(); + minStack = new Stack(); + } + + public void push(int x) { + if(minStack.size()==0||x<=minStack.peek()) { + minStack.push(x); + } + stack.push(x); + } + + public void pop() { + if(stack.peek().equals(minStack.peek())) { + minStack.pop(); + } + stack.pop(); + } + + public int top() { + return stack.peek(); + } + + public int getMin() { + return minStack.peek(); + } +``` + +### 第160题-相交链表 + +编写一个程序,找到两个单链表相交的起始节点。 + +如下面的两个链表**:** + +[![img](../static/160_statement.png)](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/12/14/160_statement.png) + +在节点 c1 开始相交。 + +##### 解题思路 + +分别计算出链表A,链表B的长度,让长的链表先走n步,直到两个链表剩余节点长度一样,然后每个链表都走一步,直到节点相等,即为相交节点。 + +```java +public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + int lengthA=0; + ListNode nodeA = headA; + while(nodeA!=null) { + lengthA++; + nodeA=nodeA.next; + } + int lengthB=0; + ListNode nodeB = headB; + while(nodeB!=null) { + lengthB++; + nodeB=nodeB.next; + } + nodeA = headA; + nodeB = headB; + while(lengthA!=lengthB) { + if (lengthA>lengthB) { + lengthA--; + nodeA = nodeA.next; + } else { + lengthB--; + nodeB = nodeB.next; + } + } + while(nodeA!=null && nodeB!=null) { + if(nodeA == nodeB) { + return nodeA; + } + nodeA = nodeA.next; + nodeB = nodeB.next; + } + return null; + } +``` + +### 第142题-环形链表II + +给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 + +为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。 + +说明:不允许修改给定的链表。 + +进阶: + +你是否可以使用 O(1) 空间解决此题? + + +示例 1: + +![img](../static/circularlinkedlist-9062165.png) + +输入:head = [3,2,0,-4], pos = 1 +输出:返回索引为 1 的链表节点 +解释:链表中有一个环,其尾部连接到第二个节点。 + +##### 解题思路 + +```java +public ListNode detectCycle(ListNode head) { + //快慢节点在圆中相遇 + //然后慢节点在圆中走一圈,计算出圆的周长 + //快节点凑头 + if (head==null || head.next == null) { + return null; + } + ListNode slow = head.next; + ListNode quick = head.next.next; + //通过快慢指针,让两个指针在环中相遇 + while (quick!=slow) { + if (quick == slow && quick!=null) { + break; + } + slow = slow.next; + if (quick==null||quick.next == null) { + return null; + } + quick = quick.next; + quick = quick.next; + } + //计算环的长度 + int circleLength = 1; + ListNode copySlow = slow.next; + while (copySlow != slow) { + copySlow = copySlow.next; + circleLength++; + } + //快指针先走环的长度步 + quick = head; + while (circleLength>0) { + quick = quick.next; + circleLength--; + } + slow = head; + //慢指针出发,相遇的节点就是环的入口 + while (quick!=slow) { + quick = quick.next; + slow = slow.next; + } + return quick; + } +``` + +### 第739题-每日温度 + +请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。 + +例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 + +提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。 + +##### 解题思路 +其实跟滑动窗口最大值那个题的解题思路很像,就是维护一个还没有排序好的栈,每次把栈中比当前遍历元素小的,都出栈,然后计算等待天数,然后将当前元素入栈。 +```java + public int[] dailyTemperatures(int[] T) { + if (T == null || T.length==0) { + return null; + } + int[] result = new int[T.length]; + Stack stack = new Stack<>(); + stack.add(0); + for (int i = 1; i < T.length; i++) { + //比栈顶元素小,栈 + if (stack.size() == 0 || T[i] <= T[stack.peek()]) { + stack.push(i); + } else { + // 比栈顶元素大,就一直出栈 + while (stack.size() >0 && T[i] > T[stack.peek()]) { + int index = stack.pop(); + result[index] = i - index; + } + stack.push(i); + } + } + return result; + } +``` + +### 第347题-前K个高频元素 + +给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 + +示例 1: + +输入: nums = [1,1,1,2,2,3], k = 2 +输出: [1,2] +示例 2: + +输入: nums = [1], k = 1 +输出: [1] + +##### 解题思路 +先使用一个HashMap来统计各元素的频率,然后取前k个元素建立小顶堆,然后对后面的元素进行遍历,如果频率高于堆顶元素,就与堆顶元素交换,然后对堆进行调整。 +```java +public int[] topKFrequent(int[] nums, int k) { + HashMap map = new HashMap(); + for(int i = 0;i= 0; i--) { + adjustHeap(result,i,result.length,map); + } + for (int i = k; i < array.length ; i++) { + Integer key = (Integer) array[i]; + if (map.get(key) >= map.get(result[0])) { + result[0] = (Integer)array[i]; + adjustHeap(result,0,result.length,map); + } + } + return result; + } + void adjustHeap(int[] array, int i,int length,HashMap map) { + while (2*i+1> groupAnagrams(String[] strs) { + HashMap map = new HashMap(); + for (String str : strs) { + char[] charArray = str.toCharArray(); + Arrays.sort(charArray); + String sortedString = new String(charArray); + List strList = map.get(sortedString); + if (strList == null) { + strList = new LinkedList(); + } + strList.add(str); + map.put(sortedString,strList); + } + ArrayList totalList = new ArrayList<>(); + totalList.addAll(map.values()); + return totalList; + } +``` + +### 第32题-最长有效括号 + +给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。 + +示例 1: + +输入: "(()" +输出: 2 +解释: 最长有效括号子串为 "()" +示例 2: + +输入: ")()())" +输出: 4 +解释: 最长有效括号子串为 "()()" + +##### 解题思路 + +判断一个字符是否在是有效括号,就是对字符串遍历,如果是(,就将(字符入栈,如果是)字符,就判断栈是否为有元素,有元素代表括号匹配上了,将当前括号和栈顶元素括号都标记为有效括号。标记的方式是将recordArray数组中相应下标置为1,然后统计最长有效括号就是统计recordArray数组中连续1的个数。 + +```java + public int longestValidParentheses(String s) { + Stack stack = new Stack(); + + int[] recordArray = new int[s.length()]; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c=='(') { + stack.push(i); + } else if (c==')' && stack.size()>0) { + //说明这个位置可以匹配 + recordArray[i]=1; + int leftIndex = stack.pop(); + recordArray[leftIndex] = 1; + } + } + //统计recordArray张连续1的长度 + int max = 0; + int currentSize =0; + for (int i = 0; i < recordArray.length; i++) { + if (recordArray[i] ==0) { + currentSize=0; + } else if (recordArray[i] ==1) { + currentSize++; + } + max = max > currentSize ? max : currentSize; + } + return max; + } +``` + +### 第543题-二叉树的直径 + +给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。 + +示例 : +给定二叉树 + + 1 + / \ + 2 3 + / \ + 4 5 +返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。 + +##### 解题思路 +其实根节点的直径就是左节点深度+右节点深度,其他节点也是这个公示,所以可以递归求解出左右节点的深度,然后计算当前节点的直径,然后与左右节点的直径进行判断,返回最大值。 +```java +int maxDiameter = 0; +public int diameterOfBinaryTree(TreeNode root) { + if (root==null){return 0;} + maxDepth(root); + return maxDiameter; +} + +public int maxDepth(TreeNode root) { + if (root==null) {return 0;} + int leftDepth = maxDepth(root.left); + int rightDepth = maxDepth(root.right); + int diameter = leftDepth + rightDepth; + maxDiameter = diameter > maxDiameter ? diameter : maxDiameter; + return leftDepth>rightDepth?leftDepth+1 : rightDepth+1; +} +``` + +### 第79题-单词搜索 +给定一个二维网格和一个单词,找出该单词是否存在于网格中。 + +单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 + + + +示例: + +board = +[ + ['A','B','C','E'], + ['S','F','C','S'], + ['A','D','E','E'] +] + +给定 word = "ABCCED", 返回 true +给定 word = "SEE", 返回 true +给定 word = "ABCB", 返回 false +##### 解题思路 +就是遍历数组,判断每个字符与字符串首字符是否相同,相同的话就继续判断周围的字符是否满足要求。需要注意的时,在判断时会把走过的路径添加到HashSet中去,防止一个位置的字符重复被使用。 +```java +HashSet set = new HashSet<>(); + public boolean exist(char[][] board, String word) { + if (board==null||board[0]==null||word==null||word.length()==0) { + return false; + } + char firstChar = word.charAt(0); + for (int i = 0; i < board.length; i++) { + for (int j = 0; j < board[0].length; j++) { + set = new HashSet<>(); + if(board[i][j] == firstChar + && judgeTheChar(board, word,1,i,j) == true) { + return true; + } + } + } + return false; + } + + boolean judgeTheChar(char[][] board,String word,int index,int i,int j) { + + String key = i + "-" + j; + if (set.contains(key)) { + return false; + } else { + set.add(key); + } + if (index>= word.length()) { + return true; + } + char currentChar = word.charAt(index); + //上 + if (i-1>=0 && + board[i-1][j] == currentChar && judgeTheChar(board, word,index+1,i-1,j) == true) { + return true; + } + //下 + if (i+1=0 && + board[i][j-1] == currentChar + && judgeTheChar(board, word,index+1,i,j-1) == true) { + return true; + } + + if (j+1< board[0].length && + board[i][j+1] == currentChar + && judgeTheChar(board, word,index+1,i,j+1) == true) { + return true; + } + return false; + } +``` + +### 第96题-不同的二叉搜索树 +给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种? + +示例: + +输入: 3 +输出: 5 +解释: +给定 n = 3, 一共有 5 种不同结构的二叉搜索树: + + 1 3 3 2 1 + \ / / / \ \ + 3 2 1 1 3 2 + / / \ \ + 2 1 2 3 + +##### 解题思路 +就是把一个二叉搜索树的组合种数,其实是左子树的组合数乘以右子树的组合数,假设n为4,f(n)代表组合种数,二叉树共有四个节点,那么二叉树肯定有根节点,组合主要分为以下几类: +1.左子树有0个节点,右子树有3个节点,f(0)*f(3) +2.左子树有1个节点,右子树有2个节点,f(1)*f(2) +3.左子树有2个节点,右子树有1个节点,f(2)*f(1) +4.左子树有3个节点,右子树有0个节点,f(3)*f(0) +所以,f(4)=f(0)*f(3)+f(1)*f(2)+f(2)*f(1)+f(3)*f(0) + +而f(0)=1,f(1)=1,f(2)=2 + +```java +public int numTrees(int n) { + int[] array = new int[n+1]; + array[0] = 1; + for (int i = 1; i <= n; i++) { + int num = 0; + for (int j = 0; j <=i-1; j++) { + num += array[j]*array[i-1-j]; + } + array[i] = num; + } + return array[n]; + } +``` + +递归解法 + +```java +int[] cacheArray; +public int numTrees(int n) { + if(n == 0 || n == 1){return 1;} + if(n==2) { + return 2; + } + if(cacheArray == null) { + cacheArray = new int[n+1]; + } else if (cacheArray[n] !=0) { + return cacheArray[n]; + } + int sum = 0; + for(int i = 0; i maxQueue = new LinkedList<>(); + for (int i = 0; i < nums.length; i++) { + //将队列中小于当前数的元素出队列 + while (maxQueue.size()>0 && nums[i] > nums[maxQueue.getLast()]) { + maxQueue.removeLast(); + } + maxQueue.add(i); + //窗口的左边界 + int left = i - k+1; + //将队列中出边界的最大值移除 + if (maxQueue.getFirst() =0) { + result[left] = nums[maxQueue.getFirst()]; + } + } + return result; + } + +``` + +### 第146题-LRU缓存机制 +运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制 。 +实现 LRUCache 类: + +LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 +int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 +void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。 + +##### 解题思路 +LinkedHashMap本身是基于HashMap做了一些扩展,就是通过链表将所有键值对连接起来了,链接的就是添加键值对的顺序。(新添加的键值对在链表尾部,put方法对于已存在的key进行值覆盖时,是不会修改键值对在链表中的顺序的) +所以我们可以基于LinkedHashMap实现LRU。 +LRU的get()方法 +1.map中不存在这个key,返回-1 +2.map中存在这个key,先remove这个键值对,再put操作添加这个键值对,再返回value。(这样可以修改键值对在链表中的顺序。) +LRU的put()方法 +1.已存在这个key,先remove,再put。 +2.不存在这个key,判断是否超过容量,超过将最后一个键值对移除,将键值对添加到map。 +```java +LinkedHashMap map = new LinkedHashMap(); +int capacity; + +public LRUCache(int capacity) { + this.capacity = capacity; +} + +public int get(int key) { + Integer value = map.get(key); + if (value == null) { + return -1; + } + map.remove(key); + map.put(key,value); + return value; +} + +public void put(int key, int value) { + Integer oldValue = map.get(key); + if (oldValue!=null) { + //只是覆盖value的话,put方法不会改变键值对在链表中的顺序,所以需要先remove + map.remove(key); + map.put(key,value); + return; + } + if (map.size()>=capacity) { + map.remove(map.keySet().iterator().next()); + } + map.put(key,value); +} +``` + +### 第236题-二叉树的最近公共祖先 + +给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 + +百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” + +例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4] + +示例 1: + +输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 +输出: 3 +解释: 节点 5 和节点 1 的最近公共祖先是节点 3。 +示例 2: + +输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 +输出: 5 +解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 + +##### 解题思路 +其实所有的节点分为以下几种: +1.就是要寻找的节点1,或节点2 +2.此节点子树中中包含节点1,节点2其中的一个 +3.节点1,节点2全部位于此节点的左子树,或者是右子树 +4.此节点左子树包含节点1,右子树包含节点2 +所以第4种就是我们要寻找的节点,并且在二叉树中只有一个,所以我们对二叉树进行遍历,判断某个节点左子树,右子树都包含节点,那么就返回该节点。 + +```java +TreeNode lowestCommonAncestor(TreeNode root, TreeNode node1, TreeNode node2) { + if (root==null) { + return null; + } + if (root==node1 || root==node2) {//当前节点就是要找的节点之一 + return root; + } + TreeNode leftNode = lowestCommonAncestor(root.left,node1,node2);//判断左子树中是否有节点 + TreeNode rightNode = lowestCommonAncestor(root.right,node1,node2);//判断右子树中是否有节点 + if (leftNode!=null&&rightNode!=null) {//就是我们要找的节点 + return root; + } else if (leftNode!=null && rightNode==null) {//左子树中有节点,右子树没有节点,继续向上遍历 + return leftNode; + } else if (leftNode==null && rightNode!=null) {//继续向上遍历 + return rightNode; + } + return null; + } +``` + +### 第114题-二叉树展开为链表 + +给定一个二叉树,原地将它展开为一个单链表。 + +例如,给定二叉树 + +​ 1 + + / \ + 2 5 + / \ \ +3 4 6 +将其展开为: + +1 + \ + 2 + \ + 3 + \ + 4 + \ + 5 + \ + 6 + +##### 解题思路 + +其实就是先序遍历,需要注意的是,需要先将节点的左右节点保存,然后再进行修改操作。其次是需要将所有节点的left指针置为null. + + +```java + TreeNode lastNode;//lastNode用于保存上一个遍历的节点 + public void flatten(TreeNode root) { + if(root == null) {return;} + TreeNode left = root.left; + TreeNode right = root.right; + if(lastNode==null) { + lastNode = root; + lastNode.left=null; + } else { + lastNode.left=null; + lastNode.right = root; + lastNode=root; + } + flatten(left); + flatten(right); + } +``` + +### 第84题-柱状图中最大的矩形 + +给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 + +求在该柱状图中,能够勾勒出来的矩形的最大面积。 + +![img](../static/histogram.png) + + + +以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。 + +![img](../static/histogram_area.png) + + + +图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。 + +示例: + +输入: [2,1,5,6,2,3] +输出: 10 + +##### 解题思路 + +可以暴力求解,就对每个height[i]都计算包含它的元素的最大高度,这样复杂度是O(N^2),计算的方法主要是对于每个i找到左边最后一个大于height[i]的边界left,和右边最后一个大于height[i]的边界right, + +矩形面积=height[i]*(right-left+1),例如对于第五个柱子值为2的那个柱子来说,左边界left就是值为5的柱子,右边界就是值为3的柱子。 + +单调栈解法 + +理论就是假设f(i)代表包含圆柱i的最大矩形,那么其实就是在圆柱i的左边和右边各自找到第一个高度更小的圆柱k和j,f(i) = height[i]*(j-k+1)。所以可以使用单调栈来保存比当前圆柱高度height[i]小的元素,如果栈中元素高度比height[i]大,那么那些元素就需要出栈了。 + + + +可以使用一个栈Stack来保存左边圆柱高度比height[i]小的元素,也就是Stack中的值只能是比height[i]小的元素。栈底元素是最小的,栈顶元素高度是最大的。 + +遍历时,如果当前height[i]>栈顶元素的高度,说明当前栈顶元素还没有碰到比它小的数,所以还不能计算面积,就把height[i]入栈。 + +如果现在的height[i]<栈顶的元素高度小,说明栈顶元素碰到比它小的元素了,就需要出栈计算矩形面积, + + 面积=heights[j] * (i - left) + +```java +public int largestRectangleArea(int[] heights) { + if (heights==null||heights.length==0) { + return 0; + } + int maxArea = 0; + Stack stack = new Stack(); + stack.add(0); + for (int i = 1; i <= heights.length; i++) { + int currentH; + if (i==heights.length) { + //这是最后新增了一个0元素 + //防止整个数组是全体递增的,这样会计算不出面积 + currentH = 0; + } else { + currentH = heights[i]; + } + + if(currentH > heights[stack.peek()]) { + stack.push(i); + } else if (currentH == heights[stack.peek()]) { + stack.pop(); + stack.push(i); + } else {//当heights[i] < stack.peek()时,进行出栈计算 + while (stack.size()>0 && currentH0) { + leftIndex = stack.peek() + 1; + } else { + leftIndex = 0; + } + int area = heights[index] * (i - leftIndex); + maxArea = maxArea > area ? maxArea : area; + } + stack.push(i); + } + } + return maxArea; + } +``` + +### 第148题-排序链表 + +给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。 + +进阶: + +你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗? + +示例 1: + +![img](../static/sort_list_1.jpg) + + +输入:head = [4,2,1,3] +输出:[1,2,3,4] + +##### 解题思路 + +就是归并排序 + +```java + ListNode preHead = new ListNode(1); + ListNode sortList(ListNode start) { + //归并排序划分到只有一个节点时,直接返回 + if (start.next == null || start==null) {return start;} + ListNode quick = start; + ListNode slow = start; + while (quick!=null) { + quick=quick.next; + if (quick.next==null){break;} + quick = quick.next; + slow = slow.next; + } + //4 2 1 3 + ListNode otherStart = slow.next; + slow.next = null; + //对链表分解和合并后,新链表的头结点不一定还是start + start = sortList(start); + otherStart = sortList(otherStart); + //开始merge + //preHead用于我们来保存新组成的链表的头结点 + ListNode currentNode = preHead; + while (start != null && otherStart!=null) { + if (start.valvalue?max:value; + times =0; + } + } + return max; + } + public int maxProduct(int[] nums,int start,int end,int times) { + if (start>end) { + return 0; + } + if (start==end) { + return nums[start]; + } + if (times%2 == 0) {//负数为偶数个 + return calculateValue(nums,start,end); + } else {// + //第一个负数的下标 + Integer firstNegativeIndex=null; + //最后一个负数的下标 + Integer lastNegativeIndex=null; + for (int i = start; i <= end; i++) { + if (nums[i] < 0 && firstNegativeIndex == null) { + firstNegativeIndex = i; + } + if (nums[i] < 0) { + lastNegativeIndex = i; + } + } + int vaule1 = calculateValue(nums,start,lastNegativeIndex-1); + int value2 = calculateValue(nums,firstNegativeIndex+1,end); + return vaule1>value2?vaule1:value2; + } + } + //计算start到end之间的元素乘积和 + int calculateValue(int[] nums, int start, int end) { + if (start>end) { + return 0; + } + int result = 1; + for (int i = start; i <= end; i++) { + result*=nums[i]; + } + return result; + } +``` + +### 第72题-编辑距离 + +给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 + +你可以对一个单词进行如下三种操作: + +插入一个字符 +删除一个字符 +替换一个字符 + + +示例 1: + +输入:word1 = "horse", word2 = "ros" +输出:3 +解释: +horse -> rorse (将 'h' 替换为 'r') +rorse -> rose (删除 'r') +rose -> ros (删除 'e') + +##### 解题思路 +就是我们要把word1转换为word2,从word2第一个字符开始遍历,对于每一个字符而言,i,j分别为word1和word2当前遍历的位置有四种选择: +##### 1.直接跳过 +当words[i]与word[j]相同时,简单举例来说,就是假设要把"acc"转换为"abb",由于第一个字符相同,那么编辑的步数其实是等于"cc"转换为"bb"的步数 +所以假设使用restLength[i][j]代表剩余子串需要的步数, +``` +restLength[i][j] = restLength[i+1][j+1] +``` +##### 2.将word1[i]删除 +假设我们要将"abcd"转换为"bc",那么其实我们可以将a删除掉,相当于对于word1跳过了当前字符,继续遍历 +``` +restLength[i][j] = restLength[i+1][j] +``` +##### 3.在word1[i]前增加新字符word2[j] +假设我们要将"bbb"转换为"abbb",那么就可以对于word1增加一个word2当前的字符,然后继续遍历,相当于是对word2的字符跳过了 +``` +restLength[i][j] = restLength[i][j+1] +``` +##### 4.对word1[i]替换成word2[j] + +假设我们要将"abbb"转换为"cbbb",那么直接将a替换成c就好了,相当于对word1和word2字符串都跳过了当前字符 +``` +restLength[i][j] = restLength[i+1][j+1] +``` +代码 +```java + int[][] cacheLength; + public int minDistance(String word1, String word2) { + if (word1==null||word2==null) { + return 0; + } + cacheLength = new int[word1.length()+1][word2.length()+1]; + return minDistance(word1,word2,0,0); + } + + public int minDistance(String word1, String word2,int i,int j) { + //当有一个字符串已经走到末尾了,要变成另一个字符串只能是将另一个字符串剩余字符全部添加过来 + if (cacheLength[i][j]!=0) { + return cacheLength[i][j]; + } + if (i == word1.length()) { + return word2.length() - j; + } + if (j == word2.length()) { + return word1.length() - i; + } + if (word1.charAt(i) == word2.charAt(j)) {//相等就直接向后移动 + return minDistance(word1,word2,i+1,j+1); + } + //这一步选择删除,那么就是将word1的当前字符删除 + int deleteLength = minDistance(word1,word2,i+1,j) + 1; + //这一步选择插入,就是将word2的当前字符插入到word1 + int insertLength = minDistance(word1,word2,i,j+1) + 1; + //这一步走替换,就是将word2的当前字符替换到word1 + int replaceLength = minDistance(word1,word2,i+1,j+1) + 1; + //选择上面三种路线中最短的 + int min = deleteLength < insertLength ? deleteLength : insertLength; + min = replaceLength < min ? replaceLength : min; + cacheLength[i][j] = min; + return min; + } +``` + +### 第139题-单词拆分 +给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 + +说明: + +拆分时可以重复使用字典中的单词。 +你可以假设字典中没有重复的单词。 +示例 1: + +输入: s = "leetcode", wordDict = ["leet", "code"] +输出: true +解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。 +示例 2: + +输入: s = "applepenapple", wordDict = ["apple", "pen"] +输出: true +解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 +  注意你可以重复使用字典中的单词。 + +##### 解题思路 +就是对于字符串"abc"来说,可以拆分成"a"和"bc",如果说字典中存在"a",我们只需要递归调用函数对剩余字符"bc"进行判断,判断"bc"是否能分解。最坏复杂度应该是O(N^2)。这里优化的点就是每次判断时,如果计算得到子串"bc"不能被分解,我们应该将它添加到falseSet,避免重复计算。 +```java +HashSet falseSet = new HashSet(); + public boolean wordBreak(String s, List wordDict) { + if (s==null||s.length()==0) {return true;} + if (falseSet.contains(s)) { + return false; + } + HashSet wordSet = new HashSet<>(wordDict); + for (int i = 0; i < s.length(); i++) { + //当前子串 + String subString = s.substring(0,i+1); + //剩余子串 + String restString = s.substring(i+1,s.length()); + //如果剩余子串存在,那么就分解成功了 + if (wordSet.contains(subString) && wordBreak(restString, wordDict)) { + return true; + } + } + //分解不成功,将子串添加到falseSet,避免重复计算 + falseSet.add(s); + return false; + } +``` + +### 第76题-最小覆盖子串 + +给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。 + +注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。 + +示例 1: + +输入:s = "ADOBECODEBANC", t = "ABC" +输出:"BANC" + +##### 解题思路 + +首先本题的最小子串是不需要保证子串的顺序的,也就是子串是ABC,我们的最小覆盖子串是BANC也可以,不一定非需要保证ABC的顺序。其实就是滑动窗口,我们用一个needMap来记录,key是需要查找的子串的字符,value是字符从次数。然后就用两个指针作为滑动窗口来遍历字符串,滑动窗口中字符及出现的次数用windowMap来存储, + +1.然后每次右指针移动,获取新进入窗口的字符,更新windowMap,如果当前字符是needMap中存在的,且windowMap该字符出现次数已达标,那么就更新needSize(子串中的字符在窗口中出现的次数)。 + +2.左指针进行移动,判断当前字符是否能移除窗口,(如果是子串需要的,且在窗口出现的次数<=子串需要的次数,就不能移除) + +3.判断当前needSize是否达标,达标说明当前窗口就是一个覆盖子串,如果比之前最小的覆盖子串小,那么就进行替换。 + +```java +public String minWindow(String s, String t) { + //needMap的key就是字符串t中出现的每个字符,value就是这个字符出现的次数 + HashMap needMap = new HashMap<>(); + //windowMap就是滑动窗口中当前字符及字符出现次数 + HashMap windowMap = new HashMap<>(); + for(int i = 0;i leftNeedTimes) {//需要这个字符,且所包含该字符个数大于需要的,可以左移 + windowMap.put(currentLeftChar,leftWindowTimes-1); + left++; + } else if(leftWindowTimes <= leftNeedTimes){//需要该字符,并且窗口不能左移 + break; + } + } + //判断当前窗口是否满足需求 + if(needSize >= t.length() && (minStr.equals("") || right-left+1 < minStr.length())) { + minStr = s.substring(left,right+1); + } + right++; + } + return minStr; + } + +``` + +### 第124题-二叉树中的最大路径和 +给定一个非空二叉树,返回其最大路径和。 + +本题中,路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。 + +示例 1: + +输入:[1,2,3] + + 1 + / \ + 2 3 + +输出:6 +示例 2: + +输入:[-10,9,20,null,null,15,7] + +  -10 +   / \ +  9  20 +    /  \ +   15   7 + +输出:42 +##### 解题思路 +这个题由于不要求路径开始和路径结束的节点是叶子节点,所以是有点复杂,我们可以先定义一个函数,这个函数可以计算每个节点单条路径的长度,就是可以是节点本身,也可以是节点+左子树中的节点连接,也可以是节点+左子树中的节点连接,但是节点不能既连接左节点,由连接右节点。在遍历过程中,可以得到(根节点,根节点+左子树路径最大值,根节点+右子树路径最大值)三者的最大值,然后进行返回。在遍历的过程中,由于我们知道左子树路径最大值和右子树路径最大值,我们可以计算以该节点为根节点的路径最大值,然后与totalMax进行比较和替换。 + +```java +//因为最大路径出现时,肯定是有一个根节点的,所以在计算单条路径时, + // 可以同时计算最大路径和,然后存储到totalMax + Integer totalMax = Integer.MIN_VALUE; + public int maxPathSum(TreeNode root) { + siglePath(root); + return totalMax; + } + + //这里计算的是单条路径长度,就是根节点+左路+右路的 + // 根节点,根节点+左节点中的路径,根节点+右节点中的路径 + //但是不能是左节点中的路径+根节点+右节点的路径 + public int siglePath(TreeNode root) { + if (root == null) { + return 0; + } + //左边单条路径的长度 + int leftPath = siglePath(root.left); + leftPath = leftPath>0?leftPath:0; + int rightPath = siglePath(root.right); + rightPath = rightPath>0?rightPath:0; + //根节点+左路大,还是根节点+右路大 + int max = leftPath > rightPath ? + leftPath + root.val : rightPath + root.val; + //实时计算左路单条路径+根节点+右路单条路径哪个大 + totalMax = leftPath+rightPath+root.val > totalMax ? leftPath+rightPath+root.val : totalMax; + return max; + } +``` + +### 第461题-汉明距离 + +两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。 +给出两个整数 x 和 y,计算它们之间的汉明距离。 +注意: +0 ≤ x, y < 2的31次方. + +示例: + +输入: x = 1, y = 4 + +输出: 2 + +解释: +1 (0 0 0 1) +4 (0 1 0 0) + ↑ ↑ +上面的箭头指出了对应二进制位不同的位置。 + +##### 解题思路 + +```java +public int hammingDistance(int x, int y) { + int result = x^y; + int num=0; + while(result>0) { + if((result&1) == 1) { + num++; + } + result = result>>1; + } + return num; +} +``` + +### 第128题-最长连续序列 + +给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 +进阶:你可以设计并实现时间复杂度为 O(n) 的解决方案吗? +示例 1: +输入:nums = [100,4,200,1,3,2] +输出:4 +解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 +示例 2: +输入:nums = [0,3,7,2,5,8,4,6,0,1] +输出:9 + +##### 解题思路 +就是先使用HashMap存储各个值,然后遍历键值对,判断每个键值对周围的数字是否存在。这里做的优化点就是HashMap的key存的就是数组中出现的元素,value存的是一个区间,代表的是连续数组的左右边界。 +```java +public int longestConsecutive(int[] nums) { + HashMap> map = new HashMap<>(); + //遍历数组,key就是数组中的元素,value记录的是连续子数组的左右边界 + //value初始区间只包含一个元素,就是[key,key] + for (int i = 0; i < nums.length; i++) { + List list = new ArrayList<>(); + list.add(nums[i]); + list.add(nums[i]); + map.put(nums[i],list); + } + Integer max = 0; + //根据每个key去判断左右元素是否存在,计算最大区间 + for (Integer key: map.keySet()) { + List list = map.get(key); + int left = list.get(0)-1; + int right = list.get(1)+1; + //向左扩展 + while (map.containsKey(left)) { + List currentList = map.get(left); + list.set(0,currentList.get(0)); + left = currentList.get(0) - 1; + } + //向右扩展 + while (map.containsKey(right)) { + List currentList = map.get(right); + list.set(1,currentList.get(1)); + right = currentList.get(1) + 1; + } + int currentValue = list.get(1) - list.get(0) + 1; + max = max > currentValue ? max : currentValue; + } + return max; + } +``` + +### 第647题-回文子串 +给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。 +具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。 + +示例 1: +输入:"abc" +输出:3 +解释:三个回文子串: "a", "b", "c" + +示例 2: +输入:"aaa" +输出:6 +解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa" +##### 解题思路 +这个题本身没有什么技巧,就是就是对每个字符,从中心往两边扩展,判断是否是回文串,一旦发现不是回文串,后面就不需要继续扩展了。需要注意的是,回文串分为奇数回文串,偶数回文串,所以中心可以是当前单个字符,也可以是当前字符+右边的字符。 +```java + int num=0; + public int countSubstrings(String s) { + char[] array = s.toCharArray(); + for (int i = 0; i < array.length; i++) { + //奇数回文串 + calculateNum(array,i,i); + //偶数回文串 + calculateNum(array,i,i+1); + } + return num; + } + + void calculateNum(char[] array,int start,int end) { + while (start>=0 && end value2 ? value1 : value2; + } else {//当前根节点不能被选择 + max = rob(root.left,true) + rob(root.right,true); + } + return max; + } +``` + +### 第238题-除自身以外数组的乘积 + +给你一个长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。 + +示例: + +输入: [1,2,3,4] +输出: [24,12,8,6] + +##### 解题思路 + +就是对于位置i,本题要求去求nums[0]*nums[1]...nums[i-1]nums[i+1]...nums[length-1],不能用除法,认识要求的这个乘积可以分成两部分,一部分是i以前的部分,nums[0]*nums[1]...nums[i-1],一部分是nums[i+1]...nums[length-1],所以可以先分别计算出left数组,对于每个元素存在0到i的乘积,计算right数组,对于每个元素,计算i到length-1的乘积。 + +```java + public int[] productExceptSelf(int[] nums) { + int[] left = new int[nums.length]; + int[] right = new int[nums.length]; + int[] result = new int[nums.length]; + int temp =1; + //left[i]存储的是nums[0]到nums[i]的乘积 + for (int i = 0; i < nums.length; i++) { + temp = temp*nums[i]; + left[i] = temp; + } + temp = 1; + //right[i]存储的是nums[i]到nums[length-1]的乘积 + for (int i = nums.length-1; i >=0 ; i--) { + temp = temp * nums[i]; + right[i] = temp; + } + for (int i = 0; i < nums.length; i++) { + int leftValue = i-1 >= 0 ? left[i-1] : 1; + int rightValue = i+1 < nums.length ? right[i+1] : 1; + result[i] = leftValue * rightValue; + } + return result; + } +``` + +### 第207题-课程表 + +你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。 + +在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1] + +给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习? + +示例 1: + +输入: 2, [[1,0]] +输出: true +解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。 +示例 2: + +输入: 2, [[1,0],[0,1]] +输出: false +解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。 + +##### 解题思路 +其实就是判断有向图是否存在环,有两种解法 +##### 深度优先遍历 +就是先根据二维数组构建一个邻接表,这里我们使用一个map来作为领接表,然后递归遍历map中的节点,对于图中每个节点有三种状态: + +1.未被访问过(在statusMap中值为null)。 + +2.已被访问过,并且它的子节点没有遍历访问完成(在statusMap中值为1)。 + +3.已被访问过,并且子节点也遍历访问完成(在statusMap中值为2)。 + +在递归遍历过程中,遇到上面第2种节点,说明就存在环。 + +```java +public boolean canFinish(int numCourses, int[][] prerequisites) { + HashMap> map = new HashMap<>(); + //构建邻接表 + for (int i = 0; i < prerequisites.length; i++) { + Integer key = prerequisites[i][0]; + List valueList = map.get(key); + if (valueList == null) { + valueList = new ArrayList<>(); + } + valueList.add(prerequisites[i][1]); + map.put(key,valueList); + } + HashMap statusMap = new HashMap<>(); + for (Integer key : map.keySet()) { + if (judgeIfHasCircle(map,statusMap,key)) {//有环 + return false; + } + } + return true; + } + //判断每个节点是否存在环 + boolean judgeIfHasCircle(HashMap> map, HashMap statusMap,Integer key) { + Integer status = statusMap.get(key); + if (status== null) { + statusMap.put(key,1); + } else if (status==1) { + return true; + } else if (status == 2) { + return false; + } + + List valueList = map.get(key); + if (valueList!=null) { + //遍历子节点 + for (Integer everyKey : valueList) { + if (judgeIfHasCircle(map, statusMap, everyKey)) { + return true; + } + } + } + //代表子节点遍历完毕 + statusMap.put(key,2); + return false; + } +``` +##### 拓扑排序 +这种解法有点像是宽度优先遍历,就是先建立邻接表,并且计算每个节点的入度,然后找到入度为0的节点(也就是没有被其他节点指向的节点),将它们入队列,然后对队列元素进行出队操作,取出队首元素,将它的子节点的入度都-1,然后子节点入度减到0时,就将这个子节点添加到队列中,在过程中会统计入过队列的节点数。原理就是如果没有环的,最终队列出队完成后,进入过队列的节点数是等于总节点数的。就是假设图的结构是1->2,2->3,3->4,4->2,也就是2,3,4形成一个环,最开始1是入度为0的节点,1会入队列,然后对节点2的入度-1,节点2的入度还剩下1,此时2不会入队列,所以最终进过队列的元素只有节点1,所以最终统计的数量是<总节点数的(如果不存在环,则所有节点的入度都会变成0,也就是结果集中的节点树会等于总节点数)。 + +### 第309题-最佳买卖股票时机含冷冻期 + +给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​ + +设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): + +你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 +卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 + +``` java +dp[i][0]//代表不持有股票 +dp[i][1]//代表持有股票 + + //如果今天不持有股票,要么是之前没有股票,要么是卖了股票 + dp[i][0] = max(dp[i-1][0],dp[i-1][1] + value[i]) + //如果今天持有股票,要么是之前就持有,要么是今天新买的 + dp[i][1] = max(dp[i-1][1],dp[i-2][0] - value[i]) + + +``` + + + +示例: + +输入: [1,2,3,0,2] +输出: 3 +解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出] + +##### 解题思路 + +这个跟上一题的区别就是有冷冻期,就是当你第i天要持有股票时,要么是第i-1天已持有股票,要么是第i-1天没有买卖股票才能在第天买股票。 +卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 + +```java + //如果今天不持有股票,要么是之前没有股票,要么是第i天卖了股票 + dp[i][0] = max(dp[i-1][0],dp[i-1][1] + value[i]) + //如果今天持有股票,要么是之前就持有,要么是第i天新买的 + dp[i][1] = max(dp[i-1][1],dp[i-2][0] - value[i]) +``` + +代码 + +```java + public int maxProfit(int[] prices) { + if(prices==null||prices.length<=1) {return 0;} + int[][] dp = new int[prices.length][2]; + dp[0][0] = 0; + dp[0][1] = 0 - prices[0]; + dp[1][0] = prices[1]-prices[0] < 0 ? 0 : prices[1]-prices[0]; + dp[1][1] = 0-prices[1] > 0- prices[0]? 0-prices[1] : 0-prices[0]; + for (int i = 2; i < prices.length; i++) { + dp[i][0] = Math.max(dp[i-1][1]+prices[i], dp[i-1][0]); + dp[i][1] = Math.max(dp[i-2][0]-prices[i],dp[i-1][1]); + } + return dp[prices.length-1][0]; + } +``` +这一题的时间复杂度为O(N),空间复杂度也为O(N),有一个可以优化的点就是d[i]只依赖于dp[i-1]和dp[i-2],所以理论上我们只需要一个几个常数变量就可以了,空间复杂度为O(1); +```java +public int maxProfit1(int[] prices) { + if(prices==null||prices.length<=1) {return 0;} + int last_last_0 = 0; + int last_0 = prices[1]-prices[0] < 0 ? 0 : prices[1]-prices[0]; + int last_1 = 0-prices[1] > 0- prices[0]? 0-prices[1] : 0-prices[0]; + for (int i = 2; i < prices.length; i++) { + int temp_0 = last_0; + last_0 = Math.max(last_1+prices[i], last_0); + last_1 = Math.max(last_last_0-prices[i],last_1); + last_last_0 = temp_0; + } + return last_0; + } +``` + +### 第416题-分割等和子集 +给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +注意: +每个数组中的元素不会超过 100 +数组的大小不会超过 200 +示例 1: +输入: [1, 5, 11, 5] +输出: true +解释: 数组可以分割成 [1, 5, 5] 和 [11]. +##### 解题思路 +本题就是可以转化为从数组中挑选i个元素,最终使和为数组和的一半,所以就转换为01背包问题了,只不过判断条件由选择让装的物品价值更大,变为装的物品的价值正好是总价值的一半。 +```java +public boolean canPartition(int[] nums) { + if (nums==null||nums.length==0) { + return false; + } + int sum = 0; + for (int i = 0; i < nums.length; i++) { + sum+=nums[i]; + } + if (sum%2==1) { + return false; + } else { + return canPartition(nums,nums.length-1,sum/2); + } + } + //使用HashMap缓存结果,避免重复计算 + HashMap resultCacheMap = new HashMap(); + //判断在0到i-1这些元素中进行能选择,看能否选择出的元素和为sum + public boolean canPartition(int[] nums,int i,int sum) { + if (sum<0) { return false; } + if (sum==0) { return true; } + if (i<0) { return false; } + if (i==0) {//只有一个元素了 + return nums[0]==sum; + } + String key = i+"-"+sum; + if (resultCacheMap.containsKey(key)) { + return resultCacheMap.get(key); + } + //选择元素i,看剩下的元素能否凑出sum 和不选择元素i + boolean result = canPartition(nums,i-1,sum-nums[i]) || canPartition(nums,i-1,sum); + resultCacheMap.put(key,result); + return result; + } +``` + +### 第560题-和为K的子数组 +给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。 +示例 1 : +输入:nums = [1,1,1], k = 2 +输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。 +说明 : +数组的长度为 [1, 20,000]。 +数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。 +##### 解题思路 +假设一个数组nums的元素为[a1,a2,a3,a4],假设我们使用f(a,b)代表从数组下标a到数组下标b的连续子数组和,f(a,b)=f(0,b)-f(0,a),也就是说假设子数组的和[a2,a3]=[a1,a2,a3]-[a1],所以k = [a1,a2,a3]-[a1],我们判断和为k的数量,其实也就判断从0开始的子树组之间的差为k的数量,所以我们计算将从下标为0的数组的和,添加到HashMap中去,然后遍历时进行判断。 +```java + public int subarraySum(int[] nums, int k) { + if (nums==null||nums.length==0) { return 0; } + int matchTimes=0; + int sum=0; + HashMap sumMap = new HashMap<>(); + //这个主要是为了统计为从0到i的和为sum的子数组 + sumMap.put(0,1); + for (int i = 0; i < nums.length; i++) { + sum+=nums[i]; + int key = sum - k; + if (sumMap.containsKey(key)) { + matchTimes+=sumMap.get(key); + } + //将当前sum和添加到map中去 + Integer times = sumMap.get(sum); + if (times==null) { + times=0; + } + sumMap.put(sum,times+1); + } + return matchTimes; + } +``` + +### 第448题-找到所有数组中消失的数字 +给定一个范围在  1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。 +找到所有在 [1, n] 范围之间没有出现在数组中的数字。 +您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。 +示例: + +输入: +[4,3,2,7,8,2,3,1] + +输出: +[5,6] +##### 解题思路 +这个题因为数字a的取值都是[1,nums.length]之间,所以a-1应该是在[0,nums.length-1]之间,正好跟数组的下标可以对应上,所以对数组遍历,将数字a放到下标a-1下,如果 +1.当前数字a如果为-1那么就不用调整位置了,因为这是我们设置的标志位,如果a正好等于下标i+1,也不用调整,因为是正确的位置 +2.如果下标a-1正好存的也是a,那么说明是出现两次的元素,那么将那个位置标志位-1,也不用调整了 +3.将当前元素a与下标a-1的元素交换,继续遍历。 +```java +public List findDisappearedNumbers(int[] nums) { + List list = new ArrayList<>(); + if (nums==null||nums.length==0) { + return list; + } + for (int i = 0; i < nums.length; i++) { + int index = nums[i] - 1; + if (nums[i] == -1 || nums[i] == i+1) {//说明是未出现过的数字,或者是已经调整到正确位置的数字,直接跳过 + continue; + } else if (nums[index] == index+1) {//说明这个位置已经有这个元素了,是出现两次的元素 + nums[i] = -1; + } else {//否则是出现一次的元素,进行交换 + int temp = nums[index]; + nums[index] = nums[i]; + nums[i] = temp; + i--; + } + } + for (int i = 0; i < nums.length; i++) { + if (nums[i] == -1) { + list.add(i+1); + } + } + return list; + } +``` + +### 第437题-路径总和III + +给定一个二叉树,它的每个结点都存放着一个整数值。 + +找出路径和等于给定数值的路径总数。 + +路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。 + +二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。 + +示例: + +root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 + + 10 + / \ + 5 -3 + / \ \ + 3 2 11 + / \ \ +3 -2 1 + +返回 3。和等于 8 的路径有: + +1. 5 -> 3 +2. 5 -> 2 -> 1 +3. -3 -> 11 + +##### 解题思路 +其实就是对每个节点作为起始节点,开始向下遍历,计算路径和,每当路径和为sum时,就对数量+1。 +``` +int pathNum = 0; + //这个方法主要负责对二叉树遍历 + public int pathSum(TreeNode root, int sum) { + if (root==null) { return 0; } + //必须包含根节点的 + pathSumMustHasRoot(root,sum,0); + //不包含根节点的 + pathSumMustHasRoot(root.left,root.val); + pathSumMustHasRoot(root.right,root.val); + return pathNum; + } + //对每个节点计算路径和,然后继续向下 + void pathSumMustHasRoot(TreeNode root,int sum,int currentSum) { + if (root == null) {return ;} + currentSum+=root.val; + if (currentSum == sum) { + pathNum++; + } + pathSumMustHasRoot(root.left,sum,currentSum); + pathSumMustHasRoot(root.right,sum,currentSum); + } +``` + +### 第338题-比特位计数 +给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。 + +示例 1: + +输入: 2 +输出: [0,1,1] +示例 2: + +输入: 5 +输出: [0,1,1,2,1,2] +##### 解题思路 +因为i&(i-1)的结果相当于是去掉了最右边的一个1,所以 +i中1的数量 = i&(i-1)中1的数量 + 1,所以可以使用一个数组保存以前的数的1的数量,这样就可以以O(1)的时间复杂度计算中1的数量。 +```java +public int[] countBits(int num) { +//i & (i-1)可以将最右边的0去掉 + int[] bitCountArray = new int[num+1]; + bitCountArray[0] = 0; + for (int i = 1; i <= num; i++) { + bitCountArray[i] = bitCountArray[i&(i-1)] +1; + } + return bitCountArray; + } +``` + +### 第406题-根据身高重建队列 +假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。 + +请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。 + +示例 1: + +输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] +输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] +解释: +编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 +编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 +编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 +编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 +编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 +编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 +因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。 + +##### 解题思路 +```java + public int[][] reconstructQueue(int[][] people) { + if (people==null||people[0]==null) { + return null; + } + //按照身高从大到小排列,身高相同,k从小到大排列 + //排序前:[[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] + //排序后:[[7,0],[7,1],[6,1],[5,2],[5,0],[4,4]] + Arrays.sort(people, new Comparator() { + @Override + public int compare(int[] o1, int[] o2) { + // return o1[0] != o2[0] ? o1[0]-o2[0] : o2[1] - o1[1]; + return o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0]; + } + }); + List list = new ArrayList<>(people.length); + for (int[] i : people) { + list.add(i[1],i); + } + return list.toArray(new int[list.size()][2]); + } +``` + +### 第538题-把二叉搜索树转换为累加树 + +给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。 + +提醒一下,二叉搜索树满足下列约束条件: + +节点的左子树仅包含键 小于 节点键的节点。 +节点的右子树仅包含键 大于 节点键的节点。 +左右子树也必须是二叉搜索树。 +注意:本题和 1038: https://leetcode-cn.com/problems/binary-search-tree-to-greater-sum-tree/ 相同 + +![img](../static/tree.png) + +示例 1: + +输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8] +输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8] + +##### 解题思路 + +这个题其实就是对二叉树按照右子树-根节点+左子树的顺序进行遍历,并且记录之前遍历的节点的和,然后当前节点值=之前的节点和+当前值 + +```java + int sum = 0; + public TreeNode convertBST(TreeNode root) { + if (root==null) return null; + convertBST(root.right); + sum +=root.val; + root.val = sum; + convertBST(root.left); + return root; + } +``` + +### 第297题-二叉树的序列化与反序列化 + +序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。 + +请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。 + +示例:  + +你可以将以下二叉树: + +​ 1 + + / \ + 2 3 + / \ + 4 5 + +序列化为 "[1,2,3,null,null,4,5]" +提示: 这与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。 + +说明: 不要使用类的成员 / 全局 / 静态变量来存储状态,你的序列化和反序列化算法应该是无状态的。 + +##### 解题思路 +```java + + String serialize(TreeNode root) { + StringBuffer stringBuffer = new StringBuffer(); + if (root == null) {return stringBuffer.toString();} + ArrayList queue = new ArrayList(); + queue.add(root); + while (queue.size()>0) { + TreeNode node = queue.remove(0); + if (node == null) { + stringBuffer.append("#!"); + } else { + stringBuffer.append(node.val+"!"); + queue.add(node.left); + queue.add(node.right); + } + } + return stringBuffer.toString(); +} + +TreeNode deserialize(String str) { + if (str == null || str.length() == 0) {return null;} + String[] array = str.split("!"); + + Integer rootValue = convert(array[0]); + if (rootValue == null) {return null;} + + TreeNode rootNode = new TreeNode(rootValue); + ArrayList queue = new ArrayList(); + queue.add(rootNode); + int currentIndex = 1; + while (queue.size()>0 && currentIndex findAnagrams(String s, String p) { + List list = new ArrayList<>(); + if (s==null||p==null) { + return list; + } + HashMap needMap = new HashMap<>(); + for (int i = 0; i < p.length(); i++) { + Character c = p.charAt(i); + Integer times = needMap.get(c); + times = times == null ? 1 : times + 1; + needMap.put(c,times); + } + int left=0; + int right =0; + int valid_num = 0; + HashMap windowsMap = new HashMap<>(); + while (left<=right && right目标值,需要排除更大的数,由于这一列都是比目标值大的,都需要排除掉。 + +3.当前值<目标值,需要排除更小的数,由于这一行都是比目标值小的,都需要排除掉。 + +```java +public boolean searchMatrix(int[][] matrix, int target) { + if (matrix==null||matrix[0]==null) { + return false; + } + int rowength = matrix.length; + int colLength = matrix[0].length; + int i = 0,j = colLength-1; + while (i < rowength && j >=0) { + if (matrix[i][j] == target) { + return true; + } else if (matrix[i][j] > target) {//当前值>目标值,需要排除更大的数 + j--;//排除这一列 + } else if (matrix[i][j] < target) {//当前值<目标值,需要排除更小的数 + i++;//排除这一行 + } + } + return false; + } +``` + +### 第494题-目标和 + +给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。 + +返回可以使最终数组和为目标数 S 的所有添加符号的方法数。 +示例: + +输入:nums: [1, 1, 1, 1, 1], S: 3 +输出:5 +解释: + +-1+1+1+1+1 = 3 ++1-1+1+1+1 = 3 ++1+1-1+1+1 = 3 ++1+1+1-1+1 = 3 ++1+1+1+1-1 = 3 + +一共有5种方法让最终目标和为3。 +##### 解题思路 +假设前面添加+的数组成的子数组为x,前面加-的数组成的子数组为y, +那么 +```java +x+y=sum //sum为数组之和 +x-y=S +``` +所以x= (sum+S)/2,所以问题变成了从数组中选取一些数,它们的和的为(sum+S)/2,所以转换为01背包问题了,变成从n个物品中取x个物品,正好价值等于某个数。 +```java + int result =0; + public int findTargetSumWays(int[] nums, int S) { + if (nums==null||nums.length==0) {return 0;} + int sum=0; + for (int i = 0; i < nums.length; i++) { + sum+=nums[i]; + } + if ((sum + S)%2==1) {//是奇数,不可能有结果 + return 0; + } + findTargetSumWays(nums,(sum+S)/2,nums.length-1); + return result; + } + public void findTargetSumWays(int[] nums, int sum,int i) { + if (sum<0) { + return; + } + //到最后一个元素了,不能继续递归选择了 + if (i==0) { + //如果等于sum,那么选择元素i,将result+1, + //如果sum为0,那么不选择元素i,将result+1 + if (nums[i] == sum) { + result++; + } + if (sum==0) { + result++; + } + return; + } + //不选这个元素 + findTargetSumWays(nums,sum,i-1); + //选这个元素 + findTargetSumWays(nums,sum-nums[i],i-1); + } +``` +### 第621题-任务调度器 +给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。 + +然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。 + +你需要计算完成所有任务所需要的 最短时间 。 +示例 1: + +输入:tasks = ["A","A","A","B","B","B"], n = 2 +输出:8 +解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B + 在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。 + +##### 解题思路 +假设没有某个时间点需要待命,那么任务所需要的最短时间就是任务的总数量,就是tasks.length。如果有某个时间点需要待命,那么一定是出现次数最多的那种任务导致的,此时就根据次数最多的任务来计算最大值。 +当没有时间需要待命时, +最短时间=tasks.length +需要待命时,出现次数最多的那种任务只有1种时, +最短时间=(maxCount-1)*(n+1)+1 +需要待命时,出现次数最多的那种任务有x种时, +最短时间=(maxCount-1)*(n+1)+x +```java +public int leastInterval(char[] tasks, int n) { + int[] count = new int[26]; + for (int i = 0; i < tasks.length; i++) { + int index = tasks[i] - 'A'; + count[index]++; + } + //找出频率最大的字符 + int maxCount=0; + //频率最大的字符有几个 + int times=0; + for (int i = 0; i < count.length; i++) { + if (count[i] > maxCount) { + maxCount = count[i]; + times=1; + } else if(count[i] == maxCount) { + times++; + } + } + int max = (maxCount-1)*(n+1) + 1 + times - 1; + max = max > tasks.length ? max : tasks.length; + return max; + } +``` + +### 第581题-最短无序连续子数组 + +给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。 + +你找到的子数组应是最短的,请输出它的长度。 + +示例 1: + +输入: [2, 6, 4, 8, 10, 9, 15] +输出: 5 +解释: 你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。 +说明 : + +输入的数组长度范围在 [1, 10,000]。 +输入的数组可能包含重复元素 ,所以升序的意思是<=。 +##### 解题思路 +这个题就是寻找逆序对,找出需要最左边的需要调整的逆序对位置,然后再找出最右边的逆序对位置,两者之间的距离就是子数组的长度。 +```java +public int findUnsortedSubarray(int[] nums) { + if (nums==null||nums.length==0) { + return 0; + } + //从左往右开始遍历,记录最大值,找到需要调整的最右边的位置的下标 + Integer right = null; + int maxIndex = 0; + for (int i = 0; i < nums.length; i++) { + if (nums[i] < nums[maxIndex]) {//说明是需要调整的 + right=i; + } + maxIndex = nums[i] > nums[maxIndex] ? i : maxIndex; + } + //从右往左开始遍历,记录最小值,找到需要调整的最右边的位置的下标 + Integer left = null; + int minIndex = nums.length-1; + for (int i = nums.length-1; i >=0; i--) { + if (nums[i] > nums[minIndex]) {//说明是需要调整的 + left=i; + } + minIndex = nums[i] < nums[minIndex] ? i : minIndex; + } + if (left!=null&& right!=null) { + return right-left+1; + } + return 0; + } + + +``` + diff --git a/docs/Lock.md b/docs/Lock.md index 876281f..1f1fb21 100644 --- a/docs/Lock.md +++ b/docs/Lock.md @@ -3,6 +3,8 @@ #### [1.sychronize的实现原理是怎么样的?](#sychronize的实现原理是怎么样的?) #### [2.AbstractQueuedSynchronizer(缩写为AQS)是什么?](#AbstractQueuedSynchronizer(缩写为AQS)是什么?) +#### [3.悲观锁和乐观锁是什么?](#悲观锁和乐观锁是什么?) + ### sychronize的实现原理是怎么样的? ```java @@ -66,7 +68,7 @@ public class SyncTest { 而对于`synchronized`方法而言,`javac`为其生成了一个`ACC_SYNCHRONIZED`关键字,在JVM进行方法调用时,发现调用的方法被`ACC_SYNCHRONIZED`修饰,则会先尝试获得锁。 -#### 锁 +#### synchronized锁执行流程图 这是网上看到的一个流程图: @@ -74,53 +76,120 @@ public class SyncTest { 就是Java对象的内存布局其实由对象头+实例数据+对齐填充三部分组成,而对象头主要包含Mark Word+指向对象所属的类的指针组成。Mark Word主要用于存储对象自身的运行时数据,哈希码,GC分代年龄,锁标志等。 +![img](../static/640的副本) + 下面就是Mark Word的数据映射表 ![image](../static/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31312f32382f313637353964643162306239363236383f773d37323026683d32353026663d6a70656726733d3337323831.jpeg) -##### 偏向锁 +#### 偏向锁 根据上面的表来看,Mark Word后三位为101时,加锁对象的状态为偏向锁,偏向锁的意义在于同一个线程访问sychronize代码块时不需要进行加锁,解锁操作,性能开销更低(HotSpot[1]的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。) -因为正常情况下,当一个线程访问同步块并获取轻量级锁时,需要进行CAS操作将对象头的锁记录里指向当前线程的栈中的锁记录,执行完毕后需要释放轻量级锁。如果是同一个线程多次访问sychronize代码块,多次获取和释放轻量级,开销会偏大,所以会一开始判断对象是无锁状态,会将对象头设置为偏向锁,并且这个的线程ID到Mark Word,后续同一线程判断加锁标志是偏向锁,并且线程ID一致就可以直接执行。 +因为正常情况下,当一个线程访问同步块并获取轻量级锁时,需要进行CAS操作将对象头的锁记录里指向当前线程的栈中的锁记录,执行完毕后需要释放轻量级锁。如果是同一个线程多次访问sychronize代码块,多次获取和释放轻量级,开销会偏大,所以会一开始判断对象是无锁状态,会将对象头设置为偏向锁,并且这个的线程ID添加到锁对象的Mark Word中,后续同一线程判断加锁标志是偏向锁,并且线程ID一致就可以直接执行。偏向锁的加锁过程: + +##### 场景一:当锁对象第一次被线程获得锁的时候 + +线程发现是匿名偏向状态(也就是锁对象的Mark Word没有存储线程ID),则会用CAS指令,将 `mark word`中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。 + +##### 场景二:当获取偏向锁的线程再次进入同步块时 + +发现锁对象存储的线程ID就是当前线程的ID,会往当前线程的栈中添加一条 `DisplacedMarkWord`为空的 `LockRecord`中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下, `synchronized`关键字带来的性能开销基本可以忽略。 + +##### 场景二:当没有获得锁的线程进入同步块时 + +当没有获得锁的线程进入同步块时,发现当前是偏向锁状态,并且存储的是其他线程ID(也就是其他线程正在持有偏向锁),则会进入到**撤销偏向锁**的逻辑里,一般来说,会在 `safepoint`中去查看偏向的线程是否还存活 + +- 如果线程存活且还在同步块中执行, 则将锁升级为轻量级锁,原偏向的线程继续拥有锁,只不过持有的是轻量级锁,继续执行代码块,执行完之后按照轻量级锁的解锁方式进行解锁,而其他线程则进行自旋,尝试获得轻量级锁。 +- 如果偏向的线程已经不存活或者不在同步块中, 则将对象头的 `mark word`改为无锁状态(unlocked) -![img](../static/16315cb9175365f5.png) +由此可见,偏向**锁升级**的时机为:**当一个线程获得了偏向锁,在执行时,只要有另一个线程尝试获得偏向锁,并且当前持有偏向锁的线程还在同步块中执行,则该偏向锁就会升级成轻量级锁。** + +#### 偏向锁的解锁过程 + +因此偏向锁的解锁很简单,其仅仅将线程的栈中的最近一条 `lockrecord`的 `obj`字段设置为null。需要注意的是,偏向锁的解锁步骤中**并不会修改锁对象Mark Word中的thread id,简单的说就是锁对象处于偏向锁时,Mark Word中的thread id 可能是正在执行同步块的线程的id,也可能是上次执行完已经释放偏向锁的thread id**,主要是为了上次持有偏向锁的这个线程在下次执行同步块时,判断Mark Word中的thread id相同就可以直接执行,而不用通过CAS操作去将自己的thread id设置到锁对象Mark Word中。**这是偏向锁执行的大概流程:**![img](../static/640-20201128120828308) #### 轻量级锁 -JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。 +重量级锁依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的,而在大部分时候可能并没有多线程竞争,只是多个线程交替执行,(例如:这段时间是线程A执行同步块,另外一段时间是线程B来执行同步块,仅仅是多线程交替执行,并不是同时执行,也没有竞争),如果采用重量级锁效率比较低。以及在重量级锁中,没有获得锁的线程会阻塞,获得锁之后线程会被唤醒,阻塞和唤醒的操作是比较耗时间的,如果同步块的代码执行比较快,等待锁的线程可以进行先进行自旋操作(就是不释放CPU,执行一些空指令或者是几次for循环),等待获取锁,这样效率比较高。所以轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再升级为重量级锁。 + +##### 轻量级锁的加锁过程 + +JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word里面。 + +然后线程尝试**用CAS操作将自己线程栈中拷贝的锁记录的地址写入到锁对象的Mark Word中**。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用**自旋**来获取锁。 + +自旋:不断尝试去获取锁,一般用循环来实现。 + +自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。 + +JDK采用了适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋时触发重量级锁的阀值会更高,如果自旋失败了,则自旋的次数就会减少。 + +自旋也不是一直进行下去的,如果自旋到一定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。 -线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个`Lock Record`,其包括一个用于存储对象头中的 `mark word`(官方称之为`Displaced Mark Word`)以及一个指向对象的指针。下图右边的部分就是一个`Lock Record`。 +#### 轻量级锁的释放流程 -![img](../static/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f31312f32382f313637353964643162323461633733643f773d38363926683d33353126663d706e6726733d3331313531.png) +在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。**轻量级锁的加锁解锁流程图:** -(1)轻量级锁加锁 +![img](../static/640-20201128120828359) -线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并 将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁,自旋获取锁失败的次数达到一定次数后就会进行锁升级,将锁升级为重量级锁,当前线程就会被阻塞,直到获得轻量级锁的线程执行完毕,释放锁,唤醒阻塞的线程。 +#### 重量级锁 -(2)轻量级锁解锁 +每个对象都有一个监视器monitor对象,重量级锁就是由对象监视器monitor来实现的,当多个线程同时请求某个重量级锁时,重量级锁会设置几种状态用来区分请求的线程: -轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成 功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是 两个线程同时争夺锁,导致锁膨胀的流程图。 +**Contention List 竞争队列**:所有请求锁的线程将被首先放置到该竞争队列,我也不知道为什么网上的文章都叫它队列,其实这个队列是先进后出的,更像是栈,就是当Entry List为空时,Owner线程会直接从Contention List的队列尾部(后加入的线程中)取一个线程,让它成为OnDeck线程去竞争锁。(主要是刚来获取重量级锁的线程是会进行自旋操作来获取锁,获取不到才会进入Contention List,所以OnDeck线程主要与刚进来还在自旋,还没有进入到Contention List的线程竞争) -![img](../static/16315cb9193719c2.png) +**Entry List 候选队列**:Contention List中那些有资格成为候选人的线程被移到Entry List,主要是为了减少对Contention List的并发访问,因为既会添加新线程到队尾,也会从队尾取线程。 -### 重量级锁 +**Wait Set 等待队列**:那些调用wait()方法被阻塞的线程被放置到Wait Set。 -重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。 +**OnDeck**:任何时刻最多Entry List中只能有一个线程被选中,去竞争锁,该线程称为OnDeck线程。 -重量级锁的状态下,对象的`mark word`为指向一个堆中monitor对象的指针。 +**Owner**:获得锁的线程称为Owner。 -一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。 +**!Owner**:释放锁的线程。 -其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。 +##### 重量级锁执行流程: -![image-20200516203630962](../static/image-20200516203630962.png) +流程图如下:![img](../static/640-6536508.png) -当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。 +**步骤1**是线程在进入Contention List时阻塞等待之前,程会先尝试自旋使用CAS操作获取锁,如果获取不到就进入Contention List队列的尾部(所以不是公平锁)。 -如果一个线程在同步块中调用了`Object#wait`方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。 +**步骤2**是Owner线程在解锁时,如果Entry List为空,那么会先将Contention List中队列尾部的部分线程移动到Entry List。(所以Contention List相当于是后进先出,所以也是不公平的) -#### 三种锁的优缺点对比 +**步骤3**是Owner线程在解锁时,如果Entry List不为空,从Entry List中取一个线程,让它成为OnDeck线程,Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,JVM中这种选择行为称为 “竞争切换”。(主要是与还没有进入到Contention List,还在自旋获取重量级锁的线程竞争) + +**步骤4**就是OnDeck线程获取到锁,成为Owner线程进行执行。 + +等待和通知步骤(这是调用了wait()和notify()方法才有的步骤): + +在同步块中,获得了锁的线程调用锁对象的Object.wait()方法,就是Owner线程调用锁对象的wait()方法进行等待,会移动到Wait Set中,并且会释放CPU资源,也同时释放锁, + +就是当其他线程调用锁对象的Object.notify()方法,之前调用wait方法等待的这个线程才会从Wait Set移动到Entry List,等待获取锁。 + +##### 3.为什么说是轻量级,重量级锁是不公平的? + +偏向锁由于不涉及到多个线程竞争,所以谈不上公平不公平,轻量级锁获取锁的方式是多个线程进行自旋操作,然后使用**用CAS操作将锁的Mark Word中存储的Lock Word替换为指向自己线程栈中拷贝的锁记录的指针**,所以谁能获得锁就看运气,不看先后顺序。重量级锁不公平主要在于刚进入到重量级的锁的线程不会直接进入Contention List队列,而是自旋去获取锁,所以后进来的线程也有一定的几率先获得到锁,所以是不公平的。 + +##### 4.重量级锁为什么需要自旋操作? + +因为那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthreadmutexlock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。如果同步块中代码比较少,执行比较快的话,后进来的线程先自旋获取锁,先执行,而不进入阻塞状态,减少额外的开销,可以提高系统吞吐量。 + +##### 5.什么时候会发生锁升级,锁降级? + +**偏向锁升级为轻量级锁:**就是有不同的线程竞争锁时。具体来看就是当一个线程发现当前锁状态是偏向锁,然后锁对象存储的Thread id是其他线程的id,并且去Thread id对应的线程栈查询到的lock record的obj字段不为null(代表当前持有偏向锁的线程还在执行同步块)。那么该偏向锁就会升级成轻量级锁。 + +**轻量级锁升级为重量级锁:**就是在轻量级锁中,没有获取到锁的线程进行自旋,自旋到一定次数还没有获取到锁就会进行锁升级,因为自旋也是占用CPU的,长时间自旋也是很耗性能的。 + +**锁降级**因为如果没有多线程竞争,还是使用重量级锁会造成额外的开销,所以当JVM进入SafePoint安全点(可以简单的认为安全点就是所有用户线程都停止的,只有JVM垃圾回收线程可以执行)的时候,会检查是否有闲置的Monitor,然后试图进行降级。 + +##### 6.偏向锁,轻量锁,重量锁的适用场景,优缺点是什么? + +偏向锁:加锁解锁不需要进行CAS操作,适合一个线程多次访问同步块的场景。 + +轻量级锁:加锁和解锁使用CAS操作,没有像重量级锁那样底层操作系统的互斥量来加锁解锁,不涉及到用户态和内核态的切换和线程阻塞唤醒造成的线程上下文切换。没有获得锁的线程会自旋空耗CPU,造成一些开销。适合多线程竞争比较少,但是会有多线程交替执行的场景。 + +重量级锁:使用到了底层操作系统的互斥量来加锁解锁,但是会涉及到用户态和内核态的切换和线程阻塞和唤醒造成的线程上下文切换,但是不会自旋空耗CPU。 ![image-20200516203659737](../static/image-20200516203659737.png) @@ -133,20 +202,287 @@ JVM的开发者发现在很多情况下,在Java程序运行时,同步块中 ### AbstractQueuedSynchronizer(缩写为AQS)是什么? AQS就是AbstractQueuedSynchronizer,抽象队列同步器,是一个可以用于实现基于先进先出等待队列的锁和同步器的框架。实现锁 -ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。 +ReentrantLock,CountDownLatch,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。 ReentrantLock其实就是有一个变量sync,Sync父类是AbstractQueuedSynchronizer ```java public class ReentrantLock implements Lock, java.io.Serializable { - private final Sync sync; + private final Sync sync; } ``` -ReentrantLock的非公平锁与公平锁的区别在于非公平锁在CAS更新state失败后会调用tryAcquire()来判断是否需要进入同步队列,会再次判断state的值是否为0,为0会去CAS更新state值,更新成功就直接获得锁,否则就进入等待队列。 +ReentrantLock的非公平锁与公平锁的区别在于非公平锁在CAS更新state失败后会调用tryAcquire()来判断是否需要进入同步队列,会再次判断state的值是否为0,为0会去CAS更新state值,更新成功就直接获得锁,否则就进入等待队列。(进等待队列之前会抢锁) 而公平锁首先判断state是否为0,为0并且等待队列为空,才会去使用CAS操作抢占锁,抢占成功就获得锁,没成功并且当前线程不是获得锁的线程,都会被加入到等待队列。 参考资料: -深入理解ReentrantLock的实现原理 \ No newline at end of file +深入理解ReentrantLock的实现原理 + +### synchronized锁与ReentrantLock锁的区别? + +**相同点:** + +1.可重入性 + +两个锁都是可重入的,持有锁的线程再次申请锁时,会对锁的计数器+1。 + +**不同点:** + +**1.实现原理** + +synchronized是一个Java 关键字,是由JVM实现的,底层代码应该是C++代码。而ReentrantLock是JDK实现的,是Java提供的一个类库,代码是Java代码,源码实现更加方便阅读。 + +**2.性能** + +在以前,synchronized锁的实现只有重量级锁一种模式,性能会比较差,后面引入了偏向锁和轻量级锁后就优化了很多。根据测试结果,在线程竞争不激烈的情况下,ReentrantLock与synchronized锁持平,竞争比较激烈的情况下,ReentrantLock会效率更高一些。 + +**3.功能** + +synchronized只能修饰方法,或者用于代码块,而ReentrantLock的加锁和解锁是调用lock和unlock方法,更加灵活。 + +其次是synchronized的等待队列只有一个(调用wait()方法的线程会进入等待队列),而ReentrantLock可以有多个条件等待队列。可以分组唤醒需要唤醒的线程们,而不是像synchronized要么用notify方法随机唤醒一个线程要么用notifyAll方法唤醒全部线程。ReentrantLock 提供了一种能够中断等待锁的线程的机制,就是线程通过调用lock.lockInterruptibly()方法来加锁时,一旦线程被中断,就会停止等待。 + +ReentrantLock可以使用tryLock(long timeout, TimeUnit unit)方法来尝试申请锁,设置一个超时时间,超过超时时间,就会直接返回false,而不是一直等待锁。 + +ReentrantLock可以响应中断,而synchronized锁不行 + +**4.公平性** + + synchronized锁是非公平锁,ReentrantLock有公平锁和非公平锁两种模式。 + +https://www.codercto.com/a/22884.html + +### ReentrantLock的加锁流程是怎么样的? + +ReentrantLock非公平锁的加锁流程: + +1.尝试着使用CAS操作将锁的状态state由0修改为1,修改成功则线程获得锁。 + +2.不成功就会再次尝试去抢锁,以及判断这个线程是否是当前持有锁的线程(如果是只需要将state+1,代表锁重入)。 + +3.抢锁没成功,也不是持有锁的线程,那么就会添加到等待队列然后调用Lock.Support.park()方法进行阻塞等待,然后被唤醒。 + +![图片](../static/640-3893313.png) + +公平锁加锁过程: + +1.如果当前锁没有被其他线程持有,并且等待队列中也没有其他线程等待,那么就使用CAS操作去抢锁。 + +2.或者线程就是当前持有锁的线程,那么就对state+1,代表锁重入。 + +3.以上情况都不是,就加入到等待队列进行等待。 + +非公平锁解锁流程 + +![图片](../static/640-20210221154207974.png) + +https://blog.csdn.net/qq_14996421/article/details/102967314 + +https://blog.csdn.net/fuyuwei2015/article/details/83719444 + +### 谈一谈你对AQS的理解? + +AQS是AbstractQueuedSynchronizer的缩写,是一个抽象同步队列类,可以基于它实现一个锁,例如ReentrantLock,只是需要实现tryAcquire()方法(也就是获取资源的方法,判断当前线程能否申请到独占锁)和tryRelease()方法(也就是释放资源的方法,在线程释放锁前对state进行更新),AQS会根据tryAcquire()的返回结果,来进行下一步的操作, + +如果为true,代表线程获得锁了。 + +如果为false,代表线程没有获得锁,由AQS负责将线程添加到CLH等待队列中,并且进行阻塞等待。当前一个线程释放锁时,AQS对这个线程进行唤醒。 + +(不同的自定义同步器争用共享资源的方式也不同。**自定义同步器在实现时只需要实现资源state的获取与释放方法即可**,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了) + +**公平锁加锁过程** + +加锁方法调用栈 + +```java +ReentrantLock lock = new ReentrantLock(); +lock.lock();//在我们代码中通过调用lock方法进行加锁 +FairSync.lock();//lock方法中会调用公平锁类FairSync的lock方法 +AbstractQueuedSynchronizer.acquire(1);//FairSync继承于Sync,而Sync继承于AQS,FairSync的lock方法中调用了acquire方法也就是去申请独占锁资源。 +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +FairSync.tryAcquire()//调用FairSync的tryAcquire去尝试着申请独占锁资源 +AbstractQueuedSynchronizer.addWaiter()//申请失败就将线程添加到等待队列尾部 +AbstractQueuedSynchronizer.acquireQueued()//并且让这个线程进入阻塞等待状态 +``` + +对于加锁,**ReentrantLock**只需要去实现**tryAcquire**()方法,去根据state判断当前线程能不能获取锁,能获取就会返回true。不能获取就会返回false,然后由AQS来将未获得锁的线程添加到CLH队列尾部,然后等待被唤醒。(公平锁与非公平锁也就在于**ReentrantLock的tryAcquire**实现的区别,当锁被其他线程占用时,公平锁是只有当前等待队列没有其他线程时,才能去抢锁,而非公平锁则没有这个限制,在申请锁时就能去抢锁。) + + + +可以看到AQS的acquire()方法中是会先去调用tryAcquire()去尝试着申请独占锁资源,AQS默认的tryAcquire()方法只有一行代码,会抛出UnsupportedOperationException异常(强制子类对这个方法进行实现)。所以ReentrantLock的FairSync对tryAcquire()方法进行了实现。 + +tryAcquire()返回true就代表获取独占锁资源成功: + +1.等待队列没有其他线程且这个线程CAS操作设置state成功 + +2.当前线程就是持有锁的线程,那么只需要对state+1即可。 + +tryAcquire()返回false代表获取独占锁资源失败, + +那么就调用AQS.addWaiter()方法申请失败就将线程添加到等待队列尾部,AQS.acquireQueued()方法让这个线程进入阻塞等待状态(在阻塞之前如果等待队列只有这一个线程,是会先尝试着获取锁,失败才会进入阻塞状态。) + +```java +//公平锁的tryAcquire方法实现 +protected final boolean tryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + //等待队列中没有线程,使用cas操作去抢锁,抢锁成功,就返回true + if (!hasQueuedPredecessors() && + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + //当前线程与持有锁的线程是同一个,那么进行重入 + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; + } +``` + +**公平锁解锁过程:** + +解锁方法调用栈 + +```java +ReentrantLock lock = new ReentrantLock(); +lock.unlock(); +AbstractQueuedSynchronizer.release(1); +//AbstractQueuedSynchronizer的release()方法 +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; + } +//ReentrantLock中Sync的tryRelease方法 +protected final boolean tryRelease(int releases) { + int c = getState() - releases; + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + //state为0就释放锁,否则只是锁的state减去releases + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + setState(c); + return free; + } +``` + +对于解锁,ReentrantLock的公平锁和非公平锁的实现是一样的,都是对state赋以最新的值,然后由AQS的unparkSuccessor方法负责对线程进行唤醒。 + +https://www.cnblogs.com/waterystone/p/4920797.html + +### 悲观锁和乐观锁是什么? + +### 悲观锁 + +就是假定在每次取数据的时候会修改这个数据,所以在取数据的时候就会进行加锁,这样其他调用者就不能取数据,阻塞等待,一直到获取到锁。Java中的同步锁sychronized和ReentrantLock就是悲观锁思想的实现。 + +##### 乐观锁和悲观锁的区别: + +悲观锁适合多写的场景 + +乐观锁适合多读的场景,这样只有读写冲突会发生的比较少,减少加锁的性能开销。但是如果是多写的场景,这样会导致上层应用一直重试,增加性能开销。 + +#### 乐观锁 + +就是假定在每次取数据时不会修改这个数据,所以在取数据的时候不会加锁,只有在真正修改数据时才加锁。Java中的atomic原子变量类就是乐观锁的实现。 + +##### 版本号机制 + +使用版本号来实现,对数据加上一个版本号,代表修改次数,每次修改后+1,修改数据时判断数据的版本号跟之前取的是否一致,一致才修改,不一致就重试,直到更新成功。 + +##### CAS操作 + +就是在更新数据时会传入之前取的值,在内存中判断当前内存中的值跟之前的值是否一致,一致再更新,(比较和更新都是在一个原子操作中)。 + +##### ABA问题怎么解决? + +使用CAS更新是没法解决ABA的问题,就是其他调用方对数据修改成其他值后又改回原来的值。AtomicStampedReference的compareAndSet会先判断对象的引用是否相同,相同才进行CAS更新。实现原理主要是AtomicStampedReference会保存之前对象的的引用,及一个修改版本号,只有当引用和版本号都相等的情况下,才会进行CAS更新操作。 + +##### CAS操作的缺点有哪些? + +1.循环时间长开销大 + +自旋CAS操作如果不成功就一直循环执行直到成功,如果长时间不成功,会给CPU带来非常大的执行开销 + +2.CAS 只对单个共享变量有效 + +多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作 + +#### Java的原子类 + +原子类一共有以下四种 + +- 1.基本类型: AtomicInteger, AtomicLong, AtomicBoolean ; +- 2.数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ; +- 3.引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ; +- 4.对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。 + +##### AtomicInteger + +主要是对Integer的封装,提供了一些原子性的操作,因为如果是使用Integer来完成i=i+1;操作,在内存中是三个步骤,先将从内存中取出i,放到寄存器中,然后将寄存器中的值与1相加,然后将结果写入内存,一共是三个步骤,所以不是原子性的,并发时会造成数据不一致的问题。 + +主要实现原理是AtomicInteger类有一个unsafe属性,可以通过unsafe来调用Unsafe类的一些原子性的方法Unsafe.compareAndSwapInt来实现原子性的加减运算。 + +其次是使用volatile来修饰value属性,保证一个内存可见性 + +```java +//compareAndSwapInt有四个参数,第一个是待运算的对象,第二个是对象中用于运算的属性的偏移量,第三个是期望值,第四个是更新的值。 +unsafe.compareAndSwapInt(this, valueOffset, expect, update) +``` + +```java +public class AtomicInteger extends Number implements java.io.Serializable { + private static final long serialVersionUID = 6214790243416807050L; + + // setup to use Unsafe.compareAndSwapInt for updates + private static final Unsafe unsafe = Unsafe.getUnsafe(); + private static final long valueOffset; + + static { + try { + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } + } + + private volatile int value;//使用volatiole来保证value的内存可见性 +} + +``` + +在Unsafe类中,compareAndSwapInt和getAndAddInt的区别在于,getAndAddInt会一直重试直到成功,compareAndSwapInt如果更新失败,只会返回false + +```java +public final int getAndAddInt(Object var1, long var2, int var4) { + int var5; + do { + //var1是对象,var2是对象上某个变量的偏移量, + //var5就是对象var1上偏移量为var2的一个变量 + var5 = this.getIntVolatile(var1, var2); + //当var5的值没有变化时,就会进行加法操作,也就是var5 = var5 + var4 + //如果var5的值变化了,就会取最新的var5的值,进行加法操作。 + } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); + + return var5; +} +``` \ No newline at end of file diff --git a/docs/MySQLNote.md b/docs/MySQLNote.md index b754762..04c8b6b 100644 --- a/docs/MySQLNote.md +++ b/docs/MySQLNote.md @@ -45,7 +45,7 @@ #### 2.写undo log -innodb 引擎首先开启事务,对旧数据生成一个UPDATE的语句(如果是insert会生成delete语句),用于提交失败后回滚,写入undo log,得到回滚指针,并且更新这个数据行的回滚指针和事务id。(事务提交后,Undo log并不能立马被删除,而是放入待清理的链表,由purge 线程判断是否有其他事务在使用undo 段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间,简单的说就是看之前的事务是否提交成功,这个事务及之前的事务都提交成功了,这部分undo log才能删除。) +innodb 引擎首先开启事务,获得一个事务ID(是一直递增的),根据执行的语句生成一个反向的语句,(如果是INSERT会生成一条DELETE语句,如果UPDATE语句就会生成一个UPDATE成旧数据的语句),用于提交失败后回滚,将这条反向语句写入undo log,得到回滚指针,并且更新这个数据行的回滚指针和事务id。(事务提交后,Undo log并不能立马被删除,而是放入待清理的链表,由purge 线程判断是否有其他事务在使用undo 段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间,简单的说就是看之前的事务是否提交成功,这个事务及之前的事务都提交成功了,这部分undo log才能删除。) #### 3.从索引中查找数据 @@ -53,11 +53,11 @@ innodb 引擎首先开启事务,对旧数据生成一个UPDATE的语句(如果 #### 4.更新数据 -判断数据页是否在内存中 +判断数据页是否在内存中? #### 4.1数据页在内存中 -索引是 +索引是普通索引还是唯一性索引? ##### 4.1.1普通索引 @@ -65,43 +65,45 @@ innodb 引擎首先开启事务,对旧数据生成一个UPDATE的语句(如果 ##### 4.1.2唯一性索引 -判断更新后是否会数据冲突,不会的话就更新内存中的数据页。 +判断更新后是否会数据冲突(不能破坏索引的唯一性),不会的话就更新内存中的数据页。 #### 4.2 数据页不在内存中 -索引是 +索引是普通索引还是唯一性索引? ##### 4.2.1普通索引 -将对数据页的更新操作记录到change buffer,change buffer会在空闲时异步更新到磁盘。 +将对数据页的更新操作记录到change buffer,暂时不更新到磁盘。change buffer会在空闲时异步更新到磁盘。 -##### 4.2.2唯一性索引 +##### 4.2.2 唯一性索引 因为需要保证更新后的唯一性,所以不能延迟更新,必须把数据页从磁盘加载到内存,然后判断更新后是否会数据冲突,不会的话就更新数据页。 -#### 5.写undo log(prepare状态) +#### 5.写redo log(prepare状态) -将对数据页的更改写入到redo log,将redo log设置为prepare状态。 +将对数据页的更改写入到redo log,此时redo log中这条事务的状态为prepare状态。 -#### 6.写bin log(commit状态) +#### 6.写bin log(同时将redo log设置为commit状态) -通知MySQL server已经更新操作写入到redo log 了,随时可以提交,将执行的SQL写入到bin log日志,将redo log改成commit状态,事务提交成功。 +通知MySQL server已经更新操作写入到redo log 了,随时可以提交,将执行的SQL写入到bin log日志,将redo log 中这条事务的状态改成commit状态,事务提交成功。 -#### undo log +https://www.cnblogs.com/yuyue2014/p/6121114.html + +##### undo log 主要是保证事务的原子性,事务执行失败就回滚,用于在事务执行失败后,对数据回滚。 -是逻辑日志,记录的是SQL +是逻辑日志,记录的是SQL语句。 在事务提交后,undo log日志不会立即删除,会放到一个待删除的链表中,有purge线程判断是否有其他事务在使用上一个事务之前的版本信息,然后决定是否可以清理,简单的来说就是前面的事务都提交成功了,这些undo才能删除。 -##### change buffer是什么(就是将更新数据页的操作缓存下来) +##### change buffer是什么? -在更新数据时, +(change buffer就是将更新数据页的操作缓存下来) -如果数据行所在的数据页在内存中,直接更新内存中的数据页。 +在更新数据时,如果数据行所在的数据页在内存中,直接更新内存中的数据页。 -如果不在内存中,为了减少磁盘IO的次数,innodb会将这些更新操作缓存在change buffer中,在下一次查询时需要访问这个数据页时,在执行change buffer中的操作对数据页进行更新。 +如果不在内存中,为了减少磁盘IO的次数,innodb会将这些更新操作缓存在change buffer中,在下一次查询时需要访问这个数据页时,在执行change buffer中的操作对数据页进行更新。(或者是在MySQL Server空闲时,会将change buffer中所有操作更新到磁盘,也就是俗称的‘刷页’。) 适合写多读少的场景,因为这样即便立即写了,也不太可能会被访问到,延迟更新可以减少磁盘I/O,只有普通索引会用到,因为唯一性索引,在更新时就需要判断唯一性,所以没有必要。 @@ -113,7 +115,7 @@ innodb 引擎首先开启事务,对旧数据生成一个UPDATE的语句(如果 缺点是会造成内存脏页,后台线程会自动对脏页刷盘,或者是淘汰数据页时刷盘,此时会暂时查询操作,影响查询。 -#### 二段提交制 +##### 二段提交制是什么? 更新时,先改内存中的数据页,将更新操作写入redo log日志,此时redo log进入prepare状态,然后通知MySQL Server执行完了,随时可以提交,MySQL Server将更新的SQL写入bin log,然后调用innodb接口将redo log设置为提交状态,更新完成。 @@ -137,9 +139,9 @@ https://www.infoq.cn/article/M6g1yjZqK6HiTIl_9bex ### 脏页是什么? -就是内存数据页与磁盘内存页的内容不一致时的内存页叫做脏页。内存页数据写入磁盘后,数据一致了,就是干净页了。 +就是内存数据页与磁盘内存页的内容不一致时的内存页叫做脏页。内存页数据写入磁盘后,数据一致了,就是干净页了。(其实就是将Change Buffer中缓存的更新写入到磁盘) -刷脏页的场景 +刷脏页的场景: 1.redo log 写满了,系统会停止所有更新操作,将checkpoint向前推进,将推进这部分日志的脏页更新到磁盘。 @@ -167,50 +169,48 @@ undo log是一种逻辑日志,是旧数据的备份。有两个作用用于事 执行一条INSERT语句时,会记录一条相反的DELETE语句到日志,执行一条UPDATE语句时,会记录一条相反的UPDATE语句到日志中。 - - ##### redo log是什么? -redo log用于保证数据的持久性。redo log记录的是数据页的物理变化,是新数据的备份,在事务提交前,将redo log 持久化就行,不需要将数据持久化,系统崩溃时,可以根据redo log将数据恢复到最新状态。 +redo log用于保证数据的持久性。redo log记录的是数据页的物理变化,是物理日志,是新数据的备份,在事务提交前,将redo log 持久化就行,不需要将数据持久化,系统崩溃时,可以根据redo log将数据恢复到最新状态。 redo log只做顺序追加操作,当事务需要回滚时,在redo log中也不会删除之前的事务记录。 -默认是每次事务提交时必须调用fsync操作将redo缓冲区的内容写入磁盘 +默认是每次事务提交时必须调用fsync操作将redo缓冲区的内容写入磁盘。 -例如将A=1修改为A=2 +例如:将A=1修改为A=2,它的流程如下: -事务开始 +1.事务开始 -将原始数据A=1从磁盘读取到内存, +2.将原始数据A=1从磁盘读取到内存,生成反向语句,写入undolog。 -修改A=2, +3.修改A=2, -生成一条redo log 写入到redo log 缓冲区 +4.生成一条redo log 写入到redo log 缓冲区 -调用fsync操作将redo log 缓冲区的内容写入到磁盘 +5.调用fsync操作将redo log 缓冲区的内容写入到磁盘,写入binlog -事务提交。 +6.事务提交。 ##### Bin log 是什么? 保存的是逻辑日志,主要是存储每一条会修改数据的SQL。 +https://blog.csdn.net/qq_41652863/article/details/98800650 + ### MySQL中的事务是什么? 由一系列数据库操作组成的逻辑过程,可以是一个SQL查询,也可以是一组SQL查询。 -具备四个特性,也就是acid - -原子性,一个事务就是最小的工作单元,要么执行成功提交,要么执行失败回滚。 +具备四个特性,也就是acid。 -一致性,就是事务的执行不会影响数据库的完整性,例如执行到一半时发生故障,那么已经进行的修改会回滚。 +**原子性(Atomic)**:一个事务就是最小的工作单元,要么执行成功提交,要么执行失败回滚。 -隔离性,就是事务在执行过程中,两个事务之间是隔离的,事务在执行成功之前,所做的修改对其他事务是不可见的。 - -持久性,事务执行成功后,对数据的修改是永久的,即便故障也不会丢失。 +**一致性(Consistence)**:就是事务的执行不会影响数据库的完整性,保证事务只能把数据库从一个有效(正确)的状态“转移”到另一个有效(正确)的状态,不能执行完一个事务后,使得数据库不符合数据库定义的规则,例如让某个有唯一性约束的字段存在两个相同的值,破坏了唯一性。 +**隔离性(isolation)**:就是事务在执行过程中,两个事务之间是隔离的,事务在执行成功之前,所做的修改对其他事务是不可见的。 +**持久性(durability)**:事务执行成功后,对数据的修改是永久的,即便发生故障重启也不会丢失数据。 ### MySQL的隔离级别是怎么样的? @@ -222,11 +222,13 @@ redo log只做顺序追加操作,当事务需要回滚时,在redo log中也 其他事务提交的修改,事务在执行过程中可以读取到,如果一个事务在执行过程中需要两次读取同一行数据,可能会不一致。一般发生在UPDATE和DELETE操作。(大部分数据库系统是采用的这个,但是mysql不是) -这个隔离级别下,读是不加锁的,写,更新,删除是加锁的,如果更新的行是可以通过索引查找到,那么是对这些行加行锁,否则会将所有行都加锁,然后返回给Mysql Server,让他来进行过滤,对于不满足条件的行解锁。 +这个隔离级别下,读是不加锁的,写,更新,删除是加锁的,如果更新的行是可以通过索引查找到,那么是对这些行加行锁,否则会将所有行都加锁,然后返回给MySQL Server,让他来进行过滤,对于不满足条件的行解锁。 + +但是还是会有幻读的问题发生(幻读就是事务A在读取和写入符合的条件的记录时,其他事务又插入了一条符合条件的记录,此时事务A二次读取时会产生幻行,一般发生在INSERT操作。) ##### 可重复读 -在事务开始时,记录当时的状态,在第二次读取同一行数据时,除非是本事务做的修改,否则读取的都是事务开始时的数据。可以解决脏读的问题,没法解决幻读的问题。这是mysql的默认事务隔离级别。但是还是会有幻读的问题(幻读就是事务A在读取和写入符合的条件的记录时,其他事务又插入了一条符合条件的记录,此时事务A读取时会产生幻行,一般发生在INSERT操作。) +在事务开始时,记录当时的状态,在第二次读取同一行数据时,除非是本事务做的修改,否则读取的都是事务开始时的数据。可以解决脏读的问题,没法解决幻读的问题。这是MySQL的默认事务隔离级别。(MySQL在可重复读的隔离级别下,通过MVCC机制和Next-key Lock解决了幻读的问题。) ##### 可串行化 @@ -234,47 +236,57 @@ redo log只做顺序追加操作,当事务需要回滚时,在redo log中也 ### MVCC的实现原理是怎么样的? -mvcc主要适用于提交读,可重复读,可以解决幻读的问题。 +mvcc主要适用于可重复读,可以解决幻读的问题。 + +innodb在解决幻读的问题主要是通MVVC 多版本并发版本控制来实现的。 + +就是每一行数据中额外保存两个隐藏的列: -innodb在解决幻读的问题主要是通MVVC 多版本并发版本控制来实现的 +**插入或上次更新该行的事务ID**(删除也被认为是一次更新,只不过在代表删除的更新操作中,行中的特殊位被设置为将其标记为已删除。这个事务ID可以认为是数据行的修改版本号。) -就是每一行数据中额外保存两个隐藏的列,创建时的版本号,删除时的版本号(可能为空),滚动指针(指向undo log中用于事务回滚的日志记录) +**回滚指针**(指向undo log中用于事务回滚的日志记录)。 -1.事务在对数据修改后,进行保存时,如果数据行的当前版本号与事务开始取得数据的版本号一致就保存成功,否则保存失败。 +具体流程: -##### 插入操作 +##### 1.插入操作 每次开始事务时,会对系统版本号+1作为当前事务的版本号。 插入数据后,将事务的版本号作为数据行的创建版本号。 -##### 删除操作 +##### 2.删除操作 -将当前的事务版本号作为数据行的删除版本号。 +在使用SQL语句删除行时,并不会立即将其从数据库中物理删除,只会将其标记为删除,并且修改更新该行的事务ID。(`InnoDB`只会在丢弃为删除而编写的undo log日志记录时,才物理删除相应的行及其索引记录。此删除操作称为[purge](https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_purge),它非常快,通常花费与执行删除操作的SQL语句相同的时间顺序。) -##### 更新操作 +##### 4.或更新操作 -插入一条新的数据,将事务的版本号作为数据行的创建版本号,并且将当前事务版本号作为旧数据的删除版本号。 +将当前的事务版本号作为数据行的更新版本号。 -##### 查询操作 +##### 5.查询操作 数据行要被查询出来必须满足两个条件, -删除版本号为空或者>当前事务版本号的数据(否则数据已经被标记删除了) +数据行没有标记为删除或者标记为删除但是删除的事务ID>当前事务ID的数据(否则数据已经被标记删除了) -创建版本号<=当前事务版本号的数据(否则数据是后面的事务创建出来的) +更新事务ID<=当前事务ID的数据(否则数据是后面的事务创建出来的,或者是被修改过的,那么需要去undo log中找上次的快照数据。) -如果查询时,该行数据被加了X锁,那么读数据的事务不会进行等待,而是回去undo log端里面读之前版本的数据(这里存储的数据本身是用于回滚的),在可重复读的隔离级别下,从undo log中读取的数据总是事务开始时的快照数据(也就是版本号小于当前事务id的数据),在提交读的隔离级别下,从undo log中读取的总是最新的快照数据。 +如果查询时,该行数据被加了X锁,那么读数据的事务不会进行等待,而是会根据该行数据中的回滚指针undo log日志中读之前版本的数据(这里存储的数据本身是用于回滚的),在可重复读的隔离级别下,从undo log中读取的数据总是事务开始时的快照数据(也就是版本号小于当前事务id的数据),在提交读的隔离级别下,从undo log中读取的总是最新的快照数据。 + +参考文档: + +https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html + +https://blog.csdn.net/qq_41652863/article/details/98800650 ### MySQL是怎么解决幻读的问题的? 幻读就是事务执行过程中,在查询一个范围的数据时,有新的数据插入到这个范围,导致两次查询的数据不一致。因为读分为快照读和实时读, -快照读 +**快照读** 我们普通的SELECT语句都是普通读,也就是读取的数据都是事务开始时那个状态的数据,普通读的幻读问题主要是通过MVCC来解决的,具体可以看上面的MVCC中的查询操作。 -实时读 +**实时读** SELECT *** FOR UPDATE 在查询时会先申请X锁 @@ -297,7 +309,7 @@ SELECT *** IN SHARE MODE 在查询时会先申请S锁 对数据库执行 -Flush tables with read lock命令让整个库处于只读状态。 +`Flush tables with read lock`命令让整个库处于只读状态。 ##### 2.利用innodb的事务隔离性(可重复读) @@ -313,17 +325,17 @@ Flush tables with read lock命令让整个库处于只读状态。 就是使用lock table user_table read/write命令来对表进行加读锁或者写锁 加读锁后,表对所有线程都是只能读,即便是当前线程也只能读表,不然会数据不一致。 -加写锁后,表是对当前线程写,其他线程不能读,不能回数据不一致。 +加写锁后,表是对当前线程写,其他线程不能读,不然会数据不一致。 可以通过unlock tables来解锁,客户端断开时也会自动释放锁,但是影响所有线程,影响面太大了。 ##### 元数据锁MDL(MetaData Lock) 分为读锁和写锁,加读锁时,所有的线程都可以读表,加写锁时,只能一个线程写,其他的不能读。 锁不用显式使用,是访问一个表时,自动加上的。 -对表进行增删改查时,会加读锁。 +对表执行普通SQL语句对表数据进行增删改查时,会加读锁。 对表结构做修改时,会加写锁。 -目的是为了在增删改查时不能修改表结构,修改表结构时不能去增删改查。 +元数据锁是为了修改表结构不会出现问题而设计的,因为一边修改表结构一边读数据可能会读到脏数据,所以在增删改查时会申请读锁,在这个期间不能修改表结构,要修改表结构需要先申请写锁,申请成功后对表结构进行修改,在这个期间不能进行增删改查。 ##### 自增锁 @@ -349,7 +361,7 @@ innodb_autoinc_lock_mode为2 ##### 意向锁 -意向锁定的主要目的是表明有人正在锁定表中的行,或者打算锁定表中的行,这样再加表级别的排斥锁X,共享锁S时,避免了去查询每一行数据,判断是否加了行锁,减小了性能开销。 +意向锁定的主要目的是表明有人正在锁定表中的行,或者打算锁定表中的行。意向锁的作用主要在于,当一个事务去申请表级别的排斥锁X,共享锁S时,需要去判断是否有其他事务在修改数据行,或者让数据行处于只读状态。假如没有意向锁,可能需要查询每一行数据,判断是否加了行锁。而如果有意向锁的情况下,可以快速进行判断,只需要判断当前表是否有加意向锁就可以了,减小了性能开销。 **意向共享锁(IS锁)** @@ -361,20 +373,28 @@ innodb_autoinc_lock_mode为2 意向锁之间是兼容的,IS锁和IX是兼容,因为可能我们对第一行数据加S锁,那么会申请IS锁,对第二行数据加X锁,此时跟第一行的数据的S锁不冲突,所以也会先申请IX锁,由此可见,IS锁和IX之间不冲突,IS锁,IX锁与行级别的S,行级别的X之间也不冲突。 -意向锁只是跟表级别的S,X锁可能会冲突。 +![图片](../static/640-1751368.png) + +意向锁只是跟表级别的S,X锁可能会冲突, + +场景1:假设一个事务要加表级别的S锁,让整个表只能被读。那么如果当前有意向锁IX,说明有其他事务在改数据,那么不能加,只能进行等待,等事务改完是否意向锁IX。 -| | 表级别的S锁 | 表级别的X锁 | -| ---- | ----------- | ----------- | -| IS | 兼容 | 不兼容 | -| IX | 不兼容 | 不兼容 | +场景2:假设当前事务要加表级别的S锁时,让整个表只能被读。只有IS意向锁,没有IX锁,说明只是有其他事务在让数据只能被读取,不能被修改,那么加表级别S锁,也不会其他事务造成影响。 + +场景3:假设当前事务要加表级别的X锁时,让整个表只能被这个事务写,不能被其他事务读。如果现在有其他事务加了意向读锁IS,说明有其他事务在让一些数据行只能被读,或者是一些写锁IX,说明其他事务让一些数据行正在被修改。那么当前要加表级别的X锁就不行,会跟其他事务冲突,只能等其他事务执行完毕才能申请成功。 + +| | 表级别的S锁 | 表级别的X锁 | +| ---------- | ----------- | ----------- | +| 意向读锁IS | 兼容 | 不兼容 | +| 意向写锁IX | 不兼容 | 不兼容 | ##### 那么意向锁的作用是什么呢? 假如没有意向锁,我们执行lock table read命令来申请表锁,让整个表只能读,在获得表级别的只读锁之前,需要执行的步骤是: -1.数据库会先判断当前表是否加了排斥锁,因为这个时候要是加了排斥锁,是只能由加了那个排斥锁的事务来更新数据,其他事务都不能读数据,只能阻塞等待。 +1.数据库会先判断当前表是否加了表级别的排斥锁,因为这个时候要是加了排斥锁,是只能由加了那个排斥锁的事务来更新数据,其他事务都不能读数据,只能阻塞等待。 -2.如果当前表没有加排斥锁,那么就需要对每一行数据进行判断,判断是否加了行级别的X锁,如果加了只能阻塞等待,这样需要对一行进行判断,性能开销太大了。 +2.如果当前表没有加表级别的排斥锁,那么就需要对每一行数据进行判断,判断是否加了行级别的X锁,如果加了只能阻塞等待,这样需要对一行进行判断,性能开销太大了。 所以才有了意向锁,在获得表级别的只读锁之前,需要执行的步骤是: @@ -382,7 +402,7 @@ innodb_autoinc_lock_mode为2 2.第二步只需要判断当前锁是否加了表级别的意向排斥锁,因为如果加了意向排斥锁,说明正在有事务在对数据加行锁,对数据进行更新,这样避免了对每一行数据进行判断,判断是否加了行锁。 -####Innodb的锁 +#### Innodb的锁 ##### 行锁 @@ -405,7 +425,7 @@ innodb_autoinc_lock_mode为2 例如num是一个普通索引,非唯一性索引,已有数据是1,5,10,20,30 那么 next-key lock可以锁定的区间是 -``` +```java (负无穷,1] @@ -420,40 +440,26 @@ innodb_autoinc_lock_mode为2 (30,正无穷) ``` -``` +```java //更新操作 update table set note = '1' where num = 10; //或者是使用实时读 SELECT * FROM table WHERE num = 10 for UPDATE; ``` -如果num是唯一性索引,那么只需要对num为10的这条索引加锁就行了,因为不用担心其他事务再插入一条num为10的数据,因为会有唯一性判断。但是如果num是非唯一性索引,为了防止事务执行过程中有num为10的数据插入,那么会对(5,10]和(10,20]这两个区间加锁。 - - - - - -### 死锁是什么? - -死锁就是两个或多个事务在同一资源上相互占用,并且请求对方的占用的资源。InnoDB目前处理死锁的方法就是将持有最少行级排他锁的事务进行回滚。 - - - - +如果num是唯一性索引,那么只需要对num为10的这条索引加锁就行了(就加一个Record lock锁),因为不用担心其他事务再插入一条num为10的数据,因为会有唯一性判断。但是如果num是非唯一性索引,为了防止事务执行过程中有num为10的数据插入,那么会对(5,10]和(10,20]这两个区间加锁。 #### B树是什么? - - ![img](../static/a8773912b31bb05136157756082ed7b24aede072.jpeg) 平衡二叉树就是每个节点左右子树的高度差小于等于1,B树其实是一个平衡多路查找树,假设是M阶的, -1.根节点至少有一个关键字 +1.根节点至少有一个关键字。(这里的关键字可以理解为每个节点的子节点) -2.非根非页节点的关键字数是 需要<=m-1并且>ceil(m/2)-1, +2.非根非页节点的关键字数是 需要<=m-1并且>ceil(m/2)-1。 -3.节点内的元素从左到右递增,左边节点的所有元素值<右边节点的所有元素值 +3.节点内的元素从左到右递增,左边节点的所有元素值<右边节点的所有元素值。 4.叶子节点在同一层,高度一致。 @@ -463,17 +469,19 @@ https://segmentfault.com/a/1190000020416577 https://blog.csdn.net/chai471793/article/details/99563704 -![在这里插入图片描述](../static/20190822142826661.png) +### B树与B+树的区别是什么? + +B+树是为磁盘存储专门设计的一M阶多路平衡查找树(阶数可以理解为每个节点最多的孩子节点的个数,二叉树就是2阶),所有记录节点都是按照从小到大顺序存放在最后一层的叶子节点上,由各叶子节点的指针相连接。可以认为一个叶子节点就是一个内存页(默认情况下,一个内存页大小为16K),每个内存页里面存储多个数据行,内存页直接通过指针连接,形成一个**双向链表**,所以叶子节点在**逻辑上是连续的**,在**物理上不是连续存储的**,就是每个叶子节点可以存储在不同地址上,通过指针相互连接。每个非叶子节点(也就是索引节点)也是一个内存页,里面存储了很多索引节点的值,但是B+树索引节点只存索引值,不存数据行的数据,这样可以让每个索引内存页存储更多的索引值,这样可以使得B+树的层数更少(这也是B+树比B树更优的地方)。B+树在数据库的中实现一般是只有2到4层,机械磁盘一般1秒可以进行100次IO,也意味着每次在B+树中的查询操作可以在20ms到40ms之间完成。 +![image-20210128192759236](../static/image-20210128192759236.png) -### B树与B+树的区别是什么? -1.B树每个节点会保存关键字,索引和数据。而B+树只有叶子节点保存数据,其他节点只保存关键字和索引。所以相同的内存空间可以容纳更多的节点元素。 +1.B树每个节点会保存关键字,索引和数据。而B+树只有叶子节点保存数据,其他节点只保存关键字和索引。所以相同的内存空间可以容纳更多的索引节点。 -2.B+树的所有数据都存在叶子节点上,所以查询会更加稳定,而且相邻的叶子节点都是连接在一起的,更加适合区间查找和搜索, +2.B+树的所有数据都存在叶子节点上,所以查询会更加稳定,而且相邻的叶子节点都是连接在一起的,更加适合区间查找和搜索。 -##### B+树与二叉树区别,优点?为什么不用红黑树? +##### B+树与二叉树区别是什么?为什么不用红黑树? 红黑树是一个平衡的二叉查找树。有以下几个性质: @@ -481,7 +489,7 @@ https://blog.csdn.net/chai471793/article/details/99563704 2.红色节点的子节点必须是黑色的,也就是不能有两个红色节点连续。 -3.从根节点到叶子节点的所有路径包含的黑色节点的个数是一致的。 +3.黑色的节点可以连续,但是从根节点到叶子节点的所有路径包含的黑色节点的个数是一致的。(所以**根节点到叶子节点的最长路径<=最短路径的两倍**) 红黑树是二叉查找树(也就是每个节点的左子树<当前节点的值,右子树所有节点>=当前节点值),但不是严格意义上的平衡二叉树,因为平衡二叉树要求任何节点的左右子树高度差是<=1,红黑树根节点到叶子节点的最长路径会<=最短路径的两倍,所有他是大致意义上的平衡树。 @@ -501,13 +509,13 @@ https://blog.csdn.net/chai471793/article/details/99563704 为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。而B+数中存储的叶子节点在内存中是相邻的,这样可以读取会快一些。 -(三) 存储更多的索引节点 +(三)存储更多的索引节点 -B+树跟B树的区别就是B+是叶子节点存储数据,非叶子节点只存储索引项,B树是所有节点都存储数据,所以B+树的索引节点(非叶子节点)的大小、<、between、like)就停止匹配,比如 a="3" and="" b="4" c="">5 and d=6,如果建立(a,b,c,d)顺序的索引,d是无法使用索引的,如果建立(a,b,d,c)的索引则都可以使用到,a、b、d的顺序可以任意调整。 2. =和in可以乱序,比如 a=1 and b=2 and c=3 建立(a,b,c)索引可以任意顺序,MySQL的查询优化器会帮你优化成索引可以识别的形式。 @@ -676,7 +628,7 @@ SELECT * FROM user WHERE a='2222' OR b='13281899972'//这个就不会走联合 这样的语句可以走联合索引,但是不会走联合索引,因为符号这个范围的数据很多,查出之后,只能获得这些数据的主键,还需要根据主键去聚集索引中查,效率比较低,还不如直接全部扫描,所以直接去聚集索引下顺序得对全表进行扫描。 -``` +```sql SELECT * FROM user WHERE age=1 and height = 2 SELECT * FROM user WHERE age=1 and weight=7 @@ -686,38 +638,34 @@ SELECT * FROM user WHERE weight=7 and age=1 这些是可以走联合索引的, -``` +```sql SELECT * FROM user WEHRE age=1 and height>2 and weight=7 ``` 这个也会走联合索引,查出age=1的索引,然后查出height>2的所有数据,因为height是一个范围查找,所以到weight就不会用索引,会将将这些数据载入内存,根据weight进行筛选。 -索引没有被采用? +##### 索引没有被采用? + 1、根据搜索条件,找出所有可能使用的索引 2、计算全表扫描的代价 3、计算使用不同索引执行查询的代价 4、对比各种执行方案的代价,找出成本最低的那一个 。 ### 覆盖索引是什么? -就是假如有一个联合索引(a,b,c),如果我们只是需要a,b,c这几个字段的数据,查询时就不需要根据主键id去聚集索引里面回表查询了 +就是假如有一个联合索引(a,b,c),如果我们只是需要a,b,c这几个字段的数据,查询时就不需要根据主键id去聚集索引里面回表查询了。 -``` +```SQL SELECT a,b,c FROM user where a = 1 ``` -这个就是覆盖索引。 - -https://mp.weixin.qq.com/s?src=11×tamp=1586078743&ver=2260&signature=s1B5IvEAHXmba8XNgw2q4VXKTN8CpXfX1gjQA*Rcm1M4VpRlWzXAbExHcyLoI2hqJQWNSFZCJ3xdeZsRaph1ZpIAlYt-uTvlgX93gGXhKNiMTm1LCyGosANoDYRHe49N&new=1 - 哪些情况需要建索引: 1. 主键,唯一索引 2. 经常用作查询条件的字段需要创建索引 3. 经常需要排序、分组和统计的字段需要建立索引 4. 查询中与其他表关联的字段,外键关系建立索引 - - ### 哪些情况不要建索引? + 1. 表的记录太少,百万级以下的数据不需要创建索引 2. 经常增删改的表不需要创建索引 @@ -739,13 +687,13 @@ https://mp.weixin.qq.com/s?src=11×tamp=1586078743&ver=2260&signature=s1B5 3.主键是聚集索引,普通所以是非聚集索引。 ### InnoDB和MyISAM的区别是什么? -###### InnoDB +##### InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ Next-Key Locking 防止幻影读。 -主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 +主索引是聚集索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。其次是聚集索引每个叶子节点按照主键id从小到大顺序存储了大量数据行,而每个叶子节点在实现上是一个内存页,磁盘IO读取时是读取一个内存页,会进行一定的预读,所以在进行范围查找时,InnoDB引擎效率更高,而MyISAM则需要对查找范围内所有数据行进行随机读取。(每条数据都是存储在不同地址上) 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 @@ -757,7 +705,7 @@ https://mp.weixin.qq.com/s?src=11×tamp=1586078743&ver=2260&signature=s1B5 ### 什么是分库分表? -一般认为单表数据量在1000万时,查询效率是最合适的,如果超过1000万,执行性能就会下降,可以考虑分库分表 +一般认为单表数据量在1000万时,查询效率是最合适的,如果超过1000万,执行性能就会下降,可以考虑分库分表。 ##### 垂直切分 @@ -769,19 +717,17 @@ https://mp.weixin.qq.com/s?src=11×tamp=1586078743&ver=2260&signature=s1B5 第一种 一般是用主键ID对数据表的数量进行取模运算,得到的余数就是新表的位置。(如果是字段是字符串,那么就根据字符串计算出哈希值,然后除以表的数量,得到新库的位置) -第二种 根据时间来拆分,主表只存储最近两个月的数据,副表存储之前的数据。这种主要是适合哪种访问的数据跟时间相关性比较大的情况,例如统计,我有看搜狐网他们的技术文档,文章PV,UV统计表,就是每天大概有200万的文章有PV,UV,也就是数据库每天会新增200万行的数据,一般来说查文章近期的UV,PV会多一些,查昨日PV,一周PV,或者一个月的PV会多一些。所以是安装时间来划分热库和冷库,一月一个表,或者一天一表。 +第二种 根据时间来拆分,主表只存储最近两个月的数据,副表存储之前的数据。这种主要是适合哪种访问的数据跟时间相关性比较大的情况,例如统计,我有看某新闻网站他们的技介绍,他们的文章会有一个PV,UV统计表,就是每天大概有200万的文章有PV,UV,也就是数据库每天会新增200万行的数据,一般来说查文章近期的UV,PV会多一些,查昨日PV,一周PV,或者一个月的PV会多一些。所以是安装时间来划分热库和冷库,一月一个表,或者一天一表。 -问题 +##### 问题 -事务问题,如果在事务里面操作一个表,然后再操作另外一个表,效率会比较低,然后也比较麻烦。 +1.事务问题,如果在事务里面操作一个表,然后再操作另外一个表,效率会比较低,然后也比较麻烦。 -join - -跨库join的问题 +2.跨库join的问题 在拆分之前,系统中很多列表和详情页所需的数据是可以通过sql join来完成的。而拆分后,数据库可能是分布式在不同实例和不同的主机上,join将变得非常麻烦。而且基于架构规范,性能,安全性等方面考虑,一般是禁止跨库join的。那该怎么办呢?首先要考虑下垂直分库的设计问题,如果可以调整,那就优先调整。(就是尽量不要去分库)如果无法调整的情况,下面有几种解决方案: -1. 全局表 +1.全局表 所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些表。比较类似我们理解的“数据字典”。为了避免跨库join查询,我们可以将这类表在其他每个数据库中均保存一份。同时,这类数据通常也很少发生修改(甚至几乎不会),所以也不用太担心“一致性”问题。 @@ -836,7 +782,7 @@ join 缺点是由于查第二页及以后的页数都需要知道上一页最大时间。 -``` +```java 要查order by time offset offset_value limit num时 获得上一页的最大时间 对每个库查 where time >lastTime order by time offset 0 limit num然后进行排序 @@ -844,7 +790,7 @@ join ##### 每库平均取法 -假设数据分布足够平均,每个库的数据分布都是平均的,假设有四个库,查第一页数据时,每个库都取前0.25页数据,然后合并后返回,取第二页数据时,每个库再都取0.25页。但是这样取得是不准的数据 +假设数据分布足够平均,每个库的数据分布都是平均的,假设有四个库,查第一页数据时,每个库都取前0.25页数据,然后合并后返回,取第二页数据时,每个库再都取0.25页。但是这样取得是不准的数据。 ``` 将order by time offset X limit Y,改写成order by time offset X/N limit Y/N @@ -852,9 +798,9 @@ join ##### 二次查询法 -假如要取offset X后,Y个数据, +假如要取offset 为X,limitY的数据, -1.假设有四个库,需要去每个库查offset X/4 后Y个的数据,然后得到四个结果集中time最小值time_min, +1.假设有四个库,需要去每个库查offset为X/4,limit为Y的数据,然后得到四个结果集,每个结果集的time都有一个时间最小的time_min,时间最大的time_max结果,取四个结果集中最小的time_min 2.然后去每个库去查between time_min到之前每个库的time_max的结果集, @@ -862,9 +808,9 @@ join 4.然后根据全局offset可以计算我们想到的offset X后Y个数据,就是以time_min为起点到距离我们offset的差值。 -关键在于第二步,如果数据库删除过数据,time_min到time_max之间删除过数据,这样就不准, +关键在于两次查询中间,如果数据库删除过这个区间内的数据,time_min到time_max之间删除过数据,这样就不准, -``` +```sql (1)将order by time offset X limit Y,改写成order by time offset X/N limit Y (2)找到最小值time_min @@ -878,7 +824,21 @@ join 首先需要查询两次,而且如果表数据是按照分段来查询的,会有问题。 +参考链接: + +https://blog.csdn.net/uiuan00/article/details/102716457 + +##### Zset记录法 + +例如像百度贴吧,假设一个吧里面允许跳页查询的页数为前100页,每页为20个,,每次顶贴都会把贴子顶到最前面,那么可以使用一个Zset来存储最近的2000个帖子的基本信息(id,标题,图片,等足够贴吧列表页展示的信息),每个帖子的score就是最近更新时间,帖子在Zset中按照更新时间排序。然后每次顶贴都来维护这个Zset就可以了。跳页也可以直接以Log(N)复杂度去Zset中查询到这个帖子。 +### 怎么为订单系统设计数据库的表? + +假设说一个订单系统,用户发起一个订单后,在用户端的需要查看个人订单列表。在商家端也需要查看这个商家最近订单列表。如果用户量比较大肯定是会需要分库分表,比较好的方案是在用户订单表user_order里面存储一个订单表(主键id可以是每个分库自己自增的,也可以是使用snowflake算法生成的),但是用户订单表user_order会根据用户uid进行取模运算,进行分库分表。也就是同一个用户的所有订单都在一个数据库的表中,避免查询时需要多库多表查询。 + +但是此时商家去查询订单列表时,各个订单还是分布在每个分库的user_order表中,为了避免多库查询,还会维护一个商家表business_order,(主键id可以是每个分库自己自增的,也可以是使用snowflake算法生成的),但是商家订单表business_order会根据商家id进行取模运算,得到一个数字,每个商家的订单都存在同一个表中,以此实现分库分表。 + +一般来说user_order可以存所有的订单信息,business_order表只存储少量商家需要的订单信息。 ### MySQL主从复制的工作流程是什么样的? @@ -892,15 +852,15 @@ join ##### 实现原理 -主节点日志发送线程 +**主节点日志发送线程** 当主节点和从节点建立连接后,主服务器上会起一个bin log dump线程,用于给从节点发送bin log日志(日志所包含的信息之外,还包括本次返回的信息的bin-log file 的以及bin-log position),在读取bin log日志时,会对日志文件加锁,读取完成后会解锁。 -从节点I/O线程 +**从节点I/O线程** -从节点与主节点建立连接后会起一个I/O线程来接受主节点发送过来的bin log日志内容,并且保存在从节点的relay log文件中。 +从节点与主节点建立连接后会起一个I/O线程来接受主节点发送过来的bin log日志内容,并且保存在从节点的relay log文件中,保存成功后就会给主节点回复ACK消息,表明接收成功。 -从节点SQL线程 +**从节点SQL线程** 从节点同时会起一个SQL线程,来读取 relay log 中的内容,解析成SQL,并且在从节点上执行,保证和主节点的数据一致性。 @@ -924,36 +884,116 @@ join 就是需要所有的从节点接受日志,并且写入relay log 成功,给主节点返回接受成功的ACK信息,主节点才认为成功,提交事务。 -bin log格式 +### bin log格式 -Statement-base Replication (SBR)语句模式 +**Statement-base Replication (SBR)语句模式** 就是执行什么更新的SQL,就将这些SQL保存到 bin log日志文件中。 缺点在于可能某些情况下导致数据不一致,例如根据now()当前服务器的时间,可能从节点执行语句时的时间跟主节点的语句时间不一样。 -Row-based Relication(RBR) 数据行模式 +**Row-based Relication(RBR) 数据行模式** 就是更新那些数据行,将这些更新的数据行生成SQL,保存在bin log日志文件中。 优点是能确保数据的精准,缺点是会产生大量的日志,日志内容会变大,尤其是一行SQL对大量数据行更新时,而且也不能通过bin log解析当时执行的SQL语句。 -Mixed-format Replication(MBR),MySQL NDB cluster 7.3 和7.4 使用的MBR。是以上两种模式的混合,对于一般的复制使用STATEMENT模式保存到binlog,对于STATEMENT模式无法复制的操作则使用ROW模式来保存,MySQL会根据执行的SQL语句选择日志保存方式 +**Mixed-format Replication(MBR)混合模式** +MySQL NDB cluster 7.3 和7.4 使用的MBR。是以上两种模式的混合,对于一般的复制使用STATEMENT模式保存到binlog,对于STATEMENT模式无法复制的操作则使用ROW模式来保存,MySQL会根据执行的SQL语句选择日志保存方式 +**支持版本** -![](../static/v2-d9ac9c5493d1d772f5bf57ede089f0d5_1440w.jpg) +MySQL 5.1.5 之前 binlog 的格式只有 STATEMENT,5.1.5 开始支持 ROW 格式的 binlog,从 5.1.8 版本开始,MySQL 开始支持 MIXED 格式的 binlog。 +MySQL 5.7.7 之前,binlog 的默认格式都是 STATEMENT。 + +在 5.7.7 及更高版本中,binlog 的默认格式才是 ROW。 + +![](../static/v2-d9ac9c5493d1d772f5bf57ede089f0d5_1440w.jpg) #### 死锁 + +要发生死锁,一般需要满足以下四个必要条件: + +1**.资源互斥**:就是资源不能共享,提现在InnoDB中就是一个事务申请了锁,其他事务就不能申请。 + +2.**不可抢占**:就是一个事务没有获得锁,只能等待拥有锁的事务释放锁,而不能去直接抢占锁。 + +3.**占有且等待**:就是没有获得锁的事务只能进行等待,并且也不会释放自己已拥有的锁。 + +4.**循环等待**:就是事务之间拥有的资源必须形成了一个环,例如事务A拥有一些事务B需要的资源,同时事务A也在申请事务B当前拥有的一些资源。 + +**一、预防死锁的方法:** + +预防死锁的思路主要还是从破坏死锁发生需要的必要条件入手,**资源互斥**这个条件没法去破坏, + +**1.破坏不可抢占条件**:就是当一个进程发现自己申请不到某个资源时,那么就把当前持有的资源全部释放掉,待以后需要使用的时候再重新申请。这意味着进程已占有的资源会被短暂地释放或者说是被抢占了。 + +该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作白费了,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。 + +**2.破坏占有且等待条件**: + +**方法1:执行前申请所有资源** + +所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源,申请成功,进程才能启动。 + +​ 优点:简单易实施且安全。 + +​ 缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,可能会造成每次能启动并运行的进程过少,造成资源浪费。 + +**方法2:边执行边释放资源** + +该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。 + +**3.破坏“循环等待”条件** + +可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号>i的资源。这样资源之间就不会形成环。但是也会造成资源利用率的降低,例如有些进程一开始申请了资源2,哪怕不存在进程竞争,它后续如果需要资源1也申请不到。如下图所示: + +![img](../static/2018051322430635.png) + +**二、避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源** + +这种方法就是在进程申请资源时,系统首先判断**当前可用资源是否够申请**,不够就肯定没法让这个进程运行,就让进程继续等待。够的话继续进行判断,假设把这些资源分配给这个进程,然后通过银行家算法判断**系统是否处于安全状态**,不安全那就不分配,让进程继续等待。 + +银行家算法判断流程: + +假设进程队列里面有P1,P2,P3三个队列,它们持有了一些资源,并且后续执行时还需要资源才能执行完成,假设此时来另一个进程P0,系统判断是否需要给P0分配资源银行家算法先**试探**的分配给它(当然先要看看当前资源池中的资源数量够不够),若申请的资源数量小于等于Available,然后接着判断分配给P0后剩余的资源,能不能使进程队列的某个进程执行完毕,若没有进程可执行完毕,则系统处于不安全状态(即此时没有一个进程能够完成并释放资源,随时间推移,系统终将处于死锁状态)。 + +若有进程可执行完毕,则假设这个进程执行完毕,然后释放已分配给它的资源(剩余资源数量增加),把这个进程标记为可完成,并继续判断队列中的其它进程能否在现有资源数量下执行完毕,**若所有进程都可执行完毕,则系统处于安全状态,并根据可完成进程的分配顺序生成安全序列**(如{P0,P3,P2,P1}表示将P0需要的资源先分配给P0–>P0执行完毕后,P0的资源被回收–>P3所需资源<现有资源,P3执行完毕–>P3的所有资源被回收–>分配给P2–>······满足所有进程执行完毕)。 + +**死锁的检测及解除** + +死锁预防和避免都是对资源分配进行适当限制,属于事前措施,并不利于系统资源的充分共享。而死锁检测不会试图阻止死锁,即在死锁发生前不会做任何操作,只是通过设置的检测机制,检测当前是否发生死锁。若发生死锁,则采取一些措施来解除死锁。 + +判断死锁的法则主要基于第四条死锁的必要条件: + +1.资源分配路径中没有环路,则系统不会出现死锁 + +2.资源分配路径中存在环路,则系统可能出现死锁(如果环路中每种资源只有一个,那么就肯定形成死锁了,如果每种资源有多个,就可能发生死锁) + +解决死锁的方法: + +1.资源剥夺法:剥夺陷入死锁的进程所占用的资源,但并不撤销此进程,再将这些资源分配给需要的进程,直至死锁解除。 + +2.进程撤销法 + +一次性撤销陷入死锁的所有进程,回收所有占用的资源,等死锁解除后,再重新运行进程。 + +逐个撤销陷入死锁的进程,依次回收其资源并重新分配,直至死锁解除。可以优先撤销优先级低、预计剩余执行时间最长、CPU消耗时间少的进程。 + +##### MySQL中的死锁 + +在MySQL中,死锁就是两个或多个事务在同一资源上相互占用,并且请求对方的占用的资源。InnoDB目前处理死锁的方法就是将持有最少行级排他锁的事务进行回滚。 + 除了单条更新语句外,事务获取行锁都是逐步获取的,所以有可能会造成死锁。 解决死锁有两种策略: 1.超时放弃等待 -innodb_lock_wait_timeout,默认是50s,超时会报错 +innodb_lock_wait_timeout,默认是50s,超时会报错。 2.死锁检测 -innodb会有死锁检测,但是会消耗一些cpu资源,检测到死锁会让占有锁最少的事务会滚,释放锁。 +innodb会有死锁检测,但是会消耗一些cpu资源,检测到死锁会让占有锁最少的事务回滚,然后释放锁。 3.控制并发度 -就是控制访问相同资源的并发事务量。例如将长事务拆分成短事务,这样每次事务占用时间也少,也可以减少其他事务的等待时间。 +就是控制访问相同资源的并发事务量。例如将长事务拆分成短事务,这样每次事务占用时间也少,也可以减少其他事务的等待时间,减少死锁发生的概率。 ### 怎么优化数量查询? @@ -962,8 +1002,9 @@ innodb会有死锁检测,但是会消耗一些cpu资源,检测到死锁会 SELECT Count(\*)其实是跟SELECT Count(id)是等价的,会去主键的聚集索引下扫描每一行,然后判断行是否为Null,不为Null计入Count。 ##### Count(col) 也是全表扫描,判断这一行的col值是否为null,不为null,计入Count -怎么优化count(\*)? +##### 怎么优化count(\*)? 可以使用查询一个非空的唯一索引键的数量来替代count(\*),因为count(\*)需要遍历主键的聚集索引的叶子节点,读取每一行的数据,而Count(unique_key)会去unique_key的索引下读取每个叶子的节点,因为每个叶子节点只包含unique_key和主键id,数据大小比聚集索引下的叶子节点下,IO会小一些。 + ##### Myisam可以缓存count,而innodb不能缓存count 因为innodb有事务的概念,如果是在PR的隔离级别下,每个事务查询的count应该等于事务开始时count+本事务执行过程中对count的改变,但是由于每个事务可以单独设置会话隔离级别,所以很难实现对count的缓存。 @@ -981,54 +1022,86 @@ EXPLAIN SELECT * FROM res_user ORDER BYmodifiedtime LIMIT 0,1000 table | type | possible_keys | key |key_len | ref | rows | Extra EXPLAIN列的解释: - table 显示这一行的数据是关于哪张表的 -- type 这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、indexhe和ALL +- type 这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、index和ALL +- ref 表里面的哪个索引被用到了 - rows 显示需要扫描行数 - key 使用的索引 -const就是针对主键或者唯一性索引的等值查询,通过索引查找一次就行了。仅仅是查一条数据。 + const就是针对主键或者唯一性索引的等值查询,通过索引查找一次就行了。仅仅是查一条数据。 -eq_ref 唯一性索引键扫描,对于每个索引键,表中只有一条数据与其对应,例如join查询时,对于前表查出的结果集,每个结果在第二个表只有一条数据对应。 + ref_eq 唯一性索引查询,一般使用一个表去join另外一个表的唯一性索引字段时,就是ref_eq -ref 非唯一性索引查询 + ref 非唯一性索引查询,就是根据普通索引去查找数据,例如查询条件是where a = 1,a是普通索引,但是不是唯一性索引 - ref : 此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了 `最左前缀` 规则索引的查询. 关键字:非唯一性索引 + range: 表示有范围条件的全索引范围查询,跟index全索引扫描相比就是有查询条件,可以减少一些扫描的数据量。同时除了显而易见的between,and以及'>','<'外,in和or也是索引范围扫描。 - ref_or_null:与ref方法类似,只是增加了null值的比较。 + index: 表示全索引扫描(full index scan), 这是另外一种全表扫描,例如order by 字段是索引字段,如果是直接去聚集索引下全表扫描,那么查询出来的结果集还需要在内存中排序一边,如果是去非聚集索引下进行全表扫描,然后按照扫描顺序进行回表,回表的顺序就是order by的顺序,可以减少排序的时间,但是会有回表的开销。 - `range`: 表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中. + all:这个就是全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。 +``` - `index`: 表示全索引扫描(full index scan), 关键字:查询字段和条件都是索引本身 +https://blog.csdn.net/dennis211/article/details/78170079 - index_merge:表示查询使用了两个以上的索引,最后取交集或者并集,常见and ,or的条件使用了不同的索引.效率不是很高 关键字:索引合并 +主要分为以下几个方法: - unique_subquery:用于where中的in形式子查询,子查询返回不重复值唯一值 +**1.减少请求的数据量** - index_subquery:用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重。 +列方面,避免使用SELECT *,只返回必要的列。 - fulltext:全文索引检索,要注意,全文索引的优先级很高,若全文索引和普通索引同时存在时,mysql不管代价,优先选择使用全文索引 +行方面,使用limit语句来限制返回的数据行数。 - all:这个就是全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。 -``` +对频繁访问的数据加缓存,存在redis中。 -主要分为三个方面 +**2.减少表扫描的行数** -* 1.减少请求的数据量 +主要通过使用索引和命中索引来实现,例如如果需要根据发布时间查询最近一天的文章,那么可以根据postTime建立辅助索引,避免进行全表扫描。 -列方面,避免使用SELECT *,只返回必要的列。 +对于一些常用的条件查询字段,建立联合索引,使用一些联合索引,可以减少查询次数,也可以减少磁盘空间占用。而且当查询的字段在索引中已经包含时,就会使用到覆盖索引,此时在辅助索引中查到需要的字段后就不用再根据主键id进行回表查询了。 -行方面,使用limit语句来限制返回的数据行数。 +**4.避免在查询时,对索引字段进行计算和使用函数。** -对频繁访问的数据加缓存,存在redis中。 +这样会导致不通过索引查询,将一些varchar类型的字段与整型数据进行比较时,会触发隐式类型转换,从而使用函数。 + +**5.切分大查询** -* 2.减少服务端扫描的行数,主要通过使用索引和命中索引来实现。 +因为大查询在查询时可能会锁住很多数据,也需要获取到这些数据的行锁才能进行查询,切分成小查询可以减少锁竞争,减少等待获取锁的时间。 -使用索引方面,就是去添加一些索引,以及 +##### 1.使用show profile对一条SQL查询分析当前会话中语句执行的资源消耗情况 -2.1对于一些常用的条件查询字段,建立联合索引,使用一些联合索引,可以减少查询次数,也可以减少磁盘空间占用。而且当查询的字段在索引中已经包含时,就会使用到覆盖索引。此时在索引中查到数据后就不用在回表了。 +1.profiling配置默认是不开启的,可以使用set profiling = ON;命令将配置暂时打开。 -2.2避免在查询时,对索引字段进行计算和使用函数,这样会导致不通过索引查询,将一些varchar类型的字段与整型数据进行比较时,会触发隐式类型转换,从而使用函数。 +2.执行一条查询SQL -* 3.切分大查询,因为大查询在查询时可能会锁住很多数据,也需要获取到这些数据的行锁才能进行查询,切分成小查询可以减少锁竞争,减少等待获取锁的时间。 +3.使用show profiles可以查看最近15条查询SQL及对应的查询idquery id + +4.假设查询id为9,使用这个命令show profile for query 9;可以查看每个步骤及其消耗的时间。 + +``` +mysql> show PROFILE for QUERY 9; ++----------------------+----------+ +| Status | Duration | ++----------------------+----------+ +| starting | 0.000054 | +| checking permissions | 0.000007 | +| Opening tables | 0.000116 | +| init | 0.000019 | +| System lock | 0.000009 | +| optimizing | 0.000004 | +| statistics | 0.000011 | +| preparing | 0.000010 | +| executing | 0.000002 | +| Sending data | 0.000061 | +| end | 0.000005 | +| query end | 0.000006 | +| closing tables | 0.000006 | +| freeing items | 0.000031 | +| cleaning up | 0.000010 | ++----------------------+----------+ +``` + +https://www.cnblogs.com/116970u/p/11004431.html + +https://www.jianshu.com/p/1efdddf3d461 ### char类型与varchar类型的区别? @@ -1051,19 +1124,53 @@ varchar的存储方式是,对每个英文字符占用2个字节,汉字也占 ### 如何优化MySQL慢查询? 首先对EXPLAIN 分析查询语句后extral字段中出现的一些参数进行说明: ##### Using index -使用了索引进行查询 +使用了索引进行查询。 ##### Using where -就是在innodb数据引擎将结果返回后,MySQL Server层对数据进行进一步的过滤,然后返回结果。 + +一般就是where条件中一些判断字段没有索引,那么这个条件的过滤就会在Server端进行 在innodb数据引擎将结果返回给Server层后,MySQL Server层对数据进行进一步的过滤,然后返回结果。 + +例如: + +假设test表只有主键id有索引,name没有索引,那么查询时就是把所有满足id > 100的数据行返回给Server层,然后Server层根据name='123'进行过滤,所有使用explain时会发现extra那里显示的是Using where + +```sql +SELECT * FROM test where id > 100 AND name = '123' +``` + +##### Using index condition + +这个就是在MySQL5.6以后,做的一个优化,例如有一个联合索引(a,b,c) + +查询条件为**where a = '123' AND b like '%aa% AND c like '%cc%'**时,按照联合索引的最左匹配法则,只有a可以用上索引,以前版本的MySQL中,innodb存储引擎就会在联合索引把满足a = '123'的数据的主键id找出来,然后回表,然后把所有数据发送给Sever端,Server端根据b like '%aa% AND c like '%cc%'对数据进行过滤, + +现在有这个push down优化,因为联合索引中有b和c两个字段的信息,innodb在联合索引查找时就会考虑到b like '%aa% AND c like '%cc%'条件,所有回表的数据就都会是满足这三个条件的,然后返回给Server端的也都是满足条件的数据行,Server端就不会再自己进行数据过滤了。 + +https://dev.mysql.com/doc/refman/5.7/en/index-condition-pushdown-optimization.html + ##### Using filesort -如果在关联表查询时,Order By的所有字段都来自第一个表(也就是驱动表),那么在处理驱动表时,从驱动表中取出满足条件的结果集时就会进行排序,不会涉及到临时表。 + +如果在关联表查询时,Order By的所有字段都来自第一个表(也就是驱动表),那么在处理驱动表时,从驱动表中取出满足条件的结果集时就会进行排序,不需要使用临时表存储数据行进行重排序,extral那一列就会显示是Using index。如果Order By的字段不在索引中,那么就需要在查找出结果集后,进行重排序,就会显示Using filesort。 ##### Using temporary; Using fileSort -再进行关联表查询时,如果Order By的字段不全是来自驱动表,那么会把关联结果存放在临时表中,等所有的关联都结束后,再进行文件排序。 + +再进行关联表查询时,如果Order By中的字段不全是来自驱动表,也就是使用了被驱动表中的字段进行排序,那么会把关联结果全部查找出来,存放在临时表中,等所有的关联都结束后,再在内存中对数据行进行排序。 + +##### 优化的方法: + +1.优化索引使用情况。使用explain SQL查看解析结果,首先看结果中Extra那一列是否有Using Index,如果没有看是否是where判断条件的字段没有添加索引,不能使用索引。如果出现的是Using Where,可能是where子句里面判断的字段没有加索引,这样innodb就会把所有数据行查询出来,返回给MySQL Server层,Server层做的过滤。 + +2.减少扫描的行数。查看explain SQL解析结果中rows那一列,看行数是不是特别多,通过添加索引的方式减少扫描的行数。 + +3.只查询我们需要的列。看SQL中的查询字段是不是都是我们需要的,只选取我们需要的字段,而不是所有查询SQL都是使用SELECT *,这些多的字段的存在,会增大查询的时间,以及网络传输的数据量。 + +4.优化join的方式,一般join的字段在被驱动表中有索引,那么join使用的算法就会是index Nested-Loop Join,如果没有索引那么就是block Nested-Loop Join。尽量让join的字段有索引可以使用,是在不行,可以增加join buffer Size的大小(默认是256K)。 + +5.如果单表数据量大于1000万,考虑进行分库分表。 ### 索引的创建步骤是怎么样的? 通过ALTER TABLE ADD/DROP或者CREATE/DROP INDEX 可以创建和删除索引。 -``` +```java //ALTER 命令可以为表增加主键索引,唯一性索引,普通索引 ALTER TABLE table_name add primary key (column_list) ; ALTER TABLE table_name ADD INDEX index_name (column list); @@ -1093,27 +1200,24 @@ Innodb在1.0之后支持Fast Index Creation,就是添加辅助索引(主键 就是innodb在创建索引时,会将数据库的增删改命令写入缓存日志,创建完毕后通过重放日志来保持数据库的最终一致性。 - ### MySQL的join的实现是怎么样的? -https://blog.csdn.net/u010841296/article/details/89790399 - 1.什么是Nested-Loop Join? 2.Index Nested-Loop Join怎么优化连接? 3.Block Nested-Loop Join怎么优化连接? -一.Nested-Loop Join +**Nested-Loop Join** 在Mysql中,使用Nested-Loop Join的算法思想去优化join,Nested-Loop Join翻译成中文则是“嵌套循环连接”。 举个例子: -``` +```SQL select * from t1 inner join t2 on t1.id=t2.tid ``` (1)t1称为外层表,也可称为驱动表。 (2)t2称为内层表,也可称为被驱动表。 //伪代码表示: -``` +```java List result = new ArrayList<>(); for(Row r1 in List t1){ for(Row r2 in List t2){ @@ -1123,7 +1227,7 @@ for(Row r1 in List t1){ } } ``` -在Mysql的实现中,Nested-Loop Join有3种实现的算法: +在Mysql的实现中,Nested-Loop Join具体有3种实现的算法: Simple Nested-Loop Join:SNLJ,简单嵌套循环连接 Index Nested-Loop Join:INLJ,索引嵌套循环连接 @@ -1131,42 +1235,198 @@ Block Nested-Loop Join:BNLJ,缓存块嵌套循环连接 在选择Join算法时,会有优先级,理论上会优先判断能否使用INLJ、BNLJ: Index Nested-LoopJoin > Block Nested-Loop Join > Simple Nested-Loop Join -##### 二.Simple Nested-Loop -简单嵌套循环连接实际上就是简单粗暴的嵌套循环,如果table1有1万条数据,table2有1万条数据,那么数据比较的次数=1万 * 1万 =1亿次,这种查询效率会非常慢。 +##### 一.Simple Nested-Loop + +简单嵌套循环连接实际上就是简单粗暴的嵌套循环,如果table1有1万条数据,table2有1万条数据,那么数据比较的次数=1万 * 1万 =1亿次,这种查询效率会非常慢。实现伪代码就跟上面的一样。 所以Mysql继续优化,然后衍生出Index Nested-LoopJoin、Block Nested-Loop Join两种NLJ算法。在执行join查询时mysql会根据情况选择两种之一进行join查询。 -##### 三.Index Nested-LoopJoin(减少内层表数据的匹配次数) +##### 二.Index Nested-LoopJoin(减少内层表数据的匹配次数) + 索引嵌套循环连接是基于索引进行连接的算法,索引是基于内层表的,通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录进行比较, 从而利用索引的查询减少了对内层表的匹配次数,优势极大的提升了 join的性能: -原来的匹配次数 = 外层表行数 * 内层表行数 -优化后的匹配次数= 外层表的行数 * 内层表索引的高度 +**原来的匹配次数 = 外层表行数 * 内层表行数** +**优化后的匹配次数= 外层表的行数 * 内层表索引的高度** + +伪代码实现: + +```java +select * from t1 inner join t2 on t1.id=t2.tid +/* +假设要执行上面的这个查询,然后t1作为外层表(也就是驱动表),每次从t1表中取一行数据出来,然后根据t1.id去t2表的聚集索引下查询主键id等于t1.id的数据行,如果存在,就添加到结果集中,所以 + 每次查询的匹配次数=t2表的聚集索引的层数 + 总的查询匹配次数=外层表t1的行数 * t2表的聚集索引的层数 + */ +List result = new ArrayList<>(); +for(Row r1 in List t1){ + Row> r2 = searchBTree(t1.id); + result.add(r1.join(r2)); +} +``` 使用场景:只有内层表join的列有索引时,才能用到Index Nested-Loop Join进行连接。 -使用Index Nested-Loop Join算法时SQL的EXPLAIN结果extral列是Using index。 +使用**Index Nested-Loop Join**算法时SQL的EXPLAIN结果extral列是**Using index**。 由于用到索引,如果索引是辅助索引而且返回的数据还包括内层表的其他数据,则会回内层表查询数据,多了一些IO操作。 -#### 四.Block Nested-Loop Join(减少内层表数据的循环次数) +##### 三.Block Nested-Loop Join(减少内层表数据的循环次数) + +当有时候Join字段没法使用索引的时候,那样就不能用Index Nested-Loop Join的时候,次数默认就会使用Block Nested-Loop Join。 + +大体的原理 -缓存块嵌套循环连接通过一次性缓存多条数据,把参与查询的列缓存到Join Buffer 里,然后拿join buffer里的数据批量与内层表的数据进行匹配,从而减少了内层循环的次数(遍历一次内层表就可以批量匹配一次Join Buffer里面的外层表数据)。 -当不使用Index Nested-Loop Join的时候,默认使用Block Nested-Loop Join。 +Block Nested-Loop Join通过一次性缓存多条数据,把参与查询的列缓存到Join Buffer 里,然后拿join buffer里的数据批量与内层表的数据进行匹配,从而减少了内层循环的次数(遍历一次内层表就可以批量匹配一次Join Buffer里面的外层表数据)。 -使用Block Nested-Loop Join算法时SQL的EXPLAIN结果extral列是Using join buffer +伪代码表示 + +```java +select * from t1 inner join t2 on t1.tid=t2.tid +假设字段tid在t1表,t2表中都没有建立索引,那么查找时就不能使用索引了,采用Block Nested-Loop Join算法就是每次从驱动表t1中加载一部分数据行到内存缓冲区Join Buffer 中来,然后对t2表进行全表扫描,扫描时每次拿t2表中的数据行与Join Buffer中的数据进行匹配,匹配完成就添加到结果集。 + 所以全表扫描的次数=驱动表t1的行数/Join Buffer的大小。 +因为Join Buffer是内存缓冲区,在内存中进行元素比较是比较快的,而对t2表进行全表扫描是磁盘Io,是比较慢的,所以应该是尽可能减少全表扫描的次数。所以优化的方式一般是增大Join Buffer的大小,或者是选取数据量小的表作为驱动表,这样可以减少全表扫描的次数,减少磁盘IO。 + +List result = new ArrayList<>(); +//可以把subList理解为每次从t1表中取出,加载到join buufer的那一部分数据 +for( List subList in List t1){ + for(Row r2 in List t2){ + if(subList.contains(r2.tid){ + result.add(r1.join(r2)); + } + } +} +``` + +使用**Block Nested-Loop Join**算法时SQL的EXPLAIN结果extral列是**Using join buffer** 什么是Join Buffer? (1)Join Buffer会缓存所有参与查询的列而不是只有Join的列。 (2)可以通过调整join_buffer_size缓存大小 -(3)join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL 5.1.22版本前是4G-1,而之后的版本才能在64位操作系统下申请大于4G的Join Buffer空间。 +(3)join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL 5.1.22版本前是4G,而之后的版本才能在64位操作系统下申请大于4G的Join Buffer空间。 (4)使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置block_nested_loop为on,默认为开启。 -五.如何优化Join速度 -用小结果集驱动大结果集,减少外层循环的数据量: + +https://blog.csdn.net/u010841296/article/details/89790399 + +https://mp.weixin.qq.com/s/8W3RzKE1HjKifGzJvzBYzA + +##### 怎么如何优化Join速度? +1.用小结果集驱动大结果集,减少外层循环的数据量: 如果小结果集和大结果集连接的列都是索引列,mysql在内连接时也会选择用小结果集驱动大结果集,因为索引查询的成本是比较固定的,这时候外层的循环越少,join的速度便越快。 -为匹配的条件增加索引:争取使用INLJ,减少内层表的循环次数 -增大join buffer size的大小:当使用BNLJ时,一次缓存的数据越多,那么外层表循环的次数就越少 -减少不必要的字段查询: +为匹配的条件增加索引:争取使用Index Nested-Loop Join,减少内层表的循环次数 +2.增大join buffer size的大小:当使用BNLJ时,一次缓存的数据越多,那么外层表循环的次数就越少。 +3.减少不必要的字段查询: (1)当用到BNLJ时,字段越少,join buffer 所缓存的数据就越多,外层表的循环次数就越少; (2)当用到INLJ时,如果可以不回表查询,即利用到覆盖索引,则可能可以提示速度。(未经验证,只是一个推论) -六.参考文档 + +4.排序时尽量使用驱动表中的字段 + +因为如果使用的是非驱动表中的字段会对非驱动表(的字段排序)需要对循环查询的合并结果(临时表)进行排序,比较耗时,使用Explain时会发现出现Using temporary。 + +参考文档 https://www.wengbi.com/thread_99558_1.html https://www.cnblogs.com/starhu/p/6418842.html https://www.cnblogs.com/starhu/p/6418833.html + +### Join的工作流程是怎么样的,怎么进行优化? + +##### join的大概使用是怎么样的? + +full outer join 会包含两个表不满足条件的行 + +left join 会包含左边的表不满足条件的行,一般会使用左边的表作为驱动表。 + +right join 会包含右边的表不满足条件的行,一般会使用右边的表作为驱动表。 + +inner join 就是只包含满足条件的行 + +cross join 从表A循环取出每一条记录去表B匹配,cross join 后面不能跟on,只能跟where + +##### exits 和in,join的区别是什么? + +exists是拿外表作为驱动表,外表的数据做循环,每次循环去内表中查询数据,使用适内表比较大的情况 + +例如 + +select * from t1 where t1.tid exists (select t2.tid from t2) + +转换为伪代码为: + +```java +//就是t1作为驱动表 +List result = new ArrayList<>(); +for(Row r1 in List t1){ + for(Row r2 in List t2){ + if(r1.id = r2.tid){ + result.add(r1.join(r2)); + } + } +} +``` + +而 in的话正好相反,是那内表作为驱动表,内表的数据做循环,每次循环去外表查询数据,适合内表比较小的情况。 + +``` +select * from A where cc in (select cc from B) +-- 效率低,用到了A表上cc列的索引; + +select * from A where exists(select cc from B where cc=A.cc) +-- 效率高,用到了B表上cc列的索引。 +``` + +not in 和not exists如果查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引;而not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。 + +join的实现其实是先从一个表中找出所有行(或者根据where子句查出符号条件的行),然后去下一个表中循环寻找匹配的行,依次下去,直到找到所有匹配的行,使用join不会去创建临时表,使用in的话会创建临时表,销毁临时表 + +所以不管是in子查询,exists子查询还是join连接查询,底层的实现原理都是一样的,本质上是没有任何区别的,关键的点在关联表的顺序,如果是join连接查询,MySQL会自动调整表之间的关联顺序,选择最好的一种关联方式。和上面in和exists比较的结论一样,小表驱动大表才是最优的选择方式。 + +### MySQL怎么排查使用率低的索引? + +MySQL 5.5以后,有一个`performance_schema`的配置,默认为不开启,开启后,可以统计MySQL数据库的性能进行监测统计,会将内存使用情况,SQL语句使用情况,IO读取情况等等进行统计,并将统计结果写入到performance_schema这个数据库中,这个数据库里面有很多表,记录了性能统计结果。在MySQL bentch中有一个界面会展示出没有使用到索引的结果。 + +例如图中就是我们test数据库中有一个activity表有一个index索引没有使用到。 + +![image-20210204164409753](../static/image-20210204164409753.png) + +这个界面的数据来源也是来自于`performance_schema`这个数据库,里面总共有52个性能统计结果表,其中有一个table_io_waits_summary_by_index_usage表,里面统计了索引使用情况,里面有统计索引在查询,插入,更新,删除语句中使用到的次数。 + +```SQL +SELECT + object_type,//类型,这里是table + object_schema,//索引所在的数据库名 + object_name,//索引所在的表名 + index_name,//索引名称 +COUNT_FETCH,//这个索引在查询语句中使用到的的次数 +COUNT_INSERT,//这个索引在插入语句中使用到的的次数 +COUNT_UPDATE,//这个索引在更新语句中使用到的的次数 +COUNT_DELETE//这个索引在删除语句中使用到的的次数 +FROM + PERFORMANCE_SCHEMA.table_io_waits_summary_by_index_usage; +``` + +这个是查询结果,可以看到idx_ipport这个索引在查询语句中用到了1次。 + +![image-20210204170229799](../static/image-20210204170229799.png) + +table_io_waits_summary_by_index_usage这个表里面还有更详细的统计数据,具体可以看看下面这个链接,里面有介绍 + +参考链接 + +https://www.cnblogs.com/cchust/p/5057498.html + +https://www.cnblogs.com/cchust/p/5061131.html + +### 数据库设计的三大范式是什么? + +1.确保数据库表的每一列的原子性 + +保证每一列的值是原子性的,不可分割的,而不是在查询时需要分割使用函数来查询,降低查询效率。 + +例如说有一个列是存储地址,如果你要根据省份查询,直接从地址中查可能会使用到函数,降低查询效率,最好是单独将省份另外存一份。 + +2.确保数据库表的每一列都与主键相关 + +就是表里面只应该存储一种数据,而不是多种数据,例如订单表就应该存储订单,其中包含商品的id就行了,而不是存储一些商品的具体名称,价格等信息。避免数据冗余,增大维护成本。 + +3.确保数据库表的每一列直接依赖主键,而不是间接依赖主键。 + +这一条其实更第二范式差不多。 + +https://www.cnblogs.com/linjiqin/archive/2012/04/01/2428695.html \ No newline at end of file diff --git a/docs/MySQLOptimize.md b/docs/MySQLOptimize.md index 6e98d51..1cc2260 100644 --- a/docs/MySQLOptimize.md +++ b/docs/MySQLOptimize.md @@ -1,10 +1,10 @@ -MySQL慢查询优化(真实案例调优) +## 文章说明 +这篇文章主要是记录自己最近在真实工作中遇到的慢查询的案例,然后进行调优分析的过程,欢迎大家一起讨论调优经验。(以下出现的表名,列名都是化名,实际数据也进行过一点微调。可能文章比较贴近实践,已经被51CTO的编辑申请转载了) -这篇文章主要是记录自己最近在真实工作中遇到的慢查询的案例,然后进行调优分析的过程,欢迎大家一起讨论调优经验。(以下出现的表名,列名都是化名,实际数据也进行过一点微调。) -### 复杂的深分页问题优化 +## 一.复杂的深分页问题优化 -##### 背景 +#### 背景 有一个article表,用于存储文章的基本信息的,有文章id,作者id等一些属性,有一个content表,主要用于存储文章的内容,主键是article_id,需求需要将一些满足条件的作者发布的文章导入到另外一个库,所以我同事就在项目中先查询出了符合条件的作者id,然后开启了多个线程,每个线程每次取一个作者id,执行查询和导入工作。 @@ -26,7 +26,7 @@ LIMIT 210000,100 然后我们开始分析这条命令执行慢的原因: -##### 是否是联合索引的问题 +#### 是否是联合索引的问题 当前是索引情况如下: @@ -41,26 +41,29 @@ content表的主键是article_id 流程确实是这个流程,但是去查询时,如果limit还是210000, 100时,还是查不出数据,几分钟都没有数据,一直到navica提示超时,使用Explain看的话,确实命中索引了,如果将offset调小,调成6000, 100,勉强可以查出数据,但是需要46s,所以瓶颈不在这里。 -真实原因如下: +### 查询慢的原因 -先看关于深分页的两个查询,id是主键,val是普通索引 - -##### 直接查询法 - -```SQL -select * from test where val=4 limit 300000,5; -``` -##### 先查主键再join ```SQL -select * from test a -inner join -(select id from test where val=4 limit 300000,5) as b -on a.id=b.id; +SELECT + a.*, c.* +FROM + article a +LEFT JOIN content c ON a.id = c.article_id +WHERE + a.author_id = 1111 +AND a.create_time < '2020-04-29 00:00:00' +LIMIT 210000,100 ``` +首先我们需要知道innodb引擎在执行时,并不了解我们的业务规则,它是不知道article表中如果有一篇文章存在,那么在content表里面一定会有这篇文章的内容信息,也就是它不知道article表的id在content表中一定会有一个article_id与之对应。所以innodb引擎的执行流程是这样: + +1.先去article表中找出满足`a.author_id = 1111 +AND a.create_time < '2020-04-29 00:00:00'`条件的22000条数据的所有字段,加载到内存中。(在MySQL进行join时,加载到内存中并不只是join字段,而是SELECT 的所有字段,很容易理解,如果只是join的字段,那么最后还需要根据join的字段去回表。) -这两个查询的结果都是查询出offset是30000后的5条数据,区别在于第一个查询需要先去普通索引val中查询出300005个id,然后去聚集索引下读取300005个数据页,然后抛弃前面的300000个结果,只返回最后5个结果,过程中会产生了大量的随机I/O。第二个查询一开始在普通索引val下就只会读取后5个id,然后去聚集索引下读取5个数据页。 +2.然后根据这22000数据去content表里面查找文章内容相关的字段。(由于content表存储了文章内容,一些字段是特别大的,是不会存储在聚簇索引的叶子节点中的,而且存储在其他地方,所以会产生大量随机IO,这是导致这个查询这么慢的原因。) -同理我们业务中那条查询其实是更加**复杂**的情况,因为我们业务的那条SQL不仅会读取article表中的210100条结果,而且会每条结果去content表中查询文章相关内容,而这张表有几个TEXT类型的字段,我们使用show table status命令查看表相关的信息发现 +3.最终把22000条数据返回给MySQL Server,取最后面的100条数据,返回给客户端。 + +使用show table status命令查看article表和content表显示的数据行平均长度 | Name | Engine | Row_format | Rows | Avg_Row_length | | ------- | ------ | ---------- | ------- | -------------- | @@ -71,7 +74,7 @@ on a.id=b.id; (详细了解可以看看这篇文章[深度好文带你读懂MySQL和InnoDB](https://mp.weixin.qq.com/s?src=11×tamp=1588316993&ver=2311&signature=wlqIQrV2ZK4JJhqP4E1hqr8j3SBaQSEaiPoPM2KlAF9z-*jpWnwYiORweW3LDIWfY2J6LY8coaqXDMFezKZvEIEGRIaMEs5G*0N4naBh9DBCmUjRQnvuluU8Q5LOPttc&new=1)) -![img](https://user-gold-cdn.xitu.io/2020/5/1/171cf4968afd910e?w=640&h=144&f=jpeg&s=5598) +![img](../static/f10940650d2d478a9c71bce1d9a0db3a~tplv-k3u1fbpfcp-zoom-1.image) 这样再从content表里面查询连续的100行数据时,读取每行数据时,还需要去读溢出页的数据,这样就需要大量随机IO,因为机械硬盘的硬件特性,随机IO会比顺序IO慢很多。所以我们后来又进行了测试, @@ -93,11 +96,11 @@ FROM article_content c WHERE c.article_id in(100个article_id) ``` -#### 解决方案 +### 解决方案 所以针对这个问题的解决方案主要有两种: -##### 先查出主键id再inner join +#### 先查出主键id再inner join 非连续查询的情况下,也就是我们在查第100页的数据时,不一定查了第99页,也就是允许跳页查询的情况,那么就是使用**先查主键再join**这种方法对我们的业务SQL进行改写成下面这样,下查询出210000, 100时主键id,作为临时表temp_table,将article表与temp_table表进行inner join,查询出中文章相关的信息,并且去left Join content表查询文章内容相关的信息。 第一次查询大概1.11s,后面每次查询大概0.15s @@ -115,13 +118,13 @@ INNER JOIN( LEFT JOIN content c ON a.id = c.article_id ``` -##### 优化结果 +#### 优化结果 优化前,offset达到20万的量级时,查询时间过长,一直到超时。 优化后,offset达到20万的量级时,查询时间为1.11s。 -##### 利用范围查询条件来限制取出的数据 +#### 利用范围查询条件来限制取出的数据 这种方法的大致思路如下,假设要查询test_table中offset为10000的后100条数据,假设我们事先已知第10000条数据的id,值为min_id_value @@ -139,13 +142,13 @@ while(min_id1M**,实际分配空间=len长度来存储字符串+1字节存末尾空字符+1M长度的预分配空闲内存 + 所以Redis中的简单动态字符串结构,除了包含一个字符数组的属性,还包含数组的长度,数组的实际使用长度等属性,通过增加长度属性,可以保证字符串是二进制安全的,从而可以保存任意类型的数据,例如一张图片,对象序列化后的数据等等。 ##### 字符串使用场景如下: @@ -32,8 +40,8 @@ Redis中的简单动态字符串其实是对C语言中的字符串的封装和 在Redis中,每一个Value都是一个Redis对象,对应的都是RedisObject结构,在RedisObject结构中,保存了对象的类型type,底层的编码encoding等一些属性,也拥有一个ptr指针,指向对象具体的存储地址。 ``` struct RedisObject { - int4 type; - int4 encoding; + int4 type; //类型 + int4 encoding; //编码 int24 lru; int32 refcount; void *ptr; @@ -41,6 +49,7 @@ struct RedisObject { ``` 在Redis中,字符串有两种存储方式,int编码,embstr编码和raw编码。 ##### int编码 + 当value是一个整数,并且可以使用long类型(8字节)来表示时,那么会属于int编码,ptr直接存储数值。(并且Redis会进行优化,启动时创建0~9999的字符串对象作为共享变量。) ##### embstr和raw编码 两种存储方式下,都RedisObject和SDS结构(简单动态字符串)来存储字符串,区别在于,embstr对象用于存储较短的字符串,embstr编码中RedisObject结构与ptr指向的SDS结构在内存中是连续的,内存分配次数和内存释放次数均是一次,而raw编码会分别调用两次内存分配函数来分别创建RedisObject结构和SDS结构。 @@ -48,6 +57,7 @@ struct RedisObject { ### 谈一谈你对Redis中hash对象的理解? 在Redis中,value可以是一个hash表,底层编码可以是ziplist,也可以是hashtable(默认情况下,当元素小于512个时,底层使用ziplist存储数据) #### ziplist + 元素保存的字符串长度较短且元素个数较少时(小于64字节,个数小于512),出于节约内存的考虑,hash表会使用ziplist作为的底层实现,ziplist是一块连续的内存,里面每一个节点保存了对应的key和value,然后每个节点很紧凑地存储在一起,优点是没有冗余空间,缺点插入新元素需要调用realloc扩展内存。(可能会进行内存重分配,将内容拷贝过去,也可能在原有地址上扩展)。 #### hashtable @@ -88,7 +98,7 @@ typedef struct dictEntry { 1.首先将对dict结构中ht[1]哈希表分配空间(大小取决于最接近实际使用空间的2的n次方幂),然后将rehashindex属性设置为0。 ##### 转移 -2.然后每次对ht[0]中的元素查找,修改,添加时,除了执行指定操作外,还会将对应下标的所有键值对rehash到ht[1],并且会将rehashindex+1。 +2.这种渐进式rehash转移元素不是全部转移,如果在rehash时全部转移,可能会导致rehash期间Redis实例无法处理后面的请求,所以采取的策略是每次用到某个下标下的键值对才进行转移这个下标下所有的键值对。然后每次对ht[0]中的元素查找,修改,添加时,除了执行指定操作外,还会将对应下标的所有键值对rehash到ht[1],并且会将rehashindex+1。 ##### 更改指针指向 3.当ht[0]中所有键值对都是rehash到ht[1]中后,那么会ht[1]和ht[0]的指针值进行交换,将rehashindex设置为-1,代表rehash完成。 @@ -113,21 +123,28 @@ Set是一个无序的,不重复的字符串集合,底层编码有inset和has 当元素个数较多时,Set使用hashtable来保存元素,元素的值作为key,value都是NULL。 ### 谈一谈你对Redis中有序集合ZSet的理解? + Zset与Set的区别在于每一个元素都有一个Score属性,并且存储时会将元素按照Score从低到高排列。底层是通过跳跃表实现的。 ##### ziplist -当元素较少时,ZSet的底层编码使用ziplist实现,所有元素按照Score从低到高排序。 +当元素较少时(元素个数<128个,且每个元素的长度小于64字节),ZSet的底层编码使用ziplist实现,所有元素按照Score从低到高排序。 ##### skiplist+dict -当元素较多时,使用skiplist+dict来实现, +当元素较多时,使用skiplist+dict来实现。 skiplist存储元素的值和Score,并且将所有元素按照分值有序排列。便于以O(logN)的时间复杂度插入,删除,更新,及根据Score进行范围性查找。 dict存储元素的值和Score的映射关系,便于以O(1)的时间复杂度查找元素对应的分值。 ### 布隆过滤器是什么? -布隆过滤器可以理解为一个有误差的set结构,使用布隆过滤器来判断元素是否存在其中时,如果返回结果是存在,实际可能存在也可能不存在,返回结果不存在时,实际结果肯定是不存在。布隆过滤器实际上是一个大型的位数组,添加key时,通过几个hash函数对key计算得到多个hash值,将每个hash值与布隆过滤器的位数组的size取模得到下标,然后将数组中这些下标位置的值都设置为1。 +布隆过滤器可以理解为一个有误差的set结构,使用布隆过滤器来判断元素是否存在其中时, + +如果返回结果是存在,实际可能存在也可能不存在, + +返回结果不存在时,实际结果肯定是不存在。 + +布隆过滤器实际上是一个大型的位数组,添加key时,通过几个hash函数对key计算得到多个hash值,将每个hash值与布隆过滤器的位数组的size取模得到下标,然后将数组中这些下标位置的值都设置为1。 创建key为userid的布隆过滤器,0.01是误判率,10000是初始大小 @@ -180,24 +197,83 @@ f(h)=n/(2^h) 但是这种全索引的结构有缺点,就是之后在插入新节点时,是没有办法为新节点添加一些新的索引节点的。所以有可能会导致一个小区间内有很多个节点,查找时近似于单链表,这样时间复杂度就变成O(N)了,所以Redis对跳跃表做了一些优化,可以简单认为会计算出一个随机数,代表这个添加添加的索引层数,然后进行添加。同时假设节点拥有k层索引的概率f(k),节点拥有k-1层索引结构的概率为f(k-1),f(k) = f(k-1)*P,假设p为1/2,那么也就是f(k)=f(k-1)\*0.5,那么第k层的索引节点数也就是为k-1层的0.5倍。最终索引结构跟上面的这种全索引结构是类似的,只是索引节点不一定分布均匀。空间复杂度=每一层索引节点数=n+n/2+n/4….=2n。Redis做了优化,发现P取1/4时,时间复杂度跟P取1/2是一样的,并且空间复杂度更低。 -``` -private int randomLevel() { - int level = 1; - // 当 level < MAX_LEVEL,且随机数小于设定的晋升概率时,level 1 - while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)//MAX_LEVEL为32 - level += 1; - return level; +```java +//ZSKIPLIST_P代表获取下一层的概率,是0.25 +//ZSKIPLIST_MAXLEVEL代表是最大层数,是32层 +int zslRandomLevel(void) { + int level = 1; + while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) + level += 1; + return (level server.aof_rewrite_min_size(默认为1MB)。 +4. 当前AOF文件大小和最后一次AOF重写后的大小之间的比率大于等于指定的增长百分比(默认为1倍,100%,也就是当前AOF文件大小>=上次重写后文件的2倍后) ### Redis持久化策略该如何进行选择? RDB持久化的特点是: @@ -107,7 +111,8 @@ AOF持久化主要在于将aof_buf缓冲区的数据同步到磁盘时会有I/O 一般性的故障(停电,关机)不会影响到磁盘,但是一些灾难性的故障(地震,洪水)会影响到磁盘,所以需要定时把单机上或从服务器上的AOF文件,RDB文件备份到其他地区的机房。 ### 什么是AOF文件追加阻塞? -修改命令添加到aof_buf之后,如果配置是everysec那么会每秒执行fsync操作,调用write写入磁盘一次,但是如果硬盘负载过高,fsync操作可能会超过1s,Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快,所以Redis的处理逻辑是会对比上次fsync成功的时间,如果超过2s,则主线程阻塞直到fsync同步完成,所以最多可能丢失2s的数据,而不是1s。 + +修改命令添加到aof_buf之后,如果配置是everysec,那么会有一个线程每秒执行fsync操作,调用write写入磁盘一次,但是如果此时来了很多Redis请求,Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快,fsync操作可能会超过1s,aof_buf缓冲区堆积的命令会越来越多,所以Redis的处理逻辑是会对比上次fsync成功的时间,如果超过2s,则主线程阻塞直到fsync同步完成,所以最多可能丢失2s的数据,而不是1s。(每当 AOF 追加阻塞事件发生时,在 info Persistence 统计中,aof_delayed_fsync 指标会累加,查看这个指标方便定位 AOF 阻塞问题。) ### 什么是RDB-AOF混合持久化? @@ -121,7 +126,7 @@ redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混 了解了AOF持久化过程和RDB持久化过程以后,混合持久化过程就相对简单了。 -混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图: +混合持久化同样也是通过BGREWRITEAOF完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图: ![img](../static/1075473-20180726181756270-1907770368.png) @@ -138,7 +143,7 @@ redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混 优点: -1. 混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。 +1. 混合持久化结合了RDB持久化和 AOF 持久化的优点, 由于绝大部分都是RDB格式,生成的AOF文件体积更小,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。 缺点: diff --git a/docs/RedisUserful.md b/docs/RedisUserful.md index 673594f..2695ea5 100644 --- a/docs/RedisUserful.md +++ b/docs/RedisUserful.md @@ -8,24 +8,26 @@ #### [3.客户端是怎么接入哨兵系统的?](#客户端是怎么接入哨兵系统的?) #### [4.Redis哨兵系统是怎么实现自动故障转移的?](#Redis哨兵系统是怎么实现自动故障转移的?) +#### [5.谈一谈你对Redis Cluster的理解?](#谈一谈你对RedisCluster的理解?) +#### [6.RedisCluster是怎么实现数据分片的?](#RedisCluster是怎么实现数据分片的?) +#### [7.RedisCluster是怎么做故障转移和发现的?](#RedisCluster是怎么做故障转移和发现的?) ### Redis主从同步是怎么实现的? -主从节点建立连接后,从节点会进行判断 +主从节点建立连接后,从节点会进行判断: -1.如果这是从节点之前没有同步过数据,属于初次复制,会进行**全量重同步** -那么从节点会向主节点发送PSYNC?-1 命令,请求主节点进行**全量重同步**。 +1.如果这是从节点之前没有同步过数据 +属于初次复制,会进行**全量重同步**,那么从节点会向主节点发送PSYNC?-1 命令,请求主节点进行**全量重同步**。 -2.如果这是从节点不说初次复制(例如出现掉线后重连), -这个时候从节点会将之前进行同步的Replication ID(一个随机字符串,标识主节点上的特定数据集)和offset(从服务器当前的复制偏移量)通过PSYNC 命令发送给主节点,主节点会进行判断, +2.如果从节点不是初次复制(例如出现掉线后重连) +这个时候从节点会将之前进行同步的Replication ID(一个随机字符串,标识主节点上的特定数据集)和offset(从服务器当前的复制偏移量)通过PSYNC id offset命令发送给主节点,主节点会进行判断, -* 如果Replication ID跟当前的Replication ID不一致,或者是当前buffer缓冲区中不存在对应的offset,那么会跟上面的初次复制一样,进行**全量重同步**。 +* 如果Replication ID跟当前的Replication ID不一致(可能主节点进行了变化),或者是当前buffer缓冲区中不存在对应的offset,那么会跟上面的初次复制一样,进行**全量重同步**。 * 如果Replication ID跟当前的Replication ID一致并且当前buffer缓冲区中存在对应的offset,那么会进行**部分重同步**。(部分重同步是Redis 2.8之后的版本支持的,主要基于性能考虑,为了断线期间的小部分数据修改进行全量重同步效率比较低) ##### 全量重同步 -主节点会执行BGSAVE命令,fork出一个子进程,在后台生成一个RDB持久化文件,完成后,发送给 -从服务器,从节点接受并载入RDB文件,使得从节点的数据库状态更新至主节点执行BGSAVE命令时的状态。并且在生成RDB文件期间,主节点也会使用一个缓冲区来记录这个期间执行的所有写命令,将这些命令发送给从节点,从节点执行命令将自己数据库状态更新至与主节点完全一致。 +主节点会执行BGSAVE命令,fork出一个子进程,在后台生成一个RDB持久化文件,完成后,发送给从服务器,从节点接受并载入RDB文件,使得从节点的数据库状态更新至主节点执行BGSAVE命令时的状态。并且在生成RDB文件期间,主节点也会使用一个缓冲区来记录这个期间执行的所有写命令,将这些命令发送给从节点,从节点执行命令将自己数据库状态更新至与主节点完全一致。 ##### 部分重同步 因为此时从节点只是落后主节点一小段时间的数据修改,并且偏移量在复制缓冲区buffer中可以找到,所以主节点把从节点落后的这部分数据修改命令发送给从节点,完成同步。 @@ -34,23 +36,17 @@ 在执行全量重同步或者部分重同步以后,主从节点的数据库状态达到一致后,会进入到命令传播阶段。主节点执行修改命令后,会将修改命令添加到内存中的buffer缓冲区(是一个定长的环形数组,满了时就会覆盖前面的数据),然后异步地将buffer缓冲区的命令发送给从节点。 ### Redis中哨兵是什么? -Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器,核心功能是监测主节点和从节点的运行情况,在主节点出现故障后, -完成自动故障转移,让某个从节点升级为主节点。 +Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器,核心功能是监测主节点和从节点的运行情况,在主节点出现故障后,完成自动故障转移,让某个从节点升级为主节点。 ### 客户端是怎么接入哨兵系统的? -首先Redis中的哨兵节点是一个配置提供者,而不是代理。 -区别在于, -##### 配置提供者 -前者只负责存储当前最新的主从节点信息,供客户端获取。 -##### 代理 -客户端所有请求都会经过哨兵节点。 - -所以实际开发中,通过在客户端配置哨兵节点的地址+主节点的名称(哨兵系统可能会监控多个主从节点,名称用于区分)就可以获取到主节点信息, -下面的代码在底层实现是客户端向依次向哨兵节点发送"sentinel get-master-addr-by-name"命令, -成功获得主节点信息就不向后面的哨兵节点发送命令。 -同时客户端会订阅哨兵节点的+switch-master频道,一旦主节点发送故障,哨兵服务器对主节点进行自动故障转移,会将从节点升级主节点, -并且更新哨兵服务器中存储的主节点信息,会向+switch-master频道发送消息,客户端得到消息后重新从哨兵节点获取主节点信息,初始化连接池。 +**配置提供者:**前者只负责存储当前最新的主从节点信息,供客户端获取。 -``` +**代理**:客户端所有请求都会经过哨兵节点。 + +首先Redis中的哨兵节点是一个配置提供者,而不是代理。因为客户端只是在首次连接时从哨兵节点获取主节点信息,后续直接与主节点进行连接,发送请求,接收请求结果。 + +具体流程: + +```java String masterName = "mymaster"; Set sentinels = new HashSet<>(); sentinels.add("192.168.92.128:26379"); @@ -61,11 +57,14 @@ Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器, jedis.set("key1", "value1"); pool.close(); ``` +在实际开发中,通过在客户端配置哨兵节点的地址+主节点的名称(哨兵系统可能会监控多个主从节点,名称用于区分)就可以与哨兵节点建立连接,获取到主节点信息,然后与主节点建立连接,并且订阅哨兵节点的频道,以便在主节点变化后,接受到通知。 +上面的代码在底层实现是客户端向依次向哨兵节点发送"sentinel get-master-addr-by-name"命令,成功获得主节点信息就不向后面的哨兵节点发送命令。同时客户端会订阅哨兵节点的+switch-master频道,一旦主节点发送故障,哨兵服务器对主节点进行自动故障转移,会将从节点升级主节点,并且更新哨兵服务器中存储的主节点信息,会向+switch-master频道发送消息,客户端得到消息后重新从哨兵节点获取主节点信息,初始化连接池。 + ### Redis哨兵系统是怎么实现自动故障转移的? ##### 1.认定主节点主观下线 因为每隔2s,哨兵节点会给主节点发送PING命令,如果在一定时间间隔内,都没有收到回复,那么哨兵节点就认为主节点主观下线。 ##### 2.认定主节点客观下线 -哨兵节点认定主节点主观下线后,会向其他哨兵节点发送sentinel is-master-down-by-addr命令,获取其他哨兵节点对该主节点的状态,当认定主节点下线的哨兵数量达到一定数值时,就认定主节点客观下线。 +哨兵节点认定主节点主观下线后,会向其他哨兵节点发送sentinel is-master-down-by-addr命令,获取其他哨兵节点对该主节点的状态,当认定主节点下线的哨兵数量达到一定数值时(这个阀值是Sentinel配置中quorum参数的值,通常我们设置为哨兵总节点数的1/2),就认定主节点客观下线。 ##### 3.进行领导者哨兵选举 认定主节点客观下线后,各个哨兵之间相互通信,选举出一个领导者哨兵,由它来对主节点进行故障转移操作。 @@ -77,4 +76,132 @@ Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器, * 2.然后根据优先级,复制偏移量,runid最小,来选出一个从节点作为主节点。 -向这个从节点发送slaveof no one命令,让其成为主节点,通过slaveof 命令让其他从节点成为它的从节点,将已下线的主节点更新为新的主节点的从节点。 +向这个从节点发送slaveof no one命令,让其成为主节点,通过slaveof 命令让其他从节点成为它的从节点,将已下线的主节点更新为新的主节点的从节点,将其他从节点的复制目标改完新的主节点,将旧的主服务器改为从服务器。 + +### 谈一谈你对RedisCluster的理解? + +当需要存储的数据量特别大,单个Redis实例无法满足需求,所以需要分片,早期很多业务就是在业务中进行分片,通过自定义一些业务规则,将不同的数据存储在不同的Redis实例中。后来就有了官方推出的集群化方案Redis Cluster。 + +### RedisCluster是怎么实现数据分片的? + +首先Redis Cluster设定了有16384个槽位,然后根据启动时集群的主节点数量进行均分,每个主节点得到一定数量的槽位,为了保证每个主节点挂掉之后,服务保持高可用,一般会为每个主节点配置几个从节点,从节点保存了主节点上同步过来的数据,一旦主节点挂掉,会有一个从节点会被选为主节点。客户端在与Redis Cluster建立连接时会获取到各槽位与主节点之间的映射关系,然后缓存到本地。 + +客户端执行命令的流程: + +假设客户端需要发送一个查询请求时,首先会对key使用CRC16算法计算得到一个hash值,然后将hash值与16384(也就是2的14次方)进行取模(下面是网上找的图,应该是CRC16(key)%16384),得到一个槽位slot,然后根据本地缓存的槽位映射关系表,找到这个槽位slot对应的主节点,发送查询命令。主节点在收到命令后会有以下几种情况: + +**1.这个主节点确实负责这个槽位,且不在迁移中。** + +直接查询到这个键值对,返回给客户端。 + +**2.这个主节点不负责这个槽位,或者已经确定转移到其他节点上去了(Moved指令)** + +可能是这个槽位已经迁移了,或者是客户端将指令发送到了错误的节点,或者是客户端缓存的槽位映射关系以前过期。主节点就会给客户端返回Moved指令及正确的节点信息,Moved指令相当于是一个永久重定向指令,用于纠正客户端缓存的错误槽位信息。客户端收到后会更新本地的槽位关系表,然后向正确的节点发送查询指令。 + +**3.这个槽位正在迁移中(ASKING指令)** + +如果这个槽位之前是在这个主节点上,但是目前正在迁移(槽位状态为IMPORTING),那么如果现在主节点上存在这个可以,就成功处理请求。否则就返回ASKING指令+槽位所在的新节点,ASKING指令相当于是一个临时重定向指令,客户端收到之后不会更新本地的槽位关系表,只是将本次请求发送到新节点。 + +![图片](../static/640-20210124192752928) + +#### Redis Cluster的节点扩容和下线 + +##### 扩容 + +例如数据量太大了,原有的节点太少了,希望增加一些Redis实例,分担一些数据量。在Redis Cluster中,需要程序员手动执行命令,将节点添加到集群,并执行命令从其他的主节点上分配一些槽位到这个新节点上。 + +具体执行命令流程如下: + +``` +./redis-trib.rb add-node 127.0.0.1:7006 127.0.0.1:7000 +``` + +可以看到.使用**addnode**命令来添加节点,第一个参数是新节点的地址,第二个参数是任意一个已经存在的节点的IP和端口。 + +新节点现在已经连接上了集群, 成为集群的一份子, 并且可以对客户端的命令请求进行转向了, 但是和其他主节点相比, 新节点还有两点区别: + +- 新节点没有包含任何数据, 因为它没有包含任何哈希槽位. +- 尽管新节点没有包含任何哈希槽位, 但它仍然是一个主节点, 所以在集群需要将某个从节点升级为新的主节点时, 这个新节点不会被选中。 + +接下来, 只要使用 redis-trib 程序, 将集群中的某些哈希槽位移动到新节点里面, 新节点就会成为真正的主节点了。 + +槽位迁移需要执行的命令会比较的多,大家想了解的可以看看这篇文章: + +https://www.cnblogs.com/youngchaolin/archive/2004/01/13/12034660.html + + + +![图片](../static/640-20210124203435521) + +##### 下线 + +在节点上执行 redis-trib.rb del-node{host:port} {donwNodeId} 通知其他的节点,自己下线,如果本节点是主节点,会安排对应的从节点阶梯主节点的位置。 + +#### RedisCluster是怎么做故障转移和发现的? + +##### 1.主观下线 + +当节点 1 向节点 2 例行发送 Ping 消息的时候,如果节点 2 正常工作就会返回 Pong 消息,同时会记录节点 1的相关信息,更新与节点2的最近通讯时间。如果节点 1的定时任务检测到与节点 2 上次通讯的时间超过了 cluster-node-timeout 的时候,就会更新本地节点状态,把节点 2 更新为主观下线。 + +##### 2.客观下线: + +由于 Redis Cluster 的节点不断地与集群内的节点进行通讯,下线信息也会通过 Gossip 消息传遍所有节点。 + +因此集群内的节点会不断收到下线报告,当半数以上持有槽的主节点标记了某个节点是主观下线时,便会认为节点2**客观下线**,执行后面的流程。 + +##### 3.资格检查 + +每个从节点都会检查与主节点断开的时间。如果这个时间超过了 cluster-node-timeout*cluster-slave-validity-factor(从节点有效因子,默认为 10),那么就没有故障转移的资格。也就是说这个从节点和主节点断开的太久了,很久没有同步主节点的数据了,不适合成为新的主节点,因为成为新的主节点以后,其他的从节点回同步它的数据。 + +##### 4.从节点触发选举 + +通过资格的从节点都可以触发选举。但是触发选举是有先后顺序的,这里按照复制偏移量的大小来判断。 + +这个偏移量记录了执行命令的字节数。主服务器每次向从服务器传播 N 个字节时就会将自己的复制偏移量+N,从服务在接收到主服务器传送来的 N 个字节的命令时,就将自己的复制偏移量+N。 + +复制偏移量越大说明从节点延迟越低,也就是该从节点和主节点沟通更加频繁,该从节点上面的数据也会更新一些,因此复制偏移量大的从节点会率先发起选举。 + +##### 5.从节点发起选举 + +首先每个主节点会去更新配置纪元(clusterNode.configEpoch),这个值是不断增加的整数。 + +在节点进行 Ping/Pong 消息交互时也会更新这个值,它们都会将最大的值更新到自己的配置纪元中。 + +这个值记录了每个节点的版本和整个集群的版本。每当发生重要事情的时候,例如:出现新节点,从节点精选。都会增加全局的配置纪元并且赋给相关的主节点,用来记录这个事件。 + +说白了更新这个值目的是,保证所有主节点对这件“大事”保持一致。大家都统一成一个配置纪元(一个整数),表示大家都知道这个“大事”了。 + +更新完配置纪元以后,每个从节点会向集群内发起广播选举的消息。 + +##### 6.主节点为选举投票 + +参与投票的只有主节点,从节点没有投票权。每个主节点在收到从节点请求投票的信息后,如果它还没有为其他从节点投票,那么就会把票投给从节点。(也就是主节点的票只会投给第一个请求它选票的从节点。) + +超过半数的主节点通过某一个节点成为新的主节点时投票完成。 + +如果在 cluster-node-timeout*2 的时间内从节点没有获得足够数量的票数,本次选举作废,进行第二轮选举。 + +这里每个候选的从节点会收到其他主节点投的票。在第2步领先的从节点通常此时会获得更多的票,因为它触发选举的时间更早一些。 + +获得票的机会更大,也是由于它和原主节点延迟少,理论上数据会更加新一点。 + +##### 7.选举完成 + +当满足投票条件的从节点被选出来以后,会触发替换主节点的操作。新的主节点别选出以后,删除原主节点负责的槽数据,把这些槽数据添加到自己节点上。 + +并且广播让其他的节点都知道这件事情,新的主节点诞生了。 + +#### Redis Cluster的主从复制模型 + +Redis集群的架构就是多个主节点,每个主节点负责一部分槽位,每个主节点拥有几个从节点,一旦主节点挂掉,会挑选一个从节点成为新的主节点,负责这部分槽位。如果某个主节点和它的所有从节点都挂掉了,那么这部分槽位就不可用。 + +#### Redis Cluster一致性 + +CAP理论认为C一致性,A可用性,P分区容错性,一般最多只能满足两个,也就是只能满足CA和CP,而Redis Cluster的主从复制的模式是异步复制的模式,也就是主节点执行修改命令后,返回结果给客户端后,有一个异步线程会一直从aof_buf缓冲区里面取命令发送给从节点,所以不是一种强一致性,只满足CAP理论中的CA。 + +参考链接: + +http://www.redis.cn/topics/cluster-tutorial.html + +https://blog.csdn.net/g6u8w7p06dco99fq3/article/details/105336857 + diff --git a/docs/Rule.md b/docs/Rule.md new file mode 100644 index 0000000..0d5e10a --- /dev/null +++ b/docs/Rule.md @@ -0,0 +1,78 @@ +##### 1.使用BigDecimal替代Float和Double + +主要Float和Double是使用类似于科学计数法那样"有效数字+指数"来表示的,所以在二进制存储时,是会丢失精度,没法做到精准的。所以浮点数之间不能使用==等值判断,浮点数包装类型之间不能使用equals + +```java +float a = 1.0f - 0.9f; +float b = 0.9f - 0.8f; //通过debug调试发现这两次减运算减下来,a和b的值存在一些差异,不是一模一样的,丢失了精度 +Boolean result = a == b;//结果是false + +Float x = 1.0F - 0.9F; +Float y = 0.9F - 0.8F; +Boolean result = x.equals(y);//结果是false +``` + +解决方案: + +1. 任何货币金额,均以最小货币单位且整型类型来进行存储。例如在数据库里面存10000,代表是100.00元。 + +2. 使用Bigdecimal来存这些值,并且进行加减。 + +```java +BigDecimal a1 = new BigDecimal("1.0"); +BigDecimal b1 = new BigDecimal("0.9"); +BigDecimal c1 = new BigDecimal("0.8"); + +BigDecimal a2 = a1.subtract(b1); +BigDecimal b2 = b1.subtract(c1); + +System.out.println(a2.equals(b2));//打印结果是true +``` + + + +2.布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。 + +一般假设我们定义一个Boolean变量为isHot,按照Bean规范生成的get方法应该是isIsHot,但是我们使用IDEA来自动生成get方法时,生成的是isHot()方法, + +> JavaBeans规范中对这些均有相应的规定,基本数据类型的属性,其getter和setter方法是getXXX()和setXXX,但是对于基本数据中布尔类型的数据,又有一套规定,其getter和setter方法是isXXX()和setXXX。但是包装类型都是以get开头。 + +```java +private boolean isHot; +public boolean isHot() { + return isHot; +} +public void setHot(boolean hot) { + isHot = hot; +} +``` + +这样子在实例对象转json时,序列化框架看到isHot()方法会去找hot实例变量,这样就会找不到变量值。 + +在与前端交互时,同意需要注意,避免Boolean类型参数是is前缀开头的,因为Spring MVC在接受参数时,看到前端json传过来的值是isHot,而set方法中没有setIsHot()方法,只有setHot()方法 + +https://www.cnblogs.com/goloving/p/13086151.html + +https://blog.csdn.net/qq_31145141/article/details/71597608 + +##### 正确的加锁方式 + +通过这种方式来加锁,保证抛出异常时可以正常解锁,同时lock()方法不能在try代码块中调用,防止线程还没有加上锁时抛出异常,然后进行解锁,抛出IllegalMonitorStateException异常(对未加锁对象调用释放锁tryRelease()方法就会抛这个异常)。 + +```java +Lock lock = new XxxLock(); // ... +lock.lock(); +try { + doSomething(); + doOthers(); +} finally { + lock.unlock(); +} +``` + +##### COUNT(*)与count(列名) + +1.【强制】不要使用count(列名)或count(常量)来替代count(*),count(*)是SQL92定义的标 准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。 + 说明:count(*)会统计值为 NULL 的行,而 count(列名)会将NULL值排除掉,不会统计此列为 NULL 值的行。 + +2.count(distinct col) 计算该列除 NULL 之外的不重复行数 \ No newline at end of file diff --git a/docs/Spring.md b/docs/Spring.md index 83c71e0..fb814cf 100644 --- a/docs/Spring.md +++ b/docs/Spring.md @@ -1,7 +1,9 @@ (PS:扫描[首页里面的二维码](README.md)进群,分享我自己在看的技术资料给大家,希望和大家一起学习进步!) -#### [1.HTTPS建立连接的过程是怎么样的?](#HTTPS建立连接的过程是怎么样的?) -### Spring AOP +#### [1.SpringAOP是怎么实现的?](#SpringAOP是怎么实现的?) +### SpringAOP是怎么实现的? + +Spring Aop是一种可以减少大量重复代码的一种编程技术,可以设置一个切面,比如说是某个包下面的所有方法,这些方法在执行的时候就会调用我们写的拦截方法,我们可以做一些类似于日志打印等一些操作。 实现AOP有三种方式:静态代理,使用JDK的Proxy类实现动态代理,使用CGLIB实现动态代理。 @@ -21,27 +23,24 @@ public class UserDao implements IUserDao { //代理类 public class UserDaoProxy implements IUserDao { private IUserDao target; - public UserDaoProxy(IUserDao iuserDao) { this.target = iuserDao; - } - + } public void save() { - System.out.println("开启事物..."); + System.out.println("开启事务..."); target.save(); - System.out.println("关闭事物..."); + System.out.println("关闭事务..."); } - } ``` ### JDK动态代理 -通过调用Proxy.newProxyInstance()方法可以为目标类创建一个代理类,然后调用代理类的方法时会调用InvocationHandler的invoke()方法,然后我们可以在invoke方法里面做一些日志记录之类的额外操作,然后再调用真正的实现方法,也就是目标类的方法。 +通过调用Proxy.newProxyInstance()方法可以为目标类创建一个代理类,然后调用代理类的方法时会调用InvocationHandler的invoke()方法,然后我们可以在invoke()方法里面做一些日志记录之类的额外操作,然后再调用真正的实现方法,也就是目标类的方法。 目标类必须有对应的接口类,我们拦截的方法必须是接口中定义的方法。 -``` +```java public class Test implements TestInterface { public void test(Integer a) { System.out.printf("111111"); @@ -50,18 +49,16 @@ public class Test implements TestInterface { public interface TestInterface { void test(Integer a); } - +//创建一个类,继承InvocationHandler,重写invoke()方法,在这个方法里面做一些日志打印的操作后,然后通过反射的API调用method.invoke(target, args);本来的方法。 public static class CustomInvocationHandler implements InvocationHandler { - Object target; - public CustomInvocationHandler(Object target) { this.target = target; } - @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before"); + //调用本来的方法 Object result = method.invoke(target, args); System.out.println("after"); return null; @@ -69,15 +66,16 @@ public static class CustomInvocationHandler implements InvocationHandler { } ``` -``` -Test test = new Test(); + +```java +Test test = new Test(); TestInterface proxyInstance = (TestInterface) Proxy.newProxyInstance(test.getClass().getClassLoader(), test.getClass().getInterfaces(), new CustomInvocationHandler(test)); - proxyInstance.test(11); +proxyInstance.test(11); ``` -实现原理:就是在调用Proxy.newProxyInstance()时会根据类加载器和目标类Class对象动态创建一个代理类出来,动态代理类的所有方法的实现都是向下面这样,方法内部都是调用invocationhandler的invoke方法 +实现原理:就是在调用Proxy.newProxyInstance()时会根据类加载器和目标类Class对象动态创建一个代理类出来,动态代理类的所有方法的实现都是向下面这样,方法内部都是调用invocationHandler的invoke()方法 -``` +```java public final void test(){ throws { @@ -160,9 +158,178 @@ public static class Test implements TestInterface { ### 区别 -Java动态代理只能够对接口进行代理(动态创建了一个类,实现了接口中的方法),不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类; -Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效 +目标类必须有对应的接口类,然后JDK动态代理动态创建了一个类,实现了接口中的方法,不能对没有接口类的普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类; +Java动态代理只能对有接口类的类进行代理,并且使用Java原生的反射API进行操作,在生成类上比较高效,但是执行会效率低一些。CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效。 参考链接: -https://blog.csdn.net/gyshun/article/details/81000997 \ No newline at end of file +https://blog.csdn.net/gyshun/article/details/81000997 + +### Spring IOC是什么? + +IOC就是invention of control,就是控制反转,将对象获取外界依赖资源的方式反转了。假设一个对象A依赖于对象B,在没有IOC以前,就是在对象A需要使用对象B时,去用代码显式地创建一个对象B。有了IOC以后,可以由Spring IOC容器来负责对象B的创建和销毁,创建后放在容器中,在对象A需要的时候再来取。 +DI(Dependency Injection,依赖注入)其实就是IOC的另外一种说法,就是IOC是通过依赖注入技术实现的。 +《跟我学spring3系列》https://www.iteye.com/blog/jinnianshilongnian-1413851 +https://www.cnblogs.com/xdp-gacl/p/4249939.html + +### Spring IOC是怎么解决循环依赖问题的? + +Spring IOC只能解决属性注入之间的循环依赖问题,如果是构造器之间的循环依赖,只会抛出BeanCurrentlyInCreationException异常。 + +Spring使用了3个Map来保存Bean,俗称为三级依赖: + +singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例,可以使用的。 + +earlySingletonObjects 二级缓存,bean刚刚构造完成,但是还没有进行属性填充。 + +singletonFactories 三级缓存,用于保存正在创建中的bean,以便于后面扩展有机会创建代理对象,此时的bean是没有完成属性填充的。 + +假设A类和B类相互依赖,A中有一个B类的属性,B中有一个A类的属性。那么在初始化A的Bean时,首先会依次去一级依赖,去二级依赖,三级依赖中去找,都没有就调用创建方法创建实例A,将A添加到三级依赖中,然后对A的属性进行依赖注入,填充属性时,发现B的Bean在各级依赖中都没有,就创建B的bean添加到三级依赖,然后对B的属性进行填充,填充B的属性A时,会从三级依赖中取出A,填充完放到二级依赖,然后对B进行初始化,初始化完成添加到一级依赖。B初始化完成后,将B从一级依赖中,填充到实例A,A可以进入到二级依赖,完全初始化完成后,A进入到一级依赖,供用户代码使用。 + +![](../static/d7687db8ecbd43a79d041badf07bbaf4~tplv-k3u1fbpfcp-watermark-20210316161015142.image) + + + +https://juejin.cn/post/6911692836714840077 + +https://segmentfault.com/a/1190000015221968 + +https://blog.csdn.net/qq_35165000/article/details/108185093?spm=1001.2014.3001.5501 + +### Bean的生命周期是怎么样的? + +Bean的生命周期主要分为以下四个阶段: + +1.**Bean的实例化阶段**-主要是在createBeanInstance()方法中,调用类的构造器方法来创建一个Bean实例。用户可以自定义一个类,继承InstantiationAwareBeanPostProcessorAdapter,重写它的两个方法,对Bean的实例化前后做一些额外的操作,例如打印日志。 + +```java +public class MyInstantiationAwareBeanPostProcessorAdapter extends InstantiationAwareBeanPostProcessorAdapter { + @Override + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + if (beanName.equals("car")) { + System.out.println(beanName + "在实例化之前"); + } + return super.postProcessBeforeInstantiation(beanClass, beanName); + } + @Override + public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { + if (beanName.equals("car")) { + System.out.println(beanName + "在实例化之后"); + } + return super.postProcessAfterInstantiation(bean, beanName); + } +} +``` + +2.**属性赋值阶段**-主要是在populateBean()方法中,对Bean的各项属性进行赋值。 + +3.**Bean的初始化阶段**-主要调用用户自定义的初始化方法init-Method() + +用户可以自定义一个类,继承BeanPostProcessor,重写它的两个方法,对Bean的初始化前后做一些额外的操作,例如打印日志。 + +```java +public class NdBeanPostProcessor implements BeanPostProcessor { + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + System.out.println("NdBeanPostProcessor 在" + beanName + "对象初始化之前调用......"); + if (beanName.equals("car")) { + return new CglibInterceptor().getIntance(bean.getClass()); + } + return bean; + } + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + System.out.println("NdBeanPostProcessor 在" + beanName + "对象初始化之后调用......"); + return bean; + } +} +``` + +4.**Bean销毁阶段**,用户可以自定义destroyMethod()方法,在Bean被销毁时被调用。 + +### BeanFactory和FactoryBean有什么区别? + +**BeanFactory**是一个接口,定义了IOC容器的最基本的规范,并提供了IOC容器应遵守的的最基本的方法。在Spring代码中,BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,都是附加了某种功能的实现。 + +```java +package org.springframework.beans.factory; +import org.springframework.beans.BeansException; +public interface BeanFactory { + String FACTORY_BEAN_PREFIX = "&"; + Object getBean(String name) throws BeansException; + T getBean(String name, Class requiredType) throws BeansException; + T getBean(Class requiredType) throws BeansException; + Object getBean(String name, Object... args) throws BeansException; + boolean containsBean(String name); + boolean isSingleton(String name) throws NoSuchBeanDefinitionException; + boolean isPrototype(String name) throws NoSuchBeanDefinitionException; + boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException; + Class getType(String name) throws NoSuchBeanDefinitionException; + String[] getAliases(String name); +} +``` + +**FactoryBean**是一个接口,有一个创建bean对象的方法getObject(),当一些bean对象不能由ioc容器简单得调用类的构造器方法来创建实例对象时使用,可以将Bean类实现FactoryBean接口,实现getObject()方法,供ioc容器调用来创建bean对象。 + +```java +public interface FactoryBean { + @Nullable + T getObject() throws Exception; + + @Nullable + Class getObjectType(); + + default boolean isSingleton() { + return true; + } +} +``` + +https://www.cnblogs.com/aspirant/p/9082858.html + +### Springboot启动过程 + +##### 构造SpringApplication实例 + +1.首先会调用SpringApplication的静态方法run(),在这个方法里面会调用构造器方法创建出一个SpringApplication实例,在构造器中会确定当前web应用类型,是reactive web类型,还是servlet web类型,还是none类型。以及设置监听器等等,完成一些初始化操作。(监听器就是来监听SpringApplication启动过程的,在开始启动,创建上下文,启动失败等生命周期事件时都会调用监听器相关的方法) + +##### 执行run()方法 + +2.然后去执行实例的run()方法,首先会创建一个StopWatch计时器器,来统计run()方法的启动耗时,在日志里面会显示启动时间,那个时间就是在这里统计的。然后处理环境参数,就是`java -jar ***.jar`启动命令中带的那些jvm参数。 + +![img](../static/up-70a9e95e6c1208288334341bdb54bd59c17.png) + +##### 创建applicationContext + +3.会创建出一个ApplicationContext,一般servlet的应用的context类型是AnnotationConfigServletWebServerApplicationContext。(可以认为beanFactory就是ioc容器,但是我们一般不直接使用beanFactory获取bean,而是通过applicationContext来获取,ioc容器beanFactory是应用上下文applicationContext的一个属性,applicationContext也实现了BeanFactory接口,可以认为applicationContext是一个高级容器,applicationContext支持国际化,默认是启动时加载所有bean,而不是用到时才进行懒加载,以及支持事件机制。) + +##### 执行prepareContext()方法 + +4.然后会调用prepareContext()方法来为应用上下文做一些准备工作,会将运行时的参数封装成bean,注册到beanFactory中去,以及使用load方法加载启动类。 + +##### 执行refreshContext()方法 + +5.在这里会启动容器,也就是会为beanFactory做很多配置,注册BeanPostProcessors,设置类加载器等等。在这一步也会解析启动类中@SpringBootApplication这个组合注解。 + +##### afterRefresh()方法 + +6.这个方法里面会把容器里面所有ApplicationRunner自定义子类和CommandLineRunner自定义子类的Bean全部取出来,执行它们的run()方法。(就是有时候如果需要在应用启动后执行一些我们自定义的初始化操作,可以通过自定义一个类,继承ApplicationRunner类来实现。) + +之后会调用listeners.started()方法,通知所有Listener,application已经启动完成了,以及调用listeners.running()方法通知所有Listener,application已经运行了。 + +```java +//系统启动完可以做一些业务操作 +@Component +//如果有多个runner需要指定一些顺序 +@Order(1) +public class SimosApplicationRunner implements ApplicationRunner { +@Autowired +SystemInitService systemInitService; +@Override +public void run(ApplicationArguments args) throws Exception { + systemInitService.systemInit(); +} +} +``` + +https://my.oschina.net/funcy/blog/4873261 + +https://www.imooc.com/article/264722 \ No newline at end of file diff --git a/docs/SystemDesign.md b/docs/SystemDesign.md index 1705643..ff1b842 100644 --- a/docs/SystemDesign.md +++ b/docs/SystemDesign.md @@ -1,4 +1,278 @@ (PS:扫描[首页里面的二维码](README.md)进群,分享我自己在看的技术资料给大家,希望和大家一起学习进步!) -#### [1.HTTPS建立连接的过程是怎么样的?](#HTTPS建立连接的过程是怎么样的?) -### 限流 \ No newline at end of file +#### [1.怎么设计一个大文件上传的功能?](#怎么设计一个大文件上传的功能?) + +#### [2.谈一谈分布式ID生成方案的了解?](#谈一谈分布式ID生成方案的了解?) + +### 怎么设计一个大文件上传的功能? + +首先如果是大文件上传,考虑到网络不稳定容易造成上传失败,或者需要做断点续传功能,是不是对整个文件直接进行上传的。 +#### 1.计算hash + +首先是让前端对大文件计算hash值,主要是用于后端去判断这个文件在服务器上是否已经存在,其次是这个hash值可以作为上传文件的一个标识。 +如果是直接对整个文件计算hash值,文件过大时,可能会比较慢,通常是对文件进行抽样计算。根据hash值判断文件是否在后端已经存在,已存在就不进行上传了。 + +##### 影分身Hash +就是把文件切分成2M的分片,然后对首文件+中间每个文件的首中尾取2个字节+尾文件,进行合并,计算合并文件的MD5值,作为文件的hash值。 + +如果这个hash值在后端存在: +说明这个文件可能存在也可能不存在。准确性不是100%。 + +如果这个hash值在后端不存在: +说明这个文件一定不存在。准确性是100%。 + +通过这种方式判断的思路跟布隆过滤器比较像。 + +![img](../static/v2-e3634165fbe1c09983d564c34e2bbfb9_b.jpg) + +#### 2.分片上传 + +上传时不能一窝蜂地把所有分片文件发给后端,是把每个分片包装成一个网络请求,使用异步线程对从队列里面每次取一个分片进行上传。 +每个分片文件都有一个唯一标识,就是hash值+分片编号。这样如果上传中断了,下次再进行上传时,后端可以知道哪些分片已经上传,把未上传的分片唯一标识返回给前端,前端只需要上传上次未上传的分片,这也就是断点续传。 + +https://zhuanlan.zhihu.com/p/104826733 + +#### 3.合并请求 +有两种方案,一种是后端记录所有分片的上传状态,当所有分片全部上传完毕后,自动合并文件。另外一种就是前端发现传完所有文件后,调用接口通知后端去合并。 + + +### 谈一谈分布式ID生成方案的了解? +首先ID生成方案的技术考虑点: +唯一性:不能重复。 +趋势递增:保证id作为主键时,插入数据库时的顺序性,避免随机插入。 +单调递增:满足某些特殊业务的要求,保证后一秒请求生成的id比前一秒的大。 +信息安全性:不要像UUID一样泄露mac地址,也不要像数据库主键ID自增完全连续,泄露每日id生成数据量。 + +#### 1.UUID模式 + +太长,占用存储空间过大 +id是字符串类型,查询成本高于数字类型 +如果是包含mac地址的UUID会泄密 + +#### 2.单机数据库主键自增模式 + +id生成的量受限于单机MySQL数据库的性能 +强依赖于数据库,主从切换时容易导致重复发号 +容易泄露id生成量 + +#### 3.多机数据库主键自增模式 + +主要是将每个数据库的步长设置为一样,但是起始值不一样,以此错开生成的id,不便于扩展。 + +##### 4.Leaf号段模式模式 + +就是数据库存储一个maxId代表已经发放的id最大值,每次将maxId更新为max+step,取step数量的id发放。 +优点: +1.便于扩展,发号性能取决于step,可以动态调整。(Leaf做了Step动态调整策略,一个号段使用时间<15分钟,就让号段拥有的id量step翻倍(直到最大阀值100万),一个号段使用时间>30分钟,step减半(直到最低阀值,初始号段id量)。) +2.即便主节点的宕机,短时间Leaf也能继续提供服务,其次是主从切换时影响较小。 +缺点: +1.当号段里面的id用完时,会去数据库取新的号段,此时如果来了获取id的请求会需要进行等待。(Leaf做了双Buffer优化,使用了双Buffer各存储一个号段,当一个号段使用量达到10%后,就触发另一个号段去数据库取新号段进行更新,以便于当一个号段使用完时,可以直接切换到未使用的号段。) +2.id是连续的,容易泄密。(可以自定义抛弃策略,取号段时的时候抛弃一些id,或者定时抛弃掉一些id。) + +#### 5.Leaf-snowflake模式 + +就是沿用了snowflake原本的位数分配算法,1标志位+41位毫秒时间戳+10位机器位+12位序列号。使用zookeeper作为注册中心,id生成服务启动时,去指路径下获取所有节点的列表,判断当前ip+port是否有对应的workid存在,有就使用,没有就往插入一个新的永久顺序节点,序号则为workId(并且会将workid缓存到本地磁盘上)。运行期间每过3s,都会上报最新的时间戳到zookeeper。 +**时钟回滚的处理:** +**启动时:** +zookeeper里面会存上次生成id的时间戳,如果上次存储的时间戳>当前系统时间戳,那么就抛出异常,启动失败。 +**运行时:** +如果获取id时,发现上次生成id时的时间戳>当前系统时间戳,那么说明运行时发生了时钟回滚,如果回滚的时间差<5ms,就调用wait()方法等待10ms,然后再获取id,时间差>5ms,就抛出异常。 + +优点: + +**时钟回滚优化** + +1.对时钟回滚做了特殊处理。 + +**zookeeper弱依赖** + +2.为了减轻了zookeeper的弱依赖,实现在zookeeper挂了的情况下,id生成服务也能启动,每次启动后,在本地也缓存workid配置,一旦启动时,发现zookeeper连接不上,就通过从本地缓存配置中读取workid。(但是这样也有问题,本地缓存配置只存了workid,没有存上次生成id的最大时间戳,所以一旦启动前发生了时钟回滚,或者是修改了系统时间,这样从本地缓存配置中读取workid生成的id就可能是重复的。) + +**时间差优化** + +3.做了时间差优化,就是默认的时间戳是从1970年开始的,leaf是自己选定了2010年的一个时间点,以此来计算时间戳,这样可以在时间位数固定的情况下,增长服务最大运行时间。在毫秒时间戳为41位的情况下,时间差最大是69年,如果以1970年为起点,那么最大时间就是1970+69年,如果以2010年为起点就是2010+69年。 + +**序列号优化** + +4.为了防止生成的id的序列号部分都是从0开始,导致插入数据库时,有数据倾斜的问题,所以每次用新的毫秒时间戳时,序列号不是从0开始,而是计算一个0到100之间的随机数作为起点。 + +缺点: + +1.注册中心只支持Zookeeper + +2.潜在的时钟回拨问题 + +3.时间差过大时,生成id为负数。 + +### 6.百度的uid-generator模式 + +#### 默认模式 + +每次启动时向数据库插入一条数据,这行数据的主键是自增的,主键id就是workId, + +因为默认是snowflake算法是**1标志位+41位时间戳+10位机器号+12位序列号**, + +因为百度的是每次启动都获取新的机器号,所以它修改了这些位数配比,是 + +**1标志位+28位的秒级时间差+22位的机器号+13位的序列号**,所以总共支出2的22次方次启动,也就是400万次启动。最大服务支持时间是2*28次方,也就是8.7年。(优化点是修改位数分配,让服务时间更长,我们的位数分配是**30位秒级时间差+16位机器号+7位序列号**,最长服务时间支持34年,6.5w次机器启动,每个机器每秒128个并发,总共位数没有用到64位,只用到53位,这样生成的id转化为10进制更小。) + +解决时间回拨问题: + +* 启动时时间回拨 + +因为是每次都用新的机器号,所以当前机器号都是之前没有的,所以即便时间戳回拨也不影响。 + +* 运行时时间回拨 + +会使用lastSecond来记录上次生成id的时间戳,如果当前时间戳比lastSecond还小,就抛出异常。(优化点就是**回拨时间差较小时进行等待**,较长时再抛出异常。) + +缺点: + +1.默认的最大服务年限太短,只有8年。 + +2.回拨时间差较小时也是抛出异常,没有额外的判断逻辑。 + +3.没有像Leaf一样做序列号优化,可能生成的id序列号部分都是从0开始的多一些,可能会存在数据倾斜的问题。 + +#### 缓存模式 + +主要继承自默认模式,只是用一个环形数组来存储生成好的id,每次去环形数组中去,默认大小是2的13次方,8192。这种模式使用的时间取得不是实时的系统时间,而且使用启动时的时间,每次生成一组id时,对之前保存的时间+1。 + +**阀值检测预填充**:取id时,发现可用id数小于阀值50%时,就对后面已经使用的id进行再填充。 + +**定期填充**:每5分钟定期会去检查环形数组中id使用情况,然后生成一组最大序列号个数的id(默认是8192个),然后进行填充,多的直接丢弃掉。 + +**缺点**: + +1.id只有在定期填充时,会丢弃掉一些id,其他情况下,id是完全连续的。假如每次使用量比较大,大部分时候都是5分钟内能用掉50%的话,那么就就不会触发定期填充,也没有id丢弃,导致id会一直连续,容易泄露数据信息,所以最好自定义丢弃逻辑。 + +2.其次是id跟生成id时系统的时间戳无关了,可能无法满足一些特殊业务的需求。 + +### 自己的方案 + +我们自己的主要是根据uid-generator来做的,因为只是启动的时候依赖数据库,不需要引进新的依赖。做的优化主要是修改了位数分配,使得支持最大服务年限更长,30位时间差+16位机器号+7位序列号。做了时钟回拨优化,回拨差值较小时进行等待。以及做了序列号丢弃的优化。 + +### 谈一谈你对分布式事务的理解? + +#### 2PC方案(2阶段提交制) + +![图片](../static/640-8127488.) + +这种方案就是引入了一个协调者,主要分为prepare和commit两个阶段,在第一阶段其实就是协调者给各个业务系统发送命令,业务系统收到后,就会开始执行这个子事务的具体操作,然后给协调者反馈,子事务执行成功还是失败,发送后,业务系统就会同步阻塞等待协调者的第二阶段的指令。 + +协调者第一阶段发送指令会进入超时等待状态,等待各个业务系统返回子事务的执行结果。 + +**执行成功,提交** + +如果所有业务系统都执行成功了,并且协调者收到了反馈,那么就认为整个事务执行成功了,就会通知各个业务系统进行提交。 + +**执行失败,回滚** +如果超过时间还没有收到所有业务系统返回的结果,或者是有业务系统返回了执行失败的结果,那么协调者就认为执行失败了,通知各个业务系统进行回滚,如果此时网络出现阻塞,业务系统收不到通知,那么协调者就会一直重试发送回滚的指令。 + +**缺点** + +1.单点问题:就是一旦协调者挂掉,所有子系统都会进入阻塞等待状态。 + +2.数据不一致的问题:如果在第二阶段协调者给子系统发送commit指令时,发生了局部网络异常,就会导致接收到commit的指令的子系统提交事务,而没有接收到commit指令的子系统没有提交事务,导致数据不一致。 + +3.同步阻塞:由于子系统在执行阶段都是同步阻塞的,自身没有超时的机制,一旦与协调者之间的网络断开,只能一直阻塞等待,等待协调者的指令。 + +#### 3PC三段提交制 + +这种方案就是分为三个阶段,canCommit,preCommit,doCommit三个阶段。 + +第一阶段是询问阶段 + +协调者会给参与者发送canCommit指令,询问能否正常执行,如果满足执行的条件的话,参与者就会返回ACK。 + +第二阶段是预执行阶段 + +协调者会给参与者发送preCommit指令,让参与者执行事务,但是执行完成不提交,参与者执行成功后会给协调者发送ACK。 + +第三阶段就是提交阶段 + +协调者如果收到所有参与者给他返回执行成功的ACK,那么他就会给所有协调者发送doCommit指令,让参与者提交。如果在第二阶段有一个参与者执行失败,给协调返回执行失败的结果,那么在第三阶段,协调者就会给参与者发送Abort指令,让参与者回滚。 + +![图片](../static/640-8127813.png) + +**与2PC的区别:** + +1.3pc是一个非阻塞的协议,为参与者引入了超时机制,在第一阶段或者第二阶段,参与者等待协调者的指令超时了,会进行回滚,第三阶段等待协调者的指令超时了,会进行自动提交。而2pc协议,如果协调者一直没有给参与者发指令,导致超时,参与者会一直进行阻塞等待。 + +2.2pc协议里面,第二阶段协调者挂了,选举出新的协调者是不知道参与者执行的事务是提交事务还是回滚事务。3pc里面到了第三阶段那么一定是执行提交事务。 + +**3pc的问题:** + +第三阶段如果发送的是abort回滚指令,假设有些参与者由于网络原因没有收到指令,超时后会进行自行提交事务,那么也会导致数据不一致的问题。 + +https://honeypps.com/architect/introduction-of-distributed-transaction/ + +https://www.infoq.cn/article/2018/08/rocketmq-4.3-release + +#### TCC + +TCC分为Try预留阶段,Confirm确认阶段,Cancel撤销阶段三个阶段。 + +比如某个事务需要A,B,C三个业务系统各自执行一些操作,那么事务管理器会发送Try指令,会让A,B,C三个业务系统各自去申请执行操作所需的一些资源,冻结库存之类的。A,B,C预留资源成功了就会通知事务管理器Try阶段执行成功了。那么事务管理器就会发送Confirm指令给三个业务系统,告诉他们进入到Confirm阶段,让A,B,C业务系统各自执行自己真正的事务操作。如果三个业务系统都执行成功,那么事务管理器就认为执行成功,如果有一个失败那么事务管理器就认为执行失败了,会通知每个业务执行Cancel操作,进行回滚。 + +(万一某个服务的 Cancel 或者 Confirm 逻辑执行一直失败怎么办呢? + + Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功。) + +TCC框架主要有ByteTCC,TCC-transaction,Himly。 + +缺点: + +1.侵入性太强,每个业务系统还需要写Cancel对应的数据回滚相关的逻辑代码。 + + + + + +![img](../static/v2-90179fa933c0a389ffa6ac04e244a58f_b.jpg) + +https://www.cnblogs.com/jajian/p/10014145.html + +##### 本地消息表法 + +就是上游服务先执行操作,将操作记录到数据库中的本地消息表,并且此时这条操作记录的status设置为0,也就是未通知成功。然后将操作记录封装成Kafka消息发送到消息队列,下游系统接受到,进行消费,然后消费成功后调用上游服务的接口,通知他消费成功了,上游系统将本地消息表中这条记录的status设置为1,代表通知成功。 + +并且上游系统会定时扫描本地消息表,将status为0的操作记录,封装成Kafka消息,发送到消息队列。 + +并且下游系统是通过消息中操作记录的主键id来防止不重复消费,保证幂等性的。就是消费消息时,发送操作记录的id已经在数据库中存在了,就代表之前已经处理过了,不处理这条消息了。 + +##### 可靠消息最终一致性方案 + +RocketMQ在4.3以后,增加了对分布式事务的支持,就是将事务的执行状态保存在RocketMQ中,由RocketMQ去负责将commit状态的消息推送给下游系统。 + +![img](../static/66b6ae1dec5b96084c3a6d29174a20e3.png) + +1.上游系统发送prepare消息到RocketMQ。 + +2.prepare消息发送到RocketMQ成功后,上游系统开始执行本地事务。 + +3.如果上游系统本地事务执行成功,会发送commit消息到RocketMQ,RocketMQ会将这个消息提交,推送给消费者(也就是下游系统)。如果上游系统本地事务执行失败,会发送rollback消息到RocketMQ,RocketMQ会将这个消息撤销,不推送给消费者。 + +4.如果一个prepare消息一直没有接受到上游系统的commit或者rollback指令,这样就判定prepare消息超时了,RocketMQ会去查询上游系统的这个事务的执行状态,是成功了,还是失败,做下一步的处理。 + +**底层实现原理** + +RocketMQ使用了Half topic队列来保存所有prepare消息,使用Operation Topic队列来保存commit消息和rollback消息。这样通过Operation Topic就知道哪些消息commit了,可以推送给消费者,哪些消息rollback了,不需要推送给消费者。以及那些在Half topic中有,在Operation Topic中没有的消息,就是事务超时的消息。 + +https://www.infoq.cn/article/2018/08/rocketmq-4.3-release + +##### 最大努力通知方案 + +业务系统 A 执行本地事务完成后,发送个消息到 MQ,有一个个专门消费 MQ 的服务,来消费MQ的消息,消费完会在数据库中记录下来(或者放入到内存队列),之后就一直调用系统 B 的接口,要是系统 B 执行成功就提交,执行失败或者调用超时就一直重试,直到业务系统B执行成功。 + +### 如何设计秒杀系统? + +1.前端页面 +提前把静态资源部署到CDN,减少静态资源访问的压力,其次是可以为静态资源的header设置成强缓存cache-control,2分钟后才过期,这样当第一次请求完静态资源后,再次刷新时,就会使用浏览器里面的缓存的静态资源,而不是再发请求。 +2.nginx层面 +使用limit_req_zone模块针对用户id为key,进行限流,放在同一个用户恶意发起多个请求,限制每个用户每5分钟只能请求100次接口。 +3.业务系统设置限流熔断 +当业务系统收到的请求达到一定限制后,停止接受请求,可以使用hystrix进行限流。如果是秒杀商品,当Redis里面存的商品库存减完时,就对所有用户返回统一的状态码,告诉前端商品被抢光了,不再进行后面的业务逻辑。 +4.使用消息队列削峰 +就是如果秒杀请求对应的业务逻辑处理时间如果过长,例如减库存,生成订单等,业务系统可以先将用户的秒杀请求封装成Kafka消息,发送到消息队列,由其他业务系统消费Kafka消息,完成生成订单等耗时操作。 +https://www.zhihu.com/question/54895548/answer/1352510403 \ No newline at end of file diff --git a/docs/ZooKeeper.md b/docs/ZooKeeper.md index 48d41ff..b916bc3 100644 --- a/docs/ZooKeeper.md +++ b/docs/ZooKeeper.md @@ -37,14 +37,22 @@ C得票超过半数,C成为leader,之后加入的D,E也只会投给C。 每个节点都会投给自己,然后选票发给其他节点 ##### 2.处理选票信息 从其他节点B收到投票信息后,进行处理 + 2.1 如果本节点的逻辑时钟小于接受这条投票的逻辑时钟, + 说明本节点之前错过了上一轮的投票,将当前存储的选票信息清空, + 2.2 如果本节点的逻辑时钟大于接受的这条投票的逻辑时钟,那么忽略掉这条投票信息。 + 2.3 本节点的逻辑时钟等于接受的这条投票的逻辑时钟,那么进行处理,与本节点当前投票的结果进行比较 先比较ZXID(数据ID,越大代表数据越新),ZXID越大的应该当leader,ZXID相同比较SID,SID越大的当leader。 + 2.4如果比较的结果跟当前节点的投票结果不一致,那么需要更改选票,将更改后的选票结果发送给其他节点。 -2.5将其他节点B的投票结果记录下来 + +2.5 将其他节点B的投票结果记录下来 + ##### 3.统计选票 + 对当前收到的所有投票信息进行统计,看是否有节点获得半数以上的选票,有的话就将它设置为leader,然后终止投票,否则继续投票,继续步骤2。 ##### 4.发现阶段 @@ -67,9 +75,9 @@ https://blog.csdn.net/hotchange/article/details/81192122 首先客户端连接zookeeper集群中的任何一个节点,可以是leader节点,也可以是follower节点,一旦连接,节点会给客户端分配会话ID,并向客户端发送确认,如果客户端收到确认,那么连接成功,客户端会有规律地给zookeeper发送心跳包,确保连接没有断开。 -* 客户端向zookeeper节点发送读请求,节点会直接从数据库中找到这个节点的数据然后返回。 +* 客户端向zookeeper从节点发送读请求,节点会直接从数据库中找到这个节点的数据然后返回。 -* 客户端向zookeeper节点发送写请求,节点会将znode路径和数据转发到leader节点,leader会将写请求转换为proposal提案,并且分配一个事务ID zxid,将这个proposal放到每个节点的队列(主节点会给每个从节点分配一个专用队列)中去,然后会根据先进先出的策略,将消息发送给从节点,从节点接收到后会将事务写入到磁盘中去,然后返回ACK响应给主节点,当主节点接收到半数以上的从节点的ACK响应后,主节点会认为这个事务提交成功,完成这个事务提交,同时给所有从节点发送commit消息,从节点接收到消息后,会将这条事务提交。 +* 客户端向zookeeper从节点发送写请求,节点会将znode路径和数据转发到leader节点,leader会将写请求转换为proposal提案,并且分配一个事务ID zxid,将这个proposal放到每个节点的队列(主节点会给每个从节点分配一个专用队列)中去,然后会根据先进先出的策略,将消息发送给从节点,从节点接收到后会将事务写入到磁盘中去,然后返回ACK响应给主节点,当主节点接收到半数以上的从节点的ACK响应后,主节点会认为这个事务提交成功,完成这个事务提交,同时给所有从节点发送commit消息,从节点接收到消息后,会将这条事务提交。 由此看来zookeeper没法保证客户端读取的都是最新的数据, diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 0000000..bd331c5 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,35 @@ +- [首页](README.md) +* Java + - [基础](docs/JavaBasic.md) + * 容器 + - [ArrayList和LinkedList](docs/ArrayList.md) + - [HashMap和ConcurrentHashMap](docs/HashMap.md) + - [多线程](docs/JavaMultiThread.md) + - [锁相关](docs/Lock.md) +* Redis + - [基础](docs/RedisBasic.md) + - [数据结构](docs/RedisDataStruct.md) + - [持久化(AOF和RDB)](docs/RedisStore.md) + - [高可用(主从切换和哨兵机制)](docs/RedisUserful.md) +* MySQL + - [基础](docs/MySQLNote.md) + - [慢查询优化实践](docs/MySQLWork.md) +* JVM + - [基础](docs/JavaJVM.md) +- [Kafka](docs/Kafka.md) +- [ZooKeeper](docs/ZooKeeper.md) +- [HTTP](docs/HTTP.md) +- [Spring](docs/Spring.md) +- [Nginx](docs/Nginx.md) +- [系统设计](docs/SystemDesign.md) +* 算法 + - [《剑指Offer》解题思考](docs/CodingInterviews.md) + - [《LeetCode热门100题》解题思考(上)](docs/LeetCode.md) + - [《LeetCode热门100题》解题思考(下)](docs/LeetCode1.md) +- [大厂面试公众号文章系列](docs/BATInterview.md) +* 读书笔记 + - [《Redis设计与实现》读书笔记 上](docs/RedisBook1.md) + - [《Redis设计与实现》读书笔记 下](docs/RedisBook2.md) + - [《MySQL必知必会》读书笔记](docs/MySQLBook1.md) + - [《深入理解Java虚拟机-第三版》读书笔记](docs/JVMBook.md) +- [好书推荐](docs/bookRecommend.md) diff --git a/docs/algorithm.md b/docs/algorithm.md index 9612799..eff4a06 100644 --- a/docs/algorithm.md +++ b/docs/algorithm.md @@ -1,18 +1,26 @@ 高频算法面试题集合 #### [1.常见的排序算法](#常见的排序算法) + #### [2.二分查找](#二分查找) + #### [3.查找链表倒数第K个节点](#查找链表倒数第K个节点) + #### [4.链表反转](#链表反转) + #### [5.查找第K大的数](#查找第K大的数) + #### [6.有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M,返回频数最高的100个词](#有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M,返回频数最高的100个词) + #### [7.9个硬币中有一个劣币,用天平秤,最坏几次?](#9个硬币中有一个劣币,用天平秤,最坏几次?) -#### [8.编辑距离](#编辑距离) -#### [9.打印杨辉三角](#打印杨辉三角) +#### [8.编辑距离](#编辑距离) +#### [9.打印杨辉三角](#打印杨辉三角) +#### [10.LRU算法](#LRU算法) +#### [11.【面试算法题】阿拉伯数字转化为中文读法](#[面试算法题]阿拉伯数字转化为中文读法) ### 常见的排序算法 @@ -21,7 +29,7 @@ ## 冒泡排序 -就是进行n次遍历,每次比较两个元素,判断大小,进行交换,将大的元素换到右边,最终可以遍历可以将最大的元素交换到末尾,最终让整个序列有序。 +对从[0,length-1]范围内的元素进行比较和交换,将大的元素交换到后面去,一次遍历后就可以将最大的元素交换至末尾,然后下次遍历范围将减1,在[0,length-2]范围内进行,一直到最后。 #### 优化 @@ -30,11 +38,10 @@ ```java int[] sorted(int[] array) { if (array == null || array.length ==0 || array.length ==1) {return array;} - - for(int i = array.length-1;i>=1;i--) {//i代表遍历的最大范围 + for(int i = array.length;i>0;i--) {//i代表遍历的最大范围 int sortedFlag = 0; - for(int j = 1;j<=i;j++) { - if(array[j]array[j]) {//进行比较,将大的元素交换到后面去 int temp = array[j]; array[j] = array[j-1]; array[j-1]=temp; @@ -42,9 +49,9 @@ int[] sorted(int[] array) { } } // 该趟排序中没有发生,表示已经有序 - if (0 == sortedFlag) { - break; - } + if (0 == sortedFlag) { + break; + } } return array; } @@ -52,18 +59,21 @@ int[] sorted(int[] array) { ## 插入排序 -就是每次从未排好序的序列中,每次取出一个元素,插入到已排好序的序列中去。 +就是每次从未排好序的序列中,每次取出一个元素,插入到已排好序的序列中去。比较好的写法就是数组前面一段是排好序的,这一段往前面进行比较然后互换。 ```java int[] sorted2(int[] array) { if(array == null || array.length==0 || array.length==1) {return array;} for(int i=1;i0;j--) { - if(array[j]0;j--) { + if(array[j]=end) { - return array; - } - int middle = start + (end-start)/2; - sorted44(array,start, middle); - sorted44(array,middle + 1, end); - merge(array, start,end); - return array; +void sort(int[] array) { + if (array==null||array.length==0) { return; } + //临时数组主要用于归并过程中的临时存放排序数组 + int[] tempArray = new int[array.length]; + mergeSort(array,0,array.length-1,tempArray); +} +void mergeSort(int[] array, int start, int end,int[] tempArray) { + if (start>=end) { return; } + //进行分组,一直分到只有两个元素,在小组内进行归并 + int middle = start + (end - start)/2; + mergeSort(array,start,middle,tempArray); + mergeSort(array,middle+1,end,tempArray); + + //进行合并 + int i = start; + int j = middle+1; + int currentIndex = start; + //进行合并,每次取最小值 + while (i <= middle && j<= end) { + tempArray[currentIndex++] = array[i] < array[j] ? array[i++] : array[j++]; } - -void merge(int[] array, int start,int end) { - int length = end-start+1; - int[] temp = new int[length]; - for (int k = 0; k < length; k++) { - temp[k] = array[start+k]; - } - int left = 0; - int tempMiddle = (0 + length-1)/2; - int right = tempMiddle+1; - int tempEnd =length-1; - int i = start; - while (left <= tempMiddle && right <= tempEnd) { - array[i++] = temp[left] < temp[right] ? temp[left++]:temp[right++]; - } - - while (left <= tempMiddle) { - array[i++] = temp[left++]; - } - - while (right <= tempEnd) { - array[i++] = temp[right++]; - } - } + //对剩余元素进行归并 + while (i <= middle) { + tempArray[currentIndex++] = array[i++]; + } + //对剩余元素进行归并 + while (j <= end) { + tempArray[currentIndex++] = array[j++]; + } + //将排序好的数组拷贝回原数组 + for (int k = start; k <= end; k++) { + array[k] = tempArray[k]; + } +} ``` ### 快速排序 先取第一个元素作为中间值,小于中间值的元素放一组,大于中间值的元素放一组,然后继续对每一组进行递归排序,最终让数组有序。 -``` +```java int[] quickSorted(int [] array, int start,int end) { //如果start和end相等就直接结束循环 - if(start>=end) {return array;} + if(start>=end) {return array;} //取第一个元素作为基准值 - int base = array[start]; - int i = start;//左边从第一个元素开始,如果取得基准值正好是最小值,然后遍历从第二个开始,会导致第二个位置的值与基准值交换,而第二个位置的值是>基准值的 - int j = end;//右边从最后一个元素开始 - //只要i base && i基准值的 + int j = end;//右边从最后一个元素开始 + //只要i base && i=0;i--){ - //从第一个非叶子结点从下至上,从右至左调整结构 - adjustHeap(arr,i,arr.length); +堆排序步骤: + +1.先建一个大顶堆,建堆方式主要通过从最后一个非叶子节点开始调整位置(调整时主要是通过将节点A与它的最大子节点进行交换,一直交换下去,知道都大于左右子节点) + +2.每次将堆顶的元素与最后一个元素交换,相当于取出最大元素,放到最后面,然后对堆进行调整,重新成为大顶堆,一直到最后。 + +![img](../static/1024555-20161217182750011-675658660.png) + +![img](../static/1024555-20161217182857323-2092264199.png) + +因为大顶堆都是完全二叉树, + +某节点下标为i, + +左子节点下标=2i+1, + +右节点=2i+2, + +因为最后一个数组下标是length-1,而它的父节点至少会拥有左节点,不一定会拥有左节点,所以假设父节点的下标为i,满足公式length-1=2*i +1,i就等于length/2 - 1。(假设最后一个元素是右节点,这个公式也是成立的,因为length会是奇数,相当于length/2的时候舍去了一个1) + +```java + //堆排,每次调整的时间复杂度是log(N),然后需要调整N次,所以总时间复杂度是N*log(N) + int[] heapSort(int[] array) { + if (array==null||array.length==0) { + return array; + } + //1.对所有非叶子节点进行调整,建立大顶堆 + for (int i = array.length/2 - 1; i >= 0 ; i--) { + adjustHeap(array,i,array.length); + } + //2.大顶堆建立完毕后,堆顶就是最大值,交换到数组末尾 + for (int i = array.length-1; i > 0 ; i--) { + //每次调整完毕后堆顶都是最大值 + swap(array,0,i); + //对0到i-1范围内的元素看成一个堆,进行大顶堆调整 + adjustHeap(array,0,i); + } + return array; } - //2.调整堆结构+交换堆顶元素与末尾元素 - for(int j=arr.length-1;j>0;j--){ - swap(arr,0,j);//将堆顶元素与末尾元素进行交换 - adjustHeap(arr,0,j);//重新对堆进行调整 + //调整大顶堆,需要调整节点i, + //使节点i与它的子节点都满足大顶堆的性质(就是父节点>左节点和右节点) + void adjustHeap(int[] array,int i,int length) { + //循环结束的条数是节点i存在左节点 + while (2*i+1 array[left] ? i : left; + if (right array[max]) {//右节点存在,并且还比最大值大 + max = right; + } + if (max == i) {//当前已经是最大值 + break; + } else {//左节点或者右节点是最大值,那么就将当前节点与最大的节点交换 + swap(array,i,max); + i = max; + } + } + } + //交换元素 + void swap(int[] array,int a,int b){ + int temp = array[a]; + array[a] = array[b]; + array[b] = temp; } -} -// 调整的过程其实是将从节点i的左右节点中找出最大值,然后作为当前i的节点,并且会对交换后的节点继续调整 -public static void adjustHeap(int []arr,int i,int length){ - int temp = arr[i];//先取出当前元素i - for(int k=i*2+1;ktemp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换) - arr[i] = arr[k]; - i = k; - }else{ break; } - } - arr[i] = temp;//将temp值放到最终的位置 -} -//交换元素 -public static void swap(int []arr,int a ,int b){ - int temp=arr[a]; - arr[a] = arr[b]; - arr[b] = temp; -} ``` - - ### 桶排序 核心思想就是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排序完成之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。一般步骤是: @@ -228,79 +267,129 @@ public static void swap(int []arr,int a ,int b){ 理想情况下,如果订单数据是均匀分布的话。但是,订单数据不一定是均匀分布的。划分之后可能还会存在比较大的文件,那就继续划分。比如订单金额在 1~1000 元之间的比较多,那就将这个区间继续划分为 10 个小区间,1~100、101~200 等等。如果划分之后还是很大,那么继续划分,直到所有的文件都能读入内存。 - - ### 二分查找 -``` +查找数组中是否存在某元素的算法 + +```java int findByHalf(int[] array, int target) { - if (array == null || array.length==0) { - return -1;//代表没有合适的元素 - } + if (array == null || array.length==0) { + return -1;//代表没有合适的元素 + } + int left = 0; + int right = array.length-1; + while(left<=right) {//需要注意是小于等于right,否则查找范围会是[left,right),会漏掉对最后一个元素的比较 + int middle = left + (right - left)/2; + if(array[middle]target) { + right = middle-1; + } else { + return middle; + } + } + return -1;//代表没有合适的元素 +} +``` + +二分查找左边界算法 + +左边界的定义是第一个>=目标值的下标,如果所有元素都<目标值,那么返回的会是最后一个元素的下标+1 +```java +//1,2,3,4,5,6,7,8,9 +int find_left_bound(int[] array,double target) { + if(array == null|| array.length==0) {return -1;} int left = 0; int right = array.length-1; - while(left<=right) {//需要注意是小于等于right,否则查找范围会是[left,right),会漏掉对最后一个元素的比较 - int middle = left + (right - left)/2; - if(array[middle]target) { + while(left<=right) { + int middle = left+(right-left)/2; + if(array[middle]==target) { right = middle-1; - } else { - return middle; - } + } else if (array[middle] > target) { + right = middle-1; + } else if (array[middle] < target) { + left = middle + 1; + } } - return -1;//代表没有合适的元素 + if (left >= nums.length)//如果所有元素都比target小,left会等于nums.length,此时是越界的 + {return -1;} + return left; } ``` -### 查找链表倒数第K个节点 +寻找左边界的二分查找算法 +```java +int findLeft(int[] array,int target) { + if (array==null|| array.length==0) { + return -1; + } + int left = 0; + int right = array.length-1; + while (left<=right) { + int mid = (left+right)/2; + if (array[mid] == target) { + if (mid==0 || array[mid-1]K,说明第K大的数在左边,继续从左边的子序列中找,index queue = new PriorityQueue(); + PriorityQueue queue = new PriorityQueue(); for (int i = 0; i < array.length; i++) { queue.add(array[i]); if (queue.size()>K) { @@ -391,23 +480,87 @@ public static Integer findKByPickSort(int[] input, int k) { return arrayList.get(k-1); } ``` + +### 01背包问题 +就是有很多个物品,每个物品有一个价值属性,一个重量属性,假设背包总容量为capcity,选择哪些物品,可以使得最终装的物品价值最大。 +有两种节点: +##### 分治法 +就是假设当前有i个物品,我们对于第i个物品进行选择,只有两种可能 +要么选择物品i,那么背包的容量就变为capcity-weight[i],然后继续对剩下的i-1的这些物品进行选择: + +总价值f(i,capacity) = value[i] +f(i-1,capcity-weight[i]) + +要么不选择物品i,容量还是capcity,继续对剩下的i-1的物品进行选择: + +总价值f(i,capacity) = f(i-1,capcity) + +```java + int testKnapsack1(int[] value,int[] weight, int i, int capacity) { + int result = 0; + if (i < 0 || capacity == 0){ + // 已经选择到最后了 + result = 0; + } else if(weight[i] > capacity) { + // 装不下该珠宝 + result = testKnapsack1(value,weight,i-1, capacity); + } else if (weight[i] <= capacity) {// 可以装下 + //选择物品i + int choose = testKnapsack1(value,weight,i-1, capacity-weight[i]) + value[i]; + //不选择物品i + int notChoose = testKnapsack1(value,weight,i-1, capacity); + result = choose > notChoose ? choose : notChoose; + } + return result; + } +``` +##### 动态规划解法 +```java + //动态规划解法 + public int testKnapsack2(int[] value,int[] weight, int capacity) { + int[][] dp = new int[weight.length][capacity+1]; + //对前i个物品进行选择 + for (int i = 0; i < weight.length; i++) { + //每次背包总重量为j + for (int j = 1; j <= capacity; j++) { + if (i==0) {//当前只有一个物品 + dp[i][j] = weight[i] > capacity ? 0 : value[i]; + } else if (j < weight[i]) {//背包装不下,那就不装这个物品 + dp[i][j] = dp[i-1][j]; + } else {//当前背包装得下 + //不装此物品 + int notChoose = dp[i-1][j]; + //装此物品 + int choose = dp[i-1][j-weight[i]] + value[i]; + dp[i][j] = notChoose > choose ? notChoose : choose; + } + } + } + return dp[weight.length-1][capacity]; + } +``` + + + ### 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M,返回频数最高的100个词 -首先这个问题的难点在于内存太小,1M/16 byte =64,也就是在极端情况下,单词都是16字节时,内存中可能最多存储64个。 + +首先这个问题的难点在于内存太小,1M/16 byte =2的16次方,也就是在极端情况下,单词都是16字节时,内存中可能最多存储2的16次方个。 执行流程如下: 先hash分片->每个小文件堆排取前100->对每个小文件的前100做归并排序。 1.由于内存限制是1M,所以如果每个小文件小于1M以下才能全部加载到内存中,所以可以设置模为5000,遍历每个单词,对单词取hash值%5000,得到index,写入到index名的小文件下,理论上这样的是可以达到每个小文件都是小于1M的,如果大于1M,需要对文件继续进行hash运算,切分成小文件。 -2.遍历每个小文件,使用堆排的方法对小文件进行排序,记录每个单词的出现次数,写入一个新的记录次数的小文件。 +2.遍历每个小文件,每个小文件理论上都是小于1M的,可以直接加载到内存中,使用堆排的方法对小文件进行排序,记录每个单词的单词名和出现次数,写入一个新的记录次数的小文件。 -3.然后进行归并排序取一个记录次数的文件,建了一个包含100个单词的小顶堆,遍历一次所有记录次数文件,每次遍历时取小文件的前100,与堆顶进行比较,如果比堆顶大就替换堆顶,并且对小顶堆进行调整,将最小的元素调整到堆顶。(由于内存中是存不了100个单词的,所以我们内存中只存堆顶元素,小顶堆是存在磁盘中,每次需要调整堆的时候再去磁盘中读取一部分元素然后进行调整,得到新的堆顶元素。) +3.然后进行归并排序取一个记录次数的文件,建了一个包含100个单词的小顶堆,遍历一次所有记录次数文件,每次遍历时取小文件的前100,与堆顶进行比较,如果比堆顶大就替换堆顶,并且对小顶堆进行调整,将最小的元素调整到堆顶。 对于多次hash后的文件还是大于内存限制应该怎么办呢? 这个时候就只能先对文件按照顺序进行切分,切分成小文件,然后加载每个小文件,通过map来记录每个单词及出现次数,然后根据map按照key的顺序生成一个记录了单词和出现次数的map文件。对这些map文件进行合并,每次取两个map合并后生成一个map,最终得到一个map文件,使用堆排遍历map中的key,得到前100个词,作为这部分hash文件的前100。 ### 全排序问题 + 就是给定一个字符串,返回所有可能的排序结果。例如aba的排序结果有aba,aab,baa。可以认为每一个字符串的排序结果是等于每个非重复字母出现在第一个index+后面子序列每个的组合。 + ```java //用于收集每种序列 ArrayList list = new ArrayList(); @@ -427,7 +580,7 @@ public void PermutationHelper(char[] charArray,int start) { char temp = charArray[i]; charArray[i] = charArray[start]; charArray[start] = temp; - + PermutationHelper(charArray,start+1); //交换 charArray[start] = charArray[i]; @@ -438,7 +591,9 @@ public void PermutationHelper(char[] charArray,int start) { ``` ### 字符串的最长不重复子串的长度 + 就是使用滑动窗口来实现,一开始窗口左右节点都等于0,窗口右节点的元素在Set不存在,就添加,然后右节点右移动,然后计算当前窗口大小,如果大于之前存的最大窗口值就替换。如果在Set中存在,那么就Set移除左节点中的值,左节点+1。 + ```java int findNotDuplicateStringMaxLength(String string) { if (string == null || string.length() == 0) { @@ -470,15 +625,15 @@ int findNotDuplicateStringMaxLength(String string) { **第一次称**,123 VS 456, * 如果平衡,那么说明剩下的789里面有劣币, -然后**第二次称**7VS8,平衡,9就是劣币,否则7和8中有一个是劣币,那么**第三次称**拿真币1和7来称,如果平衡则8是假币,不相等则7是假币。 + 然后**第二次称**7VS8,平衡,9就是劣币,否则7和8中有一个是劣币,那么**第三次称**拿真币1和7来称,如果平衡则8是假币,不相等则7是假币。 * 如果不平衡,说明现在的123,456个币中有一个是劣质币 -假设 -重的那一组是1 2 3 -轻的那一组 4 5 6 -由于**假币的所在的组要么会一直重(如果假币比真币重),要么会一直轻(如果假币比真币轻),所以如果一会在重组一会在轻组出现的肯定是真币**。 -所以我们先去掉3和6,并且将2和5进行调换,也就是**第二次称**对15和42进行称。 -1 5 -4 2 + 假设 + 重的那一组是1 2 3 + 轻的那一组 4 5 6 + 由于**假币的所在的组要么会一直重(如果假币比真币重),要么会一直轻(如果假币比真币轻),所以如果一会在重组一会在轻组出现的肯定是真币**。 + 所以我们先去掉3和6,并且将2和5进行调换,也就是**第二次称**对15和42进行称。 + 1 5 + 4 2 * 假设天平平了,说明15和42都是真币,3和5才假币,**第三次称**拿一个其他的币与3称,相同则5是假币,不相等则3是假币。 * 假设天平没有平,并且1 5是重的那一方,由于不平衡,所以排除3和6,由于在之前4 5 6是轻的一方,现在1 5又是重的一方,所以排除5,5不会是假币,同理也排除2,所以只有1和4有可能是假币,**第三次称**拿一个其他的币与1称,平衡则4是假币,不相等则1是假币。 * 假设天平没有平,并且1 5是轻的那一方,由于不平衡,所以排除3和6,同理根据上面的原理,可以排除一会在重组一会在轻组的1,4,所以2和5有可能是假币。同理**第三次称**可以得出2和5谁是假币 @@ -488,6 +643,7 @@ int findNotDuplicateStringMaxLength(String string) { 分成四组,**第一次称**1234 VS 5678 1.平衡说明假币在9 10 11 12中,**第二次称**9 10 VS 真币1 真币2, + * 平衡代表11,12中存在假币,**第三次称**11 VS 真币1,平衡代表12是假币,否则11是假币。 * 同理不平衡,说明9 10存在假币,对9和10实时上面的称法。 @@ -500,6 +656,7 @@ int findNotDuplicateStringMaxLength(String string) { 第一组 1 2 5(两个重组元素+一个轻组元素) 第二组 3 6 X(一个轻组元素+一个重组元素+一个真币) **第二次称**1 2 5 VS 3 6 X + * 假设平衡,说明4,7,8中存在假币,称一下7和8,平衡,那么4是假币,不平衡,由于一会在重组的,一会在轻组的肯定是真币,而7,8之前都在轻组,所以7和8对称时,轻的是假币。 * 假设1 2 5 重,由于一会在重组的,一会在轻组的肯定是真币,根据这个原则,5,3肯定是真币,所以只有1,2,6存在可能。同理,根据上面的方法**第三次称**找出假币 * 话说1 2 5轻,由于一会在重组的,一会在轻组的肯定是真币,根据这个原则,可以排除1,2,它们是真币。只有3和5存在可能,**第三次称**找出假币。 @@ -507,6 +664,7 @@ int findNotDuplicateStringMaxLength(String string) { ### 编辑距离 递归解法 + ```java // 假设是要将a变成b的样子 static int findMinValue(char[] a, int aLength, char[] b, int bLength) { @@ -526,7 +684,9 @@ static int findMinValue(char[] a, int aLength, char[] b, int bLength) { } } ``` + 动态规划解法 + ```java static int findMinValueInNewWay(char[] a, int aLength, char[] b, int bLength) { int [][] dp = new int[aLength][bLength]; @@ -534,7 +694,7 @@ static int findMinValueInNewWay(char[] a, int aLength, char[] b, int bLength) { for (int i = 0; i < aLength; i++) { dp[i][0] = i+1; } //同理,这里是假设a字符串为空 for (int j = 0; j < aLength; j++) { dp[0][j] = j+1;} - //计算a,b每个子串的之间的编辑距离 + //计算a,b每个子串的之间的编辑距离 for (int i = 1; i < aLength; i++) { for (int j = 1; j < bLength; j++) { if (a[i] == b[j]) { @@ -556,6 +716,7 @@ static int findMinValueInNewWay(char[] a, int aLength, char[] b, int bLength) { ``` ### 打印杨辉三角 + 思路其实是使用一个数组来存储上一层节点的值,然后根据每个节点index去数组中可以取值,父左节点的下标等于index-1,父右节点的下标等于index。 ```java @@ -586,7 +747,7 @@ static void printYanghui(int rows) { 根据一个数学规律来计算,就是每一层一开始是数都是1,假设前一个数是a,后面的数b,满足b = a*(上一层的层数-a的下标)/(a的下标+1) -``` +```java static void test2(int rows) { for (int i = 0; i < rows; i++) { int index = 1; @@ -597,4 +758,331 @@ static void test2(int rows) { System.out.println(" "); } } +``` + +## LRU算法 + +LRU其实就是Last Recent Used,就是最近使用淘汰策略,所以当空间满了时,就根据最近使用时间来删除。一般是使用一个双向链表来实现,同时为了快速访问节点,会使用一个HashMap来存储键值映射关系。(需要注意的是,为了在内存满时删除最后一个节点时,可以以O(1)时间复杂度从HashMap中删掉这个键值对,每个节点除了存储value以外,还需要存储key)。 + +#### 添加新元素的过程: + +首先我们增加两个方法,remove()方法用于删除一个节点,addNewNodeToHead()代表添加一个节点到头部。 + +1.判断节点是否已经存在于链表中,是的话,找出节点, + +1.1更新value, + +1.2调用remove()方法删除节点, + +1.3调用addNewNodeToHead()将节点添加到链表头部。 + +2.节点不存在于链表中,那么判断链表长度是否超出限制, + +2.1是的话remove(lastNode.key) + +2.2创建一个新节点,调用addNewNodeToHead()将节点添加到链表头部。 + +remove()方法的细节,主要是更新node的前后节点的next或pre指针,以及更新后需要判断删除的节点是否是headNode或者lastNode,是的话同时需要更新headNode或者lastNode。 + +addNewNodeToHead()方法细节,主要是要先判断head是否为null,是的话说明链表为空,需要将headNode和lastNode都设置为node,不为null就执行添加操作,将headNode.pre设置为node,node的next设置为headNode,headNode=node; + +```java +//双向链表 +public static class ListNode { + String key; + Integer value; + ListNode pre = null; + ListNode next = null; + ListNode(String key, Integer value) { + this.key = key; + this.value = value; + } +} +ListNode headNode; +ListNode lastNode; +int limit=4; +HashMap hashMap = new HashMap(); +public void put(String key, Integer val) { + ListNode existNode = hashMap.get(key); + if (existNode!=null) {//有老的节点,只是更新值,先从链表移除,然后从头部添加 + existNode.value=val; + remove(key); + addNewNodeToHead(existNode); + } else { + //达到限制,先删除尾节点 + if (hashMap.size() == limit) { remove(lastNode.key); } + ListNode newNode = new ListNode(key,val); + addNewNodeToHead(newNode); + } +} +public ListNode get(String key) { + + ListNode node = hashMap.get(key); + if(node == null) { + return null; + } + remove(node.key); + addNewNodeToHead(node); + return node; +} +public void remove(String key) { + ListNode deleteNode = hashMap.get(key); + hashMap.remove(key); + ListNode preNode = deleteNode.pre; + ListNode nextNode = deleteNode.next; + //删除操作需要更新pre节点的next指针和next节点的pre指针,以及更新head和last + if (preNode!=null) { preNode.next = nextNode; } + if (nextNode!=null) { nextNode.pre = preNode; } + if (headNode == deleteNode) { headNode = nextNode; } + if (lastNode == deleteNode) { lastNode = preNode; } +} +private void addNewNodeToHead(ListNode node) { + hashMap.put(node.key,node); + if (headNode==null||lastNode==null) { + headNode = node; + lastNode = node; + return; + } + headNode.pre = node; + node.next = headNode; + headNode = node; +} +``` + +##### 使用LinkedHashMap实现的算法 + +使用LinkedHashMap实现LRU算法, + +* LinkedHashMap默认的accessOrder为false,也就是会按照插入顺序排序, + 所以在插入新的键值对时,总是添加在队列尾部, + 如果是访问已存在的键值对,或者是put操作的键值对已存在,那么需要将键值对先移除再添加。 +* 如果是将accessOrder设置为true,get已有键值对时就不需要删除key了,会自动调整顺序,put方法需要在添加或者更新键值对后调用LinkedHashMap#get()访问key,调整顺序。 + +```java +//accessOrder为false,按照插入顺序排序的写法 +public static class LRUCache { + int capacity; + Map map; + public LRUCache(int capacity) { + this.capacity = capacity; + map = new LinkedHashMap<>(); + } + public int get(int key) { + if (!map.containsKey(key)) { + return -1; + } + //先删除旧的位置,再放入新位置 + Integer value = map.remove(key); + map.put(key, value); + return value; + } + + public void put(int key, int value) { + if (map.containsKey(key)) { + map.remove(key); + map.put(key, value); + return; + } + //超出capacity,删除最久没用的,利用迭代器,删除第一个 + if (map.size() >= capacity) { + map.remove(map.keySet().iterator().next()); + } + map.put(key, value); + } +} +``` + +accessOrder为true,按照访问顺序排序的实现方法 + +```java +public static class LRUCache2 { + int capacity; + LinkedHashMap linkedHashMap; + LRUCache2(int capacity) { + this.capacity = capacity; + //如果要修改accessOrder只能使用这种构造器方法来创建LinkedHashMap + linkedHashMap = new LinkedHashMap(16,0.75f,true); + } + public int get(int key) { + Integer value = linkedHashMap.get(key); + return value == null ? -1 : value; + } + public void put(int key, int val) { + linkedHashMap.put(key, val); + if (linkedHashMap.size() > capacity) { + linkedHashMap.remove(linkedHashMap.keySet().iterator().next()); + } + } + //通过调用get()方法访问key来调整顺序 + linkedHashMap.get(key); +} +``` + +### 【面试算法题】阿拉伯数字转化为中文读法 + +例如我们要将10100转换为中文,总体流程就是先拿10100/1个亿,发现结果为0,说明不会包含亿这个数量级,然后10100/1万,得到结果result为1,余数remain为100,说明包含万这个数量级,我们的结果肯定是等于 "result的中文表示"+单位"万"+"余数的中文表示",所以就对问题进行了分解,f(n) = f(n/数量级)+数量级单位+f(n%数量级) + +```java + static String[] nameArray1 = {"","一","二","三","四","五","六","七","八","九"}; + static String[] nameArray2 = {"","十","百","千","万","亿"}; + static int[] intArray = {1,10,100,1000,10000,100000000}; + + public static String numToChinese(int num) { + for (int i = intArray.length-1; i >= 0; i--) { + int part1 = num/intArray[i]; + int part2 = num%intArray[i]; + if (i==0) {//到个位了 + return nameArray1[part1]; + } + if (part1>0) { + //整除部分,例如10100,整除部分就是十 + String left = numToChinese(part1); + //整除部分的单位,例如10100,整除部分的单位就是万 + String unitString = nameArray2[i]; + //余数部分,例如10100,余数部分就是一百 + String right = numToChinese(part2); + return left + unitString + right; + } + } + return ""; + } +``` +## 122.买卖股票的最佳时机 II +给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 + +设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 + +注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + +示例 1:  +```java +输入: [7,1,5,3,6,4] +输出: 7 +解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 +  随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 +``` + +##### 解题思路 + +按照状态来分,每天只有持有股票和不持有股票两种状态,我们使用 +一个二维数组dp[i][isHoldStock]来保存当天利润最大值, +i代日期,isHoldStock代表当天是否持有股票, 1代表当天持有了股票 + +如果第i天持有股票,那么要么是之前买的,要么是今天买的 +dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]) +如果第i天未持有股票,那么要么是前一天也没有持有股票,要么是今天把股票卖了 +dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]) + +初始状态第一天已持有股票 dp[0][1] = -prices[0] +初始状态第一天未持有股票 dp[0][0] = 0 + +```java + public int maxProfit(int[] prices) { + if(prices==null||prices.length<=1) {return 0;} + int [][] dp = new int[prices.length][2]; + dp[0][0] = 0; + dp[0][1] = 0 - prices[0]; + for (int i = 1; i < prices.length; i++) { + dp[i][1] = dp[i-1][1] > dp[i-1][0]-prices[i] ? dp[i-1][1] : dp[i-1][0]-prices[i]; + dp[i][0] = dp[i-1][0] > dp[i-1][1]+prices[i] ? dp[i-1][0] : dp[i-1][1]+prices[i]; + } + return dp[prices.length-1][0]; + } +``` + + + +### 421. 数组中两个数的最大异或值 + +https://leetcode-cn.com/problems/maximum-xor-of-two-numbers-in-an-array/ + +给定一个非空数组,数组中元素为 a0, a1, a2, … , an-1,其中 0 ≤ ai < 231 。 + +找到 ai 和aj 最大的异或 (XOR) 运算结果,其中0 ≤ i, j < n 。 + +你能在O(n)的时间解决这个问题吗? + +示例: + +输入: [3, 10, 5, 25, 2, 8] + +输出: 28 + +解释: 最大的结果是 5 ^ 25 = 28 + + + +```java +public static class TreeNode { + public int val; + public TreeNode left = null; + public TreeNode right = null; + public TreeNode(int val) { + this.val = val; + } + } + +Integer findMaximumXOR(int[] array) { + if (array==null||array.length==0) { + return null; + } + TreeNode root = new TreeNode(-1); + //构建前缀树 + for (int i = 0; i < array.length; i++) { + insert(root,array[i]); + } + int max =0; + + for (int i = 0; i < array.length; i++) { + int result = findMaxForTheValue(root,array[i]); + if (result>max){ + max= result; + } + } + return max; +} + +void insert(TreeNode root, int insertValue) { + //最大值是是2的31次方 + int bitValue = 1<<30; + TreeNode currentNode = root; + while (bitValue!=0) { + int result = insertValue & bitValue; + if (result==0) {//array[i]这一位是0,往左创建节点 + if (currentNode.left==null) { + TreeNode node = new TreeNode(-1); + currentNode.left = node; + } + currentNode = currentNode.left; + } else {//array[i]这一位是1,往右边创建节点 + if (currentNode.right==null) { + TreeNode node = new TreeNode(-1); + currentNode.right = node; + } + currentNode = currentNode.right; + } + bitValue= bitValue>>1; + } + currentNode.val = insertValue; +} + +int findMaxForTheValue(TreeNode root, int value) { + TreeNode currentNode = root; + int bitValue = 1<<30; + while (bitValue!=0) { + int result = value & bitValue; + if (result==0) {//array[i]这一位是0,往右边找节点 + + currentNode = currentNode.right != null ? + currentNode.right : currentNode.left; + + } else {//array[i]这一位是1,往左边找节点 + currentNode = currentNode.left != null ? + currentNode.left : currentNode.right; + } + bitValue= bitValue>>1; + } + int result = value^currentNode.val; + return result; +} ``` \ No newline at end of file diff --git a/docs/idgenerator.md b/docs/idgenerator.md index f5b3082..1025af4 100644 --- a/docs/idgenerator.md +++ b/docs/idgenerator.md @@ -308,4 +308,31 @@ https://tech.meituan.com/2017/04/21/mt-leaf.html | **二进制** | 45-47位 | 48-50位 | 51-54位 | 55-57位 | 58-60位 | 61-64位 | | ---------- | ------- | ------- | ------- | ------- | ------- | ------- | -| **十进制** | 14位 | 15位 | 16位 | 17位 | 18位 | 19位 | \ No newline at end of file +| **十进制** | 14位 | 15位 | 16位 | 17位 | 18位 | 19位 | + + + +调研了Leaf和uid-generator,由于Leaf有额外的zookeeper依赖, + +所以选用了uid-generator,做的改进如下: + +1.原本是一旦时钟回拨就抛出异常,修改为时钟回拨小于1s时,就不抛出抛出异常,进行等待1s。 + +2.加了序列号抛弃策略。按照原有序列号位数分配,是13位,就是每秒可以生成的id数是8192个id,如果并发量比较小,由于每秒获取的id的序列号部分都是从0开始的,或导致后缀0的数据会比较多,容易造成数据倾斜的问题,而且也容易泄露数据信息。可以增加抛弃策略,就是取每一秒的id时,计算一个最大值为序列号的10%随机数,从这个随机数开始取。 + +3.修改了位数分配,原有的时间位是28位的秒级时间差,最长服务年限只支持8.7年,我们把时间差位数分配了30位,最长可以支持34年。原本机器位是22位,支持启动400万次,我们其实不需要那么多次启动,调整成20位,支持100万次启动。1+30+20+13 + +4.增加机器位用完时的取余操作,便于复用。 + + + +leaf的缺点: + +1.号段模式的信息安全性问题,不考虑机器线下和重启丢掉的这些id,id是完全连续的,容易被竞争对手猜到信息安全性。 + +2.小问题修复,就是Leaf为了减少对Zookeeper的依赖,在本地也存了一个上次使用的workid的缓存文件,保证在Zookeeper挂掉的情况下,id生成服务能正常启动,读取本地缓存的workid,然后启动。但是本地缓存文件里面只存储了workid,没有存储上次生成的id的时间戳,假如启动前服务器的时间被修改了,那么启动时就没法对时间进行校验,就会导致生成的id重复。 + +3.缺乏对最大支持年限的检查,时间戳部分溢出会影响符号位,导致生成的id是负数。 + +4.注册中心只支持Zookeeper,issue里面很多人提出,对于他们的项目来说,由于需要引入Zookeeper依赖,增加部署和维护Zookeeper成本。所以我fork了这个项目,增加了使用MySQL作为注册中心,以及本地配置作为注册中心的模块。 + diff --git a/docs/linux.md b/docs/linux.md new file mode 100644 index 0000000..c56e09c --- /dev/null +++ b/docs/linux.md @@ -0,0 +1,40 @@ +### kill pid和kill -9 pid的区别是什么? + +kill pid + +代表通知应用程序自行关闭。系统会发生一个SIGTERM命令给进程对应的应用程序,让应用程序释放自己的资源后,自行关闭。大部分应用程序可能接受后会释放资源,自行停止,也有一些程序不会理会。 + +kill -9 pid + +代表强制关闭进程。系统会发送一个SIGKILL命令给进程。 + +一般建议先执行kill pid,然后过一两秒后等程序做一些释放资源的操作,然后再使用kill -9命令强制删除。 + +https://www.cnblogs.com/aspirant/p/11543456.html + +### 僵尸进程和孤儿进程是什么? +僵尸进程就是子进程调用exit退出或者是运行时发生致命错误,结束运行时,一般会把进程的退出状态通知给操作系统,操作系统发送SIGCHLD信号告诉父进程“子进程退出了”,父进程一般会使用wait系统调用以获得子进程的退出状态,这样内核就可以在内存中释放子进程了,但是如果父进程没有进行wait系统调用,子进程就会驻留在内存,成为僵尸进程。 + +孤儿进程就是父进程退出,但是它的子进程还在进行,这些子进程就会变成孤儿进程,被init进程(进程号为1)所收养,由它来管理和收集子进程的状态。由于孤儿进程有init进程循环的wait()调用回收资源,所以不会产生什么危害。 + +##### Linux指令使用 + +统计access.log中ip访问次数前十的 + +``` +cat access.log | awk '{ print $1}' | sort -n | uniq -c | sort - r |head 10 +``` + +统计当前目录下(包含子目录) java 的文件的代码总行数。 + +``` +wc -l `find . -name "*.java"` | awk '{ sum=sum+$1 } END { print sum }' +``` + +### Linux进程间通信的方式? + +《Linux 的进程间通信》 https://zhuanlan.zhihu.com/p/58489873 + +浅析进程间通信的几种方式(含实例源码) https://zhuanlan.zhihu.com/p/94856678 + +https://mp.weixin.qq.com/s/WgZaS5w5IXa3IBGRsPKtbQ \ No newline at end of file diff --git a/index.html b/index.html index 3adbbbd..a1aa2b7 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - 《后端技术总结》 + 《大厂面试指北》 @@ -14,7 +14,7 @@ - - + + diff --git a/static/1024555-20161217182750011-675658660.png b/static/1024555-20161217182750011-675658660.png new file mode 100644 index 0000000..29dfa57 Binary files /dev/null and b/static/1024555-20161217182750011-675658660.png differ diff --git a/static/1024555-20161217182857323-2092264199.png b/static/1024555-20161217182857323-2092264199.png new file mode 100644 index 0000000..69924e6 Binary files /dev/null and b/static/1024555-20161217182857323-2092264199.png differ diff --git a/static/12609483-1829498245a5787b.png b/static/12609483-1829498245a5787b.png index b1be1a7..3ccd70f 100755 Binary files a/static/12609483-1829498245a5787b.png and b/static/12609483-1829498245a5787b.png differ diff --git a/static/12609483-199bd1a12bee5cc3.png b/static/12609483-199bd1a12bee5cc3.png old mode 100644 new mode 100755 index 9e84b0d..546147b Binary files a/static/12609483-199bd1a12bee5cc3.png and b/static/12609483-199bd1a12bee5cc3.png differ diff --git a/static/1415794-20190804110605330-45276489.png b/static/1415794-20190804110605330-45276489.png old mode 100644 new mode 100755 index 028c6ba..b87395d Binary files a/static/1415794-20190804110605330-45276489.png and b/static/1415794-20190804110605330-45276489.png differ diff --git a/static/160_statement.png b/static/160_statement.png new file mode 100644 index 0000000..55870e3 Binary files /dev/null and b/static/160_statement.png differ diff --git a/static/2018051322430635.png b/static/2018051322430635.png new file mode 100644 index 0000000..28c10a1 Binary files /dev/null and b/static/2018051322430635.png differ diff --git a/static/249993-20161215143120620-1544337380-20201130113344624.png b/static/249993-20161215143120620-1544337380-20201130113344624.png new file mode 100644 index 0000000..430502b Binary files /dev/null and b/static/249993-20161215143120620-1544337380-20201130113344624.png differ diff --git a/static/249993-20161215143120620-1544337380.png b/static/249993-20161215143120620-1544337380.png new file mode 100644 index 0000000..430502b Binary files /dev/null and b/static/249993-20161215143120620-1544337380.png differ diff --git a/static/31bad766983e212431077ca8da92762050214.png b/static/31bad766983e212431077ca8da92762050214.png new file mode 100644 index 0000000..fbe43d0 Binary files /dev/null and b/static/31bad766983e212431077ca8da92762050214.png differ diff --git a/static/351684196936_pic.jpg b/static/351684196936_pic.jpg new file mode 100644 index 0000000..434bb31 Binary files /dev/null and b/static/351684196936_pic.jpg differ diff --git a/static/6111.jpeg b/static/6111.jpeg new file mode 100644 index 0000000..368dee2 Binary files /dev/null and b/static/6111.jpeg differ diff --git a/static/640 b/static/640 new file mode 100644 index 0000000..ad20ba4 Binary files /dev/null and b/static/640 differ diff --git a/static/640-1487645. b/static/640-1487645. new file mode 100644 index 0000000..bc450b0 Binary files /dev/null and b/static/640-1487645. differ diff --git a/static/640-1751368.png b/static/640-1751368.png new file mode 100644 index 0000000..eb28c20 Binary files /dev/null and b/static/640-1751368.png differ diff --git a/static/640-20201128120828308 b/static/640-20201128120828308 new file mode 100644 index 0000000..92c186d Binary files /dev/null and b/static/640-20201128120828308 differ diff --git a/static/640-20201128120828359 b/static/640-20201128120828359 new file mode 100644 index 0000000..78b37d8 Binary files /dev/null and b/static/640-20201128120828359 differ diff --git a/static/640-20201128120828561.png b/static/640-20201128120828561.png new file mode 100644 index 0000000..ad7faaa Binary files /dev/null and b/static/640-20201128120828561.png differ diff --git a/static/640-20210124192752928 b/static/640-20210124192752928 new file mode 100644 index 0000000..8906ec1 Binary files /dev/null and b/static/640-20210124192752928 differ diff --git a/static/640-20210124203435521 b/static/640-20210124203435521 new file mode 100644 index 0000000..51a3329 Binary files /dev/null and b/static/640-20210124203435521 differ diff --git a/static/640-20210221154207974.png b/static/640-20210221154207974.png new file mode 100644 index 0000000..e1c83ee Binary files /dev/null and b/static/640-20210221154207974.png differ diff --git a/static/640-20210326173442637 b/static/640-20210326173442637 new file mode 100644 index 0000000..d7af96e Binary files /dev/null and b/static/640-20210326173442637 differ diff --git a/static/640-20210326191349615 b/static/640-20210326191349615 new file mode 100644 index 0000000..3bfce30 Binary files /dev/null and b/static/640-20210326191349615 differ diff --git a/static/640-3893313.png b/static/640-3893313.png new file mode 100644 index 0000000..5055f5c Binary files /dev/null and b/static/640-3893313.png differ diff --git a/static/640-6536508.png b/static/640-6536508.png new file mode 100644 index 0000000..8801d69 Binary files /dev/null and b/static/640-6536508.png differ diff --git a/static/640-8127488. b/static/640-8127488. new file mode 100644 index 0000000..3b54fcc Binary files /dev/null and b/static/640-8127488. differ diff --git a/static/640-8127813.png b/static/640-8127813.png new file mode 100644 index 0000000..93498ba Binary files /dev/null and b/static/640-8127813.png differ diff --git "a/static/640\347\232\204\345\211\257\346\234\254" "b/static/640\347\232\204\345\211\257\346\234\254" new file mode 100644 index 0000000..d50172b Binary files /dev/null and "b/static/640\347\232\204\345\211\257\346\234\254" differ diff --git a/static/66b6ae1dec5b96084c3a6d29174a20e3.png b/static/66b6ae1dec5b96084c3a6d29174a20e3.png new file mode 100644 index 0000000..c1a33ae Binary files /dev/null and b/static/66b6ae1dec5b96084c3a6d29174a20e3.png differ diff --git a/static/9379fe1666818237f842138812bf63bd85645.png b/static/9379fe1666818237f842138812bf63bd85645.png new file mode 100644 index 0000000..beeec85 Binary files /dev/null and b/static/9379fe1666818237f842138812bf63bd85645.png differ diff --git a/static/WechatIMG0328.jpeg b/static/WechatIMG0328.jpeg new file mode 100755 index 0000000..e100b6c Binary files /dev/null and b/static/WechatIMG0328.jpeg differ diff --git a/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F368dee24bab62b1c27fd4a8387cea9619605195f.jpg b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F368dee24bab62b1c27fd4a8387cea9619605195f.jpg new file mode 100644 index 0000000..368dee2 Binary files /dev/null and b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F368dee24bab62b1c27fd4a8387cea9619605195f.jpg differ diff --git a/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7-20201216185759959.png b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7-20201216185759959.png new file mode 100644 index 0000000..5381a5e Binary files /dev/null and b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7-20201216185759959.png differ diff --git a/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7-20201216185819903.png b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7-20201216185819903.png new file mode 100644 index 0000000..5381a5e Binary files /dev/null and b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7-20201216185819903.png differ diff --git a/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7.png b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7.png new file mode 100644 index 0000000..5381a5e Binary files /dev/null and b/static/assets%2F-MNvWgO3xPDngyns_iic%2Fsync%2F5381a5e30482682c1c6f111e882991113b8661f7.png differ diff --git a/static/circularlinkedlist-9062165.png b/static/circularlinkedlist-9062165.png new file mode 100644 index 0000000..b0111ae Binary files /dev/null and b/static/circularlinkedlist-9062165.png differ diff --git a/static/circularlinkedlist.png b/static/circularlinkedlist.png new file mode 100644 index 0000000..b0111ae Binary files /dev/null and b/static/circularlinkedlist.png differ diff --git a/static/coin.png b/static/coin.png new file mode 100644 index 0000000..5381a5e Binary files /dev/null and b/static/coin.png differ diff --git a/static/d7687db8ecbd43a79d041badf07bbaf4~tplv-k3u1fbpfcp-watermark-20210316161015142.image b/static/d7687db8ecbd43a79d041badf07bbaf4~tplv-k3u1fbpfcp-watermark-20210316161015142.image new file mode 100644 index 0000000..817d252 Binary files /dev/null and b/static/d7687db8ecbd43a79d041badf07bbaf4~tplv-k3u1fbpfcp-watermark-20210316161015142.image differ diff --git a/static/d7687db8ecbd43a79d041badf07bbaf4~tplv-k3u1fbpfcp-watermark.image b/static/d7687db8ecbd43a79d041badf07bbaf4~tplv-k3u1fbpfcp-watermark.image new file mode 100644 index 0000000..817d252 Binary files /dev/null and b/static/d7687db8ecbd43a79d041badf07bbaf4~tplv-k3u1fbpfcp-watermark.image differ diff --git a/static/docsify.min.js b/static/docsify.min.js index 5e3f1f1..7893b85 100644 --- a/static/docsify.min.js +++ b/static/docsify.min.js @@ -1 +1 @@ -!function(){function o(n){var r=Object.create(null);return function(e){var t=c(e)?e:JSON.stringify(e);return r[t]||(r[t]=n(e))}}var i=o(function(e){return e.replace(/([A-Z])/g,function(e){return"-"+e.toLowerCase()})}),l=Object.prototype.hasOwnProperty,d=Object.assign||function(e){for(var t=arguments,n=1;n":">",'"':""","'":"'","/":"/"};return String(e).replace(/[&<>"'/]/g,function(e){return t[e]})}function p(e,t,r,i){void 0===i&&(i=h);var a=e._hooks[t],s=function(t){var e=a[t];if(t>=a.length)i(r);else if("function"==typeof e)if(2===e.length)e(r,function(e){r=e,s(t+1)});else{var n=e(r);r=void 0===n?r:n,s(t+1)}else s(t+1)};s(0)}var g=document.body.clientWidth<=600,a=window.history&&window.history.pushState&&window.history.replaceState&&!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/),n={};function m(e,t){if(void 0===t&&(t=!1),"string"==typeof e){if(void 0!==window.Vue)return y(e);e=t?y(e):n[e]||(n[e]=y(e))}return e}var f=document,v=f.body,b=f.head;function y(e,t){return t?e.querySelector(t):f.querySelector(e)}function k(e,t){return[].slice.call(t?e.querySelectorAll(t):f.querySelectorAll(e))}function w(e,t){return e=f.createElement(e),t&&(e.innerHTML=t),e}function s(e,t){return e.appendChild(t)}function x(e,t){return e.insertBefore(t,e.children[0])}function _(e,t,n){r(t)?window.addEventListener(e,t):e.addEventListener(t,n)}function S(e,t,n){r(t)?window.removeEventListener(e,t):e.removeEventListener(t,n)}function A(e,t,n){e&&e.classList[n?t:"toggle"](n||t)}var $,C,e=Object.freeze({__proto__:null,getNode:m,$:f,body:v,head:b,find:y,findAll:k,create:w,appendTo:s,before:x,on:_,off:S,toggleClass:A,style:function(e){s(b,w("style",e))}});function E(e,t){if(void 0===t&&(t='
    {inner}
'),!e||!e.length)return"";var n="";return e.forEach(function(e){n+='
  • '+e.title+"
  • ",e.children&&(n+=E(e.children,t))}),t.replace("{inner}",n)}function F(e,t){return'

    '+t.slice(5).trim()+"

    "}function L(e){var t,n=e.loaded,r=e.total,i=e.step;$||function(){var e=w("div");e.classList.add("progress"),s(v,e),$=e}(),t=i?80<(t=parseInt($.style.width||0,10)+i)?80:t:Math.floor(n/r*100),$.style.opacity=1,$.style.width=95<=t?"100%":t+"%",95<=t&&(clearTimeout(C),C=setTimeout(function(e){$.style.opacity=0,$.style.width="0%"},200))}var T={};function R(a,e,t){void 0===e&&(e=!1),void 0===t&&(t={});function n(){s.addEventListener.apply(s,arguments)}var s=new XMLHttpRequest,r=T[a];if(r)return{then:function(e){return e(r.content,r.opt)},abort:h};for(var i in s.open("GET",a),t)l.call(t,i)&&s.setRequestHeader(i,t[i]);return s.send(),{then:function(r,i){if(void 0===i&&(i=h),e){var t=setInterval(function(e){return L({step:Math.floor(5*Math.random()+1)})},500);n("progress",L),n("loadend",function(e){L(e),clearInterval(t)})}n("error",i),n("load",function(e){var t=e.target;if(400<=t.status)i(t);else{var n=T[a]={content:t.response,opt:{updatedAt:s.getResponseHeader("last-modified")}};r(n.content,n.opt)}})},abort:function(e){return 4!==s.readyState&&s.abort()}}}function O(e,t){e.innerHTML=e.innerHTML.replace(/var\(\s*--theme-color.*?\)/g,t)}var P=/([^{]*?)\w(?=\})/g,z={YYYY:"getFullYear",YY:"getYear",MM:function(e){return e.getMonth()+1},DD:"getDate",HH:"getHours",mm:"getMinutes",ss:"getSeconds",fff:"getMilliseconds"};var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function N(e,t){return e(t={exports:{}},t.exports),t.exports}var j=N(function(v,e){!function(){var y={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:/^ {0,3}(`{3,}|~{3,})([^`~\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6}) +([^\n]*?)(?: +#+)? *(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|\\n*|\\n*|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:h,table:h,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html)[^\n]+)*)/,text:/^[^\n]+/};function l(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||m.defaults,this.rules=y.normal,this.options.pedantic?this.rules=y.pedantic:this.options.gfm&&(this.rules=y.gfm)}y._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,y._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,y.def=e(y.def).replace("label",y._label).replace("title",y._title).getRegex(),y.bullet=/(?:[*+-]|\d{1,9}\.)/,y.item=/^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/,y.item=e(y.item,"gm").replace(/bull/g,y.bullet).getRegex(),y.list=e(y.list).replace(/bull/g,y.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+y.def.source+")").getRegex(),y._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",y._comment=//,y.html=e(y.html,"i").replace("comment",y._comment).replace("tag",y._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y.paragraph=e(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} +").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}|~{3,})[^`\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",y._tag).getRegex(),y.blockquote=e(y.blockquote).replace("paragraph",y.paragraph).getRegex(),y.normal=d({},y),y.gfm=d({},y.normal,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),y.pedantic=d({},y.normal,{html:e("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",y._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,fences:h,paragraph:e(y.normal._paragraph).replace("hr",y.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),l.rules=y,l.lex=function(e,t){return new l(t).lex(e)},l.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},l.prototype.token=function(e,t){var n,r,i,a,s,o,l,c,u,h,p,d,g,f,m,v;for(e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),1 ?/gm,""),this.token(i,t),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),l={type:"list_start",ordered:f=1<(a=i[2]).length,start:f?+a:"",loose:!1},this.tokens.push(l),n=!(c=[]),g=(i=i[0].match(this.rules.item)).length,p=0;p?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:h,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*<\[])\*(?!\*)|^_([^\s<][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_<][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s<"][\s\S]*?[^\s\*])\*(?!\*|[^\spunctuation])|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:h,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\?@\\[^_{|}~",n.em=e(n.em).replace(/punctuation/g,n._punctuation).getRegex(),n._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=e(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=e(n.tag).replace("comment",y._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,n._href=/<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=e(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=e(n.reflink).replace("label",n._label).getRegex(),n.normal=d({},n),n.pedantic=d({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:e(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:e(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=d({},n.normal,{escape:e(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\/i.test(a[0])&&(this.inLink=!1),!this.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(a[0])?this.inRawBlock=!0:this.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(a[0])&&(this.inRawBlock=!1),e=e.substring(a[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0];else if(a=this.rules.link.exec(e)){var l=g(a[2],"()");if(-1$/,"$1"),o+=this.outputLink(a,{href:u.escapes(r),title:u.escapes(i)}),this.inLink=!1}else if((a=this.rules.reflink.exec(e))||(a=this.rules.nolink.exec(e))){if(e=e.substring(a[0].length),t=(a[2]||a[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=a[0].charAt(0),e=a[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(a,t),this.inLink=!1}else if(a=this.rules.strong.exec(e))e=e.substring(a[0].length),o+=this.renderer.strong(this.output(a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.em.exec(e))e=e.substring(a[0].length),o+=this.renderer.em(this.output(a[6]||a[5]||a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.code.exec(e))e=e.substring(a[0].length),o+=this.renderer.codespan(k(a[2].trim(),!0));else if(a=this.rules.br.exec(e))e=e.substring(a[0].length),o+=this.renderer.br();else if(a=this.rules.del.exec(e))e=e.substring(a[0].length),o+=this.renderer.del(this.output(a[1]));else if(a=this.rules.autolink.exec(e))e=e.substring(a[0].length),r="@"===a[2]?"mailto:"+(n=k(this.mangle(a[1]))):n=k(a[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(a=this.rules.url.exec(e))){if(a=this.rules.text.exec(e))e=e.substring(a[0].length),this.inRawBlock?o+=this.renderer.text(this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0]):o+=this.renderer.text(k(this.smartypants(a[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===a[2])r="mailto:"+(n=k(a[0]));else{for(;s=a[0],a[0]=this.rules._backpedal.exec(a[0])[0],s!==a[0];);n=k(a[0]),r="www."===a[1]?"http://"+n:n}e=e.substring(a[0].length),o+=this.renderer.link(r,null,n)}return o},u.escapes=function(e){return e?e.replace(u.rules._escapes,"$1"):e},u.prototype.outputLink=function(e,t){var n=t.href,r=t.title?k(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,k(e[1]))},u.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},u.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;i'+(n?e:k(e,!0))+"\n":"
    "+(n?e:k(e,!0))+"
    "},r.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n,r){return this.options.headerIds?"'+e+"\n":""+e+"\n"},r.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},r.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},r.prototype.checkbox=function(e){return" "},r.prototype.paragraph=function(e){return"

    "+e+"

    \n"},r.prototype.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},r.prototype.tablerow=function(e){return"\n"+e+"\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"},r.prototype.strong=function(e){return""+e+""},r.prototype.em=function(e){return""+e+""},r.prototype.codespan=function(e){return""+e+""},r.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},r.prototype.del=function(e){return""+e+""},r.prototype.link=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"},r.prototype.image=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r=''+n+'":">"},r.prototype.text=function(e){return e},i.prototype.strong=i.prototype.em=i.prototype.codespan=i.prototype.del=i.prototype.text=function(e){return e},i.prototype.link=i.prototype.image=function(e,t,n){return""+n},i.prototype.br=function(){return""},c.parse=function(e,t){return new c(t).parse(e)},c.prototype.parse=function(e){this.inline=new u(e.links,this.options),this.inlineText=new u(e.links,d({},this.options,{renderer:new i})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},c.prototype.next=function(){return this.token=this.tokens.pop(),this.token},c.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},c.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},c.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,p(this.inlineText.output(this.token.text)),this.slugger);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i="",a="";for(n="",e=0;e?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t))for(var n=t;this.seen[n]++,t=n+"-"+this.seen[n],this.seen.hasOwnProperty(t););return this.seen[t]=0,t},k.escapeTest=/[&<>"']/,k.escapeReplace=/[&<>"']/g,k.replacements={"&":"&","<":"<",">":">",'"':""","'":"'"},k.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,k.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var s={},o=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function h(){}function d(e){for(var t,n,r=arguments,i=1;it)n.splice(t);else for(;n.lengthAn error occurred:

    "+k(e.message+"",!0)+"
    ";throw e}}h.exec=h,m.options=m.setOptions=function(e){return d(m.defaults,e),m},m.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,xhtml:!1}},m.defaults=m.getDefaults(),m.Parser=c,m.parser=c.parse,m.Renderer=r,m.TextRenderer=i,m.Lexer=l,m.lexer=l.lex,m.InlineLexer=u,m.inlineLexer=u.output,m.Slugger=t,m.parse=m,v.exports=m}()}),M=N(function(e){var c=function(c){var u=/\blang(?:uage)?-([\w-]+)\b/i,t=0,T={manual:c.Prism&&c.Prism.manual,disableWorkerMessageHandler:c.Prism&&c.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof R?new R(e.type,T.util.encode(e.content),e.alias):Array.isArray(e)?e.map(T.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(y instanceof R)){if(d&&v!=t.length-1){if(u.lastIndex=b,!(A=u.exec(e)))break;for(var k=A.index+(p?A[1].length:0),w=A.index+A[0].length,x=v,_=b,S=t.length;x"+n.content+""},!c.document)return c.addEventListener&&(T.disableWorkerMessageHandler||c.addEventListener("message",function(e){var t=JSON.parse(e.data),n=t.language,r=t.code,i=t.immediateClose;c.postMessage(T.highlight(r,T.languages[n],n)),i&&c.close()},!1)),T;var e=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return e&&(T.filename=e.src,T.manual||e.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(T.highlightAll):window.setTimeout(T.highlightAll,16):document.addEventListener("DOMContentLoaded",T.highlightAll))),T}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});e.exports&&(e.exports=c),void 0!==t&&(t.Prism=c),c.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.languages.markup.tag.inside["attr-value"].inside.entity=c.languages.markup.entity,c.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(c.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:c.languages[t]},n.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:n}};r["language-"+t]={pattern:/[\s\S]+/,inside:c.languages[t]};var i={};i[e]={pattern:RegExp(/(<__[\s\S]*?>)(?:\s*|[\s\S])*?(?=<\/__>)/.source.replace(/__/g,e),"i"),lookbehind:!0,greedy:!0,inside:r},c.languages.insertBefore("markup","cdata",i)}}),c.languages.xml=c.languages.extend("markup",{}),c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,function(e){var t=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/@[\w-]+/}},url:{pattern:RegExp("url\\((?:"+t.source+"|[^\n\r()]*)\\)","i"),inside:{function:/^url/i,punctuation:/^\(|\)$/}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+t.source+")*?(?=\\s*\\{)"),string:{pattern:t,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:n.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:e.languages.css}},alias:"language-css"}},n.tag))}(c),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{"class-name":[c.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.])\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),c.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,c.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:c.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:c.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:c.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:c.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),c.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.markup.tag.addInlined("script","javascript"),c.languages.js=c.languages.javascript,"undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector&&(self.Prism.fileHighlight=function(e){e=e||document;var l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"};Array.prototype.slice.call(e.querySelectorAll("pre[data-src]")).forEach(function(e){if(!e.hasAttribute("data-src-loaded")){for(var t,n=e.getAttribute("data-src"),r=e,i=/\blang(?:uage)?-([\w-]+)\b/i;r&&!i.test(r.className);)r=r.parentNode;if(r&&(t=(e.className.match(i)||[,""])[1]),!t){var a=(n.match(/\.(\w+)$/)||[,""])[1];t=l[a]||a}var s=document.createElement("code");s.className="language-"+t,e.textContent="",s.textContent="Loading…",e.appendChild(s);var o=new XMLHttpRequest;o.open("GET",n,!0),o.onreadystatechange=function(){4==o.readyState&&(o.status<400&&o.responseText?(s.textContent=o.responseText,c.highlightElement(s),e.setAttribute("data-src-loaded","")):400<=o.status?s.textContent="✖ Error "+o.status+" while fetching file: "+o.statusText:s.textContent="✖ Error: File does not exist or is empty")},o.send(null)}}),c.plugins.toolbar&&c.plugins.toolbar.registerButton("download-file",function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-src")&&t.hasAttribute("data-download-link")){var n=t.getAttribute("data-src"),r=document.createElement("a");return r.textContent=t.getAttribute("data-download-link-label")||"Download",r.setAttribute("download",""),r.href=n,r}})},document.addEventListener("DOMContentLoaded",function(){self.Prism.fileHighlight()}))});function q(e,r){var i=[],a={};return e.forEach(function(e){var t=e.level||1,n=t-1;r?@[\]^`{|}~]/g;function Z(e){return e.toLowerCase()}function B(e){if("string"!=typeof e)return"";var t=e.trim().replace(/[A-Z]+/g,Z).replace(/<[^>\d]+>/g,"").replace(I,"").replace(/\s/g,"-").replace(/-+/g,"-").replace(/^(\d)/,"_$1"),n=H[t];return n=l.call(H,t)?n+1:0,(H[t]=n)&&(t=t+"-"+n),t}function D(e,t){return''+t+''}B.clear=function(){H={}};var U=decodeURIComponent,Y=encodeURIComponent;function G(e){var n={};return(e=e.trim().replace(/^(\?|#|&)/,""))&&e.split("&").forEach(function(e){var t=e.replace(/\+/g," ").split("=");n[t[0]]=t[1]&&U(t[1])}),n}function V(e,t){void 0===t&&(t=[]);var n=[];for(var r in e)-1=m.length);n++){var r=t[n];if("string"==typeof r||r.content&&"string"==typeof r.content){var i=m[f],a=d.tokenStack[i],s="string"==typeof r?r:r.content,o=ne(g,i),l=s.indexOf(o);if(-1"}},video:function(e,t){return{html:'"}},audio:function(e,t){return{html:'"}},code:function(e,t){var n=e.match(/\.(\w+)$/);return"md"===(n=t||n&&n[1])&&(n="markdown"),{url:e,lang:n}}},se=function(i,e){var a=this;this.config=i,this.router=e,this.cacheTree={},this.toc=[],this.cacheTOC={},this.linkTarget=i.externalLinkTarget||"_blank",this.linkRel="_blank"===this.linkTarget?i.externalLinkRel||"noopener":"",this.contentBase=e.getBasePath();var s,t=this._initRenderer(),n=i.markdown||{};s=r(n)?n(j,t):(j.setOptions(d(n,{renderer:d(t,n.renderer)})),j),this._marked=s,this.compile=function(n){var r=!0,e=o(function(e){r=!1;var t="";return n?(t=c(n)?s(n):s.parser(n),t=i.noEmoji?t:function(e){return e.replace(/<(pre|template|code)[^>]*?>[\s\S]+?<\/(pre|template|code)>/g,function(e){return e.replace(/:/g,"__colon__")}).replace(/:(\w+?):/gi,window.emojify||D).replace(/__colon__/g,":")}(t),B.clear(),t):n})(n),t=a.router.parse().file;return r?a.toc=a.cacheTOC[t]:a.cacheTOC[t]=[].concat(a.toc),e}};se.prototype.compileEmbed=function(e,t){var n,r=ie(t),i=r.str,a=r.config;if(t=i,a.include){var s;if(W(e)||(e=K(this.contentBase,X(this.router.getCurrentPath()),e)),a.type&&(s=ae[a.type]))(n=s.call(this,e,t)).type=a.type;else{var o="code";/\.(md|markdown)/.test(e)?o="markdown":/\.mmd/.test(e)?o="mermaid":/\.html?/.test(e)?o="iframe":/\.(mp4|ogg)/.test(e)?o="video":/\.mp3/.test(e)&&(o="audio"),(n=ae[o].call(this,e,t)).type=o}return n.fragment=a.fragment,n}},se.prototype._matchNotCompileLink=function(e){for(var t=this.config.noCompileLinks||[],n=0;n
    '+r+""},t.code=e.code=function(e,t){return void 0===t&&(t=""),e=e.replace(/@DOCSIFY_QM@/g,"`"),'
    '+M.highlight(e,M.languages[t]||M.languages.markup)+"
    "},t.link=e.link=function(e,t,n){void 0===t&&(t="");var r="",i=ie(t),a=i.str,s=i.config;return t=a,W(e)||c._matchNotCompileLink(e)||s.ignore?(r+=0===e.indexOf("mailto:")?"":' target="'+o+'"',r+=0===e.indexOf("mailto:")?"":""!==l?' rel="'+l+'"':""):(e===c.config.homepage&&(e="README"),e=u.toURL(e,null,u.getCurrentPath())),s.target&&(r+=" target="+s.target),s.disabled&&(r+=" disabled",e="javascript:void(0)"),t&&(r+=' title="'+t+'"'),'"+n+""},t.paragraph=e.paragraph=function(e){return/^!>/.test(e)?F("tip",e):/^\?>/.test(e)?F("warn",e):"

    "+e+"

    "},t.image=e.image=function(e,t,n){var r=e,i="",a=ie(t),s=a.str,o=a.config;t=s,o["no-zoom"]&&(i+=" data-no-zoom"),t&&(i+=' title="'+t+'"');var l=o.size;if(l){var c=l.split("x");c[1]?i+="width="+c[0]+" height="+c[1]:i+="width="+c[0]}return W(e)||(r=K(h,X(u.getCurrentPath()),e)),''+n+'"},t.list=e.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+" "+[/
  • /.test(e.split('class="task-list"')[0])?'class="task-list"':"",n&&1"+e+""},t.listitem=e.listitem=function(e){return/^(]*>)/.test(e)?'
  • ":"
  • "+e+"
  • "},e.origin=t,e},se.prototype.sidebar=function(e,t){var n=this.toc,r=this.router.getCurrentPath(),i="";if(e)i=this.compile(e);else{for(var a=0;a{inner}"),this.cacheTree[r]=l}return i},se.prototype.subSidebar=function(e){if(e){var t=this.router.getCurrentPath(),n=this.cacheTree,r=this.toc;r[0]&&r[0].ignoreAllSubs&&r.splice(0),r[0]&&1===r[0].level&&r.shift();for(var i=0;i=t||e.classList.contains("hidden")?A(v,"add","sticky"):A(v,"remove","sticky")}}function ce(e,t,r,n){var i=[];null!=(t=m(t))&&(i=k(t,"a"));var a,s=decodeURI(e.toURL(e.getCurrentPath()));return i.sort(function(e,t){return t.href.length-e.href.length}).forEach(function(e){var t=e.getAttribute("href"),n=r?e.parentNode:e;0!==s.indexOf(t)||a?A(n,"remove","active"):(a=e,A(n,"add","active"))}),n&&(f.title=a?a.title||a.innerText+" - "+oe:oe),a}function ue(e,t){for(var n=0;nthis.end&&e>=this.next}[this.direction]}},{key:"_defaultEase",value:function(e,t,n,r){return(e/=r/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}}]),fe);function fe(){var e=0o){t=t||u;break}t=u}if(t){var h=me[xe(decodeURIComponent(e),t.getAttribute("data-id"))];if(h&&h!==a&&(a&&a.classList.remove("active"),h.classList.add("active"),a=h,!ve&&v.classList.contains("sticky"))){var p=n.clientHeight,d=a.offsetTop+a.clientHeight+40,g=d-0=i.scrollTop&&d<=i.scrollTop+p?i.scrollTop:g?0:d-p;n.scrollTop=f}}}}function xe(e,t){return e+"?id="+t}function _e(e,t){if(t){var n=y("#"+t);n&&function(e){be&&be.stop(),ye=!1,be=new ge({start:window.pageYOffset,end:e.getBoundingClientRect().top+window.pageYOffset,duration:500}).on("tick",function(e){return window.scrollTo(0,e)}).on("done",function(){ye=!0,be=null}).begin()}(n);var r=me[xe(e,t)],i=y(m(".sidebar"),"li.active");i&&i.classList.remove("active"),r&&r.classList.add("active")}}var Se=f.scrollingElement||f.documentElement;var Ae={};function $e(e,i){var s=e.compiler,a=e.raw;void 0===a&&(a="");var t=e.fetch,n=Ae[a];if(n){var r=n.slice();return r.links=n.links,i(r)}var o=s._marked,l=o.lexer(a),c=[],u=o.InlineLexer.rules.link,h=l.links;l.forEach(function(e,a){"paragraph"===e.type&&(e.text=e.text.replace(new RegExp(u.source,"g"),function(e,t,n,r){var i=s.compileEmbed(n,r);return i&&c.push({index:a,embed:i}),e}))});var p=0;!function(e,a){var t,n=e.embedTokens,s=e.compile,o=(e.fetch,0),l=1;if(!n.length)return a({});for(;t=n[o++];){var r=function(i){return function(e){var t;if(e)if("markdown"===i.embed.type)t=s.lexer(e);else if("code"===i.embed.type){if(i.embed.fragment){var n=i.embed.fragment,r=new RegExp("(?:###|\\/\\/\\/)\\s*\\["+n+"\\]([\\s\\S]*)(?:###|\\/\\/\\/)\\s*\\["+n+"\\]");e=((e.match(r)||[])[1]||"").trim()}t=s.lexer("```"+i.embed.lang+"\n"+e.replace(/`/g,"@DOCSIFY_QM@")+"\n```\n")}else"mermaid"===i.embed.type?(t=[{type:"html",text:'
    \n'+e+"\n
    "}]).links={}:(t=[{type:"html",text:e}]).links={};a({token:i,embedToken:t}),++l>=o&&a({})}}(t);t.embed.url?R(t.embed.url).then(r):r(t.embed.html)}}({compile:o,embedTokens:c,fetch:t},function(e){var t=e.embedToken,n=e.token;if(n){var r=n.index+p;d(h,t.links),l=l.slice(0,r).concat(t,l.slice(r+1)),p+=t.length-1}else Ae[a]=l.concat(),l.links=Ae[a].links=h,i(l)})}function Ce(){var e=k(".markdown-section>script").filter(function(e){return!/template/.test(e.type)})[0];if(!e)return!1;var t=e.innerText.trim();if(!t)return!1;setTimeout(function(e){window.__EXECUTE_RESULT__=new Function(t)()},0)}function Ee(e,t,n){return t="function"==typeof n?n(t):"string"==typeof n?function(r,i){var a=[],s=0;return r.replace(P,function(t,e,n){a.push(r.substring(s,n-1)),s=n+=t.length+1,a.push(i&&i[t]||function(e){return("00"+("string"==typeof z[t]?e[z[t]]():z[t](e))).slice(-t.length)})}),s!==r.length&&a.push(r.substring(s)),function(e){for(var t="",n=0,r=e||new Date;n404 - Not found",this._renderTo(".markdown-section",e),this.config.loadSidebar||this._renderSidebar(),!1===this.config.executeScript||void 0===window.Vue||Ce()?this.config.executeScript&&Ce():setTimeout(function(e){var t=window.__EXECUTE_RESULT__;t&&t.$destroy&&t.$destroy(),window.__EXECUTE_RESULT__=(new window.Vue).$mount("#main")},0)}function Le(e){var t=e.config;e.compiler=new se(t,e.router),window.__current_docsify_compiler__=e.compiler;var n=t.el||"#app",r=y("nav")||w("nav"),i=y(n),a="",s=v;if(i){if(t.repo&&(a+=function(e,t){return e?(/\/\//.test(e)||(e="https://github.com/"+e),''):""}(t.repo,t.cornerExternalLinkTarge)),t.coverpage&&(a+=function(){var e=", 100%, 85%";return'
    \x3c!--cover--\x3e
    '}()),t.logo){var o=/^data:image/.test(t.logo),l=/(?:http[s]?:)?\/\//.test(t.logo),c=/^\./.test(t.logo);o||l||c||(t.logo=K(e.router.getBasePath(),t.logo))}a+=function(e){var t=e.name?u(e.name):"",n='';return(g?n+"
    ":"
    "+n)+'
    \x3c!--main--\x3e
    '}(t),e._renderTo(i,a,!0)}else e.rendered=!0;t.mergeNavbar&&g?s=y(".sidebar"):(r.classList.add("app-nav"),t.repo||r.classList.add("no-badge")),t.loadNavbar&&x(s,r),t.themeColor&&(f.head.appendChild(w("div",function(e){return""}(t.themeColor)).firstElementChild),function(n){if(!(window.CSS&&window.CSS.supports&&window.CSS.supports("(--v:red)"))){var e=k("style:not(.inserted),link");[].forEach.call(e,function(e){if("STYLE"===e.nodeName)O(e,n);else if("LINK"===e.nodeName){var t=e.getAttribute("href");if(!/\.css$/.test(t))return;R(t).then(function(e){var t=w("style",e);b.appendChild(t),O(t,n)})}})}}(t.themeColor)),e._updateRender(),A(v,"ready")}var Te={};function Re(e){this.config=e}function Oe(e){var t=location.href.indexOf("#");location.replace(location.href.slice(0,0<=t?t:0)+"#"+e)}Re.prototype.getBasePath=function(){return this.config.basePath},Re.prototype.getFile=function(e,t){void 0===e&&(e=this.getCurrentPath());var n=this.config,r=this.getBasePath(),i="string"==typeof n.ext?n.ext:".md";return e=(e=function(e,t){return new RegExp("\\.("+t.replace(/^\./,"")+"|html)$","g").test(e)?e:/\/$/g.test(e)?e+"README"+t:""+e+t}(e=n.alias?function e(t,n,r){var i=Object.keys(n).filter(function(e){return(Te[e]||(Te[e]=new RegExp("^"+e+"$"))).test(t)&&t!==r})[0];return i?e(t.replace(Te[i],n[i]),n,t):t}(e,n.alias):e,i))==="/README"+i&&n.homepage||e,e=W(e)?e:K(r,e),t&&(e=e.replace(new RegExp("^"+r),"")),e},Re.prototype.onchange=function(e){void 0===e&&(e=h),e()},Re.prototype.getCurrentPath=function(){},Re.prototype.normalize=function(){},Re.prototype.parse=function(){},Re.prototype.toURL=function(e,t,n){var r=n&&"#"===e[0],i=this.parse(te(e));if(i.query=d({},i.query,t),e=(e=i.path+V(i.query)).replace(/\.md(\?)|\.md$/,"$1"),r){var a=n.indexOf("?");e=(0([^<]*?)

    $');if(i){if("color"===i[2])n.style.background=i[1]+(i[3]||"");else{var a=i[1];A(n,"add","has-mask"),W(i[1])||(a=K(this.router.getBasePath(),i[1])),n.style.backgroundImage="url("http://github.com/+a+")",n.style.backgroundSize="cover",n.style.backgroundPosition="center center"}r=r.replace(i[0],"")}this._renderTo(".cover-main",r),le()}else A(n,"remove","show")},Ze._updateRender=function(){!function(e){var t=m(".app-name-link"),n=e.config.nameLink,r=e.route.path;if(t)if(c(e.config.nameLink))t.setAttribute("href",n);else if("object"==typeof n){var i=Object.keys(n).filter(function(e){return-1":">",'"':""","'":"'","/":"/"};return String(e).replace(/[&<>"'/]/g,function(e){return t[e]})}function p(e,t,r,i){void 0===i&&(i=h);var a=e._hooks[t],s=function(t){var e=a[t];if(t>=a.length)i(r);else if("function"==typeof e)if(2===e.length)e(r,function(e){r=e,s(t+1)});else{var n=e(r);r=void 0===n?r:n,s(t+1)}else s(t+1)};s(0)}var g=document.body.clientWidth<=600,a=window.history&&window.history.pushState&&window.history.replaceState&&!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/),n={};function m(e,t){if(void 0===t&&(t=!1),"string"==typeof e){if(void 0!==window.Vue)return y(e);e=t?y(e):n[e]||(n[e]=y(e))}return e}var f=document,v=f.body,b=f.head;function y(e,t){return t?e.querySelector(t):f.querySelector(e)}function k(e,t){return[].slice.call(t?e.querySelectorAll(t):f.querySelectorAll(e))}function w(e,t){return e=f.createElement(e),t&&(e.innerHTML=t),e}function s(e,t){return e.appendChild(t)}function x(e,t){return e.insertBefore(t,e.children[0])}function _(e,t,n){r(t)?window.addEventListener(e,t):e.addEventListener(t,n)}function S(e,t,n){r(t)?window.removeEventListener(e,t):e.removeEventListener(t,n)}function A(e,t,n){e&&e.classList[n?t:"toggle"](n||t)}var $,C,e=Object.freeze({__proto__:null,getNode:m,$:f,body:v,head:b,find:y,findAll:k,create:w,appendTo:s,before:x,on:_,off:S,toggleClass:A,style:function(e){s(b,w("style",e))}});function E(e,t){if(void 0===t&&(t='
      {inner}
    '),!e||!e.length)return"";var n="";return e.forEach(function(e){n+='
  • '+e.title+"
  • ",e.children&&(n+=E(e.children,t))}),t.replace("{inner}",n)}function F(e,t){return'

    '+t.slice(5).trim()+"

    "}function L(e){var t,n=e.loaded,r=e.total,i=e.step;$||function(){var e=w("div");e.classList.add("progress"),s(v,e),$=e}(),t=i?80<(t=parseInt($.style.width||0,10)+i)?80:t:Math.floor(n/r*100),$.style.opacity=1,$.style.width=95<=t?"100%":t+"%",95<=t&&(clearTimeout(C),C=setTimeout(function(e){$.style.opacity=0,$.style.width="0%"},200))}var T={};function R(a,e,t){void 0===e&&(e=!1),void 0===t&&(t={});function n(){s.addEventListener.apply(s,arguments)}var s=new XMLHttpRequest,r=T[a];if(r)return{then:function(e){return e(r.content,r.opt)},abort:h};for(var i in s.open("GET",a),t)l.call(t,i)&&s.setRequestHeader(i,t[i]);return s.send(),{then:function(r,i){if(void 0===i&&(i=h),e){var t=setInterval(function(e){return L({step:Math.floor(5*Math.random()+1)})},500);n("progress",L),n("loadend",function(e){L(e),clearInterval(t)})}n("error",i),n("load",function(e){var t=e.target;if(400<=t.status)i(t);else{var n=T[a]={content:t.response,opt:{updatedAt:s.getResponseHeader("last-modified")}};r(n.content,n.opt)}})},abort:function(e){return 4!==s.readyState&&s.abort()}}}function O(e,t){e.innerHTML=e.innerHTML.replace(/var\(\s*--theme-color.*?\)/g,t)}var P=/([^{]*?)\w(?=\})/g,z={YYYY:"getFullYear",YY:"getYear",MM:function(e){return e.getMonth()+1},DD:"getDate",HH:"getHours",mm:"getMinutes",ss:"getSeconds",fff:"getMilliseconds"};var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function N(e,t){return e(t={exports:{}},t.exports),t.exports}var j=N(function(v,e){!function(){var y={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:/^ {0,3}(`{3,}|~{3,})([^`~\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6}) +([^\n]*?)(?: +#+)? *(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|\\n*|\\n*|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:h,table:h,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html)[^\n]+)*)/,text:/^[^\n]+/};function l(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||m.defaults,this.rules=y.normal,this.options.pedantic?this.rules=y.pedantic:this.options.gfm&&(this.rules=y.gfm)}y._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,y._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,y.def=e(y.def).replace("label",y._label).replace("title",y._title).getRegex(),y.bullet=/(?:[*+-]|\d{1,9}\.)/,y.item=/^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/,y.item=e(y.item,"gm").replace(/bull/g,y.bullet).getRegex(),y.list=e(y.list).replace(/bull/g,y.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+y.def.source+")").getRegex(),y._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",y._comment=//,y.html=e(y.html,"i").replace("comment",y._comment).replace("tag",y._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y.paragraph=e(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} +").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}|~{3,})[^`\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",y._tag).getRegex(),y.blockquote=e(y.blockquote).replace("paragraph",y.paragraph).getRegex(),y.normal=d({},y),y.gfm=d({},y.normal,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),y.pedantic=d({},y.normal,{html:e("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",y._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,fences:h,paragraph:e(y.normal._paragraph).replace("hr",y.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),l.rules=y,l.lex=function(e,t){return new l(t).lex(e)},l.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},l.prototype.token=function(e,t){var n,r,i,a,s,o,l,c,u,h,p,d,g,f,m,v;for(e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),1 ?/gm,""),this.token(i,t),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),l={type:"list_start",ordered:f=1<(a=i[2]).length,start:f?+a:"",loose:!1},this.tokens.push(l),n=!(c=[]),g=(i=i[0].match(this.rules.item)).length,p=0;p?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:h,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*<\[])\*(?!\*)|^_([^\s<][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_<][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s<"][\s\S]*?[^\s\*])\*(?!\*|[^\spunctuation])|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:h,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\?@\\[^_{|}~",n.em=e(n.em).replace(/punctuation/g,n._punctuation).getRegex(),n._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=e(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=e(n.tag).replace("comment",y._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,n._href=/<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=e(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=e(n.reflink).replace("label",n._label).getRegex(),n.normal=d({},n),n.pedantic=d({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:e(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:e(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=d({},n.normal,{escape:e(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\/i.test(a[0])&&(this.inLink=!1),!this.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(a[0])?this.inRawBlock=!0:this.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(a[0])&&(this.inRawBlock=!1),e=e.substring(a[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0];else if(a=this.rules.link.exec(e)){var l=g(a[2],"()");if(-1$/,"$1"),o+=this.outputLink(a,{href:u.escapes(r),title:u.escapes(i)}),this.inLink=!1}else if((a=this.rules.reflink.exec(e))||(a=this.rules.nolink.exec(e))){if(e=e.substring(a[0].length),t=(a[2]||a[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=a[0].charAt(0),e=a[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(a,t),this.inLink=!1}else if(a=this.rules.strong.exec(e))e=e.substring(a[0].length),o+=this.renderer.strong(this.output(a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.em.exec(e))e=e.substring(a[0].length),o+=this.renderer.em(this.output(a[6]||a[5]||a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.code.exec(e))e=e.substring(a[0].length),o+=this.renderer.codespan(k(a[2].trim(),!0));else if(a=this.rules.br.exec(e))e=e.substring(a[0].length),o+=this.renderer.br();else if(a=this.rules.del.exec(e))e=e.substring(a[0].length),o+=this.renderer.del(this.output(a[1]));else if(a=this.rules.autolink.exec(e))e=e.substring(a[0].length),r="@"===a[2]?"mailto:"+(n=k(this.mangle(a[1]))):n=k(a[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(a=this.rules.url.exec(e))){if(a=this.rules.text.exec(e))e=e.substring(a[0].length),this.inRawBlock?o+=this.renderer.text(this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0]):o+=this.renderer.text(k(this.smartypants(a[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===a[2])r="mailto:"+(n=k(a[0]));else{for(;s=a[0],a[0]=this.rules._backpedal.exec(a[0])[0],s!==a[0];);n=k(a[0]),r="www."===a[1]?"http://"+n:n}e=e.substring(a[0].length),o+=this.renderer.link(r,null,n)}return o},u.escapes=function(e){return e?e.replace(u.rules._escapes,"$1"):e},u.prototype.outputLink=function(e,t){var n=t.href,r=t.title?k(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,k(e[1]))},u.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},u.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;i'+(n?e:k(e,!0))+"\n":"
    "+(n?e:k(e,!0))+"
    "},r.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n,r){return this.options.headerIds?"'+e+"\n":""+e+"\n"},r.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},r.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},r.prototype.checkbox=function(e){return" "},r.prototype.paragraph=function(e){return"

    "+e+"

    \n"},r.prototype.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},r.prototype.tablerow=function(e){return"\n"+e+"\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"},r.prototype.strong=function(e){return""+e+""},r.prototype.em=function(e){return""+e+""},r.prototype.codespan=function(e){return""+e+""},r.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},r.prototype.del=function(e){return""+e+""},r.prototype.link=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"},r.prototype.image=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r=''+n+'":">"},r.prototype.text=function(e){return e},i.prototype.strong=i.prototype.em=i.prototype.codespan=i.prototype.del=i.prototype.text=function(e){return e},i.prototype.link=i.prototype.image=function(e,t,n){return""+n},i.prototype.br=function(){return""},c.parse=function(e,t){return new c(t).parse(e)},c.prototype.parse=function(e){this.inline=new u(e.links,this.options),this.inlineText=new u(e.links,d({},this.options,{renderer:new i})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},c.prototype.next=function(){return this.token=this.tokens.pop(),this.token},c.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},c.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},c.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,p(this.inlineText.output(this.token.text)),this.slugger);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i="",a="";for(n="",e=0;e?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t))for(var n=t;this.seen[n]++,t=n+"-"+this.seen[n],this.seen.hasOwnProperty(t););return this.seen[t]=0,t},k.escapeTest=/[&<>"']/,k.escapeReplace=/[&<>"']/g,k.replacements={"&":"&","<":"<",">":">",'"':""","'":"'"},k.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,k.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var s={},o=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function h(){}function d(e){for(var t,n,r=arguments,i=1;it)n.splice(t);else for(;n.lengthAn error occurred:

    "+k(e.message+"",!0)+"
    ";throw e}}h.exec=h,m.options=m.setOptions=function(e){return d(m.defaults,e),m},m.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,xhtml:!1}},m.defaults=m.getDefaults(),m.Parser=c,m.parser=c.parse,m.Renderer=r,m.TextRenderer=i,m.Lexer=l,m.lexer=l.lex,m.InlineLexer=u,m.inlineLexer=u.output,m.Slugger=t,m.parse=m,v.exports=m}()}),M=N(function(e){var c=function(c){var u=/\blang(?:uage)?-([\w-]+)\b/i,t=0,T={manual:c.Prism&&c.Prism.manual,disableWorkerMessageHandler:c.Prism&&c.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof R?new R(e.type,T.util.encode(e.content),e.alias):Array.isArray(e)?e.map(T.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(y instanceof R)){if(d&&v!=t.length-1){if(u.lastIndex=b,!(A=u.exec(e)))break;for(var k=A.index+(p?A[1].length:0),w=A.index+A[0].length,x=v,_=b,S=t.length;x"+n.content+""},!c.document)return c.addEventListener&&(T.disableWorkerMessageHandler||c.addEventListener("message",function(e){var t=JSON.parse(e.data),n=t.language,r=t.code,i=t.immediateClose;c.postMessage(T.highlight(r,T.languages[n],n)),i&&c.close()},!1)),T;var e=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return e&&(T.filename=e.src,T.manual||e.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(T.highlightAll):window.setTimeout(T.highlightAll,16):document.addEventListener("DOMContentLoaded",T.highlightAll))),T}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});e.exports&&(e.exports=c),void 0!==t&&(t.Prism=c),c.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.languages.markup.tag.inside["attr-value"].inside.entity=c.languages.markup.entity,c.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(c.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:c.languages[t]},n.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:n}};r["language-"+t]={pattern:/[\s\S]+/,inside:c.languages[t]};var i={};i[e]={pattern:RegExp(/(<__[\s\S]*?>)(?:\s*|[\s\S])*?(?=<\/__>)/.source.replace(/__/g,e),"i"),lookbehind:!0,greedy:!0,inside:r},c.languages.insertBefore("markup","cdata",i)}}),c.languages.xml=c.languages.extend("markup",{}),c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,function(e){var t=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/@[\w-]+/}},url:{pattern:RegExp("url\\((?:"+t.source+"|[^\n\r()]*)\\)","i"),inside:{function:/^url/i,punctuation:/^\(|\)$/}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+t.source+")*?(?=\\s*\\{)"),string:{pattern:t,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:n.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:e.languages.css}},alias:"language-css"}},n.tag))}(c),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{"class-name":[c.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.])\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),c.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,c.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:c.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:c.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:c.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:c.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),c.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.markup.tag.addInlined("script","javascript"),c.languages.js=c.languages.javascript,"undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector&&(self.Prism.fileHighlight=function(e){e=e||document;var l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"};Array.prototype.slice.call(e.querySelectorAll("pre[data-src]")).forEach(function(e){if(!e.hasAttribute("data-src-loaded")){for(var t,n=e.getAttribute("data-src"),r=e,i=/\blang(?:uage)?-([\w-]+)\b/i;r&&!i.test(r.className);)r=r.parentNode;if(r&&(t=(e.className.match(i)||[,""])[1]),!t){var a=(n.match(/\.(\w+)$/)||[,""])[1];t=l[a]||a}var s=document.createElement("code");s.className="language-"+t,e.textContent="",s.textContent="Loading…",e.appendChild(s);var o=new XMLHttpRequest;o.open("GET",n,!0),o.onreadystatechange=function(){4==o.readyState&&(o.status<400&&o.responseText?(s.textContent=o.responseText,c.highlightElement(s),e.setAttribute("data-src-loaded","")):400<=o.status?s.textContent="✖ Error "+o.status+" while fetching file: "+o.statusText:s.textContent="✖ Error: File does not exist or is empty")},o.send(null)}}),c.plugins.toolbar&&c.plugins.toolbar.registerButton("download-file",function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-src")&&t.hasAttribute("data-download-link")){var n=t.getAttribute("data-src"),r=document.createElement("a");return r.textContent=t.getAttribute("data-download-link-label")||"Download",r.setAttribute("download",""),r.href=n,r}})},document.addEventListener("DOMContentLoaded",function(){self.Prism.fileHighlight()}))});function q(e,r){var i=[],a={};return e.forEach(function(e){var t=e.level||1,n=t-1;r?@[\]^`{|}~]/g;function Z(e){return e.toLowerCase()}function B(e){if("string"!=typeof e)return"";var t=e.trim().replace(/[A-Z]+/g,Z).replace(/<[^>\d]+>/g,"").replace(I,"").replace(/\s/g,"-").replace(/-+/g,"-").replace(/^(\d)/,"_$1"),n=H[t];return n=l.call(H,t)?n+1:0,(H[t]=n)&&(t=t+"-"+n),t}function D(e,t){return''+t+''}B.clear=function(){H={}};var U=decodeURIComponent,Y=encodeURIComponent;function G(e){var n={};return(e=e.trim().replace(/^(\?|#|&)/,""))&&e.split("&").forEach(function(e){var t=e.replace(/\+/g," ").split("=");n[t[0]]=t[1]&&U(t[1])}),n}function V(e,t){void 0===t&&(t=[]);var n=[];for(var r in e)-1=m.length);n++){var r=t[n];if("string"==typeof r||r.content&&"string"==typeof r.content){var i=m[f],a=d.tokenStack[i],s="string"==typeof r?r:r.content,o=ne(g,i),l=s.indexOf(o);if(-1"}},video:function(e,t){return{html:'"}},audio:function(e,t){return{html:'"}},code:function(e,t){var n=e.match(/\.(\w+)$/);return"md"===(n=t||n&&n[1])&&(n="markdown"),{url:e,lang:n}}},se=function(i,e){var a=this;this.config=i,this.router=e,this.cacheTree={},this.toc=[],this.cacheTOC={},this.linkTarget=i.externalLinkTarget||"_blank",this.linkRel="_blank"===this.linkTarget?i.externalLinkRel||"noopener":"",this.contentBase=e.getBasePath();var s,t=this._initRenderer(),n=i.markdown||{};s=r(n)?n(j,t):(j.setOptions(d(n,{renderer:d(t,n.renderer)})),j),this._marked=s,this.compile=function(n){var r=!0,e=o(function(e){r=!1;var t="";return n?(t=c(n)?s(n):s.parser(n),t=i.noEmoji?t:function(e){return e.replace(/<(pre|template|code)[^>]*?>[\s\S]+?<\/(pre|template|code)>/g,function(e){return e.replace(/:/g,"__colon__")}).replace(/:(\w+?):/gi,window.emojify||D).replace(/__colon__/g,":")}(t),B.clear(),t):n})(n),t=a.router.parse().file;return r?a.toc=a.cacheTOC[t]:a.cacheTOC[t]=[].concat(a.toc),e}};se.prototype.compileEmbed=function(e,t){var n,r=ie(t),i=r.str,a=r.config;if(t=i,a.include){var s;if(W(e)||(e=K(this.contentBase,X(this.router.getCurrentPath()),e)),a.type&&(s=ae[a.type]))(n=s.call(this,e,t)).type=a.type;else{var o="code";/\.(md|markdown)/.test(e)?o="markdown":/\.mmd/.test(e)?o="mermaid":/\.html?/.test(e)?o="iframe":/\.(mp4|ogg)/.test(e)?o="video":/\.mp3/.test(e)&&(o="audio"),(n=ae[o].call(this,e,t)).type=o}return n.fragment=a.fragment,n}},se.prototype._matchNotCompileLink=function(e){for(var t=this.config.noCompileLinks||[],n=0;n
    '+r+""},t.code=e.code=function(e,t){return void 0===t&&(t=""),e=e.replace(/@DOCSIFY_QM@/g,"`"),'
    '+M.highlight(e,M.languages[t]||M.languages.markup)+"
    "},t.link=e.link=function(e,t,n){void 0===t&&(t="");var r="",i=ie(t),a=i.str,s=i.config;return t=a,W(e)||c._matchNotCompileLink(e)||s.ignore?(r+=0===e.indexOf("mailto:")?"":' target="'+o+'"',r+=0===e.indexOf("mailto:")?"":""!==l?' rel="'+l+'"':""):(e===c.config.homepage&&(e="README"),e=u.toURL(e,null,u.getCurrentPath())),s.target&&(r+=" target="+s.target),s.disabled&&(r+=" disabled",e="javascript:void(0)"),t&&(r+=' title="'+t+'"'),'"+n+""},t.paragraph=e.paragraph=function(e){return/^!>/.test(e)?F("tip",e):/^\?>/.test(e)?F("warn",e):"

    "+e+"

    "},t.image=e.image=function(e,t,n){var r=e,i="",a=ie(t),s=a.str,o=a.config;t=s,o["no-zoom"]&&(i+=" data-no-zoom"),t&&(i+=' title="'+t+'"');var l=o.size;if(l){var c=l.split("x");c[1]?i+="width="+c[0]+" height="+c[1]:i+="width="+c[0]}return W(e)||(r=K(h,X(u.getCurrentPath()),e)),''+n+'"},t.list=e.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+" "+[/
  • /.test(e.split('class="task-list"')[0])?'class="task-list"':"",n&&1"+e+""},t.listitem=e.listitem=function(e){return/^(]*>)/.test(e)?'
  • ":"
  • "+e+"
  • "},e.origin=t,e},se.prototype.sidebar=function(e,t){var n=this.toc,r=this.router.getCurrentPath(),i="";if(e)i=this.compile(e);else{for(var a=0;a{inner}"),this.cacheTree[r]=l}return i},se.prototype.subSidebar=function(e){if(e){var t=this.router.getCurrentPath(),n=this.cacheTree,r=this.toc;r[0]&&r[0].ignoreAllSubs&&r.splice(0),r[0]&&1===r[0].level&&r.shift();for(var i=0;i=t||e.classList.contains("hidden")?A(v,"add","sticky"):A(v,"remove","sticky")}}function ce(e,t,r,n){var i=[];null!=(t=m(t))&&(i=k(t,"a"));var a,s=decodeURI(e.toURL(e.getCurrentPath()));return i.sort(function(e,t){return t.href.length-e.href.length}).forEach(function(e){var t=e.getAttribute("href"),n=r?e.parentNode:e;0!==s.indexOf(t)||a?A(n,"remove","active"):(a=e,A(n,"add","active"))}),n&&(f.title=a?a.title||a.innerText+" - "+oe:oe),a}function ue(e,t){for(var n=0;nthis.end&&e>=this.next}[this.direction]}},{key:"_defaultEase",value:function(e,t,n,r){return(e/=r/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}}]),fe);function fe(){var e=0o){t=t||u;break}t=u}if(t){var h=me[xe(decodeURIComponent(e),t.getAttribute("data-id"))];if(h&&h!==a&&(a&&a.classList.remove("active"),h.classList.add("active"),a=h,!ve&&v.classList.contains("sticky"))){var p=n.clientHeight,d=a.offsetTop+a.clientHeight+40,g=d-0=i.scrollTop&&d<=i.scrollTop+p?i.scrollTop:g?0:d-p;n.scrollTop=f}}}}function xe(e,t){return e+"?id="+t}function _e(e,t){if(t){var n=y("#"+t);n&&function(e){be&&be.stop(),ye=!1,be=new ge({start:window.pageYOffset,end:e.getBoundingClientRect().top+window.pageYOffset,duration:500}).on("tick",function(e){return window.scrollTo(0,e)}).on("done",function(){ye=!0,be=null}).begin()}(n);var r=me[xe(e,t)],i=y(m(".sidebar"),"li.active");i&&i.classList.remove("active"),r&&r.classList.add("active")}}var Se=f.scrollingElement||f.documentElement;var Ae={};function $e(e,i){var s=e.compiler,a=e.raw;void 0===a&&(a="");var t=e.fetch,n=Ae[a];if(n){var r=n.slice();return r.links=n.links,i(r)}var o=s._marked,l=o.lexer(a),c=[],u=o.InlineLexer.rules.link,h=l.links;l.forEach(function(e,a){"paragraph"===e.type&&(e.text=e.text.replace(new RegExp(u.source,"g"),function(e,t,n,r){var i=s.compileEmbed(n,r);return i&&c.push({index:a,embed:i}),e}))});var p=0;!function(e,a){var t,n=e.embedTokens,s=e.compile,o=(e.fetch,0),l=1;if(!n.length)return a({});for(;t=n[o++];){var r=function(i){return function(e){var t;if(e)if("markdown"===i.embed.type)t=s.lexer(e);else if("code"===i.embed.type){if(i.embed.fragment){var n=i.embed.fragment,r=new RegExp("(?:###|\\/\\/\\/)\\s*\\["+n+"\\]([\\s\\S]*)(?:###|\\/\\/\\/)\\s*\\["+n+"\\]");e=((e.match(r)||[])[1]||"").trim()}t=s.lexer("```"+i.embed.lang+"\n"+e.replace(/`/g,"@DOCSIFY_QM@")+"\n```\n")}else"mermaid"===i.embed.type?(t=[{type:"html",text:'
    \n'+e+"\n
    "}]).links={}:(t=[{type:"html",text:e}]).links={};a({token:i,embedToken:t}),++l>=o&&a({})}}(t);t.embed.url?R(t.embed.url).then(r):r(t.embed.html)}}({compile:o,embedTokens:c,fetch:t},function(e){var t=e.embedToken,n=e.token;if(n){var r=n.index+p;d(h,t.links),l=l.slice(0,r).concat(t,l.slice(r+1)),p+=t.length-1}else Ae[a]=l.concat(),l.links=Ae[a].links=h,i(l)})}function Ce(){var e=k(".markdown-section>script").filter(function(e){return!/template/.test(e.type)})[0];if(!e)return!1;var t=e.innerText.trim();if(!t)return!1;setTimeout(function(e){window.__EXECUTE_RESULT__=new Function(t)()},0)}function Ee(e,t,n){return t="function"==typeof n?n(t):"string"==typeof n?function(r,i){var a=[],s=0;return r.replace(P,function(t,e,n){a.push(r.substring(s,n-1)),s=n+=t.length+1,a.push(i&&i[t]||function(e){return("00"+("string"==typeof z[t]?e[z[t]]():z[t](e))).slice(-t.length)})}),s!==r.length&&a.push(r.substring(s)),function(e){for(var t="",n=0,r=e||new Date;n404 - Not found",this._renderTo(".markdown-section",e),this.config.loadSidebar||this._renderSidebar(),!1===this.config.executeScript||void 0===window.Vue||Ce()?this.config.executeScript&&Ce():setTimeout(function(e){var t=window.__EXECUTE_RESULT__;t&&t.$destroy&&t.$destroy(),window.__EXECUTE_RESULT__=(new window.Vue).$mount("#main")},0)}function Le(e){var t=e.config;e.compiler=new se(t,e.router),window.__current_docsify_compiler__=e.compiler;var n=t.el||"#app",r=y("nav")||w("nav"),i=y(n),a="",s=v;if(i){if(t.repo&&(a+=function(e,t){return e?(/\/\//.test(e)||(e="https://github.com/"+e),''):""}(t.repo,t.cornerExternalLinkTarge)),t.coverpage&&(a+=function(){var e=", 100%, 85%";return'
    \x3c!--cover--\x3e
    '}()),t.logo){var o=/^data:image/.test(t.logo),l=/(?:http[s]?:)?\/\//.test(t.logo),c=/^\./.test(t.logo);o||l||c||(t.logo=K(e.router.getBasePath(),t.logo))}a+=function(e){var t=e.name?u(e.name):"",n='';return(g?n+"
    ":"
    "+n)+'
    \x3c!--main--\x3e
    '}(t),e._renderTo(i,a,!0)}else e.rendered=!0;t.mergeNavbar&&g?s=y(".sidebar"):(r.classList.add("app-nav"),t.repo||r.classList.add("no-badge")),t.loadNavbar&&x(s,r),t.themeColor&&(f.head.appendChild(w("div",function(e){return""}(t.themeColor)).firstElementChild),function(n){if(!(window.CSS&&window.CSS.supports&&window.CSS.supports("(--v:red)"))){var e=k("style:not(.inserted),link");[].forEach.call(e,function(e){if("STYLE"===e.nodeName)O(e,n);else if("LINK"===e.nodeName){var t=e.getAttribute("href");if(!/\.css$/.test(t))return;R(t).then(function(e){var t=w("style",e);b.appendChild(t),O(t,n)})}})}}(t.themeColor)),e._updateRender(),A(v,"ready")}var Te={};function Re(e){this.config=e}function Oe(e){var t=location.href.indexOf("#");location.replace(location.href.slice(0,0<=t?t:0)+"#"+e)}Re.prototype.getBasePath=function(){return this.config.basePath},Re.prototype.getFile=function(e,t){void 0===e&&(e=this.getCurrentPath());var n=this.config,r=this.getBasePath(),i="string"==typeof n.ext?n.ext:".md";return e=(e=function(e,t){return new RegExp("\\.("+t.replace(/^\./,"")+"|html)$","g").test(e)?e:/\/$/g.test(e)?e+"README"+t:""+e+t}(e=n.alias?function e(t,n,r){var i=Object.keys(n).filter(function(e){return(Te[e]||(Te[e]=new RegExp("^"+e+"$"))).test(t)&&t!==r})[0];return i?e(t.replace(Te[i],n[i]),n,t):t}(e,n.alias):e,i))==="/README"+i&&n.homepage||e,e=W(e)?e:K(r,e),t&&(e=e.replace(new RegExp("^"+r),"")),e},Re.prototype.onchange=function(e){void 0===e&&(e=h),e()},Re.prototype.getCurrentPath=function(){},Re.prototype.normalize=function(){},Re.prototype.parse=function(){},Re.prototype.toURL=function(e,t,n){var r=n&&"#"===e[0],i=this.parse(te(e));if(i.query=d({},i.query,t),e=(e=i.path+V(i.query)).replace(/\.md(\?)|\.md$/,"$1"),r){var a=n.indexOf("?");e=(0([^<]*?)

    $');if(i){if("color"===i[2])n.style.background=i[1]+(i[3]||"");else{var a=i[1];A(n,"add","has-mask"),W(i[1])||(a=K(this.router.getBasePath(),i[1])),n.style.backgroundImage="url("http://github.com/+a+")",n.style.backgroundSize="cover",n.style.backgroundPosition="center center"}r=r.replace(i[0],"")}this._renderTo(".cover-main",r),le()}else A(n,"remove","show")},Ze._updateRender=function(){!function(e){var t=m(".app-name-link"),n=e.config.nameLink,r=e.route.path;if(t)if(c(e.config.nameLink))t.setAttribute("href",n);else if("object"==typeof n){var i=Object.keys(n).filter(function(e){return-1":">",'"':""","'":"'","/":"/"};return String(e).replace(/[&<>"'/]/g,function(e){return t[e]})}function p(e,t,r,i){void 0===i&&(i=h);var a=e._hooks[t],s=function(t){var e=a[t];if(t>=a.length)i(r);else if("function"==typeof e)if(2===e.length)e(r,function(e){r=e,s(t+1)});else{var n=e(r);r=void 0===n?r:n,s(t+1)}else s(t+1)};s(0)}var g=document.body.clientWidth<=600,a=window.history&&window.history.pushState&&window.history.replaceState&&!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/),n={};function m(e,t){if(void 0===t&&(t=!1),"string"==typeof e){if(void 0!==window.Vue)return y(e);e=t?y(e):n[e]||(n[e]=y(e))}return e}var f=document,v=f.body,b=f.head;function y(e,t){return t?e.querySelector(t):f.querySelector(e)}function k(e,t){return[].slice.call(t?e.querySelectorAll(t):f.querySelectorAll(e))}function w(e,t){return e=f.createElement(e),t&&(e.innerHTML=t),e}function s(e,t){return e.appendChild(t)}function x(e,t){return e.insertBefore(t,e.children[0])}function _(e,t,n){r(t)?window.addEventListener(e,t):e.addEventListener(t,n)}function S(e,t,n){r(t)?window.removeEventListener(e,t):e.removeEventListener(t,n)}function A(e,t,n){e&&e.classList[n?t:"toggle"](n||t)}var $,C,e=Object.freeze({__proto__:null,getNode:m,$:f,body:v,head:b,find:y,findAll:k,create:w,appendTo:s,before:x,on:_,off:S,toggleClass:A,style:function(e){s(b,w("style",e))}});function E(e,t){if(void 0===t&&(t='
      {inner}
    '),!e||!e.length)return"";var n="";return e.forEach(function(e){n+='
  • '+e.title+"
  • ",e.children&&(n+=E(e.children,t))}),t.replace("{inner}",n)}function F(e,t){return'

    '+t.slice(5).trim()+"

    "}function L(e){var t,n=e.loaded,r=e.total,i=e.step;$||function(){var e=w("div");e.classList.add("progress"),s(v,e),$=e}(),t=i?80<(t=parseInt($.style.width||0,10)+i)?80:t:Math.floor(n/r*100),$.style.opacity=1,$.style.width=95<=t?"100%":t+"%",95<=t&&(clearTimeout(C),C=setTimeout(function(e){$.style.opacity=0,$.style.width="0%"},200))}var T={};function R(a,e,t){void 0===e&&(e=!1),void 0===t&&(t={});function n(){s.addEventListener.apply(s,arguments)}var s=new XMLHttpRequest,r=T[a];if(r)return{then:function(e){return e(r.content,r.opt)},abort:h};for(var i in s.open("GET",a),t)l.call(t,i)&&s.setRequestHeader(i,t[i]);return s.send(),{then:function(r,i){if(void 0===i&&(i=h),e){var t=setInterval(function(e){return L({step:Math.floor(5*Math.random()+1)})},500);n("progress",L),n("loadend",function(e){L(e),clearInterval(t)})}n("error",i),n("load",function(e){var t=e.target;if(400<=t.status)i(t);else{var n=T[a]={content:t.response,opt:{updatedAt:s.getResponseHeader("last-modified")}};r(n.content,n.opt)}})},abort:function(e){return 4!==s.readyState&&s.abort()}}}function O(e,t){e.innerHTML=e.innerHTML.replace(/var\(\s*--theme-color.*?\)/g,t)}var P=/([^{]*?)\w(?=\})/g,z={YYYY:"getFullYear",YY:"getYear",MM:function(e){return e.getMonth()+1},DD:"getDate",HH:"getHours",mm:"getMinutes",ss:"getSeconds",fff:"getMilliseconds"};var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function N(e,t){return e(t={exports:{}},t.exports),t.exports}var j=N(function(v,e){!function(){var y={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:/^ {0,3}(`{3,}|~{3,})([^`~\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6}) +([^\n]*?)(?: +#+)? *(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|\\n*|\\n*|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:h,table:h,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html)[^\n]+)*)/,text:/^[^\n]+/};function l(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||m.defaults,this.rules=y.normal,this.options.pedantic?this.rules=y.pedantic:this.options.gfm&&(this.rules=y.gfm)}y._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,y._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,y.def=e(y.def).replace("label",y._label).replace("title",y._title).getRegex(),y.bullet=/(?:[*+-]|\d{1,9}\.)/,y.item=/^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/,y.item=e(y.item,"gm").replace(/bull/g,y.bullet).getRegex(),y.list=e(y.list).replace(/bull/g,y.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+y.def.source+")").getRegex(),y._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",y._comment=//,y.html=e(y.html,"i").replace("comment",y._comment).replace("tag",y._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y.paragraph=e(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} +").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}|~{3,})[^`\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",y._tag).getRegex(),y.blockquote=e(y.blockquote).replace("paragraph",y.paragraph).getRegex(),y.normal=d({},y),y.gfm=d({},y.normal,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/}),y.pedantic=d({},y.normal,{html:e("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",y._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,fences:h,paragraph:e(y.normal._paragraph).replace("hr",y.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),l.rules=y,l.lex=function(e,t){return new l(t).lex(e)},l.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},l.prototype.token=function(e,t){var n,r,i,a,s,o,l,c,u,h,p,d,g,f,m,v;for(e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),1 ?/gm,""),this.token(i,t),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),l={type:"list_start",ordered:f=1<(a=i[2]).length,start:f?+a:"",loose:!1},this.tokens.push(l),n=!(c=[]),g=(i=i[0].match(this.rules.item)).length,p=0;p?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:h,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/,em:/^_([^\s_])_(?!_)|^\*([^\s*<\[])\*(?!\*)|^_([^\s<][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_<][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s<"][\s\S]*?[^\s\*])\*(?!\*|[^\spunctuation])|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:h,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\?@\\[^_{|}~",n.em=e(n.em).replace(/punctuation/g,n._punctuation).getRegex(),n._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,n._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,n._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,n.autolink=e(n.autolink).replace("scheme",n._scheme).replace("email",n._email).getRegex(),n._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,n.tag=e(n.tag).replace("comment",y._comment).replace("attribute",n._attribute).getRegex(),n._label=/(?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,n._href=/<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*/,n._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,n.link=e(n.link).replace("label",n._label).replace("href",n._href).replace("title",n._title).getRegex(),n.reflink=e(n.reflink).replace("label",n._label).getRegex(),n.normal=d({},n),n.pedantic=d({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:e(/^!?\[(label)\]\((.*?)\)/).replace("label",n._label).getRegex(),reflink:e(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",n._label).getRegex()}),n.gfm=d({},n.normal,{escape:e(n.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~+(?=\S)([\s\S]*?\S)~+/,text:/^(`+|[^`])(?:[\s\S]*?(?:(?=[\\/i.test(a[0])&&(this.inLink=!1),!this.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(a[0])?this.inRawBlock=!0:this.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(a[0])&&(this.inRawBlock=!1),e=e.substring(a[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0];else if(a=this.rules.link.exec(e)){var l=g(a[2],"()");if(-1$/,"$1"),o+=this.outputLink(a,{href:u.escapes(r),title:u.escapes(i)}),this.inLink=!1}else if((a=this.rules.reflink.exec(e))||(a=this.rules.nolink.exec(e))){if(e=e.substring(a[0].length),t=(a[2]||a[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=a[0].charAt(0),e=a[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(a,t),this.inLink=!1}else if(a=this.rules.strong.exec(e))e=e.substring(a[0].length),o+=this.renderer.strong(this.output(a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.em.exec(e))e=e.substring(a[0].length),o+=this.renderer.em(this.output(a[6]||a[5]||a[4]||a[3]||a[2]||a[1]));else if(a=this.rules.code.exec(e))e=e.substring(a[0].length),o+=this.renderer.codespan(k(a[2].trim(),!0));else if(a=this.rules.br.exec(e))e=e.substring(a[0].length),o+=this.renderer.br();else if(a=this.rules.del.exec(e))e=e.substring(a[0].length),o+=this.renderer.del(this.output(a[1]));else if(a=this.rules.autolink.exec(e))e=e.substring(a[0].length),r="@"===a[2]?"mailto:"+(n=k(this.mangle(a[1]))):n=k(a[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(a=this.rules.url.exec(e))){if(a=this.rules.text.exec(e))e=e.substring(a[0].length),this.inRawBlock?o+=this.renderer.text(this.options.sanitize?this.options.sanitizer?this.options.sanitizer(a[0]):k(a[0]):a[0]):o+=this.renderer.text(k(this.smartypants(a[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else{if("@"===a[2])r="mailto:"+(n=k(a[0]));else{for(;s=a[0],a[0]=this.rules._backpedal.exec(a[0])[0],s!==a[0];);n=k(a[0]),r="www."===a[1]?"http://"+n:n}e=e.substring(a[0].length),o+=this.renderer.link(r,null,n)}return o},u.escapes=function(e){return e?e.replace(u.rules._escapes,"$1"):e},u.prototype.outputLink=function(e,t){var n=t.href,r=t.title?k(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,k(e[1]))},u.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},u.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;i'+(n?e:k(e,!0))+"\n":"
    "+(n?e:k(e,!0))+"
    "},r.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n,r){return this.options.headerIds?"'+e+"\n":""+e+"\n"},r.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},r.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},r.prototype.checkbox=function(e){return" "},r.prototype.paragraph=function(e){return"

    "+e+"

    \n"},r.prototype.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},r.prototype.tablerow=function(e){return"\n"+e+"\n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"},r.prototype.strong=function(e){return""+e+""},r.prototype.em=function(e){return""+e+""},r.prototype.codespan=function(e){return""+e+""},r.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},r.prototype.del=function(e){return""+e+""},r.prototype.link=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"},r.prototype.image=function(e,t,n){if(null===(e=a(this.options.sanitize,this.options.baseUrl,e)))return n;var r=''+n+'":">"},r.prototype.text=function(e){return e},i.prototype.strong=i.prototype.em=i.prototype.codespan=i.prototype.del=i.prototype.text=function(e){return e},i.prototype.link=i.prototype.image=function(e,t,n){return""+n},i.prototype.br=function(){return""},c.parse=function(e,t){return new c(t).parse(e)},c.prototype.parse=function(e){this.inline=new u(e.links,this.options),this.inlineText=new u(e.links,d({},this.options,{renderer:new i})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},c.prototype.next=function(){return this.token=this.tokens.pop(),this.token},c.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},c.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},c.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,p(this.inlineText.output(this.token.text)),this.slugger);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i="",a="";for(n="",e=0;e?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t))for(var n=t;this.seen[n]++,t=n+"-"+this.seen[n],this.seen.hasOwnProperty(t););return this.seen[t]=0,t},k.escapeTest=/[&<>"']/,k.escapeReplace=/[&<>"']/g,k.replacements={"&":"&","<":"<",">":">",'"':""","'":"'"},k.escapeTestNoEncode=/[<>"']|&(?!#?\w+;)/,k.escapeReplaceNoEncode=/[<>"']|&(?!#?\w+;)/g;var s={},o=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function h(){}function d(e){for(var t,n,r=arguments,i=1;it)n.splice(t);else for(;n.lengthAn error occurred:

    "+k(e.message+"",!0)+"
    ";throw e}}h.exec=h,m.options=m.setOptions=function(e){return d(m.defaults,e),m},m.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,xhtml:!1}},m.defaults=m.getDefaults(),m.Parser=c,m.parser=c.parse,m.Renderer=r,m.TextRenderer=i,m.Lexer=l,m.lexer=l.lex,m.InlineLexer=u,m.inlineLexer=u.output,m.Slugger=t,m.parse=m,v.exports=m}()}),M=N(function(e){var c=function(c){var u=/\blang(?:uage)?-([\w-]+)\b/i,t=0,T={manual:c.Prism&&c.Prism.manual,disableWorkerMessageHandler:c.Prism&&c.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof R?new R(e.type,T.util.encode(e.content),e.alias):Array.isArray(e)?e.map(T.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(y instanceof R)){if(d&&v!=t.length-1){if(u.lastIndex=b,!(A=u.exec(e)))break;for(var k=A.index+(p?A[1].length:0),w=A.index+A[0].length,x=v,_=b,S=t.length;x"+n.content+""},!c.document)return c.addEventListener&&(T.disableWorkerMessageHandler||c.addEventListener("message",function(e){var t=JSON.parse(e.data),n=t.language,r=t.code,i=t.immediateClose;c.postMessage(T.highlight(r,T.languages[n],n)),i&&c.close()},!1)),T;var e=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return e&&(T.filename=e.src,T.manual||e.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(T.highlightAll):window.setTimeout(T.highlightAll,16):document.addEventListener("DOMContentLoaded",T.highlightAll))),T}("undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{});e.exports&&(e.exports=c),void 0!==t&&(t.Prism=c),c.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.languages.markup.tag.inside["attr-value"].inside.entity=c.languages.markup.entity,c.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(c.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:c.languages[t]},n.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:n}};r["language-"+t]={pattern:/[\s\S]+/,inside:c.languages[t]};var i={};i[e]={pattern:RegExp(/(<__[\s\S]*?>)(?:\s*|[\s\S])*?(?=<\/__>)/.source.replace(/__/g,e),"i"),lookbehind:!0,greedy:!0,inside:r},c.languages.insertBefore("markup","cdata",i)}}),c.languages.xml=c.languages.extend("markup",{}),c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,function(e){var t=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/@[\w-]+/}},url:{pattern:RegExp("url\\((?:"+t.source+"|[^\n\r()]*)\\)","i"),inside:{function:/^url/i,punctuation:/^\(|\)$/}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+t.source+")*?(?=\\s*\\{)"),string:{pattern:t,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:n.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:e.languages.css}},alias:"language-css"}},n.tag))}(c),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{"class-name":[c.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.])\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/-[-=]?|\+[+=]?|!=?=?|<>?>?=?|=(?:==?|>)?|&[&=]?|\|[|=]?|\*\*?=?|\/=?|~|\^=?|%=?|\?|\.{3}/}),c.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,c.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:c.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:c.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:c.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:c.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),c.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.markup.tag.addInlined("script","javascript"),c.languages.js=c.languages.javascript,"undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector&&(self.Prism.fileHighlight=function(e){e=e||document;var l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"};Array.prototype.slice.call(e.querySelectorAll("pre[data-src]")).forEach(function(e){if(!e.hasAttribute("data-src-loaded")){for(var t,n=e.getAttribute("data-src"),r=e,i=/\blang(?:uage)?-([\w-]+)\b/i;r&&!i.test(r.className);)r=r.parentNode;if(r&&(t=(e.className.match(i)||[,""])[1]),!t){var a=(n.match(/\.(\w+)$/)||[,""])[1];t=l[a]||a}var s=document.createElement("code");s.className="language-"+t,e.textContent="",s.textContent="Loading…",e.appendChild(s);var o=new XMLHttpRequest;o.open("GET",n,!0),o.onreadystatechange=function(){4==o.readyState&&(o.status<400&&o.responseText?(s.textContent=o.responseText,c.highlightElement(s),e.setAttribute("data-src-loaded","")):400<=o.status?s.textContent="✖ Error "+o.status+" while fetching file: "+o.statusText:s.textContent="✖ Error: File does not exist or is empty")},o.send(null)}}),c.plugins.toolbar&&c.plugins.toolbar.registerButton("download-file",function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-src")&&t.hasAttribute("data-download-link")){var n=t.getAttribute("data-src"),r=document.createElement("a");return r.textContent=t.getAttribute("data-download-link-label")||"Download",r.setAttribute("download",""),r.href=n,r}})},document.addEventListener("DOMContentLoaded",function(){self.Prism.fileHighlight()}))});function q(e,r){var i=[],a={};return e.forEach(function(e){var t=e.level||1,n=t-1;r?@[\]^`{|}~]/g;function Z(e){return e.toLowerCase()}function B(e){if("string"!=typeof e)return"";var t=e.trim().replace(/[A-Z]+/g,Z).replace(/<[^>\d]+>/g,"").replace(I,"").replace(/\s/g,"-").replace(/-+/g,"-").replace(/^(\d)/,"_$1"),n=H[t];return n=l.call(H,t)?n+1:0,(H[t]=n)&&(t=t+"-"+n),t}function D(e,t){return''+t+''}B.clear=function(){H={}};var U=decodeURIComponent,Y=encodeURIComponent;function G(e){var n={};return(e=e.trim().replace(/^(\?|#|&)/,""))&&e.split("&").forEach(function(e){var t=e.replace(/\+/g," ").split("=");n[t[0]]=t[1]&&U(t[1])}),n}function V(e,t){void 0===t&&(t=[]);var n=[];for(var r in e)-1=m.length);n++){var r=t[n];if("string"==typeof r||r.content&&"string"==typeof r.content){var i=m[f],a=d.tokenStack[i],s="string"==typeof r?r:r.content,o=ne(g,i),l=s.indexOf(o);if(-1"}},video:function(e,t){return{html:'"}},audio:function(e,t){return{html:'"}},code:function(e,t){var n=e.match(/\.(\w+)$/);return"md"===(n=t||n&&n[1])&&(n="markdown"),{url:e,lang:n}}},se=function(i,e){var a=this;this.config=i,this.router=e,this.cacheTree={},this.toc=[],this.cacheTOC={},this.linkTarget=i.externalLinkTarget||"_blank",this.linkRel="_blank"===this.linkTarget?i.externalLinkRel||"noopener":"",this.contentBase=e.getBasePath();var s,t=this._initRenderer(),n=i.markdown||{};s=r(n)?n(j,t):(j.setOptions(d(n,{renderer:d(t,n.renderer)})),j),this._marked=s,this.compile=function(n){var r=!0,e=o(function(e){r=!1;var t="";return n?(t=c(n)?s(n):s.parser(n),t=i.noEmoji?t:function(e){return e.replace(/<(pre|template|code)[^>]*?>[\s\S]+?<\/(pre|template|code)>/g,function(e){return e.replace(/:/g,"__colon__")}).replace(/:(\w+?):/gi,window.emojify||D).replace(/__colon__/g,":")}(t),B.clear(),t):n})(n),t=a.router.parse().file;return r?a.toc=a.cacheTOC[t]:a.cacheTOC[t]=[].concat(a.toc),e}};se.prototype.compileEmbed=function(e,t){var n,r=ie(t),i=r.str,a=r.config;if(t=i,a.include){var s;if(W(e)||(e=K(this.contentBase,X(this.router.getCurrentPath()),e)),a.type&&(s=ae[a.type]))(n=s.call(this,e,t)).type=a.type;else{var o="code";/\.(md|markdown)/.test(e)?o="markdown":/\.mmd/.test(e)?o="mermaid":/\.html?/.test(e)?o="iframe":/\.(mp4|ogg)/.test(e)?o="video":/\.mp3/.test(e)&&(o="audio"),(n=ae[o].call(this,e,t)).type=o}return n.fragment=a.fragment,n}},se.prototype._matchNotCompileLink=function(e){for(var t=this.config.noCompileLinks||[],n=0;n
    '+r+""},t.code=e.code=function(e,t){return void 0===t&&(t=""),e=e.replace(/@DOCSIFY_QM@/g,"`"),'
    '+M.highlight(e,M.languages[t]||M.languages.markup)+"
    "},t.link=e.link=function(e,t,n){void 0===t&&(t="");var r="",i=ie(t),a=i.str,s=i.config;return t=a,W(e)||c._matchNotCompileLink(e)||s.ignore?(r+=0===e.indexOf("mailto:")?"":' target="'+o+'"',r+=0===e.indexOf("mailto:")?"":""!==l?' rel="'+l+'"':""):(e===c.config.homepage&&(e="README"),e=u.toURL(e,null,u.getCurrentPath())),s.target&&(r+=" target="+s.target),s.disabled&&(r+=" disabled",e="javascript:void(0)"),t&&(r+=' title="'+t+'"'),'"+n+""},t.paragraph=e.paragraph=function(e){return/^!>/.test(e)?F("tip",e):/^\?>/.test(e)?F("warn",e):"

    "+e+"

    "},t.image=e.image=function(e,t,n){var r=e,i="",a=ie(t),s=a.str,o=a.config;t=s,o["no-zoom"]&&(i+=" data-no-zoom"),t&&(i+=' title="'+t+'"');var l=o.size;if(l){var c=l.split("x");c[1]?i+="width="+c[0]+" height="+c[1]:i+="width="+c[0]}return W(e)||(r=K(h,X(u.getCurrentPath()),e)),''+n+'"},t.list=e.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+" "+[/
  • /.test(e.split('class="task-list"')[0])?'class="task-list"':"",n&&1"+e+""},t.listitem=e.listitem=function(e){return/^(]*>)/.test(e)?'
  • ":"
  • "+e+"
  • "},e.origin=t,e},se.prototype.sidebar=function(e,t){var n=this.toc,r=this.router.getCurrentPath(),i="";if(e)i=this.compile(e);else{for(var a=0;a{inner}"),this.cacheTree[r]=l}return i},se.prototype.subSidebar=function(e){if(e){var t=this.router.getCurrentPath(),n=this.cacheTree,r=this.toc;r[0]&&r[0].ignoreAllSubs&&r.splice(0),r[0]&&1===r[0].level&&r.shift();for(var i=0;i=t||e.classList.contains("hidden")?A(v,"add","sticky"):A(v,"remove","sticky")}}function ce(e,t,r,n){var i=[];null!=(t=m(t))&&(i=k(t,"a"));var a,s=decodeURI(e.toURL(e.getCurrentPath()));return i.sort(function(e,t){return t.href.length-e.href.length}).forEach(function(e){var t=e.getAttribute("href"),n=r?e.parentNode:e;0!==s.indexOf(t)||a?A(n,"remove","active"):(a=e,A(n,"add","active"))}),n&&(f.title=a?a.title||a.innerText+" - "+oe:oe),a}function ue(e,t){for(var n=0;nthis.end&&e>=this.next}[this.direction]}},{key:"_defaultEase",value:function(e,t,n,r){return(e/=r/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}}]),fe);function fe(){var e=0o){t=t||u;break}t=u}if(t){var h=me[xe(decodeURIComponent(e),t.getAttribute("data-id"))];if(h&&h!==a&&(a&&a.classList.remove("active"),h.classList.add("active"),a=h,!ve&&v.classList.contains("sticky"))){var p=n.clientHeight,d=a.offsetTop+a.clientHeight+40,g=d-0=i.scrollTop&&d<=i.scrollTop+p?i.scrollTop:g?0:d-p;n.scrollTop=f}}}}function xe(e,t){return e+"?id="+t}function _e(e,t){if(t){var n=y("#"+t);n&&function(e){be&&be.stop(),ye=!1,be=new ge({start:window.pageYOffset,end:e.getBoundingClientRect().top+window.pageYOffset,duration:500}).on("tick",function(e){return window.scrollTo(0,e)}).on("done",function(){ye=!0,be=null}).begin()}(n);var r=me[xe(e,t)],i=y(m(".sidebar"),"li.active");i&&i.classList.remove("active"),r&&r.classList.add("active")}}var Se=f.scrollingElement||f.documentElement;var Ae={};function $e(e,i){var s=e.compiler,a=e.raw;void 0===a&&(a="");var t=e.fetch,n=Ae[a];if(n){var r=n.slice();return r.links=n.links,i(r)}var o=s._marked,l=o.lexer(a),c=[],u=o.InlineLexer.rules.link,h=l.links;l.forEach(function(e,a){"paragraph"===e.type&&(e.text=e.text.replace(new RegExp(u.source,"g"),function(e,t,n,r){var i=s.compileEmbed(n,r);return i&&c.push({index:a,embed:i}),e}))});var p=0;!function(e,a){var t,n=e.embedTokens,s=e.compile,o=(e.fetch,0),l=1;if(!n.length)return a({});for(;t=n[o++];){var r=function(i){return function(e){var t;if(e)if("markdown"===i.embed.type)t=s.lexer(e);else if("code"===i.embed.type){if(i.embed.fragment){var n=i.embed.fragment,r=new RegExp("(?:###|\\/\\/\\/)\\s*\\["+n+"\\]([\\s\\S]*)(?:###|\\/\\/\\/)\\s*\\["+n+"\\]");e=((e.match(r)||[])[1]||"").trim()}t=s.lexer("```"+i.embed.lang+"\n"+e.replace(/`/g,"@DOCSIFY_QM@")+"\n```\n")}else"mermaid"===i.embed.type?(t=[{type:"html",text:'
    \n'+e+"\n
    "}]).links={}:(t=[{type:"html",text:e}]).links={};a({token:i,embedToken:t}),++l>=o&&a({})}}(t);t.embed.url?R(t.embed.url).then(r):r(t.embed.html)}}({compile:o,embedTokens:c,fetch:t},function(e){var t=e.embedToken,n=e.token;if(n){var r=n.index+p;d(h,t.links),l=l.slice(0,r).concat(t,l.slice(r+1)),p+=t.length-1}else Ae[a]=l.concat(),l.links=Ae[a].links=h,i(l)})}function Ce(){var e=k(".markdown-section>script").filter(function(e){return!/template/.test(e.type)})[0];if(!e)return!1;var t=e.innerText.trim();if(!t)return!1;setTimeout(function(e){window.__EXECUTE_RESULT__=new Function(t)()},0)}function Ee(e,t,n){return t="function"==typeof n?n(t):"string"==typeof n?function(r,i){var a=[],s=0;return r.replace(P,function(t,e,n){a.push(r.substring(s,n-1)),s=n+=t.length+1,a.push(i&&i[t]||function(e){return("00"+("string"==typeof z[t]?e[z[t]]():z[t](e))).slice(-t.length)})}),s!==r.length&&a.push(r.substring(s)),function(e){for(var t="",n=0,r=e||new Date;n404 - Not found",this._renderTo(".markdown-section",e),this.config.loadSidebar||this._renderSidebar(),!1===this.config.executeScript||void 0===window.Vue||Ce()?this.config.executeScript&&Ce():setTimeout(function(e){var t=window.__EXECUTE_RESULT__;t&&t.$destroy&&t.$destroy(),window.__EXECUTE_RESULT__=(new window.Vue).$mount("#main")},0)}function Le(e){var t=e.config;e.compiler=new se(t,e.router),window.__current_docsify_compiler__=e.compiler;var n=t.el||"#app",r=y("nav")||w("nav"),i=y(n),a="",s=v;if(i){if(t.repo&&(a+=function(e,t){return e?(/\/\//.test(e)||(e="https://github.com/"+e),''):""}(t.repo,t.cornerExternalLinkTarge)),t.coverpage&&(a+=function(){var e=", 100%, 85%";return'
    \x3c!--cover--\x3e
    '}()),t.logo){var o=/^data:image/.test(t.logo),l=/(?:http[s]?:)?\/\//.test(t.logo),c=/^\./.test(t.logo);o||l||c||(t.logo=K(e.router.getBasePath(),t.logo))}a+=function(e){var t=e.name?u(e.name):"",n='';return(g?n+"
    ":"
    "+n)+'
    \x3c!--main--\x3e
    '}(t),e._renderTo(i,a,!0)}else e.rendered=!0;t.mergeNavbar&&g?s=y(".sidebar"):(r.classList.add("app-nav"),t.repo||r.classList.add("no-badge")),t.loadNavbar&&x(s,r),t.themeColor&&(f.head.appendChild(w("div",function(e){return""}(t.themeColor)).firstElementChild),function(n){if(!(window.CSS&&window.CSS.supports&&window.CSS.supports("(--v:red)"))){var e=k("style:not(.inserted),link");[].forEach.call(e,function(e){if("STYLE"===e.nodeName)O(e,n);else if("LINK"===e.nodeName){var t=e.getAttribute("href");if(!/\.css$/.test(t))return;R(t).then(function(e){var t=w("style",e);b.appendChild(t),O(t,n)})}})}}(t.themeColor)),e._updateRender(),A(v,"ready")}var Te={};function Re(e){this.config=e}function Oe(e){var t=location.href.indexOf("#");location.replace(location.href.slice(0,0<=t?t:0)+"#"+e)}Re.prototype.getBasePath=function(){return this.config.basePath},Re.prototype.getFile=function(e,t){void 0===e&&(e=this.getCurrentPath());var n=this.config,r=this.getBasePath(),i="string"==typeof n.ext?n.ext:".md";return e=(e=function(e,t){return new RegExp("\\.("+t.replace(/^\./,"")+"|html)$","g").test(e)?e:/\/$/g.test(e)?e+"README"+t:""+e+t}(e=n.alias?function e(t,n,r){var i=Object.keys(n).filter(function(e){return(Te[e]||(Te[e]=new RegExp("^"+e+"$"))).test(t)&&t!==r})[0];return i?e(t.replace(Te[i],n[i]),n,t):t}(e,n.alias):e,i))==="/README"+i&&n.homepage||e,e=W(e)?e:K(r,e),t&&(e=e.replace(new RegExp("^"+r),"")),e},Re.prototype.onchange=function(e){void 0===e&&(e=h),e()},Re.prototype.getCurrentPath=function(){},Re.prototype.normalize=function(){},Re.prototype.parse=function(){},Re.prototype.toURL=function(e,t,n){var r=n&&"#"===e[0],i=this.parse(te(e));if(i.query=d({},i.query,t),e=(e=i.path+V(i.query)).replace(/\.md(\?)|\.md$/,"$1"),r){var a=n.indexOf("?");e=(0([^<]*?)

    $');if(i){if("color"===i[2])n.style.background=i[1]+(i[3]||"");else{var a=i[1];A(n,"add","has-mask"),W(i[1])||(a=K(this.router.getBasePath(),i[1])),n.style.backgroundImage="url("http://github.com/+a+")",n.style.backgroundSize="cover",n.style.backgroundPosition="center center"}r=r.replace(i[0],"")}this._renderTo(".cover-main",r),le()}else A(n,"remove","show")},Ze._updateRender=function(){!function(e){var t=m(".app-name-link"),n=e.config.nameLink,r=e.route.path;if(t)if(c(e.config.nameLink))t.setAttribute("href",n);else if("object"==typeof n){var i=Object.keys(n).filter(function(e){return-1': '>', + '"': '"', + '\'': ''', + '/': '/' + }; + + return String(string).replace(/[&<>"'/]/g, function (s) { return entityMap[s]; }) + } + + function getAllPaths(router) { + var paths = []; + + Docsify.dom.findAll('.sidebar-nav a:not(.section-link):not([data-nosearch])').forEach(function (node) { + var href = node.href; + var originHref = node.getAttribute('href'); + var path = router.parse(href).path; + + if ( + path && + paths.indexOf(path) === -1 && + !Docsify.util.isAbsolutePath(originHref) + ) { + paths.push(path); + } + }); + + return paths + } + + function saveData(maxAge, expireKey, indexKey) { + localStorage.setItem(expireKey, Date.now() + maxAge); + localStorage.setItem(indexKey, JSON.stringify(INDEXS)); + } + + function genIndex(path, content, router, depth) { + if ( content === void 0 ) content = ''; + + var tokens = window.marked.lexer(content); + var slugify = window.Docsify.slugify; + var index = {}; + var slug; + + tokens.forEach(function (token) { + if (token.type === 'heading' && token.depth <= depth) { + slug = router.toURL(path, {id: slugify(token.text)}); + index[slug] = {slug: slug, title: token.text, body: ''}; + } else { + if (!slug) { + return + } + if (!index[slug]) { + index[slug] = {slug: slug, title: '', body: ''}; + } else if (index[slug].body) { + index[slug].body += '\n' + (token.text || ''); + } else { + index[slug].body = token.text; + } + } + }); + slugify.clear(); + return index + } + + /** + * @param {String} query + * @returns {Array} + */ + function search(query) { + var matchingResults = []; + var data = []; + Object.keys(INDEXS).forEach(function (key) { + data = data.concat(Object.keys(INDEXS[key]).map(function (page) { return INDEXS[key][page]; })); + }); + + query = query.trim(); + var keywords = query.split(/[\s\-,\\/]+/); + if (keywords.length !== 1) { + keywords = [].concat(query, keywords); + } + + var loop = function ( i ) { + var post = data[i]; + var matchesScore = 0; + var resultStr = ''; + var postTitle = post.title && post.title.trim(); + var postContent = post.body && post.body.trim(); + var postUrl = post.slug || ''; + + if (postTitle) { + keywords.forEach( function (keyword) { + // From https://github.com/sindresorhus/escape-string-regexp + var regEx = new RegExp( + keyword.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'), + 'gi' + ); + var indexTitle = -1; + var indexContent = -1; + + indexTitle = postTitle ? postTitle.search(regEx) : -1; + indexContent = postContent ? postContent.search(regEx) : -1; + + if (indexTitle >= 0 || indexContent >= 0) { + matchesScore += indexTitle >= 0 ? 3 : indexContent >= 0 ? 2 : 0; + if (indexContent < 0) { + indexContent = 0; + } + + var start = 0; + var end = 0; + + start = indexContent < 11 ? 0 : indexContent - 10; + end = start === 0 ? 70 : indexContent + keyword.length + 60; + + if (end > postContent.length) { + end = postContent.length; + } + + var matchContent = + '...' + + escapeHtml(postContent) + .substring(start, end) + .replace(regEx, ("" + keyword + "")) + + '...'; + + resultStr += matchContent; + } + }); + + if (matchesScore > 0) { + var matchingPost = { + title: escapeHtml(postTitle), + content: postContent ? resultStr : '', + url: postUrl, + score: matchesScore + }; + + matchingResults.push(matchingPost); + } + } + }; + + for (var i = 0; i < data.length; i++) loop( i ); + + return matchingResults.sort(function (r1, r2) { return r2.score - r1.score; }); + } + + function init(config, vm) { + var isAuto = config.paths === 'auto'; + + var expireKey = resolveExpireKey(config.namespace); + var indexKey = resolveIndexKey(config.namespace); + + var isExpired = localStorage.getItem(expireKey) < Date.now(); + + INDEXS = JSON.parse(localStorage.getItem(indexKey)); + + if (isExpired) { + INDEXS = {}; + } else if (!isAuto) { + return + } + + var paths = isAuto ? getAllPaths(vm.router) : config.paths; + var len = paths.length; + var count = 0; + + paths.forEach(function (path) { + if (INDEXS[path]) { + return count++ + } + + Docsify + .get(vm.router.getFile(path), false, vm.config.requestHeaders) + .then(function (result) { + INDEXS[path] = genIndex(path, result, vm.router, config.depth); + len === ++count && saveData(config.maxAge, expireKey, indexKey); + }); + }); + } + + var NO_DATA_TEXT = ''; + var options; + + function style() { + var code = "\n.sidebar {\n padding-top: 0;\n}\n\n.search {\n margin-bottom: 20px;\n padding: 6px;\n border-bottom: 1px solid #eee;\n}\n\n.search .input-wrap {\n display: flex;\n align-items: center;\n}\n\n.search .results-panel {\n display: none;\n}\n\n.search .results-panel.show {\n display: block;\n}\n\n.search input {\n outline: none;\n border: none;\n width: 100%;\n padding: 0 7px;\n line-height: 36px;\n font-size: 14px;\n border: 1px solid transparent;\n}\n\n.search input:focus {\n box-shadow: 0 0 5px var(--theme-color, #42b983);\n border: 1px solid var(--theme-color, #42b983);\n}\n\n.search input::-webkit-search-decoration,\n.search input::-webkit-search-cancel-button,\n.search input {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.search .clear-button {\n width: 36px;\n text-align: right;\n display: none;\n}\n\n.search .clear-button.show {\n display: block;\n}\n\n.search .clear-button svg {\n transform: scale(.5);\n}\n\n.search h2 {\n font-size: 17px;\n margin: 10px 0;\n}\n\n.search a {\n text-decoration: none;\n color: inherit;\n}\n\n.search .matching-post {\n border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n border-bottom: 0;\n}\n\n.search p {\n font-size: 14px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n text-align: center;\n}\n\n.app-name.hide, .sidebar-nav.hide {\n display: none;\n}"; + + Docsify.dom.style(code); + } + + function tpl(defaultValue) { + if ( defaultValue === void 0 ) defaultValue = ''; + + var html = + "
    \n \n
    \n \n \n \n \n \n
    \n
    \n
    \n "; + var el = Docsify.dom.create('div', html); + var aside = Docsify.dom.find('aside'); + + Docsify.dom.toggleClass(el, 'search'); + Docsify.dom.before(aside, el); + } + + function doSearch(value) { + var $search = Docsify.dom.find('div.search'); + var $panel = Docsify.dom.find($search, '.results-panel'); + var $clearBtn = Docsify.dom.find($search, '.clear-button'); + var $sidebarNav = Docsify.dom.find('.sidebar-nav'); + var $appName = Docsify.dom.find('.app-name'); + + if (!value) { + $panel.classList.remove('show'); + $clearBtn.classList.remove('show'); + $panel.innerHTML = ''; + + if (options.hideOtherSidebarContent) { + $sidebarNav.classList.remove('hide'); + $appName.classList.remove('hide'); + } + return + } + var matchs = search(value); + + var html = ''; + matchs.forEach(function (post) { + html += "
    "; + }); + + $panel.classList.add('show'); + $clearBtn.classList.add('show'); + $panel.innerHTML = html || ("

    " + NO_DATA_TEXT + "

    "); + if (options.hideOtherSidebarContent) { + $sidebarNav.classList.add('hide'); + $appName.classList.add('hide'); + } + } + + function bindEvents() { + var $search = Docsify.dom.find('div.search'); + var $input = Docsify.dom.find($search, 'input'); + var $inputWrap = Docsify.dom.find($search, '.input-wrap'); + + var timeId; + // Prevent to Fold sidebar + Docsify.dom.on( + $search, + 'click', + function (e) { return e.target.tagName !== 'A' && e.stopPropagation(); } + ); + Docsify.dom.on($input, 'input', function (e) { + clearTimeout(timeId); + timeId = setTimeout(function (_) { return doSearch(e.target.value.trim()); }, 100); + }); + Docsify.dom.on($inputWrap, 'click', function (e) { + // Click input outside + if (e.target.tagName !== 'INPUT') { + $input.value = ''; + doSearch(); + } + }); + } + + function updatePlaceholder(text, path) { + var $input = Docsify.dom.getNode('.search input[type="search"]'); + + if (!$input) { + return + } + if (typeof text === 'string') { + $input.placeholder = text; + } else { + var match = Object.keys(text).filter(function (key) { return path.indexOf(key) > -1; })[0]; + $input.placeholder = text[match]; + } + } + + function updateNoData(text, path) { + if (typeof text === 'string') { + NO_DATA_TEXT = text; + } else { + var match = Object.keys(text).filter(function (key) { return path.indexOf(key) > -1; })[0]; + NO_DATA_TEXT = text[match]; + } + } + + function updateOptions(opts) { + options = opts; + } + + function init$1(opts, vm) { + var keywords = vm.router.parse().query.s; + + updateOptions(opts); + style(); + tpl(keywords); + bindEvents(); + keywords && setTimeout(function (_) { return doSearch(keywords); }, 500); + } + + function update(opts, vm) { + updateOptions(opts); + updatePlaceholder(opts.placeholder, vm.route.path); + updateNoData(opts.noData, vm.route.path); + } + + var CONFIG = { + placeholder: 'Type to search', + noData: 'No Results!', + paths: 'auto', + depth: 2, + maxAge: 86400000, // 1 day + hideOtherSidebarContent: false, + namespace: undefined + }; + + var install = function (hook, vm) { + var util = Docsify.util; + var opts = vm.config.search || CONFIG; + + if (Array.isArray(opts)) { + CONFIG.paths = opts; + } else if (typeof opts === 'object') { + CONFIG.paths = Array.isArray(opts.paths) ? opts.paths : 'auto'; + CONFIG.maxAge = util.isPrimitive(opts.maxAge) ? opts.maxAge : CONFIG.maxAge; + CONFIG.placeholder = opts.placeholder || CONFIG.placeholder; + CONFIG.noData = opts.noData || CONFIG.noData; + CONFIG.depth = opts.depth || CONFIG.depth; + CONFIG.hideOtherSidebarContent = opts.hideOtherSidebarContent || CONFIG.hideOtherSidebarContent; + CONFIG.namespace = opts.namespace || CONFIG.namespace; + } + + var isAuto = CONFIG.paths === 'auto'; + + hook.mounted(function (_) { + init$1(CONFIG, vm); + !isAuto && init(CONFIG, vm); + }); + hook.doneEach(function (_) { + update(CONFIG, vm); + isAuto && init(CONFIG, vm); + }); + }; + + $docsify.plugins = [].concat(install, $docsify.plugins); + +}()); \ No newline at end of file diff --git a/static/searchgrid2.jpg b/static/searchgrid2.jpg new file mode 100644 index 0000000..69db95b Binary files /dev/null and b/static/searchgrid2.jpg differ diff --git a/static/sort_list_1.jpg b/static/sort_list_1.jpg new file mode 100644 index 0000000..ddc64b5 Binary files /dev/null and b/static/sort_list_1.jpg differ diff --git a/static/sychronize.png b/static/sychronize.png index 978fcc6..2e65e74 100755 Binary files a/static/sychronize.png and b/static/sychronize.png differ diff --git a/static/thread.png b/static/thread.png new file mode 100644 index 0000000..6b6e56a Binary files /dev/null and b/static/thread.png differ diff --git a/static/tree.png b/static/tree.png new file mode 100644 index 0000000..fb2cfb5 Binary files /dev/null and b/static/tree.png differ diff --git a/static/up-70a9e95e6c1208288334341bdb54bd59c17.png b/static/up-70a9e95e6c1208288334341bdb54bd59c17.png new file mode 100644 index 0000000..9d65b32 Binary files /dev/null and b/static/up-70a9e95e6c1208288334341bdb54bd59c17.png differ diff --git a/static/v2-310cc7857eabd42e324c109b5ca85b1d_b.png b/static/v2-310cc7857eabd42e324c109b5ca85b1d_b.png new file mode 100644 index 0000000..a8ba3ae Binary files /dev/null and b/static/v2-310cc7857eabd42e324c109b5ca85b1d_b.png differ diff --git a/static/v2-880b5e0866906160b663827f961d8360_b.jpg b/static/v2-880b5e0866906160b663827f961d8360_b.jpg new file mode 100644 index 0000000..399766e Binary files /dev/null and b/static/v2-880b5e0866906160b663827f961d8360_b.jpg differ diff --git a/static/v2-90179fa933c0a389ffa6ac04e244a58f_b.jpg b/static/v2-90179fa933c0a389ffa6ac04e244a58f_b.jpg new file mode 100644 index 0000000..d5e6167 Binary files /dev/null and b/static/v2-90179fa933c0a389ffa6ac04e244a58f_b.jpg differ diff --git a/static/v2-e3634165fbe1c09983d564c34e2bbfb9_b.jpg b/static/v2-e3634165fbe1c09983d564c34e2bbfb9_b.jpg new file mode 100644 index 0000000..0780caf Binary files /dev/null and b/static/v2-e3634165fbe1c09983d564c34e2bbfb9_b.jpg differ diff --git a/static/vue.css b/static/vue.css index 6694c27..46e203b 100644 --- a/static/vue.css +++ b/static/vue.css @@ -1 +1 @@ -@import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30}.sidebar-toggle .sidebar-toggle-button:hover{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:800px;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;z-index:1}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#42b983);box-sizing:border-box;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{border-radius:2px;color:#e96900;font-size:.8rem;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section code,.markdown-section pre{background-color:#f8f8f8;font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section pre{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;line-height:1.5rem;margin:1.2em 0;overflow:auto;padding:0 1.4rem;position:relative;word-wrap:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#f8f8f8;border-radius:2px;color:#525252;display:block;font-family:Roboto Mono,Monaco,courier,monospace;font-size:.8rem;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;padding:2.2em 5px;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto}pre:after{color:#ccc;content:attr(data-lang);font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0} \ No newline at end of file +@import url("https://fonts.loli.net/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");*{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}.emoji{height:1.2rem;vertical-align:middle}.progress{background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30}.sidebar-toggle .sidebar-toggle-button:hover{opacity:.4}.sidebar-toggle span{background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:800px;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{-webkit-animation:none;animation:none}.github-corner .octo-arm{-webkit-animation:octocat-wave .56s ease-in-out;animation:octocat-wave .56s ease-in-out}}@-webkit-keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;height:100%;width:100%}section.cover .cover-main{flex:1;margin:-20px 16px 0;text-align:center;z-index:1}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid var(--theme-color,#42b983);box-sizing:border-box;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code{border-radius:2px;color:#e96900;font-size:.8rem;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section code,.markdown-section pre{background-color:#f8f8f8;font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section pre{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;line-height:1.5rem;margin:1.2em 0;overflow:auto;padding:0 1.4rem;position:relative;word-wrap:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;background-color:#f8f8f8;border-radius:2px;color:#525252;display:block;font-family:Roboto Mono,Monaco,courier,monospace;font-size:.8rem;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;padding:2.2em 5px;white-space:inherit}.markdown-section code:after,.markdown-section code:before{letter-spacing:.05rem}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto}pre:after{color:#ccc;content:attr(data-lang);font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0} \ No newline at end of file diff --git a/static/webp b/static/webp deleted file mode 100644 index d467c04..0000000 Binary files a/static/webp and /dev/null differ