From 921213d5c64fc867b3ceead2ba5c3137b1fff943 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 1 May 2018 16:10:20 +0000 Subject: [PATCH 01/27] Create README.md --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd4fadd --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +| [数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践) | +
+## 数据结构和算法 +## 操作系统 +## 网络 +## 数据库 +## Java基础 +## Java进阶 +## web和Spring +## 分布式 +## Hadoop +## 工具 +## 编码实践 + +## 后记 +**关于仓库** +本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 +**关于贡献** +笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以发表一个 Issue,表明您想要添加的内容,笔者会及时查看。 +您也可以在 Issues 中发表关于改进本仓库的建议。 +**关于排版** +笔记排版参考@CYC2018 +**关于转载** +本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 +**鸣谢** +[CyC2018](https://github.com/CyC2018) + From 9dcbb9eb24b0bfacbf7f3cab36a743593c558b82 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 1 May 2018 16:10:52 +0000 Subject: [PATCH 02/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd4fadd..9a81fc5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -| [数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践) | +[数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践)
## 数据结构和算法 ## 操作系统 From 9fa55dfe65870c82d6b29f2c655a92181b4d1ba4 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Wed, 2 May 2018 01:45:26 +0000 Subject: [PATCH 03/27] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9a81fc5..fb72617 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,17 @@ ## 后记 **关于仓库** 本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 + **关于贡献** 笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以发表一个 Issue,表明您想要添加的内容,笔者会及时查看。 您也可以在 Issues 中发表关于改进本仓库的建议。 + **关于排版** 笔记排版参考@CYC2018 + **关于转载** 本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 + **鸣谢** [CyC2018](https://github.com/CyC2018) From 318edbfd4847a1b395115ff524349c4fc648b37e Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 26 Jun 2018 18:26:49 +0800 Subject: [PATCH 04/27] Update README.md --- README.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fb72617..b47b340 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,37 @@ [数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践)
## 数据结构和算法 + ## 操作系统 -## 网络 -## 数据库 -## Java基础 -## Java进阶 -## web和Spring -## 分布式 -## Hadoop -## 工具 -## 编码实践 + +## 计算机网络 + +## Mysql和Redis + +## Java核心技术 + +## Java并发编程 + +## 深入理解JVM + +## Java网络编程与NIO + +## JavaWeb技术世界 + +## Spring与SpirngMVC + +## 分布式系统理论 + +## 分布式技术与实践 + +## Hadoop生态 + ## 后记 **关于仓库** 本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 -**关于贡献** -笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以发表一个 Issue,表明您想要添加的内容,笔者会及时查看。 -您也可以在 Issues 中发表关于改进本仓库的建议。 - -**关于排版** -笔记排版参考@CYC2018 - **关于转载** 本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 -**鸣谢** -[CyC2018](https://github.com/CyC2018) From 39c20d13f8c7c4de94591ecff0aaf96413cbd33f Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 26 Jun 2018 18:33:30 +0800 Subject: [PATCH 05/27] Update README.md --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b47b340..0a4f506 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,17 @@ -[数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践) +[数据结构和算法](#算法) +[操作系统](#操作系统) +[计算机网络](#计算机网络) +[Mysql和Redis](#Mysql和Redis) +[Java核心技术](#Java核心技术) +[Java并发技术](#Java并发技术) +[深入理解JVM](#深入理解JVM) +[Java网络编程与NIO](#Java网络编程与NIO) +[JavaWeb技术世界](#JavaWeb技术世界) +[Spring与SpirngMVC](#Spring与SpirngMVC) +[分布式系统理论](#分布式系统理论) +[分布式技术与实践](#分布式技术与实践) +[Hadoop生态](#Hadoop生态) +
## 数据结构和算法 @@ -10,7 +23,7 @@ ## Java核心技术 -## Java并发编程 +## Java并发技术 ## 深入理解JVM From de37fa024c0ab227f6c025ac37bfb95ded8f7396 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 26 Jun 2018 18:34:24 +0800 Subject: [PATCH 06/27] Update README.md --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a4f506..3baa901 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,27 @@ -[数据结构和算法](#算法) +[数据结构和算法](#数据结构和算法) + [操作系统](#操作系统) + [计算机网络](#计算机网络) + [Mysql和Redis](#Mysql和Redis) + [Java核心技术](#Java核心技术) + [Java并发技术](#Java并发技术) + [深入理解JVM](#深入理解JVM) + [Java网络编程与NIO](#Java网络编程与NIO) + [JavaWeb技术世界](#JavaWeb技术世界) + [Spring与SpirngMVC](#Spring与SpirngMVC) + [分布式系统理论](#分布式系统理论) + [分布式技术与实践](#分布式技术与实践) + [Hadoop生态](#Hadoop生态)
@@ -39,8 +51,8 @@ ## Hadoop生态 - ## 后记 + **关于仓库** 本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 From 64f93ab4603794e01039ff410e323791ad789605 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 26 Jun 2018 18:34:49 +0800 Subject: [PATCH 07/27] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3baa901..fe93b5d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ [Hadoop生态](#Hadoop生态)
+ ## 数据结构和算法 ## 操作系统 From e55a2e8f3cf36ee81e95f5ff1c74d6f0c5b08b63 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Sun, 8 Jul 2018 21:34:38 +0800 Subject: [PATCH 08/27] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index fe93b5d..66e34aa 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ + +| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | +| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| +| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) | + +## 算法 :pencil2: + [数据结构和算法](#数据结构和算法) [操作系统](#操作系统) From e99c41adcd00e589298cbd974f6d51ab15b884b5 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Sun, 8 Jul 2018 21:48:38 +0800 Subject: [PATCH 09/27] Update README.md --- README.md | 56 ++++++++++--------------------------------------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 66e34aa..0f9929a 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,28 @@ | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| -| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) | +| 数据结构和算法[:pencil2:](#数据结构和算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| 后记[:memo:](#后记-memo) | -## 算法 :pencil2: +## 数据结构和算法 :pencil2: -[数据结构和算法](#数据结构和算法) +## 操作系统 :computer: -[操作系统](#操作系统) +## 网络 :cloud: -[计算机网络](#计算机网络) +## 数据库 :floppy_disk: -[Mysql和Redis](#Mysql和Redis) +## Java :couple: -[Java核心技术](#Java核心技术) +## JavaWeb :coffee: -[Java并发技术](#Java并发技术) +## 分布式 :sweat_drops: -[深入理解JVM](#深入理解JVM) +## 设计模式 :hammer: -[Java网络编程与NIO](#Java网络编程与NIO) - -[JavaWeb技术世界](#JavaWeb技术世界) - -[Spring与SpirngMVC](#Spring与SpirngMVC) - -[分布式系统理论](#分布式系统理论) - -[分布式技术与实践](#分布式技术与实践) - -[Hadoop生态](#Hadoop生态) +## Hadoop :speak_no_evil:
-## 数据结构和算法 - -## 操作系统 - -## 计算机网络 - -## Mysql和Redis - -## Java核心技术 - -## Java并发技术 - -## 深入理解JVM - -## Java网络编程与NIO - -## JavaWeb技术世界 - -## Spring与SpirngMVC - -## 分布式系统理论 - -## 分布式技术与实践 - -## Hadoop生态 - ## 后记 **关于仓库** From 1fb6944979b6941d8b449a58d71ba433867cbd25 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Sun, 8 Jul 2018 21:50:11 +0800 Subject: [PATCH 10/27] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f9929a..0d1dff1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| -| 数据结构和算法[:pencil2:](#数据结构和算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| 后记[:memo:](#后记-memo) | +| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| 后记[:memo:](#后记-memo) | -## 数据结构和算法 :pencil2: +## 算法 :pencil2: ## 操作系统 :computer: From 7d19c1b1fc3cec1d0b6436fd0cc1c1efcbc84d6f Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Sun, 8 Jul 2018 21:52:23 +0800 Subject: [PATCH 11/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d1dff1..be7450b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | -| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| +| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :---------: | :---------:| :------:| | 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| 后记[:memo:](#后记-memo) | ## 算法 :pencil2: From 20de38c9425716bbbe1ac44557de04f135cd87ac Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Sun, 8 Jul 2018 21:55:31 +0800 Subject: [PATCH 12/27] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index be7450b..250a91f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ - -| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ | -| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :---------: | :---------:| :------:| -| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| 后记[:memo:](#后记-memo) | +| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | +| :------: | :---------: | :-------: | :---------: | :---: | :---------:| :---------: | :---------: | :---------:| +| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| ## 算法 :pencil2: From 5f614f3d7b42ba9bdcb0b087dd60ad1064f70680 Mon Sep 17 00:00:00 2001 From: 724888 <362294931@qq.com> Date: Sun, 8 Jul 2018 22:35:38 +0800 Subject: [PATCH 13/27] =?UTF-8?q?=E6=8A=80=E6=9C=AF=E6=80=BB=E7=BB=93?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...37\346\200\201\346\200\273\347\273\223.md" | 248 +++++++++ "md/JVM\346\200\273\347\273\223.md" | 135 +++++ ...00\346\234\257\346\200\273\347\273\223.md" | 116 +++++ ...66\345\217\221\346\200\273\347\273\223.md" | 206 ++++++++ ...00\346\234\257\346\200\273\347\273\223.md" | 136 +++++ ...344\270\216NIO\346\200\273\347\273\223.md" | 213 ++++++++ ...10\347\261\273\346\200\273\347\273\223.md" | 108 ++++ ...36\350\267\265\346\200\273\347\273\223.md" | 345 +++++++++++++ md/README.md | 27 + ...36\350\267\265\346\200\273\347\273\223.md" | 429 ++++++++++++++++ ...43\346\236\220\346\200\273\347\273\223.md" | 87 ++++ ...36\350\267\265\346\200\273\347\273\223.md" | 474 ++++++++++++++++++ ...06\350\256\272\346\200\273\347\273\223.md" | 355 +++++++++++++ 13 files changed, 2879 insertions(+) create mode 100644 "md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" create mode 100644 "md/JVM\346\200\273\347\273\223.md" create mode 100644 "md/JavaWeb\346\212\200\346\234\257\346\200\273\347\273\223.md" create mode 100644 "md/Java\345\271\266\345\217\221\346\200\273\347\273\223.md" create mode 100644 "md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" create mode 100644 "md/Java\347\275\221\347\273\234\344\270\216NIO\346\200\273\347\273\223.md" create mode 100644 "md/Java\351\233\206\345\220\210\347\261\273\346\200\273\347\273\223.md" create mode 100644 "md/Mysql\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" create mode 100644 md/README.md create mode 100644 "md/Redis\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" create mode 100644 "md/Spring\344\270\216SpringMVC\346\272\220\347\240\201\350\247\243\346\236\220\346\200\273\347\273\223.md" create mode 100644 "md/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\256\236\350\267\265\346\200\273\347\273\223.md" create mode 100644 "md/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272\346\200\273\347\273\223.md" diff --git "a/md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" "b/md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" new file mode 100644 index 0000000..b71e6eb --- /dev/null +++ "b/md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" @@ -0,0 +1,248 @@ +--- +title: Hadoop生态学习总结 +date: 2018-07-08 22:15:53 +tags: + - Hadoop +categories: + - 后端 + - 技术总结 +--- +#Hadoop生态基础学习总结 +这篇总结主要是基于我之前Hadoop生态基础系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 + +#更多详细内容可以查看我的专栏文章:Hadoop生态学习 +#https://blog.csdn.net/a724888/article/category/7779280 +# Hadoop生态 + +## hdfs + +### 架构 +hdfs是一个分布式文件系统。底层的存储采用廉价的磁盘阵列RAID,由于可以并发读写所以效率很高。 + +基本架构是一个namenode和多个dataNode。node的意思是节点,一般指主机,也可以是虚拟机。 + +每个文件都会有两个副本存放在datanode中。 + +### 读写 + +客户端写入文件时,先把请求发送到namenode,namenode会返回datanode的服务地址,接着客户端去访问datanode,进行文件写入,然后通知namenode,namenode接收到写入完成的消息以后,会另外选两个datanode存放冗余副本。 + +读取文件时,从namenode获取一个datanode的地址,然后自己去读取即可。 + +当一个文件的副本不足两份时,namenode自动会完成副本复制。并且,由于datanode一般会放在各个机架。namenode一般会把副本一个放在同一机架,一个放在其他机架,防止某个机架出问题导致整个文件读写不可用。 + +### 高可用 + +namenode节点是单点,所以宕机了就没救了,所以我们可以使用zookeeper来保证namenode的高可用。可以使用zookeeper选主来实现故障切换,namenode先注册一个节点在zk上,表示自己是主,宕机时zk会通知备份节点进行切换。 + +Hadoop2.0中加入了hdfs namenode高可用的方案,也叫HDFS HA。namenode和一个备份节点绑定在一起,并且通过一个共享数据区域进行数据同步。同时支持故障切换。 + +## MapReduce + +### 架构和流程 + +MapReduce是基于Hadoop集群的分布式计算方案。一般先编写map函数进行数据分片,然后通过shuffle进行相同分片的整合,最后通过reduce把所有的数据结果进行整理。 + +具体来说,用户提交一个MapReduce程序给namenode节点,namenode节点启动一个jobtracker进行子任务的调度和监控,然后派发每个子任务tasktracker到datanode进行任务执行,由于数据分布在各个节点,每个tasktracker只需要执行自己的那一部分即可。最后再将结果汇总给tasktracker。 + +### wordcount + +首先是一个文本文件:hi hello good hello hi hi。 +三个节点,则进行三次map。hi hello,good hello,hi hi分别由三个节点处理。结果分别是hi 1 hello 1,good 1 hello 1,hi 1,hi 1。 +shuffle时进行combine操作,得到hi 1,hello 1,good 1 hello 1,hi 2。最终reduce的结果是hi 3 hello 2 good 1. + +## hive + +hive是一个基于hdfs文件系统的数据仓库。可以通过hive sql语句执行对hdfs上文件的数据查询。原理是hive把hdfs上的数据文件看成一张张数据表,把表结构信息存在关系数据库如mysql中。然后执行sql时通过对表结构的解析再去hdfs上查询真正的数据,最后也会以结构化的形式返回。 + +## hbase + +### 简介 + +hbase是基于列的数据库。 + +他与传统关系数据库有很大不同。 + +首先在表结构上,hbase使用rowkey行键作为唯一主键,通过行键唯一确定一行数据。 + +同时,hbase使用列族的概念,每个表都有固定的列族,每一行的数据的列族都一样,但是每一行所在列族的实际列都可以不一样。 +比如列族是info,列可以是name age,也可以是sex address等。也就是说具体列可以在插入数据时再进行确认。 + +并且,hbase的每一行数据还可以有多个版本,通过时间戳来表示不同的数据版本。 +### 存储 + +一般情况下hbase使用hdfs作为底层存储,所以hdfs提供了数据的可靠性以及并发读写的高效率。 + +hbase一个表的 每n行数据会存在一个region中,并且,对于列来说,每一个列族都会用一个region来存储,假设有m个列族,那么就会有n * m个region需要存储在hdfs上。 + +同时hbase使用regionserver来管理这些region,他们可能存在不同的datanode里,所以通过regionserver可以找出每一个region的位置。 + +hbase使用zookeeper来保证regionserver的高可用,会自动进行故障切换。 + +## zk + +zk在Hadoop的作用有几个,通过选主等机制保证主节点高可用。 + +使用zk进行配置资源的统一管理,保证服务器节点无状态,所有服务信息直接从zk获取即可。 + +使用zookeeper进行节点间的通信等,也可以使用zk的目录顺序节点实现分布式锁,以及服务器选主。不仅在Hadoop中,zk在分布式系统中总能有用武之地。 + +zookeeper本身的部署方式就是一个集群,一个master和多个slave。 + +使用zab协议保证一致性和高可用。 + +zab协议实现原理: + +1 使用两段式提交的方式确保一个协议需要半数以上节点同意以后再进行广播执行。 + +2 使用基于机器编号加时间戳的id来表示每个事务,通过这个方式当初始选举或者主节点宕机时进行一轮选主,每个节点优先选择自己当主节点,在选举过程中节点优先采纳比较新的事务,将自己的选票更新,然后反馈个其他机器,最后当一个机器获得超过半数选票时当选为master。 + +3选主结束以后,主节点与slave进行主从同步,保证数据一致性,然后对外提供服务,并且写入只能通过master而读取可以通过任意一台机器。 + +## sqoop +将hive表中的内容导入到MySQL数据库,也可以将MySQL中的数据导入hive中。 + +## yarn + +没有yarn之前,hdfs使用jobtracker和tasktracker来执行和跟踪任务,jobtracker的任务太重,又要执行又要监控还要获取结果。 +并且不同机器的资源情况没有被考虑在内。 + +yarn是一个资源调度系统。提供applicationmaster对一个调度任务进行封装,然后有一个resourcemanager专门负责各节点资源的管理和监控。同时nodemanager则运行每个节点中用于监控节点状态和向rm汇报。还有一个container则是对节点资源的一个抽象,applicationmaster任务将由节点上的一个container进行执行。rm会将他调度到最合适的机器上。 +## kafka + +架构 + +> kafka是一个分布式的消息队列。 +> +> 它组成一般包括kafka broker,每个broker中有多个的partition作为存储消息的队列。 +> +> 并且向上提供服务时抽象为一个topic,我们访问topic时实际上执行的是对partition的写入和读取操作。 + + +读写和高可用 + +> partition支持顺序写入,效率比较高,并且支持零拷贝机制,通过内存映射磁盘mmap的方式,写入partition的数据顺序写入到映射的磁盘中,比传统的IO要快。 +> +> 由于partition可能会宕机,所以一般也要支持partition的备份,1个broker ,master通常会有多个 +> broker slave,是主从关系,通过zookeeper进行选主和故障切换。 +> +> 当数据写入队列时,一般也会通过日志文件的方式进行数据备份,会把broker中的partition被分在各个slave中以便于均匀分布和恢复。 + +生产者和消费者 + +> 生产者消费者需要访问kafka的队列时,如果是写入,直接向zk发送请求,一般是向一个topic写入消息,broker会自动分配partition进行写入。然后zk会告诉生产者写入的partition所在的broker地址,然后进行写入。 +> +> 如果是读取的话,也是通过zk获取partition所在位置,然后通过给定的offset进行读取,读取完后更新offset。 +> +> 由于kafka的partition支持顺序读写。所以保证一个partition中的读取和写入时是顺序的,但是如果是多个partition则不保证顺序。 +> +> 正常情况下kafka使用topic来实现消息点对点发送,并且每个consumer都要在一个consumer group中,而且comsumer group中每次只能有一个消费者能接受对应topic的消息。因为为了实现订阅也就是一对多发送,我们让每个consumer在一个单独的group,于是每个consumer都可以接受到该消息。 + + +## flume + +flume用于数据的收集和分发,flume可以监听端口的数据流入,监视文件的变动以及各种数据形式的数据流入,然后再把数据重新转发到其他需要数据的节点或存储中。 + +1、Flume的概念 + +flume是分布式的日志收集系统,它将各个服务器中的数据收集起来并送到指定的地方去,比如说送到图中的HDFS,简单来说flume就是收集日志的。 + +2、Event的概念 +在这里有必要先介绍一下flume中event的相关概念:flume的核心是把数据从数据源(source)收集过来,在将收集到的数据送到指定的目的地(sink)。为了保证输送的过程一定成功,在送到目的地(sink)之前,会先缓存数据(channel),待数据真正到达目的地(sink)后,flume在删除自己缓存的数据。 + +在整个数据的传输的过程中,流动的是event,即事务保证是在event级别进行的。那么什么是event呢?—–event将传输的数据进行封装,是flume传输数据的基本单位,如果是文本文件,通常是一行记录,event也是事务的基本单位。event从source,流向channel,再到sink,本身为一个字节数组,并可携带headers(头信息)信息。event代表着一个数据的最小完整单元,从外部数据源来,向外部的目的地去。 + +flume使用 +## ambari +ambari就是一个Hadoop的Web应用。 + +## spark +spark和MapReduce不同的地方就是,把计算过程放在内存中运行。 + +spark提出了抽象的RDD分布式内存模型,把每一步的计算操作转换成一个RDD结构,然后形成一个RDD连接而成的有向图。 + +比如data.map().filter().reduce(); +程序提交到master以后,会解析成多个RDD,并且形成一个有向图,然后spark再根据这些RD结构在内存中执行对应的操作。当然这个拓扑结构会被拆分为各个子任务分发到各个spark节点上,然后计算完以后再形成下一个rdd。最后汇总结果即可。 + +由于是在内存中对数据进行操作,省去了不必要的IO操作,,不需要像Mapreduce一样还得先去hdfs读取文件再完成计算。 + + + + +## storm + +在运行一个Storm任务之前,需要了解一些概念: + +1. Topologies +2. Streams +3. Spouts +4. Bolts +5. Stream groupings +6. Reliability +7. Tasks +8. Workers +9. Configuration + +Storm集群和Hadoop集群表面上看很类似。但是Hadoop上运行的是MapReduce jobs,而在Storm上运行的是拓扑(topology),这两者之间是非常不一样的。一个关键的区别是: 一个MapReduce job最终会结束, 而一个topology永远会运行(除非你手动kill掉)。 + +在Storm的集群里面有两种节点: 控制节点(master node)和工作节点(worker node)。控制节点上面运行一个叫Nimbus后台程序,它的作用类似Hadoop里面的JobTracker。Nimbus负责在集群里面分发代码,分配计算任务给机器, 并且监控状态。 + +每一个工作节点上面运行一个叫做Supervisor的节点。Supervisor会监听分配给它那台机器的工作,根据需要启动/关闭工作进程。每一个工作进程执行一个topology的一个子集;一个运行的topology由运行在很多机器上的很多工作进程组成。 + +![](https://img-blog.csdn.net/20160107221357281?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center) + +Nimbus和Supervisor之间的所有协调工作都是通过Zookeeper集群完成。另外,Nimbus进程和Supervisor进程都是快速失败(fail-fast)和无状态的。所有的状态要么在zookeeper里面, 要么在本地磁盘上。这也就意味着你可以用kill -9来杀死Nimbus和Supervisor进程, 然后再重启它们,就好像什么都没有发生过。这个设计使得Storm异常的稳定。 + +storm比起spark它的实时性能更高更强,storm可以做到亚秒级别的数据输入分析。而spark的方式是通过秒级的数据切分,来形成spark rdd数据集,然后再按照DAG有向图进行执行的。 + +storm则不然。 + +一:介绍Storm设计模型 + +1.Topology + +  Storm对任务的抽象,其实 就是将实时数据分析任务 分解为 不同的阶段     + +  点: 计算组件 Spout Bolt + +  边: 数据流向 数据从上一个组件流向下一个组件 带方向 + + + +2.tuple + +  Storm每条记录 封装成一个tuple + +  其实就是一些keyvalue对按顺序排列 + +  方便组件获取数据 + + + +3.Spout + +  数据采集器 + +  源源不断的日志记录 如何被topology接收进行处理? + +  Spout负责从数据源上获取数据,简单处理 封装成tuple向后面的bolt发射 + + + +4.Bolt + +  数据处理器 +   +二:开发wordcount案例 + +1.书写整个大纲的点线图 + +  ![](https://images2015.cnblogs.com/blog/1027015/201701/1027015-20170126164121691-1448959654.png) +   +topology就是一个拓扑图,类似于spark中的dag有向图,只不过storm执行的流式的数据,比dag执行更加具有实时性。 + +topology包含了spout和bolt。 +spout负责获取数据,并且将数据发送给bolt,这个过程就是把任务派发到多个节点,bolt则负责对数据进行处理,比如splitbolt负责把每个单词提取出来,countbolt负责单词数量的统计,最后的printbolt将每个结果集tuple打印出来。 + +这就形成了一个完整的流程。 + diff --git "a/md/JVM\346\200\273\347\273\223.md" "b/md/JVM\346\200\273\347\273\223.md" new file mode 100644 index 0000000..1781f33 --- /dev/null +++ "b/md/JVM\346\200\273\347\273\223.md" @@ -0,0 +1,135 @@ +--- +title: JVM原理学习总结 +date: 2018-07-08 22:09:47 +tags: + - JVM +categories: + - 后端 + - 技术总结 +--- +#JVM原理学习总结 + +这篇总结主要是基于我之前JVM系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 + +#更多详细内容可以查看我的专栏文章:深入理解JVM虚拟机 +https://blog.csdn.net/column/details/21960.html + +## JVM介绍和源码 + +首先JVM是一个虚拟机,当你安装了jre,它就包含了jvm环境。JVM有自己的内存结构,字节码执行引擎,因此class字节码才能在jvm上运行,除了Java以外,Scala,groovy等语言也可以编译成字节码而后在jvm中运行。JVM是用c开发的。 + +## JVM内存模型 + +内存模型老生常谈了,主要就是线程共享的堆区,方法区,本地方法栈。还有线程私有的虚拟机栈和程序计数器。 + +堆区存放所有对象,每个对象有一个地址,Java类jvm初始化时加载到方法区,而后会在堆区中生成一个Class对象,来负责这个类所有实例的实例化。 + + + +栈区存放的是栈帧结构,栈帧是一段内存空间,包括参数列表,返回地址,局部变量表等,局部变量表由一堆slot组成,slot的大小固定,根据变量的数据类型决定需要用到几个slot。 + +方法区存放类的元数据,将原来的字面量转换成引用,当然,方法区也提供常量池,常量池存放-128到127的数字类型的包装类。 +字符串常量池则会存放使用intern的字符串变量。 +## JVM OOM和内存泄漏 + +这里指的是oom和内存泄漏这类错误。 + +oom一般分为三种,堆区内存溢出,栈区内存溢出以及方法区内存溢出。 + +堆内存溢出主要原因是创建了太多对象,比如一个集合类死循环添加一个数,此时设置jvm参数使堆内存最大值为10m,一会就会报oom异常。 + +栈内存溢出主要与栈空间和线程有关,因为栈是线程私有的,如果创建太多线程,内存值超过栈空间上限,也会报oom。 + +方法区内存溢出主要是由于动态加载类的数量太多,或者是不断创建一个动态代理,用不了多久方法区内存也会溢出,会报oom,这里在1.7之前会报permgem oom,1.8则会报meta space oom,这是因为1.8中删除了堆中的永久代,转而使用元数据区。 + +内存泄漏一般是因为对象被引用无法回收,比如一个集合中存着很多对象,可能你在外部代码把对象的引用置空了,但是由于对象还被集合给引用着,所以无法被回收,导致内存泄漏。测试也很简单,就在集合里添加对象,添加完以后把引用置空,循环操作,一会就会出现oom异常,原因是内存泄漏太多了,导致没有空间分配新的对象。 + +## 常见调试工具 + +命令行工具有jstack jstat jmap 等,jstack可以跟踪线程的调用堆栈,以便追踪错误原因。 + +jstat可以检查jvm的内存使用情况,gc情况以及线程状态等。 + +jmap用于把堆栈快照转储到文件系统,然后可以用其他工具去排查。 + +visualvm是一款很不错的gui调试工具,可以远程登录主机以便访问其jvm的状态并进行监控。 + +## class文件结构 + +class文件结构比较复杂,首先jvm定义了一个class文件的规则,并且让jvm按照这个规则去验证与读取。 + +开头是一串魔数,然后接下来会有各种不同长度的数据,通过class的规则去读取这些数据,jvm就可以识别其内容,最后将其加载到方法区。 + +## JVM的类加载机制 + +jvm的类加载顺序是bootstrap类加载器,extclassloader加载器,最后是appclassloader用户加载器,分别加载的是jdk/bin ,jdk/ext以及用户定义的类目录下的类(一般通过ide指定),一般核心类都由bootstrap和ext加载器来加载,appclassloader用于加载自己写的类。 + +双亲委派模型,加载一个类时,首先获取当前类加载器,先找到最高层的类加载器bootstrap让他尝试加载,他如果加载不了再让ext加载器去加载,如果他也加载不了再让appclassloader去加载。这样的话,确保一个类型只会被加载一次,并且以高层类加载器为准,防止某些类与核心类重复,产生错误。 + +## defineclass findclass和loadclass + +类加载classloader中有两个方法loadclass和findclass,loadclass遵从双亲委派模型,先调用父类加载的loadclass,如果父类和自己都无法加载该类,则会去调用findclass方法,而findclass默认实现为空,如果要自定义类加载方式,则可以重写findclass方法。 + +常见使用defineclass的情况是从网络或者文件读取字节码,然后通过defineclass将其定义成一个类,并且返回一个Class对象,说明此时类已经加载到方法区了。当然1.8以前实现方法区的是永久代,1.8以后则是元空间了。 + +## JVM虚拟机字节码执行引擎 + +jvm通过字节码执行引擎来执行class代码,他是一个栈式执行引擎。这部分内容比较高深,在这里就不献丑了。 + +## 编译期优化和运行期优化 + +编译期优化主要有几种 + +1 泛型的擦除,使得泛型在编译时变成了实际类型,也叫伪泛型。 + +2 自动拆箱装箱,foreach循环自动变成迭代器实现的for循环。 + +3 条件编译,比如if(true)直接可得。 + +运行期优化主要有几种 + +1 JIT即时编译 + +Java既是编译语言也是解释语言,因为需要编译代码生成字节码,而后通过解释器解释执行。 + +但是,有些代码由于经常被使用而成为热点代码,每次都编译太过费时费力,干脆直接把他编译成本地代码,这种方式叫做JIT即时编译处理,所以这部分代码可以直接在本地运行而不需要通过jvm的执行引擎。 + +2 公共表达式擦除,就是一个式子在后面如果没有被修改,在后面调用时就会被直接替换成数值。 + +3 数组边界擦除,方法内联,比较偏,意义不大。 + +4 逃逸分析,用于分析一个对象的作用范围,如果只局限在方法中被访问,则说明不会逃逸出方法,这样的话他就是线程安全的,不需要进行并发加锁。 + +1 + +## JVM的垃圾回收 + +1 GC算法:停止复制,存活对象少时适用,缺点是需要两倍空间。标记清除,存活对象多时适用,但是容易产生随便。标记整理,存活对象少时适用,需要移动对象较多。 + +2 GC分区,一般GC发生在堆区,堆区可分为年轻代,老年代,以前有永久代,现在没有了。 + +年轻代分为eden和survior,新对象分配在eden,当年轻代满时触发minor gc,存活对象移至survivor区,然后两个区互换,等待下一场gc, +当对象存活的阈值达到设定值时进入老年代,大对象也会直接进入老年代。 + +老年代空间较大,当老年代空间不足以存放年轻代过来的对象时,开始进行full gc。同时整理年轻代和老年代。 +一般年轻代使用停止复制,老年代使用标记清除。 + +3 垃圾收集器 + +serial串行 + +parallel并行 + +它们都有年轻代与老年代的不同实现。 + +然后是scanvage收集器,注重吞吐量,可以自己设置,不过不注重延迟。 + +cms垃圾收集器,注重延迟的缩短和控制,并且收集线程和系统线程可以并发。 + +cms收集步骤主要是,初次标记gc root,然后停顿进行并发标记,而后处理改变后的标记,最后停顿进行并发清除。 + +g1收集器和cms的收集方式类似,但是g1将堆内存划分成了大小相同的小块区域,并且将垃圾集中到一个区域,存活对象集中到另一个区域,然后进行收集,防止产生碎片,同时使分配方式更灵活,它还支持根据对象变化预测停顿时间,从而更好地帮用户解决延迟等问题。 + +## JVM的锁优化 + +在Java并发中讲述了synchronized重量级锁以及锁优化的方法,包括轻量级锁,偏向锁,自旋锁等。详细内容可以参考我的专栏:Java并发技术指南 \ No newline at end of file diff --git "a/md/JavaWeb\346\212\200\346\234\257\346\200\273\347\273\223.md" "b/md/JavaWeb\346\212\200\346\234\257\346\200\273\347\273\223.md" new file mode 100644 index 0000000..ff93d34 --- /dev/null +++ "b/md/JavaWeb\346\212\200\346\234\257\346\200\273\347\273\223.md" @@ -0,0 +1,116 @@ +--- +title: JavaWeb技术总结 +date: 2018-07-08 22:13:33 +tags: + - JavaWeb +categories: + - 后端 + - 技术总结 +--- +# Java Web技术技术总结 + + +这篇总结主要是基于我之前两个系列的文章而来。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 + +更多详细内容可以查看我的专栏文章: +#JavaWeb技术世界 +# https://blog.csdn.net/column/details/21850.html +#Spring与SpringMVC源码解析 +#https://blog.csdn.net/column/details/21851.html +## Servlet及相关类 + +servlet是一个接口,它的实现类有GenericServlet,而httpservlet是GenericServlet的一个子类,一般我们都会使用这个类。 + +servletconfig是用于保存servlet配置信息的数据结构,而servletcontext则负责保持servlet的上下文,web应用启动时加载web.xml信息于servletconfig中。 + +## Jsp和ViewResolver + +jsp页面需要编译成class文件并通过tomcat的类加载器进行加载,形成servlet实例,请求到来时实际上执行的是servlet代码,然后最终再通过viewresolver渲染成页面。 + +## filter,listener + +filter是过滤器,也需要在web.xml中配置,是责任链式的调用,在servlet执行service方法前执行。 +listener则是监听器,由于容器组件都实现了lifecycle接口,所以可以在组件上添加监听器来控制生命周期。 + +## web.xml + +web.xml用来配置servlet和servlet的配置信息,listener和filter。也可以配置静态文件的目录等。 + +## war包 + +waWAR包 +WAR(Web Archive file)网络应用程序文件,是与平台无关的文件格式,它允许将许多文件组合成一个压缩文件。war专用在web方面 。 + +JAVA WEB工程,都是打成WAR包进行发布。 + +典型的war包内部结构如下: + +webapp.war + + | index.jsp + + | + + |— images + + |— META-INF + + |— WEB-INF + + | web.xml // WAR包的描述文件 + + | + + |— classes + + | action.class // java类文件 + + | + + |— lib + + other.jar // 依赖的jar包 + + share.jar + +## tomcat基础 + +上一篇文章关于网络编程和NIO已经讲过了,这里按住不表。 + +## log4j + +log4j是非常常用的日志组件,不过现在为了使用更通用的日志组件,一般使用slf4j来配置日志管理器,然后再介入日志源,比如log4j这样的日志组件。 + +## 数据库驱动和连接池 + +一般我们会使用class.forname加载数据库驱动,但是随着Spring的发展,现在一般会进行数据源DataSource这个bean的配置,bean里面填写你的数据来源信息即可,并且在实现类中可以选择支持连接池的数据源实现类,比如c3poDataSource,非常方便。 + +数据库连接池本身和线程池类似,就是为了避免频繁建立数据库连接,保存了一部分连接并存放在集合里,一般可以用队列来存放。 + +除此之外,还可以使用tomcat的配置文件来管理数据库连接池,只需要简单的一些配置,就可以让tomcat自动管理数据库的连接池了。 +应用需要使用的时候,通过jndi的方式访问即可,具体方法就是调用jndi命名服务的look方法。 + +## 单元测试 + +单元测试是工程中必不可少的组件,maven项目在打包期间会自动运行所有单元测试。一般我们使用junit做单元测试,统一地在test包中分别测试service和dao层,并且使用mock方法来构造假的数据,以便跳过数据库或者其他外部资源来完成测试。 + +## Maven + +maven是一个项目构建工具,基于约定大于配置的方式,规定了一个工程各个目录的用途,并且根据这些规则进行编译,测试和打包。 +同时他提供了方便的包管理方式,以及快速部署的优势。 + +## Git + +git是分布式的代码管理工具,比起svn有着分布式的优势。太过常见了,略了。 + +## Json和xml +数据描述形式不同,json更简洁。 + +## hibernate和mybatis + +由于jdbc方式的数据库连接和语句执行太过繁琐,重复代码太多,后来提出了jdbctemplate对数据进行bean转换。 + +但是还是差强人意,于是转而出现了hibernate这类的持久化框架。可以做到数据表和bean一一映射,程序只需要操作bean就可以完成数据库的curd。 + +mybatis比hibernate更轻量级,mybatis支持原生sql查询,并且也可以使用bean映射,同时还可以自定义地配置映射对象,更加灵活,并且在多表查询上更有优势。 + diff --git "a/md/Java\345\271\266\345\217\221\346\200\273\347\273\223.md" "b/md/Java\345\271\266\345\217\221\346\200\273\347\273\223.md" new file mode 100644 index 0000000..32ad101 --- /dev/null +++ "b/md/Java\345\271\266\345\217\221\346\200\273\347\273\223.md" @@ -0,0 +1,206 @@ +--- +title: Java并发总结 +date: 2018-07-08 22:06:18 +tags: + - Java并发 +categories: + - 后端 + - 技术总结 +--- +# Java并发 + +这篇总结主要是基于我Java并发技术系列的文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 + +#更多详细内容可以查看我的专栏文章:Java并发技术指南 + +#https://blog.csdn.net/column/details/21961.html + +## 线程安全 + +线程安全一般指多线程之间的操作结果不会因为线程调度的顺序不同而发生改变。 + +## 互斥和同步 + + 互斥一般指资源的独占访问,同步则要求同步代码中的代码顺序执行,并且也是单线程独占的。 + +## JMM内存模型 + + JVM中的内存分区包括堆,栈,方法区等区域,这些内存都是抽象出来的,实际上,系统中只有一个主内存,但是为了方便Java多线程语义的实现,以及降低程序员编写并发程序的难度,Java提出了JMM内存模型,将内存分为主内存和工作内存,工作内存是线程独占的,实际上它是一系列寄存器,编译器优化后的结果。 + +## as-if-Serial,happens-before + + as if serial语义提供单线程代码的顺序执行保证,虽然他允许指令重排序,但是前提是指令重排序不会改变执行结果。 + +## volatile + + volatile语义实际上是在代码中插入一个内存屏障,内存屏障分为读写,写读,读读,写写四种,可以用来避免volatile变量的读写操作发生重排序,从而保证了volatile的语义,实际上,volatile修饰的变量强制要求线程写时将数据从缓存刷入主内存,读时强制要求线程从主内存中读取,因此保证了它的可见性。 + + 而对于volatile修饰的64位类型数据,可以保证其原子性,不会因为指令重排序导致一个64位数据被分割成两个32位数据来读取。 + +## synchronized和锁优化 + + synchronized是Java提供的同步标识,底层是操作系统的mutex lock调用,需要进行用户态到内核态的切换,开销比较大。 + synchronized经过编译后的汇编代码会有monitor in和monitor out的字样,用于标识进入监视器模块和退出监视器模块, + 监视器模块watcher会监控同步代码块中的线程号,只允线程号正确的线程进入。 + + Java在synchronized关键字中进行了多次优化。 + + 比如轻量级锁优化,使用锁对象的对象头做文章,当一个线程需要获得该对象锁时,线程有一段空间叫做lock record,用于存储对象头的mask word,然后通过cas操作将对象头的mask word改成指向线程中的lockrecord。 + 如果成功了就是获取到了锁,否则就是发生了互斥。需要锁粗化,膨胀为互斥锁。 + + 偏向锁,去掉了更多的同步措施,检查mask word是否是可偏向状态,然后检查mask word中的线程id是否是自己的id,如果是则执行同步代码,如果不是则cas修改其id,如果修改失败,则出现锁争用,偏向锁失效,膨胀为轻量级锁。 + + 自旋锁,每个线程会被分配一段时间片,并且听候cpu调度,如果发生线程阻塞需要切换的开销,于是使用自旋锁不需要阻塞,而是忙等循环,一获取时间片就开始忙等,这样的锁就是自旋锁,一般用于并发量比较小,又担心切换开销的场景。 + +## CAS操作 + CAS操作是通过硬件实现的原子操作,通过一条指令完成比较和赋值的操作,防止发生因指令重排导致的非原子操作,在Java中通过unsafe包可以直接使用,在Java原子类中使用cas操作来完成一系列原子数据类型的构建,保证自加自减等依赖原值的操作不会出现并发问题。 + + cas操作也广泛用在其他并发类中,通过循环cas操作可以完成线程安全的并发赋值,也可以通过一次cas操作来避免使用互斥锁。 + +## Lock类 + +### AQS + +AQS是Lock类的基石,他是一个抽象类,通过操作一个变量state来判断线程锁争用的情况,通过一系列方法实现对该变量的修改。一般可以分为独占锁和互斥锁。 + +AQS维护着一个CLH阻塞队列,这个队列主要用来存放阻塞等待锁的线程节点。可以看做一个链表。 + +一:独占锁 +独占锁的state只有0和1两种情况(如果是可重入锁也可以把state一直往上加,这里不讨论),state = 1时说明已经有线程争用到锁。线程获取锁时一般是通过aqs的lock方法,如果state为0,首先尝试cas修改state=1,成功返回,失败时则加入阻塞队列。非公共锁使用时,线程节点加入阻塞队列时依然会尝试cas获取锁,最后如果还是失败再老老实实阻塞在队列中。 + +独占锁还可以分为公平锁和非公平锁,公平锁要求锁节点依据顺序加入阻塞队列,通过判断前置节点的状态来改变后置节点的状态,比如前置节点获取锁后,释放锁时会通知后置节点。 + +非公平锁则不一定会按照队列的节点顺序来获取锁,如上面所说,会先尝试cas操作,失败再进入阻塞队列。 + +二:共享锁 +共享锁的state状态可以是0到n。共享锁维护的阻塞队列和互斥锁不太一样,互斥锁的节点释放锁后只会通知后置节点,而共享锁获取锁后会通知所有的共享类型节点,让他们都来获取锁。共享锁用于countdownlatch工具类与cyliderbarrier等,可以很好地完成多线程的协调工作 + +### 锁Lock和Conditon + +Lock 锁维护这两个内部类fairsync和unfairsync,都继承自aqs,重写了部分方法,实际上大部分方法还是aqs中的,Lock只是重新把AQS做了封装,让程序员更方便地使用Lock锁。 + +和Lock锁搭配使用的还有condition,由于Lock锁只维护着一个阻塞队列,有时候想分不同情况进行锁阻塞和锁通知怎么办,原来我们一般会使用多个锁对象,现在可以使用condition来完成这件事,比如线程A和线程B分别等待事件A和事件B,可以使用两个condition分别维护两个队列,A放在A队列,B放在B队列,由于Lock和condition是绑定使用的,当事件A触发,线程A被唤醒,此时他会加入Lock自己的CLH队列中进行锁争用,当然也分为公平锁和非公平锁两种,和上面的描述一样。 + +Lock和condtion的组合广泛用于JUC包中,比如生产者和消费者模型,再比如cyliderbarrier。 + +###读写锁 + +读写锁也是Lock的一个子类,它在一个阻塞队列中同时存储读线程节点和写线程节点,读写锁采用state的高16位和低16位分别代表独占锁和共享锁的状态,如果共享锁的state > 0可以继续获取读锁,并且state-1,如果=0,则加入到阻塞队列中,写锁节点和独占锁的处理一样,因此一个队列中会有两种类型的节点,唤醒读锁节点时不会唤醒写锁节点,唤醒写锁节点时,则会唤醒后续的节点。 + +因此读写锁一般用于读多写少的场景,写锁可以降级为读锁,就是在获取到写锁的情况下可以再获取读锁。 + +## 并发工具类 + +countdownlatch主要通过AQS的共享模式实现,初始时设置state为N,N是countdownlatch初始化使用的size,每当有一个线程执行countdown,则state-1,state = 0之前所有线程阻塞在队列中,当state=0时唤醒队头节点,队头节点依次通知所有共享类型的节点,唤醒这些线程并执行后面的代码。 + +cycliderbarrier主要通过lock和condition结合实现,首先设置state为屏障等待的线程数,在某个节点设置一个屏障,所有线程运行到此处会阻塞等待,其实就是等待在一个condition的队列中,并且每当有一个线程到达,state -=1 则当所有线程到达时,state = 0,则唤醒condition队列的所有结点,去执行后面的代码。 + +samphere也是使用AQS的共享模式实现的,与countlatch大同小异,不再赘述。 + +exchanger就比较复杂了。使用exchanger时会开辟一段空间用来让两个线程进行交互操作,这个空间一般是一个栈或队列,一个线程进来时先把数据放到这个格子里,然后阻塞等待其他线程跟他交换,如果另一个线程也进来了,就会读取这个数据,并把自己的数据放到对方线程的格子里,然后双双离开。当然使用栈和队列的交互是不同的,使用栈的话匹配的是最晚进来的一个线程,队列则相反。 + +## 原子数据类型 + +原子数据类型基本都是通过cas操作实现的,避免并发操作时出现的安全问题。 + +## 同步容器 + +同步容器主要就是concurrenthashmap了,在集合类中我已经讲了chm了,所以在这里简单带过,chm1.7通过分段锁来实现锁粗化,使用的死LLock锁,而1.8则改用synchronized和cas的结合,性能更好一些。 + +还有就是concurrentlinkedlist,ConcurrentSkipListMap与CopyOnWriteArrayList。 + +第一个链表也是通过cas和synchronized实现。 + +而concurrentskiplistmap则是一个跳表,跳表分为很多层,每层都是一个链表,每个节点可以有向下和向右两个指针,先通过向右指针进行索引,再通过向下指针细化搜索,这个的搜索效率是很高的,可以达到logn,并且它的实现难度也比较低。通过跳表存map就是把entry节点放在链表中了。查询时按照跳表的查询规则即可。 + +CopyOnWriteArrayList是一个写时复制链表,查询时不加锁,而修改时则会复制一个新list进行操作,然后再赋值给原list即可。 +适合读多写少的场景。 + +## 阻塞队列 + + BlockingQueue 实现之 ArrayBlockingQueue + + ArrayBlockingQueue其实就是数组实现的阻塞队列,该阻塞队列通过一个lock和两个condition实现,一个condition负责从队头插入节点,一个condition负责队尾读取节点,通过这样的方式可以实现生产者消费者模型。 + + BlockingQueue 实现之 LinkedBlockingQueue + + LinkedBlockingQueue是用链表实现的阻塞队列,和arrayblockqueue有所区别,它支持实现为无界队列,并且它使用两个lock和对应的condition搭配使用,这是因为链表可以同时对头部和尾部进行操作,而数组进行操作后可能还要执行移位和扩容等操作。 + 所以链表实现更灵活,读写分别用两把锁,效率更高。 + + BlockingQueue 实现之 SynchronousQueue + + SynchronousQueue实现是一个不存储数据的队列,只会保留一个队列用于保存线程节点。详细请参加上面的exchanger实现类,它就是基于SynchronousQueue设计出来的工具类。 + + BlockingQueue 实现之 PriorityBlockingQueue + + PriorityBlockingQueue + + PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。 + + DelayQueue + + DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景: + + 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。 + 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。 + +## 线程池 + +### 类图 + +首先看看executor接口,只提供一个run方法,而他的一个子接口executorservice则提供了更多方法,比如提交任务,结束线程池等。 + +然后抽象类abstractexecutorservice提供了更多的实现了,最后我们最常使用的类ThreadPoolExecutor就是继承它来的。 + + +ThreadPoolExecutor可以传入多种参数来自定义实现线程池。 + +而我们也可以使用Executors中的工厂方法来实例化常用的线程池。 + +### 常用线程池 + +比如newFixedThreadPool + +newSingleThreadExecutor newCachedThreadPool + +newScheduledThreadPool等等,这些线程池即可以使用submit提交有返回结果的callable和futuretask任务,通过一个future来接收结果,或者通过callable中的回调函数call来回写执行结果。也可以用execute执行无返回值的runable任务。 + +在探讨这些线程池的区别之前,先看看线程池的几个核心概念。 + +任务队列:线程池中维护了一个任务队列,每当向线程池提交任务时,任务加入队列。 + +工作线程:也叫worker,从线程池中获取任务并执行,执行后被回收或者保留,因情况而定。 + +核心线程数和最大线程数,核心线程数是线程池需要保持存活的线程数量,以便接收任务,最大线程数是能创建的线程数上限。 + +newFixedThreadPool可以设置固定的核心线程数和最大线程数,一个任务进来以后,就会开启一个线程去执行,并且这部分线程不会被回收,当开启的线程达到核心线程数时,则把任务先放进任务队列。当任务队列已满时,才会继续开启线程去处理,如果线程总数打到最大线程数限制,任务队列又是满的时候,会执行对应的拒绝策略。 + +拒绝策略一般有几种常用的,比如丢弃任务,丢弃队尾任务,回退给调用者执行,或者抛出异常,也可以使用自定义的拒绝策略。 + +newSingleThreadExecutor是一个单线程执行的线程池,只会维护一个线程,他也有任务队列,当任务队列已满并且线程数已经是1个的时候,再提交任务就会执行拒绝策略。 + +newCachedThreadPool比较特别,第一个任务进来时会开启一个线程,而后如果线程还没执行完前面的任务又有新任务进来,就会再创建一个线程,这个线程池使用的是无容量的SynchronousQueue队列,要求请求线程和接受线程匹配时才会完成任务执行。 +所以如果一直提交任务,而接受线程来不及处理的话,就会导致线程池不断创建线程,导致cpu消耗很大。 + +ScheduledThreadPoolExecutor内部使用的是delayqueue队列,内部是一个优先级队列priorityqueue,也就是一个堆。通过这个delayqueue可以知道线程调度的先后顺序和执行时间点。 + + +## Fork/Join框架 + +又称工作窃取线程池。 + +我们在大学算法课本上,学过的一种基本算法就是:分治。其基本思路就是:把一个大的任务分成若干个子任务,这些子任务分别计算,最后再Merge出最终结果。这个过程通常都会用到递归。 + +而Fork/Join其实就是一种利用多线程来实现“分治算法”的并行框架。 + +另外一方面,可以把Fori/Join看作一个单机版的Map/Reduce,只不过这里的并行不是多台机器并行计算,而是多个线程并行计算。 + +与ThreadPool的区别 +通过上面例子,我们可以看出,它在使用上,和ThreadPool有共同的地方,也有区别点: +(1) ThreadPool只有“外部任务”,也就是调用者放到队列里的任务。 ForkJoinPool有“外部任务”,还有“内部任务”,也就是任务自身在执行过程中,分裂出”子任务“,递归,再次放入队列。 +(2)ForkJoinPool里面的任务通常有2类,RecusiveAction/RecusiveTask,这2个都是继承自FutureTask。在使用的时候,重写其compute算法。 + +工作窃取算法 +上面提到,ForkJoinPool里有”外部任务“,也有“内部任务”。其中外部任务,是放在ForkJoinPool的全局队列里面,而每个Worker线程,也有一个自己的队列,用于存放内部任务。 + +窃取的基本思路就是:当worker自己的任务队列里面没有任务时,就去scan别的线程的队列,把别人的任务拿过来执行 \ No newline at end of file diff --git "a/md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" "b/md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" new file mode 100644 index 0000000..2eba287 --- /dev/null +++ "b/md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" @@ -0,0 +1,136 @@ +--- +title: JAVA后端开发学习之路 # 文章页面上的显示名称,可以任意修改,不会出现在URL中 +date: 2018-4-20 15:56:26 # 文章生成时间,一般不改 +categories: + - 个人总结 +tags: + - 心路历程 +--- +本文主要记录了我从Java初学者到专注于Java后端技术栈的开发者的学习历程。主要分享了学习过程中的一些经验和教训,让后来人看到,少走弯路,与君共勉,共同进步。如有错误,还请见谅。 + +我的GitHub: +> https://github.com/h2pl/MyTech + +喜欢的话麻烦点下星哈 + +文章首发于我的个人博客: +> https://h2pl.github.io/2018/04/20/java + +更多关于Java后端学习的内容请到我的CSDN博客上查看: + +https://blog.csdn.net/a724888 + +相关链接:我和技术博客的这一年:https://blog.csdn.net/a724888/article/details/60879893 +>  不论你是不是网民,无论你远离互联网,还是沉浸其中;你的身影,都在这场伟大的迁徙洪流中。超越人类经验的大迁徙,温暖而无情地,开始了。 +                                 -----《互联网时代》 + + + +## 选择方向 + +  0上大学前的那些事,让它们随风逝去吧。 + +  1 个人对计算机和互联网有情怀,有兴趣,本科时在专业和学校里选择了学校,当时专业不是计算机,只能接触到一点点计算机专业课程,所以选择了考研,花半年时间复习考进了一个还不错的985,考研经历有空会发到博客上。 + +  2 本科阶段接触过Java和Android,感觉app蛮有趣的,所以研一的时候想做Android,起初花大量时间看了计算机专业课的教材,效果很差。但也稍微了解了一些计算机基础,如网络,操作系统,组成原理,数据库,软工等。 + +  3 在没确定方向的迷茫时期看了大量视频和科普性文章,帮助理清头绪和方向。期间了解了诸如游戏开发,c++开发,Android,Java甚至前端等方向,其中还包含游戏策划岗。 + +  4 后来综合自身条件以及行业发展等因素,开始锁定自己的目标在Java后台方向。于是乎各种百度,知乎,查阅该学什么该怎么学如此类的问题,学习别人的经验。当然只靠搜索引擎很难找到精品内容,那段时间可谓是病急乱投医,走了不少弯路。 + +--- + +## 夯实基础 + +  1 研一的工程实践课让我知道了我的基础不够扎实,由于并非科班,需要比别人更加勤奋,古语有云,天道酬勤,勤能补拙。赶上了17年的春招实习招聘,期间开始各种海投,各种大厂面试一问三不知,才知道自身差距很大,开始疯狂复习面试题,刷面经,看经验等。死记硬背,之乎者也,倒也是能应付一些小公司,可谓是临阵磨枪不快也光。 + +  2 不过期间的屡屡受挫让我冷静思考了一段时间,我再度调研了岗位需求,学习方法,以及需要看的书等资料。再度开工时,我的桌上开始不断出现新的经典书籍。这还要归功于我的启蒙导师:江南白衣,在知乎上看到了他的一篇文章,我的Java后端书架。在这个书架里我找寻到了很多我想看的书,以及我需要学习的技术。 + +  3 遥想研一我还在看的书:教材就不提了,脱离实际并且年代久远,而我选的入门书籍竟然还有Java web从入门到精通这种烂大街的书籍,然后就是什么Java编程思想啦,深入理解计算机系统,算法导论这种高深莫测的书,感觉有点高不成低不就的意思。要么太过难懂要么过于粗糙,这些书在当时基本上没能帮到我。 + +--- + +## 书籍选择 + +  1 江南白衣的后端书架真是救我于水火。他的书架里收录了许多Java后端需要用到的技术书籍,并且十分经典,虽不说每本都适合入门,但是只要你用心去看都会有收获,高质量的书籍给人的启发要优于普通书籍。 + +  2 每个门类的书我都挑了一些。比如网络的两本(《tcp ip卷一》以及《计算机网络自顶向下》),操作系统两本(一本《Linux内核设计与实现》,一本高级操作系统,推荐先看完《深入理解计算机系统》再来看这两本),算法看的是《数据结构与算法(Java版)》,Java的四大件(《深入理解jvm虚拟机》,《java并发编程艺术》,《深入java web技术内幕》,《Java核心技术 卷一》这本没看)。 + +  3 当然还有像《Effective Java》,《Java编程思想》,《Java性能调优指南》这种,不过新手不推荐,太不友好。接着是spring的两本《Spring实战》和《Spring源码剖析》。当然也包括一些redis,mq之类的书,还有就是一些介绍分布式组件的书籍,如zk等。 + +  4 接下来就是扩展的内容了,比如分布式的三大件,《大型网站架构设计与实践》,《分布式网站架构设计与实践》,《Java中间件设计与实践》,外加一本《分布式服务框架设计与实践》。这几本书一看,绝对让你打开新世界的大门,醍醐灌顶,三月不知肉味。 + +  5 你以为看完这些书你就无敌了,就满足了?想得倒是挺美。这些书最多就是把我从悬崖边拉回正途,能让我在正确的道路上行走了。毕竟技术书籍这种东西还是有门槛的,没有一定的知识储备,看书的过程也绝对是十分痛苦的。 + +    6 比如《深入理解jvm虚拟机》和《java并发编程艺术》这两本书,我看了好几遍,第一遍基本当天书来看,第二遍挑着章节看,第三遍能把全部章节都看了。所以有时候你觉得你看完了一本书,对,你确实看完了,但过段时间是你能记得多少呢。可以说是很少了。 + +--- + +## 谈一谈学习方法 + +  1 人们在刚开始接触自己不熟悉的领域时,往往都会犯很多错误。刚开始学习Java时,就是摸着石头过河。从在极客学院慕课上看视频,到看书,再到看博客,再到工程实践,也是学习方式转变的一个过程。 + +  2 看视频:适合0基础小白,视频给你构建一个世界观,让你对你要做的东西有个大概的了解,想要深入理解其中的技术原理,只看视频的话很难。 + +  3 看书:就如上面一节所说,看书是一个很重要的环节。当你对技术只停留在大概的了解和基本会用的阶段时,经典书籍能够让你深入这些技术的原理,你可能会对书里的内容感到惊叹,也可能只是一知半解。所以第一遍的阅读一般读个大概就可以。一本书要吃透,不仅要看好几遍,还要多上手实践,才能变成自己的东西。 + +  4 看博客,光看一些总结性的博客或者是科普性的博客可能还不够,一开始我也经常看这样的博客,后来只看这些东西,发现对技术的理解只能停留在表面。高质量的博客一般会把一个知识点讲得很透彻,比你看十篇总结都强,例如讲jdk源码的博文,可以很好地帮助你理解其原理,避免自己看的时候一脸懵逼。这里先推荐几个博客和网站,后面写复习计划的时候,会详细写出。 +博客:江南白衣、酷壳、战小狼。 +网站:并发编程网,importnew。 + +  5 实践为王,Java后端毕竟还是工程方向,只是通过文字去理解技术点,可能有点纸上谈兵的感觉了。还有一个问题就是,没有进行上手实践的技术,一般很快就会忘了,做一些实践可以更好地巩固知识点。如果有项目中涉及不到的知识点,可以单独拿出来做一些demo,实在难以进行实践的技术点,可以参考别人的实践过程。 + +--- + +## 实习,提高工程能力的好机会 + +  1 这段时间以后就是实习期了,三个月的W厂实习经历。半年的B厂实习,让我着实过了一把大厂的瘾。但是其中做的工作无非就是增删改查写写业务逻辑,很难接触到比较核心的部分。 + +  2 于是乎我花了许多时间学习部门的核心技术。比如在W厂参与数据平台的工作时,我学习了hadoop以及数据仓库的架构,也写了一些博客,并且向负责后端架构的导师请教了许多知识,收获颇丰。 + +  3 在B厂实习期间则接触了许多云计算相关的技术。因为部门做的是私有云,所以业务代码和底层的服务也是息息相关的,比如平时的业务代码也会涉及到底层的接口调用,比如新建一个虚拟机或者启动一台虚拟机,需要通过多级的服务调用,首先是HTTP服务调用,经过多级的服务调用,最终完成流程。在这期间我花了一些时间学习了OpenStack的架构以及部门的实际应用情况,同时也玩了一下docker,看了kubenetes的一些书籍,算是入门。 + +  4 但是这些东西其实离后台开发还是有一定距离的,比如后台开发的主要问题就是高并发,分布式,Linux服务器开发等。而我做的东西,只能稍微接触到这一部门的内容,因为主要是to b的内部业务。所以这段时间其实我的进步有限,虽然扩大了知识面并且积累了开发经验,但是对于后台岗位来说还是有所欠缺的。 + +  5 不过将近一年的实习也让我收获了很多东西,大厂的实习体验很好,工作高效,团队合作,版本的快速迭代,技术氛围很不错。特别是在B厂了可以解到很多前沿的技术,对自己的视野扩展很有帮助。 + +--- + +## **实习转正,还是准备秋招?** + +  1 离职以后,在考虑是否还要找实习,因为有两份实习经历了,在考虑要不要静下心来刷刷题,复习一下基础,并且回顾一下实习时用到的技术。同一时期,我了解到腾讯和阿里等大厂的实习留用率不高,并且可能影响到秋招,所以当时的想法是直接复习等到秋招内推。因此,那段时间比较放松,没什么复习状态,也导致了我在今年春招内推的阶段比较艰难。 + +  2 因为当时想着沉住气准备秋招,所以一开始对实习内推不太在意。但是由于AT招人的实习生转正比例较大,考虑到秋招的名额可能更少,所以还是不愿意错过这个机会。因为开始系统复习的时间比较晚,所以投的比较晚,担心准备不充分被刷。这次找实习主要是奔着转正去的,所以只投了bat和滴滴,京东,网易游戏等大厂。 + +  3 由于投递时间原因,所以面试的流程特别慢。并且在笔试方面还是有所欠缺,刷题刷的比较少,在线编程的算法题还是屡屡受挫。这让我有点后悔实习结束后的那段时间没有好好刷题了。 + +--- + +## **调整心态,重新上路** + +  1 目前的状态是,一边刷题,一边复习基础,投了几家大厂的实习内推,打算选一个心仪的公司准备转正,但是事情总是没那么顺利,微软,头条等公司的笔试难度超过了我的能力范围,没能接到面试电话。腾讯投了一个自己比较喜欢的部门,可惜岗位没有匹配上,后台开发被转成了运营开发,最终没能通过。阿里面试的也不顺利,当时投了一个牛客上的蚂蚁金服内推,由于投的太晚,部门已经招满,只面了一面就没了下文,前几天接到了菜鸟的面试,这个未完待续。 + +  2 目前的想法是,因为我不怎么需要实习经历来加分了,所以想多花些时间复习基础,刷题,并且巩固之前的项目经历。当然如果有好的岗位并且转正机会比较大的话,也是会考虑去实习的,那样的话可能需要多挤点时间来复习基础和刷题了。 + +  3 在这期间,我会重新梳理一下自己的复习框架,有针对性地看一些高质量的博文,同时多做些项目实践,加深对知识的理解。当然这方面还会通过写博客进行跟进,写博客,做项目。前阵子在牛客上看到一位牛友CyC2018做的名为interview notebook的GitHub仓库,内容非常好,十分精品,我全部看完了,并且参考其LeetCode题解进行刷题。 + +  4 受到这位大佬的启发,我也打算做一个类似的代码仓库或者是博客专栏,尽量在秋招之前把总结做完,并且把好的文章都放进去。上述内容只是本人个人的心得体会,如果有错误或者说的不合理的地方,还请谅解和指正。希望与广大牛友共勉,一起进步。 + + + + + + + + + + + + + + + + + + + diff --git "a/md/Java\347\275\221\347\273\234\344\270\216NIO\346\200\273\347\273\223.md" "b/md/Java\347\275\221\347\273\234\344\270\216NIO\346\200\273\347\273\223.md" new file mode 100644 index 0000000..9bbbe42 --- /dev/null +++ "b/md/Java\347\275\221\347\273\234\344\270\216NIO\346\200\273\347\273\223.md" @@ -0,0 +1,213 @@ +--- +title: Java网络编程与NIO学习总结 +date: 2018-07-08 22:08:22 +tags: + - Java网络编程 + - NIO +categories: + - 后端 + - 技术总结 +--- +#Java网络编程与NIO学习总结 +这篇总结主要是基于我之前Java网络编程与NIO系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 + +#更多详细内容可以查看我的专栏文章:Java网络编程与NIO + +#https://blog.csdn.net/column/details/21963.html +## Java IO + +Java IO的基础知识已在前面讲过 + +## Socket编程 + +socket是操作系统提供的网络编程接口,他封装了对于TCP/IP协议栈的支持,用于进程间的通信,当有连接接入主机以后,操作系统自动为其分配一个socket套接字,套接字绑定着一个IP与端口号。通过socket接口,可以获取tcp连接的输入流和输出流,并且通过他们进行读取和写入此操作。 + +Java提供了net包用于socket编程,同时支持像Inetaddress,URL等工具类,使用socket绑定一个endpoint(ip+端口号),可以用于客户端的请求处理和发送,使用serversocket绑定本地ip和端口号,可以用于服务端接收TCP请求。 + +## 客户端,服务端的线程模型 + +一般客户端使用单线程模型即可,当有数据到来时启动线程读取,需要写入数据时开启线程进行数据写入。 + +服务端一般使用多线程模型,一个线程负责接收tcp连接请求,每当接收到请求后开启一个线程处理它的读写请求。 + +udp的客户端和服务端就比较简单了,由于udp数据报长度是确定的,只需要写入一个固定的缓存和读取一个固定的缓存空间即可。 + +一般通过DatagramPacket包装一个udp数据报,然后通过DatagramSocket发送 + +## IO模型 + +上述的socket在处理IO请求时使用的是阻塞模型。 + +于是我们还是得来探讨一下IO模型。 + +一般认为,应用程序处理IO请求需要将内核缓存区中的数据拷贝到用户缓冲区。这个步骤可以通过系统调用来完成,而用户程序处理IO请求的时候,需要先检查用户缓冲区是否准备好了数据,这个操作是系统调用recevfrom,如果数据没有准备好,默认会阻塞调用该方法的线程。 + +这样就导致了线程处理IO请求需要频繁进行阻塞,特别是并发量大的时候,线程切换的开销巨大。 + +一般认为有几种IO模型 + +1 阻塞IO :就是线程会阻塞在系统调用recevfrom上,并且等待数据准备就绪以后才会返回。 + +2 非阻塞IO : 不阻塞在系统调用recevfrom,而是通过自旋忙等的方式不断询问缓冲区数据是否准备就绪,避免线程阻塞的开销。 + +3 IO多路复用 :使用IO多路复用器管理socket,由于每个socket是一个文件描述符,操作系统可以维护socket和它的连接状态,一般分为可连接,可读和可写等状态。 + +每当用户程序接受到socket请求,将请求托管给多路复用器进行监控,当程序对请求感兴趣的事件发生时,多路复用器以某种方式通知或是用户程序自己轮询请求,以便获取就绪的socket,然后只需使用一个线程进行轮询,多个线程处理就绪请求即可。 + +IO多路复用避免了每个socket请求都需要一个线程去处理,而是使用事件驱动的方式,让少数的线程去处理多数socket的IO请求。 + +Linux操作系统对IO多路复用提供了较好的支持,select,poll,epoll是Linux提供的支持IO多路复用的API。一般用户程序基于这个API去开发自己的IO复用模型。比如NIO的非阻塞模型,就是采用了IO多路复用的方式,是基于epoll实现的。 + +3.1 select方式主要是使用数组来存储socket描述符,系统将发生事件的描述符做标记,然后IO复用器在轮询描述符数组的时候,就可以知道哪些请求是就绪了的。缺点是数组的长度只能到1024,并且需要不断地在内核空间和用户空间之间拷贝数组。 + +3.2 poll方式不采用数组存储描述符,而是使用独立的数据结构来描述,并且使用id来表示描述符,能支持更多的请求数量,缺点和select方式有点类似,就是轮询的效率很低,并且需要拷贝数据。 + +当然,上述两种方法适合在请求总数较少,并且活跃请求数较多的情况,这种场景下他们的性能还是不错的。 + +3.3 epoll + +epoll函数会在内核空间开辟一个特殊的数据结构,红黑树,树节点中存放的是一个socket描述符以及用户程序感兴趣的事件类型。同时epoll还会维护一个链表。用于存储已经就绪的socket描述符节点。 + +由Linux内核完成对红黑树的维护,当事件到达时,内核将就绪的socket节点加入链表中,用户程序可以直接访问这个链表以便获取就绪的socket。 + +当然了,这些操作都linux包装在epoll的api中了。 + +epoll_create函数会执行红黑树的创建操作。 + +epoll_ctl函数会将socket和感兴趣的事件注册到红黑树中。 + +epoll_wait函数会等待内核空间发来的链表,从而执行IO请求。 + +epoll的水平触发和边缘触发有所区别,水平触发的意思是,如果用户程序没有执行就绪链表里的任务,epoll仍会不断通知程序。 + +而边缘触发只会通知程序一次,之后socket的状态不发生改变epoll就不会再通知程序了。 + +4 信号驱动 +略 + +5 异步非阻塞 + +用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。 + +事实上就是,用户提交IO请求,然后直接返回,并且内核自动完成将数据从内核缓冲区复制到用户缓冲区,完成后再通知用户。 + +当然,内核通知我们以后我们还需要执行剩余的操作,但是我们的代码已经继续往下运行了,所以AIO采用了回调的机制,为每个socket注册一个回调事件或者是回调处理器,在处理器中完成数据的操作,也就是内核通知到用户的时候,会自动触发回调函数,完成剩余操作。 +这样的方式就是异步的网络编程。 + +但是,想要让操作系统支持这样的功能并非易事,windows的IOCP可以支持AIO方式,但是Linux的AIO支持并不是很好 +## NIO + +由于Java原生的socket只支持阻塞方式处理IO + +所以Java后来推出了新版IO 也叫New IO = NIO + +NIO提出了socketChannel,serversocketchannel,bytebuffer,selector和selectedkey等概念。 + +1 socketchannel其实就是socket的替代品,他的好处是多个socket可以复用同一个bytebuffer,因为socket是从channel里打开的,所以多个socket都可以访问channel绑定着的buffer。 + +2 serversocketchannel顾名思义,是用在服务端的channel。 + +3 bytebuffer以前对用户是透明的,用户直接操作io流即可,所以之前的socket io操作都是阻塞的,引入bytebuffer以后,用户可以更灵活地进行io操作。 + +buffer可以分为不同数据类型的buffer,但是常用的还是bytebuffer。写入数据时按顺序写入,写入完使用flip方法反转缓冲区,让接收端反向读取。这个操作比较麻烦,后来的netty对缓冲区进行了重新封装,封装了这个经常容易出错的方法。 + +4 selector其实就是对io多路复用器的封装,一般基于linux的epoll来实现。 +socket把感兴趣的事件和描述符注册到selector上,然后通过遍历selectedKey来获取感兴趣的请求,进行IO操作。 +selectedkey应该就是epoll中就绪链表的实现了。 + +5 所以一般的流程是: +新建一个serversocket,启动一个线程进行while循环,当有请求接入时,使用accept方法阻塞获取socket,然后将socket和感兴趣的事件注册到selector上。再开启一个线程轮询selectoredKey,当请求就绪时开启一个线程去处理即可。 + +## AIO + +后来NIO发展到2.0,Java又推出了AIO 的API,与上面描述的异步非阻塞模型类似。 + +AIO使用回调的方式处理IO请求,在socket上注册一个回调函数,然后提交请求后直接返回。由操作系统完成数据拷贝操作,需要操作系统对AIO的支持。 + +AIO的具体使用方式还是比较复杂的,感兴趣的可以自己查阅资料。 + +## Tomcat中的NIO模型 + +Tomcat作为一个应用服务器,分为connector和container两个部分,connector负责接收请求,而container负责解析请求。 + +一般connector负责接收http请求,当然首先要建立tcp连接,所以涉及到了如何处理连接和IO请求。 + +Tomcat使用endpoint的概念来绑定一个ip+port,首先,使用acceptor循环等待连接请求。然后开启一个线程池,也叫poller池,每个请求绑定一个poller进行后续处理,poller将socket请求封装成一个事件,并且将这个事件注册到selector中。 + +poller还需要维护一个事件列表,以便获取selector上就绪的事件。然后poller再去列表中获取就绪的请求,将其封装成processor,交给后续的worker线程池,会有worker将其提交给container流程中进行处理。 + +当然,到达container之后还有非常复杂的处理过程,稍微提几个点。 + +## Tomcat的container + +container是一个多级容器,最外层到最内层依次是engine,host,context和wrapper + +下面是个server.xml文件实例,Tomcat根据该文件进行部署 + + //顶层类元素,可以包括多个Service + //顶层类元素,可包含一个Engine,多个Connecter + //连接器类元素,代表通信接口 + //容器类元素,为特定的Service组件处理客户请求,要包含多个Host + //容器类元素,为特定的虚拟主机组件处理客户请求,可包含多个Context + //容器类元素,为特定的Web应用处理所有的客户请求 + + + + + + + + +根据配置文件初始化容器信息,当请求到达时进行容器间的请求传递,事实上整个链条被称作pipeline,pipeline连接了各个容器的入口,由于每个容器和组件都实现了lifecycle接口。 + +tomcat可以在任意流程中通过加监听器的方式监听组件的生命周期,也就能够控制整个运行的流程,通过在pipeline上增加valve可以增加一些自定义的操作。 + +一般到wrapper层才开始真正的请求解析,因为wrapper其实就是对servlet的简单封装,此时进来的请求和响应已经是httprequest和httpresponse,很多信息已经解析完毕,只需要按照service方法执行业务逻辑即可,当然在执行service方法之前,会调用filter链先执行过滤操作。 + +## netty + +netty我也不是很在行,这里简单总结一下 + +netty是一个基于事件驱动的网络编程框架。 + +因为直接基于Java NIO编程复杂度太高,而且容易出错,于是netty对NIO进行了改造和封装。形成了一个比较完整的网络框架,可以通过他实现rpc,http服务。 + +先了解一下两种线程模型。reactor和proactor。 + +1 reactor就是netty采用的模型,首先也是使用一个acceptor线程接收连接请求,然后开启一个线程组reactor thread pool。 + +server会事先在endpoint上注册一系列的回调方法,然后接收socket请求后交给底层的selector进行管理,当selector对应的事件响应以后,会通知用户进程,然后reactor工作线程会执行接下来的IO请求,执行操作是写在回调处理器中的。 + + +其实netty 支持三种reactor模型 +1.1.Reactor单线程模型:Reactor单线程模型,指的是所有的I/O操作都在同一个NIO线程上面完成。对于一些小容量应用场景,可以使用单线程模型。 + +1.2.Reactor多线程模型:Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理I/O操作。主要用于高并发、大业务量场景。 + +1.3.主从Reactor多线程模型:主从Reactor线程模型的特点是服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题 + +2 proactor模型其实是基于异步非阻塞IO模型的,当accpetor接收到请求以后,直接提交异步的io请求给linux内核,内核完成io请求后会回写消息到proactor提供的事件队列中,此时工作线程查看到IO请求已完成,则会继续剩余的工作,也是通过回调处理器来进行的。 + +所以两者最大的差别是,前者基于epoll的IO多路复用,后者基于AIO实现。 + +3 netty的核心组件: + +bytebuf + +bytebuf是对NIO中Bytebuffer的优化和扩展,并且支持堆外内存分配,堆外内存避免gc,可以更好地与内核空间进行交换数据。 + +channel和NIO的channel类似,但是NIO的socket代码改成nio实现非常麻烦,所以netty优化了这个过程,只需替换几个类就可以实现不更新太多代码就完成旧IO和新IO的切换。 + +channelhandler就是任务的处理器了,使用回调函数的方式注册到channel中,更准确来说是注册到channelpipeline里。 + +channelpipeline是用来管理和连接多个channelhandler的容器,执行任务时,会根据channelpipeline的调用链完成处理器的顺序调用,启动服务器时只需要将需要的channelhandler注册在上面就可以了。 + +eventloop +在Netty的线程模型中,一个EventLoop将由一个永远不会改变的Thread驱动,而一个Channel一生只会使用一个EventLoop(但是一个EventLoop可能会被指派用于服务多个Channel),在Channel中的所有I/O操作和事件都由EventLoop中的线程处理,也就是说一个Channel的一生之中都只会使用到一个线程。 + + +bootstrap + +在深入了解地Netty的核心组件之后,发现它们的设计都很模块化,如果想要实现你自己的应用程序,就需要将这些组件组装到一起。Netty通过Bootstrap类,以对一个Netty应用程序进行配置(组装各个组件),并最终使它运行起来。 + +对于客户端程序和服务器程序所使用到的Bootstrap类是不同的,后者需要使用ServerBootstrap,这样设计是因为,在如TCP这样有连接的协议中,服务器程序往往需要一个以上的Channel,通过父Channel来接受来自客户端的连接,然后创建子Channel用于它们之间的通信,而像UDP这样无连接的协议,它不需要每个连接都创建子Channel,只需要一个Channel即可。 \ No newline at end of file diff --git "a/md/Java\351\233\206\345\220\210\347\261\273\346\200\273\347\273\223.md" "b/md/Java\351\233\206\345\220\210\347\261\273\346\200\273\347\273\223.md" new file mode 100644 index 0000000..f4a9636 --- /dev/null +++ "b/md/Java\351\233\206\345\220\210\347\261\273\346\200\273\347\273\223.md" @@ -0,0 +1,108 @@ +--- +title: Java集合框架学习总结 +date: 2018-07-08 22:03:44 +tags: + - Java集合 +categories: + - 后端 + - 技术总结 +--- +# Java集合框架学习总结 + + + +这篇总结是基于之前博客内容的一个整理和回顾。 + + + +这里先简单地总结一下,更多详细内容请参考我的专栏:深入浅出Java核心技术 + +https://blog.csdn.net/column/details/21930.html + +里面有包括Java集合类在内的众多Java核心技术系列文章。 + + +以下总结不保证全对,如有错误,还望能够指出。谢谢 + + +## Colletion,iterator,comparable + + +一般认为Collection是最上层接口,但是hashmap实际上实现的是Map接口。iterator是迭代器,是实现iterable接口的类必须要提供的一个东西,能够使用for(i : A) 这种方式实现的类型能提供迭代器,以前有一个enumeration,现在早弃用了。 + + +## List + + +List接口下的实现类有ArrayList,linkedlist,vector等等,一般就是用这两个,用法不多说,老生常谈。 +ArrayList的扩容方式是1.5倍扩容,这样扩容避免2倍扩容可能浪费空间,是一种折中的方案。 +另外他不是线程安全,vector则是线程安全的,它是两倍扩容的。 + + +linkedlist没啥好说的,多用于实现链表。 + + + + +## Map + + +map永远都是重头戏。 + + +hashmap是数组和链表的组合结构,数组是一个Entry数组,entry是k-V键值对类型,所以一个entry数组存着很entry节点,一个entry的位置通过key的hashcode方法,再进行hash(移位等操作),最后与表长-1进行相与操作,其实就是取hash值到的后n - 1位,n代表表长是2的n次方。 + + +hashmap的默认负载因子是0.75,阈值是16 * 0.75 = 12;初始长度为16; + + +hashmap的增删改查方式比较简单,都是遍历,替换。有一点要注意的是key相等时,替换元素,不相等时连成链表。 + + +除此之外,1.8jdk改进了hashmap,当链表上的元素个数超过8个时自动转化成红黑树,节点变成树节点,以提高搜索效率和插入效率到logn。 + + +还有一点值得一提的是,hashmap的扩容操作,由于hashmap非线程安全,扩容时如果多线程并发进行操作,则可能有两个线程分别操作新表和旧表,导致节点成环,查询时会形成死锁。chm避免了这个问题。 + + +另外,扩容时会将旧表元素移到新表,原来的版本移动时会有rehash操作,每个节点都要rehash,非常不方便,而1.8改成另一种方式,对于同一个index下的链表元素,由于一个元素的hash值在扩容后只有两种情况,要么是hash值不变,要么是hash值变为原来值+2^n次方,这是因为表长翻倍,所以hash值取后n位,第一位要么是0要么是1,所以hash值也只有两种情况。这两种情况的元素分别加到两个不同的链表。这两个链表也只需要分别放到新表的两个位置即可,是不是很酷。 + + +最后有一个比较冷门的知识点,hashmap1.7版本链表使用的是节点的头插法,扩容时转移链表仍然使用头插法,这样的结果就是扩容后链表会倒置,而hashmap.1.8在插入时使用尾插法,扩容时使用头插法,这样可以保证顺序不变。 + + +## CHM + + +concurrenthashmap也稍微提一下把,chm1.7使用分段锁来控制并发,每个segment对应一个segmentmask,通过key的hash值相与这个segmentmask得到segment位置,然后在找到具体的entry数组下标。所以chm需要维护多个segment,每个segment对应一个数组。分段锁使用的是reetreetlock可重入锁实现。查询时不加锁。 + + +1.8则放弃使用分段锁,改用cas+synchronized方式实现并发控制,查询时不加锁,插入时如果没有冲突直接cas到成功为止,有冲突则使用synchronized插入。 + + + + +## Set + + +set就是hashmap将value固定为一个object,只存key元素包装成一个entry即可,其他不变。 + + +## Linkedhashmap + + +在原来hashmap基础上将所有的节点依据插入的次序另外连成一个链表。用来保持顺序,可以使用它实现lru缓存,当访问命中时将节点移到队头,当插入元素超过长度时,删除队尾元素即可。 + + +## collections和Arrays工具类 +两个工具类分别操作集合和数组,可以进行常用的排序,合并等操作。 + + +## comparable和comparator +实现comparable接口可以让一个类的实例互相使用compareTo方法进行比较大小,可以自定义比较规则,comparator则是一个通用的比较器,比较指定类型的两个元素之间的大小关系。 + + +## treemap和treeset + + +主要是基于红黑树实现的两个数据结构,可以保证key序列是有序的,获取sortedset就可以顺序打印key值了。其中涉及到红黑树的插入和删除,调整等操作,比较复杂,这里就不细说了。 \ No newline at end of file diff --git "a/md/Mysql\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" "b/md/Mysql\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" new file mode 100644 index 0000000..b7f96f4 --- /dev/null +++ "b/md/Mysql\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" @@ -0,0 +1,345 @@ +--- +title: Mysql原理与实践总结 +date: 2018-07-08 22:15:04 +tags: + - Mysql +categories: + - 后端 + - 技术总结 +--- +# 数据库(MySQL) + +本文根据自己对MySQL的学习和实践以及各类文章与书籍总结而来。 +囊括了MySQL数据库的基本原理和技术。本文主要是我的一个学习总结,基于之前的系列文章做了一个概括,如有错误,还望指出,谢谢。 + +详细内容请参考我的系列文章: +#重新学习MySQL与Redis +#https://blog.csdn.net/column/details/21877.html? + +# 数据库原理 +Mysql是关系数据库。 + +## 范式 反范式 +范式设计主要是避免冗余,以及数据不一致。反范式设计主要是避免多表连接,增加了冗余。 + +## 主键 外键 +主键是一个表中一行数据的唯一标识。 +外键则是值某一列的键值是其他表的主键,外键的作用一般用来作为两表连接的键,并且保证数据的一致性。 + +## 锁 共享锁和排它锁 +数据库的锁用来进行并发控制,排它锁也叫写锁,共享锁也叫行锁,根据不同粒度可以分为行锁和表锁。 + +## 存储过程与视图 +存储过程是对sql语句进行预编译并且以文件形式包装为一个可以快速执行的程序。但是缺点是不易修改,稍微改动语句就需要重新开发储存过程,优点是执行效率快。视图就是对其他一个或多个表进行重新包装,是一个外观模式,对视图数据的改动也会影响到数据报本身。 + +## 事务与隔离级别 +事务的四个性质:原子性,一致性,持久性,隔离性。 + +原子性:一个事务中的操作要么全部成功要么全部失败。 + +一致性:事务执行成功的状态都是一致的,即使失败回滚了,也应该和事务执行前的状态是一致的。 + +隔离性:两个事务之间互不相干,不能互相影响。 + +事务的隔离级别 +读未提交:事务A和事务B,A事务中执行的操作,B也可以看得到,因为级别是未提交读,别人事务中还没提交的数据你也看得到。这是没有任何并发措施的级别,也是默认级别。这个问题叫做脏读,为了解决这个问题,提出了读已提交。 + +读已提交:事务A和B,A中的操作B看不到,只有A提交后,在B中才看得到。虽然A的操作B看不到,但是B可以修改A用到的数据,导致A读两次的数据结果不同。这就是不可重读问题。 + +可重复读:事务A和B,事务A和B,A在数据行上加读锁,B虽然看得到但是改不了。所以是可重复读的,但是A的其他行仍然会被B访问并修改,所以导致了幻读问题。 + +序列化:数据库强制事务A和B串行化操作,避免了并发问题,但是效率比较低。 + +后面可以看一下mysql对隔离级别的实现。 + +## 索引 + +索引的作用就和书的目录类似,比如根据书名做索引,然后我们通过书名就可以直接翻到某一页。数据表中我们要找一条数据,也可以根据它的主键来找到对应的那一页。当然数据库的搜索不是翻书,如果一页一页翻书,就相当于是全表扫描了,效率很低,所以人翻书肯定也是跳着翻。数据库也会基于类似的原理"跳着”翻书,快速地找到索引行。 + +# mysql原理 + +MySQL是oracle公司的免费数据库,作为关系数据库火了很久了。所以我们要学他。 + +## mysql客户端,服务端,存储引擎,文件系统 + +MySQL数据库的架构可以分为客户端,服务端,存储引擎和文件系统。 + +详细可以看下架构图,我稍微总结下 + + 最高层的客户端,通过tcp连接mysql的服务器,然后执行sql语句,其中涉及了查询缓存,执行计划处理和优化,接下来再到存储引擎层执行查询,底层实际上访问的是主机的文件系统。 + +![image](http://image20.it168.com/201611_670x502/2701/39b96aa41090f9bb.png) + +## mysql常用语法 + +1 登录mysql + +mysql -h 127.0.0.1 -u 用户名 -p + +2 创建表 +语法还是比较复杂的,之前有腾讯面试官问这个,然后答不上来。 + + CREATE TABLE `user_accounts` ( + `id` int(100) unsigned NOT NULL AUTO_INCREMENT primary key, + `password` varchar(32) NOT NULL DEFAULT '' COMMENT '用户密码', + `reset_password` tinyint(32) NOT NULL DEFAULT 0 COMMENT '用户类型:0-不需要重置密码;1-需要重置密码', + `mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机', + `create_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `update_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + -- 创建唯一索引,不允许重复 + UNIQUE INDEX idx_user_mobile(`mobile`) + ) + ENGINE=InnoDB DEFAULT CHARSET=utf8 + + +3 crud比较简单,不谈 + +4 join用于多表连接,查询的通常是两个表的字段。 + +union用于组合同一种格式的多个select查询。 + +6 聚合函数,一般和group by一起使用,比如查找某部门员工的工资平均值。 +就是select AVE(money) from departmentA group by department + +7 建立索引 + + +唯一索引(UNIQUE) +语法:ALTER TABLE 表名字 ADD UNIQUE (字段名字) + +添加多列索引 +语法: + +ALTER TABLE table_name ADD INDEX index_name ( column1, column2, column3) + +8 修改添加列 + +添加列 +语法:alter table 表名 add 列名 列数据类型 [after 插入位置]; + +删除列 +语法:alter table 表名 drop 列名称; + +9 清空表数据 +方法一:delete from 表名; +方法二:truncate from "表名"; + +DELETE:1. DML语言;2. 可以回退;3. 可以有条件的删除; + +TRUNCATE:1. DDL语言;2. 无法回退;3. 默认所有的表内容都删除;4. 删除速度比delete快。 + +## MySQL的存储原理 + +下面我们讨论的是innodb的存储原理 + +innodb的存储引擎将数据存储单元分为多层。按此不表 + +MySQL中的逻辑数据库只是一个shchme。事实上物理数据库只有一个。 + +mysql使用两个文件分别存储数据库的元数据和数据库的真正数据。 +### 数据页page + +数据页结构 +页是 InnoDB 存储引擎管理数据的最小磁盘单位,而 B-Tree 节点就是实际存放表中数据的页面,我们在这里将要介绍页是如何组织和存储记录的;首先,一个 InnoDB 页有以下七个部分: + +![InnoDB-B-Tree-Node](https://raw.githubusercontent.com/Draveness/Analyze/master/contents/Database/images/mysql/InnoDB-B-Tree-Node.jpg) + +每一个页中包含了两对 header/trailer:内部的 Page Header/Page Directory 关心的是页的状态信息,而 Fil Header/Fil Trailer 关心的是记录页的头信息。 + + 也就是说,外部的h-t对用来和其他页形成联系,而内部的h-t用来是保存内部记录的状态。 + +![https://raw.githubusercontent.com/Draveness/Analyze/master/contents/Database/images/mysql/Infimum-Rows-Supremum.jpg](https://raw.githubusercontent.com/Draveness/Analyze/master/contents/Database/images/mysql/Infimum-Rows-Supremum.jpg) + +User Records 就是整个页面中真正用于存放行记录的部分,而 Free Space 就是空余空间了,它是一个链表的数据结构,为了保证插入和删除的效率,整个页面并不会按照主键顺序对所有记录进行排序,它会自动从左侧向右寻找空白节点进行插入,行记录在物理存储上并不是按照顺序的,它们之间的顺序是由 next_record 这一指针控制的。 + + 也就是说,一个页中存了非常多行的数据,而每一行数据和相邻行使用指针进行链表连接。 + +## mysql的索引,b树,聚集索引 + +1 MySQL的innodb支持聚簇索引,myisam不支持聚簇索引。 + +innodb在建表时自动按照第一个非空字段或者主键建立聚簇索引。mysql使用B+树建立索引。 + +每一个非叶子结点只存储主键值,而叶子节点则是一个数据页,这个数据页就是上面所说的存储数据的page页。 + +一个节点页对应着多行数据,每个节点按照顺序使用指针连成一个链表。mysql使用索引访问一行数据时,先通过log2n的时间访问到叶子节点,然后在数据页中按照行数链表执行顺序查找,直到找到那一行数据。 + +2 b+树索引可以很好地支持范围搜索,因为叶子节点通过指针相连。 + +## mysql的explain 慢查询日志 + +explain主要用于检查sql语句的执行计划,然后分析sql是否使用到索引,是否进行了全局扫描等等。 + +mysql慢查询日志可以在mysql的,my.cnf文件中配置开启,然后执行操作超过设置时间就会记录慢日志。 + +比如分析一个sql: + + explain查看执行计划 + + id select_type table partitions type possible_keys key key_len ref rows filtered Extra + + 1 SIMPLE vote_record \N ALL votenum,vote \N \N \N 996507 50.00 Using where + + + + 还是没用到索引,因为不符合最左前缀匹配。查询需要3.5秒左右 + + + + 最后修改一下sql语句 + + EXPLAIN SELECT * FROM vote_record WHERE id > 0 AND vote_num > 1000; + + id select_type table partitions type possible_keys key key_len ref rows filtered Extra + + 1 SIMPLE vote_record \N range PRIMARY,votenum,vote PRIMARY 4 \N 498253 50.00 Using where + + + + 用到了索引,但是只用到了主键索引。再修改一次 + + + + EXPLAIN SELECT * FROM vote_record WHERE id > 0 AND vote_num = 1000; + + + + id select_type table partitions type possible_keys key key_len ref rows filtered Extra + + 1 SIMPLE vote_record \N index_merge PRIMARY,votenum,vote votenum,PRIMARY 8,4 \N 51 100.00 Using intersect(votenum,PRIMARY); Using where + + + + 用到了两个索引,votenum,PRIMARY。 + +## mysql的binlog,redo log和undo log。 + +binlog就是二进制日志,用于记录用户数据操作的日志。用于主从复制。 + +redolog负责事务的重做,记录事务中的每一步操作,记录完再执行操作,并且在数据刷入磁盘前刷入磁盘,保证可以重做成功。 + +undo日志负责事务的回滚,记录事务操作中的原值,记录完再执行操作,在事务提交前刷入磁盘,保证可以回滚成功。 + +这两个日志也是实现分布式事务的基础。 + +## mysql的数据类型 + +mysql一般提供多种数据类型,int,double,varchar,tinyint,datatime等等。文本的话有fulltext,mediumtext等。没啥好说的。 + +## mysql的sql优化。 + +sql能优化的点是在有点多。 + +比如基本的,不使用null判断,不使用>< +分页的时候利用到索引,查询的时候注意顺序。 + +如果是基于索引的优化,则要注意索引列是否能够使用到 + + 1 索引列不要使用>< != 以及 null,还有exists等。 + + 2 索引列不要使用聚集函数。 + + 3 如果是联合索引,排在第一位的索引一定要用到,否则后面的也会失效,为什么呢,因为第一列索引不同时才会找第二列,如果没有第一列索引,后续的索引页没有意义。 + + 举个例子。联合索引A,B,C。查询时必须要用到A,但是A的位置无所谓,只要用到就行,A,B,C或者C,B,A都可以。 + + 4 分页时直接limit n 5可能用不到索引,假设索引列是ID,那么我们使用where id > n limit 5就可以实现上述操作了。 + +## MySQL的事务实现和锁 + + +innodb支持行级锁和事务,而myisam只支持表锁,它的所有操作都需要加锁。 + +1 锁 + + 锁可以分为共享锁和排它锁,也叫读锁和写锁。 + + select操作默认不加锁,需要加锁时会用for update加排它锁,或者用in share mode表示加共享锁。 + + 这里的锁都是行锁。 + innodb会使用行锁配合mvcc一同完成事务的实现。 + 并且使用next-key lock来实现可重复读,而不必加表锁或者串行化执行。 + +2 MVCC + + MVCC是多版本控制协议。 + + 通过时间戳来判断先后顺序,并且是无锁的。但是需要额外存一个字段。 + + 读操作比较自己的版本号,自动读取比自己版本号新的版本。不读。 + + 写操作自动覆盖写版本号比自己的版本号早的版本。否则不写。 + + 这样保证一定程度上的一致性。 + + MVCC比较好地支持读多写少的情景。 + + 但是偶尔需要加锁时才会进行加锁。 + +3 事务 + +所以看看innodb如何实现事务的。 + +首先,innodb的行锁是加在索引上的,因为innodb默认有聚簇索引,但实际上的行锁是对整个索引节点进行加锁,锁了该节点所有的行。 + +看看innodb如何实现隔离级别以及解决一致问题 + + 未提交读,会导致脏读,没有并发措施 + + 已提交读,写入时需要加锁,使用行级写锁锁加锁指定行,其他事务就看不到未提交事务的数据了。但是会导致不可重读, + + 可重复读:在原来基础上,在读取行时也需要加行级读锁,这样其他事务不能修改这些数据。就避免了不可重读。 + 但是这样会导致幻读。 + + 序列化:序列化会串行化读写操作来避免幻读,事实上就是事务在读取数据时加了表级读锁。 + +但是实际上。mysql的新版innodb引擎已经解决了幻读的问题,并且使用的是可重复读级别就能解决幻读了。 + +实现的原理是next-key lock。是gap lock的加强版。不会锁住全表,只会锁住被读取行前后的间隙行。 + + + + +## 分库分表 + +分库分表的方案比较多,首先看下分表。 + +当一个大表没办法继续优化的时候,可以使用分表,横向拆分的方案就是把一个表的数据放到多个表中。一般可以按照某个键来分表。比如最常用的id,1-100w放在表一。100w-200w在表二,以此类推。 + +如果是纵向分表,则可以按列拆分,比如用户信息的字段放在一个表,用户使用数据放在另一个表,这其实就是一次性拆表了。 + +分库的话就是把数据表存到多个库中了,和横向分表的效果差不多。 + +如果只是单机的分表分库,其性能瓶颈在于主机。 + +我们需要考虑扩展性,所以需要使用分布式的数据库。 + +==分布式数据库解决方案mycat== + + mycat是一款支持分库分表的数据库中间件,支持单机也支持分布式。 + + 首先部署mycat,mycat的访问方式和一个mysqlserver是类似的。里面可以配置数据库和数据表。 + + 然后在mycat的配置文件中,我们可以指定分片,比如按照id分片,然后在每个分片下配置mysql节点,可以是本地的数据库实例也可以是其他主机上的数据库。 + + 这样的话,每个分片都能找到对应机器上的数据库和表了。 + + 用户连接mycat执行数据库操作,实际上会根据id映射到对应的数据库和表中, + +## 主从复制,读写分离 + +主从复制大法好,为了避免单点mysql宕机和丢失数据,我们一般使用主从部署,主节点将操作日志写入binlog,然后日志文件通过一个连接传给从节点的relaylog。从节点定时去relaylog读取日志,并且执行操作。这样保证了主从的同步。 + +读写分离大法好,为了避免主库的读写压力太大,由于业务以读操作为主,所以主节点一般作为主库,读节点作为从库,从库负责读,主库负责写,写入主库的数据通过日志同步给从库。这样的部署就是读写分离。 + +使用mycat中间件也可以配置读写分离,只需在分片时指定某个主机是读节点还是写节点即可。 + +## 分布式数据库 + +分布式关系数据库无非就是关系数据库的分布式部署方案。 + +真正的分布式数据库应该是nosql数据库,比如基于hdfs的hbase数据库。底层就是分布式的。 + +redis的分布式部署方案也比较成熟。 + +## \ No newline at end of file diff --git a/md/README.md b/md/README.md new file mode 100644 index 0000000..9a81fc5 --- /dev/null +++ b/md/README.md @@ -0,0 +1,27 @@ +[数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践) +
+## 数据结构和算法 +## 操作系统 +## 网络 +## 数据库 +## Java基础 +## Java进阶 +## web和Spring +## 分布式 +## Hadoop +## 工具 +## 编码实践 + +## 后记 +**关于仓库** +本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 +**关于贡献** +笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以发表一个 Issue,表明您想要添加的内容,笔者会及时查看。 +您也可以在 Issues 中发表关于改进本仓库的建议。 +**关于排版** +笔记排版参考@CYC2018 +**关于转载** +本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 +**鸣谢** +[CyC2018](https://github.com/CyC2018) + diff --git "a/md/Redis\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" "b/md/Redis\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" new file mode 100644 index 0000000..2ef3830 --- /dev/null +++ "b/md/Redis\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265\346\200\273\347\273\223.md" @@ -0,0 +1,429 @@ +--- +title: Redis原理与实践总结 +date: 2018-07-08 22:15:12 +tags: + - Redis +categories: + - 后端 + - 技术总结 +--- +# Redis设计与实现学习总结 + +本文主要对Redis的设计和实现原理做了一个介绍很总结,有些东西我也介绍的不是很详细准确,尽量在自己的理解范围内把一些知识点和关键性技术做一个描述。如有错误,还望见谅,欢迎指出。 +这篇文章主要还是参考我之前的技术专栏总结而来的。欢迎查看: +#重新学习Redis +#https://blog.csdn.net/column/details/21877.html + +## 使用和基础数据结构(外观) + +redis的基本使用方式是建立在redis提供的数据结构上的。 + +字符串 +REDIS_STRING (字符串)是 Redis 使用得最为广泛的数据类型,它除了是 SET 、GET 等命令 的操作对象之外,数据库中的所有键,以及执行命令时提供给 Redis 的参数,都是用这种类型 保存的。 + +字符串类型分别使用 REDIS_ENCODING_INT 和 REDIS_ENCODING_RAW 两种编码 + +只有能表示为 long 类型的值,才会以整数的形式保存,其他类型 的整数、小数和字符串,都是用 sdshdr 结构来保存 + +哈希表 +REDIS_HASH (哈希表)是HSET 、HLEN 等命令的操作对象 + +它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_HT 两种编码方式 + +Redis 中每个hash可以存储232-1键值对(40多亿) + +列表 +REDIS_LIST(列表)是LPUSH 、LRANGE等命令的操作对象 + +它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST 这两种方式编码 + +一个列表最多可以包含232-1 个元素(4294967295, 每个列表超过40亿个元素)。 + +集合 +REDIS_SET (集合) 是 SADD 、 SRANDMEMBER 等命令的操作对象 + +它使用 REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT 两种方式编码 + +Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 + +集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员) + +有序集 +REDIS_ZSET (有序集)是ZADD 、ZCOUNT 等命令的操作对象 + +它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_SKIPLIST 两种方式编码 + +不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 + +有序集合的成员是唯一的,但分数(score)却可以重复。 + +集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员) + +下图说明了,外部数据结构和底层实际数据结构是通过realobject来连接的。一个外观类型里面必然存着一个realobject,通过它来访问底层数据结构。 + +![image](https://user-gold-cdn.xitu.io/2017/9/17/2c71cff03efc96d2280d12602cc2aa92?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + + +## 底层数据结构 + +下面讨论redis底层数据结构 + + +1 SDS动态字符串 + +sds字符串是字符串的实现 + +动态字符串是一个结构体,内部有一个buf数组,以及字符串长度,剩余长度等字段,优点是通过长度限制写入,避免缓冲区溢出,另外剩余长度不足时会自动扩容,扩展性较好,不需要频繁分配内存。 + +并且sds支持写入二进制数据,而不一定是字符。 + +2 dict字典 + +dict字典是哈希表的实现。 + +dict字典与Java中的哈希表实现简直如出一辙,首先都是数组+链表组成的结构,通过dictentry保存节点。 + +其中dict同时保存两个entry数组,当需要扩容时,把节点转移到第二个数组即可,平时只使用一个数组。 + +![image](http://zhangtielei.com/assets/photos_redis/redis_dict_structure.png) + +3 压缩链表ziplist + +3.1 ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。 + +3.2 实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。 + +3.3 而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。 + +3.4 另外,ziplist为了在细节上节省内存,对于值的存储采用了变长的编码方式,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。 + +实际上。redis的字典一开始的数据比较少时,会使用ziplist的方式来存储,也就是key1,value1,key2,value2这样的顺序存储,对于小数据量来说,这样存储既省空间,查询的效率也不低。 + +当数据量超过阈值时,哈希表自动膨胀为之前我们讨论的dict。 + +4 quicklist + +quicklist是结合ziplist存储优势和链表灵活性与一身的双端链表。 + +quicklist的结构为什么这样设计呢?总结起来,大概又是一个空间和时间的折中: + +4.1 双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。 + +首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。 + +4.2 ziplist由于是一整块连续内存,所以存储效率很高。 + +但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。 + +![image](http://zhangtielei.com/assets/photos_redis/redis_quicklist_structure.png) + +5 zset +zset其实是两种结构的合并。也就是dict和skiplist结合而成的。dict负责保存数据对分数的映射,而skiplist用于根据分数进行数据的查询(相辅相成) + +6 skiplist + +sortset数据结构使用了ziplist+zset两种数据结构。 + +Redis里面使用skiplist是为了实现sorted set这种对外的数据结构。sorted set提供的操作非常丰富,可以满足非常多的应用场景。这也意味着,sorted set相对来说实现比较复杂。 + +sortedset是由skiplist,dict和ziplist组成的。 + +当数据较少时,sorted set是由一个ziplist来实现的。 +当数据多的时候,sorted + +set是由一个叫zset的数据结构来实现的,这个zset包含一个dict + 一个skiplist。dict用来查询数据到分数(score)的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)。 + + 在本系列前面关于ziplist的文章里,我们介绍过,ziplist就是由很多数据项组成的一大块连续内存。由于sorted set的每一项元素都由数据和score组成,因此,当使用zadd命令插入一个(数据, score)对的时候,底层在相应的ziplist上就插入两个数据项:数据在前,score在后。 + +![image](http://zhangtielei.com/assets/photos_redis/skiplist/redis_skiplist_example.png) + +skiplist的节点中存着节点值和分数。并且跳表是根据节点的分数进行排序的,所以可以根据节点分数进行范围查找。 + +7inset + +inset是一个数字结合,他使用灵活的数据类型来保持数字。 + +![image](http://zhangtielei.com/assets/photos_redis/intset/redis_intset_add_example.png) + +新创建的intset只有一个header,总共8个字节。其中encoding = 2, length = 0。 +添加13, 5两个元素之后,因为它们是比较小的整数,都能使用2个字节表示,所以encoding不变,值还是2。 +当添加32768的时候,它不再能用2个字节来表示了(2个字节能表达的数据范围是-215~215-1,而32768等于215,超出范围了),因此encoding必须升级到INTSET_ENC_INT32(值为4),即用4个字节表示一个元素。 + +8总结 + +sds是一个灵活的字符串数组,并且支持直接存储二进制数据,同时提供长度和剩余空间的字段来保证伸缩性和防止溢出。 + +dict是一个字典结构,实现方式就是Java中的hashmap实现,同时持有两个节点数组,但只使用其中一个,扩容时换成另外一个。 + +ziplist是一个压缩链表,他放弃内存不连续的连接方式,而是直接分配连续内存进行存储,减少内存碎片。提高利用率,并且也支持存储二进制数据。 + +quicklist是ziplist和传统链表的中和形成的链表结果,每个链表节点都是一个ziplist。 + +skiplist一般有ziplist和zset两种实现方法,根据数据量来决定。zset本身是由skiplist和dict实现的。 + +inset是一个数字集合,他根据插入元素的数据类型来决定数组元素的长度。并自动进行扩容。 + +9 他们实现了哪些结构 + +字符串由sds实现 + +list由ziplist和quicklist实现 + +sortset由ziplist和zset实现 + +hash表由dict实现 + +集合由inset实现。 + +![image](https://user-gold-cdn.xitu.io/2017/9/17/2c71cff03efc96d2280d12602cc2aa92?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + + +## redis server结构和数据库redisDb + +1 redis服务器中维护着一个数据库名为redisdb,实际上他是一个dict结构。 + +Redis的数据库使用字典作为底层实现,数据库的增、删、查、改都是构建在字典的操作之上的。 + +2 redis服务器将所有数据库都保存在服务器状态结构redisServer(redis.h/redisServer)的db数组(应该是一个链表)里: + +同理也有一个redis client结构,通过指针可以选择redis client访问的server是哪一个。 + +3 redisdb的键空间 + + typedef struct redisDb { + // 数据库键空间,保存着数据库中的所有键值对 + dict *dict; /* The keyspace for this DB */ + // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳 + dict *expires; /* Timeout of keys with a timeout set */ + // 数据库号码 + int id; /* Database ID */ + // 数据库的键的平均 TTL ,统计信息 + long long avg_ttl; /* Average TTL, just for stats */ + //.. + } redisDb + +这部分的代码说明了,redisdb除了维护一个dict组以外,还需要对应地维护一个expire的字典数组。 + +大的dict数组中有多个小的dict字典,他们共同负责存储redisdb的所有键值对。 + +同时,对应的expire字典则负责存储这些键的过期时间 +![image](https://img-blog.csdn.net/20180701224321524?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2E3MjQ4ODg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) + +4 过期键的删除策略 + +2、过期键删除策略 +通过前面的介绍,大家应该都知道数据库键的过期时间都保存在过期字典里,那假如一个键过期了,那么这个过期键是什么时候被删除的呢?现在来看看redis的过期键的删除策略: + +a、定时删除:在设置键的过期时间的同时,创建一个定时器,在定时结束的时候,将该键删除; + +b、惰性删除:放任键过期不管,在访问该键的时候,判断该键的过期时间是否已经到了,如果过期时间已经到了,就执行删除操作; + +c、定期删除:每隔一段时间,对数据库中的键进行一次遍历,删除过期的键。 + +## redis的事件模型 + +redis处理请求的方式基于reactor线程模型,即一个线程处理连接,并且注册事件到IO多路复用器,复用器触发事件以后根据不同的处理器去执行不同的操作。总结以下客户端到服务端的请求过程 + +总结 + + 远程客户端连接到 redis 后,redis服务端会为远程客户端创建一个 redisClient 作为代理。 + + redis 会读取嵌套字中的数据,写入 querybuf 中。 + + 解析 querybuf 中的命令,记录到 argc 和 argv 中。 + + 根据 argv[0] 查找对应的 recommand。 + + 执行 recommend 对应的执行函数。 + + 执行以后将结果存入 buf & bufpos & reply 中。 + + 返回给调用方。返回数据的时候,会控制写入数据量的大小,如果过大会分成若干次。保证 redis 的相应时间。 + + Redis 作为单线程应用,一直贯彻的思想就是,每个步骤的执行都有一个上限(包括执行时间的上限或者文件尺寸的上限)一旦达到上限,就会记录下当前的执行进度,下次再执行。保证了 Redis 能够及时响应不发生阻塞。 + +## 备份方式 + +快照(RDB):就是我们俗称的备份,他可以在定期内对数据进行备份,将Redis服务器中的数据持久化到硬盘中; + +只追加文件(AOF):他会在执行写命令的时候,将执行的写命令复制到硬盘里面,后期恢复的时候,只需要重新执行一下这个写命令就可以了。类似于我们的MySQL数据库在进行主从复制的时候,使用的是binlog二进制文件,同样的是执行一遍写命令; + +appendfsync同步频率的区别如下图: + +![这里写图片描述](https://img-blog.csdn.net/20170313210401173?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveGxnZW4xNTczODc=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) + +## redis主从复制 + +Redis复制工作过程: + +slave向master发送sync命令。 + +master开启子进程来讲dataset写入rdb文件,同时将子进程完成之前接收到的写命令缓存起来。 + +子进程写完,父进程得知,开始将RDB文件发送给slave。 + +master发送完RDB文件,将缓存的命令也发给slave。 + +master增量的把写命令发给slave。 + + 注意有两步操作,一个是写入rdb的时候要缓存写命令,防止数据不一致。发完rdb后还要发写命令给salve,以后增量发命令就可以了 + +## 分布式锁实现 + +### 使用setnx加expire实现加锁和时限 + + 加锁时使用setnx设置key为1并设置超时时间,解锁时删除键 + + tryLock(){ + SETNX Key 1 + EXPIRE Key Seconds + } + release(){ + DELETE Key + } + +这个方案的一个问题在于每次提交一个Redis请求,如果执行完第一条命令后应用异常或者重启,锁将无法过期,一种改善方案就是使用Lua脚本(包含SETNX和EXPIRE两条命令),但是如果Redis仅执行了一条命令后crash或者发生主从切换,依然会出现锁没有过期时间,最终导致无法释放。 + +### 使用getset加锁和获取过期时间 + +针对锁无法释放问题的一个解决方案基于GETSET命令来实现 + + 思路: + + SETNX(Key,ExpireTime)获取锁 + + 如果获取锁失败,通过GET(Key)返回的时间戳检查锁是否已经过期 + + GETSET(Key,ExpireTime)修改Value为NewExpireTime + + 检查GETSET返回的旧值,如果等于GET返回的值,则认为获取锁成功 + + 注意:这个版本去掉了EXPIRE命令,改为通过Value时间戳值来判断过期 + + +### 2.0的setnx可以配置过期时间。 + + V2.0 基于SETNX + + tryLock(){ + SETNX Key 1 Seconds + } + release(){ + DELETE Key + } +Redis 2.6.12版本后SETNX增加过期时间参数,这样就解决了两条命令无法保证原子性的问题。但是设想下面一个场景: +1. C1成功获取到了锁,之后C1因为GC进入等待或者未知原因导致任务执行过长,最后在锁失效前C1没有主动释放锁 2. C2在C1的锁超时后获取到锁,并且开始执行,这个时候C1和C2都同时在执行,会因重复执行造成数据不一致等未知情况 3. C1如果先执行完毕,则会释放C2的锁,此时可能导致另外一个C3进程获取到了锁 + +流程图如下 +![2.](http://tech.dianwoda.com/content/images/2018/04/unsafe-lock.png) + +### 使用sentx将值设为时间戳,通过lua脚本进行cas比较和删除操作 + + V3.0 + tryLock(){ + SETNX Key UnixTimestamp Seconds + } + release(){ + EVAL( + //LuaScript + if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) + else + return 0 + end + ) + } +这个方案通过指定Value为时间戳,并在释放锁的时候检查锁的Value是否为获取锁的Value,避免了V2.0版本中提到的C1释放了C2持有的锁的问题;另外在释放锁的时候因为涉及到多个Redis操作,并且考虑到Check And Set 模型的并发问题,所以使用Lua脚本来避免并发问题。 + + 如果在并发极高的场景下,比如抢红包场景,可能存在UnixTimestamp重复问题,另外由于不能保证分布式环境下的物理时钟一致性,也可能存在UnixTimestamp重复问题,只不过极少情况下会遇到。 + +### 分布式Redis锁:Redlock + +redlock的思想就是要求一个节点获取集群中N/2 + 1个节点 +上的锁才算加锁成功。 + + +### 总结 +不论是基于SETNX版本的Redis单实例分布式锁,还是Redlock分布式锁,都是为了保证下特性 + + 1. 安全性:在同一时间不允许多个Client同时持有锁 + 2. 活性 + 死锁:锁最终应该能够被释放,即使Client端crash或者出现网络分区(通常基于超时机制) + 容错性:只要超过半数Redis节点可用,锁都能被正确获取和释放 +## 分布式方案 + +1 主从复制,优点是备份简易使用。缺点是不能故障切换,并且不易扩展。 + +2 使用sentinel哨兵工具监控和实现自动切换。 + +3 codis集群方案 + +首先codis使用代理的方式隐藏底层redis,这样可以完美融合以前的代码,不需要更改redis访问操作。 + +然后codis使用了zookeeper进行监控和自动切换。同时使用了redis-group的概念,保证一个group里是一主多从的主从模型,基于此来进行切换。 + +4 redis cluster集群 + +该集群是一个p2p方式部署的集群 + +Redis cluster是一个去中心化、多实例Redis间进行数据共享的集群。 + +每个节点上都保存着其他节点的信息,通过任一节点可以访问正常工作的节点数据,因为每台机器上的保留着完整的分片信息,某些机器不正常工作不影响整体集群的工作。并且每一台redis主机都会配备slave,通过sentinel自动切换。 + +![image](http://7xivgs.com1.z0.glb.clouddn.com/codis02.png) + +## redis事务 + +事务 +MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。事务可以一次执行多个命令, 并且带有以下两个重要的保证: + +事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 + +事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 + +redis事务有一个特点,那就是在2.6以前,事务的一系列操作,如果有的成功有的失败,仍然会提交成功的那部分,后来改为全部不提交了。 + +但是Redis事务不支持回滚,提交以后不能执行回滚操作。 + + 为什么 Redis 不支持回滚(roll back) + 如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。 + + 以下是这种做法的优点: + + Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。 + 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 + + +### redis脚本事务 + +Redis 脚本和事务 +从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。 + +因为脚本功能是 Redis 2.6 才引入的, 而事务功能则更早之前就存在了, 所以 Redis 才会同时存在两种处理事务的方法。 + +redis事务的ACID特性 +在传统的关系型数据库中,尝尝用ACID特质来检测事务功能的可靠性和安全性。 +在redis中事务总是具有原子性(Atomicity),一致性(Consistency)和隔离性(Isolation),并且当redis运行在某种特定的持久化 +模式下,事务也具有耐久性(Durability). + +①原子性 + +事务具有原子性指的是,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。 +但是对于redis的事务功能来说,事务队列中的命令要么就全部执行,要么就一个都不执行,因此redis的事务是具有原子性的。 + +②一致性 + + 事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然一致的。 + ”一致“指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。redis通过谨慎的错误检测和简单的设计来保证事务一致性。 +③隔离性 + + 事务的隔离性指的是,即使数据库中有多个事务并发在执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全 + 相同。 + 因为redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事物进行中断,因此,redis的事务总是以串行 + 的方式运行的,并且事务也总是具有隔离性的 +④持久性 + + 事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保持到永久存储介质里面。 + 因为redis事务不过是简单的用队列包裹起来一组redis命令,redis并没有为事务提供任何额外的持久化功能,所以redis事务的耐久性由redis使用的模式 + 决定 + \ No newline at end of file diff --git "a/md/Spring\344\270\216SpringMVC\346\272\220\347\240\201\350\247\243\346\236\220\346\200\273\347\273\223.md" "b/md/Spring\344\270\216SpringMVC\346\272\220\347\240\201\350\247\243\346\236\220\346\200\273\347\273\223.md" new file mode 100644 index 0000000..1b4fbbb --- /dev/null +++ "b/md/Spring\344\270\216SpringMVC\346\272\220\347\240\201\350\247\243\346\236\220\346\200\273\347\273\223.md" @@ -0,0 +1,87 @@ +--- +title: Spring与SpringMVC源码解析总结 +date: 2018-07-08 22:13:58 +tags: + - Spring +categories: + - 后端 + - 技术总结 +--- +#Spring和SpringMVC源码学习总结 +这篇总结主要是基于我之前Spring和SpringMVC源码系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 + +#更多详细内容可以查看我的专栏文章:#Spring和SpringMVC源码解析 +#https://blog.csdn.net/column/details/21851.html +# Spring和SpringMVC + +Spring是一个框架,除了提供IOC和AOP以外,还加入了web等众多内容。 + +1 IOC:控制反转,改变类实例化的方式,通过xml等配置文件指定接口的实现类,让实现类和代码解耦,通过配置文件灵活调整实现类。 + +2 AOP: 面向切面编程,将切面代码封装,比如权限验证,日志模块等,这些逻辑重复率大,通过一个增强器封装功能,然后定义需要加入这些功能的切面,切面一般用表达式或者注解去匹配方法,可以完成前置和后置的处理逻辑。 + +3 SpringMVC是一个web框架,基于Spring之上,实现了web相关的功能,使用dispatcherservlet作为一切请求的处理入口。通过配置viewresolver解析页面,通过配置管理静态文件,还可以注入其他的配置信息,除此之外,springmvc可以访问spring容器的所有bean。 + + +## Spring源码总结 + +IOC: + +1 Spring的bean容器也叫beanfactory,我们常用的applicationcontext实际上内部有一个listablebeanfactory实际存储bean的map。 + +2 bean加载过程:spring容器加载时先读取配置文件,一般是xml,然后解析xml,找到其中所有bean,依次解析,然后生成每个bean的beandefinition,存在一个map中,根据beanid映射实际bean的map。 + +3 bean初始化:加载完以后,如果不启用懒加载模式,则默认使用单例加载,在注册完bean以后,可以获取到beandefinition信息,然后根据该信息首先先检查依赖关系,如果依赖其他bean则先加载其他bean,然后通过反射的方式即newinstance创建一个单例bean。 + +为什么要用反射呢,因为实现类可以通过配置改变,但接口是一致的,使用反射可以避免实现类改变时无法自动进行实例化。 + +当然,bean也可以使用原型方式加载,使用原型的话,每次创建bean都会是全新的。 + +AOP: + +AOP的切面,切点,增强器一般也是配置在xml文件中的,所以bean容器在解析xml时会找到这些内容,并且首先创建增强器bean的实例。 + +基于上面创建bean的过程,AOP起到了什么作用呢,或者是是否有参与到其中呢,答案是有的。 + +在获得beandefinition的时候,spring容器会检查该bean是否有aop切面所修饰,是否有能够匹配切点表达式的方法,如果有的话,在创建bean之前,会将bean重新封装成一个动态代理的对象。 + +代理类会为bean增加切面中配置的advisor增强器,然后返回bean的时候实际上返回的是一个动态代理对象。 + +所以我们在调用bean的方法时,会自动织入切面的增强器,当然,动态代理既可以选择jdk增强器,也可以选择cglib增强器。 + +Spring事务: + +spring事务其实是一种特殊的aop方式。在spring配置文件中配置好事务管理器和声明式事务注解后,就可以使用@transactional进行事务方法的处理了。 + +事务管理器的bean中会配置基本的信息,然后需要配置事务的增强器,不同方法使用不同的增强器。当然如果使用注解的话就不用这么麻烦了。 + +然后和aop的动态代理方式类似,当Spring容器为bean生成代理时,会注入事务的增强器,其中实际上实现了事务中的begin和commit,所以执行方法的过程实际上就是在事务中进行的。 + +# SpringMVC源码总结 + +1 dispatcherservlet概述 +SpringMVC使用dispatcherservlet作为唯一如果,在web.xml中进行配置,他继承自frameworkservlet,向上继承自httpservletbean。 + +httpservletbean为dispatcherservlet加载了来自web.xml配置信息中的信息,保存在servletcontext上下文中,而frameworkservletbean则初始化了spring web的bean容器。 + +这个容器一般是配置在spring-mvc.xml中的,他独立于spring容器,但是把spring容器作为父容器,所以SpringMVC可以访问spring容器中的各种类。 + +而dispatcherservlet自己做了什么呢,因为springmvc中配置了很多例如静态文件目录,自动扫描bean注解,以及viewresovler和httpconverter等信息,所以它需要初始化这些策略,如果没有配置则会使用默认值。 + +2 dispatcherservlet的执行流程 + +首先web容器会加载指定扫描bean并进行初始化。 + +当请求进来后,首先执行service方法,然后到dodispatch方法执行请求转发,事实上,spring web容器已经维护了一个map,通过注解@requestmapping映射到对应的bean以及方法上。通过这个map可以获取一个handlerchain,真正要执行的方法被封装成一个handler,并且调用方法前要执行前置的一些过滤器。 + +最终执行handler方法时实际上就是去执行真正的方法了。 + +3 viewresolver + +解析完请求和执行完方法,会把modelandview对象解析成一个view对象,让后使用view.render方法执行渲染,至于使用什么样的视图解析器,就是由你配置的viewresolver来决定的,一般默认是jspviewresolver。 + +4 httpmessageconverter + +一般配合responsebody使用,可以将数据自动转换为json和xml,根据http请求中适配的数据类型来决定使用哪个转换器。 + + diff --git "a/md/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\256\236\350\267\265\346\200\273\347\273\223.md" "b/md/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\256\236\350\267\265\346\200\273\347\273\223.md" new file mode 100644 index 0000000..e12ce80 --- /dev/null +++ "b/md/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\256\236\350\267\265\346\200\273\347\273\223.md" @@ -0,0 +1,474 @@ +--- +title: 分布式技术实践总结 +date: 2018-07-08 22:15:36 +tags: + - 分布式系统 +categories: + - 后端 + - 技术总结 +--- +##本文基于之前的分布式系统理论系列文章总结而成,本部分主要是实践内容,详细内容可见我的专栏:分布式系统理论与实践 + +##https://blog.csdn.net/column/details/24090.html + +本文主要是按照我自己的理解以及参考之前文章综合而成的,其中可能会有一些错误,还请见谅,也请指出。 + +# 分布式技术 + +## 分布式数据和nosql + +分布式一般是指分布式部署的数据库。 + +比如Hbase基于HDFS分布式部署,所以他是一个分布式数据库。 + +当然MySQL也可以分布式部署,比如按照不同业务部署,或者把单表内容拆成多个表乃至多个库进行部署。 + +一般MySQL的扩展方式有: + +1 主从复制 使用冗余保证可用 + +2 读写分离 主库负责写从库负责读,分担压力,并且保证数据一致性和备份。 + +3 分表分库,横向拆分数据表放到多个表中或者多个库中,一般多个表或者多个库会使用不同节点部署,也就是一种分布式方案,提高并发的读写量。 + +Nosql的话就比较多了,redis,memcache等。 +当然hbase也是,hbase按照region将数据文件分布在hdfs上,并且hdfs提供高可用和备份,同时hbase的regionserver也保证高可用,于是hbase的分布式方案也是比较成熟的。 + +## 缓存 分布式缓存 + +一般作为缓存的软件有redis,memcache等。当然我本地写一个hashmap也可以作为缓存。 + + +memcache提出了一致性哈希的算法,但是本身不支持数据持久化,也没有提供分布式方案,需要自己完成持久化以及分布式部署并且保证其可用性。 + +redis作为新兴的内存数据库,提供了比memcache更多的数据结构,以及各种分布式方案。当然它也支持持久化。 + +### redis的部署方案: + +> 1 redis的主从复制结构,和MySQL类似,使用日志aof或者持久化文件rdb进行主从同步。 +> +> 2 读写分离,也可以做,但一般不需要。因为redis够快。 +> +> 3 redis的哨兵方案,主节点配置哨兵,每当宕机时自动完成主从切换。 +> +> 4 redis的集群方案,p2p的Redis Cluster部署了多台Redis服务器,每台Redis拥有全局的分片信息,所以任意节点都可以对外提供服务,当然每个节点只保存一部分分片,所以某台机器宕机时不会影响整个集群,当然每个节点也有slave,哨兵自动进行故障切换。 +> +> 5 codis方案,codis屏蔽了集群的内部实现,可以不更改redis api的情况下使用代理的方式提供集群访问。并且使用 group的概念封装一组节点。 + +### 缓存需要解决的问题: + + 命中:缓存有数据 + 不命中:去数据库读取 + 失效:过期 + 替换:缓存淘汰算法。 + + 一般有lru,fifo,随机缓存等。 + +### 缓存更新的方法 + +缓存更新可以先更新数据库再更新缓存,也可以先更新缓存再更新数据库。 + +一般推荐先更新数据库,否则写一条数据时刚好有人读到缓存,把旧数据读到缓存中,此时新数据在数据库确不在缓存中。 + + +还有一种方法,就是让缓存自己去完成数据库更新,而不是让应用去选择如何更新数据库,这样的话缓存和数据库的更新操作就是透明的了,我们只需要操作缓存即可。 + +### 缓存在springboot中的使用 + + springboot支持将缓存的curd操作配置在注解中,只需要在对应方法上配置好键和更新策略。 + + 则redis会根据该方法的操作类型执行对应操作,非常方便。 +### 一致性哈希 + +分布式部署时,经常要面对的问题是,一个服务集群由谁来提供给这个客户度服务,需要一种算法来完成这一步映射。 + +如果直接使用hash显然分布非常不均匀。那如果使用余数法呢,一共有N台机器,我对N取余可以映射到任意一台机器上。 + +这种方法的缺点在于,当取余的值集中在某一范围时,就容易集中访问某些机器,导致热点问题。 + +> 于是memcache推出了一个叫做一致性哈希的算法,一个哈希环,环上支持2^32次方个节点,也就是包含了所有的ip。 +> +> 然后我们把主机通过hash值分布到这个环上,请求到来时会映射到某一个节点,如果该节点没有主机,则顺时针寻找真正主机。 +> +> 当节点加入或者节点删除时,并不会影响服务的可用性,只是某些请求会被映射到别的节点。 + +但是当请求集中到某个区域时,会产生倾斜,我们引入了虚拟节点来改善这个问题,虚拟节点对应到真实节点,所以加入虚拟节点可以更好地转移请求。 + + +## session和分布式session + +session是web应用必备的一个结构。 +一般有几种方案来管理session。 + +1 web应用保存session到内存中,但是宕机会丢失 + +2 web应用持久化到数据库或者redis,增加数据库负担。 + +3 使用cookie保存加密后的session,浏览器压力大,可能被破解 + +4 使用单独的session服务集群提供session服务,并且本身也可以采用分布式部署,部署的时候可以主从。 + +保证session一致性的解决方法(客户端可以访问到自己的session): + +1 客户端cookie保存 + +2 多个webserver进行同步,效率低 + +3 反向代理绑定ip映射同一个服务器,但是宕机时出错 + +4 后端统一存储,比如redis,或则部署session服务。 + +## 负载均衡 + +负载均衡一般可以分为七层,四层负载均衡。 + +Nginx + +七层的负载均衡也就是http负载均衡,主要使用Nginx完成。 + +> 配置Nginx进行反向代理的url,然后转发请求到上游服务器,请求进来时自动转发到上游服务器,通过url进行负载均衡,所以是七层负载均衡。既然是七层负载,那么上游服务器提供了http服务,也可以解析该请求。 +> +> 四层负载均衡主要是tcp请求的负载均衡,因为tcp请求是绑定到一个端口上的,所以我们根据端口进行请求转发到上游服务器的。既然是四层负载,上游服务器监听该端口的服务就可以处理该请求。 + +LVS + +LVS术语定义: + + DS:Director Server,前端负载均衡器节点(后文用Director称呼); + + RS:Real Server,后端真实服务器; + + VIP:用户请求的目标的IP地址,一般是公网IP地址; + + DIP:Director Server IP,Director和Real Server通讯的内网IP地址; + + RIP:Real Server IP,Director和Real Server通讯的内网IP地址; + +LVS有三种实现负载均衡的方式 + +NAT 四层负载均衡 + +> NAT支持四层负载均衡,NAT中只有DS提供公网ip,并且VIP绑定在DS的mac地址上,客户端只能访问DS。同时DS和RS通过内网ip进行网络连接。当TCP数据报到达DS时,DS修改数据报,指向RS的ip和port。进行转发即可。 +> +> 同时,RS处理完请求后,由于网关时DS,所以仍然要返回给DS处理。 +> +> +> NAT模式中,RS返回数据包是返回给Director,Director再返回给客户端;事实上这跟NAT网络协议没什么关系。 + +DR 二层负载均衡 + + +> DR模式中,DS负责接收请求。接收请求后把数据报的mac地址改成指向RS的mac地址,并且由于三台机器拥有同样的vip地址。 +> 所以RS接收请求后认为该数据报应该由自己处理并相应。 +> +> 同时为了避免RS再把相应转发会DS,我们禁用了对DS的arp,所以此时RS就会通过vip把响应通过vip网关返回给客户端。 +> +> Director通过修改请求中目标地址MAC为选定的RS实现数据转发,这就要求Diretor和Real Server必须在同一个广播域内,也就是他们的mac地址是可达的。 +> DR(Direct Routing)模式中,RS返回数据是直接返回给客户端(通过额外的路由); +> + +TUN + +> TUN中使用了IP隧道技术,客户端请求发给DS时,DS会通过隧道技术把数据报通过隧道发给实际的RS,然后RS解析数据以后可以直接响应给客户端,因为他有客户端的ip地址。这就不要求DS和RS在同一网段了,当然前提是RS有公网ip。 +> +> TUN(IP Tunneling)模式中,RS返回的数据也是直接返回给客户端,这种模式通过Overlay协议(把一个IP数据包封装到另一个数据包内部叫Overlay)避免了DR的限制。 + +## zookeeper + +zookeeper集群自身的特性: + +> 1 一个zookeeper服务器集群,一开始就会进行选主,主节点挂掉后也会进行选主。 +> +> 使用zab协议中的选主机制进行选主,也就是每个节点进行一次提议,刚开始提议自己,如果有新的提议则覆盖自己原来的提议,不断重复,直到有节点获得过半的投票。完成一轮选主。 +> +> 2 选主结束后,开始进行消息广播和数据同步,保证每一台服务器的数据都和leader同步。 +> +> 3 开始提供服务,客户端向leader发送请求,leader首先发出提议,当有半数以上节点响应时,leader会发送commit信息,于是所有节点执行该操作。当有机器宕机时重启后会和leader同步。这是一个类似2pc的提交方式。 + +zookeeper提供了分布式环境中常用的服务 + +> 1 配置服务,多个机器可以通过文件节点共享配置。 +> +> 2 选主服务,通过添加顺序节点,可以进行选主。 +> +> 3 分布式锁,顺序节点和watcher +> +> 4 全局id,使用机器号+时间戳可以生成一个transactionid,是全局唯一的。 + + +## 数据库的分布式事务 + +分布式事务的实现一般可以用2PC和3PC解决。 + +成熟的方案有: + +> 1 TCC 补偿式事务,对每一个步骤都有一个补偿措施。 +> +> 2 全局事务实现。 +> +> 3 事务消息:rocketmq的事务实现,先发消息到队列中,然后本地执行事务并通知消息队列,若成功则消息主动推给另一个服务,直到服务二执行成功,消息从队列中删除。如果超时不成功,则消息要求事务A回滚。 +> +> 如果过程中失败了,本地事务也会回滚。消息队列可以回调本地接口判断事务是否执行成功,防止超时。 +> +> 4 本地实现消息表: +> 本地实现消息表并且和事务记录存在一起,自己实现消息的轮询发送。 +> 首先把本地事务操作和消息增加放在一个事务里执行,然后轮询消息表进行发送,如果执行成功则消息达到服务B,通知其执行。执行成功后消息被删除,否则回滚事务删除消息。 + +## 分布式锁问题 + +分布式锁用于分布式环境中的资源互斥,因为单机可以通过共享内存实现,而分布式环境只能通过网络实现。 + +### MySQL实现分布式锁 + + insert加锁,锁没有失效时间,容易产生死锁 + +### redis实现分布式锁 + + 1. 基于setnx、expire两个命令来实现 + + 基于setnx(set if not exist)的特点,当缓存里key不存在时,才会去set,否则直接返回false。 + + 如果返回true则获取到锁,否则获取锁失败,为了防止死锁,我们再用expire命令对这个key设置一个超时时间来避免。 + + 但是这里看似完美,实则有缺陷,当我们setnx成功后,线程发生异常中断,expire还没来的及设置,那么就会产生死锁。 + + 2 使用getset实现,可以判断自己是否获得了锁,但是可能会出现并发的原子性问题。拆分成两个操作。 + + 3 避免原子性问题可以使用lua脚本保证事务的原子性。 + + 4 上述都是单点的redis,如果是分布式环境的redis集群,可以使用redlock,要求节点向半数以上redis机器请求锁。才算成功。 + +### zookeeper实现分布式锁 + +创建有序节点,最小的抢到锁,其他的监听他的上一个节点即可。并且抢到锁的节点释放时只会通知下一个节点。 + +小结 + +在分布式系统中,共享资源互斥访问问题非常普遍,而针对访问共享资源的互斥问题,常用的解决方案就是使用分布式锁,这里只介绍了几种常用的分布式锁,分布式锁的实现方式还有有很多种,根据业务选择合适的分布式锁,下面对上述几种锁进行一下比较: + + 数据库锁: + + 优点:直接使用数据库,使用简单。 + + 缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。 + + 缓存锁: + + 优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。 + + 缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失效了锁的作用。 + + zookeeper锁: + + 优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。 + + 缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。并且zookeeper只能单点写入。而Redis可以并发写入。 + +## 消息队列 + +适合场景: + +1 服务之间解耦,比如淘宝的买家服务和物流服务,中间需要消息传递订单信息。但又不需要强耦合。便于服务的划分和独立部署 + +2 控制流量,大流量访问某服务时,避免服务出现问题,将其先存入队列,均匀释放流量。 + +3 削峰,当某一个服务如秒杀,如果直接集中访问,服务器可能会冲垮,所以先存到队列中,控制访问量,避免服务器冲击。 + +4 事务,消息事务 + +5 异步请求处理,比如一些不重要的服务可以延缓执行,比如卖家评价,站内信等。 + +常用消息队列: + +rabbitmq:使用consumer和producer的模型,并且使用了broker,broker中包含路由功能的exchanger,每个key绑定一个queue,应用通过key进行队列消费和生产。 + +一般是点对点的消息,也可以支持一对多的消息,当然也可以支持消息的订阅。还有就是主题模式,和key的区别就是主题模式是多级的key表示。 + +kafka: + + + +## 微服务和Dubbo + +分布式架构意味着服务的拆分,最早的SOA架构已经进行了服务拆分,但是每个服务还是太过庞大,不适合扩展和修改。 + +微服务的拆分粒度更加细,服务可以独立部署和快速迭代,通知支持扩展。 + +服务之间一般使用rpc调用进行访问,可以使用自定义协议也可以使用http服务,当然通过netty 实现TCP服务并且搭配合理的序列化方案也可以完成rpc功能。rpc是微服务的基础。 + +微服务一般需要配置中心来进行服务注册和发现,以便服务信息更新和配置,dubbo中使用的是zookeeper,用于配置服务信息提供给生产者使用。 + +一般情况下微服务需要有监控中心,心跳检测每一台服务器,及时完成故障切换和通知。同时监控服务的性能和使用情况。 + +序列化方式一般可以使用protobuf,http服务一般使用json。 + +微服务还支持更多的包括权限控制,流量控制,灰度发布,服务降级等内容,这里就不再细谈。 + + +### 全局id + +方法一:使用数据库的 auto_increment 来生成全局唯一递增ID + + + + 优点: + + 简单,使用数据库已有的功能 + + 能够保证唯一性 + + 能够保证递增性 + + 步长固定 + + + + 缺点: + + 可用性难以保证:数据库常见架构是一主多从+读写分离,生成自增ID是写请求,主库挂了就玩不转了 + + 扩展性差,性能有上限:因为写入是单点,数据库主库的写性能决定ID的生成性能上限,并且难以扩展 + + +方法三:uuid/guid + + + +不管是通过数据库,还是通过服务来生成ID,业务方Application都需要进行一次远程调用,比较耗时。 + + + +有没有一种本地生成ID的方法,即高性能,又时延低呢? + + + +uuid是一种常见的方案: + +string ID =GenUUID(); + + + +优点: + +本地生成ID,不需要进行远程调用,时延低 + +扩展性好,基本可以认为没有性能上限 + + + +缺点: + +无法保证趋势递增 + +uuid过长,往往用字符串表示,作为主键建立索引查询效率低,常见优化方案为“转化为两个uint64整数存储”或者“折半存储”(折半后不能保证唯一性) + + + + +方法四:取当前毫秒数 + + + +uuid是一个本地算法,生成性能高,但无法保证趋势递增,且作为字符串ID检索效率低,有没有一种能保证递增的本地算法呢? + + + +取当前毫秒数是一种常见方案: + +uint64 ID = GenTimeMS(); + + + +优点: + +本地生成ID,不需要进行远程调用,时延低 + +生成的ID趋势递增 + +生成的ID是整数,建立索引后查询效率高 + + + +缺点: + +如果并发量超过1000,会生成重复的ID + + +方法五:类snowflake算法 + + + +snowflake是twitter开源的分布式ID生成算法,其核心思想为,一个long型的ID: + +41bit作为毫秒数 + +10bit作为机器编号 + +12bit作为毫秒内序列号 + + + +算法单机每秒内理论上最多可以生成1000*(2^12),也就是400W的ID,完全能满足业务的需求。 + +## 秒杀系统 + +![image](http://i2.51cto.com/images/blog/201803/11/9eda905930f00090d55b5ae3f6796d2b.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) + + +### 第一层,客户端怎么优化(浏览器层,APP层) + +(a)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求; + +(b)JS层面,限制用户在x秒之内只能提交一次请求; + +### 第二层,站点层面的请求拦截 + +> 怎么拦截?怎么防止程序员写for循环调用,有去重依据么?ip?cookie-id?…想复杂了,这类业务都需要登录,用uid即可。在站点层面,对uid进行请求计数和去重,甚至不需要统一存储计数,直接站点层内存存储(这样计数会不准,但最简单)。一个uid,5秒只准透过1个请求,这样又能拦住99%的for循环请求。 +> +> 5s只透过一个请求,其余的请求怎么办?缓存,页面缓存,同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面。同一个item的查询,例如车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面。如此限流,既能保证用户有良好的用户体验(没有返回404)又能保证系统的健壮性(利用页面缓存,把请求拦截在站点层了)。 + +好,这个方式拦住了写for循环发http请求的程序员,有些高端程序员(黑客)控制了10w个肉鸡,手里有10w个uid,同时发请求(先不考虑实名制的问题,小米抢手机不需要实名制),这下怎么办,站点层按照uid限流拦不住了。 + + +### 第三层 服务层来拦截(反正就是不要让请求落到数据库上去)消息队列+缓存 + +服务层怎么拦截?大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?没错,请求队列! + +对于写请求,做请求队列,每次只透有限的写请求去数据层(下订单,支付这样的写业务) + +1w部手机,只透1w个下单请求去db + +3k张火车票,只透3k个下单请求去db + +如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”。 + + + +对于读请求,怎么优化?cache抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的。如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。 + + +### 好了,最后是数据库层 + +浏览器拦截了80%,站点层拦截了99.9%并做了页面缓存,服务层又做了写请求队列与数据缓存,每次透到数据库层的请求都是可控的。db基本就没什么压力了,闲庭信步,单机也能扛得住,还是那句话,库存是有限的,小米的产能有限,透这么多请求来数据库没有意义。 + +全部透到数据库,100w个下单,0个成功,请求有效率0%。透3k个到数据,全部成功,请求有效率100%。 + + +![image](http://i2.51cto.com/images/blog/201803/11/bf7107f82e635020a43f12aa4a8dc856.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) + +### 总结 +上文应该描述的非常清楚了,没什么总结了,对于秒杀系统,再次重复下我个人经验的两个架构优化思路: + +(1)尽量将请求拦截在系统上游(越上游越好); + +(2)读多写少的常用多使用缓存(缓存抗读压力); + +浏览器和APP:做限速 + +站点层:按照uid做限速,做页面缓存 + +服务层:按照业务做写请求队列控制流量,做数据缓存 + +数据层:闲庭信步 + +并且:结合业务做优化 \ No newline at end of file diff --git "a/md/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272\346\200\273\347\273\223.md" "b/md/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272\346\200\273\347\273\223.md" new file mode 100644 index 0000000..78577df --- /dev/null +++ "b/md/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272\346\200\273\347\273\223.md" @@ -0,0 +1,355 @@ +--- +title: 分布式理论学习总结 +date: 2018-07-08 22:15:23 +tags: + - 分布式系统 +categories: + - 后端 + - 技术总结 +--- +##本文基于之前的分布式系统理论系列文章总结而成,主要是理论部分,详细内容可见我的专栏:分布式系统理论与实践 + +##https://blog.csdn.net/column/details/24090.html + +本文主要是按照我自己的理解以及参考之前文章综合而成的,其中可能会有一些错误,还请见谅,也请指出。 + +# 分布式理论 +## CAP +CAP定理讲的是三个性。consistency数据一致性,availability可用性,partition tolerance分区容错性。 + +三者只能选其中两者。为什么呢,看看这三个性质意味着什么吧。 + +首先看看分区容错性,分区容错性指的是网络出现分区(丢包,断网,超时等情况都属于网络分区)时,整个服务仍然可用。 + +由于网络分区在实际环境下一定存在,所以必须首先被考虑。于是分区容错性是必须要保证的,否则一旦出现分区服务就不可用,那就没办法弄了。 + +所以实际上是2选1的问题。在可用性和一致性中做出选择。 + +在一个分布式环境下,多个节点一起对外提供服务,如果要保证可用性,那么一台机器宕机了仍然有其他机器能提供服务。 +但是宕机的机器重启以后就会发现数据和其他机器存在不一致,那么一致性就无法得到保证。 + +如果保证一致性,如果有机器宕机,那么其他节点就不能工作了,否则一定会产生数据不一致。 + +## BASE + +在这么严苛的规定下,CAP一般很难实现一个健壮的系统。于是提出了BASE来削弱这些要求。 + +BASE是基本可用basically available,soft state软状态,eventually consistent最终一致性。 + +基本可用就是允许服务在某些时候降级,比如淘宝在高峰时期会关闭退货等服务。 + +软状态就是允许数据出现中间状态,比如支付宝提交转账以后并不是立刻到账,中间经过了多次消息传递和转发。 + +最终一致性就是指数据最终要是一致的,比如多个节点的数据需要定期同步,支付宝转账最终一定会到账。 + +## 分布式系统关键词 + +### 时钟,时间,事件顺序 + +分布式系统的一个问题在与缺少全局时钟,所以大家没有一个统一的时间,就很难用时间去确定各个节点事件的发生顺序,为了保证事件的顺序执行, + + +### Lamport timestamps + +Leslie Lamport 在1978年提出逻辑时钟的概念,并描述了一种逻辑时钟的表示方法,这个方法被称为Lamport时间戳(Lamport timestamps)[3]。 + +![image](https://images2015.cnblogs.com/blog/116770/201605/116770-20160501174922566-1686627384.png) + +分布式系统中按是否存在节点交互可分为三类事件,一类发生于节点内部,二是发送事件,三是接收事件。Lamport时间戳原理如下: + + 每个事件对应一个Lamport时间戳,初始值为0 + 如果事件在节点内发生,时间戳加1 + 如果事件属于发送事件,时间戳加1并在消息中带上该时间戳 + 如果事件属于接收事件,时间戳 = Max(本地时间戳,消息中的时间戳) + 1 + +这样的话,节点内的事件有序,发送事件有序,接收事件一定在发送事件以后发生。再加上人为的一些规定,因此根据时间戳可以确定一个全序排列。 + +### Vector clock + +Lamport时间戳帮助我们得到事件顺序关系,但还有一种顺序关系不能用Lamport时间戳很好地表示出来,那就是同时发生关系(concurrent)[4]。 +Vector clock是在Lamport时间戳基础上演进的另一种逻辑时钟方法,它通过vector结构不但记录本节点的Lamport时间戳,同时也记录了其他节点的Lamport时间戳[5][6]。 + +![image](https://images2015.cnblogs.com/blog/116770/201605/116770-20160502134654404-1109556515.png) + +如果 Tb[Q] > Ta[Q] 并且 Tb[P] < Ta[P],则认为a、b同时发生,记作 a <-> b。例如图2中节点B上的第4个事件 (A:2,B:4,C:1) 与节点C上的第2个事件 (B:3,C:2) 没有因果关系、属于同时发生事件。 + +因为B4 > B3并且 C1 选举(election)是分布式系统实践中常见的问题,通过打破节点间的对等关系,选得的leader(或叫master、coordinator)有助于实现事务原子性、提升决议效率。 +> +> 多数派(quorum)的思路帮助我们在网络分化的情况下达成决议一致性,在leader选举的场景下帮助我们选出唯一leader。 +> +> 租约(lease)在一定期限内给予节点特定权利,也可以用于实现leader选举。 + +选举(electioin) + +> 一致性问题(consistency)是独立的节点间如何达成决议的问题,选出大家都认可的leader本质上也是一致性问题,因而如何应对宕机恢复、网络分化等在leader选举中也需要考量。 +> +> 在一致性算法Paxos、ZAB[2]、Raft[3]中,为提升决议效率均有节点充当leader的角色。 +> +> ZAB、Raft中描述了具体的leader选举实现,与Bully算法类似ZAB中使用zxid标识节点,具有最大zxid的节点表示其所具备的事务(transaction)最新、被选为leader。 + +多数派(quorum) + +> 在网络分化的场景下以上Bully算法会遇到一个问题,被分隔的节点都认为自己具有最大的序号、将产生多个leader,这时候就需要引入多数派(quorum)[4]。多数派的思路在分布式系统中很常见,其确保网络分化情况下决议唯一。 + + 租约(lease) + +选举中很重要的一个问题,以上尚未提到:怎么判断leader不可用、什么时候应该发起重新选举? +> +> 最先可能想到会通过心跳(heart beat)判别leader状态是否正常,但在网络拥塞或瞬断的情况下,这容易导致出现双主。 + + + +租约(lease)是解决该问题的常用方法,其最初提出时用于解决分布式缓存一致性问题[6],后面在分布式锁[7]等很多方面都有应用。 + + (a). 节点0、1、2在Z上注册自己,Z根据一定的规则(例如先到先得)颁发租约给节点,该租约同时对应一个有效时长;这里假设节点0获得租约、成为leader + + (b). leader宕机时,只有租约到期(timeout)后才重新发起选举,这里节点1获得租约、成为leader + + + +租约机制确保了一个时刻最多只有一个leader,避免只使用心跳机制产生双主的问题。在实践应用中,zookeeper、ectd可用于租约颁发。 + +## 一致性,2pc和3pc + +一致性(consensus) + +何为一致性问题?简单而言,一致性问题就是相互独立的节点之间如何达成一项决议的问题。分布式系统中,进行数据库事务提交(commit transaction)、Leader选举、序列号生成等都会遇到一致性问题。 + +为了保证执行的一致性,可以使用2pc两段式提交和3pc三段式提交。 + +2PC +> +> 2PC(tow phase commit)两阶段提交[5]顾名思义它分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts): + +举个例子,首先用户想要执行一个事务,于是提交给leader,leader先让各个节点执行该事务。 + +我们要知道,事务是通过日志来实现的。各个节点使用redo日志进行重做,使用undo日志进行回滚。 + +于是各个节点执行事务,并把执行结果是否成功返回给leader,当leader收到全部确认消息后,发送消息让所有节点commit。如果有节点执行失败,则leader要求所有节点回滚。 + +2pc可能出现的一些问题是: + +1 leader必须等待所有节点结果,如果有节点宕机或超时,则拒绝该事务,并向节点发送回滚的信息。 + +2 如果leader宕机,则一般配置watcherdog自动切换成备用leader,然后进行下一次的请求提交。 + +3这两种情况单独发生时都没有关系,有对应的措施可以进行回滚,但是如果当一个节点宕机时leader正在等待所有节点消息,其他节点也在等待leader最后的消息。 + +此时leader也不幸宕机,切换之后leader并不知道一个节点宕机了,这样的话其他的节点也会被阻塞住导致无法回滚。 +> +3PC +![image](https://images2015.cnblogs.com/blog/116770/201603/116770-20160314002734304-489496391.png) +coordinator接收完participant的反馈(vote)之后,进入阶段2,给各个participant发送准备提交(prepare to commit)指令 + +。participant接到准备提交指令后可以锁资源,但要求相关操作必须可回滚。coordinator接收完确认(ACK)后进入阶段3、进行commit/abort,3PC的阶段3与2PC的阶段2无异。协调者备份(coordinator watchdog)、状态记录(logging)同样应用在3PC。 + +participant如果在不同阶段宕机,我们来看看3PC如何应对: + +> 阶段1: coordinator或watchdog未收到宕机participant的vote,直接中止事务;宕机的participant恢复后,读取logging发现未发出赞成vote,自行中止该次事务 +> +> 阶段2: coordinator未收到宕机participant的precommit ACK,但因为之前已经收到了宕机participant的赞成反馈(不然也不会进入到阶段2),coordinator进行commit;watchdog可以通过问询其他participant获得这些信息,过程同理;宕机的participant恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务 +> +> 阶段3: 即便coordinator或watchdog未收到宕机participant的commit ACK,也结束该次事务;宕机的participant恢复后发现收到commit或者precommit,也将自行commit该次事务 +> 因为有了准备提交(prepare to +commit)阶段,3PC的事务处理延时也增加了1个RTT,变为3个RTT(propose+precommit+commit),但是它防止participant宕机后整个系统进入阻塞态,增强了系统的可用性,对一些现实业务场景是非常值得的。 +> + +总结一下就是:阶段一leader要求节点准备,节点返回ack或者fail。 + +如果节点都是ack,leader返回ack进入阶段二。 +(如果fail则回滚,因为节点没有接收到ack,所以最终都会回滚) + +阶段二时节点执行事务并且发送结果给leader,leader返回ack或者fail。由于阶段二的节点已经有了一个确定的状态ack,如果leader超时或宕机不返回,成功执行节点也会进行commit操作,这样即使有节点宕机也不会影响到其他节点。 + + +## 一致性算法paxos + +Basic Paxos + +何为一致性问题?简单而言,一致性问题是在节点宕机、消息无序等场景可能出现的情况下,相互独立的节点之间如何达成决议的问题,作为解决一致性问题的协议,Paxos的核心是节点间如何确定并只确定一个值(value)。 + +和2PC类似,Paxos先把节点分成两类,发起提议(proposal)的一方为proposer,参与决议的一方为acceptor。假如只有一个proposer发起提议,并且节点不宕机、消息不丢包,那么acceptor做到以下这点就可以确定一个值。 + +> proposer发出提议,acceptor根据提议的id和值来决定是否接收提议,接受提议则替换为自己的提议,并且返回之前id最大的提议,当超过一半节点提议该值时,则该值被确定,这样既保证了时序,也保证了多数派。 + + Multi Paxos + +通过以上步骤分布式系统已经能确定一个值,“只确定一个值有什么用?这可解决不了我面临的问题。” 你心中可能有这样的疑问。 + +其实不断地进行“确定一个值”的过程、再为每个过程编上序号,就能得到具有全序关系(total order)的系列值,进而能应用在数据库副本存储等很多场景。我们把单次“确定一个值”的过程称为实例(instance),它由proposer/acceptor/learner组成。 + +Fast Paxos + +在Multi Paxos中,proposer -> leader -> acceptor -> learner,从提议到完成决议共经过3次通信,能不能减少通信步骤? + +对Multi Paxos phase2a,如果可以自由提议value,则可以让proposer直接发起提议、leader退出通信过程,变为proposer -> acceptor -> learner,这就是Fast Paxos[2]的由来。 + +> 多次paxos的确定值使用可以让多个proposer,acceptor一起运作。多个proposer提出提议,acceptor保留最大提议比返回之前提议,proposer当提议数量满足多数派则取出最大值向acceptor提议,于是过半数的acceptor比较提议后可以接受该提议,于是最终leader将提议写入acceptor,而acceptor再写入对应的learner。 + +## raft和zab + +Zab + +Zab[5][6]的全称是Zookeeper atomic broadcast protocol,是Zookeeper内部用到的一致性协议。相比Paxos,Zab最大的特点是保证强一致性(strong consistency,或叫线性一致性linearizable consistency)。 + + + +和Raft一样,Zab要求唯一Leader参与决议,Zab可以分解成discovery、sync、broadcast三个阶段: + +![image](https://images2015.cnblogs.com/blog/116770/201610/116770-20161025133734734-658183229.jpg) +> +> discovery: 选举产生PL(prospective leader),PL收集Follower epoch(cepoch),根据Follower的反馈PL产生newepoch(每次选举产生新Leader的同时产生新epoch,类似Raft的term) + +> sync: PL补齐相比Follower多数派缺失的状态、之后各Follower再补齐相比PL缺失的状态,PL和Follower完成状态同步后PL变为正式Leader(established leader) + +> broadcast: Leader处理Client的写操作,并将状态变更广播至Follower,Follower多数派通过之后Leader发起将状态变更落地(deliver/commit) + +Raft: + +## 单个 Candidate 的竞选 + +有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。 + +* 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/111521118015898.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/111521118015898.gif) + +* 此时 A 发送投票请求给其它所有节点。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/111521118445538.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/111521118445538.gif) + +* 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/111521118483039.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/111521118483039.gif) + +* 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/111521118640738.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/111521118640738.gif) + +## [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7.md#%E5%A4%9A%E4%B8%AA-candidate-%E7%AB%9E%E9%80%89)多个 Candidate 竞选 + +* 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/111521119203347.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/111521119203347.gif) + +* 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/111521119368714.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/111521119368714.gif) + +## [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7.md#%E6%97%A5%E5%BF%97%E5%A4%8D%E5%88%B6)日志复制 + +* 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/7.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/7.gif) + +* Leader 会把修改复制到所有 Follower。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/9.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/9.gif) + +* Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/10.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/10.gif) + +* 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/11.gif)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/11.gif) + +## zookeeper + +zookeeper在分布式系统中作为协调员的角色,可应用于Leader选举、分布式锁、配置管理等服务的实现。以下我们从zookeeper供的API、应用场景和监控三方面学习和了解zookeeper(以下简称ZK)。 + + +ZK API + +ZK以Unix文件系统树结构的形式管理存储的数据,图示如下: + + + +其中每个树节点被称为znode,每个znode类似一个文件,包含文件元信息(meta data)和数据。 + + + +以下我们用server表示ZK服务的提供方,client表示ZK服务的使用方,当client连接ZK时,相应创建session会话信息。 + +有两种类型的znode: + +Regular: 该类型znode只能由client端显式创建或删除 + +Ephemeral: client端可创建或删除该类型znode;当session终止时,ZK亦会删除该类型znode + +znode创建时还可以被打上sequential标志,被打上该标志的znode,将自行加上自增的数字后缀 + + + + ZK提供了以下API,供client操作znode和znode中存储的数据: + + create(path, data, flags):创建路径为path的znode,在其中存储data[]数据,flags可设置为Regular或Ephemeral,并可选打上sequential标志。 + + delete(path, version):删除相应path/version的znode + + exists(path,watch):如果存在path对应znode,则返回true;否则返回false,watch标志可设置监听事件 + + getData(path, watch):返回对应znode的数据和元信息(如version等) + + setData(path, data, version):将data[]数据写入对应path/version的znode + + getChildren(path, watch):返回指定znode的子节点集合 + +K应用场景 + +基于以上ZK提供的znode和znode数据的操作,可轻松实现Leader选举、分布式锁、配置管理等服务。 + + + +Leader选举 + +> 利用打上sequential标志的Ephemeral,我们可以实现Leader选举。假设需要从三个client中选取Leader,实现过程如下: +> +> 1、各自创建Ephemeral类型的znode,并打上sequential标志: +> +> [zk: localhost:2181(CONNECTED) 4] ls /master +> [lock-0000000241, lock-0000000243, lock-0000000242] +> +> 2、检查 /master 路径下的所有znode,如果自己创建的znode序号最小,则认为自己是Leader;否则记录序号比自己次小的znode +> +> 3、非Leader在次小序号znode上设置监听事件,并重复执行以上步骤2 + +配置管理 + +znode可以存储数据,基于这一点,我们可以用ZK实现分布式系统的配置管理,假设有服务A,A扩容设备时需要将相应新增的ip/port同步到全网服务器的A.conf配置,实现过程如下: +> +> 1、A扩容时,相应在ZK上新增znode,该znode数据形式如下: +> +> [zk: localhost:2181(CONNECTED) 30] get /A/blk-0000340369 +> {"svr_info": [{"ip": "1.1.1.1.", "port": "11000"}]} +> cZxid = 0x2ffdeda3be +> …… +> +> 2、全网机器监听 /A,当该znode下有新节点加入时,调用相应处理函数,将服务A的新增ip/port加入A.conf +> +> 3、完成步骤2后,继续设置对 /A监听 + +ZK监控 + +ZK自身提供了一些“四字命令”,通过这些四字命令,我们可以获得ZK集群中,某台ZK的角色、znode数、健康状态等信息: + + +小结 + +zookeeper以目录树的形式管理数据,提供znode监听、数据设置等接口,基于这些接口,我们可以实现Leader选举、配置管理、命名服务等功能。结合四字命令,加上模拟zookeeper client 创建/删除znode,我们可以实现对zookeeper的有效监控。在各种分布式系统中,我们经常可以看到zookeeper的身影。 \ No newline at end of file From bf2d18ea6bb5beaceb43313f3bdb64c0009e37ae Mon Sep 17 00:00:00 2001 From: 724888 <362294931@qq.com> Date: Sun, 8 Jul 2018 23:01:25 +0800 Subject: [PATCH 14/27] =?UTF-8?q?=E4=BF=AE=E6=94=B91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...00\346\234\257\346\200\273\347\273\223.md" | 369 ++++++++++++------ 1 file changed, 254 insertions(+), 115 deletions(-) diff --git "a/md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" "b/md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" index 2eba287..973818b 100644 --- "a/md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" +++ "b/md/Java\346\240\270\345\277\203\346\212\200\346\234\257\346\200\273\347\273\223.md" @@ -1,136 +1,275 @@ --- -title: JAVA后端开发学习之路 # 文章页面上的显示名称,可以任意修改,不会出现在URL中 -date: 2018-4-20 15:56:26 # 文章生成时间,一般不改 -categories: - - 个人总结 +title: Java核心技术学习总结 +date: 2018-05-02 22:37:47 tags: - - 心路历程 + - Java基础 +categories: + - 后端 + - Java基础 --- -本文主要记录了我从Java初学者到专注于Java后端技术栈的开发者的学习历程。主要分享了学习过程中的一些经验和教训,让后来人看到,少走弯路,与君共勉,共同进步。如有错误,还请见谅。 - -我的GitHub: -> https://github.com/h2pl/MyTech - -喜欢的话麻烦点下星哈 - -文章首发于我的个人博客: -> https://h2pl.github.io/2018/04/20/java - -更多关于Java后端学习的内容请到我的CSDN博客上查看: - -https://blog.csdn.net/a724888 +本文主要是我最近复习Java基础原理过程中写的Java基础学习总结。Java的知识点其实非常多,并且有些知识点比较难以理解,有时候我们自以为理解了某些内容,其实可能只是停留在表面上,没有理解其底层实现原理。 -相关链接:我和技术博客的这一年:https://blog.csdn.net/a724888/article/details/60879893 ->  不论你是不是网民,无论你远离互联网,还是沉浸其中;你的身影,都在这场伟大的迁徙洪流中。超越人类经验的大迁徙,温暖而无情地,开始了。 -                                 -----《互联网时代》 +纸上得来终觉浅,绝知此事要躬行。笔者之前对每部分的内容 +对做了比较深入的学习以及代码实现,基本上比较全面地讲述了每一个Java基础知识点,当然可能有些遗漏和错误,还请读者指正。 -## 选择方向 +**这里先把整体的学习大纲列出来,让大家对知识框架有个基本轮廓,具体每个部分的内容,笔者都对应写了一篇博文来加以讲解和剖析,并且发表在我的个人博客和csdn技术专栏里,下面给出地址** -  0上大学前的那些事,让它们随风逝去吧。 -  1 个人对计算机和互联网有情怀,有兴趣,本科时在专业和学校里选择了学校,当时专业不是计算机,只能接触到一点点计算机专业课程,所以选择了考研,花半年时间复习考进了一个还不错的985,考研经历有空会发到博客上。 +专栏:深入理解Java原理 -  2 本科阶段接触过Java和Android,感觉app蛮有趣的,所以研一的时候想做Android,起初花大量时间看了计算机专业课的教材,效果很差。但也稍微了解了一些计算机基础,如网络,操作系统,组成原理,数据库,软工等。 +https://blog.csdn.net/column/details/21930.html -  3 在没确定方向的迷茫时期看了大量视频和科普性文章,帮助理清头绪和方向。期间了解了诸如游戏开发,c++开发,Android,Java甚至前端等方向,其中还包含游戏策划岗。 +相关代码实现在我的GitHub里: -  4 后来综合自身条件以及行业发展等因素,开始锁定自己的目标在Java后台方向。于是乎各种百度,知乎,查阅该学什么该怎么学如此类的问题,学习别人的经验。当然只靠搜索引擎很难找到精品内容,那段时间可谓是病急乱投医,走了不少弯路。 - ---- - -## 夯实基础 - -  1 研一的工程实践课让我知道了我的基础不够扎实,由于并非科班,需要比别人更加勤奋,古语有云,天道酬勤,勤能补拙。赶上了17年的春招实习招聘,期间开始各种海投,各种大厂面试一问三不知,才知道自身差距很大,开始疯狂复习面试题,刷面经,看经验等。死记硬背,之乎者也,倒也是能应付一些小公司,可谓是临阵磨枪不快也光。 - -  2 不过期间的屡屡受挫让我冷静思考了一段时间,我再度调研了岗位需求,学习方法,以及需要看的书等资料。再度开工时,我的桌上开始不断出现新的经典书籍。这还要归功于我的启蒙导师:江南白衣,在知乎上看到了他的一篇文章,我的Java后端书架。在这个书架里我找寻到了很多我想看的书,以及我需要学习的技术。 - -  3 遥想研一我还在看的书:教材就不提了,脱离实际并且年代久远,而我选的入门书籍竟然还有Java web从入门到精通这种烂大街的书籍,然后就是什么Java编程思想啦,深入理解计算机系统,算法导论这种高深莫测的书,感觉有点高不成低不就的意思。要么太过难懂要么过于粗糙,这些书在当时基本上没能帮到我。 - ---- +https://github.com/h2pl/MyTech -## 书籍选择 +**喜欢的话麻烦star一下哈** -  1 江南白衣的后端书架真是救我于水火。他的书架里收录了许多Java后端需要用到的技术书籍,并且十分经典,虽不说每本都适合入门,但是只要你用心去看都会有收获,高质量的书籍给人的启发要优于普通书籍。 +本系列技术文章首发于我的个人博客: -  2 每个门类的书我都挑了一些。比如网络的两本(《tcp ip卷一》以及《计算机网络自顶向下》),操作系统两本(一本《Linux内核设计与实现》,一本高级操作系统,推荐先看完《深入理解计算机系统》再来看这两本),算法看的是《数据结构与算法(Java版)》,Java的四大件(《深入理解jvm虚拟机》,《java并发编程艺术》,《深入java web技术内幕》,《Java核心技术 卷一》这本没看)。 +https://h2pl.github.io -  3 当然还有像《Effective Java》,《Java编程思想》,《Java性能调优指南》这种,不过新手不推荐,太不友好。接着是spring的两本《Spring实战》和《Spring源码剖析》。当然也包括一些redis,mq之类的书,还有就是一些介绍分布式组件的书籍,如zk等。 - -  4 接下来就是扩展的内容了,比如分布式的三大件,《大型网站架构设计与实践》,《分布式网站架构设计与实践》,《Java中间件设计与实践》,外加一本《分布式服务框架设计与实践》。这几本书一看,绝对让你打开新世界的大门,醍醐灌顶,三月不知肉味。 - -  5 你以为看完这些书你就无敌了,就满足了?想得倒是挺美。这些书最多就是把我从悬崖边拉回正途,能让我在正确的道路上行走了。毕竟技术书籍这种东西还是有门槛的,没有一定的知识储备,看书的过程也绝对是十分痛苦的。 - -    6 比如《深入理解jvm虚拟机》和《java并发编程艺术》这两本书,我看了好几遍,第一遍基本当天书来看,第二遍挑着章节看,第三遍能把全部章节都看了。所以有时候你觉得你看完了一本书,对,你确实看完了,但过段时间是你能记得多少呢。可以说是很少了。 - ---- - -## 谈一谈学习方法 - -  1 人们在刚开始接触自己不熟悉的领域时,往往都会犯很多错误。刚开始学习Java时,就是摸着石头过河。从在极客学院慕课上看视频,到看书,再到看博客,再到工程实践,也是学习方式转变的一个过程。 - -  2 看视频:适合0基础小白,视频给你构建一个世界观,让你对你要做的东西有个大概的了解,想要深入理解其中的技术原理,只看视频的话很难。 - -  3 看书:就如上面一节所说,看书是一个很重要的环节。当你对技术只停留在大概的了解和基本会用的阶段时,经典书籍能够让你深入这些技术的原理,你可能会对书里的内容感到惊叹,也可能只是一知半解。所以第一遍的阅读一般读个大概就可以。一本书要吃透,不仅要看好几遍,还要多上手实践,才能变成自己的东西。 - -  4 看博客,光看一些总结性的博客或者是科普性的博客可能还不够,一开始我也经常看这样的博客,后来只看这些东西,发现对技术的理解只能停留在表面。高质量的博客一般会把一个知识点讲得很透彻,比你看十篇总结都强,例如讲jdk源码的博文,可以很好地帮助你理解其原理,避免自己看的时候一脸懵逼。这里先推荐几个博客和网站,后面写复习计划的时候,会详细写出。 -博客:江南白衣、酷壳、战小狼。 -网站:并发编程网,importnew。 - -  5 实践为王,Java后端毕竟还是工程方向,只是通过文字去理解技术点,可能有点纸上谈兵的感觉了。还有一个问题就是,没有进行上手实践的技术,一般很快就会忘了,做一些实践可以更好地巩固知识点。如果有项目中涉及不到的知识点,可以单独拿出来做一些demo,实在难以进行实践的技术点,可以参考别人的实践过程。 +更多关于Java后端学习的内容请到我的CSDN博客上查看: ---- +https://blog.csdn.net/a724888 -## 实习,提高工程能力的好机会 +## Java基础学习总结 + +每部分内容会重点写一些常见知识点,方便复习和记忆,但是并不是全部内容,详细的内容请参见具体的文章地址。 + +### 面向对象三大特性 + + 继承:一般类只能单继承,内部类实现多继承,接口可以多继承 + + 封装:访问权限控制public > protected > 包 > private 内部类也是一种封装 + + 多态:编译时多态,体现在向上转型和向下转型,通过引用类型判断调用哪个方法(静态分派)。 + + 运行时多态,体现在同名函数通过不同参数实现多种方法(动态分派)。 + +### 基本数据类型 + + 基本类型位数,自动装箱,常量池 + + 例如byte类型是1byte也就是8位,可以表示的数字是-128到127,因为还有一个0,加起来一共是256,也就是2的八次方。 + + 32位和64位机器的int是4个字节也就是32位,char是1个字节就是8位,float是4个字节,double是8个字节,long是8个字节。 + + 所以它们占有字节数是相同的,这样的话两个版本才可以更好地兼容。(应该) + + 基本数据类型的包装类只在数字范围-128到127中用到常量池,会自动拆箱装箱,其余数字范围的包装类则会新建实例 + +### String及包装类 + + String类型是final类型,在堆中分配空间后内存地址不可变。 + + 底层是final修饰的char[]数组,数组的内存地址同样不可变。 + + 但实际上可以通过修改char[n] = 'a'来进行修改,不会改变String实例的内存值,不过在jdk中,用户无法直接获取char[],也没有方法能操作该数组。 + 所以String类型的不可变实际上也是理论上的不可变。所以我们在分配String对象以后,如果将其 = "abc",那也只是改变了引用的指向,实际上没有改变原来的对象。 + + StringBuffer和StringBuilder底层是可变的char[]数组,继承父类AbstractStringBuilder的各种成员和方法,实际上的操作都是由父类方法来完成的。 + +### final关键字 + + final修饰基本数据类型保证不可变 + + final修饰引用保证引用不能指向别的对象,否则会报错。 + + final修饰类,类的实例分配空间后地址不可变,子类不能重写所有父类方法。因此在cglib动态代理中,不能为一个类的final修饰的函数做代理,因为cglib要将被代理的类设置为父类,然后再生成字节码。 + + final修饰方法,子类不能重写该方法。 + +### 抽象类和接口 + + 1 抽象类可以有方法实现。 + 抽象类可以有非final成员变量。 + 抽象方法要用abstract修饰。 + 抽象类可以有构造方法,但是只能由子类进行实例化。 + + 2 接口可以用extends加多个接口实现多继承。 + 接口只能有public final类型的成员变量。 + 接口只能有抽象方法,不能有方法体、 + 接口不能实例化,但是可以作为引用类型。 + +### 代码块和加载顺序 + + 假设该类是第一次进行实例化。那么有如下加载顺序 + 静态总是比非静态优先,从早到晚的顺序是: + 1 静态代码块 和 静态成员变量的顺序根据代码位置前后来决定。 + 2 代码块和成员变量的顺序也根据代码位置来决定 + 3 最后才调用构造方法构造方法 +### 包、内部类、外部类 + 1 Java项目一般从src目录开始有com.*.*.A.java这样的目录结构。这就是包结构。所以一般编译后的结构是跟包结构一模一样的,这样的结构保证了import时能找到正确的class引用包访问权限就是指同包下的类可见。 + + import 一般加上全路径,并且使用.*时只包含当前目录的所有类文件,不包括子目录。 + + 2 外部类只有public和default两种修饰,要么全局可访问,要么包内可访问。 + + 3 内部类可以有全部访问权限,因为它的概念就是一个成员变量,所以访问权限设置与一般的成员变量相同。 + + 非静态内部类是外部类的一个成员变量,只跟外部类的实例有关。 + + 静态内部类是独立于外部类存在的一个类,与外部类实例无关,可以通过外部类.内部类直接获取Class类型。 + +### 异常 + + 1 异常体系的最上层是Throwable类 + 子类有Error和Exception + Exception的子类又有RuntimeException和其他具体的可检查异常。 + + 2 Error是jvm完全无法处理的系统错误,只能终止运行。 + + 运行时异常指的是编译正确但运行错误的异常,如数组越界异常,一般是人为失误导致的,这种异常不用try catch,而是需要程序员自己检查。 + + 可检查异常一般是jvm处理不了的一些异常,但是又经常会发生,比如Ioexception,Sqlexception等,是外部实现带来的异常。 + + 3 多线程的异常流程是独立的,互不影响。 + 大型模块的子模块异常一般需要重新封装成外部异常再次抛出,否则只能看到最外层异常信息,难以进行调试。 + + 日志框架是异常报告的最好帮手,log4j,slf4j中,在工作中必不可少。 + +### 泛型 + + Java中的泛型是伪泛型,只在编译期生效,运行期自动进行泛型擦除,将泛型替换为实际上传入的类型。 + + 泛型类用class A { + + }这样的形式表示,里面的方法和成员变量都可以用T来表示类型。泛型接口也是类似的,不过泛型类实现泛型接口时可以选择注入实际类型或者是继续使用泛型。 + + 泛型方法可以自带泛型比如void E go(); + + 泛型可以使用?通配符进行泛化 Object可以接受任何类型 + + 也可以使用 这种方式进行上下边界的限制。 + +### Class类和Object类 + + Java反射的基础是Class类,该类封装所有其他类的类型信息,并且在每个类加载后在堆区生成每个类的一个Class<类名>实例,用于该类的实例化。 + + Java中可以通过多种方式获取Class类型,比如A.class,new A().getClass()方法以及Class.forName("com.?.?.A")方法。 + + Object是所有类的父类,有着自己的一些私有方法,以及被所有类继承的9大方法。 + + 有人讨论Object和Class类型谁先加载谁后加载,因为每个类都要继承Object,但是又得先被加载到堆区,事实上,这个问题在JVM初始化时就解决了,没必要多想。 + +### javac和java + + javac 是编译一个java文件的基本命令,通过不同参数可以完成各种配置,比如导入其他类,指定编译路径等。 + + java是执行一个java文件的基本命令,通过参数配置可以以不同方式执行一个java程序或者是一个jar包。 + + javap是一个class文件的反编译程序,可以获取class文件的反编译结果,甚至是jvm执行程序的每一步代码实现。 + + + +### 反射 + + Java反射包reflection提供对Class,Method,field,constructor等信息的封装类型。 + + 通过这些api可以轻易获得一个类的各种信息并且可以进行实例化,方法调用等。 + + 类中的private参数可以通过setaccessible方法强制获取。 + + 反射的作用可谓是博大精深,JDK动态代理生成代理类的字节码后,首先把这个类通过defineclass定义成一个类,然后用class.for(name)会把该类加载到jvm,之后我们就可以通过,A.class.GetMethod()获取其方法,然后通过invoke调用其方法,在调用这个方法时,实际上会通过被代理类的引用再去调用原方法。 -  1 这段时间以后就是实习期了,三个月的W厂实习经历。半年的B厂实习,让我着实过了一把大厂的瘾。但是其中做的工作无非就是增删改查写写业务逻辑,很难接触到比较核心的部分。 - -  2 于是乎我花了许多时间学习部门的核心技术。比如在W厂参与数据平台的工作时,我学习了hadoop以及数据仓库的架构,也写了一些博客,并且向负责后端架构的导师请教了许多知识,收获颇丰。 - -  3 在B厂实习期间则接触了许多云计算相关的技术。因为部门做的是私有云,所以业务代码和底层的服务也是息息相关的,比如平时的业务代码也会涉及到底层的接口调用,比如新建一个虚拟机或者启动一台虚拟机,需要通过多级的服务调用,首先是HTTP服务调用,经过多级的服务调用,最终完成流程。在这期间我花了一些时间学习了OpenStack的架构以及部门的实际应用情况,同时也玩了一下docker,看了kubenetes的一些书籍,算是入门。 - -  4 但是这些东西其实离后台开发还是有一定距离的,比如后台开发的主要问题就是高并发,分布式,Linux服务器开发等。而我做的东西,只能稍微接触到这一部门的内容,因为主要是to b的内部业务。所以这段时间其实我的进步有限,虽然扩大了知识面并且积累了开发经验,但是对于后台岗位来说还是有所欠缺的。 - -  5 不过将近一年的实习也让我收获了很多东西,大厂的实习体验很好,工作高效,团队合作,版本的快速迭代,技术氛围很不错。特别是在B厂了可以解到很多前沿的技术,对自己的视野扩展很有帮助。 - ---- - -## **实习转正,还是准备秋招?** - -  1 离职以后,在考虑是否还要找实习,因为有两份实习经历了,在考虑要不要静下心来刷刷题,复习一下基础,并且回顾一下实习时用到的技术。同一时期,我了解到腾讯和阿里等大厂的实习留用率不高,并且可能影响到秋招,所以当时的想法是直接复习等到秋招内推。因此,那段时间比较放松,没什么复习状态,也导致了我在今年春招内推的阶段比较艰难。 - -  2 因为当时想着沉住气准备秋招,所以一开始对实习内推不太在意。但是由于AT招人的实习生转正比例较大,考虑到秋招的名额可能更少,所以还是不愿意错过这个机会。因为开始系统复习的时间比较晚,所以投的比较晚,担心准备不充分被刷。这次找实习主要是奔着转正去的,所以只投了bat和滴滴,京东,网易游戏等大厂。 - -  3 由于投递时间原因,所以面试的流程特别慢。并且在笔试方面还是有所欠缺,刷题刷的比较少,在线编程的算法题还是屡屡受挫。这让我有点后悔实习结束后的那段时间没有好好刷题了。 - ---- - -## **调整心态,重新上路** - -  1 目前的状态是,一边刷题,一边复习基础,投了几家大厂的实习内推,打算选一个心仪的公司准备转正,但是事情总是没那么顺利,微软,头条等公司的笔试难度超过了我的能力范围,没能接到面试电话。腾讯投了一个自己比较喜欢的部门,可惜岗位没有匹配上,后台开发被转成了运营开发,最终没能通过。阿里面试的也不顺利,当时投了一个牛客上的蚂蚁金服内推,由于投的太晚,部门已经招满,只面了一面就没了下文,前几天接到了菜鸟的面试,这个未完待续。 - -  2 目前的想法是,因为我不怎么需要实习经历来加分了,所以想多花些时间复习基础,刷题,并且巩固之前的项目经历。当然如果有好的岗位并且转正机会比较大的话,也是会考虑去实习的,那样的话可能需要多挤点时间来复习基础和刷题了。 - -  3 在这期间,我会重新梳理一下自己的复习框架,有针对性地看一些高质量的博文,同时多做些项目实践,加深对知识的理解。当然这方面还会通过写博客进行跟进,写博客,做项目。前阵子在牛客上看到一位牛友CyC2018做的名为interview notebook的GitHub仓库,内容非常好,十分精品,我全部看完了,并且参考其LeetCode题解进行刷题。 - -  4 受到这位大佬的启发,我也打算做一个类似的代码仓库或者是博客专栏,尽量在秋招之前把总结做完,并且把好的文章都放进去。上述内容只是本人个人的心得体会,如果有错误或者说的不合理的地方,还请谅解和指正。希望与广大牛友共勉,一起进步。 - - - - - - - - - - - - - - - - - +### 枚举类 + + 枚举类继承Enum并且每个枚举类的实例都是唯一的。 + + 枚举类可以用于封装一组常量,取值从这组常量中取,比如一周的七天,一年的十二个月。 + + 枚举类的底层实现其实是语法糖,每个实例可以被转化成内部类。并且使用静态代码块进行初始化,同时保证内部成员变量不可变。 + +### 序列化 + + 序列化的类要实现serializable接口 + + transient修饰符可以保证某个成员变量不被序列化 + + readObject和writeOject来实现实例的写入和读取。 + + 待更新。 + + 事实上,一些拥有数组变量的类都会把数组设为transient修饰,这样的话不会对整个数组进行序列化,而是利用专门的方法将有数据的数组范围进行序列化,以便节省空间。 + +### 动态代理 + + jdk自带的动态代理可以代理一个已经实现接口的类。 + + cglib代理可以代理一个普通的类。 + + 动态代理的基本实现原理都是通过字节码框架动态生成字节码,并且在用defineclass加载类后,获取代理类的实例。 + + 一般需要实现一个代理处理器,用来处理被代理类的前置操作和后置操作。在JDK动态代理中,这个类叫做invocationHandler。 + + JDK动态代理首先获取被代理类的方法,并且只获取在接口中声明的方法,生成代理类的字节码后,首先把这个类通过defineclass定义成一个类,然后把该类加载到jvm,之后我们就可以通过,A.class.GetMethod()获取其方法,然后通过invoke调用其方法,在调用这个方法时,实际上会通过被代理类的引用再去调用原方法。 + + 而对于cglib动态代理,一般会把被代理类设为代理类的父类,然后获取被代理类中所有非final的方法,通过asm字节码框架生成代理类的字节码,这个代理类很神奇,他会保留原来的方法以及代理后的方法,通过方法数组的形式保存。 + + cglib的动态代理需要实现一个enhancer和一个interceptor,在interceptor中配置我们需要的代理内容。如果没有配置interceptor,那么代理类会调用被代理类自己的方法,如果配置了interceptor,则会使用代理类修饰过的方法。 + + +### 多线程 + 这里先不讲juc包里的多线程类。juc相关内容会在Java并发专题讲解。 + + 线程的实现可以通过继承Thread类和实现Runable接口 + 也可以使用线程池。callable配合future可以实现线程中的数据获取。 + + Java中的线程有7种状态,new runable running blocked waiting time_waiting terminate + blocked是线程等待其他线程锁释放。 + waiting是wait以后线程无限等待其他线程使用notify唤醒 + time_wating是有限时间地等待被唤醒,也可能是sleep固定时间。 + + Thread的join是实例方法,比如a.join(b),则说明a线程要等b线程运行完才会运行。 + + o.wait方法会让持有该对象o的线程释放锁并且进入阻塞状态,notify则是持有o锁对象的线程通知其他等待锁的线程获取锁。notify方法并不会释放锁。注意这两个方法都只能在synchronized同步方法或同步块里使用。 + + synchronized方法底层使用系统调用的mutex锁,开销较大,jvm会为每个锁对象维护一个等待队列,让等待该对象锁的线程在这个队列中等待。当线程获取不到锁时则让线程阻塞,而其他检查notify以后则会通知任意一个线程,所以这个锁时非公平锁。 + + Thread.sleep(),Thread.interrupt()等方法都是类方法,表示当前调用该方法的线程的操作。 + + + 一个线程实例连续start两次会抛异常,这是因为线程start后会设置标识,如果再次start则判断为错误。 + +### IO流 + + IO流也是Java中比较重要的一块,Java中主要有字节流,字符流,文件等。其中文件也是通过流的方式打开,读取和写入的。 + + IO流的很多接口都使用了装饰者模式,即将原类型通过传入装饰类构造函数的方式,增强原类型,以此获得像带有缓冲区的字节流,或者将字节流封装成字符流等等,其中需要注意的是编码问题,后者打印出来的结果可能是乱码哦。 + + IO流与网络编程息息相关,一个socket接入后,我们可以获取它的输入流和输出流,以获取TCP数据包的内容,并且可以往数据报里写入内容,因为TCP协议也是按照流的方式进行传输的,实际上TCP会将这些数据进行分包处理,并且通过差错检验,超时重传,滑动窗口协议等方式,保证了TCP数据包的高效和可靠传输。 + +### 网络编程 + + 承接IO流的内容 + + IO流与网络编程息息相关,一个socket接入后,我们可以获取它的输入流和输出流,以获取TCP数据包的内容,并且可以往数据报里写入内容,因为TCP协议也是按照流的方式进行传输的,实际上TCP会将这些数据进行分包处理,并且通过差错检验,超时重传,滑动窗口协议等方式,保证了TCP数据包的高效和可靠传输。 + + 除了使用socket来获取TCP数据包外,还可以使用UDP的DatagramPacket来封装UDP数据包,因为UDP数据包的大小是确定的,所以不是使用流方式处理,而是需要事先定义他的长度,源端口和目标端口等信息。 + + 为了方便网络编程,Java提供了一系列类型来支持网络编程的api,比如URL类,InetAddress类等。 + + 后续文章会带来NIO相关的内容,敬请期待。 + + + + +### Java8 + + 接口中的默认方法,接口终于可以有方法实现了,使用注解即可标识出默认方法。 + + lambda表达式实现了函数式编程,通过注解可以声明一个函数式接口,该接口中只能有一个方法,这个方法正是使用lambda表达式时会调用到的接口。 + + Option类实现了非空检验 + + 新的日期API + + 各种api的更新,包括chm,hashmap的实现等 + + Stream流概念,实现了集合类的流式访问,可以基于此使用map和reduce并行计算。 + From 8dd2a615f14370b731047ce9f0707ba81e7c29c3 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Sun, 8 Jul 2018 23:11:10 +0800 Subject: [PATCH 15/27] Update README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 250a91f..b7618cc 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,38 @@ ## 数据库 :floppy_disk: +> [Mysql原理与实践总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +> [Redis原理与实践总结]https://github.com/h2pl/Java-Tutorial/blob/master/md/Redis%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md ## Java :couple: +> [Java核心技术总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.md) + +> [Java集合类总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E9%9B%86%E5%90%88%E7%B1%BB%E6%80%BB%E7%BB%93.md) + +> [Java并发技术总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E5%B9%B6%E5%8F%91%E6%80%BB%E7%BB%93.md) + +> [JVM原理学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/JVM%E6%80%BB%E7%BB%93.md) + +> [Java网络与NIO总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E7%BD%91%E7%BB%9C%E4%B8%8ENIO%E6%80%BB%E7%BB%93.md) + ## JavaWeb :coffee: +> [JavaWeb技术学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/JavaWeb%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.md) + +> [Spring与SpringMVC源码解析](https://github.com/h2pl/Java-Tutorial/blob/master/md/Spring%E4%B8%8ESpringMVC%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E6%80%BB%E7%BB%93.md) + ## 分布式 :sweat_drops: +> [分布式理论学习总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA%E6%80%BB%E7%BB%93.md) + +> [分布式技术学习总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) ## 设计模式 :hammer: ## Hadoop :speak_no_evil: +> [分布式技术学习总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/Hadoop%E7%94%9F%E6%80%81%E6%80%BB%E7%BB%93.md) +
## 后记 From 3d912288cd1f37c0db9ef7ca5d20ec0d341d7150 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Sun, 8 Jul 2018 23:15:05 +0800 Subject: [PATCH 16/27] Update README.md --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b7618cc..a667311 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,10 @@ ## 数据库 :floppy_disk: -> [Mysql原理与实践总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) +> [Mysql原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +> [Redis原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Redis%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) -> [Redis原理与实践总结]https://github.com/h2pl/Java-Tutorial/blob/master/md/Redis%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md ## Java :couple: > [Java核心技术总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.md) @@ -32,15 +33,16 @@ > [Spring与SpringMVC源码解析](https://github.com/h2pl/Java-Tutorial/blob/master/md/Spring%E4%B8%8ESpringMVC%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E6%80%BB%E7%BB%93.md) ## 分布式 :sweat_drops: -> [分布式理论学习总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA%E6%80%BB%E7%BB%93.md) -> [分布式技术学习总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) +> [分布式理论学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA%E6%80%BB%E7%BB%93.md) + +> [分布式技术学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) ## 设计模式 :hammer: ## Hadoop :speak_no_evil: -> [分布式技术学习总结] (https://github.com/h2pl/Java-Tutorial/blob/master/md/Hadoop%E7%94%9F%E6%80%81%E6%80%BB%E7%BB%93.md) +> [Hadoop生态学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Hadoop%E7%94%9F%E6%80%81%E6%80%BB%E7%BB%93.md)
From 32b3f5c52225f1e3c40f1cfb771a80c31359ab95 Mon Sep 17 00:00:00 2001 From: 724888 <362294931@qq.com> Date: Mon, 9 Jul 2018 22:43:24 +0800 Subject: [PATCH 17/27] add new md --- README.md | 44 ++++++++----------- ...37\346\200\201\346\200\273\347\273\223.md" | 1 - md/README.md | 27 ------------ 3 files changed, 19 insertions(+), 53 deletions(-) delete mode 100644 md/README.md diff --git a/README.md b/README.md index 250a91f..9a81fc5 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,27 @@ -| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | -| :------: | :---------: | :-------: | :---------: | :---: | :---------:| :---------: | :---------: | :---------:| -| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| - -## 算法 :pencil2: - -## 操作系统 :computer: - -## 网络 :cloud: - -## 数据库 :floppy_disk: - -## Java :couple: - -## JavaWeb :coffee: - -## 分布式 :sweat_drops: - -## 设计模式 :hammer: - -## Hadoop :speak_no_evil: - +[数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践)
+## 数据结构和算法 +## 操作系统 +## 网络 +## 数据库 +## Java基础 +## Java进阶 +## web和Spring +## 分布式 +## Hadoop +## 工具 +## 编码实践 ## 后记 - **关于仓库** 本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 - +**关于贡献** +笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以发表一个 Issue,表明您想要添加的内容,笔者会及时查看。 +您也可以在 Issues 中发表关于改进本仓库的建议。 +**关于排版** +笔记排版参考@CYC2018 **关于转载** 本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 - +**鸣谢** +[CyC2018](https://github.com/CyC2018) diff --git "a/md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" "b/md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" index b71e6eb..3165d7f 100644 --- "a/md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" +++ "b/md/Hadoop\347\224\237\346\200\201\346\200\273\347\273\223.md" @@ -7,7 +7,6 @@ categories: - 后端 - 技术总结 --- -#Hadoop生态基础学习总结 这篇总结主要是基于我之前Hadoop生态基础系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 #更多详细内容可以查看我的专栏文章:Hadoop生态学习 diff --git a/md/README.md b/md/README.md deleted file mode 100644 index 9a81fc5..0000000 --- a/md/README.md +++ /dev/null @@ -1,27 +0,0 @@ -[数据结构和算法](#算法) | [操作系统](#操作系统) | [网络](#网络) | [数据结构](#数据结构) | [数据库](#数据库) | [Java基础](#Java基础) | [Java进阶](#Java进阶) | [Web和Spring](#Web和Spring) | [分布式](#分布式) | [Hadoop](#Hadoop) | [工具](#工具) | [编码实践](#编码实践) -
-## 数据结构和算法 -## 操作系统 -## 网络 -## 数据库 -## Java基础 -## Java进阶 -## web和Spring -## 分布式 -## Hadoop -## 工具 -## 编码实践 - -## 后记 -**关于仓库** -本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 -**关于贡献** -笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以发表一个 Issue,表明您想要添加的内容,笔者会及时查看。 -您也可以在 Issues 中发表关于改进本仓库的建议。 -**关于排版** -笔记排版参考@CYC2018 -**关于转载** -本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 -**鸣谢** -[CyC2018](https://github.com/CyC2018) - From a7cb53e8a94d5d08e90c728f4d54fa1ee9d5273e Mon Sep 17 00:00:00 2001 From: 724888 <362294931@qq.com> Date: Mon, 9 Jul 2018 22:49:44 +0800 Subject: [PATCH 18/27] add mds --- ...46\344\271\240\346\200\273\347\273\223.md" | 849 +++++++++ "md/\345\211\221\346\214\207offer.md" | 1630 +++++++++++++++++ ...46\344\271\240\346\200\273\347\273\223.md" | 304 +++ ...46\344\271\240\346\200\273\347\273\223.md" | 640 +++++++ 4 files changed, 3423 insertions(+) create mode 100644 "md/Linux\345\206\205\346\240\270\344\270\216\345\237\272\347\241\200\345\221\275\344\273\244\345\255\246\344\271\240\346\200\273\347\273\223.md" create mode 100644 "md/\345\211\221\346\214\207offer.md" create mode 100644 "md/\346\223\215\344\275\234\347\263\273\347\273\237\345\255\246\344\271\240\346\200\273\347\273\223.md" create mode 100644 "md/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\255\246\344\271\240\346\200\273\347\273\223.md" diff --git "a/md/Linux\345\206\205\346\240\270\344\270\216\345\237\272\347\241\200\345\221\275\344\273\244\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/md/Linux\345\206\205\346\240\270\344\270\216\345\237\272\347\241\200\345\221\275\344\273\244\345\255\246\344\271\240\346\200\273\347\273\223.md" new file mode 100644 index 0000000..3656f63 --- /dev/null +++ "b/md/Linux\345\206\205\346\240\270\344\270\216\345\237\272\347\241\200\345\221\275\344\273\244\345\255\246\344\271\240\346\200\273\347\273\223.md" @@ -0,0 +1,849 @@ +--- +title: Linux内核与基础命令学习总结 +date: 2018-07-09 22:33:14 +tags: + - Linux +categories: + - 后端 + - 技术总结 +--- +##这部分内容主要是基于一些关于Linux系统的内核基础和基本命令的学习总结,内容不全面,只讲述了其中的一小部分,后续会再补充,如有错误,还请见谅。 + + +# Linux操作系统 + + + +Linux操作系统博大精深,其中对线程,IO,文件系统等概念的实现都很有借鉴意义。 + + + +## 文件系统和VFS + +文件系统的inode上面讲过了。VFS主要用于屏蔽底层的不同文件系统,比如接入网络中的nfs文件系统,亦或是windows文件系统,正常情况下难以办到,而vfs通过使用IO操作的posix规范来规定所有文件读写操作,每个文件系统只需要实现这些操作就可以接入VFS,不需要重新安装文件系统。 +## 进程和线程 + + > 进程、程序与线程 + > + > 程序 + > + > 程序,简单的来说就是存在磁盘上的二进制文件,是可以内核所执行的代码 + > + > 进程 + > + > 当一个用户启动一个程序,将会在内存中开启一块空间,这就创造了一个进程,一个进程包含一个独一无二的PID,和执行者的权限属性参数,以及程序所需代码与相关的资料。 + > 进程是系统分配资源的基本单位。 + > 一个进程可以衍生出其他的子进程,子进程的相关权限将会沿用父进程的相关权限。 + > + > 线程 + > + > 每个进程包含一个或多个线程,线程是进程内的活动单元,是负责执行代码和管理进程运行状态的抽象。 + > 线程是独立运行和调度的基本单位。 + + + +> 子进程和父进程 +> 进程的层次结构(父进程与子进程)在进程执行的过程中可能会衍生出其他的进程,称之为子进程,子进程拥有一个指明其父进程PID的PPID。子进程可以继承父进程的环境变量和权限参数。 +> +> 于是,linux系统中就诞生了进程的层次结构——进程树。 +> 进程树的根是第一个进程(init进程)。 +> +> 过程调用的流程: fork & exec一个进程生成子进程的过程是,系统首先复制(fork)一份父进程,生成一个暂存进程,这个暂存进程和父进程的区别是pid不一样,而且拥有一个ppid,这时候系统再去执行(exec)这个暂存进程,让他加载实际要运行的程序,最终成为一个子进程的存在。 +> +> 服务与进程 +> +> 简单的说服务(daemon)就是常驻内存的进程,通常服务会在开机时通过init.d中的一段脚本被启动。 +> +> 进程通信 +> +> 进程通信的几种基本方式:管道,信号量,消息队列,共享内存,快速用户控件互斥。 +> +## fork方法 + 一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程, + +也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。 + + + 一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都 + +复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。 + + fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值: + 1)在父进程中,fork返回新创建子进程的进程ID; + 2)在子进程中,fork返回0; + 3)如果出现错误,fork返回一个负值; + +如何理解pid在父子进程中不同? + +其实就相当于链表,进程形成了链表,父进程的pid指向了子进程的pid,因为子进程没有子进程,所以pid为0。 + +## 写时复制 + + 传统的fork机制是,调用fork时,内核会复制所有的内部数据结构,复制进程的页表项,然后把父进程的地址空间按页复制给子进程(非常耗时)。 + + 现代的fork机制采用了一种惰性算法的优化策略。 + + 为了避免复制时系统开销,就尽可能的减少“复制”操作,当多个进程需要读取他们自己那部分资源的副本时,并不复制多个副本出来,而是为每个进程设定一个文件指针,让它们读取同一个实际文件。 + + 显然这样的方式会在写入时产生冲突(类似并发),于是当某个进程想要修改自己的那个副本时,再去复制该资源,(只有写入时才复制,所以叫写时复制)这样就减少了复制的频率。 + +## 父子进程,僵尸进程,孤儿进程,守护进程 + +父进程通过fork产生子进程。 + +孤儿进程:当子进程未结束时父进程异常退出,原本需要由父进程进行处理的子进程变成了孤儿进程,init系统进程会把这些进程领养,避免他们成为孤儿。 + +僵尸进程:当子进程结束时,会在内存中保留一部分数据结构等待父亲进程显式结束,如果父进程没有执行结束操作,则会导致子进程的剩余结构无法被释放,占用空间造成严重后果。 + +守护进程:守护进程用于监控其他进程,当发现大量僵尸进程时,会找到他们的父节点并杀死,同时让init线程认养他们以便释放这些空间。 + +僵尸进程是有害的,孤儿进程由于内核进程的认养不会造成危害。 + +## 进程组和会话 + +> 会话和进程组进程组每个进程都属于某个进程组,进程组就是由一个或者多个为了实现作业控制而相互关联的进程组成的。 +> +> 一个进程组的id是进程组首进程的pid(如果一个进程组只有一个进程,那进程组和进程其实没啥区别)。 +> +> 进程组的意义在于,信号可以发送给进程组中的所有进程。这样可以实现对多个进程的同时操作。 +> 会话会话是一个或者多个进程组的集合。 +> +> 一般来说,会话(session)和shell没有什么本质上的区别。 +> 我们通常使用用户登录一个终端进行一系列操作这样的例子来描述一次会话。 + +举例 + +$cat ship-inventory.txt | grep + +booty|sort上面就是在某次会话中的一个shell命令,它会产生一个由3个进程组成的进程组。 +## 守护进程 +守护进程(服务)守护进程(daemon)运行在后台,不与任何控制终端相关联。通常在系统启动时通过init脚本被调用而开始运行。 + +在linux系统中,守护进程和服务没有什么区别。 +对于一个守护进程,有两个基本的要求:其一:必须作为init进程的子进程运行,其二:不与任何控制终端交互。 + + +## 硬连接和软连接 + +硬链接指的是不同的文件名指向同一个inode节点,比如某个目录下的a和另一个目录下的b,建立一个软连接让a指向b,则a和b共享同一个inode。 + +软连接是指一个文件的inode节点不存数据,而是存储着另一个文件的绝对路径,访问文件内容时实际上是去访问对应路径下的文件inode,这样的话文件发生改动或者移动都会导致软连接失效。 + +## 线程 + +线程基础概念线程是进程内的执行单元(比进程更低一层的概念),具体包括 虚拟处理器,堆栈,程序状态等。 +可以认为 线程是操作系统调度的最小执行单元。 + +现代操作系统对用户空间做两个基础抽象:虚拟内存和虚拟处理器。这使得进程内部“感觉”自己独占机器资源。 + +虚拟内存系统会为每个进程分配独立的内存空间,这会让进程以为自己独享全部的RAM。 + +但是同一个进程内的所有线程共享该进程的内存空间。 +虚拟处理器这是一个针对线程的概念,它让每个线程都“感觉”自己独享CPU。实际上对于进程也是一样的。 + +## 线程模型 + +线程模型线程的概念同时存在于内核和用户空间中。下面介绍三种线程模型。 + + 内核级线程模型每个内核线程直接转换成用户空间的线程。即内核线程:用户空间线程=1:1 + + 用户级线程模型这种模型下,一个保护了n个线程的用户进程只会映射到一个内核进程。即n:1。 + 可以减少上下文切换的成本,但在linux下没什么意义,因为linux下进程间的上下文切换本身就没什么消耗,所以很少使用。 + + 混合式线程模型上述两种模型的混合,即n:m型。 + 很难实现。 +## 内核线程实现 + +系统线程实现:PThreads +原始的linux系统调用中,没有像C++11或者是Java那样完整的线程库。 + +整体看来pthread的api比较冗余和复杂,但是基本操作也主要是 创建、退出等。 + +1.创建线程 + + int pthread_create + + (若线程创建成功,则返回0。若线程创建失败,则返回出错编号) + +   注意:线程创建者和新建线程之间没有fork()调用那样的父子关系,它们是对等关系。调用pthread_create()创建线程后,线程创建者和新建线程哪个先运行是不确定的,特别是在多处理机器上。 +   +2.终止线程 + + void pthread_exit(void *value_ptr); + + 线程调用pthread_exit()结束自己,参数value_ptr作为线程的返回值被调用pthread_join的线程使用。由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,但是可以用pthread_join()函数来同步并释放资源 + + +3.取消线程 + + int pthread_cancel(pthread_t thread); + +   注意:若是在整个程序退出时,要终止各个线程,应该在成功发送 CANCEL指令后,使用 pthread_join函数,等待指定的线程已经完全退出以后,再继续执行;否则,很容易产生 “段错误”。 +   +4.连接线程(阻塞) + + int pthread_join(pthread_t thread, void **value_ptr); + +   等待线程thread结束,并设置*value_ptr为thread的返回值。pthread_join阻塞调用者,一直到线程thread结束为止。当函数返回时,被等待线程的资源被收回。如果进程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。 + + 需要留意的一点是linux机制下,线程存在一个被称为joinable的状态。下面简要了解一下: + +Join和Detach +这块的概念,非常类似于之前父子进程那部分,等待子进程退出的内容(一系列的wait函数)。 + +linux机制下,线程存在两种不同的状态:joinable和unjoinable。 + + 如果一个线程被标记为joinable时,即便它的线程函数执行完了,或者使用了pthread_exit()结束了该线程,它所占用的堆栈资源和进程描述符都不会被释放(类似僵尸进程),这种情况应该由线程的创建者调用pthread_join()来等待线程的结束并回收其资源(类似wait系函数)。默认情况下创建的线程都是这种状态。 + + 如果一个线程被标记成unjoinable,称它被分离(detach)了,这时候如果该线程结束,所有它的资源都会被自动回收。省去了给它擦屁股的麻烦。 + + 因为创建的线程默认都是joinable的,所以要么在父线程调用pthread_detach(thread_id)将其分离,要么在线程内部,调用pthread_detach(pthread_self())来把自己标记成分离的。 + + + +## 文件系统 + + 文件描述符在linux内核中,文件是用一个整数来表示的,称为 文件描述符,通俗的来说,你可以理解它是文件的id(唯一标识符) + + 普通文件 + 普通文件就是字节流组织的数据。 + 文件并不是通过和文件名关联来实现的,而是通过关联索引节点来实现的,文件节点拥有文件系统为普通文件分配的唯一整数值(ino),并且存放着一些文件的相关元数据。 + + 目录与链接 + 正常情况下文件是通过文件名来打开的。 + 目录是可读名称到索引编号之间的映射,名称和索引节点之间的配对称为链接。 + 可以把目录看做普通文件,只是它包含着文件名称到索引节点的映射(链接) + + + +文件系统是基于底层存储建立的一个树形文件结构。比较经典的是Linux的文件系统,首先在硬盘的超级块中安装文件系统,磁盘引导时会加载文件系统的信息。 + +linux使用inode来标识任意一个文件。inode存储除了文件名以外的文件信息,包括创建时间,权限,以及一个指向磁盘存储位置的指针,那里才是真正存放数据的地方。 + +一个目录也是一个inode节点。 + +详细阐述一次文件访问的过程: + + 首先用户ls查看目录。由于一个目录也是一个文件,所以相当于是看目录文件下有哪些东西。 + + 实际上目录文件是一个特殊的inode节点,它不需要存储实际数据,而只是维护一个文件名到inode的映射表。 + + 于是我们ls到另一个目录。同理他也是一个inode。我们在这个inode下执行vi操作打开某个文件,于是linux通过inode中的映射表找到了我们请求访问的文件名对应的inode。 + + 然后寻道到对应的磁盘位置,读取内容到缓冲区,通过系统调用把内容读到内存中,最后进行访问。 +## IO操作 + +# 文件描述符 + +  对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时,使用open或create返回的文件描述符表示该文件,将其作为参数传给read或write函数。 + +# write函数 + +   write函数定义如下: + + + +
#include  ssize_t write(int filedes, void *buf, size_t nbytes); // 返回:若成功则返回写入的字节数,若出错则返回-1 // filedes:文件描述符 // buf:待写入数据缓存区 // nbytes:要写入的字节数
+ + + +  同样,为了保证写入数据的完整性,在《UNIX网络编程 卷1》中,作者将该函数进行了封装,具体程序如下: + +![](http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif) + + + +![复制代码](http://common.cnblogs.com/images/copycode.gif) + +
 1 ssize_t                        /* Write "n" bytes to a descriptor. */
+ 2 writen(int fd, const void *vptr, size_t n)
+ 3 {
+ 4     size_t nleft;
+ 5     ssize_t nwritten;
+ 6     const char *ptr;
+ 7 
+ 8     ptr = vptr; 9     nleft = n; 10     while (nleft > 0) { 11         if ( (nwritten = write(fd, ptr, nleft)) <= 0) { 12             if (nwritten < 0 && errno == EINTR) 13                 nwritten = 0;        /* and call write() again */
+14             else
+15                 return(-1);            /* error */
+16 } 17 
+18         nleft -= nwritten; 19         ptr   += nwritten; 20 } 21     return(n); 22 } 23 /* end writen */
+24 
+25 void
+26 Writen(int fd, void *ptr, size_t nbytes) 27 { 28     if (writen(fd, ptr, nbytes) != nbytes) 29         err_sys("writen error"); 30 }
+ +![复制代码](http://common.cnblogs.com/images/copycode.gif) + + + + + +# read函数 + +  read函数定义如下: + + + +
#include  ssize_t read(int filedes, void *buf, size_t nbytes); // 返回:若成功则返回读到的字节数,若已到文件末尾则返回0,若出错则返回-1 // filedes:文件描述符 // buf:读取数据缓存区 // nbytes:要读取的字节数
+ + + +   有几种情况可使实际读到的字节数少于要求读的字节数: + +  1)读普通文件时,在读到要求字节数之前就已经达到了文件末端。例如,若在到达文件末端之前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,它将返回0(文件末端)。 + +  2)当从终端设备读时,通常一次最多读一行。 + +  3)当从网络读时,网络中的缓存机构可能造成返回值小于所要求读的字结束。 + +  4)当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。 + +  5)当从某些面向记录的设备(例如磁带)读时,一次最多返回一个记录。 + +  6)当某一个信号造成中断,而已经读取了部分数据。 + +  在《UNIX网络编程 卷1》中,作者将该函数进行了封装,以确保数据读取的完整,具体程序如下: + +![](http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif) + + + +![复制代码](http://common.cnblogs.com/images/copycode.gif) + +
 1 ssize_t                        /* Read "n" bytes from a descriptor. */
+ 2 readn(int fd, void *vptr, size_t n)
+ 3 {
+ 4     size_t nleft;
+ 5     ssize_t nread;
+ 6     char *ptr;
+ 7 
+ 8     ptr = vptr; 9     nleft = n; 10     while (nleft > 0) { 11         if ( (nread = read(fd, ptr, nleft)) < 0) { 12             if (errno == EINTR) 13                 nread = 0;        /* and call read() again */
+14             else
+15                 return(-1); 16         } else if (nread == 0) 17             break;                /* EOF */
+18 
+19         nleft -= nread; 20         ptr   += nread; 21 } 22     return(n - nleft);        /* return >= 0 */
+23 } 24 /* end readn */
+25 
+26 ssize_t 27 Readn(int fd, void *ptr, size_t nbytes) 28 { 29 ssize_t        n; 30 
+31     if ( (n = readn(fd, ptr, nbytes)) < 0) 32         err_sys("readn error"); 33     return(n); 34 }
+ +![复制代码](http://common.cnblogs.com/images/copycode.gif) + + + + + +本文下半部分摘自博文[浅谈TCP/IP网络编程中socket的行为](http://www.cnblogs.com/promise6522/archive/2012/03/03/2377935.html)。 + +# **read/write的语义:为什么会阻塞?** + +  先从write说起: + + + +
#include  ssize_t write(int fd, const void *buf, size_t count);
+ + + +  首先,write成功返回,**只是buf中的数据被复制到了kernel中的TCP发送缓冲区。**至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。 + +  write在什么情况下会阻塞?当kernel的该socket的发送缓冲区已满时。对于每个socket,拥有自己的send buffer和receive buffer。从Linux 2.6开始,两个缓冲区大小都由系统来自动调节(autotuning),但一般在default和max之间浮动。 + + + +
# 获取socket的发送/接受缓冲区的大小:(后面的值是在Linux 2.6.38 x86_64上测试的结果)
+ +
sysctl net.core.wmem_default       #126976
+sysctl net.core.wmem_max        #131071
+ + + +  已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。 + +  一般来说,由于**接收端进程从socket读数据的速度**跟不上**发送端进程向socket写数据的速度**,最终导致**发送端write调用阻塞。** + +  而read调用的行为相对容易理解,从socket的receive buffer中拷贝数据到应用程序的buffer中。read调用阻塞,通常是发送端的数据没有到达。 + +## Linux常用命令和基础知识 + +### 查看进程 + + 1. ps + 查看某个时间点的进程信息 + + 示例一:查看自己的进程 + + # ps -l + 示例二:查看系统所有进程 + + # ps aux + 示例三:查看特定的进程 + + # ps aux | grep threadx + + 2. top + 实时显示进程信息 + + 示例:两秒钟刷新一次 + + # top -d 2 + 3. pstree + 查看进程树 + + 示例:查看所有进程树 + + # pstree -A + 4. netstat + 查看占用端口的进程 + + 示例:查看特定端口的进程 + + # netstat -anp | grep port + +### 文件操作 +ls -a ,all列出全部文件包括隐藏 + +ls -l,list显示文件的全部属性 + +ls -d,仅列出目录本身 + +cd mkdir rmdir 常用不解释 rm -rf永久删除 cp复制 mv移动或改名 + +touch,更新文件时间或者建立新文件。 + + +### 权限操作 + + chmod rwx 分别对应 421 + + chmod 754 .bashrc 将权限改为rwxr-xr-- + + 对应权限分配是对于 拥有者,所属群组,以及其他人。 + + +文件默认权限 + +文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 + +目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 + + +目录的权限 + +ps:拥有目录权限才能修改文件名,拥有文件权限是没用的 + + 文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 + +目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 + + +## 连接操作 + + +硬链接: + + 使用ln建立了一个硬连接,通过ll -i获得他们的inode节点。发现他们的inode节点是相同的。符合硬连接规定。 + + + # ln /etc/crontab . + # ll -i /etc/crontab crontab + + 34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab + 34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab + +软连接: + + 符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。 + + 当源文件被删除了或者被移动到其他位置了,链接文件就打不开了。 + + 可以为目录建立链接。 + + # ll -i /etc/crontab /root/crontab2 + + 34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab + 53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab + +## 获取内容 + +cat 读取内容 加上-n 按行打印 + +tac是cat的反向操作 + +more允许翻页查看,而不像cat一次显示全部内容 + +less可以先前翻页和向后翻页,more只能向前翻页 + +head 和tail 负责取得文件的前几行和后几行 + +## 搜索和定位 + + 1 which负责指令搜索,并显示第一条 比如which pwd,会找到pwd对应的程序。加-a 打印全部。 + + 2 whereis负责搜索文件, 后面接上dirname/filename + + 文件搜索。速度比较快,因为它只搜索几个特定的目录。 + 比如 whereis /bin hello.c + + 3 locate + 文件搜索。可以用关键字或者正则表达式进行搜索。 + + locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 + + # locate [-ir] keyword + -r:正则表达式 + + locate hello + locate he* + vi heeee + updatedb + locate he? + + 4. find + 文件搜索。可以使用文件的属性和权限进行搜索。 + + # find [basedir] [option] + example: find . -name "shadow*" + + find -name "hike" + find +属性后缀 "属性" + + (一)与时间有关的选项 + + -mtime n :列出在 n 天前的那一天修改过内容的文件 + + (二)与文件拥有者和所属群组有关的选项 + + -uid n + -gid n + -user name + + (三)与文件权限和名称有关的选项 + + -name filename + -size [+-]SIZE:搜寻比 SIZE 还要大 (+) 或小 (-) 的文件。这个 SIZE 的规格有:c: 代表 byte,k: 代表 1024bytes。所以,要找比 50KB 还要大的文件,就是 -size +50k + -type TYPE + +## 压缩 + gzip压缩和解压,还有bzip,xz等压缩 + + 而tar可以用打包,打包的时候也可以执行压缩 + + 压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。 + + $ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩 + $ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看 + $ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 + + +## 管道指令 + +1 | + +2 cut切分数据,分成多列,last显示登陆者信息 + +## 正则 + + +grep + + g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。 + + $ grep [-acinv] [--color=auto] 搜寻字符串 filename + -c : 计算找到个数 + -i : 忽略大小写 + -n : 输出行号 + -v : 反向选择,亦即显示出没有 搜寻字符串 内容的那一行 + --color=auto :找到的关键字加颜色显示 + +awk + + $ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename + 示例 2:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 + + $ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' + root 0 + bin 1 + daemon 2 + sed + + 示例 3:输出正在处理的行号,并显示每一行有多少字段 + + $ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}' + dmtsai lines: 1 columns: 10 + dmtsai lines: 2 columns: 10 + dmtsai lines: 3 columns: 10 + dmtsai lines: 4 columns: 10 + dmtsai lines: 5 columns: 9 + +sed: + + awk用于匹配每一行中的内容并打印 + 而sed负责把文件内容重定向到输出,所以sed读取完文件并重定向到输出并且通过awk匹配这些内容并打印。 + + 他们俩经常搭配使用。 + +## linux指令实践和常见场景 + +## 查看进程状态 + +Linux进程状态(ps stat)之R、S、D、T、Z、X + + + + D 不可中断 Uninterruptible sleep (usually IO) + R 正在运行,或在队列中的进程 + S 处于休眠状态 + T 停止或被追踪 + Z 僵尸进程 + W 进入内存交换(从内核2.6开始无效) + X 死掉的进程 + + + < 高优先级 + N 低优先级 + L 有些页被锁进内存 + s 包含子进程 + + 位于后台的进程组; + l 多线程,克隆线程 multi-threaded (using CLONE_THREAD, like NPTL pthreads do) + + +ps aux + +![](https://img-blog.csdn.net/20180703221736541?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2E3MjQ4ODg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) + +## strace +strace用于跟踪程序执行过程中的系统调用,如跟踪test进程,只需要: + +strace -p [test_pid] 或直接strace ./test + +比如,跟踪pid为12345的进程中所有线程的read和write系统调用,输出字符串的长度限制为1024: + +strace -s 1024 -f -e trace=read,write -p 12345 + +## tcpdump +tcpdump是Linux上的抓包工具,如抓取eth0网卡上的包,使用: + +sudo tcpdump -i eth0 + +比如,抓取80端口的HTTP报文,以文本形式展示: + +sudo tcpdump -i any port 80 -A +这样你就可以清楚看到GET、POST请求的内容了。 + +## nc + +nc可以在Linux上开启TCP Server、TCP Client、UDP Server、UDP Client。 + +如在端口号12345上开启TCP Server和Client模拟TCP通信: + +Server: nc -l 127.0.0.1 12345 +Client: nc 127.0.0.1 12345 +在端口号12345上开启UDP Server和Client模拟TCP通信: + +Server: nc -ul 127.0.0.1 12345 +Client: nc -u 127.0.0.1 12345 +Unix Socket通信示例: + +Server: nc -Ul /tmp/1.sock +Client: nc -U /tmp/1.sock + +## curl +curl用于模拟HTTP请求,在终端模拟请求时常用,如最基本的用法: + +curl http://www.baidu.com + +## lsof + +lsof命令主要用法包括: + +sudo lsof -i :[port] 查看端口占用进程信息,经常用于端口绑定失败时确认端口被哪个进程占用 + +sudo lsof -p [pid] 查看进程打开了哪些文件或套接字 + +## ss +Linux上的ss命令可以用于替换netstat,ss直接读取解析/proc/net下的统计信息,相比netstat遍历/proc下的每个PID目录,速度快很多。 + +## awk/sed +awk和sed在文本处理方面十分强大,其中,awk按列进行处理,sed按行进行处理。 + +如采用冒号分隔数据,输出第一列数据($0代表行全部列数据,$1代表第一列,$2代表第二列...) + +awk -F ":" '{print $1}' +在awk的结果基础上,结合sort、uniq和head等命令可以轻松完成频率统计等功能 + +查看文件的第100行到第200行: +sed -n '100,200p' log.txt +替换字符串中的特定子串 +echo "int charset=gb2312 float"|sed "s/charset=gb2312/charset=UTF-8/g" +替换test文件每行匹配ab的部分为cd +sed -i 's/ab/cd/g' test + +## vim +打开文件并跳到第10行 + +$ vim +10 filename.txt +打开文件跳到第一个匹配的行 + +$ vim +/search-term filename.txt +以只读模式打开文件 + +$ vim -R /etc/passwd + +## crontab +查看某个用户的crontab入口 + +$ crontab -u john -l +设置一个每十分钟执行一次的计划任务 + +*/10 * * * * /home/ramesh/check-disk-space +更多示例:Linux Crontab: 15 Awesome Cron Job Examples + +## service +service命令用于运行System V init脚本,这些脚本一般位于/etc/init.d文件下,这个命令可以直接运行这个文件夹里面的脚本,而不用加上路径 + +查看服务状态 + +$ service ssh status +查看所有服务状态 + +$ service --status-all +重启服务 + +$ service ssh restart + +## free +这个命令用于显示系统当前内存的使用情况,包括已用内存、可用内存和交换内存的情况 + +默认情况下free会以字节为单位输出内存的使用量 + + $ free + total used free shared buffers cached + Mem: 3566408 1580220 1986188 0 203988 902960 + -/+ buffers/cache: 473272 3093136 + Swap: 4000176 0 4000176 + +如果你想以其他单位输出内存的使用量,需要加一个选项,-g为GB,-m为MB,-k为KB,-b为字节 + + $ free -g + total used free shared buffers cached + Mem: 3 1 1 0 0 0 + -/+ buffers/cache: 0 2 + Swap: 3 0 3 + +如果你想查看所有内存的汇总,请使用-t选项,使用这个选项会在输出中加一个汇总行 + + ramesh@ramesh-laptop:~$ free -t + total used free shared buffers cached + Mem: 3566408 1592148 1974260 0 204260 912556 + -/+ buffers/cache: 475332 3091076 + Swap: 4000176 0 4000176 + Total: 7566584 1592148 5974436 + +## top + +top命令会显示当前系统中占用资源最多的一些进程(默认以CPU占用率排序)如果你想改变排序方式,可以在结果列表中点击O(大写字母O)会显示所有可用于排序的列,这个时候你就可以选择你想排序的列 + + Current Sort Field: P for window 1:Def + Select sort field via field letter, type any other key to return + + a: PID = Process Id v: nDRT = Dirty Pages count + d: UID = User Id y: WCHAN = Sleeping in Function + e: USER = User Name z: Flags = Task Flags + ........ + +如果只想显示某个特定用户的进程,可以使用-u选项 + +$ top -u oracle + +## df +显示文件系统的磁盘使用情况,默认情况下df -k 将以字节为单位输出磁盘的使用量 + +$ df -k + + Filesystem 1K-blocks Used Available Use% Mounted on + /dev/sda1 29530400 3233104 24797232 12% / + /dev/sda2 120367992 50171596 64082060 44% /home + +使用-h选项可以以更符合阅读习惯的方式显示磁盘使用量 + +$ df -h + + Filesystem Size Used Avail Capacity iused ifree %iused Mounted on + /dev/disk0s2 232Gi 84Gi 148Gi 37% 21998562 38864868 36% / + devfs 187Ki 187Ki 0Bi 100% 648 0 100% /dev + map -hosts 0Bi 0Bi 0Bi 100% 0 0 100% /net + map auto_home 0Bi 0Bi 0Bi 100% 0 0 100% /home + /dev/disk0s4 466Gi 45Gi 421Gi 10% 112774 440997174 0% /Volumes/BOOTCAMP + //app@izenesoft.cn/public 2.7Ti 1.3Ti 1.4Ti 48% + +## kill +kill用于终止一个进程。一般我们会先用ps -ef查找某个进程得到它的进程号,然后再使用kill -9 进程号终止该进程。你还可以使用killall、pkill、xkill来终止进程 + +$ ps -ef | grep vim +ramesh 7243 7222 9 22:43 pts/2 00:00:00 vim + +$ kill -9 7243 + +## mount +如果要挂载一个文件系统,需要先创建一个目录,然后将这个文件系统挂载到这个目录上 + +mkdir /u01 +mount /dev/sdb1 /u01 +也可以把它添加到fstab中进行自动挂载,这样任何时候系统重启的时候,文件系统都会被加载 + +/dev/sdb1 /u01 ext2 defaults 0 2 +## chmod +chmod用于改变文件和目录的权限 + +给指定文件的属主和属组所有权限(包括读、写、执行) + +$ chmod ug+rwx file.txt +删除指定文件的属组的所有权限 + +$ chmod g-rwx file.txt +修改目录的权限,以及递归修改目录下面所有文件和子目录的权限 + +$ chmod -R ug+rwx file.txt +更多示例:7 Chmod Command Examples for Beginners + +## chown +chown用于改变文件属主和属组 + +同时将某个文件的属主改为oracle,属组改为db + +$ chown oracle:dba dbora.sh +使用-R选项对目录和目录下的文件进行递归修改 + +$ chown -R oracle:dba /home/oracle + +## ifconfig +ifconfig用于查看和配置Linux系统的网络接口 + +## uname +uname可以显示一些重要的系统信息,例如内核名称、主机名、内核版本号、处理器类型之类的信息 + +## 实际场景问题 + + 1 cpu占用率 + + top可以看 + ps看不了 + 但是ps -aux可以看到各个线程的cpu和内存占用 + + 2 进程状态: + + ps -ef看不了 + ps aux可以看进程状态S R之类 + + 3 IO + iostat查看io状态 + + 4网络 + netstat查看tcp连接状态和socket情况, + + ipconfig查看网络设备 + + lsof可以查看端口使用情况 + + 5内存 + free \ No newline at end of file diff --git "a/md/\345\211\221\346\214\207offer.md" "b/md/\345\211\221\346\214\207offer.md" new file mode 100644 index 0000000..193d42b --- /dev/null +++ "b/md/\345\211\221\346\214\207offer.md" @@ -0,0 +1,1630 @@ +--- +title: 剑指offer算法学习总结 +date: 2018-07-09 22:32:40 +tags: + - 算法 +categories: + - 后端 + - 技术总结 +--- +##节选剑指offer比较经典和巧妙的一些题目,以便复习使用。一部分题目给出了完整代码,一部分题目比较简单直接给出思路。但是不保证我说的思路都是正确的,个人对算法也不是特别在行,只不过这本书的算法多看了几遍多做了几遍多了点心得体会。于是想总结一下。如果有错误也希望能指出,谢谢。 + +#具体代码可以参考我的GitHub仓库: + +#https://github.com/h2pl/SwordToOffer + +# 数论和数字规律 + +## 从1到n整数中1出现的次数 + +题目描述 +求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数。 + +1暴力办法,把整数转为字符串,依次枚举相加。复杂度是O(N * k)k为数字长度。 + +2第二种办法看不懂,需要数学推导,太长不看 + +## 排数组排成最小的数 + +输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。 + +解析:本题的关键是,两个数如何排成最小的,答案是,如果把数字看成字符串a,b那么如果a+b>b+a,则a应该放在b后面。 +例如 3和32 3 + 32 = 332,32 + 3 = 323,332>323,所以32要放在前面。 + +根据这个规律,构造一个比较器,使用排序方法即可。 + +## 丑数 + +题目描述 +把只包含因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 + +解析 + +1 暴力枚举每个丑数,找出第N个即可。 + +2 这个思路比较巧妙,由于丑数一定是由2,3,5三个因子构成的,所以我们每次构造出一个比前面丑数大但是比后面小的丑数,构造N次即可。 + + public class Solution { + public static int GetUglyNumber_Solution(int index) { + if (index == 0) return 0; + int []res = new int[index]; + res[0] = 1; + int i2,i3,i5; + i2 = i3 = i5 = 0; + for (int i = 1;i < index;i ++) { + res[i] = Math.min(res[i2] * 2, Math.min(res[i3] * 3, res[i5] * 5)); + if (res[i] == res[i2] * 2) i2 ++; + if (res[i] == res[i3] * 3) i3 ++; + if (res[i] == res[i5] * 5) i5 ++; + } + return res[index - 1]; + } + } + } + i2,i3,i5分别代表目前有几个2,3,5的因子,每次选一个最小的丑数,然后开始找下一个。当然i2,i3,i5也要跟着变。 +# 数组和矩阵 + + +## 二维数组的查找 + + /** + * Created by 周杰伦 on 2018/2/25. + * 题目描述 + 在一个二维数组中,每一行都按照从左到右递增的顺序排序, + 每一列都按照从上到下递增的顺序排序。请完成一个函数, + 输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 + 1 2 3 + 2 3 4 + 3 4 5 + */ + + 解析:比较经典的一题,解法也比较巧妙,由于数组从左向右和从上到下的都是递增的,所以找一个数可以先从最右开始找。 + 假设最右值为a,待查数为x,那么如果x < a说明x在a的左边,往左找即可,如果x > a,说明x 在 a的下面一行,到下面一行继续按照该规则查找,就可以遍历所有数。 + + 算法的时间复杂度是O(M * N) + + public class 二维数组中的查找 { + public static boolean Find(int target, int[][] array) { + + if(array[0][0] > target) { + return false; + } + + int row = 0; + int col = 0; + while (row < array.length && col >0) { + if (target == array[row][col]) { + return true; + } + else if (target array[row][col]) { + col ++; + } + else row++; + } + return false; + } + } +## 顺时针打印矩阵。 + +输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10. + +这题还是有点麻烦的,因为要顺时针打印,所以实际上是由外向内打印,边界的处理和递归调用需要谨慎。 + +这题我自己没写出标准答案。参考一个答案吧。关键在于四个循环中的分界点设置。 + + //主体循环部分才5行。其实是有规律可循的。将每一层的四个边角搞清楚就可以打印出来了 + + import java.util.ArrayList; + public class Solution { + public ArrayList printMatrix(int [][] array) { + ArrayList result = new ArrayList (); + if(array.length==0) return result; + int n = array.length,m = array[0].length; + if(m==0) return result; + int layers = (Math.min(n,m)-1)/2+1;//这个是层数 + for(int i=0;i=i)&&(n-i-1!=i);k--) result.add(array[n-i-1][k]);//右至左 + for(int j=n-i-2;(j>i)&&(m-i-1!=i);j--) result.add(array[j][i]);//左下至左上 + } + return result; + } + } + +## 调整数组中数字的顺序,使正数在负数的前面 + +双指针即可以解决,变式有正负,奇偶等等。 + +## 数组中出现次数超过一半的数字 + +本题有很多种解法。 + +1 最笨的解法,统计每个数的出现次数,O(n2) + +2 使用hashmap,空间换时间O(n) + +3 由于出现超过一半的数字一定也是中位数,所以可以先排序,再找到第n/2位置上的节点。 + +4 使用快速排序的复杂度是O(nlogn),基于快排的特性,每一轮的过程都会把一个数放到最终位置,所以我们可以判断一下这个数的位置是不是n/2,如果是的话,那么就直接返回即可。这样就优化了快排的步骤。 + +4.5事实上,上述办法的复杂度仍然是O(nlogn) + + 快速排序的partition函数将一个数组分为左右两边,并且我们可以知道,如果flag值在k位置左边,那么往左找,如果在k位置右边,那么往左找。 + + 这里科普一下经典快排中的一个方法partition,剑指offer书中直接跳过了这部分,让我摸不着头脑。 + + 虽然快排用到了经典的分而治之的思想,但是快排实现的前提还是在于 partition 函数。正是有了 partition 的存在,才使得可以将整个大问题进行划分,进而分别进行处理。 + + 除了用来进行快速排序,partition 还可以用 O(N) 的平均时间复杂度从无序数组中寻找第K大的值。和快排一样,这里也用到了分而治之的思想。首先用 partition 将数组分为两部分,得到分界点下标 pos,然后分三种情况: + + pos == k-1,则找到第 K 大的值,arr[pos]; + pos > k-1,则第 K 大的值在左边部分的数组。 + pos < k-1,则第 K 大的值在右边部分的数组。 + 下面给出基于迭代的实现(用来寻找第 K 小的数): + + + int find_kth_number(vector &arr, int k){ + int begin = 0, end = arr.size(); + assert(k>0 && k<=end); + int target_num = 0; + while (begin < end){ + int pos = partition(arr, begin, end); + if(pos == k-1){ + target_num = arr[pos]; + break; + } + else if(pos > k-1){ + end = pos; + } + else{ + begin = pos + 1; + } + } + return target_num; + } + + + 该算法的时间复杂度是多少呢?考虑最坏情况下,每次 partition 将数组分为长度为 N-1 和 1 的两部分,然后在长的一边继续寻找第 K 大,此时时间复杂度为 O(N^2 )。不过如果在开始之前将数组进行随机打乱,那么可以尽量避免最坏情况的出现。而在最好情况下,每次将数组均分为长度相同的两半,运行时间 T(N) = N + T(N/2),时间复杂度是 O(N)。 + +所以也就是说,本题用这个方法解的话,复杂度只需要O(n),因为第一次交换需要N/2,j接下来的交换的次数越来越少,最后加起来就是O(N)了。 + +5 由于数字出现次数超过长度的一半,也就是平均每两个数字就有一个该数字,但他们不一定连续,所以变量time保存一个数的出现次数,然后变量x代表目前选择的数字,遍历中,如果x与后一位不相等则time--,time=0时x改为后一位,time重新变为1。最终x指向的数字就是出现次数最多的。 + +举两个例子,比如1,2,3,4,5,6,6,6,6,6,6。明显符合。1,6,2,6,3,6,4,6,5,6,6 遍历到最后得到x=6,以此类推,可以满足要求。 + +## 找出前k小的数 + * 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。 + */ + +解析: + +1如果允许改变数组,那么则可以继承上一题的思想。,使用快速排序中的partition方法,只需要O(N)的复杂度 + +2使用堆排序 + + 解析:用前k个数构造一个大小为k的大顶堆,然后遍历余下数字,如果比堆顶大,则跳过,如果比堆顶小,则替换掉堆顶元素,然后执行一次堆排序(即根节点向下调整)。此时的堆顶元素已被替换, + + 然后遍历完所有元素,堆中的元素就是最小的k个元素了。 + + 如果要求最大的k个元素,则构造小顶堆就可以了。 + + 构造堆的方法是,数组的第N/2号元素到0号元素依次向下调整,此时数组就构成了堆。 + + 实际上我们可以使用现成的集合类,红黑树是一棵搜索树,他是排序的,所以可以得到最大和最小值,那么我们每次和最小值比较,符合条件就进行替换即可。复杂度是O(nlogn) + + + public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + + ArrayListarrayList=new ArrayList<>(); + if(input==null || input.length==0 ||k==0 ||k>input.length)return arrayList; + + TreeSet treeSet=new TreeSet<>(); + + + for(int i=0;i 0) + + +## 逆序对 + +/** + * Created by 周杰伦 on 2017/3/23. + * 题目描述 + 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。 + 输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 + 即输出P%1000000007 + */ + + 解析:本题采用归并排序的框架,只是在归并的时候做出逆序对查找,具体参见下面代码。 + 核心点是,在归并两个有序数组时,如果a数组的元素a1比b数组的元素b1大时,说明有mid - i + 1个数都比b1大。i为a1元素的位置。 + 这样子我们就可以统计逆序对的个数了。经典巧妙。! + + public class 逆序对 { + public double Pairs = 0; + public int InversePairs(int [] array) { + if (array.length==0 ||array==null) + return 0; + mergesort(array,0,array.length-1); + Pairs = Pairs + 1000000007; + return (int) (Pairs % 1000000007); + } + public void merge(int []array,int left,int mid,int right){ + //有一点很重要的是,归并分成两部分,其中一段是left到mid,第二段是mid+1到right。 + //不能从0到mid-1,然后mid到right。因为这样左右不均分,会出错。千万注意。 + //mid=(left+right)/2 + if (array.length==0 ||array==null ||left>=right) + return ; + int p=left,q=mid+1,k=0; + + int []temp=new int[right-left+1]; + + while (p<=mid && q<=right){ + if(array[p]>array[q]){ + temp[k++]=array[q++]; + //当前半数组中有一个数p比后半个数组中的一个数q大时,由于两个数组 + //已经分别有序,所以说明p到中间数之间的所有数都比q大。 + Pairs+=mid-p+1; + } + else temp[k++]=array[p++]; + } + + while (p<=mid){ + temp[k++]=array[p++];} + while (q<=right){ + temp[k++]=array[q++];} + + + + for (int m = 0; m < temp.length; m++) + array[left + m] = temp[m]; + + } + + public void mergesort(int []arr,int left,int right){ + if (arr.length==0 ||arr==null) + return ; + int mid=(right+left)/2; + if(left list = new ArrayList<>(); + public static void Insert(Integer num) { + list.add(num); + Collections.sort(list); + } + + public static Double GetMedian() { + if (list.size() % 2 == 0) { + int l = list.get(list.size()/2); + int r = list.get(list.size()/2 - 1); + return (l + r)/2.0; + } + else { + return list.get(list.size()/2)/1.0; + } + } + + + } + +## 滑动窗口中的最大值 + +给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。 + +解析: +1 保持窗口为3进行右移,每次计算出一个最大值即可。 + +2 使用两个栈实现一个队列,复杂度O(N),使用两个栈实现最大值栈,复杂度O(1)。两者结合可以完成本题。但是太麻烦了。 + +3 使用双端队列解决该问题。 + + import java.util.*; + /** + 用一个双端队列,队列第一个位置(队头)保存当前窗口的最大值,当窗口滑动一次 + 1.判断当前最大值是否过期(如果最大值所在的下标已经不在窗口范围内,则过期) + 2.对于一个新加入的值,首先一定要先放入队列,即使他比队头元素小,因为队头元素可能过期。 + 3.新增加的值从队尾开始比较,把所有比他小的值丢掉(因为队列只存最大值,所以之前比他小的可以丢掉) + + */ + public class Solution { + public ArrayList maxInWindows(int [] num, int size) + { + ArrayList res = new ArrayList<>(); + if(size == 0) return res; + int begin; + ArrayDeque q = new ArrayDeque<>(); + for(int i = 0; i < num.length; i++){ + begin = i - size + 1; + if(q.isEmpty()) + q.add(i); + else if(begin > q.peekFirst()) + q.pollFirst(); + + while((!q.isEmpty()) && num[q.peekLast()] <= num[i]) + q.pollLast(); + q.add(i); + if(begin >= 0) + res.add(num[q.peekFirst()]); + } + return res; + } + } + +# 字符串 + +## 字符串的排列 + +输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 + + 解析:这是一个全排列问题,也就是N个不同的数排成所有不同的序列,只不过把数换成了字符串。 + 全排列的过程就是,第一个元素与后续的某个元素交换,然后第二个元素也这么做,直到最后一个元素为之,过程是一个递归的过程,也是一个dfs的过程。 + + 注意元素也要和自己做一次交换,要不然会漏掉自己作为头部的情况。 + + 然后再进行一次字典序的排序即可。 + + public static ArrayList Permutation(String str) { + char []arr = str.toCharArray(); + List list = new ArrayList<>(); + all(arr, 0, arr.length - 1, list); + Collections.sort(list, (o1, o2) -> String.valueOf(o1).compareTo(String.valueOf(o2))); + ArrayList res = new ArrayList<>(); + for (char[] c : list) { + if (!res.contains(String.valueOf(c))) + res.add(String.valueOf(c)); + } + return res; + } + + //注意要换完为之,因为每换一次可以去掉头部一个数字,这样可以避免重复 + public static void all(char []arr, int cur, int end, List list) { + if (cur == end) { + // System.out.println(Arrays.toString(arr)); + list.add(Arrays.copyOf(arr, arr.length)); + } + for (int i = cur;i <= end;i ++) { + //这里的交换包括跟自己换,所以只有一轮换完才能确定一个结果 + swap(arr, cur, i); + all(arr, cur + 1, end, list); + swap(arr, cur, i); + } + } + public static void swap(char []arr, int i, int j) { + if (i > arr.length || j > arr.length || i >= j)return; + char temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + +## 替换空格 + + /** + * Created by 周杰伦 on 2018/2/25. + * 请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 + */ + + 解析:如果单纯地按顺序替换空格,每次替换完还要将数组扩容,再右移,这部操作的时间复杂度就是O(2*N)=O(N),所以总的复杂度是O(n^2),所以应该采取先扩容的办法,统计出空格数,然后扩容,接下来按顺序添加字符,遇到空格直接改成添加%20即可,这样避免了右移操作和多次扩容,复杂度是O(N) + + + public class 替换空格 { + public static String replaceSpace(StringBuffer str) { + int newlen = 0; + for(int i = 0; i < str.length(); i++) { + if(str.charAt(i) == ' ') { + newlen = newlen + 3; + } + else { + newlen ++; + } + } + char []newstr = new char[newlen]; + int j = 0; + for(int i = 0 ; i < str.length(); i++) { + if (str.charAt(i) == ' ') { + newstr[j++] = '%'; + newstr[j++] = '2'; + newstr[j++] = '0'; + }else { + newstr[j++] = str.charAt(i); + } + } + return String.valueOf(newstr); + } + + +## 第一次只出现一次的字符 + +哈希表可解 + +## 翻转单词顺序和左旋转字符串 + +1 +题目描述 +牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么? + + 这个解法很经典,先把每个单词逆序,再把整个字符串逆序,结果就是把每个单词都进行了翻转。 + +2 +汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! + + 字符串循环左移N位的处理方法也很经典,先把前N位逆序,再把剩余字符串逆序,最后整体逆序。 + + abcXYZdef -> cbafedZYX -> XYZdefabc + +## 把字符串转换为整数 + +题目描述 +将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0 + +解析:首先需要判断正负号,然后判断每一位是否是数字,然后判断是否溢出,判断溢出可以通过加完第n位的和与未加第n位的和进行比较。最后可以得出结果。所以需要3-4步判断。 + + +## 表示数值的字符串 + +请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。 + + 不得不说这种题型太恶心了,就是需要一直判断边界条件 + + 参考一个答案。比较完整 + + bool isNumeric(char* str) { + // 标记符号、小数点、e是否出现过 + bool sign = false, decimal = false, hasE = false; + for (int i = 0; i < strlen(str); i++) { + if (str[i] == 'e' || str[i] == 'E') { + if (i == strlen(str)-1) return false; // e后面一定要接数字 + if (hasE) return false; // 不能同时存在两个e + hasE = true; + } else if (str[i] == '+' || str[i] == '-') { + // 第二次出现+-符号,则必须紧接在e之后 + if (sign && str[i-1] != 'e' && str[i-1] != 'E') return false; + // 第一次出现+-符号,且不是在字符串开头,则也必须紧接在e之后 + if (!sign && i > 0 && str[i-1] != 'e' && str[i-1] != 'E') return false; + sign = true; + } else if (str[i] == '.') { + // e后面不能接小数点,小数点不能出现两次 + if (hasE || decimal) return false; + decimal = true; + } else if (str[i] < '0' || str[i] > '9') // 不合法字符 + return false; + } + return true; + } + +## 字符流中第一个不重复的字符 + +题目描述 +请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。 + +本题主要要注意的是流。也就是说每次输入一个字符就要做一次判断。比如输入aaaabbbcd,输出就是a###b##cd + + StringBuilder sb = new StringBuilder(); + int []map = new int[256]; + public void Insert(char ch) + { + sb.append(ch); + if (map[ch] == 0) { + map[ch] = 1; + }else { + map[ch] ++; + } + System.out.println(FirstAppearingOnce()); + + } + //return the first appearence once char in current stringstream + public char FirstAppearingOnce() + { + for (int i = 0;i < sb.length();i ++) { + if (map[sb.charAt(i)] == 1) { + return sb.charAt(i); + } + } + return '#'; + } + +# 链表 + +## 从尾到头打印链表 + +考查递归,递归可以使输出的顺序倒置 + + public static void printReverse(Node node) { + if (node.next != null) { + printReverse(node.next); + } + System.out.print(node.val + " "); + } + + +## 链表倒数第k个节点 + +使用两个指针,一个先走k步。然后一起走即可。 + +## 反转链表 + +老生常谈,但是容易写错。 + + public ListNode ReverseList(ListNode head) { + if(head==null || head.next==null)return head; + ListNode pre,next; + pre=null; + next=null; + while(head!=null){ + //保存下一个结点 + next=head.next; + //连接下一个结点 + head.next=pre; + pre=head; + head=next; + } + return pre; + } + } + +## 合并两个排序链表 + +与归并排序的合并类似 + +## 复杂链表的复制 + +题目描述 +输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空) + +这题比较恶心。 + +解析: + +1 直接复制链表,然后再去复制特殊指针,复杂度是O(n2) + +2 使用hash表保存特殊指针的映射关系,第二步简化操作,复杂度是O(n) + +3 复制每个节点并且连成一个大链表A-A'-B-B',然后从头到尾判断特殊指针,如果有特殊指针,则让后续节点的特殊指针指向原节点特殊指针指向的节点的后置节点,晕了吧,其实就是原来是A指向B,现在是A’指向B‘。 + +最后我们根据奇偶序号把链表拆开,复杂度是O(N)且不用额外空间。 + +## 两个链表的第一个公共节点 + +1 逆置链表,反向找第一个不同节点,前一个就是公共节点 + +2 求长度并相减得n,短的链表先走n步,然后一起走即可。 + +## 孩子们的游戏(圆圈中最后剩下的数) + +这是一个约瑟夫环问题。 + +1 使用循环链表求解,每次走n步摘取一个节点,然后继续,直到最后一个节点就是剩下的数,空间复杂度为O(n) + +2 使用数组来做 +public static int LastRemaining_Solution(int n, int m) { + int []arr = new int[n]; + for (int i = 0;i < n;i ++) { + arr[i] = i; + } + + int cnt = 0; + int sum = 0; + for (int i = 0;i < n;i = (i + 1) % n) { + if (arr[i] == -1) { + continue; + } + cnt ++; + if (cnt == m) { + arr[i] = -1; + cnt = 0; + sum ++; + } + if (sum == n) { + return i; + } + } + return n - 1; + } + +3 使用余数法求解 + + + int LastRemaining_Solution(int n, int m) { + if (m == 0 || n == 0) { + return -1; + } + ArrayList data = new ArrayList(); + for (int i = 0; i < n; i++) { + data.add(i); + } + int index = -1; + while (data.size() > 1) { + // System.out.println(data); + index = (index + m) % data.size(); + // System.out.println(data.get(index)); + data.remove(index); + index--; + } + return data.get(0); + } + +## 链表的环的入口结点 + +一个链表中包含环,请找出该链表的环的入口结点。 + +解析: + +1 指定两个指针,一个一次走两步,一个一次走一步,然后当两个节点相遇时,这个节点必定在环中。既然这个节点在环中,那么让这个节点走一圈直到与自己相等为之,可以得到环的长度n。 + +2 得到了环的长度以后,根据数学推导的结果,我们可以指定两个指针,一个先走n步,然后两者同时走,这样的话,当慢节点到达入口节点时,快节点也转了一圈刚好又到达入口节点,所以也就是他们相等的时候就是入口节点了。 + +## 删除链表中重复的节点 + +题目描述 +在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5 + +保留头结点,然后找到下一个不重复的节点,与他相连,重复的节点直接跳过即可。 + +#二叉树 + +## 二叉搜索树转换为双向链表 + +输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 + +二叉搜索树要转换成有序的双向链表,实际上就是使用中序遍历把节点连入链表中,并且题目要求在原来节点上进行操作,也就是使用左指针和右指针表示链表的前置节点和后置节点。 + +使用栈实现中序遍历的非递归算法,便可以找出节点的先后关系,依次连接即可。 + + public TreeNode Convert(TreeNode root) { + if(root==null) + return null; + Stack stack = new Stack(); + TreeNode p = root; + TreeNode pre = null;// 用于保存中序遍历序列的上一节点 + boolean isFirst = true; + while(p!=null||!stack.isEmpty()){ + while(p!=null){ + stack.push(p); + p = p.left; + } + p = stack.pop(); + if(isFirst){ + root = p;// 将中序遍历序列中的第一个节点记为root + pre = root; + isFirst = false; + }else{ + pre.right = p; + p.left = pre; + pre = p; + } + p = p.right; + } + return root; + } + } + +## 重建二叉树 + + * 题目描述 + 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 + */ + + 解析:首先,头结点一定是先序遍历的首位,并且该节点把中序分为左右子树,根据这个规则,左子树由左边数组来完成,右子树由右边数组来完成,根节点由中间节点来构建,于是便有了如下的递归代码。该题的难点就在于边界的判断。 + + public TreeNode reConstructBinaryTree(int [] pre, int [] in) { + if(pre.length == 0||in.length == 0){ + return null; + } + TreeNode node = new TreeNode(pre[0]); + for(int i = 0; i < in.length; i++){ + if(pre[0] == in[i]){ + node.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), Arrays.copyOfRange(in, 0, i));//为什么不是i和i-1呢,因为要避免出错,中序找的元素需要再用一次。 + node.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i+1, pre.length), Arrays.copyOfRange(in, i+1,in.length)); + } + } + return node; + } + +## 树的子结构 + + /** + * Created by 周杰伦 on 2018/3/27. + * 输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) + */ + + 解析:本题还是有点难度的,子结构要求节点完全相同,所以先判断节点是否相同,然后使用先序遍历进行递判断,判断的依据是如果子树为空,则说明节点都找到了,如果原树节点为空,说明找不到对应节点,接着递归地判断该节点的左右子树是否符合要求. + + public class 树的子结构 { + + public boolean HasSubtree(TreeNode root1, TreeNode root2) { + boolean res = false; + if (root1 != null && root2 != null) { + if (root1.val == root2.val) { + res = aHasb(root1, root2); + } + if (res == false) { + res = HasSubtree(root1.left,root2); + } + if (res == false) { + res = HasSubtree(root1.right,root2); + } + return res; + } + else return false; + } + public boolean aHasb(TreeNode t1, TreeNode t2){ + if (t2 == null) return true; + if (t1 == null) return false; + if (t1.val != t2.val) return false; + + return aHasb(t1.left,t2.left) && aHasb(t1.right,t2.right); + } + } + + +## 镜像二叉树 + + /** + * Created by 周杰伦 on 2017/3/19.操作给定的二叉树,将其变换为源二叉树的镜像。 + 输入描述: + 二叉树的镜像定义:源二叉树 + 8 + / \ + 6 10 + / \ / \ + 5 7 9 11 + 镜像二叉树 + 8 + / \ + 10 6 + / \ / \ + 11 9 7 5 + */ + + + 解析:其实镜像二叉树就是交换所有节点的左右子树,所以使用遍历并且进行交换即可。 + + /** + public class TreeNode { + int val = 0; + TreeNode left = null; + TreeNode right = null; + + public TreeNode(int val) { + this.val = val; + + } + + } + */ + public class 镜像二叉树 { + public void Mirror(TreeNode root) { + if(root == null)return; + if(root.left!=null || root.right!=null) + { + TreeNode temp=root.left; + root.left=root.right; + root.right=temp; + } + Mirror(root.left); + Mirror(root.right); + + } + + +## 树的层次遍历 + +也就是从上到下打印节点,使用队列即可完成。 + +## 二叉树的深度 + +经典遍历。 + +## 判断是否平衡二叉树 + +判断左右子树的高度差是否 <= 1即可。 + +## 二叉搜索树的后序遍历 + +题目描述 +输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。 + +解析:这题其实也非常巧妙。二叉搜索树的特点就是他的左子树都比根节点小,右子树都比跟节点大。而后序遍历的根节点在最后,所以后续遍历的第1到N-1个节点应该是左右子树的节点(不一定左右子树都存在)。 + +后续遍历的序列是先左子树,再右子树,最后根节点,那么就要求,左半部分比根节点小,右半部分比根节点大,当然,左右部分不一定都存在。 + +所以,找出根节点后,首先找出左半部分,要求小于根节点,然后找出右半部分,要求大于根节点,如果符合,则递归地判断左右子树到的根节点(本步骤已经将左右部分划分,割据中间节点进行递归),如果不符合,直接返回false。 + +同理也可以判断前序遍历和中序遍历。 + + public class 二叉搜索树的后序遍历序列 { + public static void main(String[] args) { + int []a = {7,4,6,5}; + System.out.println(VerifySquenceOfBST(a)); + } + public static boolean VerifySquenceOfBST(int [] sequence) { + if (sequence == null || sequence.length == 0) { + return false; + } + return isBST(sequence, 0, sequence.length - 1); + } + public static boolean isBST(int []arr, int start, int end) { + if (start >= end) return true; + int root = arr[end]; + int mid = start; + for (mid = start;mid < end && arr[mid] < root;mid ++) { + + } + for (int i = mid;i < end; i ++) { + if (arr[i] < root)return false; + } + return isBST(arr, start, mid - 1) && isBST(arr, mid, end - 1); + } + } + +## 二叉树中和为某一值的路径 + +/** + * Created by 周杰伦 on 2018/3/29. + * 题目描述 + 输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 + */ + + 解析:由于要求从根节点到达叶子节点,并且要打印出所有路径,所以实际上用到了回溯的思想。 + + 通过target跟踪当前和,进行先序遍历,当和满足要求时,加入集合,由于有多种结果,所以需要回溯,将访问过的节点弹出访问序列,才能继续访问下一个节点。 + + 终止条件是和满足要求,并且节点是叶节点,或者已经访问到空节点也会返回。 + + + public class 二叉树中和为某一值的路径 { + private ArrayList> listAll = new ArrayList>(); + private ArrayList list = new ArrayList(); + public ArrayList> FindPath(TreeNode root,int target) { + if(root == null) return listAll; + list.add(root.val); + target -= root.val; + if(target == 0 && root.left == null && root.right == null) + listAll.add(new ArrayList(list)); + FindPath(root.left, target); + FindPath(root.right, target); + list.remove(list.size()-1); + return listAll; + } + + static int count = 0; + static Stack path = new Stack<>(); + static Stack stack = new Stack<>(); + static ArrayList> lists = new ArrayList<>(); + } + +## 二叉树的下一个节点 + +给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 + + 解析:给出一个比较好懂的解法,中序遍历的结果存在集合中,找到根节点,进行中序遍历,然后找到该节点,下一个节点就是集合后一位 + + public TreeLinkNode GetNext(TreeLinkNode TreeLinkNode) + { + return findNextNode(TreeLinkNode); + } + public TreeLinkNode findNextNode(TreeLinkNode anynode) { + if (anynode == null) return null; + TreeLinkNode p = anynode; + while (p.next != null) { + p = p.next; + } + ArrayList list = inOrderSeq(p); + for (int i = 0;i < list.size();i ++) { + if (list.get(i) == anynode) { + if (i + 1 < list.size()) { + return list.get(i + 1); + } + else return null; + } + } + return null; + + } + static ArrayList list = new ArrayList<>(); + public static ArrayList inOrderSeq(TreeLinkNode TreeLinkNode) { + if (TreeLinkNode == null) return null; + inOrderSeq(TreeLinkNode.left); + list.add(TreeLinkNode); + inOrderSeq(TreeLinkNode.right); + return list; + } + +## 对称的二叉树 + +请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 + +解析,之前有一题是二叉树的镜像,递归交换左右子树即可求出镜像,然后递归比较两个树的每一个节点,则可以判断是否对称。 + + boolean isSymmetrical(TreeNode pRoot) + { + TreeNode temp = copyTree(pRoot); + Mirror(pRoot); + return isSameTree(temp, pRoot); + } + + + void Mirror(TreeNode root) { + if(root == null)return; + Mirror(root.left); + Mirror(root.right); + if(root.left!=null || root.right!=null) + { + TreeNode temp=root.left; + root.left=root.right; + root.right=temp; + } + + + } + boolean isSameTree(TreeNode t1,TreeNode t2){ + if(t1==null && t2==null)return true; + else if(t1!=null && t2!=null && t1.val==t2.val) { + boolean left = isSameTree(t1.left, t2.left); + boolean right = isSameTree(t1.right, t2.right); + return left && right; + } + else return false; + } + + TreeNode copyTree (TreeNode root) { + if (root == null) return null; + TreeNode t = new TreeNode(root.val); + t.left = copyTree(root.left); + t.right = copyTree(root.right); + return t; + } +## 把二叉树打印成多行 + +题目描述 +从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。 + +解析:1 首先要知道到本题的基础思想,层次遍历。 + +2 然后是进阶的思想,按行打印二叉树并输出行号,方法是,一个节点last指向当前行的最后一个节点,一个节点nlast指向下一行最后一个节点。使用t表示现在遍历的节点,当t = last时,表示本行结束。此时last = nlast,开始下一行遍历。 + +同时,当t的左右子树不为空时,令nlast = t的左子树和右子树。每当last 赋值为nlast时,行号加一即可。 + +## 按之字形顺序打印二叉树 + +请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 + +解析:1 首先要知道到本题的基础思想,层次遍历。 + +2 然后是进阶的思想,按行打印二叉树并输出行号,方法是,一个节点last指向当前行的最后一个节点,一个节点nlast指向下一行最后一个节点。使用t表示现在遍历的节点,当t = last时,表示本行结束。此时last = nlast,开始下一行遍历。 + +同时,当t的左右子树不为空时,令nlast = t的左子树和右子树。每当last 赋值为nlast时,行号加一即可。 + +3 基于第2步的思想,现在要z字型打印,只需把偶数行逆序即可。所以把每一行的数存起来,然后偶数行逆置即可。 + + ArrayList > Print(TreeNode pRoot) { + LinkedList queue = new LinkedList<>(); + TreeNode root = pRoot; + if(root == null) { + return new ArrayList<>(); + } + TreeNode last = root; + TreeNode nlast = root; + queue.offer(root); + ArrayList list = new ArrayList<>(); + list.add(root.val); + ArrayList one = new ArrayList<>(); + one.addAll(list); + ArrayList> lists = new ArrayList<>(); + lists.add(one); + list.clear(); + + int row = 1; + while (!queue.isEmpty()){ + + TreeNode t = queue.poll(); + + if(t.left != null) { + queue.offer(t.left); + list.add(t.left.val); + nlast = t.left; + } + if(t.right != null) { + queue.offer(t.right); + list.add(t.right.val); + nlast = t.right; + } + if(t == last) { + if(!queue.isEmpty()) { + last = nlast; + row ++; + ArrayList temp = new ArrayList<>(); + temp.addAll(list); + list.clear(); + if (row % 2 == 0) { + Collections.reverse(temp); + } + lists.add(temp); + + } + } + } + + return lists; + } + +## 序列化和反序列化二叉树 + +解析:序列化和反序列化关键是要确定序列化方式。我么使用字符串来序列化。 + +用#代表空,用!分隔左右子树。 + +比如 1 + 2 3 + 4 5 + +使用先序遍历 +序列化结果是1!2!4!###3!#5!## + +反序列化先让根节点指向第一位字符,然后左子树递归进行连接,右子树 + + public class Solution { + public int index = -1; + StringBuffer sb = new StringBuffer(); + + String Serialize(TreeNode root) { + if(root == null) { + sb.append("#!") ; + } + else { + sb.append(root.val + "!"); + Serialize(root.left); + Serialize(root.right); + } + + return sb.toString(); + } + TreeNode Deserialize(String str) { + index ++; + int len = str.length(); + if(index >= len) { + return null; + } + String[] strr = str.split("!"); + TreeNode node = null; + if(!strr[index].equals("#")) { + node = new TreeNode(Integer.valueOf(strr[index])); + node.left = Deserialize(str); + node.right = Deserialize(str); + } + return node; + } + } + +## 二叉搜索树的第k个结点 + + 解析:二叉搜索树的中序遍历是有序的,只需要在中序中判断数字是否在第k个位置即可。 + 如果在左子树中发现了,那么递归返回该节点,如果在右子树出现,也递归返回该节点。注意必须要返回,否则结果会被递归抛弃掉。 + + TreeNode KthNode(TreeNode pRoot, int k) + { + count = 0; + return inOrderSeq(pRoot, k); + } + static int count = 0; + public TreeNode inOrderSeq(TreeNode treeNode, int k) { + if (treeNode == null) return null; + TreeNode left = inOrderSeq(treeNode.left, k); + if (left != null) return left; + if (++ count == k) return treeNode; + TreeNode right = inOrderSeq(treeNode.right, k); + if (right != null) return right; + return null; + } + +# 栈和队列 + +## 用两个队列实现栈,用两个栈实现队列。 + + +简单说下思路 + +1 两个栈实现队列,要求先进先出,入队时节点先进入栈A,如果栈A满并且栈B空则把全部节点压入栈B。 + +出队时,如果栈B为空,那么直接把栈A节点全部压入栈B,再从栈B出栈,如果栈B不为空,则从栈B出栈。 + +2 两个队列实现栈,要求后进先出。入栈时,节点先加入队列A,出栈时,如果队列B不为空,则把头结点以后的节点出队并加入到队列B,然后自己出队。 + +如果出栈时队列B不为空,则把B头结点以后的节点移到队列A,然后出队头结点,以此类推。 + +## 包含min函数的栈 + +/** + * 设计一个返回最小值的栈 + * 定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的min函数。 + * Created by 周杰伦 on 2017/3/22. + */ + + 解析:这道题的解法也是非常巧妙的。因为每次进栈和出栈都有可能导致最小值发生改变。而我们要维护的是整个栈的最小值。 + + 如果单纯使用一个数来保存最小值,会出现一种情况,最小值出栈时,你此时的最小值只能改成栈顶元素,但这个元素不一定时最小值。 + + 所以需要一个数组来存放最小值,或者是一个栈。 + + 使用另一个栈B存放最小值,每次压栈时比较节点值和栈B顶端节点值,如果比它小则压栈,否则不压栈,这样就可以从b的栈顶到栈顶依次访问最小值,次小值。以此类推。 + + 当最小值节点出栈时,判断栈B顶部的节点和出栈节点是否相同,相同则栈B也出栈。 + + 这样就可以维护一个最小值的函数了。 + + 同理,最大值也是这样。 + + + public class 包含min函数的栈 { + Stack stack=new Stack<>(); + Stack minstack=new Stack<>(); + + public void push(int node) { + if(stack.isEmpty()) + { + stack.push(node); + minstack.push(node); + } + else if(node stack = new Stack<>(); + int j = 0; + int i = 0; + while (i < pushA.length) { + stack.push(pushA[i]); + i++; + while (!stack.empty() && stack.peek() == popA[j]) { + stack.pop(); + j++; + } + if (i == pushA.length) { + if (!stack.empty()) { + return false; + } else return true; + } + } + return false; + } + + +# 排序和查找 + +## 旋转数组的最小数字 + + 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 + + 解析:这题的思路很巧妙,如果直接遍历复杂度为O(N),但是使用二分查找可以加快速度,因为两边的数组都是递增的最小值一定在两边数组的边缘,于是通过二分查找,逐渐缩短左右指针的距离,知道左指针和右指针只差一步,那么右指针所在的数就是最小值了。 + 复杂度是O(logN) + + //这段代码忽略了三者相等的情况 + public int minNumberInRotateArray(int [] array) { + if (array.length == 0) return 0; + if (array.length == 1) return array[0]; + int min = 0; + + int left = 0, right = array.length - 1; + //只有左边值大于右边值时,最小值才可能出现在中间 + while (array[left] > array[right]) { + int mid = (left + right)/2; + if (right - left == 1) { + min = array[right]; + break; + } + //如果左半部分递增,则最小值在右侧 + if (array[left] < array[mid]) { + left = mid; + } + //如果右半部分递增,则最小值在左侧。 + //由于左边值比右边值大,所以两种情况不会同时发生 + else if (array[right] > array[mid]) { + right = mid ; + } + } + return array[min]; + + } + + + 注意:但是当arr[left] = arr[right] = arr[min]时。三个数都相等无法确定最小值,此时只能遍历。 + +# 递归 + +## 斐波那契数列 + +1递归做法 + +2记忆搜索,用数组存放使用过的元素。 + +3DP,本题中dp就是记忆化搜索 + +## 青蛙跳台阶 + +一次跳两步或者跳一步,问一共多少种跳法到达n级,所以和斐波那契数列是一样的。 + +## 变态跳台阶 + +一次跳1到n步,问一共几种跳法,这题是找数字规律的,一共有2^(n-1)种方法 + +## 矩形覆盖 + +和上题一样,也是找规律,答案也是2^(n-1) + +# 位运算 + +## 二进制中1的个数 + + * Created by 周杰伦 on 2018/6/29. + * 题目描述 + * 输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。 + + 解析: + 1 循环右移数字n,每次判断最低位是否为1,但是可能会导致死循环。 + + 2 使用数字a = 1和n相与,a每次左移一位,再与n相与得到次低位,最多循环32次,当数字1左移32次也会等于0,所以结束循环。 + + 3 非常奇葩的做法,把一个整数减去1,再与原整数相与,会把最右边的一个1变成0,于是统计可以完成该操作的次数即可知道有多少1了。 + + public class 二进制中1的个数 { + public static int NumberOf1(int n) { + int count = 0; + while (n != 0) { + ++count; + n = (n - 1) & n; + } + return count; + } + } + +## 数组中只出现一次的数字 + +题目描述 +一个整型数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这一个只出现一次的数字。 + +解析:左神称之为神仙题。 + +利用位运算的异或操作^。 +由于a^a = 0,0^b=b,所以。所有数执行异或操作,结果就是只出现一次的数。 + +## 不用加减乘除做加法 + +解析:不用加减乘,那么只能用二进制了。 + +两个数a和b,如果不考虑进位,则0 + 1 = 1,1 + 1 = 0,0 + 0 = 0,这就相当于异或操作。 +如果考虑进位,则只有1 + 1有进位,所以使用相与左移的方法得到每一位的进位值,再通过异或操作和原来的数相加。当没有进位值的时候,运算结束。 + public static int Add(int num1,int num2) { + if( num2 == 0 )return num1; + if( num1 == 0 )return num2; + + int temp = num2; + while(num2!=0) { + temp = num1 ^num2; + num2 = (num1 & num2)<<1; + num1 = temp; + } + return num1; + } + +# 回溯和DFS + +## 矩阵中的路径 + +题目描述 +请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 + +解析:回溯法也就是特殊的dfs,需要找到所有的路径,所以每当到达边界条件或抵达目标时,递归返回,由于需要保存路径中的字母,所以递归返回时需要删除路径最后的节点,来保证路径合法。不过本题只有一个解,所以找到即可返回。 + + public class 矩阵中的路径 { + public static void main(String[] args) { + char[][]arr = {{'a','b','c','e'},{'s','f','c','s'},{'a','d','e','e'}}; + char []str = {'b','c','c','e','d'}; + System.out.println(hasPath(arr, arr.length, arr[0].length, str)); + } + static int flag = 0; + public static boolean hasPath(char[][] matrix, int rows, int cols, char[] str) + { + int [][]visit = new int[rows][cols]; + StringBuilder sb = new StringBuilder(); + for (int i = 0;i < rows;i ++) { + for (int j = 0;j < cols;j ++) { + if (matrix[i][j] == str[0]) { + visit[i][j] = 1; + sb.append(str[0]); + dfs(matrix, i, j, visit, str, 1, sb); + visit[i][j] = 0; + sb.deleteCharAt(sb.length() - 1); + } + } + } + return flag == 1; + } + public static void dfs(char [][]matrix, int row, int col, int [][]visit, char []str, int cur, StringBuilder sb) { + if (sb.length() == str.length) { + // System.out.println(sb.toString()); + flag = 1; + return; + } + + int [][]pos = {{1,0},{-1,0},{0,1},{0,-1}}; + for (int i = 0;i < pos.length;i ++) { + int x = row + pos[i][0]; + int y = col + pos[i][1]; + if (x >= matrix.length || x < 0 || y >= matrix[0].length || y < 0) { + continue; + } + if (visit[x][y] == 0 && matrix[x][y] == str[cur]) { + sb.append(matrix[x][y]); + visit[x][y] = 1; + dfs(matrix, x, y, visit, str, cur + 1, sb); + sb.deleteCharAt(sb.length() - 1); + visit[x][y] = 0; + } + } + } + + +## 机器人的运动范围 + +题目描述 +地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子? + + 解析:这是一个可达性问题,使用dfs方法,走到的每一格标记为走过,走到无路可走时就是最终的结果。每次都有四个方向可以选择,所以写四个递归即可。 + + public class Solution { + static int count = 0; + public static int movingCount(int threshold, int rows, int cols) + { + count = 0; + int [][]visit = new int[rows][cols]; + dfs(0, 0, visit, threshold); + return count; + } + + public static void dfs(int row, int col, int[][]visit, int k) { + if (row >= visit.length || row < 0 || col >= visit[0].length || col < 0) { + return; + } + if (sum(row) + sum(col) > k) { + return; + } + + if (visit[row][col] == 1){ + return; + } + + visit[row][col] = 1; + count ++; + dfs(row + 1,col,visit, k); + dfs(row - 1,col,visit, k); + dfs(row,col + 1,visit, k); + dfs(row,col - 1,visit, k); + + } + + public static int sum(int num) { + String s = String.valueOf(num); + int sum = 0; + for (int i = 0;i < s.length();i ++) { + sum += Integer.valueOf(s.substring(i, i + 1)); + } + return sum; + } + } \ No newline at end of file diff --git "a/md/\346\223\215\344\275\234\347\263\273\347\273\237\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/md/\346\223\215\344\275\234\347\263\273\347\273\237\345\255\246\344\271\240\346\200\273\347\273\223.md" new file mode 100644 index 0000000..2740f29 --- /dev/null +++ "b/md/\346\223\215\344\275\234\347\263\273\347\273\237\345\255\246\344\271\240\346\200\273\347\273\223.md" @@ -0,0 +1,304 @@ +--- +title: 操作系统学习总结 +date: 2018-07-09 22:33:03 +tags: + - 操作系统 +categories: + - 后端 + - 技术总结 +--- +##这部分内容主要是基于一些关于操作系统基础的学习总结,内容不全面,只讲述了其中的一小部分,后续会再补充,如有错误,还请见谅。 +# 操作系统 + +## CPU +cpu是中央处理器,他是计算机的核心。 +cpu通过和寄存器,高速缓存,以及内存交互来执行程序。 + +主板分为南桥和北桥,北桥主要是内存总线,通往内存。 +而南桥主要是慢速设备的IO总线,包括硬盘,网卡等IO设备。 + +32位cpu最多寻址4g内存,而64位cpu目前来说没有上限。 + +### 程序执行过程 +cpu发出指令,将硬盘上的一段程序读入内存,由于cpu和硬盘的速度差距更大,一般使用中断,dma等方式来将硬盘数据载入内存。 +然后cpu通过寄存器以及指令集执行指令,cpu读取内存上的代码,内存上的方法执行是一个栈调用的过程。 + +### 高速缓存 读写缓冲区 +为了弥补cpu和内存的速度差,cpu配有多级缓存。 + +一般有一级缓存和二级缓存,缓存根据局部性原理把经常使用的代码加载如缓存,能比直接访问内存快上几百倍。 + +同样的,内存和硬盘间的速度差距也很大,需要通过读写缓冲区来进行速度匹配,内存写入磁盘时先写入缓冲区,读数据时从缓冲区读取硬盘准备好的数据。 + + +## 内存管理和虚拟内存 + +由于程序的大小越来越大,而计算机想要支持多道程序,当一个程序遇到IO操作时转而去执行另一个程序,这就要求内存中装有多个程序的代码了。 + +然而程序的内存消耗与日俱增,同时装载多个程序越来越困难,所以人们提出了,只在程序需要使用到的时候再把他装入内存,平时把代码放在硬盘中即可。 + +### 分页 + +由于内存需要装载硬盘中的数据,所以需要约定一个存储单元,操作系统把它叫做页,一个页一般长度是8kb或者16kb。内存从硬盘读取数据时读取的是一个页或多个页。 + +### 虚拟内存 + + +由于这些代码持久化在硬盘中,占用了一部分空间,并且需要运行时会加载到内存中,所以这部分的空间一般也成为虚拟内存的空间(大小)。 + +### 页表和页面置换算法 + +为了知道每个程序对应的代码存在硬盘中的位置,操作系统需要维护一个程序到页面的映射表,cpu要内存加载一个页面时,首先要访问页表,来得知页面在硬盘中的位置,然后让内存去该位置把对应的页面调入内存中。 + +为了提升页面调入的效率,也使用了多种的页面置换算法,比如lru最近最久未使用,fifo先进先出,时钟法,多级队列法等。 + +当然,为了进一步提高效率,还会使用多级页表的方式,不过一般需要硬件维护一个页表映射页面的快速转换结构,以便能迅速地完成页面解析和调度。 + +### 中断和缺页中断 + +计算机提供一系列中断指令与硬件进行交互,操作系统可以使用这些中断去控制硬件,比如使用中断通知cpu硬盘的IO操作已经准备好,键盘通过一次又一次的中断来输入字符。 + +中断一般适用于快速返回的字符设备,比如鼠标键盘,而硬盘这类耗时IO操作,使用中断后cpu仍然需要等待硬盘把数据装入内存。是非常缓慢的。 + +于是才会使用DMA来完成IO操作,DMA额外提供一个处理器去处理IO操作,当硬盘数据加载到内存中后再去通知CPU操作已完成。 + + 缺页中断就是因为内存中没有cpu需要访问的页面,必须根据页表到硬盘中获取页面,并根据置换算法进行页面调度。如果找不到对应页面,则程序执行会报错。 + +### 分页和分段 + +分页是上述所说的,通过内存和硬盘的约定,将调度单元设定为一个页面。 + + 分段则不同,分段并不是物理意义上的分段,而是逻辑上把代码使用到的空间划分成多个部分,比如受保护部分,公开部分,核心部分等,这样可以更好地描述每个段的代码信息,为使用者提供便利,为了支持程序员的这种需求,操作系统加入了分段的概念,将代码对应的一段虚拟内存划分为不同的逻辑段。 + + 同时为了根据不同段来以不同方式访问内存,操作系统需要另外维护一个段表,以用于段的映射。 + +由于分段只是逻辑上的概念,所以底层的内存分页仍然是必不可少的,因此在逻辑段的基础上,物理上又会划分为多个页,一个段中可能包含了多个页面。 + +因此,完善的虚拟内存管理器需要支持段页表,先映射段,再映射页面。 + +## 进程与线程 + +### 进程 +进程是资源分配的资本单位,操作系统为进程开辟一段内存空间,内存空间从高位向低位,包括函数调用栈,变量以及其他区域。cpu根据这些信息配合寄存器进行函数调用和程序执行。 + +### 多进程 +由于计算机是分时系统,所以多进程的使用不可避免,操作系统需要进行进程的切换,方法是内存指针指向新位置,保存原来的进程信息,同时刷新寄存器等数据。然后开始执行新的进程. + +一般操作系统会使用pcb结构来记录进程的信息和上下文。通过他和cpu配合来完成进程切换。 + +### 线程 +线程是系统调度的基本单位,没有线程以前,一般会使用多进程模型,一个程序往往需要多个进程配合使用,但是多进程之间并没有共享内存,导致进程间通信非常麻烦。 + +比如文本输入工具,一边键入文字一边需要保存数据,如果是单进程执行,则每次输入都触发IO操作,非常费时,如果一个进程负责输入展示一个进程负责输入保存,确实速度很快,但是两个进程没办法共享数据。除非使用额外的通讯手段。 +### 多线程 + +而多线程就好办了,多线程都是基于进程产生的,线程被创建后只需要分配少量空间支持堆栈操作即可,同时线程还共享进程中的内存,所以一般使用进程分配资源,线程进行调度的方式。 + + 操作系统对线程的支持: + 一般情况下操作系统都支持线程,并且可以创建内核级线程,内核可以识别线程并且为其分配空间,进行线程调度。 + 但是内核级线程实现比较复杂,使用起来也不甚方便。 + + 所以往往开发人员会使用用户级线程,用户级线程比如Java的多线程,通过简单的api实现,当需要操作系统支持时,才使用底层调用api接口进行内核级的系统调用。 + + 但是一般情况下用户级线程是不被内核识别的,也就是说,用户级线程会被内核认为是一个进程,从而执行进程的调度。这样的话就没有意义了。 + + 所以一般情况下用户级线程会映射到对应的内核级线程中,内核为进程创建一定数量的内核级线程以供使用。Java中的线程基本上都是使用这种方式实现的。 + +### 线程通信和进程通信 + +线程通信一般只需要使用共享内存的方式即可实现。 + +而进程通信则需要额外的通信机制。 + +> 1 信号量,一般多进程间的同步使用信号量来完成,系统为临界区添加支持并发量为n的信号量,多进程访问临界区资源时,首先需要执行p操作来减少信号量,如果信号量等于0则操作失败,并且挂起进程,否则成功进入临界区执行。 +> +> 当进程退出临界区时,执行v操作,将信号量加一,并唤醒挂起的进程进行操作。 +> +> 2 管程,管程是对信号量的一个包装,避免使用信号量时出错。 +> +> 3 管道,直接连接两个进程,一个进程写入管道,另一个进程可以读取管道,但是他不支持全双工,并且只能在父子进程间使用,所以局限性比较大 +> +> 4 消息队列 +> +> 操作系统维护一个消息队列,进程将消息写入队列中,其他进程轮询消息队列看是否有自己的消息,增加了轮询的开销,但是提高了消息的可靠性和易用性,同时支持了订阅消息。 +> +> 5 socket +> +> socket一般用于不同主机上的进程通信,双方通过ip和port的方式寻址到对方主机并找到监听该端口的进程,为了完成通信,他们先建立tcp连接,在此基础上交换数据,也就完成了进程间的通信。 + +### 进程调度 + +不小心把这茬忘了,进程调度算法有几种,fifo先来先服务,短作业优先,时间片轮转,优先级调度,多级反馈队列等。 +基本上可以通过名字知道算法的大概实现。 + +### 死锁 + +死锁的必要条件: + + 1互斥:资源必须是互斥的,只能给一个进程使用 + + 2占有和等待:占有资源时可以请求其他资源 + + 3不可抢占:资源占有时不会被抢 + + 4环路等待:有两个以上的进程组成一个环路,每个进程都在等待下一个进程的资源释放。 + +死锁的处理方法: + +1鸵鸟 + +2死锁预防 + +在程序运行之前预防发生死锁。 + + (一)破坏互斥条件 + + 例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。 + + 这样子就破坏了互斥条件,转而使用单个队列串行执行操作。 + + (二)破坏占有和等待条件 + + 一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 + + 分配了全部资源就不需要去等待其他资源。 + + (三)破坏不可抢占条件 + + 允许抢占式调度。 + + (四)破坏环路等待 + + 给资源统一编号,进程只能按编号顺序来请求资源。 + + 按正确顺序请求资源,就不会发生死锁。 +3死锁避免 + +==银行家算法用于在程序运行时避免发生死锁。== + +银行家算法用于在程序运行时判断资源的分配情况是否是安全状态,如果某一步骤使程序可能发生死锁,银行家算法会拒绝该操作执行,从而避免进入不安全状态。 + +(一)安全状态 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/ed523051-608f-4c3f-b343-383e2d194470.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/ed523051-608f-4c3f-b343-383e2d194470.png) + +图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。 + +定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。 + +(二)单个资源的银行家算法 + +一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/d160ec2e-cfe2-4640-bda7-62f53e58b8c0.png) + +上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 + +4死锁检测和恢复 + +==银行家算法检测程序并且阻止死锁发生,而死锁检测和恢复则 +不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。== + +(一)每种类型一个资源的死锁检测 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/b1fa0453-a4b0-4eae-a352-48acca8fff74.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/b1fa0453-a4b0-4eae-a352-48acca8fff74.png) + +上图为==资源分配图==,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。 + +如果有环则说明有死锁。 + +(二)每种类型多个资源的死锁检测 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png) + +==资源分配矩阵== + +上图中,有三个进程四个资源,每个数据代表的含义如下: + +* E 向量:资源总量 +* A 向量:资源剩余量 +* C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量 +* R 矩阵:每个进程请求的资源数量 + +如果存在请求数量无法被满足时,就会出现死锁。 + +==(三)死锁恢复== + +利用抢占恢复,允许其他进程抢占资源。 + +利用回滚恢复,回滚进程操作,释放其资源。 + +通过杀死进程恢复,杀死进程后释放资源。 + +## IO和磁盘 + +磁盘是块设备,键盘是字符设备,网卡是网络设备。他们都接在IO总线上,属于慢速设备。 + +### 磁盘和寻址 + +磁盘的结构比较复杂,主要通过扇区,盘面和磁头位置决定当前访问的磁盘位置。 + +cpu为了能够访问磁盘内容,首先要把磁盘的内容载入内存中,于是要和内存约定统一的寻址单元,cpu指定一个起始位置,访问该位置以后的n个存储单元,一般存储单元是一个页,16K或者8K。 + +这一操作中,指向起始位置需要随机寻址,而接下来的访问操作是顺序访问,磁盘的随机读写和顺序读写的速度差距是很大的。所以一般会通过缓冲区来缓存IO数据。 + + 磁盘内部一般也会分为很多部分,比如操作系统会将磁盘做一个分区,使用磁盘的一些位置存储元数据信息,以保证磁盘能够支持操作系统以及文件系统。 + + 一般在物理分区的起始位置会有一个引导区和分区表,BIOS自动将磁盘中引导区的内核程序载入内存,此时操作系统才开始运行,并且根据分区表操作系统可以知道每个分区的起始位置在哪。 + +读写一个磁盘块的时间的影响因素有: + +旋转时间(主轴旋转磁盘,使得磁头移动到适当的扇区上) +寻道时间(制动手臂移动,使得磁头移动到适当的磁道上) +实际的数据传输时间 +其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。 + +1. 先来先服务 +FCFS, First Come First Served + +按照磁盘请求的顺序进行调度。 + +优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。 + +2. 最短寻道时间优先 +SSTF, Shortest Seek Time First + +优先调度与当前磁头所在磁道距离最近的磁道。 + +虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两边的磁道请求更容易出现饥饿现象。 + +3. 电梯算法 +SCAN + +电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。 + +电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。 + +因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。 + +### IO设备 + +除了硬盘以外,还有键盘,网卡等IO设备,这些设备需要操作系统通过驱动程序来进行交互,驱动程序用于适配这些设备。 + +为了执行IO操作,内核一般要为IO设备提供一个缓存区,比如网卡的IO操作,会为socket提供一个缓存区,当多个socket使用一个缓冲区进行通信,就是复用了缓冲区,也就是IO复用的一种方式。 + +同时,内核还会维护一个IO请求的列表,当IO请求就绪时,让几个线程去执行IO操作,实现了线程的复用。 + +### 文件系统 + +文件系统是基于底层存储建立的一个树形文件结构。比较经典的是Linux的文件系统,首先在硬盘的超级块中安装文件系统,磁盘引导时会加载文件系统的信息。 + +linux使用inode来标识任意一个文件。inode存储除了文件名以外的文件信息,包括创建时间,权限,以及一个指向磁盘存储位置的指针,那里才是真正存放数据的地方。 + +一个目录也是一个inode节点。 + +详细阐述一次文件访问的过程: + + 首先用户ls查看目录。由于一个目录也是一个文件,所以相当于是看目录文件下有哪些东西。 + + 实际上目录文件是一个特殊的inode节点,它不需要存储实际数据,而只是维护一个文件名到inode的映射表。 + + 于是我们ls到另一个目录。同理他也是一个inode。我们在这个inode下执行vi操作打开某个文件,于是linux通过inode中的映射表找到了我们请求访问的文件名对应的inode。 + + 然后寻道到对应的磁盘位置,读取内容到缓冲区,通过系统调用把内容读到内存中,最后进行访问。 + diff --git "a/md/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/md/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\255\246\344\271\240\346\200\273\347\273\223.md" new file mode 100644 index 0000000..cae10ae --- /dev/null +++ "b/md/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\345\255\246\344\271\240\346\200\273\347\273\223.md" @@ -0,0 +1,640 @@ +--- +title: 计算机网络学习总结 +date: 2018-07-09 22:32:57 +tags: + - 计算机网络 +categories: + - 后端 + - 技术总结 +--- +##这部分内容主要是基于一些关于计算机网络基础的学习总结,内容不全面,只讲述了其中的一小部分,后续会再补充,如有错误,还请见谅。 + +# 计算机网络常见概念 + +## 网卡和路由器 + +网卡是一个有mac地址的物理设备,通过mac地址与局域网内的交换机通信,交换机可以识别mac地址。 + +而单纯的中继器,集线器,双绞线等设备只识别物理层设备。 + +路由器则工作在3层ip层,必须要有ip才能工作,所以路由器每一个接口都对应一个ip,维护一个可以识别ip的路由表,进行ip数据报转发。 + +## 交换机 +交换机具有自学习能力,学习的是交换表的内容。交换表中存储着 MAC 地址到接口的映射。 + +## 以太网 +以太网是一种星型拓扑结构局域网。 + +早期使用集线器进行连接,它是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离。之后再将这个比特向其它所有接口。特别是,如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。 + +目前以太网使用交换机替代了集线器,它不会发生碰撞,能根据 MAC 地址进行存储转发。 + +## 虚拟局域网VLAN +正常情况下,局域网中的链路层广播在整个局域网可达,而vlan可以在物理局域网中划分虚拟局域网,使广播帧只有在vlan当中的主机才能收到。 + +虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息,例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 + +## DHCP协议(动态主机配置协议) + +首先DHCP是为了让主机获得一个ip地址,所以主机会发一个0.0.0.0为发送方,255.255.255.255为接收方的ip数据报,也就是广播数据报,并且广播数据包只在局域网中有效,然后链路层解析为数据帧,发送给局域网内的DHCP服务器。 + +## ARP协议 +arp负责把ip地址解析成局域网内的一个mac地址,只在局域网中有效。逆arp则把mac地址解析成ip地址。 + +网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/66192382-558b-4b05-a35d-ac4a2b1a9811.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/66192382-558b-4b05-a35d-ac4a2b1a9811.jpg) + +ARP 实现由 IP 地址得到 MAC 地址。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/b9d79a5a-e7af-499b-b989-f10483e71b8b.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/b9d79a5a-e7af-499b-b989-f10483e71b8b.jpg) + +每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。 + +> 如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。 + +## 网关和NAT +当需要和外部局域网访问时,需要经过网关服务器以便兼容不同协议栈。局域网内部使用内网ip,经过网关时要转成外网ip,网关会帮你完成改写操作,当收到数据报时,网关又会帮你把ip改为内网ip。这种修改ip隐藏内部网络的方式叫做NAT。 + +nat穿透的方式是主机和网关服务器协定一个ip地址作为主机服务的ip,所以主机可以通过这个ip和外网交流。 + +## DNS协议和http请求过程 +访问一个域名时,会发送dns报文请求(应用层)给本地的DNS服务器,解析出域名对应的ip,然后三次握手建立连接,(当然TCP数据报由本地局域网经过网关转给外网,再经过多次路由才到达目标主机),然后发送http请求获得响应报文 + +## ICMP +ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/e3124763-f75e-46c3-ba82-341e6c98d862.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/e3124763-f75e-46c3-ba82-341e6c98d862.jpg) + +ICMP 报文分为差错报告报文和询问报文。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/aa29cc88-7256-4399-8c7f-3cf4a6489559.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/aa29cc88-7256-4399-8c7f-3cf4a6489559.png) + + + 1. Ping + Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 + + Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报。 + + 2. Traceroute + Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径,事实上,traceroute也封装着无法交付的udp,和ping类似。。 + +源主机向目的主机发送一连串的 IP 数据报,每个数据包的ttl时间不同,所以可以跟踪每一跳路由的信息。 + +==但是因为数据报封装的是无法交付的UDP报文,因此目的主机要向源主机发送 ICMP终点不可达差错报告报文。之后源主机知道了到达目的主机所经过的路由器 IP地址以及到达每个路由器的往返时间。== + +## 虚拟专用网VPN和内网ip + +由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。 + +有三个专用地址块: + +* 10.0.0.0 ~ 10.255.255.255 +* 172.16.0.0 ~ 172.31.255.255 +* 192.168.0.0 ~ 192.168.255.255 + +这些ip也称为内网ip,用于局域网间的通信,只能通过网关抵达公网。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/1556770b-8c01-4681-af10-46f1df69202c.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/1556770b-8c01-4681-af10-46f1df69202c.jpg) + +使用隧道技术实现vpn。 + + 原理是;普通的内网ip无法被访问到,一般可以使用nat技术让网关作为中转人,而ip数据报也会改写成网关服务器的地址。 + + 如果想让数据报保留内网地址,并且实现跨公网访问,那么只能通过隧道技术,把内网数据报加密包装在公网ip数据报中,然后通过公网ip抵达对方的专用网络,进行拆包和发送。 + + 为什么vpn能翻墙呢,因为我们通过对vpn服务器的连接,可以将内网ip数据报装在里面,发送给vpn,vpn解析后再发送给真正的服务器。 + + 由于本地网关阻拦了某些网站的请求,所以我们要把这个请求加密封装,然后通过隧道把数据发给一个海外服务器,让他真正完成请求。 + +## 应用层 + +应用层的协议主要是http,ftp这类协议,http访问超文本html,而ftp访问文件系统。 + +### http + +通过浏览器可以方便地进行dns解析,建立tcp连接,发送http请求,得到http响应,这些工作都是浏览器完成的。 + +### http1.0 1.1和2.0 + +#### 1.0和1.1的主要变化 + + 1 http1.0经过多年发展,在1.1提出了改进。 + 首先是提出了长连接,http请求可以在一次tcp连接中不断发送。 + + 2 然后是http1.1支持只发送header而不发送body。原因是先用header判断能否成功,再发数据,节约带宽,事实上,post请求默认就是这样做的。 + + 3 http1.1的host字段。由于虚拟主机可以支持多个域名,所以一般将域名解析后得到host。 + +#### http1.0和http2.0的区别。 + + http2.0变化巨大。 + + 1 http支持多路复用,同一个连接可以并发处理多个请求,方法是把http数据包拆为多个帧,并发有序的发送,根据序号在另一端进行重组,而不需要一个个http请求顺序到达。 + + 2 http2.0支持服务端推送,就是服务端在http请求到达后,除了返回数据之外,还推送了额外的内容给客户端。 + + 3HTTP2.0压缩了请求头,同时基本单位是二进制帧流,这样的数据占用空间更少。 + + 4http2.0只适用于https场景,因为其在http和tcp中间加了一层ssl层。 + +### get和post + + get和post本质都是http请求,只不过对他们的作用做了界定和适配,并且让他们适应各自的场景。 + + 1本质区别是get只是一次http请求,post先发请求体再发请求体,实际上是两次请求 + + 2表面区别: + + get可以cache而post不能,因为浏览器是这么安排的 + + 一般设计get是幂等的而post不是 + + get的参数放在url传递,而post放在请求体里,因为get没有请求体。 + 所以get请求不安全,并且有长度限制(url不能太长),而post几乎没有限制,请求体可以很大。 + +### + + +#### session和cookie + +并且浏览器还维护了cookie以便记录用于对网站的一些信息,下次请求时在http报文中带上这些数据,服务器接收以后根据cookie中的sessionid获取对应的session即可 + +#### token + +session一般维护在内存中,有时候也会持久化到数据库,但是如果session由单点维护可能出现宕机等情况,于是一般会采用分布式的方案。 + + + session存放的几种方案。 + 0 存在内存中。用sessionid标识用户。 + 这样的session十分依赖于cookie。如果浏览器禁用了cookie则session无用武之地。 + + 当然也可以把内容存在数据库里,缺点是数据库访问压力较大。 + + 1有做法会将session内容存在cookie中,但前提是经过了加密,然后下次服务器对其进行解密,但是这样浏览器需要维护太多内容了。 + + 2当用户登录或者执行某些操作,则使用用户的一部分字段信息进行加密算法得到一串字符串成为token,用于唯一标识用户,或者是某些操作,比如登录,支付,服务端生成该token返回给用户,用户提交请求时必须带上这个token,就可以确认用户信息以及操作是否合法了。 + + 这样我们不需要存session,只需要在想得到用户信息时解密token即可。 + + token还有一个好处就是可以在移动端和pc端兼容,因为移动端不支持cookie。 + + 3token和oauth。经常有第三方授权登录的例子,本质就是使用token。首先我们打开授权登录页,登陆后服务端返回token,我们提交第三方的请求时,带上这个token,第三方不知道他是啥意思,并且token过段时间就过期了。 + +#### cas单点登录 + +单点登录是为了多个平台之间公用一个授权系统,做法是,所有登录都要指向统一登录服务,登陆成功以后在认证中心建立session,并且得到ticket,然后重定向页面,此时页面也会向认证中心确认ticket是否合法,然后就可以访问其他系统的页面了。 + +从而访问其他系统时,由于已经有了认证中心的cookie,所以直接带上ticket访问即可。 + +每次访问新系统时需要在认证中心注册session,然后单点退出时再把这些session退出,才能实现用户登出。 + +## web安全和https + +### 密码加密 + +MD5等加密方法可以用来对密码进行加密。一般还会加盐 + +### xss跨站脚本攻击 + +利用有输入功能网站的输入框来注入JavaScript脚本代码,用户访问该页面时会自动执行某些脚本代码,导致cookie等个人信息泄露,可能会被转发到其他网站。 + +解决办法是对输入进行检验,利用一个些工具类就可以做到。 + +### 跨站点请求伪造csrf +首先用户访问了一个网站并登陆,会把cookie保留在浏览器, +然后某些网站用一些隐性链接诱导用户点击,点击时发送请求会携带浏览器中的cookie,比如支付宝的账号密码,通过该cookie再去伪造一个支付宝支付请求,达到伪造请求的目的。 + +解决这个问题的办法就是禁止js请求跨域名。但是他为ajax提供了特殊定制。 + +### SQL 注入攻击 +1. 概念 +服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。 + +3. 防范手段 +(一)使用参数化查询 + +以下以 Java 中的 PreparedStatement 为例,它是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。 + +(二)单引号转换 + +将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。 + +### 拒绝服务攻击 +拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 + +分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 + +DDoS攻击通过大量合法的请求占用大量网络资源,以达到瘫痪网络的目的。 + +这种攻击方式可分为以下几种: + + 通过使网络过载来干扰甚至阻断正常的网络通讯; + 通过向服务器提交大量请求,使服务器超负荷; + 阻断某一用户访问服务器; + 阻断某服务与特定系统或个人的通讯。 + +攻击现象 + + 被攻击主机上有大量等待的TCP连接; + 网络中充斥着大量的无用的数据包; + 源地址为假 制造高流量无用数据,造成网络拥塞,使受害主机无法正常和外界通讯; + 利用受害主机提供的传输协议上的缺陷反复高速的发出特定的服务请求,使主机无法处理所有正常请求; + 严重时会造成系统死机。 + +总体来说,对DoS和DDoS的防范主要从下面几个方面考虑: + + 尽可能对系统加载最新补丁,并采取有效的合规性配置,降低漏洞利用风险; + + 采取合适的安全域划分,配置防火墙、入侵检测和防范系统,减缓攻击。 + + 采用分布式组网、负载均衡、提升系统容量等可靠性措施,增强总体服务能力。 +### https + +https博大精深,首先先来看看他的基础知识 + + 1对称加密和非对称加密 + + 对称加密两方使用同一把密钥加密和解密,传输密钥时如果丢失就会被破解。 + + 2非对称加密两方各有一把私钥,而公钥公开,A用私钥加密,把公钥和数据传给B,B用公钥解密。同理,B用私钥对数据进行加密,返回给A,A也用公钥进行解密。 + + 3非对称加密只要私钥不丢就很安全,但是效率比较低,所以一般使用非对称加密传输对称加密的密钥,使用对称加密完成数据传输。 + + 4数字签名,为了避免数据在传输过程中被替换,比如黑客修改了你的报文内容,但是你并不知道,所以我们让发送端做一个数字签名,把数据的摘要消息进行一个加密,比如MD5,得到一个签名,和数据一起发送。然后接收端把数据摘要进行md5加密,如果和签名一样,则说明数据确实是真的。 + + 5数字证书,对称加密中,双方使用公钥进行解密。虽然数字签名可以保证数据不被替换,但是数据是由公钥加密的,如果公钥也被替换,则仍然可以伪造数据,因为用户不知道对方提供的公钥其实是假的。 + + 所以为了保证发送方的公钥是真的,CA证书机构会负责颁发一个证书,里面的公钥保证是真的,用户请求服务器时,服务器将证书发给用户,这个证书是经由系统内置证书的备案的。 + + + + 6 https过程 + + 用户发送请求,服务器返回一个数字证书。 + + 用户在浏览器端生成一个随机数,使用证书中的公钥加密,发送给服务端。 + + 服务端使用公钥解密该密文,得到随机数。 + + 往后两者使用该随机数作为公钥进行对称加密。 + + + 番外:关于公钥加密私钥解密与私钥加密公钥解密说明 + 第一种是签名,使用私钥加密,公钥解密,用于让所有公钥所有者验证私钥所有者的身份并且用来防止私钥所有者发布的内容被篡改.但是不用来保证内容不被他人获得. + + 第二种是加密,用公钥加密,私钥解密,用于向公钥所有者发布信息,这个信息可能被他人篡改,但是无法被他人获得.搜索 + + +## 传输层 + + UDP 和 TCP 的特点 + 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。 + + 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。 + +TCP是传输层最重要的协议。 +> +> 由于网络层只提供最大交付的服务,尽可能地完成路由转发,以及把链路层报文传送给任意一台主机。他做的工作很专注,所以不会提供其他的可靠性保证。 +> +> 但是真实网络环境下随时会发生丢包,乱序,数据内容出错等情况,这些情况必须得到处理,于是我们使用传输层tcp来解决这些问题。 + +### UDP报文 + +伪首部的意义:伪首部并非TCP&UDP数据报中实际的有效成分。伪首部是一个虚拟的数据结构,其中的信息是从数据报所在IP分组头的分组头中提取的,既不向下传送也不向上递交,而仅仅是为计算校验和。 + +![image](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg) + +首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。 + +### TCP 首部格式 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/55dc4e84-573d-4c13-a765-52ed1dd251f9.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/55dc4e84-573d-4c13-a765-52ed1dd251f9.png) + +* **序号** :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。 + +* **确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。 + +* **数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。 + +* **确认 ACK** :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。 + +* **同步 SYN** :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。 + +* **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。 + +* **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。 +* + +### 三次握手和四次挥手 + 为了保证tcp的可靠传输,需要建立起一条通路,也就是所谓连接。这条通路必须保证有效并且能正确结束。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/e92d0ebc-7d46-413b-aec1-34a39602f787.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/e92d0ebc-7d46-413b-aec1-34a39602f787.png) + + + + 三次握手 + + 1 首先客户端发送连接请求syn,携带随机数x。 + 2 服务端返回请求ack,x + 1,说明服务端对x进行了回复。 + 3 客户端返回请求ack,y,说明接受到了信息并且开始传输数据,起始数据为y。 + + 客户端状态时syn_send和establish + 服务端则是从listen到syn_rcvd,再到establish + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg) + + 四次挥手 + + 1 首先客户端请求断开连接,发送fin请求,服务端返回fin的ack,继续处理断开前需要处理完的数据。 + + 2 过了一会,服务端处理完数据发送给客户端ack,表明已经关闭,客户端最后再发一个ack给服务端,如果服务端已关闭则无反应,客户端经过两个ttl后挥手完毕,确认服务端断开。这两个ttl成为time wait状态,用于确定服务端真的关闭。 + + 3 客户端发完fin后的状态从establish变为fin1——wait,服务端发完ack后的状态从establish变为closewait。 + + 4 客户端收到第一个ack后进入fin_2wait状态,服务端过了一会发送last——ack给客户端,说明关闭好了,客户端收到ack后进入timewait,然后发送ack。双方都closed。 + +### 半连接syn和洪泛法攻击 + +黑客开启大量的syn请求而不发送ack,服务端开启半连接等待ack,直到资源耗尽,所以必须检测来访ip +### 为什么要三次握手 + +三次握手的原因 + +第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。 + +也就是说,如果只有两次握手,服务端返回ack后直接通信,那么如果客户端因为网络问题没有收到ack,可能会再次请求连接,但时服务端不知道这其实是同一个请求,于是又打开了一个连接,相当于维护了很多的无用连接。 +### time wait的作用 + +1 需要服务端可靠地终止连接,如果处于time_wait客户端发给服务端的ack报文丢失,则服务端会再发一次fin,此时客户端不应该关闭。 + +2 保证迟来的tcp报文有时间被丢弃,因为2msl里超时抵达的报文都会被丢弃。 + +## 可靠传输协议 + +TCP协议有三个重要属性。 + + 可靠传输,主要通过有序接收,确认后发送,以及超时重传来实现,并且使用分片来提高发送效率,通过检验和避免错误。 + + 流量控制,主要通过窗口限制接收和发送速率。 + + 拥塞控制,主要通过不同拥塞状态的算法来处理拥塞,一开始发的比较慢,然后指数增加,当丢包时再降低速度,重新开始第一阶段,避免拥塞。 + +总结以下就是几个特点: + +TCP 可靠传输 + + TCP 使用超时重传来实现可靠传输: + + 1 如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 + + + 2 滑动窗口可以连续发送多个数据再统一进行确认。 + + 因为发送端希望在收到确认前,继续发送其它报文段。比如说在收到0号报文的确认前还发出了1-3号的报文,这样提高了信道的利用率。 + + 3 滑动窗口只重传丢失的数据报 + + 但可以想想,0-4发出去后可能要重传,所以需要一个缓冲区维护这些报文,所以就有了窗口。 + + 4每当完成一个确认窗口往前滑动一格,可以传新的一个数据,因此可以顺序发送顺序确认 + +TCP 流量控制 + + 流量控制是为了控制发送方发送速率,保证接收方来得及接收。 + + 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 + +TCP 拥塞控制 + + 如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接受,而拥塞控制是为了降低整个网络的拥塞程度。 + + TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 + + 一般刚开始时慢开始,然后拥塞避免,出现个别丢包时(连续三个包序号不对), + + 则执行快重传,然后进入快恢复阶段,接着继续拥塞避免。如果发生多次超时也就是拥塞时,直接进入慢开始。 +> +>这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。 + + ==发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口==。 + +滑动窗口协议综合实现了上述这一些内容: + +为什么要使用滑动窗口,因为滑动窗口可以实现可靠传输,流量控制和拥塞控制(拥塞控制用的是拥塞窗口变量) + +### tcp的粘包拆包 + +tcp报文是流式的数据,没有标识数据结束,只有序号等字段,tcp协议自动完成数据报的切分。由于tcp使用缓冲区发送,又没有标识结束,当缓冲区的数据没清空又有新数据进来,就会发生粘包,如果数据太大存装不下,就会被拆包。 + +## 网络层 + +## IP 数据报格式 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/85c05fb1-5546-4c50-9221-21f231cdc8c5.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/85c05fb1-5546-4c50-9221-21f231cdc8c5.jpg) + +* **版本** : 有 4(IPv4)和 6(IPv6)两个值; + +* **首部长度** : 占 4 位,因此最大值为 15。 + +* **总长度** : 包括首部长度和数据部分长度。 + +* **生存时间** :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。 + +==* **协议** :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。== + +* **首部检验和** :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。 + + +* **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。 + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/23ba890e-e11c-45e2-a20c-64d217f83430.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/23ba890e-e11c-45e2-a20c-64d217f83430.png) + +总结: + + ip层只保证尽最大努力交付,他所承载的一切都是对路由,转发,已经网络传输最友好的设计。 + + 路由器负责记录路由表和转发ip数据报,路由表记录着ip地址和下一跳路由的端口的对应关系。 + + 由于路由聚合的缘故,一般用170.177.233.0/24就可以标识好几个网络了。 + + 以前会使用A,B,C类地址,和子网,现在直接使用地址聚合,前24位是网络号,后面8位是主机号。 + + ## 某个聚合路由地址划分网络给n台机器,是否符合要求。。 + + 要看这个网络中的主机号能否达到n个。 + +### IP 地址编址方式 + +IP 地址的编址方式经历了三个历史阶段: + +* 分类 +* 子网划分 +* 无分类 + +### [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C.md#1-%E5%88%86%E7%B1%BB)1\. 分类 + +由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。 + +IP 地址 ::= {< 网络号 >, < 主机号 >} + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/cbf50eb8-22b4-4528-a2e7-d187143d57f7.png)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/cbf50eb8-22b4-4528-a2e7-d187143d57f7.png) + +2. 子网划分 + + + 通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。注意,外部网络看不到子网的存在。 + + IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >} + + 要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。 + +3. 无分类 + + + 无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。 + + IP 地址 ::= {< 网络前缀号 >, < 主机号 >} + + CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。 + + CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。 + +一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。 + +把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 构成超网 。 + +在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。 + +总结 + + 使用分类法的ip必须标识是哪一类地址,比较麻烦,而且一旦设置为某类地址它就只能使用那一部分地址空间了。 + + 使用子网掩码可以避免使用分类并且更灵活地决定网络号和主机号的划分。但是需要配置子网掩码,比较复杂。 + + CIDR 138.1.2.11/24 + 使用CIDR避免了子网划分,直接使用后n位作为网络号,简化了子网的配置(实际上用n代替了子网掩码)。并且在路由器中可以使用地址聚合,一个ip可以聚合多个网络号。 + +### ip分片详谈 + +在TCP/IP分层中,数据链路层用MTU(Maximum Transmission Unit,最大传输单元)来限制所能传输的数据包大小,MTU是指一次传送的数据最大长度,不包括数据链路层数据帧的帧头,如以太网的MTU为1500字节,实际上数据帧的最大长度为1512字节,其中以太网数据帧的帧头为12字节。 + +当发送的IP数据报的大小超过了MTU时,IP层就需要对数据进行分片,否则数据将无法发送成功。 + +IP分片的实现 + +> IP分片发生在IP层,不仅源端主机会进行分片,中间的路由器也有可能分片,因为不同的网络的MTU是不一样的,如果传输路径上的某个网络的MTU比源端网络的MTU要小,路由器就可能对IP数据报再次进行分片。而分片数据的重组只会发生在目的端的IP层。 + +==避免IP分片== +> 在网络编程中,我们要避免出现IP分片,那么为什么要避免呢?原因是IP层是没有超时重传机制的,如果IP层对一个数据包进行了分片,只要有一个分片丢失了,只能依赖于传输层进行重传,结果是所有的分片都要重传一遍,这个代价有点大。由此可见,IP分片会大大降低传输层传送数据的成功率,所以我们要避免IP分片。 +> + + 对于UDP包,我们需要在应用层去限制每个包的大小,一般不要超过1472字节,即以太网MTU(1500)—UDP首部(8)—IP首部(20)。 + + 对于TCP数据,应用层就不需要考虑这个问题了,因为传输层已经帮我们做了。 + +在建立连接的三次握手的过程中,连接双方会相互通告MSS(Maximum Segment =Size,最大报文段长度),MSS一般是MTU—IP首部(20)—TCP首部(20),每次发送的TCP数据都不会超过双方MSS的最小值,所以就保证了IP数据报不会超过MTU,避免了IP分片。 + +3. 外部网关协议 BGP + + + BGP(Border Gateway Protocol,边界网关协议) + + AS 之间的路由选择很困难,主要是因为互联网规模很大。并且各个 AS 内部使用不同的路由选择协议,就无法准确定义路径的度量。并且 AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。 + + BGP 只能寻找一条比较好的路由,而不是最佳路由。 + + 每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。 + +### 路由选择协议和算法 +> +> 路由选择协议 +> 路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。 +> +> 互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。 +> +> 可以把路由选择协议划分为两大类: +> +> 自治系统内部的路由选择:RIP 和 OSPF +> 自治系统间的路由选择:BGP + + +总结: + + 1. 内部网关协议 RIP + RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1,跳数最多为 15,超过 15 表示不可达。 + + RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。 + + 2. 内部网关协议 OSPF + 开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。 + + 开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。 + +OSPF 具有以下特点: + + 计算出最短路径,然后向本自治系统中的所有路由器发送信息,这种方法是洪泛法。 + + 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。 + + 变化时,路由器才会发送信息。 + + 所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。 + +总结: + + AS是一个自治域,一般是指相似度很大公用一个协议的路由器族,比如同一个运营商的网络。 + + 因特网中AS之间的路由选择协议是BGP。 + + AS内的路由选择协议有RIP和OSPF。 + + RIP两两交换,最后大家都同步。 + + OSPF找到最短路径。告诉大家。 + +## 链路层 +链路层最主要是指局域网内的网络交互了,使用mac地址通过交换机进行通信,其中用得最多的局域网协议就是以太网。 + +链路层使用MTU表示最大传输帧长度,报文长度不能超过MTU,否则会进行分片,比如比较大的IP数据报就会被分片,为了避免被分片。一般要控制IP报文长度。 + +广播: + +要理解什么是广播风暴,就必须先理解网络通信技术。 网络上的一个节点,它发送一个数据帧或包,被传输到由广播域定义的本地网段上的每个节点就是广播。 + +> 网络广播分为第2层广播和第3层广播。第2层广播也称硬件广播,用于在局域网内向所有的结点发送数据,通常不会穿过局域网的边界(路由器),除非它变成一个单播。广播将是一个二进制的全1或者十六进制全F的地址。而第3层广播用于在这个网络内向所有的结点发送数据。 + +帧的传输方式,即单播帧(Unicast Frame)、多播帧(Multicast Frame)和广播帧(Broadcast Frame)。 + + 1、单播帧 + 单播帧也称“点对点”通信。此时帧的接收和传递只在两个节点之间进行,帧的目的MAC地址就是对方的MAC地址,网络设备(指交换机和路由器)根据帧中的目的MAC地址,将帧转发出去。 + + 2、多播帧 + 多播帧可以理解为一个人向多个人(但不是在场的所有人)说话,这样能够提高通话的效率。多播占网络中的比重并不多,主要应用于网络设备内部通信、网上视频会议、网上视频点播等。 + + 3、广播帧 + 广播帧可以理解为一个人对在场的所有人说话,这样做的好处是通话效率高,信息一下子就可以传递到全体。在广播帧中,帧头中的目的MAC地址是“FF.FF.FF.FF.FF.FF”,代表网络上所有主机网卡的MAC地址。 + + 广播帧在网络中是必不可少的,如客户机通过DHCP自动获得IP地址的过程就是通过广播帧来实现的。而且,由于设备之间也需要相互通信,因此在网络中即使没有用户人为地发送广播帧,网络上也会出现一定数量的广播帧。 + + 同单播和多播相比,广播几乎占用了子网内网络的所有带宽。网络中不能长时间出现大量的广播帧,否则就会出现所谓的“广播风暴”(每秒的广播帧数在1000以上)。拿开会打一个比方,在会场上只能有一个人发言,如果所有人都同时发言的话,会场上就会乱成一锅粥。广播风暴就是网络长时间被大量的广播数据包所占用,使正常的点对点通信无法正常进行,其外在表现为网络速度奇慢无比。出现广播风暴的原因有很多,一块故障网卡就可能长时间地在网络上发送广播包而导致广播风暴。 + + 使用路由器或三层交换机能够实现在不同子网间隔离广播风暴的作用。当路由器或三层交换机收到广播帧时并不处理它,使它无法再传递到其他子网中,从而达到隔离广播风暴的目的。因此在由几百台甚至上千台电脑构成的大中型局域网中,为了隔离广播风暴,都要进行子网划分。 + 使用vlan完全可以隔离广播风暴。 + +> 在交换以太网上运行TCP/IP环境下: +> 二层广播是在数据链路层的广播,它 的广播范围是二层交换机连接的所有端口;二层广播不能通过路由器。 +> +> 三层广播就是在网络层的广播,它的范围是同一IP子网内的设备,子网广播也不能通过路由器。 +> +> 第三层的数据必须通过第二层的封装再发送,所以三层广播必然通过二层广播来实现。 +> +> 设想在同一台二层交换机上连接2个ip子网的设备,所有的设备都可以接收到二层广播,但三层广播只对本子网设备有效,非本子网的设备也会接收到广播包,但会被丢弃。 + +广播风暴(broadcast storm) + +简单的讲是指当广播数据充斥网络无法处理,并占用大量网络带宽,导致正常业务不能运行,甚至彻底瘫痪,这就发生了“广播风暴” + +。一个数据帧或包被传输到本地网段 (由广播域定义)上的每个节点就是广播;由于网络拓扑的设计和连接问题,或其他原因导致广播在网段内大量复制,传播数据帧,导致网络性能下降,甚至网络瘫痪,这就是广播风暴。 + +要避免广播风暴,可以采用恰当划分VLAN、缩小广播域、隔离广播风暴,还可在千兆以太网口上启用广播风暴控制,最大限度地避免网络再次陷入瘫痪。 \ No newline at end of file From 15b71a95047d41a4b068f624442a415ac0434868 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Mon, 9 Jul 2018 22:52:40 +0800 Subject: [PATCH 19/27] Create README.md --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7b1daf --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | +| :------: | :---------: | :-------: | :---------: | :---: | :---------:| :---------: | :---------: | :---------:| +| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| + +## 算法 :pencil2: + +> [Mysql原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +## 操作系统 :computer: + +> [操作系统学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) + +> [Mysql原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +## 网络 :cloud: + +> [计算机网络学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) + +## 数据库 :floppy_disk: + +> [Mysql原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +> [Redis原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Redis%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +## Java :couple: + +> [Java核心技术总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.md) + +> [Java集合类总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E9%9B%86%E5%90%88%E7%B1%BB%E6%80%BB%E7%BB%93.md) + +> [Java并发技术总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E5%B9%B6%E5%8F%91%E6%80%BB%E7%BB%93.md) + +> [JVM原理学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/JVM%E6%80%BB%E7%BB%93.md) + +> [Java网络与NIO总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Java%E7%BD%91%E7%BB%9C%E4%B8%8ENIO%E6%80%BB%E7%BB%93.md) + +## JavaWeb :coffee: + +> [JavaWeb技术学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/JavaWeb%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93.md) + +> [Spring与SpringMVC源码解析](https://github.com/h2pl/Java-Tutorial/blob/master/md/Spring%E4%B8%8ESpringMVC%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E6%80%BB%E7%BB%93.md) + +## 分布式 :sweat_drops: + +> [分布式理论学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA%E6%80%BB%E7%BB%93.md) + +> [分布式技术学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) + +## 设计模式 :hammer: + +## Hadoop :speak_no_evil: + +> [Hadoop生态学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Hadoop%E7%94%9F%E6%80%81%E6%80%BB%E7%BB%93.md) + +
+ +## 后记 + +**关于仓库** +本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 + +**关于转载** +本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 + + + From a226ce9a12754fc621dbf0432b5416eb813d110c Mon Sep 17 00:00:00 2001 From: 724888 <362294931@qq.com> Date: Mon, 9 Jul 2018 23:12:32 +0800 Subject: [PATCH 20/27] update --- ...46\344\271\240\346\200\273\347\273\223.md" | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 "md/\350\256\276\350\256\241\346\250\241\345\274\217\345\255\246\344\271\240\346\200\273\347\273\223.md" diff --git "a/md/\350\256\276\350\256\241\346\250\241\345\274\217\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/md/\350\256\276\350\256\241\346\250\241\345\274\217\345\255\246\344\271\240\346\200\273\347\273\223.md" new file mode 100644 index 0000000..d1196eb --- /dev/null +++ "b/md/\350\256\276\350\256\241\346\250\241\345\274\217\345\255\246\344\271\240\346\200\273\347\273\223.md" @@ -0,0 +1,189 @@ +--- +title: 设计模式学习总结 +date: 2018-07-09 23:05:07 +tags: + - 设计模式 +categories: + - 后端 + - 技术总结 +--- +设计模式基础学习总结 +这篇总结主要是基于我之前设计模式基础系列文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 + +更多详细内容可以查看我的专栏文章:设计模式学习 +https://blog.csdn.net/a724888/article/category/6780980 + + +# 设计模式 + +# 创建型模式 + +创建型模式 +创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。 + +## 单例 + + 单例模式保证全局的单例类只有一个实例,这样的话使用的时候直接获取即可,比如数据库的一个连接,Spring里的bean,都可以是单例的。 + + 单例模式一般有5种写法。 + + 第一种是饿汉模式,先把单例进行实例化,获取的时候通过静态方法直接获取即可。缺点是类加载后就完成了类的实例化,浪费部分空间。 + + 第二种是饱汉模式,先把单例置为null,然后通过静态方法获取单例时再进行实例化,但是可能有多线程同时进行实例化,会出现并发问题。 + + 第三种是逐步改进的方法,一开始可以用synchronized关键字进行同步,但是开销太大,而后改成使用volatile修饰单例,然后通过一次检查判断单例是否已初始化,如果未初始化就使用synchronized代码块,再次检查单例防止在这期间被初始化,而后才真正进行初始化。 + + 第四种是使用静态内部类来实现,静态内部类只在被使用的时候才进行初始化,所以在内部类中进行单例的实例化,只有用到的时候才会运行实例化代码。然后外部类再通过静态方法返回静态内部类的单例即可。 + + 第五种是枚举类,枚举类的底层实现其实也是内部类。枚举类确保每个类对象在全局是唯一的。所以保证它是单例,这个方法是最简单的。 + +## 工厂模式 + + 简单工厂一般是用一个工厂创建多个类的实例。 + + 工厂模式一般是指一个工厂服务一个接口,为这个接口的实现类进行实例化 + + 抽象工厂模式是指一个工厂服务于一个产品族,一个产品族可能包含多个接口,接口又会包含多个实现类,通过一个工厂就可以把这些绑定在一起,非常方便。 + +## 原型模式 + + 一般通过一个实例进行克隆从而获得更多同一原型的实例。使用实例的clone方法即可完成。 + +## 建造者模式 + + 建造者模式中有一个概念叫做链式调用,链式调用为一个类的实例化提供便利,一般提供系列的方法进行实例化,实际上就是将set方法改造一下,将原本返回为空的set方法改为返回this实例,从而实现链式调用。 + + 建造者模式在此基础上加入了builder方法,提供给外部进行调用,同样使用链式调用来完成参数注入。 + +# 结构型模式 + + +结构型模式 +前面创建型模式介绍了创建对象的一些设计模式,这节介绍的结构型模式旨在通过改变代码结构来达到解耦的目的,使得我们的代码容易维护和扩展。 + +## 桥接模式 + +有点复杂。建议参考原文 + +## 适配器模式 + +适配器模式用于将两个不同的类进行适配。 + +适配器模式和代理模式的异同 + + 比较这两种模式,其实是比较对象适配器模式和代理模式,在代码结构上, + 它们很相似,都需要一个具体的实现类的实例。 + 但是它们的目的不一样,代理模式做的是增强原方法的活; + 适配器做的是适配的活,为的是提供“把鸡包装成鸭,然后当做鸭来使用”, + 而鸡和鸭它们之间原本没有继承关系。 + + 适配器模式可以分为类适配器,对象适配器等。 + + 类适配器通过继承父类就可以把自己适配成父类了。 + 而对象适配器则需要把对象传入另一个对象的构造方法中,以便进行包装。 + +## 享元模式 + +/ 享元模式的核心在于享元工厂类, +// 享元工厂类的作用在于提供一个用于存储享元对象的享元池, +// 用户需要对象时,首先从享元池中获取, +// 如果享元池中不存在,则创建一个新的享元对象返回给用户, +// 在享元池中保存该新增对象。 + +//享元模式 +// 英文是 Flyweight Pattern,不知道是谁最先翻译的这个词,感觉这翻译真的不好理解,我们试着强行关联起来吧。Flyweight 是轻量级的意思,享元分开来说就是 共享 元器件,也就是复用已经生成的对象,这种做法当然也就是轻量级的了。 +// +// 复用对象最简单的方式是,用一个 HashMap 来存放每次新生成的对象。每次需要一个对象的时候,先到 HashMap 中看看有没有,如果没有,再生成新的对象,然后将这个对象放入 HashMap 中。 +// +// 这种简单的代码我就不演示了。 + +## 代理模式 + +// 我们发现没有,代理模式说白了就是做 “方法包装” 或做 “方法增强”。 +// 在面向切面编程中,算了还是不要吹捧这个名词了,在 AOP 中, +// 其实就是动态代理的过程。比如 Spring 中, +// 我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理, +// 然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。 + +## 外观模式 + +外观模式一般封装具体的实现细节,为用户提供一个更加简单的接口。 + +通过一个方法调用就可以获取需要的内容。 + +## 组合模式 + +//组合模式用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。 + +//直接看一个例子吧,每个员工都有姓名、部门、薪水这些属性, +// 同时还有下属员工集合(虽然可能集合为空), +// 而下属员工和自己的结构是一样的, +// 也有姓名、部门这些属性, +// 同时也有他们的下属员工集合。 + + class Employee { + private String name; + private String dept; + private int salary; + private List subordinates; // 下属 + } + +## 装饰者模式 + +## 装饰者 +装饰者模式把每个增强类都继承最高级父类。然后需要功能增强时把类实例传入增强类即可,然后增强类在使用时就可以增强原有类的功能了。 + +和代理模式不同的是,装饰者模式每个装饰类都继承父类,并且可以进行多级封装。 + + +# 行为型模式 + +行为型模式 +行为型模式关注的是各个类之间的相互作用,将职责划分清楚,使得我们的代码更加地清晰。 + +## 策略模式 + +策略模式一般把一个策略作为一个类,并且在需要指定策略的时候传入实例,于是我们可以在需要使用算法的地方传入指定算法。 + +## 命令模式 + +命令模式一般分为命令发起者,命令以及命令接受者三个角色。 + +命令发起者在使用时需要注入命令实例。然后执行命令调用。 + +命令调用实际上会调用命令接收者的方法进行实际调用。 + +比如遥控器按钮相当于一条命令,点击按钮时命令运行,自动调用电视机提供的方法即可。 + +## 模板方法模式 + +模板方法一般指提供了一个方法模板,并且其中有部分实现类和部分抽象类,并且规定了执行顺序。 + +实现类是模板提供好的方法。而抽象类则需要用户自行实现。 + +模板方法规定了一个模板中方法的执行顺序,非常适合一些开发框架,于是模板方法也广泛运用在开源框架中。 + +## 状态模式 + +少见。 + +## 观察者模式和事件监听机制 + +观察者模式一般用于订阅者和消息发布者之间的数据订阅。 + +一般分为观察者和主题,观察者订阅主题,把实例注册到主题维护的观察者列表上。 + +而主题更新数据时自动把数据推给观察者或者通知观察者数据已经更新。 + +但是由于这样的方式消息推送耦合关系比较紧。并且很难在不打开数据的情况下知道数据类型是什么。 + +知道后来为了使数据格式更加灵活,使用了事件和事件监听器的模式,事件包装的事件类型和事件数据,从主题和观察者中解耦。 + +主题当事件发生时,触发该事件的所有监听器,把该事件通过监听器列表发给每个监听器,监听得到事件以后,首先根据自己支持处理的事件类型中找到对应的事件处理器,再用处理器处理对应事件。 + + + +## 责任链模式 + +责任链通常需要先建立一个单向链表,然后调用方只需要调用头部节点就可以了,后面会自动流转下去。比如流程审批就是一个很好的例子,只要终端用户提交申请,根据申请的内容信息,自动建立一条责任链,然后就可以开始流转了。 + From 96cca0ac09726f15a2c19732aa655fda3f738739 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Mon, 9 Jul 2018 23:16:24 +0800 Subject: [PATCH 21/27] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f7b1daf..4bb6502 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ ## 算法 :pencil2: -> [Mysql原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) +> [剑指offer算法总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%89%91%E6%8C%87offer.md) ## 操作系统 :computer: > [操作系统学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) -> [Mysql原理与实践总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Mysql%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) +> [Linux内核与基础命令学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/Linux%E5%86%85%E6%A0%B8%E4%B8%8E%E5%9F%BA%E7%A1%80%E5%91%BD%E4%BB%A4%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) ## 网络 :cloud: @@ -47,6 +47,7 @@ > [分布式技术学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%88%86%E5%B8%83%E5%BC%8F%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93.md) ## 设计模式 :hammer: +> [设计模式学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) ## Hadoop :speak_no_evil: From 13b1e9dbfba8979140d913642de437e40ab108ee Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 24 Jul 2018 18:14:08 +0800 Subject: [PATCH 22/27] Update README.md --- README.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4bb6502..ca8c799 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ +## 声明 + +**关于仓库** + +本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,其中大部分都是笔者根据自己的理解总结而来的。 + +其中有少数内容可能会包含瞎XX说,语句不通顺,内容不全面等各方面问题,还请见谅。 + +每个部分都会有笔者更加详细的原创文章可供参考,这些文章也被我发表在CSDN技术博客上,整理成博客专栏,欢迎查看━(*`∀´*)ノ亻! + +具体内容请见我的CSDN技术博客:https://blog.csdn.net/a724888 + +也可以来我个人技术小站逛逛:https://h2pl.github.io/ + +**关于转载** + +转载的时候请注明一下出处就行啦。 + +另外我这个仓库的格式模仿的是@CyC2018 大佬的仓库 + +并且其中一篇LeetCode刷题指南也是fork这位大佬而来的。我只是自己刷了一遍然后稍微加了一些解析,站在大佬肩膀上。 + + | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | | :------: | :---------: | :-------: | :---------: | :---: | :---------:| :---------: | :---------: | :---------:| | 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| @@ -55,13 +78,6 @@
-## 后记 - -**关于仓库** -本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,每个部分都会有笔者更加详细的原创文章可供参考,欢迎查看。 - -**关于转载** -本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 From 4eca6dfa30c4a192a1bc49a3c6910a7d0daceece Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Tue, 24 Jul 2018 18:23:21 +0800 Subject: [PATCH 23/27] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ca8c799..04868b6 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ **关于仓库** -本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,其中大部分都是笔者根据自己的理解总结而来的。 +本仓库是笔者在准备 2018 年秋招复习过程中的学习总结,内容以Java后端的知识总结为主,其中大部分都是笔者根据自己的理解加上个人博客总结而来的。 其中有少数内容可能会包含瞎XX说,语句不通顺,内容不全面等各方面问题,还请见谅。 -每个部分都会有笔者更加详细的原创文章可供参考,这些文章也被我发表在CSDN技术博客上,整理成博客专栏,欢迎查看━(*`∀´*)ノ亻! +每篇文章都会有笔者更加详细的一系列博客可供参考,这些文章也被我发表在CSDN技术博客上,整理成博客专栏,欢迎查看━(*`∀´*)ノ亻! 具体内容请见我的CSDN技术博客:https://blog.csdn.net/a724888 From e4814a187e8a790f6b8bc17437175677428ecf81 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Fri, 7 Sep 2018 12:31:08 +0800 Subject: [PATCH 24/27] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 04868b6..22a22f2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ 也可以来我个人技术小站逛逛:https://h2pl.github.io/ +更多校招干货和技术文章请关注我的公众号:程序员江湖 + **关于转载** 转载的时候请注明一下出处就行啦。 From 67ee8fb8c88e2026f43227ec1b70d3e08f3edc65 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Fri, 7 Sep 2018 12:31:45 +0800 Subject: [PATCH 25/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22a22f2..2f44827 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ 也可以来我个人技术小站逛逛:https://h2pl.github.io/ -更多校招干货和技术文章请关注我的公众号:程序员江湖 +**更多校招干货和技术文章请关注我的公众号:程序员江湖** **关于转载** From b2fb3498557b8395d5e18fb02bdf6aeb6dc61094 Mon Sep 17 00:00:00 2001 From: 724888 <362294931@qq.com> Date: Fri, 7 Sep 2018 12:33:55 +0800 Subject: [PATCH 26/27] update --- ...67\351\242\230\346\214\207\345\215\227.md" | 5093 +++++++++++++++++ 1 file changed, 5093 insertions(+) create mode 100644 "md/LeetCode\345\210\267\351\242\230\346\214\207\345\215\227.md" diff --git "a/md/LeetCode\345\210\267\351\242\230\346\214\207\345\215\227.md" "b/md/LeetCode\345\210\267\351\242\230\346\214\207\345\215\227.md" new file mode 100644 index 0000000..e9a3d2b --- /dev/null +++ "b/md/LeetCode\345\210\267\351\242\230\346\214\207\345\215\227.md" @@ -0,0 +1,5093 @@ +## 数组和矩阵 + +**把数组中的 0 移到末尾** + +[283\. Move Zeroes (Easy)](https://leetcode.com/problems/move-zeroes/description/) + + + +
For example, given nums = [0, 1, 0, 3, 12], after calling your function, nums should be [1, 3, 12, 0, 0].
+ + + + + +
public void moveZeroes(int[] nums) {
+    int idx = 0;
+    for (int num : nums) {
+        if (num != 0) {
+            nums[idx++] = num;
+        }
+    }
+    while (idx < nums.length) {
+        nums[idx++] = 0;
+    }
+}
+ + + +**改变矩阵维度** + +[566\. Reshape the Matrix (Easy)](https://leetcode.com/problems/reshape-the-matrix/description/) + + + +
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.
+ + + + + +
public int[][] matrixReshape(int[][] nums, int r, int c) {
+    int m = nums.length, n = nums[0].length;
+    if (m * n != r * c) {
+        return nums;
+    }
+    int[][] reshapedNums = new int[r][c];
+    int index = 0;
+    for (int i = 0; i < r; i++) {
+        for (int j = 0; j < c; j++) {
+            reshapedNums[i][j] = nums[index / n][index % n];
+            index++;
+        }
+    }
+    return reshapedNums;
+}
+ + + +**找出数组中最长的连续 1** + +[485\. Max Consecutive Ones (Easy)](https://leetcode.com/problems/max-consecutive-ones/description/) + + + +
public int findMaxConsecutiveOnes(int[] nums) {
+    int max = 0, cur = 0;
+    for (int x : nums) {
+        cur = x == 0 ? 0 : cur + 1;
+        max = Math.max(max, cur);
+    }
+    return max;
+}
+ + + +**一个数组元素在 [1, n] 之间,其中一个数被替换为另一个数,找出重复的数和丢失的数** + +[645\. Set Mismatch (Easy)](https://leetcode.com/problems/set-mismatch/description/) + + + +
Input: nums = [1,2,2,4]
+Output: [2,3]
+ + + + + +
Input: nums = [1,2,2,4]
+Output: [2,3]
+ + + +最直接的方法是先对数组进行排序,这种方法时间复杂度为 O(NlogN)。本题可以以 O(N) 的时间复杂度、O(1) 空间复杂度来求解。 + +主要思想是通过交换数组元素,使得数组上的元素在正确的位置上。遍历数组,如果第 i 位上的元素不是 i + 1,那么一直交换第 i 位和 nums[i] - 1 位置上的元素。 + + + +
public int[] findErrorNums(int[] nums) {
+    for (int i = 0; i < nums.length; i++) {
+        while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) {
+            swap(nums, i, nums[i] - 1);
+        }
+    }
+    for (int i = 0; i < nums.length; i++) {
+        if (nums[i] != i + 1) {
+            return new int[]{nums[i], i + 1};
+        }
+    }
+    return null;
+}
+
+private void swap(int[] nums, int i, int j) {
+    int tmp = nums[i];
+    nums[i] = nums[j];
+    nums[j] = tmp;
+}
+ + + +类似题目: + +* [448\. Find All Numbers Disappeared in an Array (Easy)](https://leetcode.com/problems/find-all-numbers-disappeared-in-an-array/description/),寻找所有丢失的元素 +* [442\. Find All Duplicates in an Array (Medium)](https://leetcode.com/problems/find-all-duplicates-in-an-array/description/),寻找所有重复的元素。 + +**找出数组中重复的数,数组值在 [1, n] 之间** + +[287\. Find the Duplicate Number (Medium)](https://leetcode.com/problems/find-the-duplicate-number/description/) + +要求不能修改数组,也不能使用额外的空间。 + +二分查找解法: + + + +
public int findDuplicate(int[] nums) {
+     int l = 1, h = nums.length - 1;
+     while (l <= h) {
+         int mid = l + (h - l) / 2;
+         int cnt = 0;
+         for (int i = 0; i < nums.length; i++) {
+             if (nums[i] <= mid) cnt++;
+         }
+         if (cnt > mid) h = mid - 1;
+         else l = mid + 1;
+     }
+     return l;
+}
+ + + +双指针解法,类似于有环链表中找出环的入口: + + + +
public int findDuplicate(int[] nums) {
+    int slow = nums[0], fast = nums[nums[0]];
+    while (slow != fast) {
+        slow = nums[slow];
+        fast = nums[nums[fast]];
+    }
+    fast = 0;
+    while (slow != fast) {
+        slow = nums[slow];
+        fast = nums[fast];
+    }
+    return slow;
+}
+ + + +**有序矩阵查找** + +[240\. Search a 2D Matrix II (Medium)](https://leetcode.com/problems/search-a-2d-matrix-ii/description/) + + + +
[
+   [ 1,  5,  9],
+   [10, 11, 13],
+   [12, 13, 15]
+]
+ + + + + +
public boolean searchMatrix(int[][] matrix, int target) {
+    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false;
+    int m = matrix.length, n = matrix[0].length;
+    int row = 0, col = n - 1;
+    while (row < m && col >= 0) {
+        if (target == matrix[row][col]) return true;
+        else if (target < matrix[row][col]) col--;
+        else row++;
+    }
+    return false;
+}
+ + + +**有序矩阵的 Kth Element** + +[378\. Kth Smallest Element in a Sorted Matrix ((Medium))](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/description/) + + + +
matrix = [
+  [ 1,  5,  9],
+  [10, 11, 13],
+  [12, 13, 15]
+],
+k = 8,
+
+return 13.
+ + + +解题参考:[Share my thoughts and Clean Java Code](https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/discuss/85173) + +二分查找解法: + + + +
public int kthSmallest(int[][] matrix, int k) {
+    int m = matrix.length, n = matrix[0].length;
+    int lo = matrix[0][0], hi = matrix[m - 1][n - 1];
+    while(lo <= hi) {
+        int mid = lo + (hi - lo) / 2;
+        int cnt = 0;
+        for(int i = 0; i < m; i++) {
+            for(int j = 0; j < n && matrix[i][j] <= mid; j++) {
+                cnt++;
+            }
+        }
+        if(cnt < k) lo = mid + 1;
+        else hi = mid - 1;
+    }
+    return lo;
+}
+ + + +堆解法: + + + +
public int kthSmallest(int[][] matrix, int k) {
+    int m = matrix.length, n = matrix[0].length;
+    PriorityQueue pq = new PriorityQueue();
+    for(int j = 0; j < n; j++) pq.offer(new Tuple(0, j, matrix[0][j]));
+    for(int i = 0; i < k - 1; i++) { // 小根堆,去掉 k - 1 个堆顶元素,此时堆顶元素就是第 k 的数
+        Tuple t = pq.poll();
+        if(t.x == m - 1) continue;
+        pq.offer(new Tuple(t.x + 1, t.y, matrix[t.x + 1][t.y]));
+    }
+    return pq.poll().val;
+}
+
+class Tuple implements Comparable {
+    int x, y, val;
+    public Tuple(int x, int y, int val) {
+        this.x = x; this.y = y; this.val = val;
+    }
+
+    @Override
+    public int compareTo(Tuple that) {
+        return this.val - that.val;
+    }
+}
+ + + +**数组相邻差值的个数** + +[667\. Beautiful Arrangement II (Medium)](https://leetcode.com/problems/beautiful-arrangement-ii/description/) + + + +
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. + + + +
public int[] constructArray(int n, int k) {
+    int[] ret = new int[n];
+    ret[0] = 1;
+    for (int i = 1, interval = k; i <= k; i++, interval--) {
+        ret[i] = i % 2 == 1 ? ret[i - 1] + interval : ret[i - 1] - interval;
+    }
+    for (int i = k + 1; i < n; i++) {
+        ret[i] = i + 1;
+    }
+    return ret;
+}
+ + + +**数组的度** + +[697\. Degree of an Array (Easy)](https://leetcode.com/problems/degree-of-an-array/description/) + + + +
Input: [1,2,2,3,1,4,2]
+Output: 6
+ + + +题目描述:数组的度定义为元素出现的最高频率,例如上面的数组度为 3。要求找到一个最小的子数组,这个子数组的度和原数组一样。 + + + +
public int findShortestSubArray(int[] nums) {
+    Map numsCnt = new HashMap<>();
+    Map numsLastIndex = new HashMap<>();
+    Map numsFirstIndex = new HashMap<>();
+    for (int i = 0; i < nums.length; i++) {
+        int num = nums[i];
+        numsCnt.put(num, numsCnt.getOrDefault(num, 0) + 1);
+        numsLastIndex.put(num, i);
+        if (!numsFirstIndex.containsKey(num)) {
+            numsFirstIndex.put(num, i);
+        }
+    }
+    int maxCnt = 0;
+    for (int num : nums) {
+        maxCnt = Math.max(maxCnt, numsCnt.get(num));
+    }
+    int ret = nums.length;
+    for (int i = 0; i < nums.length; i++) {
+        int num = nums[i];
+        int cnt = numsCnt.get(num);
+        if (cnt != maxCnt) continue;
+        ret = Math.min(ret, numsLastIndex.get(num) - numsFirstIndex.get(num) + 1);
+    }
+    return ret;
+}
+ + + +**对角元素相等的矩阵** + +[766\. Toeplitz Matrix (Easy)](https://leetcode.com/problems/toeplitz-matrix/description/) + + + +
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.
+ + + + + +
public boolean isToeplitzMatrix(int[][] matrix) {
+    for (int i = 0; i < matrix[0].length; i++) {
+        if (!check(matrix, matrix[0][i], 0, i)) {
+            return false;
+        }
+    }
+    for (int i = 0; i < matrix.length; i++) {
+        if (!check(matrix, matrix[i][0], i, 0)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+private boolean check(int[][] matrix, int expectValue, int row, int col) {
+    if (row >= matrix.length || col >= matrix[0].length) {
+        return true;
+    }
+    if (matrix[row][col] != expectValue) {
+        return false;
+    }
+    return check(matrix, expectValue, row + 1, col + 1);
+}
+ + + +**嵌套数组** + +[565\. Array Nesting (Medium)](https://leetcode.com/problems/array-nesting/description/) + + + +
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]。 + + + +
public int arrayNesting(int[] nums) {
+    int max = 0;
+    for (int i = 0; i < nums.length; i++) {
+        int cnt = 0;
+        for (int j = i; nums[j] != -1; ) {
+            cnt++;
+            int t = nums[j];
+            nums[j] = -1; // 标记该位置已经被访问
+            j = t;
+
+        }
+        max = Math.max(max, cnt);
+    }
+    return max;
+}
+ + + +**分隔数组** + +[769\. Max Chunks To Make Sorted (Medium)](https://leetcode.com/problems/max-chunks-to-make-sorted/description/) + + + +
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.
+ + + +题目描述:分隔数组,使得对每部分排序后数组就为有序。 + + + +
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;
+}
+ + + +## 字符串 + +**两个字符串包含的字符是否完全相同** + +[242\. Valid Anagram (Easy)](https://leetcode.com/problems/valid-anagram/description/) + + + +
s = "anagram", t = "nagaram", return true.
+s = "rat", t = "car", return false.
+ + + +字符串只包含小写字符,总共有 26 个小写字符。可以用 HashMap 来映射字符与出现次数。因为键的范围很小,因此可以使用长度为 26 的整型数组对字符串出现的字符进行统计,然后比较两个字符串出现的字符数量是否相同。 + + + +
public boolean isAnagram(String s, String t) {
+    int[] cnts = new int[26];
+    for (char c : s.toCharArray()) {
+        cnts[c - 'a']++;
+    }
+    for (char c : t.toCharArray()) {
+        cnts[c - 'a']--;
+    }
+    for (int cnt : cnts) {
+        if (cnt != 0) {
+            return false;
+        }
+    }
+    return true;
+}
+ + + +**计算一组字符集合可以组成的回文字符串的最大长度** + +[409\. Longest Palindrome (Easy)](https://leetcode.com/problems/longest-palindrome/description/) + + + +
Input : "abccccdd"
+Output : 7
+Explanation : One longest palindrome that can be built is "dccaccd", whose length is 7.
+ + + +使用长度为 256 的整型数组来统计每个字符出现的个数,每个字符有偶数个可以用来构成回文字符串。 + +因为回文字符串最中间的那个字符可以单独出现,所以如果有单独的字符就把它放到最中间。 + + + +
public int longestPalindrome(String s) {
+    int[] cnts = new int[256];
+    for (char c : s.toCharArray()) {
+        cnts[c]++;
+    }
+    int palindrome = 0;
+    for (int cnt : cnts) {
+        palindrome += (cnt / 2) * 2;
+    }
+    if (palindrome < s.length()) {
+        palindrome++;   // 这个条件下 s 中一定有单个未使用的字符存在,可以把这个字符放到回文的最中间
+    }
+    return palindrome;
+}
+ + + +**字符串同构** + +[205\. Isomorphic Strings (Easy)](https://leetcode.com/problems/isomorphic-strings/description/) + + + +
Given "egg", "add", return true.
+Given "foo", "bar", return false.
+Given "paper", "title", return true.
+ + + +记录一个字符上次出现的位置,如果两个字符串中的字符上次出现的位置一样,那么就属于同构。 + + + +
public boolean isIsomorphic(String s, String t) {
+    int[] preIndexOfS = new int[256];
+    int[] preIndexOfT = new int[256];
+    for (int i = 0; i < s.length(); i++) {
+        char sc = s.charAt(i), tc = t.charAt(i);
+        if (preIndexOfS[sc] != preIndexOfT[tc]) {
+            return false;
+        }
+        preIndexOfS[sc] = i + 1;
+        preIndexOfT[tc] = i + 1;
+    }
+    return true;
+}
+ + + +**回文子字符串** + +[647\. Palindromic Substrings (Medium)](https://leetcode.com/problems/palindromic-substrings/description/) + + + +
Input: "aaa"
+Output: 6
+Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
+ + + +从字符串的某一位开始,尝试着去扩展子字符串。 + + + +
private int cnt = 0;
+
+public int countSubstrings(String s) {
+    for (int i = 0; i < s.length(); i++) {
+        extendSubstrings(s, i, i);     // 奇数长度
+        extendSubstrings(s, i, i + 1); // 偶数长度
+    }
+    return cnt;
+}
+
+private void extendSubstrings(String s, int start, int end) {
+    while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)) {
+        start--;
+        end++;
+        cnt++;
+    }
+}
+ + + +**判断一个整数是否是回文数** + +[9\. Palindrome Number (Easy)](https://leetcode.com/problems/palindrome-number/description/) + +要求不能使用额外空间,也就不能将整数转换为字符串进行判断。 + +将整数分成左右两部分,右边那部分需要转置,然后判断这两部分是否相等。 + + + +
public boolean isPalindrome(int x) {
+    if (x == 0) {
+        return true;
+    }
+    if (x < 0 || x % 10 == 0) {
+        return false;
+    }
+    int right = 0;
+    while (x > right) {
+        right = right * 10 + x % 10;
+        x /= 10;
+    }
+    return x == right || x == right / 10;
+}
+ + + +**统计二进制字符串中连续 1 和连续 0 数量相同的子字符串个数** + +[696\. Count Binary Substrings (Easy)](https://leetcode.com/problems/count-binary-substrings/description/) + + + +
Input: "00110011"
+Output: 6
+Explanation: There are 6 substrings that have equal number of consecutive 1's and 0's: "0011", "01", "1100", "10", "0011", and "01".
+ + + + + +
public int countBinarySubstrings(String s) {
+    int preLen = 0, curLen = 1, count = 0;
+    for (int i = 1; i < s.length(); i++) {
+        if (s.charAt(i) == s.charAt(i - 1)) {
+            curLen++;
+        } else {
+            preLen = curLen;
+            curLen = 1;
+        }
+
+        if (preLen >= curLen) {
+            count++;
+        }
+    }
+    return count;
+}
+ + + +**字符串循环移位包含** + +[编程之美:3.1](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#) + + + +
s1 = AABCD, s2 = CDAA
+Return : true
+ + + +给定两个字符串 s1 和 s2,要求判定 s2 是否能够被 s1 做循环移位得到的字符串包含。 + +s1 进行循环移位的结果是 s1s1 的子字符串,因此只要判断 s2 是否是 s1s1 的子字符串即可。 + +**字符串循环移位** + +[编程之美:2.17](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#) + + + +
s = "abcd123" k = 3
+Return "123abcd"
+ + + +将字符串向右循环移动 k 位。 + +将 abcd123 中的 abcd 和 123 单独逆序,得到 dcba321,然后对整个字符串进行逆序,得到 123abcd。 + +**字符串中单词的翻转** + +[程序员代码面试指南](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#) + + + +
s = "I am a student"
+return "student a am I"
+ + + +将每个单词逆序,然后将整个字符串逆序。 + +## 栈和队列 + +## 哈希表 + +哈希表使用 O(N) 空间复杂度存储数据,从而能够以 O(1) 时间复杂度求解问题。 + +Java 中的 **HashSet** 用于存储一个集合,可以查找元素是否在集合中。 + +如果元素有穷,并且范围不大,那么可以用一个布尔数组来存储一个元素是否存在。例如对于只有小写字符的元素,就可以用一个长度为 26 的布尔数组来存储一个字符集合,使得空间复杂度降低为 O(1)。 + +Java 中的 **HashMap** 主要用于映射关系,从而把两个元素联系起来。 + +在对一个内容进行压缩或者其它转换时,利用 HashMap 可以把原始内容和转换后的内容联系起来。例如在一个简化 url 的系统中[Leetcdoe : 535\. Encode and Decode TinyURL (Medium)](https://leetcode.com/problems/encode-and-decode-tinyurl/description/),利用 HashMap 就可以存储精简后的 url 到原始 url 的映射,使得不仅可以显示简化的 url,也可以根据简化的 url 得到原始 url 从而定位到正确的资源。 + +HashMap 也可以用来对元素进行计数统计,此时键为元素,值为计数。和 HashSet 类似,如果元素有穷并且范围不大,可以用整型数组来进行统计。 + +**数组中的两个数和为给定值** + +[1\. Two Sum (Easy)](https://leetcode.com/problems/two-sum/description/) + +可以先对数组进行排序,然后使用双指针方法或者二分查找方法。这样做的时间复杂度为 O(NlogN),空间复杂度为 O(1)。 + +用 HashMap 存储数组元素和索引的映射,在访问到 nums[i] 时,判断 HashMap 中是否存在 target - nums[i],如果存在说明 target - nums[i] 所在的索引和 i 就是要找的两个数。该方法的时间复杂度为 O(N),空间复杂度为 O(N),使用空间来换取时间。 + + + +
public int[] twoSum(int[] nums, int target) {
+    HashMap indexForNum = new HashMap<>();
+    for (int i = 0; i < nums.length; i++) {
+        if (indexForNum.containsKey(target - nums[i])) {
+            return new int[]{indexForNum.get(target - nums[i]), i};
+        } else {
+            indexForNum.put(nums[i], i);
+        }
+    }
+    return null;
+}
+ + + +**判断数组是否含有重复元素** + +[217\. Contains Duplicate (Easy)](https://leetcode.com/problems/contains-duplicate/description/) + + + +
public boolean containsDuplicate(int[] nums) {
+    Set set = new HashSet<>();
+    for (int num : nums) {
+        set.add(num);
+    }
+    return set.size() < nums.length;
+}
+ + + +**最长和谐序列** + +[594\. Longest Harmonious Subsequence (Easy)](https://leetcode.com/problems/longest-harmonious-subsequence/description/) + + + +
Input: [1,3,2,2,5,2,3,7]
+Output: 5
+Explanation: The longest harmonious subsequence is [3,2,2,2,3].
+ + + +和谐序列中最大数和最小数只差正好为 1,应该注意的是序列的元素不一定是数组的连续元素。 + + + +
public int findLHS(int[] nums) {
+    Map countForNum = new HashMap<>();
+    for (int num : nums) {
+        countForNum.put(num, countForNum.getOrDefault(num, 0) + 1);
+    }
+    int longest = 0;
+    for (int num : countForNum.keySet()) {
+        if (countForNum.containsKey(num + 1)) {
+            longest = Math.max(longest, countForNum.get(num + 1) + countForNum.get(num));
+        }
+    }
+    return longest;
+}
+ + + +**最长连续序列** + +[128\. Longest Consecutive Sequence (Hard)](https://leetcode.com/problems/longest-consecutive-sequence/description/) + + + +
Given [100, 4, 200, 1, 3, 2],
+The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4.
+ + + +要求以 O(N) 的时间复杂度求解。 + + + +
public int longestConsecutive(int[] nums) {
+    Map countForNum = new HashMap<>();
+    for (int num : nums) {
+        countForNum.put(num, 1);
+    }
+    for (int num : nums) {
+        forward(countForNum, num);
+    }
+    return maxCount(countForNum);
+}
+
+private int forward(Map countForNum, int num) {
+    if (!countForNum.containsKey(num)) {
+        return 0;
+    }
+    int cnt = countForNum.get(num);
+    if (cnt > 1) {
+        return cnt;
+    }
+    cnt = forward(countForNum, num + 1) + 1;
+    countForNum.put(num, cnt);
+    return cnt;
+}
+
+private int maxCount(Map countForNum) {
+    int max = 0;
+    for (int num : countForNum.keySet()) {
+        max = Math.max(max, countForNum.get(num));
+    }
+    return max;
+}
+ + + +## 贪心算法 + +一般什么时候需要用到贪心,其实就是在题目推导比较难解,但是直观思维却比较简单。比如经典的排课问题,就是使用贪心,先进行排序,再进行选择,贪心算法也时常用来求近似解。 + +所以一般解法可以考虑为,先排序,再根据条件求结果。证明的过程是非常难的,所以我们一般不会讨论证明 + +贪心思想 +贪心思想保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 + + +>455.分发饼干: + 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 +> +> 注意: +> +> 你可以假设胃口值为正。 +> 一个小朋友最多只能拥有一块饼干。 +> +> 示例 1: +> +> 输入: [1,2,3], [1,1] +> +> 输出: 1 +> +> 解释: +> 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 +> 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 +> 所以你应该输出1。 +> 示例 2: +> +> 输入: [1,2], [1,2,3] +> +> 输出: 2 +> +> 解释: +> 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 +> 你拥有的饼干数量和尺寸都足以让所有孩子满足。 +> 所以你应该输出2. + +由于题目想让尽量多的孩子满足胃口值,所以应该先用量小的饼干满足胃口小的。这样得到的结果是最优的。 + + public int findContentChildren(int[] g, int[] s) { + int count = 0; + Arrays.sort(g); + Arrays.sort(s); + int i = 0,j = 0; + while (i < g.length && j < s.length) { + if (g[i] <= s[j]) { + i ++; + j ++; + count ++; + }else { + j ++; + } + } + return count; + } + +> 435. 无重叠区间:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 +> +> 注意: +> +> 可以认为区间的终点总是大于它的起点。 +> 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 +> 示例 1: +> +> 输入: [ [1,2], [2,3], [3,4], [1,3] ] +> +> 输出: 1 +> +> 解释: 移除 [1,3] 后,剩下的区间没有重叠。 +> 示例 2: +> +> 输入: [ [1,2], [1,2], [1,2] ] +> +> 输出: 2 +> +> 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 +> 示例 3: +> +> 输入: [ [1,2], [2,3] ] +> +> 输出: 0 +> +> 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 + +本题类似于课程排课,我们应该让课程结束时间最早的先排课,这样可以让排课最大化,并且需要让课程结束的时间小于下一节课程开始的时间。并且[1,2][2,3]不算课程重叠。 + +所以我们的想法是,根据数组的第二位进行排序,也就是按照课程的结束时间排序,然后依次寻找不重叠的区间,然后用总个数减去不重叠的区间,剩下的就是要删除的区间。 + +不过,要注意的是,不重叠的区间并不一定是连续的,如果1和2区间重叠了,还要判断1和3是否重叠,直到找到不重叠的区间,再从3区间开始找下一个区间。 + + /** + * Definition for an interval. + * public class Interval { + * int start; + * int end; + * Interval() { start = 0; end = 0; } + * Interval(int s, int e) { start = s; end = e; } + * } + */ + import java.util.*; + class Solution { + public int eraseOverlapIntervals(Interval[] intervals) { + int len = intervals.length; + if (len <= 1)return 0; + Arrays.sort(intervals, (a,b) -> a.end - b.end); + int count = 1; + int end = intervals[0].end; + for (int i = 1;i < intervals.length;i ++) { + if (intervals[i].start < end) { + continue; + } + count ++; + end = intervals[i].end; + } + return len - count; + } + } + +本题要注意的点有几个: + +1 需要用一个值标识起始值的end,然后再往后找一个符合条件的end。由于是顺序查找,所以只需要一个变量i。并且使用end标识起始元素。 + +2 默认的count应该为1,因为自己本身就是不重叠的。所以找到其他不重叠的区域,使用n-count才对。 + +> 452. 用最少数量的箭引爆气球 +> 在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。 +> +> 一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。 +> +> Example: +> +> 输入: +> [[10,16], [2,8], [1,6], [7,12]] +> +> 输出: +> 2 +> +> 解释: +> 对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。 + + import java.util.*; + class Solution { + public int findMinArrowShots(int[][] points) { + if (points.length <= 1){ + return points.length; + } + Arrays.sort(points, (a, b) -> a[1] - b[1]); + int end = points[0][1]; + int cnt = 1; + for (int i = 1;i < points.length;i ++) { + if (points[i][0] <= end) { + continue; + } + end = points[i][1]; + cnt ++; + } + return cnt; + } + } + +和上一题类似,要注意的地方是: +1.本题是求不重叠区域的个数,而上一题是求要删除重叠区域的个数。 +2.本题中[1,2][2,3]也算是重叠区域 + +> 406. 根据身高重建队列 +> +> 这题思路不直观,跳过 + +763. 划分字母区间 +> 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一个字母只会出现在其中的一个片段。返回一个表示每个字符串片段的长度的列表。 +> +> 示例 1: +> +> 输入: S = "ababcbacadefegdehijhklij" +> 输出: [9,7,8] +> 解释: +> 划分结果为 "ababcbaca", "defegde", "hijhklij"。 +> 每个字母最多出现在一个片段中。 +> 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。 +> 注意: +> +> S的长度在[1, 500]之间。 +> S只包含小写字母'a'到'z'。 + +本题的思路是,先把每个字母的最后一位找出来,存在数组里,然后从头开始找到这样一个字符串,对于字符串中的每个字母,它出现的最后一个字母已经包含在整个字符串内。 + + import java.util.*; + class Solution { + public List partitionLabels(String S) { + int []arr = new int[26]; + List list = new ArrayList<>(); + for (int i = 0;i < S.length();i ++) { + arr[S.charAt(i) - 'a'] = i; + } + int start = 0; + int end = arr[S.charAt(0) - 'a']; + for (int i = 0;i < S.length();i ++) { + end = Math.max(arr[S.charAt(i) - 'a'], end); + if (i < end) { + continue; + }else { + list.add(end - start + 1); + start = i + 1; + } + } + return list; + } + } +本题要点: + +1.要使用一个数组存储每个字母的最后出现位置。 +通过x - 'a'的方式得到其下标。 + +2.由于需要每一次截取的长度,所以用start和end来表示,可以用于保存长度。 + + +> 605. 种花问题 +> 假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。 +> +> 给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。 +> +> 示例 1: +> +> 输入: flowerbed = [1,0,0,0,1], n = 1 +> 输出: True +> 示例 2: +> +> 输入: flowerbed = [1,0,0,0,1], n = 2 +> 输出: False +> 注意: +> +> 数组内已种好的花不会违反种植规则。 +> 输入的数组长度范围为 [1, 20000]。 +> n 是非负整数,且不会超过输入数组的大小。 + +思路:算出花坛中一共有几个空位,看看是否大于等于花的数量 + + class Solution { + public boolean canPlaceFlowers(int[] flowerbed, int n) { + int cnt = 0; + if (flowerbed.length == 1 && flowerbed[0] == 0) { + return n <= 1; + } + if (flowerbed.length >= 2) { + if (flowerbed[0] == 0 && flowerbed[1] == 0) { + flowerbed[0] = 1; + cnt ++; + } + if (flowerbed[flowerbed.length - 1] == 0 && flowerbed[flowerbed.length - 2] == 0) { + flowerbed[flowerbed.length - 1] = 1; + cnt ++; + } + } + for (int i = 1;i < flowerbed.length - 1;) { + if (flowerbed[i - 1] == 0 && flowerbed[i] == 0 && flowerbed[i + 1] == 0 ) { + cnt ++; + flowerbed[i] = 1; + i = i + 2; + }else { + i ++; + } + } + return cnt >= n; + } + } + +注意点: + +1从头到尾找到符合0 0 0情况的个数。 + +2注意数组两边的特殊情况处理 0 0。当长度大于1时处理即可。 + +3。处理长度为1时的数组 + +> 392. 判断子序列 +> +> 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 +> +> 你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。 +> +> 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 +> +> 示例 1: +> s = "abc", t = "ahbgdc" +> +> 返回 true. +> +> 示例 2: +> s = "axc", t = "ahbgdc" +> +> 返回 false. + +解析:本题我刚开始想的办法是使用dp求出LCS最长公共子序列,判断长度是否等于t的长度,结果超时了。事实证明我想太多了。 + +只需要按顺序查找t的字母是否都在s中即可,当然,要注意查找时候的下标移动,否则也是O(N2)的复杂度 + +DP解法:超时 + + import java.util.*; + class Solution { + public boolean isSubsequence(String s, String t) { + return LCS(s,t); + } + public boolean LCS(String s, String t) { + int [][]dp = new int[s.length() + 1][t.length() + 1]; + for (int i = 1;i <= s.length();i ++) { + for (int j = 1;j <= t.length();j ++) { + if (s.charAt(i - 1) == t.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + }else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + int len = dp[s.length()][t.length()]; + return len == s.length(); + } + } + +正解: 巧用indexOf方法indexOf(c,index + 1)来找到从index + 1开始的c字母。 + + import java.util.*; + class Solution { + public boolean isSubsequence(String s, String t) { + int index = -1; + for (int i = 0;i < s.length();i ++) { + index = t.indexOf(s.charAt(i), index + 1); + if (index == -1) { + return false; + } + } + return true; + } + } + +> 665. 非递减数列 这题暂时没有想到比较好的方法 + +> 给定一个长度为 n 的整数数组,你的任务是判断在最多改变 1 个元素的情况下,该数组能否变成一个非递减数列。 +> +> 我们是这样定义一个非递减数列的: 对于数组中所有的 i (1 <= i < n),满足 array[i] <= array[i + 1]。 +> +> 示例 1: +> +> 输入: [4,2,3] +> 输出: True +> 解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。 +> 示例 2: +> +> 输入: [4,2,1] +> 输出: False +> 解释: 你不能在只改变一个元素的情况下将其变为非递减数列。 + + + +> 122. 买卖股票的最佳时机 II +> 题意: +> +> 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 +> +> 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 +> +> 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 +> +> 示例 1: +> +> 输入: [7,1,5,3,6,4] +> 输出: 7 +> 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 +> 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 +> 示例 2: +> +> 输入: [1,2,3,4,5] +> 输出: 4 +> 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 +> 注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 +> 因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 +> 示例 3: +> +> 输入: [7,6,4,3,1] +> 输出: 0 +> 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 + +题意:只要出现价差为正时就买入,这样一定是最赚的,注意本题中同一天可以进行卖出后再进行买入。 + +对于 [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] 添加到收益中,从而在局部最优的情况下也保证全局最优。 + + class Solution { + public int maxProfit(int[] prices) { + int buy = 0; + int sell = 1; + int cnt = 0; + while(buy < sell && sell < prices.length) { + if(prices[sell] > prices[buy]) { + cnt += prices[sell] - prices[buy]; + } + buy = sell; + sell = buy + 1; + } + return cnt; + } + } + + +## 双指针 + +双指针 +双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。 + +双指针其实一般不会抽取出来单独作为一种算法,因为数组中经常会用到,而且我们熟悉的二分查找也使用了双指针。 + +二分查找 +> 167. 两数之和 II - 输入有序数组 +> +> 给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。 +> +> 函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。 +> +> 说明: +> +> 返回的下标值(index1 和 index2)不是从零开始的。 +> 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 +> 示例: +> +> 输入: numbers = [2, 7, 11, 15], target = 9 +> 输出: [1,2] +> 解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 + +这题基本操作了。 + + class Solution { + public int[] twoSum(int[] numbers, int target) { + int left = 0,right = numbers.length - 1; + int []arr = new int[2]; + while (left < right) { + if (numbers[left] + numbers[right] < target) { + left ++; + }else if (numbers[left] + numbers[right] > target) { + right --; + }else { + arr[0] = left + 1; + arr[1] = right + 1; + return arr; + } + } + return arr; + } + } + +> 633. 平方数之和 +> +> 给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c。 +> +> 示例1: +> +> 输入: 5 +> 输出: True +> 解释: 1 * 1 + 2 * 2 = 5 +> +> +> 示例2: +> +> 输入: 3 +> 输出: False + +基操 + + import java.util.*; + class Solution { + public boolean judgeSquareSum(int c) { + double n = Math.sqrt(c); + for (double i = 0;i <= n;i ++) { + double diff = c - i * i; + int j = (int) Math.sqrt(diff); + if (j * j == diff) { + return true; + } + } + return false; + } + } + +345. 反转字符串中的元音字母 +编写一个函数,以字符串作为输入,反转该字符串中的元音字母。 + +示例 1: +给定 s = "hello", 返回 "holle". + +示例 2: +给定 s = "leetcode", 返回 "leotcede". + +注意: +元音字母不包括 "y". + +快排思想进行交换即可 + + import java.util.*; + class Solution { + public String reverseVowels(String s) { + char[] arr = s.toCharArray(); + int left = 0,right = s.length() - 1; + while (left < right){ + while (left < right && !isVowels(arr[left])) { + left ++; + } + while (left < right && !isVowels(arr[right])) { + right --; + } + char temp = arr[left]; + arr[left] = arr[right]; + arr[right] = temp; + left ++; + right --; + } + return String.valueOf(arr); + } + public boolean isVowels(char c) { + char[]arr = {'a', 'i', 'e', 'u', 'o', 'A', 'I', 'E', 'U', 'O'}; + for (int k = 0;k < arr.length;k ++) { + if (c == arr[k]) { + return true; + } + } + return false; + } + } + +> 680. 验证回文字符串 Ⅱ +> +> 给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。 +> +> 示例 1: +> +> 输入: "aba" +> 输出: True +> 示例 2: +> +> 输入: "abca" +> 输出: True +> 解释: 你可以删除c字符。 +> 注意: +> +> 字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。 + +在验证回文的基础上加上一步,当遇到不符合要求的字符时,再往前走一步即可。当然机会只有一次。 + +本题可能遇到一个问题,如果直接用while循环写的话,会遇到两种情况,一种是左边加一,一种是右边减一。只要一种情况满足即可。所以我们要另外写一个判断函数,然后用||来表示两种情况即可。 + + class Solution { + public boolean validPalindrome(String s) { + int left = 0,right = s.length() - 1; + while (left < right) { + if (s.charAt(left) == s.charAt(right)) { + left ++; + right --; + }else { + return valid(s, left + 1,right) || valid(s, left, right - 1); + } + } + return true; + } + + public boolean valid(String s, int i, int j) { + int left = i,right = j; + while (left < right) { + if (s.charAt(left) == s.charAt(right)) { + left ++; + right --; + } + else return false; + } + return true; + } + } + + +> 88. 合并两个有序数组 +> +> 这题给的用例有毒,不谈。 +> +> +> 141. 环形链表 +> +> 剑指offer +> 使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。 +> +> + +> 524. 通过删除字母匹配到字典里最长单词 +> 给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。 +> +> 示例 1: +> +> 输入: +> s = "abpcplea", d = ["ale","apple","monkey","plea"] +> +> 输出: +> "apple" +> 示例 2: +> +> 输入: +> s = "abpcplea", d = ["a","b","c"] +> +> 输出: +> "a" +> 说明: +> +> 所有输入的字符串只包含小写字母。 +> 字典的大小不会超过 1000。 +> 所有输入的字符串长度不会超过 1000。 + +解析:本题的双指针不是指左右指针了,而是分别扫描两个字符串所用的指针。 + +由于题目要求先按照长度排序再按照字典序排序,于是使用比较器可以实现该逻辑,然后再一一匹配即可。 + + import java.util.*; + class Solution { + public String findLongestWord(String s, List d) { + Collections.sort(d, new Comparator() { + @Override + public int compare(String o1, String o2) { + if (o1.length() != o2.length()) { + return o2.length() - o1.length(); + } else { + return o1.compareTo(o2); + } + } + }); + for (String str : d) { + int i = 0,j = 0; + while (i < s.length() && j < str.length()) { + if (s.charAt(i) == str.charAt(j)) { + i ++; + j ++; + }else { + i ++; + } + } + if (j == str.length()) { + return str; + } + } + return ""; + } + } + +## 排序 + +排序 +> +> 快速选择 +> 一般用于求解 Kth Element 问题,可以在 O(N) 时间复杂度,O(1) 空间复杂度完成求解工作。 +> +> 与快速排序一样,快速选择一般需要先打乱数组,否则最坏情况下时间复杂度为 O(N2)。 +> +> 堆排序 +> +> 堆排序用于求解 TopK Elements 问题,通过维护一个大小为 K 的堆,堆中的元素就是 TopK Elements。当然它也可以用于求解 Kth Element 问题,堆顶元素就是 Kth Element。快速选择也可以求解 TopK Elements 问题,因为找到 Kth Element 之后,再遍历一次数组,所有小于等于 Kth Element 的元素都是 TopK Elements。可以看到,快速选择和堆排序都可以求解 Kth Element 和 TopK Elements 问题。 + + 排序 :时间复杂度 O(NlogN),空间复杂度 O(1) + + public int findKthLargest(int[] nums, int k) { + Arrays.sort(nums); + return nums[nums.length - k]; + } + + 堆排序 :时间复杂度 O(NlogK),空间复杂度 O(K)。 + 每次插入一个元素,当元素超过k个时,弹出顶部的最小值,当元素push完以后,剩下的元素就是前k大的元素,堆顶元素就是第K大的元素。 + + public int findKthLargest(int[] nums, int k) { + PriorityQueue pq = new PriorityQueue<>(); // 小顶堆 + for (int val : nums) { + pq.add(val); + if (pq.size() > k) // 维护堆的大小为 K + pq.poll(); + } + return pq.peek(); + } + + 快速选择(也可以认为是快速排序的partition加上二分的算法) + + 利用partition函数求出一个数的最终位置,再通过二分来逼近第k个位置,算法结论表明该算法的时间复杂度是O(N) + + class Solution { + public int findKthLargest(int[] nums, int k) { + k = nums.length - k; + int l = 0, r = nums.length - 1; + while (l < r) { + int pos = partition(nums, l , r); + if (pos == k) return nums[pos]; + else if (pos < k) { + l = pos + 1; + }else { + r = pos - 1; + } + } + return nums[k]; + } + public int partition(int[] nums, int left, int right) { + int l = left, r = right; + int temp = nums[l]; + while (l < r) { + while (l < r && nums[r] >= temp) { + r --; + } + while (l < r && nums[l] <= temp) { + l ++; + } + if (l < r) { + int tmp = nums[l]; + nums[l] = nums[r]; + nums[r] = tmp; + } + } + nums[left] = nums[l]; + nums[l] = temp; + return l; + } + } + +桶排序 + +> 347. 前K个高频元素 +> +> 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 +> +> 例如, +> +> 给定数组 [1,1,1,2,2,3] , 和 k = 2,返回 [1,2]。 +> +> 注意: +> +> 你可以假设给定的 k 总是合理的,1 ≤ k ≤ 数组中不相同的元素的个数。 +> 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。 + + + + + 解析: + 设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。 + + import java.util.*; + class Solution { + public List topKFrequent(int[] nums, int k) { + Map map = new HashMap<>(); + for (int i : nums) { + if (map.containsKey(i)) { + map.put(i, map.get(i) + 1); + }else { + map.put(i, 1); + } + } + ArrayList[] timesMap = new ArrayList[nums.length + 1]; + for (int key : map.keySet()) { + int times = map.get(key); + if (timesMap[times] == null) { + timesMap[times] = new ArrayList<>(); + timesMap[times].add(key); + } + else { + timesMap[times].add(key); + } + } + List top = new ArrayList(); + for (int i = timesMap.length - 1;i > 0 && top.size() < k;i --) { + if (timesMap[i] != null) { + top.addAll(timesMap[i]); + } + } + return top; + } + } + + 注意: + + 1本题的难点在于先用hashmap存储数据得到每个数的频率,再用数组存储每个频率对应哪些数。 + + 2最后再通过频率数组的最后一位开始往前找,找到k个数为之,就是出现频率最高的k个数了。 + +> 451. 根据字符出现频率排序 +> +> 给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 + +输入: +"tree" + +输出: +"eert" + +解释: +'e'出现两次,'r'和't'都只出现一次。 +因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。 + + +我下面这个写法只考虑了小写字母的情况,大写字母与其他字符没有考虑,是错误的。正确的做法还是应该用一个128长度的char数组 +。因为char是1一个字节长度,也就是8位,2的8次方是256,考虑正数的话就是128。 + +上题使用map是因为32位整数太大,数组存不下,而本题char数组只需要长度为128即可,不用使用map。 + +错误解: + + public static String frequencySort(String s) { + int []arr = new int[26]; + char []crr = s.toCharArray(); + for (char c : crr) { + arr[c - 'a']++; + } + + List[]times = new ArrayList[s.length() + 1]; + for (int i = 0;i < arr.length;i ++) { + if (times[arr[i]] == null) { + times[arr[i]] = new ArrayList<>(); + times[arr[i]].add((char) ('a' + i)); + }else { + times[arr[i]].add((char) ('a' + i)); + } + } + StringBuilder sb = new StringBuilder(); + for (int i = times.length - 1;i > 0 ;i --) { + if (times[i] != null) { + for (char c : times[i]) { + int time = 0; + while (time < i) { + sb.append(c); + time ++; + } + } + } + } + return sb.toString(); + } + +正解: + + class Solution { + public static String frequencySort(String s) { + int []arr = new int[128]; + char []crr = s.toCharArray(); + for (char c : crr) { + arr[c]++; + } + + List[]times = new ArrayList[s.length() + 1]; + for (int i = 0;i < arr.length;i ++) { + if (times[arr[i]] == null) { + times[arr[i]] = new ArrayList<>(); + times[arr[i]].add((char) (i)); + }else { + times[arr[i]].add((char) (i)); + } + } + StringBuilder sb = new StringBuilder(); + for (int i = times.length - 1;i > 0 ;i --) { + if (times[i] != null) { + for (char c : times[i]) { + int time = 0; + while (time < i) { + sb.append(c); + time ++; + } + } + } + } + return sb.toString(); + } + } + +> 75. 分类颜色 +> 给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。 +> +> 此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 +> +> 注意: +> 不能使用代码库中的排序函数来解决这道题。 + +> 进阶: +> +> 一个直观的解决方案是使用计数排序的两趟扫描算法。 +> 首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。 +> 你能想出一个仅使用常数空间的一趟扫描算法吗? + +解析:本题的思路一个就是题目所说的计数排序,还有一个便是使用交换算法,设置三个下标,zero, one, two,分别表示0的结尾,1的结尾,2的结尾,并且在遍历过程中把0换到one前面,把2换到one后面,中间的就是1了。 + + class Solution { + public void sortColors(int[] nums) { + if (nums.length <= 1)return; + int zero = -1, one = 0,two = nums.length; + while (one < two) { + if (nums[one] == 0) { + swap(nums, ++zero, one++); + }else if (nums[one] == 2) { + swap(nums, --two, one); + }else { + one ++; + } + } + } + public void swap(int []nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + } + +## 二分查找 +正常实现 + + public int binarySearch(int[] nums, int key) { + int l = 0, h = nums.length - 1; + while (l <= h) { + int m = l + (h - l) / 2; + if (nums[m] == key) { + return m; + } else if (nums[m] > key) { + h = m - 1; + } else { + l = m + 1; + } + } + return -1; + } + +> 时间复杂度 +> +> 二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。 +> +> m 计算 +> +> 有两种计算中值 m 的方式: +> +> m = (l + h) / 2 +> m = l + (h - l) / 2 +> l + h 可能出现加法溢出,最好使用第二种方式。 +> +> 返回值 +> +> 循环退出时如果仍然没有查找到 key,那么表示查找失败。可以有两种返回值: +> +> -1:以一个错误码表示没有查找到 key +> l:将 key 插入到 nums 中的正确位置 + +变种 + +题目:在一个有重复元素的数组中查找 key 的最左位置 + +如果是直接查找那么复杂度为O(n)所以可以采用二分优化 + +二分查找可以有很多变种,变种实现要注意边界值的判断。 +例如在一个有重复元素的数组中查找 key 的最左位置的实现如下: + + public int binarySearch(int[] nums, int key) { + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= key) { + h = m; + } else { + l = m + 1; + } + } + return l; + } + +> 该实现和正常实现有以下不同: +> +> 循环条件为 l < h +> h 的赋值表达式为 h = m +> 最后返回 l 而不是 -1 +> 在 nums[m] >= key 的情况下,可以推导出最左 key 位于 [l, m] 区间中,这是一个闭区间。h 的赋值表达式为 h = m,因为 m 位置也可能是解。 +> +> 在 h 的赋值表达式为 h = mid 的情况下,如果循环条件为 l <= h,那么会出现循环无法退出的情况,因此循环条件只能是 l < h。以下演示了循环条件为 l <= h 时循环无法退出的情况: +> +nums = {0, 1, 2}, key = 1 +l m h +0 1 2 nums[m] >= key +0 0 1 nums[m] < key +1 1 1 nums[m] >= key +1 1 1 nums[m] >= key +... +当循环体退出时,不表示没有查找到 key,因此最后返回的结果不应该为 -1。为了验证有没有查找到,需要在调用端判断一下返回位置上的值和 key 是否相等 + +> 69. x 的平方根 +> +> 实现 int sqrt(int x) 函数。 +> +> 计算并返回 x 的平方根,其中 x 是非负整数。 +> +> 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。 +> +> 示例 1: +> +> 输入: 4 +> 输出: 2 +> 示例 2: +> +> 输入: 8 +> 输出: 2 +> 说明: 8 的平方根是 2.82842..., +> 由于返回类型是整数,小数部分将被舍去。 + +一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 ~ x 之间查找 sqrt。 + +对于 x = 8,它的开方是 2.82842...,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时,h 总是比 l 小 1,也就是说 h = 2,l = 3,因此最后的返回值应该为 h 而不是 l。 + + public int mySqrt(int x) { + if (x <= 1) { + return x; + } + int l = 1, h = x; + while (l <= h) { + int mid = l + (h - l) / 2; + int sqrt = x / mid; + if (sqrt == mid) { + return mid; + } else if (mid > sqrt) { + h = mid - 1; + } else { + l = mid + 1; + } + } + return h; + } + +注意:由于要取的值是比原值小的整数,所以等sqrt小于mid时,并且此时l > h时说明h此时已经是最接近sqrt且比它小的值了。当然如果前面有相等的情况时已经返回了。 +744. 寻找比目标字母大的最小字母 + +> 给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。 +> +> 数组里字母的顺序是循环的。举个例子,如果目标字母target = 'z' 并且有序数组为 letters = ['a', 'b'],则答案返回 'a'。 +> +> 示例: +> +> 输入: +> letters = ["c", "f", "j"] +> target = "a" +> 输出: "c" +> +> 输入: +> letters = ["c", "f", "j"] +> target = "c" +> 输出: "f" +> +> 输入: +> letters = ["c", "f", "j"] +> target = "d" +> 输出: "f" +> +> 输入: +> letters = ["c", "f", "j"] +> target = "g" +> 输出: "j" +> +> 输入: +> letters = ["c", "f", "j"] +> target = "j" +> 输出: "c" +> +> 输入: +> letters = ["c", "f", "j"] +> target = "k" +> 输出: "c" +> 注: +> +> letters长度范围在[2, 10000]区间内。 +> letters 仅由小写字母组成,最少包含两个不同的字母。 +> 目标字母target 是一个小写字母。 + +解析:使用二分查找逼近,找到字母后右边那个就是最小的,找不到的话返回结束位置的右边第一个字母。 + +注意: +1 与上一题相反,本题的要找的是比指定值大一点的数,所以此时l > r满足时,l就是比指定值大一点的数了。 + +2 注意可能有连续重复的数字,所以一直往右找到一个数大于指定值 + + class Solution { + public char nextGreatestLetter(char[] letters, char target) { + if (letters == null || letters.length == 0) return 'a'; + int l = 0,r = letters.length - 1; + while (l <= r) { + int m = l + (r - l)/2; + if (letters[m] <= target ) { + l = m + 1; + }else { + r = m - 1; + } + } + + if (l <= letters.length - 1) { + return letters[l]; + }else { + return letters[0]; + } + + } + } + +540. 有序数组中的单一元素 + +给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。 + +示例 1: + +输入: [1,1,2,3,3,4,4,8,8] +输出: 2 +示例 2: + +输入: [3,3,7,7,10,11,11] +输出: 10 +注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。 + +解析:本题其实可以用位运算做,但是限制了时间复杂度,所以考虑使用二分,这题我做不出来,可以参考下面答案 + + +令 index 为 Single Element 在数组中的位置。如果 m 为偶数,并且 m + 1 < index,那么 nums[m] == nums[m + 1];m + 1 >= index,那么 nums[m] != nums[m + 1]。 + +从上面的规律可以知道,如果 nums[m] == nums[m + 1],那么 index 所在的数组位置为 [m + 2, h],此时令 l = m + 2;如果 nums[m] != nums[m + 1],那么 index 所在的数组位置为 [l, m],此时令 h = m。 + +因为 h 的赋值表达式为 h = m,那么循环条件也就只能使用 l < h 这种形式。 + + public int singleNonDuplicate(int[] nums) { + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (m % 2 == 1) { + m--; // 保证 l/h/m 都在偶数位,使得查找区间大小一直都是奇数 + } + if (nums[m] == nums[m + 1]) { + l = m + 2; + } else { + h = m; + } + } + return nums[l]; + } +153. 寻找旋转排序数组中的最小值 + +假设按照升序排序的数组在预先未知的某个点上进行了旋转。 + +( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 + +请找出其中最小的元素。 + +你可以假设数组中不存在重复元素。 + +示例 1: + +输入: [3,4,5,1,2] +输出: 1 +示例 2: + +输入: [4,5,6,7,0,1,2] +输出: 0 + +解析:比较经典的题目,正常情况下是顺序的,仅当arr[i] > arr[i + 1]可以得知arr[i + 1]是最小值。 +顺序扫描需要O(n),使用二分查找可以优化到Log2n + +旋转数组的两个递增数组由最小值来划分。 +所以对于l, m, r来说,如果arr[m] < arr[h],说明到m到h是有序部分,最小值应该在l到m之间。所以令r = m; +如果arr[h] < arr[m],说明最小值在m到h之间。所以令l = m + 1。 +当l > r时,说明nums[m] > nums[h]已经到达终点,此时nums[m + 1 ]就是最小值 + + public int findMin(int[] nums) { + int l = 0, h = nums.length - 1; + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] <= nums[h]) { + h = m; + } else { + l = m + 1; + } + } + return nums[l]; + } + +> 34. 在排序数组中查找元素的第一个和最后一个位置 +> +> 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 +> +> 你的算法时间复杂度必须是 O(log n) 级别。 +> +> 如果数组中不存在目标值,返回 [-1, -1]。 +> +> 示例 1: +> +> 输入: nums = [5,7,7,8,8,10], target = 8 +> 输出: [3,4] +> 示例 2: +> +> 输入: nums = [5,7,7,8,8,10], target = 6 +> 输出: [-1,-1] + +解析:参考别人的答案: + +1 首先通过二分查找找到该数出现的最左边位置(与例题一样) + +2 然后通过二分查找找到比该数大1的数出现的位置,如果不存在,则刚好在所求数右边一位,再减1即可。 + +3 边界条件判断 + + public int[] searchRange(int[] nums, int target) { + int first = binarySearch(nums, target); + int last = binarySearch(nums, target + 1) - 1; + if (first == nums.length || nums[first] != target) { + return new int[]{-1, -1}; + } else { + return new int[]{first, Math.max(first, last)}; + } + } + + private int binarySearch(int[] nums, int target) { + int l = 0, h = nums.length; // 注意 h 的初始值 + while (l < h) { + int m = l + (h - l) / 2; + if (nums[m] >= target) { + h = m; + } else { + l = m + 1; + } + } + return l; + } +## DFS和BFS,回溯 +## 搜索 + +深度优先搜索和广度优先搜索广泛运用于树和图中,但是它们的应用远远不止如此。 + +### [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#bfs)BFS + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/4ff355cf-9a7f-4468-af43-e5b02038facc.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/4ff355cf-9a7f-4468-af43-e5b02038facc.jpg) + +广度优先搜索的搜索过程有点像一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。 + +第一层: + +* 0 -> {6,2,1,5}; + +第二层: + +* 6 -> {4} +* 2 -> {} +* 1 -> {} +* 5 -> {3} + +第三层: + +* 4 -> {} +* 3 -> {} + +可以看到,每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di<=dj。利用这个结论,可以求解最短路径等 **最优解** 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径。 + +在程序实现 BFS 时需要考虑以下问题: + +* 队列:用来存储每一轮遍历得到的节点; +* 标记:对于遍历过的节点,应该将它标记,防止重复遍历。 +* + +计算在网格中从原点到特定点的最短路径长度 + +[[1,1,0,1], + [1,0,1,0], + [1,1,1,1], + [1,0,1,1]] + +1 表示可以经过某个位置,求解从 (0, 0) 位置到 (tr, tc) 位置的最短路径长度。 +2 由于每个点需要保存x坐标,y坐标以及长度,所以必须要用一个类将三个属性封装起来。 +3 由于bfs每次只将距离加一,所以当位置抵达终点时,此时的距离就是最短路径了。 + + private static class Position { + int r, c, length; + public Position(int r, int c, int length) { + this.r = r; + this.c = c; + this.length = length; + } + } + + public static int minPathLength(int[][] grids, int tr, int tc) { + int[][] next = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + int m = grids.length, n = grids[0].length; + Queue queue = new LinkedList<>(); + queue.add(new Position(0, 0, 1)); + while (!queue.isEmpty()) { + Position pos = queue.poll(); + for (int i = 0; i < 4; i++) { + Position nextPos = new Position(pos.r + next[i][0], pos.c + next[i][1], pos.length + 1); + if (nextPos.r < 0 || nextPos.r >= m || nextPos.c < 0 || nextPos.c >= n) continue; + if (grids[nextPos.r][nextPos.c] != 1) continue; + grids[nextPos.r][nextPos.c] = 0; + if (nextPos.r == tr && nextPos.c == tc) return nextPos.length; + queue.add(nextPos); + } + } + return -1; + } + + 279. 完全平方数 + +> 组成整数的最小平方数数量 +> +> 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 + + +> 示例 1: +> +> 输入: n = 12 +> 输出: 3 +> 解释: 12 = 4 + 4 + 4. +> 示例 2: +> +> 输入: n = 13 +> 输出: 2 +> 解释: 13 = 4 + 9. + +1 可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。 + +2 要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。 + +3 首先生成平方数序列放入数组,然后通过队列,每次减去一个平方数,把剩下的数加入队列,也就是通过bfs的方式,当此时的数刚好等于平方数,则满足题意,由于每次循环level加一,所以最后输出的level就是需要的平方数个数。 + +本题也可以用动态规划求解,在之后动态规划部分中会再次出现。 + + public int numSquares(int n) { + List squares = generateSquares(n); + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[n + 1]; + queue.add(n); + marked[n] = true; + int level = 0; + while (!queue.isEmpty()) { + int size = queue.size(); + level++; + while (size-- > 0) { + int cur = queue.poll(); + for (int s : squares) { + int next = cur - s; + if (next < 0) { + break; + } + if (next == 0) { + return level; + } + if (marked[next]) { + continue; + } + marked[next] = true; + queue.add(cur - s); + } + } + } + return n; + } + + /** + * 生成小于 n 的平方数序列 + * @return 1,4,9,... + */ + private List generateSquares(int n) { + List squares = new ArrayList<>(); + int square = 1; + int diff = 3; + while (square <= n) { + squares.add(square); + square += diff; + diff += 2; + } + return squares; + } +127. 单词接龙 +> +> 给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则: +> +> 每次转换只能改变一个字母。 +> 转换过程中的中间单词必须是字典中的单词。 +> 说明: +> +> 如果不存在这样的转换序列,返回 0。 +> 所有单词具有相同的长度。 +> 所有单词只由小写字母组成。 +> 字典中不存在重复的单词。 +> 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。 +> 示例 1: +> +> 输入: +> beginWord = "hit", +> endWord = "cog", +> wordList = ["hot","dot","dog","lot","log","cog"] +> +> 输出: 5 +> +> 解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", +> 返回它的长度 5。 +> 示例 2: +> +> 输入: +> beginWord = "hit" +> endWord = "cog" +> wordList = ["hot","dot","dog","lot","log"] +> +> 输出: 0 + +解释: endWord "cog" 不在字典中,所以无法进行转换。 + +找出一条从 beginWord 到 endWord 的最短路径,每次移动规定为改变一个字符,并且改变之后的字符串必须在 wordList 中。 + +单词台阶问题,亚马逊面试时考了。 + +这个参考别人的答案,我会加上解析。 + + public int ladderLength(String beginWord, String endWord, List wordList) { + //注意此处把首个单词放到了list的最后面,所以start才会是N-1。别搞错了。 + wordList.add(beginWord); + int N = wordList.size(); + int start = N - 1; + int end = 0; + while (end < N && !wordList.get(end).equals(endWord)) { + end++; + } + if (end == N) { + return 0; + } + List[] graphic = buildGraphic(wordList); + return getShortestPath(graphic, start, end); + } + + 本方法用于把每个单词开头的完整序列保存起来,以便让bfs过程中遍历到所有情况。 + + private List[] buildGraphic(List wordList) { + int N = wordList.size(); + List[] graphic = new List[N]; + for (int i = 0; i < N; i++) { + graphic[i] = new ArrayList<>(); + for (int j = 0; j < N; j++) { + if (isConnect(wordList.get(i), wordList.get(j))) { + graphic[i].add(j); + } + } + } + return graphic; + } + + 本方法用于上面这个方法连接单词序列时,需要判断两个单词是否只需要一次改变即可,如果不满足要求,则跳过这个单词。 + + private boolean isConnect(String s1, String s2) { + int diffCnt = 0; + for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { + if (s1.charAt(i) != s2.charAt(i)) { + diffCnt++; + } + } + return diffCnt == 1; + } + + 这一步就是通过BFS进行单词序列连接了。 + 让初始所在位置入队,然后去遍历它能转变成的单词,接着进行bfs的遍历。 + + 最终当next = end时,说明已经能到达最终位置了。所以此时的路径时最短的。每次出队都是一个路径,所以返回path即为最短路径长度。 + + private int getShortestPath(List[] graphic, int start, int end) { + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[graphic.length]; + queue.add(start); + marked[start] = true; + int path = 1; + while (!queue.isEmpty()) { + int size = queue.size(); + path++; + while (size-- > 0) { + int cur = queue.poll(); + for (int next : graphic[cur]) { + if (next == end) { + return path; + } + if (marked[next]) { + continue; + } + marked[next] = true; + queue.add(next); + } + } + } + return 0; + } + +## DFS + + +广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。 + +而深度优先搜索在得到一个新节点时立马对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。 + +从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。 + +在程序实现 DFS 时需要考虑以下问题: + +栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。 +标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。 + +695. 岛屿的最大面积 + +> 给定一个包含了一些 0 和 1的非空二维数组 grid , 一个 岛屿 是由四个方向 (水平或垂直) 的 1 (代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。 +> +> 找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为0。) +> +> 示例 1: +> +> [[0,0,1,0,0,0,0,1,0,0,0,0,0], +> [0,0,0,0,0,0,0,1,1,1,0,0,0], +> [0,1,1,0,1,0,0,0,0,0,0,0,0], +> [0,1,0,0,1,1,0,0,1,0,1,0,0], +> [0,1,0,0,1,1,0,0,1,1,1,0,0], +> [0,0,0,0,0,0,0,0,0,0,1,0,0], +> [0,0,0,0,0,0,0,1,1,1,0,0,0], +> [0,0,0,0,0,0,0,1,1,0,0,0,0]] +> 对于上面这个给定矩阵应返回 6。注意答案不应该是11,因为岛屿只能包含水平或垂直的四个方向的‘1’。 +> +> 示例 2: +> +> [[0,0,0,0,0,0,0,0]] +> 对于上面这个给定的矩阵, 返回 0。 +> +> 注意: 给定的矩阵grid 的长度和宽度都不超过 50。 + +//只需要从每个1出发,然后遍历相连的所有1,得到总和,更新最大值即可。 + + + public static int maxAreaOfIsland(int[][] grid) { + int [][]visit = new int[grid.length][grid[0].length]; + int max = 0; + for (int i = 0;i < grid.length;i ++) { + for (int j = 0;j < grid[0].length;j ++) { + if (grid[i][j] == 1) { + max = Math.max(max, dfs(grid, i, j, visit, 0)); + } + } + } + return max; + } + + //通过递归进行了各个方向的可达性遍历,于是可以遍历到所有的1,然后更新最大值。 + + public static int dfs(int [][]grid, int x, int y, int [][]visit, int count) { + if (x < 0 || x > grid.length - 1 || y < 0 || y > grid[0].length - 1) { + return count; + } + if (visit[x][y] == 1 || grid[x][y] == 0) { + return count; + } + + visit[x][y] = 1; + count ++; + + count += dfs(grid, x + 1, y, visit, 0); + count += dfs(grid, x - 1, y, visit, 0); + count += dfs(grid, x, y + 1, visit, 0); + count += dfs(grid, x, y - 1, visit, 0); + return count; + } + +200. 岛屿的个数 + +> 给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。 +> +> 示例 1: +> +> 输入: +> 11110 +> 11010 +> 11000 +> 00000 +> +> 输出: 1 +> 示例 2: +> +> 输入: +> 11000 +> 11000 +> 00100 +> 00011 +> +> 输出: 3 + + public class 图的连通分量个数 { + static int count = 0; + public int findCircleNum(int[][] M) { + count = 0; + int []visit = new int[M.length]; + Arrays.fill(visit, 0); + for (int i = 0;i < M.length;i ++) { + if (visit[i] == 0) { + dfs(M, i, visit); + count ++; + } + } + + return count; + } + + //每次访问把能到达的点标记为1,并且访问结束时计数加一。最终得到岛屿个数。 + public void dfs (int [][]M, int j, int []visit) { + for (int i = 0;i < M.length;i ++) { + if (M[j][i] == 1 && visit[i] == 0) { + visit[i] = 1; + dfs(M, i, visit); + } + } + } + + + } + +547. 朋友圈 + +> 班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。 +> +> 给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。 +> +> 示例 1: +> +> 输入: +> [[1,1,0], +> [1,1,0], +> [0,0,1]] +> 输出: 2 +> 说明:已知学生0和学生1互为朋友,他们在一个朋友圈。 +> 第2个学生自己在一个朋友圈。所以返回2。 +> 示例 2: +> +> 输入: +> [[1,1,0], +> [1,1,1], +> [0,1,1]] +> 输出: 1 +> 说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。 +> 注意: +> +> N 在[1,200]的范围内。 +> 对于所有学生,有M[i][i] = 1。 +> 如果有M[i][j] = 1,则有M[j][i] = 1。 + +这题的答案是这样的: + + private int n; + + public int findCircleNum(int[][] M) { + n = M.length; + int circleNum = 0; + boolean[] hasVisited = new boolean[n]; + for (int i = 0; i < n; i++) { + if (!hasVisited[i]) { + dfs(M, i, hasVisited); + circleNum++; + } + } + return circleNum; + } + + private void dfs(int[][] M, int i, boolean[] hasVisited) { + hasVisited[i] = true; + for (int k = 0; k < n; k++) { + if (M[i][k] == 1 && !hasVisited[k]) { + dfs(M, k, hasVisited); + } + } + } + +但是我的做法跟他一样,却会递归栈溢出,我只是把boolean判断换成了int判断,有点奇怪,还望指教。 + + // private static int n; + // public static int findCircleNum(int[][] M) { + // n = M.length; + // int cnt = 0 ; + // int []visit = new int[n]; + // for (int i = 0;i < M.length;i ++) { + // if(visit[i] == 0) { + // dfs(M, visit, i); + // cnt ++; + // } + // } + // return cnt; + // } + // public static void dfs(int[][]M, int[] visit, int i) { + // visit[i] = 1; + // for (int j = 0;j < M.length;j ++) { + // if(visit[j] == 0 && M[i][j] == 1) { + // dfs(M, visit, i); + // } + // } + // } + + +130. 被围绕的区域 + +> 给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。 +> +> 找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 +> +> 示例: +> +> X X X X +> X O O X +> X X O X +> X O X X +> 运行你的函数后,矩阵变为: +> +> X X X X +> X X X X +> X X X X +> X O X X +> 解释: +> +> 被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 + +参考大佬答案:很有意思的解法 + +1 我们是要把X包围的O变成X但是有一个很麻烦的问题就是,如何判断O被X完全包住,这是非常难解决的。 + +2 于是换一种思路,把不被X包住的那些O找出来,剩下的不就是X了吗。 + +3 不被X包住的O,首先它们的起点一定是在边缘处,所以我们从边缘处找出一个O,然后从O出发,找到所有相连的O,把它们变成T(为了不跟里面的O混淆) + +4 最后遍历一次棋盘,把T变成O,把O变成X,就搞定了。妙啊,妙啊。 + + + + private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + private int m, n; + + public void solve(char[][] board) { + if (board == null || board.length == 0) { + return; + } + + m = board.length; + n = board[0].length; + + for (int i = 0; i < m; i++) { + dfs(board, i, 0); + dfs(board, i, n - 1); + } + for (int i = 0; i < n; i++) { + dfs(board, 0, i); + dfs(board, m - 1, i); + } + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == 'T') { + board[i][j] = 'O'; + } else if (board[i][j] == 'O') { + board[i][j] = 'X'; + } + } + } + } + + private void dfs(char[][] board, int r, int c) { + if (r < 0 || r >= m || c < 0 || c >= n || board[r][c] != 'O') { + return; + } + board[r][c] = 'T'; + for (int[] d : direction) { + dfs(board, r + d[0], c + d[1]); + } + } + +417.能到达的太平洋和大西洋的区域 + +> 417. Pacific Atlantic Water Flow (Medium) +> +> Given the following 5x5 matrix: +> +> Pacific ~ ~ ~ ~ ~ +> ~ 1 2 2 3 (5) * +> ~ 3 2 3 (4) (4) * +> ~ 2 4 (5) 3 1 * +> ~ (6) (7) 1 4 5 * +> ~ (5) 1 1 2 4 * +> * * * * * Atlantic +> +> Return: +> [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix). +> 左边和上边是太平洋,右边和下边是大西洋,内部的数字代表海拔,海拔高的地方的水能够流到低的地方,求解水能够流到太平洋和大西洋的所有位置。 + +1 如果说上一题已经很有趣了,这一题可以说是更奇葩了。 +力扣中国甚至没有翻译这道题。根据题意,我们要求的是水能流到太平洋和大西洋的所有点。 + +2 首先,在大西洋和 太平洋两边的水一定可以分别流入这两个海洋。 +我们用一个数组canreach[i][j]来表达能够流入到海洋。所以我么需要两个数组。 + +3 然后从海洋边上的水开始进行dfs,遇到海拔比自己高的水就把它也设置成canreach即可,于是我们就可以得到两个数组。最后遍历两个数组,都满足的点就是结果了。 + + private int m, n; + private int[][] matrix; + private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + public List pacificAtlantic(int[][] matrix) { + List ret = new ArrayList<>(); + if (matrix == null || matrix.length == 0) { + return ret; + } + + m = matrix.length; + n = matrix[0].length; + this.matrix = matrix; + boolean[][] canReachP = new boolean[m][n]; + boolean[][] canReachA = new boolean[m][n]; + + for (int i = 0; i < m; i++) { + dfs(i, 0, canReachP); + dfs(i, n - 1, canReachA); + } + for (int i = 0; i < n; i++) { + dfs(0, i, canReachP); + dfs(m - 1, i, canReachA); + } + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (canReachP[i][j] && canReachA[i][j]) { + ret.add(new int[]{i, j}); + } + } + } + + return ret; + } + + private void dfs(int r, int c, boolean[][] canReach) { + if (canReach[r][c]) { + return; + } + canReach[r][c] = true; + for (int[] d : direction) { + int nextR = d[0] + r; + int nextC = d[1] + c; + if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n + || matrix[r][c] > matrix[nextR][nextC]) { + + continue; + } + dfs(nextR, nextC, canReach); + } + } + +## Backtracking +Backtracking(回溯)属于 DFS。 + +普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。 +而 Backtracking 主要用于求解 排列组合 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。 +因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题: + +在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素; +但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。 + +17. 电话号码的字母组合 + +> 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 +> +> 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 +> +![image](http://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Telephone-keypad2.svg/200px-Telephone-keypad2.svg.png) +> +> 示例: +> +> 输入:"23" +> 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. +> + +解析:回溯法一般用于需要保存结果集的dfs,比如迷宫路径,走到头如果没有达到终点则回头。而一般的dfs走到头就开始尝试下一种情况,因为它没有保存结果集。 + +1 本题需要把数字键盘和字母做一个映射,使用数组是一个不错的办法。而使用hashmap可能会显得有点臃肿。 + +2 接着我们可以使用String或者Stringbuilder保存结果,由于string不会被改变,所以我们不需要维持其状态,直接递归即可。 + +而使用stringbuilder则需要在dfs前后维护相应变化。 + + class Solution { + public List letterCombinations(String digits) { + if (digits.equals("")) { + return new ArrayList(); + } + List list = new ArrayList<>(); + String []arr = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + dfs(list, arr, new StringBuilder(), 0, digits); + return list; + } + + public void dfs(List list, String []arr, StringBuilder sb, int cur, String digits) { + if (cur == digits.length()) { + list.add(sb.toString()); + return; + } + int num = Integer.parseInt(digits.substring(cur, cur + 1)); + + for (int i = 0;i < arr[num].length();i ++) { + sb.append(arr[num].charAt(i)); + dfs(list, arr, sb, cur + 1, digits); + sb.deleteCharAt(sb.length() - 1); + } + } + } + +93. 复原IP地址 + +给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。 + +示例: + +输入: "25525511135" +输出: ["255.255.11.135", "255.255.111.35"] + +解析:本题的解题思路不难,从左到右拼凑ip地址即可 + +但是需要判断每个段的ip是否有效,包括开头不能是0,长度<=3,大小<=255等。 + +下面是一个暴力解法,4重循环可以遍历所有情况。 + + public List restoreIpAddresses(String s) { + int i,j,k,m; + ArrayList list = new ArrayList<>(); + int len = s.length(); + for (i = 1;i< 4 && i < len - 2;i ++) { + for (j = i + 1;j < i + 4 && j < len - 1;j ++) { + for (m = j + 1;m < j + 4 && m < len;m ++) { + //substring后面的下标是不算在内的。 + String s1 = s.substring(0,i); + String s2 = s.substring(i,j); + String s3 = s.substring(j,m); + String s4 = s.substring(m,len); + if(isValid(s1) && isValid(s2) && isValid(s3) && isValid(s4)) + { + list.add(s1 + '.' + s2 + '.' + s3 + '.' + s4); + } + } + } + } + return list; + } + + public boolean isValid(String s) { + if (s.length() == 0 || s.length() > 3 || s.charAt(0) == '0' && s.length() > 1 + || Integer.parseInt(s) > 255) { + return false; + }else return true; + } + +解析: + 使用回溯法来做该题。首先从头开始,k代表ip的段数,s代表总长度,每次使用一个数字则s -= 1,直到s = 0并且k = 4时,符合题意,加入结果集。 + + 当然在for循环中,i 从0到2进行遍历。然后更新当前结果。 + + public List restoreIpAddresses(String s) { + List addresses = new ArrayList<>(); + StringBuilder tempAddress = new StringBuilder(); + doRestore(0, tempAddress, addresses, s); + return addresses; + } + + private void doRestore(int k, StringBuilder tempAddress, List addresses, String s) { + if (k == 4 || s.length() == 0) { + if (k == 4 && s.length() == 0) { + addresses.add(tempAddress.toString()); + } + return; + } + for (int i = 0; i < s.length() && i <= 2; i++) { + if (i != 0 && s.charAt(0) == '0') { + break; + } + String part = s.substring(0, i + 1); + if (Integer.valueOf(part) <= 255) { + if (tempAddress.length() != 0) { + part = "." + part; + } + tempAddress.append(part); + doRestore(k + 1, tempAddress, addresses, s.substring(i + 1)); + tempAddress.delete(tempAddress.length() - part.length(), tempAddress.length()); + } + } + } + +79. 单词搜索 + +给定一个二维网格和一个单词,找出该单词是否存在于网格中。 + +单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 + +示例: +board = +[ + ['A','B','C','E'], + ['S','F','C','S'], + ['A','D','E','E'] +] + +给定 word = "ABCCED", 返回 true. +给定 word = "SEE", 返回 true. +给定 word = "ABCB", 返回 false. + + +本题使用dfs中的回溯法,主要思路也是递归。 +但比较麻烦的一点是当遍历到满足条件的情况时,不应该继续其他递归分支了。有一个办法是使用for循环进行方向遍历,只要有一个分支满足就返回true。 + + private final static int[][] direction = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + private int m; + private int n; + + public boolean exist(char[][] board, String word) { + if (word == null || word.length() == 0) { + return true; + } + if (board == null || board.length == 0 || board[0].length == 0) { + return false; + } + + m = board.length; + n = board[0].length; + boolean[][] hasVisited = new boolean[m][n]; + + for (int r = 0; r < m; r++) { + for (int c = 0; c < n; c++) { + if (backtracking(0, r, c, hasVisited, board, word)) { + return true; + } + } + } + + return false; + } + + private boolean backtracking(int curLen, int r, int c, boolean[][] visited, final char[][] board, final String word) { + if (curLen == word.length()) { + return true; + } + if (r < 0 || r >= m || c < 0 || c >= n + || board[r][c] != word.charAt(curLen) || visited[r][c]) { + + return false; + } + + visited[r][c] = true; + + for (int[] d : direction) { + //此处完成了剪枝,如果分支i满足则不会走后续分支。 + if (backtracking(curLen + 1, r + d[0], c + d[1], visited, board, word)) { + return true; + } + } + + visited[r][c] = false; + + return false; + } + +解析:方法二: + +首先使用dfs进行递归遍历,当满足条件时,直接抛出异常退出循环,脑洞比较大,但是确实可行,可以用于递归时的剪枝 +。 + +但是实际上时间复杂度要比第一种方法大得多,这里只是提供一个终止递归的思路。 + + class Solution { + static class StopMsgException extends RuntimeException { + + } + + static boolean flag; + + public boolean exist(char[][] board, String word) { + if (word.equals("")) { + return true; + } + int [][]visit = new int[board.length][board[0].length]; + flag = false; + try { + for (int i = 0;i < board.length;i ++) { + for (int j = 0;j < board[0].length;j ++) { + if (word.charAt(0) == board[i][j]) { + dfs(board, word, visit, i, j); + } + } + } + } catch (StopMsgException e) { + System.out.println(e); + } + return flag; + } + + public void dfs(char[][] board, String word, int [][]visit, int i, int j) { + if (word.equals("")) { + flag = true; + throw new StopMsgException(); + } + if (i > board.length - 1 || i < 0 || j > board[0].length - 1 || j < 0) { + return; + } + if (visit[i][j] == 1) { + return; + } + + if (word.charAt(0) == board[i][j]) { + visit[i][j] = 1; + //没有进行剪枝,效率比较低,于是在递归判断条件中进行剪枝,避免后续不必要的递归。 + + dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i + 1, j); + dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i - 1, j); + dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i, j - 1); + dfs(board, word.length() == 1 ? "" : word.substring(1, word.length()), visit, i, j + 1); + visit[i][j] = 0; + } + } + } + +46. 全排列 + +> 给定一个没有重复数字的序列,返回其所有可能的全排列。 +> +> 示例: +> +> 输入: [1,2,3] +> 输出: +> [ +> [1,2,3], +> [1,3,2], +> [2,1,3], +> [2,3,1], +> [3,1,2], +> [3,2,1] +> ] + +解析:经典而不失优雅。dfs中使用for循环遍历,visit和list记录状态进行回溯,满足条件时加入集合并返回。 + + class Solution { + static List> Alllist = new ArrayList<>(); + public List> permute(int[] nums) { + Alllist.clear(); + List list = new ArrayList<>(); + int []visit = new int[nums.length]; + dfs(list, nums, visit); + return Alllist; + } + public void dfs(List list, int []nums, int []visit) { + if (list.size() == nums.length) { + Alllist.add(new ArrayList(list)); + return; + } + + for (int i = 0;i < nums.length;i ++) { + if (visit[i] == 0) { + visit[i] = 1; + list.add(nums[i]); + dfs(list, nums, visit); + visit[i] = 0; + list.remove(list.size() - 1); + } + } + } + } + +47. 全排列 II + +> 给定一个可包含重复数字的序列,返回所有不重复的全排列。 +> +> 示例: +> +> 输入: [1,1,2] +> 输出: +> [ +> [1,1,2], +> [1,2,1], +> [2,1,1] +> ] +> + + 解析:本题在上一题的基础上加上了一个条件,数组中有重复数字,但是结果集返回的是不同的排列。 + 这就要求我们对相同数字做过滤了。 + + 我们要明确的是,不重复排列,要求的是相同数字不能在同一次递归中出现在同一个位置。 + 比如 1 2 1 和1 2 1,这里开头的两个1可能分别对应数组下标的0和1,但只能取一个。 + + 所以我们加了一个条件,当该数没有被访问过时,我们直接过滤掉所有重复的数,只把当前数作为本次递归的首位数。 + + class Solution { + static List> Alllist = new ArrayList<>(); + public List> permuteUnique(int[] nums) { + Alllist.clear(); + List list = new ArrayList<>(); + int []visit = new int[nums.length]; + Arrays.sort(nums); + dfs(list, nums, visit); + return Alllist; + } + public void dfs(List list, int []nums, int []visit) { + if (list.size() == nums.length) { + Alllist.add(new ArrayList(list)); + return; + } + + for (int i = 0;i < nums.length;i ++) { + if(i - 1 >= 0 && nums[i] == nums[i - 1] && visit[i - 1] == 0) { + continue; + } + if (visit[i] == 0) { + visit[i] = 1; + list.add(nums[i]); + dfs(list, nums, visit); + visit[i] = 0; + list.remove(list.size() - 1); + } + } + } + } + +77. 组合 + + +给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。 + +示例: + +输入: n = 4, k = 2 +输出: +[ + [2,4], + [3,4], + [2,3], + [1,2], + [1,3], + [1,4], +] + +解析:组合长度固定为k,不能有重复组合,所以我们规定顺序是从小到大进行组合,这样的话就不会有重复情况出现了。 + + class Solution { + static List> Alllist = new ArrayList<>(); + public List> combine(int n, int k) { + Alllist.clear(); + List list = new ArrayList<>(); + dfs(list, n, k, 1); + return Alllist; + } + public void dfs(List list, int n, int k, int cur) { + if (list.size() == k) { + Alllist.add(new ArrayList(list)); + return; + } + for (int i = cur;i <= n;i ++) { + list.add(i); + dfs(list, n, k, i + 1); + list.remove(list.size() - 1); + } + } + } + +### 39. 组合总和 + +> 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 +> +> candidates 中的数字可以无限制重复被选取。 +> +> 说明: +> +> 所有数字(包括 target)都是正整数。 +> 解集不能包含重复的组合。 +> 示例 1: +> +> 输入: candidates = [2,3,6,7], target = 7, +> 所求解集为: +> [ +> [7], +> [2,2,3] +> ] +> 示例 2: +> +> 输入: candidates = [2,3,5], target = 8, +> 所求解集为: +> [ +> [2,2,2,2], +> [2,3,3], +> [3,5] +> ] + +解析:由于组合中没有重复数字,并且每个数可以出现多次,所以我们依然可以按顺序进行组合,每次i取自己或比自己更大的数组下标即可。 + + class Solution { + static List> Alllist = new ArrayList<>(); + public List> combinationSum(int[] candidates, int target) { + Alllist.clear(); + List list = new ArrayList<>(); + Arrays.sort(candidates); + dfs(list, candidates, 0, target, 0); + return Alllist; + } + public void dfs(List list, int [] candidates, int sum, int target, int cur) { + if (sum == target) { + Alllist.add(new ArrayList(list)); + return; + } + for (int i = cur;i < candidates.length;i ++) { + if (sum + candidates[i] <= target) { + list.add(candidates[i]); + dfs(list, candidates, sum + candidates[i], target, i); + list.remove(list.size() - 1); + } + } + } + + +### 40. 组合总和 II + +> 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 +> +> candidates 中的每个数字在每个组合中只能使用一次。 +> +> 说明: +> +> 所有数字(包括目标数)都是正整数。 +> 解集不能包含重复的组合。 +> 示例 1: +> +> 输入: candidates = [10,1,2,7,6,1,5], target = 8, +> 所求解集为: +> [ +> [1, 7], +> [1, 2, 5], +> [2, 6], +> [1, 1, 6] +> ] +> 示例 2: +> +> 输入: candidates = [2,5,2,1,2], target = 5, +> 所求解集为: +> [ +> [1,2,2], +> [5] +> ] + +解析:本题类似排序的第二题,组合求和,但是组合不能重复,而数组中允许重复数字。 + +所以我们依然要过滤掉同一位置上的重复数字。 + + class Solution { + static List> Alllist = new ArrayList<>(); + public List> combinationSum2(int[] candidates, int target) { + Alllist.clear(); + List list = new ArrayList<>(); + int []visit = new int[candidates.length]; + Arrays.sort(candidates); + dfs(list, candidates, 0, target, 0, visit); + return Alllist; + } + public void dfs(List list, int [] candidates, int sum, int target, int cur, int[] visit) { + if (sum == target) { + Alllist.add(new ArrayList(list)); + return; + } + for (int i = cur;i < candidates.length;i ++) { + if (i - 1 >= 0 && candidates[i] == candidates[i - 1] && visit[i - 1] == 0) { + continue; + } + if (sum + candidates[i] <= target) { + visit[i] = 1; + list.add(candidates[i]); + dfs(list, candidates, sum + candidates[i], target, i + 1, visit); + list.remove(list.size() - 1); + visit[i] = 0; + } + } + } + } +### 216. 组合总和 III + +> 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。 +> +> 说明: +> +> 所有数字都是正整数。 +> 解集不能包含重复的组合。 +> 示例 1: +> +> 输入: k = 3, n = 7 +> 输出: [[1,2,4]] +> 示例 2: +> +> 输入: k = 3, n = 9 +> 输出: [[1,2,6], [1,3,5], [2,3,4]] + +解析:与前面类似,没啥难度 + + class Solution { + public List> combinationSum3(int k, int n) { + Alllist.clear(); + List list = new ArrayList<>(); + dfs(list, 0, n, k, 1); + return Alllist; + } + static List> Alllist = new ArrayList<>(); + public void dfs(List list, int sum, int n, int k, int cur) { + if (sum == n && list.size() == k) { + Alllist.add(new ArrayList(list)); + return; + } + for (int i = cur;i <= 9;i ++) { + if (sum + i <= n && list.size() < k) { + list.add(i); + dfs(list, sum + i, n, k, i + 1); + list.remove(list.size() - 1); + } + } + } + } +### 78. 子集 + +> 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 +> +> 说明:解集不能包含重复的子集。 +> +> 示例: +> +> 输入: nums = [1,2,3] +> 输出: +> [ +> [3], +> [1], +> [2], +> [1,2,3], +> [1,3], +> [2,3], +> [1,2], +> [] +> ] + +解析:本题要注意的是,数字不重复,所以我们可以按顺序来查找子集,并且递归结束条件是当cur = nums.length。时结束。 + + + class Solution { + List> Alllist = new ArrayList<>(); + public List> subsets(int[] nums) { + List list = new ArrayList<>(); + dfs(nums, 0, list); + return Alllist; + } + + public void dfs(int []nums, int cur, List list) { + if (cur <= nums.length) { + Alllist.add(new ArrayList(list)); + } else { + return; + } + for (int i = cur;i < nums.length;i ++) { + list.add(nums[i]); + dfs(nums, i + 1, list); + list.remove(list.size() - 1); + } + } + } + +### 90. 子集 II + +> 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 +> +> 说明:解集不能包含重复的子集。 +> +> 示例: +> +> 输入: [1,2,2] +> 输出: +> [ +> [2], +> [1], +> [1,2,2], +> [2,2], +> [1,2], +> [] +> ] + + class Solution { + List> Alllist = new ArrayList<>(); + public List> subsetsWithDup(int[] nums) { + List list = new ArrayList<>(); + Arrays.sort(nums); + int []visit = new int[nums.length]; + dfs(nums, 0, visit, list); + return Alllist; + } + public void dfs(int []nums, int cur, int []visit, List list) { + if (cur <= nums.length) { + Alllist.add(new ArrayList(list)); + } else { + return; + } + for (int i = cur;i < nums.length;i ++) { + if (i > 0 && visit[i - 1] == 0 && nums[i] == nums[i - 1]) { + continue; + } + if (visit[i] == 0) { + visit[i] = 1; + list.add(nums[i]); + dfs(nums, i + 1,visit, list); + list.remove(list.size() - 1); + visit[i] = 0; + } + } + } + } + +### 131. 分割回文串 + +> 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。 +> +> 返回 s 所有可能的分割方案。 +> +> 示例: +> +> 输入: "aab" +> 输出: +> [ +> ["aa","b"], +> ["a","a","b"] +> ] + +解析: +这题其实也类似,只不过比较巧妙一点。 + +1 要把字符串分割成回文字符串组合,那么我们要存的其实还是所有的字符串,所以返回条件还是当cur = s.length()。 + +2 字符串分割,首先在循环中遍历满足回文的第一个子串,在此基础上找下一个回文子串,达到了剪枝的目的。 + + class Solution { + List> allList = new ArrayList<>(); + public List> partition(String s) { + List list = new ArrayList<>(); + dfs(s, 0, list); + return allList; + } + public void dfs(String s, int cur, List list) { + if (cur == s.length()) { + allList.add(new ArrayList(list)); + return; + } + for (int i = cur;i < s.length();i ++) { + String str = s.substring(cur, i + 1); + if (legal(str)) { + list.add(str); + dfs(s, i + 1, list); + list.remove(list.size() - 1); + } + } + } + public boolean legal(String s) { + int i = 0,j = s.length() - 1; + while (i < j) { + if (s.charAt(i) == s.charAt(j)) { + i ++; + j --; + }else { + return false; + } + } + return true; + } + } +## 动态规划 + +动态规划 +递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。 + +### 第一大类:斐波那契数列型,选择n - 1或n - 2的一种 +爬楼梯 + +### 70. Climbing Stairs (Easy) + +> 题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 +> +> 定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 +> +> + +dp[N] 即为所求。 + +考虑到 dp[i] 只与 dp[i - 1] 和 dp[i - 2] 有关,因此可以只用两个变量来存储 dp[i - 1] 和 dp[i - 2],使得原来的 O(N) 空间复杂度优化为 O(1) 复杂度。 + + public int climbStairs(int n) { + if (n <= 2) { + return n; + } + int pre2 = 1, pre1 = 2; + for (int i = 2; i < n; i++) { + int cur = pre1 + pre2; + pre2 = pre1; + pre1 = cur; + } + return pre1; + } + +### 198. 打家劫舍 + +> +你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 +> +> 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 +> +> 示例 1: +> +> 输入: [1,2,3,1] +> 输出: 4 +> 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 +> 偷窃到的最高金额 = 1 + 3 = 4 。 + +解析:注意抢劫的时候,对于 [1,2,3,4],要么偷1,要么投2。 +但是对于4来说,前面的偷法可以分为两种,要么偷1不偷2和3,要么偷2不偷3。 + + +举个例子[100,2,3,4]偷4的时候,我们应该放弃2和3,直接偷100。同时我们不用再往后判断了,因为前面也考虑到了这种情况。 + + public static int rob(int[] nums) { + if (nums == null || nums.length == 0) return 0; + if (nums.length == 1)return nums[0]; + if (nums.length == 2)return nums[0] > nums[1] ? nums[0] : nums[1]; + int []dp = new int[nums.length + 1]; + //dp代表最右只抢到第n家时的总钱数。 + dp[1] = nums[0]; + dp[2] = nums[0] > nums[1] ? nums[0] : nums[1]; + for (int i = 3;i <= nums.length;i ++) { + dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); + } + return dp[nums.length]; + } + +### 213. 打家劫舍 II + +> +> 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 +> +> 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 +> +> 示例 1: +> +> 输入: [2,3,2] +> 输出: 3 +> 解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。 +> 示例 2: +> +> 输入: [1,2,3,1] +> 输出: 4 +> 解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 +> 偷窃到的最高金额 = 1 + 3 = 4 。 + +解析:环形区域主要考虑两种情况,第一家要抢的话,最后一家一定不能抢,第一家不抢的话,最后一家可以抢也可以不抢。然后根据上一题的做法使用dp即可。 + + + public static int rob(int[] nums) { + + if (nums == null || nums.length == 0) return 0; + if (nums.length == 1)return nums[0]; + if (nums.length == 2)return nums[0] > nums[1] ? nums[0] : nums[1]; + int []dp = new int[nums.length + 1]; + //dp代表最右只抢到第n家时的总钱数。 + //如果抢了第一家 + dp[1] = nums[0]; + dp[2] = nums[0] > nums[1] ? nums[0] : nums[1]; + for (int i = 3;i < nums.length;i ++) { + dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); + } + int max = dp[nums.length - 1]; + //如果不抢第一家 + dp[1] = 0; + dp[2] = nums[1]; + for (int i = 3;i <= nums.length;i ++) { + dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]); + } + + if (dp[nums.length] > max)max = dp[nums.length]; + return max; + } +> +> +> 母牛生产 +> +> 程序员代码面试指南-P181 +> +> 题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。 +> +> 第 i 年成熟的牛的数量为: + + + +> 信件错排 +> +> 题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。 +> +> 定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: +> +> i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-2] 种错误装信方式。 +> i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-1] 种错误装信方式。 +> 综上所述,错误装信数量方式数量为: +> +> +> +> dp[N] 即为所求。 + +### 第二大类:矩阵路径 +求矩阵路径和,走法总数或者最短距离等 + +### 64. 最小路径和 + + +> 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 +> +> 说明:每次只能向下或者向右移动一步。 +> +> 示例: +> +> 输入: +> [ +> [1,3,1], +> [1,5,1], +> [4,2,1] +> ] +> 输出: 7 +> 解释: 因为路径 1→3→1→1→1 的总和最小。 + + class Solution { + public int minPathSum(int[][] grid) { + int [][]dp = new int[grid.length][grid[0].length]; + dp[0][0] = grid[0][0]; + for (int i = 1;i < grid.length;i ++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + for (int i = 1;i < grid[0].length;i ++) { + dp[0][i] = dp[0][i - 1] + grid[0][i]; + } + for (int i = 1;i < grid.length;i ++) { + for (int j = 1;j < grid[0].length;j ++) { + dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; + } + } + return dp[grid.length - 1][grid[0].length - 1]; + } + } + +### 62. 不同路径 + +> 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 +> +> 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 +> +> 问总共有多少条不同的路径? +> +> +> +> 例如,上图是一个7 x 3 的网格。有多少可能的路径? +> +> 说明:m 和 n 的值均不超过 100。 +> +> 示例 1: +> +> 输入: m = 3, n = 2 +> 输出: 3 +> 解释: +> 从左上角开始,总共有 3 条路径可以到达右下角。 +> 1. 向右 -> 向右 -> 向下 +> 2. 向右 -> 向下 -> 向右 +> 3. 向下 -> 向右 -> 向右 +> 示例 2: +> +> 输入: m = 7, n = 3 +> 输出: 28 + + class Solution { + public int uniquePaths(int m, int n) { + int [][]dp = new int[m][n]; + for (int i = 0; i < m;i ++) { + dp[i][0] = 1; + } + for (int i = 0; i < n;i ++) { + dp[0][i] = 1; + } + for (int i = 1;i < m;i ++) { + for (int j = 1;j < n;j ++) { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[m - 1][n - 1]; + } + } + +### 第三大类:数组区间 + +### 53. 最大子序和 + +> 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 +> +> 示例: +> +> 输入: [-2,1,-3,4,-1,2,1,-5,4], +> 输出: 6 +> 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 +> 进阶: +> +> 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。 +> +> 解析:设置数组dp表示结尾为第i个数的最大子数组和是多少。然后方程是dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]) +> +> 最后遍历一下dp数组找到最大子序和即可, + + class Solution { + public int maxSubArray(int[] nums) { + if (nums.length == 0) { + return 0; + } + if (nums.length == 1) { + return nums[0]; + } + int []dp = new int[nums.length]; + dp[0] = nums[0]; + for (int i = 1;i < nums.length;i ++) { + dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); + } + int max = nums[0]; + for (int i = 0;i < dp.length;i ++) { + max = Math.max(max, dp[i]); + } + return max; + } + } + +### 第四类:分割整数 + +### 343. 整数拆分 + + + +> 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 +> +> 例如,给定 n = 2,返回1(2 = 1 + 1);给定 n = 10,返回36(10 = 3 + 3 + 4)。 +> +> 注意:你可以假设 n 不小于2且不大于58。 + +参考答案: + +解析:对于一个数来说,拆分包括两种情况,一种是把i拆成j和i - j,另一种是i拆成j与i - j这个数所能构成的组合。 +所以方程就是dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); + +由于dp代表的是数字n能够得到的最大乘积,为了满足上面这个式子,dp[2] = dp[1] * 1,所以dp[1] = 1; + + public int integerBreak(int n) { + int[] dp = new int[n + 1]; + dp[1] = 1; + for (int i = 2; i <= n; i++) { + for (int j = 1; j <= i - 1; j++) { + dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); + } + } + return dp[n]; + } + +279. 完全平方数 + + +> 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 +> +> 示例 1: +> +> 输入: n = 12 +> 输出: 3 +> 解释: 12 = 4 + 4 + 4. +> 示例 2: +> +> 输入: n = 13 +> 输出: 2 +> 解释: 13 = 4 + 9. + +解析:恕在下直言,有些DP的题目也是神仙题,确实不容易想到。dp代表的和为n的完全平方数所需的最少个数。 + +本题还可以用bfs来做,详细可参见上面的bfs部分。 + +dp[1] = 1。所以对于每个i,我们都要遍历所有小于等于它的平方数,以便找到所需个数最少的个数。 + +方程写作:对于每个数i有一个最小值dp[i] +min = Math.min(min, dp[i - square] + 1); +最后dp[n]即为所求。 + +非常巧妙,值得学习和记忆。 + + public int numSquares(int n) { + List squareList = generateSquareList(n); + int[] dp = new int[n + 1]; + for (int i = 1; i <= n; i++) { + int min = Integer.MAX_VALUE; + for (int square : squareList) { + if (square > i) { + break; + } + min = Math.min(min, dp[i - square] + 1); + } + dp[i] = min; + } + return dp[n]; + } + + private List generateSquareList(int n) { + List squareList = new ArrayList<>(); + int diff = 3; + int square = 1; + while (square <= n) { + squareList.add(square); + square += diff; + diff += 2; + } + return squareList; + } + +### 第五类:最长递增子序列 + +> 最长递增子序列 +> 已知一个序列 {S1, S2,...,Sn} ,取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 子序列 。 +> +> 如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 递增子序列 。 +> +> 定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn ,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。 +> +> 因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即: + +解析: +dp数组代表以第i个元素作为序列结尾时的最大递增序列,由于之前的序列是可选的,所以我们遍历之前所有满足情况的点(也就是满足nums[i] > nums[j]点),找到前面最长的递增序列即可。 + +转移方程是 +dp[i] = Math.max(dp[j] + 1, dp[i]); + + class Solution { + public int lengthOfLIS(int[] nums) { + if (nums == null || nums.length == 0)return 0; + if (nums.length == 1)return 1; + int []dp = new int[nums.length]; + dp[0] = 1; + + for (int i = 1;i < nums.length;i ++) { + for (int j = 0;j < i;j ++) { + if (nums[i] > nums[j]) { + dp[i] = Math.max(dp[j] + 1, dp[i]); + }else { + dp[i] = Math.max(dp[i], 1); + } + } + } + + int max = 0; + for (int i = 0;i < dp.length;i ++) { + max = Math.max(max, dp[i]); + } + return max; + } + } + +### 第六类:最长公共子序列 + +> 最长公共子序列 +> 对于两个子序列 S1 和 S2,找出它们最长的公共子序列。 +> +> 定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况: +> +> 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。 +> 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,与 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。 +> 综上,最长公共子序列的状态转移方程为: + + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + +> 对于长度为 N 的序列 S1 和 长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。 +> +> 与最长递增子序列相比,最长公共子序列有以下不同点: +> +> 针对的是两个序列,求它们的最长公共子序列。 +> 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。 +> 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。 + + + public int lengthOfLCS(int[] nums1, int[] nums2) { + int n1 = nums1.length, n2 = nums2.length; + int[][] dp = new int[n1 + 1][n2 + 1]; + for (int i = 1; i <= n1; i++) { + for (int j = 1; j <= n2; j++) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[n1][n2]; + } + +### 第七类:01背包 + +> 0-1 背包 +> 有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。 +> +> 定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论: +> +> 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。 +> 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。 +> 第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。 +> +> 综上,0-1 背包的状态转移方程为: +> +> + + public int knapsack(int W, int N, int[] weights, int[] values) { + int[][] dp = new int[N + 1][W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = 1; j <= W; j++) { + if (j >= w) { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); + } else { + dp[i][j] = dp[i - 1][j]; + } + } + } + return dp[N][W]; + } + +在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅由前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时, + + + +因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。 + +public int knapsack(int W, int N, int[] weights, int[] values) { + int[] dp = new int[W + 1]; + for (int i = 1; i <= N; i++) { + int w = weights[i - 1], v = values[i - 1]; + for (int j = W; j >= 1; j--) { + if (j >= w) { + dp[j] = Math.max(dp[j], dp[j - w] + v); + } + } + } + return dp[W]; +} +无法使用贪心算法的解释 + +0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。考虑下面的物品和一个容量为 5 的背包,如果先添加物品 0 再添加物品 1,那么只能存放的价值为 16,浪费了大小为 2 的空间。最优的方式是存放物品 1 和物品 2,价值为 22. + +id w v v/w +0 1 6 6 +1 2 10 5 +2 3 12 4 +变种 + +完全背包:物品数量为无限个 + +多重背包:物品数量有限制 + +多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制 + +其它:物品之间相互约束或者依赖 + +划分数组为和相等的两部分 + +416. Partition Equal Subset Sum (Medium) + +> Input: [1, 5, 11, 5] +> +> Output: true +> +> Explanation: The array can be partitioned as [1, 5, 5] and [11]. +> 可以看成一个背包大小为 sum/2 的 0-1 背包问题。 + + + public boolean canPartition(int[] nums) { + int sum = computeArraySum(nums); + if (sum % 2 != 0) { + return false; + } + int W = sum / 2; + boolean[] dp = new boolean[W + 1]; + dp[0] = true; + Arrays.sort(nums); + for (int num : nums) { // 0-1 背包一个物品只能用一次 + for (int i = W; i >= num; i--) { // 从后往前,先计算 dp[i] 再计算 dp[i-num] + dp[i] = dp[i] || dp[i - num]; + } + } + return dp[W]; + } + + private int computeArraySum(int[] nums) { + int sum = 0; + for (int num : nums) { + sum += num; + } + return sum; + } + +找零钱的方法数 + +322. Coin Change (Medium) + +Example 1: +coins = [1, 2, 5], amount = 11 +return 3 (11 = 5 + 5 + 1) + +Example 2: +coins = [2], amount = 3 +return -1. +题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。 + +物品:硬币 +物品大小:面额 +物品价值:数量 +因为硬币可以重复使用,因此这是一个完全背包问题。 + + public int coinChange(int[] coins, int amount) { + if (coins == null || coins.length == 0) { + return 0; + } + int[] minimum = new int[amount + 1]; + Arrays.fill(minimum, amount + 1); + minimum[0] = 0; + Arrays.sort(coins); + for (int i = 1; i <= amount; i++) { + for (int j = 0; j < coins.length && coins[j] <= i; j++) { + minimum[i] = Math.min(minimum[i], minimum[i - coins[j]] + 1); + } + } + return minimum[amount] > amount ? -1 : minimum[amount]; + } + +### 第八类:股票买卖 + +只能进行 k 次的股票交易 + +188. Best Time to Buy and Sell Stock IV (Hard) + + public int maxProfit(int k, int[] prices) { + int n = prices.length; + if (k >= n / 2) { // 这种情况下该问题退化为普通的股票交易问题 + int maxProfit = 0; + for (int i = 1; i < n; i++) { + if (prices[i] > prices[i - 1]) { + maxProfit += prices[i] - prices[i - 1]; + } + } + return maxProfit; + } + int[][] maxProfit = new int[k + 1][n]; + for (int i = 1; i <= k; i++) { + int localMax = maxProfit[i - 1][0] - prices[0]; + for (int j = 1; j < n; j++) { + maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax); + localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]); + } + } + return maxProfit[k][n - 1]; + } + +只能进行两次的股票交易 + +123. Best Time to Buy and Sell Stock III (Hard) + + 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; + } + + +需要冷却期的股票交易 + +309. Best Time to Buy and Sell Stock with Cooldown(Medium) + +题目描述:交易之后需要有一天的冷却时间。 + + + + public int maxProfit(int[] prices) { + if (prices == null || prices.length == 0) { + return 0; + } + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = s2[i - 1] - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); + } + return Math.max(sell[N - 1], s2[N - 1]); + } + +需要交易费用的股票交易 + +714. Best Time to Buy and Sell Stock with Transaction Fee (Medium) + +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. +题目描述:每交易一次,都要支付一定的费用。 + + + + public int maxProfit(int[] prices, int fee) { + int N = prices.length; + int[] buy = new int[N]; + int[] s1 = new int[N]; + int[] sell = new int[N]; + int[] s2 = new int[N]; + s1[0] = buy[0] = -prices[0]; + sell[0] = s2[0] = 0; + for (int i = 1; i < N; i++) { + buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; + s1[i] = Math.max(buy[i - 1], s1[i - 1]); + sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; + s2[i] = Math.max(s2[i - 1], sell[i - 1]); + } + return Math.max(sell[N - 1], s2[N - 1]); + } + +买入和售出股票最大的收益 + +121. Best Time to Buy and Sell Stock (Easy) + +题目描述:只进行一次交易。 + +只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 + + public int maxProfit(int[] prices) { + int n = prices.length; + if (n == 0) return 0; + int soFarMin = prices[0]; + int max = 0; + for (int i = 1; i < n; i++) { + if (soFarMin > prices[i]) soFarMin = prices[i]; + else max = Math.max(max, prices[i] - soFarMin); + } + return max; + } +### 第九类:字符串编辑 + +删除两个字符串的字符使它们相等 + +583. Delete Operation for Two Strings (Medium) + +Input: "sea", "eat" +Output: 2 +Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea". +可以转换为求两个字符串的最长公共子序列问题。 + + public int minDistance(String word1, String word2) { + int m = word1.length(), n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + for (int i = 0; i <= m; i++) { + for (int j = 0; j <= n; j++) { + if (i == 0 || j == 0) { + continue; + } + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + return m + n - 2 * dp[m][n]; + } + +复制粘贴字符 + +650. 2 Keys Keyboard (Medium) + +题目描述:最开始只有一个字符 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'. + + public int minSteps(int n) { + if (n == 1) return 0; + for (int i = 2; i <= Math.sqrt(n); i++) { + if (n % i == 0) return i + minSteps(n / i); + } + return n; + } + public int minSteps(int n) { + int[] dp = new int[n + 1]; + int h = (int) Math.sqrt(n); + for (int i = 2; i <= n; i++) { + dp[i] = i; + for (int j = 2; j <= h; j++) { + if (i % j == 0) { + dp[i] = dp[j] + dp[i / j]; + break; + } + } + } + return dp[n]; + } + + +## 位运算 + +## 数学 + +数学 +素数 +素数分解 + +每一个数都可以分解成素数的乘积,例如 84 = 22 * 31 * 50 * 71 * 110 * 130 * 170 * … + +整除 + +令 x = 2m0 * 3m1 * 5m2 * 7m3 * 11m4 * … + +令 y = 2n0 * 3n1 * 5n2 * 7n3 * 11n4 * … + +如果 x 整除 y(y mod x == 0),则对于所有 i,mi <= ni。 + +最大公约数最小公倍数 + +x 和 y 的最大公约数为:gcd(x,y) = 2min(m0,n0) * 3min(m1,n1) * 5min(m2,n2) * ... + +x 和 y 的最小公倍数为:lcm(x,y) = 2max(m0,n0) * 3max(m1,n1) * 5max(m2,n2) * ... + +## 分治 +241. 为运算表达式设计优先级 + +> 给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。 +> +> 示例 1: +> +> 输入: "2-1-1" +> 输出: [0, 2] +> 解释: +> ((2-1)-1) = 0 +> (2-(1-1)) = 2 +> 示例 2: +> +> 输入: "2*3-4*5" +> 输出: [-34, -14, -10, -10, 10] +> 解释: +> (2*(3-(4*5))) = -34 +> ((2*3)-(4*5)) = -14 +> ((2*(3-4))*5) = -10 +> (2*((3-4)*5)) = -10 +> (((2*3)-4)*5) = 10 + +### Trie + +[![](https://github.com/CyC2018/Interview-Notebook/raw/master/pics/5c638d59-d4ae-4ba4-ad44-80bdc30f38dd.jpg)](https://github.com/CyC2018/Interview-Notebook/blob/master/pics/5c638d59-d4ae-4ba4-ad44-80bdc30f38dd.jpg) + +Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。 + +**实现一个 Trie** + +[208\. Implement Trie (Prefix Tree) (Medium)](https://leetcode.com/problems/implement-trie-prefix-tree/description/) + + + +
class Trie {
+
+    private class Node {
+        Node[] childs = new Node[26];
+        boolean isLeaf;
+    }
+
+    private Node root = new Node();
+
+    public Trie() {
+    }
+
+    public void insert(String word) {
+        insert(word, root);
+    }
+
+    private void insert(String word, Node node) {
+        if (node == null) return;
+        if (word.length() == 0) {
+            node.isLeaf = true;
+            return;
+        }
+        int index = indexForChar(word.charAt(0));
+        if (node.childs[index] == null) {
+            node.childs[index] = new Node();
+        }
+        insert(word.substring(1), node.childs[index]);
+    }
+
+    public boolean search(String word) {
+        return search(word, root);
+    }
+
+    private boolean search(String word, Node node) {
+        if (node == null) return false;
+        if (word.length() == 0) return node.isLeaf;
+        int index = indexForChar(word.charAt(0));
+        return search(word.substring(1), node.childs[index]);
+    }
+
+    public boolean startsWith(String prefix) {
+        return startWith(prefix, root);
+    }
+
+    private boolean startWith(String prefix, Node node) {
+        if (node == null) return false;
+        if (prefix.length() == 0) return true;
+        int index = indexForChar(prefix.charAt(0));
+        return startWith(prefix.substring(1), node.childs[index]);
+    }
+
+    private int indexForChar(char c) {
+        return c - 'a';
+    }
+}
+ + + +**实现一个 Trie,用来求前缀和** + +[677\. Map Sum Pairs (Medium)](https://leetcode.com/problems/map-sum-pairs/description/) + + + +
Input: insert("apple", 3), Output: Null
+Input: sum("ap"), Output: 3
+Input: insert("app", 2), Output: Null
+Input: sum("ap"), Output: 5
+ + + + + +
class MapSum {
+
+    private class Node {
+        Node[] child = new Node[26];
+        int value;
+    }
+
+    private Node root = new Node();
+
+    public MapSum() {
+
+    }
+
+    public void insert(String key, int val) {
+        insert(key, root, val);
+    }
+
+    private void insert(String key, Node node, int val) {
+        if (node == null) return;
+        if (key.length() == 0) {
+            node.value = val;
+            return;
+        }
+        int index = indexForChar(key.charAt(0));
+        if (node.child[index] == null) {
+            node.child[index] = new Node();
+        }
+        insert(key.substring(1), node.child[index], val);
+    }
+
+    public int sum(String prefix) {
+        return sum(prefix, root);
+    }
+
+    private int sum(String prefix, Node node) {
+        if (node == null) return 0;
+        if (prefix.length() != 0) {
+            int index = indexForChar(prefix.charAt(0));
+            return sum(prefix.substring(1), node.child[index]);
+        }
+        int sum = node.value;
+        for (Node child : node.child) {
+            sum += sum(prefix, child);
+        }
+        return sum;
+    }
+
+    private int indexForChar(char c) {
+        return c - 'a';
+    }
+}
+ + + +## [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E5%9B%BE)图 + +### [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E4%BA%8C%E5%88%86%E5%9B%BE)二分图 + +如果可以用两种颜色对图中的节点进行着色,并且保证相邻的节点颜色不同,那么这个图就是二分图。 + +**判断是否为二分图** + +[785\. Is Graph Bipartite? (Medium)](https://leetcode.com/problems/is-graph-bipartite/description/) + + + +
Input: [[1,3], [0,2], [1,3], [0,2]]
+Output: true
+Explanation:
+The graph looks like this:
+0----1
+|    |
+|    |
+3----2
+We can divide the vertices into two groups: {0, 2} and {1, 3}.
+ + + + + +
Example 2:
+Input: [[1,2,3], [0,2], [0,1,3], [0,2]]
+Output: false
+Explanation:
+The graph looks like this:
+0----1
+| \  |
+|  \ |
+3----2
+We cannot find a way to divide the set of nodes into two independent subsets.
+ + + + + +
public boolean isBipartite(int[][] graph) {
+    int[] colors = new int[graph.length];
+    Arrays.fill(colors, -1);
+    for (int i = 0; i < graph.length; i++) {  // 处理图不是连通的情况
+        if (colors[i] == -1 && !isBipartite(i, 0, colors, graph)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+private boolean isBipartite(int curNode, int curColor, int[] colors, int[][] graph) {
+    if (colors[curNode] != -1) {
+        return colors[curNode] == curColor;
+    }
+    colors[curNode] = curColor;
+    for (int nextNode : graph[curNode]) {
+        if (!isBipartite(nextNode, 1 - curColor, colors, graph)) {
+            return false;
+        }
+    }
+    return true;
+}
+ + + +### [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F)拓扑排序 + +常用于在具有先序关系的任务规划中。 + +**课程安排的合法性** + +[207\. Course Schedule (Medium)](https://leetcode.com/problems/course-schedule/description/) + + + +
2, [[1,0]]
+return true
+ + + + + +
2, [[1,0],[0,1]]
+return false
+ + + +题目描述:一个课程可能会先修课程,判断给定的先修课程规定是否合法。 + +本题不需要使用拓扑排序,只需要检测有向图是否存在环即可。 + + + +
public boolean canFinish(int numCourses, int[][] prerequisites) {
+    List[] graphic = new List[numCourses];
+    for (int i = 0; i < numCourses; i++) {
+        graphic[i] = new ArrayList<>();
+    }
+    for (int[] pre : prerequisites) {
+        graphic[pre[0]].add(pre[1]);
+    }
+    boolean[] globalMarked = new boolean[numCourses];
+    boolean[] localMarked = new boolean[numCourses];
+    for (int i = 0; i < numCourses; i++) {
+        if (hasCycle(globalMarked, localMarked, graphic, i)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked,
+                         List[] graphic, int curNode) {
+
+    if (localMarked[curNode]) {
+        return true;
+    }
+    if (globalMarked[curNode]) {
+        return false;
+    }
+    globalMarked[curNode] = true;
+    localMarked[curNode] = true;
+    for (int nextNode : graphic[curNode]) {
+        if (hasCycle(globalMarked, localMarked, graphic, nextNode)) {
+            return true;
+        }
+    }
+    localMarked[curNode] = false;
+    return false;
+}
+ + + +**课程安排的顺序** + +[210\. Course Schedule II (Medium)](https://leetcode.com/problems/course-schedule-ii/description/) + + + +
4, [[1,0],[2,0],[3,1],[3,2]]
+There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2\. Both courses 1 and 2 should be taken after you finished course 0\. So one correct course order is [0,1,2,3]. Another correct ordering is[0,2,1,3].
+ + + +使用 DFS 来实现拓扑排序,使用一个栈存储后序遍历结果,这个栈的逆序结果就是拓扑排序结果。 + +证明:对于任何先序关系:v->w,后序遍历结果可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。 + + + +
public int[] findOrder(int numCourses, int[][] prerequisites) {
+    List[] graphic = new List[numCourses];
+    for (int i = 0; i < numCourses; i++) {
+        graphic[i] = new ArrayList<>();
+    }
+    for (int[] pre : prerequisites) {
+        graphic[pre[0]].add(pre[1]);
+    }
+    Stack postOrder = new Stack<>();
+    boolean[] globalMarked = new boolean[numCourses];
+    boolean[] localMarked = new boolean[numCourses];
+    for (int i = 0; i < numCourses; i++) {
+        if (hasCycle(globalMarked, localMarked, graphic, i, postOrder)) {
+            return new int[0];
+        }
+    }
+    int[] orders = new int[numCourses];
+    for (int i = numCourses - 1; i >= 0; i--) {
+        orders[i] = postOrder.pop();
+    }
+    return orders;
+}
+
+private boolean hasCycle(boolean[] globalMarked, boolean[] localMarked, List[] graphic,
+                         int curNode, Stack postOrder) {
+
+    if (localMarked[curNode]) {
+        return true;
+    }
+    if (globalMarked[curNode]) {
+        return false;
+    }
+    globalMarked[curNode] = true;
+    localMarked[curNode] = true;
+    for (int nextNode : graphic[curNode]) {
+        if (hasCycle(globalMarked, localMarked, graphic, nextNode, postOrder)) {
+            return true;
+        }
+    }
+    localMarked[curNode] = false;
+    postOrder.push(curNode);
+    return false;
+}
+ + + +### [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E5%B9%B6%E6%9F%A5%E9%9B%86)并查集 + +并查集可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。 + +**冗余连接** + +[684\. Redundant Connection (Medium)](https://leetcode.com/problems/redundant-connection/description/) + + + +
Input: [[1,2], [1,3], [2,3]]
+Output: [2,3]
+Explanation: The given undirected graph will be like this:
+  1
+ / \
+2 - 3
+ + + +题目描述:有一系列的边连成的图,找出一条边,移除它之后该图能够成为一棵树。 + + + +
public int[] findRedundantConnection(int[][] edges) {
+    int N = edges.length;
+    UF uf = new UF(N);
+    for (int[] e : edges) {
+        int u = e[0], v = e[1];
+        if (uf.connect(u, v)) {
+            return e;
+        }
+        uf.union(u, v);
+    }
+    return new int[]{-1, -1};
+}
+
+private class UF {
+    private int[] id;
+
+    UF(int N) {
+        id = new int[N + 1];
+        for (int i = 0; i < id.length; i++) {
+            id[i] = i;
+        }
+    }
+
+    void union(int u, int v) {
+        int uID = find(u);
+        int vID = find(v);
+        if (uID == vID) {
+            return;
+        }
+        for (int i = 0; i < id.length; i++) {
+            if (id[i] == uID) {
+                id[i] = vID;
+            }
+        }
+    }
+
+    int find(int p) {
+        return id[p];
+    }
+
+    boolean connect(int u, int v) {
+        return find(u) == find(v);
+    }
+}
+ + + +## [](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#%E4%BD%8D%E8%BF%90%E7%AE%97)位运算 + +**1\. 基本原理** + +0s 表示一串 0,1s 表示一串 1。 + +``` +x ^ 0s = x x & 0s = 0 x | 0s = x +x ^ 1s = ~x x & 1s = x x | 1s = 1s +x ^ x = 0 x & x = x x | x = x + +``` + +* 利用 x ^ 1s = ~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。 +* 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask :00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。 +* 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask:00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。 + +位与运算技巧: + +* n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 **100** ,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。 +* n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1,对于二进制表示 10110 **100** ,-n 得到 01001**100**,相与得到 00000**100**。 +* n-n&(~n+1) 去除 n 的位级表示中最高的那一位。 + +移位运算: + +* >> n 为算术右移,相当于除以 2n; +* >>> n 为无符号右移,左边会补上 0。 +* << n 为算术左移,相当于乘以 2n。 + +**2\. mask 计算** + +要获取 111111111,将 0 取反即可,~0。 + +要得到只有第 i 位为 1 的 mask,将 1 向左移动 i-1 位即可,1<<(i-1) 。例如 1<<4 得到只有第 5 位为 1 的 mask :00010000。 + +要得到 1 到 i 位为 1 的 mask,1<<(i+1)-1 即可,例如将 1<<(4+1)-1 = 00010000-1 = 00001111。 + +要得到 1 到 i 位为 0 的 mask,只需将 1 到 i 位为 1 的 mask 取反,即 ~(1<<(i+1)-1)。 + +**3\. Java 中的位操作** + + + +
static int Integer.bitCount();           // 统计 1 的数量
+static int Integer.highestOneBit();      // 获得最高位
+static String toBinaryString(int i);     // 转换为二进制表示的字符串
+ + + +**统计两个数的二进制表示有多少位不同** + +[461\. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/) + + + +
Input: x = 1, y = 4
+
+Output: 2
+
+Explanation:
+1   (0 0 0 1)
+4   (0 1 0 0)
+       ↑   ↑
+
+The above arrows point to positions where the corresponding bits are different.
+ + + +对两个数进行异或操作,位级表示不同的那一位为 1,统计有多少个 1 即可。 + + + +
public int hammingDistance(int x, int y) {
+    int z = x ^ y;
+    int cnt = 0;
+    while(z != 0) {
+        if ((z & 1) == 1) cnt++;
+        z = z >> 1;
+    }
+    return cnt;
+}
+ + + +使用 z&(z-1) 去除 z 位级表示最低的那一位。 + + + +
public int hammingDistance(int x, int y) {
+    int z = x ^ y;
+    int cnt = 0;
+    while (z != 0) {
+        z &= (z - 1);
+        cnt++;
+    }
+    return cnt;
+}
+ + + +可以使用 Integer.bitcount() 来统计 1 个的个数。 + + + +
public int hammingDistance(int x, int y) {
+    return Integer.bitCount(x ^ y);
+}
+ + + +**数组中唯一一个不重复的元素** + +[136\. Single Number (Easy)](https://leetcode.com/problems/single-number/description/) + + + +
Input: [4,1,2,1,2]
+Output: 4
+ + + +两个相同的数异或的结果为 0,对所有数进行异或操作,最后的结果就是单独出现的那个数。 + + + +
public int singleNumber(int[] nums) {
+    int ret = 0;
+    for (int n : nums) ret = ret ^ n;
+    return ret;
+}
+ + + +**找出数组中缺失的那个数** + +[268\. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/) + + + +
Input: [3,0,1]
+Output: 2
+ + + +题目描述:数组元素在 0-n 之间,但是有一个数是缺失的,要求找到这个缺失的数。 + + + +
public int missingNumber(int[] nums) {
+    int ret = 0;
+    for (int i = 0; i < nums.length; i++) {
+        ret = ret ^ i ^ nums[i];
+    }
+    return ret ^ nums.length;
+}
+ + + +**数组中不重复的两个元素** + +[260\. Single Number III (Medium)](https://leetcode.com/problems/single-number-iii/description/) + +两个不相等的元素在位级表示上必定会有一位存在不同。 + +将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。 + +diff &= -diff 得到出 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位,利用这一位就可以将两个元素区分开来。 + + + +
public int[] singleNumber(int[] nums) {
+    int diff = 0;
+    for (int num : nums) diff ^= num;
+    diff &= -diff;  // 得到最右一位
+    int[] ret = new int[2];
+    for (int num : nums) {
+        if ((num & diff) == 0) ret[0] ^= num;
+        else ret[1] ^= num;
+    }
+    return ret;
+}
+ + + +**翻转一个数的比特位** + +[190\. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/) + + + +
public int reverseBits(int n) {
+    int ret = 0;
+    for (int i = 0; i < 32; i++) {
+        ret <<= 1;
+        ret |= (n & 1);
+        n >>>= 1;
+    }
+    return ret;
+}
+ + + +如果该函数需要被调用很多次,可以将 int 拆成 4 个 byte,然后缓存 byte 对应的比特位翻转,最后再拼接起来。 + + + +
private static Map cache = new HashMap<>();
+
+public int reverseBits(int n) {
+    int ret = 0;
+    for (int i = 0; i < 4; i++) {
+        ret <<= 8;
+        ret |= reverseByte((byte) (n & 0b11111111));
+        n >>= 8;
+    }
+    return ret;
+}
+
+private int reverseByte(byte b) {
+    if (cache.containsKey(b)) return cache.get(b);
+    int ret = 0;
+    byte t = b;
+    for (int i = 0; i < 8; i++) {
+        ret <<= 1;
+        ret |= t & 1;
+        t >>= 1;
+    }
+    cache.put(b, ret);
+    return ret;
+}
+ + + +**不用额外变量交换两个整数** + +[程序员代码面试指南 :P317](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3.md#) + + + +
a = a ^ b;
+b = a ^ b;
+a = a ^ b;
+ + + +**判断一个数是不是 2 的 n 次方** + +[231\. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/) + +二进制表示只有一个 1 存在。 + + + +
public boolean isPowerOfTwo(int n) {
+    return n > 0 && Integer.bitCount(n) == 1;
+}
+ + + +利用 1000 & 0111 == 0 这种性质,得到以下解法: + + + +
public boolean isPowerOfTwo(int n) {
+    return n > 0 && (n & (n - 1)) == 0;
+}
+ + + +**判断一个数是不是 4 的 n 次方** + +[342\. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/) + +这种数在二进制表示中有且只有一个奇数位为 1,例如 16(10000)。 + + + +
public boolean isPowerOfFour(int num) {
+    return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0;
+}
+ + + +也可以使用正则表达式进行匹配。 + + + +
public boolean isPowerOfFour(int num) {
+    return Integer.toString(num, 4).matches("10*");
+}
+ + + +**判断一个数的位级表示是否不会出现连续的 0 和 1** + +[693\. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/) + + + +
Input: 10
+Output: True
+Explanation:
+The binary representation of 10 is: 1010.
+
+Input: 11
+Output: False
+Explanation:
+The binary representation of 11 is: 1011.
+ + + +对于 1010 这种位级表示的数,把它向右移动 1 位得到 101,这两个数每个位都不同,因此异或得到的结果为 1111。 + + + +
public boolean hasAlternatingBits(int n) {
+    int a = (n ^ (n >> 1));
+    return (a & (a + 1)) == 0;
+}
+ + + +**求一个数的补码** + +[476\. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/) + + + +
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。 + + + +
public int findComplement(int num) {
+    if (num == 0) return 1;
+    int mask = 1 << 30;
+    while ((num & mask) == 0) mask >>= 1;
+    mask = (mask << 1) - 1;
+    return num ^ mask;
+}
+ + + +可以利用 Java 的 Integer.highestOneBit() 方法来获得含有首 1 的数。 + + + +
public int findComplement(int num) {
+    if (num == 0) return 1;
+    int mask = Integer.highestOneBit(num);
+    mask = (mask << 1) - 1;
+    return num ^ mask;
+}
+ + + +对于 10000000 这样的数要扩展成 11111111,可以利用以下方法: + + + +
mask |= mask >> 1    11000000
+mask |= mask >> 2    11110000
+mask |= mask >> 4    11111111
+ + + + + +
public int findComplement(int num) {
+    int mask = num;
+    mask |= mask >> 1;
+    mask |= mask >> 2;
+    mask |= mask >> 4;
+    mask |= mask >> 8;
+    mask |= mask >> 16;
+    return (mask ^ num);
+}
+ + + +**实现整数的加法** + +[371\. Sum of Two Integers (Easy)](https://leetcode.com/problems/sum-of-two-integers/description/) + +a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。 + +递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 + + + +
public int getSum(int a, int b) {
+    return b == 0 ? a : getSum((a ^ b), (a & b) << 1);
+}
+ + + +**字符串数组最大乘积** + +[318\. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/) + + + +
Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"]
+Return 16
+The two words can be "abcw", "xtfn".
+ + + +题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。 + +本题主要问题是判断两个字符串是否含相同字符,由于字符串只含有小写字符,总共 26 位,因此可以用一个 32 位的整数来存储每个字符是否出现过。 + + + +
public int maxProduct(String[] words) {
+    int n = words.length;
+    int[] val = new int[n];
+    for (int i = 0; i < n; i++) {
+        for (char c : words[i].toCharArray()) {
+            val[i] |= 1 << (c - 'a');
+        }
+    }
+    int ret = 0;
+    for (int i = 0; i < n; i++) {
+        for (int j = i + 1; j < n; j++) {
+            if ((val[i] & val[j]) == 0) {
+                ret = Math.max(ret, words[i].length() * words[j].length());
+            }
+        }
+    }
+    return ret;
+}
+ + + +**统计从 0 ~ n 每个数的二进制表示中 1 的个数** + +[338\. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/) + +对于数字 6(110),它可以看成是 4(100) 再加一个 2(10),因此 dp[i] = dp[i&(i-1)] + 1; + + + +
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;
+}
+ From c06878914464b6ac8dc5cb78c01a6c758b9ee880 Mon Sep 17 00:00:00 2001 From: How_2_Play_Life <362294931@qq.com> Date: Fri, 7 Sep 2018 12:38:11 +0800 Subject: [PATCH 27/27] Update README.md --- README.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2f44827..e325fcc 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,10 @@ 每篇文章都会有笔者更加详细的一系列博客可供参考,这些文章也被我发表在CSDN技术博客上,整理成博客专栏,欢迎查看━(*`∀´*)ノ亻! -具体内容请见我的CSDN技术博客:https://blog.csdn.net/a724888 - -也可以来我个人技术小站逛逛:https://h2pl.github.io/ +详细内容请见我的CSDN技术博客:https://blog.csdn.net/a724888 **更多校招干货和技术文章请关注我的公众号:程序员江湖** -**关于转载** - -转载的时候请注明一下出处就行啦。 - -另外我这个仓库的格式模仿的是@CyC2018 大佬的仓库 - -并且其中一篇LeetCode刷题指南也是fork这位大佬而来的。我只是自己刷了一遍然后稍微加了一些解析,站在大佬肩膀上。 - - | Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | | :------: | :---------: | :-------: | :---------: | :---: | :---------:| :---------: | :---------: | :---------:| | 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 数据库[:floppy_disk:](#数据库-floppy_disk)| Java[:couple:](#Java-couple) |JavaWeb [:coffee:](#JavaWeb-coffee)| 分布式 [:sweat_drops:](#分布式-sweat_drops)| 设计模式[:hammer:](#设计模式-hammer)| Hadoop[:speak_no_evil:](#Hadoop-speak_no_evil)| @@ -31,6 +20,8 @@ > [剑指offer算法总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E5%89%91%E6%8C%87offer.md) +> [LeetCode刷题指南](https://github.com/h2pl/Java-Tutorial/blob/master/md/LeetCode%E5%88%B7%E9%A2%98%E6%8C%87%E5%8D%97.md) + ## 操作系统 :computer: > [操作系统学习总结](https://github.com/h2pl/Java-Tutorial/blob/master/md/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93.md) @@ -80,6 +71,12 @@
+**关于转载** + +转载的时候请注明一下出处就行啦。 + +另外我这个仓库的格式模仿的是@CyC2018 大佬的仓库 +并且其中一篇LeetCode刷题指南也是fork这位大佬而来的。我只是自己刷了一遍然后稍微加了一些解析,站在大佬肩膀上。