12
README.md
@ -179,12 +179,6 @@
|
||||
|
||||
欢迎在 Issue 中提交对本仓库的改进建议~
|
||||
|
||||
### Authorization
|
||||
|
||||
虽然没有加开源协议,但是允许非商业性使用。
|
||||
|
||||
转载使用请注明出处,谢谢!
|
||||
|
||||
### Typesetting
|
||||
|
||||
笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/) 进行排版,以保证内容的可读性。
|
||||
@ -201,6 +195,12 @@
|
||||
|
||||
笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github,或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。
|
||||
|
||||
### License
|
||||
|
||||
在对本作品进行演绎时,请署名并以相同方式共享。
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a>
|
||||
|
||||
### Statement
|
||||
|
||||
本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.)
|
||||
|
108
notes/Java 并发.md
@ -733,7 +733,6 @@ java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.
|
||||
|
||||
```java
|
||||
public class CountdownLatchExample {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
final int totalThread = 10;
|
||||
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
|
||||
@ -759,15 +758,30 @@ run..run..run..run..run..run..run..run..run..run..end
|
||||
|
||||
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
|
||||
|
||||
和 CountdownLatch 相似,都是通过维护计数器来实现的。但是它的计数器是递增的,每次执行 await() 方法之后,计数器会加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。和 CountdownLatch 的另一个区别是,CyclicBarrier 的计数器可以循环使用,所以它才叫做循环屏障。
|
||||
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 awati() 方法而在等待的线程才能继续执行。
|
||||
|
||||
下图应该从下往上看才正确。
|
||||
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
|
||||
|
||||
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
|
||||
|
||||
```java
|
||||
public CyclicBarrier(int parties, Runnable barrierAction) {
|
||||
if (parties <= 0) throw new IllegalArgumentException();
|
||||
this.parties = parties;
|
||||
this.count = parties;
|
||||
this.barrierCommand = barrierAction;
|
||||
}
|
||||
|
||||
public CyclicBarrier(int parties) {
|
||||
this(parties, null);
|
||||
}
|
||||
```
|
||||
|
||||
<div align="center"> <img src="../pics//CyclicBarrier.png" width=""/> </div><br>
|
||||
|
||||
```java
|
||||
public class CyclicBarrierExample {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
public static void main(String[] args) {
|
||||
final int totalThread = 10;
|
||||
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
|
||||
ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
@ -776,9 +790,7 @@ public class CyclicBarrierExample {
|
||||
System.out.print("before..");
|
||||
try {
|
||||
cyclicBarrier.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
} catch (BrokenBarrierException e) {
|
||||
} catch (InterruptedException | BrokenBarrierException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.print("after..");
|
||||
@ -1198,8 +1210,6 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即
|
||||
|
||||
上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
|
||||
|
||||
主要有以下这些原则:
|
||||
|
||||
### 1. 单一线程原则
|
||||
|
||||
> Single Thread rule
|
||||
@ -1270,14 +1280,16 @@ Thread 对象的结束先行发生于 join() 方法返回。
|
||||
|
||||
### 1. 不可变
|
||||
|
||||
不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施,只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。
|
||||
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。
|
||||
|
||||
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
|
||||
|
||||
不可变的类型:
|
||||
|
||||
- final 关键字修饰的基本数据类型;
|
||||
- final 关键字修饰的基本数据类型
|
||||
- String
|
||||
- 枚举类型
|
||||
- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。
|
||||
- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
|
||||
|
||||
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
|
||||
|
||||
@ -1305,21 +1317,19 @@ public V put(K key, V value) {
|
||||
}
|
||||
```
|
||||
|
||||
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
|
||||
|
||||
### 2. 绝对线程安全
|
||||
|
||||
不管运行时环境如何,调用者都不需要任何额外的同步措施。
|
||||
|
||||
### 3. 相对线程安全
|
||||
|
||||
相对的线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
|
||||
相对线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
|
||||
|
||||
在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
|
||||
|
||||
对于下面的代码,如果删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
|
||||
对于下面的代码,如果删除元素的线程删除了 Vector 的一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
|
||||
|
||||
```java
|
||||
```Java
|
||||
public class VectorUnsafeExample {
|
||||
private static Vector<Integer> vector = new Vector<>();
|
||||
|
||||
@ -1389,15 +1399,17 @@ synchronized 和 ReentrantLock。
|
||||
|
||||
### 2. 非阻塞同步
|
||||
|
||||
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
|
||||
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
|
||||
|
||||
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
|
||||
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
|
||||
|
||||
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
|
||||
**(一)CAS**
|
||||
|
||||
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
|
||||
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
|
||||
|
||||
硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
|
||||
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
|
||||
|
||||
**(二)AtomicInteger**
|
||||
|
||||
J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
|
||||
|
||||
@ -1419,7 +1431,7 @@ public final int incrementAndGet() {
|
||||
}
|
||||
```
|
||||
|
||||
以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值 ==var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
|
||||
以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
|
||||
|
||||
可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
|
||||
|
||||
@ -1434,23 +1446,19 @@ public final int getAndAddInt(Object var1, long var2, int var4) {
|
||||
}
|
||||
```
|
||||
|
||||
ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
|
||||
**(三)ABA**
|
||||
|
||||
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
|
||||
|
||||
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
|
||||
|
||||
### 3. 无同步方案
|
||||
|
||||
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。
|
||||
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
|
||||
|
||||
**(一)可重入代码(Reentrant Code)**
|
||||
**(一)栈封闭**
|
||||
|
||||
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
|
||||
|
||||
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
|
||||
|
||||
**(二)栈封闭**
|
||||
|
||||
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。
|
||||
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
|
||||
|
||||
```java
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@ -1482,11 +1490,11 @@ public static void main(String[] args) {
|
||||
100
|
||||
```
|
||||
|
||||
**(三)线程本地存储(Thread Local Storage)**
|
||||
**(二)线程本地存储(Thread Local Storage)**
|
||||
|
||||
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
|
||||
|
||||
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
|
||||
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
|
||||
|
||||
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
|
||||
|
||||
@ -1584,17 +1592,25 @@ public T get() {
|
||||
}
|
||||
```
|
||||
|
||||
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
|
||||
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
|
||||
|
||||
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
|
||||
|
||||
**(三)可重入代码(Reentrant Code)**
|
||||
|
||||
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
|
||||
|
||||
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
|
||||
|
||||
# 十二、锁优化
|
||||
|
||||
这里的锁优化主要是指虚拟机对 synchronized 的优化。
|
||||
这里的锁优化主要是指 JVM 对 synchronized 的优化。
|
||||
|
||||
## 自旋锁
|
||||
|
||||
互斥同步的进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
|
||||
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
|
||||
|
||||
自选锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
|
||||
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
|
||||
|
||||
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
|
||||
|
||||
@ -1624,7 +1640,7 @@ public static String concatString(String s1, String s2, String s3) {
|
||||
}
|
||||
```
|
||||
|
||||
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会“逃逸”到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
|
||||
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
|
||||
|
||||
## 锁粗化
|
||||
|
||||
@ -1636,9 +1652,9 @@ public static String concatString(String s1, String s2, String s3) {
|
||||
|
||||
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
|
||||
|
||||
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
|
||||
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
|
||||
|
||||
<div align="center"> <img src="../pics//bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="600"/> </div><br>
|
||||
<div align="center"> <img src="../pics//bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="500"/> </div><br>
|
||||
|
||||
下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。
|
||||
|
||||
@ -1646,9 +1662,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
|
||||
|
||||
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
|
||||
|
||||
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
|
||||
当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。
|
||||
|
||||
<div align="center"> <img src="../pics//baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="500"/> </div><br>
|
||||
<div align="center"> <img src="../pics//baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="400"/> </div><br>
|
||||
|
||||
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
|
||||
|
||||
@ -1666,9 +1682,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
|
||||
|
||||
- 给线程起个有意义的名字,这样可以方便找 Bug。
|
||||
|
||||
- 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。
|
||||
- 缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。
|
||||
|
||||
- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂的控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
|
||||
- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
|
||||
|
||||
- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
||||
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
|
||||
|
||||
```java
|
||||
java -Xss=512M HackTheJava
|
||||
java -Xss512M HackTheJava
|
||||
```
|
||||
|
||||
该区域可能抛出以下异常:
|
||||
@ -81,7 +81,7 @@ java -Xss=512M HackTheJava
|
||||
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
|
||||
|
||||
```java
|
||||
java -Xms=1M -Xmx=2M HackTheJava
|
||||
java -Xms1M -Xmx2M HackTheJava
|
||||
```
|
||||
|
||||
## 方法区
|
||||
|
@ -129,7 +129,7 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提
|
||||
|
||||
### 1. 数据结构
|
||||
|
||||
B Tree 指的是 Balance Tree,也就是平衡树。平衡树时一颗查找树,并且所有叶子节点位于同一层。
|
||||
B Tree 指的是 Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层。
|
||||
|
||||
B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。
|
||||
|
||||
|
@ -10,8 +10,8 @@
|
||||
* [9. 用两个栈实现队列](#9-用两个栈实现队列)
|
||||
* [10.1 斐波那契数列](#101-斐波那契数列)
|
||||
* [10.2 跳台阶](#102-跳台阶)
|
||||
* [10.3 变态跳台阶](#103-变态跳台阶)
|
||||
* [10.4 矩形覆盖](#104-矩形覆盖)
|
||||
* [10.3 矩形覆盖](#103-矩形覆盖)
|
||||
* [10.4 变态跳台阶](#104-变态跳台阶)
|
||||
* [11. 旋转数组的最小数字](#11-旋转数组的最小数字)
|
||||
* [12. 矩阵中的路径](#12-矩阵中的路径)
|
||||
* [13. 机器人的运动范围](#13-机器人的运动范围)
|
||||
@ -58,8 +58,8 @@
|
||||
* [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置)
|
||||
* [51. 数组中的逆序对](#51-数组中的逆序对)
|
||||
* [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点)
|
||||
* [53 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数)
|
||||
* [54. 二叉搜索树的第 K 个结点](#54-二叉搜索树的第-k-个结点)
|
||||
* [53. 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数)
|
||||
* [54. 二叉查找树的第 K 个结点](#54-二叉查找树的第-k-个结点)
|
||||
* [55.1 二叉树的深度](#551-二叉树的深度)
|
||||
* [55.2 平衡二叉树](#552-平衡二叉树)
|
||||
* [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字)
|
||||
@ -112,11 +112,11 @@ Output:
|
||||
|
||||
要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。
|
||||
|
||||
这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。
|
||||
这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上。
|
||||
|
||||
以 (2, 3, 1, 0, 2, 5) 为例:
|
||||
|
||||
```text-html-basic
|
||||
```text
|
||||
position-0 : (2,3,1,0,2,5) // 2 <-> 1
|
||||
(1,3,2,0,2,5) // 1 <-> 3
|
||||
(3,1,2,0,2,5) // 3 <-> 0
|
||||
@ -146,7 +146,9 @@ public boolean duplicate(int[] nums, int length, int[] duplication) {
|
||||
}
|
||||
|
||||
private void swap(int[] nums, int i, int j) {
|
||||
int t = nums[i]; nums[i] = nums[j]; nums[j] = t;
|
||||
int t = nums[i];
|
||||
nums[i] = nums[j];
|
||||
nums[j] = t;
|
||||
}
|
||||
```
|
||||
|
||||
@ -176,12 +178,12 @@ Given target = 20, return false.
|
||||
|
||||
从右上角开始查找。矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。
|
||||
|
||||
复杂度:O(M + N) + O(1)
|
||||
|
||||
当前元素的查找区间为左下角的所有元素,例如元素 12 的查找区间如下:
|
||||
|
||||
<div align="center"> <img src="../pics//f94389e9-55b1-4f49-9d37-00ed05900ae0.png" width="250"/> </div><br>
|
||||
|
||||
复杂度:O(M + N) + O(1)
|
||||
|
||||
```java
|
||||
public boolean Find(int target, int[][] matrix) {
|
||||
if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
|
||||
@ -193,7 +195,7 @@ public boolean Find(int target, int[][] matrix) {
|
||||
return true;
|
||||
else if (target > matrix[r][c])
|
||||
r++;
|
||||
else
|
||||
else
|
||||
c--;
|
||||
}
|
||||
return false;
|
||||
@ -206,26 +208,33 @@ public boolean Find(int target, int[][] matrix) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为 We Are Happy. 则经过替换之后的字符串为 We%20Are%20Happy。
|
||||
|
||||
将一个字符串中的空格替换成 "%20"。
|
||||
|
||||
```text
|
||||
Input:
|
||||
"We Are Happy"
|
||||
|
||||
Output:
|
||||
"We%20Are%20Happy"
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
在字符串尾部填充任意字符,使得字符串的长度等于字符串替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。
|
||||
在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。
|
||||
|
||||
令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。
|
||||
|
||||
从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。
|
||||
|
||||
复杂度:O(N) + O(1)
|
||||
|
||||
```java
|
||||
public String replaceSpace(StringBuffer str) {
|
||||
int oldLen = str.length();
|
||||
for (int i = 0; i < oldLen; i++)
|
||||
int P1 = str.length() - 1;
|
||||
for (int i = 0; i < str.length(); i++)
|
||||
if (str.charAt(i) == ' ')
|
||||
str.append(" ");
|
||||
|
||||
int P1 = oldLen - 1, P2 = str.length() - 1;
|
||||
int P2 = str.length() - 1;
|
||||
while (P1 >= 0 && P2 > P1) {
|
||||
char c = str.charAt(P1--);
|
||||
if (c == ' ') {
|
||||
@ -345,23 +354,23 @@ inorder = [9,3,15,20,7]
|
||||
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
|
||||
|
||||
```java
|
||||
// 缓存中序遍历数组的每个值对应的索引
|
||||
private Map<Integer, Integer> inOrderNumsIndexs = new HashMap<>();
|
||||
// 缓存中序遍历数组每个值对应的索引
|
||||
private Map<Integer, Integer> indexForInOrders = new HashMap<>();
|
||||
|
||||
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
|
||||
for (int i = 0; i < in.length; i++)
|
||||
inOrderNumsIndexs.put(in[i], i);
|
||||
return reConstructBinaryTree(pre, 0, pre.length - 1, 0, in.length - 1);
|
||||
indexForInOrders.put(in[i], i);
|
||||
return reConstructBinaryTree(pre, 0, pre.length - 1, 0);
|
||||
}
|
||||
|
||||
private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL, int inR) {
|
||||
private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) {
|
||||
if (preL > preR)
|
||||
return null;
|
||||
TreeNode root = new TreeNode(pre[preL]);
|
||||
int inIndex = inOrderNumsIndexs.get(root.val);
|
||||
int inIndex = indexForInOrders.get(root.val);
|
||||
int leftTreeSize = inIndex - inL;
|
||||
root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL, inL + leftTreeSize - 1);
|
||||
root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1, inR);
|
||||
root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL);
|
||||
root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
@ -456,7 +465,7 @@ public int pop() throws Exception {
|
||||
|
||||
## 题目描述
|
||||
|
||||
求菲波那契数列的第 n 项,n <= 39。
|
||||
求斐波那契数列的第 n 项,n <= 39。
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?f(n)=\left\{\begin{array}{rcl}0&&{n=0}\\1&&{n=1}\\f(n-1)+f(n-2)&&{n>1}\end{array}\right."/></div> <br>
|
||||
|
||||
@ -526,23 +535,6 @@ public class Solution {
|
||||
|
||||
## 解题思路
|
||||
|
||||
复杂度:O(N) + O(N)
|
||||
|
||||
```java
|
||||
public int JumpFloor(int n) {
|
||||
if (n == 1)
|
||||
return 1;
|
||||
int[] dp = new int[n];
|
||||
dp[0] = 1;
|
||||
dp[1] = 2;
|
||||
for (int i = 2; i < n; i++)
|
||||
dp[i] = dp[i - 1] + dp[i - 2];
|
||||
return dp[n - 1];
|
||||
}
|
||||
```
|
||||
|
||||
复杂度:O(N) + O(1)
|
||||
|
||||
```java
|
||||
public int JumpFloor(int n) {
|
||||
if (n <= 2)
|
||||
@ -558,7 +550,32 @@ public int JumpFloor(int n) {
|
||||
}
|
||||
```
|
||||
|
||||
# 10.3 变态跳台阶
|
||||
# 10.3 矩形覆盖
|
||||
|
||||
[NowCoder](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||
|
||||
## 题目描述
|
||||
|
||||
我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法?
|
||||
|
||||
## 解题思路
|
||||
|
||||
```java
|
||||
public int RectCover(int n) {
|
||||
if (n <= 2)
|
||||
return n;
|
||||
int pre2 = 1, pre1 = 2;
|
||||
int result = 0;
|
||||
for (int i = 3; i <= n; i++) {
|
||||
result = pre2 + pre1;
|
||||
pre2 = pre1;
|
||||
pre1 = result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
# 10.4 变态跳台阶
|
||||
|
||||
[NowCoder](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&tqId=11162&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||
|
||||
@ -579,47 +596,6 @@ public int JumpFloorII(int target) {
|
||||
}
|
||||
```
|
||||
|
||||
# 10.4 矩形覆盖
|
||||
|
||||
[NowCoder](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||
|
||||
## 题目描述
|
||||
|
||||
我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法?
|
||||
|
||||
## 解题思路
|
||||
|
||||
复杂度:O(N) + O(N)
|
||||
|
||||
```java
|
||||
public int RectCover(int n) {
|
||||
if (n <= 2)
|
||||
return n;
|
||||
int[] dp = new int[n];
|
||||
dp[0] = 1;
|
||||
dp[1] = 2;
|
||||
for (int i = 2; i < n; i++)
|
||||
dp[i] = dp[i - 1] + dp[i - 2];
|
||||
return dp[n - 1];
|
||||
}
|
||||
```
|
||||
|
||||
复杂度:O(N) + O(1)
|
||||
|
||||
```java
|
||||
public int RectCover(int n) {
|
||||
if (n <= 2)
|
||||
return n;
|
||||
int pre2 = 1, pre1 = 2;
|
||||
int result = 0;
|
||||
for (int i = 3; i <= n; i++) {
|
||||
result = pre2 + pre1;
|
||||
pre2 = pre1;
|
||||
pre1 = result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
# 11. 旋转数组的最小数字
|
||||
|
||||
@ -629,18 +605,34 @@ public int RectCover(int n) {
|
||||
|
||||
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
|
||||
|
||||
例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。
|
||||
例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。
|
||||
|
||||
## 解题思路
|
||||
|
||||
在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。
|
||||
|
||||
本题可以修改二分查找算法进行求解:
|
||||
|
||||
- 当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m;
|
||||
- 否则解在 [m + 1, h] 之间,令 l = m + 1。
|
||||
|
||||
因为 h 的赋值表达式为 h = m,因此循环体的循环条件应该为 l < h,详细解释请见 [Leetcode 题解](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md) 二分查找部分。
|
||||
```java
|
||||
public int minNumberInRotateArray(int[] nums) {
|
||||
if (nums.length == 0)
|
||||
return 0;
|
||||
int l = 0, h = nums.length - 1;
|
||||
while (l < h) {
|
||||
int m = l + (h - l) / 2;
|
||||
if (nums[m] <= nums[h])
|
||||
h = m;
|
||||
else
|
||||
l = m + 1;
|
||||
}
|
||||
return nums[l];
|
||||
}
|
||||
```
|
||||
|
||||
但是如果出现 nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。
|
||||
|
||||
复杂度:O(logN) + O(1)
|
||||
如果数组元素允许重复的话,那么就会出现一个特殊的情况:nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。
|
||||
|
||||
```java
|
||||
public int minNumberInRotateArray(int[] nums) {
|
||||
@ -728,7 +720,9 @@ private char[][] buildMatrix(char[] array) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格(35, 37),因为 3+5+3+7=18。但是,它不能进入方格(35, 38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?
|
||||
地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。
|
||||
|
||||
例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,37),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -797,7 +791,7 @@ return 36 (10 = 3 + 3 + 4)
|
||||
|
||||
### 贪心
|
||||
|
||||
尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。
|
||||
尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。
|
||||
|
||||
证明:当 n >= 5 时,3(n - 3) - 2(n - 2) = n - 5 >= 0。因此把长度大于 5 的绳子切成两段,令其中一段长度为 3 可以使得两段的乘积最大。
|
||||
|
||||
@ -838,19 +832,9 @@ public int integerBreak(int n) {
|
||||
|
||||
输入一个整数,输出该数二进制表示中 1 的个数。
|
||||
|
||||
### Integer.bitCount()
|
||||
|
||||
```java
|
||||
public int NumberOf1(int n) {
|
||||
return Integer.bitCount(n);
|
||||
}
|
||||
```
|
||||
|
||||
### n&(n-1)
|
||||
|
||||
O(M) 时间复杂度解法,其中 M 表示 1 的个数。
|
||||
|
||||
该位运算是去除 n 的位级表示中最低的那一位。
|
||||
该位运算去除 n 的位级表示中最低的那一位。
|
||||
|
||||
```
|
||||
n : 10110100
|
||||
@ -858,6 +842,9 @@ n-1 : 10110011
|
||||
n&(n-1) : 10110000
|
||||
```
|
||||
|
||||
时间复杂度:O(M),其中 M 表示 1 的个数。
|
||||
|
||||
|
||||
```java
|
||||
public int NumberOf1(int n) {
|
||||
int cnt = 0;
|
||||
@ -869,13 +856,22 @@ public int NumberOf1(int n) {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Integer.bitCount()
|
||||
|
||||
```java
|
||||
public int NumberOf1(int n) {
|
||||
return Integer.bitCount(n);
|
||||
}
|
||||
```
|
||||
|
||||
# 16. 数值的整数次方
|
||||
|
||||
[NowCoder](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent。求 base 的 exponent 次方。
|
||||
给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -883,7 +879,7 @@ public int NumberOf1(int n) {
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?x^n=\left\{\begin{array}{rcl}(x*x)^{n/2}&&{n\%2=0}\\x*(x*x)^{n/2}&&{n\%2=1}\end{array}\right."/></div> <br>
|
||||
|
||||
因为 (x\*x)<sup>n/2</sup> 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logN)。
|
||||
因为 (x\*x)<sup>n/2</sup> 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。
|
||||
|
||||
```java
|
||||
public double Power(double base, int exponent) {
|
||||
@ -1019,6 +1015,7 @@ public ListNode deleteDuplication(ListNode pHead) {
|
||||
|
||||
```java
|
||||
public boolean match(char[] str, char[] pattern) {
|
||||
|
||||
int m = str.length, n = pattern.length;
|
||||
boolean[][] dp = new boolean[m + 1][n + 1];
|
||||
|
||||
@ -1049,9 +1046,24 @@ public boolean match(char[] str, char[] pattern) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
|
||||
```html
|
||||
true
|
||||
|
||||
"+100"
|
||||
"5e2"
|
||||
"-123"
|
||||
"3.1416"
|
||||
"-1E-16"
|
||||
|
||||
false
|
||||
|
||||
"12e"
|
||||
"1a3.14"
|
||||
"1.2.3"
|
||||
"+-5"
|
||||
"12e+4.3"
|
||||
```
|
||||
|
||||
例如,字符串 "+100","5e2","-123","3.1416" 和 "-1E-16" 都表示数值。但是 "12e","1a3.14","1.2.3","+-5" 和 "12e+4.3" 都不是。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -1115,8 +1127,7 @@ public void reOrderArray(int[] nums) {
|
||||
<div align="center"> <img src="../pics//ea2304ce-268b-4238-9486-4d8f8aea8ca4.png" width="500"/> </div><br>
|
||||
|
||||
```java
|
||||
public ListNode FindKthToTail(ListNode head, int k)
|
||||
{
|
||||
public ListNode FindKthToTail(ListNode head, int k) {
|
||||
if (head == null)
|
||||
return null;
|
||||
ListNode P1 = head;
|
||||
@ -1139,9 +1150,7 @@ public ListNode FindKthToTail(ListNode head, int k)
|
||||
|
||||
## 题目描述
|
||||
|
||||
一个链表中包含环,请找出该链表的环的入口结点。
|
||||
|
||||
要求不能使用额外的空间。
|
||||
一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -1152,8 +1161,7 @@ public ListNode FindKthToTail(ListNode head, int k)
|
||||
<div align="center"> <img src="../pics//2858f8ad-aedb-45a5-a706-e98c96d690fa.jpg" width="600"/> </div><br>
|
||||
|
||||
```java
|
||||
public ListNode EntryNodeOfLoop(ListNode pHead)
|
||||
{
|
||||
public ListNode EntryNodeOfLoop(ListNode pHead) {
|
||||
if (pHead == null || pHead.next == null)
|
||||
return null;
|
||||
ListNode slow = pHead, fast = pHead;
|
||||
@ -1295,8 +1303,6 @@ private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) {
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 递归
|
||||
|
||||
```java
|
||||
public void Mirror(TreeNode root) {
|
||||
if (root == null)
|
||||
@ -1313,29 +1319,6 @@ private void swap(TreeNode root) {
|
||||
}
|
||||
```
|
||||
|
||||
### 迭代
|
||||
|
||||
```java
|
||||
public void Mirror(TreeNode root) {
|
||||
Stack<TreeNode> stack = new Stack<>();
|
||||
stack.push(root);
|
||||
while (!stack.isEmpty()) {
|
||||
TreeNode node = stack.pop();
|
||||
if (node == null)
|
||||
continue;
|
||||
swap(node);
|
||||
stack.push(node.left);
|
||||
stack.push(node.right);
|
||||
}
|
||||
}
|
||||
|
||||
private void swap(TreeNode node) {
|
||||
TreeNode t = node.left;
|
||||
node.left = node.right;
|
||||
node.right = t;
|
||||
}
|
||||
```
|
||||
|
||||
# 28 对称的二叉树
|
||||
|
||||
[NowCder](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||
@ -1436,7 +1419,9 @@ public int min() {
|
||||
|
||||
## 题目描述
|
||||
|
||||
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。
|
||||
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。
|
||||
|
||||
例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -1829,7 +1814,7 @@ private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) {
|
||||
|
||||
多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。
|
||||
|
||||
使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。
|
||||
使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素相等时,令 cnt++,否则令 cnt--。如果前面查找了 i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。
|
||||
|
||||
```java
|
||||
public int MoreThanHalfNum_Solution(int[] nums) {
|
||||
@ -2259,7 +2244,7 @@ public int GetUglyNumber_Solution(int N) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
在一个字符串 (1 <= 字符串长度 <= 10000,全部由字母组成) 中找到第一个只出现一次的字符,并返回它的位置。
|
||||
在一个字符串 中找到第一个只出现一次的字符,并返回它的位置。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2277,7 +2262,7 @@ public int FirstNotRepeatingChar(String str) {
|
||||
}
|
||||
```
|
||||
|
||||
以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么我们只需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。
|
||||
以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。
|
||||
|
||||
```java
|
||||
public int FirstNotRepeatingChar2(String str) {
|
||||
@ -2304,13 +2289,13 @@ public int FirstNotRepeatingChar2(String str) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数 P。
|
||||
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
|
||||
|
||||
## 解题思路
|
||||
|
||||
```java
|
||||
private long cnt = 0;
|
||||
private int[] tmp; // 在这里创建辅助数组,而不是在 merge() 递归函数中创建
|
||||
private int[] tmp; // 在这里声明辅助数组,而不是在 merge() 递归函数中声明
|
||||
|
||||
public int InversePairs(int[] nums) {
|
||||
tmp = new int[nums.length];
|
||||
@ -2359,7 +2344,7 @@ private void merge(int[] nums, int l, int m, int h) {
|
||||
|
||||
设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。
|
||||
|
||||
当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
|
||||
当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。
|
||||
|
||||
```java
|
||||
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
|
||||
@ -2372,7 +2357,7 @@ public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
|
||||
}
|
||||
```
|
||||
|
||||
# 53 数字在排序数组中出现的次数
|
||||
# 53. 数字在排序数组中出现的次数
|
||||
|
||||
[NowCoder](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&tqId=11190&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||
|
||||
@ -2380,8 +2365,9 @@ public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
|
||||
|
||||
```html
|
||||
Input:
|
||||
1, 2, 3, 3, 3, 3, 4, 6
|
||||
3
|
||||
nums = 1, 2, 3, 3, 3, 3, 4, 6
|
||||
K = 3
|
||||
|
||||
Output:
|
||||
4
|
||||
```
|
||||
@ -2408,13 +2394,13 @@ private int binarySearch(int[] nums, int K) {
|
||||
}
|
||||
```
|
||||
|
||||
# 54. 二叉搜索树的第 K 个结点
|
||||
# 54. 二叉查找树的第 K 个结点
|
||||
|
||||
[NowCoder](https://www.nowcoder.com/practice/ef068f602dde4d28aab2b210e859150a?tpId=13&tqId=11215&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
|
||||
|
||||
## 解题思路
|
||||
|
||||
利用二叉搜索数中序遍历有序的特点。
|
||||
利用二叉查找树中序遍历有序的特点。
|
||||
|
||||
```java
|
||||
private TreeNode ret;
|
||||
@ -2520,7 +2506,7 @@ public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S,如果有多对数字的和等于 S,输出两个数的乘积最小的。
|
||||
输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2596,9 +2582,13 @@ public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
输入:"I am a student."
|
||||
```html
|
||||
Input:
|
||||
"I am a student."
|
||||
|
||||
输出:"student. a am I"
|
||||
Output:
|
||||
"student. a am I"
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2640,7 +2630,14 @@ private void swap(char[] c, int i, int j) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
对于一个给定的字符序列 S,请你把其循环左移 K 位后的序列输出。例如,字符序列 S=”abcXYZdef”, 要求输出循环左移 3 位后的结果,即“XYZdefabc”。
|
||||
```html
|
||||
Input:
|
||||
S="abcXYZdef"
|
||||
K=3
|
||||
|
||||
Output:
|
||||
"XYZdefabc"
|
||||
```
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2675,7 +2672,9 @@ private void swap(char[] chars, int i, int j) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。
|
||||
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。
|
||||
|
||||
例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2774,7 +2773,7 @@ public List<Map.Entry<Integer, Double>> dicesSum(int n) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
五张牌,其中大小鬼为癞子,牌面大小为 0。判断是否能组成顺子。
|
||||
五张牌,其中大小鬼为癞子,牌面大小为 0。判断这五张牌是否能组成顺子。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2803,7 +2802,7 @@ public boolean isContinuous(int[] nums) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
让小朋友们围成一个大圈。然后,他随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。
|
||||
让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2813,7 +2812,7 @@ public boolean isContinuous(int[] nums) {
|
||||
public int LastRemaining_Solution(int n, int m) {
|
||||
if (n == 0) /* 特殊输入的处理 */
|
||||
return -1;
|
||||
if (n == 1) /* 返回条件 */
|
||||
if (n == 1) /* 递归返回条件 */
|
||||
return 0;
|
||||
return (LastRemaining_Solution(n - 1, m) + m) % n;
|
||||
}
|
||||
@ -2851,7 +2850,7 @@ public int maxProfit(int[] prices) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
求 1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句(A?B:C)。
|
||||
要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 A ? B : C。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2859,7 +2858,7 @@ public int maxProfit(int[] prices) {
|
||||
|
||||
条件与 && 具有短路原则,即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性,将递归的返回条件取非然后作为 && 的第一个条件语句,递归的主体转换为第二个条件语句,那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分,递归返回。
|
||||
|
||||
以下实现中,递归的返回条件为 n <= 0,取非后就是 n > 0,递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。
|
||||
本题的递归返回条件为 n <= 0,取非后就是 n > 0;递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。
|
||||
|
||||
```java
|
||||
public int Sum_Solution(int n) {
|
||||
@ -2875,7 +2874,7 @@ public int Sum_Solution(int n) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
写一个函数,求两个整数之和,要求在函数体内不得使用 +、-、\*、/ 四则运算符号。
|
||||
写一个函数,求两个整数之和,要求不得使用 +、-、\*、/ 四则运算符号。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2895,7 +2894,7 @@ public int Add(int a, int b) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
给定一个数组 A[0, 1,..., n-1], 请构建一个数组 B[0, 1,..., n-1], 其中 B 中的元素 B[i]=A[0]\*A[1]\*...\*A[i-1]\*A[i+1]\*...\*A[n-1]。不能使用除法。
|
||||
给定一个数组 A[0, 1,..., n-1],请构建一个数组 B[0, 1,..., n-1],其中 B 中的元素 B[i]=A[0]\*A[1]\*...\*A[i-1]\*A[i+1]\*...\*A[n-1]。要求不能使用除法。
|
||||
|
||||
## 解题思路
|
||||
|
||||
@ -2917,7 +2916,7 @@ public int[] multiply(int[] A) {
|
||||
|
||||
## 题目描述
|
||||
|
||||
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为 0 或者字符串不是一个合法的数值则返回 0。
|
||||
将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 0,要求不能使用字符串转换整数的库函数。
|
||||
|
||||
```html
|
||||
Iuput:
|
||||
@ -2979,7 +2978,7 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
|
||||
|
||||
[Leetcode : 236. Lowest Common Ancestor of a Binary Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/)
|
||||
|
||||
在左右子树中查找是否存在 p 或者 q,如果 p 和 q 分别在两个子树中,那么就说明根节点就是 LCA。
|
||||
在左右子树中查找是否存在 p 或者 q,如果 p 和 q 分别在两个子树中,那么就说明根节点就是最低公共祖先。
|
||||
|
||||
```java
|
||||
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
|
||||
|
1181
notes/算法.md
BIN
pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
pics/220790c6-4377-4a2e-8686-58398afc8a18.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
pics/37e79a32-95a9-4503-bdb1-159527e628b8.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
pics/864bfa7d-1149-420c-a752-f9b3d4e782ec.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png
Normal file
After Width: | Height: | Size: 12 KiB |