diff --git a/README.md b/README.md
index 4c498af..4beca2f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+
# 《大厂面试指北》
最佳阅读地址:http://notfound9.github.io/interviewGuide/
@@ -6,26 +7,17 @@ Github项目主页:https://github.com/NotFound9/interviewGuide
作者博客地址:https://juejin.im/user/5b370a42e51d4558ce5eb969
-关注我的公众号"大厂面试",可以领取《大厂面试指北》的PDF版本,为了方便技术交流,也建了一个技术交流群,欢迎大家扫码加入!(如果下面的图挂了,是你所在的网络访问不了Github域名下的图片,可以去[最佳阅读地址里面看图](http://notfound9.github.io/interviewGuide/#/),或者是去[gitee的项目主页](https://gitee.com/notfound9/interviewGuide)里面看,谢谢了)
+## 为什么要做这个开源项目?
-
+之前在业余时间阅读技术书籍时,发现只阅读而不产出,这样收效甚微。所以就在网上找了很多常见的技术问题,根据自己的技术积累,查阅书籍,阅读文档和博客等资料,尝试着用自己的话去写了很多原创解答,最终整理开源到Github。一方面是便于自己复习巩固,一方面是将这些自己写的解答开源出来分享给大家,希望可以帮助到大家,也欢迎大家一起来完善这个项目,为开源做贡献。
-
-
-
-
## 目录
- [首页](README.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)
- - [【大厂面试04期】讲讲一条MySQL更新语句是怎么执行的?](https://mp.weixin.qq.com/s/pNe1vdTT24oEoJS_zs-5jQ)
* Java
- [基础](docs/JavaBasic.md)
- [1.Java中的多态是什么?](docs/JavaBasic.md#Java中的多态是什么?)
@@ -38,6 +30,10 @@ Github项目主页:https://github.com/NotFound9/interviewGuide
- [8.Java中的访问控制符有哪些?](docs/JavaBasic.md#Java中的访问控制符有哪些?)
- [9.Java的构造器有哪些特点?](docs/JavaBasic.md#Java的构造器有哪些特点?)
- [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的区别是什么?)
@@ -61,9 +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.sychronize的实现原理是怎么样的?](docs/JavaMultiThread.md#sychronize的实现原理是怎么样的?)
+ - [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是什么?)
@@ -95,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是什么?)
@@ -127,23 +136,38 @@ 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)
+- [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)
+ - [【大厂面试04期】讲讲一条MySQL更新语句是怎么执行的?](https://mp.weixin.qq.com/s/pNe1vdTT24oEoJS_zs-5jQ)
+ - [【大厂面试05期】说一说你对MySQL中锁的理解?](https://mp.weixin.qq.com/s/pTpPE33X-iYULYt8DOPp2w)
+ - [【大厂面试06期】谈一谈你对Redis持久化的理解?](https://mp.weixin.qq.com/s/nff4fd5TnM-CMWb1hQIT9Q)
+ - [【大厂面试07期】说一说你对synchronized锁的理解?](https://mp.weixin.qq.com/s/H8Cd2fj82qbdLZKBlo-6Dg)
+ - [【大厂面试08期】谈一谈你对HashMap的理解?](https://mp.weixin.qq.com/s/b4f5NIPl9uVLkRg_UpWSJQ)
* 读书笔记
- [《Redis设计与实现》读书笔记 上](docs/RedisBook1.md)
- [《Redis设计与实现》读书笔记 下](docs/RedisBook2.md)
@@ -151,21 +175,6 @@ Github项目主页:https://github.com/NotFound9/interviewGuide
- [《深入理解Java虚拟机-第三版》读书笔记](docs/JVMBook.md)
- [好书推荐](docs/bookRecommend.md)
-## 为什么要做这个开源项目?
-
-我就是个普通的程序员,只是喜欢在空闲时看一些技术书籍,但是发现看完后,即便每一章都写了读书笔记,看到一些相关的面试题时,自己还是一脸茫然,所以我认为学习一项技术分为三个阶段:
-
-1.看过入门教程,会用API。
-
-2.看过相关的技术书籍,了解一部分原理。
-
-3.能够对面试题进行分析,做出正确的解答,这样才能对技术有较为深入的理解,在工作中遇到复杂问题时,才能解决。
-
-所以我发起了这个项目,
-* 一方面是督促自己学习。
-* 一方面是将这些面试题整理后,写完解答后分享给大家,希望可以帮助到大家,也欢迎大家一起来完善这个项目,为开源做贡献。
-
-
## 如何为这个开源项目做贡献?
如果你想一起参与这个项目,可以提Pull Request,可以扫上面的入群二维码进群,如果入群二维码失效了,也可以扫我的微信,我们一起聊聊!
@@ -174,6 +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 5037a45..bd331c5 100644
--- a/_sidebar.md
+++ b/_sidebar.md
@@ -1,25 +1,32 @@
- [首页](README.md)
-- [大厂面试系列](docs/BATInterview.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/MySQLNote.md)
- [慢查询优化实践](docs/MySQLWork.md)
* JVM
- - [面试题总结](docs/JavaJVM.md)
+ - [基础](docs/JavaJVM.md)
- [Kafka](docs/Kafka.md)
- [ZooKeeper](docs/ZooKeeper.md)
- [HTTP](docs/HTTP.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)文章中:
-
+
先调用iterator.next()获取元素,与elem进行比较,如果相等,再调用list.remove(iterator.next());来移除元素,这个时候的iterator.next()其实已经不是与elem相等的元素了,而是后一个元素了,我们可以写个demo来测试一下
diff --git a/docs/BATInterview.md b/docs/BATInterview.md
index 4bd78dd..70e0bfa 100644
--- a/docs/BATInterview.md
+++ b/docs/BATInterview.md
@@ -6,13 +6,9 @@

-
-
-
-
## 目录:
@@ -20,13 +16,10 @@
### [【大厂面试02期】Redis过期key是怎么样清理的?](https://mp.weixin.qq.com/s/J_nOPKS17Uax2zGrZsE8ZA)
### [【大厂面试03期】MySQL是怎么解决幻读问题的?](https://mp.weixin.qq.com/s/8D6EmZM3m6RiSk0-N5YCww)
### [【大厂面试04期】讲讲一条MySQL更新语句是怎么执行的?](https://mp.weixin.qq.com/s/pNe1vdTT24oEoJS_zs-5jQ)
-
-
-
-
-
-
-
+### [【大厂面试05期】说一说你对MySQL中锁的理解?](https://mp.weixin.qq.com/s/pTpPE33X-iYULYt8DOPp2w)
+### [【大厂面试06期】谈一谈你对Redis持久化的理解?](https://mp.weixin.qq.com/s/nff4fd5TnM-CMWb1hQIT9Q)
+### [【大厂面试07期】说一说你对synchronized锁的理解?](https://mp.weixin.qq.com/s/H8Cd2fj82qbdLZKBlo-6Dg)
+### [【大厂面试08期】谈一谈你对HashMap的理解?](https://mp.weixin.qq.com/s/b4f5NIPl9uVLkRg_UpWSJQ)
diff --git a/docs/CodingInterviews.md b/docs/CodingInterviews.md
index 0877ede..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 二维数组中的查找
##### 题目内容:
+
+
在一个二维[数组](https://cuijiahua.com/blog/tag/数组/)中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维[数组](https://cuijiahua.com/blog/tag/数组/)和一个整数,判断数组中是否含有该整数。
如果在一个二维数组中找到数字7,则返回true,如果没有找到,则返回false。
@@ -150,45 +170,41 @@
}
```
-
-
## 题005 从尾到头打印链表
输入一个链表,按链表值从尾到头的顺序返回一个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;
-}
-```
-
总结:
首先通过开始的判断,来排除链表为空的情况,直接返回空数组,链表不为空,取下一个节点,判断下一个节点是否为空,
-* 不为空,那么递归调用printListFromTailToHead方法来获取后面的节点反序生成的ArrayList,然后添加当前的节点的值,然后返回arrayList。
+- 不为空,那么递归调用printListFromTailToHead方法来获取后面的节点反序生成的ArrayList,然后添加当前的节点的值,然后返回arrayList。
+- 为空,那么说明当前节点是链表尾部节点,直接创建一个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;
+}
+```
+
-* 为空,那么说明当前节点是链表尾部节点,直接创建一个ArrayList,然后添加当前节点的值,然后返回arrayList。
## 题006重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
-
+
前序遍历结果和中序遍历结果:
-
+
前序遍历结果分布是二叉树根节点,左子树,右子树。
@@ -211,6 +227,7 @@ 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;
@@ -226,11 +243,9 @@ 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();
@@ -249,8 +264,6 @@ 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;
}
}
@@ -287,7 +300,7 @@ int minNumberInRotateArray(int[] array) {
思路:旋转数组其实就是一个递增的数组,整体移动了一下元素,类似3,4,5,1,2这种。要查找最小的元素,可以遍历一遍数组,复杂度为O(N),这样就太暴力了,因为这个旋转数组其实是有规律的,可以根据左边界,右边界,中间值来判断最小值的位置
-* 左边界<=中间值 说明左边界到中间值这一段是递增的,也就是最小值不处于这一段。这样可以排除掉这一段,然后去另一段里面查找。
+* 左边界<=中间值 说明左边界到中间值这一段是递增的,也就是最小值不处于这一段。这样可以排除掉这一段,然后去另一段里面遍历查找。
* 中间值<=右边界 说明中间值到右边界这一段是递增的,也就是最小值不处于这一段。这样可以排除掉这一段,然后去另一段里面查找。
@@ -299,7 +312,7 @@ int minNumberInRotateArray(int[] array) {
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39
-
+##### 递归解法
f(n) =f(n-1)+f(n-2)
@@ -318,16 +331,46 @@ int Fibonacci(int n) {
}
```
+
## 题010求某个数的二进制格式下1的个数
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
-第一种解法就是每次拿二进制数最低位与1取&结果,如果结果为1,代表最低位为1,count+1,然后将二进制数>>1位,让倒数第二位成为最低位然后比较。
+第一种解法就是每次拿二进制数最低位与1取&结果,如果结果为1,代表最低位为1,count+1,然后将二进制数>>1位,让倒数第二位成为最低位然后比较。(负数右移动,左边的空位是会补1的,所以如果是负数A右移动,会先变成变成A/2,最终变为-1,-1的二进制数继续右移还是-1)
-这种解法的问题在于负数的最高位是1,向右移动一位后,为了保证移位后还是一个负数,最高位还是设置为1,这样就会陷入死循环。
+在计算机中,数值都是使用补码进行表示的,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。正数的补码就是原码,负数的补码就是绝对值取原码,取反得到反码,然后再+1
+
+```
+-1的补码是
+1、先取1的原码:00000000 00000000 00000000 00000001
+2、得反码: 11111111 11111111 11111111 11111110
+3、得补码: 11111111 11111111 11111111 11111111
+
+-2的补码是
+1、先取1的原码:00000000 00000000 00000000 00000010
+2、得反码: 11111111 11111111 11111111 11111101
+3、得补码: 11111111 11111111 11111111 11111110
+```
+
+这种解法的问题在于负数的最高位是1,向右移动一位后,为了保证移位后还是一个负数,最高位还是设置为1,这样就会陷入死循环(-1右移动还-1)。
第二种解法就是不拿最低位去进行比较了,而是定义一个变量flag=1,拿二进制数与flag进行比较,判断最低位是否为1,为1那么count+1,然后将flag<<1位,拿二进制数与flag进行比较,判断倒数第二位是否为1,然后一直把每一位都判断完,但是在Java中,int是4字节,32位,这样需要判断32次。
+```java
+public int NumberOf1(int n) {
+ int count = 0;
+ int bit = 1;
+ int times =0;
+ while(times<32) {
+ //不为0说明这一个二进制位为1,
+ if((bit&n) != 0) {count++;}
+ times++;
+ bit = bit<<1;
+ }
+ return count;
+}
+```
+
第三种解法可以做到二进制数有多少个1就判断多少次。具体原理是
n&(n-1)的结果其实是将n的最右边的1去掉,所以多次执行n&(n-1)直到将所有的1都去掉,以此来计数。
@@ -336,7 +379,7 @@ n&(n-1)的结果其实是将n的最右边的1去掉,所以多次执行n&(n-1)
public int NumberOf1(int n) {
int count = 0;
while (n != 0) {
- ++count;
+ count++;
n = n & (n-1);
}
return count;
@@ -382,11 +425,11 @@ double Power(double base ,int exponent) {
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
-如果可以使用额外的内存空间,可以对数组遍历两遍,一遍将奇数取出,存放在额外的数组中去,一遍把剩下的偶数存放到额外的数组中去。
+解题思路:
-如果不能使用额外的内存空间,就是查找奇数,然后与前面的元素替换,一直到替换到最后一个奇数的后面,有点像是插入排序
+如果可以使用额外的内存空间,可以对数组遍历两遍,一遍将奇数取出,存放在额外的数组中去,一遍把剩下的偶数存放到额外的数组中去。
-(插入排序就是向一个已经排好序的序列中插入元素。过程是遍历数组,对每个元素A与前面的元素比较,如果前面的元素大于A,就会将前面的元素后移,直到找到一个小于A的元素,然后将A放在这个元素后面。)。
+如果不能使用额外的内存空间,就是查找奇数,然后与前面的元素互换,一直到替换到最后一个奇数的后面,有点像是冒泡排序。(因为不能改变相对位置,所以不能用快排)
冒泡排序是其实是交换,从头开始,依次判断两个相邻的元素,将更大的元素向右交换,遍历一次后可以将当前序列最大的元素交换到最后面去,下次遍历就不用管最后一个元素。
@@ -418,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;
@@ -478,6 +519,33 @@ public static ListNode ReverseList(ListNode head) {
}
```
+这种解法好理解一点,就是使用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;
+}
+```
+
+
+
## 题015 合并链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
@@ -688,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) {
ArrayList arrayList = new ArrayList();
@@ -757,9 +811,51 @@ public ArrayList PrintFromTopToBottom(TreeNode root) {
}
```
+也可以通过队列来实现,将根节点添加到队列中,然后对队列进行循环,每次从队列取出一个元素,添加到ArrayList中去,然后将左,右子节点添加到队列中去,然后继续循环,一直到队列中取不到元素。(Java中队列的实现Queue,add(),remove())
+
+##### 深度优先遍历
+
+一般是使用栈来实现,一开始将
+
+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;
+}
+```
-也可以通过队列来实现,将根节点添加到队列中,然后对队列进行循环,每次从队列取出一个元素,添加到ArrayList中去,然后将左,右子节点添加到队列中去,然后继续循环,一直到队列中取不到元素。(Java中队列的实现Queue,add(),remove())
## 题022 判断是否是二叉搜索树的后序遍历
@@ -799,15 +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 中,数组长度大的数组靠前。)
就是递归调用每个节点的左右子树,然后将节点值相加,如果节点值和为某个预期值,并且该节点为叶子节点,那么这条路径就是要找的路径。
@@ -862,7 +957,6 @@ public RandomListNode Clone(RandomListNode pHead)
}
-
//设置新链表的特殊指针
RandomListNode oldCurrentNode= pHead;
RandomListNode newCurrentNode;
@@ -900,28 +994,28 @@ public RandomListNode Clone(RandomListNode pHead)
## 题025 二叉搜索树与双向链表
+
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
二叉搜索树的中序遍历的结果就是递增的序列,所以递归实现二叉搜索树的中序遍历
```java
- TreeNode head = null;
- TreeNode lastNode = null;
+ TreeNode head = null;//主要记录双向链表头结点
+ TreeNode lastNode = null;//主要记录中序遍历时,上一次遍历的节点
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) {
return null;
}
Convert(pRootOfTree.left);
- if (head == null) {
+ if (head == null) {//这里相当于是把第一次执行Convert方法的元素设置为链表头结点,也就是中序遍历第一个位置的节点,也就最左边的叶子节点。
head = pRootOfTree;
}
- if (lastNode == null) {
- lastNode = pRootOfTree;
- } else {
- lastNode.right = pRootOfTree;
+ if (lastNode != null) {//中序遍历时,假设存在上一个遍历的节点,将上一个节点与这个节点进行关联
+ lastNode.right = pRootOfTree;
pRootOfTree.left = lastNode;
- lastNode = pRootOfTree;
- }
+ }
+ //完成对当前节点的遍历,将当前设置为lastNode。
+ lastNode = pRootOfTree;
Convert(pRootOfTree.right);
return head;
}
@@ -933,37 +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;
-}
+1.遍历字符串,每次取一个不同的字符作为首字符,然后对后面剩余的字符串递归调用方法,对后面的字符串进行排列。
-public void Permutation(ArrayList arrayList, String str, int start,int end) {
+2.在递归的尽头,也就是当前子字符串只有1个元素,会将所有元素添加到数组。
- if (str == null || str.length() == 0|| start>end) {
- return;
- }
- if (end - start == 0) {
- arrayList.add(str);
- return;
+```java
+ public ArrayList Permutation1(String str) {
+ ArrayList list = new ArrayList();
+ if (str==null||str.length()==0) {
+ return list;
+ }
+ Permutation(list,str,0,str.length()-1);
+ return list;
}
- 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);
- Permutation(arrayList, stringBuffer.toString(), start +1, end);
+ 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;
+ }
+ for (int i = start; i <= end; i++) {
+ Character c = str.charAt(i);
+ if (set.contains(c) == false) {
+ //添加当前字符到set,代表已经做了首元素了
+ set.add(c);
+ //将当前字符与首字符交换
+ Character startChar = stringBuffer.charAt(start);
+ stringBuffer.setCharAt(start,c);
+ stringBuffer.setCharAt(i,startChar);
+ //继续递归
+ Permutation(list, stringBuffer.toString(),start+1,end);
+ }
}
}
-}
```
## 题027数组中出现的次数超过一半的数字
@@ -972,6 +1074,8 @@ public void Permutation(ArrayList arrayList, String str, int start,int end) {
就是一个数组,假设包含一个超过次数一半的元素,那么去除掉两个不相等的元素后,剩下的数组中,这个元素还是会出现次数超过一半。
+(原理就是每次排除两个不相等的元素,最后剩下的一个元素,或者两个元素一定是次数超过一半的这个数字。)
+
```java
public int MoreThanHalfNum_Solution(int [] array) {
if (array==null||array.length==0) {
@@ -992,18 +1096,15 @@ public int MoreThanHalfNum_Solution(int [] array) {
times--;
}
}
- if (times==0) {
- return 0;
- } else {
- int statTimes = 0;
- for (int i = 0; i < array.length; i++) {
- if (array[i] == result) {
- statTimes++;
- }
- }
- if (statTimes>array.length/2) {
- return result;
- }
+ //下面就是判断这个数字是否满足条件
+ 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;
}
@@ -1011,28 +1112,28 @@ public int MoreThanHalfNum_Solution(int [] array) {
## 题028最小的k个数
-输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
+输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
-其实就是插入排序,只不过只是对k个数进行插入排序
+其实就是插入排序,只不过只是对k个数进行插入排序,但是这样的时间复杂度会是O(N^2),如果是使用快排来做,平均时间复杂度就是O(N)。
```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,并且当前数比子数组最后一个数大
+ } else if (input[i] > 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);
@@ -1048,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]
@@ -1064,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;
}
```
@@ -1086,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) {
@@ -1108,7 +1209,7 @@ public int NumberOf1Between1AndN_Solution(int n) {
就是把小的数排前面,这样排出来的数就是最小的,
-思路就是冒泡排序,将小数往后面挪动,只是判断数A与数B直接的大小,是以AB和BA的大小来决定的,所以对A和B进行拼接成,AB和BA,通过字符串比较,判断AB和BA的大小,AB>BA,说明A排起来会比较大,往后面挪。
+思路就是冒泡排序,将小数往后面挪动,只是判断数A与数B之间的大小,是以AB和BA的大小来决定的,所以对A和B进行拼接成,AB和BA,通过字符串比较,判断AB和BA的大小,AB>BA,说明A排起来会比较大,往后面挪。
```java
public String PrintMinNumber(int [] numbers) {
@@ -1153,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个丑数。
@@ -1164,20 +1267,19 @@ 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;
+ 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需要分开判断赋值
+ // 例如12可以是6*2,也可以是4*3,所以这里的三个if需要分开判断赋值
index3++;
}
if (temp5 == minTemp) {
@@ -1193,14 +1295,14 @@ public int GetUglyNumber_Solution(int index) {
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
-因为java中的字符char类型是 1字节的,也就是8位,所以只有256种可能,所以可以使用一个数组来对字符出现的次数进行记录。第一遍扫描字符串,统计字符出现次数,第二遍扫码字符串,返回只出现一次的字符的下标。
+因为java中的字符char类型是 2字节的,也就是16位,所以只有256*256种可能,所以可以使用一个数组来对字符出现的次数进行记录。第一遍扫描字符串,统计字符出现次数,第二遍扫码字符串,返回只出现一次的字符的下标。
```java
public int FirstNotRepeatingChar(String str) {
if (str == null|| str.length() == 0) {
return -1;
}
- int[] array = new int[256];
+ int[] array = new int[256*256];
for (int i = 0; i < str.length(); i++) {
char value = str.charAt(i);
array[value]++;
@@ -1229,7 +1331,7 @@ public int GetUglyNumber_Solution(int index) {
对于%100的数据,size<=2*10^5
-逆序对就是前面的数比后面的数大,就是一个逆序对,可以使用归并排序,每次组合并时,右边的组的数比左边的数小时,会出现逆序对,逆序对的个数为左边的组的数组元素个数。
+逆序对就是前面的数比后面的数大,就是一个逆序对,可以使用归并排序,每次组合并时,右边的组的数比左边的数小时,会出现逆序对,逆序对的个数为当前左边的组的元素个数。
```java
public int InversePairs(int [] array) {
@@ -1280,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) {
@@ -1515,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;
}
}
@@ -1523,7 +1625,7 @@ public int IsBalanced_Solution_Depth(TreeNode root) {
}
```
-## 题039 数组中只出现一次的数组
+## 题039 数组中只出现一次的数
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
@@ -1623,8 +1725,15 @@ public ArrayList> FindContinuousSequence(int sum) {
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
+输入字符串S=”abcXYZdef”,要输出“XYZdefabc”,其实就是将前n个字符移动到字符串末尾,直接移动的话很容易实现,不管是直接使用添加字符串的方法,还是直接使用字符串截取的方法,这里是一种新方法,就是将整个字符串翻转,再将length-n的字符翻转,再将后n个字符翻转,这样得到的也是前n个字符移动到末尾的结果。
-输入字符串S=”abcXYZdef”,要输出“XYZdefabc”,其实就是将前n个字符移动到字符串末尾,直接移动的话很容易实现,不管是直接使用添加字符串的方法,还是直接使用字符串截取的方法,这里是一种新方法,就是将前n个字符翻转,再将前n以后的字符翻转,然后对整个字符串翻转,这样得到的也是前n个字符移动到末尾的结果。
+原字符串: abcXYZdef
+
+整个字符翻转: fedZYXcba
+
+前length-n个字符翻转: XYZdefcba
+
+后n个字符翻转得到最终结果:XYZdefabc
```java
public String LeftRotateString(String str,int n) {
@@ -1754,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;
@@ -1777,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) {
@@ -1791,12 +1912,29 @@ 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 + A&B<<1 进行相加
+
+递归写法
+
+```java
public int Add(int num1,int num2) {
+ if(num2 == 0) {
+ return num1;
+ }
+ return Add(num1^num2, (num1&num2)<<1);
+}
+```
+for循环写法
+
+```java
+public int Add(int num1,int num2) {
while (num2!=0) {
int temp = num1 ^ num2;
int temp2 = (num1 & num2) << 1;
@@ -1817,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;
@@ -1857,7 +1995,7 @@ public static int StrToInt(String str) {
解法一就是在遍历时将每个元素添加到hashSet,通过判断hashset中是否包含当前元素,来判断是否重复,由于这个长度为n数组中元素的取值范围是0-n-1,所以可以使用一个长度为n的数组array来代替hashSet记录元素是否出现,例如x出现了,将数组array[x]设置为1。
-解法二就是将当前数组作为标记数组,每次遍历到下标为i的元素时,将array[array[i]]与当前元素交换,并且将array[i]设置为-1,代表已经这个元素是重复元素。
+解法二就是将当前数组作为标记数组,每次遍历到下标为i的元素时,将array[array[i]]与当前元素交换,并且将array[array[i]]设置为-1,代表已经这个元素是重复元素,然后i- -,继续遍历交换后的这个元素。
```
public boolean duplicate(int numbers[],int length,int [] duplication) {
@@ -1886,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]
@@ -1898,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;
@@ -1919,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)
{
@@ -1931,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 链表中环的入口节点
@@ -1949,11 +2089,13 @@ public char FirstAppearingOnce()
一种方法是遍历整个链表,将每个节点添加到HashSet中,判断是否在HashSet中出现过,第一个重复的节点就是环的入口节点。
-另一种解决方法是,假设存在环,环的长度为x,第一个指针先走x步,然后第二个指针从链表头结点出发,两个指针一起走,当第而个指针刚好走到环入口时,第一个指针正好在环中走了一圈,也在环的入口,此时的节点就是环的的入口节点,
+另一种解决方法是,假设存在环,环的长度为x,第一个指针先走x步,然后第二个指针从链表头结点出发,两个指针一起走,当第二个指针刚好走到环入口时,第一个指针正好在环中走了一圈,也在环的入口,此时的节点就是环的的入口节点,
-怎么得到环的长度呢,就是一个指针每次走2步,一个指针每次走一步,他们相遇时的节点肯定就是在环中的某个节点,然后这个节点在环中遍历一圈,回到原点,就可以得到环的长度。
+怎么得到环的长度呢,就是一个指针每次走2步,一个指针每次走一步,他们相遇时的节点肯定就是在环中的某个节点,然后这个节点在环中遍历一圈,回到原点,就可以得到环的长度count。
-```
+两个指针从头出发,第一个指针先走count步,然后两个指针每次都只走一步,相遇的地方就是环的入口。
+
+```java
public ListNode EntryNodeOfLoop(ListNode pHead)
{
if (pHead == null || pHead.next==null) {
@@ -1984,9 +2126,9 @@ public ListNode EntryNodeOfLoop(ListNode pHead)
tempNode = tempNode.next;
count++;
}
- //从链表头结点出发,第一个指针先走count步,
+ //从链表头结点出发,第一个指针先走count步,然后两个指针每次只走一步,相遇的地方就是环的入口,
// 然后第一个指针和第二个指针一起走,当第二个指针刚好走了x步到环入口时,
- // 第一个指针正好走了x+count步,在环中走了一圈,也在环的入口
+ // 第一个指针正好走了x+count步,在环中走了一圈,也在环的入口,
quickNode = pHead;
for (int i = 0; i < count; i++) {
quickNode = quickNode.next;
@@ -2003,7 +2145,7 @@ public ListNode EntryNodeOfLoop(ListNode pHead)
使用hashSet的解法
-```
+```java
public ListNode EntryNodeOfLoop1(ListNode pHead)
{
if (pHead==null) {
@@ -2025,7 +2167,13 @@ public ListNode EntryNodeOfLoop1(ListNode pHead)
## 题055 删除链表中重复的节点
-就先创建一个我们自己的节点ourHead,ourHead.next= head,
+在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
+
+解法:
+
+就先创建一个我们自己的节点ourHead,
+
+ourHead.next= head,
pre = ourHead
@@ -2037,7 +2185,7 @@ currentNode = pre.next
不相等,那么就直接让pre和currentNode向后移动一步。
-```
+```java
public ListNode deleteDuplication(ListNode pHead)
{
if (pHead == null || pHead.next == null) {
@@ -2046,12 +2194,11 @@ public ListNode deleteDuplication(ListNode 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) {
+ while (currentNode!=null) {//往后遍历
+ if (currentNode.next!=null && currentNode.val == currentNode.next.val) {//如果当前节点与下一个节点相等,就找到一个与当前节点不相等的节点,然后把中间多出来的这些相等的节点都删除掉
ListNode tempNode = currentNode.next;
//找到第一个不相等的节点
while (tempNode!=null) {
@@ -2060,7 +2207,7 @@ public ListNode deleteDuplication(ListNode pHead)
}
preNode.next = tempNode;
currentNode = preNode.next;
- } else {
+ } else {//如果当前节点与下一个节点相等,就跳过,遍历下一个节点
preNode = preNode.next;
currentNode = currentNode.next;
}
@@ -2081,23 +2228,23 @@ public ListNode deleteDuplication(ListNode pHead)
所以当前节点的中序遍历中的下一个节点是
-1.有右子树
+1.当前节点有右子树
右子树有左节点,一直向下遍历,找到最左的叶子节点。
右子树没有左节点,就是右子树节点。
-2.没有右子树
+2.当前节点没有右子树
没有父节点,那么没有下一个节点。
- 有父节点
+ 这个节点有父节点
- 父节点是左子树,直接返回父节点。
+ 这个节点父节点是属于左边分支的,直接返回父节点。
- 父节点是右子树,一直向上遍历,直到找到祖先节点是左子树的,找到就返回,找不到就返回空。
+ 这个节点父节点是属于右边分支的,一直向上遍历,直到找到一个父节点,他是祖先节点是左节点的,找到就返回祖先节点,找不到就返回空。
-```
+```java
public TreeLinkNode GetNext(TreeLinkNode pNode)
{
//这个节点有右子树
@@ -2144,7 +2291,7 @@ public TreeLinkNode GetNext(TreeLinkNode pNode)
假设有另外一种遍历是根节点,右子树,左子树,如果二叉树是对称,那么这两种遍历的结果是一样的,所以使用递归来进行两种遍历,然后在过程中判断两种遍历结果是否一样。
-```
+```java
boolean isSymmetrical(TreeNode pRoot)
{
return isSymmetrical(pRoot,pRoot);
@@ -2173,16 +2320,16 @@ boolean isSymmetrical(TreeNode leftRoot,TreeNode rightRoot)
当前出于偶数层时,每次对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;
+ 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();
@@ -2207,6 +2354,49 @@ public ArrayList> Print(TreeNode pRoot) {
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 把二叉树打印成多行
@@ -2220,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;
@@ -2249,9 +2439,9 @@ ArrayList> Print(TreeNode pRoot) {
就是递归遍历每一个节点,遍历时传入深度depth,将节点加入到ArrayList中特定深度对应的数组中去。
-这种方法也可以用来进行二叉树深度遍历,遍历完之后将嵌套数组拆分成单层的数组。
+这种方法也可以用来进行二叉树先序遍历,遍历完之后将嵌套数组拆分成单层的数组。
-```
+```java
ArrayList> Print2(TreeNode pRoot) {
ArrayList> arrayLists = new ArrayList>();
if (pRoot==null)return arrayLists;
@@ -2288,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();}
@@ -2361,7 +2551,7 @@ Integer convert(String str) {
由于前序遍历是先左子树,根节点,右子树的顺序,所以前序遍历的结果,就是二叉搜索树中元素按递增顺序排列的结果,所以按照前序遍历到第K个元素就是第K小的节点。
-```
+```java
Integer index = 0;
TreeNode kNode = null;
@@ -2393,7 +2583,7 @@ void find(TreeNode node, Integer k) {
-```
+```java
ArrayList arrayList = new ArrayList();
public void Insert(Integer num) {
@@ -2427,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);
@@ -2482,7 +2672,7 @@ ArrayList removeValueIntoSorted(ArrayList sortList, int curren
因为这些元素的index比A都小,而且值也比A小,不可能再成为最大值了,
然后判断队列头结点的最大值是否过期,过期的话也删除
-```
+```java
public ArrayList maxInWindows(int[] num, int size) {
ArrayList arrayList = new ArrayList();
@@ -2521,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];
@@ -2548,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)
@@ -2576,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;}
@@ -2656,17 +2846,13 @@ public int cutRope(int target) {
}
```
-
-
-
-
# 基础篇
## 二分查找
普通的二分查找
-```
+```java
public int findByHalf(int[] array, int target) {
if (array==null||array.length==0) {
return -1;
@@ -2689,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};
@@ -2716,134 +2902,4 @@ public int findByHalf(int[] array, int target) {
// 检查出界情况,因为循环结束的条件是left=right+1,如果target比数组所有元素都大会导致越界
//如果有左边界就返回左边界,没有时,此时的left应该是最接近左边界,并且大于左边界的数的位置
```
-
-
-
-## LRU算法
-
-LRU其实就是Last Recent Used,就是最近使用淘汰策略,所以当空间满了时,就根据最近使用时间来删除。一般是使用一个双向链表来实现,同时为了快速访问节点,会使用一个HashMap来存储键值映射关系。(需要注意的是,为了在内存满时删除最后一个节点时,可以以O(1)时间复杂度从HashMap中删掉这个键值对,每个节点除了存储value以外,还需要存储key)。
-
-```
-public class Test003 {
- //双向链表
- 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 head;
- ListNode last;
- int limit=4;
- HashMap hashMap = new HashMap();
-
- public void add(String key, Integer val) {
- ListNode existNode = hashMap.get(key);
- if (existNode!=null) {
- //从链表中删除这个元素
- ListNode pre = existNode.pre;
- ListNode next = existNode.next;
- if (pre!=null) {
- pre.next = next;
- }
- if (next!=null) {
- next.pre = pre;
- }
- //更新尾节点
- if (last==existNode) {
- last = existNode.pre;
- }
- //移动到最前面
- head.pre = existNode;
- existNode.next = head;
- head = existNode;
- //更新值
- existNode.value = val;
- } else {
- //达到限制,先删除尾节点
- if (hashMap.size() == limit) {
- ListNode deleteNode = last;
- hashMap.remove(deleteNode.key);//正是因为需要删除,所以才需要每个ListNode保存key
- last = deleteNode.pre;
- deleteNode.pre = null;
- last.next = null;
- }
- ListNode node = new ListNode(key,val);
- hashMap.put(key,node);
- if (head==null) {
- head = node;
- last = node;
- } else {
- //插入头结点
- node.next = head;
- head.pre = node;
- head = node;
- }
- }
- }
- public ListNode get(String key) {
- return hashMap.get(key);
- }
- public void remove(String key) {
- ListNode deleteNode = hashMap.get(key);
- ListNode preNode = deleteNode.pre;
- ListNode nextNode = deleteNode.next;
- if (preNode!=null) {
- preNode.next = nextNode;
- }
- if (nextNode!=null) {
- nextNode.pre = preNode;
- }
- if (head==deleteNode) {
- head = nextNode;
- }
- if (last == deleteNode) {
- last = preNode;
- }
- hashMap.remove(key);
- }
- public static void main(String[] args) {
- Test003 test003 = new Test003();
- for (int i = 0; i < 5; i++) {
- test003.add("key="+i,i);
- for (ListNode j = test003.head; j!=null ; j=j.next) {
- System.out.println("for 循环"+j.key+j.value);
- }
- System.out.println("\n");
- }
- test003.remove("key=3");
- System.out.println("\n");
- for (ListNode i = test003.head; i!=null ; i=i.next) {
- System.out.println("删除节点后"+i.key+i.value);
- }
- }
-}
-```
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 二维数组中的查找
+
+##### 题目内容:
+
+
+
+在一个二维[数组](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},则重建二叉树并返回。
+
+
+
+前序遍历结果和中序遍历结果:
+
+
+
+前序遍历结果分布是二叉树根节点,左子树,右子树。
+
+中序遍历结果分布是左子树,二叉树根节点,右子树。
+
+所以根据前序遍历结果的第一个元素获取到根节点,然后根据根节点把中序遍历结果分为两半,得到左子树的中序遍历结果,然后根据左子树的长度可以去前序遍历结果中分离出左子树的前序遍历结果,右子树也是如此,所以可以递归得构造出整个二叉树。
+
+```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 。假设输入的数组的任意两个数字都互不相同。
+
+
+
+二叉搜索树的特点在于,左子树所有节点<根节点<右子树所有节点,后续遍历的遍历顺序是左子树,右子树,根节点,所以取出数组最后一个元素,也就是根节点,然后遍历序列,发现第一个比根节点大的数之后,这个是也就是临界点,后面的数肯定也需要比根节点大,否则不是后续遍历,遍历完成后。
+
+* 没有临界点(也就是第一个右子树的节点),全部都是左子树,递归调用判断,
+* 临界点等于第一个元素,全部都是右子树,递归调用判断,
+* 其他情况,将序列分为左子树,右子树,递归调用判断,
+
+(遍历时如果只有一个元素,那么直接返回正确,肯定满足要求)
+
+```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到远程。
+
+
+`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 6683646..fbd2a92 100644
--- a/docs/HTTP.md
+++ b/docs/HTTP.md
@@ -2,23 +2,48 @@
#### [1.HTTPS建立连接的过程是怎么样的?](#HTTPS建立连接的过程是怎么样的?)
#### [2.HTTP的缓存策略是怎么样的?](#HTTP的缓存策略是怎么样的?)
+#### [3. TCP三次握手和四次挥手是怎么样的?](#TCP三次握手和四次挥手是怎么样的?)
+#### [4.TCP怎么保证可靠性的?](#TCP怎么保证可靠性的?)
+#### [5.TCP拥塞控制怎么实现?](#TCP拥塞控制怎么实现?)
+#### [6.close_wait太多怎么处理?](#close_wait太多怎么处理?)
### HTTPS建立连接的过程是怎么样的?
-在发起连接之前,服务器会将自己的公钥发给CA证书机构,CA证书机构会用自己的私钥对服务器的公钥加密,生成CA证书给服务器,服务器存储后供之后建立安全连接使用。
+
+
+在发起连接之前,服务器会向证书机构申请SSL证书,流程是服务器将自己的公钥发给CA证书机构,CA证书机构会用自己的私钥对服务器的公钥加密,生成SSL证书给服务器,服务器将SSL证书存储后供之后建立安全连接使用。
1.客户端发起请求,TCP三次握手,跟服务器建立连接。
-2.发送请求将随机数(时间戳+随机数),sessionID,支持的加密方式发送给服务器
-3.服务器接受请求,将随机数,sessionID,挑选的加密方式,CA证书发给客户端,CA证书会包括发布机构,过期时间,公共密钥
-4.客户端收到CA证书后,会将证书跟客户端内置的CA证书进行校验,CA证书中会有一个证书机构用自己的私钥对公钥,域名,有效期生成的hash值加密结果,也就是签名,客户端中内置的证书用公钥签名进行解密,看解密后得到的结果跟公钥,域名,有效期的hash值是否一样,这个如果是真实的,然后会看过期时间,域名是否匹配
-5.客户端会随机生成一个对称加密的公钥,根据根据CA证书里面的公钥将它加密后发给服务器
-6.服务器用自己的私钥解密,得到对称加密的公钥
-7.之后就根据对称加密的公钥进行通信
+如上图所示,在第 ② 步时服务器发送了一个SSL证书给客户端,SSL 证书中包含的具体内容有:
-假设没有CA,那么如果服务器返回的包含公钥的包被hack截取,然后hack也生成一对公私钥,他将自己的公钥发给客户端。hack得到客户端数据后,解密,然后再通过服务器的公钥加密发给服务器,这样数据就被hack获取。
-有了CA后,客户端根据内置的CA根证书,很容易识别出hack的公钥不合法,或者说hack的证书不合法。
+(1)证书的发布机构CA
+
+(2)证书的有效期
+
+(3)服务器的公钥
+
+(4)证书所有者
+
+(5)签名
+
+3、客户端在接受到服务端发来的SSL证书时,会对证书的真伪进行校验,以浏览器为例说明如下:
+
+(1)首先浏览器读取证书中的证书所有者、有效期等信息进行一一校验
+(2)浏览器开始查找操作系统中已内置的受信任的证书发布机构CA证书,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发 。
+(3)如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。
+
+(4)如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密
+
+(5)浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比
+
+(6)对比结果一致,则证明服务器发来的证书合法,没有被冒充
+
+(7)此时浏览器就可以读取证书中的公钥,用于后续加密了。
+
+假设没有CA,那么如果服务器返回的包含公钥的包被hack截取,然后hack也生成一对公私钥,他将自己的公钥发给客户端。hack得到客户端数据后,解密,然后再通过服务器的公钥加密发给服务器,这样数据就被hack获取。
+有了CA后,客户端根据内置的CA根证书,很容易识别出hack的公钥不合法,或者说hack的证书不合法。
### HTTP的缓存策略是怎么样的?
@@ -70,3 +95,265 @@ Cache-Control:该字段表示缓存最大有效时间,该时间是一个相
优先级方面排序是 强缓存(Cache-Control)> 强缓存(Expires)> 对比缓存(Etag/If-None-Match)> 对比缓存(Last-Modified/If-Modified-Since)
+### TCP三次握手和四次挥手是怎么样的?
+
+#### 三次握手:
+
+TCP是一个面向连接的可靠的传输协议。建立连接前需要进行三次握手。
+
+流程如下:
+
+
+
+1.客户端首先会生成一个随机数J作为数据包的序号,给服务端发送一个SYN包,包的序号seq设置为J,发送成功后,自己进入SYN_SENT 状态,代表发送SYN包成功,等待服务端的确认。
+
+2.服务端收到SYN包之后,会进入到SYN_RECV状态,同时为了检测服务端到客户端是否通畅,会给客户端发送一个SYN包,将ACK设置为1,并且会带上ack=J+1,生成随机数作为包的序号,seq=K,用于确认之前收到了客户端发送的SYN包。
+
+3.客户端收到服务端发送的SYN包后,会检测ACK标志位是否为1,并且ack是否等于J+1,如果是的话,就说明之前服务器收到了客户端发送的SYN包,并且服务端发送给客户端的SYN包也是可以收到的,所以需要给服务端发送ACK包,ACK=1,ack=K+1。
+
+4.服务端收到ACK回应后,检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
+
+#### 四次挥手
+
+
+
+TCP是全双工的连接,断开每一方向的连接都需要发送断开请求,断开确认。所以总共需要发送4个包才能确定连接的断开。
+
+1.首先客户端给服务端发送一个FIN包,seq=J,客户端进入FIN_WAIT_1状态,代表客户端已经没有数据发送给服务端了。
+
+2.服务端接收到FIN包之后,会给客户端发送一个ACK,ack=J+1,用于确认客户端-服务端这边的连接进行断开。同时会进入到CLOSE_WAIT状态,表示在等待关闭,因为服务端往客户端可能还在继续传输数据,暂时还不能断开。客户收到服务端返回的ACK回应后,会进入到FIN_WAIT_2状态,代表客户端-服务端的连接已经断开成功,等待服务端发送FIN包断开服务端-客户端的连接。
+
+3.服务端发现没有数据发给客户端后,会发一个FIN包给客户端,并且进入LAST_ACK,代表等待客户端的ACK,一旦收到ACK,代表连接正式断开,服务端可以进入CLOSE状态。
+
+4.客户端收到服务端发送的FIN包后,说明连接已经断开了,但是需要服务端知道,客户端会给服务端发送一个ACK包通知服务端,并且会进入TIME_WATING状态,代表超过超时时间后自动进入CLOSE状态,如果ACK包中途丢了,服务端会再发送FIN包,客户端会进行ACK包重发,这也是TIME_WAITING状态的意义。
+
+### TCP怎么保证可靠性的?
+
+TCP主要提供了**检验和**、**序列号/确认应答**、**超时重传**、**最大消息长度**、**滑动窗口控制**等方法实现了可靠性传输。
+
+**校验和**
+
+主要是将数据切分成若干个16位的二进制串,将每个二进制串看成一个二进制数,对这些数进行相加等运算得到一个校验和,然后接收方收到数据会使用同样的算法来对数据计算校验和,如果结果和发送端发过来的校验和一样,那么就校验成功。主要是为了防止接收方收到的是已经损坏的数据。
+
+**序列号/确认应答,超时重传**
+
+对发送的每个数据包都有一个序号,服务端收到数据包会返回一个ACK进行确认,如果在超过超时时间后,客户端还是没有收到服务端返回的ACK确认,就会对数据包进行超时重传。并且超时时间是动态变化的,初始值会大于正常的报文发送到接收到ACK回应的时间,重传后还没有得到回应会调大重传时间,然后进行重传。
+例如:在Linux中,超时以500ms为单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。其规律为:如果重发一次仍得不到应答,就等待2500ms后再进行重传,如果仍然得不到应答就等待4500ms后重传,依次类推,以指数形式递增,重传次数累计到一定次数后,TCP认为网络或对端主机出现异常,就会强行关闭连接。
+
+**最大消息长度**
+
+在建立TCP连接的时候,双方约定一个最大的长度(MSS)作为发送的单位,重传的时候也是以这个单位来进行重传。理想的情况下是该长度的数据刚好不被网络层分块。
+
+**滑动窗口控制**
+
+就是发送方在发送一个包时,不需要等到收到接收方对上一个包的确认应答后,才能发。只需要当前发送的包在滑动窗口内就行了。发送方每接收到一个确认应答,就会向后移动一位。 对于接收方而言,只会给客户端返回当前需要的包的序号,也就是对目前已收到的包的确认应答。(在拥塞控制里面,一般判断是否是网络拥塞导致丢包,有两种机制,超时重传,超过重传时间,还没有收到回应,另一个是收到三个重复确认ACK,就是比如中间某个数据包N丢了,那么后面发送的3个数据包如果被服务端接收到了,返回的确认回答ACK=N,代表没有收到数据包N,需要发送方重传,重复确认ACK超过3个,就认为发送了网络拥塞)
+
+
+
+### 流量控制是怎么实现的?
+
+因为滑动窗口设置得太大或太小都不易于数据传输,所以是根据接收端的反馈,发送端可以对滑动窗口大小进行动态调整的。发送端在发送的数据包的序号必须小于最大的滑动窗口值,所以当发送的数据包过多,导致接收端的缓冲区写满时,接收端会通知给客户端将滑动窗口设置为更小的值,减少发送的量,达到一个流量控制的效果。
+
+### TCP拥塞控制怎么实现?
+
+
+
+拥塞控制主要由**慢启动**,**拥塞避免**,**拥塞发生时算法**,**快速恢复**四个算法组成。
+
+**慢启动**
+
+TCP连接刚建立,一点一点地提速,试探一下网络的承受能力,以免直接扰乱了网络通道的秩序,是呈指数增长的。一开始拥塞窗口cwnd大小是1,就是每收到一个ACK确认回答,就会把拥塞窗口cwnd的大小+1,这样在每个往返时延下,窗口cwnd大小都会变为原来的二倍,所以就会呈指数增长。
+
+**拥塞避免**
+
+因为慢启动的拥塞窗口cwnd呈指数增长的,一旦达到网络的最大承受能力时,有可能已经发出大量数据包了,造成网络拥塞了,所以为了避免网络拥塞,当拥塞窗口cwnd达到慢启动的阀值ssthresh的大小时,就会停止指数增长,进入线性增长状态,在每个往返时延下,窗口cwnd大小都+1。
+
+**拥塞发生时算法**
+
+一般认为,网络拥塞时就会发生网络丢包,所以判定拥塞发生就是以丢包为主。有两种判定方法,**超时重传**,在发送一个数据以后就开启一个计时器,在一定时间(RTO[Retransmission Timeout])内如果没有得到发送数据报的ACK报文,那么就重新发送数据,直到发送成功为止。另一个是**收到三个重复确认ACK**,就是比如中间某个数据包N丢了,那么后面发送的3个数据包如果被服务端接收到了,返回的确认回答ACK=N,代表没有收到数据包N,需要发送方重传,重复确认ACK超过3个,就认为发送了网络拥塞)。
+
+一旦认定网络拥塞,就会将慢启动阀值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大小的一半后,
+
+如果是早期的算法TCP Tahoe,此时会将cwnd重置为1,重新开始慢启动的过程。
+
+如果现在的TCP Reno算发,会将cwnd窗口设置为新的ssthresh值的大小,后续开始进入拥塞避免算法的流程,对cwnd窗口进入线性增长的状态。
+
+### 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 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。
+HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。
+
+#### 2.Header 压缩
+HTTP/2并没有使用传统的压缩算法,而是开发了专门的"HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。
+
+具体来说:
+
+在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;
+
+首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
+
+每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值
+
+例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。
+
+#### 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。
+
+### HTTP报文结构是怎么样的?
+
+#### 请求报文
+
+请求报文结构如下:
+
+
+
+
+
+HTTP请求由请求行+请求header+空行+请求内容组成,第一行就是请求行,主要包含请求方法,URL,HTTP协议版本。第二行开始就是请求Header,请求Header后面会有一个空行,用于区分Header和请求内容。
+
+#### 请求行
+
+由请求方法字段、URL字段、协议版本字段三部分构成,它们之间由空格隔开。常用的请求方法有:GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。
+
+#### 请求头
+
+请求头由key/value对组成,每行为一对,key和value之间通过冒号(:)分割。请求头的作用主要用于通知服务端有关于客户端的请求信息。
+
+典型的请求头有:
+
+User-Agent:生成请求的浏览器类型
+
+Accept:客户端可识别的响应内容类型列表;星号`*` 用于按范围将类型分组。`*/*`表示可接受全部类型,`type/*`表示可接受type类型的所有子类型。
+
+Accept-Language: 客户端可接受的自然语言
+
+Accept-Encoding: 客户端可接受的编码压缩格式
+
+Accept-Charset: 可接受的字符集
+
+Host: 请求的主机名,允许多个域名绑定同一IP地址
+
+connection:连接方式(close或keeplive)
+
+Cookie: 存储在客户端的扩展字段
+
+#### 空行
+
+最后一个请求头之后就是空行,用于告诉服务端以下内容不再是请求头的内容了。
+
+#### 请求内容
+
+请求内容主要用于POST请求,与POST请求方法配套的请求头一般有Content-Type(标识请求内容的类型)和Content-Length(标识请求内容的长度)
+
+
+
+#### 响应报文
+
+
+
+HTTP响应报文由状态行、响应头、空行和响应内容4个部分构成。第一行是状态行,由HTTP协议版本,状态码,状态描述组成,第二行开始是响应头,响应头后面是一个空行,用于区分响应头和响应内容。
+
+#### 状态行
+
+由HTTP协议版本、状态码、状态码描述三部分构成,它们之间由空格隔开。
+
+状态码由3位数字组成,第一位标识响应的类型,常用的5大类状态码如下:
+
+1xx:表示服务器已接收了客户端的请求,客户端可以继续发送请求
+
+2xx:表示服务器已成功接收到请求并进行处理
+
+3xx:表示服务器要求客户端重定向
+
+4xx:表示客户端的请求有非法内容
+
+5xx:标识服务器未能正常处理客户端的请求而出现意外错误
+
+常见状态码说明:
+
+200 OK: 表示客户端请求成功
+
+304 Not Modified:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。
+
+400 Bad Request: 表示客户端请求有语法错误,不能被服务器端解析
+
+401 Unauthorized: 表示请求未经授权,该状态码必须与WWW-Authenticate报文头一起使用
+
+404 Not Found:请求的资源不存在,例如输入了错误的url
+
+500 Internal Server Error: 表示服务器发生了不可预期的错误,导致无法完成客户端的请求
+
+503 Service Unavailable:表示服务器当前不能处理客户端的请求,在一段时间后服务器可能恢复正常
+
+#### 响应头
+
+一般情况下,响应头会包含以下,甚至更多的信息。
+
+Location:服务器返回给客户端,用于重定向到新的位置
+
+Server: 包含服务器用来处理请求的软件信息及版本信息
+
+Vary:标识不可缓存的请求头列表
+
+Connection: 连接方式。
+
+对于请求端来讲:close是告诉服务端,断开连接,不用等待后续的求请了。keeplive则是告诉服务端,在完成本次请求的响应后,保持连接,等待本次连接后的后续请求。
+
+对于响应端来讲:close表示连接已经关闭。keeplive则表示连接保持中,可以继续处理后续请求。Keep-Alive表示如果请求端保持连接,则该请求头部信息表明期望服务端保持连接多长时间(秒),例如300秒,应该这样写Keep-Alive: 300
+
+#### 空行
+
+最后一个响应头之后就是空行,用于告诉请求端以下内容不再是响应头的内容了。
+
+#### 响应内容
+
+服务端返回给请求端的文本信息。
+
+下面是一个实际的例子:
+
+
+
+##### 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 3d60e69..95ae725 100644
--- a/docs/HashMap.md
+++ b/docs/HashMap.md
@@ -3,9 +3,11 @@
下面是主要是自己看了很多Java容器类相关的博客,以及很多面经中涉及到的Java容器相关的面试题后,自己全部手写的解答,也花了一些流程图,之后会继续更新这一部分。
#### [1.HashMap添加一个键值对的过程是怎么样的?](#HashMap添加一个键值对的过程是怎么样的?)
+
#### [2.ConcurrentHashMap添加一个键值对的过程是怎么样的?](#ConcurrentHashMap添加一个键值对的过程是怎么样的?)
#### [3.HashMap与HashTable,ConcurrentHashMap的区别是什么?](#HashMap与HashTable,ConcurrentHashMap的区别是什么?)
#### [4.HashMap扩容后是否需要rehash?](#HashMap扩容后是否需要rehash?)
+
#### [5.HashMap扩容是怎样扩容的,为什么都是2的N次幂的大小?](#HashMap扩容是怎样扩容的,为什么都是2的N次幂的大小?)
#### [6.ConcurrentHashMap是怎么记录元素个数size的?](#ConcurrentHashMap是怎么记录元素个数size的?)
#### [7.为什么ConcurrentHashMap,HashTable不支持key,value为null?](#为什么ConcurrentHashMap,HashTable不支持key,value为null?)
@@ -38,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的话把链表转换为红黑树。
@@ -64,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
@@ -148,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同步锁,然后进行以下判断:
@@ -210,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 ||
@@ -245,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,
@@ -265,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;
@@ -285,26 +290,27 @@ 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时,将红黑树转换为链表。之所以临界点为8是因为红黑树的查找时间复杂度为logN,链表的平均时间查找复杂度为N/2,当N为8时,logN为3,是小于N/2的,正好可以通过转换为红黑树减少查找的时间复杂度。
-
-#### Hashtable=数组+链表
-Hashtable底层数据结构跟HashMap一致,底层数据结构是一个数组+链表,也是通过链地址法来解决冲突,只是链表过长时,不会转换为红黑树来减少查找时的时间复杂度。Hashtable属于历史遗留类,实际开发中很少使用。
+HashMap的底层数据结构是一个数组+链表+红黑树,数组的每个元素存储是一个链表的头结点,链表中存储了一组哈希值冲突的键值对,通过链地址法来解决哈希冲突的。为了避免链表长度过长,影响查找元素的效率,当链表的长度>8时,会将链表转换为红黑树,链表的长度<6时,将红黑树转换为链表(但是红黑树转换为链表的时机不是在删除链表时,而是在扩容时,发现红黑树分解后的两个链表<6,就按链表处理,否则就建立两个小的红黑树,设置到扩容后的位置)。之所以临界点为8是因为红黑树的查找时间复杂度为logN,链表的平均时间查找复杂度为N/2,当N为8时,logN为3,是小于N/2的,正好可以通过转换为红黑树减少查找的时间复杂度。
#### ConcurrentHashMap=数组+链表+红黑树
ConcurrentHashMap底层数据结构跟HashMap一致,底层数据结构是一个数组+链表+红黑树。只不过使用了volatile来进行修饰它的属性,来保证内存可见性(一个线程修改了这些属性后,会使得其他线程中对于该属性的缓存失效,以便下次读取时取最新的值)。
+#### Hashtable=数组+链表
+Hashtable底层数据结构跟HashMap一致,底层数据结构是一个数组+链表,也是通过链地址法来解决冲突,只是链表过长时,不会转换为红黑树来减少查找时的时间复杂度。Hashtable属于历史遗留类,实际开发中很少使用。
+
#### 2.线程安全
##### HashMap 非线程安全
+
HashMap是非线程安全的。(例如多个线程插入多个键值对,如果两个键值对的key哈希冲突,可能会使得两个线程在操作同一个链表中的节点,导致一个键值对的value被覆盖)
##### HashTable 线程安全
@@ -339,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 ||
@@ -359,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,
@@ -376,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);
@@ -399,7 +405,7 @@ public synchronized V put(K key, V value) {
}
Entry<?,?> tab[] = table;
int hash = key.hashCode();
- ...其他代码
+ //...其他代码
}
```
@@ -417,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) {
@@ -437,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值的方法
@@ -449,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。
@@ -489,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,当前线程会进行辅助扩容或阻塞等待,扩容完成后去新数组中更新或插入元素)
#### 迁移元素
@@ -499,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,
@@ -580,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的区别?
@@ -792,4 +794,216 @@ abstract class HashIterator {
因为ArrayList和HashMap的Iterator都是上面所说的“fail-fast Iterator”,Iterator在获取下一个元素,删除元素时,都会比较expectedModCount和modCount,不一致就会抛出异常。
-所以当使用Iterator遍历元素(for-each遍历底层实现也是Iterator)时,需要删除元素,一定需要使用 **Iterator的remove()方法** 来删除,而不是直接调用ArrayList或HashMap自身的remove()方法,否则会导致Iterator中的expectedModCount没有及时更新,之后获取下一个元素或者删除元素时,expectedModCount和modCount不一致,然后抛出ConcurrentModificationException异常。
\ No newline at end of file
+所以当使用Iterator遍历元素(for-each遍历底层实现也是Iterator)时,需要删除元素,一定需要使用 **Iterator的remove()方法** 来删除,而不是直接调用ArrayList或HashMap自身的remove()方法,否则会导致Iterator中的expectedModCount没有及时更新,之后获取下一个元素或者删除元素时,expectedModCount和modCount不一致,然后抛出ConcurrentModificationException异常。
+
+### 谈一谈你对LinkedHashMap的理解?
+
+LinkedHashMap是HashMap的子类,与HashMap的实现基本一致,只是说在HashMap的基础上做了一些扩展,所有的节点都有一个before指针和after指针,根据插入顺序形成一个**双向链表**。默认accessOrder是false,也就是按照**插入顺序**来排序的,每次新插入的元素都是插入到链表的末尾。map.keySet().iterator().next()第一个元素是最早插入的元素的key。LinkedHashMap可以用来实现LRU算法。(accessOrder为true,会按照访问顺序来排序。)
+
+LRU算法实现:
+
+```java
+//使用LinkedHashMap实现LRU算法(accessOrder为false的实现方式)
+// LinkedHashMap默认的accessOrder为false,也就是会按照插入顺序排序,
+// 所以在插入新的键值对时,总是添加在队列尾部,
+// 如果是访问已存在的键值对,或者是put操作的键值对已存在,那么需要将键值对先移除再添加。
+public 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);
+ }
+}
+```
+**下面是另外一种实现方法:**
+
+```java
+//使用LinkedHashMap实现LRU算法(accessOrder为true的实现方式)
+//如果是将accessOrder设置为true,get和put已有键值对时就不需要删除key了
+public static class LRUCache2 {
+ int capacity;
+ LinkedHashMap