diff --git a/README.md b/README.md index f35ee455..d511fa7c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| -| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 系统设计[:bulb:](#系统设计-bulb)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) | +| 算法[:pencil2:](#pencil2-算法) | 操作系统[:computer:](#computer-操作系统)|网络[:cloud:](#cloud-网络) | 面向对象[:couple:](#couple-面向对象) |数据库[:floppy_disk:](#floppy_disk-数据库)| Java [:coffee:](#coffee-java)| 系统设计[:bulb:](#bulb-系统设计)| 工具[:hammer:](#hammer-工具)| 编码实践[:speak_no_evil:](#speak_no_evil-编码实践)| 后记[:memo:](#memo-后记) |
@@ -11,7 +11,7 @@ -## 算法 :pencil2: +## :pencil2: 算法 - [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) @@ -20,12 +20,12 @@ - [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) 对题目做了一个大致分类,并对每种题型的解题思路做了总结。 + + - [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) -- [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) + 排序、并查集、栈和队列、红黑树、散列表。 - 排序、并查集、栈和队列、红黑树、散列表。 - -## 操作系统 :computer: +## :computer: 操作系统 - [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md) @@ -35,7 +35,7 @@ 基本实现原理以及基本操作。 -## 网络 :cloud: +## :cloud: 网络 - [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) @@ -49,7 +49,7 @@ I/O 模型、I/O 多路复用。 -## 面向对象 :couple: +## :couple: 面向对象 - [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md) @@ -59,7 +59,7 @@ 三大原则(继承、封装、多态)、类图、设计原则。 -## 数据库 :floppy_disk: +## :floppy_disk: 数据库 - [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md) @@ -81,7 +81,7 @@ 五种数据类型、字典和跳跃表数据结构、使用场景、和 Memcache 的比较、淘汰策略、持久化、文件事件的 Reactor 模式、复制。 -## Java :coffee: +## :coffee: Java - [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) @@ -103,7 +103,7 @@ NIO 的原理以及实例。 -## 系统设计 :bulb: +## :bulb: 系统设计 - [系统设计基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/系统设计基础.md) @@ -129,7 +129,7 @@ 消息处理模型、使用场景、可靠性 -## 工具 :hammer: +## :hammer: 工具 - [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md) @@ -147,7 +147,7 @@ 构建工具的基本概念、主流构建工具介绍。 -## 编码实践 :speak_no_evil: +## :speak_no_evil: 编码实践 - [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md) @@ -161,7 +161,7 @@ Google 开源项目的代码风格规范。 -## 后记 :memo: +## :memo: 后记 ### About @@ -195,12 +195,6 @@ 笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github,或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 -### License - -在对本作品进行演绎时,请署名并以相同方式共享。 - -知识共享许可协议 - ### Statement 本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.) @@ -235,5 +229,9 @@ Power by [logomakr](https://logomakr.com/). +### License +在对本作品进行演绎时,请署名并以相同方式共享。 + +知识共享许可协议 diff --git a/notes/Docker.md b/notes/Docker.md index f230f762..80108896 100644 --- a/notes/Docker.md +++ b/notes/Docker.md @@ -30,15 +30,15 @@ Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程 ## 启动速度 -启动虚拟机需要启动虚拟机的操作系统,再启动相应的应用,这个过程会非常慢; +启动虚拟机需要启动虚拟机的操作系统,再启动应用,这个过程非常慢; 而启动 Docker 相当于启动宿主操作系统上的一个进程。 ## 占用资源 -虚拟机是一个完整的操作系统,需要占用大量的磁盘空间、内存和 CPU,一台机器只能开启几十个的虚拟机。 +虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU,一台机器只能开启几十个的虚拟机。 -而 Docker 只是一个进程,只需要将应用以及相应的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。 +而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。 参考资料: @@ -58,7 +58,7 @@ Docker 使用分层技术和镜像,使得应用可以更容易复用重复部 ## 更容易扩展 -可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像得到我们想要的镜像非常容易。 +可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像可以非常容易得到我们想要的镜像。 参考资料: @@ -91,7 +91,7 @@ Docker 轻量级的特点使得它很适合用于部署、维护、组合微服 镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。 -在构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。 +构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。

@@ -100,3 +100,4 @@ Docker 轻量级的特点使得它很适合用于部署、维护、组合微服 - [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/) - [理解 Docker(2):Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html) + diff --git a/notes/Java 基础.md b/notes/Java 基础.md index adf899fa..44ca873a 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -576,9 +576,22 @@ SuperExtendExample.func() ## 重写与重载 -- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。子类的返回值类型要等于或者小于父类的返回值; +**1. 重写(Override)** -- 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 +存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。 + +为了满足里式替换原则,重写有有以下两个限制: + +- 子类方法的访问权限必须大于等于父类方法; +- 子类方法的返回类型必须是父类方法返回类型或为其子类型。 + +使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。 + +**2. 重载(Overload)** + +存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。 + +应该注意的是,返回值不同,其它都相同不算是重载。 # 五、Object 通用方法 @@ -694,7 +707,7 @@ public class EqualExample { ## hashCode() -hasCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。 +hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。 在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。 diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 9959e2ca..001f864c 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -742,7 +742,7 @@ new capacity : 00100000 对于一个 Key, -- 它的哈希值如果在第 6 位上为 0,那么取模得到的结果和之前一样; +- 它的哈希值如果在第 5 位上为 0,那么取模得到的结果和之前一样; - 如果为 1,那么得到的结果为原来的结果 +16。 ### 7. 扩容-计算数组容量 diff --git a/notes/Java 并发.md b/notes/Java 并发.md index 9cd5955b..1403f764 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -168,6 +168,8 @@ public static void main(String[] args) throws ExecutionException, InterruptedExc 同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。 +当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。 + ```java public class MyThread extends Thread { public void run() { @@ -1018,7 +1020,7 @@ ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线 如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。 -以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。 +以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。 ```java public class ThreadUnsafeExample { diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md index 891211da..a4a4ba12 100644 --- a/notes/Java 虚拟机.md +++ b/notes/Java 虚拟机.md @@ -45,7 +45,7 @@ 可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小: ```java -java -Xss=512M HackTheJava +java -Xss512M HackTheJava ``` 该区域可能抛出以下异常: @@ -81,7 +81,7 @@ java -Xss=512M HackTheJava 可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 ```java -java -Xms=1M -Xmx=2M HackTheJava +java -Xms1M -Xmx2M HackTheJava ``` ## 方法区 diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md index 44f3d9dc..04a751c7 100644 --- a/notes/Leetcode 题解.md +++ b/notes/Leetcode 题解.md @@ -2389,8 +2389,6 @@ private void backtracking(int row) {

-dp[N] 即为所求。 - 考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 ```java @@ -2468,16 +2466,6 @@ private int rob(int[] nums, int first, int last) { } ``` -**母牛生产** - -[程序员代码面试指南-P181](#) - -题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 - -第 i 年成熟的牛的数量为: - -

- **信件错排** 题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 @@ -2491,7 +2479,15 @@ private int rob(int[] nums, int first, int last) {

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

### 矩阵路径 @@ -2517,10 +2513,8 @@ public int minPathSum(int[][] grid) { 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]; // 只能从左侧走到该位置 + if (i == 0) { + dp[j] = dp[j - 1]; } else { dp[j] = Math.min(dp[j - 1], dp[j]); } @@ -2584,17 +2578,18 @@ sumRange(0, 5) -> -3 ```java class NumArray { + private int[] sums; public NumArray(int[] nums) { - sums = new int[nums.length]; - for (int i = 0; i < nums.length; i++) { - sums[i] = i == 0 ? nums[0] : sums[i - 1] + nums[i]; + 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 i == 0 ? sums[j] : sums[j] - sums[i - 1]; + return sums[j + 1] - sums[i]; } } ``` @@ -2634,7 +2629,7 @@ return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] i 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。 +在 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) { @@ -2747,17 +2742,17 @@ public int numDecodings(String s) { ### 最长递增子序列 -已知一个序列 {S1, S2,...,Sn} ,取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列** 。 +已知一个序列 {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 存储最长递增子序列的长度,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} 即为所求。 +对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。 **最长递增子序列** @@ -2790,7 +2785,7 @@ for (int i = 0; i < n; i++) { return ret; ``` -以上解法的时间复杂度为 O(N2) ,可以使用二分查找将时间复杂度降低为 O(NlogN)。 +以上解法的时间复杂度为 O(N2),可以使用二分查找将时间复杂度降低为 O(NlogN)。 定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x, @@ -2915,19 +2910,19 @@ public int wiggleMaxLength(int[] nums) { 定义一个二维数组 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] }。 +- 当 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 的最长公共子序列长度。 +对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 与最长递增子序列相比,最长公共子序列有以下不同点: - 针对的是两个序列,求它们的最长公共子序列。 -- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。 +- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。 - 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 ```java @@ -2956,9 +2951,7 @@ public int lengthOfLCS(int[] nums1, int[] nums2) { - 第 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 背包的状态转移方程为: +第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:

@@ -2981,11 +2974,11 @@ public int knapsack(int W, int N, int[] weights, int[] values) { **空间优化** -在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅由前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时, +在程序实现时可以对 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],在程序实现时需要按倒序来循环求解。 +因为 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) { @@ -3205,7 +3198,7 @@ public int findMaxForm(String[] strs, int m, int n) { } ``` -**找零钱的方法数** +**找零钱的最少硬币数** [322. Coin Change (Medium)](https://leetcode.com/problems/coin-change/description/) @@ -3286,60 +3279,6 @@ public int combinationSum4(int[] nums, int target) { } ``` -**只能进行 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]; -} -``` - -**只能进行两次的股票交易** - -[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; -} -``` - ### 股票交易 **需要冷却期的股票交易** @@ -3432,6 +3371,60 @@ public int maxProfit(int[] prices) { } ``` +**只能进行两次的股票交易** + +[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]; +} +``` + ### 字符串编辑 **删除两个字符串的字符使它们相等** @@ -3450,11 +3443,8 @@ Explanation: You need one step to make "sea" to "ea" and another step to make "e 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 = 0; i <= m; i++) { - for (int j = 0; j <= n; j++) { - if (i == 0 || j == 0) { - continue; - } + 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 { @@ -3612,7 +3602,7 @@ public int countPrimes(int n) { ```java int gcd(int a, int b) { - return b == 0 ? a : gcd(b, a% b); + return b == 0 ? a : gcd(b, a % b); } ``` @@ -3683,7 +3673,7 @@ public String convertToBase7(int num) { } ``` -Java 中 static String toString(int num, int radix) 可以将一个整数转换为 redix 进制表示的字符串。 +Java 中 static String toString(int num, int radix) 可以将一个整数转换为 radix 进制表示的字符串。 ```java public String convertToBase7(int num) { @@ -6413,7 +6403,6 @@ public int maxChunksToSorted(int[] arr) { } ``` - ## 图 ### 二分图 @@ -6630,6 +6619,7 @@ public int[] findRedundantConnection(int[][] edges) { } private class UF { + private int[] id; UF(int N) { @@ -6675,7 +6665,7 @@ 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 = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask:00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。 - 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。 位与运算技巧: @@ -6920,7 +6910,6 @@ public boolean isPowerOfFour(int num) { } ``` - **判断一个数的位级表示是否不会出现连续的 0 和 1** [693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) diff --git a/notes/Linux.md b/notes/Linux.md index 14d3b317..8ff3e2c6 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -129,15 +129,16 @@ info 与 man 类似,但是 info 将文档分成一个个页面,每个页面 /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin ``` -env 命令可以获取当前终端的环境变量。 - ## sudo sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。 ## 包管理工具 -RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。 +RPM 和 DPKG 为最常见的两类软件包管理工具: + +- RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。 +- 与 RPM 进行竞争的是基于 Debian 操作系统 (Ubuntu) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。 YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能。 @@ -194,13 +195,13 @@ IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/ ### 2. SATA -SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,因抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能,SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。 +SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能。SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。

### 3. SCSI -SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II,到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且资料传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。 +SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II 到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。

@@ -229,7 +230,7 @@ Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘 MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。 -分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它扇区用来记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 +分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区用记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 @@ -251,10 +252,10 @@ MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。 -BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。 -

+BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。 + 主要开机记录(MBR)中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 下图中,第一扇区的主要开机记录(MBR)中的开机管理程序提供了两个选单:M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。 @@ -283,11 +284,10 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 除此之外还包括: - superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; -- block bitmap:记录 block 是否被使用的位域; +- block bitmap:记录 block 是否被使用的位域。

- ## 文件读取 对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 diff --git a/notes/MySQL.md b/notes/MySQL.md index 7ac9685f..54067977 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -1,22 +1,23 @@ -* [一、存储引擎](#一存储引擎) +* [一、索引](#一索引) + * [B+ Tree 原理](#b-tree-原理) + * [MySQL 索引](#mysql-索引) + * [索引优化](#索引优化) + * [索引的优点](#索引的优点) + * [索引的使用场景](#索引的使用场景) +* [二、查询性能优化](#二查询性能优化) + * [使用 Explain 进行分析](#使用-explain-进行分析) + * [优化数据访问](#优化数据访问) + * [重构查询方式](#重构查询方式) +* [三、存储引擎](#三存储引擎) * [InnoDB](#innodb) * [MyISAM](#myisam) * [比较](#比较) -* [二、数据类型](#二数据类型) +* [四、数据类型](#四数据类型) * [整型](#整型) * [浮点数](#浮点数) * [字符串](#字符串) * [时间和日期](#时间和日期) -* [三、索引](#三索引) - * [B+ Tree 原理](#b-tree-原理) - * [索引分类](#索引分类) - * [索引的优点](#索引的优点) - * [索引优化](#索引优化) -* [四、查询性能优化](#四查询性能优化) - * [使用 Explain 进行分析](#使用-explain-进行分析) - * [优化数据访问](#优化数据访问) - * [重构查询方式](#重构查询方式) * [五、切分](#五切分) * [水平切分](#水平切分) * [垂直切分](#垂直切分) @@ -29,101 +30,7 @@ -# 一、存储引擎 - -## InnoDB - -InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。 - -实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ 间隙锁(next-key locking)防止幻影读。 - -主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 - -内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 - -支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 - -## MyISAM - -MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用 MyISAM。 - -MyISAM 提供了大量的特性,包括压缩表、空间数据索引等。 - -不支持事务。 - -不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 - -可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 - -如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 - -## 比较 - -- 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。 - -- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 - -- 外键:InnoDB 支持外键。 - -- 备份:InnoDB 支持在线热备份。 - -- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 - -- 其它特性:MyISAM 支持压缩表和空间数据索引。 - -# 二、数据类型 - -## 整型 - -TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。 - -INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。 - -## 浮点数 - -FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。 - -FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。 - -## 字符串 - -主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。 - -VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。 - -VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。 - -## 时间和日期 - -MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。 - -### 1. DATETIME - -能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 - -它与时区无关。 - -默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 - -### 2. TIMESTAMP - -和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。 - -它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。 - -MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。 - -默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。 - -应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。 - -# 三、索引 - -索引能够轻易将查询性能提升几个数量级。 - -对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 - -索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 +# 一、索引 ## B+ Tree 原理 @@ -141,37 +48,37 @@ B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具 进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。 -插入删除操作记录会破坏平衡树的平衡性,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作。 +插入删除操作记录会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。 ### 3. 与红黑树的比较 红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因: -(一)更少的检索次数 +(一)更少的查找次数 -平衡树检索数据的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。 +平衡树查找操作的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。 -红黑树的出度为 2,而 B+ Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B+ Tree 大非常多,因此检索的次数也就更多。 +红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,检索的次数也就更多。 (二)利用计算机预读特性 -为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。 +为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。 操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。 -## 索引分类 +## MySQL 索引 + +索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 ### 1. B+Tree 索引 -B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型。 +是大多数 MySQL 存储引擎的默认索引类型。 因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。 可以指定多个列作为索引列,多个索引列共同组成键。 -B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。 - -如果不是按照索引列的顺序进行查找,则无法使用索引。 +适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。 InnoDB 的 B+Tree 索引分为主索引和辅助索引。 @@ -185,12 +92,12 @@ InnoDB 的 B+Tree 索引分为主索引和辅助索引。 ### 2. 哈希索引 -InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 - 哈希索引能以 O(1) 时间进行查找,但是失去了有序性,它具有以下限制: - 无法用于排序与分组; -- 只支持精确查找,无法用于部分查找和范围查找; +- 只支持精确查找,无法用于部分查找和范围查找。 + +InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 ### 3. 全文索引 @@ -200,20 +107,12 @@ MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而 InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 -### 4. 空间数据索引(R-Tree) +### 4. 空间数据索引 -MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 +MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 必须使用 GIS 相关的函数来维护数据。 -## 索引的优点 - -- 大大减少了服务器需要扫描的数据行数。 - -- 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作); - -- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。 - ## 索引优化 ### 1. 独立的列 @@ -266,11 +165,25 @@ customer_id_selectivity: 0.0373 具有以下优点: -- 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。 +- 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。 - 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。 - 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。 -# 四、查询性能优化 +## 索引的优点 + +- 大大减少了服务器需要扫描的数据行数。 + +- 帮助服务器避免进行排序和分组,也就不需要创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。 + +- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。 + +## 索引的使用场景 + +- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。 +- 对于中到大型的表,索引就非常有效。 +- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 + +# 二、查询性能优化 ## 使用 Explain 进行分析 @@ -282,23 +195,13 @@ Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explai - key : 使用的索引 - rows : 扫描的行数 -更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735) - ## 优化数据访问 ### 1. 减少请求的数据量 -(一)只返回必要的列 - -最好不要使用 SELECT * 语句。 - -(二)只返回必要的行 - -使用 WHERE 语句进行查询过滤,有时候也需要使用 LIMIT 语句来限制返回的数据。 - -(三)缓存重复查询的数据 - -使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。 +- 只返回必要的列:最好不要使用 SELECT * 语句。 +- 只返回必要的行:使用 LIMIT 语句来限制返回的数据。 +- 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。 ### 2. 减少服务器端扫描的行数 @@ -324,12 +227,12 @@ do { ### 2. 分解大连接查询 -将一个大连接查询(JOIN)分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有: +将一个大连接查询分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有: - 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。 - 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。 - 减少锁竞争; -- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可扩展。 +- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。 - 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。 ```sql @@ -345,23 +248,111 @@ SELECT * FROM tag_post WHERE tag_id=1234; SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); ``` +# 三、存储引擎 + +## InnoDB + +是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 + +实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ 间隙锁(Next-Key Locking)防止幻影读。 + +主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 + +内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 + +支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 + +## MyISAM + +设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 + +提供了大量的特性,包括压缩表、空间数据索引等。 + +不支持事务。 + +不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 + +可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 + +如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 + +## 比较 + +- 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。 + +- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 + +- 外键:InnoDB 支持外键。 + +- 备份:InnoDB 支持在线热备份。 + +- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 + +- 其它特性:MyISAM 支持压缩表和空间数据索引。 + +# 四、数据类型 + +## 整型 + +TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。 + +INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。 + +## 浮点数 + +FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。 + +FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。 + +## 字符串 + +主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。 + +VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。 + +VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。 + +## 时间和日期 + +MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。 + +### 1. DATETIME + +能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 + +它与时区无关。 + +默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 + +### 2. TIMESTAMP + +和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。 + +它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。 + +MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。 + +默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。 + +应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。 + # 五、切分 ## 水平切分 -

- 水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。 当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 +

+ ## 垂直切分

垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 -在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userDB 等。 +在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。 ## Sharding 策略 @@ -375,15 +366,15 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); 使用分布式事务来解决,比如 XA 接口。 -### 2. JOIN +### 2. 链接 -可以将原来的 JOIN 查询分解成多个单表查询,然后在用户程序中进行 JOIN。 +可以将原来的 JOIN 分解成多个单表查询,然后在用户程序中进行 JOIN。 ### 3. ID 唯一性 -- 使用全局唯一 ID:GUID。 -- 为每个分片指定一个 ID 范围。 -- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)。 +- 使用全局唯一 ID:GUID +- 为每个分片指定一个 ID 范围 +- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法) 更多内容请参考: @@ -396,24 +387,24 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); 主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。 -- **binlog 线程** :负责将主服务器上的数据更改写入二进制文件(binlog)中。 -- **I/O 线程** :负责从主服务器上读取二进制日志文件,并写入从服务器的中继日志中。 +- **binlog 线程** :负责将主服务器上的数据更改写入二进制日志中。 +- **I/O 线程** :负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。 - **SQL 线程** :负责读取中继日志并重放其中的 SQL 语句。

## 读写分离 -主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作。 +主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。 -读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 - -MySQL 读写分离能提高性能的原因在于: +读写分离能提高性能的原因在于: - 主从服务器负责各自的读和写,极大程度缓解了锁的争用; -- 从服务器可以配置 MyISAM 引擎,提升查询性能以及节约系统开销; +- 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销; - 增加冗余,提高可用性。 +读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 +

# 参考资料 @@ -425,3 +416,4 @@ MySQL 读写分离能提高性能的原因在于: - [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases) - [SQL Azure Federation – Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.") - [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) +- [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735) diff --git a/notes/Redis.md b/notes/Redis.md index 0981abf0..8f231382 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -49,7 +49,7 @@ Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 -键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。 +键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、散列表、有序集合。 Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 @@ -58,7 +58,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中, | 数据类型 | 可以存储的值 | 操作 | | :--: | :--: | :--: | | STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | -| LIST | 列表 | 从两端压入或者弹出元素
读取单个或者多个元素
进行修剪,只保留一个范围内的元素 | +| LIST | 列表 | 从两端压入或者弹出元素
对单个或者多个元素
进行修剪,只保留一个范围内的元素 | | SET | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | | HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在| | ZSET | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | @@ -555,7 +555,7 @@ Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状 分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。 -假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。 +假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... ,有不同的方式来选择一个指定的键存储在哪个实例中。 - 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 - 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index 0ac7d7e4..1bac6bbb 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -10,8 +10,8 @@ * [9. 用两个栈实现队列](#9-用两个栈实现队列) * [10.1 斐波那契数列](#101-斐波那契数列) * [10.2 跳台阶](#102-跳台阶) -* [10.3 变态跳台阶](#103-变态跳台阶) -* [10.4 矩形覆盖](#104-矩形覆盖) +* [10.3 矩形覆盖](#103-矩形覆盖) +* [10.4 变态跳台阶](#104-变态跳台阶) * [11. 旋转数组的最小数字](#11-旋转数组的最小数字) * [12. 矩阵中的路径](#12-矩阵中的路径) * [13. 机器人的运动范围](#13-机器人的运动范围) @@ -465,7 +465,7 @@ public int pop() throws Exception { ## 题目描述 -求菲波那契数列的第 n 项,n <= 39。 +求斐波那契数列的第 n 项,n <= 39。

@@ -535,23 +535,6 @@ public class Solution { ## 解题思路 -复杂度:O(N) + O(N) - -```java -public int JumpFloor(int n) { - if (n == 1) - return 1; - int[] dp = new int[n]; - dp[0] = 1; - dp[1] = 2; - for (int i = 2; i < n; i++) - dp[i] = dp[i - 1] + dp[i - 2]; - return dp[n - 1]; -} -``` - -复杂度:O(N) + O(1) - ```java public int JumpFloor(int n) { if (n <= 2) @@ -567,7 +550,32 @@ public int JumpFloor(int n) { } ``` -# 10.3 变态跳台阶 +# 10.3 矩形覆盖 + +[NowCoder](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +## 题目描述 + +我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法? + +## 解题思路 + +```java +public int RectCover(int n) { + if (n <= 2) + return n; + int pre2 = 1, pre1 = 2; + int result = 0; + for (int i = 3; i <= n; i++) { + result = pre2 + pre1; + pre2 = pre1; + pre1 = result; + } + return result; +} +``` + +# 10.4 变态跳台阶 [NowCoder](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&tqId=11162&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -588,47 +596,6 @@ public int JumpFloorII(int target) { } ``` -# 10.4 矩形覆盖 - -[NowCoder](https://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6?tpId=13&tqId=11163&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -## 题目描述 - -我们可以用 2\*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2\*1 的小矩形无重叠地覆盖一个 2\*n 的大矩形,总共有多少种方法? - -## 解题思路 - -复杂度:O(N) + O(N) - -```java -public int RectCover(int n) { - if (n <= 2) - return n; - int[] dp = new int[n]; - dp[0] = 1; - dp[1] = 2; - for (int i = 2; i < n; i++) - dp[i] = dp[i - 1] + dp[i - 2]; - return dp[n - 1]; -} -``` - -复杂度:O(N) + O(1) - -```java -public int RectCover(int n) { - if (n <= 2) - return n; - int pre2 = 1, pre1 = 2; - int result = 0; - for (int i = 3; i <= n; i++) { - result = pre2 + pre1; - pre2 = pre1; - pre1 = result; - } - return result; -} -``` # 11. 旋转数组的最小数字 @@ -638,18 +605,34 @@ public int RectCover(int n) { 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 -例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。 +例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。 ## 解题思路 +在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。 + +本题可以修改二分查找算法进行求解: + - 当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m; - 否则解在 [m + 1, h] 之间,令 l = m + 1。 -因为 h 的赋值表达式为 h = m,因此循环体的循环条件应该为 l < h,详细解释请见 [Leetcode 题解](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md) 二分查找部分。 +```java +public int minNumberInRotateArray(int[] nums) { + if (nums.length == 0) + return 0; + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] <= nums[h]) + h = m; + else + l = m + 1; + } + return nums[l]; +} +``` -但是如果出现 nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。 - -复杂度:O(logN) + O(1) +如果数组元素允许重复的话,那么就会出现一个特殊的情况:nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。 ```java public int minNumberInRotateArray(int[] nums) { @@ -737,7 +720,9 @@ private char[][] buildMatrix(char[] array) { ## 题目描述 -地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格(35, 37),因为 3+5+3+7=18。但是,它不能进入方格(35, 38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? +地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。 + +例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,37),因为 3+5+3+8=19。请问该机器人能够达到多少个格子? ## 解题思路 @@ -806,7 +791,7 @@ return 36 (10 = 3 + 3 + 4) ### 贪心 -尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 +尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 证明:当 n >= 5 时,3(n - 3) - 2(n - 2) = n - 5 >= 0。因此把长度大于 5 的绳子切成两段,令其中一段长度为 3 可以使得两段的乘积最大。 @@ -847,19 +832,9 @@ public int integerBreak(int n) { 输入一个整数,输出该数二进制表示中 1 的个数。 -### Integer.bitCount() - -```java -public int NumberOf1(int n) { - return Integer.bitCount(n); -} -``` - ### n&(n-1) -O(M) 时间复杂度解法,其中 M 表示 1 的个数。 - -该位运算是去除 n 的位级表示中最低的那一位。 +该位运算去除 n 的位级表示中最低的那一位。 ``` n : 10110100 @@ -867,6 +842,9 @@ n-1 : 10110011 n&(n-1) : 10110000 ``` +时间复杂度:O(M),其中 M 表示 1 的个数。 + + ```java public int NumberOf1(int n) { int cnt = 0; @@ -878,13 +856,22 @@ public int NumberOf1(int n) { } ``` + +### Integer.bitCount() + +```java +public int NumberOf1(int n) { + return Integer.bitCount(n); +} +``` + # 16. 数值的整数次方 [NowCoder](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) ## 题目描述 -给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent。求 base 的 exponent 次方。 +给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。 ## 解题思路 @@ -892,7 +879,7 @@ public int NumberOf1(int n) {

-因为 (x\*x)n/2 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 +因为 (x\*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 ```java public double Power(double base, int exponent) { @@ -1028,6 +1015,7 @@ public ListNode deleteDuplication(ListNode pHead) { ```java public boolean match(char[] str, char[] pattern) { + int m = str.length, n = pattern.length; boolean[][] dp = new boolean[m + 1][n + 1]; @@ -1058,9 +1046,24 @@ public boolean match(char[] str, char[] pattern) { ## 题目描述 -请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。 +```html +true + +"+100" +"5e2" +"-123" +"3.1416" +"-1E-16" + +false + +"12e" +"1a3.14" +"1.2.3" +"+-5" +"12e+4.3" +``` -例如,字符串 "+100","5e2","-123","3.1416" 和 "-1E-16" 都表示数值。但是 "12e","1a3.14","1.2.3","+-5" 和 "12e+4.3" 都不是。 ## 解题思路 @@ -1124,8 +1127,7 @@ public void reOrderArray(int[] nums) {

```java -public ListNode FindKthToTail(ListNode head, int k) -{ +public ListNode FindKthToTail(ListNode head, int k) { if (head == null) return null; ListNode P1 = head; @@ -1148,9 +1150,7 @@ public ListNode FindKthToTail(ListNode head, int k) ## 题目描述 -一个链表中包含环,请找出该链表的环的入口结点。 - -要求不能使用额外的空间。 +一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。 ## 解题思路 @@ -1161,8 +1161,7 @@ public ListNode FindKthToTail(ListNode head, int k)

```java -public ListNode EntryNodeOfLoop(ListNode pHead) -{ +public ListNode EntryNodeOfLoop(ListNode pHead) { if (pHead == null || pHead.next == null) return null; ListNode slow = pHead, fast = pHead; @@ -1304,8 +1303,6 @@ private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) { ## 解题思路 -### 递归 - ```java public void Mirror(TreeNode root) { if (root == null) @@ -1322,29 +1319,6 @@ private void swap(TreeNode root) { } ``` -### 迭代 - -```java -public void Mirror(TreeNode root) { - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - TreeNode node = stack.pop(); - if (node == null) - continue; - swap(node); - stack.push(node.left); - stack.push(node.right); - } -} - -private void swap(TreeNode node) { - TreeNode t = node.left; - node.left = node.right; - node.right = t; -} -``` - # 28 对称的二叉树 [NowCder](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -1445,7 +1419,9 @@ public int min() { ## 题目描述 -输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。 +输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。 + +例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。 ## 解题思路 @@ -1838,7 +1814,7 @@ private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) { 多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。 -使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。 +使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素相等时,令 cnt++,否则令 cnt--。如果前面查找了 i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。 ```java public int MoreThanHalfNum_Solution(int[] nums) { diff --git a/notes/攻击技术.md b/notes/攻击技术.md index d5a735ba..46dd4616 100644 --- a/notes/攻击技术.md +++ b/notes/攻击技术.md @@ -207,7 +207,7 @@ ResultSet rs = stmt.executeQuery(); 拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 -分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 +分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 # 参考资料 diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index e7ef804b..1bcd2533 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -20,6 +20,7 @@ * [可串行化(SERIALIZABLE)](#可串行化serializable) * [五、多版本并发控制](#五多版本并发控制) * [版本号](#版本号) + * [隐藏的列](#隐藏的列) * [Undo 日志](#undo-日志) * [实现过程](#实现过程) * [快照读与当前读](#快照读与当前读) @@ -58,9 +59,7 @@ ### 2. 一致性(Consistency) -数据库在事务执行前后都保持一致性状态。 - -在一致性状态下,所有事务对一个数据的读取结果都是相同的。 +数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。 ### 3. 隔离性(Isolation) @@ -78,10 +77,10 @@ - 只有满足一致性,事务的执行结果才是正确的。 - 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 -- 在并发的情况下,多个事务并发执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 +- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 - 事务满足持久化是为了能应对数据库崩溃的情况。 -

+

## AUTOCOMMIT @@ -95,25 +94,25 @@ MySQL 默认采用自动提交模式。也就是说,如果不显式使用`STAR T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。 -

+

## 读脏数据 T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 -

+

## 不可重复读 T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 -

+

## 幻影读 T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 -

+

---- @@ -123,7 +122,6 @@ T1 读取某个范围的数据,T2 在这个范围内插 ## 封锁粒度 - MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 @@ -150,8 +148,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 | - | X | S | | :--: | :--: | :--: | -|X|NO|NO| -|S|NO|YES| +|X|×|×| +|S|×|√| ### 2. 意向锁 @@ -170,10 +168,10 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 | - | X | IX | S | IS | | :--: | :--: | :--: | :--: | :--: | -|X |NO |NO |NO | NO| -|IX |NO |YES |NO | YES| -|S |NO |NO |YES | YES| -|IS |NO |YES |YES | YES| +|X |× |× |× | ×| +|IX |× |√ |× | √| +|S |× |× |√ | √| +|IS |× |√ |√ | √| 解释如下: @@ -298,32 +296,30 @@ SELECT ... FOR UPDATE; | 隔离级别 | 脏读 | 不可重复读 | 幻影读 | | :---: | :---: | :---:| :---: | -| 未提交读 | YES | YES | YES | -| 提交读 | NO | YES | YES | -| 可重复读 | NO | NO | YES | -| 可串行化 | NO | NO | NO | +| 未提交读 | √ | √ | √ | +| 提交读 | × | √ | √ | +| 可重复读 | × | × | √ | +| 可串行化 | × | × | × | # 五、多版本并发控制 -多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。 - -而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。 - -可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 +多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 ## 版本号 - 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 - 事务版本号:事务开始时的系统版本号。 -InooDB 的 MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号: +## 隐藏的列 + +MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号: - 创建版本号:指示创建一个数据行的快照时的系统版本号; - 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。 ## Undo 日志 -InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。 +MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。

@@ -331,15 +327,13 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 以下实现过程针对可重复读隔离级别。 -### 1. SELECT - 当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。 +### 1. SELECT + 多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 -把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 - -除了上面的要求,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 +把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 ### 2. INSERT @@ -385,7 +379,7 @@ MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题 锁定一个记录上的索引,而不是记录本身。 -如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。 +如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。 ## Gap Locks @@ -397,7 +391,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ## Next-Key Locks -它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定范围内的索引。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间: +它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间: ```sql (negative infinity, 10] @@ -432,10 +426,10 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; 不符合范式的关系,会产生很多异常,主要有以下四种异常: -- 冗余数据:例如 学生-2 出现了两次。 +- 冗余数据:例如 `学生-2` 出现了两次。 - 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 -- 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。 -- 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 +- 删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 `课程-1` 需要删除第一行和第三行,那么 `学生-1` 的信息就会丢失。 +- 插入异常:例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 ## 范式 @@ -506,7 +500,11 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门 非主属性不传递函数依赖于键码。 -上面的 关系-1 中存在以下传递函数依赖:Sno -> Sdept -> Mname,可以进行以下分解: +上面的 关系-1 中存在以下传递函数依赖: + +- Sno -> Sdept -> Mname + +可以进行以下分解: 关系-11 @@ -533,13 +531,19 @@ Entity-Relationship,有三个组成部分:实体、属性、联系。 包含一对一,一对多,多对多三种。 -如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B;如果是一对一,画两个带箭头的线段;如果是多对多,画两个不带箭头的线段。下图的 Course 和 Student 是一对多的关系。 +- 如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B; +- 如果是一对一,画两个带箭头的线段; +- 如果是多对多,画两个不带箭头的线段。 + +下图的 Course 和 Student 是一对多的关系。

## 表示出现多次的关系 -一个实体在联系出现几次,就要用几条线连接。下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。 +一个实体在联系出现几次,就要用几条线连接。 + +下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。

@@ -549,7 +553,7 @@ Entity-Relationship,有三个组成部分:实体、属性、联系。

-一般只使用二元联系,可以把多元关系转换为二元关系。 +一般只使用二元联系,可以把多元联系转换为二元联系。

diff --git a/notes/消息队列.md b/notes/消息队列.md index c5687e8a..209d962a 100644 --- a/notes/消息队列.md +++ b/notes/消息队列.md @@ -9,6 +9,7 @@ * [三、可靠性](#三可靠性) * [发送端的可靠性](#发送端的可靠性) * [接收端的可靠性](#接收端的可靠性) +* [参考资料](#参考资料) @@ -29,15 +30,10 @@ 发布与订阅模式和观察者模式有以下不同: - 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 -- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息。 +- 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息,可以立即返回。

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

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

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

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

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

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

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

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

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

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

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

+

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

+

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

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

+

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

+

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

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

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

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

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

+ +```java +public class WeightedQuickUnionUF extends UF { + + // 保存节点的数量信息 + private int[] sz; + + + public WeightedQuickUnionUF(int N) { + super(N); + this.sz = new int[N]; + for (int i = 0; i < N; i++) { + this.sz[i] = 1; + } + } + + + @Override + public int find(int p) { + while (p != id[p]) { + p = id[p]; + } + return p; + } + + + @Override + public void union(int p, int q) { + + int i = find(p); + int j = find(q); + + if (i == j) return; + + if (sz[i] < sz[j]) { + id[i] = j; + sz[j] += sz[i]; + } else { + id[j] = i; + sz[i] += sz[j]; + } + } +} +``` + +## 路径压缩的加权 Quick Union + +在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 + +## 比较 + +| 算法 | union | find | +| :---: | :---: | :---: | +| Quick Find | N | 1 | +| Quick Union | 树高 | 树高 | +| 加权 Quick Union | logN | logN | +| 路径压缩的加权 Quick Union | 非常接近 1 | 非常接近 1 | + +# 五、栈和队列 + +## 栈 + +```java +public interface MyStack extends Iterable { + + MyStack push(Item item); + + Item pop() throws Exception; + + boolean isEmpty(); + + int size(); + +} +``` + +### 1. 数组实现 + +```java +public class ArrayStack implements MyStack { + + // 栈元素数组,只能通过转型来创建泛型数组 + private Item[] a = (Item[]) new Object[1]; + + // 元素数量 + private int N = 0; + + + @Override + public MyStack push(Item item) { + check(); + a[N++] = item; + return this; + } + + + @Override + public Item pop() throws Exception { + + if (isEmpty()) { + throw new Exception("stack is empty"); + } + + Item item = a[--N]; + + check(); + + // 避免对象游离 + a[N] = null; + + return item; + } + + + private void check() { + + if (N >= a.length) { + resize(2 * a.length); + + } else if (N > 0 && N <= a.length / 4) { + resize(a.length / 2); + } + } + + + /** + * 调整数组大小,使得栈具有伸缩性 + */ + private void resize(int size) { + + Item[] tmp = (Item[]) new Object[size]; + + for (int i = 0; i < N; i++) { + tmp[i] = a[i]; + } + + a = tmp; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public Iterator iterator() { + + // 返回逆序遍历的迭代器 + return new Iterator() { + + private int i = N; + + @Override + public boolean hasNext() { + return i > 0; + } + + @Override + public Item next() { + return a[--i]; + } + }; + + } +} +``` + +### 2. 链表实现 + +需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素时就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 + +```java +public class ListStack implements MyStack { + + private Node top = null; + private int N = 0; + + + private class Node { + Item item; + Node next; + } + + + @Override + public MyStack push(Item item) { + + Node newTop = new Node(); + + newTop.item = item; + newTop.next = top; + + top = newTop; + + N++; + + return this; + } + + + @Override + public Item pop() throws Exception { + + if (isEmpty()) { + throw new Exception("stack is empty"); + } + + Item item = top.item; + + top = top.next; + N--; + + return item; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public Iterator iterator() { + + return new Iterator() { + + private Node cur = top; + + + @Override + public boolean hasNext() { + return cur != null; + } + + + @Override + public Item next() { + Item item = cur.item; + cur = cur.next; + return item; + } + }; + + } +} +``` + +## 队列 + +First-In-First-Out + +下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 + +这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。 + +```java +public interface MyQueue extends Iterable { + + int size(); + + boolean isEmpty(); + + MyQueue add(Item item); + + Item remove() throws Exception; +} +``` + +```java +public class ListQueue implements MyQueue { + + private Node first; + private Node last; + int N = 0; + + + private class Node { + Item item; + Node next; + } + + + @Override + public boolean isEmpty() { + return N == 0; + } + + + @Override + public int size() { + return N; + } + + + @Override + public MyQueue add(Item item) { + + Node newNode = new Node(); + newNode.item = item; + newNode.next = null; + + if (isEmpty()) { + last = newNode; + first = newNode; + } else { + last.next = newNode; + last = newNode; + } + + N++; + return this; + } + + + @Override + public Item remove() throws Exception { + + if (isEmpty()) { + throw new Exception("queue is empty"); + } + + Node node = first; + first = first.next; + N--; + + if (isEmpty()) { + last = null; + } + + return node.item; + } + + + @Override + public Iterator iterator() { + + return new Iterator() { + + Node cur = first; + + + @Override + public boolean hasNext() { + return cur != null; + } + + + @Override + public Item next() { + Item item = cur.item; + cur = cur.next; + return item; + } + }; + } +} +``` + + + + + # 六、查找 符号表(Symbol Table)是一种存储键值对的数据结构,可以支持快速查找操作。 diff --git a/notes/缓存.md b/notes/缓存.md index 9a9f2810..ed834253 100644 --- a/notes/缓存.md +++ b/notes/缓存.md @@ -6,6 +6,7 @@ * [五、缓存问题](#五缓存问题) * [六、数据分布](#六数据分布) * [七、一致性哈希](#七一致性哈希) +* [参考资料](#参考资料) @@ -29,10 +30,6 @@ - LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最远的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。 -参考资料: - -- [缓存那些事](https://tech.meituan.com/cache_about.html) - # 二、LRU 以下是一个基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下: @@ -143,10 +140,6 @@ public class LRU implements Iterable { } ``` -源代码: - -- [CyC2018/Algorithm](https://github.com/CyC2018/Algorithm/tree/master/Caching) - # 三、缓存位置 ## 浏览器 @@ -169,7 +162,7 @@ public class LRU implements Iterable { 使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。 -相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存。而本地缓存需要在服务器集群之间进行同步,实现和性能开销上都非常大。 +相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存,而本地缓存需要在服务器集群之间进行同步,实现和性能开销上都非常大。 ## 数据库缓存 @@ -177,7 +170,7 @@ MySQL 等数据库管理系统具有自己的查询缓存机制来提高 SQL 查 # 四、CDN -内容分发网络(Content distribution network,CDN)是一种通过互连的网络系统,利用更靠近用户的服务器更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。 +内容分发网络(Content distribution network,CDN)是一种互连的网络系统,它利用更靠近用户的服务器从而更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。 CDN 主要有以下优点: @@ -187,11 +180,6 @@ CDN 主要有以下优点:

-参考资料: - -- [内容分发网络](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/) - # 五、缓存问题 ## 缓存穿透 @@ -213,7 +201,7 @@ CDN 主要有以下优点: - 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现; - 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。 -- 也可以在进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。 +- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。 ## 缓存一致性 @@ -243,10 +231,6 @@ CDN 主要有以下优点: - 能保持数据原有的顺序; - 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。 -参考资料: - -- 大规模分布式存储系统 - # 七、一致性哈希 Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据失效的问题。 @@ -267,6 +251,10 @@ Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了 数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得大,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。 -参考资料: +# 参考资料 +- 大规模分布式存储系统 +- [缓存那些事](https://tech.meituan.com/cache_about.html) - [一致性哈希算法](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/notes/设计模式.md b/notes/设计模式.md index b8f4dc9b..531595e3 100644 --- a/notes/设计模式.md +++ b/notes/设计模式.md @@ -60,9 +60,9 @@ (一)懒汉式-线程不安全 -以下实现中,私有静态变量 uniqueInstance 被延迟化实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。 +以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。 -这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 `if (uniqueInstance == null)` ,并且此时 uniqueInstance 为 null,那么多个线程会执行 `uniqueInstance = new Singleton();` 语句,这将导致多次实例化 uniqueInstance。 +这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 `if (uniqueInstance == null)` ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 `uniqueInstance = new Singleton();` 语句,这将导致多次实例化 uniqueInstance。 ```java public class Singleton { @@ -81,11 +81,21 @@ public class Singleton { } ``` -(二)懒汉式-线程安全 +(二)饿汉式-线程安全 -只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了对 uniqueInstance 进行多次实例化的问题。 +线程不安全问题主要是由于 uniqueInstance 被多次实例化,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。 -但是这样有一个问题,就是当一个线程进入该方法之后,其它线程试图进入该方法都必须等待,因此性能上有一定的损耗。 +但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。 + +```java +private static Singleton uniqueInstance = new Singleton(); +``` + +(三)懒汉式-线程安全 + +只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了多次实例化 uniqueInstance 的问题。 + +但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,因此性能上有一定的损耗。 ```java public static synchronized Singleton getUniqueInstance() { @@ -96,16 +106,6 @@ public static synchronized Singleton getUniqueInstance() { } ``` -(三)饿汉式-线程安全 - -线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。 - -但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。 - -```java -private static Singleton uniqueInstance = new Singleton(); -``` - (四)双重校验锁-线程安全 uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。 @@ -175,7 +175,7 @@ public class Singleton { } ``` -(五)枚举实现 +(六)枚举实现 这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。 @@ -231,27 +231,9 @@ public class Singleton implements Serializable { 简单工厂不是设计模式,更像是一种编程习惯。它把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化。 -

- 这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。因为客户类往往有多个,如果不使用简单工厂,所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。 -如果存在下面这种代码,就需要使用简单工厂将对象实例化的部分放到简单工厂中。 - -```java -public class Client { - public static void main(String[] args) { - int type = 1; - Product product; - if (type == 1) { - product = new ConcreteProduct1(); - } else if (type == 2) { - product = new ConcreteProduct2(); - } else { - product = new ConcreteProduct(); - } - } -} -``` +

### 实现 @@ -275,6 +257,27 @@ public class ConcreteProduct2 implements Product { } ``` +以下的 Client 类中包含了实例化的代码,这是一种错误的实现,如果在客户类中存在实例化代码,就需要将代码放到简单工厂中。 + +```java +public class Client { + public static void main(String[] args) { + int type = 1; + Product product; + if (type == 1) { + product = new ConcreteProduct1(); + } else if (type == 2) { + product = new ConcreteProduct2(); + } else { + product = new ConcreteProduct(); + } + // do something with the product + } +} +``` + +以下的 SimpleFactory 是简单工厂实现,它被所有需要进行实例化的客户类调用。 + ```java public class SimpleFactory { public Product createProduct(int type) { @@ -293,6 +296,7 @@ public class Client { public static void main(String[] args) { SimpleFactory simpleFactory = new SimpleFactory(); Product product = simpleFactory.createProduct(1); + // do something with the product } } ``` @@ -2906,7 +2910,7 @@ Java 利用缓存来加速大量小对象的访问时间。 - 远程代理(Remote Proxy):控制对远程对象(不同地址空间)的访问,它负责将请求及其参数进行编码,并向不同地址空间中的对象发送已经编码的请求。 - 虚拟代理(Virtual Proxy):根据需要创建开销很大的对象,它可以缓存实体的附加信息,以便延迟对它的访问,例如在网站加载一个很大图片时,不能马上完成,可以用虚拟代理缓存图片的大小信息,然后生成一张临时图片代替原始图片。 - 保护代理(Protection Proxy):按权限控制对象的访问,它负责检查调用者是否具有实现一个请求所必须的访问权限。 -- 智能代理(Smart Reference):取代了简单的指针,它在访问对象时执行一些附加操作:记录对象的引用次数,比如智能智能;当第一次引用一个持久化对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。 +- 智能代理(Smart Reference):取代了简单的指针,它在访问对象时执行一些附加操作:记录对象的引用次数;当第一次引用一个持久化对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。

diff --git a/notes/面向对象思想.md b/notes/面向对象思想.md index 9b59e43d..203c7ebf 100644 --- a/notes/面向对象思想.md +++ b/notes/面向对象思想.md @@ -37,6 +37,7 @@ ```java public class Person { + private String name; private int gender; private int age; @@ -63,17 +64,20 @@ public class Person { 继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。 +继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。 + Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型** 。 ```java Animal animal = new Cat(); ``` -继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。 - ## 多态 -多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。 +多态分为编译时多态和运行时多态: + +- 编译时多态主要指方法的重载 +- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定 运行时多态有三个条件: @@ -116,7 +120,7 @@ public class Music { # 二、类图 -以下类图使用 [PlantUML](https://www.planttext.com/) 绘制,更多语法及使用请参考:http://plantuml.com/ +以下类图使用 [PlantUML](https://www.planttext.com/) 绘制,更多语法及使用请参考:http://plantuml.com/ 。 ## 泛化关系 (Generalization) @@ -327,7 +331,7 @@ Vihicle .. N ### 2. 合成复用原则 -尽量使用对象组合,而不是继承来达到复用的目的。 +尽量使用对象组合,而不是通过继承来达到复用的目的。 ### 3. 共同封闭原则 @@ -349,3 +353,4 @@ 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/pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png b/pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png new file mode 100644 index 00000000..fc0999f9 Binary files /dev/null and b/pics/0157d362-98dd-4e51-ac26-00ecb76beb3e.png differ diff --git a/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png b/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png new file mode 100644 index 00000000..c4592305 Binary files /dev/null and b/pics/1a2f2998-d0da-41c8-8222-1fd95083a66b.png differ diff --git a/pics/220790c6-4377-4a2e-8686-58398afc8a18.png b/pics/220790c6-4377-4a2e-8686-58398afc8a18.png new file mode 100644 index 00000000..79105257 Binary files /dev/null and b/pics/220790c6-4377-4a2e-8686-58398afc8a18.png differ diff --git a/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png b/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png new file mode 100644 index 00000000..a97e49a6 Binary files /dev/null and b/pics/2a8e1442-2381-4439-a83f-0312c8678b1f.png differ diff --git a/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png b/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png new file mode 100644 index 00000000..3b05b25b Binary files /dev/null and b/pics/37e79a32-95a9-4503-bdb1-159527e628b8.png differ diff --git a/pics/766aedd0-1b00-4065-aa2b-7d31138df84f.png b/pics/766aedd0-1b00-4065-aa2b-7d31138df84f.png new file mode 100644 index 00000000..e8a69bff Binary files /dev/null and b/pics/766aedd0-1b00-4065-aa2b-7d31138df84f.png differ diff --git a/pics/864bfa7d-1149-420c-a752-f9b3d4e782ec.png b/pics/864bfa7d-1149-420c-a752-f9b3d4e782ec.png new file mode 100644 index 00000000..72cc988f Binary files /dev/null and b/pics/864bfa7d-1149-420c-a752-f9b3d4e782ec.png differ diff --git a/pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png b/pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png new file mode 100644 index 00000000..86e2294f Binary files /dev/null and b/pics/f8047846-efd4-42be-b6b7-27a7c4998b51.png differ