diff --git a/README.md b/README.md index e0ebf6dc..25b5c533 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@


- 本项目包含了技术面试必备的基础知识,内容浅显易懂,你不需要花很长的时间去阅读和理解成堆的技术书籍就可以快速掌握这些知识,从而节省宝贵的面试复习时间。推荐使用 https://cyc2018.github.io/CS-Notes 进行阅读,从而获得更好的阅读体验。你也可以订阅 面试进阶指南,包含了学习指导和面试技巧,让你更轻松拿到满意的 Offer。

欢迎关注公众号“CyC2018”,每天发布一道高频基础知识面试题,让你在闲暇时间也能学习进步!公众号也提供了一个学习打卡圈子,记录你每天的学习收获,见证你的成长!

+ 本项目包含了技术面试必备的基础知识,内容浅显易懂,你不需要花很长的时间去阅读和理解成堆的技术书籍就可以快速掌握这些知识,从而节省宝贵的面试复习时间。推荐使用 https://cyc2018.github.io/CS-Notes 进行阅读,从而获得更好的阅读体验。你也可以订阅 面试进阶指南,包含了学习指导和面试技巧,让你更轻松拿到满意的 Offer。

欢迎关注公众号“CyC2018”,这里有最核心的高频基础知识面试题,后台回复“ziliao”更能领取复习大纲,帮你理清复习重点。

@@ -21,18 +21,18 @@ ## :pencil2: 算法 -- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指%20offer%20题解.md) -- [Leetcode 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Leetcode%20题解.md) -- [算法](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/算法.md) +- [剑指 Offer 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/剑指%20Offer%20题解%20-%20目录.md) +- [Leetcode 题解](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Leetcode%20题解%20-%20目录.md) +- [算法](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/算法%20-%20目录.md) ## :computer: 操作系统 -- [计算机操作系统](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/计算机操作系统.md) +- [计算机操作系统](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/计算机操作系统%20-%20目录.md) - [Linux](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Linux.md) ## :cloud: 网络 -- [计算机网络](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/计算机网络.md) +- [计算机网络](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/计算机网络%20-%20目录.md) - [HTTP](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/HTTP.md) - [Socket](https://github.com/CyC2018/CS-Notes/blob/master/docs/notes/Socket.md) diff --git a/docs/README.md b/docs/README.md index 435ac1e4..2a6f995c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,19 +2,19 @@ ## ✏️ 算法 -- [剑指 Offer 题解](notes/剑指%20offer%20题解.md)
-- [Leetcode 题解](notes/Leetcode%20题解)
-- [算法](notes/算法.md)
+- [剑指 Offer 题解](notes/剑指%20Offer%20题解%20-%20目录1.md)
+- [Leetcode 题解](notes/Leetcode%20题解%20-%20目录1.md)
+- [算法](notes/算法%20-%20目录1.md)
- [点击订阅面试进阶指南](https://xiaozhuanlan.com/CyC2018) ## 💻 操作系统 -- [计算机操作系统](notes/计算机操作系统.md)
+- [计算机操作系统](notes/计算机操作系统%20-%20目录1.md)
- [Linux](notes/Linux.md) ## ☁️ 网络 -- [计算机网络](notes/计算机网络.md)
+- [计算机网络](notes/计算机网络%20-%20目录1.md)
- [HTTP](notes/HTTP.md)
- [Socket](notes/Socket.md) @@ -55,6 +55,8 @@ - [正则表达式](notes/正则表达式.md)
- [构建工具](notes/构建工具.md) -欢迎关注 公众号 “CyC2018” ,每天发布一道高频基础知识面试题,让你在闲暇时间也能学习进步!公众号也提供了一个学习打卡圈子,记录你每天的学习收获,见证你的成长! -![](https://cyc-1256109796.cos.ap-guangzhou.myqcloud.com/%E5%85%AC%E4%BC%97%E5%8F%B7.jpg) + +欢迎关注公众号“CyC2018”,这里有最核心的高频基础知识面试题,后台回复“ziliao”更能领取复习大纲,帮你理清复习重点。 + + diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 671d1495..6906cd0d 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -2,9 +2,11 @@ # CS-Notes -- 本项目包含了技术面试必备的基础知识,内容浅显易懂,你不需要花很长的时间去阅读和理解成堆的技术书籍就可以快速掌握这些知识,从而节省宝贵的面试复习时间。你也可以订阅 面试进阶指南,包含了学习指导和面试技巧,让你更轻松拿到满意的 Offer。欢迎关注 公众号“CyC2018” ,每天发布一道高频基础知识面试题,让你在闲暇时间也能学习进步!公众号也提供了一个学习打卡圈子,记录你每天的学习收获,见证你的成长! +- 本项目包含了技术面试必备的基础知识,内容浅显易懂,你不需要花很长的时间去阅读和理解成堆的技术书籍就可以快速掌握这些知识,从而节省宝贵的面试复习时间。 -Site View : + + +[![stars](https://badgen.net/github/stars/CyC2018/CS-Notes?icon=github&color=4ab8a1)](https://github.com/CyC2018/CS-Notes) [![forks](https://badgen.net/github/forks/CyC2018/CS-Notes?icon=github&color=4ab8a1)](https://github.com/CyC2018/CS-Notes) [Get Started](README.md) diff --git a/docs/index.html b/docs/index.html index 72c1d9bf..17b35152 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,7 +7,7 @@ - + @@ -34,15 +34,30 @@ margin: 2rem 0 1rem; } + img, + pre { + border-radius: 8px; + } + .content, .sidebar, .markdown-section, body, - .search input, - .sidebar-toggle { + .search input { background-color: rgba(243, 242, 238, 1) !important; } + @media (min-width:600px) { + .sidebar-toggle { + background-color: #f3f2ee; + } + } + + .docsify-copy-code-button { + background: #f8f8f8 !important; + color: #7a7a7a !important; + } + body { /*font-family: Microsoft YaHei, Source Sans Pro, Helvetica Neue, Arial, sans-serif !important;*/ } diff --git a/docs/notes/Docker.md b/docs/notes/Docker.md index acbdd6f5..b0945c54 100644 --- a/docs/notes/Docker.md +++ b/docs/notes/Docker.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、解决的问题](#一解决的问题) * [二、与虚拟机的比较](#二与虚拟机的比较) @@ -90,3 +89,9 @@ Docker 轻量级的特点使得它很适合用于部署、维护、组合微服 - [What is Docker](https://www.docker.com/what-docker) - [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Git.md b/docs/notes/Git.md index 6cd8a829..a728f27f 100644 --- a/docs/notes/Git.md +++ b/docs/notes/Git.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [集中式与分布式](#集中式与分布式) * [中心服务器](#中心服务器) @@ -159,3 +158,9 @@ $ ssh-keygen -t rsa -C "youremail@example.com" - [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html) - [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000) - [Learn Git Branching](https://learngitbranching.js.org/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/HTTP.md b/docs/notes/HTTP.md index 92f1846b..3bcdafb8 100644 --- a/docs/notes/HTTP.md +++ b/docs/notes/HTTP.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一 、基础概念](#一-基础概念) * [URI](#uri) @@ -878,3 +877,9 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 - [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences) - [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2) - [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Java IO.md b/docs/notes/Java IO.md index 1c634d1d..fdcae096 100644 --- a/docs/notes/Java IO.md +++ b/docs/notes/Java IO.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概览](#一概览) * [二、磁盘操作](#二磁盘操作) @@ -619,3 +618,9 @@ NIO 与普通 I/O 的区别主要有以下两点: - [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499) - [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document) - [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Java 基础.md b/docs/notes/Java 基础.md index 06f40fc4..0072eec9 100644 --- a/docs/notes/Java 基础.md +++ b/docs/notes/Java 基础.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、数据类型](#一数据类型) * [基本类型](#基本类型) @@ -54,7 +53,7 @@ - double/64 - boolean/\~ -boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 并不支持 boolean 数组,而是使用 byte 数组来表示 int 数组来表示。 +boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 并不直接支持 boolean 数组,而是使用 byte 数组来表示 int 数组。 - [Primitive Data Types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) - [The Java® Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf) @@ -1395,3 +1394,9 @@ Java 注解是附加在代码中的一些元信息,用于一些工具在编译 - Eckel B. Java 编程思想[M]. 机械工业出版社, 2002. - Bloch J. Effective java[M]. Addison-Wesley Professional, 2017. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Java 容器.md b/docs/notes/Java 容器.md index c62bb02b..2accf9f8 100644 --- a/docs/notes/Java 容器.md +++ b/docs/notes/Java 容器.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概览](#一概览) * [Collection](#collection) @@ -1112,3 +1111,9 @@ public final class ConcurrentCache { - [Java 集合细节(二):asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html) - [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Java 并发.md b/docs/notes/Java 并发.md index 394d6189..4f1850d0 100644 --- a/docs/notes/Java 并发.md +++ b/docs/notes/Java 并发.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、线程状态转换](#一线程状态转换) * [新建(New)](#新建new) @@ -1635,3 +1634,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态: - [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example") - [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/) - [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Java 虚拟机.md b/docs/notes/Java 虚拟机.md index b2a5fe69..efa5ca5a 100644 --- a/docs/notes/Java 虚拟机.md +++ b/docs/notes/Java 虚拟机.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、运行时数据区域](#一运行时数据区域) * [程序计数器](#程序计数器) @@ -740,3 +739,9 @@ public class FileSystemClassLoader extends ClassLoader { - [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6) - [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap) - [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 二分查找.md b/docs/notes/Leetcode 题解 - 二分查找.md new file mode 100644 index 00000000..5183c0ef --- /dev/null +++ b/docs/notes/Leetcode 题解 - 二分查找.md @@ -0,0 +1,300 @@ + +* [原理](#原理) + * [1. 正常实现](#1-正常实现) + * [2. 时间复杂度](#2-时间复杂度) + * [3. m 计算](#3-m-计算) + * [4. 返回值](#4-返回值) + * [5. 变种](#5-变种) +* [例题](#例题) + * [1. 求开方](#1-求开方) + * [2. 大于给定元素的最小元素](#2-大于给定元素的最小元素) + * [3. 有序数组的 Single Element](#3-有序数组的-single-element) + * [4. 第一个错误的版本](#4-第一个错误的版本) + * [5. 旋转数组的最小数字](#5-旋转数组的最小数字) + * [6. 查找区间](#6-查找区间) + + + +# 原理 + +## 1. 正常实现 + +```java +public int binarySearch(int[] nums, int key) { + int l = 0, h = nums.length - 1; + while (l <= h) { + int m = l + (h - l) / 2; + if (nums[m] == key) { + return m; + } else if (nums[m] > key) { + h = m - 1; + } else { + l = m + 1; + } + } + return -1; +} +``` + +## 2. 时间复杂度 + +二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。 + +## 3. m 计算 + +有两种计算中值 m 的方式: + +- m = (l + h) / 2 +- m = l + (h - l) / 2 + +l + h 可能出现加法溢出,最好使用第二种方式。 + +## 4. 返回值 + +循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值: + +- -1:以一个错误码表示没有查找到 key +- l:将 key 插入到 nums 中的正确位置 + +## 5. 变种 + +二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下: + +```java +public int binarySearch(int[] nums, int key) { + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= key) { + h = m; + } else { + l = m + 1; + } + } + return l; +} +``` + +该实现和正常实现有以下不同: + +- 循环条件为 l < h +- h 的赋值表达式为 h = m +- 最后返回 l 而不是 -1 + +在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。 + +在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况: + +```text +nums = {0, 1, 2}, key = 1 +l m h +0 1 2 nums[m] >= key +0 0 1 nums[m] < key +1 1 1 nums[m] >= key +1 1 1 nums[m] >= key +... +``` + +当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。 + +# 例题 + +## 1. 求开方 + +[69. Sqrt(x) (Easy)](https://leetcode.com/problems/sqrtx/description/) + +```html +Input: 4 +Output: 2 + +Input: 8 +Output: 2 +Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated. +``` + +一个数 x 的开方 sqrt 一定在 0 \~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 \~ x 之间查找 sqrt。 + +对于 x = 8,它的开方是 2.82842...,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。 + +```java +public int mySqrt(int x) { + if (x <= 1) { + return x; + } + int l = 1, h = x; + while (l <= h) { + int mid = l + (h - l) / 2; + int sqrt = x / mid; + if (sqrt == mid) { + return mid; + } else if (mid > sqrt) { + h = mid - 1; + } else { + l = mid + 1; + } + } + return h; +} +``` + +## 2. 大于给定元素的最小元素 + +[744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/) + +```html +Input: +letters = ["c", "f", "j"] +target = "d" +Output: "f" + +Input: +letters = ["c", "f", "j"] +target = "k" +Output: "c" +``` + +题目描述:给定一个有序的字符数组 letters 和一个字符 target,要求找出 letters 中大于 target 的最小字符,如果找不到就返回第 1 个字符。 + +```java +public char nextGreatestLetter(char[] letters, char target) { + int n = letters.length; + int l = 0, h = n - 1; + while (l <= h) { + int m = l + (h - l) / 2; + if (letters[m] <= target) { + l = m + 1; + } else { + h = m - 1; + } + } + return l < n ? letters[l] : letters[0]; +} +``` + +## 3. 有序数组的 Single Element + +[540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) + +```html +Input: [1, 1, 2, 3, 3, 4, 4, 8, 8] +Output: 2 +``` + +题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。 + +令 index 为 Single Element 在数组中的位置。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。 + +从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。 + +因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。 + +```java +public int singleNonDuplicate(int[] nums) { + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (m % 2 == 1) { + m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数 + } + if (nums[m] == nums[m + 1]) { + l = m + 2; + } else { + h = m; + } + } + return nums[l]; +} +``` + +## 4. 第一个错误的版本 + +[278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/) + +题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。 + +如果第 m 个版本出错,则表示第一个错误的版本在 [l, m] 之间,令 h = m;否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。 + +因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。 + +```java +public int firstBadVersion(int n) { + int l = 1, h = n; + while (l < h) { + int mid = l + (h - l) / 2; + if (isBadVersion(mid)) { + h = mid; + } else { + l = mid + 1; + } + } + return l; +} +``` + +## 5. 旋转数组的最小数字 + +[153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) + +```html +Input: [3,4,5,1,2], +Output: 1 +``` + +```java +public int findMin(int[] nums) { + 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]; +} +``` + +## 6. 查找区间 + +[34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/) + +```html +Input: nums = [5,7,7,8,8,10], target = 8 +Output: [3,4] + +Input: nums = [5,7,7,8,8,10], target = 6 +Output: [-1,-1] +``` + +```java +public int[] searchRange(int[] nums, int target) { + int first = binarySearch(nums, target); + int last = binarySearch(nums, target + 1) - 1; + if (first == nums.length || nums[first] != target) { + return new int[]{-1, -1}; + } else { + return new int[]{first, Math.max(first, last)}; + } +} + +private int binarySearch(int[] nums, int target) { + int l = 0, h = nums.length; // 注意 h 的初始值 + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= target) { + h = m; + } else { + l = m + 1; + } + } + return l; +} +``` + + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 位运算.md b/docs/notes/Leetcode 题解 - 位运算.md new file mode 100644 index 00000000..c4769f54 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 位运算.md @@ -0,0 +1,433 @@ + +* [原理](#原理) + * [1. 基本原理](#1-基本原理) + * [2. mask 计算](#2-mask-计算) + * [3. Java 中的位操作](#3-java-中的位操作) +* [例题](#例题) + * [统计两个数的二进制表示有多少位不同](#统计两个数的二进制表示有多少位不同) + * [数组中唯一一个不重复的元素](#数组中唯一一个不重复的元素) + * [找出数组中缺失的那个数](#找出数组中缺失的那个数) + * [数组中不重复的两个元素](#数组中不重复的两个元素) + * [翻转一个数的比特位](#翻转一个数的比特位) + * [不用额外变量交换两个整数](#不用额外变量交换两个整数) + * [判断一个数是不是 2 的 n 次方](#判断一个数是不是-2-的-n-次方) + * [判断一个数是不是 4 的 n 次方](#判断一个数是不是-4-的-n-次方) + * [判断一个数的位级表示是否不会出现连续的 0 和 1](#判断一个数的位级表示是否不会出现连续的-0-和-1) + * [求一个数的补码](#求一个数的补码) + * [实现整数的加法](#实现整数的加法) + * [字符串数组最大乘积](#字符串数组最大乘积) + * [统计从 0 \~ n 每个数的二进制表示中 1 的个数](#统计从-0-\~-n-每个数的二进制表示中-1-的个数) + + + +# 原理 + +## 1. 基本原理 + +0s 表示一串 0,1s 表示一串 1。 + +``` +x ^ 0s = x x & 0s = 0 x | 0s = x +x ^ 1s = ~x x & 1s = x x | 1s = 1s +x ^ x = 0 x & x = x x | x = x +``` + +- 利用 x ^ 1s = \~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。 +- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。 +- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。 + +位与运算技巧: + +- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100,减去 1 得到 10110011,这两个数相与得到 10110000。 +- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110100,-n 得到 01001100,相与得到 00000100。 +- n-n&(\~n+1) 去除 n 的位级表示中最高的那一位。 + +移位运算: + +- \>\> n 为算术右移,相当于除以 2n; +- \>\>\> n 为无符号右移,左边会补上 0。 +- << n 为算术左移,相当于乘以 2n。 + +## 2. mask 计算 + +要获取 111111111,将 0 取反即可,\~0。 + +要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。 + +要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。 + +要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 \~(1<<(i+1)-1)。 + +## 3. Java 中的位操作 + +```html +static int Integer.bitCount(); // 统计 1 的数量 +static int Integer.highestOneBit(); // 获得最高位 +static String toBinaryString(int i); // 转换为二进制表示的字符串 +``` + +# 例题 + +## 统计两个数的二进制表示有多少位不同 + +[461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/) + +```html +Input: x = 1, y = 4 + +Output: 2 + +Explanation: +1 (0 0 0 1) +4 (0 1 0 0) + ↑ ↑ + +The above arrows point to positions where the corresponding bits are different. +``` + +对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。 + +```java +public int hammingDistance(int x, int y) { + int z = x ^ y; + int cnt = 0; + while(z != 0) { + if ((z & 1) == 1) cnt++; + z = z >> 1; + } + return cnt; +} +``` + +使用 z&(z-1) 去除 z 位级表示最低的那一位。 + +```java +public int hammingDistance(int x, int y) { + int z = x ^ y; + int cnt = 0; + while (z != 0) { + z &= (z - 1); + cnt++; + } + return cnt; +} +``` + +可以使用 Integer.bitcount() 来统计 1 个的个数。 + +```java +public int hammingDistance(int x, int y) { + return Integer.bitCount(x ^ y); +} +``` + +## 数组中唯一一个不重复的元素 + +[136. Single Number (Easy)](https://leetcode.com/problems/single-number/description/) + +```html +Input: [4,1,2,1,2] +Output: 4 +``` + +两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。 + +```java +public int singleNumber(int[] nums) { + int ret = 0; + for (int n : nums) ret = ret ^ n; + return ret; +} +``` + +## 找出数组中缺失的那个数 + +[268. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/) + +```html +Input: [3,0,1] +Output: 2 +``` + +题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。 + +```java +public int missingNumber(int[] nums) { + int ret = 0; + for (int i = 0; i < nums.length; i++) { + ret = ret ^ i ^ nums[i]; + } + return ret ^ nums.length; +} +``` + +## 数组中不重复的两个元素 + +[260. Single Number III (Medium)](https://leetcode.com/problems/single-number-iii/description/) + +两个不相等的元素在位级表示上必定会有一位存在不同。 + +将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 + +diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 + +```java +public int[] singleNumber(int[] nums) { + int diff = 0; + for (int num : nums) diff ^= num; + diff &= -diff; // 得到最右一位 + int[] ret = new int[2]; + for (int num : nums) { + if ((num & diff) == 0) ret[0] ^= num; + else ret[1] ^= num; + } + return ret; +} +``` + +## 翻转一个数的比特位 + +[190. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/) + +```java +public int reverseBits(int n) { + int ret = 0; + for (int i = 0; i < 32; i++) { + ret <<= 1; + ret |= (n & 1); + n >>>= 1; + } + return ret; +} +``` + +如果该函数需要被调用很多次,可以将 int 拆成 4 个 byte,然后缓存 byte 对应的比特位翻转,最后再拼接起来。 + +```java +private static Map cache = new HashMap<>(); + +public int reverseBits(int n) { + int ret = 0; + for (int i = 0; i < 4; i++) { + ret <<= 8; + ret |= reverseByte((byte) (n & 0b11111111)); + n >>= 8; + } + return ret; +} + +private int reverseByte(byte b) { + if (cache.containsKey(b)) return cache.get(b); + int ret = 0; + byte t = b; + for (int i = 0; i < 8; i++) { + ret <<= 1; + ret |= t & 1; + t >>= 1; + } + cache.put(b, ret); + return ret; +} +``` + +## 不用额外变量交换两个整数 + +[程序员代码面试指南 :P317](#) + +```java +a = a ^ b; +b = a ^ b; +a = a ^ b; +``` + +## 判断一个数是不是 2 的 n 次方 + +[231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/) + +二进制表示只有一个 1 存在。 + +```java +public boolean isPowerOfTwo(int n) { + return n > 0 && Integer.bitCount(n) == 1; +} +``` + +利用 1000 & 0111 == 0 这种性质,得到以下解法: + +```java +public boolean isPowerOfTwo(int n) { + return n > 0 && (n & (n - 1)) == 0; +} +``` + +## 判断一个数是不是 4 的 n 次方 + +[342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/) + +这种数在二进制表示中有且只有一个奇数位为 1,例如 16(10000)。 + +```java +public boolean isPowerOfFour(int num) { + return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0; +} +``` + +也可以使用正则表达式进行匹配。 + +```java +public boolean isPowerOfFour(int num) { + return Integer.toString(num, 4).matches("10*"); +} +``` + +## 判断一个数的位级表示是否不会出现连续的 0 和 1 + +[693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) + +```html +Input: 10 +Output: True +Explanation: +The binary representation of 10 is: 1010. + +Input: 11 +Output: False +Explanation: +The binary representation of 11 is: 1011. +``` + +对于 1010 这种位级表示的数,把它向右移动 1 位得到 101,这两个数每个位都不同,因此异或得到的结果为 1111。 + +```java +public boolean hasAlternatingBits(int n) { + int a = (n ^ (n >> 1)); + return (a & (a + 1)) == 0; +} +``` + +## 求一个数的补码 + +[476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/) + +```html +Input: 5 +Output: 2 +Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010. So you need to output 2. +``` + +题目描述:不考虑二进制表示中的首 0 部分。 + +对于 00000101,要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。 + +```java +public int findComplement(int num) { + if (num == 0) return 1; + int mask = 1 << 30; + while ((num & mask) == 0) mask >>= 1; + mask = (mask << 1) - 1; + return num ^ mask; +} +``` + +可以利用 Java 的 Integer.highestOneBit() 方法来获得含有首 1 的数。 + +```java +public int findComplement(int num) { + if (num == 0) return 1; + int mask = Integer.highestOneBit(num); + mask = (mask << 1) - 1; + return num ^ mask; +} +``` + +对于 10000000 这样的数要扩展成 11111111,可以利用以下方法: + +```html +mask |= mask >> 1 11000000 +mask |= mask >> 2 11110000 +mask |= mask >> 4 11111111 +``` + +```java +public int findComplement(int num) { + int mask = num; + mask |= mask >> 1; + mask |= mask >> 2; + mask |= mask >> 4; + mask |= mask >> 8; + mask |= mask >> 16; + return (mask ^ num); +} +``` + +## 实现整数的加法 + +[371. Sum of Two Integers (Easy)](https://leetcode.com/problems/sum-of-two-integers/description/) + +a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。 + +递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 + +```java +public int getSum(int a, int b) { + return b == 0 ? a : getSum((a ^ b), (a & b) << 1); +} +``` + +## 字符串数组最大乘积 + +[318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) + +```html +Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"] +Return 16 +The two words can be "abcw", "xtfn". +``` + +题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。 + +本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。 + +```java +public int maxProduct(String[] words) { + int n = words.length; + int[] val = new int[n]; + for (int i = 0; i < n; i++) { + for (char c : words[i].toCharArray()) { + val[i] |= 1 << (c - 'a'); + } + } + int ret = 0; + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + if ((val[i] & val[j]) == 0) { + ret = Math.max(ret, words[i].length() * words[j].length()); + } + } + } + return ret; +} +``` + +## 统计从 0 \~ n 每个数的二进制表示中 1 的个数 + +[338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/) + +对于数字 6(110),它可以看成是 4(100) 再加一个 2(10),因此 dp[i] = dp[i&(i-1)] + 1; + +```java +public int[] countBits(int num) { + int[] ret = new int[num + 1]; + for(int i = 1; i <= num; i++){ + ret[i] = ret[i&(i-1)] + 1; + } + return ret; +} +``` + + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 分治.md b/docs/notes/Leetcode 题解 - 分治.md new file mode 100644 index 00000000..17cae52a --- /dev/null +++ b/docs/notes/Leetcode 题解 - 分治.md @@ -0,0 +1,55 @@ + +* [1. 给表达式加括号](#1-给表达式加括号) + + + +# 1. 给表达式加括号 + +[241. Different Ways to Add Parentheses (Medium)](https://leetcode.com/problems/different-ways-to-add-parentheses/description/) + +```html +Input: "2-1-1". + +((2-1)-1) = 0 +(2-(1-1)) = 2 + +Output : [0, 2] +``` + +```java +public List diffWaysToCompute(String input) { + List ways = new ArrayList<>(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c == '+' || c == '-' || c == '*') { + List left = diffWaysToCompute(input.substring(0, i)); + List right = diffWaysToCompute(input.substring(i + 1)); + for (int l : left) { + for (int r : right) { + switch (c) { + case '+': + ways.add(l + r); + break; + case '-': + ways.add(l - r); + break; + case '*': + ways.add(l * r); + break; + } + } + } + } + } + if (ways.size() == 0) { + ways.add(Integer.valueOf(input)); + } + return ways; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 动态规划.md b/docs/notes/Leetcode 题解 - 动态规划.md new file mode 100644 index 00000000..e71b9427 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 动态规划.md @@ -0,0 +1,1227 @@ + +* [斐波那契数列](#斐波那契数列) + * [爬楼梯](#爬楼梯) + * [强盗抢劫](#强盗抢劫) + * [强盗在环形街区抢劫](#强盗在环形街区抢劫) + * [信件错排](#信件错排) + * [母牛生产](#母牛生产) +* [矩阵路径](#矩阵路径) + * [矩阵的最小路径和](#矩阵的最小路径和) + * [矩阵的总路径数](#矩阵的总路径数) +* [数组区间](#数组区间) + * [数组区间和](#数组区间和) + * [数组中等差递增子区间的个数](#数组中等差递增子区间的个数) +* [分割整数](#分割整数) + * [分割整数的最大乘积](#分割整数的最大乘积) + * [按平方数来分割整数](#按平方数来分割整数) + * [分割整数构成字母字符串](#分割整数构成字母字符串) +* [最长递增子序列](#最长递增子序列) + * [最长递增子序列](#最长递增子序列) + * [一组整数对能够构成的最长链](#一组整数对能够构成的最长链) + * [最长摆动子序列](#最长摆动子序列) +* [最长公共子序列](#最长公共子序列) +* [0-1 背包](#0-1-背包) + * [空间优化](#空间优化) + * [无法使用贪心算法的解释](#无法使用贪心算法的解释) + * [变种](#变种) + * [划分数组为和相等的两部分](#划分数组为和相等的两部分) + * [改变一组数的正负号使得它们的和为一给定数](#改变一组数的正负号使得它们的和为一给定数) + * [01 字符构成最多的字符串](#01-字符构成最多的字符串) + * [找零钱的最少硬币数](#找零钱的最少硬币数) + * [找零钱的硬币数组合](#找零钱的硬币数组合) + * [字符串按单词列表分割](#字符串按单词列表分割) + * [组合总和](#组合总和) +* [股票交易](#股票交易) + * [需要冷却期的股票交易](#需要冷却期的股票交易) + * [需要交易费用的股票交易](#需要交易费用的股票交易) + * [只能进行两次的股票交易](#只能进行两次的股票交易) + * [只能进行 k 次的股票交易](#只能进行-k-次的股票交易) +* [字符串编辑](#字符串编辑) + * [删除两个字符串的字符使它们相等](#删除两个字符串的字符使它们相等) + * [编辑距离](#编辑距离) + * [复制粘贴字符](#复制粘贴字符) + + + +递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 + +# 斐波那契数列 + +## 爬楼梯 + +[70. Climbing Stairs (Easy)](https://leetcode.com/problems/climbing-stairs/description/) + +题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 + +定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。 + +第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 + + + +

+ + +考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 + +```java +public int climbStairs(int n) { + if (n <= 2) { + return n; + } + int pre2 = 1, pre1 = 2; + for (int i = 2; i < n; i++) { + int cur = pre1 + pre2; + pre2 = pre1; + pre1 = cur; + } + return pre1; +} +``` + +## 强盗抢劫 + +[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/) + +题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。 + +定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。 + +由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以 + + + +

+ +```java +public int rob(int[] nums) { + int pre2 = 0, pre1 = 0; + for (int i = 0; i < nums.length; i++) { + int cur = Math.max(pre2 + nums[i], pre1); + pre2 = pre1; + pre1 = cur; + } + return pre1; +} +``` + +## 强盗在环形街区抢劫 + +[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/) + +```java +public int rob(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int n = nums.length; + if (n == 1) { + return nums[0]; + } + return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1)); +} + +private int rob(int[] nums, int first, int last) { + int pre2 = 0, pre1 = 0; + for (int i = first; i <= last; i++) { + int cur = Math.max(pre1, pre2 + nums[i]); + pre2 = pre1; + pre1 = cur; + } + return pre1; +} +``` + +## 信件错排 + +题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 + +定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: + +- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。 +- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。 + +综上所述,错误装信数量方式数量为: + + + +

+ +## 母牛生产 + +[程序员代码面试指南-P181](#) + +题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 + +第 i 年成熟的牛的数量为: + + + +

+ +# 矩阵路径 + +## 矩阵的最小路径和 + +[64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/) + +```html +[[1,3,1], + [1,5,1], + [4,2,1]] +Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum. +``` + +题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。 + +```java +public int minPathSum(int[][] grid) { + if (grid.length == 0 || grid[0].length == 0) { + return 0; + } + int m = grid.length, n = grid[0].length; + int[] dp = new int[n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (j == 0) { + dp[j] = dp[j]; // 只能从上侧走到该位置 + } else if (i == 0) { + dp[j] = dp[j - 1]; // 只能从左侧走到该位置 + } else { + dp[j] = Math.min(dp[j - 1], dp[j]); + } + dp[j] += grid[i][j]; + } + } + return dp[n - 1]; +} +``` + +## 矩阵的总路径数 + +[62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/) + +题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。 + +

+ +```java +public int uniquePaths(int m, int n) { + int[] dp = new int[n]; + Arrays.fill(dp, 1); + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[j] = dp[j] + dp[j - 1]; + } + } + return dp[n - 1]; +} +``` + +也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。 + +```java +public int uniquePaths(int m, int n) { + int S = m + n - 2; // 总共的移动次数 + int D = m - 1; // 向下的移动次数 + long ret = 1; + for (int i = 1; i <= D; i++) { + ret = ret * (S - D + i) / i; + } + return (int) ret; +} +``` + +# 数组区间 + +## 数组区间和 + +[303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/) + +```html +Given nums = [-2, 0, 3, -5, 2, -1] + +sumRange(0, 2) -> 1 +sumRange(2, 5) -> -1 +sumRange(0, 5) -> -3 +``` + +求区间 i \~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 \~ i - 1 的和。 + +```java +class NumArray { + + private int[] sums; + + public NumArray(int[] nums) { + sums = new int[nums.length + 1]; + for (int i = 1; i <= nums.length; i++) { + sums[i] = sums[i - 1] + nums[i - 1]; + } + } + + public int sumRange(int i, int j) { + return sums[j + 1] - sums[i]; + } +} +``` + +## 数组中等差递增子区间的个数 + +[413. Arithmetic Slices (Medium)](https://leetcode.com/problems/arithmetic-slices/description/) + +```html +A = [1, 2, 3, 4] +return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself. +``` + +dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。 + +在 A[i] - A[i - 1] == A[i - 1] - A[i - 2] 的条件下,{A[i - 2], A[i - 1], A[i]} 是一个等差递增子区间。如果 {A[i - 3], A[i - 2], A[i - 1]} 是一个等差递增子区间,那么 {A[i - 3], A[i - 2], A[i - 1], A[i]} 也是等差递增子区间,dp[i] = dp[i-1] + 1。 + +```java +public int numberOfArithmeticSlices(int[] A) { + if (A == null || A.length == 0) { + return 0; + } + int n = A.length; + int[] dp = new int[n]; + for (int i = 2; i < n; i++) { + if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) { + dp[i] = dp[i - 1] + 1; + } + } + int total = 0; + for (int cnt : dp) { + total += cnt; + } + return total; +} +``` + +# 分割整数 + +## 分割整数的最大乘积 + +[343. Integer Break (Medim)](https://leetcode.com/problems/integer-break/description/) + +题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4). + +```java +public int integerBreak(int n) { + int[] dp = new int[n + 1]; + dp[1] = 1; + for (int i = 2; i <= n; i++) { + for (int j = 1; j <= i - 1; j++) { + dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); + } + } + return dp[n]; +} +``` + +## 按平方数来分割整数 + +[279. Perfect Squares(Medium)](https://leetcode.com/problems/perfect-squares/description/) + +题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. + +```java +public int numSquares(int n) { + List squareList = generateSquareList(n); + int[] dp = new int[n + 1]; + for (int i = 1; i <= n; i++) { + int min = Integer.MAX_VALUE; + for (int square : squareList) { + if (square > i) { + break; + } + min = Math.min(min, dp[i - square] + 1); + } + dp[i] = min; + } + return dp[n]; +} + +private List generateSquareList(int n) { + List squareList = new ArrayList<>(); + int diff = 3; + int square = 1; + while (square <= n) { + squareList.add(square); + square += diff; + diff += 2; + } + return squareList; +} +``` + +## 分割整数构成字母字符串 + +[91. Decode Ways (Medium)](https://leetcode.com/problems/decode-ways/description/) + +题目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12). + +```java +public int numDecodings(String s) { + if (s == null || s.length() == 0) { + return 0; + } + int n = s.length(); + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = s.charAt(0) == '0' ? 0 : 1; + for (int i = 2; i <= n; i++) { + int one = Integer.valueOf(s.substring(i - 1, i)); + if (one != 0) { + dp[i] += dp[i - 1]; + } + if (s.charAt(i - 2) == '0') { + continue; + } + int two = Integer.valueOf(s.substring(i - 2, i)); + if (two <= 26) { + dp[i] += dp[i - 2]; + } + } + return dp[n]; +} +``` + +# 最长递增子序列 + +已知一个序列 {S1, S2,...,Sn},取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。 + +如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 **递增子序列** 。 + +定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 + +因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: + + + +

+ +对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。 + +## 最长递增子序列 + +[300. Longest Increasing Subsequence (Medium)](https://leetcode.com/problems/longest-increasing-subsequence/description/) + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + int[] dp = new int[n]; + for (int i = 0; i < n; i++) { + int max = 1; + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + max = Math.max(max, dp[j] + 1); + } + } + dp[i] = max; + } + return Arrays.stream(dp).max().orElse(0); +} +``` + +使用 Stream 求最大值会导致运行时间过长,可以改成以下形式: + +```java +int ret = 0; +for (int i = 0; i < n; i++) { + ret = Math.max(ret, dp[i]); +} +return ret; +``` + +以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。 + +定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x, + +- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1; +- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。 + +例如对于数组 [4,3,6,5],有: + +```html +tails len num +[] 0 4 +[4] 1 3 +[3] 1 6 +[3,6] 2 5 +[3,5] 2 null +``` + +可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。 + +```java +public int lengthOfLIS(int[] nums) { + int n = nums.length; + int[] tails = new int[n]; + int len = 0; + for (int num : nums) { + int index = binarySearch(tails, len, num); + tails[index] = num; + if (index == len) { + len++; + } + } + return len; +} + +private int binarySearch(int[] tails, int len, int key) { + int l = 0, h = len; + while (l < h) { + int mid = l + (h - l) / 2; + if (tails[mid] == key) { + return mid; + } else if (tails[mid] > key) { + h = mid; + } else { + l = mid + 1; + } + } + return l; +} +``` + +## 一组整数对能够构成的最长链 + +[646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) + +```html +Input: [[1,2], [2,3], [3,4]] +Output: 2 +Explanation: The longest chain is [1,2] -> [3,4] +``` + +题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。 + +```java +public int findLongestChain(int[][] pairs) { + if (pairs == null || pairs.length == 0) { + return 0; + } + Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); + int n = pairs.length; + int[] dp = new int[n]; + Arrays.fill(dp, 1); + for (int i = 1; i < n; i++) { + for (int j = 0; j < i; j++) { + if (pairs[j][1] < pairs[i][0]) { + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + } + return Arrays.stream(dp).max().orElse(0); +} +``` + +## 最长摆动子序列 + +[376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/) + +```html +Input: [1,7,4,9,2,5] +Output: 6 +The entire sequence is a wiggle sequence. + +Input: [1,17,5,10,13,15,10,5,16,8] +Output: 7 +There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8]. + +Input: [1,2,3,4,5,6,7,8,9] +Output: 2 +``` + +要求:使用 O(N) 时间复杂度求解。 + +```java +public int wiggleMaxLength(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int up = 1, down = 1; + for (int i = 1; i < nums.length; i++) { + if (nums[i] > nums[i - 1]) { + up = down + 1; + } else if (nums[i] < nums[i - 1]) { + down = up + 1; + } + } + return Math.max(up, down); +} +``` + +# 最长公共子序列 + +对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 + +定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: + +- 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。 +- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 + +综上,最长公共子序列的状态转移方程为: + + + +

+ +对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 + +与最长递增子序列相比,最长公共子序列有以下不同点: + +- 针对的是两个序列,求它们的最长公共子序列。 +- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。 +- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 + +```java +public int lengthOfLCS(int[] nums1, int[] nums2) { + int n1 = nums1.length, n2 = nums2.length; + int[][] dp = new int[n1 + 1][n2 + 1]; + for (int i = 1; i <= n1; i++) { + for (int j = 1; j <= n2; j++) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[n1][n2]; +} +``` + +# 0-1 背包 + +有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 + +定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: + +- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 +- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 + +第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为: + + + +

+ +```java +public int knapsack(int W, int N, int[] weights, int[] values) { + int[][] dp = new int[N + 1][W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = 1; j <= W; j++) { + if (j >= w) { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); + } else { + dp[i][j] = dp[i - 1][j]; + } + } + } + return dp[N][W]; +} +``` + +## 空间优化 + +在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时, + + + +

+ +因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 + +```java +public int knapsack(int W, int N, int[] weights, int[] values) { + int[] dp = new int[W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = W; j >= 1; j--) { + if (j >= w) { + dp[j] = Math.max(dp[j], dp[j - w] + v); + } + } + } + return dp[W]; +} +``` + +## 无法使用贪心算法的解释 + +0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22. + +| id | w | v | v/w | +| --- | --- | --- | --- | +| 0 | 1 | 6 | 6 | +| 1 | 2 | 10 | 5 | +| 2 | 3 | 12 | 4 | + +## 变种 + +- 完全背包:物品数量为无限个 + +- 多重背包:物品数量有限制 + +- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制 + +- 其它:物品之间相互约束或者依赖 + +## 划分数组为和相等的两部分 + +[416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/) + +```html +Input: [1, 5, 11, 5] + +Output: true + +Explanation: The array can be partitioned as [1, 5, 5] and [11]. +``` + +可以看成一个背包大小为 sum/2 的 0-1 背包问题。 + +```java +public boolean canPartition(int[] nums) { + int sum = computeArraySum(nums); + if (sum % 2 != 0) { + return false; + } + int W = sum / 2; + boolean[] dp = new boolean[W + 1]; + dp[0] = true; + for (int num : nums) { // 0-1 背包一个物品只能用一次 + for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num] + dp[i] = dp[i] || dp[i - num]; + } + } + return dp[W]; +} + +private int computeArraySum(int[] nums) { + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; +} +``` + +## 改变一组数的正负号使得它们的和为一给定数 + +[494. Target Sum (Medium)](https://leetcode.com/problems/target-sum/description/) + +```html +Input: nums is [1, 1, 1, 1, 1], S is 3. +Output: 5 +Explanation: + +-1+1+1+1+1 = 3 ++1-1+1+1+1 = 3 ++1+1-1+1+1 = 3 ++1+1+1-1+1 = 3 ++1+1+1+1-1 = 3 + +There are 5 ways to assign symbols to make the sum of nums be target 3. +``` + +该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。 + +可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导: + +```html + sum(P) - sum(N) = target +sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N) + 2 * sum(P) = target + sum(nums) +``` + +因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。 + +```java +public int findTargetSumWays(int[] nums, int S) { + int sum = computeArraySum(nums); + if (sum < S || (sum + S) % 2 == 1) { + return 0; + } + int W = (sum + S) / 2; + int[] dp = new int[W + 1]; + dp[0] = 1; + for (int num : nums) { + for (int i = W; i >= num; i--) { + dp[i] = dp[i] + dp[i - num]; + } + } + return dp[W]; +} + +private int computeArraySum(int[] nums) { + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; +} +``` + +DFS 解法: + +```java +public int findTargetSumWays(int[] nums, int S) { + return findTargetSumWays(nums, 0, S); +} + +private int findTargetSumWays(int[] nums, int start, int S) { + if (start == nums.length) { + return S == 0 ? 1 : 0; + } + return findTargetSumWays(nums, start + 1, S + nums[start]) + + findTargetSumWays(nums, start + 1, S - nums[start]); +} +``` + +## 01 字符构成最多的字符串 + +[474. Ones and Zeroes (Medium)](https://leetcode.com/problems/ones-and-zeroes/description/) + +```html +Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 +Output: 4 + +Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0" +``` + +这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。 + +```java +public int findMaxForm(String[] strs, int m, int n) { + if (strs == null || strs.length == 0) { + return 0; + } + int[][] dp = new int[m + 1][n + 1]; + for (String s : strs) { // 每个字符串只能用一次 + int ones = 0, zeros = 0; + for (char c : s.toCharArray()) { + if (c == '0') { + zeros++; + } else { + ones++; + } + } + for (int i = m; i >= zeros; i--) { + for (int j = n; j >= ones; j--) { + dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1); + } + } + } + return dp[m][n]; +} +``` + +## 找零钱的最少硬币数 + +[322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/) + +```html +Example 1: +coins = [1, 2, 5], amount = 11 +return 3 (11 = 5 + 5 + 1) + +Example 2: +coins = [2], amount = 3 +return -1. +``` + +题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 + +- 物品:硬币 +- 物品大小:面额 +- 物品价值:数量 + +因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包中逆序遍历 dp 数组改为正序遍历即可。 + +```java +public int coinChange(int[] coins, int amount) { + if (amount == 0 || coins == null || coins.length == 0) { + return 0; + } + int[] dp = new int[amount + 1]; + for (int coin : coins) { + for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历 + if (i == coin) { + dp[i] = 1; + } else if (dp[i] == 0 && dp[i - coin] != 0) { + dp[i] = dp[i - coin] + 1; + } else if (dp[i - coin] != 0) { + dp[i] = Math.min(dp[i], dp[i - coin] + 1); + } + } + } + return dp[amount] == 0 ? -1 : dp[amount]; +} +``` + +## 找零钱的硬币数组合 + +[518\. Coin Change 2 (Medium)](https://leetcode.com/problems/coin-change-2/description/) + +```text-html-basic +Input: amount = 5, coins = [1, 2, 5] +Output: 4 +Explanation: there are four ways to make up the amount: +5=5 +5=2+2+1 +5=2+1+1+1 +5=1+1+1+1+1 +``` + +完全背包问题,使用 dp 记录可达成目标的组合数目。 + +```java +public int change(int amount, int[] coins) { + if (amount == 0 || coins == null || coins.length == 0) { + return 0; + } + int[] dp = new int[amount + 1]; + dp[0] = 1; + for (int coin : coins) { + for (int i = coin; i <= amount; i++) { + dp[i] += dp[i - coin]; + } + } + return dp[amount]; +} +``` + +## 字符串按单词列表分割 + +[139. Word Break (Medium)](https://leetcode.com/problems/word-break/description/) + +```html +s = "leetcode", +dict = ["leet", "code"]. +Return true because "leetcode" can be segmented as "leet code". +``` + +dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。该问题涉及到字典中单词的使用顺序,因此可理解为涉及顺序的完全背包问题。 + +求解顺序的完全背包问题时,对物品的迭代应该放在最里层。 + +```java +public boolean wordBreak(String s, List wordDict) { + int n = s.length(); + boolean[] dp = new boolean[n + 1]; + dp[0] = true; + for (int i = 1; i <= n; i++) { + for (String word : wordDict) { // 对物品的迭代应该放在最里层 + int len = word.length(); + if (len <= i && word.equals(s.substring(i - len, i))) { + dp[i] = dp[i] || dp[i - len]; + } + } + } + return dp[n]; +} +``` + +## 组合总和 + +[377. Combination Sum IV (Medium)](https://leetcode.com/problems/combination-sum-iv/description/) + +```html +nums = [1, 2, 3] +target = 4 + +The possible combination ways are: +(1, 1, 1, 1) +(1, 1, 2) +(1, 2, 1) +(1, 3) +(2, 1, 1) +(2, 2) +(3, 1) + +Note that different sequences are counted as different combinations. + +Therefore the output is 7. +``` + +涉及顺序的完全背包。 + +```java +public int combinationSum4(int[] nums, int target) { + if (nums == null || nums.length == 0) { + return 0; + } + int[] maximum = new int[target + 1]; + maximum[0] = 1; + Arrays.sort(nums); + for (int i = 1; i <= target; i++) { + for (int j = 0; j < nums.length && nums[j] <= i; j++) { + maximum[i] += maximum[i - nums[j]]; + } + } + return maximum[target]; +} +``` + +# 股票交易 + +## 需要冷却期的股票交易 + +[309. Best Time to Buy and Sell Stock with Cooldown(Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/) + +题目描述:交易之后需要有一天的冷却时间。 + +

+ +```java +public int maxProfit(int[] prices) { + if (prices == null || prices.length == 0) { + return 0; + } + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = s2[i - 1] - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); + } + return Math.max(sell[N - 1], s2[N - 1]); +} +``` + +## 需要交易费用的股票交易 + +[714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/) + +```html +Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 +Output: 8 +Explanation: The maximum profit can be achieved by: +Buying at prices[0] = 1 +Selling at prices[3] = 8 +Buying at prices[4] = 4 +Selling at prices[5] = 9 +The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. +``` + +题目描述:每交易一次,都要支付一定的费用。 + +

+ +```java +public int maxProfit(int[] prices, int fee) { + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); + } + return Math.max(sell[N - 1], s2[N - 1]); +} +``` + + +## 只能进行两次的股票交易 + +[123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) + +```java +public int maxProfit(int[] prices) { + int firstBuy = Integer.MIN_VALUE, firstSell = 0; + int secondBuy = Integer.MIN_VALUE, secondSell = 0; + for (int curPrice : prices) { + if (firstBuy < -curPrice) { + firstBuy = -curPrice; + } + if (firstSell < firstBuy + curPrice) { + firstSell = firstBuy + curPrice; + } + if (secondBuy < firstSell - curPrice) { + secondBuy = firstSell - curPrice; + } + if (secondSell < secondBuy + curPrice) { + secondSell = secondBuy + curPrice; + } + } + return secondSell; +} +``` + +## 只能进行 k 次的股票交易 + +[188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) + +```java +public int maxProfit(int k, int[] prices) { + int n = prices.length; + if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题 + int maxProfit = 0; + for (int i = 1; i < n; i++) { + if (prices[i] > prices[i - 1]) { + maxProfit += prices[i] - prices[i - 1]; + } + } + return maxProfit; + } + int[][] maxProfit = new int[k + 1][n]; + for (int i = 1; i <= k; i++) { + int localMax = maxProfit[i - 1][0] - prices[0]; + for (int j = 1; j < n; j++) { + maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax); + localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]); + } + } + return maxProfit[k][n - 1]; +} +``` + +# 字符串编辑 + +## 删除两个字符串的字符使它们相等 + +[583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/) + +```html +Input: "sea", "eat" +Output: 2 +Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea". +``` + +可以转换为求两个字符串的最长公共子序列问题。 + +```java +public int minDistance(String word1, String word2) { + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + return m + n - 2 * dp[m][n]; +} +``` + +## 编辑距离 + +[72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/) + +```html +Example 1: + +Input: word1 = "horse", word2 = "ros" +Output: 3 +Explanation: +horse -> rorse (replace 'h' with 'r') +rorse -> rose (remove 'r') +rose -> ros (remove 'e') +Example 2: + +Input: word1 = "intention", word2 = "execution" +Output: 5 +Explanation: +intention -> inention (remove 't') +inention -> enention (replace 'i' with 'e') +enention -> exention (replace 'n' with 'x') +exention -> exection (replace 'n' with 'c') +exection -> execution (insert 'u') +``` + +题目描述:修改一个字符串成为另一个字符串,使得修改次数最少。一次修改操作包括:插入一个字符、删除一个字符、替换一个字符。 + +```java +public int minDistance(String word1, String word2) { + if (word1 == null || word2 == null) { + return 0; + } + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + dp[i][0] = i; + } + for (int i = 1; i <= n; i++) { + dp[0][i] = i; + } + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; + } + } + } + return dp[m][n]; +} +``` + +## 复制粘贴字符 + +[650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/) + +题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。 + +``` +Input: 3 +Output: 3 +Explanation: +Intitally, we have one character 'A'. +In step 1, we use Copy All operation. +In step 2, we use Paste operation to get 'AA'. +In step 3, we use Paste operation to get 'AAA'. +``` + +```java +public int minSteps(int n) { + if (n == 1) return 0; + for (int i = 2; i <= Math.sqrt(n); i++) { + if (n % i == 0) return i + minSteps(n / i); + } + return n; +} +``` + +```java +public int minSteps(int n) { + int[] dp = new int[n + 1]; + int h = (int) Math.sqrt(n); + for (int i = 2; i <= n; i++) { + dp[i] = i; + for (int j = 2; j <= h; j++) { + if (i % j == 0) { + dp[i] = dp[j] + dp[i / j]; + break; + } + } + } + return dp[n]; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 双指针.md b/docs/notes/Leetcode 题解 - 双指针.md new file mode 100644 index 00000000..397c890c --- /dev/null +++ b/docs/notes/Leetcode 题解 - 双指针.md @@ -0,0 +1,244 @@ + +* [有序数组的 Two Sum](#有序数组的-two-sum) +* [两数平方和](#两数平方和) +* [反转字符串中的元音字符](#反转字符串中的元音字符) +* [回文字符串](#回文字符串) +* [归并两个有序数组](#归并两个有序数组) +* [判断链表是否存在环](#判断链表是否存在环) +* [最长子序列](#最长子序列) + + + +双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 + +# 有序数组的 Two Sum + +[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) + +```html +Input: numbers={2, 7, 11, 15}, target=9 +Output: index1=1, index2=2 +``` + +题目描述:在有序数组中找出两个数,使它们的和为 target。 + +使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 + +- 如果两个指针指向元素的和 sum == target,那么得到要求的结果; +- 如果 sum > target,移动较大的元素,使 sum 变小一些; +- 如果 sum < target,移动较小的元素,使 sum 变大一些。 + +```java +public int[] twoSum(int[] numbers, int target) { + int i = 0, j = numbers.length - 1; + while (i < j) { + int sum = numbers[i] + numbers[j]; + if (sum == target) { + return new int[]{i + 1, j + 1}; + } else if (sum < target) { + i++; + } else { + j--; + } + } + return null; +} +``` + +# 两数平方和 + +[633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/) + +```html +Input: 5 +Output: True +Explanation: 1 * 1 + 2 * 2 = 5 +``` + +题目描述:判断一个数是否为两个数的平方和。 + +```java +public boolean judgeSquareSum(int c) { + int i = 0, j = (int) Math.sqrt(c); + while (i <= j) { + int powSum = i * i + j * j; + if (powSum == c) { + return true; + } else if (powSum > c) { + j--; + } else { + i++; + } + } + return false; +} +``` + +# 反转字符串中的元音字符 + +[345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) + +```html +Given s = "leetcode", return "leotcede". +``` + +使用双指针指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。 + +```java +private final static HashSet vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); + +public String reverseVowels(String s) { + int i = 0, j = s.length() - 1; + char[] result = new char[s.length()]; + while (i <= j) { + char ci = s.charAt(i); + char cj = s.charAt(j); + if (!vowels.contains(ci)) { + result[i++] = ci; + } else if (!vowels.contains(cj)) { + result[j--] = cj; + } else { + result[i++] = cj; + result[j--] = ci; + } + } + return new String(result); +} +``` + +# 回文字符串 + +[680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/) + +```html +Input: "abca" +Output: True +Explanation: You could delete the character 'c'. +``` + +题目描述:可以删除一个字符,判断是否能构成回文字符串。 + +```java +public boolean validPalindrome(String s) { + int i = -1, j = s.length(); + while (++i < --j) { + if (s.charAt(i) != s.charAt(j)) { + return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j); + } + } + return true; +} + +private boolean isPalindrome(String s, int i, int j) { + while (i < j) { + if (s.charAt(i++) != s.charAt(j--)) { + return false; + } + } + return true; +} +``` + +# 归并两个有序数组 + +[88. Merge Sorted Array (Easy)](https://leetcode.com/problems/merge-sorted-array/description/) + +```html +Input: +nums1 = [1,2,3,0,0,0], m = 3 +nums2 = [2,5,6], n = 3 + +Output: [1,2,2,3,5,6] +``` + +题目描述:把归并结果存到第一个数组上。 + +需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值。 + +```java +public void merge(int[] nums1, int m, int[] nums2, int n) { + int index1 = m - 1, index2 = n - 1; + int indexMerge = m + n - 1; + while (index1 >= 0 || index2 >= 0) { + if (index1 < 0) { + nums1[indexMerge--] = nums2[index2--]; + } else if (index2 < 0) { + nums1[indexMerge--] = nums1[index1--]; + } else if (nums1[index1] > nums2[index2]) { + nums1[indexMerge--] = nums1[index1--]; + } else { + nums1[indexMerge--] = nums2[index2--]; + } + } +} +``` + +# 判断链表是否存在环 + +[141. Linked List Cycle (Easy)](https://leetcode.com/problems/linked-list-cycle/description/) + +使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。 + +```java +public boolean hasCycle(ListNode head) { + if (head == null) { + return false; + } + ListNode l1 = head, l2 = head.next; + while (l1 != null && l2 != null && l2.next != null) { + if (l1 == l2) { + return true; + } + l1 = l1.next; + l2 = l2.next.next; + } + return false; +} +``` + +# 最长子序列 + +[524. Longest Word in Dictionary through Deleting (Medium)](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/) + +``` +Input: +s = "abpcplea", d = ["ale","apple","monkey","plea"] + +Output: +"apple" +``` + +题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回字典序的最小字符串。 + +```java +public String findLongestWord(String s, List d) { + String longestWord = ""; + for (String target : d) { + int l1 = longestWord.length(), l2 = target.length(); + if (l1 > l2 || (l1 == l2 && longestWord.compareTo(target) < 0)) { + continue; + } + if (isValid(s, target)) { + longestWord = target; + } + } + return longestWord; +} + +private boolean isValid(String s, String target) { + int i = 0, j = 0; + while (i < s.length() && j < target.length()) { + if (s.charAt(i) == target.charAt(j)) { + j++; + } + i++; + } + return j == target.length(); +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 哈希表.md b/docs/notes/Leetcode 题解 - 哈希表.md new file mode 100644 index 00000000..ec4a34f8 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 哈希表.md @@ -0,0 +1,129 @@ + +* [1. 数组中两个数的和为给定值](#1-数组中两个数的和为给定值) +* [2. 判断数组是否含有重复元素](#2-判断数组是否含有重复元素) +* [3. 最长和谐序列](#3-最长和谐序列) +* [4. 最长连续序列](#4-最长连续序列) + + + +哈希表使用 O(N) 空间复杂度存储数据,并且以 O(1) 时间复杂度求解问题。 + +- Java 中的 **HashSet** 用于存储一个集合,可以查找元素是否在集合中。如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。 + +- Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 + + +# 1. 数组中两个数的和为给定值 + +[1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/) + +可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。 + +用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i],如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。 + +```java +public int[] twoSum(int[] nums, int target) { + HashMap indexForNum = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + if (indexForNum.containsKey(target - nums[i])) { + return new int[]{indexForNum.get(target - nums[i]), i}; + } else { + indexForNum.put(nums[i], i); + } + } + return null; +} +``` + +# 2. 判断数组是否含有重复元素 + +[217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/) + +```java +public boolean containsDuplicate(int[] nums) { + Set set = new HashSet<>(); + for (int num : nums) { + set.add(num); + } + return set.size() < nums.length; +} +``` + +# 3. 最长和谐序列 + +[594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/) + +```html +Input: [1,3,2,2,5,2,3,7] +Output: 5 +Explanation: The longest harmonious subsequence is [3,2,2,2,3]. +``` + +和谐序列中最大数和最小数之差正好为 1,应该注意的是序列的元素不一定是数组的连续元素。 + +```java +public int findLHS(int[] nums) { + Map countForNum = new HashMap<>(); + for (int num : nums) { + countForNum.put(num, countForNum.getOrDefault(num, 0) + 1); + } + int longest = 0; + for (int num : countForNum.keySet()) { + if (countForNum.containsKey(num + 1)) { + longest = Math.max(longest, countForNum.get(num + 1) + countForNum.get(num)); + } + } + return longest; +} +``` + +# 4. 最长连续序列 + +[128. Longest Consecutive Sequence (Hard)](https://leetcode.com/problems/longest-consecutive-sequence/description/) + +```html +Given [100, 4, 200, 1, 3, 2], +The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4. +``` + +要求以 O(N) 的时间复杂度求解。 + +```java +public int longestConsecutive(int[] nums) { + Map countForNum = new HashMap<>(); + for (int num : nums) { + countForNum.put(num, 1); + } + for (int num : nums) { + forward(countForNum, num); + } + return maxCount(countForNum); +} + +private int forward(Map countForNum, int num) { + if (!countForNum.containsKey(num)) { + return 0; + } + int cnt = countForNum.get(num); + if (cnt > 1) { + return cnt; + } + cnt = forward(countForNum, num + 1) + 1; + countForNum.put(num, cnt); + return cnt; +} + +private int maxCount(Map countForNum) { + int max = 0; + for (int num : countForNum.keySet()) { + max = Math.max(max, countForNum.get(num)); + } + return max; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 图.md b/docs/notes/Leetcode 题解 - 图.md new file mode 100644 index 00000000..f660c2c7 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 图.md @@ -0,0 +1,263 @@ + +* [二分图](#二分图) + * [判断是否为二分图](#判断是否为二分图) +* [拓扑排序](#拓扑排序) + * [课程安排的合法性](#课程安排的合法性) + * [课程安排的顺序](#课程安排的顺序) +* [并查集](#并查集) + * [冗余连接](#冗余连接) + + + +# 二分图 + +如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。 + +## 判断是否为二分图 + +[785. Is Graph Bipartite? (Medium)](https://leetcode.com/problems/is-graph-bipartite/description/) + +```html +Input: [[1,3], [0,2], [1,3], [0,2]] +Output: true +Explanation: +The graph looks like this: +0----1 +| | +| | +3----2 +We can divide the vertices into two groups: {0, 2} and {1, 3}. +``` + +```html +Example 2: +Input: [[1,2,3], [0,2], [0,1,3], [0,2]] +Output: false +Explanation: +The graph looks like this: +0----1 +| \ | +| \ | +3----2 +We cannot find a way to divide the set of nodes into two independent subsets. +``` + +```java +public boolean isBipartite(int[][] graph) { + int[] colors = new int[graph.length]; + Arrays.fill(colors, -1); + for (int i = 0; i < graph.length; i++) { // 处理图不是连通的情况 + if (colors[i] == -1 && !isBipartite(i, 0, colors, graph)) { + return false; + } + } + return true; +} + +private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] graph) { + if (colors[curNode] != -1) { + return colors[curNode] == curColor; + } + colors[curNode] = curColor; + for (int nextNode : graph[curNode]) { + if (!isBipartite(nextNode, 1 - curColor, colors, graph)) { + return false; + } + } + return true; +} +``` + +# 拓扑排序 + +常用于在具有先序关系的任务规划中。 + +## 课程安排的合法性 + +[207. Course Schedule (Medium)](https://leetcode.com/problems/course-schedule/description/) + +```html +2, [[1,0]] +return true +``` + +```html +2, [[1,0],[0,1]] +return false +``` + +题目描述:一个课程可能会先修课程,判断给定的先修课程规定是否合法。 + +本题不需要使用拓扑排序,只需要检测有向图是否存在环即可。 + +```java +public boolean canFinish(int numCourses, int[][] prerequisites) { + List[] graphic = new List[numCourses]; + for (int i = 0; i < numCourses; i++) { + graphic[i] = new ArrayList<>(); + } + for (int[] pre : prerequisites) { + graphic[pre[0]].add(pre[1]); + } + boolean[] globalMarked = new boolean[numCourses]; + boolean[] localMarked = new boolean[numCourses]; + for (int i = 0; i < numCourses; i++) { + if (hasCycle(globalMarked, localMarked, graphic, i)) { + return false; + } + } + return true; +} + +private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, + List[] graphic, int curNode) { + + if (localMarked[curNode]) { + return true; + } + if (globalMarked[curNode]) { + return false; + } + globalMarked[curNode] = true; + localMarked[curNode] = true; + for (int nextNode : graphic[curNode]) { + if (hasCycle(globalMarked, localMarked, graphic, nextNode)) { + return true; + } + } + localMarked[curNode] = false; + return false; +} +``` + +## 课程安排的顺序 + +[210. Course Schedule II (Medium)](https://leetcode.com/problems/course-schedule-ii/description/) + +```html +4, [[1,0],[2,0],[3,1],[3,2]] +There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3]. +``` + +使用 DFS 来实现拓扑排序,使用一个栈存储后序遍历结果,这个栈的逆序结果就是拓扑排序结果。 + +证明:对于任何先序关系:v->w,后序遍历结果可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。 + +```java +public int[] findOrder(int numCourses, int[][] prerequisites) { + List[] graphic = new List[numCourses]; + for (int i = 0; i < numCourses; i++) { + graphic[i] = new ArrayList<>(); + } + for (int[] pre : prerequisites) { + graphic[pre[0]].add(pre[1]); + } + Stack postOrder = new Stack<>(); + boolean[] globalMarked = new boolean[numCourses]; + boolean[] localMarked = new boolean[numCourses]; + for (int i = 0; i < numCourses; i++) { + if (hasCycle(globalMarked, localMarked, graphic, i, postOrder)) { + return new int[0]; + } + } + int[] orders = new int[numCourses]; + for (int i = numCourses - 1; i >= 0; i--) { + orders[i] = postOrder.pop(); + } + return orders; +} + +private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List[] graphic, + int curNode, Stack postOrder) { + + if (localMarked[curNode]) { + return true; + } + if (globalMarked[curNode]) { + return false; + } + globalMarked[curNode] = true; + localMarked[curNode] = true; + for (int nextNode : graphic[curNode]) { + if (hasCycle(globalMarked, localMarked, graphic, nextNode, postOrder)) { + return true; + } + } + localMarked[curNode] = false; + postOrder.push(curNode); + return false; +} +``` + +# 并查集 + +并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。 + +## 冗余连接 + +[684. Redundant Connection (Medium)](https://leetcode.com/problems/redundant-connection/description/) + +```html +Input: [[1,2], [1,3], [2,3]] +Output: [2,3] +Explanation: The given undirected graph will be like this: + 1 + / \ +2 - 3 +``` + +题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。 + +```java +public int[] findRedundantConnection(int[][] edges) { + int N = edges.length; + UF uf = new UF(N); + for (int[] e : edges) { + int u = e[0], v = e[1]; + if (uf.connect(u, v)) { + return e; + } + uf.union(u, v); + } + return new int[]{-1, -1}; +} + +private class UF { + + private int[] id; + + UF(int N) { + id = new int[N + 1]; + for (int i = 0; i < id.length; i++) { + id[i] = i; + } + } + + void union(int u, int v) { + int uID = find(u); + int vID = find(v); + if (uID == vID) { + return; + } + for (int i = 0; i < id.length; i++) { + if (id[i] == uID) { + id[i] = vID; + } + } + } + + int find(int p) { + return id[p]; + } + + boolean connect(int u, int v) { + return find(u) == find(v); + } +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 字符串.md b/docs/notes/Leetcode 题解 - 字符串.md new file mode 100644 index 00000000..28d7178b --- /dev/null +++ b/docs/notes/Leetcode 题解 - 字符串.md @@ -0,0 +1,231 @@ + +* [字符串循环移位包含](#字符串循环移位包含) +* [字符串循环移位](#字符串循环移位) +* [字符串中单词的翻转](#字符串中单词的翻转) +* [两个字符串包含的字符是否完全相同](#两个字符串包含的字符是否完全相同) +* [计算一组字符集合可以组成的回文字符串的最大长度](#计算一组字符集合可以组成的回文字符串的最大长度) +* [字符串同构](#字符串同构) +* [回文子字符串个数](#回文子字符串个数) +* [判断一个整数是否是回文数](#判断一个整数是否是回文数) +* [统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数](#统计二进制字符串中连续-1-和连续-0-数量相同的子字符串个数) + + + +# 字符串循环移位包含 + +[编程之美 3.1](#) + +```html +s1 = AABCD, s2 = CDAA +Return : true +``` + +给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。 + +s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。 + +# 字符串循环移位 + +[编程之美 2.17](#) + +```html +s = "abcd123" k = 3 +Return "123abcd" +``` + +将字符串向右循环移动 k 位。 + +将 abcd123 中的 abcd 和 123 单独翻转,得到 dcba321,然后对整个字符串进行翻转,得到 123abcd。 + +# 字符串中单词的翻转 + +[程序员代码面试指南](#) + +```html +s = "I am a student" +Return "student a am I" +``` + +将每个单词翻转,然后将整个字符串翻转。 + +# 两个字符串包含的字符是否完全相同 + +[242. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/) + +```html +s = "anagram", t = "nagaram", return true. +s = "rat", t = "car", return false. +``` + +可以用 HashMap 来映射字符与出现次数,然后比较两个字符串出现的字符数量是否相同。 + +由于本题的字符串只包含 26 个小写字符,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,不再使用 HashMap。 + +```java +public boolean isAnagram(String s, String t) { + int[] cnts = new int[26]; + for (char c : s.toCharArray()) { + cnts[c - 'a']++; + } + for (char c : t.toCharArray()) { + cnts[c - 'a']--; + } + for (int cnt : cnts) { + if (cnt != 0) { + return false; + } + } + return true; +} +``` + +# 计算一组字符集合可以组成的回文字符串的最大长度 + +[409. Longest Palindrome (Easy)](https://leetcode.com/problems/longest-palindrome/description/) + +```html +Input : "abccccdd" +Output : 7 +Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7. +``` + +使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。 + +因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。 + +```java +public int longestPalindrome(String s) { + int[] cnts = new int[256]; + for (char c : s.toCharArray()) { + cnts[c]++; + } + int palindrome = 0; + for (int cnt : cnts) { + palindrome += (cnt / 2) * 2; + } + if (palindrome < s.length()) { + palindrome++; // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间 + } + return palindrome; +} +``` + +# 字符串同构 + +[205. Isomorphic Strings (Easy)](https://leetcode.com/problems/isomorphic-strings/description/) + +```html +Given "egg", "add", return true. +Given "foo", "bar", return false. +Given "paper", "title", return true. +``` + +记录一个字符上次出现的位置,如果两个字符串中的字符上次出现的位置一样,那么就属于同构。 + +```java +public boolean isIsomorphic(String s, String t) { + int[] preIndexOfS = new int[256]; + int[] preIndexOfT = new int[256]; + for (int i = 0; i < s.length(); i++) { + char sc = s.charAt(i), tc = t.charAt(i); + if (preIndexOfS[sc] != preIndexOfT[tc]) { + return false; + } + preIndexOfS[sc] = i + 1; + preIndexOfT[tc] = i + 1; + } + return true; +} +``` + +# 回文子字符串个数 + +[647. Palindromic Substrings (Medium)](https://leetcode.com/problems/palindromic-substrings/description/) + +```html +Input: "aaa" +Output: 6 +Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa". +``` + +从字符串的某一位开始,尝试着去扩展子字符串。 + +```java +private int cnt = 0; + +public int countSubstrings(String s) { + for (int i = 0; i < s.length(); i++) { + extendSubstrings(s, i, i); // 奇数长度 + extendSubstrings(s, i, i + 1); // 偶数长度 + } + return cnt; +} + +private void extendSubstrings(String s, int start, int end) { + while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) { + start--; + end++; + cnt++; + } +} +``` + +# 判断一个整数是否是回文数 + +[9. Palindrome Number (Easy)](https://leetcode.com/problems/palindrome-number/description/) + +要求不能使用额外空间,也就不能将整数转换为字符串进行判断。 + +将整数分成左右两部分,右边那部分需要转置,然后判断这两部分是否相等。 + +```java +public boolean isPalindrome(int x) { + if (x == 0) { + return true; + } + if (x < 0 || x % 10 == 0) { + return false; + } + int right = 0; + while (x > right) { + right = right * 10 + x % 10; + x /= 10; + } + return x == right || x == right / 10; +} +``` + +# 统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数 + +[696. Count Binary Substrings (Easy)](https://leetcode.com/problems/count-binary-substrings/description/) + +```html +Input: "00110011" +Output: 6 +Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01". +``` + +```java +public int countBinarySubstrings(String s) { + int preLen = 0, curLen = 1, count = 0; + for (int i = 1; i < s.length(); i++) { + if (s.charAt(i) == s.charAt(i - 1)) { + curLen++; + } else { + preLen = curLen; + curLen = 1; + } + + if (preLen >= curLen) { + count++; + } + } + return count; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 排序.md b/docs/notes/Leetcode 题解 - 排序.md new file mode 100644 index 00000000..5c88726b --- /dev/null +++ b/docs/notes/Leetcode 题解 - 排序.md @@ -0,0 +1,232 @@ + +* [快速选择](#快速选择) +* [堆排序](#堆排序) + * [Kth Element](#kth-element) +* [桶排序](#桶排序) + * [出现频率最多的 k 个数](#出现频率最多的-k-个数) + * [按照字符出现次数对字符串排序](#按照字符出现次数对字符串排序) +* [荷兰国旗问题](#荷兰国旗问题) + * [按颜色进行排序](#按颜色进行排序) + + + +# 快速选择 + +用于求解 **Kth Element** 问题,使用快速排序的 partition() 进行实现。 + +需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。 + +# 堆排序 + +用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。 + +堆排序也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。 + +快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。 + +可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。 + +## Kth Element + +[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) + +题目描述:找到第 k 大的元素。 + +**排序** :时间复杂度 O(NlogN),空间复杂度 O(1) + +```java +public int findKthLargest(int[] nums, int k) { + Arrays.sort(nums); + return nums[nums.length - k]; +} +``` + +**堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。 + +```java +public int findKthLargest(int[] nums, int k) { + PriorityQueue pq = new PriorityQueue<>(); // 小顶堆 + for (int val : nums) { + pq.add(val); + if (pq.size() > k) // 维护堆的大小为 K + pq.poll(); + } + return pq.peek(); +} +``` + +**快速选择** :时间复杂度 O(N),空间复杂度 O(1) + +```java +public int findKthLargest(int[] nums, int k) { + k = nums.length - k; + int l = 0, h = nums.length - 1; + while (l < h) { + int j = partition(nums, l, h); + if (j == k) { + break; + } else if (j < k) { + l = j + 1; + } else { + h = j - 1; + } + } + return nums[k]; +} + +private int partition(int[] a, int l, int h) { + int i = l, j = h + 1; + while (true) { + while (a[++i] < a[l] && i < h) ; + while (a[--j] > a[l] && j > l) ; + if (i >= j) { + break; + } + swap(a, i, j); + } + swap(a, l, j); + return j; +} + +private void swap(int[] a, int i, int j) { + int t = a[i]; + a[i] = a[j]; + a[j] = t; +} +``` + +# 桶排序 + +## 出现频率最多的 k 个数 + +[347. Top K Frequent Elements (Medium)](https://leetcode.com/problems/top-k-frequent-elements/description/) + +```html +Given [1,1,1,2,2,3] and k = 2, return [1,2]. +``` + +设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。 + +把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。 + +```java +public List topKFrequent(int[] nums, int k) { + Map frequencyForNum = new HashMap<>(); + for (int num : nums) { + frequencyForNum.put(num, frequencyForNum.getOrDefault(num, 0) + 1); + } + List[] buckets = new ArrayList[nums.length + 1]; + for (int key : frequencyForNum.keySet()) { + int frequency = frequencyForNum.get(key); + if (buckets[frequency] == null) { + buckets[frequency] = new ArrayList<>(); + } + buckets[frequency].add(key); + } + List topK = new ArrayList<>(); + for (int i = buckets.length - 1; i >= 0 && topK.size() < k; i--) { + if (buckets[i] == null) { + continue; + } + if (buckets[i].size() <= (k - topK.size())) { + topK.addAll(buckets[i]); + } else { + topK.addAll(buckets[i].subList(0, k - topK.size())); + } + } + return topK; +} +``` + +## 按照字符出现次数对字符串排序 + +[451. Sort Characters By Frequency (Medium)](https://leetcode.com/problems/sort-characters-by-frequency/description/) + +```html +Input: +"tree" + +Output: +"eert" + +Explanation: +'e' appears twice while 'r' and 't' both appear once. +So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer. +``` + +```java +public String frequencySort(String s) { + Map frequencyForNum = new HashMap<>(); + for (char c : s.toCharArray()) + frequencyForNum.put(c, frequencyForNum.getOrDefault(c, 0) + 1); + + List[] frequencyBucket = new ArrayList[s.length() + 1]; + for (char c : frequencyForNum.keySet()) { + int f = frequencyForNum.get(c); + if (frequencyBucket[f] == null) { + frequencyBucket[f] = new ArrayList<>(); + } + frequencyBucket[f].add(c); + } + StringBuilder str = new StringBuilder(); + for (int i = frequencyBucket.length - 1; i >= 0; i--) { + if (frequencyBucket[i] == null) { + continue; + } + for (char c : frequencyBucket[i]) { + for (int j = 0; j < i; j++) { + str.append(c); + } + } + } + return str.toString(); +} +``` + +# 荷兰国旗问题 + +荷兰国旗包含三种颜色:红、白、蓝。 + +有三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。 + +它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。 + +

+ +## 按颜色进行排序 + +[75. Sort Colors (Medium)](https://leetcode.com/problems/sort-colors/description/) + +```html +Input: [2,0,2,1,1,0] +Output: [0,0,1,1,2,2] +``` + +题目描述:只有 0/1/2 三种颜色。 + +```java +public void sortColors(int[] nums) { + int zero = -1, one = 0, two = nums.length; + while (one < two) { + if (nums[one] == 0) { + swap(nums, ++zero, one++); + } else if (nums[one] == 2) { + swap(nums, --two, one); + } else { + ++one; + } + } +} + +private void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 搜索.md b/docs/notes/Leetcode 题解 - 搜索.md new file mode 100644 index 00000000..2bbde488 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 搜索.md @@ -0,0 +1,1274 @@ + +* [BFS](#bfs) + * [计算在网格中从原点到特定点的最短路径长度](#计算在网格中从原点到特定点的最短路径长度) + * [组成整数的最小平方数数量](#组成整数的最小平方数数量) + * [最短单词路径](#最短单词路径) +* [DFS](#dfs) + * [查找最大的连通面积](#查找最大的连通面积) + * [矩阵中的连通分量数目](#矩阵中的连通分量数目) + * [好友关系的连通分量数目](#好友关系的连通分量数目) + * [填充封闭区域](#填充封闭区域) + * [能到达的太平洋和大西洋的区域](#能到达的太平洋和大西洋的区域) +* [Backtracking](#backtracking) + * [数字键盘组合](#数字键盘组合) + * [IP 地址划分](#ip-地址划分) + * [在矩阵中寻找字符串](#在矩阵中寻找字符串) + * [输出二叉树中所有从根到叶子的路径](#输出二叉树中所有从根到叶子的路径) + * [排列](#排列) + * [含有相同元素求排列](#含有相同元素求排列) + * [组合](#组合) + * [组合求和](#组合求和) + * [含有相同元素的求组合求和](#含有相同元素的求组合求和) + * [1-9 数字的组合求和](#1-9-数字的组合求和) + * [子集](#子集) + * [含有相同元素求子集](#含有相同元素求子集) + * [分割字符串使得每个部分都是回文数](#分割字符串使得每个部分都是回文数) + * [数独](#数独) + * [N 皇后](#n-皇后) + + + +深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 + +# BFS + +

+ +广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。 + +第一层: + +- 0 -> {6,2,1,5} + +第二层: + +- 6 -> {4} +- 2 -> {} +- 1 -> {} +- 5 -> {3} + +第三层: + +- 4 -> {} +- 3 -> {} + +每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。 + +在程序实现 BFS 时需要考虑以下问题: + +- 队列:用来存储每一轮遍历得到的节点; +- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。 + +## 计算在网格中从原点到特定点的最短路径长度 + +```html +[[1,1,0,1], + [1,0,1,0], + [1,1,1,1], + [1,0,1,1]] +``` + +1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。 + +```java +public int minPathLength(int[][] grids, int tr, int tc) { + final int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + final int m = grids.length, n = grids[0].length; + Queue> queue = new LinkedList<>(); + queue.add(new Pair<>(0, 0)); + int pathLength = 0; + while (!queue.isEmpty()) { + int size = queue.size(); + pathLength++; + while (size-- > 0) { + Pair cur = queue.poll(); + int cr = cur.getKey(), cc = cur.getValue(); + grids[cr][cc] = 0; // 标记 + for (int[] d : direction) { + int nr = cr + d[0], nc = cc + d[1]; + if (nr < 0 || nr >= m || nc < 0 || nc >= n || grids[nr][nc] == 0) { + continue; + } + if (nr == tr && nc == tc) { + return pathLength; + } + queue.add(new Pair<>(nr, nc)); + } + } + } + return -1; +} +``` + +## 组成整数的最小平方数数量 + +[279. Perfect Squares (Medium)](https://leetcode.com/problems/perfect-squares/description/) + +```html +For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. +``` + +可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。 + +要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。 + +本题也可以用动态规划求解,在之后动态规划部分中会再次出现。 + +```java +public int numSquares(int n) { + List squares = generateSquares(n); + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[n + 1]; + queue.add(n); + marked[n] = true; + int level = 0; + while (!queue.isEmpty()) { + int size = queue.size(); + level++; + while (size-- > 0) { + int cur = queue.poll(); + for (int s : squares) { + int next = cur - s; + if (next < 0) { + break; + } + if (next == 0) { + return level; + } + if (marked[next]) { + continue; + } + marked[next] = true; + queue.add(next); + } + } + } + return n; +} + +/** + * 生成小于 n 的平方数序列 + * @return 1,4,9,... + */ +private List generateSquares(int n) { + List squares = new ArrayList<>(); + int square = 1; + int diff = 3; + while (square <= n) { + squares.add(square); + square += diff; + diff += 2; + } + return squares; +} +``` + +## 最短单词路径 + +[127. Word Ladder (Medium)](https://leetcode.com/problems/word-ladder/description/) + +```html +Input: +beginWord = "hit", +endWord = "cog", +wordList = ["hot","dot","dog","lot","log","cog"] + +Output: 5 + +Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog", +return its length 5. +``` + +```html +Input: +beginWord = "hit" +endWord = "cog" +wordList = ["hot","dot","dog","lot","log"] + +Output: 0 + +Explanation: The endWord "cog" is not in wordList, therefore no possible transformation. +``` + +题目描述:找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。 + +```java +public int ladderLength(String beginWord, String endWord, List wordList) { + wordList.add(beginWord); + int N = wordList.size(); + int start = N - 1; + int end = 0; + while (end < N && !wordList.get(end).equals(endWord)) { + end++; + } + if (end == N) { + return 0; + } + List[] graphic = buildGraphic(wordList); + return getShortestPath(graphic, start, end); +} + +private List[] buildGraphic(List wordList) { + int N = wordList.size(); + List[] graphic = new List[N]; + for (int i = 0; i < N; i++) { + graphic[i] = new ArrayList<>(); + for (int j = 0; j < N; j++) { + if (isConnect(wordList.get(i), wordList.get(j))) { + graphic[i].add(j); + } + } + } + return graphic; +} + +private boolean isConnect(String s1, String s2) { + int diffCnt = 0; + for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { + if (s1.charAt(i) != s2.charAt(i)) { + diffCnt++; + } + } + return diffCnt == 1; +} + +private int getShortestPath(List[] graphic, int start, int end) { + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[graphic.length]; + queue.add(start); + marked[start] = true; + int path = 1; + while (!queue.isEmpty()) { + int size = queue.size(); + path++; + while (size-- > 0) { + int cur = queue.poll(); + for (int next : graphic[cur]) { + if (next == end) { + return path; + } + if (marked[next]) { + continue; + } + marked[next] = true; + queue.add(next); + } + } + } + return 0; +} +``` + +# DFS + +

+ +广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。 + +而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 + +从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。 + +在程序实现 DFS 时需要考虑以下问题: + +- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。 +- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。 + +## 查找最大的连通面积 + +[695. Max Area of Island (Easy)](https://leetcode.com/problems/max-area-of-island/description/) + +```html +[[0,0,1,0,0,0,0,1,0,0,0,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,1,1,0,1,0,0,0,0,0,0,0,0], + [0,1,0,0,1,1,0,0,1,0,1,0,0], + [0,1,0,0,1,1,0,0,1,1,1,0,0], + [0,0,0,0,0,0,0,0,0,0,1,0,0], + [0,0,0,0,0,0,0,1,1,1,0,0,0], + [0,0,0,0,0,0,0,1,1,0,0,0,0]] +``` + +```java +private int m, n; +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + +public int maxAreaOfIsland(int[][] grid) { + if (grid == null || grid.length == 0) { + return 0; + } + m = grid.length; + n = grid[0].length; + int maxArea = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + maxArea = Math.max(maxArea, dfs(grid, i, j)); + } + } + return maxArea; +} + +private int dfs(int[][] grid, int r, int c) { + if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) { + return 0; + } + grid[r][c] = 0; + int area = 1; + for (int[] d : direction) { + area += dfs(grid, r + d[0], c + d[1]); + } + return area; +} +``` + +## 矩阵中的连通分量数目 + +[200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/) + +```html +Input: +11000 +11000 +00100 +00011 + +Output: 3 +``` + +可以将矩阵表示看成一张有向图。 + +```java +private int m, n; +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + +public int numIslands(char[][] grid) { + if (grid == null || grid.length == 0) { + return 0; + } + m = grid.length; + n = grid[0].length; + int islandsNum = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] != '0') { + dfs(grid, i, j); + islandsNum++; + } + } + } + return islandsNum; +} + +private void dfs(char[][] grid, int i, int j) { + if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') { + return; + } + grid[i][j] = '0'; + for (int[] d : direction) { + dfs(grid, i + d[0], j + d[1]); + } +} +``` + +## 好友关系的连通分量数目 + +[547. Friend Circles (Medium)](https://leetcode.com/problems/friend-circles/description/) + +```html +Input: +[[1,1,0], + [1,1,0], + [0,0,1]] + +Output: 2 + +Explanation:The 0th and 1st students are direct friends, so they are in a friend circle. +The 2nd student himself is in a friend circle. So return 2. +``` + +题目描述:好友关系可以看成是一个无向图,例如第 0 个人与第 1 个人是好友,那么 M[0][1] 和 M[1][0] 的值都为 1。 + +```java +private int n; + +public int findCircleNum(int[][] M) { + n = M.length; + int circleNum = 0; + boolean[] hasVisited = new boolean[n]; + for (int i = 0; i < n; i++) { + if (!hasVisited[i]) { + dfs(M, i, hasVisited); + circleNum++; + } + } + return circleNum; +} + +private void dfs(int[][] M, int i, boolean[] hasVisited) { + hasVisited[i] = true; + for (int k = 0; k < n; k++) { + if (M[i][k] == 1 && !hasVisited[k]) { + dfs(M, k, hasVisited); + } + } +} +``` + +## 填充封闭区域 + +[130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/) + +```html +For example, +X X X X +X O O X +X X O X +X O X X + +After running your function, the board should be: +X X X X +X X X X +X X X X +X O X X +``` + +题目描述:使被 'X' 包围的 'O' 转换为 'X'。 + +先填充最外侧,剩下的就是里侧了。 + +```java +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; +private int m, n; + +public void solve(char[][] board) { + if (board == null || board.length == 0) { + return; + } + + m = board.length; + n = board[0].length; + + for (int i = 0; i < m; i++) { + dfs(board, i, 0); + dfs(board, i, n - 1); + } + for (int i = 0; i < n; i++) { + dfs(board, 0, i); + dfs(board, m - 1, i); + } + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == 'T') { + board[i][j] = 'O'; + } else if (board[i][j] == 'O') { + board[i][j] = 'X'; + } + } + } +} + +private void dfs(char[][] board, int r, int c) { + if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { + return; + } + board[r][c] = 'T'; + for (int[] d : direction) { + dfs(board, r + d[0], c + d[1]); + } +} +``` + +## 能到达的太平洋和大西洋的区域 + +[417. Pacific Atlantic Water Flow (Medium)](https://leetcode.com/problems/pacific-atlantic-water-flow/description/) + +```html +Given the following 5x5 matrix: + + Pacific ~ ~ ~ ~ ~ + ~ 1 2 2 3 (5) * + ~ 3 2 3 (4) (4) * + ~ 2 4 (5) 3 1 * + ~ (6) (7) 1 4 5 * + ~ (5) 1 1 2 4 * + * * * * * Atlantic + +Return: +[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix). +``` + +左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。 + +```java + +private int m, n; +private int[][] matrix; +private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + +public List pacificAtlantic(int[][] matrix) { + List ret = new ArrayList<>(); + if (matrix == null || matrix.length == 0) { + return ret; + } + + m = matrix.length; + n = matrix[0].length; + this.matrix = matrix; + boolean[][] canReachP = new boolean[m][n]; + boolean[][] canReachA = new boolean[m][n]; + + for (int i = 0; i < m; i++) { + dfs(i, 0, canReachP); + dfs(i, n - 1, canReachA); + } + for (int i = 0; i < n; i++) { + dfs(0, i, canReachP); + dfs(m - 1, i, canReachA); + } + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (canReachP[i][j] && canReachA[i][j]) { + ret.add(new int[]{i, j}); + } + } + } + + return ret; +} + +private void dfs(int r, int c, boolean[][] canReach) { + if (canReach[r][c]) { + return; + } + canReach[r][c] = true; + for (int[] d : direction) { + int nextR = d[0] + r; + int nextC = d[1] + c; + if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n + || matrix[r][c] > matrix[nextR][nextC]) { + + continue; + } + dfs(nextR, nextC, canReach); + } +} +``` + +# Backtracking + +Backtracking(回溯)属于 DFS。 + +- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特点的位置然后返回即可。 +- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。 + +因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题: + +- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素; +- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。 + +## 数字键盘组合 + +[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) + +

+ +```html +Input:Digit string "23" +Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. +``` + +```java +private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + +public List letterCombinations(String digits) { + List combinations = new ArrayList<>(); + if (digits == null || digits.length() == 0) { + return combinations; + } + doCombination(new StringBuilder(), combinations, digits); + return combinations; +} + +private void doCombination(StringBuilder prefix, List combinations, final String digits) { + if (prefix.length() == digits.length()) { + combinations.add(prefix.toString()); + return; + } + int curDigits = digits.charAt(prefix.length()) - '0'; + String letters = KEYS[curDigits]; + for (char c : letters.toCharArray()) { + prefix.append(c); // 添加 + doCombination(prefix, combinations, digits); + prefix.deleteCharAt(prefix.length() - 1); // 删除 + } +} +``` + +## IP 地址划分 + +[93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/) + +```html +Given "25525511135", +return ["255.255.11.135", "255.255.111.35"]. +``` + +```java +public List restoreIpAddresses(String s) { + List addresses = new ArrayList<>(); + StringBuilder tempAddress = new StringBuilder(); + doRestore(0, tempAddress, addresses, s); + return addresses; +} + +private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) { + if (k == 4 || s.length() == 0) { + if (k == 4 && s.length() == 0) { + addresses.add(tempAddress.toString()); + } + return; + } + for (int i = 0; i < s.length() && i <= 2; i++) { + if (i != 0 && s.charAt(0) == '0') { + break; + } + String part = s.substring(0, i + 1); + if (Integer.valueOf(part) <= 255) { + if (tempAddress.length() != 0) { + part = "." + part; + } + tempAddress.append(part); + doRestore(k + 1, tempAddress, addresses, s.substring(i + 1)); + tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length()); + } + } +} +``` + +## 在矩阵中寻找字符串 + +[79. Word Search (Medium)](https://leetcode.com/problems/word-search/description/) + +```html +For example, +Given board = +[ + ['A','B','C','E'], + ['S','F','C','S'], + ['A','D','E','E'] +] +word = "ABCCED", -> returns true, +word = "SEE", -> returns true, +word = "ABCB", -> returns false. +``` + +```java +private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; +private int m; +private int n; + +public boolean exist(char[][] board, String word) { + if (word == null || word.length() == 0) { + return true; + } + if (board == null || board.length == 0 || board[0].length == 0) { + return false; + } + + m = board.length; + n = board[0].length; + boolean[][] hasVisited = new boolean[m][n]; + + for (int r = 0; r < m; r++) { + for (int c = 0; c < n; c++) { + if (backtracking(0, r, c, hasVisited, board, word)) { + return true; + } + } + } + + return false; +} + +private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { + if (curLen == word.length()) { + return true; + } + if (r < 0 || r >= m || c < 0 || c >= n + || board[r][c] != word.charAt(curLen) || visited[r][c]) { + + return false; + } + + visited[r][c] = true; + + for (int[] d : direction) { + if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) { + return true; + } + } + + visited[r][c] = false; + + return false; +} +``` + +## 输出二叉树中所有从根到叶子的路径 + +[257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/) + +```html + 1 + / \ +2 3 + \ + 5 +``` + +```html +["1->2->5", "1->3"] +``` + +```java + +public List binaryTreePaths(TreeNode root) { + List paths = new ArrayList<>(); + if (root == null) { + return paths; + } + List values = new ArrayList<>(); + backtracking(root, values, paths); + return paths; +} + +private void backtracking(TreeNode node, List values, List paths) { + if (node == null) { + return; + } + values.add(node.val); + if (isLeaf(node)) { + paths.add(buildPath(values)); + } else { + backtracking(node.left, values, paths); + backtracking(node.right, values, paths); + } + values.remove(values.size() - 1); +} + +private boolean isLeaf(TreeNode node) { + return node.left == null && node.right == null; +} + +private String buildPath(List values) { + StringBuilder str = new StringBuilder(); + for (int i = 0; i < values.size(); i++) { + str.append(values.get(i)); + if (i != values.size() - 1) { + str.append("->"); + } + } + return str.toString(); +} +``` + +## 排列 + +[46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/) + +```html +[1,2,3] have the following permutations: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + +```java +public List> permute(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; +} + +private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); // 重新构造一个 List + return; + } + for (int i = 0; i < visited.length; i++) { + if (visited[i]) { + continue; + } + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } +} +``` + +## 含有相同元素求排列 + +[47. Permutations II (Medium)](https://leetcode.com/problems/permutations-ii/description/) + +```html +[1,1,2] have the following unique permutations: +[[1,1,2], [1,2,1], [2,1,1]] +``` + +数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。 + +在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。 + +```java +public List> permuteUnique(int[] nums) { + List> permutes = new ArrayList<>(); + List permuteList = new ArrayList<>(); + Arrays.sort(nums); // 排序 + boolean[] hasVisited = new boolean[nums.length]; + backtracking(permuteList, permutes, hasVisited, nums); + return permutes; +} + +private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { + if (permuteList.size() == nums.length) { + permutes.add(new ArrayList<>(permuteList)); + return; + } + + for (int i = 0; i < visited.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) { + continue; // 防止重复 + } + if (visited[i]){ + continue; + } + visited[i] = true; + permuteList.add(nums[i]); + backtracking(permuteList, permutes, visited, nums); + permuteList.remove(permuteList.size() - 1); + visited[i] = false; + } +} +``` + +## 组合 + +[77. Combinations (Medium)](https://leetcode.com/problems/combinations/description/) + +```html +If n = 4 and k = 2, a solution is: +[ + [2,4], + [3,4], + [2,3], + [1,2], + [1,3], + [1,4], +] +``` + +```java +public List> combine(int n, int k) { + List> combinations = new ArrayList<>(); + List combineList = new ArrayList<>(); + backtracking(combineList, combinations, 1, k, n); + return combinations; +} + +private void backtracking(List combineList, List> combinations, int start, int k, final int n) { + if (k == 0) { + combinations.add(new ArrayList<>(combineList)); + return; + } + for (int i = start; i <= n - k + 1; i++) { // 剪枝 + combineList.add(i); + backtracking(combineList, combinations, i + 1, k - 1, n); + combineList.remove(combineList.size() - 1); + } +} +``` + +## 组合求和 + +[39. Combination Sum (Medium)](https://leetcode.com/problems/combination-sum/description/) + +```html +given candidate set [2, 3, 6, 7] and target 7, +A solution set is: +[[7],[2, 2, 3]] +``` + +```java +public List> combinationSum(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + backtracking(new ArrayList<>(), combinations, 0, target, candidates); + return combinations; +} + +private void backtracking(List tempCombination, List> combinations, + int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + backtracking(tempCombination, combinations, i, target - candidates[i], candidates); + tempCombination.remove(tempCombination.size() - 1); + } + } +} +``` + +## 含有相同元素的求组合求和 + +[40. Combination Sum II (Medium)](https://leetcode.com/problems/combination-sum-ii/description/) + +```html +For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, +A solution set is: +[ + [1, 7], + [1, 2, 5], + [2, 6], + [1, 1, 6] +] +``` + +```java +public List> combinationSum2(int[] candidates, int target) { + List> combinations = new ArrayList<>(); + Arrays.sort(candidates); + backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates); + return combinations; +} + +private void backtracking(List tempCombination, List> combinations, + boolean[] hasVisited, int start, int target, final int[] candidates) { + + if (target == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + for (int i = start; i < candidates.length; i++) { + if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) { + continue; + } + if (candidates[i] <= target) { + tempCombination.add(candidates[i]); + hasVisited[i] = true; + backtracking(tempCombination, combinations, hasVisited, i + 1, target - candidates[i], candidates); + hasVisited[i] = false; + tempCombination.remove(tempCombination.size() - 1); + } + } +} +``` + +## 1-9 数字的组合求和 + +[216. Combination Sum III (Medium)](https://leetcode.com/problems/combination-sum-iii/description/) + +```html +Input: k = 3, n = 9 + +Output: + +[[1,2,6], [1,3,5], [2,3,4]] +``` + +从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。 + +```java +public List> combinationSum3(int k, int n) { + List> combinations = new ArrayList<>(); + List path = new ArrayList<>(); + backtracking(k, n, 1, path, combinations); + return combinations; +} + +private void backtracking(int k, int n, int start, + List tempCombination, List> combinations) { + + if (k == 0 && n == 0) { + combinations.add(new ArrayList<>(tempCombination)); + return; + } + if (k == 0 || n == 0) { + return; + } + for (int i = start; i <= 9; i++) { + tempCombination.add(i); + backtracking(k - 1, n - i, i + 1, tempCombination, combinations); + tempCombination.remove(tempCombination.size() - 1); + } +} +``` + +## 子集 + +[78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/) + +找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复 + +```java +public List> subsets(int[] nums) { + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小 + } + return subsets; +} + +private void backtracking(int start, List tempSubset, List> subsets, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + tempSubset.add(nums[i]); + backtracking(i + 1, tempSubset, subsets, size, nums); + tempSubset.remove(tempSubset.size() - 1); + } +} +``` + +## 含有相同元素求子集 + +[90. Subsets II (Medium)](https://leetcode.com/problems/subsets-ii/description/) + +```html +For example, +If nums = [1,2,2], a solution is: + +[ + [2], + [1], + [1,2,2], + [2,2], + [1,2], + [] +] +``` + +```java +public List> subsetsWithDup(int[] nums) { + Arrays.sort(nums); + List> subsets = new ArrayList<>(); + List tempSubset = new ArrayList<>(); + boolean[] hasVisited = new boolean[nums.length]; + for (int size = 0; size <= nums.length; size++) { + backtracking(0, tempSubset, subsets, hasVisited, size, nums); // 不同的子集大小 + } + return subsets; +} + +private void backtracking(int start, List tempSubset, List> subsets, boolean[] hasVisited, + final int size, final int[] nums) { + + if (tempSubset.size() == size) { + subsets.add(new ArrayList<>(tempSubset)); + return; + } + for (int i = start; i < nums.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !hasVisited[i - 1]) { + continue; + } + tempSubset.add(nums[i]); + hasVisited[i] = true; + backtracking(i + 1, tempSubset, subsets, hasVisited, size, nums); + hasVisited[i] = false; + tempSubset.remove(tempSubset.size() - 1); + } +} +``` + +## 分割字符串使得每个部分都是回文数 + +[131. Palindrome Partitioning (Medium)](https://leetcode.com/problems/palindrome-partitioning/description/) + +```html +For example, given s = "aab", +Return + +[ + ["aa","b"], + ["a","a","b"] +] +``` + +```java +public List> partition(String s) { + List> partitions = new ArrayList<>(); + List tempPartition = new ArrayList<>(); + doPartition(s, partitions, tempPartition); + return partitions; +} + +private void doPartition(String s, List> partitions, List tempPartition) { + if (s.length() == 0) { + partitions.add(new ArrayList<>(tempPartition)); + return; + } + for (int i = 0; i < s.length(); i++) { + if (isPalindrome(s, 0, i)) { + tempPartition.add(s.substring(0, i + 1)); + doPartition(s.substring(i + 1), partitions, tempPartition); + tempPartition.remove(tempPartition.size() - 1); + } + } +} + +private boolean isPalindrome(String s, int begin, int end) { + while (begin < end) { + if (s.charAt(begin++) != s.charAt(end--)) { + return false; + } + } + return true; +} +``` + +## 数独 + +[37. Sudoku Solver (Hard)](https://leetcode.com/problems/sudoku-solver/description/) + +

+ +```java +private boolean[][] rowsUsed = new boolean[9][10]; +private boolean[][] colsUsed = new boolean[9][10]; +private boolean[][] cubesUsed = new boolean[9][10]; +private char[][] board; + +public void solveSudoku(char[][] board) { + this.board = board; + for (int i = 0; i < 9; i++) + for (int j = 0; j < 9; j++) { + if (board[i][j] == '.') { + continue; + } + int num = board[i][j] - '0'; + rowsUsed[i][num] = true; + colsUsed[j][num] = true; + cubesUsed[cubeNum(i, j)][num] = true; + } + backtracking(0, 0); +} + +private boolean backtracking(int row, int col) { + while (row < 9 && board[row][col] != '.') { + row = col == 8 ? row + 1 : row; + col = col == 8 ? 0 : col + 1; + } + if (row == 9) { + return true; + } + for (int num = 1; num <= 9; num++) { + if (rowsUsed[row][num] || colsUsed[col][num] || cubesUsed[cubeNum(row, col)][num]) { + continue; + } + rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = true; + board[row][col] = (char) (num + '0'); + if (backtracking(row, col)) { + return true; + } + board[row][col] = '.'; + rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = false; + } + return false; +} + +private int cubeNum(int i, int j) { + int r = i / 3; + int c = j / 3; + return r * 3 + c; +} +``` + +## N 皇后 + +[51. N-Queens (Hard)](https://leetcode.com/problems/n-queens/description/) + +

+ +在 n\*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,求所有的 n 皇后的解。 + +一行一行地摆放,在确定一行中的那个皇后应该摆在哪一列时,需要用三个标记数组来确定某一列是否合法,这三个标记数组分别为:列标记数组、45 度对角线标记数组和 135 度对角线标记数组。 + +45 度对角线标记数组的长度为 2 \* n - 1,通过下图可以明确 (r, c) 的位置所在的数组下标为 r + c。 + +

+ +135 度对角线标记数组的长度也是 2 \* n - 1,(r, c) 的位置所在的数组下标为 n - 1 - (r - c)。 + +

+ +```java +private List> solutions; +private char[][] nQueens; +private boolean[] colUsed; +private boolean[] diagonals45Used; +private boolean[] diagonals135Used; +private int n; + +public List> solveNQueens(int n) { + solutions = new ArrayList<>(); + nQueens = new char[n][n]; + for (int i = 0; i < n; i++) { + Arrays.fill(nQueens[i], '.'); + } + colUsed = new boolean[n]; + diagonals45Used = new boolean[2 * n - 1]; + diagonals135Used = new boolean[2 * n - 1]; + this.n = n; + backtracking(0); + return solutions; +} + +private void backtracking(int row) { + if (row == n) { + List list = new ArrayList<>(); + for (char[] chars : nQueens) { + list.add(new String(chars)); + } + solutions.add(list); + return; + } + + for (int col = 0; col < n; col++) { + int diagonals45Idx = row + col; + int diagonals135Idx = n - 1 - (row - col); + if (colUsed[col] || diagonals45Used[diagonals45Idx] || diagonals135Used[diagonals135Idx]) { + continue; + } + nQueens[row][col] = 'Q'; + colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = true; + backtracking(row + 1); + colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = false; + nQueens[row][col] = '.'; + } +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 数学.md b/docs/notes/Leetcode 题解 - 数学.md new file mode 100644 index 00000000..c97d016d --- /dev/null +++ b/docs/notes/Leetcode 题解 - 数学.md @@ -0,0 +1,515 @@ + +* [素数分解](#素数分解) +* [整除](#整除) + * [最大公约数最小公倍数](#最大公约数最小公倍数) + * [生成素数序列](#生成素数序列) + * [最大公约数](#最大公约数) + * [使用位操作和减法求解最大公约数](#使用位操作和减法求解最大公约数) +* [进制转换](#进制转换) + * [7 进制](#7-进制) + * [16 进制](#16-进制) + * [26 进制](#26-进制) +* [阶乘](#阶乘) + * [统计阶乘尾部有多少个 0](#统计阶乘尾部有多少个-0) +* [字符串加法减法](#字符串加法减法) + * [二进制加法](#二进制加法) + * [字符串加法](#字符串加法) +* [相遇问题](#相遇问题) + * [改变数组元素使所有的数组元素都相等](#改变数组元素使所有的数组元素都相等) + * [解法 1](#解法-1) + * [解法 2](#解法-2) +* [多数投票问题](#多数投票问题) + * [数组中出现次数多于 n / 2 的元素](#数组中出现次数多于-n--2-的元素) +* [其它](#其它) + * [平方数](#平方数) + * [3 的 n 次方](#3-的-n-次方) + * [乘积数组](#乘积数组) + * [找出数组中的乘积最大的三个数](#找出数组中的乘积最大的三个数) + + + +# 素数分解 + +每一个数都可以分解成素数的乘积,例如 84 = 22 \* 31 \* 50 \* 71 \* 110 \* 130 \* 170 \* … + +# 整除 + +令 x = 2m0 \* 3m1 \* 5m2 \* 7m3 \* 11m4 \* … + +令 y = 2n0 \* 3n1 \* 5n2 \* 7n3 \* 11n4 \* … + +如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。 + +## 最大公约数最小公倍数 + +x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) \* 3min(m1,n1) \* 5min(m2,n2) \* ... + +x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) \* 3max(m1,n1) \* 5max(m2,n2) \* ... + +## 生成素数序列 + +[204. Count Primes (Easy)](https://leetcode.com/problems/count-primes/description/) + +埃拉托斯特尼筛法在每次找到一个素数时,将能被素数整除的数排除掉。 + +```java +public int countPrimes(int n) { + boolean[] notPrimes = new boolean[n + 1]; + int count = 0; + for (int i = 2; i < n; i++) { + if (notPrimes[i]) { + continue; + } + count++; + // 从 i * i 开始,因为如果 k < i,那么 k * i 在之前就已经被去除过了 + for (long j = (long) (i) * i; j < n; j += i) { + notPrimes[(int) j] = true; + } + } + return count; +} +``` + +### 最大公约数 + +```java +int gcd(int a, int b) { + return b == 0 ? a : gcd(b, a % b); +} +``` + +最小公倍数为两数的乘积除以最大公约数。 + +```java +int lcm(int a, int b) { + return a * b / gcd(a, b); +} +``` + +## 使用位操作和减法求解最大公约数 + +[编程之美:2.7](#) + +对于 a 和 b 的最大公约数 f(a, b),有: + +- 如果 a 和 b 均为偶数,f(a, b) = 2\*f(a/2, b/2); +- 如果 a 是偶数 b 是奇数,f(a, b) = f(a/2, b); +- 如果 b 是偶数 a 是奇数,f(a, b) = f(a, b/2); +- 如果 a 和 b 均为奇数,f(a, b) = f(b, a-b); + +乘 2 和除 2 都可以转换为移位操作。 + +```java +public int gcd(int a, int b) { + if (a < b) { + return gcd(b, a); + } + if (b == 0) { + return a; + } + boolean isAEven = isEven(a), isBEven = isEven(b); + if (isAEven && isBEven) { + return 2 * gcd(a >> 1, b >> 1); + } else if (isAEven && !isBEven) { + return gcd(a >> 1, b); + } else if (!isAEven && isBEven) { + return gcd(a, b >> 1); + } else { + return gcd(b, a - b); + } +} +``` + +# 进制转换 + +## 7 进制 + +[504. Base 7 (Easy)](https://leetcode.com/problems/base-7/description/) + +```java +public String convertToBase7(int num) { + if (num == 0) { + return "0"; + } + StringBuilder sb = new StringBuilder(); + boolean isNegative = num < 0; + if (isNegative) { + num = -num; + } + while (num > 0) { + sb.append(num % 7); + num /= 7; + } + String ret = sb.reverse().toString(); + return isNegative ? "-" + ret : ret; +} +``` + +Java 中 static String toString(int num, int radix) 可以将一个整数转换为 radix 进制表示的字符串。 + +```java +public String convertToBase7(int num) { + return Integer.toString(num, 7); +} +``` + +## 16 进制 + +[405. Convert a Number to Hexadecimal (Easy)](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/) + +```html +Input: +26 + +Output: +"1a" + +Input: +-1 + +Output: +"ffffffff" +``` + +负数要用它的补码形式。 + +```java +public String toHex(int num) { + char[] map = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + if (num == 0) return "0"; + StringBuilder sb = new StringBuilder(); + while (num != 0) { + sb.append(map[num & 0b1111]); + num >>>= 4; // 因为考虑的是补码形式,因此符号位就不能有特殊的意义,需要使用无符号右移,左边填 0 + } + return sb.reverse().toString(); +} +``` + +## 26 进制 + +[168. Excel Sheet Column Title (Easy)](https://leetcode.com/problems/excel-sheet-column-title/description/) + +```html +1 -> A +2 -> B +3 -> C +... +26 -> Z +27 -> AA +28 -> AB +``` + +因为是从 1 开始计算的,而不是从 0 开始,因此需要对 n 执行 -1 操作。 + +```java +public String convertToTitle(int n) { + if (n == 0) { + return ""; + } + n--; + return convertToTitle(n / 26) + (char) (n % 26 + 'A'); +} +``` + +# 阶乘 + +## 统计阶乘尾部有多少个 0 + +[172. Factorial Trailing Zeroes (Easy)](https://leetcode.com/problems/factorial-trailing-zeroes/description/) + +尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。 + +对于一个数 N,它所包含 5 的个数为:N/5 + N/52 + N/53 + ...,其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5,N/52 表示不大于 N 的数中 52 的倍数再贡献一个 5 ...。 + +```java +public int trailingZeroes(int n) { + return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5); +} +``` + +如果统计的是 N! 的二进制表示中最低位 1 的位置,只要统计有多少个 2 即可,该题目出自 [编程之美:2.2](#) 。和求解有多少个 5 一样,2 的个数为 N/2 + N/22 + N/23 + ... + +# 字符串加法减法 + +## 二进制加法 + +[67. Add Binary (Easy)](https://leetcode.com/problems/add-binary/description/) + +```html +a = "11" +b = "1" +Return "100". +``` + +```java +public String addBinary(String a, String b) { + int i = a.length() - 1, j = b.length() - 1, carry = 0; + StringBuilder str = new StringBuilder(); + while (carry == 1 || i >= 0 || j >= 0) { + if (i >= 0 && a.charAt(i--) == '1') { + carry++; + } + if (j >= 0 && b.charAt(j--) == '1') { + carry++; + } + str.append(carry % 2); + carry /= 2; + } + return str.reverse().toString(); +} +``` + +## 字符串加法 + +[415. Add Strings (Easy)](https://leetcode.com/problems/add-strings/description/) + +字符串的值为非负整数。 + +```java +public String addStrings(String num1, String num2) { + StringBuilder str = new StringBuilder(); + int carry = 0, i = num1.length() - 1, j = num2.length() - 1; + while (carry == 1 || i >= 0 || j >= 0) { + int x = i < 0 ? 0 : num1.charAt(i--) - '0'; + int y = j < 0 ? 0 : num2.charAt(j--) - '0'; + str.append((x + y + carry) % 10); + carry = (x + y + carry) / 10; + } + return str.reverse().toString(); +} +``` + +# 相遇问题 + +## 改变数组元素使所有的数组元素都相等 + +[462. Minimum Moves to Equal Array Elements II (Medium)](https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/description/) + +```html +Input: +[1,2,3] + +Output: +2 + +Explanation: +Only two moves are needed (remember each move increments or decrements one element): + +[1,2,3] => [2,2,3] => [2,2,2] +``` + +每次可以对一个数组元素加一或者减一,求最小的改变次数。 + +这是个典型的相遇问题,移动距离最小的方式是所有元素都移动到中位数。理由如下: + +设 m 为中位数。a 和 b 是 m 两边的两个元素,且 b > a。要使 a 和 b 相等,它们总共移动的次数为 b - a,这个值等于 (b - m) + (m - a),也就是把这两个数移动到中位数的移动次数。 + +设数组长度为 N,则可以找到 N/2 对 a 和 b 的组合,使它们都移动到 m 的位置。 + +## 解法 1 + +先排序,时间复杂度:O(NlogN) + +```java +public int minMoves2(int[] nums) { + Arrays.sort(nums); + int move = 0; + int l = 0, h = nums.length - 1; + while (l <= h) { + move += nums[h] - nums[l]; + l++; + h--; + } + return move; +} +``` + +## 解法 2 + +使用快速选择找到中位数,时间复杂度 O(N) + +```java +public int minMoves2(int[] nums) { + int move = 0; + int median = findKthSmallest(nums, nums.length / 2); + for (int num : nums) { + move += Math.abs(num - median); + } + return move; +} + +private int findKthSmallest(int[] nums, int k) { + int l = 0, h = nums.length - 1; + while (l < h) { + int j = partition(nums, l, h); + if (j == k) { + break; + } + if (j < k) { + l = j + 1; + } else { + h = j - 1; + } + } + return nums[k]; +} + +private int partition(int[] nums, int l, int h) { + int i = l, j = h + 1; + while (true) { + while (nums[++i] < nums[l] && i < h) ; + while (nums[--j] > nums[l] && j > l) ; + if (i >= j) { + break; + } + swap(nums, i, j); + } + swap(nums, l, j); + return j; +} + +private void swap(int[] nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; +} +``` + +# 多数投票问题 + +## 数组中出现次数多于 n / 2 的元素 + +[169. Majority Element (Easy)](https://leetcode.com/problems/majority-element/description/) + +先对数组排序,最中间那个数出现次数一定多于 n / 2。 + +```java +public int majorityElement(int[] nums) { + Arrays.sort(nums); + return nums[nums.length / 2]; +} +``` + +可以利用 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。 + +```java +public int majorityElement(int[] nums) { + int cnt = 0, majority = nums[0]; + for (int num : nums) { + majority = (cnt == 0) ? num : majority; + cnt = (majority == num) ? cnt + 1 : cnt - 1; + } + return majority; +} +``` + +# 其它 + +## 平方数 + +[367. Valid Perfect Square (Easy)](https://leetcode.com/problems/valid-perfect-square/description/) + +```html +Input: 16 +Returns: True +``` + +平方序列:1,4,9,16,.. + +间隔:3,5,7,... + +间隔为等差数列,使用这个特性可以得到从 1 开始的平方序列。 + +```java +public boolean isPerfectSquare(int num) { + int subNum = 1; + while (num > 0) { + num -= subNum; + subNum += 2; + } + return num == 0; +} +``` + +## 3 的 n 次方 + +[326. Power of Three (Easy)](https://leetcode.com/problems/power-of-three/description/) + +```java +public boolean isPowerOfThree(int n) { + return n > 0 && (1162261467 % n == 0); +} +``` + +## 乘积数组 + +[238. Product of Array Except Self (Medium)](https://leetcode.com/problems/product-of-array-except-self/description/) + +```html +For example, given [1,2,3,4], return [24,12,8,6]. +``` + +给定一个数组,创建一个新数组,新数组的每个元素为原始数组中除了该位置上的元素之外所有元素的乘积。 + +要求时间复杂度为 O(N),并且不能使用除法。 + +```java +public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int[] products = new int[n]; + Arrays.fill(products, 1); + int left = 1; + for (int i = 1; i < n; i++) { + left *= nums[i - 1]; + products[i] *= left; + } + int right = 1; + for (int i = n - 2; i >= 0; i--) { + right *= nums[i + 1]; + products[i] *= right; + } + return products; +} +``` + +## 找出数组中的乘积最大的三个数 + +[628. Maximum Product of Three Numbers (Easy)](https://leetcode.com/problems/maximum-product-of-three-numbers/description/) + +```html +Input: [1,2,3,4] +Output: 24 +``` + +```java +public int maximumProduct(int[] nums) { + int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE, min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; + for (int n : nums) { + if (n > max1) { + max3 = max2; + max2 = max1; + max1 = n; + } else if (n > max2) { + max3 = max2; + max2 = n; + } else if (n > max3) { + max3 = n; + } + + if (n < min1) { + min2 = min1; + min1 = n; + } else if (n < min2) { + min2 = n; + } + } + return Math.max(max1*max2*max3, max1*min1*min2); +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 数组与矩阵.md b/docs/notes/Leetcode 题解 - 数组与矩阵.md new file mode 100644 index 00000000..66152af9 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 数组与矩阵.md @@ -0,0 +1,439 @@ + +* [1. 把数组中的 0 移到末尾](#1-把数组中的-0-移到末尾) +* [2. 改变矩阵维度](#2-改变矩阵维度) +* [3. 找出数组中最长的连续 1](#3-找出数组中最长的连续-1) +* [4. 有序矩阵查找](#4-有序矩阵查找) +* [5. 有序矩阵的 Kth Element](#5-有序矩阵的-kth-element) +* [6. 一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数](#6-一个数组元素在-[1,-n]-之间,其中一个数被替换为另一个数,找出重复的数和丢失的数) +* [7. 找出数组中重复的数,数组值在 [1, n] 之间](#7-找出数组中重复的数,数组值在-[1,-n]-之间) +* [8. 数组相邻差值的个数](#8-数组相邻差值的个数) +* [9. 数组的度](#9-数组的度) +* [10. 对角元素相等的矩阵](#10-对角元素相等的矩阵) +* [11. 嵌套数组](#11-嵌套数组) +* [12. 分隔数组](#12-分隔数组) + + + +# 1. 把数组中的 0 移到末尾 + +[283. Move Zeroes (Easy)](https://leetcode.com/problems/move-zeroes/description/) + +```html +For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0]. +``` + +```java +public void moveZeroes(int[] nums) { + int idx = 0; + for (int num : nums) { + if (num != 0) { + nums[idx++] = num; + } + } + while (idx < nums.length) { + nums[idx++] = 0; + } +} +``` + +# 2. 改变矩阵维度 + +[566. Reshape the Matrix (Easy)](https://leetcode.com/problems/reshape-the-matrix/description/) + +```html +Input: +nums = +[[1,2], + [3,4]] +r = 1, c = 4 + +Output: +[[1,2,3,4]] + +Explanation: +The row-traversing of nums is [1,2,3,4]. The new reshaped matrix is a 1 * 4 matrix, fill it row by row by using the previous list. +``` + +```java +public int[][] matrixReshape(int[][] nums, int r, int c) { + int m = nums.length, n = nums[0].length; + if (m * n != r * c) { + return nums; + } + int[][] reshapedNums = new int[r][c]; + int index = 0; + for (int i = 0; i < r; i++) { + for (int j = 0; j < c; j++) { + reshapedNums[i][j] = nums[index / n][index % n]; + index++; + } + } + return reshapedNums; +} +``` + +# 3. 找出数组中最长的连续 1 + +[485. Max Consecutive Ones (Easy)](https://leetcode.com/problems/max-consecutive-ones/description/) + +```java +public int findMaxConsecutiveOnes(int[] nums) { + int max = 0, cur = 0; + for (int x : nums) { + cur = x == 0 ? 0 : cur + 1; + max = Math.max(max, cur); + } + return max; +} +``` + +# 4. 有序矩阵查找 + +[240. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) + +```html +[ + [ 1, 5, 9], + [10, 11, 13], + [12, 13, 15] +] +``` + +```java +public boolean searchMatrix(int[][] matrix, int target) { + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false; + int m = matrix.length, n = matrix[0].length; + int row = 0, col = n - 1; + while (row < m && col >= 0) { + if (target == matrix[row][col]) return true; + else if (target < matrix[row][col]) col--; + else row++; + } + return false; +} +``` + +# 5. 有序矩阵的 Kth Element + +[378. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) + +```html +matrix = [ + [ 1, 5, 9], + [10, 11, 13], + [12, 13, 15] +], +k = 8, + +return 13. +``` + +解题参考:[Share my thoughts and Clean Java Code](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/discuss/85173) + +二分查找解法: + +```java +public int kthSmallest(int[][] matrix, int k) { + int m = matrix.length, n = matrix[0].length; + int lo = matrix[0][0], hi = matrix[m - 1][n - 1]; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + int cnt = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n && matrix[i][j] <= mid; j++) { + cnt++; + } + } + if (cnt < k) lo = mid + 1; + else hi = mid - 1; + } + return lo; +} +``` + +堆解法: + +```java +public int kthSmallest(int[][] matrix, int k) { + int m = matrix.length, n = matrix[0].length; + PriorityQueue pq = new PriorityQueue(); + for(int j = 0; j < n; j++) pq.offer(new Tuple(0, j, matrix[0][j])); + for(int i = 0; i < k - 1; i++) { // 小根堆,去掉 k - 1 个堆顶元素,此时堆顶元素就是第 k 的数 + Tuple t = pq.poll(); + if(t.x == m - 1) continue; + pq.offer(new Tuple(t.x + 1, t.y, matrix[t.x + 1][t.y])); + } + return pq.poll().val; +} + +class Tuple implements Comparable { + int x, y, val; + public Tuple(int x, int y, int val) { + this.x = x; this.y = y; this.val = val; + } + + @Override + public int compareTo(Tuple that) { + return this.val - that.val; + } +} +``` + +# 6. 一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数 + +[645. Set Mismatch (Easy)](https://leetcode.com/problems/set-mismatch/description/) + +```html +Input: nums = [1,2,2,4] +Output: [2,3] +``` + +```html +Input: nums = [1,2,2,4] +Output: [2,3] +``` + +最直接的方法是先对数组进行排序,这种方法时间复杂度为 O(NlogN)。本题可以以 O(N) 的时间复杂度、O(1) 空间复杂度来求解。 + +主要思想是通过交换数组元素,使得数组上的元素在正确的位置上。 + +```java +public int[] findErrorNums(int[] nums) { + for (int i = 0; i < nums.length; i++) { + while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) { + swap(nums, i, nums[i] - 1); + } + } + for (int i = 0; i < nums.length; i++) { + if (nums[i] != i + 1) { + return new int[]{nums[i], i + 1}; + } + } + return null; +} + +private void swap(int[] nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; +} +``` + +类似题目: + +- [448. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素 +- [442. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。 + +# 7. 找出数组中重复的数,数组值在 [1, n] 之间 + +[287. Find the Duplicate Number (Medium)](https://leetcode.com/problems/find-the-duplicate-number/description/) + +要求不能修改数组,也不能使用额外的空间。 + +二分查找解法: + +```java +public int findDuplicate(int[] nums) { + int l = 1, h = nums.length - 1; + while (l <= h) { + int mid = l + (h - l) / 2; + int cnt = 0; + for (int i = 0; i < nums.length; i++) { + if (nums[i] <= mid) cnt++; + } + if (cnt > mid) h = mid - 1; + else l = mid + 1; + } + return l; +} +``` + +双指针解法,类似于有环链表中找出环的入口: + +```java +public int findDuplicate(int[] nums) { + int slow = nums[0], fast = nums[nums[0]]; + while (slow != fast) { + slow = nums[slow]; + fast = nums[nums[fast]]; + } + fast = 0; + while (slow != fast) { + slow = nums[slow]; + fast = nums[fast]; + } + return slow; +} +``` + +# 8. 数组相邻差值的个数 + +[667. Beautiful Arrangement II (Medium)](https://leetcode.com/problems/beautiful-arrangement-ii/description/) + +```html +Input: n = 3, k = 2 +Output: [1, 3, 2] +Explanation: The [1, 3, 2] has three different positive integers ranging from 1 to 3, and the [2, 1] has exactly 2 distinct integers: 1 and 2. +``` + +题目描述:数组元素为 1\~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。 + +让前 k+1 个元素构建出 k 个不相同的差值,序列为:1 k+1 2 k 3 k-1 ... k/2 k/2+1. + +```java +public int[] constructArray(int n, int k) { + int[] ret = new int[n]; + ret[0] = 1; + for (int i = 1, interval = k; i <= k; i++, interval--) { + ret[i] = i % 2 == 1 ? ret[i - 1] + interval : ret[i - 1] - interval; + } + for (int i = k + 1; i < n; i++) { + ret[i] = i + 1; + } + return ret; +} +``` + +# 9. 数组的度 + +[697. Degree of an Array (Easy)](https://leetcode.com/problems/degree-of-an-array/description/) + +```html +Input: [1,2,2,3,1,4,2] +Output: 6 +``` + +题目描述:数组的度定义为元素出现的最高频率,例如上面的数组度为 3。要求找到一个最小的子数组,这个子数组的度和原数组一样。 + +```java +public int findShortestSubArray(int[] nums) { + Map numsCnt = new HashMap<>(); + Map numsLastIndex = new HashMap<>(); + Map numsFirstIndex = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + int num = nums[i]; + numsCnt.put(num, numsCnt.getOrDefault(num, 0) + 1); + numsLastIndex.put(num, i); + if (!numsFirstIndex.containsKey(num)) { + numsFirstIndex.put(num, i); + } + } + int maxCnt = 0; + for (int num : nums) { + maxCnt = Math.max(maxCnt, numsCnt.get(num)); + } + int ret = nums.length; + for (int i = 0; i < nums.length; i++) { + int num = nums[i]; + int cnt = numsCnt.get(num); + if (cnt != maxCnt) continue; + ret = Math.min(ret, numsLastIndex.get(num) - numsFirstIndex.get(num) + 1); + } + return ret; +} +``` + +# 10. 对角元素相等的矩阵 + +[766. Toeplitz Matrix (Easy)](https://leetcode.com/problems/toeplitz-matrix/description/) + +```html +1234 +5123 +9512 + +In the above grid, the diagonals are "[9]", "[5, 5]", "[1, 1, 1]", "[2, 2, 2]", "[3, 3]", "[4]", and in each diagonal all elements are the same, so the answer is True. +``` + +```java +public boolean isToeplitzMatrix(int[][] matrix) { + for (int i = 0; i < matrix[0].length; i++) { + if (!check(matrix, matrix[0][i], 0, i)) { + return false; + } + } + for (int i = 0; i < matrix.length; i++) { + if (!check(matrix, matrix[i][0], i, 0)) { + return false; + } + } + return true; +} + +private boolean check(int[][] matrix, int expectValue, int row, int col) { + if (row >= matrix.length || col >= matrix[0].length) { + return true; + } + if (matrix[row][col] != expectValue) { + return false; + } + return check(matrix, expectValue, row + 1, col + 1); +} +``` + +# 11. 嵌套数组 + +[565. Array Nesting (Medium)](https://leetcode.com/problems/array-nesting/description/) + +```html +Input: A = [5,4,0,3,1,6,2] +Output: 4 +Explanation: +A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2. + +One of the longest S[K]: +S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0} +``` + +题目描述:S[i] 表示一个集合,集合的第一个元素是 A[i],第二个元素是 A[A[i]],如此嵌套下去。求最大的 S[i]。 + +```java +public int arrayNesting(int[] nums) { + int max = 0; + for (int i = 0; i < nums.length; i++) { + int cnt = 0; + for (int j = i; nums[j] != -1; ) { + cnt++; + int t = nums[j]; + nums[j] = -1; // 标记该位置已经被访问 + j = t; + + } + max = Math.max(max, cnt); + } + return max; +} +``` + +# 12. 分隔数组 + +[769. Max Chunks To Make Sorted (Medium)](https://leetcode.com/problems/max-chunks-to-make-sorted/description/) + +```html +Input: arr = [1,0,2,3,4] +Output: 4 +Explanation: +We can split into two chunks, such as [1, 0], [2, 3, 4]. +However, splitting into [1, 0], [2], [3], [4] is the highest number of chunks possible. +``` + +题目描述:分隔数组,使得对每部分排序后数组就为有序。 + +```java +public int maxChunksToSorted(int[] arr) { + if (arr == null) return 0; + int ret = 0; + int right = arr[0]; + for (int i = 0; i < arr.length; i++) { + right = Math.max(right, arr[i]); + if (right == i) ret++; + } + return ret; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 栈和队列.md b/docs/notes/Leetcode 题解 - 栈和队列.md new file mode 100644 index 00000000..e3546535 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 栈和队列.md @@ -0,0 +1,226 @@ + +* [用栈实现队列](#用栈实现队列) +* [用队列实现栈](#用队列实现栈) +* [最小值栈](#最小值栈) +* [用栈实现括号匹配](#用栈实现括号匹配) +* [数组中元素与下一个比它大的元素之间的距离](#数组中元素与下一个比它大的元素之间的距离) +* [循环数组中比当前元素大的下一个元素](#循环数组中比当前元素大的下一个元素) + + + +# 用栈实现队列 + +[232. Implement Queue using Stacks (Easy)](https://leetcode.com/problems/implement-queue-using-stacks/description/) + +栈的顺序为后进先出,而队列的顺序为先进先出。使用两个栈实现队列,一个元素需要经过两个栈才能出队列,在经过第一个栈时元素顺序被反转,经过第二个栈时再次被反转,此时就是先进先出顺序。 + +```java +class MyQueue { + + private Stack in = new Stack<>(); + private Stack out = new Stack<>(); + + public void push(int x) { + in.push(x); + } + + public int pop() { + in2out(); + return out.pop(); + } + + public int peek() { + in2out(); + return out.peek(); + } + + private void in2out() { + if (out.isEmpty()) { + while (!in.isEmpty()) { + out.push(in.pop()); + } + } + } + + public boolean empty() { + return in.isEmpty() && out.isEmpty(); + } +} +``` + +# 用队列实现栈 + +[225. Implement Stack using Queues (Easy)](https://leetcode.com/problems/implement-stack-using-queues/description/) + +在将一个元素 x 插入队列时,为了维护原来的后进先出顺序,需要让 x 插入队列首部。而队列的默认插入顺序是队列尾部,因此在将 x 插入队列尾部之后,需要让除了 x 之外的所有元素出队列,再入队列。 + +```java +class MyStack { + + private Queue queue; + + public MyStack() { + queue = new LinkedList<>(); + } + + public void push(int x) { + queue.add(x); + int cnt = queue.size(); + while (cnt-- > 1) { + queue.add(queue.poll()); + } + } + + public int pop() { + return queue.remove(); + } + + public int top() { + return queue.peek(); + } + + public boolean empty() { + return queue.isEmpty(); + } +} +``` + +# 最小值栈 + +[155. Min Stack (Easy)](https://leetcode.com/problems/min-stack/description/) + +```java +class MinStack { + + private Stack dataStack; + private Stack minStack; + private int min; + + public MinStack() { + dataStack = new Stack<>(); + minStack = new Stack<>(); + min = Integer.MAX_VALUE; + } + + public void push(int x) { + dataStack.add(x); + min = Math.min(min, x); + minStack.add(min); + } + + public void pop() { + dataStack.pop(); + minStack.pop(); + min = minStack.isEmpty() ? Integer.MAX_VALUE : minStack.peek(); + } + + public int top() { + return dataStack.peek(); + } + + public int getMin() { + return minStack.peek(); + } +} +``` + +对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。 + +# 用栈实现括号匹配 + +[20. Valid Parentheses (Easy)](https://leetcode.com/problems/valid-parentheses/description/) + +```html +"()[]{}" + +Output : true +``` + +```java +public boolean isValid(String s) { + Stack stack = new Stack<>(); + for (char c : s.toCharArray()) { + if (c == '(' || c == '{' || c == '[') { + stack.push(c); + } else { + if (stack.isEmpty()) { + return false; + } + char cStack = stack.pop(); + boolean b1 = c == ')' && cStack != '('; + boolean b2 = c == ']' && cStack != '['; + boolean b3 = c == '}' && cStack != '{'; + if (b1 || b2 || b3) { + return false; + } + } + } + return stack.isEmpty(); +} +``` + +# 数组中元素与下一个比它大的元素之间的距离 + +[739. Daily Temperatures (Medium)](https://leetcode.com/problems/daily-temperatures/description/) + +```html +Input: [73, 74, 75, 71, 69, 72, 76, 73] +Output: [1, 1, 4, 2, 1, 1, 0, 0] +``` + +在遍历数组时用栈把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。 + +```java +public int[] dailyTemperatures(int[] temperatures) { + int n = temperatures.length; + int[] dist = new int[n]; + Stack indexs = new Stack<>(); + for (int curIndex = 0; curIndex < n; curIndex++) { + while (!indexs.isEmpty() && temperatures[curIndex] > temperatures[indexs.peek()]) { + int preIndex = indexs.pop(); + dist[preIndex] = curIndex - preIndex; + } + indexs.add(curIndex); + } + return dist; +} +``` + +# 循环数组中比当前元素大的下一个元素 + +[503. Next Greater Element II (Medium)](https://leetcode.com/problems/next-greater-element-ii/description/) + +```text +Input: [1,2,1] +Output: [2,-1,2] +Explanation: The first 1's next greater number is 2; +The number 2 can't find next greater number; +The second 1's next greater number needs to search circularly, which is also 2. +``` + +与 739. Daily Temperatures (Medium) 不同的是,数组是循环数组,并且最后要求的不是距离而是下一个元素。 + +```java +public int[] nextGreaterElements(int[] nums) { + int n = nums.length; + int[] next = new int[n]; + Arrays.fill(next, -1); + Stack pre = new Stack<>(); + for (int i = 0; i < n * 2; i++) { + int num = nums[i % n]; + while (!pre.isEmpty() && nums[pre.peek()] < num) { + next[pre.pop()] = num; + } + if (i < n){ + pre.push(i); + } + } + return next; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 树.md b/docs/notes/Leetcode 题解 - 树.md new file mode 100644 index 00000000..cdf43d7a --- /dev/null +++ b/docs/notes/Leetcode 题解 - 树.md @@ -0,0 +1,1126 @@ + +* [递归](#递归) + * [树的高度](#树的高度) + * [平衡树](#平衡树) + * [两节点的最长路径](#两节点的最长路径) + * [翻转树](#翻转树) + * [归并两棵树](#归并两棵树) + * [判断路径和是否等于一个数](#判断路径和是否等于一个数) + * [统计路径和等于一个数的路径数量](#统计路径和等于一个数的路径数量) + * [子树](#子树) + * [树的对称](#树的对称) + * [最小路径](#最小路径) + * [统计左叶子节点的和](#统计左叶子节点的和) + * [相同节点值的最大路径长度](#相同节点值的最大路径长度) + * [间隔遍历](#间隔遍历) + * [找出二叉树中第二小的节点](#找出二叉树中第二小的节点) +* [层次遍历](#层次遍历) + * [一棵树每层节点的平均数](#一棵树每层节点的平均数) + * [得到左下角的节点](#得到左下角的节点) +* [前中后序遍历](#前中后序遍历) + * [非递归实现二叉树的前序遍历](#非递归实现二叉树的前序遍历) + * [非递归实现二叉树的后序遍历](#非递归实现二叉树的后序遍历) + * [非递归实现二叉树的中序遍历](#非递归实现二叉树的中序遍历) +* [BST](#bst) + * [修剪二叉查找树](#修剪二叉查找树) + * [寻找二叉查找树的第 k 个元素](#寻找二叉查找树的第-k-个元素) + * [把二叉查找树每个节点的值都加上比它大的节点的值](#把二叉查找树每个节点的值都加上比它大的节点的值) + * [二叉查找树的最近公共祖先](#二叉查找树的最近公共祖先) + * [二叉树的最近公共祖先](#二叉树的最近公共祖先) + * [从有序数组中构造二叉查找树](#从有序数组中构造二叉查找树) + * [根据有序链表构造平衡的二叉查找树](#根据有序链表构造平衡的二叉查找树) + * [在二叉查找树中寻找两个节点,使它们的和为一个给定值](#在二叉查找树中寻找两个节点,使它们的和为一个给定值) + * [在二叉查找树中查找两个节点之差的最小绝对值](#在二叉查找树中查找两个节点之差的最小绝对值) + * [寻找二叉查找树中出现次数最多的值](#寻找二叉查找树中出现次数最多的值) +* [Trie](#trie) + * [实现一个 Trie](#实现一个-trie) + * [实现一个 Trie,用来求前缀和](#实现一个-trie,用来求前缀和) + + + +# 递归 + +一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。 + +## 树的高度 + +[104. Maximum Depth of Binary Tree (Easy)](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/) + +```java +public int maxDepth(TreeNode root) { + if (root == null) return 0; + return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; +} +``` + +## 平衡树 + +[110. Balanced Binary Tree (Easy)](https://leetcode.com/problems/balanced-binary-tree/description/) + +```html + 3 + / \ + 9 20 + / \ + 15 7 +``` + +平衡树左右子树高度差都小于等于 1 + +```java +private boolean result = true; + +public boolean isBalanced(TreeNode root) { + maxDepth(root); + return result; +} + +public int maxDepth(TreeNode root) { + if (root == null) return 0; + int l = maxDepth(root.left); + int r = maxDepth(root.right); + if (Math.abs(l - r) > 1) result = false; + return 1 + Math.max(l, r); +} +``` + +## 两节点的最长路径 + +[543. Diameter of Binary Tree (Easy)](https://leetcode.com/problems/diameter-of-binary-tree/description/) + +```html +Input: + + 1 + / \ + 2 3 + / \ + 4 5 + +Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3]. +``` + +```java +private int max = 0; + +public int diameterOfBinaryTree(TreeNode root) { + depth(root); + return max; +} + +private int depth(TreeNode root) { + if (root == null) return 0; + int leftDepth = depth(root.left); + int rightDepth = depth(root.right); + max = Math.max(max, leftDepth + rightDepth); + return Math.max(leftDepth, rightDepth) + 1; +} +``` + +## 翻转树 + +[226. Invert Binary Tree (Easy)](https://leetcode.com/problems/invert-binary-tree/description/) + +```java +public TreeNode invertTree(TreeNode root) { + if (root == null) return null; + TreeNode left = root.left; // 后面的操作会改变 left 指针,因此先保存下来 + root.left = invertTree(root.right); + root.right = invertTree(left); + return root; +} +``` + +## 归并两棵树 + +[617. Merge Two Binary Trees (Easy)](https://leetcode.com/problems/merge-two-binary-trees/description/) + +```html +Input: + Tree 1 Tree 2 + 1 2 + / \ / \ + 3 2 1 3 + / \ \ + 5 4 7 + +Output: + 3 + / \ + 4 5 + / \ \ + 5 4 7 +``` + +```java +public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) return null; + if (t1 == null) return t2; + if (t2 == null) return t1; + TreeNode root = new TreeNode(t1.val + t2.val); + root.left = mergeTrees(t1.left, t2.left); + root.right = mergeTrees(t1.right, t2.right); + return root; +} +``` + +## 判断路径和是否等于一个数 + +[Leetcdoe : 112. Path Sum (Easy)](https://leetcode.com/problems/path-sum/description/) + +```html +Given the below binary tree and sum = 22, + + 5 + / \ + 4 8 + / / \ + 11 13 4 + / \ \ + 7 2 1 + +return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22. +``` + +路径和定义为从 root 到 leaf 的所有节点的和。 + +```java +public boolean hasPathSum(TreeNode root, int sum) { + if (root == null) return false; + if (root.left == null && root.right == null && root.val == sum) return true; + return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); +} +``` + +## 统计路径和等于一个数的路径数量 + +[437. Path Sum III (Easy)](https://leetcode.com/problems/path-sum-iii/description/) + +```html +root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 + + 10 + / \ + 5 -3 + / \ \ + 3 2 11 + / \ \ +3 -2 1 + +Return 3. The paths that sum to 8 are: + +1. 5 -> 3 +2. 5 -> 2 -> 1 +3. -3 -> 11 +``` + +路径不一定以 root 开头,也不一定以 leaf 结尾,但是必须连续。 + +```java +public int pathSum(TreeNode root, int sum) { + if (root == null) return 0; + int ret = pathSumStartWithRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum); + return ret; +} + +private int pathSumStartWithRoot(TreeNode root, int sum) { + if (root == null) return 0; + int ret = 0; + if (root.val == sum) ret++; + ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val); + return ret; +} +``` + +## 子树 + +[572. Subtree of Another Tree (Easy)](https://leetcode.com/problems/subtree-of-another-tree/description/) + +```html +Given tree s: + 3 + / \ + 4 5 + / \ + 1 2 + +Given tree t: + 4 + / \ + 1 2 + +Return true, because t has the same structure and node values with a subtree of s. + +Given tree s: + + 3 + / \ + 4 5 + / \ + 1 2 + / + 0 + +Given tree t: + 4 + / \ + 1 2 + +Return false. +``` + +```java +public boolean isSubtree(TreeNode s, TreeNode t) { + if (s == null) return false; + return isSubtreeWithRoot(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t); +} + +private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) { + if (t == null && s == null) return true; + if (t == null || s == null) return false; + if (t.val != s.val) return false; + return isSubtreeWithRoot(s.left, t.left) && isSubtreeWithRoot(s.right, t.right); +} +``` + +## 树的对称 + +[101. Symmetric Tree (Easy)](https://leetcode.com/problems/symmetric-tree/description/) + +```html + 1 + / \ + 2 2 + / \ / \ +3 4 4 3 +``` + +```java +public boolean isSymmetric(TreeNode root) { + if (root == null) return true; + return isSymmetric(root.left, root.right); +} + +private boolean isSymmetric(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) return true; + if (t1 == null || t2 == null) return false; + if (t1.val != t2.val) return false; + return isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left); +} +``` + +## 最小路径 + +[111. Minimum Depth of Binary Tree (Easy)](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/) + +树的根节点到叶子节点的最小路径长度 + +```java +public int minDepth(TreeNode root) { + if (root == null) return 0; + int left = minDepth(root.left); + int right = minDepth(root.right); + if (left == 0 || right == 0) return left + right + 1; + return Math.min(left, right) + 1; +} +``` + +## 统计左叶子节点的和 + +[404. Sum of Left Leaves (Easy)](https://leetcode.com/problems/sum-of-left-leaves/description/) + +```html + 3 + / \ + 9 20 + / \ + 15 7 + +There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24. +``` + +```java +public int sumOfLeftLeaves(TreeNode root) { + if (root == null) return 0; + if (isLeaf(root.left)) return root.left.val + sumOfLeftLeaves(root.right); + return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right); +} + +private boolean isLeaf(TreeNode node){ + if (node == null) return false; + return node.left == null && node.right == null; +} +``` + +## 相同节点值的最大路径长度 + +[687. Longest Univalue Path (Easy)](https://leetcode.com/problems/longest-univalue-path/) + +```html + 1 + / \ + 4 5 + / \ \ + 4 4 5 + +Output : 2 +``` + +```java +private int path = 0; + +public int longestUnivaluePath(TreeNode root) { + dfs(root); + return path; +} + +private int dfs(TreeNode root){ + if (root == null) return 0; + int left = dfs(root.left); + int right = dfs(root.right); + int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0; + int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0; + path = Math.max(path, leftPath + rightPath); + return Math.max(leftPath, rightPath); +} +``` + +## 间隔遍历 + +[337. House Robber III (Medium)](https://leetcode.com/problems/house-robber-iii/description/) + +```html + 3 + / \ + 2 3 + \ \ + 3 1 +Maximum amount of money the thief can rob = 3 + 3 + 1 = 7. +``` + +```java +public int rob(TreeNode root) { + if (root == null) return 0; + int val1 = root.val; + if (root.left != null) val1 += rob(root.left.left) + rob(root.left.right); + if (root.right != null) val1 += rob(root.right.left) + rob(root.right.right); + int val2 = rob(root.left) + rob(root.right); + return Math.max(val1, val2); +} +``` + +## 找出二叉树中第二小的节点 + +[671. Second Minimum Node In a Binary Tree (Easy)](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/description/) + +```html +Input: + 2 + / \ + 2 5 + / \ + 5 7 + +Output: 5 +``` + +一个节点要么具有 0 个或 2 个子节点,如果有子节点,那么根节点是最小的节点。 + +```java +public int findSecondMinimumValue(TreeNode root) { + if (root == null) return -1; + if (root.left == null && root.right == null) return -1; + int leftVal = root.left.val; + int rightVal = root.right.val; + if (leftVal == root.val) leftVal = findSecondMinimumValue(root.left); + if (rightVal == root.val) rightVal = findSecondMinimumValue(root.right); + if (leftVal != -1 && rightVal != -1) return Math.min(leftVal, rightVal); + if (leftVal != -1) return leftVal; + return rightVal; +} +``` + +# 层次遍历 + +使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 + +## 一棵树每层节点的平均数 + +[637. Average of Levels in Binary Tree (Easy)](https://leetcode.com/problems/average-of-levels-in-binary-tree/description/) + +```java +public List averageOfLevels(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + int cnt = queue.size(); + double sum = 0; + for (int i = 0; i < cnt; i++) { + TreeNode node = queue.poll(); + sum += node.val; + if (node.left != null) queue.add(node.left); + if (node.right != null) queue.add(node.right); + } + ret.add(sum / cnt); + } + return ret; +} +``` + +## 得到左下角的节点 + +[513. Find Bottom Left Tree Value (Easy)](https://leetcode.com/problems/find-bottom-left-tree-value/description/) + +```html +Input: + + 1 + / \ + 2 3 + / / \ + 4 5 6 + / + 7 + +Output: +7 +``` + +```java +public int findBottomLeftValue(TreeNode root) { + Queue queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + root = queue.poll(); + if (root.right != null) queue.add(root.right); + if (root.left != null) queue.add(root.left); + } + return root.val; +} +``` + +# 前中后序遍历 + +```html + 1 + / \ + 2 3 + / \ \ +4 5 6 +``` + +- 层次遍历顺序:[1 2 3 4 5 6] +- 前序遍历顺序:[1 2 4 5 3 6] +- 中序遍历顺序:[4 2 5 1 3 6] +- 后序遍历顺序:[4 5 2 6 3 1] + +层次遍历使用 BFS 实现,利用的就是 BFS 一层一层遍历的特性;而前序、中序、后序遍历利用了 DFS 实现。 + +前序、中序、后序遍只是在对节点访问的顺序有一点不同,其它都相同。 + +① 前序 + +```java +void dfs(TreeNode root) { + visit(root); + dfs(root.left); + dfs(root.right); +} +``` + +② 中序 + +```java +void dfs(TreeNode root) { + dfs(root.left); + visit(root); + dfs(root.right); +} +``` + +③ 后序 + +```java +void dfs(TreeNode root) { + dfs(root.left); + dfs(root.right); + visit(root); +} +``` + +## 非递归实现二叉树的前序遍历 + +[144. Binary Tree Preorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-preorder-traversal/description/) + +```java +public List preorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node == null) continue; + ret.add(node.val); + stack.push(node.right); // 先右后左,保证左子树先遍历 + stack.push(node.left); + } + return ret; +} +``` + +## 非递归实现二叉树的后序遍历 + +[145. Binary Tree Postorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) + +前序遍历为 root -> left -> right,后序遍历为 left -> right -> root。可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。 + +```java +public List postorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node == null) continue; + ret.add(node.val); + stack.push(node.left); + stack.push(node.right); + } + Collections.reverse(ret); + return ret; +} +``` + +## 非递归实现二叉树的中序遍历 + +[94. Binary Tree Inorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) + +```java +public List inorderTraversal(TreeNode root) { + List ret = new ArrayList<>(); + if (root == null) return ret; + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + while (cur != null) { + stack.push(cur); + cur = cur.left; + } + TreeNode node = stack.pop(); + ret.add(node.val); + cur = node.right; + } + return ret; +} +``` + +# BST + +二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。 + +二叉查找树中序遍历有序。 + +## 修剪二叉查找树 + +[669. Trim a Binary Search Tree (Easy)](https://leetcode.com/problems/trim-a-binary-search-tree/description/) + +```html +Input: + + 3 + / \ + 0 4 + \ + 2 + / + 1 + + L = 1 + R = 3 + +Output: + + 3 + / + 2 + / + 1 +``` + +题目描述:只保留值在 L \~ R 之间的节点 + +```java +public TreeNode trimBST(TreeNode root, int L, int R) { + if (root == null) return null; + if (root.val > R) return trimBST(root.left, L, R); + if (root.val < L) return trimBST(root.right, L, R); + root.left = trimBST(root.left, L, R); + root.right = trimBST(root.right, L, R); + return root; +} +``` + +## 寻找二叉查找树的第 k 个元素 + +[230. Kth Smallest Element in a BST (Medium)](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) + + +中序遍历解法: + +```java +private int cnt = 0; +private int val; + +public int kthSmallest(TreeNode root, int k) { + inOrder(root, k); + return val; +} + +private void inOrder(TreeNode node, int k) { + if (node == null) return; + inOrder(node.left, k); + cnt++; + if (cnt == k) { + val = node.val; + return; + } + inOrder(node.right, k); +} +``` + +递归解法: + +```java +public int kthSmallest(TreeNode root, int k) { + int leftCnt = count(root.left); + if (leftCnt == k - 1) return root.val; + if (leftCnt > k - 1) return kthSmallest(root.left, k); + return kthSmallest(root.right, k - leftCnt - 1); +} + +private int count(TreeNode node) { + if (node == null) return 0; + return 1 + count(node.left) + count(node.right); +} +``` + +## 把二叉查找树每个节点的值都加上比它大的节点的值 + +[Convert BST to Greater Tree (Easy)](https://leetcode.com/problems/convert-bst-to-greater-tree/description/) + +```html +Input: The root of a Binary Search Tree like this: + + 5 + / \ + 2 13 + +Output: The root of a Greater Tree like this: + + 18 + / \ + 20 13 +``` + +先遍历右子树。 + +```java +private int sum = 0; + +public TreeNode convertBST(TreeNode root) { + traver(root); + return root; +} + +private void traver(TreeNode node) { + if (node == null) return; + traver(node.right); + sum += node.val; + node.val = sum; + traver(node.left); +} +``` + +## 二叉查找树的最近公共祖先 + +[235. Lowest Common Ancestor of a Binary Search Tree (Easy)](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) + +```html + _______6______ + / \ + ___2__ ___8__ + / \ / \ +0 4 7 9 + / \ + 3 5 + +For example, the lowest common ancestor (LCA) of nodes 2 and 8 is 6. Another example is LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition. +``` + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q); + if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q); + return root; +} +``` + +## 二叉树的最近公共祖先 + +[236. Lowest Common Ancestor of a Binary Tree (Medium) ](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) + +```html + _______3______ + / \ + ___5__ ___1__ + / \ / \ +6 2 0 8 + / \ + 7 4 + +For example, the lowest common ancestor (LCA) of nodes 5 and 1 is 3. Another example is LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition. +``` + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == null || root == p || root == q) return root; + TreeNode left = lowestCommonAncestor(root.left, p, q); + TreeNode right = lowestCommonAncestor(root.right, p, q); + return left == null ? right : right == null ? left : root; +} +``` + +## 从有序数组中构造二叉查找树 + +[108. Convert Sorted Array to Binary Search Tree (Easy)](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) + +```java +public TreeNode sortedArrayToBST(int[] nums) { + return toBST(nums, 0, nums.length - 1); +} + +private TreeNode toBST(int[] nums, int sIdx, int eIdx){ + if (sIdx > eIdx) return null; + int mIdx = (sIdx + eIdx) / 2; + TreeNode root = new TreeNode(nums[mIdx]); + root.left = toBST(nums, sIdx, mIdx - 1); + root.right = toBST(nums, mIdx + 1, eIdx); + return root; +} +``` + +## 根据有序链表构造平衡的二叉查找树 + +[109. Convert Sorted List to Binary Search Tree (Medium)](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/) + +```html +Given the sorted linked list: [-10,-3,0,5,9], + +One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST: + + 0 + / \ + -3 9 + / / + -10 5 +``` + +```java +public TreeNode sortedListToBST(ListNode head) { + if (head == null) return null; + if (head.next == null) return new TreeNode(head.val); + ListNode preMid = preMid(head); + ListNode mid = preMid.next; + preMid.next = null; // 断开链表 + TreeNode t = new TreeNode(mid.val); + t.left = sortedListToBST(head); + t.right = sortedListToBST(mid.next); + return t; +} + +private ListNode preMid(ListNode head) { + ListNode slow = head, fast = head.next; + ListNode pre = head; + while (fast != null && fast.next != null) { + pre = slow; + slow = slow.next; + fast = fast.next.next; + } + return pre; +} +``` + +## 在二叉查找树中寻找两个节点,使它们的和为一个给定值 + +[653. Two Sum IV - Input is a BST (Easy)](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) + +```html +Input: + + 5 + / \ + 3 6 + / \ \ +2 4 7 + +Target = 9 + +Output: True +``` + +使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。 + +应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。 + +```java +public boolean findTarget(TreeNode root, int k) { + List nums = new ArrayList<>(); + inOrder(root, nums); + int i = 0, j = nums.size() - 1; + while (i < j) { + int sum = nums.get(i) + nums.get(j); + if (sum == k) return true; + if (sum < k) i++; + else j--; + } + return false; +} + +private void inOrder(TreeNode root, List nums) { + if (root == null) return; + inOrder(root.left, nums); + nums.add(root.val); + inOrder(root.right, nums); +} +``` + +## 在二叉查找树中查找两个节点之差的最小绝对值 + +[530. Minimum Absolute Difference in BST (Easy)](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) + +```html +Input: + + 1 + \ + 3 + / + 2 + +Output: + +1 +``` + +利用二叉查找树的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。 + +```java +private int minDiff = Integer.MAX_VALUE; +private TreeNode preNode = null; + +public int getMinimumDifference(TreeNode root) { + inOrder(root); + return minDiff; +} + +private void inOrder(TreeNode node) { + if (node == null) return; + inOrder(node.left); + if (preNode != null) minDiff = Math.min(minDiff, node.val - preNode.val); + preNode = node; + inOrder(node.right); +} +``` + +## 寻找二叉查找树中出现次数最多的值 + +[501. Find Mode in Binary Search Tree (Easy)](https://leetcode.com/problems/find-mode-in-binary-search-tree/description/) + +```html + 1 + \ + 2 + / + 2 + +return [2]. +``` + +答案可能不止一个,也就是有多个值出现的次数一样多。 + +```java +private int curCnt = 1; +private int maxCnt = 1; +private TreeNode preNode = null; + +public int[] findMode(TreeNode root) { + List maxCntNums = new ArrayList<>(); + inOrder(root, maxCntNums); + int[] ret = new int[maxCntNums.size()]; + int idx = 0; + for (int num : maxCntNums) { + ret[idx++] = num; + } + return ret; +} + +private void inOrder(TreeNode node, List nums) { + if (node == null) return; + inOrder(node.left, nums); + if (preNode != null) { + if (preNode.val == node.val) curCnt++; + else curCnt = 1; + } + if (curCnt > maxCnt) { + maxCnt = curCnt; + nums.clear(); + nums.add(node.val); + } else if (curCnt == maxCnt) { + nums.add(node.val); + } + preNode = node; + inOrder(node.right, nums); +} +``` + +# Trie + +

+ +Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。 + +## 实现一个 Trie + +[208. Implement Trie (Prefix Tree) (Medium)](https://leetcode.com/problems/implement-trie-prefix-tree/description/) + +```java +class Trie { + + private class Node { + Node[] childs = new Node[26]; + boolean isLeaf; + } + + private Node root = new Node(); + + public Trie() { + } + + public void insert(String word) { + insert(word, root); + } + + private void insert(String word, Node node) { + if (node == null) return; + if (word.length() == 0) { + node.isLeaf = true; + return; + } + int index = indexForChar(word.charAt(0)); + if (node.childs[index] == null) { + node.childs[index] = new Node(); + } + insert(word.substring(1), node.childs[index]); + } + + public boolean search(String word) { + return search(word, root); + } + + private boolean search(String word, Node node) { + if (node == null) return false; + if (word.length() == 0) return node.isLeaf; + int index = indexForChar(word.charAt(0)); + return search(word.substring(1), node.childs[index]); + } + + public boolean startsWith(String prefix) { + return startWith(prefix, root); + } + + private boolean startWith(String prefix, Node node) { + if (node == null) return false; + if (prefix.length() == 0) return true; + int index = indexForChar(prefix.charAt(0)); + return startWith(prefix.substring(1), node.childs[index]); + } + + private int indexForChar(char c) { + return c - 'a'; + } +} +``` + +## 实现一个 Trie,用来求前缀和 + +[677. Map Sum Pairs (Medium)](https://leetcode.com/problems/map-sum-pairs/description/) + +```html +Input: insert("apple", 3), Output: Null +Input: sum("ap"), Output: 3 +Input: insert("app", 2), Output: Null +Input: sum("ap"), Output: 5 +``` + +```java +class MapSum { + + private class Node { + Node[] child = new Node[26]; + int value; + } + + private Node root = new Node(); + + public MapSum() { + + } + + public void insert(String key, int val) { + insert(key, root, val); + } + + private void insert(String key, Node node, int val) { + if (node == null) return; + if (key.length() == 0) { + node.value = val; + return; + } + int index = indexForChar(key.charAt(0)); + if (node.child[index] == null) { + node.child[index] = new Node(); + } + insert(key.substring(1), node.child[index], val); + } + + public int sum(String prefix) { + return sum(prefix, root); + } + + private int sum(String prefix, Node node) { + if (node == null) return 0; + if (prefix.length() != 0) { + int index = indexForChar(prefix.charAt(0)); + return sum(prefix.substring(1), node.child[index]); + } + int sum = node.value; + for (Node child : node.child) { + sum += sum(prefix, child); + } + return sum; + } + + private int indexForChar(char c) { + return c - 'a'; + } +} +``` + + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 目录.md b/docs/notes/Leetcode 题解 - 目录.md new file mode 100644 index 00000000..22764bc6 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 目录.md @@ -0,0 +1,44 @@ + +* [算法思想](#算法思想) +* [数据结构相关](#数据结构相关) +* [参考资料](#参考资料) + + + +# 算法思想 + +- [双指针](Leetcode%20题解%20-%20双指针.md) +- [排序](Leetcode%20题解%20-%20排序.md) +- [贪心思想](Leetcode%20题解%20-%20贪心思想.md) +- [二分查找](Leetcode%20题解%20-%20二分查找.md) +- [分治](Leetcode%20题解%20-%20分治.md) +- [搜索](Leetcode%20题解%20-%20搜索.md) +- [动态规划](Leetcode%20题解%20-%20动态规划.md) +- [数学](Leetcode%20题解%20-%20数学.md) + +# 数据结构相关 + +- [链表](Leetcode%20题解%20-%20链表.md) +- [树](Leetcode%20题解%20-%20树.md) +- [栈和队列](Leetcode%20题解%20-%20栈和队列.md) +- [哈希表](Leetcode%20题解%20-%20哈希表.md) +- [字符串](Leetcode%20题解%20-%20字符串.md) +- [数组与矩阵](Leetcode%20题解%20-%20数组与矩阵.md) +- [图](Leetcode%20题解%20-%20图.md) +- [位运算](Leetcode%20题解%20-%20位运算.md) + +# 参考资料 + + +- Leetcode +- Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004. +- Sedgewick R. Algorithms[M]. Pearson Education India, 1988. +- 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014. +- 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008. +- 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 目录1.md b/docs/notes/Leetcode 题解 - 目录1.md new file mode 100644 index 00000000..80e84238 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 目录1.md @@ -0,0 +1,44 @@ + +* [算法思想](#算法思想) +* [数据结构相关](#数据结构相关) +* [参考资料](#参考资料) + + + +# 算法思想 + +- [双指针](notes/Leetcode%20题解%20-%20双指针.md) +- [排序](notes/Leetcode%20题解%20-%20排序.md) +- [贪心思想](notes/Leetcode%20题解%20-%20贪心思想.md) +- [二分查找](notes/Leetcode%20题解%20-%20二分查找.md) +- [分治](notes/Leetcode%20题解%20-%20分治.md) +- [搜索](notes/Leetcode%20题解%20-%20搜索.md) +- [动态规划](notes/Leetcode%20题解%20-%20动态规划.md) +- [数学](notes/Leetcode%20题解%20-%20数学.md) + +# 数据结构相关 + +- [链表](notes/Leetcode%20题解%20-%20链表.md) +- [树](notes/Leetcode%20题解%20-%20树.md) +- [栈和队列](notes/Leetcode%20题解%20-%20栈和队列.md) +- [哈希表](notes/Leetcode%20题解%20-%20哈希表.md) +- [字符串](notes/Leetcode%20题解%20-%20字符串.md) +- [数组与矩阵](notes/Leetcode%20题解%20-%20数组与矩阵.md) +- [图](notes/Leetcode%20题解%20-%20图.md) +- [位运算](notes/Leetcode%20题解%20-%20位运算.md) + +# 参考资料 + + +- Leetcode +- Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004. +- Sedgewick R. Algorithms[M]. Pearson Education India, 1988. +- 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014. +- 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008. +- 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 贪心思想.md b/docs/notes/Leetcode 题解 - 贪心思想.md new file mode 100644 index 00000000..6249fdbe --- /dev/null +++ b/docs/notes/Leetcode 题解 - 贪心思想.md @@ -0,0 +1,372 @@ + +* [分配饼干](#分配饼干) +* [不重叠的区间个数](#不重叠的区间个数) +* [投飞镖刺破气球](#投飞镖刺破气球) +* [根据身高和序号重组队列](#根据身高和序号重组队列) +* [分隔字符串使同种字符出现在一起](#分隔字符串使同种字符出现在一起) +* [种植花朵](#种植花朵) +* [判断是否为子序列](#判断是否为子序列) +* [修改一个数成为非递减数组](#修改一个数成为非递减数组) +* [股票的最大收益](#股票的最大收益) +* [子数组最大的和](#子数组最大的和) +* [买入和售出股票最大的收益](#买入和售出股票最大的收益) + + + +保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 + +# 分配饼干 + +[455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/) + +```html +Input: [1,2], [1,2,3] +Output: 2 + +Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. +You have 3 cookies and their sizes are big enough to gratify all of the children, +You need to output 2. +``` + +题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 + +给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。 + +证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。 + +```java +public int findContentChildren(int[] g, int[] s) { + Arrays.sort(g); + Arrays.sort(s); + int gi = 0, si = 0; + while (gi < g.length && si < s.length) { + if (g[gi] <= s[si]) { + gi++; + } + si++; + } + return gi; +} +``` + +# 不重叠的区间个数 + +[435. Non-overlapping Intervals (Medium)](https://leetcode.com/problems/non-overlapping-intervals/description/) + +```html +Input: [ [1,2], [1,2], [1,2] ] + +Output: 2 + +Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping. +``` + +```html +Input: [ [1,2], [2,3] ] + +Output: 0 + +Explanation: You don't need to remove any of the intervals since they're already non-overlapping. +``` + +题目描述:计算让一组区间不重叠所需要移除的区间个数。 + +先计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。 + +在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 + +按区间的结尾进行排序,每次选择结尾最小,并且和前一个区间不重叠的区间。 + +```java +public int eraseOverlapIntervals(Interval[] intervals) { + if (intervals.length == 0) { + return 0; + } + Arrays.sort(intervals, Comparator.comparingInt(o -> o.end)); + int cnt = 1; + int end = intervals[0].end; + for (int i = 1; i < intervals.length; i++) { + if (intervals[i].start < end) { + continue; + } + end = intervals[i].end; + cnt++; + } + return intervals.length - cnt; +} +``` + +使用 lambda 表示式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句: + +```java +Arrays.sort(intervals, new Comparator() { + @Override + public int compare(Interval o1, Interval o2) { + return o1.end - o2.end; + } +}); +``` + +# 投飞镖刺破气球 + +[452. Minimum Number of Arrows to Burst Balloons (Medium)](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) + +``` +Input: +[[10,16], [2,8], [1,6], [7,12]] + +Output: +2 +``` + +题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。 + +也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。 + +```java +public int findMinArrowShots(int[][] points) { + if (points.length == 0) { + return 0; + } + Arrays.sort(points, Comparator.comparingInt(o -> o[1])); + int cnt = 1, end = points[0][1]; + for (int i = 1; i < points.length; i++) { + if (points[i][0] <= end) { + continue; + } + cnt++; + end = points[i][1]; + } + return cnt; +} +``` + +# 根据身高和序号重组队列 + +[406. Queue Reconstruction by Height(Medium)](https://leetcode.com/problems/queue-reconstruction-by-height/description/) + +```html +Input: +[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] + +Output: +[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] +``` + +题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。 + +为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。 + +身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。 + +```java +public int[][] reconstructQueue(int[][] people) { + if (people == null || people.length == 0 || people[0].length == 0) { + return new int[0][0]; + } + Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0])); + List queue = new ArrayList<>(); + for (int[] p : people) { + queue.add(p[1], p); + } + return queue.toArray(new int[queue.size()][]); +} +``` + +# 分隔字符串使同种字符出现在一起 + +[763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/) + +```html +Input: S = "ababcbacadefegdehijhklij" +Output: [9,7,8] +Explanation: +The partition is "ababcbaca", "defegde", "hijhklij". +This is a partition so that each letter appears in at most one part. +A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts. +``` + +```java +public List partitionLabels(String S) { + int[] lastIndexsOfChar = new int[26]; + for (int i = 0; i < S.length(); i++) { + lastIndexsOfChar[char2Index(S.charAt(i))] = i; + } + List partitions = new ArrayList<>(); + int firstIndex = 0; + while (firstIndex < S.length()) { + int lastIndex = firstIndex; + for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) { + int index = lastIndexsOfChar[char2Index(S.charAt(i))]; + if (index > lastIndex) { + lastIndex = index; + } + } + partitions.add(lastIndex - firstIndex + 1); + firstIndex = lastIndex + 1; + } + return partitions; +} + +private int char2Index(char c) { + return c - 'a'; +} +``` + + +# 种植花朵 + +[605. Can Place Flowers (Easy)](https://leetcode.com/problems/can-place-flowers/description/) + +```html +Input: flowerbed = [1,0,0,0,1], n = 1 +Output: True +``` + +题目描述:花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。 + +```java +public boolean canPlaceFlowers(int[] flowerbed, int n) { + int len = flowerbed.length; + int cnt = 0; + for (int i = 0; i < len && cnt < n; i++) { + if (flowerbed[i] == 1) { + continue; + } + int pre = i == 0 ? 0 : flowerbed[i - 1]; + int next = i == len - 1 ? 0 : flowerbed[i + 1]; + if (pre == 0 && next == 0) { + cnt++; + flowerbed[i] = 1; + } + } + return cnt >= n; +} +``` + +# 判断是否为子序列 + +[392. Is Subsequence (Medium)](https://leetcode.com/problems/is-subsequence/description/) + +```html +s = "abc", t = "ahbgdc" +Return true. +``` + +```java +public boolean isSubsequence(String s, String t) { + int index = -1; + for (char c : s.toCharArray()) { + index = t.indexOf(c, index + 1); + if (index == -1) { + return false; + } + } + return true; +} +``` + +# 修改一个数成为非递减数组 + +[665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/) + +```html +Input: [4,2,3] +Output: True +Explanation: You could modify the first 4 to 1 to get a non-decreasing array. +``` + +题目描述:判断一个数组能不能只修改一个数就成为非递减数组。 + +在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。 + +```java +public boolean checkPossibility(int[] nums) { + int cnt = 0; + for (int i = 1; i < nums.length && cnt < 2; i++) { + if (nums[i] >= nums[i - 1]) { + continue; + } + cnt++; + if (i - 2 >= 0 && nums[i - 2] > nums[i]) { + nums[i] = nums[i - 1]; + } else { + nums[i - 1] = nums[i]; + } + } + return cnt <= 1; +} +``` + +# 股票的最大收益 + +[122. Best Time to Buy and Sell Stock II (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) + +题目描述:一次股票交易包含买入和卖出,多个交易之间不能交叉进行。 + +对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中,从而在局部最优的情况下也保证全局最优。 + +```java +public int maxProfit(int[] prices) { + int profit = 0; + for (int i = 1; i < prices.length; i++) { + if (prices[i] > prices[i - 1]) { + profit += (prices[i] - prices[i - 1]); + } + } + return profit; +} +``` + +# 子数组最大的和 + +[53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/) + +```html +For example, given the array [-2,1,-3,4,-1,2,1,-5,4], +the contiguous subarray [4,-1,2,1] has the largest sum = 6. +``` + +```java +public int maxSubArray(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int preSum = nums[0]; + int maxSum = preSum; + for (int i = 1; i < nums.length; i++) { + preSum = preSum > 0 ? preSum + nums[i] : nums[i]; + maxSum = Math.max(maxSum, preSum); + } + return maxSum; +} +``` + +# 买入和售出股票最大的收益 + +[121. Best Time to Buy and Sell Stock (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) + +题目描述:只进行一次交易。 + +只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 + +```java +public int maxProfit(int[] prices) { + int n = prices.length; + if (n == 0) return 0; + int soFarMin = prices[0]; + int max = 0; + for (int i = 1; i < n; i++) { + if (soFarMin > prices[i]) soFarMin = prices[i]; + else max = Math.max(max, prices[i] - soFarMin); + } + return max; +} +``` + + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解 - 链表.md b/docs/notes/Leetcode 题解 - 链表.md new file mode 100644 index 00000000..67de7e64 --- /dev/null +++ b/docs/notes/Leetcode 题解 - 链表.md @@ -0,0 +1,333 @@ + +* [找出两个链表的交点](#找出两个链表的交点) +* [链表反转](#链表反转) +* [归并两个有序的链表](#归并两个有序的链表) +* [从有序链表中删除重复节点](#从有序链表中删除重复节点) +* [删除链表的倒数第 n 个节点](#删除链表的倒数第-n-个节点) +* [交换链表中的相邻结点](#交换链表中的相邻结点) +* [链表求和](#链表求和) +* [回文链表](#回文链表) +* [分隔链表](#分隔链表) +* [链表元素按奇偶聚集](#链表元素按奇偶聚集) + + + +链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。 + +# 找出两个链表的交点 + +[160. Intersection of Two Linked Lists (Easy)](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) + +```html +A: a1 → a2 + ↘ + c1 → c2 → c3 + ↗ +B: b1 → b2 → b3 +``` + +要求:时间复杂度为 O(N),空间复杂度为 O(1) + +设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 + +当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 + +```java +public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + ListNode l1 = headA, l2 = headB; + while (l1 != l2) { + l1 = (l1 == null) ? headB : l1.next; + l2 = (l2 == null) ? headA : l2.next; + } + return l1; +} +``` + +如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美 3.6]() 的问题。有两种解法: + +- 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环; +- 或者直接比较两个链表的最后一个节点是否相同。 + +# 链表反转 + +[206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/) + +递归 + +```java +public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode next = head.next; + ListNode newHead = reverseList(next); + next.next = head; + head.next = null; + return newHead; +} +``` + +头插法 + +```java +public ListNode reverseList(ListNode head) { + ListNode newHead = new ListNode(-1); + while (head != null) { + ListNode next = head.next; + head.next = newHead.next; + newHead.next = head; + head = next; + } + return newHead.next; +} +``` + +# 归并两个有序的链表 + +[21. Merge Two Sorted Lists (Easy)](https://leetcode.com/problems/merge-two-sorted-lists/description/) + +```java +public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + if (l1 == null) return l2; + if (l2 == null) return l1; + if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l1, l2.next); + return l2; + } +} +``` + +# 从有序链表中删除重复节点 + +[83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) + +```html +Given 1->1->2, return 1->2. +Given 1->1->2->3->3, return 1->2->3. +``` + +```java +public ListNode deleteDuplicates(ListNode head) { + if (head == null || head.next == null) return head; + head.next = deleteDuplicates(head.next); + return head.val == head.next.val ? head.next : head; +} +``` + +# 删除链表的倒数第 n 个节点 + +[19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) + +```html +Given linked list: 1->2->3->4->5, and n = 2. +After removing the second node from the end, the linked list becomes 1->2->3->5. +``` + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode fast = head; + while (n-- > 0) { + fast = fast.next; + } + if (fast == null) return head.next; + ListNode slow = head; + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + slow.next = slow.next.next; + return head; +} +``` + +# 交换链表中的相邻结点 + +[24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/) + +```html +Given 1->2->3->4, you should return the list as 2->1->4->3. +``` + +题目要求:不能修改结点的 val 值,O(1) 空间复杂度。 + +```java +public ListNode swapPairs(ListNode head) { + ListNode node = new ListNode(-1); + node.next = head; + ListNode pre = node; + while (pre.next != null && pre.next.next != null) { + ListNode l1 = pre.next, l2 = pre.next.next; + ListNode next = l2.next; + l1.next = next; + l2.next = l1; + pre.next = l2; + + pre = l1; + } + return node.next; +} +``` + +# 链表求和 + +[445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/) + +```html +Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) +Output: 7 -> 8 -> 0 -> 7 +``` + +题目要求:不能修改原始链表。 + +```java +public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + Stack l1Stack = buildStack(l1); + Stack l2Stack = buildStack(l2); + ListNode head = new ListNode(-1); + int carry = 0; + while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) { + int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); + int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); + int sum = x + y + carry; + ListNode node = new ListNode(sum % 10); + node.next = head.next; + head.next = node; + carry = sum / 10; + } + return head.next; +} + +private Stack buildStack(ListNode l) { + Stack stack = new Stack<>(); + while (l != null) { + stack.push(l.val); + l = l.next; + } + return stack; +} +``` + +# 回文链表 + +[234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) + +题目要求:以 O(1) 的空间复杂度来求解。 + +切成两半,把后半段反转,然后比较两半是否相等。 + +```java +public boolean isPalindrome(ListNode head) { + if (head == null || head.next == null) return true; + ListNode slow = head, fast = head.next; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + if (fast != null) slow = slow.next; // 偶数节点,让 slow 指向下一个节点 + cut(head, slow); // 切成两个链表 + return isEqual(head, reverse(slow)); +} + +private void cut(ListNode head, ListNode cutNode) { + while (head.next != cutNode) { + head = head.next; + } + head.next = null; +} + +private ListNode reverse(ListNode head) { + ListNode newHead = null; + while (head != null) { + ListNode nextNode = head.next; + head.next = newHead; + newHead = head; + head = nextNode; + } + return newHead; +} + +private boolean isEqual(ListNode l1, ListNode l2) { + while (l1 != null && l2 != null) { + if (l1.val != l2.val) return false; + l1 = l1.next; + l2 = l2.next; + } + return true; +} +``` + +# 分隔链表 + +[725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/) + +```html +Input: +root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 +Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] +Explanation: +The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. +``` + +题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 + +```java +public ListNode[] splitListToParts(ListNode root, int k) { + int N = 0; + ListNode cur = root; + while (cur != null) { + N++; + cur = cur.next; + } + int mod = N % k; + int size = N / k; + ListNode[] ret = new ListNode[k]; + cur = root; + for (int i = 0; cur != null && i < k; i++) { + ret[i] = cur; + int curSize = size + (mod-- > 0 ? 1 : 0); + for (int j = 0; j < curSize - 1; j++) { + cur = cur.next; + } + ListNode next = cur.next; + cur.next = null; + cur = next; + } + return ret; +} +``` + +# 链表元素按奇偶聚集 + +[328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/) + +```html +Example: +Given 1->2->3->4->5->NULL, +return 1->3->5->2->4->NULL. +``` + +```java +public ListNode oddEvenList(ListNode head) { + if (head == null) { + return head; + } + ListNode odd = head, even = head.next, evenHead = even; + while (even != null && even.next != null) { + odd.next = odd.next.next; + odd = odd.next; + even.next = even.next.next; + even = even.next; + } + odd.next = evenHead; + return head; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Leetcode 题解.md b/docs/notes/Leetcode 题解.md deleted file mode 100644 index ef41be44..00000000 --- a/docs/notes/Leetcode 题解.md +++ /dev/null @@ -1,7111 +0,0 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - -* [算法思想](#算法思想) - * [双指针](#双指针) - * [排序](#排序) - * [快速选择](#快速选择) - * [堆排序](#堆排序) - * [桶排序](#桶排序) - * [荷兰国旗问题](#荷兰国旗问题) - * [贪心思想](#贪心思想) - * [二分查找](#二分查找) - * [分治](#分治) - * [搜索](#搜索) - * [BFS](#bfs) - * [DFS](#dfs) - * [Backtracking](#backtracking) - * [动态规划](#动态规划) - * [斐波那契数列](#斐波那契数列) - * [矩阵路径](#矩阵路径) - * [数组区间](#数组区间) - * [分割整数](#分割整数) - * [最长递增子序列](#最长递增子序列) - * [最长公共子序列](#最长公共子序列) - * [0-1 背包](#0-1-背包) - * [股票交易](#股票交易) - * [字符串编辑](#字符串编辑) - * [数学](#数学) - * [素数](#素数) - * [最大公约数](#最大公约数) - * [进制转换](#进制转换) - * [阶乘](#阶乘) - * [字符串加法减法](#字符串加法减法) - * [相遇问题](#相遇问题) - * [多数投票问题](#多数投票问题) - * [其它](#其它) -* [数据结构相关](#数据结构相关) - * [链表](#链表) - * [树](#树) - * [递归](#递归) - * [层次遍历](#层次遍历) - * [前中后序遍历](#前中后序遍历) - * [BST](#bst) - * [Trie](#trie) - * [栈和队列](#栈和队列) - * [哈希表](#哈希表) - * [字符串](#字符串) - * [数组与矩阵](#数组与矩阵) - * [图](#图) - * [二分图](#二分图) - * [拓扑排序](#拓扑排序) - * [并查集](#并查集) - * [位运算](#位运算) -* [参考资料](#参考资料) - - - -# 算法思想 - -## 双指针 - -双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 - -**有序数组的 Two Sum** - -[Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) - -```html -Input: numbers={2, 7, 11, 15}, target=9 -Output: index1=1, index2=2 -``` - -题目描述:在有序数组中找出两个数,使它们的和为 target。 - -使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 - -- 如果两个指针指向元素的和 sum == target,那么得到要求的结果; -- 如果 sum > target,移动较大的元素,使 sum 变小一些; -- 如果 sum < target,移动较小的元素,使 sum 变大一些。 - -```java -public int[] twoSum(int[] numbers, int target) { - int i = 0, j = numbers.length - 1; - while (i < j) { - int sum = numbers[i] + numbers[j]; - if (sum == target) { - return new int[]{i + 1, j + 1}; - } else if (sum < target) { - i++; - } else { - j--; - } - } - return null; -} -``` - -**两数平方和** - -[633. Sum of Square Numbers (Easy)](https://leetcode.com/problems/sum-of-square-numbers/description/) - -```html -Input: 5 -Output: True -Explanation: 1 * 1 + 2 * 2 = 5 -``` - -题目描述:判断一个数是否为两个数的平方和。 - -```java -public boolean judgeSquareSum(int c) { - int i = 0, j = (int) Math.sqrt(c); - while (i <= j) { - int powSum = i * i + j * j; - if (powSum == c) { - return true; - } else if (powSum > c) { - j--; - } else { - i++; - } - } - return false; -} -``` - -**反转字符串中的元音字符** - -[345. Reverse Vowels of a String (Easy)](https://leetcode.com/problems/reverse-vowels-of-a-string/description/) - -```html -Given s = "leetcode", return "leotcede". -``` - -使用双指针指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。 - -```java -private final static HashSet vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); - -public String reverseVowels(String s) { - int i = 0, j = s.length() - 1; - char[] result = new char[s.length()]; - while (i <= j) { - char ci = s.charAt(i); - char cj = s.charAt(j); - if (!vowels.contains(ci)) { - result[i++] = ci; - } else if (!vowels.contains(cj)) { - result[j--] = cj; - } else { - result[i++] = cj; - result[j--] = ci; - } - } - return new String(result); -} -``` - -**回文字符串** - -[680. Valid Palindrome II (Easy)](https://leetcode.com/problems/valid-palindrome-ii/description/) - -```html -Input: "abca" -Output: True -Explanation: You could delete the character 'c'. -``` - -题目描述:可以删除一个字符,判断是否能构成回文字符串。 - -```java -public boolean validPalindrome(String s) { - int i = -1, j = s.length(); - while (++i < --j) { - if (s.charAt(i) != s.charAt(j)) { - return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j); - } - } - return true; -} - -private boolean isPalindrome(String s, int i, int j) { - while (i < j) { - if (s.charAt(i++) != s.charAt(j--)) { - return false; - } - } - return true; -} -``` - -**归并两个有序数组** - -[88. Merge Sorted Array (Easy)](https://leetcode.com/problems/merge-sorted-array/description/) - -```html -Input: -nums1 = [1,2,3,0,0,0], m = 3 -nums2 = [2,5,6], n = 3 - -Output: [1,2,2,3,5,6] -``` - -题目描述:把归并结果存到第一个数组上。 - -需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值。 - -```java -public void merge(int[] nums1, int m, int[] nums2, int n) { - int index1 = m - 1, index2 = n - 1; - int indexMerge = m + n - 1; - while (index1 >= 0 || index2 >= 0) { - if (index1 < 0) { - nums1[indexMerge--] = nums2[index2--]; - } else if (index2 < 0) { - nums1[indexMerge--] = nums1[index1--]; - } else if (nums1[index1] > nums2[index2]) { - nums1[indexMerge--] = nums1[index1--]; - } else { - nums1[indexMerge--] = nums2[index2--]; - } - } -} -``` - -**判断链表是否存在环** - -[141. Linked List Cycle (Easy)](https://leetcode.com/problems/linked-list-cycle/description/) - -使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。 - -```java -public boolean hasCycle(ListNode head) { - if (head == null) { - return false; - } - ListNode l1 = head, l2 = head.next; - while (l1 != null && l2 != null && l2.next != null) { - if (l1 == l2) { - return true; - } - l1 = l1.next; - l2 = l2.next.next; - } - return false; -} -``` - -**最长子序列** - -[524. Longest Word in Dictionary through Deleting (Medium)](https://leetcode.com/problems/longest-word-in-dictionary-through-deleting/description/) - -``` -Input: -s = "abpcplea", d = ["ale","apple","monkey","plea"] - -Output: -"apple" -``` - -题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回字典序的最小字符串。 - -```java -public String findLongestWord(String s, List d) { - String longestWord = ""; - for (String target : d) { - int l1 = longestWord.length(), l2 = target.length(); - if (l1 > l2 || (l1 == l2 && longestWord.compareTo(target) < 0)) { - continue; - } - if (isValid(s, target)) { - longestWord = target; - } - } - return longestWord; -} - -private boolean isValid(String s, String target) { - int i = 0, j = 0; - while (i < s.length() && j < target.length()) { - if (s.charAt(i) == target.charAt(j)) { - j++; - } - i++; - } - return j == target.length(); -} -``` - -## 排序 - -### 快速选择 - -用于求解 **Kth Element** 问题,使用快速排序的 partition() 进行实现。 - -需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。 - -### 堆排序 - -用于求解 **TopK Elements** 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。 - -堆排序也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。 - -快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。 - -可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。 - -**Kth Element** - -[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) - -题目描述:找到第 k 大的元素。 - -**排序** :时间复杂度 O(NlogN),空间复杂度 O(1) - -```java -public int findKthLargest(int[] nums, int k) { - Arrays.sort(nums); - return nums[nums.length - k]; -} -``` - -**堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。 - -```java -public int findKthLargest(int[] nums, int k) { - PriorityQueue pq = new PriorityQueue<>(); // 小顶堆 - for (int val : nums) { - pq.add(val); - if (pq.size() > k) // 维护堆的大小为 K - pq.poll(); - } - return pq.peek(); -} -``` - -**快速选择** :时间复杂度 O(N),空间复杂度 O(1) - -```java -public int findKthLargest(int[] nums, int k) { - k = nums.length - k; - int l = 0, h = nums.length - 1; - while (l < h) { - int j = partition(nums, l, h); - if (j == k) { - break; - } else if (j < k) { - l = j + 1; - } else { - h = j - 1; - } - } - return nums[k]; -} - -private int partition(int[] a, int l, int h) { - int i = l, j = h + 1; - while (true) { - while (a[++i] < a[l] && i < h) ; - while (a[--j] > a[l] && j > l) ; - if (i >= j) { - break; - } - swap(a, i, j); - } - swap(a, l, j); - return j; -} - -private void swap(int[] a, int i, int j) { - int t = a[i]; - a[i] = a[j]; - a[j] = t; -} -``` - -### 桶排序 - -**出现频率最多的 k 个数** - -[347. Top K Frequent Elements (Medium)](https://leetcode.com/problems/top-k-frequent-elements/description/) - -```html -Given [1,1,1,2,2,3] and k = 2, return [1,2]. -``` - -设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。 - -把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。 - -```java -public List topKFrequent(int[] nums, int k) { - Map frequencyForNum = new HashMap<>(); - for (int num : nums) { - frequencyForNum.put(num, frequencyForNum.getOrDefault(num, 0) + 1); - } - List[] buckets = new ArrayList[nums.length + 1]; - for (int key : frequencyForNum.keySet()) { - int frequency = frequencyForNum.get(key); - if (buckets[frequency] == null) { - buckets[frequency] = new ArrayList<>(); - } - buckets[frequency].add(key); - } - List topK = new ArrayList<>(); - for (int i = buckets.length - 1; i >= 0 && topK.size() < k; i--) { - if (buckets[i] == null) { - continue; - } - if (buckets[i].size() <= (k - topK.size())) { - topK.addAll(buckets[i]); - } else { - topK.addAll(buckets[i].subList(0, k - topK.size())); - } - } - return topK; -} -``` - -**按照字符出现次数对字符串排序** - -[451. Sort Characters By Frequency (Medium)](https://leetcode.com/problems/sort-characters-by-frequency/description/) - -```html -Input: -"tree" - -Output: -"eert" - -Explanation: -'e' appears twice while 'r' and 't' both appear once. -So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer. -``` - -```java -public String frequencySort(String s) { - Map frequencyForNum = new HashMap<>(); - for (char c : s.toCharArray()) - frequencyForNum.put(c, frequencyForNum.getOrDefault(c, 0) + 1); - - List[] frequencyBucket = new ArrayList[s.length() + 1]; - for (char c : frequencyForNum.keySet()) { - int f = frequencyForNum.get(c); - if (frequencyBucket[f] == null) { - frequencyBucket[f] = new ArrayList<>(); - } - frequencyBucket[f].add(c); - } - StringBuilder str = new StringBuilder(); - for (int i = frequencyBucket.length - 1; i >= 0; i--) { - if (frequencyBucket[i] == null) { - continue; - } - for (char c : frequencyBucket[i]) { - for (int j = 0; j < i; j++) { - str.append(c); - } - } - } - return str.toString(); -} -``` - -### 荷兰国旗问题 - -荷兰国旗包含三种颜色:红、白、蓝。 - -有三种颜色的球,算法的目标是将这三种球按颜色顺序正确地排列。 - -它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。 - -

- -**按颜色进行排序** - -[75. Sort Colors (Medium)](https://leetcode.com/problems/sort-colors/description/) - -```html -Input: [2,0,2,1,1,0] -Output: [0,0,1,1,2,2] -``` - -题目描述:只有 0/1/2 三种颜色。 - -```java -public void sortColors(int[] nums) { - int zero = -1, one = 0, two = nums.length; - while (one < two) { - if (nums[one] == 0) { - swap(nums, ++zero, one++); - } else if (nums[one] == 2) { - swap(nums, --two, one); - } else { - ++one; - } - } -} - -private void swap(int[] nums, int i, int j) { - int t = nums[i]; - nums[i] = nums[j]; - nums[j] = t; -} -``` - -## 贪心思想 - -保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 - -**分配饼干** - -[455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/) - -```html -Input: [1,2], [1,2,3] -Output: 2 - -Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. -You have 3 cookies and their sizes are big enough to gratify all of the children, -You need to output 2. -``` - -题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 - -给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。 - -证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。 - -```java -public int findContentChildren(int[] g, int[] s) { - Arrays.sort(g); - Arrays.sort(s); - int gi = 0, si = 0; - while (gi < g.length && si < s.length) { - if (g[gi] <= s[si]) { - gi++; - } - si++; - } - return gi; -} -``` - -**不重叠的区间个数** - -[435. Non-overlapping Intervals (Medium)](https://leetcode.com/problems/non-overlapping-intervals/description/) - -```html -Input: [ [1,2], [1,2], [1,2] ] - -Output: 2 - -Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping. -``` - -```html -Input: [ [1,2], [2,3] ] - -Output: 0 - -Explanation: You don't need to remove any of the intervals since they're already non-overlapping. -``` - -题目描述:计算让一组区间不重叠所需要移除的区间个数。 - -先计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。 - -在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 - -按区间的结尾进行排序,每次选择结尾最小,并且和前一个区间不重叠的区间。 - -```java -public int eraseOverlapIntervals(Interval[] intervals) { - if (intervals.length == 0) { - return 0; - } - Arrays.sort(intervals, Comparator.comparingInt(o -> o.end)); - int cnt = 1; - int end = intervals[0].end; - for (int i = 1; i < intervals.length; i++) { - if (intervals[i].start < end) { - continue; - } - end = intervals[i].end; - cnt++; - } - return intervals.length - cnt; -} -``` - -使用 lambda 表示式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句: - -```java -Arrays.sort(intervals, new Comparator() { - @Override - public int compare(Interval o1, Interval o2) { - return o1.end - o2.end; - } -}); -``` - -**投飞镖刺破气球** - -[452. Minimum Number of Arrows to Burst Balloons (Medium)](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) - -``` -Input: -[[10,16], [2,8], [1,6], [7,12]] - -Output: -2 -``` - -题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。 - -也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。 - -```java -public int findMinArrowShots(int[][] points) { - if (points.length == 0) { - return 0; - } - Arrays.sort(points, Comparator.comparingInt(o -> o[1])); - int cnt = 1, end = points[0][1]; - for (int i = 1; i < points.length; i++) { - if (points[i][0] <= end) { - continue; - } - cnt++; - end = points[i][1]; - } - return cnt; -} -``` - -**根据身高和序号重组队列** - -[406. Queue Reconstruction by Height(Medium)](https://leetcode.com/problems/queue-reconstruction-by-height/description/) - -```html -Input: -[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] - -Output: -[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] -``` - -题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。 - -为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。 - -身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。 - -```java -public int[][] reconstructQueue(int[][] people) { - if (people == null || people.length == 0 || people[0].length == 0) { - return new int[0][0]; - } - Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0])); - List queue = new ArrayList<>(); - for (int[] p : people) { - queue.add(p[1], p); - } - return queue.toArray(new int[queue.size()][]); -} -``` - -**分隔字符串使同种字符出现在一起** - -[763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/) - -```html -Input: S = "ababcbacadefegdehijhklij" -Output: [9,7,8] -Explanation: -The partition is "ababcbaca", "defegde", "hijhklij". -This is a partition so that each letter appears in at most one part. -A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts. -``` - -```java -public List partitionLabels(String S) { - int[] lastIndexsOfChar = new int[26]; - for (int i = 0; i < S.length(); i++) { - lastIndexsOfChar[char2Index(S.charAt(i))] = i; - } - List partitions = new ArrayList<>(); - int firstIndex = 0; - while (firstIndex < S.length()) { - int lastIndex = firstIndex; - for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) { - int index = lastIndexsOfChar[char2Index(S.charAt(i))]; - if (index > lastIndex) { - lastIndex = index; - } - } - partitions.add(lastIndex - firstIndex + 1); - firstIndex = lastIndex + 1; - } - return partitions; -} - -private int char2Index(char c) { - return c - 'a'; -} -``` - - -**种植花朵** - -[605. Can Place Flowers (Easy)](https://leetcode.com/problems/can-place-flowers/description/) - -```html -Input: flowerbed = [1,0,0,0,1], n = 1 -Output: True -``` - -题目描述:花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。 - -```java -public boolean canPlaceFlowers(int[] flowerbed, int n) { - int len = flowerbed.length; - int cnt = 0; - for (int i = 0; i < len && cnt < n; i++) { - if (flowerbed[i] == 1) { - continue; - } - int pre = i == 0 ? 0 : flowerbed[i - 1]; - int next = i == len - 1 ? 0 : flowerbed[i + 1]; - if (pre == 0 && next == 0) { - cnt++; - flowerbed[i] = 1; - } - } - return cnt >= n; -} -``` - -**判断是否为子序列** - -[392. Is Subsequence (Medium)](https://leetcode.com/problems/is-subsequence/description/) - -```html -s = "abc", t = "ahbgdc" -Return true. -``` - -```java -public boolean isSubsequence(String s, String t) { - int index = -1; - for (char c : s.toCharArray()) { - index = t.indexOf(c, index + 1); - if (index == -1) { - return false; - } - } - return true; -} -``` - -**修改一个数成为非递减数组** - -[665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/) - -```html -Input: [4,2,3] -Output: True -Explanation: You could modify the first 4 to 1 to get a non-decreasing array. -``` - -题目描述:判断一个数组能不能只修改一个数就成为非递减数组。 - -在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。 - -```java -public boolean checkPossibility(int[] nums) { - int cnt = 0; - for (int i = 1; i < nums.length && cnt < 2; i++) { - if (nums[i] >= nums[i - 1]) { - continue; - } - cnt++; - if (i - 2 >= 0 && nums[i - 2] > nums[i]) { - nums[i] = nums[i - 1]; - } else { - nums[i - 1] = nums[i]; - } - } - return cnt <= 1; -} -``` - -**股票的最大收益** - -[122. Best Time to Buy and Sell Stock II (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) - -题目描述:一次股票交易包含买入和卖出,多个交易之间不能交叉进行。 - -对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中,从而在局部最优的情况下也保证全局最优。 - -```java -public int maxProfit(int[] prices) { - int profit = 0; - for (int i = 1; i < prices.length; i++) { - if (prices[i] > prices[i - 1]) { - profit += (prices[i] - prices[i - 1]); - } - } - return profit; -} -``` - -**子数组最大的和** - -[53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/) - -```html -For example, given the array [-2,1,-3,4,-1,2,1,-5,4], -the contiguous subarray [4,-1,2,1] has the largest sum = 6. -``` - -```java -public int maxSubArray(int[] nums) { - if (nums == null || nums.length == 0) { - return 0; - } - int preSum = nums[0]; - int maxSum = preSum; - for (int i = 1; i < nums.length; i++) { - preSum = preSum > 0 ? preSum + nums[i] : nums[i]; - maxSum = Math.max(maxSum, preSum); - } - return maxSum; -} -``` - -**买入和售出股票最大的收益** - -[121. Best Time to Buy and Sell Stock (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) - -题目描述:只进行一次交易。 - -只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 - -```java -public int maxProfit(int[] prices) { - int n = prices.length; - if (n == 0) return 0; - int soFarMin = prices[0]; - int max = 0; - for (int i = 1; i < n; i++) { - if (soFarMin > prices[i]) soFarMin = prices[i]; - else max = Math.max(max, prices[i] - soFarMin); - } - return max; -} -``` - -## 二分查找 - -**正常实现** - -```java -public int binarySearch(int[] nums, int key) { - int l = 0, h = nums.length - 1; - while (l <= h) { - int m = l + (h - l) / 2; - if (nums[m] == key) { - return m; - } else if (nums[m] > key) { - h = m - 1; - } else { - l = m + 1; - } - } - return -1; -} -``` - -**时间复杂度** - -二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度为 O(logN)。 - -**m 计算** - -有两种计算中值 m 的方式: - -- m = (l + h) / 2 -- m = l + (h - l) / 2 - -l + h 可能出现加法溢出,最好使用第二种方式。 - -**返回值** - -循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值: - -- -1:以一个错误码表示没有查找到 key -- l:将 key 插入到 nums 中的正确位置 - -**变种** - -二分查找可以有很多变种,变种实现要注意边界值的判断。例如在一个有重复元素的数组中查找 key 的最左位置的实现如下: - -```java -public int binarySearch(int[] nums, int key) { - int l = 0, h = nums.length - 1; - while (l < h) { - int m = l + (h - l) / 2; - if (nums[m] >= key) { - h = m; - } else { - l = m + 1; - } - } - return l; -} -``` - -该实现和正常实现有以下不同: - -- 循环条件为 l < h -- h 的赋值表达式为 h = m -- 最后返回 l 而不是 -1 - -在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。 - -在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况: - -```text -nums = {0, 1, 2}, key = 1 -l m h -0 1 2 nums[m] >= key -0 0 1 nums[m] < key -1 1 1 nums[m] >= key -1 1 1 nums[m] >= key -... -``` - -当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等。 - -**求开方** - -[69. Sqrt(x) (Easy)](https://leetcode.com/problems/sqrtx/description/) - -```html -Input: 4 -Output: 2 - -Input: 8 -Output: 2 -Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated. -``` - -一个数 x 的开方 sqrt 一定在 0 \~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 \~ x 之间查找 sqrt。 - -对于 x = 8,它的开方是 2.82842...,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。 - -```java -public int mySqrt(int x) { - if (x <= 1) { - return x; - } - int l = 1, h = x; - while (l <= h) { - int mid = l + (h - l) / 2; - int sqrt = x / mid; - if (sqrt == mid) { - return mid; - } else if (mid > sqrt) { - h = mid - 1; - } else { - l = mid + 1; - } - } - return h; -} -``` - -**大于给定元素的最小元素** - -[744. Find Smallest Letter Greater Than Target (Easy)](https://leetcode.com/problems/find-smallest-letter-greater-than-target/description/) - -```html -Input: -letters = ["c", "f", "j"] -target = "d" -Output: "f" - -Input: -letters = ["c", "f", "j"] -target = "k" -Output: "c" -``` - -题目描述:给定一个有序的字符数组 letters 和一个字符 target,要求找出 letters 中大于 target 的最小字符,如果找不到就返回第 1 个字符。 - -```java -public char nextGreatestLetter(char[] letters, char target) { - int n = letters.length; - int l = 0, h = n - 1; - while (l <= h) { - int m = l + (h - l) / 2; - if (letters[m] <= target) { - l = m + 1; - } else { - h = m - 1; - } - } - return l < n ? letters[l] : letters[0]; -} -``` - -**有序数组的 Single Element** - -[540. Single Element in a Sorted Array (Medium)](https://leetcode.com/problems/single-element-in-a-sorted-array/description/) - -```html -Input: [1, 1, 2, 3, 3, 4, 4, 8, 8] -Output: 2 -``` - -题目描述:一个有序数组只有一个数不出现两次,找出这个数。要求以 O(logN) 时间复杂度进行求解。 - -令 index 为 Single Element 在数组中的位置。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。 - -从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。 - -因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。 - -```java -public int singleNonDuplicate(int[] nums) { - int l = 0, h = nums.length - 1; - while (l < h) { - int m = l + (h - l) / 2; - if (m % 2 == 1) { - m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数 - } - if (nums[m] == nums[m + 1]) { - l = m + 2; - } else { - h = m; - } - } - return nums[l]; -} -``` - -**第一个错误的版本** - -[278. First Bad Version (Easy)](https://leetcode.com/problems/first-bad-version/description/) - -题目描述:给定一个元素 n 代表有 [1, 2, ..., n] 版本,可以调用 isBadVersion(int x) 知道某个版本是否错误,要求找到第一个错误的版本。 - -如果第 m 个版本出错,则表示第一个错误的版本在 [l, m] 之间,令 h = m;否则第一个错误的版本在 [m + 1, h] 之间,令 l = m + 1。 - -因为 h 的赋值表达式为 h = m,因此循环条件为 l < h。 - -```java -public int firstBadVersion(int n) { - int l = 1, h = n; - while (l < h) { - int mid = l + (h - l) / 2; - if (isBadVersion(mid)) { - h = mid; - } else { - l = mid + 1; - } - } - return l; -} -``` - -**旋转数组的最小数字** - -[153. Find Minimum in Rotated Sorted Array (Medium)](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) - -```html -Input: [3,4,5,1,2], -Output: 1 -``` - -```java -public int findMin(int[] nums) { - 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]; -} -``` - -**查找区间** - -[34. Search for a Range (Medium)](https://leetcode.com/problems/search-for-a-range/description/) - -```html -Input: nums = [5,7,7,8,8,10], target = 8 -Output: [3,4] - -Input: nums = [5,7,7,8,8,10], target = 6 -Output: [-1,-1] -``` - -```java -public int[] searchRange(int[] nums, int target) { - int first = binarySearch(nums, target); - int last = binarySearch(nums, target + 1) - 1; - if (first == nums.length || nums[first] != target) { - return new int[]{-1, -1}; - } else { - return new int[]{first, Math.max(first, last)}; - } -} - -private int binarySearch(int[] nums, int target) { - int l = 0, h = nums.length; // 注意 h 的初始值 - while (l < h) { - int m = l + (h - l) / 2; - if (nums[m] >= target) { - h = m; - } else { - l = m + 1; - } - } - return l; -} -``` - -## 分治 - -**给表达式加括号** - -[241. Different Ways to Add Parentheses (Medium)](https://leetcode.com/problems/different-ways-to-add-parentheses/description/) - -```html -Input: "2-1-1". - -((2-1)-1) = 0 -(2-(1-1)) = 2 - -Output : [0, 2] -``` - -```java -public List diffWaysToCompute(String input) { - List ways = new ArrayList<>(); - for (int i = 0; i < input.length(); i++) { - char c = input.charAt(i); - if (c == '+' || c == '-' || c == '*') { - List left = diffWaysToCompute(input.substring(0, i)); - List right = diffWaysToCompute(input.substring(i + 1)); - for (int l : left) { - for (int r : right) { - switch (c) { - case '+': - ways.add(l + r); - break; - case '-': - ways.add(l - r); - break; - case '*': - ways.add(l * r); - break; - } - } - } - } - } - if (ways.size() == 0) { - ways.add(Integer.valueOf(input)); - } - return ways; -} -``` - -## 搜索 - -深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 - -### BFS - -

- -广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。 - -第一层: - -- 0 -> {6,2,1,5} - -第二层: - -- 6 -> {4} -- 2 -> {} -- 1 -> {} -- 5 -> {3} - -第三层: - -- 4 -> {} -- 3 -> {} - -每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。 - -在程序实现 BFS 时需要考虑以下问题: - -- 队列:用来存储每一轮遍历得到的节点; -- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。 - -**计算在网格中从原点到特定点的最短路径长度** - -```html -[[1,1,0,1], - [1,0,1,0], - [1,1,1,1], - [1,0,1,1]] -``` - -1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。 - -```java -public int minPathLength(int[][] grids, int tr, int tc) { - final int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - final int m = grids.length, n = grids[0].length; - Queue> queue = new LinkedList<>(); - queue.add(new Pair<>(0, 0)); - int pathLength = 0; - while (!queue.isEmpty()) { - int size = queue.size(); - pathLength++; - while (size-- > 0) { - Pair cur = queue.poll(); - int cr = cur.getKey(), cc = cur.getValue(); - grids[cr][cc] = 0; // 标记 - for (int[] d : direction) { - int nr = cr + d[0], nc = cc + d[1]; - if (nr < 0 || nr >= m || nc < 0 || nc >= n || grids[nr][nc] == 0) { - continue; - } - if (nr == tr && nc == tc) { - return pathLength; - } - queue.add(new Pair<>(nr, nc)); - } - } - } - return -1; -} -``` - -**组成整数的最小平方数数量** - -[279. Perfect Squares (Medium)](https://leetcode.com/problems/perfect-squares/description/) - -```html -For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. -``` - -可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。 - -要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。 - -本题也可以用动态规划求解,在之后动态规划部分中会再次出现。 - -```java -public int numSquares(int n) { - List squares = generateSquares(n); - Queue queue = new LinkedList<>(); - boolean[] marked = new boolean[n + 1]; - queue.add(n); - marked[n] = true; - int level = 0; - while (!queue.isEmpty()) { - int size = queue.size(); - level++; - while (size-- > 0) { - int cur = queue.poll(); - for (int s : squares) { - int next = cur - s; - if (next < 0) { - break; - } - if (next == 0) { - return level; - } - if (marked[next]) { - continue; - } - marked[next] = true; - queue.add(next); - } - } - } - return n; -} - -/** - * 生成小于 n 的平方数序列 - * @return 1,4,9,... - */ -private List generateSquares(int n) { - List squares = new ArrayList<>(); - int square = 1; - int diff = 3; - while (square <= n) { - squares.add(square); - square += diff; - diff += 2; - } - return squares; -} -``` - -**最短单词路径** - -[127. Word Ladder (Medium)](https://leetcode.com/problems/word-ladder/description/) - -```html -Input: -beginWord = "hit", -endWord = "cog", -wordList = ["hot","dot","dog","lot","log","cog"] - -Output: 5 - -Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog", -return its length 5. -``` - -```html -Input: -beginWord = "hit" -endWord = "cog" -wordList = ["hot","dot","dog","lot","log"] - -Output: 0 - -Explanation: The endWord "cog" is not in wordList, therefore no possible transformation. -``` - -题目描述:找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。 - -```java -public int ladderLength(String beginWord, String endWord, List wordList) { - wordList.add(beginWord); - int N = wordList.size(); - int start = N - 1; - int end = 0; - while (end < N && !wordList.get(end).equals(endWord)) { - end++; - } - if (end == N) { - return 0; - } - List[] graphic = buildGraphic(wordList); - return getShortestPath(graphic, start, end); -} - -private List[] buildGraphic(List wordList) { - int N = wordList.size(); - List[] graphic = new List[N]; - for (int i = 0; i < N; i++) { - graphic[i] = new ArrayList<>(); - for (int j = 0; j < N; j++) { - if (isConnect(wordList.get(i), wordList.get(j))) { - graphic[i].add(j); - } - } - } - return graphic; -} - -private boolean isConnect(String s1, String s2) { - int diffCnt = 0; - for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { - if (s1.charAt(i) != s2.charAt(i)) { - diffCnt++; - } - } - return diffCnt == 1; -} - -private int getShortestPath(List[] graphic, int start, int end) { - Queue queue = new LinkedList<>(); - boolean[] marked = new boolean[graphic.length]; - queue.add(start); - marked[start] = true; - int path = 1; - while (!queue.isEmpty()) { - int size = queue.size(); - path++; - while (size-- > 0) { - int cur = queue.poll(); - for (int next : graphic[cur]) { - if (next == end) { - return path; - } - if (marked[next]) { - continue; - } - marked[next] = true; - queue.add(next); - } - } - } - return 0; -} -``` - -### DFS - -

- -广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。 - -而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 - -从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。 - -在程序实现 DFS 时需要考虑以下问题: - -- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。 -- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。 - -**查找最大的连通面积** - -[695. Max Area of Island (Easy)](https://leetcode.com/problems/max-area-of-island/description/) - -```html -[[0,0,1,0,0,0,0,1,0,0,0,0,0], - [0,0,0,0,0,0,0,1,1,1,0,0,0], - [0,1,1,0,1,0,0,0,0,0,0,0,0], - [0,1,0,0,1,1,0,0,1,0,1,0,0], - [0,1,0,0,1,1,0,0,1,1,1,0,0], - [0,0,0,0,0,0,0,0,0,0,1,0,0], - [0,0,0,0,0,0,0,1,1,1,0,0,0], - [0,0,0,0,0,0,0,1,1,0,0,0,0]] -``` - -```java -private int m, n; -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; - -public int maxAreaOfIsland(int[][] grid) { - if (grid == null || grid.length == 0) { - return 0; - } - m = grid.length; - n = grid[0].length; - int maxArea = 0; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - maxArea = Math.max(maxArea, dfs(grid, i, j)); - } - } - return maxArea; -} - -private int dfs(int[][] grid, int r, int c) { - if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 0) { - return 0; - } - grid[r][c] = 0; - int area = 1; - for (int[] d : direction) { - area += dfs(grid, r + d[0], c + d[1]); - } - return area; -} -``` - -**矩阵中的连通分量数目** - -[200. Number of Islands (Medium)](https://leetcode.com/problems/number-of-islands/description/) - -```html -Input: -11000 -11000 -00100 -00011 - -Output: 3 -``` - -可以将矩阵表示看成一张有向图。 - -```java -private int m, n; -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; - -public int numIslands(char[][] grid) { - if (grid == null || grid.length == 0) { - return 0; - } - m = grid.length; - n = grid[0].length; - int islandsNum = 0; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (grid[i][j] != '0') { - dfs(grid, i, j); - islandsNum++; - } - } - } - return islandsNum; -} - -private void dfs(char[][] grid, int i, int j) { - if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') { - return; - } - grid[i][j] = '0'; - for (int[] d : direction) { - dfs(grid, i + d[0], j + d[1]); - } -} -``` - -**好友关系的连通分量数目** - -[547. Friend Circles (Medium)](https://leetcode.com/problems/friend-circles/description/) - -```html -Input: -[[1,1,0], - [1,1,0], - [0,0,1]] - -Output: 2 - -Explanation:The 0th and 1st students are direct friends, so they are in a friend circle. -The 2nd student himself is in a friend circle. So return 2. -``` - -题目描述:好友关系可以看成是一个无向图,例如第 0 个人与第 1 个人是好友,那么 M[0][1] 和 M[1][0] 的值都为 1。 - -```java -private int n; - -public int findCircleNum(int[][] M) { - n = M.length; - int circleNum = 0; - boolean[] hasVisited = new boolean[n]; - for (int i = 0; i < n; i++) { - if (!hasVisited[i]) { - dfs(M, i, hasVisited); - circleNum++; - } - } - return circleNum; -} - -private void dfs(int[][] M, int i, boolean[] hasVisited) { - hasVisited[i] = true; - for (int k = 0; k < n; k++) { - if (M[i][k] == 1 && !hasVisited[k]) { - dfs(M, k, hasVisited); - } - } -} -``` - -**填充封闭区域** - -[130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/) - -```html -For example, -X X X X -X O O X -X X O X -X O X X - -After running your function, the board should be: -X X X X -X X X X -X X X X -X O X X -``` - -题目描述:使被 'X' 包围的 'O' 转换为 'X'。 - -先填充最外侧,剩下的就是里侧了。 - -```java -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; -private int m, n; - -public void solve(char[][] board) { - if (board == null || board.length == 0) { - return; - } - - m = board.length; - n = board[0].length; - - for (int i = 0; i < m; i++) { - dfs(board, i, 0); - dfs(board, i, n - 1); - } - for (int i = 0; i < n; i++) { - dfs(board, 0, i); - dfs(board, m - 1, i); - } - - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (board[i][j] == 'T') { - board[i][j] = 'O'; - } else if (board[i][j] == 'O') { - board[i][j] = 'X'; - } - } - } -} - -private void dfs(char[][] board, int r, int c) { - if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { - return; - } - board[r][c] = 'T'; - for (int[] d : direction) { - dfs(board, r + d[0], c + d[1]); - } -} -``` - -**能到达的太平洋和大西洋的区域** - -[417. Pacific Atlantic Water Flow (Medium)](https://leetcode.com/problems/pacific-atlantic-water-flow/description/) - -```html -Given the following 5x5 matrix: - - Pacific ~ ~ ~ ~ ~ - ~ 1 2 2 3 (5) * - ~ 3 2 3 (4) (4) * - ~ 2 4 (5) 3 1 * - ~ (6) (7) 1 4 5 * - ~ (5) 1 1 2 4 * - * * * * * Atlantic - -Return: -[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix). -``` - -左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。 - -```java - -private int m, n; -private int[][] matrix; -private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; - -public List pacificAtlantic(int[][] matrix) { - List ret = new ArrayList<>(); - if (matrix == null || matrix.length == 0) { - return ret; - } - - m = matrix.length; - n = matrix[0].length; - this.matrix = matrix; - boolean[][] canReachP = new boolean[m][n]; - boolean[][] canReachA = new boolean[m][n]; - - for (int i = 0; i < m; i++) { - dfs(i, 0, canReachP); - dfs(i, n - 1, canReachA); - } - for (int i = 0; i < n; i++) { - dfs(0, i, canReachP); - dfs(m - 1, i, canReachA); - } - - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (canReachP[i][j] && canReachA[i][j]) { - ret.add(new int[]{i, j}); - } - } - } - - return ret; -} - -private void dfs(int r, int c, boolean[][] canReach) { - if (canReach[r][c]) { - return; - } - canReach[r][c] = true; - for (int[] d : direction) { - int nextR = d[0] + r; - int nextC = d[1] + c; - if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n - || matrix[r][c] > matrix[nextR][nextC]) { - - continue; - } - dfs(nextR, nextC, canReach); - } -} -``` - -### Backtracking - -Backtracking(回溯)属于 DFS。 - -- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特点的位置然后返回即可。 -- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。 - -因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题: - -- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素; -- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。 - -**数字键盘组合** - -[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) - -

- -```html -Input:Digit string "23" -Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. -``` - -```java -private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; - -public List letterCombinations(String digits) { - List combinations = new ArrayList<>(); - if (digits == null || digits.length() == 0) { - return combinations; - } - doCombination(new StringBuilder(), combinations, digits); - return combinations; -} - -private void doCombination(StringBuilder prefix, List combinations, final String digits) { - if (prefix.length() == digits.length()) { - combinations.add(prefix.toString()); - return; - } - int curDigits = digits.charAt(prefix.length()) - '0'; - String letters = KEYS[curDigits]; - for (char c : letters.toCharArray()) { - prefix.append(c); // 添加 - doCombination(prefix, combinations, digits); - prefix.deleteCharAt(prefix.length() - 1); // 删除 - } -} -``` - -**IP 地址划分** - -[93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/) - -```html -Given "25525511135", -return ["255.255.11.135", "255.255.111.35"]. -``` - -```java -public List restoreIpAddresses(String s) { - List addresses = new ArrayList<>(); - StringBuilder tempAddress = new StringBuilder(); - doRestore(0, tempAddress, addresses, s); - return addresses; -} - -private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) { - if (k == 4 || s.length() == 0) { - if (k == 4 && s.length() == 0) { - addresses.add(tempAddress.toString()); - } - return; - } - for (int i = 0; i < s.length() && i <= 2; i++) { - if (i != 0 && s.charAt(0) == '0') { - break; - } - String part = s.substring(0, i + 1); - if (Integer.valueOf(part) <= 255) { - if (tempAddress.length() != 0) { - part = "." + part; - } - tempAddress.append(part); - doRestore(k + 1, tempAddress, addresses, s.substring(i + 1)); - tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length()); - } - } -} -``` - -**在矩阵中寻找字符串** - -[79. Word Search (Medium)](https://leetcode.com/problems/word-search/description/) - -```html -For example, -Given board = -[ - ['A','B','C','E'], - ['S','F','C','S'], - ['A','D','E','E'] -] -word = "ABCCED", -> returns true, -word = "SEE", -> returns true, -word = "ABCB", -> returns false. -``` - -```java -private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; -private int m; -private int n; - -public boolean exist(char[][] board, String word) { - if (word == null || word.length() == 0) { - return true; - } - if (board == null || board.length == 0 || board[0].length == 0) { - return false; - } - - m = board.length; - n = board[0].length; - boolean[][] hasVisited = new boolean[m][n]; - - for (int r = 0; r < m; r++) { - for (int c = 0; c < n; c++) { - if (backtracking(0, r, c, hasVisited, board, word)) { - return true; - } - } - } - - return false; -} - -private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { - if (curLen == word.length()) { - return true; - } - if (r < 0 || r >= m || c < 0 || c >= n - || board[r][c] != word.charAt(curLen) || visited[r][c]) { - - return false; - } - - visited[r][c] = true; - - for (int[] d : direction) { - if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) { - return true; - } - } - - visited[r][c] = false; - - return false; -} -``` - -**输出二叉树中所有从根到叶子的路径** - -[257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/) - -```html - 1 - / \ -2 3 - \ - 5 -``` - -```html -["1->2->5", "1->3"] -``` - -```java - -public List binaryTreePaths(TreeNode root) { - List paths = new ArrayList<>(); - if (root == null) { - return paths; - } - List values = new ArrayList<>(); - backtracking(root, values, paths); - return paths; -} - -private void backtracking(TreeNode node, List values, List paths) { - if (node == null) { - return; - } - values.add(node.val); - if (isLeaf(node)) { - paths.add(buildPath(values)); - } else { - backtracking(node.left, values, paths); - backtracking(node.right, values, paths); - } - values.remove(values.size() - 1); -} - -private boolean isLeaf(TreeNode node) { - return node.left == null && node.right == null; -} - -private String buildPath(List values) { - StringBuilder str = new StringBuilder(); - for (int i = 0; i < values.size(); i++) { - str.append(values.get(i)); - if (i != values.size() - 1) { - str.append("->"); - } - } - return str.toString(); -} -``` - -**排列** - -[46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/) - -```html -[1,2,3] have the following permutations: -[ - [1,2,3], - [1,3,2], - [2,1,3], - [2,3,1], - [3,1,2], - [3,2,1] -] -``` - -```java -public List> permute(int[] nums) { - List> permutes = new ArrayList<>(); - List permuteList = new ArrayList<>(); - boolean[] hasVisited = new boolean[nums.length]; - backtracking(permuteList, permutes, hasVisited, nums); - return permutes; -} - -private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { - if (permuteList.size() == nums.length) { - permutes.add(new ArrayList<>(permuteList)); // 重新构造一个 List - return; - } - for (int i = 0; i < visited.length; i++) { - if (visited[i]) { - continue; - } - visited[i] = true; - permuteList.add(nums[i]); - backtracking(permuteList, permutes, visited, nums); - permuteList.remove(permuteList.size() - 1); - visited[i] = false; - } -} -``` - -**含有相同元素求排列** - -[47. Permutations II (Medium)](https://leetcode.com/problems/permutations-ii/description/) - -```html -[1,1,2] have the following unique permutations: -[[1,1,2], [1,2,1], [2,1,1]] -``` - -数组元素可能含有相同的元素,进行排列时就有可能出现重复的排列,要求重复的排列只返回一个。 - -在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。 - -```java -public List> permuteUnique(int[] nums) { - List> permutes = new ArrayList<>(); - List permuteList = new ArrayList<>(); - Arrays.sort(nums); // 排序 - boolean[] hasVisited = new boolean[nums.length]; - backtracking(permuteList, permutes, hasVisited, nums); - return permutes; -} - -private void backtracking(List permuteList, List> permutes, boolean[] visited, final int[] nums) { - if (permuteList.size() == nums.length) { - permutes.add(new ArrayList<>(permuteList)); - return; - } - - for (int i = 0; i < visited.length; i++) { - if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) { - continue; // 防止重复 - } - if (visited[i]){ - continue; - } - visited[i] = true; - permuteList.add(nums[i]); - backtracking(permuteList, permutes, visited, nums); - permuteList.remove(permuteList.size() - 1); - visited[i] = false; - } -} -``` - -**组合** - -[77. Combinations (Medium)](https://leetcode.com/problems/combinations/description/) - -```html -If n = 4 and k = 2, a solution is: -[ - [2,4], - [3,4], - [2,3], - [1,2], - [1,3], - [1,4], -] -``` - -```java -public List> combine(int n, int k) { - List> combinations = new ArrayList<>(); - List combineList = new ArrayList<>(); - backtracking(combineList, combinations, 1, k, n); - return combinations; -} - -private void backtracking(List combineList, List> combinations, int start, int k, final int n) { - if (k == 0) { - combinations.add(new ArrayList<>(combineList)); - return; - } - for (int i = start; i <= n - k + 1; i++) { // 剪枝 - combineList.add(i); - backtracking(combineList, combinations, i + 1, k - 1, n); - combineList.remove(combineList.size() - 1); - } -} -``` - -**组合求和** - -[39. Combination Sum (Medium)](https://leetcode.com/problems/combination-sum/description/) - -```html -given candidate set [2, 3, 6, 7] and target 7, -A solution set is: -[[7],[2, 2, 3]] -``` - -```java -public List> combinationSum(int[] candidates, int target) { - List> combinations = new ArrayList<>(); - backtracking(new ArrayList<>(), combinations, 0, target, candidates); - return combinations; -} - -private void backtracking(List tempCombination, List> combinations, - int start, int target, final int[] candidates) { - - if (target == 0) { - combinations.add(new ArrayList<>(tempCombination)); - return; - } - for (int i = start; i < candidates.length; i++) { - if (candidates[i] <= target) { - tempCombination.add(candidates[i]); - backtracking(tempCombination, combinations, i, target - candidates[i], candidates); - tempCombination.remove(tempCombination.size() - 1); - } - } -} -``` - -**含有相同元素的求组合求和** - -[40. Combination Sum II (Medium)](https://leetcode.com/problems/combination-sum-ii/description/) - -```html -For example, given candidate set [10, 1, 2, 7, 6, 1, 5] and target 8, -A solution set is: -[ - [1, 7], - [1, 2, 5], - [2, 6], - [1, 1, 6] -] -``` - -```java -public List> combinationSum2(int[] candidates, int target) { - List> combinations = new ArrayList<>(); - Arrays.sort(candidates); - backtracking(new ArrayList<>(), combinations, new boolean[candidates.length], 0, target, candidates); - return combinations; -} - -private void backtracking(List tempCombination, List> combinations, - boolean[] hasVisited, int start, int target, final int[] candidates) { - - if (target == 0) { - combinations.add(new ArrayList<>(tempCombination)); - return; - } - for (int i = start; i < candidates.length; i++) { - if (i != 0 && candidates[i] == candidates[i - 1] && !hasVisited[i - 1]) { - continue; - } - if (candidates[i] <= target) { - tempCombination.add(candidates[i]); - hasVisited[i] = true; - backtracking(tempCombination, combinations, hasVisited, i + 1, target - candidates[i], candidates); - hasVisited[i] = false; - tempCombination.remove(tempCombination.size() - 1); - } - } -} -``` - -**1-9 数字的组合求和** - -[216. Combination Sum III (Medium)](https://leetcode.com/problems/combination-sum-iii/description/) - -```html -Input: k = 3, n = 9 - -Output: - -[[1,2,6], [1,3,5], [2,3,4]] -``` - -从 1-9 数字中选出 k 个数不重复的数,使得它们的和为 n。 - -```java -public List> combinationSum3(int k, int n) { - List> combinations = new ArrayList<>(); - List path = new ArrayList<>(); - backtracking(k, n, 1, path, combinations); - return combinations; -} - -private void backtracking(int k, int n, int start, - List tempCombination, List> combinations) { - - if (k == 0 && n == 0) { - combinations.add(new ArrayList<>(tempCombination)); - return; - } - if (k == 0 || n == 0) { - return; - } - for (int i = start; i <= 9; i++) { - tempCombination.add(i); - backtracking(k - 1, n - i, i + 1, tempCombination, combinations); - tempCombination.remove(tempCombination.size() - 1); - } -} -``` - -**子集** - -[78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/) - -找出集合的所有子集,子集不能重复,[1, 2] 和 [2, 1] 这种子集算重复 - -```java -public List> subsets(int[] nums) { - List> subsets = new ArrayList<>(); - List tempSubset = new ArrayList<>(); - for (int size = 0; size <= nums.length; size++) { - backtracking(0, tempSubset, subsets, size, nums); // 不同的子集大小 - } - return subsets; -} - -private void backtracking(int start, List tempSubset, List> subsets, - final int size, final int[] nums) { - - if (tempSubset.size() == size) { - subsets.add(new ArrayList<>(tempSubset)); - return; - } - for (int i = start; i < nums.length; i++) { - tempSubset.add(nums[i]); - backtracking(i + 1, tempSubset, subsets, size, nums); - tempSubset.remove(tempSubset.size() - 1); - } -} -``` - -**含有相同元素求子集** - -[90. Subsets II (Medium)](https://leetcode.com/problems/subsets-ii/description/) - -```html -For example, -If nums = [1,2,2], a solution is: - -[ - [2], - [1], - [1,2,2], - [2,2], - [1,2], - [] -] -``` - -```java -public List> subsetsWithDup(int[] nums) { - Arrays.sort(nums); - List> subsets = new ArrayList<>(); - List tempSubset = new ArrayList<>(); - boolean[] hasVisited = new boolean[nums.length]; - for (int size = 0; size <= nums.length; size++) { - backtracking(0, tempSubset, subsets, hasVisited, size, nums); // 不同的子集大小 - } - return subsets; -} - -private void backtracking(int start, List tempSubset, List> subsets, boolean[] hasVisited, - final int size, final int[] nums) { - - if (tempSubset.size() == size) { - subsets.add(new ArrayList<>(tempSubset)); - return; - } - for (int i = start; i < nums.length; i++) { - if (i != 0 && nums[i] == nums[i - 1] && !hasVisited[i - 1]) { - continue; - } - tempSubset.add(nums[i]); - hasVisited[i] = true; - backtracking(i + 1, tempSubset, subsets, hasVisited, size, nums); - hasVisited[i] = false; - tempSubset.remove(tempSubset.size() - 1); - } -} -``` - -**分割字符串使得每个部分都是回文数** - -[131. Palindrome Partitioning (Medium)](https://leetcode.com/problems/palindrome-partitioning/description/) - -```html -For example, given s = "aab", -Return - -[ - ["aa","b"], - ["a","a","b"] -] -``` - -```java -public List> partition(String s) { - List> partitions = new ArrayList<>(); - List tempPartition = new ArrayList<>(); - doPartition(s, partitions, tempPartition); - return partitions; -} - -private void doPartition(String s, List> partitions, List tempPartition) { - if (s.length() == 0) { - partitions.add(new ArrayList<>(tempPartition)); - return; - } - for (int i = 0; i < s.length(); i++) { - if (isPalindrome(s, 0, i)) { - tempPartition.add(s.substring(0, i + 1)); - doPartition(s.substring(i + 1), partitions, tempPartition); - tempPartition.remove(tempPartition.size() - 1); - } - } -} - -private boolean isPalindrome(String s, int begin, int end) { - while (begin < end) { - if (s.charAt(begin++) != s.charAt(end--)) { - return false; - } - } - return true; -} -``` - -**数独** - -[37. Sudoku Solver (Hard)](https://leetcode.com/problems/sudoku-solver/description/) - -

- -```java -private boolean[][] rowsUsed = new boolean[9][10]; -private boolean[][] colsUsed = new boolean[9][10]; -private boolean[][] cubesUsed = new boolean[9][10]; -private char[][] board; - -public void solveSudoku(char[][] board) { - this.board = board; - for (int i = 0; i < 9; i++) - for (int j = 0; j < 9; j++) { - if (board[i][j] == '.') { - continue; - } - int num = board[i][j] - '0'; - rowsUsed[i][num] = true; - colsUsed[j][num] = true; - cubesUsed[cubeNum(i, j)][num] = true; - } - backtracking(0, 0); -} - -private boolean backtracking(int row, int col) { - while (row < 9 && board[row][col] != '.') { - row = col == 8 ? row + 1 : row; - col = col == 8 ? 0 : col + 1; - } - if (row == 9) { - return true; - } - for (int num = 1; num <= 9; num++) { - if (rowsUsed[row][num] || colsUsed[col][num] || cubesUsed[cubeNum(row, col)][num]) { - continue; - } - rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = true; - board[row][col] = (char) (num + '0'); - if (backtracking(row, col)) { - return true; - } - board[row][col] = '.'; - rowsUsed[row][num] = colsUsed[col][num] = cubesUsed[cubeNum(row, col)][num] = false; - } - return false; -} - -private int cubeNum(int i, int j) { - int r = i / 3; - int c = j / 3; - return r * 3 + c; -} -``` - -**N 皇后** - -[51. N-Queens (Hard)](https://leetcode.com/problems/n-queens/description/) - -

- -在 n\*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,求所有的 n 皇后的解。 - -一行一行地摆放,在确定一行中的那个皇后应该摆在哪一列时,需要用三个标记数组来确定某一列是否合法,这三个标记数组分别为:列标记数组、45 度对角线标记数组和 135 度对角线标记数组。 - -45 度对角线标记数组的长度为 2 \* n - 1,通过下图可以明确 (r, c) 的位置所在的数组下标为 r + c。 - -

- -135 度对角线标记数组的长度也是 2 \* n - 1,(r, c) 的位置所在的数组下标为 n - 1 - (r - c)。 - -

- -```java -private List> solutions; -private char[][] nQueens; -private boolean[] colUsed; -private boolean[] diagonals45Used; -private boolean[] diagonals135Used; -private int n; - -public List> solveNQueens(int n) { - solutions = new ArrayList<>(); - nQueens = new char[n][n]; - for (int i = 0; i < n; i++) { - Arrays.fill(nQueens[i], '.'); - } - colUsed = new boolean[n]; - diagonals45Used = new boolean[2 * n - 1]; - diagonals135Used = new boolean[2 * n - 1]; - this.n = n; - backtracking(0); - return solutions; -} - -private void backtracking(int row) { - if (row == n) { - List list = new ArrayList<>(); - for (char[] chars : nQueens) { - list.add(new String(chars)); - } - solutions.add(list); - return; - } - - for (int col = 0; col < n; col++) { - int diagonals45Idx = row + col; - int diagonals135Idx = n - 1 - (row - col); - if (colUsed[col] || diagonals45Used[diagonals45Idx] || diagonals135Used[diagonals135Idx]) { - continue; - } - nQueens[row][col] = 'Q'; - colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = true; - backtracking(row + 1); - colUsed[col] = diagonals45Used[diagonals45Idx] = diagonals135Used[diagonals135Idx] = false; - nQueens[row][col] = '.'; - } -} -``` - -## 动态规划 - -递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 - -### 斐波那契数列 - -**爬楼梯** - -[70. Climbing Stairs (Easy)](https://leetcode.com/problems/climbing-stairs/description/) - -题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 - -定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。 - -第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 - - - -

- - -考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 - -```java -public int climbStairs(int n) { - if (n <= 2) { - return n; - } - int pre2 = 1, pre1 = 2; - for (int i = 2; i < n; i++) { - int cur = pre1 + pre2; - pre2 = pre1; - pre1 = cur; - } - return pre1; -} -``` - -**强盗抢劫** - -[198. House Robber (Easy)](https://leetcode.com/problems/house-robber/description/) - -题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。 - -定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。 - -由于不能抢劫邻近住户,如果抢劫了第 i -1 个住户,那么就不能再抢劫第 i 个住户,所以 - - - -

- -```java -public int rob(int[] nums) { - int pre2 = 0, pre1 = 0; - for (int i = 0; i < nums.length; i++) { - int cur = Math.max(pre2 + nums[i], pre1); - pre2 = pre1; - pre1 = cur; - } - return pre1; -} -``` - -**强盗在环形街区抢劫** - -[213. House Robber II (Medium)](https://leetcode.com/problems/house-robber-ii/description/) - -```java -public int rob(int[] nums) { - if (nums == null || nums.length == 0) { - return 0; - } - int n = nums.length; - if (n == 1) { - return nums[0]; - } - return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1)); -} - -private int rob(int[] nums, int first, int last) { - int pre2 = 0, pre1 = 0; - for (int i = first; i <= last; i++) { - int cur = Math.max(pre1, pre2 + nums[i]); - pre2 = pre1; - pre1 = cur; - } - return pre1; -} -``` - -**信件错排** - -题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 - -定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: - -- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。 -- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。 - -综上所述,错误装信数量方式数量为: - - - -

- -**母牛生产** - -[程序员代码面试指南-P181](#) - -题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 - -第 i 年成熟的牛的数量为: - - - -

- -### 矩阵路径 - -**矩阵的最小路径和** - -[64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/) - -```html -[[1,3,1], - [1,5,1], - [4,2,1]] -Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum. -``` - -题目描述:求从矩阵的左上角到右下角的最小路径和,每次只能向右和向下移动。 - -```java -public int minPathSum(int[][] grid) { - if (grid.length == 0 || grid[0].length == 0) { - return 0; - } - int m = grid.length, n = grid[0].length; - int[] dp = new int[n]; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (j == 0) { - dp[j] = dp[j]; // 只能从上侧走到该位置 - } else if (i == 0) { - dp[j] = dp[j - 1]; // 只能从左侧走到该位置 - } else { - dp[j] = Math.min(dp[j - 1], dp[j]); - } - dp[j] += grid[i][j]; - } - } - return dp[n - 1]; -} -``` - -**矩阵的总路径数** - -[62. Unique Paths (Medium)](https://leetcode.com/problems/unique-paths/description/) - -题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。 - -

- -```java -public int uniquePaths(int m, int n) { - int[] dp = new int[n]; - Arrays.fill(dp, 1); - for (int i = 1; i < m; i++) { - for (int j = 1; j < n; j++) { - dp[j] = dp[j] + dp[j - 1]; - } - } - return dp[n - 1]; -} -``` - -也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 中取出 D 个位置的组合数量,这个问题的解为 C(S, D)。 - -```java -public int uniquePaths(int m, int n) { - int S = m + n - 2; // 总共的移动次数 - int D = m - 1; // 向下的移动次数 - long ret = 1; - for (int i = 1; i <= D; i++) { - ret = ret * (S - D + i) / i; - } - return (int) ret; -} -``` - -### 数组区间 - -**数组区间和** - -[303. Range Sum Query - Immutable (Easy)](https://leetcode.com/problems/range-sum-query-immutable/description/) - -```html -Given nums = [-2, 0, 3, -5, 2, -1] - -sumRange(0, 2) -> 1 -sumRange(2, 5) -> -1 -sumRange(0, 5) -> -3 -``` - -求区间 i \~ j 的和,可以转换为 sum[j + 1] - sum[i],其中 sum[i] 为 0 \~ i - 1 的和。 - -```java -class NumArray { - - private int[] sums; - - public NumArray(int[] nums) { - sums = new int[nums.length + 1]; - for (int i = 1; i <= nums.length; i++) { - sums[i] = sums[i - 1] + nums[i - 1]; - } - } - - public int sumRange(int i, int j) { - return sums[j + 1] - sums[i]; - } -} -``` - -**数组中等差递增子区间的个数** - -[413. Arithmetic Slices (Medium)](https://leetcode.com/problems/arithmetic-slices/description/) - -```html -A = [1, 2, 3, 4] -return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself. -``` - -dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。 - -在 A[i] - A[i - 1] == A[i - 1] - A[i - 2] 的条件下,{A[i - 2], A[i - 1], A[i]} 是一个等差递增子区间。如果 {A[i - 3], A[i - 2], A[i - 1]} 是一个等差递增子区间,那么 {A[i - 3], A[i - 2], A[i - 1], A[i]} 也是等差递增子区间,dp[i] = dp[i-1] + 1。 - -```java -public int numberOfArithmeticSlices(int[] A) { - if (A == null || A.length == 0) { - return 0; - } - int n = A.length; - int[] dp = new int[n]; - for (int i = 2; i < n; i++) { - if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) { - dp[i] = dp[i - 1] + 1; - } - } - int total = 0; - for (int cnt : dp) { - total += cnt; - } - return total; -} -``` - -### 分割整数 - -**分割整数的最大乘积** - -[343. Integer Break (Medim)](https://leetcode.com/problems/integer-break/description/) - -题目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4). - -```java -public int integerBreak(int n) { - int[] dp = new int[n + 1]; - dp[1] = 1; - for (int i = 2; i <= n; i++) { - for (int j = 1; j <= i - 1; j++) { - dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); - } - } - return dp[n]; -} -``` - -**按平方数来分割整数** - -[279. Perfect Squares(Medium)](https://leetcode.com/problems/perfect-squares/description/) - -题目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9. - -```java -public int numSquares(int n) { - List squareList = generateSquareList(n); - int[] dp = new int[n + 1]; - for (int i = 1; i <= n; i++) { - int min = Integer.MAX_VALUE; - for (int square : squareList) { - if (square > i) { - break; - } - min = Math.min(min, dp[i - square] + 1); - } - dp[i] = min; - } - return dp[n]; -} - -private List generateSquareList(int n) { - List squareList = new ArrayList<>(); - int diff = 3; - int square = 1; - while (square <= n) { - squareList.add(square); - square += diff; - diff += 2; - } - return squareList; -} -``` - -**分割整数构成字母字符串** - -[91. Decode Ways (Medium)](https://leetcode.com/problems/decode-ways/description/) - -题目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12). - -```java -public int numDecodings(String s) { - if (s == null || s.length() == 0) { - return 0; - } - int n = s.length(); - int[] dp = new int[n + 1]; - dp[0] = 1; - dp[1] = s.charAt(0) == '0' ? 0 : 1; - for (int i = 2; i <= n; i++) { - int one = Integer.valueOf(s.substring(i - 1, i)); - if (one != 0) { - dp[i] += dp[i - 1]; - } - if (s.charAt(i - 2) == '0') { - continue; - } - int two = Integer.valueOf(s.substring(i - 2, i)); - if (two <= 26) { - dp[i] += dp[i - 2]; - } - } - return dp[n]; -} -``` - -### 最长递增子序列 - -已知一个序列 {S1, S2,...,Sn},取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。 - -如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 **递增子序列** 。 - -定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 - -因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: - - - -

- -对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。 - -**最长递增子序列** - -[300. Longest Increasing Subsequence (Medium)](https://leetcode.com/problems/longest-increasing-subsequence/description/) - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - int[] dp = new int[n]; - for (int i = 0; i < n; i++) { - int max = 1; - for (int j = 0; j < i; j++) { - if (nums[i] > nums[j]) { - max = Math.max(max, dp[j] + 1); - } - } - dp[i] = max; - } - return Arrays.stream(dp).max().orElse(0); -} -``` - -使用 Stream 求最大值会导致运行时间过长,可以改成以下形式: - -```java -int ret = 0; -for (int i = 0; i < n; i++) { - ret = Math.max(ret, dp[i]); -} -return ret; -``` - -以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。 - -定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x, - -- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1; -- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。 - -例如对于数组 [4,3,6,5],有: - -```html -tails len num -[] 0 4 -[4] 1 3 -[3] 1 6 -[3,6] 2 5 -[3,5] 2 null -``` - -可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。 - -```java -public int lengthOfLIS(int[] nums) { - int n = nums.length; - int[] tails = new int[n]; - int len = 0; - for (int num : nums) { - int index = binarySearch(tails, len, num); - tails[index] = num; - if (index == len) { - len++; - } - } - return len; -} - -private int binarySearch(int[] tails, int len, int key) { - int l = 0, h = len; - while (l < h) { - int mid = l + (h - l) / 2; - if (tails[mid] == key) { - return mid; - } else if (tails[mid] > key) { - h = mid; - } else { - l = mid + 1; - } - } - return l; -} -``` - -**一组整数对能够构成的最长链** - -[646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) - -```html -Input: [[1,2], [2,3], [3,4]] -Output: 2 -Explanation: The longest chain is [1,2] -> [3,4] -``` - -题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。 - -```java -public int findLongestChain(int[][] pairs) { - if (pairs == null || pairs.length == 0) { - return 0; - } - Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); - int n = pairs.length; - int[] dp = new int[n]; - Arrays.fill(dp, 1); - for (int i = 1; i < n; i++) { - for (int j = 0; j < i; j++) { - if (pairs[j][1] < pairs[i][0]) { - dp[i] = Math.max(dp[i], dp[j] + 1); - } - } - } - return Arrays.stream(dp).max().orElse(0); -} -``` - -**最长摆动子序列** - -[376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/) - -```html -Input: [1,7,4,9,2,5] -Output: 6 -The entire sequence is a wiggle sequence. - -Input: [1,17,5,10,13,15,10,5,16,8] -Output: 7 -There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8]. - -Input: [1,2,3,4,5,6,7,8,9] -Output: 2 -``` - -要求:使用 O(N) 时间复杂度求解。 - -```java -public int wiggleMaxLength(int[] nums) { - if (nums == null || nums.length == 0) { - return 0; - } - int up = 1, down = 1; - for (int i = 1; i < nums.length; i++) { - if (nums[i] > nums[i - 1]) { - up = down + 1; - } else if (nums[i] < nums[i - 1]) { - down = up + 1; - } - } - return Math.max(up, down); -} -``` - -### 最长公共子序列 - -对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 - -定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: - -- 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。 -- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 - -综上,最长公共子序列的状态转移方程为: - - - -

- -对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 - -与最长递增子序列相比,最长公共子序列有以下不同点: - -- 针对的是两个序列,求它们的最长公共子序列。 -- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。 -- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 - -```java -public int lengthOfLCS(int[] nums1, int[] nums2) { - int n1 = nums1.length, n2 = nums2.length; - int[][] dp = new int[n1 + 1][n2 + 1]; - for (int i = 1; i <= n1; i++) { - for (int j = 1; j <= n2; j++) { - if (nums1[i - 1] == nums2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + 1; - } else { - dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); - } - } - } - return dp[n1][n2]; -} -``` - -### 0-1 背包 - -有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 - -定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: - -- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 -- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 - -第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为: - - - -

- -```java -public int knapsack(int W, int N, int[] weights, int[] values) { - int[][] dp = new int[N + 1][W + 1]; - for (int i = 1; i <= N; i++) { - int w = weights[i - 1], v = values[i - 1]; - for (int j = 1; j <= W; j++) { - if (j >= w) { - dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); - } else { - dp[i][j] = dp[i - 1][j]; - } - } - } - return dp[N][W]; -} -``` - -**空间优化** - -在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时, - - - -

- -因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 - -```java -public int knapsack(int W, int N, int[] weights, int[] values) { - int[] dp = new int[W + 1]; - for (int i = 1; i <= N; i++) { - int w = weights[i - 1], v = values[i - 1]; - for (int j = W; j >= 1; j--) { - if (j >= w) { - dp[j] = Math.max(dp[j], dp[j - w] + v); - } - } - } - return dp[W]; -} -``` - -**无法使用贪心算法的解释** - -0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22. - -| id | w | v | v/w | -| --- | --- | --- | --- | -| 0 | 1 | 6 | 6 | -| 1 | 2 | 10 | 5 | -| 2 | 3 | 12 | 4 | - -**变种** - -- 完全背包:物品数量为无限个 - -- 多重背包:物品数量有限制 - -- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制 - -- 其它:物品之间相互约束或者依赖 - -**划分数组为和相等的两部分** - -[416. Partition Equal Subset Sum (Medium)](https://leetcode.com/problems/partition-equal-subset-sum/description/) - -```html -Input: [1, 5, 11, 5] - -Output: true - -Explanation: The array can be partitioned as [1, 5, 5] and [11]. -``` - -可以看成一个背包大小为 sum/2 的 0-1 背包问题。 - -```java -public boolean canPartition(int[] nums) { - int sum = computeArraySum(nums); - if (sum % 2 != 0) { - return false; - } - int W = sum / 2; - boolean[] dp = new boolean[W + 1]; - dp[0] = true; - for (int num : nums) { // 0-1 背包一个物品只能用一次 - for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num] - dp[i] = dp[i] || dp[i - num]; - } - } - return dp[W]; -} - -private int computeArraySum(int[] nums) { - int sum = 0; - for (int num : nums) { - sum += num; - } - return sum; -} -``` - -**改变一组数的正负号使得它们的和为一给定数** - -[494. Target Sum (Medium)](https://leetcode.com/problems/target-sum/description/) - -```html -Input: nums is [1, 1, 1, 1, 1], S is 3. -Output: 5 -Explanation: - --1+1+1+1+1 = 3 -+1-1+1+1+1 = 3 -+1+1-1+1+1 = 3 -+1+1+1-1+1 = 3 -+1+1+1+1-1 = 3 - -There are 5 ways to assign symbols to make the sum of nums be target 3. -``` - -该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。 - -可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导: - -```html - sum(P) - sum(N) = target -sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N) - 2 * sum(P) = target + sum(nums) -``` - -因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。 - -```java -public int findTargetSumWays(int[] nums, int S) { - int sum = computeArraySum(nums); - if (sum < S || (sum + S) % 2 == 1) { - return 0; - } - int W = (sum + S) / 2; - int[] dp = new int[W + 1]; - dp[0] = 1; - for (int num : nums) { - for (int i = W; i >= num; i--) { - dp[i] = dp[i] + dp[i - num]; - } - } - return dp[W]; -} - -private int computeArraySum(int[] nums) { - int sum = 0; - for (int num : nums) { - sum += num; - } - return sum; -} -``` - -DFS 解法: - -```java -public int findTargetSumWays(int[] nums, int S) { - return findTargetSumWays(nums, 0, S); -} - -private int findTargetSumWays(int[] nums, int start, int S) { - if (start == nums.length) { - return S == 0 ? 1 : 0; - } - return findTargetSumWays(nums, start + 1, S + nums[start]) - + findTargetSumWays(nums, start + 1, S - nums[start]); -} -``` - -**01 字符构成最多的字符串** - -[474. Ones and Zeroes (Medium)](https://leetcode.com/problems/ones-and-zeroes/description/) - -```html -Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 -Output: 4 - -Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0" -``` - -这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。 - -```java -public int findMaxForm(String[] strs, int m, int n) { - if (strs == null || strs.length == 0) { - return 0; - } - int[][] dp = new int[m + 1][n + 1]; - for (String s : strs) { // 每个字符串只能用一次 - int ones = 0, zeros = 0; - for (char c : s.toCharArray()) { - if (c == '0') { - zeros++; - } else { - ones++; - } - } - for (int i = m; i >= zeros; i--) { - for (int j = n; j >= ones; j--) { - dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1); - } - } - } - return dp[m][n]; -} -``` - -**找零钱的最少硬币数** - -[322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/) - -```html -Example 1: -coins = [1, 2, 5], amount = 11 -return 3 (11 = 5 + 5 + 1) - -Example 2: -coins = [2], amount = 3 -return -1. -``` - -题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 - -- 物品:硬币 -- 物品大小:面额 -- 物品价值:数量 - -因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包中逆序遍历 dp 数组改为正序遍历即可。 - -```java -public int coinChange(int[] coins, int amount) { - if (amount == 0 || coins == null || coins.length == 0) { - return 0; - } - int[] dp = new int[amount + 1]; - for (int coin : coins) { - for (int i = coin; i <= amount; i++) { //将逆序遍历改为正序遍历 - if (i == coin) { - dp[i] = 1; - } else if (dp[i] == 0 && dp[i - coin] != 0) { - dp[i] = dp[i - coin] + 1; - } else if (dp[i - coin] != 0) { - dp[i] = Math.min(dp[i], dp[i - coin] + 1); - } - } - } - return dp[amount] == 0 ? -1 : dp[amount]; -} -``` - -**找零钱的硬币数组合** - -[518\. Coin Change 2 (Medium)](https://leetcode.com/problems/coin-change-2/description/) - -```text-html-basic -Input: amount = 5, coins = [1, 2, 5] -Output: 4 -Explanation: there are four ways to make up the amount: -5=5 -5=2+2+1 -5=2+1+1+1 -5=1+1+1+1+1 -``` - -完全背包问题,使用 dp 记录可达成目标的组合数目。 - -```java -public int change(int amount, int[] coins) { - if (amount == 0 || coins == null || coins.length == 0) { - return 0; - } - int[] dp = new int[amount + 1]; - dp[0] = 1; - for (int coin : coins) { - for (int i = coin; i <= amount; i++) { - dp[i] += dp[i - coin]; - } - } - return dp[amount]; -} -``` - -**字符串按单词列表分割** - -[139. Word Break (Medium)](https://leetcode.com/problems/word-break/description/) - -```html -s = "leetcode", -dict = ["leet", "code"]. -Return true because "leetcode" can be segmented as "leet code". -``` - -dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。该问题涉及到字典中单词的使用顺序,因此可理解为涉及顺序的完全背包问题。 - -求解顺序的完全背包问题时,对物品的迭代应该放在最里层。 - -```java -public boolean wordBreak(String s, List wordDict) { - int n = s.length(); - boolean[] dp = new boolean[n + 1]; - dp[0] = true; - for (int i = 1; i <= n; i++) { - for (String word : wordDict) { // 对物品的迭代应该放在最里层 - int len = word.length(); - if (len <= i && word.equals(s.substring(i - len, i))) { - dp[i] = dp[i] || dp[i - len]; - } - } - } - return dp[n]; -} -``` - -**组合总和** - -[377. Combination Sum IV (Medium)](https://leetcode.com/problems/combination-sum-iv/description/) - -```html -nums = [1, 2, 3] -target = 4 - -The possible combination ways are: -(1, 1, 1, 1) -(1, 1, 2) -(1, 2, 1) -(1, 3) -(2, 1, 1) -(2, 2) -(3, 1) - -Note that different sequences are counted as different combinations. - -Therefore the output is 7. -``` - -涉及顺序的完全背包。 - -```java -public int combinationSum4(int[] nums, int target) { - if (nums == null || nums.length == 0) { - return 0; - } - int[] maximum = new int[target + 1]; - maximum[0] = 1; - Arrays.sort(nums); - for (int i = 1; i <= target; i++) { - for (int j = 0; j < nums.length && nums[j] <= i; j++) { - maximum[i] += maximum[i - nums[j]]; - } - } - return maximum[target]; -} -``` - -### 股票交易 - -**需要冷却期的股票交易** - -[309. Best Time to Buy and Sell Stock with Cooldown(Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/) - -题目描述:交易之后需要有一天的冷却时间。 - -

- -```java -public int maxProfit(int[] prices) { - if (prices == null || prices.length == 0) { - return 0; - } - int N = prices.length; - int[] buy = new int[N]; - int[] s1 = new int[N]; - int[] sell = new int[N]; - int[] s2 = new int[N]; - s1[0] = buy[0] = -prices[0]; - sell[0] = s2[0] = 0; - for (int i = 1; i < N; i++) { - buy[i] = s2[i - 1] - prices[i]; - s1[i] = Math.max(buy[i - 1], s1[i - 1]); - sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; - s2[i] = Math.max(s2[i - 1], sell[i - 1]); - } - return Math.max(sell[N - 1], s2[N - 1]); -} -``` - -**需要交易费用的股票交易** - -[714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/) - -```html -Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 -Output: 8 -Explanation: The maximum profit can be achieved by: -Buying at prices[0] = 1 -Selling at prices[3] = 8 -Buying at prices[4] = 4 -Selling at prices[5] = 9 -The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. -``` - -题目描述:每交易一次,都要支付一定的费用。 - -

- -```java -public int maxProfit(int[] prices, int fee) { - int N = prices.length; - int[] buy = new int[N]; - int[] s1 = new int[N]; - int[] sell = new int[N]; - int[] s2 = new int[N]; - s1[0] = buy[0] = -prices[0]; - sell[0] = s2[0] = 0; - for (int i = 1; i < N; i++) { - buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; - s1[i] = Math.max(buy[i - 1], s1[i - 1]); - sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; - s2[i] = Math.max(s2[i - 1], sell[i - 1]); - } - return Math.max(sell[N - 1], s2[N - 1]); -} -``` - - -**只能进行两次的股票交易** - -[123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) - -```java -public int maxProfit(int[] prices) { - int firstBuy = Integer.MIN_VALUE, firstSell = 0; - int secondBuy = Integer.MIN_VALUE, secondSell = 0; - for (int curPrice : prices) { - if (firstBuy < -curPrice) { - firstBuy = -curPrice; - } - if (firstSell < firstBuy + curPrice) { - firstSell = firstBuy + curPrice; - } - if (secondBuy < firstSell - curPrice) { - secondBuy = firstSell - curPrice; - } - if (secondSell < secondBuy + curPrice) { - secondSell = secondBuy + curPrice; - } - } - return secondSell; -} -``` - -**只能进行 k 次的股票交易** - -[188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) - -```java -public int maxProfit(int k, int[] prices) { - int n = prices.length; - if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题 - int maxProfit = 0; - for (int i = 1; i < n; i++) { - if (prices[i] > prices[i - 1]) { - maxProfit += prices[i] - prices[i - 1]; - } - } - return maxProfit; - } - int[][] maxProfit = new int[k + 1][n]; - for (int i = 1; i <= k; i++) { - int localMax = maxProfit[i - 1][0] - prices[0]; - for (int j = 1; j < n; j++) { - maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax); - localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]); - } - } - return maxProfit[k][n - 1]; -} -``` - -### 字符串编辑 - -**删除两个字符串的字符使它们相等** - -[583. Delete Operation for Two Strings (Medium)](https://leetcode.com/problems/delete-operation-for-two-strings/description/) - -```html -Input: "sea", "eat" -Output: 2 -Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea". -``` - -可以转换为求两个字符串的最长公共子序列问题。 - -```java -public int minDistance(String word1, String word2) { - int m = word1.length(), n = word2.length(); - int[][] dp = new int[m + 1][n + 1]; - for (int i = 1; i <= m; i++) { - for (int j = 1; j <= n; j++) { - if (word1.charAt(i - 1) == word2.charAt(j - 1)) { - dp[i][j] = dp[i - 1][j - 1] + 1; - } else { - dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); - } - } - } - return m + n - 2 * dp[m][n]; -} -``` - -**编辑距离** - -[72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/) - -```html -Example 1: - -Input: word1 = "horse", word2 = "ros" -Output: 3 -Explanation: -horse -> rorse (replace 'h' with 'r') -rorse -> rose (remove 'r') -rose -> ros (remove 'e') -Example 2: - -Input: word1 = "intention", word2 = "execution" -Output: 5 -Explanation: -intention -> inention (remove 't') -inention -> enention (replace 'i' with 'e') -enention -> exention (replace 'n' with 'x') -exention -> exection (replace 'n' with 'c') -exection -> execution (insert 'u') -``` - -题目描述:修改一个字符串成为另一个字符串,使得修改次数最少。一次修改操作包括:插入一个字符、删除一个字符、替换一个字符。 - -```java -public int minDistance(String word1, String word2) { - if (word1 == null || word2 == null) { - return 0; - } - int m = word1.length(), n = word2.length(); - int[][] dp = new int[m + 1][n + 1]; - for (int i = 1; i <= m; i++) { - dp[i][0] = i; - } - for (int i = 1; i <= n; i++) { - dp[0][i] = i; - } - for (int i = 1; i <= m; i++) { - for (int j = 1; j <= n; j++) { - if (word1.charAt(i - 1) == word2.charAt(j - 1)) { - dp[i][j] = dp[i - 1][j - 1]; - } else { - dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; - } - } - } - return dp[m][n]; -} -``` - -**复制粘贴字符** - -[650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/) - -题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。 - -``` -Input: 3 -Output: 3 -Explanation: -Intitally, we have one character 'A'. -In step 1, we use Copy All operation. -In step 2, we use Paste operation to get 'AA'. -In step 3, we use Paste operation to get 'AAA'. -``` - -```java -public int minSteps(int n) { - if (n == 1) return 0; - for (int i = 2; i <= Math.sqrt(n); i++) { - if (n % i == 0) return i + minSteps(n / i); - } - return n; -} -``` - -```java -public int minSteps(int n) { - int[] dp = new int[n + 1]; - int h = (int) Math.sqrt(n); - for (int i = 2; i <= n; i++) { - dp[i] = i; - for (int j = 2; j <= h; j++) { - if (i % j == 0) { - dp[i] = dp[j] + dp[i / j]; - break; - } - } - } - return dp[n]; -} -``` - -## 数学 - -### 素数 - -**素数分解** - -每一个数都可以分解成素数的乘积,例如 84 = 22 \* 31 \* 50 \* 71 \* 110 \* 130 \* 170 \* … - -**整除** - -令 x = 2m0 \* 3m1 \* 5m2 \* 7m3 \* 11m4 \* … - -令 y = 2n0 \* 3n1 \* 5n2 \* 7n3 \* 11n4 \* … - -如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。 - -**最大公约数最小公倍数** - -x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) \* 3min(m1,n1) \* 5min(m2,n2) \* ... - -x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) \* 3max(m1,n1) \* 5max(m2,n2) \* ... - -**生成素数序列** - -[204. Count Primes (Easy)](https://leetcode.com/problems/count-primes/description/) - -埃拉托斯特尼筛法在每次找到一个素数时,将能被素数整除的数排除掉。 - -```java -public int countPrimes(int n) { - boolean[] notPrimes = new boolean[n + 1]; - int count = 0; - for (int i = 2; i < n; i++) { - if (notPrimes[i]) { - continue; - } - count++; - // 从 i * i 开始,因为如果 k < i,那么 k * i 在之前就已经被去除过了 - for (long j = (long) (i) * i; j < n; j += i) { - notPrimes[(int) j] = true; - } - } - return count; -} -``` - -### 最大公约数 - -```java -int gcd(int a, int b) { - return b == 0 ? a : gcd(b, a % b); -} -``` - -最小公倍数为两数的乘积除以最大公约数。 - -```java -int lcm(int a, int b) { - return a * b / gcd(a, b); -} -``` - -**使用位操作和减法求解最大公约数** - -[编程之美:2.7](#) - -对于 a 和 b 的最大公约数 f(a, b),有: - -- 如果 a 和 b 均为偶数,f(a, b) = 2\*f(a/2, b/2); -- 如果 a 是偶数 b 是奇数,f(a, b) = f(a/2, b); -- 如果 b 是偶数 a 是奇数,f(a, b) = f(a, b/2); -- 如果 a 和 b 均为奇数,f(a, b) = f(b, a-b); - -乘 2 和除 2 都可以转换为移位操作。 - -```java -public int gcd(int a, int b) { - if (a < b) { - return gcd(b, a); - } - if (b == 0) { - return a; - } - boolean isAEven = isEven(a), isBEven = isEven(b); - if (isAEven && isBEven) { - return 2 * gcd(a >> 1, b >> 1); - } else if (isAEven && !isBEven) { - return gcd(a >> 1, b); - } else if (!isAEven && isBEven) { - return gcd(a, b >> 1); - } else { - return gcd(b, a - b); - } -} -``` - -### 进制转换 - -**7 进制** - -[504. Base 7 (Easy)](https://leetcode.com/problems/base-7/description/) - -```java -public String convertToBase7(int num) { - if (num == 0) { - return "0"; - } - StringBuilder sb = new StringBuilder(); - boolean isNegative = num < 0; - if (isNegative) { - num = -num; - } - while (num > 0) { - sb.append(num % 7); - num /= 7; - } - String ret = sb.reverse().toString(); - return isNegative ? "-" + ret : ret; -} -``` - -Java 中 static String toString(int num, int radix) 可以将一个整数转换为 radix 进制表示的字符串。 - -```java -public String convertToBase7(int num) { - return Integer.toString(num, 7); -} -``` - -**16 进制** - -[405. Convert a Number to Hexadecimal (Easy)](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/) - -```html -Input: -26 - -Output: -"1a" - -Input: --1 - -Output: -"ffffffff" -``` - -负数要用它的补码形式。 - -```java -public String toHex(int num) { - char[] map = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - if (num == 0) return "0"; - StringBuilder sb = new StringBuilder(); - while (num != 0) { - sb.append(map[num & 0b1111]); - num >>>= 4; // 因为考虑的是补码形式,因此符号位就不能有特殊的意义,需要使用无符号右移,左边填 0 - } - return sb.reverse().toString(); -} -``` - -**26 进制** - -[168. Excel Sheet Column Title (Easy)](https://leetcode.com/problems/excel-sheet-column-title/description/) - -```html -1 -> A -2 -> B -3 -> C -... -26 -> Z -27 -> AA -28 -> AB -``` - -因为是从 1 开始计算的,而不是从 0 开始,因此需要对 n 执行 -1 操作。 - -```java -public String convertToTitle(int n) { - if (n == 0) { - return ""; - } - n--; - return convertToTitle(n / 26) + (char) (n % 26 + 'A'); -} -``` - -### 阶乘 - -**统计阶乘尾部有多少个 0** - -[172. Factorial Trailing Zeroes (Easy)](https://leetcode.com/problems/factorial-trailing-zeroes/description/) - -尾部的 0 由 2 * 5 得来,2 的数量明显多于 5 的数量,因此只要统计有多少个 5 即可。 - -对于一个数 N,它所包含 5 的个数为:N/5 + N/52 + N/53 + ...,其中 N/5 表示不大于 N 的数中 5 的倍数贡献一个 5,N/52 表示不大于 N 的数中 52 的倍数再贡献一个 5 ...。 - -```java -public int trailingZeroes(int n) { - return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5); -} -``` - -如果统计的是 N! 的二进制表示中最低位 1 的位置,只要统计有多少个 2 即可,该题目出自 [编程之美:2.2](#) 。和求解有多少个 5 一样,2 的个数为 N/2 + N/22 + N/23 + ... - -### 字符串加法减法 - -**二进制加法** - -[67. Add Binary (Easy)](https://leetcode.com/problems/add-binary/description/) - -```html -a = "11" -b = "1" -Return "100". -``` - -```java -public String addBinary(String a, String b) { - int i = a.length() - 1, j = b.length() - 1, carry = 0; - StringBuilder str = new StringBuilder(); - while (carry == 1 || i >= 0 || j >= 0) { - if (i >= 0 && a.charAt(i--) == '1') { - carry++; - } - if (j >= 0 && b.charAt(j--) == '1') { - carry++; - } - str.append(carry % 2); - carry /= 2; - } - return str.reverse().toString(); -} -``` - -**字符串加法** - -[415. Add Strings (Easy)](https://leetcode.com/problems/add-strings/description/) - -字符串的值为非负整数。 - -```java -public String addStrings(String num1, String num2) { - StringBuilder str = new StringBuilder(); - int carry = 0, i = num1.length() - 1, j = num2.length() - 1; - while (carry == 1 || i >= 0 || j >= 0) { - int x = i < 0 ? 0 : num1.charAt(i--) - '0'; - int y = j < 0 ? 0 : num2.charAt(j--) - '0'; - str.append((x + y + carry) % 10); - carry = (x + y + carry) / 10; - } - return str.reverse().toString(); -} -``` - -### 相遇问题 - -**改变数组元素使所有的数组元素都相等** - -[462. Minimum Moves to Equal Array Elements II (Medium)](https://leetcode.com/problems/minimum-moves-to-equal-array-elements-ii/description/) - -```html -Input: -[1,2,3] - -Output: -2 - -Explanation: -Only two moves are needed (remember each move increments or decrements one element): - -[1,2,3] => [2,2,3] => [2,2,2] -``` - -每次可以对一个数组元素加一或者减一,求最小的改变次数。 - -这是个典型的相遇问题,移动距离最小的方式是所有元素都移动到中位数。理由如下: - -设 m 为中位数。a 和 b 是 m 两边的两个元素,且 b > a。要使 a 和 b 相等,它们总共移动的次数为 b - a,这个值等于 (b - m) + (m - a),也就是把这两个数移动到中位数的移动次数。 - -设数组长度为 N,则可以找到 N/2 对 a 和 b 的组合,使它们都移动到 m 的位置。 - -**解法 1** - -先排序,时间复杂度:O(NlogN) - -```java -public int minMoves2(int[] nums) { - Arrays.sort(nums); - int move = 0; - int l = 0, h = nums.length - 1; - while (l <= h) { - move += nums[h] - nums[l]; - l++; - h--; - } - return move; -} -``` - -**解法 2** - -使用快速选择找到中位数,时间复杂度 O(N) - -```java -public int minMoves2(int[] nums) { - int move = 0; - int median = findKthSmallest(nums, nums.length / 2); - for (int num : nums) { - move += Math.abs(num - median); - } - return move; -} - -private int findKthSmallest(int[] nums, int k) { - int l = 0, h = nums.length - 1; - while (l < h) { - int j = partition(nums, l, h); - if (j == k) { - break; - } - if (j < k) { - l = j + 1; - } else { - h = j - 1; - } - } - return nums[k]; -} - -private int partition(int[] nums, int l, int h) { - int i = l, j = h + 1; - while (true) { - while (nums[++i] < nums[l] && i < h) ; - while (nums[--j] > nums[l] && j > l) ; - if (i >= j) { - break; - } - swap(nums, i, j); - } - swap(nums, l, j); - return j; -} - -private void swap(int[] nums, int i, int j) { - int tmp = nums[i]; - nums[i] = nums[j]; - nums[j] = tmp; -} -``` - -### 多数投票问题 - -**数组中出现次数多于 n / 2 的元素** - -[169. Majority Element (Easy)](https://leetcode.com/problems/majority-element/description/) - -先对数组排序,最中间那个数出现次数一定多于 n / 2。 - -```java -public int majorityElement(int[] nums) { - Arrays.sort(nums); - return nums[nums.length / 2]; -} -``` - -可以利用 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。 - -```java -public int majorityElement(int[] nums) { - int cnt = 0, majority = nums[0]; - for (int num : nums) { - majority = (cnt == 0) ? num : majority; - cnt = (majority == num) ? cnt + 1 : cnt - 1; - } - return majority; -} -``` - -### 其它 - -**平方数** - -[367. Valid Perfect Square (Easy)](https://leetcode.com/problems/valid-perfect-square/description/) - -```html -Input: 16 -Returns: True -``` - -平方序列:1,4,9,16,.. - -间隔:3,5,7,... - -间隔为等差数列,使用这个特性可以得到从 1 开始的平方序列。 - -```java -public boolean isPerfectSquare(int num) { - int subNum = 1; - while (num > 0) { - num -= subNum; - subNum += 2; - } - return num == 0; -} -``` - -**3 的 n 次方** - -[326. Power of Three (Easy)](https://leetcode.com/problems/power-of-three/description/) - -```java -public boolean isPowerOfThree(int n) { - return n > 0 && (1162261467 % n == 0); -} -``` - -**乘积数组** - -[238. Product of Array Except Self (Medium)](https://leetcode.com/problems/product-of-array-except-self/description/) - -```html -For example, given [1,2,3,4], return [24,12,8,6]. -``` - -给定一个数组,创建一个新数组,新数组的每个元素为原始数组中除了该位置上的元素之外所有元素的乘积。 - -要求时间复杂度为 O(N),并且不能使用除法。 - -```java -public int[] productExceptSelf(int[] nums) { - int n = nums.length; - int[] products = new int[n]; - Arrays.fill(products, 1); - int left = 1; - for (int i = 1; i < n; i++) { - left *= nums[i - 1]; - products[i] *= left; - } - int right = 1; - for (int i = n - 2; i >= 0; i--) { - right *= nums[i + 1]; - products[i] *= right; - } - return products; -} -``` - -**找出数组中的乘积最大的三个数** - -[628. Maximum Product of Three Numbers (Easy)](https://leetcode.com/problems/maximum-product-of-three-numbers/description/) - -```html -Input: [1,2,3,4] -Output: 24 -``` - -```java -public int maximumProduct(int[] nums) { - int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE, max3 = Integer.MIN_VALUE, min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; - for (int n : nums) { - if (n > max1) { - max3 = max2; - max2 = max1; - max1 = n; - } else if (n > max2) { - max3 = max2; - max2 = n; - } else if (n > max3) { - max3 = n; - } - - if (n < min1) { - min2 = min1; - min1 = n; - } else if (n < min2) { - min2 = n; - } - } - return Math.max(max1*max2*max3, max1*min1*min2); -} -``` - -# 数据结构相关 - -## 链表 - -链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。 - -**找出两个链表的交点** - -[160. Intersection of Two Linked Lists (Easy)](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) - -```html -A: a1 → a2 - ↘ - c1 → c2 → c3 - ↗ -B: b1 → b2 → b3 -``` - -要求:时间复杂度为 O(N),空间复杂度为 O(1) - -设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 - -当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 - -```java -public ListNode getIntersectionNode(ListNode headA, ListNode headB) { - ListNode l1 = headA, l2 = headB; - while (l1 != l2) { - l1 = (l1 == null) ? headB : l1.next; - l2 = (l2 == null) ? headA : l2.next; - } - return l1; -} -``` - -如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美 3.6]() 的问题。有两种解法: - -- 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环; -- 或者直接比较两个链表的最后一个节点是否相同。 - -**链表反转** - -[206. Reverse Linked List (Easy)](https://leetcode.com/problems/reverse-linked-list/description/) - -递归 - -```java -public ListNode reverseList(ListNode head) { - if (head == null || head.next == null) { - return head; - } - ListNode next = head.next; - ListNode newHead = reverseList(next); - next.next = head; - head.next = null; - return newHead; -} -``` - -头插法 - -```java -public ListNode reverseList(ListNode head) { - ListNode newHead = new ListNode(-1); - while (head != null) { - ListNode next = head.next; - head.next = newHead.next; - newHead.next = head; - head = next; - } - return newHead.next; -} -``` - -**归并两个有序的链表** - -[21. Merge Two Sorted Lists (Easy)](https://leetcode.com/problems/merge-two-sorted-lists/description/) - -```java -public ListNode mergeTwoLists(ListNode l1, ListNode l2) { - if (l1 == null) return l2; - if (l2 == null) return l1; - if (l1.val < l2.val) { - l1.next = mergeTwoLists(l1.next, l2); - return l1; - } else { - l2.next = mergeTwoLists(l1, l2.next); - return l2; - } -} -``` - -**从有序链表中删除重复节点** - -[83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) - -```html -Given 1->1->2, return 1->2. -Given 1->1->2->3->3, return 1->2->3. -``` - -```java -public ListNode deleteDuplicates(ListNode head) { - if (head == null || head.next == null) return head; - head.next = deleteDuplicates(head.next); - return head.val == head.next.val ? head.next : head; -} -``` - -**删除链表的倒数第 n 个节点** - -[19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) - -```html -Given linked list: 1->2->3->4->5, and n = 2. -After removing the second node from the end, the linked list becomes 1->2->3->5. -``` - -```java -public ListNode removeNthFromEnd(ListNode head, int n) { - ListNode fast = head; - while (n-- > 0) { - fast = fast.next; - } - if (fast == null) return head.next; - ListNode slow = head; - while (fast.next != null) { - fast = fast.next; - slow = slow.next; - } - slow.next = slow.next.next; - return head; -} -``` - -**交换链表中的相邻结点** - -[24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/) - -```html -Given 1->2->3->4, you should return the list as 2->1->4->3. -``` - -题目要求:不能修改结点的 val 值,O(1) 空间复杂度。 - -```java -public ListNode swapPairs(ListNode head) { - ListNode node = new ListNode(-1); - node.next = head; - ListNode pre = node; - while (pre.next != null && pre.next.next != null) { - ListNode l1 = pre.next, l2 = pre.next.next; - ListNode next = l2.next; - l1.next = next; - l2.next = l1; - pre.next = l2; - - pre = l1; - } - return node.next; -} -``` - -**链表求和** - -[445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/) - -```html -Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) -Output: 7 -> 8 -> 0 -> 7 -``` - -题目要求:不能修改原始链表。 - -```java -public ListNode addTwoNumbers(ListNode l1, ListNode l2) { - Stack l1Stack = buildStack(l1); - Stack l2Stack = buildStack(l2); - ListNode head = new ListNode(-1); - int carry = 0; - while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) { - int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); - int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); - int sum = x + y + carry; - ListNode node = new ListNode(sum % 10); - node.next = head.next; - head.next = node; - carry = sum / 10; - } - return head.next; -} - -private Stack buildStack(ListNode l) { - Stack stack = new Stack<>(); - while (l != null) { - stack.push(l.val); - l = l.next; - } - return stack; -} -``` - -**回文链表** - -[234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) - -题目要求:以 O(1) 的空间复杂度来求解。 - -切成两半,把后半段反转,然后比较两半是否相等。 - -```java -public boolean isPalindrome(ListNode head) { - if (head == null || head.next == null) return true; - ListNode slow = head, fast = head.next; - while (fast != null && fast.next != null) { - slow = slow.next; - fast = fast.next.next; - } - if (fast != null) slow = slow.next; // 偶数节点,让 slow 指向下一个节点 - cut(head, slow); // 切成两个链表 - return isEqual(head, reverse(slow)); -} - -private void cut(ListNode head, ListNode cutNode) { - while (head.next != cutNode) { - head = head.next; - } - head.next = null; -} - -private ListNode reverse(ListNode head) { - ListNode newHead = null; - while (head != null) { - ListNode nextNode = head.next; - head.next = newHead; - newHead = head; - head = nextNode; - } - return newHead; -} - -private boolean isEqual(ListNode l1, ListNode l2) { - while (l1 != null && l2 != null) { - if (l1.val != l2.val) return false; - l1 = l1.next; - l2 = l2.next; - } - return true; -} -``` - -**分隔链表** - -[725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/) - -```html -Input: -root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 -Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] -Explanation: -The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. -``` - -题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 - -```java -public ListNode[] splitListToParts(ListNode root, int k) { - int N = 0; - ListNode cur = root; - while (cur != null) { - N++; - cur = cur.next; - } - int mod = N % k; - int size = N / k; - ListNode[] ret = new ListNode[k]; - cur = root; - for (int i = 0; cur != null && i < k; i++) { - ret[i] = cur; - int curSize = size + (mod-- > 0 ? 1 : 0); - for (int j = 0; j < curSize - 1; j++) { - cur = cur.next; - } - ListNode next = cur.next; - cur.next = null; - cur = next; - } - return ret; -} -``` - -**链表元素按奇偶聚集** - -[328. Odd Even Linked List (Medium)](https://leetcode.com/problems/odd-even-linked-list/description/) - -```html -Example: -Given 1->2->3->4->5->NULL, -return 1->3->5->2->4->NULL. -``` - -```java -public ListNode oddEvenList(ListNode head) { - if (head == null) { - return head; - } - ListNode odd = head, even = head.next, evenHead = even; - while (even != null && even.next != null) { - odd.next = odd.next.next; - odd = odd.next; - even.next = even.next.next; - even = even.next; - } - odd.next = evenHead; - return head; -} -``` - -## 树 - -### 递归 - -一棵树要么是空树,要么有两个指针,每个指针指向一棵树。树是一种递归结构,很多树的问题可以使用递归来处理。 - -**树的高度** - -[104. Maximum Depth of Binary Tree (Easy)](https://leetcode.com/problems/maximum-depth-of-binary-tree/description/) - -```java -public int maxDepth(TreeNode root) { - if (root == null) return 0; - return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; -} -``` - -**平衡树** - -[110. Balanced Binary Tree (Easy)](https://leetcode.com/problems/balanced-binary-tree/description/) - -```html - 3 - / \ - 9 20 - / \ - 15 7 -``` - -平衡树左右子树高度差都小于等于 1 - -```java -private boolean result = true; - -public boolean isBalanced(TreeNode root) { - maxDepth(root); - return result; -} - -public int maxDepth(TreeNode root) { - if (root == null) return 0; - int l = maxDepth(root.left); - int r = maxDepth(root.right); - if (Math.abs(l - r) > 1) result = false; - return 1 + Math.max(l, r); -} -``` - -**两节点的最长路径** - -[543. Diameter of Binary Tree (Easy)](https://leetcode.com/problems/diameter-of-binary-tree/description/) - -```html -Input: - - 1 - / \ - 2 3 - / \ - 4 5 - -Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3]. -``` - -```java -private int max = 0; - -public int diameterOfBinaryTree(TreeNode root) { - depth(root); - return max; -} - -private int depth(TreeNode root) { - if (root == null) return 0; - int leftDepth = depth(root.left); - int rightDepth = depth(root.right); - max = Math.max(max, leftDepth + rightDepth); - return Math.max(leftDepth, rightDepth) + 1; -} -``` - -**翻转树** - -[226. Invert Binary Tree (Easy)](https://leetcode.com/problems/invert-binary-tree/description/) - -```java -public TreeNode invertTree(TreeNode root) { - if (root == null) return null; - TreeNode left = root.left; // 后面的操作会改变 left 指针,因此先保存下来 - root.left = invertTree(root.right); - root.right = invertTree(left); - return root; -} -``` - -**归并两棵树** - -[617. Merge Two Binary Trees (Easy)](https://leetcode.com/problems/merge-two-binary-trees/description/) - -```html -Input: - Tree 1 Tree 2 - 1 2 - / \ / \ - 3 2 1 3 - / \ \ - 5 4 7 - -Output: - 3 - / \ - 4 5 - / \ \ - 5 4 7 -``` - -```java -public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { - if (t1 == null && t2 == null) return null; - if (t1 == null) return t2; - if (t2 == null) return t1; - TreeNode root = new TreeNode(t1.val + t2.val); - root.left = mergeTrees(t1.left, t2.left); - root.right = mergeTrees(t1.right, t2.right); - return root; -} -``` - -**判断路径和是否等于一个数** - -[Leetcdoe : 112. Path Sum (Easy)](https://leetcode.com/problems/path-sum/description/) - -```html -Given the below binary tree and sum = 22, - - 5 - / \ - 4 8 - / / \ - 11 13 4 - / \ \ - 7 2 1 - -return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22. -``` - -路径和定义为从 root 到 leaf 的所有节点的和。 - -```java -public boolean hasPathSum(TreeNode root, int sum) { - if (root == null) return false; - if (root.left == null && root.right == null && root.val == sum) return true; - return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); -} -``` - -**统计路径和等于一个数的路径数量** - -[437. Path Sum III (Easy)](https://leetcode.com/problems/path-sum-iii/description/) - -```html -root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 - - 10 - / \ - 5 -3 - / \ \ - 3 2 11 - / \ \ -3 -2 1 - -Return 3. The paths that sum to 8 are: - -1. 5 -> 3 -2. 5 -> 2 -> 1 -3. -3 -> 11 -``` - -路径不一定以 root 开头,也不一定以 leaf 结尾,但是必须连续。 - -```java -public int pathSum(TreeNode root, int sum) { - if (root == null) return 0; - int ret = pathSumStartWithRoot(root, sum) + pathSum(root.left, sum) + pathSum(root.right, sum); - return ret; -} - -private int pathSumStartWithRoot(TreeNode root, int sum) { - if (root == null) return 0; - int ret = 0; - if (root.val == sum) ret++; - ret += pathSumStartWithRoot(root.left, sum - root.val) + pathSumStartWithRoot(root.right, sum - root.val); - return ret; -} -``` - -**子树** - -[572. Subtree of Another Tree (Easy)](https://leetcode.com/problems/subtree-of-another-tree/description/) - -```html -Given tree s: - 3 - / \ - 4 5 - / \ - 1 2 - -Given tree t: - 4 - / \ - 1 2 - -Return true, because t has the same structure and node values with a subtree of s. - -Given tree s: - - 3 - / \ - 4 5 - / \ - 1 2 - / - 0 - -Given tree t: - 4 - / \ - 1 2 - -Return false. -``` - -```java -public boolean isSubtree(TreeNode s, TreeNode t) { - if (s == null) return false; - return isSubtreeWithRoot(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t); -} - -private boolean isSubtreeWithRoot(TreeNode s, TreeNode t) { - if (t == null && s == null) return true; - if (t == null || s == null) return false; - if (t.val != s.val) return false; - return isSubtreeWithRoot(s.left, t.left) && isSubtreeWithRoot(s.right, t.right); -} -``` - -**树的对称** - -[101. Symmetric Tree (Easy)](https://leetcode.com/problems/symmetric-tree/description/) - -```html - 1 - / \ - 2 2 - / \ / \ -3 4 4 3 -``` - -```java -public boolean isSymmetric(TreeNode root) { - if (root == null) return true; - return isSymmetric(root.left, root.right); -} - -private boolean isSymmetric(TreeNode t1, TreeNode t2) { - if (t1 == null && t2 == null) return true; - if (t1 == null || t2 == null) return false; - if (t1.val != t2.val) return false; - return isSymmetric(t1.left, t2.right) && isSymmetric(t1.right, t2.left); -} -``` - -**最小路径** - -[111. Minimum Depth of Binary Tree (Easy)](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/) - -树的根节点到叶子节点的最小路径长度 - -```java -public int minDepth(TreeNode root) { - if (root == null) return 0; - int left = minDepth(root.left); - int right = minDepth(root.right); - if (left == 0 || right == 0) return left + right + 1; - return Math.min(left, right) + 1; -} -``` - -**统计左叶子节点的和** - -[404. Sum of Left Leaves (Easy)](https://leetcode.com/problems/sum-of-left-leaves/description/) - -```html - 3 - / \ - 9 20 - / \ - 15 7 - -There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24. -``` - -```java -public int sumOfLeftLeaves(TreeNode root) { - if (root == null) return 0; - if (isLeaf(root.left)) return root.left.val + sumOfLeftLeaves(root.right); - return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right); -} - -private boolean isLeaf(TreeNode node){ - if (node == null) return false; - return node.left == null && node.right == null; -} -``` - -**相同节点值的最大路径长度** - -[687. Longest Univalue Path (Easy)](https://leetcode.com/problems/longest-univalue-path/) - -```html - 1 - / \ - 4 5 - / \ \ - 4 4 5 - -Output : 2 -``` - -```java -private int path = 0; - -public int longestUnivaluePath(TreeNode root) { - dfs(root); - return path; -} - -private int dfs(TreeNode root){ - if (root == null) return 0; - int left = dfs(root.left); - int right = dfs(root.right); - int leftPath = root.left != null && root.left.val == root.val ? left + 1 : 0; - int rightPath = root.right != null && root.right.val == root.val ? right + 1 : 0; - path = Math.max(path, leftPath + rightPath); - return Math.max(leftPath, rightPath); -} -``` - -**间隔遍历** - -[337. House Robber III (Medium)](https://leetcode.com/problems/house-robber-iii/description/) - -```html - 3 - / \ - 2 3 - \ \ - 3 1 -Maximum amount of money the thief can rob = 3 + 3 + 1 = 7. -``` - -```java -public int rob(TreeNode root) { - if (root == null) return 0; - int val1 = root.val; - if (root.left != null) val1 += rob(root.left.left) + rob(root.left.right); - if (root.right != null) val1 += rob(root.right.left) + rob(root.right.right); - int val2 = rob(root.left) + rob(root.right); - return Math.max(val1, val2); -} -``` - -**找出二叉树中第二小的节点** - -[671. Second Minimum Node In a Binary Tree (Easy)](https://leetcode.com/problems/second-minimum-node-in-a-binary-tree/description/) - -```html -Input: - 2 - / \ - 2 5 - / \ - 5 7 - -Output: 5 -``` - -一个节点要么具有 0 个或 2 个子节点,如果有子节点,那么根节点是最小的节点。 - -```java -public int findSecondMinimumValue(TreeNode root) { - if (root == null) return -1; - if (root.left == null && root.right == null) return -1; - int leftVal = root.left.val; - int rightVal = root.right.val; - if (leftVal == root.val) leftVal = findSecondMinimumValue(root.left); - if (rightVal == root.val) rightVal = findSecondMinimumValue(root.right); - if (leftVal != -1 && rightVal != -1) return Math.min(leftVal, rightVal); - if (leftVal != -1) return leftVal; - return rightVal; -} -``` - -### 层次遍历 - -使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 - -**一棵树每层节点的平均数** - -[637. Average of Levels in Binary Tree (Easy)](https://leetcode.com/problems/average-of-levels-in-binary-tree/description/) - -```java -public List averageOfLevels(TreeNode root) { - List ret = new ArrayList<>(); - if (root == null) return ret; - Queue queue = new LinkedList<>(); - queue.add(root); - while (!queue.isEmpty()) { - int cnt = queue.size(); - double sum = 0; - for (int i = 0; i < cnt; i++) { - TreeNode node = queue.poll(); - sum += node.val; - if (node.left != null) queue.add(node.left); - if (node.right != null) queue.add(node.right); - } - ret.add(sum / cnt); - } - return ret; -} -``` - -**得到左下角的节点** - -[513. Find Bottom Left Tree Value (Easy)](https://leetcode.com/problems/find-bottom-left-tree-value/description/) - -```html -Input: - - 1 - / \ - 2 3 - / / \ - 4 5 6 - / - 7 - -Output: -7 -``` - -```java -public int findBottomLeftValue(TreeNode root) { - Queue queue = new LinkedList<>(); - queue.add(root); - while (!queue.isEmpty()) { - root = queue.poll(); - if (root.right != null) queue.add(root.right); - if (root.left != null) queue.add(root.left); - } - return root.val; -} -``` - -### 前中后序遍历 - -```html - 1 - / \ - 2 3 - / \ \ -4 5 6 -``` - -- 层次遍历顺序:[1 2 3 4 5 6] -- 前序遍历顺序:[1 2 4 5 3 6] -- 中序遍历顺序:[4 2 5 1 3 6] -- 后序遍历顺序:[4 5 2 6 3 1] - -层次遍历使用 BFS 实现,利用的就是 BFS 一层一层遍历的特性;而前序、中序、后序遍历利用了 DFS 实现。 - -前序、中序、后序遍只是在对节点访问的顺序有一点不同,其它都相同。 - -① 前序 - -```java -void dfs(TreeNode root) { - visit(root); - dfs(root.left); - dfs(root.right); -} -``` - -② 中序 - -```java -void dfs(TreeNode root) { - dfs(root.left); - visit(root); - dfs(root.right); -} -``` - -③ 后序 - -```java -void dfs(TreeNode root) { - dfs(root.left); - dfs(root.right); - visit(root); -} -``` - -**非递归实现二叉树的前序遍历** - -[144. Binary Tree Preorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-preorder-traversal/description/) - -```java -public List preorderTraversal(TreeNode root) { - List ret = new ArrayList<>(); - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode node = stack.pop(); - if (node == null) continue; - ret.add(node.val); - stack.push(node.right); // 先右后左,保证左子树先遍历 - stack.push(node.left); - } - return ret; -} -``` - -**非递归实现二叉树的后序遍历** - -[145. Binary Tree Postorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) - -前序遍历为 root -> left -> right,后序遍历为 left -> right -> root。可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。 - -```java -public List postorderTraversal(TreeNode root) { - List ret = new ArrayList<>(); - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode node = stack.pop(); - if (node == null) continue; - ret.add(node.val); - stack.push(node.left); - stack.push(node.right); - } - Collections.reverse(ret); - return ret; -} -``` - -**非递归实现二叉树的中序遍历** - -[94. Binary Tree Inorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) - -```java -public List inorderTraversal(TreeNode root) { - List ret = new ArrayList<>(); - if (root == null) return ret; - Stack stack = new Stack<>(); - TreeNode cur = root; - while (cur != null || !stack.isEmpty()) { - while (cur != null) { - stack.push(cur); - cur = cur.left; - } - TreeNode node = stack.pop(); - ret.add(node.val); - cur = node.right; - } - return ret; -} -``` - -### BST - -二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。 - -二叉查找树中序遍历有序。 - -**修剪二叉查找树** - -[669. Trim a Binary Search Tree (Easy)](https://leetcode.com/problems/trim-a-binary-search-tree/description/) - -```html -Input: - - 3 - / \ - 0 4 - \ - 2 - / - 1 - - L = 1 - R = 3 - -Output: - - 3 - / - 2 - / - 1 -``` - -题目描述:只保留值在 L \~ R 之间的节点 - -```java -public TreeNode trimBST(TreeNode root, int L, int R) { - if (root == null) return null; - if (root.val > R) return trimBST(root.left, L, R); - if (root.val < L) return trimBST(root.right, L, R); - root.left = trimBST(root.left, L, R); - root.right = trimBST(root.right, L, R); - return root; -} -``` - -**寻找二叉查找树的第 k 个元素** - -[230. Kth Smallest Element in a BST (Medium)](https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/) - - -中序遍历解法: - -```java -private int cnt = 0; -private int val; - -public int kthSmallest(TreeNode root, int k) { - inOrder(root, k); - return val; -} - -private void inOrder(TreeNode node, int k) { - if (node == null) return; - inOrder(node.left, k); - cnt++; - if (cnt == k) { - val = node.val; - return; - } - inOrder(node.right, k); -} -``` - -递归解法: - -```java -public int kthSmallest(TreeNode root, int k) { - int leftCnt = count(root.left); - if (leftCnt == k - 1) return root.val; - if (leftCnt > k - 1) return kthSmallest(root.left, k); - return kthSmallest(root.right, k - leftCnt - 1); -} - -private int count(TreeNode node) { - if (node == null) return 0; - return 1 + count(node.left) + count(node.right); -} -``` - -**把二叉查找树每个节点的值都加上比它大的节点的值** - -[Convert BST to Greater Tree (Easy)](https://leetcode.com/problems/convert-bst-to-greater-tree/description/) - -```html -Input: The root of a Binary Search Tree like this: - - 5 - / \ - 2 13 - -Output: The root of a Greater Tree like this: - - 18 - / \ - 20 13 -``` - -先遍历右子树。 - -```java -private int sum = 0; - -public TreeNode convertBST(TreeNode root) { - traver(root); - return root; -} - -private void traver(TreeNode node) { - if (node == null) return; - traver(node.right); - sum += node.val; - node.val = sum; - traver(node.left); -} -``` - -**二叉查找树的最近公共祖先** - -[235. Lowest Common Ancestor of a Binary Search Tree (Easy)](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) - -```html - _______6______ - / \ - ___2__ ___8__ - / \ / \ -0 4 7 9 - / \ - 3 5 - -For example, the lowest common ancestor (LCA) of nodes 2 and 8 is 6. Another example is LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition. -``` - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q); - if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q); - return root; -} -``` - -**二叉树的最近公共祖先** - -[236. Lowest Common Ancestor of a Binary Tree (Medium) ](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree/description/) - -```html - _______3______ - / \ - ___5__ ___1__ - / \ / \ -6 2 0 8 - / \ - 7 4 - -For example, the lowest common ancestor (LCA) of nodes 5 and 1 is 3. Another example is LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition. -``` - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - if (root == null || root == p || root == q) return root; - TreeNode left = lowestCommonAncestor(root.left, p, q); - TreeNode right = lowestCommonAncestor(root.right, p, q); - return left == null ? right : right == null ? left : root; -} -``` - -**从有序数组中构造二叉查找树** - -[108. Convert Sorted Array to Binary Search Tree (Easy)](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) - -```java -public TreeNode sortedArrayToBST(int[] nums) { - return toBST(nums, 0, nums.length - 1); -} - -private TreeNode toBST(int[] nums, int sIdx, int eIdx){ - if (sIdx > eIdx) return null; - int mIdx = (sIdx + eIdx) / 2; - TreeNode root = new TreeNode(nums[mIdx]); - root.left = toBST(nums, sIdx, mIdx - 1); - root.right = toBST(nums, mIdx + 1, eIdx); - return root; -} -``` - -**根据有序链表构造平衡的二叉查找树** - -[109. Convert Sorted List to Binary Search Tree (Medium)](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/) - -```html -Given the sorted linked list: [-10,-3,0,5,9], - -One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST: - - 0 - / \ - -3 9 - / / - -10 5 -``` - -```java -public TreeNode sortedListToBST(ListNode head) { - if (head == null) return null; - if (head.next == null) return new TreeNode(head.val); - ListNode preMid = preMid(head); - ListNode mid = preMid.next; - preMid.next = null; // 断开链表 - TreeNode t = new TreeNode(mid.val); - t.left = sortedListToBST(head); - t.right = sortedListToBST(mid.next); - return t; -} - -private ListNode preMid(ListNode head) { - ListNode slow = head, fast = head.next; - ListNode pre = head; - while (fast != null && fast.next != null) { - pre = slow; - slow = slow.next; - fast = fast.next.next; - } - return pre; -} -``` - -**在二叉查找树中寻找两个节点,使它们的和为一个给定值** - -[653. Two Sum IV - Input is a BST (Easy)](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) - -```html -Input: - - 5 - / \ - 3 6 - / \ \ -2 4 7 - -Target = 9 - -Output: True -``` - -使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。 - -应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。 - -```java -public boolean findTarget(TreeNode root, int k) { - List nums = new ArrayList<>(); - inOrder(root, nums); - int i = 0, j = nums.size() - 1; - while (i < j) { - int sum = nums.get(i) + nums.get(j); - if (sum == k) return true; - if (sum < k) i++; - else j--; - } - return false; -} - -private void inOrder(TreeNode root, List nums) { - if (root == null) return; - inOrder(root.left, nums); - nums.add(root.val); - inOrder(root.right, nums); -} -``` - -**在二叉查找树中查找两个节点之差的最小绝对值** - -[530. Minimum Absolute Difference in BST (Easy)](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) - -```html -Input: - - 1 - \ - 3 - / - 2 - -Output: - -1 -``` - -利用二叉查找树的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。 - -```java -private int minDiff = Integer.MAX_VALUE; -private TreeNode preNode = null; - -public int getMinimumDifference(TreeNode root) { - inOrder(root); - return minDiff; -} - -private void inOrder(TreeNode node) { - if (node == null) return; - inOrder(node.left); - if (preNode != null) minDiff = Math.min(minDiff, node.val - preNode.val); - preNode = node; - inOrder(node.right); -} -``` - -**寻找二叉查找树中出现次数最多的值** - -[501. Find Mode in Binary Search Tree (Easy)](https://leetcode.com/problems/find-mode-in-binary-search-tree/description/) - -```html - 1 - \ - 2 - / - 2 - -return [2]. -``` - -答案可能不止一个,也就是有多个值出现的次数一样多。 - -```java -private int curCnt = 1; -private int maxCnt = 1; -private TreeNode preNode = null; - -public int[] findMode(TreeNode root) { - List maxCntNums = new ArrayList<>(); - inOrder(root, maxCntNums); - int[] ret = new int[maxCntNums.size()]; - int idx = 0; - for (int num : maxCntNums) { - ret[idx++] = num; - } - return ret; -} - -private void inOrder(TreeNode node, List nums) { - if (node == null) return; - inOrder(node.left, nums); - if (preNode != null) { - if (preNode.val == node.val) curCnt++; - else curCnt = 1; - } - if (curCnt > maxCnt) { - maxCnt = curCnt; - nums.clear(); - nums.add(node.val); - } else if (curCnt == maxCnt) { - nums.add(node.val); - } - preNode = node; - inOrder(node.right, nums); -} -``` - -### Trie - -

- -Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。 - -**实现一个 Trie** - -[208. Implement Trie (Prefix Tree) (Medium)](https://leetcode.com/problems/implement-trie-prefix-tree/description/) - -```java -class Trie { - - private class Node { - Node[] childs = new Node[26]; - boolean isLeaf; - } - - private Node root = new Node(); - - public Trie() { - } - - public void insert(String word) { - insert(word, root); - } - - private void insert(String word, Node node) { - if (node == null) return; - if (word.length() == 0) { - node.isLeaf = true; - return; - } - int index = indexForChar(word.charAt(0)); - if (node.childs[index] == null) { - node.childs[index] = new Node(); - } - insert(word.substring(1), node.childs[index]); - } - - public boolean search(String word) { - return search(word, root); - } - - private boolean search(String word, Node node) { - if (node == null) return false; - if (word.length() == 0) return node.isLeaf; - int index = indexForChar(word.charAt(0)); - return search(word.substring(1), node.childs[index]); - } - - public boolean startsWith(String prefix) { - return startWith(prefix, root); - } - - private boolean startWith(String prefix, Node node) { - if (node == null) return false; - if (prefix.length() == 0) return true; - int index = indexForChar(prefix.charAt(0)); - return startWith(prefix.substring(1), node.childs[index]); - } - - private int indexForChar(char c) { - return c - 'a'; - } -} -``` - -**实现一个 Trie,用来求前缀和** - -[677. Map Sum Pairs (Medium)](https://leetcode.com/problems/map-sum-pairs/description/) - -```html -Input: insert("apple", 3), Output: Null -Input: sum("ap"), Output: 3 -Input: insert("app", 2), Output: Null -Input: sum("ap"), Output: 5 -``` - -```java -class MapSum { - - private class Node { - Node[] child = new Node[26]; - int value; - } - - private Node root = new Node(); - - public MapSum() { - - } - - public void insert(String key, int val) { - insert(key, root, val); - } - - private void insert(String key, Node node, int val) { - if (node == null) return; - if (key.length() == 0) { - node.value = val; - return; - } - int index = indexForChar(key.charAt(0)); - if (node.child[index] == null) { - node.child[index] = new Node(); - } - insert(key.substring(1), node.child[index], val); - } - - public int sum(String prefix) { - return sum(prefix, root); - } - - private int sum(String prefix, Node node) { - if (node == null) return 0; - if (prefix.length() != 0) { - int index = indexForChar(prefix.charAt(0)); - return sum(prefix.substring(1), node.child[index]); - } - int sum = node.value; - for (Node child : node.child) { - sum += sum(prefix, child); - } - return sum; - } - - private int indexForChar(char c) { - return c - 'a'; - } -} -``` - - -## 栈和队列 - -**用栈实现队列** - -[232. Implement Queue using Stacks (Easy)](https://leetcode.com/problems/implement-queue-using-stacks/description/) - -栈的顺序为后进先出,而队列的顺序为先进先出。使用两个栈实现队列,一个元素需要经过两个栈才能出队列,在经过第一个栈时元素顺序被反转,经过第二个栈时再次被反转,此时就是先进先出顺序。 - -```java -class MyQueue { - - private Stack in = new Stack<>(); - private Stack out = new Stack<>(); - - public void push(int x) { - in.push(x); - } - - public int pop() { - in2out(); - return out.pop(); - } - - public int peek() { - in2out(); - return out.peek(); - } - - private void in2out() { - if (out.isEmpty()) { - while (!in.isEmpty()) { - out.push(in.pop()); - } - } - } - - public boolean empty() { - return in.isEmpty() && out.isEmpty(); - } -} -``` - -**用队列实现栈** - -[225. Implement Stack using Queues (Easy)](https://leetcode.com/problems/implement-stack-using-queues/description/) - -在将一个元素 x 插入队列时,为了维护原来的后进先出顺序,需要让 x 插入队列首部。而队列的默认插入顺序是队列尾部,因此在将 x 插入队列尾部之后,需要让除了 x 之外的所有元素出队列,再入队列。 - -```java -class MyStack { - - private Queue queue; - - public MyStack() { - queue = new LinkedList<>(); - } - - public void push(int x) { - queue.add(x); - int cnt = queue.size(); - while (cnt-- > 1) { - queue.add(queue.poll()); - } - } - - public int pop() { - return queue.remove(); - } - - public int top() { - return queue.peek(); - } - - public boolean empty() { - return queue.isEmpty(); - } -} -``` - -**最小值栈** - -[155. Min Stack (Easy)](https://leetcode.com/problems/min-stack/description/) - -```java -class MinStack { - - private Stack dataStack; - private Stack minStack; - private int min; - - public MinStack() { - dataStack = new Stack<>(); - minStack = new Stack<>(); - min = Integer.MAX_VALUE; - } - - public void push(int x) { - dataStack.add(x); - min = Math.min(min, x); - minStack.add(min); - } - - public void pop() { - dataStack.pop(); - minStack.pop(); - min = minStack.isEmpty() ? Integer.MAX_VALUE : minStack.peek(); - } - - public int top() { - return dataStack.peek(); - } - - public int getMin() { - return minStack.peek(); - } -} -``` - -对于实现最小值队列问题,可以先将队列使用栈来实现,然后就将问题转换为最小值栈,这个问题出现在 编程之美:3.7。 - -**用栈实现括号匹配** - -[20. Valid Parentheses (Easy)](https://leetcode.com/problems/valid-parentheses/description/) - -```html -"()[]{}" - -Output : true -``` - -```java -public boolean isValid(String s) { - Stack stack = new Stack<>(); - for (char c : s.toCharArray()) { - if (c == '(' || c == '{' || c == '[') { - stack.push(c); - } else { - if (stack.isEmpty()) { - return false; - } - char cStack = stack.pop(); - boolean b1 = c == ')' && cStack != '('; - boolean b2 = c == ']' && cStack != '['; - boolean b3 = c == '}' && cStack != '{'; - if (b1 || b2 || b3) { - return false; - } - } - } - return stack.isEmpty(); -} -``` - -**数组中元素与下一个比它大的元素之间的距离** - -[739. Daily Temperatures (Medium)](https://leetcode.com/problems/daily-temperatures/description/) - -```html -Input: [73, 74, 75, 71, 69, 72, 76, 73] -Output: [1, 1, 4, 2, 1, 1, 0, 0] -``` - -在遍历数组时用栈把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。 - -```java -public int[] dailyTemperatures(int[] temperatures) { - int n = temperatures.length; - int[] dist = new int[n]; - Stack indexs = new Stack<>(); - for (int curIndex = 0; curIndex < n; curIndex++) { - while (!indexs.isEmpty() && temperatures[curIndex] > temperatures[indexs.peek()]) { - int preIndex = indexs.pop(); - dist[preIndex] = curIndex - preIndex; - } - indexs.add(curIndex); - } - return dist; -} -``` - -**循环数组中比当前元素大的下一个元素** - -[503. Next Greater Element II (Medium)](https://leetcode.com/problems/next-greater-element-ii/description/) - -```text -Input: [1,2,1] -Output: [2,-1,2] -Explanation: The first 1's next greater number is 2; -The number 2 can't find next greater number; -The second 1's next greater number needs to search circularly, which is also 2. -``` - -与 739. Daily Temperatures (Medium) 不同的是,数组是循环数组,并且最后要求的不是距离而是下一个元素。 - -```java -public int[] nextGreaterElements(int[] nums) { - int n = nums.length; - int[] next = new int[n]; - Arrays.fill(next, -1); - Stack pre = new Stack<>(); - for (int i = 0; i < n * 2; i++) { - int num = nums[i % n]; - while (!pre.isEmpty() && nums[pre.peek()] < num) { - next[pre.pop()] = num; - } - if (i < n){ - pre.push(i); - } - } - return next; -} -``` - -## 哈希表 - -哈希表使用 O(N) 空间复杂度存储数据,并且以 O(1) 时间复杂度求解问题。 - -- Java 中的 **HashSet** 用于存储一个集合,可以查找元素是否在集合中。如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。 - -- Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中 [Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 - - -**数组中两个数的和为给定值** - -[1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/) - -可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。 - -用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i],如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。 - -```java -public int[] twoSum(int[] nums, int target) { - HashMap indexForNum = new HashMap<>(); - for (int i = 0; i < nums.length; i++) { - if (indexForNum.containsKey(target - nums[i])) { - return new int[]{indexForNum.get(target - nums[i]), i}; - } else { - indexForNum.put(nums[i], i); - } - } - return null; -} -``` - -**判断数组是否含有重复元素** - -[217. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/) - -```java -public boolean containsDuplicate(int[] nums) { - Set set = new HashSet<>(); - for (int num : nums) { - set.add(num); - } - return set.size() < nums.length; -} -``` - -**最长和谐序列** - -[594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/) - -```html -Input: [1,3,2,2,5,2,3,7] -Output: 5 -Explanation: The longest harmonious subsequence is [3,2,2,2,3]. -``` - -和谐序列中最大数和最小数之差正好为 1,应该注意的是序列的元素不一定是数组的连续元素。 - -```java -public int findLHS(int[] nums) { - Map countForNum = new HashMap<>(); - for (int num : nums) { - countForNum.put(num, countForNum.getOrDefault(num, 0) + 1); - } - int longest = 0; - for (int num : countForNum.keySet()) { - if (countForNum.containsKey(num + 1)) { - longest = Math.max(longest, countForNum.get(num + 1) + countForNum.get(num)); - } - } - return longest; -} -``` - -**最长连续序列** - -[128. Longest Consecutive Sequence (Hard)](https://leetcode.com/problems/longest-consecutive-sequence/description/) - -```html -Given [100, 4, 200, 1, 3, 2], -The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4. -``` - -要求以 O(N) 的时间复杂度求解。 - -```java -public int longestConsecutive(int[] nums) { - Map countForNum = new HashMap<>(); - for (int num : nums) { - countForNum.put(num, 1); - } - for (int num : nums) { - forward(countForNum, num); - } - return maxCount(countForNum); -} - -private int forward(Map countForNum, int num) { - if (!countForNum.containsKey(num)) { - return 0; - } - int cnt = countForNum.get(num); - if (cnt > 1) { - return cnt; - } - cnt = forward(countForNum, num + 1) + 1; - countForNum.put(num, cnt); - return cnt; -} - -private int maxCount(Map countForNum) { - int max = 0; - for (int num : countForNum.keySet()) { - max = Math.max(max, countForNum.get(num)); - } - return max; -} -``` - -## 字符串 - -**字符串循环移位包含** - -[编程之美 3.1](#) - -```html -s1 = AABCD, s2 = CDAA -Return : true -``` - -给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。 - -s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。 - -**字符串循环移位** - -[编程之美 2.17](#) - -```html -s = "abcd123" k = 3 -Return "123abcd" -``` - -将字符串向右循环移动 k 位。 - -将 abcd123 中的 abcd 和 123 单独翻转,得到 dcba321,然后对整个字符串进行翻转,得到 123abcd。 - -**字符串中单词的翻转** - -[程序员代码面试指南](#) - -```html -s = "I am a student" -Return "student a am I" -``` - -将每个单词翻转,然后将整个字符串翻转。 - -**两个字符串包含的字符是否完全相同** - -[242. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/) - -```html -s = "anagram", t = "nagaram", return true. -s = "rat", t = "car", return false. -``` - -可以用 HashMap 来映射字符与出现次数,然后比较两个字符串出现的字符数量是否相同。 - -由于本题的字符串只包含 26 个小写字符,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,不再使用 HashMap。 - -```java -public boolean isAnagram(String s, String t) { - int[] cnts = new int[26]; - for (char c : s.toCharArray()) { - cnts[c - 'a']++; - } - for (char c : t.toCharArray()) { - cnts[c - 'a']--; - } - for (int cnt : cnts) { - if (cnt != 0) { - return false; - } - } - return true; -} -``` - -**计算一组字符集合可以组成的回文字符串的最大长度** - -[409. Longest Palindrome (Easy)](https://leetcode.com/problems/longest-palindrome/description/) - -```html -Input : "abccccdd" -Output : 7 -Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7. -``` - -使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。 - -因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。 - -```java -public int longestPalindrome(String s) { - int[] cnts = new int[256]; - for (char c : s.toCharArray()) { - cnts[c]++; - } - int palindrome = 0; - for (int cnt : cnts) { - palindrome += (cnt / 2) * 2; - } - if (palindrome < s.length()) { - palindrome++; // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间 - } - return palindrome; -} -``` - -**字符串同构** - -[205. Isomorphic Strings (Easy)](https://leetcode.com/problems/isomorphic-strings/description/) - -```html -Given "egg", "add", return true. -Given "foo", "bar", return false. -Given "paper", "title", return true. -``` - -记录一个字符上次出现的位置,如果两个字符串中的字符上次出现的位置一样,那么就属于同构。 - -```java -public boolean isIsomorphic(String s, String t) { - int[] preIndexOfS = new int[256]; - int[] preIndexOfT = new int[256]; - for (int i = 0; i < s.length(); i++) { - char sc = s.charAt(i), tc = t.charAt(i); - if (preIndexOfS[sc] != preIndexOfT[tc]) { - return false; - } - preIndexOfS[sc] = i + 1; - preIndexOfT[tc] = i + 1; - } - return true; -} -``` - -**回文子字符串个数** - -[647. Palindromic Substrings (Medium)](https://leetcode.com/problems/palindromic-substrings/description/) - -```html -Input: "aaa" -Output: 6 -Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa". -``` - -从字符串的某一位开始,尝试着去扩展子字符串。 - -```java -private int cnt = 0; - -public int countSubstrings(String s) { - for (int i = 0; i < s.length(); i++) { - extendSubstrings(s, i, i); // 奇数长度 - extendSubstrings(s, i, i + 1); // 偶数长度 - } - return cnt; -} - -private void extendSubstrings(String s, int start, int end) { - while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) { - start--; - end++; - cnt++; - } -} -``` - -**判断一个整数是否是回文数** - -[9. Palindrome Number (Easy)](https://leetcode.com/problems/palindrome-number/description/) - -要求不能使用额外空间,也就不能将整数转换为字符串进行判断。 - -将整数分成左右两部分,右边那部分需要转置,然后判断这两部分是否相等。 - -```java -public boolean isPalindrome(int x) { - if (x == 0) { - return true; - } - if (x < 0 || x % 10 == 0) { - return false; - } - int right = 0; - while (x > right) { - right = right * 10 + x % 10; - x /= 10; - } - return x == right || x == right / 10; -} -``` - -**统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数** - -[696. Count Binary Substrings (Easy)](https://leetcode.com/problems/count-binary-substrings/description/) - -```html -Input: "00110011" -Output: 6 -Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01". -``` - -```java -public int countBinarySubstrings(String s) { - int preLen = 0, curLen = 1, count = 0; - for (int i = 1; i < s.length(); i++) { - if (s.charAt(i) == s.charAt(i - 1)) { - curLen++; - } else { - preLen = curLen; - curLen = 1; - } - - if (preLen >= curLen) { - count++; - } - } - return count; -} -``` - -## 数组与矩阵 - -**把数组中的 0 移到末尾** - -[283. Move Zeroes (Easy)](https://leetcode.com/problems/move-zeroes/description/) - -```html -For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0]. -``` - -```java -public void moveZeroes(int[] nums) { - int idx = 0; - for (int num : nums) { - if (num != 0) { - nums[idx++] = num; - } - } - while (idx < nums.length) { - nums[idx++] = 0; - } -} -``` - -**改变矩阵维度** - -[566. Reshape the Matrix (Easy)](https://leetcode.com/problems/reshape-the-matrix/description/) - -```html -Input: -nums = -[[1,2], - [3,4]] -r = 1, c = 4 - -Output: -[[1,2,3,4]] - -Explanation: -The row-traversing of nums is [1,2,3,4]. The new reshaped matrix is a 1 * 4 matrix, fill it row by row by using the previous list. -``` - -```java -public int[][] matrixReshape(int[][] nums, int r, int c) { - int m = nums.length, n = nums[0].length; - if (m * n != r * c) { - return nums; - } - int[][] reshapedNums = new int[r][c]; - int index = 0; - for (int i = 0; i < r; i++) { - for (int j = 0; j < c; j++) { - reshapedNums[i][j] = nums[index / n][index % n]; - index++; - } - } - return reshapedNums; -} -``` - -**找出数组中最长的连续 1** - -[485. Max Consecutive Ones (Easy)](https://leetcode.com/problems/max-consecutive-ones/description/) - -```java -public int findMaxConsecutiveOnes(int[] nums) { - int max = 0, cur = 0; - for (int x : nums) { - cur = x == 0 ? 0 : cur + 1; - max = Math.max(max, cur); - } - return max; -} -``` - -**有序矩阵查找** - -[240. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) - -```html -[ - [ 1, 5, 9], - [10, 11, 13], - [12, 13, 15] -] -``` - -```java -public boolean searchMatrix(int[][] matrix, int target) { - if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false; - int m = matrix.length, n = matrix[0].length; - int row = 0, col = n - 1; - while (row < m && col >= 0) { - if (target == matrix[row][col]) return true; - else if (target < matrix[row][col]) col--; - else row++; - } - return false; -} -``` - -**有序矩阵的 Kth Element** - -[378. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) - -```html -matrix = [ - [ 1, 5, 9], - [10, 11, 13], - [12, 13, 15] -], -k = 8, - -return 13. -``` - -解题参考:[Share my thoughts and Clean Java Code](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/discuss/85173) - -二分查找解法: - -```java -public int kthSmallest(int[][] matrix, int k) { - int m = matrix.length, n = matrix[0].length; - int lo = matrix[0][0], hi = matrix[m - 1][n - 1]; - while (lo <= hi) { - int mid = lo + (hi - lo) / 2; - int cnt = 0; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n && matrix[i][j] <= mid; j++) { - cnt++; - } - } - if (cnt < k) lo = mid + 1; - else hi = mid - 1; - } - return lo; -} -``` - -堆解法: - -```java -public int kthSmallest(int[][] matrix, int k) { - int m = matrix.length, n = matrix[0].length; - PriorityQueue pq = new PriorityQueue(); - for(int j = 0; j < n; j++) pq.offer(new Tuple(0, j, matrix[0][j])); - for(int i = 0; i < k - 1; i++) { // 小根堆,去掉 k - 1 个堆顶元素,此时堆顶元素就是第 k 的数 - Tuple t = pq.poll(); - if(t.x == m - 1) continue; - pq.offer(new Tuple(t.x + 1, t.y, matrix[t.x + 1][t.y])); - } - return pq.poll().val; -} - -class Tuple implements Comparable { - int x, y, val; - public Tuple(int x, int y, int val) { - this.x = x; this.y = y; this.val = val; - } - - @Override - public int compareTo(Tuple that) { - return this.val - that.val; - } -} -``` - -**一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数** - -[645. Set Mismatch (Easy)](https://leetcode.com/problems/set-mismatch/description/) - -```html -Input: nums = [1,2,2,4] -Output: [2,3] -``` - -```html -Input: nums = [1,2,2,4] -Output: [2,3] -``` - -最直接的方法是先对数组进行排序,这种方法时间复杂度为 O(NlogN)。本题可以以 O(N) 的时间复杂度、O(1) 空间复杂度来求解。 - -主要思想是通过交换数组元素,使得数组上的元素在正确的位置上。 - -```java -public int[] findErrorNums(int[] nums) { - for (int i = 0; i < nums.length; i++) { - while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) { - swap(nums, i, nums[i] - 1); - } - } - for (int i = 0; i < nums.length; i++) { - if (nums[i] != i + 1) { - return new int[]{nums[i], i + 1}; - } - } - return null; -} - -private void swap(int[] nums, int i, int j) { - int tmp = nums[i]; - nums[i] = nums[j]; - nums[j] = tmp; -} -``` - -类似题目: - -- [448. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素 -- [442. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。 - -**找出数组中重复的数,数组值在 [1, n] 之间** - -[287. Find the Duplicate Number (Medium)](https://leetcode.com/problems/find-the-duplicate-number/description/) - -要求不能修改数组,也不能使用额外的空间。 - -二分查找解法: - -```java -public int findDuplicate(int[] nums) { - int l = 1, h = nums.length - 1; - while (l <= h) { - int mid = l + (h - l) / 2; - int cnt = 0; - for (int i = 0; i < nums.length; i++) { - if (nums[i] <= mid) cnt++; - } - if (cnt > mid) h = mid - 1; - else l = mid + 1; - } - return l; -} -``` - -双指针解法,类似于有环链表中找出环的入口: - -```java -public int findDuplicate(int[] nums) { - int slow = nums[0], fast = nums[nums[0]]; - while (slow != fast) { - slow = nums[slow]; - fast = nums[nums[fast]]; - } - fast = 0; - while (slow != fast) { - slow = nums[slow]; - fast = nums[fast]; - } - return slow; -} -``` - -**数组相邻差值的个数** - -[667. Beautiful Arrangement II (Medium)](https://leetcode.com/problems/beautiful-arrangement-ii/description/) - -```html -Input: n = 3, k = 2 -Output: [1, 3, 2] -Explanation: The [1, 3, 2] has three different positive integers ranging from 1 to 3, and the [2, 1] has exactly 2 distinct integers: 1 and 2. -``` - -题目描述:数组元素为 1\~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。 - -让前 k+1 个元素构建出 k 个不相同的差值,序列为:1 k+1 2 k 3 k-1 ... k/2 k/2+1. - -```java -public int[] constructArray(int n, int k) { - int[] ret = new int[n]; - ret[0] = 1; - for (int i = 1, interval = k; i <= k; i++, interval--) { - ret[i] = i % 2 == 1 ? ret[i - 1] + interval : ret[i - 1] - interval; - } - for (int i = k + 1; i < n; i++) { - ret[i] = i + 1; - } - return ret; -} -``` - -**数组的度** - -[697. Degree of an Array (Easy)](https://leetcode.com/problems/degree-of-an-array/description/) - -```html -Input: [1,2,2,3,1,4,2] -Output: 6 -``` - -题目描述:数组的度定义为元素出现的最高频率,例如上面的数组度为 3。要求找到一个最小的子数组,这个子数组的度和原数组一样。 - -```java -public int findShortestSubArray(int[] nums) { - Map numsCnt = new HashMap<>(); - Map numsLastIndex = new HashMap<>(); - Map numsFirstIndex = new HashMap<>(); - for (int i = 0; i < nums.length; i++) { - int num = nums[i]; - numsCnt.put(num, numsCnt.getOrDefault(num, 0) + 1); - numsLastIndex.put(num, i); - if (!numsFirstIndex.containsKey(num)) { - numsFirstIndex.put(num, i); - } - } - int maxCnt = 0; - for (int num : nums) { - maxCnt = Math.max(maxCnt, numsCnt.get(num)); - } - int ret = nums.length; - for (int i = 0; i < nums.length; i++) { - int num = nums[i]; - int cnt = numsCnt.get(num); - if (cnt != maxCnt) continue; - ret = Math.min(ret, numsLastIndex.get(num) - numsFirstIndex.get(num) + 1); - } - return ret; -} -``` - -**对角元素相等的矩阵** - -[766. Toeplitz Matrix (Easy)](https://leetcode.com/problems/toeplitz-matrix/description/) - -```html -1234 -5123 -9512 - -In the above grid, the diagonals are "[9]", "[5, 5]", "[1, 1, 1]", "[2, 2, 2]", "[3, 3]", "[4]", and in each diagonal all elements are the same, so the answer is True. -``` - -```java -public boolean isToeplitzMatrix(int[][] matrix) { - for (int i = 0; i < matrix[0].length; i++) { - if (!check(matrix, matrix[0][i], 0, i)) { - return false; - } - } - for (int i = 0; i < matrix.length; i++) { - if (!check(matrix, matrix[i][0], i, 0)) { - return false; - } - } - return true; -} - -private boolean check(int[][] matrix, int expectValue, int row, int col) { - if (row >= matrix.length || col >= matrix[0].length) { - return true; - } - if (matrix[row][col] != expectValue) { - return false; - } - return check(matrix, expectValue, row + 1, col + 1); -} -``` - -**嵌套数组** - -[565. Array Nesting (Medium)](https://leetcode.com/problems/array-nesting/description/) - -```html -Input: A = [5,4,0,3,1,6,2] -Output: 4 -Explanation: -A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2. - -One of the longest S[K]: -S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0} -``` - -题目描述:S[i] 表示一个集合,集合的第一个元素是 A[i],第二个元素是 A[A[i]],如此嵌套下去。求最大的 S[i]。 - -```java -public int arrayNesting(int[] nums) { - int max = 0; - for (int i = 0; i < nums.length; i++) { - int cnt = 0; - for (int j = i; nums[j] != -1; ) { - cnt++; - int t = nums[j]; - nums[j] = -1; // 标记该位置已经被访问 - j = t; - - } - max = Math.max(max, cnt); - } - return max; -} -``` - -**分隔数组** - -[769. Max Chunks To Make Sorted (Medium)](https://leetcode.com/problems/max-chunks-to-make-sorted/description/) - -```html -Input: arr = [1,0,2,3,4] -Output: 4 -Explanation: -We can split into two chunks, such as [1, 0], [2, 3, 4]. -However, splitting into [1, 0], [2], [3], [4] is the highest number of chunks possible. -``` - -题目描述:分隔数组,使得对每部分排序后数组就为有序。 - -```java -public int maxChunksToSorted(int[] arr) { - if (arr == null) return 0; - int ret = 0; - int right = arr[0]; - for (int i = 0; i < arr.length; i++) { - right = Math.max(right, arr[i]); - if (right == i) ret++; - } - return ret; -} -``` - -## 图 - -### 二分图 - -如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。 - -**判断是否为二分图** - -[785. Is Graph Bipartite? (Medium)](https://leetcode.com/problems/is-graph-bipartite/description/) - -```html -Input: [[1,3], [0,2], [1,3], [0,2]] -Output: true -Explanation: -The graph looks like this: -0----1 -| | -| | -3----2 -We can divide the vertices into two groups: {0, 2} and {1, 3}. -``` - -```html -Example 2: -Input: [[1,2,3], [0,2], [0,1,3], [0,2]] -Output: false -Explanation: -The graph looks like this: -0----1 -| \ | -| \ | -3----2 -We cannot find a way to divide the set of nodes into two independent subsets. -``` - -```java -public boolean isBipartite(int[][] graph) { - int[] colors = new int[graph.length]; - Arrays.fill(colors, -1); - for (int i = 0; i < graph.length; i++) { // 处理图不是连通的情况 - if (colors[i] == -1 && !isBipartite(i, 0, colors, graph)) { - return false; - } - } - return true; -} - -private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] graph) { - if (colors[curNode] != -1) { - return colors[curNode] == curColor; - } - colors[curNode] = curColor; - for (int nextNode : graph[curNode]) { - if (!isBipartite(nextNode, 1 - curColor, colors, graph)) { - return false; - } - } - return true; -} -``` - -### 拓扑排序 - -常用于在具有先序关系的任务规划中。 - -**课程安排的合法性** - -[207. Course Schedule (Medium)](https://leetcode.com/problems/course-schedule/description/) - -```html -2, [[1,0]] -return true -``` - -```html -2, [[1,0],[0,1]] -return false -``` - -题目描述:一个课程可能会先修课程,判断给定的先修课程规定是否合法。 - -本题不需要使用拓扑排序,只需要检测有向图是否存在环即可。 - -```java -public boolean canFinish(int numCourses, int[][] prerequisites) { - List[] graphic = new List[numCourses]; - for (int i = 0; i < numCourses; i++) { - graphic[i] = new ArrayList<>(); - } - for (int[] pre : prerequisites) { - graphic[pre[0]].add(pre[1]); - } - boolean[] globalMarked = new boolean[numCourses]; - boolean[] localMarked = new boolean[numCourses]; - for (int i = 0; i < numCourses; i++) { - if (hasCycle(globalMarked, localMarked, graphic, i)) { - return false; - } - } - return true; -} - -private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, - List[] graphic, int curNode) { - - if (localMarked[curNode]) { - return true; - } - if (globalMarked[curNode]) { - return false; - } - globalMarked[curNode] = true; - localMarked[curNode] = true; - for (int nextNode : graphic[curNode]) { - if (hasCycle(globalMarked, localMarked, graphic, nextNode)) { - return true; - } - } - localMarked[curNode] = false; - return false; -} -``` - -**课程安排的顺序** - -[210. Course Schedule II (Medium)](https://leetcode.com/problems/course-schedule-ii/description/) - -```html -4, [[1,0],[2,0],[3,1],[3,2]] -There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3]. -``` - -使用 DFS 来实现拓扑排序,使用一个栈存储后序遍历结果,这个栈的逆序结果就是拓扑排序结果。 - -证明:对于任何先序关系:v->w,后序遍历结果可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。 - -```java -public int[] findOrder(int numCourses, int[][] prerequisites) { - List[] graphic = new List[numCourses]; - for (int i = 0; i < numCourses; i++) { - graphic[i] = new ArrayList<>(); - } - for (int[] pre : prerequisites) { - graphic[pre[0]].add(pre[1]); - } - Stack postOrder = new Stack<>(); - boolean[] globalMarked = new boolean[numCourses]; - boolean[] localMarked = new boolean[numCourses]; - for (int i = 0; i < numCourses; i++) { - if (hasCycle(globalMarked, localMarked, graphic, i, postOrder)) { - return new int[0]; - } - } - int[] orders = new int[numCourses]; - for (int i = numCourses - 1; i >= 0; i--) { - orders[i] = postOrder.pop(); - } - return orders; -} - -private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List[] graphic, - int curNode, Stack postOrder) { - - if (localMarked[curNode]) { - return true; - } - if (globalMarked[curNode]) { - return false; - } - globalMarked[curNode] = true; - localMarked[curNode] = true; - for (int nextNode : graphic[curNode]) { - if (hasCycle(globalMarked, localMarked, graphic, nextNode, postOrder)) { - return true; - } - } - localMarked[curNode] = false; - postOrder.push(curNode); - return false; -} -``` - -### 并查集 - -并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。 - -**冗余连接** - -[684. Redundant Connection (Medium)](https://leetcode.com/problems/redundant-connection/description/) - -```html -Input: [[1,2], [1,3], [2,3]] -Output: [2,3] -Explanation: The given undirected graph will be like this: - 1 - / \ -2 - 3 -``` - -题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。 - -```java -public int[] findRedundantConnection(int[][] edges) { - int N = edges.length; - UF uf = new UF(N); - for (int[] e : edges) { - int u = e[0], v = e[1]; - if (uf.connect(u, v)) { - return e; - } - uf.union(u, v); - } - return new int[]{-1, -1}; -} - -private class UF { - - private int[] id; - - UF(int N) { - id = new int[N + 1]; - for (int i = 0; i < id.length; i++) { - id[i] = i; - } - } - - void union(int u, int v) { - int uID = find(u); - int vID = find(v); - if (uID == vID) { - return; - } - for (int i = 0; i < id.length; i++) { - if (id[i] == uID) { - id[i] = vID; - } - } - } - - int find(int p) { - return id[p]; - } - - boolean connect(int u, int v) { - return find(u) == find(v); - } -} -``` - -## 位运算 - -**1. 基本原理** - -0s 表示一串 0,1s 表示一串 1。 - -``` -x ^ 0s = x x & 0s = 0 x | 0s = x -x ^ 1s = ~x x & 1s = x x | 1s = 1s -x ^ x = 0 x & x = x x | x = x -``` - -- 利用 x ^ 1s = \~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。 -- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。 -- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。 - -位与运算技巧: - -- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 **100** ,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。 -- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110 **100** ,-n 得到 01001**100**,相与得到 00000**100**。 -- n-n&(\~n+1) 去除 n 的位级表示中最高的那一位。 - -移位运算: - -- \>\> n 为算术右移,相当于除以 2n; -- \>\>\> n 为无符号右移,左边会补上 0。 -- << n 为算术左移,相当于乘以 2n。 - -**2. mask 计算** - -要获取 111111111,将 0 取反即可,\~0。 - -要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。 - -要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。 - -要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 \~(1<<(i+1)-1)。 - -**3. Java 中的位操作** - -```html -static int Integer.bitCount(); // 统计 1 的数量 -static int Integer.highestOneBit(); // 获得最高位 -static String toBinaryString(int i); // 转换为二进制表示的字符串 -``` - -**统计两个数的二进制表示有多少位不同** - -[461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/) - -```html -Input: x = 1, y = 4 - -Output: 2 - -Explanation: -1 (0 0 0 1) -4 (0 1 0 0) - ↑ ↑ - -The above arrows point to positions where the corresponding bits are different. -``` - -对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。 - -```java -public int hammingDistance(int x, int y) { - int z = x ^ y; - int cnt = 0; - while(z != 0) { - if ((z & 1) == 1) cnt++; - z = z >> 1; - } - return cnt; -} -``` - -使用 z&(z-1) 去除 z 位级表示最低的那一位。 - -```java -public int hammingDistance(int x, int y) { - int z = x ^ y; - int cnt = 0; - while (z != 0) { - z &= (z - 1); - cnt++; - } - return cnt; -} -``` - -可以使用 Integer.bitcount() 来统计 1 个的个数。 - -```java -public int hammingDistance(int x, int y) { - return Integer.bitCount(x ^ y); -} -``` - -**数组中唯一一个不重复的元素** - -[136. Single Number (Easy)](https://leetcode.com/problems/single-number/description/) - -```html -Input: [4,1,2,1,2] -Output: 4 -``` - -两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。 - -```java -public int singleNumber(int[] nums) { - int ret = 0; - for (int n : nums) ret = ret ^ n; - return ret; -} -``` - -**找出数组中缺失的那个数** - -[268. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/) - -```html -Input: [3,0,1] -Output: 2 -``` - -题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。 - -```java -public int missingNumber(int[] nums) { - int ret = 0; - for (int i = 0; i < nums.length; i++) { - ret = ret ^ i ^ nums[i]; - } - return ret ^ nums.length; -} -``` - -**数组中不重复的两个元素** - -[260. Single Number III (Medium)](https://leetcode.com/problems/single-number-iii/description/) - -两个不相等的元素在位级表示上必定会有一位存在不同。 - -将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 - -diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 - -```java -public int[] singleNumber(int[] nums) { - int diff = 0; - for (int num : nums) diff ^= num; - diff &= -diff; // 得到最右一位 - int[] ret = new int[2]; - for (int num : nums) { - if ((num & diff) == 0) ret[0] ^= num; - else ret[1] ^= num; - } - return ret; -} -``` - -**翻转一个数的比特位** - -[190. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/) - -```java -public int reverseBits(int n) { - int ret = 0; - for (int i = 0; i < 32; i++) { - ret <<= 1; - ret |= (n & 1); - n >>>= 1; - } - return ret; -} -``` - -如果该函数需要被调用很多次,可以将 int 拆成 4 个 byte,然后缓存 byte 对应的比特位翻转,最后再拼接起来。 - -```java -private static Map cache = new HashMap<>(); - -public int reverseBits(int n) { - int ret = 0; - for (int i = 0; i < 4; i++) { - ret <<= 8; - ret |= reverseByte((byte) (n & 0b11111111)); - n >>= 8; - } - return ret; -} - -private int reverseByte(byte b) { - if (cache.containsKey(b)) return cache.get(b); - int ret = 0; - byte t = b; - for (int i = 0; i < 8; i++) { - ret <<= 1; - ret |= t & 1; - t >>= 1; - } - cache.put(b, ret); - return ret; -} -``` - -**不用额外变量交换两个整数** - -[程序员代码面试指南 :P317](#) - -```java -a = a ^ b; -b = a ^ b; -a = a ^ b; -``` - -**判断一个数是不是 2 的 n 次方** - -[231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/) - -二进制表示只有一个 1 存在。 - -```java -public boolean isPowerOfTwo(int n) { - return n > 0 && Integer.bitCount(n) == 1; -} -``` - -利用 1000 & 0111 == 0 这种性质,得到以下解法: - -```java -public boolean isPowerOfTwo(int n) { - return n > 0 && (n & (n - 1)) == 0; -} -``` - -**判断一个数是不是 4 的 n 次方** - -[342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/) - -这种数在二进制表示中有且只有一个奇数位为 1,例如 16(10000)。 - -```java -public boolean isPowerOfFour(int num) { - return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0; -} -``` - -也可以使用正则表达式进行匹配。 - -```java -public boolean isPowerOfFour(int num) { - return Integer.toString(num, 4).matches("10*"); -} -``` - -**判断一个数的位级表示是否不会出现连续的 0 和 1** - -[693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) - -```html -Input: 10 -Output: True -Explanation: -The binary representation of 10 is: 1010. - -Input: 11 -Output: False -Explanation: -The binary representation of 11 is: 1011. -``` - -对于 1010 这种位级表示的数,把它向右移动 1 位得到 101,这两个数每个位都不同,因此异或得到的结果为 1111。 - -```java -public boolean hasAlternatingBits(int n) { - int a = (n ^ (n >> 1)); - return (a & (a + 1)) == 0; -} -``` - -**求一个数的补码** - -[476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/) - -```html -Input: 5 -Output: 2 -Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010. So you need to output 2. -``` - -题目描述:不考虑二进制表示中的首 0 部分。 - -对于 00000101,要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。 - -```java -public int findComplement(int num) { - if (num == 0) return 1; - int mask = 1 << 30; - while ((num & mask) == 0) mask >>= 1; - mask = (mask << 1) - 1; - return num ^ mask; -} -``` - -可以利用 Java 的 Integer.highestOneBit() 方法来获得含有首 1 的数。 - -```java -public int findComplement(int num) { - if (num == 0) return 1; - int mask = Integer.highestOneBit(num); - mask = (mask << 1) - 1; - return num ^ mask; -} -``` - -对于 10000000 这样的数要扩展成 11111111,可以利用以下方法: - -```html -mask |= mask >> 1 11000000 -mask |= mask >> 2 11110000 -mask |= mask >> 4 11111111 -``` - -```java -public int findComplement(int num) { - int mask = num; - mask |= mask >> 1; - mask |= mask >> 2; - mask |= mask >> 4; - mask |= mask >> 8; - mask |= mask >> 16; - return (mask ^ num); -} -``` - -**实现整数的加法** - -[371. Sum of Two Integers (Easy)](https://leetcode.com/problems/sum-of-two-integers/description/) - -a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。 - -递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 - -```java -public int getSum(int a, int b) { - return b == 0 ? a : getSum((a ^ b), (a & b) << 1); -} -``` - -**字符串数组最大乘积** - -[318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) - -```html -Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"] -Return 16 -The two words can be "abcw", "xtfn". -``` - -题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。 - -本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。 - -```java -public int maxProduct(String[] words) { - int n = words.length; - int[] val = new int[n]; - for (int i = 0; i < n; i++) { - for (char c : words[i].toCharArray()) { - val[i] |= 1 << (c - 'a'); - } - } - int ret = 0; - for (int i = 0; i < n; i++) { - for (int j = i + 1; j < n; j++) { - if ((val[i] & val[j]) == 0) { - ret = Math.max(ret, words[i].length() * words[j].length()); - } - } - } - return ret; -} -``` - -**统计从 0 \~ n 每个数的二进制表示中 1 的个数** - -[338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/) - -对于数字 6(110),它可以看成是 4(100) 再加一个 2(10),因此 dp[i] = dp[i&(i-1)] + 1; - -```java -public int[] countBits(int num) { - int[] ret = new int[num + 1]; - for(int i = 1; i <= num; i++){ - ret[i] = ret[i&(i-1)] + 1; - } - return ret; -} -``` - -# 参考资料 - -- [Leetcode](https://leetcode.com/problemset/algorithms/?status=Todo) -- Weiss M A, 冯舜玺. 数据结构与算法分析——C 语言描述[J]. 2004. -- Sedgewick R. Algorithms[M]. Pearson Education India, 1988. -- 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014. -- 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008. -- 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015. diff --git a/docs/notes/Leetcode-Database 题解.md b/docs/notes/Leetcode-Database 题解.md index 629aba39..4fcf7ff7 100644 --- a/docs/notes/Leetcode-Database 题解.md +++ b/docs/notes/Leetcode-Database 题解.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [595. Big Countries](#595-big-countries) * [627. Swap Salary](#627-swap-salary) @@ -949,3 +948,9 @@ WHERE ORDER BY id; ``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Linux.md b/docs/notes/Linux.md index 061bedbb..ee595b55 100644 --- a/docs/notes/Linux.md +++ b/docs/notes/Linux.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、常用操作以及概念](#一常用操作以及概念) * [快捷键](#快捷键) @@ -1246,3 +1245,9 @@ options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 w - [File system design case studies](https://www.cs.rutgers.edu/\~pxk/416/notes/13-fs-studies.html) - [Programming Project #4](https://classes.soe.ucsc.edu/cmps111/Fall08/proj4.shtml) - [FILE SYSTEM DESIGN](http://web.cs.ucla.edu/classes/fall14/cs111/scribe/11a/index.html) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/MySQL.md b/docs/notes/MySQL.md index 329e4ecb..ef17c89d 100644 --- a/docs/notes/MySQL.md +++ b/docs/notes/MySQL.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、索引](#一索引) * [B+ Tree 原理](#b-tree-原理) @@ -422,3 +421,9 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 - [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) - [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html) - [B + 树](https://zh.wikipedia.org/wiki/B%2B%E6%A0%91) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Redis.md b/docs/notes/Redis.md index 4291f6a8..67837c43 100644 --- a/docs/notes/Redis.md +++ b/docs/notes/Redis.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [二、数据类型](#二数据类型) @@ -607,3 +606,9 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据 - [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide) - [Redis 应用场景](http://www.scienjus.com/redis-use-case/) - [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/SQL.md b/docs/notes/SQL.md index 2ac1b071..8471bb8e 100644 --- a/docs/notes/SQL.md +++ b/docs/notes/SQL.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、基础](#一基础) * [二、创建表](#二创建表) @@ -766,3 +765,9 @@ SET PASSWROD FOR myuser = Password('new_password'); # 参考资料 - BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/Socket.md b/docs/notes/Socket.md index f30b5b1f..901ef13d 100644 --- a/docs/notes/Socket.md +++ b/docs/notes/Socket.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、I/O 模型](#一io-模型) * [阻塞式 I/O](#阻塞式-io) @@ -322,3 +321,9 @@ poll 没有最大描述符数量的限制,如果平台支持并且对实时性 - [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html) - [select / poll / epoll: practical difference for system architects](http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/) - [Browse the source code of userspace/glibc/sysdeps/unix/sysv/linux/ online](https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/pics/067b310c-6877-40fe-9dcf-10654e737485.jpg b/docs/notes/pics/067b310c-6877-40fe-9dcf-10654e737485.jpg new file mode 100644 index 00000000..c07bd924 Binary files /dev/null and b/docs/notes/pics/067b310c-6877-40fe-9dcf-10654e737485.jpg differ diff --git a/docs/notes/pics/0e8fdc96-83c1-4798-9abe-45fc91d70b9d.png b/docs/notes/pics/0e8fdc96-83c1-4798-9abe-45fc91d70b9d.png new file mode 100644 index 00000000..d8df24d8 Binary files /dev/null and b/docs/notes/pics/0e8fdc96-83c1-4798-9abe-45fc91d70b9d.png differ diff --git a/docs/notes/pics/14fe1e71-8518-458f-a220-116003061a83.png b/docs/notes/pics/14fe1e71-8518-458f-a220-116003061a83.png new file mode 100644 index 00000000..ec381029 Binary files /dev/null and b/docs/notes/pics/14fe1e71-8518-458f-a220-116003061a83.png differ diff --git a/docs/notes/pics/2de794ca-aa7b-48f3-a556-a0e2708cb976.jpg b/docs/notes/pics/2de794ca-aa7b-48f3-a556-a0e2708cb976.jpg new file mode 100644 index 00000000..5398511f Binary files /dev/null and b/docs/notes/pics/2de794ca-aa7b-48f3-a556-a0e2708cb976.jpg differ diff --git a/docs/notes/pics/49e53613-46f8-4308-9ee5-c09d6231552088893397.png b/docs/notes/pics/49e53613-46f8-4308-9ee5-c09d6231552088893397.png new file mode 100644 index 00000000..e853dc13 Binary files /dev/null and b/docs/notes/pics/49e53613-46f8-4308-9ee5-c09d6231552088893397.png differ diff --git a/docs/notes/pics/6646db4a-7f43-45e4-96ff-0891a57a9ade.jpg b/docs/notes/pics/6646db4a-7f43-45e4-96ff-0891a57a9ade.jpg new file mode 100644 index 00000000..693d7b97 Binary files /dev/null and b/docs/notes/pics/6646db4a-7f43-45e4-96ff-0891a57a9ade.jpg differ diff --git a/docs/notes/pics/6c0cf1e8-b03f-4eff-9b1a-ab262e0c7866.png b/docs/notes/pics/6c0cf1e8-b03f-4eff-9b1a-ab262e0c7866.png new file mode 100644 index 00000000..5b9202f6 Binary files /dev/null and b/docs/notes/pics/6c0cf1e8-b03f-4eff-9b1a-ab262e0c7866.png differ diff --git a/docs/notes/pics/74dc31eb-6baa-47ea-ab1c-d27a0ca35093.png b/docs/notes/pics/74dc31eb-6baa-47ea-ab1c-d27a0ca35093.png new file mode 100644 index 00000000..837f995a Binary files /dev/null and b/docs/notes/pics/74dc31eb-6baa-47ea-ab1c-d27a0ca35093.png differ diff --git a/docs/notes/pics/79e9a938-43e2-4c5a-8de9-fe55522a14c9.png b/docs/notes/pics/79e9a938-43e2-4c5a-8de9-fe55522a14c9.png new file mode 100644 index 00000000..4938fc58 Binary files /dev/null and b/docs/notes/pics/79e9a938-43e2-4c5a-8de9-fe55522a14c9.png differ diff --git a/docs/notes/pics/879814ee-48b5-4bcb-86f5-dcc400cb81ad.png b/docs/notes/pics/879814ee-48b5-4bcb-86f5-dcc400cb81ad.png new file mode 100644 index 00000000..14d98b8c Binary files /dev/null and b/docs/notes/pics/879814ee-48b5-4bcb-86f5-dcc400cb81ad.png differ diff --git a/docs/notes/pics/8cb2be66-3d47-41ba-b55b-319fc68940d4.png b/docs/notes/pics/8cb2be66-3d47-41ba-b55b-319fc68940d4.png new file mode 100644 index 00000000..c90982c2 Binary files /dev/null and b/docs/notes/pics/8cb2be66-3d47-41ba-b55b-319fc68940d4.png differ diff --git a/docs/notes/pics/95903878-725b-4ed9-bded-bc4aae0792a9.jpg b/docs/notes/pics/95903878-725b-4ed9-bded-bc4aae0792a9.jpg new file mode 100644 index 00000000..da95c5d8 Binary files /dev/null and b/docs/notes/pics/95903878-725b-4ed9-bded-bc4aae0792a9.jpg differ diff --git a/docs/notes/pics/9823768c-212b-4b1a-b69a-b3f59e07b977.jpg b/docs/notes/pics/9823768c-212b-4b1a-b69a-b3f59e07b977.jpg new file mode 100644 index 00000000..8b907d61 Binary files /dev/null and b/docs/notes/pics/9823768c-212b-4b1a-b69a-b3f59e07b977.jpg differ diff --git a/docs/notes/pics/9ae89f16-7905-4a6f-88a2-874b4cac91f4.jpg b/docs/notes/pics/9ae89f16-7905-4a6f-88a2-874b4cac91f4.jpg new file mode 100644 index 00000000..77c54780 Binary files /dev/null and b/docs/notes/pics/9ae89f16-7905-4a6f-88a2-874b4cac91f4.jpg differ diff --git a/docs/notes/pics/c847d6e4-3610-4f3c-a909-89a5048426e6.png b/docs/notes/pics/c847d6e4-3610-4f3c-a909-89a5048426e6.png new file mode 100644 index 00000000..167c33bc Binary files /dev/null and b/docs/notes/pics/c847d6e4-3610-4f3c-a909-89a5048426e6.png differ diff --git a/docs/notes/pics/da1f96b9-fd4d-44ca-8925-fb14c5733388.png b/docs/notes/pics/da1f96b9-fd4d-44ca-8925-fb14c5733388.png new file mode 100644 index 00000000..fdca0324 Binary files /dev/null and b/docs/notes/pics/da1f96b9-fd4d-44ca-8925-fb14c5733388.png differ diff --git a/docs/notes/pics/dc82f0f3-c1d4-4ac8-90ac-d5b32a9bd75a.jpg b/docs/notes/pics/dc82f0f3-c1d4-4ac8-90ac-d5b32a9bd75a.jpg new file mode 100644 index 00000000..0be8743c Binary files /dev/null and b/docs/notes/pics/dc82f0f3-c1d4-4ac8-90ac-d5b32a9bd75a.jpg differ diff --git a/docs/notes/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ff1552090620367.png b/docs/notes/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ff1552090620367.png new file mode 100644 index 00000000..25ed7497 Binary files /dev/null and b/docs/notes/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ff1552090620367.png differ diff --git a/docs/notes/pics/ee994da4-0fc7-443d-ac56-c08caf00a204.jpg b/docs/notes/pics/ee994da4-0fc7-443d-ac56-c08caf00a204.jpg new file mode 100644 index 00000000..732e73e0 Binary files /dev/null and b/docs/notes/pics/ee994da4-0fc7-443d-ac56-c08caf00a204.jpg differ diff --git a/docs/notes/pics/f1ff65ed-bbc2-4b92-8a94-7c5c0874da0f.jpg b/docs/notes/pics/f1ff65ed-bbc2-4b92-8a94-7c5c0874da0f.jpg new file mode 100644 index 00000000..4308e4c9 Binary files /dev/null and b/docs/notes/pics/f1ff65ed-bbc2-4b92-8a94-7c5c0874da0f.jpg differ diff --git a/docs/notes/代码可读性.md b/docs/notes/代码可读性.md index cae693f7..724770a9 100644 --- a/docs/notes/代码可读性.md +++ b/docs/notes/代码可读性.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、可读性的重要性](#一可读性的重要性) * [二、用名字表达代码含义](#二用名字表达代码含义) @@ -332,3 +331,9 @@ public int findClostElement(int[] arr) { # 参考资料 - Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/代码风格规范.md b/docs/notes/代码风格规范.md index c94a413b..948c672c 100644 --- a/docs/notes/代码风格规范.md +++ b/docs/notes/代码风格规范.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) @@ -6,3 +5,9 @@ - [Twitter Java Style Guide](https://github.com/twitter/commons/blob/master/src/java/com/twitter/common/styleguide.md) - [Google Java Style Guide](http://google.github.io/styleguide/javaguide.html) - [阿里巴巴Java开发手册](https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E8%AF%A6%E5%B0%BD%E7%89%88%EF%BC%89.pdf) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/分布式.md b/docs/notes/分布式.md index c9673335..e8b81360 100644 --- a/docs/notes/分布式.md +++ b/docs/notes/分布式.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、分布式锁](#一分布式锁) * [数据库的唯一索引](#数据库的唯一索引) @@ -342,3 +341,9 @@ Raft 也是分布式一致性协议,主要是用来竞选主节点。 - [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/) - [Paxos By Example](https://angus.nyc/2012/paxos-by-example/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 10~19.md b/docs/notes/剑指 Offer 题解 - 10~19.md new file mode 100644 index 00000000..581af3b4 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 10~19.md @@ -0,0 +1,691 @@ + +* [10.1 斐波那契数列](#101-斐波那契数列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [10.2 矩形覆盖](#102-矩形覆盖) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [10.3 跳台阶](#103-跳台阶) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [10.4 变态跳台阶](#104-变态跳台阶) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [动态规划](#动态规划) + * [数学推导](#数学推导) +* [11. 旋转数组的最小数字](#11-旋转数组的最小数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [12. 矩阵中的路径](#12-矩阵中的路径) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [13. 机器人的运动范围](#13-机器人的运动范围) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [14. 剪绳子](#14-剪绳子) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [贪心](#贪心) + * [动态规划](#动态规划) +* [15. 二进制中 1 的个数](#15-二进制中-1-的个数) + * [题目描述](#题目描述) + * [n&(n-1)](#n&n-1) + * [Integer.bitCount()](#integerbitcount) +* [16. 数值的整数次方](#16-数值的整数次方) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [17. 打印从 1 到最大的 n 位数](#17-打印从-1-到最大的-n-位数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [18.1 在 O(1) 时间内删除链表节点](#181-在-o1-时间内删除链表节点) + * [解题思路](#解题思路) +* [18.2 删除链表中重复的结点](#182-删除链表中重复的结点) + * [题目描述](#题目描述) + * [解题描述](#解题描述) +* [19. 正则表达式匹配](#19-正则表达式匹配) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 10.1 斐波那契数列 + +[NowCoder](https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=13&tqId=11160&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +求斐波那契数列的第 n 项,n <= 39。 + + + +

+ +## 解题思路 + +如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。 + +

+ + +递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。 + +```java +public int Fibonacci(int n) { + if (n <= 1) + return n; + int[] fib = new int[n + 1]; + fib[1] = 1; + for (int i = 2; i <= n; i++) + fib[i] = fib[i - 1] + fib[i - 2]; + return fib[n]; +} +``` + +考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。 + +```java +public int Fibonacci(int n) { + if (n <= 1) + return n; + int pre2 = 0, pre1 = 1; + int fib = 0; + for (int i = 2; i <= n; i++) { + fib = pre2 + pre1; + pre2 = pre1; + pre1 = fib; + } + return fib; +} +``` + +由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。 + +```java +public class Solution { + + private int[] fib = new int[40]; + + public Solution() { + fib[1] = 1; + fib[2] = 2; + for (int i = 2; i < fib.length; i++) + fib[i] = fib[i - 1] + fib[i - 2]; + } + + public int Fibonacci(int n) { + return fib[n]; + } +} +``` + +# 10.2 矩形覆盖 + +[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.3 跳台阶 + +[NowCoder](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +

+ +## 解题思路 + +```java +public int JumpFloor(int n) { + if (n <= 2) + return n; + int pre2 = 1, pre1 = 2; + int result = 1; + for (int i = 2; 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) + +## 题目描述 + +一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 + +

+ +## 解题思路 + +### 动态规划 + +```java +public int JumpFloorII(int target) { + int[] dp = new int[target]; + Arrays.fill(dp, 1); + for (int i = 1; i < target; i++) + for (int j = 0; j < i; j++) + dp[i] += dp[j]; + return dp[target - 1]; +} +``` + +### 数学推导 + +跳上 n-1 级台阶,可以从 n-2 级跳 1 级上去,也可以从 n-3 级跳 2 级上去...,那么 + +``` +f(n-1) = f(n-2) + f(n-3) + ... + f(0) +``` + +同样,跳上 n 级台阶,可以从 n-1 级跳 1 级上去,也可以从 n-2 级跳 2 级上去... ,那么 + +``` +f(n) = f(n-1) + f(n-2) + ... + f(0) +``` + +综上可得 + +``` +f(n) - f(n-1) = f(n-1) +``` + +即 + +``` +f(n) = 2*f(n-1) +``` + +所以 f(n) 是一个等比数列 + +```source-java +public int JumpFloorII(int target) { + return (int) Math.pow(2, target - 1); +} +``` + + +# 11. 旋转数组的最小数字 + +[NowCoder](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 + +例如数组 {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。 + +```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],那么此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。 + +```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[l] == nums[m] && nums[m] == nums[h]) + return minNumber(nums, l, h); + else if (nums[m] <= nums[h]) + h = m; + else + l = m + 1; + } + return nums[l]; +} + +private int minNumber(int[] nums, int l, int h) { + for (int i = l; i < h; i++) + if (nums[i] > nums[i + 1]) + return nums[i + 1]; + return nums[l]; +} +``` + +# 12. 矩阵中的路径 + +[NowCoder](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 + +例如下面的矩阵包含了一条 bfce 路径。 + +

+ +## 解题思路 + +```java +private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; +private int rows; +private int cols; + +public boolean hasPath(char[] array, int rows, int cols, char[] str) { + if (rows == 0 || cols == 0) + return false; + this.rows = rows; + this.cols = cols; + boolean[][] marked = new boolean[rows][cols]; + char[][] matrix = buildMatrix(array); + for (int i = 0; i < rows; i++) + for (int j = 0; j < cols; j++) + if (backtracking(matrix, str, marked, 0, i, j)) + return true; + return false; +} + +private boolean backtracking(char[][] matrix, char[] str, boolean[][] marked, int pathLen, int r, int c) { + if (pathLen == str.length) + return true; + if (r < 0 || r >= rows || c < 0 || c >= cols || matrix[r][c] != str[pathLen] || marked[r][c]) + return false; + marked[r][c] = true; + for (int[] n : next) + if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1])) + return true; + marked[r][c] = false; + return false; +} + +private char[][] buildMatrix(char[] array) { + char[][] matrix = new char[rows][cols]; + for (int i = 0, idx = 0; i < rows; i++) + for (int j = 0; j < cols; j++) + matrix[i][j] = array[idx++]; + return matrix; +} +``` + +# 13. 机器人的运动范围 + +[NowCoder](https://www.nowcoder.com/practice/6e5207314b5241fb83f2329e89fdecc8?tpId=13&tqId=11219&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。 + +例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? + +## 解题思路 + +```java +private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; +private int cnt = 0; +private int rows; +private int cols; +private int threshold; +private int[][] digitSum; + +public int movingCount(int threshold, int rows, int cols) { + this.rows = rows; + this.cols = cols; + this.threshold = threshold; + initDigitSum(); + boolean[][] marked = new boolean[rows][cols]; + dfs(marked, 0, 0); + return cnt; +} + +private void dfs(boolean[][] marked, int r, int c) { + if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c]) + return; + marked[r][c] = true; + if (this.digitSum[r][c] > this.threshold) + return; + cnt++; + for (int[] n : next) + dfs(marked, r + n[0], c + n[1]); +} + +private void initDigitSum() { + int[] digitSumOne = new int[Math.max(rows, cols)]; + for (int i = 0; i < digitSumOne.length; i++) { + int n = i; + while (n > 0) { + digitSumOne[i] += n % 10; + n /= 10; + } + } + this.digitSum = new int[rows][cols]; + for (int i = 0; i < this.rows; i++) + for (int j = 0; j < this.cols; j++) + this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j]; +} +``` + +# 14. 剪绳子 + +[Leetcode](https://leetcode.com/problems/integer-break/description/) + +## 题目描述 + +把一根绳子剪成多段,并且使得每段的长度乘积最大。 + +```html +n = 2 +return 1 (2 = 1 + 1) + +n = 10 +return 36 (10 = 3 + 3 + 4) +``` + +## 解题思路 + +### 贪心 + +尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 + +证明:当 n >= 5 时,3(n - 3) - n = 2n - 9 > 0,且 2(n - 2) - n = n - 4 > 0。因此在 n >= 5 的情况下,将绳子剪成一段为 2 或者 3,得到的乘积会更大。又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0,所以剪成一段长度为 3 比长度为 2 得到的乘积更大。 + +```java +public int integerBreak(int n) { + if (n < 2) + return 0; + if (n == 2) + return 1; + if (n == 3) + return 2; + int timesOf3 = n / 3; + if (n - timesOf3 * 3 == 1) + timesOf3--; + int timesOf2 = (n - timesOf3 * 3) / 2; + return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2)); +} +``` + +### 动态规划 + +```java +public int integerBreak(int n) { + int[] dp = new int[n + 1]; + dp[1] = 1; + for (int i = 2; i <= n; i++) + for (int j = 1; j < i; j++) + dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); + return dp[n]; +} +``` + +# 15. 二进制中 1 的个数 + +[NowCoder](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个整数,输出该数二进制表示中 1 的个数。 + +### n&(n-1) + +该位运算去除 n 的位级表示中最低的那一位。 + +``` +n : 10110100 +n-1 : 10110011 +n&(n-1) : 10110000 +``` + +时间复杂度:O(M),其中 M 表示 1 的个数。 + + +```java +public int NumberOf1(int n) { + int cnt = 0; + while (n != 0) { + cnt++; + n &= (n - 1); + } + return cnt; +} +``` + + +### 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 次方。 + +## 解题思路 + +下面的讨论中 x 代表 base,n 代表 exponent。 + + + +

+ +因为 (x\*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 + +```java +public double Power(double base, int exponent) { + if (exponent == 0) + return 1; + if (exponent == 1) + return base; + boolean isNegative = false; + if (exponent < 0) { + exponent = -exponent; + isNegative = true; + } + double pow = Power(base * base, exponent / 2); + if (exponent % 2 != 0) + pow = pow * base; + return isNegative ? 1 / pow : pow; +} +``` + +# 17. 打印从 1 到最大的 n 位数 + +## 题目描述 + +输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。 + +## 解题思路 + +由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。 + +使用回溯法得到所有的数。 + +```java +public void print1ToMaxOfNDigits(int n) { + if (n <= 0) + return; + char[] number = new char[n]; + print1ToMaxOfNDigits(number, 0); +} + +private void print1ToMaxOfNDigits(char[] number, int digit) { + if (digit == number.length) { + printNumber(number); + return; + } + for (int i = 0; i < 10; i++) { + number[digit] = (char) (i + '0'); + print1ToMaxOfNDigits(number, digit + 1); + } +} + +private void printNumber(char[] number) { + int index = 0; + while (index < number.length && number[index] == '0') + index++; + while (index < number.length) + System.out.print(number[index++]); + System.out.println(); +} +``` + +# 18.1 在 O(1) 时间内删除链表节点 + +## 解题思路 + +① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。 + +

+ +② 如果链表只有一个节点,那么直接 + +② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。 + +

+ +综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N \~ 2,因此该算法的平均时间复杂度为 O(1)。 + +```java +public ListNode deleteNode(ListNode head, ListNode tobeDelete) { + if (head == null || tobeDelete == null) + return null; + if (tobeDelete.next != null) { + // 要删除的节点不是尾节点 + ListNode next = tobeDelete.next; + tobeDelete.val = next.val; + tobeDelete.next = next.next; + } else { + if (head == tobeDelete) + // 只有一个节点 + head = null; + else { + ListNode cur = head; + while (cur.next != tobeDelete) + cur = cur.next; + cur.next = null; + } + } + return head; +} +``` + +# 18.2 删除链表中重复的结点 + +[NowCoder](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +

+ +## 解题描述 + +```java +public ListNode deleteDuplication(ListNode pHead) { + if (pHead == null || pHead.next == null) + return pHead; + ListNode next = pHead.next; + if (pHead.val == next.val) { + while (next != null && pHead.val == next.val) + next = next.next; + return deleteDuplication(next); + } else { + pHead.next = deleteDuplication(pHead.next); + return pHead; + } +} +``` + +# 19. 正则表达式匹配 + +[NowCoder](https://www.nowcoder.com/practice/45327ae22b7b413ea21df13ee7d6429c?tpId=13&tqId=11205&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。 + +在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配。 + +## 解题思路 + +应该注意到,'.' 是用来当做一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而把它当成重复前面字符一次。 + +```java +public boolean match(char[] str, char[] pattern) { + + int m = str.length, n = pattern.length; + boolean[][] dp = new boolean[m + 1][n + 1]; + + dp[0][0] = true; + for (int i = 1; i <= n; i++) + if (pattern[i - 1] == '*') + dp[0][i] = dp[0][i - 2]; + + for (int i = 1; i <= m; i++) + for (int j = 1; j <= n; j++) + if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.') + dp[i][j] = dp[i - 1][j - 1]; + else if (pattern[j - 1] == '*') + if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.') { + dp[i][j] |= dp[i][j - 1]; // a* counts as single a + dp[i][j] |= dp[i - 1][j]; // a* counts as multiple a + dp[i][j] |= dp[i][j - 2]; // a* counts as empty + } else + dp[i][j] = dp[i][j - 2]; // a* only counts as empty + + return dp[m][n]; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 20~29.md b/docs/notes/剑指 Offer 题解 - 20~29.md new file mode 100644 index 00000000..9e1d7cbc --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 20~29.md @@ -0,0 +1,384 @@ + +* [20. 表示数值的字符串](#20-表示数值的字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [21. 调整数组顺序使奇数位于偶数前面](#21-调整数组顺序使奇数位于偶数前面) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [22. 链表中倒数第 K 个结点](#22-链表中倒数第-k-个结点) + * [解题思路](#解题思路) +* [23. 链表中环的入口结点](#23-链表中环的入口结点) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [24. 反转链表](#24-反转链表) + * [解题思路](#解题思路) + * [递归](#递归) + * [迭代](#迭代) +* [25. 合并两个排序的链表](#25-合并两个排序的链表) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [递归](#递归) + * [迭代](#迭代) +* [26. 树的子结构](#26-树的子结构) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [27. 二叉树的镜像](#27-二叉树的镜像) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [28 对称的二叉树](#28-对称的二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [29. 顺时针打印矩阵](#29-顺时针打印矩阵) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 20. 表示数值的字符串 + +[NowCoder](https://www.nowcoder.com/practice/6f8c901d091949a5837e24bb82a731f2?tpId=13&tqId=11206&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +```html +true + +"+100" +"5e2" +"-123" +"3.1416" +"-1E-16" + +false + +"12e" +"1a3.14" +"1.2.3" +"+-5" +"12e+4.3" +``` + + +## 解题思路 + +使用正则表达式进行匹配。 + +```html +[] : 字符集合 +() : 分组 +? : 重复 0 ~ 1 ++ : 重复 1 ~ n +* : 重复 0 ~ n +. : 任意字符 +\\. : 转义后的 . +\\d : 数字 +``` + +```java +public boolean isNumeric(char[] str) { + if (str == null || str.length == 0) + return false; + return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); +} +``` + +# 21. 调整数组顺序使奇数位于偶数前面 + +[NowCoder](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 + +

+ +## 解题思路 + +```java +public void reOrderArray(int[] nums) { + // 奇数个数 + int oddCnt = 0; + for (int val : nums) + if (val % 2 == 1) + oddCnt++; + int[] copy = nums.clone(); + int i = 0, j = oddCnt; + for (int num : copy) { + if (num % 2 == 1) + nums[i++] = num; + else + nums[j++] = num; + } +} +``` + +# 22. 链表中倒数第 K 个结点 + +[NowCoder](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 解题思路 + +设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。 + +

+ +```java +public ListNode FindKthToTail(ListNode head, int k) { + if (head == null) + return null; + ListNode P1 = head; + while (P1 != null && k-- > 0) + P1 = P1.next; + if (k > 0) + return null; + ListNode P2 = head; + while (P1 != null) { + P1 = P1.next; + P2 = P2.next; + } + return P2; +} +``` + +# 23. 链表中环的入口结点 + +[NowCoder](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。 + +## 解题思路 + +使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。 + +在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。 + +

+ + +```java +public ListNode EntryNodeOfLoop(ListNode pHead) { + if (pHead == null || pHead.next == null) + return null; + ListNode slow = pHead, fast = pHead; + do { + fast = fast.next.next; + slow = slow.next; + } while (slow != fast); + fast = pHead; + while (slow != fast) { + slow = slow.next; + fast = fast.next; + } + return slow; +} +``` + +# 24. 反转链表 + +[NowCoder](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 解题思路 + +### 递归 + +```java +public ListNode ReverseList(ListNode head) { + if (head == null || head.next == null) + return head; + ListNode next = head.next; + head.next = null; + ListNode newHead = ReverseList(next); + next.next = head; + return newHead; +} +``` + +### 迭代 + +```java +public ListNode ReverseList(ListNode head) { + ListNode newList = new ListNode(-1); + while (head != null) { + ListNode next = head.next; + head.next = newList.next; + newList.next = head; + head = next; + } + return newList.next; +} +``` + +# 25. 合并两个排序的链表 + +[NowCoder](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +

+ +## 解题思路 + +### 递归 + +```java +public ListNode Merge(ListNode list1, ListNode list2) { + if (list1 == null) + return list2; + if (list2 == null) + return list1; + if (list1.val <= list2.val) { + list1.next = Merge(list1.next, list2); + return list1; + } else { + list2.next = Merge(list1, list2.next); + return list2; + } +} +``` + +### 迭代 + +```java +public ListNode Merge(ListNode list1, ListNode list2) { + ListNode head = new ListNode(-1); + ListNode cur = head; + while (list1 != null && list2 != null) { + if (list1.val <= list2.val) { + cur.next = list1; + list1 = list1.next; + } else { + cur.next = list2; + list2 = list2.next; + } + cur = cur.next; + } + if (list1 != null) + cur.next = list1; + if (list2 != null) + cur.next = list2; + return head.next; +} +``` + +# 26. 树的子结构 + +[NowCoder](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +

+ +## 解题思路 + +```java +public boolean HasSubtree(TreeNode root1, TreeNode root2) { + if (root1 == null || root2 == null) + return false; + return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2); +} + +private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) { + if (root2 == null) + return true; + if (root1 == null) + return false; + if (root1.val != root2.val) + return false; + return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right); +} +``` + +# 27. 二叉树的镜像 + +[NowCoder](https://www.nowcoder.com/practice/564f4c26aa584921bc75623e48ca3011?tpId=13&tqId=11171&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +

+ +## 解题思路 + +```java +public void Mirror(TreeNode root) { + if (root == null) + return; + swap(root); + Mirror(root.left); + Mirror(root.right); +} + +private void swap(TreeNode root) { + TreeNode t = root.left; + root.left = root.right; + root.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) + +## 题目描述 + +

+ +## 解题思路 + +```java +boolean isSymmetrical(TreeNode pRoot) { + if (pRoot == null) + return true; + return isSymmetrical(pRoot.left, pRoot.right); +} + +boolean isSymmetrical(TreeNode t1, TreeNode t2) { + if (t1 == null && t2 == null) + return true; + if (t1 == null || t2 == null) + return false; + if (t1.val != t2.val) + return false; + return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left); +} +``` + +# 29. 顺时针打印矩阵 + +[NowCoder](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 + +

+ +## 解题思路 + +```java +public ArrayList printMatrix(int[][] matrix) { + ArrayList ret = new ArrayList<>(); + int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1; + while (r1 <= r2 && c1 <= c2) { + for (int i = c1; i <= c2; i++) + ret.add(matrix[r1][i]); + for (int i = r1 + 1; i <= r2; i++) + ret.add(matrix[i][c2]); + if (r1 != r2) + for (int i = c2 - 1; i >= c1; i--) + ret.add(matrix[r2][i]); + if (c1 != c2) + for (int i = r2 - 1; i > r1; i--) + ret.add(matrix[i][c1]); + r1++; r2--; c1++; c2--; + } + return ret; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 30~39.md b/docs/notes/剑指 Offer 题解 - 30~39.md new file mode 100644 index 00000000..b0aaefbf --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 30~39.md @@ -0,0 +1,499 @@ + +* [30. 包含 min 函数的栈](#30-包含-min-函数的栈) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [31. 栈的压入、弹出序列](#31-栈的压入弹出序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [32.1 从上往下打印二叉树](#321-从上往下打印二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [32.2 把二叉树打印成多行](#322-把二叉树打印成多行) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [35. 复杂链表的复制](#35-复杂链表的复制) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [36. 二叉搜索树与双向链表](#36-二叉搜索树与双向链表) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [37. 序列化二叉树](#37-序列化二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [38. 字符串的排列](#38-字符串的排列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [39. 数组中出现次数超过一半的数字](#39-数组中出现次数超过一半的数字) + * [解题思路](#解题思路) + + + +# 30. 包含 min 函数的栈 + +[NowCoder](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。 + +## 解题思路 + +```java +private Stack dataStack = new Stack<>(); +private Stack minStack = new Stack<>(); + +public void push(int node) { + dataStack.push(node); + minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node)); +} + +public void pop() { + dataStack.pop(); + minStack.pop(); +} + +public int top() { + return dataStack.peek(); +} + +public int min() { + return minStack.peek(); +} +``` + +# 31. 栈的压入、弹出序列 + +[NowCoder](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。 + +例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。 + +## 解题思路 + +使用一个栈来模拟压入弹出操作。 + +```java +public boolean IsPopOrder(int[] pushSequence, int[] popSequence) { + int n = pushSequence.length; + Stack stack = new Stack<>(); + for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) { + stack.push(pushSequence[pushIndex]); + while (popIndex < n && !stack.isEmpty() + && stack.peek() == popSequence[popIndex]) { + stack.pop(); + popIndex++; + } + } + return stack.isEmpty(); +} +``` + +# 32.1 从上往下打印二叉树 + +[NowCoder](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +从上往下打印出二叉树的每个节点,同层节点从左至右打印。 + +例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7 + +

+ +## 解题思路 + +使用队列来进行层次遍历。 + +不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 + +```java +public ArrayList PrintFromTopToBottom(TreeNode root) { + Queue queue = new LinkedList<>(); + ArrayList ret = new ArrayList<>(); + queue.add(root); + while (!queue.isEmpty()) { + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode t = queue.poll(); + if (t == null) + continue; + ret.add(t.val); + queue.add(t.left); + queue.add(t.right); + } + } + return ret; +} +``` + +# 32.2 把二叉树打印成多行 + +[NowCoder](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +和上题几乎一样。 + +## 解题思路 + +```java +ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + while (!queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) + continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (list.size() != 0) + ret.add(list); + } + return ret; +} +``` + +# 32.3 按之字形顺序打印二叉树 + +[NowCoder](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 + +## 解题思路 + +```java +public ArrayList> Print(TreeNode pRoot) { + ArrayList> ret = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(pRoot); + boolean reverse = false; + while (!queue.isEmpty()) { + ArrayList list = new ArrayList<>(); + int cnt = queue.size(); + while (cnt-- > 0) { + TreeNode node = queue.poll(); + if (node == null) + continue; + list.add(node.val); + queue.add(node.left); + queue.add(node.right); + } + if (reverse) + Collections.reverse(list); + reverse = !reverse; + if (list.size() != 0) + ret.add(list); + } + return ret; +} +``` + +# 33. 二叉搜索树的后序遍历序列 + +[NowCoder](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。 + +例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树。 + +

+ +## 解题思路 + +```java +public boolean VerifySquenceOfBST(int[] sequence) { + if (sequence == null || sequence.length == 0) + return false; + return verify(sequence, 0, sequence.length - 1); +} + +private boolean verify(int[] sequence, int first, int last) { + if (last - first <= 1) + return true; + int rootVal = sequence[last]; + int cutIndex = first; + while (cutIndex < last && sequence[cutIndex] <= rootVal) + cutIndex++; + for (int i = cutIndex; i < last; i++) + if (sequence[i] < rootVal) + return false; + return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1); +} +``` + +# 34. 二叉树中和为某一值的路径 + +[NowCoder](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 + +下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12 + +

+ +## 解题思路 + +```java +private ArrayList> ret = new ArrayList<>(); + +public ArrayList> FindPath(TreeNode root, int target) { + backtracking(root, target, new ArrayList<>()); + return ret; +} + +private void backtracking(TreeNode node, int target, ArrayList path) { + if (node == null) + return; + path.add(node.val); + target -= node.val; + if (target == 0 && node.left == null && node.right == null) { + ret.add(new ArrayList<>(path)); + } else { + backtracking(node.left, target, path); + backtracking(node.right, target, path); + } + path.remove(path.size() - 1); +} +``` + +# 35. 复杂链表的复制 + +[NowCoder](https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。 + +```java +public class RandomListNode { + int label; + RandomListNode next = null; + RandomListNode random = null; + + RandomListNode(int label) { + this.label = label; + } +} +``` + +

+ +## 解题思路 + +第一步,在每个节点的后面插入复制的节点。 + +

+ +第二步,对复制节点的 random 链接进行赋值。 + +

+ +第三步,拆分。 + +

+ +```java +public RandomListNode Clone(RandomListNode pHead) { + if (pHead == null) + return null; + // 插入新节点 + RandomListNode cur = pHead; + while (cur != null) { + RandomListNode clone = new RandomListNode(cur.label); + clone.next = cur.next; + cur.next = clone; + cur = clone.next; + } + // 建立 random 链接 + cur = pHead; + while (cur != null) { + RandomListNode clone = cur.next; + if (cur.random != null) + clone.random = cur.random.next; + cur = clone.next; + } + // 拆分 + cur = pHead; + RandomListNode pCloneHead = pHead.next; + while (cur.next != null) { + RandomListNode next = cur.next; + cur.next = next.next; + cur = next; + } + return pCloneHead; +} +``` + +# 36. 二叉搜索树与双向链表 + +[NowCoder](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 + +

+ +## 解题思路 + +```java +private TreeNode pre = null; +private TreeNode head = null; + +public TreeNode Convert(TreeNode root) { + inOrder(root); + return head; +} + +private void inOrder(TreeNode node) { + if (node == null) + return; + inOrder(node.left); + node.left = pre; + if (pre != null) + pre.right = node; + pre = node; + if (head == null) + head = node; + inOrder(node.right); +} +``` + +# 37. 序列化二叉树 + +[NowCoder](https://www.nowcoder.com/practice/cf7e25aa97c04cc1a68c8f040e71fb84?tpId=13&tqId=11214&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +请实现两个函数,分别用来序列化和反序列化二叉树。 + +## 解题思路 + +```java +private String deserializeStr; + +public String Serialize(TreeNode root) { + if (root == null) + return "#"; + return root.val + " " + Serialize(root.left) + " " + Serialize(root.right); +} + +public TreeNode Deserialize(String str) { + deserializeStr = str; + return Deserialize(); +} + +private TreeNode Deserialize() { + if (deserializeStr.length() == 0) + return null; + int index = deserializeStr.indexOf(" "); + String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index); + deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1); + if (node.equals("#")) + return null; + int val = Integer.valueOf(node); + TreeNode t = new TreeNode(val); + t.left = Deserialize(); + t.right = Deserialize(); + return t; +} +``` + +# 38. 字符串的排列 + +[NowCoder](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。 + +## 解题思路 + +```java +private ArrayList ret = new ArrayList<>(); + +public ArrayList Permutation(String str) { + if (str.length() == 0) + return ret; + char[] chars = str.toCharArray(); + Arrays.sort(chars); + backtracking(chars, new boolean[chars.length], new StringBuilder()); + return ret; +} + +private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) { + if (s.length() == chars.length) { + ret.add(s.toString()); + return; + } + for (int i = 0; i < chars.length; i++) { + if (hasUsed[i]) + continue; + if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */ + continue; + hasUsed[i] = true; + s.append(chars[i]); + backtracking(chars, hasUsed, s); + s.deleteCharAt(s.length() - 1); + hasUsed[i] = false; + } +} +``` + +# 39. 数组中出现次数超过一半的数字 + +[NowCoder](https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?tpId=13&tqId=11181&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 解题思路 + +多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。 + +使用 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) { + int majority = nums[0]; + for (int i = 1, cnt = 1; i < nums.length; i++) { + cnt = nums[i] == majority ? cnt + 1 : cnt - 1; + if (cnt == 0) { + majority = nums[i]; + cnt = 1; + } + } + int cnt = 0; + for (int val : nums) + if (val == majority) + cnt++; + return cnt > nums.length / 2 ? majority : 0; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 3~9.md b/docs/notes/剑指 Offer 题解 - 3~9.md new file mode 100644 index 00000000..8850b4cd --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 3~9.md @@ -0,0 +1,388 @@ + +* [3. 数组中重复的数字](#3-数组中重复的数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [4. 二维数组中的查找](#4-二维数组中的查找) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [5. 替换空格](#5-替换空格) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [6. 从尾到头打印链表](#6-从尾到头打印链表) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [使用递归](#使用递归) + * [使用头插法](#使用头插法) + * [使用栈](#使用栈) +* [7. 重建二叉树](#7-重建二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [8. 二叉树的下一个结点](#8-二叉树的下一个结点) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [9. 用两个栈实现队列](#9-用两个栈实现队列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 3. 数组中重复的数字 + +[NowCoder](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 + +```html +Input: +{2, 3, 1, 0, 2, 5} + +Output: +2 +``` + +## 解题思路 + +要求是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。 + +对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。 + +以 (2, 3, 1, 0, 2, 5) 为例,遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复: + +

+ +```java +public boolean duplicate(int[] nums, int length, int[] duplication) { + if (nums == null || length <= 0) + return false; + for (int i = 0; i < length; i++) { + while (nums[i] != i) { + if (nums[i] == nums[nums[i]]) { + duplication[0] = nums[i]; + return true; + } + swap(nums, i, nums[i]); + } + } + return false; +} + +private void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; +} +``` + +# 4. 二维数组中的查找 + +[NowCoder](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。 + +```html +Consider the following matrix: +[ + [1, 4, 7, 11, 15], + [2, 5, 8, 12, 19], + [3, 6, 9, 16, 22], + [10, 13, 14, 17, 24], + [18, 21, 23, 26, 30] +] + +Given target = 5, return true. +Given target = 20, return false. +``` + +## 解题思路 + +要求时间复杂度 O(M + N),空间复杂度 O(1)。 + +该二维数组中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。 + +

+ +```java +public boolean Find(int target, int[][] matrix) { + if (matrix == null || matrix.length == 0 || matrix[0].length == 0) + return false; + int rows = matrix.length, cols = matrix[0].length; + int r = 0, c = cols - 1; // 从右上角开始 + while (r <= rows - 1 && c >= 0) { + if (target == matrix[r][c]) + return true; + else if (target > matrix[r][c]) + r++; + else + c--; + } + return false; +} +``` + +# 5. 替换空格 + +[NowCoder](https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423?tpId=13&tqId=11155&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + + +将一个字符串中的空格替换成 "%20"。 + +```text +Input: +"A B" + +Output: +"A%20B" +``` + +## 解题思路 + +在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。 + +令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。 + +从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。 + +

+ +```java +public String replaceSpace(StringBuffer str) { + int P1 = str.length() - 1; + for (int i = 0; i <= P1; i++) + if (str.charAt(i) == ' ') + str.append(" "); + + int P2 = str.length() - 1; + while (P1 >= 0 && P2 > P1) { + char c = str.charAt(P1--); + if (c == ' ') { + str.setCharAt(P2--, '0'); + str.setCharAt(P2--, '2'); + str.setCharAt(P2--, '%'); + } else { + str.setCharAt(P2--, c); + } + } + return str.toString(); +} +``` + +# 6. 从尾到头打印链表 + +[NowCoder](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +从尾到头反过来打印出每个结点的值。 + +

+ +## 解题思路 + +### 使用递归 + +

+ + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList ret = new ArrayList<>(); + if (listNode != null) { + ret.addAll(printListFromTailToHead(listNode.next)); + ret.add(listNode.val); + } + return ret; +} +``` + +### 使用头插法 + +利用链表头插法为逆序的特点。 + +头结点和第一个节点的区别: + +- 头结点是在头插法中使用的一个额外节点,这个节点不存储值; +- 第一个节点就是链表的第一个真正存储值的节点。 + + +

+ +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + // 头插法构建逆序链表 + ListNode head = new ListNode(-1); + while (listNode != null) { + ListNode memo = listNode.next; + listNode.next = head.next; + head.next = listNode; + listNode = memo; + } + // 构建 ArrayList + ArrayList ret = new ArrayList<>(); + head = head.next; + while (head != null) { + ret.add(head.val); + head = head.next; + } + return ret; +} +``` + +### 使用栈 + +

+ +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + Stack stack = new Stack<>(); + while (listNode != null) { + stack.add(listNode.val); + listNode = listNode.next; + } + ArrayList ret = new ArrayList<>(); + while (!stack.isEmpty()) + ret.add(stack.pop()); + return ret; +} +``` + +# 7. 重建二叉树 + +[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 + +```html +preorder = [3,9,20,15,7] +inorder = [9,3,15,20,7] +``` + +

+ +## 解题思路 + +前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。 + +

+ +```java +// 缓存中序遍历数组每个值对应的索引 +private Map indexForInOrders = new HashMap<>(); + +public TreeNode reConstructBinaryTree(int[] pre, int[] in) { + for (int i = 0; i < in.length; i++) + indexForInOrders.put(in[i], i); + return reConstructBinaryTree(pre, 0, pre.length - 1, 0); +} + +private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) { + if (preL > preR) + return null; + TreeNode root = new TreeNode(pre[preL]); + int inIndex = indexForInOrders.get(root.val); + int leftTreeSize = inIndex - inL; + root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL); + root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1); + return root; +} +``` + +# 8. 二叉树的下一个结点 + +[NowCoder](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 + +```java +public class TreeLinkNode { + + int val; + TreeLinkNode left = null; + TreeLinkNode right = null; + TreeLinkNode next = null; + + TreeLinkNode(int val) { + this.val = val; + } +} +``` + +## 解题思路 + +① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; + +

+ +② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 + +

+ +```java +public TreeLinkNode GetNext(TreeLinkNode pNode) { + if (pNode.right != null) { + TreeLinkNode node = pNode.right; + while (node.left != null) + node = node.left; + return node; + } else { + while (pNode.next != null) { + TreeLinkNode parent = pNode.next; + if (parent.left == pNode) + return parent; + pNode = pNode.next; + } + } + return null; +} +``` + +# 9. 用两个栈实现队列 + +[NowCoder](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。 + +## 解题思路 + +in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。 + +

+ + +```java +Stack in = new Stack(); +Stack out = new Stack(); + +public void push(int node) { + in.push(node); +} + +public int pop() throws Exception { + if (out.isEmpty()) + while (!in.isEmpty()) + out.push(in.pop()); + + if (out.isEmpty()) + throw new Exception("queue is empty"); + + return out.pop(); +} +``` + + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 40~49.md b/docs/notes/剑指 Offer 题解 - 40~49.md new file mode 100644 index 00000000..358a5a39 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 40~49.md @@ -0,0 +1,446 @@ + +* [40. 最小的 K 个数](#40-最小的-k-个数) + * [解题思路](#解题思路) + * [快速选择](#快速选择) + * [大小为 K 的最小堆](#大小为-k-的最小堆) +* [41.1 数据流中的中位数](#411-数据流中的中位数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [41.2 字符流中第一个不重复的字符](#412-字符流中第一个不重复的字符) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [42. 连续子数组的最大和](#42-连续子数组的最大和) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [43. 从 1 到 n 整数中 1 出现的次数](#43-从-1-到-n-整数中-1-出现的次数) + * [解题思路](#解题思路) +* [44. 数字序列中的某一位数字](#44-数字序列中的某一位数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [45. 把数组排成最小的数](#45-把数组排成最小的数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [46. 把数字翻译成字符串](#46-把数字翻译成字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [47. 礼物的最大价值](#47-礼物的最大价值) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [48. 最长不含重复字符的子字符串](#48-最长不含重复字符的子字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [49. 丑数](#49-丑数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 40. 最小的 K 个数 + +[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 解题思路 + +### 快速选择 + +- 复杂度:O(N) + O(1) +- 只有当允许修改数组元素时才可以使用 + +快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。 + +```java +public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { + ArrayList ret = new ArrayList<>(); + if (k > nums.length || k <= 0) + return ret; + findKthSmallest(nums, k - 1); + /* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */ + for (int i = 0; i < k; i++) + ret.add(nums[i]); + return ret; +} + +public void findKthSmallest(int[] nums, int k) { + int l = 0, h = nums.length - 1; + while (l < h) { + int j = partition(nums, l, h); + if (j == k) + break; + if (j > k) + h = j - 1; + else + l = j + 1; + } +} + +private int partition(int[] nums, int l, int h) { + int p = nums[l]; /* 切分元素 */ + int i = l, j = h + 1; + while (true) { + while (i != h && nums[++i] < p) ; + while (j != l && nums[--j] > p) ; + if (i >= j) + break; + swap(nums, i, j); + } + swap(nums, l, j); + return j; +} + +private void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; +} +``` + +### 大小为 K 的最小堆 + +- 复杂度:O(NlogK) + O(K) +- 特别适合处理海量数据 + +应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。 + +维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。 + +```java +public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { + if (k > nums.length || k <= 0) + return new ArrayList<>(); + PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1); + for (int num : nums) { + maxHeap.add(num); + if (maxHeap.size() > k) + maxHeap.poll(); + } + return new ArrayList<>(maxHeap); +} +``` + +# 41.1 数据流中的中位数 + +[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 + +## 解题思路 + +```java +/* 大顶堆,存储左半边元素 */ +private PriorityQueue left = new PriorityQueue<>((o1, o2) -> o2 - o1); +/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */ +private PriorityQueue right = new PriorityQueue<>(); +/* 当前数据流读入的元素个数 */ +private int N = 0; + +public void Insert(Integer val) { + /* 插入要保证两个堆存于平衡状态 */ + if (N % 2 == 0) { + /* N 为偶数的情况下插入到右半边。 + * 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大, + * 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */ + left.add(val); + right.add(left.poll()); + } else { + right.add(val); + left.add(right.poll()); + } + N++; +} + +public Double GetMedian() { + if (N % 2 == 0) + return (left.peek() + right.peek()) / 2.0; + else + return (double) right.peek(); +} +``` + +# 41.2 字符流中第一个不重复的字符 + +[NowCoder](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。 + +## 解题思路 + +```java +private int[] cnts = new int[256]; +private Queue queue = new LinkedList<>(); + +public void Insert(char ch) { + cnts[ch]++; + queue.add(ch); + while (!queue.isEmpty() && cnts[queue.peek()] > 1) + queue.poll(); +} + +public char FirstAppearingOnce() { + return queue.isEmpty() ? '#' : queue.peek(); +} +``` + +# 42. 连续子数组的最大和 + +[NowCoder](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 + +## 解题思路 + +```java +public int FindGreatestSumOfSubArray(int[] nums) { + if (nums == null || nums.length == 0) + return 0; + int greatestSum = Integer.MIN_VALUE; + int sum = 0; + for (int val : nums) { + sum = sum <= 0 ? val : sum + val; + greatestSum = Math.max(greatestSum, sum); + } + return greatestSum; +} +``` + +# 43. 从 1 到 n 整数中 1 出现的次数 + +[NowCoder](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&tqId=11184&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 解题思路 + +```java +public int NumberOf1Between1AndN_Solution(int n) { + int cnt = 0; + for (int m = 1; m <= n; m *= 10) { + int a = n / m, b = n % m; + cnt += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0); + } + return cnt; +} +``` + +> [Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython) + +# 44. 数字序列中的某一位数字 + +## 题目描述 + +数字以 0123456789101112131415... 的格式序列化到一个字符串中,求这个字符串的第 index 位。 + +## 解题思路 + +```java +public int getDigitAtIndex(int index) { + if (index < 0) + return -1; + int place = 1; // 1 表示个位,2 表示 十位... + while (true) { + int amount = getAmountOfPlace(place); + int totalAmount = amount * place; + if (index < totalAmount) + return getDigitAtIndex(index, place); + index -= totalAmount; + place++; + } +} + +/** + * place 位数的数字组成的字符串长度 + * 10, 90, 900, ... + */ +private int getAmountOfPlace(int place) { + if (place == 1) + return 10; + return (int) Math.pow(10, place - 1) * 9; +} + +/** + * place 位数的起始数字 + * 0, 10, 100, ... + */ +private int getBeginNumberOfPlace(int place) { + if (place == 1) + return 0; + return (int) Math.pow(10, place - 1); +} + +/** + * 在 place 位数组成的字符串中,第 index 个数 + */ +private int getDigitAtIndex(int index, int place) { + int beginNumber = getBeginNumberOfPlace(place); + int shiftNumber = index / place; + String number = (beginNumber + shiftNumber) + ""; + int count = index % place; + return number.charAt(count) - '0'; +} +``` + +# 45. 把数组排成最小的数 + +[NowCoder](https://www.nowcoder.com/practice/8fecd3f8ba334add803bf2a06af1b993?tpId=13&tqId=11185&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。 + +## 解题思路 + +可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。 + +```java +public String PrintMinNumber(int[] numbers) { + if (numbers == null || numbers.length == 0) + return ""; + int n = numbers.length; + String[] nums = new String[n]; + for (int i = 0; i < n; i++) + nums[i] = numbers[i] + ""; + Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1)); + String ret = ""; + for (String str : nums) + ret += str; + return ret; +} +``` + +# 46. 把数字翻译成字符串 + +[Leetcode](https://leetcode.com/problems/decode-ways/description/) + +## 题目描述 + +给定一个数字,按照如下规则翻译成字符串:1 翻译成“a”,2 翻译成“b”... 26 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 abbeh,lbeh,aveh,abyh,lyh。实现一个函数,用来计算一个数字有多少种不同的翻译方法。 + +## 解题思路 + +```java +public int numDecodings(String s) { + if (s == null || s.length() == 0) + return 0; + int n = s.length(); + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = s.charAt(0) == '0' ? 0 : 1; + for (int i = 2; i <= n; i++) { + int one = Integer.valueOf(s.substring(i - 1, i)); + if (one != 0) + dp[i] += dp[i - 1]; + if (s.charAt(i - 2) == '0') + continue; + int two = Integer.valueOf(s.substring(i - 2, i)); + if (two <= 26) + dp[i] += dp[i - 2]; + } + return dp[n]; +} +``` + +# 47. 礼物的最大价值 + +[NowCoder](https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab) + +## 题目描述 + +在一个 m\*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于 0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘 + +``` +1 10 3 8 +12 2 9 6 +5 7 4 11 +3 7 16 5 +``` + +礼物的最大价值为 1+12+5+7+7+16+5=53。 + +## 解题思路 + +应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。 + +```java +public int getMost(int[][] values) { + if (values == null || values.length == 0 || values[0].length == 0) + return 0; + int n = values[0].length; + int[] dp = new int[n]; + for (int[] value : values) { + dp[0] += value[0]; + for (int i = 1; i < n; i++) + dp[i] = Math.max(dp[i], dp[i - 1]) + value[i]; + } + return dp[n - 1]; +} +``` + +# 48. 最长不含重复字符的子字符串 + +## 题目描述 + +输入一个字符串(只包含 a\~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。 + +## 解题思路 + +```java +public int longestSubStringWithoutDuplication(String str) { + int curLen = 0; + int maxLen = 0; + int[] preIndexs = new int[26]; + Arrays.fill(preIndexs, -1); + for (int curI = 0; curI < str.length(); curI++) { + int c = str.charAt(curI) - 'a'; + int preI = preIndexs[c]; + if (preI == -1 || curI - preI > curLen) { + curLen++; + } else { + maxLen = Math.max(maxLen, curLen); + curLen = curI - preI; + } + preIndexs[c] = curI; + } + maxLen = Math.max(maxLen, curLen); + return maxLen; +} +``` + +# 49. 丑数 + +[NowCoder](https://www.nowcoder.com/practice/6aa9e04fc3794f68acf8778237ba065b?tpId=13&tqId=11186&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。 + +## 解题思路 + +```java +public int GetUglyNumber_Solution(int N) { + if (N <= 6) + return N; + int i2 = 0, i3 = 0, i5 = 0; + int[] dp = new int[N]; + dp[0] = 1; + for (int i = 1; i < N; i++) { + int next2 = dp[i2] * 2, next3 = dp[i3] * 3, next5 = dp[i5] * 5; + dp[i] = Math.min(next2, Math.min(next3, next5)); + if (dp[i] == next2) + i2++; + if (dp[i] == next3) + i3++; + if (dp[i] == next5) + i5++; + } + return dp[N - 1]; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 50~59.md b/docs/notes/剑指 Offer 题解 - 50~59.md new file mode 100644 index 00000000..fb2c2cf9 --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 50~59.md @@ -0,0 +1,505 @@ + +* [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [51. 数组中的逆序对](#51-数组中的逆序对) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [53. 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [54. 二叉查找树的第 K 个结点](#54-二叉查找树的第-k-个结点) + * [解题思路](#解题思路) +* [55.1 二叉树的深度](#551-二叉树的深度) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [55.2 平衡二叉树](#552-平衡二叉树) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [57.1 和为 S 的两个数字](#571-和为-s-的两个数字) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [58.1 翻转单词顺序列](#581-翻转单词顺序列) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [58.2 左旋转字符串](#582-左旋转字符串) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [59. 滑动窗口的最大值](#59-滑动窗口的最大值) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + + + +# 50. 第一个只出现一次的字符位置 + +[NowCoder](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +在一个字符串中找到第一个只出现一次的字符,并返回它的位置。 + +## 解题思路 + +最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。 + +```java +public int FirstNotRepeatingChar(String str) { + int[] cnts = new int[256]; + for (int i = 0; i < str.length(); i++) + cnts[str.charAt(i)]++; + for (int i = 0; i < str.length(); i++) + if (cnts[str.charAt(i)] == 1) + return i; + return -1; +} +``` + +以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。 + +```java +public int FirstNotRepeatingChar2(String str) { + BitSet bs1 = new BitSet(256); + BitSet bs2 = new BitSet(256); + for (char c : str.toCharArray()) { + if (!bs1.get(c) && !bs2.get(c)) + bs1.set(c); // 0 0 -> 0 1 + else if (bs1.get(c) && !bs2.get(c)) + bs2.set(c); // 0 1 -> 1 1 + } + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (bs1.get(c) && !bs2.get(c)) // 0 1 + return i; + } + return -1; +} +``` + +# 51. 数组中的逆序对 + +[NowCoder](https://www.nowcoder.com/practice/96bd6684e04a44eb80e6a68efc0ec6c5?tpId=13&tqId=11188&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 + +## 解题思路 + +```java +private long cnt = 0; +private int[] tmp; // 在这里声明辅助数组,而不是在 merge() 递归函数中声明 + +public int InversePairs(int[] nums) { + tmp = new int[nums.length]; + mergeSort(nums, 0, nums.length - 1); + return (int) (cnt % 1000000007); +} + +private void mergeSort(int[] nums, int l, int h) { + if (h - l < 1) + return; + int m = l + (h - l) / 2; + mergeSort(nums, l, m); + mergeSort(nums, m + 1, h); + merge(nums, l, m, h); +} + +private void merge(int[] nums, int l, int m, int h) { + int i = l, j = m + 1, k = l; + while (i <= m || j <= h) { + if (i > m) + tmp[k] = nums[j++]; + else if (j > h) + tmp[k] = nums[i++]; + else if (nums[i] < nums[j]) + tmp[k] = nums[i++]; + else { + tmp[k] = nums[j++]; + this.cnt += m - i + 1; // nums[i] >= nums[j],说明 nums[i...mid] 都大于 nums[j] + } + k++; + } + for (k = l; k <= h; k++) + nums[k] = tmp[k]; +} +``` + +# 52. 两个链表的第一个公共结点 + +[NowCoder](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=11189&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +

+ +## 解题思路 + +设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 + +当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + ListNode l1 = pHead1, l2 = pHead2; + while (l1 != l2) { + l1 = (l1 == null) ? pHead2 : l1.next; + l2 = (l2 == null) ? pHead1 : l2.next; + } + return l1; +} +``` + +# 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) + +## 题目描述 + +```html +Input: +nums = 1, 2, 3, 3, 3, 3, 4, 6 +K = 3 + +Output: +4 +``` + +## 解题思路 + +```java +public int GetNumberOfK(int[] nums, int K) { + int first = binarySearch(nums, K); + int last = binarySearch(nums, K + 1); + return (first == nums.length || nums[first] != K) ? 0 : last - first; +} + +private int binarySearch(int[] nums, int K) { + int l = 0, h = nums.length; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= K) + h = m; + else + l = m + 1; + } + return l; +} +``` + +# 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; +private int cnt = 0; + +public TreeNode KthNode(TreeNode pRoot, int k) { + inOrder(pRoot, k); + return ret; +} + +private void inOrder(TreeNode root, int k) { + if (root == null || cnt >= k) + return; + inOrder(root.left, k); + cnt++; + if (cnt == k) + ret = root; + inOrder(root.right, k); +} +``` + +# 55.1 二叉树的深度 + +[NowCoder](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 + +

+ +## 解题思路 + +```java +public int TreeDepth(TreeNode root) { + return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); +} +``` + +# 55.2 平衡二叉树 + +[NowCoder](https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=13&tqId=11192&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +平衡二叉树左右子树高度差不超过 1。 + +

+ +## 解题思路 + +```java +private boolean isBalanced = true; + +public boolean IsBalanced_Solution(TreeNode root) { + height(root); + return isBalanced; +} + +private int height(TreeNode root) { + if (root == null || !isBalanced) + return 0; + int left = height(root.left); + int right = height(root.right); + if (Math.abs(left - right) > 1) + isBalanced = false; + return 1 + Math.max(left, right); +} +``` + +# 56. 数组中只出现一次的数字 + +[NowCoder](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。 + +## 解题思路 + +两个不相等的元素在位级表示上必定会有一位存在不同,将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 + +diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 + +```java +public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) { + int diff = 0; + for (int num : nums) + diff ^= num; + diff &= -diff; + for (int num : nums) { + if ((num & diff) == 0) + num1[0] ^= num; + else + num2[0] ^= num; + } +} +``` + +# 57.1 和为 S 的两个数字 + +[NowCoder](https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。 + +## 解题思路 + +使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 + +- 如果两个指针指向元素的和 sum == target,那么得到要求的结果; +- 如果 sum > target,移动较大的元素,使 sum 变小一些; +- 如果 sum < target,移动较小的元素,使 sum 变大一些。 + +```java +public ArrayList FindNumbersWithSum(int[] array, int sum) { + int i = 0, j = array.length - 1; + while (i < j) { + int cur = array[i] + array[j]; + if (cur == sum) + return new ArrayList<>(Arrays.asList(array[i], array[j])); + if (cur < sum) + i++; + else + j--; + } + return new ArrayList<>(); +} +``` + +# 57.2 和为 S 的连续正数序列 + +[NowCoder](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&tqId=11194&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +输出所有和为 S 的连续正数序列。 + +例如和为 100 的连续序列有: + +``` +[9, 10, 11, 12, 13, 14, 15, 16] +[18, 19, 20, 21, 22]。 +``` + +## 解题思路 + +```java +public ArrayList> FindContinuousSequence(int sum) { + ArrayList> ret = new ArrayList<>(); + int start = 1, end = 2; + int curSum = 3; + while (end < sum) { + if (curSum > sum) { + curSum -= start; + start++; + } else if (curSum < sum) { + end++; + curSum += end; + } else { + ArrayList list = new ArrayList<>(); + for (int i = start; i <= end; i++) + list.add(i); + ret.add(list); + curSum -= start; + start++; + end++; + curSum += end; + } + } + return ret; +} +``` + +# 58.1 翻转单词顺序列 + +[NowCoder](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +```html +Input: +"I am a student." + +Output: +"student. a am I" +``` + +## 解题思路 + +题目应该有一个隐含条件,就是不能用额外的空间。虽然 Java 的题目输入参数为 String 类型,需要先创建一个字符数组使得空间复杂度为 O(N),但是正确的参数类型应该和原书一样,为字符数组,并且只能使用该字符数组的空间。任何使用了额外空间的解法在面试时都会大打折扣,包括递归解法。 + +正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。 + +```java +public String ReverseSentence(String str) { + int n = str.length(); + char[] chars = str.toCharArray(); + int i = 0, j = 0; + while (j <= n) { + if (j == n || chars[j] == ' ') { + reverse(chars, i, j - 1); + i = j + 1; + } + j++; + } + reverse(chars, 0, n - 1); + return new String(chars); +} + +private void reverse(char[] c, int i, int j) { + while (i < j) + swap(c, i++, j--); +} + +private void swap(char[] c, int i, int j) { + char t = c[i]; + c[i] = c[j]; + c[j] = t; +} +``` + +# 58.2 左旋转字符串 + +[NowCoder](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +```html +Input: +S="abcXYZdef" +K=3 + +Output: +"XYZdefabc" +``` + +## 解题思路 + +先将 "abc" 和 "XYZdef" 分别翻转,得到 "cbafedZYX",然后再把整个字符串翻转得到 "XYZdefabc"。 + +```java +public String LeftRotateString(String str, int n) { + if (n >= str.length()) + return str; + char[] chars = str.toCharArray(); + reverse(chars, 0, n - 1); + reverse(chars, n, chars.length - 1); + reverse(chars, 0, chars.length - 1); + return new String(chars); +} + +private void reverse(char[] chars, int i, int j) { + while (i < j) + swap(chars, i++, j--); +} + +private void swap(char[] chars, int i, int j) { + char t = chars[i]; + chars[i] = chars[j]; + chars[j] = t; +} +``` + +# 59. 滑动窗口的最大值 + +[NowCoder](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。 + +例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。 + +## 解题思路 + +```java +public ArrayList maxInWindows(int[] num, int size) { + ArrayList ret = new ArrayList<>(); + if (size > num.length || size < 1) + return ret; + PriorityQueue heap = new PriorityQueue<>((o1, o2) -> o2 - o1); /* 大顶堆 */ + for (int i = 0; i < size; i++) + heap.add(num[i]); + ret.add(heap.peek()); + for (int i = 0, j = i + size; j < num.length; i++, j++) { /* 维护一个大小为 size 的大顶堆 */ + heap.remove(num[i]); + heap.add(num[j]); + ret.add(heap.peek()); + } + return ret; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 60~68.md b/docs/notes/剑指 Offer 题解 - 60~68.md new file mode 100644 index 00000000..a3d5610f --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 60~68.md @@ -0,0 +1,347 @@ + +* [60. n 个骰子的点数](#60-n-个骰子的点数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) + * [动态规划解法](#动态规划解法) + * [动态规划解法 + 旋转数组](#动态规划解法--旋转数组) +* [61. 扑克牌顺子](#61-扑克牌顺子) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [63. 股票的最大利润](#63-股票的最大利润) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [64. 求 1+2+3+...+n](#64-求-123n) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [65. 不用加减乘除做加法](#65-不用加减乘除做加法) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [66. 构建乘积数组](#66-构建乘积数组) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [67. 把字符串转换成整数](#67-把字符串转换成整数) + * [题目描述](#题目描述) + * [解题思路](#解题思路) +* [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先) + * [解题思路](#解题思路) + * [二叉查找树](#二叉查找树) + * [普通二叉树](#普通二叉树) + + + +# 60. n 个骰子的点数 + +[Lintcode](https://www.lintcode.com/en/problem/dices-sum/) + +## 题目描述 + +把 n 个骰子仍在地上,求点数和为 s 的概率。 + +

+ +## 解题思路 + +### 动态规划解法 + +使用一个二维数组 dp 存储点数出现的次数,其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。 + +空间复杂度:O(N2) + +```java +public List> dicesSum(int n) { + final int face = 6; + final int pointNum = face * n; + long[][] dp = new long[n + 1][pointNum + 1]; + + for (int i = 1; i <= face; i++) + dp[1][i] = 1; + + for (int i = 2; i <= n; i++) + for (int j = i; j <= pointNum; j++) /* 使用 i 个骰子最小点数为 i */ + for (int k = 1; k <= face && k <= j; k++) + dp[i][j] += dp[i - 1][j - k]; + + final double totalNum = Math.pow(6, n); + List> ret = new ArrayList<>(); + for (int i = n; i <= pointNum; i++) + ret.add(new AbstractMap.SimpleEntry<>(i, dp[n][i] / totalNum)); + + return ret; +} +``` + +### 动态规划解法 + 旋转数组 + +空间复杂度:O(N) + +```java +public List> dicesSum(int n) { + final int face = 6; + final int pointNum = face * n; + long[][] dp = new long[2][pointNum + 1]; + + for (int i = 1; i <= face; i++) + dp[0][i] = 1; + + int flag = 1; /* 旋转标记 */ + for (int i = 2; i <= n; i++, flag = 1 - flag) { + for (int j = 0; j <= pointNum; j++) + dp[flag][j] = 0; /* 旋转数组清零 */ + + for (int j = i; j <= pointNum; j++) + for (int k = 1; k <= face && k <= j; k++) + dp[flag][j] += dp[1 - flag][j - k]; + } + + final double totalNum = Math.pow(6, n); + List> ret = new ArrayList<>(); + for (int i = n; i <= pointNum; i++) + ret.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalNum)); + + return ret; +} +``` + +# 61. 扑克牌顺子 + +[NowCoder](https://www.nowcoder.com/practice/762836f4d43d43ca9deb273b3de8e1f4?tpId=13&tqId=11198&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +五张牌,其中大小鬼为癞子,牌面大小为 0。判断这五张牌是否能组成顺子。 + +

+ +## 解题思路 + +```java +public boolean isContinuous(int[] nums) { + + if (nums.length < 5) + return false; + + Arrays.sort(nums); + + // 统计癞子数量 + int cnt = 0; + for (int num : nums) + if (num == 0) + cnt++; + + // 使用癞子去补全不连续的顺子 + for (int i = cnt; i < nums.length - 1; i++) { + if (nums[i + 1] == nums[i]) + return false; + cnt -= nums[i + 1] - nums[i] - 1; + } + + return cnt >= 0; +} +``` + +# 62. 圆圈中最后剩下的数 + +[NowCoder](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&tqId=11199&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。 + +## 解题思路 + +约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。 + +```java +public int LastRemaining_Solution(int n, int m) { + if (n == 0) /* 特殊输入的处理 */ + return -1; + if (n == 1) /* 递归返回条件 */ + return 0; + return (LastRemaining_Solution(n - 1, m) + m) % n; +} +``` + +# 63. 股票的最大利润 + +[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) + +## 题目描述 + +可以有一次买入和一次卖出,那么买入必须在前。求最大收益。 + +

+ +## 解题思路 + +使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。 + +```java +public int maxProfit(int[] prices) { + if (prices == null || prices.length == 0) + return 0; + int soFarMin = prices[0]; + int maxProfit = 0; + for (int i = 1; i < prices.length; i++) { + soFarMin = Math.min(soFarMin, prices[i]); + maxProfit = Math.max(maxProfit, prices[i] - soFarMin); + } + return maxProfit; +} +``` + +# 64. 求 1+2+3+...+n + +[NowCoder](https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 A ? B : C。 + +## 解题思路 + +使用递归解法最重要的是指定返回条件,但是本题无法直接使用 if 语句来指定返回条件。 + +条件与 && 具有短路原则,即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性,将递归的返回条件取非然后作为 && 的第一个条件语句,递归的主体转换为第二个条件语句,那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分,递归返回。 + +本题的递归返回条件为 n <= 0,取非后就是 n > 0;递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。 + +```java +public int Sum_Solution(int n) { + int sum = n; + boolean b = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0); + return sum; +} +``` + +# 65. 不用加减乘除做加法 + +[NowCoder](https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&tqId=11201&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +写一个函数,求两个整数之和,要求不得使用 +、-、\*、/ 四则运算符号。 + +## 解题思路 + +a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。 + +递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 + +```java +public int Add(int a, int b) { + return b == 0 ? a : Add(a ^ b, (a & b) << 1); +} +``` + +# 66. 构建乘积数组 + +[NowCoder](https://www.nowcoder.com/practice/94a4d381a68b47b7a8bed86f2975db46?tpId=13&tqId=11204&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +给定一个数组 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]。要求不能使用除法。 + +

+ +## 解题思路 + +```java +public int[] multiply(int[] A) { + int n = A.length; + int[] B = new int[n]; + for (int i = 0, product = 1; i < n; product *= A[i], i++) /* 从左往右累乘 */ + B[i] = product; + for (int i = n - 1, product = 1; i >= 0; product *= A[i], i--) /* 从右往左累乘 */ + B[i] *= product; + return B; +} +``` + +# 67. 把字符串转换成整数 + +[NowCoder](https://www.nowcoder.com/practice/1277c681251b4372bdef344468e4f26e?tpId=13&tqId=11202&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 0,要求不能使用字符串转换整数的库函数。 + +```html +Iuput: ++2147483647 +1a33 + +Output: +2147483647 +0 +``` + +## 解题思路 + +```java +public int StrToInt(String str) { + if (str == null || str.length() == 0) + return 0; + boolean isNegative = str.charAt(0) == '-'; + int ret = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i == 0 && (c == '+' || c == '-')) /* 符号判定 */ + continue; + if (c < '0' || c > '9') /* 非法输入 */ + return 0; + ret = ret * 10 + (c - '0'); + } + return isNegative ? -ret : ret; +} +``` + +# 68. 树中两个节点的最低公共祖先 + +## 解题思路 + +### 二叉查找树 + +

+ +[Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) + +二叉查找树中,两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val。 + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == null) + return root; + if (root.val > p.val && root.val > q.val) + return lowestCommonAncestor(root.left, p, q); + if (root.val < p.val && root.val < q.val) + return lowestCommonAncestor(root.right, p, q); + return root; +} +``` + +### 普通二叉树 + +

+ +[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 分别在两个子树中,那么就说明根节点就是最低公共祖先。 + +```java +public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == null || root == p || root == q) + return root; + TreeNode left = lowestCommonAncestor(root.left, p, q); + TreeNode right = lowestCommonAncestor(root.right, p, q); + return left == null ? right : right == null ? left : root; +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 目录.md b/docs/notes/剑指 Offer 题解 - 目录.md new file mode 100644 index 00000000..d4d5966d --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 目录.md @@ -0,0 +1,27 @@ + +* [目录](#目录) +* [参考文献](#参考文献) + + + +# 目录 + +部分绘图文件可以在这里免费下载:[剑指 Offer](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49),后续会慢慢把所有题目都配上 GIF 演示图。 + +- [3\~9](剑指%20Offer%20题解%20-%203\~9.md) +- [10\~19](剑指%20Offer%20题解%20-%2010\~19.md) +- [20\~29](剑指%20Offer%20题解%20-%2020\~29.md) +- [30\~39](剑指%20Offer%20题解%20-%2030\~39.md) +- [40\~49](剑指%20Offer%20题解%20-%2040\~49.md) +- [50\~59](剑指%20Offer%20题解%20-%2050\~59.md) +- [60\~68](剑指%20Offer%20题解%20-%2060\~68.md) + +# 参考文献 + +何海涛. 剑指 Offer[M]. 电子工业出版社, 2012. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 Offer 题解 - 目录1.md b/docs/notes/剑指 Offer 题解 - 目录1.md new file mode 100644 index 00000000..045b515f --- /dev/null +++ b/docs/notes/剑指 Offer 题解 - 目录1.md @@ -0,0 +1,27 @@ + +* [目录](#目录) +* [参考文献](#参考文献) + + + +# 目录 + +部分绘图文件可以在这里免费下载:[剑指 Offer](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49),后续会慢慢把所有题目都配上 GIF 演示图。 + +- [3\~9](notes/剑指%20Offer%20题解%20-%203\~9.md) +- [10\~19](notes/剑指%20Offer%20题解%20-%2010\~19.md) +- [20\~29](notes/剑指%20Offer%20题解%20-%2020\~29.md) +- [30\~39](notes/剑指%20Offer%20题解%20-%2030\~39.md) +- [40\~49](notes/剑指%20Offer%20题解%20-%2040\~49.md) +- [50\~59](notes/剑指%20Offer%20题解%20-%2050\~59.md) +- [60\~68](notes/剑指%20Offer%20题解%20-%2060\~68.md) + +# 参考文献 + +何海涛. 剑指 Offer[M]. 电子工业出版社, 2012. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/剑指 offer 题解.md b/docs/notes/剑指 offer 题解.md deleted file mode 100644 index 6e6f70cb..00000000 --- a/docs/notes/剑指 offer 题解.md +++ /dev/null @@ -1,3043 +0,0 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - -* [3. 数组中重复的数字](#3-数组中重复的数字) -* [4. 二维数组中的查找](#4-二维数组中的查找) -* [5. 替换空格](#5-替换空格) -* [6. 从尾到头打印链表](#6-从尾到头打印链表) -* [7. 重建二叉树](#7-重建二叉树) -* [8. 二叉树的下一个结点](#8-二叉树的下一个结点) -* [9. 用两个栈实现队列](#9-用两个栈实现队列) -* [10.1 斐波那契数列](#101-斐波那契数列) -* [10.2 矩形覆盖](#102-矩形覆盖) -* [10.3 跳台阶](#103-跳台阶) -* [10.4 变态跳台阶](#104-变态跳台阶) -* [11. 旋转数组的最小数字](#11-旋转数组的最小数字) -* [12. 矩阵中的路径](#12-矩阵中的路径) -* [13. 机器人的运动范围](#13-机器人的运动范围) -* [14. 剪绳子](#14-剪绳子) -* [15. 二进制中 1 的个数](#15-二进制中-1-的个数) -* [16. 数值的整数次方](#16-数值的整数次方) -* [17. 打印从 1 到最大的 n 位数](#17-打印从-1-到最大的-n-位数) -* [18.1 在 O(1) 时间内删除链表节点](#181-在-o1-时间内删除链表节点) -* [18.2 删除链表中重复的结点](#182-删除链表中重复的结点) -* [19. 正则表达式匹配](#19-正则表达式匹配) -* [20. 表示数值的字符串](#20-表示数值的字符串) -* [21. 调整数组顺序使奇数位于偶数前面](#21-调整数组顺序使奇数位于偶数前面) -* [22. 链表中倒数第 K 个结点](#22-链表中倒数第-k-个结点) -* [23. 链表中环的入口结点](#23-链表中环的入口结点) -* [24. 反转链表](#24-反转链表) -* [25. 合并两个排序的链表](#25-合并两个排序的链表) -* [26. 树的子结构](#26-树的子结构) -* [27. 二叉树的镜像](#27-二叉树的镜像) -* [28 对称的二叉树](#28-对称的二叉树) -* [29. 顺时针打印矩阵](#29-顺时针打印矩阵) -* [30. 包含 min 函数的栈](#30-包含-min-函数的栈) -* [31. 栈的压入、弹出序列](#31-栈的压入弹出序列) -* [32.1 从上往下打印二叉树](#321-从上往下打印二叉树) -* [32.2 把二叉树打印成多行](#322-把二叉树打印成多行) -* [32.3 按之字形顺序打印二叉树](#323-按之字形顺序打印二叉树) -* [33. 二叉搜索树的后序遍历序列](#33-二叉搜索树的后序遍历序列) -* [34. 二叉树中和为某一值的路径](#34-二叉树中和为某一值的路径) -* [35. 复杂链表的复制](#35-复杂链表的复制) -* [36. 二叉搜索树与双向链表](#36-二叉搜索树与双向链表) -* [37. 序列化二叉树](#37-序列化二叉树) -* [38. 字符串的排列](#38-字符串的排列) -* [39. 数组中出现次数超过一半的数字](#39-数组中出现次数超过一半的数字) -* [40. 最小的 K 个数](#40-最小的-k-个数) -* [41.1 数据流中的中位数](#411-数据流中的中位数) -* [41.2 字符流中第一个不重复的字符](#412-字符流中第一个不重复的字符) -* [42. 连续子数组的最大和](#42-连续子数组的最大和) -* [43. 从 1 到 n 整数中 1 出现的次数](#43-从-1-到-n-整数中-1-出现的次数) -* [44. 数字序列中的某一位数字](#44-数字序列中的某一位数字) -* [45. 把数组排成最小的数](#45-把数组排成最小的数) -* [46. 把数字翻译成字符串](#46-把数字翻译成字符串) -* [47. 礼物的最大价值](#47-礼物的最大价值) -* [48. 最长不含重复字符的子字符串](#48-最长不含重复字符的子字符串) -* [49. 丑数](#49-丑数) -* [50. 第一个只出现一次的字符位置](#50-第一个只出现一次的字符位置) -* [51. 数组中的逆序对](#51-数组中的逆序对) -* [52. 两个链表的第一个公共结点](#52-两个链表的第一个公共结点) -* [53. 数字在排序数组中出现的次数](#53-数字在排序数组中出现的次数) -* [54. 二叉查找树的第 K 个结点](#54-二叉查找树的第-k-个结点) -* [55.1 二叉树的深度](#551-二叉树的深度) -* [55.2 平衡二叉树](#552-平衡二叉树) -* [56. 数组中只出现一次的数字](#56-数组中只出现一次的数字) -* [57.1 和为 S 的两个数字](#571-和为-s-的两个数字) -* [57.2 和为 S 的连续正数序列](#572-和为-s-的连续正数序列) -* [58.1 翻转单词顺序列](#581-翻转单词顺序列) -* [58.2 左旋转字符串](#582-左旋转字符串) -* [59. 滑动窗口的最大值](#59-滑动窗口的最大值) -* [60. n 个骰子的点数](#60-n-个骰子的点数) -* [61. 扑克牌顺子](#61-扑克牌顺子) -* [62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数) -* [63. 股票的最大利润](#63-股票的最大利润) -* [64. 求 1+2+3+...+n](#64-求-123n) -* [65. 不用加减乘除做加法](#65-不用加减乘除做加法) -* [66. 构建乘积数组](#66-构建乘积数组) -* [67. 把字符串转换成整数](#67-把字符串转换成整数) -* [68. 树中两个节点的最低公共祖先](#68-树中两个节点的最低公共祖先) -* [参考文献](#参考文献) - - - -部分绘图文件可以在这里免费下载:[剑指 Offer](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49),后续会慢慢把所有题目都配上 GIF 演示图。 - -# 3. 数组中重复的数字 - -[NowCoder](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 - -```html -Input: -{2, 3, 1, 0, 2, 5} - -Output: -2 -``` - -## 解题思路 - -要求是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。 - -对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。 - -以 (2, 3, 1, 0, 2, 5) 为例,遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复: - -

- -```java -public boolean duplicate(int[] nums, int length, int[] duplication) { - if (nums == null || length <= 0) - return false; - for (int i = 0; i < length; i++) { - while (nums[i] != i) { - if (nums[i] == nums[nums[i]]) { - duplication[0] = nums[i]; - return true; - } - swap(nums, i, nums[i]); - } - } - return false; -} - -private void swap(int[] nums, int i, int j) { - int t = nums[i]; - nums[i] = nums[j]; - nums[j] = t; -} -``` - -# 4. 二维数组中的查找 - -[NowCoder](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。 - -```html -Consider the following matrix: -[ - [1, 4, 7, 11, 15], - [2, 5, 8, 12, 19], - [3, 6, 9, 16, 22], - [10, 13, 14, 17, 24], - [18, 21, 23, 26, 30] -] - -Given target = 5, return true. -Given target = 20, return false. -``` - -## 解题思路 - -要求时间复杂度 O(M + N),空间复杂度 O(1)。 - -该二维数组中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。 - -

- -```java -public boolean Find(int target, int[][] matrix) { - if (matrix == null || matrix.length == 0 || matrix[0].length == 0) - return false; - int rows = matrix.length, cols = matrix[0].length; - int r = 0, c = cols - 1; // 从右上角开始 - while (r <= rows - 1 && c >= 0) { - if (target == matrix[r][c]) - return true; - else if (target > matrix[r][c]) - r++; - else - c--; - } - return false; -} -``` - -# 5. 替换空格 - -[NowCoder](https://www.nowcoder.com/practice/4060ac7e3e404ad1a894ef3e17650423?tpId=13&tqId=11155&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - - -将一个字符串中的空格替换成 "%20"。 - -```text -Input: -"A B" - -Output: -"A%20B" -``` - -## 解题思路 - -在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。 - -令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。 - -从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。 - -

- -```java -public String replaceSpace(StringBuffer str) { - int P1 = str.length() - 1; - for (int i = 0; i <= P1; i++) - if (str.charAt(i) == ' ') - str.append(" "); - - int P2 = str.length() - 1; - while (P1 >= 0 && P2 > P1) { - char c = str.charAt(P1--); - if (c == ' ') { - str.setCharAt(P2--, '0'); - str.setCharAt(P2--, '2'); - str.setCharAt(P2--, '%'); - } else { - str.setCharAt(P2--, c); - } - } - return str.toString(); -} -``` - -# 6. 从尾到头打印链表 - -[NowCoder](https://www.nowcoder.com/practice/d0267f7f55b3412ba93bd35cfa8e8035?tpId=13&tqId=11156&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -从尾到头反过来打印出每个结点的值。 - -

- -## 解题思路 - -### 使用递归 - -

- -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - ArrayList ret = new ArrayList<>(); - if (listNode != null) { - ret.addAll(printListFromTailToHead(listNode.next)); - ret.add(listNode.val); - } - return ret; -} -``` - -### 使用头插法 - -利用链表头插法为逆序的特点。 - -头结点和第一个节点的区别: - -- 头结点是在头插法中使用的一个额外节点,这个节点不存储值; -- 第一个节点就是链表的第一个真正存储值的节点。 - -

- -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - // 头插法构建逆序链表 - ListNode head = new ListNode(-1); - while (listNode != null) { - ListNode memo = listNode.next; - listNode.next = head.next; - head.next = listNode; - listNode = memo; - } - // 构建 ArrayList - ArrayList ret = new ArrayList<>(); - head = head.next; - while (head != null) { - ret.add(head.val); - head = head.next; - } - return ret; -} -``` - -### 使用栈 - -

- -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - Stack stack = new Stack<>(); - while (listNode != null) { - stack.add(listNode.val); - listNode = listNode.next; - } - ArrayList ret = new ArrayList<>(); - while (!stack.isEmpty()) - ret.add(stack.pop()); - return ret; -} -``` - -# 7. 重建二叉树 - -[NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 - -```html -preorder = [3,9,20,15,7] -inorder = [9,3,15,20,7] -``` - -

- -## 解题思路 - -前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。 - -

- -```java -// 缓存中序遍历数组每个值对应的索引 -private Map indexForInOrders = new HashMap<>(); - -public TreeNode reConstructBinaryTree(int[] pre, int[] in) { - for (int i = 0; i < in.length; i++) - indexForInOrders.put(in[i], i); - return reConstructBinaryTree(pre, 0, pre.length - 1, 0); -} - -private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) { - if (preL > preR) - return null; - TreeNode root = new TreeNode(pre[preL]); - int inIndex = indexForInOrders.get(root.val); - int leftTreeSize = inIndex - inL; - root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL); - root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1); - return root; -} -``` - -# 8. 二叉树的下一个结点 - -[NowCoder](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 - -```java -public class TreeLinkNode { - - int val; - TreeLinkNode left = null; - TreeLinkNode right = null; - TreeLinkNode next = null; - - TreeLinkNode(int val) { - this.val = val; - } -} -``` - -## 解题思路 - -① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; - -

- -② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 - -

- -```java -public TreeLinkNode GetNext(TreeLinkNode pNode) { - if (pNode.right != null) { - TreeLinkNode node = pNode.right; - while (node.left != null) - node = node.left; - return node; - } else { - while (pNode.next != null) { - TreeLinkNode parent = pNode.next; - if (parent.left == pNode) - return parent; - pNode = pNode.next; - } - } - return null; -} -``` - -# 9. 用两个栈实现队列 - -[NowCoder](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。 - -## 解题思路 - -in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。 - -

- - -```java -Stack in = new Stack(); -Stack out = new Stack(); - -public void push(int node) { - in.push(node); -} - -public int pop() throws Exception { - if (out.isEmpty()) - while (!in.isEmpty()) - out.push(in.pop()); - - if (out.isEmpty()) - throw new Exception("queue is empty"); - - return out.pop(); -} -``` - -# 10.1 斐波那契数列 - -[NowCoder](https://www.nowcoder.com/practice/c6c7742f5ba7442aada113136ddea0c3?tpId=13&tqId=11160&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -求斐波那契数列的第 n 项,n <= 39。 - - - -

- -## 解题思路 - -如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。 - -

- - -递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。 - -```java -public int Fibonacci(int n) { - if (n <= 1) - return n; - int[] fib = new int[n + 1]; - fib[1] = 1; - for (int i = 2; i <= n; i++) - fib[i] = fib[i - 1] + fib[i - 2]; - return fib[n]; -} -``` - -考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。 - -```java -public int Fibonacci(int n) { - if (n <= 1) - return n; - int pre2 = 0, pre1 = 1; - int fib = 0; - for (int i = 2; i <= n; i++) { - fib = pre2 + pre1; - pre2 = pre1; - pre1 = fib; - } - return fib; -} -``` - -由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。 - -```java -public class Solution { - - private int[] fib = new int[40]; - - public Solution() { - fib[1] = 1; - fib[2] = 2; - for (int i = 2; i < fib.length; i++) - fib[i] = fib[i - 1] + fib[i - 2]; - } - - public int Fibonacci(int n) { - return fib[n]; - } -} -``` - -# 10.2 矩形覆盖 - -[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.3 跳台阶 - -[NowCoder](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 - -

- -## 解题思路 - -```java -public int JumpFloor(int n) { - if (n <= 2) - return n; - int pre2 = 1, pre1 = 2; - int result = 1; - for (int i = 2; 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) - -## 题目描述 - -一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 - -

- -## 解题思路 - -### 动态规划 - -```java -public int JumpFloorII(int target) { - int[] dp = new int[target]; - Arrays.fill(dp, 1); - for (int i = 1; i < target; i++) - for (int j = 0; j < i; j++) - dp[i] += dp[j]; - return dp[target - 1]; -} -``` - -### 数学推导 - -跳上 n-1 级台阶,可以从 n-2 级跳 1 级上去,也可以从 n-3 级跳 2 级上去...,那么 - -``` -f(n-1) = f(n-2) + f(n-3) + ... + f(0) -``` - -同样,跳上 n 级台阶,可以从 n-1 级跳 1 级上去,也可以从 n-2 级跳 2 级上去... ,那么 - -``` -f(n) = f(n-1) + f(n-2) + ... + f(0) -``` - -综上可得 - -``` -f(n) - f(n-1) = f(n-1) -``` - -即 - -``` -f(n) = 2*f(n-1) -``` - -所以 f(n) 是一个等比数列 - -```source-java -public int JumpFloorII(int target) { - return (int) Math.pow(2, target - 1); -} -``` - - -# 11. 旋转数组的最小数字 - -[NowCoder](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&tqId=11159&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 - -例如数组 {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。 - -```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],那么此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。 - -```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[l] == nums[m] && nums[m] == nums[h]) - return minNumber(nums, l, h); - else if (nums[m] <= nums[h]) - h = m; - else - l = m + 1; - } - return nums[l]; -} - -private int minNumber(int[] nums, int l, int h) { - for (int i = l; i < h; i++) - if (nums[i] > nums[i + 1]) - return nums[i + 1]; - return nums[l]; -} -``` - -# 12. 矩阵中的路径 - -[NowCoder](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 - -例如下面的矩阵包含了一条 bfce 路径。 - -

- -## 解题思路 - -```java -private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; -private int rows; -private int cols; - -public boolean hasPath(char[] array, int rows, int cols, char[] str) { - if (rows == 0 || cols == 0) - return false; - this.rows = rows; - this.cols = cols; - boolean[][] marked = new boolean[rows][cols]; - char[][] matrix = buildMatrix(array); - for (int i = 0; i < rows; i++) - for (int j = 0; j < cols; j++) - if (backtracking(matrix, str, marked, 0, i, j)) - return true; - return false; -} - -private boolean backtracking(char[][] matrix, char[] str, boolean[][] marked, int pathLen, int r, int c) { - if (pathLen == str.length) - return true; - if (r < 0 || r >= rows || c < 0 || c >= cols || matrix[r][c] != str[pathLen] || marked[r][c]) - return false; - marked[r][c] = true; - for (int[] n : next) - if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1])) - return true; - marked[r][c] = false; - return false; -} - -private char[][] buildMatrix(char[] array) { - char[][] matrix = new char[rows][cols]; - for (int i = 0, idx = 0; i < rows; i++) - for (int j = 0; j < cols; j++) - matrix[i][j] = array[idx++]; - return matrix; -} -``` - -# 13. 机器人的运动范围 - -[NowCoder](https://www.nowcoder.com/practice/6e5207314b5241fb83f2329e89fdecc8?tpId=13&tqId=11219&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。 - -例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? - -## 解题思路 - -```java -private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; -private int cnt = 0; -private int rows; -private int cols; -private int threshold; -private int[][] digitSum; - -public int movingCount(int threshold, int rows, int cols) { - this.rows = rows; - this.cols = cols; - this.threshold = threshold; - initDigitSum(); - boolean[][] marked = new boolean[rows][cols]; - dfs(marked, 0, 0); - return cnt; -} - -private void dfs(boolean[][] marked, int r, int c) { - if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c]) - return; - marked[r][c] = true; - if (this.digitSum[r][c] > this.threshold) - return; - cnt++; - for (int[] n : next) - dfs(marked, r + n[0], c + n[1]); -} - -private void initDigitSum() { - int[] digitSumOne = new int[Math.max(rows, cols)]; - for (int i = 0; i < digitSumOne.length; i++) { - int n = i; - while (n > 0) { - digitSumOne[i] += n % 10; - n /= 10; - } - } - this.digitSum = new int[rows][cols]; - for (int i = 0; i < this.rows; i++) - for (int j = 0; j < this.cols; j++) - this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j]; -} -``` - -# 14. 剪绳子 - -[Leetcode](https://leetcode.com/problems/integer-break/description/) - -## 题目描述 - -把一根绳子剪成多段,并且使得每段的长度乘积最大。 - -```html -n = 2 -return 1 (2 = 1 + 1) - -n = 10 -return 36 (10 = 3 + 3 + 4) -``` - -## 解题思路 - -### 贪心 - -尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 - -证明:当 n >= 5 时,3(n - 3) - n = 2n - 9 > 0,且 2(n - 2) - n = n - 4 > 0。因此在 n >= 5 的情况下,将绳子剪成一段为 2 或者 3,得到的乘积会更大。又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0,所以剪成一段长度为 3 比长度为 2 得到的乘积更大。 - -```java -public int integerBreak(int n) { - if (n < 2) - return 0; - if (n == 2) - return 1; - if (n == 3) - return 2; - int timesOf3 = n / 3; - if (n - timesOf3 * 3 == 1) - timesOf3--; - int timesOf2 = (n - timesOf3 * 3) / 2; - return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2)); -} -``` - -### 动态规划 - -```java -public int integerBreak(int n) { - int[] dp = new int[n + 1]; - dp[1] = 1; - for (int i = 2; i <= n; i++) - for (int j = 1; j < i; j++) - dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); - return dp[n]; -} -``` - -# 15. 二进制中 1 的个数 - -[NowCoder](https://www.nowcoder.com/practice/8ee967e43c2c4ec193b040ea7fbb10b8?tpId=13&tqId=11164&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个整数,输出该数二进制表示中 1 的个数。 - -### n&(n-1) - -该位运算去除 n 的位级表示中最低的那一位。 - -``` -n : 10110100 -n-1 : 10110011 -n&(n-1) : 10110000 -``` - -时间复杂度:O(M),其中 M 表示 1 的个数。 - - -```java -public int NumberOf1(int n) { - int cnt = 0; - while (n != 0) { - cnt++; - n &= (n - 1); - } - return cnt; -} -``` - - -### 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 次方。 - -## 解题思路 - -下面的讨论中 x 代表 base,n 代表 exponent。 - - - -

- -因为 (x\*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 - -```java -public double Power(double base, int exponent) { - if (exponent == 0) - return 1; - if (exponent == 1) - return base; - boolean isNegative = false; - if (exponent < 0) { - exponent = -exponent; - isNegative = true; - } - double pow = Power(base * base, exponent / 2); - if (exponent % 2 != 0) - pow = pow * base; - return isNegative ? 1 / pow : pow; -} -``` - -# 17. 打印从 1 到最大的 n 位数 - -## 题目描述 - -输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。 - -## 解题思路 - -由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。 - -使用回溯法得到所有的数。 - -```java -public void print1ToMaxOfNDigits(int n) { - if (n <= 0) - return; - char[] number = new char[n]; - print1ToMaxOfNDigits(number, 0); -} - -private void print1ToMaxOfNDigits(char[] number, int digit) { - if (digit == number.length) { - printNumber(number); - return; - } - for (int i = 0; i < 10; i++) { - number[digit] = (char) (i + '0'); - print1ToMaxOfNDigits(number, digit + 1); - } -} - -private void printNumber(char[] number) { - int index = 0; - while (index < number.length && number[index] == '0') - index++; - while (index < number.length) - System.out.print(number[index++]); - System.out.println(); -} -``` - -# 18.1 在 O(1) 时间内删除链表节点 - -## 解题思路 - -① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。 - -

- -② 如果链表只有一个节点,那么直接 - -② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。 - -

- -综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N \~ 2,因此该算法的平均时间复杂度为 O(1)。 - -```java -public ListNode deleteNode(ListNode head, ListNode tobeDelete) { - if (head == null || tobeDelete == null) - return null; - if (tobeDelete.next != null) { - // 要删除的节点不是尾节点 - ListNode next = tobeDelete.next; - tobeDelete.val = next.val; - tobeDelete.next = next.next; - } else { - if (head == tobeDelete) - // 只有一个节点 - head = null; - else { - ListNode cur = head; - while (cur.next != tobeDelete) - cur = cur.next; - cur.next = null; - } - } - return head; -} -``` - -# 18.2 删除链表中重复的结点 - -[NowCoder](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -

- -## 解题描述 - -```java -public ListNode deleteDuplication(ListNode pHead) { - if (pHead == null || pHead.next == null) - return pHead; - ListNode next = pHead.next; - if (pHead.val == next.val) { - while (next != null && pHead.val == next.val) - next = next.next; - return deleteDuplication(next); - } else { - pHead.next = deleteDuplication(pHead.next); - return pHead; - } -} -``` - -# 19. 正则表达式匹配 - -[NowCoder](https://www.nowcoder.com/practice/45327ae22b7b413ea21df13ee7d6429c?tpId=13&tqId=11205&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。 - -在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配。 - -## 解题思路 - -应该注意到,'.' 是用来当做一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而把它当成重复前面字符一次。 - -```java -public boolean match(char[] str, char[] pattern) { - - int m = str.length, n = pattern.length; - boolean[][] dp = new boolean[m + 1][n + 1]; - - dp[0][0] = true; - for (int i = 1; i <= n; i++) - if (pattern[i - 1] == '*') - dp[0][i] = dp[0][i - 2]; - - for (int i = 1; i <= m; i++) - for (int j = 1; j <= n; j++) - if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.') - dp[i][j] = dp[i - 1][j - 1]; - else if (pattern[j - 1] == '*') - if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.') { - dp[i][j] |= dp[i][j - 1]; // a* counts as single a - dp[i][j] |= dp[i - 1][j]; // a* counts as multiple a - dp[i][j] |= dp[i][j - 2]; // a* counts as empty - } else - dp[i][j] = dp[i][j - 2]; // a* only counts as empty - - return dp[m][n]; -} -``` - -# 20. 表示数值的字符串 - -[NowCoder](https://www.nowcoder.com/practice/6f8c901d091949a5837e24bb82a731f2?tpId=13&tqId=11206&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -```html -true - -"+100" -"5e2" -"-123" -"3.1416" -"-1E-16" - -false - -"12e" -"1a3.14" -"1.2.3" -"+-5" -"12e+4.3" -``` - - -## 解题思路 - -使用正则表达式进行匹配。 - -```html -[] : 字符集合 -() : 分组 -? : 重复 0 ~ 1 -+ : 重复 1 ~ n -* : 重复 0 ~ n -. : 任意字符 -\\. : 转义后的 . -\\d : 数字 -``` - -```java -public boolean isNumeric(char[] str) { - if (str == null || str.length == 0) - return false; - return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); -} -``` - -# 21. 调整数组顺序使奇数位于偶数前面 - -[NowCoder](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 - -

- -## 解题思路 - -```java -public void reOrderArray(int[] nums) { - // 奇数个数 - int oddCnt = 0; - for (int val : nums) - if (val % 2 == 1) - oddCnt++; - int[] copy = nums.clone(); - int i = 0, j = oddCnt; - for (int num : copy) { - if (num % 2 == 1) - nums[i++] = num; - else - nums[j++] = num; - } -} -``` - -# 22. 链表中倒数第 K 个结点 - -[NowCoder](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 解题思路 - -设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。 - -

- -```java -public ListNode FindKthToTail(ListNode head, int k) { - if (head == null) - return null; - ListNode P1 = head; - while (P1 != null && k-- > 0) - P1 = P1.next; - if (k > 0) - return null; - ListNode P2 = head; - while (P1 != null) { - P1 = P1.next; - P2 = P2.next; - } - return P2; -} -``` - -# 23. 链表中环的入口结点 - -[NowCoder](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。 - -## 解题思路 - -使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。 - -在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。 - -

- - -```java -public ListNode EntryNodeOfLoop(ListNode pHead) { - if (pHead == null || pHead.next == null) - return null; - ListNode slow = pHead, fast = pHead; - do { - fast = fast.next.next; - slow = slow.next; - } while (slow != fast); - fast = pHead; - while (slow != fast) { - slow = slow.next; - fast = fast.next; - } - return slow; -} -``` - -# 24. 反转链表 - -[NowCoder](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 解题思路 - -### 递归 - -```java -public ListNode ReverseList(ListNode head) { - if (head == null || head.next == null) - return head; - ListNode next = head.next; - head.next = null; - ListNode newHead = ReverseList(next); - next.next = head; - return newHead; -} -``` - -### 迭代 - -```java -public ListNode ReverseList(ListNode head) { - ListNode newList = new ListNode(-1); - while (head != null) { - ListNode next = head.next; - head.next = newList.next; - newList.next = head; - head = next; - } - return newList.next; -} -``` - -# 25. 合并两个排序的链表 - -[NowCoder](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -

- -## 解题思路 - -### 递归 - -```java -public ListNode Merge(ListNode list1, ListNode list2) { - if (list1 == null) - return list2; - if (list2 == null) - return list1; - if (list1.val <= list2.val) { - list1.next = Merge(list1.next, list2); - return list1; - } else { - list2.next = Merge(list1, list2.next); - return list2; - } -} -``` - -### 迭代 - -```java -public ListNode Merge(ListNode list1, ListNode list2) { - ListNode head = new ListNode(-1); - ListNode cur = head; - while (list1 != null && list2 != null) { - if (list1.val <= list2.val) { - cur.next = list1; - list1 = list1.next; - } else { - cur.next = list2; - list2 = list2.next; - } - cur = cur.next; - } - if (list1 != null) - cur.next = list1; - if (list2 != null) - cur.next = list2; - return head.next; -} -``` - -# 26. 树的子结构 - -[NowCoder](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -

- -## 解题思路 - -```java -public boolean HasSubtree(TreeNode root1, TreeNode root2) { - if (root1 == null || root2 == null) - return false; - return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2); -} - -private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) { - if (root2 == null) - return true; - if (root1 == null) - return false; - if (root1.val != root2.val) - return false; - return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right); -} -``` - -# 27. 二叉树的镜像 - -[NowCoder](https://www.nowcoder.com/practice/564f4c26aa584921bc75623e48ca3011?tpId=13&tqId=11171&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -

- -## 解题思路 - -```java -public void Mirror(TreeNode root) { - if (root == null) - return; - swap(root); - Mirror(root.left); - Mirror(root.right); -} - -private void swap(TreeNode root) { - TreeNode t = root.left; - root.left = root.right; - root.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) - -## 题目描述 - -

- -## 解题思路 - -```java -boolean isSymmetrical(TreeNode pRoot) { - if (pRoot == null) - return true; - return isSymmetrical(pRoot.left, pRoot.right); -} - -boolean isSymmetrical(TreeNode t1, TreeNode t2) { - if (t1 == null && t2 == null) - return true; - if (t1 == null || t2 == null) - return false; - if (t1.val != t2.val) - return false; - return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left); -} -``` - -# 29. 顺时针打印矩阵 - -[NowCoder](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 - -

- -## 解题思路 - -```java -public ArrayList printMatrix(int[][] matrix) { - ArrayList ret = new ArrayList<>(); - int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1; - while (r1 <= r2 && c1 <= c2) { - for (int i = c1; i <= c2; i++) - ret.add(matrix[r1][i]); - for (int i = r1 + 1; i <= r2; i++) - ret.add(matrix[i][c2]); - if (r1 != r2) - for (int i = c2 - 1; i >= c1; i--) - ret.add(matrix[r2][i]); - if (c1 != c2) - for (int i = r2 - 1; i > r1; i--) - ret.add(matrix[i][c1]); - r1++; r2--; c1++; c2--; - } - return ret; -} -``` - -# 30. 包含 min 函数的栈 - -[NowCoder](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。 - -## 解题思路 - -```java -private Stack dataStack = new Stack<>(); -private Stack minStack = new Stack<>(); - -public void push(int node) { - dataStack.push(node); - minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node)); -} - -public void pop() { - dataStack.pop(); - minStack.pop(); -} - -public int top() { - return dataStack.peek(); -} - -public int min() { - return minStack.peek(); -} -``` - -# 31. 栈的压入、弹出序列 - -[NowCoder](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&tqId=11174&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。 - -例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。 - -## 解题思路 - -使用一个栈来模拟压入弹出操作。 - -```java -public boolean IsPopOrder(int[] pushSequence, int[] popSequence) { - int n = pushSequence.length; - Stack stack = new Stack<>(); - for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) { - stack.push(pushSequence[pushIndex]); - while (popIndex < n && !stack.isEmpty() - && stack.peek() == popSequence[popIndex]) { - stack.pop(); - popIndex++; - } - } - return stack.isEmpty(); -} -``` - -# 32.1 从上往下打印二叉树 - -[NowCoder](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -从上往下打印出二叉树的每个节点,同层节点从左至右打印。 - -例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7 - -

- -## 解题思路 - -使用队列来进行层次遍历。 - -不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 - -```java -public ArrayList PrintFromTopToBottom(TreeNode root) { - Queue queue = new LinkedList<>(); - ArrayList ret = new ArrayList<>(); - queue.add(root); - while (!queue.isEmpty()) { - int cnt = queue.size(); - while (cnt-- > 0) { - TreeNode t = queue.poll(); - if (t == null) - continue; - ret.add(t.val); - queue.add(t.left); - queue.add(t.right); - } - } - return ret; -} -``` - -# 32.2 把二叉树打印成多行 - -[NowCoder](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -和上题几乎一样。 - -## 解题思路 - -```java -ArrayList> Print(TreeNode pRoot) { - ArrayList> ret = new ArrayList<>(); - Queue queue = new LinkedList<>(); - queue.add(pRoot); - while (!queue.isEmpty()) { - ArrayList list = new ArrayList<>(); - int cnt = queue.size(); - while (cnt-- > 0) { - TreeNode node = queue.poll(); - if (node == null) - continue; - list.add(node.val); - queue.add(node.left); - queue.add(node.right); - } - if (list.size() != 0) - ret.add(list); - } - return ret; -} -``` - -# 32.3 按之字形顺序打印二叉树 - -[NowCoder](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 - -## 解题思路 - -```java -public ArrayList> Print(TreeNode pRoot) { - ArrayList> ret = new ArrayList<>(); - Queue queue = new LinkedList<>(); - queue.add(pRoot); - boolean reverse = false; - while (!queue.isEmpty()) { - ArrayList list = new ArrayList<>(); - int cnt = queue.size(); - while (cnt-- > 0) { - TreeNode node = queue.poll(); - if (node == null) - continue; - list.add(node.val); - queue.add(node.left); - queue.add(node.right); - } - if (reverse) - Collections.reverse(list); - reverse = !reverse; - if (list.size() != 0) - ret.add(list); - } - return ret; -} -``` - -# 33. 二叉搜索树的后序遍历序列 - -[NowCoder](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。 - -例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树。 - -

- -## 解题思路 - -```java -public boolean VerifySquenceOfBST(int[] sequence) { - if (sequence == null || sequence.length == 0) - return false; - return verify(sequence, 0, sequence.length - 1); -} - -private boolean verify(int[] sequence, int first, int last) { - if (last - first <= 1) - return true; - int rootVal = sequence[last]; - int cutIndex = first; - while (cutIndex < last && sequence[cutIndex] <= rootVal) - cutIndex++; - for (int i = cutIndex; i < last; i++) - if (sequence[i] < rootVal) - return false; - return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1); -} -``` - -# 34. 二叉树中和为某一值的路径 - -[NowCoder](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 - -下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12 - -

- -## 解题思路 - -```java -private ArrayList> ret = new ArrayList<>(); - -public ArrayList> FindPath(TreeNode root, int target) { - backtracking(root, target, new ArrayList<>()); - return ret; -} - -private void backtracking(TreeNode node, int target, ArrayList path) { - if (node == null) - return; - path.add(node.val); - target -= node.val; - if (target == 0 && node.left == null && node.right == null) { - ret.add(new ArrayList<>(path)); - } else { - backtracking(node.left, target, path); - backtracking(node.right, target, path); - } - path.remove(path.size() - 1); -} -``` - -# 35. 复杂链表的复制 - -[NowCoder](https://www.nowcoder.com/practice/f836b2c43afc4b35ad6adc41ec941dba?tpId=13&tqId=11178&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。 - -```java -public class RandomListNode { - int label; - RandomListNode next = null; - RandomListNode random = null; - - RandomListNode(int label) { - this.label = label; - } -} -``` - -

- -## 解题思路 - -第一步,在每个节点的后面插入复制的节点。 - -

- -第二步,对复制节点的 random 链接进行赋值。 - -

- -第三步,拆分。 - -

- -```java -public RandomListNode Clone(RandomListNode pHead) { - if (pHead == null) - return null; - // 插入新节点 - RandomListNode cur = pHead; - while (cur != null) { - RandomListNode clone = new RandomListNode(cur.label); - clone.next = cur.next; - cur.next = clone; - cur = clone.next; - } - // 建立 random 链接 - cur = pHead; - while (cur != null) { - RandomListNode clone = cur.next; - if (cur.random != null) - clone.random = cur.random.next; - cur = clone.next; - } - // 拆分 - cur = pHead; - RandomListNode pCloneHead = pHead.next; - while (cur.next != null) { - RandomListNode next = cur.next; - cur.next = next.next; - cur = next; - } - return pCloneHead; -} -``` - -# 36. 二叉搜索树与双向链表 - -[NowCoder](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 - -

- -## 解题思路 - -```java -private TreeNode pre = null; -private TreeNode head = null; - -public TreeNode Convert(TreeNode root) { - inOrder(root); - return head; -} - -private void inOrder(TreeNode node) { - if (node == null) - return; - inOrder(node.left); - node.left = pre; - if (pre != null) - pre.right = node; - pre = node; - if (head == null) - head = node; - inOrder(node.right); -} -``` - -# 37. 序列化二叉树 - -[NowCoder](https://www.nowcoder.com/practice/cf7e25aa97c04cc1a68c8f040e71fb84?tpId=13&tqId=11214&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -请实现两个函数,分别用来序列化和反序列化二叉树。 - -## 解题思路 - -```java -private String deserializeStr; - -public String Serialize(TreeNode root) { - if (root == null) - return "#"; - return root.val + " " + Serialize(root.left) + " " + Serialize(root.right); -} - -public TreeNode Deserialize(String str) { - deserializeStr = str; - return Deserialize(); -} - -private TreeNode Deserialize() { - if (deserializeStr.length() == 0) - return null; - int index = deserializeStr.indexOf(" "); - String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index); - deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1); - if (node.equals("#")) - return null; - int val = Integer.valueOf(node); - TreeNode t = new TreeNode(val); - t.left = Deserialize(); - t.right = Deserialize(); - return t; -} -``` - -# 38. 字符串的排列 - -[NowCoder](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。 - -## 解题思路 - -```java -private ArrayList ret = new ArrayList<>(); - -public ArrayList Permutation(String str) { - if (str.length() == 0) - return ret; - char[] chars = str.toCharArray(); - Arrays.sort(chars); - backtracking(chars, new boolean[chars.length], new StringBuilder()); - return ret; -} - -private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) { - if (s.length() == chars.length) { - ret.add(s.toString()); - return; - } - for (int i = 0; i < chars.length; i++) { - if (hasUsed[i]) - continue; - if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */ - continue; - hasUsed[i] = true; - s.append(chars[i]); - backtracking(chars, hasUsed, s); - s.deleteCharAt(s.length() - 1); - hasUsed[i] = false; - } -} -``` - -# 39. 数组中出现次数超过一半的数字 - -[NowCoder](https://www.nowcoder.com/practice/e8a1b01a2df14cb2b228b30ee6a92163?tpId=13&tqId=11181&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 解题思路 - -多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。 - -使用 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) { - int majority = nums[0]; - for (int i = 1, cnt = 1; i < nums.length; i++) { - cnt = nums[i] == majority ? cnt + 1 : cnt - 1; - if (cnt == 0) { - majority = nums[i]; - cnt = 1; - } - } - int cnt = 0; - for (int val : nums) - if (val == majority) - cnt++; - return cnt > nums.length / 2 ? majority : 0; -} -``` - -# 40. 最小的 K 个数 - -[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 解题思路 - -### 快速选择 - -- 复杂度:O(N) + O(1) -- 只有当允许修改数组元素时才可以使用 - -快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。 - -```java -public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { - ArrayList ret = new ArrayList<>(); - if (k > nums.length || k <= 0) - return ret; - findKthSmallest(nums, k - 1); - /* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */ - for (int i = 0; i < k; i++) - ret.add(nums[i]); - return ret; -} - -public void findKthSmallest(int[] nums, int k) { - int l = 0, h = nums.length - 1; - while (l < h) { - int j = partition(nums, l, h); - if (j == k) - break; - if (j > k) - h = j - 1; - else - l = j + 1; - } -} - -private int partition(int[] nums, int l, int h) { - int p = nums[l]; /* 切分元素 */ - int i = l, j = h + 1; - while (true) { - while (i != h && nums[++i] < p) ; - while (j != l && nums[--j] > p) ; - if (i >= j) - break; - swap(nums, i, j); - } - swap(nums, l, j); - return j; -} - -private void swap(int[] nums, int i, int j) { - int t = nums[i]; - nums[i] = nums[j]; - nums[j] = t; -} -``` - -### 大小为 K 的最小堆 - -- 复杂度:O(NlogK) + O(K) -- 特别适合处理海量数据 - -应该使用大顶堆来维护最小堆,而不能直接创建一个小顶堆并设置一个大小,企图让小顶堆中的元素都是最小元素。 - -维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。 - -```java -public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { - if (k > nums.length || k <= 0) - return new ArrayList<>(); - PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1); - for (int num : nums) { - maxHeap.add(num); - if (maxHeap.size() > k) - maxHeap.poll(); - } - return new ArrayList<>(maxHeap); -} -``` - -# 41.1 数据流中的中位数 - -[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 - -## 解题思路 - -```java -/* 大顶堆,存储左半边元素 */ -private PriorityQueue left = new PriorityQueue<>((o1, o2) -> o2 - o1); -/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */ -private PriorityQueue right = new PriorityQueue<>(); -/* 当前数据流读入的元素个数 */ -private int N = 0; - -public void Insert(Integer val) { - /* 插入要保证两个堆存于平衡状态 */ - if (N % 2 == 0) { - /* N 为偶数的情况下插入到右半边。 - * 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大, - * 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */ - left.add(val); - right.add(left.poll()); - } else { - right.add(val); - left.add(right.poll()); - } - N++; -} - -public Double GetMedian() { - if (N % 2 == 0) - return (left.peek() + right.peek()) / 2.0; - else - return (double) right.peek(); -} -``` - -# 41.2 字符流中第一个不重复的字符 - -[NowCoder](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g"。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 "l"。 - -## 解题思路 - -```java -private int[] cnts = new int[256]; -private Queue queue = new LinkedList<>(); - -public void Insert(char ch) { - cnts[ch]++; - queue.add(ch); - while (!queue.isEmpty() && cnts[queue.peek()] > 1) - queue.poll(); -} - -public char FirstAppearingOnce() { - return queue.isEmpty() ? '#' : queue.peek(); -} -``` - -# 42. 连续子数组的最大和 - -[NowCoder](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 - -## 解题思路 - -```java -public int FindGreatestSumOfSubArray(int[] nums) { - if (nums == null || nums.length == 0) - return 0; - int greatestSum = Integer.MIN_VALUE; - int sum = 0; - for (int val : nums) { - sum = sum <= 0 ? val : sum + val; - greatestSum = Math.max(greatestSum, sum); - } - return greatestSum; -} -``` - -# 43. 从 1 到 n 整数中 1 出现的次数 - -[NowCoder](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&tqId=11184&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 解题思路 - -```java -public int NumberOf1Between1AndN_Solution(int n) { - int cnt = 0; - for (int m = 1; m <= n; m *= 10) { - int a = n / m, b = n % m; - cnt += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0); - } - return cnt; -} -``` - -> [Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython) - -# 44. 数字序列中的某一位数字 - -## 题目描述 - -数字以 0123456789101112131415... 的格式序列化到一个字符串中,求这个字符串的第 index 位。 - -## 解题思路 - -```java -public int getDigitAtIndex(int index) { - if (index < 0) - return -1; - int place = 1; // 1 表示个位,2 表示 十位... - while (true) { - int amount = getAmountOfPlace(place); - int totalAmount = amount * place; - if (index < totalAmount) - return getDigitAtIndex(index, place); - index -= totalAmount; - place++; - } -} - -/** - * place 位数的数字组成的字符串长度 - * 10, 90, 900, ... - */ -private int getAmountOfPlace(int place) { - if (place == 1) - return 10; - return (int) Math.pow(10, place - 1) * 9; -} - -/** - * place 位数的起始数字 - * 0, 10, 100, ... - */ -private int getBeginNumberOfPlace(int place) { - if (place == 1) - return 0; - return (int) Math.pow(10, place - 1); -} - -/** - * 在 place 位数组成的字符串中,第 index 个数 - */ -private int getDigitAtIndex(int index, int place) { - int beginNumber = getBeginNumberOfPlace(place); - int shiftNumber = index / place; - String number = (beginNumber + shiftNumber) + ""; - int count = index % place; - return number.charAt(count) - '0'; -} -``` - -# 45. 把数组排成最小的数 - -[NowCoder](https://www.nowcoder.com/practice/8fecd3f8ba334add803bf2a06af1b993?tpId=13&tqId=11185&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。 - -## 解题思路 - -可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。 - -```java -public String PrintMinNumber(int[] numbers) { - if (numbers == null || numbers.length == 0) - return ""; - int n = numbers.length; - String[] nums = new String[n]; - for (int i = 0; i < n; i++) - nums[i] = numbers[i] + ""; - Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1)); - String ret = ""; - for (String str : nums) - ret += str; - return ret; -} -``` - -# 46. 把数字翻译成字符串 - -[Leetcode](https://leetcode.com/problems/decode-ways/description/) - -## 题目描述 - -给定一个数字,按照如下规则翻译成字符串:1 翻译成“a”,2 翻译成“b”... 26 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 abbeh,lbeh,aveh,abyh,lyh。实现一个函数,用来计算一个数字有多少种不同的翻译方法。 - -## 解题思路 - -```java -public int numDecodings(String s) { - if (s == null || s.length() == 0) - return 0; - int n = s.length(); - int[] dp = new int[n + 1]; - dp[0] = 1; - dp[1] = s.charAt(0) == '0' ? 0 : 1; - for (int i = 2; i <= n; i++) { - int one = Integer.valueOf(s.substring(i - 1, i)); - if (one != 0) - dp[i] += dp[i - 1]; - if (s.charAt(i - 2) == '0') - continue; - int two = Integer.valueOf(s.substring(i - 2, i)); - if (two <= 26) - dp[i] += dp[i - 2]; - } - return dp[n]; -} -``` - -# 47. 礼物的最大价值 - -[NowCoder](https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab) - -## 题目描述 - -在一个 m\*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于 0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘 - -``` -1 10 3 8 -12 2 9 6 -5 7 4 11 -3 7 16 5 -``` - -礼物的最大价值为 1+12+5+7+7+16+5=53。 - -## 解题思路 - -应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。 - -```java -public int getMost(int[][] values) { - if (values == null || values.length == 0 || values[0].length == 0) - return 0; - int n = values[0].length; - int[] dp = new int[n]; - for (int[] value : values) { - dp[0] += value[0]; - for (int i = 1; i < n; i++) - dp[i] = Math.max(dp[i], dp[i - 1]) + value[i]; - } - return dp[n - 1]; -} -``` - -# 48. 最长不含重复字符的子字符串 - -## 题目描述 - -输入一个字符串(只包含 a\~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。 - -## 解题思路 - -```java -public int longestSubStringWithoutDuplication(String str) { - int curLen = 0; - int maxLen = 0; - int[] preIndexs = new int[26]; - Arrays.fill(preIndexs, -1); - for (int curI = 0; curI < str.length(); curI++) { - int c = str.charAt(curI) - 'a'; - int preI = preIndexs[c]; - if (preI == -1 || curI - preI > curLen) { - curLen++; - } else { - maxLen = Math.max(maxLen, curLen); - curLen = curI - preI; - } - preIndexs[c] = curI; - } - maxLen = Math.max(maxLen, curLen); - return maxLen; -} -``` - -# 49. 丑数 - -[NowCoder](https://www.nowcoder.com/practice/6aa9e04fc3794f68acf8778237ba065b?tpId=13&tqId=11186&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。 - -## 解题思路 - -```java -public int GetUglyNumber_Solution(int N) { - if (N <= 6) - return N; - int i2 = 0, i3 = 0, i5 = 0; - int[] dp = new int[N]; - dp[0] = 1; - for (int i = 1; i < N; i++) { - int next2 = dp[i2] * 2, next3 = dp[i3] * 3, next5 = dp[i5] * 5; - dp[i] = Math.min(next2, Math.min(next3, next5)); - if (dp[i] == next2) - i2++; - if (dp[i] == next3) - i3++; - if (dp[i] == next5) - i5++; - } - return dp[N - 1]; -} -``` - -# 50. 第一个只出现一次的字符位置 - -[NowCoder](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -在一个字符串中找到第一个只出现一次的字符,并返回它的位置。 - -## 解题思路 - -最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。 - -```java -public int FirstNotRepeatingChar(String str) { - int[] cnts = new int[256]; - for (int i = 0; i < str.length(); i++) - cnts[str.charAt(i)]++; - for (int i = 0; i < str.length(); i++) - if (cnts[str.charAt(i)] == 1) - return i; - return -1; -} -``` - -以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。 - -```java -public int FirstNotRepeatingChar2(String str) { - BitSet bs1 = new BitSet(256); - BitSet bs2 = new BitSet(256); - for (char c : str.toCharArray()) { - if (!bs1.get(c) && !bs2.get(c)) - bs1.set(c); // 0 0 -> 0 1 - else if (bs1.get(c) && !bs2.get(c)) - bs2.set(c); // 0 1 -> 1 1 - } - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if (bs1.get(c) && !bs2.get(c)) // 0 1 - return i; - } - return -1; -} -``` - -# 51. 数组中的逆序对 - -[NowCoder](https://www.nowcoder.com/practice/96bd6684e04a44eb80e6a68efc0ec6c5?tpId=13&tqId=11188&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 - -## 解题思路 - -```java -private long cnt = 0; -private int[] tmp; // 在这里声明辅助数组,而不是在 merge() 递归函数中声明 - -public int InversePairs(int[] nums) { - tmp = new int[nums.length]; - mergeSort(nums, 0, nums.length - 1); - return (int) (cnt % 1000000007); -} - -private void mergeSort(int[] nums, int l, int h) { - if (h - l < 1) - return; - int m = l + (h - l) / 2; - mergeSort(nums, l, m); - mergeSort(nums, m + 1, h); - merge(nums, l, m, h); -} - -private void merge(int[] nums, int l, int m, int h) { - int i = l, j = m + 1, k = l; - while (i <= m || j <= h) { - if (i > m) - tmp[k] = nums[j++]; - else if (j > h) - tmp[k] = nums[i++]; - else if (nums[i] < nums[j]) - tmp[k] = nums[i++]; - else { - tmp[k] = nums[j++]; - this.cnt += m - i + 1; // nums[i] >= nums[j],说明 nums[i...mid] 都大于 nums[j] - } - k++; - } - for (k = l; k <= h; k++) - nums[k] = tmp[k]; -} -``` - -# 52. 两个链表的第一个公共结点 - -[NowCoder](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=11189&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -

- -## 解题思路 - -设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。 - -当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 - -```java -public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { - ListNode l1 = pHead1, l2 = pHead2; - while (l1 != l2) { - l1 = (l1 == null) ? pHead2 : l1.next; - l2 = (l2 == null) ? pHead1 : l2.next; - } - return l1; -} -``` - -# 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) - -## 题目描述 - -```html -Input: -nums = 1, 2, 3, 3, 3, 3, 4, 6 -K = 3 - -Output: -4 -``` - -## 解题思路 - -```java -public int GetNumberOfK(int[] nums, int K) { - int first = binarySearch(nums, K); - int last = binarySearch(nums, K + 1); - return (first == nums.length || nums[first] != K) ? 0 : last - first; -} - -private int binarySearch(int[] nums, int K) { - int l = 0, h = nums.length; - while (l < h) { - int m = l + (h - l) / 2; - if (nums[m] >= K) - h = m; - else - l = m + 1; - } - return l; -} -``` - -# 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; -private int cnt = 0; - -public TreeNode KthNode(TreeNode pRoot, int k) { - inOrder(pRoot, k); - return ret; -} - -private void inOrder(TreeNode root, int k) { - if (root == null || cnt >= k) - return; - inOrder(root.left, k); - cnt++; - if (cnt == k) - ret = root; - inOrder(root.right, k); -} -``` - -# 55.1 二叉树的深度 - -[NowCoder](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 - -

- -## 解题思路 - -```java -public int TreeDepth(TreeNode root) { - return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); -} -``` - -# 55.2 平衡二叉树 - -[NowCoder](https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=13&tqId=11192&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -平衡二叉树左右子树高度差不超过 1。 - -

- -## 解题思路 - -```java -private boolean isBalanced = true; - -public boolean IsBalanced_Solution(TreeNode root) { - height(root); - return isBalanced; -} - -private int height(TreeNode root) { - if (root == null || !isBalanced) - return 0; - int left = height(root.left); - int right = height(root.right); - if (Math.abs(left - right) > 1) - isBalanced = false; - return 1 + Math.max(left, right); -} -``` - -# 56. 数组中只出现一次的数字 - -[NowCoder](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个数。 - -## 解题思路 - -两个不相等的元素在位级表示上必定会有一位存在不同,将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 - -diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 - -```java -public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) { - int diff = 0; - for (int num : nums) - diff ^= num; - diff &= -diff; - for (int num : nums) { - if ((num & diff) == 0) - num1[0] ^= num; - else - num2[0] ^= num; - } -} -``` - -# 57.1 和为 S 的两个数字 - -[NowCoder](https://www.nowcoder.com/practice/390da4f7a00f44bea7c2f3d19491311b?tpId=13&tqId=11195&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。 - -## 解题思路 - -使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 - -- 如果两个指针指向元素的和 sum == target,那么得到要求的结果; -- 如果 sum > target,移动较大的元素,使 sum 变小一些; -- 如果 sum < target,移动较小的元素,使 sum 变大一些。 - -```java -public ArrayList FindNumbersWithSum(int[] array, int sum) { - int i = 0, j = array.length - 1; - while (i < j) { - int cur = array[i] + array[j]; - if (cur == sum) - return new ArrayList<>(Arrays.asList(array[i], array[j])); - if (cur < sum) - i++; - else - j--; - } - return new ArrayList<>(); -} -``` - -# 57.2 和为 S 的连续正数序列 - -[NowCoder](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&tqId=11194&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -输出所有和为 S 的连续正数序列。 - -例如和为 100 的连续序列有: - -``` -[9, 10, 11, 12, 13, 14, 15, 16] -[18, 19, 20, 21, 22]。 -``` - -## 解题思路 - -```java -public ArrayList> FindContinuousSequence(int sum) { - ArrayList> ret = new ArrayList<>(); - int start = 1, end = 2; - int curSum = 3; - while (end < sum) { - if (curSum > sum) { - curSum -= start; - start++; - } else if (curSum < sum) { - end++; - curSum += end; - } else { - ArrayList list = new ArrayList<>(); - for (int i = start; i <= end; i++) - list.add(i); - ret.add(list); - curSum -= start; - start++; - end++; - curSum += end; - } - } - return ret; -} -``` - -# 58.1 翻转单词顺序列 - -[NowCoder](https://www.nowcoder.com/practice/3194a4f4cf814f63919d0790578d51f3?tpId=13&tqId=11197&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -```html -Input: -"I am a student." - -Output: -"student. a am I" -``` - -## 解题思路 - -题目应该有一个隐含条件,就是不能用额外的空间。虽然 Java 的题目输入参数为 String 类型,需要先创建一个字符数组使得空间复杂度为 O(N),但是正确的参数类型应该和原书一样,为字符数组,并且只能使用该字符数组的空间。任何使用了额外空间的解法在面试时都会大打折扣,包括递归解法。 - -正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。 - -```java -public String ReverseSentence(String str) { - int n = str.length(); - char[] chars = str.toCharArray(); - int i = 0, j = 0; - while (j <= n) { - if (j == n || chars[j] == ' ') { - reverse(chars, i, j - 1); - i = j + 1; - } - j++; - } - reverse(chars, 0, n - 1); - return new String(chars); -} - -private void reverse(char[] c, int i, int j) { - while (i < j) - swap(c, i++, j--); -} - -private void swap(char[] c, int i, int j) { - char t = c[i]; - c[i] = c[j]; - c[j] = t; -} -``` - -# 58.2 左旋转字符串 - -[NowCoder](https://www.nowcoder.com/practice/12d959b108cb42b1ab72cef4d36af5ec?tpId=13&tqId=11196&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -```html -Input: -S="abcXYZdef" -K=3 - -Output: -"XYZdefabc" -``` - -## 解题思路 - -先将 "abc" 和 "XYZdef" 分别翻转,得到 "cbafedZYX",然后再把整个字符串翻转得到 "XYZdefabc"。 - -```java -public String LeftRotateString(String str, int n) { - if (n >= str.length()) - return str; - char[] chars = str.toCharArray(); - reverse(chars, 0, n - 1); - reverse(chars, n, chars.length - 1); - reverse(chars, 0, chars.length - 1); - return new String(chars); -} - -private void reverse(char[] chars, int i, int j) { - while (i < j) - swap(chars, i++, j--); -} - -private void swap(char[] chars, int i, int j) { - char t = chars[i]; - chars[i] = chars[j]; - chars[j] = t; -} -``` - -# 59. 滑动窗口的最大值 - -[NowCoder](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。 - -例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。 - -## 解题思路 - -```java -public ArrayList maxInWindows(int[] num, int size) { - ArrayList ret = new ArrayList<>(); - if (size > num.length || size < 1) - return ret; - PriorityQueue heap = new PriorityQueue<>((o1, o2) -> o2 - o1); /* 大顶堆 */ - for (int i = 0; i < size; i++) - heap.add(num[i]); - ret.add(heap.peek()); - for (int i = 0, j = i + size; j < num.length; i++, j++) { /* 维护一个大小为 size 的大顶堆 */ - heap.remove(num[i]); - heap.add(num[j]); - ret.add(heap.peek()); - } - return ret; -} -``` - -# 60. n 个骰子的点数 - -[Lintcode](https://www.lintcode.com/en/problem/dices-sum/) - -## 题目描述 - -把 n 个骰子仍在地上,求点数和为 s 的概率。 - -

- -## 解题思路 - -### 动态规划解法 - -使用一个二维数组 dp 存储点数出现的次数,其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。 - -空间复杂度:O(N2) - -```java -public List> dicesSum(int n) { - final int face = 6; - final int pointNum = face * n; - long[][] dp = new long[n + 1][pointNum + 1]; - - for (int i = 1; i <= face; i++) - dp[1][i] = 1; - - for (int i = 2; i <= n; i++) - for (int j = i; j <= pointNum; j++) /* 使用 i 个骰子最小点数为 i */ - for (int k = 1; k <= face && k <= j; k++) - dp[i][j] += dp[i - 1][j - k]; - - final double totalNum = Math.pow(6, n); - List> ret = new ArrayList<>(); - for (int i = n; i <= pointNum; i++) - ret.add(new AbstractMap.SimpleEntry<>(i, dp[n][i] / totalNum)); - - return ret; -} -``` - -### 动态规划解法 + 旋转数组 - -空间复杂度:O(N) - -```java -public List> dicesSum(int n) { - final int face = 6; - final int pointNum = face * n; - long[][] dp = new long[2][pointNum + 1]; - - for (int i = 1; i <= face; i++) - dp[0][i] = 1; - - int flag = 1; /* 旋转标记 */ - for (int i = 2; i <= n; i++, flag = 1 - flag) { - for (int j = 0; j <= pointNum; j++) - dp[flag][j] = 0; /* 旋转数组清零 */ - - for (int j = i; j <= pointNum; j++) - for (int k = 1; k <= face && k <= j; k++) - dp[flag][j] += dp[1 - flag][j - k]; - } - - final double totalNum = Math.pow(6, n); - List> ret = new ArrayList<>(); - for (int i = n; i <= pointNum; i++) - ret.add(new AbstractMap.SimpleEntry<>(i, dp[1 - flag][i] / totalNum)); - - return ret; -} -``` - -# 61. 扑克牌顺子 - -[NowCoder](https://www.nowcoder.com/practice/762836f4d43d43ca9deb273b3de8e1f4?tpId=13&tqId=11198&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -五张牌,其中大小鬼为癞子,牌面大小为 0。判断这五张牌是否能组成顺子。 - -

- -## 解题思路 - -```java -public boolean isContinuous(int[] nums) { - - if (nums.length < 5) - return false; - - Arrays.sort(nums); - - // 统计癞子数量 - int cnt = 0; - for (int num : nums) - if (num == 0) - cnt++; - - // 使用癞子去补全不连续的顺子 - for (int i = cnt; i < nums.length - 1; i++) { - if (nums[i + 1] == nums[i]) - return false; - cnt -= nums[i + 1] - nums[i] - 1; - } - - return cnt >= 0; -} -``` - -# 62. 圆圈中最后剩下的数 - -[NowCoder](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&tqId=11199&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。 - -## 解题思路 - -约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。 - -```java -public int LastRemaining_Solution(int n, int m) { - if (n == 0) /* 特殊输入的处理 */ - return -1; - if (n == 1) /* 递归返回条件 */ - return 0; - return (LastRemaining_Solution(n - 1, m) + m) % n; -} -``` - -# 63. 股票的最大利润 - -[Leetcode](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) - -## 题目描述 - -可以有一次买入和一次卖出,那么买入必须在前。求最大收益。 - -

- -## 解题思路 - -使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。 - -```java -public int maxProfit(int[] prices) { - if (prices == null || prices.length == 0) - return 0; - int soFarMin = prices[0]; - int maxProfit = 0; - for (int i = 1; i < prices.length; i++) { - soFarMin = Math.min(soFarMin, prices[i]); - maxProfit = Math.max(maxProfit, prices[i] - soFarMin); - } - return maxProfit; -} -``` - -# 64. 求 1+2+3+...+n - -[NowCoder](https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 A ? B : C。 - -## 解题思路 - -使用递归解法最重要的是指定返回条件,但是本题无法直接使用 if 语句来指定返回条件。 - -条件与 && 具有短路原则,即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性,将递归的返回条件取非然后作为 && 的第一个条件语句,递归的主体转换为第二个条件语句,那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分,递归返回。 - -本题的递归返回条件为 n <= 0,取非后就是 n > 0;递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。 - -```java -public int Sum_Solution(int n) { - int sum = n; - boolean b = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0); - return sum; -} -``` - -# 65. 不用加减乘除做加法 - -[NowCoder](https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&tqId=11201&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -写一个函数,求两个整数之和,要求不得使用 +、-、\*、/ 四则运算符号。 - -## 解题思路 - -a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。 - -递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 - -```java -public int Add(int a, int b) { - return b == 0 ? a : Add(a ^ b, (a & b) << 1); -} -``` - -# 66. 构建乘积数组 - -[NowCoder](https://www.nowcoder.com/practice/94a4d381a68b47b7a8bed86f2975db46?tpId=13&tqId=11204&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -给定一个数组 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]。要求不能使用除法。 - -

- -## 解题思路 - -```java -public int[] multiply(int[] A) { - int n = A.length; - int[] B = new int[n]; - for (int i = 0, product = 1; i < n; product *= A[i], i++) /* 从左往右累乘 */ - B[i] = product; - for (int i = n - 1, product = 1; i >= 0; product *= A[i], i--) /* 从右往左累乘 */ - B[i] *= product; - return B; -} -``` - -# 67. 把字符串转换成整数 - -[NowCoder](https://www.nowcoder.com/practice/1277c681251b4372bdef344468e4f26e?tpId=13&tqId=11202&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 0,要求不能使用字符串转换整数的库函数。 - -```html -Iuput: -+2147483647 -1a33 - -Output: -2147483647 -0 -``` - -## 解题思路 - -```java -public int StrToInt(String str) { - if (str == null || str.length() == 0) - return 0; - boolean isNegative = str.charAt(0) == '-'; - int ret = 0; - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if (i == 0 && (c == '+' || c == '-')) /* 符号判定 */ - continue; - if (c < '0' || c > '9') /* 非法输入 */ - return 0; - ret = ret * 10 + (c - '0'); - } - return isNegative ? -ret : ret; -} -``` - -# 68. 树中两个节点的最低公共祖先 - -## 解题思路 - -### 二叉查找树 - -

- -[Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) - -二叉查找树中,两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val。 - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - if (root == null) - return root; - if (root.val > p.val && root.val > q.val) - return lowestCommonAncestor(root.left, p, q); - if (root.val < p.val && root.val < q.val) - return lowestCommonAncestor(root.right, p, q); - return root; -} -``` - -### 普通二叉树 - -

- -[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 分别在两个子树中,那么就说明根节点就是最低公共祖先。 - -```java -public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { - if (root == null || root == p || root == q) - return root; - TreeNode left = lowestCommonAncestor(root.left, p, q); - TreeNode right = lowestCommonAncestor(root.right, p, q); - return left == null ? right : right == null ? left : root; -} -``` - -# 参考文献 - -- 何海涛. 剑指 Offer[M]. 电子工业出版社, 2012. diff --git a/docs/notes/攻击技术.md b/docs/notes/攻击技术.md index b698e496..90ec94b2 100644 --- a/docs/notes/攻击技术.md +++ b/docs/notes/攻击技术.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、跨站脚本攻击](#一跨站脚本攻击) * [二、跨站请求伪造](#二跨站请求伪造) @@ -192,3 +191,9 @@ ResultSet rs = stmt.executeQuery(); - [维基百科:SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A) - [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0) - [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/数据库系统原理.md b/docs/notes/数据库系统原理.md index 34a2a3e3..b1bfac5f 100644 --- a/docs/notes/数据库系统原理.md +++ b/docs/notes/数据库系统原理.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、事务](#一事务) * [概念](#概念) @@ -580,3 +579,9 @@ Entity-Relationship,有三个组成部分:实体、属性、联系。 - [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/) - [浅入浅出 MySQL 和 InnoDB](https://draveness.me/mysql-innodb) - [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/构建工具.md b/docs/notes/构建工具.md index fcdd6b44..888b9ac0 100644 --- a/docs/notes/构建工具.md +++ b/docs/notes/构建工具.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、构建工具的作用](#一构建工具的作用) * [二、Java 主流构建工具](#二java-主流构建工具) @@ -140,3 +139,9 @@ A -> C -> X(2.0) - [maven 2 gradle](http://sagioto.github.io/maven2gradle/) - [新一代构建工具 gradle](https://www.imooc.com/learn/833) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/正则表达式.md b/docs/notes/正则表达式.md index 2b6505a4..b735dfe9 100644 --- a/docs/notes/正则表达式.md +++ b/docs/notes/正则表达式.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [二、匹配单个字符](#二匹配单个字符) @@ -387,3 +386,9 @@ aBCd # 参考资料 - BenForta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/消息队列.md b/docs/notes/消息队列.md index 16ac80df..df3e619f 100644 --- a/docs/notes/消息队列.md +++ b/docs/notes/消息队列.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、消息模型](#一消息模型) * [点对点](#点对点) @@ -81,3 +80,9 @@ - [Observer vs Pub-Sub](http://developers-club.com/posts/270339/) - [消息队列中点对点与发布订阅区别](https://blog.csdn.net/lizhitao/article/details/47723105) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 其它.md b/docs/notes/算法 - 其它.md new file mode 100644 index 00000000..cc761d5a --- /dev/null +++ b/docs/notes/算法 - 其它.md @@ -0,0 +1,140 @@ + +* [汉诺塔](#汉诺塔) +* [哈夫曼编码](#哈夫曼编码) + + + +# 汉诺塔 + +

+ +有三个柱子,分别为 from、buffer、to。需要将 from 上的圆盘全部移动到 to 上,并且要保证小圆盘始终在大圆盘上。 + +这是一个经典的递归问题,分为三步求解: + +① 将 n-1 个圆盘从 from -> buffer + +

+ +② 将 1 个圆盘从 from -> to + +

+ +③ 将 n-1 个圆盘从 buffer -> to + +

+ +如果只有一个圆盘,那么只需要进行一次移动操作。 + +从上面的讨论可以知道,an = 2 * an-1 + 1,显然 an = 2n - 1,n 个圆盘需要移动 2n - 1 次。 + +```java +public class Hanoi { + public static void move(int n, String from, String buffer, String to) { + if (n == 1) { + System.out.println("from " + from + " to " + to); + return; + } + move(n - 1, from, to, buffer); + move(1, from, buffer, to); + move(n - 1, buffer, from, to); + } + + public static void main(String[] args) { + Hanoi.move(3, "H1", "H2", "H3"); + } +} +``` + +```html +from H1 to H3 +from H1 to H2 +from H3 to H2 +from H1 to H3 +from H2 to H1 +from H2 to H3 +from H1 to H3 +``` + +# 哈夫曼编码 + +根据数据出现的频率对数据进行编码,从而压缩原始数据。 + +例如对于一个文本文件,其中各种字符出现的次数如下: + +- a : 10 +- b : 20 +- c : 40 +- d : 80 + +可以将每种字符转换成二进制编码,例如将 a 转换为 00,b 转换为 01,c 转换为 10,d 转换为 11。这是最简单的一种编码方式,没有考虑各个字符的权值(出现频率)。而哈夫曼编码采用了贪心策略,使出现频率最高的字符的编码最短,从而保证整体的编码长度最短。 + +首先生成一颗哈夫曼树,每次生成过程中选取频率最少的两个节点,生成一个新节点作为它们的父节点,并且新节点的频率为两个节点的和。选取频率最少的原因是,生成过程使得先选取的节点位于树的更低层,那么需要的编码长度更长,频率更少可以使得总编码长度更少。 + +生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到叶子节点,叶子节点代表的字符的编码就是这个路径编码。 + +

+ +```java +public class Huffman { + + private class Node implements Comparable { + char ch; + int freq; + boolean isLeaf; + Node left, right; + + public Node(char ch, int freq) { + this.ch = ch; + this.freq = freq; + isLeaf = true; + } + + public Node(Node left, Node right, int freq) { + this.left = left; + this.right = right; + this.freq = freq; + isLeaf = false; + } + + @Override + public int compareTo(Node o) { + return this.freq - o.freq; + } + } + + public Map encode(Map frequencyForChar) { + PriorityQueue priorityQueue = new PriorityQueue<>(); + for (Character c : frequencyForChar.keySet()) { + priorityQueue.add(new Node(c, frequencyForChar.get(c))); + } + while (priorityQueue.size() != 1) { + Node node1 = priorityQueue.poll(); + Node node2 = priorityQueue.poll(); + priorityQueue.add(new Node(node1, node2, node1.freq + node2.freq)); + } + return encode(priorityQueue.poll()); + } + + private Map encode(Node root) { + Map encodingForChar = new HashMap<>(); + encode(root, "", encodingForChar); + return encodingForChar; + } + + private void encode(Node node, String encoding, Map encodingForChar) { + if (node.isLeaf) { + encodingForChar.put(node.ch, encoding); + return; + } + encode(node.left, encoding + '0', encodingForChar); + encode(node.right, encoding + '1', encodingForChar); + } +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 并查集.md b/docs/notes/算法 - 并查集.md new file mode 100644 index 00000000..6648d886 --- /dev/null +++ b/docs/notes/算法 - 并查集.md @@ -0,0 +1,198 @@ + +* [前言](#前言) +* [Quick Find](#quick-find) +* [Quick Union](#quick-union) +* [加权 Quick Union](#加权-quick-union) +* [路径压缩的加权 Quick Union](#路径压缩的加权-quick-union) +* [比较](#比较) + + + +# 前言 + +用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 + +

+ +| 方法 | 描述 | +| :---: | :---: | +| 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 | + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 排序.md b/docs/notes/算法 - 排序.md new file mode 100644 index 00000000..db81624d --- /dev/null +++ b/docs/notes/算法 - 排序.md @@ -0,0 +1,591 @@ + +* [约定](#约定) +* [选择排序](#选择排序) +* [冒泡排序](#冒泡排序) +* [插入排序](#插入排序) +* [希尔排序](#希尔排序) +* [归并排序](#归并排序) + * [1. 归并方法](#1-归并方法) + * [2. 自顶向下归并排序](#2-自顶向下归并排序) + * [3. 自底向上归并排序](#3-自底向上归并排序) +* [快速排序](#快速排序) + * [1. 基本算法](#1-基本算法) + * [2. 切分](#2-切分) + * [3. 性能分析](#3-性能分析) + * [4. 算法改进](#4-算法改进) + * [5. 基于切分的快速选择算法](#5-基于切分的快速选择算法) +* [堆排序](#堆排序) + * [1. 堆](#1-堆) + * [2. 上浮和下沉](#2-上浮和下沉) + * [3. 插入元素](#3-插入元素) + * [4. 删除最大元素](#4-删除最大元素) + * [5. 堆排序](#5-堆排序) + * [6. 分析](#6-分析) +* [小结](#小结) + * [1. 排序算法的比较](#1-排序算法的比较) + * [2. Java 的排序算法实现](#2-java-的排序算法实现) + + + +# 约定 + +待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 + +研究排序算法的成本模型时,统计的是比较和交换的次数。 + +使用辅助函数 less() 和 swap() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。 + +```java +public abstract class Sort> { + + public abstract void sort(T[] nums); + + protected boolean less(T v, T w) { + return v.compareTo(w) < 0; + } + + protected void swap(T[] a, int i, int j) { + T t = a[i]; + a[i] = a[j]; + a[j] = t; + } +} +``` + +# 选择排序 + +选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 + +选择排序需要 \~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 - 1; i++) { + int min = i; + for (int j = i + 1; j < N; j++) { + if (less(nums[j], nums[min])) { + min = j; + } + } + swap(nums, i, min); + } + } +} +``` + +# 冒泡排序 + +从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。 + +在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。 + +

+ +```java +public class Bubble> extends Sort { + + @Override + public void sort(T[] nums) { + int N = nums.length; + boolean hasSorted = false; + for (int i = N - 1; i > 0 && !hasSorted; i--) { + hasSorted = true; + for (int j = 0; j < i; j++) { + if (less(nums[j + 1], nums[j])) { + hasSorted = false; + 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,因此插入排序需要交换的次数为逆序数量。 + +插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,逆序较少,那么插入排序会很快。 + +- 平均情况下插入排序需要 \~N2/4 比较以及 \~N2/4 次交换; +- 最坏的情况下需要 \~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。 + +希尔排序的出现就是为了解决插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。 + +希尔排序使用插入排序对间隔 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 >= 1) { + 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 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 + +# 归并排序 + +归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。 + +

+ +## 1. 归并方法 + +归并方法将数组中两个已经排序的部分归并成一个。 + +```java +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++) { + if (i > m) { + nums[k] = aux[j++]; + + } else if (j > h) { + nums[k] = aux[i++]; + + } else if (aux[i].compareTo(aux[j]) <= 0) { + nums[k] = aux[i++]; // 先进行这一步,保证稳定性 + + } else { + nums[k] = aux[j++]; + } + } + } +} +``` + +## 2. 自顶向下归并排序 + +将一个大数组分成两个小数组去求解。 + +因为每次都将问题对半分成两个子问题,这种对半分的算法复杂度一般为 O(NlogN)。 + +```java +public class Up2DownMergeSort> extends MergeSort { + + @Override + public void sort(T[] nums) { + aux = (T[]) new Comparable[nums.length]; + sort(nums, 0, nums.length - 1); + } + + private void sort(T[] nums, int l, int h) { + if (h <= l) { + return; + } + int mid = l + (h - l) / 2; + sort(nums, l, mid); + sort(nums, mid + 1, h); + merge(nums, l, mid, h); + } +} +``` + +## 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) { + merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); + } + } + } +} + +``` + +# 快速排序 + +## 1. 基本算法 + +- 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序; +- 快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 + +

+ +```java +public class QuickSort> extends Sort { + + @Override + public void sort(T[] nums) { + shuffle(nums); + sort(nums, 0, nums.length - 1); + } + + private void sort(T[] nums, int l, int h) { + if (h <= l) + return; + int j = partition(nums, l, h); + sort(nums, l, j - 1); + sort(nums, j + 1, h); + } + + private void shuffle(T[] nums) { + List list = Arrays.asList(nums); + Collections.shuffle(list); + list.toArray(nums); + } +} +``` + +## 2. 切分 + +取 a[l] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于它的元素,交换这两个元素。不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[l] 和 a[j] 交换位置。 + +

+ +```java +private int partition(T[] nums, int l, int h) { + int i = l, j = h + 1; + T v = nums[l]; + while (true) { + while (less(nums[++i], v) && i != h) ; + while (less(v, nums[--j]) && j != l) ; + if (i >= j) + break; + swap(nums, i, j); + } + swap(nums, l, j); + return j; +} +``` + +## 3. 性能分析 + +快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 + +快速排序最好的情况下是每次都正好将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,复杂度为 O(NlogN)。 + +最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 + +## 4. 算法改进 + +#### 4.1 切换到插入排序 + +因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 + +#### 4.2 三数取中 + +最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。一种折中方法是取 3 个元素,并将大小居中的元素作为切分元素。 + +#### 4.3 三向切分 + +对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 + +三向切分快速排序对于有大量重复元素的随机数组可以在线性时间内完成排序。 + +```java +public class ThreeWayQuickSort> extends QuickSort { + + @Override + protected void sort(T[] nums, int l, int h) { + 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) { + swap(nums, lt++, i++); + } else if (cmp > 0) { + swap(nums, i, gt--); + } else { + i++; + } + } + sort(nums, l, lt - 1); + sort(nums, gt + 1, h); + } +} +``` + +## 5. 基于切分的快速选择算法 + +快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。 + +可以利用这个特性找出数组的第 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) { + return nums[k]; + + } else if (j > k) { + h = j - 1; + + } else { + l = j + 1; + } + } + return nums[k]; +} +``` + +# 堆排序 + +## 1. 堆 + +堆中某个节点的值总是大于等于其子节点的值,并且堆是一颗完全二叉树。 + +堆可以用数组来表示,这是因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。 + +

+ +```java +public class Heap> { + + private T[] heap; + private int N = 0; + + public Heap(int maxN) { + this.heap = (T[]) new Comparable[maxN + 1]; + } + + public boolean isEmpty() { + return N == 0; + } + + public int size() { + return N; + } + + private boolean less(int i, int j) { + return heap[i].compareTo(heap[j]) < 0; + } + + private void swap(int i, int j) { + T t = heap[i]; + heap[i] = heap[j]; + heap[j] = t; + } +} +``` + +## 2. 上浮和下沉 + +在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为上浮。 + +

+ +```java +private void swim(int k) { + while (k > 1 && less(k / 2, k)) { + swap(k / 2, k); + k = k / 2; + } +} +``` + +类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为下沉。一个节点如果有两个子节点,应当与两个子节点中最大那个节点进行交换。 + +

+ +```java +private void sink(int k) { + while (2 * k <= N) { + int j = 2 * k; + if (j < N && less(j, j + 1)) + j++; + if (!less(k, j)) + break; + swap(k, j); + k = j; + } +} +``` + +## 3. 插入元素 + +将新元素放到数组末尾,然后上浮到合适的位置。 + +```java +public void insert(Comparable v) { + heap[++N] = v; + swim(N); +} +``` + +## 4. 删除最大元素 + +从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 + +```java +public T delMax() { + T max = heap[1]; + swap(1, N--); + heap[N + 1] = null; + sink(1); + return max; +} +``` + +## 5. 堆排序 + +把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列,这就是堆排序。 + +#### 5.1 构建堆 + +无序数组建立堆最直接的方法是从左到右遍历数组进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。 + +

+ +#### 5.2 交换堆顶元素与最后一个元素 + +交换之后需要进行下沉操作维持堆的有序状态。 + +

+ +```java +public class HeapSort> extends Sort { + /** + * 数组第 0 个位置不能有元素 + */ + @Override + public void sort(T[] nums) { + int N = nums.length - 1; + for (int k = N / 2; k >= 1; k--) + sink(nums, k, N); + + while (N > 1) { + swap(nums, 1, N--); + sink(nums, 1, N); + } + } + + private void sink(T[] nums, int k, int N) { + while (2 * k <= N) { + int j = 2 * k; + if (j < N && less(nums, j, j + 1)) + j++; + if (!less(nums, k, j)) + break; + swap(nums, k, j); + k = j; + } + } + + private boolean less(T[] nums, int i, int j) { + return nums[i].compareTo(nums[j]) < 0; + } +} +``` + +## 6. 分析 + +一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。 + +对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。 + +堆排序是一种原地排序,没有利用额外的空间。 + +现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。 + +# 小结 + +## 1. 排序算法的比较 + +| 算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 | +| :---: | :---: |:---: | :---: | :---: | +| 选择排序 | × | N2 | 1 | | +| 冒泡排序 | √ | N2 | 1 | | +| 插入排序 | √ | N \~ N2 | 1 | 时间复杂度和初始顺序有关 | +| 希尔排序 | × | N 的若干倍乘于递增序列的长度 | 1 | 改进版插入排序 | +| 快速排序 | × | NlogN | logN | | +| 三向切分快速排序 | × | N \~ NlogN | logN | 适用于有大量重复主键| +| 归并排序 | √ | NlogN | N | | +| 堆排序 | × | NlogN | 1 | 无法利用局部性原理| + +快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其它线性对数级别的排序算法都要小。 + +使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 + +## 2. Java 的排序算法实现 + +Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 栈和队列.md b/docs/notes/算法 - 栈和队列.md new file mode 100644 index 00000000..58c1c455 --- /dev/null +++ b/docs/notes/算法 - 栈和队列.md @@ -0,0 +1,324 @@ + +* [栈](#栈) + * [1. 数组实现](#1-数组实现) + * [2. 链表实现](#2-链表实现) +* [队列](#队列) + + + +# 栈 + +```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 和 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; + } + }; + } +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 目录.md b/docs/notes/算法 - 目录.md new file mode 100644 index 00000000..edfe69fe --- /dev/null +++ b/docs/notes/算法 - 目录.md @@ -0,0 +1,27 @@ + +* [目录](#目录) +* [参考资料](#参考资料) + + + +# 目录 + +实现代码:[Algorithm](https://github.com/CyC2018/Algorithm),绘图文件:[ProcessOn](https://www.processon.com/view/link/5a3e4c1ee4b0ce9ffea8c727)。 + + +- [算法分析](算法%20-%20算法分析.md) +- [排序](算法%20-%20排序.md) +- [并查集](算法%20-%20并查集.md) +- [栈和队列](算法%20-%20栈和队列.md) +- [符号表](算法%20-%20符号表.md) +- [其它](算法%20-%20其它.md) + +# 参考资料 + +- Sedgewick, Robert, and Kevin Wayne. _Algorithms_. Addison-Wesley Professional, 2011. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 目录1.md b/docs/notes/算法 - 目录1.md new file mode 100644 index 00000000..1d63773d --- /dev/null +++ b/docs/notes/算法 - 目录1.md @@ -0,0 +1,27 @@ + +* [目录](#目录) +* [参考资料](#参考资料) + + + +# 目录 + +实现代码:[Algorithm](https://github.com/CyC2018/Algorithm),绘图文件:[ProcessOn](https://www.processon.com/view/link/5a3e4c1ee4b0ce9ffea8c727)。 + + +- [算法分析](notes/算法%20-%20算法分析.md) +- [排序](notes/算法%20-%20排序.md) +- [并查集](notes/算法%20-%20并查集.md) +- [栈和队列](notes/算法%20-%20栈和队列.md) +- [符号表](notes/算法%20-%20符号表.md) +- [其它](notes/算法%20-%20其它.md) + +# 参考资料 + +- Sedgewick, Robert, and Kevin Wayne. _Algorithms_. Addison-Wesley Professional, 2011. + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 符号表.md b/docs/notes/算法 - 符号表.md new file mode 100644 index 00000000..c6d55cbc --- /dev/null +++ b/docs/notes/算法 - 符号表.md @@ -0,0 +1,945 @@ + +* [前言](#前言) +* [初级实现](#初级实现) + * [1. 链表实现无序符号表](#1-链表实现无序符号表) + * [2. 二分查找实现有序符号表](#2-二分查找实现有序符号表) +* [二叉查找树](#二叉查找树) + * [1. get()](#1-get) + * [2. put()](#2-put) + * [3. 分析](#3-分析) + * [4. floor()](#4-floor) + * [5. rank()](#5-rank) + * [6. min()](#6-min) + * [7. deleteMin()](#7-deletemin) + * [8. delete()](#8-delete) + * [9. keys()](#9-keys) + * [10. 分析](#10-分析) +* [2-3 查找树](#2-3-查找树) + * [1. 插入操作](#1-插入操作) + * [2. 性质](#2-性质) +* [红黑树](#红黑树) + * [1. 左旋转](#1-左旋转) + * [2. 右旋转](#2-右旋转) + * [3. 颜色转换](#3-颜色转换) + * [4. 插入](#4-插入) + * [5. 分析](#5-分析) +* [散列表](#散列表) + * [1. 散列函数](#1-散列函数) + * [2. 拉链法](#2-拉链法) + * [3. 线性探测法](#3-线性探测法) +* [小结](#小结) + * [1. 符号表算法比较](#1-符号表算法比较) + * [2. Java 的符号表实现](#2-java-的符号表实现) + * [3. 稀疏向量乘法](#3-稀疏向量乘法) + + + +# 前言 + +符号表(Symbol Table)是一种存储键值对的数据结构,可以支持快速查找操作。 + +符号表分为有序和无序两种,有序符号表主要指支持 min()、max() 等根据键的大小关系来实现的操作。 + +有序符号表的键需要实现 Comparable 接口。 + +```java +public interface UnorderedST { + + int size(); + + Value get(Key key); + + void put(Key key, Value value); + + void delete(Key key); +} +``` + +```java +public interface OrderedST, Value> { + + int size(); + + void put(Key key, Value value); + + Value get(Key key); + + Key min(); + + Key max(); + + int rank(Key key); + + List keys(Key l, Key h); +} +``` + +# 初级实现 + +## 1. 链表实现无序符号表 + +```java +public class ListUnorderedST implements UnorderedST { + + private Node first; + + private class Node { + Key key; + Value value; + Node next; + + Node(Key key, Value value, Node next) { + this.key = key; + this.value = value; + this.next = next; + } + } + + @Override + public int size() { + int cnt = 0; + Node cur = first; + while (cur != null) { + cnt++; + cur = cur.next; + } + return cnt; + } + + @Override + public void put(Key key, Value value) { + Node cur = first; + // 如果在链表中找到节点的键等于 key 就更新这个节点的值为 value + while (cur != null) { + if (cur.key.equals(key)) { + cur.value = value; + return; + } + cur = cur.next; + } + // 否则使用头插法插入一个新节点 + first = new Node(key, value, first); + } + + @Override + public void delete(Key key) { + if (first == null) + return; + if (first.key.equals(key)) + first = first.next; + Node pre = first, cur = first.next; + while (cur != null) { + if (cur.key.equals(key)) { + pre.next = cur.next; + return; + } + pre = pre.next; + cur = cur.next; + } + } + + @Override + public Value get(Key key) { + Node cur = first; + while (cur != null) { + if (cur.key.equals(key)) + return cur.value; + cur = cur.next; + } + return null; + } +} +``` + +## 2. 二分查找实现有序符号表 + +使用一对平行数组,一个存储键一个存储值。 + +二分查找的 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 + +二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 + +```java +public class BinarySearchOrderedST, Value> implements OrderedST { + + private Key[] keys; + private Value[] values; + private int N = 0; + + public BinarySearchOrderedST(int capacity) { + keys = (Key[]) new Comparable[capacity]; + values = (Value[]) new Object[capacity]; + } + + @Override + public int size() { + return N; + } + + @Override + public int rank(Key key) { + int l = 0, h = N - 1; + while (l <= h) { + int m = l + (h - l) / 2; + int cmp = key.compareTo(keys[m]); + if (cmp == 0) + return m; + else if (cmp < 0) + h = m - 1; + else + l = m + 1; + } + return l; + } + + @Override + public List keys(Key l, Key h) { + int index = rank(l); + List list = new ArrayList<>(); + while (keys[index].compareTo(h) <= 0) { + list.add(keys[index]); + index++; + } + return list; + } + + @Override + public void put(Key key, Value value) { + int index = rank(key); + // 如果找到已经存在的节点键为 key,就更新这个节点的值为 value + if (index < N && keys[index].compareTo(key) == 0) { + values[index] = value; + return; + } + // 否则在数组中插入新的节点,需要先将插入位置之后的元素都向后移动一个位置 + for (int j = N; j > index; j--) { + keys[j] = keys[j - 1]; + values[j] = values[j - 1]; + } + keys[index] = key; + values[index] = value; + N++; + } + + @Override + public Value get(Key key) { + int index = rank(key); + if (index < N && keys[index].compareTo(key) == 0) + return values[index]; + return null; + } + + @Override + public Key min() { + return keys[0]; + } + + @Override + public Key max() { + return keys[N - 1]; + } +} +``` + +# 二叉查找树 + +**二叉树** 是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。 + +

+ +**二叉查找树** (BST)是一颗二叉树,并且每个节点的值都大于等于其左子树中的所有节点的值而小于等于右子树的所有节点的值。 + +

+ +BST 有一个重要性质,就是它的中序遍历结果递增排序。 + +

+ +基本数据结构: + +```java +public class BST, Value> implements OrderedST { + + protected Node root; + + protected class Node { + Key key; + Value val; + Node left; + Node right; + // 以该节点为根的子树节点总数 + int N; + // 红黑树中使用 + boolean color; + + Node(Key key, Value val, int N) { + this.key = key; + this.val = val; + this.N = N; + } + } + + @Override + public int size() { + return size(root); + } + + private int size(Node x) { + if (x == null) + return 0; + return x.N; + } + + protected void recalculateSize(Node x) { + x.N = size(x.left) + size(x.right) + 1; + } +} +``` + +为了方便绘图,下文中二叉树的空链接不画出来。 + +## 1. get() + +- 如果树是空的,则查找未命中; +- 如果被查找的键和根节点的键相等,查找命中; +- 否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 + +```java +@Override +public Value get(Key key) { + return get(root, key); +} + +private Value get(Node x, Key key) { + if (x == null) + return null; + int cmp = key.compareTo(x.key); + if (cmp == 0) + return x.val; + else if (cmp < 0) + return get(x.left, key); + else + return get(x.right, key); +} +``` + +## 2. put() + +当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接指向该节点,使得该节点正确地链接到树中。 + +

+ +```java + @Override +public void put(Key key, Value value) { + root = put(root, key, value); +} + +private Node put(Node x, Key key, Value value) { + if (x == null) + return new Node(key, value, 1); + int cmp = key.compareTo(x.key); + if (cmp == 0) + x.val = value; + else if (cmp < 0) + x.left = put(x.left, key, value); + else + x.right = put(x.right, key, value); + recalculateSize(x); + return x; +} +``` + +## 3. 分析 + +二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。 + +最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。 + +

+ +在最坏的情况下,树的高度为 N。 + +

+ +## 4. floor() + +floor(key):小于等于键的最大键 + +- 如果键小于根节点的键,那么 floor(key) 一定在左子树中; +- 如果键大于根节点的键,需要先判断右子树中是否存在 floor(key),如果存在就返回,否则根节点就是 floor(key)。 + +```java +public Key floor(Key key) { + Node x = floor(root, key); + if (x == null) + return null; + return x.key; +} + +private Node floor(Node x, Key key) { + if (x == null) + return null; + int cmp = key.compareTo(x.key); + if (cmp == 0) + return x; + if (cmp < 0) + return floor(x.left, key); + Node t = floor(x.right, key); + return t != null ? t : x; +} +``` + +## 5. rank() + +rank(key) 返回 key 的排名。 + +- 如果键和根节点的键相等,返回左子树的节点数; +- 如果小于,递归计算在左子树中的排名; +- 如果大于,递归计算在右子树中的排名,加上左子树的节点数,再加上 1(根节点)。 + +```java +@Override +public int rank(Key key) { + return rank(key, root); +} + +private int rank(Key key, Node x) { + if (x == null) + return 0; + int cmp = key.compareTo(x.key); + if (cmp == 0) + return size(x.left); + else if (cmp < 0) + return rank(key, x.left); + else + return 1 + size(x.left) + rank(key, x.right); +} +``` + +## 6. min() + +```java +@Override +public Key min() { + return min(root).key; +} + +private Node min(Node x) { + if (x == null) + return null; + if (x.left == null) + return x; + return min(x.left); +} +``` + +## 7. deleteMin() + +令指向最小节点的链接指向最小节点的右子树。 + +

+ +```java +public void deleteMin() { + root = deleteMin(root); +} + +public Node deleteMin(Node x) { + if (x.left == null) + return x.right; + x.left = deleteMin(x.left); + recalculateSize(x); + return x; +} +``` + +## 8. delete() + +- 如果待删除的节点只有一个子树, 那么只需要让指向待删除节点的链接指向唯一的子树即可; +- 否则,让右子树的最小节点替换该节点。 + +

+ +```java +public void delete(Key key) { + root = delete(root, key); +} +private Node delete(Node x, Key key) { + if (x == null) + return null; + int cmp = key.compareTo(x.key); + if (cmp < 0) + x.left = delete(x.left, key); + else if (cmp > 0) + x.right = delete(x.right, key); + else { + if (x.right == null) + return x.left; + if (x.left == null) + return x.right; + Node t = x; + x = min(t.right); + x.right = deleteMin(t.right); + x.left = t.left; + } + recalculateSize(x); + return x; +} +``` + +## 9. keys() + +利用二叉查找树中序遍历的结果为递增的特点。 + +```java +@Override +public List keys(Key l, Key h) { + return keys(root, l, h); +} + +private List keys(Node x, Key l, Key h) { + List list = new ArrayList<>(); + if (x == null) + return list; + int cmpL = l.compareTo(x.key); + int cmpH = h.compareTo(x.key); + if (cmpL < 0) + list.addAll(keys(x.left, l, h)); + if (cmpL <= 0 && cmpH >= 0) + list.add(x.key); + if (cmpH > 0) + list.addAll(keys(x.right, l, h)); + return list; +} +``` + +## 10. 分析 + +二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 + +# 2-3 查找树 + +2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 + +

+ +## 1. 插入操作 + +插入操作和 BST 的插入操作有很大区别,BST 的插入操作是先进行一次未命中的查找,然后再将节点插入到对应的空链接上。但是 2-3 查找树如果也这么做的话,那么就会破坏了平衡性。它是将新节点插入到叶子节点上。 + +根据叶子节点的类型不同,有不同的处理方式: + +- 如果插入到 2- 节点上,那么直接将新节点和原来的节点组成 3- 节点即可。 + +

+ +- 如果是插入到 3- 节点上,就会产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。 + +

+ +## 2. 性质 + +2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。 + +2-3 查找树的查找和插入操作复杂度和插入顺序无关,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 + +# 红黑树 + +红黑树是 2-3 查找树,但它不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 + +

+ +红黑树具有以下性质: + +- 红链接都为左链接; +- 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。 + +画红黑树时可以将红链接画平。 + +

+ +```java +public class RedBlackBST, Value> extends BST { + + private static final boolean RED = true; + private static final boolean BLACK = false; + + private boolean isRed(Node x) { + if (x == null) + return false; + return x.color == RED; + } +} +``` + +## 1. 左旋转 + +因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。 + +

+ +```java +public Node rotateLeft(Node h) { + Node x = h.right; + h.right = x.left; + x.left = h; + x.color = h.color; + h.color = RED; + x.N = h.N; + recalculateSize(h); + return x; +} +``` + +## 2. 右旋转 + +进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。 + +

+ +```java +public Node rotateRight(Node h) { + Node x = h.left; + h.left = x.right; + x.color = h.color; + h.color = RED; + x.N = h.N; + recalculateSize(h); + return x; +} +``` + +## 3. 颜色转换 + +一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。 + +

+ +```java +void flipColors(Node h) { + h.color = RED; + h.left.color = BLACK; + h.right.color = BLACK; +} +``` + +## 4. 插入 + +先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作: + +- 如果右子节点是红色的而左子节点是黑色的,进行左旋转; +- 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转; +- 如果左右子节点均为红色的,进行颜色转换。 + +

+ +```java +@Override +public void put(Key key, Value value) { + root = put(root, key, value); + root.color = BLACK; +} + +private Node put(Node x, Key key, Value value) { + if (x == null) { + Node node = new Node(key, value, 1); + node.color = RED; + return node; + } + int cmp = key.compareTo(x.key); + if (cmp == 0) + x.val = value; + else if (cmp < 0) + x.left = put(x.left, key, value); + else + x.right = put(x.right, key, value); + + if (isRed(x.right) && !isRed(x.left)) + x = rotateLeft(x); + if (isRed(x.left) && isRed(x.left.left)) + x = rotateRight(x); + if (isRed(x.left) && isRed(x.right)) + flipColors(x); + + recalculateSize(x); + return x; +} +``` + +可以看到该插入操作和二叉查找树的插入操作类似,只是在最后加入了旋转和颜色变换操作即可。 + +根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1. + +## 5. 分析 + +一颗大小为 N 的红黑树的高度不会超过 2logN。最坏的情况下是它所对应的 2-3 树,构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 + +红黑树大多数的操作所需要的时间都是对数级别的。 + +# 散列表 + +散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。 + +由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。 + +## 1. 散列函数 + +对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。 + +散列表存在冲突,也就是两个不同的键可能有相同的 hash 值。 + +散列函数应该满足以下三个条件: + +- 一致性:相等的键应当有相等的 hash 值,两个键相等表示调用 equals() 返回的值相等。 +- 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回。 +- 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,如果不能满足这个条件,有可能产生很多冲突,从而导致散列表的性能下降。 + +除留余数法可以将整数散列到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 最好是一个素数,否则无法利用键包含的所有信息。例如 M 为 10k,那么只能利用键的后 k 位。 + +对于其它数,可以将其转换成整数的形式,然后利用除留余数法。例如对于浮点数,可以将其的二进制形式转换成整数。 + +对于多部分组合的类型,每个部分都需要计算 hash 值,这些 hash 值都具有同等重要的地位。为了达到这个目的,可以将该类型看成 R 进制的整数,每个部分都具有不同的权值。 + +例如,字符串的散列函数实现如下: + +```java +int hash = 0; +for (int i = 0; i < s.length(); i++) + hash = (R * hash + s.charAt(i)) % M; +``` + +再比如,拥有多个成员的自定义类的哈希函数如下: + +```java +int hash = (((day * R + month) % M) * R + year) % M; +``` + +R 通常取 31。 + +Java 中的 hashCode() 实现了哈希函数,但是默认使用对象的内存地址值。在使用 hashCode() 时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。 + +```java +int hash = (x.hashCode() & 0x7fffffff) % M; +``` + +使用 Java 的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode(): + +```java +public class Transaction { + + private final String who; + private final Date when; + private final double amount; + + public Transaction(String who, Date when, double amount) { + this.who = who; + this.when = when; + this.amount = amount; + } + + public int hashCode() { + int hash = 17; + int R = 31; + hash = R * hash + who.hashCode(); + hash = R * hash + when.hashCode(); + hash = R * hash + ((Double) amount).hashCode(); + return hash; + } +} +``` + +## 2. 拉链法 + +拉链法使用链表来存储 hash 值相同的键,从而解决冲突。 + +查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 + +对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 + +

+ +## 3. 线性探测法 + +线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。 + +使用线性探测法,数组的大小 M 应当大于键的个数 N(M>N)。 + +

+ +```java +public class LinearProbingHashST implements UnorderedST { + + private int N = 0; + private int M = 16; + private Key[] keys; + private Value[] values; + + public LinearProbingHashST() { + init(); + } + + public LinearProbingHashST(int M) { + this.M = M; + init(); + } + + private void init() { + keys = (Key[]) new Object[M]; + values = (Value[]) new Object[M]; + } + + private int hash(Key key) { + return (key.hashCode() & 0x7fffffff) % M; + } +} +``` + +#### 3.1 查找 + +```java +public Value get(Key key) { + for (int i = hash(key); keys[i] != null; i = (i + 1) % M) + if (keys[i].equals(key)) + return values[i]; + + return null; +} +``` + +#### 3.2 插入 + +```java +public void put(Key key, Value value) { + resize(); + putInternal(key, value); +} + +private void putInternal(Key key, Value value) { + int i; + for (i = hash(key); keys[i] != null; i = (i + 1) % M) + if (keys[i].equals(key)) { + values[i] = value; + return; + } + + keys[i] = key; + values[i] = value; + N++; +} +``` + +#### 3.3 删除 + +删除操作应当将右侧所有相邻的键值对重新插入散列表中。 + +```java +public void delete(Key key) { + int i = hash(key); + while (keys[i] != null && !key.equals(keys[i])) + i = (i + 1) % M; + + // 不存在,直接返回 + if (keys[i] == null) + return; + + keys[i] = null; + values[i] = null; + + // 将之后相连的键值对重新插入 + i = (i + 1) % M; + while (keys[i] != null) { + Key keyToRedo = keys[i]; + Value valToRedo = values[i]; + keys[i] = null; + values[i] = null; + N--; + putInternal(keyToRedo, valToRedo); + i = (i + 1) % M; + } + N--; + resize(); +} +``` + +#### 3.5 调整数组大小 + +线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。例如下图中 2\~5 位置就是一个聚簇。 + +

+ +α = N/M,把 α 称为使用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。 + +```java +private void resize() { + if (N >= M / 2) + resize(2 * M); + else if (N <= M / 8) + resize(M / 2); +} + +private void resize(int cap) { + LinearProbingHashST t = new LinearProbingHashST(cap); + for (int i = 0; i < M; i++) + if (keys[i] != null) + t.putInternal(keys[i], values[i]); + + keys = t.keys; + values = t.values; + M = t.M; +} +``` + +# 小结 + +## 1. 符号表算法比较 + +| 算法 | 插入 | 查找 | 是否有序 | +| :---: | :---: | :---: | :---: | +| 链表实现的无序符号表 | N | N | yes | +| 二分查找实现的有序符号表 | N | logN | yes | +| 二叉查找树 | logN | logN | yes | +| 2-3 查找树 | logN | logN | yes | +| 拉链法实现的散列表 | N/M | N/M | no | +| 线性探测法实现的散列表 | 1 | 1 | no | + +应当优先考虑散列表,当需要有序性操作时使用红黑树。 + +## 2. Java 的符号表实现 + +- java.util.TreeMap:红黑树 +- java.util.HashMap:拉链法的散列表 + +## 3. 稀疏向量乘法 + +当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。 + +```java +public class SparseVector { + private HashMap hashMap; + + public SparseVector(double[] vector) { + hashMap = new HashMap<>(); + for (int i = 0; i < vector.length; i++) + if (vector[i] != 0) + hashMap.put(i, vector[i]); + } + + public double get(int i) { + return hashMap.getOrDefault(i, 0.0); + } + + public double dot(SparseVector other) { + double sum = 0; + for (int i : hashMap.keySet()) + sum += this.get(i) * other.get(i); + return sum; + } +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法 - 算法分析.md b/docs/notes/算法 - 算法分析.md new file mode 100644 index 00000000..67cdb49c --- /dev/null +++ b/docs/notes/算法 - 算法分析.md @@ -0,0 +1,243 @@ + +* [数学模型](#数学模型) + * [1. 近似](#1-近似) + * [2. 增长数量级](#2-增长数量级) + * [3. 内循环](#3-内循环) + * [4. 成本模型](#4-成本模型) +* [注意事项](#注意事项) + * [1. 大常数](#1-大常数) + * [2. 缓存](#2-缓存) + * [3. 对最坏情况下的性能的保证](#3-对最坏情况下的性能的保证) + * [4. 随机化算法](#4-随机化算法) + * [5. 均摊分析](#5-均摊分析) +* [ThreeSum](#threesum) + * [1. ThreeSumSlow](#1-threesumslow) + * [2. ThreeSumBinarySearch](#2-threesumbinarysearch) + * [3. ThreeSumTwoPointer](#3-threesumtwopointer) +* [倍率实验](#倍率实验) + + + +# 数学模型 + +## 1. 近似 + +N3/6-N2/2+N/3 \~ N3/6。使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数。 + +## 2. 增长数量级 + +N3/6-N2/2+N/3 的增长数量级为 O(N3)。增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 O(N3) 与它是否用 Java 实现,是否运行于特定计算机上无关。 + +## 3. 内循环 + +执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。 + +## 4. 成本模型 + +使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 + +# 注意事项 + +## 1. 大常数 + +在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 + +## 2. 缓存 + +计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 + +## 3. 对最坏情况下的性能的保证 + +在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 + +## 4. 随机化算法 + +通过打乱输入,去除算法对输入的依赖。 + +## 5. 均摊分析 + +将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的次数为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素的操作次数,其余的都是调整数组大小时进行复制需要的访问数组次数),均摊后访问数组的平均次数为常数。 + +# ThreeSum + +ThreeSum 用于统计一个数组中和为 0 的三元组数量。 + +```java +public interface ThreeSum { + int count(int[] nums); +} +``` + +## 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 + public int count(int[] nums) { + int N = nums.length; + int cnt = 0; + for (int i = 0; i < N; i++) { + for (int j = i + 1; j < N; j++) { + for (int k = j + 1; k < N; k++) { + if (nums[i] + nums[j] + nums[k] == 0) { + cnt++; + } + } + } + } + return cnt; + } +} +``` + +## 2. ThreeSumBinarySearch + +通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 + +应该注意的是,只有数组不含有相同元素才能使用这种解法,否则二分查找的结果会出错。 + +该方法可以将 ThreeSum 算法增长数量级降低为 O(N2logN)。 + +```java +public class ThreeSumBinarySearch implements ThreeSum { + + @Override + public int count(int[] nums) { + Arrays.sort(nums); + int N = nums.length; + int cnt = 0; + for (int i = 0; i < N; i++) { + for (int j = i + 1; j < N; j++) { + int target = -nums[i] - nums[j]; + int index = BinarySearch.search(nums, target); + // 应该注意这里的下标必须大于 j,否则会重复统计。 + if (index > j) { + cnt++; + } + } + } + return cnt; + } +} +``` + +```java +public class BinarySearch { + + public static int search(int[] nums, int target) { + int l = 0, h = nums.length - 1; + while (l <= h) { + int m = l + (h - l) / 2; + if (target == nums[m]) { + return m; + } else if (target > nums[m]) { + l = m + 1; + } else { + h = m - 1; + } + } + return -1; + } +} +``` + +## 3. ThreeSumTwoPointer + +更有效的方法是先将数组排序,然后使用双指针进行查找,时间复杂度为 O(N2)。 + +```java +public class ThreeSumTwoPointer implements ThreeSum { + + @Override + public int count(int[] nums) { + int N = nums.length; + int cnt = 0; + Arrays.sort(nums); + for (int i = 0; i < N - 2; i++) { + int l = i + 1, h = N - 1, target = -nums[i]; + if (i > 0 && nums[i] == nums[i - 1]) continue; + while (l < h) { + int sum = nums[l] + nums[h]; + if (sum == target) { + cnt++; + while (l < h && nums[l] == nums[l + 1]) l++; + while (l < h && nums[h] == nums[h - 1]) h--; + l++; + h--; + } else if (sum < target) { + l++; + } else { + h--; + } + } + } + return cnt; + } +} +``` + +# 倍率实验 + +如果 T(N) \~ aNblogN,那么 T(2N)/T(N) \~ 2b。 + +例如对于暴力的 ThreeSum 算法,近似时间为 \~N3/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果: + +| N | Time(ms) | Ratio | +| :---: | :---: | :---: | +| 500 | 48 | / | +| 1000 | 320 | 6.7 | +| 2000 | 555 | 1.7 | +| 4000 | 4105 | 7.4 | +| 8000 | 33575 | 8.2 | +| 16000 | 268909 | 8.0 | + +可以看到,T(2N)/T(N) \~ 23,因此可以确定 T(N) \~ aN3logN。 + +```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; + } + } +} +``` + +```java +public class StopWatch { + + private static long start; + + + public static void start() { + start = System.currentTimeMillis(); + } + + + public static double elapsedTime() { + long now = System.currentTimeMillis(); + return (now - start) / 1000.0; + } +} +``` + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/算法.md b/docs/notes/算法.md deleted file mode 100644 index 3492026f..00000000 --- a/docs/notes/算法.md +++ /dev/null @@ -1,2361 +0,0 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) - -* [一、前言](#一前言) -* [二、算法分析](#二算法分析) - * [数学模型](#数学模型) - * [注意事项](#注意事项) - * [ThreeSum](#threesum) - * [倍率实验](#倍率实验) -* [三、排序](#三排序) - * [选择排序](#选择排序) - * [冒泡排序](#冒泡排序) - * [插入排序](#插入排序) - * [希尔排序](#希尔排序) - * [归并排序](#归并排序) - * [快速排序](#快速排序) - * [堆排序](#堆排序) - * [小结](#小结) -* [四、并查集](#四并查集) - * [Quick Find](#quick-find) - * [Quick Union](#quick-union) - * [加权 Quick Union](#加权-quick-union) - * [路径压缩的加权 Quick Union](#路径压缩的加权-quick-union) - * [比较](#比较) -* [五、栈和队列](#五栈和队列) - * [栈](#栈) - * [队列](#队列) -* [六、符号表](#六符号表) - * [初级实现](#初级实现) - * [二叉查找树](#二叉查找树) - * [2-3 查找树](#2-3-查找树) - * [红黑树](#红黑树) - * [散列表](#散列表) - * [小结](#小结) -* [七、其它](#七其它) - * [汉诺塔](#汉诺塔) - * [哈夫曼编码](#哈夫曼编码) -* [参考资料](#参考资料) - - - -# 一、前言 - -- 实现代码:[Algorithm](https://github.com/CyC2018/Algorithm) -- 绘图文件:[ProcessOn](https://www.processon.com/view/link/5a3e4c1ee4b0ce9ffea8c727) - -# 二、算法分析 - -## 数学模型 - -### 1. 近似 - -N3/6-N2/2+N/3 \~ N3/6。使用 \~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数。 - -### 2. 增长数量级 - -N3/6-N2/2+N/3 的增长数量级为 O(N3)。增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 O(N3) 与它是否用 Java 实现,是否运行于特定计算机上无关。 - -### 3. 内循环 - -执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。 - -### 4. 成本模型 - -使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。 - -## 注意事项 - -### 1. 大常数 - -在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。 - -### 2. 缓存 - -计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。 - -### 3. 对最坏情况下的性能的保证 - -在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。 - -### 4. 随机化算法 - -通过打乱输入,去除算法对输入的依赖。 - -### 5. 均摊分析 - -将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的次数为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素的操作次数,其余的都是调整数组大小时进行复制需要的访问数组次数),均摊后访问数组的平均次数为常数。 - -## ThreeSum - -ThreeSum 用于统计一个数组中和为 0 的三元组数量。 - -```java -public interface ThreeSum { - int count(int[] nums); -} -``` - -### 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 - public int count(int[] nums) { - int N = nums.length; - int cnt = 0; - for (int i = 0; i < N; i++) { - for (int j = i + 1; j < N; j++) { - for (int k = j + 1; k < N; k++) { - if (nums[i] + nums[j] + nums[k] == 0) { - cnt++; - } - } - } - } - return cnt; - } -} -``` - -### 2. ThreeSumBinarySearch - -通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 - -应该注意的是,只有数组不含有相同元素才能使用这种解法,否则二分查找的结果会出错。 - -该方法可以将 ThreeSum 算法增长数量级降低为 O(N2logN)。 - -```java -public class ThreeSumBinarySearch implements ThreeSum { - - @Override - public int count(int[] nums) { - Arrays.sort(nums); - int N = nums.length; - int cnt = 0; - for (int i = 0; i < N; i++) { - for (int j = i + 1; j < N; j++) { - int target = -nums[i] - nums[j]; - int index = BinarySearch.search(nums, target); - // 应该注意这里的下标必须大于 j,否则会重复统计。 - if (index > j) { - cnt++; - } - } - } - return cnt; - } -} -``` - -```java -public class BinarySearch { - - public static int search(int[] nums, int target) { - int l = 0, h = nums.length - 1; - while (l <= h) { - int m = l + (h - l) / 2; - if (target == nums[m]) { - return m; - } else if (target > nums[m]) { - l = m + 1; - } else { - h = m - 1; - } - } - return -1; - } -} -``` - -### 3. ThreeSumTwoPointer - -更有效的方法是先将数组排序,然后使用双指针进行查找,时间复杂度为 O(N2)。 - -```java -public class ThreeSumTwoPointer implements ThreeSum { - - @Override - public int count(int[] nums) { - int N = nums.length; - int cnt = 0; - Arrays.sort(nums); - for (int i = 0; i < N - 2; i++) { - int l = i + 1, h = N - 1, target = -nums[i]; - if (i > 0 && nums[i] == nums[i - 1]) continue; - while (l < h) { - int sum = nums[l] + nums[h]; - if (sum == target) { - cnt++; - while (l < h && nums[l] == nums[l + 1]) l++; - while (l < h && nums[h] == nums[h - 1]) h--; - l++; - h--; - } else if (sum < target) { - l++; - } else { - h--; - } - } - } - return cnt; - } -} -``` - -## 倍率实验 - -如果 T(N) \~ aNblogN,那么 T(2N)/T(N) \~ 2b。 - -例如对于暴力的 ThreeSum 算法,近似时间为 \~N3/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果: - -| N | Time(ms) | Ratio | -| :---: | :---: | :---: | -| 500 | 48 | / | -| 1000 | 320 | 6.7 | -| 2000 | 555 | 1.7 | -| 4000 | 4105 | 7.4 | -| 8000 | 33575 | 8.2 | -| 16000 | 268909 | 8.0 | - -可以看到,T(2N)/T(N) \~ 23,因此可以确定 T(N) \~ aN3logN。 - -```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; - } - } -} -``` - -```java -public class StopWatch { - - private static long start; - - - public static void start() { - start = System.currentTimeMillis(); - } - - - public static double elapsedTime() { - long now = System.currentTimeMillis(); - return (now - start) / 1000.0; - } -} -``` - -# 三、排序 - -待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 - -研究排序算法的成本模型时,统计的是比较和交换的次数。 - -使用辅助函数 less() 和 swap() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。 - -```java -public abstract class Sort> { - - public abstract void sort(T[] nums); - - protected boolean less(T v, T w) { - return v.compareTo(w) < 0; - } - - protected void swap(T[] a, int i, int j) { - T t = a[i]; - a[i] = a[j]; - a[j] = t; - } -} -``` - -## 选择排序 - -选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 - -选择排序需要 \~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 - 1; i++) { - int min = i; - for (int j = i + 1; j < N; j++) { - if (less(nums[j], nums[min])) { - min = j; - } - } - swap(nums, i, min); - } - } -} -``` - -## 冒泡排序 - -从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。 - -在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。 - -

- -```java -public class Bubble> extends Sort { - - @Override - public void sort(T[] nums) { - int N = nums.length; - boolean hasSorted = false; - for (int i = N - 1; i > 0 && !hasSorted; i--) { - hasSorted = true; - for (int j = 0; j < i; j++) { - if (less(nums[j + 1], nums[j])) { - hasSorted = false; - 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,因此插入排序需要交换的次数为逆序数量。 - -插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,逆序较少,那么插入排序会很快。 - -- 平均情况下插入排序需要 \~N2/4 比较以及 \~N2/4 次交换; -- 最坏的情况下需要 \~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。 - -希尔排序的出现就是为了解决插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。 - -希尔排序使用插入排序对间隔 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 >= 1) { - 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 的若干倍乘于递增序列的长度。后面介绍的高级排序算法只会比希尔排序快两倍左右。 - -## 归并排序 - -归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。 - -

- -### 1. 归并方法 - -归并方法将数组中两个已经排序的部分归并成一个。 - -```java -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++) { - if (i > m) { - nums[k] = aux[j++]; - - } else if (j > h) { - nums[k] = aux[i++]; - - } else if (aux[i].compareTo(aux[j]) <= 0) { - nums[k] = aux[i++]; // 先进行这一步,保证稳定性 - - } else { - nums[k] = aux[j++]; - } - } - } -} -``` - -### 2. 自顶向下归并排序 - -将一个大数组分成两个小数组去求解。 - -因为每次都将问题对半分成两个子问题,这种对半分的算法复杂度一般为 O(NlogN)。 - -```java -public class Up2DownMergeSort> extends MergeSort { - - @Override - public void sort(T[] nums) { - aux = (T[]) new Comparable[nums.length]; - sort(nums, 0, nums.length - 1); - } - - private void sort(T[] nums, int l, int h) { - if (h <= l) { - return; - } - int mid = l + (h - l) / 2; - sort(nums, l, mid); - sort(nums, mid + 1, h); - merge(nums, l, mid, h); - } -} -``` - -### 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) { - merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); - } - } - } -} - -``` - -## 快速排序 - -### 1. 基本算法 - -- 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序; -- 快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 - -

- -```java -public class QuickSort> extends Sort { - - @Override - public void sort(T[] nums) { - shuffle(nums); - sort(nums, 0, nums.length - 1); - } - - private void sort(T[] nums, int l, int h) { - if (h <= l) - return; - int j = partition(nums, l, h); - sort(nums, l, j - 1); - sort(nums, j + 1, h); - } - - private void shuffle(T[] nums) { - List list = Arrays.asList(nums); - Collections.shuffle(list); - list.toArray(nums); - } -} -``` - -### 2. 切分 - -取 a[l] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于它的元素,交换这两个元素。不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[l] 和 a[j] 交换位置。 - -

- -```java -private int partition(T[] nums, int l, int h) { - int i = l, j = h + 1; - T v = nums[l]; - while (true) { - while (less(nums[++i], v) && i != h) ; - while (less(v, nums[--j]) && j != l) ; - if (i >= j) - break; - swap(nums, i, j); - } - swap(nums, l, j); - return j; -} -``` - -### 3. 性能分析 - -快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 - -快速排序最好的情况下是每次都正好将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,复杂度为 O(NlogN)。 - -最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 - -### 4. 算法改进 - -#### 4.1 切换到插入排序 - -因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 - -#### 4.2 三数取中 - -最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。一种折中方法是取 3 个元素,并将大小居中的元素作为切分元素。 - -#### 4.3 三向切分 - -对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 - -三向切分快速排序对于有大量重复元素的随机数组可以在线性时间内完成排序。 - -```java -public class ThreeWayQuickSort> extends QuickSort { - - @Override - protected void sort(T[] nums, int l, int h) { - 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) { - swap(nums, lt++, i++); - } else if (cmp > 0) { - swap(nums, i, gt--); - } else { - i++; - } - } - sort(nums, l, lt - 1); - sort(nums, gt + 1, h); - } -} -``` - -### 5. 基于切分的快速选择算法 - -快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。 - -可以利用这个特性找出数组的第 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) { - return nums[k]; - - } else if (j > k) { - h = j - 1; - - } else { - l = j + 1; - } - } - return nums[k]; -} -``` - -## 堆排序 - -### 1. 堆 - -堆中某个节点的值总是大于等于其子节点的值,并且堆是一颗完全二叉树。 - -堆可以用数组来表示,这是因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。 - -

- -```java -public class Heap> { - - private T[] heap; - private int N = 0; - - public Heap(int maxN) { - this.heap = (T[]) new Comparable[maxN + 1]; - } - - public boolean isEmpty() { - return N == 0; - } - - public int size() { - return N; - } - - private boolean less(int i, int j) { - return heap[i].compareTo(heap[j]) < 0; - } - - private void swap(int i, int j) { - T t = heap[i]; - heap[i] = heap[j]; - heap[j] = t; - } -} -``` - -### 2. 上浮和下沉 - -在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为上浮。 - -

- -```java -private void swim(int k) { - while (k > 1 && less(k / 2, k)) { - swap(k / 2, k); - k = k / 2; - } -} -``` - -类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为下沉。一个节点如果有两个子节点,应当与两个子节点中最大那个节点进行交换。 - -

- -```java -private void sink(int k) { - while (2 * k <= N) { - int j = 2 * k; - if (j < N && less(j, j + 1)) - j++; - if (!less(k, j)) - break; - swap(k, j); - k = j; - } -} -``` - -### 3. 插入元素 - -将新元素放到数组末尾,然后上浮到合适的位置。 - -```java -public void insert(Comparable v) { - heap[++N] = v; - swim(N); -} -``` - -### 4. 删除最大元素 - -从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 - -```java -public T delMax() { - T max = heap[1]; - swap(1, N--); - heap[N + 1] = null; - sink(1); - return max; -} -``` - -### 5. 堆排序 - -把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列,这就是堆排序。 - -#### 5.1 构建堆 - -无序数组建立堆最直接的方法是从左到右遍历数组进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。 - -

- -#### 5.2 交换堆顶元素与最后一个元素 - -交换之后需要进行下沉操作维持堆的有序状态。 - -

- -```java -public class HeapSort> extends Sort { - /** - * 数组第 0 个位置不能有元素 - */ - @Override - public void sort(T[] nums) { - int N = nums.length - 1; - for (int k = N / 2; k >= 1; k--) - sink(nums, k, N); - - while (N > 1) { - swap(nums, 1, N--); - sink(nums, 1, N); - } - } - - private void sink(T[] nums, int k, int N) { - while (2 * k <= N) { - int j = 2 * k; - if (j < N && less(nums, j, j + 1)) - j++; - if (!less(nums, k, j)) - break; - swap(nums, k, j); - k = j; - } - } - - private boolean less(T[] nums, int i, int j) { - return nums[i].compareTo(nums[j]) < 0; - } -} -``` - -### 6. 分析 - -一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。 - -对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。 - -堆排序是一种原地排序,没有利用额外的空间。 - -现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。 - -## 小结 - -### 1. 排序算法的比较 - -| 算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 | -| :---: | :---: |:---: | :---: | :---: | -| 选择排序 | × | N2 | 1 | | -| 冒泡排序 | √ | N2 | 1 | | -| 插入排序 | √ | N \~ N2 | 1 | 时间复杂度和初始顺序有关 | -| 希尔排序 | × | N 的若干倍乘于递增序列的长度 | 1 | 改进版插入排序 | -| 快速排序 | × | NlogN | logN | | -| 三向切分快速排序 | × | N \~ NlogN | logN | 适用于有大量重复主键| -| 归并排序 | √ | NlogN | N | | -| 堆排序 | × | NlogN | 1 | 无法利用局部性原理| - -快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其它线性对数级别的排序算法都要小。 - -使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 - -### 2. Java 的排序算法实现 - -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 和 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)是一种存储键值对的数据结构,可以支持快速查找操作。 - -符号表分为有序和无序两种,有序符号表主要指支持 min()、max() 等根据键的大小关系来实现的操作。 - -有序符号表的键需要实现 Comparable 接口。 - -```java -public interface UnorderedST { - - int size(); - - Value get(Key key); - - void put(Key key, Value value); - - void delete(Key key); -} -``` - -```java -public interface OrderedST, Value> { - - int size(); - - void put(Key key, Value value); - - Value get(Key key); - - Key min(); - - Key max(); - - int rank(Key key); - - List keys(Key l, Key h); -} -``` - -## 初级实现 - -### 1. 链表实现无序符号表 - -```java -public class ListUnorderedST implements UnorderedST { - - private Node first; - - private class Node { - Key key; - Value value; - Node next; - - Node(Key key, Value value, Node next) { - this.key = key; - this.value = value; - this.next = next; - } - } - - @Override - public int size() { - int cnt = 0; - Node cur = first; - while (cur != null) { - cnt++; - cur = cur.next; - } - return cnt; - } - - @Override - public void put(Key key, Value value) { - Node cur = first; - // 如果在链表中找到节点的键等于 key 就更新这个节点的值为 value - while (cur != null) { - if (cur.key.equals(key)) { - cur.value = value; - return; - } - cur = cur.next; - } - // 否则使用头插法插入一个新节点 - first = new Node(key, value, first); - } - - @Override - public void delete(Key key) { - if (first == null) - return; - if (first.key.equals(key)) - first = first.next; - Node pre = first, cur = first.next; - while (cur != null) { - if (cur.key.equals(key)) { - pre.next = cur.next; - return; - } - pre = pre.next; - cur = cur.next; - } - } - - @Override - public Value get(Key key) { - Node cur = first; - while (cur != null) { - if (cur.key.equals(key)) - return cur.value; - cur = cur.next; - } - return null; - } -} -``` - -### 2. 二分查找实现有序符号表 - -使用一对平行数组,一个存储键一个存储值。 - -二分查找的 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 - -二分查找最多需要 logN+1 次比较,使用二分查找实现的符号表的查找操作所需要的时间最多是对数级别的。但是插入操作需要移动数组元素,是线性级别的。 - -```java -public class BinarySearchOrderedST, Value> implements OrderedST { - - private Key[] keys; - private Value[] values; - private int N = 0; - - public BinarySearchOrderedST(int capacity) { - keys = (Key[]) new Comparable[capacity]; - values = (Value[]) new Object[capacity]; - } - - @Override - public int size() { - return N; - } - - @Override - public int rank(Key key) { - int l = 0, h = N - 1; - while (l <= h) { - int m = l + (h - l) / 2; - int cmp = key.compareTo(keys[m]); - if (cmp == 0) - return m; - else if (cmp < 0) - h = m - 1; - else - l = m + 1; - } - return l; - } - - @Override - public List keys(Key l, Key h) { - int index = rank(l); - List list = new ArrayList<>(); - while (keys[index].compareTo(h) <= 0) { - list.add(keys[index]); - index++; - } - return list; - } - - @Override - public void put(Key key, Value value) { - int index = rank(key); - // 如果找到已经存在的节点键为 key,就更新这个节点的值为 value - if (index < N && keys[index].compareTo(key) == 0) { - values[index] = value; - return; - } - // 否则在数组中插入新的节点,需要先将插入位置之后的元素都向后移动一个位置 - for (int j = N; j > index; j--) { - keys[j] = keys[j - 1]; - values[j] = values[j - 1]; - } - keys[index] = key; - values[index] = value; - N++; - } - - @Override - public Value get(Key key) { - int index = rank(key); - if (index < N && keys[index].compareTo(key) == 0) - return values[index]; - return null; - } - - @Override - public Key min() { - return keys[0]; - } - - @Override - public Key max() { - return keys[N - 1]; - } -} -``` - -## 二叉查找树 - -**二叉树** 是一个空链接,或者是一个有左右两个链接的节点,每个链接都指向一颗子二叉树。 - -

- -**二叉查找树** (BST)是一颗二叉树,并且每个节点的值都大于等于其左子树中的所有节点的值而小于等于右子树的所有节点的值。 - -

- -BST 有一个重要性质,就是它的中序遍历结果递增排序。 - -

- -基本数据结构: - -```java -public class BST, Value> implements OrderedST { - - protected Node root; - - protected class Node { - Key key; - Value val; - Node left; - Node right; - // 以该节点为根的子树节点总数 - int N; - // 红黑树中使用 - boolean color; - - Node(Key key, Value val, int N) { - this.key = key; - this.val = val; - this.N = N; - } - } - - @Override - public int size() { - return size(root); - } - - private int size(Node x) { - if (x == null) - return 0; - return x.N; - } - - protected void recalculateSize(Node x) { - x.N = size(x.left) + size(x.right) + 1; - } -} -``` - -为了方便绘图,下文中二叉树的空链接不画出来。 - -### 1. get() - -- 如果树是空的,则查找未命中; -- 如果被查找的键和根节点的键相等,查找命中; -- 否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 - -```java -@Override -public Value get(Key key) { - return get(root, key); -} - -private Value get(Node x, Key key) { - if (x == null) - return null; - int cmp = key.compareTo(x.key); - if (cmp == 0) - return x.val; - else if (cmp < 0) - return get(x.left, key); - else - return get(x.right, key); -} -``` - -### 2. put() - -当插入的键不存在于树中,需要创建一个新节点,并且更新上层节点的链接指向该节点,使得该节点正确地链接到树中。 - -

- -```java - @Override -public void put(Key key, Value value) { - root = put(root, key, value); -} - -private Node put(Node x, Key key, Value value) { - if (x == null) - return new Node(key, value, 1); - int cmp = key.compareTo(x.key); - if (cmp == 0) - x.val = value; - else if (cmp < 0) - x.left = put(x.left, key, value); - else - x.right = put(x.right, key, value); - recalculateSize(x); - return x; -} -``` - -### 3. 分析 - -二叉查找树的算法运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。 - -最好的情况下树是完全平衡的,每条空链接和根节点的距离都为 logN。 - -

- -在最坏的情况下,树的高度为 N。 - -

- -### 4. floor() - -floor(key):小于等于键的最大键 - -- 如果键小于根节点的键,那么 floor(key) 一定在左子树中; -- 如果键大于根节点的键,需要先判断右子树中是否存在 floor(key),如果存在就返回,否则根节点就是 floor(key)。 - -```java -public Key floor(Key key) { - Node x = floor(root, key); - if (x == null) - return null; - return x.key; -} - -private Node floor(Node x, Key key) { - if (x == null) - return null; - int cmp = key.compareTo(x.key); - if (cmp == 0) - return x; - if (cmp < 0) - return floor(x.left, key); - Node t = floor(x.right, key); - return t != null ? t : x; -} -``` - -### 5. rank() - -rank(key) 返回 key 的排名。 - -- 如果键和根节点的键相等,返回左子树的节点数; -- 如果小于,递归计算在左子树中的排名; -- 如果大于,递归计算在右子树中的排名,加上左子树的节点数,再加上 1(根节点)。 - -```java -@Override -public int rank(Key key) { - return rank(key, root); -} - -private int rank(Key key, Node x) { - if (x == null) - return 0; - int cmp = key.compareTo(x.key); - if (cmp == 0) - return size(x.left); - else if (cmp < 0) - return rank(key, x.left); - else - return 1 + size(x.left) + rank(key, x.right); -} -``` - -### 6. min() - -```java -@Override -public Key min() { - return min(root).key; -} - -private Node min(Node x) { - if (x == null) - return null; - if (x.left == null) - return x; - return min(x.left); -} -``` - -### 7. deleteMin() - -令指向最小节点的链接指向最小节点的右子树。 - -

- -```java -public void deleteMin() { - root = deleteMin(root); -} - -public Node deleteMin(Node x) { - if (x.left == null) - return x.right; - x.left = deleteMin(x.left); - recalculateSize(x); - return x; -} -``` - -### 8. delete() - -- 如果待删除的节点只有一个子树, 那么只需要让指向待删除节点的链接指向唯一的子树即可; -- 否则,让右子树的最小节点替换该节点。 - -

- -```java -public void delete(Key key) { - root = delete(root, key); -} -private Node delete(Node x, Key key) { - if (x == null) - return null; - int cmp = key.compareTo(x.key); - if (cmp < 0) - x.left = delete(x.left, key); - else if (cmp > 0) - x.right = delete(x.right, key); - else { - if (x.right == null) - return x.left; - if (x.left == null) - return x.right; - Node t = x; - x = min(t.right); - x.right = deleteMin(t.right); - x.left = t.left; - } - recalculateSize(x); - return x; -} -``` - -### 9. keys() - -利用二叉查找树中序遍历的结果为递增的特点。 - -```java -@Override -public List keys(Key l, Key h) { - return keys(root, l, h); -} - -private List keys(Node x, Key l, Key h) { - List list = new ArrayList<>(); - if (x == null) - return list; - int cmpL = l.compareTo(x.key); - int cmpH = h.compareTo(x.key); - if (cmpL < 0) - list.addAll(keys(x.left, l, h)); - if (cmpL <= 0 && cmpH >= 0) - list.add(x.key); - if (cmpH > 0) - list.addAll(keys(x.right, l, h)); - return list; -} -``` - -### 10. 分析 - -二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。 - -## 2-3 查找树 - -2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 - -

- -### 1. 插入操作 - -插入操作和 BST 的插入操作有很大区别,BST 的插入操作是先进行一次未命中的查找,然后再将节点插入到对应的空链接上。但是 2-3 查找树如果也这么做的话,那么就会破坏了平衡性。它是将新节点插入到叶子节点上。 - -根据叶子节点的类型不同,有不同的处理方式: - -- 如果插入到 2- 节点上,那么直接将新节点和原来的节点组成 3- 节点即可。 - -

- -- 如果是插入到 3- 节点上,就会产生一个临时 4- 节点时,需要将 4- 节点分裂成 3 个 2- 节点,并将中间的 2- 节点移到上层节点中。如果上移操作继续产生临时 4- 节点则一直进行分裂上移,直到不存在临时 4- 节点。 - -

- -### 2. 性质 - -2-3 查找树插入操作的变换都是局部的,除了相关的节点和链接之外不必修改或者检查树的其它部分,而这些局部变换不会影响树的全局有序性和平衡性。 - -2-3 查找树的查找和插入操作复杂度和插入顺序无关,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 - -## 红黑树 - -红黑树是 2-3 查找树,但它不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 - -

- -红黑树具有以下性质: - -- 红链接都为左链接; -- 完美黑色平衡,即任意空链接到根节点的路径上的黑链接数量相同。 - -画红黑树时可以将红链接画平。 - -

- -```java -public class RedBlackBST, Value> extends BST { - - private static final boolean RED = true; - private static final boolean BLACK = false; - - private boolean isRed(Node x) { - if (x == null) - return false; - return x.color == RED; - } -} -``` - -### 1. 左旋转 - -因为合法的红链接都为左链接,如果出现右链接为红链接,那么就需要进行左旋转操作。 - -

- -```java -public Node rotateLeft(Node h) { - Node x = h.right; - h.right = x.left; - x.left = h; - x.color = h.color; - h.color = RED; - x.N = h.N; - recalculateSize(h); - return x; -} -``` - -### 2. 右旋转 - -进行右旋转是为了转换两个连续的左红链接,这会在之后的插入过程中探讨。 - -

- -```java -public Node rotateRight(Node h) { - Node x = h.left; - h.left = x.right; - x.color = h.color; - h.color = RED; - x.N = h.N; - recalculateSize(h); - return x; -} -``` - -### 3. 颜色转换 - -一个 4- 节点在红黑树中表现为一个节点的左右子节点都是红色的。分裂 4- 节点除了需要将子节点的颜色由红变黑之外,同时需要将父节点的颜色由黑变红,从 2-3 树的角度看就是将中间节点移到上层节点。 - -

- -```java -void flipColors(Node h) { - h.color = RED; - h.left.color = BLACK; - h.right.color = BLACK; -} -``` - -### 4. 插入 - -先将一个节点按二叉查找树的方法插入到正确位置,然后再进行如下颜色操作: - -- 如果右子节点是红色的而左子节点是黑色的,进行左旋转; -- 如果左子节点是红色的,而且左子节点的左子节点也是红色的,进行右旋转; -- 如果左右子节点均为红色的,进行颜色转换。 - -

- -```java -@Override -public void put(Key key, Value value) { - root = put(root, key, value); - root.color = BLACK; -} - -private Node put(Node x, Key key, Value value) { - if (x == null) { - Node node = new Node(key, value, 1); - node.color = RED; - return node; - } - int cmp = key.compareTo(x.key); - if (cmp == 0) - x.val = value; - else if (cmp < 0) - x.left = put(x.left, key, value); - else - x.right = put(x.right, key, value); - - if (isRed(x.right) && !isRed(x.left)) - x = rotateLeft(x); - if (isRed(x.left) && isRed(x.left.left)) - x = rotateRight(x); - if (isRed(x.left) && isRed(x.right)) - flipColors(x); - - recalculateSize(x); - return x; -} -``` - -可以看到该插入操作和二叉查找树的插入操作类似,只是在最后加入了旋转和颜色变换操作即可。 - -根节点一定为黑色,因为根节点没有上层节点,也就没有上层节点的左链接指向根节点。flipColors() 有可能会使得根节点的颜色变为红色,每当根节点由红色变成黑色时树的黑链接高度加 1. - -### 5. 分析 - -一颗大小为 N 的红黑树的高度不会超过 2logN。最坏的情况下是它所对应的 2-3 树,构成最左边的路径节点全部都是 3- 节点而其余都是 2- 节点。 - -红黑树大多数的操作所需要的时间都是对数级别的。 - -## 散列表 - -散列表类似于数组,可以把散列表的散列值看成数组的索引值。访问散列表和访问数组元素一样快速,它可以在常数时间内实现查找和插入操作。 - -由于无法通过散列值知道键的大小关系,因此散列表无法实现有序性操作。 - -### 1. 散列函数 - -对于一个大小为 M 的散列表,散列函数能够把任意键转换为 [0, M-1] 内的正整数,该正整数即为 hash 值。 - -散列表存在冲突,也就是两个不同的键可能有相同的 hash 值。 - -散列函数应该满足以下三个条件: - -- 一致性:相等的键应当有相等的 hash 值,两个键相等表示调用 equals() 返回的值相等。 -- 高效性:计算应当简便,有必要的话可以把 hash 值缓存起来,在调用 hash 函数时直接返回。 -- 均匀性:所有键的 hash 值应当均匀地分布到 [0, M-1] 之间,如果不能满足这个条件,有可能产生很多冲突,从而导致散列表的性能下降。 - -除留余数法可以将整数散列到 [0, M-1] 之间,例如一个正整数 k,计算 k%M 既可得到一个 [0, M-1] 之间的 hash 值。注意 M 最好是一个素数,否则无法利用键包含的所有信息。例如 M 为 10k,那么只能利用键的后 k 位。 - -对于其它数,可以将其转换成整数的形式,然后利用除留余数法。例如对于浮点数,可以将其的二进制形式转换成整数。 - -对于多部分组合的类型,每个部分都需要计算 hash 值,这些 hash 值都具有同等重要的地位。为了达到这个目的,可以将该类型看成 R 进制的整数,每个部分都具有不同的权值。 - -例如,字符串的散列函数实现如下: - -```java -int hash = 0; -for (int i = 0; i < s.length(); i++) - hash = (R * hash + s.charAt(i)) % M; -``` - -再比如,拥有多个成员的自定义类的哈希函数如下: - -```java -int hash = (((day * R + month) % M) * R + year) % M; -``` - -R 通常取 31。 - -Java 中的 hashCode() 实现了哈希函数,但是默认使用对象的内存地址值。在使用 hashCode() 时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。 - -```java -int hash = (x.hashCode() & 0x7fffffff) % M; -``` - -使用 Java 的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode(): - -```java -public class Transaction { - - private final String who; - private final Date when; - private final double amount; - - public Transaction(String who, Date when, double amount) { - this.who = who; - this.when = when; - this.amount = amount; - } - - public int hashCode() { - int hash = 17; - int R = 31; - hash = R * hash + who.hashCode(); - hash = R * hash + when.hashCode(); - hash = R * hash + ((Double) amount).hashCode(); - return hash; - } -} -``` - -### 2. 拉链法 - -拉链法使用链表来存储 hash 值相同的键,从而解决冲突。 - -查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 - -对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 - -

- -### 3. 线性探测法 - -线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。 - -使用线性探测法,数组的大小 M 应当大于键的个数 N(M>N)。 - -

- -```java -public class LinearProbingHashST implements UnorderedST { - - private int N = 0; - private int M = 16; - private Key[] keys; - private Value[] values; - - public LinearProbingHashST() { - init(); - } - - public LinearProbingHashST(int M) { - this.M = M; - init(); - } - - private void init() { - keys = (Key[]) new Object[M]; - values = (Value[]) new Object[M]; - } - - private int hash(Key key) { - return (key.hashCode() & 0x7fffffff) % M; - } -} -``` - -#### 3.1 查找 - -```java -public Value get(Key key) { - for (int i = hash(key); keys[i] != null; i = (i + 1) % M) - if (keys[i].equals(key)) - return values[i]; - - return null; -} -``` - -#### 3.2 插入 - -```java -public void put(Key key, Value value) { - resize(); - putInternal(key, value); -} - -private void putInternal(Key key, Value value) { - int i; - for (i = hash(key); keys[i] != null; i = (i + 1) % M) - if (keys[i].equals(key)) { - values[i] = value; - return; - } - - keys[i] = key; - values[i] = value; - N++; -} -``` - -#### 3.3 删除 - -删除操作应当将右侧所有相邻的键值对重新插入散列表中。 - -```java -public void delete(Key key) { - int i = hash(key); - while (keys[i] != null && !key.equals(keys[i])) - i = (i + 1) % M; - - // 不存在,直接返回 - if (keys[i] == null) - return; - - keys[i] = null; - values[i] = null; - - // 将之后相连的键值对重新插入 - i = (i + 1) % M; - while (keys[i] != null) { - Key keyToRedo = keys[i]; - Value valToRedo = values[i]; - keys[i] = null; - values[i] = null; - N--; - putInternal(keyToRedo, valToRedo); - i = (i + 1) % M; - } - N--; - resize(); -} -``` - -#### 3.5 调整数组大小 - -线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。例如下图中 2\~5 位置就是一个聚簇。 - -

- -α = N/M,把 α 称为使用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。 - -```java -private void resize() { - if (N >= M / 2) - resize(2 * M); - else if (N <= M / 8) - resize(M / 2); -} - -private void resize(int cap) { - LinearProbingHashST t = new LinearProbingHashST(cap); - for (int i = 0; i < M; i++) - if (keys[i] != null) - t.putInternal(keys[i], values[i]); - - keys = t.keys; - values = t.values; - M = t.M; -} -``` - -## 小结 - -### 1. 符号表算法比较 - -| 算法 | 插入 | 查找 | 是否有序 | -| :---: | :---: | :---: | :---: | -| 链表实现的无序符号表 | N | N | yes | -| 二分查找实现的有序符号表 | N | logN | yes | -| 二叉查找树 | logN | logN | yes | -| 2-3 查找树 | logN | logN | yes | -| 拉链法实现的散列表 | N/M | N/M | no | -| 线性探测法实现的散列表 | 1 | 1 | no | - -应当优先考虑散列表,当需要有序性操作时使用红黑树。 - -### 2. Java 的符号表实现 - -- java.util.TreeMap:红黑树 -- java.util.HashMap:拉链法的散列表 - -### 3. 稀疏向量乘法 - -当向量为稀疏向量时,可以使用符号表来存储向量中的非 0 索引和值,使得乘法运算只需要对那些非 0 元素进行即可。 - -```java -public class SparseVector { - private HashMap hashMap; - - public SparseVector(double[] vector) { - hashMap = new HashMap<>(); - for (int i = 0; i < vector.length; i++) - if (vector[i] != 0) - hashMap.put(i, vector[i]); - } - - public double get(int i) { - return hashMap.getOrDefault(i, 0.0); - } - - public double dot(SparseVector other) { - double sum = 0; - for (int i : hashMap.keySet()) - sum += this.get(i) * other.get(i); - return sum; - } -} -``` - -# 七、其它 - -## 汉诺塔 - -

- -有三个柱子,分别为 from、buffer、to。需要将 from 上的圆盘全部移动到 to 上,并且要保证小圆盘始终在大圆盘上。 - -这是一个经典的递归问题,分为三步求解: - -① 将 n-1 个圆盘从 from -> buffer - -

- -② 将 1 个圆盘从 from -> to - -

- -③ 将 n-1 个圆盘从 buffer -> to - -

- -如果只有一个圆盘,那么只需要进行一次移动操作。 - -从上面的讨论可以知道,an = 2 * an-1 + 1,显然 an = 2n - 1,n 个圆盘需要移动 2n - 1 次。 - -```java -public class Hanoi { - public static void move(int n, String from, String buffer, String to) { - if (n == 1) { - System.out.println("from " + from + " to " + to); - return; - } - move(n - 1, from, to, buffer); - move(1, from, buffer, to); - move(n - 1, buffer, from, to); - } - - public static void main(String[] args) { - Hanoi.move(3, "H1", "H2", "H3"); - } -} -``` - -```html -from H1 to H3 -from H1 to H2 -from H3 to H2 -from H1 to H3 -from H2 to H1 -from H2 to H3 -from H1 to H3 -``` - -## 哈夫曼编码 - -根据数据出现的频率对数据进行编码,从而压缩原始数据。 - -例如对于一个文本文件,其中各种字符出现的次数如下: - -- a : 10 -- b : 20 -- c : 40 -- d : 80 - -可以将每种字符转换成二进制编码,例如将 a 转换为 00,b 转换为 01,c 转换为 10,d 转换为 11。这是最简单的一种编码方式,没有考虑各个字符的权值(出现频率)。而哈夫曼编码采用了贪心策略,使出现频率最高的字符的编码最短,从而保证整体的编码长度最短。 - -首先生成一颗哈夫曼树,每次生成过程中选取频率最少的两个节点,生成一个新节点作为它们的父节点,并且新节点的频率为两个节点的和。选取频率最少的原因是,生成过程使得先选取的节点位于树的更低层,那么需要的编码长度更长,频率更少可以使得总编码长度更少。 - -生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到叶子节点,叶子节点代表的字符的编码就是这个路径编码。 - -

- -```java -public class Huffman { - - private class Node implements Comparable { - char ch; - int freq; - boolean isLeaf; - Node left, right; - - public Node(char ch, int freq) { - this.ch = ch; - this.freq = freq; - isLeaf = true; - } - - public Node(Node left, Node right, int freq) { - this.left = left; - this.right = right; - this.freq = freq; - isLeaf = false; - } - - @Override - public int compareTo(Node o) { - return this.freq - o.freq; - } - } - - public Map encode(Map frequencyForChar) { - PriorityQueue priorityQueue = new PriorityQueue<>(); - for (Character c : frequencyForChar.keySet()) { - priorityQueue.add(new Node(c, frequencyForChar.get(c))); - } - while (priorityQueue.size() != 1) { - Node node1 = priorityQueue.poll(); - Node node2 = priorityQueue.poll(); - priorityQueue.add(new Node(node1, node2, node1.freq + node2.freq)); - } - return encode(priorityQueue.poll()); - } - - private Map encode(Node root) { - Map encodingForChar = new HashMap<>(); - encode(root, "", encodingForChar); - return encodingForChar; - } - - private void encode(Node node, String encoding, Map encodingForChar) { - if (node.isLeaf) { - encodingForChar.put(node.ch, encoding); - return; - } - encode(node.left, encoding + '0', encodingForChar); - encode(node.right, encoding + '1', encodingForChar); - } -} -``` - -# 参考资料 - -- Sedgewick, Robert, and Kevin Wayne. _Algorithms_. Addison-Wesley Professional, 2011. - diff --git a/docs/notes/系统设计基础.md b/docs/notes/系统设计基础.md index 2b095426..9fa7dff8 100644 --- a/docs/notes/系统设计基础.md +++ b/docs/notes/系统设计基础.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、性能](#一性能) * [二、伸缩性](#二伸缩性) @@ -108,3 +107,9 @@ # 参考资料 - 大型网站技术架构:核心原理与案例分析 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/缓存.md b/docs/notes/缓存.md index 3e43a32b..c59257ea 100644 --- a/docs/notes/缓存.md +++ b/docs/notes/缓存.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、缓存特征](#一缓存特征) * [二、LRU](#二lru) @@ -284,3 +283,9 @@ Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了 - [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849) - [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF) - [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 内存管理.md b/docs/notes/计算机操作系统 - 内存管理.md new file mode 100644 index 00000000..944694ec --- /dev/null +++ b/docs/notes/计算机操作系统 - 内存管理.md @@ -0,0 +1,142 @@ + +* [虚拟内存](#虚拟内存) +* [分页系统地址映射](#分页系统地址映射) +* [页面置换算法](#页面置换算法) + * [1. 最佳](#1-最佳) + * [2. 最近最久未使用](#2-最近最久未使用) + * [3. 最近未使用](#3-最近未使用) + * [4. 先进先出](#4-先进先出) + * [5. 第二次机会算法](#5-第二次机会算法) + * [6. 时钟](#6-时钟) +* [分段](#分段) +* [段页式](#段页式) +* [分页与分段的比较](#分页与分段的比较) + + + +# 虚拟内存 + +虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 + +为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 + +从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。 + +

+ +# 分页系统地址映射 + +内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。 + +一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。 + +下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。例如对于虚拟地址(0010 000000000100),前 4 位是存储页面号 2,读取表项内容为(110 1),页表项最后一位表示是否存在于内存中,1 表示存在。后 12 位存储偏移量。这个页对应的页框的地址为 (110 000000000100)。 + +

+ +# 页面置换算法 + +在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 + +页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。 + +页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 + +## 1. 最佳 + +> OPT, Optimal replacement algorithm + +所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。 + +是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 + +举例:一个系统为某进程分配了三个物理块,并有如下页面引用序列: + +

+ +开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。 + +## 2. 最近最久未使用 + +> LRU, Least Recently Used + +虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。 + +为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。 + +因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。 + +

+ +

+ +## 3. 最近未使用 + +> NRU, Not Recently Used + +每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类: + +- R=0,M=0 +- R=0,M=1 +- R=1,M=0 +- R=1,M=1 + +当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。 + +NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。 + +## 4. 先进先出 + +> FIFO, First In First Out + +选择换出的页面是最先进入的页面。 + +该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。 + +## 5. 第二次机会算法 + +FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改: + +当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。 + +

+ +## 6. 时钟 + +> Clock + +第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。 + +

+ +# 分段 + +虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。 + +下图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。 + +

+ +分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 + +

+ +# 段页式 + +程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。 + +# 分页与分段的比较 + +- 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。 + +- 地址空间的维度:分页是一维地址空间,分段是二维的。 + +- 大小是否可以改变:页的大小不可变,段的大小可以动态改变。 + +- 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 概述.md b/docs/notes/计算机操作系统 - 概述.md new file mode 100644 index 00000000..5653d20d --- /dev/null +++ b/docs/notes/计算机操作系统 - 概述.md @@ -0,0 +1,128 @@ + +* [基本特征](#基本特征) + * [1. 并发](#1-并发) + * [2. 共享](#2-共享) + * [3. 虚拟](#3-虚拟) + * [4. 异步](#4-异步) +* [基本功能](#基本功能) + * [1. 进程管理](#1-进程管理) + * [2. 内存管理](#2-内存管理) + * [3. 文件管理](#3-文件管理) + * [4. 设备管理](#4-设备管理) +* [系统调用](#系统调用) +* [宏内核和微内核](#宏内核和微内核) + * [1. 宏内核](#1-宏内核) + * [2. 微内核](#2-微内核) +* [中断分类](#中断分类) + * [1. 外中断](#1-外中断) + * [2. 异常](#2-异常) + * [3. 陷入](#3-陷入) + + + +# 基本特征 + +## 1. 并发 + +并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。 + +并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。 + +操作系统通过引入进程和线程,使得程序能够并发运行。 + +## 2. 共享 + +共享是指系统中的资源可以被多个并发进程共同使用。 + +有两种共享方式:互斥共享和同时共享。 + +互斥共享的资源称为临界资源,例如打印机等,在同一时间只允许一个进程访问,需要用同步机制来实现对临界资源的访问。 + +## 3. 虚拟 + +虚拟技术把一个物理实体转换为多个逻辑实体。 + +主要有两种虚拟技术:时分复用技术和空分复用技术。 + +多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。 + +虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。 + +## 4. 异步 + +异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 + +# 基本功能 + +## 1. 进程管理 + +进程控制、进程同步、进程通信、死锁处理、处理机调度等。 + +## 2. 内存管理 + +内存分配、地址映射、内存保护与共享、虚拟内存等。 + +## 3. 文件管理 + +文件存储空间的管理、目录管理、文件读写管理和保护等。 + +## 4. 设备管理 + +完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。 + +主要包括缓冲管理、设备分配、设备处理、虛拟设备等。 + +# 系统调用 + +如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。 + +

+ +Linux 的系统调用主要有以下这些: + +| Task | Commands | +| :---: | --- | +| 进程控制 | fork(); exit(); wait(); | +| 进程通信 | pipe(); shmget(); mmap(); | +| 文件操作 | open(); read(); write(); | +| 设备操作 | ioctl(); read(); write(); | +| 信息维护 | getpid(); alarm(); sleep(); | +| 安全 | chmod(); umask(); chown(); | + +# 宏内核和微内核 + +## 1. 宏内核 + +宏内核是将操作系统功能作为一个紧密结合的整体放到内核。 + +由于各模块共享信息,因此有很高的性能。 + +## 2. 微内核 + +由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。 + +在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。 + +因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。 + +

+ +# 中断分类 + +## 1. 外中断 + +由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。 + +## 2. 异常 + +由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。 + +## 3. 陷入 + +在用户程序中使用系统调用。 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 死锁.md b/docs/notes/计算机操作系统 - 死锁.md new file mode 100644 index 00000000..b284c886 --- /dev/null +++ b/docs/notes/计算机操作系统 - 死锁.md @@ -0,0 +1,148 @@ + +* [必要条件](#必要条件) +* [处理方法](#处理方法) +* [鸵鸟策略](#鸵鸟策略) +* [死锁检测与死锁恢复](#死锁检测与死锁恢复) + * [1. 每种类型一个资源的死锁检测](#1-每种类型一个资源的死锁检测) + * [2. 每种类型多个资源的死锁检测](#2-每种类型多个资源的死锁检测) + * [3. 死锁恢复](#3-死锁恢复) +* [死锁预防](#死锁预防) + * [1. 破坏互斥条件](#1-破坏互斥条件) + * [2. 破坏占有和等待条件](#2-破坏占有和等待条件) + * [3. 破坏不可抢占条件](#3-破坏不可抢占条件) + * [4. 破坏环路等待](#4-破坏环路等待) +* [死锁避免](#死锁避免) + * [1. 安全状态](#1-安全状态) + * [2. 单个资源的银行家算法](#2-单个资源的银行家算法) + * [3. 多个资源的银行家算法](#3-多个资源的银行家算法) + + + +# 必要条件 + +

+ +- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 +- 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 +- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 +- 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 + +# 处理方法 + +主要有以下四种方法: + +- 鸵鸟策略 +- 死锁检测与死锁恢复 +- 死锁预防 +- 死锁避免 + +# 鸵鸟策略 + +把头埋在沙子里,假装根本没发生问题。 + +因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。 + +当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。 + +大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。 + +# 死锁检测与死锁恢复 + +不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 + +## 1. 每种类型一个资源的死锁检测 + +

+ +上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。 + +图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。 + +每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。 + +## 2. 每种类型多个资源的死锁检测 + +

+ +上图中,有三个进程四个资源,每个数据代表的含义如下: + +- E 向量:资源总量 +- A 向量:资源剩余量 +- C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量 +- R 矩阵:每个进程请求的资源数量 + +进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。 + +算法总结如下: + +每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。 + +1. 寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。 +2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。 +3. 如果没有这样一个进程,算法终止。 + +## 3. 死锁恢复 + +- 利用抢占恢复 +- 利用回滚恢复 +- 通过杀死进程恢复 + +# 死锁预防 + +在程序运行之前预防发生死锁。 + +## 1. 破坏互斥条件 + +例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。 + +## 2. 破坏占有和等待条件 + +一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 + +## 3. 破坏不可抢占条件 + +## 4. 破坏环路等待 + +给资源统一编号,进程只能按编号顺序来请求资源。 + +# 死锁避免 + +在程序运行时避免发生死锁。 + +## 1. 安全状态 + +

+ +图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。 + +定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。 + +安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。 + +## 2. 单个资源的银行家算法 + +一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。 + +

+ +上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 + +## 3. 多个资源的银行家算法 + +

+ +上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。 + +检查一个状态是否安全的算法如下: + +- 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。 +- 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。 +- 重复以上两步,直到所有进程都标记为终止,则状态时安全的。 + +如果一个状态不是安全的,需要拒绝进入这个状态。 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 目录.md b/docs/notes/计算机操作系统 - 目录.md new file mode 100644 index 00000000..cbe35f74 --- /dev/null +++ b/docs/notes/计算机操作系统 - 目录.md @@ -0,0 +1,32 @@ + +* [目录](#目录) +* [参考资料](#参考资料) + + + +# 目录 + +- [概述](计算机操作系统%20-%20概述.md) +- [进程管理](计算机操作系统%20-%20进程管理.md) +- [死锁](计算机操作系统%20-%20死锁.md) +- [内存管理](计算机操作系统%20-%20内存管理.md) +- [设备管理](计算机操作系统%20-%20设备管理.md) +- [链接](计算机操作系统%20-%20链接.md) + +# 参考资料 + +- Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014. +- 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001. +- Bryant, R. E., & O’Hallaron, D. R. (2004). 深入理解计算机系统. +- 史蒂文斯. UNIX 环境高级编程 [M]. 人民邮电出版社, 2014. +- [Operating System Notes](https://applied-programming.github.io/Operating-Systems-Notes/) +- [Operating-System Structures](https://www.cs.uic.edu/\~jbell/CourseNotes/OperatingSystems/2_Structures.html) +- [Processes](http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php) +- [Inter Process Communication Presentation[1]](https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1) +- [Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 目录1.md b/docs/notes/计算机操作系统 - 目录1.md new file mode 100644 index 00000000..0f17d34f --- /dev/null +++ b/docs/notes/计算机操作系统 - 目录1.md @@ -0,0 +1,32 @@ + +* [目录](#目录) +* [参考资料](#参考资料) + + + +# 目录 + +- [概述](notes/计算机操作系统%20-%20概述.md) +- [进程管理](notes/计算机操作系统%20-%20进程管理.md) +- [死锁](notes/计算机操作系统%20-%20死锁.md) +- [内存管理](notes/计算机操作系统%20-%20内存管理.md) +- [设备管理](notes/计算机操作系统%20-%20设备管理.md) +- [链接](notes/计算机操作系统%20-%20链接.md) + +# 参考资料 + +- Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014. +- 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001. +- Bryant, R. E., & O’Hallaron, D. R. (2004). 深入理解计算机系统. +- 史蒂文斯. UNIX 环境高级编程 [M]. 人民邮电出版社, 2014. +- [Operating System Notes](https://applied-programming.github.io/Operating-Systems-Notes/) +- [Operating-System Structures](https://www.cs.uic.edu/\~jbell/CourseNotes/OperatingSystems/2_Structures.html) +- [Processes](http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php) +- [Inter Process Communication Presentation[1]](https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1) +- [Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 设备管理.md b/docs/notes/计算机操作系统 - 设备管理.md new file mode 100644 index 00000000..997a40d6 --- /dev/null +++ b/docs/notes/计算机操作系统 - 设备管理.md @@ -0,0 +1,65 @@ + +* [磁盘结构](#磁盘结构) +* [磁盘调度算法](#磁盘调度算法) + * [1. 先来先服务](#1-先来先服务) + * [2. 最短寻道时间优先](#2-最短寻道时间优先) + * [3. 电梯算法](#3-电梯算法) + + + +# 磁盘结构 + +- 盘面(Platter):一个磁盘有多个盘面; +- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道; +- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小; +- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写); +- 制动手臂(Actuator arm):用于在磁道之间移动磁头; +- 主轴(Spindle):使整个盘面转动。 + +

+ +# 磁盘调度算法 + +读写一个磁盘块的时间的影响因素有: + +- 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上) +- 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上) +- 实际的数据传输时间 + +其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。 + +## 1. 先来先服务 + +> FCFS, First Come First Served + +按照磁盘请求的顺序进行调度。 + +优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 + +## 2. 最短寻道时间优先 + +> SSTF, Shortest Seek Time First + +优先调度与当前磁头所在磁道距离最近的磁道。 + +虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。 + +

+ +## 3. 电梯算法 + +> SCAN + +电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。 + +电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。 + +因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。 + +

+ + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 进程管理.md b/docs/notes/计算机操作系统 - 进程管理.md new file mode 100644 index 00000000..f62a42a8 --- /dev/null +++ b/docs/notes/计算机操作系统 - 进程管理.md @@ -0,0 +1,594 @@ + +* [进程与线程](#进程与线程) + * [1. 进程](#1-进程) + * [2. 线程](#2-线程) + * [3. 区别](#3-区别) +* [进程状态的切换](#进程状态的切换) +* [进程调度算法](#进程调度算法) + * [1. 批处理系统](#1-批处理系统) + * [2. 交互式系统](#2-交互式系统) + * [3. 实时系统](#3-实时系统) +* [进程同步](#进程同步) + * [1. 临界区](#1-临界区) + * [2. 同步与互斥](#2-同步与互斥) + * [3. 信号量](#3-信号量) + * [4. 管程](#4-管程) +* [经典同步问题](#经典同步问题) + * [1. 读者-写者问题](#1-读者-写者问题) + * [2. 哲学家进餐问题](#2-哲学家进餐问题) +* [进程通信](#进程通信) + * [1. 管道](#1-管道) + * [2. FIFO](#2-fifo) + * [3. 消息队列](#3-消息队列) + * [4. 信号量](#4-信号量) + * [5. 共享存储](#5-共享存储) + * [6. 套接字](#6-套接字) + + + +# 进程与线程 + +## 1. 进程 + +进程是资源分配的基本单位。 + +进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 + +下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。 + +

+ +## 2. 线程 + +线程是独立调度的基本单位。 + +一个进程中可以有多个线程,它们共享进程资源。 + +QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 + +

+ +## 3. 区别 + +Ⅰ 拥有资源 + +进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 + +Ⅱ 调度 + +线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。 + +Ⅲ 系统开销 + +由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 + +Ⅳ 通信方面 + +线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。 + +# 进程状态的切换 + +

+ +- 就绪状态(ready):等待被调度 +- 运行状态(running) +- 阻塞状态(waiting):等待资源 + +应该注意以下内容: + +- 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 +- 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 + +# 进程调度算法 + +不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。 + +## 1. 批处理系统 + +批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。 + +**1.1 先来先服务 first-come first-serverd(FCFS)** + +按照请求的顺序进行调度。 + +有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 + +**1.2 短作业优先 shortest job first(SJF)** + +按估计运行时间最短的顺序进行调度。 + +长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 + +**1.3 最短剩余时间优先 shortest remaining time next(SRTN)** + +按估计剩余时间最短的顺序进行调度。 + +## 2. 交互式系统 + +交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。 + +**2.1 时间片轮转** + +将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 + +时间片轮转算法的效率和时间片的大小有很大关系: + +- 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 +- 而如果时间片过长,那么实时性就不能得到保证。 + +

+ +**2.2 优先级调度** + +为每个进程分配一个优先级,按优先级进行调度。 + +为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。 + +**2.3 多级反馈队列** + +一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。 + +多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。 + +每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。 + +可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。 + +

+ +## 3. 实时系统 + +实时系统要求一个请求在一个确定时间内得到响应。 + +分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。 + +# 进程同步 + +## 1. 临界区 + +对临界资源进行访问的那段代码称为临界区。 + +为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。 + +```html +// entry section +// critical section; +// exit section +``` + +## 2. 同步与互斥 + +- 同步:多个进程按一定顺序执行; +- 互斥:多个进程在同一时刻只有一个进程能进入临界区。 + +## 3. 信号量 + +信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。 + +- **down** : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0; +- **up** :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。 + +down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。 + +如果信号量的取值只能为 0 或者 1,那么就成为了 **互斥量(Mutex)** ,0 表示临界区已经加锁,1 表示临界区解锁。 + +```c +typedef int semaphore; +semaphore mutex = 1; +void P1() { + down(&mutex); + // 临界区 + up(&mutex); +} + +void P2() { + down(&mutex); + // 临界区 + up(&mutex); +} +``` + + **使用信号量实现生产者-消费者问题**
+ +问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。 + +因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。 + +为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。 + +注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。 + +```c +#define N 100 +typedef int semaphore; +semaphore mutex = 1; +semaphore empty = N; +semaphore full = 0; + +void producer() { + while(TRUE) { + int item = produce_item(); + down(&empty); + down(&mutex); + insert_item(item); + up(&mutex); + up(&full); + } +} + +void consumer() { + while(TRUE) { + down(&full); + down(&mutex); + int item = remove_item(); + consume_item(item); + up(&mutex); + up(&empty); + } +} +``` + +## 4. 管程 + +使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。 + +c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。 + +```pascal +monitor ProducerConsumer + integer i; + condition c; + + procedure insert(); + begin + // ... + end; + + procedure remove(); + begin + // ... + end; +end monitor; +``` + +管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。 + +管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。 + + **使用管程实现生产者-消费者问题**
+ +```pascal +// 管程 +monitor ProducerConsumer + condition full, empty; + integer count := 0; + condition c; + + procedure insert(item: integer); + begin + if count = N then wait(full); + insert_item(item); + count := count + 1; + if count = 1 then signal(empty); + end; + + function remove: integer; + begin + if count = 0 then wait(empty); + remove = remove_item; + count := count - 1; + if count = N -1 then signal(full); + end; +end monitor; + +// 生产者客户端 +procedure producer +begin + while true do + begin + item = produce_item; + ProducerConsumer.insert(item); + end +end; + +// 消费者客户端 +procedure consumer +begin + while true do + begin + item = ProducerConsumer.remove; + consume_item(item); + end +end; +``` + +# 经典同步问题 + +生产者和消费者问题前面已经讨论过了。 + +## 1. 读者-写者问题 + +允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。 + +一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。 + +```c +typedef int semaphore; +semaphore count_mutex = 1; +semaphore data_mutex = 1; +int count = 0; + +void reader() { + while(TRUE) { + down(&count_mutex); + count++; + if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问 + up(&count_mutex); + read(); + down(&count_mutex); + count--; + if(count == 0) up(&data_mutex); + up(&count_mutex); + } +} + +void writer() { + while(TRUE) { + down(&data_mutex); + write(); + up(&data_mutex); + } +} +``` + +以下内容由 [@Bandi Yugandhar](https://github.com/yugandharbandi) 提供。 + +The first case may result Writer to starve. This case favous Writers i.e no writer, once added to the queue, shall be kept waiting longer than absolutely necessary(only when there are readers that entered the queue before the writer). + +```source-c +int readcount, writecount; //(initial value = 0) +semaphore rmutex, wmutex, readLock, resource; //(initial value = 1) + +//READER +void reader() { + + down(&readLock); // reader is trying to enter + down(&rmutex); // lock to increase readcount + readcount++; + if (readcount == 1) + down(&resource); //if you are the first reader then lock the resource + up(&rmutex); //release for other readers + up(&readLock); //Done with trying to access the resource + + +//reading is performed + + + down(&rmutex); //reserve exit section - avoids race condition with readers + readcount--; //indicate you're leaving + if (readcount == 0) //checks if you are last reader leaving + up(&resource); //if last, you must release the locked resource + up(&rmutex); //release exit section for other readers +} + +//WRITER +void writer() { + + down(&wmutex); //reserve entry section for writers - avoids race conditions + writecount++; //report yourself as a writer entering + if (writecount == 1) //checks if you're first writer + down(&readLock); //if you're first, then you must lock the readers out. Prevent them from trying to enter CS + up(&wmutex); //release entry section + + + down(&resource); //reserve the resource for yourself - prevents other writers from simultaneously editing the shared resource + //writing is performed + up(&resource); //release file + + + down(&wmutex); //reserve exit section + writecount--; //indicate you're leaving + if (writecount == 0) //checks if you're the last writer + up(&readLock); //if you're last writer, you must unlock the readers. Allows them to try enter CS for reading + up(&wmutex); //release exit section +} +``` + +We can observe that every reader is forced to acquire ReadLock. On the otherhand, writers doesn’t need to lock individually. Once the first writer locks the ReadLock, it will be released only when there is no writer left in the queue. + +From the both cases we observed that either reader or writer has to starve. Below solutionadds the constraint that no thread shall be allowed to starve; that is, the operation of obtaining a lock on the shared data will always terminate in a bounded amount of time. + +```source-c +int readCount; // init to 0; number of readers currently accessing resource + +// all semaphores initialised to 1 +Semaphore resourceAccess; // controls access (read/write) to the resource +Semaphore readCountAccess; // for syncing changes to shared variable readCount +Semaphore serviceQueue; // FAIRNESS: preserves ordering of requests (signaling must be FIFO) + +void writer() +{ + down(&serviceQueue); // wait in line to be servicexs + // + down(&resourceAccess); // request exclusive access to resource + // + up(&serviceQueue); // let next in line be serviced + + // + writeResource(); // writing is performed + // + + // + up(&resourceAccess); // release resource access for next reader/writer + // +} + +void reader() +{ + down(&serviceQueue); // wait in line to be serviced + down(&readCountAccess); // request exclusive access to readCount + // + if (readCount == 0) // if there are no readers already reading: + down(&resourceAccess); // request resource access for readers (writers blocked) + readCount++; // update count of active readers + // + up(&serviceQueue); // let next in line be serviced + up(&readCountAccess); // release access to readCount + + // + readResource(); // reading is performed + // + + down(&readCountAccess); // request exclusive access to readCount + // + readCount--; // update count of active readers + if (readCount == 0) // if there are no readers left: + up(&resourceAccess); // release resource access for all + // + up(&readCountAccess); // release access to readCount +} + +``` + + +## 2. 哲学家进餐问题 + +

+ +五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。 + +下面是一种错误的解法,考虑到如果所有哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。 + +```c +#define N 5 + +void philosopher(int i) { + while(TRUE) { + think(); + take(i); // 拿起左边的筷子 + take((i+1)%N); // 拿起右边的筷子 + eat(); + put(i); + put((i+1)%N); + } +} +``` + +为了防止死锁的发生,可以设置两个条件: + +- 必须同时拿起左右两根筷子; +- 只有在两个邻居都没有进餐的情况下才允许进餐。 + +```c +#define N 5 +#define LEFT (i + N - 1) % N // 左邻居 +#define RIGHT (i + 1) % N // 右邻居 +#define THINKING 0 +#define HUNGRY 1 +#define EATING 2 +typedef int semaphore; +int state[N]; // 跟踪每个哲学家的状态 +semaphore mutex = 1; // 临界区的互斥 +semaphore s[N]; // 每个哲学家一个信号量 + +void philosopher(int i) { + while(TRUE) { + think(); + take_two(i); + eat(); + put_two(i); + } +} + +void take_two(int i) { + down(&mutex); + state[i] = HUNGRY; + test(i); + up(&mutex); + down(&s[i]); +} + +void put_two(i) { + down(&mutex); + state[i] = THINKING; + test(LEFT); + test(RIGHT); + up(&mutex); +} + +void test(i) { // 尝试拿起两把筷子 + if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) { + state[i] = EATING; + up(&s[i]); + } +} +``` + +# 进程通信 + +进程同步与进程通信很容易混淆,它们的区别在于: + +- 进程同步:控制多个进程按一定顺序执行; +- 进程通信:进程间传输信息。 + +进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。 + +## 1. 管道 + +管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。 + +```c +#include +int pipe(int fd[2]); +``` + +它具有以下限制: + +- 只支持半双工通信(单向交替传输); +- 只能在父子进程中使用。 + +

+ +## 2. FIFO + +也称为命名管道,去除了管道只能在父子进程中使用的限制。 + +```c +#include +int mkfifo(const char *path, mode_t mode); +int mkfifoat(int fd, const char *path, mode_t mode); +``` + +FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。 + +

+ +## 3. 消息队列 + +相比于 FIFO,消息队列具有以下优点: + +- 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难; +- 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法; +- 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。 + +## 4. 信号量 + +它是一个计数器,用于为多个进程提供对共享数据对象的访问。 + +## 5. 共享存储 + +允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。 + +需要使用信号量用来同步对共享存储的访问。 + +多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用使用内存的匿名段。 + +## 6. 套接字 + +与其它通信机制不同的是,它可用于不同机器间的进程通信。 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统 - 链接.md b/docs/notes/计算机操作系统 - 链接.md new file mode 100644 index 00000000..fd222df4 --- /dev/null +++ b/docs/notes/计算机操作系统 - 链接.md @@ -0,0 +1,71 @@ + +* [编译系统](#编译系统) +* [静态链接](#静态链接) +* [目标文件](#目标文件) +* [动态链接](#动态链接) + + + +# 编译系统 + +以下是一个 hello.c 程序: + +```c +#include + +int main() +{ + printf("hello, world\n"); + return 0; +} +``` + +在 Unix 系统上,由编译器把源文件转换为目标文件。 + +```bash +gcc -o hello hello.c +``` + +这个过程大致如下: + +

+ +- 预处理阶段:处理以 # 开头的预处理命令; +- 编译阶段:翻译成汇编文件; +- 汇编阶段:将汇编文件翻译成可重定向目标文件; +- 链接阶段:将可重定向目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。 + +# 静态链接 + +静态链接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务: + +- 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。 +- 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。 + +

+ +# 目标文件 + +- 可执行目标文件:可以直接在内存中执行; +- 可重定向目标文件:可与其它可重定向目标文件在链接阶段合并,创建一个可执行目标文件; +- 共享目标文件:这是一种特殊的可重定向目标文件,可以在运行时被动态加载进内存并链接; + +# 动态链接 + +静态库有以下两个问题: + +- 当静态库更新时那么整个程序都要重新进行链接; +- 对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。 + +共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点: + +- 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; +- 在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。 + +

+ + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机操作系统.md b/docs/notes/计算机操作系统.md index 9520d1be..d175443f 100644 --- a/docs/notes/计算机操作系统.md +++ b/docs/notes/计算机操作系统.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [基本特征](#基本特征) @@ -1077,3 +1076,9 @@ gcc -o hello hello.c - [Processes](http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php) - [Inter Process Communication Presentation[1]](https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1) - [Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 传输层.md b/docs/notes/计算机网络 - 传输层.md new file mode 100644 index 00000000..78c55af9 --- /dev/null +++ b/docs/notes/计算机网络 - 传输层.md @@ -0,0 +1,171 @@ + +* [UDP 和 TCP 的特点](#udp-和-tcp-的特点) +* [UDP 首部格式](#udp-首部格式) +* [TCP 首部格式](#tcp-首部格式) +* [TCP 的三次握手](#tcp-的三次握手) +* [TCP 的四次挥手](#tcp-的四次挥手) +* [TCP 可靠传输](#tcp-可靠传输) +* [TCP 滑动窗口](#tcp-滑动窗口) +* [TCP 流量控制](#tcp-流量控制) +* [TCP 拥塞控制](#tcp-拥塞控制) + * [1. 慢开始与拥塞避免](#1-慢开始与拥塞避免) + * [2. 快重传与快恢复](#2-快重传与快恢复) + + + +网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。传输层提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道。 + +# UDP 和 TCP 的特点 + +- 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。 + +- 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。 + +# UDP 首部格式 + +

+ +首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。 + +# TCP 首部格式 + +

+ +- **序号** :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。 + +- **确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。 + +- **数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。 + +- **确认 ACK** :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。 + +- **同步 SYN** :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。 + +- **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。 + +- **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。 + +# TCP 的三次握手 + +

+ +假设 A 为客户端,B 为服务器端。 + +- 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 + +- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。 + +- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 + +- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 + +- B 收到 A 的确认后,连接建立。 + +**三次握手的原因** + +第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。 + +客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。 + +# TCP 的四次挥手 + +

+ +以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 + +- A 发送连接释放报文,FIN=1。 + +- B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。 + +- 当 B 不再需要连接时,发送连接释放报文,FIN=1。 + +- A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。 + +- B 收到 A 的确认后释放连接。 + +**四次挥手的原因** + +客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。 + +**TIME_WAIT** + +客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由: + +- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。 + +- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。 + +# TCP 可靠传输 + +TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 + +一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: + +

+ +其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。 + +超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下: + +

+ +其中 RTTd 为偏差的加权平均值。 + +# TCP 滑动窗口 + +窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 + +发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 + +接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 + +

+ +# TCP 流量控制 + +流量控制是为了控制发送方发送速率,保证接收方来得及接收。 + +接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 + +# TCP 拥塞控制 + +如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。 + +

+ +TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 + +发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 + +为了便于讨论,做如下假设: + +- 接收方有足够大的接收缓存,因此不会发生流量控制; +- 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。 + +

+ +## 1. 慢开始与拥塞避免 + +发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ... + +注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。 + +如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。 + +## 2. 快重传与快恢复 + +在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。 + +在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。 + +在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 + +慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。 + +

+ + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 应用层.md b/docs/notes/计算机网络 - 应用层.md new file mode 100644 index 00000000..eaebad98 --- /dev/null +++ b/docs/notes/计算机网络 - 应用层.md @@ -0,0 +1,172 @@ + +* [域名系统](#域名系统) +* [文件传送协议](#文件传送协议) +* [动态主机配置协议](#动态主机配置协议) +* [远程登录协议](#远程登录协议) +* [电子邮件协议](#电子邮件协议) + * [1. SMTP](#1-smtp) + * [2. POP3](#2-pop3) + * [3. IMAP](#3-imap) +* [常用端口](#常用端口) +* [Web 页面请求过程](#web-页面请求过程) + * [1. DHCP 配置主机信息](#1-dhcp-配置主机信息) + * [2. ARP 解析 MAC 地址](#2-arp-解析-mac-地址) + * [3. DNS 解析域名](#3-dns-解析域名) + * [4. HTTP 请求页面](#4-http-请求页面) + + + +# 域名系统 + +DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。 + +域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。 + +

+ +DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输: + +- 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。 +- 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。 + +# 文件传送协议 + +FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件: + +- 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。 +- 数据连接:用来传送一个文件数据。 + +根据数据连接是否是服务器端主动建立,FTP 有主动和被动两种模式: + +- 主动模式:服务器端主动建立数据连接,其中服务器端的端口号为 20,客户端的端口号随机,但是必须大于 1024,因为 0\~1023 是熟知端口号。 + +

+ +- 被动模式:客户端主动建立数据连接,其中客户端的端口号由客户端自己指定,服务器端的端口号随机。 + +

+ +主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。 + +# 动态主机配置协议 + +DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。 + +DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。 + +DHCP 工作过程如下: + +1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。 +2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。 +3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。 +4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。 + +

+ +# 远程登录协议 + +TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。 + +TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。 + +# 电子邮件协议 + +一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。 + +邮件协议包含发送协议和读取协议,发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。 + +

+ +## 1. SMTP + +SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。 + +

+ +## 2. POP3 + +POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 + +## 3. IMAP + +IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 + +# 常用端口 + +|应用| 应用层协议 | 端口号 | 传输层协议 | 备注 | +| :---: | :--: | :--: | :--: | :--: | +| 域名解析 | DNS | 53 | UDP/TCP | 长度超过 512 字节时使用 TCP | +| 动态主机配置协议 | DHCP | 67/68 | UDP | | +| 简单网络管理协议 | SNMP | 161/162 | UDP | | +| 文件传送协议 | FTP | 20/21 | TCP | 控制连接 21,数据连接 20 | +| 远程终端协议 | TELNET | 23 | TCP | | +| 超文本传送协议 | HTTP | 80 | TCP | | +| 简单邮件传送协议 | SMTP | 25 | TCP | | +| 邮件读取协议 | POP3 | 110 | TCP | | +| 网际报文存取协议 | IMAP | 143 | TCP | | + +# Web 页面请求过程 + +## 1. DHCP 配置主机信息 + +- 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。 + +- 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。 + +- 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。 + +- 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。 + +- 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。 + +- 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。 + +- 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。 + +## 2. ARP 解析 MAC 地址 + +- 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。 + +- 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。 + +- 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。 + +- 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。 + +- DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。 + +- 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。 + +- 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。 + +## 3. DNS 解析域名 + +- 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。 + +- 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。 + +- 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。 + +- 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。 + +- 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。 + +## 4. HTTP 请求页面 + +- 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。 + +- 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。 + +- HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。 + +- 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。 + +- HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。 + +- 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 概述.md b/docs/notes/计算机网络 - 概述.md new file mode 100644 index 00000000..8a410b3c --- /dev/null +++ b/docs/notes/计算机网络 - 概述.md @@ -0,0 +1,134 @@ + +* [网络的网络](#网络的网络) +* [ISP](#isp) +* [主机之间的通信方式](#主机之间的通信方式) +* [电路交换与分组交换](#电路交换与分组交换) + * [1. 电路交换](#1-电路交换) + * [2. 分组交换](#2-分组交换) +* [时延](#时延) + * [1. 排队时延](#1-排队时延) + * [2. 处理时延](#2-处理时延) + * [3. 传输时延](#3-传输时延) + * [4. 传播时延](#4-传播时延) +* [计算机网络体系结构](#计算机网络体系结构) + * [1. 五层协议](#1-五层协议) + * [2. OSI](#2-osi) + * [3. TCP/IP](#3-tcpip) + * [4. 数据在各层之间的传递过程](#4-数据在各层之间的传递过程) + + + +# 网络的网络 + +网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。 + +

+ +# ISP + +互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址,同时拥有通信线路以及路由器等联网设备,个人或机构向 ISP 缴纳一定的费用就可以接入互联网。 + +

+ +目前的互联网是一种多层次 ISP 结构,ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。 + +

+ +# 主机之间的通信方式 + +- 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。 + +

+ +- 对等(P2P):不区分客户和服务器。 + +

+ +# 电路交换与分组交换 + +## 1. 电路交换 + +电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%。 + +## 2. 分组交换 + +每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。 + +在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。 + +# 时延 + +总时延 = 排队时延 + 处理时延 + 传输时延 + 传播时延 + +

+ +## 1. 排队时延 + +分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。 + +## 2. 处理时延 + +主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据、进行差错检验或查找适当的路由等。 + +## 3. 传输时延 + +主机或路由器传输数据帧所需要的时间。 + +

+ +其中 l 表示数据帧的长度,v 表示传输速率。 + +## 4. 传播时延 + +电磁波在信道中传播所需要花费的时间,电磁波传播的速度接近光速。 + +

+ +其中 l 表示信道长度,v 表示电磁波在信道上的传播速度。 + +# 计算机网络体系结构 + +

+ + +## 1. 五层协议 + +- **应用层** :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等协议。数据单位为报文。 + +- **传输层** :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 + +- **网络层** :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。 + +- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。 + +- **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 + +## 2. OSI + +其中表示层和会话层用途如下: + +- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。 + +- **会话层** :建立及管理会话。 + +五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。 + +## 3. TCP/IP + +它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。 + +TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。 + +

+ +## 4. 数据在各层之间的传递过程 + +在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。 + +路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要传输层和应用层。 + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 物理层.md b/docs/notes/计算机网络 - 物理层.md new file mode 100644 index 00000000..2980af33 --- /dev/null +++ b/docs/notes/计算机网络 - 物理层.md @@ -0,0 +1,26 @@ + +* [通信方式](#通信方式) +* [带通调制](#带通调制) + + + +# 通信方式 + +根据信息在传输线上的传送方向,分为以下三种通信方式: + +- 单工通信:单向传输 +- 半双工通信:双向交替传输 +- 全双工通信:双向同时传输 + +# 带通调制 + +模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。 + +

+ + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 目录.md b/docs/notes/计算机网络 - 目录.md new file mode 100644 index 00000000..7655fa1b --- /dev/null +++ b/docs/notes/计算机网络 - 目录.md @@ -0,0 +1,38 @@ + +* [目录](#目录) +* [参考链接](#参考链接) + + + +# 目录 + +- [概述](计算机网络%20-%20概述.md) +- [物理层](计算机网络%20-%20物理层.md) +- [链路层](计算机网络%20-%20链路层.md) +- [网络层](计算机网络%20-%20网络层.md) +- [传输层](计算机网络%20-%20传输层.md) +- [应用层](计算机网络%20-%20应用层.md) + +# 参考链接 + +- 计算机网络, 谢希仁 +- JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014. +- W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006. +- [Active vs Passive FTP Mode: Which One is More Secure?](https://securitywing.com/active-vs-passive-ftp-mode/) +- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined) +- [Traceroute](https://zh.wikipedia.org/wiki/Traceroute) +- [ping](https://zh.wikipedia.org/wiki/Ping) +- [How DHCP works and DHCP Interview Questions and Answers](http://webcache.googleusercontent.com/search?q=cache:http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html) +- [What is process of DORA in DHCP?](https://www.quora.com/What-is-process-of-DORA-in-DHCP) +- [What is DHCP Server ?](https://tecadmin.net/what-is-dhcp-server/) +- [Tackling emissions targets in Tokyo](http://www.climatechangenews.com/2011/html/university-tokyo.html) +- [What does my ISP know when I use Tor?](http://www.climatechangenews.com/2011/html/university-tokyo.html) +- [Technology-Computer Networking[1]-Computer Networks and the Internet](http://www.linyibin.cn/2017/02/12/technology-ComputerNetworking-Internet/) +- [P2P 网络概述.](http://slidesplayer.com/slide/11616167/) +- [Circuit Switching (a) Circuit switching. (b) Packet switching.](http://slideplayer.com/slide/5115386/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 目录1.md b/docs/notes/计算机网络 - 目录1.md new file mode 100644 index 00000000..b7cc3542 --- /dev/null +++ b/docs/notes/计算机网络 - 目录1.md @@ -0,0 +1,39 @@ + +* [目录](#目录) +* [参考链接](#参考链接) + + + +# 目录 + +- [概述](notes/计算机网络%20-%20概述.md) +- [物理层](notes/计算机网络%20-%20物理层.md) +- [链路层](notes/计算机网络%20-%20链路层.md) +- [网络层](notes/计算机网络%20-%20网络层.md) +- [传输层](notes/计算机网络%20-%20传输层.md) +- [应用层](notes/计算机网络%20-%20应用层.md) + +# 参考链接 + +- 计算机网络, 谢希仁 +- JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014. +- W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006. +- [Active vs Passive FTP Mode: Which One is More Secure?](https://securitywing.com/active-vs-passive-ftp-mode/) +- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined) +- [Traceroute](https://zh.wikipedia.org/wiki/Traceroute) +- [ping](https://zh.wikipedia.org/wiki/Ping) +- [How DHCP works and DHCP Interview Questions and Answers](http://webcache.googleusercontent.com/search?q=cache:http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html) +- [What is process of DORA in DHCP?](https://www.quora.com/What-is-process-of-DORA-in-DHCP) +- [What is DHCP Server ?](https://tecadmin.net/what-is-dhcp-server/) +- [Tackling emissions targets in Tokyo](http://www.climatechangenews.com/2011/html/university-tokyo.html) +- [What does my ISP know when I use Tor?](http://www.climatechangenews.com/2011/html/university-tokyo.html) +- [Technology-Computer Networking[1]-Computer Networks and the Internet](http://www.linyibin.cn/2017/02/12/technology-ComputerNetworking-Internet/) +- [P2P 网络概述.](http://slidesplayer.com/slide/11616167/) +- [Circuit Switching (a) Circuit switching. (b) Packet switching.](http://slideplayer.com/slide/5115386/) + + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 网络层.md b/docs/notes/计算机网络 - 网络层.md new file mode 100644 index 00000000..0e59b88a --- /dev/null +++ b/docs/notes/计算机网络 - 网络层.md @@ -0,0 +1,248 @@ + +* [概述](#概述) +* [IP 数据报格式](#ip-数据报格式) +* [IP 地址编址方式](#ip-地址编址方式) + * [1. 分类](#1-分类) + * [2. 子网划分](#2-子网划分) + * [3. 无分类](#3-无分类) +* [地址解析协议 ARP](#地址解析协议-arp) +* [网际控制报文协议 ICMP](#网际控制报文协议-icmp) + * [1. Ping](#1-ping) + * [2. Traceroute](#2-traceroute) +* [虚拟专用网 VPN](#虚拟专用网-vpn) +* [网络地址转换 NAT](#网络地址转换-nat) +* [路由器的结构](#路由器的结构) +* [路由器分组转发流程](#路由器分组转发流程) +* [路由选择协议](#路由选择协议) + * [1. 内部网关协议 RIP](#1-内部网关协议-rip) + * [2. 内部网关协议 OSPF](#2-内部网关协议-ospf) + * [3. 外部网关协议 BGP](#3-外部网关协议-bgp) + + + +# 概述 + +因为网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。 + +使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。 + +

+ +与 IP 协议配套使用的还有三个协议: + +- 地址解析协议 ARP(Address Resolution Protocol) +- 网际控制报文协议 ICMP(Internet Control Message Protocol) +- 网际组管理协议 IGMP(Internet Group Management Protocol) + +# IP 数据报格式 + +

+ +- **版本** : 有 4(IPv4)和 6(IPv6)两个值; + +- **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。 + +- **区分服务** : 用来获得更好的服务,一般情况下不使用。 + +- **总长度** : 包括首部长度和数据部分长度。 + +- **生存时间** :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。 + +- **协议** :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。 + +- **首部检验和** :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。 + +- **标识** : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。 + +- **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。 + +

+ +# IP 地址编址方式 + +IP 地址的编址方式经历了三个历史阶段: + +- 分类 +- 子网划分 +- 无分类 + +## 1. 分类 + +由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。 + +IP 地址 ::= {< 网络号 >, < 主机号 >} + +

+ +## 2. 子网划分 + +通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。 + +IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >} + +要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。 + +注意,外部网络看不到子网的存在。 + +## 3. 无分类 + +无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。 + +IP 地址 ::= {< 网络前缀号 >, < 主机号 >} + +CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。 + +CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。 + +一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 **构成超网** 。 + +在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。 + +# 地址解析协议 ARP + +网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。 + +

+ +ARP 实现由 IP 地址得到 MAC 地址。 + +

+ +每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。 + +如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。 + +

+ +# 网际控制报文协议 ICMP + +ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。 + +

+ +ICMP 报文分为差错报告报文和询问报文。 + +

+ +## 1. Ping + +Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 + +Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。 + +## 2. Traceroute + +Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。 + +Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。 + +- 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; +- 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 +- 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 +- 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 + +# 虚拟专用网 VPN + +由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。 + +有三个专用地址块: + +- 10.0.0.0 \~ 10.255.255.255 +- 172.16.0.0 \~ 172.31.255.255 +- 192.168.0.0 \~ 192.168.255.255 + +VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。 + +下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。 + +

+ +# 网络地址转换 NAT + +专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。 + +在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。 + +

+ +# 路由器的结构 + +路由器从功能上可以划分为:路由选择和分组转发。 + +分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。 + +

+ +# 路由器分组转发流程 + +- 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。 +- 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; +- 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; +- 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; +- 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; +- 报告转发分组出错。 + +

+ +# 路由选择协议 + +路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。 + +互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。 + +可以把路由选择协议划分为两大类: + +- 自治系统内部的路由选择:RIP 和 OSPF +- 自治系统间的路由选择:BGP + +## 1. 内部网关协议 RIP + +RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15,超过 15 表示不可达。 + +RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。 + +距离向量算法: + +- 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1; +- 对修改后的 RIP 报文中的每一个项目,进行以下步骤: + - 若原来的路由表中没有目的网络 N,则把该项目添加到路由表中; + - 否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。 +- 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。 + +RIP 协议实现简单,开销小。但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。 + +## 2. 内部网关协议 OSPF + +开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。 + +开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。 + +OSPF 具有以下特点: + +- 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。 +- 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。 +- 只有当链路状态发生变化时,路由器才会发送信息。 + +所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。 + +## 3. 外部网关协议 BGP + +BGP(Border Gateway Protocol,边界网关协议) + +AS 之间的路由选择很困难,主要是由于: + +- 互联网规模很大; +- 各个 AS 内部使用不同的路由选择协议,无法准确定义路径的度量; +- AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。 + +BGP 只能寻找一条比较好的路由,而不是最佳路由。 + +每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。 + +

+ + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络 - 链路层.md b/docs/notes/计算机网络 - 链路层.md new file mode 100644 index 00000000..198d4b6d --- /dev/null +++ b/docs/notes/计算机网络 - 链路层.md @@ -0,0 +1,194 @@ + +* [基本问题](#基本问题) + * [1. 封装成帧](#1-封装成帧) + * [2. 透明传输](#2-透明传输) + * [3. 差错检测](#3-差错检测) +* [信道分类](#信道分类) + * [1. 广播信道](#1-广播信道) + * [2. 点对点信道](#2-点对点信道) +* [信道复用技术](#信道复用技术) + * [1. 频分复用](#1-频分复用) + * [2. 时分复用](#2-时分复用) + * [3. 统计时分复用](#3-统计时分复用) + * [4. 波分复用](#4-波分复用) + * [5. 码分复用](#5-码分复用) +* [CSMA/CD 协议](#csmacd-协议) +* [PPP 协议](#ppp-协议) +* [MAC 地址](#mac-地址) +* [局域网](#局域网) +* [以太网](#以太网) +* [交换机](#交换机) +* [虚拟局域网](#虚拟局域网) + + + +# 基本问题 + +## 1. 封装成帧 + +将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。 + +

+ +## 2. 透明传输 + +透明表示一个实际存在的事物看起来好像不存在一样。 + +帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。 + +

+ +## 3. 差错检测 + +目前数据链路层广泛使用了循环冗余检验(CRC)来检查比特差错。 + +# 信道分类 + +## 1. 广播信道 + +一对多通信,一个节点发送的数据能够被广播信道上所有的节点接收到。 + +所有的节点都在同一个广播信道上发送数据,因此需要有专门的控制方法进行协调,避免发生冲突(冲突也叫碰撞)。 + +主要有两种控制方法进行协调,一个是使用信道复用技术,一是使用 CSMA/CD 协议。 + +## 2. 点对点信道 + +一对一通信。 + +因为不会发生碰撞,因此也比较简单,使用 PPP 协议进行控制。 + +# 信道复用技术 + +## 1. 频分复用 + +频分复用的所有主机在相同的时间占用不同的频率带宽资源。 + +

+ +## 2. 时分复用 + +时分复用的所有主机在不同的时间占用相同的频率带宽资源。 + +

+ +使用频分复用和时分复用进行通信,在通信的过程中主机会一直占用一部分信道资源。但是由于计算机数据的突发性质,通信过程没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。 + +## 3. 统计时分复用 + +是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。 + +

+ +## 4. 波分复用 + +光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。 + +## 5. 码分复用 + +为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 有 + +

+ +为了讨论方便,取 m=8,设码片 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。 + +在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到 + +

+ +

+ +其中 的反码。 + +利用上面的式子我们知道,当接收端使用码片 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 的是用户发送的比特 0。 + +码分复用需要发送的数据量为原先的 m 倍。 + +

+ +# CSMA/CD 协议 + +CSMA/CD 表示载波监听多点接入 / 碰撞检测。 + +- **多点接入** :说明这是总线型网络,许多主机以多点的方式连接到总线上。 +- **载波监听** :每个主机都必须不停地监听信道。在发送前,如果监听到信道正在使用,就必须等待。 +- **碰撞检测** :在发送中,如果监听到信道已有其它主机正在发送数据,就表示发生了碰撞。虽然每个主机在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。 + +记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期** 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。 + +当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定。从离散的整数集合 {0, 1, .., (2k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。 + +

+ +# PPP 协议 + +互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。 + +

+ +PPP 的帧格式: + +- F 字段为帧的定界符 +- A 和 C 字段暂时没有意义 +- FCS 字段是使用 CRC 的检验序列 +- 信息部分的长度不超过 1500 + +

+ +# MAC 地址 + +MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标识网络适配器(网卡)。 + +一台主机拥有多少个网络适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。 + +# 局域网 + +局域网是一种典型的广播信道,主要特点是网络为一个单位所拥有,且地理范围和站点数目均有限。 + +主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。 + +可以按照网络拓扑结构对局域网进行分类: + +

+ +# 以太网 + +以太网是一种星型拓扑结构局域网。 + +早期使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到两个不同接口的帧,那么就发生了碰撞。 + +目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。 + +以太网帧格式: + +- **类型** :标记上层使用的协议; +- **数据** :长度在 46-1500 之间,如果太小则需要填充; +- **FCS** :帧检验序列,使用的是 CRC 检验方法; + +

+ +# 交换机 + +交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。 + +正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。 + +下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧,主机 B 回应该帧向主机 A 发送数据包时,交换机查找交换表得到主机 A 映射的接口为 1,就发送数据帧到接口 1,同时交换机添加主机 B 到接口 2 的映射。 + +

+ +# 虚拟局域网 + +虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。 + +例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 + +使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。 + +

+ + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/计算机网络.md b/docs/notes/计算机网络.md index 47908e26..f3337fbc 100644 --- a/docs/notes/计算机网络.md +++ b/docs/notes/计算机网络.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [网络的网络](#网络的网络) @@ -893,3 +892,9 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删 - [Technology-Computer Networking[1]-Computer Networks and the Internet](http://www.linyibin.cn/2017/02/12/technology-ComputerNetworking-Internet/) - [P2P 网络概述.](http://slidesplayer.com/slide/11616167/) - [Circuit Switching (a) Circuit switching. (b) Packet switching.](http://slideplayer.com/slide/5115386/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/设计模式.md b/docs/notes/设计模式.md index 4a35404f..612985e1 100644 --- a/docs/notes/设计模式.md +++ b/docs/notes/设计模式.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、概述](#一概述) * [二、创建型](#二创建型) @@ -3067,3 +3066,9 @@ public class ImageViewer { - [Design Patterns](http://www.oodesign.com/) - [Design patterns implemented in Java](http://java-design-patterns.com/) - [The breakdown of design patterns in JDK](http://www.programering.com/a/MTNxAzMwATY.html) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/集群.md b/docs/notes/集群.md index 1ab268db..fb625bbf 100644 --- a/docs/notes/集群.md +++ b/docs/notes/集群.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、负载均衡](#一负载均衡) * [负载均衡算法](#负载均衡算法) @@ -199,3 +198,9 @@ HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服 - [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/notes/面向对象思想.md b/docs/notes/面向对象思想.md index 75f7ca8f..f90b4907 100644 --- a/docs/notes/面向对象思想.md +++ b/docs/notes/面向对象思想.md @@ -1,4 +1,3 @@ -* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide) * [一、三大特性](#一三大特性) * [封装](#封装) @@ -358,3 +357,9 @@ Vihicle .. N - [看懂 UML 类图和时序图](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization) - [UML 系列——时序图(顺序图)sequence diagram](http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html) - [面向对象编程三大特性 ------ 封装、继承、多态](http://blog.csdn.net/jianyuerensheng/article/details/51602015) + + + + +
欢迎关注公众号,获取最新文章!


+
diff --git a/docs/pics/067b310c-6877-40fe-9dcf-10654e737485.jpg b/docs/pics/067b310c-6877-40fe-9dcf-10654e737485.jpg new file mode 100644 index 00000000..c07bd924 Binary files /dev/null and b/docs/pics/067b310c-6877-40fe-9dcf-10654e737485.jpg differ diff --git a/docs/pics/0e8fdc96-83c1-4798-9abe-45fc91d70b9d.png b/docs/pics/0e8fdc96-83c1-4798-9abe-45fc91d70b9d.png new file mode 100644 index 00000000..d8df24d8 Binary files /dev/null and b/docs/pics/0e8fdc96-83c1-4798-9abe-45fc91d70b9d.png differ diff --git a/docs/pics/14fe1e71-8518-458f-a220-116003061a83.png b/docs/pics/14fe1e71-8518-458f-a220-116003061a83.png new file mode 100644 index 00000000..ec381029 Binary files /dev/null and b/docs/pics/14fe1e71-8518-458f-a220-116003061a83.png differ diff --git a/docs/pics/2de794ca-aa7b-48f3-a556-a0e2708cb976.jpg b/docs/pics/2de794ca-aa7b-48f3-a556-a0e2708cb976.jpg new file mode 100644 index 00000000..5398511f Binary files /dev/null and b/docs/pics/2de794ca-aa7b-48f3-a556-a0e2708cb976.jpg differ diff --git a/docs/pics/49e53613-46f8-4308-9ee5-c09d6231552088893397.png b/docs/pics/49e53613-46f8-4308-9ee5-c09d6231552088893397.png new file mode 100644 index 00000000..e853dc13 Binary files /dev/null and b/docs/pics/49e53613-46f8-4308-9ee5-c09d6231552088893397.png differ diff --git a/docs/pics/6646db4a-7f43-45e4-96ff-0891a57a9ade.jpg b/docs/pics/6646db4a-7f43-45e4-96ff-0891a57a9ade.jpg new file mode 100644 index 00000000..693d7b97 Binary files /dev/null and b/docs/pics/6646db4a-7f43-45e4-96ff-0891a57a9ade.jpg differ diff --git a/docs/pics/6c0cf1e8-b03f-4eff-9b1a-ab262e0c7866.png b/docs/pics/6c0cf1e8-b03f-4eff-9b1a-ab262e0c7866.png new file mode 100644 index 00000000..5b9202f6 Binary files /dev/null and b/docs/pics/6c0cf1e8-b03f-4eff-9b1a-ab262e0c7866.png differ diff --git a/docs/pics/74dc31eb-6baa-47ea-ab1c-d27a0ca35093.png b/docs/pics/74dc31eb-6baa-47ea-ab1c-d27a0ca35093.png new file mode 100644 index 00000000..837f995a Binary files /dev/null and b/docs/pics/74dc31eb-6baa-47ea-ab1c-d27a0ca35093.png differ diff --git a/docs/pics/79e9a938-43e2-4c5a-8de9-fe55522a14c9.png b/docs/pics/79e9a938-43e2-4c5a-8de9-fe55522a14c9.png new file mode 100644 index 00000000..4938fc58 Binary files /dev/null and b/docs/pics/79e9a938-43e2-4c5a-8de9-fe55522a14c9.png differ diff --git a/docs/pics/879814ee-48b5-4bcb-86f5-dcc400cb81ad.png b/docs/pics/879814ee-48b5-4bcb-86f5-dcc400cb81ad.png new file mode 100644 index 00000000..14d98b8c Binary files /dev/null and b/docs/pics/879814ee-48b5-4bcb-86f5-dcc400cb81ad.png differ diff --git a/docs/pics/8cb2be66-3d47-41ba-b55b-319fc68940d4.png b/docs/pics/8cb2be66-3d47-41ba-b55b-319fc68940d4.png new file mode 100644 index 00000000..c90982c2 Binary files /dev/null and b/docs/pics/8cb2be66-3d47-41ba-b55b-319fc68940d4.png differ diff --git a/docs/pics/95903878-725b-4ed9-bded-bc4aae0792a9.jpg b/docs/pics/95903878-725b-4ed9-bded-bc4aae0792a9.jpg new file mode 100644 index 00000000..da95c5d8 Binary files /dev/null and b/docs/pics/95903878-725b-4ed9-bded-bc4aae0792a9.jpg differ diff --git a/docs/pics/9823768c-212b-4b1a-b69a-b3f59e07b977.jpg b/docs/pics/9823768c-212b-4b1a-b69a-b3f59e07b977.jpg new file mode 100644 index 00000000..8b907d61 Binary files /dev/null and b/docs/pics/9823768c-212b-4b1a-b69a-b3f59e07b977.jpg differ diff --git a/docs/pics/9ae89f16-7905-4a6f-88a2-874b4cac91f4.jpg b/docs/pics/9ae89f16-7905-4a6f-88a2-874b4cac91f4.jpg new file mode 100644 index 00000000..77c54780 Binary files /dev/null and b/docs/pics/9ae89f16-7905-4a6f-88a2-874b4cac91f4.jpg differ diff --git a/docs/pics/c847d6e4-3610-4f3c-a909-89a5048426e6.png b/docs/pics/c847d6e4-3610-4f3c-a909-89a5048426e6.png new file mode 100644 index 00000000..167c33bc Binary files /dev/null and b/docs/pics/c847d6e4-3610-4f3c-a909-89a5048426e6.png differ diff --git a/docs/pics/da1f96b9-fd4d-44ca-8925-fb14c5733388.png b/docs/pics/da1f96b9-fd4d-44ca-8925-fb14c5733388.png new file mode 100644 index 00000000..fdca0324 Binary files /dev/null and b/docs/pics/da1f96b9-fd4d-44ca-8925-fb14c5733388.png differ diff --git a/docs/pics/dc82f0f3-c1d4-4ac8-90ac-d5b32a9bd75a.jpg b/docs/pics/dc82f0f3-c1d4-4ac8-90ac-d5b32a9bd75a.jpg new file mode 100644 index 00000000..0be8743c Binary files /dev/null and b/docs/pics/dc82f0f3-c1d4-4ac8-90ac-d5b32a9bd75a.jpg differ diff --git a/docs/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ff1552090620367.png b/docs/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ff1552090620367.png new file mode 100644 index 00000000..25ed7497 Binary files /dev/null and b/docs/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ff1552090620367.png differ diff --git a/docs/pics/ee994da4-0fc7-443d-ac56-c08caf00a204.jpg b/docs/pics/ee994da4-0fc7-443d-ac56-c08caf00a204.jpg new file mode 100644 index 00000000..732e73e0 Binary files /dev/null and b/docs/pics/ee994da4-0fc7-443d-ac56-c08caf00a204.jpg differ diff --git a/docs/pics/f1ff65ed-bbc2-4b92-8a94-7c5c0874da0f.jpg b/docs/pics/f1ff65ed-bbc2-4b92-8a94-7c5c0874da0f.jpg new file mode 100644 index 00000000..4308e4c9 Binary files /dev/null and b/docs/pics/f1ff65ed-bbc2-4b92-8a94-7c5c0874da0f.jpg differ