diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index 29e3f06388..08ae7cfc9c
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,6 @@
-node_modules
+node_modules/
package-lock.json
package.json
yarn.lock
-compile.sh
-deploy.sh
-
-
+scripts/
+.DS_Store
\ No newline at end of file
diff --git a/docs/.DS_Store b/docs/.DS_Store
index 8693aee5f4..cfba763044 100644
Binary files a/docs/.DS_Store and b/docs/.DS_Store differ
diff --git a/docs/.obsidian/appearance.json b/docs/.obsidian/appearance.json
index 976ddd65bf..acf703af81 100644
--- a/docs/.obsidian/appearance.json
+++ b/docs/.obsidian/appearance.json
@@ -1,4 +1,5 @@
{
"theme": "obsidian",
- "cssTheme": "Obsidian Nord"
+ "cssTheme": "Obsidian Nord",
+ "accentColor": ""
}
\ No newline at end of file
diff --git a/docs/.obsidian/core-plugins-migration.json b/docs/.obsidian/core-plugins-migration.json
new file mode 100644
index 0000000000..318ec8585d
--- /dev/null
+++ b/docs/.obsidian/core-plugins-migration.json
@@ -0,0 +1,31 @@
+{
+ "file-explorer": true,
+ "global-search": true,
+ "switcher": true,
+ "graph": true,
+ "backlink": true,
+ "outgoing-link": true,
+ "tag-pane": true,
+ "page-preview": true,
+ "daily-notes": true,
+ "templates": true,
+ "note-composer": true,
+ "command-palette": true,
+ "slash-command": false,
+ "editor-status": true,
+ "starred": true,
+ "markdown-importer": false,
+ "zk-prefixer": false,
+ "random-note": false,
+ "outline": true,
+ "word-count": true,
+ "slides": false,
+ "audio-recorder": false,
+ "workspaces": false,
+ "file-recovery": true,
+ "publish": false,
+ "sync": false,
+ "canvas": true,
+ "bookmarks": true,
+ "properties": false
+}
\ No newline at end of file
diff --git a/docs/.obsidian/core-plugins.json b/docs/.obsidian/core-plugins.json
index 96869dfde4..30410c96ee 100644
--- a/docs/.obsidian/core-plugins.json
+++ b/docs/.obsidian/core-plugins.json
@@ -1,19 +1,32 @@
-[
- "file-explorer",
- "global-search",
- "switcher",
- "graph",
- "backlink",
- "outgoing-link",
- "tag-pane",
- "page-preview",
- "daily-notes",
- "templates",
- "note-composer",
- "command-palette",
- "editor-status",
- "starred",
- "outline",
- "word-count",
- "file-recovery"
-]
\ No newline at end of file
+{
+ "file-explorer": true,
+ "global-search": true,
+ "switcher": true,
+ "graph": true,
+ "backlink": true,
+ "outgoing-link": true,
+ "tag-pane": true,
+ "page-preview": true,
+ "daily-notes": true,
+ "templates": true,
+ "note-composer": true,
+ "command-palette": true,
+ "slash-command": false,
+ "editor-status": true,
+ "starred": true,
+ "markdown-importer": false,
+ "zk-prefixer": false,
+ "random-note": false,
+ "outline": true,
+ "word-count": true,
+ "slides": false,
+ "audio-recorder": false,
+ "workspaces": false,
+ "file-recovery": true,
+ "publish": false,
+ "sync": false,
+ "canvas": true,
+ "bookmarks": true,
+ "properties": false,
+ "webviewer": false
+}
\ No newline at end of file
diff --git a/docs/.obsidian/graph.json b/docs/.obsidian/graph.json
new file mode 100644
index 0000000000..f1c3cdafa8
--- /dev/null
+++ b/docs/.obsidian/graph.json
@@ -0,0 +1,22 @@
+{
+ "collapse-filter": true,
+ "search": "",
+ "showTags": false,
+ "showAttachments": false,
+ "hideUnresolved": false,
+ "showOrphans": true,
+ "collapse-color-groups": true,
+ "colorGroups": [],
+ "collapse-display": false,
+ "showArrow": false,
+ "textFadeMultiplier": 0,
+ "nodeSizeMultiplier": 1,
+ "lineSizeMultiplier": 1,
+ "collapse-forces": true,
+ "centerStrength": 0.518713248970312,
+ "repelStrength": 9.42057291666667,
+ "linkStrength": 0.345987955729167,
+ "linkDistance": 250,
+ "scale": 0.8171482109060346,
+ "close": true
+}
\ No newline at end of file
diff --git a/docs/.obsidian/workspace b/docs/.obsidian/workspace
index 540db7d687..2005115db2 100644
--- a/docs/.obsidian/workspace
+++ b/docs/.obsidian/workspace
@@ -4,13 +4,13 @@
"type": "split",
"children": [
{
- "id": "d328e22c3a421c71",
+ "id": "17a0267a38d6cd5c",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
- "file": "data-structure-algorithms/soultion/Array-Solution.md",
- "mode": "source",
+ "file": "data-management/MySQL/MySQL-Index.md",
+ "mode": "preview",
"source": false
}
}
@@ -77,7 +77,7 @@
"state": {
"type": "backlink",
"state": {
- "file": "data-structure-algorithms/soultion/Array-Solution.md",
+ "file": "data-management/MySQL/MySQL-Index.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
@@ -94,7 +94,7 @@
"state": {
"type": "outgoing-link",
"state": {
- "file": "data-structure-algorithms/soultion/Array-Solution.md",
+ "file": "data-management/MySQL/MySQL-Index.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
}
@@ -117,7 +117,7 @@
"state": {
"type": "outline",
"state": {
- "file": "data-structure-algorithms/soultion/Array-Solution.md"
+ "file": "data-management/MySQL/MySQL-Index.md"
}
}
}
@@ -128,8 +128,14 @@
"direction": "horizontal",
"width": 300
},
- "active": "d328e22c3a421c71",
+ "active": "17a0267a38d6cd5c",
"lastOpenFiles": [
- "data-structure-algorithms/Array.md"
+ "interview/Kafka-FAQ.md",
+ "work/DPA.md",
+ "data-structure-algorithms/soultion/Array-Solution.md",
+ "data-structure-algorithms/Array.md",
+ "data-structure-algorithms/Recursion.md",
+ "data-management/Big-Data/Bloom-Filter.md",
+ "data-structure-algorithms/Linked-List.md"
]
}
\ No newline at end of file
diff --git a/docs/.obsidian/workspace.json b/docs/.obsidian/workspace.json
new file mode 100644
index 0000000000..d648b6a945
--- /dev/null
+++ b/docs/.obsidian/workspace.json
@@ -0,0 +1,302 @@
+{
+ "main": {
+ "id": "2c9313c97191b3d5",
+ "type": "split",
+ "children": [
+ {
+ "id": "233cec07312ea132",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "17a0267a38d6cd5c",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "data-management/MySQL/MySQL-Index.md",
+ "mode": "preview",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "MySQL-Index"
+ }
+ },
+ {
+ "id": "264bec0c6c3126ab",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "data-structure-algorithms/soultion/LinkedList-Soultion.md",
+ "mode": "source",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "LinkedList-Soultion"
+ }
+ },
+ {
+ "id": "e24a131a4a8c13f9",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "data-structure-algorithms/algorithm/Dynamic-Programming.md",
+ "mode": "source",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "Dynamic-Programming"
+ }
+ },
+ {
+ "id": "56243bd84331da43",
+ "type": "leaf",
+ "state": {
+ "type": "release-notes",
+ "state": {
+ "currentVersion": "1.8.9"
+ },
+ "icon": "lucide-book-up",
+ "title": "Release Notes 1.8.9"
+ }
+ },
+ {
+ "id": "80d5c719688f185a",
+ "type": "leaf",
+ "state": {
+ "type": "release-notes",
+ "state": {
+ "currentVersion": "1.8.10"
+ },
+ "icon": "lucide-book-up",
+ "title": "Release Notes 1.8.10"
+ }
+ },
+ {
+ "id": "c7c1397df03f59cc",
+ "type": "leaf",
+ "state": {
+ "type": "empty",
+ "state": {},
+ "icon": "lucide-file",
+ "title": "新标签页"
+ }
+ },
+ {
+ "id": "f02bfed475530fbe",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "data-structure-algorithms/soultion/DP-Solution.md",
+ "mode": "source",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "DP-Solution"
+ }
+ },
+ {
+ "id": "0d9db99c2c25b1c2",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "data-structure-algorithms/soultion/DP-Solution.md",
+ "mode": "source",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "DP-Solution"
+ }
+ }
+ ],
+ "currentTab": 4
+ }
+ ],
+ "direction": "vertical"
+ },
+ "left": {
+ "id": "c2ba06bd2f318734",
+ "type": "split",
+ "children": [
+ {
+ "id": "8aec8ec279c7aff1",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "ae760b575766f3f0",
+ "type": "leaf",
+ "state": {
+ "type": "file-explorer",
+ "state": {
+ "sortOrder": "alphabetical",
+ "autoReveal": false
+ },
+ "icon": "lucide-folder-closed",
+ "title": "文件列表"
+ }
+ },
+ {
+ "id": "f88a42db0e3bb960",
+ "type": "leaf",
+ "state": {
+ "type": "search",
+ "state": {
+ "query": "",
+ "matchingCase": false,
+ "explainSearch": false,
+ "collapseAll": false,
+ "extraContext": false,
+ "sortOrder": "alphabetical"
+ },
+ "icon": "lucide-search",
+ "title": "搜索"
+ }
+ },
+ {
+ "id": "c7bda26138bbb70d",
+ "type": "leaf",
+ "state": {
+ "type": "starred",
+ "state": {},
+ "icon": "lucide-file",
+ "title": "插件不再活动"
+ }
+ },
+ {
+ "id": "123cfa366479ce4d",
+ "type": "leaf",
+ "state": {
+ "type": "bookmarks",
+ "state": {},
+ "icon": "lucide-bookmark",
+ "title": "书签"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "width": 200
+ },
+ "right": {
+ "id": "4d54828aaab36d9f",
+ "type": "split",
+ "children": [
+ {
+ "id": "4acb1c44f8a68ec8",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "f56e43f509009e33",
+ "type": "leaf",
+ "state": {
+ "type": "backlink",
+ "state": {
+ "file": "data-management/Redis/Redis-Datatype.md",
+ "collapseAll": true,
+ "extraContext": false,
+ "sortOrder": "alphabetical",
+ "showSearch": false,
+ "searchQuery": "",
+ "backlinkCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-coming-in",
+ "title": "Redis-Datatype 的反向链接列表"
+ }
+ },
+ {
+ "id": "e22cbd6030bde448",
+ "type": "leaf",
+ "state": {
+ "type": "outgoing-link",
+ "state": {
+ "file": "data-management/Redis/Redis-Datatype.md",
+ "linksCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-going-out",
+ "title": "Redis-Datatype 的出链列表"
+ }
+ },
+ {
+ "id": "37585a229386609d",
+ "type": "leaf",
+ "state": {
+ "type": "tag",
+ "state": {
+ "sortOrder": "frequency",
+ "useHierarchy": true
+ },
+ "icon": "lucide-tags",
+ "title": "标签"
+ }
+ },
+ {
+ "id": "1b738c49f6ecdf2c",
+ "type": "leaf",
+ "state": {
+ "type": "outline",
+ "state": {
+ "followCursor": false,
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-list",
+ "title": "大纲"
+ }
+ }
+ ],
+ "currentTab": 3
+ }
+ ],
+ "direction": "horizontal",
+ "width": 300
+ },
+ "left-ribbon": {
+ "hiddenItems": {
+ "switcher:打开快速切换": false,
+ "graph:查看关系图谱": false,
+ "canvas:新建白板": false,
+ "daily-notes:打开/创建今天的日记": false,
+ "templates:插入模板": false,
+ "command-palette:打开命令面板": false
+ }
+ },
+ "active": "80d5c719688f185a",
+ "lastOpenFiles": [
+ "framework/SpringWebFlux/Webflux~.md",
+ "framework/SpringWebFlux/Webflux.md",
+ "framework/SpringWebFlux/Untitled.md",
+ "framework/SpringWebFlux/响应式编程~.md",
+ "framework/SpringWebFlux/SpringWebFlux~.md",
+ "framework/SpringWebFlux/WebFlux~.md",
+ "data-management/Redis/Redis-Datatype.md",
+ "data-management/Redis/Redis-Database.md",
+ "data-management/Redis/Nosql-Overview.md",
+ "data-management/Redis/Nosql-Overview~.md",
+ "data-management/Redis/reproduce/Cache-Design.md",
+ "data-management/Redis/reproduce/Redis为什么变慢了-常见延迟问题定位与分析.md",
+ "data-management/Redis/reproduce/Key 寻址算法.md",
+ "interview/Redis-FAQ.md",
+ "data-structure-algorithms/soultion/Binary-Tree-Solution.md",
+ "未命名文件夹",
+ "java/IO~.md",
+ "java/IO.md",
+ "java/未命名.md",
+ "interview/Ability-FAQ~.md",
+ "data-structure-algorithms/algorithm/BFS.md",
+ "data-structure-algorithms/algorithm/Untitled.md",
+ "interview/Algorithm.md",
+ "interview/Alto.md",
+ "interview/Untitled.md",
+ "data-structure-algorithms/data-structure/Binary-Tree.md",
+ "data-structure-algorithms/soultion/DP-Solution.md",
+ "Untitled.canvas",
+ "data-structure-algorithms/algorithm",
+ "data-structure-algorithms/未命名文件夹"
+ ]
+}
\ No newline at end of file
diff --git a/docs/.vuepress/.DS_Store b/docs/.vuepress/.DS_Store
index 76da44d2c3..cd94417745 100644
Binary files a/docs/.vuepress/.DS_Store and b/docs/.vuepress/.DS_Store differ
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index e64f9edb72..42bf241df7 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -150,33 +150,43 @@ function genDSASidebar() {
return [
{
title: "数据结构",
- collapsable: false,
- sidebarDepth: 2, // 可选的, 默认值是 1
- children: ["Array","Linked-List","Stack","Queue","Binary-Tree","Skip-List"]
+ collapsable: true,
+ //sidebarDepth: 2, // 可选的, 默认值是 1
+ children: [
+ ['data-structure/Array','数组'],
+ ['data-structure/Linked-List','链表'],
+ ['data-structure/Stack','栈'],
+ ['data-structure/Queue','队列'],
+ ['data-structure/Binary-Tree','二叉树'],
+ ['data-structure/Skip-List','跳表']
+ ]
},
{
title: "算法",
collapsable: true,
children: [
"complexity",
- "Sort",
- ['Binary-Search', '二分查找'],
- ['Recursion', '递归'],
- ['Double-Pointer', '双指针'],
- ['Dynamic-Programming', '动态规划'],
- ['DFS', 'DFS']
+ ['algorithm/Sort', '排序'],
+ ['algorithm/Binary-Search', '二分查找'],
+ ['algorithm/Recursion', '递归'],
+ ['algorithm/Backtracking', '回溯'],
+ ['algorithm/DFS-BFS', 'DFS vs BFS'],
+ ['algorithm/Double-Pointer', '双指针'],
+ ['algorithm/Dynamic-Programming', '动态规划']
]
},
{
title: "刷题",
collapsable: true,
children: [
- ['soultion/binary-tree', '二叉树'],
+ ['soultion/Binary-Tree-Solution', '二叉树'],
['soultion/Array-Solution', '数组'],
['soultion/String-Solution', '字符串'],
['soultion/LinkedList-Soultion', '链表'],
+ ['soultion/DFS-Solution', 'DFS'],
['soultion/Math-Solution', '数学'],
- ['soultion/stock-problems', '股票问题']
+ ['soultion/stock-problems', '股票问题'],
+ ['soultion/剑指offer', '剑指offer']
]
}
];
@@ -185,19 +195,41 @@ function genDSASidebar() {
function genDesignPatternSidebar() {
return [
['Design-Pattern-Overview', '设计模式前传'],
- ['Singleton-Pattern', '单例模式'],
- ['Factory-Pattern', '工厂模式'],
- ['Prototype-Pattern', '原型模式'],
- ['Builder-Pattern', '建造者模式'],
- ['Decorator-Pattern', '装饰模式'],
- ['Proxy-Pattern', '代理模式'],
- ['Adapter-Pattern', '适配器模式'],
- ['Chain-of-Responsibility-Pattern', '责任链模式'],
- ['Observer-Pattern', '观察者模式'],
- ['Facade-Pattern', '外观模式'],
- ['Template-Pattern', '模板方法模式'],
- ['Strategy-Pattern', '策略模式'],
- ['Pipeline-Pattern', '管道模式']
+ {
+ title: "创建型模式",
+ collapsable: true,
+ sidebarDepth: 3, // 可选的, 默认值是 1
+ children: [
+ ['Singleton-Pattern', '单例模式'],
+ ['Factory-Pattern', '工厂模式'],
+ ['Prototype-Pattern', '原型模式'],
+ ['Builder-Pattern', '建造者模式']
+ ]
+ },
+ {
+ title: "结构型模式",
+ collapsable: true,
+ sidebarDepth: 3, // 可选的, 默认值是 1
+ children: [
+ ['Decorator-Pattern', '装饰模式'],
+ ['Proxy-Pattern', '代理模式'],
+ ['Adapter-Pattern', '适配器模式']
+ ]
+ },
+ {
+ title: "行为模式",
+ collapsable: true,
+ sidebarDepth: 2, // 可选的, 默认值是 1
+ children: [
+ ['Chain-of-Responsibility-Pattern', '责任链模式'],
+ ['Observer-Pattern', '观察者模式'],
+ ['Template-Pattern', '模板方法模式'],
+ ['Strategy-Pattern', '策略模式'],
+ ['Facade-Pattern', '外观模式']
+ ]
+ },
+ ['Pipeline-Pattern', '管道模式'],
+ ['Spring-Design.md', 'Spring 中的设计模式']
];
}
@@ -214,8 +246,9 @@ function genDataManagementSidebar(){
['MySQL/MySQL-Transaction', 'MySQL 事务'],
['MySQL/MySQL-Log', 'MySQL 日志'],
['MySQL/MySQL-Lock', 'MySQL 锁'],
- ['MySQL/MySQL-select', 'MySQL 查询'],
- ['MySQL/数据库三范式', '数据库三范式'],
+ ['MySQL/MySQL-Select', 'MySQL 查询'],
+ ['MySQL/MySQL-Optimization', 'MySQL 优化'],
+ ['MySQL/Three-Normal-Forms', '数据库三范式']
]
},
{
@@ -228,7 +261,7 @@ function genDataManagementSidebar(){
['Redis/Redis-Persistence', 'Redis 持久化'],
['Redis/Redis-Conf', 'Redis 配置'],
['Redis/Redis-Transaction', 'Redis 事务'],
- ['Redis/Reids-Lock', 'Redis 分布式锁'],
+ ['Redis/Redis-Lock', 'Redis 分布式锁'],
['Redis/Redis-Master-Slave', 'Redis 主从'],
['Redis/Redis-Sentinel', 'Redis 哨兵'],
['Redis/Redis-Cluster', 'Redis 集群'],
@@ -246,9 +279,7 @@ function genDataManagementSidebar(){
['Big-Data/HBase', 'HBase'],
['Big-Data/Phoenix', 'Phoneix']
]
- },
-
- 'Redis/Cache-Design'
+ }
];
}
@@ -344,7 +375,7 @@ function genNetworkSidebar(){
function genInterviewSidebar(){
return [
['Java-Basics-FAQ', 'Java基础部分'],
- ['Collections-FAQ', 'Java集合部分'],
+ ['Java-Collections-FAQ', 'Java集合部分'],
['JUC-FAQ', 'Java 多线程部分'],
['JVM-FAQ', 'JVM 部分'],
['MySQL-FAQ', 'MySQL 部分'],
@@ -355,9 +386,7 @@ function genInterviewSidebar(){
['RPC-FAQ', 'RPC 部分'],
['MyBatis-FAQ', 'MyBatis 部分'],
['Spring-FAQ', 'Spring 部分'],
- ['SpringBoot-FAQ', 'Spring Boot 部分'],
['Design-Pattern-FAQ', '设计模式部分'],
- ['Tomcat-FAQ', 'Tomcat 部分'],
['Elasticsearch-FAQ', 'Elasticsearch 部分'],
];
}
\ No newline at end of file
diff --git a/docs/.vuepress/dist b/docs/.vuepress/dist
index f970aadd9a..105cc8a4b2 160000
--- a/docs/.vuepress/dist
+++ b/docs/.vuepress/dist
@@ -1 +1 @@
-Subproject commit f970aadd9ad37262fe1b788592818ca46ea620b0
+Subproject commit 105cc8a4b28dbf98f7e847970e21da82cdaba28c
diff --git a/docs/.vuepress/public/qcode.png b/docs/.vuepress/public/qcode.png
index 22641ffb9e..0c93c28828 100644
Binary files a/docs/.vuepress/public/qcode.png and b/docs/.vuepress/public/qcode.png differ
diff --git a/docs/.vuepress/theme/.DS_Store b/docs/.vuepress/theme/.DS_Store
index 5139a2bae2..b6fd03ec58 100644
Binary files a/docs/.vuepress/theme/.DS_Store and b/docs/.vuepress/theme/.DS_Store differ
diff --git a/docs/.vuepress/theme/vuepress-theme-reco/.DS_Store b/docs/.vuepress/theme/vuepress-theme-reco/.DS_Store
index 26f6db1bae..41406f682d 100644
Binary files a/docs/.vuepress/theme/vuepress-theme-reco/.DS_Store and b/docs/.vuepress/theme/vuepress-theme-reco/.DS_Store differ
diff --git a/docs/.vuepress/theme/vuepress-theme-reco/components/.DS_Store b/docs/.vuepress/theme/vuepress-theme-reco/components/.DS_Store
index 149cc56f80..f5d0703465 100644
Binary files a/docs/.vuepress/theme/vuepress-theme-reco/components/.DS_Store and b/docs/.vuepress/theme/vuepress-theme-reco/components/.DS_Store differ
diff --git a/docs/.vuepress/theme/vuepress-theme-reco/styles/palette.styl b/docs/.vuepress/theme/vuepress-theme-reco/styles/palette.styl
index 21b758e69a..ea6620af89 100644
--- a/docs/.vuepress/theme/vuepress-theme-reco/styles/palette.styl
+++ b/docs/.vuepress/theme/vuepress-theme-reco/styles/palette.styl
@@ -20,6 +20,8 @@ $lightColor3 = rgba(255, 255, 255, .3)
$lightColor2 = rgba(255, 255, 255, .2)
$lightColor1 = rgba(255, 255, 255, .1)
+$accentColor = #2A6FDB
+
$textShadow = 0 2px 4px $darkColor1;
$borderRadius = .25rem
$lineNumbersWrapperWidth = 2.5rem
diff --git "a/docs/TODO/complexity\347\232\204\345\211\257\346\234\254.md" "b/docs/TODO/complexity\347\232\204\345\211\257\346\234\254.md"
deleted file mode 100644
index 32979ae6e1..0000000000
--- "a/docs/TODO/complexity\347\232\204\345\211\257\346\234\254.md"
+++ /dev/null
@@ -1,40 +0,0 @@
-作为一个伪高级开发,想换坑了,之前也没系统刷过算法题,今天打开LeetCode第一题,看解析突然发现大学时候学的时间复杂度、空间复杂度在我脑中只有$O(n)$、$O(0)$ 这几个符号,具体什么意思,学渣体质的我那会应该就没学会,所以,,,输入并输出一下,
-
-
-
-
-
-
-
-
-
-算法与数据结构是面试考察的重中之重,也是大家日后学习时需要着重训练的部分。简单的总结一下,大约有这些内容:
-
-
-
-## **算法 - Algorithms**
-
-1. 排序算法:快速排序、归并排序、计数排序
-2. 搜索算法:回溯、递归、剪枝技巧
-3. 图论:最短路、最小生成树、网络流建模
-4. 动态规划:背包问题、最长子序列、计数问题
-5. 基础技巧:分治、倍增、二分、贪心
-
-## **数据结构 - Data Structures**
-
-1. 数组与链表:单 / 双向链表、跳舞链
-2. 栈与队列
-3. 树与图:最近公共祖先、并查集
-4. 哈希表
-5. 堆:大 / 小根堆、可并堆
-6. 字符串:字典树、后缀树
-
-
-
-对于上面总结的这部分内容,力扣已经为大家准备好了相关专题,等待大家来练习啦。
-
-算法部分,我们开设了 [初级算法 - 帮助入门](https://leetcode-cn.com/explore/interview/card/top-interview-questions-easy/)、[中级算法 - 巩固训练](https://leetcode-cn.com/explore/interview/card/top-interview-questions-medium/)、 [高级算法 - 提升进阶](https://leetcode-cn.com/explore/interview/card/top-interview-questions-hard/) 三个不同的免费探索主题,包含:数组、字符串、搜索、排序、动态规划、数学、图论等许多内容。大家可以针对自己当前的基础与能力,选择相对应的栏目进行练习。为了能够达到较好的效果,建议小伙伴将所有题目都练习 2~3 遍,吃透每一道题目哦。
-
-数据结构部分,我们也开设了一个非常系统性的 [数据结构](https://leetcode-cn.com/explore/learn/) 板块,有练习各类数据结构的探索主题,其中包含:队列与栈、数组与字符串、链表、哈希表、二叉树等丰富的内容。每一个章节都包含文字讲解与生动的图片演示,同时配套相关题目。相信只要认真练习,一定能受益匪浅。
-
-力扣将热门面试问题里比较新的题目按照类别进行了整理,以供大家按模块练习。
diff --git a/docs/_images/.DS_Store b/docs/_images/.DS_Store
index 3e3e4c509e..90d90ebc34 100644
Binary files a/docs/_images/.DS_Store and b/docs/_images/.DS_Store differ
diff --git a/docs/_images/design-pattern/UML-type.png b/docs/_images/design-pattern/UML-type.png
deleted file mode 100644
index 35d9bd2829..0000000000
Binary files a/docs/_images/design-pattern/UML-type.png and /dev/null differ
diff --git a/docs/_images/design-pattern/abstract-factory-demo.png b/docs/_images/design-pattern/abstract-factory-demo.png
deleted file mode 100644
index 7162ebfb46..0000000000
Binary files a/docs/_images/design-pattern/abstract-factory-demo.png and /dev/null differ
diff --git a/docs/_images/design-pattern/abstract-factory-uml.png b/docs/_images/design-pattern/abstract-factory-uml.png
deleted file mode 100644
index 88ca80d309..0000000000
Binary files a/docs/_images/design-pattern/abstract-factory-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/ali-strategy.png b/docs/_images/design-pattern/ali-strategy.png
deleted file mode 100644
index 952bd661ae..0000000000
Binary files a/docs/_images/design-pattern/ali-strategy.png and /dev/null differ
diff --git a/docs/_images/design-pattern/builder-UML.png b/docs/_images/design-pattern/builder-UML.png
deleted file mode 100644
index de7d87a074..0000000000
Binary files a/docs/_images/design-pattern/builder-UML.png and /dev/null differ
diff --git a/docs/_images/design-pattern/builder-car.png b/docs/_images/design-pattern/builder-car.png
deleted file mode 100644
index c6f41f4554..0000000000
Binary files a/docs/_images/design-pattern/builder-car.png and /dev/null differ
diff --git a/docs/_images/design-pattern/decorator-uml.png b/docs/_images/design-pattern/decorator-uml.png
deleted file mode 100644
index b534ffad1f..0000000000
Binary files a/docs/_images/design-pattern/decorator-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/factory-method-uml.png b/docs/_images/design-pattern/factory-method-uml.png
deleted file mode 100644
index 7f6a0eb799..0000000000
Binary files a/docs/_images/design-pattern/factory-method-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/hello-file.png b/docs/_images/design-pattern/hello-file.png
deleted file mode 100644
index 64042c701c..0000000000
Binary files a/docs/_images/design-pattern/hello-file.png and /dev/null differ
diff --git a/docs/_images/design-pattern/observer-uml.png b/docs/_images/design-pattern/observer-uml.png
deleted file mode 100644
index fa14885acb..0000000000
Binary files a/docs/_images/design-pattern/observer-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/pipeline-pattern-csharp-uml.png b/docs/_images/design-pattern/pipeline-pattern-csharp-uml.png
deleted file mode 100644
index b465b1135f..0000000000
Binary files a/docs/_images/design-pattern/pipeline-pattern-csharp-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/pipeline-result.png b/docs/_images/design-pattern/pipeline-result.png
deleted file mode 100644
index 1e64c5061b..0000000000
Binary files a/docs/_images/design-pattern/pipeline-result.png and /dev/null differ
diff --git a/docs/_images/design-pattern/pipeline-uml.png b/docs/_images/design-pattern/pipeline-uml.png
deleted file mode 100644
index c771d84092..0000000000
Binary files a/docs/_images/design-pattern/pipeline-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/responsibility-pattern-uml.png b/docs/_images/design-pattern/responsibility-pattern-uml.png
deleted file mode 100644
index abc7ce238e..0000000000
Binary files a/docs/_images/design-pattern/responsibility-pattern-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/simple-factory-uml.png b/docs/_images/design-pattern/simple-factory-uml.png
deleted file mode 100644
index f82cdab658..0000000000
Binary files a/docs/_images/design-pattern/simple-factory-uml.png and /dev/null differ
diff --git a/docs/_images/design-pattern/strategy-pattern-if-else.png b/docs/_images/design-pattern/strategy-pattern-if-else.png
deleted file mode 100644
index d67a23b714..0000000000
Binary files a/docs/_images/design-pattern/strategy-pattern-if-else.png and /dev/null differ
diff --git a/docs/_images/design-pattern/strategy-pattern-uml.png b/docs/_images/design-pattern/strategy-pattern-uml.png
deleted file mode 100644
index d36f2bc71b..0000000000
Binary files a/docs/_images/design-pattern/strategy-pattern-uml.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/DMA.png b/docs/_images/distribution/message-queue/Kafka/DMA.png
deleted file mode 100644
index a8cf48a85c..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/DMA.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/Sendfile.png b/docs/_images/distribution/message-queue/Kafka/Sendfile.png
deleted file mode 100644
index ce51efc629..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/Sendfile.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/controller-leader.png b/docs/_images/distribution/message-queue/Kafka/controller-leader.png
deleted file mode 100644
index cbab054742..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/controller-leader.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/imprint.png b/docs/_images/distribution/message-queue/Kafka/imprint.png
deleted file mode 100644
index 515369bf61..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/imprint.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/interceptor-demo.png b/docs/_images/distribution/message-queue/Kafka/interceptor-demo.png
deleted file mode 100644
index 804d1ce455..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/interceptor-demo.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-ack-slg.png b/docs/_images/distribution/message-queue/Kafka/kafka-ack-slg.png
deleted file mode 100644
index 494769ff03..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-ack-slg.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-ack=-1.png b/docs/_images/distribution/message-queue/Kafka/kafka-ack=-1.png
deleted file mode 100644
index 0647f7439d..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-ack=-1.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-ack=1.png b/docs/_images/distribution/message-queue/Kafka/kafka-ack=1.png
deleted file mode 100644
index 95be4d96a5..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-ack=1.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-apis.png b/docs/_images/distribution/message-queue/Kafka/kafka-apis.png
deleted file mode 100644
index db6053ccc2..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-apis.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-consume-group.png b/docs/_images/distribution/message-queue/Kafka/kafka-consume-group.png
deleted file mode 100644
index c3931c7a75..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-consume-group.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-leo.png b/docs/_images/distribution/message-queue/Kafka/kafka-leo.png
deleted file mode 100644
index d5f2b6e9c4..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-leo.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-partition.jpg b/docs/_images/distribution/message-queue/Kafka/kafka-partition.jpg
deleted file mode 100644
index e8cd1de32b..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-partition.jpg and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-producer-thread.png b/docs/_images/distribution/message-queue/Kafka/kafka-producer-thread.png
deleted file mode 100644
index 1ecda6e19c..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-producer-thread.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-segement.jpg b/docs/_images/distribution/message-queue/Kafka/kafka-segement.jpg
deleted file mode 100644
index 8bc98a8dd6..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-segement.jpg and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-start.png b/docs/_images/distribution/message-queue/Kafka/kafka-start.png
deleted file mode 100644
index 1080ae6a7b..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-start.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-streams-data-clean.png b/docs/_images/distribution/message-queue/Kafka/kafka-streams-data-clean.png
deleted file mode 100644
index 3c6109c9a0..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-streams-data-clean.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-structure.png b/docs/_images/distribution/message-queue/Kafka/kafka-structure.png
deleted file mode 100644
index 9778e71664..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-structure.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-tags.png b/docs/_images/distribution/message-queue/Kafka/kafka-tags.png
deleted file mode 100644
index 10dd26e699..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-tags.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-version.png b/docs/_images/distribution/message-queue/Kafka/kafka-version.png
deleted file mode 100644
index e8720f136f..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-version.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-workflow.jpg b/docs/_images/distribution/message-queue/Kafka/kafka-workflow.jpg
deleted file mode 100644
index 49ed343ac4..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-workflow.jpg and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kafka-write-flow.png b/docs/_images/distribution/message-queue/Kafka/kafka-write-flow.png
deleted file mode 100644
index 487d735acb..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kafka-write-flow.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kakfa-java-demo.png b/docs/_images/distribution/message-queue/Kafka/kakfa-java-demo.png
deleted file mode 100644
index 491cb41ad0..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kakfa-java-demo.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kakfa-principle.png b/docs/_images/distribution/message-queue/Kafka/kakfa-principle.png
deleted file mode 100644
index c1ce1730bc..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kakfa-principle.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/kakfa-streams-flow.png b/docs/_images/distribution/message-queue/Kafka/kakfa-streams-flow.png
deleted file mode 100644
index 180c313609..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/kakfa-streams-flow.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/log_anatomy.png b/docs/_images/distribution/message-queue/Kafka/log_anatomy.png
deleted file mode 100644
index a649499926..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/log_anatomy.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/log_consumer.png b/docs/_images/distribution/message-queue/Kafka/log_consumer.png
deleted file mode 100644
index fbc45f2060..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/log_consumer.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/mmap.png b/docs/_images/distribution/message-queue/Kafka/mmap.png
deleted file mode 100644
index 9b5668086f..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/mmap.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/mq.png b/docs/_images/distribution/message-queue/Kafka/mq.png
deleted file mode 100644
index 0eb2e236d7..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/mq.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/sumer-groups.png b/docs/_images/distribution/message-queue/Kafka/sumer-groups.png
deleted file mode 100644
index 16fe2936cb..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/sumer-groups.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/zero-copy.png b/docs/_images/distribution/message-queue/Kafka/zero-copy.png
deleted file mode 100644
index f9ac3ba2b3..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/zero-copy.png and /dev/null differ
diff --git a/docs/_images/distribution/message-queue/Kafka/zookeeper-store.png b/docs/_images/distribution/message-queue/Kafka/zookeeper-store.png
deleted file mode 100644
index 143f682a4a..0000000000
Binary files a/docs/_images/distribution/message-queue/Kafka/zookeeper-store.png and /dev/null differ
diff --git "a/docs/_images/distribution/message-queue/Kafka/\344\274\240\347\273\237\346\266\210\346\201\257\351\230\237\345\210\227.png" "b/docs/_images/distribution/message-queue/Kafka/\344\274\240\347\273\237\346\266\210\346\201\257\351\230\237\345\210\227.png"
deleted file mode 100644
index 8122b29cf3..0000000000
Binary files "a/docs/_images/distribution/message-queue/Kafka/\344\274\240\347\273\237\346\266\210\346\201\257\351\230\237\345\210\227.png" and /dev/null differ
diff --git a/docs/_images/interview/mysql-cache-demo.png b/docs/_images/interview/mysql-cache-demo.png
deleted file mode 100644
index ef4bfb73fe..0000000000
Binary files a/docs/_images/interview/mysql-cache-demo.png and /dev/null differ
diff --git a/docs/_images/java/.DS_Store b/docs/_images/java/.DS_Store
index 1404c41a0d..c56bcdd79b 100644
Binary files a/docs/_images/java/.DS_Store and b/docs/_images/java/.DS_Store differ
diff --git a/docs/_images/java/JVM/00831rSTly1gdeb84yh71j30yq0j6akl.jpeg b/docs/_images/java/JVM/00831rSTly1gdeb84yh71j30yq0j6akl.jpeg
deleted file mode 100644
index fbbd6934b0..0000000000
Binary files a/docs/_images/java/JVM/00831rSTly1gdeb84yh71j30yq0j6akl.jpeg and /dev/null differ
diff --git a/docs/_images/java/JVM/3j31ci0m6k5w.jpeg b/docs/_images/java/JVM/3j31ci0m6k5w.jpeg
deleted file mode 100644
index d1623c1132..0000000000
Binary files a/docs/_images/java/JVM/3j31ci0m6k5w.jpeg and /dev/null differ
diff --git a/docs/_images/java/JVM/classloader.png b/docs/_images/java/JVM/classloader.png
deleted file mode 100644
index b388d438bf..0000000000
Binary files a/docs/_images/java/JVM/classloader.png and /dev/null differ
diff --git a/docs/_images/java/JVM/error-oom.png b/docs/_images/java/JVM/error-oom.png
deleted file mode 100644
index 71476a4069..0000000000
Binary files a/docs/_images/java/JVM/error-oom.png and /dev/null differ
diff --git a/docs/_images/java/JVM/java-object.jpg b/docs/_images/java/JVM/java-object.jpg
deleted file mode 100644
index bc17bccfaf..0000000000
Binary files a/docs/_images/java/JVM/java-object.jpg and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-bytecode.png b/docs/_images/java/JVM/jvm-bytecode.png
deleted file mode 100644
index 19c7898e17..0000000000
Binary files a/docs/_images/java/JVM/jvm-bytecode.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-class-load.png b/docs/_images/java/JVM/jvm-class-load.png
deleted file mode 100644
index e4f7fd62b6..0000000000
Binary files a/docs/_images/java/JVM/jvm-class-load.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-compile.png b/docs/_images/java/JVM/jvm-compile.png
deleted file mode 100644
index 3429c6dcc9..0000000000
Binary files a/docs/_images/java/JVM/jvm-compile.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-dynamic-linking.png b/docs/_images/java/JVM/jvm-dynamic-linking.png
deleted file mode 100644
index d706307643..0000000000
Binary files a/docs/_images/java/JVM/jvm-dynamic-linking.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-framework.png b/docs/_images/java/JVM/jvm-framework.png
deleted file mode 100644
index e152cf5509..0000000000
Binary files a/docs/_images/java/JVM/jvm-framework.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-javap.png b/docs/_images/java/JVM/jvm-javap.png
deleted file mode 100644
index 240b310de9..0000000000
Binary files a/docs/_images/java/JVM/jvm-javap.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-jdk-jre.png b/docs/_images/java/JVM/jvm-jdk-jre.png
deleted file mode 100644
index a7e43e5232..0000000000
Binary files a/docs/_images/java/JVM/jvm-jdk-jre.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-pc-counter.png b/docs/_images/java/JVM/jvm-pc-counter.png
deleted file mode 100644
index 2a03b82c4d..0000000000
Binary files a/docs/_images/java/JVM/jvm-pc-counter.png and /dev/null differ
diff --git a/docs/_images/java/JVM/jvm-stack-frame.png b/docs/_images/java/JVM/jvm-stack-frame.png
deleted file mode 100644
index 53b6fd1e63..0000000000
Binary files a/docs/_images/java/JVM/jvm-stack-frame.png and /dev/null differ
diff --git a/docs/_images/java/JVM/minorgc_0.png b/docs/_images/java/JVM/minorgc_0.png
deleted file mode 100644
index e7bc84da07..0000000000
Binary files a/docs/_images/java/JVM/minorgc_0.png and /dev/null differ
diff --git a/docs/_images/java/JVM/minorgc_end.png b/docs/_images/java/JVM/minorgc_end.png
deleted file mode 100644
index 99a808d32e..0000000000
Binary files a/docs/_images/java/JVM/minorgc_end.png and /dev/null differ
diff --git a/docs/_images/java/JVM/oom-end.png b/docs/_images/java/JVM/oom-end.png
deleted file mode 100644
index d78256bbf4..0000000000
Binary files a/docs/_images/java/JVM/oom-end.png and /dev/null differ
diff --git a/docs/_images/java/JVM/oom.png b/docs/_images/java/JVM/oom.png
deleted file mode 100644
index f452f0f13d..0000000000
Binary files a/docs/_images/java/JVM/oom.png and /dev/null differ
diff --git a/docs/_images/java/JVM/swapspace.png b/docs/_images/java/JVM/swapspace.png
deleted file mode 100644
index 6472c67ce5..0000000000
Binary files a/docs/_images/java/JVM/swapspace.png and /dev/null differ
diff --git a/docs/_images/java/JVM/toomanythread.png b/docs/_images/java/JVM/toomanythread.png
deleted file mode 100644
index 5657984e64..0000000000
Binary files a/docs/_images/java/JVM/toomanythread.png and /dev/null differ
diff --git "a/docs/_images/java/JVM/\345\240\206\345\206\205\345\255\230\347\251\272\351\227\264.png" "b/docs/_images/java/JVM/\345\240\206\345\206\205\345\255\230\347\251\272\351\227\264.png"
deleted file mode 100644
index 0ac7cd1298..0000000000
Binary files "a/docs/_images/java/JVM/\345\240\206\345\206\205\345\255\230\347\251\272\351\227\264.png" and /dev/null differ
diff --git "a/docs/_images/java/JVM/\345\244\215\345\210\266\347\256\227\346\263\225.png" "b/docs/_images/java/JVM/\345\244\215\345\210\266\347\256\227\346\263\225.png"
deleted file mode 100644
index 85d6e5cffd..0000000000
Binary files "a/docs/_images/java/JVM/\345\244\215\345\210\266\347\256\227\346\263\225.png" and /dev/null differ
diff --git a/docs/_images/mysql/.DS_Store b/docs/_images/mysql/.DS_Store
index 1b4d8ddc7e..0e7bf57afa 100644
Binary files a/docs/_images/mysql/.DS_Store and b/docs/_images/mysql/.DS_Store differ
diff --git a/docs/_images/mysql/ACID.jpg b/docs/_images/mysql/ACID.jpg
deleted file mode 100644
index b4d61b7839..0000000000
Binary files a/docs/_images/mysql/ACID.jpg and /dev/null differ
diff --git a/docs/_images/mysql/BTree-vs-B+Tree.png b/docs/_images/mysql/BTree-vs-B+Tree.png
deleted file mode 100644
index 5aaa92502d..0000000000
Binary files a/docs/_images/mysql/BTree-vs-B+Tree.png and /dev/null differ
diff --git a/docs/_images/mysql/COMPACT-And-REDUNDANT-Row-Format.jpg b/docs/_images/mysql/COMPACT-And-REDUNDANT-Row-Format.jpg
deleted file mode 100644
index ac01b41a4c..0000000000
Binary files a/docs/_images/mysql/COMPACT-And-REDUNDANT-Row-Format.jpg and /dev/null differ
diff --git a/docs/_images/mysql/Infimum-Rows-Supremum.jpg b/docs/_images/mysql/Infimum-Rows-Supremum.jpg
deleted file mode 100644
index 2894d4b105..0000000000
Binary files a/docs/_images/mysql/Infimum-Rows-Supremum.jpg and /dev/null differ
diff --git a/docs/_images/mysql/InnoDB-B-Tree-Node.jpg b/docs/_images/mysql/InnoDB-B-Tree-Node.jpg
deleted file mode 100644
index 22409dfe63..0000000000
Binary files a/docs/_images/mysql/InnoDB-B-Tree-Node.jpg and /dev/null differ
diff --git a/docs/_images/mysql/MySQL-architecture.png b/docs/_images/mysql/MySQL-architecture.png
deleted file mode 100644
index f1bb2fbf21..0000000000
Binary files a/docs/_images/mysql/MySQL-architecture.png and /dev/null differ
diff --git a/docs/_images/mysql/bTree.png b/docs/_images/mysql/bTree.png
deleted file mode 100644
index c25ab404d3..0000000000
Binary files a/docs/_images/mysql/bTree.png and /dev/null differ
diff --git a/docs/_images/mysql/composite-index.png b/docs/_images/mysql/composite-index.png
deleted file mode 100644
index 6e274d0701..0000000000
Binary files a/docs/_images/mysql/composite-index.png and /dev/null differ
diff --git a/docs/_images/mysql/disk-io.png b/docs/_images/mysql/disk-io.png
deleted file mode 100644
index 09659f5f8c..0000000000
Binary files a/docs/_images/mysql/disk-io.png and /dev/null differ
diff --git a/docs/_images/mysql/expalin.jpg b/docs/_images/mysql/expalin.jpg
deleted file mode 100644
index da052f7fe6..0000000000
Binary files a/docs/_images/mysql/expalin.jpg and /dev/null differ
diff --git a/docs/_images/mysql/explain-key.png b/docs/_images/mysql/explain-key.png
deleted file mode 100644
index 5d4f617e2f..0000000000
Binary files a/docs/_images/mysql/explain-key.png and /dev/null differ
diff --git a/docs/_images/mysql/index-background.jpeg b/docs/_images/mysql/index-background.jpeg
deleted file mode 100644
index 63d2ca1bd9..0000000000
Binary files a/docs/_images/mysql/index-background.jpeg and /dev/null differ
diff --git a/docs/_images/mysql/optimization-orderby.png b/docs/_images/mysql/optimization-orderby.png
deleted file mode 100644
index d13f3656b6..0000000000
Binary files a/docs/_images/mysql/optimization-orderby.png and /dev/null differ
diff --git a/docs/_images/mysql/tablespace-segment-extent-page-row.jpg b/docs/_images/mysql/tablespace-segment-extent-page-row.jpg
deleted file mode 100644
index 66db246165..0000000000
Binary files a/docs/_images/mysql/tablespace-segment-extent-page-row.jpg and /dev/null differ
diff --git a/docs/_images/redis/.DS_Store b/docs/_images/redis/.DS_Store
deleted file mode 100644
index bf1618e94f..0000000000
Binary files a/docs/_images/redis/.DS_Store and /dev/null differ
diff --git a/docs/_images/redis/CLIENT_DIRTY_CAS.png b/docs/_images/redis/CLIENT_DIRTY_CAS.png
deleted file mode 100644
index a6c6cbc1ea..0000000000
Binary files a/docs/_images/redis/CLIENT_DIRTY_CAS.png and /dev/null differ
diff --git a/docs/_images/redis/RLock-UML.png b/docs/_images/redis/RLock-UML.png
deleted file mode 100644
index b8c878acf7..0000000000
Binary files a/docs/_images/redis/RLock-UML.png and /dev/null differ
diff --git a/docs/_images/redis/RLock.png b/docs/_images/redis/RLock.png
deleted file mode 100644
index 95e0e9a3ba..0000000000
Binary files a/docs/_images/redis/RLock.png and /dev/null differ
diff --git a/docs/_images/redis/bgrewriteaof.png b/docs/_images/redis/bgrewriteaof.png
deleted file mode 100644
index c180db8a1c..0000000000
Binary files a/docs/_images/redis/bgrewriteaof.png and /dev/null differ
diff --git a/docs/_images/redis/bitmap1.gif b/docs/_images/redis/bitmap1.gif
deleted file mode 100644
index f1e5916fa6..0000000000
Binary files a/docs/_images/redis/bitmap1.gif and /dev/null differ
diff --git a/docs/_images/redis/bitmap2.gif b/docs/_images/redis/bitmap2.gif
deleted file mode 100644
index 7ee3a1509b..0000000000
Binary files a/docs/_images/redis/bitmap2.gif and /dev/null differ
diff --git a/docs/_images/redis/c-string.jpg b/docs/_images/redis/c-string.jpg
deleted file mode 100644
index f53ed206f9..0000000000
Binary files a/docs/_images/redis/c-string.jpg and /dev/null differ
diff --git a/docs/_images/redis/cluster-info.png b/docs/_images/redis/cluster-info.png
deleted file mode 100644
index 1c270fed92..0000000000
Binary files a/docs/_images/redis/cluster-info.png and /dev/null differ
diff --git a/docs/_images/redis/data-type-structure.jpg b/docs/_images/redis/data-type-structure.jpg
deleted file mode 100644
index 0d9540e811..0000000000
Binary files a/docs/_images/redis/data-type-structure.jpg and /dev/null differ
diff --git a/docs/_images/redis/mq.jpg b/docs/_images/redis/mq.jpg
deleted file mode 100644
index 092a50c599..0000000000
Binary files a/docs/_images/redis/mq.jpg and /dev/null differ
diff --git a/docs/_images/redis/nosql-index.jpg b/docs/_images/redis/nosql-index.jpg
deleted file mode 100644
index 9eb9900db9..0000000000
Binary files a/docs/_images/redis/nosql-index.jpg and /dev/null differ
diff --git a/docs/_images/redis/nosqlwhy.png b/docs/_images/redis/nosqlwhy.png
deleted file mode 100644
index 0b299c4c19..0000000000
Binary files a/docs/_images/redis/nosqlwhy.png and /dev/null differ
diff --git a/docs/_images/redis/redis-aof-conf.jpg b/docs/_images/redis/redis-aof-conf.jpg
deleted file mode 100644
index a12c68cdaa..0000000000
Binary files a/docs/_images/redis/redis-aof-conf.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-aof-conf.png b/docs/_images/redis/redis-aof-conf.png
deleted file mode 100644
index 55d2f09535..0000000000
Binary files a/docs/_images/redis/redis-aof-conf.png and /dev/null differ
diff --git a/docs/_images/redis/redis-aof-file.png b/docs/_images/redis/redis-aof-file.png
deleted file mode 100644
index 6f6c2eed04..0000000000
Binary files a/docs/_images/redis/redis-aof-file.png and /dev/null differ
diff --git a/docs/_images/redis/redis-aof-rewrite-work.png b/docs/_images/redis/redis-aof-rewrite-work.png
deleted file mode 100644
index 10f919c7b1..0000000000
Binary files a/docs/_images/redis/redis-aof-rewrite-work.png and /dev/null differ
diff --git a/docs/_images/redis/redis-aof-summary.png b/docs/_images/redis/redis-aof-summary.png
deleted file mode 100644
index 80d9d01f38..0000000000
Binary files a/docs/_images/redis/redis-aof-summary.png and /dev/null differ
diff --git a/docs/_images/redis/redis-aof-write-log.png b/docs/_images/redis/redis-aof-write-log.png
deleted file mode 100644
index c10f2933b9..0000000000
Binary files a/docs/_images/redis/redis-aof-write-log.png and /dev/null differ
diff --git a/docs/_images/redis/redis-backlog_buffer.jpg b/docs/_images/redis/redis-backlog_buffer.jpg
deleted file mode 100644
index 27552e4945..0000000000
Binary files a/docs/_images/redis/redis-backlog_buffer.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-backlog_buffer.png b/docs/_images/redis/redis-backlog_buffer.png
deleted file mode 100644
index 3770fefc90..0000000000
Binary files a/docs/_images/redis/redis-backlog_buffer.png and /dev/null differ
diff --git a/docs/_images/redis/redis-bgsave.jpg b/docs/_images/redis/redis-bgsave.jpg
deleted file mode 100644
index e74b3ab9e2..0000000000
Binary files a/docs/_images/redis/redis-bgsave.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-bgsave.png b/docs/_images/redis/redis-bgsave.png
deleted file mode 100644
index 85fed50b84..0000000000
Binary files a/docs/_images/redis/redis-bgsave.png and /dev/null differ
diff --git a/docs/_images/redis/redis-cap.png b/docs/_images/redis/redis-cap.png
deleted file mode 100644
index 9a16045c7f..0000000000
Binary files a/docs/_images/redis/redis-cap.png and /dev/null differ
diff --git a/docs/_images/redis/redis-cg-commands.png b/docs/_images/redis/redis-cg-commands.png
deleted file mode 100644
index 45919a7063..0000000000
Binary files a/docs/_images/redis/redis-cg-commands.png and /dev/null differ
diff --git a/docs/_images/redis/redis-cluster-framework.png b/docs/_images/redis/redis-cluster-framework.png
deleted file mode 100644
index 6802ac9e60..0000000000
Binary files a/docs/_images/redis/redis-cluster-framework.png and /dev/null differ
diff --git a/docs/_images/redis/redis-cluster-new.jpg b/docs/_images/redis/redis-cluster-new.jpg
deleted file mode 100644
index fb9032fcfe..0000000000
Binary files a/docs/_images/redis/redis-cluster-new.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-cluster-ping.png b/docs/_images/redis/redis-cluster-ping.png
deleted file mode 100644
index 9e85719ac8..0000000000
Binary files a/docs/_images/redis/redis-cluster-ping.png and /dev/null differ
diff --git a/docs/_images/redis/redis-cluster-slot.png b/docs/_images/redis/redis-cluster-slot.png
deleted file mode 100644
index 230d2ccac0..0000000000
Binary files a/docs/_images/redis/redis-cluster-slot.png and /dev/null differ
diff --git a/docs/_images/redis/redis-cluster-vote.png b/docs/_images/redis/redis-cluster-vote.png
deleted file mode 100644
index e9a2f97c6e..0000000000
Binary files a/docs/_images/redis/redis-cluster-vote.png and /dev/null differ
diff --git a/docs/_images/redis/redis-consistency.png b/docs/_images/redis/redis-consistency.png
deleted file mode 100644
index c840f20b96..0000000000
Binary files a/docs/_images/redis/redis-consistency.png and /dev/null differ
diff --git a/docs/_images/redis/redis-gossip.png b/docs/_images/redis/redis-gossip.png
deleted file mode 100644
index d3e44367ec..0000000000
Binary files a/docs/_images/redis/redis-gossip.png and /dev/null differ
diff --git a/docs/_images/redis/redis-group-strucure.png b/docs/_images/redis/redis-group-strucure.png
deleted file mode 100644
index 1f94555cb2..0000000000
Binary files a/docs/_images/redis/redis-group-strucure.png and /dev/null differ
diff --git a/docs/_images/redis/redis-hash.jpg b/docs/_images/redis/redis-hash.jpg
deleted file mode 100644
index 2f95be5e61..0000000000
Binary files a/docs/_images/redis/redis-hash.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-increment-copy.png b/docs/_images/redis/redis-increment-copy.png
deleted file mode 100644
index d0c39734a3..0000000000
Binary files a/docs/_images/redis/redis-increment-copy.png and /dev/null differ
diff --git a/docs/_images/redis/redis-kv-conflict.jpg b/docs/_images/redis/redis-kv-conflict.jpg
deleted file mode 100644
index 32025db80d..0000000000
Binary files a/docs/_images/redis/redis-kv-conflict.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-kv.jpg b/docs/_images/redis/redis-kv.jpg
deleted file mode 100644
index 1f032fa1de..0000000000
Binary files a/docs/_images/redis/redis-kv.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-lock-banner.jpg b/docs/_images/redis/redis-lock-banner.jpg
deleted file mode 100644
index ee596157cb..0000000000
Binary files a/docs/_images/redis/redis-lock-banner.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-master-slave-index.png b/docs/_images/redis/redis-master-slave-index.png
deleted file mode 100644
index d9d7a42c92..0000000000
Binary files a/docs/_images/redis/redis-master-slave-index.png and /dev/null differ
diff --git a/docs/_images/redis/redis-master-slave-mode.png b/docs/_images/redis/redis-master-slave-mode.png
deleted file mode 100644
index d9d7a42c92..0000000000
Binary files a/docs/_images/redis/redis-master-slave-mode.png and /dev/null differ
diff --git a/docs/_images/redis/redis-message-type.png b/docs/_images/redis/redis-message-type.png
deleted file mode 100644
index 0304c198f4..0000000000
Binary files a/docs/_images/redis/redis-message-type.png and /dev/null differ
diff --git a/docs/_images/redis/redis-mix-persistence-file.png b/docs/_images/redis/redis-mix-persistence-file.png
deleted file mode 100644
index db19558671..0000000000
Binary files a/docs/_images/redis/redis-mix-persistence-file.png and /dev/null differ
diff --git a/docs/_images/redis/redis-mix-persistence.png b/docs/_images/redis/redis-mix-persistence.png
deleted file mode 100644
index b15d20828a..0000000000
Binary files a/docs/_images/redis/redis-mix-persistence.png and /dev/null differ
diff --git a/docs/_images/redis/redis-mq-banner.jpg b/docs/_images/redis/redis-mq-banner.jpg
deleted file mode 100644
index fc3e6dea37..0000000000
Binary files a/docs/_images/redis/redis-mq-banner.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-psubscribe-demo.jpg b/docs/_images/redis/redis-psubscribe-demo.jpg
deleted file mode 100644
index 1151a2c0a0..0000000000
Binary files a/docs/_images/redis/redis-psubscribe-demo.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-psubscribe.jpg b/docs/_images/redis/redis-psubscribe.jpg
deleted file mode 100644
index 7db65740f6..0000000000
Binary files a/docs/_images/redis/redis-psubscribe.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-psubscribe.png b/docs/_images/redis/redis-psubscribe.png
deleted file mode 100644
index 58ab1599de..0000000000
Binary files a/docs/_images/redis/redis-psubscribe.png and /dev/null differ
diff --git a/docs/_images/redis/redis-psubscribe1.png b/docs/_images/redis/redis-psubscribe1.png
deleted file mode 100644
index d3979a6d67..0000000000
Binary files a/docs/_images/redis/redis-psubscribe1.png and /dev/null differ
diff --git a/docs/_images/redis/redis-pub-sub.png b/docs/_images/redis/redis-pub-sub.png
deleted file mode 100644
index b2ae50d962..0000000000
Binary files a/docs/_images/redis/redis-pub-sub.png and /dev/null differ
diff --git a/docs/_images/redis/redis-pub_sub.jpg b/docs/_images/redis/redis-pub_sub.jpg
deleted file mode 100644
index 55d307ab0b..0000000000
Binary files a/docs/_images/redis/redis-pub_sub.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-publish.jpg b/docs/_images/redis/redis-publish.jpg
deleted file mode 100644
index 74554c3ca5..0000000000
Binary files a/docs/_images/redis/redis-publish.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-publish.png b/docs/_images/redis/redis-publish.png
deleted file mode 100644
index fe9d88da26..0000000000
Binary files a/docs/_images/redis/redis-publish.png and /dev/null differ
diff --git a/docs/_images/redis/redis-rdb-bak.jpg b/docs/_images/redis/redis-rdb-bak.jpg
deleted file mode 100644
index c372b38599..0000000000
Binary files a/docs/_images/redis/redis-rdb-bak.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-rdb-bak.png b/docs/_images/redis/redis-rdb-bak.png
deleted file mode 100644
index 31c538690f..0000000000
Binary files a/docs/_images/redis/redis-rdb-bak.png and /dev/null differ
diff --git a/docs/_images/redis/redis-rdb-file.jpg b/docs/_images/redis/redis-rdb-file.jpg
deleted file mode 100644
index 8aaaed90a3..0000000000
Binary files a/docs/_images/redis/redis-rdb-file.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-rdb-file.png b/docs/_images/redis/redis-rdb-file.png
deleted file mode 100644
index 81e2e0ed6a..0000000000
Binary files a/docs/_images/redis/redis-rdb-file.png and /dev/null differ
diff --git a/docs/_images/redis/redis-rdb-summary.jpg b/docs/_images/redis/redis-rdb-summary.jpg
deleted file mode 100644
index 5d427afaef..0000000000
Binary files a/docs/_images/redis/redis-rdb-summary.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-rdb-summary.png b/docs/_images/redis/redis-rdb-summary.png
deleted file mode 100644
index c65b393bd3..0000000000
Binary files a/docs/_images/redis/redis-rdb-summary.png and /dev/null differ
diff --git a/docs/_images/redis/redis-rdb.png b/docs/_images/redis/redis-rdb.png
deleted file mode 100644
index 0279a22be3..0000000000
Binary files a/docs/_images/redis/redis-rdb.png and /dev/null differ
diff --git a/docs/_images/redis/redis-readme-banner.jpg b/docs/_images/redis/redis-readme-banner.jpg
deleted file mode 100644
index ae239878bb..0000000000
Binary files a/docs/_images/redis/redis-readme-banner.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-reansaction-banner.jpg b/docs/_images/redis/redis-reansaction-banner.jpg
deleted file mode 100644
index 71c85ba90c..0000000000
Binary files a/docs/_images/redis/redis-reansaction-banner.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-rehash.jpg b/docs/_images/redis/redis-rehash.jpg
deleted file mode 100644
index 1cbe4c1ca6..0000000000
Binary files a/docs/_images/redis/redis-rehash.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-rehash.png b/docs/_images/redis/redis-rehash.png
deleted file mode 100644
index 374ef066e2..0000000000
Binary files a/docs/_images/redis/redis-rehash.png and /dev/null differ
diff --git a/docs/_images/redis/redis-replicaof.png b/docs/_images/redis/redis-replicaof.png
deleted file mode 100644
index 920ed8ca86..0000000000
Binary files a/docs/_images/redis/redis-replicaof.png and /dev/null differ
diff --git a/docs/_images/redis/redis-rpop.jpg b/docs/_images/redis/redis-rpop.jpg
deleted file mode 100644
index 44e235185d..0000000000
Binary files a/docs/_images/redis/redis-rpop.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-rpop.png b/docs/_images/redis/redis-rpop.png
deleted file mode 100644
index 0e15bfb30b..0000000000
Binary files a/docs/_images/redis/redis-rpop.png and /dev/null differ
diff --git a/docs/_images/redis/redis-rpoplpush.jpg b/docs/_images/redis/redis-rpoplpush.jpg
deleted file mode 100644
index 5628234856..0000000000
Binary files a/docs/_images/redis/redis-rpoplpush.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-rpoplpush.png b/docs/_images/redis/redis-rpoplpush.png
deleted file mode 100644
index f91da98939..0000000000
Binary files a/docs/_images/redis/redis-rpoplpush.png and /dev/null differ
diff --git a/docs/_images/redis/redis-sds.jpg b/docs/_images/redis/redis-sds.jpg
deleted file mode 100644
index 4d74127985..0000000000
Binary files a/docs/_images/redis/redis-sds.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-select-master.png b/docs/_images/redis/redis-select-master.png
deleted file mode 100644
index e5f4d2bbdd..0000000000
Binary files a/docs/_images/redis/redis-select-master.png and /dev/null differ
diff --git a/docs/_images/redis/redis-sentinel-cluster.png b/docs/_images/redis/redis-sentinel-cluster.png
deleted file mode 100644
index 6029c71e13..0000000000
Binary files a/docs/_images/redis/redis-sentinel-cluster.png and /dev/null differ
diff --git a/docs/_images/redis/redis-sentinel-ps.png b/docs/_images/redis/redis-sentinel-ps.png
deleted file mode 100644
index 7bf9f6471a..0000000000
Binary files a/docs/_images/redis/redis-sentinel-ps.png and /dev/null differ
diff --git a/docs/_images/redis/redis-sentinel-select-master.jpg b/docs/_images/redis/redis-sentinel-select-master.jpg
deleted file mode 100644
index 090b4297c9..0000000000
Binary files a/docs/_images/redis/redis-sentinel-select-master.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-sentinel-slave.png b/docs/_images/redis/redis-sentinel-slave.png
deleted file mode 100644
index 1ef37adbf4..0000000000
Binary files a/docs/_images/redis/redis-sentinel-slave.png and /dev/null differ
diff --git a/docs/_images/redis/redis-sentinel.png b/docs/_images/redis/redis-sentinel.png
deleted file mode 100644
index d256f130dd..0000000000
Binary files a/docs/_images/redis/redis-sentinel.png and /dev/null differ
diff --git a/docs/_images/redis/redis-set.gif b/docs/_images/redis/redis-set.gif
deleted file mode 100644
index 3e511647fe..0000000000
Binary files a/docs/_images/redis/redis-set.gif and /dev/null differ
diff --git a/docs/_images/redis/redis-setkv-aof.png b/docs/_images/redis/redis-setkv-aof.png
deleted file mode 100644
index 4d31075e18..0000000000
Binary files a/docs/_images/redis/redis-setkv-aof.png and /dev/null differ
diff --git a/docs/_images/redis/redis-snapshotting.jpg b/docs/_images/redis/redis-snapshotting.jpg
deleted file mode 100644
index 58e787fb1b..0000000000
Binary files a/docs/_images/redis/redis-snapshotting.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-snapshotting.png b/docs/_images/redis/redis-snapshotting.png
deleted file mode 100644
index 4a39a8868f..0000000000
Binary files a/docs/_images/redis/redis-snapshotting.png and /dev/null differ
diff --git a/docs/_images/redis/redis-stream-cg.jpg b/docs/_images/redis/redis-stream-cg.jpg
deleted file mode 100644
index 7d78098f05..0000000000
Binary files a/docs/_images/redis/redis-stream-cg.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-stream-cg.png b/docs/_images/redis/redis-stream-cg.png
deleted file mode 100644
index 4dba663ec1..0000000000
Binary files a/docs/_images/redis/redis-stream-cg.png and /dev/null differ
diff --git a/docs/_images/redis/redis-stream.png b/docs/_images/redis/redis-stream.png
deleted file mode 100644
index a46e76e04f..0000000000
Binary files a/docs/_images/redis/redis-stream.png and /dev/null differ
diff --git a/docs/_images/redis/redis-string-length.jpg b/docs/_images/redis/redis-string-length.jpg
deleted file mode 100644
index 5f907a1214..0000000000
Binary files a/docs/_images/redis/redis-string-length.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-string.jpg b/docs/_images/redis/redis-string.jpg
deleted file mode 100644
index 3c4625ae76..0000000000
Binary files a/docs/_images/redis/redis-string.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-subscribe.jpg b/docs/_images/redis/redis-subscribe.jpg
deleted file mode 100644
index d8c5c8e3bd..0000000000
Binary files a/docs/_images/redis/redis-subscribe.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-subscribe.png b/docs/_images/redis/redis-subscribe.png
deleted file mode 100644
index 510f5146e8..0000000000
Binary files a/docs/_images/redis/redis-subscribe.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-case1.png b/docs/_images/redis/redis-transaction-case1.png
deleted file mode 100644
index 33c0c28b86..0000000000
Binary files a/docs/_images/redis/redis-transaction-case1.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-case2.png b/docs/_images/redis/redis-transaction-case2.png
deleted file mode 100644
index bb6478bf6b..0000000000
Binary files a/docs/_images/redis/redis-transaction-case2.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-case3.png b/docs/_images/redis/redis-transaction-case3.png
deleted file mode 100644
index dc8a3d47f1..0000000000
Binary files a/docs/_images/redis/redis-transaction-case3.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-case4.png b/docs/_images/redis/redis-transaction-case4.png
deleted file mode 100644
index 75e37e2cdc..0000000000
Binary files a/docs/_images/redis/redis-transaction-case4.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-client-cut.png b/docs/_images/redis/redis-transaction-client-cut.png
deleted file mode 100644
index a6c6cbc1ea..0000000000
Binary files a/docs/_images/redis/redis-transaction-client-cut.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-watch1.png b/docs/_images/redis/redis-transaction-watch1.png
deleted file mode 100644
index 168d241076..0000000000
Binary files a/docs/_images/redis/redis-transaction-watch1.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-watch2.png b/docs/_images/redis/redis-transaction-watch2.png
deleted file mode 100644
index 8a0e0fd888..0000000000
Binary files a/docs/_images/redis/redis-transaction-watch2.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-watch3.png b/docs/_images/redis/redis-transaction-watch3.png
deleted file mode 100644
index 9470a87ac0..0000000000
Binary files a/docs/_images/redis/redis-transaction-watch3.png and /dev/null differ
diff --git a/docs/_images/redis/redis-transaction-watch4.png b/docs/_images/redis/redis-transaction-watch4.png
deleted file mode 100644
index b903cb109b..0000000000
Binary files a/docs/_images/redis/redis-transaction-watch4.png and /dev/null differ
diff --git a/docs/_images/redis/redis-watch-client99.png b/docs/_images/redis/redis-watch-client99.png
deleted file mode 100644
index f27d10c410..0000000000
Binary files a/docs/_images/redis/redis-watch-client99.png and /dev/null differ
diff --git a/docs/_images/redis/redis-watch-key.png b/docs/_images/redis/redis-watch-key.png
deleted file mode 100644
index 0c4822bd24..0000000000
Binary files a/docs/_images/redis/redis-watch-key.png and /dev/null differ
diff --git a/docs/_images/redis/redis-watch.png b/docs/_images/redis/redis-watch.png
deleted file mode 100644
index f27d10c410..0000000000
Binary files a/docs/_images/redis/redis-watch.png and /dev/null differ
diff --git a/docs/_images/redis/redis-xgroup.jpg b/docs/_images/redis/redis-xgroup.jpg
deleted file mode 100644
index b1656875b2..0000000000
Binary files a/docs/_images/redis/redis-xgroup.jpg and /dev/null differ
diff --git a/docs/_images/redis/redis-zset-code.svg b/docs/_images/redis/redis-zset-code.svg
deleted file mode 100644
index 67bde867f4..0000000000
--- a/docs/_images/redis/redis-zset-code.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/_images/redis/redis-zset.gif b/docs/_images/redis/redis-zset.gif
deleted file mode 100644
index 826a5ceef4..0000000000
Binary files a/docs/_images/redis/redis-zset.gif and /dev/null differ
diff --git a/docs/_images/redis/rehash.gif b/docs/_images/redis/rehash.gif
deleted file mode 100644
index cc9edcc425..0000000000
Binary files a/docs/_images/redis/rehash.gif and /dev/null differ
diff --git a/docs/_images/redis/watched_keys.png b/docs/_images/redis/watched_keys.png
deleted file mode 100644
index 0c4822bd24..0000000000
Binary files a/docs/_images/redis/watched_keys.png and /dev/null differ
diff --git a/docs/_images/redis/zset.svg b/docs/_images/redis/zset.svg
deleted file mode 100644
index 67bde867f4..0000000000
--- a/docs/_images/redis/zset.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git "a/docs/architecture/\347\247\222\346\235\200\347\263\273\347\273\237\350\256\276\350\256\241.md" "b/docs/architecture/\347\247\222\346\235\200\347\263\273\347\273\237\350\256\276\350\256\241.md"
new file mode 100755
index 0000000000..bd47b14253
--- /dev/null
+++ "b/docs/architecture/\347\247\222\346\235\200\347\263\273\347\273\237\350\256\276\350\256\241.md"
@@ -0,0 +1,26 @@
+**秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统**。
+
+## 架构原则:“4 要 1 不要”
+
+1. 数据要尽量少
+
+ 所谓“数据要尽量少”,首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户 的数据(通常就是网页)。
+
+2. 请求数要尽量少
+3. 路径要尽量短
+4. 依赖要尽量少
+5. 不要有单点
+
+
+
+# 动静分离
+
+所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。
+
+**第一,你应该把静态数据缓存到离用户最近的地方**。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN 上或者在服务端的 Cache 中。你应该根据情况,把它们尽量缓存到离用户最近的地方。
+
+**第二,静态化改造就是要直接缓存 HTTP 连接**。相较于普通的数据缓存而言,你肯定还听过系统的静态化改造。静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据,如下图所示,Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。
+
+
+
+第三,让谁来缓存静态数据也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例,因为 Java 系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet 容器解析 HTTP 协议较慢),所以你可以不在 Java 层做缓存,而是直接在 Web 服务器层上做,这样你就可以屏蔽 Java 语言层面的一些弱点;而相比起来,Web 服务器(如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。
\ No newline at end of file
diff --git a/docs/data-management/.DS_Store b/docs/data-management/.DS_Store
index 669936cb49..c40a6e6781 100644
Binary files a/docs/data-management/.DS_Store and b/docs/data-management/.DS_Store differ
diff --git a/docs/data-management/Big-Data/.DS_Store b/docs/data-management/Big-Data/.DS_Store
index b0c3898aa2..b24b0adada 100644
Binary files a/docs/data-management/Big-Data/.DS_Store and b/docs/data-management/Big-Data/.DS_Store differ
diff --git a/docs/data-management/Big-Data/Bloom-Filter.md b/docs/data-management/Big-Data/Bloom-Filter.md
index a575e78e75..9bffa3d59d 100644
--- a/docs/data-management/Big-Data/Bloom-Filter.md
+++ b/docs/data-management/Big-Data/Bloom-Filter.md
@@ -387,11 +387,11 @@ public class RedissonBloomFilterDemo {
## 参考与感谢
-https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf
+- https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf
-http://www.justdojava.com/2019/10/22/bloomfilter/
+- http://www.justdojava.com/2019/10/22/bloomfilter/
-https://www.cnblogs.com/cpselvis/p/6265825.html
+- https://www.cnblogs.com/cpselvis/p/6265825.html
-https://juejin.im/post/5cc5aa7ce51d456e431adac5
+- https://juejin.im/post/5cc5aa7ce51d456e431adac5
diff --git a/docs/data-management/Big-Data/Doris.md b/docs/data-management/Big-Data/Doris.md
index c1a5e81b83..2a649d38a9 100644
--- a/docs/data-management/Big-Data/Doris.md
+++ b/docs/data-management/Big-Data/Doris.md
@@ -1,13 +1,14 @@
-当前大部分业务数据经过一定处理以后保存到 Hive (依托 HDFS 存储系统),使用 HiveSQL 进行离线数据分析,主要 MOLAP 引擎为 Kylin (MOLAP 是预计算生产,在增量业务,预设维度分析场景下表现良好),业务事务性分析使用 MySQL。有以下痛点:
+## 背景
-- 应用层模型复杂,根据业务需要以及Kylin生产需要,还要做较多模型预处理。这样在不同的业务场景中,模型的利用率也比较低。
-- Kylin配置过程繁琐,需要配置模型设计,并配合适当的“剪枝”策略,以实现计算成本与查询效率的平衡。
-- 由于MOLAP不支持明细数据的查询,在“汇总+明细”的应用场景中,明细数据需要同步到DBMS引擎来响应交互,增加了生产的运维成本。
-- 较多的预处理伴随着较高的生产成本。
+Doris 由百度大数据部研发 ( 之前叫百度 Palo,2018年贡献到 Apache 社区后,更名为 Apache Doris ), Doris 从最初的只为解决百度凤巢报表的专用系统,到现在在百度内部,已有有超过200个产品线在使用,部署机器超过1000台,单一业务最大可达到上百 TB。
+Apache Doris 作为一款开源的 MPP 分析型数据库产品,主要用于解决近实时的报表和多维分析。不仅能够在亚秒级响应时间即可获得查询结果,有效的支持实时数据分析。相较于其他业界比较火的 OLAP 数据库系统,Doris 的分布式架构非常简洁,支持弹性伸缩,易于运维,节省大量人力和时间成本。目前国内社区比较活跃,也有腾讯、京东、美团、小米等大厂在使用。
+## 功能特性
+
+
diff --git a/docs/data-management/Big-Data/HBase.md b/docs/data-management/Big-Data/HBase.md
index 8a6cd5db76..d34e43f028 100755
--- a/docs/data-management/Big-Data/HBase.md
+++ b/docs/data-management/Big-Data/HBase.md
@@ -1,11 +1,19 @@
+---
+title: HBase
+date: 2023-03-09
+tags:
+ - HBase
+categories: Big Data
+---
+
+
+
# 一、HBase 简介
### 1.1 HBase 定义
HBase 是一种分布式、可扩展、支持海量数据存储的 NoSQL 数据库。
-
-
### 1.2 HBase 的起源
HBase 是一个基于 HDFS 的分布式、面向列的开源数据库,是一个结构化数据的分布式存储系统,利用 HBase 技术可在廉价 PC Server上搭建起大规模结构化存储集群。
@@ -33,7 +41,7 @@ HBase 的原型是 Google 的 BigTable 论文,受到了该论文思想的启
### 1.4 特性
-Hbase是一种NoSQL数据库,这意味着它不像传统的RDBMS数据库那样支持SQL作为查询语言。Hbase是一种分布式存储的数据库,技术上来讲,它更像是分布式存储而不是分布式数据库,它缺少很多RDBMS系统的特性,比如列类型,辅助索引,触发器,和高级查询语言等待。那Hbase有什么特性呢?如下:
+Hbase是一种NoSQL数据库,这意味着它不像传统的RDBMS数据库那样支持SQL作为查询语言。Hbase是一种分布式存储的数据库,技术上来讲,它更像是分布式存储而不是分布式数据库,它缺少很多RDBMS系统的特性,比如列类型,辅助索引,触发器,和高级查询语言等。那Hbase有什么特性呢?如下:
- 强读写一致,但是不是“最终一致性”的数据存储,这使得它非常适合高速的计算聚合
- 自动分片,通过Region分散在集群中,当行数增长的时候,Region也会自动的切分和再分配
@@ -57,21 +65,19 @@ Hbase在单机环境也能运行,但是请在开发环境的时候使用
-
-
### 1.6 HBase 数据模型
逻辑上,HBase 的数据模型同关系型数据库很类似,数据存储在一张表中,有行有列。但从 HBase 的底层物理存储结构(K-V)来看,HBase 更像是一个 **multi-dimensional map**(多维度map)。
#### 1.6.1 HBase逻辑结构
-
+
> 通过横向切分 Region 和纵向切分 列族 来存储大数据
#### 1.6.2 HBase 物理存储结构
-
+
@@ -128,7 +134,7 @@ Region 是基于 HDFS 的,它的所有数据存取操作都是调用了 HDFS
### 1.8 HBase基本架构
-
+
- Zookeeper,作为分布式的协调。RegionServer也会把自己的信息写到ZooKeeper中。
- HDFS是Hbase运行的底层文件系统
@@ -139,7 +145,7 @@ Region 是基于 HDFS 的,它的所有数据存取操作都是调用了 HDFS
详细点的:
-
+
架构角色:
@@ -173,7 +179,7 @@ HDFS为Hbase提供最终的底层数据存储服务,同时为HBase提供高可
#### 架构细化
-
+
- HMaster是Master Server的实现,负责监控集群中的RegionServer实例,同时是所有metadata改变的接口,在集群中,通常运行在NameNode上面,[这里有一篇更细的HMaster介绍](https://links.jianshu.com/go?to=http%3A%2F%2Fblog.zahoor.in%2F2012%2F08%2Fhbase-hmaster-architecture%2F)
- HMasterInterface暴露的接口,Table(createTable, modifyTable, removeTable, enable, disable),ColumnFamily (addColumn, modifyColumn, removeColumn),Region (move, assign, unassign)
@@ -197,7 +203,7 @@ https://hbase.apache.org/book.html#quickstart
#### 3.1 Hbase中RegionServer架构
-
+
##### 1)StoreFile
@@ -224,7 +230,7 @@ WAL的检查间隔由hbase.regionserver.logroll.period定义,默认值为1小
#### 一、写数据流程
-
+
写流程:
@@ -274,7 +280,7 @@ memstore满的时候,或触发了其他的flush条件,memstore中的数据
#### 二、读取数据流程
-
+
读流程
diff --git a/docs/data-management/Big-Data/Kylin.md b/docs/data-management/Big-Data/Kylin.md
index fd828602e9..e6dc3c4dad 100644
--- a/docs/data-management/Big-Data/Kylin.md
+++ b/docs/data-management/Big-Data/Kylin.md
@@ -1,4 +1,10 @@
-# Kylin
+---
+title: Kylin
+date: 2023-03-09
+tags:
+ - OLAP
+categories: OLAP
+---

@@ -18,7 +24,7 @@
在这种情况下,Apache Kylin 应运而生。不同于“大规模并行处理”(Massive Parallel Processing,MPP)架构的 Hive、Presto 等,Apache Kylin 采用“**预计算**”的模式,用户只需要提前定义好查询维度,Kylin 将帮助我们进行计算,并将结果存储到 **HBase** 中,为海量数据的查询和分析提供亚秒级返回,是一种典型的“**空间换时间**”的解决方案。Apache Kylin 的出现不仅很好地解决了海量数据快速查询的问题,也避免了手动开发和维护提前计算程序带来的一系列麻烦。
-Apache Kylin 最初由 eBay 公司开发,并贡献给 Apache 基金会,但是目前 Apache Kylin 的核心开发团队已经自立门户,创建了Kyligence 公司。值得一提的是,Apache Kylin 是第一个由中国人主导的Apache顶级项目(2017 年 4 月 19 日,华为的 CarbonData 成为 Apache 顶级项目,因此 Apache Kylin 不再是唯一由国人贡献的 Apache 顶级项目)。由于互联网技术和开源思想进入我国的时间较晚,开源软件的世界一直是由西方国家主导,在数据领域也不例外。从 Hadoop 到 Spark,再到最近大热的机器学习平台 TenserFlow 等,均是如此。但近些年来,我们很欣喜地看到以 Apache Kylin 为首的各种以国人主导的开源项目不断地涌现出来,这些技术不断缩小着我国与西方开源技术强国之间的差距,提升我国技术人员在国际开源社区的影响力。
+Apache Kylin 最初由 eBay 公司开发,并贡献给 Apache 基金会,但是目前 Apache Kylin 的核心开发团队已经自立门户,创建了 Kyligence 公司。值得一提的是,Apache Kylin 是第一个由中国人主导的 Apache 顶级项目(2017 年 4 月 19 日,华为的 CarbonData 成为 Apache 顶级项目,因此 Apache Kylin 不再是唯一由国人贡献的 Apache 顶级项目)。由于互联网技术和开源思想进入我国的时间较晚,开源软件的世界一直是由西方国家主导,在数据领域也不例外。从 Hadoop 到 Spark,再到最近大热的机器学习平台 TenserFlow 等,均是如此。但近些年来,我们很欣喜地看到以 Apache Kylin 为首的各种以国人主导的开源项目不断地涌现出来,这些技术不断缩小着我国与西方开源技术强国之间的差距,提升我国技术人员在国际开源社区的影响力。
## 一、核心概念
@@ -44,11 +50,11 @@ OLAP(Online Analytical Process),联机分析处理,以多维度的方式
简单地说,维度就是观察数据的角度。比如传感器的采集数据,可以从时间的维度来观察:
-
+
也可以进一步细化,从时间和设备两个角度观察:
-
+
**维度**一般是离散的值,比如时间维度上的每一个独立的日期,或者设备维度上的每一个独立的设备。因此统计时可以把维度相同的记录聚合在一起,然后应用聚合函数做累加、均值、最大值、最小值等聚合计算。
@@ -140,7 +146,7 @@ Apache Kylin 的这种架构使得它拥有许多非常棒的特性:
### 参考与感谢:
-- 原文:[《一文读懂Apache Kylin》](https://www.jianshu.com/p/abd5e90ab051):
+- 原文:[《一文读懂Apache Kylin》](https://www.jianshu.com/p/abd5e90ab051)
- [《Apache Kylin 在百度地图的实践》](https://www.infoq.cn/article/practis-of-apache-kylin-in-baidu-map/)
- 美团技术团队:[Apache Kylin的实践与优化](https://tech.meituan.com/2020/11/19/apache-kylin-practice-in-meituan.html)
- [【硬刚Kylin】Kylin入门/原理/调优/OLAP解决方案和行业典型应用](https://www.modb.pro/db/79232)
diff --git a/docs/data-management/MySQL/.DS_Store b/docs/data-management/MySQL/.DS_Store
index 6d9ec48527..62c3b6dcb5 100644
Binary files a/docs/data-management/MySQL/.DS_Store and b/docs/data-management/MySQL/.DS_Store differ
diff --git a/docs/data-management/MySQL/MySQL-FAQ.md b/docs/data-management/MySQL/MySQL-FAQ.md
deleted file mode 100644
index cc56e8ffe4..0000000000
--- a/docs/data-management/MySQL/MySQL-FAQ.md
+++ /dev/null
@@ -1,1705 +0,0 @@
-
-
-> 写在之前:不建议那种上来就是各种面试题罗列,然后背书式的去记忆,对技术的提升帮助很小,对正经面试也没什么帮助,有点东西的面试官深挖下就懵逼了。
->
-> 个人建议把面试题看作是费曼学习法中的回顾、简化的环节,准备面试的时候,跟着题目先自己讲给自己听,看看自己会满意吗,不满意就继续学习这个点,如此反复,好的offer离你不远的,奥利给
-
-## 一、MySQL架构
-
-和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,**插件式的存储引擎架构将查询处理和其它的系统任务以及数据的存储提取相分离**。这种架构可以根据业务的需求和实际需要选择合适的存储引擎。
-
-
-
-
-
-- **连接层**:最上层是一些客户端和连接服务。**主要完成一些类似于连接处理、授权认证、及相关的安全方案**。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
-
-- **服务层**:第二层服务层,主要完成大部分的核心服务功能, 包括查询解析、分析、优化、缓存、以及所有的内置函数,所有跨存储引擎的功能也都在这一层实现,包括触发器、存储过程、视图等
-
-- **引擎层**:第三层存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取
-
-- **存储层**:第四层为数据存储层,主要是将数据存储在运行于该设备的文件系统之上,并完成与存储引擎的交互
-
-
-
-> 画出 MySQL 架构图,这种变态问题都能问的出来
->
-> MySQL 的查询流程具体是?or 一条SQL语句在MySQL中如何执行的?
->
-
-客户端请求 ---> 连接器(验证用户身份,给予权限) ---> 查询缓存(存在缓存则直接返回,不存在则执行后续操作) ---> 分析器(对SQL进行词法分析和语法分析操作) ---> 优化器(主要对执行的sql优化选择最优的执行方案方法) ---> 执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口) ---> 去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)
-
-
-
-------
-
-
-
-> 说说MySQL有哪些存储引擎?都有哪些区别?
-
-## 二、存储引擎
-
-存储引擎是MySQL的组件,用于处理不同表类型的SQL操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。
-
-使用哪一种引擎可以灵活选择,**一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求**,使用合适的存储引擎,将会提高整个数据库的性能 。
-
- MySQL服务器使用**可插拔**的存储引擎体系结构,可以从运行中的 MySQL 服务器加载或卸载存储引擎 。
-
-### 查看存储引擎
-
-```mysql
--- 查看支持的存储引擎
-SHOW ENGINES
-
--- 查看默认存储引擎
-SHOW VARIABLES LIKE 'storage_engine'
-
---查看具体某一个表所使用的存储引擎,这个默认存储引擎被修改了!
-show create table tablename
-
---准确查看某个数据库中的某一表所使用的存储引擎
-show table status like 'tablename'
-show table status from database where name="tablename"
-```
-
-### 设置存储引擎
-
-```mysql
--- 建表时指定存储引擎。默认的就是INNODB,不需要设置
-CREATE TABLE t1 (i INT) ENGINE = INNODB;
-CREATE TABLE t2 (i INT) ENGINE = CSV;
-CREATE TABLE t3 (i INT) ENGINE = MEMORY;
-
--- 修改存储引擎
-ALTER TABLE t ENGINE = InnoDB;
-
--- 修改默认存储引擎,也可以在配置文件my.cnf中修改默认引擎
-SET default_storage_engine=NDBCLUSTER;
-```
-
-默认情况下,每当 `CREATE TABLE` 或 `ALTER TABLE` 不能使用默认存储引擎时,都会生成一个警告。为了防止在所需的引擎不可用时出现令人困惑的意外行为,可以启用 `NO_ENGINE_SUBSTITUTION SQL` 模式。如果所需的引擎不可用,则此设置将产生错误而不是警告,并且不会创建或更改表
-
-
-
-### 存储引擎对比
-
-常见的存储引擎就 InnoDB、MyISAM、Memory、NDB。
-
-InnoDB 现在是 MySQL 默认的存储引擎,支持**事务、行级锁定和外键**
-
-#### 文件存储结构对比
-
-在 MySQL中建立任何一张数据表,在其数据目录对应的数据库目录下都有对应表的 `.frm` 文件,`.frm` 文件是用来保存每个数据表的元数据(meta)信息,包括表结构的定义等,与数据库存储引擎无关,也就是任何存储引擎的数据表都必须有`.frm`文件,命名方式为 数据表名.frm,如user.frm。
-
-查看MySQL 数据保存在哪里:`show variables like 'data%'`
-
-MyISAM 物理文件结构为:
-
-- `.frm`文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等
-- `.MYD` (`MYData`) 文件:MyISAM 存储引擎专用,用于存储MyISAM 表的数据
-- `.MYI` (`MYIndex`)文件:MyISAM 存储引擎专用,用于存储MyISAM 表的索引相关信息
-
-InnoDB 物理文件结构为:
-
-- `.frm` 文件:与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等
-
-- `.ibd` 文件或 `.ibdata` 文件: 这两种文件都是存放 InnoDB 数据的文件,之所以有两种文件形式存放 InnoDB 的数据,是因为 InnoDB 的数据存储方式能够通过配置来决定是使用**共享表空间**存放存储数据,还是用**独享表空间**存放存储数据。
-
- 独享表空间存储方式使用`.ibd`文件,并且每个表一个`.ibd`文件
- 共享表空间存储方式使用`.ibdata`文件,所有表共同使用一个`.ibdata`文件(或多个,可自己配置)
-
-> ps:正经公司,这些都有专业运维去做,数据备份、恢复啥的,让我一个 Javaer 搞这的话,加钱不?
-
-#### 面试这么回答
-
-1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
-2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
-3. InnoDB 是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
-4. InnoDB 不保存表的具体行数,执行` select count(*) from table` 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
-5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
-
-| 对比项 | MyISAM | InnoDB |
-| -------- | -------------------------------------------------------- | ------------------------------------------------------------ |
-| 主外键 | 不支持 | 支持 |
-| 事务 | 不支持 | 支持 |
-| 行表锁 | 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 | 行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作 |
-| 缓存 | 只缓存索引,不缓存真实数据 | 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 |
-| 表空间 | 小 | 大 |
-| 关注点 | 性能 | 事务 |
-| 默认安装 | 是 | 是 |
-
-
-
-> 一张表,里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把Mysql重启,再insert一条记录,这条记录的ID是18还是15 ?
-
-如果表的类型是MyISAM,那么是18。因为MyISAM表会把自增主键的最大ID 记录到数据文件中,重启MySQL自增主键的最大ID也不会丢失;
-
-如果表的类型是InnoDB,那么是15。因为InnoDB 表只是把自增主键的最大ID记录到内存中,所以重启数据库或对表进行OPTION操作,都会导致最大ID丢失。
-
-> 哪个存储引擎执行 select count(*) 更快,为什么?
-
-MyISAM更快,因为MyISAM内部维护了一个计数器,可以直接调取。
-
-- 在 MyISAM 存储引擎中,把表的总行数存储在磁盘上,当执行 select count(\*) from t 时,直接返回总数据。
-
-- 在 InnoDB 存储引擎中,跟 MyISAM 不一样,没有将总行数存储在磁盘上,当执行 select count(\*) from t 时,会先把数据读出来,一行一行的累加,最后返回总数量。
-
-InnoDB 中 count(\*) 语句是在执行的时候,全表扫描统计总数量,所以当数据越来越大时,语句就越来越耗时了,为什么 InnoDB 引擎不像 MyISAM 引擎一样,将总行数存储到磁盘上?这跟 InnoDB 的事务特性有关,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的。
-
-
-
-## 三、数据类型
-
-主要包括以下五大类:
-
-- 整数类型:BIT、BOOL、TINY INT、SMALL INT、MEDIUM INT、 INT、 BIG INT
-- 浮点数类型:FLOAT、DOUBLE、DECIMAL
-- 字符串类型:CHAR、VARCHAR、TINY TEXT、TEXT、MEDIUM TEXT、LONGTEXT、TINY BLOB、BLOB、MEDIUM BLOB、LONG BLOB
-- 日期类型:Date、DateTime、TimeStamp、Time、Year
-- 其他数据类型:BINARY、VARBINARY、ENUM、SET、Geometry、Point、MultiPoint、LineString、MultiLineString、Polygon、GeometryCollection等
-
-
-
-
-
-
-
-
-
-> CHAR 和 VARCHAR 的区别?
-
-char是固定长度,varchar长度可变:
-
-char(n) 和 varchar(n) 中括号中 n 代表字符的个数,并不代表字节个数,比如 CHAR(30) 就可以存储 30 个字符。
-
-存储时,前者不管实际存储数据的长度,直接按 char 规定的长度分配存储空间;而后者会根据实际存储的数据分配最终的存储空间
-
-相同点:
-
-1. char(n),varchar(n)中的n都代表字符的个数
-2. 超过char,varchar最大长度n的限制后,字符串会被截断。
-
-不同点:
-
-1. char不论实际存储的字符数都会占用n个字符的空间,而varchar只会占用实际字符应该占用的字节空间加1(实际长度length,0<=length<255)或加2(length>255)。因为varchar保存数据时除了要保存字符串之外还会加一个字节来记录长度(如果列声明长度大于255则使用两个字节来保存长度)。
-2. 能存储的最大空间限制不一样:char的存储上限为255字节。
-3. char在存储时会截断尾部的空格,而varchar不会。
-
-char是适合存储很短的、一般固定长度的字符串。例如,char非常适合存储密码的MD5值,因为这是一个定长的值。对于非常短的列,char比varchar在存储空间上也更有效率。
-
-
-
-> 列的字符串类型可以是什么?
-
-字符串类型是:SET、BLOB、ENUM、CHAR、CHAR、TEXT、VARCHAR
-
-
-
-> BLOB和TEXT有什么区别?
-
-BLOB是一个二进制对象,可以容纳可变数量的数据。有四种类型的BLOB:TINYBLOB、BLOB、MEDIUMBLO和 LONGBLOB
-
-TEXT是一个不区分大小写的BLOB。四种TEXT类型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。
-
-BLOB 保存二进制数据,TEXT 保存字符数据。
-
-------
-
-
-
-## 四、索引
-
->说说你对 MySQL 索引的理解?
->
->数据库索引的原理,为什么要用 B+树,为什么不用二叉树?
->
->聚集索引与非聚集索引的区别?
->
->InnoDB引擎中的索引策略,了解过吗?
->
->创建索引的方式有哪些?
->
->聚簇索引/非聚簇索引,mysql索引底层实现,为什么不用B-tree,为什么不用hash,叶子结点存放的是数据还是指向数据的内存地址,使用索引需要注意的几个地方?
-
-- MYSQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构,所以说**索引的本质是:数据结构**
-
-- 索引的目的在于提高查询效率,可以类比字典、 火车站的车次表、图书的目录等 。
-
-- 可以简单的理解为“排好序的快速查找数据结构”,数据本身之外,**数据库还维护者一个满足特定查找算法的数据结构**,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。下图是一种可能的索引方式示例。
-
- 
-
- 左边的数据表,一共有两列七条记录,最左边的是数据记录的物理地址
-
- 为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值,和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在一定的复杂度内获取到对应的数据,从而快速检索出符合条件的记录。
-
-- 索引本身也很大,不可能全部存储在内存中,**一般以索引文件的形式存储在磁盘上**
-
-- 平常说的索引,没有特别指明的话,就是B+树(多路搜索树,不一定是二叉树)结构组织的索引。其中聚集索引,次要索引,覆盖索引,复合索引,前缀索引,唯一索引默认都是使用B+树索引,统称索引。此外还有哈希索引等。
-
-
-
-
-### 基本语法:
-
-- 创建:
-
- - 创建索引:`CREATE [UNIQUE] INDEX indexName ON mytable(username(length));`
-
- 如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length。
-
- - 修改表结构(添加索引):`ALTER table tableName ADD [UNIQUE] INDEX indexName(columnName)`
-
-- 删除:`DROP INDEX [indexName] ON mytable;`
-
-- 查看:`SHOW INDEX FROM table_name\G` --可以通过添加 \G 来格式化输出信息。
-
-- 使用ALERT命令
-
- - `ALTER TABLE tbl_name ADD PRIMARY KEY (column_list):` 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL。
- - `ALTER TABLE tbl_name ADD UNIQUE index_name (column_list` 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)。
- - `ALTER TABLE tbl_name ADD INDEX index_name (column_list)` 添加普通索引,索引值可出现多次。
- - `ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list)`该语句指定了索引为 FULLTEXT ,用于全文索引。
-
-
-
-### 优势
-
-- **提高数据检索效率,降低数据库IO成本**
-
-- **降低数据排序的成本,降低CPU的消耗**
-
-
-
-### 劣势
-
-- 索引也是一张表,保存了主键和索引字段,并指向实体表的记录,所以也需要占用内存
-- 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。
- 因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,
- 都会调整因为更新所带来的键值变化后的索引信息
-
-
-
-### MySQL索引分类
-
-#### 数据结构角度
-
-- B+树索引
-- Hash索引
-- Full-Text全文索引
-- R-Tree索引
-
-#### 从物理存储角度
-
-- 聚集索引(clustered index)
-
-- 非聚集索引(non-clustered index),也叫辅助索引(secondary index)
-
- 聚集索引和非聚集索引都是B+树结构
-
-#### 从逻辑角度
-
-- 主键索引:主键索引是一种特殊的唯一索引,不允许有空值
-- 普通索引或者单列索引:每个索引只包含单个列,一个表可以有多个单列索引
-- 多列索引(复合索引、联合索引):复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用复合索引时遵循最左前缀集合
-- 唯一索引或者非唯一索引
-- 空间索引:空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。
- MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建
-
-
-
-> 为什么MySQL 索引中用B+tree,不用B-tree 或者其他树,为什么不用 Hash 索引
->
-> 聚簇索引/非聚簇索引,MySQL 索引底层实现,叶子结点存放的是数据还是指向数据的内存地址,使用索引需要注意的几个地方?
->
-> 使用索引查询一定能提高查询的性能吗?为什么?
-
-### MySQL索引结构
-
-**首先要明白索引(index)是在存储引擎(storage engine)层面实现的,而不是server层面**。不是所有的存储引擎都支持所有的索引类型。即使多个存储引擎支持某一索引类型,它们的实现和行为也可能有所差别。
-
-#### B+Tree索引
-
-MyISAM 和 InnoDB 存储引擎,都使用 B+Tree的数据结构,它相对与 B-Tree结构,所有的数据都存放在叶子节点上,且把叶子节点通过指针连接到一起,形成了一条数据链表,以加快相邻数据的检索效率。
-
-**先了解下 B-Tree 和 B+Tree 的区别**
-
-##### B-Tree
-
-B-Tree是为磁盘等外存储设备设计的一种平衡查找树。
-
-系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。
-
-InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB 存储引擎中默认每个页的大小为16KB,可通过参数 `innodb_page_size` 将页的大小设置为 4K、8K、16K,在 MySQL 中可通过如下命令查看页的大小:`show variables like 'innodb_page_size';`
-
-而系统一个磁盘块的存储空间往往没有这么大,因此 InnoDB 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16KB。InnoDB 在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。
-
-B-Tree 结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述 B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key值互不相同。
-
-一棵m阶的B-Tree有如下特性:
-1. 每个节点最多有m个孩子
-2. 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
-3. 若根节点不是叶子节点,则至少有2个孩子
-4. 所有叶子节点都在同一层,且不包含其它关键字信息
-5. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
-6. 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
-7. ki(i=1,…n)为关键字,且关键字升序排序
-8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)
-
-B-Tree 中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个 3 阶的 B-Tree:
-
-
-
-每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
-
-模拟查找关键字29的过程:
-
-1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
-2. 比较关键字29在区间(17,35),找到磁盘块1的指针P2。
-3. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
-4. 比较关键字29在区间(26,30),找到磁盘块3的指针P2。
-5. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
-6. 在磁盘块8中的关键字列表中找到关键字29。
-
-分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。
-
-##### B+Tree
-
-B+Tree 是在 B-Tree 基础上的一种优化,使其更适合实现外存储索引结构,InnoDB 存储引擎就是用 B+Tree 实现其索引结构。
-
-从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,**所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上**,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。
-
-B+Tree相对于B-Tree有几点不同:
-
-1. 非叶子节点只存储键值信息;
-2. 所有叶子节点之间都有一个链指针;
-3. 数据记录都存放在叶子节点中
-
-将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:
-
-
-通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
-
-可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:
-
-InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为10^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。
-
-实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。
-
-B+Tree性质
-
-1. 通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。
-2. 当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即**索引的最左匹配特性**。
-
-
-
-##### MyISAM主键索引与辅助索引的结构
-
-MyISAM引擎的索引文件和数据文件是分离的。**MyISAM引擎索引结构的叶子节点的数据域,存放的并不是实际的数据记录,而是数据记录的地址**。索引文件与数据文件分离,这样的索引称为"**非聚簇索引**"。MyISAM的主索引与辅助索引区别并不大,只是主键索引不能有重复的关键字。
-
-
-
-在MyISAM中,索引(含叶子节点)存放在单独的.myi文件中,叶子节点存放的是数据的物理地址偏移量(通过偏移量访问就是随机访问,速度很快)。
-
-主索引是指主键索引,键值不可能重复;辅助索引则是普通索引,键值可能重复。
-
-通过索引查找数据的流程:先从索引文件中查找到索引节点,从中拿到数据的文件指针,再到数据文件中通过文件指针定位了具体的数据。辅助索引类似。
-
-
-
-##### InnoDB主键索引与辅助索引的结构
-
-**InnoDB引擎索引结构的叶子节点的数据域,存放的就是实际的数据记录**(对于主索引,此处会存放表中所有的数据记录;对于辅助索引此处会引用主键,检索的时候通过主键到主键索引中找到对应数据行),或者说,**InnoDB的数据文件本身就是主键索引文件**,这样的索引被称为"“聚簇索引”,一个表只能有一个聚簇索引。
-
-###### 主键索引:
-
-我们知道InnoDB索引是聚集索引,它的索引和数据是存入同一个.idb文件中的,因此它的索引结构是在同一个树节点中同时存放索引和数据,如下图中最底层的叶子节点有三行数据,对应于数据表中的id、stu_id、name数据项。
-
-
-
-在Innodb中,索引分叶子节点和非叶子节点,非叶子节点就像新华字典的目录,单独存放在索引段中,叶子节点则是顺序排列的,在数据段中。Innodb的数据文件可以按照表来切分(只需要开启`innodb_file_per_table)`,切分后存放在`xxx.ibd`中,默认不切分,存放在`xxx.ibdata`中。
-
-###### 辅助(非主键)索引:
-
-这次我们以示例中学生表中的name列建立辅助索引,它的索引结构跟主键索引的结构有很大差别,在最底层的叶子结点有两行数据,第一行的字符串是辅助索引,按照ASCII码进行排序,第二行的整数是主键的值。
-
-这就意味着,对name列进行条件搜索,需要两个步骤:
-
-① 在辅助索引上检索name,到达其叶子节点获取对应的主键;
-
-② 使用主键在主索引上再进行对应的检索操作
-
-这也就是所谓的“**回表查询**”
-
-
-
-
-
-**InnoDB 索引结构需要注意的点**
-
-1. 数据文件本身就是索引文件
-
-2. 表数据文件本身就是按 B+Tree 组织的一个索引结构文件
-3. 聚集索引中叶节点包含了完整的数据记录
-4. InnoDB 表必须要有主键,并且推荐使用整型自增主键
-
-正如我们上面介绍 InnoDB 存储结构,索引与数据是共同存储的,不管是主键索引还是辅助索引,在查找时都是通过先查找到索引节点才能拿到相对应的数据,如果我们在设计表结构时没有显式指定索引列的话,MySQL 会从表中选择数据不重复的列建立索引,如果没有符合的列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,并且这个字段长度为6个字节,类型为整型。
-
-> 那为什么推荐使用整型自增主键而不是选择UUID?
-
-- UUID是字符串,比整型消耗更多的存储空间;
-
-- 在B+树中进行查找时需要跟经过的节点值比较大小,整型数据的比较运算比字符串更快速;
-
-- 自增的整型索引在磁盘中会连续存储,在读取一页数据时也是连续;UUID是随机产生的,读取的上下两行数据存储是分散的,不适合执行where id > 5 && id < 20的条件查询语句。
-
-- 在插入或删除数据时,整型自增主键会在叶子结点的末尾建立新的叶子节点,不会破坏左侧子树的结构;UUID主键很容易出现这样的情况,B+树为了维持自身的特性,有可能会进行结构的重构,消耗更多的时间。
-
-
-> 为什么非主键索引结构叶子节点存储的是主键值?
-
-保证数据一致性和节省存储空间,可以这么理解:商城系统订单表会存储一个用户ID作为关联外键,而不推荐存储完整的用户信息,因为当我们用户表中的信息(真实名称、手机号、收货地址···)修改后,不需要再次维护订单表的用户数据,同时也节省了存储空间。
-
-
-
-#### Hash索引
-
-- 主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。
-
- 检索算法:在检索查询时,就再次对待查关键字再次执行相同的Hash算法,得到Hash值,到对应Hash表对应位置取出数据即可,如果发生Hash碰撞,则需要在取值时进行筛选。目前使用Hash索引的数据库并不多,主要有Memory等。
-
- MySQL目前有Memory引擎和NDB引擎支持Hash索引。
-
-
-#### full-text全文索引
-
-- 全文索引也是MyISAM的一种特殊索引类型,主要用于全文索引,InnoDB从MYSQL5.6版本提供对全文索引的支持。
-
-- 它用于替代效率较低的LIKE模糊匹配操作,而且可以通过多字段组合的全文索引一次性全模糊匹配多个字段。
-- 同样使用B-Tree存放索引数据,但使用的是特定的算法,将字段数据分割后再进行索引(一般每4个字节一次分割),索引文件存储的是分割前的索引字符串集合,与分割后的索引信息,对应Btree结构的节点存储的是分割后的词信息以及它在分割前的索引字符串集合中的位置。
-
-#### R-Tree空间索引
-
-空间索引是MyISAM的一种特殊索引类型,主要用于地理空间数据类型
-
-
-
-> 为什么Mysql索引要用B+树不是B树?
-
-用B+树不用B树考虑的是IO对性能的影响,B树的每个节点都存储数据,而B+树只有叶子节点才存储数据,所以查找相同数据量的情况下,B树的高度更高,IO更频繁。数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。其中在MySQL底层对B+树进行进一步优化:在叶子节点中是双向链表,且在链表的头结点和尾节点也是循环指向的。
-
-
-
-> 面试官:为何不采用Hash方式?
-
-因为Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ Tree是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描。
-
-哈希索引不支持多列联合索引的最左匹配规则,如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题。
-
-
-
-### 哪些情况需要创建索引
-
-1. 主键自动建立唯一索引
-
-2. 频繁作为查询条件的字段
-
-3. 查询中与其他表关联的字段,外键关系建立索引
-
-4. 单键/组合索引的选择问题,高并发下倾向创建组合索引
-
-5. 查询中排序的字段,排序字段通过索引访问大幅提高排序速度
-
-6. 查询中统计或分组字段
-
-
-
-### 哪些情况不要创建索引
-
-1. 表记录太少
-2. 经常增删改的表
-3. 数据重复且分布均匀的表字段,只应该为最经常查询和最经常排序的数据列建立索引(如果某个数据类包含太多的重复数据,建立索引没有太大意义)
-4. 频繁更新的字段不适合创建索引(会加重IO负担)
-5. where条件里用不到的字段不创建索引
-
-
-
-### MySQL高效索引
-
-**覆盖索引**(Covering Index),或者叫索引覆盖, 也就是平时所说的不需要回表操作
-
-- 就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说**查询列要被所建的索引覆盖**。
-
-- 索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据,当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含(覆盖)满足查询结果的数据就叫做覆盖索引。
-
-- **判断标准**
-
- 使用explain,可以通过输出的extra列来判断,对于一个索引覆盖查询,显示为**using index**,MySQL查询优化器在执行查询前会决定是否有索引覆盖查询
-
-
-
-## 五、MySQL查询
-
-> count(*) 和 count(1)和count(列名)区别 ps:这道题说法有点多
-
-执行效果上:
-
-- count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL
-- count(1)包括了所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
-- count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。
-
-执行效率上:
-
-- 列名为主键,count(列名)会比count(1)快
-- 列名不为主键,count(1)会比count(列名)快
-- 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
-- 如果有主键,则 select count(主键)的执行效率是最优的
-- 如果表只有一个字段,则 select count(*) 最优。
-
-
-
-> MySQL中 in和 exists 的区别?
-
-- exists:exists对外表用loop逐条查询,每次查询都会查看exists的条件语句,当exists里的条件语句能够返回记录行时(无论记录行是的多少,只要能返回),条件就为真,返回当前loop到的这条记录;反之,如果exists里的条件语句不能返回记录行,则当前loop到的这条记录被丢弃,exists的条件就像一个bool条件,当能返回结果集则为true,不能返回结果集则为false
-- in:in查询相当于多个or条件的叠加
-
-```mysql
-SELECT * FROM A WHERE A.id IN (SELECT id FROM B);
-SELECT * FROM A WHERE EXISTS (SELECT * from B WHERE B.id = A.id);
-```
-
-**如果查询的两个表大小相当,那么用in和exists差别不大**。
-
-如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in:
-
-
-
-> UNION和UNION ALL的区别?
-
-UNION和UNION ALL都是将两个结果集合并为一个,**两个要联合的SQL语句 字段个数必须一样,而且字段类型要“相容”(一致);**
-
-- UNION在进行表连接后会筛选掉重复的数据记录(效率较低),而UNION ALL则不会去掉重复的数据记录;
-
-- UNION会按照字段的顺序进行排序,而UNION ALL只是简单的将两个结果合并就返回;
-
-
-
-### SQL执行顺序
-
-- 手写
-
- ```mysql
- SELECT DISTINCT
- FROM
- JOIN ON
- WHERE
- GROUP BY
- HAVING
- ORDER BY
- LIMIT
- ```
-
-- 机读
-
- ```mysql
- FROM
- ON
- JOIN
- WHERE
- GROUP BY
- HAVING
- SELECT
- DISTINCT
- ORDER BY
- LIMIT
- ```
-
-- 总结
-
- 
-
-
-
-> mysql 的内连接、左连接、右连接有什么区别?
->
-> 什么是内连接、外连接、交叉连接、笛卡尔积呢?
-
-### Join图
-
-
-
-------
-
-
-
-## 六、MySQL 事务
-
-> 事务的隔离级别有哪些?MySQL的默认隔离级别是什么?
->
-> 什么是幻读,脏读,不可重复读呢?
->
-> MySQL事务的四大特性以及实现原理
->
-> MVCC熟悉吗,它的底层原理?
-
-MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!
-
-
-
-### ACID — 事务基本要素
-
-
-
-事务是由一组SQL语句组成的逻辑处理单元,具有4个属性,通常简称为事务的ACID属性。
-
-- **A (Atomicity) 原子性**:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
-- **C (Consistency) 一致性**:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏
-- **I (Isolation)隔离性**:一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰
-- **D (Durability) 持久性**:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚
-
-
-
-**并发事务处理带来的问题**
-
-- 更新丢失(Lost Update): 事务A和事务B选择同一行,然后基于最初选定的值更新该行时,由于两个事务都不知道彼此的存在,就会发生丢失更新问题
-- 脏读(Dirty Reads):事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
-- 不可重复读(Non-Repeatable Reads):事务 A 多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
-- 幻读(Phantom Reads):幻读与不可重复读类似。它发生在一个事务A读取了几行数据,接着另一个并发事务B插入了一些数据时。在随后的查询中,事务A就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
-
-
-
-**幻读和不可重复读的区别:**
-
-- **不可重复读的重点是修改**:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改)
-- **幻读的重点在于新增或者删除**:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除)
-
-
-
-**并发事务处理带来的问题的解决办法:**
-
-- “更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
-
-- “脏读” 、 “不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决:
-
- - 一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
- - 另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 **MVCC** 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。
-
-
-
-### 事务隔离级别
-
-数据库事务的隔离级别有4种,由低到高分别为
-
-- **READ-UNCOMMITTED(读未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。
-- **READ-COMMITTED(读已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。
-- **REPEATABLE-READ(可重复读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。
-- **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。
-
-查看当前数据库的事务隔离级别:
-
-```mysql
-show variables like 'tx_isolation'
-```
-
-下面通过事例一一阐述在事务的并发操作中可能会出现脏读,不可重复读,幻读和事务隔离级别的联系。
-
-数据库的事务隔离越严格,并发副作用越小,但付出的代价就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
-
-#### Read uncommitted
-
-读未提交,就是一个事务可以读取另一个未提交事务的数据。
-
-事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
-
-分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读。
-
-那怎么解决脏读呢?Read committed!读提交,能解决脏读问题。
-
-#### Read committed
-
-读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
-
-事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…
-
-分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是**不可重复读**。
-
-那怎么解决可能的不可重复读问题?Repeatable read !
-
-#### Repeatable read
-
-重复读,就是在开始读取数据(事务开启)时,不再允许修改操作。 **MySQL的默认事务隔离级别**
-
-事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
-
-分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,**不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作**。
-
-**什么时候会出现幻读?**
-
-事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。
-
-那怎么解决幻读问题?Serializable!
-
-#### Serializable 序列化
-
-Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。简单来说,Serializable会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用问题。这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
-
-
-
-#### 比较
-
-| 事务隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
-| ---------------------------- | ---------------------------------------- | ---- | ---------- | ---- |
-| 读未提交(read-uncommitted) | 最低级被,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
-| 读已提交(read-committed) | 语句级 | 否 | 是 | 是 |
-| 可重复读(repeatable-read) | 事务级 | 否 | 否 | 是 |
-| 串行化(serializable) | 最高级别,事务级 | 否 | 否 | 否 |
-
-需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。
-
-
-
-MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`
-
-这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 **REPEATABLE-READ(可重读)**事务隔离级别下使用的是Next-Key Lock 算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 **SERIALIZABLE(可串行化)**隔离级别,而且保留了比较好的并发性能。
-
-因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是**READ-COMMITTED(读已提交):**,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。
-
-
-
-### MVCC 多版本并发控制
-
-MySQL的大多数事务型存储引擎实现都不是简单的行级锁。基于提升并发性考虑,一般都同时实现了多版本并发控制(MVCC),包括Oracle、PostgreSQL。只是实现机制各不相同。
-
-可以认为 MVCC 是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。
-
-MVCC 的实现是通过保存数据在某个时间点的快照来实现的。也就是说不管需要执行多长时间,每个事物看到的数据都是一致的。
-
-典型的MVCC实现方式,分为**乐观(optimistic)并发控制和悲观(pressimistic)并发控制**。下边通过 InnoDB的简化版行为来说明 MVCC 是如何工作的。
-
-InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列来实现。这两个列,一个保存了行的创建时间,一个保存行的过期时间(删除时间)。当然存储的并不是真实的时间,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
-
-**REPEATABLE READ(可重读)隔离级别下MVCC如何工作:**
-
-- SELECT
-
- InnoDB会根据以下两个条件检查每行记录:
-
- - InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在开始事务之前已经存在要么是事务自身插入或者修改过的
-
- - 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除
-
- 只有符合上述两个条件的才会被查询出来
-
-- INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号
-
-- DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识
-
-- UPDATE:InnoDB为插入的一行新纪录保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识
-
-
-保存这两个额外系统版本号,使大多数操作都不用加锁。使数据操作简单,性能很好,并且也能保证只会读取到符合要求的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。
-
-MVCC 只在 COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作。
-
-
-
-### 事务日志
-
-InnoDB 使用日志来减少提交事务时的开销。因为日志中已经记录了事务,就无须在每个事务提交时把缓冲池的脏块刷新(flush)到磁盘中。
-
-事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机 IO。
-
-InnoDB 假设使用常规磁盘,随机IO比顺序IO昂贵得多,因为一个IO请求需要时间把磁头移到正确的位置,然后等待磁盘上读出需要的部分,再转到开始位置。
-
-InnoDB 用日志把随机IO变成顺序IO。一旦日志安全写到磁盘,事务就持久化了,即使断电了,InnoDB可以重放日志并且恢复已经提交的事务。
-
-InnoDB 使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。
-
-事务日志可以帮助提高事务效率:
-
-- 使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。
-- 事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。
-- 事务日志持久以后,内存中被修改的数据在后台可以慢慢刷回到磁盘。
-- 如果数据的修改已经记录到事务日志并持久化,但数据本身没有写回到磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这一部分修改的数据。
-
-目前来说,大多数存储引擎都是这样实现的,我们通常称之为**预写式日志**(Write-Ahead Logging),修改数据需要写两次磁盘。
-
-
-
-### 事务的实现
-
-事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。MySQL 中支持事务的存储引擎有 InnoDB 和 NDB。
-
-事务的实现就是如何实现ACID特性。
-
-事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现 。
-
-
-
-> 事务是如何通过日志来实现的,说得越深入越好。
-
-事务日志包括:**重做日志redo**和**回滚日志undo**
-
-- **redo log(重做日志**) 实现持久化和原子性
-
- 在innoDB的存储引擎中,事务日志通过重做(redo)日志和innoDB存储引擎的日志缓冲(InnoDB Log Buffer)实现。事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的“日志先行”(Write-Ahead Logging)。当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。
-
- 在系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。所有的事务共享redo log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起。
-
-- **undo log(回滚日志)** 实现一致性
-
- undo log 主要为事务的回滚服务。在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
-
- Undo记录的是已部分完成并且写入硬盘的未完成的事务,默认情况下回滚日志是记录下表空间中的(共享表空间或者独享表空间)
-
-二种日志均可以视为一种恢复操作,redo_log是恢复提交事务修改的页操作,而undo_log是回滚行记录到特定版本。二者记录的内容也不同,redo_log是物理日志,记录页的物理修改操作,而undo_log是逻辑日志,根据每行记录进行记录。
-
-
-
-> 又引出个问题:你知道MySQL 有多少种日志吗?
-
-- **错误日志**:记录出错信息,也记录一些警告信息或者正确的信息。
-
-- **查询日志**:记录所有对数据库请求的信息,不论这些请求是否得到了正确的执行。
-
-- **慢查询日志**:设置一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询的日志文件中。
-
-- **二进制日志**:记录对数据库执行更改的所有操作。
-
-- **中继日志**:中继日志也是二进制日志,用来给slave 库恢复
-
-- **事务日志**:重做日志redo和回滚日志undo
-
-
-
-> 分布式事务相关问题,可能还会问到 2PC、3PC,,,
-
-### MySQL对分布式事务的支持
-
-分布式事务的实现方式有很多,既可以采用 InnoDB 提供的原生的事务支持,也可以采用消息队列来实现分布式事务的最终一致性。这里我们主要聊一下 InnoDB 对分布式事务的支持。
-
-MySQL 从 5.0.3 InnoDB 存储引擎开始支持XA协议的分布式事务。一个分布式事务会涉及多个行动,这些行动本身是事务性的。所有行动都必须一起成功完成,或者一起被回滚。
-
-在MySQL中,使用分布式事务涉及一个或多个资源管理器和一个事务管理器。
-
-
-
-如图,MySQL 的分布式事务模型。模型中分三块:应用程序(AP)、资源管理器(RM)、事务管理器(TM):
-
-- 应用程序:定义了事务的边界,指定需要做哪些事务;
-- 资源管理器:提供了访问事务的方法,通常一个数据库就是一个资源管理器;
-- 事务管理器:协调参与了全局事务中的各个事务。
-
-分布式事务采用两段式提交(two-phase commit)的方式:
-
-- 第一阶段所有的事务节点开始准备,告诉事务管理器ready。
-- 第二阶段事务管理器告诉每个节点是commit还是rollback。如果有一个节点失败,就需要全局的节点全部rollback,以此保障事务的原子性。
-
-------
-
-
-
-## 七、MySQL锁机制
-
-> 数据库的乐观锁和悲观锁?
->
-> MySQL 中有哪几种锁,列举一下?
->
-> MySQL中InnoDB引擎的行锁是怎么实现的?
->
-> MySQL 间隙锁有没有了解,死锁有没有了解,写一段会造成死锁的 sql 语句,死锁发生了如何解决,MySQL 有没有提供什么机制去解决死锁
-
-锁是计算机协调多个进程或线程并发访问某一资源的机制。
-
-在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。
-
-打个比方,我们到淘宝上买一件商品,商品只有一件库存,这个时候如果还有另一个人买,那么如何解决是你买到还是另一个人买到的问题?这里肯定要用到事物,我们先从库存表中取出物品数量,然后插入订单,付款后插入付款表信息,然后更新商品数量。在这个过程中,使用锁可以对有限的资源进行保护,解决隔离和并发的矛盾。
-
-
-
-### 锁的分类
-
-**从对数据操作的类型分类**:
-
-- **读锁**(共享锁):针对同一份数据,多个读操作可以同时进行,不会互相影响
-
-- **写锁**(排他锁):当前写操作没有完成前,它会阻断其他写锁和读锁
-
-**从对数据操作的粒度分类**:
-
-
-为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取,检查,释放锁等动作),因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念。
-
-- **表级锁**:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低(MyISAM 和 MEMORY 存储引擎采用的是表级锁);
-
-- **行级锁**:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高(InnoDB 存储引擎既支持行级锁也支持表级锁,但默认情况下是采用行级锁);
-
-- **页面锁**:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
-
-适用:从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
-
-| | 行锁 | 表锁 | 页锁 |
-| ------ | ---- | ---- | ---- |
-| MyISAM | | √ | |
-| BDB | | √ | √ |
-| InnoDB | √ | √ | |
-| Memory | | √ | |
-
-
-
-### MyISAM 表锁
-
-MyISAM 的表锁有两种模式:
-
-- 表共享读锁 (Table Read Lock):不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;
-- 表独占写锁 (Table Write Lock):会阻塞其他用户对同一表的读和写操作;
-
-MyISAM 表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后, 只有持有锁的线程可以对表进行更新操作。 其他线程的读、 写操作都会等待,直到锁被释放为止。
-
-默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,然后再给读锁队列中等候的获取锁请求。
-
-
-
-### InnoDB 行锁
-
-InnoDB 实现了以下两种类型的**行锁**:
-
-- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
-- 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
-
-为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是**表锁**:
-
-- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
-- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
-
-**索引失效会导致行锁变表锁**。比如 vchar 查询不写单引号的情况。
-
-#### 加锁机制
-
-**乐观锁与悲观锁是两种并发控制的思想,可用于解决丢失更新问题**
-
-乐观锁会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务。用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式
-
-悲观锁会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。另外与乐观锁相对应的,**悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。**
-
-
-
-#### 锁模式(InnoDB有三种行锁的算法)
-
-- **记录锁(Record Locks)**: 单个行记录上的锁。对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
-
- ```mysql
- SELECT * FROM table WHERE id = 1 FOR UPDATE;
- ```
-
- 它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行
-
- 在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:
-
- ```mysql
- -- id 列为主键列或唯一索引列
- UPDATE SET age = 50 WHERE id = 1;
- ```
-
-- **间隙锁(Gap Locks)**: 当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。对于键值在条件范围内但并不存在的记录,叫做“间隙”。
-
- InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
-
- 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。
-
- 间隙锁基于非唯一索引,它锁定一段范围内的索引记录。间隙锁基于下面将会提到的`Next-Key Locking` 算法,请务必牢记:**使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据**。
-
- ```mysql
- SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;
- ```
-
- 即所有在`(1,10)`区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。
-
- GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况
-
-- **临键锁(Next-key Locks)**: **临键锁**,是**记录锁与间隙锁的组合**,它的封锁范围,既包含索引记录,又包含索引区间。(临键锁的主要目的,也是为了避免**幻读**(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。)
-
- Next-Key 可以理解为一种特殊的**间隙锁**,也可以理解为一种特殊的**算法**。通过**临建锁**可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,`InnoDB` 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
-
- 对于行的查询,都是采用该方法,主要目的是解决幻读的问题。
-
-> select for update有什么含义,会锁表还是锁行还是其他
-
-for update 仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。
-
-InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
-假设有个表单 products ,里面有id跟name二个栏位,id是主键。
-
-- 明确指定主键,并且有此笔资料,row lock
-
-```mysql
-SELECT * FROM products WHERE id='3' FOR UPDATE;
-SELECT * FROM products WHERE id='3' and type=1 FOR UPDATE;
-```
-
-- 明确指定主键,若查无此笔资料,无lock
-
-```mysql
-SELECT * FROM products WHERE id='-1' FOR UPDATE;
-```
-
-- 无主键,table lock
-
-```mysql
-SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
-```
-
-- 主键不明确,table lock
-
-```mysql
-SELECT * FROM products WHERE id<>'3' FOR UPDATE;
-```
-
-- 主键不明确,table lock
-
-```mysql
-SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
-```
-
-**注1**: FOR UPDATE仅适用于InnoDB,且必须在交易区块(BEGIN/COMMIT)中才能生效。
-**注2**: 要测试锁定的状况,可以利用MySQL的Command Mode ,开二个视窗来做测试。
-
-
-
-> MySQL 遇到过死锁问题吗,你是如何解决的?
-
-### 死锁
-
-**死锁产生**:
-
-- 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环
-- 当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁
-- 锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁有些不会——死锁有双重原因:真正的数据冲突;存储引擎的实现方式。
-
-**检测死锁**:数据库系统实现了各种死锁检测和死锁超时的机制。InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误。
-
-**死锁恢复**:死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。所以事务型应用程序在设计时必须考虑如何处理死锁,多数情况下只需要重新执行因死锁回滚的事务即可。
-
-**外部锁的死锁检测**:发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决
-
-**死锁影响性能**:死锁会影响性能而不是会产生严重错误,因为InnoDB会自动检测死锁状况并回滚其中一个受影响的事务。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。 有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖`innodb_lock_wait_timeout`设置进行事务回滚。
-
-
-
-**MyISAM避免死锁**:
-
-- 在自动加锁的情况下,MyISAM 总是一次获得 SQL 语句所需要的全部锁,所以 MyISAM 表不会出现死锁。
-
-**InnoDB避免死锁**:
-
-- 为了在单个InnoDB表上执行多个并发写入操作时避免死锁,可以在事务开始时通过为预期要修改的每个元祖(行)使用`SELECT ... FOR UPDATE`语句来获取必要的锁,即使这些行的更改语句是在之后才执行的。
-- 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁、更新时再申请排他锁,因为这时候当用户再申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁
-- 如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会
-- 通过`SELECT ... LOCK IN SHARE MODE`获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。
-- 改变事务隔离级别
-
-如果出现死锁,可以用 `show engine innodb status; `命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的 SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。
-
-------
-
-
-
-## 八、MySQL调优
-
-> 日常工作中你是怎么优化SQL的?
->
-> SQL优化的一般步骤是什么,怎么看执行计划(explain),如何理解其中各个字段的含义?
->
-> 如何写sql能够有效的使用到复合索引?
->
-> 一条sql执行过长的时间,你如何优化,从哪些方面入手?
->
-> 什么是最左前缀原则?什么是最左匹配原则?
-
-### 影响mysql的性能因素
-
-- 业务需求对MySQL的影响(合适合度)
-
-- 存储定位对MySQL的影响
- - 不适合放进MySQL的数据
- - 二进制多媒体数据
- - 流水队列数据
- - 超大文本数据
- - 需要放进缓存的数据
- - 系统各种配置及规则数据
- - 活跃用户的基本信息数据
- - 活跃用户的个性化定制信息数据
- - 准实时的统计信息数据
- - 其他一些访问频繁但变更较少的数据
-
-- Schema设计对系统的性能影响
- - 尽量减少对数据库访问的请求
- - 尽量减少无用数据的查询请求
-
-- 硬件环境对系统性能的影响
-
-
-
-### 性能分析
-
-#### MySQL Query Optimizer
-
-1. MySQL 中有专门负责优化 SELECT 语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的 Query 提供他认为最优的执行计划(他认为最优的数据检索方式,但不见得是 DBA 认为是最优的,这部分最耗费时间)
-
-2. 当客户端向 MySQL 请求一条 Query,命令解析器模块完成请求分类,区别出是 SELECT 并转发给 MySQL Query Optimize r时,MySQL Query Optimizer 首先会对整条 Query 进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对 Query 中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析 Query 中的 Hint 信息(如果有),看显示 Hint 信息是否可以完全确定该 Query 的执行计划。如果没有 Hint 或 Hint 信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据 Query 进行写相应的计算分析,然后再得出最后的执行计划。
-
-#### MySQL常见瓶颈
-
-- CPU:CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候
-
-- IO:磁盘I/O瓶颈发生在装入数据远大于内存容量的时候
-
-- 服务器硬件的性能瓶颈:top,free,iostat 和 vmstat来查看系统的性能状态
-
-#### 性能下降SQL慢 执行时间长 等待时间长 原因分析
-
-- 查询语句写的烂
-- 索引失效(单值、复合)
-- 关联查询太多join(设计缺陷或不得已的需求)
-- 服务器调优及各个参数设置(缓冲、线程数等)
-
-
-
-#### MySQL常见性能分析手段
-
-在优化MySQL时,通常需要对数据库进行分析,常见的分析手段有**慢查询日志**,**EXPLAIN 分析查询**,**profiling分析**以及**show命令查询系统状态及系统变量**,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。
-
-##### 性能瓶颈定位
-
-我们可以通过 show 命令查看 MySQL 状态及变量,找到系统的瓶颈:
-
-```mysql
-Mysql> show status ——显示状态信息(扩展show status like ‘XXX’)
-
-Mysql> show variables ——显示系统变量(扩展show variables like ‘XXX’)
-
-Mysql> show innodb status ——显示InnoDB存储引擎的状态
-
-Mysql> show processlist ——查看当前SQL执行,包括执行状态、是否锁表等
-
-Shell> mysqladmin variables -u username -p password——显示系统变量
-
-Shell> mysqladmin extended-status -u username -p password——显示状态信息
-```
-
-
-
-##### Explain(执行计划)
-
-是什么:使用 **Explain** 关键字可以模拟优化器执行SQL查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的。分析你的查询语句或是表结构的性能瓶颈
-
-能干吗:
-- 表的读取顺序
-- 数据读取操作的操作类型
-- 哪些索引可以使用
-- 哪些索引被实际使用
-- 表之间的引用
-- 每张表有多少行被优化器查询
-
-怎么玩:
-
-- Explain + SQL语句
-- 执行计划包含的信息(如果有分区表的话还会有**partitions**)
-
-
-
-各字段解释
-
-- **id**(select 查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序)
-
- - id相同,执行顺序从上往下
- - id全不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
- - id部分相同,执行顺序是先按照数字大的先执行,然后数字相同的按照从上往下的顺序执行
-
-- **select_type**(查询类型,用于区别普通查询、联合查询、子查询等复杂查询)
-
- - **SIMPLE** :简单的select查询,查询中不包含子查询或UNION
- - **PRIMARY**:查询中若包含任何复杂的子部分,最外层查询被标记为PRIMARY
- - **SUBQUERY**:在select或where列表中包含了子查询
- - **DERIVED**:在from列表中包含的子查询被标记为DERIVED,MySQL会递归执行这些子查询,把结果放在临时表里
- - **UNION**:若第二个select出现在UNION之后,则被标记为UNION,若UNION包含在from子句的子查询中,外层select将被标记为DERIVED
- - **UNION RESULT**:从UNION表获取结果的select
-
-- **table**(显示这一行的数据是关于哪张表的)
-
-- **type**(显示查询使用了那种类型,从最好到最差依次排列 **system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL** )
-
- - system:表只有一行记录(等于系统表),是 const 类型的特例,平时不会出现
- - const:表示通过索引一次就找到了,const 用于比较 primary key 或 unique 索引,因为只要匹配一行数据,所以很快,如将主键置于 where 列表中,mysql 就能将该查询转换为一个常量
- - eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描
- - ref:非唯一性索引扫描,范围匹配某个单独值得所有行。本质上也是一种索引访问,他返回所有匹配某个单独值的行,然而,它可能也会找到多个符合条件的行,多以他应该属于查找和扫描的混合体
- - range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引,一般就是在你的where语句中出现了between、<、>、in等的查询,这种范围扫描索引比全表扫描要好,因为它只需开始于索引的某一点,而结束于另一点,不用扫描全部索引
- - index:Full Index Scan,index于ALL区别为index类型只遍历索引树。通常比ALL快,因为索引文件通常比数据文件小。(**也就是说虽然all和index都是读全表,但index是从索引中读取的,而all是从硬盘中读的**)
- - ALL:Full Table Scan,将遍历全表找到匹配的行
-
- tip: 一般来说,得保证查询至少达到range级别,最好到达ref
-
-- **possible_keys**(显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段若存在索引,则该索引将被列出,但不一定被查询实际使用)
-
-- **key**
-
- - 实际使用的索引,如果为NULL,则没有使用索引
-
- - **查询中若使用了覆盖索引,则该索引和查询的 select 字段重叠,仅出现在key列表中**
-
-
-
-- **key_len**
-
- - 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好
- - key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的
-
-- **ref** (显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值)
-
-- **rows** (根据表统计信息及索引选用情况,大致估算找到所需的记录所需要读取的行数)
-
-- **Extra**(包含不适合在其他列中显示但十分重要的额外信息)
-
- 1. using filesort: 说明mysql会对数据使用一个外部的索引排序,不是按照表内的索引顺序进行读取。mysql中无法利用索引完成的排序操作称为“文件排序”。常见于order by和group by语句中
-
- 2. Using temporary:使用了临时表保存中间结果,mysql在对查询结果排序时使用临时表。常见于排序order by和分组查询group by。
-
- 3. using index:表示相应的select操作中使用了覆盖索引,避免访问了表的数据行,效率不错,如果同时出现using where,表明索引被用来执行索引键值的查找;否则索引被用来读取数据而非执行查找操作
-
- 4. using where:使用了where过滤
-
- 5. using join buffer:使用了连接缓存
-
- 6. impossible where:where子句的值总是false,不能用来获取任何元祖
-
- 7. select tables optimized away:在没有group by子句的情况下,基于索引优化操作或对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化
-
- 8. distinct:优化distinct操作,在找到第一匹配的元祖后即停止找同样值的动作
-
-
-
-**case**:
-
-
-
-1. 第一行(执行顺序4):id列为1,表示是union里的第一个select,select_type列的primary表示该查询为外层查询,table列被标记为,表示查询结果来自一个衍生表,其中derived3中3代表该查询衍生自第三个select查询,即id为3的select。【select d1.name......】
-
-2. 第二行(执行顺序2):id为3,是整个查询中第三个select的一部分。因查询包含在from中,所以为derived。【select id,name from t1 where other_column=''】
-3. 第三行(执行顺序3):select列表中的子查询select_type为subquery,为整个查询中的第二个select。【select id from t3】
-4. 第四行(执行顺序1):select_type为union,说明第四个select是union里的第二个select,最先执行【select name,id from t2】
-5. 第五行(执行顺序5):代表从union的临时表中读取行的阶段,table列的表示用第一个和第四个select的结果进行union操作。【两个结果union操作】
-
-
-
-##### 慢查询日志
-
-MySQL 的慢查询日志是 MySQL 提供的一种日志记录,它用来记录在 MySQL 中响应时间超过阈值的语句,具体指运行时间超过 `long_query_time` 值的 SQL,则会被记录到慢查询日志中。
-
-- `long_query_time` 的默认值为10,意思是运行10秒以上的语句
-- 默认情况下,MySQL数据库没有开启慢查询日志,需要手动设置参数开启
-
-**查看开启状态**
-
-```mysql
-SHOW VARIABLES LIKE '%slow_query_log%'
-```
-
-**开启慢查询日志**
-
-- 临时配置:
-
-```mysql
-mysql> set global slow_query_log='ON';
-mysql> set global slow_query_log_file='/var/lib/mysql/hostname-slow.log';
-mysql> set global long_query_time=2;
-```
-
- 也可set文件位置,系统会默认给一个缺省文件host_name-slow.log
-
- 使用set操作开启慢查询日志只对当前数据库生效,如果MySQL重启则会失效。
-
-- 永久配置
-
- 修改配置文件my.cnf或my.ini,在[mysqld]一行下面加入两个配置参数
-
-```mysql
-[mysqld]
-slow_query_log = ON
-slow_query_log_file = /var/lib/mysql/hostname-slow.log
-long_query_time = 3
-```
-
-注:log-slow-queries 参数为慢查询日志存放的位置,一般这个目录要有 MySQL 的运行帐号的可写权限,一般都将这个目录设置为 MySQL 的数据存放目录;long_query_time=2 中的 2 表示查询超过两秒才记录;在my.cnf或者 my.ini 中添加 log-queries-not-using-indexes 参数,表示记录下没有使用索引的查询。
-
-可以用 `select sleep(4)` 验证是否成功开启。
-
-在生产环境中,如果手工分析日志,查找、分析SQL,还是比较费劲的,所以MySQL提供了日志分析工具**mysqldumpslow**。
-
-通过 mysqldumpslow --help 查看操作帮助信息
-
-- 得到返回记录集最多的10个SQL
-
- `mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log`
-
-- 得到访问次数最多的10个SQL
-
- `mysqldumpslow -s c -t 10 /var/lib/mysql/hostname-slow.log`
-
-- 得到按照时间排序的前10条里面含有左连接的查询语句
-
- `mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/hostname-slow.log`
-
-- 也可以和管道配合使用
-
- `mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log | more`
-
-**也可使用 pt-query-digest 分析 RDS MySQL 慢查询日志**
-
-
-
-##### Show Profile 分析查询
-
-通过慢日志查询可以知道哪些 SQL 语句执行效率低下,通过 explain 我们可以得知 SQL 语句的具体执行情况,索引使用等,还可以结合`Show Profile`命令查看执行状态。
-
-- Show Profile 是 MySQL 提供可以用来分析当前会话中语句执行的资源消耗情况。可以用于SQL的调优的测量
-
-- 默认情况下,参数处于关闭状态,并保存最近15次的运行结果
-
-- 分析步骤
-
- 1. 是否支持,看看当前的mysql版本是否支持
-
- ```mysql
- mysql>Show variables like 'profiling'; --默认是关闭,使用前需要开启
- ```
-
- 2. 开启功能,默认是关闭,使用前需要开启
-
- ```mysql
- mysql>set profiling=1;
- ```
-
- 3. 运行SQL
-
- 4. 查看结果
-
- ```mysql
- mysql> show profiles;
- +----------+------------+---------------------------------+
- | Query_ID | Duration | Query |
- +----------+------------+---------------------------------+
- | 1 | 0.00385450 | show variables like "profiling" |
- | 2 | 0.00170050 | show variables like "profiling" |
- | 3 | 0.00038025 | select * from t_base_user |
- +----------+------------+---------------------------------+
- ```
- ```
-
- 5. 诊断SQL,show profile cpu,block io for query id(上一步前面的问题SQL数字号码)
-
- 6. 日常开发需要注意的结论
-
- - converting HEAP to MyISAM 查询结果太大,内存都不够用了往磁盘上搬了。
-
- - create tmp table 创建临时表,这个要注意
-
- - Copying to tmp table on disk 把内存临时表复制到磁盘
-
- - locked
- ```
-
-
-
-> 查询中哪些情况不会使用索引?
-
-### 性能优化
-
-#### 索引优化
-
-1. 全值匹配我最爱
-2. 最佳左前缀法则,比如建立了一个联合索引(a,b,c),那么其实我们可利用的索引就有(a), (a,b), (a,b,c)
-3. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
-4. 存储引擎不能使用索引中范围条件右边的列
-5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select
-7. is null ,is not null 也无法使用索引
-8. like "xxxx%" 是可以用到索引的,like "%xxxx" 则不行(like "%xxx%" 同理)。like以通配符开头('%abc...')索引失效会变成全表扫描的操作,
-9. 字符串不加单引号索引失效
-10. 少用or,用它来连接时会索引失效
-10. <,<=,=,>,>=,BETWEEN,IN 可用到索引,<>,not in ,!= 则不行,会导致全表扫描
-
-
-
-**一般性建议**
-
-- 对于单键索引,尽量选择针对当前query过滤性更好的索引
-
-- 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
-
-- 在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引
-
-- 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的
-
-- 少用Hint强制索引
-
-
-
-#### 查询优化
-
-**永远小标驱动大表(小的数据集驱动大的数据集)**
-
-```mysql
-slect * from A where id in (select id from B)`等价于
-#等价于
-select id from B
-select * from A where A.id=B.id
-```
-
-当 B 表的数据集必须小于 A 表的数据集时,用 in 优于 exists
-
-```mysql
-select * from A where exists (select 1 from B where B.id=A.id)
-#等价于
-select * from A
-select * from B where B.id = A.id`
-```
-
-当 A 表的数据集小于B表的数据集时,用 exists优于用 in
-
-注意:A表与B表的ID字段应建立索引。
-
-
-
-**order by关键字优化**
-
-- order by子句,尽量使用 Index 方式排序,避免使用 FileSort 方式排序
-
-- MySQL 支持两种方式的排序,FileSort 和 Index,Index效率高,它指 MySQL 扫描索引本身完成排序,FileSort 效率较低;
-- ORDER BY 满足两种情况,会使用Index方式排序;①ORDER BY语句使用索引最左前列 ②使用where子句与ORDER BY子句条件列组合满足索引最左前列
-
-- 尽可能在索引列上完成排序操作,遵照索引建的最佳最前缀
-- 如果不在索引列上,filesort 有两种算法,mysql就要启动双路排序和单路排序
- - 双路排序:MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据
- - 单路排序:从磁盘读取查询需要的所有列,按照order by 列在 buffer对它们进行排序,然后扫描排序后的列表进行输出,效率高于双路排序
-
-- 优化策略
-
- - 增大sort_buffer_size参数的设置
- - 增大max_lencth_for_sort_data参数的设置
-
-
-
-
-**GROUP BY关键字优化**
-
-- group by实质是先排序后进行分组,遵照索引建的最佳左前缀
-- 当无法使用索引列,增大 `max_length_for_sort_data` 参数的设置,增大`sort_buffer_size`参数的设置
-- where高于having,能写在where限定的条件就不要去having限定了
-
-
-
-#### 数据类型优化
-
-MySQL 支持的数据类型非常多,选择正确的数据类型对于获取高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于做出更好的选择。
-
-- 更小的通常更好:一般情况下,应该尽量使用可以正确存储数据的最小数据类型。
-
- 简单就好:简单的数据类型通常需要更少的CPU周期。例如,整数比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较复杂。
-
-- 尽量避免NULL:通常情况下最好指定列为NOT NULL
-
-------
-
-
-
-## 九、分区、分表、分库
-
-### MySQL分区
-
-一般情况下我们创建的表对应一组存储文件,使用`MyISAM`存储引擎时是一个`.MYI`和`.MYD`文件,使用`Innodb`存储引擎时是一个`.ibd`和`.frm`(表结构)文件。
-
-当数据量较大时(一般千万条记录级别以上),MySQL的性能就会开始下降,这时我们就需要将数据分散到多组存储文件,保证其单个文件的执行效率
-
-**能干嘛**
-
-- 逻辑数据分割
-- 提高单一的写和读应用速度
-- 提高分区范围读查询的速度
-- 分割数据能够有多个不同的物理文件路径
-- 高效的保存历史数据
-
-**怎么玩**
-
-首先查看当前数据库是否支持分区
-
-- MySQL5.6以及之前版本:
-
- ```mysql
- SHOW VARIABLES LIKE '%partition%';
- ```
-
-- MySQL5.6:
-
- ```mysql
- show plugins;
- ```
-
-**分区类型及操作**
-
-- **RANGE分区**:基于属于一个给定连续区间的列值,把多行分配给分区。mysql将会根据指定的拆分策略,,把数据放在不同的表文件上。相当于在文件上,被拆成了小块.但是,对外给客户的感觉还是一张表,透明的。
-
- 按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,比如交易表啊,销售表啊等,可以根据年月来存放数据。可能会产生热点问题,大量的流量都打在最新的数据上了。
-
- range 来分,好处在于说,扩容的时候很简单。
-
-- **LIST分区**:类似于按RANGE分区,每个分区必须明确定义。它们的主要区别在于,LIST分区中每个分区的定义和选择是基于某列的值从属于一个值列表集中的一个值,而RANGE分区是从属于一个连续区间值的集合。
-
-- **HASH分区**:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL 中有效的、产生非负整数值的任何表达式。
-
- hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表
-
-- **KEY分区**:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值。
-
-**看上去分区表很帅气,为什么大部分互联网还是更多的选择自己分库分表来水平扩展咧?**
-
-- 分区表,分区键设计不太灵活,如果不走分区键,很容易出现全表锁
-- 一旦数据并发量上来,如果在分区表实施关联,就是一个灾难
-- 自己分库分表,自己掌控业务场景与访问模式,可控。分区表,研发写了一个sql,都不确定mysql是怎么玩的,不太可控
-
-
-
-> 随着业务的发展,业务越来越复杂,应用的模块越来越多,总的数据量很大,高并发读写操作均超过单个数据库服务器的处理能力怎么办?
-
-这个时候就出现了**数据分片**,数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中。数据分片的有效手段就是对关系型数据库进行分库和分表。
-
-区别于分区的是,分区一般都是放在单机里的,用的比较多的是时间范围分区,方便归档。只不过分库分表需要代码实现,分区则是mysql内部实现。分库分表和分区并不冲突,可以结合使用。
-
-
-
->说说分库与分表的设计
-
-### MySQL分表
-
-分表有两种分割方式,一种垂直拆分,另一种水平拆分。
-
-- **垂直拆分**
-
- 垂直分表,通常是按照业务功能的使用频次,把主要的、热门的字段放在一起做为主要表。然后把不常用的,按照各自的业务属性进行聚集,拆分到不同的次要表中;主要表和次要表的关系一般都是一对一的。
-
-- **水平拆分(数据分片)**
-
- 单表的容量不超过500W,否则建议水平拆分。是把一个表复制成同样表结构的不同表,然后把数据按照一定的规则划分,分别存储到这些表中,从而保证单表的容量不会太大,提升性能;当然这些结构一样的表,可以放在一个或多个数据库中。
-
- 水平分割的几种方法:
-
- - 使用MD5哈希,做法是对UID进行md5加密,然后取前几位(我们这里取前两位),然后就可以将不同的UID哈希到不同的用户表(user_xx)中了。
- - 还可根据时间放入不同的表,比如:article_201601,article_201602。
- - 按热度拆分,高点击率的词条生成各自的一张表,低热度的词条都放在一张大表里,待低热度的词条达到一定的贴数后,再把低热度的表单独拆分成一张表。
- - 根据ID的值放入对应的表,第一个表user_0000,第二个100万的用户数据放在第二 个表user_0001中,随用户增加,直接添加用户表就行了。
-
-
-
-
-
-### MySQL分库
-
-> 为什么要分库?
-
-数据库集群环境后都是多台 slave,基本满足了读取操作; 但是写入或者说大数据、频繁的写入操作对master性能影响就比较大,这个时候,单库并不能解决大规模并发写入的问题,所以就会考虑分库。
-
-> 分库是什么?
-
-一个库里表太多了,导致了海量数据,系统性能下降,把原本存储于一个库的表拆分存储到多个库上, 通常是将表按照功能模块、关系密切程度划分出来,部署到不同库上。
-
-优点:
-
-- 减少增量数据写入时的锁对查询的影响
-
-- 由于单表数量下降,常见的查询操作由于减少了需要扫描的记录,使得单表单次查询所需的检索行数变少,减少了磁盘IO,时延变短
-
-但是它无法解决单表数据量太大的问题
-
-
-
-**分库分表后的难题**
-
-分布式事务的问题,数据的完整性和一致性问题。
-
-数据操作维度问题:用户、交易、订单各个不同的维度,用户查询维度、产品数据分析维度的不同对比分析角度。 跨库联合查询的问题,可能需要两次查询 跨节点的count、order by、group by以及聚合函数问题,可能需要分别在各个节点上得到结果后在应用程序端进行合并 额外的数据管理负担,如:访问数据表的导航定位 额外的数据运算压力,如:需要在多个节点执行,然后再合并计算程序编码开发难度提升,没有太好的框架解决,更多依赖业务看如何分,如何合,是个难题。
-
-
-
-> 配主从,正经公司的话,也不会让 Javaer 去搞的,但还是要知道
-
-## 十、主从复制
-
-### 复制的基本原理
-
-- slave 会从 master 读取 binlog 来进行数据同步
-
-- 三个步骤
-
- 1. master将改变记录到二进制日志(binary log)。这些记录过程叫做二进制日志事件,binary log events;
- 2. salve 将 master 的 binary log events 拷贝到它的中继日志(relay log);
- 3. slave 重做中继日志中的事件,将改变应用到自己的数据库中。MySQL 复制是异步且是串行化的。
-
- 
-
-### 复制的基本原则
-
-- 每个 slave只有一个 master
-- 每个 salve只能有一个唯一的服务器 ID
-- 每个master可以有多个salve
-
-### 复制的最大问题
-
-- 延时
-
-------
-
-
-
-## 十一、其他问题
-
-### 说一说三个范式
-
-- 第一范式(1NF):数据库表中的字段都是单一属性的,不可再分。这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等。
-- 第二范式(2NF):数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于任意一组候选关键字。
-- 第三范式(3NF):在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如 果存在"A → B → C"的决定关系,则C传递函数依赖于A。因此,满足第三范式的数据库表应该不存在如下依赖关系: 关键字段 → 非关键字段 x → 非关键字段y
-
-
-
-### 百万级别或以上的数据如何删除
-
-关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
-
-1. 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
-2. 然后删除其中无用数据(此过程需要不到两分钟)
-3. 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
-4. 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。
-
-
-
-
-
-
-## 参考与感谢:
-
-https://zhuanlan.zhihu.com/p/29150809
-
-https://juejin.im/post/5e3eb616f265da570d734dcb#heading-105
-
-https://blog.csdn.net/yin767833376/article/details/81511377
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/data-management/MySQL/MySQL-Framework.md b/docs/data-management/MySQL/MySQL-Framework.md
index 5ed41b3801..f616a373f6 100644
--- a/docs/data-management/MySQL/MySQL-Framework.md
+++ b/docs/data-management/MySQL/MySQL-Framework.md
@@ -162,11 +162,15 @@ ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that
### MySQL 的查询流程大致是?
> 一条 SQL 查询语句是如何执行的?
-
-1. MySQL 客户端通过协议与 MySQL 服务器建连接,发送查询语句,先检查查询缓存,如果命中,直接返回结果,否则进行语句解析(MySQL 8.0 已取消了缓存)
-2. 有一系列预处理,比如检查语句是否写正确了,然后是查询优化(比如是否使用索引扫描,如果是一个不可能的条件,则提前终止),生成查询计划,然后查询引擎启动,开始执行查询,从底层存储引擎调用 API 获取数据,最后返回给客户端。怎么存数据、怎么取数据,都与存储引擎有关。
-3. 然后,MySQL 默认使用的 BTREE 索引,并且一个大方向是,无论怎么折腾 sql,至少在目前来说,MySQL 最多只用到表中的一个索引。
-
+1. **客户端请求**:MySQL 客户端通过协议与 MySQL 服务器建连接,发送查询语句,先检查查询缓存,如果命中,直接返回结果,否则进行语句解析(MySQL 8.0 已取消了缓存)
+2. **查询接收**:连接器接收请求,管理连接
+3. **解析器**:对 SQL 进行词法分析和语法分析,转换为解析树
+4. **优化器**:优化器生成执行计划,选择最优索引和连接顺序
+5. **查询执行器**:执行器执行查询,通过存储引擎接口获取数据
+6. **存储引擎**:存储引擎检索数据,返回给执行器
+7. **返回结果**:结果通过连接器返回给客户端
+
+
## Reference
diff --git a/docs/data-management/MySQL/MySQL-Index-FAQ.md b/docs/data-management/MySQL/MySQL-Index-FAQ.md
deleted file mode 100755
index 97df06f415..0000000000
--- a/docs/data-management/MySQL/MySQL-Index-FAQ.md
+++ /dev/null
@@ -1,79 +0,0 @@
-## 索引结构
-
-聚簇索引/非聚簇索引,MySQL 索引底层实现,叶子结点存放的是数据还是指向数据的内存地址,使用索引需要注意的几个地方?
-
-使用索引查询一定能提高查询的性能吗?为什么?
-
-
-
-
-
-
-
-### [优化要注意的一些事](https://www.cnblogs.com/frankdeng/p/8990181.html "优化要注意的一些事")
-
-1. 索引其实就是一种归类方式,当某一个字段属性都不能归类,建立索引后是没什么效果的,或归类就二种(0和1),且各自都数据对半分,建立索引后的效果也不怎么强。
-
-2. 主键的索引是不一样的,要区别理解。
-
-3. 当时间存储为时间戳保存的可以建立前缀索引。
-
-4. 在什么字段上建立索引,需要根据查询条件而定,不要一上来就建立索引,浪费内存还有可能用不到。
-
-5. 大字段(blob)不要建立索引,查询也不会走索引。
-
-6. 常用建立索引的地方:
- - 主键的聚集索引
- - 外键索引
- - 类别只有0和1就不要建索引了,没有意义,对性能没有提升,还影响写入性能
- - 用模糊其实是可以走前缀索引
-
-7. 唯一索引一定要小心使用,它带有唯一约束,由于前期需求不明等情况下,可能造成我们对于唯一列的误判。
-
-8. 由于我们建立索引并想让索引能达到最高性能,这个时候我们应当充分考虑该列是否适合建立索引,可以根据列的区分度来判断,区分度太低的情况下可以不考虑建立索引,区分度越高效率越高。
-
-9. 写入比较频繁的时候,不能开启MySQL的查询缓存,因为在每一次写入的时候不光要写入磁盘还的更新缓存中的数据。
-10. 二次SQL查询区别不大的时候,不能按照二次执行的时间来判断优化结果,没准第一次查询后又保存缓存数据,导致第二次查询速度比第二次快,很多时候我们看到的都是假象。
-11. Explain 执行计划只能解释SELECT操作。
-12. 使用UNION ALL 替换OR多条件查询并集。
-13. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
-14. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:`select id from t where num is null` 可以在 num上设置默认值 0,确保表中 num 列没有 null 值,然后这样查询:`select id from t where num=0`
-15. 应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
-16. 应尽量避免在 where 子句中使用or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:`select id from t where num=10 or num=20` 可以这样查询:`select id from t where num=10 union all select id from t where num=20`
-17. in 和 not in 也要慎用,否则会导致全表扫描,如:`select id from t where num in(1,2,3)` 对于连续的数值,能用 between 就不要用 in 了:`select id from t where num between 1 and 3`
-18. 下面的查询也将导致全表扫描:`select id from t where name like '李%'` 若要提高效率,可以考虑全文检索。
-19. 如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:`select id from t where num=@num` 可以改为强制查询使用索引:`select id from t with(index(索引名)) where num=@num`
-20. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:`select id from t where num/2=100` 应改为: `select id from t where num=100*2`
-21. 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:`select id from t where substring(name,1,3)='abc' `,name 以 abc 开头的 id 应改为: `select id from t where name like 'abc%'`
-22. 不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
-23. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
-24. 不要写一些没有意义的查询,如需要生成一个空表结构:`select col1,col2 into #t from t where 1=0` 这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样: create table #t(...)
-25. 很多时候用 exists 代替 in 是一个好的选择:`select num from a where num in(select num from b)` 用下面的语句替换:` select num from a where exists(select 1 from b where num=a.num)`
-26. 并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。
-27. 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。
-28. 应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
-29. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
-30. 尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
-31. 任何地方都不要使用 `select * from t` ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
-
-
-
-
-
-### 聚簇索引优缺点
-
-聚族索引的优点
-
-1. 可以把相关数据保存在一起。例如实现电子邮件时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚族索引,则每封邮件都可能导致一次磁盘I/O;
-2. 数据访问更快。聚族索引将索引和数据保存在同一个B-Tree中,因此从聚族索引中获取数据通常比在非聚族索引中查找更快。
-3. 使用覆盖索引扫描的查询可以直接使用节点中的主键值。
-
-聚族索引的缺点
-
-1. 聚簇数据最大限度的提高了I/O密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没有那么重要了,聚簇索引也就没有那么优势了;
-2. 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE命令重新组织一下表。
-3. 更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。
-4. 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次分裂操作。页分裂会导致表占用更多的磁盘空间。
-5. 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
-6. 二级索引(非聚簇索引)可能比想象的要更大,因为在二级索引的叶子节点包含了引用行的主键列。
-7. 二级索引访问需要两次索引查找,而不是一次。
\ No newline at end of file
diff --git a/docs/data-management/MySQL/MySQL-Index.md b/docs/data-management/MySQL/MySQL-Index.md
index 9006f1c4f1..fe6c8f80c3 100644
--- a/docs/data-management/MySQL/MySQL-Index.md
+++ b/docs/data-management/MySQL/MySQL-Index.md
@@ -26,7 +26,7 @@ categories: MySQL
- 索引的目的在于提高查询效率,可以类比字典、 火车站的车次表、图书的目录等 。
-- 可以简单的理解为“排好序的快速查找数据结构”,数据本身之外,**数据库还维护者一个满足特定查找算法的数据结构**,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
+- 可以简单的理解为“排好序的快速查找数据结构”,数据本身之外,**数据库还维护着一个满足特定查找算法的数据结构**,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。
> 常见的索引模型其实有很多,哈希表、有序数组,各种搜索树都可以实现索引结构
@@ -247,6 +247,8 @@ InnoDB 的数据文件可以按照表来切分(只需要开启`innodb_file_per
> 扩展点:建议将这个值设置为 ON。因为,一个表单独存储为一个文件更容易管理,而且在你不需要这个表的时候,通过 drop table 命令,系统就会直接删除这个文件。而如果是放在共享表空间中,即使表删掉了,空间也是不会回收的。
>
> 所以会碰到这种情况,数据库占用空间太大后,把一个最大的表删掉了一半的数据,表文件的大小还是没变~
+>
+> 在 MySQL 8.0 版本以前,表结构是存在以.frm 为后缀的文件里。而 MySQL 8.0 版本,则已经允许把表结构定义放在系统数据表中了。
#### 辅助(非主键)索引:
@@ -270,9 +272,34 @@ InnoDB 的数据文件可以按照表来切分(只需要开启`innodb_file_per
正如我们上面介绍 InnoDB 存储结构,索引与数据是共同存储的,不管是主键索引还是辅助索引,在查找时都是通过先查找到索引节点才能拿到相对应的数据,如果我们在设计表结构时没有显式指定索引列的话,MySQL 会从表中选择数据不重复的列建立索引,如果没有符合的列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,并且这个字段长度为 6 个字节,类型为整型。
+> 你可能在一些建表规范里面见到过类似的描述,要求建表语句里一定要有自增主键。当然事无绝对,我们来分析一下哪些场景下应该使用自增主键,而哪些场景下不应该。
+>
+> 自增主键的插入数据模式,正符合了递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。
+>
+> 而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
+>
+> 除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
+>
+> 由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。
+>
+> **显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。**
+>
+> 所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
+>
+> 有没有什么场景适合用业务字段直接做主键的呢?还是有的。比如,有些业务的场景需求是这样的:
+>
+> 1. 只有一个索引;
+> 2. 该索引必须是唯一索引。
+>
+> 你一定看出来了,这就是典型的 KV 场景。
+>
+> 由于没有其他索引,所以也就不用考虑其他索引的叶子节点大小的问题。
+>
+> 这时候我们就要优先考虑上一段提到的“尽量使用主键查询”原则,直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
+
-## 三、索引策略
+## 四、索引策略
### 哪些情况需要创建索引
@@ -359,7 +386,7 @@ select id,name,email from user where emai='zhangsan@qq.com'
为了决定前缀的合适长度,需要找到最常见的值的列表,然后和最常见的前缀列进行比较。
-前缀索引是一种能使索引更小、更快的有效办法,但另一方面也有缺点:MySQL 无法使用前缀索引做 ORDER BY 和 GROUP BY,也无法使用前缀索引做『覆盖索引』。
+前缀索引是一种能使索引更小、更快的有效办法,但另一方面也有缺点:**MySQL 无法使用前缀索引做 ORDER BY 和 GROUP BY,也无法使用前缀索引做『覆盖索引』**。
> 一个常见的场景是针对很长的十六进制唯一 ID 使用前缀索引。
>
@@ -477,7 +504,7 @@ MySQL 有两种方式可以生成有序的结果,通过排序操作或者按
**MySQL 可以使用同一个索引既满足排序,又用于查找行,因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的**。
-只有当索引的列顺序和 order by 子句的顺序完全一致,并且所有列的排序方向(倒序或升序,创建索引时可以指定 ASC 或 DESC)都一样时,MySQL 才能使用索引来对结果做排序,如果查询需要关联多张表,则只有当 order by 子句引用的字段全部为第一个表时,才能使用索引做排序,order by 子句和查找型查询的限制是一样的,需要满足索引的最左前缀的要求,否则 MySQL 都需要执行排序操作,而无法使用索引排序。
+**只有当索引的列顺序和 order by 子句的顺序完全一致,并且所有列的排序方向(倒序或升序,创建索引时可以指定 ASC 或 DESC)都一样时,MySQL 才能使用索引来对结果做排序**,如果查询需要关联多张表,则只有当 order by 子句引用的字段全部为第一个表时,才能使用索引做排序,order by 子句和查找型查询的限制是一样的,需要满足索引的最左前缀的要求,否则 MySQL 都需要执行排序操作,而无法使用索引排序。
@@ -519,7 +546,7 @@ MySQL 允许在相同列上创建多个索引,无论是有意的还是无意
-## 四、索引优化
+## 五、索引优化
### 导致 SQL 执行慢的原因
@@ -535,16 +562,40 @@ MySQL 允许在相同列上创建多个索引,无论是有意的还是无意
### 索引优化
-1. 全值匹配我最爱
+```mysql
+CREATE TABLE hero(
+ id INT NOT NULL auto_increment,
+ name VARCHAR(100) NOT NULL,
+ phone CHAR(11) NOT NULL,
+ country varchar(100) NOT NULL,
+ PRIMARY KEY (id),
+ KEY idx_name_phone (name, phone)
+);
+```
+
+1. 全值匹配我最爱(就是搜索条件中的列和索引列一致)
+
+ > `select name, phone from hero where name = 'star' and phone = '13266666666'`
+ >
+ > 因为有「查询优化器」的存在,所有搜索条件调换顺序,改成 `phone = '13266666666' and name = 'star'` 无影响
2. 最佳左前缀法则,比如建立了一个联合索引(a,b,c),那么其实我们可利用的索引就有(a) (a,b)(a,c)(a,b,c)
3. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
4. 存储引擎不能使用索引中范围条件右边的列
+
+ ```mysql
+ -- 只能用到 name 列
+ SELECT * FROM hero WHERE name > 'Join' AND name < 'Lily' AND phone > '13222223333';
+
+ -- 但是如果左边的列是精确查找,则右边的列可以进行范围查找, 也可以用到 phone 列
+ SELECT * FROM hero WHERE name = 'Join' AND phone > '13222223333';
+ ```
5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少 select *
-6. is null ,is not null 也无法使用索引
+6. `is null` , `is not null` 也无法使用索引
7. `like "xxxx%"` 是可以用到索引的,`like "%xxxx"` 则不行(like "%xxx%" 同理)。like 以通配符开头('%abc...')索引失效会变成全表扫描的操作,
-8. 字符串不加单引号索引失效
-9. 少用or,用它来连接时会索引失效(这个其实不是绝对的,or 走索引与否,还和优化器的**预估**有关,5.0 之后出现的 index merge 技术就是优化这个的)
+8. 字符串不加单引号索引失效(隐式类型转换)
+9. 少用 or,用它来连接时会索引失效(这个其实不是绝对的,or 走索引与否,还和优化器的**预估**有关,5.0 之后出现的 index merge 技术就是优化这个的)
10. <,<=,=,>,>=,BETWEEN,IN 可用到索引,<>,not in ,!= 则不行,会导致全表扫描
+11. 使用联合索引时,ASC、DESC 混用会导致索引失效
@@ -560,6 +611,10 @@ MySQL 允许在相同列上创建多个索引,无论是有意的还是无意
5. 尽量的扩展索引,不要新建索引。比如表中已经有 a 的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
+6. 索引列的类型尽量小
+ - 数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘`I/O`带来的性能损耗
+
+
> 我有一个公众号「 **JavaKeeper** 」
diff --git a/docs/data-management/MySQL/MySQL-Lock.md b/docs/data-management/MySQL/MySQL-Lock.md
index 267b6d1479..9f7fa29e92 100644
--- a/docs/data-management/MySQL/MySQL-Lock.md
+++ b/docs/data-management/MySQL/MySQL-Lock.md
@@ -14,93 +14,189 @@ categories: MySQL
>
> 数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。主要用来处理并发问题。
-
+ 为什么需要锁,只有并发操作时候才有锁的必要,并发事务访问相同记录的情况大致可以划分为 3 种:
+
+- `读-读`情况:即并发事务相继读取相同的记录
+- `写-写`情况:即并发事务相继对相同的记录做出改动
+- `读-写`或`写-读`情况:也就是一个事务进行读取操作,另一个进行改动操作。
-## 锁的分类
-#### 从对数据操作的类型分类:
-- **读锁**(共享锁):针对同一份数据,多个读操作可以同时进行,不会互相影响
+## 一、锁的分类有哪些
-- **写锁**(排他锁):当前写操作没有完成前,它会阻断其他写锁和读锁。
-#### 从对数据操作的粒度分类:
+#### 按操作粒度分类:
> 为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取,检查,释放锁等动作),因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念。
-- **全局锁**:对整个数据库实例加锁,可以用 `Flush tables with read lock (FTWRL)`设置为只读,就相当于加全局锁了
-- **表级锁**:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
+- **全局锁**:对整个数据库实例加锁,可以用 `Flush tables with read lock (FTWRL)`设置为只读,就相当于加全局锁了。**全局锁的典型使用场景是,做全库逻辑备份。**也就是把整库每个表都 select 出来存成文本。
+- **页级锁**:对数据页(通常是连续的几个行)加锁,控制并发事务对该页的访问。( BDB 存储引擎使用页级锁)
+- **表级锁**:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率较高,并发度最低;
+ - 表锁的语法是`lock tables … read/write`
+ - 另一类表级的锁是 MDL(metadata lock)。MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。
+
- **行级锁**:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
-适用:从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
+适用:从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
+ MySQL 不同的存储引擎支持不同的锁机制,所有的存储引擎都以自己的方式实现了锁机制
+| | 行锁 | 表锁 | 页锁 |
+| ------ | ---- | ---- | ---- |
+| MyISAM | | √ | |
+| BDB | | √ | √ |
+| InnoDB | √ | √ | |
+| Memory | | √ | |
-#### 加锁机制
+#### 按加锁机制分类
**乐观锁与悲观锁是两种并发控制的思想,可用于解决丢失更新问题**
-乐观锁会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务;
+- 乐观锁会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务;
+
+- 悲观锁会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁;
+
+#### 按锁模式(算法)分类
+
+- 记录锁(Record Lock):行级锁的特定类型,锁定单个行,确保其他事务无法同时修改或读取该行
+
+- 间隙锁(Gap Lock):对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行
+
+- MDL(Metadata Lock):锁定数据库对象的元数据,如表结构,用于保证数据定义的一致性
+- 临建锁(next-key Lock): 锁定索引项本身和索引范围。即 Record Lock 和 Gap Lock 的结合。可解决幻读问题。
-悲观锁会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁;
+#### 按属性分类:
+- **读锁**(共享锁):
+
+ - `共享锁`,英文名:`Shared Locks`,简称`S锁`。在事务要读取一条记录时,需要先获取该记录的`S锁`。
+
+ - 针对同一份数据,多个读操作可以同时进行,不会互相影响
+
+ - ```mysql
+ SELECT ... LOCK IN SHARE MODE; //对读取的记录加S锁
+ ```
+
+- **写锁**(独占锁、排他锁):
+
+ - 当前写操作没有完成前,它会阻断其他写锁和读锁
+
+ - `独占锁`,也常称`排他锁`,英文名:`Exclusive Locks`,简称`X锁`。在事务要改动一条记录时,需要先获取该记录的`X锁`
+
+ - ```mysql
+ SELECT ... FOR UPDATE; //对读取的记录加X锁
+ ```
+
+#### 按状态分类
+
+- 意向共享锁(Intention Shared Lock):表级锁的辅助锁,表示事务要在某个表或页级锁上获取共享锁。
+- 意向排它锁(Intention Exclusive Lock):表级锁的辅助锁,表示事务要在某个表或页级锁上获取排它锁。
+
+
+
+## 二、全局锁
+
+要使用全局锁,则要执行这条命令:
+
+```sql
+flush tables with read lock
+```
+执行后,**整个数据库就处于只读状态了**,这时其他线程执行以下操作,都会被阻塞
-#### 锁模式
+如果要释放全局锁,则要执行这条命令:
-- 记录锁: 对索引项加锁,锁定符合条件的行。其他事务不能修改 和删除加锁项;
-- gap锁: 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。
-- next-key锁: 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。
+```sql
+unlock tables
+```
+
+全局锁主要应用于做**全库逻辑备份**,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
+
+
+
+## 三、表锁
+
+MySQL 里面表级别的锁有这几种:
+
+- 表锁
+- 元数据锁(MDL)
- 意向锁
-- 插入意向锁
+- AUTO-INC 锁
+#### 表锁
+MySQL 支持多种存储引擎,不同存储引擎对锁的支持也是不一样的。
- MySQL 不同的存储引擎支持不同的锁机制,所有的存储引擎都以自己的方式实现了锁机制
+对于`MyISAM`、`MEMORY`、`MERGE`这些存储引擎来说,它们只支持表级锁,而且这些引擎并不支持事务,所以使用这些存储引擎的锁一般都是针对当前会话来说的。
-| | 行锁 | 表锁 | 页锁 |
-| ------ | ---- | ---- | ---- |
-| MyISAM | | √ | |
-| BDB | | √ | √ |
-| InnoDB | √ | √ | |
-| Memory | | √ | |
+#### 元数据锁
+
+**另一类表级的锁是 MDL(metadata lock)。**MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
+因此,在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
+- 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
-### 表锁(偏读)
+- 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
-#### 特点:
+虽然 MDL 锁是系统默认会加的,但却是你不能忽略的一个机制。比如下面这个例子,我经常看到有人掉到这个坑里:给一个小表加个字段,导致整个库挂了。
-**偏向MyISAM存储引擎,开销小,加锁快,无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低**
+1. 首先,线程 A 先启用了事务(但是一直不提交),然后执行一条 select 语句,此时就先对该表加上 MDL 读锁;
+2. 然后,线程 B 也执行了同样的 select 语句,此时并不会阻塞,因为「读读」并不冲突;
+3. 接着,线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,就会被阻塞,
-MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。
-MySQL 的表级锁有两种模式:
+那么在线程 C 阻塞后,后续有对该表的 select 语句,就都会被阻塞,如果此时有大量该表的 select 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了。
-- 表共享读锁(Table Read Lock)
-- 表独占写锁(Table Write Lock)
+> 为什么线程 C 因为申请不到 MDL 写锁,而导致后续的申请读锁的查询操作也会被阻塞?
-| 锁类型 | 可否兼容 | 读锁 | 写锁 |
-| ------ | -------- | ---- | ---- |
-| 读锁 | 是 | 是 | 否 |
-| 写锁 | 是 | 否 | 否 |
+这是因为申请 MDL 锁的操作会形成一个队列,队列中**写锁获取优先级高于读锁**,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
+所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。
- 结合上表,所以对MyISAM表进行操作,会有以下情况:
+#### 表级别的意向锁
-1. 对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
-2. 对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。
+- 意向共享锁,英文名:`Intention Shared Lock`,简称`IS锁`。在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
+- 意向独占锁,英文名:`Intention Exclusive Lock`,简称`IX锁`。在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
+
+`IS锁`和`IX锁`的使命只是为了后续在加表级别的`S锁`和`X锁`时判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录。
+
+#### AUTO-INC 锁
+
+表里的主键通常都会设置成自增的,这是通过对主键字段声明 `AUTO_INCREMENT` 属性实现的。
+
+之后在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 **AUTO-INC 锁**实现的。
+
+AUTO-INC 锁是特殊的表锁机制,锁**不是在一个事务提交后才释放,而是在执行完插入语句后就会立即释放**。
+
+**在插入数据时,会加一个表级别的 AUTO-INC 锁**,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。
+
+那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 `AUTO_INCREMENT` 修饰的字段的值是连续递增的。
+
+但是, AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。
+
+因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种**轻量级的锁**来实现自增。
+
+一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,**然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁**。
+
+InnoDB 存储引擎提供了个 `innodb_autoinc_lock_mode` 的系统变量,是用来控制选择用 AUTO-INC 锁,还是轻量级的锁。
+
+- 当 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;
+- 当 innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
+- 当 innodb_autoinc_lock_mode = 1,相当于两种方式混着来
+ - 普通 insert 语句,自增锁在申请之后就马上释放;
+ - 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
+
+当 innodb_autoinc_lock_mode = 2 是性能最高的方式,但是当搭配 binlog 的日志格式是 statement 一起使用的时候,可能会造成不同事务中的插入语句为 AUTO_INCREMENT 修饰的列生成的值是交叉的,在「主从复制的场景」中会发生**数据不一致的问题**。
-简而言之,就是读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞。
-MyISAM表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。
#### 如何加表锁
-MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。
+MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。
-#### MyISAM表锁优化建议
+#### MyISAM 表锁优化建议
-对于MyISAM存储引擎,虽然使用表级锁定在锁定实现的过程中比实现行级锁定或者页级锁所带来的附加成本都要小,锁定本身所消耗的资源也是最少。但是由于锁定的颗粒度比较大,所以造成锁定资源的争用情况也会比其他的锁定级别都要多,从而在较大程度上会降低并发处理能力。所以,在优化MyISAM存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别是不可能改变的了,所以我们首先需要**尽可能让锁定的时间变短**,然后就是让可能并发进行的操作尽可能的并发。
+对于 MyISAM 存储引擎,虽然使用表级锁定在锁定实现的过程中比实现行级锁定或者页级锁所带来的附加成本都要小,锁定本身所消耗的资源也是最少。但是由于锁定的颗粒度比较大,所以造成锁定资源的争用情况也会比其他的锁定级别都要多,从而在较大程度上会降低并发处理能力。所以,在优化MyISAM存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别是不可能改变的了,所以我们首先需要**尽可能让锁定的时间变短**,然后就是让可能并发进行的操作尽可能的并发。
看看哪些表被加锁了:
@@ -110,15 +206,13 @@ mysql>show open tables;
1. ##### 查询表级锁争用情况
-MySQL内部有两组专门的状态变量记录系统内部锁资源争用情况:
+MySQL 内部有两组专门的状态变量记录系统内部锁资源争用情况:
```mysql
mysql> show status like 'table%';
```
-
-
-这里有两个状态变量记录MySQL内部表级锁定的情况,两个变量说明如下:
+这里有两个状态变量记录 MySQL 内部表级锁定的情况,两个变量说明如下:
- Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1
@@ -126,7 +220,7 @@ mysql> show status like 'table%';
两个状态值都是从系统启动后开始记录,出现一次对应的事件则数量加1。如果这里的Table_locks_waited状态值比较高,那么说明系统中表级锁定争用现象比较严重,就需要进一步分析为什么会有较多的锁定资源争用了。
-?> 此外,Myisam的读写锁调度是写优先,这也是myisam不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞
+> 此外,Myisam的读写锁调度是写优先,这也是myisam不适合做写为主表的引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞
2. **缩短锁定时间**
@@ -173,188 +267,218 @@ mysql> show status like 'table%';
-### 行锁(偏写)
-
-- 偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
-
-- InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁
-
-
-
-Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,Innodb的整体性能和MyISAM相比就会有比较明显的优势了。
-
-
-
-1. InnoDB锁定模式及实现机制
-
- InnoDB的行级锁定同样分为两种类型,**共享锁和排他锁**,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB也同样使用了**意向锁**(表级锁定)的概念,也就有了**意向共享锁**和**意向排他锁**这两种。
-
- 当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。所以,可以说**InnoDB的锁定模式实际上可以分为四种:共享锁(S),排他锁(X),意向共享锁(IS)和意向排他锁(IX)**,我们可以通过以下表格来总结上面这四种所的共存逻辑关系:
-
-
-
-如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
-
-意向锁是InnoDB自动加的,不需用户干预。**对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁**(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
+## 四、行锁
-共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
+InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
-排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE
+> InnoDB 与 MyISAM 的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁
-用SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。
+`行锁`,也称为`记录锁`,顾名思义就是在记录上加的锁。
-但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE方式获得排他锁。
-2. InnoDB行锁实现方式
- **InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁**
+行级锁的类型主要有三类:
- 在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
+- Record Lock,记录锁,也就是仅仅把一条记录锁上;
+- Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
+- Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
- (1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
+#### Record Lock
- (2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
+Innodb 存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于 MyISAM 的表级锁定的。当系统并发量较高的时候,Innodb 的整体性能和 MyISAM 相比就会有比较明显的优势了。
- (3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
+Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
- (4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,**在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引**。
+- 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
+- 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
-
-
-#### 如何分析行锁定
-
-通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
+举个例子,当一个事务执行了下面这条语句:
-```mysql
-mysql>show status like 'innodb_row_lock%';
+```sql
+mysql > begin;
+mysql > select * from t where id = 4 for update;
```
-
+就是对 t 表中主键 id 为 4 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。
+
-对各个状态量的说明如下:
+当事务执行 commit 后,事务过程中生成的锁都会被释放。
-Innodb_row_lock_current_waits:当前正在等待锁定的数量;
-Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
-Innodb_row_lock_time_avg:每次等待所花平均时间;
-Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
-Innodb_row_lock_waits:系统启动后到现在总共等待的次数;
-对于这5个状态变量,比较重要的主要是
- Innodb_row_lock_time_avg(等待平均时长),
- Innodb_row_lock_waits(等待总次数)
- Innodb_row_lock_time(等待总时长)这三项。
-尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
+#### Gap Lock
-#### 行锁优化
+Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
-- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁。
+假设,表中有一个范围 id 为(4,8)间隙锁,那么其他事务就无法插入 id = 5、6、7 的记录了,这样就有效的防止幻读现象的发生。
-- 合理设计索引,尽量缩小锁的范围
+
-- 尽可能较少检索条件,避免间隙锁
+间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,**间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的**。
-- 尽量控制事务大小,减少锁定资源量和时间长度
-- 尽可能低级别事务隔离
+#### Next-Key Lock
+Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
-### 页锁
+假设,表中有一个范围 id 为(4,8] 的 next-key lock,那么其他事务即不能插入 id = 5,6,7 记录,也不能修改 id = 8 这条记录。
-开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
+
+所以,next-key lock 即能保护该记录,又能阻止其他事务将新记录插入到被保护记录前面的间隙中。
+**next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的**。
+比如,一个事务持有了范围为 (4, 8] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。
+虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的。
-## 死锁
-死锁是指两个或者多个事务在同一资源上互相占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。
-`例如:` 设想下面两个事务同时处理 `StockPrice` 表:
+#### 插入意向锁
-事务1
+一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。
-
+如果有的话,插入操作就会发生**阻塞**,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个**插入意向锁**,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
-事务2:
+举个例子,假设事务 A 已经对表加了一个范围 id 为(4,8)间隙锁。
-
+当事务 A 还没提交的时候,事务 B 向该表插入一条 id = 5 的新记录,这时会判断到插入的位置已经被事务 A 加了间隙锁,于是事物 B 会生成一个插入意向锁,然后将锁的状态设置为等待状态(*PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁*),此时事务 B 就会发生阻塞,直到事务 A 提交了事务。
-如果凑巧,两个事务都执行了第一条 `UPDATE` 语句,更新了一行数据,同时也锁定了改行数据,接着每个事务都尝试去执行
+插入意向锁名字虽然有意向锁,但是它并**不是意向锁,它是一种特殊的间隙锁,属于行级别锁**。
-第二条 `UPDATE` 语句,缺发现该行已经被对方锁定,然后两个事务都在等待对方释放锁,同时又持有对方需要的锁,则陷入死
+如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。
-循环。除非有外部因素介入才可能解除`死锁`。
+插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。
-为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。
+#### InnoDB 行锁实现方式
-越复杂的系统,比如 `InnoDB` 存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。
+**InnoDB 行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁**
-还有一种解决方式,就是当查询的时候达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。
+在实际应用中,要特别注意 InnoDB 行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
-`InnoDB` 目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。
+1. 在不通过索引条件查询的时候,InnoDB 确实使用的是表锁,而不是行锁。
+2. 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
+3. 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
+4. 即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,**在分析锁冲突时,别忘了检查 SQL 的执行计划,以确认是否真正使用了索引**。
+
+#### 如何分析行锁定
-当出现死锁以后,有两种策略:
+通过检查 `InnoDB_row_lock` 状态变量来分析系统上的行锁的争夺情况
-- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
-- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
+```mysql
+mysql>show status like 'innodb_row_lock%';
+```
-在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
+
-但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。
+- `Innodb_row_lock_current_waits`:当前正在等待锁定的数量;
+- `Innodb_row_lock_time`:从系统启动到现在锁定总时间长度;
+- `Innodb_row_lock_time_avg`:每次等待所花平均时间;
+- `Innodb_row_lock_time_max`:从系统启动到现在等待最常的一次所花的时间;
+- `Innodb_row_lock_waits`:系统启动后到现在总共等待的次数;
-所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。
+#### 行锁优化
+- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
+- 合理设计索引,尽量缩小锁的范围
-乐观锁
+- 尽可能较少检索条件,避免间隙锁
-悲观锁
+- 尽量控制事务大小,减少锁定资源量和时间长度
+- 尽可能低级别事务隔离
+### 页锁
+开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
+## 五、InnoDB 锁内存结构
+```mysql
+*# 事务T1*
+SELECT * FROM hero LOCK IN SHARE MODE*;*
+```
+很显然这条语句需要为`hero表`中的所有记录进行加锁,那是不是需要为每条记录都生成一个`锁结构`呢?
-## 参考
+在对不同记录加锁时,如果符合下边这些条件:
-- 《高性能 MySQL》
+- 在同一个事务中进行加锁操作
+- 被加锁的记录在同一个页面中
+- 加锁的类型是一样的
+- 等待状态是一样的
+那么这些记录的锁就可以被放到一个`锁结构`中
+`锁`是一个内存结构,InnoDB中用 `lock_t` 这个结构来定义(8.0):
+```c
+struct lock_t {
+ /** transaction owning the lock */
+ trx_t *trx;
+ /** list of the locks of the transaction */
+ UT_LIST_NODE_T(lock_t) trx_locks;
+ /** Index for a record lock */
+ dict_index_t *index;
+ /** Hash chain node for a record lock. The link node in a singly
+ linked list, used by the hash table. */
+ lock_t *hash;
+ union {
+ /** Table lock */
+ lock_table_t tab_lock;
+ /** Record lock */
+ lock_rec_t rec_lock;
+ };
+```
+## 六、死锁
+
+死锁是指两个或者多个事务在同一资源上互相占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。
+
+为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。
+当出现死锁以后,有两种策略:
+- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
+- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
+在 InnoDB 中,`innodb_lock_wait_timeout` 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
+但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。
+所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 `innodb_deadlock_detect` 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。
+## 参考
+- 《高性能 MySQL》
+- 《MySQL技术内幕:innodb》
+- 《MySQL实战45讲》
+- 《从根儿上理解MySQL》
diff --git a/docs/data-management/MySQL/MySQL-Log.md b/docs/data-management/MySQL/MySQL-Log.md
index 5e9937359b..76ad66835e 100755
--- a/docs/data-management/MySQL/MySQL-Log.md
+++ b/docs/data-management/MySQL/MySQL-Log.md
@@ -1,5 +1,5 @@
---
-Lltitle: MySQL 是怎么想的,搞这么多种日志
+title: MySQL 是怎么想的,搞这么多种日志
date: 2022-08-01
tags:
- MySQL
@@ -19,7 +19,7 @@ MySQL日志文件:用来记录 MySQL 实例对某种条件做出响应时写
他们都有什么作用和关联,我们一起捋一捋(基于 InnoDB)
-
+
我们从一条 SQL 语句说起吧
@@ -57,13 +57,11 @@ SET GLOBAL general_log = 'ON'
-
-
## 二、重做日志(redo log)
-解这块知识,我们要先知道这么几个前置知识点,先看下官网的 InnoDB 架构图
+了解这块知识,我们先要知道这么几个前置知识点,先看下官网的 InnoDB 架构图,有个大概印象
-
+
> #### 什么是 随机 IO 和 顺序 IO
>
@@ -73,20 +71,50 @@ SET GLOBAL general_log = 'ON'
>
> MySQL 的 InnoDB 存储引擎以 Data Page(数据页)作为磁盘和内存之间交互的基本单位,他的大小一般为默认值 16K。
>
-> 从数据页的作用来分,可以分为Free Page(空闲页)、Clean Page(干净页)、Dirty Page(脏页);
+> 从数据页的作用来分,可以分为 Free Page(空闲页)、Clean Page(干净页)、Dirty Page(脏页);
>
> - **当内存数据页 和 磁盘数据页内容不一致的时候,这个内存页 就为 “脏页”**。将内存中的数据同步到磁盘中的这个过程就被称为**“刷脏”**
> - **内存数据页写入磁盘后,内存数据页 和 磁盘数据页内容一致,称之为 “干净页”**
>
> 从类型来分的话,还可以分成存放 UNDO 日志的页、存放 INODE 信息的页、存放表空间头部信息的页等。
-> #### 什么是缓冲池? TODO 是被取消了吗
+> #### 什么是缓冲池 | buffer pool?
>
> 关系型数据库的特点就是需要对磁盘中大量的数据进行存取,所以有时候也被叫做基于磁盘的数据库。正是因为数据库需要频繁对磁盘进行 IO 操作,为了改善因为直接读写磁盘导致的 IO 性能问题,所以引入了缓冲池。
>
-> 缓冲池是一片内存区域,存储引擎在读取数据时,会先将页读取到缓冲池中。下次读取时,先判断是否在缓冲池,如果在,则直接读取,否则从磁盘中读取。在修改数据时,如果缓冲池中不存在所需的数据页,则从磁盘读入缓冲池,否则直接对缓冲池中的数据页进行修改。
+> 不过不论是什么类型的页面,每当我们从页面中读取或写入数据时,都必须先将其从硬盘上加载到内存中的`buffer pool`中(也就是说内存中的页面其实就是硬盘中页面的一个副本),然后才能对内存中页面进行读取或写入。如果要修改内存中的页面,为了减少磁盘 I/O,修改后的页面并不立即同步到磁盘,而是作为`脏页`继续呆在内存中,等待后续合适时机将其刷新到硬盘(一般是有后台线程异步刷新),将该页刷到磁盘的操作称为 刷脏页 (本句是重点,后面要吃)。
>
-> 这样的好处是,如果我们频繁修改某一个位于磁盘的数据页,我们可以不用每次都去磁盘读写(注意是读和写)该页,而是直接对缓冲池中的内容修改,在一定的时机再把数据刷新到磁盘。这样就会使得对磁盘的多次操作变为一次。即便修改的内容在磁盘中相距较远的不同数据页上,我们也可以将对多次对磁盘的 IO 合并为一次随机 IO。被修改的数据页会与磁盘上的数据产生短暂的不一致,我们称此时缓冲池中的数据页为 脏页 ,将该页刷到磁盘的操作称为 刷脏页 (本句是重点,后面要吃)。
+> #### 内存缓冲区
+>>
+> InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。由于 CPU 速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用内存缓冲区技术来提高数据库的整体性能。
+>
+> ##### Page Cache
+>
+> InnoDB 会将读取过的页缓存在内存中,并采取最近最少使用(Least Recently Used,LRU) 算法将缓冲池作为列表进行管理,以增加缓存命中率。
+>
+> InnoDB 对 LRU 算法进行了一定的改进,执行**中点插入策略**,默认前 5/8 为`New Sublist`,存储经常被使用的热点页,后 3/8 为`Old Sublist`。新读入的 page 默认被加在`old Sublist`的头部,如果在运行过程中 `old Sublist` 的数据被访问到了,那么这个页就会被移动到 `New Sublist` 的头部。
+>
+> 
+>
+> 每当 InnoDB 缓存新的数据页时,会优先从 `Free List` 中查找空余的缓存区域,如果不存在,那么就需要从 `LRU List` 淘汰一定的尾部节点。不管数据页位于 `New Sublist` 还是 `Old Sublist`,如果没有被访问到,那么最终都会被移动到 `LRU List` 的尾部作为牺牲者。
+>
+> ##### Change Buffer
+>
+> Change Buffer 用于记录数据的修改,因为 InnoDB 的辅助索引不同于聚集索引的顺序插入,如果每次修改二级索引都直接写入磁盘,则会有大量频繁的随机 IO。
+>
+> InnoDB 从 1.0.x 版本开始引入了 Change Buffer,主要目的是将对**非唯一辅助索引**页的操作缓存下来,如果辅助索引页已经在缓冲区了,则直接修改;如果不在,则先将修改保存到 Change Buffer。当对应辅助索引页读取到缓冲区时,才将 Change Buffer 的数据合并到真正的辅助索引页中,以此减少辅助索引的随机 IO,并达到操作合并的效果。
+>
+> 
+>
+> 在 MySQL 5.5 之前 Change Buffer 其实叫 Insert Buffer,最初只支持 INSERT 操作的缓存,随着支持操作类型的增加,改名为 Change Buffer,现在 InnoDB 存储引擎可以对 INSERT、DELETE、UPDATE 都进行缓冲,对应着:Insert Buffer、Delete Buffer、Purge buffer。
+>
+> ##### Double Write Buffer
+>
+> 当发生数据库宕机时,可能存储引擎正在写入某个页到表中,而这个页只写了一部分,比如 16KB 的页,只写了前 4KB,之后就发生了宕机。虽然可以通过日志进行数据恢复,但是如果这个页本身已经发生了损坏,再对其进行重新写入是没有意义的。因此 InnoDB 引入 Double Write Buffer 解决数据页的半写问题。
+>
+> Double Write Buffer 大小默认为 2M,即 128 个数据页。其中分为两部分,一部分留给`batch write`,提供批量刷新脏页的操作,另一部分是`single page write`,留给用户线程发起的单页刷脏操作。
+>
+> 在对缓冲池的脏页进行刷新时,脏页并不是直接写到磁盘,而是会通过`memcpy()`函数将脏页先复制到内存中的 Double Write Buffer 中,如果 Double Write Buffer 写满了,那么就会调用`fsync()`系统调用,一次性将 Double Write Buffer 所有的数据写入到磁盘中,因为这个过程是顺序写入,开销几乎可以忽略。在确保写入成功后,再使用异步 IO 把各个数据页写回自己的表空间中。
### 2.1 为什么需要 redo log
@@ -110,17 +138,17 @@ SET GLOBAL general_log = 'ON'
这样的性能问题,我们是不能忍的,前面我们讲到数据页在缓冲池中被修改会变成脏页。如果这时宕机,脏页就会失效,这就导致我们修改的数据丢失了,也就无法保证事务的**持久性**。
-为了解决这些问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路,设计了`redo log`,**具体来说就是只记录事务对数据页做了哪些修改**,这样就能完美地解决性能问题了(相对而言文件更小并且是顺序IO)。
+为了解决这些问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路,设计了`redo log`,**具体来说就是只记录事务对数据页做了哪些修改**,这样就能完美地解决性能问题了(相对而言文件更小并且是顺序 IO)。
而粉板和账本配合的整个过程,其实就类似 MySQL 里经常说到的 **WAL 技术**(Write-Ahead Loging),它的关键点就是**先写日志,再写磁盘**,也就是先写粉板,等不忙的时候再写账本。这又解决了我们的持久性问题。
-具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。
+具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 `redo log`(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。
-> DBA 口中的日志先行说的就是这个 WAL 技术。
+> DBA 口中的**日志先行**说的就是这个 WAL 技术。
>
-> 记录下对磁盘中某某页某某位置数据的修改结果的 redo log,这种日志被称为**物理日志**,可以节省很多磁盘空间
+> 记录下对磁盘中某某页某某位置数据的修改结果的 redo log,这种日志被称为**物理日志**,可以节省很多磁盘空间。
>
-> 最开始看到的通用查询日志,记录了所有数据库的操作,我们叫**逻辑日志**,还有下边会说的 binlog、undo log 也都属于逻辑日志
+> 最开始看到的通用查询日志,记录了所有数据库的操作,我们叫**逻辑日志**,还有下边会说的 binlog、undo log 也都属于逻辑日志。
@@ -134,9 +162,9 @@ MySQL redo日志是一组日志文件,在 MySQL 8.0.30 版本中,MySQL 会
> [`innodb_log_file_size`](https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_log_file_size) and [`innodb_log_files_in_group`](https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_log_files_in_group) are deprecated in MySQL 8.0.30. These variables are superseded by [`innodb_redo_log_capacity`](https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_redo_log_capacity). For more information, see [Section 15.6.5, “Redo Log”](https://dev.mysql.com/doc/refman/8.0/en/innodb-redo-log.html).
-为了应对 InnoDB 各种各样不同的需求,到 MySQL 8.0 为止,已经有多达 65 种的 REDO 记录,每种都不太一样,我们看下比较通用的结构了解了解就 OK(主要有作用于 Page,作用于 Space 以及提供额外信息的 Logicd 类型的三大类)
+为了应对 InnoDB 各种各样不同的需求,到 MySQL 8.0 为止,已经有多达 65 种的 REDO 记录,每种都不太一样,我们看下比较通用的结构了解了解就 OK(主要有作用于 Page,作用于 Space 以及提供额外信息的 Logic 类型的三大类)
-
+
比如 `MLOG_WRITE_STRING` 类型的 REDO 表示写入一串数据,但是因为不能确定写入的数据占多少字节,所以需要在日志结构中添加一个长度字段来表示写入了多长的数据。
@@ -144,17 +172,17 @@ MySQL redo日志是一组日志文件,在 MySQL 8.0.30 版本中,MySQL 会
#### 「Mini-Transaction」
-一个事务中可能有多个增删改的SQL语句,而一个SQL语句在执行过程中可能修改若干个页面,会有多个操作。
+一个事务中可能有多个增删改的 SQL语句,而一个 SQL 语句在执行过程中可能修改若干个页面,会有多个操作。
> 例如一个 INSERT 语句:
>
-> - 如果表没有主键,会去更新内存中的`Max Row ID`属性,并在其值为`256`的倍数时,将其刷新到`系统表空间`的页号为`7`的`Max Row ID`属性处。
+> - 如果表没有主键,会去更新内存中的 `Max Row ID` 属性,并在其值为 `256` 的倍数时,将其刷新到`系统表空间`的页号为 `7` 的`Max Row ID `属性处。
> - 接着向聚簇索引插入数据,这个过程要根据索引找到要插入的缓存页位置,向数据页插入记录。这个过程还可能会涉及数据页和索引页的分裂,那就会增加或修改一些缓存页,移动页中的记录。
> - 如果有二级索引,还会向二级索引中插入记录。
>
> 最后还可能要改动一些系统页面,比如要修改各种段、区的统计信息,各种链表的统计信息等等。
-所以 InnoDB 将执行语句的过程中产生的`redo log`划分成了若干个不可分割的组,一组`redo log`就是对底层页面的一次原子访问,这个原子访问也称为 `Mini-Transaction`,简称 **mtr**。一个 `mtr` 就包含一组`redo log`,在崩溃恢复时这一组`redo log`就是一个不可分割的整体。
+所以 InnoDB 将执行语句的过程中产生的 `redo log` 划分成了若干个不可分割的组,一组 `redo log` 就是对底层页面的一次原子访问,这个原子访问也称为 `Mini-Transaction`,简称 **mtr**。一个 `mtr` 就包含一组 `redo log`,在崩溃恢复时这一组 `redo log` 就是一个不可分割的整体。
#### 「redo log block」
@@ -162,14 +190,14 @@ MySQL redo日志是一组日志文件,在 MySQL 8.0.30 版本中,MySQL 会
磁盘是块设备,InnoDB 中也用 Block 的概念来读写数据,设计了一个 `redo log block` 的数据结构,称为重做日志块(`block`),重做日志块跟缓存页有点类似,只不过日志块记录的是一条条 redo log。
-S_FILE_LOG_BLOCK_SIZE 等于磁盘扇区的大小 512B,每次 IO 读写的最小单位都是一个 Block。
+`S_FILE_LOG_BLOCK_SIZE` 等于磁盘扇区的大小 512B,每次 IO 读写的最小单位都是一个 Block。
一个 `redo log block` 固定 `512字节` 大小,由三个部分组成:
-- 12字节的**Block Header**,主要记录一些额外的信息,包括文件信息、log 版本、lsn 等
+- 12 字节的 **Block Header**,主要记录一些额外的信息,包括文件信息、log 版本、lsn 等
- Block 中剩余的中间 498 个字节就是 REDO 真正内容的存放位置
-- Block 末尾是 4 字节的 **Block Tailer**,记录当前 Block 的 Checksum,通过这个值,读取 Log 时可以明确Block 数据有没有被完整写盘。
+- Block 末尾是 4 字节的 **Block Tailer**,记录当前 Block 的 Checksum,通过这个值,读取 Log 时可以明确 Block 数据有没有被完整写盘。
#### 「redo log 组成」
@@ -177,23 +205,50 @@ S_FILE_LOG_BLOCK_SIZE 等于磁盘扇区的大小 512B,每次 IO 读写的最
> 用户态下的缓冲区数据是无法直接写入磁盘的。因为中间必须经过操作系统的内核空间缓冲区(OS Buffer)。
-写入 redo log buffer 后,再写入 OS Buffer,然后操作系统调用 fsync() 函数将日志刷到磁盘。
+写入 `redo log buffer` 后,再写入 `OS Buffer`,然后操作系统调用 `fsync()` 函数将日志刷到磁盘。
-
+
-写完 redo log buffer 后,我们就要顺序追加日志了,可是每次往哪里写,肯定需要个标识的,类似 offset
+> 扩展点:
+>
+> - redo log buffer 里面的内容,既然是在操作系统调用 fsync() 函数持久化到磁盘的,那如果事务执行期间 MySQL 发生异常重启,那这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失。
+> - fsync() 的时机不是我们控制的,那就有可能在事务还没提交的时候,redo log buffer 中的部分日志被持久化到磁盘中
+>
+> 所以,redo log 是存在不同状态的
+>
+> 这三种状态分别是:
+>
+> 1. 存在 redo log buffer 中,物理上是在 MySQL 进程内存中;
+> 2. 写到磁盘 (write),但是没有持久化(fsync),物理上是在文件系统的 page cache 里面;
+> 3. 持久化到磁盘,对应的是 hard disk。
+>
+> 
+>
+> 日志写到 redo log buffer 是很快的,wirte 到 page cache 也差不多,但是持久化到磁盘的速度就慢多了。
+>
+> 为了控制 redo log 的写入策略,InnoDB 提供了 `innodb_flush_log_at_trx_commit` 参数,它有三种可能取值(刷盘策略还会说到):
+>
+> 1. 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
+> 2. 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
+> 3. 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。
+>
+> InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
+>
+> 注意,事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些 redo log 也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的 redo log,也是可能已经持久化到磁盘的。
+
+写完 redo log buffer 后,我们就要顺序追加日志了,可是每次往哪里写,肯定需要个标识的,类似 offset,小结一下接着聊。
#### 「redo 结构小结」
-我们那把这几个 redo 内容串起来,其实就是 redo log 有 32 个文件,每个文件以 Block 为单位划分,多个文件首尾相连顺序写入 REDO 内容,Redo 又按不同类型有不同内容。
+我们把这几个 redo 内容串起来,其实就是 redo log 有 32 个文件,每个文件以 Block 为单位划分,多个文件首尾相连顺序写入 REDO 内容,Redo 又按不同类型有不同内容。
一个 `mtr` 中的 redo log 实际上是先写到 redo log buffer,然后再”找机会“ 将一个个 mtr 的日志记录复制到`block`中,最后在一些时机将`block`刷新到磁盘日志文件中。
redo 文件结构大致是下图这样:
-
+
@@ -205,10 +260,11 @@ redo 文件结构大致是下图这样:
写入到日志文件(刷新到磁盘)的时机有这么 3 种:
-- MySQL 正常关闭时;
+- MySQL 正常关闭时
- 每秒刷新一次
-- redo log buffer 剩余空间小于 1/2 时
-- 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这个策略可由 innodb_flush_log_at_trx_commit 参数控制,有 3 种策略可选择
+- redo log buffer 剩余空间小于 1/2 时(内存不够用了,要先将脏页写到磁盘)
+
+每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这个策略可由 `innodb_flush_log_at_trx_commit` 参数控制
#### 刷盘策略
@@ -216,15 +272,16 @@ redo 文件结构大致是下图这样:
- 当设置该值为 1 时,每次事务提交都要做一次 fsync,这是最安全的配置,即使宕机也不会丢失事务,这是默认值;
-- 当设置为 2 时,则在事务提交时只做write操作,只保证写到系统的 page cache,因此实例 crash 不会丢失事务,但宕机则可能丢失事务;
+- 当设置为 2 时,则在事务提交时只做 write 操作,只保证写到系统的 page cache,因此实例 crash 不会丢失事务,但宕机则可能丢失事务;
-- 当设置为 0 时,事务提交不会触发 redo 写操作,而是留给后台线程每秒一次的刷盘操作,因此实例crash将最多丢失1秒钟内的事务
+- 当设置为 0 时,事务提交不会触发 redo 写操作,而是留给后台线程每秒一次的刷盘操作,因此实例 crash 最多丢失 1 秒钟内的事务
- 
+ 
+
-#### 「LSN」
+#### 日志逻辑序列号(log sequence number,LSN)
-刷盘时机、刷盘策略看着好像 挺合适,如果刷盘还没结束,服务器 GG(宕机)了呢? 知道你不慌,redo 可以用来保证持久性嘛~
+刷盘时机、刷盘策略看着好像挺合适,如果刷盘还没结束,服务器 GG(宕机)了呢? 知道你不慌,redo 可以用来保证持久性嘛~
重启服务后,我们肯定需要通过 redo 重放来恢复数据,但是从哪开始恢复呢?
@@ -234,23 +291,44 @@ redo 文件结构大致是下图这样:
在 InnoDB 的日志系统中,LSN 无处不在,它既用于表示修改脏页时的日志序号,也用于记录 checkpoint,通过 LSN,可以具体的定位到其在 redo log 文件中的位置。
-> 那如何由一个给定 LSN 的日志,在日志文件中找到它存储的位置的偏移量并能正确的读出来呢。所有的日志文件要属于日志组,而在 log_group_t 里的 lsn 和 lsn_offset字段已经记录了某个日志 lsn 和其存放在文件内的偏移量之间的对应关系。我们可以利用存储在 group 内的 lsn 和给定 lsn 之间的相对位置,来计算出给定 lsn 在文件中的存储位置。(具体怎么算我们先不讨论)
+> 那如何由一个给定 LSN 的日志,在日志文件中找到它存储的位置的偏移量并能正确的读出来呢。所有的日志文件要属于日志组,而在 log_group_t 里的 lsn 和 lsn_offset 字段已经记录了某个日志 lsn 和其存放在文件内的偏移量之间的对应关系。我们可以利用存储在 group 内的 lsn 和给定 lsn 之间的相对位置,来计算出给定 lsn 在文件中的存储位置。(具体怎么算我们先不讨论)
-越新的日志 LSN 越大。InnoDB 用检查点( checkpoint_lsn )指示未被刷盘的数据从这里开始,用 lsn 指示下一个应该被写入日志的位置。不过由于有 redo log buffer 的缘故,实际被写入磁盘的位置往往比 lsn 要小。
+LSN 是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redo log, LSN 的值就会加上 length。越新的日志 LSN 越大。
-redo log采用逻辑环形结构来复用空间(循环写入),这种环形结构一般需要几个指针去配合使用
+InnoDB 用检查点( checkpoint_lsn )指示未被刷盘的数据从这里开始,用 lsn 指示下一个应该被写入日志的位置。不过由于有 redo log buffer 的缘故,实际被写入磁盘的位置往往比 lsn 要小。
-
+redo log 采用逻辑环形结构来复用空间(循环写入),这种环形结构一般需要几个指针去配合使用
+
-如果 lsn 追上了 checkpoint,就意味着 **redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞**(*因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要*),此时**会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针)**,然后 MySQL 恢复正常运行,继续执行新的更新操作。
+如果 lsn 追上了 checkpoint,就意味着 **redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞**(*所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要*),此时**会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针)**,然后 MySQL 恢复正常运行,继续执行新的更新操作。
+> #### 组提交(group commit)
+>
+> redo log 的刷盘操作将会是最终影响 MySQL TPS 的瓶颈所在。为了缓解这一问题,MySQL 使用了组提交,将多个刷盘操作合并成一个,如果说 10 个事务依次排队刷盘的时间成本是 10,那么将这 10 个事务一次性一起刷盘的时间成本则近似于 1。
+>
+> 当开启 binlog 时(下边还会介绍)
+>
+> 为了保证 redo log 和 binlog 的数据一致性,MySQL 使用了二阶段提交,由 binlog 作为事务的协调者。而引入二阶段提交使得binlog 又成为了性能瓶颈,先前的 Redo log 组提交也成了摆设。为了再次缓解这一问题,MySQL 增加了 binlog 的组提交,目的同样是将 binlog 的多个刷盘操作合并成一个,结合 redo log 本身已经实现的组提交,分为三个阶段(Flush 阶段、Sync 阶段、Commit 阶段)完成 binlog 组提交,最大化每次刷盘的收益,弱化磁盘瓶颈,提高性能。
+>
+> 通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。
+>
+> 这时候,你可能有一个疑问,这意味着我从 MySQL 看到的 TPS 是每秒两万的话,每秒就会写四万次磁盘。但是,我用工具测试出来,磁盘能力也就两万左右,怎么能实现两万的 TPS?
+>
+> 解释这个问题,就要用到组提交(group commit)机制了。假设有三个并发事务 (trx1, trx2, trx3) 在 prepare 阶段,都写完 redo log buffer,持久化到磁盘的过程,对应的 LSN 分别是 50、120 和 160。
+>
+> 1. trx1 是第一个到达的,会被选为这组的 leader;
+> 2. 等 trx1 要开始写盘的时候,这个组里面已经有了三个事务,这时候 LSN 也变成了 160;
+> 3. trx1 去写盘的时候,带的就是 LSN=160,因此等 trx1 返回时,所有 LSN 小于等于 160 的 redo log,都已经被持久化到磁盘;
+> 4. 这时候 trx2 和 trx3 就可以直接返回了。
+>
+> 所以,一次组提交里面,组员越多,节约磁盘 IOPS 的效果越好。
#### Checkpoint
-CheckPiont的意思是检查点,用于推进Redo Log的失效。当触发Checkpoint后,会去看Flush List中最早的那个节点old_lsn是多少,也就是说当前Flush List还剩的最早被修改的数据页的redo log lsn是多少,并且将这个lsn记录到Checkpoint中,因为在这之前被修改的数据页都已经刷新到磁盘了,对应的redo log也就无效了,所以说之后在这个old_lsn之后的redo log才是有用的。这就解释了之前说的redo log文件组如何覆盖无效日志。
+CheckPoint 的意思是检查点,用于推进 Redo Log 的失效。当触发 Checkpoint 后,会去看 Flush List 中最早的那个节点 old_lsn 是多少,也就是说当前 Flush List 还剩的最早被修改的数据页的 redo log lsn 是多少,并且将这个 lsn 记录到 Checkpoint 中,因为在这之前被修改的数据页都已经刷新到磁盘了,对应的 redo log 也就无效了,所以说之后在这个 old_lsn 之后的 redo log 才是有用的。这就解释了之前说的 redo log 文件组如何覆盖无效日志。
@@ -258,13 +336,13 @@ CheckPiont的意思是检查点,用于推进Redo Log的失效。当触发Check
我们小结下 redo log 的过程
-
+
以更新事务为例
1. 将原始数据读入内存,修改数据的内存副本。
-2. 先将内存中Buffer pool 的脏页写入到 Redo log buffer 当中**记录数据的变化**。然后再将 redo log buffer 当中记录数据变化的日志通过**顺序IO**刷新到磁盘的 redo log file 当中
+2. 先将内存中 Buffer pool 的脏页写入到 Redo log buffer 当中**记录数据的变化**。然后再将 redo log buffer 当中记录数据变化的日志通过 **顺序IO** 刷新到磁盘的 redo log file 当中
> 在缓冲池中有一条 Flush 链表用来维护被修改的数据页面,也就是脏页所组成的链表。
@@ -276,10 +354,17 @@ CheckPiont的意思是检查点,用于推进Redo Log的失效。当触发Check
6. 随后正常将内存中的脏页刷回磁盘。
-
+### 2.4 redo 小结
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 **crash-safe**。
+所以有了 redo log,再通过 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为 **crash-safe**(崩溃恢复)。可以看出来, **redo log 保证了事务四大特性中的持久性**。
+
+redo log 作用:
+
+- **实现事务的持久性,让 MySQL 有 crash-safe 的能力**,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
+- **将写操作从「随机写」变成了「顺序写」**,提升 MySQL 写入磁盘的性能。
+
## 三、回滚日志(undo log)
@@ -288,29 +373,29 @@ CheckPiont的意思是检查点,用于推进Redo Log的失效。当触发Check
当事务执行的时候,回滚日志中记录了事务中每次数据更新前的状态。当事务需要回滚的时候,可以通过读取回滚日志,恢复到指定的位置。另一方面,回滚日志也可以让其他的事务读取到这个事务对数据更改之前的值,从而确保了其他事务可以不受这个事务修改数据的影响。
-> Undo Log 是 InnoDB 十分重要的组成部分,它的作用横贯 InnoDB 中两个最主要的部分,并发控制(Concurrency Control)和故障恢复(Crash Recovery)。
+> undo Log 是 InnoDB 十分重要的组成部分,它的作用横贯 InnoDB 中两个最主要的部分,并发控制(Concurrency Control)和故障恢复(Crash Recovery)。
>
-> - Undo Log用来记录每次修改之前的历史值,配合Redo Log用于故障恢复
+> - Undo Log 用来记录每次修改之前的历史值,配合 Redo Log 用于故障恢复
#### 3.1 为什么需要 undo log
##### 事务回滚
-由于如硬件故障,软件Bug,运维操作等原因的存在,数据库在任何时刻都有突然崩溃的可能。
+由于如硬件故障,软件 Bug,运维操作等原因的存在,数据库在任何时刻都有突然崩溃的可能。
-这个时候没有完成提交的事务可能已经有部分数据写入了磁盘,如果不加处理,会违反数据库对原子性的保证。
+这个时候没有完成提交的事务可能已经有部分数据写入了磁盘,如果不加处理,会违反数据库对**原子性**的保证。
针对这个问题,直观的想法是等到事务真正提交时,才能允许这个事务的任何修改落盘,也就是 No-Steal 策略。显而易见,这种做法一方面造成很大的内存空间压力,另一方面提交时的大量随机 IO 会极大的影响性能。因此,数据库实现中通常会在正常事务进行中,就不断的连续写入 Undo Log,来记录本次修改之前的历史值。当 Crash 真正发生时,可以在 Recovery 过程中通过回放 Undo Log 将未提交事务的修改抹掉。InnoDB 采用的就是这种方式。
##### MVCC(Multi-Versioin Concurrency Control)
-用于MVCC(实现非锁定读),读取一行记录时,若已被其他事务占据,则通过undo读取之前的版本
+用于 MVCC(实现非锁定读),读取一行记录时,若已被其他事务占据,则通过 undo 读取之前的版本。
为了避免只读事务与写事务之间的冲突,避免写操作等待读操作,几乎所有的主流数据库都采用了多版本并发控制(MVCC)的方式,也就是为每条记录保存多份历史数据供读事务访问,新的写入只需要添加新的版本即可,无需等待。
-InnoDB在这里复用了Undo Log中已经记录的历史版本数据来满足 MVCC 的需求。
+InnoDB 在这里复用了 Undo Log 中已经记录的历史版本数据来满足 MVCC 的需求。
-InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是说,Undo Log日志本身也像其他的数据库数据一样,会写自己对应的Redo Log,通过 Redo Log 来保证自己的原子性。因此,更合适的称呼应该是 **Undo Data**。
+InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是说,Undo Log 日志本身也像其他的数据库数据一样,会写自己对应的Redo Log,通过 Redo Log 来保证自己的原子性。因此,更合适的称呼应该是 **Undo Data**。
@@ -318,13 +403,6 @@ InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是
>
> 执行一条语句是否自动提交事务,是由 `autocommit` 参数决定的,默认是开启。所以,执行一条 update 语句也是会使用事务的。
>
-> 那么,考虑一个问题。一个事务在执行过程中,在还没有提交事务之前,如果MySQL 发生了崩溃,要怎么回滚到事务之前的数据呢?
->
-> 如果我们每次在事务执行过程中,都记录下回滚时需要的信息到一个日志里,那么在事务执行中途发生了 MySQL 崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过这个日志回滚到事务之前的数据。
->
-> 实现这一机制就是 **undo log(回滚日志),它保证了事务的 [ACID 特性 (opens new window)](https://xiaolincoding.com/mysql/transaction/mvcc.html#事务有哪些特性)中的原子性(Atomicity)**。
->
-> undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
@@ -334,9 +412,8 @@ InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是
>
> Undo Log 需要的是事务之间的并发,以及方便的多版本数据维护,其重放逻辑不希望因 DB 的物理存储变化而变化。因此,InnoDB 中的 Undo Log 采用了基于事务的 **Logical Logging** 的方式。
>
-> InnoDB中其实是把Undo当做一种数据来维护和使用的,也就是说,Undo Log日志本身也像其他的数据库数据一样,会写自己对应的Redo Log,通过Redo Log来保证自己的原子性。因此,更合适的称呼应该是**Undo Data**。
-
+
> 各个版本的 MySQL,undo tablespaces 存储有一些差距,我们以 8.0 版本说明
@@ -346,19 +423,19 @@ InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是
- **insert undo log,是在 insert 操作中产生的。**
- insert操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo log 可以在事务提交后直接删除而不需要进行purge操作。
+ insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo log 可以在事务提交后直接删除而不需要进行purge操作。
- Insert Undo Record仅仅是为了可能的事务回滚准备的,并不在MVCC功能中承担作用。
+ Insert Undo Record 仅仅是为了可能的事务回滚准备的,并不在 MVCC 功能中承担作用。
- 
+ 
存在一组长度不定的 Key Fields,因为对应表的主键可能由多个 field 组成,这里需要记录 Record 完整的主键信息,回滚的时候可以通过这个信息在索引中定位到对应的 Record。
- **update undo log 是 update 或 delete 操作中产生。**
- 由于MVCC需要保留Record的多个历史版本,当某个Record的历史版本还在被使用时,这个Record是不能被真正的删除的。
+ 由于 MVCC 需要保留 Record 的多个历史版本,当某个 Record 的历史版本还在被使用时,这个 Record 是不能被真正的删除的。
- 因此,当需要删除时,其实只是修改对应Record的Delete Mark标记。对应的,如果这时这个Record又重新插入,其实也只是修改一下Delete Mark标记,也就是将这两种情况的delete和insert转变成了update操作。再加上常规的Record修改,因此这里的Update Undo Record会对应三种Type:
+ 因此,当需要删除时,其实只是修改对应 Record 的Delete Mark标记。对应的,如果这时这个Record又重新插入,其实也只是修改一下Delete Mark标记,也就是将这两种情况的delete和insert转变成了update操作。再加上常规的Record修改,因此这里的Update Undo Record会对应三种Type:
- TRX_UNDO_UPD_EXIST_REC
- TRX_UNDO_DEL_MARK_REC
@@ -366,9 +443,9 @@ InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是
他们的存储内容也类似,我们看下 TRX_UNDO_UPD_EXIST_REC
- 
+ 
- 除了跟 Insert Undo Record 相同的头尾信息,以及主键 Key Fileds之外,Update Undo Record 增加了:
+ 除了跟 Insert Undo Record 相同的头尾信息,以及主键 Key Fileds 之外,Update Undo Record 增加了:
- Transaction Id记录了产生这个历史版本事务Id,用作后续MVCC中的版本可见性判断
- Rollptr指向的是该记录的上一个版本的位置,包括space number,page number和page内的offset。沿着Rollptr可以找到一个Record的所有历史版本。
@@ -384,15 +461,15 @@ InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是
每当 InnoDB 中需要修改某个 Record 时,都会将其历史版本写入一个 Undo Log 中,对应的 Undo Record 是 Update 类型。
-当插入新的Record 时,还没有一个历史版本,但为了方便事务回滚时做逆向(Delete)操作,这里还是会写入一个Insert类型的 Undo Record。
+当插入新的 Record 时,还没有一个历史版本,但为了方便事务回滚时做逆向(Delete)操作,这里还是会写入一个 Insert 类型的 Undo Record。
#### 「组织方式」
-每一次的修改都会产生至少一个Undo Record,那么大量 Undo Record 如何组织起来,来支持高效的访问和管理呢?
+每一次的修改都会产生至少一个 Undo Record,那么大量 Undo Record 如何组织起来,来支持高效的访问和管理呢?
-每个事务其实会修改一组的 Record,对应的也就会产生一组Undo Record,这些Undo Record收尾相连就组成了这个事务的**Undo Log**。除了一个个的 Undo Record 之外,还在开头增加了一个Undo Log Header来记录一些必要的控制信息,因此,一个Undo Log的结构如下所示:
+每个事务其实会修改一组的 Record,对应的也就会产生一组 Undo Record,这些 Undo Record 首尾相连就组成了这个事务的**Undo Log**。除了一个个的 Undo Record 之外,还在开头增加了一个Undo Log Header 来记录一些必要的控制信息,因此,一个 Undo Log 的结构如下所示:
-
+
- Trx Id:事务Id
- Trx No:事务的提交顺序,也会用这个来判断是否能Purge
@@ -404,15 +481,17 @@ InnoDB 中其实是把 Undo 当做一种数据来维护和使用的,也就是
- Prev Undo Log:标记前边的Undo Log
- History List Node:
-
+索引中的同一个 Record 被不同事务修改,会产生不同的历史版本,这些历史版本又通过 **Rollptr** 串成一个链表,供 MVCC 使用。如下图所示:
+
+> 示例中有三个事务操作了表 t 上,主键 id 是 1 的记录,首先事务 X 插入了一条记录,事务 Y、Z 去修改了这条记录。X,Y,Z 三个事务分别有自己的逻辑上连续的三条 Undo Log,每条 Undo Log 有自己的 Undo Log Header。从索引中的这条 Record 沿着 Rollptr 可以依次找到这三个事务 Undo Log 中关于这条记录的历史版本。同时可以看出,Insert 类型 Undo Record 中只记录了对应的主键值:id=1,而 Update 类型的 Undo Record 中还记录了对应的历史版本的生成事务 Trx_id,以及被修改的 name 的历史值。
- undo是逻辑日志,只是将数据库**逻辑的**恢复到执行语句或事务之前。
+undo 是逻辑日志,只是将数据库**逻辑的**恢复到执行语句或事务之前。
-我们知道 InnoDB 中默认以 块 为单位存储,一个块默认是16KB。那么如何用固定的块大小承载不定长的Undo Log,以实现高效的空间分配、复用,避免空间浪费。InnoDB的**基本思路**是让多个较小的Undo Log紧凑存在一个Undo Page中,而对较大的Undo Log则随着不断的写入,按需分配足够多的Undo Page分散承载
+我们知道 InnoDB 中默认以 块 为单位存储,一个块默认是 16KB。那么如何用固定的块大小承载不定长的 Undo Log,以实现高效的空间分配、复用,避免空间浪费。InnoDB 的**基本思路**是让多个较小的 Undo Log 紧凑存在一个 Undo Page 中,而对较大的 Undo Log 则随着不断的写入,按需分配足够多的 Undo Page 分散承载
-
+
Undo 的物理组织格式是—— Undo Segment,它会持有至少一个 Undo Page。
@@ -426,72 +505,228 @@ Undo 的文件组织格式是——Undo Tablespace,每个 Undo Tablespace 最
#### 3.3 MVCC 是如何实现的
-多版本的目的是为了避免写事务和读事务的互相等待,那么每个读事务都需要在不对Record加Lock的情况下, 找到对应的应该看到的历史版本。所谓历史版本就是假设在该只读事务开始的时候对整个DB打一个快照,之后该事务的所有读请求都从这个快照上获取。当然实现上不能真正去为每个事务打一个快照,这个时间空间都太高了。InnoDB的做法,是在读事务第一次读取的时候获取一份ReadView,并一直持有,其中记录所有当前活跃的写事务ID,由于写事务的ID是自增分配的,通过这个ReadView我们可以知道在这一瞬间,哪些事务已经提交哪些还在运行,根据Read Committed的要求,未提交的事务的修改就是不应该被看见的,对应地,已经提交的事务的修改应该被看到。
+多版本的目的是为了避免写事务和读事务的互相等待,那么每个读事务都需要在不对 Record 加 Lock 的情况下, 找到对应的应该看到的历史版本。所谓历史版本就是假设在该只读事务开始的时候对整个 DB 打一个快照,之后该事务的所有读请求都从这个快照上获取。当然实现上不能真正去为每个事务打一个快照,这个时间空间成本都太高了。
-作为存储历史版本的Undo Record,其中记录的trx_id就是做这个可见性判断的,对应的主索引的Record上也有这个值。当一个读事务拿着自己的ReadView访问某个表索引上的记录时,会通过比较Record上的trx_id确定是否是可见的版本,如果不可见就沿着Record或Undo Record中记录的rollptr一路找更老的历史版本。如下图所示,事务R开始需要查询表t上的id为1的记录,R开始时事务I已经提交,事务J还在运行,事务K还没开始,这些信息都被记录在了事务R的ReadView中。事务R从索引中找到对应的这条Record[1, C],对应的trx_id是K,不可见。沿着Rollptr找到Undo中的前一版本[1, B],对应的trx_id是J,不可见。继续沿着Rollptr找到[1, A],trx_id是I可见,返回结果。
+> MVCC 的实现还有一个概念:快照读,快照信息就记录在 undo 中
+>
+> 所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的**普通的SELECT语句在不加锁情况下就是快照读**。如:
+>
+> ```mysql
+> SELECT * FROM t WHERE ...
+> ```
+>
+> 和快照读相对应的另外一个概念叫做当前读,当前读就是读取最新数据,所以,**加锁的 SELECT,或者对数据进行增删改都会进行当前读**,比如:
+>
+> ```mysql
+> SELECT * FROM t LOCK IN SHARE MODE;
+>
+> SELECT * FROM t FOR UPDATE;
+>
+> INSERT INTO t ...
+>
+> DELETE FROM t ...
+>
+> UPDATE t ...
+> ```
-
+InnoDB **通过 ReadView + undo log 实现 MVCC**
-前面提到过,作为Logical Log,Undo中记录的其实是前后两个版本的diff信息,而读操作最终是要获得完整的Record内容的,也就是说这个沿着rollptr指针一路查找的过程中需要用Undo Record中的diff内容依次构造出对应的历史版本,这个过程在函数**row_search_mvcc**中,其中**trx_undo_prev_version_build**会根据当前的rollptr找到对应的Undo Record位置,这里如果是rollptr指向的是insert类型,或者找到了已经Purge了的位置,说明到头了,会直接返回失败。否则,就会解析对应的Undo Record,恢复出trx_id、指向下一条Undo Record的rollptr、主键信息,diff信息update vector等信息。之后通过**row_upd_rec_in_place**,用update vector修改当前持有的Record拷贝中的信息,获得Record的这个历史版本。之后调用自己ReadView的**changes_visible**判断可见性,如果可见则返回用户。完成这个历史版本的读取。
+对于「读提交」和「可重复读」隔离级别的事务来说,它们的快照读(普通 select 语句)是通过 Read View + undo log 来实现的,它们的区别在于创建 Read View 的时机不同:
+- 「读提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
+- 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。
+这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列(trx_id 和 roll_pointer)」的比对,如果不满足可见性,就会顺着 undo log 版本链里找到满足其可见性的记录,从而控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。
-#### 3.4 Undo Log 清理
+InnoDB 的做法,是在读事务第一次读取的时候获取一份 ReadView,并一直持有,其中记录所有当前活跃的写事务 ID,由于写事务的 ID 是自增分配的,通过这个 ReadView 我们可以知道在这一瞬间,哪些事务已经提交哪些还在运行,根据 Read Committed 的要求,未提交的事务的修改就是不应该被看见的,对应地,已经提交的事务的修改应该被看到。
-我们已经知道,InnoDB在Undo Log中保存了多份历史版本来实现MVCC,当某个历史版本已经确认不会被任何现有的和未来的事务看到的时候,就应该被清理掉。
+> **Read View 主要来帮我们解决可见性的问题的**, 即他会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。
+>
+> 在 Read View 中有几个重要的属性:
+>
+> - trx_ids,系统当前未提交的事务 ID 的列表。
+> - low_limit_id,未提交的事务中最大的事务 ID。
+> - up_limit_id,未提交的事务中最小的事务 ID。
+> - creator_trx_id,创建这个 Read View 的事务 ID。
+>
+> 每开启一个事务,我们都会从数据库中获得一个事务 ID,这个事务 ID 是自增长的,通过 ID 大小,我们就可以判断事务的时间顺序。
+
+作为存储历史版本的 Undo Record,其中记录的 trx_id 就是做这个可见性判断的,对应的主索引的 Record 上也有这个值。当一个读事务拿着自己的 ReadView 访问某个表索引上的记录时,会通过比较 Record 上的 trx_id 确定是否是可见的版本,如果不可见就沿着 Record 或 Undo Record 中记录的 rollptr 一路找更老的历史版本。
+
+具体的事务 id,指向 undo log 的指针 rollptr,这些信息是放在哪里呢,这就是我们常说的 InnoDB 隐藏字段了
> #### InnoDB存储引擎的行结构
>
-> InnoDB表数据的组织方式为主键聚簇索引,二级索引中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录。
-> InnoDB表数据为主键聚簇索引,mysql默认为每个索引行添加了4个隐藏的字段,分别是:
+> InnoDB 表数据的组织方式为主键聚簇索引,二级索引中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录。
+> InnoDB表数据为主键聚簇索引,mysql默认为每个索引行添加了4个隐藏的字段,分别是:
>
> - DB_ROW_ID:InnoDB引擎中一个表只能有一个主键,用于聚簇索引,如果表没有定义主键会选择第一个非Null 的唯一索引作为主键,如果还没有,生成一个隐藏的DB_ROW_ID作为主键构造聚簇索引。
> - DB_TRX_ID:最近更改该行数据的事务ID。
-> - DB_ROLL_PTR:undo log的指针,用于记录之前历史数据在undo log中的位置。
-> - DELETE BIT:索引删除标志,如果DB删除了一条数据,是优先通知索引将该标志位设置为1,然后通过(purge)清除线程去异步删除真实的数据。
+> - DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,其实他指向的就是 Undo Log 中的上一个版本的快照的地址
+> - DELETE BIT:索引删除标志,如果DB删除了一条数据,是优先通知索引将该标志位设置为1,然后通过(purge)清除线程去异步删除真实的数据。
+>
+
+如下图所示,事务 R 需要查询表 t 上的 id 为 1 的记录,R 开始时事务 X 已经提交,事务 Y 还在运行,事务 Z 还没开始,这些信息都被记录在了事务 R 的 ReadView 中。事务 R 从索引中找到对应的这条 Record[1, stafish],对应的 trx_id 是 Z,不可见。沿着 Rollptr 找到Undo 中的前一版本[1, fish],对应的 trx_id 是 Y,不可见。继续沿着 Rollptr 找到[1, star],trx_id是 X 可见,返回结果。
+
+
+
+前面提到过,作为 Logical Log,Undo 中记录的其实是前后两个版本的 diff 信息,而读操作最终是要获得完整的 Record 内容的,也就是说这个沿着 rollptr 指针一路查找的过程中需要用 Undo Record 中的 diff 内容依次构造出对应的历史版本,这个过程在函数 **row_search_mvcc **中,其中 **trx_undo_prev_version_build** 会根据当前的 rollptr 找到对应的 Undo Record 位置,这里如果是 rollptr指向的是 insert 类型,或者找到了已经 Purge 了的位置,说明到头了,会直接返回失败。否则,就会解析对应的 Undo Record,恢复出trx_id、指向下一条 Undo Record 的 rollptr、主键信息,diff 信息 update vector 等信息。之后通过 **row_upd_rec_in_place**,用update vector 修改当前持有的 Record 拷贝中的信息,获得 Record 的这个历史版本。之后调用自己 ReadView 的 **changes_visible** 判断可见性,如果可见则返回用户。完成这个历史版本的读取。
+
+
+
+#### 3.4 Undo Log 清理
+
+我们已经知道,InnoDB 在 Undo Log 中保存了多份历史版本来实现 MVCC,当某个历史版本已经确认不会被任何现有的和未来的事务看到的时候,就应该被清理掉。
+
+InnoDB中每个写事务结束时都会拿一个递增的编号**trx_no**作为事务的提交序号,而每个读事务会在自己的ReadView中记录自己开始的时候看到的最大的trx_no为**m_low_limit_no**。那么,如果一个事务的trx_no小于当前所有活跃的读事务Readview中的这个**m_low_limit_no**,说明这个事务在所有的读开始之前已经提交了,其修改的新版本是可见的, 因此不再需要通过undo构建之前的版本,这个事务的Undo Log也就可以被清理了。
+
+
+
+> redo log 和 undo log 区别在哪?
>
-> 
+> - redo log 记录了此次事务「**完成后**」的数据状态,记录的是更新**之后**的值;
+> - undo log 记录了此次事务「**开始前**」的数据状态,记录的是更新**之前**的值;
## 四、二进制日志(binlog)
-二进制日志,也被叫做归档日志,是 Server 层生成的日志,主要**用于数据备份和主从复制**
+前面我们讲过,MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的 redo log 和 undo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(二进制日志)。
+
+二进制日志,也被叫做「归档日志」,主要**用于数据备份和主从复制**
-- **主从复制**:在`Master`端开启`binlog`,然后将`binlog`发送到各个`Slave`端,`Slave`端重放`binlog`从而达到主从数据一致。
+- **主从复制**:在 `Master` 端开启 `binlog`,然后将 `binlog` 发送到各个 `Slave` 端,`Slave` 端重放 `binlog` 从而达到主从数据一致
- **数据恢复**:可以用 `mysqldump` 做数据备份,binlog 格式是二进制日志,可以使用 `mysqlbinlog` 工具解析,实现数据恢复
二进制日志主要记录数据库的更新事件,比如创建数据表、更新表中的数据、数据更新所花费的时长等信息。通过这些信息,我们可以再现数据更新操作的全过程。而且,由于日志的延续性和时效性,我们还可以利用日志,完成无损失的数据恢复和主从服务器之间的数据同步。
-这就是我们最常说的 binlog
-前面我们讲过,MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的粉板 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(二进制日志)。
-我想你肯定会问,为什么会有两份日志呢?
+### 4.1 binlog VS redolog
+
+是不会有点疑惑,binlog 和 redo log 是不是有点重复?这个问题跟 MySQL 的时间线有关系。
因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
-这两种日志有以下三点不同。
+这两种日志有以下四点区别。
1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
+4. redo log 用于掉电等故障恢复。binlog 用于备份恢复、主从复制
+
+
+
+### 4.2 查看 binlog
+
+查看二进制日志主要有 3 种情况,分别是查看当前正在写入的二进制日志、查看所有的二进制日志和查看二进制日志中的所有数据更新事件。
+
+```mysql
+mysql> show master status;
++---------------+----------+--------------+------------------+-------------------+
+| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
++---------------+----------+--------------+------------------+-------------------+
+| binlog.000002 | 27736 | | | |
++---------------+----------+--------------+------------------+-------------------+
+1 row in set (0.00 sec)
+```
+
+```mysql
+mysql> show binary logs;
++---------------+-----------+-----------+
+| Log_name | File_size | Encrypted |
++---------------+-----------+-----------+
+| binlog.000001 | 638 | No |
+| binlog.000002 | 27736 | No |
++---------------+-----------+-----------+
+2 rows in set (0.01 sec)
+```
+
+```mysql
+mysql> show variables like '%binlog_format%';
++---------------+-------+
+| Variable_name | Value |
++---------------+-------+
+| binlog_format | ROW |
++---------------+-------+
+1 row in set (0.00 sec)
+```
+
+
+
+### 4.3 Binary logging formats
+
+`binlog`日志有三种格式,分别为`STATMENT`、`ROW`和`MIXED`。
+
+> 在 `MySQL 5.7.7`之前,默认的格式是`STATEMENT`,`MySQL 5.7.7`之后,默认值是 `ROW`。日志格式通过 `binlog-format` 指定。
-有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
+- `STATMENT` :基于 SQL 语句的复制(`statement-based replication, SBR`),每一条会修改数据的 sql 语句会记录到 binlog 中**。**
+ - 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO, 从而提高了性能;
+ - 缺点:在某些情况下会导致主从数据不一致,比如执行`sysdate()`、`slepp()`等。
-1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
-2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
+- `ROW` :基于行的复制(`row-based replication, RBR`),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了**。 **
+ - 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题**;**
+ - 缺点:会产生大量的日志,尤其是 `alter table` 的时候会让日志暴涨
+
+- `MIXED` :基于 STATMENT 和 ROW 两种模式的混合复制(`mixed-based replication, MBR`),mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式
+
+> ```
+>SET GLOBAL binlog_format = 'STATEMENT';
+> SET GLOBAL binlog_format = 'ROW';
+> SET GLOBAL binlog_format = 'MIXED';
+> ```
+>
+
+
+
+### 4.4 binlog 的写入机制
+
+binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
+
+一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。这就涉及到了 binlog cache 的保存问题。
+
+系统给 binlog cache 分配了一片内存,每个线程一个,参数 `binlog_cache_size` 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
+
+事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache。
+
+
+
+
+
+可以看到,每个线程有自己 binlog cache,但是共用同一份 binlog 文件。
+
+- 图中的 write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
+- 图中的 fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync 才占磁盘的 IOPS。
+
+write 和 fsync 的时机,是由参数 sync_binlog 控制的:
+
+1. sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
+2. sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
+3. sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
+
+因此,在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成 0,比较常见的是将其设置为 100~1000 中的某个数值。
+
+但是,将 sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。
+
+
+
+### 4.5 update 语句执行过程
+
+比较重要的 undo、redo、binlog 都介绍完了,我们来看执行器和 InnoDB 引擎在执行一个简单的 update 语句时的内部流程。`update t set name='starfish' where id = 1;`
+
+1. 执行器先找引擎取 id=1 这一行。id 是主键,引擎直接用树搜索找到这一行。如果 id=1 这一行所在的数据页本来就在内存(buffer pool)中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
+2. 执行器拿到引擎给的行数据,更新行数据,再调用引擎接口写入这行新数据。
3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
这里我给出这个 update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。
-
+
你可能注意到了,最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
-### 两阶段提交
+### 4.6 两阶段提交
为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。要说明这个问题,我们得从文章开头的那个问题说起:**怎样让数据库恢复到半个月内任意一秒的状态?**
@@ -523,68 +758,51 @@ Undo 的文件组织格式是——Undo Tablespace,每个 Undo Tablespace 最
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
-
-
+### 4.7 主从同步
+MySQL主从同步的作用主要有以下几点:
+- 故障切换。
+- 提供一定程度上的备份服务。
+- 实现MySQL数据库的读写分离。
+MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
-### 查看 binlog
+这个过程一般是**异步**的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。
-查看二进制日志主要有 3 种情况,分别是查看当前正在写入的二进制日志、查看所有的二进制日志和查看二进制日志中的所有数据更新事件。
+
-```mysql
-mysql> show master status;
-+---------------+----------+--------------+------------------+-------------------+
-| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
-+---------------+----------+--------------+------------------+-------------------+
-| binlog.000002 | 27736 | | | |
-+---------------+----------+--------------+------------------+-------------------+
-1 row in set (0.00 sec)
-```
+具体详细过程如下:
-```mysql
-mysql> show binary logs;
-+---------------+-----------+-----------+
-| Log_name | File_size | Encrypted |
-+---------------+-----------+-----------+
-| binlog.000001 | 638 | No |
-| binlog.000002 | 27736 | No |
-+---------------+-----------+-----------+
-2 rows in set (0.01 sec)
-```
+- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
+- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
+- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
+在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
+> #### 中继日志(relay log)
+>
+> 中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。
+>
+> Relay log(中继日志)是在MySQL主从复制时产生的日志,在MySQL的主从复制主要涉及到三个线程:
+>
+> 1) Log dump线程:向从库的IO线程传输主库的Binlog日志
+>
+> 2) IO线程:向主库请求Binlog日志,并将Binlog日志写入到本地的relay log中。
+>
+> 3) SQL线程:读取Relay log日志,将其解析为SQL语句并逐一执行。
+>
+>
-### **Binary logging formats**
-
-`binlog`日志有三种格式,分别为`STATMENT`、`ROW`和`MIXED`。
-
-> 在 `MySQL 5.7.7`之前,默认的格式是`STATEMENT`,`MySQL 5.7.7`之后,默认值是`ROW`。日志格式通过`binlog-format`指定。
-
-- `STATMENT` **基于`SQL`语句的复制(`statement-based replication, SBR`),每一条会修改数据的sql语句会记录到`binlog`中**。 优点:**不需要记录每一行的变化,减少了`binlog`日志量,节约了`IO`, 从而提高了性能**; 缺点:**在某些情况下会导致主从数据不一致,比如执行`sysdate()`、`slepp()`等**。
-- `ROW` **基于行的复制(`row-based replication, RBR`),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了**。 优点:**不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题**; 缺点:**会产生大量的日志,尤其是`alter table`的时候会让日志暴涨**
-- `MIXED` **基于`STATMENT`和`ROW`两种模式的混合复制(`mixed-based replication, MBR`),一般的复制使用`STATEMENT`模式保存`binlog`,对于`STATEMENT`模式无法复制的操作使用`ROW`模式保存`binlog`**
-
-
-
-MySQL database logs offer three formats for binary logging.
-
-- **Statement-based logging:** In this format, MySQL records the SQL statements that produce data changes. Statement-based logging is useful when many rows are affected by an event because it is more efficient to log a few statements than many rows.
-- **Row-based logging:** In this format, changes to individual rows are recorded instead of the SQL statements. This is useful for queries that require a lot of execution time on the source but result in just a few rows being modified.
-- **Mixed logging:** This is the recommended logging format. It uses statement-based logging by default but switches to row-based logging when required.
-
-The binary logging format can be changed using the code below. However, you should note that it is not recommended to do so at runtime or while replication is ongoing.
+> MySQL 主从复制还有哪些模型?
-```
-SET GLOBAL binlog_format = 'STATEMENT';
-SET GLOBAL binlog_format = 'ROW';
-SET GLOBAL binlog_format = 'MIXED';
-```
+主要有三种:
-Enabling binary logging on your MySQL instance will lower the performance slightly. However, the advantages discussed above generally outweigh this minor dip in performance.
+- **同步复制**:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
+- **异步复制**(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
+- **半同步复制**:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种**半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险**。
@@ -657,80 +875,59 @@ mysql> show variables like '%row_limit%';
+## 小结
+感谢你读到这里,送你两道面试题吧
+### 说下 一条 MySQL 更新语句的执行流程是怎样的吧?
+```mysql
+mysql> update t set name='starfish' where salary > 999999;
+```
-## 七、中继日志(relay log)
-
-中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。
-
-Relay log(中继日志)是在MySQL主从复制时产生的日志,在MySQL的主从复制主要涉及到三个线程:
-
-\1) Log dump线程:向从库的IO线程传输主库的Binlog日志
-
-\2) IO线程:向主库请求Binlog日志,并将Binlog日志写入到本地的relay log中。
-
-\3) SQL线程:读取Relay log日志,将其解析为SQL语句并逐一执行。
-
-
-
-图2
-
-从图2中可以看出,从库的IO线程接收到主库的logdump线程传递的Binlog日志后,会将其写入到本地的一个日志中,这个日志就是Relaylog。在文件目录中,一般由多个host_name-relay-bin.nnnnnn 的日志文件和host_name-relay-bin.index索引文件组成,其中日志文件记录的是事务中修改数据的信息,索引文件记录的是使用过的日志文件信息。
-
-Relaylog日志的格式与Binlog的一致,但是相较于Binlog多了master.info和relay-log.info两个日志(默认存储于数据文件目录中)。master.info主要记录上一次读取到master同步过来的binlog的位置,从节点的连接信息和主节点信息,以及连接master和启动复制必须的所有信息。relay-log.info主要记录了从节点文件复制的进度,下一个事件从什么位置开始,由sql线程负责更新。
-
-
-
+server 层和 InnoDB 层之间是如何沟通:
+1. salary 有二级索引,行器先找引擎取扫描区间的第一行。根据这条二级索引记录中的主键值执行回表操作(即通过聚簇索引的B+树根节点一层一层向下找,直到在叶子节点中找到相应记录),将获取到的聚簇索引记录返回给 server 层。
+2. server 层得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样,如果一样的话就不更新了,如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作
+3. InnoDB 收到更新请求后,先更新记录的聚簇索引记录,再更新记录的二级索引记录。最后将更新结果返回给 server 层
+4. server 层继续向 InnoDB 索要下一条记录,由于已经通过 B+ 树定位到二级索引扫描区间 `[999999, +∞)` 的第一条二级索引记录,而记录又是被串联成单向链表,所以 InnoDB 直接通过记录头信息的 `next_record` 的属性即可获取到下一条二级索引记录。然后通过该二级索引的主键值进行回表操作,获取到完整的聚簇索引记录再返回给 server 层。
+5. 就这样一层一层的处理
-
+具体执行流程:
+1. 先在 B+ 树中定位到该记录(这个过程也被称作**加锁读**),如果该记录所在的页面不在 buffer pool 里,先将其加载到 buffer pool 里再读取。
+2. 首先更新聚簇索引记录。 更新聚簇索引记录时:
-## 小结
+ ① 先向 Undo 页面写 undo 日志。不过由于这是在更改页面,所以修改 Undo 页面前需要先记录一下相应的 redo 日志。
-感谢你读到这里,送你两道面试题吧
+ ② 将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
-### 说下 一条 MySQL 更新语句的执行流程是怎样的吧?
+ > 这里可以会有点疑惑。我们可以直接理解成先写 undo 再写 redo,这里修改后的页面并没有加入 buffer pool 的 flush 链表,记录的 redo 日志也没有加入到 redo log buffer。当这个函数执行完后,才会:先将这个过程产生的 redo 日志写入到 redo log buffer,再将这个过程修改的页面加入到 buffer pool 的 flush 链表中。
-```
-mysql> update T set c=c+1 where ID=2;
-```
+3. 更新其他的二级索引记录。
-1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
-2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
-3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
-4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
-5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
+ > 更新二级索引记录时不会再记录 undo 日志,但由于是在修改页面内容,会先记录相应的 redo 日志。
-> 最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
->
-> redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
+4. 记录该语句对应的 binlog 日志,此时记录的 binlog 并没有刷新到硬盘上的 binlog 日志文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 日志刷新到硬盘。
-
+5. 引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
-- 客户端先通过连接器建立连接,连接器自会判断用户身份;
-- 因为这是一条 update 语句,所以不需要经过查询缓存,但是表上有更新语句,是会把整个表的查询缓存情空的,所以说查询缓存很鸡肋,在 MySQL 8.0 就被移除这个功能了;
-- 解析器会通过词法分析识别出关键字 update,表名等等,构建出语法树,接着还会做语法分析,判断输入的语句是否符合 MySQL 语法;
-- 预处理器会判断表和字段是否存在;
-- 优化器确定执行计划,因为 where 条件中的 id 是主键索引,所以决定要使用 id 这个索引;
-- 执行器负责具体执行,找到这一行,然后更新。
+### 日志执行顺序?
+时序上先 undo log,redo log 先 prepare, 再写 binlog,最后再把 redo log commit
+
-### 为什么需要记录REDO
+### 为什么需要记录 REDO
redo log 是 Innodb 存储引擎层生成的日志,实现了事务中的**持久性**,主要**用于掉电等故障恢复**:
-1. 在系统遇到故障的恢复过程中,可以修复被未完成的事务修改的数据。
-2. InnoDB 为了提高数据存取的效率,减少磁盘操作的频率,对数据的更新操作不会立即写到磁盘上,而是把数据更新先保存在内存中(**InnoDB Buffer Pool**),积累到一定程度,再集中进行磁盘读写操作。这样就存在一个问题:一旦出现宕机或者停电等异常情况,内存中保存的数据更新操作可能会丢失。为了保证数据库本身的一致性和**持久性**,InnoDB 维护了 REDO LOG。修改 Page 之前需要先将修改的内容记录到 REDO 中,并保证 REDO LOG 早于对应的 Page 落盘,也就是常说的WAL。当故障发生导致内存数据丢失后,InnoDB 会在重启时,通过重放 REDO,将 Page 恢复到崩溃前的状态。
+1. 在系统遇到故障的恢复过程中,可以修复未完成的事务修改的数据。
+2. InnoDB 为了提高数据存取的效率,减少磁盘操作的频率,对数据的更新操作不会立即写到磁盘上,而是把数据更新先保存在内存中(**InnoDB Buffer Pool**),积累到一定程度,再集中进行磁盘读写操作。这样就存在一个问题:一旦出现宕机或者停电等异常情况,内存中保存的数据更新操作可能会丢失。为了保证数据库本身的一致性和**持久性**,InnoDB 维护了 REDO LOG。修改 Page 之前需要先将修改的内容记录到 REDO 中,并保证 REDO LOG 早于对应的 Page 落盘,也就是常说的 WAL。当故障发生导致内存数据丢失后,InnoDB 会在重启时,通过重放 REDO,将 Page 恢复到崩溃前的状态。
回答面试官问题时候,如果能指明不同版本的差异,会加分的
-### Mysql 日志的执行顺序
-
## References
diff --git a/docs/data-management/MySQL/MySQL-Master-Slave.md b/docs/data-management/MySQL/MySQL-Master-Slave.md
index 30404ce4c5..fb7ba2e62f 100644
--- a/docs/data-management/MySQL/MySQL-Master-Slave.md
+++ b/docs/data-management/MySQL/MySQL-Master-Slave.md
@@ -1 +1,13 @@
-TODO
\ No newline at end of file
+TODO
+
+
+
+
+
+备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:
+
+1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
+2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接。
+3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
+4. 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
+5. sql_thread 读取中转日志,解析出日志里的命令,并执行。
\ No newline at end of file
diff --git a/docs/data-management/MySQL/MySQL-Optimization.md b/docs/data-management/MySQL/MySQL-Optimization.md
index e9689f2a35..89292e3b0e 100644
--- a/docs/data-management/MySQL/MySQL-Optimization.md
+++ b/docs/data-management/MySQL/MySQL-Optimization.md
@@ -1,422 +1,638 @@
-《高性能MySQL》给出的性能定义:完成某件任务所需要的的时间度量,性能既响应时间。
+---
+title: MySQL 优化
+date: 2024-05-09
+tags:
+ - MySQL
+categories: MySQL
+---
-假设性能优化就是在一定负载下尽可能的降低响应时间。
+
-性能监测工具: **New Relic** **OneAPM**
+> 《高性能MySQL》给出的性能定义:完成某件任务所需要的的时间度量,性能既响应时间。
+>
+> 我们主要探讨 Select 的优化,包括 MySQL Server 做了哪些工作以及我们作为开发,如何定位问题,以及如何优化,怎么写出高性能 SQL
-## 1. 影响mysql的性能因素
-##### 1.1 业务需求对mysql的影响(合适合度)
-##### 1.2 存储定位对mysql的影响
+## 一、MySQL Server 优化了什么
-- 不适合放进mysql的数据
- - 二进制多媒体数据
- - 流水队列数据
- - 超大文本数据
-- 需要放进缓存的数据
- - 系统各种配置及规则数据
- - 活跃用户的基本信息数据
- - 活跃用户的个性化定制信息数据
- - 准实时的统计信息数据
- - 其他一些访问频繁但变更较少的数据
+### MySQL Query Optimizer
-##### 1.3 Schema设计对系统的性能影响
+MySQL 中有专门负责优化 SELECT 语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的 Query 提供他认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)
-- 尽量减少对数据库访问的请求
-- 尽量减少无用数据的查询请求
+当客户端向 MySQL 请求一条 Query,命令解析器模块完成请求分类,区别出是 SELECT 并转发给 MySQL Query Optimizer 时,MySQL Query Optimizer 首先会对整条 Query 进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对 Query 中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析 Query 中的 Hint 信息(如果有),看显示 Hint 信息是否可以完全确定该 Query 的执行计划。如果没有 Hint 或Hint 信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据 Query 进行写相应的计算分析,然后再得出最后的执行计划。
-##### 1.4 硬件环境对系统性能的影响
+MySQL 查询优化器是一个复杂的组件,它的主要任务是确定执行给定查询的最优方式。以下是 MySQL 查询优化器在处理查询时所进行的一些关键活动:
-**典型OLTP应用系统**
+#### 1.1 解析查询:
- 什么是OLTP:OLTP即联机事务处理,就是我们经常说的关系数据库,意即记录即时的增、删、改、查,就是我们经常应用的东西,这是数据库的基础
+优化器首先解析查询语句,理解其语法和语义。
-对于各种数据库系统环境中大家最常见的OLTP系统,其特点是并发量大,整体数据量比较多,但每次访问的数据比较少,且访问的数据比较离散,活跃数据占总体数据的比例不是太大。对于这类系统的数据库实际上是最难维护,最难以优化的,对主机整体性能要求也是最高的。因为不仅访问量很高,数据量也不小。
+#### 1.2 词法和语法分析:
-针对上面的这些特点和分析,我们可以对OLTP的得出一个大致的方向。 虽然系统总体数据量较大,但是系统活跃数据在数据总量中所占的比例不大,那么我们可以通过扩大内存容量来尽可能多的将活跃数据cache到内存中; 虽然IO访问非常频繁,但是每次访问的数据量较少且很离散,那么我们对磁盘存储的要求是IOPS表现要很好,吞吐量是次要因素; 并发量很高,CPU每秒所要处理的请求自然也就很多,所以CPU处理能力需要比较强劲; 虽然与客户端的每次交互的数据量并不是特别大,但是网络交互非常频繁,所以主机与客户端交互的网络设备对流量能力也要求不能太弱。
+检查查询语句是否符合SQL语法规则。
-**典型OLAP应用系统**
+#### 1.3 语义分析:
-用于数据分析的OLAP系统的主要特点就是数据量非常大,并发访问不多,但每次访问所需要检索的数据量都比较多,而且数据访问相对较为集中,没有太明显的活跃数据概念。
+确保查询引用的所有数据库对象(如表、列、别名等)都是存在的,并且用户具有相应的访问权限。
-什么是OLAP:OLAP即联机分析处理,是数据仓库的核心部心,所谓数据仓库是对于大量已经由OLTP形成的数据的一种分析型的数据库,用于处理商业智能、决策支持等重要的决策信息;数据仓库是在数据库应用到一定程序之后而对历史数据的加工与分析 基于OLAP系统的各种特点和相应的分析,针对OLAP系统硬件优化的大致策略如下: 数据量非常大,所以磁盘存储系统的单位容量需要尽量大一些; 单次访问数据量较大,而且访问数据比较集中,那么对IO系统的性能要求是需要有尽可能大的每秒IO吞吐量,所以应该选用每秒吞吐量尽可能大的磁盘; 虽然IO性能要求也比较高,但是并发请求较少,所以CPU处理能力较难成为性能瓶颈,所以CPU处理能力没有太苛刻的要求;
+#### 1.4 查询重写:
-虽然每次请求的访问量很大,但是执行过程中的数据大都不会返回给客户端,最终返回给客户端的数据量都较小,所以和客户端交互的网络设备要求并不是太高;
+可能对查询进行一些变换,以提高其效率。例如,使用等价变换简化查询或应用数据库的视图定义。
-此外,由于OLAP系统由于其每次运算过程较长,可以很好的并行化,所以一般的OLAP系统都是由多台主机构成的一个集群,而集群中主机与主机之间的数据交互量一般来说都是非常大的,所以在集群中主机之间的网络设备要求很高。
+#### 1.5 确定执行计划:
+优化器会生成一个或多个可能的执行计划,并估算每个计划的成本(如I/O操作、CPU使用等)。
+#### 1.6 选择最佳执行计划:
-## 2. 性能分析
+执行成本包括 I/O 成本和 CPU 成本。MySQL 有一套自己的计算公式,在一条单表查询语句真正执行之前,MySQL 的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案,这个成本最低的方案就是所谓的`执行计划`,之后才会调用存储引擎提供的接口真正的执行查询。
-### 2.1 MySQL常见瓶颈
+#### 1.7 索引选择:
-- CPU:CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候
+确定是否使用索引以及使用哪个索引。考虑因素包括索引的选择性、查询条件、索引的前缀等。
-- IO:磁盘I/O瓶颈发生在装入数据远大于内存容量的时候
+#### 1.8 表访问顺序:
-- 服务器硬件的性能瓶颈:top,free, iostat和vmstat来查看系统的性能状态
+对于涉及多个表的查询,优化器决定最佳的表访问顺序,以减少数据的访问量。
+#### 1.9 连接算法选择:
+join 我们每天都在用,左、右连接、内连接就不详细介绍了
-**查看Linux系统性能的常用命令**
+
-MySQL数据库是常见的两个瓶颈是CPU和I/O的瓶颈。CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候,磁盘I/O瓶颈发生在装入数据远大于内存容量的时候,如果应用分布在网络上,那么查询量相当大的时候那么瓶颈就会出现在网络上。Linux中我们常用mpstat、vmstat、iostat、sar和top来查看系统的性能状态。
+对于连接操作,优化器会选择最合适的算法,如嵌套循环、块嵌套循环、哈希连接等。
-`mpstat`: mpstat是Multiprocessor Statistics的缩写,是实时系统监控工具。其报告为CPU的一些统计信息,这些信息存放在/proc/stat文件中。在多CPUs系统里,其不但能查看所有CPU的平均状况信息,而且能够查看特定CPU的信息。mpstat最大的特点是可以查看多核心cpu中每个计算核心的统计数据,而类似工具vmstat只能查看系统整体cpu情况。
+- **嵌套循环连接(Nested-Loop Join)**:驱动表只访问一次,但被驱动表却可能被多次访问,访问次数取决于对驱动表执行单表查询后的结果集中的记录条数的连接执行方式称之为`嵌套循环连接`。
-`vmstat`:vmstat命令是最常见的Linux/Unix监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况,IO读写情况。这个命令是我查看Linux/Unix最喜爱的命令,一个是Linux/Unix都支持,二是相比top,我可以看到整个机器的CPU、内存、IO的使用情况,而不是单单看到各个进程的CPU使用率和内存使用率(使用场景不一样)。
+ 左(外)连接的驱动表就是左边的那个表,右(外)连接的驱动表就是右边的那个表。内连接驱动表就无所谓了。
-`iostat`: 主要用于监控系统设备的IO负载情况,iostat首次运行时显示自系统启动开始的各项统计信息,之后运行iostat将显示自上次运行该命令以后的统计信息。用户可以通过指定统计的次数和时间来获得所需的统计信息。
+- **基于块的嵌套循环连接(Block Nested-Loop Join)**: 块嵌套循环连接是嵌套循环连接的优化版本。每次访问被驱动表,被驱动表的记录会被加载到内存中,与驱动表匹配,然后清理内存,然后再取下一条,这样 I/O 成本是超级高的。
-`sar`: sar(System Activity Reporter系统活动情况报告)是目前 Linux 上最为全面的系统性能分析工具之一,可以从多方面对系统的活动进行报告,包括:文件的读写情况、系统调用的使用情况、磁盘I/O、CPU效率、内存使用状况、进程活动及IPC有关的活动等。
+ 所以为了减少了对被驱动表的访问次数,引入了 `join buffer` 的概念,执行连接查询前申请的一块固定大小的内存,先把若干条驱动表结果集中的记录装在这个`join buffer`中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和`join buffer`中的多条驱动表记录做匹配,因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的`I/O`代价。
-`top`:top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。top显示系统当前的进程和其他状况,是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止。比较准确的说,top命令提供了实时的对系统处理器的状态监视。它将显示系统中CPU最“敏感”的任务列表。该命令可以按CPU使用。内存使用和执行时间对任务进行排序;而且该命令的很多特性都可以通过交互式命令或者在个人定制文件中进行设定。
+#### 1.10 子查询优化:
-除了服务器硬件的性能瓶颈,对于MySQL系统本身,我们可以使用工具来优化数据库的性能,通常有三种:使用索引,使用EXPLAIN分析查询以及调整MySQL的内部配置。
+对于子查询,优化器决定是将其物化、转换为半连接、还是其他形式。
+#### 1.11 谓词下推:
+将查询条件(谓词)尽可能地下推到存储引擎层面,以便尽早过滤数据。
-### 2.2 性能下降SQL慢 执行时间长 等待时间长 原因分析
+#### 1.12 分区修剪:
-- 查询语句写的烂
-- 索引失效(单值 复合)
-- 关联查询太多join(设计缺陷或不得已的需求)
-- 服务器调优及各个参数设置(缓冲、线程数等)
+如果表被分区,优化器会识别出只需要扫描的分区。
+#### 1.13 排序和分组优化:
+优化器会考虑使用索引来执行排序和分组操作。
-### 2.3 MySql Query Optimizer
+#### 1.14 临时表和物化:
-1. Mysql中有专门负责优化SELECT语句的优化器模块,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供他认为最优的执行计划(他认为最优的数据检索方式,但不见得是DBA认为是最优的,这部分最耗费时间)
+优化器可能会决定使用临时表来存储中间结果,以简化查询。
-2. 当客户端向MySQL 请求一条Query,命令解析器模块完成请求分类,区别出是 SELECT 并转发给MySQL Query Optimizer时,MySQL Query Optimizer 首先会对整条Query进行优化,处理掉一些常量表达式的预算,直接换算成常量值。并对 Query 中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析 Query 中的 Hint 信息(如果有),看显示Hint信息是否可以完全确定该Query 的执行计划。如果没有 Hint 或Hint 信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据 Query 进行写相应的计算分析,然后再得出最后的执行计划。
+#### 1.15 并行查询执行:
+对于某些查询,优化器可以决定使用并行执行来提高性能。
+#### 1.16 执行计划缓存:
-### 2.4 MySQL常见性能分析手段
+如果可能,优化器会重用之前缓存的执行计划,以减少解析和优化的开销。
-在优化MySQL时,通常需要对数据库进行分析,常见的分析手段有**慢查询日志**,**EXPLAIN 分析查询**,**profiling分析**以及**show命令查询系统状态及系统变量**,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。
+#### 1.17 生成执行语句:
-#### 2.4.1 性能瓶颈定位
+最终,优化器生成用于执行查询的底层指令。
-我们可以通过show命令查看MySQL状态及变量,找到系统的瓶颈:
+#### 1.18 监控和调整:
-```shell
-Mysql> show status ——显示状态信息(扩展show status like ‘XXX’)
+优化器的行为可以通过各种参数进行调整,以适应特定的工作负载和系统配置。
-Mysql> show variables ——显示系统变量(扩展show variables like ‘XXX’)
+#### 1.19 统计信息更新:
-Mysql> show innodb status ——显示InnoDB存储引擎的状态
+优化器依赖于表和索引的统计信息来做出决策,因此需要确保统计信息是最新的。
-Mysql> show processlist ——查看当前SQL执行,包括执行状态、是否锁表等
+InnoDB存储引擎的统计数据收集是数据库性能优化的重要组成部分,因为这些统计数据会被MySQL查询优化器用来生成查询执行计划。以下是InnoDB统计数据收集的一些关键点:
-Shell> mysqladmin variables -u username -p password——显示系统变量
+1. **统计数据存储方式**:
+ - InnoDB提供了两种存储统计数据的方式:永久性统计数据和非永久性统计数据。
+ - 永久性统计数据存储在磁盘上,服务器重启后依然存在。
+ - 非永久性统计数据存储在内存中,服务器关闭时会被清除。
+2. **系统变量控制**:
+ - `innodb_stats_persistent`:控制是否使用永久性统计数据,默认在MySQL 5.6.6之后的版本中为ON。
+ - `innodb_stats_persistent_sample_pages`:控制永久性统计数据采样的页数,默认值为2012。
+ - `innodb_stats_transient_sample_pages`:控制非永久性统计数据采样的页数,默认值为82。
+3. **统计信息的更新**:
+ - `ANALYZE TABLE`:可以用于手动更新统计信息,它将重新计算表的统计数据3。
+ - `innodb_stats_auto_recalc`:控制是否自动重新计算统计数据,默认为ON24。
+4. **统计数据的收集**:
+ - InnoDB通过采样页面来估计表中的行数和其他统计信息24。
+ - `innodb_table_stats`和`innodb_index_stats`:这两个内部表存储了关于表和索引的统计数据24。
+5. **特定表的统计数据属性**:
+ - 在创建或修改表时,可以通过`STATS_PERSISTENT`、`STATS_AUTO_RECALC`和`STATS_SAMPLE_PAGES`属性来控制表的统计数据行为234。
+6. **NULL值的处理**:
+ - `innodb_stats_method`变量决定了在统计索引列不重复值的数量时如何对待NULL值310。
+7. **手动更新统计数据**:
+ - 可以手动更新`innodb_table_stats`和`innodb_index_stats`表中的统计数据,之后需要使用`FLUSH TABLE`命令让优化器重新加载统计信息4。
+8. **非永久性统计数据**:
+ - 当`innodb_stats_persistent`设置为OFF时,新创建的表将使用非永久性统计数据,这些数据存储在内存中45。
+9. **统计数据的自动更新**:
+ - 如果表中数据变动超过一定比例(默认10%),并且`innodb_stats_auto_recalc`为ON,InnoDB将自动更新统计数据34。
-Shell> mysqladmin extended-status -u username -p password——显示状态信息
-```
+通过以上信息,我们了解到 InnoDB 的统计数据收集是一个动态的过程,旨在帮助优化器做出更好的查询执行计划决策。数据库管理员可以根据系统的具体需求和性能指标来调整相关的系统变量,以优化统计数据的收集和使用。
+> 问个问题:为什么 InnoDB `rows`这个统计项的值是估计值呢?
+>
+> `InnoDB`统计一个表中有多少行记录的套路大概是这样的:按照一定算法(并不是纯粹随机的)选取几个叶子节点页面,计算每个页面中主键值记录数量,然后计算平均一个页面中主键值的记录数量乘以全部叶子节点的数量就算是该表的`n_rows`值。
+通过`EXPLAIN`或`EXPLAIN ANALYZE`命令可以查看查询优化器的执行计划,这有助于理解查询的执行方式,并据此进行优化。优化器的目标是找到最快、最高效的执行计划,但有时它也可能做出不理想的决策,特别是在数据量变化或统计信息不准确时。在这种情况下,可以通过调整索引、修改查询或使用SQL提示词来引导优化器做出更好的选择。
-#### 2.4.2 Explain(执行计划)
-- 是什么:使用Explain关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈
-- 能干吗
- - 表的读取顺序
- - 数据读取操作的操作类型
- - 哪些索引可以使用
- - 哪些索引被实际使用
- - 表之间的引用
- - 每张表有多少行被优化器查询
-- 怎么玩
+## 二、业务开发者可以优化什么
- - Explain + SQL语句
- - 执行计划包含的信息
+
-
-- 各字段解释
- - **id**(select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序)
- - id相同,执行顺序从上往下
- - id不同,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
- - id相同不同,同时存在
- - **select_type**(查询的类型,用于区别普通查询、联合查询、子查询等复杂查询)
+假设性能优化就是在一定负载下尽可能的降低响应时间。那我们作为一名业务开发,想优化 MySQL,一般就是优化 CRUD 性能,那优化的前提肯定是比较烂,才优化,要么是服务器或者网络烂,要么是 DB 设计的烂,当然更多的一般是 SQL 写的烂。
- - **SIMPLE** :简单的select查询,查询中不包含子查询或UNION
- - **PRIMARY**:查询中若包含任何复杂的子部分,最外层查询被标记为PRIMARY
- - **SUBQUERY**:在select或where列表中包含了子查询
- - **DERIVED**:在from列表中包含的子查询被标记为DERIVED,mysql会递归执行这些子查询,把结果放在临时表里
- - **UNION**:若第二个select出现在UNION之后,则被标记为UNION,若UNION包含在from子句的子查询中,外层select将被标记为DERIVED
- - **UNION RESULT**:从UNION表获取结果的select
+
- - **table**(显示这一行的数据是关于哪张表的)
+### 2.1 影响 MySQL 的性能因素 | 常见瓶颈
- - **type**(显示查询使用了那种类型,从最好到最差依次排列 **system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL** )
+- ##### 硬件资源
- - system:表只有 一行记录(等于系统表),是const类型的特例,平时不会出现
- - const:表示通过索引一次就找到了,const用于比较primary key或unique索引,因为只要匹配一行数据,所以很快,如将主键置于where列表中,mysql就能将该查询转换为一个常量
- - eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描
- - ref:非唯一性索引扫描,范围匹配某个单独值得所有行。本质上也是一种索引访问,他返回所有匹配某个单独值的行,然而,它可能也会找到多个符合条件的行,多以他应该属于查找和扫描的混合体
- - range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引,一般就是在你的where语句中出现了between、<、>、in等的查询,这种范围扫描索引比全表扫描要好,因为它只需开始于索引的某一点,而结束于另一点,不用扫描全部索引
- - index:Full Index Scan,index于ALL区别为index类型只遍历索引树。通常比ALL快,因为索引文件通常比数据文件小。(**也就是说虽然all和index都是读全表,但index是从索引中读取的,而all是从硬盘中读的**)
- - all:Full Table Scan,将遍历全表找到匹配的行
+ - CPU:CPU 的性能直接影响到 MySQL 的计算速度。多核 CPU 可以提高并发处理能力
- ?> 一般来说,得保证查询至少达到range级别,最好到达ref
+ - 内存:足够的内存可以有效减少磁盘 I/O,提高缓存效率
- - **possible_keys**(显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段若存在索引,则该索引将被列出,但不一定被查询实际使用)
-
- - **key**
+ - 磁盘:磁盘的 I/O 性能对数据库读写速度有显著影响,特别是对于读密集型操作。比如装入数据远大于内存容量的时候,磁盘 I/O 就会达到瓶颈
- - (实际使用的索引,如果为NULL,则没有使用索引)
+- ##### 网络
- - **查询中若使用了覆盖索引,则该索引和查询的select字段重叠,仅出现在key列表中**
+ 网络带宽和延迟也会影响分布式数据库或应用服务器与数据库服务器之间的通信效率
- 
+- ##### DB 设计
- - **key_len**
- - 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好
- - key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的
+ - 存储引擎的选择
+ - 参数配置,如 `innodb_buffer_pool_size`,对数据库性能有决定性影响
- - **ref** (显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值)
- - **rows** (根据表统计信息及索引选用情况,大致估算找到所需的记录所需要读取的行数)
- - **Extra**(包含不适合在其他列中显示但十分重要的额外信息)
+- ##### 连接数和线程管理:
- 1. using filesort: 说明mysql会对数据使用一个外部的索引排序,不是按照表内的索引顺序进行读取。mysql中无法利用索引完成的排序操作称为“文件排序”
+ - 高并发时,连接数和线程的高效管理对性能至关重要
- 2. Using temporary:使用了临时表保存中间结果,mysql在对查询结果排序时使用临时表。常见于排序order by和分组查询group by。
+- ##### 数据库设计
- 3. using index:表示相应的select操作中使用了覆盖索引,避免访问了表的数据行,效率不错,如果同时出现using where,表明索引被用来执行索引键值的查找;否则索引被用来读取数据而非执行查找操作
+ - 合理的表结构设计、索引优化、数据类型选择等都会影响性能
+ - 比如哪种超大文本、二进制媒体数据啥的就别往 MySQL 放了
- 4. using where:使用了where过滤
+- ##### 开发的技术水平
- 5. using join buffer:使用了连接缓存
+ - 开发的水平对性能的影响,查询语句写的烂,比如不管啥上来就 `SELECT *`,一个劲的 `left join`
+ - 索引失效(单值 复合)
- 6. impossible where:where子句的值总是false,不能用来获取任何元祖
+当然锁、长事务这类使用中的坑,会对性能造成影响,我也举不全。
- 7. select tables optimized away:在没有group by子句的情况下,基于索引优化操作或对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化
- 8. distinct:优化distinct操作,在找到第一匹配的元祖后即停止找同样值的动作
-
+### 2.2 性能分析
-- case:
+#### 先查外忧
-
+外忧,就是我们业务开发,一般情况下不用解决,或者一般这锅背不到我们头上的问题,比如硬件、网络这种
-
-
-1. 第一行(执行顺序4):id列为1,表示是union里的第一个select,select_type列的primary表 示该查询为外层查询,table列被标记为,表示查询结果来自一个衍生表,其中derived3中3代表该查询衍生自第三个select查询,即id为3的select。【select d1.name......】
+> 查看 Linux 系统性能的常用命令
+>
+> MySQL 数据库是常见的两个瓶颈是 CPU 和 I/O 的瓶颈。CPU 在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候,磁盘 I/O 瓶颈发生在装入数据远大于内存容量的时候,如果应用分布在网络上,那么查询量相当大的时候那么瓶颈就会出现在网络上。Linux 中我们常用 mpstat、vmstat、iostat、sar 和 top 来查看系统的性能状态。
+>
+> `mpstat`: mpstat 是 Multiprocessor Statistics 的缩写,是实时系统监控工具。其报告为 CPU 的一些统计信息,这些信息存放在 /proc/stat 文件中。在多CPUs系统里,其不但能查看所有CPU的平均状况信息,而且能够查看特定CPU的信息。mpstat最大的特点是可以查看多核心cpu中每个计算核心的统计数据,而类似工具vmstat只能查看系统整体cpu情况。
+>
+> `vmstat`:vmstat 命令是最常见的 Linux/Unix 监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的 CPU 使用率,内存使用,虚拟内存交换情况,IO 读写情况。这个命令是我查看 Linux/Unix 最喜爱的命令,一个是 Linux/Unix 都支持,二是相比top,我可以看到整个机器的CPU、内存、IO的使用情况,而不是单单看到各个进程的CPU使用率和内存使用率(使用场景不一样)。
+>
+> `iostat`: 主要用于监控系统设备的 IO 负载情况,iostat 首次运行时显示自系统启动开始的各项统计信息,之后运行 iostat 将显示自上次运行该命令以后的统计信息。用户可以通过指定统计的次数和时间来获得所需的统计信息。
+>
+> `sar`: sar(System Activity Reporter系统活动情况报告)是目前 Linux 上最为全面的系统性能分析工具之一,可以从多方面对系统的活动进行报告,包括:文件的读写情况、系统调用的使用情况、磁盘 I/O、CPU效率、内存使用状况、进程活动及 IPC 有关的活动等。
+>
+> `top`:top 命令是 Linux 下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于 Windows 的任务管理器。top 显示系统当前的进程和其他状况,是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止。比较准确的说,top 命令提供了实时的对系统处理器的状态监视。它将显示系统中 CPU 最“敏感”的任务列表。该命令可以按 CPU 使用。内存使用和执行时间对任务进行排序;而且该命令的很多特性都可以通过交互式命令或者在个人定制文件中进行设定。
-2. 第二行(执行顺序2):id为3,是整个查询中第三个select的一部分。因查询包含在from中,所以为derived。【select id,name from t1 where other_column=''】
-3. 第三行(执行顺序3):select列表中的子查询select_type为subquery,为整个查询中的第二个select。【select id from t3】
-4. 第四行(执行顺序1):select_type为union,说明第四个select是union里的第二个select,最先执行【select name,id from t2】
-5. 第五行(执行顺序5):代表从union的临时表中读取行的阶段,table列的表示用第一个和第四个select的结果进行union操作。【两个结果union操作】
+除了服务器硬件的性能瓶颈,对于 MySQL 系统本身,我们可以使用工具来优化数据库的性能,通常有三种:使用索引,使用 EXPLAIN 分析查询以及调整 MySQL 的内部配置。
-#### 2.4.3 慢查询日志
+#### 再定内患 | MySQL 常见性能分析手段
-MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
+在优化 MySQL 时,通常需要对数据库进行分析,常见的分析手段有**慢查询日志**,**EXPLAIN 分析查询**,**profiling 分析**以及**show命令查询系统状态及系统变量**,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。
-- long_query_time的默认值为10,意思是运行10秒以上的语句。
-- 默认情况下,MySQL数据库没有开启慢查询日志,需要手动设置参数开启。
-- 如果不是调优需要的话,一般不建议启动该参数。
+##### 2.2.1 性能瓶颈定位
-**查看开启状态**
-
-`SHOW VARIABLES LIKE '%slow_query_log%'`
-
-**开启慢查询日志**
-
-- 临时配置:
+我们可以通过 show 命令查看 MySQL 状态及变量,找到系统的瓶颈:
```mysql
-mysql> set global slow_query_log='ON';
-mysql> set global slow_query_log_file='/var/lib/mysql/hostname-slow.log';
-mysql> set global long_query_time=2;
-```
+Mysql> show status ——显示状态信息(扩展show status like ‘XXX’)
- 也可set文件位置,系统会默认给一个缺省文件host_name-slow.log
+Mysql> show variables ——显示系统变量(扩展show variables like ‘XXX’)
- 使用set操作开启慢查询日志只对当前数据库生效,如果MySQL重启则会失效。
+Mysql> show innodb status ——显示InnoDB存储引擎的状态
-- 永久配置
+Mysql> show processlist ——查看当前SQL执行,包括执行状态、是否锁表等
- 修改配置文件my.cnf或my.ini,在[mysqld]一行下面加入两个配置参数
+Shell> mysqladmin variables -u username -p password——显示系统变量
-```cnf
-[mysqld]
-slow_query_log = ON
-slow_query_log_file = /var/lib/mysql/hostname-slow.log
-long_query_time = 3
+Shell> mysqladmin extended-status -u username -p password——显示状态信息
```
- 注:log-slow-queries参数为慢查询日志存放的位置,一般这个目录要有mysql的运行帐号的可写权限,一般都 将这个目录设置为mysql的数据存放目录;long_query_time=2中的2表示查询超过两秒才记录;在my.cnf或者 my.ini中添加log-queries-not-using-indexes参数,表示记录下没有使用索引的查询。
-
-可以用 `select sleep(4)` 验证是否成功开启。
-在生产环境中,如果手工分析日志,查找、分析SQL,还是比较费劲的,所以MySQL提供了日志分析工具mysqldumpslow。
-通过 mysqldumpslow --help查看操作帮助信息
+##### 2.2.2 Explain(执行计划)
-- 得到返回记录集最多的10个SQL
+- 是什么:使用 Explain 关键字可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的。分析你的查询语句或是表结构的性能瓶颈
+- 能干吗
+ - 表的读取顺序
+ - 数据读取操作的操作类型
+ - 哪些索引可以使用
+ - 哪些索引被实际使用
+ - 表之间的引用
+ - 每张表有多少行被优化器查询
- `mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log`
+- 怎么玩
-- 得到访问次数最多的10个SQL
+ - Explain + SQL语句
+ - 执行计划包含的信息
+
+ 这里我建两个一样的表 t1、t2 用于示例说明
+
+ ```mysql
+ CREATE TABLE t1 (
+ id INT NOT NULL AUTO_INCREMENT,
+ col1 VARCHAR(100),
+ col2 INT,
+ col3 VARCHAR(100),
+ part1 VARCHAR(100),
+ part2 VARCHAR(100),
+ part3 VARCHAR(100),
+ common_field VARCHAR(100),
+ PRIMARY KEY (id),
+ KEY idx_key1 (col1),
+ UNIQUE KEY idx_key2 (col2),
+ KEY idx_key3 (col3),
+ KEY idx_key_part(part1, part2, part3)
+ ) Engine=InnoDB CHARSET=utf8;
+ ```
- `mysqldumpslow -s c -t 10 /var/lib/mysql/hostname-slow.log`
+- 各字段解释
-- 得到按照时间排序的前10条里面含有左连接的查询语句
+ - **id**(select 查询的序列号,包含一组数字,表示查询中执行 select 子句或操作表的顺序)
- `mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/hostname-slow.log`
+ - id 相同,执行顺序从上往下
+ - id 不同,如果是子查询,id 的序号会递增,id 值越大优先级越高,越先被执行
+ - id 相同不同,同时存在,相同的属于一组,从上往下执行
-- 也可以和管道配合使用
+ - **select_type**(查询的类型,用于区别普通查询、联合查询、子查询等复杂查询)
- `mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log | more`
+ - **SIMPLE** :简单的 select 查询,查询中不包含子查询或 UNION
+ - **PRIMARY**:查询中若包含任何复杂的子部分,最外层查询被标记为 PRIMARY
+ - **SUBQUERY**:在 select 或 where 列表中包含了子查询
+ - **DERIVED**:在 from 列表中包含的子查询被标记为 DERIVED,mysql 会递归执行这些子查询,把结果放在临时表里
+ - **UNION**:若第二个 select 出现在 UNION 之后,则被标记为 UNION,若 UNION 包含在 from 子句的子查询中,外层 select 将被标记为 DERIVED
+ - **UNION RESULT**:从 UNION 表获取结果的 select
-**也可使用 pt-query-digest 分析 RDS MySQL 慢查询日志**
+ - **table**(显示这一行的数据是关于哪张表的)
+ - **partitions**(匹配的分区信息,高版本才有的)
+ - **type**(显示查询使用了那种类型,从最好到最差依次排列 **system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL** )
-#### 2.4.4 Show Profile分析查询
+ - system:表只有一行记录(等于系统表),是 const 类型的特例,平时不会出现
+ - const:表示通过索引一次就找到了,const 用于比较 primary key 或 unique 索引,因为只要匹配一行数据,所以很快,如将主键置于 where 列表中,mysql 就能将该查询转换为一个常量
+ - eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描
+ - ref:非唯一性索引扫描,范围匹配某个单独值得所有行。本质上也是一种索引访问,他返回所有匹配某个单独值的行,然而,它可能也会找到多个符合条件的行,多以他应该属于查找和扫描的混合体
+ - ref_or_null:当对普通二级索引进行等值匹配查询,该索引列的值也可以是`NULL`值时,那么对该表的访问方法就可能是ref_or_null
+ - index_merge: 在某些场景下可以使用`Intersection`、`Union`、`Sort-Union`这三种索引合并的方式来执行查询
+ - range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引,一般就是在你的 where 语句中出现了between、<、>、in等的查询,这种范围扫描索引比全表扫描要好,因为它只需开始于索引的某一点,而结束于另一点,不用扫描全部索引
+ - index:Full Index Scan,index 于 ALL 区别为 index 类型只遍历索引树。通常比 ALL 快,因为索引文件通常比数据文件小。(**也就是说虽然 all 和 index 都是读全表,但 index 是从索引中读取的,而 all 是从硬盘中读的**)
+ - all:Full Table Scan,将遍历全表找到匹配的行
-通过慢日志查询可以知道哪些SQL语句执行效率低下,通过explain我们可以得知SQL语句的具体执行情况,索引使用等,还可以结合Show Profile命令查看执行状态。
+ > 一般来说,得保证查询至少达到 range 级别,最好到达 ref
-- Show Profile是mysql提供可以用来分析当前会话中语句执行的资源消耗情况。可以用于SQL的调优的测量
+ - **possible_keys**(显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段若存在索引,则该索引将被列出,但不一定被查询实际使用)
+
+ - **key**
-- 默认情况下,参数处于关闭状态,并保存最近15次的运行结果
+ - 实际使用的索引,如果为NULL,则没有使用索引
-- 分析步骤
+ - **查询中若指定了使用了覆盖索引,则该索引和查询的 select 字段重叠,仅出现在 key 列表中**
- 1. 是否支持,看看当前的mysql版本是否支持
+ - **key_len**
+
+ - 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精确性的情况下,长度越短越好
+ - key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的
+
+ - **ref** (显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值)
- ```mysql
- mysql>Show variables like 'profiling'; --默认是关闭,使用前需要开启
- ```
+ - **rows** (根据表统计信息及索引选用情况,大致估算找到所需的记录所需要读取的行数)
- 2. 开启功能,默认是关闭,使用前需要开启
+ - **filtered**(某个表经过搜索条件过滤后剩余记录条数的百分比)
- ```mysql
- mysql>set profiling=1;
- ```
+ - **Extra**(包含不适合在其他列中显示但十分重要的额外信息)
- 3. 运行SQL
+ 额外信息有好几十个,我们看几个常见的
- 4. 查看结果,show profiles;
+ 1. `using filesort`:说明 MySQL 会对数据使用一个外部的索引排序,不是按照表内的索引顺序进行读取。MySQL 中无法利用索引完成的排序操作称为“文件排序”
- 5. 诊断SQL,show profile cpu,block io for query 上一步前面的问题SQL数字号码;
+ 2. `Using temporary`:使用了临时表保存中间结果,比如去重、排序之类的,比如我们在执行许多包含`DISTINCT`、`GROUP BY`、`UNION`等子句的查询过程中,如果不能有效利用索引来完成查询,`MySQL`很有可能寻求通过建立内部的临时表来执行查询。
- 6. 日常开发需要注意的结论
+ 3. `using index`:表示相应的 select 操作中使用了覆盖索引,避免访问了表的数据行,效率不错,如果同时出现 `using where`,表明索引被用来执行索引键值的查找;否则索引被用来读取数据而非执行查找操作
- - converting HEAP to MyISAM 查询结果太大,内存都不够用了往磁盘上搬了。
+ 4. `using where`:当某个搜索条件需要在`server层`进行判断时
- - create tmp table 创建临时表,这个要注意
+ 5. `using join buffer`:使用了连接缓存
- - Copying to tmp table on disk 把内存临时表复制到磁盘
+ 6. `impossible where`:where 子句的值总是 false,不能用来获取任何元祖
- - locked
+ 7. `Using index condition` : 查询使用了索引,但是查询条件不能完全由索引本身来满足
+ `Using index condition `通常出现在以下几种情况:
+
+ - **索引条件下推(Index Condition Pushdown, ICP)**:这是 MySQL 的一个优化策略,它将查询条件的过滤逻辑“下推”到存储引擎层,而不是在服务器层处理。这样可以减少从存储引擎检索的数据量,从而提高查询效率。
+ - **部分索引**:当查询条件只涉及索引的一部分列时,MySQL 可以使用索引来快速定位到满足条件的行,但是可能需要回表(即访问表的实际数据行)来检查剩余的条件。
+ - **复合索引**:在使用复合索引(即索引包含多个列)的情况下,如果查询条件只匹配索引的前几列,那么剩余的列可能需要通过 `Using index condition` 来进一步过滤。
+
+ 8. `select tables optimized away`:在没有group by子句的情况下,基于索引优化操作或对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化
+
+ 9. `distinct`:优化distinct操作,在找到第一匹配的元祖后即停止找同样值的动作
+
+ > **Json格式的执行计划**
+ >
+ > MySQL 5.7及以上版本支持使用`EXPLAIN FORMAT=JSON`命令,该命令返回查询的执行计划,以 JSON 格式展示。
+ >
+ > `EXPLAIN` 的 JSON 输出包含了多个层次和字段,以下是一些主要的字段:
+ >
+ > - **`query_block`**: 表示查询块,对应于`EXPLAIN`表格中的一行。对于简单查询,只有一个查询块;对于包含子查询或UNION的复杂查询,会有多个查询块。
+ > - **`select_id`**: 查询块的唯一标识符。
+ > - **`select_type`**: 查询类型(如`SIMPLE`、`PRIMARY`、`UNION`等)。
+ > - **`table`**: 正在访问的表名。
+ > - **`partitions`**: 表分区信息。
+ > - **`join`**: 如果是连接查询,这里会提供连接的详细信息。
+ > - **`condition`**: 应用的条件。
+ > - **`used_columns`**: 实际使用到的列。
+ > - **`attached_condition`**: 附加条件,如`WHERE`子句或`ON`子句的条件。
+ > - **`output`**: 表示查询块的输出,通常是一个子查询或派生表。
+ > - **`cost_model`**: 包含成本模型相关的信息,如:
+ > - **`rows_estimated`**: 估计需要读取的行数。
+ > - **`rows_examined`**: 实际检查的行数。
+ > - **`cost`**: 查询的成本。
+ > - **`execution_info`**: 包含执行信息,如:
+ > - **`execution_mode`**: 执行模式(如`PACKET_BASED`或`ROW_BASED`)。
+ >
+ > ```mysql
+ > mysql> EXPLAIN FORMAT=JSON SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2 WHERE t1.common_field = 'a'\G
+ > *************************** 1. row ***************************
+ > EXPLAIN: {
+ > "query_block": {
+ > "select_id": 1,
+ > "cost_info": {
+ > "query_cost": "0.70"
+ > },
+ > "nested_loop": [
+ > {
+ > "table": {
+ > "table_name": "t1", # t1表是驱动表
+ > "access_type": "ALL", # 访问方法为ALL,意味着使用全表扫描访问
+ > "possible_keys": [ # 可能使用的索引
+ > "idx_key1"
+ > ],
+ > "rows_examined_per_scan": 1,
+ > "rows_produced_per_join": 1,
+ > "filtered": "100.00",
+ > "cost_info": {
+ > "read_cost": "0.25",
+ > "eval_cost": "0.10",
+ > "prefix_cost": "0.35",
+ > "data_read_per_join": "1K"
+ > },
+ > "used_columns": [
+ > "id",
+ > "col1",
+ > "col2",
+ > "col3",
+ > "part1",
+ > "part2",
+ > "part3",
+ > "common_field"
+ > ],
+ > "attached_condition": "((`fish`.`t1`.`common_field` = 'a') and (`fish`.`t1`.`col1` is not null))"
+ > }
+ > },
+ > {
+ > "table": {
+ > "table_name": "t2",
+ > "access_type": "eq_ref",
+ > "possible_keys": [
+ > "idx_key2"
+ > ],
+ > "key": "idx_key2",
+ > "used_key_parts": [
+ > "col2"
+ > ],
+ > "key_length": "5",
+ > "ref": [
+ > "fish.t1.col1"
+ > ],
+ > "rows_examined_per_scan": 1,
+ > "rows_produced_per_join": 1,
+ > "filtered": "100.00",
+ > "index_condition": "(cast(`fish`.`t1`.`col1` as double) = cast(`fish`.`t2`.`col2` as double))",
+ > "cost_info": {
+ > "read_cost": "0.25",
+ > "eval_cost": "0.10",
+ > "prefix_cost": "0.70",
+ > "data_read_per_join": "1K"
+ > },
+ > "used_columns": [
+ > "id",
+ > "col1",
+ > "col2",
+ > "col3",
+ > "part1",
+ > "part2",
+ > "part3",
+ > "common_field"
+ > ]
+ > }
+ > }
+ > ]
+ > }
+ > }
+ > ```
+
+##### 2.2.3 OPTIMIZER TRACE
+
+MySQL 的 `OPTIMIZER TRACE` 特性提供了一种深入理解查询优化器如何决定执行计划的方法。通过这个特性,你可以获取关于查询优化过程的详细信息,包括优化器所做的选择、考虑的替代方案、成本估算等
+
+MySQL 5.6.3 版本开始,`OPTIMIZER TRACE` 作为一个系统变量引入。要使用它,你需要设置 `optimizer_trace` 变量为 `ON`,并确保你有 `TRACE` 权限。
+
+启用 `OPTIMIZER TRACE`:
+```mysql
+SET optimizer_trace="enabled=on";
+```
-## 3. 性能优化
+执行你的查询后,获取优化器跟踪结果:
-### 3.1 索引优化
+```mysql
+SELECT * FROM information_schema.OPTIMIZER_TRACE;
+```
-1. 全值匹配我最爱
-2. 最佳左前缀法则
-3. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
-4. 存储引擎不能使用索引中范围条件右边的列
-5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select
-6. mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描
-7. is null ,is not null 也无法使用索引
-8. like以通配符开头('%abc...')mysql索引失效会变成全表扫描的操作
-9. 字符串不加单引号索引失效
-10. 少用or,用它来连接时会索引失效
+`OPTIMIZER_TRACE `表包含了多个列,提供了优化器决策过程的详细信息。以下是一些关键列:
+- **`query`**: 执行的SQL查询。
+- **`trace`**: 优化过程的详细跟踪信息,通常以JSON格式展示。
+- **`missed_uses`**: 优化器未能使用的潜在优化。
+- **`step`**: 优化过程中的步骤编号。
+- **`level`**: 跟踪信息的层次级别。
+- **`OK`**: 指示步骤是否成功完成。
+- **`reason`**: 如果步骤未成功,原因说明。
-**一般性建议**
-- 对于单键索引,尽量选择针对当前query过滤性更好的索引
+##### 2.2.4 慢查询日志
-- 在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
+MySQL 的慢查询日志是 MySQL 提供的一种日志记录,它用来记录在 MySQL 中响应时间超过阈值的语句,具体指运行时间超过`long_query_time` 值的SQL,则会被记录到慢查询日志中。
-- 在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引
+- `long_query_time` 的默认值为10,意思是运行10 秒以上的语句。
+- 默认情况下,MySQL 数据库没有开启慢查询日志,需要手动设置参数开启。
+- 如果不是调优需要的话,一般不建议启动该参数。
-- 尽可能通过分析统计信息和调整query的写法来达到选择合适索引的目的
+**开启慢查询日志**
-- 少用Hint强制索引
+临时配置:
-
+```mysql
+mysql> set global slow_query_log='ON';
+mysql> set global slow_query_log_file='/var/lib/mysql/hostname-slow.log';
+mysql> set global long_query_time=2;
+```
-### 3.2 查询优化
+可以用 `select sleep(4)` 验证是否成功开启。
-- **永远小标驱动大表(小的数据集驱动大的数据集)**
+在生产环境中,如果手工分析日志,查找、分析SQL,还是比较费劲的,所以 MySQL 提供了日志分析工具 `mysqldumpslow`。
+
+> 通过 mysqldumpslow --help 查看操作帮助信息
+>
+> - 得到返回记录集最多的10个SQL
+>
+> `mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log`
+>
+> - 得到访问次数最多的10个SQL
+>
+> `mysqldumpslow -s c -t 10 /var/lib/mysql/hostname-slow.log`
+>
+> - 得到按照时间排序的前10条里面含有左连接的查询语句
+>
+> `mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/hostname-slow.log`
+>
+> - 也可以和管道配合使用
+>
+> `mysqldumpslow -s r -t 10 /var/lib/mysql/hostname-slow.log | more`
- `slect * from A where id in (select id from B)`
+**也可使用 pt-query-digest 分析 RDS MySQL 慢查询日志**
- 等价于
- `select id from B`
- `select * from A where A.id=B.id`
+##### 2.2.5 Show Profile 分析查询
- 当B表的数据集必须小于A表的数据集时,用in优于exists
+通过慢日志查询可以知道哪些 SQL 语句执行效率低下,通过 explain 我们可以得知 SQL 语句的具体执行情况,索引使用等,还可以结合`Show Profile` 命令查看执行状态。
- `select * from A where exists (select 1 from B where B.id=A.id)`
+- `Show Profile`是 MySQL 提供可以用来分析当前会话中语句执行的资源消耗情况。可以用于 SQL 的调优的测量
+- 默认情况下,参数处于关闭状态,并保存最近 15 次的运行结果
+- 分析步骤
- 等价于
+1. 启用profiling并执行查询:
- `select *from A`
+ ```mysql
+ SET profiling = 1;
+ SELECT * FROM t1 WHERE col1 = 'a';
+ ```
- `select * from B where B.id = A.id`
+2. 查看所有PROFILES:
- 当A表的数据集小于B表的数据集时,用exists优于用in
+ ```mysql
+ SHOW PROFILES;
+ ```
- 注意:A表与B表的ID字段应建立索引。
+3. 查看特定查询的 PROFILE, 进行诊断:
-
+ ```mysql
+ SHOW PROFILE CPU, BLOCK IO FOR QUERY query_id;
+ ```
-- order by关键字优化
+
-- - order by子句,尽量使用Index方式排序,避免使用FileSort方式排序
-- - - mysql支持两种方式的排序,FileSort和Index,Index效率高,它指MySQL扫描索引本身完成排序,FileSort效率较低;
- - ORDER BY 满足两种情况,会使用Index方式排序;①ORDER BY语句使用索引最左前列 ②使用where子句与ORDER BY子句条件列组合满足索引最左前列
-
+#### 2.3 性能优化
-- - 尽可能在索引列上完成排序操作,遵照索引建的最佳最前缀
- - 如果不在索引列上,filesort有两种算法,mysql就要启动双路排序和单路排序
+##### 2.3.1 索引优化
-- - - 双路排序
- - 单路排序
- - 由于单路是后出的,总体而言好过双路
+1. **选择合适的索引类型**:根据查询需求,选择适合的索引类型,如普通索引、唯一索引、全文索引等。
+2. **合理设计索引**:创建索引时,应考虑列的选择性,即不同值的比例,选择性高的列更应优先索引。
+3. **使用覆盖索引**:创建覆盖索引,使得查询可以直接从索引中获取所需的数据,而无需回表查询。
+4. **遵循最左前缀匹配原则**:在使用复合索引时,查询条件应该包含索引最左边的列,以确保索引的有效使用。
+5. **避免冗余和重复索引**:定期检查并删除重复或冗余的索引,以减少维护成本和提高性能。
+6. **使用索引条件下推(ICP)**:在MySQL 5.6及以上版本,利用索引条件下推减少服务器层的数据处理。
+7. **索引维护**:定期对索引进行维护,如重建索引,以保持索引的性能和效率。
+8. **使用EXPLAIN分析查询**:通过EXPLAIN命令分析查询的执行计划,确保索引被正确使用。
+9. **避免在索引列上进行运算或使用函数**:这会导致索引失效,从而进行全表扫描。
+10. **负向条件索引**:负向条件查询(如不等于、not in等)不会使用索引,建议用in查询优化。
+11. **对文本建立前缀索引**:对于长文本字段,考虑建立前缀索引以减少索引大小并提高效率。
+12. **建立索引的列不为NULL**:NULL值会影响索引的有效性,尽量确保索引列不包含NULL值。
+13. **明确知道只会返回一条记录时,使用limit 1**:这可以提高查询效率。
+14. **范围查询字段放最后**:在复合索引中,将范围查询的字段放在最后,以提高查询效率。
+15. **尽量全值匹配**:尽量使用索引列的全值匹配,以提高索引的使用效率。
+16. **Like查询时,左侧不要加%**:这会导致索引失效,应尽量避免。
+17. **注意null/not null对索引的影响**:null值可能会影响索引的使用,需要特别注意。
+18. **字符类型务必加上引号**:确保字符类型的值在使用时加上引号,以利用索引。
+19. **OR关键字左右尽量都为索引列**:确保OR条件的两侧字段都是索引列,以提高查询效率。
-- - 优化策略
+这些策略可以帮助提高 MySQL 数据库的性能,但应根据具体的数据分布和查询模式来设计索引。索引不是越多越好,不恰当的索引可能会降低数据库的插入、更新和删除性能
-- - - 增大sort_buffer_size参数的设置
- - 增大max_lencth_for_sort_data参数的设置
- 
+##### 2.3.2 查询优化
-- GROUP BY关键字优化
- - group by实质是先排序后进行分组,遵照索引建的最佳左前缀
- - 当无法使用索引列,增大max_length_for_sort_data参数的设置+增大sort_buffer_size参数的设置
- - where高于having,能写在where限定的条件就不要去having限定了。
+1. **减少返回的列**:避免使用`SELECT *`,只选择需要的列。
+2. **减少返回的行**:使用WHERE子句精确过滤数据、使用LIMIT子句限制返回结果的数量。
+3. **优化JOIN操作**:
+ - 确保JOIN操作中的表已经正确索引。
+ - 使用小表驱动大表的原则。
+ - 考虑表的连接顺序,减少中间结果集的大小。
+4. **优化子查询**:将子查询转换为连接(JOIN)操作,以减少数据库的嵌套查询开销。
+5. **使用临时表或派生表**:对于复杂的子查询,可以考虑使用临时表或派生表来简化查询。
+6. **使用聚合函数和GROUP BY优化**:
+ - 在适当的情况下使用聚合函数减少数据量。
+ - 确保GROUP BY子句中的列上有索引。
+7. **避免在WHERE子句中使用函数**:这可能导致索引失效,从而进行全表扫描。
+8. **优化ORDER BY子句**:如果可能,使用索引排序来优化排序操作。
+9. **使用UNION ALL代替UNION**:如果不需要去重,使用UNION ALL可以提高性能。
+10. **优化数据表结构**:避免冗余字段,使用合适的数据类型。
+11. **使用分区表**:对于非常大的表,使用分区可以提高查询效率。
+12. **使用缓存**:对于重复查询的数据,使用缓存来减少数据库的访问。
+13. **避免使用SELECT DISTINCT**:DISTINCT操作会降低查询性能,仅在必要时使用。
+14. **优化LIKE语句**:避免在LIKE语句中使用前导通配符(如'%keyword')。
+15. **使用合适的事务隔离级别**:降低事务隔离级别可以减少锁的竞争,但要注意数据一致性。
+16. **监控和优化慢查询**:开启慢查询日志,定期分析慢查询,找出性能瓶颈。
+17. **批量操作**:对于插入或更新操作,尽量使用批量操作来减少事务开销。
+18. **避免在索引列上使用OR条件**:OR条件可能导致查询优化器放弃使用索引。
+19. **使用物化视图**:对于复杂的查询,可以考虑使用物化视图来预先计算并存储结果。
-### 3.3 数据类型优化
+##### 2.3.3 数据类型优化
-MySQL支持的数据类型非常多,选择正确的数据类型对于获取高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于做出更好的选择。
+MySQL 支持的数据类型非常多,选择正确的数据类型对于获取高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于做出更好的选择。
- 更小的通常更好:一般情况下,应该尽量使用可以正确存储数据的最小数据类型。
@@ -424,7 +640,50 @@ MySQL支持的数据类型非常多,选择正确的数据类型对于获取高
- 尽量避免NULL:通常情况下最好指定列为NOT NULL
+
+
+> 规范:
+>
+> - 必须把字段定义为NOT NULL并且提供默认值
+> - null的列使索引/索引统计/值比较都更加复杂,对MySQL来说更难优化
+> - null 这种类型MySQL内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条件下,表中有较多空字段的时候,数据库的处理性能会降低很多
+> - null值需要更多的存储空,无论是表还是索引中每行中的null的列都需要额外的空间来标识
+> - 对null 的处理时候,只能采用is null或is not null,而不能采用=、in、<、<>、!=、not in这些操作符号。如:where name!=’shenjian’,如果存在name为null值的记录,查询结果就不会包含name为null值的记录
+>
+> - **禁止使用TEXT、BLOB类型**:会浪费更多的磁盘和内存空间,非必要的大量的大字段查询会淘汰掉热数据,导致内存命中率急剧降低,影响数据库性能
+>
+> - **禁止使用小数存储货币**
+> - 必须使用varchar(20)存储手机号
+>
+> - **禁止使用ENUM,可使用TINYINT代替**
+>
+> - 禁止在更新十分频繁、区分度不高的属性上建立索引
+>
+> - 禁止使用INSERT INTO t_xxx VALUES(xxx),必须显示指定插入的列属性
+>
+> - 禁止使用属性隐式转换:SELECT uid FROM t_user WHERE phone=13812345678 会导致全表扫描
+>
+> - 禁止在WHERE条件的属性上使用函数或者表达式
+>
+> - `SELECT uid FROM t_user WHERE from_unixtime(day)>='2017-02-15' `会导致全表扫描
+>
+> - 正确的写法是:`SELECT uid FROM t_user WHERE day>= unix_timestamp('2017-02-15 00:00:00')`
+>
+> - 建立组合索引,必须把区分度高的字段放在前面
+>
+> - 禁止负向查询,以及%开头的模糊查询
+>
+> - 负向查询条件:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等,会导致全表扫描
+> - %开头的模糊查询,会导致全表扫描
+>
+> - 禁止大表使用JOIN查询,禁止大表使用子查询:会产生临时表,消耗较多内存与CPU,极大影响数据库性能
+>
+
+### References
+
+- [58到家数据库30条军规解读](https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959906&idx=1&sn=2cbdc66cfb5b53cf4327a1e0d18d9b4a&chksm=bd2d07be8a5a8ea86dc3c04eced3f411ee5ec207f73d317245e1fefea1628feb037ad71531bc&scene=21#wechat_redirect)
+
+
-> https://www.jianshu.com/p/3c79039e82aa
diff --git a/docs/data-management/MySQL/MySQL-Optimize.md b/docs/data-management/MySQL/MySQL-Optimize.md
deleted file mode 100755
index f7766dfff5..0000000000
--- a/docs/data-management/MySQL/MySQL-Optimize.md
+++ /dev/null
@@ -1,38 +0,0 @@
----
-title: MySQL 优化
-date: 2022-02-09
-tags:
- - MySQL 优化
-categories: MySQL
----
-
-
-
-MySQL 优化包括查询优化、索引优化、库表结构优化 等等
-
-操作系统和硬件
-
-## 查询优化
-
-### 为什么查询速度会慢
-
-
-
-## 索引优化
-
-
-
-
-
-## Schema 与 数据类型优化
-
-
-
-## 操作系统和硬件优化
-
-
-
-
-
-
-
diff --git a/docs/data-management/MySQL/MySQL-Schema.md b/docs/data-management/MySQL/MySQL-Schema.md
deleted file mode 100644
index 411e779fcb..0000000000
--- a/docs/data-management/MySQL/MySQL-Schema.md
+++ /dev/null
@@ -1,237 +0,0 @@
-- 尽量避免过度设计,例如会导致极其复杂查询的schema设计,或者有很多列的表设计
-- 使用小而简单的合适数据类型,除非真实数据模型中有确切的需要,否则应该尽可能的避免使用NULL值。
-- 尽量使用相同的数据类型存储相似或相关的值,尤其是要在关联条件中使用的列。
-- 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存。
-- 尽量使用整型定义标识列。
-- 避免使用MySQL已经遗弃的特性,例如指定浮点数的精度,或者整数的显示宽度。
-- 小心使用ENUM和SET。
-- 最好避免使用BIT。
-
-
-
-
-
-# 58到家数据库30条军规解读
-
-**军规适用场景**:并发量大、数据量大的互联网业务
-
-**军规**:介绍内容
-
-**解读**:讲解原因,解读比军规更重要
-
-
-
-## 一、基础规范
-
-**(1)必须使用InnoDB存储引擎**
-
-解读:支持事务、行级锁、并发性能更好、CPU及内存缓存页优化使得资源利用率更高
-
-
-
-**(2)必须使用UTF8字符集**
-
-解读:万国码,无需转码,无乱码风险,节省空间
-
-
-
-**(3)数据表、数据字段必须加入中文注释**
-
-解读:N年后谁tm知道这个r1,r2,r3字段是干嘛的
-
-
-
-**(4)禁止使用存储过程、视图、触发器、Event**
-
-解读:高并发大数据的互联网业务,架构设计思路是“解放数据库CPU,将计算转移到服务层”,并发量大的情况下,这些功能很可能将数据库拖死,业务逻辑放到服务层具备更好的扩展性,能够轻易实现“增机器就加性能”。数据库擅长存储与索引,CPU计算还是上移吧
-
-
-
-**(5)禁止存储大文件或者大照片**
-
-解读:为何要让数据库做它不擅长的事情?大文件和照片存储在文件系统,数据库里存URI多好
-
-
-
-## 二、命名规范
-
-**(6)只允许使用内网域名,而不是ip连接数据库**
-
-
-
-**(7)线上环境、开发环境、测试环境数据库内网域名遵循命名规范**
-
-业务名称:xxx
-
-线上环境:dj.xxx.db
-
-开发环境:dj.xxx.rdb
-
-测试环境:dj.xxx.tdb
-
-**从库**在名称后加-s标识,**备库**在名称后加-ss标识
-
-线上从库:dj.xxx-s.db
-
-线上备库:dj.xxx-sss.db
-
-
-**(8)库名、表名、字段名:小写,下划线风格,不超过32个字符,必须见名知意,禁止拼音英文混用
-
-(9)表名t_xxx,非唯一索引名idx_xxx,唯一索引名uniq_xxx**
-
-**(10)单实例表数目必须小于500**
-
-
-**(11)单表列数目必须小于30**
-
-解读:
-
-a)主键递增,数据行写入可以提高插入性能,可以避免page分裂,减少表碎片提升空间和内存的使用
-
-b)主键要选择较短的数据类型, Innodb引擎普通索引都会保存主键的值,较短的数据类型可以有效的减少索引的磁盘空间,提高索引的缓存效率
-
-c) 无主键的表删除,在row模式的主从架构,会导致备库夯住
-
-
-
-**(13)禁止使用外键,如果有外键完整性约束,需要应用程序控制**
-
-解读:外键会导致表与表之间耦合,update与delete操作都会涉及相关联的表,十分影响sql 的性能,甚至会造成死锁。高并发情况下容易造成数据库性能,大数据高并发业务场景数据库使用以性能优先
-
-
-
-## 四、字段设计规范
-
-**(14)必须把字段定义为NOT NULL并且提供默认值**
-
-解读:
-
-a)null的列使索引/索引统计/值比较都更加复杂,对MySQL来说更难优化
-
-b)null 这种类型MySQL内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条件下,表中有较多空字段的时候,数据库的处理性能会降低很多
-
-c)null值需要更多的存储空,无论是表还是索引中每行中的null的列都需要额外的空间来标识
-
-d)对null 的处理时候,只能采用is null或is not null,而不能采用=、in、<、<>、!=、not in这些操作符号。如:where name!=’shenjian’,如果存在name为null值的记录,查询结果就不会包含name为null值的记录
-
-
-
-**(15)禁止使用TEXT、BLOB类型**
-
-解读:会浪费更多的磁盘和内存空间,非必要的大量的大字段查询会淘汰掉热数据,导致内存命中率急剧降低,影响数据库性能
-
-
-
-**(16)禁止使用小数存储货币**
-
-解读:使用整数吧,小数容易导致钱对不上
-
-
-
-**(17)必须使用varchar(20)存储手机号**
-
-解读:
-
-a)涉及到区号或者国家代号,可能出现+-()
-
-b)手机号会去做数学运算么?
-
-c)varchar可以支持模糊查询,例如:like“138%”
-
-
-
-**(18)禁止使用ENUM,可使用TINYINT代替**
-
-解读:
-
-a)增加新的ENUM值要做DDL操作
-
-b)ENUM的内部实际存储就是整数,你以为自己定义的是字符串?
-
-
-
-## 五、索引设计规范
-
-**(19)单表索引建议控制在5个以内**
-
-
-
-**(20)单索引字段数不允许超过5个**
-
-解读:字段超过5个时,实际已经起不到有效过滤数据的作用了
-
-
-
-**(21)禁止在更新十分频繁、区分度不高的属性上建立索引**
-
-解读:
-
-a)更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能
-
-b)“性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似
-
-
-
-**(22)建立组合索引,必须把区分度高的字段放在前面**
-
-解读:能够更加有效的过滤数据
-
-
-
-## 六、SQL使用规范
-
-**(23)禁止使用SELECT \*,只获取必要的字段,需要显示说明列属性**
-
-解读:
-
-a)读取不需要的列会增加CPU、IO、NET消耗
-
-b)不能有效的利用覆盖索引
-
-c)使用SELECT *容易在增加或者删除字段后出现程序BUG
-
-
-
-**(24)禁止使用INSERT INTO t_xxx VALUES(xxx),必须显示指定插入的列属性**
-
-解读:容易在增加或者删除字段后出现程序BUG
-
-
-
-**(25)禁止使用属性隐式转换**
-
-解读:SELECT uid FROM t_user WHERE phone=13812345678 会导致全表扫描,而不能命中phone索引,猜猜为什么?(这个线上问题不止出现过一次)
-
-
-
-**(26)禁止在WHERE条件的属性上使用函数或者表达式**
-
-解读:SELECT uid FROM t_user WHERE from_unixtime(day)>='2017-02-15' 会导致全表扫描
-
-正确的写法是:SELECT uid FROM t_user WHERE day>= unix_timestamp('2017-02-15 00:00:00')
-
-
-
-**(27)禁止负向查询,以及%开头的模糊查询**
-
-解读:
-
-a)负向查询条件:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等,会导致全表扫描
-
-b)%开头的模糊查询,会导致全表扫描
-
-
-
-**(28)禁止大表使用JOIN查询,禁止大表使用子查询**
-
-解读:会产生临时表,消耗较多内存与CPU,极大影响数据库性能
-
-
-
-**(29)禁止使用OR条件,必须改为IN查询**
-
-解读:旧版本Mysql的OR查询是不能命中索引的,即使能命中索引,为何要让数据库耗费更多的CPU帮助实施查询优化呢?
-
-
-**(30)应用程序必须捕获SQL异常,并有相应处理**
\ No newline at end of file
diff --git a/docs/data-management/MySQL/MySQL-Segmentation.md b/docs/data-management/MySQL/MySQL-Segmentation.md
index 190a52d5f8..c406866d38 100644
--- a/docs/data-management/MySQL/MySQL-Segmentation.md
+++ b/docs/data-management/MySQL/MySQL-Segmentation.md
@@ -36,48 +36,77 @@ subtopic
-### 分区类型及操作
-
-#### RANGE分区
-
-mysql将会根据指定的拆分策略,,把数据放在不同的表文件上。相当于在文件上,被拆成了小块.但是,对外给客户的感觉还是一张表,透明的。
-
- 按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
-
- range 来分,好处在于说,扩容的时候很简单
-
-#### List分区
-
-MySQL中的LIST分区在很多方面类似于RANGE分区。和按照RANGE分区一样,每个分区必须明确定义。它们的主要区别在于,LIST分区中每个分区的定义和选择是基于某列的值从属于一个值列表集中的一个值,而RANGE分区是从属于一个连续区间值的集合。
-
-#### 其它
-
-- Hash分区: hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表
-
-- Key分区
-
-- 子分区
-
-#### 对NULL值的处理
-
-
-MySQL中的分区在禁止空值NULL上没有进行处理,无论它是一个列值还是一个用户定义表达式的值,一般而言,在这种情况下MySQL把NULL当做零。如果你不希望出现类似情况,建议在设计表时声明该列“NOT NULL”
-
+- **RANGE分区**:基于属于一个给定连续区间的列值,把多行分配给分区。mysql将会根据指定的拆分策略,把数据放在不同的表文件上。相当于在文件上,被拆成了小块.但是,对外给客户的感觉还是一张表,透明的。
+
+ 按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,比如交易表啊,销售表啊等,可以根据年月来存放数据。可能会产生热点问题,大量的流量都打在最新的数据上了。
+
+ range 来分,好处在于说,扩容的时候很简单。
+
+ ```mysql
+ CREATE TABLE sales (
+ id INT,
+ amount DECIMAL(10,2),
+ sale_date DATE
+ )
+ PARTITION BY RANGE (YEAR(sale_date)) (
+ PARTITION p0 VALUES LESS THAN (2000),
+ PARTITION p1 VALUES LESS THAN (2005),
+ PARTITION p2 VALUES LESS THAN (2010),
+ PARTITION p3 VALUES LESS THAN MAXVALUE
+ );
+ ```
+
+- **LIST分区**:按列表划分,类似于RANGE分区,但使用的是明确的值列表。
+
+ 它们的主要区别在于,LIST分区中每个分区的定义和选择是基于某列的值从属于一个值列表集中的一个值,而RANGE分区是从属于一个连续区间值的集合。
+
+ ```mysql
+ CREATE TABLE customers (
+ id INT,
+ name VARCHAR(50),
+ country VARCHAR(50)
+ )
+ PARTITION BY LIST (country) (
+ PARTITION p0 VALUES IN ('USA', 'Canada'),
+ PARTITION p1 VALUES IN ('UK', 'France'),
+ PARTITION p2 VALUES IN ('Germany', 'Italy')
+ );
+ ```
+
+- **HASH分区**:按哈希算法划分,将数据根据某个列的哈希值均匀分布到不同的分区中。
+
+ hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表
+
+ ```mysql
+ CREATE TABLE orders (
+ id INT,
+ order_date DATE,
+ customer_id INT
+ )
+ PARTITION BY HASH(id) PARTITIONS 4;
+ ```
+
+- **KEY分区**:类似于HASH分区,但使用MySQL内部的哈希函数。
+
+ ```mysql
+ CREATE TABLE logs (
+ id INT,
+ log_date DATE
+ )
+ PARTITION BY KEY(id) PARTITIONS 4;
+ ```
+
**看上去分区表很帅气,为什么大部分互联网还是更多的选择自己分库分表来水平扩展咧?**
-回答:
+- 分区表,分区键设计不太灵活,如果不走分区键,很容易出现全表锁
-1)分区表,分区键设计不太灵活,如果不走分区键,很容易出现全表锁
-
-2)一旦数据量并发量上来,如果在分区表实施关联,就是一个灾难
-
-3)自己分库分表,自己掌控业务场景与访问模式,可控。分区表,研发写了一个sql,都不确定mysql是怎么玩的,不太可控
-
-4)运维的坑,嘿嘿
+- 一旦数据并发量上来,如果在分区表实施关联,就是一个灾难
+- 自己分库分表,自己掌控业务场景与访问模式,可控。分区表,研发写了一个sql,都不确定mysql是怎么玩的,不太可控
+
## Mysql分库
diff --git a/docs/data-management/MySQL/MySQL-Storage-Engines.md b/docs/data-management/MySQL/MySQL-Storage-Engines.md
index 0e93d617c1..19f5543935 100644
--- a/docs/data-management/MySQL/MySQL-Storage-Engines.md
+++ b/docs/data-management/MySQL/MySQL-Storage-Engines.md
@@ -1,14 +1,39 @@
-# Mysql Storage Engines
+---
+title: MySQL Storage Engine
+date: 2023-05-31
+tags:
+ - MySQL
+categories: MySQL
+---
+
+
+
+> 存储引擎是 MySQL 的组件,用于处理不同表类型的 SQL 操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。
+>
+> 使用哪一种引擎可以灵活选择,**一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求**,使用合适的存储引擎,将会提高整个数据库的性能 。
+>
+> MySQL 服务器使用可插拔的存储引擎体系结构,可以从运行中的MySQL服务器加载或卸载存储引擎 。
-存储引擎是 MySQL 的组件,用于处理不同表类型的 SQL 操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。
+> [MySQL 5.7 可供选择的存储引擎](https://dev.mysql.com/doc/refman/5.7/en/storage-engines.html)
-使用哪一种引擎可以灵活选择,**一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求**,使用合适的存储引擎,将会提高整个数据库的性能 。
+## 一、存储引擎的作用与架构
- MySQL服务器使用可插拔的存储引擎体系结构,可以从运行中的MySQL服务器加载或卸载存储引擎 。
+MySQL 存储引擎是数据库的底层核心组件,负责数据的**存储、检索、事务控制**以及**并发管理**。其架构采用**插件式设计**,允许用户根据业务需求灵活选择引擎类型,例如 InnoDB、MyISAM、Memory 等。这种设计将**查询处理**与**数据存储**解耦,提升了系统的可扩展性和灵活性 。
+
+MySQL 的体系架构分为四层:
+
+- **连接层**:管理客户端连接、认证与线程分配,支持 SSL 安全协议。
+- **核心服务层**:处理 SQL 解析、优化、缓存及内置函数执行。
+- **存储引擎层**:实际负责数据的存储和提取,支持多引擎扩展。
+- **数据存储层**:通过文件系统与存储引擎交互,管理物理文件。
-> [MySQL 5.7 可供选择的存储引擎](https://dev.mysql.com/doc/refman/5.7/en/storage-engines.html)
-### 查看存储引擎
+
+## 二、核心存储引擎详解
+
+### 2.1 常用存储引擎
+
+**查看存储引擎**
```mysql
-- 查看支持的存储引擎
@@ -25,166 +50,143 @@ show table status like 'tablename'
show table status from database where name="tablename"
```
-
+以下是 MySQL 主要存储引擎的对比表格,整合了各引擎的核心特性及适用场景,结合最新版本(MySQL 8.0+)特性更新:
+| **存储引擎** | **核心特性** | **事务支持** | **锁级别** | **索引类型** | **文件结构** | **适用场景** |
+| ---------------------- | ------------------------------------------------------------ | ------------ | -------------- | ----------------------- | -------------------------------------- | -------------------------------- |
+| **InnoDB** | 支持ACID事务、行级锁、MVCC、外键约束,具备崩溃恢复能力,默认使用聚簇索引 | ✅ | 行锁/表锁 | B+Tree/全文索引(5.6+) | `.ibd`(数据+索引)、`.frm`(表结构) | 高并发OLTP(电商交易、金融系统) |
+| **MyISAM** | 非事务型,表级锁,支持全文索引和压缩表,查询速度快 | ❌ | 表锁 | B+Tree/全文索引 | `.MYD`(数据)、`.MYI`(索引)、`.frm` | 静态报表、日志分析、只读业务 |
+| **Memory** | 数据全内存存储,哈希索引加速查询,重启后数据丢失 | ❌ | 表锁 | Hash/B-Tree | `.frm`(仅表结构) | 临时表、会话缓存、高速缓存层 |
+| **Archive** | 仅支持INSERT/SELECT,Zlib压缩存储(压缩率10:1),无索引 | ❌ | 行锁(仅插入) | ❌ | `.ARZ`(数据)、`.ARM`(元数据) | 历史数据归档、审计日志 |
+| **CSV** | 数据以CSV格式存储,可直接文本编辑,不支持索引 | ❌ | 表锁 | ❌ | `.CSV`(数据)、`.CSM`(元数据) | 数据导入/导出中间表 |
+| **Blackhole** | 写入数据即丢弃,仅保留二进制日志,用于复制链路中继 | ❌ | ❌ | ❌ | `.frm`(仅表结构) | 主从复制中继、性能测试 |
+| **Federated** | 代理访问远程表,本地无实际数据存储 | ❌ | 依赖远程表引擎 | 依赖远程表引擎 | `.frm`(仅表结构) | 分布式数据聚合 |
+| **NDB** | 集群式存储引擎,支持数据自动分片和高可用性 | ✅ | 行锁 | Hash/B-Tree | 数据存储在集群节点 | MySQL Cluster分布式系统 |
+| **Merge** | 聚合多个MyISAM表,逻辑上作为单个表操作 | ❌ | 表锁 | B-Tree | `.MRG`(聚合定义)、底层使用MyISAM文件 | 分库分表聚合查询 |
+| **Performance Schema** | 内置性能监控引擎,采集服务器运行时指标 | ❌ | ❌ | ❌ | 内存存储,无物理文件 | 性能监控与诊断 |
-### 设置存储引擎
-```mysql
--- 建表时指定存储引擎。默认的就是INNODB,不需要设置
-CREATE TABLE t1 (i INT) ENGINE = INNODB;
-CREATE TABLE t2 (i INT) ENGINE = CSV;
-CREATE TABLE t3 (i INT) ENGINE = MEMORY;
+### 2.2 存储引擎架构演进
--- 修改存储引擎
-ALTER TABLE t ENGINE = InnoDB;
+**1. MySQL 8.0 关键改进**
--- 修改默认存储引擎,也可以在配置文件my.cnf中修改默认引擎
-SET default_storage_engine=NDBCLUSTER;
-```
-
- 默认情况下,每当CREATE TABLE或ALTER TABLE不能使用默认存储引擎时,都会生成一个警告。为了防止在所需的引擎不可用时出现令人困惑的意外行为,可以启用`NO_ENGINE_SUBSTITUTION SQL`模式。如果所需的引擎不可用,则此设置将产生错误而不是警告,并且不会创建或更改表
+- 原子 DDL:DDL操作(如CREATE TABLE)具备事务性,失败时自动回滚元数据变更
+- 数据字典升级:系统表全部转为InnoDB引擎,替代原有的.frm文件,实现事务化元数据管理
+- Redo日志优化:MySQL 8.0.30+ 引入 `innodb_redo_log_capacity` 参数替代旧版日志配置,支持动态调整redo日志大小
+ **2. 物理文件结构变化**
+| 文件类型 | 5.7及之前版本 | 8.0+版本 | 作用 |
+| -------------- | ------------- | ------------------ | ------------------ |
+| 表结构定义文件 | .frm | .sdi (JSON格式) | 存储表结构元数据 6 |
+| 事务日志 | ibdata1 | undo_001, undo_002 | 独立UNDO表空间 |
+| 数据文件 | .ibd | .ibd | 表数据与索引存储 |
+| 临时文件 | ibtmp1 | ibtmp1 | 临时表空间 |
-### 常用存储引擎
+> 示例:通过 `SHOW CREATE TABLE` 可查看SDI元数据,支持 JSON 格式导出
-#### InnoDB
-**InnoDB 是 MySQL5.7 默认的存储引擎,主要特性有**
-- InnoDB存储引擎维护自己的缓冲池,在访问数据时将表和索引数据缓存在主内存中
-- 支持事务
-- 支持外键
-- B-Tree索引
-- 不支持集群
-- 聚簇索引
-- 行锁
-- 支持地理位置的数据类型和索引
+### 2.3 Innodb 引擎的 4 大特性
+#### **1. 插入缓冲(Insert Buffer / Change Buffer)**
+- **作用**:优化非唯一二级索引的插入、删除、更新(即 DML 操作)性能,减少磁盘随机 I/O 开销。
+- 原理:
+ - 当非唯一索引页不在内存中时,操作会被暂存到 Change Buffer(内存区域)中,而非直接写入磁盘。
+ - 后续通过合并(Merge)操作,将多个离散的修改批量写入磁盘,减少 I/O 次数。
+- 适用条件:
+ - 仅针对非唯一二级索引。
+ - 可通过参数 `innodb_change_buffer_max_size` 调整缓冲区大小(默认 25% 缓冲池)。
+#### 2. 二次写(Double Write)
-##### MySQL之Innodb引擎的4大特性
+- 作用:防止因部分页写入(Partial Page Write)导致的数据页损坏,确保崩溃恢复的可靠性。
+- 流程:
+ - 脏页刷盘时,先写入内存的 Doublewrite Buffer,再分两次(每次 1MB)顺序写入共享表空间的连续磁盘区域。
+ - 若数据页写入过程中崩溃,恢复时从共享表空间副本还原损坏页,再通过 Redo Log 恢复。
+- 意义:牺牲少量顺序 I/O 换取数据完整性,避免因随机 I/O 中断导致数据丢失。
-1. 插入缓冲 (Insert Buffer/Change Buffer)
-2. 双写机制(Double Write)
-3. 自适应哈希索引(Adaptive Hash Index,AHI)
-4. 预读 (Read Ahead)
+#### 3. 自适应哈希索引(Adaptive Hash Index, AHI)
+- 作用:自动为高频访问的索引页创建哈希索引,加速查询速度(尤其等值查询)。
+- 触发条件:
-#### MyISAM
+ - 同一索引被连续访问 17 次以上。
+ - 某页被访问超过 100 次,且访问模式一致(如固定 WHERE 条件)。
-在 5.1 版本之前,MyISAM 是 MySQL 的默认存储引擎,MyISAM 并发性比较差,使用的场景比较少,主要特点是
+- 限制
-每个MyISAM表存储在磁盘上的三个文件中 。这些文件的名称以表名开头,并有一个扩展名来指示文件类型 。
+ :仅对热点数据生效,无法手动指定,可通过参数 `innodb_adaptive_hash_index` 启用或关闭。
-`.frm`文件存储表的格式。 `.MYD` (`MYData`) 文件存储表的数据。 `.MYI` (`MYIndex`) 文件存储索引。
+#### 4. 预读(Read Ahead)
- **MyISAM表具有以下特征**
+- 作用:基于空间局部性原理,异步预加载相邻数据页到缓冲池,减少未来查询的磁盘 I/O。
+- 模式:
+ - 线性预读:按顺序访问的页超过阈值时,预加载下一批连续页(默认 64 页为一个块)。
+ - 随机预读(已废弃):当某块中部分页在缓冲池时,预加载剩余页,但因性能问题被弃用。
-- 每个MyISAM表最大索引数是64,这可以通过重新编译来改变。每个索引最大的列数是16
-- 每个MyISAM表都支持一个`AUTO_INCREMENT`的内部列。当执行`INSERT`或者`UPDATE`操作的时候,MyISAM自动更新这个列,这使得`AUTO_INCREMENT`列更快。
-- 当把删除和更新及插入操作混合使用的时候,动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块,若下一个块被删除,就扩展到下一块自动完成
+#### 其他重要特性补充
-- MyISAM支持**并发插入**
-- **可以将数据文件和索引文件放在不同物理设备上的不同目录中**,以更快地使用数据目录和索引目录表选项来创建表
-- BLOB和TEXT列可以被索引
-- **NULL被允许在索引的列中**,这个值占每个键的0~1个字节
-- 每个字符列可以有不同的字符集
-- **`MyISAM` 表使用 B-tree 索引**
-- MyISAM表的行最大限制为 (2^32)^2 (1.844E+19)
-- 大文件(达到63位文件长度)在支持大文件的文件系统和操作系统上被支持
-- 键的最大长度为1000字节,这也可以通过重新编译来改变,对于键长度超过250字节的情况,一个超过1024字节的键将被用上
+尽管上述四点是核心性能优化特性,但 InnoDB 的其他关键能力也值得注意:
-- VARCHAR支持固定或动态记录长度
-- 表中VARCHAR和CHAR列的长度总和有可能达到64KB
-- 任意长度的唯一约束
+- 事务支持:通过 ACID 特性(原子性、一致性、隔离性、持久性)保障数据一致性。
+- 行级锁与外键约束:支持高并发与数据完整性。
+- **崩溃恢复**:结合 Redo Log 和 Double Write 实现快速恢复
-- All data values are stored with the low byte first. This makes the data machine and operating system independent.
-- All numeric key values are stored with the high byte first to permit better index compression
- todo:最后两条没搞懂啥意思
+### 2.4 数据的存储
+在整个数据库体系结构中,我们可以使用不同的存储引擎来存储数据,而绝大多数存储引擎都以二进制的形式存储数据;我们来看下InnoDB 中对数据是如何存储的。
+在 InnoDB 存储引擎中,所有的数据都被逻辑地存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page)
-### 存储引擎对比
-
-| 对比项 | MyISAM | InnoDB |
-| -------- | -------------------------------------------------------- | ------------------------------------------------------------ |
-| 主外键 | 不支持 | 支持 |
-| 事务 | 不支持 | 支持 |
-| 行表锁 | 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 | 行锁,操作时只锁某一行,不对其它行有影响,
适合高并发的操作 |
-| 缓存 | 只缓存索引,不缓存真实数据 | 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 |
-| 表空间 | 小 | 大 |
-| 关注点 | 性能 | 事务 |
-| 默认安装 | 是 | 是 |
+
-官方提供的多种引擎对比
-
-| Feature | MyISAM | Memory | InnoDB | Archive | NDB |
-| ------------------------------------------ | ------------ | ---------------- | ------------ | ------------ | ------------ |
-| **B-tree indexes** | Yes | Yes | Yes | No | No |
-| **Backup/point-in-time recovery** (note 1) | Yes | Yes | Yes | Yes | Yes |
-| **Cluster database support** | No | No | No | No | Yes |
-| **Clustered indexes** | No | No | Yes | No | No |
-| **Compressed data** | Yes (note 2) | No | Yes | Yes | No |
-| **Data caches** | No | N/A | Yes | No | Yes |
-| **Encrypted data** | Yes (note 3) | Yes (note 3) | Yes (note 4) | Yes (note 3) | Yes (note 3) |
-| **Foreign key support** | No | No | Yes | No | Yes (note 5) |
-| **Full-text search indexes** | Yes | No | Yes (note 6) | No | No |
-| **Geospatial data type support** | Yes | No | Yes | Yes | Yes |
-| **Geospatial indexing support** | Yes | No | Yes (note 7) | No | No |
-| **Hash indexes** | No | Yes | No (note 8) | No | Yes |
-| **Index caches** | Yes | N/A | Yes | No | Yes |
-| **Locking granularity** | Table | Table | Row | Row | Row |
-| **MVCC** | No | No | Yes | No | No |
-| **Replication support** (note 1) | Yes | Limited (note 9) | Yes | Yes | Yes |
-| **Storage limits** | 256TB | RAM | 64TB | None | 384EB |
-| **T-tree indexes** | No | No | No | No | Yes |
-| **Transactions** | No | No | Yes | No | Yes |
-| **Update statistics for data dictionary** | Yes | Yes | Yes | Yes | Yes |
-
-
-
-### 数据的存储
-
-在整个数据库体系结构中,我们可以使用不同的存储引擎来存储数据,而绝大多数存储引擎都以二进制的形式存储数据;这一节会介绍 InnoDB 中对数据是如何存储的。
+ 同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 `innodb_page_size` 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同
-在 InnoDB 存储引擎中,所有的数据都被逻辑地存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page)
+对于 16KB 的页来说,连续的 64 个页就是一个区,也就是 1 个区默认占用 1 MB 空间的大小。
+
+#### 数据页结构
- 
+页是 InnoDB 存储引擎管理数据的最小磁盘单位,一个页的大小一般是 `16KB`。
- 同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 `innodb_page_size` 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同
+`InnoDB` 为了不同的目的而设计了许多种不同类型的`页`,比如存放表空间头部信息的页,存放 `Insert Buffer` 信息的页,存放 `INODE`信息的页,存放 `undo` 日志信息的页等等等等。
-
+ B-Tree 节点就是实际存放表中数据的页面,我们在这里将要介绍页是如何组织和存储记录的;首先,一个 InnoDB 页有以下七个部分:
-从图中可以看出,在 InnoDB 存储引擎中,一个区的大小最小为 1MB,页的数量最少为 64 个。
+
+有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。
+| 名称 | 中文名 | 占用空间大小 | 简单描述 |
+| -------------------- | ------------------ | ------------ | ------------------------ |
+| `File Header` | 文件头部 | `38`字节 | 页的一些通用信息 |
+| `Page Header` | 页面头部 | `56`字节 | 数据页专有的一些信息 |
+| `Infimum + Supremum` | 最小记录和最大记录 | `26`字节 | 两个虚拟的行记录 |
+| `User Records` | 用户记录 | 不确定 | 实际存储的行记录内容 |
+| `Free Space` | 空闲空间 | 不确定 | 页中尚未使用的空间 |
+| `Page Directory` | 页面目录 | 不确定 | 页中的某些记录的相对位置 |
+| `File Trailer` | 文件尾部 | `8`字节 | 校验页是否完整 |
-#### 如何存储表
+在页的 7 个组成部分中,我们自己存储的记录会按照我们指定的`行格式`存储到 `User Records` 部分。但是在一开始生成页的时候,其实并没有 `User Records` 这个部分,每当我们插入一条记录,都会从 `Free Space` 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 `User Records` 部分,当 `Free Space` 部分的空间全部被 `User Records` 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了,这个过程的图示如下:
-MySQL 使用 InnoDB 存储表时,会将表的定义和数据索引等信息分开存储,其中前者存储在 `.frm` 文件中,后者存储在 `.ibd` 文件中,这一节就会对这两种不同的文件分别进行介绍。
+
-
-#### .frm 文件
-无论在 MySQL 中选择了哪个存储引擎,所有的 MySQL 表都会在硬盘上创建一个 `.frm` 文件用来描述表的格式或者说定义;`.frm` 文件的格式在不同的平台上都是相同的。
+#### 如何存储表
-```mysql
-`CREATE TABLE test_frm(`` ``column1 CHAR(5),`` ``column2 INTEGER``);`
-```
+MySQL 使用 InnoDB 存储表时,会将表的定义和数据索引等信息分开存储,其中前者存储在 `.frm` 文件中,后者存储在 `.ibd` 文件中。
-当我们使用上面的代码创建表时,会在磁盘上的 `datadir` 文件夹中生成一个 `test_frm.frm` 的文件,这个文件中就包含了表结构相关的信息:
+#### .frm 文件
-
+无论在 MySQL 中选择了哪个存储引擎,所有的 MySQL 表都会在硬盘上创建一个 `.frm` 文件用来描述表的格式或者说定义;`.frm` 文件的格式在不同的平台上都是相同的。
> MySQL 官方文档中的 [11.1 MySQL .frm File Format](https://dev.mysql.com/doc/internals/en/frm-file-format.html) 一文对于 `.frm` 文件格式中的二进制的内容有着非常详细的表述。
@@ -194,54 +196,62 @@ InnoDB 中用于存储数据的文件总共有两个部分,一是系统表空
当打开 `innodb_file_per_table` 选项时,`.ibd` 文件就是每一个表独有的表空间,文件存储了当前表的数据和相关的索引数据。
-#### 如何存储记录
+#### 如何存储记录 | InnoDB 行格式
-与现有的大多数存储引擎一样,InnoDB 使用页作为磁盘管理的最小单位;数据在 InnoDB 存储引擎中都是按行存储的,每个 16KB 大小的页中可以存放 2-7992 行的记录。(至少是2条记录,最多是7992条记录)
+InnoDB 存储引擎和大多数数据库一样,记录是以行的形式存储的,每个 16KB 大小的页中可以存放多条行记录。
-当 InnoDB 存储数据时,它可以使用不同的行格式进行存储;MySQL 5.7 版本支持以下格式的行存储方式:
+它可以使用不同的行格式进行存储。
-
+InnoDB 早期的文件格式为 `Antelope`,可以定义两种行记录格式,分别是 `Compact` 和 `Redundant`,InnoDB 1.0.x 版本开始引入了新的文件格式 `Barracuda`。`Barracuda `文件格式下拥有两种新的行记录格式:`Compressed` 和 `Dynamic`。
-Antelope 是 InnoDB 最开始支持的文件格式,它包含两种行格式 Compact 和 Redundant,它最开始并没有名字;Antelope 的名字是在新的文件格式 Barracuda 出现后才起的,Barracuda 的出现引入了两种新的行格式 Compressed 和 Dynamic;InnoDB 对于文件格式都会向前兼容,而官方文档中也对之后会出现的新文件格式预先定义好了名字:Cheetah、Dragon、Elk 等等。
+> [InnoDB Row Formats](https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html#innodb-row-format-redundant)
-两种行记录格式 Compact 和 Redundant 在磁盘上按照以下方式存储:
+
-
+MySQL 5.7 版本支持以上格式的行存储方式。
-Compact 和 Redundant 格式最大的不同就是记录格式的第一个部分;在 Compact 中,行记录的第一部分倒序存放了一行数据中列的长度(Length),而 Redundant 中存的是每一列的偏移量(Offset),从总体上看,Compact 行记录格式相比 Redundant 格式能够减少 20% 的存储空间。
+我们可以在创建或修改表的语句中指定行格式:
-#### 行溢出数据
+```mysql
+CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
+
+ALTER TABLE 表名 ROW_FORMAT=行格式名称
+```
-当 InnoDB 使用 Compact 或者 Redundant 格式存储极长的 VARCHAR 或者 BLOB 这类大对象时,我们并不会直接将所有的内容都存放在数据页节点中,而是将行数据中的前 768 个字节存储在数据页中,后面会通过偏移量指向溢出页。
+`Compact `行记录格式是在 MySQL 5.0 中引入的,其首部是一个非 NULL 变长列长度列表,并且是逆序放置的,其长度为:
-
+- 若列的长度小于等于 255 字节,用 1 个字节表示;
+- 若列的长度大于 255 字节,用 2 个字节表示。
-但是当我们使用新的行记录格式 Compressed 或者 Dynamic 时都只会在行记录中保存 20 个字节的指针,实际的数据都会存放在溢出页面中。
+
-
+变长字段的长度最大不可以超过 2 字节,这是因为 MySQL 数据库中 VARCHAR 类型的最大长度限制为 65535。变长字段之后的第二个部分是 NULL 标志位,该标志位指示了该行数据中某列是否为 NULL 值,有则用 1 表示,NULL 标志位也是不定长的。接下来是记录头部信息,固定占用 5 字节。
-当然在实际存储中,可能会对不同长度的 TEXT 和 BLOB 列进行优化,不过这就不是本文关注的重点了。
+`Redundant` 是 MySQL 5.0 版本之前 InnoDB 的行记录格式,`Redundant` 行记录格式的首部是每一列长度偏移列表,同样是逆序存放的。从整体上看,`Compact `格式的存储空间减少了约 20%,但代价是某些操作会增加 CPU 的使用。
-> 想要了解更多与 InnoDB 存储引擎中记录的数据格式的相关信息,可以阅读 [InnoDB Record Structure](https://dev.mysql.com/doc/internals/en/innodb-record-structure.html)
+`Dynamic` 和 `Compressed `是 `Compact `行记录格式的变种,`Compressed `会对存储在其中的行数据会以 `zlib` 的算法进行压缩,因此对于 BLOB、TEXT、VARCHAR 这类大长度类型的数据能够进行非常有效的存储。
-#### 数据页结构
+> 高版本,比如 8.3 默认使用的是 Dynamic
+>
+> ```sql
+> SELECT @@innodb_default_row_format;
+> ```
-页是 InnoDB 存储引擎管理数据的最小磁盘单位,而 B-Tree 节点就是实际存放表中数据的页面,我们在这里将要介绍页是如何组织和存储记录的;首先,一个 InnoDB 页有以下七个部分:
-
-每一个页中包含了两对 header/trailer:内部的 Page Header/Page Directory 关心的是页的状态信息,而 Fil Header/Fil Trailer 关心的是记录页的头信息。
+#### 行溢出数据
-在页的头部和尾部之间就是用户记录和空闲空间了,每一个数据页中都包含 Infimum 和 Supremum 这两个虚拟的记录(可以理解为占位符),Infimum 记录是比该页中任何主键值都要小的值,Supremum 是该页中的最大值:
+当 InnoDB 存储极长的 TEXT 或者 BLOB 这类大对象时,MySQL 并不会直接将所有的内容都存放在数据页中。因为 InnoDB 存储引擎使用 B+Tree 组织索引,每个页中至少应该有两条行记录,因此,如果页中只能存放下一条记录,那么 InnoDB 存储引擎会自动将行数据存放到溢出页中。
-
+如果我们使用 `Compact` 或 `Redundant` 格式,那么会将行数据中的前 768 个字节存储在数据页中,后面的数据会通过指针指向 Uncompressed BLOB Page。
-User Records 就是整个页面中真正用于存放行记录的部分,而 Free Space 就是空余空间了,它是一个链表的数据结构,为了保证插入和删除的效率,整个页面并不会按照主键顺序对所有记录进行排序,它会自动从左侧向右寻找空白节点进行插入,行记录在物理存储上并不是按照顺序的,它们之间的顺序是由 `next_record` 这一指针控制的。
+但是如果我们使用新的行记录格式 `Compressed` 或者 `Dynamic` 时只会在行记录中保存 20 个字节的指针,实际的数据都会存放在溢出页面中。
-B+ 树在查找对应的记录时,并不会直接从树中找出对应的行记录,它只能获取记录所在的页,将整个页加载到内存中,再通过 Page Directory 中存储的稀疏索引和 `n_owned`、`next_record` 属性取出对应的记录,不过因为这一操作是在内存中进行的,所以通常会忽略这部分查找的耗时。
-InnoDB 存储引擎中对数据的存储是一个非常复杂的话题,这一节中也只是对表、行记录以及页面的存储进行一定的分析和介绍,虽然作者相信这部分知识对于大部分开发者已经足够了,但是想要真正消化这部分内容还需要很多的努力和实践。
+### 参考与引用:
+- https://www.linkedin.com/pulse/leverage-innodb-architecture-optimize-django-model-design-bouslama
+- [踏雪无痕-InnoDB存储引擎](https://www.cnblogs.com/chenpingzhao/p/9177324.html)
+- [MySQL 与 InnoDB 存储引擎总结](https://wingsxdu.com/posts/database/mysql/innodb/)
-> [踏雪无痕-InnoDB存储引擎](https://www.cnblogs.com/chenpingzhao/p/9177324.html)
\ No newline at end of file
diff --git a/docs/data-management/MySQL/MySQL-Transaction.md b/docs/data-management/MySQL/MySQL-Transaction.md
index ddcb54a82c..45e21bd4f4 100644
--- a/docs/data-management/MySQL/MySQL-Transaction.md
+++ b/docs/data-management/MySQL/MySQL-Transaction.md
@@ -20,6 +20,8 @@ categories: MySQL
事务是由一组 SQL 语句组成的逻辑处理单元,具有 4 个属性,通常简称为事务的 ACID 属性。
+
+
- **A (Atomicity) 原子性**:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- **C (Consistency) 一致性**:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- **I (Isolation)隔离性**:一个事务所做的修改在最终提交以前,对其他事务是不可见的。这种属性有时称为『串行化』,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。
@@ -29,7 +31,7 @@ categories: MySQL
## 二、MySQL 中事务的使用
- MySQL的服务层不管理事务,而是由下层的存储引擎实现。MySQL提供了两种事务型的存储引擎:InnoDB和NDB。
+ MySQL 的服务层不管理事务,而是由下层的存储引擎实现。MySQL 提供了两种事务型的存储引擎:InnoDB 和 NDB。
**MySQL支持本地事务的语句:**
@@ -48,36 +50,35 @@ SET AUTOCOMMIT = {0 | 1}
**事务使用注意点:**
- 如果在锁表期间,用 start transaction 命令开始一个新事务,会造成一个隐含的 unlock tables 被执行。
-- 在同一个事务中,最好不使用不同存储引擎的表,否则 ROLLBACK 时需要对非事
- 务类型的表进行特别的处理,因为 COMMIT、ROLLBACK 只能对事务类型的表进行提交和回滚。
+- 在同一个事务中,最好不使用不同存储引擎的表,否则 ROLLBACK 时需要对非事务类型的表进行特别的处理,因为 COMMIT、ROLLBACK 只能对事务类型的表进行提交和回滚。
- 和 Oracle 的事务管理相同,所有的 DDL 语句是不能回滚的,并且部分的 DDL 语句会造成隐式的提交。
- 在事务中可以通过定义 SAVEPOINT(例如:mysql> savepoint test; 定义 savepoint,名称为 test),指定回滚事务的一个部分,但是不能指定提交事务的一个部分。对于复杂的应用,可以定义多个不同的 SAVEPOINT,满足不同的条件时,回滚
不同的 SAVEPOINT。需要注意的是,如果定义了相同名字的 SAVEPOINT,则后面定义的SAVEPOINT 会覆盖之前的定义。对于不再需要使用的 SAVEPOINT,可以通过 RELEASE SAVEPOINT 命令删除 SAVEPOINT, 删除后的 SAVEPOINT, 不能再执行 ROLLBACK TO SAVEPOINT命令。
**自动提交(autocommit):**
-Mysql默认采用自动提交模式,可以通过设置autocommit变量来启用或禁用自动提交模式
+Mysql 默认采用自动提交模式,可以通过设置 `autocommit` 变量来启用或禁用自动提交模式
- **隐式锁定**
- InnoDB在事务执行过程中,使用两阶段锁协议:
+ InnoDB 在事务执行过程中,使用两阶段锁协议:
- 随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;
+ 随时都可以执行锁定,InnoDB 会根据隔离级别在需要的时候自动加锁;
- 锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在**同一时刻**被释放。
+ 锁只有在执行 commit 或者 rollback 的时候才会释放,并且所有的锁都是在**同一时刻**被释放。
- **显式锁定**
- InnoDB也支持通过特定的语句进行显示锁定(存储引擎层):
+ InnoDB 也支持通过特定的语句进行显示锁定(存储引擎层):
```mysql
select ... lock in share mode //共享锁
select ... for update //排他锁
```
- MySQL Server层的显示锁定:
+ MySQL Server 层的显示锁定:
```mysql
-lock table和unlock table
+lock table 和 unlock table
```
@@ -178,6 +179,25 @@ mysql> show variables like 'transaction_isolation';
+#### demo
+
+```mysql
+create table T(t int) engine=InnoDB;
+insert into T(t) values(1);
+```
+
+
+
+- “读未提交”:则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。
+- “读提交”:则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
+- “可重复读”:则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
+- “串行化”:则在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。
+
+> - 读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到。
+> - 读已提交:别人改数据的事务已经提交,我在我的事务中才能读到。
+> - 可重复读:别人改数据的事务已经提交,我在我的事务中也不去读。
+> - 串行:我的事务尚未提交,别人就别想改数据。
+
| 事务隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
| ---------------------------- | ---------------------------------------- | ---- | ---------- | ---- |
| 读未提交(read-uncommitted) | 最低级被,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
@@ -185,22 +205,84 @@ mysql> show variables like 'transaction_isolation';
| 可重复读(repeatable-read) | 事务级 | 否 | 否 | 是 |
| 串行化(serializable) | 最高级别,事务级 | 否 | 否 | 否 |
-
-
需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。
## 三、MVCC 多版本并发控制
+#### 核心概念
+
+1. **快照读(Snapshot Read)**:
+ - 每个事务在开始时,会获取一个数据快照,事务在读取数据时,总是读取该快照中的数据。
+ - 这意味着即使在事务进行期间,其他事务对数据的更新也不会影响当前事务的读取。
+2. **版本链(Version Chain)**:
+ - 每个数据行都有多个版本,每个版本包含数据和元数据(如创建时间、删除时间等)。
+ - 新版本的数据行会被链接到旧版本的数据行,形成一个版本链。
+3. **隐式锁(Implicit Locking)**:
+ - MVCC 通过版本管理避免了显式锁定,减少了锁争用问题。
+ - 对于读取操作,事务读取其开始时的快照数据,不会被写操作阻塞。
+
+#### MVCC 的底层实现
+
+1. **数据行的多版本存储**:
+ - 每个数据行在物理存储上会有多个版本,每个版本包含该行在特定时间点的值。
+ - 数据行版本包含元数据,如事务ID(Transaction ID)、创建时间戳和删除时间戳。
+2. **快照读取**:
+ - 每个事务在开始时,会记录当前系统的事务ID作为快照ID。
+ - 读取数据时,只读取那些创建时间戳早于快照ID,并且删除时间戳为空或晚于快照ID的数据版本。
+3. **事务提交和版本更新**:
+ - 当一个事务对数据行进行更新时,会创建一个新的数据版本,并将其链接到现有版本链上。
+ - 旧版本仍然存在,直到没有任何活动事务需要访问它们。
+
+#### MVCC 在MySQL中的实现
+
+MySQL InnoDB 存储引擎使用 MVCC 来实现可重复读(REPEATABLE READ)隔离级别,避免脏读、不可重复读和幻读问题。具体机制如下:
+
+1. **隐藏列**:
+ - InnoDB 在每行记录中存储两个隐藏列:`trx_id`(事务ID)和`roll_pointer`(回滚指针)。
+ - `trx_id` 记录最后一次修改该行的事务ID,`roll_pointer` 指向该行的上一版本。
+
+> 其实,InnoDB下的 Compact 行结构,有三个隐藏的列
+>
+> | 列名 | 是否必须 | 描述 |
+> | -------------- | -------- | ------------------------------------------------------------ |
+> | row_id | 否 | 行ID,唯一标识一条记录(如果定义主键,它就没有啦) |
+> | transaction_id | 是 | 事务ID |
+> | roll_pointer | 是 | DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本 |
+
+2. **Undo日志**:
+
+ - 每次数据更新时,InnoDB 会在 Undo 日志中记录旧版本数据。
+
+ - 如果需要读取旧版本数据,InnoDB 会通过 `roll_pointer` 找到 Undo 日志中的旧版本。
+
+3. **一致性视图(Consistent Read View)**:
+
+ - InnoDB 为每个事务创建一致性视图,记录当前活动的所有事务ID。
+
+ - 读取数据时,会根据一致性视图决定哪些版本的数据对当前事务可见。
+
+
+
+在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
+
+假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
+
+
+
+当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。
+
+同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。
+
+
+
MySQL 的大多数事务型存储引擎实现都不是简单的行级锁。基于提升并发性考虑,一般都同时实现了多版本并发控制(MVCC),包括Oracle、PostgreSQL。只是实现机制各不相同。
可以认为 MVCC 是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。
MVCC 的实现是通过保存数据在某个时间点的快照来实现的。也就是说不管需要执行多长时间,每个事物看到的数据都是一致的。
-
-
典型的 MVCC 实现方式,分为**乐观(optimistic)并发控制和悲观(pressimistic)并发控制**。
下边通过 InnoDB 的简化版行为来说明 MVCC 是如何工作的。
@@ -213,7 +295,7 @@ InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列来实
InnoDB 会根据以下两个条件检查每行记录:
- - nnoDB 只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在开始事务之前已经存在要么是事务自身插入或者修改过的
+ - InnoDB 只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在开始事务之前已经存在要么是事务自身插入或者修改过的
- 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除
只有符合上述两个条件的才会被查询出来
@@ -234,13 +316,24 @@ InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列来实
MVCC 只在 COMMITTED READ(读提交)和 REPEATABLE READ(可重复读)两种隔离级别下工作。
+> 所谓的`MVCC`,就是通过生成一个`ReadView`,然后通过`ReadView`找到符合条件的记录版本(历史版本是由`undo日志`构建的),其实就像是在生成`ReadView`的那个时刻做了一次时间静止(就像用相机拍了一个快照),查询语句只能读到在生成`ReadView`之前已提交事务所做的更改,在生成`ReadView`之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用`MVCC`时,`读-写`操作并不冲突。
+
## 四、事务的实现
> 事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现 。
+### 一致性读(Consistent Reads)
+
+事务利用`MVCC`进行的读取操作称为`一致性读`,或者`一致性无锁读`,有的地方也称之为`快照读`。所有普通的`SELECT`语句(`plain SELECT`)在`READ COMMITTED`、`REPEATABLE READ`隔离级别下都算是`一致性读`,比方说:
+
+```sql
+SELECT * FROM t;
+SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2
+```
+`一致性读`并不会对表中的任何记录做`加锁`操作,其他事务可以自由的对表中的记录做改动。
### 事务日志
@@ -302,7 +395,7 @@ undo log 主要为事务的回滚服务。在事务执行的过程中,除了
>
> 假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
>
-> 
+> 
>
> 当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。
>
diff --git a/docs/data-management/MySQL/MySQL-select.md b/docs/data-management/MySQL/MySQL-select.md
index 7831d41f21..ac0c35efdd 100644
--- a/docs/data-management/MySQL/MySQL-select.md
+++ b/docs/data-management/MySQL/MySQL-select.md
@@ -1,4 +1,121 @@
-## 常见通用的Join查询
+---
+title: MySQL 查询
+date: 2023-03-31
+tags:
+ - MySQL
+categories: MySQL
+---
+
+
+
+
+
+> `SQL`的全称是`Structured Query Language`,翻译后就是`结构化查询语言`。
+
+## Order By
+
+在开发应用的时候,一定会经常碰到需要根据指定的字段排序来显示结果的需求。
+
+```mysql
+CREATE TABLE `t` (
+ `id` int(11) NOT NULL,
+ `city` varchar(16) NOT NULL,
+ `name` varchar(16) NOT NULL,
+ `age` int(11) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `city` (`city`)
+) ENGINE=InnoDB;
+```
+
+业务需求来了,
+
+
+
+Extra 这个字段中的“Using filesort”表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。
+
+```mysql
+mysql> show variables like 'sort_buffer_size';
++------------------+--------+
+| Variable_name | Value |
++------------------+--------+
+| sort_buffer_size | 262144 |
++------------------+--------+
+1 row in set (0.00 sec)
+```
+
+### 全字段排序
+
+这个语句的大概执行流程是这样的:
+
+1. 初始化 sort_buffer,确定放入 name、city、age 这三个字段;
+2. 从索引 city 找到第一个满足 city='北京’ 条件的主键 id;
+3. 到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;
+4. 从索引 city 取下一个记录的主键 id;
+5. 重复步骤 3、4 直到 city 的值不满足查询条件为止;
+6. 对 sort_buffer 中的数据按照字段 name 做快速排序;
+7. 按照排序结果取前 3 行返回给客户端。
+
+我们暂且把这个排序过程,称为『全字段排序』
+
+“按 name 排序”这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。
+
+sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
+
+### rowid 排序
+
+在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。
+
+所以如果单行很大,这个方法效率不够好。
+
+那么,**如果 MySQL 认为排序的单行长度太大会怎么做呢?**
+
+接下来,我来修改一个参数,让 MySQL 采用另外一种算法。
+
+```mysql
+SET max_length_for_sort_data = 16;
+```
+
+max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。
+
+city、name、age 这三个字段的定义总长度是 36,我把 max_length_for_sort_data 设置为 16,我们再来看看计算过程有什么改变。
+
+新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。
+
+但这时,排序的结果就因为少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成如下所示的样子:
+
+1. 初始化 sort_buffer,确定放入两个字段,即 name 和 id;
+2. 从索引 city 找到第一个满足 city='杭州’条件的主键 id;
+3. 到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;
+4. 从索引 city 取下一个记录的主键 id;
+5. 重复步骤 3、4 直到不满足 city='杭州’条件为止;
+6. 对 sort_buffer 中的数据按照字段 name 进行排序;
+7. 遍历排序结果,取前 3 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
+
+对全字段排序流程你会发现,rowid 排序多访问了一次表 t 的主键索引,就是步骤 7。
+
+
+
+### 全字段排序 VS rowid 排序
+
+如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
+
+如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
+
+这也就体现了 MySQL 的一个设计思想:**如果内存够,就要多利用内存,尽量减少磁盘访问。**
+
+
+
+当然,如果我们创建了覆盖索引,就不需要再排序了
+
+```mysql
+alter table t add index city_user_age(city, name, age);
+```
+
+
+
+
+
+## 常见通用的 Join 查询
### SQL执行顺序
@@ -32,13 +149,11 @@
- 总结
- 
-
-
+ 
### Join图
-
+
### demo
@@ -111,7 +226,7 @@ INSERT INTO tbl_emp(NAME,deptId) VALUES('s9',51);
```
6. AB全有
-
+
**MySQL Full Join的实现 因为MySQL不支持FULL JOIN,替代方法:left join + union(可去除重复数据)+ right join**
@@ -131,3 +246,42 @@ INSERT INTO tbl_emp(NAME,deptId) VALUES('s9',51);
+## count()
+
+count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加。最后返回累计值。
+
+### count(*) 的实现方式
+
+你首先要明确的是,在不同的 MySQL 引擎中,count(*) 有不同的实现方式。
+
+- MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高;
+- 而 InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
+
+> 那**为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢?**
+>
+> 这是因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的。
+
+
+
+
+
+**对于 count(主键 id) 来说**,InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。
+
+**对于 count(1) 来说**,InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
+
+单看这两个用法的差别的话,你能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。
+
+**对于 count(字段) 来说**:
+
+1. 如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;
+2. 如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。
+
+
+
+按照效率排序的话,count(字段) 数据库的设计三范式:
+>
+> - 1NF:字段不可分。第一范式要求数据原子化
+> - 2NF:唯一性 一个表只说明一个事物。有主键,非主键字段依赖主键。第二范式消除部分依赖
+> - 3NF:非主键字段不能相互依赖。第三范式消除传递依赖,从而提高数据的完整性和一致性
+
+在当今信息化时代,数据库已经成为各行各业中不可或缺的一部分。从小型应用到企业级系统,数据库都在背后默默支撑着数据的存储、管理与分析。而在数据库设计中,**三范式**(3NF)是关系型数据库模型设计中最为基础和重要的规范化原则之一。三范式的核心目标是通过规范化数据库的结构,减少冗余数据,增强数据一致性和完整性,避免更新异常,确保数据的高效存储与访问。
+
+### 什么是数据库规范化?
+
+数据库规范化(Normalization)是指对数据库表的设计进行优化的过程,通过一系列规则来消除冗余数据,并提高数据的组织方式。规范化不仅帮助减少数据的重复性,还确保数据之间的逻辑关系清晰,减少因数据修改引发的不一致问题。
+
+规范化的核心思想是通过分解数据库表,降低数据冗余性和依赖性。在实际操作中,数据库规范化通常分为多个阶段,称为**范式**。每一范式都是在前一范式的基础上进一步精化和完善的。三范式是数据库设计中最为基础且常用的范式,它通过消除部分依赖和传递依赖等问题,保证数据库的高效性与一致性。
+
+### 第一范式(1NF)
+
+**第一范式**是数据库规范化的最基础范式,它的要求是:**表中的每个列必须包含原子值,即每个字段只能存储不可再分的单一数据单元**。简而言之,1NF 要求每个列的数据必须是“原子的”,也就是没有重复数据和多值属性。
+
+#### 1NF的具体要求:
+
+1. 每列中的数据必须是不可分割的单一数据项。
+2. 每一行必须唯一,不允许有重复的记录。
+3. 表中的每个字段都必须具有唯一的标识符(即主键)。
+
+**示例**: 假设我们有一个学生信息表,如下所示:
+
+| 学生ID | 姓名 | 电话号码 |
+| ------ | ---- | ---------------------- |
+| 1 | 张三 | 1234567890, 0987654321 |
+| 2 | 李四 | 1357924680, 2468013579 |
+
+从上表可以看出,"电话号码"字段存储了多个值,违反了1NF。要满足1NF要求,电话号码字段应拆分为多行或多列,像这样:
+
+| 学生ID | 姓名 | 电话号码 |
+| ------ | ---- | ---------- |
+| 1 | 张三 | 1234567890 |
+| 1 | 张三 | 0987654321 |
+| 2 | 李四 | 1357924680 |
+| 2 | 李四 | 2468013579 |
+
+这样,每个字段就变成了原子值,符合了1NF的要求。
+
+### 第二范式(2NF)
+
+第二范式建立在第一范式的基础之上,要求**消除部分依赖**。具体而言,2NF要求表中的所有非主属性(即不参与主键的字段)必须完全依赖于主键,而不能仅依赖于主键的一部分。换句话说,表中的每一个非主属性必须依赖于完整的主键,而不是主键的某一部分。
+
+#### 2NF的要求:
+
+1. 表必须满足1NF。
+2. 表中的每个非主键列必须完全依赖于整个主键。
+3. 如果表有复合主键(由多个列组成的主键),则不能有部分依赖。
+
+**示例**: 考虑以下的订单表:
+
+| 订单ID | 产品ID | 产品名称 | 数量 | 单价 |
+| ------ | ------ | -------- | ---- | ---- |
+| 1 | 101 | 手机 | 2 | 2000 |
+| 1 | 102 | 耳机 | 1 | 500 |
+| 2 | 101 | 手机 | 1 | 2000 |
+
+在这个例子中,复合主键由**订单ID**和**产品ID**组成。虽然**产品名称**只依赖于**产品ID**,但它却被存储在了当前的表中,这种依赖关系违反了2NF。我们可以通过拆分表格来消除部分依赖:
+
+**订单表:**
+
+| 订单ID | 产品ID | 数量 | 单价 |
+| ------ | ------ | ---- | ---- |
+| 1 | 101 | 2 | 2000 |
+| 1 | 102 | 1 | 500 |
+| 2 | 101 | 1 | 2000 |
+
+**产品表:**
+
+| 产品ID | 产品名称 |
+| ------ | -------- |
+| 101 | 手机 |
+| 102 | 耳机 |
+
+通过这样的拆分,我们确保了所有非主键字段完全依赖于复合主键,符合了2NF。
+
+### 第三范式(3NF)
+
+第三范式是在第二范式的基础上进一步规范化,它要求消除**传递依赖**。传递依赖是指某个非主键列依赖于另一个非主键列,而这个非主键列又依赖于主键。换句话说,第三范式要求表中的每个非主属性都直接依赖于主键,而不是间接依赖。
+
+#### 3NF的要求:
+
+1. 表必须满足2NF。
+2. 表中的每个非主键列必须直接依赖于主键,不能依赖于其他非主键列。
+
+**示例**: 考虑以下员工表:
+
+| 员工ID | 部门ID | 部门名称 | 部门经理 |
+| ------ | ------ | -------- | -------- |
+| 1 | 101 | 销售部 | 王经理 |
+| 2 | 102 | 技术部 | 张经理 |
+
+在这个例子中,**部门名称**和**部门经理**依赖于**部门ID**,而**部门ID**又依赖于**员工ID**,这就构成了传递依赖。为了满足3NF,我们应该将部门相关信息提取到单独的表中:
+
+**员工表:**
+
+| 员工ID | 部门ID |
+| ------ | ------ |
+| 1 | 101 |
+| 2 | 102 |
+
+**部门表:**
+
+| 部门ID | 部门名称 | 部门经理 |
+| ------ | -------- | -------- |
+| 101 | 销售部 | 王经理 |
+| 102 | 技术部 | 张经理 |
+
+通过这样的拆分,我们消除了传递依赖,确保了表符合3NF的要求。
+
+### 规范化的优点与实际应用
+
+数据库规范化的主要优点是减少冗余数据,提高数据的一致性和完整性。通过规范化,数据的修改操作变得更加简便,不容易出现更新异常,例如**插入异常**、**删除异常**和**更新异常**。
+
+然而,在实际应用中,数据库设计并非总是严格遵循最高范式。过度规范化可能会导致表的拆分过多,从而影响查询性能。在这种情况下,数据库设计师可能会选择在一定程度上**反规范化**,即故意增加一些冗余数据,以提高查询效率。
+
+### 总结
+
+数据库的三范式是关系型数据库设计的基础,它通过消除数据冗余和不必要的依赖关系,帮助提高数据的一致性和完整性。第一范式要求数据原子化,第二范式消除部分依赖,而第三范式消除传递依赖。在实际的数据库设计中,三范式为我们提供了科学的规范化思路,但在一些特定场景下,可能需要适当进行反规范化来优化查询性能。
+
+了解并掌握三范式的概念,能帮助开发人员在设计数据库时做出更加高效且一致的决策,提升数据库系统的整体性能和可维护性。
+
diff --git a/docs/data-management/MySQL/readMySQL.md b/docs/data-management/MySQL/readMySQL.md
index 1939b1d318..c8f198ff9f 100644
--- a/docs/data-management/MySQL/readMySQL.md
+++ b/docs/data-management/MySQL/readMySQL.md
@@ -23,7 +23,7 @@ MySQL是个啥,就说一句话——**MySQL是一个关系型数据库管理
-
+
diff --git a/docs/data-management/MySQL/MySQL-count.md b/docs/data-management/MySQL/reproduce/MySQL-count.md
similarity index 100%
rename from docs/data-management/MySQL/MySQL-count.md
rename to docs/data-management/MySQL/reproduce/MySQL-count.md
diff --git "a/docs/data-management/MySQL/\346\225\260\346\215\256\345\272\223\344\270\211\350\214\203\345\274\217.md" "b/docs/data-management/MySQL/\346\225\260\346\215\256\345\272\223\344\270\211\350\214\203\345\274\217.md"
deleted file mode 100644
index e8f391d2a9..0000000000
--- "a/docs/data-management/MySQL/\346\225\260\346\215\256\345\272\223\344\270\211\350\214\203\345\274\217.md"
+++ /dev/null
@@ -1,96 +0,0 @@
-# 数据库三范式
-
-## mysql 数据库的设计三范式
-
-1NF:字段不可分;
-
-2NF:有主键,非主键字段依赖主键;
-
-3NF:非主键字段不能相互依赖;
-
-解释:
-
-1NF:原子性 字段不可再分,否则就不是关系数据库;
-
-2NF:唯一性 一个表只说明一个事物;
-
-3NF:每列都与主键有直接关系,不存在传递依赖;
-
-#### 第一范式(1NF)
-
-即表的列的具有原子性,不可再分解,即列的信息,不能分解, 只要数据库是关系型数据库(mysql/oracle/db2/informix/sysbase/sql server),就自动的满足1NF。数据库表的每一列都是不可分割的原子数据项,而不能是集合,数组,记录等非原子数据项。如果实体中的某个属性有多个值时,必须拆分为不同的属性 。通俗理解即一个字段只存储一项信息。
-
-
-
-关系型数据库: mysql/oracle/db2/informix/sysbase/sql server 非关系型数据库: (特点: 面向对象或者集合) NoSql数据库: MongoDB/redis(特点是面向文档)
-
-#### 第二范式(2NF)
-
-第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要我们设计一个主键来实现(这里的主键不包含业务逻辑)。
-
-即满足第一范式前提,当存在多个主键的时候,才会发生不符合第二范式的情况。比如有两个主键,不能存在这样的属性,它只依赖于其中一个主键,这就是不符合第二范式。通俗理解是任意一个字段都只依赖表中的同一个字段。(涉及到表的拆分)
-
-看下面的学生选课表:
-
-| 学号 | 课程 | 成绩 | 课程学分 |
-| ----- | ---- | ---- | -------- |
-| 10001 | 数学 | 100 | 6 |
-| 10001 | 语文 | 90 | 2 |
-| 10001 | 英语 | 85 | 3 |
-| 10002 | 数学 | 90 | 6 |
-| 10003 | 数学 | 99 | 6 |
-| 10004 | 语文 | 89 | 2 |
-
-表中主键为 (学号,课程),我们可以表示为 (学号,课程) -> (成绩,课程学分), 表示所有非主键列 (成绩,课程学分)都依赖于主键 (学号,课程)。 但是,表中还存在另外一个依赖:(课程)->(课程学分)。这样非主键列 ‘课程学分‘ 依赖于部分主键列 ’课程‘, 所以上表是不满足第二范式的。
-
-我们把它拆成如下2张表:
-
-
-
-学生选课表:
-
-| 学号 | 课程 | 成绩 |
-| ----- | ---- | ---- |
-| 10001 | 数学 | 100 |
-| 10001 | 语文 | 90 |
-| 10001 | 英语 | 85 |
-| 10002 | 数学 | 90 |
-| 10003 | 数学 | 99 |
-| 10004 | 语文 | 89 |
-
-课程信息表:
-
-| 课程 | 课程学分 |
-| ---- | -------- |
-| 数学 | 6 |
-| 语文 | 3 |
-| 英语 | 2 |
-
-那么上面2个表,学生选课表主键为(学号,课程),课程信息表主键为(课程),表中所有非主键列都完全依赖主键。不仅符合第二范式,还符合第三范式。
-
-
-
-再看这样一个学生信息表:
-
-| 学号 | 姓名 | 性别 | 班级 | 班主任 |
-| ----- | ------ | ---- | ---- | ------ |
-| 10001 | 张三 | 男 | 一班 | 小王 |
-| 10002 | 李四 | 男 | 一班 | 小王 |
-| 10003 | 王五 | 男 | 二班 | 小李 |
-| 10004 | 张小三 | 男 | 二班 | 小李 |
-
-上表中,主键为:(学号),所有字段 (姓名,性别,班级,班主任)都依赖与主键(学号),不存在对主键的部分依赖。所以是满足第二范式。
-
-
-
-#### 第三范式(3NF)
-
-满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主键字段。就是说,表的信息,如果能够被推导出来,就不应该单独的设计一个字段来存放(能尽量外键join就用外键join)。很多时候,我们为了满足第三范式往往会把一张表分成多张表。
-
-即满足第二范式前提,如果某一属性依赖于其他非主键属性,而其他非主键属性又依赖于主键,那么这个属性就是间接依赖于主键,这被称作传递依赖于主属性。 通俗解释就是一张表最多只存两层同类型信息。
-
-
-
-反三范式
-
-没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,提高读性能,就必须降低范式标准,适当保留冗余数据。具体做法是: 在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,减少了查询时的关联,提高查询效率,因为在数据库的操作中查询的比例要远远大于DML的比例。但是反范式化一定要适度,并且在原本已满足三范式的基础上再做调整的。
\ No newline at end of file
diff --git a/docs/data-management/Redis/.DS_Store b/docs/data-management/Redis/.DS_Store
index 859c02b40f..45706ae4e1 100644
Binary files a/docs/data-management/Redis/.DS_Store and b/docs/data-management/Redis/.DS_Store differ
diff --git a/docs/data-management/Redis/Nosql-Overview.md b/docs/data-management/Redis/Nosql-Overview.md
index a80275eaa5..4e5c48ff95 100644
--- a/docs/data-management/Redis/Nosql-Overview.md
+++ b/docs/data-management/Redis/Nosql-Overview.md
@@ -1,5 +1,3 @@
-
-
# NoSQL的前世今生
### 啥玩意:
@@ -12,7 +10,7 @@ NoSQL(NoSQL = Not Only SQL ),“不仅仅是SQL”,泛指**非关系型的
#### 1. 单机MySQL的美好年代
- 在以前,一个网站的访问量一般都不大,用单个数据库完全可以轻松应付。在那个时候,更多的都是静态网页,动态交互类型的网站不多。上述架构下,我们来看看数据存储的瓶颈是什么?
+在以前,一个网站的访问量一般都不大,用单个数据库完全可以轻松应付。在那个时候,更多的都是静态网页,动态交互类型的网站不多。上述架构下,我们来看看数据存储的瓶颈是什么?
- 数据量的总大小 一个机器放不下时
- 数据的索引(B+ Tree)一个机器的内存放不下时
@@ -20,27 +18,27 @@ NoSQL(NoSQL = Not Only SQL ),“不仅仅是SQL”,泛指**非关系型的
#### 2. Memcached(缓存)+MySQL+垂直拆分
- 后来,随着访问量的上升,几乎大部分使用MySQL架构的网站在数据库上都开始出现了性能问题,web程序不再仅仅专注在功能上,同时也在追求性能。程序员们开始大量的使用**缓存技术**来缓解数据库的压力,优化数据库的结构和索引。开始比较流行的是通过**文件缓存**来缓解数据库压力,但是当访问量继续增大的时候,多台web机器通过文件缓存不能共享,大量的小文件缓存也带了了比较高的IO压力。在这个时候,Memcached就自然的成为一个非常时尚的技术产品。
+后来,随着访问量的上升,几乎大部分使用MySQL架构的网站在数据库上都开始出现了性能问题,web程序不再仅仅专注在功能上,同时也在追求性能。程序员们开始大量的使用**缓存技术**来缓解数据库的压力,优化数据库的结构和索引。开始比较流行的是通过**文件缓存**来缓解数据库压力,但是当访问量继续增大的时候,多台web机器通过文件缓存不能共享,大量的小文件缓存也带了了比较高的IO压力。在这个时候,Memcached就自然的成为一个非常时尚的技术产品。
- Memcached作为一个**独立的分布式的缓存服务器**,为多个web服务器提供了一个共享的高性能缓存服务,在Memcached服务器上,又发展了根据hash算法来进行多台Memcached缓存服务的扩展,然后又出现了一致性hash来解决增加或减少缓存服务器导致重新hash带来的大量缓存失效的弊端
+Memcached作为一个**独立的分布式的缓存服务器**,为多个web服务器提供了一个共享的高性能缓存服务,在Memcached服务器上,又发展了根据hash算法来进行多台Memcached缓存服务的扩展,然后又出现了一致性hash来解决增加或减少缓存服务器导致重新hash带来的大量缓存失效的弊端
#### 3. Mysql主从读写分离
- 由于数据库的写入压力增加,Memcached只能缓解数据库的读取压力。读写集中在一个数据库上让数据库不堪重负,大部分网站开始**使用主从复制技术来达到读写分离,以提高读写性能和读库的可扩展性**。**Mysql的master-slave模式**成为这个时候的网站标配了。
+由于数据库的写入压力增加,Memcached只能缓解数据库的读取压力。读写集中在一个数据库上让数据库不堪重负,大部分网站开始**使用主从复制技术来达到读写分离,以提高读写性能和读库的可扩展性**。**Mysql的master-slave模式**成为这个时候的网站标配了。
#### 4. 分表分库+水平拆分+mysql集群
- 在Memcached的高速缓存,MySQL的主从复制,读写分离的基础之上,这时MySQL主库的写压力开始出现瓶颈,而数据量的持续猛增,由于**MyISAM**使用**表锁**,在高并发下会出现严重的锁问题,大量的高并发MySQL应用开始使用**InnoDB**引擎代替MyISAM。
+在Memcached的高速缓存,MySQL的主从复制,读写分离的基础之上,这时MySQL主库的写压力开始出现瓶颈,而数据量的持续猛增,由于**MyISAM**使用**表锁**,在高并发下会出现严重的锁问题,大量的高并发MySQL应用开始使用**InnoDB**引擎代替MyISAM。
- 同时,开始流行**使用分表分库来缓解写压力和数据增长的扩展问题**。这个时候,分表分库成了一个热门技术,是面试的热门问题也是业界讨论的热门技术问题。也就在这个时候,MySQL推出了还不太稳定的表分区,这也给技术实力一般的公司带来了希望。虽然MySQL推出了MySQL Cluster集群,但性能也不能很好满足互联网的要求,只是在高可靠性上提供了非常大的保证。
+同时,开始流行**使用分表分库来缓解写压力和数据增长的扩展问题**。这个时候,分表分库成了一个热门技术,是面试的热门问题也是业界讨论的热门技术问题。也就在这个时候,MySQL推出了还不太稳定的表分区,这也给技术实力一般的公司带来了希望。虽然MySQL推出了MySQL Cluster集群,但性能也不能很好满足互联网的要求,只是在高可靠性上提供了非常大的保证。
#### 5. MySQL的扩展性瓶颈
- MySQL数据库也经常存储一些大文本字段,导致数据库表非常的大,在做数据库恢复的时候就导致非常的慢,不容易快速恢复数据库。比如1000万4KB大小的文本就接近40GB的大小,如果能把这些数据从MySQL省去,MySQL将变得非常的小。关系数据库很强大,但是它并不能很好的应付所有的应用场景。MySQL的扩展性差(需要复杂的技术来实现),大数据下IO压力大,表结构更改困难,正是当前使用MySQL的开发人员面临的问题。
+MySQL数据库也经常存储一些大文本字段,导致数据库表非常的大,在做数据库恢复的时候就导致非常的慢,不容易快速恢复数据库。比如1000万4KB大小的文本就接近40GB的大小,如果能把这些数据从MySQL省去,MySQL将变得非常的小。关系数据库很强大,但是它并不能很好的应付所有的应用场景。MySQL的扩展性差(需要复杂的技术来实现),大数据下IO压力大,表结构更改困难,正是当前使用MySQL的开发人员面临的问题。
#### 6. 为什么用NoSQL
- 今天我们可以通过第三方平台(如:Google,Facebook等)可以很容易的**访问和抓取数据**(爬虫私密信息有风险哈)。用户的个人信息,社交网络,地理位置,用户生成的数据和用户操作日志已经成倍的增加。我们如果要对这些用户数据进行挖掘,那SQL数据库已经不适合这些应用了, NoSQL数据库的发展也不能很好的处理这些大的数据。
+今天我们可以通过第三方平台(如:Google,Facebook等)可以很容易的**访问和抓取数据**(爬虫私密信息有风险哈)。用户的个人信息,社交网络,地理位置,用户生成的数据和用户操作日志已经成倍的增加。我们如果要对这些用户数据进行挖掘,那SQL数据库已经不适合这些应用了, NoSQL数据库的发展也不能很好的处理这些大的数据。

diff --git a/docs/data-management/Redis/ReadRedis.md b/docs/data-management/Redis/ReadRedis.md
index 530b5e53a0..2ecbb031b8 100644
--- a/docs/data-management/Redis/ReadRedis.md
+++ b/docs/data-management/Redis/ReadRedis.md
@@ -1,4 +1,4 @@
-
+
@@ -18,99 +18,64 @@
Redis: **REmote DIctionary Server**(远程字典服务器)。
-Redis 是一个全开源免费(BSD许可)的,内存中的数据结构存储系统,它可以用作**数据库、缓存和消息中间件**。一般作为一个高性能的(key/value)分布式内存数据库,基于**内存**运行并支持持久化的 NoSQL 数据库,是当前最热门的 NoSql 数据库之一,也被人们称为**数据结构服务器**
+Redis 是一个全开源免费(BSD许可)的,使用 C 语言编写,内存中的数据结构存储系统,它可以用作**数据库、缓存和消息中间件**。一般作为一个高性能的(key/value)分布式内存数据库,基于**内存**运行并支持持久化的 NoSQL 数据库,是当前最热门的 NoSql 数据库之一,也被人们称为**数据结构服务器**
+它支持多种数据结构,如字符串(Strings)、哈希(Hashes)、列表(Lists)、集合(Sets)、有序集合(Sorted Sets)、位图(bitmaps)、HyperLogLogs 和地理空间索引(geospatial indexes),并带有半持久化存储的选项。
+### 主要特点
-## Redis 介绍
+1. **高性能**:Redis 的读写速度非常快,支持每秒百万级的请求处理能力。其所有数据都存储在内存中,保证了极高的读写性能。
+2. **多种数据结构**:Redis 不仅支持简单的键值对,还支持多种高级数据结构,如列表、集合、有序集合、哈希等,可以满足复杂的数据存储需求。
+3. **持久化**:Redis 支持多种持久化机制,如 RDB(快照)和 AOF(追加文件),可以将内存中的数据持久化到磁盘,保证数据的持久性。
+4. **高可用性**:通过 Redis 的复制(Replication)、Sentinel 和 Cluster 特性,可以实现高可用性和自动故障转移。复制机制允许数据从主节点复制到多个从节点,从而提高数据的冗余性和读取的可扩展性。
+5. **Lua脚本**:Redis 内置 Lua 脚本引擎,支持在服务器端运行复杂的脚本,减少了网络往返次数,提高了操作的原子性。
+6. **事务支持**:Redis 支持事务,通过 MULTI、EXEC、DISCARD 和 WATCH 等命令实现事务操作。
+7. **发布/订阅**:Redis 提供了发布/订阅功能,可以实现消息通知和实时消息传递。
+8. **丰富的生态系统**:Redis 拥有丰富的客户端库,支持多种编程语言,包括 C、C++、Java、Python、Go、Node.js等。
-Redis 是一个开源的、使用 C 语言编写的、支持网络交互的、可基于内存也可持久化的 Key-Value 数据库。
+### 应用场景
-Redis 是一个 key-value 存储系统。和 Memcached 类似,它支持存储的 value 类型相对更多,包括**string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和 hash(哈希类型)**。这些数据类型都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis 支持各种不同方式的排序。与 memcached 一样,为了保证效率,数据都是缓存在内存中。区别的是 redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了 master-slave(主从)同步,所以 Redis 也可以被看成是一个数据结构服务器。
+1. **缓存**:Redis 作为缓存系统,可以极大地提高数据读取速度,减轻数据库的压力。
+2. **会话存储**:利用 Redis 的高性能和持久化特性,可以用于存储用户会话信息。
+3. **实时分析**:利用 Redis 的集合和有序集合,可以进行实时数据分析和排名。
+4. **消息队列**:利用 Redis 的列表和发布/订阅特性,可以实现简单的消息队列系统。
+5. **计数器和限流**:利用 Redis 的原子递增操作,可以实现高效的计数器和限流机制。
-
-
-Redis 支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得 Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。
+> **具体以某一论坛为例:**
+>
+> - 记录帖子的点赞数、评论数和点击数 (hash)。
+> - 记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。
+> - 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。
+> - 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。
+> - 缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。
+> - 记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。
+> - 如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。
+> - 收藏集和帖子之间的关系 (zset)。
+> - 记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。
+> - 缓存用户行为历史,进行恶意行为过滤 (zset,hash)。
Redis 的官网地址,非常好记,是 redis.io。(域名后缀io属于国家域名,是 british Indian Ocean territory,即英属印度洋领地)目前,Vmware 在资助着 Redis 项目的开发和维护。
-Redis 的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上(这称为“半持久化模式”);也可以把每一次数据变化都写入到一个 append only file(aof)里面(这称为“全持久化模式”)。这就是 redis 提供的两种持久化的方式,RDB(Redis DataBase)和 AOF(Append Only File)。
-
-
-
-## Redis 特点
-
-Redis 是一个开源,先进的 key-value 存储,并用于构建高性能,可扩展的Web应用程序的完美解决方案。
-
-Redis从它的许多竞争继承来的三个主要特点:
-
-- Redis数据库完全在内存中,使用磁盘仅用于持久性。
-
-- 相比许多键值数据存储,Redis拥有一套较为丰富的数据类型。
-
-- Redis可以将数据复制到任意数量的从服务器。
-
-
-
-## Redis 优势
-
-- 异常快速:Redis 的速度非常快,每秒能执行约 11万集合,每秒约 81000+条记录。SET 操作每秒钟 110000 次,GET 操作每秒钟 81000 次,网站一般使用 Redis 作为**缓存服务器**。
-
-- 支持**丰富的数据类型**:Redis 支持大多数开发人员已经知道像列表,集合,有序集合,散列数据类型。这使得它非常容易解决各种各样的问题,因为我们知道哪些问题是可以处理通过它的数据类型更好。
-
-- 操作都是**原子性**:所有 Redis 操作是原子的,这保证了如果两个客户端同时访问的 Redis 服务器将获得更新后的值。
-
-- MultiUtility 工具:Redis 是一个多功能实用工具,可以在很多如:缓存,消息传递队列中使用(Redis 原生支持发布/订阅),在应用程序中,如:Web应用程序会话,网站页面点击数等任何短暂的数据;
-
-
-
-#### Redis 使用场景
-
-- 取最新 N 个数据的操作
-- 排行榜应用,取 TOP N 操作
-- 需要精确设定过期时间的应用
-- 定时器、计数器应用
-- Uniq 操作,获取某段时间所有数据排重值
-- 实时系统,反垃圾系统
-- Pub/Sub 构建实时消息系统
-- 构建队列系统
-- 缓存
-
-
-
-**具体以某一论坛为例:**
-
-- 记录帖子的点赞数、评论数和点击数 (hash)。
-- 记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。
-- 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。
-- 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。
-- 缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。
-- 记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。
-- 如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。
-- 收藏集和帖子之间的关系 (zset)。
-- 记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。
-- 缓存用户行为历史,进行恶意行为过滤 (zset,hash)。
-
**安装**
-```
+```shell
$ wget http://download.redis.io/releases/redis-5.0.6.tar.gz
$ tar xzf redis-5.0.6.tar.gz
$ cd redis-5.0.6
$ make
```
-新版本的编译文件在src中(之前在bin目录),启动server
+新版本的编译文件在 src 中(之前在bin目录),启动 server
-```
+```sh
$ src/redis-server
```
启动客户端
-```
+```shell
$ src/redis-cli
redis> set foo bar
OK
@@ -122,7 +87,7 @@ redis> get foo
## Redis 知识全景
-
+
“两大维度”就是指系统维度和应用维度,“三大主线”也就是指高性能、高可靠和高可扩展(可以简称为“三高”)。
@@ -138,6 +103,8 @@ Redis 作为庞大的键值数据库,可以说遍地都是知识,一抓一
+
+
## 推荐阅读
[《我是如何学习Redis的?高效学习Redis的路径和方法分享》](http://kaito-kidd.com/2020/09/09/how-i-learned-redis/)
\ No newline at end of file
diff --git a/docs/data-management/Redis/Redis-Cache-Model.md b/docs/data-management/Redis/Redis-Cache-Model.md
new file mode 100644
index 0000000000..9cb60433a4
--- /dev/null
+++ b/docs/data-management/Redis/Redis-Cache-Model.md
@@ -0,0 +1,191 @@
+### **什么是缓存模式?**
+
+缓存模式是指在系统中如何设计和实现缓存机制,用以快速存取数据并减轻后端数据存储的负载。Redis 提供了灵活多样的缓存策略,常见模式包括:
+
+1. **直写模式(Write Through)**
+2. **回写模式(Write Back)**
+3. **旁路缓存模式(Cache Aside)**
+4. **只缓存模式(Read Through)**
+
+通过合理选择和优化这些模式,可以满足不同业务场景对性能、数据一致性和可用性的需求。
+
+------
+
+### **Redis 缓存模式详解**
+
+#### **1. Cache Aside(旁路缓存)**
+
+Cache Aside 模式又称为 **Lazy Loading**,是使用最广泛的缓存模式之一,通常由业务代码显式管理缓存和数据库,核心思想是:
+
+- 数据从数据库加载到缓存中,缓存作为数据库的一个“旁路”。
+- 应用程序负责读取缓存,缓存未命中时再从数据库读取并更新缓存。
+
+**读请求**:
+
+- 先从缓存中读取数据;
+- 如果缓存中没有数据(缓存未命中),从数据库中获取数据,将数据写入缓存,返回给客户端。
+
+**写请求**:
+
+- 数据写入数据库后,将缓存中的数据清除或更新。
+
+**适用场景:**
+
+- 数据更新较少但读取频率较高的场景,例如商品详情、热搜榜单。
+- 对数据一致性要求不严格的系统。
+
+**优缺点:**
+
+- **优点**:简单易用,缓存与数据库解耦。
+- **缺点**:缓存预热需要时间,容易出现缓存击穿问题。
+
+
+
+#### 2. Write Through(直写模式)
+
+在直写模式中,应用程序的所有写操作都会同时更新缓存和数据库:
+
+- 数据写入数据库的同时同步写入缓存。
+
+**工作流程:**
+
+1. 应用将数据同时写入 Redis 和数据库。
+2. 读取时直接从 Redis 获取数据。
+
+**适用场景:**
+
+- 需要缓存和数据库一致性非常高的场景,例如账户余额、订单状态等敏感数据。
+
+**优缺点:**
+
+- **优点**:一致性高,数据实时同步。
+- **缺点**:写入速度较慢,因为每次写操作都需要更新两处存储。
+
+
+
+#### 3. Write Back(回写模式)
+
+在回写模式下,数据首先写入缓存,之后异步写入数据库:
+
+- 写入数据库的操作由后台线程或任务队列完成。
+
+**工作流程:**
+
+1. 数据先写入 Redis。
+2. Redis 异步将数据批量写入数据库。
+
+**适用场景:**
+
+- 写频率高、读频率较低且对一致性要求不严格的场景,例如日志系统。
+
+**优缺点:**
+
+- **优点**:写入性能高,因为写数据库是异步的。
+- **缺点**:可能导致数据丢失,如果缓存写入后还未同步到数据库时发生故障。
+
+
+
+#### 4. Read Through(只缓存模式)
+
+在这种模式中,所有的读写操作都必须通过缓存完成:
+
+- 缓存未命中时,应用从数据库中加载数据并自动更新缓存。
+
+**工作流程:**
+
+1. 应用读取 Redis,如果未命中,自动从数据库加载并更新。
+2. 写入时同步更新缓存和数据库。
+
+**适用场景:**
+
+- 读多写少且对实时性要求较高的场景。
+
+**优缺点:**
+
+- **优点**:应用层逻辑简单。
+- **缺点**:依赖于缓存层,缓存崩溃可能导致大量数据库请求。
+
+
+
+#### 5、Refresh Ahead(提前刷新缓存)
+
+Refresh Ahead 模式通过提前异步加载数据,防止缓存失效时查询数据库的性能抖动。
+
+- 基于预设的过期时间,在缓存即将失效前,后台异步加载数据并更新缓存。
+
+
+
+#### 6、Singleflight 模式
+
+Singleflight 是一种**抑制重复请求**的模式,用于解决缓存未命中时的高并发问题。
+
+- 当多个请求同时查询相同的缓存未命中数据时,只有一个请求会执行数据库查询,其余请求等待结果返回。
+
+**核心思路**
+
+- 当多个并发请求同时访问**同一个缓存键**,导致缓存未命中时,**Singleflight** 机制保证只有**一个请求**去执行实际的数据库查询或计算操作,其余请求会等待第一个请求的结果完成后直接返回相同的结果。
+- 这样可以有效避免重复查询或计算,减少对数据库、API 等资源的压力。
+
+**流程**
+
+1. 多个请求到来时,检测是否已有请求正在执行。
+ - 如果没有请求在执行,则当前请求负责查询数据。
+ - 如果已有请求在执行,其他请求进入等待队列。
+2. **第一个请求执行数据库查询或计算操作,并保存结果。**
+3. **等待中的请求获取到第一个请求的结果,直接返回相同数据。**
+
+
+
+### Redis 缓存模式的优化策略
+
+#### **1. 缓存雪崩**
+
+缓存雪崩指缓存集中失效时,大量请求直接击穿数据库,导致数据库压力激增甚至崩溃。
+
+**解决方案:**
+
+- **设置随机过期时间**:避免大量缓存同时失效。
+- **多级缓存**:在 Redis 之上增加本地缓存(如 Guava)。
+- **请求限流**:通过限流机制控制瞬时流量。
+
+#### **2. 缓存击穿**
+
+缓存击穿指某个热点数据缓存失效后,短时间内大量请求直接打到数据库。
+
+**解决方案:**
+
+- **热点数据预加载**:提前将热点数据缓存。
+- **加互斥锁**:在缓存未命中时,通过锁机制防止数据库过载。
+
+#### 3. 缓存穿透
+
+缓存穿透指用户请求的数据既不在缓存中,也不存在于数据库中,导致所有请求打到数据库。
+
+**解决方案:**
+
+- **布隆过滤器**:拦截无效请求,减少数据库查询。
+- **缓存空结果**:将不存在的数据写入缓存,避免重复查询。
+
+
+
+### Redis 缓存模式的应用场景
+
+#### **1. 电商秒杀**
+
+在高并发的秒杀场景中,Redis 通常用于缓存商品库存数据。典型模式为:
+
+- 使用 `Cache Aside` 模式缓存库存数据,避免频繁访问数据库。
+- 配合分布式锁,防止超卖问题。
+
+#### **2. 社交网络**
+
+在社交平台上,Redis 用于存储用户会话、好友关系等数据:
+
+- 使用 `Write Through` 模式保证数据一致性。
+- 通过 Redis 的 Set 结构实现快速去重和交集运算。
+
+#### **3. 实时排行榜**
+
+Redis 的 Sorted Set 结构非常适合实现排行榜功能:
+
+- 使用 `Cache Aside` 模式,定期将缓存中的排行榜数据同步到数据库。
\ No newline at end of file
diff --git a/docs/data-management/Redis/Redis-Cluster.md b/docs/data-management/Redis/Redis-Cluster.md
index 9cb3be1a63..856af90b08 100644
--- a/docs/data-management/Redis/Redis-Cluster.md
+++ b/docs/data-management/Redis/Redis-Cluster.md
@@ -6,10 +6,6 @@ tags:
categories: Redis
---
-
-
-
-
## 一、Redis 集群是啥
我们先回顾下前边介绍的几种 Redis 高可用方案:持久化、主从同步和哨兵机制。但这些方案仍有痛点,其中最主要的问题就是存储能力受单机限制,以及没办法实现写操作的负载均衡。
@@ -34,7 +30,7 @@ Redis 集群刚好解决了上述问题,实现了较为完善的高可用方
2. **高可用**: 集群支持主从复制和主节点的 **自动故障转移** *(与哨兵类似)*,当任一节点发生故障时,集群仍然可以对外提供服务。
-
+
上图展示了 **Redis Cluster** 典型的架构图,集群中的每一个 Redis 节点都 **互相两两相连**,客户端任意 **直连** 到集群中的 **任意一台**,就可以对其他 Redis 节点进行 **读写** 的操作。
@@ -225,7 +221,7 @@ Redis 集群相对单机在功能上存在一些限制,需要开发人员提
### 3.3 节点通信
-集群的建立离不开节点之间的通信,例如我们上面启动六个集群节点之后通过 `redis-cli` 命令帮助我们搭建起来了集群,实际上背后每个集群之间的两两连接是通过了 `CLUSTER MEET ` 命令发送 `MEET` 消息完成的。
+集群的建立离不开节点之间的通信,例如我们上面启动六个集群节点之后通过 `redis-cli` 命令帮助我们搭建起来了集群,实际上背后每个集群之间的两两连接是通过了 `CLUSTER MEET` 命令发送 `MEET` 消息完成的。
通信过程说明:
@@ -258,9 +254,7 @@ Redis 集群相对单机在功能上存在一些限制,需要开发人员提
(为什么需要随机呢? )
- Gossip 协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。
-
-Gossip 协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的 Gossip 消息,了解这些消息有助于我们理解集群如何完成信息交换。
+ Gossip 协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似**流言传播**。Gossip 协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的 Gossip 消息,了解这些消息有助于我们理解集群如何完成信息交换。
#### 消息类型
@@ -489,7 +483,7 @@ Redis 集群自身实现了高可用。高可用首先需要解决集群部分
- 更新配置纪元
- 配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元 (`clusterNode.configEpoch`)标示当前主节点的版本,所有主节点的配置纪元 都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配 置纪元(`clusterState.current Epoch`),用于记录集群内所有主节点配置纪元的最大版本。
+ 配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元 (`clusterNode.configEpoch`)标示当前主节点的版本,所有主节点的配置纪元 都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配置纪元(`clusterState.current Epoch`),用于记录集群内所有主节点配置纪元的最大版本。
- 广播选举消息
@@ -503,7 +497,7 @@ Redis 集群自身实现了高可用。高可用首先需要解决集群部分
当从节点收集到 N/2+1 个持有槽的主节点投票时,从节点可以执行替换主节点操作,例如集群内有 5 个持有槽的主节点,主节点 b 故障后还有 4 个, 当其中一个从节点收集到 3 张投票时代表获得了足够的选票可以进行替换主节点操作。
- 
+
5. 替换主节点
@@ -544,7 +538,7 @@ Redis 集群自身实现了高可用。高可用首先需要解决集群部分
像 redis-cli 这种客户端又叫 Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到 Redis 上进行重定向才能找到要执行命令的节点,额外增加了 IO 开销, 这不是Redis 集群高效的使用方式。正因为如此通常集群客户端都采用另一 种实现:Smart(智能)客户端。
-#### 3.5.2 Smart客户端
+#### 3.5.2 Smart 客户端
大多数开发语言的 Redis 客户端都采用 Smart 客户端支持集群协议。Smart 客户端通过在内部维护 slot→node 的映射关系,本地就可实现键到节点的查找,从而保证 IO 效率的最大化,而 MOVED 重定向负责协助 Smart 客户端更新 slot→node 映射。
@@ -580,7 +574,8 @@ Redis 集群自身实现了高可用。高可用首先需要解决集群部分
### 参考与来源
-1. https://redis.io/topics/cluster-tutorial
-2. 《Redis 设计与实现》
-3. 《Redis 开发与运维》
-4. https://www.cnblogs.com/kismetv/p/9853040.html
\ No newline at end of file
+1. https://www.mybluelinux.com/redis-explained/
+2. https://redis.io/topics/cluster-tutorial
+3. 《Redis 设计与实现》
+4. 《Redis 开发与运维》
+5. https://www.cnblogs.com/kismetv/p/9853040.html
\ No newline at end of file
diff --git a/docs/data-management/Redis/Redis-Database.md b/docs/data-management/Redis/Redis-Database.md
index a0464d8c5a..9c71d14892 100644
--- a/docs/data-management/Redis/Redis-Database.md
+++ b/docs/data-management/Redis/Redis-Database.md
@@ -2,7 +2,7 @@
Redis 如何表示一个数据库?数据库操作是如何实现的?
-> 这边文章是基于源码来让我们理解 Redis 的,不管是我们自己下载 redis 还是直接在 Github 上看源码,我们先要了解下 redis 更目录下的重要目录
+> 这篇文章是基于源码来让我们理解 Redis 的,不管是我们自己下载 redis 还是直接在 Github 上看源码,我们先要了解下 redis 根目录下的重要目录
>
> - `src`:用C编写的Redis实现
> - `tests`:包含在Tcl中实现的单元测试
@@ -16,8 +16,6 @@ Redis 如何表示一个数据库?数据库操作是如何实现的?
理解程序如何工作的最简单方法是理解它使用的数据结构。 从 `redis/src` 目录下可以看到 server 的源码文件(基于 `redis-6.0.5`,redis3.0 叫 `redis.c` 和 `redis.h`)。
-
-
Redis的主头文件 `server.h` 中定义了各种结构体,比如Redis 对象`redisObject` 、存储结构`redisDb `、客户端`client` 等等。
```c
@@ -116,5 +114,4 @@ Redis 解决哈希碰撞的方式 和 Java 中的 HashMap 类似,采取链表
-
-https://redisbook.readthedocs.io/en/latest/index.html
\ No newline at end of file
+> https://redisbook.readthedocs.io/en/latest/index.html
diff --git a/docs/data-management/Redis/Redis-Datatype.md b/docs/data-management/Redis/Redis-Datatype.md
index 0bb67656da..fc8ff7ad04 100644
--- a/docs/data-management/Redis/Redis-Datatype.md
+++ b/docs/data-management/Redis/Redis-Datatype.md
@@ -1,4 +1,10 @@
-# Redis 数据类型篇
+---
+title: Redis 数据类型篇
+date: 2022-08-25
+tags:
+ - Redis
+categories: Redis
+---
> 一提到 Redis,我们的脑子里马上就会出现一个词:“快。”
>
@@ -10,7 +16,7 @@
我们都知道 Redis 是个 KV 数据库,那 KV 结构的数据在 Redis 中是如何存储的呢?
-### 一、KV 如何存储?
+## 一、KV 如何存储?
为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。类似我们的 HashMap
@@ -20,10 +26,20 @@
在下图中,可以看到,哈希桶中的 entry 元素中保存了 `*key` 和 `*value` 指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过 `*value` 指针被查找到。
-
+
因为这个哈希表保存了所有的键值对,所以,也把它称为**全局哈希表**。哈希表的最大好处很明显,就是让我们可以用 $O(1)$ 的时间复杂度来快速查找到键值对——我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。
+```c
+struct redisObject {
+ unsigned type:4; // 类型
+ unsigned encoding:4; // 编码
+ unsigned lru:LRU_BITS; // 对象最后一次被访问的时间
+ int refcount; //引用计数
+ void *ptr; //指向实际值的指针
+};
+```
+
你看,这个查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。也就是说,不管哈希表里有 10 万个键还是 100 万个键,我们只需要一次计算就能找到相应的键。但是,如果你只是了解了哈希表的 $O(1)$ 复杂度和快速查找特性,那么,当你往 Redis 中写入大量数据后,就可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在的风险点,那就是哈希表的冲突问题和 rehash 可能带来的操作阻塞。
### 为什么哈希表操作变慢了?
@@ -32,15 +48,32 @@
Redis 解决哈希冲突的方式,就是链式哈希。和 JDK7 中的 HahsMap 类似,链式哈希也很容易理解,**就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接**。
-如下图所示:哈希桶 6 上就有 3 个连着的 entry,也叫作哈希冲突链。
-
-
+
但是,这里依然存在一个问题,哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。对于追求“快”的 Redis 来说,这是不太能接受的。
所以,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。那具体怎么做呢?
-其实,为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
+
+
+其实,为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。
+
+```c
+struct dict {
+ dictType *type;
+
+ dictEntry **ht_table[2];
+ unsigned long ht_used[2];
+
+ long rehashidx; /* rehashing not in progress if rehashidx == -1 */
+
+ /* Keep small vars at end for optimal (minimal) struct padding */
+ int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
+ signed char ht_size_exp[2]; /* exponent of size. (size = 1<链式哈希。和 JDK7
到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。为了避免这个问题,Redis 采用了渐进式 rehash。
-简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:
-
-
+简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。
渐进式 rehash 这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
@@ -62,31 +93,29 @@ Redis 解决哈希冲突的方式,就是链式哈希。和 JDK7
## 一、Redis 的五种基本数据类型和其数据结构
-由于 Redis 是基于标准 C 写的,只有最基础的数据类型,因此 Redis 为了满足对外使用的 5 种基本数据类型,开发了属于自己**独有的一套基础数据结构**,使用这些数据结构来实现 5 种数据类型。
+
+
+由于 Redis 是基于标准 C 写的,只有最基础的数据类型,因此 Redis 为了满足对外使用的 5 种基本数据类型,开发了属于自己**独有的一套基础数据结构**。
**Redis** 有 5 种基础数据类型,它们分别是:**string(字符串)**、**list(列表)**、**hash(字典)**、**set(集合)** 和 **zset(有序集合)**。
-Redis 底层的数据结构包括:**简单动态数组SDS、链表、字典、跳跃链表、整数集合、压缩列表、对象。**
+Redis 底层的数据结构包括:**简单动态数组SDS、链表、字典、跳跃链表、整数集合、快速列表、压缩列表、对象。**
Redis 为了平衡空间和时间效率,针对 value 的具体类型在底层会采用不同的数据结构来实现,其中哈希表和压缩列表是复用比较多的数据结构,如下图展示了对外数据类型和底层数据结构之间的映射关系:
-
+
下面我们具体看下各种数据类型的底层实现和操作。
> 安装好 Redis,我们可以使用 `redis-cli` 来对 Redis 进行命令行的操作,当然 Redis 官方也提供了在线的调试器,你也可以在里面敲入命令进行操作:http://try.redis.io/#run
-### 1、String(字符串)
-
-String 是 Redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
-String 类型是二进制安全的。意思是 Redis 的 String 可以包含任何数据。比如 jpg 图片或者序列化的对象 。
-Redis 的字符串是动态字符串,是可以修改的字符串,**内部结构实现上类似于 Java 的 ArrayList**,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
-
-
+### 1、String(字符串)
+String 类型是二进制安全的。意思是 Redis 的 String 可以包含任何数据。比如 jpg 图片或者序列化的对象 。
+Redis 的字符串是动态字符串,是可以修改的字符串,**内部结构实现上类似于 Java 的 ArrayList**,采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。
@@ -94,7 +123,7 @@ Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾
比如说, 下图就展示了一个值为 `"Redis"` 的 C 字符串:
-
+
C 语言使用的这种简单的字符串表示方式, 并不能满足 Redis 对字符串在安全性、效率、以及功能方面的要求
@@ -108,7 +137,21 @@ C 语言使用的这种简单的字符串表示方式, 并不能满足 Redis
举个例子, 对于下图所示的 SDS 来说, 程序只要访问 SDS 的 `len` 属性, 就可以立即知道 SDS 的长度为 `5` 字节:
- 
+ 
+
+ > **len**:当前字符串的长度(不包括末尾的 null 字符)。这使得 Redis 不需要在每次操作时都遍历整个字符串来获取其长度。
+ >
+ > **alloc**:当前为字符串分配的总空间(包括字符串数据和额外的内存空间)。由于 Redis 使用的是动态分配内存,因此可以避免频繁的内存分配和释放。
+ >
+ > **buf**:实际的字符串数据部分,存储字符串的字符数组。Redis 通过这个区域存储字符串的内容。
+ >
+ > ```c
+ > struct sdshdr {
+ > int len; // 当前字符串的长度
+ > int alloc; // 为字符串分配的空间, 2.X 版本用一个 free 表示当前字符串缓冲区中未使用的内存量
+ > unsigned char buf[]; // 字符串数据
+ > };
+ > ```
通过使用 SDS 而不是 C 字符串, Redis 将获取字符串长度所需的复杂度从 $O(N)$ 降低到了 $O(1)$ , 这确保了获取字符串长度的工作不会成为 Redis 的性能瓶颈
@@ -129,11 +172,20 @@ C 语言使用的这种简单的字符串表示方式, 并不能满足 Redis
通过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略。
-- **二进制安全**
+- **SDS 如何保证二进制安全**:
- 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据
+ - **不依赖于 null 字符**:**二进制安全**意味着 SDS 字符串可以包含任何数据,包括 null 字节。传统的 C 字符串依赖于 null 字符('\0')来表示字符串的结束,但 Redis 的 SDS 不依赖于 null 字符来确定字符串的结束位置。
+ - **动态扩展**:SDS 会根据需要动态地扩展其内部缓冲区。Redis 会使用 `alloc` 字段来记录已分配的内存大小。当你向 SDS 中追加数据时,Redis 会确保分配足够的内存,而不需要担心数据的终止符。
+ - **不需要二次编码**:二进制数据直接存储在 SDS 的 `buf` 区域内,不需要进行任何编码或转换。因此,Redis 可以原样存储任意二进制数据。
+ > C 语言中的字符串必须符合某种编码(比如 ASCII),并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据
+- **编码方式**:
+ - **int**:当值可以用整数表示时,Redis 会使用整数编码。这样可以节省内存,并且在执行数值操作时更高效。
+ - **embstr**:这是一种特殊的编码方式,用于存储短字符串。当字符串的长度小于或等于 44 字节时,Redis 会使用 embstr 编码。只读,修改后自动转为 raw。这种编码方式将字符串直接存储在 Redis 对象的内部,这样可以减少内存分配和内存拷贝的次数,提高性能。
+ - **raw**:当字符串值不是整数时,Redis 会使用 raw 编码。raw 编码就是简单地将字符串存储为字节序列。Redis 会根据客户端发送的字节序列来存储字符串,因此可以存储任何类型的数据,包括二进制数据。
+
+> 可以通过 `TYPE KEY_NAME` 查看 key 所存储的值的类型验证下。
### 2、List(列表)
@@ -141,8 +193,6 @@ C 语言使用的这种简单的字符串表示方式, 并不能满足 Redis
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
-
-
Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理
**右边进左边出:队列**
@@ -188,21 +238,19 @@ Redis 的列表结构常用来做异步队列使用。将需要延后处理的
- 列表中保存的单个数据(有可能是字符串类型的)小于 64 字节;
- 列表中数据个数少于 512 个。
->听到“压缩”两个字,直观的反应就是节省内存。之所以说这种存储结构节省内存,是相较于数组的存储思路而言的。我们知道,数组要求每个元素的大小相同,如果我们要存储不同长度的字符串,那我们就需要用最大长度的字符串大小作为元素的大小(假设是 20 个字节)。那当我们存储小于 20 个字节长度的字符串的时候,便会浪费部分存储空间。听起来有点儿拗口,我画个图解释一下。
->
->
->
->压缩列表这种存储结构,一方面比较节省内存,另一方面可以支持不同类型数据的存储。而且,因为数据存储在一片连续的内存空间,通过键来获取值为列表类型的数据,读取的效率也非常高。
+从 Redis 3.2 版本开始,列表的底层实现由压缩列表组成的快速列表(quicklist)所取代。
-当列表中存储的数据量比较大的时候,也就是不能同时满足刚刚讲的两个条件的时候,列表就要通过双向循环链表来实现了。
+- 快速列表由多个 ziplist 节点组成,每个节点使用指针连接,形成一个链表。
+- 这种方式结合了压缩列表的内存效率和小元素的快速访问,以及双向链表的灵活性。
-Redis 的这种双向链表的实现方式,非常值得借鉴。它额外定义一个 list 结构体,来组织链表的首、尾指针,还有长度等信息。这样,在使用的时候就会非常方便。
+>听到“压缩”两个字,直观的反应就是节省内存。之所以说这种存储结构节省内存,是相较于数组的存储思路而言的。
+>
+>它并不是基础数据结构,而是 Redis 自己设计的一种数据存储结构。它有点儿类似数组,通过一片连续的内存空间,来存储数据。不过,它跟数组不同的一点是,它允许存储的数据大小不同。听到“压缩”两个字,直观的反应就是节省内存。之所以说这种存储结构节省内存,是相较于数组的存储思路而言的。我们知道,数组要求每个元素的大小相同,如果我们要存储不同长度的字符串,那我们就需要用最大长度的字符串大小作为元素的大小(假设是 20 个字节)。那当我们存储小于 20 个字节长度的字符串的时候,便会浪费部分存储空间。压缩列表这种存储结构,一方面比较节省内存,另一方面可以支持不同类型数据的存储。而且,因为数据存储在一片连续的内存空间,通过键来获取值为列表类型的数据,读取的效率也非常高。当列表中存储的数据量比较大的时候,也就是不能同时满足刚刚讲的两个条件的时候,列表就要通过双向循环链表来实现了。
我们可以从 [源码](https://github.com/redis/redis/blob/unstable/src/adlist.h "redis源码") 的 `adlist.h/listNode` 来看到对其的定义:
```c
/* Node, List, and Iterator are the only data structures used currently. */
-
typedef struct listNode {
struct listNode *prev; // 前置节点
struct listNode *next; // 后置节点
@@ -230,23 +278,95 @@ typedef struct list {
Redis hash 是一个键值对集合。KV 模式不变,但 V 又是一个键值对。
-字典类型也有两种实现方式。一种是我们刚刚讲到的压缩列表,另一种是散列表。
+#### 1. **Redis Hash 的实现**
+
+Redis 使用的是 **哈希表(Hash Table)** 来存储 `Hash` 类型的数据。每个 `Hash` 对象都由一个或多个字段(field)和相应的值(value)组成。
+
+在 Redis 中,哈希表的数据结构是通过 **`dict`(字典)** 来实现的,`dict` 的结构包括键和值,其中每个键都是哈希表中的字段(field),而值是字段的值。
+
+哈希表的基本实现如下:
+
+- **哈希表**:采用 **开地址法** 来处理哈希冲突。每个 `Hash` 存储为一个哈希表,哈希表根据一定的哈希算法将键映射到一个位置。
+- **动态扩展**:当哈希表存储的元素数量达到一定的阈值时,Redis 会自动进行 **rehash(重哈希)** 操作,重新分配内存并调整哈希表的大小。
+
+Redis 字典由 **嵌套的三层结构** 构成,采用链地址法处理哈希冲突:
+
+```c
+// 哈希表结构
+typedef struct dictht {
+ dictEntry **table; // 二维数组(哈希桶)
+ unsigned long size; // 总槽位数(2^n 对齐)
+ unsigned long sizemask; // 槽位掩码(size-1)
+ unsigned long used; // 已用槽位数量
+} dictht;
+
+// 字典结构
+typedef struct dict {
+ dictType *type; // 类型特定函数(实现多态)
+ void *privdata; // 私有数据
+ dictht ht[2]; // 双哈希表(用于渐进式rehash)
+ long rehashidx; // rehash进度标记(-1表示未进行)
+} dict;
+
+// 哈希节点结构
+typedef struct dictEntry {
+ void *key; // 键(SDS字符串)
+ union {
+ void *val; // 值(Redis对象)
+ uint64_t u64;
+ int64_t s64;
+ } v;
+ struct dictEntry *next; // 链地址法指针
+} dictEntry;
+```
+
+#### 2. **Redis Hash 的内部结构**
+
+Redis 中的 `Hash` 是由 **哈希表** 和 **ziplist** 两种不同的数据结构实现的,具体使用哪一种结构取决于哈希表中键值对的数量和大小。
+
+2.1 **哈希表(Hash Table)**
+
+- 哈希表是 Redis 中实现字典(`dict`)的基础数据结构,使用 **哈希冲突解决方法**(例如链地址法或开地址法)来存储键值对。
+- 每个哈希表由两个数组组成:**键数组(key array)** 和 **值数组(value array)**,它们通过哈希算法映射到表中的相应位置。
+
+2.2 **Ziplist(压缩列表)**
+
+- **ziplist** 是一种内存高效的列表数据结构,在 Redis 中用于存储小型的 `Hash`,当哈希表中的元素个数较少时,Redis 会使用 `ziplist` 来节省内存。
+- Ziplist 是连续内存块,采用压缩存储。它适用于存储小量数据,避免哈希表内存开销。
+
+2.3 **切换机制**
+
+> - 字典中保存的键和值的大小都要小于 64 字节;
+> - 字典中键值对的个数要小于 512 个。
+
+当 Redis 中 `Hash` 的元素数量较小时,会使用 `ziplist`;当元素数量增多时,会切换到使用哈希表的方式。
-同样,只有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件:字典中保存的键和值的大小都要小于 64 字节;字典中键值对的个数要小于 512 个。
+- **小型 Hash**:当哈希表的字段数量很少时,Redis 会使用 `ziplist` 来存储 `Hash`,因为它可以节省内存。
+- **大型 Hash**:当字段数量较多时,Redis 会将 `ziplist` 转换为 `哈希表` 来优化性能和内存管理。
-当不能同时满足上面两个条件的时候,Redis 就使用散列表来实现字典类型。Redis 使用 MurmurHash2 这种运行速度快、随机性好的哈希算法作为哈希函数。对于哈希冲突问题,Redis 使用链表法来解决。除此之外,Redis 还支持散列表的动态扩容、缩容。当数据动态增加之后,散列表的装载因子会不停地变大。为了避免散列表性能的下降,当装载因子大于 1 的时候,Redis 会触发扩容,将散列表扩大为原来大小的 2 倍左右(具体值需要计算才能得到,如果感兴趣,你可以去[阅读源码](https://github.com/redis/redis/blob/unstable/src/dict.c))。
+#### 3. Rehash(重哈希)
-扩容缩容要做大量的数据搬移和哈希值的重新计算,所以比较耗时。针对这个问题,Redis 使用我们在散列表(中)讲的渐进式扩容缩容策略,将数据的搬移分批进行,避免了大量数据一次性搬移导致的服务停顿。
+**Rehash** 是 Redis 用来扩展哈希表的一种机制。随着 `Hash` 中元素数量的增加,哈希表的负载因子(load factor)会逐渐增大,最终可能会导致哈希冲突增多,降低查询效率。为了解决这个问题,Redis 会在哈希表负载因子达到一定阈值时,执行 **rehash** 操作,即扩展哈希表。
+3.1 **Rehash 的过程**
+- **扩展哈希表**:Redis 会将哈希表的大小翻倍,并将现有的数据重新映射到新的哈希表中。扩展哈希表的目的是减少哈希冲突,提高查找效率。
-Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典, 内部实现结构上同 Java 的 HashMap 也是一致的,同样的**数组 + 链表**二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
+- **渐进式 rehash(Incremental Rehash)**:Redis 在执行重哈希时,并不会一次性将所有数据都重新映射到新的哈希表中,这样可以避免大量的阻塞操作。Redis 会分阶段地逐步迁移哈希表中的元素。这一过程通过增量的方式进行,逐步从旧哈希表中取出元素,放入新哈希表。
-不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。
+ 这种增量迁移的方式保证了 **rehash** 操作不会一次性占用过多的 CPU 时间,避免了阻塞。
-渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当搬迁完成了,就会使用新的 hash 结构取而代之。
+3.2 **Rehash 的触发条件**
-当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。
+Redis 会在以下情况下触发 rehash 操作:
+
+- 当哈希表的元素数量超过哈希表容量的负载因子阈值时(例如,默认阈值为 1),Redis 会开始进行 rehash 操作。
+- 当哈希表的空间变得非常紧张,Redis 会执行扩展操作。
+
+扩容就会涉及到键值对的迁移。具体来说,迁移操作会在以下两种情况下进行:
+
+1. **Lazy Rehashing(懒惰重哈希):** Redis 采用了懒惰重哈希的策略,即在进行哈希表扩容时,并不会立即将所有键值对都重新散列到新的存储桶中。而是在有需要的时候,例如进行读取操作时,才会将相应的键值对从旧存储桶迁移到新存储桶中。这种方式避免了一次性大规模的迁移操作,减少了扩容期间的阻塞时间。
+2. **Redis 事件循环(Event Loop):** Redis 会在事件循环中定期执行一些任务,包括一些与哈希表相关的操作。在事件循环中,Redis会检查是否有需要进行迁移的键值对,并将它们从旧存储桶迁移到新存储桶中。这样可以保证在系统负载较轻的时候进行迁移,减少对服务性能的影响。
@@ -288,7 +408,7 @@ typedef struct dict {
### 4、Set(集合)
-集合这种数据类型用来存储一组不重复的数据。这种数据类型也有两种实现方法,一种是基于有序数组,另一种是基于散列表。当要存储的数据,同时满足下面这样两个条件的时候,Redis 就采用有序数组,来实现集合这种数据类型。
+集合这种数据类型用来存储一组不重复的数据。这种数据类型也有两种实现方法,一种是基于整数集合,另一种是基于散列表。当要存储的数据,同时满足下面这样两个条件的时候,Redis 就采用整数集合(intset),来实现集合这种数据类型。
- 存储的数据都是整数;
- 存储的数据元素个数不超过 512 个。
@@ -299,9 +419,45 @@ Redis 的 Set 是 String 类型的无序集合。它是通过 HashTable 实现
当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
+```c
+// 定义整数集合结构
+typedef struct intset {
+ uint32_t encoding; // 编码方式
+ uint32_t length; // 集合长度
+ int8_t contents[]; // 元素数组
+} intset;
+
+// 定义哈希表节点结构
+typedef struct dictEntry {
+ void *key; // 键
+ union {
+ void *val; // 值
+ uint64_t u64;
+ int64_t s64;
+ double d;
+ } v;
+ struct dictEntry *next; // 指向下一个节点的指针
+} dictEntry;
+
+// 定义哈希表结构
+typedef struct dictht {
+ dictEntry **table; // 存储桶数组
+ unsigned long size; // 存储桶数量
+ unsigned long sizemask; // 存储桶数量掩码
+ unsigned long used; // 已使用存储桶数量
+} dictht;
+
+// 定义集合结构
+typedef struct {
+ uint32_t encoding; // 编码方式
+ dictht *ht; // 哈希表
+ intset *is; // 整数集合
+} set;
+```
+
-### 5、zset(sorted set:有序集合)
+### 5、Zset(sorted set:有序集合)
zset 和 set 一样也是 String 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。
@@ -318,41 +474,48 @@ zset 中最后一个 value 被移除后,数据结构自动删除,内存被
-为什么 Redis 要用跳表来实现有序集合,而不是红黑树?
-
-Redis 中的有序集合是通过跳表来实现的,严格点讲,其实还用到了散列表。不过散列表我们后面才会讲到,所以我们现在暂且忽略这部分。如果你去查看 Redis 的开发手册,就会发现,Redis 中的有序集合支持的核心操作主要有下面这几个:
-
-- 插入一个数据;
-- 删除一个数据;
-- 查找一个数据;
-- 按照区间查找数据(比如查找值在[100, 356]之间的数据);
-- 迭代输出有序序列。
-
-其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。对于按照区间查找数据这个操作,跳表可以做到 $O(logn)$ 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。当然,Redis 之所以用跳表来实现有序集合,还有其他原因,比如,跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。还有,跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。不过,跳表也不能完全替代红黑树。因为红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现的。我们做业务开发的时候,直接拿来用就可以了,不用费劲自己去实现一个红黑树,但是跳表并没有一个现成的实现,所以在开发中,如果你想使用跳表,必须要自己实现。
+> **为什么 Redis 要用跳表来实现有序集合,而不是红黑树?**
+>
+> Redis 中的有序集合是通过跳表来实现的,严格点讲,其实还用到了散列表。不过散列表我们后面才会讲到,所以我们现在暂且忽略这部分。如果你去查看 Redis 的开发手册,就会发现,Redis 中的有序集合支持的核心操作主要有下面这几个:
+>
+> - 插入一个数据;
+> - 删除一个数据;
+> - 查找一个数据;
+> - 按照区间查找数据(比如查找值在[100, 356]之间的数据);
+> - 迭代输出有序序列。
+>
+> 其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。对于按照区间查找数据这个操作,跳表可以做到 $O(logn)$ 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。当然,Redis 之所以用跳表来实现有序集合,还有其他原因,比如,跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。还有,跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。不过,跳表也不能完全替代红黑树。因为红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现的。我们做业务开发的时候,直接拿来用就可以了,不用费劲自己去实现一个红黑树,但是跳表并没有一个现成的实现,所以在开发中,如果你想使用跳表,必须要自己实现。
## 二、其他数据类型
-### bitmaps
+### Bitmap
-##### ☆☆位图:
+Redis 的 Bitmap 数据结构是一种基于 String 类型的位数组,它允许用户将字符串当作位向量来使用,并对这些位执行位操作。Bitmap 并不是 Redis 中的一个独立数据类型,而是通过在 String 类型上定义的一组位操作命令来实现的。由于 Redis 的 String 类型是二进制安全的,最大长度可以达到 512 MB,因此可以表示最多 $2^{32}$ 个不同的位。
-在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。
+Bitmap 在 Redis 中的使用场景包括但不限于:
-为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
+1. **集合表示**:当集合的成员对应于整数 0 到 N 时,Bitmap 可以高效地表示这种集合。
+2. **对象权限**:每个位代表一个特定的权限,类似于文件系统存储权限的方式。
+3. **签到系统**:记录用户在特定时间段内的签到状态。
+4. **用户在线状态**:跟踪大量用户的在线或离线状态。
-
+Bitmap 操作的基本命令包括:
+- `SETBIT key offset value`:设置或清除 key 中 offset 位置的位值(只能是 0 或 1)。
+- `GETBIT key offset`:获取 key 中 offset 位置的位值,如果 key 不存在,则返回 0。
+Bitmap 还支持更复杂的位操作,如:
-位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理
+- `BITOP operation destkey key [key ...]`:对一个或多个 key 的 Bitmap 进行位操作(AND、OR、NOT、XOR)并将结果保存到 destkey。
+- `BITCOUNT key [start] [end]`:计算 key 中位数为 1 的数量,可选地在指定的 start 和 end 范围内进行计数。
- Redis 的位数组是自动扩展,如果设置了某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充。
+Bitmap 在存储空间方面非常高效,例如,表示一亿个用户的登录状态,每个用户用一个位来表示,总共只需要 12 MB 的内存空间。
-
+在实际应用中,Bitmap 可以用于实现诸如亿级数据统计、用户行为跟踪等大规模数据集的高效管理。
-接下来我们使用 redis-cli 设置第一个字符,也就是位数组的前 8 位,我们只需要设置值为 1 的位,如上图所示,h 字符只有 1/2/4 位需要设置,e 字符只有 9/10/13/15 位需要设置。值得注意的是位数组的顺序和字符的位顺序是相反的。
+总的来说,Redis 的 Bitmap 是一种非常节省空间且功能强大的数据结构,适用于需要对大量二进制数据进行操作的场景。
```sh
127.0.0.1:6379> setbit s 1 1
@@ -375,43 +538,37 @@ Redis 中的有序集合是通过跳表来实现的,严格点讲,其实还
上面这个例子可以理解为「零存整取」,同样我们还也可以「零存零取」,「整存零取」。「零存」就是使用 setbit 对位值进行逐个设置,「整存」就是使用字符串一次性填充所有位数组,覆盖掉旧值。
-bitcount 和 bitop, bitpos, bitfield 都是操作位图的指令。
-
### HyperLogLog
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。
-场景:可以用来统计站点的UV...
+Redis HyperLogLog 是一种用于基数统计的数据结构,它提供了一个近似的、不精确的解决方案来估算集合中唯一元素的数量,即集合的基数。HyperLogLog 特别适用于需要处理大量数据并且对精度要求不是特别高的场景,因为它使用非常少的内存(通常每个 HyperLogLog 实例只需要 12.4KB 左右,无论集合中有多少元素)。
-Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。但是会有误差。
-
-| 命令 | 用法 | 描述 |
-| ------- | ------------------------------------------ | ----------------------------------------- |
-| pfadd | [PFADD key element [element ...] | 添加指定元素到 HyperLogLog 中 |
-| pfcount | [PFCOUNT key [key ...] | 返回给定 HyperLogLog 的基数估算值。 |
-| pfmerge | [PFMERGE destkey sourcekey [sourcekey ...] | 将多个 HyperLogLog 合并为一个 HyperLogLog |
-
-```java
-public class JedisTest {
- public static void main(String[] args) {
- Jedis jedis = new Jedis();
- for (int i = 0; i < 100000; i++) {
- jedis.pfadd("codehole", "user" + i);
- }
- long total = jedis.pfcount("codehole");
- System.out.printf("%d %d\n", 100000, total);
- jedis.close();
- }
-}
-```
+HyperLogLog 的主要特点包括:
+
+1. **近似统计**:HyperLogLog 不保证精确计算基数,但它提供了一个非常接近真实值的近似值。
+2. **内存效率**:HyperLogLog 能够使用固定大小的内存来估算基数,这使得它在处理大规模数据集时非常有用。
+3. **可合并性**:多个 HyperLogLog 实例可以合并,以估算多个集合的并集基数。
+
+HyperLogLog 的主要命令包括:
-[HyperLogLog图解](http://content.research.neustar.biz/blog/hll.html )
+- `PFADD key element [element ...]`:向 HyperLogLog 数据结构添加元素。如果 key 不存在,它将被创建。
+- `PFCOUNT key`:返回给定 HyperLogLog 的近似基数。
+- `PFMERGE destkey sourcekey [sourcekey ...]`:将多个 HyperLogLog 结构合并到一个单独的 HyperLogLog 结构中。
+
+HyperLogLog 的工作原理基于一个数学算法,它使用一个固定大小的位数组和一些哈希函数。当添加一个新元素时,它被哈希函数映射到一个位数组的索引,并根据哈希值的前几位来设置位数组中的位。基数估算是通过分析位数组中 0 的位置来完成的。
+
+由于 HyperLogLog 提供的是近似值,它有一个标准误差率,通常在 0.81% 左右。这意味着如果实际基数是 1000,HyperLogLog 估算的基数可能在 992 到 1008 之间。
+
+HyperLogLog 是处理大数据集基数统计的理想选择,尤其是当数据集太大而无法在内存中完全加载时。它在数据挖掘、日志分析、用户行为分析等领域有着广泛的应用。
+
+场景:可以用来统计站点的UV...
+> [HyperLogLog图解](http://content.research.neustar.biz/blog/hll.html )
-### Geo
diff --git a/docs/data-management/Redis/Reids-Lock.md b/docs/data-management/Redis/Redis-Lock.md
similarity index 99%
rename from docs/data-management/Redis/Reids-Lock.md
rename to docs/data-management/Redis/Redis-Lock.md
index f91c4af2c0..5719d23d85 100644
--- a/docs/data-management/Redis/Reids-Lock.md
+++ b/docs/data-management/Redis/Redis-Lock.md
@@ -146,7 +146,7 @@ end
1. 获取锁时,过期时间要设置多少合适呢?
- 预估一个合适的时间,其实没那么容易,比如操作资源的时间最慢可能要 10 s,而我们只设置了 5 s 就过期,那就存在锁提前过期的风险。这个问题先记下,我们一会看下 Javaer 要怎么在代码中用 Redis 锁。
+ 预估一个合适的时间,其实没那么容易,比如操作资源的时间最慢可能要 10s,而我们只设置了 5s 就过期,那就存在锁提前过期的风险。这个问题先记下,我们一会看下 Javaer 要怎么在代码中用 Redis 锁。
2. 容错性如何保证呢?
@@ -162,7 +162,7 @@ Redisson 是 Redis 官方的分布式锁组件。GitHub 地址:[https://github
> Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的 Java 常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson 提供了使用 Redis 的最简单和最便捷的方法。Redisson 的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
-redisson 现在已经很强大了,github 的 wiki 也很详细,分布式锁的介绍直接戳 [Distributed locks and synchronizers](https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers)
+Redisson 现在已经很强大了,github 的 wiki 也很详细,分布式锁的介绍直接戳 [Distributed locks and synchronizers](https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers)
Redisson 支持单点模式、主从模式、哨兵模式、集群模式,只是配置的不同,我们以单点模式来看下怎么使用,代码很简单,都已经为我们封装好了,直接拿来用就好,详细的 demo,我放在了 github: starfish-learn-redisson 上,这里就不一步步来了
@@ -666,7 +666,7 @@ Redisson 提供了看门狗,每获得一个锁时,只设置一个很短的
-
+
## 四、RedLock
diff --git a/docs/data-management/Redis/Redis-MQ.md b/docs/data-management/Redis/Redis-MQ.md
index 8c3dceed9d..64527748f9 100644
--- a/docs/data-management/Redis/Redis-MQ.md
+++ b/docs/data-management/Redis/Redis-MQ.md
@@ -142,7 +142,7 @@ Redis 提供了好几对 List 指令,先大概看下这些命令,混个眼
-因为 Redis 单线程的特点,所以在消费数据时,同一个消息会不会同时被多个 `consumer` 消费掉,但是需要我们考虑消费不成功的情况。
+因为 Redis 单线程的特点,所以在消费数据时,同一个消息不会同时被多个 `consumer` 消费掉,但是需要我们考虑消费不成功的情况。
#### 可靠队列模式 | ack 机制
@@ -260,7 +260,7 @@ Redis 5.0 版本新增了一个更强大的数据结构——**Stream**。它提
-Streams 是 Redis 专门为消息队列设计的数据类型,所以提供了丰富的消息队列操作命令。
+Stream 是 Redis 专门为消息队列设计的数据类型,所以提供了丰富的消息队列操作命令。
#### Stream 常用命令
@@ -379,7 +379,7 @@ Redis Stream 借鉴了很多 Kafka 的设计。

-Stream 不像 Kafak 那样有分区的概念,如果想实现类似分区的功能,就要在客户端使用一定的策略将消息写到不同的 Stream。
+Stream 不像 Kafka 那样有分区的概念,如果想实现类似分区的功能,就要在客户端使用一定的策略将消息写到不同的 Stream。
- `xgroup create`:创建消费者组
- `xgreadgroup`:读取消费组中的消息
diff --git a/docs/data-management/Redis/Redis-Master-Slave.md b/docs/data-management/Redis/Redis-Master-Slave.md
index 848efd8214..bce755ff33 100644
--- a/docs/data-management/Redis/Redis-Master-Slave.md
+++ b/docs/data-management/Redis/Redis-Master-Slave.md
@@ -6,7 +6,7 @@ tags:
categories: Redis
---
-
+
> 我们总说的 Redis 具有高可靠性,其实,这里有两层含义:一是数据尽量少丢失,二是服务尽量少中断。
>
@@ -171,6 +171,17 @@ Redis 主从库之间的同步,在不同阶段有不同的处理方式,我
3. 最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。
具体的操作是,当主库完成 RDB 文件发送后,就会把此时 `replication buffer` 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
+
+ > 主节点在生成 RDB 文件时,会将新的写命令(例如 `SET`、`DEL` 等)追加到**复制积压缓冲区**中,同时这些命令也会通过网络直接发送到所有已连接的从节点。
+ >
+ > 这些写操作是以 **Redis 协议格式(RESP)** 逐条发送的。
+ >
+ > - 双管齐下(RDB + 命令传播)
+ >
+ > ##### **数据传输协议:RESP**(Redis Serialization Protocol)
+ >
+ > - Redis 的所有数据通信,包括 RDB 文件和增量数据的发送,均基于其内部协议 **RESP(Redis Serialization Protocol)**。
+ > - 增量数据是以 Redis 命令流的形式,序列化为 RESP 格式后通过 TCP 连接发送的
@@ -243,11 +254,11 @@ replicaof 所选从库的IP 6379
主库对应的偏移量是 `master_repl_offset`,从库的偏移量 `slave_repl_offset` 。正常情况下,这两个偏移量基本相等。
-
+
在网络断连阶段,主库可能会收到新的写操作命令,这时,`master_repl_offset` 会大于 `slave_repl_offset`。此时,主库只用把 `master_repl_offset` 和 `slave_repl_offset` 之间的命令操作同步给从库就可以了。
-
+
> PS:因为 repl_backlog_buffer 是一个环形缓冲区(可以理解为是一个定长的环形数组),所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作。**如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致**。如果从库和主库**断连时间过长**,造成它在主库 repl_backlog_buffer 的 slave_repl_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。
>
diff --git a/docs/data-management/Redis/Redis-Persistence.md b/docs/data-management/Redis/Redis-Persistence.md
index af421c9311..9bb9414067 100644
--- a/docs/data-management/Redis/Redis-Persistence.md
+++ b/docs/data-management/Redis/Redis-Persistence.md
@@ -6,7 +6,7 @@ tags:
categories: Redis
---
-
+
> 带着疑问,或者是面试问题去看 Redis 的持久化,或许会有不一样的视角,这几个问题你废了吗?
>
@@ -48,11 +48,11 @@ RDB 的缺点是最后一次持久化后的数据可能丢失。
**配置位置**: SNAPSHOTTING
-
+
rdb 默认保存的是 **dump.rdb** 文件,如下(不可读)
-
+
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。
@@ -71,7 +71,20 @@ rdb 默认保存的是 **dump.rdb** 文件,如下(不可读)
> 简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作(例如图中的键值对K1),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 K3),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
>
-> 
+> 
+>
+> 当 Redis 触发 RDB 快照时,它会通过 `fork` 系统调用来创建一个子进程,子进程负责将内存中的数据写入到磁盘,而父进程则继续响应客户端请求。**写时复制(COW)** 机制使得在 fork 后,父子进程共享同一份内存页,只有当父进程或子进程对内存进行修改时,操作系统才会复制出新的内存页,从而减少了内存复制的开销。
+>
+> 尽管如此,`fork` 子进程的操作本身仍然是有一定开销的,尤其在数据量非常大的时候,尽管采用了 COW,但以下因素可能会导致 **性能下降**:
+>
+> - **大量数据**:当 Redis 中的数据量非常大时(几千万或几亿条键值对),fork 子进程仍然需要花费一定的时间来生成 RDB 文件,即使是写时复制也会带来一定的延迟。
+> - **系统资源压力**:如果机器的内存不足,频繁的 fork 和写时复制操作可能导致系统的 **内存不足** 和 **CPU 高负载**,尤其是在数据量较大的时候。
+> - **磁盘 I/O**:即便使用写时复制,子进程最终需要将数据写入磁盘,磁盘 I/O 的性能也会影响快照的时间。如果磁盘写入速度较慢,生成 RDB 文件的过程可能会很慢。
+>
+> 如果 Redis 在执行一次 RDB 快照时,快照尚未完成,新的写操作又触发了一个新的 RDB 快照,那么会发生 **重叠触发** 的问题。
+>
+> - **新的 RDB 快照会等待前一个快照完成**。
+> - 如果 **`save` 配置的触发条件**(如 `save 900 1`)满足且当前快照未完成,Redis 会 **阻塞新的快照请求**,直到当前的快照操作完成。这是为了避免对系统资源的过度消耗,防止多次快照操作同时进行。
@@ -89,7 +102,7 @@ rdb 默认保存的是 **dump.rdb** 文件,如下(不可读)
将备份文件 (dump.rdb) 移动到 Redis 安装目录并启动服务即可(`CONFIG GET dir` 获取目录)
-
+
@@ -116,7 +129,7 @@ rdb 默认保存的是 **dump.rdb** 文件,如下(不可读)
#### 小总结
-
+
- RDB 是一个非常紧凑的文件
@@ -140,7 +153,7 @@ AOF 默认保存的是 **appendonly.aof ** 文件
**配置位置**: APPEND ONLY MODE
-
+
@@ -167,7 +180,7 @@ AOF 默认保存的是 **appendonly.aof ** 文件
不过,AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志,如下图所示:
-
+
> Tip:日志先行的方式,如果宕机后,还可以通过之前保存的日志恢复到之前的数据状态。可是 AOF 后写日志的方式,如果宕机后,不就会把写入到内存的数据丢失吗?
>
@@ -179,7 +192,7 @@ AOF 默认保存的是 **appendonly.aof ** 文件
例如,`*2` 表示有两个部分,`$6` 表示 6 个字节,也就是下边的 “SELECT” 命令,`$1` 表示 1 个字节,也就是下边的 “0” 命令,合起来就是 `SELECT 0`,选择 0 库。下边的指令同理,就很好理解了 `SET K1 V1`。
-
+
但是,为了避免额外的检查开销,**Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错**。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以**不会阻塞当前的写操作**。
@@ -231,19 +244,26 @@ AOF 默认保存的是 **appendonly.aof ** 文件
#### rewrite(AOF 重写)
-- 是什么:AOF 采用文件追加方式,文件会越来越大,为了避免出现这种情况,新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令 `bgrewriteaof`,这个操作相当于对 AOF 文件“瘦身”。在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。
-
- 
+- 是什么:AOF 采用文件追加方式,文件会越来越大,为了避免出现这种情况,新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,**Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集**,可以使用命令 `bgrewriteaof`,这个操作相当于对 AOF 文件“瘦身”。在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。
-- 重写原理:AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,转换成一条条的操作指令,再序列化到一个新的 AOF 文件中。
+ 
- PS: 重写 AOF 文件的操作,并没有读取旧的 AOF 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 AOF 文件,这点和快照有点类似。
+- **重写原理**:AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,转换成一条条的操作指令,再序列化到一个新的 AOF 文件中。
-- 触发机制:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发
+ > PS: 重写 AOF 文件的操作,并没有读取旧的 AOF 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 AOF 文件,这点和快照有点类似。
- 我们在客户端输入两次 `set k1 v1` ,然后比较 `bgrewriteaof` 前后两次的 appendonly.aof 文件(先要关闭混合持久化)
+- 触发机制:
-
+ - **AOF 文件增长比例超过一定阈值**:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的**一倍**且文件大于 64M 时触发
+
+ ```
+ auto-aof-rewrite-min-size 64mb //指定触发重写的 AOF 文件最小大小(默认为 64MB)
+ auto-aof-rewrite-percentage 100 //指定 AOF 文件增长的百分比(默认为 100%)
+ ```
+
+ - **手动触发 AOF 重写**: 通过执行 `BGREWRITEAOF` 命令,用户可以手动触发 AOF 重写
+
+ 我们在客户端输入两次 `set k1 v1` ,然后比较 `bgrewriteaof` 前后两次的 appendonly.aof 文件(先要关闭混合持久化)
@@ -271,13 +291,22 @@ AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制
以下是 AOF 重写的执行步骤:
-1. Redis 执行 `fork()` ,现在同时拥有父进程和子进程。
-2. 子进程开始将新 AOF 文件的内容写入到临时文件。
-3. 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
-4. 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
-5. 搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。
+1. **创建一个新的 AOF 文件**:Redis 执行 `fork()` ,它会**异步地**在后台创建一个新的 AOF 文件(通常命名为 `appendonly.aof.new`)。现在同时拥有父进程和子进程。
+
+2. **重写的内容——只保留当前数据库状态**:子进程开始将新 AOF 文件的内容写入到临时文件。
+
+ 在执行 AOF 重写时,Redis 不会记录每个命令的详细内容,而是通过以下方式来确保重写后的 AOF 文件有效:
+
+ - **仅记录当前数据集的重建命令**:Redis 会根据数据库当前的状态生成一系列 `SET`、`HSET`、`LPUSH` 等命令,这些命令能够重建当前数据库的状态。这些命令是可以直接执行的、能够恢复数据的命令。
+ - **压缩冗余命令**:在重写过程中,Redis 会去除一些无意义或重复的命令。例如,如果有大量重复的 `SET key value` 操作,Redis 只会保留最新的 `SET` 命令,而忽略之前的操作。
+
+3. **复制 AOF文件**:对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件(即 `appendonly.aof`)的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的,这个过程称为 **追加操作**。
+
+4. **重写完成后的替换**:当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
+
+5. **删除过时的 AOF 文件**:搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。
-
+
#### 优势
@@ -293,7 +322,7 @@ AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制
#### 总结
-
+
- AOF 文件是一个只进行追加的日志文件
- Redis 可以在 AOF 文件体积变得过大时,自动在后台对 AOF 进行重写
@@ -356,7 +385,7 @@ Redis 4.0 中提出了一个**混合使用 AOF 日志和内存快照的方法**
同样我们执行 3 次 `set k1 v1`,然后手动瘦身 `bgrewriteaof` 后,查看 appendonly.aof 文件:
-
+
这样做的好处是可以结合 rdb 和 aof 的优点,快速加载同时避免丢失过多的数据,缺点是 aof 里面的 rdb 部分就是压缩格式不再是 aof 格式,可读性差。
@@ -366,7 +395,7 @@ Redis 4.0 中提出了一个**混合使用 AOF 日志和内存快照的方法**
如下图所示,两次快照中间时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
-
+
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,有点“鱼和熊掌可以兼得”的意思。
diff --git a/docs/data-management/Redis/Redis-Sentinel.md b/docs/data-management/Redis/Redis-Sentinel.md
index f185e1fc29..fcab77ecf2 100644
--- a/docs/data-management/Redis/Redis-Sentinel.md
+++ b/docs/data-management/Redis/Redis-Sentinel.md
@@ -6,7 +6,7 @@ tags:
categories: Redis
---
-
+
> 我们知道 Reids 提供了主从模式的机制,来保证可用性,可是如果主库发生故障了,那就直接会影响到从库的同步,怎么办呢?
>
@@ -274,7 +274,16 @@ master0:name=mymaster,status=ok,address=127.0.0.1:6381,slaves=2,sentinels=3

-
+> ##### **哨兵模式是否会出现脑裂问题?**
+>
+> - **哨兵模式下存在脑裂风险。**
+> - 当网络分区或通信异常时,可能导致旧主节点未完全下线,新的主节点被选出,导致两个主节点同时存在,形成**脑裂**问题。
+>
+> #### **解决方法:**
+>
+> 1. **主节点心跳检测**:通过哨兵的客观下线判断,多数哨兵节点确认主节点下线,减少误判。
+> 2. **客户端重连机制**:客户端连接断开后,需要重新通过哨兵获取正确的主节点地址。
+> 3. **配置防止脑裂**:`quorum` 参数设置哨兵节点的投票数量,避免少数节点误判主节点下线。
### 四、哨兵集群的原理
@@ -365,7 +374,7 @@ PSUBSCRIBE *
### 五、小结
-> Redis 哨兵是 Redis 的高可用实现方案:故障发现、故障自动转移、配置中心、客户端通知。
+Redis 哨兵是 Redis 的高可用实现方案:故障发现、故障自动转移、配置中心、客户端通知。
#### 5.1 哨兵机制其实就有三大功能:
diff --git a/docs/data-management/Redis/Redis-Transaction.md b/docs/data-management/Redis/Redis-Transaction.md
index 81fe5618bf..97c95bcb8c 100644
--- a/docs/data-management/Redis/Redis-Transaction.md
+++ b/docs/data-management/Redis/Redis-Transaction.md
@@ -34,7 +34,7 @@ Redis 在形式上看起来也差不多,分为三个阶段
2. 命令入队(业务操作)
3. 执行事务(exec)或取消事务(discard)
-```
+```sh
> multi
OK
> incr star
@@ -87,11 +87,11 @@ MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令
**正常执行**(可以批处理,挺爽,每条操作成功的话都会各取所需,互不影响)
-
+
**放弃事务**(discard 操作表示放弃事务,之前的操作都不算数)
-
+
@@ -112,11 +112,11 @@ Redis 针对如上两种错误采用了不同的处理策略,对于发生在 `
**全体连坐**(某一条操作记录报错的话,exec 后所有操作都不会成功)
-
+
**冤头债主**(示例中 k1 被设置为 String 类型,decr k1 可以放入操作队列中,因为只有在执行的时候才可以判断出语句错误,其他正确的会被正常执行)
-
+
@@ -154,17 +154,17 @@ OK
我们看个简单的例子,用 watch 监控我的账号余额(一周100零花钱的我),正常消费
-
+
但这个卡,还绑定了我媳妇的支付宝,如果在我消费的时候,她也消费了,会怎么样呢?
犯困的我去楼下 711 买了包烟,买了瓶水,这时候我媳妇在超市直接刷了 100,此时余额不足的我还在挑口香糖来着,,,
-
+
这时候我去结账,发现刷卡失败(事务中断),尴尬的一批
-
+
@@ -172,7 +172,7 @@ OK
> 当然,这里也会出现只要你媳妇刷了你的卡,就没办法刷成功的问题,这时候可以先查下余额,重新开启事务继续刷
-
+
diff --git a/docs/data-management/Redis/Cache-Design.md b/docs/data-management/Redis/reproduce/Cache-Design.md
similarity index 100%
rename from docs/data-management/Redis/Cache-Design.md
rename to docs/data-management/Redis/reproduce/Cache-Design.md
diff --git "a/docs/data-management/Redis/Key \345\257\273\345\235\200\347\256\227\346\263\225.md" "b/docs/data-management/Redis/reproduce/Key \345\257\273\345\235\200\347\256\227\346\263\225.md"
similarity index 100%
rename from "docs/data-management/Redis/Key \345\257\273\345\235\200\347\256\227\346\263\225.md"
rename to "docs/data-management/Redis/reproduce/Key \345\257\273\345\235\200\347\256\227\346\263\225.md"
diff --git a/docs/data-structure-algorithms/.DS_Store b/docs/data-structure-algorithms/.DS_Store
index 7c10cd529b..661b6ce50c 100644
Binary files a/docs/data-structure-algorithms/.DS_Store and b/docs/data-structure-algorithms/.DS_Store differ
diff --git a/docs/data-structure-algorithms/BFS.md b/docs/data-structure-algorithms/BFS.md
deleted file mode 100755
index 8c3521d7b3..0000000000
--- a/docs/data-structure-algorithms/BFS.md
+++ /dev/null
@@ -1,205 +0,0 @@
-## 齐头并进的广度优先遍历
-
-> DFS(深度优先搜索)和 BFS(广度优先搜索)就像孪生兄弟,提到一个总是想起另一个。然而在实际使用中,我们用 DFS 的时候远远多于 BFS。那么,是不是 BFS 就没有什么用呢?
->
-> 如果我们使用 DFS/BFS 只是为了遍历一棵树、一张图上的所有结点的话,那么 DFS 和 BFS 的能力没什么差别,我们当然更倾向于更方便写、空间复杂度更低的 DFS 遍历。不过,某些使用场景是 DFS 做不到的,只能使用 BFS 遍历。
->
-> DFS 遍历使用递归:
->
-> ```java
-> void dfs(TreeNode root) {
-> if (root == null) {
-> return;
-> }
-> dfs(root.left);
-> dfs(root.right);
-> }
-> ```
->
-> BFS 遍历使用队列数据结构:
->
-> ```jaava
-> void bfs(TreeNode root) {
-> Queue queue = new ArrayDeque<>();
-> queue.add(root);
-> while (!queue.isEmpty()) {
-> TreeNode node = queue.poll(); // Java 的 pop 写作 poll()
-> if (node.left != null) {
-> queue.add(node.left);
-> }
-> if (node.right != null) {
-> queue.add(node.right);
-> }
-> }
-> }
-> ```
->
-> 只是比较两段代码的话,最直观的感受就是:DFS 遍历的代码比 BFS 简洁太多了!这是因为递归的方式隐含地使用了系统的 栈,我们不需要自己维护一个数据结构。如果只是简单地将二叉树遍历一遍,那么 DFS 显然是更方便的选择。
->
-> 虽然 DFS 与 BFS 都是将二叉树的所有结点遍历了一遍,但它们遍历结点的顺序不同。
->
-> 
-
-
-
-
-
-「广度优先遍历」的思想在生活中随处可见:
-
-如果我们要找一个医生或者律师,我们会先在自己的一度人脉中遍历(查找),如果没有找到,继续在自己的二度人脉中遍历(查找),直到找到为止。
-
-### 广度优先遍历借助「队列」实现
-
-广度优先遍历呈现出「一层一层向外扩张」的特点,**先看到的结点先遍历,后看到的结点后遍历**,因此「广度优先遍历」可以借助「队列」实现。
-
-
-
-**说明**:遍历到一个结点时,如果这个结点有左(右)孩子结点,依次将它们加入队列。
-
-> 友情提示:广度优先遍历的写法相对固定,我们不建议大家背代码、记模板。在深刻理解广度优先遍历的应用场景(找无权图的最短路径),借助「队列」实现的基础上,多做练习,写对代码就是自然而然的事情了
->
-
-我们先介绍「树」的广度优先遍历,再介绍「图」的广度优先遍历。事实上,它们是非常像的。
-
-
-
-
-
-### 树的广度优先遍历
-
-例 1:「力扣」第 102 题:二叉树的层序遍历(中等)
-
-> 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
->
-> 示例:
->
-> 二叉树:[3,9,20,null,null,15,7],
->
-> ```
-> 3
-> / \
-> 9 20
-> / \
-> 15 7
-> ```
->
-> 返回其层序遍历结果:
->
-> ```
-> [
-> [3],
-> [9,20],
-> [15,7]
-> ]
-> ```
-
-思路分析:
-
-- 题目要求我们一层一层输出树的结点的值,很明显需要使用「广度优先遍历」实现;
-- 广度优先遍历借助「队列」实现;
-
-- 注意:
- - 这样写 for (int i = 0; i < queue.size(); i++) { 代码是不能通过测评的,这是因为 queue.size() 在循环中是变量(这条规则在 Python 中不成立,请各位读者自行验证)。正确的做法是:每一次在队列中取出元素的个数须要先暂存起来,请见参考代码;
- - 子结点入队的时候,非空的判断很重要:在队列的队首元素出队的时候,一定要在左(右)子结点非空的时候才将左(右)子结点入队。
-- 树的广度优先遍历的写法模式相对固定:
- - 使用队列;
- - 在队列非空的时候,动态取出队首元素;
- - 取出队首元素的时候,把队首元素相邻的结点(非空)加入队列。
-
-大家在做题的过程中需要多加练习,融汇贯通,不须要死记硬背。
-
-
-
-```java
-public class Solution {
-
- public List> levelOrder(TreeNode root) {
- List> res = new ArrayList<>();
- if (root == null) {
- return res;
- }
-
- Queue queue = new LinkedList<>();
- queue.offer(root);
- while (!queue.isEmpty()) {
- // 注意 1:一定要先把当前队列的结点总数暂存起来
- int currentSize = queue.size();
-
- List currentLevel = new ArrayList<>();
- for (int i = 0; i < currentSize; i++) {
- TreeNode front = queue.poll();
- currentLevel.add(front.val);
- // 注意 2:左(右)孩子结点非空才加入队列
- if (front.left != null) {
- queue.offer(front.left);
- }
- if (front.right != null) {
- queue.offer(front.right);
- }
- }
- res.add(currentLevel);
- }
- return res;
- }
-}
-```
-
-
-
-### 使用广度优先遍历得到无权图的最短路径
-
-在 无权图 中,由于广度优先遍历本身的特点,假设源点为 source,只有在遍历到 所有 距离源点 source 的距离为 d 的所有结点以后,才能遍历到所有 距离源点 source 的距离为 d + 1 的所有结点。也可以使用「两点之间、线段最短」这条经验来辅助理解如下结论:从源点 source 到目标结点 target 走直线走过的路径一定是最短的。
-
-> 在一棵树中,一个结点到另一个结点的路径是唯一的,但在图中,结点之间可能有多条路径,其中哪条路最近呢?这一类问题称为最短路径问题。最短路径问题也是 BFS 的典型应用,而且其方法与层序遍历关系密切。
->
-> 在二叉树中,BFS 可以实现一层一层的遍历。在图中同样如此。从源点出发,BFS 首先遍历到第一层结点,到源点的距离为 1,然后遍历到第二层结点,到源点的距离为 2…… 可以看到,用 BFS 的话,距离源点更近的点会先被遍历到,这样就能找到到某个点的最短路径了。
->
-> 
->
-> 小贴士:
->
-> 很多同学一看到「最短路径」,就条件反射地想到「Dijkstra 算法」。为什么 BFS 遍历也能找到最短路径呢?
->
-> 这是因为,Dijkstra 算法解决的是带权最短路径问题,而我们这里关注的是无权最短路径问题。也可以看成每条边的权重都是 1。这样的最短路径问题,用 BFS 求解就行了。
->
-> 在面试中,你可能更希望写 BFS 而不是 Dijkstra。毕竟,敢保证自己能写对 Dijkstra 算法的人不多。
->
-> 最短路径问题属于图算法。由于图的表示和描述比较复杂,本文用比较简单的网格结构代替。网格结构是一种特殊的图,它的表示和遍历都比较简单,适合作为练习题。在 LeetCode 中,最短路径问题也以网格结构为主。
->
->
-
-### 图论中的最短路径问题概述
-
-在图中,由于 图中存在环,和深度优先遍历一样,广度优先遍历也需要在遍历的时候记录已经遍历过的结点。特别注意:将结点添加到队列以后,一定要马上标记为「已经访问」,否则相同结点会重复入队,这一点在初学的时候很容易忽略。如果很难理解这样做的必要性,建议大家在代码中打印出队列中的元素进行调试:在图中,如果入队的时候不马上标记为「已访问」,相同的结点会重复入队,这是不对的。
-
-另外一点还需要强调,广度优先遍历用于求解「无权图」的最短路径,因此一定要认清「无权图」这个前提条件。如果是带权图,就需要使用相应的专门的算法去解决它们。事实上,这些「专门」的算法的思想也都基于「广度优先遍历」的思想,我们为大家例举如下:
-
-- 带权有向图、且所有权重都非负的单源最短路径问题:使用 Dijkstra 算法;
-- 带权有向图的单源最短路径问题:Bellman-Ford 算法;
-
-- 一个图的所有结点对的最短路径问题:Floy-Warshall 算法。
-
-这里列出的以三位计算机科学家的名字命名的算法,大家可以在《算法导论》这本经典著作的第 24 章、第 25 章找到相关知识的介绍。值得说明的是:应用任何一种算法,都需要认清使用算法的前提,不满足前提直接套用算法是不可取的。深刻理解应用算法的前提,也是学习算法的重要方法。例如我们在学习「二分查找」算法、「滑动窗口」算法的时候,就可以问自己,这个问题为什么可以使用「二分查找」,为什么可以使用「滑动窗口」。我们知道一个问题可以使用「优先队列」解决,是什么样的需求促使我们想到使用「优先队列」,而不是「红黑树(平衡二叉搜索树)」,想清楚使用算法(数据结构)的前提更重要。
-
-
-
-> 「力扣」第 323 题:无向图中连通分量的数目(中等)
-
-### 练习
-
-> 友情提示:第 1 - 4 题是广度优先遍历的变形问题,写对这些问题有助于掌握广度优先遍历的代码编写逻辑和细节。
-
-1. 完成「力扣」第 107 题:二叉树的层次遍历 II(简单);
-2. 完成《剑指 Offer》第 32 - I 题:从上到下打印二叉树(中等);
-3. 完成《剑指 Offer》第 32 - III 题:从上到下打印二叉树 III(中等);
-4. 完成「力扣」第 103 题:二叉树的锯齿形层次遍历(中等);
-5. 完成「力扣」第 429 题:N 叉树的层序遍历(中等);
-6. 完成「力扣」第 993 题:二叉树的堂兄弟节点(中等);
-
-
-
-
-
-## Reference
-
-- https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/
diff --git a/docs/data-structure-algorithms/Binary-Search.md b/docs/data-structure-algorithms/Binary-Search.md
deleted file mode 100755
index 650ab21ac9..0000000000
--- a/docs/data-structure-algorithms/Binary-Search.md
+++ /dev/null
@@ -1,682 +0,0 @@
-> https://labuladong.gitee.io/algo/2/22/61/
-
-### 二分查找
-
-```java
-int binarySearch(int[] nums, int target) {
- int left = 0;
- int right = nums.length - 1; // 注意
-
- while(left <= right) {
- int mid = left + (right - left) / 2;
- if(nums[mid] == target)
- return mid;
- else if (nums[mid] < target)
- left = mid + 1; // 注意
- else if (nums[mid] > target)
- right = mid - 1; // 注意
- }
- return -1;
-}
-```
-
-**1、为什么 while 循环的条件中是 <=,而不是 <**?
-
-答:因为初始化 `right` 的赋值是 `nums.length - 1`,即最后一个元素的索引,而不是 `nums.length`。
-
-**2、为什么 `left = mid + 1`,`right = mid - 1`?我看有的代码是 `right = mid` 或者 `left = mid`,没有这些加加减减,到底怎么回事,怎么判断**?
-
-答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。
-
-刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 `[left, right]`。那么当我们发现索引 `mid` 不是要找的 `target` 时,下一步应该去搜索哪里呢?
-
-
-
-### 寻找左侧边界的二分搜索
-
-```java
-public static int getLeftNums(int[] nums,int target) {
- int left = 0;
- int right = nums.length;
- while (left < right) {
- int mid = left + (right - left) / 2;
- if (nums[mid] == target) {
- right = mid;
- } else if (nums[mid] > target) {
- right = mid - 1;
- } else {
- left = mid + 1;
- }
- }
- return left;
-}
-```
-
-**1、为什么 while 中是 `<` 而不是 `<=`**?
-
-答:用相同的方法分析,因为 `right = nums.length` 而不是 `nums.length - 1`。因此每次循环的「搜索区间」是 `[left, right)` 左闭右开。
-
-`while(left < right)` 终止的条件是 `left == right`,此时搜索区间 `[left, left)` 为空,所以可以正确终止。
-
-
-
-**为什么 `left = mid + 1`,`right = mid` ?和之前的算法不一样**?
-
-答:这个很好解释,因为我们的「搜索区间」是 `[left, right)` 左闭右开,所以当 `nums[mid]` 被检测之后,下一步的搜索区间应该去掉 `mid` 分割成两个区间,即 `[left, mid)` 或 `[mid + 1, right)`。
-
-
-
-**4、为什么该算法能够搜索左侧边界**?
-
-答:关键在于对于 `nums[mid] == target` 这种情况的处理:
-
-```java
- if (nums[mid] == target)
- right = mid;
-```
-
-可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 `right`,在区间 `[left, mid)` 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
-
-
-
-```java
-int left_bound(int[] nums, int target) {
- int left = 0, right = nums.length - 1;
- // 搜索区间为 [left, right]
- while (left <= right) {
- int mid = left + (right - left) / 2;
- if (nums[mid] < target) {
- // 搜索区间变为 [mid+1, right]
- left = mid + 1;
- } else if (nums[mid] > target) {
- // 搜索区间变为 [left, mid-1]
- right = mid - 1;
- } else if (nums[mid] == target) {
- // 收缩右侧边界
- right = mid - 1;
- }
- }
- // 检查出界情况
- if (left >= nums.length || nums[left] != target) {
- return -1;
- }
- return left;
-}
-```
-
-
-
-### 寻找右侧边界的二分查找
-
-左闭右开的写法
-
-```java
-int right_bound(int[] nums, int target) {
- if (nums.length == 0) return -1;
- int left = 0, right = nums.length;
-
- while (left < right) {
- int mid = left + (right - left) / 2;
- if (nums[mid] == target) {
- left = mid + 1; // 注意
- } else if (nums[mid] < target) {
- left = mid + 1;
- } else if (nums[mid] > target) {
- right = mid;
- }
- }
- return left - 1; // 注意
-}
-```
-
-
-
-```java
-int right_bound(int[] nums, int target) {
- int left = 0, right = nums.length - 1;
- while (left <= right) {
- int mid = left + (right - left) / 2;
- if (nums[mid] < target) {
- left = mid + 1;
- } else if (nums[mid] > target) {
- right = mid - 1;
- } else if (nums[mid] == target) {
- // 这里改成收缩左侧边界即可
- left = mid + 1;
- }
- }
- // 这里改为检查 right 越界的情况,见下图
- if (right < 0 || nums[right] != target) {
- return -1;
- }
- return right;
-}
-```
-
-
-
-
-
-对于寻找左右边界的二分搜索,常见的手法是使用左闭右开的「搜索区间」,**我们还根据逻辑将「搜索区间」全都统一成了两端都闭,便于记忆,只要修改两处即可变化出三种写法**:
-
-```java
-int binary_search(int[] nums, int target) {
- int left = 0, right = nums.length - 1;
- while(left <= right) {
- int mid = left + (right - left) / 2;
- if (nums[mid] < target) {
- left = mid + 1;
- } else if (nums[mid] > target) {
- right = mid - 1;
- } else if(nums[mid] == target) {
- // 直接返回
- return mid;
- }
- }
- // 直接返回
- return -1;
-}
-
-int left_bound(int[] nums, int target) {
- int left = 0, right = nums.length - 1;
- while (left <= right) {
- int mid = left + (right - left) / 2;
- if (nums[mid] < target) {
- left = mid + 1;
- } else if (nums[mid] > target) {
- right = mid - 1;
- } else if (nums[mid] == target) {
- // 别返回,锁定左侧边界
- right = mid - 1;
- }
- }
- // 最后要检查 left 越界的情况
- if (left >= nums.length || nums[left] != target) {
- return -1;
- }
- return left;
-}
-
-int right_bound(int[] nums, int target) {
- int left = 0, right = nums.length - 1;
- while (left <= right) {
- int mid = left + (right - left) / 2;
- if (nums[mid] < target) {
- left = mid + 1;
- } else if (nums[mid] > target) {
- right = mid - 1;
- } else if (nums[mid] == target) {
- // 别返回,锁定右侧边界
- left = mid + 1;
- }
- }
- // 最后要检查 right 越界的情况
- if (right < 0 || nums[right] != target) {
- return -1;
- }
- return right;
-}
-```
-
-
-
-### [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)
-
-> 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
-> 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
-> 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
-> 注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
->
-> 给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
->
-> 你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
->
-> ```
-> 输入:nums = [3,4,5,1,2]
-> 输出:1
-> 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
-> ```
->
-> ```
-> 输入:nums = [11,13,15,17]
-> 输出:11
-> 解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
-> ```
-
-**思路**:
-
-升序数组+旋转,仍然是部分有序,考虑用二分查找。
-
-> 这种二分查找难就难在,arr[mid] 跟谁比。
->
-> 我们的目的是:当进行一次比较时,一定能够确定答案在 mid 的某一侧。一次比较为 arr[mid] 跟谁比的问题。
-> 一般的比较原则有:
->
-> - 如果有目标值 target,那么直接让 arr[mid] 和 target 比较即可。
-> - 如果没有目标值,一般可以考虑 **端点**
-
-
-
-旋转数组,那最小值右侧的元素肯定都小于数组中的最后一个元素 `nums[n-1]`,左侧元素都大于 `num[n-1]`
-
-```java
-public static int findMin(int[] nums) {
- int left = 0;
- int right = nums.length - 1;
- //左闭右开
- while (left < right) {
- int mid = left + (right - left) / 2;
- //疑问:为什么right = mid;而不是 right = mid-1;
- //解答:{4,5,1,2,3},如果right = mid-1,则丢失了最小值1
- if (nums[mid] < nums[right]) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
- return nums[left];
-}
-```
-
-
-
-**如果是求旋转数组中的最大值呢**
-
-```java
-public static int findMax(int[] nums) {
- int left = 0;
- int right = nums.length - 1;
-
- while (left < right) {
- int mid = left + (right - left) >> 1;
-
- //因为向下取整,left可能会等于mid,所以要考虑
- if (nums[left] < nums[right]) {
- return nums[right];
- }
-
- //[left,mid] 是递增的,最大值只会在[mid,right]中
- if (nums[left] < nums[mid]) {
- left = mid;
- } else {
- //[mid,right]递增,最大值只会在[left, mid-1]中
- right = mid - 1;
- }
- }
- return nums[left];
-}
-```
-
-
-
-### [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)
-
-> 整数数组 nums 按升序排列,数组中的值 互不相同 。
->
-> 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
->
-> 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
->
-> ```
-> 输入:nums = [4,5,6,7,0,1,2], target = 0
-> 输出:4
-> ```
->
-> ```
-> 输入:nums = [4,5,6,7,0,1,2], target = 3
-> 输出:-1
-> ```
-
-**思路**:
-
-旋转数组后,依然是局部有序,从数组中间分成左右两部分后,一定有一部分是有序的
-
-- 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [{nums}[l],{nums}[mid])),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
-- 如果 [mid, r] 是有序数组,且 target 的大小满足 ({nums}[mid+1],{nums}[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。
-
-```java
-public static int search(int[] nums,int target) {
- int n = nums.length;
- //特例
- if (n == 0) {
- return -1;
- }
- if (n == 1) {
- return nums[0] == target ? 0 : -1;
- }
-
- int left = 0;
- int right = nums.length - 1;
-
- while (left <= right) {
- int mid = left + (right - left) / 2;
- if (target == nums[mid]) {
- return mid;
- }
- //左侧有序的话
- if (nums[0] <= nums[mid]) {
- if (nums[0] <= target && target < nums[mid]) {
- right = mid - 1;
- } else {
- left = mid + 1;
- }
- } else { //右侧有序
- if (nums[mid] < target && target <= nums[n - 1]) {
- left = mid + 1;
- } else {
- right = mid - 1;
- }
- }
- }
- return -1;
-}
-```
-
-
-
-### [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)
-
-> 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
->
-> 如果数组中不存在目标值 target,返回 [-1, -1]。
->
-> 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
->
-> ```
-> 输入:nums = [5,7,7,8,8,10], target = 8
-> 输出:[3,4]
-> ```
->
-> ```
-> 输入:nums = [5,7,7,8,8,10], target = 6
-> 输出:[-1,-1]
-> ```
-
-**思路**:二分法寻找左右边界值
-
-```java
-public int[] searchRange(int[] nums, int target) {
- int first = binarySearch(nums, target, true);
- int last = binarySearch(nums, target, false);
- return new int[]{first, last};
-}
-
-public int binarySearch(int[] nums, int target, boolean findLast) {
- int length = nums.length;
- int left = 0, right = length - 1;
- //结果,因为可能有多个值,所以需要先保存起来
- int index = -1;
- while (left <= right) {
- //取中间值
- int middle = left + (right - left) / 2;
-
- //找到相同的值(只有这个地方和普通二分查找有不同)
- if (nums[middle] == target) {
- //先赋值一下,肯定是找到了,只是不知道这个值是不是在区域的边界内
- index = middle;
- //如果是查找最后的
- if (findLast) {
- //那我们将浮标移动到下一个值试探一下后面的值还是否有target
- left = middle + 1;
- } else {
- //否则,就是查找第一个值,也是同理,移动指针到上一个值去试探一下上一个值是不是等于target
- right = middle - 1;
- }
-
- //下面2个就是普通的二分查找流程,大于小于都移动指针
- } else if (nums[middle] < target) {
- left = middle + 1;
- } else {
- right = middle - 1;
- }
-
- }
- return index;
-}
-```
-
-
-
-### [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number/)
-
-> 给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
->
-> 假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
->
-> 你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
->
-> ```
-> 输入:nums = [1,3,4,2,2]
-> 输出:2
-> ```
->
-> ```
-> 输入:nums = [3,1,3,4,2]
-> 输出:3
-> ```
-
-**思路**:
-
-二分查找的思路是先猜一个数(有效范围 [left..right] 里位于中间的数 mid),然后统计原始数组中 小于等于 mid 的元素的个数 cnt:
-
-如果 cnt 严格大于 mid。根据抽屉原理,重复元素就在区间 [left..mid] 里;
-否则,重复元素就在区间 [mid + 1..right] 里。
-与绝大多数使用二分查找问题不同的是,这道题正着思考是容易的,即:思考哪边区间存在重复数是容易的,因为有抽屉原理做保证。
-
-```java
-public int findDuplicate(int[] nums) {
- int len = nums.length;
- int left = 1;
- int right = len - 1;
- while (left < right) {
- int mid = left + (right - left) / 2;
-
- int cnt = 0;
- for (int num : nums) {
- if (num <= mid) {
- cnt += 1;
- }
- }
-
- // 根据抽屉原理,小于等于 4 的个数如果严格大于 4 个,此时重复元素一定出现在 [1..4] 区间里
- if (cnt > mid) {
- // 重复元素位于区间 [left..mid]
- right = mid;
- } else {
- // if 分析正确了以后,else 搜索的区间就是 if 的反面区间 [mid + 1..right]
- left = mid + 1;
- }
- }
- return left;
-}
-```
-
-
-
-### [162. 寻找峰值](https://leetcode-cn.com/problems/find-peak-element/)
-
-> 峰值元素是指其值严格大于左右相邻值的元素。
->
-> 给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
->
-> 你可以假设 nums[-1] = nums[n] = -∞ 。
->
-> 你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
->
-> ```
-> 输入:nums = [1,2,3,1]
-> 输出:2
-> 解释:3 是峰值元素,你的函数应该返回其索引 2。
-> ```
->
-> ```
-> 输入:nums = [1,2,1,3,5,6,4]
-> 输出:1 或 5
-> 解释:你的函数可以返回索引 1,其峰值元素为 2;
-> 或者返回索引 5, 其峰值元素为 6。
-> ```
-
-**思路**:
-
-这题,最简单的思路就是直接找最大值,但这样复杂度是 $O(n)$
-
-在二分查找中,每次会找到一个位置 midmid。我们发现,midmid 只有如下三种情况:
-
-- midmid 为一个峰值,此时我们通过比较 midmid 位置元素与两边元素大小即可。
-- midmid 在一个峰值右侧,此时有 nums[mid] < nums[mid + 1]nums[mid] nums[1]) return 0;
- if (nums[n - 1] > nums[n - 2]) return n - 1;
-
- int l = 0, r = n - 1;
- while (l <= r) {
- int mid = (l + r) / 2;
-
- // 当前为峰值
- if (mid >= 1 && mid < n - 1 && nums[mid] > nums[mid - 1] && nums[mid] > nums[mid + 1]) {
- return mid;
- } else if (mid >= 1 && nums[mid] < nums[mid - 1]) {
- // 峰值在 mid 左侧
- r = mid - 1;
- } else if (mid < n - 1 && nums[mid] < nums[mid + 1]) {
- // 峰值在 mid 右侧
- l = mid + 1;
- }
- }
- return -1;
-}
-```
-
-
-
-### [240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/)
-
-> [剑指 Offer 04. 二维数组中的查找](https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/) 一样的题目
->
-> 在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
->
-> 现有矩阵 matrix 如下:
->
-> ```
-> [
-> [1, 4, 7, 11, 15],
-> [2, 5, 8, 12, 19],
-> [3, 6, 9, 16, 22],
-> [10, 13, 14, 17, 24],
-> [18, 21, 23, 26, 30]
-> ]
-> ```
->
-> 给定 target = 5,返回 true。
->
-> 给定 target = 20,返回 false。
->
-
-**思路**:
-
-站在右上角看。这个矩阵其实就像是一个Binary Search Tree。然后,聪明的大家应该知道怎么做了。
-
-```java
-public static boolean findNumberIn2DArray(int[][] matrix, int target) {
-
- if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
- return false;
- }
-
- int rows = matrix.length;
- int columns = matrix[0].length;
- for (int i = 0; i < rows; i++) {
- for (int j = 0; j < columns; j++) {
- if (matrix[i][j] == target) {
- return true;
- }
- }
- }
- return false;
-}
-```
-
-如果你写出这样的暴力解法,面试官可能就会反问你了:你还有什么想问的吗?
-
-言归正传,有序的数组,我们首先应该想到二分
-
-```java
-
-class Solution {
- public boolean searchMatrix(int[][] matrix, int target) {
- for (int[] row : matrix) {
- int index = search(row, target);
- if (index >= 0) {
- return true;
- }
- }
- return false;
- }
-
- public int search(int[] nums, int target) {
- int low = 0, high = nums.length - 1;
- while (low <= high) {
- int mid = (high - low) / 2 + low;
- int num = nums[mid];
- if (num == target) {
- return mid;
- } else if (num > target) {
- high = mid - 1;
- } else {
- low = mid + 1;
- }
- }
- return -1;
- }
-}
-```
-
-**Z 字形查找**
-
-> 假设arr数组,val,tar如下图所示:
-> 如果我们把二分值定在右上角或者左下角,就可以进行二分。这里以右上角为例,左下角可自行分析:
-> 
-> 1)我么设初始值为右上角元素,arr[0][5] = val,目标tar = arr[3][1]
-> 2)接下来进行二分操作:
-> 3)如果val == target,直接返回
-> 4)如果 tar > val, 说明target在更大的位置,val左边的元素显然都是 < val,间接 < tar,说明第 0 行都是无效的,所以val下移到arr[1][5]
-> 5)如果 tar < val, 说明target在更小的位置,val下边的元素显然都是 > val,间接 > tar,说明第 5 列都是无效的,所以val左移到arr[0][4]
-> 6)继续步骤2)
-
-```java
-public static boolean findNumberIn2DArray(int[][] matrix, int target) {
-
- if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
- return false;
- }
-
- int rows = matrix.length;
- int columns = matrix[0].length;
- //右上角坐标
- int row = 0;
- int col = columns - 1;
- while (row < rows && col >= 0) {
- int num = matrix[row][col];
- if (num == target) {
- return true;
- } else if (target > num) {
- row++;
- } else {
- col--;
- }
- }
- return false;
-}
-```
-
-
-
diff --git a/docs/data-structure-algorithms/DFS.md b/docs/data-structure-algorithms/DFS.md
deleted file mode 100755
index 7df2e920a9..0000000000
--- a/docs/data-structure-algorithms/DFS.md
+++ /dev/null
@@ -1,1614 +0,0 @@
-## 一、深度优先搜索
-
-
-
-在线性结构中,按照顺序一个一个地看到所有的元素,称为线性遍历。在非线性结构中,由于元素之间的组织方式变得复杂,就有了不同的遍历行为。其中最常见的遍历有:**深度优先遍历**(Depth-First-Search)和**广度优先遍历**(Breadth-First-Search)。它们的思想非常简单,但是在算法的世界里发挥着巨大的作用。
-
-
-
-### 深度优先遍历的形象描述
-
-「一条路走到底,不撞南墙不回头」是对「深度优先遍历」的最直观描述。
-
-说明:
-
-- 深度优先遍历只要前面有可以走的路,就会一直向前走,直到无路可走才会回头;
-- 「无路可走」有两种情况:① 遇到了墙;② 遇到了已经走过的路;
-- 在「无路可走」的时候,沿着原路返回,直到回到了还有未走过的路的路口,尝试继续走没有走过的路径;
-- 有一些路径没有走到,这是因为找到了出口,程序就停止了;
-- 「深度优先遍历」也叫「深度优先搜索」,遍历是行为的描述,搜索是目的(用途);
-- 遍历不是很深奥的事情,把 **所有** 可能的情况都看一遍,才能说「找到了目标元素」或者「没找到目标元素」。遍历也称为 **穷举**,穷举的思想在人类看来虽然很不起眼,但借助 **计算机强大的计算能力**,穷举可以帮助我们解决很多专业领域知识不能解决的问题。
-
-
-
-### 初识「搜索」
-
-「遍历」和「搜索」可以看作是两个等价概念,通过遍历 **所有** 的可能的情况达到搜索的目的。遍历是手段,搜索是目的。因此「深度优先遍历」也叫「深度优先搜索」。
-
-
-
-### 树的深度优先遍历
-
-我们以「二叉树」的深度优先遍历为例,向大家介绍树的深度优先遍历。
-
-二叉树的深度优先遍历从「根结点」开始,依次 「递归地」 遍历「左子树」的所有结点和「右子树」的所有结点。
-
-
-
-事实上,「根结点 → 右子树 → 左子树」也是一种深度优先遍历的方式,为了符合人们「先左再右」的习惯。如果没有特别说明,树的深度优先遍历默认都按照 「根结点 → 左子树 → 右子树」 的方式进行。
-
-**二叉树深度优先遍历的递归终止条件**:遍历完一棵树的 **所有** 叶子结点,等价于遍历到 **空结点**。
-
-
-
-二叉树的深度优先遍历还可以分为:前序遍历、中序遍历和后序遍历。
-
-1. 前序遍历
-
- 对于任意一棵子树,先输出根结点,再递归输出左子树的 所有 结点、最后递归输出右子树的 所有 结点。上图前序遍历的结果就是深度优先遍历的结果:[0、1、3、4、7、2、5、8、9、6、10]。
-
-2. 中序遍历
-
- 对于任意一棵子树,先递归输出左子树的 所有 结点,然后输出根结点,最后递归输出右子树的 所有 结点。上图中序遍历的结果是:[3、1、7、4、0、8、5、9、2、10、6]。
-
-3. 后序遍历(重要)
- 对于任意一棵子树,总是先递归输出左子树的 所有 结点,然后递归输出右子树的 所有 结点,最后输出根结点。后序遍历体现的思想是:先必需得到左右子树的结果,才能得到当前子树的结果,这一点在解决一些问题的过程中非常有用。上图后序遍历的结果是:[3、7、4、1、8、9、5、10、6、2、0]。
-
-> 友情提示:后序遍历是非常重要的遍历方式,解决很多树的问题都采用了后序遍历的思想,请大家务必重点理解「后序遍历」一层一层向上传递信息的遍历方式。并在做题的过程中仔细体会「后序遍历」思想的应用。
-
-4. 为什么前、中、后序遍历都是深度优先遍历
-
- 可以把树的深度优先遍历想象成一只蚂蚁,从根结点绕着树的外延走一圈。每一个结点的外延按照下图分成三个部分:前序遍历是第一部分,中序遍历是第二部分,后序遍历是第三部分。
-
-
-
-只看结点的第一部分(红色区域),深度优先遍历到的结点顺序就是「前序遍历」的顺序
-
-
-
-只看结点的第二部分(黄色区域),深度优先遍历到的结点顺序就是「中序遍历」的顺序
-
-
-
-
-
-只看结点的第三部分(绿色区域),深度优先遍历到的结点顺序就是「后序遍历」的顺序
-
-
-
-5. 重要性质
-
- 根据定义不难得到以下性质。
-
- - 性质 1:二叉树的 前序遍历 序列,根结点一定是 最先 访问到的结点;
- - 性质 2:二叉树的 后序遍历 序列,根结点一定是 最后 访问到的结点;
- - 性质 3:根结点把二叉树的 中序遍历 序列划分成两个部分,第一部分的所有结点构成了根结点的左子树,第二部分的所有结点构成了根结点的右子树。
-
-> 友情提示:根据这些性质,可以完成「力扣」第 105 题、第 106 题,这两道问题是面试高频问题,请大家务必掌握。
-
-
-
-### 图的深度优先遍历
-
-深度优先遍历有「回头」的过程,在树中由于不存在「环」(回路),对于每一个结点来说,每一个结点只会被递归处理一次。而「图」中由于存在「环」(回路),就需要 记录已经被递归处理的结点(通常使用布尔数组或者哈希表),以免结点被重复遍历到。
-
-
-
-### 练习
-
-下面这些练习可能是大家在入门「树」这个专题的过程中做过的问题,以前我们在做这些问题的时候可以总结为:树的问题可以递归求解。现在我们可以用「深度优先遍历」的思想,特别是「后序遍历」的思想重新看待这些问题。
-
-请大家通过这些问题体会 「**如何设计递归函数的返回值**」 帮助我们解决问题。并理解这些简单的问题其实都是「深度优先遍历」的思想中「后序遍历」思想的体现,真正程序在执行的时候,是通过「一层一层向上汇报」的方式,最终在根结点汇总整棵树遍历的结果。
-
-1. 完成「力扣」第 104 题:二叉树的最大深度(简单):设计递归函数的返回值;
-2. 完成「力扣」第 111 题:二叉树的最小深度(简单):设计递归函数的返回值;
-3. 完成「力扣」第 112 题:路径总和(简单):设计递归函数的返回值;
-4. 完成「力扣」第 226 题:翻转二叉树(简单):前中后序遍历、广度优先遍历均可,中序遍历有一个小小的坑;
-5. 完成「力扣」第 100 题:相同的树(简单):设计递归函数的返回值;
-6. 完成「力扣」第 101 题:对称二叉树(简单):设计递归函数的返回值;
-7. 完成「力扣」第 129 题:求根到叶子节点数字之和(中等):设计递归函数的返回值。
-8. 完成「力扣」第 236 题:二叉树的最近公共祖先(中等):使用后序遍历的典型问题。
-
-请大家完成下面这些树中的问题,加深对前序遍历序列、中序遍历序列、后序遍历序列的理解。
-
-9. 完成「力扣」第 105 题:从前序与中序遍历序列构造二叉树(中等);
-10. 完成「力扣」第 106 题:从中序与后序遍历序列构造二叉树(中等);
-11. 完成「力扣」第 1008 题:前序遍历构造二叉搜索树(中等);
-
-12. 完成「力扣」第 1028 题:从先序遍历还原二叉树(困难)。
-
-> 友情提示:需要用到后序遍历思想的一些经典问题,这些问题可能有一些难度,可以不用急于完成。先做后面的问题,见多了类似的问题以后,慢慢理解「后序遍历」一层一层向上汇报,在根结点汇总的遍历思想。
-
-
-
-### 总结
-
-- 遍历可以用于搜索,思想是穷举,遍历是实现搜索的手段;
-- 树的「前、中、后」序遍历都是深度优先遍历;
-- 树的后序遍历很重要;
-- 由于图中存在环(回路),图的深度优先遍历需要记录已经访问过的结点,以避免重复访问;
-- 遍历是一种简单、朴素但是很重要的算法思想,很多树和图的问题就是在树和图上执行一次遍历,在遍历的过程中记录有用的信息,得到需要结果,区别在于为了解决不同的问题,在遍历的时候传递了不同的 与问题相关 的数据。
-
-
-
-## 二、数据结构-栈
-
-
-
-### 深度优先遍历的两种实现方式
-
-在深度优先遍历的过程中,需要将 当前遍历到的结点 的相邻结点 暂时保存 起来,以便在回退的时候可以继续访问它们。遍历到的结点的顺序呈现「后进先出」的特点,因此 深度优先遍历可以通过「栈」实现。
-
-再者,深度优先遍历有明显的递归结构。我们知道支持递归实现的数据结构也是栈。因此实现深度优先遍历有以下两种方式:
-
-- 编写递归方法;
-- 编写栈,通过迭代的方式实现。
-
-
-
-
-
-### 二叉树三种遍历方式的非递归实现
-
-我们通过例题的方式向大家展现一种使用栈模拟的二叉树深度优先遍历的过程。但是要向大家说的是:
-
-- 并不是所有的递归(深度优先遍历)的问题都可以很方便地使用「栈」实现,了解非递归实现可以作为编程练习;
-- 虽然「递归调用」在一些编程语言中会造成系统资源开销,性能不如非递归好,还有可能造成「栈溢出」的风险,但是 在工程实践 中,递归方法的可读性更强,更易于理解和以后的维护,因此没有必要苛求必需要将递归方法转换成为非递归实现。
-
-例 1:「力扣」第 144 题:二叉树的前序遍历(简单)
-
-思路分析:递归方法相信大家都会。这里介绍使用栈模拟递归的过程。
-
-对于二叉树的遍历,每一个结点有两种处理方式:
-
-- 输出该结点;
-- 递归处理该结点。
-
-我们可以在结点存入栈的时候附加一个「指令信息」,ADDTORESULT 表示输出该结点(添加到结果集),GO 表示递归处理该结点。在栈顶元素的弹出的时候,读取「指令信息」,遇到 GO 的时候,就将当前结点的左、右孩子结点按照「前序遍历」(根 -> 左 -> 右)的「倒序」的方式压入栈中。
-
-「倒序」是因为栈处理元素的顺序是「后进先出」,弹栈的时候才会按照我们想要的顺序输出到结果集。
-
-读者可以结合下面的参考代码理解这种使用「栈」模拟了递归(深度优先遍历)的思想。
-
-注意:
-
-使用栈模拟递归实现的方式并不唯一,这里介绍的栈模拟的方法可以迁移到「力扣」第 94 题(二叉树的中序遍历)和「力扣」第 145 题(二叉树的后序遍历),例 2 和例 3 我们不再过多描述;
-感兴趣的朋友还可以参考这些问题的官方题解了解更多使用栈模拟深度优先遍历的实现。
-
-参考代码 1:递归
-
-```java
-public class Solution {
-
- public List preorderTraversal(TreeNode root) {
- List res = new ArrayList<>();
- dfs(root, res);
- return res;
- }
-
- private void dfs(TreeNode treeNode, List res) {
- if (treeNode == null) {
- return;
- }
- res.add(treeNode.val);
- dfs(treeNode.left, res);
- dfs(treeNode.right, res);
- }
-}
-```
-
-复杂度分析:
-
-时间复杂度:O(N),这里 NN 为二叉树的结点总数;
-空间复杂度:O(N),栈的深度为需要使用的空间的大小,极端情况下,树成为一个链表的时候,栈的深度达到最大。
-
-参考代码 2:使用栈模拟
-
-```java
-public class Solution {
-
- private enum Action {
- /**
- * 如果当前结点有孩子结点(左右孩子结点至少存在一个),执行 GO
- */
- GO,
- /**
- * 添加到结果集(真正输出这个结点)
- */
- ADDTORESULT
- }
-
- private class Command {
- private Action action;
- private TreeNode node;
-
- /**
- * 将动作类与结点类封装起来
- *
- * @param action
- * @param node
- */
- public Command(Action action, TreeNode node) {
- this.action = action;
- this.node = node;
- }
- }
-
- public List preorderTraversal(TreeNode root) {
- List res = new ArrayList<>();
- if (root == null) {
- return res;
- }
-
- Deque stack = new ArrayDeque<>();
- stack.addLast(new Command(Action.GO, root));
- while (!stack.isEmpty()) {
- Command command = stack.removeLast();
- if (command.action == Action.ADDTORESULT) {
- res.add(command.node.val);
- } else {
- // 特别注意:以下的顺序与递归执行的顺序反着来,即:倒过来写的结果
- // 前序遍历:根结点、左子树、右子树、
- // 添加到栈的顺序:右子树、左子树、根结点
- if (command.node.right != null) {
- stack.add(new Command(Action.GO, command.node.right));
- }
- if (command.node.left != null) {
- stack.add(new Command(Action.GO, command.node.left));
- }
- stack.add(new Command(Action.ADDTORESULT, command.node));
- }
- }
- return res;
- }
-}
-```
-
-复杂度分析:(同参考代码 1)。
-
-说明:在理解了例 1 以后,例 2 和 例 3 可以类似地完成,我们不再对例 2 和例 3 进行详解、对复杂度展开分析,只给出参考代码。
-
-
-
-## 三、深度优先遍历的应用
-
-
-
-### 应用 1:获得图(树)的一些属性
-
-在一些树的问题中,其实就是通过一次深度优先遍历,获得树的某些属性。例如:「二叉树」的最大深度、「二叉树」的最小深度、平衡二叉树、是否 BST。在遍历的过程中,通常需要设计一些变量,一边遍历,一边更新设计的变量的值。
-
-「力扣」第 129 题:求根到叶子节点数字之和(中等)
-
-```java
-public int sumNumbers(TreeNode root) {
- return dfs(root,0);
-}
-
-public int dfs(TreeNode root,int prevSum){
- if(root == null){
- return 0;
- }
- int sum = prevSum * 10 + root.val;
- if(root.left == null && root.right == null){
- return sum;
- }else{
- return dfs(root.left,sum) + dfs(root.right,sum);
- }
-}
-```
-友情提示:既然可以一层一层得到一个数,广度优先遍历也是很自然的想法,读者可以尝试使用广度优先遍历的思想完成该问题。
-
-
-
-### 应用 2:计算无向图的连通分量
-
-「力扣」第 323 题:无向图中连通分量的数目(中等)
-
-思路分析:
-
-首先需要对输入数组进行处理,由于 n 个结点的编号从 0 到 n - 1 ,因此使用「嵌套数组」表示邻接表即可(具体实现见代码);
-然后遍历每一个顶点,对每一个顶点执行一次深度优先遍历,注意:在遍历的过程中使用 visited 布尔数组记录已经遍历过的结点。
-
-```java
-public class Solution {
-public int countComponents(int n, int[][] edges) {
- // 第 1 步:构建图
- List[] adj = new ArrayList[n];
- for (int i = 0; i < n; i++) {
- adj[i] = new ArrayList<>();
- }
- // 无向图,所以需要添加双向引用
- for (int[] edge : edges) {
- adj[edge[0]].add(edge[1]);
- adj[edge[1]].add(edge[0]);
- }
-
- // 第 2 步:开始深度优先遍历
- int count = 0;
- boolean[] visited = new boolean[n];
- for (int i = 0; i < n; i++) {
- if (!visited[i]) {
- dfs(adj, i, visited);
- count++;
- }
- }
- return count;
-}
-
-/**
- * @param adj 邻接表
- * @param u 从顶点 u 开始执行深度优先遍历
- * @param visited 记录某个结点是否被访问过
- */
-private void dfs(List[] adj, int u, boolean[] visited) {
- visited[u] = true;
- List successors = adj[u];
- for (int successor : successors) {
- if (!visited[successor]) {
- dfs(adj, successor, visited);
- }
- }
-}
- }
-```
-
-
-### 应用 3:检测图中是否存在环
-
-我们分别通过两个例子讲解「无向图」中环的检测和「有向图」中环的检测(是不是要讲一下拓扑排序)。
-
-例 3:「力扣」第 684 题:冗余连接(中等)
-
-```java
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class Solution {
-
- private Map> graph;
- private Set visited;
-
- public int[] findRedundantConnection(int[][] edges) {
- this.graph = new HashMap<>();
- this.visited = new HashSet<>();
-
- // 遍历每一条边
- for (int[] edge : edges) {
- int u = edge[0];
- int v = edge[1];
- if (graph.containsKey(u) && graph.containsKey(v)) {
- visited.clear();
- // 深度优先遍历该图,判断 u 到 v 之间是否已经存在了一条路径
- if (dfs(u, v)) {
- return edge;
- }
- }
- // 所有相邻顶点都找不到回路,才向图中添加这条边,由于是无向图,所以要添加两条边
- addEdge(u, v);
- addEdge(v, u);
- }
- return null;
- }
-
- private void addEdge(int u, int v) {
- if (graph.containsKey(u)) {
- graph.get(u).add(v);
- return;
- }
- List successors = new ArrayList<>();
- successors.add(v);
- graph.put(u, successors);
- }
-
-
- /**
- * 从 source 开始进行深度优先遍历,看看是不是能够找到一条到 target 的回路
- *
- * @param source
- * @param target
- * @return 找不到回路返回 false
- */
- private boolean dfs(int source, int target) {
- if (source == target) {
- return true;
- }
- visited.add(source);
- // 遍历 source 的所有相邻顶点
- for (int adj : graph.get(source)) {
- if (!visited.contains(adj)) {
- if (dfs(adj, target)) {
- return true;
- }
- }
- }
- // 所有相邻顶点都找不到,才能返回 false
- return false;
- }
-}
-```
-
-> 友情提示:该问题还可以使用拓扑排序完成。事实上,无向图找是否存在环是「并查集」这个数据结构的典型应用。
-
-
-
-「力扣」第 802 题:找到最终的安全状态(中等)
-
-```java
-import java.util.ArrayList;
-import java.util.List;
-
-public class Solution {
-
- /**
- * 使用 Boolean 利用了 null 表示还未计算出结果
- * true 表示从当前顶点出发的所有路径有回路
- * false 表示从当前顶点出发的所有路径没有回路
- */
- private Boolean[] visited;
-
- public List eventualSafeNodes(int[][] graph) {
- int len = graph.length;
- visited = new Boolean[len];
- List res = new ArrayList<>();
- for (int i = 0; i < len; ++i) {
- if (dfs(i, graph)) {
- continue;
- }
- res.add(i);
- }
- return res;
-
- }
-
- /**
- * @param u
- * @param graph
- * @return 从顶点 u 出发的所有路径是不是有一条能够回到 u,有回路就返回 true
- */
- private boolean dfs(int u, int[][] graph) {
- if (visited[u] != null) {
- return visited[u];
- }
- // 先默认从 u 出发的所有路径有回路
- visited[u] = true;
- // 结点 u 的所有后继结点都不能回到自己,才能认为结点 u 是安全的
- for (int successor : graph[u]) {
- if (dfs(successor, graph)) {
- return true;
- }
- }
- // 注意:这里需要重置
- visited[u] = false;
- return false;
- }
-}
-```
-复杂度分析:
-
-- 时间复杂度:O(V + E)O(V+E),这里 VV 为图的顶点总数,EE 为图的边数;
-
-- 空间复杂度:O(V + E)O(V+E)。
-
-总结:
-
-- 在声明变量、设计递归函数的时候,一定要明确递归函数的变量的定义和递归函数的返回值,写上必要的注释,这样在编写代码逻辑的时候,才不会乱。
-
-> 友情提示:还可以使用拓扑排序(借助入度和广度优先遍历)或者并查集两种方法完成。
-
-
-
-### 应用 5:拓扑排序
-
-「力扣」第 210 题:课程表 II(中等)
-
-> 现在你总共有 n 门课需要选,记为 0 到 n-1。
->
-> 在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]。
->
-> 给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
->
-> 可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
->
-> 示例 1:
->
-> ```
-> 输入: 2, [[1,0]]
-> 输出: [0,1]
-> 解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
-> ```
->
->
-> 示例 2:
->
-> ```
-> 输入: 4, [[1,0],[2,0],[3,1],[3,2]]
-> 输出: [0,1,2,3] or [0,2,1,3]
-> 解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
-> ```
-
-说明:
-
-1. 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。
-
-2. 你可以假定输入的先决条件中没有重复的边。
-
-提示:
-
-- 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
-
-思路分析:
-
-- 题目中提示已经说得很清楚了,要求我们在「有向图」中检测是否有环,如果没有环,则输出拓扑排序的结果;
-- 所谓「拓扑排序」需要保证:① 每一门课程只出现一次;② 必需保证先修课程的顺序在所有它的后续课程的前面;
-- 拓扑排序的结果不唯一,并且只有「有向无环图」才有拓扑排序;
-- 关键要把握题目的要求:「必需保证先修课程的顺序在所有它的后续课程的前面」,「所有」关键字提示我们可以使用「 遍历」的思想遍历当前课程的 所有 后续课程,并且保证这些课程之间的学习顺序不存在「环」,可以使用「深度优先遍历」或者「广度优先遍历」;
-- 我们这里给出「深度优先遍历」的实现代码,注意:需要在当前课程的所有 后续课程 结束以后才输出当前课程,所以 ① 收集结果的位置应该在「后序」的位置(类比二叉树的后序遍历);② 后序遍历的结果需要逆序才是拓扑排序的结果;
-- 事实上,「拓扑排序」问题使用「广度优先遍历」的思想和实现是更经典的做法,我们放在「广度优先遍历」专题里向大家介绍。
-
-编码前的说明:
-
-深度优先遍历的写法需要注意:
-
-- 递归函数返回值的意义:这里返回 true 表示在有向图中找到了环,返回 false 表示没有环;
-- 我们扩展了布尔数组 visited 的含义,如果在无向图中,只需要 true 和 false 两种状态。在有向图中,为了检测是否存在环,我们新增一个状态,用于表示在对一门课程进行深度优先遍历的过程中,已经被标记,使用整数 1 表示。原来的 false 用整数 0 表示,含义为还未访问;原来的 true 用整数 2 表示,含义为「已经访问」,确切地说是「当前课程的所有后续课程」已经被访问。
-
-```java
-public class Solution {
-
- public int[] findOrder(int numCourses, int[][] prerequisites) {
- // 步骤 1:构建邻接表
- Set[] adj = new HashSet[numCourses];
- for (int i = 0; i < numCourses; i++) {
- adj[i] = new HashSet<>();
- }
- int pLen = prerequisites.length;
- for (int i = 0; i < pLen; i++) {
- // 后继课程
- int second = prerequisites[i][0];
- // 先行课程
- int first = prerequisites[i][1];
- // 注意 dfs 中,后继课程作为 key,前驱课程作为 value,这种方式不符合邻接表的习惯,邻接表总是通过前驱得到后继
- adj[second].add(first);
- }
-
- // 步骤二:对每一个结点执行一次深度优先遍历
- // 0 表示没有访问过,对应于 boolean 数组里的 false
- // 1 表示已经访问过,新增状态,如果 dfs 的时候遇到 1 ,表示当前遍历的过程中形成了环
- // 2 表示当前结点的所有后继结点已经遍历完成,对应于 boolean 数组里的 true
- int[] visited = new int[numCourses];
-
- List res = new ArrayList<>();
- for (int i = 0; i < numCourses; i++) {
- // 对每一个结点执行一次深度优先遍历
- if (dfs(i, adj, visited, res)) {
- return new int[]{};
- }
- }
- return res.stream().mapToInt(i -> i).toArray();
- }
-
- /**
- * @param current
- * @param adj
- * @param visited
- * @param res
- * @return true 表示有环,false 表示没有环
- */
- private boolean dfs(int current, Set[] adj,
- int[] visited, List res) {
-
- if (visited[current] == 1) {
- return true;
- }
- if (visited[current] == 2) {
- return false;
- }
-
- visited[current] = 1;
- for (Integer successor : adj[current]) {
- if (dfs(successor, adj, visited, res)) {
- // 如果有环,返回空数组
- return true;
- }
- }
-
- // 注意:在「后序」这个位置添加到结果集
- res.add(current);
- visited[current] = 2;
- // 所有的后继结点都遍历完成以后,都没有遇到重复,才可以说没有环
- return false;
- }
-}
-```
-复杂度分析:
-
-- 时间复杂度:O(V + E),这里 V 表示课程总数,E 表示课程依赖关系总数,对每一个顶点执行一次深度优先遍历,所有顶点需要遍历的操作总数与边总数有关;
-- 空间复杂度:O(V + E),邻接表的大小为 V + E,递归调用栈的深度最多为 V,因此空间复杂度是 O(V + E)。
-
-
-
-## 四、回溯算法
-
-
-
-### 回溯算法是深度优先遍历思想的应用
-
-回溯算法是一种通过不断 尝试 ,搜索一个问题的一个解或者 所有解 的方法。在求解的过程中,如果继续求解不能得到题目要求的结果,就需要 回退 到上一步尝试新的求解路径。回溯算法的核心思想是:**在一棵 隐式的树(看不见的树) 上进行一次深度优先遍历**。
-
-我们通过一道经典的问题 N 皇后问题,向大家介绍「回溯算法」的思想。
-
-
-
-### 例题:「力扣」第 51 题:N 皇后(困难)
-
-n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
-
-
-
-上图为 8 皇后问题的一种解法。
-
-给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
-
-每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
-
-```
-输入:4
-输出:[
- [".Q..", // 解法 1
- "...Q",
- "Q...",
- "..Q."],
-
- ["..Q.", // 解法 2
- "Q...",
- "...Q",
- ".Q.."]
-]
-解释: 4 皇后问题存在两个不同的解法。
-```
-
-提示:
-
-- 皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。
-
-思路分析:解决这个问题的思路是尝试每一种可能,然后逐个判断。只不过回溯算法按照一定的顺序进行尝试,在一定不可能得到解的时候进行剪枝,进而减少了尝试的可能。下面的幻灯片展示了整个搜索的过程。
-
-
-
-
-
-**在遍历的过程中记录已经放置的皇后的位置**
-
-由于我们需要根据前面已经放置的皇后的位置,来决定当前位置是否可以放置皇后,因此记住已经放置的皇后的位置就很重要。
-
-- 由于我们一行一行考虑放置皇后,摆放的这些皇后肯定不在同一行;
-- 为了避免它们在同一列,需要一个长度为 NN 的布尔数组 cols,已经放置的皇后占据的列,就需要在对应的列的位置标记为 true;
-- 还需要考虑「任何两个皇后不能位于同一条斜线上」,下面的图展示了位于一条斜线上的皇后的位置特点。
-
-
-
-为此,我们需要一个表示主对角线方向的布尔数组 main(Main diagonal,长度为 2N-12N−1),如果某个单元格放放置了一个皇后,就需要将对应的主对角线标记为 true。注意:由于有 3 个方向的横坐标 - 纵坐标的结果为负值,可以统一地为每一个横坐标 - 纵坐标的结果增加一个偏移,具体请见参考代码 1。
-
-
-
-同理,我们还需要一个表示副对角线方向的布尔数组 sub(Sub diagonal,长度为 2N-12N−1),如果某个单元格放放置了一个皇后,就需要将对应的副对角线标记为 true。
-
-```java
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.List;
-
-public class Solution {
-
- private int n;
- /**
- * 记录某一列是否放置了皇后
- */
- private boolean[] col;
- /**
- * 记录主对角线上的单元格是否放置了皇后
- */
- private boolean[] main;
- /**
- * 记录了副对角线上的单元格是否放置了皇后
- */
- private boolean[] sub;
-
- private List> res;
-
- public List> solveNQueens(int n) {
- res = new ArrayList<>();
- if (n == 0) {
- return res;
- }
-
- // 设置成员变量,减少参数传递,具体作为方法参数还是作为成员变量,请参考团队开发规范
- this.n = n;
- this.col = new boolean[n];
- this.main = new boolean[2 * n - 1];
- this.sub = new boolean[2 * n - 1];
- Deque path = new ArrayDeque<>();
- dfs(0, path);
- return res;
- }
-
- private void dfs(int row, Deque path) {
- if (row == n) {
- // 深度优先遍历到下标为 n,表示 [0.. n - 1] 已经填完,得到了一个结果
- List board = convert2board(path);
- res.add(board);
- return;
- }
-
- // 针对下标为 row 的每一列,尝试是否可以放置
- for (int j = 0; j < n; j++) {
- if (!col[j] && !main[row - j + n - 1] && !sub[row + j]) {
- path.addLast(j);
- col[j] = true;
- main[row - j + n - 1] = true;
- sub[row + j] = true;
- dfs(row + 1, path);
- sub[row + j] = false;
- main[row - j + n - 1] = false;
- col[j] = false;
- path.removeLast();
- }
- }
- }
-
- private List convert2board(Deque path) {
- List board = new ArrayList<>();
- for (Integer num : path) {
- StringBuilder row = new StringBuilder();
- row.append(".".repeat(Math.max(0, n)));
- row.replace(num, num + 1, "Q");
- board.add(row.toString());
- }
- return board;
- }
-}
-```
-
-复杂度分析:
-
-- 时间复杂度:O(N!)O(N!),这里 NN 为皇后的个数,这里讨论的时间复杂度很宽松,第一行皇后可以摆放的位置有 NN 个,第二行皇后可以摆放的位置有 N - 1N−1 个,依次类推下去,按照分步计数乘法原理,一共有 N!N! 种可能;
-
-- 空间复杂度:O(N)O(N),递归调用栈的深度最多为 NN,三个布尔数组的长度分别为 NN、2N+12N+1、2N+12N+1,都是 NN 的线性组合。
-
-其实,判断是否重复,可以使用哈希表,下面给出的参考代码 2 就使用了哈希表判断是否重复,可以不用处理主对角线方向上「横坐标 - 纵坐标」的下标偏移。但事实上,哈希表底层也是数组。
-
-
-
-### 树形问题
-
-回溯算法其实是在一棵隐式的树或者图上进行了一次深度优先遍历,我们在解决问题的过程中需要把问题抽象成一个树形问题。充分理解树形问题最好的办法就是用一个小的测试用例,在纸上画出树形结构图,然后再针对树形结构图进行编码。
-
-重要的事情我们说三遍:画图分析很重要、画图分析很重要、画图分析很重要。
-
-要理解「回溯算法」的递归前后,变量需要恢复也需要想象代码是在一个树形结构中执行深度优先遍历,回到以前遍历过的结点,变量需要恢复成和第一次来到该结点的时候一样的值。
-
-另一个理解回溯算法执行流程的重要方法是:在递归方法执行的过程中,将涉及到的变量的值打印出来看,观察变量的值的变化。
-
-> 友情提示:画图分析问题是思考算法问题的重要方法,画图这个技巧在解决链表问题、回溯算法、动态规划的问题上都有重要的体现,请大家不要忽视「画图」这个简单的分析问题的方法,很多时候思路就出现在我们在草稿纸上写写画画以后。
-
-
-
-### 回溯算法问题的问法
-
-问「一个问题 **所有的** 解」一般考虑使用回溯算法。因此回溯算法也叫「暴力搜索」,但不同于最粗暴的多个 `for` 循环,回溯算法是有方向的遍历。
-
-
-
-### 再谈「搜索」
-
-计算机擅长做的事情是「计算」,即「做重复的事情」。能用编程的方法解决的问题通常 结构相同,问题规模不同。因此,我们解决一个问题的时候,通常需要将问题一步一步进行拆解,把一个大问题拆解为结构相同的若干个小问题。
-
-友情提示:我们介绍「状态」和「状态空间」这两个概念是为了方便后面的问题描述,其实大家在完成了一定练习以后对这两个概念就会有形象的理解。如果一开始不理解这些概念完全可以跳过。
-
-##### 「状态」和「状态空间」
-
-为了区分解决问题的不同阶段、不同规模,我们可以通过语言描述进行交流。在算法的世界里,是通过变量进行描述的,不同的变量的值就代表了解决一个实际问题中所处的不同的阶段,这些变量就叫做「状态变量」。所有的状态变量构成的集合称为「状态空间」。
-
-友情提示:「空间」这个词经常代表的含义是「所有」。在《线性代数》里,线性空间(向量空间)就是规定了「加法」和「数乘」,且对这两种运算封闭的 所有 元素的集合。
-
-##### 不同状态之间的联系形成图(树)结构
-
-我们可以把某种规模的问题描述想象成一个结点。由于规模相近的问题之间存在联系,我们把有联系的结点之间使用一条边连接,因此形成的状态空间就是一张图。
-
-树结构有唯一的起始结点(根结点),且不存在环,树是特殊的图。这一章节绝大多数的问题都从一个基本的问题出发,拆分成多个子问题,并且继续拆分的子问题没有相同的部分,因此这一章节遇到的绝大多数问题的状态空间是一棵树。
-
-我们要了解这个问题的状态空间,就需要通过 遍历 的方式。正是因为通过遍历,我们能够访问到状态空间的所有结点,因此可以获得一个问题的 所有 解。
-
-
-
-### 为什么叫「回溯」(难点)
-
-而「回溯」就是 深度优先遍历 状态空间的过程中发现的特有的现象,程序会回到以前访问过的结点。而程序在回到以前访问过的结点的时候,就需要将状态变量恢复成为第一次来到该结点的值。
-
-在代码层面上,在递归方法结束以后,执行递归方法之前的操作的 逆向操作 即可。
-
-> 友情提示:理解回溯算法的「回溯」需要基于一定的练习,可以不必一开始就理解透彻。另外,理解「回溯算法」的一个重要技巧是 在程序中打印状态变量进行观察,一步一步看到变量的变化。
-
-
-
-### 回溯算法的实现细节
-
-#### 解释递归后面状态重置是怎么回事
-
-- 当回到上一级的时候,所有的状态变量需要重置为第一次来到该结点的状态,这样继续尝试新的选择才有意义;
-- 在代码层面上,需要在递归结束以后,添加递归之前的操作的逆向操作;
-
-#### 基本类型变量和对象类型变量的不同处理
-
-基本类型变量每一次向下传递的时候的行为是复制,所以无需重置;
-对象类型变量在遍历的全程只有一份,因此再回退的时候需要重置;
-类比于 Java 中的 方法参数 的传递机制:
-基本类型变量在方法传递的过程中的行为是复制,每一次传递复制了参数的值;
-对象类型变量在方法传递的过程中复制的是对象地址,对象全程在内存中共享地址。
-
-#### 字符串问题的特殊性
-
-如果使用 + 拼接字符串,每一次拼接产生新的字符串,因此无需重置;
-如果使用 StringBuilder 拼接字符串,整个搜索的过程 StringBuilder 对象只有一份,需要状态重置。
-
-#### 为什么不是广度优先遍历
-
-广度优先遍历每一层需要保存所有的「状态」,如果状态空间很大,需要占用很大的内存空间;
-深度优先遍历只要有路径可以走,就继续尝试走新的路径,不同状态的差距只有一个操作,而广度优先遍历在不同的层之前,状态差异很大,就不能像深度优先遍历一样,可以 使用一份状态变量去遍历所有的状态空间,在合适的时候记录状态的值就能得到一个问题的所有的解。
-
-
-
-### 练习
-
-1. 完成「力扣」第 46 题:全排列(中等);
-2. 完成「力扣」第 37 题:数独(困难);
-
-下面是字符串的搜索问题,完成这些问题可以帮助理解回溯算法的实现细节。
-
-1. 完成「力扣」第 22 题:括号生成(中等);
-2. 完成「力扣」第 17 题:电话号码的字母组合(中等);
-3. 完成「力扣」第 784 题:字母大小写全排列(中等)。
-
-
-
-## 五、剪枝
-
-
-
-### 剪枝的必要性
-
-剪枝的想法是很自然的。回溯算法本质上是遍历算法,如果 在遍历的过程中,可以分析得到这样一条分支一定不存在需要的结果,就可以跳过这个分支。
-
-发现剪枝条件依然是通过举例的例子,画图分析,即:通过具体例子抽象出一般的剪枝规则。通常可以选取一些较典型的例子,以便抽象出一般规律。
-
-> 友情提示:阅读下面的文字,可能会有一些晦涩,建议大家了解思路,通过对具体例子的分析,逐渐分析出解决这个问题的细节。
-
-### 「剪枝」技巧例举
-
-#### 技巧 1:按照一定顺序搜索
-
-按照顺序搜索其实也是去除重复结果的必要条件。
-
-「力扣」第 47 题:全排列 II
-
-> 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
->
-> 示例 1:
->
-> ```
-> 输入:nums = [1,1,2]
-> 输出:
-> [[1,1,2],
-> [1,2,1],
-> [2,1,1]]
-> ```
->
->
-> 示例 2:
->
-> ```
-> 输入:nums = [1,2,3]
-> 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
-> ```
-
-思路分析:
-
-- 这道题基于「力扣」第 46 题(全排列)的思想完成,首先依然是先画出树形结构,然后编写深度优先遍历的代码,在遍历的过程中收集所有的全排列;
-- 与「力扣」第 46 题(全排列)不同的是:输入数组中有重复元素,如果还按照第 46 题的写法做,就会出现重复列表;
-- 如果搜索出来结果列表,再在结果列表里去重,比较相同的列表是一件比较麻烦的事情,我们可以 ①:依次对列表排序,再逐个比较列表中的元素;② 将列表封装成为类,使用哈希表去重的方式去掉重复的列表。这两种方式编码都不容易实现;
-- 既然需要排序,我们可以在一开始的时候,就对输入数组进行排序,在遍历的过程中,通过一定剪枝条件,发现一定会搜索到重复元素的结点,跳过它,这样在遍历完成以后,就能得到不重复的列表。
-
-我们画出树形图,找出它们重复的部分,进而发现产生重复的原因。
-
-
-
-产生重复列表的原因:
-
-- 很容易看到,在树的同一层,如果当前考虑的数字相同,就有可能搜索到重复的结果(前提:输入数组按照升序排序),因此剪枝条件为 nums[i] == nums[i - 1] 这里为了保证数组下标不越界,前提是 i > 0;
-- 光有这个条件还不够,我们观察下面两个分支,中间被着重标注的分支,满足 nums[i] == nums[i - 1] 并且 nums[i - 1] 还未被使用,就下来由于还要使用 1 一定会与前一个分支搜索出的结果重复;
-- 而左边被着重标注的分支,也满足 nums[i] == nums[i - 1] ,但是 nums[i - 1] 已经被使用,接下来不会再用到它,因此不会产生重复。
-
-
-
-```java
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Deque;
-import java.util.List;
-
-public class Solution {
-
- public List> permuteUnique(int[] nums) {
- int len = nums.length;
- List> res = new ArrayList<>();
- if (len == 0) {
- return res;
- }
-
- // 剪枝的前提是排序
- Arrays.sort(nums);
- boolean[] used = new boolean[len];
- // 使用 Deque 是 Java 官方 Stack 类的建议
- Deque path = new ArrayDeque<>(len);
- dfs(nums, 0, len, used, path, res);
- return res;
- }
-
- private void dfs(int[] nums, int index, int len, boolean[] used, Deque path, List> res) {
- if (index == len) {
- res.add(new ArrayList<>(path));
- return;
- }
-
- for (int i = 0; i < len; i++) {
- if (used[i]) {
- continue;
- }
-
- // 注意:理解 !used[i - 1],很关键
- // 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
- // 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
- if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
- continue;
- }
-
- used[i] = true;
- path.addLast(nums[i]);
- dfs(nums, index + 1, len, used, path, res);
- // 回溯部分的代码,和 dfs 之前的代码是对称的
- used[i] = false;
- path.removeLast();
- }
- }
-}
-```
-
-「力扣」第 39 题:(组合问题)
-
-> 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
->
-> candidates 中的数字可以无限制重复被选取。
->
-> 说明:
->
-> - 所有数字(包括 target)都是正整数。
-> - 解集不能包含重复的组合。
->
-> 示例 1:
->
-> ```
-> 输入:candidates = [2,3,6,7], target = 7,
-> 所求解集为:
-> [
-> [7],
-> [2,2,3]
-> ]
-> ```
-
-思路分析:有了之前问题的求解经验,我们 强烈建议 大家使用示例 1 ,以自己熟悉的方式画出树形结构,再尝试编码、通过调试的方式把这个问题做出来。
-
-- 可以从目标值 target = 7 开始,逐个减去 2 、3 、6 、7 ,把问题转化成使用 [2, 3, 6, 7] 能够得到的组合之和为 5、 4、 1、0 的所有列表,如果能够得到有效的列表,再加上减去的那个数,就是原始问题的一个列表,这是这个问题的递归结构;
-- 减去一个数以后,得到的数为 0 或者负数以后,就可以停止了,请大家想一想这是为什么。画好这棵树以后,我们关注叶子结点的值为 0 的结点,从根结点到叶子结点的一条路径,满足沿途减去的数的和为 target = 7;
-
-
-
-
-- 由于这个问题得到的结果是组合,[2, 2, 3]、[2, 3, 2] 与 [3, 2, 2] 只能作为一个列表在结果集里输出,我们依然是按照第 47 题的分析,在图中标注出这些重复的分支,发现剪枝的条件;
-
-
-
-- 去除重复的方法通常是按照一定的顺序考虑问题,我们观察重复的三个列表 [2, 2, 3]、[2, 3, 2] 与 [2, 3, 2] ,我们只需要一个,保留自然顺序 [2, 2, 3] 即可,于是我们可以指定如下规则:如果在深度较浅的层减去的数等于 a ,那么更深的层只能减去大于等于 a 的数(根据题意,一个元素可以使用多次,因此可以减去等于 a 的数),这样就可以跳过重复的分支,深度优先遍历得到的结果就不会重复;
-- 容易发现,如果减去一个数的值小于 0 ,就没有必要再减去更大的数,这也是可以剪枝的地方。
-
-```java
-public class Solution {
-
- public List> combinationSum(int[] candidates, int target) {
- int len = candidates.length;
- List> res = new ArrayList<>();
- if (len == 0) {
- return res;
- }
-
- // 排序是剪枝的前提
- Arrays.sort(candidates);
- Deque path = new ArrayDeque<>();
- dfs(candidates, 0, len, target, path, res);
- return res;
- }
-
- private void dfs(int[] candidates, int begin, int len, int target, Deque path, List> res) {
- // 由于进入更深层的时候,小于 0 的部分被剪枝,因此递归终止条件值只判断等于 0 的情况
- if (target == 0) {
- res.add(new ArrayList<>(path));
- return;
- }
-
- for (int i = begin; i < len; i++) {
- // 重点理解这里剪枝,前提是候选数组已经有序
- if (target - candidates[i] < 0) {
- break;
- }
-
- path.addLast(candidates[i]);
- dfs(candidates, i, len, target - candidates[i], path, res);
- path.removeLast();
- }
- }
-}
-```
-
-如果对这个问题研究比较深入,可以发现,其实只要保持深层结点不重复使用浅层结点使用过的数值即可,也就是说排序对于这道问题来说不是必须的,排序用于提速,而真正去除重复元素的技巧是:设置搜索起点,这是另一种意义上的按顺序搜索(搜索起点不回头)。下面的代码也可以通过系统测评。
-
-```java
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.List;
-
-public class Solution {
-
- public List> combinationSum(int[] candidates, int target) {
- int len = candidates.length;
- List> res = new ArrayList<>();
- if (len == 0) {
- return res;
- }
-
- Deque path = new ArrayDeque<>();
- dfs(candidates, 0, len, target, path, res);
- return res;
- }
-
- /**
- * @param candidates 候选数组
- * @param begin 搜索起点
- * @param len 冗余变量,是 candidates 里的属性,可以不传
- * @param target 每减去一个元素,目标值变小
- * @param path 从根结点到叶子结点的路径,是一个栈
- * @param res 结果集列表
- */
- private void dfs(int[] candidates, int begin, int len, int target, Deque path, List> res) {
- // target 为负数和 0 的时候不再产生新的孩子结点
- if (target < 0) {
- return;
- }
- if (target == 0) {
- res.add(new ArrayList<>(path));
- return;
- }
-
- // 重点理解这里从 begin 开始搜索的语意
- for (int i = begin; i < len; i++) {
- path.addLast(candidates[i]);
- // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错
- dfs(candidates, i, len, target - candidates[i], path, res);
- // 状态重置
- path.removeLast();
- }
- }
-}
-```
-
-「力扣」第 77 题:子集(中等)
-
-> 给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
->
-> 示例:
->
-> ```
-> 输入: n = 4, k = 2
-> 输出:
-> [
-> [2,4],
-> [3,4],
-> [2,3],
-> [1,2],
-> [1,3],
-> [1,4],
-> ]
-> ```
-
-思路分析:
-
-- 依然是 强烈建议 大家在纸上根据示例画出树形结构图;
-- 根据第 39 题的经验,可以设置搜索起点,以防止不重不漏;
-- 如果对这个问题研究得比较深入,由于 k 是一个正整数,搜索起点不一定需要枚举到 n ,具体搜索起点的上限可以尝试举出一个数值合适的例子找找规律,我们在这里直接给出参考的代码。
-
-参考代码:
-
-```java
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.List;
-
-public class Solution {
-
- public List> combine(int n, int k) {
- List> res = new ArrayList<>();
- if (k <= 0 || n < k) {
- return res;
- }
- Deque path = new ArrayDeque<>(k);
- dfs(n, k, 1, path, res);
- return res;
- }
-
- // i 的极限值满足: n - i + 1 = (k - pre.size())
- // n - i + 1 是闭区间 [i, n] 的长度
- // k - pre.size() 是剩下还要寻找的数的个数
- private void dfs(int n, int k, int index, Deque path, List> res) {
- if (path.size() == k) {
- res.add(new ArrayList<>(path));
- return;
- }
-
- // 注意:这里搜索起点的上限为 n - (k - path.size()) + 1 ,这一步有很强的剪枝效果
- for (int i = index; i <= n - (k - path.size()) + 1; i++) {
- path.addLast(i);
- dfs(n, k, i + 1, path, res);
- path.removeLast();
- }
- }
-}
-
-```
-
-
-### 练习
-
-1. 完成「力扣」第 40 题:组合总和 II(中等);
-2. 完成「力扣」第 78 题:子集(中等);
-3. 完成「力扣」第 90 题:子集 II(中等)。
-4. 完成「力扣」第 1593 题:拆分字符串使唯一子字符串的数目最大(中等);
-5. 完成「力扣」第 1079 题:活字印刷(中等)。
-
-
-
-### 总结
-
-「剪枝」条件通常是具体问题具体分析,因此需要我们积累一定求解问题的经验。
-
-
-
-## 六、二维平面上的搜索问题(Flood Fill)
-
-「力扣」第 79 题:单词搜索(中等)
-
-> 给定一个二维网格和一个单词,找出该单词是否存在于网格中。
->
-> 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
->
-> ```
-> board =
-> [
-> ['A','B','C','E'],
-> ['S','F','C','S'],
-> ['A','D','E','E']
-> ]
->
-> 给定 word = "ABCCED", 返回 true
-> 给定 word = "SEE", 返回 true
-> 给定 word = "ABCB", 返回 false
-> ```
-
-提示:
-
-- board 和 word 中只包含大写和小写英文字母;
-- 1 <= board.length <= 200
-- 1 <= board[i].length <= 200
-- 1 <= word.length <= 10^3
-
-思路分析:
-
-- 这道题要求我们在一个二维表格上找出给定目标单词 word 的一个路径,题目中说:「相邻」单元格是那些水平相邻或垂直相邻的单元格。也就是说:如果当前单元格恰好与 word 的某个位置的字符匹配,应该从当前单元格的上、下、左、右 44 个方向继续匹配剩下的部分;
-- 下面的幻灯片展示了整个匹配的过程,请大家注意:
- - 对于每一个单元格,在第一个字符匹配的时候,才执行一次深度优先遍历,直到找到符合条件的一条路径结束。如果第一个字符都不匹配,当然没有必要继续遍历下去;
- - 递归终止的条件是:匹配到了 word 的最后一个字符;
- - 在不能匹配的时候,需要 原路返回,尝试新的路径。这一点非常重要,我们修改了题目中的示例,请大家仔细观察下面的幻灯片中的例子,体会「回溯」在什么时候发生?
-
-整个搜索的过程可以用下面的树形结构表示,由于空间限制我们没有画出完整的树的样子,希望大家能够结合上面的幻灯片想清楚这个问题「当一条路径不能匹配的时候是如何回退的」,并且结合参考代码理解程序的执行流程。
-
-
-
-```java
-public class Solution {
-
- private boolean[][] visited;
- private int[][] directions = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
- private int rows;
- private int cols;
- private int len;
- private char[] charArray;
- private char[][] board;
-
- public boolean exist(char[][] board, String word) {
- len = word.length();
- rows = board.length;
- if (rows == 0) {
- return false;
- }
- cols = board[0].length;
- visited = new boolean[rows][cols];
- this.charArray = word.toCharArray();
- this.board = board;
-
- for (int i = 0; i < rows; i++) {
- for (int j = 0; j < cols; j++) {
- if (dfs(i, j, 0)) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * @param i
- * @param j
- * @param begin 从 word[begin] 处开始搜索
- * @return
- */
- private boolean dfs(int i, int j, int begin) {
- // 字符串的最后一个字符匹配,即返回 true
- if (begin == len - 1) {
- return board[i][j] == charArray[begin];
- }
-
- // 只要当前考虑的字符能够匹配,就从四面八方继续搜索
- if (board[i][j] == charArray[begin]) {
- visited[i][j] = true;
- for (int[] direction : directions) {
- int newX = i + direction[0];
- int newY = j + direction[1];
- if (inArea(newX, newY) && !visited[newX][newY]) {
- if (dfs(newX, newY, begin + 1)) {
- return true;
- }
- }
- }
- visited[i][j] = false;
- }
- return false;
- }
-
- private boolean inArea(int x, int y) {
- return x >= 0 && x < rows && y >= 0 && y < cols;
- }
-}
-```
-
-说明:
-
-- DIRECTIONS 表示方向数组,44 个元素分别表示下、右、上、左 44 个方向向量,顺序无关紧要,建议四连通、八连通的问题都这样写;
-- 有一些朋友可能会觉得封装私有函数会降低程序的执行效率,这一点在一些编程语言中的确是这样,但是我们在日常编写代码的过程中,语义清晰和可读性是更重要的,因此在编写代码的时候,最好能够做到「一行代码只做一件事情」
-
-
-
-「力扣」第 695 题:岛屿的最大面积(中等)
-
-> 下面我们再展示一个问题,希望大家通过这个问题熟悉二维平面上回溯算法的编码技巧。
->
-> 给定一个包含了一些 0 和 1 的非空二维数组 grid 。
->
-> 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
->
-> 找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 。)
->
-> ```
-> [[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` 。
-
-思路分析:
-
-找到一个岛屿,就是在 1(表示土地)的上、下、左、右 44 个方向执行一次深度优先遍历遍历,只要这 44 个方向上还有 1 ,就继续执行深度优先遍历。
-
-递归实现
-
-```java
-public class Solution {
-
- private int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
- private int rows;
- private int cols;
- private int[][] grid;
- private boolean[][] visited;
-
- public int maxAreaOfIsland(int[][] grid) {
- if (grid == null) {
- return 0;
- }
- rows = grid.length;
- if (rows == 0) {
- return 0;
- }
- cols = grid[0].length;
- if (cols == 0) {
- return 0;
- }
-
- this.grid = grid;
- this.visited = new boolean[rows][cols];
- int res = 0;
- for (int i = 0; i < rows; i++) {
- for (int j = 0; j < cols; j++) {
- if (grid[i][j] == 1 && !visited[i][j]) {
- res = Math.max(res, dfs(i, j));
- }
- }
- }
- return res;
- }
-
- private int dfs(int x, int y) {
- visited[x][y] = true;
- int res = 1;
- for (int[] direction:directions) {
- int nextX = x + direction[0];
- int nextY = y + direction[1];
- if (inArea(nextX, nextY) && grid[nextX][nextY] == 1 && !visited[nextX][nextY]) {
- res += dfs(nextX, nextY);
- }
- }
- return res;
- }
-
- private boolean inArea(int x, int y) {
- return x >= 0 && x < rows && y >= 0 && y < cols;
- }
-}
-```
-模拟栈
-
-```java
-import java.util.ArrayDeque;
-import java.util.Deque;
-
-public class Solution {
-
- private final static int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
-
- public int maxAreaOfIsland(int[][] grid) {
- int rows = grid.length;
- int cols = grid[0].length;
- boolean[][] visited = new boolean[rows][cols];
-
- int maxArea = 0;
- for (int i = 0; i < rows; i++) {
- for (int j = 0; j < cols; j++) {
- if (grid[i][j] == 1 && !visited[i][j]) {
- maxArea = Math.max(maxArea, dfs(grid, i, j, rows, cols, visited));
- }
- }
- }
- return maxArea;
- }
-
- private int dfs(int[][] grid, int i, int j, int rows, int cols, boolean[][] visited) {
- int count = 0;
- Deque stack = new ArrayDeque<>();
- stack.addLast(new int[]{i, j});
- visited[i][j] = true;
- while (!stack.isEmpty()) {
- int[] top = stack.removeLast();
- int curX = top[0];
- int curY = top[1];
- count++;
- for (int[] direction : DIRECTIONS) {
- int newX = curX + direction[0];
- int newY = curY + direction[1];
- if (inArea(newX, newY, rows, cols) && grid[newX][newY] == 1 && !visited[newX][newY]) {
- stack.addLast(new int[]{newX, newY});
- visited[newX][newY] = true;
- }
- }
- }
- return count;
- }
-
- private boolean inArea(int i, int j, int rows, int cols) {
- return i >= 0 && i < rows && j >= 0 && j < cols;
- }
-}
-```
-
-
-### 练习
-
-提示读者这部分所有的问题都可以使用广度优先遍历完成。
-
-1. 完成「力扣」第 130 题:被围绕的区域(中等);深度优先遍历、广度优先遍历、并查集。
-2. 完成「力扣」第 200 题:岛屿数量(中等):深度优先遍历、广度优先遍历、并查集;
-3. 完成「力扣」第 417 题:太平洋大西洋水流问题(中等):深度优先遍历、广度优先遍历;
-4. 完成「力扣」第 1020 题:飞地的数量(中等):方法同第 130 题,深度优先遍历、广度优先遍历;
-5. 完成「力扣」第 1254 题:统计封闭岛屿的数目(中等):深度优先遍历、广度优先遍历;
-6. 完成「力扣」第 1034 题:边框着色(中等):深度优先遍历、广度优先遍历;
-7. 完成「力扣」第 133 题:克隆图(中等):借助哈希表,使用深度优先遍历、广度优先遍历;
-8. 完成「剑指 Offer」第 13 题:机器人的运动范围(中等):深度优先遍历、广度优先遍历;
-9. 完成「力扣」第 529 题:扫雷问题(中等):深度优先遍历、广度优先遍历;
-
-
-
-
-
-## 七、动态规划与深度优先遍历思想的结合
-
-深度优先遍历是一种重要的算法设计思想,可以用于解决「力扣」上很多问题,熟练掌握「深度优先遍历」以及与之相关的「递归」、「分治」思想的应用是十分有帮助的。事实上,有一类问题需要「深度优先遍历」思想与「动态规划」思想的结合。
-
-
-
-### 树形动态规划问题
-
-在动态规划问题里,有一类问题叫做「树形动态规划 DP」问题。这一类问题通常的解决的思路是:通过对树结构执行一次深度优先遍历,采用 后序遍历 的方式,一层一层向上传递信息,并且利用「无后效性」的思想(固定住一些状态,或者对当前维度进行升维)解决问题。即这一类问题通常采用「后序遍历」 + 「动态规划(无后效性)」的思路解决。
-
-> 友情提示:「无后效性」是「动态规划」的一个重要特征,也是一个问题可以使用「动态规划」解决的必要条件,「无后效性」就是字面意思:当前阶段的状态值一旦被计算出来就不会被修改,即:在计算之后阶段的状态值时不会修改之前阶段的状态值。
->
-> 利用「无后效性」解决动态规划问题通常有两种表现形式:
->
-> - 对当前维度进行「升维」,在「自底向上递推」的过程中记录更多的信息,避免复杂的分类讨论;
->
-> - 固定住一种的状态,通常这种状态的形式最简单,它可以组合成复杂的状态。
->
-> 理解「无后效性」需要做一些经典的使用「动态规划」解决的问题。例如:「力扣」第 62 题、第 120 题、第 53 题、第 152 题、第 300 题、第 1142 题。
-
-
-
-「力扣」第 543 题:二叉树的直径(简单)
-
-> 给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
->
-> 示例:
->
-> 给定二叉树
->
-> ```
-> 1
-> / \
-> 2 3
-> / \
-> 4 5
-> ```
->
-> 返回 **3**, 它的长度是路径 `[4, 2, 1, 3]` 或者 `[5, 2, 1, 3]`。
->
-> **注意**:两结点之间的路径长度是以它们之间边的数目表示。
-
-思路分析:
-
-- 首先理解题意。在题目最后的「注意」中有强调:两个结点之间边的数目为直径,而不是结点的数目;
-- 要了解树当中的信息,通常来说需要执行一次 深度优先遍历,最后在根结点汇总值,自底向上,一层一层向上汇报信息,这是 后序遍历;
-- 我们再看直径的概念,题目中已经强调了:直径可能穿过也可能不穿过根结点。并且示例给出的路径 [4, 2, 1, 3] 或者 [5, 2, 1, 3] 是弯曲的,不是「从根结点到叶子结点的最长路径」,因此一条路径是否经过某个结点,就需要分类讨论。在动态规划里,可以利用一个概念,叫做「无后效性」,即:将不确定的事情确定下来,以方便以后的讨论。
-
-我们采用逐步完善代码的方式向大家展示编码过程,首先写出代码大致的框架,请大家留意代码中的注释,注释体现了编码的思路。
-
-```java
-public class Solution {
-
- public int diameterOfBinaryTree(TreeNode root) {
- dfs(root);
- return res;
- }
-
-
- /**
- * @param node 某个子树的根结点
- * @return 必需经过当前 node 结点的「单边」路径长度的「最大值」,这是动态规划「无后效性」的应用
- */
- private int dfs(TreeNode node) {
- // 递归终止条件
- if (node == null) {
- return 0;
- }
-
- // 根据左右子树的结果,再得到当前结点的结果,这是典型的「后序遍历」的思想
- int left = dfs(node.left);
- int right = dfs(node.right);
-
- // 注意:递归函数的返回值的定义,必需经过 node 并且只有一边
- return Math.max(left, right) + 1;
- }
-}
-```
-
-注意:这里递归函数 dfs 的定义,有两点很重要:① 必需经过当前 node 结点,也就是说当前结点 node 必需被选取,这一点是我们上面向大家介绍的「固定住」一些信息,方便分类讨论;② 「单边路径」是我们为了方便说明这个问题引入的概念。「单边路径」指的是 node 作为某一条路径的端点,它或者是「左端点」或者是「右端点」,它一定不是位于在这条路径中间的结点。
-
-
-
-比较难理解的地方是:为什么只讨论「单边路径」?这是因为「单边」的情况最简单,是可以拆分的最小单元。「弯曲」的情况可以由「单边」的情况组合而成。
-
-题目要求的直径,可以「弯曲」。「弯曲」的部分就是「左边单边」的长度 + 「右边单边」的长度之和,可以在遍历的过程中记录最大值。
-
-```java
-public class Solution {
-
- private int res;
-
- public int diameterOfBinaryTree(TreeNode root) {
- dfs(root);
- return res;
- }
-
-
- /**
- * @param node
- * @return 必需经过当前 node 结点的路径长度的「最大值」
- */
- private int dfs(TreeNode node) {
- if (node == null) {
- return 0;
- }
-
- int left = dfs(node.left);
- int right = dfs(node.right);
- // 注意:在深度优先遍历的过程中,记录最大值
- res = Math.max(res, left + right);
-
- return Math.max(left, right) + 1;
- }
-}
-```
-
-
-
-### 练习
-
-1. 完成「力扣」第 124 题:二叉树中的最大路径和(困难);
-2. 完成「力扣」第 298 题:二叉树最长连续序列(中等);
-3. 完成「力扣」第 549 题:二叉树中最长的连续序列(中等);
-4. 完成「力扣」第 687 题:最长同值路径(中等);
-5. 完成「力扣」第 865 题:具有所有最深节点的最小子树(中等);
-6. 完成「力扣」第 1372 题:二叉树中的最长交错路径(中等)。
-
-下面的问题可以使用「二分答案 + DFS 或者 BFS」的思想解决。
-
-7. 完成「力扣」第 1102 题:得分最高的路径(中等);
-8. 完成「力扣」第 1631 题:最小体力消耗路径(中等);
-9. 完成「力扣」第 778 题:水位上升的泳池中游泳(困难);
-10. 完成「力扣」第 403 题:青蛙过河(困难)。
-
-
-
-### 八、总结
-
-- 深度优先遍历的直观理解非常重要,支撑深度优先遍历实现的数据结构是「栈」;
-- 「力扣」上很多树和图的问题都可以通过深度优先遍历实现、使用深度优先遍历实现的问题很多时候也可以使用广度优先遍历实现;
-- 「回溯算法」是深度优先遍历算法的应用;
-- 「回溯算法」的细节很多,需要通过练习和调试理解。
-
diff --git a/docs/data-structure-algorithms/Double-Pointer.md b/docs/data-structure-algorithms/Double-Pointer.md
deleted file mode 100755
index 28ef87a38c..0000000000
--- a/docs/data-structure-algorithms/Double-Pointer.md
+++ /dev/null
@@ -1,814 +0,0 @@
-
-
-归纳下双指针算法,其实总共就三类
-
-- 左右指针,数组和字符串问题
-- 快慢指针,主要是成环问题
-- 滑动窗口,针对子串问题
-
-
-
-#### [42. 接雨水](https://leetcode-cn.com/problems/trapping-rain-water/)
-
-
-
-
-
-## 一、左右指针
-
-左右指针在数组中其实就是两个索引值,
-
-TODO: 一般都是有序数组?或者先排序后?
-
-Javaer 一般这么表示:
-
-```java
-int left = i + 1;
-int right = nums.length - 1;
-while(left < right)
- ***
-```
-
-这两个指针 **相向交替移动**
-
-
-
-
-
-> [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/)
->
-> [15. 三数之和](https://leetcode-cn.com/problems/3sum/)
->
-> [167. 两数之和 II - 输入有序数组](https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/)
->
-> [125. 验证回文串](https://leetcode-cn.com/problems/valid-palindrome/)
->
-> [344. 反转字符串](https://leetcode-cn.com/problems/reverse-string/)
->
-> [283. 移动零](https://leetcode-cn.com/problems/move-zeroes/)
->
-> [704. 二分查找](https://leetcode-cn.com/problems/binary-search/)
->
-> [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)
-
-TODO: 画图对比各个算法
-
-### 两数之和 II - 输入有序数组
-
-> 给定一个整数数组 `nums` 和一个整数目标值 `target`,请你在该数组中找出 **和为目标值** *`target`* 的那 **两个** 整数,并返回它们的数组下标。
->
-> 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
->
-> 你可以按任意顺序返回答案。
->
-> ```
-> 输入:nums = [2,7,11,15], target = 9
-> 输出:[0,1]
-> 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
-> ```
-
-直接用左右指针套就可以
-
-```java
-public static int[] towSum(int[] nums, int target) {
- int left = 0;
- int rigth = nums.length - 1;
- while(left < rigth){
- int tmp = nums[left] + nums[rigth];
- if (target == tmp) {
- return new int[]{left, rigth};
- } else if (tmp > target) {
- rigth--; //右移
- } else {
- left++; //左移
- }
- }
- return new int[]{-1, -1};
-}
-```
-
-
-
-### 三数之和
-
-**排序、双指针、去重**
-
-第一个想法是,这三个数,两个指针?
-
-- 对数组排序,固定一个数 $nums[i]$ ,然后遍历数组,并移动左右指针求和,判断是否有等于 0 的情况
-- 特例:
- - 排序后第一个数就大于 0,不干了
- - 有三个需要去重的地方
- - nums[i] == nums[i - 1] 直接跳过本次遍历
- - nums[left] == nums[left + 1] 移动指针,即去重
- - nums[right] == nums[right - 1] 移动指针
-
-
-
-
-
-
-
-```java
-public static List> threeSum(int[] nums) {
- //存放结果list
- List> result = new ArrayList<>();
- int length = nums.length;
- //特例判断
- if (length < 3) {
- return result;
- }
- Arrays.sort(nums);
- for (int i = 0; i < length; i++) {
- //排序后的第一个数字就大于0,就说明没有符合要求的结果
- if (nums[i] > 0) break;
-
- //去重
- if (i > 0 && nums[i] == nums[i - 1]) continue;
- //左右指针
- int l = i + 1;
- int r = length - 1;
- while (l < r) {
- int sum = nums[i] + nums[l] + nums[r];
- if (sum == 0) {
- result.add(Arrays.asList(nums[i], nums[l], nums[r]));
- //去重(相同数字的话就移动指针)
- while (nums[l] == nums[l + 1]) l++;
- while (nums[r] == nums[r - 1]) r--;
- //移动指针
- l++;
- r--;
- } else if (sum < 0) {
- l++;
- } else if (sum > 0) {
- r--;
- }
- }
- }
- return result;
-}
-```
-
-
-
-### 盛最多水的容器
-
-> 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
->
-> ```
-> 输入:[1,8,6,2,5,4,8,3,7]
-> 输出:49
-> 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
-> ```
->
-> 
-
-思路:
-
-- 求得是水量,水量 = 两个指针指向的数字中较小值 * 指针之间的距离(水桶原理,最短的板才不会漏水)
-- 为了求最大水量,我们需要存储所有条件的水量,进行比较才行
-- **双指针相向移动**,循环收窄,直到两个指针相遇
-- 往哪个方向移动,需要考虑清楚,如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会更小,所以我们移动**数字较小的那个指针**
-
-```java
-public int maxArea(int[] height){
- int left = 0;
- int right = height.length - 1;
- //需要保存各个阶段的值
- int result = 0;
- while(left < right){
- //水量 = 两个指针指向的数字中较小值∗指针之间的距离
- int area = Math.min(height[left],height[right]) * (right - left);
- result = Math.max(result,area);
- //移动数字较小的指针
- if(height[left] <= height[right]){
- left ++;
- }else{
- right--;
- }
- }
- return result;
-}
-```
-
-
-
-### 验证回文串
-
-> 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
->
-> 说明:本题中,我们将空字符串定义为有效的回文串。
->
-> ```
-> 输入: "A man, a plan, a canal: Panama"
-> 输出: true
-> 解释:"amanaplanacanalpanama" 是回文串
-> ```
-
-思路:
-
-- 没看题解前,因为这个例子中有各种逗号、空格啥的,我第一想到的其实就是先遍历放在一个数组里,然后再去判断,看题解可以在原字符串完成,降低了空间复杂度
-- 首先需要知道三个 API
- - `Character.isLetterOrDigit` 确定指定的字符是否为字母或数字
- - `Character.toLowerCase` 将大写字符转换为小写
- - `public char charAt(int index)` String 中的方法,用于返回指定索引处的字符
-- 双指针,每移动一步,判断这两个值是不是相同
-- 两个指针相遇,则是回文串
-
-```java
-public boolean isPalindrome(String s) {
- int left = 0;
- int right = s.length() - 1;
- while (left < right) {
- //这里还得加个left 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
->
-> 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
->
-> 你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
->
-> ```
-> 输入:["h","e","l","l","o"]
-> 输出:["o","l","l","e","h"]
-> ```
->
-> ```
-> 输入:["H","a","n","n","a","h"]
-> 输出:["h","a","n","n","a","H"]
-> ```
-
-思路:
-
-- 因为要反转,所以就不需要相向移动了,如果用双指针思路的话,其实就是遍历中交换左右指针的字符
-
-```java
-public void reverseString(char[] s) {
- int left = 0;
- int right = s.length - 1;
- while (left < right){
- char tmp = s[left];
- s[left] = s[right];
- s[right] = tmp;
- left++;
- right--;
- }
-}
-```
-
-
-
-### 二分查找
-
-有重复数字的话,返回的其实就是最右匹配
-
-```java
-public static int search(int[] nums, int target) {
- int left = 0;
- int right = nums.length - 1;
- while (left <= right) {
- //不直接使用(right+left)/2 是考虑数据大的时候溢出
- int mid = (right - left) / 2 + left;
- int tmp = nums[mid];
- if (tmp == target) {
- return mid;
- } else if (tmp > target) {
- //右指针移到中间位置 - 1,也避免不存在的target造成死循环
- right = mid - 1;
- } else {
- //
- left = mid + 1;
- }
- }
- return -1;
-}
-```
-
-
-
-## 二、快慢指针
-
-「快慢指针」,也称为「同步指针」
-
-> [141. 环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)
->
-> [142. 环形链表II](https://leetcode-cn.com/problems/linked-list-cycle-ii)
->
-> [19. 删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/)
->
-> [876. 链表的中间结点](https://leetcode-cn.com/problems/middle-of-the-linked-list/)
->
-> [26. 删除有序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/)
-
-### 环形链表
-
-
-
-思路:
-
-- 快慢指针,两个指针,一块一慢的话,慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
-
-```java
-public boolean hasCycle(ListNode head) {
- if (head == null || head.next == null) {
- return false;
- }
- // 龟兔起跑
- ListNode fast = head;
- ListNode slow = head;
-
- while (fast != null && fast.next != null) {
- // 龟走一步
- slow = slow.next;
- // 兔走两步
- fast = fast.next.next;
- if (slow == fast) {
- return true;
- }
- }
- return false;
-}
-```
-
-
-
-### 环形链表 II
-
-> 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 `null`。
->
-> ```
-> 输入:head = [3,2,0,-4], pos = 1
-> 输出:返回索引为 1 的链表节点
-> 解释:链表中有一个环,其尾部连接到第二个节点。
-> ```
-
-思路:
-
-- 最初,我就把有环理解错了,看题解觉得快慢指针相交的地方就是入环的节点
-- 假设环是这样的,slow 指针进入环后,又走了 b 的距离与 fast 相遇
-- 
-
-
-
-
-
-### 链表的中间结点
-
-> 给定一个头结点为 `head` 的非空单链表,返回链表的中间结点。
->
-> 如果有两个中间结点,则返回第二个中间结点。(给定链表的结点数介于 `1` 和 `100` 之间。)
->
-> ```
-> 输入:[1,2,3,4,5]
-> 输出:此列表中的结点 3 (序列化形式:[3,4,5])
-> 返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
-> 注意,我们返回了一个 ListNode 类型的对象 ans,这样:
-> ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
-> ```
-
-思路:
-
-- 快慢指针遍历,当 `fast` 到达链表的末尾时,`slow` 必然位于中间
-
-```java
-public ListNode middleNode(ListNode head) {
- ListNode fast = head;
- ListNode slow = head;
- while (fast != null && fast.next != null) {
- slow = slow.next;
- fast = fast.next.next;
- }
- return slow;
-}
-```
-
-
-
-
-
-### 删除链表的倒数第 N 个结点
-
-> 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
->
-> ```
-> 输入:head = [1,2,3,4,5], n = 2
-> 输出:[1,2,3,5]
-> ```
-
-
-
-### 删除有序数组中的重复项
-
-> 给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
->
-> 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 $O(1)$ 额外空间的条件下完成。
->
-> 说明:
->
-> 为什么返回数值是整数,但输出的答案是数组呢?
->
-> 请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
->
-> 你可以想象内部操作如下:
->
-> ```java
-> // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
-> int len = removeDuplicates(nums);
-> // 在函数里修改输入数组对于调用者是可见的。
-> // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
-> for (int i = 0; i < len; i++) {
-> print(nums[i]);
-> }
-> ```
->
-> ```
-> 输入:nums = [1,1,2]
-> 输出:2, nums = [1,2]
-> 解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
-> ```
->
-> ```
-> 输入:nums = [0,0,1,1,1,2,2,3,3,4]
-> 输出:5, nums = [0,1,2,3,4]
-> 解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
-> ```
-
-**思路**:
-
-- 数组有序,那相等的元素在数组中的下标一定是连续的
-- 使用快慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置
-- 第一个元素不需要删除,所有快慢指针都从下标 1 开始
-
-```java
-public static int removeDuplicates(int[] nums) {
- if (nums == null) {
- return 0;
- }
- int fast = 1;
- int slow = 1;
- while (fast < nums.length) {
- //和前一个值比较
- if (nums[fast] != nums[fast - 1]) {
- //不一样的话,把快指针的值放在慢指针上,实现了去重,并往前移动慢指针
- nums[slow] = nums[fast];
- ++slow;
- }
- //相等的话,移动快指针就行
- ++fast;
- }
- //慢指针的位置就是不重复的数量
- return slow;
-}
-```
-
-
-
-### 最长连续递增序列
-
-> 给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
->
-> 连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
->
-> ```
-> 输入:nums = [1,3,5,4,7]
-> 输出:3
-> 解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
-> ```
-
-思路分析:
-
-- 这个题的思路和删除有序数组中的重复项,很像
-
-```java
-public int findLengthOfLCIS(int[] nums) {
- int result = 0;
- int fast = 0;
- int slow = 0;
- while (fast < nums.length) {
- //前一个数大于后一个数的时候
- if (fast > 0 || nums[fast - 1] > nums[fast]) {
- slow = fast;
- }
- fast++;
- result = Math.max(result, fast - slow);
- }
- return result;
-}
-```
-
-
-
-## 三、滑动窗口
-
-有一类数组上的问题,需要使用两个指针变量(我们称为左指针和右指针),同向、交替向右移动完成任务。这样的过程像极了一个窗口在平面上滑动的过程,因此我们将解决这一类问题的算法称为「滑动窗口」问题
-
-
-
-
-
-滑动窗口,就是两个指针齐头并进,好像一个窗口一样,不断往前滑
-
-子串问题,几乎都是滑动窗口
-
-> [643. 子数组最大平均数 I](https://leetcode-cn.com/problems/maximum-average-subarray-i/)
->
-> [1052. 爱生气的书店老板](https://leetcode-cn.com/problems/grumpy-bookstore-owner/)
->
-> [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)
->
-> [76. 最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/)
->
-> [424. 替换后的最长重复字符](https://leetcode-cn.com/problems/longest-repeating-character-replacement/)
->
->
-
-```java
-int left = 0, right = 0;
-
-while (right < s.size()) {
- // 增大窗口
- window.add(s[right]);
- right++;
-
- while (window needs shrink) {
- // 缩小窗口
- window.remove(s[left]);
- left++;
- }
-}
-```
-
-
-
-### 3.1 同向交替移动的两个变量
-
-有一类数组上的问题,问我们固定长度的滑动窗口的性质,这类问题还算相对简单。
-
-#### 子数组最大平均数 I
-
-> 给定 `n` 个整数,找出平均数最大且长度为 `k` 的连续子数组,并输出该最大平均数。
->
-> ```
-> 输入:[1,12,-5,-6,50,3], k = 4
-> 输出:12.75
-> 解释:最大平均数 (12-5-6+50)/4 = 51/4 = 12.75
-> ```
-
-**思路**:
-
-- 长度为固定的 K,想到用滑动窗口
-- 保存每个窗口的值,取这 k 个数的最大和就可以得出最大平均数
-- 怎么保存每个窗口的值,这一步
-
-```java
-public static double getMaxAverage(int[] nums, int k) {
- int sum = 0;
- //先求出前k个数的和
- for (int i = 0; i < nums.length; i++) {
- sum += nums[i];
- }
- //目前最大的数是前k个数
- int result = sum;
- //然后从第 K 个数开始移动,保存移动中的和值,返回最大的
- for (int i = k; i < nums.length; i++) {
- sum = sum - nums[i - k] + nums[i];
- result = Math.max(result, sum);
- }
- //返回的是double
- return 1.0 * result / k;
-}
-```
-
-
-
-### 3.2 不定长度的滑动窗口
-
-#### 无重复字符的最长子串
-
-> 给定一个字符串 `s` ,请你找出其中不含有重复字符的 **最长子串** 的长度。
->
-> ```
-> 输入: s = "abcabcbb"
-> 输出: 3
-> 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
-> ```
-
-思路:
-
-- 滑动窗口,其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列
-- 如何移动?我们只要把队列的左边的元素移出就行了,直到满足题目要求!
-- 一直维持这样的队列,找出队列出现最长的长度时候,求出解!
-
-```java
-public static int lengthOfLongestSubstring(String s){
- HashMap map = new HashMap<>();
- int result = 0;
- int left = 0;
- //为了有左右指针的思想,我把我们常用的 i 写成了 right
- for (int right = 0; right < s.length(); right++) {
- //当前字符包含在当前有效的子段中,如:abca,当我们遍历到第二个a,当前有效最长子段是 abc,我们又遍历到a,
- //那么此时更新 left 为 map.get(a)+1=1,当前有效子段更新为 bca;
- //相当于左指针往前移动了一位
- if (map.containsKey(s.charAt(right))) {
- left = Math.max(left, map.get(s.charAt(right)) + 1);
- }
- //右指针一直往前移动
- map.put(s.charAt(right), right);
- result = Math.max(result, right - left + 1);
- }
- return result;
-}
-```
-
-
-
-#### 最小覆盖子串
-
-> 给你一个字符串 `s` 、一个字符串 `t` 。返回 `s` 中涵盖 `t` 所有字符的最小子串。如果 `s` 中不存在涵盖 `t` 所有字符的子串,则返回空字符串 `""` 。
->
-> ```
-> 输入:s = "ADOBECODEBANC", t = "ABC"
-> 输出:"BANC"
-> ```
-
-
-
-
-
-#### 替换后的最长重复字符
-
-> 给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。
->
-> 注意:字符串长度 和 k 不会超过 10^4
->
-> ```
-> 输入:s = "ABAB", k = 2
-> 输出:4
-> 解释:用两个'A'替换为两个'B',反之亦然。
-> ```
->
-> ```
-> 输入:s = "AABABBA", k = 1
-> 输出:4
-> 解释:将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。子串 "BBBB" 有最长重复字母, 答案为 4。
-> ```
-
-思路:
-
--
-
-
-
-```java
-public int characterReplacement(String s, int k) {
- int len = s.length();
- if (len < 2) {
- return len;
- }
-
- char[] charArray = s.toCharArray();
- int left = 0;
- int right = 0;
-
- int res = 0;
- int maxCount = 0;
- int[] freq = new int[26];
- // [left, right) 内最多替换 k 个字符可以得到只有一种字符的子串
- while (right < len){
- freq[charArray[right] - 'A']++;
- // 在这里维护 maxCount,因为每一次右边界读入一个字符,字符频数增加,才会使得 maxCount 增加
- maxCount = Math.max(maxCount, freq[charArray[right] - 'A']);
- right++;
-
- if (right - left > maxCount + k){
- // 说明此时 k 不够用
- // 把其它不是最多出现的字符替换以后,都不能填满这个滑动的窗口,这个时候须要考虑左边界向右移动
- // 移出滑动窗口的时候,频数数组须要相应地做减法
- freq[charArray[left] - 'A']--;
- left++;
- }
- res = Math.max(res, right - left);
- }
- return res;
-}
-```
-
-
-
-### 3.3 计数问题
-
-> ### 至多包含两个不同字符的最长子串
->
-> ### 至多包含 K 个不同字符的最长子串
->
-> ### 区间子数组个数
->
-> ### K 个不同整数的子数组
-
-#### 至多包含两个不同字符的最长子串
-
-> 给定一个字符串 `s`,找出 **至多** 包含两个不同字符的最长子串 `t` ,并返回该子串的长度。
->
-> ```
-> 输入: "eceba"
-> 输出: 3
-> 解释: t 是 "ece",长度为3。
-> ```
-
-思路:
-
-- 这种字符串用滑动窗口的题目,一般用 `toCharArray()` 先转成字符数组
-
-
-
-#### 至多包含 K 个不同字符的最长子串
-
-> 给定一个字符串 `s`,找出 **至多** 包含 `k` 个不同字符的最长子串 `T`。
->
-> ```
-> 输入: s = "eceba", k = 2
-> 输出: 3
-> 解释: 则 T 为 "ece",所以长度为 3。
-> ```
-
-
-
-#### 区间子数组个数
-
-> 给定一个元素都是正整数的数组`A` ,正整数 `L` 以及 `R` (`L <= R`)。
->
-> 求连续、非空且其中最大元素满足大于等于`L` 小于等于`R`的子数组个数。
->
-> ```
-> 例如 :
-> 输入:
-> A = [2, 1, 4, 3]
-> L = 2
-> R = 3
-> 输出: 3
-> 解释: 满足条件的子数组: [2], [2, 1], [3].
-> ```
-
-
-
-#### K 个不同整数的子数组
-
->
-
-
-
-### 3.4 使用数据结构维护窗口性质
-
-有一类问题只是名字上叫「滑动窗口」,但解决这一类问题需要用到常见的数据结构。这一节给出的问题可以当做例题进行学习,一些比较复杂的问题是基于这些问题衍生出来的。
-
-#### 滑动窗口最大值
-
-#### 滑动窗口中位数
-
-
-
-
-
-## 四、其他双指针问题
-
-#### [88. 合并两个有序数组](https://leetcode-cn.com/problems/merge-sorted-array/)
-
-
-
-
-
-
-
-### 总结
-
-区间不同的定义决定了不同的初始化逻辑、遍历过程中的逻辑。
-
-- 移除元素
-- 删除排序数组中的重复项 II
-- 移动零
-
-
-
diff --git a/docs/data-structure-algorithms/Leetcode-dynamic-programming.md b/docs/data-structure-algorithms/Leetcode-dynamic-programming.md
deleted file mode 100755
index 2063844409..0000000000
--- a/docs/data-structure-algorithms/Leetcode-dynamic-programming.md
+++ /dev/null
@@ -1,519 +0,0 @@
-## 概述
-
-这里是 LeetCode 官方推出的动态规划精讲系列第一弹。
-
-完成本 LeetBook 后,你将能够:
-
-- 理解动态规划的基本思想
-- 了解动态规划算法的优缺点和问题分类
-- 掌握运用动态规划解决问题的思路
-- 能够运用动态规划解决线性、前缀和、区间这三类问题
-
-
-
-## 动态规划简介
-
-### 动态规划的背景
-
-动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
-
-动态规划不是某一种具体的算法,而是一种算法思想:若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
-
-应用这种算法思想解决问题的可行性,对子问题与原问题的关系,以及子问题之间的关系这两方面有一些要求,它们分别对应了最优子结构和重复子问题。
-
-#### 最优子结构
-
-最优子结构规定的是子问题与原问题的关系
-
-动态规划要解决的都是一些问题的最优解,即从很多解决问题的方案中找到最优的一个。当我们在求一个问题最优解的时候,如果可以把这个问题分解成多个子问题,然后递归地找到每个子问题的最优解,最后通过一定的数学方法对各个子问题的最优解进行组合得出最终的结果。总结来说就是一个问题的最优解是由它的各个子问题的最优解决定的。
-
-将子问题的解进行组合可以得到原问题的解是动态规划可行性的关键。在解题中一般用状态转移方程描述这种组合。例如原问题的解为 f(n)f(n),其中 f(n)f(n) 也叫状态。状态转移方程 f(n) = f(n - 1) + f(n - 2)f(n)=f(n−1)+f(n−2) 描述了一种原问题与子问题的组合关系 。在原问题上有一些选择,不同选择可能对应不同的子问题或者不同的组合方式。例如
-
-
-
-n = 2kn=2k 和 n = 2k + 1n=2k+1 对应了原问题 nn 上不同的选择,分别对应了不同的子问题和组合方式。
-
-找到了最优子结构,也就能推导出一个状态转移方程 f(n)f(n),通过这个状态转移方程,我们能很快的写出问题的递归实现方法。
-
-
-
-#### 重复子问题
-
-重复子问题规定的是子问题与子问题的关系。
-
-当我们在递归地寻找每个子问题的最优解的时候,有可能会重复地遇到一些更小的子问题,而且这些子问题会重叠地出现在子问题里,出现这样的情况,会有很多重复的计算,动态规划可以保证每个重叠的子问题只会被求解一次。当重复的问题很多的时候,动态规划可以减少很多重复的计算。
-
-重复子问题不是保证解的正确性必须的,但是如果递归求解子问题时,没有出现重复子问题,则没有必要用动态规划,直接普通的递归就可以了。
-
-例如,斐波那契问题的状态转移方程 f(n) = f(n - 1) + f(n - 2)。在求 f(5) 时,需要先求子问题 f(4) 和 f(3),得到结果后再组合成原问题 f(5) 的解。递归地求 f(4) 时,又要先求子问题 f(3) 和 f(2) ,这里的 f(3) 与求 f(5) 时的子问题重复了。
-
-
-
-解决动态规划问题的核心:找出子问题及其子问题与原问题的关系
-
-找到了子问题以及子问题与原问题的关系,就可以递归地求解子问题了。但重叠的子问题使得直接递归会有很多重复计算,于是就想到记忆化递归法:若能事先确定子问题的范围就可以建表存储子问题的答案。
-
-动态规划算法中关于最优子结构和重复子问题的理解的关键点:
-
-1. 证明问题的方案中包含一种选择,选择之后留下一个或多个子问题
-2. 设计子问题的递归描述方式
-3. 证明对原问题的最优解包括了对所有子问题的最优解
-4. 证明子问题是重叠的(这一步不是动态规划正确性必需的,但是如果子问题无重叠,则效率与一般递归是相同的)
-
-
-
-
-
-## 线性动态规划
-
-### 线性动态规划简介
-
-线性动态规划的主要特点是状态的推导是按照问题规模 i 从小到大依次推过去的,较大规模的问题的解依赖较小规模的问题的解。
-
-这里问题规模为 i 的含义是考虑前 i 个元素 [0..i] 时问题的解。
-
-状态定义:
-
-```
-dp[n] := [0..n] 上问题的解
-```
-
-
-状态转移:
-
-```
-dp[n] = f(dp[n-1], ..., dp[0])
-```
-
-
-从以上状态定义和状态转移可以看出,大规模问题的状态只与较小规模的问题有关,而问题规模完全用一个变量 i 表示,i 的大小表示了问题规模的大小,因此从小到大推 i 直至推到 n,就得到了大规模问题的解,这就是线性动态规划的过程。
-
-按照问题的输入格式,线性动态规划解决的问题主要是单串,双串,矩阵上的问题,因为在单串,双串,矩阵上问题规模可以完全用位置表示,并且位置的大小就是问题规模的大小。因此从前往后推位置就相当于从小到大推问题规模。
-
-线性动态规划是动态规划中最基本的一类。问题的形式、dp 状态和方程的设计、以及与其它算法的结合上面变化很多。按照 dp 方程中各个维度的含义,可以大致总结出几个主流的问题类型,见后面的小节。除此之外还有很多没有总结进来的变种问题,小众问题,和困难问题,这些问题的解法更多地需要结合自己的做题经验去积累,除此之外,常见的,主流的问题和解法都可以总结成下面的四个小类别。
-
-
-
-
-
-### 单串
-
-单串 dp[i] 线性动态规划最简单的一类问题,输入是一个串,状态一般定义为 dp[i] := 考虑[0..i]上,原问题的解,其中 i 位置的处理,根据不同的问题,主要有两种方式:
-
-- 第一种是 i 位置必须取,此时状态可以进一步描述为 dp[i] := 考虑[0..i]上,且取 i,原问题的解;
-
-- 第二种是 i 位置可以取可以不取
-
-大部分的问题,对 i 位置的处理是第一种方式,例如力扣:
-
-- 70 爬楼梯问题
-- 801 使序列递增的最小交换次数
-- 790 多米诺和托米诺平铺
-- 746 使用最小花费爬楼梯
-
-线性动态规划中单串 dp[i] 的问题,状态的推导方向以及推导公式如下
-
-
-
-
-
-#### 1. 依赖比 i 小的 O(1) 个子问题
-
-dp[n] 只与常数个小规模子问题有关,状态的推导过程 dp[i] = f(dp[i - 1], dp[i - 2], ...)。时间复杂度 O(n)O(n),空间复杂度 O(n)O(n) 可以优化为 O(1)O(1),例如上面提到的 70, 801, 790, 746 都属于这类。
-
-如图所示,虽然紫色部分的 dp[i-1], dp[i-2], ..., dp[0] 均已经计算过,但计算橙色的当前状态时,仅用到 dp[i-1],这属于比 i 小的 O(1)O(1) 个子问题。
-
-例如,当 f(dp[i-1], ...) = dp[i-1] + nums[i] 时,当前状态 dp[i] 仅与 dp[i-1] 有关。这个例子是一种数据结构前缀和的状态计算方式,关于前缀和的详细内容请参考下一章。
-
-
-
-#### 2. 依赖比 i 小的 O(n) 个子问题
-
-dp[n] 与此前的更小规模的所有子问题 dp[n - 1], dp[n - 2], ..., dp[1] 都可能有关系。
-
-状态推导过程如下:
-
-```
-dp[i] = f(dp[i - 1], dp[i - 2], ..., dp[0])
-```
-
-
-依然如图所示,计算橙色的当前状态 dp[i] 时,紫色的此前计算过的状态 dp[i-1], ..., dp[0] 均有可能用到,在计算 dp[i] 时需要将它们遍历一遍完成计算。
-
-其中 f 常见的有 max/min,可能还会对 i-1,i-2,...,0 有一些筛选条件,但推导 dp[n] 时依然是 O(n)O(n) 级的子问题数量。
-
-例如:
-
-- 139 单词拆分
-
-- 818 赛车
-
-以 min 函数为例,这种形式的问题的代码常见写法如下
-
-```
-for i = 1, ..., n
- for j = 1, ..., i-1
- dp[i] = min(dp[i], f(dp[j])
-```
-
-时间复杂度 *O*(*n*2),空间复杂度 O*(*n)
-
-
-
-#### 单串 dp[i] 经典问题
-
-以下内容将涉及到的知识点对应的典型问题进行讲解,题目和解法具有代表性,可以从一个问题推广到一类问题。
-
-1. 依赖比 i 小的 O(1) 个子问题
-
-> 53. 最大子数组和
->
-> 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
-
-一个数组有很多个子数组,求哪个子数组的和最大。可以按照子数组的最后一个元素来分子问题,确定子问题后设计状态
-
-```
-dp[i] := [0..i] 中,以 nums[i] 结尾的最大子数组和
-```
-
-
-状态的推导是按照 i 从 0 到 n - 1 按顺序推的,推到 dp[i],时,dp[i - 1], ..., dp[0] 已经计算完。因为子数组是连续的,所以子问题 dp[i] 其实只与子问题 dp[i - 1] 有关。如果 [0..i-1] 上以 nums[i-1] 结尾的最大子数组和(缓存在 dp[i-1] )为非负数,则以 nums[i] 结尾的最大子数组和就在 dp[i-1] 的基础上加上 nums[i] 就是 dp[i] 的结果否则以 i 结尾的子数组就不要 i-1 及之前的数,因为选了的话子数组的和只会更小。
-
-按照以上的分析,状态的转移可以写出来,如下
-
-```
-dp[i] = nums[i] + max(dp[i - 1], 0)
-```
-
-这个是单串 dp[i] 的问题,状态的推导方向,以及推导公式如下
-
-
-
-在本题中,f(dp[i-1], ..., dp[0]) 即为 max(dp[i-1], 0) + nums[i],dp[i] 仅与 dp[i-1] 1 个子问题有关。因此虽然紫色部分的子问题已经计算完,但是推导当前的橙色状态时,只需要 dp[i-1] 这一个历史状态。
-
-
-
-2. 依赖比 i 小的 O(n) 个子问题
-
-> 30. 最长上升子序列
->
-> 给定一个无序的整数数组,找到其中最长上升子序列的长度。
-
-输入是一个单串,首先思考单串问题中设计状态 dp[i] 时拆分子问题的方式:枚举子串或子序列的结尾元素来拆分子问题,设计状态 dp[i] := 在子数组 [0..i] 上,且选了 nums[i] 时,的最长上升子序列。
-
-因为子序列需要上升,因此以 i 结尾的子序列中,nums[i] 之前的数字一定要比 nums[i] 小才行,因此目标就是先找到以此前比 nums[i] 小的各个元素,然后每个所选元素对应一个以它们结尾的最长子序列,从这些子序列中选择最长的,其长度加 1 就是当前的问题的结果。如果此前没有比 nums[i] 小的数字,则当前问题的结果就是 1 。
-
-按照以上的分析,状态的转移方程可以写出来,如下
-
-
-
-其中 0 <= j < i, nums[j] < nums[i]。
-
-本题依然是单串 dp[i] 的问题,状态的推导方向,以及推导公式与上一题的图示相同,
-
-状态的推导依然是按照 i 从 0 到 n-1 推的,计算 dp[i] 时,dp[i-1], dp[i-2], ..., dp[0] 依然已经计算完。
-
-但本题与上一题的区别是推导 dp[i] 时,dp[i-1]. dp[i-2], ..., dp[0] 均可能需要用上,即,因此计算当前的橙色状态时,紫色部分此前计算过的状态都可能需要用上。
-
-
-
-#### 单串相关练习题
-
-- 最经典单串 LIS 系列
-- 最大子数组和系列
-- 打家劫舍系列
-- 变形:需要两个位置的情况
-- 与其它算法配合
-- 其它单串 dp[i] 问题
-- 带维度单串 dp[i][k]
-- 股票系列
-
-
-
-
-
-### 双串
-
-有两个输入从串,长度分别为 m, n,此时子问题需要用 i, j 两个变量表示,分别代表第一个串和第二个串考虑的位置 dp[i][j]:=第一串考虑[0..i],第二串考虑[0..j]时,原问题的解
-
-较大规模的子问题只与常数个较小规模的子问题有关,其中较小规模可能是 i 更小,或者是 j 更小,也可以是 i,j 同时变小。
-其中一种最常见的状态转移形式:推导 dp[i][j] 时,dp[i][j] 仅与 dp[i-1][j], dp[i][j-1], dp[i-1][j-1],例如
-
-- 72 编辑距离
-- 712 两个字符串的最小 ASCII 删除和
-
-线性动态规划中双串 dp[i][j] 的问题,状态的推导方向以及推导公式如下
-
-
-
-如图所示,绿色部分的 dp\[i-1 ~ 0][j-1 ~ 0] 均已经计算过,但计算橙色的当前状态时,仅用到 dp\[i-1][j], dp\[i][j-1], dp\[i-1][j-1],即比 i, j 小的 O(1)个子问题。
-
-这种形式的线性 DP 的代码常见写法
-
-```
-for i = 1..m
- for j = 1..n
- dp[i][j] = f(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])
-
-```
-
-时间复杂度 O(mn),空间复杂度 O(mn)
-
-以上是 O(1) 转移的情况,即计算 dp\[i][j] 时,虽然绿色部分的子问题均已经计算完,但只需要用到 dp\[i-1][j], dp\[i][j-1], dp\[i-1][j-1]。也可能出现更高复杂度的转移,类似单串中依赖比 i 小的 O(n)) 个子问题的情况。
-
-
-
-#### 双串 dp\[i][j] 经典问题
-
-以下将涉及到的知识点对应的典型问题进行讲解,题目和解法具有代表性,可以从一个问题推广到一类问题。
-
-1143. > 1143. 最长公共子序列
- >
- > 给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
-
-输入是双串,首先思考双串问题中设计状态 dp\[i][j] 时拆分子问题的方式:枚举第一串的子序列的结尾和第二串的子序列的结尾来拆分子问题,设计状态 dp\[i][j] := text1 考虑 [0..i], text2 考虑 [0..j] 时,原问题的解,即 LCS 长度
-
-这个是单串 dp\[i][j] 的问题,状态的推导方向,以及推导公式如下
-
-
-
-
-
-状态的推导是按照 i 从 0 到 n - 1、j 从 0 到 m - 1 顺序推的,推到 dp\[i][j] 时,dp\[i - 1 .. 0][j - 1 .. 0] 均已经计算完。
-
-因为两个子序列需要相同,若两个串的末尾元素相同,则可以选择 text1[i] 和 text2[j],此时再根据此前已经 text1[0..i-1] 和 text[0..j-1] 的 LCS 长度。若两个串的末尾元素不同,则 text1[i] 和 text2[j] 中只能选一个,
-
-若选了 text1[i],则 text2 只能取到 j-1,此时 dp[i-1][j] 的结果就是当前状态 dp[i][j] 的结果。
-若选了 text2[j],则 text1 只能取到 i-1,此时 dp[i][j-1] 的结果就是当前状态 dp[i][j] 的结果。
-两个结果要取一个最长的。
-
-按照以上的分析,状态的转移方程可以写出来,如下
-
-```
-dp[i][j] =
-1. dp[i-1][j-1] + 1 (text1[i] == text2[j])
-2. max(dp[i][j-1], dp[i-1][j]) (text1[i] != text2[j])
-两者取较大值
-```
-
-
-
-> 72. 编辑距离
-> 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
->
-> 你可以对一个单词进行如下三种操作:
->
-> 插入一个字符
-> 删除一个字符
-> 替换一个字符
-
-输入是双串,首先思考双串问题中设计状态 dp[i][j] 时拆分子问题的方式:枚举第一串的子序列的结尾和第二串的子序列的结尾来拆分子问题,设计状态 dp[i][j] := word1 考虑 [0..i], word2 考虑 [0..j] 时,原问题的解,即 word1 转换成 word2 的最少操作数
-
-这个是单串 dp[i][j] 的问题,状态的推导方向,以及推导公式与上一题的图示相同
-
-同样地,状态的推导是按照 i 从 0 到 n - 1、j 从 0 到 m - 1 顺序推的,推到 dp[i][j] 时,dp[i - 1 .. 0][j - 1 .. 0] 均已经计算完。
-
-因为操作之后两个 word 需要相同,如果两个串的末尾元素 word1[i] 和 word2[j] 不相同,则可以在 word1 的末尾元素上使用插入,删除,替>换这三种操作,操作数都要 + 1,如果两个串的末尾元素 word1[i] 和 word2[j] 相同,依然可以在 word1 的末尾元素上使用插入,删除,替换这三种操作,但是此时如果使用改,则操作数不 +1,因为两个末尾元素已经相等了。
-
-按照以上的分析,状态的转移方程可以写出来,如下
-
-```
-dp[i][j] =
-1. dp[i][j-1] + 1 (最后一步是插入)
-2. dp[i-1][j] + 1 (最后一步是删)
-3. dp[i-1][j-1] + 1 (最后一步是改,且 word1[i] != word2[j])
-4. dp[i-1][j-1] (最后一步是改,且 word1[i] == word2[j])
-取较小值
-```
-
-
-
-## 双串相关练习题
-
-1. 最经典双串 LCS 系列
-2. 字符串匹配系列
-3. 其它双串 dp[i][j] 问题
-4. 带维度双串 dp[i][j][k]
-
-
-
-
-
-## 前缀和
-
-前缀和是一种查询数组中任意区间的元素的和的数据结构,这里数组给定之后就不变了。针对这个不变的数组,前缀和用于多次查询区间 [i, j] 上元素的和。
-
-对于动态规划而言,前缀和的意义主要有两点:
-
-一维和二维前缀和的推导,分别用到了单串和矩阵中最经典的状态设计以及状态转移;
-在一些更复杂的动态规划问题中,状态转移的时候需要依赖区间和,因为状态转移是非常频繁的操作,因此必须高效地求区间和才能使得状态转移的时间复杂度可接受,此时就必须用到前缀和了。
-除此之外,一些问题需要前缀和与其它数据结构配合来解决,也有两类:
-
-先预处理出前缀和数组,这一步是动态规划,然后在前缀和数组上用其它数据结构解决;
-还是按照动态规划的方式求前缀和,也需要额外的数据结构维护前缀和,但不是预处理好前缀和数组之后再用数据结构计算,而是每求出一个前缀和,就更新一次数据结构并维护答案。
-前缀和的推导和计算隐含着动态规划的基本思想,同时它的状态设计是线性动态规划中比较简单的那一类。与线性动态规划一样,前缀和也有一维和二维两种场景。
-虽然前缀和本身很简单,但需要用到它解决的问题非常多,与其它数据结构配合的变化也很多,因此需要从线性动态规划中剥离出来单独学习。
-
-
-
-
-
-## 区间动态规划
-
-在输入为长度为 n 的数组时,子问题用区间 [i..j] 表示。
-状态的定义和转移都与区间有关,称为区间动态规划
-
-
-
-### 区间动态规划简介
-
-区间 DP 是状态的定义和转移都与区间有关,其中区间用两个端点表示。
-
-状态定义 dp\[i][j] = [i..j] 上原问题的解。i 变大,j 变小都可以得到更小规模的子问题。
-
-对于单串上的问题,我们可以对比一下线性动态规划和区间动态规划。线性动态规划, 一般是定义 dp[i], 表示考虑到前 i 个元素,原问题的解,i 变小即得到更小规模的子问题,推导状态时候是从前往后,即 i 从小到大推的。区间动态规划,一般是定义 dp[i][j],表示考虑 [i..j] 范围内的元素,原问题的解增加 i,减小 j 都可以得到更小规模的子问题。推导状态一般是按照区间长度从短到长推的。
-
-区间动态规划的状态设计,状态转移都与线性动态规划有明显区别,但是由于这两种方法都经常用在单串问题上,拿到一个单串的问题时,往往不能快速地判断到底是用线性动态规划还是区间动态规划,这也是区间动态规划的难点之一。
-
-状态转移,推导状态 dp\[i][j] 时,有两种常见情况
-
-#### 1. dp\[i][j] 仅与常数个更小规模子问题有关
-
-一般是与 dp\[i + 1][j], dp\[i][j - 1], dp\[i + 1][j - 1] 有关。
-
-dp\[i][j] = f(dp\[i + 1][j], dp\[i][j - 1], dp\[i + 1][j - 1])
-
-
-
-代码常见写法
-
-```
-for len = 1..n
- for i = i..len
- j = i + len - 1
- dp[i][j] = max(dp[i][j], f(dp[i+1][j], dp[i][j-1], dp[i+1][j-1]))
-
-```
-
-时间复杂度和空间复杂度均为 O(n^{2})*O*(*n*2)
-
-例如力扣第 516 题,详细过程参考下一节。
-
-
-
-#### 2. dp\[i][j] 与 O(n) 个更小规模子问题有关
-
-一般是枚举 [i,j] 的分割点,将区间分为 [i,k] 和 [k+1,j],对每个 k 分别求解(下面公式的 f),再汇总(下面公式的 g)。
-
-dp\[i][j] = g(f(dp\[i][k], dp\[k + 1][j])) 其中 k = i .. j-1。
-
-
-
-代码常见写法, 以下代码以 f 为 max 为例
-
-```
-for len = 1..n
- for i = i..len
- j = i + len - 1
- for k = i..j
- dp[i][j] = max(dp[i][j], f(dp[i][k], dp[k][j]))
-```
-
-时间复杂度可以达到 O(n^3),空间复杂度还是 O(n^2)
-
-例如力扣第 664 题,详细过程参考下一节
-
-
-
-#### 总结
-
-区间动态规划一般用在单串问题上,以区间 [i, j] 为单位思考状态的设计和转移。它与线性动态规划在状态设计和状态转移上都有明显的不同,但由于这两个方法都经常用在单串问题上,导致我们拿到一个单串的问题时,经常不能快速反映出应该用哪种方法。这是区间动态规划的难点之一,但是这个难点也是好解决的,就是做一定数量的练习题,因为区间动态规划的题目比线性动态规划少很多,并且区间动态规划的状态设计和转移都比较朴素,变化也比线性动态规划少很多,所以通过不多的题目数量就可以把区间动态规划常见的方法和变化看个大概了。
-
-后续节介绍区间动态规划的几个典型例题,学习区间动态规划问题常见的模式。
-并且配有力扣上出现的区间动态规划的练习题,加深理解。
-
-
-
-### 区间动态规划经典问题
-
-#### 大规模问题与常数个小规模问题有关
-
-最常见的形式如下:
-
-推导 dp\[i][j] 时,需要用到 dp\[i][j-1], dp\[i+1][j], dp\[i+1][j-1] 三个子问题
-
-> 最长回文子序列
-> 此问题在力扣上也有,是 516 题。
-> 考虑一个字符串 s 的所有子序列, 这些子序列中最长的回文子序列长度是多少
-
-这个问题如果用线性动态规划的经典思路,状态如下:
-dp[i] := 考虑 [0..i] , 原文题的答案
-但是此后我们就遇到了困难,会发现这个状态有些难以转移
-
-而如果考虑区间动态规划,状态如下:
-dp[i][j] := 区间 [i..j] 上, 原问题的答案
-转移的时候,考虑 dp\[i][j-1], dp\[i+1][j], dp\[i+1][j-1] 这三个子问题,这是考虑把边界去掉的模式,回文的特点恰好时候这种模式,
-根据两个边界的元素关系可以得到转移方程如下:
-
-```
-dp\[i][j] = dp\[i + 1][j - 1] + 2; if(s[i] == s[j])
-dp\[i][j] = max(dp\[i + 1][j], dp\[i][j - 1]); if(s[i] != s[j])
-```
-
-回文是用区间动态规划解决的常见问题,有很多变种,下一节中列出的练习题有很多类似的。
-
-#### 大规模问题与 O(n) 个小规模问题有关
-
-推导 dp\[i][j] 时,需要 [i..j] 的所有子区间信息,其中子区间的其中一个端点与原区间重合,共 O(n)O(n) 个子区间
-
-最常见的形式
-dp\[i][j] = g(f(dp\[i][k], dp\[k][j])) 其中 k = i+1 .. j-1。
-
-其中 g 常见的有 max/min,例如 664 就是 min
-
-下面就以 664 题讲解这种模式的思考方式
-
-奇怪的打印机
-有台奇怪的打印机有以下两个特殊要求:
-
-打印机每次只能打印同一个字符序列。
-每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。
-给定一个只包含小写英文字母的字符串 s,你的任务是计算这个打印机打印它需要的最少次数。
-
-首先区间动态规划的状态定义与前面一样,还是经典的定义方式,状态定义模式化这也是区间动态规划的一个特点。
-
-dp\[i][j] := 打印出 [i..j] 上的字符需要的最少次数
-在转移时,枚举中间的切分位置 k,考虑 i 边界以及中间枚举的切分位置 k 转移时的情况
-
-i 要自己涂一次,则 dp\[i][j] = 1 + dp\[i + 1][j]
-其中第一项 1 表示 i 位置单独花费一次次数
-i 与中间的某个切分位置 k 一起打印 (条件是 s[i] = s[k]),则 dp\[i][j] = dp\[i+1][k] + dp\[k+1][j]
-其中第一项 dp\[i+1][k] 表示 i 位置跟着 k 一起转移了,不在单独考虑 i 花费的次数了
-综合以上分析可以写出状态转移方程如下
-
-```
-dp\[i][j] = dp\[i + 1][j] + 1;
-dp\[i][j] = min(dp\[i][j], dp\[i + 1][k] + dp\[k + 1][j]); 其中 i < k <= j 且 s[i] == s[k]
-```
-
-
-
-#### 总结
-
-本小节通过两个例题介绍了区间动态规划的状态转移的两种模式。这两种模式基本上就涵盖了大部分区间动态规划问题,后续节整理了力扣上出现的区间动态规划问题,通过这些题的练习,区间动态规划就可以掌握的差不多了。
-
-
-
-### 回文相关问题
-
-以下六道题是力扣上利用区间动态规划解决的与回文相关的问题。
-
-- 最长回文子串
-- 回文子串
-- 最长回文子序列
-- 段式回文
-- 统计不同回文子字符串
-- 让字符串成为回文串的最少插入次数 —— 最长回文子序列
-
-
-
diff --git a/docs/data-structure-algorithms/README.md b/docs/data-structure-algorithms/README.md
index 315fd6d525..c306a84679 100644
--- a/docs/data-structure-algorithms/README.md
+++ b/docs/data-structure-algorithms/README.md
@@ -1,119 +1,546 @@
-# 数据结构开篇
+---
+title: 数据结构与算法:Java开发者的必备修炼
+date: 2025-05-09
+categories: Algorithm
+---
+
+# 🚀 数据结构与算法开篇
+
+
+
+> 💡 **关于怎么刷题的帖子**:
+>
+> - 📖 《论如何4个月高效刷满 500 题并形成长期记忆》 https://leetcode-cn.com/circle/discuss/jq9Zke/
+
+---
+
+## 🎯 专栏介绍
+
+本专栏致力于为Java开发者提供全面的数据结构与算法学习资源,涵盖从基础概念到高级应用的完整知识体系。通过系统性的学习和实践,帮助开发者提升编程能力,掌握解决复杂问题的核心技能。
+
+### 📚 专栏特色
+
+- 🎯 **系统性学习**:从基础到进阶,循序渐进
+- 💻 **Java实现**:所有代码示例均使用Java语言
+- 🔥 **实战导向**:结合LeetCode经典题目
+- 📊 **可视化理解**:丰富的图表和动画演示
+- 🎨 **美观排版**:精心设计的文档格式
+
+---
+
+## 📊 第一部分:数据结构分类
+
+数据结构是计算机存储、组织数据的方式,是算法的基础。掌握各种数据结构的特点和应用场景,是成为优秀程序员的必经之路。
+
+### 📏 线性数据结构
+
+线性数据结构是指数据元素之间存在一对一关系的数据结构,元素按线性顺序排列。
+
+#### 📋 1. 数组 (Array)
+```
+数组结构示意图:
+Index: [0] [1] [2] [3] [4]
+Value: [12][45][78][23][56]
+ ↑ ↑ ↑ ↑ ↑
+ 连续的内存地址空间
+```
+- **特点**:连续存储,支持随机访问
+- **时间复杂度**:访问O(1),插入/删除O(n)
+- **Java实现**:`int[]`、`ArrayList`
+
+#### 🔗 2. 链表 (Linked List)
+```
+单链表结构示意图:
+[Data|Next] -> [Data|Next] -> [Data|Next] -> null
+ | | |
+ 节点1 节点2 节点3
+
+双链表结构示意图:
+null <- [Prev|Data|Next] <-> [Prev|Data|Next] <-> [Prev|Data|Next] -> null
+```
+- **特点**:动态存储,插入删除高效
+- **时间复杂度**:访问O(n),插入/删除O(1)
+- **Java实现**:`LinkedList`
+
+#### 📚 3. 栈 (Stack)
+```
+栈结构示意图:
+ | | <- top (栈顶)
+ | C |
+ | B |
+ | A |
+ |_____| <- bottom (栈底)
+
+操作:LIFO (后进先出)
+```
+- **特点**:后进先出(LIFO)
+- **时间复杂度**:push/pop/peek都是O(1)
+- **Java实现**:`Stack`、`Deque`
+
+#### 🚶 4. 队列 (Queue)
+```
+队列结构示意图:
+出队 <- [A][B][C][D] <- 入队
+ front rear
+
+操作:FIFO (先进先出)
+```
+- **特点**:先进先出(FIFO)
+- **时间复杂度**:enqueue/dequeue都是O(1)
+- **Java实现**:`Queue`、`LinkedList`
+
+### 🌳 非线性数据结构
+
+非线性数据结构是指数据元素之间存在一对多或多对多关系的数据结构,形成复杂的层次或网状结构。
+
+#### 🌲 5. 二叉树 (Binary Tree)
+```
+二叉树结构示意图:
+ A (根节点)
+ / \
+ B C
+ / \ / \
+ D E F G (叶子节点)
+
+层次:
+第0层: A
+第1层: B C
+第2层: D E F G
+```
+- **特点**:每个节点最多有两个子节点
+- **时间复杂度**:搜索/插入/删除O(logn)~O(n)
+- **Java实现**:`TreeMap`、`TreeSet`
+
+#### 🏔️ 6. 堆 (Heap)
+```
+最大堆示意图:
+ 90
+ / \
+ 80 70
+ / \ / \
+ 60 50 40 30
+
+特点:父节点 >= 子节点 (最大堆)
+数组表示:[90,80,70,60,50,40,30]
+```
+- **特点**:完全二叉树,堆序性质
+- **时间复杂度**:插入/删除O(logn),查找最值O(1)
+- **Java实现**:`PriorityQueue`
+
+#### 🕸️ 7. 图 (Graph)
+```
+无向图示意图:
+ A --- B
+ /| |\
+ / | | \
+ D | | C
+ | | /
+ E --- F
+
+邻接矩阵表示:
+ A B C D E F
+A [ 0 1 0 1 1 0 ]
+B [ 1 0 1 0 0 1 ]
+C [ 0 1 0 0 0 1 ]
+D [ 1 0 0 0 1 0 ]
+E [ 1 0 0 1 0 1 ]
+F [ 0 1 1 0 1 0 ]
+```
+- **特点**:顶点和边的集合,表示复杂关系
+- **时间复杂度**:遍历O(V+E),最短路径O(V²)
+- **Java实现**:`Map>`
+
+#### 🗂️ 8. 哈希表 (Hash Table)
+```
+哈希表结构示意图:
+Hash函数:key → hash(key) % size → index
+
+Key: "apple" → hash("apple") = 5 → index: 5 % 8 = 5
+Key: "banana" → hash("banana")= 3 → index: 3 % 8 = 3
+
+Table:
+[0] → null
+[1] → null
+[2] → null
+[3] → "banana" → value
+[4] → null
+[5] → "apple" → value
+[6] → null
+[7] → null
+
+冲突处理(链地址法):
+[3] → ["banana",value1] → ["grape",value2] → null
+```
+- **特点**:基于键值对,快速查找
+- **时间复杂度**:平均O(1),最坏O(n)
+- **Java实现**:`HashMap`、`HashSet`
+
+---
+
+## ⚡ 第二部分:常见算法
+
+算法是解决问题的步骤和方法,是程序设计的核心。掌握各种算法的思想和实现,能够帮助我们高效地解决各种复杂问题。
+
+### 🔢 1. 排序算法
+
+排序算法是计算机科学中最基础也是最重要的算法之一,掌握各种排序算法的特点和应用场景至关重要。
+
+| 算法 | 时间复杂度(平均) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
+|------|-------------------|-------------------|------------|--------|
+| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
+| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
+| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
+| 快速排序 | O(nlogn) | O(n²) | O(logn) | 不稳定 |
+| 归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 |
+| 堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
+| 计数排序 | O(n+k) | O(n+k) | O(k) | 稳定 |
+| 基数排序 | O(nk) | O(nk) | O(n+k) | 稳定 |
+
+### 🔍 2. 搜索算法
+
+搜索算法用于在数据集合中查找特定元素,不同的搜索算法适用于不同的场景。
+
+- **线性搜索**:O(n) - 适用于无序数组
+- **二分搜索**:O(logn) - 适用于有序数组
+- **哈希查找**:O(1) - 适用于HashMap/HashSet
+- **树搜索**:O(logn) - 适用于二叉搜索树
+- **图搜索**:DFS/BFS - O(V+E)
+
+### 🕸️ 3. 图算法
+
+图算法是处理图结构数据的重要工具,广泛应用于网络分析、路径规划等领域。
+
+#### 🔍 3.1 图遍历
+- **深度优先搜索(DFS)**:O(V+E)
+- **广度优先搜索(BFS)**:O(V+E)
+
+#### 🛣️ 3.2 最短路径
+- **Dijkstra算法**:O((V+E)logV) - 单源最短路径
+- **Floyd-Warshall算法**:O(V³) - 全源最短路径
+- **Bellman-Ford算法**:O(VE) - 含负权边
+
+#### 🌲 3.3 最小生成树
+- **Prim算法**:O(ElogV)
+- **Kruskal算法**:O(ElogE)
-
+#### 📋 3.4 拓扑排序
+- **Kahn算法**:O(V+E)
+- **DFS算法**:O(V+E)
-## 概念
+### 🎯 4. 动态规划
-**数据**(data)是描述客观事物的数值、字符以及能输入机器且能被处理的各种符号集合。 数据的含义非常广泛,除了通常的数值数据、字符、字符串是数据以外,声音、图像等一切可以输入计算机并能被处理的都是数据。例如除了表示人的姓名、身高、体重等的字符、数字是数据,人的照片、指纹、三维模型、语音指令等也都是数据。
+动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
+#### 🏗️ 4.1 基础DP
+- **斐波那契数列**:O(n)
+- **爬楼梯**:O(n)
+- **最大子序和**:O(n)
+#### 📝 4.2 序列DP
+- **最长递增子序列(LIS)**:O(nlogn)
+- **最长公共子序列(LCS)**:O(mn)
+- **编辑距离**:O(mn)
-**数据元素**(data element)是数据的基本单位,是数据集合的个体,在计算机程序中通常作为一个整体来进行处理。例如一条描述一位学生的完整信息的数据记录就是一个数据元素;空间中一点的三维坐标也可以是一个数据元素。数据元素通常由若干个数据项组成,例如描述学生相关信息的姓名、性别、学号等都是数据项;三维坐标中的每一维坐标值也是数据项。数据项具有原子性,是不可分割的最小单位。
+#### 🎒 4.3 背包问题
+- **0-1背包**:O(nW)
+- **完全背包**:O(nW)
+- **多重背包**:O(nWlogM)
+#### 📏 4.4 区间DP
+- **最长回文子串**:O(n²)
+- **矩阵链乘法**:O(n³)
+### 🎯 5. 贪心算法
-**数据对象**(data object)是性质相同的数据元素的集合,是数据的子集。例如一个学校的所有学生的集合就是数据对象,空间中所有点的集合也是数据对象。
+贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,从而希望导致结果是最好或最优的算法。
+- **活动选择问题**:O(nlogn)
+- **分数背包**:O(nlogn)
+- **最小生成树(Prim/Kruskal)**:O(ElogV)
+- **霍夫曼编码**:O(nlogn)
+- **区间调度**:O(nlogn)
+### 🔄 6. 分治算法
-**数据结构**(data structure)是指相互之间存在一种或多种特定关系的数据元素的集合。是组织并存储数据以便能够有效使用的一种专门格式,它用来反映一个数据的内部构成,即一个数据由哪些成分数据构成,以什么方式构成,呈什么结构。
+分治算法是一种很重要的算法,字面上的解释是"分而治之",就是把一个复杂的问题分成两个或更多的相同或相似的子问题。
-由于信息可以存在于逻辑思维领域,也可以存在于计算机世界,因此作为信息载体的数据同样存在于两个世界中。表示一组数据元素及其相互关系的数据结构同样也有两种不同的表现形式,一种是数据结构的逻辑层面,即数据的逻辑结构;一种是存在于计算机世界的物理层面,即数据的存储结构
+- **归并排序**:O(nlogn)
+- **快速排序**:O(nlogn)
+- **二分搜索**:O(logn)
+- **最大子数组和**:O(nlogn)
+- **最近点对**:O(nlogn)
+### 🔙 7. 回溯算法
+回溯算法是一种通过穷举所有可能情况来找到所有解的算法。当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择。
-## 逻辑结构和物理结构
+- **N皇后问题**:O(N!)
+- **数独求解**:O(9^(n*n))
+- **全排列**:O(n!)
+- **子集生成**:O(2^n)
+- **组合问题**:O(C(n,k))
-按照视点的不同,我们把数据结构分为逻辑结构和物理结构。
+### 📝 8. 字符串算法
-### 逻辑结构
+字符串算法是处理文本数据的重要工具,广泛应用于文本搜索、模式匹配等领域。
-是指数据对象中数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。分为以下四种:
+- **KMP算法**:O(n+m) - 字符串匹配
+- **Rabin-Karp算法**:O(n+m) - 字符串匹配
+- **最长公共前缀**:O(S) - S为所有字符串长度和
+- **字典树(Trie)**:插入/查找 O(m)
+- **后缀数组**:O(nlogn)
-- 集合结构:集合结构中的数据元素除了同属于一个集合外,他们之间没有其他关系
+### 🧮 9. 数学算法
- 
+数学算法是解决数学问题的计算方法,在编程中经常需要用到各种数学算法。
-- 线性结构:数据之间是一对一关系
+- **最大公约数(GCD)**:O(logn)
+- **快速幂**:O(logn)
+- **素数筛选**:O(nloglogn)
+- **模运算**:O(1)
+- **组合数学**:O(nlogn)
- 
+### 🔢 10. 位运算算法
-- 树形结构:数据之间存在一对多的层次关系
+位运算是计算机中最底层的运算,掌握位运算技巧可以写出更高效的代码。
- 
+- **位运算基础**:O(1)
+- **状态压缩DP**:O(n*2^m)
+- **子集枚举**:O(2^n)
+- **位操作技巧**:O(1)
-- 图形结构:数据之间多对多的关系
+### 🏗️ 11. 高级数据结构算法
- 
+高级数据结构算法是建立在基础数据结构之上的复杂算法,能够解决更复杂的问题。
-### 物理结构
+- **并查集**:O(α(n)) - 接近常数时间
+- **线段树**:O(logn) - 区间查询/更新
+- **树状数组**:O(logn) - 前缀和查询
+- **平衡树(AVL/红黑树)**:O(logn)
+- **跳表**:O(logn)
-是指数据的逻辑结构在计算机中的存储形式。(有时也被叫存储结构)
+---
-数据是数据元素的集合,根据物理结构的定义,实际上就是如何把数据元素存储到计算机的存储器中。存储器主要是针对内存而言的,像硬盘、软盘、光盘等外部存储器的数据组织通常用文件结构来描述。
+## 🎯 第三部分:LeetCode经典题目
-数据元素的存储结构形式有两种:顺序存储和链式存储。
+LeetCode是程序员刷题的重要平台,通过系统性的刷题练习,可以快速提升算法能力。以下是按类型分类的经典题目。
-- 顺序存储:把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系一致
+### 📋 1. 数组类题目
- 
+数组是最基础的数据结构,掌握数组的各种操作技巧是算法学习的基础。
-- 链式存储:把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。
+#### 🔧 基础操作
+- **1. 两数之和** - 哈希表优化
+- **26. 删除排序数组中的重复项** - 双指针
+- **27. 移除元素** - 双指针
+- **88. 合并两个有序数组** - 双指针
- 
+#### 🔍 搜索与查找
+- **33. 搜索旋转排序数组** - 二分搜索
+- **34. 在排序数组中查找元素的第一个和最后一个位置** - 二分搜索
+- **35. 搜索插入位置** - 二分搜索
+- **153. 寻找旋转排序数组中的最小值** - 二分搜索
+#### 👆 双指针技巧
+- **15. 三数之和** - 排序+双指针
+- **16. 最接近的三数之和** - 排序+双指针
+- **18. 四数之和** - 排序+双指针
+- **42. 接雨水** - 双指针
+- **11. 盛最多水的容器** - 双指针
+#### 🪟 滑动窗口
+- **3. 无重复字符的最长子串** - 滑动窗口
+- **76. 最小覆盖子串** - 滑动窗口
+- **209. 长度最小的子数组** - 滑动窗口
+- **438. 找到字符串中所有字母异位词** - 滑动窗口
-## 抽象数据结构类型
+### 🔗 2. 链表类题目
-**数据类型**(data type)是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。例如 Java 语言中就有许多不同的数据类型,包括数值型的数据类型、字符串、布尔型等数据类型。以 Java 中的 int 型为例,int 型的数据元素的集合是[-2147483648,2147483647] 间的整数,定义在其上的操作有加、减、乘、除四则运算,还有模运算等。
+链表是动态数据结构,掌握链表的操作技巧对于理解指针和递归非常重要。
-数据类型是按照值得不同进行划分的。在高级语言中,每个变量、常量和表达式都有各自的取值范围。类型就用来说明变量或表达式取值范围和所能进行的操作。
+#### 🔧 基础操作
+- **206. 反转链表** - 迭代/递归
+- **21. 合并两个有序链表** - 双指针
+- **83. 删除排序链表中的重复元素** - 单指针
+- **82. 删除排序链表中的重复元素 II** - 双指针
-定义数据类型的作用一个是隐藏计算机硬件及其特性和差别,使硬件对于用户而言是透明的,即用户可以不关心数据类型是怎么实现的而可以使用它。定义数据类型的另一个作用是,用户能够使用数据类型定义的操作,方便的实现问题的求解。例如,用户可以使用 Java 定义在 int 型的加法操作完成两个整数的加法运算,而不用关心两个整数的加法在计算机中到底是如何实现的。这样不但加快了用户解决问题的速度,也使得用户可以在更高的层面上 考虑问题。
+#### 👆 双指针技巧
+- **141. 环形链表** - 快慢指针
+- **142. 环形链表 II** - 快慢指针
+- **160. 相交链表** - 双指针
+- **19. 删除链表的倒数第 N 个结点** - 快慢指针
-**抽象数据类型**(abstract data type, 简称 ADT)由一种数据模型和在该数据模型上的一组操作组成。
+#### 🔄 复杂操作
+- **234. 回文链表** - 快慢指针+反转
+- **143. 重排链表** - 找中点+反转+合并
+- **148. 排序链表** - 归并排序
+- **23. 合并K个排序链表** - 分治/优先队列
+
+### 📚 3. 栈与队列类题目
+
+栈和队列是重要的线性数据结构,在算法中有着广泛的应用。
-抽象数据类型一方面使得使用它的人可以只关心它的逻辑特征,不需要了解它的实现方式。另一方面可以使我们更容易描述现实世界,使得我们可以在更高的层面上来考虑问题。 例如可以使用树来描述行政区划,使用图来描述通信网络。
-
-
-
-## 数据结构分类
-
-- 数组
-- 栈
-- 链表
-- 队列
-- 树
-- 图
-- 堆
-- 散列表
-
-
-
-# 算法
-
-算法设计是最具创造性的工作之一,人们解决任何问题的思想、方法和步骤实际上都可以认为是算法。人们解决问题的方法有好有坏,因此算法在性能上也就有高低之分。
-
-## 概念
-
-算法(algorithm)是指令的集合,是为解决特定问题而规定的一系列操作。它是明确定义的可计算过程,以一个数据集合作为输入,并产生一个数据集合作为输出。一个算法通常来说具有以下五个特性:
-
-- 输入:一个算法应以待解决的问题的信息作为输入。
-- 输出:输入对应指令集处理后得到的信息。
-- 可行性:算法是可行的,即算法中的每一条指令都是可以实现的,均能在有限的时间内完成。
-- 有穷性:算法执行的指令个数是有限的,每个指令又是在有限时间内完成的,因此 整个算法也是在有限时间内可以结束的。
-- 确定性:算法对于特定的合法输入,其对应的输出是唯一的。即当算法从一个特定 输入开始,多次执行同一指令集结果总是相同的。 对于随机算法,该特性应当被放宽
-
-
-
-## 算法设计要求
-
-- 正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义、能正确反映问题的需求、能得到问题的正确答案
-- 可读性:算法设计的另一目的是为了便于阅读、理解和交流
-- 健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或错误结果
-- 时间效率高和存储量低
-
-
-
-
\ No newline at end of file
+#### 📚 栈的应用
+- **20. 有效的括号** - 栈匹配
+- **155. 最小栈** - 辅助栈
+- **225. 用队列实现栈** - 设计问题
+- **232. 用栈实现队列** - 设计问题
+
+#### 📈 单调栈
+- **496. 下一个更大元素 I** - 单调栈
+- **503. 下一个更大元素 II** - 单调栈
+- **739. 每日温度** - 单调栈
+- **84. 柱状图中最大的矩形** - 单调栈
+
+### 4. 树类题目
+
+#### 二叉树遍历
+- **94. 二叉树的中序遍历** - 递归/迭代
+- **144. 二叉树的前序遍历** - 递归/迭代
+- **145. 二叉树的后序遍历** - 递归/迭代
+- **102. 二叉树的层序遍历** - BFS
+
+#### 二叉树性质
+- **104. 二叉树的最大深度** - DFS
+- **111. 二叉树的最小深度** - BFS/DFS
+- **226. 翻转二叉树** - DFS
+- **101. 对称二叉树** - DFS
+
+#### 二叉搜索树
+- **98. 验证二叉搜索树** - 中序遍历
+- **700. 二叉搜索树中的搜索** - 二分搜索
+- **701. 二叉搜索树中的插入操作** - 递归
+- **450. 删除二叉搜索树中的节点** - 递归
+
+#### 树的路径问题
+- **112. 路径总和** - DFS
+- **113. 路径总和 II** - DFS+回溯
+- **124. 二叉树中的最大路径和** - DFS
+- **257. 二叉树的所有路径** - DFS+回溯
+
+### 5. 图类题目
+
+#### 图的遍历
+- **200. 岛屿数量** - DFS/BFS
+- **695. 岛屿的最大面积** - DFS/BFS
+- **994. 腐烂的橘子** - BFS
+- **130. 被围绕的区域** - DFS/BFS
+
+#### 拓扑排序
+- **207. 课程表** - 拓扑排序
+- **210. 课程表 II** - 拓扑排序
+
+#### 联通分量
+- **547. 省份数量** - 并查集/DFS
+- **684. 冗余连接** - 并查集
+- **721. 账户合并** - 并查集
+
+### 6. 动态规划类题目
+
+#### 基础DP
+- **70. 爬楼梯** - 基础DP
+- **198. 打家劫舍** - 线性DP
+- **213. 打家劫舍 II** - 环形DP
+- **53. 最大子序和** - Kadane算法
+
+#### 序列DP
+- **300. 最长递增子序列** - LIS
+- **1143. 最长公共子序列** - LCS
+- **72. 编辑距离** - 字符串DP
+- **5. 最长回文子串** - 区间DP
+
+#### 背包问题
+- **416. 分割等和子集** - 0-1背包
+- **494. 目标和** - 0-1背包
+- **322. 零钱兑换** - 完全背包
+- **518. 零钱兑换 II** - 完全背包
+
+#### 状态机型DP
+- **121. 买卖股票的最佳时机** - 状态机DP
+- **122. 买卖股票的最佳时机 II** - 状态机DP
+- **123. 买卖股票的最佳时机 III** - 状态机DP
+- **188. 买卖股票的最佳时机 IV** - 状态机DP
+
+### 7. 回溯算法类题目
+
+#### 排列组合
+- **46. 全排列** - 回溯
+- **47. 全排列 II** - 回溯+去重
+- **77. 组合** - 回溯
+- **78. 子集** - 回溯
+
+#### 分割问题
+- **131. 分割回文串** - 回溯+动态规划
+- **93. 复原IP地址** - 回溯
+
+#### 棋盘问题
+- **51. N 皇后** - 回溯
+- **37. 解数独** - 回溯
+
+### 8. 贪心算法类题目
+
+#### 区间问题
+- **435. 无重叠区间** - 贪心
+- **452. 用最少数量的箭引爆气球** - 贪心
+- **55. 跳跃游戏** - 贪心
+- **45. 跳跃游戏 II** - 贪心
+
+### 9. 字符串类题目
+
+#### 字符串匹配
+- **28. 实现 strStr()** - KMP算法
+- **459. 重复的子字符串** - KMP算法
+
+#### 回文串
+- **125. 验证回文串** - 双指针
+- **5. 最长回文子串** - 中心扩展
+- **647. 回文子串** - 中心扩展
+
+### 10. 位运算类题目
+
+- **136. 只出现一次的数字** - 异或运算
+- **191. 位1的个数** - 位运算
+- **338. 比特位计数** - 位运算+DP
+- **461. 汉明距离** - 位运算
+
+
+
+### 🎯 刷题建议
+1. **由易到难**:从Easy开始,逐步提升到Hard
+2. **分类练习**:按数据结构和算法类型分类刷题
+3. **反复练习**:重要题目要反复练习,形成肌肉记忆
+4. **总结模板**:每种题型都要总结解题模板
+5. **时间管理**:每天固定时间刷题,保持连续性
+6. **记录总结**:建立错题本,定期回顾总结
+
+---
+
+## 🛠️ 学习工具推荐
+
+### 📚 在线平台
+- [LeetCode中国](https://leetcode-cn.com/) - 主要刷题平台
+- [牛客网](https://www.nowcoder.com/) - 面试真题
+- [AcWing](https://www.acwing.com/) - 算法竞赛
+- [洛谷](https://www.luogu.com.cn/) - 算法学习
+
+### 🎥 学习资源
+- [算法可视化](https://visualgo.net/) - 算法动画演示
+- [VisuAlgo](https://visualgo.net/) - 数据结构可视化
+- [算法导论](https://mitpress.mit.edu/books/introduction-algorithms) - 经典教材
+- [算法4](https://algs4.cs.princeton.edu/) - Java实现
+
+### 💻 开发工具
+- **IDE**:IntelliJ IDEA、Eclipse
+- **调试**:LeetCode插件、本地调试
+- **版本控制**:Git管理代码
+- **笔记**:Markdown记录学习心得
+
+---
+
+> 💡 **记住**:数据结构和算法是程序员的内功,需要持续练习和积累
+>
+> 🚀 **建议**:每天至少刷一道LeetCode,坚持100天必有收获
+>
+> 📚 **资源**:[LeetCode中国](https://leetcode-cn.com/) | [算法可视化](https://visualgo.net/)
+>
+> 🎉 **加油**:相信通过系统性的学习,你一定能够掌握数据结构和算法的精髓!
\ No newline at end of file
diff --git a/docs/data-structure-algorithms/Recursion.md b/docs/data-structure-algorithms/Recursion.md
deleted file mode 100644
index b139d44ce7..0000000000
--- a/docs/data-structure-algorithms/Recursion.md
+++ /dev/null
@@ -1,646 +0,0 @@
-
-
-
-
-文章目录:
-
-1. 什么是递归
-2.
-
-
-
-**什么是递归**
-
-递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题,而递归是把问题逐级分解,是纵向的拆分。
-
-**简单地说,就是如果在函数中存在着调用函数本身的情况,这种现象就叫递归。**
-
-你以前肯定写过递归,只是不知道这就是递归罢了。
-
-
-
-以阶乘函数为例,如下, 在 factorial 函数中存在着 factorial(n - 1) 的调用,所以此函数是递归函数
-
-```
-public int factorial(int n) {
- if (n < =1) {
- return1;
- }
- return n * factorial(n - 1)
-}
-int fibonacci(int n) {
- // Base case
- if (n == 0 || n == 1) return n;
-
- // Recursive step
- return fibonacci(n-1) + fibonacci(n-2);
-}
-```
-
-进一步剖析「递归」,先有「递」再有「归」,「递」的意思是将问题拆解成子问题来解决, 子问题再拆解成子子问题,...,直到被拆解的子问题无需再拆分成更细的子问题(即可以求解),「归」是说最小的子问题解决了,那么它的上一层子问题也就解决了,上一层的子问题解决了,上上层子问题自然也就解决了,....,直到最开始的问题解决,文字说可能有点抽象,那我们就以阶层 f(6) 为例来看下它的「递」和「归」。
-
-
-
-求解问题 f(6), 由于 f(6) = n * f(5), 所以 f(6) 需要拆解成 f(5) 子问题进行求解,同理 f(5) = n * f(4) ,也需要进一步拆分,... ,直到 f(1), 这是「递」,f(1) 解决了,由于 f(2) = 2 f(1) = 2 也解决了,.... f(n)到最后也解决了,这是「归」,所以递归的本质是能把问题拆分成具有**相同解决思路**的子问题,。。。直到最后被拆解的子问题再也不能拆分,解决了最小粒度可求解的子问题后,在「归」的过程中自然顺其自然地解决了最开始的问题。
-
-
-
- 递归原理
-
-------
-
-> 递归是一种解决问题的有效方法,在递归过程中,函数将自身作为子例程调用
-
-你可能想知道如何实现调用自身的函数。诀窍在于,每当递归函数调用自身时,它都会将给定的问题拆解为子问题。递归调用继续进行,直到到子问题无需进一步递归就可以解决的地步。
-
-为了确保递归函数不会导致无限循环,它应具有以下属性:
-
-1. 一个简单的`基本案例(basic case)`(或一些案例) —— 能够不使用递归来产生答案的终止方案。
-2. 一组规则,也称作`递推关系(recurrence relation)`,可将所有其他情况拆分到基本案例。
-
-注意,函数可能会有多个位置进行自我调用。
-
-
-
-递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题,而递归是把问题逐级分解,是纵向的拆分。
-
-递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。
-
-```
-int func(你今年几岁) {
- // 最简子问题,结束条件
- if (你1999年几岁) return 我0岁;
- // 自我调用,缩小规模
- return func(你去年几岁) + 1;
-}
-```
-
-
-
-
-
-### 反转字符串(344)
-
-> 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 `char[]` 的形式给出。
->
-> 不要给另外的数组分配额外的空间,你必须**原地修改输入数组**、使用 O(1) 的额外空间解决这一问题。
->
-> 你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
->
-> **示例 1:**
->
-> ```
-> 输入:["h","e","l","l","o"]
-> 输出:["o","l","l","e","h"]
-> ```
-
-
-
-
-
-
-
-
-
-# 递归
-
-递归实在计算机科学、数学等领域运用非常广泛的一种方法。使用递归的方法解决问题,一般具有这样的特征:我们在寻找一个复杂问题的解时,不能立即给出答案,然后从一个规模较小的相同问题的答案开始,却可以较为容易的求解复杂的问题。
-
-我们主要介绍两种基于递归的算法设计技术,即基于归纳的递归和分治法。
-
-
-
-## 概念
-
-递归(recursion)是指在定义自身的同时又出现了对自身的引用。如果一个算法直接或间接的调用自己,则称这个算法是一个递归算法。
-
-递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。
-
-任何一个有意义的递归算法总是两部分组成:**递归调用**和**递归终止条件**。
-
-
-
-## 如何理解递归
-
-递归是一种应用非常广泛的算法或者编程技巧。很多数据结构和算法的编码实现都要用到递归,比如DFS深度优先搜索、前中后序二叉树遍历等等。所以搞懂递归对学习一些复杂的数据结构和算法是非常有必要的。
-
-案例:*周末带着女朋友去电影院看电影,女朋友问,咱们现在坐在第几排啊?电影院里面太黑了,看不清,没法数,现在怎么办?*
-
-于是你就问前面一排的人他是第几排,你想只要在他的数字上加一,就知道自己在哪一排了。但是,前面的人也看不清啊,所以他也问他前面的人。就这样一排一排往前问,直到问到第一排的人,说我在第一排,然后再这样一排一排再把数字传回来。直到你前面的人告诉你他在哪一排,于是你就知道答案了。
-
-这就是一个非常标准的递归求解问题的分解过程,去的过程叫“递”,回来的过程叫“归”。
-
-基本上,所有的递归问题都可以用递推公式来表示。比如上面的案例我们用递推公式将它表示出来就是这样:
-
-```
-f(n) = f(n-1) + 1 //其中 f(1) = 1
-```
-
-f(n) 表示想知道自己在哪一排,f(n-1) 表示前面一排所在的排数,f(1) = 1表示第一排的人知道自己在第一排。有了这个递推公式,我们就可以很轻松地将它改为递归代码:
-
-```
-int f(int n) {
- if (n == 1) return 1;
- return f(n - 1) + 1;
-}
-```
-
-
-
-## 递归需要满足的三个条件
-
-只要同时满足以下三个条件,就可以用递归来解决。
-
-1. **一个问题的解可以分解为几个子问题的解**
-
- 何为子问题?子问题就是数据规模更小的问题。比如前面的案例,要知道“自己在哪一排”,可以分解为“前一排的人在哪一排”这样的一个子问题。
-
-2. **这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样**
-
- 如案例所示,求解“自己在哪一排”的思路,和前面一排人求解“自己在哪一排”的思路是一模一样的。
-
-3. **存在递归终止条件**
-
- 把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。前面的案例:第一排的人知道自己在哪一排,不需要再问别人,f(1) = 1就是递归的终止条件。
-
-
-
-## 怎样编写递归代码
-
-写递归代码,可以按三步走:
-
-**第一要素:明确你这个函数想要干什么**
-
-对于递归,我觉得很重要的一个事就是,**这个函数的功能是什么**,他要完成什么样的一件事,而这个,是完全由你自己来定义的。也就是说,我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。
-
-例如,我定义了一个函数
-
-```
-// 算 n 的阶乘(假设n不为0)
-int f(int n){
-
-}
-```
-
-这个函数的功能是算 n 的阶乘。好了,我们已经定义了一个函数,并且定义了它的功能是什么,接下来我们看第二要素。
-
-**第二要素:寻找递归结束条件**
-
-所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出**递归的结束条件**,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出**当参数为啥时,递归结束,之后直接把结果返回**,请注意,这个时候我们必须能根据这个参数的值,能够**直接**知道函数的结果是什么。
-
-例如,上面那个例子,当 n = 1 时,那你应该能够直接知道 f(n) 是啥吧?此时,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面,如下
-
-```
-// 算 n 的阶乘(假设n不为0)
-int f(int n){
- if(n == 1){
- return 1;
- }
-}
-```
-
-有人可能会说,当 n = 2 时,那我们可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作为递归的结束条件吗?
-
-当然可以,只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。
-
-```
-// 算 n 的阶乘(假设n>=2)
-int f(int n){
- if(n == 2){
- return 2;
- }
-}
-```
-
-注意我代码里面写的注释,假设 n >= 2,因为如果 n = 1时,会被漏掉,当 n <= 2时,f(n) = n,所以为了更加严谨,我们可以写成这样:
-
-```
-// 算 n 的阶乘(假设n不为0)
-int f(int n){
- if(n <= 2){
- return n;
- }
-}
-```
-
-**第三要素:找出函数的等价关系式**
-
-第三要素就是,我们要**不断缩小参数的范围**,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。
-
-例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。
-
-说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即
-
-f(n) = n * f(n-1)。
-
-
-
-写递归代码最关键的是**写出递推公式,找到终止条件**,剩下就是将递推公式转化为代码。
-
-案例:*假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?如果有 7 个台阶,你可以 2,2,2,1 这样子上去,也可以 1,2,1,1,2 这样子上去,总之走法有很多,那如何用编程求得总共有多少种走法呢?*
-
-我们可以根据第一步的走法把所有走法分为两类,第一类是第一步走了1个台阶,另一类是第一步走了2个台阶。所以n个台阶的走法就等于先走1阶后,n-1个台阶的走法 加上先走2阶后,n-2个台阶的走法,用公式表示:
-
-```
-f(n) = f(n-1) + f(n-2)
-```
-
-再来看下终止条件。当有一个台阶时,我们不需要再继续递归,就只有一种走法。所以f(1) = 1。这个递归终止条件足够吗?我们试试用n = 2, n = 3这样比较小的数实验一下。
-
-n = 2时,f(2) = f(1) + f(0)。如果递归终止条件只有一个f(1) = 1,那f(2)就无法求解了。所以除了f(1) = 1这一个递归终止条件外,还要有f(0) = 1,表示走0个台阶有一种走法,不过这样看起来不符合正常的逻辑思维。所以,我们可以把f(2) = 2作为一种终止条件,表示走2个台阶,只有两种走法,一步走完或者分两步走。
-
-所以,递归终止条件就是f(1) = 1,f(2) = 2。这个时候,可以再拿n = 3,n = 4来验证下,这个终止条件是否足够并且正确。
-
-我们把递归终止条件和刚刚得到的递推公式放在一起就是这样:
-
-```
-f(1) = 1;
-f(2) = 2;
-f(n) = f(n - 1) + f(n - 2);
-```
-
-最终的递归代码就是这样:
-
-```
-int f(int n) {
- if (n == 1) return 1;
- if (n == 2) return 2;
- return f(n -1) + f(n - 2);
-}
-```
-
-**写递归代码的关键就是找到如何将大问题分解为小问题的规律,请求基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码**。
-
-> 当我们面对一个问题需要分解为多个子问题的时候,递归代码往往没那么好理解,比如第二个案例,人脑几乎没办法把整个“递”和“归”的过程一步一步都想清楚。
->
-> 计算机擅长做重复的事情,所以递归正符合它的胃口。而我们人脑更喜欢平铺直叙的思维方式。当我们看到递归时,我们总想把递归平铺展开,脑子里就会循环,一层一层往下调,然后再一层一层返回,试图想搞清楚计算机每一步都是怎么执行的,这样就很容易被绕进去。
->
-> 对于递归代码,这种试图想清楚整个递和归过程的做法,实际上是进入了一个思维误区。很多时候,我们理解起来比较吃力,主要原因就是自己给自己制造了这种理解障碍。那正确的思维方式应该是怎样的呢?
->
-> 如果一个问题 A 可以分解为若干子问题 B、C、D,可以假设子问题 B、C、D 已经解决,在此基础上思考如何解决问题 A。而且,只需要思考问题 A 与子问题 B、C、D 两层之间的关系即可,不需要一层一层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。屏蔽掉递归细节,这样子理解起来就简单多了。
-
-换句话说就是:千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。
-
-所以,编写递归代码的关键是:**只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤**。
-
-
-
-## 递归代码要警惕堆栈溢出
-
-在实际开发中,编写递归代码我们通常会遇到很多问题,比如堆栈溢出。而堆栈溢出会造成系统性崩溃,后果非常严重。为什么递归代码容易造成堆栈溢出呢?
-
-我们知道在函数调用时,会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险,出现`java.lang.StackOverflowError`。
-
-如何避免出现堆栈溢出?
-
-可以通过在代码中限制递归调用的最大深度的方式来解决这个问题。递归调用超过一定深度(比如1000)之后,我们就不再继续往下递归了,直接返回报错。比如前面电影院的案例,改造后的伪代码如下:
-
-```c
-// 全局变量,表示递归的深度。
-int depth = 0;
-
-int f(int n) {
- ++depth;
- if (depth > 1000) throw exception;
-
- if (n == 1) return 1;
- return f(n-1) + 1;
-}
-```
-
-但这种做法并不能完全解决问题,因为最大允许的递归深度跟当前线程剩余的栈空间大小有关,事先无法计算。如果实时计算,代码又会过于复杂,影响到代码的可读性。所以如果最大深度比较小,比如10、50,还可以用这种方法,否则这种方法不是很实用。
-
-
-
-## 递归代码要警惕重复计算
-
-使用递归时要注意重复计算的问题,比如案例二,我们把整个递归过程分解一下,那就是这样的:
-
-
-
-从图中,我们可以看到,想要计算f(5),需要先计算f(4)和f(3),而计算f(4)还需要计算f(3),因此,f(3)就被计算了很多次,这就是重复计算的问题。
-
-为了解决重复计算,我们可以通过散列表等数据结构来保存已经求解过的f(k)。当递归调用到f(k)时,先看下是否已经求解过了。如果是,则直接从散列表中取值返回,就不再重复计算了。
-
-如上思路,改造下刚才的代码:
-
-```
-public int f(int n) {
- if (n == 1) return 1;
- if (n == 2) return 2;
-
- // hasSolvedList 可以理解成一个 Map,key 是 n,value 是 f(n)
- if (hasSolvedList.containsKey(n)) {
- return hasSovledList.get(n);
- }
-
- int ret = f(n-1) + f(n-2);
- hasSovledList.put(n, ret);
- return ret;
-}
-```
-
-除了堆栈溢出、重复计算这两个常见的问题,递归代码还有很多别的问题。
-
-在时间效率上,递归代码里多了很多函数调用,当这些函数调用的数量较大时,就会积累成一个可观的时间成本。在空间复杂度上,因为递归调用一次就会在内存栈中保存一次现场数据,所以在分析递归代码空间复杂度时,需要额外考虑这部分的开销,比如前面的案例一的递归代码,空间复杂度并不是O(1),而是O(n)。
-
-
-
-## 案例
-
-### 案例1:斐波那契数列
-
-> 斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34....,即第一项 f(1) = 1,第二项 f(2) = 1.....,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。
-
-**1、第一递归函数功能**
-
-假设 f(n) 的功能是求第 n 项的值,代码如下:
-
-```
-int f(int n){
-
-}
-```
-
-**2、找出递归结束的条件**
-
-显然,当 n = 1 或者 n = 2 ,我们可以轻易着知道结果 f(1) = f(2) = 1。所以递归结束条件可以为 n <= 2。代码如下:
-
-```
-int f(int n){
- if(n <= 2){
- return 1;
- }
-}
-```
-
-**第三要素:找出函数的等价关系式**
-
-题目已经把等价关系式给我们了,所以我们很容易就能够知道 f(n) = f(n-1) + f(n-2)。我说过,等价关系式是最难找的一个,而这个题目却把关系式给我们了,这也太容易,好吧,我这是为了兼顾几乎零基础的读者。
-
-所以最终代码如下:
-
-```
-int f(int n){
- // 1.先写递归结束条件
- if(n <= 2){
- return 1;
- }
- // 2.接着写等价关系式
- return f(n-1) + f(n - 2);
-}
-```
-
-搞定,是不是很简单?
-
-
-
-### 案例2:小青蛙跳台阶
-
-> 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
-
-**1、第一递归函数功能**
-
-假设 f(n) 的功能是求青蛙跳上一个n级的台阶总共有多少种跳法,代码如下:
-
-```
-int f(int n){
-
-}
-```
-
-**2、找出递归结束的条件**
-
-我说了,求递归结束的条件,你直接把 n 压缩到很小很小就行了,因为 n 越小,我们就越容易直观着算出 f(n) 的多少,所以当 n = 1时,你知道 f(1) 为多少吧?够直观吧?即 f(1) = 1。代码如下:
-
-```
-int f(int n){
- if(n == 1){
- return 1;
- }
-}
-```
-
-**第三要素:找出函数的等价关系式**
-
-每次跳的时候,小青蛙可以跳一个台阶,也可以跳两个台阶,也就是说,每次跳的时候,小青蛙有两种跳法。
-
-第一种跳法:第一次我跳了一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。
-
-第二种跳法:第一次跳了两个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。
-
-所以,小青蛙的全部跳法就是这两种跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了。于是写出代码:
-
-```
-int f(int n){
- if(n == 1){
- return 1;
- }
- ruturn f(n-1) + f(n-2);
-}
-```
-
-大家觉得上面的代码对不对?
-
-答是不大对,当 n = 2 时,显然会有 f(2) = f(1) + f(0)。我们知道,f(0) = 0,按道理是递归结束,不用继续往下调用的,但我们上面的代码逻辑中,会继续调用 f(0) = f(-1) + f(-2)。这会导致无限调用,进入**死循环**。
-
-这也是我要和你们说的,关于**递归结束条件是否够严谨问题**,有很多人在使用递归的时候,由于结束条件不够严谨,导致出现死循环。也就是说,当我们在第二步找出了一个递归结束条件的时候,可以把结束条件写进代码,然后进行第三步,但是**请注意**,当我们第三步找出等价函数之后,还得再返回去第二步,根据第三步函数的调用关系,会不会出现一些漏掉的结束条件。就像上面,f(n-2)这个函数的调用,有可能出现 f(0) 的情况,导致死循环,所以我们把它补上。代码如下:
-
-```
-int f(int n){
- //f(0) = 0,f(1) = 1,等价于 n<=1时,f(n) = n。
- if(n <= 1){
- return n;
- }
- ruturn f(n-1) + f(n-2);
-}
-```
-
-有人可能会说,我不知道我的结束条件有没有漏掉怎么办?别怕,多练几道就知道怎么办了。
-
-看到这里有人可能要吐槽了,这两道题也太容易了吧??能不能被这么敷衍。少侠,别走啊,下面出道难一点的。
-
-### 案例3:反转单链表。
-
-> 反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1
-
-链表的节点定义如下:
-
-```
-class Node{
- int date;
- Node next;
-}
-```
-
-虽然是 Java语言,但就算你没学过 Java,我觉得也是影响不大,能看懂。
-
-还是老套路,三要素一步一步来。
-
-**1、定义递归函数功能**
-
-假设函数 reverseList(head) 的功能是反转但链表,其中 head 表示链表的头节点。代码如下:
-
-```
-Node reverseList(Node head){
-
-}
-```
-
-**2. 寻找结束条件**
-
-当链表只有一个节点,或者如果是空表的话,你应该知道结果吧?直接啥也不用干,直接把 head 返回呗。代码如下:
-
-```
-Node reverseList(Node head){
- if(head == null || head.next == null){
- return head;
- }
-}
-```
-
-**3. 寻找等价关系**
-
-这个的等价关系不像 n 是个数值那样,比较容易寻找。但是我告诉你,它的等价条件中,一定是范围不断在缩小,对于链表来说,就是链表的节点个数不断在变小,所以,如果你实在找不出,你就先对 reverseList(head.next) 递归走一遍,看看结果是咋样的。例如链表节点如下
-
-
-
-我们就缩小范围,先对 2->3->4递归下试试,即代码如下
-
-```
-Node reverseList(Node head){
- if(head == null || head.next == null){
- return head;
- }
- // 我们先把递归的结果保存起来,先不返回,因为我们还不清楚这样递归是对还是错。,
- Node newList = reverseList(head.next);
-}
-```
-
-我们在第一步的时候,就已经定义了 reverseLis t函数的功能可以把一个单链表反转,所以,我们对 2->3->4反转之后的结果应该是这样:
-
-
-
-我们把 2->3->4 递归成 4->3->2。不过,1 这个节点我们并没有去碰它,所以 1 的 next 节点仍然是连接这 2。
-
-接下来呢?该怎么办?
-
-其实,接下来就简单了,我们接下来只需要**把节点 2 的 next 指向 1,然后把 1 的 next 指向 null,不就行了?**,即通过改变 newList 链表之后的结果如下:
-
-
-
-也就是说,reverseList(head) 等价于 ** reverseList(head.next)** + **改变一下1,2两个节点的指向**。好了,等价关系找出来了,代码如下(有详细的解释):
-
-```
-//用递归的方法反转链表
-public static Node reverseList2(Node head){
- // 1.递归结束条件
- if (head == null || head.next == null) {
- return head;
- }
- // 递归反转 子链表
- Node newList = reverseList2(head.next);
- // 改变 1,2节点的指向。
- // 通过 head.next获取节点2
- Node t1 = head.next;
- // 让 2 的 next 指向 2
- t1.next = head;
- // 1 的 next 指向 null.
- head.next = null;
- // 把调整之后的链表返回。
- return newList;
- }
-```
-
-这道题的第三步看的很懵?正常,因为你做的太少了,可能没有想到还可以这样,多练几道就可以了。但是,我希望通过这三道题,给了你以后用递归做题时的一些思路,你以后做题可以按照我这个模式去想。通过一篇文章是不可能掌握递归的,还得多练,我相信,只要你认真看我的这篇文章,多看几次,一定能找到一些思路!!
-
-> 我已经强调了好多次,多练几道了,所以呢,后面我也会找大概 10 道递归的练习题供大家学习,不过,我找的可能会有一定的难度。不会像今天这样,比较简单,所以呢,初学者还得自己多去找题练练,相信我,掌握了递归,你的思维抽象能力会更强!
-
-接下来我讲讲有关递归的一些优化。
-
-### 有关递归的一些优化思路
-
-**1. 考虑是否重复计算**
-
-告诉你吧,如果你使用递归的时候不进行优化,是有非常非常非常多的**子问题**被重复计算的。
-
-> 啥是子问题? f(n-1),f(n-2)....就是 f(n) 的子问题了。
-
-例如对于案例2那道题,f(n) = f(n-1) + f(n-2)。递归调用的状态图如下:
-
-
-
-看到没有,递归计算的时候,重复计算了两次 f(5),五次 f(4)。。。。这是非常恐怖的,n 越大,重复计算的就越多,所以我们必须进行优化。
-
-如何优化?一般我们可以把我们计算的结果保证起来,例如把 f(4) 的计算结果保证起来,当再次要计算 f(4) 的时候,我们先判断一下,之前是否计算过,如果计算过,直接把 f(4) 的结果取出来就可以了,没有计算过的话,再递归计算。
-
-用什么保存呢?可以用数组或者 HashMap 保存,我们用数组来保存把,把 n 作为我们的数组下标,f(n) 作为值,例如 arr[n] = f(n)。f(n) 还没有计算过的时候,我们让 arr[n] 等于一个特殊值,例如 arr[n] = -1。
-
-当我们要判断的时候,如果 arr[n] = -1,则证明 f(n) 没有计算过,否则, f(n) 就已经计算过了,且 f(n) = arr[n]。直接把值取出来就行了。代码如下:
-
-```
-// 我们实现假定 arr 数组已经初始化好的了。
-int f(int n){
- if(n <= 1){
- return n;
- }
- //先判断有没计算过
- if(arr[n] != -1){
- //计算过,直接返回
- return arr[n];
- }else{
- // 没有计算过,递归计算,并且把结果保存到 arr数组里
- arr[n] = f(n-1) + f(n-1);
- reutrn arr[n];
- }
-}
-```
-
-也就是说,使用递归的时候,必要
-须要考虑有没有重复计算,如果重复计算了,一定要把计算过的状态保存起来。
-
-**2. 考虑是否可以自底向上**
-
-对于递归的问题,我们一般都是**从上往下递归**的,直到递归到最底,再一层一层着把值返回。
-
-不过,有时候当 n 比较大的时候,例如当 n = 10000 时,那么必须要往下递归10000层直到 n <=1 才将结果慢慢返回,如果n太大的话,可能栈空间会不够用。
-
-对于这种情况,其实我们是可以考虑自底向上的做法的。例如我知道
-
-f(1) = 1;
-
-f(2) = 2;
-
-那么我们就可以推出 f(3) = f(2) + f(1) = 3。从而可以推出f(4),f(5)等直到f(n)。因此,我们可以考虑使用自底向上的方法来取代递归,代码如下:
-
-```
-public int f(int n) {
- if(n <= 2)
- return n;
- int f1 = 1;
- int f2 = 2;
- int sum = 0;
-
- for (int i = 3; i <= n; i++) {
- sum = f1 + f2;
- f1 = f2;
- f2 = sum;
- }
- return sum;
- }
-```
-
-这种方法,其实也被称之为**递推**。
-
-
-
-
-
-
-
-来源:
-
-https://aleej.com/2019/10/09/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E4%B9%8B%E7%BE%8E%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
-
-https://www.cnblogs.com/kubidemanong/p/10538799.html
-
diff --git a/docs/data-structure-algorithms/Sort.md b/docs/data-structure-algorithms/Sort.md
deleted file mode 100644
index 035398fe8b..0000000000
--- a/docs/data-structure-algorithms/Sort.md
+++ /dev/null
@@ -1,443 +0,0 @@
-# 排序
-
-排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:**插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序**等。用一张图概括:
-
-
-
-**关于时间复杂度**:
-
-1. 平方阶 ($O(n2)$) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
-2. 线性对数阶 (O(nlog2n)) 排序: 快速排序、堆排序和归并排序;
-3. O(n1+§) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
-4. 线性阶 (O(n)) 排序: 基数排序,此外还有桶、箱排序。
-
-**关于稳定性**:
-
-稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
-
-不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
-
-**名词解释**:
-
-**n**:数据规模
-
-**k**:“桶”的个数
-
-**In-place**:占用常数内存,不占用额外内存
-
-**Out-place**:占用额外内存
-
-**稳定性**:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
-
-
-
-十种常见排序算法可以分为两大类:
-
-**非线性时间比较类排序**:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破$O(nlogn)$,因此称为非线性时间比较类排序。
-
-**线性时间非比较类排序**:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
-
-
-
-## 冒泡排序
-
-冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
-
-作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
-
-### 1. 算法步骤
-
-1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
-2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
-3. 针对所有的元素重复以上的步骤,除了最后一个。
-4. 重复步骤1~3,直到排序完成。
-
-### 2. 动图演示
-
-
-
-
-
-### 3. 什么时候最快
-
-当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊)。
-
-### 4. 什么时候最慢
-
-当输入的数据是反序时(写一个 for 循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗)。
-
-```java
-public class BubbleSort {
-
- public static void main(String[] args) {
- int[] arrs = {1, 3, 4, 2, 6, 5};
-
- for (int i = 0; i < arrs.length; i++) {
- for (int j = 0; j < arrs.length - 1 - i; j++) {
- if (arrs[j] > arrs[j + 1]) {
- int tmp = arrs[j];
- arrs[j] = arrs[j + 1];
- arrs[j + 1] = tmp;
- }
- }
- }
-
- for (int arr : arrs) {
- System.out.print(arr + " ");
- }
- }
-}
-```
-
-嵌套循环,应该立马就可以得出这个算法的时间复杂度为 $O(n²)$。
-
-
-
-## 选择排序
-
-选择排序的思路是这样的:首先,找到数组中最小的元素,拎出来,将它和数组的第一个元素交换位置,第二步,在剩下的元素中继续寻找最小的元素,拎出来,和数组的第二个元素交换位置,如此循环,直到整个数组排序完成。
-
-选择排序是一种简单直观的排序算法,无论什么数据进去都是 $O(n²)$ 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
-
-### 1. 算法步骤
-
-1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
-2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
-3. 重复第二步,直到所有元素均排序完毕。
-
-### 2. 动图演示
-
-
-
-```java
-public class SelectionSort {
-
- public static void main(String[] args) {
- int[] arrs = {5, 2, 4, 6, 1, 3};
-
- for (int i = 0; i < arrs.length; i++) {
- //最小元素下标
- int min = i;
- for (int j = i +1; j < arrs.length; j++) {
- if (arrs[j] < arrs[min]) {
- min = j;
- }
- }
- //交换位置
- int temp = arrs[i];
- arrs[i] = arrs[min];
- arrs[min] = temp;
- }
- for (int arr : arrs) {
- System.out.println(arr);
- }
- }
-}
-```
-
-
-
-## 插入排序
-
-插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
-
-插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
-
-### 1. 算法步骤
-
-1. 从第一个元素开始,该元素可以认为已经被排序;
-2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
-3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
-4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
-5. 将新元素插入到该位置后;
-6. 重复步骤2~5。
-
-### 2. 动图演示
-
-
-
-```java
-public static void main(String[] args) {
- int[] arr = {5, 2, 4, 6, 1, 3};
- // 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
- for (int i = 1; i < arr.length; i++) {
-
- // 记录要插入的数据
- int tmp = arr[i];
-
- // 从已经排序的序列最右边的开始比较,找到比其小的数
- int j = i;
- while (j > 0 && tmp < arr[j - 1]) {
- arr[j] = arr[j - 1];
- j--;
- }
-
- // 存在比其小的数,插入
- if (j != i) {
- arr[j] = tmp;
- }
- }
-
- for (int i : arr) {
- System.out.println(i);
- }
-}
-}
-```
-
-
-
-## 快速排序
-
-这篇很好:https://www.cxyxiaowu.com/5262.html
-
-快速排序的核心思想也是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。
-
-> 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
-
-### 1. 算法步骤
-
-1. 从数列中挑出一个元素,称为 “基准”(pivot);
-2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
-3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
-
-递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
-
-### 2. 动图演示
-
-
-
-### 单边扫描
-
-快速排序的关键之处在于切分,切分的同时要进行比较和移动,这里介绍一种叫做单边扫描的做法。
-
-我们随意抽取一个数作为基准值,同时设定一个标记 mark 代表左边序列最右侧的下标位置,当然初始为 0 ,接下来遍历数组,如果元素大于基准值,无操作,继续遍历,如果元素小于基准值,则把 mark + 1 ,再将 mark 所在位置的元素和遍历到的元素交换位置,mark 这个位置存储的是比基准值小的数据,当遍历结束后,将基准值与 mark 所在元素交换位置即可。
-
-```java
-public static void sort(int[] arrs, int startIndex, int endIndex) {
- if (startIndex > endIndex) {
- return;
- }
- int pivotIndex = partion(arrs, startIndex, endIndex);
- sort(arrs, startIndex, pivotIndex - 1);
- sort(arrs, pivotIndex + 1, endIndex);
-}
-
-public static int partion(int[] arrs, int startIndex, int endIndex) {
- int pivot = arrs[startIndex];
- int mark = startIndex;
-
- for (int i = startIndex + 1; i < arrs.length; i++) {
- if (arrs[i] < pivot) {
- mark++;
- int tmp = arrs[mark];
- arrs[mark] = arrs[i];
- arrs[i] = tmp;
- }
- }
- arrs[startIndex] = arrs[mark];
- arrs[mark] = pivot;
- return mark;
-}
-```
-
-### 双边扫描
-
-另外还有一种双边扫描的做法,看起来比较直观:我们随意抽取一个数作为基准值,然后从数组左右两边进行扫描,先从左往右找到一个大于基准值的元素,将下标指针记录下来,然后转到从右往左扫描,找到一个小于基准值的元素,交换这两个元素的位置,重复步骤,直到左右两个指针相遇,再将基准值与左侧最右边的元素交换。
-
-我们来看一下实现代码,不同之处只有 partition 方法:
-
-```java
-public static void sort(int[] arr) {
- sort(arr, 0, arr.length - 1);
-}
-
-private static void sort(int[] arr, int startIndex, int endIndex) {
- if (endIndex <= startIndex) {
- return;
- }
- //切分
- int pivotIndex = partition(arr, startIndex, endIndex);
- sort(arr, startIndex, pivotIndex-1);
- sort(arr, pivotIndex+1, endIndex);
-}
-
-
-private static int partition(int[] arr, int startIndex, int endIndex) {
- int left = startIndex;
- int right = endIndex;
- int pivot = arr[startIndex];//取第一个元素为基准值
-
- while (true) {
- //从左往右扫描
- while (arr[left] <= pivot) {
- left++;
- if (left == right) {
- break;
- }
- }
-
- //从右往左扫描
- while (pivot < arr[right]) {
- right--;
- if (left == right) {
- break;
- }
- }
-
- //左右指针相遇
- if (left >= right) {
- break;
- }
-
- //交换左右数据
- int temp = arr[left];
- arr[left] = arr[right];
- arr[right] = temp;
- }
-
- //将基准值插入序列
- int temp = arr[startIndex];
- arr[startIndex] = arr[right];
- arr[right] = temp;
- return right;
-}
-```
-
-
-
-
-
-## 希尔排序
-
-希尔排序这个名字,来源于它的发明者希尔,也称作“缩小增量排序”,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
-
-希尔排序是基于插入排序的以下两点性质而提出改进方法的:
-
-- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
-- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
-
-希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
-
-### 1. 算法步骤
-
-1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
-2. 按增量序列个数 k,对序列进行 k 趟排序;
-3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
-
-### 2. 动图演示
-
-
-
-
-
-## 归并排序
-
-> https://www.cnblogs.com/chengxiao/p/6194356.html
-
-归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
-
-作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
-
-- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
-- 自下而上的迭代;
-
-在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。但是对于递归法,作者却认为:
-
-> However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
->
-> 然而,在 JavaScript 中这种方式不太可行,因为这个算法的递归深度对它来讲太深了。
-
-说实话,我不太理解这句话。意思是 JavaScript 编译器内存太小,递归太深容易造成内存溢出吗?还望有大神能够指教。
-
-和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
-
-### 2. 算法步骤
-
-1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
-2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
-3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
-4. 重复步骤 3 直到某一指针达到序列尾;
-5. 将另一序列剩下的所有元素直接复制到合并序列尾。
-
-### 3. 动图演示
-
-
-
-
-
-
-
-## 堆排序
-
-堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
-
-1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
-2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
-
-堆排序的平均时间复杂度为 Ο(nlogn)。
-
-### 1. 算法步骤
-
-1. 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
-2. 把堆首(最大值)和堆尾互换;
-3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
-4. 重复步骤 2,直到堆的尺寸为 1。
-
-### 2. 动图演示
-
-[](https://github.com/hustcc/JS-Sorting-Algorithm/blob/master/res/heapSort.gif)
-
-
-
-
-
-## 计数排序
-
-计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
-
-### 1. 动图演示
-
-[](https://github.com/hustcc/JS-Sorting-Algorithm/blob/master/res/countingSort.gif)
-
-
-
-
-
-## 桶排序
-
-桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
-
-1. 在额外空间充足的情况下,尽量增大桶的数量
-2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
-
-同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
-
-### 1. 什么时候最快
-
-当输入的数据可以均匀的分配到每一个桶中。
-
-### 2. 什么时候最慢
-
-当输入的数据被分配到了同一个桶中。
-
-
-
-## 基数排序
-
-基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
-
-### 1. 基数排序 vs 计数排序 vs 桶排序
-
-基数排序有两种方法:
-
-这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:
-
-- 基数排序:根据键值的每位数字来分配桶;
-- 计数排序:每个桶只存储单一键值;
-- 桶排序:每个桶存储一定范围的数值;
-
-### 2. LSD 基数排序动图演示
-
-[](https://github.com/hustcc/JS-Sorting-Algorithm/blob/master/res/radixSort.gif)
-
diff --git a/docs/data-structure-algorithms/Stack.md b/docs/data-structure-algorithms/Stack.md
deleted file mode 100644
index 3b072cb431..0000000000
--- a/docs/data-structure-algorithms/Stack.md
+++ /dev/null
@@ -1,279 +0,0 @@
-# 栈
-
-## 一、概述
-
-### 定义
-
-注意:本文所说的栈是数据结构中的栈,而不是内存模型中栈
-
-栈(stack)是限定仅在表尾一端进行插入或删除操作的**特殊线性表**。又称为堆栈。
-
-对于栈来说, 允许进行插入或删除操作的一端称为栈顶(top),而另一端称为栈底(bottom)。不含元素栈称为空栈,向栈中插入一个新元素称为入栈或压栈, 从栈中删除一个元素称为出栈或退栈。
-
-假设有一个栈S=(a1, a2, …, an),a1先进栈, an最后进栈。称 a1 为栈底元素,an 为栈顶元素。出栈时只允许在栈顶进行,所以 an 先出栈,a1最后出栈。因此又称栈为后进先出(Last In First Out,LIFO)的线性表。
-
-栈(stack),是一种线性存储结构,它有以下几个特点:
-
-- 栈中数据是按照"后进先出(LIFO, Last In First Out)"方式进出栈的。
-- 向栈中添加/删除数据时,只能从栈顶进行操作。
-
-
-
-在上图中,当 ABCD 均已入栈后,出栈时得到的序列为 DCBA,这就是后进先出。
-
-
-
-### 基本操作
-
-栈的基本操作除了进栈 `push()`,出栈 `pop()` 之外,还有判空 `isEmpty()`、取栈顶元素 `peek()` 等操作。
-
-抽象成接口如下:
-
-```java
-public interface MyStack {
-
- /**
- * 返回堆栈的大小
- */
- public int getSize();
-
- /**
- * 判断堆栈是否为空
- */
- public boolean isEmpty();
-
- /**
- * 入栈
- */
- public void push(Object e);
-
- /**
- * 出栈,并删除
- */
- public Object pop();
-
- /**
- * 返回栈顶元素
- */
- public Object peek();
-}
-```
-
-
-
-和线性表类似,栈也有两种存储结构:顺序存储和链式存储。
-
-## 二、栈的顺序存储与实现
-
-顺序栈是使用顺序存储结构实现的栈,即利用一组地址连续的存储单元依次存放栈中的数据元素。由于栈是一种特殊的线性表,因此在线性表的顺序存储结构的基础上,选择线性表的一端作为栈顶即可。那么根据数组操作的特性,选择数组下标大的一端,即线性表顺序存储的表尾来作为栈顶,此时入栈、出栈操作可以 $O(1)$ 时间完成。
-
-由于栈的操作都是在栈顶完成,因此在顺序栈的实现中需要附设一个指针 top 来动态地指示栈顶元素在数组中的位置。通常 top 可以用栈顶元素所在的数组下标来表示,top=-1时表示空栈。
-
-栈在使用过程中所需的最大空间难以估计,所以,一般构造栈的时候不应设定最大容量。一种合理的做法和线性表类似,先为栈分配一个基本容量,然后在实际的使用过程中,当栈的空间不够用时再倍增存储空间。
-
-```java
-public class MyArrayStack implements MyStack {
-
- private final int capacity = 2; //默认容量
- private Object[] arrs; //数据元素数组
- private int top; //栈顶指针
-
- MyArrayStack(){
- top = -1;
- arrs = new Object[capacity];
- }
-
- public int getSize() {
- return top + 1;
- }
-
- public boolean isEmpty() {
- return top < 0;
- }
-
- public void push(Object e) {
- if(getSize() >= arrs.length){
- expandSapce(); //扩容
- }
- arrs[++top]=e;
- }
-
- private void expandSapce() {
- Object[] a = new Object[arrs.length * 2];
- for (int i = 0; i < arrs.length; i++) {
- a[i] = arrs[i];
- }
- arrs = a;
- }
-
- public Object pop() {
- if(getSize()<1){
- throw new RuntimeException("栈为空");
- }
- Object obj = arrs[top];
- arrs[top--] = null;
- return obj;
- }
-
- public Object peek() {
- if(getSize()<1){
- throw new RuntimeException("栈为空");
- }
- return arrs[top];
- }
-}
-```
-
-以上基于数据实现的栈代码并不难理解。由于有 top 指针的存在,所以`size()`、`isEmpty()`方法均可在 $O(1) $ 时间内完成。`push()`、`pop()`和`peek()`方法,除了需要`ensureCapacity()`外,都执行常数基本操作,因此它们的运行时间也是 $O(1)$
-
-
-
-## 三、栈的链式存储与实现
-
-栈的链式存储即采用链表实现栈。当采用单链表存储线性表后,根据单链表的操作特性选择单链表的头部作为栈顶,此时,入栈和出栈等操作可以在 $O(1)$ 时间内完成。
-
-由于栈的操作只在线性表的一端进行,在这里使用带头结点的单链表或不带头结点的单链表都可以。使用带头结点的单链表时,结点的插入和删除都在头结点之后进行;使用不带头结点的单链表时,结点的插入和删除都在链表的首结点上进行。
-
-下面以不带头结点的单链表为例实现栈,如下示意图所示:
-
-
-
-在上图中,top 为栈顶结点的引用,始终指向当前栈顶元素所在的结点。若 top 为null,则表示空栈。入栈操作是在 top 所指结点之前插入新的结点,使新结点的 next 域指向 top,top 前移即可;出栈则直接让 top 后移即可。
-
-```java
-public class MyLinkedStack implements MyStack {
-
- class Node {
- private Object element;
- private Node next;
-
- public Node() {
- this(null, null);
- }
-
- public Node(Object ele, Node next) {
- this.element = ele;
- this.next = next;
- }
-
- public Node getNext() {
- return next;
- }
-
- public void setNext(Node next) {
- this.next = next;
- }
-
- public Object getData() {
- return element;
- }
-
- public void setData(Object obj) {
- element = obj;
- }
- }
-
- private Node top;
- private int size;
-
- public MyLinkedStack() {
- top = null;
- size = 0;
- }
-
- public int getSize() {
- return size;
- }
-
- public boolean isEmpty() {
- return size == 0;
- }
-
- public void push(Object e) {
- Node node = new Node(e, top);
- top = node;
- size++;
- }
-
- public Object pop() {
- if (size < 1) {
- throw new RuntimeException("堆栈为空");
- }
- Object obj = top.getData();
- top = top.getNext();
- size--;
- return obj;
- }
-
- public Object peek() {
- if (size < 1) {
- throw new RuntimeException("堆栈为空");
- }
- return top.getData();
- }
-}
-```
-
-上述 `MyLinkedStack` 类中有两个成员变量,其中 `top` 表示首结点,也就是栈顶元素所在的结点;`size` 指示栈的大小,即栈中数据元素的个数。不难理解,所有的操作均可以在 $O(1)$ 时间内完成。
-
-
-
-## 四、JDK 中的栈实现 Stack
-
-Java 工具包中的 Stack 是继承于 Vector(矢量队列)的,由于 Vector 是通过数组实现的,这就意味着,Stack 也是通过数组实现的,而非链表。当然,我们也可以将 LinkedList 当作栈来使用。
-
-### Stack的继承关系
-
-```java
-java.lang.Object
- java.util.AbstractCollection
- java.util.AbstractList
- java.util.Vector
- java.util.Stack
-
-public class Stack extends Vector {}
-```
-
-
-
-
-
-## 五、栈应用
-
-栈有一个很重要的应用,在程序设计语言里实现了递归。
-
-### 有效的括号
-
->给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串,判断字符串是否有效。
->
->有效字符串需满足:
->
->1. 左括号必须用相同类型的右括号闭合。
->2. 左括号必须以正确的顺序闭合。
->
->注意空字符串可被认为是有效字符串。
->
->```
->输入: "{[]}"
->输出: true
->输入: "([)]"
->输出: false
->```
-
-
-
-
-
-
-
->请根据每日 `气温` 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 `0` 来代替。
->
->例如,给定一个列表 `temperatures = [73, 74, 75, 71, 69, 72, 76, 73]`,你的输出应该是 `[1, 1, 4, 2, 1, 1, 0, 0]`。
->
->**提示:**`气温` 列表长度的范围是 `[1, 30000]`。每个气温的值的均为华氏度,都是在 `[30, 100]` 范围内的整数。
-
-
-
-
-
-> 逆波兰表达式求值
\ No newline at end of file
diff --git a/docs/data-structure-algorithms/about-leetcode.md b/docs/data-structure-algorithms/about-leetcode.md
deleted file mode 100644
index 90653ea597..0000000000
--- a/docs/data-structure-algorithms/about-leetcode.md
+++ /dev/null
@@ -1,3 +0,0 @@
-关于怎么刷题的帖子:
-
-论如何4个月高效刷满 500 题并形成长期记忆https://leetcode-cn.com/circle/discuss/jq9Zke/
\ No newline at end of file
diff --git a/docs/data-structure-algorithms/algorithm/Backtracking.md b/docs/data-structure-algorithms/algorithm/Backtracking.md
new file mode 100755
index 0000000000..bdce9eaa4f
--- /dev/null
+++ b/docs/data-structure-algorithms/algorithm/Backtracking.md
@@ -0,0 +1,834 @@
+---
+title: 回溯算法
+date: 2023-05-09
+tags:
+ - back tracking
+categories: Algorithm
+---
+
+
+
+> 🔍 **回溯算法**是解决很多算法问题的常见思想,它也是传统的人工智能的方法,其本质是 **在树形问题中寻找解** 。
+>
+> 回溯算法实际上是一个类似枚举的搜索尝试过程,主要是在**搜索尝试**过程中寻找问题的解,当发现已不满足求解条件时,就"**回溯**"返回,尝试别的路径。所以也可以叫做**回溯搜索法**。
+>
+> 💡 回溯是递归的副产品,只要有递归就会有回溯。
+
+# 一、回溯算法
+
+回溯算法是一种**深度优先搜索**(DFS)的算法,它通过递归的方式,逐步建立解空间树,从根节点开始,逐层深入,直到找到一个解或路径不可行时回退到上一个状态(即回溯)。每一步的尝试可能会有多个选择,回溯算法通过剪枝来减少不必要的计算。
+
+> 🌲 **深度优先搜索** 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 **尽可能深** 的搜索树的分支。当结点 `v` 的所在边都己被探寻过,搜索将 **回溯** 到发现结点 `v` 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
+
+回溯的基本步骤通常包含以下几个要素:
+
+- 🎯 **选择**:在当前状态下,做出一个选择,进入下一个状态。
+- ⚖️ **约束**:每一步的选择必须满足问题的约束条件。
+- 🎪 **目标**:找到一个解或判断是否无法继续。
+- ↩️ **回溯**:如果当前的选择不符合目标,撤销当前的选择,回到上一状态继续尝试其他可能的选择。
+
+### 🧠 核心思想
+
+**回溯法** 采用试错的思想,它尝试分步的去解决一个问题。
+
+1. 🔍 **穷举所有可能的解**:回溯法通过在每个状态下尝试不同的选择,来遍历解空间树。每个分支代表着做出的一个选择,每次递归都尝试不同的路径,直到找到一个解或回到根节点。
+2. ✂️ **剪枝**:在回溯过程中,我们可能会遇到一些不符合约束条件的选择,这时候可以及时退出当前分支,避免无谓的计算,这被称为"剪枝"。剪枝是提高回溯算法效率的关键,能减少不必要的计算。
+3. 🌲 **深度优先搜索(DFS)**:回溯算法在解空间树中深度优先遍历,尝试选择每个分支。直到走到树的叶子节点或回溯到一个不满足条件的节点。
+
+
+
+### 🏗️ 基本框架
+
+回溯算法的基本框架可以用递归来实现,通常包含以下几个步骤:
+
+1. 🎯 **选择和扩展**:选择一个可行的扩展步骤,扩展当前的解。
+2. ✅ **约束检查**:检查当前扩展后的解是否满足问题的约束条件。
+3. 🔄 **递归调用**:如果当前解满足约束条件,则递归地尝试扩展该解。
+4. ↩️ **回溯**:如果当前解不满足约束条件,或所有扩展步骤都已经尝试,则回溯到上一步,尝试其他可能的扩展步骤。
+
+以下是回溯算法的一般伪代码:
+
+```java
+result = []
+function backtrack(solution, candidates): //入参可以理解为 路径, 选择列表
+ if solution 是一个完整解: //满足结束条件
+ result.add(solution) // 处理当前完整解
+ return
+ for candidate in candidates:
+ if candidate 满足约束条件:
+ solution.add(candidate) // 扩展解
+ backtrack(solution, new_candidates) // 递归调用
+ solution.remove(candidate) // 回溯,撤销选择
+
+```
+
+对应到 java 的一般框架如下:
+
+```java
+public void backtrack(List tempList, int start, int[] nums) {
+ // 1. 终止条件
+ if (tempList.size() == nums.length) {
+ // 找到一个解
+ result.add(new ArrayList<>(tempList));
+ return;
+ }
+
+ for (int i = start; i < nums.length; i++) {
+ // 2. 剪枝:跳过相同的数字,避免重复
+ if (i > start && nums[i] == nums[i - 1]) {
+ continue;
+ }
+
+ // 3. 做出选择
+ tempList.add(nums[i]);
+
+ // 4. 递归
+ backtrack(tempList, i + 1, nums);
+
+ // 5. 撤销选择
+ tempList.remove(tempList.size() - 1);
+ }
+}
+```
+
+**其实就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」**
+
+> 💡 **关键理解**:回溯算法 = 递归 + 选择 + 撤销选择
+
+
+
+### 🎯 常见题型
+
+回溯法,一般可以解决如下几种问题:
+
+- 🎲 **组合问题**:N个数里面按一定规则找出k个数的集合
+
+ - **题目示例**:`LeetCode 39. 组合总和`,`LeetCode 40. 组合总和 II`,`LeetCode 77. 组合`
+ - **解题思路**: 组合问题要求我们在给定的数组中选取若干个数字,组合成目标值或某种形式的子集。回溯算法的基本思路是从一个起点开始,选择当前数字或者跳过当前数字,直到找到一个合法的组合。组合问题通常有**去重**的要求,避免重复的组合。
+
+- 🔄 **排列问题**:N个数按一定规则全排列,有几种排列方式
+
+ - **题目示例**:`LeetCode 46. 全排列`,`LeetCode 47. 全排列 II`,`LeetCode 31. 下一个排列`
+ - **解题思路**: 排列问题要求我们通过给定的数字生成所有可能的排列。回溯算法通过递归生成所有排列,通过交换位置来改变元素的顺序。对于全排列 II 这类题目,必须处理重复元素的问题,确保生成的排列不重复。
+
+- 📦 **子集问题**:一个N个数的集合里有多少符合条件的子集
+
+ - **题目示例**:`LeetCode 78. 子集`,`LeetCode 90. 子集 II`
+ - **解题思路**: 子集问题要求我们生成数组的所有子集,回溯算法通过递归生成每个可能的子集。在生成子集时,每个元素有两种选择——要么包含它,要么不包含它。因此,回溯法通过逐步选择来生成所有的子集。
+
+- ✂️ **切割问题**:一个字符串按一定规则有几种切割方式
+
+ - 题目实例:` LeetCode 416. Partition Equal Subset Sum`,`LeetCode 698. Partition to K Equal Sum Subsets`
+ - 解题思路:回溯法适用于切割问题中的"探索所有可能的分割方式"的场景。特别是在无法直接通过动态规划推导出最优解时,回溯法通过递归尝试所有可能的分割方式,并通过剪枝减少不必要的计算
+
+- ♟️ **棋盘问题**:
+
+ - **题目示例**:`LeetCode 37. 解数独`,`LeetCode 51. N 皇后`,`LeetCode 52. N 皇后 II`
+
+ **解题思路**: 棋盘问题常常涉及到在二维数组中进行回溯搜索,比如在数独中填入数字,或者在 N 皇后问题中放置皇后。回溯法在这里用于逐步尝试每个位置,满足棋盘的约束条件,直到找到一个解或者回溯到一个合法的状态。
+
+- 🗺️ **图的遍历问题**:
+
+ - **题目示例**:`LeetCode 79. 单词搜索`,`LeetCode 130. 被围绕的区域`
+ - **解题思路**: 回溯算法在图遍历中的应用主要是通过递归搜索路径。常见的问题是从某个起点出发,寻找是否存在某个目标路径。通过回溯算法,可以逐步尝试每一个可能的路径,直到找到符合条件的解。
+
+
+
+
+### ⚡ 回溯算法的优化技巧
+
+1. ✂️ **剪枝**:在递归过程中,如果当前路径不符合问题约束,就提前返回,避免继续深入。例如在排列问题中,遇到重复的数字时可以跳过该分支。
+2. 📊 **排序**:对输入数据进行排序,有助于我们在递归时判断是否可以剪枝,尤其是在去重的场景下。
+3. 🗜️ **状态压缩**:在一些问题中,使用位运算或其他方式对状态进行压缩,可以显著减少存储空间和计算时间。例如在解决旅行商问题时,常常使用状态压缩来存储已经访问的节点。
+4. 🛑 **提前终止**:如果在递归过程中发现某条路径不可能达到目标(例如目标已经超过了剩余可用值),可以直接结束该分支,节省时间。
+
+
+
+# 二、热门面试题
+
+## 🎲 排列、组合类
+
+> 无论是排列、组合还是子集问题,简单说无非就是让你从序列 `nums` 中以给定规则取若干元素,主要有以下几种变体:
+>
+> **元素无重不可复选,即 `nums` 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式**。
+>
+> - 以组合为例,如果输入 `nums = [2,3,6,7]`,和为 7 的组合应该只有 `[7]`。
+>
+> **元素可重不可复选,即 `nums` 中的元素可以存在重复,每个元素最多只能被使用一次**。
+>
+> - 以组合为例,如果输入 `nums = [2,5,2,1,2]`,和为 7 的组合应该有两种 `[2,2,2,1]` 和 `[5,2]`。
+>
+> **元素无重可复选,即 `nums` 中的元素都是唯一的,每个元素可以被使用若干次**。
+>
+> - 以组合为例,如果输入 `nums = [2,3,6,7]`,和为 7 的组合应该有两种 `[2,2,3]` 和 `[7]`。
+>
+> 当然,也可以说有第四种形式,即元素可重可复选。但既然元素可复选,那又何必存在重复元素呢?元素去重之后就等同于形式三,所以这种情况不用考虑。
+>
+> 上面用组合问题举的例子,但排列、组合、子集问题都可以有这三种基本形式,所以共有 9 种变化。
+>
+> 除此之外,题目也可以再添加各种限制条件,比如让你求和为 `target` 且元素个数为 `k` 的组合,那这么一来又可以衍生出一堆变体,怪不得面试笔试中经常考到排列组合这种基本题型。
+>
+> **但无论形式怎么变化,其本质就是穷举所有解,而这些解呈现树形结构,所以合理使用回溯算法框架,稍改代码框架即可把这些问题一网打尽**。
+
+
+
+### 🎯 一、元素无重不可复选
+
+#### 📦 [子集_78](https://leetcode.cn/problems/subsets/)
+
+> 给你一个整数数组 `nums` ,数组中的元素 **互不相同** 。返回该数组所有可能的子集(幂集)。
+>
+> 解集 **不能** 包含重复的子集。你可以按 **任意顺序** 返回解集。
+>
+> ```
+> 输入:nums = [1,2,3]
+> 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
+> ```
+
+**💡 思路**:
+
+**子集的特性**:
+
+- 对于给定的数组 `[1, 2, 3]`,它的所有子集应该包括空集、单个元素的子集、两个元素的组合和完整数组。
+- 每个元素都有两种选择:要么加入子集,要么不加入子集。
+
+**回溯算法**:
+
+- 使用回溯的方式可以从空集开始,逐步添加元素来生成所有子集。
+- 从当前的元素出发,尝试包含它或者不包含它,然后递归地处理下一个元素。
+
+参数定义:
+
+- `res`:一个列表,存储最终的所有子集,类型是 `List>`。
+
+- `track`:一个临时列表,记录当前路径(即当前递归中形成的子集)。
+- `start`:当前递归要开始的位置(即考虑从哪个位置开始生成子集)。这个 `start` 是非常重要的,它确保了我们在递归时不会重复生成相同的子集。
+
+完成回溯树的遍历就收集了所有子集。
+
+
+
+```java
+public List> subsets(int[] nums) {
+ List> res = new ArrayList<>();
+ // 记录回溯算法的递归路径
+ List track = new ArrayList<>();
+
+ if (nums.length == 0) {
+ return res;
+ }
+
+ backtrack(nums, 0, res, track);
+ return res;
+}
+
+private void backtrack(int[] nums, int start, List> res, List track) {
+
+ res.add(new ArrayList<>(track));
+
+ // 回溯算法标准框架
+ for (int i = start; i < nums.length; i++) {
+ // 做选择
+ track.add(nums[i]);
+ // 通过 start 参数控制树枝的遍历,避免产生重复的子集
+ backtrack(nums, i + 1, res, track);
+ // 撤销选择
+ track.remove(track.size() - 1);
+ }
+}
+```
+
+
+
+#### 🎲 [组合_77](https://leetcode.cn/problems/combinations/)
+
+> 给定两个整数 `n` 和 `k`,返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。你可以按 **任何顺序** 返回答案。
+>
+> ```
+> 输入:n = 4, k = 2
+> 输出:
+> [
+> [2,4],
+> [3,4],
+> [2,3],
+> [1,2],
+> [1,3],
+> [1,4],
+> ]
+> ```
+
+**💡 思路**:翻译一下就变成子集问题了:**给你输入一个数组 `nums = [1,2..,n]` 和一个正整数 `k`,请你生成所有大小为 `k` 的子集**。
+
+
+
+反映到代码上,只需要稍改 base case,控制算法仅仅收集第 `k` 层节点的值即可:
+
+```java
+public List> combine(int n, int k) {
+ List> res = new ArrayList<>();
+ // 记录回溯算法的递归路径
+ List track = new ArrayList<>();
+ // start 从 1 开始即可
+ backtrack(n, k, 1, track, res);
+ return res;
+}
+
+private void backtrack(int n, int k, int start, List track, List> res) {
+ // 遍历到了第 k 层,收集当前节点的值
+ if (track.size() == k) {
+ res.add(new ArrayList<>(track)); // 深拷贝
+ return;
+ }
+
+ // 从当前数字开始尝试
+ for (int i = start; i <= n; i++) {
+ track.add(i);
+ // 通过 start 参数控制树枝的遍历,避免产生重复的子集
+ backtrack(n, k, i + 1, track, res); // 递归选下一个数字
+ track.remove(track.size() - 1); // 撤销选择
+ }
+}
+```
+
+
+
+#### 🔄 [全排列_46](https://leetcode.cn/problems/permutations/description/)
+
+> 给定一个不含重复数字的数组 `nums` ,返回其 *所有可能的全排列* 。你可以 **按任意顺序** 返回答案。
+>
+> ```
+> 输入:nums = [1,2,3]
+> 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
+> ```
+
+**💡 思路**:组合/子集问题使用 `start` 变量保证元素 `nums[start]` 之后只会出现 `nums[start+1..]`中的元素,通过固定元素的相对位置保证不出现重复的子集。
+
+**但排列问题本身就是让你穷举元素的位置,`nums[i]` 之后也可以出现 `nums[i]` 左边的元素,所以之前的那一套玩不转了,需要额外使用 `used` 数组来标记哪些元素还可以被选择**。
+
+全排列共有 `n!` 个,我们可以按阶乘举例的思想,画出「回溯树」
+
+
+
+> 回溯树是一种树状结构,树的每个节点表示一个状态(即当前的选择或部分解),树的每条边表示一次决策的选择。在回溯过程中,我们从根节点开始,递归地选择下一个数字,每次递归都相当于进入树的下一层。
+
+> **labuladong 称为 决策树,你在每个节点上其实都在做决策**。因为比如你选了 2 之后,只能再选 1 或者 3,全排列是不允许重复使用数字的。**`[2]` 就是「路径」,记录你已经做过的选择;`[1,3]` 就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层叶子节点,这里也就是选择列表为空的时候**。
+
+```java
+public class Solution {
+ public List> permute(int[] nums) {
+ List> res = new ArrayList<>();
+ // 记录「路径」
+ List track = new ArrayList<>();
+ boolean[] used = new boolean[nums.length]; // 标记数字是否被使用过
+ backtrack(nums, used, track, res);
+ return res;
+ }
+
+ private void backtrack(int[] nums, boolean[] used, List track, List> res) {
+ // 当排列的大小达到nums.length时,说明当前排列完成
+ if (track.size() == nums.length) {
+ res.add(new ArrayList<>(track)); // 将当前排列加入结果
+ return;
+ }
+
+ // 尝试每一个数字
+ for (int i = 0; i < nums.length; i++) {
+ if (used[i]) continue; // 如果当前数字已经被使用过,则跳过,剪枝操作
+
+ // 做选择
+ track.add(nums[i]);
+ used[i] = true; // 标记当前数字为已使用
+
+ // 递归进入下一层
+ backtrack(nums, used, track, res);
+
+ // 撤销选择
+ track.remove(track.size() - 1);
+ used[i] = false; // 回溯时将当前数字标记为未使用
+ }
+ }
+}
+```
+
+
+
+### 🔄 二、元素可重不可复选
+
+#### 📦 [子集 II_90](https://leetcode.cn/problems/subsets-ii/)
+
+> 给你一个整数数组 `nums` ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
+>
+> 解集 **不能** 包含重复的子集。返回的解集中,子集可以按 **任意顺序** 排列。
+>
+> ```
+> 输入:nums = [1,2,2]
+> 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
+> ```
+
+**💡 思路**:该问题的关键是**去重**(剪枝),**体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 `nums[i] == nums[i-1]`,则跳过**
+
+> LeetCode 78 **Subsets** 问题并没有重复的子集。我们生成的是所有可能的子集,并且不需要考虑去除重复的子集,因为给定的数组 `nums` 不含重复元素。而在 **Subsets II** 中,由于输入数组可能包含重复元素,所以我们需要特殊处理来避免生成重复的子集。
+
+```java
+public class Solution {
+ public List> subsetsWithDup(int[] nums) {
+ List> res = new ArrayList<>();
+ Arrays.sort(nums); // 排序,确保相同的元素相邻
+ backtrack(nums, 0, new ArrayList<>(), res);
+ return res;
+ }
+
+ private void backtrack(int[] nums, int start, List track, List> res) {
+ // 每次递归时,将当前的track添加到结果中
+ res.add(new ArrayList<>(track));
+
+ // 从start位置开始遍历
+ for (int i = start; i < nums.length; i++) {
+ // 如果当前元素与前一个元素相同,并且前一个元素没有被选择,跳过当前元素
+ if (i > start && nums[i] == nums[i - 1]) {
+ continue; // 剪枝
+ }
+
+ // 做选择
+ track.add(nums[i]);
+ // 递归进入下一层
+ backtrack(nums, i + 1, track, res);
+ // 撤销选择
+ track.remove(track.size() - 1);
+ }
+ }
+}
+```
+
+
+
+#### 🎯 [组合总和 II_40](https://leetcode.cn/problems/combination-sum-ii/)
+
+> 给定一个候选人编号的集合 `candidates` 和一个目标数 `target` ,找出 `candidates` 中所有可以使数字和为 `target` 的组合。
+>
+> `candidates` 中的每个数字在每个组合中只能使用 **一次** 。
+>
+> **注意:**解集不能包含重复的组合。
+>
+> ```
+> 输入: candidates = [10,1,2,7,6,1,5], target = 8,
+> 输出:
+> [
+> [1,1,6],
+> [1,2,5],
+> [1,7],
+> [2,6]
+> ]
+> ```
+
+**💡 思路**:说这是一个组合问题,其实换个问法就变成子集问题了:请你计算 `candidates` 中所有和为 `target` 的子集。
+
+1. **排序**:首先对 `candidates` 数组进行排序,排序后的数组方便处理重复数字。
+2. **递归选择**:在递归过程中,确保如果当前数字和上一个数字相同,且上一个数字没有被选择过,则跳过当前数字,从而避免重复组合。
+3. **递归终止条件**:如果 `target` 变为 0,表示找到了一个符合条件的组合;如果 `target` 小于 0,表示当前路径不合法,应该回溯。
+
+```java
+public class Solution {
+ public List> combinationSum2(int[] candidates, int target) {
+ List> res = new ArrayList<>();
+ Arrays.sort(candidates); // 排序,便于后续去重
+ backtrack(candidates, target, 0, new ArrayList<>(), res);
+ return res;
+ }
+
+ private void backtrack(int[] candidates, int target, int start, List track, List> res) {
+ // 当目标值为0时,找到一个符合条件的组合
+ if (target == 0) {
+ res.add(new ArrayList<>(track)); // 复制当前组合并加入结果
+ return;
+ }
+
+ // 遍历候选数组
+ for (int i = start; i < candidates.length; i++) {
+ // 剪枝:当前数字大于目标值,后续不可能有合法的组合
+ if (candidates[i] > target) {
+ break;
+ }
+ // 剪枝:跳过重复的数字
+ if (i > start && candidates[i] == candidates[i - 1]) {
+ continue;
+ }
+
+ // 做选择:选择当前数字
+ track.add(candidates[i]);
+ // 递归,注意i + 1表示下一个位置,确保每个数字只使用一次
+ backtrack(candidates, target - candidates[i], i + 1, track, res);
+ // 撤销选择
+ track.remove(track.size() - 1);
+ }
+ }
+}
+
+```
+
+
+
+#### 🔄 [全排列 II_47](https://leetcode.cn/problems/permutations-ii/)
+
+> 给定一个可包含重复数字的序列 `nums` ,***按任意顺序*** 返回所有不重复的全排列。
+>
+> ```
+> 输入:nums = [1,1,2]
+> 输出:
+> [[1,1,2],
+> [1,2,1],
+> [2,1,1]]
+> ```
+
+**💡 思路**:典型的回溯
+
+1. **排序**:排序的目的是为了能够在回溯时做出剪枝选择。如果两个数字相同,并且前一个数字未被使用过,那么就可以跳过当前数字,避免产生重复的排列
+2. **回溯过程**:`backtrack` 是核心的递归函数。它每次递归时尝试将 `nums` 中的元素添加到 `track` 列表中。当 `track` 的大小等于 `nums` 的长度时,说明我们找到了一个排列,加入到结果 `res` 中。
+ - **标记数字是否被使用**:用一个布尔数组 `used[]` 来标记当前数字是否已经在某一层递归中被使用过,避免重复排列。
+ - **剪枝条件**:如果当前数字和前一个数字相同,并且前一个数字还没有被使用过,那么跳过当前数字,因为此时使用相同的数字会导致重复的排列。
+3. **递归回溯的选择与撤销**:
+ - **做选择**:选择当前数字 `nums[i]`,将其标记为已用,并添加到当前排列中。
+ - **递归**:继续递归地选择下一个数字,直到 `track` 的大小等于 `nums` 的大小。
+ - **撤销选择**:递归回到上一层时,撤销刚刚的选择,将当前数字从 `track` 中移除,并将其标记为未使用。
+
+> 剪枝的逻辑:
+>
+> - **第一轮回溯**:选择第一个 `1`,然后继续递归选择,生成一个排列。
+>
+> - **第二轮回溯**:当我们尝试选择第二个 `1` 时,假如第一个 `1` 没有被使用,第二个 `1` 会被选择,这时就会生成一个和第一轮相同的排列。为避免这种情况,我们需要 **剪枝**。
+
+```java
+public class Solution {
+ public List> permuteUnique(int[] nums) {
+ List> res = new ArrayList<>();
+ Arrays.sort(nums); // 排序,确保相同元素相邻
+ backtrack(nums, new boolean[nums.length], new ArrayList<>(), res);
+ return res;
+ }
+
+ private void backtrack(int[] nums, boolean[] used, List track, List> res) {
+ // 当排列的大小达到nums.length时,找到一个合法排列
+ if (track.size() == nums.length) {
+ res.add(new ArrayList<>(track)); // 加入当前排列
+ return;
+ }
+
+ // 遍历数组,递归生成排列
+ for (int i = 0; i < nums.length; i++) {
+ // 剪枝:当前元素已经被使用过,跳过
+ if (used[i]) continue;
+ // 剪枝:跳过相同的元素,避免生成重复排列
+ if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
+
+ // 做选择
+ track.add(nums[i]);
+ used[i] = true; // 标记当前元素已使用
+
+ // 递归
+ backtrack(nums, used, track, res);
+
+ // 撤销选择
+ track.remove(track.size() - 1);
+ used[i] = false; // 标记当前元素未使用
+ }
+ }
+}
+
+```
+
+
+
+### ♻️ 三、元素无重复可复选
+
+输入数组无重复元素,但每个元素可以被无限次使用
+
+#### 🎯 [组合总和_39](https://leetcode.cn/problems/combination-sum/)
+
+> 给你一个 **无重复元素** 的整数数组 `candidates` 和一个目标整数 `target` ,找出 `candidates` 中可以使数字和为目标数 `target` 的 所有 **不同组合** ,并以列表形式返回。你可以按 **任意顺序** 返回这些组合。
+>
+> `candidates` 中的 **同一个** 数字可以 **无限制重复被选取** 。如果至少一个数字的被选数量不同,则两种组合是不同的。
+>
+> 对于给定的输入,保证和为 `target` 的不同组合数少于 `150` 个。
+>
+> ```
+> 输入:candidates = [2,3,6,7], target = 7
+> 输出:[[2,2,3],[7]]
+> 解释:
+> 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
+> 7 也是一个候选, 7 = 7 。
+> 仅有这两种组合。
+> ```
+
+**💡 思路**:**元素无重可复选,即 `nums` 中的元素都是唯一的,每个元素可以被使用若干次**,只要删掉去重逻辑即可
+
+```java
+public class Solution {
+ public List> combinationSum(int[] candidates, int target) {
+ List> res = new ArrayList<>();
+ List track = new ArrayList<>();
+ backtrack(candidates, target, 0, track, res);
+ return res;
+ }
+
+ private void backtrack(int[] candidates, int target, int start, List track, List> res) {
+ // 如果目标值为0,表示当前组合符合条件
+ if (target == 0) {
+ res.add(new ArrayList<>(track)); // 将当前组合加入结果
+ return;
+ }
+
+ // 遍历候选数组
+ for (int i = start; i < candidates.length; i++) {
+ // 如果当前数字大于目标值,跳过
+ if (candidates[i] > target) continue;
+
+ // 做选择:选择当前数字
+ track.add(candidates[i]);
+
+ // 递归,注意这里传入 i,因为可以重复选择当前数字
+ backtrack(candidates, target - candidates[i], i, track, res);
+
+ // 撤销选择
+ track.remove(track.size() - 1);
+ }
+ }
+}
+```
+
+
+
+## 其他问题
+
+### 📞 [电话号码的字母组合_17](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/)
+
+> 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
+>
+> 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
+>
+> 
+>
+> ```
+> 输入:digits = "23"
+> 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
+> ```
+
+**💡 思路**:回溯,递归地尝试每一位数字对应的所有字母,直到找出所有有效的组合
+
+首先,我们需要将每个数字 2 到 9 映射到其对应的字母,可以用 Map , 也可以用数组。然后就是递归处理。
+
+**递归终止条件**:当当前组合的长度与输入的数字字符串长度相同,就说明我们已经得到了一个有效的组合,可以将其加入结果集。
+
+
+
+```java
+public class Solution {
+ public List letterCombinations(String digits) {
+ List res = new ArrayList<>();
+ if (digits == null || digits.length() == 0) {
+ return res; // 如果输入为空,返回空结果
+ }
+
+ // 数字到字母的映射
+ String[] mapping = {
+ "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
+ };
+
+ // 使用回溯法生成字母组合
+ backtrack(digits, 0, mapping, new StringBuilder(), res);
+ return res;
+ }
+
+ private void backtrack(String digits, int index, String[] mapping, StringBuilder current, List res) {
+ // 如果当前组合的长度等于输入的长度,说明已经生成了一个有效的字母组合
+ if (index == digits.length()) {
+ res.add(current.toString());
+ return;
+ }
+
+ // 获取当前数字对应的字母
+ String letters = mapping[digits.charAt(index) - '0'];
+
+ // 递归选择字母
+ for (char letter : letters.toCharArray()) {
+ current.append(letter); // 选择一个字母
+ backtrack(digits, index + 1, mapping, current, res); // 递归处理下一个数字
+ current.deleteCharAt(current.length() - 1); // 撤销选择
+ }
+ }
+}
+```
+
+
+
+### 🔗 [括号生成_22](https://leetcode.cn/problems/generate-parentheses/)
+
+> 数字 `n` 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。
+>
+> ```
+> 输入:n = 3
+> 输出:["((()))","(()())","(())()","()(())","()()()"]
+> ```
+
+**💡 思路**:
+
+
+
+```java
+
+public List generateParenthesis(int n) {
+ List res = new ArrayList<>();
+ // 回溯过程中的路径
+ StringBuilder track = new StringBuilder();
+ if (n == 0) {
+ return res;
+ }
+ trackback(n, n, res, track);
+ return res;
+ }
+
+ // 可用的左括号数量为 left 个,可用的右括号数量为 right 个
+ private void trackback(int left, int right, List res, StringBuilder track) {
+ //如果剩余的左括号数量大于右括号数量
+ if (left < 0 || right < 0 || left > right) {
+ return;
+ }
+
+ // 当所有括号都恰好用完时,得到一个合法的括号组合
+ if (left == 0 && right == 0) {
+ res.add(track.toString());
+ return;
+ }
+
+ // 做选择,尝试放一个左括号
+ track.append('(');
+ trackback(left - 1, right, res, track);
+ // 撤销选择
+ track.deleteCharAt(track.length() - 1);
+
+ track.append(')');
+ trackback(left, right - 1, res, track);
+ track.deleteCharAt(track.length() - 1);
+
+ }
+```
+
+
+
+### ♛ [N 皇后_51](https://leetcode.cn/problems/n-queens/)
+
+> 按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
+>
+> **n 皇后问题** 研究的是如何将 `n` 个皇后放置在 `n×n` 的棋盘上,并且使皇后彼此之间不能相互攻击。
+>
+> 给你一个整数 `n` ,返回所有不同的 **n 皇后问题** 的解决方案。
+>
+> 每一种解法包含一个不同的 **n 皇后问题** 的棋子放置方案,该方案中 `'Q'` 和 `'.'` 分别代表了皇后和空位。
+>
+> 
+>
+> ```
+>输入:n = 4
+> 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
+> 解释:如上图所示,4 皇后问题存在两个不同的解法。
+> ```
+
+**💡 思路**:通过回溯算法逐行放置皇后,每次递归时确保当前行、列和对角线不被其他皇后攻击。通过标记已占用的列和对角线,避免重复搜索,最终生成所有合法的解。
+
+- 如果在某一列或对角线处已有皇后,就不能在该位置放置皇后。我们可以使用三个辅助数组来追踪列和对角线的使用情况:
+ - `cols[i]`:表示第 `i` 列是否已经放置了皇后。
+ - `diag1[i]`:表示从左上到右下的对角线(`row - col`)是否已经有皇后。
+ - `diag2[i]`:表示从右上到左下的对角线(`row + col`)是否已经有皇后。
+
+- 主对角线是从左上角到右下角的对角线、副对角线是从右上角到左下角的对角线
+
+```java
+public class NQueens {
+
+ public List> solveNQueens(int N) {
+ List> result = new ArrayList<>();
+ char[][] board = new char[N][N];
+
+ // 初始化棋盘,每个位置设为'.'
+ for (int i = 0; i < N; i++) {
+ for (int j = 0; j < N; j++) {
+ board[i][j] = '.';
+ }
+ }
+
+ // 用来记录列、主对角线、副对角线是否已被占用
+ boolean[] cols = new boolean[N]; // 列占用标记
+ boolean[] diag1 = new boolean[2 * N - 1]; // 主对角线占用标记
+ boolean[] diag2 = new boolean[2 * N - 1]; // 副对角线占用标记
+
+ backtrack(N, 0, board, cols, diag1, diag2, result);
+ return result;
+ }
+
+ // 回溯函数
+ private void backtrack(int N, int row, char[][] board, boolean[] cols, boolean[] diag1, boolean[] diag2, List> result) {
+ if (row == N) { // 如果已经放置了 N 个皇后
+ List solution = new ArrayList<>();
+ for (int i = 0; i < N; i++) {
+ solution.add(new String(board[i])); // 将每一行转化为字符串并添加到结果中
+ }
+ result.add(solution);
+ return;
+ }
+
+ // 遍历每一列,尝试放置皇后
+ for (int col = 0; col < N; col++) {
+ // 判断当前位置是否可以放置皇后
+ if (cols[col] || diag1[row - col + (N - 1)] || diag2[row + col]) {
+ continue; // 如果列、主对角线或副对角线已被占用,跳过当前列
+ }
+
+ // 放置皇后
+ board[row][col] = 'Q';
+ cols[col] = true; // 标记该列已被占用
+ diag1[row - col + (N - 1)] = true; // 标记主对角线已被占用
+ diag2[row + col] = true; // 标记副对角线已被占用
+
+ // 递归放置下一行的皇后
+ backtrack(N, row + 1, board, cols, diag1, diag2, result);
+
+ // 回溯,撤销当前位置的选择
+ board[row][col] = '.';
+ cols[col] = false;
+ diag1[row - col + (N - 1)] = false;
+ diag2[row + col] = false;
+ }
+ }
+
+ // 打印结果
+ public void printSolutions(List> solutions) {
+ for (List solution : solutions) {
+ for (String row : solution) {
+ System.out.println(row);
+ }
+ System.out.println();
+ }
+ }
+
+ public static void main(String[] args) {
+ NQueens nq = new NQueens();
+ List> solutions = nq.solveNQueens(4);
+ nq.printSolutions(solutions);
+ }
+}
+
+```
+
+
+
+
+
+## 📚 参考与感谢:
+
+- https://yuminlee2.medium.com/combinations-and-combination-sum-3ed2accc8d12
+- https://medium.com/@sunshine990316/leetcode-python-backtracking-summary-medium-1-e8ae88839e85
+- https://blog.devgenius.io/10-daily-practice-problems-day-18-f7293b55224d
+- [hello 算法- 回溯算法](https://www.hello-algo.com/chapter_backtracking/backtracking_algorithm/#1312)
+
+---
+
+> 🎉 **恭喜你完成了回溯算法的学习!** 回溯算法是解决很多复杂问题的强大工具,掌握了它,你就拥有了解决排列、组合、子集等问题的钥匙。记住:**选择 → 递归 → 撤销选择** 是回溯的核心思想!
diff --git a/docs/data-structure-algorithms/algorithm/Binary-Search.md b/docs/data-structure-algorithms/algorithm/Binary-Search.md
new file mode 100755
index 0000000000..9fb5f32679
--- /dev/null
+++ b/docs/data-structure-algorithms/algorithm/Binary-Search.md
@@ -0,0 +1,1030 @@
+---
+title: 二分查找
+date: 2023-02-09
+tags:
+ - binary-search
+ - algorithms
+categories: algorithms
+---
+
+
+
+> 二分查找【折半查找】,一种简单高效的搜索算法,一般是利用有序数组的特性,通过逐步比较中间元素来快速定位目标值。
+>
+> 二分查找并不简单,Knuth 大佬(发明 KMP 算法的那位)都说二分查找:**思路很简单,细节是魔鬼**。比如二分查找让人头疼的细节问题,到底要给 `mid` 加一还是减一,while 里到底用 `<=` 还是 `<`。
+
+## 一、二分查找基础框架
+
+```java
+int binarySearch(int[] nums, int target) {
+ int left = 0, right = ...;
+
+ while(...) {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] == target) {
+ ...
+ } else if (nums[mid] < target) {
+ left = ...
+ } else if (nums[mid] > target) {
+ right = ...
+ }
+ }
+ return ...;
+}
+```
+
+**分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节**。本文都会使用 else if,旨在讲清楚,读者理解后可自行简化。
+
+其中 `...` 标记的部分,就是可能出现细节问题的地方,当你见到一个二分查找的代码时,首先注意这几个地方。
+
+**另外提前说明一下,计算 `mid` 时需要防止溢出**,代码中 `left + (right - left) / 2` 就和 `(left + right) / 2` 的结果相同,但是有效防止了 `left` 和 `right` 太大,直接相加导致溢出的情况。
+
+
+
+## 二、二分查找性能分析
+
+**时间复杂度**:二分查找的时间复杂度为 $O(log n)$,其中 n 是数组的长度。这是因为每次比较后,搜索范围都会减半,非常高效。
+
+> logn 是一个非常“恐怖”的数量级,即便 n 非常非常大,对应的 logn 也很小。比如 n 等于 2 的 32 次方,这个数很大了吧?大约是 42 亿。也就是说,如果我们在 42 亿个数据中用二分查找一个数据,最多需要比较 32 次。
+
+**空间复杂度**:
+
+- 迭代法:$O(1)$,因为只需要常数级别的额外空间。
+- 递归法:$O(log n)$,因为递归调用会占用栈空间。
+
+**最坏情况**:最坏情况下,目标值位于数组两端或不存在,需要$log n$次比较才能确定。
+
+**二分查找与其他搜索算法的比较**:
+
+- 线性搜索:线性搜索简单,时间复杂度为$O(n)$,但在大规模数据集上效率较低。
+
+- 哈希:哈希在查找上能提供平均$O(1)$的时间复杂度,但需要额外空间存储哈希表,且对数据有序性无要求。
+
+
+
+## 三、刷刷热题
+
+- 二分查找,可以用循环(迭代)实现,也可以用递归实现
+- 二分查找依赖的是顺序表结构(也就是数组)
+- 二分查找针对的是有序数组
+- 数据量太小太大都不是很适用二分(太小直接顺序遍历就够了,太大的话对连续内存空间要求更高)
+
+### [二分查找『704』](https://leetcode.cn/problems/binary-search/)(基本的二分搜索)
+
+> 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
+>
+
+```java
+int binarySearch(int[] nums, int target) {
+ int left = 0;
+ int right = nums.length - 1; // 注意
+
+ while(left <= right) {
+ int mid = left + (right - left) / 2;
+ if(nums[mid] == target)
+ return mid;
+ else if (nums[mid] < target)
+ left = mid + 1; // 注意
+ else if (nums[mid] > target)
+ right = mid - 1; // 注意
+ }
+ return -1;
+}
+```
+
+**时间复杂度**:O(log n),每次都将搜索范围缩小一半
+**空间复杂度**:O(1),只使用常量级别的额外空间
+
+**1、为什么 while 循环的条件中是 <=,而不是 <**?
+
+答:因为初始化 `right` 的赋值是 `nums.length - 1`,即最后一个元素的索引,而不是 `nums.length`。
+
+这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 `[left, right]`,后者相当于左闭右开区间 `[left, right)`。因为索引大小为 `nums.length` 是越界的,所以我们把 `right` 这一边视为开区间。
+
+我们这个算法中使用的是前者 `[left, right]` 两端都闭的区间。**这个区间其实就是每次进行搜索的区间**。
+
+**2、为什么 `left = mid + 1`,`right = mid - 1`?我看有的代码是 `right = mid` 或者 `left = mid`,没有这些加加减减,到底怎么回事,怎么判断**?
+
+答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。
+
+刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 `[left, right]`。那么当我们发现索引 `mid` 不是要找的 `target` 时,下一步应该去搜索哪里呢?
+
+当然是去搜索区间 `[left, mid-1]` 或者区间 `[mid+1, right]` 对不对?**因为 `mid` 已经搜索过,应该从搜索区间中去除**。
+
+> ##### 1. **左闭右闭区间 `[left, right]`**
+>
+> - **循环条件**:`while (left <= right)`,因为 `left == right` 时区间仍有意义。
+> - 边界调整:
+> - `nums[mid] < target` → `left = mid + 1`(排除 `mid` 左侧)
+> - `nums[mid] > target` → `right = mid - 1`(排除 `mid` 右侧)
+> - 适用场景:明确目标值存在于数组时,直接返回下标。
+>
+> ##### 2. **左闭右开区间 `[left, right)`**
+>
+> - **初始化**:`right = nums.length`。
+> - **循环条件**:`while (left < right)`,因为 `left == right` 时区间为空。
+> - 边界调整:
+> - `nums[mid] < target` → `left = mid + 1`
+> - `nums[mid] > target` → `right = mid`(右开,不包含 `mid`)
+> - **适用场景**:需要处理目标值可能不在数组中的情况,例如插入位置问题
+
+> 比如说给你有序数组 `nums = [1,2,2,2,3]`,`target` 为 2,此算法返回的索引是 2,没错。但是如果我想得到 `target` 的左侧边界,即索引 1,或者我想得到 `target` 的右侧边界,即索引 3,这样的话此算法是无法处理的。
+>
+> 所以又有了一些含有重复元素,带有边界问题的二分。
+
+### 寻找左侧边界的二分搜索
+
+```java
+public int leftBound(int[] nums, int target) {
+ int left = 0;
+ int right = nums.length - 1;
+ while (left <= right) {
+ int mid = (right - left) / 2 + left;
+ if (nums[mid] > target) {
+ right = mid - 1;
+ } else if (nums[mid] < target) {
+ left = mid + 1;
+ } else {
+ //mid 是第一个元素,或者前一个元素不等于查找值,锁定,且返回的是mid
+ if (mid == 0 || nums[mid - 1] != target) return mid;
+ else right = mid - 1;
+ }
+ }
+ return -1;
+}
+```
+
+### 寻找右侧边界的二分查找
+
+```java
+public int rightBound(int[] nums, int target){
+ int left = 0;
+ int right = nums.length - 1;
+ while(left <= right){
+ int mid = left + (right - left)/2;
+ if(nums[mid] > target){
+ right = mid - 1;
+ }else if(nums[mid] < target){
+ left = mid +1;
+ }else{
+ if(mid == nums.length - 1 || nums[mid +1] != target) return mid;
+ else left = mid + 1;
+ }
+ }
+ return -1;
+}
+```
+
+### 查找第一个大于等于给定值的元素
+
+```JAVA
+//查找第一个大于等于给定值的元素 1,3,5,7,9 找出第一个大于等于5的元素
+public int firstNum(int[] nums, int target) {
+ int left = 0;
+ int right = nums.length - 1;
+ while (left <= right) {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] >= target) {
+ if (mid == 0 || nums[mid - 1] < target) return mid;
+ else right = mid - 1;
+ } else {
+ left = mid + 1;
+ }
+ }
+ return -1;
+}
+```
+
+
+
+### [搜索旋转排序数组『33』](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)
+
+> 整数数组 nums 按升序排列,数组中的值 互不相同 。
+>
+> 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
+>
+> 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
+>
+> ```
+> 输入:nums = [4,5,6,7,0,1,2], target = 0
+> 输出:4
+> ```
+>
+> ```
+> 输入:nums = [4,5,6,7,0,1,2], target = 3
+> 输出:-1
+> ```
+
+**思路**:
+
+对于有序数组(部分有序也可以),可以使用二分查找的方法查找元素。
+
+旋转数组后,依然是局部有序,从数组中间分成左右两部分后,一定有一部分是有序的
+
+- 如果 [L, mid - 1] 是有序数组,且 target 的大小满足 [nums[L],nums[mid],则我们应该将搜索范围缩小至 [L, mid - 1],否则在 [mid + 1, R] 中寻找。
+- 如果 [mid, R] 是有序数组,且 target 的大小满足 ({nums}[mid+1],{nums}[R]],则我们应该将搜索范围缩小至 [mid + 1, R],否则在 [l, mid - 1] 中寻找。
+
+```java
+public int search(int[] nums, int target) {
+ if (nums == null || nums.length == 0) return -1;
+
+ int left = 0, right = nums.length - 1;
+
+ while (left <= right) {
+ int mid = left + (right - left) / 2;
+
+ if (nums[mid] == target) {
+ return mid; // 找到目标
+ }
+
+ // 判断左半部分是否有序
+ if (nums[left] <= nums[mid]) {
+ // 左半部分有序
+ if (nums[left] <= target && target < nums[mid]) {
+ right = mid - 1; // 目标在左半部分
+ } else {
+ left = mid + 1; // 目标在右半部分
+ }
+ } else {
+ // 右半部分有序
+ if (nums[mid] < target && target <= nums[right]) {
+ left = mid + 1; // 目标在右半部分
+ } else {
+ right = mid - 1; // 目标在左半部分
+ }
+ }
+ }
+
+ return -1; // 未找到目标
+}
+```
+
+**时间复杂度**:$O(log n) $,二分查找的时间复杂度
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+
+
+### [在排序数组中查找元素的第一个和最后一个位置『34』](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)
+
+> 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
+>
+> 如果数组中不存在目标值 target,返回 [-1, -1]。
+>
+> 你可以设计并实现时间复杂度为 $O(log n) $ 的算法解决此问题吗?
+>
+> ```
+> 输入:nums = [5,7,7,8,8,10], target = 8
+> 输出:[3,4]
+> ```
+>
+> ```
+> 输入:nums = [5,7,7,8,8,10], target = 6
+> 输出:[-1,-1]
+> ```
+
+**思路**:二分法寻找左右边界值
+
+```java
+public int[] searchRange(int[] nums, int target) {
+ int first = binarySearch(nums, target, true);
+ int last = binarySearch(nums, target, false);
+ return new int[]{first, last};
+}
+
+public int binarySearch(int[] nums, int target, boolean findLast) {
+ int length = nums.length;
+ int left = 0, right = length - 1;
+ //结果,因为可能有多个值,所以需要先保存起来
+ int index = -1;
+ while (left <= right) {
+ //取中间值
+ int middle = left + (right - left) / 2;
+
+ //找到相同的值(只有这个地方和普通二分查找有不同)
+ if (nums[middle] == target) {
+ //先赋值一下,肯定是找到了,只是不知道这个值是不是在区域的边界内
+ index = middle;
+ //如果是查找最后的
+ if (findLast) {
+ //那我们将浮标移动到下一个值试探一下后面的值还是否有target
+ left = middle + 1;
+ } else {
+ //否则,就是查找第一个值,也是同理,移动指针到上一个值去试探一下上一个值是不是等于target
+ right = middle - 1;
+ }
+
+ //下面2个就是普通的二分查找流程,大于小于都移动指针
+ } else if (nums[middle] < target) {
+ left = middle + 1;
+ } else {
+ right = middle - 1;
+ }
+
+ }
+ return index;
+}
+```
+
+**时间复杂度**:$O(log n) $,需要进行两次二分查找
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+
+
+### [搜索插入位置『35』](https://leetcode.cn/problems/search-insert-position/)
+
+> 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 `O(log n)` 的算法。
+>
+> ```
+> 输入: nums = [1,3,5,6], target = 2
+> 输出: 1
+> ```
+
+```java
+public int searchInsert(int[] nums, int target) {
+ int left = 0;
+ int right = nums.length - 1;
+ //注意:特例处理
+ if (nums[left] > target) return 0;
+ if (nums[right] < target) return right + 1;
+ while (left <= right) {
+ int mid = left + (right - left) / 2;
+ if (nums[mid] > target) {
+ right = mid - 1;
+ } else if (nums[mid] < target) {
+ left = mid + 1;
+ } else {
+ return mid;
+ }
+ }
+ //注意:这里如果没有查到,返回left,也就是需要插入的位置
+ return left;
+ }
+```
+
+**时间复杂度**:$O(log n) $,标准的二分查找时间复杂度
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+
+
+### [寻找旋转排序数组中的最小值『153』](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)
+
+> 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
+> 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2] 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
+> 注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
+>
+>给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
+>
+>你必须设计一个时间复杂度为 $O(log n)$ 的算法解决此问题。
+>
+>```
+> 输入:nums = [3,4,5,1,2]
+> 输出:1
+> 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
+> ```
+>
+
+**思路**:
+
+升序数组+旋转,仍然是部分有序,考虑用二分查找。
+
+
+
+> 我们先搞清楚题目中的数组是通过怎样的变化得来的,基本上就是等于将整个数组向右平移
+
+> 这种二分查找难就难在,arr[mid] 跟谁比。
+>
+> 我们的目的是:当进行一次比较时,一定能够确定答案在 mid 的某一侧。一次比较为 arr[mid] 跟谁比的问题。
+> 一般的比较原则有:
+>
+> - 如果有目标值 target,那么直接让 arr[mid] 和 target 比较即可。
+> - 如果没有目标值,一般可以考虑 **端点**
+>
+> 如果中值 < 右值,则最小值在左半边,可以收缩右边界。
+> 如果中值 > 右值,则最小值在右半边,可以收缩左边界。
+
+旋转数组,最小值右侧的元素肯定都小于或等于数组中的最后一个元素 `nums[n-1]`,左侧元素都大于 `num[n-1]`
+
+```java
+public static int findMin(int[] nums) {
+ int left = 0;
+ int right = nums.length - 1;
+ //左闭右开
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ //疑问:为什么right = mid;而不是 right = mid-1;
+ //解答:{4,5,1,2,3},如果right = mid-1,则丢失了最小值1
+ if (nums[mid] < nums[right]) {
+ right = mid;
+ } else {
+ left = mid + 1;
+ }
+ }
+ //循环结束条件,left = right,最小值输出nums[left]或nums[right]均可
+ return nums[left];
+}
+```
+
+**时间复杂度**:$O(log n) $,每次都将搜索范围缩小一半
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+**如果是求旋转数组中的最大值呢**
+
+```java
+public static int findMax(int[] nums) {
+ int left = 0;
+ int right = nums.length - 1;
+
+ while (left < right) {
+ int mid = left + (right - left) >> 1;
+
+ //因为向下取整,left可能会等于mid,所以要考虑
+ if (nums[left] < nums[right]) {
+ return nums[right];
+ }
+
+ //[left,mid] 是递增的,最大值只会在[mid,right]中
+ if (nums[left] < nums[mid]) {
+ left = mid;
+ } else {
+ //[mid,right]递增,最大值只会在[left, mid-1]中
+ right = mid - 1;
+ }
+ }
+ return nums[left];
+}
+```
+
+### [寻找重复数『287』](https://leetcode-cn.com/problems/find-the-duplicate-number/)
+
+> 长度为 n+1 的数组,元素在 1~n 之间,有且仅有一个重复数(可能重复多次)。要求不修改数组且只用 O (1) 空间。
+>
+> ```
+>输入:nums = [1,3,4,2,2]
+> 输出:2
+>```
+>
+> ```
+> 输入:nums = [3,1,3,4,2]
+> 输出:3
+>```
+
+**思路**:
+
+- 统计小于等于 mid 的元素个数
+- 若 count > mid,说明重复数在 [1, mid] 区间
+- 否则在 [mid+1, n] 区间
+
+> 抽屉原理:把 `10` 个苹果放进 `9` 个抽屉,至少有一个抽屉里至少放 `2` 个苹果。
+
+```java
+public int findDuplicate(int[] nums) {
+ int left = 0;
+ int right = nums.length - 1;
+ while (left <= right) {
+ int mid = left + (right - left) / 2;
+
+ // nums 中小于等于 mid 的元素的个数
+ int count = 0;
+ for (int num : nums) {
+ //看这里,是 <= mid,而不是 nums[mid]
+ if (num <= mid) {
+ count += 1;
+ }
+ }
+
+ // 根据抽屉原理,小于等于 4 的个数如果严格大于 4 个,此时重复元素一定出现在 [1..4] 区间里
+ if (count > mid) {
+ // 重复元素位于区间 [left..mid]
+ right = mid - 1;
+ } else {
+ // if 分析正确了以后,else 搜索的区间就是 if 的反面区间 [mid + 1..right]
+ left = mid + 1;
+ }
+ }
+ return left;
+}
+```
+
+**时间复杂度**:$O(n log n)$,每次需要遍历数组O(n)
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+
+
+### [寻找峰值『162』](https://leetcode-cn.com/problems/find-peak-element/)
+
+> 峰值元素是指其值严格大于左右相邻值的元素。
+>
+> 给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
+>
+> 你可以假设 nums[-1] = nums[n] = -∞ 。
+>
+> 你必须实现时间复杂度为 $O(log n) $的算法来解决此问题。
+>
+> ```
+> 输入:nums = [1,2,3,1]
+> 输出:2
+> 解释:3 是峰值元素,你的函数应该返回其索引 2。
+> ```
+>
+> ```
+> 输入:nums = [1,2,1,3,5,6,4]
+> 输出:1 或 5
+> 解释:你的函数可以返回索引 1,其峰值元素为 2;
+> 或者返回索引 5, 其峰值元素为 6。
+> ```
+
+**思路**:
+
+- 比较 `nums[mid]` 和 `nums[mid+1]`
+- 如果 `nums[mid] < nums[mid+1]`,说明右侧一定有峰值
+- 否则左侧一定有峰值
+
+**为什么有效**:
+
+- 峰值条件 `nums[i] > nums[i+1]` 保证单调性
+- 二分查找每次可以缩小一半搜索范围
+
+```java
+public int findPeakElement(int[] nums) {
+ int left = 0, right = nums.length - 1;
+
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+
+ if (nums[mid] < nums[mid + 1]) {
+ // 右侧有峰值
+ left = mid + 1;
+ } else {
+ // 左侧有峰值
+ right = mid;
+ }
+ }
+
+ return left; // left == right,即为峰值位置
+}
+```
+
+**时间复杂度**:$O(log n) $,二分查找的时间复杂度
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+
+
+### [搜索二维矩阵『74』](https://leetcode.cn/problems/search-a-2d-matrix/)
+
+> 给你一个满足以下两点的 `m x n` 矩阵:
+>
+> - 每行从左到右递增
+> - 每行第一个数大于上一行最后一个数
+>
+> 判断目标值 `target` 是否在矩阵中。
+>
+> 给你一个整数 `target` ,如果 `target` 在矩阵中,返回 `true` ;否则,返回 `false` 。
+>
+> 
+>
+> ```
+> 输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
+> 输出:true
+> ```
+
+**思路**:由于每行的第一个元素都大于前一行的最后一个元素,整个矩阵可以被看作一个**完全有序的一维数组**。
+
+例如,上面的示例矩阵可以被 “拉直” 为:`[1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60]`
+
+要实现 “一次二分”,必须能将一维数组的索引 `mid`(二分查找中的中间位置)转换回二维矩阵的 `(行号, 列号)`,公式如下:
+
+- **行号 = mid /n**(整除,因为每一行有 `n` 个元素,商表示当前元素在第几行)
+- **列号 = mid % n**(取余,余数表示当前元素在该行的第几列)
+
+举例:
+
+- 一维索引 `mid = 5`,`n = 4`(列数):
+ - 行号 = 5 / 4 = 1(第 2 行,索引从 0 开始)
+ - 列号 = 5 % 4 = 1(第 2 列)
+ - 对应矩阵值:`matrix[1][1] = 11`,与一维数组索引 5 的值一致。
+
+```java
+public boolean searchMatrix(int[][] matrix, int target) {
+ // 1. 处理边界情况:矩阵为空(行数为 0)
+ int m = matrix.length;
+ if (m == 0) {
+ return false;
+ }
+
+ // 2. 处理边界情况:矩阵列数为空(每行没有元素)
+ int n = matrix[0].length;
+ if (n == 0) {
+ return false;
+ }
+
+ // 3. 初始化二分查找的左右指针(对应一维数组的起始和末尾索引)
+ int left = 0;
+ int right = m * n - 1; // 总元素数 = 行数 × 列数,末尾索引 = 总元素数 - 1
+
+ // 4. 二分查找循环:left <= right 确保不遗漏元素
+ while (left <= right) {
+ // 计算中间索引:避免 left + right 直接相加导致整数溢出
+ int mid = left + (right - left) / 2;
+
+ // 关键:将一维索引 mid 转换为二维矩阵的 (row, col) 坐标
+ int row = mid / n; // 行号 = 中间索引 ÷ 列数(整除)
+ int col = mid % n; // 列号 = 中间索引 % 列数(取余)
+
+ // 获取当前中间位置的矩阵值
+ int midValue = matrix[row][col];
+
+ // 5. 比较 midValue 与 target,调整二分范围
+ if (midValue == target) {
+ // 找到目标值,直接返回 true
+ return true;
+ } else if (midValue < target) {
+ // 中间值比目标小:目标在右半部分,left 移到 mid + 1
+ left = mid + 1;
+ } else {
+ // 中间值比目标大:目标在左半部分,right 移到 mid - 1
+ right = mid - 1;
+ }
+ }
+
+ // 6. 循环结束仍未找到,说明目标不在矩阵中
+ return false;
+}
+```
+
+**时间复杂度**:$O(log(mn)) $:二分查找的时间复杂度是 `O(log(总元素数))`,总元素数为 `m×n`,因此复杂度为 `O(log(m×n))`
+**空间复杂度**:$O(1)$:仅使用了 `m、n、left、right、mid、row、col、midValue` 等常数个变量,没有额外占用空间
+
+
+
+### [搜索二维矩阵 II『240』](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/)
+
+> [剑指 Offer 04. 二维数组中的查找](https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/) 一样的题目
+>
+> 在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
+>
+> 现有矩阵 matrix 如下:
+>
+> 
+>
+> ```
+> 输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
+> 输出:true
+> ```
+
+**思路**:
+
+站在左下角或者右上角看。这个矩阵其实就像是一个Binary Search Tree。然后,聪明的大家应该知道怎么做了。
+
+
+
+有序的数组,我们首先应该想到二分
+
+```java
+public boolean searchMatrix(int[][] matrix, int target) {
+ // 1. 处理边界情况:矩阵为空、行数为0、或列数为0
+ if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
+ return false;
+ }
+
+ // 2. 获取矩阵的行数 m 和列数 n
+ int m = matrix.length;
+ int n = matrix[0].length;
+
+ // 3. 初始化指针,从左下角开始
+ int row = m - 1; // 行指针指向最后一行
+ int col = 0; // 列指针指向第一列
+
+ // 4. 循环条件:行指针不能越界(>= 0),列指针不能越界(< n)
+ while (row >= 0 && col < n) {
+ // 获取当前指针指向的元素值
+ int current = matrix[row][col];
+
+ // 5. 比较 current 与 target
+ if (current == target) {
+ // 找到了目标值,直接返回 true
+ return true;
+ } else if (current < target) {
+ // 当前值小于目标值:目标值不可能在当前行(因为行是递增的)
+ // 将列指针向右移动,去更大的区域查找
+ col++;
+ } else { // current > target
+ // 当前值大于目标值:目标值不可能在当前列(因为列是递增的)
+ // 将行指针向上移动,去更小的区域查找
+ row--;
+ }
+ }
+
+ // 6. 如果循环结束仍未找到,说明目标值不存在于矩阵中
+ return false;
+ }
+}
+```
+
+**时间复杂度**:$O(m + n)$,其中m是行数,n是列数,最坏情况下需要遍历m+n个元素
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+
+
+### [最长递增子序列『300』](https://leetcode.cn/problems/longest-increasing-subsequence/)
+
+> 给你一个整数数组 `nums` ,找到其中最长严格递增子序列的长度。
+>
+> **子序列** 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,`[3,6,2,7]`是数组 `[0,3,1,6,2,2,7]` 的子序列。
+>
+> ```
+> 输入:nums = [10,9,2,5,3,7,101,18]
+> 输出:4
+> 解释:最长递增子序列是 [2,3,7,101],因此长度为 4。
+> ```
+
+**思路**:
+
+**动态规划解法**:
+
+```java
+public int lengthOfLIS(int[] nums) {
+ if (nums == null || nums.length == 0) return 0;
+ int[] dp = new int[nums.length];
+ //初始时,每个元素自身构成一个长度为1的子序列
+ Arrays.fill(dp, 1);
+ // 记录全局最长递增子序列的长度,初始为1
+ int maxLen = 1;
+
+ for (int i = 1; i < nums.length; i++) {
+ // 对于每个i,检查所有j < i的元素
+ for (int j = 0; j < i; j++) {
+ // 如果nums[j] < nums[i],说明nums[i]可以接在nums[j]后面
+ if (nums[j] < nums[i]) {
+ // 更新dp[i]为dp[j] + 1和当前dp[i]的较大值
+ dp[i] = Math.max(dp[i], dp[j] + 1);
+ }
+ }
+ // 更新全局最大值
+ maxLen = Math.max(maxLen, dp[i]);
+ }
+ return maxLen;
+}
+```
+
+**二分查找优化解法**:
+```java
+public int lengthOfLIS(int[] nums) {
+ if (nums == null || nums.length == 0) return 0;
+
+ List tails = new ArrayList<>();
+
+ for (int num : nums) {
+ // 二分查找第一个大于等于num的位置
+ int left = 0, right = tails.size();
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ if (tails.get(mid) < num) {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+
+ // 如果找到末尾,说明num比所有元素都大,直接添加
+ if (left == tails.size()) {
+ tails.add(num);
+ } else {
+ // 否则替换找到的位置
+ tails.set(left, num);
+ }
+ }
+
+ return tails.size();
+}
+```
+
+**时间复杂度**:
+- 动态规划:$O(n²)$
+- 二分查找优化:$O(n log n)$
+
+**空间复杂度**:$O(n)$,需要额外的数组空间
+
+
+
+### [寻找两个正序数组的中位数『4』](https://leetcode.cn/problems/median-of-two-sorted-arrays/)
+
+> 给定两个大小分别为 `m` 和 `n` 的正序(从小到大)数组 `nums1` 和 `nums2`。请你找出并返回这两个正序数组的 **中位数** 。
+>
+> 算法的时间复杂度应该为 `O(log (m+n))` 。
+>
+> ```
+> 输入:nums1 = [1,3], nums2 = [2]
+> 输出:2.00000
+> 解释:合并数组 = [1,2,3] ,中位数 2
+> ```
+
+**思路**:
+
+中位数是指将一组数据从小到大排序后,位于中间位置的数值。如果数据集中的元素数量是奇数,中位数就是中间的那个元素;如果是偶数,则中位数是中间两个元素的平均值。
+
+这道题要求时间复杂度为 O(log(m+n)),提示我们使用二分查找。关键思路是:
+
+1. **问题转化**:寻找第 k 小的元素,其中 k = (m+n+1)/2(奇数情况)或需要找第k和第k+1小的元素(偶数情况)
+2. **二分搜索**:在较短的数组上进行二分,确保左半部分元素个数等于右半部分
+3. **分割线性质**:左半部分的最大值 ≤ 右半部分的最小值
+
+```java
+public double findMedianSortedArrays(int[] nums1, int[] nums2) {
+ // 确保 nums1 是较短的数组
+ if (nums1.length > nums2.length) {
+ return findMedianSortedArrays(nums2, nums1);
+ }
+
+ int m = nums1.length;
+ int n = nums2.length;
+ int left = 0, right = m;
+
+ while (left <= right) {
+ // nums1的分割点
+ int cut1 = (left + right) / 2;
+ // nums2的分割点
+ int cut2 = (m + n + 1) / 2 - cut1;
+
+ // 处理边界情况
+ int left1 = (cut1 == 0) ? Integer.MIN_VALUE : nums1[cut1 - 1];
+ int left2 = (cut2 == 0) ? Integer.MIN_VALUE : nums2[cut2 - 1];
+ int right1 = (cut1 == m) ? Integer.MAX_VALUE : nums1[cut1];
+ int right2 = (cut2 == n) ? Integer.MAX_VALUE : nums2[cut2];
+
+ // 找到正确的分割
+ if (left1 <= right2 && left2 <= right1) {
+ // 总长度为偶数
+ if ((m + n) % 2 == 0) {
+ return (Math.max(left1, left2) + Math.min(right1, right2)) / 2.0;
+ } else {
+ // 总长度为奇数
+ return Math.max(left1, left2);
+ }
+ }
+ // nums1分割点太靠右
+ else if (left1 > right2) {
+ right = cut1 - 1;
+ }
+ // nums1分割点太靠左
+ else {
+ left = cut1 + 1;
+ }
+ }
+
+ return 0.0;
+}
+```
+
+**算法步骤详解**:
+1. 确保在较短数组上进行二分搜索,减少搜索空间
+2. 计算两个数组的分割点,使得左半部分元素个数 = 右半部分元素个数(或多1个)
+3. 检查分割是否正确:左半部分最大值 ≤ 右半部分最小值
+4. 根据总长度奇偶性计算中位数
+
+**时间复杂度**:$O(log(min(m,n)))$,在较短数组上进行二分搜索
+**空间复杂度**:$O(1)$,只使用常量级别的额外空间
+
+
+
+## 四、常见题目补充
+
+
+### [x 的平方根『69』](https://leetcode.cn/problems/sqrtx/)
+
+> 给你一个非负整数 `x`,计算并返回 `x` 的平方根的整数部分。
+
+**思路**:在区间 `[1, x/2]` 上二分,寻找最大的 `mid` 使得 `mid*mid <= x`。注意用 `long` 防止乘法溢出。
+
+```java
+public int mySqrt(int x) {
+ if (x < 2) return x;
+ int left = 1, right = x / 2, ans = 0;
+ while (left <= right) {
+ int mid = left + (right - left) / 2;
+ if ((long) mid * mid <= x) {
+ ans = mid;
+ left = mid + 1;
+ } else {
+ right = mid - 1;
+ }
+ }
+ return ans;
+}
+```
+
+**时间复杂度**:$O(log x)$
+**空间复杂度**:$O(1)$
+
+
+### [第一个错误的版本『278』](https://leetcode.cn/problems/first-bad-version/)
+
+> 有 `n` 个版本,从 1 到 n,给你 `isBadVersion(version)` 接口。找出第一个错误版本。
+
+**思路**:典型“找左边界”。`isBad(mid)` 为真时收缩到左侧,并记录答案。
+
+```java
+// isBadVersion(version) 由系统提供
+public int firstBadVersion(int n) {
+ int left = 1, right = n, ans = n;
+ while (left <= right) {
+ int mid = left + (right - left) / 2;
+ if (isBadVersion(mid)) {
+ ans = mid;
+ right = mid - 1;
+ } else {
+ left = mid + 1;
+ }
+ }
+ return ans;
+}
+```
+
+**时间复杂度**:$O(log n) $
+**空间复杂度**:$O(1)$
+
+
+### [有序数组中的单一元素『540』](https://leetcode.cn/problems/single-element-in-a-sorted-array/)
+
+> 一个按升序排列的数组,除了某个元素只出现一次外,其余每个元素均出现两次。找出这个元素。
+
+**思路**:利用“成对对齐”的性质。让 `mid` 偶数化(`mid -= mid % 2`),若 `nums[mid] == nums[mid+1]`,唯一元素在右侧;否则在左侧(含 `mid`)。
+
+```java
+public int singleNonDuplicate(int[] nums) {
+ int left = 0, right = nums.length - 1;
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ if (mid % 2 == 1) mid--; // 偶数化
+ if (nums[mid] == nums[mid + 1]) {
+ left = mid + 2;
+ } else {
+ right = mid;
+ }
+ }
+ return nums[left];
+}
+```
+
+**时间复杂度**:$O(log n) $
+**空间复杂度**:$O(1)$
+
+
+### [爱吃香蕉的珂珂『875』](https://leetcode.cn/problems/koko-eating-bananas/)
+
+> 给定每堆香蕉数量 `piles` 和总小时数 `h`,最小化吃速 `k` 使得能在 `h` 小时内吃完。
+
+**思路**:对答案 `k` 二分。检验函数为以速率 `k` 需要的总小时数是否 `<= h`。
+
+```java
+public int minEatingSpeed(int[] piles, int h) {
+ int left = 1, right = 0;
+ for (int p : piles) right = Math.max(right, p);
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ long hours = 0;
+ for (int p : piles) {
+ hours += (p + mid - 1) / mid; // 向上取整
+ }
+ if (hours <= h) right = mid;
+ else left = mid + 1;
+ }
+ return left;
+}
+```
+
+**时间复杂度**:$O(n log max(piles))$
+**空间复杂度**:$O(1)$
+
+
+### [在 D 天内送达包裹的能力『1011』](https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/)
+
+> 给定包裹重量数组 `weights` 与天数 `days`,求最小运力使得能按顺序在 `days` 天内送达。
+
+**思路**:对答案(运力)二分。下界是最大单件重量,上界是总重量。检验函数为用给定运力需要的天数是否 `<= days`。
+
+```java
+public int shipWithinDays(int[] weights, int days) {
+ int left = 0, right = 0;
+ for (int w : weights) {
+ left = Math.max(left, w);
+ right += w;
+ }
+ while (left < right) {
+ int mid = left + (right - left) / 2;
+ int need = 1, cur = 0;
+ for (int w : weights) {
+ if (cur + w > mid) {
+ need++;
+ cur = 0;
+ }
+ cur += w;
+ }
+ if (need <= days) right = mid;
+ else left = mid + 1;
+ }
+ return left;
+}
+```
+
+**时间复杂度**:$O(n log(sum(weights)))$
+**空间复杂度**:$O(1)$
diff --git a/docs/data-structure-algorithms/algorithm/DFS-BFS.md b/docs/data-structure-algorithms/algorithm/DFS-BFS.md
new file mode 100644
index 0000000000..658a24b592
--- /dev/null
+++ b/docs/data-structure-algorithms/algorithm/DFS-BFS.md
@@ -0,0 +1,627 @@
+---
+title: DFS 与 BFS
+date: 2025-03-09
+tags:
+ - Algorithm
+categories: BFS DFS
+---
+
+
+
+> 在线性结构中,按照顺序一个一个地看到所有的元素,称为线性遍历。在非线性结构中,由于元素之间的组织方式变得复杂,就有了不同的遍历行为。其中最常见的遍历有:**深度优先遍历**(Depth-First-Search)和**广度优先遍历**(Breadth-First-Search)。它们的思想非常简单,但是在算法的世界里发挥着巨大的作用,也是面试高频考点。
+>
+
+「遍历」和「搜索」可以看作是两个等价概念,通过遍历 **所有** 的可能的情况达到搜索的目的。遍历是手段,搜索是目的。因此「优先遍历」也叫「优先搜索」。
+
+
+
+## 一、DFS 与 BFS的核心原理
+
+
+
+1. **DFS(深度优先搜索)**
+
+ - 核心思想:优先沿一条路径深入探索,直到无法继续再回溯到上一个节点继续搜索,类似“不撞南墙不回头”。
+ - **适用场景**:寻找所有可能路径、拓扑排序、连通性问题等。
+ - **实现方式**:递归(隐式栈)或显式栈。
+
+ ```Java
+ void dfs(TreeNode node) {
+ if (node == null) return;
+ // 处理当前节点
+ dfs(node.left); // 深入左子树
+ dfs(node.right); // 深入右子树
+ }
+ ```
+
+2. **BFS(广度优先搜索)**
+
+ - 核心思想:按层次逐层遍历,优先访问同一层的所有节点,常用于最短路径问题。
+ - **适用场景**:层序遍历、最短路径(无权图)、扩散类问题。
+ - **实现方式**:队列(FIFO)。
+
+ ```Java
+ void bfs(TreeNode root) {
+ Queue queue = new LinkedList<>();
+ queue.offer(root);
+ while (!queue.isEmpty()) {
+ int size = queue.size();
+ for (int i = 0; i < size; i++) {
+ TreeNode node = queue.poll();
+ // 处理当前节点
+ if (node.left != null) queue.offer(node.left);
+ if (node.right != null) queue.offer(node.right);
+ }
+ }
+ }
+ ```
+
+只是比较两段代码的话,最直观的感受就是:DFS 遍历的代码比 BFS 简洁太多了!这是因为递归的方式隐含地使用了系统的 栈,我们不需要自己维护一个数据结构。如果只是简单地将二叉树遍历一遍,那么 DFS 显然是更方便的选择。
+
+
+
+## 二、勇往直前的深度优先搜索
+
+### 2.1 深度优先遍历的形象描述
+
+「一条路走到底,不撞南墙不回头」是对「深度优先遍历」的最直观描述。
+
+说明:
+
+- 深度优先遍历只要前面有可以走的路,就会一直向前走,直到无路可走才会回头;
+- 「无路可走」有两种情况:① 遇到了墙;② 遇到了已经走过的路;
+- 在「无路可走」的时候,沿着原路返回,直到回到了还有未走过的路的路口,尝试继续走没有走过的路径;
+- 有一些路径没有走到,这是因为找到了出口,程序就停止了;
+- 「深度优先遍历」也叫「深度优先搜索」,遍历是行为的描述,搜索是目的(用途);
+- 遍历不是很深奥的事情,把 **所有** 可能的情况都看一遍,才能说「找到了目标元素」或者「没找到目标元素」。遍历也称为 **穷举**,穷举的思想在人类看来虽然很不起眼,但借助 **计算机强大的计算能力**,穷举可以帮助我们解决很多专业领域知识不能解决的问题。
+
+
+
+### 2.2 树的深度优先遍历
+
+我们以「二叉树」的深度优先遍历为例,介绍树的深度优先遍历。
+
+二叉树的深度优先遍历从「根结点」开始,依次 「递归地」 遍历「左子树」的所有结点和「右子树」的所有结点。
+
+
+
+> 事实上,「根结点 → 右子树 → 左子树」也是一种深度优先遍历的方式,为了符合人们「先左再右」的习惯。如果没有特别说明,树的深度优先遍历默认都按照 「根结点 → 左子树 → 右子树」 的方式进行。
+
+**二叉树深度优先遍历的递归终止条件**:遍历完一棵树的 **所有** 叶子结点,等价于遍历到 **空结点**。
+
+二叉树的深度优先遍历可以分为:前序遍历、中序遍历和后序遍历。
+
+
+
+- 前序遍历:根节点 → 左子树 → 右子树
+- 中序遍历: 左子树 → 根节点 → 右子树
+- 后序遍历:左子树 → 右子树 → 根节点
+
+> 友情提示:后序遍历是非常重要的遍历方式,解决很多树的问题都采用了后序遍历的思想,请大家务必重点理解「后序遍历」一层一层向上传递信息的遍历方式。并在做题的过程中仔细体会「后序遍历」思想的应用。
+
+**为什么前、中、后序遍历都是深度优先遍历**
+
+可以把树的深度优先遍历想象成一只蚂蚁,从根结点绕着树的外延走一圈。每一个结点的外延按照下图分成三个部分:前序遍历是第一部分,中序遍历是第二部分,后序遍历是第三部分。
+
+
+
+**重要性质**
+
+根据定义不难得到以下性质。
+
+ - 性质 1:二叉树的 前序遍历 序列,根结点一定是 最先 访问到的结点;
+ - 性质 2:二叉树的 后序遍历 序列,根结点一定是 最后 访问到的结点;
+ - 性质 3:根结点把二叉树的 中序遍历 序列划分成两个部分,第一部分的所有结点构成了根结点的左子树,第二部分的所有结点构成了根结点的右子树。
+
+> 根据这些性质,可以完成「力扣」第 105 题、第 106 题
+
+
+
+### 2.3 图的深度优先遍历
+
+深度优先遍历有「回头」的过程,在树中由于不存在「环」(回路),对于每一个结点来说,每一个结点只会被递归处理一次。而「图」中由于存在「环」(回路),就需要 记录已经被递归处理的结点(通常使用布尔数组或者哈希表),以免结点被重复遍历到。
+
+**说明**:深度优先遍历的结果通常与图的顶点如何存储有关,所以图的深度优先遍历的结果并不唯一。
+
+#### [课程表](https://leetcode.cn/problems/course-schedule/)
+
+> 你这个学期必须选修 `numCourses` 门课程,记为 `0` 到 `numCourses - 1` 。
+>
+> 在选修某些课程之前需要一些先修课程。 先修课程按数组 `prerequisites` 给出,其中 `prerequisites[i] = [ai, bi]` ,表示如果要学习课程 `ai` 则 **必须** 先学习课程 `bi` 。
+>
+> - 例如,先修课程对 `[0, 1]` 表示:想要学习课程 `0` ,你需要先完成课程 `1` 。
+>
+> 请你判断是否可能完成所有课程的学习?如果可以,返回 `true` ;否则,返回 `false` 。
+>
+> ```
+> 输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
+> 输出:false
+> 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
+> ```
+
+思路:对于课程表问题,实际上是在寻找一个有向无环图(DAG)的环,以确定是否存在一个有效的课程学习顺序。
+
+```java
+public class Solution {
+ private boolean hasCycle = false;
+
+ public boolean canFinish(int numCourses, int[][] prerequisites) {
+ // 构建图的邻接表表示
+ List> graph = new ArrayList<>();
+ for (int i = 0; i < numCourses; i++) {
+ graph.add(new ArrayList<>());
+ }
+ for (int[] prerequisite : prerequisites) {
+ int course = prerequisite[0];
+ int prerequisiteCourse = prerequisite[1];
+ graph.get(prerequisiteCourse).add(course);
+ }
+
+ // 初始化访问状态数组
+ boolean[] visited = new boolean[numCourses];
+ boolean[] recursionStack = new boolean[numCourses];
+
+ // 对每个节点进行DFS遍历
+ for (int i = 0; i < numCourses; i++) {
+ if (!visited[i]) {
+ dfs(graph, visited, recursionStack, i);
+ }
+ // 如果在DFS过程中检测到了环,则提前返回false
+ if (hasCycle) {
+ return false;
+ }
+ }
+
+ // 如果没有检测到环,则返回true
+ return true;
+ }
+
+ private void dfs(List> graph, boolean[] visited, boolean[] recursionStack, int node) {
+ // 将当前节点标记为已访问
+ visited[node] = true;
+ // 将当前节点加入递归栈
+ recursionStack[node] = true;
+
+ // 遍历当前节点的所有邻接节点
+ for (int neighbor : graph.get(node)) {
+ // 如果邻接节点未被访问,则递归访问
+ if (!visited[neighbor]) {
+ dfs(graph, visited, recursionStack, neighbor);
+ } else if (recursionStack[neighbor]) {
+ // 如果邻接节点已经在递归栈中,说明存在环
+ hasCycle = true;
+ }
+ }
+
+ // 将当前节点从递归栈中移除
+ recursionStack[node] = false;
+ }
+}
+```
+
+
+
+### 2.4 深度优先遍历的两种实现方式
+
+在深度优先遍历的过程中,需要将 当前遍历到的结点 的相邻结点 暂时保存 起来,以便在回退的时候可以继续访问它们。遍历到的结点的顺序呈现「后进先出」的特点,因此 深度优先遍历可以通过「栈」实现。
+
+再者,深度优先遍历有明显的递归结构。我们知道支持递归实现的数据结构也是栈。因此实现深度优先遍历有以下两种方式:
+
+- 编写递归方法;
+- 编写栈,通过迭代的方式实现。
+
+
+
+### 2.5 DFS 算法框架
+
+#### **1. 基础模板(以全排列为例)**
+
+```java
+// 全局变量:记录结果和路径
+List> result = new ArrayList<>();
+List path = new ArrayList<>();
+boolean[] visited; // 访问标记数组
+
+void dfs(int[] nums, int depth) {
+ // 终止条件:路径长度达到要求
+ if (depth == nums.length) {
+ result.add(new ArrayList<>(path));
+ return;
+ }
+
+ // 遍历选择列表
+ for (int i = 0; i < nums.length; i++) {
+ if (!visited[i]) {
+ // 做选择:标记已访问,加入路径
+ visited[i] = true;
+ path.add(nums[i]);
+ // 递归进入下一层
+ dfs(nums, depth + 1);
+ // 撤销选择:回溯
+ visited[i] = false;
+ path.remove(path.size() - 1);
+ }
+ }
+}
+```
+
+**关键点**:
+
+- **路径记录**:通过`path`列表保存当前路径。
+- **访问标记**:使用`visited`数组避免重复访问。
+- **递归与回溯**:递归调用后必须撤销选择以恢复状态
+
+#### **2. 二维矩阵遍历框架(如岛屿问题)**
+
+```java
+void dfs(int[][] grid, int i, int j) {
+ // 边界检查
+ if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length) return;
+ // 终止条件:遇到非陆地或已访问
+ if (grid[i][j] != '1') return;
+
+ // 标记为已访问(直接修改原矩阵)
+ grid[i][j] = '0';
+ // 四个方向递归
+ dfs(grid, i + 1, j); // 下
+ dfs(grid, i - 1, j); // 上
+ dfs(grid, i, j + 1); // 右
+ dfs(grid, i, j - 1); // 左
+}
+```
+
+**适用场景**:矩阵中的连通性问题(如岛屿数量、迷宫路径)
+
+
+
+### 2.6 练习
+
+> https://leetcode.cn/problem-list/depth-first-search/
+
+请大家通过这些问题体会 「**如何设计递归函数的返回值**」 帮助我们解决问题。并理解这些简单的问题其实都是「深度优先遍历」的思想中「后序遍历」思想的体现,真正程序在执行的时候,是通过「一层一层向上汇报」的方式,最终在根结点汇总整棵树遍历的结果。
+
+1. 完成「力扣」第 104 题:二叉树的最大深度(简单):设计递归函数的返回值;
+2. 完成「力扣」第 111 题:二叉树的最小深度(简单):设计递归函数的返回值;
+3. 完成「力扣」第 112 题:路径总和(简单):设计递归函数的返回值;
+4. 完成「力扣」第 226 题:翻转二叉树(简单):前中后序遍历、广度优先遍历均可,中序遍历有一个小小的坑;
+5. 完成「力扣」第 100 题:相同的树(简单):设计递归函数的返回值;
+6. 完成「力扣」第 101 题:对称二叉树(简单):设计递归函数的返回值;
+7. 完成「力扣」第 129 题:求根到叶子节点数字之和(中等):设计递归函数的返回值。
+8. 完成「力扣」第 236 题:二叉树的最近公共祖先(中等):使用后序遍历的典型问题。
+
+请大家完成下面这些树中的问题,加深对前序遍历序列、中序遍历序列、后序遍历序列的理解。
+
+9. 完成「力扣」第 105 题:从前序与中序遍历序列构造二叉树(中等);
+10. 完成「力扣」第 106 题:从中序与后序遍历序列构造二叉树(中等);
+11. 完成「力扣」第 1008 题:前序遍历构造二叉搜索树(中等);
+
+12. 完成「力扣」第 1028 题:从先序遍历还原二叉树(困难)。
+
+> 友情提示:需要用到后序遍历思想的一些经典问题,这些问题可能有一些难度,可以不用急于完成。先做后面的问题,见多了类似的问题以后,慢慢理解「后序遍历」一层一层向上汇报,在根结点汇总的遍历思想。
+
+
+
+### 2.7 总结
+
+- 遍历可以用于搜索,思想是穷举,遍历是实现搜索的手段;
+- 树的「前、中、后」序遍历都是深度优先遍历;
+- 树的后序遍历很重要;
+- 由于图中存在环(回路),图的深度优先遍历需要记录已经访问过的结点,以避免重复访问;
+- 遍历是一种简单、朴素但是很重要的算法思想,很多树和图的问题就是在树和图上执行一次遍历,在遍历的过程中记录有用的信息,得到需要结果,区别在于为了解决不同的问题,在遍历的时候传递了不同的 与问题相关 的数据。
+
+
+
+
+
+## 三、齐头并进的广度优先搜索
+
+> DFS(深度优先搜索)和 BFS(广度优先搜索)就像孪生兄弟,提到一个总是想起另一个。然而在实际使用中,我们用 DFS 的时候远远多于 BFS。那么,是不是 BFS 就没有什么用呢?
+>
+> 如果我们使用 DFS/BFS 只是为了遍历一棵树、一张图上的所有结点的话,那么 DFS 和 BFS 的能力没什么差别,我们当然更倾向于更方便写、空间复杂度更低的 DFS 遍历。不过,某些使用场景是 DFS 做不到的,只能使用 BFS 遍历。
+>
+
+
+
+「广度优先遍历」的思想在生活中随处可见:
+
+如果我们要找一个医生或者律师,我们会先在自己的一度人脉中遍历(查找),如果没有找到,继续在自己的二度人脉中遍历(查找),直到找到为止。
+
+### 3.1 广度优先遍历借助「队列」实现
+
+广度优先遍历呈现出「一层一层向外扩张」的特点,**先看到的结点先遍历,后看到的结点后遍历**,因此「广度优先遍历」可以借助「队列」实现。
+
+
+
+**说明**:遍历到一个结点时,如果这个结点有左(右)孩子结点,依次将它们加入队列。
+
+> 友情提示:广度优先遍历的写法相对固定,我们不建议大家背代码、记模板。在深刻理解广度优先遍历的应用场景(找无权图的最短路径),借助「队列」实现的基础上,多做练习,写对代码就是自然而然的事情了
+
+我们先介绍「树」的广度优先遍历,再介绍「图」的广度优先遍历。事实上,它们是非常像的。
+
+
+
+### 3.2 树的广度优先遍历
+
+二叉树的层序遍历
+
+> 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
+>
+
+思路分析:
+
+- 题目要求我们一层一层输出树的结点的值,很明显需要使用「广度优先遍历」实现;
+- 广度优先遍历借助「队列」实现;
+
+- 注意:
+ - 这样写 `for (int i = 0; i < queue.size(); i++) { `代码是不能通过测评的,这是因为 `queue.size()` 在循环中是变量。正确的做法是:每一次在队列中取出元素的个数须要先暂存起来;
+ - 子结点入队的时候,非空的判断很重要:在队列的队首元素出队的时候,一定要在左(右)子结点非空的时候才将左(右)子结点入队。
+- 树的广度优先遍历的写法模式相对固定:
+ - 使用队列;
+ - 在队列非空的时候,动态取出队首元素;
+ - 取出队首元素的时候,把队首元素相邻的结点(非空)加入队列。
+
+大家在做题的过程中需要多加练习,融汇贯通,不须要死记硬背。
+
+
+
+```java
+public List> levelOrder(TreeNode root) {
+ List> result = new ArrayList<>(); // 存储遍历结果的列表
+ if (root == null) {
+ return result; // 如果树为空,直接返回空列表
+ }
+
+ Queue queue = new LinkedList<>(); // 创建一个队列用于层序遍历
+ queue.offer(root); // 将根节点加入队列
+
+ while (!queue.isEmpty()) { // 当队列不为空时继续遍历
+ int levelSize = queue.size(); // 当前层的节点数
+ List currentLevel = new ArrayList<>(); // 存储当前层节点值的列表
+
+ for (int i = 0; i < levelSize; i++) {
+ TreeNode currentNode = queue.poll(); // 从队列中取出一个节点
+ currentLevel.add(currentNode.val); // 将节点值加入当前层列表
+
+ // 如果左子节点不为空,将其加入队列
+ if (currentNode.left != null) {
+ queue.offer(currentNode.left);
+ }
+ // 如果右子节点不为空,将其加入队列
+ if (currentNode.right != null) {
+ queue.offer(currentNode.right);
+ }
+ }
+
+ result.add(currentLevel); // 将当前层列表加入结果列表
+ }
+
+ return result; // 返回遍历结果
+}
+```
+
+
+
+### 3.3 BFS 算法框架
+
+**基础模板(队列+访问标记)**
+
+```java
+public int bfs(Node start, Node target) {
+ Queue queue = new LinkedList<>(); // 核心队列结构
+ Set visited = new HashSet<>(); // 防止重复访问
+ int step = 0; // 记录扩散步数
+
+ queue.offer(start);
+ visited.add(start);
+
+ while (!queue.isEmpty()) {
+ int levelSize = queue.size(); // 当前层节点数
+ for (int i = 0; i < levelSize; i++) { // 遍历当前层所有节点
+ Node cur = queue.poll();
+ // 终止条件(根据问题场景调整)
+ if (cur.equals(target)) return step;
+
+ // 扩散相邻节点(根据数据结构调整)
+ for (Node neighbor : getNeighbors(cur)) {
+ if (!visited.contains(neighbor)) {
+ queue.offer(neighbor);
+ visited.add(neighbor);
+ }
+ }
+ }
+ step++; // 步数递增
+ }
+ return -1; // 未找到目标
+}
+```
+
+**关键点** :
+
+- **队列控制层次**:通过 `levelSize` 逐层遍历,保证找到最短路径。
+- **访问标记**:避免重复访问(如矩阵问题可改为修改原数据)。
+- **扩散逻辑**:`getNeighbors()` 需根据具体数据结构实现(如二叉树、图、网格等)。
+
+
+
+### 3.4 使用广度优先遍历得到无权图的最短路径
+
+在 无权图 中,由于广度优先遍历本身的特点,假设源点为 source,只有在遍历到 所有 距离源点 source 的距离为 d 的所有结点以后,才能遍历到所有 距离源点 source 的距离为 d + 1 的所有结点。也可以使用「两点之间、线段最短」这条经验来辅助理解如下结论:从源点 source 到目标结点 target 走直线走过的路径一定是最短的。
+
+> 在一棵树中,一个结点到另一个结点的路径是唯一的,但在图中,结点之间可能有多条路径,其中哪条路最近呢?这一类问题称为最短路径问题。最短路径问题也是 BFS 的典型应用,而且其方法与层序遍历关系密切。
+>
+> 在二叉树中,BFS 可以实现一层一层的遍历。在图中同样如此。从源点出发,BFS 首先遍历到第一层结点,到源点的距离为 1,然后遍历到第二层结点,到源点的距离为 2…… 可以看到,用 BFS 的话,距离源点更近的点会先被遍历到,这样就能找到到某个点的最短路径了。
+>
+> 
+>
+> 小贴士:
+>
+> 很多同学一看到「最短路径」,就条件反射地想到「Dijkstra 算法」。为什么 BFS 遍历也能找到最短路径呢?
+>
+> 这是因为,Dijkstra 算法解决的是带权最短路径问题,而我们这里关注的是无权最短路径问题。也可以看成每条边的权重都是 1。这样的最短路径问题,用 BFS 求解就行了。
+>
+> 在面试中,你可能更希望写 BFS 而不是 Dijkstra。毕竟,敢保证自己能写对 Dijkstra 算法的人不多。
+>
+> 最短路径问题属于图算法。由于图的表示和描述比较复杂,本文用比较简单的网格结构代替。网格结构是一种特殊的图,它的表示和遍历都比较简单,适合作为练习题。在 LeetCode 中,最短路径问题也以网格结构为主。
+
+
+
+### 3.5 图论中的最短路径问题概述
+
+在图中,由于 图中存在环,和深度优先遍历一样,广度优先遍历也需要在遍历的时候记录已经遍历过的结点。特别注意:将结点添加到队列以后,一定要马上标记为「已经访问」,否则相同结点会重复入队,这一点在初学的时候很容易忽略。如果很难理解这样做的必要性,建议大家在代码中打印出队列中的元素进行调试:在图中,如果入队的时候不马上标记为「已访问」,相同的结点会重复入队,这是不对的。
+
+另外一点还需要强调,广度优先遍历用于求解「无权图」的最短路径,因此一定要认清「无权图」这个前提条件。如果是带权图,就需要使用相应的专门的算法去解决它们。事实上,这些「专门」的算法的思想也都基于「广度优先遍历」的思想,我们为大家例举如下:
+
+- 带权有向图、且所有权重都非负的单源最短路径问题:使用 Dijkstra 算法;
+- 带权有向图的单源最短路径问题:Bellman-Ford 算法;
+
+- 一个图的所有结点对的最短路径问题:Floy-Warshall 算法。
+
+这里列出的以三位计算机科学家的名字命名的算法,大家可以在《算法导论》这本经典著作的第 24 章、第 25 章找到相关知识的介绍。值得说明的是:应用任何一种算法,都需要认清使用算法的前提,不满足前提直接套用算法是不可取的。深刻理解应用算法的前提,也是学习算法的重要方法。例如我们在学习「二分查找」算法、「滑动窗口」算法的时候,就可以问自己,这个问题为什么可以使用「二分查找」,为什么可以使用「滑动窗口」。我们知道一个问题可以使用「优先队列」解决,是什么样的需求促使我们想到使用「优先队列」,而不是「红黑树(平衡二叉搜索树)」,想清楚使用算法(数据结构)的前提更重要。
+
+> [无向图中连通分量的数目『323』](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/)
+>
+> 你有一个包含 `n` 个节点的图。给定一个整数 `n` 和一个数组 `edges` ,其中 `edges[i] = [ai, bi]` 表示图中 `ai` 和 `bi` 之间有一条边。
+>
+> 返回 *图中已连接分量的数目* 。
+>
+> 
+>
+> ```
+> 输入: n = 5, edges = [[0, 1], [1, 2], [3, 4]]
+> 输出: 2
+> ```
+>
+> 思路分析:
+>
+> 首先需要对输入数组进行处理,由于 n 个结点的编号从 0 到 n - 1 ,因此可以使用「嵌套数组」表示邻接表,具体实现请见参考代码;
+> 然后遍历每一个顶点,对每一个顶点执行一次广度优先遍历,注意:在遍历的过程中使用 visited 布尔数组记录已经遍历过的结点。
+>
+> ```java
+> public int countComponents(int n, int[][] edges) {
+> // 第 1 步:构建图
+> List[] adj = new ArrayList[n];
+> for (int i = 0; i < n; i++) {
+> adj[i] = new ArrayList<>();
+> }
+> // 无向图,所以需要添加双向引用
+> for (int[] edge : edges) {
+> adj[edge[0]].add(edge[1]);
+> adj[edge[1]].add(edge[0]);
+> }
+>
+> // 第 2 步:开始广度优先遍历
+> int res = 0;
+> boolean[] visited = new boolean[n];
+> for (int i = 0; i < n; i++) {
+> if (!visited[i]) {
+> bfs(adj, i, visited);
+> res++;
+> }
+> }
+> return res;
+> }
+>
+> /**
+> * @param adj 邻接表
+> * @param u 从 u 这个顶点开始广度优先遍历
+> * @param visited 全局使用的 visited 布尔数组
+> */
+> private void bfs(List[] adj, int u, boolean[] visited) {
+> Queue queue = new LinkedList<>();
+> queue.offer(u);
+> visited[u] = true;
+>
+> while (!queue.isEmpty()) {
+> Integer front = queue.poll();
+> // 获得队首结点的所有后继结点
+> List successors = adj[front];
+> for (int successor : successors) {
+> if (!visited[successor]) {
+> queue.offer(successor);
+> // 特别注意:在加入队列以后一定要将该结点标记为访问,否则会出现结果重复入队的情况
+> visited[successor] = true;
+> }
+> }
+> }
+> }
+> ```
+>
+> 复杂度分析:
+>
+> - 时间复杂度:O(V + E)O(V+E),这里 EE 是边的条数,即数组 edges 的长度,初始化的时候遍历数组得到邻接表。这里 VV 为输入整数 n,遍历的过程是每一个结点执行一次深度优先遍历,时间复杂度为 O(V)O(V);
+> - 空间复杂度:O(V + E)O(V+E),综合考虑邻接表 O(V + E)O(V+E)、visited 数组 O(V)O(V)、队列的长度 O(V)O(V) 三者得到。
+> 说明:和深度优先遍历一样,图的广度优先遍历的结果并不唯一,与每个结点的相邻结点的访问顺序有关。
+
+### 3.6 练习
+
+> 友情提示:第 1 - 4 题是广度优先遍历的变形问题,写对这些问题有助于掌握广度优先遍历的代码编写逻辑和细节。
+
+1. 完成「力扣」第 107 题:二叉树的层次遍历 II(简单);
+2. 完成《剑指 Offer》第 32 - I 题:从上到下打印二叉树(中等);
+3. 完成《剑指 Offer》第 32 - III 题:从上到下打印二叉树 III(中等);
+4. 完成「力扣」第 103 题:二叉树的锯齿形层次遍历(中等);
+5. 完成「力扣」第 429 题:N 叉树的层序遍历(中等);
+6. 完成「力扣」第 993 题:二叉树的堂兄弟节点(中等);
+
+
+
+#### 二维矩阵遍历(岛屿问题)
+
+```java
+void bfs(char[][] grid, int x, int y) {
+ int[][] dirs = {{1,0}, {-1,0}, {0,1}, {0,-1}};
+ Queue queue = new LinkedList<>();
+ queue.offer(new int[]{x, y});
+ grid[x][y] = '0'; // 直接修改矩阵代替visited
+
+ while (!queue.isEmpty()) {
+ int[] pos = queue.poll();
+ for (int[] d : dirs) {
+ int nx = pos[0] + d[0], ny = pos[1] + d[1];
+ if (nx >=0 && ny >=0 && nx < grid.length && ny < grid[0].length
+ && grid[nx][ny] == '1') {
+ queue.offer(new int[]{nx, ny});
+ grid[nx][ny] = '0'; // 标记为已访问
+ }
+ }
+ }
+}
+```
+
+**优化点** :
+
+- 直接修改原矩阵代替`visited`集合,节省空间。
+- 四方向扩散的坐标计算。
+
+#### 图的邻接表遍历
+
+```java
+public void bfsGraph(Map> graph, int start) {
+ Queue queue = new LinkedList<>();
+ boolean[] visited = new boolean[graph.size()];
+ queue.offer(start);
+ visited[start] = true;
+
+ while (!queue.isEmpty()) {
+ int node = queue.poll();
+ System.out.print(node + " ");
+ for (int neighbor : graph.get(node)) {
+ if (!visited[neighbor]) {
+ queue.offer(neighbor);
+ visited[neighbor] = true;
+ }
+ }
+ }
+}
+```
+
+**数据结构** :
+
+- 使用`Map`或邻接表存储图结构。
+- 通过布尔数组标记访问状态。
+
+
+
+## Reference
+
+- https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/
diff --git a/docs/data-structure-algorithms/algorithm/Double-Pointer.md b/docs/data-structure-algorithms/algorithm/Double-Pointer.md
new file mode 100755
index 0000000000..69bcd7b253
--- /dev/null
+++ b/docs/data-structure-algorithms/algorithm/Double-Pointer.md
@@ -0,0 +1,1466 @@
+---
+title: 双指针
+date: 2023-05-17
+tags:
+ - pointers
+ - algorithms
+categories: algorithms
+---
+
+
+
+> 在数组中并没有真正意义上的指针,但我们可以把索引当做数组中的指针
+>
+> 归纳下双指针算法,其实总共就三类
+>
+> - 左右指针,数组和字符串问题
+> - 快慢指针,主要是成环问题
+> - 滑动窗口,针对子串问题
+
+
+
+## 一、左右指针
+
+
+
+左右指针在数组中其实就是两个索引值,两个指针相向而行或者相背而行
+
+Javaer 一般这么表示:
+
+```java
+int left = 0;
+int right = arr.length - 1;
+while(left < right)
+ ***
+```
+
+这两个指针 **相向交替移动**, 看着像二分查找是吧,二分也属于左右指针。
+
+
+
+### [反转字符串](https://leetcode.cn/problems/reverse-string/)
+
+> 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
+>
+> 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
+>
+> 你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
+>
+> ```
+> 输入:["h","e","l","l","o"]
+> 输出:["o","l","l","e","h"]
+> ```
+>
+
+思路:
+
+- 因为要反转,所以就不需要相向移动了,如果用双指针思路的话,其实就是遍历中交换左右指针的字符
+
+```java
+public void reverseString(char[] s) {
+ int left = 0;
+ int right = s.length - 1;
+ while (left < right){
+ char tmp = s[left];
+ s[left] = s[right];
+ s[right] = tmp;
+ left++;
+ right--;
+ }
+}
+```
+
+
+
+### [两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/)
+
+> 给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
+>
+> 以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
+>
+> 你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
+>
+> 你所设计的解决方案必须只使用常量级的额外空间。
+>
+> ```
+> 输入:numbers = [2,7,11,15], target = 9
+> 输出:[1,2]
+> 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
+> ```
+
+直接用左右指针套就可以
+
+```java
+public int[] twoSum(int[] nums, int target) {
+ int left = 0;
+ int rigth = nums.length - 1;
+ while (left < rigth) {
+ int tmp = nums[left] + nums[rigth];
+ if (target == tmp) {
+ //数组下标是从1开始的
+ return new int[]{left + 1, rigth + 1};
+ } else if (tmp > target) {
+ rigth--; //右移
+ } else {
+ left++; //左移
+ }
+ }
+ return new int[]{-1, -1};
+}
+```
+
+
+
+### [三数之和](https://leetcode.cn/problems/3sum/)
+
+> 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
+>
+> 注意:答案中不可以包含重复的三元组。
+>
+
+思路:**排序、双指针、去重**
+
+第一个想法是,这三个数,两个指针?
+
+- 对数组排序,固定一个数 $nums[i]$ ,然后遍历数组,并移动左右指针求和,判断是否有等于 0 的情况
+
+- 特例:
+ - 排序后第一个数就大于 0,不干了
+
+ - 有三个需要去重的地方
+ - `nums[i] == nums[i - 1]` 直接跳过本次遍历
+
+ > **避免重复三元组:**
+ >
+ > - 我们从第一个元素开始遍历数组,逐步往后移动。如果当前的 `nums[i]` 和前一个 `nums[i - 1]` 相同,说明我们已经处理过以 `nums[i - 1]` 为起点的组合(即已经找过包含 `nums[i - 1]` 的三元组),此时再处理 `nums[i]` 会导致生成重复的三元组,因此可以跳过。
+ > - 如果我们检查 `nums[i] == nums[i + 1]`,由于 `nums[i + 1]` 还没有被处理,这种方式无法避免重复,并且会产生错误的逻辑。
+
+ - `nums[left] == nums[left + 1]` 移动指针,即去重
+
+ - `nums[right] == nums[right - 1]` 移动指针
+
+ > **避免重复的配对:**
+ >
+ > 在每次固定一个 `nums[i]` 后,剩下的两数之和问题通常使用双指针法来解决。双指针的左右指针 `left` 和 `right` 分别从数组的两端向中间逼近,寻找合适的配对。
+ >
+ > 为了**避免相同的数字被重复使用**,导致重复的三元组,双指针法中也需要跳过相同的元素。
+ >
+ > - 左指针跳过重复元素:
+ > - 如果 `nums[left] == nums[left + 1]`,说明接下来的数字与之前处理过的数字相同。为了避免生成相同的三元组,我们将 `left` 向右移动跳过这个重复的数字。
+ > - 右指针跳过重复元素:
+ > - 同样地,`nums[right] == nums[right - 1]` 也会导致重复的配对,因此右指针也要向左移动,跳过这个重复数字。
+
+```java
+public List> threeSum(int[] nums) {
+ //存放结果list
+ List> result = new ArrayList<>();
+ int length = nums.length;
+ //特例判断
+ if (length < 3) {
+ return result;
+ }
+ Arrays.sort(nums);
+ for (int i = 0; i < length; i++) {
+ //排序后的第一个数字就大于0,就说明没有符合要求的结果
+ if (nums[i] > 0) break;
+
+ //去重, 不能是 nums[i] == nums[i +1 ],因为顺序遍历的逻辑使得前一个元素已经被处理过,而后续的元素还没有处理
+ if (i > 0 && nums[i] == nums[i - 1]) continue;
+ //左右指针
+ int l = i + 1;
+ int r = length - 1;
+ while (l < r) {
+ int sum = nums[i] + nums[l] + nums[r];
+ if (sum == 0) {
+ result.add(Arrays.asList(nums[i], nums[l], nums[r]));
+ //去重(相同数字的话就移动指针)
+ //在将左指针和右指针移动的时候,先对左右指针的值,进行判断,以防[0,0,0]这样的造成数组越界
+ //不要用成 if 判断,只跳过 1 条,还会有重复的,且需要再加上 l 0) r--;
+ }
+ }
+ return result;
+}
+```
+
+
+
+### 盛最多水的容器
+
+> 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
+>
+> ```
+> 输入:[1,8,6,2,5,4,8,3,7]
+> 输出:49
+> 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
+> ```
+>
+> 
+
+**思路**:
+
+- 求得是水量,水量 = 两个指针指向的数字中较小值 * 指针之间的距离(水桶原理,最短的板才不会漏水)
+- 为了求最大水量,我们需要存储所有条件的水量,进行比较才行
+- **双指针相向移动**,循环收窄,直到两个指针相遇
+- 往哪个方向移动,需要考虑清楚,如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会更小,所以我们移动**数字较小的那个指针**
+
+```java
+public int maxArea(int[] height){
+ int left = 0;
+ int right = height.length - 1;
+ //需要保存各个阶段的值
+ int result = 0;
+ while(left < right){
+ //水量 = 两个指针指向的数字中较小值∗指针之间的距离
+ int area = Math.min(height[left],height[right]) * (right - left);
+ result = Math.max(result,area);
+ //移动数字较小的指针
+ if(height[left] <= height[right]){
+ left ++;
+ }else{
+ right--;
+ }
+ }
+ return result;
+}
+```
+
+
+
+### [验证回文串](https://leetcode.cn/problems/valid-palindrome/)
+
+> 如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
+>
+> 字母和数字都属于字母数字字符。
+>
+> 给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。
+>
+> ```
+> 输入: "A man, a plan, a canal: Panama"
+> 输出: true
+> 解释:"amanaplanacanalpanama" 是回文串
+> ```
+
+思路:
+
+- 没看题解前,因为这个例子中有各种逗号、空格啥的,我第一想到的其实就是先遍历放在一个数组里,然后再去判断,看题解可以在原字符串完成,降低了空间复杂度
+- 首先需要知道三个 API
+ - `Character.isLetterOrDigit` 确定指定的字符是否为字母或数字
+ - `Character.toLowerCase` 将大写字符转换为小写
+ - `public char charAt(int index)` String 中的方法,用于返回指定索引处的字符
+- 双指针,每移动一步,判断这两个值是不是相同
+- 两个指针相遇,则是回文串
+
+```java
+public boolean isPalindrome(String s) {
+ // 转换为小写并去掉非字母和数字的字符
+ int left = 0, right = s.length() - 1;
+
+ while (left < right) {
+ // 忽略左边非字母和数字字符
+ while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
+ left++;
+ }
+ // 忽略右边非字母和数字字符
+ while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
+ right--;
+ }
+ // 比较两边字符
+ if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) {
+ return false;
+ }
+ left++;
+ right--;
+ }
+ return true;
+}
+```
+
+
+
+### 二分查找
+
+有重复数字的话,返回的其实就是最右匹配
+
+```java
+public static int search(int[] nums, int target) {
+ int left = 0;
+ int right = nums.length - 1;
+ while (left <= right) {
+ //不直接使用(right+left)/2 是考虑数据大的时候溢出
+ int mid = (right - left) / 2 + left;
+ int tmp = nums[mid];
+ if (tmp == target) {
+ return mid;
+ } else if (tmp > target) {
+ //右指针移到中间位置 - 1,也避免不存在的target造成死循环
+ right = mid - 1;
+ } else {
+ //
+ left = mid + 1;
+ }
+ }
+ return -1;
+}
+```
+
+
+
+## 二、快慢指针
+
+「快慢指针」,也称为「同步指针」,所谓快慢指针,就是两个指针同向而行,一快一慢。快慢指针处理的大都是链表问题。
+
+
+
+### [环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)
+
+> 给你一个链表的头节点 head ,判断链表中是否有环。
+>
+> 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
+>
+> 如果链表中存在环 ,则返回 true 。 否则,返回 false 。
+>
+> 
+
+思路:
+
+- 快慢指针,两个指针,一快一慢的话,慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
+
+```java
+public boolean hasCycle(ListNode head) {
+ if (head == null || head.next == null) {
+ return false;
+ }
+ // 龟兔起跑
+ ListNode fast = head;
+ ListNode slow = head;
+
+ while (fast != null && fast.next != null) {
+ // 龟走一步
+ slow = slow.next;
+ // 兔走两步
+ fast = fast.next.next;
+ if (slow == fast) {
+ return true;
+ }
+ }
+ return false;
+}
+```
+
+
+
+### [环形链表II](https://leetcode-cn.com/problems/linked-list-cycle-ii)
+
+> 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 `null`。
+>
+> ```
+> 输入:head = [3,2,0,-4], pos = 1
+> 输出:返回索引为 1 的链表节点
+> 解释:链表中有一个环,其尾部连接到第二个节点。
+> ```
+
+思路:
+
+- 最初,我就把有环理解错了,看题解觉得快慢指针相交的地方就是入环的节点
+
+- 假设环是这样的,slow 指针进入环后,又走了 b 的距离与 fast 相遇
+
+ 
+
+1. **检测是否有环**:通过快慢指针来判断链表中是否存在环。慢指针一次走一步,快指针一次走两步。如果链表中有环,两个指针最终会相遇;如果没有环,快指针会到达链表末尾。
+
+2. **找到环的起点**:
+- 当快慢指针相遇时,我们已经确认链表中存在环。
+
+- 从相遇点开始,慢指针保持不动,快指针回到链表头部,此时两个指针每次都走一步。两个指针会在环的起点再次相遇。
+
+```java
+public ListNode detectCycle(ListNode head) {
+ if (head == null || head.next == null) {
+ return null;
+ }
+
+ ListNode slow = head;
+ ListNode fast = head;
+
+ // 判断是否有环
+ while (fast != null && fast.next != null) {
+ slow = slow.next;
+ fast = fast.next.next;
+ // 快慢指针相遇,说明有环
+ if (slow == fast) {
+ break;
+ }
+ }
+
+ // 如果没有环
+ if (fast == null || fast.next == null) {
+ return null;
+ }
+
+ // 快指针回到起点,慢指针保持在相遇点
+ fast = head;
+ while (fast != slow) {
+ fast = fast.next;
+ slow = slow.next;
+ }
+
+ // 此时快慢指针相遇的地方就是环的起点
+ return slow;
+}
+```
+
+
+
+### [链表的中间结点](https://leetcode.cn/problems/middle-of-the-linked-list/)
+
+> 给定一个头结点为 `head` 的非空单链表,返回链表的中间结点。
+>
+> 如果有两个中间结点,则返回第二个中间结点。(给定链表的结点数介于 `1` 和 `100` 之间。)
+>
+> ```
+> 输入:[1,2,3,4,5]
+> 输出:此列表中的结点 3 (序列化形式:[3,4,5])
+> 返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
+> 注意,我们返回了一个 ListNode 类型的对象 ans,这样:
+> ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
+> ```
+
+思路:
+
+- 快慢指针遍历,当 `fast` 到达链表的末尾时,`slow` 必然位于中间
+
+```java
+public ListNode middleNode(ListNode head) {
+ ListNode fast = head;
+ ListNode slow = head;
+ while (fast != null && fast.next != null) {
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+ return slow;
+}
+```
+
+
+
+### [ 回文链表](https://leetcode.cn/problems/palindrome-linked-list/)
+
+> 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false
+>
+> ```
+> 输入:head = [1,2,2,1]
+> 输出:true
+> ```
+
+思路:
+
+- 双指针:将值复制到数组中后用双指针法
+- 或者使用快慢指针来确定中间结点,然后反转后半段链表,将前半部分链表和后半部分进行比较
+
+```java
+public boolean isPalindrome(ListNode head) {
+ List vals = new ArrayList();
+
+ // 将链表的值复制到数组中
+ ListNode currentNode = head;
+ while (currentNode != null) {
+ vals.add(currentNode.val);
+ currentNode = currentNode.next;
+ }
+
+ // 使用双指针判断是否回文
+ int front = 0;
+ int back = vals.size() - 1;
+ while (front < back) {
+ if (!vals.get(front).equals(vals.get(back))) {
+ return false;
+ }
+ front++;
+ back--;
+ }
+ return true;
+}
+```
+
+
+
+### 删除链表的倒数第 N 个结点
+
+> 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
+>
+> ```
+> 输入:head = [1,2,3,4,5], n = 2
+> 输出:[1,2,3,5]
+> ```
+
+思路:
+
+1. 计算链表长度:从头节点开始对链表进行一次遍历,得到链表的长度,随后我们再从头节点开始对链表进行一次遍历,当遍历到第 L−n+1 个节点时,它就是我们需要删除的节点(为了与题目中的 n 保持一致,节点的编号从 1 开始)
+2. 栈:根据栈「先进后出」的原则,我们弹出栈的第 n 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点
+3. 双指针:由于我们需要找到倒数第 n 个节点,因此我们可以使用两个指针 first 和 second 同时对链表进行遍历,并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。
+
+```java
+public ListNode removeNthFromEnd(ListNode head, int n) {
+ ListNode dummy = new ListNode(0, head);
+ ListNode first = head;
+ ListNode second = dummy;
+
+ //让 first 指针先移动 n 步
+ for (int i = 0; i < n; ++i) {
+ first = first.next;
+ }
+ while (first != null) {
+ first = first.next;
+ second = second.next;
+ }
+ second.next = second.next.next;
+ return dummy.next;
+}
+```
+
+
+
+### [删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/)
+
+> 给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
+>
+> 由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。
+>
+> 将最终结果插入 nums 的前 k 个位置后返回 k 。
+>
+> 不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
+>
+> ```
+> 输入:nums = [1,1,2]
+> 输出:2, nums = [1,2,_]
+> 解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
+> ```
+
+**思路**:
+
+- 数组有序,那相等的元素在数组中的下标一定是连续的
+- 使用快慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置
+- 第一个元素不需要删除,所有快慢指针都从下标 1 开始
+
+```java
+public static int removeDuplicates(int[] nums) {
+ if (nums == null) {
+ return 0;
+ }
+ int fast = 1;
+ int slow = 1;
+ while (fast < nums.length) {
+ //和前一个值比较
+ if (nums[fast] != nums[fast - 1]) {
+ //不一样的话,把快指针的值放在慢指针上,实现了去重,并往前移动慢指针
+ nums[slow] = nums[fast];
+ ++slow;
+ }
+ //相等的话,移动快指针就行
+ ++fast;
+ }
+ //慢指针的位置就是不重复的数量
+ return slow;
+}
+```
+
+
+
+### 最长连续递增序列
+
+> 给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
+>
+> 连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
+>
+> ```
+> 输入:nums = [1,3,5,4,7]
+> 输出:3
+> 解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
+> ```
+
+思路分析:
+
+- 这个题的思路和删除有序数组中的重复项,很像
+
+```java
+public int findLengthOfLCIS(int[] nums) {
+ int result = 0;
+ int fast = 0;
+ int slow = 0;
+ while (fast < nums.length) {
+ //前一个数大于后一个数的时候
+ if (fast > 0 || nums[fast - 1] > nums[fast]) {
+ slow = fast;
+ }
+ fast++;
+ result = Math.max(result, fast - slow);
+ }
+ return result;
+}
+```
+
+
+
+## 三、滑动窗口
+
+有一类数组上的问题,需要使用两个指针变量(我们称为左指针和右指针),同向、交替向右移动完成任务。这样的过程像极了一个窗口在平面上滑动的过程,因此我们将解决这一类问题的算法称为「滑动窗口」问题
+
+
+
+滑动窗口,就是两个指针齐头并进,好像一个窗口一样,不断往前滑。
+
+滑动窗口算法通过维护一个动态调整的窗口范围,高效解决子串、子数组、限流等场景问题。其核心逻辑可概括为以下步骤:
+
+1. **初始化窗口** 使用双指针 `left` 和 `right` 定义窗口边界,初始状态均指向起点。
+2. **扩展右边界** 移动 `right` 指针,将新元素加入窗口,并根据问题需求更新状态(如统计字符频率或请求计数)。
+3. **收缩左边界** 当窗口满足特定条件(如达到限流阈值或包含冗余元素),逐步移动 `left` 指针缩小窗口,直至不满足条件为止。
+4. **记录结果** 在窗口状态变化的每个阶段,捕获符合要求的解(如最长子串长度或限流通过状态)。
+
+
+
+子串问题,几乎都是滑动窗口。滑动窗口算法技巧的思路,就是维护一个窗口,不断滑动,然后更新答案,该算法的大致逻辑如下:
+
+```java
+int left = 0, right = 0;
+
+while (right < s.size()) {
+ // 增大窗口
+ window.add(s[right]);
+ right++;
+
+ while (window needs shrink) {
+ // 缩小窗口
+ window.remove(s[left]);
+ left++;
+ }
+}
+```
+
+> 以下是适用于字符串处理、限流等场景的通用框架:
+>
+> ```java
+> public class SlidingWindow {
+>
+> // 核心双指针定义
+> int left = 0, right = 0;
+> // 窗口状态容器(如哈希表、数组)
+> Map window = new HashMap<>();
+> // 结果记录
+> List result = new ArrayList<>();
+>
+> public void slidingWindow(String s, String target) {
+> // 初始化目标状态(如字符频率)
+> Map need = new HashMap<>();
+> for (char c : target.toCharArray()) {
+> need.put(c, need.getOrDefault(c, 0) + 1);
+> }
+>
+> while (right < s.length()) {
+> char c = s.charAt(right);
+> right++;
+> // 更新窗口状态
+> window.put(c, window.getOrDefault(c, 0) + 1);
+>
+> // 窗口收缩条件
+> while (window.get(c) > need.getOrDefault(c, 0)) {
+> char d = s.charAt(left);
+> left++;
+> // 更新窗口状态
+> window.put(d, window.get(d) - 1);
+> }
+>
+> // 记录结果(如最小覆盖子串长度)
+> if (right - left == target.length()) {
+> result.add(left);
+> }
+> }
+> }
+> }
+> ```
+>
+>
+
+### 3.1 同向交替移动的两个变量
+
+有一类数组上的问题,问我们固定长度的滑动窗口的性质,这类问题还算相对简单。
+
+#### [子数组最大平均数 I(643)](https://leetcode-cn.com/problems/maximum-average-subarray-i/)
+
+> 给定 `n` 个整数,找出平均数最大且长度为 `k` 的连续子数组,并输出该最大平均数。
+>
+> ```
+> 输入:[1,12,-5,-6,50,3], k = 4
+> 输出:12.75
+> 解释:最大平均数 (12-5-6+50)/4 = 51/4 = 12.75
+> ```
+
+**思路**:
+
+- 长度为固定的 K,想到用滑动窗口
+- 保存每个窗口的值,取这 k 个数的最大和就可以得出最大平均数
+- 怎么保存每个窗口的值,这一步
+
+```java
+public static double getMaxAverage(int[] nums, int k) {
+ int sum = 0;
+ //先求出前k个数的和
+ for (int i = 0; i < nums.length; i++) {
+ sum += nums[i];
+ }
+ //目前最大的数是前k个数
+ int result = sum;
+ //然后从第 K 个数开始移动,保存移动中的和值,返回最大的
+ for (int i = k; i < nums.length; i++) {
+ sum = sum - nums[i - k] + nums[i];
+ result = Math.max(result, sum);
+ }
+ //返回的是double
+ return 1.0 * result / k;
+}
+```
+
+
+
+### 3.2 不定长度的滑动窗口
+
+#### [无重复字符的最长子串_3](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)
+
+> 给定一个字符串 `s` ,请你找出其中不含有重复字符的 **最长子串** 的长度。
+>
+> ```
+> 输入: s = "abcabcbb"
+> 输出: 3
+> 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
+> ```
+
+思路:
+
+- 滑动窗口,其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列
+- 如何移动?我们只要把队列的左边的元素移出就行了,直到满足题目要求!
+- 一直维持这样的队列,找出队列出现最长的长度时候,求出解!
+
+```java
+int lengthOfLongestSubstring(String s) {
+ Map window = new HashMap<>();
+
+ int left = 0, right = 0;
+ int res = 0; // 记录结果
+ while (right < s.length()) {
+ char c = s.charAt(right);
+ right++;
+ // 进行窗口内数据的一系列更新
+ window.put(c, window.getOrDefault(c, 0) + 1);
+ // 判断左侧窗口是否要收缩,字符串重复时收缩,注意这里是 while 不是 if
+ while (window.get(c) > 1) {
+ char d = s.charAt(left);
+ left++;
+ // 进行窗口内数据的一系列更新
+ window.put(d, window.get(d) - 1);
+ }
+ // 在这里更新答案,我们窗口是左闭右开的,所以窗口实际包含的字符数是 right - left,无需 +1
+ res = Math.max(res, right - left);
+ }
+ return res;
+}
+```
+
+
+
+#### [最小覆盖子串(76)](https://leetcode-cn.com/problems/minimum-window-substring/)
+
+> 给你一个字符串 `s` 、一个字符串 `t` 。返回 `s` 中涵盖 `t` 所有字符的最小子串。如果 `s` 中不存在涵盖 `t` 所有字符的子串,则返回空字符串 `""` 。
+>
+> ```
+> 输入:s = "ADOBECODEBANC", t = "ABC"
+> 输出:"BANC"
+> ```
+
+思路:
+
+1. 我们在字符串 `S` 中使用双指针中的左右指针技巧,初始化 `left = right = 0`,把索引**左闭右开**区间 `[left, right)` 称为一个「窗口」。
+
+ > [!IMPORTANT]
+ >
+ > 为什么要「左闭右开」区间
+ >
+ > **理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的**。
+ >
+ > 因为这样初始化 `left = right = 0` 时区间 `[0, 0)` 中没有元素,但只要让 `right` 向右移动(扩大)一位,区间 `[0, 1)` 就包含一个元素 `0` 了。
+ >
+ > 如果你设置为两端都开的区间,那么让 `right` 向右移动一位后开区间 `(0, 1)` 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 `[0, 0]` 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。
+
+2. 我们先不断地增加 `right` 指针扩大窗口 `[left, right)`,直到窗口中的字符串符合要求(包含了 `T` 中的所有字符)。
+
+3. 此时,我们停止增加 `right`,转而不断增加 `left` 指针缩小窗口 `[left, right)`,直到窗口中的字符串不再符合要求(不包含 `T` 中的所有字符了)。同时,每次增加 `left`,我们都要更新一轮结果。
+
+4. 重复第 2 和第 3 步,直到 `right` 到达字符串 `S` 的尽头。
+
+```java
+public String minWindow(String s, String t) {
+ // 两个map,window 记录窗口中的字符频率,need 记录t中字符的频率
+ HashMap window = new HashMap<>();
+ HashMap need = new HashMap<>();
+
+ for (int i = 0; i < t.length(); i++) {
+ char c = t.charAt(i);
+ //算出每个字符的数量,有可能有重复的
+ need.put(c, need.getOrDefault(c, 0) + 1);
+ }
+
+ //左开右闭的区间,然后创建移动窗口
+ int left = 0, right = 0;
+ // 窗口中满足need条件的字符个数, valid == need.size 说明窗口满足条件
+ int valid = 0;
+ //记录最小覆盖子串的开始索引和长度
+ int start = 0, len = Integer.MAX_VALUE;
+ while (right < s.length()) {
+ // c 代表将移入窗口的字符
+ char c = s.charAt(right);
+ //扩大窗口
+ right++;
+
+ //先判断当前滑动窗口右端(right 指针处)的字符 c 是否是目标字符串 t 中的一个字符
+ if (need.containsKey(c)) {
+ window.put(c, window.getOrDefault(c, 0) + 1);
+ //检查当前字符 c 的频率在滑动窗口中是否达到了目标字符串 t 中所要求的频率
+ if (window.get(c).equals(need.get(c))) {
+ valid++;
+ }
+ }
+ //判断左窗口是否需要收缩
+ while (valid == need.size()) {
+ //如果当前滑动窗口的长度比已记录的最小长度 len 更短,则说明找到了一个更小的符合条件的覆盖子串
+ if (right - left < len) {
+ start = left;
+ len = right - left;
+ }
+ //d 是将移除窗口的字符
+ char d = s.charAt(left);
+ left++; //缩小窗口
+
+ //更新窗口,收缩,更新窗口中的字符频率并检查是否还满足覆盖条件
+ if (need.containsKey(d)) {
+ if (window.get(d).equals(need.get(d))) {
+ valid--;
+ window.put(d, window.get(d) - 1);
+ }
+ }
+ }
+ }
+ //返回最小覆盖子串
+ return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
+}
+```
+
+
+
+#### [字符串的排列(567)](https://leetcode.cn/problems/permutation-in-string/description/)
+
+> 给你两个字符串 `s1` 和 `s2` ,写一个函数来判断 `s2` 是否包含 `s1` 的排列。如果是,返回 `true` ;否则,返回 `false` 。
+>
+> 换句话说,`s1` 的排列之一是 `s2` 的 **子串** 。
+>
+> ```
+> 输入:s1 = "ab" s2 = "eidbaooo"
+> 输出:true
+> 解释:s2 包含 s1 的排列之一 ("ba").
+> ```
+
+思路:
+
+通过滑动窗口(Sliding Window)和字符频率统计来解决
+
+和上一题基本一致,只是 移动 `left` 缩小窗口的时机是窗口大小大于 `t.length()` 时,当发现 `valid == need.size()` 时,就说明窗口中就是一个合法的排列
+
+```java
+// 判断 s 中是否存在 t 的排列
+public boolean checkInclusion(String t, String s) {
+ Map need = new HashMap<>();
+ Map window = new HashMap<>();
+ for (char c : t.toCharArray()) {
+ need.put(c, need.getOrDefault(c, 0) + 1);
+ }
+
+ int left = 0, right = 0;
+ int valid = 0;
+ while (right < s.length()) {
+ char c = s.charAt(right);
+ right++;
+ // 进行窗口内数据的一系列更新
+ if (need.containsKey(c)) {
+ window.put(c, window.getOrDefault(c, 0) + 1);
+ if (window.get(c).intValue() == need.get(c).intValue())
+ valid++;
+ }
+
+ // 判断左侧窗口是否要收缩
+ while (right - left >= t.length()) {
+ // 在这里判断是否找到了合法的子串
+ if (valid == need.size())
+ return true;
+ char d = s.charAt(left);
+ left++;
+ // 进行窗口内数据的一系列更新
+ if (need.containsKey(d)) {
+ if (window.get(d).intValue() == need.get(d).intValue())
+ valid--;
+ window.put(d, window.get(d) - 1);
+ }
+ }
+ }
+ // 未找到符合条件的子串
+ return false;
+}
+```
+
+
+
+#### [替换后的最长重复字符(424)](https://leetcode-cn.com/problems/longest-repeating-character-replacement/)
+
+> 给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。
+>
+> 注意:字符串长度 和 k 不会超过 10^4
+>
+> ```
+> 输入:s = "ABAB", k = 2
+> 输出:4
+> 解释:用两个'A'替换为两个'B',反之亦然。
+> ```
+>
+> ```
+> 输入:s = "AABABBA", k = 1
+> 输出:4
+> 解释:将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。子串 "BBBB" 有最长重复字母, 答案为 4。
+> ```
+
+思路:
+
+```java
+public int characterReplacement(String s, int k) {
+ int len = s.length();
+ if (len < 2) {
+ return len;
+ }
+
+ char[] charArray = s.toCharArray();
+ int left = 0;
+ int right = 0;
+
+ int res = 0;
+ int maxCount = 0;
+ int[] freq = new int[26];
+ // [left, right) 内最多替换 k 个字符可以得到只有一种字符的子串
+ while (right < len){
+ freq[charArray[right] - 'A']++;
+ // 在这里维护 maxCount,因为每一次右边界读入一个字符,字符频数增加,才会使得 maxCount 增加
+ maxCount = Math.max(maxCount, freq[charArray[right] - 'A']);
+ right++;
+
+ if (right - left > maxCount + k){
+ // 说明此时 k 不够用
+ // 把其它不是最多出现的字符替换以后,都不能填满这个滑动的窗口,这个时候须要考虑左边界向右移动
+ // 移出滑动窗口的时候,频数数组须要相应地做减法
+ freq[charArray[left] - 'A']--;
+ left++;
+ }
+ res = Math.max(res, right - left);
+ }
+ return res;
+}
+```
+
+
+
+### 3.3 计数问题
+
+#### 至多包含两个不同字符的最长子串
+
+> 给定一个字符串 `s`,找出 **至多** 包含两个不同字符的最长子串 `t` ,并返回该子串的长度。
+>
+> ```
+> 输入: "eceba"
+> 输出: 3
+> 解释: t 是 "ece",长度为3。
+> ```
+
+
+
+```java
+public int lengthOfLongestSubstringTwoDistinct(String s) {
+ if (s == null || s.length() == 0) {
+ return 0;
+ }
+
+ // 滑动窗口的左指针
+ int left = 0;
+ // 记录滑动窗口内的字符及其出现的频率
+ Map map = new HashMap<>();
+ // 记录最长子串的长度
+ int maxLen = 0;
+
+ // 遍历整个字符串
+ for (int right = 0; right < s.length(); right++) {
+ // 右指针的字符
+ char c = s.charAt(right);
+ // 将字符 c 加入到窗口中,并更新其出现的次数
+ map.put(c, map.getOrDefault(c, 0) + 1);
+
+ // 当窗口内的不同字符数超过 2 时,开始缩小窗口
+ while (map.size() > 2) {
+ // 左指针的字符
+ char leftChar = s.charAt(left);
+ // 减少左指针字符的频率
+ map.put(leftChar, map.get(leftChar) - 1);
+ // 如果左指针字符的频率为 0,则从窗口中移除该字符
+ if (map.get(leftChar) == 0) {
+ map.remove(leftChar);
+ }
+ // 移动左指针,缩小窗口
+ left++;
+ }
+
+ // 更新最大长度
+ maxLen = Math.max(maxLen, right - left + 1);
+ }
+
+ return maxLen;
+}
+
+
+```
+
+
+
+#### 至多包含 K 个不同字符的最长子串_340
+
+> 给定一个字符串 `s`,找出 **至多** 包含 `k` 个不同字符的最长子串 `T`。
+>
+> ```
+> 输入: s = "eceba", k = 2
+> 输出: 3
+> 解释: 则 T 为 "ece",所以长度为 3。
+> ```
+
+```java
+public int lengthOfLongestSubstringKDistinct(String s, int k) {
+ if (s == null || s.length() == 0 || k == 0) {
+ return 0;
+ }
+
+ // 滑动窗口的左指针
+ int left = 0;
+ // 记录滑动窗口内的字符及其出现的频率
+ Map map = new HashMap<>();
+ // 记录最长子串的长度
+ int maxLen = 0;
+
+ // 遍历整个字符串
+ for (int right = 0; right < s.length(); right++) {
+ // 右指针的字符
+ char c = s.charAt(right);
+ // 将字符 c 加入到窗口中,并更新其出现的次数
+ map.put(c, map.getOrDefault(c, 0) + 1);
+
+ // 当窗口内的不同字符数超过 K 时,开始缩小窗口
+ while (map.size() > k) {
+ // 左指针的字符
+ char leftChar = s.charAt(left);
+ // 减少左指针字符的频率
+ map.put(leftChar, map.get(leftChar) - 1);
+ // 如果左指针字符的频率为 0,则从窗口中移除该字符
+ if (map.get(leftChar) == 0) {
+ map.remove(leftChar);
+ }
+ // 移动左指针,缩小窗口
+ left++;
+ }
+
+ // 更新最大长度
+ maxLen = Math.max(maxLen, right - left + 1);
+ }
+
+ return maxLen;
+}
+```
+
+
+
+#### [ 区间子数组个数_795](https://leetcode.cn/problems/number-of-subarrays-with-bounded-maximum/)
+
+> 给定一个元素都是正整数的数组`A` ,正整数 `L` 以及 `R` (`L <= R`)。
+>
+> 求连续、非空且其中最大元素满足大于等于`L` 小于等于`R`的子数组个数。
+>
+> ```
+> 例如 :
+> 输入:
+> A = [2, 1, 4, 3]
+> L = 2
+> R = 3
+> 输出: 3
+> 解释: 满足条件的子数组: [2], [2, 1], [3].
+> ```
+
+```java
+public int numSubarrayBoundedMax(int[] nums, int left, int right) {
+ int count = 0;
+ int start = -1; // 记录大于right的元素下标
+ int last_bounded = -1; // 记录符合[Left, Right]区间的元素下标
+
+ for (int i = 0; i < nums.length; i++) {
+ if (nums[i] > right) {
+ // 遇到大于right的元素,重置窗口起始点
+ start = i;
+ }
+ if (nums[i] >= left && nums[i] <= right) {
+ // 记录符合区间条件的元素下标
+ last_bounded = i;
+ }
+ // 计算当前有效子数组的数量
+ count += last_bounded - start;
+ }
+
+ return count;
+}
+```
+
+
+
+
+
+### 3.4 使用数据结构维护窗口性质
+
+有一类问题只是名字上叫「滑动窗口」,但解决这一类问题需要用到常见的数据结构。这一节给出的问题可以当做例题进行学习,一些比较复杂的问题是基于这些问题衍生出来的。
+
+#### 滑动窗口最大值
+
+#### 滑动窗口中位数
+
+
+
+
+
+## 四、其他双指针问题
+
+#### [最长回文子串_5](https://leetcode.cn/problems/longest-palindromic-substring/)
+
+> 给你一个字符串 `s`,找到 `s` 中最长的 回文子串。
+
+```java
+public static String longestPalindrome(String s){
+ //处理边界
+ if(s == null || s.length() < 2){
+ return s;
+ }
+
+ //初始化start和maxLength变量,用来记录最长回文子串的起始位置和长度
+ int start = 0, maxLength = 0;
+
+ //遍历每个字符
+ for (int i = 0; i < s.length(); i++) {
+ //以当前字符为中心的奇数长度回文串
+ int len1 = centerExpand(s, i, i);
+ //以当前字符和下一个字符之间的中心的偶数长度回文串
+ int len2 = centerExpand(s, i, i+1);
+
+ int len = Math.max(len1, len2);
+
+ //当前找到的回文串大于之前的记录,更新start和maxLength
+ if(len > maxLength){
+ // i 是当前扩展的中心位置, len 是找到的回文串的总长度,我们要用这两个值计算出起始位置 start
+ // (len - 1)/2 为什么呢,计算中心到回文串起始位置的距离, 为什么不用 len/2, 这里考虑的是奇数偶数的通用性,比如'abcba' 和 'abba' 或者 'cabbad',巧妙的同时处理两种,不需要分别考虑
+ start = i - (len - 1)/2;
+ maxLength = len;
+ }
+
+ }
+
+ return s.substring(start, start + maxLength);
+}
+
+private static int centerExpand(String s, int left, int right){
+ while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
+ left --;
+ right ++;
+ }
+ //这个的含义: 假设扩展过程中,left 和 right 已经超出了回文返回, 此时回文范围是 (left+1,right-1), 那么回文长度= (right-1)-(left+1)+1=right-left-1
+ return right - left - 1;
+}
+```
+
+
+
+#### [合并两个有序数组_88](https://leetcode-cn.com/problems/merge-sorted-array/)
+
+
+
+#### [下一个排列_31](https://leetcode.cn/problems/next-permutation/)
+
+> 整数数组的一个 **排列** 就是将其所有成员以序列或线性顺序排列。
+>
+> - 例如,`arr = [1,2,3]` ,以下这些都可以视作 `arr` 的排列:`[1,2,3]`、`[1,3,2]`、`[3,1,2]`、`[2,3,1]` 。
+>
+> 整数数组的 **下一个排列** 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 **下一个排列** 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
+>
+> - 例如,`arr = [1,2,3]` 的下一个排列是 `[1,3,2]` 。
+> - 类似地,`arr = [2,3,1]` 的下一个排列是 `[3,1,2]` 。
+> - 而 `arr = [3,2,1]` 的下一个排列是 `[1,2,3]` ,因为 `[3,2,1]` 不存在一个字典序更大的排列。
+>
+> 给你一个整数数组 `nums` ,找出 `nums` 的下一个排列。
+>
+> 必须**[ 原地 ](https://baike.baidu.com/item/原地算法)**修改,只允许使用额外常数空间。
+
+**Approach**:
+
+1. 我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
+2. 我们还希望下一个数 增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:
+ - 在 尽可能靠右的低位 进行交换,需要 从后向前 查找
+ - 将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
+ - 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序
+
+该算法可以分为三个步骤:
+
+1. **从右向左找到第一个升序对**(即`nums[i] < nums[i + 1]`),记为`i`。如果找不到,则说明数组已经是最大的排列,直接将数组反转为最小的排列。
+2. **从右向左找到第一个比`nums[i]`大的数**,记为`j`,然后交换`nums[i]`和`nums[j]`。
+3. **反转`i + 1`之后的数组**,使其变成最小的排列。
+
+```java
+public void nextPermutation(int[] nums){
+
+ //为什么从倒数第二个元素开始,因为我们第一步要从右往左找到第一个“升序对”,
+ int i = nums.length - 2;
+ //step 1: 找到第一个下降的元素
+ while (i >= 0 && nums[i] >= nums[i + 1]) {
+ i--;
+ }
+
+ //step2 : 如果找到了 i, 找到第一个比 nums[i] 大的元素 j
+ if (i > 0) {
+ int j = nums.length - 1;
+ while (j >= 0 && nums[j] <= nums[i]) {
+ j--;
+ }
+ //交换i 和 j 的位置
+ swap(nums, i, j);
+
+ }
+
+ // step3: 反转从 start 开始到末尾的部分(不需要重新排序,是因为这半部分再交换前就是降序的,我们第一步找的升序对)
+ reverse(nums, i+1);
+}
+
+private void swap(int[] nums, int i, int j){
+ int tmp = nums[i];
+ nums[i] = nums[j];
+ nums[j] = tmp;
+}
+
+private void reverse(int[] nums, int start){
+ int end = nums.length - 1;
+ while(start < end){
+ swap(nums, start, end);
+ start ++;
+ end --;
+ }
+}
+```
+
+
+
+#### [颜色分类_75](https://leetcode.cn/problems/sort-colors/)
+
+> 给定一个包含红色、白色和蓝色、共 `n` 个元素的数组 `nums` ,**[原地](https://baike.baidu.com/item/原地算法)** 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
+>
+> 我们使用整数 `0`、 `1` 和 `2` 分别表示红色、白色和蓝色。
+>
+> 必须在不使用库内置的 sort 函数的情况下解决这个问题。
+>
+> ```
+> 输入:nums = [2,0,2,1,1,0]
+> 输出:[0,0,1,1,2,2]
+> ```
+
+**Approach**:
+
+荷兰国旗问题
+
+我们可以使用三个指针:`low`、`mid` 和 `high`,分别用来处理 0、1 和 2 的排序问题。
+
+- `low` 表示红色 (0) 的边界,指向的元素是 1 的位置,即把所有 0 放在 `low` 的左边。
+- `mid` 表示当前处理的元素索引。
+- `high` 表示蓝色 (2) 的边界,指向的元素是 2 的位置,把所有 2 放在 `high` 的右边。
+
+**算法步骤:**
+
+1. 初始化:`low = 0`,`mid = 0`,`high = nums.length - 1`。
+2. 当 `mid <= high` 时,进行以下判断:
+ - 如果 `nums[mid] == 0`,将其与 `nums[low]` 交换,并将 `low` 和 `mid` 都加 1。
+ - 如果 `nums[mid] == 1`,只需将 `mid` 加 1,因为 1 已经在正确的位置。
+ - 如果 `nums[mid] == 2`,将其与 `nums[high]` 交换,并将 `high` 减 1,但 `mid` 不动,因为交换过来的数还未处理。
+
+```java
+public void sortColors(int[] nums) {
+ int low = 0, mid = 0, high = nums.length - 1;
+
+ while (mid <= high) {
+ if (nums[mid] == 0) {
+ // 交换 nums[mid] 和 nums[low]
+ swap(nums, low, mid);
+ low++;
+ mid++;
+ } else if (nums[mid] == 1) {
+ mid++;
+ } else if (nums[mid] == 2) {
+ // 交换 nums[mid] 和 nums[high]
+ swap(nums, mid, high);
+ high--;
+ }
+ }
+}
+
+private void swap(int[] nums, int i, int j) {
+ int temp = nums[i];
+ nums[i] = nums[j];
+ nums[j] = temp;
+}
+
+```
+
+- 时间复杂度:O(n),每个元素只遍历一次。
+
+- 空间复杂度:O(1),不需要额外的空间,只在原数组中进行操作。
+
+双指针方法的话,就是两次遍历。
+
+```java
+public void sortColors(int[] nums) {
+ int left = 0;
+ int right = nums.length - 1;
+
+ // 第一次遍历,把 0 移动到数组的左边
+ for (int i = 0; i <= right; i++) {
+ if (nums[i] == 0) {
+ swap(nums, i, left);
+ left++;
+ }
+ }
+
+ // 第二次遍历,把 2 移动到数组的右边
+ for (int i = nums.length - 1; i >= left; i--) {
+ if (nums[i] == 2) {
+ swap(nums, i, right);
+ right--;
+ }
+ }
+}
+
+private void swap(int[] nums, int i, int j) {
+ int temp = nums[i];
+ nums[i] = nums[j];
+ nums[j] = temp;
+}
+
+```
+
+
+
+#### [排序链表_148](https://leetcode.cn/problems/sort-list/description/)
+
+> 给你链表的头结点 `head` ,请将其按 **升序** 排列并返回 **排序后的链表** 。
+>
+> ```
+> 输入:head = [4,2,1,3]
+> 输出:[1,2,3,4]
+> ```
+
+**Approach**: 要将链表排序,并且时间复杂度要求为 O(nlogn)O(n \log n)O(nlogn),这提示我们需要使用 **归并排序**。归并排序的特点就是时间复杂度是 O(nlogn)O(n \log n)O(nlogn),并且它在链表上的表现很好,因为链表的分割和合并操作相对容易。
+
+具体实现步骤:
+
+1. **分割链表**:我们可以使用 **快慢指针** 来找到链表的中点,从而将链表一分为二。
+2. **递归排序**:分别对左右两部分链表进行排序。
+3. **合并有序链表**:最后将两个已经排序好的链表合并成一个有序链表。
+
+```java
+
+public ListNode sortList(ListNode head) {
+ // base case: if the list is empty or contains a single element, it's already sorted
+ if (head == null || head.next == null) {
+ return head;
+ }
+
+ // Step 1: split the linked list into two halves
+ ListNode mid = getMiddle(head);
+ // right 为链表右半部分的头结点
+ ListNode right = mid.next;
+ mid.next = null; //断开
+
+ // Step 2: recursively sort both halves
+ ListNode leftSorted = sortList(head);
+ ListNode rightSorted = sortList(right);
+
+ // Step 3: merge the sorted halves
+ return mergeTwoLists(leftSorted, rightSorted);
+}
+
+// Helper method to find the middle node of the linked list
+private ListNode getMiddle(ListNode head) {
+ ListNode slow = head;
+ ListNode fast = head;
+
+ while (fast != null && fast.next != null) {
+ slow = slow.next;
+ fast = fast.next.next;
+ }
+
+ return slow;
+}
+
+// Helper method to merge two sorted linked lists
+private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
+ ListNode dummy = new ListNode(-1);
+ ListNode current = dummy;
+
+ while (l1 != null && l2 != null) {
+ if (l1.val < l2.val) {
+ current.next = l1;
+ l1 = l1.next;
+ } else {
+ current.next = l2;
+ l2 = l2.next;
+ }
+ current = current.next;
+ }
+
+ // Append the remaining elements of either list
+ if (l1 != null) {
+ current.next = l1;
+ } else {
+ current.next = l2;
+ }
+
+ return dummy.next;
+}
+```
+
+
+
+### 总结
+
+区间不同的定义决定了不同的初始化逻辑、遍历过程中的逻辑。
+
+- 移除元素
+- 删除排序数组中的重复项 II
+- 移动零
+
+
+
diff --git a/docs/data-structure-algorithms/Dynamic-Programming.md b/docs/data-structure-algorithms/algorithm/Dynamic-Programming.md
similarity index 79%
rename from docs/data-structure-algorithms/Dynamic-Programming.md
rename to docs/data-structure-algorithms/algorithm/Dynamic-Programming.md
index 6bc1cbf484..9ea856aedc 100644
--- a/docs/data-structure-algorithms/Dynamic-Programming.md
+++ b/docs/data-structure-algorithms/algorithm/Dynamic-Programming.md
@@ -1,4 +1,10 @@
-# 动态规划——刷题有套路
+---
+title: 动态规划——刷题有套路
+date: 2024-03-09
+tags:
+ - Algorithm
+categories: Algorithm
+---
> 动态规划,简直就是刷题模板、套路届的典范
@@ -23,6 +29,20 @@
所有的算法都是在让计算机【如何聪明地穷举】而已,动态规划也是如此。
+> A : "1+1+1+1+1+1+1+1 =?等式的值是多少"
+>
+> B : 计算 "8"
+>
+> A : 在上面等式的左边写上 "1+" 呢? "此时等式的值为多少"
+>
+> B : 很快得出答案 "9"
+>
+> A : "你怎么这么快就知道答案了"
+>
+> B : "只要在8的基础上加1就行了"
+>
+> A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"
+
本文将会从以下角度来讲解动态规划:
- 什么是动态规划
@@ -41,19 +61,19 @@
- **多阶段决策**:比如说我们有一个复杂的问题要处理,我们可以按问题的时间或从空间关系分解成几个互相联系的阶段,使每个阶段的决策问题都是一个比较容易求解的“**子问题**”,这样依次做完每个阶段的最优决策后,他们就构成了整个问题的最优决策。简单地说,就是每做一次决策就可以得到解的一部分,当所有决策做完之后,完整的解就“浮出水面”了。有一种**大事化小,小事化了**的感觉。
-- **最优子结构**:在我们拆成一个个子问题的时候,每个子问题一定都有一个最优解,既然它分解的子问题是全局最优解,那么依赖于它们解的原问题自然也是全局最优解。比如说,你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。
+- **最优子结构**:在我们拆成一个个子问题的时候,每个子问题一定都有一个最优解,既然它分解的子问题是全局最优解,那么依赖于它们解的原问题自然也是全局最优解。比如说,你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。
- **自下而上**:或者叫自底向上,对应的肯定有**自上而下**(自顶向下)
- 啥叫**自顶向下**,比如我们求解递归问题,画递归树的时候,是从上向下延伸,都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 触底,然后逐层返回答案,这就叫「自顶向下」,比如我们用递归法计算斐波那契数列的时候
- 
+ 
- 反过来,自底向上,肯定就是从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算。
- 
+ 
@@ -65,7 +85,7 @@
> 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
>
-> 动态规划在查找有很多**重叠子问题**的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
+> 动态规划在查找有很多**重叠子问题**的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,而不会在解决同样问题时再花费时间。
>
> 动态规划只能应用于有**最优子结构**的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。
@@ -77,7 +97,7 @@
### 斐波那契数列
-PS:我们先从一个简单的斐波那契数列来进一步理解下重叠子问题与状态转移方程(斐波那契数列并不是严格意义上的动态规划,因为它没有求最值,所以也没设计到最优子结构的问题)
+PS:我们先从一个简单的斐波那契数列来进一步理解下重叠子问题与状态转移方程(斐波那契数列并不是严格意义上的动态规划,因为它没有求最值,所以也没涉及到最优子结构的问题)
**1、暴力递归**
@@ -92,8 +112,6 @@ int fib(int N) {
这个不用多说了,我们在 **自顶向下** 那部分画出的就是它的递归树,他有大量的重复计算问题,比如 `f(18)` 被计算了两次,而且你可以看到,以 `f(18)` 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 `f(18)` 这一个节点被重复计算,所以这个算法及其低效。
-
-
这就是动态规划问题的第一个性质:**重叠子问题**。下面,我们想办法解决这个问题。
**2、带备忘录的递归解法**
@@ -138,7 +156,7 @@ public int fib(int n) {
有了上一步「备忘录」的启发,**自顶向下**的递推,每次“缓存”之前的结果,那**自底向上**的推算不也可以吗?而且推算的时候,我们只需要存储之前的两个状态就行,还省了很多空间,我靠,真是个天才,这就是,**动态规划**的做法。
-
+
画个图就很好理解了,我们一层一层的往上计算,得到最后的结果。
@@ -167,8 +185,6 @@ public int fib(int n) {
## 四、什么样的题目适合用动态规划
-求最值的核心问题,无非就是穷举,就是把所有可能的结果都穷举出来,然后找到最值。但穷举从来不是一个好方法。
-
可以使用动态规划的问题一般都有一些特点可以遵循。如题目的问法一般是三种方式:
1. 求最大值/最小值(除了类似找出数组中最大值这种)
@@ -181,7 +197,7 @@ public int fib(int n) {
3. 求方案总数
- 路径规划问题
+ 硬币组合问题、路径规划问题
如果你碰到一个问题,是问你这三个问题之一的,那么有 90% 的概率是可以使用动态规划来求解。
@@ -207,12 +223,18 @@ public int fib(int n) {
我们知道了动态规划三要素:重叠子问题、最优子结构、状态转移方程。
-那要解决一个动态规划问题的大概步骤,就围绕这人这三要素展开:
+那要解决一个动态规划问题的大概步骤,就围绕这三要素展开:
1. **划分阶段:**分析题目可以用动态规划解决,那就先看这个问题如何划分成各个子问题
+
2. **状态定义**:也有叫选择状态的,其实就是定义子问题,我理解其实就是看求解的结果,我们一般用数组来存储子问题结果,所以状态我们一般定义为 $dp[i]$,表示规模为 i 的问题的解,$dp[i-1]$ 就是规模为 i-1 的子问题的解
+
3. **确定决策并写出状态转移方程:**听名字就觉得牛逼的一步,肯定也是最难的一步,其实就是我们从 f(1)、f(2)、f(3) ... f(n-1) 一步步递推出 f(n) 的表达式,也就是说,dp[n] 一定会和 dp[n-1], dp[n-2]....存在某种关系的,这一步就是找出数组元素的关系式,比如斐波那契数列的关系式 $dp[n] = dp[n-1] + dp[n-2]$
+
+ > 一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的
+
4. **找出初始值(包括边界条件):**既然状态转移方程式写好了,但是还需要一个**支点**来撬动它进行不断的计算下去,比如斐波那契数列中的 f(1)=1,f(2)=1,就是初始值
+
5. **优化**:思考有没有可以优化的点
@@ -223,7 +245,7 @@ public int fib(int n) {
按上面的套路走,最后的结果就可以套这个框架:
-```
+```java
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
@@ -277,8 +299,6 @@ for 状态1 in 状态1的所有取值:
>
> f(5) = f(4) + 1 .......
-。。。
-
这不是上一节的斐波那契数列吗?????
用 $f(x)$ 表示爬到第 x 级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以我们可以列出如下式子:
@@ -300,7 +320,7 @@ $f(x) = f(x - 1) + f(x - 2)$
public int climbStairs(int n) {
// 创建一个数组来保存历史数据
int[] dp = new int[n + 1];
- // 给出初始值, 爬楼梯的初始值应该是爬 1 级有1 种,2级的话有 2 种,这里2级也是个初始值
+ // 给出初始值, 爬楼梯的初始值
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i++) {
@@ -336,7 +356,7 @@ public int climbStairs(int n) {
想想这道题,用哪种遍历方式合适一些呢?
-用哪种遍历方式,可以逐个分析嘛。第一种遍历方式通常用于暴力解法,第二中后边我们也会用到(最长回文子串),第三种由于可以产生递推关系,动态规划问题用的挺多的。
+用哪种遍历方式,可以逐个分析嘛。第一种遍历方式通常用于暴力解法,第二种后边我们也会用到(最长回文子串),第三种由于可以产生递推关系,动态规划问题用的挺多的。
#### 分析题目
@@ -350,14 +370,14 @@ public int climbStairs(int n) {
2. **初始状态**:如果数组只有 1 个元素,那 dp 数组的第一个元素也就是数组的第一个元素本身,**`dp[0] = nums[0]`**;
-3. **状态转移方程**:因为我们的数组中可能有负数的情况,从头遍历的话,如有下一个元素是负数的话,和反而更小了,所以我们遍历以元素结尾的子序列,若前一个元素大于 0($nums[i-1] > 0$),则将它加到当前元素上 $nums[i] = nums[i-1]+nums[i]$。最终 $nums[i]$ 保存的是以原先数组中 $nums[i]$ 结尾的最大子序列和。最后整体遍历一边 $nums[i]$ 就能找到整个数组最大的子序列和啦。所以状态转移方程:
+3. **状态转移方程**:因为我们的数组中可能有负数的情况,从头遍历的话,如有下一个元素是负数的话,和反而更小了,所以我们遍历以元素结尾的子序列,若前一个元素大于 0($nums[i-1] > 0$),则将它加到当前元素上 $nums[i] = nums[i-1]+nums[i]$。最终 $nums[i]$ 保存的是以原先数组中 $nums[i]$ 结尾的最大子序列和。最后整体遍历一遍 $nums[i]$ 就能找到整个数组最大的子序列和啦。所以状态转移方程:
$dp[i]=\max \{nums[i],dp[i−1]+nums[i]\}$
4. **输出结果**:转移方程只是保存了当前元素的最大和,我们要求的是最终的那个最大值,所以需要从 dp[i] 中找到最大值返回
```java
-public int maxSubArray3(int[] nums) {
+public int maxSubArray(int[] nums) {
//特判
if (nums == null || nums.length == 0) {
return 0;
@@ -400,7 +420,7 @@ public int maxSubArray(int[] nums) {
> 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
>
-> 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
+> 给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
>
> ```
>输入:[1,2,3,1]
@@ -409,18 +429,11 @@ public int maxSubArray(int[] nums) {
> 偷窃到的最高金额 = 1 + 3 = 4 。
> ```
>
-> ```
->输入:[2,7,9,3,1]
-> 输出:12
->解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
-> 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
-> ```
>
->
-> 提示:
+>提示:
>
>0 <= nums.length <= 100
->0 <= nums[i] <= 400
+> 0 <= nums[i] <= 400
#### 分析题目
@@ -459,7 +472,7 @@ public int rob(int[] nums) {
#### 优化
-同样的优化套路,上述方法使用了数组存储结果。但是每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关,因此可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额。和斐波那契额数列优化同理。
+同样的优化套路,上述方法使用了数组存储结果。但是每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关,因此可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额。和斐波那契数列优化同理。
```java
public int rob(int[] nums) {
@@ -526,11 +539,11 @@ public int rob(int[] nums) {
dp[0…m-1] [0] = 1; // 机器人一直向下走,第 0 列统统为 1
```
-3. **状态转移方程**:要到达任一位置 (m,n) 的总路径条数,总是等于位置 (m-1,n) 的路径条数加上位置(m,n-1) 的路径条数。即 $dp[m][n] = dp[m-1][n] + dp[m][n-1]$
+3. **状态转移方程**:要到达任一位置 (m,n) 的总路径条数,总是等于位置 (m-1,n) 的路径条数加上位置(m,n-1) 的路径条数。即 $dp[m][n] = dp[m-1][n] + dp[m][n-1]$
4. **输出结果**:由于数组是从下标 0 开始算起的,所以 $dp[m - 1][n - 1]$ 才是我们要的结果
-
+
```java
public int uniquePaths(int m, int n) {
@@ -599,11 +612,9 @@ public int uniquePaths(int m, int n) {
以 coins = [1, 2, 5],amount = 11 为例。我们要求组成 11 的最少硬币数,可以考虑组合中的最后一个硬币分别是1,2,5 的情况,比如:
-最后一个硬币是 1 的话,最少硬币数应该为【组成 10 的最少硬币数】+ 1枚(1块硬币)
-
-最后一个硬币是 2 的话,最少硬币数应该为【组成 9 的最少硬币数】+ 1枚(2块硬币)
-
-最后一个硬币是 5 的话,最少硬币数应该为【组成 6 的最少硬币数】+ 1枚(5块硬币)
+- 最后一个硬币是 1 的话,最少硬币数应该为【组成 10 的最少硬币数】+ 1枚(1块硬币)
+- 最后一个硬币是 2 的话,最少硬币数应该为【组成 9 的最少硬币数】+ 1枚(2块硬币)
+- 最后一个硬币是 5 的话,最少硬币数应该为【组成 6 的最少硬币数】+ 1枚(5块硬币)
在这 3 种情况中硬币数最少的那个就是结果
@@ -617,39 +628,42 @@ public int uniquePaths(int m, int n) {
```java
for(int coin : coins){
- result = Math.min(result,1+dp[n-coin])
+ result = Math.min(result,1+dp[amout-coin])
}
```
4. **输出结果**: $dp[amout]$
```java
-public static int coinChange(int[] coins, int amount) {
+public int coinChange(int[] coins, int amount) {
//定义数组
int[] dp = new int[amount + 1];
int max = amount + 1;
- // 初始化每个值为 amount+1,这样当最终求得的 dp[amount] 为 amount+1 时,说明问题无解
+ // 初始化每个值为 amount+1,这样当最终求得的 dp[amount] 为 amount+1 时,说明问题无解, 或者初始化一个特殊值
Arrays.fill(dp, max);
//初始值
dp[0] = 0;
- // 外层 for 循环在遍历所有状态的所有取值
+ // 外层 for 循环在遍历所有可能得金额,(从1到amount)
//dp[i]上的值不断选择已含有硬币值当前位置的数组值 + 1,min保证每一次保存的是最小值
for (int i = 1; i < amount + 1; i++) {
- //内层 for 循环在求所有选择的最小值 状态转移方程
+ //内层循环所有硬币面额
for (int coin : coins) {
+ //如果i= coin) {
//分两种情况,使用硬币coin和不使用,取最小值
dp[i] = Math.min(dp[i - coin] + 1, dp[i]);
}
}
}
- return dp[amount] > amount ? -1 : dp[amount];
+ return dp[amount] == amount + 1 ? -1 : dp[amount];
}
```
> 为啥 `dp` 数组初始化为 `amount + 1` 呢,因为凑成 `amount` 金额的硬币数最多只可能等于 `amount`(全用 1 元面值的硬币),所以初始化为 `amount + 1` 就相当于初始化为正无穷,便于后续取最小值。为啥不直接初始化为 int 型的最大值 `Integer.MAX_VALUE` 呢?因为后面有 `dp[i - coin] + 1`,这就会导致整型溢出。
+>
+> 最终,dp[amout] 就是凑成总金额所需的最少硬币数,如果dp[amount] 仍是初始化的较大值,说明无法凑出,返回 -1。
@@ -679,7 +693,7 @@ public static int coinChange(int[] coins, int amount) {
我们需要找出给定数组中两个数字之间的最大差值(即,最大利润)。此外,第二个数字(卖出价格)必须大于第一个数字(买入价格)
```java
-public static int dp(int[] prices) {
+public int dp(int[] prices) {
int length = prices.length;
if (length == 0) {
return 0;
@@ -709,13 +723,13 @@ public static int dp(int[] prices) {
#### 分析题目
-回文的意思是正着念和倒着念一样,如:大波美人美波大
+回文的意思是正着念和倒着念一样,如:大波美人鱼人美波大
建立二维数组 `dp` ,找出所有的回文子串。
回文串两边加上两个相同字符,会形成一个新的回文串 。
-
+
@@ -723,8 +737,6 @@ public static int dp(int[] prices) {

-
-
首先,单个字符就形成一个回文串,所以,所有 `dp[i][i] = true` 。

@@ -785,158 +797,133 @@ public String longestPalindrome_1(String s) {
+### 8、数字三角形问题
+```
+7
+3 8
+8 1 0
+2 7 4 4
+4 5 2 6 5
+```
-## 总结
+从上到下选择一条路,使得经过的数字之和最大。
-
+路径上的每一步只能往左下或者右下走。
+#### 分析题目
+递归解法
-## Reference
+可以看出每走第n行第m列时有两种后续:向下或者向右下。由于最后一行可以确定,当做边界条件,所以我们自然而然想到递归求解
-- http://netedu.xauat.edu.cn/jpkc/netedu/jpkc/ycx/kcjy/kejian/pdf/05.pdf
+```java
+class Solution{
+
+ public int getMax(){
+ int MAX = 101;
+ int[][] D = new int[MAX][MAX]; //存储数字三角形
+ int n; //n表示层数
+ int i = 0; int j = 0;
+ int maxSum = getMaxSum(D,n,i,j);
+ return maxSum;
+ }
+
+ public int getMaxSum(int[][] D,int n,int i,int j){
+ if(i == n){
+ return D[i][j];
+ }
+ int x = getMaxSum(D,n,i+1,j);
+ int y = getMaxSum(D,n,i+1,j+1);
+ return Math.max(x,y)+D[i][j];
+ }
+}
+```
-- https://leetcode-cn.com/circle/article/lxC3ZB/
-- https://labuladong.gitbook.io/algo/dong-tai-gui-hua-xi-lie/dong-tai-gui-hua-xiang-jie-jin-jie
-- https://www.zhihu.com/question/39948290
-- https://zhuanlan.zhihu.com/p/26743197
-- https://writings.sh/post/algorithm-longest-palindromic-substrings
+## 总结
+
+## 番外篇
### 动态规划与其它算法的关系
-这一章我们将会介绍分治和贪心算法的核心思想,并与动态规划算法进行比较。
-
-#### 分治
-
-解决分治问题的时候,思路就是想办法把问题的规模减小,有时候减小一个,有时候减小一半,然后将每个小问题的解以及当前的情况组合起来得出最终的结果。例如归并排序和快速排序,归并排序将要排序的数组平均地分成两半,快速排序将数组随机地分成两半。然后不断地对它们递归地进行处理。
-
-这里存在有最优的子结构,即原数组的排序结果是在子数组排序的结果上组合出来的,但是不存在重复子问题,因为不断地对待排序的数组进行对半分的时候,两半边的数据并不重叠,分别解决左半边和右半边的两个子问题的时候,没有子问题重复出现,这是动态规划和分治的区别。
-
-#### 贪心
-
-关于最优子结构
-
-- 贪心:每一步的最优解一定包含上一步的最优解,上一步之前的最优解无需记录
-- 动态规划:全局最优解中一定包含某个局部最优解,但不一定包含上一步的局部最优解,因此需要记录之前的所有的局部最优解
-
-关于子问题最优解组合成原问题最优解的组合方式
-
-- 贪心:如果把所有的子问题看成一棵树的话,贪心从根出发,每次向下遍历最优子树即可,这里的最优是贪心意义上的最优。此时不需要知道一个节点的所有子树情况,于是构不成一棵完整的树
-- 动态规划:动态规划需要对每一个子树求最优解,直至下面的每一个叶子的值,最后得到一棵完整的树,在所有子树都得到最优解后,将他们组合成答案
-
-结果正确性
-
-- 贪心不能保证求得的最后解是最佳的,复杂度低
-- 动态规划本质是穷举法,可以保证结果是最佳的,复杂度高
-
-
-
-
-
-#### 线性动态规划
+#### **1. 贪心算法(Greedy Algorithm)**
-单串
-单串 dp[i] 线性动态规划最简单的一类问题,输入是一个串,状态一般定义为 dp[i] := 考虑[0..i]上,原问题的解,其中 i 位置的处理,根据不同的问题,主要有两种方式:
+**核心思想**:每一步都做出当前状态下的**局部最优选择**(即 “贪心选择”),不考虑该选择对后续步骤的影响,最终通过局部最优的累积试图得到全局最优解。本质是 “短视的”:只看眼前,不回头。
-第一种是 i 位置必须取,此时状态可以进一步描述为 dp[i] := 考虑[0..i]上,且取 i,原问题的解;
-第二种是 i 位置可以取可以不取
-大部分的问题,对 i 位置的处理是第一种方式,例如力扣:
+**特点**:
-70 爬楼梯问题
-801 使序列递增的最小交换次数
-790 多米诺和托米诺平铺
-746 使用最小花费爬楼梯
-线性动态规划中单串 dp[i] 的问题,状态的推导方向以及推导公式如下
+- 必须满足**贪心选择性质**:局部最优选择能导致全局最优解(否则贪心会失效)。
+- 子问题无依赖:每一步的选择不影响后续子问题的求解(子问题独立)。
+**适用场景**:
+- 最优子结构明确(如霍夫曼编码)
+- 贪心选择性质成立(如活动选择问题)
+- 无需全局最优验证(如零钱兑换特殊场景)
-1. 依赖比 i 小的 O(1) 个子问题
-dp[n] 只与常数个小规模子问题有关,状态的推导过程 dp[i] = f(dp[i - 1], dp[i - 2], ...)。时间复杂度 O(n)O(n),空间复杂度 O(n)O(n) 可以优化为 O(1)O(1),例如上面提到的 70, 801, 790, 746 都属于这类。
+#### 2. 分治法(Divide and Conquer)
-如图所示,虽然紫色部分的 dp[i-1], dp[i-2], ..., dp[0] 均已经计算过,但计算橙色的当前状态时,仅用到 dp[i-1],这属于比 i 小的 O(1)O(1) 个子问题。
+**核心思想**: 将原问题**分解为若干个规模更小、结构相同的子问题**,递归求解子问题后,**合并子问题的解**得到原问题的解。
+本质是 “分而治之”:拆分独立子问题,逐个击破再整合。
-例如,当 f(dp[i-1], ...) = dp[i-1] + nums[i] 时,当前状态 dp[i] 仅与 dp[i-1] 有关。这个例子是一种数据结构前缀和的状态计算方式,关于前缀和的详细内容请参考下一章。
+**特点**:
-2. 依赖比 i 小的 O(n) 个子问题
-dp[n] 与此前的更小规模的所有子问题 dp[n - 1], dp[n - 2], ..., dp[1] 都可能有关系。
+- **递归结构**:子问题相互独立(无重叠)
+- **合并成本高**:结果合并是关键步骤
+- **并行潜力**:子问题可并发求解
-状态推导过程如下:
+**适用场景**:
+- 问题可自然拆分(如排序、树操作)
+- 子问题规模相似(如二分搜索)
+- 合并操作复杂度可控(如归并排序)
-dp[i] = f(dp[i - 1], dp[i - 2], ..., dp[0])
-依然如图所示,计算橙色的当前状态 dp[i] 时,紫色的此前计算过的状态 dp[i-1], ..., dp[0] 均有可能用到,在计算 dp[i] 时需要将它们遍历一遍完成计算。
+#### **3. 动态规划(Dynamic Programming)**
-其中 f 常见的有 max/min,可能还会对 i-1,i-2,...,0 有一些筛选条件,但推导 dp[n] 时依然是 O(n)O(n) 级的子问题数量。
+**核心思想**: 对于具有**重叠子问题**和**最优子结构**的问题,将其分解为子问题后,**存储子问题的解(记忆化)** 以避免重复计算,通过子问题的解推导出原问题的解。本质是 “精打细算的”:记录历史,避免重复劳动。
-例如:
+**特点**:
-139 单词拆分
-818 赛车
-以 min 函数为例,这种形式的问题的代码常见写法如下
+- **最优子结构**:全局最优包含局部最优
+- **重叠子问题**:子问题反复出现(用表存储)
+- **状态转移方程**:定义问题间递推关系
+**适用场景**:
-for i = 1, ..., n
- for j = 1, ..., i-1
- dp[i] = min(dp[i], f(dp[j])
-时间复杂度 O(n^{2})O(n
-2
- ),空间复杂度 O(n)O(n)
+- 子问题重叠(如斐波那契数列)
+- 多阶段决策最优解(如背包问题)
+- 需要回溯最优路径(如最长公共子序列)
+| **特性** | 贪心算法 | 分治法 | 动态规划 |
+| -------------- | ---------------------- | -------------------- | ------------------ |
+| **决策依据** | 当前局部最优 | 子问题独立解 | 历史子问题最优解 |
+| **子问题关系** | 无重复计算 | 完全独立 | 高度重叠 |
+| **解空间处理** | 永不回溯 | 显式分割 | 存储+重用 |
+| **时间复杂度** | 通常最低 | 中等(依赖合并成本) | 通常较高 |
+| **结果保证** | **不保证全局最优** | 保证正确解 | **保证全局最优** |
+| **经典案例** | Dijkstra算法、活动选择 | 归并排序、快速排序 | 背包问题、最短路径 |
-**单串 dp[i] 经典问题**
-#### 1. 依赖比 i 小的 O(1) 个子问题
-
-- [53. 最大子数组和](https://leetcode-cn.com/problems/maximum-subarray/)
-
- 状态的推导是按照 i 从 0 到 n - 1 按顺序推的,推到 dp[i],时,dp[i - 1], ..., dp[0] 已经计算完。因为子数组是连续的,所以子问题 dp[i] 其实只与子问题 dp[i - 1] 有关。如果 [0..i-1] 上以 nums[i-1] 结尾的最大子数组和(缓存在 dp[i-1] )为非负数,则以 nums[i] 结尾的最大子数组和就在 dp[i-1] 的基础上加上 nums[i] 就是 dp[i] 的结果否则以 i 结尾的子数组就不要 i-1 及之前的数,因为选了的话子数组的和只会更小。
-
- 按照以上的分析,状态的转移可以写出来,如下
-
-
- dp[i] = nums[i] + max(dp[i - 1], 0)
- 这个是单串 dp[i] 的问题,状态的推导方向,以及推导公式如下
-
-
-
-
-#### 2. 依赖比 i 小的 O(n) 个子问题
-
-- [300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)
-
- 输入是一个单串,首先思考单串问题中设计状态 dp[i] 时拆分子问题的方式:枚举子串或子序列的结尾元素来拆分子问题,设计状态 dp[i] := 在子数组 [0..i] 上,且选了 nums[i] 时,的最长上升子序列。
-
- 因为子序列需要上升,因此以 i 结尾的子序列中,nums[i] 之前的数字一定要比 nums[i] 小才行,因此目标就是先找到以此前比 nums[i] 小的各个元素,然后每个所选元素对应一个以它们结尾的最长子序列,从这些子序列中选择最长的,其长度加 1 就是当前的问题的结果。如果此前没有比 nums[i] 小的数字,则当前问题的结果就是 1 。
+## Reference
- 按照以上的分析,状态的转移方程可以写出来,如下
+- http://netedu.xauat.edu.cn/jpkc/netedu/jpkc/ycx/kcjy/kejian/pdf/05.pdf
- dp[i] = max_{j}(dp[j]) + 1
- dp[i]=max
- j
+- https://leetcode-cn.com/circle/article/lxC3ZB/
- (dp[j])+1
+- https://labuladong.gitbook.io/algo/dong-tai-gui-hua-xi-lie/dong-tai-gui-hua-xiang-jie-jin-jie
- 其中 0 \leq j < i, nums[j] < nums[i]0≤j 贪心算法(greedy algorithm)是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期获得全局最优解。贪心算法简洁且高效,在许多实际问题中有着广泛的应用。
+>
+> 贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理不同。
+>
+> - 动态规划会根据之前阶段的所有决策来考虑当前决策,并使用过去子问题的解来构建当前子问题的解。
+> - 贪心算法不会考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决。
+
+
+
+### 贪心算法的应用场景
+
+解决一个问题需要多个步骤,每一个步骤有多种选择。可以使用贪心算法解决的问题,每一步只需要解决一个子问题,只做出一种选择,就可以完成任务。
+
+### 贪心算法特性
+
+较于动态规划,贪心算法的使用条件更加苛刻,其主要关注问题的两个性质。
+
+- **贪心选择性质**:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证得到最优解。
+- **最优子结构**:原问题的最优解包含子问题的最优解。
+
+#### 贪心算法与回溯算法、动态规划的区别
+
+「解决一个问题需要多个步骤,每一个步骤有多种选择」这样的描述我们在「回溯算法」「动态规划」算法中都会看到。它们的区别如下:
+
+- 「回溯算法」需要记录每一个步骤、每一个选择,用于回答所有具体解的问题;
+- 「动态规划」需要记录的是每一个步骤、所有选择的汇总值(最大、最小或者计数);
+- 「贪心算法」由于适用的问题,每一个步骤只有一种选择,一般而言只需要记录与当前步骤相关的变量的值。
+
+对于不同的求解目标和不同的问题场景,需要使用不同的算法。
+
+#### 可以使用「贪心算法」的问题需要满足的条件
+
+- 最优子结构:规模较大的问题的解由规模较小的子问题的解组成,区别于「动态规划」,可以使用「贪心算法」的问题「规模较大的问题的解」只由其中一个「规模较小的子问题的解」决定;
+- 无后效性:后面阶段的求解不会修改前面阶段已经计算好的结果;
+
+- 贪心选择性质:从局部最优解可以得到全局最优解。
+
+对「最优子结构」和「无后效性」的理解同「动态规划」,「贪心选择性质」是「贪心算法」最需要关注的内容。
+
+
+
+### 贪心算法解题步骤
+
+贪心问题的解决流程大体可分为以下三步。
+
+1. **问题分析**:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。这一步在回溯和动态规划中都有涉及。
+2. **确定贪心策略**:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终解决整个问题。
+3. **正确性证明**:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要用到数学证明,例如归纳法或反证法等。
+
+确定贪心策略是求解问题的核心步骤,但实施起来可能并不容易,主要有以下原因。
+
+- **不同问题的贪心策略的差异较大**。对于许多问题来说,贪心策略比较浅显,我们通过一些大概的思考与尝试就能得出。而对于一些复杂问题,贪心策略可能非常隐蔽,这种情况就非常考验个人的解题经验与算法能力了。
+- **某些贪心策略具有较强的迷惑性**。当我们满怀信心设计好贪心策略,写出解题代码并提交运行,很可能发现部分测试样例无法通过。这是因为设计的贪心策略只是“部分正确”的,上文介绍的零钱兑换就是一个典型案例。
+
+
+
+#### 「力扣」第 455 题:分发饼干(简单)
+
+> 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
+>
+> 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
+>
+> 示例 1:
+>
+> 输入: g = [1,2,3], s = [1,1]
+> 输出: 1
+> 解释:
+> 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
+> 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
+> 所以你应该输出1。
+
+```java
+class Solution {
+ public int findContentChildren(int[] g, int[] s) {
+ Arrays.sort(g);
+ Arrays.sort(s);
+ int numOfChildren = g.length, numOfCookies = s.length;
+ int count = 0;
+ for (int i = 0, j = 0; i < numOfChildren && j < numOfCookies; i++, j++) {
+ while (j < numOfCookies && g[i] > s[j]) {
+ j++;
+ }
+ if (j < numOfCookies) {
+ count++;
+ }
+ }
+ return count;
+ }
+}
+```
+
+「贪心算法」总是做出在当前看来最好的选择就可以完成任务;
+解决「贪心算法」几乎没有套路,到底如何贪心,贪什么与我们要解决的问题密切相关。因此刚开始学习「贪心算法」的时候需要学习和模仿,然后才有直觉,猜测一个问题可能需要使用「贪心算法」,进而尝试证明,学会证明。
+
+
+
+## 贪心算法典型例题
+
+贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下列举了一些典型的贪心算法问题。
+
+- **硬币找零问题**:在某些硬币组合下,贪心算法总是可以得到最优解。
+- **区间调度问题**:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
+- **分数背包问题**:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解。
+- **股票买卖问题**:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
+- **霍夫曼编码**:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最低的两个节点合并,最后得到的霍夫曼树的带权路径长度(编码长度)最小。
+- **Dijkstra 算法**:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。
+
diff --git a/docs/data-structure-algorithms/algorithm/Recursion.md b/docs/data-structure-algorithms/algorithm/Recursion.md
new file mode 100644
index 0000000000..f8a1992021
--- /dev/null
+++ b/docs/data-structure-algorithms/algorithm/Recursion.md
@@ -0,0 +1,389 @@
+---
+title: 递归算法
+date: 2024-05-09
+tags:
+ - Recursion
+categories: Algorithm
+---
+
+
+
+
+
+### 什么是递归
+
+递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。
+
+**简单地说,就是如果在函数中存在着调用函数本身的情况,这种现象就叫递归。**
+
+你以前肯定写过递归,只是有可能某些不知道这就是递归罢了。
+
+以阶乘函数为例,在 factorial 函数中存在着 `factorial(n - 1)` 的调用,所以此函数是递归函数
+
+```java
+public long factorial(int n) {
+ if (n < =1) {
+ return 1;
+ }
+ return n * factorial(n - 1)
+}
+```
+
+进一步剖析「递归」,先有「递」再有「归」,「递」的意思是将问题拆解成子问题来解决, 子问题再拆解成子子问题,...,直到被拆解的子问题无需再拆分成更细的子问题(即可以求解),「归」是说最小的子问题解决了,那么它的上一层子问题也就解决了,上一层的子问题解决了,上上层子问题自然也就解决了,....,直到最开始的问题解决,文字说可能有点抽象,那我们就以阶层 f(6) 为例来看下它的「递」和「归」。
+
+
+
+求解问题 `f(6)`,由于 `f(6) = n * f(5)`, 所以 `f(6)` 需要拆解成 `f(5)` 子问题进行求解,同理 `f(5) = n * f(4) `,也需要进一步拆分,... ,直到 `f(1)`, 这是「递」,`f(1) `解决了,由于 `f(2) = 2 f(1) = 2` 也解决了,.... `f(n)` 到最后也解决了,这是「归」,所以递归的本质是能把问题拆分成具有**相同解决思路**的子问题,。。。直到最后被拆解的子问题再也不能拆分,解决了最小粒度可求解的子问题后,在「归」的过程中自然顺其自然地解决了最开始的问题。
+
+
+
+### 递归原理
+
+递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。
+
+我们只需要关注如何把原问题划分成符合条件的子问题,而不需要去研究这个子问题是如何被解决的。
+
+递归和枚举的区别在于:枚举是横向地把问题划分,然后依次求解子问题,而递归是把问题逐级分解,是纵向的拆分。
+
+```java
+int func(你今年几岁) {
+ // 最简子问题,递归终止条件
+ if (你1999年几岁) return 我0岁;
+ // 递归调用,缩小规模
+ return func(你去年几岁) + 1;
+}
+```
+
+任何一个有意义的递归算法总是两部分组成:**递归调用**和**递归终止条件**。
+
+为了确保递归函数不会导致无限循环,它应具有以下属性:
+
+1. 一个简单的`基本案例(basic case)`(或一些案例) —— 能够不使用递归来产生答案的终止方案。
+2. 一组规则,也称作`递推关系(recurrence relation)`,可将所有其他情况拆分到基本案例。
+
+注意,函数可能会有多个位置进行自我调用。
+
+```java
+public returnType recursiveFunction(parameters) {
+ // 1. 递归终止条件(Base Case)
+ if (isBaseCase(parameters)) {
+ return baseCaseResult; // 返回问题的基本解,避免继续递归
+ }
+
+ // 2. 递归调用(Recursive Call)
+ // 将当前问题分解为更小的子问题
+ return recursiveFunction(modifiedParameters);
+
+ // 或者对于分治问题,递归调用多个子问题:
+ // return combineResults(
+ // recursiveFunction(subProblem1),
+ // recursiveFunction(subProblem2)
+ // );
+}
+
+```
+
+
+
+### 递归需要满足的三个条件
+
+只要同时满足以下三个条件,就可以用递归来解决。
+
+1. **一个问题的解可以分解为几个子问题的解**
+
+ 何为子问题?子问题就是数据规模更小的问题。比如前面的案例,要知道“自己在哪一排”,可以分解为“前一排的人在哪一排”这样的一个子问题。
+
+2. **这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样**
+
+ 如案例所示,求解“自己在哪一排”的思路,和前面一排人求解“自己在哪一排”的思路是一模一样的。
+
+3. **存在递归终止条件**
+
+ 把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。
+
+
+
+### 怎样编写递归代码
+
+写递归代码,可以按三步走:
+
+**第一要素:明确你这个函数想要干什么**
+
+首先,你需要明确你要解决的问题,以及这个问题是否适合用递归来解决。
+
+也就是说,我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。
+
+例如,我定义了一个函数,算 n 的阶乘
+
+```java
+// 算 n 的阶乘(假设n不为0)
+int factorial(int n){
+
+}
+```
+
+**第二要素:寻找递归结束条件**
+
+递归基(Base Case)是递归函数结束的条件。如果没有递归基,递归函数会无限循环下去。递归基应该是问题的最小单位,在这种情况下,函数可以直接返回结果而无需进一步递归。
+
+也就是说,我们需要找出**当参数为啥时,递归结束,之后直接把结果返回**,请注意,这个时候我们必须能根据这个参数的值,能够**直接**知道函数的结果是什么。
+
+例如,上面那个例子,当 n = 1 时,那你应该能够直接知道 f(n) 是啥吧?此时,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面
+
+```java
+// 算 n 的阶乘(假设n不为0)
+int f(int n){
+ if(n == 1){
+ return 1;
+ }
+}
+```
+
+有人可能会说,当 n = 2 时,那我们可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作为递归的结束条件吗?
+
+当然可以,只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。
+
+```java
+// 算 n 的阶乘(假设n>=2)
+int f(int n){
+ if(n == 2){
+ return 2;
+ }
+}
+```
+
+注意我代码里面写的注释,假设 n >= 2,因为如果 n = 1时,会被漏掉,当 n <= 2时,f(n) = n,所以为了更加严谨,我们可以写成这样:
+
+```java
+// 算 n 的阶乘(假设n不为0)
+int f(int n){
+ if(n <= 2){
+ return n;
+ }
+}
+```
+
+**第三要素:定义递归关系**
+
+第三要素就是,我们要**不断缩小参数的范围**,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。
+
+例如,f(n) 这个范围比较大,我们可以让 `f(n) = n * f(n-1)`。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数 f(n) 不变,我们需要让 f(n-1) 乘以 n。
+
+说白了,就是要找到原函数的一个等价关系式,`f(n)` 的等价关系式为 `n * f(n-1)`,即 `f(n) = n * f(n-1)`。
+
+> **写递归代码的关键就是找到如何将大问题分解为小问题的规律,请求基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码**。
+>
+> 当我们面对一个问题需要分解为多个子问题的时候,递归代码往往没那么好理解,比如第二个案例,人脑几乎没办法把整个“递”和“归”的过程一步一步都想清楚。
+>
+> 计算机擅长做重复的事情,所以递归正符合它的胃口。而我们人脑更喜欢平铺直叙的思维方式。当我们看到递归时,我们总想把递归平铺展开,脑子里就会循环,一层一层往下调,然后再一层一层返回,试图想搞清楚计算机每一步都是怎么执行的,这样就很容易被绕进去。
+>
+> 对于递归代码,这种试图想清楚整个递和归过程的做法,实际上是进入了一个思维误区。很多时候,我们理解起来比较吃力,主要原因就是自己给自己制造了这种理解障碍。那正确的思维方式应该是怎样的呢?
+>
+> 如果一个问题 A 可以分解为若干子问题 B、C、D,可以假设子问题 B、C、D 已经解决,在此基础上思考如何解决问题 A。而且,只需要思考问题 A 与子问题 B、C、D 两层之间的关系即可,不需要一层一层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。屏蔽掉递归细节,这样子理解起来就简单多了。
+>
+> 换句话说就是:千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。
+>
+> 所以,编写递归代码的关键是:**只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤**。
+
+
+
+#### 递归代码要警惕堆栈溢出
+
+递归调用的深度受限于程序的栈空间。如果递归深度太深,可能会导致栈溢出。对于深度较大的递归问题,可以考虑使用迭代方法或尾递归优化(Tail Recursion Optimization)。
+
+
+
+#### 递归代码要警惕重复计算
+
+在某些问题中(如斐波那契数列),直接递归可能导致大量重复计算,影响效率。可以使用记忆化(Memoization)或动态规划(Dynamic Programming)来优化递归,避免重复计算。
+
+
+
+### 案例
+
+#### 斐波那契数列
+
+> 斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34....,即第一项 f(1) = 1,第二项 f(2) = 1.....,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。
+
+```java
+int f(int n){
+ // 1.先写递归结束条件
+ if(n <= 2){
+ return 1;
+ }
+ // 2.接着写等价关系式
+ return f(n-1) + f(n - 2);
+}
+```
+
+
+
+#### 小青蛙跳台阶
+
+> 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
+
+每次跳的时候,小青蛙可以跳一个台阶,也可以跳两个台阶,也就是说,每次跳的时候,小青蛙有两种跳法。
+
+第一种跳法:第一次我跳了一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。
+
+第二种跳法:第一次跳了两个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。
+
+所以,小青蛙的全部跳法就是这两种跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了
+
+```java
+int f(int n){
+ // 1.先写递归结束条件
+ if(n == 1){
+ return 1;
+ }
+ // 2.接着写等价关系式
+ ruturn f(n-1) + f(n-2);
+}
+```
+
+大家觉得上面的代码对不对?
+
+答是不大对,当 n = 2 时,显然会有 f(2) = f(1) + f(0)。我们知道,f(0) = 0,按道理是递归结束,不用继续往下调用的,但我们上面的代码逻辑中,会继续调用 f(0) = f(-1) + f(-2)。这会导致无限调用,进入**死循环**。
+
+这也是我要和你们说的,关于**递归结束条件是否够严谨问题**,有很多人在使用递归的时候,由于结束条件不够严谨,导致出现死循环。也就是说,当我们在第二步找出了一个递归结束条件的时候,可以把结束条件写进代码,然后进行第三步,但是**请注意**,当我们第三步找出等价函数之后,还得再返回去第二步,根据第三步函数的调用关系,会不会出现一些漏掉的结束条件。就像上面,f(n-2)这个函数的调用,有可能出现 f(0) 的情况,导致死循环,所以我们把它补上。代码如下:
+
+```java
+int f(int n){
+ //f(0) = 0,f(1) = 1,等价于 n<=1时,f(n) = n。
+ if(n <= 1){
+ return n;
+ }
+ ruturn f(n-1) + f(n-2);
+}
+```
+
+#### 反转单链表
+
+> 反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1
+
+链表的节点定义如下:
+
+```java
+class Node{
+ int date;
+ Node next;
+}
+```
+
+这个的等价关系不像 n 是个数值那样,比较容易寻找。但是我告诉你,它的等价条件中,一定是范围不断在缩小,对于链表来说,就是链表的节点个数不断在变小
+
+```java
+//用递归的方法反转链表
+public Node reverseList(Node head){
+ // 1.递归结束条件
+ if (head == null || head.next == null) {
+ return head;
+ }
+ // 递归反转 子链表
+ Node newList = reverseList(head.next);
+ // 改变 1,2节点的指向。
+ // 通过 head.next获取节点2
+ Node t1 = head.next;
+ // 让 2 的 next 指向 2
+ t1.next = head;
+ // 1 的 next 指向 null.
+ head.next = null;
+ // 把调整之后的链表返回。
+ return newList;
+ }
+```
+
+
+
+#### [两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/)
+
+> 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
+>
+> 
+>
+> ```
+> 输入:head = [1,2,3,4]
+> 输出:[2,1,4,3]
+> ```
+
+当前节点 next,指向当前节点,指针互换
+
+```java
+ public ListNode swapPairs(ListNode head) {
+ // 基本情况:链表为空或只有一个节点
+ if (head == null || head.next == null) {
+ return head;
+ }
+
+ // 交换前两个节点
+ ListNode first = head;
+ ListNode second = head.next;
+
+ // 递归交换后续的节点
+ first.next = swapPairs(second.next);
+
+ // 交换后的第二个节点成为新的头节点
+ second.next = first;
+
+ // 返回新的头节点
+ return second;
+ }
+```
+
+下边这么写也可以,少了一个局部变量,交换操作和递归调用在一行内完成。
+
+```java
+public ListNode swapPairs(ListNode head) {
+ //递归的终止条件
+ if(head==null || head.next==null) {
+ return head;
+ }
+ //假设链表是 1->2->3->4
+ //这句就先保存节点2
+ ListNode tmp = head.next;
+ //继续递归,处理节点3->4
+ //当递归结束返回后,就变成了4->3
+ //于是head节点就指向了4,变成1->4->3
+ head.next = swapPairs(tmp.next);
+ //将2节点指向1
+ tmp.next = head;
+ return tmp;
+}
+```
+
+当然,也可以迭代实现~
+
+
+
+```java
+public class Solution {
+ public ListNode swapPairs(ListNode head) {
+ // 创建虚拟头节点,指向链表的头节点
+ ListNode dummy = new ListNode(0);
+ dummy.next = head;
+
+ // 当前节点指针,初始化为虚拟头节点
+ ListNode current = dummy;
+
+ // 遍历链表
+ while (current.next != null && current.next.next != null) {
+ // 初始化两个要交换的节点
+ ListNode first = current.next;
+ ListNode second = current.next.next;
+
+ // 交换这两个节点
+ first.next = second.next;
+ second.next = first;
+ current.next = second;
+
+ // 移动 current 指针到下一个需要交换的位置
+ current = first;
+ }
+
+ // 返回交换后的链表头
+ return dummy.next;
+ }
+}
+
+```
+
diff --git a/docs/data-structure-algorithms/algorithm/Sort.md b/docs/data-structure-algorithms/algorithm/Sort.md
new file mode 100644
index 0000000000..dbbc9842d3
--- /dev/null
+++ b/docs/data-structure-algorithms/algorithm/Sort.md
@@ -0,0 +1,826 @@
+---
+title: 排序
+date: 2023-10-09
+tags:
+ - Algorithm
+categories: Algorithm
+---
+
+
+
+> 🔢 **排序算法**,从接触计算机学科就会遇到的一个问题。
+
+排序算法可以分为**内部排序**和**外部排序**:
+- 🧠 **内部排序**:数据记录在内存中进行排序
+- 💾 **外部排序**:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存
+
+常见的内部排序算法有:**插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序**等。
+
+| 排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
+| :----------------------: | -------------- | ----------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------- |
+| 冒泡排序-Bubble Sort | $O(n²)$ | $O(n)$(当数组已经有序时,只需遍历一次) | $O(n²)$(当数组是逆序时,需要多次交换) | $O(1)$(原地排序) | 稳定 |
+| 选择排序-Selection Sort | $O(n²)$ | $O(n²)$ | $O(n²)$ | $O(1)$(原地排序) | 不稳定(交换元素时可能破坏相对顺序) |
+| 插入排序-Insertion Sort | $O(n²)$ | $O(n)$(当数组已经有序时) | $O(n²)$(当数组是逆序时) | $O(1)$(原地排序) | 稳定 |
+| 快速排序-Quick Sort | $O(n \log n)$ | $O(n \log n)$(每次划分的子数组大小相等) | $O(n²)$(每次选取的基准值使得数组划分非常不平衡) | $O(\log n)$(对于递归栈) $O(n)$(最坏情况递归栈深度为 n) | 不稳定(交换可能改变相同元素的相对顺序) |
+| 希尔排序-Shell Sort | $O(n \log² n)$ | $O(n \log n)$ | $O(n²)$(不同的增量序列有不同的最坏情况) | $O(1)$(原地排序) | 不稳定 |
+| 归并排序-Merge Sort | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(n)$(需要额外的空间用于辅助数组) | 稳定 |
+| 堆排序-Heap Sort | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(1)$(原地排序) | 不稳定(在调整堆时可能改变相同元素的相对顺序) |
+| 计数排序-Counting Sort) | $O(n + k)$ | $O(n + k)$(k 是数组中元素的取值范围) | $O(n + k)$ | $O(n + k)$(需要额外的数组来存储计数结果) | 稳定 |
+| 桶排序-Bucket Sort) | $O(n + k)$ | $O(n + k)$(k 是桶的数量,n 是元素数量) | $O(n²)$(所有元素都集中到一个桶里,退化成冒泡排序) | $O(n + k)$ | 稳定 |
+| 基数排序-Radix Sort | $O(d(n + k))$ | $O(d(n + k))$(d 是位数,k 是取值范围,n 是元素数量) | $O(d(n + k))$ | $O(n + k)$ | 稳定 |
+
+## 📊 排序算法分类
+
+十种常见排序算法可以分为两大类:
+
+### 🔄 非线性时间比较类排序
+通过比较来决定元素间的相对次序,由于其时间复杂度不能突破$O(nlogn)$,因此称为非线性时间比较类排序。
+
+> 💡 **特点**:需要比较元素大小,时间复杂度下界为 $O(n\log n)$
+
+### ⚡ 线性时间非比较类排序
+不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
+
+> 💡 **特点**:利用数据特性,可以达到线性时间复杂度
+
+
+
+## 🫧 冒泡排序
+
+冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
+
+> 🎯 **算法特点**:
+> - 简单易懂,适合学习排序思想
+> - 时间复杂度:$O(n²)$
+> - 空间复杂度:$O(1)$
+> - 稳定排序
+
+作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
+
+### 1. 算法步骤
+
+1. 🔍 **比较相邻元素**:如果第一个比第二个大,就交换他们两个
+2. 🔄 **重复比较**:对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
+3. 🎯 **缩小范围**:针对所有的元素重复以上的步骤,除了最后一个
+4. 🔁 **重复过程**:重复步骤1~3,直到排序完成
+
+### 2. 动图演示
+
+
+
+
+
+### 3. 性能分析
+
+- 🚀 **什么时候最快**:当输入的数据已经是正序时(都已经是正序了,我还要你冒泡排序有何用啊)
+- 🐌 **什么时候最慢**:当输入的数据是反序时(写一个 for 循环反序输出数据不就行了,干嘛要用你冒泡排序呢,我是闲的吗)
+
+```java
+//冒泡排序,a 表示数组, n 表示数组大小
+public void bubbleSort(int[] a) {
+ int n = a.length;
+ if (n <= 1) return;
+ // 外层循环遍历每一个元素
+ for (int i = 0; i < n; i++) {
+ //提前退出冒泡循环的标志位
+ boolean flag = false;
+ // 内层循环进行元素的比较与交换
+ for (int j = 0; j < n - i - 1; j++) {
+ if (a[j] > a[j+1]) {
+ int temp = a[j];
+ a[j] = a[j+1];
+ a[j+1] = temp;
+ flag = true; //表示有数据交换
+ }
+ }
+ if (!flag) break; //没有数据交换,提前退出。
+ }
+}
+```
+
+嵌套循环,应该立马就可以得出这个算法的时间复杂度为 $O(n²)$。
+
+冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度是 $O(1)$,是一个原地排序算法。
+
+
+
+## 🎯 选择排序
+
+选择排序的思路是这样的:首先,找到数组中最小的元素,拎出来,将它和数组的第一个元素交换位置,第二步,在剩下的元素中继续寻找最小的元素,拎出来,和数组的第二个元素交换位置,如此循环,直到整个数组排序完成。
+
+> 🎯 **算法特点**:
+> - 简单直观,容易理解
+> - 时间复杂度:$O(n²)$(无论什么情况)
+> - 空间复杂度:$O(1)$
+> - 不稳定排序
+
+选择排序是一种简单直观的排序算法,无论什么数据进去都是 $O(n²)$ 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
+
+### 1. 算法步骤
+
+1. 🔍 **找最小元素**:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
+2. 🔄 **继续寻找**:再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
+3. 🔁 **重复过程**:重复第二步,直到所有元素均排序完毕
+
+### 2. 动图演示
+
+
+
+```java
+public void selectionSort(int [] arrs) {
+ for (int i = 0; i < arrs.length; i++) {
+ //最小元素下标
+ int min = i;
+ for (int j = i +1; j < arrs.length; j++) {
+ if (arrs[j] < arrs[min]) {
+ min = j;
+ }
+ }
+ //如果当前位置i的元素已经是未排序部分的最小元素,就不需要交换了
+ if(min != i){
+ //交换位置
+ int temp = arrs[i];
+ arrs[i] = arrs[min];
+ arrs[min] = temp;
+ }
+ }
+ }
+}
+```
+
+
+
+## 📝 插入排序
+
+插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。
+
+> 🎯 **算法特点**:
+> - 像整理扑克牌一样直观
+> - 时间复杂度:$O(n²)$(最坏),$O(n)$(最好)
+> - 空间复杂度:$O(1)$
+> - 稳定排序
+
+它的工作原理为将待排列元素划分为「已排序」和「未排序」两部分,每次从「未排序的」元素中选择一个插入到「已排序的」元素中的正确位置。
+
+插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
+
+### 1. 算法步骤
+
+1. 🎯 **起始点**:从第一个元素开始(下标为 0 的元素),该元素可以认为已经被排序
+2. 🔍 **取下一个元素**:取出下一个元素,在已经排序的元素序列中**从后向前**扫描
+3. 🔄 **比较移动**:如果该元素(已排序)大于新元素,将该元素移到下一位置
+4. 🔁 **重复比较**:重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
+5. 📍 **插入位置**:将新元素插入到该位置后
+6. 🔁 **重复过程**:重复步骤2~5
+
+### 2. 动图演示
+
+
+
+```java
+public void insertionSort(int[] arr) {
+ // 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
+ for (int i = 1; i < arr.length; i++) {
+ int current = arr[i]; // 当前待插入元素
+ int j = i - 1; // 已排序部分的末尾索引
+
+ // 从后向前扫描,找到插入位置
+ while (j >= 0 && arr[j] > current) {
+ arr[j + 1] = arr[j]; // 元素后移
+ j--; // 索引左移
+ }
+ arr[j + 1] = current; // 插入到正确位置
+ }
+}
+```
+
+
+
+## ⚡ 快速排序
+
+快速排序的核心思想是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。
+
+> 🎯 **算法特点**:
+> - 分治法思想,效率高
+> - 时间复杂度:$O(n\log n)$(平均),$O(n²)$(最坏)
+> - 空间复杂度:$O(\log n)$
+> - 不稳定排序
+
+> 快速排序的最坏运行情况是 $O(n²)$,比如说顺序数列的快排。但它的平摊期望时间是 $O(nlogn)$,且 $O(nlogn)$ 记号中隐含的常数因子很小,比复杂度稳定等于 $O(nlogn) $的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
+>
+> 
+
+### 1. 算法步骤
+
+1. 🎯 **选择基准值**:从数组中选择一个元素作为基准值(pivot)。常见的选择方法有选取第一个元素、最后一个元素、中间元素或随机选取一个元素
+2. 🔄 **分区(Partition)**:遍历数组,将所有小于基准值的元素放在基准值的左侧,大于基准值的元素放在右侧。基准值放置在它的正确位置上
+3. 🔁 **递归排序**:对基准值左右两边的子数组分别进行递归排序,直到每个子数组的元素个数为 0 或 1,此时数组已经有序
+
+递归的最底部情形,是数列的大小是零或一,也就是数组都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
+
+### 2. 动图演示
+
+
+
+```java
+public class QuickSort {
+ // 对外暴露的排序方法:传入待排序数组
+ public static void sort(int[] arr) {
+ if (arr == null || arr.length <= 1) {
+ return; // 数组为空或只有1个元素,无需排序
+ }
+ // 调用递归方法:初始排序范围是整个数组(从0到最后一个元素)
+ quickSort(arr, 0, arr.length - 1);
+ }
+
+ // 递归排序方法:排序 arr 的 [left, right] 区间
+ private static void quickSort(int[] arr, int left, int right) {
+ // 递归终止条件:当 left >= right 时,子数组只有1个元素或为空,无需排序
+ if (left >= right) {
+ return;
+ }
+
+ // 1. 分区操作:返回基准值最终的索引位置
+ // (把比基准小的放左边,比基准大的放右边,基准在中间)
+ int pivotIndex = partition(arr, left, right);
+
+ quickSort(arr, left, pivotIndex - 1);
+ quickSort(arr, pivotIndex + 1, right);
+ }
+
+ // 分区核心方法:选 arr[right] 为基准,完成分区并返回基准索引
+ private static int partition(int[] arr, int left, int right) {
+ // 基准值:这里选当前区间的最后一个元素
+ int pivot = arr[right];
+ // i 指针:指向“小于基准的区域”的最后一个元素(初始时区域为空,i = left-1)
+ int i = left - 1;
+
+ // j 指针:遍历当前区间的所有元素(从 left 到 right-1,跳过基准)
+ for (int j = left; j < right; j++) {
+ // 如果当前元素 arr[j] 小于等于基准,就加入“小于基准的区域”
+ if (arr[j] <= pivot) {
+ i++; // 先扩大“小于基准的区域”
+ swap(arr, i, j); // 交换 arr[i] 和 arr[j],把 arr[j] 放进区域
+ }
+ }
+
+ // 最后:把基准值放到“小于基准区域”的后面(i+1 位置)
+ // 此时 i+1 就是基准的最终位置(左边都<=基准,右边都>基准)
+ swap(arr, i + 1, right);
+ return i + 1;
+ }
+
+ // 辅助方法:交换数组中两个位置的元素
+ private static void swap(int[] arr, int a, int b) {
+ int temp = arr[a];
+ arr[a] = arr[b];
+ arr[b] = temp;
+ }
+
+}
+```
+
+**📊 时间复杂度分析**
+
+**最优情况:$O(n \log n)$**
+
+- **适用场景**:每次分区操作都能将数组**均匀划分**(基准值接近中位数)
+- **推导过程**:
+ - 每次分区将数组分为两个近似相等的子数组
+ - 递归深度为 $\log_2 n$(分治次数),每层需遍历 $n$ 个元素
+ - 总比较次数满足递推公式:$T(n) = 2T(n/2) + O(n)$
+ - 通过主定理或递归树展开可得时间复杂度为 $O(n \log n)$
+
+**最坏情况:$O(n²)$**
+
+- **适用场景**:每次分区划分极不均衡(如基准值始终为最大/最小元素)
+- **常见案例**:输入数组已完全有序或逆序,且基准固定选择首/尾元素
+- **推导过程**:
+ - 每次分区仅减少一个元素(类似冒泡排序)
+ - 总比较次数为等差数列求和:$(n-1) + (n-2) + \cdots + 1 = \frac{n(n-1)}{2} = O(n^2)$
+ - 递归深度为 $n-1$ 层,导致时间复杂度退化为 $O(n²)$
+
+**平均情况:$O(n \log n)$**
+
+- **适用场景**:输入数据**随机分布**,基准值随机选取
+
+
+
+## 🔀 归并排序
+
+> 
+
+归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
+
+> 🎯 **算法特点**:
+> - 分治法典型应用
+> - 时间复杂度:$O(n\log n)$(稳定)
+> - 空间复杂度:$O(n)$
+> - 稳定排序
+
+分治,就是分而治之,将一个大问题分解成小的子问题来解决。小的问题解决了,大问题也就解决了。
+
+分治思想和递归思想很像。分治算法一般都是用递归来实现的。**分治是一种解决问题的处理思想,递归是一种编程技巧**,这两者并不冲突。
+
+作为一种典型的分而治之思想的算法应用,归并排序的实现有两种方法:
+
+- 🔄 **自上而下的递归**
+- 🔁 **自下而上的迭代**(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法)
+
+和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 $O(n\log n)$ 的时间复杂度。代价是需要额外的内存空间。
+
+### 1. 算法步骤
+
+1. 🔄 **分解**:将数组分成两半,递归地对每一半进行归并排序,直到每个子数组的大小为1(单个元素是有序的)
+2. 🔀 **合并**:将两个有序子数组合并成一个有序数组
+
+### 2. 动图演示
+
+
+
+
+
+```java
+public class MergeSort {
+ // 主排序函数
+ public static void mergeSort(int[] arr) {
+ if (arr.length < 2) return; // 基本情况
+ int mid = arr.length / 2;
+
+ // 分解
+ int[] left = new int[mid];
+ int[] right = new int[arr.length - mid];
+
+ // 填充左子数组
+ for (int i = 0; i < mid; i++) {
+ left[i] = arr[i];
+ }
+
+ // 填充右子数组
+ for (int i = mid; i < arr.length; i++) {
+ right[i - mid] = arr[i];
+ }
+
+ // 递归排序
+ mergeSort(left);
+ mergeSort(right);
+
+ // 合并已排序的子数组
+ merge(arr, left, right);
+ }
+
+ // 合并两个有序数组
+ private static void merge(int[] arr, int[] left, int[] right) {
+ // i、j、k 分别代表左子数组、右子数组、合并数组的指针
+ int i = 0, j = 0, k = 0;
+
+ // 合并两个有序数组,直到子数组都插入到合并数组
+ while (i < left.length && j < right.length) {
+ //比较 left[i] 和 right[j], 左右哪边小的,就放入合并数组,指针要 +1
+ if (left[i] <= right[j]) {
+ arr[k++] = left[i++];
+ } else {
+ arr[k++] = right[j++];
+ }
+ }
+
+ // 复制剩余元素
+ while (i < left.length) {
+ arr[k++] = left[i++];
+ }
+ while (j < right.length) {
+ arr[k++] = right[j++];
+ }
+ }
+
+ public static void main(String[] args) {
+ int[] arr = {38, 27, 43, 3, 9, 82, 10};
+ mergeSort(arr);
+ System.out.println("排序后的数组:");
+ for (int num : arr) {
+ System.out.print(num + " ");
+ }
+ }
+}
+
+```
+
+
+
+
+
+## 🌳 堆排序
+
+> 
+
+堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。
+
+> 🎯 **算法特点**:
+> - 基于堆数据结构
+> - 时间复杂度:$O(n\log n)$
+> - 空间复杂度:$O(1)$
+> - 不稳定排序
+
+分为两种方法:
+
+1. 📈 **大顶堆**:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列
+2. 📉 **小顶堆**:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列
+
+堆排序的平均时间复杂度为 $Ο(n\log n)$。
+
+### 1. 算法步骤
+
+1. 🏗️ **构建最大堆**:
+ - 首先将无序数组转换为一个**最大堆**。最大堆是一个完全二叉树,其中每个节点的值都大于或等于其子节点的值
+ - 最大堆的根节点(堆顶)是整个堆中的最大元素
+
+2. 🔄 **反复取出堆顶元素**:
+ - 将堆顶元素(最大值)与堆的最后一个元素交换,然后减少堆的大小,堆顶元素移到数组末尾
+ - 调整剩余的元素使其重新成为一个最大堆
+ - 重复这个过程,直到所有元素有序
+
+### 2. 动图演示
+
+
+
+```java
+public class HeapSort {
+ // 主排序函数
+ public static void heapSort(int[] arr) {
+ int n = arr.length;
+
+ // 1. 构建最大堆
+ for (int i = n / 2 - 1; i >= 0; i--) {
+ heapify(arr, n, i);
+ }
+
+ // 2. 逐步将堆顶元素与末尾元素交换,并缩小堆的范围
+ for (int i = n - 1; i > 0; i--) {
+ // 将当前堆顶(最大值)移到末尾
+ swap(arr, 0, i);
+
+ // 重新调整堆,使剩余元素保持最大堆性质
+ heapify(arr, i, 0);
+ }
+ }
+
+ // 调整堆的函数
+ private static void heapify(int[] arr, int n, int i) {
+ int largest = i; // 设当前节点为最大值
+ int left = 2 * i + 1; // 左子节点
+ int right = 2 * i + 2; // 右子节点
+
+ // 如果左子节点比当前最大值大
+ if (left < n && arr[left] > arr[largest]) {
+ largest = left;
+ }
+
+ // 如果右子节点比当前最大值大
+ if (right < n && arr[right] > arr[largest]) {
+ largest = right;
+ }
+
+ // 如果最大值不是根节点,则交换,并递归调整
+ if (largest != i) {
+ swap(arr, i, largest);
+ heapify(arr, n, largest);
+ }
+ }
+
+ // 交换两个元素的值
+ private static void swap(int[] arr, int i, int j) {
+ int temp = arr[i];
+ arr[i] = arr[j];
+ arr[j] = temp;
+ }
+}
+
+```
+
+
+
+## 🔢 计数排序
+
+**计数排序(Counting Sort)** 是一种基于计数的非比较排序算法,适用于对**非负整数**进行排序。计数排序通过统计每个元素出现的次数,然后利用这些信息将元素直接放到它们在有序数组中的位置,从而实现排序。
+
+> 🎯 **算法特点**:
+> - 非比较排序算法
+> - 时间复杂度:$O(n + k)$
+> - 空间复杂度:$O(k)$
+> - 稳定排序
+> - 适用于数据范围不大的场景
+
+计数排序的时间复杂度为 `O(n + k)`,其中 `n` 是输入数据的大小,`k` 是数据范围的大小。它特别适用于数据范围不大但数据量较多的场景。
+
+计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
+
+### 1. 算法步骤
+
+1. 🔍 **找到最大值和最小值**:找到数组中最大值和最小值,以确定计数数组的范围
+2. 🏗️ **创建计数数组**:创建一个计数数组,数组长度为 `max - min + 1`,用来记录每个元素出现的次数
+3. 📊 **统计每个元素的出现次数**:遍历原数组,将每个元素出现的次数记录在计数数组中
+4. 🔄 **累积计数**:将计数数组变为累积计数数组,使其表示元素在有序数组中的位置
+5. 📝 **回填到结果数组**:根据累积计数数组,回填到结果数组,得到最终的排序结果
+
+### 2. 动图演示
+
+[](https://github.com/hustcc/JS-Sorting-Algorithm/blob/master/res/countingSort.gif)
+
+```java
+import java.util.Arrays;
+
+public class CountingSort {
+ // 计数排序函数
+ public static void countingSort(int[] arr) {
+ if (arr.length == 0) return;
+
+ // 1. 找到数组中的最大值和最小值
+ int max = arr[0];
+ int min = arr[0];
+ for (int i = 1; i < arr.length; i++) {
+ if (arr[i] > max) {
+ max = arr[i];
+ } else if (arr[i] < min) {
+ min = arr[i];
+ }
+ }
+
+ // 2. 创建计数数组
+ int range = max - min + 1;
+ int[] count = new int[range];
+
+ // 3. 统计每个元素的出现次数
+ for (int i = 0; i < arr.length; i++) {
+ count[arr[i] - min]++;
+ }
+
+ // 4. 计算累积计数,确定元素的最终位置
+ for (int i = 1; i < count.length; i++) {
+ count[i] += count[i - 1];
+ }
+
+ // 5. 创建结果数组,并根据累积计数将元素放到正确位置
+ int[] output = new int[arr.length];
+ for (int i = arr.length - 1; i >= 0; i--) {
+ output[count[arr[i] - min] - 1] = arr[i];
+ count[arr[i] - min]--;
+ }
+
+ // 6. 将排序好的结果复制回原数组
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = output[i];
+ }
+ }
+}
+
+```
+
+
+
+## 🪣 桶排序
+
+桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
+
+桶排序(Bucket Sort)是一种基于**分配**的排序算法,尤其适用于**均匀分布**的数据。它通过将元素分布到多个桶中,分别对每个桶进行排序,最后将各个桶中的元素合并起来,得到一个有序数组。
+
+> 🎯 **算法特点**:
+> - 基于分配的排序算法
+> - 时间复杂度:$O(n + k)$(平均)
+> - 空间复杂度:$O(n + k)$
+> - 稳定排序
+> - 适用于均匀分布的数据
+
+桶排序的平均时间复杂度为 `O(n + k)`,其中 `n` 是数据的数量,`k` 是桶的数量。桶排序通常适用于小范围的、均匀分布的浮点数或整数。
+
+为了使桶排序更加高效,我们需要做到这两点:
+
+1. 📈 **增加桶的数量**:在额外空间充足的情况下,尽量增大桶的数量
+2. 🎯 **均匀分配**:使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
+
+同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
+
+### 1. 算法步骤
+
+1. 🏗️ **创建桶**:创建若干个桶,每个桶表示一个数值范围
+2. 📊 **将元素分配到桶中**:根据元素的大小,将它们分配到对应的桶中
+3. 🔄 **对每个桶内部排序**:对每个桶中的元素分别进行排序(可以使用插入排序、快速排序等)
+4. 🔀 **合并所有桶中的元素**:依次将每个桶中的元素合并起来,得到最终的有序数组
+
+
+
+```java
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class BucketSort {
+ // 主函数:进行桶排序
+ public static void bucketSort(float[] arr, int n) {
+ if (n <= 0) return;
+
+ // 1. 创建 n 个桶,每个桶是一个空的 ArrayList
+ ArrayList[] buckets = new ArrayList[n];
+
+ for (int i = 0; i < n; i++) {
+ buckets[i] = new ArrayList();
+ }
+
+ // 2. 将数组中的元素分配到各个桶中
+ for (int i = 0; i < n; i++) {
+ int bucketIndex = (int) arr[i] * n; // 根据值分配桶
+ buckets[bucketIndex].add(arr[i]);
+ }
+
+ // 3. 对每个桶中的元素进行排序
+ for (int i = 0; i < n; i++) {
+ Collections.sort(buckets[i]); // 可以使用任意内置排序算法
+ }
+
+ // 4. 合并所有桶中的元素,形成最终的排序数组
+ int index = 0;
+ for (int i = 0; i < n; i++) {
+ for (Float value : buckets[i]) {
+ arr[index++] = value;
+ }
+ }
+ }
+
+ // 测试主函数
+ public static void main(String[] args) {
+ float[] arr = { (float)0.42, (float)0.32, (float)0.33, (float)0.52, (float)0.37, (float)0.47, (float)0.51 };
+ int n = arr.length;
+ bucketSort(arr, n);
+
+ System.out.println("排序后的数组:");
+ for (float value : arr) {
+ System.out.print(value + " ");
+ }
+ }
+}
+
+```
+
+**性能分析**
+
+- 🚀 **什么时候最快**:当输入的数据可以均匀的分配到每一个桶中
+- 🐌 **什么时候最慢**:当输入的数据被分配到了同一个桶中
+
+> 💡 **学习提示**:思路一定要理解了,不背题哈,比如有些直接问你
+>
+> 📝 **例题**:已知一组记录的排序码为(46,79,56,38,40,80, 95,24),写出对其进行快速排序的第一趟的划分结果
+
+
+
+## 🔢 基数排序
+
+**基数排序(Radix Sort)** 是一种**非比较排序算法**,用于对整数或字符串等进行排序。它的核心思想是将数据按位(如个位、十位、百位等)进行排序,从低位到高位依次进行。基数排序的时间复杂度为 `O(n * k)`,其中 `n` 是待排序元素的个数,`k` 是数字的最大位数或字符串的长度。由于它不涉及比较操作,基数排序适用于一些特定的场景,如排序长度相同的字符串或范围固定的整数。
+
+> 🎯 **算法特点**:
+> - 非比较排序算法
+> - 时间复杂度:$O(n \times k)$
+> - 空间复杂度:$O(n + k)$
+> - 稳定排序
+> - 适用于位数固定的数据
+
+基数排序有两种实现方式:
+
+- 🔢 **LSD(Least Significant Digit)**:从最低位(个位)开始排序,常用的实现方式
+- 🔢 **MSD(Most Significant Digit)**:从最高位开始排序
+
+基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
+
+### 1. 算法步骤(LSD 实现)
+
+1. 🔍 **确定最大位数**:找出待排序数据中最大数的位数
+2. 🔄 **按位排序**:从最低位到最高位,对每一位进行排序。每次排序使用**稳定的排序算法**(如计数排序或桶排序),确保相同位数的元素相对位置不变
+
+### 2. 动图演示
+
+[](https://github.com/hustcc/JS-Sorting-Algorithm/blob/master/res/radixSort.gif)
+
+```java
+import java.util.Arrays;
+
+public class RadixSort {
+ // 主函数:进行基数排序
+ public static void radixSort(int[] arr) {
+ // 找到数组中的最大数,确定最大位数
+ int max = getMax(arr);
+
+ // 从个位开始,对每一位进行排序
+ for (int exp = 1; max / exp > 0; exp *= 10) {
+ countingSortByDigit(arr, exp);
+ }
+ }
+
+ // 找到数组中的最大值
+ private static int getMax(int[] arr) {
+ int max = arr[0];
+ for (int i = 1; i < arr.length; i++) {
+ if (arr[i] > max) {
+ max = arr[i];
+ }
+ }
+ return max;
+ }
+
+ // 根据当前位数进行计数排序
+ private static void countingSortByDigit(int[] arr, int exp) {
+ int n = arr.length;
+ int[] output = new int[n]; // 输出数组
+ int[] count = new int[10]; // 计数数组(0-9,用于处理每一位上的数字)
+
+ // 1. 统计每个数字在当前位的出现次数
+ for (int i = 0; i < n; i++) {
+ int digit = (arr[i] / exp) % 10;
+ count[digit]++;
+ }
+
+ // 2. 计算累积计数
+ for (int i = 1; i < 10; i++) {
+ count[i] += count[i - 1];
+ }
+
+ // 3. 从右到左遍历数组,按当前位将元素放入正确位置
+ for (int i = n - 1; i >= 0; i--) {
+ int digit = (arr[i] / exp) % 10;
+ output[count[digit] - 1] = arr[i];
+ count[digit]--;
+ }
+
+ // 4. 将排序好的结果复制回原数组
+ for (int i = 0; i < n; i++) {
+ arr[i] = output[i];
+ }
+ }
+
+}
+
+```
+
+
+
+## 🐚 希尔排序
+
+希尔排序这个名字,来源于它的发明者希尔,也称作"缩小增量排序",是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
+
+**希尔排序(Shell Sort)** 是一种**基于插入排序**的排序算法,又称为**缩小增量排序**,它通过将数组按一定间隔分成若干个子数组,分别进行插入排序,逐步缩小间隔,最终进行一次标准的插入排序。通过这种方式,希尔排序能够减少数据移动次数,使得整体排序过程更为高效。
+
+> 🎯 **算法特点**:
+> - 插入排序的改进版本
+> - 时间复杂度:$O(n^{1.3})$ 到 $O(n^2)$(取决于增量序列)
+> - 空间复杂度:$O(1)$
+> - 不稳定排序
+
+希尔排序的时间复杂度依赖于**增量序列**的选择,通常在 `O(n^1.3)` 到 `O(n^2)` 之间。
+
+### 1. 算法步骤
+
+1. 🎯 **确定增量序列**:首先确定一个增量序列 `gap`,通常初始的 `gap` 为数组长度的一半,然后逐步缩小
+2. 🔄 **分组排序**:将数组按 `gap` 分组,对每个分组进行插入排序。`gap` 表示当前元素与其分组中的前一个元素的间隔
+3. 📉 **缩小增量并继续排序**:每次将 `gap` 缩小一半,重复分组排序过程,直到 `gap = 1`,即对整个数组进行一次标准的插入排序
+4. ✅ **最终排序完成**:当 `gap` 变为 1 时,希尔排序相当于执行了一次插入排序,此时数组已经接近有序,因此插入排序的效率非常高
+
+### 2. 动图演示
+
+
+
+```java
+import java.util.Arrays;
+
+public class ShellSort {
+ // 主函数:希尔排序
+ public static void shellSort(int[] arr) {
+ int n = arr.length;
+
+ // 1. 初始 gap 为数组长度的一半
+ for (int gap = n / 2; gap > 0; gap /= 2) {
+ // 2. 对每个子数组进行插入排序
+ for (int i = gap; i < n; i++) {
+ int temp = arr[i];
+ int j = i;
+
+ // 3. 对当前分组进行插入排序
+ while (j >= gap && arr[j - gap] > temp) {
+ arr[j] = arr[j - gap];
+ j -= gap;
+ }
+
+ // 将 temp 插入到正确的位置
+ arr[j] = temp;
+ }
+ }
+ }
+}
+```
+
+
+
+
+
+## 📚 参考与感谢
+
+- https://yuminlee2.medium.com/sorting-algorithms-summary-f17ea88a9174
+
+---
+
+> 🎉 **恭喜你完成了排序算法的学习!** 排序是计算机科学的基础,掌握了这些算法,你就拥有了解决各种排序问题的强大工具。记住:**选择合适的排序算法比掌握所有算法更重要**!
diff --git a/docs/data-structure-algorithms/complexity.md b/docs/data-structure-algorithms/complexity.md
index e6a522bdc9..c347f82d35 100644
--- a/docs/data-structure-algorithms/complexity.md
+++ b/docs/data-structure-algorithms/complexity.md
@@ -1,23 +1,39 @@
-# 时间复杂度
+---
+title: 算法复杂度分析:开发者的必备技能
+date: 2025-05-09
+categories: Algorithm
+---
> 高级工程师title的我,最近琢磨着好好刷刷算法题更高级一些,然鹅,当我准备回忆大学和面试时候学的数据结构之时,我发现自己对这个算法复杂度的记忆只有OOOOOooo
>
> 文章收录在 GitHub [JavaKeeper](https://github.com/Jstarfish/JavaKeeper) ,N线互联网开发必备技能兵器谱
+## 前言
+
+作为Java后端开发者,我们每天都在编写代码解决业务问题。但你是否思考过:**为什么有些系统在用户量上升时响应越来越慢?为什么某些查询在数据量大时会卡死?为什么同样功能的代码,有些占用内存巨大?**
+
+答案就在算法复杂度中。
+
算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
+在实际的Java开发中,复杂度分析帮助我们:
+- **预测系统性能瓶颈**:在代码上线前就能预判性能问题
+- **优化关键路径**:识别出最需要优化的代码段
+- **合理选择数据结构**:HashMap vs TreeMap,ArrayList vs LinkedList
+- **面试加分项**:大厂面试必考,体现算法功底
+
那么我们应该如何去衡量不同算法之间的优劣呢?
-主要还是从算法所占用的「时间」和「空间」两个维度去考量。
+## 复杂度分析的两个维度
+
+主要还是从算法所占用的「时间」和「空间」两个维度去考量:
-- 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
-- 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
+- **时间维度**:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
+- **空间维度**:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。
-> 数据结构和算法本身解决的是“快”和“省”的问题,即如何让代码运行得更快,如何让代码更省存储空间。所以,执行效率是算法一个非常重要的考量指标。那如何来衡量你编写的算法代码的执行效率呢?
->
-> 就是:时间、空间复杂度
+> 数据结构和算法本身解决的是"快"和"省"的问题,即如何让代码运行得更快,如何让代码更省存储空间。所以,执行效率是算法一个非常重要的考量指标。
## **时间复杂度**
@@ -27,147 +43,1217 @@
这种表示方法我们称为「 **大O符号表示法** 」,又称为**渐进符号**,是用于描述函数渐进行为的数学符号
-常见的时间复杂度量级有:
+## 如何分析算法的时间复杂度
+
+### 复杂度分析的核心思路
+
+**核心原则**:关注当输入规模n趋向无穷大时,算法执行时间的增长趋势,而非具体的执行时间。
+
+**分析步骤**:
+1. **识别基本操作**:找出算法中最频繁执行的操作
+2. **计算执行次数**:分析这个操作随输入规模n的执行次数
+3. **忽略低阶项**:只保留增长最快的项
+4. **忽略系数**:去掉常数因子
+
+### 复杂度分析的实战方法
+
+#### 方法1:数循环层数
+这是最直观的分析方法:
+
+**单层循环 → O(n)**
+```java
+for (int i = 0; i < n; i++) {
+ // 基本操作
+}
+// 分析过程:循环n次,每次执行基本操作1次,总共n次 → O(n)
+```
+
+**双层嵌套循环 → O(n²)**
+```java
+for (int i = 0; i < n; i++) { // 外层:n次
+ for (int j = 0; j < n; j++) { // 内层:每轮n次
+ // 基本操作
+ }
+}
+// 分析过程:外层n次 × 内层n次 = n² 次 → O(n²)
+```
-- 常数阶$O(1)$
-- 线性阶$O(n)$
-- 平方阶$O(n^2)$
-- 立方阶$O(n^3)$
-- 对数阶$O(logn)$
-- 线性对数阶$O(nlogn)$
-- 指数阶$O(2^n)$
+**特殊情况:内层循环次数递减**
-#### 常数阶$O(1)$
+```java
+for (int i = 0; i < n; i++) { // 外层:n次
+ for (int j = i; j < n; j++) { // 内层:第i轮执行(n-i)次
+ // 基本操作
+ }
+}
+// 分析过程:总次数 = n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 ≈ n²/2
+// 忽略系数:O(n²)
+```
-$O(1)$,表示该算法的执行时间(或执行时占用空间)总是为一个常量,不论输入的数据集是大是小,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:
+#### 方法2:分析递归调用
+递归算法的复杂度 = 递归调用总次数 × 每次调用的复杂度
+**递归分析实例:计算斐波那契数列**
```java
-int i = 1;
-int j = 2;
-int k = i + j;
+int fibonacci(int n) {
+ if (n <= 1) return n; // 基本情况:O(1)
+ return fibonacci(n-1) + fibonacci(n-2); // 递归调用:2次
+}
```
-上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用$O(1)$来表示它的时间复杂度。
+**分析过程:**
+1. **画出递归树**:
+ ```
+ fib(n)
+ ├── fib(n-1)
+ │ ├── fib(n-2)
+ │ └── fib(n-3)
+ └── fib(n-2)
+ ├── fib(n-3)
+ └── fib(n-4)
+ ```
+
+2. **分析递归深度**:最深到fib(0),深度约为n
+
+3. **分析每层节点数**:每层节点数翻倍,第k层有2^k个节点
+
+4. **计算总节点数**:2^0 + 2^1 + ... + 2^n ≈ 2^n
-#### 线性阶$O(n)$
+5. **得出复杂度**:O(2^n)
-$O(n)$,表示一个算法的性能会随着输入数据的大小变化而线性变化,如
+**为什么这么慢?** 因为有大量重复计算!fib(n-2)既在fib(n-1)中计算,又在fib(n)中计算。
+#### 方法3:分析分治算法
+分治算法的复杂度可以用递推关系式分析。
+
+**二分查找分析**
```java
-for (int i = 0; i < n; i++) {
- j = i;
- j++;
+int binarySearch(int[] arr, int target, int left, int right) {
+ if (left > right) return -1; // 基本情况
+
+ int mid = (left + right) / 2; // O(1)操作
+ if (arr[mid] == target) return mid; // O(1)操作
+ else if (arr[mid] < target)
+ return binarySearch(arr, target, mid + 1, right); // 递归一半
+ else
+ return binarySearch(arr, target, left, mid - 1); // 递归一半
}
```
-这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用$O(n)$来表示它的时间复杂度。
+**分析过程:**
+1. **建立递推关系**:T(n) = T(n/2) + O(1)
+ - 每次递归处理一半的数据
+ - 除了递归外,其他操作都是O(1)
+
+2. **展开递推式**:
+ - T(n) = T(n/2) + 1
+ - T(n/2) = T(n/4) + 1
+ - T(n/4) = T(n/8) + 1
+ - ...
+ - T(1) = 1
+
+3. **求解**:总共需要log₂n层递归,每层O(1),所以T(n) = O(logn)
+
+### 常见复杂度等级及其识别
+
+| 复杂度 | 识别特征 | 典型例子 | 数据规模感受 |
+|--------|----------|----------|-------------|
+| **O(1)** | 不随n变化 | 数组索引访问、HashMap查找 | 任何规模都很快 |
+| **O(logn)** | 每次减半 | 二分查找、平衡树操作 | 100万数据只需20步 |
+| **O(n)** | 单层循环 | 数组遍历、链表查找 | n=1000万时还可接受 |
+| **O(nlogn)** | 分治+合并 | 归并排序、快速排序 | n=100万时性能良好 |
+| **O(n²)** | 双层循环 | 冒泡排序、暴力匹配 | n>1000就开始慢了 |
+| **O(2^n)** | 无优化递归 | 递归斐波那契 | n>30就要等很久 |
+
+## 复杂度分析实战练习
+
+学会了分析方法,让我们通过几个经典例子来实际练习,重点关注**分析过程**而不是记忆结果。
+
+### 练习1:分析冒泡排序的复杂度
-#### 平方阶$O(n^2)$
+```java
+public void bubbleSort(int[] arr) {
+ int n = arr.length;
+ for (int i = 0; i < n - 1; i++) {
+ for (int j = 0; j < n - i - 1; j++) {
+ if (arr[j] > arr[j + 1]) {
+ swap(arr, j, j + 1);
+ }
+ }
+ }
+}
+```
-$O(n²)$ 表示一个算法的性能将会随着输入数据的增长而呈现出二次增长。最常见的就是对输入数据进行嵌套循环。如果嵌套层级不断深入的话,算法的性能将会变为立方阶$O(n^3)$,$O(n^4)$,$O(n^k)$以此类推
+**分析过程:**
+1. **识别基本操作**:比较和交换操作 `arr[j] > arr[j + 1]`
+2. **分析循环次数**:
+ - 外层循环:i从0到n-2,共(n-1)次
+ - 内层循环:第i轮时,j从0到(n-i-2),共(n-i-1)次
+3. **计算总次数**:
+ - 第0轮:n-1次比较
+ - 第1轮:n-2次比较
+ - 第2轮:n-3次比较
+ - ...
+ - 第(n-2)轮:1次比较
+ - 总计:(n-1) + (n-2) + ... + 1 = n(n-1)/2 次
+4. **简化结果**:n(n-1)/2 ≈ n²/2,忽略系数得到 **O(n²)**
+
+### 练习2:分析HashMap查找的复杂度
```java
-for(x=1; i<=n; x++){
- for(i=1; i<=n; i++){
- j = i;
- j++;
+// HashMap的get操作
+public V get(Object key) {
+ int hash = hash(key); // 计算hash值,O(1)
+ int index = hash % table.length; // 计算索引,O(1)
+ Node node = table[index]; // 直接访问数组,O(1)
+
+ // 在链表/红黑树中查找
+ while (node != null) {
+ if (node.key.equals(key)) {
+ return node.value;
+ }
+ node = node.next; // 遍历链表
}
+ return null;
}
```
-#### 指数阶$O(2^n)$
+**分析过程:**
+1. **理想情况**:没有hash冲突,直接命中 → **O(1)**
+2. **最坏情况**:所有元素都hash到同一个位置,形成长度为n的链表 → **O(n)**
+3. **平均情况**:hash函数分布均匀,每个链表长度约为1 → **O(1)**
+
+**为什么说HashMap是O(1)?** 指的是平均情况下的复杂度。
-$O(2^n)$,表示一个算法的性能会随着输入数据的每次增加而增大两倍,典型的方法就是裴波那契数列的递归计算实现
+### 练习3:分析递归求阶乘的复杂度
```java
-int Fibonacci(int number)
-{
- if (number <= 1) return number;
+public int factorial(int n) {
+ if (n <= 1) return 1; // 基本情况
+ return n * factorial(n - 1); // 递归调用
+}
+```
+
+**分析过程:**
+1. **递归深度**:从n递减到1,深度为n
+2. **每层工作量**:除了递归调用,只有一次乘法运算,O(1)
+3. **总复杂度**:n层 × 每层O(1) = **O(n)**
+
+**递归调用栈:**
+```
+factorial(5)
+├── 5 * factorial(4)
+ ├── 4 * factorial(3)
+ ├── 3 * factorial(2)
+ ├── 2 * factorial(1)
+ └── 1
+```
+
+### 练习4:分析快速排序的复杂度
+
+```java
+public void quickSort(int[] arr, int low, int high) {
+ if (low < high) {
+ int pivot = partition(arr, low, high); // 分区,O(n)
+ quickSort(arr, low, pivot - 1); // 递归左半部分
+ quickSort(arr, pivot + 1, high); // 递归右半部分
+ }
+}
+```
+
+**分析过程:**
+1. **最好情况**:每次都能均匀分割
+ - 递归深度:log₂n
+ - 每层工作量:O(n)
+ - 总复杂度:O(nlogn)
+
+2. **最坏情况**:每次都是最不均匀的分割(已排序数组)
+ - 递归深度:n
+ - 每层工作量:O(n)
+ - 总复杂度:O(n²)
+
+3. **平均情况**:O(nlogn)
+
+**关键洞察**:快速排序的性能很大程度上取决于pivot的选择。
+
+### 复杂度分析的误区和技巧
+
+#### 常见误区
+
+1. **误区1:认为递归一定比循环慢**
+ ```java
+ // 递归版本:O(logn)
+ int binarySearchRecursive(int[] arr, int target, int left, int right) {
+ if (left > right) return -1;
+ int mid = (left + right) / 2;
+ if (arr[mid] == target) return mid;
+ else if (arr[mid] < target)
+ return binarySearchRecursive(arr, target, mid + 1, right);
+ else
+ return binarySearchRecursive(arr, target, left, mid - 1);
+ }
+
+ // 循环版本:同样O(logn)
+ int binarySearchIterative(int[] arr, int target) {
+ int left = 0, right = arr.length - 1;
+ while (left <= right) {
+ int mid = (left + right) / 2;
+ if (arr[mid] == target) return mid;
+ else if (arr[mid] < target) left = mid + 1;
+ else right = mid - 1;
+ }
+ return -1;
+ }
+ ```
+
+2. **误区2:认为代码行数多就复杂度高**
+ ```java
+ // 虽然代码很长,但复杂度是O(1)
+ public boolean isValidSudoku(char[][] board) {
+ for (int i = 0; i < 9; i++) { // 固定9次
+ for (int j = 0; j < 9; j++) { // 固定9次
+ // 检查逻辑...
+ }
+ }
+ return true; // 9×9=81次操作,是常数,所以O(1)
+ }
+ ```
+
+#### 实用技巧
+
+1. **看循环变量的变化规律**
+ ```java
+ // 线性增长 → O(n)
+ for (int i = 0; i < n; i++)
+
+ // 指数增长 → O(logn)
+ for (int i = 1; i < n; i *= 2)
+
+ // 嵌套循环 → O(n²)
+ for (int i = 0; i < n; i++)
+ for (int j = 0; j < n; j++)
+ ```
+
+2. **分析递归的关键问题**
+ - 递归了多少次?
+ - 每次递归处理多大的子问题?
+ - 除了递归还做了什么?
+
+3. **利用已知的复杂度**
+ - 排序通常是O(nlogn)
+ - 哈希表查找通常是O(1)
+ - 数组遍历是O(n)
+ - 二分查找是O(logn)
+
+除此之外,其实还有平均情况复杂度、最好时间复杂度、最坏时间复杂度。。。一般没有特殊说明的情况下,都是指最坏时间复杂度。
+
+------
+
+## **空间复杂度分析方法**
+
+空间复杂度分析相对简单,主要关注算法运行过程中需要的**额外存储空间**。
+
+### 空间复杂度的组成
+1. **算法本身**:代码指令占用的空间(通常忽略)
+2. **输入数据**:输入参数占用的空间(通常忽略)
+3. **额外空间**:算法运行时临时申请的空间(主要分析对象)
+
+### 空间复杂度分析步骤
- return Fibonacci(number - 2) + Fibonacci(number - 1);
+#### 第一步:识别额外空间的来源
+- **局部变量**:函数内声明的变量
+- **数据结构**:数组、链表、栈、队列等
+- **递归调用栈**:递归函数的调用栈
+
+#### 第二步:分析空间随输入规模的变化
+
+**O(1) 常量空间**
+```java
+public void swap(int[] arr, int i, int j) {
+ int temp = arr[i]; // 只使用了一个额外变量
+ arr[i] = arr[j];
+ arr[j] = temp;
}
+// 无论数组多大,只用了temp一个额外空间 → O(1)
```
-#### 对数阶$O(logn)$
+**O(n) 线性空间**
+```java
+public int[] copyArray(int[] arr) {
+ int[] newArr = new int[arr.length]; // 创建了n大小的新数组
+ for (int i = 0; i < arr.length; i++) {
+ newArr[i] = arr[i];
+ }
+ return newArr;
+}
+// 创建了大小为n的数组 → O(n)
+```
+**递归空间复杂度分析**
```java
-int i = 1;
-while(i nodeStack = new Stack<>();
+ Stack sumStack = new Stack<>();
+
+ nodeStack.push(root);
+ sumStack.push(sum - root.val);
+
+ while (!nodeStack.isEmpty()) {
+ TreeNode node = nodeStack.pop();
+ int currSum = sumStack.pop();
+
+ if (node.left == null && node.right == null && currSum == 0) {
+ return true;
+ }
+
+ if (node.left != null) {
+ nodeStack.push(node.left);
+ sumStack.push(currSum - node.left.val);
+ }
+ if (node.right != null) {
+ nodeStack.push(node.right);
+ sumStack.push(currSum - node.right.val);
+ }
}
+ return false;
}
```
-除此之外,其实还有平均情况复杂度、最好时间复杂度、最坏时间复杂度。。。一般没有特殊说明的情况下,都是值最坏时间复杂度。
+------
+
+## 复杂度速查表
+
+来源:https://liam.page/2016/06/20/big-O-cheat-sheet/ 源地址:https://www.bigocheatsheet.com/
------
+#### 1. 线性递归 - O(n)空间
+
+**LeetCode应用:链表递归**
+```java
+// 反转链表(递归版本)
+public ListNode reverseList(ListNode head) {
+ if (head == null || head.next == null) {
+ return head;
+ }
+
+ ListNode newHead = reverseList(head.next); // 递归调用
+ head.next.next = head;
+ head.next = null;
+
+ return newHead;
+}
+// 递归深度等于链表长度n,空间复杂度O(n)
-## **空间复杂度**
+// 计算链表长度(递归)
+public int getLength(ListNode head) {
+ if (head == null) return 0;
+ return 1 + getLength(head.next);
+}
+// 空间复杂度:O(n),因为递归栈深度为n
+```
-空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,一个算法所需的存储空间用f(n)表示。$S(n)=O(f(n))$,其中 n 为问题的规模,S(n) 表示空间复杂度。
+#### 2. 二分递归 - O(logn)空间
-一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。
+**示例:二分查找(递归版本)**
-一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为 $O(1)$。当一个算法的空间复杂度与n成线性比例关系时,可表示为$0(n)$,类比时间复杂度。
+```java
+public int binarySearch(int[] nums, int target, int left, int right) {
+ if (left > right) return -1;
+
+ int mid = left + (right - left) / 2;
+ if (nums[mid] == target) {
+ return mid;
+ } else if (nums[mid] < target) {
+ return binarySearch(nums, target, mid + 1, right);
+ } else {
+ return binarySearch(nums, target, left, mid - 1);
+ }
+}
+// 每次递归都将问题规模减半,递归深度为log₂n,空间复杂度O(logn)
+```
-空间复杂度比较常用的有:$O(1)$、$O(n)$、$O(n²)$
+**LeetCode应用:树的遍历**
+```java
+// 二叉树的最大深度
+public int maxDepth(TreeNode root) {
+ if (root == null) return 0;
+
+ int leftDepth = maxDepth(root.left); // 递归左子树
+ int rightDepth = maxDepth(root.right); // 递归右子树
+
+ return Math.max(leftDepth, rightDepth) + 1;
+}
+// 平衡树:递归深度为O(logn),空间复杂度O(logn)
+// 最坏情况(链状树):递归深度为O(n),空间复杂度O(n)
+```
-#### 空间复杂度 $O(1)$
+#### 3. 多分支递归 - 注意陷阱!
-如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
-举例:
+**错误理解:认为空间复杂度是O(2^n)**
+```java
+// 斐波那契数列(递归版本)
+public int fibonacci(int n) {
+ if (n <= 1) return n;
+ return fibonacci(n - 1) + fibonacci(n - 2);
+}
+```
+
+**正确分析:**
+- **时间复杂度**:O(2^n) - 因为有2^n个函数调用
+- **空间复杂度**:O(n) - 因为递归栈的最大深度是n
+
+> 关键理解:虽然有很多递归调用,但任何时刻调用栈的深度最多是n层
+
+#### 4. 记忆化递归的空间复杂度
```java
-int i = 1;
-int j = 2;
-++i;
-j++;
-int m = i + j;
+// 带记忆化的斐波那契
+private Map memo = new HashMap<>();
+
+public int fibonacciMemo(int n) {
+ if (n <= 1) return n;
+
+ if (memo.containsKey(n)) {
+ return memo.get(n);
+ }
+
+ int result = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
+ memo.put(n, result);
+ return result;
+}
+// 时间复杂度:O(n)
+// 空间复杂度:O(n) = 递归栈O(n) + 缓存数组O(n)
```
-代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
+### LeetCode中的空间复杂度优化技巧
+
+#### 1. 滚动数组优化
+```java
+// 原始DP:O(n)空间
+int[] dp = new int[n];
+
+// 优化后:O(1)空间
+int prev1 = dp[0], prev2 = dp[1];
+```
-#### 空间复杂度 $O(n)$
+#### 2. 就地修改避免额外空间
+```java
+// 数组去重(就地修改)
+public int removeDuplicates(int[] nums) {
+ if (nums.length == 0) return 0;
+
+ int i = 0;
+ for (int j = 1; j < nums.length; j++) {
+ if (nums[j] != nums[i]) {
+ i++;
+ nums[i] = nums[j]; // 就地修改,不需要额外空间
+ }
+ }
+ return i + 1;
+} // 空间复杂度:O(1)
+```
+#### 3. 递归转迭代
```java
-int[] m = new int[n]
-for(i=1; i<=n; ++i)
-{
- j = i;
- j++;
+// 递归版本:O(n)空间
+public void inorderRecursive(TreeNode root) {
+ if (root == null) return;
+ inorderRecursive(root.left);
+ System.out.println(root.val);
+ inorderRecursive(root.right);
+}
+
+// 迭代版本:O(h)空间,h为树的高度
+public void inorderIterative(TreeNode root) {
+ Stack stack = new Stack<>();
+ TreeNode current = root;
+
+ while (current != null || !stack.isEmpty()) {
+ while (current != null) {
+ stack.push(current);
+ current = current.left;
+ }
+ current = stack.pop();
+ System.out.println(current.val);
+ current = current.right;
+ }
}
```
-这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
+### 常见面试问题:时间空间复杂度权衡
+
+| 算法 | 时间复杂度 | 空间复杂度 | 权衡点 |
+|------|------------|------------|--------|
+| 快速排序 | O(nlogn) | O(logn) | 递归栈空间 |
+| 归并排序 | O(nlogn) | O(n) | 需要额外数组 |
+| 堆排序 | O(nlogn) | O(1) | 就地排序 |
+| 计数排序 | O(n+k) | O(k) | 需要额外计数数组 |
+| 哈希表查找 | O(1) | O(n) | 用空间换时间 |
+| 二分查找 | O(logn) | O(1) | 要求数组有序 |
------
+## 复杂度分析在实际开发中的应用
+### 系统设计中的复杂度考量
-## 复杂度速查表
+#### 1. 数据库查询优化
+```java
+// ❌ 没有索引的查询 - O(n)
+SELECT * FROM users WHERE email = 'user@example.com';
-来源:https://liam.page/2016/06/20/big-O-cheat-sheet/ 源地址:https://www.bigocheatsheet.com/
+// ✅ 有索引的查询 - O(logn)
+CREATE INDEX idx_email ON users(email);
+SELECT * FROM users WHERE email = 'user@example.com';
+```
+
+**在Java中的体现:**
+```java
+// JPA查询优化
+@Entity
+public class User {
+ @Column(name = "email")
+ @Index(name = "idx_email") // 添加索引
+ private String email;
+}
+
+// 避免N+1查询问题
+@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :userId")
+User findUserWithOrders(@Param("userId") Long userId);
+```
+
+#### 2. 缓存策略选择
+```java
+// Redis缓存 vs 数据库查询的复杂度对比
+public class UserService {
+
+ // ❌ 每次都查数据库 - O(logn)到O(n)
+ public User getUserById(Long id) {
+ return userRepository.findById(id);
+ }
+
+ // ✅ 使用缓存 - O(1)
+ @Cacheable(value = "users", key = "#id")
+ public User getUserByIdCached(Long id) {
+ return userRepository.findById(id);
+ }
+}
+```
+
+#### 3. 分页查询的复杂度陷阱
+```java
+// ❌ 深度分页性能问题
+// LIMIT 1000000, 20 在MySQL中是O(n),需要跳过100万条记录
+public Page getUsers(int page, int size) {
+ return userRepository.findAll(PageRequest.of(page, size));
+}
+
+// ✅ 游标分页优化为O(logn)
+public List getUsersAfterCursor(Long lastUserId, int limit) {
+ return userRepository.findByIdGreaterThanOrderById(lastUserId,
+ PageRequest.of(0, limit));
+}
+```
+
+### 微服务架构中的复杂度问题
+
+#### 1. 接口聚合的复杂度
+```java
+// ❌ 串行调用多个服务 - O(n)
+@RestController
+public class OrderController {
+
+ public OrderDetailVO getOrderDetail(Long orderId) {
+ Order order = orderService.getOrder(orderId); // 100ms
+ User user = userService.getUser(order.getUserId()); // 100ms
+ Product product = productService.getProduct(order.getProductId()); // 100ms
+ // 总耗时:300ms
+
+ return buildOrderDetail(order, user, product);
+ }
+}
-#### 图例
+// ✅ 并行调用优化 - O(1)
+@Async
+public CompletableFuture getOrderDetailAsync(Long orderId) {
+ CompletableFuture orderFuture =
+ CompletableFuture.supplyAsync(() -> orderService.getOrder(orderId));
+
+ CompletableFuture userFuture = orderFuture.thenCompose(order ->
+ CompletableFuture.supplyAsync(() -> userService.getUser(order.getUserId())));
+
+ CompletableFuture productFuture = orderFuture.thenCompose(order ->
+ CompletableFuture.supplyAsync(() -> productService.getProduct(order.getProductId())));
+
+ return CompletableFuture.allOf(orderFuture, userFuture, productFuture)
+ .thenApply(v -> buildOrderDetail(orderFuture.join(), userFuture.join(), productFuture.join()));
+ // 总耗时:约100ms
+}
+```
+
+#### 2. 批量处理优化
+```java
+// ❌ 循环调用接口 - O(n)
+public void processUsers(List userIds) {
+ for (Long userId : userIds) {
+ User user = userService.getUser(userId); // 每次一个网络调用
+ processUser(user);
+ }
+ // 1000个用户 = 1000次网络调用
+}
+
+// ✅ 批量调用 - O(1)
+public void processUsersBatch(List userIds) {
+ List users = userService.getUsers(userIds); // 一次网络调用
+ users.forEach(this::processUser);
+ // 1000个用户 = 1次网络调用
+}
+```
+
+### 大数据处理场景
+
+#### 1. 日志分析系统
+```java
+// 实时日志处理的复杂度考量
+@Component
+public class LogProcessor {
+
+ // ❌ 实时逐条处理 - 无法处理高并发
+ public void processLogRealtimeSync(LogEvent event) {
+ // 同步处理每条日志
+ analyzeLog(event); // 假设需要10ms
+ saveToDatabase(event); // 假设需要5ms
+ // 每秒最多处理:1000/(10+5) = 66条
+ }
+
+ // ✅ 批量异步处理 - 提升吞吐量
+ @EventListener
+ @Async
+ public void processLogBatch(List events) {
+ // 批量处理,均摊网络开销
+ List results = batchAnalyze(events); // 批量分析
+ batchSaveToDatabase(results); // 批量保存
+ // 每秒可处理:数万条
+ }
+}
+```
+
+#### 2. 报表查询优化
+```java
+// 大数据量报表查询的复杂度优化
+@Service
+public class ReportService {
+
+ // ❌ 实时聚合计算 - O(n),数据量大时很慢
+ public SalesReport getDailySalesReport(LocalDate date) {
+ List orders = orderRepository.findByCreateDate(date);
+ // 需要遍历所有订单进行聚合计算
+ return calculateSalesReport(orders);
+ }
+
+ // ✅ 预计算报表 - O(1)查询
+ @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
+ public void generateDailyReports() {
+ LocalDate yesterday = LocalDate.now().minusDays(1);
+ SalesReport report = calculateSalesReport(
+ orderRepository.findByCreateDate(yesterday));
+ reportRepository.save(report); // 预计算结果
+ }
+
+ public SalesReport getDailySalesReportCached(LocalDate date) {
+ return reportRepository.findByDate(date); // O(1)查询
+ }
+}
+```
+
+### Spring Boot性能优化实战
+
+#### 1. 启动时间优化
+```java
+// 启动时间复杂度优化
+@SpringBootApplication
+public class Application {
+
+ // ❌ 启动时加载大量数据
+ @PostConstruct
+ public void initData() {
+ // 启动时从数据库加载10万条配置 - 增加启动时间
+ configService.loadAllConfigs(); // O(n)
+ }
+
+ // ✅ 懒加载优化
+ @Bean
+ @Lazy
+ public ConfigCache configCache() {
+ return new ConfigCache(); // 需要时才初始化
+ }
+}
+```
+
+#### 2. 内存使用优化
+```java
+// 内存复杂度优化
+@RestController
+public class DataController {
+
+ // ❌ 一次性加载所有数据到内存 - O(n)空间
+ public List getAllData() {
+ List allData = dataRepository.findAll(); // 可能有百万条数据
+ return allData.stream()
+ .map(this::convertToVO)
+ .collect(Collectors.toList()); // 内存可能溢出
+ }
+
+ // ✅ 流式处理 - O(1)空间
+ public void exportAllData(HttpServletResponse response) {
+ try (Stream dataStream = dataRepository.findAllStream()) {
+ dataStream.map(this::convertToVO)
+ .forEach(vo -> writeToResponse(response, vo)); // 逐条处理
+ }
+ }
+}
+```
+
+### 消息队列的复杂度考量
+
+```java
+// 消息处理的复杂度优化
+@Component
+public class MessageProcessor {
+
+ // ❌ 顺序处理消息 - O(n)时间
+ @RabbitListener(queues = "order.queue")
+ public void processOrderSync(OrderMessage message) {
+ // 顺序处理,一个消息处理完才处理下一个
+ validateOrder(message); // 50ms
+ saveOrder(message); // 100ms
+ sendNotification(message); // 30ms
+ // 总计180ms/消息,吞吐量低
+ }
+
+ // ✅ 并行处理消息 - 提升吞吐量
+ @RabbitListener(queues = "order.queue", concurrency = "10")
+ public void processOrderAsync(OrderMessage message) {
+ CompletableFuture.allOf(
+ CompletableFuture.runAsync(() -> validateOrder(message)),
+ CompletableFuture.runAsync(() -> saveOrder(message)),
+ CompletableFuture.runAsync(() -> sendNotification(message))
+ ).join();
+ // 并行处理,吞吐量大幅提升
+ }
+}
+```
+
+### 实际项目中的复杂度思考框架
+
+#### 1. 性能需求评估
+```java
+// 根据业务规模选择合适的算法复杂度
+public class BusinessScaleAnalysis {
+
+ // 小规模系统(<1万用户)
+ public void smallScale() {
+ // O(n²)算法可以接受
+ // 简单直接的实现优先
+ }
+
+ // 中等规模系统(1万-100万用户)
+ public void mediumScale() {
+ // 需要O(nlogn)或更优算法
+ // 开始考虑缓存、索引优化
+ }
+
+ // 大规模系统(>100万用户)
+ public void largeScale() {
+ // 必须O(logn)或O(1)算法
+ // 分布式缓存、数据库分片等
+ }
+}
+```
+
+#### 2. 技术选型的复杂度考量
+```java
+// 不同技术栈的复杂度特性
+public class TechStackComplexity {
+
+ // HashMap vs TreeMap选择
+ Map userCache;
+
+ // 如果只需要快速查找:HashMap O(1)
+ userCache = new HashMap<>();
+
+ // 如果需要有序遍历:TreeMap O(logn)
+ userCache = new TreeMap<>();
+
+ // ArrayList vs LinkedList选择
+ List users;
+
+ // 频繁随机访问:ArrayList O(1)
+ users = new ArrayList<>();
+
+ // 频繁插入删除:LinkedList O(1)
+ users = new LinkedList<>();
+}
+```
+
+## 复杂度优化的最佳实践
+
+### LeetCode刷题的复杂度优化套路
+
+#### 1. 暴力解法 → 哈希表优化(降时间复杂度)
+```java
+// 模式:O(n²) → O(n)
+// 适用场景:查找、匹配类问题
+
+// 示例:两数之和类问题
+// 暴力:双层循环查找 O(n²)
+// 优化:哈希表存储 O(n)
+Map map = new HashMap<>();
+```
+
+#### 2. 递归 → 动态规划(消除重复计算)
+```java
+// 模式:O(2^n) → O(n)
+// 适用场景:有重叠子问题的递归
+
+// 递归优化三步走:
+// 1. 记忆化递归(自顶向下)
+Map memo = new HashMap<>();
+
+// 2. 动态规划(自底向上)
+int[] dp = new int[n+1];
+
+// 3. 空间优化(滚动变量)
+int prev1 = 0, prev2 = 1;
+```
+
+#### 3. 排序 + 双指针(降低循环层数)
+```java
+// 模式:O(n²) → O(nlogn)
+// 适用场景:需要查找配对的问题
+
+Arrays.sort(nums); // O(nlogn)
+int left = 0, right = nums.length - 1; // O(n)
+// 总体:O(nlogn),比O(n²)好
+```
+
+#### 4. 二分查找(利用有序性)
+```java
+// 模式:O(n) → O(logn)
+// 适用场景:在有序数组中查找
+
+// 关键:寻找单调性
+while (left <= right) {
+ int mid = left + (right - left) / 2;
+ // 根据mid位置的性质决定搜索方向
+}
+```
+
+### Java集合类的复杂度选择指南
+
+#### 1. List选择策略
+```java
+// 根据操作频率选择
+public class ListChoiceGuide {
+
+ // 频繁随机访问 + 较少插入删除 → ArrayList
+ List data = new ArrayList<>(); // get: O(1), add: O(1)
+
+ // 频繁插入删除 + 较少随机访问 → LinkedList
+ List data = new LinkedList<>(); // add/remove: O(1), get: O(n)
+
+ // 需要线程安全 → Vector 或 Collections.synchronizedList
+ List data = new Vector<>();
+
+ // 不可变列表 → Arrays.asList 或 List.of
+ List data = List.of("a", "b", "c");
+}
+```
+
+#### 2. Map选择策略
+```java
+public class MapChoiceGuide {
+
+ // 一般情况,追求最快查找 → HashMap
+ Map cache = new HashMap<>(); // O(1)
+
+ // 需要有序遍历 → TreeMap
+ Map sortedCache = new TreeMap<>(); // O(logn)
+
+ // 需要插入顺序 → LinkedHashMap
+ Map orderedCache = new LinkedHashMap<>(); // O(1) + 顺序
+
+ // 高并发环境 → ConcurrentHashMap
+ Map concurrentCache = new ConcurrentHashMap<>();
+}
+```
+
+#### 3. Set选择策略
+```java
+public class SetChoiceGuide {
+
+ // 一般去重需求 → HashSet
+ Set uniqueItems = new HashSet<>(); // O(1)
+
+ // 需要有序 → TreeSet
+ Set sortedItems = new TreeSet<>(); // O(logn)
+
+ // 需要保持插入顺序 → LinkedHashSet
+ Set orderedItems = new LinkedHashSet<>(); // O(1) + 顺序
+}
+```
+
+### 常见性能陷阱及避免方法
+
+#### 1. 字符串操作陷阱
+```java
+// ❌ String拼接陷阱 - O(n²)
+String result = "";
+for (String str : list) {
+ result += str; // 每次都创建新字符串
+}
+
+// ✅ 使用StringBuilder - O(n)
+StringBuilder sb = new StringBuilder();
+for (String str : list) {
+ sb.append(str);
+}
+String result = sb.toString();
+
+// ✅ Java 8 Stream方式 - O(n)
+String result = list.stream().collect(Collectors.joining());
+```
+
+#### 2. 集合遍历陷阱
+```java
+// ❌ 在遍历中修改集合 - 可能O(n²)
+for (int i = 0; i < list.size(); i++) {
+ if (shouldRemove(list.get(i))) {
+ list.remove(i); // ArrayList.remove()是O(n)操作
+ i--; // 还要调整索引
+ }
+}
+
+// ✅ 使用Iterator - O(n)
+Iterator iterator = list.iterator();
+while (iterator.hasNext()) {
+ if (shouldRemove(iterator.next())) {
+ iterator.remove(); // O(1)操作
+ }
+}
+
+// ✅ 使用removeIf - O(n)
+list.removeIf(this::shouldRemove);
+```
+
+#### 3. 数据库查询陷阱
+```java
+// ❌ N+1查询问题
+public List getOrdersWithUser(List orderIds) {
+ List orders = orderRepository.findByIds(orderIds); // 1次查询
+ return orders.stream().map(order -> {
+ User user = userRepository.findById(order.getUserId()); // N次查询
+ return new OrderVO(order, user);
+ }).collect(Collectors.toList());
+}
+
+// ✅ 批量查询优化
+public List getOrdersWithUserOptimized(List orderIds) {
+ List orders = orderRepository.findByIds(orderIds);
+ Set userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
+ Map userMap = userRepository.findByIds(userIds)
+ .stream().collect(Collectors.toMap(User::getId, user -> user));
+
+ return orders.stream().map(order -> {
+ User user = userMap.get(order.getUserId()); // O(1)查找
+ return new OrderVO(order, user);
+ }).collect(Collectors.toList());
+}
+```
+
+### 复杂度分析的实用技巧
+
+#### 1. 快速估算方法
+```java
+// 根据数据规模快速判断可接受的复杂度
+public class ComplexityEstimation {
+
+ public void analyzeDataScale(int n) {
+ if (n <= 10) {
+ // 任何算法都可以,包括O(n!)
+ System.out.println("可以使用任何算法");
+ } else if (n <= 20) {
+ // O(2^n)可以接受,如回溯算法
+ System.out.println("指数级算法可接受");
+ } else if (n <= 100) {
+ // O(n³)勉强可以
+ System.out.println("三次方算法勉强可以");
+ } else if (n <= 1000) {
+ // O(n²)是上限
+ System.out.println("平方级算法是上限");
+ } else if (n <= 100000) {
+ // 必须O(nlogn)或更优
+ System.out.println("必须线性对数级或更优");
+ } else {
+ // 必须O(n)或O(logn)
+ System.out.println("必须线性级或对数级");
+ }
+ }
+}
+```
+
+#### 2. 复杂度分析检查清单
+```java
+// 代码复杂度自检清单
+public class ComplexityCheckList {
+
+ public void checkComplexity() {
+ // 1. 有几层嵌套循环?
+ // - 1层 → O(n)
+ // - 2层 → O(n²)
+ // - k层 → O(n^k)
+
+ // 2. 有递归调用吗?
+ // - 递归深度 × 每层复杂度 = 总复杂度
+
+ // 3. 有二分查找吗?
+ // - 每次减半 → O(logn)
+
+ // 4. 有排序操作吗?
+ // - 通用排序 → O(nlogn)
+ // - 特殊排序 → 可能O(n)
+
+ // 5. 使用了什么数据结构?
+ // - HashMap → O(1)
+ // - TreeMap → O(logn)
+ // - LinkedList查找 → O(n)
+ }
+}
+```
+
+#### 3. 空间复杂度优化技巧
+```java
+// 常见空间优化模式
+public class SpaceOptimization {
+
+ // 1. 滚动数组优化DP
+ public int optimizedDP(int n) {
+ // 原始:int[] dp = new int[n]; // O(n)空间
+ // 优化:只保存必要的状态
+ int prev = 0, curr = 1; // O(1)空间
+ return curr;
+ }
+
+ // 2. 就地修改避免额外空间
+ public void reverseArrayInPlace(int[] arr) {
+ int left = 0, right = arr.length - 1;
+ while (left < right) {
+ // 就地交换,不需要额外数组
+ int temp = arr[left];
+ arr[left] = arr[right];
+ arr[right] = temp;
+ left++;
+ right--;
+ }
+ }
+
+ // 3. 流式处理大数据
+ public void processLargeData() {
+ // 避免一次性加载所有数据到内存
+ try (Stream lines = Files.lines(Paths.get("large-file.txt"))) {
+ lines.filter(line -> line.contains("target"))
+ .forEach(this::processLine); // 逐行处理
+ }
+ }
+}
+```
+
+## 复杂度速查表

@@ -197,7 +1283,80 @@ for(i=1; i<=n; ++i)
-## 参考
+## 总结
+
+### 核心要点回顾
-- 《大话数据结构》
-- https://zhuanlan.zhihu.com/p/50479555
\ No newline at end of file
+通过本文的学习,我们掌握了算法复杂度分析的核心知识:
+
+#### 1. 理论基础
+- **时间复杂度**:衡量算法执行时间随输入规模增长的趋势
+- **空间复杂度**:衡量算法占用内存空间随输入规模增长的趋势
+- **大O符号**:用于描述算法复杂度的数学工具
+
+#### 2. 常见复杂度等级(从优到劣)
+1. **O(1)** - 常数阶:HashMap查找、数组随机访问
+2. **O(logn)** - 对数阶:二分查找、TreeMap操作
+3. **O(n)** - 线性阶:数组遍历、链表查找
+4. **O(nlogn)** - 线性对数阶:快速排序、归并排序
+5. **O(n²)** - 平方阶:冒泡排序、嵌套循环
+6. **O(2^n)** - 指数阶:递归斐波那契(需要避免)
+
+#### 3. LeetCode刷题复杂度优化套路
+- **暴力 → 哈希表**:O(n²) → O(n)
+- **递归 → 动态规划**:O(2^n) → O(n)
+- **暴力查找 → 二分查找**:O(n) → O(logn)
+- **嵌套循环 → 双指针**:O(n²) → O(n)
+
+#### 4. Java开发实战经验
+- **集合选择**:根据操作频率选择ArrayList/LinkedList、HashMap/TreeMap
+- **避免陷阱**:字符串拼接、集合遍历修改、N+1查询
+- **空间优化**:滚动数组、就地修改、流式处理
+
+### 给Java后端开发者的建议
+
+#### 学习路径
+1. **理论基础**:掌握本文的复杂度分析方法
+2. **刷题实战**:在LeetCode上练习复杂度分析
+3. **项目应用**:在实际项目中运用复杂度思维
+4. **持续优化**:定期review代码的性能瓶颈
+
+#### 面试准备
+- **必备技能**:能快速分析代码的时间空间复杂度
+- **常考题型**:两数之和、排序算法、动态规划、树遍历
+- **优化思路**:从暴力解法开始,逐步优化到最优解
+- **实际应用**:结合项目经验谈复杂度优化案例
+
+#### 职场应用
+- **代码review**:关注性能复杂度,不只是功能正确性
+- **系统设计**:提前评估算法复杂度,避免性能瓶颈
+- **技术选型**:基于复杂度分析选择合适的数据结构和算法
+- **性能调优**:使用复杂度分析定位和解决性能问题
+
+### 进阶学习方向
+
+如果你想在算法和数据结构方面更进一步,建议关注:
+
+1. **高级数据结构**:并查集、线段树、字典树
+2. **算法设计模式**:分治、贪心、回溯、动态规划
+3. **系统设计**:分布式系统中的复杂度考量
+4. **性能优化**:JVM调优、数据库优化、缓存策略
+
+### 写在最后
+
+复杂度分析不是纸上谈兵的理论知识,而是每个Java后端开发者都应该掌握的实用技能。它帮助我们:
+
+- **写出更高效的代码**
+- **在面试中展现技术功底**
+- **在系统设计时做出正确决策**
+- **在性能调优时快速定位问题**
+
+希望这篇文章能帮助你建立起完整的复杂度分析知识体系。记住,**理论学习 + 刷题实战 + 项目应用** 才是掌握算法复杂度的最佳路径。
+
+---
+
+**下期预告**:我们将深入探讨**数组与链表**的底层实现和应用技巧,敬请期待!
+
+> 如果觉得文章对你有帮助,欢迎点赞分享,你的支持是我创作的动力!
+>
+> 更多Java技术文章请关注 GitHub [JavaKeeper](https://github.com/Jstarfish/JavaKeeper)
\ No newline at end of file
diff --git a/docs/data-structure-algorithms/Array.md b/docs/data-structure-algorithms/data-structure/Array.md
similarity index 99%
rename from docs/data-structure-algorithms/Array.md
rename to docs/data-structure-algorithms/data-structure/Array.md
index 06af5f6157..81505f83ea 100644
--- a/docs/data-structure-algorithms/Array.md
+++ b/docs/data-structure-algorithms/data-structure/Array.md
@@ -6,7 +6,7 @@
-## 前言``
+## 前言
具体介绍数组之前,我们先来了解一下集合、列表和数组的概念之间的差别。
diff --git a/docs/data-structure-algorithms/Binary-Tree.md b/docs/data-structure-algorithms/data-structure/Binary-Tree.md
similarity index 93%
rename from docs/data-structure-algorithms/Binary-Tree.md
rename to docs/data-structure-algorithms/data-structure/Binary-Tree.md
index b3321b32b3..4f22ac687b 100755
--- a/docs/data-structure-algorithms/Binary-Tree.md
+++ b/docs/data-structure-algorithms/data-structure/Binary-Tree.md
@@ -34,12 +34,15 @@ categories: data-structure
- **路径**: 连续边的序列称为路径。 在上图所示的树中,节点`E`的路径为`A→B→E`
- **祖先节点**: 节点的祖先是从根到该节点的路径上的任何前节点。根节点没有祖先节点。 在上图所示的树中,节点`F`的祖先是`B`和`A`
- **度**: 节点的度数等于子节点数。 在上图所示的树中,节点`B`的度数为`2`。叶子节点的度数总是`0`,而在完整的二叉树中,每个节点的度数等于`2`
-- **高度(Height)/深度(Depth)**:树中层的数量。比如上图中的树有 4 层,则高度为 4
+- **高度(Height)**:[根]节点到叶子节点的最常路径(边数)
+- 深度(Depth):根节点到这个节点所经历的边的个数
- **级别编号**: 为树的每个节点分配一个级别编号,使得每个节点都存在于高于其父级的一个级别。树的根节点始终是级别`0`。
- **层级(Level)**:根为 Level 0 层,根的子节点为 Level 1 层,以此类推
- 有序树、无序树:如果将树中的各个子树看成是从左到右是有次序的,则称该树是有序树;若不考虑子树的顺序称为无序树
- 森林:m(m>=0)棵互不交互的树的集合。对树中每个结点而言,其子树的集合即为森林
+
+
### 基本操作
1. 构造空树(初始化)
@@ -203,36 +206,77 @@ public class BinarySearchTree>{
**1.中序遍历:当到达某个节点时,先访问左子节点,再输出该节点,最后访问右子节点。**
```java
-public void inOrder(TreeNode cursor){
- if(cursor == null) return;
- inOrder(cursor.getLeft());
- System.out.println(cursor.getData());
- inOrder(cursor.getRight());
+/* 中序遍历 */
+void inOrder(TreeNode root) {
+ if (root == null)
+ return;
+ // 访问优先级:左子树 -> 根节点 -> 右子树
+ inOrder(root.left);
+ list.add(root.val);
+ inOrder(root.right);
}
```
**2. 前序遍历:当到达某个节点时,先输出该节点,再访问左子节点,最后访问右子节点。**
```java
-public void preOrder(TreeNode cursor){
- if(cursor == null) return;
- System.out.println(cursor.getData());
- inOrder(cursor.getLeft());
- inOrder(cursor.getRight());
+/* 前序遍历 */
+void preOrder(TreeNode root) {
+ if (root == null)
+ return;
+ // 访问优先级:根节点 -> 左子树 -> 右子树
+ list.add(root.val);
+ preOrder(root.left);
+ preOrder(root.right);
}
```
**3. 后序遍历:当到达某个节点时,先访问左子节点,再访问右子节点,最后输出该节点。**
```java
-public void postOrder(TreeNode cursor){
- if(cursor == null) return;
- inOrder(cursor.getLeft());
- inOrder(cursor.getRight());
- System.out.println(cursor.getData());
+/* 后序遍历 */
+void postOrder(TreeNode root) {
+ if (root == null)
+ return;
+ // 访问优先级:左子树 -> 右子树 -> 根节点
+ postOrder(root.left);
+ postOrder(root.right);
+ list.add(root.val);
}
```
+前序、中序和后序遍历都属于深度优先遍历(depth-first traversal),也称深度优先搜索(depth-first search, DFS),它体现了一种“先走到尽头,再回溯继续”的遍历方式。
+
+**深度优先遍历就像是绕着整棵二叉树的外围“走”一圈**,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。
+
+
+
+**4. 层序遍历:**层序遍历(level-order traversal)从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
+
+层序遍历本质上属于广度优先遍历(breadth-first traversal),也称广度优先搜索(breadth-first search, BFS),它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
+
+```java
+/* 层序遍历 */
+List levelOrder(TreeNode root) {
+ // 初始化队列,加入根节点
+ Queue queue = new LinkedList<>();
+ queue.add(root);
+ // 初始化一个列表,用于保存遍历序列
+ List list = new ArrayList<>();
+ while (!queue.isEmpty()) {
+ TreeNode node = queue.poll(); // 队列出队
+ list.add(node.val); // 保存节点值
+ if (node.left != null)
+ queue.offer(node.left); // 左子节点入队
+ if (node.right != null)
+ queue.offer(node.right); // 右子节点入队
+ }
+ return list;
+}
+```
+
+
+
#### 2. 树的搜索:
树的搜索和树的遍历差不多,就是在遍历的时候只搜索不输出就可以了(类比有序数组的搜索)
@@ -452,25 +496,20 @@ AVL 树和二叉查找树的删除操作情况一致,都分为四种情况:
对于删除操作造成的非平衡状态的修正,可以这样理解:对左或者右子树的删除操作相当于对右或者左子树的插入操作,然后再对应上插入的四种情况选择相应的旋转就好了。
-## 红黑树
-
-**红黑树的定义:**红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。**它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(logn)时间内做查找,插入和删除,这里的n是树中元素的数目。**
-红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用如实时应用(real time application)中有价值,而且使它们有在提供最坏情况担保的其他数据结构中作为建造板块的价值;例如,在计算几何中使用的很多数据结构都可以基于红黑树。此外,红黑树还是2-3-4树的一种等同,它们的思想是一样的,只不过红黑树是2-3-4树用二叉树的形式表示的。
-**红黑树的性质:**
+## 红黑树
-红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制的一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
+顾名思义,红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:
-- 每个节点要么是黑色,要么是红色
-- 根节点是黑色
-- 所有叶子都是黑色(叶子是NIL节点)
-- 每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)
-- **任意一结点到每个叶子结点的简单路径都包含数量相同的黑结点**
+- 根节点是黑色的;
+- 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
+- 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
+- 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
下面是一个具体的红黑树的图例:
-[](https://camo.githubusercontent.com/5db2fdaae07fbac460e1ef727d9b890ce292cbcb33783efece8fc2fad30006a8/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f7468756d622f362f36362f5265642d626c61636b5f747265655f6578616d706c652e7376672f34353070782d5265642d626c61636b5f747265655f6578616d706c652e7376672e706e67)
+
这些约束确保了红黑树的关键特性: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
diff --git a/docs/data-structure-algorithms/data-structure/HashTable.md b/docs/data-structure-algorithms/data-structure/HashTable.md
new file mode 100755
index 0000000000..edb7eb82ab
--- /dev/null
+++ b/docs/data-structure-algorithms/data-structure/HashTable.md
@@ -0,0 +1,168 @@
+---
+title: 深入解析哈希表:从散列冲突到算法应用全景解读
+date: 2022-06-09
+tags:
+ - data-structure
+ - HashTable
+categories: data-structure
+---
+
+
+
+## 一、散列冲突的本质与解决方案
+
+哈希表作为数据结构的核心组件,其灵魂在于通过哈希函数实现 $(1)$ 时间复杂度的数据存取。但正如硬币的两面,哈希算法在带来高效存取的同时,也面临着不可避免的**散列冲突**问题——不同的输入值经过哈希运算后映射到同一存储位置的现象。
+
+### 1.1 开放寻址法:空间换时间的博弈
+
+**典型代表**:Java 的 ThreadLocalMap
+
+开放寻址法采用"线性探测+数组存储"的经典组合,当冲突发生时,通过系统性的探测策略(线性探测/二次探测/双重哈希)在数组中寻找下一个可用槽位。这种方案具有三大显著优势:
+
+- **缓存友好性**:数据连续存储在数组中,有效利用CPU缓存行预取机制
+- **序列化简单**:无需处理链表指针等复杂内存结构
+- **空间紧凑**:内存分配完全可控,无动态内存开销
+
+但硬币的另一面是:
+
+- **删除复杂度**:需引入墓碑标记(TOMBSTONE)处理逻辑
+- **负载因子限制**:建议阈值0.7以下,否则探测次数指数级增长
+- **内存浪费**:动态扩容时旧数组需要保留至数据迁移完成
+
+```Java
+// 线性探测的典型实现片段
+int index = hash(key);
+while (table[index] != null) {
+ if (table[index].key.equals(key)) break;
+ index = (index + 1) % capacity;
+}
+```
+
+### 1.2 链表法:时间与空间的动态平衡
+
+**典型代表**:Java的LinkedHashMap
+
+链表法采用"数组+链表/树"的复合结构,每个桶位维护一个动态数据结构。其核心优势体现在:
+
+- **高负载容忍**:允许负载因子突破1.0(Java HashMap默认0.75)
+- **内存利用率**:按需创建节点,避免空槽浪费
+- **结构灵活性**:可升级为红黑树(Java8+)应对哈希碰撞攻击
+
+但需要注意:
+
+- **指针开销**:每个节点多消耗4-8字节指针空间
+- **缓存不友好**:节点内存地址离散影响访问局部性
+- **小对象劣势**:当存储值小于指针大小时内存利用率降低
+
+```Java
+// 树化转换阈值定义(Java HashMap)
+static final int TREEIFY_THRESHOLD = 8;
+static final int UNTREEIFY_THRESHOLD = 6;
+```
+
+
+
+## 二、哈希算法:从理论到工程实践
+
+哈希算法作为数字世界的"指纹生成器",必须满足四大黄金准则:
+
+1. **不可逆性**:哈希值到原文的逆向推导在计算上不可行
+2. **雪崩效应**:微小的输入变化导致输出剧变
+3. **低碰撞率**:不同输入的哈希相同概率趋近于零
+4. **高效计算**:处理海量数据时仍保持线性时间复杂度
+
+### 2.1 七大核心应用场景解析
+
+#### 场景1:安全加密(SHA-256示例)
+
+```Java
+MessageDigest md = MessageDigest.getInstance("SHA-256");
+byte[] hashBytes = md.digest("secret".getBytes());
+```
+
+#### 场景2:内容寻址存储(IPFS协议)
+
+通过三级哈希验证确保内容唯一性:
+
+1. 内容分块哈希
+2. 分块组合哈希
+3. 最终Merkle根哈希
+
+#### 场景3:P2P传输校验(BitTorrent协议)
+
+种子文件包含分片哈希树,下载时逐层校验:
+
+```
+ 分片1(SHA1) → 分片2(SHA1) → ... → 分片N(SHA1)
+ ↘ ↙ ↘ ↙
+ 中间哈希节点 根哈希
+```
+
+#### 场景4:高性能散列函数(MurmurHash3)
+
+针对不同场景的哈希优化:
+
+- 内存型:CityHash
+- 加密型:SipHash
+- 流式处理:XXHash
+
+#### 场景5:会话保持负载均衡
+
+```Python
+def get_server(client_ip):
+ hash_val = hashlib.md5(client_ip).hexdigest()
+ return servers[hash_val % len(servers)]
+```
+
+#### 场景6:大数据分片处理
+
+```SQL
+-- 按用户ID哈希分库
+CREATE TABLE user_0 (
+ id BIGINT PRIMARY KEY,
+ ...
+) PARTITION BY HASH(id) PARTITIONS 4;
+```
+
+#### 场景7:一致性哈希分布式存储
+
+构建虚拟节点环解决数据倾斜问题:
+
+```
+ NodeA → 1000虚拟节点
+NodeB → 1000虚拟节点
+NodeC → 1000虚拟节点
+```
+
+
+
+## 三、工程实践中的进阶技巧
+
+### 3.1 动态扩容策略
+
+- 渐进式扩容:避免一次性rehash导致的STW停顿
+- 容量质数选择:降低哈希聚集现象(如Java HashMap使用2^n优化模运算)
+
+### 3.2 哈希攻击防御
+
+- 盐值加密:password_hash(pass,PASSWORDBCRYPT,[′salt′=>*p**a**ss*,*P**A**SS**W**OR**D**B**CR**Y**PT*,[′*s**a**l**t*′=>salt])
+- 密钥哈希:HMAC-SHA256(secretKey, message)
+
+### 3.3 性能优化指标
+
+| 指标 | 开放寻址法 | 链表法 |
+| ------------ | ---------- | ------ |
+| 平均查询时间 | O(1/(1-α)) | O(α) |
+| 内存利用率 | 60-70% | 80-90% |
+| 最大负载因子 | 0.7 | 1.0+ |
+| 并发修改支持 | 困难 | 较容易 |
+
+
+
+## 四、未来演进方向
+
+- **量子安全哈希**:抗量子计算的Lattice-based哈希算法
+- **同态哈希**:支持密文域计算的哈希方案
+- **AI驱动哈希**:基于神经网络的自适应哈希函数
+
+哈希表及其相关算法作为计算机科学的基石,在从单机系统到云原生架构的演进历程中持续发挥着关键作用。理解其核心原理并掌握工程化实践技巧,将帮助开发者在高并发、分布式场景下构建出更健壮、更高效的系统。
diff --git a/docs/data-structure-algorithms/Linked-List.md b/docs/data-structure-algorithms/data-structure/Linked-List.md
similarity index 100%
rename from docs/data-structure-algorithms/Linked-List.md
rename to docs/data-structure-algorithms/data-structure/Linked-List.md
diff --git a/docs/data-structure-algorithms/Queue.md b/docs/data-structure-algorithms/data-structure/Queue.md
similarity index 90%
rename from docs/data-structure-algorithms/Queue.md
rename to docs/data-structure-algorithms/data-structure/Queue.md
index 30bce5fd26..2c91ca275f 100644
--- a/docs/data-structure-algorithms/Queue.md
+++ b/docs/data-structure-algorithms/data-structure/Queue.md
@@ -1,25 +1,26 @@
-# 队列
+---
+title: Queue
+date: 2023-05-03
+tags:
+ - Stack
+categories: data-structure
+---
-## 一、前言
+
+
+> 队列(queue)是一种采用先进先出(FIFO)策略的抽象数据结构,它的想法来自于生活中排队的策略。顾客在付款结账的时候,按照到来的先后顺序排队结账,先来的顾客先结账,后来的顾客后结账。
-队列(queue)是一种采用先进先出(FIFO)策略的抽象数据结构,它的想法来自于生活中排队的策略。顾客在付款结账的时候,按照到来的先后顺序排队结账,先来的顾客先结账,后来的顾客后结账。
+## 一、前言
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
-
+
1. 队列是一个有序列表,可以用数组或是链表来实现。
2. 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
-
-
-在 FIFO 数据结构中,将`首先处理添加到队列中的第一个元素`。
-
-如上图所示,队列是典型的 FIFO 数据结构。插入(insert)操作也称作入队(enqueue),新元素始终被添加在`队列的末尾`。 删除(delete)操作也被称为出队(dequeue)。 你只能移除`第一个元素`。
-
-
## 二、基本属性
@@ -85,13 +86,11 @@ public interface MyQueue {
### 3.1 基于数组实现的队列
-- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中 capacity是该队列的最大容量。
-- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标, **front 会随着数据输出而改变,而 rear 则是随着数据输入而改变**,如图所示:
-
-
+- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中 capacity 是该队列的最大容量。
+- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标, **front 会随着数据输出而改变,而 rear 则是随着数据输入而改变**、
- 当我们将数据存入队列时的处理需要有两个步骤:
- 1. 将尾指针往后移:`rear+1` , 当 `front == rear`队列为空
+ 1. 将尾指针往后移:`rear+1` , 当 `front == rear` 队列为空
2. 若尾指针 rear 小于队列的最大下标 `capacity-1`,则将数据存入 rear 所指的数组元素中,否则无法存入数据,即队列满了。
```java
@@ -149,15 +148,17 @@ public class MyArrayQueue implements MyQueue {
}
```
+### 3.2 基于链表实现的队列
+链表实现的队列没有固定的大小限制,可以动态地添加更多的元素。这种方式更加灵活,有效避免了空间浪费,但其操作可能比数组实现稍微复杂一些,因为需要处理节点之间的链接。
-**缺点**
-上面的实现很简单,但在某些情况下效率很低。
-假设我们分配一个最大长度为 5 的数组。当队列满时,我们想要循环利用的空间的话,在执行取出队首元素的时候,我们就必须将数组中其他所有元素都向前移动一个位置,时间复杂度就变成了 $O(n)$
+## 四、队列的变体
+上面的实现很简单,但在某些情况下效率很低。
+假设我们分配一个最大长度为 5 的数组。当队列满时,我们想要循环利用的空间的话,在执行取出队首元素的时候,我们就必须将数组中其他所有元素都向前移动一个位置,时间复杂度就变成了 $O(n)$

@@ -170,7 +171,7 @@ public class MyArrayQueue implements MyQueue {
为了提高运算的效率,我们用另一种方式来表达数组中各单元的位置关系,将数组看做是一个环形的。当 rear 到达数组的最大下标时,重新指回数组下标为`0`的位置,这样就避免了数据迁移的低效率问题。
-
+
用循环数组实现的队列称为循环队列,我们将循环队列中从对首到队尾的元素按逆时针方向存放在循环数组中的一段连续的单元中。当新元素入队时,将队尾指针 rear 按逆时针方向移动一位即可,出队操作也很简单,只要将对首指针 front 逆时针方向移动一位即可。
@@ -435,9 +436,13 @@ public class MyPriorityQueue {
-## 队列的应用
+## 五、队列的应用
+队列在计算机科学的许多领域都有应用,包括:
+- **操作系统**:在多任务处理和调度中,队列用来管理进程执行的顺序。
+- **网络**:在数据包的传输中,队列帮助管理数据包的发送顺序和处理。
+- **算法**:在广度优先搜索(BFS)等算法中,队列用于存储待处理的节点。
diff --git a/docs/data-structure-algorithms/Skip-List.md b/docs/data-structure-algorithms/data-structure/Skip-List.md
similarity index 99%
rename from docs/data-structure-algorithms/Skip-List.md
rename to docs/data-structure-algorithms/data-structure/Skip-List.md
index dacfdfde16..ad767655f8 100644
--- a/docs/data-structure-algorithms/Skip-List.md
+++ b/docs/data-structure-algorithms/data-structure/Skip-List.md
@@ -1,4 +1,10 @@
-# 跳表
+---
+title: 跳表
+date: 2023-05-09
+tags:
+ - Skip List
+categories: data-structure
+---

diff --git a/docs/data-structure-algorithms/data-structure/Stack.md b/docs/data-structure-algorithms/data-structure/Stack.md
new file mode 100644
index 0000000000..4e8996f3a0
--- /dev/null
+++ b/docs/data-structure-algorithms/data-structure/Stack.md
@@ -0,0 +1,546 @@
+---
+title: Stack
+date: 2023-05-09
+tags:
+ - Stack
+categories: data-structure
+---
+
+
+
+> 栈(stack)又名堆栈,它是**一种运算受限的线性表**。 限定仅在表尾进行插入和删除操作的线性表。
+
+
+
+## 一、概述
+
+### 定义
+
+注意:本文所说的栈是数据结构中的栈,而不是内存模型中栈。
+
+栈(stack)是限定仅在表尾一端进行插入或删除操作的**特殊线性表**。又称为堆栈。
+
+对于栈来说, 允许进行插入或删除操作的一端称为栈顶(top),而另一端称为栈底(bottom)。不含元素栈称为空栈,向栈中插入一个新元素称为入栈或压栈, 从栈中删除一个元素称为出栈或退栈。
+
+假设有一个栈S=(a1, a2, …, an),a1先进栈, an最后进栈。称 a1 为栈底元素,an 为栈顶元素。出栈时只允许在栈顶进行,所以 an 先出栈,a1最后出栈。因此又称栈为后进先出(Last In First Out,LIFO)的线性表。
+
+栈(stack),是一种线性存储结构,它有以下几个特点:
+
+- 栈中数据是按照"后进先出(LIFO, Last In First Out)"方式进出栈的。
+- 向栈中添加/删除数据时,只能从栈顶进行操作。
+
+
+
+
+
+### 基本操作
+
+栈的基本操作除了进栈 `push()`,出栈 `pop()` 之外,还有判空 `isEmpty()`、取栈顶元素 `peek()` 等操作。
+
+抽象成接口如下:
+
+```java
+public interface MyStack {
+
+ /**
+ * 返回堆栈的大小
+ */
+ public int getSize();
+
+ /**
+ * 判断堆栈是否为空
+ */
+ public boolean isEmpty();
+
+ /**
+ * 入栈
+ */
+ public void push(Object e);
+
+ /**
+ * 出栈,并删除
+ */
+ public Object pop();
+
+ /**
+ * 返回栈顶元素
+ */
+ public Object peek();
+}
+```
+
+
+
+和线性表类似,栈也有两种存储结构:顺序存储和链式存储。
+
+实际上,栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,我们叫作**顺序栈**,用链表实现的栈,我们叫作**链式栈**。
+
+## 二、栈的顺序存储与实现
+
+顺序栈是使用顺序存储结构实现的栈,即利用一组地址连续的存储单元依次存放栈中的数据元素。由于栈是一种特殊的线性表,因此在线性表的顺序存储结构的基础上,选择线性表的一端作为栈顶即可。那么根据数组操作的特性,选择数组下标大的一端,即线性表顺序存储的表尾来作为栈顶,此时入栈、出栈操作可以 $O(1)$ 时间完成。
+
+由于栈的操作都是在栈顶完成,因此在顺序栈的实现中需要附设一个指针 top 来动态地指示栈顶元素在数组中的位置。通常 top 可以用栈顶元素所在的数组下标来表示,`top=-1` 时表示空栈。
+
+栈在使用过程中所需的最大空间难以估计,所以,一般构造栈的时候不应设定最大容量。一种合理的做法和线性表类似,先为栈分配一个基本容量,然后在实际的使用过程中,当栈的空间不够用时再倍增存储空间。
+
+```java
+public class MyArrayStack implements MyStack {
+
+ private final int capacity = 2; //默认容量
+ private Object[] arrs; //数据元素数组
+ private int top; //栈顶指针
+
+ MyArrayStack(){
+ top = -1;
+ arrs = new Object[capacity];
+ }
+
+ public int getSize() {
+ return top + 1;
+ }
+
+ public boolean isEmpty() {
+ return top < 0;
+ }
+
+ public void push(Object e) {
+ if(getSize() >= arrs.length){
+ expandSapce(); //扩容
+ }
+ arrs[++top]=e;
+ }
+
+ private void expandSapce() {
+ Object[] a = new Object[arrs.length * 2];
+ for (int i = 0; i < arrs.length; i++) {
+ a[i] = arrs[i];
+ }
+ arrs = a;
+ }
+
+ public Object pop() {
+ if(getSize()<1){
+ throw new RuntimeException("栈为空");
+ }
+ Object obj = arrs[top];
+ arrs[top--] = null;
+ return obj;
+ }
+
+ public Object peek() {
+ if(getSize()<1){
+ throw new RuntimeException("栈为空");
+ }
+ return arrs[top];
+ }
+}
+```
+
+以上基于数据实现的栈代码并不难理解。由于有 top 指针的存在,所以`size()`、`isEmpty()`方法均可在 $O(1) $ 时间内完成。`push()`、`pop()`和`peek()`方法,除了需要`ensureCapacity()`外,都执行常数基本操作,因此它们的运行时间也是 $O(1)$
+
+
+
+## 三、栈的链式存储与实现
+
+栈的链式存储即采用链表实现栈。当采用单链表存储线性表后,根据单链表的操作特性选择单链表的头部作为栈顶,此时,入栈和出栈等操作可以在 $O(1)$ 时间内完成。
+
+由于栈的操作只在线性表的一端进行,在这里使用带头结点的单链表或不带头结点的单链表都可以。使用带头结点的单链表时,结点的插入和删除都在头结点之后进行;使用不带头结点的单链表时,结点的插入和删除都在链表的首结点上进行。
+
+下面以不带头结点的单链表为例实现栈,如下示意图所示:
+
+
+
+在上图中,top 为栈顶结点的引用,始终指向当前栈顶元素所在的结点。若 top 为null,则表示空栈。入栈操作是在 top 所指结点之前插入新的结点,使新结点的 next 域指向 top,top 前移即可;出栈则直接让 top 后移即可。
+
+```java
+public class MyLinkedStack implements MyStack {
+
+ class Node {
+ private Object element;
+ private Node next;
+
+ public Node() {
+ this(null, null);
+ }
+
+ public Node(Object ele, Node next) {
+ this.element = ele;
+ this.next = next;
+ }
+
+ public Node getNext() {
+ return next;
+ }
+
+ public void setNext(Node next) {
+ this.next = next;
+ }
+
+ public Object getData() {
+ return element;
+ }
+
+ public void setData(Object obj) {
+ element = obj;
+ }
+ }
+
+ private Node top;
+ private int size;
+
+ public MyLinkedStack() {
+ top = null;
+ size = 0;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public boolean isEmpty() {
+ return size == 0;
+ }
+
+ public void push(Object e) {
+ Node node = new Node(e, top);
+ top = node;
+ size++;
+ }
+
+ public Object pop() {
+ if (size < 1) {
+ throw new RuntimeException("堆栈为空");
+ }
+ Object obj = top.getData();
+ top = top.getNext();
+ size--;
+ return obj;
+ }
+
+ public Object peek() {
+ if (size < 1) {
+ throw new RuntimeException("堆栈为空");
+ }
+ return top.getData();
+ }
+}
+```
+
+上述 `MyLinkedStack` 类中有两个成员变量,其中 `top` 表示首结点,也就是栈顶元素所在的结点;`size` 指示栈的大小,即栈中数据元素的个数。不难理解,所有的操作均可以在 $O(1)$ 时间内完成。
+
+
+
+## 四、JDK 中的栈实现 Stack
+
+Java 工具包中的 Stack 是继承于 Vector(矢量队列)的,由于 Vector 是通过数组实现的,这就意味着,Stack 也是通过数组实现的,而非链表。当然,我们也可以将 LinkedList 当作栈来使用。
+
+### Stack的继承关系
+
+```java
+java.lang.Object
+ java.util.AbstractCollection
+ java.util.AbstractList
+ java.util.Vector
+ java.util.Stack
+
+public class Stack extends Vector {}
+```
+
+
+
+
+
+## 五、栈应用
+
+栈有一个很重要的应用,在程序设计语言里实现了递归。
+
+### [20. 有效的括号](https://leetcode.cn/problems/valid-parentheses/)
+
+>给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串,判断字符串是否有效。
+>
+>有效字符串需满足:
+>
+>1. 左括号必须用相同类型的右括号闭合。
+>2. 左括号必须以正确的顺序闭合。
+>
+>注意空字符串可被认为是有效字符串。
+>
+>```
+>输入: "{[]}"
+>输出: true
+>输入: "([)]"
+>输出: false
+>```
+
+**思路**
+
+左括号进栈,右括号匹配出栈
+
+- 栈先入后出特点恰好与本题括号排序特点一致,即若遇到左括号时,把右括号入栈,遇到右括号时将对应栈顶元素与其对比并出栈,相同的话说明匹配,继续遍历,遍历完所有括号后 `stack` 仍然为空,说明是有效的。
+
+```java
+ public boolean isValid(String s) {
+ if(s.isEmpty()) {
+ return true;
+ }
+ Stack stack=new Stack();
+ //字符串转为字符串数组 遍历
+ for(char c:s.toCharArray()){
+ if(c=='(') {
+ stack.push(')');
+ } else if(c=='{') {
+ stack.push('}');
+ } else if(c=='[') {
+ stack.push(']');
+ } else if(stack.empty()||c!=stack.pop()) {
+ return false;
+ }
+ }
+ return stack.empty();
+ }
+```
+
+
+
+### [739. 每日温度](https://leetcode.cn/problems/daily-temperatures/)
+
+>给定一个整数数组 `temperatures` ,表示每天的温度,返回一个数组 `answer` ,其中 `answer[i]` 是指对于第 `i`天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 `0` 来代替。
+>
+>```
+>输入: temperatures = [73,74,75,71,69,72,76,73]
+>输出: [1,1,4,2,1,1,0,0]
+>```
+
+**思路**:
+
+维护一个存储下标的单调栈,从栈底到栈顶的下标对应的温度列表中的温度依次递减。如果一个下标在单调栈里,则表示尚未找到下一次温度更高的下标。
+
+正向遍历温度列表。
+
+1. 栈为空,先入栈
+2. 如果栈内有元素,用栈顶元素对应的温度和当前温度比较,temperatures[i] > temperatures[pre] 的话,就把栈顶元素移除,把 pre 对应的天数设置为 i-pre,重复操作区比较,知道栈为空或者栈顶元素对应的温度小于当前问题,把当前温度索引入栈
+
+```java
+ public int[] dailyTemperatures_stack(int[] temperatures) {
+ int length = temperatures.length;
+ int[] result = new int[length];
+ Stack stack = new Stack<>();
+ for (int i = 0; i < length; i++) {
+ while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
+ int pre = stack.pop();
+ result[pre] = i - pre;
+ }
+ stack.add(i);
+ }
+ return result;
+ }
+```
+
+
+
+### [150. 逆波兰表达式求值](https://leetcode.cn/problems/evaluate-reverse-polish-notation/)
+
+> 给你一个字符串数组 `tokens` ,表示一个根据 [逆波兰表示法](https://baike.baidu.com/item/逆波兰式/128437) 表示的算术表达式。
+>
+> 请你计算该表达式。返回一个表示表达式值的整数。
+>
+> **注意:**
+>
+> - 有效的算符为 `'+'`、`'-'`、`'*'` 和 `'/'` 。
+> - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
+> - 两个整数之间的除法总是 **向零截断** 。
+> - 表达式中不含除零运算。
+> - 输入是一个根据逆波兰表示法表示的算术表达式。
+> - 答案及所有中间计算结果可以用 **32 位** 整数表示。
+>
+> ```
+> 输入:tokens = ["4","13","5","/","+"]
+> 输出:6
+> 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
+> ```
+
+> **逆波兰表达式:**
+>
+> 逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
+>
+> - 平常使用的算式则是一种中缀表达式,如 `( 1 + 2 ) * ( 3 + 4 )` 。
+> - 该算式的逆波兰表达式写法为 `( ( 1 2 + ) ( 3 4 + ) * )` 。
+>
+> 逆波兰表达式主要有以下两个优点:
+>
+> - 去掉括号后表达式无歧义,上式即便写成 `1 2 + 3 4 + * `也可以依据次序计算出正确结果。
+> - 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
+
+**思路**:逆波兰表达式严格遵循「从左到右」的运算。
+
+计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作:
+
+- 如果遇到操作数,则将操作数入栈;
+
+- 如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。
+
+整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。
+
+```java
+ public int evalRPN(String[] tokens) {
+ List charts = Arrays.asList("+", "-", "*", "/");
+ Stack stack = new Stack<>();
+ for (String s : tokens) {
+ if (!charts.contains(s)) {
+ stack.push(Integer.parseInt(s));
+ } else {
+ int num2 = stack.pop();
+ int num1 = stack.pop();
+ switch (s) {
+ case "+":
+ stack.push(num1 + num2);
+ break;
+ case "-":
+ stack.push(num1 - num2);
+ break;
+ case "*":
+ stack.push(num1 * num2);
+ break;
+ case "/":
+ stack.push(num1 / num2);
+ break;
+ }
+ }
+ }
+ return stack.peek();
+ }
+}
+```
+
+
+
+### [155. 最小栈](https://leetcode.cn/problems/min-stack/)
+
+> 设计一个支持 `push` ,`pop` ,`top` 操作,并能在常数时间内检索到最小元素的栈。
+>
+> 实现 `MinStack` 类:
+>
+> - `MinStack()` 初始化堆栈对象。
+> - `void push(int val)` 将元素val推入堆栈。
+> - `void pop()` 删除堆栈顶部的元素。
+> - `int top()` 获取堆栈顶部的元素。
+> - `int getMin()` 获取堆栈中的最小元素。
+
+**思路**: 添加一个辅助栈,这个栈同时保存的是每个数字 `x` 进栈的时候的**值 与 插入该值后的栈内最小值**。
+
+```java
+import java.util.Stack;
+
+public class MinStack {
+
+ // 数据栈
+ private Deque data;
+ // 辅助栈
+ private Deque helper;
+
+ public MinStack() {
+ data = new LinkedList();
+ helper = new LinkedList();
+ helper.push(Integer.MAX_VALUE);
+ }
+
+ public void push(int x) {
+ data.push(x);
+ helper.push(Math.min(helper.peek(), x));
+ }
+
+ public void pop() {
+ data.pop();
+ helper.pop();
+ }
+
+ public int top() {
+ return data.peek();
+ }
+
+ public int getMin() {
+ return helper.peek();
+ }
+
+}
+```
+
+
+
+### [227. 基本计算器 II](https://leetcode.cn/problems/basic-calculator-ii/)
+
+> 给你一个字符串表达式 `s` ,请你实现一个基本计算器来计算并返回它的值。
+>
+> 整数除法仅保留整数部分。
+>
+> 你可以假设给定的表达式总是有效的。所有中间结果将在 `[-231, 231 - 1]` 的范围内。
+>
+> **注意:**不允许使用任何将字符串作为数学表达式计算的内置函数,比如 `eval()` 。
+>
+> ```
+> 输入:s = "3+2*2"
+> 输出:7
+> ```
+
+**思路**:和逆波兰表达式有点像
+
+- 加号:将数字压入栈;
+- 减号:将数字的相反数压入栈;
+- 乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。
+
+> 这个得先知道怎么把字符串形式的正整数转成 int
+>
+> ```java
+> String s = "458";
+>
+> int n = 0;
+> for (int i = 0; i < s.length(); i++) {
+> char c = s.charAt(i);
+> n = 10 * n + (c - '0');
+> }
+> // n 现在就等于 458
+> ```
+>
+> 这个还是很简单的吧,老套路了。但是即便这么简单,依然有坑:**`(c - '0')` 的这个括号不能省略,否则可能造成整型溢出**。
+>
+> 因为变量 `c` 是一个 ASCII 码,如果不加括号就会先加后减,想象一下 `s` 如果接近 INT_MAX,就会溢出。所以用括号保证先减后加才行。
+
+```java
+ public static int calculate(String s) {
+ Stack stack = new Stack<>();
+ int length = s.length();
+ int num = 0;
+ char operator = '+';
+ for (int i = 0; i < length; i++) {
+ if (Character.isDigit(s.charAt(i))) {
+ // 转为 int
+ num = num * 10 + (s.charAt(i) - '0');
+ }
+ // 计算符 (排除空格)
+ if (!Character.isDigit(s.charAt(i)) && s.charAt(i) != ' ' || i == length - 1) {
+ // switch (s.charAt(i)){ 这里是要和操作符比对,考虑第一次
+ switch (operator) {
+ case '+':
+ stack.push(num);
+ break;
+ case '-':
+ stack.push(-num);
+ break;
+ case '*':
+ stack.push(stack.pop() * num);
+ break;
+ default:
+ stack.push(stack.pop() / num);
+ }
+ operator = s.charAt(i);
+ num = 0;
+ }
+ }
+ int result = 0;
+ while (!stack.isEmpty()) {
+ result += stack.pop();
+ }
+ return result;
+ }
+```
+
diff --git a/docs/data-structure-algorithms/soultion/.DS_Store b/docs/data-structure-algorithms/soultion/.DS_Store
index 286a850337..57bc0bb425 100644
Binary files a/docs/data-structure-algorithms/soultion/.DS_Store and b/docs/data-structure-algorithms/soultion/.DS_Store differ
diff --git a/docs/data-structure-algorithms/soultion/Array-Solution.md b/docs/data-structure-algorithms/soultion/Array-Solution.md
index 0143da45ea..1462dee476 100755
--- a/docs/data-structure-algorithms/soultion/Array-Solution.md
+++ b/docs/data-structure-algorithms/soultion/Array-Solution.md
@@ -1,5 +1,326 @@
+---
+title: 数组-热题
+date: 2025-01-08
+tags:
+ - Array
+ - algorithms
+categories: leetcode
+---
+
+
+
+> **导读**:数组是最基础的数据结构,也是面试中最高频的考点。数组题目看似简单,但往往需要巧妙的算法技巧才能高效解决。掌握双指针、滑动窗口、前缀和等核心技巧是解决数组问题的关键。
+>
+> **关键词**:双指针、滑动窗口、前缀和、哈希表、排序
+
+
+### 📋 分类索引
+
+1. **🔥 双指针技巧类**:[三数之和](#_15-三数之和)、[盛最多水的容器](#_11-盛最多水的容器)、[移动零](#_283-移动零)、[颜色分类](#_75-颜色分类)、[接雨水](#_42-接雨水)
+2. **🔍 哈希表优化类**:[两数之和](#_1-两数之和)、[字母异位词分组](#_49-字母异位词分组)、[最长连续序列](#_128-最长连续序列)、[存在重复元素](#_217-存在重复元素)
+3. **🪟 滑动窗口类**:[无重复字符的最长子串](#_3-无重复字符的最长子串)、最小覆盖子串
+4. **📊 前缀和技巧类**:[和为K的子数组](#_560-和为-k-的子数组)、[最大子数组和](#_53-最大子数组和)、[除自身以外数组的乘积](#_238-除自身以外数组的乘积)
+5. **🔄 排序与搜索类**:[合并区间](#_56-合并区间)、[数组中的第K个最大元素](#_215-数组中的第k个最大元素)、[寻找重复数](#_287-寻找重复数)、[下一个排列](#_31-下一个排列)、[搜索旋转排序数组](#_33-搜索旋转排序数组)
+6. **⚡ 原地算法类**:[合并两个有序数组](#_88-合并两个有序数组)、[旋转数组](#_189-旋转数组)
+7. **🧮 数学技巧类**:[只出现一次的数字](#_136-只出现一次的数字)、[多数元素](#_169-多数元素)、[找到所有数组中消失的数字](#_448-找到所有数组中消失的数字)、[缺失的第一个正数](#_41-缺失的第一个正数)
+8. **🎯 综合应用类**:[买卖股票的最佳时机](#_121-买卖股票的最佳时机)、[乘积最大子数组](#_152-乘积最大子数组)、[子集](#_78-子集)、[跳跃游戏](#_55-跳跃游戏)、[跳跃游戏 II](#_45-跳跃游戏-ii)、[最长公共前缀](#_14-最长公共前缀)、[螺旋矩阵](#_54-螺旋矩阵)
+
+### 🎯 核心考点概览
+
+- **双指针技巧**:快慢指针、左右指针、滑动窗口
+- **哈希表优化**:空间换时间,$O(1)$查找
+- **前缀和技巧**:区间求和,子数组问题
+- **排序与搜索**:二分查找、快速选择
+- **原地算法**:$O(1)$空间复杂度优化
+
+### 📝 解题万能模板
+
+#### 基础模板
+
+```java
+// 双指针模板
+int left = 0, right = nums.length - 1;
+while (left < right) {
+ if (condition) {
+ left++;
+ } else {
+ right--;
+ }
+}
+
+// 滑动窗口模板
+int left = 0, right = 0;
+while (right < nums.length) {
+ // 扩展窗口
+ window.add(nums[right]);
+ right++;
+
+ while (window needs shrink) {
+ // 收缩窗口
+ window.remove(nums[left]);
+ left++;
+ }
+}
+
+// 前缀和模板
+int[] prefixSum = new int[nums.length + 1];
+for (int i = 0; i < nums.length; i++) {
+ prefixSum[i + 1] = prefixSum[i] + nums[i];
+}
+```
+
+#### 边界检查清单
+
+- ✅ 空数组:`if (nums == null || nums.length == 0) return ...`
+- ✅ 单元素:`if (nums.length == 1) return ...`
+- ✅ 数组越界:确保索引在有效范围内
+- ✅ 整数溢出:使用long或检查边界
+
+#### 💡 记忆口诀(朗朗上口版)
+
+- **双指针**:"左右夹逼找目标,快慢追踪解环题"
+- **滑动窗口**:"右扩左缩维护窗,条件满足就更新"
+- **前缀和**:"累积求和建数组,区间相减得答案"
+- **哈希表**:"空间换时间来优化,常数查找是关键"
+- **二分查找**:"有序数组用二分,对数时间最高效"
+- **原地算法**:"双指针巧妙来交换,空间复杂度为一"
+
+**最重要的一个技巧就是,你得行动,写起来**
+
+
+
+## 一、双指针技巧类(核心中的核心)🔥
+
+**💡 核心思想**
+
+- **左右指针**:从两端向中间逼近,解决对撞类问题
+- **快慢指针**:不同速度遍历,解决环形、去重问题
+- **滑动窗口**:双指针维护窗口,解决子数组问题
+
+**🎯 必掌握模板**
+
+```java
+// 左右指针模板
+int left = 0, right = nums.length - 1;
+while (left < right) {
+ if (sum < target) {
+ left++;
+ } else if (sum > target) {
+ right--;
+ } else {
+ // 找到答案
+ return ...;
+ }
+}
+```
+
+### [15. 三数之和](https://leetcode-cn.com/problems/3sum/)
+
+**🎯 考察频率:极高 | 难度:中等 | 重要性:双指针经典**
+
+> 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
+
+**💡 核心思路**:排序后双指针
+
+```java
+public List> threeSum(int[] nums) {
+ //存放结果list
+ List> result = new ArrayList<>();
+ int length = nums.length;
+ //特例判断
+ if (length < 3) {
+ return result;
+ }
+ Arrays.sort(nums);
+ for (int i = 0; i < length; i++) {
+ //排序后的第一个数字就大于0,就说明没有符合要求的结果
+ if (nums[i] > 0) break;
+
+ //去重,当起始的值等于前一个元素,那么得到的结果将会和前一次相同,不能是 nums[i] == nums[i +1 ],会造成遗漏
+ if (i > 0 && nums[i] == nums[i - 1]) continue;
+ //左右指针
+ int l = i + 1;
+ int r = length - 1;
+ while (l < r) {
+ int sum = nums[i] + nums[l] + nums[r];
+ if (sum == 0) {
+ result.add(Arrays.asList(nums[i], nums[l], nums[r]));
+ //去重(相同数字的话就移动指针)
+ //在将左指针和右指针移动的时候,先对左右指针的值,进行判断,以防[0,0,0]这样的造成数组越界
+ //不要用成 if 判断,只跳过 1 条,还会有重复的
+ while (l< r && nums[l] == nums[l + 1]) l++;
+ while (l< r && nums[r] == nums[r - 1]) r--;
+ //移动指针
+ l++;
+ r--;
+ } else if (sum < 0) l++;
+ else if (sum > 0) r--;
+ }
+ }
+ return result;
+}
+```
+
+### [11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/)
+
+**🎯 考察频率:高 | 难度:中等 | 重要性:双指针应用**
+
+> 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
+
+**💡 核心思路**:双指针
+
+水量 = 两个指针指向的数字中较小值∗指针之间的距离
+
+```java
+public int maxArea(int[] height){
+ int l = 0;
+ int r = height.length - 1;
+ int ans = 0;
+ while(l < r){
+ int area = Math.min(height[l], height[r]) * (r - l);
+ ans = Math.max(ans,area);
+ if(height[l] < height[r]){
+ l ++;
+ }else {
+ r --;
+ }
+ }
+ return ans;
+}
+```
+
+### [283. 移动零](https://leetcode-cn.com/problems/move-zeroes/)
+
+**🎯 考察频率:高 | 难度:简单 | 重要性:双指针基础**
+
+> 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
+
+**💡 核心思路**:双指针法,当快指针指向非零元素时,将其与慢指针指向的元素交换,然后慢指针向前移动一位。
+
+```java
+public void moveZero(int[] nums) {
+ int j = 0; //慢指针
+ for (int i = 0; i < nums.length; i++) {
+ if (nums[i] != 0) {
+ nums[j++] = nums[i]; // 直接将非零元素放到j指针位置,并移动j指针
+ }
+ }
+ // 将j指针之后的所有元素置为零
+ for (int i = j; i < nums.length; i++) {
+ nums[i] = 0;
+ }
+}
+```
+
+### [75. 颜色分类](https://leetcode.cn/problems/sort-colors/)
+
+**🎯 考察频率:中等 | 难度:中等 | 重要性:三指针技巧**
+
+> 给定一个包含红色(0)、白色(1)、蓝色(2)的数组 `nums`,要求你对它们进行排序,使得相同颜色相邻,并按照红、白、蓝的顺序排列。
+
+**💡 核心思路**:双指针(荷兰国旗问题)。用 3 个指针遍历数组。
+
+这题被称为 **荷兰国旗问题 (Dutch National Flag Problem)**。
+ 用 **三个指针** 解决:
+
+- `low`:下一个 0 应该放置的位置
+- `high`:下一个 2 应该放置的位置
+- `i`:当前遍历位置
+
+规则:
+
+1. 如果 `nums[i] == 0` → 和 `nums[low]` 交换,`low++,i++`
+2. 如果 `nums[i] == 1` → 正常情况,`i++`
+3. 如果 `nums[i] == 2` → 和 `nums[high]` 交换,`high--`,**但 i 不++**,因为换过来的数还要检查
+
+这样一次遍历就能完成排序。
+
+```java
+public void sortColors(int[] nums) {
+ int left = 0, right = nums.length - 1, curr = 0;
+ while (curr <= right) {
+ if (nums[curr] == 0) {
+ swap(nums, curr++, left++);
+ } else if (nums[curr] == 2) {
+ swap(nums, curr, right--);
+ } else {
+ curr++;
+ }
+ }
+}
+
+private void swap(int[] nums, int i, int j) {
+ int temp = nums[i];
+ nums[i] = nums[j];
+ nums[j] = temp;
+}
+```
+
+### [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/)
+
+**🎯 考察频率:极高 | 难度:困难 | 重要性:双指针+贪心**
+
+> 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能够接多少雨水。
+
+**💡 核心思路**:双指针法
+
+```java
+public int trap(int[] height) {
+ int left = 0, right = height.length - 1;
+ int leftMax = 0, rightMax = 0;
+ int result = 0;
+
+ while (left < right) {
+ if (height[left] < height[right]) {
+ if (height[left] >= leftMax) {
+ leftMax = height[left];
+ } else {
+ result += leftMax - height[left];
+ }
+ left++;
+ } else {
+ if (height[right] >= rightMax) {
+ rightMax = height[right];
+ } else {
+ result += rightMax - height[right];
+ }
+ right--;
+ }
+ }
+ return result;
+}
+```
+
+
+
+---
+
+## 二、哈希表优化类(空间换时间)🔍
+
+**💡 核心思想**
+
+- **O(1)查找**:利用哈希表常数时间查找特性
+- **空间换时间**:用额外空间降低时间复杂度
+- **去重与计数**:处理重复元素和频次统计
+
+**🎯 必掌握模板**
+
+```java
+// 哈希表查找模板
+Map map = new HashMap<>();
+for (int i = 0; i < nums.length; i++) {
+ if (map.containsKey(target - nums[i])) {
+ // 找到答案
+ return new int[]{map.get(target - nums[i]), i};
+ }
+ map.put(nums[i], i);
+}
+```
+
### [1. 两数之和](https://leetcode-cn.com/problems/two-sum/)
+**🎯 考察频率:极高 | 难度:简单 | 重要性:哈希表经典**
+
> 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
>
> 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
@@ -10,127 +331,734 @@
> 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
> ```
-**思路**:哈希表
+**思路**:哈希表
+
+```java
+public static int[] twoSum(int[] nums,int target){
+ Map map = new HashMap<>();
+ for (int i = 0; i < nums.length; i++) {
+ int temp = target - nums[i];
+ if(map.containsKey(temp)){
+ return new int[]{map.get(temp),i};
+ }
+ map.put(nums[i],i);
+ }
+ return new int[]{-1,-1};
+}
+```
+
+**⏱️ 复杂度分析**:
+
+- **时间复杂度**:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。
+- **空间复杂度**:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
+
+### [49. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/)
+
+**🎯 考察频率:高 | 难度:中等 | 重要性:分组统计经典**
+
+> 给你一个字符串数组,请你将 **字母异位词** 组合在一起。可以按任意顺序返回结果列表。
+>
+> **字母异位词** 是由重新排列源单词的所有字母得到的一个新单词。
+>
+> ```
+> 输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
+> 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
+> ```
+
+**思路**:哈希表。键是字符串中的字符排序后的字符串,值是具有相同键的原始字符串列表
+
+```java
+public List> groupAnagrams(String[] strs) {
+ Map> map = new HashMap<>();
+
+ for (String str : strs) {
+ // 将字符串转换为字符数组并排序
+ char[] chars = str.toCharArray();
+ Arrays.sort(chars);
+
+ // 将排序后的字符数组转换回字符串,作为哈希表的键
+ String key = new String(chars);
+
+ // 如果键不存在于哈希表中,则创建新的列表并添加到哈希表中
+ if (!map.containsKey(key)) {
+ map.put(key, new ArrayList<>());
+ }
+
+ // 将原始字符串添加到对应的列表中
+ map.get(key).add(str);
+ }
+
+ // 返回哈希表中所有值的列表
+ return new ArrayList<>(map.values());
+ }
+```
+
+- 时间复杂度:$O(nklogk)$,其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要 O(klogk) 的时间进行排序以及 O(1) 的时间更新哈希表,因此总时间复杂度是 O(nklogk)。
+
+- 空间复杂度:$O(nk)$,其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要用哈希表存储全部字符串
+
+### [217. 存在重复元素](https://leetcode-cn.com/problems/contains-duplicate/)
+
+**🎯 考察频率:简单 | 难度:简单 | 重要性:哈希表基础**
+
+> 给定一个整数数组,判断是否存在重复元素。如果存在一值在数组中出现至少两次,函数返回 `true` 。如果数组中每个元素都不相同,则返回 `false` 。
+
+**💡 核心思路**:哈希表,和两数之和的思路一样 | 或者排序
+
+```java
+public boolean containsDuplicate(int[] nums){
+ Map map = new HashMap<>();
+ for(int i=0;i 给定一个未排序的整数数组 `nums` ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
+>
+> 请你设计并实现时间复杂度为 `O(n)` 的算法解决此问题。
+>
+> ```
+> 输入:nums = [100,4,200,1,3,2]
+> 输出:4
+> 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
+> ```
+
+**思路**:哈希表,**每个数都判断一次这个数是不是连续序列的开头那个数**
+
+```java
+public int longestConsecutive(int[] nums) {
+ Set numSet = new HashSet<>();
+ int longestStreak = 0;
+
+ // 将数组中的所有数字添加到哈希表中
+ for (int num : nums) {
+ numSet.add(num);
+ }
+
+ // 遍历哈希表中的每个数字
+ for (int num : numSet) {
+ // 如果 num-1 不在哈希表中,说明 num 是一个连续序列的起点
+ if (!numSet.contains(num - 1)) {
+ int currentNum = num;
+ int currentStreak = 1;
+
+ // 检查 num+1, num+2, ... 是否在哈希表中,并计算连续序列的长度
+ while (numSet.contains(currentNum + 1)) {
+ currentNum++;
+ currentStreak++;
+ }
+
+ // 更新最长连续序列的长度
+ longestStreak = Math.max(longestStreak, currentStreak);
+ }
+ }
+
+ return longestStreak;
+}
+```
+
+### [3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/)
+
+**🎯 考察频率:极高 | 难度:中等 | 重要性:滑动窗口经典**
+
+> 给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。
+
+**💡 核心思路**:滑动窗口 + 哈希表
+
+```java
+public int lengthOfLongestSubstring(String s) {
+ Set window = new HashSet<>();
+ int left = 0, maxLen = 0;
+
+ for (int right = 0; right < s.length(); right++) {
+ char c = s.charAt(right);
+ while (window.contains(c)) {
+ window.remove(s.charAt(left));
+ left++;
+ }
+ window.add(c);
+ maxLen = Math.max(maxLen, right - left + 1);
+ }
+ return maxLen;
+}
+```
+
+**⏱️ 复杂度分析**:
+- **时间复杂度**:O(n) - 每个字符最多被访问两次(一次by right,一次by left)
+- **空间复杂度**:O(min(m,n)) - m是字符集大小,n是字符串长度
+
+
+---
+
+## 三、前缀和技巧类(区间计算利器)📊
+
+**💡 核心思想**
+
+- **累积求和**:预处理前缀和数组,快速计算区间和
+- **差值技巧**:利用前缀和差值解决子数组问题
+- **哈希优化**:结合哈希表优化前缀和查找
+
+**🎯 必掌握模板**
+
+```java
+// 前缀和模板
+int[] prefixSum = new int[nums.length + 1];
+for (int i = 0; i < nums.length; i++) {
+ prefixSum[i + 1] = prefixSum[i] + nums[i];
+}
+// 区间[i,j]的和 = prefixSum[j+1] - prefixSum[i]
+
+// 前缀和+哈希表模板
+Map map = new HashMap<>();
+map.put(0, 1); // 前缀和为0出现1次
+int prefixSum = 0;
+for (int num : nums) {
+ prefixSum += num;
+ if (map.containsKey(prefixSum - k)) {
+ count += map.get(prefixSum - k);
+ }
+ map.put(prefixSum, map.getOrDefault(prefixSum, 0) + 1);
+}
+```
+
+### [560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/)
+
+**🎯 考察频率:极高 | 难度:中等 | 重要性:前缀和+哈希表经典**
+
+> 给你一个整数数组 `nums` 和一个整数 `k` ,请你统计并返回 *该数组中和为 `k` 的子数组的个数* 。
+>
+> 子数组是数组中元素的连续非空序列。
+>
+> ```
+> 输入:nums = [1,2,3], k = 3
+> 输出:2
+> ```
+
+思路:**前缀和 + 哈希表**
+
+1. **遍历数组**,逐步计算前缀和(表示从数组起始位置到当前位置的所有元素之和) `prefixSum`。
+2. 检查哈希表中是否存在 `prefixSum - k`:
+ - 若存在,累加其出现次数到结果 `count`。
+3. **更新哈希表**:将当前前缀和存入哈希表,若已存在则次数加 1。
+
+```java
+public int subarraySum(int[] nums, int k) {
+ int count = 0;
+ int prefixSum = 0;
+ Map