diff --git a/.gitignore b/.gitignore index e43b0f98..c91a56b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +*.txt diff --git a/BOOKLIST.md b/BOOKLIST.md index bdd8eb92..4805a7dc 100644 --- a/BOOKLIST.md +++ b/BOOKLIST.md @@ -79,4 +79,4 @@ - [JavaScript 语言精粹](https://book.douban.com/subject/3590768/) - [利用 Python 进行数据分析](https://book.douban.com/subject/25779298/) -- [概率论与数理统计](https://book.douban.com/subject/2201479/) \ No newline at end of file +- [概率论与数理统计](https://book.douban.com/subject/2201479/) diff --git a/README.md b/README.md index 89625ec3..42788e54 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,169 @@ - | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| -| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) | +| 算法[: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: -> [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) +- [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) -目录根据原书第二版进行编排。 + 目录根据原书第二版进行编排,代码和原书有所不同,尽量比原书更简洁。 -> [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) +- [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) -主要参考 Robert Sedgewick 的算法书进行实现,源代码以及测试代码可在另一个仓库获取。 + 排序、并查集、栈和队列、红黑树、散列表。 ## 操作系统 :computer: -> [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md) +- [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md) -参考 现代操作系统、Unix 环境高级编程、深入理解计算机系统。 + 进程管理、内存管理、设备管理、链接。 -> [Linux](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Linux.md) +- [Linux](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Linux.md) -参考 鸟哥的 Linux 私房菜。 + 基本实现原理以及基本操作。 ## 网络 :cloud: -> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) +- [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) -参考 谢希仁的计算机网络、计算机网络 自顶向下方法、TCP/IP 详解。 + 物理层、链路层、网络层、运输层、应用层。 -> [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md) +- [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md) -参考 图解 HTTP,更多的是参考网上的文档,比如 MDN、维基百科等。 + 方法、状态码、Cookie、缓存、连接管理、HTTPs、HTTP 2.0。 -> [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md) +- [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md) -参考 Unix 网络编程。 + I/O 模型、I/O 多路复用。 ## 面向对象 :couple: -> [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md) +- [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md) -参考 Head First 设计模式、设计模式 可复用面向对象软件的基础,实现了 Gof 的 23 种设计模式。 + 实现了 Gof 的 23 种设计模式。 -> [面向对象思想](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/面向对象思想.md) +- [面向对象思想](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/面向对象思想.md) -内容包括三大原则(继承、封装、多态)、类图、设计原则。 + 三大原则(继承、封装、多态)、类图、设计原则。 ## 数据库 :floppy_disk: -> [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md) +- [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md) -参考 数据库系统原理。 + 事务、锁、隔离级别、MVCC、间隙锁、范式。 -> [SQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/SQL.md) +- [SQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/SQL.md) -参考 SQL 必知必会。 + SQL 基本语法。 -> [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md) +- [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md) -Leetcode 上数据库题目的解题记录。 + Leetcode 上数据库题目的解题记录。 -> [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md) +- [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md) -参考 高性能 MySQL。 + 存储引擎、索引、查询优化、切分、复制。 -> [Redis](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Redis.md) +- [Redis](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Redis.md) -参考 Redis 设计与实现、Redis 实战。 + 五种数据类型、字典和跳跃表数据结构、使用场景、和 Memcache 的比较、淘汰策略、持久化、文件事件的 Reactor 模式、复制。 ## Java :coffee: -> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) +- [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) -参考 Effective Java、Java 编程思想,也有部分内容参考官方文档以及 StackOverflow。 + 不会涉及很多基本语法介绍,主要是一些实现原理以及关键特性。 -> [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md) +- [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md) -参考 深入理解 Java 虚拟机。 + 源码分析:ArrayList、Vector、CopyOnWriteArrayList、LinkedList、HashMap、ConcurrentHashMap、LinkedHashMap、WeekHashMap。 -> [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md) +- [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md) -参考 Java 编程思想、深入理解 Java 虚拟机。 + 线程使用方式、两种互斥同步方法、线程协作、JUC、线程安全、内存模型、锁优化。 -> [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md) +- [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md) -包含容器源码的分析。 + 运行时数据区域、垃圾收集、类加载。 -> [Java I/O](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md) +- [Java I/O](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md) -包含 NIO 的原理以及实例。 + NIO 的原理以及实例。 -## 分布式 :sweat_drops: +## 系统设计 :bulb: -> [一致性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/一致性.md) +- [系统设计基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/系统设计基础.md) -CAP、BASE、Paxos、Raft + 性能、伸缩性、扩展性、可用性、安全性 ->[分布式问题分析](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式问题分析.md) +- [分布式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式.md) -分布式事务、分布式锁、分布式 Session、负载均衡 + 分布式锁、分布式事务、CAP、BASE、Paxos、Raft + +- [集群](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/集群.md) + + 负载均衡、Session 管理 + +- [攻击技术](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/攻击技术.md) + + XSS、CSRF、SQL 注入、DDoS + +- [缓存](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/缓存.md) + + 缓存特征、缓存位置、缓存问题、数据分布、一致性哈希、LRU、CDN + +- [消息队列](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/消息队列.md) + + 消息处理模型、使用场景、可靠性 ## 工具 :hammer: -> [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md) +- [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md) -一些 Git 的使用和概念。 + 一些 Git 的使用和概念。 -> [正则表达式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/正则表达式.md) +- [Docker](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Docker.md) -参考 正则表达式必知必会。 + Docker 基本原理。 + +- [正则表达式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/正则表达式.md) + + 正则表达式基本语法。 + +- [构建工具](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/构建工具.md) + + 构建工具的基本概念、主流构建工具介绍。 ## 编码实践 :speak_no_evil: -> [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md) +- [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md) -参考 重构 改善既有代码的设计。 + 参考 重构 改善既有代码的设计。 -> [代码可读性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码可读性.md) +- [代码可读性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码可读性.md) -参考 编写可读代码的艺术。 + 参考 编写可读代码的艺术。 -> [代码风格规范](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码风格规范.md) +- [代码风格规范](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码风格规范.md) -Google 开源项目的代码风格规范。 + Google 开源项目的代码风格规范。 ## 后记 :memo: -**关于仓库** +### About 这个仓库是笔者的一个学习笔记,主要总结一些比较重要的知识点,希望对大家有所帮助。 @@ -139,38 +171,69 @@ Google 开源项目的代码风格规范。 [BOOKLIST](https://github.com/CyC2018/Interview-Notebook/blob/master/BOOKLIST.md),这个书单是笔者至今看的一些比较好的技术书籍,虽然没有全都看完,但每本书多多少少都看了一部分。 -**如何贡献** +### How To Contribute 笔记内容是笔者一个字一个字打上去的,难免会有一些笔误,如果发现笔误可直接在相应文档进行编辑修改。 -欢迎提交对本仓库的改进建议~ +如果想要提交一个仓库现在还没有的全新内容,可以先将相应的文档放到 other 目录下。 -**授权相关** +欢迎在 Issue 中提交对本仓库的改进建议~ + +### Authorization 虽然没有加开源协议,但是允许非商业性使用。 转载使用请注明出处,谢谢! -**上传方案** - -笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上,实现了一整套自动化上传方案,包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。 - -进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。 - -这里提供了笔者实现的 GFM 文档转换工具的链接:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 - -**排版指南** +### Typesetting 笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/) 进行排版,以保证内容的可读性。 笔记不使用 `![]()` 这种方式来引用图片,而是用 `` 标签。一方面是为了能够控制图片以合适的大小显示,另一方面是因为 GFM 不支持 `
![]()
` 让图片居中显示,只能使用 `
` 达到居中的效果。 -这里提供了笔者实现的中英混排文档在线排版工具的链接:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。 +笔者将自己实现的文档排版功能提取出来,放在 Github Page 中,无需下载安装即可免费使用:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。 -**声明** +### Uploading + +笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上,实现了一整套自动化上传方案,包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。 + +进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。 + +笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github,或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 + +### Statement 本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.) -**鸣谢** +### Logo + +Power by [logomakr](https://logomakr.com/). + +### Acknowledgements + +感谢以下人员对本仓库做出的贡献,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR,请与笔者联系。 + + + + + + + + + + + + + + + + + + + + + + + + -[TeeKee](https://github.com/linw7) [g10guang](https://github.com/g10guang) [crossoverJie](https://github.com/crossoverJie) diff --git a/SUMMARY.md b/SUMMARY.md index e60f018e..69b6103c 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,3 +1,5 @@ +This file used to generate gitbook catalogue. + # Summary * 算法 @@ -32,5 +34,3 @@ - - diff --git a/notes/Docker.md b/notes/Docker.md new file mode 100644 index 00000000..f230f762 --- /dev/null +++ b/notes/Docker.md @@ -0,0 +1,102 @@ + +* [一、解决的问题](#一解决的问题) +* [二、与虚拟机的比较](#二与虚拟机的比较) +* [三、优势](#三优势) +* [四、使用场景](#四使用场景) +* [五、镜像与容器](#五镜像与容器) + + + +

+ +# 一、解决的问题 + +由于不同的机器有不同的操作系统,以及不同的库和组件,在将一个应用部署到多台机器上需要进行大量的环境配置操作。 + +Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其他机器中。 + +参考资料: + +- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/) +- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html) + +# 二、与虚拟机的比较 + +虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。 + +

+ +

+ +## 启动速度 + +启动虚拟机需要启动虚拟机的操作系统,再启动相应的应用,这个过程会非常慢; + +而启动 Docker 相当于启动宿主操作系统上的一个进程。 + +## 占用资源 + +虚拟机是一个完整的操作系统,需要占用大量的磁盘空间、内存和 CPU,一台机器只能开启几十个的虚拟机。 + +而 Docker 只是一个进程,只需要将应用以及相应的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。 + +参考资料: + +- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php) + +# 三、优势 + +除了启动速度快以及占用资源少之外,Docker 具有以下优势: + +## 更容易迁移 + +Docker 可以提供一致性的运行环境,可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。 + +## 更容易维护 + +Docker 使用分层技术和镜像,使得应用可以更容易复用重复部分。复用程度越高,维护工作也越容易。 + +## 更容易扩展 + +可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像得到我们想要的镜像非常容易。 + +参考资料: + +- [为什么要使用 Docker?](https://yeasy.gitbooks.io/docker_practice/introduction/why.html) + +# 四、使用场景 + +## 持续集成 + +持续集成指的是频繁地将代码集成到主干上,这样能够更快地发现错误。 + +Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。 + +## 提供可伸缩的云服务 + +根据应用的负载情况,可以很容易地增加或者减少 Docker。 + +## 搭建微服务架构 + +Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。 + +参考资料: + +- [What is Docker](https://www.docker.com/what-docker) +- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html) + +# 五、镜像与容器 + +镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。 + +镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。 + +在构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。 + +

+ +参考资料: + +- [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/HTTP.md b/notes/HTTP.md index 7d64c901..537b24fe 100644 --- a/notes/HTTP.md +++ b/notes/HTTP.md @@ -40,12 +40,12 @@ * [完整性保护](#完整性保护) * [HTTPs 的缺点](#https-的缺点) * [配置 HTTPs](#配置-https) -* [七、Web 攻击技术](#七web-攻击技术) - * [跨站脚本攻击](#跨站脚本攻击) - * [跨站请求伪造](#跨站请求伪造) - * [SQL 注入攻击](#sql-注入攻击) - * [拒绝服务攻击](#拒绝服务攻击) -* [八、GET 和 POST 的区别](#八get-和-post-的区别) +* [七、HTTP/2.0](#七http20) + * [HTTP/1.x 缺陷](#http1x-缺陷) + * [二进制分帧层](#二进制分帧层) + * [服务端推送](#服务端推送) + * [首部压缩](#首部压缩) +* [八、GET 和 POST 比较](#八get-和-post-比较) * [作用](#作用) * [参数](#参数) * [安全](#安全) @@ -53,11 +53,6 @@ * [可缓存](#可缓存) * [XMLHttpRequest](#xmlhttprequest) * [九、HTTP/1.0 与 HTTP/1.1 的区别](#九http10-与-http11-的区别) -* [十、HTTP/2.0](#十http20) - * [HTTP/1.x 缺陷](#http1x-缺陷) - * [二进制分帧层](#二进制分帧层) - * [服务端推送](#服务端推送) - * [首部压缩](#首部压缩) * [参考资料](#参考资料) @@ -66,13 +61,13 @@ ## URL -- URI(Uniform Resource Indentifier,统一资源标识符) -- URL(Uniform Resource Locator,统一资源定位符) -- URN(Uniform Resource Name,统一资源名称),例如 urn:isbn:0-486-27557-4。 - URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。 -

+- URI(Uniform Resource Identifier,统一资源标识符) +- URL(Uniform Resource Locator,统一资源定位符) +- URN(Uniform Resource Name,统一资源名称) + +

## 请求和响应报文 @@ -202,7 +197,7 @@ CONNECT www.example.com:443 HTTP/1.1 - **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 -- **206 Partial Content** :表示客户端进行了范围请求。响应报文包含由 Content-Range 指定范围的实体内容。 +- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 ## 3XX 重定向 @@ -224,7 +219,7 @@ CONNECT www.example.com:443 HTTP/1.1 - **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 -- **403 Forbidden** :请求被拒绝,服务器端没有必要给出拒绝的详细理由。 +- **403 Forbidden** :请求被拒绝。 - **404 Not Found** @@ -232,7 +227,7 @@ CONNECT www.example.com:443 HTTP/1.1 - **500 Internal Server Error** :服务器正在执行请求时发生错误。 -- **503 Service Unavilable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 +- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 # 四、HTTP 首部 @@ -313,7 +308,9 @@ CONNECT www.example.com:443 HTTP/1.1 HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 -Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。它用于告知服务端两个请求是否来自同一浏览器,并保持用户的登录状态。 +Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。 ### 1. 用途 @@ -321,8 +318,6 @@ Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据 - 个性化设置(如用户自定义设置、主题等) - 浏览器行为跟踪(如跟踪分析用户行为等) -Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。 - ### 2. 创建过程 服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 @@ -336,7 +331,7 @@ Set-Cookie: tasty_cookie=strawberry [page content] ``` -客户端之后对同一个服务器发送请求时,会从浏览器中读出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。 +客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。 ```html GET /sample_page.html HTTP/1.1 @@ -365,9 +360,9 @@ console.log(document.cookie); ### 5. Secure 和 HttpOnly -标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 +标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 -标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。因为跨域脚本 (XSS) 攻击常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 +标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 ```html Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly @@ -387,15 +382,15 @@ Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径 除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 -Session 可以存储在服务器上的文件、数据库或者内存中,现在最常见的是将 Session 存储在内存型数据库中,比如 Redis。 +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 -使用 Session 维护用户登录的过程如下: +使用 Session 维护用户登录状态的过程如下: - 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; - 服务器验证该用户名和密码; -- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID; +- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; - 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; -- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之后的业务操作。 +- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 @@ -414,7 +409,7 @@ Session 可以存储在服务器上的文件、数据库或者内存中,现在 ### 1. 优点 - 缓解服务器压力; -- 降低客户端获取资源的延迟(缓存资源比服务器上的资源离客户端更近)。 +- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。 ### 2. 实现方法 @@ -465,7 +460,10 @@ max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中 Cache-Control: max-age=31536000 ``` -Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中,Cache-Control : max-age 指令会被忽略掉。 +Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 + +- 在 HTTP/1.1 中,会优先处理 max-age 指令; +- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 ```html Expires: Wed, 04 Jul 2012 08:26:05 GMT @@ -485,7 +483,7 @@ ETag: "82e22293907ce725faf67773957acd12" If-None-Match: "82e22293907ce725faf67773957acd12" ``` -Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应, +Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应。 ```html Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT @@ -501,13 +499,16 @@ If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT ### 1. 短连接与长连接 -当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 +当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。 -从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。 +长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 + +- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`; +- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。 ### 2. 流水线 -默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 +默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。 @@ -517,17 +518,17 @@ If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT ### 1. 类型 -**(一)服务端驱动型内容协商** +**(一)服务端驱动型** 客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag,服务器根据这些字段返回特定的资源。 它存在以下问题: - 服务器很难知道客户端浏览器的全部信息; -- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术)。 +- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术); - 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 -**(二)代理驱动型协商** +**(二)代理驱动型** 服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。 @@ -543,9 +544,11 @@ Vary: Accept-Language ## 内容编码 -内容编码将实体主体进行压缩,从而减少传输的数据量。常用的内容编码有:gzip、compress、deflate、identity。 +内容编码将实体主体进行压缩,从而减少传输的数据量。 -浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中,Vary 首部中至少要包含 Content-Encoding,这样的话,缓存服务器就可以对资源的不同展现形式进行缓存。 +常用的内容编码有:gzip、compress、deflate、identity。 + +浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。 ## 范围请求 @@ -627,10 +630,14 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名, - 网络访问控制 - 访问日志记录 -代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在,而反向代理一般位于内部网络中,用户察觉不到。 +代理服务器分为正向代理和反向代理两种: + +- 用户察觉得到正向代理的存在。

+- 而反向代理一般位于内部网络中,用户察觉不到。 +

### 2. 网关 @@ -639,7 +646,7 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名, ### 3. 隧道 -使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。 +使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。 # 六、HTTPs @@ -649,7 +656,7 @@ HTTP 有以下安全性问题: - 不验证通信方的身份,通信方的身份有可能遭遇伪装; - 无法证明报文的完整性,报文有可能遭篡改。 -HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。 +HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPs 使用了隧道进行通信。 通过使用 SSL,HTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 @@ -681,7 +688,7 @@ HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer) ### 3. HTTPs 采用的加密方式 -HTTPs 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证安全性,之后使用对称密钥加密进行通信来保证效率。(下图中的 Session Key 就是对称密钥) +HTTPs 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥)

@@ -710,221 +717,53 @@ HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认 ## HTTPs 的缺点 - 因为需要进行加密解密等过程,因此速度会更慢; -- 需要支付证书授权的高费用。 +- 需要支付证书授权的高额费用。 ## 配置 HTTPs [Nginx 配置 HTTPS 服务器](https://aotu.io/notes/2016/08/16/nginx-https/index.html) -# 七、Web 攻击技术 +# 七、HTTP/2.0 -## 跨站脚本攻击 +## HTTP/1.x 缺陷 -### 1. 概念 + HTTP/1.x 实现简单是以牺牲性能为代价的: -跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。 +- 客户端需要使用多个连接才能实现并发和缩短延迟; +- 不会压缩请求和响应首部,从而导致不必要的网络流量; +- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 -例如有一个论坛网站,攻击者可以在上面发布以下内容: +## 二进制分帧层 -```html - -``` +HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 -之后该内容可能会被渲染成以下形式: +

-```html -

-``` +在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 -另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 +- 一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 +- 消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。 +- 帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 -### 2. 危害 +

-- 窃取用户的 Cookie 值 -- 伪造虚假的输入表单骗取个人信息 -- 显示伪造的文章或者图片 +## 服务端推送 -### 3. 防范手段 +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 -**(一)设置 Cookie 为 HttpOnly** +

-设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。 +## 首部压缩 -**(二)过滤特殊字符** +HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。 -例如将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。 +HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。 -**(三)富文本编辑器的处理** +不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 -富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。 +

-富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。 - -以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。 - -[XSS 过滤在线测试](http://jsxss.com/zh/try.html) - -```html -

XSS Demo

- -

-Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist. -

- -
- - -
- -
hello
- -

- http -

- -

Features:

- - - -``` - -```html -

XSS Demo

- -

-Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist. -

- -<form> - <input type="text" name="q" value="test"> - <button id="submit">Submit</button> -</form> - -
hello
- -

- http -

- -

Features:

- - -<script type="text/javascript"> -alert(/xss/); -</script> -``` - -## 跨站请求伪造 - -### 1. 概念 - -跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。 - -XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。 - -假如一家银行用以执行转账操作的 URL 地址如下: - -``` -http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。 -``` - -那么,一个恶意攻击者可以在另一个网站上放置如下代码: - -``` -。 -``` - -如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 - -这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 - -透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 - -### 2. 防范手段 - -**(一)检查 Referer 首部字段** - -Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 XSRF 攻击。 - -这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 - -**(二)添加校验 Token** - -在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。 - -**(三)输入验证码** - -因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。 - -也可以要求用户输入验证码来进行校验。 - -## SQL 注入攻击 - -### 1. 概念 - -服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。 - -### 2. 攻击原理 - -例如一个网站登录验证的 SQL 查询代码为: - -```sql -strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');" -``` - -如果填入以下内容: - -```sql -userName = "1' OR '1'='1"; -passWord = "1' OR '1'='1"; -``` - -那么 SQL 查询字符串为: - -```sql -strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');" -``` - -此时无需验证通过就能执行以下查询: - -```sql -strSQL = "SELECT * FROM users;" -``` - -### 3. 防范手段 - -**(一)使用参数化查询** - -以下以 Java 中的 PreparedStatement 为例,它是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。 - -```java -PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?"); -stmt.setString(1, userid); -stmt.setString(2, password); -ResultSet rs = stmt.executeQuery(); -``` - -**(二)单引号转换** - -将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。 - -## 拒绝服务攻击 - -拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 - -分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 - -> [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A) - -# 八、GET 和 POST 的区别 +# 八、GET 和 POST 比较 ## 作用 @@ -932,7 +771,9 @@ GET 用于获取资源,而 POST 用于传输实体主体。 ## 参数 -GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。 +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 + +因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。 ``` GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 @@ -944,10 +785,6 @@ Host: w3schools.com name1=value1&name2=value2 ``` -不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 - -因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。 - ## 安全 安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 @@ -960,9 +797,13 @@ GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实 ## 幂等性 -幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。 +幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 -GET /pageX HTTP/1.1 是幂等的。连续调用多次,客户端接收到的结果都是一样的: +所有的安全方法也都是幂等的。 + +在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 + +GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的: ``` GET /pageX HTTP/1.1 @@ -971,7 +812,7 @@ GET /pageX HTTP/1.1 GET /pageX HTTP/1.1 ``` -POST /add_row HTTP/1.1 不是幂等的。如果调用多次,就会增加多行记录: +POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录: ``` POST /add_row HTTP/1.1 -> Adds a 1nd row @@ -1001,7 +842,8 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 > XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 -在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。而 GET 方法 Header 和 Data 会一起发送。 +- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 +- 而 GET 方法 Header 和 Data 会一起发送。 # 九、HTTP/1.0 与 HTTP/1.1 的区别 @@ -1021,37 +863,6 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 - HTTP/1.1 新增缓存处理指令 max-age -# 十、HTTP/2.0 - -## HTTP/1.x 缺陷 - - HTTP/1.x 实现简单是以牺牲应用性能为代价的: - -- 客户端需要使用多个连接才能实现并发和缩短延迟; -- 不会压缩请求和响应首部,从而导致不必要的网络流量; -- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 - -## 二进制分帧层 - -HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 - -

- -在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 - -

- -## 服务端推送 - -HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 - -

- -## 首部压缩 - -HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 - -

# 参考资料 @@ -1059,6 +870,7 @@ HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 - [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) - [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) - [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php) +- [Difference between file URI and URL in java](http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java) - [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement) - [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html) - [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/) @@ -1075,10 +887,6 @@ HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 - [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827) - [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html) - [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/) -- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC) -- [维基百科: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) - [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn) - [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) - [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/) diff --git a/notes/Java IO.md b/notes/Java IO.md index 08dc80f4..1e3aedcd 100644 --- a/notes/Java IO.md +++ b/notes/Java IO.md @@ -40,8 +40,7 @@ File 类可以用于表示文件和目录的信息,但是它不表示文件的 递归地输出一个目录下所有文件: ```java -public static void listAllFiles(File dir) -{ +public static void listAllFiles(File dir) { if (dir == null || !dir.exists()) { return; } @@ -60,17 +59,19 @@ public static void listAllFiles(File dir) 使用字节流操作进行文件复制: ```java -public static void copyFile(String src, String dist) throws IOException -{ +public static void copyFile(String src, String dist) throws IOException { + FileInputStream in = new FileInputStream(src); FileOutputStream out = new FileOutputStream(dist); byte[] buffer = new byte[20 * 1024]; + // read() 最多读取 buffer.length 个字节 // 返回的是实际读取的个数 // 返回 -1 的时候表示读到 eof,即文件尾 while (in.read(buffer, 0, buffer.length) != -1) { out.write(buffer); } + in.close(); out.close(); } @@ -78,7 +79,11 @@ public static void copyFile(String src, String dist) throws IOException

-Java I/O 使用了装饰者模式来实现。以 InputStream 为例,InputStream 是抽象组件,FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 +Java I/O 使用了装饰者模式来实现。以 InputStream 为例, + +- InputStream 是抽象组件; +- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; +- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 @@ -99,8 +104,7 @@ DataInputStream 装饰者提供了对更多数据类型进行输入的操作, 逐行输出文本文件的内容: ```java -public static void readFileContent(String filePath) throws IOException -{ +public static void readFileContent(String filePath) throws IOException { FileReader fileReader = new FileReader(filePath); BufferedReader bufferedReader = new BufferedReader(fileReader); String line; @@ -126,7 +130,7 @@ UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF- Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 -String 可以看成一个字符序列,可以指定一个编码方式将它转换为字节序列,也可以指定一个编码方式将一个字节序列转换为 String。 +String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。 ```java String str1 = "中文"; @@ -151,8 +155,7 @@ byte[] bytes = str1.getBytes(); 序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。 ```java -public static void main(String[] args) throws IOException, ClassNotFoundException -{ +public static void main(String[] args) throws IOException, ClassNotFoundException { A a1 = new A(123, "abc"); String objectFile = "file/a1"; ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile)); @@ -165,20 +168,17 @@ public static void main(String[] args) throws IOException, ClassNotFoundExceptio System.out.println(a2); } -private static class A implements Serializable -{ +private static class A implements Serializable { private int x; private String y; - A(int x, String y) - { + A(int x, String y) { this.x = x; this.y = y; } @Override - public String toString() - { + public String toString() { return "x = " + x + " " + "y = " + y; } } @@ -188,7 +188,7 @@ private static class A implements Serializable transient 关键字可以使一些属性不会被序列化。 -**ArrayList 序列化和反序列化的实现** :ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 +ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 ```java private transient Object[] elementData; @@ -205,7 +205,7 @@ Java 中的网络支持: ## InetAddress -没有公有构造函数,只能通过静态方法来创建实例。 +没有公有的构造函数,只能通过静态方法来创建实例。 ```java InetAddress.getByName(String host); @@ -217,19 +217,24 @@ InetAddress.getByAddress(byte[] address); 可以直接从 URL 中读取字节流数据。 ```java -public static void main(String[] args) throws IOException -{ +public static void main(String[] args) throws IOException { + URL url = new URL("http://www.baidu.com"); - // 字节流 + + /* 字节流 */ InputStream is = url.openStream(); - // 字符流 + + /* 字符流 */ InputStreamReader isr = new InputStreamReader(is, "utf-8"); + + /* 提供缓存功能 */ BufferedReader br = new BufferedReader(isr); - String line = br.readLine(); - while (line != null) { + + String line; + while ((line = br.readLine()) != null) { System.out.println(line); - line = br.readLine(); } + br.close(); } ``` @@ -253,7 +258,7 @@ public static void main(String[] args) throws IOException - [Java NIO 浅析](https://tech.meituan.com/nio.html) - [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html) -新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 +新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 ## 流与块 @@ -271,7 +276,7 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 -通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 +通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 通道包括以下类型: @@ -329,21 +334,41 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 以下展示了使用 NIO 快速复制文件的实例: ```java -public static void fastCopy(String src, String dist) throws IOException -{ - FileInputStream fin = new FileInputStream(src); /* 获取源文件的输入字节流 */ - FileChannel fcin = fin.getChannel(); /* 获取输入字节流的文件通道 */ - FileOutputStream fout = new FileOutputStream(dist); /* 获取目标文件的输出字节流 */ - FileChannel fcout = fout.getChannel(); /* 获取输出字节流的通道 */ - ByteBuffer buffer = ByteBuffer.allocateDirect(1024); /* 为缓冲区分配 1024 个字节 */ +public static void fastCopy(String src, String dist) throws IOException { + + /* 获得源文件的输入字节流 */ + FileInputStream fin = new FileInputStream(src); + + /* 获取输入字节流的文件通道 */ + FileChannel fcin = fin.getChannel(); + + /* 获取目标文件的输出字节流 */ + FileOutputStream fout = new FileOutputStream(dist); + + /* 获取输出字节流的通道 */ + FileChannel fcout = fout.getChannel(); + + /* 为缓冲区分配 1024 个字节 */ + ByteBuffer buffer = ByteBuffer.allocateDirect(1024); + while (true) { - int r = fcin.read(buffer); /* 从输入通道中读取数据到缓冲区中 */ - if (r == -1) { /* read() 返回 -1 表示 EOF */ + + /* 从输入通道中读取数据到缓冲区中 */ + int r = fcin.read(buffer); + + /* read() 返回 -1 表示 EOF */ + if (r == -1) { break; } - buffer.flip(); /* 切换读写 */ - fcout.write(buffer); /* 把缓冲区的内容写入输出文件中 */ - buffer.clear(); /* 清空缓冲区 */ + + /* 切换读写 */ + buffer.flip(); + + /* 把缓冲区的内容写入输出文件中 */ + fcout.write(buffer); + + /* 清空缓冲区 */ + buffer.clear(); } } ``` @@ -448,10 +473,10 @@ while (true) { ## 套接字 NIO 实例 ```java -public class NIOServer -{ - public static void main(String[] args) throws IOException - { +public class NIOServer { + + public static void main(String[] args) throws IOException { + Selector selector = Selector.open(); ServerSocketChannel ssChannel = ServerSocketChannel.open(); @@ -463,32 +488,45 @@ public class NIOServer serverSocket.bind(address); while (true) { + selector.select(); Set keys = selector.selectedKeys(); Iterator keyIterator = keys.iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isAcceptable()) { + ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); + // 服务器会为每个新连接创建一个 SocketChannel SocketChannel sChannel = ssChannel1.accept(); sChannel.configureBlocking(false); + // 这个新连接主要用于从客户端读取数据 sChannel.register(selector, SelectionKey.OP_READ); + } else if (key.isReadable()) { + SocketChannel sChannel = (SocketChannel) key.channel(); System.out.println(readDataFromSocketChannel(sChannel)); sChannel.close(); } + keyIterator.remove(); } } } private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1024); StringBuilder data = new StringBuilder(); + while (true) { + buffer.clear(); int n = sChannel.read(buffer); if (n == -1) { @@ -509,10 +547,9 @@ public class NIOServer ``` ```java -public class NIOClient -{ - public static void main(String[] args) throws IOException - { +public class NIOClient { + + public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 8888); OutputStream out = socket.getOutputStream(); String s = "hello world"; @@ -526,9 +563,9 @@ public class NIOClient 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 -向内存映射文件写入可能是危险的,仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 +向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 -下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 +下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 ```java MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); diff --git a/notes/Java 基础.md b/notes/Java 基础.md index af1c6530..bcb94acc 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -4,7 +4,7 @@ * [缓存池](#缓存池) * [二、String](#二string) * [概览](#概览) - * [String 不可变的好处](#string-不可变的好处) + * [不可变的好处](#不可变的好处) * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) * [String.intern()](#stringintern) * [三、运算](#三运算) @@ -153,7 +153,7 @@ public final class String private final char value[]; ``` -## String 不可变的好处 +## 不可变的好处 **1. 可以缓存 hash 值** @@ -212,7 +212,7 @@ String s5 = "bbb"; System.out.println(s4 == s5); // true ``` -在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 +在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被移到 Native Method 中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 - [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) - [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) @@ -223,7 +223,7 @@ System.out.println(s4 == s5); // true Java 的参数是以值传递的形式传入方法中,而不是引用传递。 -Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。 +以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。 ```java public class Dog { @@ -549,7 +549,7 @@ SuperExtendExample.func() ## 重写与重载 -- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法; +- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法,子类的返回值类型要等于或者小于父类的返回值; - 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 @@ -583,19 +583,7 @@ protected void finalize() throws Throwable {} ## equals() -**1. equals() 与 == 的区别** - -- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 -- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价。 - -```java -Integer x = new Integer(1); -Integer y = new Integer(1); -System.out.println(x.equals(y)); // true -System.out.println(x == y); // false -``` - -**2. 等价关系** +**1. 等价关系** (一)自反性 @@ -632,6 +620,18 @@ x.equals(y) == x.equals(y); // true x.equals(null); // false; ``` +**2. equals() 与 ==** + +- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 +- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价,根据引用对象 equals() 方法的具体实现来进行比较。 + +```java +Integer x = new Integer(1); +Integer y = new Integer(1); +System.out.println(x.equals(y)); // true +System.out.println(x == y); // false +``` + **3. 实现** - 检查是否为同一个对象的引用,如果是直接返回 true; @@ -763,10 +763,10 @@ try { ``` ```html -java.lang.CloneNotSupportedException: CloneTest +java.lang.CloneNotSupportedException: CloneExample ``` -以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。 +以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。 ```java public class CloneExample implements Cloneable { @@ -1005,7 +1005,7 @@ public class A { **4. 静态内部类** -非静态内部类依赖于需要外部类的实例,而静态内部类不需要。 +非静态内部类依赖于外部类的实例,而静态内部类不需要。 ```java public class OuterClass { @@ -1177,7 +1177,6 @@ Java 注解是附加在代码中的一些元信息,用于一些工具在编译 - Java 支持自动垃圾回收,而 C++ 需要手动回收。 - Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。 - Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。 -- Java 内置了线程的支持,而 C++ 需要依靠第三方库。 - Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。 - Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。 diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 9e6e2ac3..bd4ecfdb 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -8,9 +8,13 @@ * [三、源码分析](#三源码分析) * [ArrayList](#arraylist) * [Vector](#vector) + * [CopyOnWriteArrayList](#copyonwritearraylist) * [LinkedList](#linkedlist) * [HashMap](#hashmap) * [ConcurrentHashMap](#concurrenthashmap) + * [LinkedHashMap](#linkedhashmap) + * [WeekHashMap](#weekhashmap) +* [附录](#附录) * [参考资料](#参考资料) @@ -21,39 +25,39 @@ ## Collection -

+

### 1. Set -- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的; +- HashSet:基于哈希表实现,支持快速查找。但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 -- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN); +- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN)。 -- LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。 +- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 ### 2. List -- ArrayList:基于动态数组实现,支持随机访问; +- ArrayList:基于动态数组实现,支持随机访问。 -- Vector:和 ArrayList 类似,但它是线程安全的; +- Vector:和 ArrayList 类似,但它是线程安全的。 - LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 ### 3. Queue -- LinkedList:可以用它来支持双向队列; +- LinkedList:可以用它来实现双向队列。 - PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 ## Map -

+

-- HashMap:基于哈希实现; +- HashMap:基于哈希表实现; - HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 -- LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 +- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 - TreeMap:基于红黑树实现。 @@ -61,7 +65,7 @@ ## 迭代器模式 -

+

Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 @@ -85,14 +89,14 @@ java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 public static List asList(T... a) ``` -如果要将数组类型转换为 List 类型,应该注意的是 asList() 的参数为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 +应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 ```java Integer[] arr = {1, 2, 3}; List list = Arrays.asList(arr); ``` -也可以使用以下方式生成 List。 +也可以使用以下方式调用 asList(): ```java List list = Arrays.asList(1,2,3); @@ -108,7 +112,7 @@ List list = Arrays.asList(1,2,3); ### 1. 概览 -实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。 +实现了 RandomAccess 接口,因此支持随机访问。这是理所当然的,因为 ArrayList 是基于数组实现的。 ```java public class ArrayList extends AbstractList @@ -123,7 +127,9 @@ private static final int DEFAULT_CAPACITY = 10; ### 2. 序列化 -基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 +ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。 + +保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 ```java transient Object[] elementData; // non-private to simplify nested class access @@ -171,7 +177,7 @@ private void grow(int minCapacity) { ### 4. 删除元素 -需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上。 +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。 ```java public E remove(int index) { @@ -235,12 +241,12 @@ public synchronized E get(int index) { } ``` -### 2. ArrayList 与 Vector +### 2. 与 ArrayList 的区别 - Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制; - Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。 -### 3. Vector 替代方案 +### 3. 替代方案 为了获得线程安全的 ArrayList,可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。 @@ -255,7 +261,15 @@ List synList = Collections.synchronizedList(list); List list = new CopyOnWriteArrayList<>(); ``` -CopyOnWriteArrayList 是一种 CopyOnWrite 容器,从以下源码看出:读取元素是从原数组读取;添加元素是在复制的新数组上。读写分离,因而可以在并发条件下进行不加锁的读取,读取效率高,适用于读操作远大于写操作的场景。 +## CopyOnWriteArrayList + +### 读写分离 + +写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。 + +写操作需要加锁,防止同时并发写入时导致的写入数据丢失。 + +写操作结束之后需要把原始数组指向新的复制数组。 ```java public boolean add(E e) { @@ -264,7 +278,7 @@ public boolean add(E e) { try { Object[] elements = getArray(); int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1); + Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; @@ -276,18 +290,31 @@ public boolean add(E e) { final void setArray(Object[] a) { array = a; } +``` +```java @SuppressWarnings("unchecked") private E get(Object[] a, int index) { return (E) a[index]; } ``` +### 适用场景 + +CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。 + +但是 CopyOnWriteArrayList 有其缺陷: + +- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右; +- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。 + +所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。 + ## LinkedList ### 1. 概览 -基于双向链表实现,内部使用 Node 来存储链表节点信息。 +基于双向链表实现,使用 Node 存储链表节点信息。 ```java private static class Node { @@ -297,14 +324,14 @@ private static class Node { } ``` -每个链表存储了 Head 和 Tail 指针: +每个链表存储了 first 和 last 指针: ```java transient Node first; transient Node last; ``` -

+

### 2. ArrayList 与 LinkedList @@ -450,7 +477,7 @@ public V put(K key, V value) { } ``` -HashMap 允许插入键为 null 的键值对。因为无法调用 null 的 hashCode(),也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 +HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 ```java private V putForNullKey(V value) { @@ -577,10 +604,10 @@ static int indexFor(int h, int length) { | 参数 | 含义 | | :--: | :-- | -| capacity | table 的容量大小,默认为 16,需要注意的是 capacity 必须保证为 2 的 n 次方。| +| capacity | table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。| | size | table 的实际使用量。 | | threshold | size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。 | -| load_factor | 装载因子,table 能够使用的比例,threshold = capacity * load_factor。| +| loadFactor | 装载因子,table 能够使用的比例,threshold = capacity * loadFactor。| ```java static final int DEFAULT_INITIAL_CAPACITY = 16; @@ -650,14 +677,14 @@ void transfer(Entry[] newTable) { 在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。 -假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16: +假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32: ```html capacity : 00010000 new capacity : 00100000 ``` -对于一个 Key,它的哈希值如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。 +对于一个 Key,它的哈希值如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 +16。 ### 7. 扩容-计算数组容量 @@ -825,15 +852,287 @@ public int size() { } ``` - ### 3. JDK 1.8 的改动 -JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发程度与 Segment 数量相等。 +JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。 JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。 并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 +## LinkedHashMap + +### 存储结构 + +继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。 + +```java +public class LinkedHashMap extends HashMap implements Map +``` + +内存维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。 + +```java +/** + * The head (eldest) of the doubly linked list. + */ +transient LinkedHashMap.Entry head; + +/** + * The tail (youngest) of the doubly linked list. + */ +transient LinkedHashMap.Entry tail; +``` + +accessOrder 决定了顺序,默认为 false,此时使用的是插入顺序。 + +```java +final boolean accessOrder; +``` + +LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。 + +```java +void afterNodeAccess(Node p) { } +void afterNodeInsertion(boolean evict) { } +``` + +### afterNodeAccess() + +当一个节点被访问时,如果 accessOrder 为 true,则会将 该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。 + +```java +void afterNodeAccess(Node e) { // move node to last + LinkedHashMap.Entry last; + if (accessOrder && (last = tail) != e) { + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + p.after = null; + if (b == null) + head = a; + else + b.after = a; + if (a != null) + a.before = b; + else + last = b; + if (last == null) + head = p; + else { + p.before = last; + last.after = p; + } + tail = p; + ++modCount; + } +} +``` + +### afterNodeInsertion() + +在 put 等操作之后执行,当 removeEldestEntry() 方法返回 ture 时会移除最晚的节点,也就是链表首部节点 first。 + +evict 只有在构建 Map 的时候才为 false,在这里为 true。 + +```java +void afterNodeInsertion(boolean evict) { // possibly remove eldest + LinkedHashMap.Entry first; + if (evict && (first = head) != null && removeEldestEntry(first)) { + K key = first.key; + removeNode(hash(key), key, null, false, true); + } +} +``` + +removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。 + +```java +protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } +``` + +### LRU 缓存 + +以下是使用 LinkedHashMap 实现的一个 LRU 缓存: + +- 设定最大缓存空间 MAX_ENTRIES 为 3; +- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LUR 顺序; +- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。 + +```java +class LRUCache extends LinkedHashMap { + private static final int MAX_ENTRIES = 3; + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_ENTRIES; + } + + LRUCache() { + super(MAX_ENTRIES, 0.75f, true); + } +} +``` + +```java +public static void main(String[] args) { + LRUCache cache = new LRUCache<>(); + cache.put(1, "a"); + cache.put(2, "b"); + cache.put(3, "c"); + cache.get(1); + cache.put(4, "d"); + System.out.println(cache.keySet()); +} +``` + +```html +[3, 1, 4] +``` + +## WeekHashMap + +### 存储结构 + +WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。 + +WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。 + +```java +private static class Entry extends WeakReference implements Map.Entry +``` + +### ConcurrentCache + +Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。 + +ConcurrentCache 采取的是分代缓存: + +- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园); +- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。 +- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,保证频繁被访问的节点不容易被回收。 +- 当调用 put() 方法时,如果缓存当前容量大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。 + +```java +public final class ConcurrentCache { + + private final int size; + + private final Map eden; + + private final Map longterm; + + public ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K k) { + V v = this.eden.get(k); + if (v == null) { + v = this.longterm.get(k); + if (v != null) + this.eden.put(k, v); + } + return v; + } + + public void put(K k, V v) { + if (this.eden.size() >= size) { + this.longterm.putAll(this.eden); + this.eden.clear(); + } + this.eden.put(k, v); + } +} +``` + +# 附录 + +Collection 绘图源码: + +``` +@startuml + +interface Collection +interface Set +interface List +interface Queue +interface SortSet + +class HashSet +class LinkedHashSet +class TreeSet +class ArrayList +class Vector +class LinkedList +class PriorityQueue + + +Collection <|-- Set +Collection <|-- List +Collection <|-- Queue +Set <|-- SortSet + +Set <|.. HashSet +Set <|.. LinkedHashSet +SortSet <|.. TreeSet +List <|.. ArrayList +List <|.. Vector +List <|.. LinkedList +Queue <|.. LinkedList +Queue <|.. PriorityQueue + +@enduml +``` + +Map 绘图源码 + +``` +@startuml + +interface Map +interface SortMap + +class HashTable +class LinkedHashMap +class HashMap +class TreeMap + +Map <|.. HashTable +Map <|.. LinkedHashMap +Map <|.. HashMap +Map <|-- SortMap +SortMap <|.. TreeMap + +@enduml +``` + +迭代器类图 + +``` +@startuml + +interface Iterable +interface Collection +interface List +interface Set +interface Queue +interface Iterator +interface ListIterator + +Iterable <|-- Collection +Collection <|.. List +Collection <|.. Set +Collection <|-- Queue +Iterator <-- Iterable +Iterator <|.. ListIterator +ListIterator <-- List + +@enduml +``` + # 参考资料 - Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002. diff --git a/notes/Java 并发.md b/notes/Java 并发.md index 9c55c715..89ab7cac 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -23,7 +23,8 @@ * [五、互斥同步](#五互斥同步) * [synchronized](#synchronized) * [ReentrantLock](#reentrantlock) - * [synchronized 和 ReentrantLock 比较](#synchronized-和-reentrantlock-比较) + * [比较](#比较) + * [使用选择](#使用选择) * [六、线程之间的协作](#六线程之间的协作) * [join()](#join) * [wait() notify() notifyAll()](#wait-notify-notifyall) @@ -498,6 +499,8 @@ public synchronized static void fun() { ## ReentrantLock +ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。 + ```java public class LockExample { @@ -529,23 +532,8 @@ public static void main(String[] args) { 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 ``` -ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能: -**1. 等待可中断** - -当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。 - -**2. 可实现公平锁** - -公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。 - -synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。 - -**3. 锁绑定多个条件** - -一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。 - -## synchronized 和 ReentrantLock 比较 +## 比较 **1. 锁的实现** @@ -553,13 +541,25 @@ synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 **2. 性能** -新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。 +新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。 -**3. 功能** +**3. 等待可中断** -ReentrantLock 多了一些高级功能。 +当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。 -**4. 使用选择** +ReentrantLock 可中断,而 synchronized 不行。 + +**4. 公平锁** + +公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。 + +synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。 + +**5. 锁绑定多个条件** + +一个 ReentrantLock 可以同时绑定多个 Condition 对象。 + +## 使用选择 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。 @@ -952,7 +952,7 @@ produce..produce..consume..consume..produce..consume..produce..consume..produce. ```java public class ForkJoinExample extends RecursiveTask { - private final int threhold = 5; + private final int threshold = 5; private int first; private int last; @@ -964,7 +964,7 @@ public class ForkJoinExample extends RecursiveTask { @Override protected Integer compute() { int result = 0; - if (last - first <= threhold) { + if (last - first <= threshold) { // 任务足够小则直接计算 for (int i = first; i <= last; i++) { result += i; @@ -1134,7 +1134,7 @@ public static void main(String[] args) throws InterruptedException { 1000 ``` -除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的完整性,它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 +除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 ```java public class AtomicSynchronizedExample { @@ -1176,9 +1176,13 @@ public static void main(String[] args) throws InterruptedException { 可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 -volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 +主要有有三种实现可见性的方式: -对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。 +- volatile +- synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。 +- final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 + +对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。 ### 3. 有序性 @@ -1232,7 +1236,7 @@ Thread 对象的 start() 方法调用先行发生于此线程的每一个动作 > Thread Join Rule -join() 方法返回先行发生于 Thread 对象的结束。 +Thread 对象的结束先行发生于 join() 方法返回。

@@ -1240,7 +1244,7 @@ join() 方法返回先行发生于 Thread 对象的结束。 > Thread Interruption Rule -对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。 +对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。 ### 7. 对象终结规则 @@ -1385,22 +1389,21 @@ synchronized 和 ReentrantLock。 ### 2. 非阻塞同步 -互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。 +互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。 -从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 +互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 -随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。 +随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。 -乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。 +乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。 -CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示)。CAS 指令执行时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则它就不执行更新。但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作。 +硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。 J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。 -在下面的代码 1 中,使用了 AtomicInteger 执行了自增的操作。代码 2 是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。代码 3 是 getAndAddInt() 源码,var1 指示内存位置,var2 指示新值,var4 指示操作需要加的数值,这里为 1。在代码 3 的实现中,通过 getIntVolatile(var1, var2) 得到旧的预期值。通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2=var5,那么就更新内存地址为 var1 的变量为 var5+var4。可以看到代码 3 是在一个循环中进行,发生冲突的做法是不断的进行重试。 +以下代码使用了 AtomicInteger 执行了自增的操作。 ```java -// 代码 1 private AtomicInteger cnt = new AtomicInteger(); public void add() { @@ -1408,15 +1411,19 @@ public void add() { } ``` +以下代码是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。 + ```java -// 代码 2 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } ``` +以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值 ==var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。 + +可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。 + ```java -// 代码 3 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { @@ -1427,7 +1434,9 @@ public final int getAndAddInt(Object var1, long var2, int var4) { } ``` -ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 +ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。 + +J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 ### 3. 无同步方案 @@ -1435,9 +1444,9 @@ ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 **(一)可重入代码(Reentrant Code)** -这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。 +这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。 -可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。 +可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。 **(二)栈封闭** @@ -1659,9 +1668,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态: - 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。 -- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。 +- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂的控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。 -- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashtable。 +- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。 - 使用本地变量和不可变类来保证线程安全。 diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md index 1d2b4f14..136ca5ad 100644 --- a/notes/Java 虚拟机.md +++ b/notes/Java 虚拟机.md @@ -1,7 +1,7 @@ * [一、运行时数据区域](#一运行时数据区域) * [程序计数器](#程序计数器) - * [虚拟机栈](#虚拟机栈) + * [Java 虚拟机栈](#java-虚拟机栈) * [本地方法栈](#本地方法栈) * [堆](#堆) * [方法区](#方法区) @@ -23,19 +23,19 @@ # 一、运行时数据区域 -

+

## 程序计数器 记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。 -## 虚拟机栈 +## Java 虚拟机栈 -每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 +每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息,从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -

+

-可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小: +可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小: ```java java -Xss=512M HackTheJava @@ -52,31 +52,28 @@ java -Xss=512M HackTheJava 与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 -

+本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的, 并且被编译为基于本机硬件和操作系统的程序。 + +

## 堆 -所有对象实例都在这里分配内存。 +所有对象实例都在这里分配内存,是垃圾收集的主要区域("GC 堆")。 -是垃圾收集的主要区域("GC 堆")。现代的垃圾收集器基本都是采用分代收集算法,主要思想是针对不同的对象采取不同的垃圾回收算法。虚拟机把 Java 堆分成以下三块: +现代的垃圾收集器基本都是采用分代收集算法,针对不同的对象采取不同的垃圾回收算法,可以将堆分成两块: - 新生代(Young Generation) - 老年代(Old Generation) -- 永久代(Permanent Generation) -当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。 - -新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间: +新生代可以继续划分成以下三个空间: - Eden(伊甸园) - From Survivor(幸存者) - To Survivor -

+堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 -Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 - -可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 +可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 ```java java -Xms=1M -Xmx=2M HackTheJava @@ -86,11 +83,13 @@ java -Xms=1M -Xmx=2M HackTheJava 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 -和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 +和堆一样不需要连续的内存,并且可以动态扩展。 + +动态扩展失败一样会抛出 OutOfMemoryError 异常。 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 -JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,JDK 1.8 之后,取消了永久代,用 metaspace(元数据)区替代。 +JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是从 JDK 1.7 开始,已经把原本放在永久代的字符串常量池移到 Native Method 中。 ## 运行时常量池 @@ -102,11 +101,15 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用 ## 直接内存 -在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 +在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。 + +这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 # 二、垃圾收集 -程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。 +垃圾回收主要是针对堆和方法区进行。 + +程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。 ## 判断一个对象是否存活 @@ -116,6 +119,8 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 +正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。 + ```java public class ReferenceCountingGC { public Object instance = null; @@ -129,8 +134,6 @@ public class ReferenceCountingGC { } ``` -正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。 - ### 2. 可达性分析算法 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。 @@ -152,7 +155,7 @@ Java 具有四种强度不同的引用类型。 **(一)强引用** -被强引用关联的对象不会被垃圾收集器回收。 +被强引用关联的对象不会被回收。 使用 new 一个新对象的方式来创建强引用。 @@ -162,7 +165,7 @@ Object obj = new Object(); **(二)软引用** -被软引用关联的对象,只有在内存不够的情况下才会被回收。 +被软引用关联的对象只有在内存不够的情况下才会被回收。 使用 SoftReference 类来创建软引用。 @@ -174,7 +177,7 @@ obj = null; // 使对象只被软引用关联 **(三)弱引用** -被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。 +被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾收集。 使用 WeakReference 类来实现弱引用。 @@ -184,54 +187,11 @@ WeakReference wf = new WeakReference(obj); obj = null; ``` -WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。 - -```java -private static class Entry extends WeakReference implements Map.Entry -``` - -Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。 - -```java -public final class ConcurrentCache { - - private final int size; - - private final Map eden; - - private final Map longterm; - - public ConcurrentCache(int size) { - this.size = size; - this.eden = new ConcurrentHashMap<>(size); - this.longterm = new WeakHashMap<>(size); - } - - public V get(K k) { - V v = this.eden.get(k); - if (v == null) { - v = this.longterm.get(k); - if (v != null) - this.eden.put(k, v); - } - return v; - } - - public void put(K k, V v) { - if (this.eden.size() >= size) { - this.longterm.putAll(this.eden); - this.eden.clear(); - } - this.eden.put(k, v); - } -} -``` - **(四)虚引用** 又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。 -为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 +为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。 使用 PhantomReference 来实现虚引用。 @@ -243,20 +203,20 @@ obj = null; ### 4. 方法区的回收 -因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。 +因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。 主要是对常量池的回收和对类的卸载。 +在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 + 类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载: -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 +- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。 - 加载该类的 ClassLoader 已经被回收。 -- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 +- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。 -在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 - ### 5. finalize() finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 @@ -290,16 +250,18 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 主要不足是只使用了内存的一半。 -现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。 +现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。 + +HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。 ### 4. 分代收集 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 -一般将 Java 堆分为新生代和老年代。 +一般将堆分为新生代和老年代。 - 新生代使用:复制算法 -- 老年代使用:标记 - 清理 或者 标记 - 整理 算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 ## 垃圾收集器 @@ -307,8 +269,8 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 -- 单线程与并行(多线程):单线程指的是垃圾收集器只使用一个线程进行收集,而并行使用多个线程。 -- 串行与并发:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并发指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 +- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程; +- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 ### 1. Serial 收集器 @@ -334,15 +296,15 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 ### 3. Parallel Scavenge 收集器 -与 ParNew 一样是并行的多线程收集器。 +与 ParNew 一样是多线程收集器。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 -提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 +缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 -还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。 +可以通过一个开关参数打卡 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 。 ### 4. Serial Old 收集器 @@ -367,8 +329,6 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 -特点:并发收集、低停顿。 - 分为以下四个流程: - 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 @@ -388,11 +348,11 @@ CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 -Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老生代,而 G1 可以直接对新生代和永久代一起回收。 +堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

-G1 把堆划分成多个大小相等的独立区域(Region),新生代和永久代不再物理隔离。 +G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

@@ -416,18 +376,6 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html) -### 8. 比较 - -| 收集器 | 单线程/并行 | 串行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 | -| :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| **Serial** | 单线程 | 串行 | 新生代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | -| **Serial Old** | 单线程 | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 | -| **ParNew** | 并行 |串行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | -| **Parallel Scavenge** | 并行 | 串行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **Parallel Old** | 并行 | 串行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **CMS** | 并行 | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | -| **G1** | 并行 | 并发 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS | - ## 内存分配与回收策略 ### 1. Minor GC 和 Full GC @@ -457,15 +405,17 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 (四)动态对象年龄判定 -虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 +虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 (五)空间分配担保 -在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。 +在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。 + +如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。 ### 3. Full GC 的触发条件 -对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: +对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: (一)调用 System.gc() @@ -483,11 +433,15 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 (四)JDK 1.7 及以前的永久代空间不足 -在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 +在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 + +当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。 + +为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 (五)Concurrent Mode Failure -执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是指 CMS GC 当前的浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 +执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 # 三、类加载机制 @@ -664,7 +618,7 @@ public static void main(String[] args) { 从 Java 开发人员的角度看,类加载器可以划分得更细致一些: -- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 +- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 - 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 @@ -788,7 +742,9 @@ public class FileSystemClassLoader extends ClassLoader { # 参考资料 - 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011. +- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4) - [Jvm memory](https://www.slideshare.net/benewu/jvm-memory) +- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/) - [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/) - [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/) - [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271) diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md index 293f4328..44f3d9dc 100644 --- a/notes/Leetcode 题解.md +++ b/notes/Leetcode 题解.md @@ -1,18 +1,18 @@ * [算法思想](#算法思想) - * [贪心思想](#贪心思想) * [双指针](#双指针) * [排序](#排序) * [快速选择](#快速选择) * [堆排序](#堆排序) * [桶排序](#桶排序) * [荷兰国旗问题](#荷兰国旗问题) + * [贪心思想](#贪心思想) * [二分查找](#二分查找) + * [分治](#分治) * [搜索](#搜索) * [BFS](#bfs) * [DFS](#dfs) * [Backtracking](#backtracking) - * [分治](#分治) * [动态规划](#动态规划) * [斐波那契数列](#斐波那契数列) * [矩阵路径](#矩阵路径) @@ -33,10 +33,6 @@ * [多数投票问题](#多数投票问题) * [其它](#其它) * [数据结构相关](#数据结构相关) - * [栈和队列](#栈和队列) - * [哈希表](#哈希表) - * [字符串](#字符串) - * [数组与矩阵](#数组与矩阵) * [链表](#链表) * [树](#树) * [递归](#递归) @@ -44,6 +40,10 @@ * [前中后序遍历](#前中后序遍历) * [BST](#bst) * [Trie](#trie) + * [栈和队列](#栈和队列) + * [哈希表](#哈希表) + * [字符串](#字符串) + * [数组与矩阵](#数组与矩阵) * [图](#图) * [二分图](#二分图) * [拓扑排序](#拓扑排序) @@ -55,9 +55,449 @@ # 算法思想 +## 双指针 + +双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 + +**有序数组的 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 +``` + +题目描述:判断一个数是否为两个数的平方和,例如 5 = 12 + 22。 + +```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/) + +**排序** :时间复杂度 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) { + topK.addAll(buckets[i]); + } + } + 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; +} +``` + ## 贪心思想 -贪心思想保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 +保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 **分配饼干** @@ -74,7 +514,7 @@ You need to output 2. 题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 -因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因此贪心策略 +给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。 证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。 @@ -362,434 +802,6 @@ public int maxProfit(int[] prices) { } ``` -## 双指针 - -双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 - -**有序数组的 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 -``` - -题目描述:判断一个数是否为两个数的平方和,例如 5 = 12 + 22。 - -```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** 问题,可以在 O(N) 时间复杂度,O(1) 空间复杂度完成求解工作。 - -与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 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/) - -**排序** :时间复杂度 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) { - topK.addAll(buckets[i]); - } - } - 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; -} -``` - ## 二分查找 **正常实现** @@ -1065,6 +1077,53 @@ private int binarySearch(int[] nums, int target) { } ``` +## 分治 + +**给表达式加括号** + +[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; +} +``` + ## 搜索 深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 @@ -2312,53 +2371,6 @@ private void backtracking(int row) { } ``` -## 分治 - -**给表达式加括号** - -[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; -} -``` - ## 动态规划 递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 @@ -2371,7 +2383,9 @@ public List diffWaysToCompute(String input) { 题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 -定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 +定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。 + +第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。

@@ -2400,7 +2414,9 @@ public int climbStairs(int n) { 题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。 -定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 或者 i - 3 的住户,所以 +定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。 + +由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 或者 i - 3 的住户,所以

@@ -4032,973 +4048,6 @@ public int maximumProduct(int[] nums) { # 数据结构相关 -## 栈和队列 - -**用栈实现队列** - -[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 在队首,第一个出队列,实现了后进先出顺序。 - -```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] -``` - -在遍历数组时用 Stack 把数组中的数存起来,如果当前遍历的数比栈顶元素来的大,说明栈顶元素的下一个比它大的数就是当前元素。 - -```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 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中[Leetcdoe : 535. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 - -HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。 - -**数组中的两个数和为给定值** - -[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; -} -``` - -## 字符串 - -**两个字符串包含的字符是否完全相同** - -[242. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/) - -```html -s = "anagram", t = "nagaram", return true. -s = "rat", t = "car", return false. -``` - -字符串只包含小写字符,总共有 26 个小写字符。可以用 HashMap 来映射字符与出现次数。因为键的范围很小,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,然后比较两个字符串出现的字符数量是否相同。 - -```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; -} -``` - -**字符串循环移位包含** - -[编程之美: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" -``` - -将每个单词逆序,然后将整个字符串逆序。 - -## 数组与矩阵 - -**把数组中的 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; -} -``` - -**一个数组元素在 [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) 空间复杂度来求解。 - -主要思想是通过交换数组元素,使得数组上的元素在正确的位置上。遍历数组,如果第 i 位上的元素不是 i + 1,那么一直交换第 i 位和 nums[i] - 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; -} -``` - -**有序矩阵查找** - -[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; - } -} -``` - -**数组相邻差值的个数** - -[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; -} -``` - ## 链表 链表是空节点,或者有一个值和一个指向下一个链表的指针,因此很多链表问题可以用递归来处理。 @@ -5032,7 +4081,10 @@ public ListNode getIntersectionNode(ListNode headA, ListNode headB) { } ``` -如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美:3.6]() 的问题。有两种解法:把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环;或者直接比较两个链表的最后一个节点是否相同。 +如果只是判断是否存在交点,那么就是另一个问题,即 [编程之美 3.6]() 的问题。有两种解法: + +- 把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环; +- 或者直接比较两个链表的最后一个节点是否相同。 **链表反转** @@ -5097,7 +4149,7 @@ Given 1->1->2->3->3, return 1->2->3. ```java public ListNode deleteDuplicates(ListNode head) { - if(head == null || head.next == null) return head; + if (head == null || head.next == null) return head; head.next = deleteDuplicates(head.next); return head.val == head.next.val ? head.next : head; } @@ -5419,8 +4471,8 @@ Input: 3 2 1 3 / \ \ 5 4 7 + Output: -Merged tree: 3 / \ 4 5 @@ -5446,6 +4498,7 @@ public TreeNode mergeTrees(TreeNode t1, TreeNode t2) { ```html Given the below binary tree and sum = 22, + 5 / \ 4 8 @@ -5453,10 +4506,11 @@ Given the below binary tree and sum = 22, 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 的所有节点的和 +路径和定义为从 root 到 leaf 的所有节点的和。 ```java public boolean hasPathSum(TreeNode root, int sum) { @@ -5848,7 +4902,7 @@ public List preorderTraversal(TreeNode root) { [145. Binary Tree Postorder Traversal (Medium)](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) -前序遍历为 root -> left -> right,后序遍历为 left -> right -> root,可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。 +前序遍历为 root -> left -> right,后序遍历为 left -> right -> root。可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。 ```java public List postorderTraversal(TreeNode root) { @@ -6106,7 +5160,7 @@ 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; + ListNode mid = preMid.next; preMid.next = null; // 断开链表 TreeNode t = new TreeNode(mid.val); t.left = sortedListToBST(head); @@ -6222,7 +5276,7 @@ private void inOrder(TreeNode node) { return [2]. ``` -答案可能不止一个,也就是有多个值出现的次数一样多,并且是最大的。 +答案可能不止一个,也就是有多个值出现的次数一样多。 ```java private int curCnt = 1; @@ -6392,6 +5446,974 @@ class MapSum { } ``` + +## 栈和队列 + +**用栈实现队列** + +[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; +} +``` + + ## 图 ### 二分图 @@ -7052,3 +7074,4 @@ public int[] countBits(int num) { - 何海涛, 软件工程师. 剑指 Offer: 名企面试官精讲典型编程题[M]. 电子工业出版社, 2014. - 《编程之美》小组. 编程之美[M]. 电子工业出版社, 2008. - 左程云. 程序员代码面试指南[M]. 电子工业出版社, 2015. + diff --git a/notes/Leetcode-Database 题解.md b/notes/Leetcode-Database 题解.md index 5ab726c5..5b4a3d00 100644 --- a/notes/Leetcode-Database 题解.md +++ b/notes/Leetcode-Database 题解.md @@ -461,7 +461,7 @@ Employee 表: +----+-------+--------+-----------+ ``` -查找所有员工,他们的薪资大于其经理薪资。 +查找薪资大于其经理薪资的员工信息。 ## SQL Schema @@ -924,27 +924,27 @@ VALUES ```sql SELECT s1.id - 1 AS id, - s1.student + s1.student FROM - seat s1 + seat s1 WHERE s1.id MOD 2 = 0 UNION SELECT s2.id + 1 AS id, - s2.student + s2.student FROM - seat s2 + seat s2 WHERE - s2.id MOD 2 = 1 + s2.id MOD 2 = 1 AND s2.id != ( SELECT max( s3.id ) FROM seat s3 ) UNION SELECT s4.id AS id, - s4.student + s4.student FROM - seat s4 + seat s4 WHERE - s4.id MOD 2 = 1 - AND s4.id = ( SELECT max( s5.id ) FROM seat s5 ) + s4.id MOD 2 = 1 + AND s4.id = ( SELECT max( s5.id ) FROM seat s5 ) ORDER BY id; ``` diff --git a/notes/Linux.md b/notes/Linux.md index 27094e5c..14d3b317 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -11,7 +11,6 @@ * [GNU](#gnu) * [开源协议](#开源协议) * [二、磁盘](#二磁盘) - * [HDD](#hdd) * [磁盘接口](#磁盘接口) * [磁盘的文件名](#磁盘的文件名) * [三、分区](#三分区) @@ -185,21 +184,6 @@ GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操 # 二、磁盘 -## HDD - -Hard Disk Drives(HDD) 俗称硬盘,具有以下结构: - -- 盘面(Platter):一个硬盘有多个盘面; -- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道; -- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小; -- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写); -- 制动手臂(Actuator arm):用于在磁道之间移动磁头; -- 主轴(Spindle):使整个盘面转动。 - -

- -[Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) - ## 磁盘接口 ### 1. IDE @@ -291,8 +275,6 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 ## 组成 -

- 最主要的几个组成部分如下: - inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号; @@ -303,6 +285,9 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 - superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; - block bitmap:记录 block 是否被使用的位域; +

+ + ## 文件读取 对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 @@ -352,7 +337,9 @@ inode 中记录了文件内容所在的 block 编号,但是每个 block 非常 ## 目录 -建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 +建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。 + +可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 ## 日志 @@ -871,7 +858,9 @@ $ ls -al /etc | less ## 提取指令 -cut 对数据进行切分,取出想要的部分。切分过程一行一行地进行。 +cut 对数据进行切分,取出想要的部分。 + +切分过程一行一行地进行。 ```html $ cut @@ -891,7 +880,7 @@ root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16) $ last | cut -d ' ' -f 1 ``` -示例 2:将 export 输出的讯息,取出第 12 字符以后的所有字符串。 +示例 2:将 export 输出的信息,取出第 12 字符以后的所有字符串。 ```html $ export @@ -901,12 +890,12 @@ declare -x HOME="/home/dmtsai" declare -x HOSTNAME="study.centos.vbird" .....(其他省略)..... -$ export | cut -c 12 +$ export | cut -c 12- ``` ## 排序指令 -**sort** 进行排序。 +**sort** 用于排序。 ```html $ sort [-fbMnrtuk] [file or stdin] @@ -1023,10 +1012,10 @@ g/re/p(globally search a regular expression and print),使用正则表示式 ```html $ grep [-acinv] [--color=auto] 搜寻字符串 filename --c : 计算找到个数 +-c : 统计个数 -i : 忽略大小写 -n : 输出行号 --v : 反向选择,亦即显示出没有 搜寻字符串 内容的那一行 +-v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 --color=auto :找到的关键字加颜色显示 ``` @@ -1066,7 +1055,7 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。 -示例 1:取出登录用户的用户名和 ip +示例:取出登录用户的用户名和 IP ```html $ last -n 5 @@ -1075,8 +1064,10 @@ dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22) dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12) dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14) dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15) +``` -$ last -n 5 | awk '{print $1 "\t" $3} +```html +$ last -n 5 | awk '{print $1 "\t" $3}' ``` 可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。 @@ -1085,7 +1076,7 @@ $ last -n 5 | awk '{print $1 "\t" $3} $ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename ``` -示例 2:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 +示例:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 ```text $ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' @@ -1102,7 +1093,7 @@ awk 变量: | NR | 目前所处理的是第几行数据 | | FS | 目前的分隔字符,默认是空格键 | -示例 3:输出正在处理的行号,并显示每一行有多少字段 +示例:显示正在处理的行号以及每一行有多少字段 ```html $ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}' @@ -1123,19 +1114,19 @@ dmtsai lines: 5 columns: 9 示例一:查看自己的进程 -``` +```sh # ps -l ``` 示例二:查看系统所有进程 -``` +```sh # ps aux ``` 示例三:查看特定的进程 -``` +```sh # ps aux | grep threadx ``` @@ -1145,7 +1136,7 @@ dmtsai lines: 5 columns: 9 示例:两秒钟刷新一次 -``` +```sh # top -d 2 ``` @@ -1155,7 +1146,7 @@ dmtsai lines: 5 columns: 9 示例:查看所有进程树 -``` +```sh # pstree -A ``` @@ -1165,14 +1156,12 @@ dmtsai lines: 5 columns: 9 示例:查看特定端口的进程 -``` +```sh # netstat -anp | grep port ``` ## 进程状态 -

- | 状态 | 说明 | | :---: | --- | | R | running or runnable (on run queue) | @@ -1181,9 +1170,11 @@ dmtsai lines: 5 columns: 9 | Z | zombie (terminated but not reaped by its parent) | | T | stopped (either by a job control signal or because it is being traced) | +

+ ## SIGCHLD -当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中: +当一个子进程改变了它的状态时(停止运行,继续运行或者退出),有两件事会发生在父进程中: - 得到 SIGCHLD 信号; - waitpid() 或者 wait() 调用会返回。 @@ -1192,7 +1183,7 @@ dmtsai lines: 5 columns: 9 其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。 -在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 +在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 ## wait() @@ -1204,11 +1195,7 @@ pid_t wait(int *status) 如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。 -参数 status 用来保存被收集的子进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL: - -```c -pid = wait(NULL); -``` +参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。 ## waitpid() @@ -1218,7 +1205,7 @@ pid_t waitpid(pid_t pid, int *status, int options) 作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。 -pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 +pid 参数指示一个子进程的 ID,表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。 @@ -1236,9 +1223,9 @@ options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 w 僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。 -系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 +系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 -要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时所有的僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵尸进程。 +要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。 # 参考资料 diff --git a/notes/MySQL.md b/notes/MySQL.md index 5348018d..690425db 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -9,7 +9,7 @@ * [字符串](#字符串) * [时间和日期](#时间和日期) * [三、索引](#三索引) - * [B Tree 原理](#b-tree-原理) + * [B+ Tree 原理](#b-tree-原理) * [索引分类](#索引分类) * [索引的优点](#索引的优点) * [索引优化](#索引优化) @@ -95,15 +95,15 @@ VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。 ## 时间和日期 -MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。 +MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。 -### 1. DATATIME +### 1. DATETIME 能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 它与时区无关。 -默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 +默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 ### 2. TIMESTAMP @@ -125,49 +125,33 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 -## B Tree 原理 +## B+ Tree 原理 -### 1. B-Tree +### 1. 数据结构 -

+B Tree 指的是 Balance Tree,也就是平衡树。平衡树时一颗查找树,并且所有叶子节点位于同一层。 -定义一条数据记录为一个二元组 [key, data],B-Tree 是满足下列条件的数据结构: +B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。 -- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的; -- 一个节点中的 key 从左到右非递减排列; -- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 - -查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。 - -由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作以保持 B-Tree 性质。 - -### 2. B+Tree - -

- -与 B-Tree 相比,B+Tree 有以下不同点: - -- 每个节点的指针上限为 2d 而不是 2d+1(d 为节点的出度); -- 内节点不存储 data,只存储 key; -- 叶子节点不存储指针。 - -### 3. 顺序访问指针 +在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1

-一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。 +### 2. 操作 -### 4. 优势 +进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。 -红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B Tree 作为索引结构,主要有以下两个原因: +插入删除操作记录会破坏平衡树的平衡性,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作。 + +### 3. 与红黑树的比较 + +红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因: (一)更少的检索次数 平衡树检索数据的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。 -红黑树的出度为 2,而 B Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B Tree 大非常多,因此检索的次数也就更多。 - -B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高。 +红黑树的出度为 2,而 B+ Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B+ Tree 大非常多,因此检索的次数也就更多。 (二)利用计算机预读特性 @@ -175,8 +159,6 @@ B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。 -更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) - ## 索引分类 ### 1. B+Tree 索引 @@ -379,7 +361,7 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 -在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userBD 等。 +在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userDB 等。 ## Sharding 策略 @@ -442,3 +424,4 @@ MySQL 读写分离能提高性能的原因在于: - [服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策") - [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) diff --git a/notes/Redis.md b/notes/Redis.md index 8bd8aed0..0981abf0 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -25,20 +25,19 @@ * [六、键的过期时间](#六键的过期时间) * [七、数据淘汰策略](#七数据淘汰策略) * [八、持久化](#八持久化) - * [快照持久化](#快照持久化) + * [RDB 持久化](#rdb-持久化) * [AOF 持久化](#aof-持久化) -* [九、发布与订阅](#九发布与订阅) -* [十、事务](#十事务) -* [十一、事件](#十一事件) +* [九、事务](#九事务) +* [十、事件](#十事件) * [文件事件](#文件事件) * [时间事件](#时间事件) * [事件的调度与执行](#事件的调度与执行) -* [十二、复制](#十二复制) +* [十一、复制](#十一复制) * [连接过程](#连接过程) * [主从链](#主从链) -* [十三、Sentinel](#十三sentinel) -* [十四、分片](#十四分片) -* [十五、一个简单的论坛系统分析](#十五一个简单的论坛系统分析) +* [十二、Sentinel](#十二sentinel) +* [十三、分片](#十三分片) +* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) * [文章信息](#文章信息) * [点赞功能](#点赞功能) * [对文章进行排序](#对文章进行排序) @@ -210,17 +209,7 @@ OK ## 字典 -以下是 Redis 字典的主要数据结构,从上往下分析,一个 dict 有两个 dictht,一个 dictht 有一个 dictEntry 数组,每个 dictEntry 有 next 指针因此是一个链表结构。从上面的分析可以看出 Redis 的字典是一个基于拉链法解决冲突的哈希表结构。 - -```c -typedef struct dict { - dictType *type; - void *privdata; - dictht ht[2]; - long rehashidx; /* rehashing not in progress if rehashidx == -1 */ - unsigned long iterators; /* number of iterators currently running */ -} dict; -``` +dictht 是一个散列表结构,使用拉链法保存哈希冲突的 dictEntry。 ```c /* This is our hash table structure. Every dictionary has two of this as we @@ -246,11 +235,21 @@ typedef struct dictEntry { } dictEntry; ``` -哈希表需要具备扩容能力,在扩容时就需要对每个键值对进行 rehash。dict 有两个 dictht,在 rehash 的时候会将一个 dictht 上的键值对重新插入另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。 +Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。 + +```c +typedef struct dict { + dictType *type; + void *privdata; + dictht ht[2]; + long rehashidx; /* rehashing not in progress if rehashidx == -1 */ + unsigned long iterators; /* number of iterators currently running */ +} dict; +``` rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。 -渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。 +渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。 在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。 @@ -320,13 +319,13 @@ int dictRehash(dict *d, int n) {

-在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。例如下图演示了查找 22 的过程。 +在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。

与红黑树等平衡树相比,跳跃表具有以下优点: -- 插入速度非常快速,因为不需要平衡树的旋转操作; +- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性; - 更容易实现; - 支持无锁操作。 @@ -336,7 +335,7 @@ int dictRehash(dict *d, int n) { 可以对 String 进行自增自减运算,从而实现计数器功能。 -例如对于网站访问量,如果使用 MySQL 数据库进行存储,那么每访问一次网站就要对磁盘进行读写操作。而对 Redis 这种内存型数据库的读写性能非常高,很适合存储这种频繁读写的计数量。 +Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 ## 缓存 @@ -346,7 +345,7 @@ int dictRehash(dict *d, int n) { 例如 DNS 记录就很适合使用 Redis 进行存储。 -查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效。 +查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。 ## 消息队列 @@ -356,27 +355,29 @@ List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息 ## 会话缓存 -在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息,使得某个应用服务器宕机时不会丢失会话信息,从而保证高可用。 +在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息。 + +当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器。 ## 分布式锁实现 -在分布式场景下,无法使用单机环境下的锁实现。当多个节点上的进程都需要获取同一个锁时,就需要使用分布式锁来进行同步。 +在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。 -除了可以使用 Redis 自带的 SETNX 命令实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。 +可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 ## 其它 -Set 可以实现交集、并集等操作,例如共同好友功能。 +Set 可以实现交集、并集等操作,从而实现共同好友等功能。 -ZSet 可以实现有序性操作,例如排行榜功能。 +ZSet 可以实现有序性操作,从而实现排行榜等功能。 # 五、Redis 与 Memcached -两者都是非关系型内存键值数据库。有以下主要不同: +两者都是非关系型内存键值数据库,主要有以下不同: ## 数据类型 -Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 +Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。 ## 数据持久化 @@ -384,15 +385,15 @@ Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不 ## 分布式 -Memcached 不支持分布式,只能通过在客户端使用一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 +Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 Redis Cluster 实现了分布式的支持。 ## 内存管理机制 -在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。 +- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。 -Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 +- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 # 六、键的过期时间 @@ -402,7 +403,9 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除 # 七、数据淘汰策略 -可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 +可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。 + +Reids 具体有 6 种淘汰策略: | 策略 | 描述 | | :--: | :--: | @@ -413,15 +416,17 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除 | allkeys-random | 从所有数据集中任意选择数据进行淘汰 | | noeviction | 禁止驱逐数据 | -如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 +作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。 -作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key。 +使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 + +Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。 # 八、持久化 Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 -## 快照持久化 +## RDB 持久化 将某个时间点的所有数据都存放到硬盘上。 @@ -435,9 +440,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 将写命令添加到 AOF 文件(Append Only File)的末尾。 -对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。 - -有以下同步选项: +使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项: | 选项 | 同步频率 | | :--: | :--: | @@ -446,25 +449,12 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 | no | 让操作系统来决定何时同步 | - always 选项会严重减低服务器的性能; -- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; -- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。 +- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; +- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。 随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 -# 九、发布与订阅 - -订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 - -某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。 - -发布与订阅模式和观察者模式有以下不同: - -- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 -- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息。 - -

- -# 十、事务 +# 九、事务 一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 @@ -472,7 +462,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 -# 十一、事件 +# 十、事件 Redis 服务器是一个事件驱动程序。 @@ -537,7 +527,7 @@ def main():

-# 十二、复制 +# 十一、复制 通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 @@ -557,15 +547,18 @@ def main():

-# 十三、Sentinel +# 十二、Sentinel Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 -# 十四、分片 +# 十三、分片 -分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 +分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。 -假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 +假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。 + +- 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 +- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 根据执行分片的位置,可以分为三种分片方式: @@ -573,7 +566,7 @@ Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状 - 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 - 服务器分片:Redis Cluster。 -# 十五、一个简单的论坛系统分析 +# 十四、一个简单的论坛系统分析 该论坛系统功能如下: @@ -613,3 +606,4 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据 - [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide) - [Redis 应用场景](http://www.scienjus.com/redis-use-case/) - [Observer vs Pub-Sub](http://developers-club.com/posts/270339/) +- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) diff --git a/notes/SQL.md b/notes/SQL.md index ca9b7b29..4a44aee8 100644 --- a/notes/SQL.md +++ b/notes/SQL.md @@ -19,7 +19,7 @@ * [十八、存储过程](#十八存储过程) * [十九、游标](#十九游标) * [二十、触发器](#二十触发器) -* [二十一、事务处理](#二十一事务处理) +* [二十一、事务管理](#二十一事务管理) * [二十二、字符集](#二十二字符集) * [二十三、权限管理](#二十三权限管理) * [参考资料](#参考资料) @@ -553,7 +553,7 @@ WHERE col5 = val; # 十八、存储过程 -存储过程可以看成是对一系列 SQL 操作的批处理; +存储过程可以看成是对一系列 SQL 操作的批处理。 使用存储过程的好处: @@ -642,11 +642,11 @@ SELECT @result; -- 获取结果 DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。 -UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改地,而 OLD 是只读的。 +UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改的,而 OLD 是只读的。 MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。 -# 二十一、事务处理 +# 二十一、事务管理 基本术语: @@ -708,12 +708,12 @@ SELECT user FROM user; **创建账户** +新创建的账户没有任何权限。 + ```sql CREATE USER myuser IDENTIFIED BY 'mypassword'; ``` -新创建的账户没有任何权限。 - **修改账户名** ```sql @@ -734,18 +734,14 @@ SHOW GRANTS FOR myuser; **授予权限** +账户用 username@host 的形式定义,username@% 使用的是默认主机名。 + ```sql GRANT SELECT, INSERT ON mydatabase.* TO myuser; ``` -账户用 username@host 的形式定义,username@% 使用的是默认主机名。 - **删除权限** -```sql -REVOKE SELECT, INSERT ON mydatabase.* FROM myuser; -``` - GRANT 和 REVOKE 可在几个层次上控制访问权限: - 整个服务器,使用 GRANT ALL 和 REVOKE ALL; @@ -754,6 +750,10 @@ GRANT 和 REVOKE 可在几个层次上控制访问权限: - 特定的列; - 特定的存储过程。 +```sql +REVOKE SELECT, INSERT ON mydatabase.* FROM myuser; +``` + **更改密码** 必须使用 Password() 函数 diff --git a/notes/代码风格规范.md b/notes/代码风格规范.md index b1ce8137..dedada7d 100644 --- a/notes/代码风格规范.md +++ b/notes/代码风格规范.md @@ -2,5 +2,5 @@ -- [Twitter Java Style Guide](https://github.com/twitter/commons/blob/master/src/java/com/twitter/common/styleguide.mde) +- [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) diff --git a/notes/一致性.md b/notes/分布式.md similarity index 52% rename from notes/一致性.md rename to notes/分布式.md index 7209dccf..c048b43e 100644 --- a/notes/一致性.md +++ b/notes/分布式.md @@ -1,32 +1,176 @@ -* [一、CAP](#一cap) +* [一、分布式锁](#一分布式锁) + * [数据库的唯一索引](#数据库的唯一索引) + * [Redis 的 SETNX 指令](#redis-的-setnx-指令) + * [Redis 的 RedLock 算法](#redis-的-redlock-算法) + * [Zookeeper 的有序节点](#zookeeper-的有序节点) +* [二、分布式事务](#二分布式事务) + * [本地消息表](#本地消息表) + * [2PC](#2pc) +* [三、CAP](#三cap) * [一致性](#一致性) * [可用性](#可用性) * [分区容忍性](#分区容忍性) * [权衡](#权衡) -* [二、BASE](#二base) +* [四、BASE](#四base) * [基本可用](#基本可用) * [软状态](#软状态) * [最终一致性](#最终一致性) -* [三、2PC](#三2pc) - * [运行过程](#运行过程) - * [存在的问题](#存在的问题) -* [四、Paxos](#四paxos) +* [五、Paxos](#五paxos) * [执行过程](#执行过程) * [约束条件](#约束条件) * [五、Raft](#五raft) * [单个 Candidate 的竞选](#单个-candidate-的竞选) * [多个 Candidate 竞选](#多个-candidate-竞选) * [日志复制](#日志复制) -* [参考资料](#参考资料) -# 一、CAP +# 一、分布式锁 + +在单机场景下,可以使用 Java 提供的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。 + +阻塞锁通常使用互斥量来实现: + +- 互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态; +- 互斥量为 0 表示未锁定状态。 + +1 和 0 可以用一个整型值表示,也可以用某个数据存在或者不存在表示,存在表示互斥量为 1。 + +## 数据库的唯一索引 + +当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。 + +存在以下几个问题: + +- 锁没有失效时间,解锁失败的话其它进程无法再获得锁。 +- 只能是非阻塞锁,插入失败直接就报错了,无法重试。 +- 不可重入,已经获得锁的进程也必须重新获取锁。 + +## Redis 的 SETNX 指令 + +使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。 + +SETNX 指令和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。 + +EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。 + +## Redis 的 RedLock 算法 + +使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。 + +- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个; +- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,那么就认为锁获取成功了; +- 如果锁获取失败,就到每个实例上释放锁。 + +## Zookeeper 的有序节点 + +### 1. Zookeeper 抽象模型 + +Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。 + +

+ +### 2. 节点类型 + +- 永久节点:不会因为会话结束或者超时而消失; +- 临时节点:如果会话结束或者超时就会消失; +- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推。 + +### 3. 监听器 + +为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。 + +### 4. 分布式锁实现 + +- 创建一个锁目录 /lock; +- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点; +- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁; +- 执行业务代码,完成后,删除对应的子节点。 + +### 5. 会话超时 + +如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的释放锁失败问题。 + +### 6. 羊群效应 + +一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 + +参考: + +- [Distributed locks with Redis](https://redis.io/topics/distlock) +- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023) +- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html) + +# 二、分布式事务 + +指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。 + +## 本地消息表 + +### 1. 原理 + +本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。 + +1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 +2. 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 +3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。 + +

+ +### 2. 分析 + +本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。 + +## 2PC + +两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。 + +### 1. 运行过程 + +(一)准备阶段 + +协调者询问参与者事务是否执行成功,参与者发回事务执行结果。 + +

+ +(二)提交阶段 + +如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 + +需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 + +

+ +### 2. 存在的问题 + +(一)同步阻塞 + +所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。 + +(二)单点问题 + +协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。 + +(三)数据不一致 + +在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。 + +(四)太过保守 + +任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 + +参考: + +- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) +- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html) +- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be) + +# 三、CAP 分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。 -

+

## 一致性 @@ -54,18 +198,23 @@ 可用性和一致性往往是冲突的,很难都使它们同时满足。在多个节点之间进行数据同步时, -- 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成; -- 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。 +* 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成; +* 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。 -

+

-# 二、BASE +参考: + +- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015. +- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/) + +# 四、BASE BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。 -BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 +BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 -

+

## 基本可用 @@ -85,49 +234,7 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 -# 三、2PC - -两阶段提交(Two-phase Commit,2PC) - -主要用于实现分布式事务,分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。 - -通过引入协调者(Coordinator)来调度参与者的行为,,并最终决定这些参与者是否要真正执行事务。 - -## 运行过程 - -### 1. 准备阶段 - -协调者询问参与者事务是否执行成功,参与者发回事务执行结果。 - -

- -### 2. 提交阶段 - -如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 - -

- -需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 - -## 存在的问题 - -### 1. 同步阻塞 - -所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。 - -### 2. 单点问题 - -协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响,特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。 - -### 3. 数据不一致 - -在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。 - -### 4. 太过保守 - -任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 - -# 四、Paxos +# 五、Paxos 用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。 @@ -137,7 +244,7 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE - 接受者(Acceptor):对每个提议进行投票; - 告知者(Learner):被告知投票的结果,不参与投票过程。 -

+

## 执行过程 @@ -145,19 +252,19 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。 -

+

当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。 如下图,Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。 -

+

如果 Acceptor 接收到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2,那么就丢弃该提议请求;否则,发送提议响应,该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收到的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。 如下图,Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的提议请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2,因此就抛弃该提议请求;Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的提议请求,因为之前接收到的提议为 [n=2, v=8],并且 2 <= 4,因此就发送 [n=2, v=8] 的提议响应,设置当前接收到的提议为 [n=4, v=5],并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。 -

+

当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。 @@ -165,26 +272,31 @@ Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求 Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。 -

+

Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。 -

+

## 约束条件 -### 1. 正确性 +### 1\. 正确性 指只有一个提议值会生效。 因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。 -### 2. 可终止性 +### 2\. 可终止性 指最后总会有一个提议生效。 Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。 +参考: + +- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/) +- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/) + # 五、Raft Raft 和 Paxos 类似,但是更容易理解,也更容易实现。 @@ -195,55 +307,51 @@ Raft 主要是用来竞选主节点。 有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。 -- 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 +* 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 -

+

-- 此时 A 发送投票请求给其它所有节点。 +* 此时 A 发送投票请求给其它所有节点。 -

+

-- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 +* 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 -

+

-- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 +* 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 -

+

## 多个 Candidate 竞选 * 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 -

+

* 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 -

+

## 日志复制 -- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 +* 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 -

+

-- Leader 会把修改复制到所有 Follower。 +* Leader 会把修改复制到所有 Follower。 -

+

-- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 +* Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 -

+

-- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 +* 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 -

+

-# 参考资料 +参考: -- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015. -- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/) -- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/) - [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) -- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/) diff --git a/notes/分布式问题分析.md b/notes/分布式问题分析.md deleted file mode 100644 index 4f019525..00000000 --- a/notes/分布式问题分析.md +++ /dev/null @@ -1,255 +0,0 @@ - -* [一、分布式锁](#一分布式锁) - * [数据库的唯一索引](#数据库的唯一索引) - * [Redis 的 SETNX 指令](#redis-的-setnx-指令) - * [Redis 的 RedLock 算法](#redis-的-redlock-算法) - * [Zookeeper 的有序节点](#zookeeper-的有序节点) -* [二、分布式事务](#二分布式事务) - * [本地消息表](#本地消息表) - * [两阶段提交协议](#两阶段提交协议) -* [三、分布式 Session](#三分布式-session) - * [Sticky Sessions](#sticky-sessions) - * [Session Replication](#session-replication) - * [Session Server](#session-server) -* [四、负载均衡](#四负载均衡) - * [算法](#算法) - * [实现](#实现) - - - -# 一、分布式锁 - -在单机场景下,可以使用 Java 提供的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。 - -阻塞锁通常使用互斥量来实现,互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态,互斥量为 0 表示未锁定状态。1 和 0 可以用一个整型值来存储,也可以用某个数据存在或者不存在来存储,某个数据存在表示互斥量为 1。 - -## 数据库的唯一索引 - -当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。 - -存在以下几个问题: - -- 锁没有失效时间,解锁失败的话其他线程无法再获得锁。 -- 只能是非阻塞锁,插入失败直接就报错了,无法重试。 -- 不可重入,已经获得锁的进程也必须重新获取锁。 - -## Redis 的 SETNX 指令 - -使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。 - -SETNX 指令和数据库的唯一索引类似,可以保证只存在一个 Key 的键值对,可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。 - -EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。 - -## Redis 的 RedLock 算法 - -使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。 - -- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。 -- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。 -- 如果锁获取失败,会到每个实例上释放锁。 - -## Zookeeper 的有序节点 - -### 1. Zookeeper 抽象模型 - -Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。 - -

- -### 2. 节点类型 - -- 永久节点:不会因为会话结束或者超时而消失; -- 临时节点:如果会话结束或者超时就会消失; -- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推。 - -### 3. 监听器 - -为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。 - -### 4. 分布式锁实现 - -- 创建一个锁目录 /lock; -- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点; -- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁; -- 执行业务代码,完成后,删除对应的子节点。 - -### 5. 会话超时 - -如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的释放锁失败问题。 - -### 6. 羊群效应 - -一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 - -参考: - -- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023) -- [Distributed locks with Redis](https://redis.io/topics/distlock) -- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html) - -# 二、分布式事务 - -指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。 - -## 本地消息表 - -### 1. 原理 - -本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。 - -1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 -2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 -3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。 - -

- -### 2. 分析 - -本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。 - -## 两阶段提交协议 - -[CyC2018/Interview-Notebook/一致性.md/2PC](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7.md) - -参考: - -- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) -- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html) -- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be) - -# 三、分布式 Session - -在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。 - -## Sticky Sessions - -需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。 - -缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。 - -

- -## Session Replication - -在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。 - -缺点:占用过多内存;同步过程占用网络带宽以及服务器处理器时间。 - -

- -## Session Server - -使用一个单独的服务器存储 Session 数据,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。 - -缺点:需要去实现存取 Session 的代码。 - -

- -参考: - -- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/) - -# 四、负载均衡 - -## 算法 - -### 1. 轮询(Round Robin) - -轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。 - -

- -该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。 - -

- -### 2. 加权轮询(Weighted Round Robbin) - -加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。 - -

- -### 3. 最少连接(least Connections) - -由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。 - -

- -最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。 - -

- -### 4. 加权最少连接(Weighted Least Connection) - -在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。 - -

- -### 5. 随机算法(Random) - -把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。 - -

- -### 6. 源地址哈希法 (IP Hash) - -源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。 - -- 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。 -- 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。 - -

- -## 实现 - -### 1. HTTP 重定向 - -HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。 - -缺点: - -- 用户访问的延迟会增加; -- 如果负载均衡器宕机,就无法访问该站点。 - -

- -### 2. DNS 重定向 - -使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。 - -缺点: - -- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。 - -

- -### 3. 修改 MAC 地址 - -使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。 - -

- -### 4. 修改 IP 地址 - -在网络层修改请求的目的 IP 地址。 - -

- -### 5. 代理自动配置 - -正向代理与反向代理的区别: - -- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。 -- 反向代理:发生在服务器端,用户不知道代理的存在。 - -PAC 服务器是用来判断一个请求是否要经过代理。 - -

- -参考: - -- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms) -- [负载均衡算法及手段](https://segmentfault.com/a/1190000004492447) -- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#) - diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index c0c5412f..0c9bd1f9 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -1,4 +1,5 @@ +* [1. 前言](#1-前言) * [2. 实现 Singleton](#2-实现-singleton) * [3. 数组中重复的数字](#3-数组中重复的数字) * [4. 二维数组中的查找](#4-二维数组中的查找) @@ -80,6 +81,13 @@ +# 1. 前言 + +本文的绘图可通过以下途径免费获得并使用: + +- [ProcessOn](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49) +- [DrawIO](https://drive.google.com/file/d/1nSSCpPUC05MFoeFuf_aeTtkm7dG5-bJ1/view?usp=sharing) + # 2. 实现 Singleton [单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) @@ -90,12 +98,20 @@ ## 题目描述 -在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5},那么对应的输出是第一个重复的数字 2。 +在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 -要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。 +```html +Input: +{2, 3, 1, 0, 2, 5} + +Output: +2 +``` ## 解题思路 +要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。 + 这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。 以 (2, 3, 1, 0, 2, 5) 为例: @@ -158,7 +174,11 @@ Given target = 20, return false. ## 解题思路 -从右上角开始查找。因为矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。 +从右上角开始查找。矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。 + +当前元素的查找区间为左下角的所有元素,例如元素 12 的查找区间如下: + +

复杂度:O(M + N) + O(1) @@ -261,25 +281,14 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` -### 使用 Collections.reverse() - -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - ArrayList ret = new ArrayList<>(); - while (listNode != null) { - ret.add(listNode.val); - listNode = listNode.next; - } - Collections.reverse(ret); - return ret; -} -``` - ### 使用头插法 利用链表头插法为逆序的特点。 -头结点和第一个节点的区别:头结点是在头插法中使用的一个额外节点,这个节点不存储值;第一个节点就是链表的第一个真正存储值的节点。 +头结点和第一个节点的区别: + +- 头结点是在头插法中使用的一个额外节点,这个节点不存储值; +- 第一个节点就是链表的第一个真正存储值的节点。 ```java public ArrayList printListFromTailToHead(ListNode listNode) { @@ -302,6 +311,20 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` +### 使用 Collections.reverse() + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList ret = new ArrayList<>(); + while (listNode != null) { + ret.add(listNode.val); + listNode = listNode.next; + } + Collections.reverse(ret); + 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) @@ -322,22 +345,23 @@ inorder = [9,3,15,20,7] 前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。 ```java -private Map inOrderNumsIndexs = new HashMap<>(); // 缓存中序遍历数组的每个值对应的索引 +// 缓存中序遍历数组的每个值对应的索引 +private Map inOrderNumsIndexs = new HashMap<>(); public TreeNode reConstructBinaryTree(int[] pre, int[] in) { for (int i = 0; i < in.length; i++) inOrderNumsIndexs.put(in[i], i); - return reConstructBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1); + return reConstructBinaryTree(pre, 0, pre.length - 1, 0, in.length - 1); } -private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, int inL, int inR) { +private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL, int inR) { if (preL > preR) return null; TreeNode root = new TreeNode(pre[preL]); int inIndex = inOrderNumsIndexs.get(root.val); int leftTreeSize = inIndex - inL; - root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, in, inL, inL + leftTreeSize - 1); - root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, in, inL + leftTreeSize + 1, inR); + root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL, inL + leftTreeSize - 1); + root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1, inR); return root; } ``` @@ -350,16 +374,6 @@ private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 -## 解题思路 - -① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; - -

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

- ```java public class TreeLinkNode { int val; @@ -373,6 +387,16 @@ public class TreeLinkNode { } ``` +## 解题思路 + +① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; + +

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

+ ```java public TreeLinkNode GetNext(TreeLinkNode pNode) { if (pNode.right != null) { @@ -398,7 +422,7 @@ public TreeLinkNode GetNext(TreeLinkNode pNode) { ## 题目描述 -用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。队列中的元素为 int 类型。 +用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。 ## 解题思路 @@ -440,9 +464,9 @@ public int pop() throws Exception { 如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。 -

+

-递归方法是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,避免重复求解子问题。 +递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。 ```java public int Fibonacci(int n) { @@ -521,11 +545,11 @@ public int JumpFloor(int n) { ```java public int JumpFloor(int n) { - if (n <= 1) + if (n <= 2) return n; - int pre2 = 0, pre1 = 1; - int result = 0; - for (int i = 1; i <= n; i++) { + int pre2 = 1, pre1 = 2; + int result = 1; + for (int i = 2; i < n; i++) { result = pre2 + pre1; pre2 = pre1; pre1 = result; @@ -603,13 +627,18 @@ 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。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0。 ## 解题思路 -当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m;否则解在 [m + 1, h] 之间,令 l = m + 1。 +- 当 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#%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE)。 +因为 h 的赋值表达式为 h = m,因此循环体的循环条件应该为 l < h,详细解释请见 [Leetcode 题解](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md) 二分查找部分。 + +但是如果出现 nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。 复杂度:O(logN) + O(1) @@ -620,13 +649,22 @@ public int minNumberInRotateArray(int[] nums) { int l = 0, h = nums.length - 1; while (l < h) { int m = l + (h - l) / 2; - if (nums[m] <= nums[h]) + 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. 矩阵中的路径 @@ -732,10 +770,10 @@ private void initDigitSum() { n /= 10; } } - digitSum = new int[rows][cols]; + this.digitSum = new int[rows][cols]; for (int i = 0; i < this.rows; i++) for (int j = 0; j < this.cols; j++) - digitSum[i][j] = digitSumOne[i] + digitSumOne[j]; + this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j]; } ``` @@ -747,24 +785,17 @@ private void initDigitSum() { 把一根绳子剪成多段,并且使得每段的长度乘积最大。 -For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4). +```html +n = 2 +return 1 (2 = 1 + 1) + +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; j++) - dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); - return dp[n]; -} -``` - -### 贪心解法 +### 贪心 尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现,如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。 @@ -786,6 +817,19 @@ public int integerBreak(int n) { } ``` +### 动态规划 + +```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) @@ -804,7 +848,7 @@ public int NumberOf1(int n) { ### n&(n-1) -O(logM) 时间复杂度解法,其中 M 表示 1 的个数。 +O(M) 时间复杂度解法,其中 M 表示 1 的个数。 该位运算是去除 n 的位级表示中最低的那一位。 @@ -863,7 +907,7 @@ public double Power(double base, int exponent) { ## 题目描述 -输入数字 n,按顺序打印出从 1 最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。 +输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。 ## 解题思路 @@ -876,16 +920,16 @@ public void print1ToMaxOfNDigits(int n) { if (n <= 0) return; char[] number = new char[n]; - print1ToMaxOfNDigits(number, -1); + print1ToMaxOfNDigits(number, 0); } private void print1ToMaxOfNDigits(char[] number, int digit) { - if (digit == number.length - 1) { + if (digit == number.length) { printNumber(number); return; } for (int i = 0; i < 10; i++) { - number[digit + 1] = (char) (i + '0'); + number[digit] = (char) (i + '0'); print1ToMaxOfNDigits(number, digit + 1); } } @@ -906,11 +950,11 @@ private void printNumber(char[] number) { ① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 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)。 @@ -965,28 +1009,19 @@ public ListNode deleteDuplication(ListNode pHead) { ## 题目描述 -请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配。 +请实现一个函数用来匹配包括 '.' 和 '\*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '\*' 表示它前面的字符可以出现任意次(包含 0 次)。 + +在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab\*ac\*a" 匹配,但是与 "aa.a" 和 "ab\*a" 均不匹配。 ## 解题思路 应该注意到,'.' 是用来当做一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而把它当成重复前面字符一次。 -```html -if p.charAt(j) == s.charAt(i) : then dp[i][j] = dp[i-1][j-1]; -if p.charAt(j) == '.' : then dp[i][j] = dp[i-1][j-1]; -if p.charAt(j) == '*' : - if p.charAt(j-1) != s.charAt(i) : then dp[i][j] = dp[i][j-2] // a* only counts as empty - if p.charAt(j-1) == s.charAt(i) or - p.charAt(i-1) == '.' : - then dp[i][j] = dp[i-1][j] // a* counts as multiple a - or dp[i][j] = dp[i][j-1] // a* counts as single a - or dp[i][j] = dp[i][j-2] // a* counts as empty -``` - ```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] == '*') @@ -997,10 +1032,13 @@ public boolean match(char[] str, char[] pattern) { 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] || dp[i][j - 2] || dp[i - 1][j]; - else - dp[i][j] = dp[i][j - 2]; + 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]; } ``` @@ -1011,10 +1049,25 @@ public boolean match(char[] str, char[] pattern) { ## 题目描述 -请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串 "+100","5e2","-123","3.1416" 和 "-1E-16" 都表示数值。 但是 "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" 都不是。 ## 解题思路 +使用正则表达式进行匹配。 + +```html +[] : 字符集合 +() : 分组 +? : 重复 0 ~ 1 ++ : 重复 1 ~ n +* : 重复 0 ~ n +. : 任意字符 +\\. : 转义后的 . +\\d : 数字 +``` + ```java public boolean isNumeric(char[] str) { if (str == null) @@ -1029,7 +1082,7 @@ public boolean isNumeric(char[] str) { ## 题目描述 -保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 +需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。 ## 解题思路 @@ -1059,7 +1112,7 @@ public void reOrderArray(int[] nums) { 设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。 -

+

```java public ListNode FindKthToTail(ListNode head, int k) @@ -1096,7 +1149,7 @@ public ListNode FindKthToTail(ListNode head, int k) 在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。 -

+

```java public ListNode EntryNodeOfLoop(ListNode pHead) @@ -1165,8 +1218,7 @@ public ListNode ReverseList(ListNode head) { ### 递归 ```java -public ListNode Merge(ListNode list1, ListNode list2) -{ +public ListNode Merge(ListNode list1, ListNode list2) { if (list1 == null) return list2; if (list2 == null) @@ -1184,8 +1236,7 @@ public ListNode Merge(ListNode list1, ListNode list2) ### 迭代 ```java -public ListNode Merge(ListNode list1, ListNode list2) -{ +public ListNode Merge(ListNode list1, ListNode list2) { ListNode head = new ListNode(-1); ListNode cur = head; while (list1 != null && list2 != null) { @@ -1217,15 +1268,13 @@ public ListNode Merge(ListNode list1, ListNode list2) ## 解题思路 ```java -public boolean HasSubtree(TreeNode root1, TreeNode root2) -{ +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) -{ +private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) { if (root2 == null) return true; if (root1 == null) @@ -1246,9 +1295,10 @@ private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) ## 解题思路 +### 递归 + ```java -public void Mirror(TreeNode root) -{ +public void Mirror(TreeNode root) { if (root == null) return; swap(root); @@ -1256,14 +1306,36 @@ public void Mirror(TreeNode root) Mirror(root.right); } -private void swap(TreeNode root) -{ +private void swap(TreeNode root) { TreeNode t = root.left; root.left = root.right; root.right = t; } ``` +### 迭代 + +```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) @@ -1275,15 +1347,13 @@ private void swap(TreeNode root) ## 解题思路 ```java -boolean isSymmetrical(TreeNode pRoot) -{ +boolean isSymmetrical(TreeNode pRoot) { if (pRoot == null) return true; return isSymmetrical(pRoot.left, pRoot.right); } -boolean isSymmetrical(TreeNode t1, TreeNode t2) -{ +boolean isSymmetrical(TreeNode t1, TreeNode t2) { if (t1 == null && t2 == null) return true; if (t1 == null || t2 == null) @@ -1302,13 +1372,12 @@ boolean isSymmetrical(TreeNode t1, TreeNode t2) 下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 -

+

## 解题思路 ```java -public ArrayList printMatrix(int[][] matrix) -{ +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) { @@ -1342,25 +1411,21 @@ public ArrayList printMatrix(int[][] matrix) private Stack dataStack = new Stack<>(); private Stack minStack = new Stack<>(); -public void push(int node) -{ +public void push(int node) { dataStack.push(node); minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node)); } -public void pop() -{ +public void pop() { dataStack.pop(); minStack.pop(); } -public int top() -{ +public int top() { return dataStack.peek(); } -public int min() -{ +public int min() { return minStack.peek(); } ``` @@ -1378,8 +1443,7 @@ public int min() 使用一个栈来模拟压入弹出操作。 ```java -public boolean IsPopOrder(int[] pushSequence, int[] popSequence) -{ +public boolean IsPopOrder(int[] pushSequence, int[] popSequence) { int n = pushSequence.length; Stack stack = new Stack<>(); for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) { @@ -1412,8 +1476,7 @@ public boolean IsPopOrder(int[] pushSequence, int[] popSequence) 不需要使用两个队列分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。 ```java -public ArrayList PrintFromTopToBottom(TreeNode root) -{ +public ArrayList PrintFromTopToBottom(TreeNode root) { Queue queue = new LinkedList<>(); ArrayList ret = new ArrayList<>(); queue.add(root); @@ -1443,8 +1506,7 @@ public ArrayList PrintFromTopToBottom(TreeNode root) ## 解题思路 ```java -ArrayList> Print(TreeNode pRoot) -{ +ArrayList> Print(TreeNode pRoot) { ArrayList> ret = new ArrayList<>(); Queue queue = new LinkedList<>(); queue.add(pRoot); @@ -1477,8 +1539,7 @@ ArrayList> Print(TreeNode pRoot) ## 解题思路 ```java -public ArrayList> Print(TreeNode pRoot) -{ +public ArrayList> Print(TreeNode pRoot) { ArrayList> ret = new ArrayList<>(); Queue queue = new LinkedList<>(); queue.add(pRoot); @@ -1519,15 +1580,13 @@ public ArrayList> Print(TreeNode pRoot) ## 解题思路 ```java -public boolean VerifySquenceOfBST(int[] sequence) -{ +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) -{ +private boolean verify(int[] sequence, int first, int last) { if (last - first <= 1) return true; int rootVal = sequence[last]; @@ -1558,14 +1617,12 @@ private boolean verify(int[] sequence, int first, int last) ```java private ArrayList> ret = new ArrayList<>(); -public ArrayList> FindPath(TreeNode root, int target) -{ +public ArrayList> FindPath(TreeNode root, int target) { backtracking(root, target, new ArrayList<>()); return ret; } -private void backtracking(TreeNode node, int target, ArrayList path) -{ +private void backtracking(TreeNode node, int target, ArrayList path) { if (node == null) return; path.add(node.val); @@ -1588,6 +1645,18 @@ private void backtracking(TreeNode node, int target, ArrayList path) 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。 +```java +public class RandomListNode { + int label; + RandomListNode next = null; + RandomListNode random = null; + + RandomListNode(int label) { + this.label = label; + } +} +``` +

## 解题思路 @@ -1605,8 +1674,7 @@ private void backtracking(TreeNode node, int target, ArrayList path)

```java -public RandomListNode Clone(RandomListNode pHead) -{ +public RandomListNode Clone(RandomListNode pHead) { if (pHead == null) return null; // 插入新节点 @@ -1653,14 +1721,12 @@ public RandomListNode Clone(RandomListNode pHead) private TreeNode pre = null; private TreeNode head = null; -public TreeNode Convert(TreeNode root) -{ +public TreeNode Convert(TreeNode root) { inOrder(root); return head; } -private void inOrder(TreeNode node) -{ +private void inOrder(TreeNode node) { if (node == null) return; inOrder(node.left); @@ -1687,21 +1753,18 @@ private void inOrder(TreeNode node) ```java private String deserializeStr; -public String Serialize(TreeNode root) -{ +public String Serialize(TreeNode root) { if (root == null) return "#"; return root.val + " " + Serialize(root.left) + " " + Serialize(root.right); } -public TreeNode Deserialize(String str) -{ +public TreeNode Deserialize(String str) { deserializeStr = str; return Deserialize(); } -private TreeNode Deserialize() -{ +private TreeNode Deserialize() { if (deserializeStr.length() == 0) return null; int index = deserializeStr.indexOf(" "); @@ -1730,8 +1793,7 @@ private TreeNode Deserialize() ```java private ArrayList ret = new ArrayList<>(); -public ArrayList Permutation(String str) -{ +public ArrayList Permutation(String str) { if (str.length() == 0) return ret; char[] chars = str.toCharArray(); @@ -1740,8 +1802,7 @@ public ArrayList Permutation(String str) return ret; } -private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) -{ +private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) { if (s.length() == chars.length) { ret.add(s.toString()); return; @@ -1768,11 +1829,10 @@ 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--。如果前面查找了 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) -{ +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; @@ -1803,8 +1863,7 @@ public int MoreThanHalfNum_Solution(int[] nums) 快速排序的 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) -{ +public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { ArrayList ret = new ArrayList<>(); if (k > nums.length || k <= 0) return ret; @@ -1815,8 +1874,7 @@ public ArrayList GetLeastNumbers_Solution(int[] nums, int k) return ret; } -public void findKthSmallest(int[] nums, int k) -{ +public void findKthSmallest(int[] nums, int k) { int l = 0, h = nums.length - 1; while (l < h) { int j = partition(nums, l, h); @@ -1829,8 +1887,7 @@ public void findKthSmallest(int[] nums, int k) } } -private int partition(int[] nums, int l, int h) -{ +private int partition(int[] nums, int l, int h) { int p = nums[l]; /* 切分元素 */ int i = l, j = h + 1; while (true) { @@ -1844,8 +1901,7 @@ private int partition(int[] nums, int l, int h) return j; } -private void swap(int[] nums, int i, int j) -{ +private void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; @@ -1862,8 +1918,7 @@ private void swap(int[] nums, int i, int j) 维护一个大小为 K 的最小堆过程如下:在添加一个元素之后,如果大顶堆的大小大于 K,那么需要将大顶堆的堆顶元素去除。 ```java -public ArrayList GetLeastNumbers_Solution(int[] nums, int k) -{ +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); @@ -1872,7 +1927,7 @@ public ArrayList GetLeastNumbers_Solution(int[] nums, int k) if (maxHeap.size() > k) maxHeap.poll(); } - return new ArrayList<>(maxHeap) ; + return new ArrayList<>(maxHeap); } ``` @@ -1894,8 +1949,7 @@ private PriorityQueue right = new PriorityQueue<>(); /* 当前数据流读入的元素个数 */ private int N = 0; -public void Insert(Integer val) -{ +public void Insert(Integer val) { /* 插入要保证两个堆存于平衡状态 */ if (N % 2 == 0) { /* N 为偶数的情况下插入到右半边。 @@ -1910,8 +1964,7 @@ public void Insert(Integer val) N++; } -public Double GetMedian() -{ +public Double GetMedian() { if (N % 2 == 0) return (left.peek() + right.peek()) / 2.0; else @@ -1933,16 +1986,14 @@ public Double GetMedian() private int[] cnts = new int[256]; private Queue queue = new LinkedList<>(); -public void Insert(char ch) -{ +public void Insert(char ch) { cnts[ch]++; queue.add(ch); while (!queue.isEmpty() && cnts[queue.peek()] > 1) queue.poll(); } -public char FirstAppearingOnce() -{ +public char FirstAppearingOnce() { return queue.isEmpty() ? '#' : queue.peek(); } ``` @@ -1953,13 +2004,12 @@ public char FirstAppearingOnce() ## 题目描述 -{6,-3,-2,7,-15,1,2,2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 +{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 ## 解题思路 ```java -public int FindGreatestSumOfSubArray(int[] nums) -{ +public int FindGreatestSumOfSubArray(int[] nums) { if (nums == null || nums.length == 0) return 0; int greatestSum = Integer.MIN_VALUE; @@ -1979,8 +2029,7 @@ public int FindGreatestSumOfSubArray(int[] nums) ## 解题思路 ```java -public int NumberOf1Between1AndN_Solution(int n) -{ +public int NumberOf1Between1AndN_Solution(int n) { int cnt = 0; for (int m = 1; m <= n; m *= 10) { int a = n / m, b = n % m; @@ -2001,11 +2050,10 @@ public int NumberOf1Between1AndN_Solution(int n) ## 解题思路 ```java -public int getDigitAtIndex(int index) -{ +public int getDigitAtIndex(int index) { if (index < 0) return -1; - int place = 1; // 位数,1 表示个位,2 表示 十位... + int place = 1; // 1 表示个位,2 表示 十位... while (true) { int amount = getAmountOfPlace(place); int totalAmount = amount * place; @@ -2020,8 +2068,7 @@ public int getDigitAtIndex(int index) * place 位数的数字组成的字符串长度 * 10, 90, 900, ... */ -private int getAmountOfPlace(int place) -{ +private int getAmountOfPlace(int place) { if (place == 1) return 10; return (int) Math.pow(10, place - 1) * 9; @@ -2031,8 +2078,7 @@ private int getAmountOfPlace(int place) * place 位数的起始数字 * 0, 10, 100, ... */ -private int getBeginNumberOfPlace(int place) -{ +private int getBeginNumberOfPlace(int place) { if (place == 1) return 0; return (int) Math.pow(10, place - 1); @@ -2041,8 +2087,7 @@ private int getBeginNumberOfPlace(int place) /** * 在 place 位数组成的字符串中,第 index 个数 */ -private int getDigitAtIndex(int index, int place) -{ +private int getDigitAtIndex(int index, int place) { int beginNumber = getBeginNumberOfPlace(place); int shiftNumber = index / place; String number = (beginNumber + shiftNumber) + ""; @@ -2064,8 +2109,7 @@ private int getDigitAtIndex(int index, int place) 可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。 ```java -public String PrintMinNumber(int[] numbers) -{ +public String PrintMinNumber(int[] numbers) { if (numbers == null || numbers.length == 0) return ""; int n = numbers.length; @@ -2091,8 +2135,7 @@ public String PrintMinNumber(int[] numbers) ## 解题思路 ```java -public int numDecodings(String s) -{ +public int numDecodings(String s) { if (s == null || s.length() == 0) return 0; int n = s.length(); @@ -2135,8 +2178,7 @@ public int numDecodings(String s) 应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。 ```java -public int getMost(int[][] values) -{ +public int getMost(int[][] values) { if (values == null || values.length == 0 || values[0].length == 0) return 0; int n = values[0].length; @@ -2159,8 +2201,7 @@ public int getMost(int[][] values) ## 解题思路 ```java -public int longestSubStringWithoutDuplication(String str) -{ +public int longestSubStringWithoutDuplication(String str) { int curLen = 0; int maxLen = 0; int[] preIndexs = new int[26]; @@ -2192,8 +2233,7 @@ public int longestSubStringWithoutDuplication(String str) ## 解题思路 ```java -public int GetUglyNumber_Solution(int N) -{ +public int GetUglyNumber_Solution(int N) { if (N <= 6) return N; int i2 = 0, i3 = 0, i5 = 0; @@ -2226,8 +2266,7 @@ public int GetUglyNumber_Solution(int N) 最直观的解法是使用 HashMap 对出现次数进行统计,但是考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。 ```java -public int FirstNotRepeatingChar(String str) -{ +public int FirstNotRepeatingChar(String str) { int[] cnts = new int[256]; for (int i = 0; i < str.length(); i++) cnts[str.charAt(i)]++; @@ -2241,8 +2280,7 @@ public int FirstNotRepeatingChar(String str) 以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么我们只需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。 ```java -public int FirstNotRepeatingChar2(String str) -{ +public int FirstNotRepeatingChar2(String str) { BitSet bs1 = new BitSet(256); BitSet bs2 = new BitSet(256); for (char c : str.toCharArray()) { @@ -2274,15 +2312,13 @@ public int FirstNotRepeatingChar2(String str) private long cnt = 0; private int[] tmp; // 在这里创建辅助数组,而不是在 merge() 递归函数中创建 -public int InversePairs(int[] nums) -{ +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) -{ +private void mergeSort(int[] nums, int l, int h) { if (h - l < 1) return; int m = l + (h - l) / 2; @@ -2291,8 +2327,7 @@ private void mergeSort(int[] nums, int l, int h) merge(nums, l, m, h); } -private void merge(int[] nums, int l, int m, int 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) @@ -2327,8 +2362,7 @@ private void merge(int[] nums, int l, int m, int h) 当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。 ```java -public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) -{ +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { ListNode l1 = pHead1, l2 = pHead2; while (l1 != l2) { l1 = (l1 == null) ? pHead2 : l1.next; @@ -2355,15 +2389,13 @@ Output: ## 解题思路 ```java -public int GetNumberOfK(int[] nums, int K) -{ +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) -{ +private int binarySearch(int[] nums, int K) { int l = 0, h = nums.length; while (l < h) { int m = l + (h - l) / 2; @@ -2388,14 +2420,12 @@ private int binarySearch(int[] nums, int K) private TreeNode ret; private int cnt = 0; -public TreeNode KthNode(TreeNode pRoot, int k) -{ +public TreeNode KthNode(TreeNode pRoot, int k) { inOrder(pRoot, k); return ret; } -private void inOrder(TreeNode root, int k) -{ +private void inOrder(TreeNode root, int k) { if (root == null || cnt >= k) return; inOrder(root.left, k); @@ -2419,8 +2449,7 @@ private void inOrder(TreeNode root, int k) ## 解题思路 ```java -public int TreeDepth(TreeNode root) -{ +public int TreeDepth(TreeNode root) { return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right)); } ``` @@ -2440,14 +2469,12 @@ public int TreeDepth(TreeNode root) ```java private boolean isBalanced = true; -public boolean IsBalanced_Solution(TreeNode root) -{ +public boolean IsBalanced_Solution(TreeNode root) { height(root); return isBalanced; } -private int height(TreeNode root) -{ +private int height(TreeNode root) { if (root == null || !isBalanced) return 0; int left = height(root.left); @@ -2473,8 +2500,7 @@ private int height(TreeNode root) diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 ```java -public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) -{ +public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) { int diff = 0; for (int num : nums) diff ^= num; @@ -2505,8 +2531,7 @@ public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) - 如果 sum < target,移动较小的元素,使 sum 变大一些。 ```java -public ArrayList FindNumbersWithSum(int[] array, int sum) -{ +public ArrayList FindNumbersWithSum(int[] array, int sum) { int i = 0, j = array.length - 1; while (i < j) { int cur = array[i] + array[j]; @@ -2539,8 +2564,7 @@ public ArrayList FindNumbersWithSum(int[] array, int sum) ## 解题思路 ```java -public ArrayList> FindContinuousSequence(int sum) -{ +public ArrayList> FindContinuousSequence(int sum) { ArrayList> ret = new ArrayList<>(); int start = 1, end = 2; int curSum = 3; @@ -2583,8 +2607,7 @@ public ArrayList> FindContinuousSequence(int sum) 正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。 ```java -public String ReverseSentence(String str) -{ +public String ReverseSentence(String str) { int n = str.length(); char[] chars = str.toCharArray(); int i = 0, j = 0; @@ -2599,14 +2622,12 @@ public String ReverseSentence(String str) return new String(chars); } -private void reverse(char[] c, int i, int j) -{ +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) -{ +private void swap(char[] c, int i, int j) { char t = c[i]; c[i] = c[j]; c[j] = t; @@ -2623,11 +2644,10 @@ private void swap(char[] c, int i, int j) ## 解题思路 -将 "abcXYZdef" 旋转左移三位,可以先将 "abc" 和 "XYZdef" 分别旋转,得到 "cbafedZYX",然后再把整个字符串旋转得到 "XYZdefabc"。 +先将 "abc" 和 "XYZdef" 分别翻转,得到 "cbafedZYX",然后再把整个字符串翻转得到 "XYZdefabc"。 ```java -public String LeftRotateString(String str, int n) -{ +public String LeftRotateString(String str, int n) { if (n >= str.length()) return str; char[] chars = str.toCharArray(); @@ -2637,14 +2657,12 @@ public String LeftRotateString(String str, int n) return new String(chars); } -private void reverse(char[] chars, int i, int j) -{ +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) -{ +private void swap(char[] chars, int i, int j) { char t = chars[i]; chars[i] = chars[j]; chars[j] = t; @@ -2662,8 +2680,7 @@ private void swap(char[] chars, int i, int j) ## 解题思路 ```java -public ArrayList maxInWindows(int[] num, int size) -{ +public ArrayList maxInWindows(int[] num, int size) { ArrayList ret = new ArrayList<>(); if (size > num.length || size < 1) return ret; @@ -2697,8 +2714,7 @@ public ArrayList maxInWindows(int[] num, int size) 空间复杂度:O(N2) ```java -public List> dicesSum(int n) -{ +public List> dicesSum(int n) { final int face = 6; final int pointNum = face * n; long[][] dp = new long[n + 1][pointNum + 1]; @@ -2725,8 +2741,7 @@ public List> dicesSum(int n) 空间复杂度:O(N) ```java -public List> dicesSum(int n) -{ +public List> dicesSum(int n) { final int face = 6; final int pointNum = face * n; long[][] dp = new long[2][pointNum + 1]; @@ -2764,8 +2779,7 @@ public List> dicesSum(int n) ## 解题思路 ```java -public boolean isContinuous(int[] nums) -{ +public boolean isContinuous(int[] nums) { if (nums.length < 5) return false; Arrays.sort(nums); @@ -2796,8 +2810,7 @@ public boolean isContinuous(int[] nums) 约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。 ```java -public int LastRemaining_Solution(int n, int m) -{ +public int LastRemaining_Solution(int n, int m) { if (n == 0) /* 特殊输入的处理 */ return -1; if (n == 1) /* 返回条件 */ @@ -2819,8 +2832,7 @@ public int LastRemaining_Solution(int n, int m) 使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。 ```java -public int maxProfit(int[] prices) -{ +public int maxProfit(int[] prices) { if (prices == null || prices.length == 0) return 0; int soFarMin = prices[0]; @@ -2850,8 +2862,7 @@ public int maxProfit(int[] prices) 以下实现中,递归的返回条件为 n <= 0,取非后就是 n > 0,递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。 ```java -public int Sum_Solution(int n) -{ +public int Sum_Solution(int n) { int sum = n; boolean b = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0); return sum; @@ -2873,8 +2884,7 @@ a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进 递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 ```java -public int Add(int a, int b) -{ +public int Add(int a, int b) { return b == 0 ? a : Add(a ^ b, (a & b) << 1); } ``` @@ -2890,8 +2900,7 @@ public int Add(int a, int b) ## 解题思路 ```java -public int[] multiply(int[] A) -{ +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++) /* 从左往右累乘 */ @@ -2923,8 +2932,7 @@ Output: ## 解题思路 ```java -public int StrToInt(String str) -{ +public int StrToInt(String str) { if (str == null || str.length() == 0) return 0; boolean isNegative = str.charAt(0) == '-'; diff --git a/notes/攻击技术.md b/notes/攻击技术.md new file mode 100644 index 00000000..c99071a0 --- /dev/null +++ b/notes/攻击技术.md @@ -0,0 +1,219 @@ + +* [一、跨站脚本攻击](#一跨站脚本攻击) +* [二、跨站请求伪造](#二跨站请求伪造) +* [三、SQL 注入攻击](#三sql-注入攻击) +* [四、拒绝服务攻击](#四拒绝服务攻击) +* [参考资料](#参考资料) + + + +# 一、跨站脚本攻击 + +## 概念 + +跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。 + +例如有一个论坛网站,攻击者可以在上面发布以下内容: + +```html + +``` + +之后该内容可能会被渲染成以下形式: + +```html +

+``` + +另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 + +## 危害 + +- 窃取用户的 Cookie 值 +- 伪造虚假的输入表单骗取个人信息 +- 显示伪造的文章或者图片 + +## 防范手段 + +### 1. 设置 Cookie 为 HttpOnly + +设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。 + +### 2. 过滤特殊字符 + +例如将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。 + +## 富文本编辑器 + +富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。 + +富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。 + +以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。 + +> [XSS 过滤在线测试](http://jsxss.com/zh/try.html) + +```html +

XSS Demo

+ +

+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist. +

+ +
+ + +
+ +
hello
+ +

+ http +

+ +

Features:

+
    +
  • Specifies HTML tags and their attributes allowed with whitelist
  • +
  • Handle any tags or attributes using custom function
  • +
+ + +``` + +```html +

XSS Demo

+ +

+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist. +

+ +<form> + <input type="text" name="q" value="test"> + <button id="submit">Submit</button> +</form> + +
hello
+ +

+ http +

+ +

Features:

+
    +
  • Specifies HTML tags and their attributes allowed with whitelist
  • +
  • Handle any tags or attributes using custom function
  • +
+ +<script type="text/javascript"> +alert(/xss/); +</script> +``` + +# 二、跨站请求伪造 + +## 概念 + +跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。 + +XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。 + +假如一家银行用以执行转账操作的 URL 地址如下: + +``` +http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。 +``` + +那么,一个恶意攻击者可以在另一个网站上放置如下代码: + +``` +。 +``` + +如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 + +这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 + +透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 + +## 防范手段 + +### 1. 检查 Referer 首部字段 + +Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 CSRF 攻击。 + +这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 + +### 2. 添加校验 Token + +在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。 + +### 3. 输入验证码 + +因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。 + +也可以要求用户输入验证码来进行校验。 + +# 三、SQL 注入攻击 + +## 概念 + +服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。 + +## 攻击原理 + +例如一个网站登录验证的 SQL 查询代码为: + +```sql +strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');" +``` + +如果填入以下内容: + +```sql +userName = "1' OR '1'='1"; +passWord = "1' OR '1'='1"; +``` + +那么 SQL 查询字符串为: + +```sql +strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');" +``` + +此时无需验证通过就能执行以下查询: + +```sql +strSQL = "SELECT * FROM users;" +``` + +## 防范手段 + +### 1. 使用参数化查询 + +Java 中的 PreparedStatement 是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。 + +```java +PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?"); +stmt.setString(1, userid); +stmt.setString(2, password); +ResultSet rs = stmt.executeQuery(); +``` + +### 2. 单引号转换 + +将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。 + +# 四、拒绝服务攻击 + +拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 + +分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 + +# 参考资料 + +- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC) +- [维基百科: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/notes/数据库系统原理.md b/notes/数据库系统原理.md index 1d493176..e7ef804b 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -44,10 +44,10 @@ ## 概念 -

- 事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 +

+ ## ACID ### 1. 原子性(Atomicity) @@ -70,18 +70,18 @@ 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 -可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 +可以通过数据库备份和恢复来实现,在系统发生崩溃时,使用备份的数据库进行数据恢复。 ---- 事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系: - 只有满足一致性,事务的执行结果才是正确的。 -- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时要只要能满足原子性,就一定能满足一致性。 +- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 - 在并发的情况下,多个事务并发执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 -- 事务满足持久化是为了能应对数据库奔溃的情况。 +- 事务满足持久化是为了能应对数据库崩溃的情况。 -

+

## AUTOCOMMIT @@ -123,7 +123,6 @@ T1 读取某个范围的数据,T2 在这个范围内插 ## 封锁粒度 -

MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 @@ -133,6 +132,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。 +

+ ## 封锁类型 ### 1. 读写锁 @@ -304,7 +305,11 @@ SELECT ... FOR UPDATE; # 五、多版本并发控制 -多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC;可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 +多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。 + +而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。 + +可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 ## 版本号 @@ -346,7 +351,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 ### 4. UPDATE -将当前系统版本号作为更新后的数据行快照的创建版本号,同时将当前系统版本号作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。 +将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。 ## 快照读与当前读 @@ -372,15 +377,19 @@ delete; # 六、Next-Key Locks -Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 +Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。 + +MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 ## Record Locks -锁定整个记录(行)。锁定的对象是记录的索引,而不是记录本身。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。 +锁定一个记录上的索引,而不是记录本身。 + +如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。 ## Gap Locks -锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。 +锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。 ```sql SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; @@ -388,28 +397,14 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ## Next-Key Locks -它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录,也锁定范围内的索引。在 user 中有以下记录: +它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定范围内的索引。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间: ```sql -| id | last_name | first_name | age | -|------|-------------|--------------|-------| -| 4 | stark | tony | 21 | -| 1 | tom | hiddleston | 30 | -| 3 | morgan | freeman | 40 | -| 5 | jeff | dean | 50 | -| 2 | donald | trump | 80 | -+------|-------------|--------------|-------+ -``` - -那么就需要锁定以下范围: - -```sql -(-∞, 21] -(21, 30] -(30, 40] -(40, 50] -(50, 80] -(80, ∞) +(negative infinity, 10] +(10, 11] +(11, 13] +(13, 20] +(20, positive infinity) ``` # 七、关系数据库设计理论 @@ -420,7 +415,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; 如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 -对于 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖; +对于 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖。 对于 A->B,B->C,则 A->C 是一个传递函数依赖。 @@ -452,7 +447,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ### 1. 第一范式 (1NF) -属性不可分; +属性不可分。 ### 2. 第二范式 (2NF) @@ -491,7 +486,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门 有以下函数依赖: -- Sno -> Sname, Sdept, Mname +- Sno -> Sname, Sdept - Sdept -> Mname 关系-2 diff --git a/notes/构建工具.md b/notes/构建工具.md new file mode 100644 index 00000000..c0e4acd5 --- /dev/null +++ b/notes/构建工具.md @@ -0,0 +1,147 @@ + +* [一、什么是构建工具](#一什么是构建工具) +* [二、Java 主流构建工具](#二java-主流构建工具) +* [三、Maven](#三maven) + + + +# 一、什么是构建工具 + +构建工具是用于构建项目的自动化工具,主要包含以下工作: + +## 依赖管理 + +不再需要手动导入 Jar 依赖包,并且可以自动处理依赖关系,也就是说某个依赖如果依赖于其它依赖,构建工具可以帮助我们自动处理这种依赖管理。 + +## 运行单元测试 + +不再需要在项目代码中添加测试代码,从而污染项目代码。 + +## 将源代码转化为可执行文件 + +包含预处理、编译、汇编、链接等步骤。 + +## 将可执行文件进行打包 + +不再需要使用 IDE 将应用程序打包成 Jar 包。 + +## 发布到生产服务器上 + +不再需要通过 FTP 将 Jar 包上传到服务器上。 + +参考资料: + +- [What is a build tool?](https://stackoverflow.com/questions/7249871/what-is-a-build-tool) + +# 二、Java 主流构建工具 + +主要包括 Ant、Maven 和 Gradle。 + +

+ +Gradle 和 Maven 的区别是,它使用 Groovy 这种特定领域语言(DSL)来管理构建脚本,而不再使用 XML 这种标记性语言。因为项目如果庞大的话,XML 很容易就变得臃肿。 + +例如要在项目中引入 Junit,Maven 的代码如下: + +```xml + + + 4.0.0 + + jizg.study.maven.hello + hello-first + 0.0.1-SNAPSHOT + + + + junit + junit + 4.10 + test + + + +``` + +而 Gradle 只需要几行代码: + +```java +dependencies { + testCompile "junit:junit:4.10" +} +``` + +参考资料: + +- [Java Build Tools Comparisons: Ant vs Maven vs Gradle](https://programmingmitra.blogspot.com/2016/05/java-build-tools-comparisons-ant-vs.html) +- [maven 2 gradle](http://sagioto.github.io/maven2gradle/) +- [新一代构建工具 gradle](https://www.imooc.com/learn/833) + +# 三、Maven + +## 概述 + +提供了项目对象模型(POM)文件来管理项目的构建。 + +## 仓库 + +仓库的搜索顺序为:本地仓库、中央仓库、远程仓库。 + +- 本地仓库用来存储项目的依赖库; +- 中央仓库是下载依赖库的默认位置; +- 远程仓库,因为并非所有的库存储在中央仓库,或者中央仓库访问速度很慢,远程仓库是中央仓库的补充。 + +## POM + +POM 代表项目对象模型,它是一个 XML 文件,保存在项目根目录的 pom.xml 文件中。 + +```xml + + junit + junit + 4.12 + test + +``` + +[groupId, artifactId, version, packaging, classfier] 称为一个项目的坐标,其中 groupId、artifactId、version 必须定义,packaging 可选(默认为 Jar),classfier 不能直接定义的,需要结合插件使用。 + +- groupId:项目组 Id,必须全球唯一; +- artifactId:项目 Id,即项目名; +- version:项目版本; +- packaging:项目打包方式。 + +## 依赖原则 + +### 依赖路径最短优先原则 + +```html +A -> B -> C -> X(1.0) +A -> D -> X(2.0) +``` +由于 X(2.0) 路径最短,所以使用 X(2.0)。 + +### 声明顺序优先原则 + +```html +A -> B -> X(1.0) +A -> C -> X(2.0) +``` + +在 POM 中最先声明的优先,上面的两个依赖如果先声明 B,那么最后使用 X(1.0)。 + +### 覆写优先原则 + +子 POM 内声明的依赖优先于父 POM 中声明的依赖。 + +## 解决依赖冲突 + +找到 Maven 加载的 Jar 包版本,使用 `mvn dependency:tree` 查看依赖树,根据依赖原则来调整依赖在 POM 文件的声明顺序。 + +参考资料: + +- [POM Reference](http://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification) + + + diff --git a/notes/正则表达式.md b/notes/正则表达式.md index c6c21f2c..c713da63 100644 --- a/notes/正则表达式.md +++ b/notes/正则表达式.md @@ -23,11 +23,11 @@ # 二、匹配单个字符 -正则表达式一般是区分大小写的,但是也有些实现是不区分。 - **.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符; -**\\** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。 +**.** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。 + +正则表达式一般是区分大小写的,但是也有些实现是不区分。 **正则表达式** @@ -43,11 +43,11 @@ My **name** is Zheng. **[ ]** 定义一个字符集合; -0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间只能用在 [ ] 之间。 +0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间在 [ ] 中使用。 -**-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符; +**-** 只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符; -**^** 在 [ ] 字符集合中是取非操作。 +**^** 在 [ ] 中是取非操作。 **应用** @@ -78,9 +78,9 @@ abc[^0-9] | \t | 制表符 | | \v | 垂直制表符 | -\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n ;\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行; +\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n。 -. 是元字符,前提是没有对它们进行转义;f 和 n 也是元字符,但是前提是对它们进行了转义。 +\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行; ## 匹配特定的字符类别 @@ -105,11 +105,13 @@ abc[^0-9] | \s | 任何一个空白字符,等价于 [\f\n\r\t\v] | | \S | 对 \s 取非 | -\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10 ,等价于 \n,也就是它会匹配 \n 。 +\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10,等价于 \n。 # 五、重复匹配 -**\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。 +- **\+** 匹配 1 个或者多个字符 +- **\** * 匹配 0 个或者多个 +- **?** 匹配 0 个或者 1 个 **应用** @@ -127,16 +129,11 @@ abc[^0-9] **abc.def@qq.com** -为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。 +- **{n}** 匹配 n 个字符 +- **{m, n}** 匹配 m\~n 个字符 +- **{m,}** 至少匹配 m 个字符 -``` -[\w.]+@\w+\.\w+ -[\w.]+@[\w]+[\.][\w]+ -``` - -**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符; - -\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。 +\* 和 + 都是贪婪型元字符,会匹配最多的内容。在后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。 **正则表达式** @@ -220,7 +217,9 @@ a.+c **应用** -匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: +匹配 IP 地址。 + +IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: - 一位数字 - 不以 0 开头的两位数字 diff --git a/notes/消息队列.md b/notes/消息队列.md new file mode 100644 index 00000000..74155802 --- /dev/null +++ b/notes/消息队列.md @@ -0,0 +1,80 @@ + +* [一、消息模型](#一消息模型) + * [点对点](#点对点) + * [发布/订阅](#发布订阅) +* [二、使用场景](#二使用场景) + * [异步处理](#异步处理) + * [流量削锋](#流量削锋) + * [应用解耦](#应用解耦) +* [三、可靠性](#三可靠性) + * [发送端的可靠性](#发送端的可靠性) + * [接收端的可靠性](#接收端的可靠性) + + + +# 一、消息模型 + +## 点对点 + +消息生产者向消息队列中发送了一个消息之后,只能被一个消费者消费一次。 + +

+ +## 发布/订阅 + +消息生产者向频道发送一个消息之后,多个消费者可以从该频道订阅到这条消息并消费。 + +

+ +发布与订阅模式和观察者模式有以下不同: + +- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 +- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息。 + +

+ +参考: + +- [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 df5a8049..cb5d4dbe 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -1623,10 +1623,10 @@ private List keys(Node x, Key l, Key h) { ## 2-3 查找树 -

- 2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 +

+ ### 1. 插入操作 插入操作和 BST 的插入操作有很大区别,BST 的插入操作是先进行一次未命中的查找,然后再将节点插入到对应的空链接上。但是 2-3 查找树如果也这么做的话,那么就会破坏了平衡性。它是将新节点插入到叶子节点上。 @@ -2042,24 +2042,26 @@ public class SparseVector { ## 汉诺塔 +

+ 这是一个经典的递归问题,分为三步求解: -1. 将 n-1 个圆盘从 from -> buffer -2. 将 1 个圆盘从 from -> to -3. 将 n-1 个圆盘从 buffer -> to - -如果只有一个圆盘,那么只需要进行一次移动操作。 - -从上面的讨论可以知道,n 圆盘需要移动 (n-1)+1+(n-1) = 2n-1 次。 - -

+- 将 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) { @@ -2105,7 +2107,7 @@ from H1 to H3 生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到根节点,根节点代表的字符的编码就是这个路径编码。 -

+

```java public class Huffman { diff --git a/notes/系统设计基础.md b/notes/系统设计基础.md new file mode 100644 index 00000000..40f0ca9d --- /dev/null +++ b/notes/系统设计基础.md @@ -0,0 +1,104 @@ + +* [一、性能](#一性能) +* [二、伸缩性](#二伸缩性) +* [三、扩展性](#三扩展性) +* [四、可用性](#四可用性) +* [五、安全性](#五安全性) + + + +# 一、性能 + +## 性能指标 + +### 1. 响应时间 + +指从某个请求从发出到接收到响应消耗的时间。 + +在对响应时间进行测试时,通常采用重复请求方式,然后计算平均响应时间。 + +### 2. 吞吐量 + +指系统在单位时间内可以处理的请求数量,通常使用每秒的请求数来衡量。 + +### 3. 并发用户数 + +指系统能同时处理的并发用户请求数量。 + +在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s,那么平均响应时间应该为 0.01s。 + +目前的大型系统都支持多线程来处理并发请求,多线程能够提高吞吐量以及缩短响应时间,主要有两个原因: + +- 多 CPU +- IO 等待时间 + +使用 IO 多路复用等方式,系统在等待一个 IO 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。 + +并发用户数不是越高越好,因为如果并发用户数太高,系统来不及处理这么多的请求,会使得过多的请求需要等待,那么响应时间就会大大提高。 + +## 性能优化 + +### 1. 集群 + +将多台服务器组成集群,使用负载均衡将请求转发到集群中,避免单一服务器的负载压力过大导致性能降低。 + +### 2. 缓存 + +缓存能够提高性能的原因如下: + +- 缓存数据通常位于内存等介质中,这种介质对于读操作特别快; +- 缓存数据可以位于靠近用户的地理位置上; +- 可以将计算结果进行缓存,从而避免重复计算。 + +### 3. 异步 + +某些流程可以将操作转换为消息,将消息发送到消息队列之后立即返回,之后这个操作会被异步处理。 + +# 二、伸缩性 + +指不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。 + +## 伸缩性与性能 + +如果系统存在性能问题,那么单个用户的请求总是很慢的; + +如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。 + +## 实现伸缩性 + +应用服务器只要不具有状态,那么就可以很容易地通过负载均衡器向集群中添加新的服务器。 + +关系型数据库的伸缩性通过 Sharding 来实现,将数据按一定的规则分布到不同的节点上,从而解决单台存储服务器存储空间限制。 + +对于非关系型数据库,它们天生就是为海量数据而诞生,对伸缩性的支持特别好。 + +# 三、扩展性 + +指的是添加新功能时对现有系统的其它应用无影响,这就要求不同应用具备低耦合的特点。 + +实现可扩展主要有两种方式: + +- 使用消息队列进行解耦,应用之间通过消息传递的方式进行通信; +- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以用过调用可复用的服务来实现业务逻辑,对其它产品没有影响。 + +# 四、可用性 + +## 冗余 + +保证高可用的主要手段是使用冗余,当某个服务器故障时就请求其它服务器。 + +应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上不会对用户有任何影响。 + +存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器,这个过程称为切换。 + +## 监控 + +对 CPU、内存、磁盘、网络等系统负载信息进行监控,当某个数据达到一定阈值时通知运维人员,从而在系统发生故障之前及时发现问题。 + +## 服务降级 + +服务器降级是系统为了应对大量的请求,主动关闭部分功能,从而保证核心功能可用。 + +# 五、安全性 + +要求系统的应对各种攻击手段时能够有可靠的应对措施。 diff --git a/notes/缓存.md b/notes/缓存.md new file mode 100644 index 00000000..449598ff --- /dev/null +++ b/notes/缓存.md @@ -0,0 +1,270 @@ + +* [一、缓存特征](#一缓存特征) +* [二、LRU](#二lru) +* [三、缓存位置](#三缓存位置) +* [四、CDN](#四cdn) +* [五、缓存问题](#五缓存问题) +* [六、数据分布](#六数据分布) +* [七、一致性哈希](#七一致性哈希) + + + +# 一、缓存特征 + +## 命中率 + +当某个请求能够通过访问缓存而得到响应时,称为缓存命中。 + +缓存命中率越高,缓存的利用率也就越高。 + +## 最大空间 + +缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。 + +当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。 + +## 淘汰策略 + +- FIFO(First In First Out):先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使最先进入的数据(最晚的数据)被淘汰。 + +- LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最远的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。 + +参考资料: + +- [缓存那些事](https://tech.meituan.com/cache_about.html) + +# 二、LRU + +以下是一个基于 双向队列 + HashMap 的 LRU 算法实现,对算法的解释如下: + +- 最基本的思路是当访问某个节点时,将其从原来的位置删除,并重新插入到链表头部,这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就删除链表尾部的节点。 +- 为了使删除操作时间复杂度为 O(1),那么就不能采用遍历的方式找到某个节点。HashMap 存储这 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。 + +```java +public class LRU implements Iterable { + + private Node head; + private Node tail; + private HashMap map; + private int maxSize; + + private class Node { + + Node pre; + Node next; + K k; + V v; + + public Node(K k, V v) { + this.k = k; + this.v = v; + } + } + + public LRU(int maxSize) { + + this.maxSize = maxSize; + this.map = new HashMap<>(maxSize * 4 / 3); + + head = new Node(null, null); + tail = new Node(null, null); + + head.next = tail; + tail.pre = head; + } + + public V get(K key) { + + if (!map.containsKey(key)) { + return null; + } + + Node node = map.get(key); + unlink(node); + appendHead(node); + + return node.v; + } + + public void put(K key, V value) { + + if (map.containsKey(key)) { + Node node = map.get(key); + unlink(node); + } + + Node node = new Node(key, value); + map.put(key, node); + appendHead(node); + + if (map.size() > maxSize) { + Node toRemove = removeTail(); + map.remove(toRemove); + } + } + + private void unlink(Node node) { + Node pre = node.pre; + node.pre = node.next; + node.next = pre; + } + + private void appendHead(Node node) { + node.next = head.next; + head.next = node; + } + + private Node removeTail() { + Node node = tail.pre; + node.pre = tail; + return node; + } + + @Override + public Iterator iterator() { + + return new Iterator() { + + private Node cur = head.next; + + @Override + public boolean hasNext() { + return cur != tail; + } + + @Override + public K next() { + Node node = cur; + cur = cur.next; + return node.k; + } + }; + } +} +``` + +# 三、缓存位置 + +## 浏览器 + +当 HTTP 响应允许进行缓存时,浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存。 + +## ISP + +网络服务提供商(ISP)是网络访问的第一跳,通过将数据缓存在 ISP 中能够大大提高用户的访问速度。 + +## 反向代理 + +反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求时就可以直接使用缓存进行响应。 + +## 本地缓存 + +使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。 + +## 分布式缓存 + +使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。 + +相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。 + +不仅如此,服务器集群都可以访问分布式缓存。而本地缓存需要在服务器集群之间进行同步,实现和性能开销上都非常大。 + +## 数据库缓存 + +MySQL 等数据库管理系统具有自己的查询缓存机制来提高 SQL 查询效率。 + +# 四、CDN + +内容分发网络(Content distribution network,CDN)是一种通过互连的网络系统,利用更靠近用户的服务器更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。 + +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/) + +# 五、缓存问题 + +## 缓存穿透 + +指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。 + +解决方案: + +- 对这些不存在的数据缓存一个空数据; +- 对这类请求进行过滤。 + +## 缓存雪崩 + +指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都去到达数据库。 + +在存在缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。 + +解决方案: + +- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现; +- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。 +- 也可以在进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。 + +## 缓存一致性 + +缓存一致性要求数据更新的同时缓存数据也能够实时更新。 + +解决方案: + +- 在数据更新的同时立即去更新缓存; +- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。 + +要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。 + +# 六、数据分布 + +## 哈希分布 + +哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。 + +传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。 + +## 顺序分布 + +将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。 + +顺序分布相比于哈希分布的主要优点如下: + +- 能保持数据原有的顺序; +- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。 + +参考资料: + +- 大规模分布式存储系统 + +# 七、一致性哈希 + +Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据失效的问题。 + +## 基本原理 + +将哈希空间 [0, 2n-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。 + +

+ +一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。 + +

+ +## 虚拟节点 + +上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。 + +数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得大,那么虚拟节点在哈希环上分布的均匀性就会比原来的真是节点好,从而使得数据分布也更加均匀。 + +参考资料: + +- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849) diff --git a/notes/计算机操作系统.md b/notes/计算机操作系统.md index 4a4077d3..6bfb559c 100644 --- a/notes/计算机操作系统.md +++ b/notes/计算机操作系统.md @@ -1,7 +1,7 @@ * [一、概述](#一概述) - * [操作系统基本特征](#操作系统基本特征) - * [操作系统基本功能](#操作系统基本功能) + * [基本特征](#基本特征) + * [基本功能](#基本功能) * [系统调用](#系统调用) * [大内核和微内核](#大内核和微内核) * [中断分类](#中断分类) @@ -23,6 +23,7 @@ * [段页式](#段页式) * [分页与分段的比较](#分页与分段的比较) * [五、设备管理](#五设备管理) + * [磁盘结构](#磁盘结构) * [磁盘调度算法](#磁盘调度算法) * [六、链接](#六链接) * [编译系统](#编译系统) @@ -35,7 +36,7 @@ # 一、概述 -## 操作系统基本特征 +## 基本特征 ### 1. 并发 @@ -63,7 +64,7 @@ 异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 -## 操作系统基本功能 +## 基本功能 ### 1. 进程管理 @@ -152,19 +153,27 @@ Linux 的系统调用主要有以下这些: 一个进程中可以有多个线程,它们共享进程资源。 +QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 +

### 3. 区别 -- 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 +(一)拥有资源 -- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。 +进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 -- 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 +(二)调度 -- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。 +线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。 -举例:QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 +(三)系统开销 + +由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 + +(四)通信方面 + +进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。 ## 进程状态的切换 @@ -211,7 +220,10 @@ Linux 的系统调用主要有以下这些: 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 -时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 +时间片轮转算法的效率和时间片的大小有很大关系: + +- 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 +- 而如果时间片过长,那么实时性就不能得到保证。

@@ -293,7 +305,7 @@ void P2() { 为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。 -注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,也就无法执行 up(empty) 操作,empty 永远都为 0,那么生产者和消费者就会一直等待下去,造成死锁。 +注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。 ```c #define N 100 @@ -303,7 +315,7 @@ semaphore empty = N; semaphore full = 0; void producer() { - while(TRUE){ + while(TRUE) { int item = produce_item(); down(&empty); down(&mutex); @@ -314,7 +326,7 @@ void producer() { } void consumer() { - while(TRUE){ + while(TRUE) { down(&full); down(&mutex); int item = remove_item(); @@ -352,7 +364,7 @@ end monitor; 管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。 - **使用管程实现生成者-消费者问题**
+ **使用管程实现生产者-消费者问题**
```pascal // 管程 @@ -531,7 +543,7 @@ int pipe(int fd[2]); 它具有以下限制: -- 只支持半双工通信(单向传输); +- 只支持半双工通信(单向交替传输); - 只能在父子进程中使用。

@@ -696,7 +708,7 @@ FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 -为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 +为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。 @@ -704,12 +716,11 @@ FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户 ## 分页系统地址映射 -- 内存管理单元(MMU):管理着地址空间和物理内存的转换。 -- 页表(Page table):页(地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。 +内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。 -下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。因此对于虚拟地址(0010 000000000100),前 4 位是用来存储页面号,而后 12 位存储在页中的偏移量。 +下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位,也就是存储页面号,剩下 12 个比特位存储偏移量。 -(0010 000000000100)根据前 4 位得到页号为 2,读取表项内容为(110 1),它的前 3 为为页框号,最后 1 位表示该页在内存中。最后映射得到物理内存地址为(110 000000000100)。 +例如对于虚拟地址(0010 000000000100),前 4 位是存储页面号 2,读取表项内容为(110 1)。该页在内存中,并且页框的地址为 (110 000000000100)。

@@ -817,11 +828,22 @@ FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问 # 五、设备管理 +## 磁盘结构 + +- 盘面(Platter):一个磁盘有多个盘面; +- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道; +- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小; +- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写); +- 制动手臂(Actuator arm):用于在磁道之间移动磁头; +- 主轴(Spindle):使整个盘面转动。 + +

+ ## 磁盘调度算法 读写一个磁盘块的时间的影响因素有: -- 旋转时间(主轴旋转磁盘,使得磁头移动到适当的扇区上) +- 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上) - 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上) - 实际的数据传输时间 @@ -922,8 +944,9 @@ gcc -o hello hello.c - 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/) -- [进程间的几种通信方式](http://blog.csdn.net/yufaw/article/details/7409596) - [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/notes/计算机网络.md b/notes/计算机网络.md index 1c38cc0a..e80ba7f8 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -37,16 +37,16 @@ * [TCP 首部格式](#tcp-首部格式) * [TCP 的三次握手](#tcp-的三次握手) * [TCP 的四次挥手](#tcp-的四次挥手) - * [TCP 滑动窗口](#tcp-滑动窗口) * [TCP 可靠传输](#tcp-可靠传输) + * [TCP 滑动窗口](#tcp-滑动窗口) * [TCP 流量控制](#tcp-流量控制) * [TCP 拥塞控制](#tcp-拥塞控制) * [六、应用层](#六应用层) * [域名系统](#域名系统) * [文件传送协议](#文件传送协议) + * [动态主机配置协议](#动态主机配置协议) * [远程登录协议](#远程登录协议) * [电子邮件协议](#电子邮件协议) - * [动态主机配置协议](#动态主机配置协议) * [常用端口](#常用端口) * [Web 页面请求过程](#web-页面请求过程) * [参考资料](#参考资料) @@ -91,7 +91,7 @@ 每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。 -考虑在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。 +在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。 ## 时延 @@ -109,11 +109,11 @@ ### 2. 传播时延 -电磁波在信道中传播一定的距离需要花费的时间,电磁波传播速度接近光速。 +电磁波在信道中传播所需要花费的时间,电磁波传播的速度接近光速。

-其中 l 表示信道长度,v 表示电磁波在信道上的传播速率。 +其中 l 表示信道长度,v 表示电磁波在信道上的传播速度。 ### 3. 处理时延 @@ -135,33 +135,25 @@ - **网络层** :为主机间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。 -- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的节点提供服务。数据链路层把网络层传来的分组封装成帧。 +- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供服务。数据链路层把网络层传下来的分组封装成帧。 - **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 -### 2. 七层协议 +### 2. OSI 其中表示层和会话层用途如下: -- **表示层** :数据压缩、加密以及数据描述。这使得应用程序不必担心在各台主机中表示/存储的内部格式不同的问题。 +- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必担心在各台主机中数据内部格式不同的问题。 - **会话层** :建立及管理会话。 五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。 -### 3. 数据在各层之间的传递过程 - -在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。 - -路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。 - -

- -### 4. TCP/IP +### 3. TCP/IP 它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。 -现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。 +TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。

@@ -169,6 +161,14 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中

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

+ # 二、物理层 ## 通信方式 @@ -314,7 +314,7 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。 -可以按照网络拓结构扑对局域网进行分类: +可以按照网络拓扑结构对局域网进行分类:

@@ -322,9 +322,9 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 以太网是一种星型拓扑结构局域网。 -早期使用集线器进行连接。集线器是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离。之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。 +早期使用集线器进行连接,集线器是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。 -目前以太网使用交换机替代了集线器。交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。 +目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。 以太网帧格式: @@ -337,7 +337,9 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 ## 交换机* -交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。正是由于这种自学习能力,因此交换机是一种即插即可即用设备,不需要网络管理员手动配置交换表内容。 +交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。 + +正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。 下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧。主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1,就发送数据帧到接口 1,同时交换机添加主机 B 到接口 3 的映射。 @@ -345,9 +347,11 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 ## 虚拟局域网 -虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 +虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。 -使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊端口被设置为干线端口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。 +例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 + +使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。

@@ -411,12 +415,14 @@ IP 地址 ::= {< 网络号 >, < 主机号 >} ### 2. 子网划分 -通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。注意,外部网络看不到子网的存在。 +通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。 IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >} 要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。 +注意,外部网络看不到子网的存在。 + ### 3. 无分类 无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。 @@ -441,7 +447,7 @@ ARP 实现由 IP 地址得到 MAC 地址。

-每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。 +每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。 如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。 @@ -461,12 +467,14 @@ ICMP 报文分为差错报告报文和询问报文。 Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 -Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报。 +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 终点不可达差错报告报文。 @@ -614,11 +622,11 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。 - 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 -- A 向 B 发送连接请求报文段,SYN=1,ACK=0,选择一个初始的序号 x。 +- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。 -- B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 +- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 -- A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 +- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 - B 收到 A 的确认后,连接建立。 @@ -634,11 +642,11 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。 以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 -- A 发送连接释放报文段,FIN=1。 +- A 发送连接释放报文,FIN=1。 - B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。 -- 当 B 不再需要连接时,发送连接释放请求报文段,FIN=1。 +- 当 B 不再需要连接时,发送连接释放报文,FIN=1。 - A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。 @@ -652,19 +660,9 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。 客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由: -- 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段,那么就会重新发送连接释放请求报文段,A 等待一段时间就是为了处理这种情况的发生。 +- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。 -- 等待一段时间是为了让本连接持续时间内所产生的所有报文段都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文段。 - -## TCP 滑动窗口 - -

- -窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 - -发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 - -接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {32, 33} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 +- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。 ## TCP 可靠传输 @@ -680,6 +678,16 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文 其中 RTTd 为偏差。 +## TCP 滑动窗口 + +窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 + +发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 + +接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 + +

+ ## TCP 流量控制 流量控制是为了控制发送方发送速率,保证接收方来得及接收。 @@ -717,7 +725,9 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。 -在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。 +在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 + +慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。

@@ -725,23 +735,23 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 ## 域名系统 -DNS 是一个分布式数据库,提供了主机名和 IP 地址之间的转换。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。 +DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。 -域名具有层次结构,从上到下依次为:根域名、顶级域名、第二级域名。 +域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。

DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输: -- 因为 UDP 最大只支持 512 字节的数据,如果返回的响应超过的 512 字节就改用 TCP 进行传输。 -- 区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据,区域传送需要使用 TCP 进行传输。 +- 如果返回的响应超过的 512 字节就改用 TCP 进行传输(UDP 最大只支持 512 字节的数据)。 +- 区域传送需要使用 TCP 进行传输(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。 ## 文件传送协议 FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件: -- 控制连接:服务器以打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。 -- 数据连接:用来传送一个文件。 +- 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。 +- 数据连接:用来传送一个文件数据。 根据数据连接是否是服务器端主动建立,FTP 有主动和被动两种模式: @@ -755,6 +765,21 @@ FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件: 主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。 +## 动态主机配置协议 + +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 用于登录到远程主机上,并且远程主机上的输出也会返回。 @@ -769,34 +794,19 @@ TELNET 可以适应许多计算机和操作系统的差异,例如不同操作

-### 1. POP3 - -POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 - -### 2. IMAP - -IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 - -### 3. SMTP +### 1. SMTP SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。

-## 动态主机配置协议 +### 2. POP3 -DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。 +POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 -DHCP 工作过程如下: +### 3. IMAP -1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。 -2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。 -3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。 -4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。 - -

- -如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。 +IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 ## 常用端口 @@ -879,7 +889,9 @@ DHCP 工作过程如下: - 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) +- [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/) diff --git a/notes/设计模式.md b/notes/设计模式.md index ae44213e..b8f4dc9b 100644 --- a/notes/设计模式.md +++ b/notes/设计模式.md @@ -98,7 +98,9 @@ public static synchronized Singleton getUniqueInstance() { (三)饿汉式-线程安全 -线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的优势。 +线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。 + +但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。 ```java private static Singleton uniqueInstance = new Singleton(); @@ -106,7 +108,7 @@ private static Singleton uniqueInstance = new Singleton(); (四)双重校验锁-线程安全 -uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行。也就是说,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。 +uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。 双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。 @@ -131,7 +133,7 @@ public class Singleton { } ``` -考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句,只是先后的问题,也就是说会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。 +考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 `uniqueInstance = new Singleton();` 这条语句,只是先后的问题,那么就会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。 ```java if (uniqueInstance == null) { @@ -157,7 +159,7 @@ uniqueInstance 采用 volatile 关键字修饰也是很有必要的。`uniqueIns 这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。 -```source-java +```java public class Singleton { private Singleton() { @@ -299,7 +301,7 @@ public class Client { ### 意图 -定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化推迟到子类。 +定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类。 ### 类图 @@ -1851,11 +1853,7 @@ No gumball dispensed ### 与状态模式的比较 -状态模式的类图和策略模式类似,并且都是能够动态改变对象的行为。 - -但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。 - -所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。 +状态模式的类图和策略模式类似,并且都是能够动态改变对象的行为。但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。所谓的状态转移,是指 Context 在运行过程中由于一些条件发生改变而使得 State 对象发生改变,注意必须要是在运行过程中。 状态模式主要是用来解决状态转移的问题,当状态发生转移了,那么 Context 对象就会改变它的行为;而策略模式主要是用来封装一组可以互相替代的算法族,并且可以根据需要动态地去替换 Context 使用的算法。 @@ -1969,7 +1967,7 @@ public abstract class CaffeineBeverage { ``` ```java -public class Coffee extends CaffeineBeverage{ +public class Coffee extends CaffeineBeverage { @Override void brew() { System.out.println("Coffee.brew"); @@ -1983,7 +1981,7 @@ public class Coffee extends CaffeineBeverage{ ``` ```java -public class Tea extends CaffeineBeverage{ +public class Tea extends CaffeineBeverage { @Override void brew() { System.out.println("Tea.brew"); @@ -2238,7 +2236,7 @@ Number of items: 6 ### 意图 -使用什么都不做的空对象来替代 NULL。 +使用什么都不做的空对象来代替 NULL。 一个方法返回 NULL,意味着方法的调用端需要去检查返回值是否是 NULL,这么做会导致非常多的冗余的检查代码。并且如果某一个调用端忘记了做这个检查返回值,而直接使用返回的对象,那么就有可能抛出空指针异常。 @@ -2393,7 +2391,7 @@ public abstract class TV { ``` ```java -public class Sony extends TV{ +public class Sony extends TV { @Override public void on() { System.out.println("Sony.on()"); @@ -2412,7 +2410,7 @@ public class Sony extends TV{ ``` ```java -public class RCA extends TV{ +public class RCA extends TV { @Override public void on() { System.out.println("RCA.on()"); @@ -2551,9 +2549,6 @@ public abstract class Component { ``` ```java -import java.util.ArrayList; -import java.util.List; - public class Composite extends Component { private List child; @@ -2659,7 +2654,7 @@ Composite:root ### 类图 -装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。 +装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。

@@ -2770,7 +2765,7 @@ public class Client { ### 实现 -观看电影需要操作很多电器,使用外观模式可以实现一键看电影功能。 +观看电影需要操作很多电器,使用外观模式实现一键看电影功能。 ```java public class SubSystem { @@ -2811,7 +2806,7 @@ public class Client { ### 设计原则 -最少知识原则:只和你的密友谈话。也就是客户对象所需要交互的对象应当尽可能少。 +最少知识原则:只和你的密友谈话。也就是说客户对象所需要交互的对象应当尽可能少。 ## 6. 享元(Flyweight) @@ -2822,8 +2817,8 @@ public class Client { ### 类图 - Flyweight:享元对象 -- IntrinsicState:内部状态,相同的项元对象共享 -- ExtrinsicState:外部状态 +- IntrinsicState:内部状态,享元对象共享内部状态 +- ExtrinsicState:外部状态,每个享元对象的外部状态不同

@@ -2854,8 +2849,6 @@ public class ConcreteFlyweight implements Flyweight { ``` ```java -import java.util.HashMap; - public class FlyweightFactory { private HashMap flyweights = new HashMap<>(); diff --git a/notes/重构.md b/notes/重构.md index 9a3242ba..b5a834e0 100644 --- a/notes/重构.md +++ b/notes/重构.md @@ -107,6 +107,7 @@ * [10. 塑造模板函数](#10-塑造模板函数) * [11. 以委托取代继承](#11-以委托取代继承) * [12. 以继承取代委托](#12-以继承取代委托) +* [重构练习](#重构练习) * [参考资料](#参考资料) @@ -1415,6 +1416,10 @@ public Manager(String name, String id, int grade) { 让委托类继承受托类。 +# 重构练习 + +- [Refactoring Kata](https://github.com/aikin/refactoring-kata) + # 参考资料 - MartinFowler, 福勒, 贝克, 等. 重构: 改善既有代码的设计 [M]. 电子工业出版社, 2011. diff --git a/notes/集群.md b/notes/集群.md new file mode 100644 index 00000000..717ae637 --- /dev/null +++ b/notes/集群.md @@ -0,0 +1,206 @@ + +* [一、负载均衡](#一负载均衡) + * [算法实现](#算法实现) + * [转发实现](#转发实现) +* [二、集群下的 Session 管理](#二集群下的-session-管理) + * [Sticky Session](#sticky-session) + * [Session Replication](#session-replication) + * [Session Server](#session-server) + + + +# 一、负载均衡 + +集群中的应用服务器通常被设计成无状态,用户可以请求任何一个节点(应用服务器)。 + +负载均衡器会根据集群中每个节点的负载情况,将用户请求转发到合适的节点上。 + +负载均衡器可以用来实现高可用以及伸缩性: + +- 高可用:当某个节点故障时,负载均衡器不会将用户请求转发到该节点上,从而保证所有服务持续可用; +- 伸缩性:可以很容易地添加移除节点。 + +负载均衡运行过程包含两个部分: + +1. 根据负载均衡算法得到请求转发的节点; +2. 将请求进行转发; + +## 算法实现 + +### 1. 轮询(Round Robin) + +轮询算法把每个请求轮流发送到每个服务器上。 + +下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。 + +

+ +该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。 + +

+ +### 2. 加权轮询(Weighted Round Robbin) + +加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值,性能高的服务器分配更高的权值。 + +例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。 + +

+ +### 3. 最少连接(least Connections) + +由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。 + +例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。 + +

+ +最少连接算法就是将请求发送给当前最少连接数的服务器上。 + +例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。 + +

+ +### 4. 加权最少连接(Weighted Least Connection) + +在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。 + +

+ +### 5. 随机算法(Random) + +把请求随机发送到服务器上。 + +和轮询算法类似,该算法比较适合服务器性能差不多的场景。 + +

+ +### 6. 源地址哈希法 (IP Hash) + +源地址哈希通过对客户端 IP 计算哈希值之后,再对服务器数量取模得到目标服务器的序号。 + +可以保证同一 IP 的客户端的请求会转发到同一台服务器上,用来实现会话粘滞(Sticky Session) + +

+ +## 转发实现 + +### 1. HTTP 重定向 + +HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服务器的 IP 地址之后,将该地址写入 HTTP 重定向报文中,状态码为 302。客户端收到重定向报文之后,需要重新向服务器发起请求。 + +缺点: + +- 需要两次请求,因此访问延迟比较高; +- HTTP 负载均衡器处理能力有限,会限制集群的规模。 + +该负载均衡转发的缺点比较明显,实际场景中很少使用它。 + +

+ +### 2. DNS 域名解析 + +在 DNS 解析域名的同时使用负载均衡算法计算服务器地址。 + +优点: + +- DNS 能够根据地理位置进行域名解析,返回离用户最近的服务器 IP 地址。 + +缺点: + +- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效。 + +大型网站基本使用了 DNS 做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。也就是说,域名解析的结果为内部的负载均衡服务器 IP 地址。 + +

+ +### 3. 反向代理服务器 + +首先了解一下正向代理与反向代理的区别: + +- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端; +- 反向代理:发生在服务器端,用户不知道代理的存在。 + +反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来做为负载均衡服务器。 + +在这种负载均衡转发方式下,客户端不直接请求源服务器,因此源服务器不需要外部 IP 地址,而反向代理需要配置内部和外部两套 IP 地址。 + +优点: + +- 与其它功能集成在一起,部署简单。 + +缺点: + +- 所有请求和响应都需要经过反向代理服务器,它可能会成为性能瓶颈。 + +### 4. 网络层 + +在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址,并修改请求数据包的目的 IP 地址,最后进行转发。 + +源服务器返回的响应也需要经过负载均衡服务器,通常是让负载均衡服务器同时作为集群的网关服务器来实现。 + +优点: + +- 在内核进程中进行处理,性能比较高。 + +缺点: + +- 和反向代理一样,所有的请求和响应都经过负载均衡服务器,会成为性能瓶颈。 + +### 5. 链路层 + +在链路层根据负载均衡算法计算源服务器的 MAC 地址,并修改请求数据包的目的 MAC 地址,并进行转发。 + +通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,直接转发给客户端,避免了负载均衡服务器的成为瓶颈。这是一种三角传输模式,被称为直接路由,对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。 + +这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVS(Linux Virtual Server)。 + +参考: + +- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms) +- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#) + +# 二、集群下的 Session 管理 + +一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作。 + +## Sticky Session + +需要配置负载均衡器,使得一个用户的所有请求都路由到同一个服务器,这样就可以把用户的 Session 存放在该服务器中。 + +缺点: + +- 当服务器宕机时,将丢失该服务器上的所有 Session。 + +

+ +## Session Replication + +在服务器之间进行 Session 同步操作,每个服务器都有所有用户的 Session 信息,因此用户可以向任何一个服务器进行请求。 + +缺点: + +- 占用过多内存; +- 同步过程占用网络带宽以及服务器处理器时间。 + + +

+ +## Session Server + +使用一个单独的服务器存储 Session 数据,可以使用 MySQL,也使用 Redis 或者 Memcached 这种内存型数据库。 + +优点: + +- 为了使得大型网站具有伸缩性,集群中的应用服务器通常需要保持无状态,那么应用服务器不能存储用户的会话信息。Session Server 将用户的会话信息单独进行存储,从而保证了应用服务器的无状态。 + +缺点: + +- 需要去实现存取 Session 的代码。 + +

+ +参考: + +- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/) + diff --git a/other/10072416.jpg b/other/10072416.jpg new file mode 100644 index 00000000..746fe860 Binary files /dev/null and b/other/10072416.jpg differ diff --git a/other/15684156.jpg b/other/15684156.jpg new file mode 100644 index 00000000..c3cf818e Binary files /dev/null and b/other/15684156.jpg differ diff --git a/other/18458140.jpg b/other/18458140.jpg new file mode 100644 index 00000000..4ce20001 Binary files /dev/null and b/other/18458140.jpg differ diff --git a/other/21679154.png b/other/21679154.png new file mode 100644 index 00000000..bfc24d56 Binary files /dev/null and b/other/21679154.png differ diff --git a/other/22954582.jpg b/other/22954582.jpg new file mode 100644 index 00000000..f61cf626 Binary files /dev/null and b/other/22954582.jpg differ diff --git a/other/7719370.png b/other/7719370.png new file mode 100644 index 00000000..68a35e65 Binary files /dev/null and b/other/7719370.png differ diff --git a/other/8018776.jpg b/other/8018776.jpg new file mode 100644 index 00000000..eb32326a Binary files /dev/null and b/other/8018776.jpg differ diff --git a/other/Group.md b/other/Group.md new file mode 100644 index 00000000..e4d54b22 --- /dev/null +++ b/other/Group.md @@ -0,0 +1,15 @@ +创建交流群的主要目的是为了给大家提供一个交流平台,方便大家在学习的过程中互相讨论。 + +这个交流群不是一个笔者的问题回答群,我更希望大家能够愿意积极回答,我相信提问和回答的过程都可以提高大家对知识的掌握程度。 + +因为笔者白天要上班,因此不能及时进行回复,大部分时间会处于潜水状态。 + +至于交流群和 Issue 有什么区别,主要是两方面:一是交流群实时性高一些,二是交流群会更活跃一些。 + +另外,Issue 主要是用来发布一些项目中的错误和一些改进建议,当然也可以发布一些可以讨论的问题。 + +交流群可以讨论的内容比较广,例如在阅读本项目过程中不理解的地方可以在交流群中寻求别人的帮助、新技术的讨论、招聘信息、学习和工作的感受等等。 + +交流群不讨论政治,不讨论有争议性的话题,不发表仇视言论,不传播谣言,不发布广告(招聘信息之类的可以)。 + +

diff --git a/other/LogoMakr_0zpEzN.png b/other/LogoMakr_0zpEzN.png new file mode 100644 index 00000000..b1d1b238 Binary files /dev/null and b/other/LogoMakr_0zpEzN.png differ diff --git a/other/README.md b/other/README.md new file mode 100644 index 00000000..39876f02 --- /dev/null +++ b/other/README.md @@ -0,0 +1,3 @@ +- 其他人添加的全新内容 +- 主页 README 引用的图片 +- 微信群描述文件 \ No newline at end of file diff --git a/other/group.jpg b/other/group.jpg new file mode 100644 index 00000000..af212841 Binary files /dev/null and b/other/group.jpg differ diff --git a/other/group.png b/other/group.png new file mode 100644 index 00000000..d0951cce Binary files /dev/null and b/other/group.png differ diff --git a/other/leetcode 总结.md b/other/leetcode 总结.md new file mode 100644 index 00000000..82ff61c7 --- /dev/null +++ b/other/leetcode 总结.md @@ -0,0 +1,37 @@ +# LeetCode 面试必备 + - 💪 就是干!如果你觉得有帮助请点个star,谢谢! + +> **欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远** + +## LeetCode 习题集合 + +* [LeetCode 解题集合](https://github.com/apachecn/LeetCode/tree/master/docs/Leetcode_Solutions) + + +## 模版要求 + +> 提交PR基本要求(满足任意一种即可) + +* 1. 不一样的思路 +* 2. 优化时间复杂度和空间复杂度,或者解决题目的Follow up +* 3. 有意义的简化代码 +* 4. 未提交过的题目 + +> **案例模版** + +[模版:007. Reverse Integer 反转整数](https://github.com/apachecn/LeetCode/tree/master/docs/Leetcode_Solutions/007._Reverse_Integer.md) + + +## 项目贡献者 + +> 项目发起人 + +* [@Lisanaaa](https://github.com/Lisanaaa) +* [@片刻](https://github.com/jiangzhonglian) + +> 贡献者(欢迎大家来追加) + +* [@Lisanaaa](https://github.com/Lisanaaa) +* [@片刻](https://github.com/jiangzhonglian) +* [@小瑶](https://github.com/chenyyx) + diff --git a/other/sql 经典练习题.sql b/other/sql 经典练习题.sql new file mode 100644 index 00000000..a4b4f964 --- /dev/null +++ b/other/sql 经典练习题.sql @@ -0,0 +1,483 @@ +use fuxi; + +CREATE TABLE STUDENT +( + SNO VARCHAR(3) NOT NULL, + SNAME VARCHAR(4) NOT NULL, + SSEX VARCHAR(2) NOT NULL, + SBIRTHDAY DATETIME, + CLASS VARCHAR(5) +); + +CREATE TABLE COURSE +( + CNO VARCHAR(5) NOT NULL, + CNAME VARCHAR(10) NOT NULL, + TNO VARCHAR(10) NOT NULL +); + +CREATE TABLE SCORE +( + SNO VARCHAR(3) NOT NULL, + CNO VARCHAR(5) NOT NULL, + DEGREE NUMERIC(10, 1) NOT NULL +); + +CREATE TABLE TEACHER +( + TNO VARCHAR(3) NOT NULL, + TNAME VARCHAR(4) NOT NULL, + TSEX VARCHAR(2) NOT NULL, + TBIRTHDAY DATETIME NOT NULL, + PROF VARCHAR(6), + DEPART VARCHAR(10) NOT NULL +); + +INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (108, '曾华' + , '男', '1977-09-01', 95033); +INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (105, '匡明' + , '男', '1975-10-02', 95031); +INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (107, '王丽' + , '女', '1976-01-23', 95033); +INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (101, '李军' + , '男', '1976-02-20', 95033); +INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (109, '王芳' + , '女', '1975-02-10', 95031); +INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (103, '陆君' + , '男', '1974-06-03', 95031); + +INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('3-105', '计算机导论', 825); +INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('3-245', '操作系统', 804); +INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('6-166', '数据电路', 856); +INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('9-888', '高等数学', 100); + +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (103, '3-245', 86); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (105, '3-245', 75); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (109, '3-245', 68); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (103, '3-105', 92); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (105, '3-105', 88); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (109, '3-105', 76); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (101, '3-105', 64); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (107, '3-105', 91); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (101, '6-166', 85); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (107, '6-106', 79); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (108, '3-105', 78); +INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (108, '6-166', 81); + +INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART) +VALUES (804, '李诚', '男', '1958-12-02', '副教授', '计算机系'); +INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART) +VALUES (856, '张旭', '男', '1969-03-12', '讲师', '电子工程系'); +INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART) +VALUES (825, '王萍', '女', '1972-05-05', '助教', '计算机系'); +INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART) +VALUES (831, '刘冰', '女', '1977-08-14', '助教', '电子工程系'); + +-- 1、 查询Student表中的所有记录的Sname、Ssex和Class列。 +select + SNAME, + SSEX, + CLASS +from STUDENT; + +-- 2、 查询教师所有的单位即不重复的Depart列。 +select distinct DEPART +from TEACHER1; + +-- 3、 查询Student表的所有记录。 +select * +from STUDENT; + +-- 4、 查询Score表中成绩在60到80之间的所有记录。 +select * +from SCORE +where DEGREE > 60 and DEGREE < 80; + +-- 5、 查询Score表中成绩为85,86或88的记录。 +select * +from SCORE +where DEGREE = 85 or DEGREE = 86 or DEGREE = 88; + +-- 6、 查询Student表中“95031”班或性别为“女”的同学记录。 +select * +from STUDENT +where CLASS = '95031' or SSEX = '女'; + +-- 7、 以Class降序查询Student表的所有记录。 +select * +from STUDENT +order by CLASS desc; + +-- 8、 以Cno升序、Degree降序查询Score表的所有记录。 +select * +from SCORE +order by CNO asc, DEGREE desc; + +-- 9、 查询“95031”班的学生人数。 +select count(*) +from STUDENT +where CLASS = '95031'; + +-- 10、查询Score表中的最高分的学生学号和课程号。 +select + sno, + CNO +from SCORE +where DEGREE = ( + select max(DEGREE) + from SCORE +); + +-- 11、查询‘3-105’号课程的平均分。 +select avg(DEGREE) +from SCORE +where CNO = '3-105'; + +-- 12、查询Score表中至少有5名学生选修的并以3开头的课程的平均分数。 +select + avg(DEGREE), + CNO +from SCORE +where cno like '3%' +group by CNO +having count(*) > 5; + +-- 13、查询最低分大于70,最高分小于90的Sno列。 +select SNO +from SCORE +group by SNO +having min(DEGREE) > 70 and max(DEGREE) < 90; + +-- 14、查询所有学生的Sname、Cno和Degree列。 +select + SNAME, + CNO, + DEGREE +from STUDENT, SCORE +where STUDENT.SNO = SCORE.SNO; + +-- 15、查询所有学生的Sno、Cname和Degree列。 +select + SCORE.SNO, + CNO, + DEGREE +from STUDENT, SCORE +where STUDENT.SNO = SCORE.SNO; + +-- 16、查询所有学生的Sname、Cname和Degree列。 +SELECT + A.SNAME, + B.CNAME, + C.DEGREE +FROM STUDENT A + JOIN (COURSE B, SCORE C) + ON A.SNO = C.SNO AND B.CNO = C.CNO; + +-- 17、查询“95033”班所选课程的平均分。 +select avg(DEGREE) +from SCORE +where sno in (select SNO + from STUDENT + where CLASS = '95033'); + +-- 18、假设使用如下命令建立了一个grade表: +create table grade ( + low numeric(3, 0), + upp numeric(3), + rank char(1) +); +insert into grade values (90, 100, 'A'); +insert into grade values (80, 89, 'B'); +insert into grade values (70, 79, 'C'); +insert into grade values (60, 69, 'D'); +insert into grade values (0, 59, 'E'); +-- 现查询所有同学的Sno、Cno和rank列。 +SELECT + A.SNO, + A.CNO, + B.RANK +FROM SCORE A, grade B +WHERE A.DEGREE BETWEEN B.LOW AND B.UPP +ORDER BY RANK; + +-- 19、查询选修“3-105”课程的成绩高于“109”号同学成绩的所有同学的记录。 +select * +from SCORE +where CNO = '3-105' and DEGREE > ALL ( + select DEGREE + from SCORE + where SNO = '109' +); + +set @@global.sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'; +set sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'; + +-- 20、查询score中选学一门以上课程的同学中分数为非最高分成绩的记录 +select * +from SCORE +where DEGREE < (select MAX(DEGREE) + from SCORE) +group by SNO +having count(*) > 1; + +-- 21、查询成绩高于学号为“109”、课程号为“3-105”的成绩的所有记录。 +-- 同19 + +-- 22、查询和学号为108的同学同年出生的所有学生的Sno、Sname和Sbirthday列。 +select + SNO, + SNAME, + SBIRTHDAY +from STUDENT +where year(SBIRTHDAY) = ( + select year(SBIRTHDAY) + from STUDENT + where SNO = '108' +); + +-- 23、查询“张旭“教师任课的学生成绩。 +select * +from SCORE +where cno = ( + select CNO + from COURSE + inner join TEACHER on COURSE.TNO = TEACHER.TNO and TNAME = '张旭' +); + +-- 24、查询选修某课程的同学人数多于5人的教师姓名。 +select TNAME +from TEACHER +where TNO = ( + select TNO + from COURSE + where CNO = (select CNO + from SCORE + group by CNO + having count(SNO) > 5) +); + +-- 25、查询95033班和95031班全体学生的记录。 +select * +from STUDENT +where CLASS in ('95033', '95031'); + +-- 26、查询存在有85分以上成绩的课程Cno. +select cno +from SCORE +group by CNO +having MAX(DEGREE) > 85; + +-- 27、查询出“计算机系“教师所教课程的成绩表。 +select * +from SCORE +where CNO in (select CNO + from TEACHER, COURSE + where DEPART = '计算机系' and COURSE.TNO = TEACHER.TNO); + +-- 28、查询“计算机系”与“电子工程系“不同职称的教师的Tname和Prof +select + tname, + prof +from TEACHER +where depart = '计算机系' and prof not in ( + select prof + from TEACHER + where depart = '电子工程系' +); + +-- 29、查询选修编号为“3-105“课程且成绩至少高于选修编号为“3-245”的同学的Cno、Sno和Degree,并按Degree从高到低次序排序。 +select + CNO, + SNO, + DEGREE +from SCORE +where CNO = '3-105' and DEGREE > any ( + select DEGREE + from SCORE + where CNO = '3-245' +) +order by DEGREE desc; + +-- 30、查询选修编号为“3-105”且成绩高于选修编号为“3-245”课程的同学的Cno、Sno和Degree. +SELECT * +FROM SCORE +WHERE DEGREE > ALL ( + SELECT DEGREE + FROM SCORE + WHERE CNO = '3-245' +) +ORDER by DEGREE desc; + +-- 31、查询所有教师和同学的name、sex和birthday. +select + TNAME name, + TSEX sex, + TBIRTHDAY birthday +from TEACHER +union +select + sname name, + SSEX sex, + SBIRTHDAY birthday +from STUDENT; + +-- 32、查询所有“女”教师和“女”同学的name、sex和birthday. +select + TNAME name, + TSEX sex, + TBIRTHDAY birthday +from TEACHER +where TSEX = '女' +union +select + sname name, + SSEX sex, + SBIRTHDAY birthday +from STUDENT +where SSEX = '女'; + +-- 33、查询成绩比该课程平均成绩低的同学的成绩表。 +SELECT A.* +FROM SCORE A +WHERE DEGREE < (SELECT AVG(DEGREE) + FROM SCORE B + WHERE A.CNO = B.CNO); + +-- 34、查询所有任课教师的Tname和Depart. +select + TNAME, + DEPART +from TEACHER a +where exists(select * + from COURSE b + where a.TNO = b.TNO); + +-- 35、查询所有未讲课的教师的Tname和Depart. +select + TNAME, + DEPART +from TEACHER a +where tno not in (select tno + from COURSE); + +-- 36、查询至少有2名男生的班号。 +select CLASS +from STUDENT +where SSEX = '男' +group by CLASS +having count(SSEX) > 1; + +-- 37、查询Student表中不姓“王”的同学记录。 +select * +from STUDENT +where SNAME not like "王%"; + +-- 38、查询Student表中每个学生的姓名和年龄。 +select + SNAME, + year(now()) - year(SBIRTHDAY) +from STUDENT; + +-- 39、查询Student表中最大和最小的Sbirthday日期值。 +select min(SBIRTHDAY) birthday +from STUDENT +union +select max(SBIRTHDAY) birthday +from STUDENT; + +-- 40、以班号和年龄从大到小的顺序查询Student表中的全部记录。 +select * +from STUDENT +order by CLASS desc, year(now()) - year(SBIRTHDAY) desc; + +-- 41、查询“男”教师及其所上的课程。 +select * +from TEACHER, COURSE +where TSEX = '男' and COURSE.TNO = TEACHER.TNO; + +-- 42、查询最高分同学的Sno、Cno和Degree列。 +select + sno, + CNO, + DEGREE +from SCORE +where DEGREE = (select max(DEGREE) + from SCORE); + +-- 43、查询和“李军”同性别的所有同学的Sname. +select sname +from STUDENT +where SSEX = (select SSEX + from STUDENT + where SNAME = '李军'); + +-- 44、查询和“李军”同性别并同班的同学Sname. +select sname +from STUDENT +where (SSEX, CLASS) = (select + SSEX, + CLASS + from STUDENT + where SNAME = '李军'); + +-- 45、查询所有选修“计算机导论”课程的“男”同学的成绩表 +select * +from SCORE, STUDENT +where SCORE.SNO = STUDENT.SNO and SSEX = '男' and CNO = ( + select CNO + from COURSE + where CNAME = '计算机导论'); + + + +-- 46、使用游标方式来同时查询每位同学的名字,他所选课程及成绩。 + +declare + cursor student_cursor is + select S.SNO,S.SNAME,C.CNAME,SC.DEGREE as DEGREE + from STUDENT S, COURSE C, SCORE SC + where S.SNO=SC.SNO + and SC.CNO=C.CNO; + + student_row student_cursor%ROWTYPE; + +begin + open student_cursor; + loop + fetch student_cursor INTO student_row; + exit when student_cursor%NOTFOUND; + dbms_output.put_line( student_row.SNO || '' || + +student_row.SNAME|| '' || student_row.CNAME || '' || + +student_row.DEGREE); + end loop; + close student_cursor; +END; +/ + + +-- 47、 声明触发器指令,每当有同学转换班级时执行触发器显示当前和之前所在班级。 + +CREATE OR REPLACE TRIGGER display_class_changes +AFTER DELETE OR INSERT OR UPDATE ON student +FOR EACH ROW +WHEN (NEW.sno > 0) + +BEGIN + + dbms_output.put_line('Old class: ' || :OLD.class); + dbms_output.put_line('New class: ' || :NEW.class); +END; +/ + + +Update student +set class=95031 +where sno=109; + + +-- 48、 删除已设置的触发器指令 + +DROP TRIGGER display_class_changes; + diff --git a/pics/计算机网络脑图.png b/other/计算机网络脑图.png similarity index 100% rename from pics/计算机网络脑图.png rename to other/计算机网络脑图.png diff --git a/pics/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png b/pics/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png new file mode 100644 index 00000000..0e56341c Binary files /dev/null and b/pics/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png differ diff --git a/pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png b/pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png new file mode 100644 index 00000000..26d62519 Binary files /dev/null and b/pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png differ diff --git a/pics/09b52bcb-88ba-4e36-8244-b375f16ad116.jpg b/pics/09b52bcb-88ba-4e36-8244-b375f16ad116.jpg new file mode 100644 index 00000000..146336aa Binary files /dev/null and b/pics/09b52bcb-88ba-4e36-8244-b375f16ad116.jpg differ diff --git a/pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg b/pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg new file mode 100644 index 00000000..cbba7f36 Binary files /dev/null and b/pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg differ diff --git a/pics/27ff9548-edb6-4465-92c8-7e6386e0b185.png b/pics/27ff9548-edb6-4465-92c8-7e6386e0b185.png new file mode 100644 index 00000000..1aee414c Binary files /dev/null and b/pics/27ff9548-edb6-4465-92c8-7e6386e0b185.png differ diff --git a/pics/280f7728-594f-4811-a03a-fa8d32c013da.png b/pics/280f7728-594f-4811-a03a-fa8d32c013da.png new file mode 100644 index 00000000..526b6847 Binary files /dev/null and b/pics/280f7728-594f-4811-a03a-fa8d32c013da.png differ diff --git a/pics/2858f8ad-aedb-45a5-a706-e98c96d690fa.jpg b/pics/2858f8ad-aedb-45a5-a706-e98c96d690fa.jpg new file mode 100644 index 00000000..d4171d08 Binary files /dev/null and b/pics/2858f8ad-aedb-45a5-a706-e98c96d690fa.jpg differ diff --git a/pics/49495c95-52e5-4c9a-b27b-92cf235ff5ec.png b/pics/49495c95-52e5-4c9a-b27b-92cf235ff5ec.png new file mode 100644 index 00000000..662d99c3 Binary files /dev/null and b/pics/49495c95-52e5-4c9a-b27b-92cf235ff5ec.png differ diff --git a/pics/6539b9a4-2b24-4d10-8c94-2eb5aba1e296.png b/pics/6539b9a4-2b24-4d10-8c94-2eb5aba1e296.png new file mode 100644 index 00000000..053a3dc6 Binary files /dev/null and b/pics/6539b9a4-2b24-4d10-8c94-2eb5aba1e296.png differ diff --git a/pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg b/pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg new file mode 100644 index 00000000..fc86a236 Binary files /dev/null and b/pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg differ diff --git a/pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg b/pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg new file mode 100644 index 00000000..d82f46eb Binary files /dev/null and b/pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg differ diff --git a/pics/71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png b/pics/71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png new file mode 100644 index 00000000..4e99159f Binary files /dev/null and b/pics/71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png differ diff --git a/pics/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg b/pics/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg new file mode 100644 index 00000000..fd13a137 Binary files /dev/null and b/pics/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg differ diff --git a/pics/7e873b60-44dc-4911-b080-defd5b8f0b49.png b/pics/7e873b60-44dc-4911-b080-defd5b8f0b49.png new file mode 100644 index 00000000..66e26e80 Binary files /dev/null and b/pics/7e873b60-44dc-4911-b080-defd5b8f0b49.png differ diff --git a/pics/897503d0-59e3-4752-903d-529fbdb72fee.jpg b/pics/897503d0-59e3-4752-903d-529fbdb72fee.jpg new file mode 100644 index 00000000..e0935e57 Binary files /dev/null and b/pics/897503d0-59e3-4752-903d-529fbdb72fee.jpg differ diff --git a/pics/926c7438-c5e1-4b94-840a-dcb24ff1dafe.png b/pics/926c7438-c5e1-4b94-840a-dcb24ff1dafe.png new file mode 100644 index 00000000..c6cdfd77 Binary files /dev/null and b/pics/926c7438-c5e1-4b94-840a-dcb24ff1dafe.png differ diff --git a/pics/JNI-Java-Native-Interface.jpg b/pics/JNI-Java-Native-Interface.jpg new file mode 100644 index 00000000..28b45e2e Binary files /dev/null and b/pics/JNI-Java-Native-Interface.jpg differ diff --git a/pics/NP4z3i8m38Ntd28NQ4_0KCJ2q044Oez.png b/pics/NP4z3i8m38Ntd28NQ4_0KCJ2q044Oez.png new file mode 100644 index 00000000..3819cda2 Binary files /dev/null and b/pics/NP4z3i8m38Ntd28NQ4_0KCJ2q044Oez.png differ diff --git a/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png new file mode 100644 index 00000000..124ec977 Binary files /dev/null and b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png differ diff --git a/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png new file mode 100644 index 00000000..7a202d35 Binary files /dev/null and b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png differ diff --git a/pics/a0df8edc-581b-4977-95c2-d7025795b899.png b/pics/a0df8edc-581b-4977-95c2-d7025795b899.png new file mode 100644 index 00000000..9103dfa6 Binary files /dev/null and b/pics/a0df8edc-581b-4977-95c2-d7025795b899.png differ diff --git a/pics/a58e294a-615d-4ea0-9fbf-064a6daec4b2.png b/pics/a58e294a-615d-4ea0-9fbf-064a6daec4b2.png new file mode 100644 index 00000000..fdefb823 Binary files /dev/null and b/pics/a58e294a-615d-4ea0-9fbf-064a6daec4b2.png differ diff --git a/pics/c5f611f0-fd5c-4158-9003-278141136e6e.jpg b/pics/c5f611f0-fd5c-4158-9003-278141136e6e.jpg new file mode 100644 index 00000000..473091be Binary files /dev/null and b/pics/c5f611f0-fd5c-4158-9003-278141136e6e.jpg differ diff --git a/pics/c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png b/pics/c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png new file mode 100644 index 00000000..fea5821b Binary files /dev/null and b/pics/c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png differ diff --git a/pics/ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg b/pics/ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg new file mode 100644 index 00000000..73b3d739 Binary files /dev/null and b/pics/ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg differ diff --git a/pics/docker-filesystems-busyboxrw.png b/pics/docker-filesystems-busyboxrw.png new file mode 100644 index 00000000..c12710af Binary files /dev/null and b/pics/docker-filesystems-busyboxrw.png differ diff --git a/pics/ea2304ce-268b-4238-9486-4d8f8aea8ca4.png b/pics/ea2304ce-268b-4238-9486-4d8f8aea8ca4.png new file mode 100644 index 00000000..59b54d2f Binary files /dev/null and b/pics/ea2304ce-268b-4238-9486-4d8f8aea8ca4.png differ diff --git a/pics/f94389e9-55b1-4f49-9d37-00ed05900ae0.png b/pics/f94389e9-55b1-4f49-9d37-00ed05900ae0.png new file mode 100644 index 00000000..49817697 Binary files /dev/null and b/pics/f94389e9-55b1-4f49-9d37-00ed05900ae0.png differ diff --git a/pics/faecea49-9974-40db-9821-c8636137df61.jpg b/pics/faecea49-9974-40db-9821-c8636137df61.jpg new file mode 100644 index 00000000..1b236b6e Binary files /dev/null and b/pics/faecea49-9974-40db-9821-c8636137df61.jpg differ