diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..314f02b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.txt \ No newline at end of file diff --git a/README.md b/README.md index 21ddb6a7..4c2d1c28 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,28 @@ | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| -|网络[:cloud:](#网络-cloud) |操作系统[:computer:](#操作系统-computer)| 算法[:pencil2:](#数据结构与算法-pencil2)| 面向对象[: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)| 分布式[:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) |
-:loudspeaker: 本仓库的内容不涉及商业行为,不向读者收取任何费用。 +:loudspeaker: 本仓库不参与商业行为,不向读者收取任何费用。 -:loudspeaker: This repository is not involving commercial activities, and does not charge readers any fee. +:loudspeaker: This repository is not engaging in business activities, and does not charge readers any fee.

-## 网络 :cloud: +## 算法 :pencil2: -> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) -整理自《计算机网络 第七版》,重点内容会在标题后面加 \*。 +> [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) -> [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md) +《剑指 Offer 第二版》的最优解,在牛客网在线编程中出现的题目都已 AC。 -整理自《图解 HTTP》 +> [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) + +对题目做了一个分类,并对每种题型的解题思路做了总结。 + +> [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) + +整理自《算法 第四版》 ## 操作系统 :computer: @@ -29,19 +34,17 @@ 整理自《鸟哥的 Linux 私房菜》 -## 数据结构与算法 :pencil2: -> [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) +## 网络 :cloud: -整理自《算法 第四版》 +> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) -> [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) +整理自《计算机网络 第七版》,重点内容会在标题后面加 \*。 -《剑指 Offer 第二版》的最优解,在牛客网在线编程中出现的题目都已 AC。 +> [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md) -> [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) +整理自《图解 HTTP》 -对题目做了一个分类,并对每种题型的解题思路做了总结。 ## 面向对象 :couple: @@ -109,7 +112,8 @@ File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO > [分布式问题分析](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式问题分析.md) -分布式事务、复杂均衡算法与实现、分布式锁、分布式 Session、分库分表的分布式困境与应对之策。 +分布式事务、负载均衡算法与实现、分布式锁、分布式 Session、分库分表的分布式困境与应对之策。 + ## 工具 :hammer: diff --git a/notes/HTTP.md b/notes/HTTP.md index 48b69061..97f361c9 100644 --- a/notes/HTTP.md +++ b/notes/HTTP.md @@ -5,8 +5,8 @@ * [请求和响应报文](#请求和响应报文) * [二、HTTP 方法](#二http-方法) * [GET](#get) - * [POST](#post) * [HEAD](#head) + * [POST](#post) * [PUT](#put) * [PATCH](#patch) * [DELETE](#delete) @@ -14,6 +14,7 @@ * [CONNECT](#connect) * [TRACE](#trace) * [三、HTTP 状态码](#三http-状态码) + * [1XX 信息](#1xx-信息) * [2XX 成功](#2xx-成功) * [3XX 重定向](#3xx-重定向) * [4XX 客户端错误](#4xx-客户端错误) @@ -27,8 +28,9 @@ * [Cookie](#cookie) * [缓存](#缓存) * [持久连接](#持久连接) + * [管线化处理](#管线化处理) * [编码](#编码) - * [分块传输](#分块传输) + * [分块传输编码](#分块传输编码) * [多部分对象集合](#多部分对象集合) * [范围请求](#范围请求) * [内容协商](#内容协商) @@ -41,10 +43,16 @@ * [七、Web 攻击技术](#七web-攻击技术) * [攻击模式](#攻击模式) * [跨站脚本攻击](#跨站脚本攻击) - * [SQL 注入攻击](#sql-注入攻击) * [跨站点请求伪造](#跨站点请求伪造) + * [SQL 注入攻击](#sql-注入攻击) * [拒绝服务攻击](#拒绝服务攻击) -* [八、各版本比较](#八各版本比较) +* [八、GET 和 POST 的区别](#八get-和-post-的区别) + * [参数](#参数) + * [安全](#安全) + * [幂等性](#幂等性) + * [可缓存](#可缓存) + * [XMLHttpRequest](#xmlhttprequest) +* [九、各版本比较](#九各版本比较) * [HTTP/1.0 与 HTTP/1.1 的区别](#http10-与-http11-的区别) * [HTTP/1.1 与 HTTP/2.0 的区别](#http11-与-http20-的区别) * [参考资料](#参考资料) @@ -89,28 +97,6 @@ URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基 当前网络请求中,绝大部分使用的是 GET 方法。 -## POST - -> 传输实体主体 - -POST 主要目的不是获取资源,而是传输存储在内容实体中的数据。 - -GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体。 - -GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。 - -GET 和 POST 的另一个区别是,使用 GET 方法,浏览器会把 HTTP Header 和 Data 一并发送出去,服务器响应 200(OK)并返回数据。而使用 POST 方法,浏览器先发送 Header,服务器响应 100(Continue)之后,浏览器再发送 Data,最后服务器响应 200(OK)并返回数据。 - -``` -GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 -``` - -``` -POST /test/demo_form.asp HTTP/1.1 -Host: w3schools.com -name1=value1&name2=value2 -``` - ## HEAD > 获取报文首部 @@ -119,6 +105,14 @@ name1=value1&name2=value2 主要用于确认 URL 的有效性以及资源更新的日期时间等。 +## POST + +> 传输实体主体 + +POST 主要用来传输数据,而 GET 主要用来获取资源。 + +更多 POST 与 GET 的比较请见第八章。 + ## PUT > 上传文件 @@ -202,6 +196,10 @@ CONNECT www.example.com:443 HTTP/1.1 | 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 | | 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 | +## 1XX 信息 + +- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 + ## 2XX 成功 - **200 OK** @@ -424,15 +422,17 @@ Expires 字段也可以用于告知缓存服务器该资源什么时候会过期 持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始 HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是非持久化连接的,如果要维持持续连接,需要使用 Connection : Keep-Alive。 -**管线化方式** 可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。 +## 管线化处理 + +HTTP/1.1 支持管线化处理,可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。 ## 编码 编码(Encoding)主要是为了对实体进行压缩。常用的编码有:gzip、compress、deflate、identity,其中 identity 表示不执行压缩的编码格式。 -## 分块传输 +## 分块传输编码 -分块传输(Chunked Transfer Coding)可以把数据分割成多块,让浏览器逐步显示页面。 +Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。 ## 多部分对象集合 @@ -483,7 +483,9 @@ Content-Length: 1024 ## 虚拟主机 -使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 +HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 + +使用 Host 首部字段进行处理。 ## 通信数据转发 @@ -515,7 +517,7 @@ HTTP 有以下安全性问题: 2. 不验证通信方的身份,通信方的身份有可能遭遇伪装; 3. 无法证明报文的完整性,报文有可能遭篡改。 -HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Socket Layer)通信,再由 SSL 和 TCP 通信。也就是说使用了隧道进行通信。 +HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。 通过使用 SSL,HTTPs 具有了加密、认证和完整性保护。 @@ -523,23 +525,23 @@ HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Socket Layer)通信 ## 加密 -### 1. 对称密钥 +### 1. 对称密钥加密 -(Symmetric-Key Encryption),加密的加密和解密使用同一密钥。 +对称密钥加密(Symmetric-Key Encryption),加密的加密和解密使用同一密钥。 - 优点:运算速度快; - 缺点:密钥容易被获取。 -

+

-### 2. 公开密钥 +### 2. 公开密钥加密 -(Public-Key Encryption),使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 +公开密钥加密(Public-Key Encryption),也称为非对称密钥加密,使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 - 优点:更为安全; - 缺点:运算速度慢; -

+

### 3. HTTPs 采用的加密方式 @@ -551,7 +553,6 @@ HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称 通过使用 **证书** 来对通信方进行认证。 - 数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 进行 HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,先进行验证,如果验证通过,就可以开始通信。 @@ -574,13 +575,27 @@ SSL 提供报文摘要功能来验证完整性。 ### 2. 被动攻击 -设下圈套,让用户发送有攻击代码的 HTTP 请求,那么用户发送了该 HTTP 请求之后就会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。 +设下圈套,让用户发送有攻击代码的 HTTP 请求,用户会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。 ## 跨站脚本攻击 ### 1. 概念 -(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和 Cookie 等各种内容。 +跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和 Cookie 等各种内容。 + +例如有一个论坛网站,攻击者可以在上面发表以下内容: + +``` + +``` + +之后该内容可能会被渲染成以下形式: + +``` +

+``` + +另一个用户浏览了含有这个内容的页面将会跳往 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 ### 2. 危害 @@ -609,6 +624,44 @@ SSL 提供报文摘要功能来验证完整性。 ?> ``` +## 跨站点请求伪造 + +### 1. 概念 + +跨站点请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 + +XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。 + +假如一家银行用以执行转账操作的 URL 地址如下: + +``` +http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。 +``` + +那么,一个恶意攻击者可以在另一个网站上放置如下代码: + +``` +。 +``` + +如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 + +这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 + +透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 + +### 2. 防范手段 + +(一)检查 Referer 字段 + +HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。 + +这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 + +(二)添加校验 Token + +由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。 + ## SQL 注入攻击 ### 1. 概念 @@ -659,63 +712,109 @@ strSQL = "SELECT * FROM users;" - 其他,使用其他更安全的方式连接 SQL 数据库。例如已修正过 SQL 注入问题的数据库连接组件,例如 ASP.NET 的 SqlDataSource 对象或是 LINQ to SQL。 - 使用 SQL 防注入系统。 -## 跨站点请求伪造 - -### 1. 概念 - -(Cross-site request forgery,XSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 - -XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。 - -假如一家银行用以执行转账操作的 URL 地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。 - -那么,一个恶意攻击者可以在另一个网站上放置如下代码:<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">。 - -如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 - -这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 - -透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 - -### 2. 防范手段 - -(一)检查 Referer 字段 - -HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。 - -这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 - -(二)添加校验 Token - -由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。 - ## 拒绝服务攻击 ### 1. 概念 -(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 +拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 -(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 +分布式拒绝服务攻击(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 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体中。 + +GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。 + +``` +GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 +``` + +``` +POST /test/demo_form.asp HTTP/1.1 +Host: w3schools.com +name1=value1&name2=value2 +``` + +## 安全 + +安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 + +GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 + +安全的方法除了 GET 之外还有:HEAD、OPTIONS。 + +不安全的方法除了 POST 之外还有 PUT、DELETE。 + +## 幂等性 + +幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。 + +GET /pageX HTTP/1.1 是幂等的。连续调用多次,客户端接收到的结果都是一样的: + +``` +GET /pageX HTTP/1.1 +GET /pageX HTTP/1.1 +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 2nd row +POST /add_row HTTP/1.1 -> Adds a 3rd row +``` + +DELETE /idX/delete HTTP/1.1 是幂等的,即便是不同请求之间接收到的状态码不一样: + +``` +DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists +DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted +DELETE /idX/delete HTTP/1.1 -> Returns 404 +``` + +## 可缓存 + +如果要对响应进行缓存,需要满足以下条件: + +1. 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 +2. 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。 +3. 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 + +## XMLHttpRequest + +为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest: + +> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 + +在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 + +# 九、各版本比较 ## HTTP/1.0 与 HTTP/1.1 的区别 -HTTP/1.1 新增了以下内容: +1. HTTP/1.1 默认是持久连接 +2. HTTP/1.1 支持管线化处理 +3. HTTP/1.1 支持虚拟主机 +4. HTTP/1.1 新增状态码 100 +5. HTTP/1.1 支持分块传输编码 +6. HTTP/1.1 新增缓存处理指令 max-age -- 默认为长连接; -- 提供了范围请求功能; -- 提供了虚拟主机的功能; -- 多了一些缓存处理字段; -- 多了一些状态码。 +具体内容见上文 ## HTTP/1.1 与 HTTP/2.0 的区别 +> [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) + ### 1. 多路复用 -HTTP/2.0 使用多路复用技术,使用同一个 TCP 连接来处理多个请求。 +HTTP/2.0 使用多路复用技术,同一个 TCP 连接可以处理多个请求。 ### 2. 首部压缩 @@ -723,7 +822,7 @@ HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 ### 3. 服务端推送 -在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。 +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。 ### 4. 二进制格式 @@ -731,7 +830,7 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 # 参考资料 -- 上野宣. 图解 HTTP[M]. Ren min you dian chu ban she, 2014. +- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014. - [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) - [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/) - [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html) @@ -748,3 +847,8 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 - [维基百科:跨站点请求伪造](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/) +- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences) +- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2) +- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) diff --git a/notes/JDK 中的设计模式.md b/notes/JDK 中的设计模式.md index 8e2161cc..92f53cb3 100644 --- a/notes/JDK 中的设计模式.md +++ b/notes/JDK 中的设计模式.md @@ -44,11 +44,11 @@ java.awt.Desktop#getDesktop() ## 2. 简单工厂模式 -在不对用户暴露对象内部逻辑的前提下创建对象;使用通用的接口来创建对象; +在不对用户暴露对象内部逻辑的前提下创建对象。 ## 3. 工厂方法模式 -定义创建对象的接口,但是让子类来决定应该使用哪个类来创建;使用通用的接口来创建对象; +定义创建对象的接口,但是让子类来决定应该使用哪个类来创建。 ```java java.lang.Proxy#newProxyInstance() @@ -120,7 +120,7 @@ javax.swing.Action ## 3. 解释器模式 为语言创建解释器,通常由语言的语法和语法分析来定义。 - + ```java java.util.Pattern java.text.Normalizer @@ -229,7 +229,7 @@ JDBC ## 3. 组合模式 -将对象组合成树形结构来表示整理-部分层次关系,允许用户以相同的方式处理单独对象和组合对象。 +将对象组合成树形结构来表示整体-部分层次关系,允许用户以相同的方式处理单独对象和组合对象。 ```java javax.swing.JComponent#add(Component) diff --git a/notes/Java IO.md b/notes/Java IO.md index 1c1e4e1b..b48c90c3 100644 --- a/notes/Java IO.md +++ b/notes/Java IO.md @@ -156,7 +156,7 @@ I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的 面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。 -一个面向块的 I/O 系统以块的形式处理数据,一次处理数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 +一个面向块的 I/O 系统以块的形式处理数据,一次处理一个数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 @@ -177,7 +177,7 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 ### 2. 缓冲区 -发送给一个通道的所有对象都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 +发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 @@ -199,7 +199,7 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 状态变量的改变过程举例: -① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 9。capacity 变量不会改变,下面的讨论会忽略它。 +① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

diff --git a/notes/Java 基础.md b/notes/Java 基础.md index ba6cdd13..5375ce09 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -4,13 +4,15 @@ * [static](#static) * [二、Object 通用方法](#二object-通用方法) * [概览](#概览) - * [clone()](#clone) * [equals()](#equals) + * [hashCode()](#hashcode) + * [toString()](#tostring) + * [clone()](#clone) * [四、继承](#四继承) * [访问权限](#访问权限) * [抽象类与接口](#抽象类与接口) * [super](#super) - * [重载与重写](#重载与重写) + * [覆盖与重载](#覆盖与重载) * [五、String](#五string) * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) * [String 不可变的原因](#string-不可变的原因) @@ -44,7 +46,7 @@ ```java final int x = 1; -x = 2; // cannot assign value to final variable 'x' +// x = 2; // cannot assign value to final variable 'x' final A y = new A(); y.a = 1; ``` @@ -53,7 +55,7 @@ y.a = 1; 声明方法不能被子类覆盖。 -private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是重载了。 +private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是在子类中定义了一个新的方法。 **3. 类** @@ -63,7 +65,7 @@ private 方法隐式地被指定为 final,如果在子类中定义的方法和 **1. 静态变量** -静态变量在内存中只存在一份,只在类第一次实例化时初始化一次。 +静态变量在内存中只存在一份,只在类初始化时赋值一次。 - 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它; - 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 @@ -81,11 +83,23 @@ public class A { **3. 静态语句块** -静态语句块和静态变量一样在类第一次实例化时运行一次。 +静态语句块在类初始化时运行一次。 -**4. 初始化顺序** +**4. 静态内部类** -静态数据优先于其它数据的初始化,静态变量和静态语句块哪个先运行取决于它们在代码中的顺序。 +内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。 + +**5. 静态导包** + +```source-java +import static com.xxx.ClassName.* +``` + +在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 + +**6. 变量赋值顺序** + +静态变量的赋值和静态语句块的运行优先于实例变量的赋值和普通语句块的运行,静态变量的赋值和静态语句块的运行哪个先执行取决于它们在代码中的顺序。 ```java public static String staticField = "静态变量"; @@ -97,8 +111,6 @@ static { } ``` -实例变量和普通语句块的初始化在静态变量和静态语句块初始化结束之后。 - ```java public String field = "实例变量"; ``` @@ -109,7 +121,7 @@ public String field = "实例变量"; } ``` -最后才是构造函数中的数据进行初始化 +最后才运行构造函数 ```java public InitialOrderTest() { @@ -119,7 +131,7 @@ public InitialOrderTest() { 存在继承的情况下,初始化顺序为: -1. 父类(静态变量、静态语句块块) +1. 父类(静态变量、静态语句块) 2. 子类(静态变量、静态语句块) 3. 父类(实例变量、普通语句块) 4. 父类(构造函数) @@ -154,28 +166,196 @@ public final void wait() throws InterruptedException 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. 等价关系** + +(一)自反性 + +```java +x.equals(x); // true +``` + +(二)对称性 + +```java +x.equals(y) == y.equals(x) // true +``` + +(三)传递性 + +```java +if(x.equals(y) && y.equals(z)) { + x.equals(z); // true; +} +``` + +(四)一致性 + +多次调用 equals() 方法结果不变 + +```java +x.equals(y) == x.equals(y); // true +``` + +(五)与 null 的比较 + +对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false + +```java +x.euqals(null); // false; +``` + +**3. 实现** + +- 检查是否为同一个对象的引用,如果是直接返回 true; +- 检查是否是同一个类型,如果不是,直接返回 false; +- 将 Object 实例进行转型; +- 判断每个关键域是否相等。 + +```java +public class EqualExample { + private int x; + private int y; + private int z; + + public EqualExample(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + EqualExample that = (EqualExample) o; + + if (x != that.x) return false; + if (y != that.y) return false; + return z == that.z; + } +} +``` + +## hashCode() + +hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。 + +在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证相等的两个实例散列值也等价。 + +下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。 + +```java +EqualExample e1 = new EqualExample(1, 1, 1); +EqualExample e2 = new EqualExample(1, 1, 1); +System.out.println(e1.equals(e2)); // true +HashSet set = new HashSet<>(); +set.add(e1); +set.add(e2); +System.out.println(set.size()); // 2 +``` + +理想的散列函数应当具有均匀性,即不相等的实例应当均匀分不到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 + +一个数与 31 相乘可以转换成移位和减法:31\*x == (x<<5)-x。 + +```java +@Override +public int hashCode() { + int result = 17; + result = 31 * result + x; + result = 31 * result + y; + result = 31 * result + z; + return result; +} +``` + +## toString() + +默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。 + +```java +public class ToStringExample { + private int number; + + public ToStringExample(int number) { + this.number = number; + } +} +``` + +```java +ToStringExample example = new ToStringExample(123); +System.out.println(example.toString()); +``` + +```html +ToStringExample@4554617c +``` + ## clone() **1. cloneable** -clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去重载 clone() 就没有这个方法。 +clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去覆盖 clone() 就没有这个方法。 ```java -public class CloneTest { +public class CloneExample { private int a; private int b; } ``` ```java -CloneTest x = new CloneTest(); -CloneTest y = x.clone(); // 'clone()' has protected access in 'java.lang.Object' +CloneExample e1 = new CloneExample(); +// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object' ``` -接下来重载 Object 的 clone() 得到以下实现: +接下来覆盖 Object 的 clone() 得到以下实现: ```java -public class CloneTest{ +public class CloneExample { + private int a; + private int b; + + @Override + protected CloneExample clone() throws CloneNotSupportedException { + return (CloneExample)super.clone(); + } +} +``` + +```java +CloneExample e1 = new CloneExample(); +try { + CloneExample e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +``` + +```html +java.lang.CloneNotSupportedException: CloneTest +``` + +以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。 + +```java +public class CloneExample implements Cloneable { private int a; private int b; @@ -186,46 +366,130 @@ public class CloneTest{ } ``` -```java -CloneTest x = new CloneTest(); -try { - CloneTest y = (CloneTest) x.clone(); -} catch (CloneNotSupportedException e) { - e.printStackTrace(); -} -``` - -```html -java.lang.CloneNotSupportedException: CloneTest -``` - -以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 +应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 **2. 深拷贝与浅拷贝** -

+- 浅拷贝:拷贝实例和原始实例的引用类型引用同一个对象; +- 深拷贝:拷贝实例和原始实例的引用类型引用不同对象。 -- 浅拷贝:拷贝对象和原对象的引用类型引用同一个对象; -- 深拷贝:引用不同对象。 +```java +public class ShallowCloneExample implements Cloneable { + private int[] arr; -实现深拷贝的方法: + public ShallowCloneExample() { + arr = new int[10]; + for (int i = 0; i < arr.length; i++) { + arr[i] = i; + } + } -- [Defensive copying](http://www.javapractices.com/topic/TopicAction.do?Id=15) -- [copy constructors](http://www.javapractices.com/topic/TopicAction.do?Id=12) -- [static factory methods](http://www.javapractices.com/topic/TopicAction.do?Id=21). + public void set(int index, int value) { + arr[index] = value; + } -> [How do I copy an object in Java?](https://stackoverflow.com/questions/869033/how-do-i-copy-an-object-in-java) + public int get(int index) { + return arr[index]; + } -## equals() + @Override + protected ShallowCloneExample clone() throws CloneNotSupportedException { + return (ShallowCloneExample) super.clone(); + } +} +``` -**1. == 与 equals() 区别** +```java +ShallowCloneExample e1 = new ShallowCloneExample(); +ShallowCloneExample e2 = null; +try { + e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +e1.set(2, 222); +System.out.println(e2.get(2)); // 222 +``` -- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 -- 对于引用类型,== 判断两个引用是否引用同一个对象,而 equals() 判断引用的对象是否等价。 +```java +public class DeepCloneExample implements Cloneable { + private int[] arr; -**2. 等价性** + public DeepCloneExample() { + arr = new int[10]; + for (int i = 0; i < arr.length; i++) { + arr[i] = i; + } + } -> [散列](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E4%B8%89%E6%95%A3%E5%88%977) + public void set(int index, int value) { + arr[index] = value; + } + + public int get(int index) { + return arr[index]; + } + + @Override + protected DeepCloneExample clone() throws CloneNotSupportedException { + DeepCloneExample result = (DeepCloneExample) super.clone(); + result.arr = new int[arr.length]; + for (int i = 0; i < arr.length; i++) { + result.arr[i] = arr[i]; + } + return result; + } +} +``` + +```java +DeepCloneExample e1 = new DeepCloneExample(); +DeepCloneExample e2 = null; +try { + e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +e1.set(2, 222); +System.out.println(e2.get(2)); // 2 +``` + +使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。 + +```java +public class CloneConstructorExample { + private int[] arr; + + public CloneConstructorExample() { + arr = new int[10]; + for (int i = 0; i < arr.length; i++) { + arr[i] = i; + } + } + + public CloneConstructorExample(CloneConstructorExample original) { + arr = new int[original.arr.length]; + for (int i = 0; i < original.arr.length; i++) { + arr[i] = original.arr[i]; + } + } + + public void set(int index, int value) { + arr[index] = value; + } + + public int get(int index) { + return arr[index]; + } +} +``` + +```java +CloneConstructorExample e1 = new CloneConstructorExample(); +CloneConstructorExample e2 = new CloneConstructorExample(e1); +e1.set(2, 222); +System.out.println(e2.get(2)); // 2 +``` # 四、继承 @@ -235,69 +499,150 @@ Java 中有三个访问权限修饰符:private、protected 以及 public,如 可以对类或类中的成员(字段以及方法)加上访问修饰符。 -- 成员可见表示其它类可该类的对象访问到该成员; +- 成员可见表示其它类可以用这个类的实例访问到该成员; - 类可见表示其它类可以用这个类创建对象。 -在理解类的可见性时,可以把类当做包中的一个成员,然后包表示一个类,那么就可以类比成员的可见性。 +protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。 -protected 用于修饰成员,表示在继承体系中成员对于子类可见。但是这个访问修饰符对于类没有意义,因为包没有继承体系。 +设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。 -> [浅析 Java 中的访问权限控制](http://www.importnew.com/18097.html) +如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里式替换原则。 + +字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。可以使用公有的 getter 和 setter 方法来替换公有字段。 + +```java +public class AccessExample { + public int x; +} +``` + +```java +public class AccessExample { + private int x; + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } +} +``` + +但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。 + +```java +public class AccessWithInnerClassExample { + private class InnerClass { + int x; + } + + private InnerClass innerClass; + + public AccessWithInnerClassExample() { + innerClass = new InnerClass(); + } + + public int getValue() { + return innerClass.x; // 直接访问 + } +} +``` ## 抽象类与接口 **1. 抽象类** -抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 +抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。 + +抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 ```java -public abstract class GenericServlet implements Servlet, ServletConfig, Serializable { - // abstract method - abstract void service(ServletRequest req, ServletResponse res); +public abstract class AbstractClassExample { - void init() { - // Its implementation + protected int x; + private int y; + + public abstract void func1(); + + public void func2() { + System.out.println("func2"); } - // other method related to Servlet } ``` -> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) +```java +public class AbstractExtendClassExample extends AbstractClassExample{ + @Override + public void func1() { + System.out.println("func1"); + } +} +``` + +```java +// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated +AbstractClassExample ac2 = new AbstractExtendClassExample(); +ac2.func1(); +``` **2. 接口** -接口是抽象类的延伸。Java 为了安全性而不支持多重继承,一个类只能有一个父类。但是接口不同,一个类可以同时实现多个接口,不管这些接口之间有没有关系,所以接口弥补不支持多重继承的缺陷。 - -```java -public interface Externalizable extends Serializable { - - void writeExternal(ObjectOutput out) throws IOException; - - void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; -} -``` +接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。 从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 +接口也可以包含字段,并且这些字段隐式都是 static 和 final 的。 + +接口中的方法默认都是 public 的,并且不允许定义为 private 或者 protected。 + ```java -public interface InterfaceDefaultTest { - default void func() { - System.out.println("default method in interface!"); +public interface InterfaceExample { + void func1(); + + default void func2(){ + System.out.println("func2"); + } + + int x = 123; + //int y; // Variable 'y' might not have been initialized + public int z = 0; // Modifier 'public' is redundant for interface fields + // private int k = 0; // Modifier 'private' not allowed here + // protected int l = 0; // Modifier 'protected' not allowed here + // private void fun3(); // Modifier 'private' not allowed here +} +``` + +```java +public class InterfaceImplementExample implements InterfaceExample { + @Override + public void func1() { + System.out.println("func1"); } } ``` +```java +// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated +InterfaceExample ie2 = new InterfaceImplementExample(); +ie2.func1(); +System.out.println(InterfaceExample.x); +``` + **3. 比较** -- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求子类和父类具有 IS-A 关系; +- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。 - 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 +- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。 +- 接口的方法只能是 public 的,而抽象类的方法可以由多种访问权限。 **4. 使用选择** 使用抽象类: -- 需要在几个相关的类中共享代码; -- 需要能控制继承来的方法和字段的访问权限,而不是都为 public。 +- 需要在几个相关的类中共享代码。 +- 需要能控制继承来的方法和域的访问权限,而不是都为 public。 - 需要继承非静态(non-static)和非常量(non-final)字段。 使用接口: @@ -305,55 +650,65 @@ public interface InterfaceDefaultTest { - 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; - 需要使用多重继承。 -> [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) +在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 + +> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
[When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) ## super -**1. 访问父类的成员** - -如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。 +- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。 +- 访问父类的成员:如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。 ```java -public class Superclass { - public void printMethod() { - System.out.println("Printed in Superclass."); +public class SuperExample { + protected int x; + protected int y; + + public SuperExample(int x, int y) { + this.x = x; + this.y = y; + } + + public void func() { + System.out.println("SuperExample.func()"); } } ``` ```java -public class Subclass extends Superclass { - // Overrides printMethod in Superclass - public void printMethod() { - super.printMethod(); - System.out.println("Printed in Subclass"); +public class SuperExtendExample extends SuperExample { + private int z; + + public SuperExtendExample(int x, int y, int z) { + super(x, y); + this.z = z; } - public static void main(String[] args) { - Subclass s = new Subclass(); - s.printMethod(); + @Override + public void func() { + super.func(); + System.out.println("SuperExtendExample.func()"); } } ``` -**2. 访问父类的构造函数** - -可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。 - ```java -public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear) { - super(startCadence, startSpeed, startGear); - seatHeight = startHeight; -} +SuperExample e = new SuperExtendExample(1, 2, 3); +e.func(); +``` + +```html +SuperExample.func() +SuperExtendExample.func() ``` > [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) -## 重载与重写 +## 覆盖与重载 -- 重写存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法; +- 覆盖(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法; -- 重载即存在于继承体系中,也存在于同一个类中,指一个方法与已经存在的方法或者父类的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 +- 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 # 五、String @@ -376,13 +731,13 @@ public MountainBike(int startHeight, int startCadence, int startSpeed, int start **1. 可以缓存 hash 值** -因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 等情况。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 +因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 **2. String Pool 的需要** 如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 -

+

**3. 安全性** @@ -390,21 +745,50 @@ String 经常作为参数,String 不可变性可以保证参数不可变。例 **4. 线程安全** -String 不可变性天生具备线程安全,可以在多个线程中使用。 +String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 > [Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) ## String.intern() -使用 String.intern() 可以保证所有相同内容的字符串变量引用相同的内存对象。 +使用 String.intern() 可以保证相同内容的字符串实例引用相同的内存对象。 -> [揭开 String.intern() 那神秘的面纱](https://www.jianshu.com/p/95f516cb75ef) +下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用,这个方法首先把 s1 引用的对象放到 String Poll(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。 + +```java +String s1 = new String("aaa"); +String s2 = new String("aaa"); +System.out.println(s1 == s2); // false +String s3 = s1.intern(); +System.out.println(s1.intern() == s3); // true +``` + +如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Poll 中。 + +```java +String s4 = "bbb"; +String s5 = "bbb"; +System.out.println(s4 == s5); // true +``` + +在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 + +> [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) # 六、基本类型与运算 ## 包装类型 -八个基本类型:boolean/1 byte/8 char/16 short/16 int/32 float/32 long/64 double/64 +八个基本类型: + +- boolean/1 +- byte/8 +- char/16 +- short/16 +- int/32 +- float/32 +- long/64 +- double/64 基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。 @@ -413,38 +797,70 @@ Integer x = 2; // 装箱 int y = x; // 拆箱 ``` -new Integer(123) 与 Integer.valueOf(123) 的区别在于,Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。 +new Integer(123) 与 Integer.valueOf(123) 的区别在于,new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。 ```java -public static void main(String[] args) { - Integer a = new Integer(1); - Integer b = new Integer(1); - System.out.println("a==b? " + (a == b)); - - Integer c = Integer.valueOf(1); - Integer d = Integer.valueOf(1); - System.out.println("c==d? " + (c == d)); -} +Integer x = new Integer(123); +Integer y = new Integer(123); +System.out.println(x == y); // false +Integer z = Integer.valueOf(123); +Integer k = Integer.valueOf(123); +System.out.println(z == k); // true ``` -```html -a==b? false -c==d? true +编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。 + +```java +Integer m = 123; +Integer n = 123; +System.out.println(m == n); // true ``` valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。 ```java public static Integer valueOf(int i) { - final int offset = 128; - if (i >= -128 && i <= 127) { - return IntegerCache.cache[i + offset]; - } + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } ``` -基本类型中可以使用缓存池的值如下: +在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。 + +```java +static final int low = -128; +static final int high; +static final Integer cache[]; + +static { + // high value may be configured by property + int h = 127; + String integerCacheHighPropValue = + sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); + if (integerCacheHighPropValue != null) { + try { + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + } catch( NumberFormatException nfe) { + // If the property cannot be parsed into an int, ignore it. + } + } + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert IntegerCache.high >= 127; +} +``` + +Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些: - boolean values true and false - all byte values @@ -452,52 +868,42 @@ public static Integer valueOf(int i) { - int values between -128 and 127 - char in the range \u0000 to \u007F -自动装箱过程编译器会调用 valueOf() 方法,因此多个 Integer 对象使用装箱来创建并且值相同,那么就会引用相同的对象。这样做很显然是为了节省内存开销。 - -```java -Integer x = 1; -Integer y = 1; -System.out.println(c == d); // true -``` +因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。 > [Differences between new Integer(123), Integer.valueOf(123) and just 123 ](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123) ## switch -A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types and a few special classes that "wrap" certain primitive types: Character, Byte, Short, and Integer. +从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。 -In the JDK 7 release, you can use a String object in the expression of a switch statement. +```java +String s = "a"; +switch (s) { + case "a": + System.out.println("aaa"); + break; + case "b": + System.out.println("bbb"); + break; +} +``` switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 -> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) - -switch 使用查找表的方式来实现,JVM 中使用的指令是 lookupswitch。 - ```java -public static void main(String... args) { - switch (1) { - case 1: - break; - case 2: - break; - } -} - -public static void main(java.lang.String[]); - Code: - Stack=1, Locals=1, Args_size=1 - 0: iconst_1 - 1: lookupswitch{ //2 - 1: 28; - 2: 31; - default: 31 } - 28: goto 31 - 31: return +// long x = 111; +// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum' +// case 111: +// System.out.println(111); +// break; +// case 222: +// System.out.println(222); +// break; +// } ``` -> [How does Java's switch work under the hood?](https://stackoverflow.com/questions/12020048/how-does-javas-switch-work-under-the-hood) +> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) # 七、反射 @@ -515,8 +921,6 @@ Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。 -> [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) - **Advantages of Using Reflection:** - **Extensibility Features** : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names. @@ -531,21 +935,18 @@ Reflection is powerful, but should not be used indiscriminately. If it is possib - **Security Restrictions** : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet. - **Exposure of Internals** :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform. -> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) +> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
[深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) # 八、异常 -Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**,其中 Error 用来表示编译时系统错误。 - -Exception 分为两种: +Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种: 1. **受检异常** :需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复; 2. **非受检异常** :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序奔溃并且无法恢复。 -

+

-> - [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) -> - [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) +> [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception)
[Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) # 九、泛型 @@ -558,8 +959,7 @@ public class Box { } ``` -> - [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/) -> - [10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) +> [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/)
[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) # 十、注解 @@ -598,8 +998,7 @@ Java 注解是附加在代码中的一些元信息,用于一些工具在编译 7. Binary Literals, Underscore in literals 8. Diamond Syntax -> - [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17) -> - [Java 8 特性 ](http://www.importnew.com/19345.html) +> [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17)
[Java 8 特性 ](http://www.importnew.com/19345.html) ## Java 与 C++ 的区别 diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 2a05225b..9893ee27 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -5,17 +5,16 @@ * [二、容器中的设计模式](#二容器中的设计模式) * [迭代器模式](#迭代器模式) * [适配器模式](#适配器模式) -* [三、散列](#三散列) -* [四、源码分析](#四源码分析) +* [三、源码分析](#三源码分析) * [ArrayList](#arraylist) * [Vector](#vector) * [LinkedList](#linkedlist) + * [LinkedHashMap](#linkedhashmap) * [TreeMap](#treemap) * [HashMap](#hashmap) - * [LinkedHashMap](#linkedhashmap) * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17) * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18) -* [五、参考资料](#五参考资料) +* [参考资料](#参考资料) @@ -31,7 +30,7 @@ - HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 -- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logn); +- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN); - LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。 @@ -41,13 +40,13 @@ - Vector:和 ArrayList 类似,但它是线程安全的; -- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双端队列。 +- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 ### 3. Queue - LinkedList:可以用它来支持双向队列; -- PriorityQueue 是基于堆结构实现,可以用它来实现优先级队列。 +- PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 ## Map @@ -80,8 +79,6 @@ for (String item : list) { } ``` -> [迭代器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%8D%81%E4%BA%8C%E8%BF%AD%E4%BB%A3%E5%99%A8%E6%A8%A1%E5%BC%8F) - ## 适配器模式 java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 @@ -91,7 +88,7 @@ java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 public static List asList(T... a) ``` -如果要将数组类型转换为 List 类型,应该注意的是参数列表为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 +如果要将数组类型转换为 List 类型,应该注意的是 asList() 的参数为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 ```java Integer[] arr = {1, 2, 3}; @@ -104,53 +101,7 @@ List list = Arrays.asList(arr); List list = Arrays.asList(1,2,3); ``` -> [适配器模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#%E5%8D%81%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F) - -# 三、散列 - -hasCode() 返回散列值,使用的是对象的地址。 - -而 equals() 是用来判断两个对象是否相等的,相等的两个对象散列值一定要相同,但是散列值相同的两个对象不一定相等。 - -相等必须满足以下五个性质: - -**1. 自反性** - -```java -x.equals(x); // true -``` - -**2. 对称性** - -```java -x.equals(y) == y.equals(x) // true -``` - -**3. 传递性** - -```java -if(x.equals(y) && y.equals(z)) { - x.equals(z); // true; -} -``` - -**4. 一致性** - -多次调用 equals() 方法结果不变 - -```java -x.equals(y) == x.equals(y); // true -``` - -**5. 与 null 的比较** - -对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false - -```java -x.euqals(null); // false; -``` - -# 四、源码分析 +# 三、源码分析 建议先阅读 [算法-查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对容器类源码的理解有很大帮助。 @@ -160,7 +111,7 @@ x.euqals(null); // false; ## ArrayList -[ArraList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java) +[ArrayList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java) ### 1. 概览 @@ -171,7 +122,7 @@ public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable ``` -基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。这是 ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 +基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 ```java transient Object[] elementData; // non-private to simplify nested class access @@ -273,6 +224,10 @@ private void writeObject(java.io.ObjectOutputStream s) [LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java) +## LinkedHashMap + +[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) + ## TreeMap [TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) @@ -291,12 +246,10 @@ transient Entry[] table; 其中,Entry 就是存储数据的键值对,它包含了四个字段。从 next 字段我们可以看出 Entry 是一个链表,即每个桶会存放一个链表。 -

+

JDK 1.8 使用 Node 类型存储一个键值对,它依然继承自 Entry,因此可以按照上面的存储结构来理解。 -需要注意的是,Key 类型为 final,这意味着它不可改变,因此每个桶的链表采用头插法实现,也就是说新节点需要只能在链表头部插入。 - ```java static class Node implements Map.Entry { final int hash; @@ -342,32 +295,35 @@ static class Node implements Map.Entry { ### 2. 拉链法的工作原理 ```java -HashMap map = new HashMap<>(); // 默认大小为 16 -map.put("sachin", 30); -map.put("vishal", 20); -map.put("vaibhav", 20); +HashMap map = new HashMap<>(); +map.put("K1", "V1"); +map.put("K2", "V2"); +map.put("K3", "V3"); ``` -- 计算 "sachin" 的 hashcode 为 115,使用除留余数法得到 115 % 16 = 3,因此 ("sachin", 30) 键值对放到第 3 个桶上。 -- 同样得到 ("vishal", 20) 和 ("vaibhav", 20) 都应该放到第 6 个桶上。("vishal", 20) 先放入, ("vaibhav", 20) 链接到 ("vishal", 20) 之后。 +- 新建一个 HashMap,默认大小为 16; +- 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。 +- 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。 +- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 后面。 -

+

-当进行查找时,需要分成两步进行,第一步是先根据 hashcode 计算出所在的桶,第二步是在链表上顺序查找。由于 table 是数组形式的,具有随机读取的特性,因此第一步的时间复杂度为 O(1),而第二步需要在链表上顺序查找,时间复杂度显然和链表的长度成正比。 +查找需要分成两步进行: + +- 计算键值对所在的桶; +- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。 ### 3. 链表转红黑树 应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。 -

- ### 4. 扩容 因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。 -设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的数量级为 O(N/M)。 +设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。 -为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,可就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。 +为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。 和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。 @@ -445,11 +401,9 @@ void transfer(Entry[] newTable) { ### 5. 确定桶下标 -需要三步操作:计算 Key 的 hashCode、高位运算、除留余数法取模。 +很多操作都需要先确定一个键值对所在的桶下标,这个操作需要分三步进行。 -

- -**(一)hashcode()** +(一)调用 hashCode() ```java public final int hashCode() { @@ -457,9 +411,9 @@ public final int hashCode() { } ``` -**(二)高位运算** +(二)高位运算 -通过 hashCode() 的高 16 位异或低 16 位,使得数组比较小时,也能保证高低位都参与到了哈希计算中。 +将 hashCode 的高 16 位和低 16 位进行异或操作,使得在数组比较小时,也能保证高低位都参与到了哈希计算中。 ```java static final int hash(Object key) { @@ -468,7 +422,7 @@ static final int hash(Object key) { } ``` -**(三)除留余数** +(三)除留余数 令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: @@ -477,7 +431,7 @@ x : 00010000 x-1 : 00001111 ``` -令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位及以上数: +令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数: ``` y : 10110010 @@ -497,7 +451,7 @@ y%x : 00000010 拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 -以下操作在 Java 8 中没有,但是原理上相同。 +以下操作在 JDK 1.8 中没有,但是原理上相同。 ```java static int indexFor(int h, int length) { @@ -520,17 +474,24 @@ new capacity : 00100000 ### 7. 扩容-计算数组容量 -先考虑如何求一个数的补码,对于 10100000,它的补码为 11111111,可以使用以下方法得到: +HashMap 构造函数允许用户传入的容量不是 2 的幂次方,因为它可以自动地将传入的容量转换为 2 的幂次方。 + +先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到: ``` -mask |= mask >> 1 11000000 -mask |= mask >> 2 11110000 +mask |= mask >> 1 11011000 +mask |= mask >> 2 11111100 mask |= mask >> 4 11111111 ``` -如果最后令 mask+1,得到就是大于原始数字的最小的 2 次方。 +mask+1 是大于原始数字的最小的 2 幂次方。 -以下是 HashMap 中计算一个大小所需要的数组容量的代码: +``` +num 10010000 +mask+1 100000000 +``` + +以下是 HashMap 中计算数组容量的代码: ```java static final int tableSizeFor(int cap) { @@ -556,15 +517,11 @@ HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 - 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。 - HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。 -## LinkedHashMap - -[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) - ## ConcurrentHashMap - JDK 1.7 [ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java) -ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。 +ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。 相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。 @@ -581,7 +538,7 @@ static final class HashEntry { } ``` -继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 +Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 ```java static final class Segment extends ReentrantLock implements Serializable { @@ -615,48 +572,19 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16;

-### 2. HashEntery 的不可变性 +### 2. HashEntry 的不可变性 -HashEntry 中的 key,hash,next 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。 +HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 -同时,HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 +非结构性修改操作只是更改某个 HashEntry 的 value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程 “看到”。 -```java -final V remove(Object key, int hash, Object value) { - if (!tryLock()) - scanAndLock(key, hash); - V oldValue = null; - try { - HashEntry[] tab = table; - int index = (tab.length - 1) & hash; - HashEntry e = entryAt(tab, index); - HashEntry pred = null; - while (e != null) { - K k; - HashEntry next = e.next; - if ((k = e.key) == key || - (e.hash == hash && key.equals(k))) { - V v = e.value; - if (value == null || value == v || value.equals(v)) { - if (pred == null) - setEntryAt(tab, index, next); - else - pred.setNext(next); - ++modCount; - --count; - oldValue = v; - } - break; - } - pred = e; - e = next; - } - } finally { - unlock(); - } - return oldValue; -} -``` +对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。 + +结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。 + +clear 操作只是把 ConcurrentHashMap 中所有的桶 “置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。 + +put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。 在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。 @@ -664,19 +592,10 @@ final V remove(Object key, int hash, Object value) {

-除了 remove 操作,其它操作也类似。可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 +综上,可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 ### 3. Volatile 变量 -```java -static final class HashEntry { - final int hash; - final K key; - volatile V value; - volatile HashEntry next; -} -``` - 由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。 下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。 @@ -722,7 +641,7 @@ V get(Object key, int hash) { 在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。 -根据 Java 内存模型,对 同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 +根据 Java 内存模型,对同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。 @@ -738,13 +657,8 @@ ConcurrentHashMap 的高并发性主要来自于三个方面: [ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java) -

- - JDK 1.7 分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock。 -

- JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。而是使用了内置锁 synchronized,主要是出于以下考虑: 1. synchronized 的锁粒度更低; @@ -753,7 +667,7 @@ JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。 并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 -# 五、参考资料 +# 参考资料 - Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002. - [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php) diff --git a/notes/Java 并发.md b/notes/Java 并发.md index f7e64359..edc9dc0f 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -1,37 +1,110 @@ -* [一、使用线程](#一使用线程) +* [一、线程状态转换](#一线程状态转换) + * [新建(New)](#新建new) + * [可运行(Runnable)](#可运行runnable) + * [阻塞(Blocking)](#阻塞blocking) + * [无限期等待(Waiting)](#无限期等待waiting) + * [限期等待(Timed Waiting)](#限期等待timed-waiting) + * [死亡(Terminated)](#死亡terminated) +* [二、使用线程](#二使用线程) * [实现 Runnable 接口](#实现-runnable-接口) * [实现 Callable 接口](#实现-callable-接口) * [继承 Thread 类](#继承-thread-类) * [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread) -* [二、基础线程机制](#二基础线程机制) +* [三、基础线程机制](#三基础线程机制) + * [Executor](#executor) + * [Daemon](#daemon) * [sleep()](#sleep) * [yield()](#yield) +* [四、中断](#四中断) + * [InterruptedException](#interruptedexception) + * [interrupted()](#interrupted) + * [Executor 的中断操作](#executor-的中断操作) +* [五、互斥同步](#五互斥同步) + * [synchronized](#synchronized) + * [ReentrantLock](#reentrantlock) + * [synchronized 和 ReentrantLock 比较](#synchronized-和-reentrantlock-比较) +* [六、线程之间的协作](#六线程之间的协作) * [join()](#join) - * [deamon](#deamon) -* [三、结束线程](#三结束线程) - * [阻塞](#阻塞) - * [中断](#中断) -* [四、线程之间的协作](#四线程之间的协作) - * [同步与通信的概念理解](#同步与通信的概念理解) - * [线程同步](#线程同步) - * [线程通信](#线程通信) -* [五、线程状态转换](#五线程状态转换) -* [六、Executor](#六executor) -* [七、内存模型](#七内存模型) + * [wait() notify() notifyAll()](#wait-notify-notifyall) + * [await() signal() signalAll()](#await-signal-signalall) +* [七、J.U.C - AQS](#七juc---aqs) + * [CountdownLatch](#countdownlatch) + * [CyclicBarrier](#cyclicbarrier) + * [Semaphore](#semaphore) +* [八、J.U.C - 其它组件](#八juc---其它组件) + * [FutureTask](#futuretask) + * [BlockingQueue](#blockingqueue) + * [ForkJoin](#forkjoin) +* [九、线程不安全示例](#九线程不安全示例) +* [十、Java 内存模型](#十java-内存模型) * [主内存与工作内存](#主内存与工作内存) + * [内存间交互操作](#内存间交互操作) * [内存模型三大特性](#内存模型三大特性) * [先行发生原则](#先行发生原则) -* [八、线程安全](#八线程安全) +* [十一、线程安全](#十一线程安全) * [线程安全分类](#线程安全分类) * [线程安全的实现方法](#线程安全的实现方法) - * [锁优化](#锁优化) -* [九、多线程开发良好的实践](#九多线程开发良好的实践) +* [十二、锁优化](#十二锁优化) + * [自旋锁与自适应自旋](#自旋锁与自适应自旋) + * [锁消除](#锁消除) + * [锁粗化](#锁粗化) + * [轻量级锁](#轻量级锁) + * [偏向锁](#偏向锁) +* [十三、多线程开发良好的实践](#十三多线程开发良好的实践) * [参考资料](#参考资料) -# 一、使用线程 +# 一、线程状态转换 + +

+ +## 新建(New) + +创建后尚未启动。 + +## 可运行(Runnable) + +可能正在运行,也可能正在等待 CPU 时间片。 + +包含了操作系统线程状态中的 Running 和 Ready。 + +## 阻塞(Blocking) + +等待获取一个排它锁,如果其线程释放了锁就会结束此状态。 + +## 无限期等待(Waiting) + +等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。 + +| 进入方法 | 退出方法 | +| --- | --- | +| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() | +| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 | +| LockSupport.park() 方法 | - | + +## 限期等待(Timed Waiting) + +无需等待其它线程显示地唤醒,在一定时间之后会被系统自动唤醒。 + +调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。 + +调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。 + +| 进入方法 | 退出方法 | +| --- | --- | +| Thread.sleep() 方法 | 时间结束 | +| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() | +| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 | +| LockSupport.parkNanos() 方法 | - | +| LockSupport.parkUntil() 方法 | - | + +## 死亡(Terminated) + +可以是线程结束任务之后自己结束,或者产生了异常而结束。 + +# 二、使用线程 有三种使用线程的方法: @@ -52,11 +125,14 @@ public class MyRunnable implements Runnable { public void run() { // ... } - public static void main(String[] args) { - MyRunnable instance = new MyRunnable(); - Tread thread = new Thread(instance); - thread.start(); - } +} +``` + +```java +public static void main(String[] args) { + MyRunnable instance = new MyRunnable(); + Thread thread = new Thread(instance); + thread.start(); } ``` @@ -67,18 +143,21 @@ public class MyRunnable implements Runnable { ```java public class MyCallable implements Callable { public Integer call() { - // ... - } - public static void main(String[] args) { - MyCallable mc = new MyCallable(); - FutureTask ft = new FutureTask<>(mc); - Thread thread = new Thread(ft); - thread.start(); - System.out.println(ft.get()); + return 123; } } ``` +```java +public static void main(String[] args) throws ExecutionException, InterruptedException { + MyCallable mc = new MyCallable(); + FutureTask ft = new FutureTask<>(mc); + Thread thread = new Thread(ft); + thread.start(); + System.out.println(ft.get()); +} +``` + ## 继承 Thread 类 同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。 @@ -88,10 +167,13 @@ public class MyThread extends Thread { public void run() { // ... } - public static void main(String[] args) { - MyThread mt = new MyThread(); - mt.start(); - } +} +``` + +```java +public static void main(String[] args) { + MyThread mt = new MyThread(); + mt.start(); } ``` @@ -102,218 +184,686 @@ public class MyThread extends Thread { 1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; 2. 类可能只要求可执行就行,继承整个 Thread 类开销会过大。 +# 三、基础线程机制 -# 二、基础线程机制 +## Executor + +Executor 管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。 + +主要有三种 Executor: + +1. CachedThreadPool:一个任务创建一个线程; +2. FixedThreadPool:所有任务只能使用固定大小的线程; +3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 + +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < 5; i++) { + executorService.execute(new MyRunnable()); + } + executorService.shutdown(); +} +``` + +## Daemon + +守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。 + +当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。 + +main() 属于非守护线程。 + +使用 setDaemon() 方法将一个线程设置为守护线程。 + +```java +public static void main(String[] args) { + Thread thread = new Thread(new MyRunnable()); + thread.setDaemon(true); +} +``` ## sleep() -Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。 +Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。 -sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 +sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 ```java public void run() { try { - // ... - Thread.sleep(1000); - // ... + Thread.sleep(3000); } catch (InterruptedException e) { - System.err.println(e); + e.printStackTrace(); } } ``` ## yield() -对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。 +对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。 ```java public void run() { - // ... Thread.yield(); } ``` -## join() +# 四、中断 -在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。 +一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。 -可以加一个超时参数。 +## InterruptedException -## deamon +通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。 -守护线程(deamon)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。 - -当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。 - -main() 属于非后台线程。 - -使用 setDaemon() 方法将一个线程设置为后台线程。 - -# 三、结束线程 - -## 阻塞 - -一个线程进入阻塞状态可能有以下原因: - -1. 调用 Thread.sleep() 使线程睡眠; -2. 调用 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息; -3. 等待某个 I/O 的完成; -4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。 - -**阻塞 睡眠 挂起** - -阻塞是一种状态,而睡眠和挂起是一种手段,通过睡眠和挂起可以让一个线程进入阻塞状态。 - -睡眠和挂起这两种手段的区别是,挂起手段会释放对象锁,而阻塞手段不会。 - -应该注意的是,睡眠和挂起都可以设置一个等待时间,超过等待时间之后,线程会退出阻塞状态。但是如果不为挂起设置等待时间,那么它只能等到通知的到来才能退出阻塞状态。 - -## 中断 - -使用中断机制即可终止阻塞的线程。 - -使用 **interrupt()** 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。 - -应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。 - -**1. 不可中断的阻塞** - -不能中断 I/O 阻塞和 synchronized 锁阻塞。 - -**2. Executor 的中断操作** - -Executor 避免对 Thread 对象的直接操作,使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。 - -如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。 - -**3. 检查中断** - -通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。 - -interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状态,从而判断一个线程是否已经被中断。 - -interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。 - -# 四、线程之间的协作 - -## 同步与通信的概念理解 - -在操作系统中,有三个概念用来描述进程间的协作关系: - -1. 互斥:多个进程在同一时刻只有一个进程能进入临界区; -2. 同步:多个进程按一定顺序执行; -3. 通信:多个进程间的信息传递。 - -通信是一种手段,它可以用来实现同步。也就是说,通过在多个进程间传递信息,可以控制多个进程以一定顺序执行。 - -而同步又可以保证互斥。即进程按一定顺序执行,可以保证在同一时刻只有一个进程能访问临界资源。但是同步不止用来实现互斥,例如生成者消费者问题,生产者和消费者进程之间的同步不是用来控制对临界资源的访问。 - -总结起来就是:通信 -> 同步 -> 互斥。 - -进程和线程在一定程度上类似,也可以用这些概念来描述。 - -在 Java 语言中,这些概念描述有些差别: - -1. 同步:可以和操作系统的互斥等同; -2. 通信:可以和操作系统的同步等同。 - -很多时候这三个概念都会混在一起用,不同的文章有不同的解释,不能说哪个是对的哪个是错的,只要自己能理解就行。 - -## 线程同步 - -给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。 - -### 1. synchronized - -**同步一个方法** - -使多个线程不能同时访问该方法。 +对于以下代码,在 Main 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。 ```java -public synchronized void func(String name) { +public class InterruptExample { + public static void main(String[] args) throws InterruptedException { + Thread thread1 = new MyThread1(); + thread1.start(); + thread1.interrupt(); + System.out.println("Main run"); + } + + private static class MyThread1 extends Thread { + @Override + public void run() { + try { + Thread.sleep(2000); + System.out.println("Thread run"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + +``` + +```html +Main run +java.lang.InterruptedException: sleep interrupted + at java.lang.Thread.sleep(Native Method) + at InterruptExample.lambda$main$0(InterruptExample.java:5) + at InterruptExample$$Lambda$1/713338599.run(Unknown Source) + at java.lang.Thread.run(Thread.java:745) +``` + +## interrupted() + +如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。 + +但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。 + +```java +public class InterruptExample { + public static void main(String[] args) throws InterruptedException { + Thread thread2 = new MyThread2(); + thread2.start(); + thread2.interrupt(); + } + + private static class MyThread2 extends Thread { + @Override + public void run() { + while (!interrupted()) { + // .. + } + System.out.println("Thread end"); + } + } +} +``` + +```html +Thread end +``` + +## Executor 的中断操作 + +调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。 + +以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。 + +```java +public class ExecutorInterruptExample { + public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> { + try { + Thread.sleep(2000); + System.out.println("Thread run"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + executorService.shutdownNow(); + System.out.println("Main run"); + } +} +``` + +```html +Main run +java.lang.InterruptedException: sleep interrupted + at java.lang.Thread.sleep(Native Method) + at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9) + at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) + at java.lang.Thread.run(Thread.java:745) +``` + +如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。 + +```java +Future future = executorService.submit(() -> { + // .. +}); +future.cancel(true); +``` + +# 五、互斥同步 + +Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。 + +## synchronized + +**1. 同步一个代码块** + +```java +public void func () { + synchronized (this) { + // ... + } +} +``` + +它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。 + +对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步语句块,因此这两个线程就需要进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。 + +```java +public class SynchronizedExample { + + public void func1() { + synchronized (this) { + for (int i = 0; i < 10; i++) { + System.out.print(i + " "); + } + } + } + + public static void main(String[] args) { + SynchronizedExample e1 = new SynchronizedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> e1.func1()); + executorService.execute(() -> e1.func1()); + } +} +``` + +```html +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 +``` + +对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。 + +```java +public static void main(String[] args) { + SynchronizedExample e1 = new SynchronizedExample(); + SynchronizedExample e2 = new SynchronizedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> e1.func1()); + executorService.execute(() -> e2.func1()); +} +``` + +```html +0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 +``` + + +**2. 同步一个方法** + +```java +public synchronized void func () { // ... } ``` -**同步一个代码块** +它和同步代码块一样,只作用于同一个对象。 + +**3. 同步一个类** ```java -public void func(String name) { - synchronized(this) { +public void func() { + synchronized (SynchronizedExample.class) { // ... } } ``` -### 2. ReentrantLock - -可以使用 Lock 来对一个语句块进行同步。 +作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也需要进行同步。 ```java -private Lock lock; -public int func(int value) { - try { - lock.lock(); - // ... - } finally { - lock.unlock(); - } +public class SynchronizedExample { + + public void func2() { + synchronized (SynchronizedExample.class) { + for (int i = 0; i < 10; i++) { + System.out.print(i + " "); + } + } + } + + public static void main(String[] args) { + SynchronizedExample e1 = new SynchronizedExample(); + SynchronizedExample e2 = new SynchronizedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> e1.func2()); + executorService.execute(() -> e2.func2()); + } } ``` +```html +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 +``` + +**4. 同步一个静态方法** + +```java +public synchronized static void fun() { + // ... +} +``` + +作用于整个类。 + +## ReentrantLock + +```java +public class LockExample { + + private Lock lock = new ReentrantLock(); + + public void func() { + lock.lock(); + try { + for (int i = 0; i < 10; i++) { + System.out.print(i + " "); + } + } finally { + lock.unlock(); // 确保释放锁,从而避免发生死锁。 + } + } +} +``` + +```java +public static void main(String[] args) { + LockExample lockExample = new LockExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> lockExample.func()); + executorService.execute(() -> lockExample.func()); +} +``` + +```html +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 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。 -如果需要使用上述功能,选用 ReentrantLock 是一个很好的选择。从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。 +## synchronized 和 ReentrantLock 比较 -## 线程通信 +**1. 锁的实现** -### 1. wait() notify() notifyAll() +synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 + +**2. 性能** + +从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。 + +**3. 功能** + +ReentrantLock 多了一些高级功能。 + +**4. 使用选择** + +除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。 + +# 六、线程之间的协作 + +当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。 + +## join() + +在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 + +对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。 + +```java +public class JoinExample { + + private class A extends Thread { + @Override + public void run() { + System.out.println("A"); + } + } + + private class B extends Thread { + + private A a; + + B(A a) { + this.a = a; + } + + @Override + public void run() { + try { + a.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("B"); + } + } + + public void test() { + A a = new A(); + B b = new B(a); + b.start(); + a.start(); + } + + public static void main(String[] args) { + JoinExample example = new JoinExample(); + example.test(); + } +} +``` + +``` +A +B +``` + +## wait() notify() notifyAll() + +调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。 它们都属于 Object 的一部分,而不属于 Thread。 -wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。可以通过这种机制让一个线程阻塞,直到某种特定条件满足。 +只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。 -sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。 - -只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。 +使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。 ```java -private boolean flag = false; +public class WaitNotifyExample { + public synchronized void before() { + System.out.println("before"); + notifyAll(); + } -public synchronized void after() { - while(flag == false) { - wait(); - // ... + public synchronized void after() { + try { + wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("after"); + } + + public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + WaitNotifyExample example = new WaitNotifyExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); } } +``` -public synchronized void before() { - flag = true; - notifyAll(); -} +```html +before +after ``` **wait() 和 sleep() 的区别** -这两种方法都能将线程阻塞,一种是使用挂起的方式,一种使用睡眠的方式。 +1. wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; +2. wait() 会释放锁,sleep() 不会。 -1. wait() 是 Object 类的方法,而 sleep() 是 Thread 的静态方法; -2. 挂起会释放锁,睡眠不会。 +## await() signal() signalAll() -### 2. BlockingQueue +java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。 + +使用 Lock 来获取一个 Condition 对象。 + +```java +public class AwaitSignalExample { + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public void before() { + lock.lock(); + try { + System.out.println("before"); + condition.signalAll(); + } finally { + lock.unlock(); + } + } + + public void after() { + lock.lock(); + try { + condition.await(); + System.out.println("after"); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + AwaitSignalExample example = new AwaitSignalExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); + } +} +``` + +```html +before +after +``` + +# 七、J.U.C - AQS + +java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。 + +## CountdownLatch + +用来控制一个线程等待多个线程。 + +维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。 + +

+ +```java +public class CountdownLatchExample { + + public static void main(String[] args) throws InterruptedException { + final int totalTread = 10; + CountDownLatch countDownLatch = new CountDownLatch(totalTread); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < totalTread; i++) { + executorService.execute(() -> { + System.out.print("run.."); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + System.out.println("end"); + executorService.shutdown(); + } +} +``` + +```html +run..run..run..run..run..run..run..run..run..run..end +``` + +## CyclicBarrier + +用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。 + +和 CountdownLatch 相似,都是通过维护计数器来实现的。但是它的计数器是递增的,每次执行 await() 方法之后,计数器会加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。和 CountdownLatch 的另一个区别是,CyclicBarrier 的计数器可以循环使用,所以它才叫做循环屏障。 + +下图应该从下往上看才正确。 + +

+ +```java +public class CyclicBarrierExample { + + public static void main(String[] args) throws InterruptedException { + final int totalTread = 10; + CyclicBarrier cyclicBarrier = new CyclicBarrier(totalTread); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < totalTread; i++) { + executorService.execute(() -> { + System.out.print("before.."); + try { + cyclicBarrier.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (BrokenBarrierException e) { + e.printStackTrace(); + } + System.out.print("after.."); + }); + } + executorService.shutdown(); + } +} +``` + +```html +before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after.. +``` + +## Semaphore + +Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。 + +

+ +以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。 + +```java +public class SemaphoreExample { + public static void main(String[] args) { + final int clientCount = 3; + final int totalRequestCount = 10; + Semaphore semaphore = new Semaphore(clientCount); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < totalRequestCount; i++) { + executorService.execute(()->{ + try { + semaphore.acquire(); + System.out.print(semaphore.availablePermits() + " "); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + semaphore.release(); + } + }); + } + executorService.shutdown(); + } +} +``` + +```html +2 1 2 2 2 2 2 1 2 2 +``` + +# 八、J.U.C - 其它组件 + +## FutureTask + +在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。 + +```java +public class FutureTask implements RunnableFuture +``` + +```java +public interface RunnableFuture extends Runnable, Future +``` + +当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,用一个线程去执行该任务,然后其它线程继续执行其它任务。当需要该任务的计算结果时,再通过 FutureTask 的 get() 方法获取。 + +```java +public class FutureTaskExample { + public static void main(String[] args) throws ExecutionException, InterruptedException { + FutureTask futureTask = new FutureTask(new Callable() { + @Override + public Integer call() throws Exception { + int result = 0; + for (int i = 0; i < 100; i++) { + Thread.sleep(10); + result += i; + } + return result; + } + }); + + Thread computeThread = new Thread(futureTask); + computeThread.start(); + + Thread otherThread = new Thread(() -> { + System.out.println("other task is running..."); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + otherThread.start(); + System.out.println(futureTask.get()); + } +} +``` + +```html +other task is running... +4950 +``` + +## BlockingQueue java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: @@ -322,165 +872,302 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: 提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,指到队列有空闲位置。 -它们响应中断,当收到中断请求的时候会抛出 InterruptedException,从而提前结束阻塞状态。 - -是线程安全的。 - **使用 BlockingQueue 实现生产者消费者问题** ```java -// 生产者 -public class Producer implements Runnable { - private BlockingQueue queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; - public Producer(BlockingQueue queue) { - this.queue = queue; - } +public class ProducerConsumer { - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " is making product."); - String product = "Made By " + Thread.currentThread().getName(); - try { - queue.put(product); - } catch (InterruptedException e) { - e.printStackTrace(); + private static BlockingQueue queue = new ArrayBlockingQueue<>(5); + + private static class Producer extends Thread { + @Override + public void run() { + try { + queue.put("product"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.print("produce.."); } } -} -``` -```java -// 消费者 -public class Consumer implements Runnable { - private BlockingQueue queue; + private static class Consumer extends Thread { - public Consumer(BlockingQueue queue) { - this.queue = queue; - } - - @Override - public void run() { - try { - String product = queue.take(); - System.out.println(Thread.currentThread().getName() + " is consuming product." + "( " + product + " )"); - } catch (InterruptedException e) { - e.printStackTrace(); + @Override + public void run() { + try { + String product = queue.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.print("consume.."); } } -} -``` -```java -// 客户端 -public class Client { public static void main(String[] args) { - BlockingQueue queue = new LinkedBlockingQueue<>(5); for (int i = 0; i < 2; i++) { - new Thread(new Consumer(queue), "Consumer-" + i).start(); + Producer producer = new Producer(); + producer.start(); } for (int i = 0; i < 5; i++) { - // 只有两个 Product,因此只能消费两个,其它三个消费者被阻塞 - new Thread(new Producer(queue), "Producer-" + i).start(); + Consumer consumer = new Consumer(); + consumer.start(); } - for (int i = 2; i < 5; i++) { - new Thread(new Consumer(queue), "Consumer-" + i).start(); + for (int i = 0; i < 3; i++) { + Producer producer = new Producer(); + producer.start(); } } } ``` ```html -// 运行结果 -Producer-0 is making product. -Consumer-0 is consuming product.( Made By Producer-0 ) -Producer-1 is making product. -Consumer-1 is consuming product.( Made By Producer-1 ) -Producer-2 is making product. -Producer-3 is making product. -Producer-4 is making product. -Consumer-2 is consuming product.( Made By Producer-2 ) -Consumer-3 is consuming product.( Made By Producer-3 ) -Consumer-4 is consuming product.( Made By Producer-4 ) +produce..produce..consume..consume..produce..consume..produce..consume..produce..consume.. ``` -# 五、线程状态转换 +## ForkJoin -

- -1. 新建(New):创建后尚未启动; -2. 可运行(Runnale):可能正在运行,也可能正在等待 CPU 时间片; -3. 无限期等待(Waiting):等待其它线程显示地唤醒,否则不会被分配 CPU 时间片; -4. 限期等待(Timed Waiting):无需等待其它线程显示地唤醒,在一定时间之后会被系统自动唤醒; -5. 阻塞(Blocking):等待获取一个排它锁,如果其线程释放了锁就会结束此状态; -6. 死亡(Terminated):可以是线程结束任务之后自己结束,或者产生了异常而结束,中断机制就是使用了抛出中断异常的方式让一个阻塞的线程结束。 - -# 六、Executor - -Executor 管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。 - -主要有三种 Executor: - -1. CachedTreadPool:一个任务创建一个线程; -2. FixedThreadPool:所有任务只能使用固定大小的线程; -3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 +主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。 ```java -ExecutorService exec = Executors.newCachedThreadPool(); -for(int i = 0; i < 5; i++) { - exec.execute(new MyRunnable()); +public class ForkJoinExample extends RecursiveTask { + private final int threhold = 5; + private int first; + private int last; + + public ForkJoinExample(int first, int last) { + this.first = first; + this.last = last; + } + + @Override + protected Integer compute() { + int result = 0; + if (last - first <= threhold) { + // 任务足够小则直接计算 + for (int i = first; i <= last; i++) { + result += i; + } + } else { + // 拆分成小任务 + int middle = first + (last - first) / 2; + ForkJoinExample leftTask = new ForkJoinExample(first, middle); + ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last); + leftTask.fork(); + rightTask.fork(); + result = leftTask.join() + rightTask.join(); + } + return result; + } } ``` -# 七、内存模型 +```java +public static void main(String[] args) throws ExecutionException, InterruptedException { + ForkJoinExample example = new ForkJoinExample(1, 10000); + ForkJoinPool forkJoinPool = new ForkJoinPool(); + Future result = forkJoinPool.submit(example); + System.out.println(result.get()); +} +``` + +ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。 + +```java +public class ForkJoinPool extends AbstractExecutorService +``` + +ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。 + +

+ +# 九、线程不安全示例 + +如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。 + +以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。 + +```java +public class ThreadUnsafeExample { + + private int cnt = 0; + + public void add() { + cnt++; + } + + public int get() { + return cnt; + } + + public static void main(String[] args) throws InterruptedException { + final int threadSize = 1000; + ThreadUnsafeExample example = new ThreadUnsafeExample(); + final CountDownLatch countDownLatch = new CountDownLatch(threadSize); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < threadSize; i++) { + executorService.execute(() -> { + example.add(); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + executorService.shutdown(); + System.out.println(example.get()); + } +} +``` + +```html +997 +``` + +# 十、Java 内存模型 + +Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。 ## 主内存与工作内存 -对处理器上的寄存器进行读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 +处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 -所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存中,保存了被该线程使用到的变量的主内存副本拷贝,线程只能直接操作工作内存中的变量。 +加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。 -

+

+ +所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。 + +线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。 + +

+ +## 内存间交互操作 + +Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。 + +

+ +- read:把一个变量的值从主内存传输到工作内存中 +- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 +- use:把工作内存中一个变量的值传递给执行引擎 +- assign:把一个从执行引擎接收到的值赋给工作内存的变量 +- store:把工作内存的一个变量的值传送到主内存中 +- write:在 store 之后执行,把 store 得到的值放入主内存的变量中 +- lock:作用于主内存的变量 +- unlock ## 内存模型三大特性 ### 1. 原子性 -Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,也就是说对这部分数据的操作可以不具备原子性。 +Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。 -AtomicInteger、AtomicLong、AtomicReference 等特殊的原子性变量类提供了下面形式的原子性条件更新语句,使得比较和更新这两个操作能够不可分割地执行。 +有一个错误认识就是,int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 变量属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。 + +为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。 + +下图演示了两个线程同时对 cnt 变量进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。 + +

+ +AtomicInteger 能保证多个线程修改的原子性。 + +

+ +使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现: ```java -boolean compareAndSet(expectedValue, updateValue); +public class AtomicExample { + private AtomicInteger cnt = new AtomicInteger(); + + public void add() { + cnt.incrementAndGet(); + } + + public int get() { + return cnt.get(); + } + + public static void main(String[] args) throws InterruptedException { + final int threadSize = 1000; + AtomicExample example = new AtomicExample(); + final CountDownLatch countDownLatch = new CountDownLatch(threadSize); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < threadSize; i++) { + executorService.execute(() -> { + example.add(); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + executorService.shutdown(); + System.out.println(example.get()); + } +} + ``` -AtomicInteger 使用举例: +```html +1000 +``` + +除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的完整性,它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 ```java -private AtomicInteger ai = new AtomicInteger(0); +public class AtomicSynchronizedExample { + private int cnt = 0; -public int next() { - return ai.addAndGet(2) + public synchronized void add() { + cnt++; + } + + public synchronized int get() { + return cnt; + } + + public static void main(String[] args) throws InterruptedException { + final int threadSize = 1000; + AtomicSynchronizedExample example = new AtomicSynchronizedExample(); + final CountDownLatch countDownLatch = new CountDownLatch(threadSize); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < threadSize; i++) { + executorService.execute(() -> { + example.add(); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + executorService.shutdown(); + System.out.println(example.get()); + } } ``` -也可以使用 synchronized 同步操作来保证操作具备原子性,它对应的虚拟机字节码指令为 monitorenter 和 monitorexit。 +```html +1000 +``` ### 2. 可见性 -如果没有及时地对主内存与工作内存的数据进行同步,那么就会出现不一致问题。如果存在不一致的问题,一个线程对一个共享数据所做的修改就不能被另一个线程看到。 +可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 -volatile 可以保证可见性,它在修改一个共享数据时会将该值从工作内存同步到主内存,并且对一个共享数据进行读取时会先从主内存同步到工作内存。 +volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 -synchronized 也能够保证可见性,他能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主内存当中。不过只有对共享变量的 set() 和 get() 方法都加上 synchronized 才能保证可见性,如果只有 set() 方法加了 synchronized,那么 get() 方法并不能保证会从内存中读取最新的数据。 +对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题。因为 volatile 并不能保证操作的原子性。 + +// TODO:volatile 不能解决线程不安全问题的示例代码。 ### 3. 有序性 +有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。 + 在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。 -也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。 +也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。 ## 先行发生原则 @@ -490,11 +1177,11 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 ### 1. 单一线程原则 -> Single thread rule +> Single Thread rule 在一个线程内,在程序前面的操作先行发生于后面的操作。 -

+

### 2. 管程锁定规则 @@ -502,7 +1189,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。 -

+

### 3. volatile 变量规则 @@ -510,15 +1197,15 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。 -

+

### 4. 线程启动规则 > Thread Start Rule -Thread 对象的 start() 方法先行发生于此线程的每一个动作。 +Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。 -

+

### 5. 线程加入规则 @@ -526,7 +1213,7 @@ Thread 对象的 start() 方法先行发生于此线程的每一个动作。 join() 方法返回先行发生于 Thread 对象的结束。 -

+

### 6. 线程中断规则 @@ -546,104 +1233,117 @@ join() 方法返回先行发生于 Thread 对象的结束。 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。 -# 八、线程安全 - -当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。 《Java Concurrency In Practice》 +# 十一、线程安全 ## 线程安全分类 +线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。 + ### 1. 不可变 不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施,只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。 -不可变的类: +不可变的类型: +- final 关键字修饰的基本数据类型; - String +- 枚举类型 - Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。 -可以使用 final 关键字修饰一个基本数据类型的共享数据,使它具有不可变性。 - -### 2. 绝对线程安全 - -在 Java API 中标注自己是线程安全的类,大多数都不是绝对的线程安全。例如Vector 是一个线程安全的容器,它的方法被 synchronized 被修饰同步。即使是这样,也不意味着调用它的时候永远都不再需要同步手段了。 - -对于下面的代码,在多线程的环境中,如果不在方法调用端做额外的同步措施的话,使用这段代码仍然是不安全的。因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号 i 已经不再可用的话,再用 i 访问数组就会抛出一个 ArrayIndexOutOfBoundsException。 +对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。 ```java -private static Vector vector = new Vector(); - -public static void main(String[] args) { - while (true) { - for (int i = 0; i < 10; i++) { - vector.add(i); - } - - Thread removeThread = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < vector.size(); i++) { - vector.remove(i); - } - } - }); - - Thread printThread = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < vector.size(); i++) { - System.out.println((vector.get(i))); - } - } - }); - - removeThread.start(); - printThread.start(); - - while (Thread.activeCount() > 20); +public class ImmutableExample { + public static void main(String[] args) { + Map map = new HashMap<>(); + Map unmodifiableMap = Collections.unmodifiableMap(map); + unmodifiableMap.put("a", 1); } } ``` ```html -Exception in thread "Thread-132" java.lang.ArrayIndexOutOfBoundsException: -Array index out of range:17 -at java.util.Vector.remove(Vector.java:777) -at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java:21) -at java.lang.Thread.run(Thread.java:662) +Exception in thread "main" java.lang.UnsupportedOperationException + at java.util.Collections$UnmodifiableMap.put(Collections.java:1457) + at ImmutableExample.main(ImmutableExample.java:9) ``` -如果要保证上面的代码能正确执行下去,就需要对 removeThread 和 printThread 中的方法进行同步。 +Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。 ```java - Thread removeThread = new Thread(new Runnable() { - @Override - public void run() { - synchronized (vector) { - for (int i = 0; i < vector.size(); i++) { - vector.remove(i); - } - } - } -}); - -Thread printThread = new Thread(new Runnable() { - @Override - public void run() { - synchronized (vector) { - for (int i = 0; i < vector.size(); i++) { - System.out.println((vector.get(i))); - } - } - } -}); +public V put(K key, V value) { + throw new UnsupportedOperationException(); +} ``` +多线程环境下,应当尽量使对象成为不可变,来满足线程安全。 + +### 2. 绝对线程安全 + +不管运行时环境如何,调用者都不需要任何额外的同步措施。 + ### 3. 相对线程安全 -相对的线程安全需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。 +相对的线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。 在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。 +对于下面的代码,如果删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。 + +```java +public class VectorUnsafeExample { + private static Vector vector = new Vector<>(); + + public static void main(String[] args) { + while (true) { + for (int i = 0; i < 100; i++) { + vector.add(i); + } + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> { + for (int i = 0; i < vector.size(); i++) { + vector.remove(i); + } + }); + executorService.execute(() -> { + for (int i = 0; i < vector.size(); i++) { + vector.get(i); + } + }); + executorService.shutdown(); + } + } +} +``` + +```html +Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3 + at java.util.Vector.remove(Vector.java:831) + at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14) + at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source) + at java.lang.Thread.run(Thread.java:745) +``` + + +如果要保证上面的代码能正确执行下去,就需要对删除元素和获取元素的代码进行同步。 + +```java +executorService.execute(() -> { + synchronized (vector) { + for (int i = 0; i < vector.size(); i++) { + vector.remove(i); + } + } +}); +executorService.execute(() -> { + synchronized (vector) { + for (int i = 0; i < vector.size(); i++) { + vector.get(i); + } + } +}); +``` + ### 4. 线程兼容 线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。 @@ -654,8 +1354,6 @@ Thread printThread = new Thread(new Runnable() { ## 线程安全的实现方法 -如何实现线程安全与代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了非常重要的作用。 - ### 1. 互斥同步 synchronized 和 ReentrantLock。 @@ -672,33 +1370,189 @@ CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以 J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。 -ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 +在下面的代码 1 中,使用了 AtomicInteger 执行了自增的操作。代码 2 是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。代码 3 是 getAndAddInt() 源码,var1 指示内存位置,var2 指示新值,var4 指示操作需要加的数值,这里为 1。在代码 3 的实现中,通过 getIntVolatile(var1, var2) 得到旧的预期值。通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2=var5,那么就更新内存地址为 var1 的变量为 var5+var4。可以看到代码 3 是在一个循环中进行,发生冲突的做法是不断的进行重试。 + +```java +// 代码 1 +private AtomicInteger cnt = new AtomicInteger(); + +public void add() { + cnt.incrementAndGet(); +} +``` + +```java +// 代码 2 +public final int incrementAndGet() { + return unsafe.getAndAddInt(this, valueOffset, 1) + 1; +} +``` + +```java +// 代码 3 +public final int getAndAddInt(Object var1, long var2, int var4) { + int var5; + do { + var5 = this.getIntVolatile(var1, var2); + } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); + + return var5; +} +``` + +ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 ### 3. 无同步方案 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。 -**可重入代码(Reentrant Code)** +**(一)可重入代码(Reentrant Code)** 这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。 可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。 -**线程本地存储(Thread Local Storage)** +**(二)栈封闭** + +多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。 + +```java +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class StackClosedExample { + public void add100() { + int cnt = 0; + for (int i = 0; i < 100; i++) { + cnt++; + } + System.out.println(cnt); + } + + public static void main(String[] args) { + StackClosedExample example = new StackClosedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> example.add100()); + executorService.execute(() -> example.add100()); + executorService.shutdown(); + } +} +``` + +```html +100 +100 +``` + +**(三)线程本地存储(Thread Local Storage)** 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。 符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。 -Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变的”;如果一个变量要被某个线程独享,Java 中就没有类似 C++中 \_\_declspec(thread)这样的关键字,不过还是可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。每一个线程的 Thread 对象中都有一个 ThreadLocalMap 对象,这个对象存储了一组以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的 K-V 值对,ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,每一个 ThreadLocal 对象都包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程 K-V 值对中找回对应的本地线程变量。 +可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。 -ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下, 由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 +对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。 -## 锁优化 +```java +public class ThreadLocalExample { + public static void main(String[] args) { + ThreadLocal threadLocal = new ThreadLocal(); + Thread thread1 = new Thread(() -> { + threadLocal.set(1); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(threadLocal.get()); + threadLocal.remove(); + }); + Thread thread2 = new Thread(() -> { + threadLocal.set(2); + threadLocal.remove(); + }); + thread1.start(); + thread2.start(); + } +} +``` + +```html +1 +``` + +为了理解 ThreadLocal,先看以下代码: + +```java +public class ThreadLocalExample1 { + public static void main(String[] args) { + ThreadLocal threadLocal1 = new ThreadLocal(); + ThreadLocal threadLocal2 = new ThreadLocal(); + Thread thread1 = new Thread(() -> { + threadLocal1.set(1); + threadLocal2.set(1); + }); + Thread thread2 = new Thread(() -> { + threadLocal1.set(2); + threadLocal2.set(2); + }); + thread1.start(); + thread2.start(); + } +} +``` + +它所对应的底层结构图为: + +

+ +每个 Thread 都有一个 TreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。 + +```java +/* ThreadLocal values pertaining to this thread. This map is maintained + * by the ThreadLocal class. */ +ThreadLocal.ThreadLocalMap threadLocals = null; +``` + +当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。 + +```java +public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); +} +``` + +get() 方法类似。 + +```java +public T get() { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + return setInitialValue(); +} +``` + +ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 + +# 十二、锁优化 高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。 -### 1. 自旋锁与自适应自旋 +## 自旋锁与自适应自旋 前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 @@ -706,7 +1560,7 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因 在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 -### 2. 锁消除 +## 锁消除 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。 @@ -731,7 +1585,7 @@ public static String concatString(String s1, String s2, String s3) { ``` 每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。 -### 3. 锁粗化 +## 锁粗化 原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小:只在共享数据的实际作用域中才进行同步。这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。 @@ -739,7 +1593,7 @@ public static String concatString(String s1, String s2, String s3) { 上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。 -### 4. 轻量级锁 +## 轻量级锁 轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重要级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 @@ -747,7 +1601,7 @@ public static String concatString(String s1, String s2, String s3) { 简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word)。然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态。 -

+

如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是的话只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 @@ -755,7 +1609,7 @@ public static String concatString(String s1, String s2, String s3) { 轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。 -### 5. 偏向锁 +## 偏向锁 偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。 @@ -765,24 +1619,31 @@ public static String concatString(String s1, String s2, String s3) { 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。 -

+

偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。 -# 九、多线程开发良好的实践 +# 十三、多线程开发良好的实践 -1. 给线程起个有意义的名字,这样可以方便找 Bug; +- 给线程起个有意义的名字,这样可以方便找 Bug。 -2. 因为锁花费的代价很高,应该尽可能减小同步范围; +- 缩小同步范围,例如对于 synchronized,应该尽量使用同步块而不是同步方法。 -3. 多用同步类少用 wait 和 notify。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。 +- 多用同步类少用 wait() 和 notify()。首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。 -4. 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashttable。 +- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashtable。 + +- 使用本地变量和不可变类来保证线程安全。 + +- 使用线程池而不是直接创建 Thread 对象,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。 + +- 使用 BlockingQueue 实现生产者消费者问题。 # 参考资料 - BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007. - 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011. +- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html) - [线程通信](http://ifeve.com/thread-signaling/#missed_signal) - [Java 线程面试题 Top 50](http://www.importnew.com/12773.html) - [BlockingQueue](http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html) @@ -790,3 +1651,7 @@ public static String concatString(String s1, String s2, String s3) { - [CSC 456 Spring 2012/ch7 MN](http://wiki.expertiza.ncsu.edu/index.php/CSC_456_Spring_2012/ch7_MN) - [Java - Understanding Happens-before relationship](https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html) - [6장 Thread Synchronization](https://www.slideshare.net/novathinker/6-thread-synchronization) +- [How is Java's ThreadLocal implemented under the hood?](https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood/15653015) +- [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1) +- [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example") +- [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/) diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md index 81c5dff0..852e171c 100644 --- a/notes/Java 虚拟机.md +++ b/notes/Java 虚拟机.md @@ -1,9 +1,9 @@ * [一、运行时数据区域](#一运行时数据区域) * [程序计数器](#程序计数器) - * [Java 虚拟机栈](#java-虚拟机栈) + * [虚拟机栈](#虚拟机栈) * [本地方法栈](#本地方法栈) - * [Java 堆](#java-堆) + * [堆](#堆) * [方法区](#方法区) * [运行时常量池](#运行时常量池) * [直接内存](#直接内存) @@ -27,17 +27,17 @@ # 一、运行时数据区域 -

+

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

+

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

+

-## Java 堆 +## 堆 所有对象实例都在这里分配内存。 -是垃圾收集的主要区域("GC 堆 "),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把 Java 堆分成以下三块: +是垃圾收集的主要区域("GC 堆"),现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把 Java 堆分成以下三块: - 新生代(Young Generation) - 老年代(Old Generation) @@ -102,7 +102,7 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用 ## 直接内存 -在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 +在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 # 二、垃圾收集 @@ -142,7 +142,7 @@ Java 对引用的概念进行了扩充,引入四种强度不同的引用类型 **(一)强引用** -只要强引用存在,垃圾回收器永远不会回收调掉被引用的对象。 +只要强引用存在,垃圾回收器永远不会回收被引用的对象。 使用 new 一个新对象的方式来创建强引用。 @@ -154,7 +154,7 @@ Object obj = new Object(); 用来描述一些还有用但是并非必需的对象。 -在系统将要发生内存溢出异常之前,将会对这些对象列进回收范围之中进行第二次回收。 +在系统将要发生内存溢出异常之前,会将这些对象列进回收范围之中进行第二次回收。 软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。 @@ -221,12 +221,16 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 不足: -1. 标记和清除过程效率都不高 +1. 标记和清除过程效率都不高; 2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存。 -之后的算法都是基于该算法进行改进。 +### 2. 标记 - 整理 -### 2. 复制 +

+ +让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 + +### 3. 复制

@@ -236,12 +240,6 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。 -### 3. 标记 - 整理 - -

- -让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 - ### 4. 分代收集 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 @@ -394,7 +392,7 @@ JVM 为对象定义年龄计数器,经过 Minor GC 依然存活,并且能被 ### 4. 动态对象年龄判定 -JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无序等待 MaxTenuringThreshold 中要求的年龄。 +JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等待 MaxTenuringThreshold 中要求的年龄。 ### 5. 空间分配担保 @@ -418,7 +416,7 @@ JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才 ### 4. JDK 1.7 及以前的永久代空间不足 -在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 +在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 ### 5. Concurrent Mode Failure @@ -448,7 +446,7 @@ JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才 虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生): -1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。 +1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。 2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。 @@ -507,19 +505,19 @@ System.out.println(ConstClass.HELLOWORLD); 主要有以下 4 个阶段: -**(一)文件格式验证** +(一)文件格式验证 验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。 -**(二)元数据验证** +(二)元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。 -**(三)字节码验证** +(三)字节码验证 通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。 -**(四)符号引用验证** +(四)符号引用验证 发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。 @@ -598,7 +596,7 @@ public static void main(String[] args) { ### 1. 类与类加载器 -对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 +对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 ### 2. 类加载器分类 @@ -610,11 +608,11 @@ public static void main(String[] args) { 从 Java 开发人员的角度看,类加载器可以划分得更细致一些: -- 启动类加载器(Bootstrap ClassLoader) 此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 +- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 -- 扩展类加载器(Extension ClassLoader) 这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 +- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 -- 应用程序类加载器(Application ClassLoader) 这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 +- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 ### 3. 双亲委派模型 @@ -634,7 +632,7 @@ public static void main(String[] args) { ```java protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ - //check the class has been loaded or not + // 先检查请求的类是否已经被加载过了 Class c = findLoadedClass(name); if(c == null) { try{ @@ -644,9 +642,10 @@ protected synchronized Class loadClass(String name, boolean resolve) throws C c = findBootstrapClassOrNull(name); } } catch(ClassNotFoundException e) { - //if throws the exception , the father can not complete the load + // 如果父类加载器抛出 ClassNotFoundException,说明父类加载器无法完成加载请求 } if(c == null) { + // 如果父类加载器无法完成加载请求,再调用自身的 findClass() 来进行加载 c = findClass(name); } } diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md index 6be5727b..3520557c 100644 --- a/notes/Leetcode 题解.md +++ b/notes/Leetcode 题解.md @@ -15,7 +15,7 @@ * [动态规划](#动态规划) * [斐波那契数列](#斐波那契数列) * [最长递增子序列](#最长递增子序列) - * [最长公共子系列](#最长公共子系列) + * [最长公共子序列](#最长公共子序列) * [0-1 背包](#0-1-背包) * [数组区间](#数组区间) * [字符串编辑](#字符串编辑) @@ -36,8 +36,6 @@ * [哈希表](#哈希表) * [字符串](#字符串) * [数组与矩阵](#数组与矩阵) - * [1-n 分布](#1-n-分布) - * [有序矩阵](#有序矩阵) * [链表](#链表) * [树](#树) * [递归](#递归) @@ -70,11 +68,11 @@ public int search(int key, int[] array) { 实现时需要注意以下细节: -1. 在计算 mid 时不能使用 mid = (l + h) / 2 这种方式,因为 l + h 可能会导致加法溢出,应该使用 mid = l + (h - l) / 2。 +- 在计算 mid 时不能使用 mid = (l + h) / 2 这种方式,因为 l + h 可能会导致加法溢出,应该使用 mid = l + (h - l) / 2。 -2. 对 h 的赋值和循环条件有关,当循环条件为 l <= h 时,h = mid - 1;当循环条件为 l < h 时,h = mid。解释如下:在循环条件为 l <= h 时,如果 h = mid,会出现循环无法退出的情况,例如 l = 1,h = 1,此时 mid 也等于 1,如果此时继续执行 h = mid,那么就会无限循环;在循环条件为 l < h,如果 h = mid - 1,会错误跳过查找的数,例如对于数组 [1,2,3],要查找 1,最开始 l = 0,h = 2,mid = 1,判断 key < arr[mid] 执行 h = mid - 1 = 0,此时循环退出,直接把查找的数跳过了。 +- 对 h 的赋值和循环条件有关,当循环条件为 l <= h 时,h = mid - 1;当循环条件为 l < h 时,h = mid。解释如下:在循环条件为 l <= h 时,如果 h = mid,会出现循环无法退出的情况,例如 l = 1,h = 1,此时 mid 也等于 1,如果此时继续执行 h = mid,那么就会无限循环;在循环条件为 l < h,如果 h = mid - 1,会错误跳过查找的数,例如对于数组 [1,2,3],要查找 1,最开始 l = 0,h = 2,mid = 1,判断 key < arr[mid] 执行 h = mid - 1 = 0,此时循环退出,直接把查找的数跳过了。 -3. l 的赋值一般都为 l = mid + 1。 +- l 的赋值一般都为 l = mid + 1。 **求开方** @@ -129,7 +127,7 @@ public int arrangeCoins(int n) { int l = 0, h = n; while(l <= h){ int m = l + (h - l) / 2; - long x = m * (m + 1L) / 2; + long x = m * (m + 1) / 2; if(x == n) return m; else if(x < n) l = m + 1; else h = m - 1; @@ -192,11 +190,11 @@ You have 3 cookies and their sizes are big enough to gratify all of the children You need to output 2. ``` -题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 +题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 因为最小的孩子最容易得到满足,因此先满足最小孩子。给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。 -证明:假设在某次选择中,贪心策略选择给第 i 个孩子分配第 m 个饼干,并且第 i 个孩子满足度最小,第 m 个饼干为可以满足第 i 个孩子的最小饼干,利用贪心策略最终可以满足 k 个孩子。假设最优策略在这次选择中给 i 个孩子分配第 n 个饼干,并且这个饼干大于第 m 个饼干。我们发现使用第 m 个饼干去替代第 n 个饼干完全不影响后续的结果,因此不存在比贪心策略更优的策略,即贪心策略就是最优策略。 +证明:假设在某次选择中,贪心策略选择给第 i 个孩子分配第 m 个饼干,并且第 i 个孩子满足度最小,第 m 个饼干为可以满足第 i 个孩子的最小饼干。假设最优策略在这次选择中给 i 个孩子分配第 n 个饼干,并且这个饼干大于第 m 个饼干。我们发现使用第 m 个饼干去替代第 n 个饼干完全不影响后续的结果,因此不存在比贪心策略更优的策略,即贪心策略就是最优策略。 ```java public int findContentChildren(int[] g, int[] s) { @@ -253,7 +251,7 @@ public int findMinArrowShots(int[][] points) { 题目描述:一次交易包含买入和卖出,多个交易之间不能交叉进行。 -对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加加到收益中,从而在局部最优的情况下也保证全局最优。 +对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中,从而在局部最优的情况下也保证全局最优。 ```java public int maxProfit(int[] prices) { @@ -331,9 +329,10 @@ Return true. ```java public boolean isSubsequence(String s, String t) { - for (int i = 0, pos = 0; i < s.length(); i++, pos++) { - pos = t.indexOf(s.charAt(i), pos); - if(pos == -1) return false; + int index = 0; + for (char c : s.toCharArray()) { + index = t.indexOf(c, index); + if (index == -1) return false; } return true; } @@ -355,18 +354,20 @@ A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits ```java public List partitionLabels(String S) { List ret = new ArrayList<>(); - int[] lastIdxs = new int[26]; - for(int i = 0; i < S.length(); i++) lastIdxs[S.charAt(i) - 'a'] = i; - int startIdx = 0; - while(startIdx < S.length()) { - int endIdx = startIdx; - for(int i = startIdx; i < S.length() && i <= endIdx; i++) { - int lastIdx = lastIdxs[S.charAt(i) - 'a']; - if(lastIdx == i) continue; - if(lastIdx > endIdx) endIdx = lastIdx; + int[] lastIndexs = new int[26]; + for (int i = 0; i < S.length(); i++) { + lastIndexs[S.charAt(i) - 'a'] = i; + } + int firstIndex = 0; + while (firstIndex < S.length()) { + int lastIndex = firstIndex; + for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) { + int index = lastIndexs[S.charAt(i) - 'a']; + if (index == i) continue; + if (index > lastIndex) lastIndex = index; } - ret.add(endIdx - startIdx + 1); - startIdx = endIdx + 1; + ret.add(lastIndex - firstIndex + 1); + firstIndex = lastIndex + 1; } return ret; } @@ -392,23 +393,21 @@ Output: ```java public int[][] reconstructQueue(int[][] people) { - if(people == null || people.length == 0 || people[0].length == 0) return new int[0][0]; - - Arrays.sort(people, new Comparator() { - public int compare(int[] a, int[] b) { - if(a[0] == b[0]) return a[1] - b[1]; - return b[0] - a[0]; - } + if (people == null || people.length == 0 || people[0].length == 0) return new int[0][0]; + Arrays.sort(people, (a, b) -> { + if (a[0] == b[0]) return a[1] - b[1]; + return b[0] - a[0]; }); - - int n = people.length; + int N = people.length; List tmp = new ArrayList<>(); - for(int i = 0; i < n; i++) { - tmp.add(people[i][1], new int[]{people[i][0], people[i][1]}); + for (int i = 0; i < N; i++) { + int index = people[i][1]; + int[] p = new int[]{people[i][0], people[i][1]}; + tmp.add(index, p); } - int[][] ret = new int[n][2]; - for(int i = 0; i < n; i++) { + int[][] ret = new int[N][2]; + for (int i = 0; i < N; i++) { ret[i][0] = tmp.get(i)[0]; ret[i][1] = tmp.get(i)[1]; } @@ -420,7 +419,7 @@ public int[][] reconstructQueue(int[][] people) { 双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 -**从一个已经排序的数组中找出两个数,使它们的和为 0** +**有序数组的 Tow Sum** [Leetcode :167. Two Sum II - Input array is sorted (Easy)](https://leetcode.com/problems/two-sum-ii-input-array-is-sorted/description/) @@ -429,6 +428,8 @@ Input: numbers={2, 7, 11, 15}, target=9 Output: index1=1, index2=2 ``` +题目描述:从一个已经排序的数组中找出两个数,使它们的和为 0。 + 使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。 如果两个指针指向元素的和 sum == target,那么得到要求的结果;如果 sum > target,移动较大的元素,使 sum 变小一些;如果 sum < target,移动较小的元素,使 sum 变大一些。 @@ -457,22 +458,22 @@ Given s = "leetcode", return "leotcede". 使用双指针,指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。 ```java -private HashSet vowels = new HashSet<>(Arrays.asList('a','e','i','o','u','A','E','I','O','U')); +private HashSet vowels = new HashSet<>(Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')); public String reverseVowels(String s) { - if(s.length() == 0) return s; + if (s.length() == 0) return s; int i = 0, j = s.length() - 1; char[] result = new char[s.length()]; - while(i <= j){ + while (i <= j) { char ci = s.charAt(i); char cj = s.charAt(j); - if(!vowels.contains(ci)){ + if (!vowels.contains(ci)) { result[i] = ci; i++; - } else if(!vowels.contains(cj)){ + } else if (!vowels.contains(cj)) { result[j] = cj; j--; - } else{ + } else { result[i] = cj; result[j] = ci; i++; @@ -497,12 +498,12 @@ Explanation: 1 * 1 + 2 * 2 = 5 ```java public boolean judgeSquareSum(int c) { - int left = 0, right = (int) Math.sqrt(c); - while(left <= right){ - int powSum = left * left + right * right; - if(powSum == c) return true; - else if(powSum > c) right--; - else left++; + int i = 0, j = (int) Math.sqrt(c); + while (i <= j) { + int powSum = i * i + j * j; + if (powSum == c) return true; + if (powSum > c) j--; + else i++; } return false; } @@ -522,9 +523,9 @@ Explanation: You could delete the character 'c'. ```java public boolean validPalindrome(String s) { - int i = 0, j = s.length() -1; - while(i < j){ - if(s.charAt(i) != s.charAt(j)){ + int i = 0, j = s.length() - 1; + while (i < j) { + if (s.charAt(i) != s.charAt(j)) { return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j); } i++; @@ -533,12 +534,9 @@ public boolean validPalindrome(String s) { return true; } -private boolean isPalindrome(String s, int l, int r){ - while(l < r){ - if(s.charAt(l) != s.charAt(r)) - return false; - l++; - r--; +private boolean isPalindrome(String s, int l, int r) { + while (l < r) { + if (s.charAt(l++) != s.charAt(r--)) return false; } return true; } @@ -553,13 +551,13 @@ private boolean isPalindrome(String s, int l, int r){ ```java public void merge(int[] nums1, int m, int[] nums2, int n) { int i = m - 1, j = n - 1; // 需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值 - int idx = m + n - 1; - while(i >= 0 || j >= 0){ - if(i < 0) nums1[idx] = nums2[j--]; - else if(j < 0) nums1[idx] = nums1[i--]; - else if(nums1[i] > nums2[j]) nums1[idx] = nums1[i--]; - else nums1[idx] = nums2[j--]; - idx--; + int index = m + n - 1; + while (i >= 0 || j >= 0) { + if (i < 0) nums1[index] = nums2[j--]; + else if (j < 0) nums1[index] = nums1[i--]; + else if (nums1[i] > nums2[j]) nums1[index] = nums1[i--]; + else nums1[index] = nums2[j--]; + index--; } } ``` @@ -572,12 +570,12 @@ public void merge(int[] nums1, int m, int[] nums2, int n) { ```java public boolean hasCycle(ListNode head) { - if(head == null) return false; + if (head == null) return false; ListNode l1 = head, l2 = head.next; - while(l1 != null && l2 != null){ - if(l1 == l2) return true; + while (l1 != null && l2 != null) { + if (l1 == l2) return true; l1 = l1.next; - if(l2.next == null) break; + if (l2.next == null) break; l2 = l2.next.next; } return false; @@ -605,8 +603,7 @@ public String findLongestWord(String s, List d) { for (int i = 0, j = 0; i < s.length() && j < str.length(); i++) { if (s.charAt(i) == str.charAt(j)) j++; if (j == str.length()) { - if (ret.length() < str.length() - || (ret.length() == str.length() && ret.compareTo(str) > 0)) { + if (ret.length() < str.length() || (ret.length() == str.length() && ret.compareTo(str) > 0)) { ret = str; } } @@ -620,9 +617,9 @@ public String findLongestWord(String s, List d) { ### 快速选择 -一般用于求解 **Kth Element** 问题,可以在 O(n) 时间复杂度,O(1) 空间复杂度完成求解工作。 +一般用于求解 **Kth Element** 问题,可以在 O(N) 时间复杂度,O(1) 空间复杂度完成求解工作。 -与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 O(n2)。 +与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。 ### 堆排序 @@ -632,24 +629,23 @@ public String findLongestWord(String s, List d) { [Leetocde : 215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/) -**排序** :时间复杂度 O(nlgn),空间复杂度 O(1) +**排序** :时间复杂度 O(NlogN),空间复杂度 O(1) ```java public int findKthLargest(int[] nums, int k) { - int N = nums.length; - Arrays.sort(nums); - return nums[N - k]; + Arrays.sort(nums); + return nums[nums.length - k]; } ``` -**堆排序** :时间复杂度 O(nlgk),空间复杂度 O(k) +**堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。 ```java public int findKthLargest(int[] nums, int k) { PriorityQueue pq = new PriorityQueue<>(); - for(int val : nums) { - pq.offer(val); - if(pq.size() > k) { + for (int val : nums) { + pq.add(val); + if (pq.size() > k) { pq.poll(); } } @@ -657,50 +653,41 @@ public int findKthLargest(int[] nums, int k) { } ``` -**快速选择** :时间复杂度 O(n),空间复杂度 O(1) +**快速选择** :时间复杂度 O(N),空间复杂度 O(1) ```java public int findKthLargest(int[] nums, int k) { - k = nums.length - k; - int lo = 0; - int hi = nums.length - 1; - while (lo < hi) { - final int j = partition(nums, lo, hi); - if(j < k) { - lo = j + 1; - } else if (j > k) { - hi = j - 1; - } else { - break; - } - } - return nums[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; + if (j < k) l = j + 1; + else h = j - 1; } + return nums[k]; +} - private int partition(int[] a, int lo, int hi) { - int i = lo; - int j = hi + 1; - while(true) { - while(i < hi && less(a[++i], a[lo])); - while(j > lo && less(a[lo], a[--j])); - if(i >= j) { - break; - } - exch(a, i, j); - } - exch(a, lo, j); - return j; +private int partition(int[] a, int l, int h) { + int i = l, j = h + 1; + while (true) { + while (i < h && less(a[++i], a[l])) ; + while (j > l && less(a[l], a[--j])) ; + if (i >= j) break; + swap(a, i, j); } + swap(a, l, j); + return j; +} - private void exch(int[] a, int i, int j) { - final int tmp = a[i]; - a[i] = a[j]; - a[j] = tmp; - } +private void swap(int[] a, int i, int j) { + int tmp = a[i]; + a[i] = a[j]; + a[j] = tmp; +} - private boolean less(int v, int w) { - return v < w; - } +private boolean less(int v, int w) { + return v < w; } ``` @@ -719,21 +706,21 @@ Given [1,1,1,2,2,3] and k = 2, return [1,2]. ```java public List topKFrequent(int[] nums, int k) { List ret = new ArrayList<>(); - Map map = new HashMap<>(); - for(int num : nums) { - map.put(num, map.getOrDefault(num, 0) + 1); + Map frequencyMap = new HashMap<>(); + for (int num : nums) { + frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1); } List[] bucket = new List[nums.length + 1]; - for(int key : map.keySet()) { - int frequency = map.get(key); - if(bucket[frequency] == null) { + for (int key : frequencyMap.keySet()) { + int frequency = frequencyMap.get(key); + if (bucket[frequency] == null) { bucket[frequency] = new ArrayList<>(); } bucket[frequency].add(key); } - for(int i = bucket.length - 1; i >= 0 && ret.size() < k; i--) { - if(bucket[i] != null) { + for (int i = bucket.length - 1; i >= 0 && ret.size() < k; i--) { + if (bucket[i] != null) { ret.addAll(bucket[i]); } } @@ -741,6 +728,51 @@ public List topKFrequent(int[] nums, int k) { } ``` +**按照字符出现次数对字符串排序** + +[Leetcode : 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 map = new HashMap<>(); + for (char c : s.toCharArray()) { + map.put(c, map.getOrDefault(c, 0) + 1); + } + List[] frequencyBucket = new List[s.length() + 1]; + for(char c : map.keySet()){ + int f = map.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(); +} +``` + ## 搜索 深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 @@ -760,7 +792,7 @@ public List topKFrequent(int[] nums, int k) { - 6 -> {4} - 2 -> {} - 1 -> {} -- 5 -> {4,3} +- 5 -> {3} 第三层: @@ -819,7 +851,7 @@ private class Position {

-广度优先搜索一层一层遍历,每一层得到到的所有新节点,要用队列先存储起来以备下一层遍历的时候再遍历;而深度优先搜索在得到到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 +广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列先存储起来以备下一层遍历的时候再遍历;而深度优先搜索在得到到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 **可达性** 问题。 @@ -845,20 +877,21 @@ private class Position { ```java public int maxAreaOfIsland(int[][] grid) { - int m = grid.length, n = grid[0].length; int max = 0; - for(int i = 0; i < m; i++){ - for(int j = 0; j < n; j++){ - if(grid[i][j] == 1) max = Math.max(max, dfs(grid, i, j)); + for (int i = 0; i < grid.length; i++) { + for (int j = 0; j < grid[i].length; j++) { + if (grid[i][j] == 1) { + max = Math.max(max, dfs(grid, i, j)); + } } } return max; } -private int dfs(int[][] grid, int i, int j){ - int m = grid.length, n = grid[0].length; - if(i < 0 || i >= m || j < 0 || j >= n) return 0; - if(grid[i][j] == 0) return 0; +private int dfs(int[][] grid, int i, int j) { + if (i < 0 || i >= grid.length || j < 0 || j >= grid[i].length || grid[i][j] == 0) { + return 0; + } grid[i][j] = 0; return dfs(grid, i + 1, j) + dfs(grid, i - 1, j) + dfs(grid, i, j + 1) + dfs(grid, i, j - 1) + 1; } @@ -882,22 +915,22 @@ The 2nd student himself is in a friend circle. So return 2. public int findCircleNum(int[][] M) { int n = M.length; int ret = 0; - boolean[] hasFind = new boolean[n]; - for(int i = 0; i < n; i++) { - if(!hasFind[i]) { - dfs(M, i, hasFind); + boolean[] hasVisited = new boolean[n]; + for (int i = 0; i < n; i++) { + if (!hasVisited[i]) { + dfs(M, i, hasVisited); ret++; } + } return ret; } -private void dfs(int[][] M, int i, boolean[] hasFind) { - hasFind[i] = true; - int n = M.length; - for(int k = 0; k < n; k++) { - if(M[i][k] == 1 && !hasFind[k]) { - dfs(M, k, hasFind); +private void dfs(int[][] M, int i, boolean[] hasVisited) { + hasVisited[i] = true; + for (int k = 0; k < M.length; k++) { + if (M[i][k] == 1 && !hasVisited[k]) { + dfs(M, k, hasVisited); } } } @@ -949,11 +982,11 @@ private void dfs(char[][] grid, int i, int j) { [Leetcode : 257. Binary Tree Paths (Easy)](https://leetcode.com/problems/binary-tree-paths/description/) ```html - 1 -/ \ + 1 + / \ 2 3 \ - 5 + 5 ``` ```html ["1->2->5", "1->3"] @@ -979,6 +1012,41 @@ private void dfs(TreeNode root, String prefix, List ret){ } ``` +**IP 地址划分** + +[Leetcode : 93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/) + +```html +Given "25525511135", +return ["255.255.11.135", "255.255.111.35"]. +``` + +```java +private List ret; + +public List restoreIpAddresses(String s) { + ret = new ArrayList<>(); + doRestore(0, "", s); + return ret; +} + +private void doRestore(int k, String path, String s) { + if (k == 4 || s.length() == 0) { + if (k == 4 && s.length() == 0) { + ret.add(path); + } + return; + } + for (int i = 0; i < s.length() && i <= 2; i++) { + if (i != 0 && s.charAt(0) == '0') break; + String part = s.substring(0, i + 1); + if (Integer.valueOf(part) <= 255) { + doRestore(k + 1, path.length() != 0 ? path + "." + part : part, s.substring(i + 1)); + } + } +} +``` + **填充封闭区域** [Leetcode : 130. Surrounded Regions (Medium)](https://leetcode.com/problems/surrounded-regions/description/) @@ -1101,7 +1169,7 @@ private void dfs(int r, int c, boolean[][] canReach) { ### Backtracking -回溯是 DFS 的一种,它不是用在遍历图的节点上,而是用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串。 +回溯属于 DF,它不是用在遍历图的节点上,而是用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串。 在程序实现时,回溯需要注意对元素进行标记的问题。使用递归实现的回溯,在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;但是在递归返回时,需要将该元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。 @@ -1121,20 +1189,21 @@ private static final String[] KEYS = {"", "", "abc", "def", "ghi", "jkl", "mno", public List letterCombinations(String digits) { List ret = new ArrayList<>(); - if (digits != null && digits.length() != 0) { - combination("", digits, 0, ret); - } + if (digits == null || digits.length() == 0) return ret; + combination(new StringBuilder(), digits, ret); return ret; } -private void combination(String prefix, String digits, int offset, List ret) { - if (offset == digits.length()) { - ret.add(prefix); +private void combination(StringBuilder prefix, String digits, List ret) { + if (prefix.length() == digits.length()) { + ret.add(prefix.toString()); return; } - String letters = KEYS[digits.charAt(offset) - '0']; + String letters = KEYS[digits.charAt(prefix.length()) - '0']; for (char c : letters.toCharArray()) { - combination(prefix + c, digits, offset + 1, ret); + prefix.append(c); + combination(prefix, digits, ret); + prefix.deleteCharAt(prefix.length() - 1); // 删除 } } ``` @@ -1180,55 +1249,22 @@ private boolean dfs(char[][] board, String word, int start, int r, int c) { if (start == word.length()) { return true; } - if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(start) || visited[r][c] ) { + if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != word.charAt(start) || visited[r][c]) { return false; } visited[r][c] = true; for (int i = 0; i < shift.length; i++) { int nextR = r + shift[i][0]; int nextC = c + shift[i][1]; - if (dfs(board, word, start + 1, nextR, nextC)) return true; + if (dfs(board, word, start + 1, nextR, nextC)) { + return true; + } } visited[r][c] = false; return false; } ``` -**IP 地址划分** - -[Leetcode : 93. Restore IP Addresses(Medium)](https://leetcode.com/problems/restore-ip-addresses/description/) - -```html -Given "25525511135", -return ["255.255.11.135", "255.255.111.35"]. -``` - -```java -private List ret; - -public List restoreIpAddresses(String s) { - ret = new ArrayList<>(); - doRestore(0, "", s); - return ret; -} - -private void doRestore(int k, String path, String s) { - if (k == 4 || s.length() == 0) { - if (k == 4 && s.length() == 0) { - ret.add(path); - } - return; - } - for (int i = 0; i < s.length() && i <= 2; i++) { - if (i != 0 && s.charAt(0) == '0') break; - String part = s.substring(0, i + 1); - if (Integer.valueOf(part) <= 255) { - doRestore(k + 1, path.length() != 0 ? path + "." + part : part, s.substring(i + 1)); - } - } -} -``` - **排列** [Leetcode : 46. Permutations (Medium)](https://leetcode.com/problems/permutations/description/) @@ -1296,12 +1332,12 @@ public List> permuteUnique(int[] nums) { private void backtracking(List permuteList, boolean[] visited, int[] nums, List> ret) { if (permuteList.size() == nums.length) { - ret.add(new ArrayList(permuteList)); + ret.add(new ArrayList(permuteList)); // 重新构造一个 List return; } for (int i = 0; i < visited.length; i++) { - if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue; + if (i != 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue; // 防止重复 if (visited[i]) continue; visited[i] = true; permuteList.add(nums[i]); @@ -1338,7 +1374,7 @@ public List> combine(int n, int k) { private void backtracking(int start, int n, int k, List combineList, List> ret){ if(k == 0){ - ret.add(new ArrayList(combineList)); // 这里要重新构造一个 List + ret.add(new ArrayList(combineList)); return; } @@ -1427,6 +1463,46 @@ private void doCombination(int[] candidates, int target, int start, List> combinationSum3(int k, int n) { + List> ret = new ArrayList<>(); + List path = new ArrayList<>(); + for (int i = 1; i <= 9; i++) { + path.add(i); + backtracking(k - 1, n - i, path, i, ret); + path.remove(0); + } + return ret; +} + +private void backtracking(int k, int n, List path, int start, List> ret) { + if (k == 0 && n == 0) { + ret.add(new ArrayList<>(path)); + return; + } + if (k == 0 || n == 0) return; + for (int i = start + 1; i <= 9; i++) { // 只能访问下一个元素,防止遍历的结果重复 + path.add(i); + backtracking(k - 1, n - i, path, i, ret); + path.remove(path.size() - 1); + } +} +``` + **子集** [Leetcode : 78. Subsets (Medium)](https://leetcode.com/problems/subsets/description/) @@ -1440,7 +1516,7 @@ private List subsetList; public List> subsets(int[] nums) { ret = new ArrayList<>(); subsetList = new ArrayList<>(); - for (int i = 0; i <= nums.length; i++) { + for (int i = 0; i <= nums.length; i++) { // 不同的子集大小 backtracking(0, i, nums); } return ret; @@ -1717,7 +1793,7 @@ public List diffWaysToCompute(String input) { ## 动态规划 -递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解。 +递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 ### 斐波那契数列 @@ -1733,14 +1809,13 @@ public List diffWaysToCompute(String input) { dp[N] 即为所求。 -考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2] 即可,使得原来的 O(n) 空间复杂度优化为 O(1) 复杂度。 +考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 ```java public int climbStairs(int n) { - if(n == 1) return 1; - if(n == 2) return 2; - int pre1 = 2, pre2 = 1; - for(int i = 2; i < n; i++){ + if (n <= 2) return n; + int pre2 = 1, pre1 = 2; + for (int i = 2; i < n; i++) { int cur = pre1 + pre2; pre2 = pre1; pre1 = cur; @@ -1839,11 +1914,10 @@ private int rob(int[] nums, int first, int last) { 题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 -定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: +定义一个数组 dp 存储错误方式数量,dp[i] 表示 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: -① i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。 - -② i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (n-1)\*dp[i-1] 种错误装信方式。 +- i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。 +- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (n-1)\*dp[i-1] 种错误装信方式。 综上所述,错误装信数量方式数量为: @@ -1861,11 +1935,11 @@ dp[N] 即为所求。 定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn ,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 -因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,因此需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: +因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:

-对于一个长度为 N 的序列,最长子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。 +对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。 **最长递增子序列** @@ -1875,22 +1949,24 @@ dp[N] 即为所求。 public int lengthOfLIS(int[] nums) { int n = nums.length; int[] dp = new int[n]; - for(int i = 0; i < n; i++){ + for (int i = 0; i < n; i++) { int max = 1; - for(int j = 0; j < i; j++){ - if(nums[i] > nums[j]) max = Math.max(max, dp[j] + 1); + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + max = Math.max(max, dp[j] + 1); + } } dp[i] = max; } int ret = 0; - for(int i = 0; i < n; i++){ + for (int i = 0; i < n; i++) { ret = Math.max(ret, dp[i]); } return ret; } ``` -以上解法的时间复杂度为 O(n2) ,可以使用二分查找使得时间复杂度降低为 O(nlogn)。定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素,例如对于数组 [4,5,6,3],有 +以上解法的时间复杂度为 O(N2) ,可以使用二分查找将时间复杂度降低为 O(NlogN)。定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。如果有多个长度相等的最长递增子序列,那么 tails[i] 就取最小值。例如对于数组 [4,5,6,3],有 ```html len = 1 : [4], [5], [6], [3] => tails[0] = 3 @@ -1898,7 +1974,11 @@ len = 2 : [4, 5], [5, 6] => tails[1] = 5 len = 3 : [4, 5, 6] => tails[2] = 6 ``` -对于一个元素 x,如果它大于 tails 数组所有的值,那么把它添加到 tails 后面;如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x 。 + +对于一个元素 x, + +- 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1; +- 如果 tails[i-1] < x <= tails[i],那么更新 tails[i-1] = x。 可以看出 tails 数组保持有序,因此在查找 Si 位于 tails 数组的位置时就可以使用二分查找。 @@ -1926,6 +2006,43 @@ private int binarySearch(int[] nums, int first, int last, int key) { } ``` +**一组整数对能够构成的最长链** + +[Leetcode : 646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) + +```html +Input: [[1,2], [2,3], [3,4]] +Output: 2 +Explanation: The longest chain is [1,2] -> [3,4] +``` + +题目描述:对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。 + +```java +public int findLongestChain(int[][] pairs) { + if(pairs == null || pairs.length == 0) { + return 0; + } + Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); + int n = pairs.length; + int[] dp = new int[n]; + Arrays.fill(dp, 1); + for(int i = 0; i < n; i++) { + for(int j = 0; j < i; j++) { + if(pairs[i][0] > pairs[j][1]){ + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + } + + int ret = 0; + for(int num : dp) { + ret = Math.max(ret, num); + } + return ret; +} +``` + **最长摆动子序列** [Leetcode : 376. Wiggle Subsequence (Medium)](https://leetcode.com/problems/wiggle-subsequence/description/) @@ -1960,17 +2077,16 @@ public int wiggleMaxLength(int[] nums) { } ``` -### 最长公共子系列 +### 最长公共子序列 对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: -① 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。 +- 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。 +- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,与 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 -② 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,与 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 - -综上,最长公共子系列的状态转移方程为: +综上,最长公共子序列的状态转移方程为:

@@ -1978,9 +2094,9 @@ public int wiggleMaxLength(int[] nums) { 与最长递增子序列相比,最长公共子序列有以下不同点: -① 针对的是两个序列,求它们的最长公共子序列。 -② 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。 -③ 由于 2 ,在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 +- 针对的是两个序列,求它们的最长公共子序列。 +- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。 +- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 ```java public int lengthOfLCS(int[] nums1, int[] nums2) { @@ -2000,10 +2116,10 @@ public int lengthOfLCS(int[] nums1, int[] nums2) { 有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 -定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示体积不超过 j 的情况下,前 i 件物品能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: +定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: -① 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 -② 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 +- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 +- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。 @@ -2013,19 +2129,18 @@ public int lengthOfLCS(int[] nums1, int[] nums2) { ```java public int knapsack(int W, int N, int[] weights, int[] values) { - int[][] dp = new int[N][W]; - for (int i = W - 1; i >= 0; i--) { - dp[0][i] = i > weights[0] ? values[0] : 0; - } - for (int i = 1; i < N; i++) { - for (int j = W - 1; j >= weights[i]; j--) { - dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]); - } - for (int j = weights[i] - 1; j >= 0; j--) { - dp[i][j] = dp[i - 1][j]; + int[][] dp = new int[N + 1][W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = 1; j <= W; j++) { + if (j >= w) { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); + } else { + dp[i][j] = dp[i - 1][j]; + } } } - return dp[N - 1][W - 1]; + return dp[N][W]; } ``` @@ -2035,7 +2150,22 @@ public int knapsack(int W, int N, int[] weights, int[] values) {

-因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w] 防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 +因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 + +```java +public int knapsack(int W, int N, int[] weights, int[] values) { + int[] dp = new int[W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = W; j >= 1; j--) { + if (j >= w) { + dp[j] = Math.max(dp[j], dp[j - w] + v); + } + } + } + return dp[W]; +} +``` **无法使用贪心算法的解释** @@ -2049,13 +2179,13 @@ public int knapsack(int W, int N, int[] weights, int[] values) { **变种** -完全背包:物品可以无限个,可以转换为 0-1 背包,令每种物品的体积和价值变为 1/2/4... 倍数,把它们都当成一个新物品,然后一种物品只能添加一次。 +- 完全背包:物品数量为无限个 -多重背包:物品数量有限制,同样可以转换为 0-1 背包。 +- 多重背包:物品数量有限制 -多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制。 +- 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制 -其它:物品之间相互约束或者依赖。 +- 其它:物品之间相互约束或者依赖 **划分数组为和相等的两部分** @@ -2069,33 +2199,25 @@ Output: true Explanation: The array can be partitioned as [1, 5, 5] and [11]. ``` -可以看成一个背包大小为 sum/2 的 0-1 背包问题,但是也有不同的地方,这里没有价值属性,并且背包必须被填满。 - -以下实现使用了空间优化。 +可以看成一个背包大小为 sum/2 的 0-1 背包问题。 ```java -public boolean canPartition(int[] nums) { - int sum = 0; - for (int num : nums) { - sum += num; - } - if (sum % 2 != 0) { - return false; - } - int W = sum / 2; - boolean[] dp = new boolean[W + 1]; - for (int i = 0; i <= W; i++) { - if (nums[0] == i) { - dp[i] = true; - } - } - for (int i = 1; i < nums.length; i++) { - for (int j = W; j >= nums[i]; j--) { - dp[j] = dp[j] || dp[j - nums[i]]; - } - } - return dp[W]; -} + public boolean canPartition(int[] nums) { + int sum = 0; + for (int num : nums) sum += num; + if (sum % 2 != 0) return false; + int W = sum / 2; + boolean[] dp = new boolean[W + 1]; + dp[0] = true; + for (int num : nums) { // 0-1 背包一个物品只能用一次 + for (int i = W; i >= 0; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num] + if (num <= i) { + dp[i] = dp[i] || dp[i - num]; + } + } + } + return dp[W]; + } ``` **字符串按单词列表分割** @@ -2108,15 +2230,20 @@ dict = ["leet", "code"]. Return true because "leetcode" can be segmented as "leet code". ``` +这是一个完全背包问题,和 0-1 背包不同的是,完全背包中物品可以使用多次。在这一题当中,词典中的单词可以被使用多次。 + +0-1 背包和完全背包在实现上的不同之处是,0-1 背包对物品的迭代是在最外层,而完全背包对物品的迭代是在最里层。 + ```java public boolean wordBreak(String s, List wordDict) { int n = s.length(); boolean[] dp = new boolean[n + 1]; dp[0] = true; for (int i = 1; i <= n; i++) { - for (String word : wordDict) { - if (word.length() <= i && word.equals(s.substring(i - word.length(), i))) { - dp[i] = dp[i] || dp[i - word.length()]; + for (String word : wordDict) { // 每个单词可以使用多次 + int len = word.length(); + if (len <= i && word.equals(s.substring(i - len, i))) { + dp[i] = dp[i] || dp[i - len]; } } } @@ -2142,7 +2269,9 @@ Explanation: There are 5 ways to assign symbols to make the sum of nums be target 3. ``` -该问题可以转换为 subset sum 问题,从而使用 0-1 背包的方法来求解。可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导: +该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。 + +可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导: ```html sum(P) - sum(N) = target @@ -2155,26 +2284,32 @@ sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N) ```java public int findTargetSumWays(int[] nums, int S) { int sum = 0; - for (int num : nums) { - sum += num; - } - if (sum < S || (sum + S) % 2 == 1) { - return 0; - } - return subsetSum(nums, (sum + S) >>> 1); -} - -private int subsetSum(int[] nums, int targetSum) { - Arrays.sort(nums); - int[] dp = new int[targetSum + 1]; + for (int num : nums) sum += num; + if (sum < S || (sum + S) % 2 == 1) return 0; + int W = (sum + S) / 2; + int[] dp = new int[W + 1]; dp[0] = 1; - for (int i = 0; i < nums.length; i++) { - int num = nums[i]; - for (int j = targetSum; j >= num; j--) { - dp[j] = dp[j] + dp[j - num]; + for (int num : nums) { + for (int i = W; i >= 0; i--) { + if (num <= i) { + dp[i] = dp[i] + dp[i - num]; + } } } - return dp[targetSum]; + return dp[W]; +} +``` + +DFS 解法: + +```java +public int findTargetSumWays(int[] nums, int S) { + return findTargetSumWays(nums, 0, S); +} + +private int findTargetSumWays(int[] nums, int start, int S) { + if (start == nums.length) return S == 0 ? 1 : 0; + return findTargetSumWays(nums, start + 1, S + nums[start]) + findTargetSumWays(nums, start + 1, S - nums[start]); } ``` @@ -2186,7 +2321,7 @@ private int subsetSum(int[] nums, int targetSum) { Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 Output: 4 -Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are “10,”0001”,”1”,”0” +Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0" ``` 这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。 @@ -2195,16 +2330,15 @@ Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 public int findMaxForm(String[] strs, int m, int n) { if (strs == null || strs.length == 0) return 0; int[][] dp = new int[m + 1][n + 1]; - for (int i = 0; i < strs.length; i++) { - String s = strs[i]; + for (String s : strs) { // 每个字符串只能用一次 int ones = 0, zeros = 0; for (char c : s.toCharArray()) { if (c == '0') zeros++; - else if (c == '1') ones++; + else ones++; } - for (int j = m; j >= zeros; j--) { - for (int k = n; k >= ones; k--) { - dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1); + for (int i = m; i >= zeros; i--) { + for (int j = n; j >= ones; j--) { + dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1); } } } @@ -2228,18 +2362,19 @@ return -1. 题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 -这是一个完全背包问题,完全背包问题和 0-1 背包问题在实现上唯一的不同是,第二层循环是从 0 开始的,而不是从尾部开始。 +这是一个完全背包问题,完全背包问题和 0-1 背包问题在实现上的区别在于,0-1 背包遍历物品的循环在外侧,而完全背包问题遍历物品的循环在内侧,在内侧体现出物品可以使用多次。 ```java public int coinChange(int[] coins, int amount) { if (coins == null || coins.length == 0) return 0; - Arrays.sort(coins); int[] dp = new int[amount + 1]; Arrays.fill(dp, amount + 1); dp[0] = 0; for (int i = 1; i <= amount; i++) { - for (int j = 0; j < coins.length && coins[j] <= i; j++) { - dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); + for (int c : coins) { // 硬币可以使用多次 + if (c <= i) { + dp[i] = Math.min(dp[i], dp[i - c] + 1); + } } } return dp[amount] > amount ? -1 : dp[amount]; @@ -2268,15 +2403,17 @@ Note that different sequences are counted as different combinations. Therefore the output is 7. ``` +完全背包。 + ```java public int combinationSum4(int[] nums, int target) { if (nums == null || nums.length == 0) return 0; int[] dp = new int[target + 1]; dp[0] = 1; for (int i = 1; i <= target; i++) { - for (int j = 0; j < nums.length; j++) { - if (nums[j] <= i) { - dp[i] += dp[i - nums[j]]; + for (int num : nums) { + if (num <= i) { + dp[i] += dp[i - num]; } } } @@ -2284,30 +2421,13 @@ public int combinationSum4(int[] nums, int target) { } ``` -**只能进行两次的股票交易** - -[Leetcode : 123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) - -```java -public int maxProfit(int[] prices) { - int firstBuy = Integer.MIN_VALUE, firstSell = 0; - int secondBuy = Integer.MIN_VALUE, secondSell = 0; - for (int curPrice : prices) { - if (firstBuy < -curPrice) firstBuy = -curPrice; - if (firstSell < firstBuy + curPrice) firstSell = firstBuy + curPrice; - if (secondBuy < firstSell - curPrice) secondBuy = firstSell - curPrice; - if (secondSell < secondBuy + curPrice) secondSell = secondBuy + curPrice; - } - return secondSell; -} -``` - **只能进行 k 次的股票交易** [Leetcode : 188. Best Time to Buy and Sell Stock IV (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/description/) ```html -dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj])) +dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } + = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj])) ``` ```java @@ -2333,6 +2453,24 @@ public int maxProfit(int k, int[] prices) { } ``` +**只能进行两次的股票交易** + +[Leetcode : 123. Best Time to Buy and Sell Stock III (Hard)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/description/) + +```java +public int maxProfit(int[] prices) { + int firstBuy = Integer.MIN_VALUE, firstSell = 0; + int secondBuy = Integer.MIN_VALUE, secondSell = 0; + for (int curPrice : prices) { + if (firstBuy < -curPrice) firstBuy = -curPrice; + if (firstSell < firstBuy + curPrice) firstSell = firstBuy + curPrice; + if (secondBuy < firstSell - curPrice) secondBuy = firstSell - curPrice; + if (secondSell < secondBuy + curPrice) secondSell = secondBuy + curPrice; + } + return secondSell; +} +``` + ### 数组区间 **数组区间和** @@ -2446,10 +2584,57 @@ public int minDistance(String word1, String word2) { } ``` -**修改一个字符串称为另一个字符串** // TODO +**修改一个字符串成为另一个字符串** [Leetcode : 72. Edit Distance (Hard)](https://leetcode.com/problems/edit-distance/description/) +```html +Example 1: + +Input: word1 = "horse", word2 = "ros" +Output: 3 +Explanation: +horse -> rorse (replace 'h' with 'r') +rorse -> rose (remove 'r') +rose -> ros (remove 'e') +Example 2: + +Input: word1 = "intention", word2 = "execution" +Output: 5 +Explanation: +intention -> inention (remove 't') +inention -> enention (replace 'i' with 'e') +enention -> exention (replace 'n' with 'x') +exention -> exection (replace 'n' with 'c') +exection -> execution (insert 'u') +``` + +```java +public int minDistance(String word1, String word2) { + if (word1 == null || word2 == null) { + return 0; + } + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + dp[i][0] = i; + } + for (int i = 1; i <= n; i++) { + dp[0][i] = i; + } + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; + } + } + } + return dp[m][n]; +} +``` + ### 分割整数 **分割整数的最大乘积** @@ -2482,12 +2667,12 @@ public int numSquares(int n) { List squareList = generateSquareList(n); int[] dp = new int[n + 1]; for (int i = 1; i <= n; i++) { - int max = Integer.MAX_VALUE; + int min = Integer.MAX_VALUE; for (int square : squareList) { if (square > i) break; - max = Math.min(max, dp[i - square] + 1); + min = Math.min(min, dp[i - square] + 1); } - dp[i] = max; + dp[i] = min; } return dp[n]; } @@ -2552,6 +2737,20 @@ public int uniquePaths(int m, int n) { } ``` +也可以直接用数学公式求解,这是一个组合问题。机器人总共移动的次数 S=m+n-2,向下移动的次数 D=m-1,那么问题可以看成从 S 从取出 D 个位置的组合数量,这个问题的解为 C(S, D)。 + +```java +public int uniquePaths(int m, int n) { + int S = m + n - 2; // 总共的移动次数 + int D = m - 1; // 向下的移动次数 + long ret = 1; + for (int i = 1; i <= D; i++) { + ret = ret * (S - D + i) / i; + } + return (int) ret; +} +``` + **矩阵的最小路径和** [Leetcode : 64. Minimum Path Sum (Medium)](https://leetcode.com/problems/minimum-path-sum/description/) @@ -2589,67 +2788,63 @@ public int minPathSum(int[][] grid) { 题目描述:交易之后需要有一天的冷却时间。 -

- -```html -s0[i] = max(s0[i - 1], s2[i - 1]); // Stay at s0, or rest from s2 -s1[i] = max(s1[i - 1], s0[i - 1] - prices[i]); // Stay at s1, or buy from s0 -s2[i] = s1[i - 1] + prices[i]; // Only one way from s1 -``` +

```java public int maxProfit(int[] prices) { if (prices == null || prices.length == 0) return 0; - int n = prices.length; - int[] s0 = new int[n]; - int[] s1 = new int[n]; - int[] s2 = new int[n]; - s0[0] = 0; - s1[0] = -prices[0]; - s2[0] = Integer.MIN_VALUE; - for (int i = 1; i < n; i++) { - s0[i] = Math.max(s0[i - 1], s2[i - 1]); - s1[i] = Math.max(s1[i - 1], s0[i - 1] - prices[i]); - s2[i] = Math.max(s2[i - 1], s1[i - 1] + prices[i]); + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = s2[i - 1] - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); } - return Math.max(s0[n - 1], s2[n - 1]); + return Math.max(sell[N - 1], s2[N - 1]); } ``` -**一组整数对能够构成的最长链** +**需要交易费用的股票交易** -[Leetcode : 646. Maximum Length of Pair Chain (Medium)](https://leetcode.com/problems/maximum-length-of-pair-chain/description/) +[Leetcode : 714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/) ```html -Input: [[1,2], [2,3], [3,4]] -Output: 2 -Explanation: The longest chain is [1,2] -> [3,4] +Input: prices = [1, 3, 2, 8, 4, 9], fee = 2 +Output: 8 +Explanation: The maximum profit can be achieved by: +Buying at prices[0] = 1 +Selling at prices[3] = 8 +Buying at prices[4] = 4 +Selling at prices[5] = 9 +The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8. ``` -对于 (a, b) 和 (c, d) ,如果 b < c,则它们可以构成一条链。 +题目描述:每交易一次,都要支付一定的费用。 + +

```java -public int findLongestChain(int[][] pairs) { - if(pairs == null || pairs.length == 0) { - return 0; +public int maxProfit(int[] prices, int fee) { + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); } - Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); - int n = pairs.length; - int[] dp = new int[n]; - Arrays.fill(dp, 1); - for(int i = 0; i < n; i++) { - for(int j = 0; j < i; j++) { - if(pairs[i][0] > pairs[j][1]){ - dp[i] = Math.max(dp[i], dp[j] + 1); - } - } - } - - int ret = 0; - for(int num : dp) { - ret = Math.max(ret, num); - } - return ret; + return Math.max(sell[N - 1], s2[N - 1]); } ``` @@ -2659,7 +2854,7 @@ public int findLongestChain(int[][] pairs) { 只进行一次交易。 -只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看这个价格是否是当前的最大价格。 +只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 ```java public int maxProfit(int[] prices) { @@ -2679,6 +2874,18 @@ public int maxProfit(int[] prices) { [Leetcode : 650. 2 Keys Keyboard (Medium)](https://leetcode.com/problems/2-keys-keyboard/description/) +题目描述:最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。 + +``` +Input: 3 +Output: 3 +Explanation: +Intitally, we have one character 'A'. +In step 1, we use Copy All operation. +In step 2, we use Paste operation to get 'AA'. +In step 3, we use Paste operation to get 'AAA'. +``` + ```java public int minSteps(int n) { int[] dp = new int[n + 1]; @@ -2750,12 +2957,11 @@ public int countPrimes(int n) { ```java int gcd(int a, int b) { - if (b == 0) return a; - return gcd(b, a % b); + return b == 0 ? a : gcd(b, a% b); } ``` -最大公倍数为两数的乘积除以最大公约数。 +最小公倍数为两数的乘积除以最大公约数。 ```java int lcm(int a, int b){ @@ -2767,47 +2973,80 @@ int lcm(int a, int b){ 对于 a 和 b 的最大公约数 f(a, b),有: -1\. 如果 a 和 b 均为偶数,f(a, b) = 2\*f(a/2, b/2); -2\. 如果 a 是偶数 b 是奇数,f(a, b) = f(a/2, b); -3\. 如果 b 是偶数 a 是奇数,f(a, b) = f(a, b/2); -4\. 如果 a 和 b 均为奇数,f(a, b) = f(a, a-b); +- 如果 a 和 b 均为偶数,f(a, b) = 2\*f(a/2, b/2); +- 如果 a 是偶数 b 是奇数,f(a, b) = f(a/2, b); +- 如果 b 是偶数 a 是奇数,f(a, b) = f(a, b/2); +- 如果 a 和 b 均为奇数,f(a, b) = f(a, a-b); 乘 2 和除 2 都可以转换为移位操作。 ### 进制转换 -Java 中 static String toString(int num, int radix) 可以将一个整数装换为 redix 进制表示的字符串。 - **7 进制** [Leetcode : 504. Base 7 (Easy)](https://leetcode.com/problems/base-7/description/) ```java public String convertToBase7(int num) { - if (num < 0) { - return '-' + convertToBase7(-num); - } - if (num < 7) { - return num + ""; - } + if (num < 0) return '-' + convertToBase7(-num); + if (num < 7) return num + ""; return convertToBase7(num / 7) + num % 7; } ``` +```java +public String convertToBase7(int num) { + if (num == 0) return "0"; + StringBuilder sb = new StringBuilder(); + boolean isNegative = num < 0; + if (isNegative) num = -num; + while (num > 0) { + sb.append(num % 7); + num /= 7; + } + String ret = sb.reverse().toString(); + return isNegative ? "-" + ret : ret; +} +``` + +Java 中 static String toString(int num, int radix) 可以将一个整数转换为 redix 进制表示的字符串。 + +```java +public String convertToBase7(int num) { + return Integer.toString(num, 7); +} +``` + **16 进制** [Leetcode : 405. Convert a Number to Hexadecimal (Easy)](https://leetcode.com/problems/convert-a-number-to-hexadecimal/description/) +负数要用它的补码形式。 + +```html +Input: +26 + +Output: +"1a" + +Input: +-1 + +Output: +"ffffffff" +``` + ```java public String toHex(int num) { char[] map = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; if(num == 0) return "0"; - String ret = ""; + StringBuilder sb = new StringBuilder(); while(num != 0){ - ret = map[(num & 0b1111)] + ret; - num >>>= 4; + sb.append(map[num & 0b1111]); + num >>>= 4; // 无符号右移,左边填 0 } - return ret; + return sb.reverse().toString(); } ``` @@ -2844,15 +3083,14 @@ Return "100". ```java public String addBinary(String a, String b) { int i = a.length() - 1, j = b.length() - 1, carry = 0; - String str = ""; - while(i >= 0 || j >= 0){ - if(i >= 0 && a.charAt(i--) == '1') carry++; - if(j >= 0 && b.charAt(j--) == '1') carry++; - str = (carry % 2) + str; + StringBuilder str = new StringBuilder(); + while (carry == 1 || i >= 0 || j >= 0) { + if (i >= 0 && a.charAt(i--) == '1') carry++; + if (j >= 0 && b.charAt(j--) == '1') carry++; + str.append(carry % 2); carry /= 2; } - if(carry == 1) str = "1" + str; - return str; + return str.reverse().toString(); } ``` @@ -2864,15 +3102,15 @@ public String addBinary(String a, String b) { ```java public String addStrings(String num1, String num2) { - StringBuilder sb = new StringBuilder(); - int carry = 0; - for(int i = num1.length() - 1, j = num2.length() - 1; i >= 0 || j >= 0 || carry == 1; i--, j--){ - int x = i < 0 ? 0 : num1.charAt(i) - '0'; - int y = j < 0 ? 0 : num2.charAt(j) - '0'; - sb.append((x + y + carry) % 10); + StringBuilder str = new StringBuilder(); + int carry = 0, i = num1.length() - 1, j = num2.length() - 1; + while (carry == 1 || i >= 0 || j >= 0) { + int x = i < 0 ? 0 : num1.charAt(i--) - '0'; + int y = j < 0 ? 0 : num2.charAt(j--) - '0'; + str.append((x + y + carry) % 10); carry = (x + y + carry) / 10; } - return sb.reverse().toString(); + return str.reverse().toString(); } ``` @@ -2905,7 +3143,7 @@ Only two moves are needed (remember each move increments or decrements one eleme **解法 1** -先排序,时间复杂度:O(NlgN) +先排序,时间复杂度:O(NlogN) ```java public int minMoves2(int[] nums) { @@ -2923,7 +3161,7 @@ public int minMoves2(int[] nums) { **解法 2** -使用快速排序找到中位数,时间复杂度 O(N) +使用快速选择找到中位数,时间复杂度 O(N) ```java public int minMoves2(int[] nums) { @@ -2970,7 +3208,7 @@ public int majorityElement(int[] nums) { } ``` -可以利用 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。 +可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(n)。可以这么理解该算法:使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0 ,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。 ```java public int majorityElement(int[] nums) { @@ -3076,11 +3314,13 @@ public int[] productExceptSelf(int[] nums) { int n = nums.length; int[] ret = new int[n]; ret[0] = 1; - for(int i = 1; i < n; i++) { - ret[i] = ret[i - 1] * nums[i - 1]; + int left = 1; + for (int i = 1; i < n; i++) { + ret[i] = left * nums[i - 1]; + left *= nums[i - 1]; } int right = 1; - for(int i = n - 1; i >= 0; i--) { + for (int i = n - 1; i >= 0; i--) { ret[i] *= right; right *= nums[i]; } @@ -3088,22 +3328,6 @@ public int[] productExceptSelf(int[] nums) { } ``` -**统计从 0 \~ n 每个数的二进制表示中 1 的个数** - -[Leetcode : 338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/) - -对于数字 6(110),它可以看成是数字 (10) 前面加上一个 1 ,因此 dp[i] = dp[i&(i-1)] + 1; - -```java -public int[] countBits(int num) { - int[] ret = new int[num + 1]; - for(int i = 1; i <= num; i++){ - ret[i] = ret[i&(i-1)] + 1; - } - return ret; -} -``` - # 数据结构相关 ## 栈和队列 @@ -3115,16 +3339,16 @@ public int[] countBits(int num) { 一个栈实现: ```java -class MyQueue { +class MyQueue { private Stack st = new Stack(); public void push(int x) { Stack temp = new Stack(); - while(!st.isEmpty()){ + while (!st.isEmpty()) { temp.push(st.pop()); } st.push(x); - while(!temp.isEmpty()){ + while (!temp.isEmpty()) { st.push(temp.pop()); } } @@ -3146,7 +3370,7 @@ class MyQueue { 两个栈实现: ```java -class MyQueue { +class MyQueue { private Stack in = new Stack(); private Stack out = new Stack(); @@ -3164,9 +3388,9 @@ class MyQueue { return out.peek(); } - private void in2out(){ - if(out.isEmpty()){ - while(!in.isEmpty()){ + private void in2out() { + if (out.isEmpty()) { + while (!in.isEmpty()) { out.push(in.pop()); } } @@ -3193,8 +3417,9 @@ class MyStack { public void push(int x) { queue.add(x); - for(int i = 1; i < queue.size(); i++){ // 翻转 - queue.add(queue.remove()); + int cnt = queue.size(); + while (cnt-- > 1) { + queue.add(queue.poll()); } } @@ -3233,20 +3458,14 @@ class MinStack { public void push(int x) { dataStack.add(x); - if(x < min) { - min = x; - } + min = Math.min(min, x); minStack.add(min); } public void pop() { dataStack.pop(); minStack.pop(); - if(!minStack.isEmpty()) { - min = minStack.peek(); - } else{ - min = Integer.MAX_VALUE; - } + min = minStack.isEmpty() ? min = Integer.MAX_VALUE : minStack.peek(); } public int top() { @@ -3274,17 +3493,15 @@ Output : true ```java public boolean isValid(String s) { Stack stack = new Stack<>(); - for(int i = 0; i < s.length(); i++){ - char c = s.charAt(i); - if(c == '(' || c == '{' || c == '[') stack.push(c); - else{ - if(stack.isEmpty()) return false; + for (char c : s.toCharArray()) { + if (c == '(' || c == '{' || c == '[') stack.push(c); + else { + if (stack.isEmpty()) return false; char cStack = stack.pop(); - if(c == ')' && cStack != '(' || - c == ']' && cStack != '[' || - c == '}' && cStack != '{' ) { - return false; - } + boolean b1 = c == ')' && cStack != '('; + boolean b2 = c == ']' && cStack != '['; + boolean b3 = c == '}' && cStack != '{'; + if (b1 || b2 || b3) return false; } } return stack.isEmpty(); @@ -3383,9 +3600,9 @@ HashMap 也可以用来对元素进行计数统计,此时键为元素,值为 [Leetcode : 1. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/) -可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(nlgn),空间复杂度为 O(1)。 +可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。 -用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i] ,如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(n),空间复杂度为 O(n),使用空间来换取时间。 +用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i] ,如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。 ```java public int[] twoSum(int[] nums, int target) { @@ -3398,6 +3615,18 @@ public int[] twoSum(int[] nums, int target) { } ``` +**判断数组是否含有相同元素** + +[Leetcode : 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; +} +``` + **最长和谐序列** [Leetcode : 594. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/) @@ -3426,6 +3655,45 @@ public int findLHS(int[] nums) { } ``` +**最长连续序列** + +[Leetcode : 128. Longest Consecutive Sequence (Medium)](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. +``` + +```java +public int longestConsecutive(int[] nums) { + Map numCnts = new HashMap<>(); + for (int num : nums) { + numCnts.put(num, 1); + } + for (int num : nums) { + count(numCnts, num); + } + int max = 0; + for (int num : nums) { + max = Math.max(max, numCnts.get(num)); + } + return max; +} + +private int count(Map numCnts, int num) { + if (!numCnts.containsKey(num)) { + return 0; + } + int cnt = numCnts.get(num); + if (cnt > 1) { + return cnt; + } + cnt = count(numCnts, num + 1) + 1; + numCnts.put(num, cnt); + return cnt; +} +``` + ## 字符串 **两个字符串包含的字符是否完全相同** @@ -3531,7 +3799,7 @@ Output: 6 Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa". ``` -解决方案是从字符串的某一位开始,尝试着去扩展子字符串。 +从字符串的某一位开始,尝试着去扩展子字符串。 ```java private int cnt = 0; @@ -3621,18 +3889,229 @@ For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums sh ```java public void moveZeroes(int[] nums) { - int n = nums.length; int idx = 0; - for(int i = 0; i < n; i++){ - if(nums[i] != 0) nums[idx++] = nums[i]; - } - while(idx < n){ - nums[idx++] = 0; - } + for (int num : nums) if (num != 0) nums[idx++] = num; + while (idx < nums.length) nums[idx++] = 0; +} +``` + +**调整矩阵** + +[Leetcode : 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[][] ret = new int[r][c]; + int index = 0; + for (int i = 0; i < r; i++) { + for (int j = 0; j < c; j++) { + ret[i][j] = nums[index / n][index % n]; + index++; + } + } + return ret; +} +``` + +**找出数组中最长的连续 1** + +[Leetcode : 485. Max Consecutive Ones (Easy)](https://leetcode.com/problems/max-consecutive-ones/description/) + +```java +public int findMaxConsecutiveOnes(int[] nums) { + int max = 0; + int cur = 0; + for (int num : nums) { + if (num == 0) cur = 0; + else { + cur++; + max = Math.max(max, cur); + } + } + return max; +} +``` + +**数组相邻差值的个数** + +[Leetcode : 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; +} +``` + +**数组的度** + +[Leetcode : 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; +} +``` + +**对角元素相等的矩阵** + +[Leetcode : 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); +} +``` + +**嵌套数组** + +[Leetcode : 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; +} +``` + +**分隔数组** + +[Leetcode : 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; } ``` -### 1-n 分布 **一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出丢失的数和重复的数** @@ -3643,9 +4122,14 @@ 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 位上的元素,使得 num[i] - 1 的元素为 nums[i] ,也就是该位的元素是正确的。交换操作需要循环进行,因为一次交换没办法使得第 i 位上的元素是正确的。但是要交换的两个元素可能就是重复元素,那么循环就可能永远进行下去,终止循环的方法是加上 nums[i] != nums[nums[i] - 1 条件。 @@ -3656,32 +4140,29 @@ Output: [2,3] ```java public int[] findErrorNums(int[] nums) { - for(int i = 0; i < nums.length; i++){ - while(nums[i] != i + 1 && nums[i] != nums[nums[i] - 1]) { + for (int i = 0; i < nums.length; i++) { + while (nums[i] != i + 1) { + if (nums[i] == nums[nums[i] - 1]) { + return new int[]{nums[nums[i] - 1], i + 1}; + } swap(nums, i, nums[i] - 1); } } - for(int i = 0; i < nums.length; i++){ - if(i + 1 != nums[i]) { - 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; +private void swap(int[] nums, int i, int j) { + int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; } ``` -**找出数组中重复的数,数组值在 [0, n-1] 之间** +**找出数组中重复的数,数组值在 [1, n] 之间** [Leetcode : 287. Find the Duplicate Number (Medium)](https://leetcode.com/problems/find-the-duplicate-number/description/) +要求不能修改数组,也不能使用额外的空间。 + 二分查找解法: ```java @@ -3704,24 +4185,23 @@ public int findDuplicate(int[] nums) { ```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; + 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; } ``` -### 有序矩阵 +**有序矩阵查找** -有序矩阵指的是行和列分别有序的矩阵。一般可以利用有序性使用二分查找方法。 +[Leetocde : 240. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) ```html [ @@ -3731,10 +4211,6 @@ public int findDuplicate(int[] nums) { ] ``` -**有序矩阵查找** - -[Leetocde : 240. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) - ```java public boolean searchMatrix(int[][] matrix, int target) { if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false; @@ -3817,7 +4293,7 @@ class Tuple implements Comparable { ## 链表 -**判断两个链表的交点** +**找出两个链表的交点** [Leetcode : 160. Intersection of Two Linked Lists (Easy)](https://leetcode.com/problems/intersection-of-two-linked-lists/description/) @@ -3894,6 +4370,11 @@ public ListNode mergeTwoLists(ListNode l1, ListNode l2) { [Leetcode : 83. Remove Duplicates from Sorted List (Easy)](https://leetcode.com/problems/remove-duplicates-from-sorted-list/description/) +```html +Given 1->1->2, return 1->2. +Given 1->1->2->3->3, return 1->2->3. +``` + ```java public ListNode deleteDuplicates(ListNode head) { if(head == null || head.next == null) return head; @@ -3902,22 +4383,203 @@ public ListNode deleteDuplicates(ListNode head) { } ``` +**删除链表的倒数第 n 个节点** + +[Leetcode : 19. Remove Nth Node From End of List (Medium)](https://leetcode.com/problems/remove-nth-node-from-end-of-list/description/) + +```html +Given linked list: 1->2->3->4->5, and n = 2. +After removing the second node from the end, the linked list becomes 1->2->3->5. +``` + +```java +public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode newHead = new ListNode(-1); + newHead.next = head; + ListNode fast = newHead; + while (n-- > 0) { + fast = fast.next; + } + ListNode slow = newHead; + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + slow.next = slow.next.next; + return newHead.next; +} +``` + +**交换链表中的相邻结点** + +[Leetcode : 24. Swap Nodes in Pairs (Medium)](https://leetcode.com/problems/swap-nodes-in-pairs/description/) + +```html +Given 1->2->3->4, you should return the list as 2->1->4->3. +``` + +题目要求:不能修改结点的 val 值;O(1) 空间复杂度。 + +```java +public ListNode swapPairs(ListNode head) { + ListNode newHead = new ListNode(-1); + newHead.next = head; + ListNode cur = head, pre = newHead; + while (cur != null && cur.next != null) { + ListNode next = cur.next; + pre.next = next; + cur.next = next.next; + next.next = cur; + pre = cur; + cur = cur.next; + } + return newHead.next; +} +``` + +**根据有序链表构造平衡的 BST** + +[Leetcode : 109. Convert Sorted List to Binary Search Tree (Medium)](https://leetcode.com/problems/convert-sorted-list-to-binary-search-tree/description/) + +```html +Given the sorted linked list: [-10,-3,0,5,9], + +One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST: + + 0 + / \ + -3 9 + / / + -10 5 +``` + +```java +public TreeNode sortedListToBST(ListNode head) { + if (head == null) return null; + int size = size(head); + if (size == 1) return new TreeNode(head.val); + ListNode pre = head, mid = pre.next; + int step = 2; + while (step <= size / 2) { + pre = mid; + mid = mid.next; + step++; + } + pre.next = null; + TreeNode t = new TreeNode(mid.val); + t.left = sortedListToBST(head); + t.right = sortedListToBST(mid.next); + return t; +} + +private int size(ListNode node) { + int size = 0; + while (node != null) { + size++; + node = node.next; + } + return size; +} +``` + + +**链表求和** + +[Leetcode : 445. Add Two Numbers II (Medium)](https://leetcode.com/problems/add-two-numbers-ii/description/) + +```html +Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) +Output: 7 -> 8 -> 0 -> 7 +``` + +题目要求:不能修改原始链表。 + +```java +public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + Stack l1Stack = buildStack(l1); + Stack l2Stack = buildStack(l2); + ListNode head = new ListNode(-1); + int carry = 0; + while (!l1Stack.isEmpty() || !l2Stack.isEmpty() || carry != 0) { + int x = l1Stack.isEmpty() ? 0 : l1Stack.pop(); + int y = l2Stack.isEmpty() ? 0 : l2Stack.pop(); + int sum = x + y + carry; + ListNode node = new ListNode(sum % 10); + node.next = head.next; + head.next = node; + carry = sum / 10; + } + return head.next; +} + +private Stack buildStack(ListNode l) { + Stack stack = new Stack<>(); + while (l != null) { + stack.push(l.val); + l = l.next; + } + return stack; +} +``` + +**分隔链表** + +[Leetcode : 725. Split Linked List in Parts(Medium)](https://leetcode.com/problems/split-linked-list-in-parts/description/) + +```html +Input: +root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3 +Output: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]] +Explanation: +The input has been split into consecutive parts with size difference at most 1, and earlier parts are a larger size than the later parts. +``` + +题目描述:把链表分隔成 k 部分,每部分的长度都应该尽可能相同,排在前面的长度应该大于等于后面的。 + +```java +public ListNode[] splitListToParts(ListNode root, int k) { + int N = 0; + ListNode cur = root; + while (cur != null) { + N++; + cur = cur.next; + } + int mod = N % k; + int size = N / k; + ListNode[] ret = new ListNode[k]; + cur = root; + for (int i = 0; cur != null && i < k; i++) { + ret[i] = cur; + int curSize = size + (mod-- > 0 ? 1 : 0); + for (int j = 0; j < curSize - 1; j++) { + cur = cur.next; + } + ListNode next = cur.next; + cur.next = null; + cur = next; + } + return ret; +} +``` + **回文链表** [Leetcode : 234. Palindrome Linked List (Easy)](https://leetcode.com/problems/palindrome-linked-list/description/) +要求以 O(1) 的空间复杂度来求解。 + 切成两半,把后半段反转,然后比较两半是否相等。 ```java public boolean isPalindrome(ListNode head) { - if(head == null || head.next == null) return true; + if (head == null || head.next == null) return true; ListNode slow = head, fast = head.next; - while(fast != null && fast.next != null){ + while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } - if(fast != null){ // 偶数节点,让 slow 指向下一个节点 + if (fast != null) { // 偶数节点,让 slow 指向下一个节点 slow = slow.next; } @@ -3927,14 +4589,14 @@ public boolean isPalindrome(ListNode head) { return isEqual(l1, l2); } -private void cut(ListNode head, ListNode cutNode){ - while( head.next != cutNode ) head = head.next; +private void cut(ListNode head, ListNode cutNode) { + while (head.next != cutNode) head = head.next; head.next = null; } -private ListNode reverse(ListNode head){ +private ListNode reverse(ListNode head) { ListNode newHead = null; - while(head != null){ + while (head != null) { ListNode nextNode = head.next; head.next = newHead; newHead = head; @@ -3943,9 +4605,9 @@ private ListNode reverse(ListNode head){ return newHead; } -private boolean isEqual(ListNode l1, ListNode l2){ - while(l1 != null && l2 != null){ - if(l1.val != l2.val) return false; +private boolean isEqual(ListNode l1, ListNode l2) { + while (l1 != null && l2 != null) { + if (l1.val != l2.val) return false; l1 = l1.next; l2 = l2.next; } @@ -4092,7 +4754,7 @@ Return 3. The paths that sum to 8 are: 3. -3 -> 11 ``` -路径不一定以 root 开头并以 leaf 结尾,但是必须连续 +路径不一定以 root 开头,也不一定以 leaf 结尾,但是必须连续。 ```java public int pathSum(TreeNode root, int sum) { @@ -4199,19 +4861,13 @@ There are two left leaves in the binary tree, with values 9 and 15 respectively. ```java public int sumOfLeftLeaves(TreeNode root) { - if(root == null) { - return 0; - } - if(isLeaf(root.left)) { - return root.left.val + sumOfLeftLeaves(root.right); - } + if(root == null) return 0; + if(isLeaf(root.left)) return root.left.val + sumOfLeftLeaves(root.right); return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right); } private boolean isLeaf(TreeNode node){ - if(node == null) { - return false; - } + if(node == null) return false; return node.left == null && node.right == null; } ``` @@ -4268,7 +4924,7 @@ Given tree s: / \ 1 2 Given tree t: - 4 + 4 / \ 1 2 Return true, because t has the same structure and node values with a subtree of s. @@ -4543,11 +5199,11 @@ public int findBottomLeftValue(TreeNode root) { ### 前中后序遍历 ```html - 1 - / \ - 2 3 - / \ \ -4 5 6 + 1 + / \ + 2 3 + / \ \ +4 5 6 ``` 层次遍历顺序:[1 2 3 4 5 6] @@ -4596,14 +5252,14 @@ void dfs(TreeNode root){ ```java public List preorderTraversal(TreeNode root) { List ret = new ArrayList<>(); - if (root == null) return ret; Stack stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); + if (node == null) continue; ret.add(node.val); - if (node.right != null) stack.push(node.right); - if (node.left != null) stack.push(node.left); // 先添加右子树再添加左子树,这样是为了让左子树在栈顶 + stack.push(node.right); // 先右后左,保证左子树先遍历 + stack.push(node.left); } return ret; } @@ -4618,14 +5274,14 @@ public List preorderTraversal(TreeNode root) { ```java public List postorderTraversal(TreeNode root) { List ret = new ArrayList<>(); - if (root == null) return ret; Stack stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); + if (node == null) continue; ret.add(node.val); - if (node.left != null) stack.push(node.left); - if (node.right != null) stack.push(node.right); + stack.push(node.left); + stack.push(node.right); } Collections.reverse(ret); return ret; @@ -4639,11 +5295,12 @@ public List postorderTraversal(TreeNode root) { ```java public List inorderTraversal(TreeNode root) { List ret = new ArrayList<>(); + if (root == null) return ret; Stack stack = new Stack<>(); TreeNode cur = root; - while(cur != null || !stack.isEmpty()) { - while(cur != null) { // 模拟递归栈的不断深入 - stack.add(cur); + while (cur != null || !stack.isEmpty()) { + while (cur != null) { + stack.push(cur); cur = cur.left; } TreeNode node = stack.pop(); @@ -4663,7 +5320,7 @@ public List inorderTraversal(TreeNode root) { [653. Two Sum IV - Input is a BST (Easy)](https://leetcode.com/problems/two-sum-iv-input-is-a-bst/description/) ```html -Input: +Input: 5 / \ 3 6 @@ -4764,17 +5421,11 @@ public TreeNode convertBST(TreeNode root) { } private void traver(TreeNode root) { - if (root == null) { - return; - } - if (root.right != null) { - traver(root.right); - } + if (root == null) return; + if (root.right != null) traver(root.right); sum += root.val; root.val = sum; - if (root.left != null) { - traver(root.left); - } + if (root.left != null) traver(root.left); } ``` @@ -4799,31 +5450,31 @@ private List list; public int[] findMode(TreeNode root) { list = new ArrayList<>(); - inorder(root); + inOrder(root); int[] ret = new int[list.size()]; int idx = 0; - for(int num : list){ + for (int num : list) { ret[idx++] = num; } return ret; } -private void inorder(TreeNode node){ - if(node == null) return; - inorder(node.left); - if(preNode != null){ - if(preNode.val == node.val) cnt++; +private void inOrder(TreeNode node) { + if (node == null) return; + inOrder(node.left); + if (preNode != null) { + if (preNode.val == node.val) cnt++; else cnt = 1; } - if(cnt > maxCnt){ + if (cnt > maxCnt) { maxCnt = cnt; list.clear(); list.add(node.val); - } else if(cnt == maxCnt){ + } else if (cnt == maxCnt) { list.add(node.val); } preNode = node; - inorder(node.right); + inOrder(node.right); } ``` @@ -4854,19 +5505,19 @@ private int cnt = 0; private int val; public int kthSmallest(TreeNode root, int k) { - inorder(root, k); + inOrder(root, k); return val; } -private void inorder(TreeNode node, int k) { - if(node == null) return; - inorder(node.left, k); +private void inOrder(TreeNode node, int k) { + if (node == null) return; + inOrder(node.left, k); cnt++; - if(cnt == k) { + if (cnt == k) { val = node.val; return; } - inorder(node.right, k); + inOrder(node.right, k); } ``` @@ -4883,33 +5534,24 @@ Trie,又称前缀树或字典树,用于判断字符串是否存在或者是 ```java class Trie { - private class Node{ - Node[] childs = new Node[26]; - boolean isLeaf; - } - private Node root = new Node(); - /** Initialize your data structure here. */ public Trie() { } - /** Inserts a word into the trie. */ public void insert(String word) { - int idx = word.charAt(0) - 'a'; insert(word, root); } private void insert(String word, Node node){ int idx = word.charAt(0) - 'a'; - if(node.childs[idx] == null){ - node.childs[idx] = new Node(); + if(node.child[idx] == null){ + node.child[idx] = new Node(); } - if(word.length() == 1) node.childs[idx].isLeaf = true; - else insert(word.substring(1), node.childs[idx]); + if(word.length() == 1) node.child[idx].isLeaf = true; + else insert(word.substring(1), node.child[idx]); } - /** Returns if the word is in the trie. */ public boolean search(String word) { return search(word, root); } @@ -4917,12 +5559,11 @@ class Trie { private boolean search(String word, Node node){ if(node == null) return false; int idx = word.charAt(0) - 'a'; - if(node.childs[idx] == null) return false; - if(word.length() == 1) return node.childs[idx].isLeaf; - return search(word.substring(1), node.childs[idx]); + if(node.child[idx] == null) return false; + if(word.length() == 1) return node.child[idx].isLeaf; + return search(word.substring(1), node.child[idx]); } - /** Returns if there is any word in the trie that starts with the given prefix. */ public boolean startsWith(String prefix) { return startWith(prefix, root); } @@ -4931,7 +5572,12 @@ class Trie { if(node == null) return false; if(prefix.length() == 0) return true; int idx = prefix.charAt(0) - 'a'; - return startWith(prefix.substring(1), node.childs[idx]); + return startWith(prefix.substring(1), node.child[idx]); + } + + private class Node{ + Node[] child = new Node[26]; + boolean isLeaf; } } ``` @@ -4949,51 +5595,50 @@ Input: sum("ap"), Output: 5 ```java class MapSum { - private class Trie { - int val; - Map childs; - boolean isWord; - Trie() { - childs = new HashMap<>(); - } + private class Node { + Node[] child = new Node[26]; + int value; } - private Trie root; + private Node root = new Node(); public MapSum() { - root = new Trie(); + } public void insert(String key, int val) { - Trie cur = root; - for(char c : key.toCharArray()) { - if(!cur.childs.containsKey(c)) { - Trie next = new Trie(); - cur.childs.put(c, next); - } - cur = cur.childs.get(c); + insert(key, root, val); + } + + private void insert(String key, Node node, int val) { + int idx = key.charAt(0) - 'a'; + if (node.child[idx] == null) { + node.child[idx] = new Node(); + } + if (key.length() == 1) { + node.child[idx].value = val; + } else { + insert(key.substring(1), node.child[idx], val); } - cur.val = val; - cur.isWord = true; } public int sum(String prefix) { - Trie cur = root; - for(char c : prefix.toCharArray()) { - if(!cur.childs.containsKey(c)) return 0; - cur = cur.childs.get(c); - } - return dfs(cur); + return sum(prefix, root); } - private int dfs(Trie cur) { - int sum = 0; - if(cur.isWord) { - sum += cur.val; + private int sum(String prefix, Node node) { + if (node == null) { + return 0; } - for(Trie next : cur.childs.values()) { - sum += dfs(next); + int sum = node.value; + if (prefix.length() == 0) { + for (Node next : node.child) { + sum += sum(prefix, next); + } + } else { + int idx = prefix.charAt(0) - 'a'; + sum = sum(prefix.substring(1), node.child[idx]); } return sum; } @@ -5002,11 +5647,71 @@ class MapSum { ## 图 +**冗余连接** + +[Leetcode : 684. Redundant Connection (Medium)](https://leetcode.com/problems/redundant-connection/description/) + +```html +Input: [[1,2], [1,3], [2,3]] +Output: [2,3] +Explanation: The given undirected graph will be like this: + 1 + / \ +2 - 3 +``` + +题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。 + +使用 Union-Find。 + +```java +public int[] findRedundantConnection(int[][] edges) { + int N = edges.length; + UF uf = new UF(N); + for (int[] e : edges) { + int u = e[0], v = e[1]; + if (uf.find(u) == uf.find(v)) { + return e; + } + uf.union(u, v); + } + return new int[]{-1, -1}; +} + +private class UF { + int[] id; + + UF(int N) { + id = new int[N + 1]; + for (int i = 0; i < id.length; i++) { + id[i] = i; + } + } + + void union(int u, int v) { + int uID = find(u); + int vID = find(v); + if (uID == vID) { + return; + } + for (int i = 0; i < id.length; i++) { + if (id[i] == uID) { + id[i] = vID; + } + } + } + + int find(int p) { + return id[p]; + } +} +``` + ## 位运算 **1. 基本原理** -0s 表示一串 0 ,1s 表示一串 1。 +0s 表示一串 0,1s 表示一串 1。 ``` x ^ 0s = x x & 0s = 0 x | 0s = x @@ -5014,9 +5719,9 @@ x ^ 1s = ~x x & 1s = x x | 1s = 1s x ^ x = 0 x & x = x x | x = x ``` -① 利用 x ^ 1s = \~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数; -② 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask :00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位; -③ 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1 。 +- 利用 x ^ 1s = \~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数; +- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask :00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位; +- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1 。 \>\> n 为算术右移,相当于除以 2n; \>\>\> n 为无符号右移,左边会补上 0。 @@ -5032,7 +5737,7 @@ n&(-n) 该运算得到 n 的位级表示中最低的那一位。-n 得到 n 的 要获取 111111111,将 0 取反即可,\~0。 -要得到只有第 i 位为 1 的 mask,将 1 向左移动 i 位即可,1<<i 。例如 1<<5 得到只有第 5 位为 1 的 mask :00010000。 +要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。 要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。 @@ -5100,7 +5805,7 @@ static String toBinaryString(int i); // 转换为二进制表示的字符串 [Leetcode : 461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/) -对两个数进行异或操作,不同的那一位结果为 1 ,统计有多少个 1 即可。 +对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。 ```java public int hammingDistance(int x, int y) { @@ -5122,6 +5827,28 @@ public int hammingDistance(int x, int y) { } ``` +**找出数组中缺失的那个数** + +[Leetcode : 268. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/) + +```html +Input: [3,0,1] +Output: 2 +``` + +题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。 + ` + +```java +public int missingNumber(int[] nums) { + int ret = 0; + for (int i = 0; i <= nums.length; i++) { + ret = i == nums.length ? ret ^ i : ret ^ i ^ nums[i]; + } + return ret; +} +``` + **翻转一个数的比特位** [Leetcode : 190. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/) @@ -5129,7 +5856,7 @@ public int hammingDistance(int x, int y) { ```java public int reverseBits(int n) { int ret = 0; - for(int i = 0; i < 32; i++){ + for (int i = 0; i < 32; i++) { ret <<= 1; ret |= (n & 1); n >>>= 1; @@ -5138,6 +5865,37 @@ public int reverseBits(int n) { } ``` +如果该函数需要被调用很多次,可以将 int 拆成 4 个 byte,然后缓存 byte 对应的比特位翻转,最后再拼接起来。 + +```java +private static Map cache = new HashMap<>(); + +public int reverseBits(int n) { + int ret = 0; + for (int i = 0; i < 4; i++) { + byte b = (byte) (n & 0b11111111); + ret <<= 8; + ret |= reverseByte(b); + n >>= 8; + } + return ret; + +} + +private int reverseByte(byte b) { + if (cache.containsKey(b)) return cache.get(b); + int ret = 0; + byte t = b; + for (int i = 0; i < 8; i++) { + ret <<= 1; + ret |= t & 1; + t >>= 1; + } + cache.put(b, ret); + return ret; +} +``` + **不用额外变量交换两个整数** [程序员代码面试指南 :P317](#) @@ -5150,6 +5908,26 @@ a = a ^ b; 令 c = a ^ b,那么 b ^ c = b ^ b ^ a = a,a ^ c = a ^ a ^ b = b。 +**判断一个数是不是 2 的 n 次方** + +[Leetcode : 231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/) + +二进制表示只有一个 1 存在。 + +```java +public boolean isPowerOfTwo(int n) { + return n > 0 && Integer.bitCount(n) == 1; +} +``` + +利用 1000 & 0111 == 0 这种性质,得到以下解法: + +```java +public boolean isPowerOfTwo(int n) { + return n > 0 && (n & (n - 1)) == 0; +} +``` + **判断一个数是不是 4 的 n 次方** [Leetcode : 342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/) @@ -5167,23 +5945,15 @@ public boolean isPowerOfFour(int num) { } ``` -**判断一个数是不是 2 的 n 次方** - -[Leetcode : 231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/) - -同样可以用 Power of Four 的方法,但是 2 的 n 次方更特殊,它的二进制表示只有一个 1 存在。 - ```java -public boolean isPowerOfTwo(int n) { - return n > 0 && Integer.bitCount(n) == 1; +public boolean isPowerOfFour(int num) { + return Integer.toString(num, 4).matches("10*"); } ``` -利用 1000 & 0111 == 0 这种性质,得到以下解法: - ```java -public boolean isPowerOfTwo(int n) { - return n > 0 && (n & (n - 1)) == 0; +public boolean isPowerOfFour(int num) { + return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0; } ``` @@ -5245,7 +6015,13 @@ public boolean hasAlternatingBits(int n) { [Leetcode : 476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/) -不考虑二进制表示中的首 0 部分 +```html +Input: 5 +Output: 2 +Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010. So you need to output 2. +``` + +不考虑二进制表示中的首 0 部分。 对于 00000101,要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。 @@ -5306,6 +6082,12 @@ public int getSum(int a, int b) { [Leetcode : 318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) +```html +Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"] +Return 16 +The two words can be "abcw", "xtfn". +``` + 题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。 解题思路:本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。 @@ -5332,6 +6114,22 @@ public int maxProduct(String[] words) { } ``` +**统计从 0 \~ n 每个数的二进制表示中 1 的个数** + +[Leetcode : 338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/) + +对于数字 6(110),它可以看成是数字 (10) 前面加上一个 1 ,因此 dp[i] = dp[i&(i-1)] + 1; + +```java +public int[] countBits(int num) { + int[] ret = new int[num + 1]; + for(int i = 1; i <= num; i++){ + ret[i] = ret[i&(i-1)] + 1; + } + return ret; +} +``` + # 参考资料 - [Leetcode](https://leetcode.com/problemset/algorithms/?status=Todo) diff --git a/notes/Linux.md b/notes/Linux.md index 8ab9f56d..135467a7 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -50,8 +50,11 @@ * [九、进程管理](#九进程管理) * [查看进程](#查看进程) * [进程状态](#进程状态) - * [SIGCHILD](#sigchild) - * [孤儿进程和僵死进程](#孤儿进程和僵死进程) + * [SIGCHLD](#sigchld) + * [wait()](#wait) + * [waitpid()](#waitpid) + * [孤儿进程](#孤儿进程) + * [僵死进程](#僵死进程) * [十、I/O 复用](#十io-复用) * [概念理解](#概念理解) * [I/O 模型](#io-模型) @@ -154,16 +157,16 @@ Linux 发行版是 Linux 内核及各种应用软件的集成版本。 ## VIM 三个模式 -

+- 一般指令模式(Command mode):进入 VIM 的默认模式,可以用于移动游标查看内容; +- 编辑模式(Insert mode):按下 "i" 等按键之后进入,可以对文本进行编辑; +- 指令列模式(Bottom-line mode):按下 ":" 按键之后进入,用于保存退出等操作。 -- 一般指令模式:进入 VIM 的默认模式,可以用于移动游标查看内容; -- 编辑模式:按下 "i" 等按键之后进入,可以对文本进行编辑; -- 指令列模式:按下 ":" 按键之后进入,用于保存退出等操作。 +

在指令列模式下,有以下命令用于离开或者保存文件。 | 命令 | 作用 | -| -- | -- | +| :--: | -- | | :w | 写入磁盘| | :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | | :q | 离开 | @@ -202,7 +205,7 @@ GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息 GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。 -

+

## 开机检测程序 @@ -321,7 +324,7 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 - /usr (unix software resource):所有系统默认软件都会安装到这个目录; - /var (variable):存放系统或程序运行过程中的数据文件。 -

+

## 文件时间 @@ -466,10 +469,11 @@ locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内 find 可以使用文件的属性和权限进行搜索。 ```html -# find filename [option] +# find [basedir] [option] +example: find . -name "shadow*" ``` -**(一)与时间有关的选项** +(一)与时间有关的选项 ```html -mtime n :列出在 n 天前的那一天修改过内容的文件 @@ -480,9 +484,9 @@ find 可以使用文件的属性和权限进行搜索。 +4、4 和 -4 的指示的时间范围如下: -

+

-**(二)与文件拥有者和所属群组有关的选项** +(二)与文件拥有者和所属群组有关的选项 ```html -uid n @@ -493,7 +497,7 @@ find 可以使用文件的属性和权限进行搜索。 -nogroup:搜索所属群组不存在于 /etc/group 的文件 ``` -**(三)与文件权限和名称有关的选项** +(三)与文件权限和名称有关的选项 ```html -name filename @@ -516,21 +520,19 @@ find 可以使用文件的属性和权限进行搜索。 2. inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 号码; 3. block:记录文件的内容,文件太大时,会占用多个 block。 +

+ 当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 磁盘碎片是指一个文件内容所在的 block 过于分散。 -Ext2 文件系统使用了上述的文件结构,并在此之上加入了 block 群组的概念,也就是将一个文件系统划分为多个 block 群组,方便管理。 - -

- ## inode Ext2 文件系统支持的 block 大小有 1k、2k 和 4k 三种,不同的 block 大小限制了单一文件的大小。而每个 inode 大小是固定为 128 bytes。 inode 中记录了文件内容所在的 block,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块当成 inode 用来记录引用信息。 -

+

inode 具体包含以下信息: @@ -561,7 +563,7 @@ inode 具体包含以下信息: 删除任意一个条目,文件还是存在,只要引用数量不为 0。 -有以下限制:不能跨越 File System;不能对目录进行链接。 +有以下限制:不能跨越 File System、不能对目录进行链接。 ```html # ln /etc/crontab . @@ -636,7 +638,7 @@ $ bzip2 [-cdkzv#] filename 提供比 bzip2 更佳的压缩比。 -可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意,压缩比越高,压缩的时间也越长。 +可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。 查看命令:xzcat、xzmore、xzless、xzgrep。 @@ -932,7 +934,7 @@ $ grep -n 'the' regular_express.txt 18:google is the best tools for search keyword ``` -因为 { 与 } 的符号在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。 +因为 { 和 } 在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。 ```html $ grep -n 'go\{2,5\}g' regular_express.txt @@ -1026,7 +1028,7 @@ daemon 2 示例三:查看特定的进程 -```html +``` # ps aux | grep threadx ``` @@ -1060,36 +1062,66 @@ daemon 2 ## 进程状态 +

+ | 状态 | 说明 | | :---: | --- | | R | running or runnable (on run queue) | -| D | uninterruptible sleep (usually IO) | +| D | uninterruptible sleep (usually IO) | | S | interruptible sleep (waiting for an event to complete) | | Z | defunct/zombie, terminated but not reaped by its parent | | T | stopped, either by a job control signal or because it is being traced| -

- -## SIGCHILD +## SIGCHLD 当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中: - 得到 SIGCHLD 信号; -- 阻塞的 waitpid(2)(或者 wait)调用会返回。 +- waitpid() 或者 wait() 调用会返回。 -

+

-## 孤儿进程和僵死进程 +其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。 -### 1. 孤儿进程 +在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 + +## wait() + +```c +pid_t wait(int *status) +``` + +父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。 + +如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 - 1,同时 errno 被置为 ECHILD。 + +参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为 NULL: + +```c +pid = wait(NULL); +``` + +## waitpid() + +```c +pid_t waitpid(pid_t pid,int *status,int options) +``` + +作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。 + +pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么贺 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 + +options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。 + +## 孤儿进程 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。 由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。 -### 2. 僵死进程 +## 僵死进程 -一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait 或 waitpid 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait 或 waitpid,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 +一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。 僵死进程通过 ps 命令显示出来的状态为 Z。 @@ -1118,7 +1150,8 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有 同步异步是获知 I/O 完成的方式,同步需要时刻关心 I/O 是否已经完成,异步无需主动关心,在 I/O 完成时它会收到通知。 -

+

+ ### 1. 同步-阻塞 @@ -1126,31 +1159,25 @@ I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有 应该注意到,在阻塞的过程中,其他程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。 -

+

### 2. 同步-非阻塞 -非阻塞意味着用户程序在执行系统调用后还可以执行,内核并不是马上执行完 I/O,而是以一个错误码来告知用户程序 I/O 还未完成。为了获得 I/O 完成事件,用户程序必须调用多次系统调用去询问内核,甚至是忙等,也就是在一个循环里面一直询问并等待。 +非阻塞意味着用户程序在执行系统调用后还可以继续执行,内核并不是马上执行完 I/O,而是以一个错误码来告知用户程序 I/O 还未完成。为了获得 I/O 完成事件,用户程序必须调用多次系统调用去询问内核,甚至是忙等,也就是在一个循环里面一直询问并等待。 由于 CPU 要处理更多的用户程序的询问,因此这种模型的效率是比较低的。 -

+

-### 3. 异步-阻塞 +### 3. 异步 -这是 I/O 复用使用的一种模式,通过使用 select,它可以监听多个 I/O 事件,当这些事件至少有一个发生时,用户程序会收到通知。 +该模式下,I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后继续之前的操作。 -

- -### 4. 异步-非阻塞 - -该模式下,I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后完成 I/O 事务。 - -

+

## select poll epoll -这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。可以说,新出现的实现是为了修复旧实现的不足。 +这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。 ### 1. select @@ -1321,7 +1348,7 @@ select 和 poll 方式中,进程只有在调用一定的方法后,内核才 新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size,不保证服务质量。 -epoll_ct() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。 +epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。 epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到程序中,不需要像 select/poll 对注册的所有描述符遍历一遍。 @@ -1396,3 +1423,5 @@ poll 没有最大描述符数量的限制,如果平台支持应该采用 poll - [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html) - [Linux 之守护进程、僵死进程与孤儿进程](http://liubigbin.github.io/2016/03/11/Linux-%E4%B9%8B%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E3%80%81%E5%83%B5%E6%AD%BB%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%AD%A4%E5%84%BF%E8%BF%9B%E7%A8%8B/) - [Linux process states](https://idea.popcount.org/2012-12-11-linux-process-states/) +- [GUID Partition Table](https://en.wikipedia.org/wiki/GUID_Partition_Table) +- [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719) diff --git a/notes/MySQL.md b/notes/MySQL.md index 3588d9f9..56335138 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -30,31 +30,27 @@ InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。 -采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读。 +采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。 表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等。 -通过一些机制和工具支持真正的热备份。 +通过一些机制和工具支持真正的热备份,其它存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 ## MyISAM -MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等。但 MyISAM 不支持事务和行级锁,而且崩溃后无法安全恢复。 +提供了大量的特性,包括全文索引、压缩表、空间数据索引等。应该注意的是,MySQL 5.6.4 添加了对 InnoDB 引擎的全文索引支持。 -只能对整张表加锁,而不是针对行。 +不支持事务。 + +不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取查询的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 -可以包含动态或者静态的行。 - 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 -如果表在创建并导入数据以后,不会再进行修改操作,那么这样的表适合采用 MyISAM 压缩表。 - -对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。 - -MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。 +MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM。 ## 比较 @@ -62,7 +58,7 @@ MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性 2. 备份:InnoDB 支持在线热备份。 3. 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 4. 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 -5. 其它特性:MyISAM 支持全文索引,地理空间索引。 +5. 其它特性:MyISAM 支持压缩表和空间数据索引。 # 二、数据类型 @@ -146,12 +142,14 @@ InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个 限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。 -### 3. 空间索引(R-Tree) +### 3. 空间数据索引(R-Tree) MyISAM 存储引擎支持空间索引,可以用于地理数据存储。 空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 +必须使用 GIS 相关的函数来维护数据。 + ### 4. 全文索引 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。 @@ -162,9 +160,9 @@ MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而 - 大大减少了服务器需要扫描的数据量; -- 帮助服务器避免进行排序和创建临时表; +- 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作); -- 将随机 I/O 变为顺序 I/O。 +- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相关的列值都存储在一起)。 ## 索引优化 @@ -216,14 +214,14 @@ customer_id_selectivity: 0.0373 聚簇索引并不是一种索引类型,而是一种数据存储方式。 -术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引的数据行存放在 B-Tree 的叶子页中。 +术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行。 因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 **优点** -1. 可以把相关数据保存在一起,减少 I/O 操作; -2. 因为数据保存在 B-Tree 中,因此数据访问更快。 +1. 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。 +2. 数据访问更快。 **缺点** @@ -249,15 +247,15 @@ customer_id_selectivity: 0.0373

-为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data],key 为记录的键,data 为数据记录除 key 外的数据。 +为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data]。 B-Tree 是满足下列条件的数据结构: - 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的; - 一个节点中的 key 从左到右非递减排列; -- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于 keyi 且小于 keyi+1。 +- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 -在 B-Tree 中按 key 检索数据的算法非常直观:首先从根节点进行二分查找,如果找到则返回对应节点的 data,否则对相应区间的指针指向的节点递归进行查找,直到找到节点或找到 null 指针,前者查找成功,后者查找失败。 +在 B-Tree 中按 key 检索数据的算法非常直观:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。 由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。 @@ -282,7 +280,9 @@ B-Tree 是满足下列条件的数据结构: 页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为 4k),主存和磁盘以页为单位交换数据。 -一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。 +一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。 + +B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。 B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。 diff --git a/notes/Redis.md b/notes/Redis.md index 20de47e0..d33d6f04 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -212,7 +212,14 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除 # 四、发布与订阅 -发布与订阅实际上是观察者模式,订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 +订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 + +发布与订阅模式和观察者模式有以下不同: + +- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 +- 观察者模式是同步的,当事件触发时,主题会去调度观察者的方法;而发布与订阅模式是异步的; + +

发布与订阅有一些问题,很少使用它,而是使用替代的解决方案。问题如下: @@ -235,13 +242,17 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。 -如果系统发生故障,将会丢失最后一次创建快照之后的数据。并且如果数据量很大,保存快照的时间也会很长。 +如果系统发生故障,将会丢失最后一次创建快照之后的数据。 + +如果数据量很大,保存快照的时间会很长。 ## 2. AOF 持久化 -AOF 持久化将写命令添加到 AOF 文件(Append Only File)的末尾。 +将写命令添加到 AOF 文件(Append Only File)的末尾。 -对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。因此将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项: +对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。 + +将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项: | 选项 | 同步频率 | | :--: | :--: | @@ -361,7 +372,7 @@ def main(): 从事件处理的角度来看,服务器运行流程如下: -

+

# 十一、Redis 与 Memcached 的区别 @@ -391,35 +402,37 @@ Memcached 将内存分割成特定长度的块来存储数据,以完全解决 ## 缓存 -s使用 Redis 作为缓存,将热点数据放到内存中。 +将热点数据放到内存中。 ## 消息队列 -Redis 的 List 类型是双向链表,很适合用于消息队列。 +List 类型是双向链表,很适合用于消息队列。 ## 计数器 -Redis 这种内存数据库才能支持计数器的频繁读写操作。 +Redis 这种内存数据库能支持计数器频繁的读写操作。 ## 好友关系 -使用 set 类型的交集很容易就可以知道两个用户的共同好友。 +使用 Set 类型的交集操作很容易就可以知道两个用户的共同好友。 # 十三、数据淘汰策略 可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 | 策略 | 描述 | -| -- | -- | +| :--: | :--: | | volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | | volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | |volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | | allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | | allkeys-random | 从所有数据集中任意选择数据进行淘汰 | -| no-envicition | 禁止驱逐数据 | +| noeviction | 禁止驱逐数据 | 如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 +作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key。抽样数量可通过 maxmemory-samples 配置。 + # 十四、一个简单的论坛系统分析 该论坛系统功能如下: @@ -458,3 +471,4 @@ Redis 没有关系型数据库中的表这一概念来将同类型的数据存 - [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html) - [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/) diff --git a/notes/SQL.md b/notes/SQL.md index 729005b4..2eb67e58 100644 --- a/notes/SQL.md +++ b/notes/SQL.md @@ -175,7 +175,7 @@ ORDER BY col1 DESC, col2 ASC; # 九、过滤 -不进行过滤的数据非常大,导致通过网络传输了很多多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。 +不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。 ```sql SELECT * @@ -190,13 +190,13 @@ WHERE col IS NULL; | = < > | 等于 小于 大于 | | <> != | 不等于 | | <= !> | 小于等于 | -| >= !< | 大于等于 | +| >= !< | 大于等于 | | BETWEEN | 在两个值之间 | | IS NULL | 为NULL值 | 应该注意到,NULL 与 0 、空字符串都不同。 -**AND OR** 用于连接多个过滤条件。优先处理 AND,因此当一个过滤表达式涉及到多个 AND 和 OR 时,应当使用 () 来决定优先级。 +**AND OR** 用于连接多个过滤条件,优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。 **IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。 @@ -206,9 +206,9 @@ WHERE col IS NULL; 通配符也是用在过滤语句中,但它只能用于文本字段。 -- **%** 匹配 >=0 个任意字符,类似于 \*; +- **%** 匹配 >=0 个任意字符; -- **\_** 匹配 ==1 个任意字符,类似于 \.; +- **\_** 匹配 ==1 个任意字符; - **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。 @@ -246,14 +246,14 @@ FROM mytable ## 文本处理 | 函数 | 说明 | -| ------------ | ------------ | +| :---: | :---: | | LEFT() RIGHT() | 左边或者右边的字符 | | LOWER() UPPER() | 转换为小写或者大写 | | LTRIM() RTIM() | 去除左边或者右边的空格 | | LENGTH() | 长度 | -| SUNDEX() | 转换为语音值 | +| SOUNDEX() | 转换为语音值 | -其中, **SOUNDEX()** 是将一个字符串转换为描述其语音表示的字母数字模式的算法,它是根据发音而不是字母比较。 +其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。 ```sql SELECT * @@ -267,7 +267,7 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple') - 时间格式:HH:MM:SS |函 数 | 说 明| -| --- | --- | +| :---: | :---: | | AddDate() | 增加一个日期(天、周等)| | AddTime() | 增加一个时间(时、分等)| | CurDate() | 返回当前日期 | @@ -288,13 +288,16 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple') ```sql mysql> SELECT NOW(); - -> '2017-06-28 14:01:52' +``` + +``` +2018-4-14 20:25:11 ``` ## 数值处理 | 函数 | 说明 | -| --- | --- | +| :---: | :---: | | SIN() | 正弦 | | COS() | 余弦 | | TAN() | 正切 | @@ -308,7 +311,7 @@ mysql> SELECT NOW(); ## 汇总 |函 数 |说 明| -| --- | --- | +| :---: | :---: | | AVG() | 返回某列的平均值 | | COUNT() | 返回某列的行数 | | MAX() | 返回某列的最大值 | @@ -317,7 +320,7 @@ mysql> SELECT NOW(); AVG() 会忽略 NULL 行。 -使用 DISTINCT 可以汇总函数值汇总不同的值。 +使用 DISTINCT 可以让汇总函数值汇总不同的值。 ```sql SELECT AVG(DISTINCT col1) AS avg_col @@ -330,7 +333,7 @@ FROM mytable 可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。 -指定的分组字段除了能让数组按该字段进行分组,也可以按该字段进行排序,例如按 col 字段排序并分组数据: +指定的分组字段除了能按该字段进行分组,也会自动按按该字段进行排序。 ```sql SELECT col, COUNT(*) AS num @@ -338,17 +341,7 @@ FROM mytable GROUP BY col; ``` -WHERE 过滤行,HAVING 过滤分组。行过滤应当先与分组过滤; - -```sql -SELECT col, COUNT(*) AS num -FROM mytable -WHERE col > 2 -GROUP BY col -HAVING COUNT(*) >= 2; -``` - -GROUP BY 的排序结果为分组字段,而 ORDER BY 也可以以聚集字段来进行排序。 +GROUP BY 按分组字段进行排序,ORDER BY 也可以以汇总字段来进行排序。 ```sql SELECT col, COUNT(*) AS num @@ -357,10 +350,20 @@ GROUP BY col ORDER BY num; ``` +WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。 + +```sql +SELECT col, COUNT(*) AS num +FROM mytable +WHERE col > 2 +GROUP BY col +HAVING num >= 2; +``` + 分组规定: 1. GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前; -2. 除了汇总计算语句的字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出; +2. 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出; 3. NULL 的行会单独分为一组; 4. 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。 @@ -374,7 +377,7 @@ ORDER BY num; SELECT * FROM mytable1 WHERE col1 IN (SELECT col2 - FROM mytable2); + FROM mytable2); ``` 下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次: @@ -390,7 +393,7 @@ ORDER BY cust_name; # 十五、连接 -连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 Where。 +连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。 连接可以替换子查询,并且比子查询的效率一般会更快。 @@ -436,10 +439,10 @@ where department = ( 自连接版本 ```sql -select name +select e1.name from employee as e1, employee as e2 where e1.department = e2.department - and e1.name = "Jim"; + and e2.name = "Jim"; ``` 连接一般比子查询的效率高。 @@ -463,8 +466,8 @@ from employee natural join department; ```sql select Customers.cust_id, Orders.order_num - from Customers left outer join Orders - on Customers.cust_id = Orders.curt_id; +from Customers left outer join Orders +on Customers.cust_id = Orders.cust_id; ``` 如果需要统计顾客的订单数,使用聚集函数。 @@ -473,15 +476,15 @@ select Customers.cust_id, Orders.order_num select Customers.cust_id, COUNT(Orders.order_num) as num_ord from Customers left outer join Orders -on Customers.cust_id = Orders.curt_id +on Customers.cust_id = Orders.cust_id group by Customers.cust_id; ``` # 十六、组合查询 -使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果为 M+N 行。 +使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。 -每个查询必须包含相同的列、表达式或者聚集函数。 +每个查询必须包含相同的列、表达式和聚集函数。 默认会去除相同行,如果需要保留相同行,使用 UNION ALL。 @@ -522,9 +525,7 @@ WHERE col5 = val; ## 使用存储过程的好处 1. 代码封装,保证了一定的安全性; - 2. 代码复用; - 3. 由于是预先编译,因此具有很高的性能。 ## 创建存储过程 @@ -624,7 +625,7 @@ MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储 不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。 -MySQL 的事务提交默认是隐式提交,也就是每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。 +MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。 通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。 @@ -705,8 +706,6 @@ SHOW GRANTS FOR myuser; GRANT SELECT, INSERT ON mydatabase.* TO myuser; ``` -

- 账户用 username@host 的形式定义,username@% 使用的是默认主机名。 ## 删除权限 diff --git a/notes/一致性协议.md b/notes/一致性协议.md index 8913ff3b..ecac4a1e 100644 --- a/notes/一致性协议.md +++ b/notes/一致性协议.md @@ -58,7 +58,7 @@ Two-phase Commit(2PC)。

-如果 Acceptor 接受到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2,那么就丢弃该提议请求;否则,发送提议响应,该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收到的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。 +如果 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 类似。 @@ -66,7 +66,7 @@ Two-phase Commit(2PC)。 当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。 -Proposer A 接受到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。 +Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。 Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。 diff --git a/notes/分布式基础.md b/notes/分布式基础.md index 39029a48..f7f77498 100644 --- a/notes/分布式基础.md +++ b/notes/分布式基础.md @@ -81,7 +81,7 @@ ### 4. 可扩展性 -指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线程增加。 +指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线性增加。 # 二、数据分布 @@ -95,7 +95,7 @@ **一致性哈希** -Distributed Hash Table(DHT):对于哈希空间 0\~2n,将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。 +Distributed Hash Table(DHT):对于哈希空间 [0, 2n-1],将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。

@@ -153,13 +153,15 @@ Distributed Hash Table(DHT):对于哈希空间 0\~2n,将该 在设计分布式系统时,需要根据实际需求弱化某一要求。因此就有了下图中的三种设计:CA、CP 和 AP。 -

+

需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此实际上设计分布式系统需要在一致性和可用性之间做权衡。 # 六、BASE -BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 +BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +

## 基本可用 @@ -195,7 +197,7 @@ ACID 是传统数据库系统常用的设计理论,追求强一致性模型。 # 八、CDN 架构 -CND 通过将内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。不仅可以减轻服务器的负担,也可以提高用户的访问速度。 +通过将内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。不仅可以减轻服务器的负担,也可以提高用户的访问速度。 从下图可以看出,DNS 在对域名解析时不再向用户返回源服务器的 IP 地址,而是返回边缘节点的 IP 地址,所以用户最终访问的是边缘节点。边缘节点会先从源服务器中获取用户所需的数据,如果请求成功,边缘节点会将页面缓存下来,下次用户访问时可以直接读取。 diff --git a/notes/分布式问题分析.md b/notes/分布式问题分析.md index 9d4ff810..b9064a50 100644 --- a/notes/分布式问题分析.md +++ b/notes/分布式问题分析.md @@ -59,7 +59,7 @@ #### 2.1 消息处理模型 -(一)点对点 +(一)消息队列

@@ -74,11 +74,11 @@ 发送端完成操作后一定能将消息成功发送到消息系统。 -实现方法:在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。 +实现方法:在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。 (二)接收端的可靠性 -接收端仅且能够从消息中间件成功消费一次消息。 +接收端能够从消息中间件成功消费一次消息。 实现方法: @@ -117,7 +117,7 @@ ### 4. 加权最小连接(Weighted Least Connection) -在最小连接的基础上,根据服务器的性能为每台服务器分配权重,根据权重计算出每台服务器能处理的连接数。 +在最小连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。

@@ -127,6 +127,15 @@

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

+ ## 实现 ### 1. HTTP 重定向 @@ -179,7 +188,7 @@ Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchroni ## 使用场景 -在服务器端使用分布式部署的情况下,一个服务可能分布在不同的节点上,比如订单服务分布式在节点 A 和节点 B 上。如果多个客户端同时对一个服务进行请求时,就需要使用分布式锁。例如一个服务可以使用 APP 端或者 Web 端进行访问,如果一个用户同时使用 APP 端和 Web 端访问该服务,并且 APP 端的请求路由到了节点 A,WEB 端的请求被路由到了节点 B,这时候就需要使用分布式锁来进行同步。 +在服务器端使用分布式部署的情况下,一个服务可能分布在不同的节点上,比如订单服务分布在节点 A 和节点 B 上。如果多个客户端同时对一个服务进行请求时,就需要使用分布式锁。例如一个服务可以使用 APP 端或者 Web 端进行访问,如果一个用户同时使用 APP 端和 Web 端访问该服务,并且 APP 端的请求路由到了节点 A,WEB 端的请求被路由到了节点 B,这时候就需要使用分布式锁来进行同步。 ## 实现方式 diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index 7292b327..4f6bd9ad 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -90,22 +90,26 @@ 在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5, 3},那么对应的输出是第一个重复的数字 2。 +要求复杂度为 O(N) + O(1),时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。 + ## 解题思路 这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。 以 (2, 3, 1, 0, 2, 5) 为例: -```html +```text-html-basic position-0 : (2,3,1,0,2,5) // 2 <-> 1 (1,3,2,0,2,5) // 1 <-> 3 - (3,1,1,0,2,5) // 3 <-> 0 - (0,1,1,3,2,5) // already in position -position-1 : (0,1,1,3,2,5) // already in position -position-2 : (0,1,1,3,2,5) // nums[i] == nums[nums[i]], exit + (3,1,2,0,2,5) // 3 <-> 0 + (0,1,2,3,2,5) // already in position +position-1 : (0,1,2,3,2,5) // already in position +position-2 : (0,1,2,3,2,5) // already in position +position-3 : (0,1,2,3,2,5) // already in position +position-4 : (0,1,2,3,2,5) // nums[i] == nums[nums[i]], exit ``` -遍历到位置 2 时,该位置上的数为 1,但是第 1 个位置上已经有一个 1 的值了,因此可以知道 1 重复。 +遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复。 复杂度:O(N) + O(1) @@ -294,7 +298,7 @@ public ArrayList printListFromTailToHead(ListNode listNode) { ## 题目描述 -根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。 +根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 ```html preorder = [3,9,20,15,7] @@ -337,7 +341,7 @@ private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, ## 解题思路 -① 如果一个节点有右子树不为空,那么该节点的下一个节点是右子树的最左节点; +① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;

@@ -391,12 +395,15 @@ public void push(int node) { in.push(node); } -public int pop() { +public int pop() throws Exception { if (out.isEmpty()) { while (!in.isEmpty()) { out.push(in.pop()); } } + if (out.isEmpty()) { + throw new Exception("queue is empty"); + } return out.pop(); } ``` @@ -405,18 +412,48 @@ public int pop() { ## 题目描述 -以 O(1) 的时间复杂度求菲波那切数列。 +求菲波那契数列的第 n 项。

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

递归方法是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,避免重复求解子问题。 +```java +public int Fibonacci(int n) { + if(n <= 1) return n; + int[] fib = new int[n + 1]; + fib[1] = 1; + for (int i = 2; i <= n; i++) { + fib[i] = fib[i - 1] + fib[i - 2]; + } + return fib[n]; +} +``` + +考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。 + +```java +public int Fibonacci(int n) { + if(n <= 1) return n; + int pre2 = 0, pre1 = 1; + int fib = 0; + for (int i = 2; i <= n; i++) { + fib = pre2 + pre1; + pre2 = pre1; + pre1 = fib; + } + return fib; +} +``` + +由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。 + ```java public class Solution { private int[] fib = new int[40]; @@ -441,6 +478,8 @@ public class Solution { ## 解题思路 +复杂度:O(N) + O(N) + ```java public int JumpFloor(int n) { if (n == 1) return 1; @@ -454,6 +493,22 @@ public int JumpFloor(int n) { } ``` +复杂度:O(N) + O(1) + +```java +public int JumpFloor(int n) { + if (n <= 1) return n; + int pre2 = 0, pre1 = 1; + int result = 0; + for (int i = 1; i <= n; i++) { + result = pre2 + pre1; + pre2 = pre1; + pre1 = result; + } + return result; +} +``` + # 10.3 变态跳台阶 ## 题目描述 @@ -483,9 +538,11 @@ public int JumpFloorII(int n) { ## 解题思路 +复杂度:O(N) + O(N) + ```java public int RectCover(int n) { - if (n < 2) return n; + if (n <= 2) return n; int[] dp = new int[n]; dp[0] = 1; dp[1] = 2; @@ -496,6 +553,22 @@ public int RectCover(int n) { } ``` +复杂度:O(N) + O(1) + +```java +public int RectCover(int n) { + if (n <= 2) return n; + int pre2 = 1, pre1 = 2; + int result = 0; + for (int i = 3; i <= n; i++) { + result = pre2 + pre1; + pre2 = pre1; + pre1 = result; + } + return result; +} +``` + # 11. 旋转数组的最小数字 ## 题目描述 @@ -543,7 +616,11 @@ public int minNumberInRotateArray(int[] nums) { ## 题目描述 -请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。例如 a b c e s f c s a d e e 矩阵中包含一条字符串 "bcced" 的路径,但是矩阵中不包含 "abcb" 路径,因为字符串的第一个字符 b 占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 +请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 + +例如下面的矩阵包含了一条 bfce 路径。 + +

## 解题思路 @@ -797,13 +874,13 @@ 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)。 +综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N \~ 2,因此该算法的平均时间复杂度为 O(1)。 ```java public ListNode deleteNode(ListNode head, ListNode tobeDelete) { @@ -852,17 +929,17 @@ public ListNode deleteDuplication(ListNode pHead) { ## 解题思路 -应该注意到,'.' 是用来代替一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而把它当成重复前面字符一次。 +应该注意到,'.' 是用来当做一个任意字符,而 '\*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '\*' 进行类比,从而把它当成重复前面字符一次。 ```html p.charAt(j) == s.charAt(i) : dp[i][j] = dp[i-1][j-1]; p.charAt(j) == '.' : dp[i][j] = dp[i-1][j-1]; p.charAt(j) == '*' : - p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] // in this case, a* only counts as empty + p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] //a* only counts as empty p.charAt(j-1) == s.charAt(i) or p.charAt(i-1) == '.': - dp[i][j] = dp[i-1][j] // in this case, a* counts as multiple a - or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a - or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty + 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 @@ -1067,7 +1144,7 @@ public ListNode Merge(ListNode list1, ListNode list2) { ListNode head = new ListNode(-1); ListNode cur = head; while (list1 != null && list2 != null) { - if (list1.val < list2.val) { + if (list1.val <= list2.val) { cur.next = list1; list1 = list1.next; } else { @@ -1217,6 +1294,8 @@ public int min() { ## 解题思路 +使用一个栈来模拟压入弹出操作。 + ```java public boolean IsPopOrder(int[] pushA, int[] popA) { int n = pushA.length; @@ -1332,7 +1411,7 @@ public ArrayList> Print(TreeNode pRoot) { ## 题目描述 -输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。 +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。假设输入的数组的任意两个数字都互不相同。 例如,下图是后序遍历序列 3,1,2 所对应的二叉搜索树。 @@ -1494,7 +1573,7 @@ public class Solution { public String Serialize(TreeNode root) { if (root == null) return "#"; - return root.val + " " + Serialize(root.left) + " " + Serialize(root.right); + return root.val + " " + Serialize(root.left) + " " + Serialize(root.right); } public TreeNode Deserialize(String str) { @@ -1557,7 +1636,7 @@ private void backtracking(char[] chars, boolean[] hasUsed, StringBuffer s) { ## 解题思路 -多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(n)。 +多数投票问题,可以利用 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。 @@ -1596,36 +1675,30 @@ public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { int kthSmallest = findKthSmallest(nums, k - 1); ArrayList ret = new ArrayList<>(); for (int val : nums) { - if (val <= kthSmallest && ret.size() < k) ret.add(val); + if (val <= kthSmallest && ret.size() < k) { + ret.add(val); + } } return ret; } public int findKthSmallest(int[] nums, int k) { - int l = 0; - int h = nums.length - 1; + int l = 0, h = nums.length - 1; while (l < h) { int j = partition(nums, l, h); - if (j < k) { - l = j + 1; - } else if (j > k) { - h = j - 1; - } else { - break; - } + if (j == k) break; + if (j > k) h = j - 1; + else l = j + 1; } return nums[k]; } private int partition(int[] nums, int l, int h) { - int i = l; - int j = h + 1; + int i = l, j = h + 1; while (true) { while (i < h && nums[++i] < nums[l]) ; while (j > l && nums[l] < nums[--j]) ; - if (i >= j) { - break; - } + if (i >= j) break; swap(nums, i, j); } swap(nums, l, j); @@ -1633,9 +1706,7 @@ private int partition(int[] nums, int l, int h) { } private void swap(int[] nums, int i, int j) { - int t = nums[i]; - nums[i] = nums[j]; - nums[j] = t; + int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } ``` @@ -1805,7 +1876,7 @@ private int getAmountOfDigit(int digit) { } /** - * 在 digit 位数组成的字符串中,第 index 为的数 + * 在 digit 位数组成的字符串中,第 index 个数 */ private int digitAtIndex(int index, int digit) { int number = beginNumber(digit) + index / digit; @@ -1917,6 +1988,7 @@ public int longestSubStringWithoutDuplication(String str) { int curLen = 0; int maxLen = 0; int[] indexs = new int[26]; + Arrays.fill(indexs, -1); for (int i = 0; i < str.length(); i++) { int c = str.charAt(i) - 'a'; int preIndex = indexs[c]; @@ -1941,21 +2013,19 @@ public int longestSubStringWithoutDuplication(String str) { ## 解题思路 ```java -public int GetUglyNumber_Solution(int N) { - if (N <= 6) return N; +public int GetUglyNumber_Solution(int index) { + if (index <= 6) return index; int i2 = 0, i3 = 0, i5 = 0; - int cnt = 1; - int[] dp = new int[N]; + int[] dp = new int[index]; dp[0] = 1; - while (cnt < N) { + for (int i = 1; i < index; i++) { int n2 = dp[i2] * 2, n3 = dp[i3] * 3, n5 = dp[i5] * 5; - int min = Math.min(n2, Math.min(n3, n5)); - dp[cnt++] = min; - if (min == n2) i2++; - if (min == n3) i3++; - if (min == n5) i5++; + dp[i] = Math.min(n2, Math.min(n3, n5)); + if (dp[i] == n2) i2++; + if (dp[i] == n3) i3++; + if (dp[i] == n5) i5++; } - return dp[N - 1]; + return dp[index - 1]; } ``` @@ -1978,7 +2048,7 @@ public int FirstNotRepeatingChar(String str) { } ``` -以上的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么我们只需要统计的次数信息只有 0,1,更大,那么使用两个比特位就能存储这些信息。 +以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么我们只需要统计的次数信息只有 0,1,更大,那么使用两个比特位就能存储这些信息。 ```java public int FirstNotRepeatingChar(String str) { @@ -2077,7 +2147,10 @@ Output: ## 解题思路 -可以用二分查找找出数字在数组的最左端和最右端。 +可以用二分查找找出数字在数组的最左端和最右端,找最左端和最右端在方法实现上的区别主要在于对 nums[m] == K 的处理: + +- 找最左端令 h = m - 1 +- 找最右端令 l = m + 1 ```java public int GetNumberOfK(int[] nums, int K) { @@ -2313,7 +2386,7 @@ private void reverse(char[] c, int i, int j) { ## 解题思路 ```java -public String LeftRotateString(String str,int n) { +public String LeftRotateString(String str, int n) { if(str.length() == 0) return ""; char[] c = str.toCharArray(); reverse(c, 0, n - 1); @@ -2365,7 +2438,7 @@ public ArrayList maxInWindows(int[] num, int size) { ### 动态规划解法 -空间复杂度:O(n2) +空间复杂度:O(N2) ```java private static int face = 6; @@ -2393,7 +2466,7 @@ public double countProbability(int n, int s) { ### 动态规划解法 + 旋转数组 -空间复杂度:O(n) +空间复杂度:O(N) ```java private static int face = 6; @@ -2449,11 +2522,11 @@ public boolean isContinuous(int[] nums) { ## 题目描述 -让小朋友们围成一个大圈。然后 , 他随机指定一个数 m, 让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌 , 然后可以在礼品箱中任意的挑选礼物 , 并且不再回到圈中 , 从他的下一个小朋友开始 , 继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友 , 可以不用表演。 +让小朋友们围成一个大圈。然后,他随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。 ## 解题思路 -约瑟夫环 +约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。 ```java public int LastRemaining_Solution(int n, int m) { @@ -2471,6 +2544,8 @@ public int LastRemaining_Solution(int n, int m) { ## 解题思路 +使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该是 i 之前并且价格最低。 + ```java public int maxProfit(int[] prices) { int n = prices.length; @@ -2503,7 +2578,11 @@ public int Sum_Solution(int n) { # 65. 不用加减乘除做加法 -a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 +## 解题思路 + +a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。 + +递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 ```java public int Add(int num1, int num2) { @@ -2536,6 +2615,8 @@ public int[] multiply(int[] A) { # 67. 把字符串转换成整数 +## 解题思路 + ```java public int StrToInt(String str) { if (str.length() == 0) return 0; @@ -2544,7 +2625,7 @@ public int StrToInt(String str) { int ret = 0; for (int i = 0; i < chars.length; i++) { if (i == 0 && (chars[i] == '+' || chars[i] == '-')) continue; - if (chars[i] < '0' || chars[i] > '9') return 0; + if (chars[i] < '0' || chars[i] > '9') return 0; // 非法输入 ret = ret * 10 + (chars[i] - '0'); } return isNegative ? -ret : ret; @@ -2559,6 +2640,8 @@ public int StrToInt(String str) {

+二叉查找树中,两个节点 p, q 的公共祖先 root 满足 p.val <= root.val && root.val <= q.val,只要找到满足这个条件的最低层节点即可。换句话说,应该先考虑子树的解而不是根节点的解,二叉树的后序遍历操作满足这个特性。在本题中我们可以利用后序遍历的特性,先在左右子树中查找解,最后再考虑根节点的解。 + ```java public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if(root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q); @@ -2571,6 +2654,8 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {

+在左右子树中查找两个节点的最低公共祖先,如果在其中一颗子树中查找到,那么就返回这个解,否则可以认为根节点就是最低公共祖先。 + ```java public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) return root; diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index 56d77b06..19a76d1a 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -42,13 +42,13 @@ ## 概念 -

+

事务指的是满足 ACID 特性的一系列操作。在数据库中,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 ## 四大特性 -

+

### 1. 原子性(Atomicity) @@ -76,23 +76,25 @@ T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。 +

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

+

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

+

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

+

## 解决方法 @@ -100,7 +102,7 @@ T1 读取某个范围的数据,T2 在这个范围内插 在没有并发的情况下,事务以串行的方式执行,互不干扰,因此可以保证隔离性。在并发的情况下,如果能通过并发控制,让事务的执行结果和某一个串行执行的结果相同,就认为事务的执行结果满足隔离性要求,也就是说是正确的。把这种事务执行方式称为 **可串行化调度** 。 -**并发控制可以通过封锁来实现,但是封锁操作都要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。** +**并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。** # 三、封锁 @@ -108,11 +110,13 @@ T1 读取某个范围的数据,T2 在这个范围内插

+MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 + 应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 -但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。需要在锁开销以及数据安全性之间做一个权衡。 +但是加锁需要消耗资源,锁的各种操作,包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。 -MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 +在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。 ## 封锁类型 @@ -130,7 +134,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 | - | X | S | | :--: | :--: | :--: | -|X|NO|No| +|X|No|No| |S|No|Yes| ### 2. 意向锁 @@ -147,7 +151,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 | - | X | IX | S | IS | | :--: | :--: | :--: | :--: | :--: | |X |No |No |No | No| -|IX |No |YES|No | Yes| +|IX |No |Yes|No | Yes| |S |No |No |Yes| Yes| |IS |No |Yes|Yes| Yes| @@ -157,7 +161,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 **一级封锁协议** -事务 T 要修改数据 A 时必须加 X 锁,直到事务结束才释放锁。 +事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。 可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么一个事务的修改就不会被覆盖。 @@ -225,13 +229,13 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。 ```html -lock-x(A)...lock-s(B)...lock-s(c)...unlock(A)...unlock(C)...unlock(B) +lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B) ``` 但不是必要条件,例如以下操作不满足两段锁协议,但是它还是可串行化调度。 ```html -lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(c)...unlock(C)... +lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)... ``` # 四、隔离级别 @@ -287,62 +291,62 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 ### 1. SELECT -该操作必须保证多个事务读取到同一个数据行的快照,这个快照是最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 +当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。 -当开始新一个事务时,该事务的版本号肯定会大于所有数据行快照的创建版本号,理解这一点很关键。 +多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 -把没对一个数据行做修改的事务称为 T1,T1 所要读取的数据行快照的创建版本号必须小于当前事务的版本号,因为如果大于或者等于当前事务的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 +把没对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 -除了上面的要求,T1 所要读取的数据行快照的删除版本号必须大于当前事务版本号,因为如果小于等于当前事务版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 +除了上面的要求,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 ### 2. INSERT -将系统版本号作为数据行快照的创建版本号。 +将当前系统版本号作为数据行快照的创建版本号。 ### 3. DELETE -将系统版本号作为数据行快照的删除版本号。 +将当前系统版本号作为数据行快照的删除版本号。 ### 4. UPDATE -将系统版本号作为更新后的数据行快照的创建版本号,同时将系统版本号作为作为更新前的数据行快照的删除版本号。可以理解为新执行 DELETE 后执行 INSERT。 +将当前系统版本号作为更新后的数据行快照的创建版本号,同时将当前系统版本号作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。 ## 快照读与当前读 -快照读读指的是读取快照中的数据,而当前读指的是读取最新的数据。 +### 1. 快照读 -当前读: +读取快照中的数据,可以减少加锁所带来的开销。 ```sql select * from table ....; ``` -快照读: +### 2. 当前读 + +读取最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。 ```sql select * from table where ? lock in share mode; select * from table where ? for update; insert; -update ; +update; delete; ``` -引入当前读的目的主要是为了免去加锁操作带来的性能开销,但是快照读需要加锁。 - # 六、Next-Key Locks -Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读隔离级别下,MVCC + Next-Key Locks,就可以防止幻读的出现。 +Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 ## Record Locks -锁定的对象时索引,而不是数据。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Lock 依然可以使用。 +锁定的对象是索引,而不是数据。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。 ## Grap Locks -锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c1 中插入 15。 +锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。 ```sql -SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; +SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ``` ## Next-Key Locks @@ -377,7 +381,7 @@ SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。 -如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 +如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 对于 W->A,如果能找到 W 的真子集 W',使得 W'-> A,那么 W->A 就是部分函数依赖,否则就是完全函数依赖; @@ -393,9 +397,9 @@ SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 不符合范式的关系,会产生很多异常,主要有以下四种异常: -1. 冗余数据,例如学生-2 出现了两次。 -2. 修改异常,修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 -3. 删除异常,删除一个信息,那么也会丢失其它信息。例如如果删除了课程-1,需要删除第二行和第三行,那么学生-1 的信息就会丢失。 +1. 冗余数据:例如 学生-2 出现了两次。 +2. 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 +3. 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。 4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 ## 范式 @@ -425,14 +429,14 @@ SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖: - Sno, Cname -> Sname, Sdept, Mname -- Son -> Sname, Sdept +- Sno -> Sname, Sdept - Sdept -> Mname -- Sno -> Manme +- Sno -> Mname - Sno, Cname-> Grade Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。 -Sname, Sdept 和 Manme 都函数依赖于 Sno,而部分依赖于键码。当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。 +Sname, Sdept 和 Mname 都函数依赖于 Sno,而部分依赖于键码。当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。 **分解后**
@@ -464,7 +468,7 @@ Sname, Sdept 和 Manme 都函数依赖于 Sno,而部分依赖于键码。当 非主属性不传递依赖于键码。 -上面的关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname,可以进行以下分解: +上面的 关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname,可以进行以下分解: 关系-11 diff --git a/notes/正则表达式.md b/notes/正则表达式.md index d890273b..e21a85c5 100644 --- a/notes/正则表达式.md +++ b/notes/正则表达式.md @@ -19,7 +19,7 @@ 正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。 -[正则表达式在线工具](http://tool.chinaz.com/regex) +[正则表达式在线工具](https://regexr.com/) # 二、匹配单个字符 @@ -118,7 +118,7 @@ abc[^0-9] **正则表达式** ``` -[\w.]+@\w+.\w+ +[\w.]+@\w+\.\w+ ``` [\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符; @@ -130,8 +130,8 @@ abc[^0-9] 为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。 ``` -[\w.]+@\w+.\w+ -[\w.]+@[\w]+.[\w]+ +[\w.]+@\w+\.\w+ +[\w.]+@[\w]+\.[\w]+ ``` **{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符; @@ -164,7 +164,7 @@ a.+c ^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。 -使用 (?m) 来打开分行匹配模式,在该模式下,换行被当做字符串的边界。 +分行匹配模式(multiline)下,换行被当做字符串的边界。 **应用** @@ -173,10 +173,10 @@ a.+c **正则表达式** ``` -(?m)^\s*//.*$ +^\s*\/\/.*$ ``` -如果没用 (?m),则只会匹配 // 注释 1 以及之后的所有内容。用了分行匹配模式之后,换行符被当成是字符串分隔符,因此能正确匹配出两个注释内容。 +

**匹配结果** @@ -197,7 +197,7 @@ a.+c **正则表达式** ``` -(ab) {2,} +(ab){2,} ``` **匹配结果** @@ -206,6 +206,8 @@ a.+c **|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。 +**正则表达式** + ``` (19|20)\d{2} ``` @@ -220,21 +222,23 @@ a.+c 匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: -1. 一位或者两位的数字 -2. 1 开头的三位数 -3. 2 开头,第 2 位是 0-4 的三位数 -4. 25 开头,第 3 位是 0-5 的三位数 +1. 一位数字 +2. 不以 0 开头的两位数字 +3. 1 开头的三位数 +4. 2 开头,第 2 位是 0-4 的三位数 +5. 25 开头,第 3 位是 0-5 的三位数 **正则表达式** ``` -(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.) {3}(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))) +((25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))\.){3}(25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d)) ``` **匹配结果** 1. **192.168.0.1** -2. 555.555.555.555 +2. 00.00.00.00 +3. 555.555.555.555 # 八、回溯引用 @@ -249,7 +253,7 @@ a.+c \1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。 ``` -<(h[1-6])>\w*? +<(h[1-6])>\w*?<\/\1> ``` **匹配结果** diff --git a/notes/算法.md b/notes/算法.md index 3dca215a..b6faef12 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -75,13 +75,13 @@ public class ThreeSum { } ``` -该算法的内循环为 if(a[i]+a[j]+a[k]==0) 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 N3。 +该算法的内循环为 if(a[i]+a[j]+a[k]==0) 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 O(N3)。 **改进**
通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。 -该方法可以将 ThreeSum 算法增长数量级降低为 N2logN。 +该方法可以将 ThreeSum 算法增长数量级降低为 O(N2logN)。 ```java public class ThreeSumFast { @@ -104,9 +104,9 @@ public class ThreeSumFast { int l = 0, h = nums.length - 1; while (l <= h) { int m = l + (h - l) / 2; - if (nums[m] == target) return m; - else if (nums[m] < target) h = m - 1; - else l = m + 1; + if (target == nums[m]) return m; + else if (target > nums[m]) l = m + 1; + else h = m - 1; } return -1; } @@ -301,10 +301,12 @@ public class Queue { } // 出队列 - public Item dequeue(){ + public Item dequeue() { + if (isEmpty()) return null; Node node = first; first = first.next; N--; + if (isEmpty()) last = null; return node.item; } } @@ -315,11 +317,11 @@ public class Queue { 用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 -

+

| 方法 | 描述 | -| ---: | :--- | +| :---: | :---: | | UF(int N) | 构造一个大小为 N 的并查集 | | void union(int p, int q) | 连接 p 和 q 节点 | | int find(int p) | 查找 p 所在的连通分量 | @@ -399,7 +401,7 @@ public void union(int p, int q) { 理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 -

+

```java public class WeightedQuickUnionUF { @@ -450,13 +452,11 @@ public class WeightedQuickUnionUF { | :---: | :---: | :---: | | quick-find | N | 1 | | quick-union | 树高 | 树高 | -| 加权 quick-union | lgN | lgN | +| 加权 quick-union | logN | logN | | 路径压缩的加权 quick-union | 非常接近 1 | 非常接近 1 | # 四、排序 - **约定**
- 待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 研究排序算法的成本模型时,计算的是比较和交换的次数。 @@ -464,11 +464,11 @@ public class WeightedQuickUnionUF { 使用辅助函数 less() 和 exch() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。 ```java -private boolean less(Comparable v, Comparable w){ +private boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } -private void exch(Comparable[] a, int i, int j){ +private void exch(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; @@ -479,11 +479,11 @@ private void exch(Comparable[] a, int i, int j){ 找到数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 -

+

```java public class Selection { - public static void sort(Comparable[] a) { + public void sort(Comparable[] a) { int N = a.length; for (int i = 0; i < N; i++) { int min = i; @@ -506,7 +506,7 @@ public class Selection { ```java public class Insertion { - public static void sort(Comparable[] a) { + public void sort(Comparable[] a) { int N = a.length; for (int i = 1; i < N; i++) { for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) { @@ -517,7 +517,11 @@ public class Insertion { } ``` -插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么插入排序会很快。平均情况下插入排序需要 \~N2/4 比较以及 \~N2/4 次交换,最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是逆序的;而最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。 +插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么插入排序会很快。 + +- 平均情况下插入排序需要 \~N2/4 比较以及 \~N2/4 次交换; +- 最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是逆序的; +- 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。 插入排序对于部分有序数组和小规模数组特别高效。 @@ -529,11 +533,11 @@ public class Insertion { 希尔排序使用插入排序对间隔 h 的序列进行排序,如果 h 很大,那么元素就能很快的移到很远的地方。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 -

+

```java public class Shell { - public static void sort(Comparable[] a) { + public void sort(Comparable[] a) { int N = a.length; int h = 1; while (h < N / 3) { @@ -565,9 +569,9 @@ public class Shell { ```java public class MergeSort { - private static Comparable[] aux; + private Comparable[] aux; - private static void merge(Comparable[] a, int lo, int mid, int hi) { + private void merge(Comparable[] a, int lo, int mid, int hi) { int i = lo, j = mid + 1; for (int k = lo; k <= hi; k++) { @@ -577,7 +581,7 @@ public class MergeSort { for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = aux[j++]; else if (j > hi) a[k] = aux[i++]; - else if (aux[i].compareTo(a[j]) < 0) a[k] = aux[i++]; // 先进行这一步,保证稳定性 + else if (aux[i].compareTo(a[j]) <= 0) a[k] = aux[i++]; // 先进行这一步,保证稳定性 else a[k] = aux[j++]; } } @@ -590,12 +594,12 @@ public class MergeSort { ```java -public static void sort(Comparable[] a) { +public void sort(Comparable[] a) { aux = new Comparable[a.length]; sort(a, 0, a.length - 1); } -private static void sort(Comparable[] a, int lo, int hi) { +private void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int mid = lo + (hi - lo) / 2; sort(a, lo, mid); @@ -613,7 +617,7 @@ private static void sort(Comparable[] a, int lo, int hi) { 先归并那些微型数组,然后成对归并得到的子数组。 ```java -public static void busort(Comparable[] a) { +public void busort(Comparable[] a) { int N = a.length; aux = new Comparable[N]; for (int sz = 1; sz < N; sz += sz) { @@ -628,23 +632,30 @@ public static void busort(Comparable[] a) { ### 1. 基本算法 -归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序;快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。 +- 归并排序将数组分为两个子数组分别排序,并将有序的子数组归并使得整个数组排序; +- 快速排序通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也就将整个数组排序了。

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

+

```java -private static int partition(Comparable[] a, int lo, int hi) { +private int partition(Comparable[] a, int lo, int hi) { int i = lo, j = hi + 1; Comparable v = a[lo]; while (true) { @@ -679,15 +690,15 @@ private static int partition(Comparable[] a, int lo, int hi) { ### 4. 算法改进 -**(一)切换到插入排序** +(一)切换到插入排序 因为快速排序在小数组中也会调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 -**(二)三取样** +(二)三取样 最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 -**(三)三向切分** +(三)三向切分 对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。 @@ -695,7 +706,7 @@ private static int partition(Comparable[] a, int lo, int hi) { ```java public class Quick3Way { - public static void sort(Comparable[] a, int lo, int hi) { + public void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int lt = lo, i = lo + 1, gt = hi; Comparable v = a[lo]; @@ -720,7 +731,7 @@ public class Quick3Way { 堆的某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。 -堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地理解节点的关系。 +堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。

@@ -813,13 +824,13 @@ public Key delMax() { 由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序,并且堆排序是原地排序,不占用额外空间。 -**构建堆** +(一)构建堆 无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,因此可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。

-**交换堆顶元素与最后一个元素** +(二)交换堆顶元素与最后一个元素 交换之后需要进行下沉操作维持堆的有序状态。 @@ -901,7 +912,7 @@ public static Comparable select(Comparable[] a, int k) { ## 二分查找实现有序符号表 -使用一对平行数组,一个存储键一个存储值。其中键的数组为 Comparable 数组,值的数组为 Object 数组。 +使用一对平行数组,一个存储键一个存储值。 rank() 方法至关重要,当键在表中时,它能够知道该键的位置;当键不在表中时,它也能知道在何处插入新键。 @@ -970,13 +981,13 @@ public class BinarySearchST, Value> {

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

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

+

基本数据结构: @@ -1017,8 +1028,6 @@ public class BST, Value> { - 如果被查找的键和根节点的键相等,查找命中; - 否则递归地在子树中查找:如果被查找的键较小就在左子树中查找,较大就在右子树中查找。 -BST 的查找操作每次递归都会让区间减少一半,和二分查找类似,因此查找的复杂度为 O(logN)。 - ```java public Value get(Key key) { return get(root, key); @@ -1116,6 +1125,7 @@ private int rank(Key key, Node x) { ```java private Node min(Node x) { + if (x == null) return null; if (x.left == null) return x; return min(x.left); } @@ -1125,7 +1135,7 @@ private Node min(Node x) { 令指向最小节点的链接指向最小节点的右子树。 -

+

```java public void deleteMin() { @@ -1144,7 +1154,8 @@ public Node deleteMin(Node x) { - 如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可; - 否则,让右子树的最小节点替换该节点。 -

+

+ ```java public void delete(Key key) { @@ -1196,7 +1207,7 @@ private void keys(Node x, Queue queue, Key lo, Key hi) {

-2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树更平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 +2-3 查找树引入了 2- 节点和 3- 节点,目的是为了让树平衡。一颗完美平衡的 2-3 查找树的所有空链接到根节点的距离应该是相同的。 ### 1. 插入操作 @@ -1218,8 +1229,6 @@ private void keys(Node x, Queue queue, Key lo, Key hi) { 2-3 查找树的查找和插入操作复杂度和插入顺序无关,在最坏的情况下查找和插入操作访问的节点必然不超过 logN 个,含有 10 亿个节点的 2-3 查找树最多只需要访问 30 个节点就能进行任意的查找和插入操作。 -

- ## 红黑二叉查找树 2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。 @@ -1233,7 +1242,7 @@ private void keys(Node x, Queue queue, Key lo, Key hi) { 画红黑树时可以将红链接画平。 -

+

```java public class RedBlackBST, Value> { @@ -1424,13 +1433,13 @@ public class Transaction{ 拉链法使用链表来存储 hash 值相同的键,从而解决冲突。此时查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。 -

+

对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。 ### 3. 基于线性探测法的散列表 -线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线程探测法,数组的大小 M 应当大于键的个数 N(M>N)。 +线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。使用线性探测法,数组的大小 M 应当大于键的个数 N(M>N)。

@@ -1522,12 +1531,12 @@ public void delete(Key key) { **(四)调整数组大小** -线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。 +线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。例如下图中 2\~5 位置就是一个聚簇。 + +

α = N/M,把 α 称为利用率。理论证明,当 α 小于 1/2 时探测的预计次数只在 1.5 到 2.5 之间。 -

- 为了保证散列表的性能,应当调整数组的大小,使得 α 在 [1/4, 1/2] 之间。 ```java @@ -1549,21 +1558,17 @@ private void resize(int cap) { } ``` -虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1,这是因为散列表中每个键都需要重新计算散列值。随后平均值会下降。 - -

- ## 应用 ### 1. 各种符号表实现的比较 | 算法 | 插入 | 查找 | 是否有序 | | :---: | :---: | :---: | :---: | -| 二分查找实现的有序表 | logN | N | yes | +| 二分查找实现的有序表 | N | logN | yes | | 二叉查找树 | logN | logN | yes | | 2-3 查找树 | logN | logN | yes | -| 拉链法实现的散列表 | logN | N/M | no | -| 线性探测法试下的删列表 | logN | 1 | no | +| 拉链法实现的散列表 | N/M | N/M | no | +| 线性探测法试下的散列表 | 1 | 1 | no | 应当优先考虑散列表,当需要有序性操作时使用红黑树。 diff --git a/notes/计算机操作系统.md b/notes/计算机操作系统.md index 86f82e10..e992bd63 100644 --- a/notes/计算机操作系统.md +++ b/notes/计算机操作系统.md @@ -564,7 +564,7 @@ Linux 中管道通过空文件实现。 1. 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 2. 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 -3. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。 +3. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 4. 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 ## 死锁的处理方法 diff --git a/notes/计算机网络.md b/notes/计算机网络.md index abb448de..d775d73b 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -2,7 +2,6 @@ * [一、概述](#一概述) * [网络的网络](#网络的网络) * [ISP](#isp) - * [互联网的组成](#互联网的组成) * [主机之间的通信方式](#主机之间的通信方式) * [电路交换与分组交换](#电路交换与分组交换) * [时延](#时延) @@ -30,6 +29,7 @@ * [路由选择协议](#路由选择协议) * [网际控制报文协议 ICMP](#网际控制报文协议-icmp) * [分组网间探测 PING](#分组网间探测-ping) + * [Traceroute](#traceroute) * [虚拟专用网 VPN](#虚拟专用网-vpn) * [网络地址转换 NAT](#网络地址转换-nat) * [五、运输层*](#五运输层) @@ -61,25 +61,20 @@ 网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。 -

+

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

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

+

-## 互联网的组成 - -1. 边缘部分:所有连接在互联网上的主机,用户可以直接使用; - -2. 核心部分:由大量的网络和连接这些网络的路由器组成,为边缘部分的主机提供服务。 - -

## 主机之间的通信方式 @@ -87,9 +82,13 @@ 2. 对等(P2P):不区分客户和服务器。 +

+ ## 电路交换与分组交换 -

+

+ +(以上分别为:电路交换、报文交换以及分组交换) ### 1. 电路交换 @@ -103,8 +102,6 @@ 分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。 -

- 存储转发允许在一条传输线路上传送多个主机的分组,也就是说两个用户之间的通信不需要占用端到端的线路资源。 相比于报文交换,由于分组比报文更小,因此分组交换的存储转发速度更加快速。 @@ -224,7 +221,7 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中

-为了方便,取 m=8,设码片 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。 +为了讨论方便,取 m=8,设码片 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。 在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到 @@ -259,7 +256,7 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中 透明表示一个实际存在的事物看起来好像不存在一样。 -帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符,如果出现转移字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。 +帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符,如果出现转义字符,那么就在转义字符前面再加个转义字符,在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。

@@ -537,13 +534,14 @@ PING 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连 Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报。 -Ping 的过程: +## Traceroute -1. 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,但 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; -2. 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTl 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 -3. 不断执行这样的步骤,知道最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 -4. 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 +Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。 +1. 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,但 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; +2. 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 +3. 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 +4. 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 ## 虚拟专用网 VPN @@ -557,7 +555,7 @@ Ping 的过程: VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。 -下图中,场所 A 和 B 的通信部经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。 +下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。

@@ -621,7 +619,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 **三次握手的原因** -为了防止失效的连接请求到达服务器,让服务器错误打开连接。 +第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。 失效的连接请求是指,客户端发送的连接请求在网络中滞留,客户端因为没及时收到服务器端发送的连接确认,因此就重新发送了连接请求。滞留的连接请求并不是丢失,之后还是会到达服务器。如果不进行第三次握手,那么服务器会误认为客户端重新请求连接,然后打开了连接。但是并不是客户端真正打开这个连接,因此客户端不会给服务器发送数据,这个连接就白白浪费了。 @@ -631,11 +629,11 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 -1. A 发送连接释放报文段,FIN=1; +1. A 发送连接释放报文段,FIN=1。 -2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据; +2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。 -3. 当 B 要不再需要连接时,发送连接释放请求报文段,FIN=1 +3. 当 B 要不再需要连接时,发送连接释放请求报文段,FIN=1。 4. A 收到后发出确认,进入 TIME-WAIT 状态,等待 2MSL 时间后释放连接。 @@ -689,7 +687,7 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文

-TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。发送方需要维护有一个叫做拥塞窗口(cwnd)的状态变量。注意拥塞窗口与发送方窗口的区别,拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 +TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量。注意拥塞窗口与发送方窗口的区别,拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 为了便于讨论,做如下假设: @@ -755,6 +753,10 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、

+### 3. 使用的运输层协议 + +DNS 在解析的过程使用 UDP 进行传输,因为 UDP 最大只支持 512 字节的数据,如果超过的话就需要使用 TCP 传输。 + ## 文件传输协议 FTP FTP 在运输层使用 TCP,并且需要建立两个并行的 TCP 连接:控制连接和数据连接。控制连接在整个会话期间一直保持打开,而数据连接在数据传送完毕之后就关闭。控制连接使用端口号 21,数据连接使用端口号 20。 @@ -871,18 +873,25 @@ P2P 是一个分布式系统,任何时候都有对等方加入或者退出。 ## 常用端口 -| 应用层协议 | 端口号 | 运输层协议 | -| -- | -- | -- | -| DNS | 53 | UDP | -| FTP | 控制连接 21,数据连接 20 | TCP | -| TELNET | 23 | TCP | -| DHCP | 67 68 | UDP | -| HTTP | 80 | TCP | -| SMTP | 25 | TCP | -| POP3 | 110 | TCP | -| IMAP | 143 | TCP | +|应用| 应用层协议 | 端口号 | 运输层协议 | 备注 | +| :---: | :--: | :--: | :--: | :--: +| 域名解析 | DNS | 53 | UDP/TCP | 长度超过 512 字节时使用 TCP | +| 动态主机配置协议 | DHCP | 67/68 | UDP | | +| 简单网络管理协议 | SNMP | 161/162 | UDP | | +| 文件传送协议 | FTP | 20/21 | TCP | 控制连接 21,数据连接 20 +| 远程终端协议 | TELNET | 23 | TCP | | +|超文本传送协议 | HTTP | 80 | TCP | | +| 简单邮件传送协议 | SMTP | 25 | TCP | | +| 邮件读取协议 | POP3 | 110 | TCP | | +| 网际报文存取协议 | IMAP | 143 | TCP | | + # 参考资料 - 计算机网络, 谢希仁 - JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014. +- [Tackling emissions targets in Tokyo](http://www.climatechangenews.com/2011/html/university-tokyo.html) +- [What does my ISP know when I use Tor?](http://www.climatechangenews.com/2011/html/university-tokyo.html) +- [Technology-Computer Networking[1]-Computer Networks and the Internet](http://www.linyibin.cn/2017/02/12/technology-ComputerNetworking-Internet/) +- [P2P 网络概述.](http://slidesplayer.com/slide/11616167/) +- [Circuit Switching (a) Circuit switching. (b) Packet switching.](http://slideplayer.com/slide/5115386/) diff --git a/notes/设计模式.md b/notes/设计模式.md index 7df60ac7..93cdd0da 100644 --- a/notes/设计模式.md +++ b/notes/设计模式.md @@ -1,25 +1,20 @@ -* [一、前言](#一前言) -* [二、设计模式概念](#二设计模式概念) -* [三、单例模式](#三单例模式) -* [四、简单工厂](#四简单工厂) -* [五、工厂方法模式](#五工厂方法模式) -* [六、抽象工厂模式](#六抽象工厂模式) +* [一、概述](#一概述) +* [二、单例模式](#二单例模式) +* [三、简单工厂](#三简单工厂) +* [四、工厂方法模式](#四工厂方法模式) +* [五、抽象工厂模式](#五抽象工厂模式) * [参考资料](#参考资料) -# 一、前言 - -文中涉及一些 UML 类图,为了更好地理解,可以先阅读 [UML 类图](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E6%80%9D%E6%83%B3.md#%E7%AC%AC%E4%B8%89%E7%AB%A0-uml)。 - -# 二、设计模式概念 +# 一、概述 设计模式不是代码,而是解决问题的方案,学习现有的设计模式可以做到经验复用。 拥有设计模式词汇,在沟通时就能用更少的词汇来讨论,并且不需要了解底层细节。 -# 三、单例模式 +# 二、单例模式 ## 意图 @@ -131,7 +126,19 @@ if (uniqueInstance == null) { } ``` -# 四、简单工厂 +uniqueInstance 采用 volatile 关键字修饰也是很有必要的。 + +uniqueInstance = new Singleton(); 这段代码其实是分为三步执行。 + +1. 分配内存空间。 +2. 初始化对象。 +3. 将 uniqueInstance 指向分配的内存地址。 + +但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2,这在单线程情况下自然是没有问题。但如果是多线程就有可能 B 线程获得是一个还没有被初始化的对象以致于程序出错。 + +所以使用 volatile 修饰的目的是禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 + +# 三、简单工厂 ## 意图 @@ -171,17 +178,17 @@ public interface Product { ``` ```java -public class ConcreteProduct implements Product{ +public class ConcreteProduct implements Product { } ``` ```java -public class ConcreteProduct1 implements Product{ +public class ConcreteProduct1 implements Product { } ``` ```java -public class ConcreteProduct2 implements Product{ +public class ConcreteProduct2 implements Product { } ``` @@ -207,8 +214,7 @@ public class Client { } ``` - -# 五、工厂方法模式 +# 四、工厂方法模式 ## 意图 @@ -243,7 +249,7 @@ public class ConcreteFactory extends Factory { ``` ```java -public class ConcreteFactory1 extends Factory{ +public class ConcreteFactory1 extends Factory { public Product factoryMethod() { return new ConcreteProduct1(); } @@ -258,7 +264,7 @@ public class ConcreteFactory2 extends Factory { } ``` -# 六、抽象工厂模式 +# 五、抽象工厂模式 ## 意图 @@ -266,17 +272,16 @@ public class ConcreteFactory2 extends Factory { ## 类图 -

+

抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 -抽象工厂模式用到了工厂模式来创建单一对象,在类图左部,AbstractFactory 中的 createProductA 和 createProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。 +抽象工厂模式用到了工厂模式来创建单一对象,AbstractFactory 中的 createProductA 和 createProductB 方法都是让子类来实现,这两个方法单独来看就是在创建一个对象,这符合工厂模式的定义。 至于创建对象的家族这一概念是在 Client 体现,Client 要通过 AbstractFactory 同时调用两个方法来创建出两个对象,在这里这两个对象就有很大的相关性,Client 需要同时创建出这两个对象。 从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂模式使用了继承。 - ## 代码实现 ```java @@ -300,12 +305,12 @@ public class ProductA2 extends AbstractProductA { ``` ```java -public class ProductB1 extends AbstractProductB{ +public class ProductB1 extends AbstractProductB { } ``` ```java -public class ProductB2 extends AbstractProductB{ +public class ProductB2 extends AbstractProductB { } ``` @@ -317,7 +322,7 @@ public abstract class AbstractFactory { ``` ```java -public class ConcreteFactory1 extends AbstractFactory{ +public class ConcreteFactory1 extends AbstractFactory { AbstractProductA createProductA() { return new ProductA1(); } diff --git a/notes/面向对象思想.md b/notes/面向对象思想.md index 02c7222d..8cd324cb 100644 --- a/notes/面向对象思想.md +++ b/notes/面向对象思想.md @@ -21,8 +21,6 @@ ## S.O.L.I.D - - | 简写 | 全拼 | 中文翻译 | | :--: | :--: | :--: | | SRP | The Single Responsibility Principle | 单一责任原则 | @@ -111,11 +109,13 @@ 利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 -封装有三大好处: +优点: -1. 减少耦合 -2. 隐藏内部细节,因此内部结构可以自由修改 -3. 可以对成员进行更精确的控制 +- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改 +- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 +- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能 +- 提高软件的可重用性 +- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的 以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 @@ -206,31 +206,31 @@ public class Music { 用来描述继承关系,在 Java 中使用 extends 关键字。 -

+

## 实现关系 (Realization) 用来实现一个接口,在 Java 中使用 implement 关键字。 -

+

## 聚合关系 (Aggregation) 表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。 -

+

## 组合关系 (Composition) 和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。 -

+

## 关联关系 (Association) 表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。 -

+

## 依赖关系 (Dependency) diff --git a/pics/004edd56-1546-4052-a7f9-a9f7895ccec5.png b/pics/004edd56-1546-4052-a7f9-a9f7895ccec5.png deleted file mode 100644 index 539a6c22..00000000 Binary files a/pics/004edd56-1546-4052-a7f9-a9f7895ccec5.png and /dev/null differ diff --git a/pics/00eda100-dba1-4ec2-9140-5fe5f3855951.jpg b/pics/00eda100-dba1-4ec2-9140-5fe5f3855951.jpg deleted file mode 100644 index 4b063372..00000000 Binary files a/pics/00eda100-dba1-4ec2-9140-5fe5f3855951.jpg and /dev/null differ diff --git a/pics/05e41947-3cbc-4f02-a428-96765ec916ff.png b/pics/05e41947-3cbc-4f02-a428-96765ec916ff.png deleted file mode 100644 index d15370b4..00000000 Binary files a/pics/05e41947-3cbc-4f02-a428-96765ec916ff.png and /dev/null differ diff --git a/pics/07903a31-0fb3-45fc-86f5-26f0b28fa4e7.png b/pics/07903a31-0fb3-45fc-86f5-26f0b28fa4e7.png new file mode 100644 index 00000000..40573164 Binary files /dev/null and b/pics/07903a31-0fb3-45fc-86f5-26f0b28fa4e7.png differ diff --git a/pics/095720ee-84b3-42ff-af71-70ceb6a2f4a3.png b/pics/095720ee-84b3-42ff-af71-70ceb6a2f4a3.png deleted file mode 100644 index 57e4309b..00000000 Binary files a/pics/095720ee-84b3-42ff-af71-70ceb6a2f4a3.png and /dev/null differ diff --git a/pics/09e6e8d4-4d83-4949-b908-6d6b4c2fd064.png b/pics/09e6e8d4-4d83-4949-b908-6d6b4c2fd064.png new file mode 100644 index 00000000..f76e5b5f Binary files /dev/null and b/pics/09e6e8d4-4d83-4949-b908-6d6b4c2fd064.png differ diff --git a/pics/123bdf81-1ef5-48a9-a08c-2db97057b4d2.png b/pics/123bdf81-1ef5-48a9-a08c-2db97057b4d2.png new file mode 100644 index 00000000..3c248d37 Binary files /dev/null and b/pics/123bdf81-1ef5-48a9-a08c-2db97057b4d2.png differ diff --git a/pics/14cfe8d4-e31b-49e0-ac6a-6f4f7aa06ab6.png b/pics/14cfe8d4-e31b-49e0-ac6a-6f4f7aa06ab6.png new file mode 100644 index 00000000..54a1f458 Binary files /dev/null and b/pics/14cfe8d4-e31b-49e0-ac6a-6f4f7aa06ab6.png differ diff --git a/pics/15b45dc6-27aa-4519-9194-f4acfa2b077f.jpg b/pics/15b45dc6-27aa-4519-9194-f4acfa2b077f.jpg new file mode 100644 index 00000000..a61ab275 Binary files /dev/null and b/pics/15b45dc6-27aa-4519-9194-f4acfa2b077f.jpg differ diff --git a/pics/185b9c49-4c13-4241-a848-fbff85c03a64.png b/pics/185b9c49-4c13-4241-a848-fbff85c03a64.png new file mode 100644 index 00000000..6f56bc5e Binary files /dev/null and b/pics/185b9c49-4c13-4241-a848-fbff85c03a64.png differ diff --git a/pics/1974a836-aa6b-4fb8-bce1-6eb11969284a.jpg b/pics/1974a836-aa6b-4fb8-bce1-6eb11969284a.jpg deleted file mode 100644 index bf7776c4..00000000 Binary files a/pics/1974a836-aa6b-4fb8-bce1-6eb11969284a.jpg and /dev/null differ diff --git a/pics/1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg b/pics/1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg new file mode 100644 index 00000000..f43e0f25 Binary files /dev/null and b/pics/1a231f2a-5c2f-4231-8e0f-915aa5894347.jpg differ diff --git a/pics/1b7f180e-7fee-4eaf-8ebb-164b68ae2b29.png b/pics/1b7f180e-7fee-4eaf-8ebb-164b68ae2b29.png new file mode 100644 index 00000000..081bf5aa Binary files /dev/null and b/pics/1b7f180e-7fee-4eaf-8ebb-164b68ae2b29.png differ diff --git a/pics/1be8b4b0-cc7a-44d7-9c77-85be37b76f7d.png b/pics/1be8b4b0-cc7a-44d7-9c77-85be37b76f7d.png new file mode 100644 index 00000000..6ba8797b Binary files /dev/null and b/pics/1be8b4b0-cc7a-44d7-9c77-85be37b76f7d.png differ diff --git a/pics/1bfa3118-f3cd-4480-a950-cf6d646015db.png b/pics/1bfa3118-f3cd-4480-a950-cf6d646015db.png new file mode 100644 index 00000000..c28fe627 Binary files /dev/null and b/pics/1bfa3118-f3cd-4480-a950-cf6d646015db.png differ diff --git a/pics/2018040302.jpg b/pics/2018040302.jpg new file mode 100644 index 00000000..27daefae Binary files /dev/null and b/pics/2018040302.jpg differ diff --git a/pics/26020e1a-06ab-4114-a6b3-e428de690c7e.png b/pics/26020e1a-06ab-4114-a6b3-e428de690c7e.png deleted file mode 100644 index 02876968..00000000 Binary files a/pics/26020e1a-06ab-4114-a6b3-e428de690c7e.png and /dev/null differ diff --git a/pics/2ad244f5-939c-49fa-9385-69bc688677ab.jpg b/pics/2ad244f5-939c-49fa-9385-69bc688677ab.jpg new file mode 100644 index 00000000..5c1e0af9 Binary files /dev/null and b/pics/2ad244f5-939c-49fa-9385-69bc688677ab.jpg differ diff --git a/pics/3086c248-b552-499e-b101-9cffe5c2773e.png b/pics/3086c248-b552-499e-b101-9cffe5c2773e.png new file mode 100644 index 00000000..c23e85c9 Binary files /dev/null and b/pics/3086c248-b552-499e-b101-9cffe5c2773e.png differ diff --git a/pics/3290673d-edab-4678-8b2e-f18e0f6b7fc1.png b/pics/3290673d-edab-4678-8b2e-f18e0f6b7fc1.png deleted file mode 100644 index 8322e80a..00000000 Binary files a/pics/3290673d-edab-4678-8b2e-f18e0f6b7fc1.png and /dev/null differ diff --git a/pics/3646544a-cb57-451d-9e03-d3c4f5e4434a.png b/pics/3646544a-cb57-451d-9e03-d3c4f5e4434a.png new file mode 100644 index 00000000..76d45e19 Binary files /dev/null and b/pics/3646544a-cb57-451d-9e03-d3c4f5e4434a.png differ diff --git a/pics/386cd64f-7a9d-40e6-8c55-22b90ee2d258.png b/pics/386cd64f-7a9d-40e6-8c55-22b90ee2d258.png new file mode 100644 index 00000000..5f5f5636 Binary files /dev/null and b/pics/386cd64f-7a9d-40e6-8c55-22b90ee2d258.png differ diff --git a/pics/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png b/pics/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png new file mode 100644 index 00000000..8e363e47 Binary files /dev/null and b/pics/39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png differ diff --git a/pics/41392d76-dd1d-4712-85d9-e8bb46b04a2d.png b/pics/41392d76-dd1d-4712-85d9-e8bb46b04a2d.png new file mode 100644 index 00000000..d2fd2c63 Binary files /dev/null and b/pics/41392d76-dd1d-4712-85d9-e8bb46b04a2d.png differ diff --git a/pics/46cec213-3048-4a80-aded-fdd577542801.jpg b/pics/46cec213-3048-4a80-aded-fdd577542801.jpg new file mode 100644 index 00000000..32e1f3d5 Binary files /dev/null and b/pics/46cec213-3048-4a80-aded-fdd577542801.jpg differ diff --git a/pics/47358f87-bc4c-496f-9a90-8d696de94cee.png b/pics/47358f87-bc4c-496f-9a90-8d696de94cee.png new file mode 100644 index 00000000..83d59359 Binary files /dev/null and b/pics/47358f87-bc4c-496f-9a90-8d696de94cee.png differ diff --git a/pics/4e760981-a0c5-4dbf-9fbf-ce963e0629fb.png b/pics/4e760981-a0c5-4dbf-9fbf-ce963e0629fb.png new file mode 100644 index 00000000..e5768980 Binary files /dev/null and b/pics/4e760981-a0c5-4dbf-9fbf-ce963e0629fb.png differ diff --git a/pics/4f4deaf4-8487-4de2-9d62-5ad017ee9589.png b/pics/4f4deaf4-8487-4de2-9d62-5ad017ee9589.png deleted file mode 100644 index d1589cd8..00000000 Binary files a/pics/4f4deaf4-8487-4de2-9d62-5ad017ee9589.png and /dev/null differ diff --git a/pics/4fc032e0-ac6f-4b42-9182-ee104a25e7a1.png b/pics/4fc032e0-ac6f-4b42-9182-ee104a25e7a1.png new file mode 100644 index 00000000..4c61977c Binary files /dev/null and b/pics/4fc032e0-ac6f-4b42-9182-ee104a25e7a1.png differ diff --git a/pics/518f16f2-a9f7-499a-98e1-f1dbb37b5a9a.png b/pics/518f16f2-a9f7-499a-98e1-f1dbb37b5a9a.png new file mode 100644 index 00000000..3a1010b9 Binary files /dev/null and b/pics/518f16f2-a9f7-499a-98e1-f1dbb37b5a9a.png differ diff --git a/pics/5341d726-ffde-4d2a-a000-46597bcc9c5a.png b/pics/5341d726-ffde-4d2a-a000-46597bcc9c5a.png new file mode 100644 index 00000000..61161099 Binary files /dev/null and b/pics/5341d726-ffde-4d2a-a000-46597bcc9c5a.png differ diff --git a/pics/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png b/pics/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png new file mode 100644 index 00000000..29bbc9de Binary files /dev/null and b/pics/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png differ diff --git a/pics/540631a4-6018-40a5-aed7-081e2eeeaeea.png b/pics/540631a4-6018-40a5-aed7-081e2eeeaeea.png new file mode 100644 index 00000000..e22b2c83 Binary files /dev/null and b/pics/540631a4-6018-40a5-aed7-081e2eeeaeea.png differ diff --git a/pics/54cb3f21-485b-4159-8bf5-dcde1c4d4c36.png b/pics/54cb3f21-485b-4159-8bf5-dcde1c4d4c36.png new file mode 100644 index 00000000..3a69d2ce Binary files /dev/null and b/pics/54cb3f21-485b-4159-8bf5-dcde1c4d4c36.png differ diff --git a/pics/5930aeb8-847d-4e9f-a168-9334d7dec744.png b/pics/5930aeb8-847d-4e9f-a168-9334d7dec744.png new file mode 100644 index 00000000..6c9a572e Binary files /dev/null and b/pics/5930aeb8-847d-4e9f-a168-9334d7dec744.png differ diff --git a/pics/5aac64d3-2c7b-4f32-9e9a-1df2186f588b.png b/pics/5aac64d3-2c7b-4f32-9e9a-1df2186f588b.png new file mode 100644 index 00000000..f457a99d Binary files /dev/null and b/pics/5aac64d3-2c7b-4f32-9e9a-1df2186f588b.png differ diff --git a/pics/5e8d3c04-d93b-48a7-875e-41ababed00e0.jpg b/pics/5e8d3c04-d93b-48a7-875e-41ababed00e0.jpg new file mode 100644 index 00000000..b8d0adf8 Binary files /dev/null and b/pics/5e8d3c04-d93b-48a7-875e-41ababed00e0.jpg differ diff --git a/pics/600e9c75-5033-4dad-ae2b-930957db638e.png b/pics/600e9c75-5033-4dad-ae2b-930957db638e.png new file mode 100644 index 00000000..bf0834d5 Binary files /dev/null and b/pics/600e9c75-5033-4dad-ae2b-930957db638e.png differ diff --git a/pics/61942711-45a0-4e11-bbc9-434e31436f33.png b/pics/61942711-45a0-4e11-bbc9-434e31436f33.png new file mode 100644 index 00000000..8f093ef6 Binary files /dev/null and b/pics/61942711-45a0-4e11-bbc9-434e31436f33.png differ diff --git a/pics/68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png b/pics/68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png new file mode 100644 index 00000000..39d7ec60 Binary files /dev/null and b/pics/68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png differ diff --git a/pics/688dacfe-1057-412f-b3a1-86abb5b0f914.png b/pics/688dacfe-1057-412f-b3a1-86abb5b0f914.png new file mode 100644 index 00000000..21fa725f Binary files /dev/null and b/pics/688dacfe-1057-412f-b3a1-86abb5b0f914.png differ diff --git a/pics/6c0f4afb-20ab-49fd-837d-8144f4e38bfd.png b/pics/6c0f4afb-20ab-49fd-837d-8144f4e38bfd.png new file mode 100644 index 00000000..d86b4635 Binary files /dev/null and b/pics/6c0f4afb-20ab-49fd-837d-8144f4e38bfd.png differ diff --git a/pics/6f4abf41-3728-4a6b-9b94-85eed7ca8163.png b/pics/6f4abf41-3728-4a6b-9b94-85eed7ca8163.png new file mode 100644 index 00000000..1fb8642f Binary files /dev/null and b/pics/6f4abf41-3728-4a6b-9b94-85eed7ca8163.png differ diff --git a/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png b/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png new file mode 100644 index 00000000..748980c4 Binary files /dev/null and b/pics/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png differ diff --git a/pics/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png b/pics/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png new file mode 100644 index 00000000..b636edf5 Binary files /dev/null and b/pics/7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png differ diff --git a/pics/8229e8e7-a183-4d29-94e6-e8d8537c6ce5.png b/pics/8229e8e7-a183-4d29-94e6-e8d8537c6ce5.png new file mode 100644 index 00000000..3aaa6323 Binary files /dev/null and b/pics/8229e8e7-a183-4d29-94e6-e8d8537c6ce5.png differ diff --git a/pics/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png b/pics/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png new file mode 100644 index 00000000..c6344ad2 Binary files /dev/null and b/pics/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png differ diff --git a/pics/89091427-7b2b-4923-aff6-44681319a8aa.jpg b/pics/89091427-7b2b-4923-aff6-44681319a8aa.jpg deleted file mode 100644 index d2cc88f7..00000000 Binary files a/pics/89091427-7b2b-4923-aff6-44681319a8aa.jpg and /dev/null differ diff --git a/pics/8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png b/pics/8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png new file mode 100644 index 00000000..2e53fbe3 Binary files /dev/null and b/pics/8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png differ diff --git a/pics/920c034c-c212-4f79-9ddb-84e4bb6cd088.png b/pics/920c034c-c212-4f79-9ddb-84e4bb6cd088.png new file mode 100644 index 00000000..ac17fd0c Binary files /dev/null and b/pics/920c034c-c212-4f79-9ddb-84e4bb6cd088.png differ diff --git a/pics/952afa9a-458b-44ce-bba9-463e60162945.png b/pics/952afa9a-458b-44ce-bba9-463e60162945.png new file mode 100644 index 00000000..db93c1ac Binary files /dev/null and b/pics/952afa9a-458b-44ce-bba9-463e60162945.png differ diff --git a/pics/992faced-afcf-414d-b801-9c16d6570fec.jpg b/pics/992faced-afcf-414d-b801-9c16d6570fec.jpg new file mode 100644 index 00000000..1a363f1e Binary files /dev/null and b/pics/992faced-afcf-414d-b801-9c16d6570fec.jpg differ diff --git a/pics/9d0a637c-6a8f-4f5a-99b9-fdcfa26793ff.png b/pics/9d0a637c-6a8f-4f5a-99b9-fdcfa26793ff.png new file mode 100644 index 00000000..34a8f664 Binary files /dev/null and b/pics/9d0a637c-6a8f-4f5a-99b9-fdcfa26793ff.png differ diff --git a/pics/CountdownLatch.png b/pics/CountdownLatch.png new file mode 100644 index 00000000..1a581bbf Binary files /dev/null and b/pics/CountdownLatch.png differ diff --git a/pics/CyclicBarrier.png b/pics/CyclicBarrier.png new file mode 100644 index 00000000..5fe5b88b Binary files /dev/null and b/pics/CyclicBarrier.png differ diff --git a/pics/GUID_Partition_Table_Scheme.svg.png b/pics/GUID_Partition_Table_Scheme.svg.png new file mode 100644 index 00000000..6638ae7a Binary files /dev/null and b/pics/GUID_Partition_Table_Scheme.svg.png differ diff --git a/pics/PPjwP.png b/pics/PPjwP.png new file mode 100644 index 00000000..80631505 Binary files /dev/null and b/pics/PPjwP.png differ diff --git a/pics/Semaphore.png b/pics/Semaphore.png new file mode 100644 index 00000000..6b8c30b1 Binary files /dev/null and b/pics/Semaphore.png differ diff --git a/pics/Technology-ComputerNetworking-Internet-ISPs.png b/pics/Technology-ComputerNetworking-Internet-ISPs.png new file mode 100644 index 00000000..72a30a7b Binary files /dev/null and b/pics/Technology-ComputerNetworking-Internet-ISPs.png differ diff --git a/pics/a3da4342-078b-43e2-b748-7e71bec50dc4.png b/pics/a3da4342-078b-43e2-b748-7e71bec50dc4.png new file mode 100644 index 00000000..03b37a61 Binary files /dev/null and b/pics/a3da4342-078b-43e2-b748-7e71bec50dc4.png differ diff --git a/pics/a46cf05d-e665-4937-a939-a3ab783bc8ee.png b/pics/a46cf05d-e665-4937-a939-a3ab783bc8ee.png deleted file mode 100644 index f8a0bb48..00000000 Binary files a/pics/a46cf05d-e665-4937-a939-a3ab783bc8ee.png and /dev/null differ diff --git a/pics/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png b/pics/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png new file mode 100644 index 00000000..461fffb0 Binary files /dev/null and b/pics/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png differ diff --git a/pics/a57a6fc8-c5e9-456c-80ff-a5139dda4b6e.png b/pics/a57a6fc8-c5e9-456c-80ff-a5139dda4b6e.png new file mode 100644 index 00000000..384e7aa6 Binary files /dev/null and b/pics/a57a6fc8-c5e9-456c-80ff-a5139dda4b6e.png differ diff --git a/pics/a5c25452-6fa5-49e7-9322-823077442775.jpg b/pics/a5c25452-6fa5-49e7-9322-823077442775.jpg deleted file mode 100644 index 49219388..00000000 Binary files a/pics/a5c25452-6fa5-49e7-9322-823077442775.jpg and /dev/null differ diff --git a/pics/ace830df-9919-48ca-91b5-60b193f593d2.png b/pics/ace830df-9919-48ca-91b5-60b193f593d2.png new file mode 100644 index 00000000..79efa287 Binary files /dev/null and b/pics/ace830df-9919-48ca-91b5-60b193f593d2.png differ diff --git a/pics/b15ed62e-b955-44ac-b5cb-6fa7a16c79b5.png b/pics/b15ed62e-b955-44ac-b5cb-6fa7a16c79b5.png deleted file mode 100644 index 2bb67d95..00000000 Binary files a/pics/b15ed62e-b955-44ac-b5cb-6fa7a16c79b5.png and /dev/null differ diff --git a/pics/bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg b/pics/bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg new file mode 100644 index 00000000..e4becc19 Binary files /dev/null and b/pics/bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg differ diff --git a/pics/c73aa08e-a987-43c9-92be-adea4a884c25.png b/pics/c73aa08e-a987-43c9-92be-adea4a884c25.png deleted file mode 100644 index 33c22c9d..00000000 Binary files a/pics/c73aa08e-a987-43c9-92be-adea4a884c25.png and /dev/null differ diff --git a/pics/c812c28a-1513-4a82-bfda-ab6a40981aa0.png b/pics/c812c28a-1513-4a82-bfda-ab6a40981aa0.png new file mode 100644 index 00000000..b1142bff Binary files /dev/null and b/pics/c812c28a-1513-4a82-bfda-ab6a40981aa0.png differ diff --git a/pics/c8d18ca9-0b09-441a-9a0c-fb063630d708.png b/pics/c8d18ca9-0b09-441a-9a0c-fb063630d708.png new file mode 100644 index 00000000..514ab054 Binary files /dev/null and b/pics/c8d18ca9-0b09-441a-9a0c-fb063630d708.png differ diff --git a/pics/cdbe1d12-5ad9-4acb-a717-bbc822c2acf3.png b/pics/cdbe1d12-5ad9-4acb-a717-bbc822c2acf3.png new file mode 100644 index 00000000..63792c74 Binary files /dev/null and b/pics/cdbe1d12-5ad9-4acb-a717-bbc822c2acf3.png differ diff --git a/pics/d0175e0c-859e-4991-b263-8378e52f7ee5.jpg b/pics/d0175e0c-859e-4991-b263-8378e52f7ee5.jpg deleted file mode 100644 index 5c7aa860..00000000 Binary files a/pics/d0175e0c-859e-4991-b263-8378e52f7ee5.jpg and /dev/null differ diff --git a/pics/d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg b/pics/d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg deleted file mode 100644 index bbf38872..00000000 Binary files a/pics/d1ab24fa-1a25-4804-aa91-513df55cbaa6.jpg and /dev/null differ diff --git a/pics/d3352e6a-483a-44f2-930e-28c1d677f9b9.png b/pics/d3352e6a-483a-44f2-930e-28c1d677f9b9.png deleted file mode 100644 index 0c3cf565..00000000 Binary files a/pics/d3352e6a-483a-44f2-930e-28c1d677f9b9.png and /dev/null differ diff --git a/pics/d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg b/pics/d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg deleted file mode 100644 index 9ccdace0..00000000 Binary files a/pics/d589eca6-c7cf-49c5-ac96-8e4ca0cccadd.jpg and /dev/null differ diff --git a/pics/d5c16be7-a1c0-4c8d-b6b9-5999cdc6f9b3.png b/pics/d5c16be7-a1c0-4c8d-b6b9-5999cdc6f9b3.png new file mode 100644 index 00000000..df5be529 Binary files /dev/null and b/pics/d5c16be7-a1c0-4c8d-b6b9-5999cdc6f9b3.png differ diff --git a/pics/d7c6c42d-a4d8-4b85-82fb-c21250bf5ca1.png b/pics/d7c6c42d-a4d8-4b85-82fb-c21250bf5ca1.png deleted file mode 100644 index 20ef25cf..00000000 Binary files a/pics/d7c6c42d-a4d8-4b85-82fb-c21250bf5ca1.png and /dev/null differ diff --git a/pics/dc752c5b-bb59-4616-bf9c-21276690a24d.png b/pics/dc752c5b-bb59-4616-bf9c-21276690a24d.png deleted file mode 100644 index 66e77b6d..00000000 Binary files a/pics/dc752c5b-bb59-4616-bf9c-21276690a24d.png and /dev/null differ diff --git a/pics/dd15a984-e977-4644-b127-708cddb8ca99.png b/pics/dd15a984-e977-4644-b127-708cddb8ca99.png new file mode 100644 index 00000000..4397e88e Binary files /dev/null and b/pics/dd15a984-e977-4644-b127-708cddb8ca99.png differ diff --git a/pics/dd782132-d830-4c55-9884-cfac0a541b8e.png b/pics/dd782132-d830-4c55-9884-cfac0a541b8e.png new file mode 100644 index 00000000..e338c1bb Binary files /dev/null and b/pics/dd782132-d830-4c55-9884-cfac0a541b8e.png differ diff --git a/pics/dda1608d-26e0-4f10-8327-a459969b150a.png b/pics/dda1608d-26e0-4f10-8327-a459969b150a.png new file mode 100644 index 00000000..7bab8f15 Binary files /dev/null and b/pics/dda1608d-26e0-4f10-8327-a459969b150a.png differ diff --git a/pics/e31abb94-9201-4e06-9902-61101b92f475.png b/pics/e31abb94-9201-4e06-9902-61101b92f475.png new file mode 100644 index 00000000..90833a5c Binary files /dev/null and b/pics/e31abb94-9201-4e06-9902-61101b92f475.png differ diff --git a/pics/eb4a7007-d437-4740-865d-672973effe25.png b/pics/eb4a7007-d437-4740-865d-672973effe25.png new file mode 100644 index 00000000..e2a0faab Binary files /dev/null and b/pics/eb4a7007-d437-4740-865d-672973effe25.png differ diff --git a/pics/ed7b96ac-6428-4bd5-9986-674c54c2a959.png b/pics/ed7b96ac-6428-4bd5-9986-674c54c2a959.png new file mode 100644 index 00000000..78b3cace Binary files /dev/null and b/pics/ed7b96ac-6428-4bd5-9986-674c54c2a959.png differ diff --git a/pics/eebdeb57-8efb-4848-9bb6-97512990897c.jpg b/pics/eebdeb57-8efb-4848-9bb6-97512990897c.jpg new file mode 100644 index 00000000..5632fdca Binary files /dev/null and b/pics/eebdeb57-8efb-4848-9bb6-97512990897c.jpg differ diff --git a/pics/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png b/pics/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png new file mode 100644 index 00000000..f6256867 Binary files /dev/null and b/pics/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png differ diff --git a/pics/f47f9729-29ed-4c17-9924-76139342fac7.png b/pics/f47f9729-29ed-4c17-9924-76139342fac7.png deleted file mode 100644 index d67672f7..00000000 Binary files a/pics/f47f9729-29ed-4c17-9924-76139342fac7.png and /dev/null differ diff --git a/pics/f4cdda3e-324c-49b5-8c14-08a3db634b29.png b/pics/f4cdda3e-324c-49b5-8c14-08a3db634b29.png new file mode 100644 index 00000000..60fc81bb Binary files /dev/null and b/pics/f4cdda3e-324c-49b5-8c14-08a3db634b29.png differ diff --git a/pics/f5757d09-88e7-4bbd-8cfb-cecf55604854.png b/pics/f5757d09-88e7-4bbd-8cfb-cecf55604854.png new file mode 100644 index 00000000..1664247b Binary files /dev/null and b/pics/f5757d09-88e7-4bbd-8cfb-cecf55604854.png differ diff --git a/pics/fa568fac-ac58-48dd-a9bb-23b3065bf2dc.png b/pics/fa568fac-ac58-48dd-a9bb-23b3065bf2dc.png new file mode 100644 index 00000000..736a2861 Binary files /dev/null and b/pics/fa568fac-ac58-48dd-a9bb-23b3065bf2dc.png differ diff --git a/pics/fbe54203-c005-48f0-8883-b05e564a3173.png b/pics/fbe54203-c005-48f0-8883-b05e564a3173.png new file mode 100644 index 00000000..dee6a88d Binary files /dev/null and b/pics/fbe54203-c005-48f0-8883-b05e564a3173.png differ diff --git a/pics/fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg b/pics/fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg deleted file mode 100644 index f9b95f53..00000000 Binary files a/pics/fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg and /dev/null differ diff --git a/pics/ff0c019c-6461-467d-a266-0455341fd4f4.png b/pics/ff0c019c-6461-467d-a266-0455341fd4f4.png new file mode 100644 index 00000000..658867d2 Binary files /dev/null and b/pics/ff0c019c-6461-467d-a266-0455341fd4f4.png differ diff --git a/pics/network-of-networks.gif b/pics/network-of-networks.gif new file mode 100644 index 00000000..7473f913 Binary files /dev/null and b/pics/network-of-networks.gif differ