diff --git a/README.md b/README.md index 42788e54..f35ee455 100644 --- a/README.md +++ b/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 + +在对本作品进行演绎时,请署名并以相同方式共享。 + +知识共享许可协议 + ### Statement 本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.) diff --git a/notes/Java 并发.md b/notes/Java 并发.md index 89ab7cac..9cd5955b 100644 --- a/notes/Java 并发.md +++ b/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); +} +```

```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 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 状态,其它四个状态已经在前面介绍过了。 -

+

下图左侧是一个线程的虚拟机栈,其中有一部分称为 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,表示该对象处于轻量级锁状态。 -

+

如果 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。 diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md index 891211da..a4a4ba12 100644 --- a/notes/Java 虚拟机.md +++ b/notes/Java 虚拟机.md @@ -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 ``` ## 方法区 diff --git a/notes/MySQL.md b/notes/MySQL.md index 690425db..7ac9685f 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -129,7 +129,7 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 ### 1. 数据结构 -B Tree 指的是 Balance Tree,也就是平衡树。平衡树时一颗查找树,并且所有叶子节点位于同一层。 +B Tree 指的是 Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层。 B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。 diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index 0c9bd1f9..1bac6bbb 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -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 的查找区间如下:

-复杂度: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 inOrderNumsIndexs = new HashMap<>(); +// 缓存中序遍历数组每个值对应的索引 +private Map 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。

@@ -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) {

-因为 (x\*x)n/2 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 +因为 (x\*x)n/2 可以通过递归求解,并且每次递归 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) {

```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)

```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 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> 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> 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) { diff --git a/notes/算法.md b/notes/算法.md index cb5d4dbe..30467b42 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -2,19 +2,10 @@ * [一、前言](#一前言) * [二、算法分析](#二算法分析) * [数学模型](#数学模型) + * [注意事项](#注意事项) * [ThreeSum](#threesum) * [倍率实验](#倍率实验) - * [注意事项](#注意事项) -* [三、栈和队列](#三栈和队列) - * [栈](#栈) - * [队列](#队列) -* [四、并查集](#四并查集) - * [quick-find](#quick-find) - * [quick-union](#quick-union) - * [加权 quick-union](#加权-quick-union) - * [路径压缩的加权 quick-union](#路径压缩的加权-quick-union) - * [各种 union-find 算法的比较](#各种-union-find-算法的比较) -* [五、排序](#五排序) +* [三、排序](#三排序) * [选择排序](#选择排序) * [冒泡排序](#冒泡排序) * [插入排序](#插入排序) @@ -23,6 +14,15 @@ * [快速排序](#快速排序) * [堆排序](#堆排序) * [小结](#小结) +* [四、并查集](#四并查集) + * [Quick Find](#quick-find) + * [Quick Union](#quick-union) + * [加权 Quick Union](#加权-quick-union) + * [路径压缩的加权 Quick Union](#路径压缩的加权-quick-union) + * [比较](#比较) +* [五、栈和队列](#五栈和队列) + * [栈](#栈) + * [队列](#队列) * [六、查找](#六查找) * [初级实现](#初级实现) * [二叉查找树](#二叉查找树) @@ -39,7 +39,8 @@ # 一、前言 -本文实现代码以及测试代码放在 [Algorithm](https://github.com/CyC2018/Algorithm) +- 实现代码:[Algorithm](https://github.com/CyC2018/Algorithm) +- 绘图文件:[ProcessOn](https://www.processon.com/view/link/5a3e4c1ee4b0ce9ffea8c727) # 二、算法分析 @@ -61,6 +62,28 @@ N3/6-N2/2+N/3 的增长数量级为 O(N3)。增 使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 +## 注意事项 + +### 1. 大常数 + +在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 + +### 2. 缓存 + +计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 + +### 3. 对最坏情况下的性能的保证 + +在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 + +### 4. 随机化算法 + +通过打乱输入,去除算法对输入的依赖。 + +### 5. 均摊分析 + +将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 + ## ThreeSum ThreeSum 用于统计一个数组中和为 0 的三元组数量。 @@ -71,6 +94,10 @@ public interface ThreeSum { } ``` +### 1. ThreeSumSlow + +该算法的内循环为 `if (nums[i] + nums[j] + nums[k] == 0)` 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 O(N3)。 + ```java public class ThreeSumSlow implements ThreeSum { @Override @@ -87,9 +114,7 @@ public class ThreeSumSlow implements ThreeSum { } ``` -该算法的内循环为 `if (nums[i] + nums[j] + nums[k] == 0)` 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 O(N3)。 - - **改进**
+### 2. ThreeSumFast 通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 @@ -153,21 +178,31 @@ public class BinarySearch { ```java public class RatioTest { + public static void main(String[] args) { + int N = 500; int loopTimes = 7; double preTime = -1; + while (loopTimes-- > 0) { + int[] nums = new int[N]; + StopWatch.start(); + ThreeSum threeSum = new ThreeSumSlow(); + int cnt = threeSum.count(nums); System.out.println(cnt); + double elapsedTime = StopWatch.elapsedTime(); double ratio = preTime == -1 ? 0 : elapsedTime / preTime; System.out.println(N + " " + elapsedTime + " " + ratio); + preTime = elapsedTime; N *= 2; + } } } @@ -175,12 +210,15 @@ public class RatioTest { ```java public class StopWatch { + private static long start; - - public static void start(){ + + + public static void start() { start = System.currentTimeMillis(); } - + + public static double elapsedTime() { long now = System.currentTimeMillis(); return (now - start) / 1000.0; @@ -188,438 +226,7 @@ public class StopWatch { } ``` -## 注意事项 - -### 1. 大常数 - -在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 - -### 2. 缓存 - -计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 - -### 3. 对最坏情况下的性能的保证 - -在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 - -### 4. 随机化算法 - -通过打乱输入,去除算法对输入的依赖。 - -### 5. 均摊分析 - -将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作),均摊后每次操作访问数组的平均次数为常数。 - -# 三、栈和队列 - -## 栈 - -First-In-Last-Out - -```java -public interface MyStack extends Iterable { - MyStack push(Item item); - - Item pop() throws Exception; - - boolean isEmpty(); - - int size(); -} -``` - -### 1. 数组实现 - -```java -public class ArrayStack implements MyStack { - // 栈元素数组,只能通过转型来创建泛型数组 - private Item[] a = (Item[]) new Object[1]; - // 元素数量 - private int N = 0; - - @Override - public MyStack push(Item item) { - check(); - a[N++] = item; - return this; - } - - @Override - public Item pop() throws Exception { - if (isEmpty()) - throw new Exception("stack is empty"); - - Item item = a[--N]; - check(); - a[N] = null; // 避免对象游离 - return item; - } - - private void check() { - if (N >= a.length) - resize(2 * a.length); - else if (N > 0 && N <= a.length / 4) - resize(a.length / 2); - } - - /** - * 调整数组大小,使得栈具有伸缩性 - */ - private void resize(int size) { - Item[] tmp = (Item[]) new Object[size]; - for (int i = 0; i < N; i++) - tmp[i] = a[i]; - a = tmp; - } - - @Override - public boolean isEmpty() { - return N == 0; - } - - @Override - public int size() { - return N; - } - - @Override - public Iterator iterator() { - // 返回逆序遍历的迭代器 - return new Iterator() { - private int i = N; - - @Override - public boolean hasNext() { - return i > 0; - } - - @Override - public Item next() { - return a[--i]; - } - }; - } -} -``` - -### 2. 链表实现 - -需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素时就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 - -```java -public class ListStack implements MyStack { - private Node top = null; - private int N = 0; - - private class Node { - Item item; - Node next; - } - - @Override - public MyStack push(Item item) { - Node newTop = new Node(); - newTop.item = item; - newTop.next = top; - top = newTop; - N++; - return this; - } - - @Override - public Item pop() throws Exception { - if (isEmpty()) - throw new Exception("stack is empty"); - Item item = top.item; - top = top.next; - N--; - return item; - } - - @Override - public boolean isEmpty() { - return N == 0; - } - - @Override - public int size() { - return N; - } - - @Override - public Iterator iterator() { - return new Iterator() { - private Node cur = top; - - @Override - public boolean hasNext() { - return cur != null; - } - - @Override - public Item next() { - Item item = cur.item; - cur = cur.next; - return item; - } - }; - } -} -``` - -## 队列 - -First-In-First-Out - -下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 - -这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。 - -```java -public interface MyQueue extends Iterable { - int size(); - - boolean isEmpty(); - - MyQueue add(Item item); - - Item remove() throws Exception; -} -``` - -```java -public class ListQueue implements MyQueue { - private Node first; - private Node last; - int N = 0; - - private class Node { - Item item; - Node next; - } - - @Override - public boolean isEmpty() { - return N == 0; - } - - @Override - public int size() { - return N; - } - - @Override - public MyQueue add(Item item) { - Node newNode = new Node(); - newNode.item = item; - newNode.next = null; - if (isEmpty()) { - last = newNode; - first = newNode; - } else { - last.next = newNode; - last = newNode; - } - N++; - return this; - } - - @Override - public Item remove() throws Exception { - if (isEmpty()) - throw new Exception("queue is empty"); - Node node = first; - first = first.next; - N--; - if (isEmpty()) - last = null; - return node.item; - } - - @Override - public Iterator iterator() { - return new Iterator() { - Node cur = first; - - @Override - public boolean hasNext() { - return cur != null; - } - - @Override - public Item next() { - Item item = cur.item; - cur = cur.next; - return item; - } - }; - } -} -``` - -# 四、并查集 - -用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 - -

- -| 方法 | 描述 | -| :---: | :---: | -| UF(int N) | 构造一个大小为 N 的并查集 | -| void union(int p, int q) | 连接 p 和 q 节点 | -| int find(int p) | 查找 p 所在的连通分量 | -| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 | - -```java -public abstract class UF { - protected int[] id; - - public UF(int N) { - id = new int[N]; - for (int i = 0; i < N; i++) - id[i] = i; - } - - public boolean connected(int p, int q) { - return find(p) == find(q); - } - - public abstract int find(int p); - - public abstract void union(int p, int q); -} -``` - -## quick-find - -可以快速进行 find 操作,即可以快速判断两个节点是否连通。 - -同一连通分量的所有节点的 id 值相等。 - -但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 - -

- -```java -public class QuickFindUF extends UF { - public QuickFindUF(int N) { - super(N); - } - - @Override - public int find(int p) { - return id[p]; - } - - @Override - public void union(int p, int q) { - int pID = find(p); - int qID = find(q); - - if (pID == qID) - return; - - for (int i = 0; i < id.length; i++) - if (id[i] == pID) - id[i] = qID; - } -} - -``` - -## quick-union - -可以快速进行 union 操作,只需要修改一个节点的 id 值即可。 - -但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同,id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。 - -

- -```java -public class QuickUnionUF extends UF { - public QuickUnionUF(int N) { - super(N); - } - - @Override - public int find(int p) { - while (p != id[p]) - p = id[p]; - return p; - } - - @Override - public void union(int p, int q) { - int pRoot = find(p); - int qRoot = find(q); - if (pRoot != qRoot) - id[pRoot] = qRoot; - } -} -``` - -这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 - -

- -## 加权 quick-union - -为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 - -理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 - -

- -```java -public class WeightedQuickUnionUF extends UF { - - // 保存节点的数量信息 - private int[] sz; - - public WeightedQuickUnionUF(int N) { - super(N); - this.sz = new int[N]; - for (int i = 0; i < N; i++) - this.sz[i] = 1; - } - - @Override - public int find(int p) { - while (p != id[p]) - p = id[p]; - return p; - } - - @Override - public void union(int p, int q) { - int i = find(p); - int j = find(q); - if (i == j) return; - if (sz[i] < sz[j]) { - id[i] = j; - sz[j] += sz[i]; - } else { - id[j] = i; - sz[i] += sz[j]; - } - } -} -``` - -## 路径压缩的加权 quick-union - -在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 - -## 各种 union-find 算法的比较 - -| 算法 | union | find | -| :---: | :---: | :---: | -| quick-find | N | 1 | -| quick-union | 树高 | 树高 | -| 加权 quick-union | logN | logN | -| 路径压缩的加权 quick-union | 非常接近 1 | 非常接近 1 | - -# 五、排序 +# 三、排序 待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 @@ -648,41 +255,49 @@ public abstract class Sort> { 选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 -

+选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 + +

```java public class Selection> extends Sort { + @Override public void sort(T[] nums) { int N = nums.length; for (int i = 0; i < N; i++) { int min = i; - for (int j = i + 1; j < N; j++) - if (less(nums[j], nums[min])) + for (int j = i + 1; j < N; j++) { + if (less(nums[j], nums[min])) { min = j; + } + } swap(nums, i, min); } } } ``` -选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 - ## 冒泡排序 -通过从左到右不断交换相邻逆序的相邻元素,在一轮的交换之后,可以让未排序的元素上浮到右侧。 +从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。 在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。 +以下演示了在一轮循环中,将最大的元素 5 上浮到最右侧。 + +

+ ```java public class Bubble> extends Sort { + @Override public void sort(T[] nums) { int N = nums.length; boolean hasSorted = false; - for (int i = 0; i < N && !hasSorted; i++) { + for (int i = N - 1; i > 0 && !hasSorted; i--) { hasSorted = true; - for (int j = 0; j < N - i - 1; j++) { + for (int j = 0; j < i; j++) { if (less(nums[j + 1], nums[j])) { hasSorted = false; swap(nums, j, j + 1); @@ -695,23 +310,7 @@ public class Bubble> extends Sort { ## 插入排序 -插入排序从左到右进行,每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左部数组依然有序。 - -第 j 元素是通过不断向左比较并交换来实现插入过程:当第 j 元素小于第 j - 1 元素,就将它们的位置交换,然后令 j 指针向左移动一个位置,不断进行以上操作。 - -

- -```java -public class Insertion> extends Sort { - @Override - public void sort(T[] nums) { - int N = nums.length; - for (int i = 1; i < N; i++) - for (int j = i; j > 0 && less(nums[j], nums[j - 1]); j--) - swap(nums, j, j - 1); - } -} -``` +每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左侧数组依然有序。 对于数组 {3, 5, 2, 4, 1},它具有以下逆序:(3, 2), (3, 1), (5, 2), (5, 4), (5, 1), (2, 1), (4, 1),插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。 @@ -721,6 +320,25 @@ public class Insertion> extends Sort { - 最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是倒序的; - 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。 +以下演示了在一轮循环中,将元素 2 插入到左侧已经排序的数组中。 + +

+ +```java +public class Insertion> extends Sort { + + @Override + public void sort(T[] nums) { + int N = nums.length; + for (int i = 1; i < N; i++) { + for (int j = i; j > 0 && less(nums[j], nums[j - 1]); j--) { + swap(nums, j, j - 1); + } + } + } +} +``` + ## 希尔排序 对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。 @@ -729,25 +347,32 @@ public class Insertion> extends Sort { 希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 -

+

```java public class Shell> extends Sort { + @Override public void sort(T[] nums) { + int N = nums.length; int h = 1; - while (h < N / 3) - h = 3 * h + 1; // 1, 4, 13, 40, ... + + while (h < N / 3) { + h = 3 * h + 1; // 1, 4, 13, 40, ... + } while (h >= 1) { - for (int i = h; i < N; i++) - for (int j = i; j >= h && less(nums[j], nums[j - h]); j -= h) + for (int i = h; i < N; i++) { + for (int j = i; j >= h && less(nums[j], nums[j - h]); j -= h) { swap(nums, j, j - h); + } + } h = h / 3; } } } + ``` 希尔排序的运行时间达不到平方级别,使用递增序列 1, 4, 13, 40, ... 的希尔排序所需要的比较次数不会超过 N 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 @@ -756,7 +381,7 @@ public class Shell> extends Sort { 归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。 -

+

### 1. 归并方法 @@ -767,21 +392,28 @@ public abstract class MergeSort> extends Sort { protected T[] aux; + protected void merge(T[] nums, int l, int m, int h) { + int i = l, j = m + 1; - for (int k = l; k <= h; k++) - aux[k] = nums[k]; // 将数据复制到辅助数组 + for (int k = l; k <= h; k++) { + aux[k] = nums[k]; // 将数据复制到辅助数组 + } for (int k = l; k <= h; k++) { - if (i > m) + if (i > m) { nums[k] = aux[j++]; - else if (j > h) + + } else if (j > h) { nums[k] = aux[i++]; - else if (aux[i].compareTo(nums[j]) <= 0) - nums[k] = aux[i++]; // 先进行这一步,保证稳定性 - else + + } else if (aux[i].compareTo(nums[j]) <= 0) { + nums[k] = aux[i++]; // 先进行这一步,保证稳定性 + + } else { nums[k] = aux[j++]; + } } } } @@ -789,10 +421,13 @@ public abstract class MergeSort> extends Sort { ### 2. 自顶向下归并排序 -

+将一个大数组分成两个小数组去求解。 + +因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlogN),因此该归并排序方法的时间复杂度也为 O(NlogN)。 ```java public class Up2DownMergeSort> extends MergeSort { + @Override public void sort(T[] nums) { aux = (T[]) new Comparable[nums.length]; @@ -800,8 +435,9 @@ public class Up2DownMergeSort> extends MergeSort { } private void sort(T[] nums, int l, int h) { - if (h <= l) + if (h <= l) { return; + } int mid = l + (h - l) / 2; sort(nums, l, mid); sort(nums, mid + 1, h); @@ -810,23 +446,27 @@ public class Up2DownMergeSort> extends MergeSort { } ``` -因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlogN),因此该归并排序方法的时间复杂度也为 O(NlogN)。 - ### 3. 自底向上归并排序 先归并那些微型数组,然后成对归并得到的微型数组。 ```java public class Down2UpMergeSort> extends MergeSort { + @Override public void sort(T[] nums) { + int N = nums.length; aux = (T[]) new Comparable[N]; - for (int sz = 1; sz < N; sz += sz) - for (int lo = 0; lo < N - sz; lo += sz + sz) + + for (int sz = 1; sz < N; sz += sz) { + for (int lo = 0; lo < N - sz; lo += sz + sz) { merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); + } + } } } + ``` ## 快速排序 @@ -836,10 +476,11 @@ public class Down2UpMergeSort> extends MergeSort { - 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序; - 快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 -

+

```java public class QuickSort> extends Sort { + @Override public void sort(T[] nums) { shuffle(nums); @@ -864,9 +505,9 @@ public class QuickSort> extends Sort { ### 2. 切分 -取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和 a[j] 交换位置。 +取 a[l] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素。不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[l] 和 a[j] 交换位置。 -

+

```java private int partition(T[] nums, int l, int h) { @@ -910,20 +551,23 @@ private int partition(T[] nums, int l, int h) { ```java public class ThreeWayQuickSort> extends QuickSort { + @Override protected void sort(T[] nums, int l, int h) { - if (h <= l) + if (h <= l) { return; + } int lt = l, i = l + 1, gt = h; T v = nums[l]; while (i <= gt) { int cmp = nums[i].compareTo(v); - if (cmp < 0) + if (cmp < 0) { swap(nums, lt++, i++); - else if (cmp > 0) + } else if (cmp > 0) { swap(nums, i, gt--); - else + } else { i++; + } } sort(nums, l, lt - 1); sort(nums, gt + 1, h); @@ -937,24 +581,28 @@ public class ThreeWayQuickSort> extends QuickSort { 可以利用这个特性找出数组的第 k 个元素。 +该算法是线性级别的,因为每次能将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 + ```java public T select(T[] nums, int k) { int l = 0, h = nums.length - 1; while (h > l) { int j = partition(nums, l, h); - if (j == k) + + if (j == k) { return nums[k]; - else if (j > k) + + } else if (j > k) { h = j - 1; - else + + } else { l = j + 1; + } } return nums[k]; } ``` -该算法是线性级别的。因为每次能将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 - ## 堆排序 ### 1. 堆 @@ -1122,14 +770,14 @@ public class HeapSort> extends Sort { | 算法 | 稳定 | 时间复杂度 | 空间复杂度 | 备注 | | :---: | :---: |:---: | :---: | :---: | -| 选择排序 | no | N2 | 1 | | -| 冒泡排序 | yes | N2 | 1 | | -| 插入排序 | yes | N \~ N2 | 1 | 时间复杂度和初始顺序有关 | -| 希尔排序 | no | N 的若干倍乘于递增序列的长度 | 1 | | -| 快速排序 | no | NlogN | logN | | -| 三向切分快速排序 | no | N \~ NlogN | logN | 适用于有大量重复主键| -| 归并排序 | yes | NlogN | N | | -| 堆排序 | no | NlogN | 1 | | | +| 选择排序 | √| N2 | 1 | | +| 冒泡排序 | √ | N2 | 1 | | +| 插入排序 | √ | N \~ N2 | 1 | 时间复杂度和初始顺序有关 | +| 希尔排序 | × | N 的若干倍乘于递增序列的长度 | 1 | | +| 快速排序 | × | NlogN | logN | | +| 三向切分快速排序 | × | N \~ NlogN | logN | 适用于有大量重复主键| +| 归并排序 | √ | NlogN | N | | +| 堆排序 | × | NlogN | 1 | | | 快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 @@ -1137,6 +785,511 @@ public class HeapSort> extends Sort { Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 +# 四、并查集 + +用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 + +

+ +| 方法 | 描述 | +| :---: | :---: | +| UF(int N) | 构造一个大小为 N 的并查集 | +| void union(int p, int q) | 连接 p 和 q 节点 | +| int find(int p) | 查找 p 所在的连通分量 | +| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 | + +```java +public abstract class UF { + + protected int[] id; + + public UF(int N) { + id = new int[N]; + for (int i = 0; i < N; i++) { + id[i] = i; + } + } + + public boolean connected(int p, int q) { + return find(p) == find(q); + } + + public abstract int find(int p); + + public abstract void union(int p, int q); +} +``` + +## Quick Find + +可以快速进行 find 操作,即可以快速判断两个节点是否连通。 + +需要保证同一连通分量的所有节点的 id 值相等。 + +但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 + +

+ +```java +public class QuickFindUF extends UF { + + public QuickFindUF(int N) { + super(N); + } + + + @Override + public int find(int p) { + return id[p]; + } + + + @Override + public void union(int p, int q) { + + int pID = find(p); + int qID = find(q); + + if (pID == qID) { + return; + } + + for (int i = 0; i < id.length; i++) { + if (id[i] == pID) { + id[i] = qID; + } + } + } +} +``` + +## Quick Union + +可以快速进行 union 操作,只需要修改一个节点的 id 值即可。 + +但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同,id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。 + +

+ +```java +public class QuickUnionUF extends UF { + + public QuickUnionUF(int N) { + super(N); + } + + + @Override + public int find(int p) { + + while (p != id[p]) { + p = id[p]; + } + return p; + } + + + @Override + public void union(int p, int q) { + + int pRoot = find(p); + int qRoot = find(q); + + if (pRoot != qRoot) { + id[pRoot] = qRoot; + } + } +} +``` + +这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 + +

+ +## 加权 Quick Union + +为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 + +理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 + +

+ +```java +public class WeightedQuickUnionUF extends UF { + + // 保存节点的数量信息 + private int[] sz; + + + public WeightedQuickUnionUF(int N) { + super(N); + this.sz = new int[N]; + for (int i = 0; i < N; i++) { + this.sz[i] = 1; + } + } + + + @Override + public int find(int p) { + while (p != id[p]) { + p = id[p]; + } + return p; + } + + + @Override + public void union(int p, int q) { + + int i = find(p); + int j = find(q); + + if (i == j) return; + + if (sz[i] < sz[j]) { + id[i] = j; + sz[j] += sz[i]; + } else { + id[j] = i; + sz[i] += sz[j]; + } + } +} +``` + +## 路径压缩的加权 Quick Union + +在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 + +## 比较 + +| 算法 | union | find | +| :---: | :---: | :---: | +| Quick Find | N | 1 | +| Quick Union | 树高 | 树高 | +| 加权 Quick Union | logN | logN | +| 路径压缩的加权 Quick Union | 非常接近 1 | 非常接近 1 | + +# 五、栈和队列 + +## 栈 + +```java +public interface MyStack extends Iterable { + + MyStack push(Item item); + + Item pop() throws Exception; + + boolean isEmpty(); + + int size(); + +} +``` + +### 1. 数组实现 + +```java +public class ArrayStack implements MyStack { + + // 栈元素数组,只能通过转型来创建泛型数组 + private Item[] a = (Item[]) new Object[1]; + + // 元素数量 + private int N = 0; + + + @Override + public MyStack push(Item item) { + check(); + a[N++] = item; + return this; + } + + + @Override + public Item pop() throws Exception { + + if (isEmpty()) { + throw new Exception("stack is empty"); + } + + Item item = a[--N]; + + check(); + + // 避免对象游离 + a[N] = null; + + return item; + } + + + private void check() { + + if (N >= a.length) { + resize(2 * a.length); + + } else if (N > 0 && N <= a.length / 4) { + resize(a.length / 2); + } + } + + + /** + * 调整数组大小,使得栈具有伸缩性 + */ + private void resize(int size) { + + Item[] tmp = (Item[]) new Object[size]; + + for (int i = 0; i < N; i++) { + tmp[i] = a[i]; + } + + a = tmp; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public Iterator iterator() { + + // 返回逆序遍历的迭代器 + return new Iterator() { + + private int i = N; + + @Override + public boolean hasNext() { + return i > 0; + } + + @Override + public Item next() { + return a[--i]; + } + }; + + } +} +``` + +### 2. 链表实现 + +需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素时就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 + +```java +public class ListStack implements MyStack { + + private Node top = null; + private int N = 0; + + + private class Node { + Item item; + Node next; + } + + + @Override + public MyStack push(Item item) { + + Node newTop = new Node(); + + newTop.item = item; + newTop.next = top; + + top = newTop; + + N++; + + return this; + } + + + @Override + public Item pop() throws Exception { + + if (isEmpty()) { + throw new Exception("stack is empty"); + } + + Item item = top.item; + + top = top.next; + N--; + + return item; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public Iterator iterator() { + + return new Iterator() { + + private Node cur = top; + + + @Override + public boolean hasNext() { + return cur != null; + } + + + @Override + public Item next() { + Item item = cur.item; + cur = cur.next; + return item; + } + }; + + } +} +``` + +## 队列 + +First-In-First-Out + +下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 + +这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。 + +```java +public interface MyQueue extends Iterable { + + int size(); + + boolean isEmpty(); + + MyQueue add(Item item); + + Item remove() throws Exception; +} +``` + +```java +public class ListQueue implements MyQueue { + + private Node first; + private Node last; + int N = 0; + + + private class Node { + Item item; + Node next; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public MyQueue add(Item item) { + + Node newNode = new Node(); + newNode.item = item; + newNode.next = null; + + if (isEmpty()) { + last = newNode; + first = newNode; + } else { + last.next = newNode; + last = newNode; + } + + N++; + return this; + } + + + @Override + public Item remove() throws Exception { + + if (isEmpty()) { + throw new Exception("queue is empty"); + } + + Node node = first; + first = first.next; + N--; + + if (isEmpty()) { + last = null; + } + + return node.item; + } + + + @Override + public Iterator iterator() { + + return new Iterator() { + + Node cur = first; + + + @Override + public boolean hasNext() { + return cur != null; + } + + + @Override + public Item next() { + Item item = cur.item; + cur = cur.next; + return item; + } + }; + } +} +``` + + + + + # 六、查找 符号表(Symbol Table)是一种存储键值对的数据结构,可以支持快速查找操作。 diff --git a/pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png b/pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png new file mode 100644 index 00000000..fc0999f9 Binary files /dev/null and b/pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png differ diff --git a/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png b/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png new file mode 100644 index 00000000..c4592305 Binary files /dev/null and b/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png differ diff --git a/pics/220790c6-4377-4a2e-8686-58398afc8a18.png b/pics/220790c6-4377-4a2e-8686-58398afc8a18.png new file mode 100644 index 00000000..79105257 Binary files /dev/null and b/pics/220790c6-4377-4a2e-8686-58398afc8a18.png differ diff --git a/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png b/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png new file mode 100644 index 00000000..a97e49a6 Binary files /dev/null and b/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png differ diff --git a/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png b/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png new file mode 100644 index 00000000..3b05b25b Binary files /dev/null and b/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png differ diff --git a/pics/864bfa7d-1149-420c-a752-f9b3d4e782ec.png b/pics/864bfa7d-1149-420c-a752-f9b3d4e782ec.png new file mode 100644 index 00000000..72cc988f Binary files /dev/null and b/pics/864bfa7d-1149-420c-a752-f9b3d4e782ec.png differ diff --git a/pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png b/pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png new file mode 100644 index 00000000..86e2294f Binary files /dev/null and b/pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png differ