diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2f8c0b9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://cdn.jsdelivr.net/gh/yuanguangxin/Curriculum-Vitae/funding.jpg"] diff --git a/.gitignore b/.gitignore index 8ede3c2..c445fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,27 +4,7 @@ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries +.idea/ # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 5c98b42..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Default ignored files -/workspace.xml \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 6560a98..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/markdown-doclet.xml b/.idea/markdown-doclet.xml deleted file mode 100644 index a0355e9..0000000 --- a/.idea/markdown-doclet.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 0548357..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8fd8bd1..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index e96534f..0000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 0d36467..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,802 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - 1577620314304 - - - 1581270301073 - - - 1581441609925 - - - 1581519773712 - - - 1581704713975 - - - 1581705212871 - - - 1581705262462 - - - 1581705659513 - - - 1581851491537 - - - 1581855774959 - - - 1581857522107 - - - 1581858072286 - - - 1581858428131 - - - 1581954761859 - - - 1581954878045 - - - 1581954948951 - - - 1581954955102 - - - 1581956318327 - - - 1581957184398 - - - 1581957264975 - - - 1582020729706 - - - 1582020913236 - - - 1582641766540 - - - 1582643176570 - - - 1582708681247 - - - 1582711917874 - - - 1582718525647 - - - 1582723258830 - - - 1582888928990 - - - 1582901003378 - - - 1582962309100 - - - 1582971922719 - - - 1582971955761 - - - 1582972061183 - - - 1583497819094 - - - 1583851879205 - - - 1584289863273 - - - 1584416595529 - - - 1585027889054 - - - 1585041706193 - - - 1585108622120 - - - 1585220219370 - - - 1585226292787 - - - 1585230484320 - - - 1585230789181 - - - 1585232616596 - - - 1585234195148 - - - 1585657673868 - - - 1585658308922 - - - 1585658338742 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index cef71bb..e7f0b9b 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,139 @@ # LeetCode题目分类与面试问题整理 +> [English edition](/README_EN.md) + ## 题目分类 ### Hash相关 -* [q1_两数之和](/src/hash相关/q1_两数之和) +- [q1_两数之和](/src/hash相关/q1_两数之和) +- [q387_字符串中的第一个唯一字符](/src/hash相关/q387_字符串中的第一个唯一字符) ### 链表操作 -* [q2_两数相加](/src/链表操作/q2_两数相加) -* [q19_删除链表的倒数第N个节点](/src/链表操作/q19_删除链表的倒数第N个节点) -* [q61_旋转链表](/src/链表操作/q61_旋转链表) -* [q138_复制带随机指针的链表](/src/链表操作/q138_复制带随机指针的链表) -* [q206_反转链表](/src/链表操作/q206_反转链表) +- [q2_两数相加](/src/链表操作/q2_两数相加) +- [q19_删除链表的倒数第N个节点](/src/链表操作/q19_删除链表的倒数第N个节点) +- [q25_k个一组翻转链表](/src/链表操作/q25_k个一组翻转链表) +- [q61_旋转链表](/src/链表操作/q61_旋转链表) +- [q138_复制带随机指针的链表](/src/链表操作/q138_复制带随机指针的链表) +- [q160_相交链表](/src/链表操作/q160_相交链表) +- [q206_反转链表](/src/链表操作/q206_反转链表) ### 双指针遍历/滑动窗口 -* [q3_无重复字符的最长子串](/src/双指针遍历/q3_无重复字符的最长子串) -* [q11_盛最多水的容器](/src/双指针遍历/q11_盛最多水的容器) -* [q15_三数之和](/src/双指针遍历/q15_三数之和) -* [q16_最接近的三数之和](/src/双指针遍历/q16_最接近的三数之和) -* [q26_删除排序数组中的重复项](/src/双指针遍历/q26_删除排序数组中的重复项) -* [q42_接雨水](/src/双指针遍历/q42_接雨水) -* [q121_买卖股票的最佳时机](/src/双指针遍历/q121_买卖股票的最佳时机) -* [q209_长度最小的子数组](/src/双指针遍历/q209_长度最小的子数组) +- [q3_无重复字符的最长子串](/src/双指针遍历/q3_无重复字符的最长子串) +- [q11_盛最多水的容器](/src/双指针遍历/q11_盛最多水的容器) +- [q15_三数之和](/src/双指针遍历/q15_三数之和) +- [q16_最接近的三数之和](/src/双指针遍历/q16_最接近的三数之和) +- [q26_删除排序数组中的重复项](/src/双指针遍历/q26_删除排序数组中的重复项) +- [q42_接雨水](/src/双指针遍历/q42_接雨水) +- [q121_买卖股票的最佳时机](/src/双指针遍历/q121_买卖股票的最佳时机) +- [q209_长度最小的子数组](/src/双指针遍历/q209_长度最小的子数组) ### 快慢指针遍历 -* [q141_环形链表](/src/快慢指针遍历/q141_环形链表) -* [q202_快乐数](/src/快慢指针遍历/q202_快乐数) -* [q876_链表的中间结点](/src/快慢指针遍历/q876_链表的中间结点) +- [q141_环形链表](/src/快慢指针遍历/q141_环形链表) +- [q202_快乐数](/src/快慢指针遍历/q202_快乐数) +- [q876_链表的中间结点](/src/快慢指针遍历/q876_链表的中间结点) ### 区间合并 -* [q56_合并区间](/src/区间合并/q56_合并区间) +- [q56_合并区间](/src/区间合并/q56_合并区间) ### 字符串操作 -* [q6_Z字形变换](/src/字符串操作/q6_Z字形变换) -* [q14_最长公共前缀](/src/字符串操作/q14_最长公共前缀) -* [q736_划分字母区间](/src/字符串操作/q736_划分字母区间) +- [q6_Z字形变换](/src/字符串操作/q6_Z字形变换) +- [q14_最长公共前缀](/src/字符串操作/q14_最长公共前缀) +- [q763_划分字母区间](/src/字符串操作/q763_划分字母区间) ### 数字操作 -* [q7_整数反转](/src/数字操作/q7_整数反转) -* [q8_字符串转换整数](/src/数字操作/q8_字符串转换整数) -* [q9_回文数](/src/数字操作/q9_回文数) -* [q43_字符串相乘](/src/数字操作/q43_字符串相乘) -* [q172_阶乘后的零](/src/数字操作/q172_阶乘后的零) -* [q258_各位相加](/src/数字操作/q258_各位相加) +- [q7_整数反转](/src/数字操作/q7_整数反转) +- [q8_字符串转换整数](/src/数字操作/q8_字符串转换整数) +- [q9_回文数](/src/数字操作/q9_回文数) +- [q43_字符串相乘](/src/数字操作/q43_字符串相乘) +- [q172_阶乘后的零](/src/数字操作/q172_阶乘后的零) +- [q258_各位相加](/src/数字操作/q258_各位相加) +- [q1920_基于排列构建数组](/src/数字操作/q1920_基于排列构建数组) ### 数组操作 -* [q54_螺旋矩阵](/src/数组操作/q54_螺旋矩阵) -* [q73_矩阵置零](/src/数组操作/q73_矩阵置零) -* [q945_使数组唯一的最小增量](/src/数组操作/q945_使数组唯一的最小增量) +- [q54_螺旋矩阵](/src/数组操作/q54_螺旋矩阵) +- [q73_矩阵置零](/src/数组操作/q73_矩阵置零) +- [q78_子集](/src/数组操作/q78_子集) +- [q384_打乱数组](/src/数组操作/q384_打乱数组) +- [q581_最短无序连续子数组](/src/数组操作/q581_最短无序连续子数组) +- [q945_使数组唯一的最小增量](/src/数组操作/q945_使数组唯一的最小增量) ### 栈相关 -* [q20_有效的括号](/src/栈相关/q20_有效的括号) -* [q32_最长有效括号](/src/栈相关/q32_最长有效括号) -* [q155_最小栈](/src/栈相关/q155_最小栈) -* [q224_基本计算器](/src/栈相关/q224_基本计算器) -* [q316_去除重复字母](/src/栈相关/q316_去除重复字母) +- [q20_有效的括号](/src/栈相关/q20_有效的括号) +- [q32_最长有效括号](/src/栈相关/q32_最长有效括号) +- [q155_最小栈](/src/栈相关/q155_最小栈) +- [q224_基本计算器](/src/栈相关/q224_基本计算器) +- [q232_用栈实现队列](/src/栈相关/q232_用栈实现队列) +- [q316_去除重复字母](/src/栈相关/q316_去除重复字母) ### 堆相关 -* [q215_数组中的第K个最大元素](/src/堆相关/q215_数组中的第K个最大元素) -* [q347_前K个高频元素](/src/堆相关/q347_前K个高频元素) +- [q215_数组中的第K个最大元素](/src/堆相关/q215_数组中的第K个最大元素) +- [q347_前K个高频元素](/src/堆相关/q347_前K个高频元素) ### 递归 -* [q21_合并两个有序链表](/src/递归/q21_合并两个有序链表) -* [q101_对称二叉树](/src/递归/q101_对称二叉树) -* [q104_二叉树的最大深度](/src/递归/q104_二叉树的最大深度) -* [q226_翻转二叉树](/src/递归/q226_翻转二叉树) -* [q236_二叉树的最近公共祖先](/src/递归/q236_二叉树的最近公共祖先) +- [q21_合并两个有序链表](/src/递归/q21_合并两个有序链表) +- [q101_对称二叉树](/src/递归/q101_对称二叉树) +- [q104_二叉树的最大深度](/src/递归/q104_二叉树的最大深度) +- [q226_翻转二叉树](/src/递归/q226_翻转二叉树) +- [q236_二叉树的最近公共祖先](/src/递归/q236_二叉树的最近公共祖先) +- [q1325_删除给定值的叶子节点](/src/递归/q1325_删除给定值的叶子节点) ### 分治法/二分法 -* [q23_合并K个排序链表](/src/分治法/q23_合并K个排序链表) -* [q33_搜索旋转排序数组](/src/分治法/q33_搜索旋转排序数组) -* [q34_在排序数组中查找元素的第一个和最后一个位置](/src/分治法/q34_在排序数组中查找元素的第一个和最后一个位置) +- [q23_合并K个排序链表](/src/分治法/q23_合并K个排序链表) +- [q33_搜索旋转排序数组](/src/分治法/q33_搜索旋转排序数组) +- [q34_在排序数组中查找元素的第一个和最后一个位置](/src/分治法/q34_在排序数组中查找元素的第一个和最后一个位置) ### 动态规划 -* [q5_最长回文子串](/src/动态规划/q5_最长回文子串) -* [q53_最大子序和](/src/动态规划/q53_最大子序和) -* [q62_不同路径](/src/动态规划/q62_不同路径) -* [q64_最小路径和](/src/动态规划/q64_最小路径和) -* [q70_爬楼梯](/src/动态规划/q70_爬楼梯) -* [q118_杨辉三角](/src/动态规划/q118_杨辉三角) -* [q300_最长上升子序列](/src/动态规划/q300_最长上升子序列) -* [q746_使用最小花费爬楼梯](/src/动态规划/q746_使用最小花费爬楼梯) -* [q1277_统计全为1的正方形子矩阵](/src/动态规划/q1277_统计全为1的正方形子矩阵) +- [q5_最长回文子串](/src/动态规划/q5_最长回文子串) +- [q53_最大子序和](/src/动态规划/q53_最大子序和) +- [q62_不同路径](/src/动态规划/q62_不同路径) +- [q64_最小路径和](/src/动态规划/q64_最小路径和) +- [q70_爬楼梯](/src/动态规划/q70_爬楼梯) +- [q118_杨辉三角](/src/动态规划/q118_杨辉三角) +- [q300_最长上升子序列](/src/动态规划/q300_最长上升子序列) +- [q1143_最长公共子序列](/src/动态规划/q1143_最长公共子序列) +- [q1277_统计全为1的正方形子矩阵](/src/动态规划/q1277_统计全为1的正方形子矩阵) ### 回溯法 -* [q10_正则表达式匹配](/src/回溯法/q10_正则表达式匹配) -* [q22_括号生成](/src/回溯法/q22_括号生成) -* [q46_全排列](/src/回溯法/q46_全排列) +- [q10_正则表达式匹配](/src/回溯法/q10_正则表达式匹配) +- [q22_括号生成](/src/回溯法/q22_括号生成) +- [q40_组合总和2](/src/回溯法/q40_组合总和2) +- [q46_全排列](/src/回溯法/q46_全排列) + +### 字典树(前缀树) + +- [q648_单词替换](/src/字典树/q648_单词替换) ### 树的遍历 -* [q94_二叉树的中序遍历](/src/树的遍历/q94_二叉树的中序遍历) -* [q102_二叉树的层次遍历](/src/树的遍历/q102_二叉树的层次遍历) -* [q110_平衡二叉树](/src/树的遍历/q110_平衡二叉树) -* [q144_二叉树的前序遍历](/src/树的遍历/q144_二叉树的前序遍历) -* [q145_二叉树的后序遍历](/src/树的遍历/q145_二叉树的后序遍历) +- [q94_二叉树的中序遍历](/src/树的遍历/q94_二叉树的中序遍历) +- [q102_二叉树的层次遍历](/src/树的遍历/q102_二叉树的层次遍历) +- [q103_二叉树的锯齿形层序遍历](/src/树的遍历/q103_二叉树的锯齿形层序遍历) +- [q110_平衡二叉树](/src/树的遍历/q110_平衡二叉树) +- [q144_二叉树的前序遍历](/src/树的遍历/q144_二叉树的前序遍历) +- [q145_二叉树的后序遍历](/src/树的遍历/q145_二叉树的后序遍历) ### 二叉搜索树相关 -* [q98_验证二叉搜索树](/src/二叉搜索树相关/q98_验证二叉搜索树) -* [q450_删除二叉搜索树中的节点](/src/二叉搜索树相关/q450_删除二叉搜索树中的节点) -* [q701_二叉搜索树中的插入操作](/src/二叉搜索树相关/q701_二叉搜索树中的插入操作) +- [q98_验证二叉搜索树](/src/二叉搜索树相关/q98_验证二叉搜索树) +- [q450_删除二叉搜索树中的节点](/src/二叉搜索树相关/q450_删除二叉搜索树中的节点) +- [q701_二叉搜索树中的插入操作](/src/二叉搜索树相关/q701_二叉搜索树中的插入操作) ------- ## 面试问题整理 -* [面试问题整理](/Rocket.md) \ No newline at end of file +- [面试问题整理](/Rocket.md) diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..e9e4f32 --- /dev/null +++ b/README_EN.md @@ -0,0 +1,139 @@ +# LeetCode Topics and Interview Questions Collection + +> [中文版](/README.md) + +## Topics + +### Hash + +- [Question 1 : Two Sum](/src/hash相关/q1_两数之和) +- [Question 387 : First Unique Character in a String](/src/hash相关/q387_字符串中的第一个唯一字符) + +### Linked List Operations + +- [Question 2 : Add Two Numbers](/src/链表操作/q2_两数相加) +- [Question 19 : Remove Nth Node From End of List](/src/链表操作/q19_删除链表的倒数第N个节点) +- [Question 25 : Reverse Nodes in k-Group](/src/链表操作/q25_k个一组翻转链表) +- [Question 61 : Rotate List](/src/链表操作/q61_旋转链表) +- [Question 138 : Copy List with Random Pointer](/src/链表操作/q138_复制带随机指针的链表) +- [Question 160 : Intersection of Two Linked Lists](/src/链表操作/q160_相交链表) +- [Question 206 : Reverse Linked List](/src/链表操作/q206_反转链表) + +### Two Pointers Traversal / Sliding Window + +- [Question 3 : Longest Substring Without Repeating Characters](/src/双指针遍历/q3_无重复字符的最长子串) +- [Question 11 : Container With Most Water](/src/双指针遍历/q11_盛最多水的容器) +- [Question 15 : 3Sum](/src/双指针遍历/q15_三数之和) +- [Question 16 : 3Sum Closest](/src/双指针遍历/q16_最接近的三数之和) +- [Question 26 : Remove Duplicates from Sorted Array](/src/双指针遍历/q26_删除排序数组中的重复项) +- [Question 42 : Trapping Rain Water](/src/双指针遍历/q42_接雨水) +- [Question 121 : Best Time to Buy and Sell Stock](/src/双指针遍历/q121_买卖股票的最佳时机) +- [Question 209 : Minimum Size Subarray Sum](/src/双指针遍历/q209_长度最小的子数组) + +### Fast and Slow Pointers Traversal + +- [Question 141 : Linked List Cycle](/src/快慢指针遍历/q141_环形链表) +- [Question 202 : Happy Number](/src/快慢指针遍历/q202_快乐数) +- [Question 876 : Middle of the Linked List](/src/快慢指针遍历/q876_链表的中间结点) + +### Interval Merge + +- [Question 56 : Merge Intervals](/src/区间合并/q56_合并区间) + +### String Manipulation + +- [Question 6 : ZigZag Conversion](/src/字符串操作/q6_Z字形变换) +- [Question 14 : Longest Common Prefix](/src/字符串操作/q14_最长公共前缀) +- [Question 763 : Partition Labels](/src/字符串操作/q763_划分字母区间) + +### Digital Operations + +- [Question 7 : Reverse Integer](/src/数字操作/q7_整数反转) +- [Question 8 : String to Integer (atoi)](/src/数字操作/q8_字符串转换整数) +- [Question 9 : Palindrome Number](/src/数字操作/q9_回文数) +- [Question 43 : Multiply Strings](/src/数字操作/q43_字符串相乘) +- [Question 172 : Factorial Trailing Zeroes](/src/数字操作/q172_阶乘后的零) +- [Question 258 : Add Digits](/src/数字操作/q258_各位相加) +- [Question 1920 : Build Array from Permutation](/src/数字操作/q1920_基于排列构建数组) + +### Array Operations + +- [Question 54 : Spiral Matrix](/src/数组操作/q54_螺旋矩阵) +- [Question 73 : Set Matrix Zeroes](/src/数组操作/q73_矩阵置零) +- [Question 78 : Subsets](/src/数组操作/q78_子集) +- [Question 384 : Shuffle an Array](/src/数组操作/q384_打乱数组) +- [Question 581 : Shortest Unsorted Continuous Subarray](/src/数组操作/q581_最短无序连续子数组) +- [Question 945 : Minimum Increment to Make Array Unique](/src/数组操作/q945_使数组唯一的最小增量) + +### Stack + +- [Question 20 : Valid Parentheses](/src/栈相关/q20_有效的括号) +- [Question 32 : Longest Valid Parentheses](/src/栈相关/q32_最长有效括号) +- [Question 155 : Min Stack](/src/栈相关/q155_最小栈) +- [Question 224 : Basic Calculator](/src/栈相关/q224_基本计算器) +- [Question 232 : Implement Queue using Stacks](/src/栈相关/q232_用栈实现队列) +- [Question 316 : Remove Duplicate Letters](/src/栈相关/q316_去除重复字母) + +### Heap + +- [Question 215 : Kth Largest Element in an Array](/src/堆相关/q215_数组中的第K个最大元素) +- [Question 347 : Top K Frequent Elements](/src/堆相关/q347_前K个高频元素) + +### Recursion + +- [Question 21 : Merge Two Sorted Lists](/src/递归/q21_合并两个有序链表) +- [Question 101 : Symmetric Tree](/src/递归/q101_对称二叉树) +- [Question 104 : Maximum Depth of Binary Tree](/src/递归/q104_二叉树的最大深度) +- [Question 226 : Invert Binary Tree](/src/递归/q226_翻转二叉树) +- [Question 236 : Lowest Common Ancestor of a Binary Tree](/src/递归/q236_二叉树的最近公共祖先) +- [Question 1325 : Delete Leaves With a Given Value](/src/递归/q1325_删除给定值的叶子节点) + +### Divide and Conquer / Dichotomy + +- [Question 23 : Merge k Sorted Lists](/src/分治法/q23_合并K个排序链表) +- [Question 33 : Search in Rotated Sorted Array](/src/分治法/q33_搜索旋转排序数组) +- [Question 34 : Find First and Last Position of Element in Sorted Array](/src/分治法/q34_在排序数组中查找元素的第一个和最后一个位置) + +### Dynamic Programming + +- [Question 5 : Longest Palindromic Substring](/src/动态规划/q5_最长回文子串) +- [Question 53 : Maximum Subarray](/src/动态规划/q53_最大子序和) +- [Question 62 : Unique Paths](/src/动态规划/q62_不同路径) +- [Question 64 : Minimum Path Sum](/src/动态规划/q64_最小路径和) +- [Question 70 : Climbing Stairs](/src/动态规划/q70_爬楼梯) +- [Question 118 : Pascal's Triangle](/src/动态规划/q118_杨辉三角) +- [Question 300 : Longest Increasing Subsequence](/src/动态规划/q300_最长上升子序列) +- [Question 1143 : Longest Common Subsequence](/src/动态规划/q1143_最长公共子序列) +- [Question 1277 : Count Square Submatrices with All One](/src/动态规划/q1277_统计全为1的正方形子矩阵) + +### Backtracking + +- [Question 10 : Regular Expression Matching](/src/回溯法/q10_正则表达式匹配) +- [Question 22 : Generate Parentheses](/src/回溯法/q22_括号生成) +- [Question 40 : Combination Sum II](/src/回溯法/q40_组合总和2) +- [Question 46 : Permutations](/src/回溯法/q46_全排列) + +### Trie + +- [Question 648 : Replace Words](/src/字典树/q648_单词替换) + +### Tree Traversal + +- [Question 94 : Binary Tree Inorder Traversal](/src/树的遍历/q94_二叉树的中序遍历) +- [Question 102 : Binary Tree Level Order Traversal](/src/树的遍历/q102_二叉树的层次遍历) +- [Question 103 : Binary Tree Zigzag Level Order Traversal](/src/树的遍历/q103_二叉树的锯齿形层序遍历) +- [Question 110 : Balanced Binary Tree](/src/树的遍历/q110_平衡二叉树) +- [Question 144 : Binary Tree Preorder Traversal](/src/树的遍历/q144_二叉树的前序遍历) +- [Question 145 : Binary Tree Postorder Traversal](/src/树的遍历/q145_二叉树的后序遍历) + +### Binary Search Tree + +- [Question 98 : Validate Binary Search Tree](/src/二叉搜索树相关/q98_验证二叉搜索树) +- [Question 450 : Delete Node in a BST](/src/二叉搜索树相关/q450_删除二叉搜索树中的节点) +- [Question 701 : Insert into a Binary Search Tree](/src/二叉搜索树相关/q701_二叉搜索树中的插入操作) + +--- + +## Interview Questions Collection (CN) + +- [Interview Questions Collection](/Rocket.md) diff --git a/Rocket.md b/Rocket.md index 9e5f8db..5785a8e 100644 --- a/Rocket.md +++ b/Rocket.md @@ -2,11 +2,25 @@ ## ZooKeeper ### CAP定理: -一个分布式系统不可能同时满足以下三种,一致性(C:Consistency),可用性(A:Available),分区容错性(P:Partition Tolerance).在此ZooKeeper保证的是CP,ZooKeeper不能保证每次服务请求的可用性,在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。另外在进行leader选举时集群都是不可用,所以说,ZooKeeper不能保证服务可用性。(Base理论CA强一致性和最终一致性) +一个分布式系统不可能在满足分区容错性(P)的情况下同时满足一致性(C)和可用性(A)。在此ZooKeeper保证的是CP,ZooKeeper不能保证每次服务请求的可用性,在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。另外在进行leader选举时集群都是不可用,所以说,ZooKeeper不能保证服务可用性。 + +### BASE理论 + +BASE理论是基本可用,软状态,最终一致性三个短语的缩写。BASE理论是对CAP中一致性和可用性(CA)权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,它大大降低了我们对系统的要求。 +1. 基本可用:基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。比如正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒。 +2. 软状态:软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 +3. 最终一致性:最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 + +### ZooKeeper特点 + +1. 顺序一致性:同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。 +2. 原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 +3. 单一系统映像:无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 +4. 可靠性:一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 ### ZAB协议: -ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Zookeeper 集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。 +ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Zookeeper 集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案(超过半数同意)来进行事务请求处理。 ### 选举算法和流程:FastLeaderElection(默认提供的选举算法) @@ -17,6 +31,22 @@ ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Z 4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为follower。 5. 服务器5启动,后面的逻辑同服务器4成为follower。 +### zk中的监控原理 + +zk类似于linux中的目录节点树方式的数据存储,即分层命名空间,zk并不是专门存储数据的,它的作用是主要是维护和监控存储数据的状态变化,通过监控这些数据状态的变化,从而可以达到基于数据的集群管理,zk中的节点的数据上限时1M。 + +client端会对某个znode建立一个watcher事件,当该znode发生变化时,这些client会收到zk的通知,然后client可以根据znode变化来做出业务上的改变等。 + +### zk实现分布式锁 + +zk实现分布式锁主要利用其临时顺序节点,实现分布式锁的步骤如下: + +1. 创建一个目录mylock +2. 线程A想获取锁就在mylock目录下创建临时顺序节点 +3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁 +4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点 +5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁 + ## Redis ### 应用场景 @@ -31,7 +61,7 @@ ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Z 1. 纯内存操作 2. 单线程操作,避免了频繁的上下文切换 3. 合理高效的数据结构 -4. 采用了非阻塞I/O多路复用机制 +4. 采用了非阻塞I/O多路复用机制(有一个文件描述符同时监听多个文件描述符是否有数据到来) ### Redis 的数据结构及使用场景 @@ -41,6 +71,29 @@ ZAB协议包括两种基本的模式:崩溃恢复和消息广播。当整个 Z 4. Set集合:集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。 5. Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。 +### Redis - ziplist、quicklist、listpack + +ziplist 是一个特殊双向链表,不像普通的链表使用前后指针关联在一起,它是存储在连续内存上的。 + +1. zlbytes: 32 位无符号整型,记录 ziplist 整个结构体的占用空间大小。当然了也包括 zlbytes 本身。这个结构有个很大的用处,就是当需要修改 ziplist 时候不需要遍历即可知道其本身的大小。 这个 SDS 中记录字符串的长度有相似之处,这些好的设计往往在平时的开发中可以采纳一下。 +2. zltail: 32 位无符号整型, 记录整个 ziplist 中最后一个 entry 的偏移量。所以在尾部进行 POP 操作时候不需要先遍历一次。 +3. zllen: 16 位无符号整型, 记录 entry 的数量, 所以只能表示 2^16。但是 Redis 作了特殊的处理:当实体数超过 2^16 ,该值被固定为 2^16 - 1。 所以这种时候要知道所有实体的数量就必须要遍历整个结构了。 +4. entry: 真正存数据的结构。 +5. zlend: 8 位无符号整型, 固定为 255 (0xFF)。为 ziplist 的结束标识 + +连锁更新是 ziplist 一个比较大的缺点,这也是在 v7.0 被 listpack 所替代的一个重要原因。 + +ziplist 在更新或者新增时候,如空间不够则需要对整个列表进行重新分配。当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。 + +quicklist 的设计,其实是结合了链表和 ziplist 各自的优势。简单来说,一个 quicklist 就是一个链表,而链表中的每个元素又是一个 ziplist。 + +listpack 也叫紧凑列表,它的特点就是用一块连续的内存空间来紧凑地保存数据,同时为了节省内存空间,listpack 列表项使用了多种编码方式,来表示不同长度的数据,这些数据包括整数和字符串。在 listpack 中,因为每个列表项只记录自己的长度,而不会像 ziplist 中的列表项那样,会记录前一项的长度。所以,当我们在 listpack 中新增或修改元素时,实际上只会涉及每个列表项自己的操作,而不会影响后续列表项的长度变化,这就避免了连锁更新。 + +### Redis的跳表和Mysql的B+树结构 +1. B+树是多叉平衡搜索树,只需要3层左右就能存放2kw左右的数据,同样情况下跳表则需要24层左右,假设层高对应磁盘IO,那么B+树的读性能会比跳表要好,因此mysql选了B+树做索引。 +2. redis的读写全在内存里进行操作,不涉及磁盘IO,同时跳表实现简单,相比B+树、AVL树、少了旋转树结构的开销,因此redis使用跳表来实现ZSET,而不是树结构。 +3. 存储引擎RocksDB内部使用了跳表,对比使用B+树的innodb,虽然写性能更好,但读性能属实差了些。在读多写少的场景下,B+树依旧是最佳选择。 + ### Redis 的数据过期策略 Redis 中数据过期策略采用定期删除+惰性删除策略 @@ -54,10 +107,35 @@ Redis 中数据过期策略采用定期删除+惰性删除策略 5. 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。 6. 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。 +### Redis的set和setnx + +Redis中setnx不支持设置过期时间,做分布式锁时要想避免某一客户端中断导致死锁,需设置lock过期时间,在高并发时 setnx与 expire 不能实现原子操作,如果要用,得在程序代码上显示的加锁。使用SET代替SETNX ,相当于SETNX+EXPIRE实现了原子性,不必担心SETNX成功,EXPIRE失败的问题。 + ### Redis的LRU具体实现: 传统的LRU是使用栈的形式,每次都将最新使用的移入栈顶,但是用栈的形式会导致执行select *的时候大量非热点数据占领头部数据,所以需要改进。Redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳。Redis初始的实现算法很简单,随机从dict中取出五个key,淘汰一个lru字段值最小的。在3.0的时候,又改进了一版算法,首先第一次随机选取的key都会放入一个pool中(pool的大小为16),pool中的key是按lru大小顺序排列的。接下来每次随机选取的keylru值必须小于pool中最小的lru才会继续放入,直到将pool放满。放满之后,每次如果有新的key需要放入,需要将pool中lru最大的一个key取出。淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰。 +### Redis如何发现热点key + +1. 凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此Key作为热点Key。 +2. 服务端收集:在操作redis之前,加入一行代码进行数据统计。 +3. 抓包进行评估:Redis使用TCP协议与客户端进行通信,通信协议采用的是RESP,所以自己写程序监听端口也能进行拦截包进行解析。 +4. 在proxy层,对每一个 redis 请求进行收集上报。 +5. Redis自带命令查询:Redis4.0.4版本提供了redis-cli –hotkeys就能找出热点Key。(如果要用Redis自带命令查询时,要注意需要先把内存逐出策略设置为allkeys-lfu或者volatile-lfu,否则会返回错误。进入Redis中使用config set maxmemory-policy allkeys-lfu即可。) + +### Redis的热点key解决方案 + +1. 服务端缓存:即将热点数据缓存至服务端的内存中.(利用Redis自带的消息通知机制来保证Redis和服务端热点Key的数据一致性,对于热点Key客户端建立一个监听,当热点Key有更新操作的时候,服务端也随之更新。) +2. 备份热点Key:即将热点Key+随机数,随机分配至Redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。 + +### Redis的RedLock算法 + +1. RedLock算法‌是由Redis的作者Salvatore Sanfilippo(也称为Antirez)提出的一种分布式锁的实现方案,旨在解决在分布式系统中实现可靠锁的问题,特别是解决单个Redis实例作为分布式锁时可能出现的单点故障问题。 +2. 过程1:‌获取当前时间戳‌:客户端在尝试获取锁时,首先记录当前的时间戳t0。 +3. 过程2:‌在多个Redis实例上尝试获取锁‌:客户端会向N个(通常建议是奇数个,如5个)独立的Redis实例发送请求,尝试获取相同的锁。每个请求都会设置相同的锁名、唯一标识符(如UUID)和过期时间。 +4. 过程3:‌统计获取锁成功的实例数量‌:客户端统计在多少个Redis实例上成功获取了锁。 +5. 过程4:‌判断锁是否获取成功‌:如果在过半数(即N/2+1)的Redis实例上成功获取了锁,并且从获取第一个锁到最后一个锁的总时间小于锁的过期时间的一半,那么认为锁获取成功。否则,认为锁获取失败,客户端需要释放已经获取的锁(如果有的话)‌ + ### 如何解决 Redis 缓存雪崩问题 1. 使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉 @@ -67,26 +145,83 @@ Redis 中数据过期策略采用定期删除+惰性删除策略 ### 如何解决 Redis 缓存穿透问题 1. 在接口做校验 -2. 存null值(缓存击穿加锁) +2. 存null值(缓存击穿加锁,或设置不过期) 3. 布隆过滤器拦截: 将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。 ### Redis的持久化机制 Redis为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis的持久化策略有两种: - 1. RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。 -当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉。 - 1. AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。 + +1. RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉。 +2. AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。 + 使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。 Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。 -### Redis和memcached的区别 +### Redis的事务 + +1. Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 +2. Redis事务没有隔离级别的概念,批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。 +3. Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。 + +### Redis事务相关命令 + +1. watch key1 key2 ... : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断(类似乐观锁) +2. multi : 标记一个事务块的开始(queued) +3. exec : 执行所有事务块的命令(一旦执行exec后,之前加的监控锁都会被取消掉) +4. discard : 取消事务,放弃事务块中的所有命令 +5. unwatch : 取消watch对所有key的监控 + +### Redis和 memcached 的区别 1. 存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性。 2. 数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,,而redis支持五种数据类型。 3. 用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。 4. value的大小:redis可以达到1GB,而memcache只有1MB。 +### Redis6.0 多线程的实现机制 + +在 redis 6.0 以前,完整的 redis 线程模型是 主线程(1个)+ 后台线程(三个),三个后台线程分别处理: +1. 关闭 AOF、RDB 等过程中产生的大临时文件 +2. 将追加至 AOF 文件的数据刷盘(一般情况下 write 调用之后,数据被写入内核缓冲区,通过 fsync 调用才将内核缓冲区的数据写入磁盘) +3. 惰性释放大对象(大键的空间回收交由单独线程实现,主线程只做关系解除,可以快速返回,继续处理其他事件,避免服务器长时间阻塞) + +Redis 6.0之后,Redis 正式在核心网络模型中引入了多线程,也就是所谓的 I/O threading,至此 Redis 真正拥有了多线程模型。一般来说,一个正常的客户端请求会经历 建立连接、IO就绪监听/读、命令执行、IO写等一系列操作。 +1. 主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列 +2. 主线程处理完读事件之后,通过Round Robin 将这些连接分配给这些IO线程。 +3. 主线程阻塞等待IO线程读取socket +4. 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行回写 socket +5. 主线程阻塞等待 IO 线程将数据回写 socket 完毕 + +Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。 + +### Redis的几种集群模式 + +1. 主从复制 +2. 哨兵模式 +3. cluster模式 + +### Redis的哨兵模式 + +哨兵是一个分布式系统,在主从复制的基础上你可以在一个架构中运行多个哨兵进程,这些进程使用流言协议来接收关于Master是否下线的信息,并使用投票协议来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master。 + +每个哨兵会向其它哨兵、master、slave定时发送消息,以确认对方是否活着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机”)。 + +若“哨兵群“中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。 + +### Redis的rehash + +Redis的rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的,redis会维护维持一个索引计数器变量rehashidx来表示rehash的进度。 + +这种渐进式的 rehash 避免了集中式rehash带来的庞大计算量和内存操作,但是需要注意的是redis在进行rehash的时候,正常的访问请求可能需要做多要访问两次hashtable(ht[0], ht[1]),例如键值被rehash到新ht1,则需要先访问ht0,如果ht0中找不到,则去ht1中找。 + +### Redis的hash表被扩展的条件 + +1. 哈希表中保存的key数量超过了哈希表的大小. +2. Redis服务器目前没有在执行BGSAVE命令(rdb)或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1. +3. Redis服务器目前在执行BGSAVE命令(rdb)或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5.(负载因子=哈希表已保存节点数量 / 哈希表大小,当哈希表的负载因子小于0.1时,对哈希表执行收缩操作。) + ### Redis并发竞争key的解决方案 1. 分布式锁+时间戳 @@ -109,6 +244,15 @@ Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚 3. 隔离性:同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。 4. 持久性:事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。 +### Mysql的存储引擎 + +1. InnoDB存储引擎:InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理(OLTP)的应用。其特点是行锁设计,支持外键,并支持非锁定锁,即默认读取操作不会产生锁。从Mysql5.5.8版本开始,InnoDB存储引擎是默认的存储引擎。 +2. MyISAM存储引擎:MyISAM存储引擎不支持事务、表锁设计,支持全文索引,主要面向一些OLAP数据库应用。InnoDB的数据文件本身就是主索引文件,而MyISAM的主索引和数据是分开的。 +3. NDB存储引擎:NDB存储引擎是一个集群存储引擎,其结构是share nothing的集群架构,能提供更高的可用性。NDB的特点是数据全部放在内存中(从MySQL 5.1版本开始,可以将非索引数据放在磁盘上),因此主键查找的速度极快,并且通过添加NDB数据存储节点可以线性地提高数据库性能,是高可用、高性能的集群系统。NDB存储引擎的连接操作是在MySQL数据库层完成的,而不是在存储引擎层完成的。这意味着,复杂的连接操作需要巨大的网络开销,因此查询速度很慢。如果解决了这个问题,NDB存储引擎的市场应该是非常巨大的。 +4. Memory存储引擎:Memory存储引擎(之前称HEAP存储引擎)将表中的数据存放在内存中,如果数据库重启或发生崩溃,表中的数据都将消失。它非常适合用于存储临时数据的临时表,以及数据仓库中的纬度表。Memory存储引擎默认使用哈希索引,而不是我们熟悉的B+树索引。虽然Memory存储引擎速度非常快,但在使用上还是有一定的限制。比如,只支持表锁,并发性能较差,并且不支持TEXT和BLOB列类型。最重要的是,存储变长字段时是按照定常字段的方式进行的,因此会浪费内存。 +5. Archive存储引擎:Archive存储引擎只支持INSERT和SELECT操作,从MySQL 5.1开始支持索引。Archive存储引擎使用zlib算法将数据行(row)进行压缩后存储,压缩比一般可达1∶10。正如其名字所示,Archive存储引擎非常适合存储归档数据,如日志信息。Archive存储引擎使用行锁来实现高并发的插入操作,但是其本身并不是事务安全的存储引擎,其设计目标主要是提供高速的插入和压缩功能。 +6. Maria存储引擎:Maria存储引擎是新开发的引擎,设计目标主要是用来取代原有的MyISAM存储引擎,从而成为MySQL的默认存储引擎。它可以看做是MyISAM的后续版本。Maria存储引擎的特点是:支持缓存数据和索引文件,应用了行锁设计,提供了MVCC功能,支持事务和非事务安全的选项,以及更好的BLOB字符类型的处理性能。 + ### 事务的并发问题 1. 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据 @@ -124,14 +268,11 @@ Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚 | 可重复读 | 否 | 否 | 是 | | 串行化 | 否 | 否 | 否 | -在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决时不彻底的。 -通过索引加锁,间隙锁,next key lock可以解决幻读的问题。 - ### Mysql的逻辑结构 * 最上层的服务类似其他CS结构,比如连接处理,授权处理。 * 第二层是Mysql的服务层,包括SQL的解析分析优化,存储过程触发器视图等也在这一层实现。 -* 最后一层是存储引擎的实现,类似于Java接口的实现,Mysql的执行器在执行SQL的时候只会关注API的调用,完全屏蔽了不同引擎实现间的差异。比如Select语句,先会判断当前用户是否拥有权限,其次到缓存(内存)查询是否有相应的结果集,如果没有再执行解析sql,优化生成执行计划,调用API执行。 +* 最后一层是存储引擎的实现,类似于Java接口的实现,Mysql的执行器在执行SQL的时候只会关注API的调用,完全屏蔽了不同引擎实现间的差异。比如Select语句,先会判断当前用户是否拥有权限,其次到缓存(内存)查询是否有相应的结果集,如果没有再执行解析sql,检查SQL 语句语法是否正确,再优化生成执行计划,调用API执行。 ### SQL执行顺序 @@ -140,18 +281,36 @@ SQL的执行顺序:from---where--group by---having---select---order by ### MVCC,redolog,undolog,binlog * undoLog 也就是我们常说的回滚日志文件 主要用于事务中执行失败,进行回滚,以及MVCC中对于数据历史版本的查看。由引擎层的InnoDB引擎实现,是逻辑日志,记录数据修改被修改前的值,比如"把id='B' 修改为id = 'B2' ,那么undo日志就会用来存放id ='B'的记录”。当一条数据需要更新前,会先把修改前的记录存储在undolog中,如果这个修改出现异常,,则会使用undo日志来实现回滚操作,保证事务的一致性。当事务提交之后,undo log并不能立马被删除,而是会被放到待清理链表中,待判断没有事物用到该版本的信息时才可以清理相应undolog。它保存了事务发生之前的数据的一个版本,用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 -* redoLog 是重做日志文件是记录数据修改之后的值,用于持久化到磁盘中。redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。由引擎层的InnoDB引擎实现,是物理日志,记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动”。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redoLog 在内存中,然后找个时间将redoLog的操作执行到磁盘上的文件上。不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性。 -* MVCC多版本并发控制是MySQL中基于乐观锁理论实现隔离级别的方式,用于读已提交和可重复读取隔离级别的实现。在MySQL中,会在表中每一条数据后面添加两个字段:最近修改该行数据的事务ID,指向该行(undolog表中)回滚段的指针。Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。 -* binlog由Mysql的Server层实现,是逻辑日志,记录的是sql语句的原始逻辑,比如"把id='B' 修改为id = ‘B2’。binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。 产生:事务提交的时候,一次性将事务中的sql语句,按照一定的格式记录到binlog中。用于复制和恢复在主从复制中,从库利用主库上的binlog进行重播(执行日志中记录的修改逻辑),实现主从同步。业务数据不一致或者错了,用binlog恢复。 +* redoLog 是重做日志文件是记录数据修改之后的值,用于持久化到磁盘中。redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。由引擎层的InnoDB引擎实现,是物理日志,记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动”。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redoLog 在内存中,然后找个时间将redoLog的操作执行到磁盘上的文件上。不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性。每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组下至少有2个重做日志文件,如默认的ib_logfile0和ib_logfile1。为了得到更高的可靠性,用户可以设置多个的镜像日志组(mirrored log groups),将不同的文件组放在不同的磁盘上,以此提高重做日志的高可用性。在日志组中每个重做日志文件的大小一致,并以循环写入的方式运行。InnoDB存储引擎先写重做日志文件1,当达到文件的最后时,会切换至重做日志文件2,再当重做日志文件2也被写满时,会再切换到重做日志文件1中。 +* MVCC多版本并发控制是MySQL中基于乐观锁理论实现隔离级别的方式,用于读已提交和可重复读取隔离级别的实现。在MySQL中,会在表中每一条数据后面添加两个字段:最近修改该行数据的事务ID,指向该行(undolog表中)回滚段的指针。Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。MVCC的可以实现不同的隔离级别,这取决于是否针对同一个事物生成一个还是多个ReadView。 ### binlog和redolog的区别 1. redolog是在InnoDB存储引擎层产生,而binlog是MySQL数据库的上层服务层产生的。 -2. 两种日志记录的内容形式不同。MySQL的binlog是逻辑日志,其记录是对应的SQL语句。而innodb存储引擎层面的重做日志是物理日志。 +2. 两种日志记录的内容形式不同。MySQL的binlog是逻辑日志,其记录是对应的SQL语句,对应的事务。而innodb存储引擎层面的重做日志是物理日志,是关于每个页(Page)的更改的物理情况。 3. 两种日志与记录写入磁盘的时间点不同,binlog日志只在事务提交完成后进行一次写入。而innodb存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的。 4. binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用。 5. binlog可以作为恢复数据使用,主从复制搭建,redolog作为异常宕机或者介质故障后的数据恢复使用。 +### Mysql读写分离以及主从同步 + +1. 原理:主库将变更写binlog日志,然后从库连接到主库后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中,接着从库中有一个sql线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再执行一遍sql,这样就可以保证自己跟主库的数据一致。 +2. 问题:这里有很重要一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行操作,在从库上会串行化执行,由于从库从主库拷贝日志以及串行化执行sql特点,在高并发情况下,从库数据一定比主库慢一点,是有延时的,所以经常出现,刚写入主库的数据可能读不到了,要过几十毫秒,甚至几百毫秒才能读取到。还有一个问题,如果突然主库宕机了,然后恰巧数据还没有同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。所以mysql实际上有两个机制,一个是半同步复制,用来解决主库数据丢失问题,一个是并行复制,用来解决主从同步延时问题。 +3. 半同步复制:semi-sync复制,指的就是主库写入binlog日志后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库ack之后才会认为写完成。 +4. 并发复制:指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这样库级别的并行。(将主库分库也可缓解延迟问题) + +### Next-Key Lock + +InnoDB 采用 Next-Key Lock 解决幻读问题。在`insert into test(xid) values (1), (3), (5), (8), (11);`后,由于xid上是有索引的,该算法总是会去锁住索引记录。现在,该索引可能被锁住的范围如下:(-∞, 1], (1, 3], (3, 5], (5, 8], (8, 11], (11, +∞)。Session A(`select * from test where id = 8 for update`)执行后会锁住的范围:(5, 8], (8, 11]。除了锁住8所在的范围,还会锁住下一个范围,所谓Next-Key。 + +### InnoDB的关键特性 + +1. 插入缓冲:对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。 +2. 两次写:两次写带给InnoDB存储引擎的是数据页的可靠性,有经验的DBA也许会想,如果发生写失效,可以通过重做日志进行恢复。这是一个办法。但是必须清楚地认识到,如果这个页本身已经发生了损坏(物理到page页的物理日志成功页内逻辑日志失败),再对其进行重做是没有意义的。这就是说,在应用(apply)重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,这就是doublewrite。 +3. 自适应哈希索引:InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引。 +4. 异步IO:为了提高磁盘操作性能,当前的数据库系统都采用异步IO(AIO)的方式来处理磁盘操作。AIO的另一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO,这样可以提高IOPS的性能。 +5. 刷新邻接页:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著的优势。 + ### Mysql如何保证一致性和持久性 MySQL为了保证ACID中的一致性和持久性,使用了WAL(Write-Ahead Logging,先写日志再写磁盘)。Redo log就是一种WAL的应用。当数据库忽然掉电,再重新启动时,MySQL可以通过Redo log还原数据。也就是说,每次事务提交时,不用同步刷新磁盘数据文件,只需要同步刷新Redo log就足够了。 @@ -168,6 +327,7 @@ MySQL为了保证ACID中的一致性和持久性,使用了WAL(Write-Ahead Logg * 平衡二叉树:通过旋转解决了平衡的问题,但是旋转操作效率太低。 * 红黑树:通过舍弃严格的平衡和引入红黑节点,解决了 AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多。 * B+树:在B树的基础上,将非叶节点改造为不存储数据纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效。 +* B+树是由多个页组成的多层级结构,每个页16Kb,对于主键索引来说,最末级的叶子结点放行数据,非叶子结点放的则是索引信息(主键id和页号),用于加速查询。 ### B+树的叶子节点都可以存哪些东西 @@ -182,11 +342,66 @@ MySQL为了保证ACID中的一致性和持久性,使用了WAL(Write-Ahead Logg 1. 模糊查询 %like 2. 索引列参与计算,使用了函数 3. 非最左前缀顺序 -4. where对null判断 +4. where单列索引对null判断 5. where不等于 6. or操作有至少一个字段没有索引 7. 需要回表的查询结果集过大(超过配置的范围) +### 为什么Mysql数据库存储不建议使用NULL + +1. NOT IN子查询在有NULL值的情况下返回永远为空结果,查询容易出错。 +2. 索引问题,单列索引无法存储NULL值,where对null判断会不走索引。 +3. 如果在两个字段进行拼接(CONCAT函数),首先要各字段进行非null判断,否则只要任意一个字段为空都会造成拼接的结果为null +4. 如果有 Null column 存在的情况下,count(Null column)需要格外注意,null 值不会参与统计。 +5. Null列需要更多的存储空间:需要一个额外的字节作为判断是否为NULL的标志位 + +### explain命令概要 + +1. id:select选择标识符 +2. select_type:表示查询的类型。 +3. table:输出结果集的表 +4. partitions:匹配的分区 +5. type:表示表的连接类型 +6. possible_keys:表示查询时,可能使用的索引 +7. key:表示实际使用的索引 +8. key_len:索引字段的长度 +9. ref:列与索引的比较 +10. rows:扫描出的行数(估算的行数) +11. filtered:按表条件过滤的行百分比 +12. Extra:执行情况的描述和说明 + +### explain 中的 select_type(查询的类型) + +1. SIMPLE(简单SELECT,不使用UNION或子查询等) +2. PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY) +3. UNION(UNION中的第二个或后面的SELECT语句) +4. DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询) +5. UNION RESULT(UNION的结果,union语句中第二个select开始后面所有select) +6. SUBQUERY(子查询中的第一个SELECT,结果不依赖于外部查询) +7. DEPENDENT SUBQUERY(子查询中的第一个SELECT,依赖于外部查询) +8. DERIVED(派生表的SELECT, FROM子句的子查询) +9. UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行) + +### explain 中的 type(表的连接类型) + +1. system:最快,主键或唯一索引查找常量值,只有一条记录,很少能出现 +2. const:PK或者unique上的等值查询 +3. eq_ref:PK或者unique上的join查询,等值匹配,对于前表的每一行(row),后表只有一行命中 +4. ref:非唯一索引,等值匹配,可能有多行命中 +5. range:索引上的范围扫描,例如:between/in +6. index:索引上的全集扫描,例如:InnoDB的count +7. ALL:最慢,全表扫描(full table scan) + +### explain 中的 Extra(执行情况的描述和说明) + +1. Using where:不用读取表中所有信息,仅通过索引就可以获取所需数据,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示mysql服务器将在存储引擎检索行后再进行过滤 +2. Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询,常见 group by ; order by +3. Using filesort:当Query中包含 order by 操作,而且无法利用索引完成的排序操作称为“文件排序” +4. Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。 +5. Impossible where:这个值强调了where语句会导致没有符合条件的行(通过收集统计信息不可能存在结果)。 +6. Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 +7. No tables used:Query语句中使用from dual 或不含任何from子句 + ### 数据库优化指南 1. 创建并使用正确的索引 @@ -212,20 +427,47 @@ HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。 +### 动态年龄计算 + +Hotspot在遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。 + +JVM引入动态年龄计算,主要基于如下两点考虑: + +1. 如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件: a)MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了。 b)MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能。 +2. 相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面相同的问题。 + + ### 常见的垃圾回收机制 1. 引用计数法:引用计数法是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象引用计数为0时,就释放其占用的空间。 2. 可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。 +### CMS的执行过程 + +1. 初始标记(STW initial mark):这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。 +2. 并发标记(Concurrent marking):这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。 +3. 并发预清理(Concurrent precleaning):并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。 +4. 重新标记(STW remark):这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。 +5. 并发清理(Concurrent sweeping):清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。 +6. 并发重置(Concurrent reset):这个阶段,重置CMS收集器的数据结构状态,等待下一次垃圾回收。 + +### G1的执行过程 + +1. 标记阶段:首先是初始标记(Initial-Mark),这个阶段也是停顿的(stop-the-word),并且会稍带触发一次yong GC。 +2. 并发标记:这个过程在整个堆中进行,并且和应用程序并发运行。并发标记过程可能被yong GC中断。在并发标记阶段,如果发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,每个区域的对象活性(区域中存活对象的比例)被计算。 +3. 再标记:这个阶段是用来补充收集并发标记阶段产新的新垃圾。与之不同的是,G1中采用了更快的算法:SATB。 +4. 清理阶段:选择活性低的区域(同时考虑停顿时间),等待下次yong GC一起收集,这个过程也会有停顿(STW)。 +5. 回收/完成:新的yong GC清理被计算好的区域。但是有一些区域还是可能存在垃圾对象,可能是这些区域中对象活性较高,回收不划算,也肯能是为了迎合用户设置的时间,不得不舍弃一些区域的收集。 + ### G1和CMS的比较 -1. CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低手机停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。 +1. CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。 2. CMS仅作用于老年代,是基于标记清除算法,所以清理的过程中会有大量的空间碎片。 3. CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。 4. G1是一款面向服务端应用的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。 5. 从JDK 9开始,G1成为默认的垃圾回收器。当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5s甚至1s)。 6. G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法。 -7. G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。 +7. G1 需要记忆集来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。(Card Table(CMS中)的结构是一个连续的byte[]数组,扫描Card Table的时间比扫描整个老年代的代价要小很多!G1也参照了这个思路,不过采用了一种新的数据结构 Remembered Set 简称Rset。RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。每个Region都有一个对应的Rset。) ### 哪些对象可以作为GC Roots @@ -238,7 +480,11 @@ HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分 在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下来,这对于很多的应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。 -但不是说GC必须STW,你也可以选择降低运行速度但是可以并发执行的收集算法,这取决于你的业务。 +### 如果不STW可能会出现的问题 + +1. 数据一致性问题: 如果不STW,应用程序线程和垃圾回收线程同时运行,可能会导致数据不一致。例如,在垃圾回收线程正在标记对象时,应用程序线程可能对对象的引用关系进行了修改。比如原本一个对象A引用对象B,垃圾回收线程在标记过程中认为对象B是可回收的,但应用程序线程在垃圾回收线程标记完成后,又重新创建了一个引用指向对象B,这就导致了垃圾回收器错误地回收了对象B,而应用程序线程还在使用它,从而引发错误。 +2. 内存泄漏风险增加: 如果垃圾回收线程不能准确地识别存活对象,可能会导致一些本应该被回收的对象没有被回收或者不该被回收的对象会被错误的垃圾回收。例如,当应用程序线程在垃圾回收过程中动态地创建和销毁对象时,垃圾回收线程如果没有暂停应用程序线程,可能会遗漏一些已经没有引用的对象。这些对象会一直占用内存,随着时间的推移,可能导致内存泄漏,最终使Java虚拟机的堆内存耗尽,引发OutOfMemoryError错误。 +3. 垃圾回收效率降低: 在并发环境中,垃圾回收线程和应用程序线程的交互会变得更加复杂。垃圾回收线程需要处理应用程序线程对对象的动态修改,这会增加垃圾回收的复杂性和开销。例如,垃圾回收线程需要不断检查应用程序线程对对象引用的修改,以确保垃圾回收的准确性。这种并发操作可能会导致垃圾回收的效率降低,使得垃圾回收时间变长,虽然没有STW,但可能会以更频繁的垃圾回收或者更长的垃圾回收周期来弥补,从而影响应用程序的整体性能。 ### 垃圾回收算法 @@ -256,6 +502,31 @@ HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分 3. 方法区空间不足 4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存 5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 + +### 对象什么时候进入老年代 + +1. 大对象直接进入老年代。 虚拟机提供了一个阈值参数,令大于这个设置值的对象直接在老年代中分配。如果大对象进入新生代,新生代采用的复制算法收集内存,会导致在Eden区和两个Survivor区之间发生大量的内存复制,应该避免这种情况。 +2. 长期存活的对象进入老年代。 虚拟机给每个对象定义了一个年龄计数器,对象在Eden区出生,经过一次Minor GC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor区中,此时对象年龄设为1。然后对象在Survivor区中每熬过一次 Minor GC,年龄就增加1,当年龄超过设定的阈值时,就会被移动到老年代中。 +3. 动态对象年龄判定: 如果在 Survivor 空间中所有相同年龄的对象,大小总和大于 Survivor 空间的一半,那么年龄大于或等于该年龄的对象就直接进入老年代,无须等到阈值中要求的年龄。 +4. 空间分配担保: 如果老年代中最大可用的连续空间大于新生代所有对象的总空间,那么 Minor GC 是安全的。如果老年代中最大可用的连续空间大于历代晋升到老年代的对象的平均大小,就进行一次有风险的 Minor GC,如果小于平均值,就进行 Full GC 来让老年代腾出更多的空间。因为新生代使用的是复制算法,为了内存利用率,只使用其中一个 Survivor 空间来做轮换备份,因此如果大量对象在 Minor GC 后仍然存活,导致 Survivor 空间不够用,就会通过分配担保机制,将多出来的对象提前转到老年代,但老年代要进行担保的前提是自己本身还有容纳这些对象的剩余空间,由于无法提前知道会有多少对象存活下来,所以取之前每次晋升到老年代的对象的平均大小作为经验值,与老年代的剩余空间做比较。 + +### TLAB + +在Java中,典型的对象不再堆上分配的情况有两种:TLAB和栈上分配(通过逃逸分析)。JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。 + +### Java对象分配的过程 + +1. 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入2. +2. 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3. +3. 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4。 +4. 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5。 +5. 执行一次Young GC(minor collection) +6. 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。 + +### 对象内存分配的两种方法 + +1. 指针碰撞(Serial、ParNew等带Compact过程的收集器) :假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。 +2. 空闲列表(CMS这种基于Mark-Sweep算法的收集器) :如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。 ### JVM类加载过程 @@ -270,7 +541,6 @@ HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分 双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。 - ### 双亲委派模型的"破坏" 一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码那该怎么办? @@ -318,22 +588,79 @@ HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分 * JConsole * VisualVM - -### JVM调优实战 - -1. 外部命令导致系统缓慢 - - 一个数字校园应用系统,发现请求响应时间比较慢,通过操作系统的mpstat工具发现CPU使用率很高,并且系统占用绝大多数的CPU资 源的程序并不是应用系统本身。每个用户请求的处理都需要执行一个外部shell脚本来获得系统的一些信息,执行这个shell脚本是通过Java的 Runtime.getRuntime().exec()方法来调用的。这种调用方式可以达到目的,但是它在Java 虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建进程 的开销也非常可观。Java虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一 样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执 行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重。用户根据建议去掉这个Shell脚本执行的语句,改为使用Java的API去获取这些信息后, 系统很快恢复了正常。 - -2. 由Windows虚拟内存导致的长时间停顿 - - 一个带心跳检测功能的GUI桌面程序,每15秒会发送一次心跳检测信号,如果 对方30秒以内都没有信号返回,那就认为和对方程序的连接已经断开。程序上线后发现心跳 检测有误报的概率,查询日志发现误报的原因是程序会偶尔出现间隔约一分钟左右的时间完 全无日志输出,处于停顿状态。 - + +### JVM常见参数 + +1. -Xms20M:表示设置JVM启动内存的最小值为20M,必须以M为单位 +2. -Xmx20M:表示设置JVM启动内存的最大值为20M,必须以M为单位。将-Xmx和-Xms设置为一样可以避免JVM内存自动扩展。大的项目-Xmx和-Xms一般都要设置到10G、20G甚至还要高 +3. -verbose:gc:表示输出虚拟机中GC的详细情况 +4. -Xss128k:表示可以设置虚拟机栈的大小为128k +5. -Xoss128k:表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的 +6. -XX:PermSize=10M:表示JVM初始分配的永久代(方法区)的容量,必须以M为单位 +7. -XX:MaxPermSize=10M:表示JVM允许分配的永久代(方法区)的最大容量,必须以M为单位,大部分情况下这个参数默认为64M +8. -Xnoclassgc:表示关闭JVM对类的垃圾回收 +9. -XX:+TraceClassLoading表示查看类的加载信息 +10. -XX:+TraceClassUnLoading:表示查看类的卸载信息 +11. -XX:NewRatio=4:表示设置年轻代(包括Eden和两个Survivor区)/老年代 的大小比值为1:4,这意味着年轻代占整个堆的1/5 +12. -XX:SurvivorRatio=8:表示设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8 +13. -Xmn20M:表示设置年轻代的大小为20M +14. -XX:+HeapDumpOnOutOfMemoryError:表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照 +15. -XX:+UseG1GC:表示让JVM使用G1垃圾收集器 +16. -XX:+PrintGCDetails:表示在控制台上打印出GC具体细节 +17. -XX:+PrintGC:表示在控制台上打印出GC信息 +18. -XX:PretenureSizeThreshold=3145728:表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位 +19. -XX:MaxTenuringThreshold=1:表示对象年龄大于1,自动进入老年代,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代被回收的概率。 +20. -XX:CompileThreshold=1000:表示一个方法被调用1000次之后,会被认为是热点代码,并触发即时编译 +21. -XX:+PrintHeapAtGC:表示可以看到每次GC前后堆内存布局 +22. -XX:+PrintTLAB:表示可以看到TLAB的使用情况 +23. -XX:+UseSpining:开启自旋锁 +24. -XX:PreBlockSpin:更改自旋锁的自旋次数,使用这个参数必须先开启自旋锁 +25. -XX:+UseSerialGC:表示使用jvm的串行垃圾回收机制,该机制适用于单核cpu的环境下 +26. -XX:+UseParallelGC:表示使用jvm的并行垃圾回收机制,该机制适合用于多cpu机制,同时对响应时间无强硬要求的环境下,使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数,此值可以设置与机器处理器数量相等。 +27. -XX:+UseParallelOldGC:表示年老代使用并行的垃圾回收机制 +28. -XX:+UseConcMarkSweepGC:表示使用并发模式的垃圾回收机制,该模式适用于对响应时间要求高,具有多cpu的环境下 +29. -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。 +30. -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开 + +### JVM调优目标-何时需要做jvm调优 + +1. heap 内存(老年代)持续上涨达到设置的最大内存值; +2. Full GC 次数频繁; +3. GC 停顿时间过长(超过1秒); +4. 应用出现OutOfMemory 等内存异常; +5. 应用中有使用本地缓存且占用大量内存空间; +6. 系统吞吐量与响应性能不高或下降。 + +### JVM调优实战 + +1. Major GC和Minor GC频繁 + + 首先优化Minor GC频繁问题。通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过增大新生代空间来降低Minor GC的频率。例如在相同的内存分配率的前提下,新生代中的Eden区增加一倍,Minor GC的次数就会减少一半。 + + 扩容Eden区虽然可以减少Minor GC的次数,但会增加单次Minor GC时间么?扩容后,Minor GC时增加了T1(扫描时间),但省去T2(复制对象)的时间,更重要的是对于虚拟机来说,复制对象的成本要远高于扫描成本,所以,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小。因此如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加。 + +2. 请求高峰期发生GC,导致服务可用性下降 + + 由于跨代引用的存在,CMS在Remark阶段必须扫描整个堆,同时为了避免扫描时新生代有很多对象,增加了可中断的预清理阶段用来等待Minor GC的发生。只是该阶段有时间限制,如果超时等不到Minor GC,Remark时新生代仍然有很多对象,我们的调优策略是,通过参数强制Remark前进行一次Minor GC,从而降低Remark阶段的时间。 + 另外,类似的JVM是如何避免Minor GC时扫描全堆的? 经过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。卡表的具体策略是将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表3被标记为脏(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。 + +3. STW过长的GC + + 对于性能要求很高的服务,建议将MaxPermSize和MinPermSize设置成一致(JDK8开始,Perm区完全消失,转而使用元空间。而元空间是直接存在内存中,不在JVM中),Xms和Xmx也设置为相同,这样可以减少内存自动扩容和收缩带来的性能损失。虚拟机启动的时候就会把参数中所设定的内存全部化为私有,即使扩容前有一部分内存不会被用户代码用到,这部分内存在虚拟机中被标识为虚拟内存,也不会交给其他进程使用。 + +4. 外部命令导致系统缓慢 + + 一个数字校园应用系统,发现请求响应时间比较慢,通过操作系统的mpstat工具发现CPU使用率很高,并且系统占用绝大多数的CPU资 源的程序并不是应用系统本身。每个用户请求的处理都需要执行一个外部shell脚本来获得系统的一些信息,执行这个shell脚本是通过Java的 Runtime.getRuntime().exec()方法来调用的。这种调用方式可以达到目的,但是它在Java 虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时创建进程 的开销也非常可观。Java虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执行这个操作,系统的消耗会很大,不仅是CPU,内存负担也很重。用户根据建议去掉这个Shell脚本执行的语句,改为使用Java的API去获取这些信息后,系统很快恢复了正常。 + +5. 由Windows虚拟内存导致的长时间停顿 + + 一个带心跳检测功能的GUI桌面程序,每15秒会发送一次心跳检测信号,如果对方30秒以内都没有信号返回,那就认为和对方程序的连接已经断开。程序上线后发现心跳 检测有误报的概率,查询日志发现误报的原因是程序会偶尔出现间隔约一分钟左右的时间完 全无日志输出,处于停顿状态。 + 因为是桌面程序,所需的内存并不大(-Xmx256m),所以开始并没有想到是GC导致的 程序停顿,但是加入参数-XX:+PrintGCApplicationStoppedTime-XX:+PrintGCDateStamps- Xloggc:gclog.log后,从GC日志文件中确认了停顿确实是由GC导致的,大部分GC时间都控 制在100毫秒以内,但偶尔就会出现一次接近1分钟的GC。 从GC日志中找到长时间停顿的具体日志信息(添加了-XX:+PrintReferenceGC参数), 找到的日志片段如下所示。从日志中可以看出,真正执行GC动作的时间不是很长,但从准 备开始GC,到真正开始GC之间所消耗的时间却占了绝大部分。 - - 除GC日志之外,还观察到这个GUI程序内存变化的一个特点,当它最小化的时候,资源 管理中显示的占用内存大幅度减小,但是虚拟内存则没有变化,因此怀疑程序在最小化时它 的工作内存被自动交换到磁盘的页面文件之中了,这样发生GC时就有可能因为恢复页面文 件的操作而导致不正常的GC停顿。在Java的GUI程序中要避免这种现象,可以 加入参数“-Dsun.awt.keepWorkingSetOnMinimize=true”来解决。 + + 除GC日志之外,还观察到这个GUI程序内存变化的一个特点,当它最小化的时候,资源 管理中显示的占用内存大幅度减小,但是虚拟内存则没有变化,因此怀疑程序在最小化时它的工作内存被自动交换到磁盘的页面文件之中了,这样发生GC时就有可能因为恢复页面文件的操作而导致不正常的GC停顿。在Java的GUI程序中要避免这种现象,可以 加入参数“-Dsun.awt.keepWorkingSetOnMinimize=true”来解决。 ## Java基础 @@ -350,7 +677,7 @@ HashMap的环:若当前线程此时获得ertry节点,但是被线程中断 ### HashMap如果我想要让自己的Object作为K应该怎么办 1. 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; -2. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性; +2. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性(Java建议重写equal方法的时候需重写hashcode的方法) ### volatile @@ -364,6 +691,15 @@ CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。 如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。 +### Synchronized的四种使用方式 + +1. synchronized(this):当a线程执行到该语句时,锁住该语句所在对象object,其它线程无法访问object中的所有synchronized代码块。 +2. synchronized(obj):锁住对象obj,其它线程对obj中的所有synchronized代码块的访问被阻塞。 +3. synchronized method():与(1)类似,区别是(1)中线程执行到某方法中的该语句才会获得锁,而对方法加锁则是当方法调用时立刻获得锁。 +4. synchronized static method():当线程执行到该语句时,获得锁,所有调用该方法的其它线程阻塞,但是这个类中的其它非static声明的方法可以访问,即使这些方法是用synchronized声明的,但是static声明的方法会被阻塞;注意,这个锁与对象无关。 + +前三种方式加的是对象锁,但是如果(2)中obj是一个class类型的对象,那么加的是类锁,并且锁的范围比(4)还要大;如果该class类型的某个实例对象获得了类锁,那么该class类型的所有实例对象的synchronized代码块均无法访问。 + ### Synchronized和Lock的区别 1. 首先synchronized是java内置关键字在jvm层面,Lock是个java类。 @@ -377,6 +713,10 @@ CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。 AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计数器),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列。 +AQS是自旋锁,在等待唤醒的时候,经常会使用自旋的方式,不停地尝试获取锁,直到被其他线程获取成功。 + +AQS有两个队列,同步对列和条件队列。同步队列依赖一个双向链表来完成同步状态的管理,当前线程获取同步状态失败后,同步器会将线程构建成一个节点,并将其加入同步队列中。通过signal或signalAll将条件队列中的节点转移到同步队列。 + ### 如何指定多个线程的执行顺序 1. 设定一个 orderNum,每个线程执行结束之后,更新 orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程。 @@ -385,7 +725,7 @@ AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计 ### 为什么要使用线程池 1. 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 -2. 可以根据系统的承受能力,调整线程池中工作线程的数目,放置因为消耗过多的内存,而把服务器累趴下 +2. 可以根据系统的承受能力,调整线程池中工作线程的数目,放置因为消耗过多的内存,而把服务器累趴下。 ### 核心线程池ThreadPoolExecutor内部参数 @@ -397,6 +737,22 @@ AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计 6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。 7. handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。 +### 线程池的执行流程 + +1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务 +2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列 +3. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务 +4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException + +### 线程池都有哪几种工作队列 + +1. ArrayBlockingQueue:底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。 +2. LinkedBlockingQueue:底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。 +3. SynchronousQueue:本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。 +4. PriorityBlockingQueue:无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。 + +举例 ArrayBlockingQueue 实现并发同步的原理:原理就是读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。 + ### 线程池的拒绝策略 1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 @@ -404,6 +760,16 @@ AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计 3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 +### 线程池的正确创建方式 + +不能用Executors,newFixed和newSingle,因为队列无限大,容易造成耗尽资源和OOM,newCached和newScheduled最大线程数是Integer.MAX_VALUE,线程创建过多和OOM。应该通过ThreadPoolExecutor手动创建。 + +### 线程提交submit()和execute()有什么区别 + +1. submit()相比于excute(),支持callable接口,也可以获取到任务抛出来的异常 +2. 可以获取到任务返回结果 +3. 用submit()方法执行任务,用Future.get()获取异常 + ### 线程池的线程数量怎么确定 1. 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。 @@ -418,15 +784,15 @@ AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计 ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。 -一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。 +一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。这个储值的Map并非ThreadLocal的成员变量,而是java.lang.Thread 类的成员变量。ThreadLocalMap实例是作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,”线程局部变量”为值,所以一个线程下可以保存多个”线程局部变量”。对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。由于Tomcat线程池的原因,我最初使用的”线程局部变量”保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值。所以在线程池的情况下,处理完成后主动调用该业务treadLocal的remove()方法,将”线程局部变量”清空,避免本线程下次处理的时候依然存在旧数据。 -### ThreadLocal中的内存泄漏 +### ThreadLocal为什么要使用弱引用和内存泄露问题 在ThreadLocal中内存泄漏是指ThreadLocalMap中的Entry中的key为null,而value不为null。因为key为null导致value一直访问不到,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。如果 key 是强引用,那么发生 GC 时 ThreadLocalMap 还持有 ThreadLocal 的强引用,会导致 ThreadLocal 不会被回收,从而导致内存泄漏。弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 方法时被清除,这算是最优的解决方案。 -### ThreadLocal为什么要使用弱引用和内存泄露问题 +Map中的key为一个threadlocal实例.如果使用强引用,当ThreadLocal对象(假设为ThreadLocal@123456)的引用被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123456的强引用,如果没有手动删除这个key,则ThreadLocal@123456不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。 -Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key.每个key都弱引用指向threadlocal.假如每个key都强引用指向threadlocal,也就是上图虚线那里是个强引用,那么这个threadlocal就会因为和entry存在强引用无法被回收!造成内存泄漏 ,除非线程结束,线程被回收了,map也跟着回收。 +如果使用弱引用,那指向ThreadLocal@123456对象的引用就两个:ThreadLocal强引用和ThreadLocalMap中Entry的弱引用。一旦ThreadLocal强引用被回收,则指向ThreadLocal@123456的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123456就会被回收。 虽然上述的弱引用解决了key,也就是线程的ThreadLocal能及时被回收,但是value却依然存在内存泄漏的问题。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收.map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露,因为存在一条从current thread连接过来的强引用.只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.所以当线程的某个localThread使用完了,马上调用threadlocal的remove方法,就不会发生这种情况了。 @@ -442,8 +808,12 @@ HashSet的value存的是一个static finial PRESENT = newObject()。而HashSet ### 阻塞非阻塞与同步异步的区别 -1. 同步和异步关注的是消息通信机制,所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。 -2. 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 +1. 同步和异步关注的是消息通信机制,所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。 +2. 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。 + +### Java SPI + +由于双亲委派模型损失了一丢丢灵活性。就比如java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。Java从1.6搞出了SPI就是为了优雅的解决这类问题——JDK提供接口,供应商提供服务。编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现。 ## Spring @@ -453,19 +823,11 @@ HashSet的value存的是一个static finial PRESENT = newObject()。而HashSet 2. 第二级缓存:早期提前暴露的对象缓存earlySingletonObjects。(属性还没有值对象也没有被初始化) 3. 第三级缓存:singletonFactories单例对象工厂缓存。 -### 创建Bean的整个过程 - -1. getBean方法肯定不陌生,必经之路,然后调用doGetBean,进来以后首先会执行transformedBeanName找别名,看你的Bean上面是否起了别名。然后进行很重要的一步,getSingleton,这段代码就是从你的单例缓存池中获取Bean的实例。那么你第一次进来肯定是没有的,缓存里肯定是拿不到的。也就是一级缓存里是没有的。那么它怎么办呢?他会尝试去二级缓存中去拿,但是去二级缓存中拿并不是无条件的,首先要判断isSingletonCurrentlyInCreation(beanName)他要看你这个对象是否正在创建当中,如果不是直接就退出该方法,如果是的话,他就会去二级缓存earlySingletonObjects里面取,如果没拿到,它还接着判断allowEarlyReference这个东西是否为true。它的意思是说,是否允许让你从单例工厂对象缓存中去拿对象。默认为true。好了,此时如果进来那么就会通过singletonFactory.getObject()去单例工厂缓存中去拿。然后将缓存级别提升至二级缓存也就早期暴露的缓存。 -2. getSingleton执行完以后会走dependsOn方法,判断是否有dependsOn标记的循环引用,有的话直接卡死,抛出异常。比如说A依赖于B,B依赖于A 通过dependsOn注解去指定。此时执行到这里就会抛出异常。这里所指并非是构造函数的循环依赖。 -3. beforeSingletonCreation在这里方法里。就把你的对象标记为了早期暴露的对象。提前暴露对象用于创建Bean的实例。 -4. 紧接着就走创建Bean的流程开始。在创建Bean之前执行了一下resolveBeforeInstantiation。它的意思是说,代理AOPBean定义注册信息但是这里并不是实际去代理你的对象,因为对象还没有被创建。只是代理了Bean定义信息,还没有被实例化。把Bean定义信息放进缓存,以便我想代理真正的目标对象的时候,直接去缓存里去拿。 -5. 接下来就真正的走创建Bean流程,首先走进真正做事儿的方法doCreateBean然后找到createBeanInstance这个方法,在这里面它将为你创建你的Bean实例信息(Bean的实例)。如果说创建成功了,那么就把你的对象放入缓存中去(将创建好的提前曝光的对象放入singletonFactories三级缓存中)将对象从二级缓存中移除因为它已经不是提前暴露的对象了。但是。如果说在createBeanInstance这个方法中在创建Bean的时候它会去检测你的依赖关系,会去检测你的构造器。然后,如果说它在创建A对象的时候,发现了构造器里依赖了B,然后它又会重新走getBean的这个流程,当在走到这里的时候,又发现依赖了A此时就会抛出异常。为什么会抛出异常,因为,走getBean的时候他会去从你的单例缓存池中去拿,因为你这里的Bean还没有被创建好。自然不会被放进缓存中,所以它是在缓存中拿不到B对象的。反过来也是拿不到A对象的。造成了死循环故此直接抛异常。这就是为什么Spring IOC不能解决构造器循环依赖的原因。因为你还没来的急放入缓存你的对象是不存在的。所以不能创建。同理@Bean标注的循环依赖方法也是不能解决的,跟这个同理。那么多例就更不能解决了。为什么?因为在走createBeanInstance的时候,会判断是否是单例的Bean定义信息mbd.isSingleton();如果是才会进来。所以多例的Bean压根就不会走进来,而是走了另一段逻辑,这里不做介绍。至此,构造器循环依赖和@Bean的循环依赖还有多例Bean的循环依赖为什么不能解决已经解释清楚。然后如果说,Bean创建成功了。那么会走后面的逻辑。 -6. 将创建好的Bean放入缓存,addSingletonFactory方法就是将你创建好的Bean放入三级缓存中。并且移除早期暴露的对象。 -7. 通过populateBean给属性赋值,我们知道,创建好的对象,并不是一个完整的对象,里面的属性还没有被赋值。所以这个方法就是为创建好的Bean为它的属性赋值。并且调用了我们实现的的XXXAware接口进行回调初始化,。然后调用我们实现的Bean的后置处理器,给我们最后一次机会去修改Bean的属性。 +三级缓存详解:[根据 Spring 源码写一个带有三级缓存的 IOC](https://zhuanlan.zhihu.com/p/144627581) ### Spring如何解决循环依赖问题 -Spring使用了三级缓存解决了循环依赖的问题。在populateBean()给属性赋值阶段里面Spring会解析你的属性,并且赋值,当发现,A对象里面依赖了B,此时又会走getBean方法,但这个时候,你去缓存中是可以拿的到的。因为我们在对createBeanInstance对象创建完成以后已经放入了缓存当中,所以创建B的时候发现依赖A,直接就从缓存中去拿,此时B创建完,A也创建完,一共执行了4次。至此Bean的创建完成,最后将创建好的Bean放入单例缓存池中。 +Spring使用了三级缓存解决了循环依赖的问题。在populateBean()给属性赋值阶段里面Spring会解析你的属性,并且赋值,当发现,A对象里面依赖了B,此时又会走getBean方法,但这个时候,你去缓存中是可以拿的到的。因为我们在对createBeanInstance对象创建完成以后已经放入了缓存当中,所以创建B的时候发现依赖A,直接就从缓存中去拿,此时B创建完,A也创建完,一共执行了4次。至此Bean的创建完成,最后将创建好的Bean放入单例缓存池中。(非单例的实例作用域是不允许出现循环依赖) ### BeanFactory和ApplicationContext的区别 @@ -479,23 +841,55 @@ Spring使用了三级缓存解决了循环依赖的问题。在populateBean()给 2. CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。 3. 区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。 -### Spring的的事务传播机制 +### @Transactional错误使用失效场景 + +1. @Transactional 在private上:当标记在protected、private、package-visible方法上时,不会产生错误,但也不会表现出为它指定的事务配置。可以认为它作为一个普通的方法参与到一个public方法的事务中。 +2. @Transactional 的事务传播方式配置错误。 +3. @Transactional 注解属性 rollbackFor 设置错误:Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。 +4. 同一个类中方法调用,导致@Transactional失效:由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。 +5. 异常被 catch 捕获导致@Transactional失效。 +6. 数据库引擎不支持事务。 + +### @Transactional配合分布式锁的使用 +1. 在分布式系统中,先获取分布式锁还是先开始事务,取决于具体的应用场景和业务需求。这两种方式各有优缺点,选择哪种方式需要根据实际需求权衡。 +2. 先获取分布式锁,再开始事务:如果锁的持有者在事务中发生故障,可能会导致锁无法释放,从而引发死锁。如果锁的粒度过大,可能会降低系统的并发性能。 +3. 先获取分布式锁,再开始事务适用场景:需要严格互斥访问共享资源的场景如对数据库中的某个表或某个记录进行更新操作,操作复杂且需要多个步骤的场景 +4. 先开始事务,再获取分布式锁:这种方式较少见,通常用于一些特殊的场景,例如事务的执行时间非常短,或者锁的获取成本较高。需要在事务中处理锁的获取和释放逻辑,增加了实现的复杂性。如果事务在获取锁之前提交,可能会导致数据不一致。 +5. 先开始事务,再获取分布式锁适用场景:事务执行时间非常短的场景:例如,简单的数据库更新操作。锁的获取成本较高的场景:例如,锁服务的响应时间较长,或者锁的粒度非常细。 -1. REQUIRED(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务。 +### Spring中的事务传播机制 + +1. REQUIRED(默认,常用):支持使用当前事务,如果当前事务不存在,创建一个新事务。eg:方法B用REQUIRED修饰,方法A调用方法B,如果方法A当前没有事务,方法B就新建一个事务(若还有C则B和C在各自的事务中独立执行),如果方法A有事务,方法B就加入到这个事务中,当成一个事务。 2. SUPPORTS:支持使用当前事务,如果当前事务不存在,则不使用事务。 3. MANDATORY:强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。 -4. REQUIRES_NEW:创建一个新事务,如果当前事务存在,把当前事务挂起。 +4. REQUIRES_NEW(常用):创建一个新事务,如果当前事务存在,把当前事务挂起。eg:方法B用REQUIRES_NEW修饰,方法A调用方法B,不管方法A上有没有事务方法B都新建一个事务,在该事务执行。 5. NOT_SUPPORTED:无事务执行,如果当前事务存在,把当前事务挂起。 6. NEVER:无事务执行,如果当前有事务则抛出Exception。 7. NESTED:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。 +### Spring中的@Conditional注解 +1. @Conditional 注解是一个非常强大的功能,用于根据特定条件动态决定是否创建某个Bean。它允许开发者在运行时根据不同的环境、配置或其他因素来灵活地控制Bean的实例化过程。 +2. 它的核心作用是基于条件判断来决定是否加载对应的Bean。如果条件成立,则创建并注册该Bean;如果条件不成立,则跳过该Bean的创建。 +3. @Conditional 注解需要配合一个或多个条件类(实现了 Condition 接口的类)一起使用。条件类需要实现 Condition 接口的 matches 方法,该方法返回一个布尔值,用于判断条件是否满足。 + +### Spring中Bean的生命周期 + +1. 实例化 Instantiation +2. 属性赋值 Populate +3. 初始化 Initialization +4. 销毁 Destruction + ### Spring的后置处理器 -1. BeanPostProcessor:Bean的后置处理器,主要在bean初始化前后工作。 -2. InstantiationAwareBeanPostProcessor:继承于BeanPostProcessor,主要在实例化bean前后工作; AOP创建代理对象就是通过该接口实现。 +1. BeanPostProcessor:Bean的后置处理器,主要在bean初始化前后工作。(before和after两个回调中间只处理了init-method) +2. InstantiationAwareBeanPostProcessor:继承于BeanPostProcessor,主要在实例化bean前后工作(TargetSource的AOP创建代理对象就是通过该接口实现) 3. BeanFactoryPostProcessor:Bean工厂的后置处理器,在bean定义(bean definitions)加载完成后,bean尚未初始化前执行。 4. BeanDefinitionRegistryPostProcessor:继承于BeanFactoryPostProcessor。其自定义的方法postProcessBeanDefinitionRegistry会在bean定义(bean definitions)将要加载,bean尚未初始化前真执行,即在BeanFactoryPostProcessor的postProcessBeanFactory方法前被调用。 +### Spring MVC的工作流程(源码层面) + +参考文章:[自己写个Spring MVC](https://zhuanlan.zhihu.com/p/139751932) + ## 消息队列 ### 为什么需要消息队列 @@ -513,6 +907,10 @@ Kafka中消息是以topic进行分类的,生产者通过topic向Kafka broker * acks = 1:意味若 Leader 在收到消息并把它写入到分区数据文件(不一定同步到磁盘上)时会返回确认或错误响应。在这个模式下,如果发生正常的 Leader 选举,生产者会在选举时收到一个 LeaderNotAvailableException 异常,如果生产者能恰当地处理这个错误,它会重试发送悄息,最终消息会安全到达新的 Leader 那里。不过在这个模式下仍然有可能丢失数据,比如消息已经成功写入 Leader,但在消息被复制到 follower 副本之前 Leader发生崩溃。 * acks = all(这个和 request.required.acks = -1 含义一样):意味着 Leader 在返回确认或错误响应之前,会等待所有同步副本都收到悄息。如果和min.insync.replicas 参数结合起来,就可以决定在返回确认前至少有多少个副本能够收到悄息,生产者会一直重试直到消息被成功提交。不过这也是最慢的做法,因为生产者在继续发送其他消息之前需要等待所有副本都收到当前的消息。 +另外可以设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。 + +除此之外,可以设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。 + ### Kafka消息是采用Pull模式,还是Push模式 Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息。push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达。 @@ -525,11 +923,52 @@ Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers 4. 批量发送:Kafka允许进行批量发送消息,先将消息缓存在内存中,然后一次请求批量发送出去 5. 数据压缩:Kafka还支持对消息集合进行压缩,Producer可以通过GZIP或Snappy格式对消息集合进行压缩 +### Kafka中broker数量和partition数量设置 + +1. 一个partition最好对应一个硬盘,这样能最大限度发挥顺序写的优势。一个broker如果对应多个partition,需要随机分发,顺序IO会退化成随机IO。 +2. 当broker数量大于partition数量,则有些broker空闲,此时增加partition会带来性能提升。而且是线性增长。 +3. 当两者相等,则所有broker都启用,吞吐达到瓶颈。 +4. 继续增加,则broker会不均衡,有点会分到更多的partition,顺序IO退化成随机IO。 + +### Kafka的rebalance机制 + +1. 消费者的再平衡是指分区的所属权从一个消费者转移到另一消费者的行为,它为消费组具备高可用性和伸缩性提供保障, 使我们可以既方便又安全地删除消费组内的消费者或往消费组内添加消费者。不过在再平衡发生期间, 消费组内的消费者是无法读取消息的。 +2. 再平衡会在以下情况下被触发:消费者加入或离开消费者组,消费者心跳超时或会话过期,分区数量变化,消费者处理超时。 +3. 再平衡的影响:传统再平衡会使所有消费者在再平衡期间会停止消费,直到新的分区分配完成。增量式再平衡可以让部分消费者可以继续消费未被重新分配的分区,减少停顿时间。 + +### Kafka中partition数量和消费者数量设置 + +消费者的数量不应该比分区数多,因为多出来的消费者是空闲的,没有任何帮助 + ### Kafka判断一个节点还活着的两个条件 1. 节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接 2. 如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久 +### 如何提升 Kafka 生产者的吞吐量 + +1. buffer.memory:设置发送消息的缓冲区,默认值是33554432,就是32MB。如果发送消息出去的速度小于写入消息进去的速度,就会导致缓冲区写满,此时生产消息就会阻塞住,所以说这里就应该多做一些压测,尽可能保证说这块缓冲区不会被写满导致生产行为被阻塞住。 +2. compression.type:默认是none,不压缩,但是也可以使用lz4压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大producer端的cpu开销。 +3. batch.size:设置merge batch的大小,如果 batch 太小,会导致频繁网络请求,吞吐量下降。如果batch太大,会导致一条消息需要等待很久才能被发送出去,而且会让内存缓冲区有很大压力,过多数据缓冲在内存里。默认值是:16384,就是16kb,也就是一个batch满了16kb就发送出去,一般在实际生产环境,这个batch的值可以增大一些来提升吞吐量。 + +### Kafka中的副本 + +在kafka中有topic的概念,每个topic会包含多个partition,而副本所描述的对象就是partition,通常情况下(默认情况下kafka集群最少3个broker节点),每个partition会有1个leader副本,2个follower副本。3个partition副本会保存到不同的broker上,这样即使当某个broker宕机了,由于副本的存在,kafka依旧可以对外提供服务。 + +在partition 副本机制中,只有leader副本可以对外提供读写服务,follower不对外提供服务,只是异步的从leader中拉群最新的数据,来保证follower和leader数据的一致性。 + +当leader所在broker宕机挂掉后,kafka依托Zookeeper提供的监控功能 ,能够实时的感知到,并立即在存活的follower中选择一个副本作为leader副本,对外提供服务,当老的leader副本重新回来后,只能作为follower副本加入到集群中。 + +虽然利用offset可以解决主从切换数据不一致问题,但是会导致消息写入性能下降,牺牲了kafka的高吞吐的特性。kafka自身也考虑到了,主从切换对数据一致性的影响,采取了ISR来降低数据的不一致。 + +ISR本质上是一个partition副本的集合,只不过副本要想进入这个集合中是有条件的。这个条件是:和leader副本中的数据是同步的follower副本才可以进入到ISR中。 + +用来衡量follower是否符合“和leader副本是同步的”,不是依据两者数据的差异量,而是两者数据不一致持续的时间,如果持续的时间过长,那么就认为两者不同步。其实这种设定也是有道理的,试想:发送者一次批量发送了大量的消息,这些消息先写入到leader中,此时follower中没有这些数据,那么所有follower都变成和leader是不同步的了,但是不足之处也很明显:ISR中和leader数据差异比较大的follower,是有可能被选为新的leader的。 + +上述持续时间的长短有参数 replica.lag.max.ms 来定义,默认10s,如果ISR中的follower副本和leader副本数据存在差异的时间超过10s,那么这个follower副本就会被从ISR中踢出出去,当然如果该follower后续慢慢追上了leader副本,那么这个follower就会被重新添加到ISR中,也就是说iSR是一个动态的集合。 + +kafka 把所有不在 ISR 中的存活副本都称为非同步副本,通常来说,非同步副本落后 leader 太多,因此,如果选择这些副本作为新 leader,就可能出现数据的丢失。毕竟,这些副本中保存的消息远远落后于老 leader 中的消息。在 Kafka 中,选举这种副本的过程称为 Unclean 领导者选举。Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举。 + ## Dubbo ### Dubbo的容错机制 @@ -545,6 +984,12 @@ Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers 可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信。 +### Dubbo提供的线程池 + +1. fixed:固定大小线程池,启动时建立线程,不关闭,一直持有。  +2. cached:缓存线程池,空闲一分钟自动删除,需要时重建。  +3. limited:可伸缩线程池,但池中的线程数只会增长不会收缩。(为避免收缩时突然来了大流量引起的性能问题)。 + ### Dubbo框架设计结构 1. 服务接口层:该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。 @@ -568,6 +1013,10 @@ Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers 4. 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉。 5. 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。 +### 多线程和单线程 + +线程不是越多越好,假如你的业务逻辑全部是计算型的(CPU密集型),不涉及到IO,并且只有一个核心。那肯定一个线程最好,多一个线程就多一点线程切换的计算,CPU不能完完全全的把计算能力放在业务计算上面,线程越多就会造成CPU利用率(用在业务计算的时间/总的时间)下降。但是在WEB场景下,业务并不是CPU密集型任务,而是IO密集型的任务,一个线程是不合适,如果一个线程在等待数据时,把CPU的计算能力交给其他线程,这样也能充分的利用CPU资源。但是线程数量也要有个限度,一般线程数有一个公式:最佳启动线程数=[任务执行时间/(任务执行时间-IO等待时间)]*CPU内核数超过这个数量,CPU要进行多余的线程切换从而浪费计算能力,低于这个数量,CPU要进行IO等待从而造成计算能力不饱和。总之就是要尽可能的榨取CPU的计算能力。如果你的CPU处于饱和状态,并且没有多余的线程切换浪费,那么此时就是你服务的完美状态,如果再加大并发量,势必会造成性能上的下降。 + ### 进程的组成部分 进程由进程控制块(PCB)、程序段、数据段三部分组成。 @@ -588,6 +1037,29 @@ Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers 4. 信号量:不能传递复杂消息,只能用来同步。 5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。 +### 内存管理有哪几种方式 + +1. 块式管理:把主存分为一大块、一大块的,当所需的程序片断不在主存时就分配一块主存空间,把程序片断load入主存,就算所需的程序片度只有几个字节也只能把这一块分配给它。这样会造成很大的浪费,平均浪费了50%的内存空间,但是易于管理。 +2. 页式管理:把主存分为一页一页的,每一页的空间要比一块一块的空间小很多,显然这种方法的空间利用率要比块式管理高很多。 +3. 段式管理:把主存分为一段一段的,每一段的空间又要比一页一页的空间小很多,这种方法在空间利用率上又比页式管理高很多,但是也有另外一个缺点。一个程序片断可能会被分为几十段,这样很多时间就会被浪费在计算每一段的物理地址上。 +4. 段页式管理:结合了段式管理和页式管理的优点。将程序分成若干段,每个段分成若干页。段页式管理每取一数据,要访问3次内存。 + +### 页面置换算法 + +1. 最佳置换算法OPT:只具有理论意义的算法,用来评价其他页面置换算法。置换策略是将当前页面中在未来最长时间内不会被访问的页置换出去。 +2. 先进先出置换算法FIFO:简单粗暴的一种置换算法,没有考虑页面访问频率信息。每次淘汰最早调入的页面。 +3. 最近最久未使用算法LRU:算法赋予每个页面一个访问字段,用来记录上次页面被访问到现在所经历的时间t,每次置换的时候把t值最大的页面置换出去(实现方面可以采用寄存器或者栈的方式实现)。 +4. 时钟算法clock(也被称为是最近未使用算法NRU):页面设置一个访问位,并将页面链接为一个环形队列,页面被访问的时候访问位设置为1。页面置换的时候,如果当前指针所指页面访问为为0,那么置换,否则将其置为0,循环直到遇到一个访问为位0的页面。 +5. 改进型Clock算法:在Clock算法的基础上添加一个修改位,替换时根究访问位和修改位综合判断。优先替换访问位和修改位都是0的页面,其次是访问位为0修改位为1的页面。 +6. LFU最少使用算法LFU:设置寄存器记录页面被访问次数,每次置换的时候置换当前访问次数最少的。 + +### 操作系统中进程调度策略有哪几种 + +1. 先来先服务调度算法FCFS:队列实现,非抢占,先请求CPU的进程先分配到CPU,可以作为作业调度算法也可以作为进程调度算法;按作业或者进程到达的先后顺序依次调度,对于长作业比较有利. +2. 最短作业优先调度算法SJF:作业调度算法,算法从就绪队列中选择估计时间最短的作业进行处理,直到得出结果或者无法继续执行,平均等待时间最短,但难以知道下一个CPU区间长度;缺点:不利于长作业;未考虑作业的重要性;运行时间是预估的,并不靠谱. +3. 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿. +4. 时间片轮转调度算法(可抢占的):按到达的先后对进程放入队列中,然后给队首进程分配CPU时间片,时间片用完之后计时器发出中断,暂停当前进程并将其放到队列尾部,循环 ;队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。 + ### 死锁的4个必要条件 1. 互斥条件:一个资源每次只能被一个线程使用; @@ -619,6 +1091,14 @@ Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers 4. 服务端响应HTTP响应报文,报文由状态行(status line)、相应头部(headers)、空行(blank line)和响应数据(response body)4个部分组成。 5. 浏览器解析渲染 +### 计算机网络的五层模型 + +1. 应用层:为操作系统或网络应用程序提供访问网络服务的接口 ,通过应用进程间的交互完成特定网络应用。应用层定义的是应用进程间通信和交互的规则。(HTTP,FTP,SMTP,RPC) +2. 传输层:负责向两个主机中进程之间的通信提供通用数据服务。(TCP,UDP) +3. 网络层:负责对数据包进行路由选择和存储转发。(IP,ICMP(ping命令)) +4. 数据链路层:两个相邻节点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧,在两个相邻的链路上传送帧(frame)。每一帧包括数据和必要的控制信息。 +5. 物理层:物理层所传数据单位是比特(bit)。物理层要考虑用多大的电压代表1 或 0 ,以及接受方如何识别发送方所发送的比特。 + ### tcp和udp区别 1. TCP面向连接,UDP是无连接的,即发送数据之前不需要建立连接。 @@ -665,7 +1145,7 @@ TCP是一个双向通信协议,通信双方都有能力发送信息,并接 3. 插入排序:将一个记录插入到已排序的有序表中,从而得到一个新的,记录数增1的有序表 4. 快速排序:通过一趟排序将序列分成左右两部分,其中左半部分的的值均比右半部分的值小,然后再分别对左右部分的记录进行排序,直到整个序列有序。 ``` -int partition(int a[], int low, int high){ +int partition(int a[], int low, int high){ int key = a[low]; while( low < high ){ while(low < high && a[high] >= key) high--; @@ -684,11 +1164,54 @@ void quick_sort(int a[], int low, int high){ } ``` -5. 堆排序:假设序列有n个元素,先将这n建成大顶堆,然后取堆顶元素,与序列第n个元素交换,然后调整前n-1元素,使其重新成为堆,然后再取堆顶元素,与第n-1个元素交换,再调整前n-2个元素...直至整个序列有序。 +5. 堆排序:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。 +``` +public class Test { + + public void sort(int[] arr) { + for (int i = arr.length / 2 - 1; i >= 0; i--) { + adjustHeap(arr, i, arr.length); + } + for (int j = arr.length - 1; j > 0; j--) { + swap(arr, 0, j); + adjustHeap(arr, 0, j); + } + + } + + public void adjustHeap(int[] arr, int i, int length) { + int temp = arr[i]; + for (int k = i * 2 + 1; k < length; k = k * 2 + 1) { + if (k + 1 < length && arr[k] < arr[k + 1]) { + k++; + } + if (arr[k] > temp) { + arr[i] = arr[k]; + i = k; + } else { + break; + } + } + arr[i] = temp; + } + + public void swap(int[] arr, int a, int b) { + int temp = arr[a]; + arr[a] = arr[b]; + arr[b] = temp; + } + + public static void main(String[] args) { + int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1}; + new Test().sort(arr); + System.out.println(Arrays.toString(arr)); + } +} +``` 6. 希尔排序:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。 7. 归并排序:把有序表划分成元素个数尽量相等的两半,把两半元素分别排序,两个有序表合并成一个 -## 实际问题 +## 其他 ### 高并发系统的设计与实现 @@ -698,6 +1221,13 @@ void quick_sort(int a[], int low, int high){ * 降级:服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。 * 限流:限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。 +### 负载均衡算法: + +1. 轮询 +2. 加权轮询 +3. 随机算法 +4. 一致性Hash + ### 常见的限流算法: 常见的限流算法有计数器、漏桶和令牌桶算法。漏桶算法在分布式环境中消息中间件或者Redis都是可选的方案。发放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。 @@ -710,8 +1240,8 @@ void quick_sort(int a[], int low, int high){ update products set quantity = quantity-1 WHERE id=3; select quantity from products WHERE id=3 for update; ``` -1. update语句在更新的同时加上一个条件 +3. update语句在更新的同时加上一个条件 ``` quantity = select quantity from products WHERE id=3; update products set quantity = ($quantity-1) WHERE id=3 and queantity = $quantity; -``` \ No newline at end of file +``` diff --git "a/src/hash\347\233\270\345\205\263/q387_\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\347\254\254\344\270\200\344\270\252\345\224\257\344\270\200\345\255\227\347\254\246/Solution.java" "b/src/hash\347\233\270\345\205\263/q387_\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\347\254\254\344\270\200\344\270\252\345\224\257\344\270\200\345\255\227\347\254\246/Solution.java" new file mode 100644 index 0000000..61b0070 --- /dev/null +++ "b/src/hash\347\233\270\345\205\263/q387_\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\347\254\254\344\270\200\344\270\252\345\224\257\344\270\200\345\255\227\347\254\246/Solution.java" @@ -0,0 +1,25 @@ +package hash相关.q387_字符串中的第一个唯一字符; + +import java.util.HashMap; + +/** + * Hash o(n) + */ +public class Solution { + + public int firstUniqChar(String s) { + HashMap count = new HashMap<>(); + int n = s.length(); + for (int i = 0; i < n; i++) { + char c = s.charAt(i); + count.put(c, count.getOrDefault(c, 0) + 1); + } + + for (int i = 0; i < n; i++) { + if (count.get(s.charAt(i)) == 1) { + return i; + } + } + return -1; + } +} diff --git "a/src/\345\205\266\344\273\226/lru\345\256\236\347\216\260/LRUCache.java" "b/src/\345\205\266\344\273\226/lru\345\256\236\347\216\260/LRUCache.java" new file mode 100644 index 0000000..91228f5 --- /dev/null +++ "b/src/\345\205\266\344\273\226/lru\345\256\236\347\216\260/LRUCache.java" @@ -0,0 +1,93 @@ +package 其他.lru实现; + +import java.util.HashMap; +import java.util.Map; + +public class LRUCache { + class DLinkedNode { + int key; + int value; + DLinkedNode prev; + DLinkedNode next; + + public DLinkedNode() { + } + + public DLinkedNode(int _key, int _value) { + key = _key; + value = _value; + } + } + + private Map cache = new HashMap<>(); + private int size; + private int capacity; + private DLinkedNode head, tail; + + public LRUCache(int capacity) { + this.size = 0; + this.capacity = capacity; + // 使用伪头部和伪尾部节点 + head = new DLinkedNode(); + tail = new DLinkedNode(); + head.next = tail; + tail.prev = head; + } + + public int get(int key) { + DLinkedNode node = cache.get(key); + if (node == null) { + return -1; + } + // 如果 key 存在,先通过哈希表定位,再移到头部 + moveToHead(node); + return node.value; + } + + public void put(int key, int value) { + DLinkedNode node = cache.get(key); + if (node == null) { + // 如果 key 不存在,创建一个新的节点 + DLinkedNode newNode = new DLinkedNode(key, value); + // 添加进哈希表 + cache.put(key, newNode); + // 添加至双向链表的头部 + addToHead(newNode); + ++size; + if (size > capacity) { + // 如果超出容量,删除双向链表的尾部节点 + DLinkedNode tail = removeTail(); + // 删除哈希表中对应的项 + cache.remove(tail.key); + --size; + } + } else { + // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部 + node.value = value; + moveToHead(node); + } + } + + private void addToHead(DLinkedNode node) { + node.prev = head; + node.next = head.next; + head.next.prev = node; + head.next = node; + } + + private void removeNode(DLinkedNode node) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + private void moveToHead(DLinkedNode node) { + removeNode(node); + addToHead(node); + } + + private DLinkedNode removeTail() { + DLinkedNode res = tail.prev; + removeNode(res); + return res; + } +} diff --git "a/src/\345\205\266\344\273\226/\344\270\242\347\216\273\347\222\203\347\220\203/Solution.java" "b/src/\345\205\266\344\273\226/\344\270\242\347\216\273\347\222\203\347\220\203/Solution.java" new file mode 100644 index 0000000..d374624 --- /dev/null +++ "b/src/\345\205\266\344\273\226/\344\270\242\347\216\273\347\222\203\347\220\203/Solution.java" @@ -0,0 +1,33 @@ +package 其他.丢玻璃球; + +import java.util.Scanner; + +/** + * F(N) = min{ max(1(碎了), F(N - 1) + 1(没碎)), max(2, F(N - 2) + 1), max(3, F(N - 3) + 1), …… , max(N - 1, F(1)) + */ +public class Solution { + public static void main(String[] args) { + int N = 0; + Scanner scanner = new Scanner(System.in); + while (N < 1) { + N = scanner.nextInt(); + } + + int[] dp = new int[N + 1]; + dp[0] = 0; + dp[1] = 1; + + for (int i = 2; i <= N; ++i) { + int min = i; + for (int j = 1; j < i; ++j) { + int tmp = Math.max(j, dp[i - j] + 1); + if (tmp < min) { + min = tmp; + } + } + dp[i] = min; + } + + System.out.println(dp[N]); + } +} diff --git "a/src/\345\205\266\344\273\226/\346\237\245\346\211\276\347\254\254k\345\244\247\347\232\204\346\225\260\345\255\227/QuickSelect.java" "b/src/\345\205\266\344\273\226/\346\237\245\346\211\276\347\254\254k\345\244\247\347\232\204\346\225\260\345\255\227/QuickSelect.java" new file mode 100644 index 0000000..076c1de --- /dev/null +++ "b/src/\345\205\266\344\273\226/\346\237\245\346\211\276\347\254\254k\345\244\247\347\232\204\346\225\260\345\255\227/QuickSelect.java" @@ -0,0 +1,47 @@ +package 其他.查找第k大的数字; + +public class QuickSelect { + public static int findKthLargest(int[] nums, int k) { + return quickSelect(nums, 0, nums.length - 1, nums.length - k); + } + + private static int quickSelect(int[] nums, int left, int right, int kSmallest) { + if (left == right) { // 如果只剩一个元素,那么它就是第k小的 + return nums[left]; + } + + int pivotIndex = partition(nums, left, right); + + if (kSmallest == pivotIndex) { + return nums[kSmallest]; + } else if (kSmallest < pivotIndex) { + return quickSelect(nums, left, pivotIndex - 1, kSmallest); + } else { + return quickSelect(nums, pivotIndex + 1, right, kSmallest); + } + } + + private static int partition(int[] nums, int left, int right) { + int pivot = nums[right]; // 选择最右边的元素作为pivot + int i = left; // i是小于pivot的元素的最后一个位置 + for (int j = left; j < right; j++) { + if (nums[j] < pivot) { + swap(nums, i++, j); + } + } + swap(nums, i, right); // 把pivot放到中间位置 + return i; // 返回pivot的正确位置 + } + + private static void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + + public static void main(String[] args) { + int[] nums = {3, 2, 1, 5, 6, 4}; + int k = 2; // 找第2大的数字(即第k大的数字) + System.out.println("第 " + k + " 大的数字是: " + findKthLargest(nums, k)); // 输出应该是5 + } +} diff --git "a/src/\345\205\266\344\273\226/\347\272\277\347\250\213\345\256\211\345\205\250\347\232\204\346\234\254\345\234\260\347\274\223\345\255\230/Main.java" "b/src/\345\205\266\344\273\226/\347\272\277\347\250\213\345\256\211\345\205\250\347\232\204\346\234\254\345\234\260\347\274\223\345\255\230/Main.java" index 3b7cdf1..b57d0e5 100644 --- "a/src/\345\205\266\344\273\226/\347\272\277\347\250\213\345\256\211\345\205\250\347\232\204\346\234\254\345\234\260\347\274\223\345\255\230/Main.java" +++ "b/src/\345\205\266\344\273\226/\347\272\277\347\250\213\345\256\211\345\205\250\347\232\204\346\234\254\345\234\260\347\274\223\345\255\230/Main.java" @@ -11,7 +11,8 @@ public class Main { Map cache = new ConcurrentHashMap<>(); public static Object load(Object key) { - return key + "load"; + String value = key + ":value"; + return value; } public Object get(Object key) { diff --git "a/src/\345\205\266\344\273\226/\350\277\252\346\235\260\346\226\257\347\211\271\346\213\211/Main.java" "b/src/\345\205\266\344\273\226/\350\277\252\346\235\260\346\226\257\347\211\271\346\213\211/Main.java" new file mode 100644 index 0000000..0d18f5e --- /dev/null +++ "b/src/\345\205\266\344\273\226/\350\277\252\346\235\260\346\226\257\347\211\271\346\213\211/Main.java" @@ -0,0 +1,122 @@ +package 其他.迪杰斯特拉; + +import java.util.*; + +public class Main { + private static int MAX = 256; + public static int[][] weight; + + public static int getVal(int[][] graph, int index) { + return graph[index / graph.length][index % graph.length]; + } + + public static int getX(int[][] graph, int index) { + return index / graph.length; + } + + public static int getY(int[][] graph, int index) { + return index % graph.length; + } + + public static void main(String[] args) { + Scanner in = new Scanner(System.in); + int width = in.nextInt(); + int height = in.nextInt(); + int startX = in.nextInt(); + int startY = in.nextInt(); + int[][] graph = new int[height][width]; + int total = height * width; + weight = new int[total][total]; + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + graph[i][j] = in.nextInt(); + } + } + for (int i = 0; i < total; i++) { + for (int j = 0; j < total; j++) { + weight[i][j] = MAX; + } + } + List list = new ArrayList<>(); + for (int i = 0; i < weight.length; i++) { + weight[i][i] = 0; + int val = getVal(graph, i); + int x = getX(graph, i); + int y = getY(graph, i); + if (x == 0 || x == height - 1) { + list.add(i); + } + if (y == 0 || y == width - 1) { + list.add(i); + } + if (y < (width - 1) && i + 1 < total && getVal(graph, i + 1) <= val) { + weight[i][i + 1] = 1; + } + if (y != 0 && i - 1 >= 0 && getVal(graph, i - 1) <= val) { + weight[i][i - 1] = 1; + } + if (i + width < total && getVal(graph, i + width) <= val) { + weight[i][i + width] = 1; + } + if (i - width >= 0 && getVal(graph, i - width) <= val) { + weight[i][i - width] = 1; + } + } + + int start = startX * width + startY; + int resultIndex = -1; + int resultLen = 256; + for (int i = 0; i < list.size(); i++) { + if (getVal(graph, list.get(i)) <= start) { + int len = resolve(start, list.get(i)); + if (len < resultLen) { + resultLen = len; + resultIndex = list.get(i); + } + } + } + if (resultLen >= 256) { + System.out.print("-1 -1 -1"); + return; + } + System.out.print(resultIndex / graph.length + " " + resultIndex % graph.length + " " + resultLen); + } + + public static int resolve(int start, int end) { + + if (start < 0 || end < 0 || start >= weight.length || end >= weight.length) { + return MAX; + } + + boolean[] isVisited = new boolean[weight.length]; + int[] d = new int[weight.length]; + + for (int i = 0; i < weight.length; i++) { + isVisited[i] = false; + d[i] = MAX; + } + d[start] = 0; + isVisited[start] = true; + int unVisitedNum = weight.length; + int index = start; + + while (unVisitedNum > 0 && index != end) { + int min = MAX; + for (int i = 0; i < weight.length; i++) { + if (min > d[i] && !isVisited[i]) { + min = d[i]; + index = i; + } + } + for (int i = 0; i < weight.length; i++) { + if (d[index] + weight[index][i] < d[i]) { + d[i] = d[index] + weight[index][i]; + } + } + unVisitedNum--; + isVisited[index] = true; + } + + return d[end]; + } +} diff --git "a/src/\345\205\266\344\273\226/\351\230\277\346\213\211\344\274\257\346\225\260\345\255\227\350\275\254\344\270\255\346\226\207/Main.java" "b/src/\345\205\266\344\273\226/\351\230\277\346\213\211\344\274\257\346\225\260\345\255\227\350\275\254\344\270\255\346\226\207/Main.java" new file mode 100644 index 0000000..1ff4aa2 --- /dev/null +++ "b/src/\345\205\266\344\273\226/\351\230\277\346\213\211\344\274\257\346\225\260\345\255\227\350\275\254\344\270\255\346\226\207/Main.java" @@ -0,0 +1,50 @@ +package 其他.阿拉伯数字转中文; + +/** + * @author yuanguangxin + */ +public class Main { + private static final char[] numArrays = {'零', '一', '二', '三', '四', '五', '六', '七', '八', '九'}; + private static final char[] units = {'十', '百', '千', '万', '亿'}; + private static final StringBuilder ans = new StringBuilder(); + + private static void intToChineseNum(int num) { + String s = String.valueOf(num); + char[] chars = s.toCharArray(); + int n = chars.length; + + // 只剩下一位时, 直接返回 numArrays 数组中对应的数字 + if (n == 1) { + ans.append(numArrays[chars[0] - '0']); + // 如果 num 超过 5 位, 则先判断是否上亿, 然后将 num 拆分 + } else if (n >= 5) { + n = n >= 9 ? 9 : 5; + int multi = (int) Math.pow(10, n - 1); + // div 表示 num 中上亿或上万的部分数值 + int div = num / multi; + // mod 表示剩余的部分数值 + int mod = num % multi; + // 对前一部分数值进行转换, 然后添加单位万/亿 + intToChineseNum(div); + ans.append(n == 5 ? units[3] : units[4]); + String s1 = String.valueOf(div); + String s2 = String.valueOf(mod); + // 判断中间是否有 0 + if (s.charAt(s1.length() - 1) == '0' || s2.length() < n - 1) ans.append("零"); + // 转换剩余部分 + intToChineseNum(mod); + // 如果 num 不超过 5 位, 处理过程与上面相似 + } else { + int multi = (int) Math.pow(10, n - 1); + int div = num / multi; + int mod = num % multi; + ans.append(numArrays[div]).append(units[n - 2]); + if (mod != 0) { + if (String.valueOf(mod).length() < n - 1) { + ans.append("零"); + } + intToChineseNum(mod); + } + } + } +} diff --git "a/src/\345\212\250\346\200\201\350\247\204\345\210\222/q1143_\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227/Solution.java" "b/src/\345\212\250\346\200\201\350\247\204\345\210\222/q1143_\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227/Solution.java" new file mode 100644 index 0000000..9f55848 --- /dev/null +++ "b/src/\345\212\250\346\200\201\350\247\204\345\210\222/q1143_\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227/Solution.java" @@ -0,0 +1,61 @@ +package 动态规划.q1143_最长公共子序列; + +/** + * 动态规划 dp[i + 1][j + 1] = Math.max(dp[i+1][j], dp[i][j+1]) o(m*n) + *

+ * 若题目为最长公共子串,则在c1,c2不相等时不做处理(赋值0),在遍历过程中记录最大值即可 + */ +public class Solution { + + public int longestCommonSubsequence(String text1, String text2) { + int m = text1.length(); + int n = text2.length(); + int[][] dp = new int[m + 1][n + 1]; + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + char c1 = text1.charAt(i); + char c2 = text2.charAt(j); + if (c1 == c2) { + dp[i + 1][j + 1] = dp[i][j] + 1; + } else { + dp[i + 1][j + 1] = Math.max(dp[i + 1][j], dp[i][j + 1]); + } + } + } + return dp[m][n]; + } + + /** + * 最长公共字串 + * + * @param str1 + * @param str2 + * @return + */ + public static String longestCommonSubstring(String str1, String str2) { + int m = str1.length(); + int n = str2.length(); + int[][] dp = new int[m + 1][n + 1]; + int maxLength = 0; + int endIndex = -1; + + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (str1.charAt(i - 1) == str2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + if (dp[i][j] > maxLength) { + maxLength = dp[i][j]; + endIndex = i - 1; + } + } else { + dp[i][j] = 0; + } + } + } + if (maxLength == 0) { + return ""; + } + return str1.substring(endIndex - maxLength + 1, endIndex + 1); + } +} diff --git "a/src/\345\217\214\346\214\207\351\222\210\351\201\215\345\216\206/q209_\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204/Solution.java" "b/src/\345\217\214\346\214\207\351\222\210\351\201\215\345\216\206/q209_\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204/Solution.java" index 9f8a4bf..dad0410 100644 --- "a/src/\345\217\214\346\214\207\351\222\210\351\201\215\345\216\206/q209_\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204/Solution.java" +++ "b/src/\345\217\214\346\214\207\351\222\210\351\201\215\345\216\206/q209_\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204/Solution.java" @@ -14,16 +14,7 @@ public int minSubArrayLen(int s, int[] nums) { if (k == nums.length && i == nums.length) { break; } - if (sum == s) { - min = Math.min(k - i, min); - if (k < nums.length) { - sum += nums[k]; - k++; - } else { - sum -= nums[i]; - i++; - } - } else if (sum < s) { + if (sum < s) { if (k == nums.length) { break; } diff --git "a/src/\345\233\236\346\272\257\346\263\225/q40_\347\273\204\345\220\210\346\200\273\345\222\2142/Solution.java" "b/src/\345\233\236\346\272\257\346\263\225/q40_\347\273\204\345\220\210\346\200\273\345\222\2142/Solution.java" new file mode 100644 index 0000000..b6aadee --- /dev/null +++ "b/src/\345\233\236\346\272\257\346\263\225/q40_\347\273\204\345\220\210\346\200\273\345\222\2142/Solution.java" @@ -0,0 +1,42 @@ +package 回溯法.q40_组合总和2; + +import java.util.*; + +/** + * 回溯法 O(n*log(n)) + */ +class Solution { + + public List> combinationSum2(int[] candidates, int target) { + List> res = new ArrayList<>(); + if (candidates.length == 0) { + return res; + } + Arrays.sort(candidates); + helper(candidates, target, 0, new LinkedList<>(), res); + return res; + } + + public void helper(int[] candidates, int target, int start, LinkedList stack, List> res) { + if (start > candidates.length) { + return; + } + if (target == 0 && !stack.isEmpty()) { + List item = new ArrayList<>(stack); + res.add(item); + } + HashSet set = new HashSet<>(); + for (int i = start; i < candidates.length; ++i) { + if (!set.contains(candidates[i]) && target >= candidates[i]) { + stack.push(candidates[i]); + helper(candidates, target - candidates[i], i + 1, stack, res); + stack.pop(); + set.add(candidates[i]); + } + } + } + + public static void main(String[] args) { + new Solution().combinationSum2(new int[]{10, 1, 2, 7, 6, 1, 5}, 8); + } +} diff --git "a/src/\345\255\227\345\205\270\346\240\221/q648_\345\215\225\350\257\215\346\233\277\346\215\242/Solution.java" "b/src/\345\255\227\345\205\270\346\240\221/q648_\345\215\225\350\257\215\346\233\277\346\215\242/Solution.java" new file mode 100644 index 0000000..fb040fc --- /dev/null +++ "b/src/\345\255\227\345\205\270\346\240\221/q648_\345\215\225\350\257\215\346\233\277\346\215\242/Solution.java" @@ -0,0 +1,49 @@ +package 字典树.q648_单词替换; + +import java.util.List; + +/** + * 构建字典树(前缀树)o(n) + */ +class Solution { + public String replaceWords(List roots, String sentence) { + TrieNode trie = new TrieNode(); + for (String root : roots) { + TrieNode cur = trie; + for (char letter : root.toCharArray()) { + if (cur.children[letter - 'a'] == null) { + cur.children[letter - 'a'] = new TrieNode(); + } + cur = cur.children[letter - 'a']; + } + cur.word = root; + } + + StringBuilder ans = new StringBuilder(); + + for (String word : sentence.split(" ")) { + if (ans.length() > 0) { + ans.append(" "); + } + + TrieNode cur = trie; + for (char letter : word.toCharArray()) { + if (cur.children[letter - 'a'] == null || cur.word != null) { + break; + } + cur = cur.children[letter - 'a']; + } + ans.append(cur.word != null ? cur.word : word); + } + return ans.toString(); + } +} + +class TrieNode { + TrieNode[] children; + String word; + + TrieNode() { + children = new TrieNode[26]; + } +} diff --git "a/src/\345\255\227\347\254\246\344\270\262\346\223\215\344\275\234/q736_\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264/Solution.java" "b/src/\345\255\227\347\254\246\344\270\262\346\223\215\344\275\234/q763_\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264/Solution.java" similarity index 94% rename from "src/\345\255\227\347\254\246\344\270\262\346\223\215\344\275\234/q736_\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264/Solution.java" rename to "src/\345\255\227\347\254\246\344\270\262\346\223\215\344\275\234/q763_\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264/Solution.java" index de3e054..418d11b 100644 --- "a/src/\345\255\227\347\254\246\344\270\262\346\223\215\344\275\234/q736_\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264/Solution.java" +++ "b/src/\345\255\227\347\254\246\344\270\262\346\223\215\344\275\234/q763_\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264/Solution.java" @@ -1,4 +1,4 @@ -package 字符串操作.q736_划分字母区间; +package 字符串操作.q763_划分字母区间; import java.util.ArrayList; import java.util.List; diff --git "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/ListNode.java" "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/ListNode.java" index 94c7688..4aee435 100644 --- "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/ListNode.java" +++ "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/ListNode.java" @@ -1,4 +1,4 @@ -package hash相关.q141_环形链表.f1; +package 快慢指针遍历.q141_环形链表.f1; public class ListNode { diff --git "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/Solution.java" "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/Solution.java" index d2397e0..bc1f608 100644 --- "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/Solution.java" +++ "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f1/Solution.java" @@ -1,4 +1,4 @@ -package hash相关.q141_环形链表.f1; +package 快慢指针遍历.q141_环形链表.f1; import java.util.HashSet; import java.util.Set; diff --git "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/ListNode.java" "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/ListNode.java" index 07603ce..92332c4 100644 --- "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/ListNode.java" +++ "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/ListNode.java" @@ -1,4 +1,4 @@ -package hash相关.q141_环形链表.f2; +package 快慢指针遍历.q141_环形链表.f2; public class ListNode { diff --git "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/Solution.java" "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/Solution.java" index 2b1721e..c297e24 100644 --- "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/Solution.java" +++ "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q141_\347\216\257\345\275\242\351\223\276\350\241\250/f2/Solution.java" @@ -1,4 +1,4 @@ -package hash相关.q141_环形链表.f2; +package 快慢指针遍历.q141_环形链表.f2; /** * 快慢指针 o(n) diff --git "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q202_\345\277\253\344\271\220\346\225\260/Solution.java" "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q202_\345\277\253\344\271\220\346\225\260/Solution.java" index 624eb3e..76a30d7 100644 --- "a/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q202_\345\277\253\344\271\220\346\225\260/Solution.java" +++ "b/src/\345\277\253\346\205\242\346\214\207\351\222\210\351\201\215\345\216\206/q202_\345\277\253\344\271\220\346\225\260/Solution.java" @@ -1,4 +1,4 @@ -package 数字操作.q202_快乐数; +package 快慢指针遍历.q202_快乐数; /** * 快慢指针,思想同q141判断是否有环,用快慢指针找出循环终止条件 o(n) diff --git "a/src/\346\225\260\345\255\227\346\223\215\344\275\234/q1920_\345\237\272\344\272\216\346\216\222\345\210\227\346\236\204\345\273\272\346\225\260\347\273\204/Solution.java" "b/src/\346\225\260\345\255\227\346\223\215\344\275\234/q1920_\345\237\272\344\272\216\346\216\222\345\210\227\346\236\204\345\273\272\346\225\260\347\273\204/Solution.java" new file mode 100644 index 0000000..9c2cc24 --- /dev/null +++ "b/src/\346\225\260\345\255\227\346\223\215\344\275\234/q1920_\345\237\272\344\272\216\346\216\222\345\210\227\346\236\204\345\273\272\346\225\260\347\273\204/Solution.java" @@ -0,0 +1,23 @@ +package 数字操作.q1920_基于排列构建数组; + +/** + * 注意观察题目,数组中数字为[0,999]闭区间 + */ +class Solution { + public int[] buildArray(int[] nums) { + int n = nums.length; + for (int i = 0; i < n; i++) { + nums[i] += 1000 * (nums[nums[i]] % 1000); + System.out.println(nums[i]); + } + for (int i = 0; i < n; i++) { + nums[i] /= 1000; + } + return nums; + } + + public static void main(String[] args) { + int[] nums = new int[]{3, 2, 0, 1, 4}; + new Solution().buildArray(nums); + } +} diff --git "a/src/\346\225\260\347\273\204\346\223\215\344\275\234/q384_\346\211\223\344\271\261\346\225\260\347\273\204/Solution.java" "b/src/\346\225\260\347\273\204\346\223\215\344\275\234/q384_\346\211\223\344\271\261\346\225\260\347\273\204/Solution.java" new file mode 100644 index 0000000..f7a5831 --- /dev/null +++ "b/src/\346\225\260\347\273\204\346\223\215\344\275\234/q384_\346\211\223\344\271\261\346\225\260\347\273\204/Solution.java" @@ -0,0 +1,46 @@ +package 数组操作.q384_打乱数组; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * 洗牌算法 o(n) + */ +public class Solution { + private int[] array; + private int[] original; + + private Random rand = new Random(); + + private List getArrayCopy() { + List asList = new ArrayList<>(); + for (int i = 0; i < array.length; i++) { + asList.add(array[i]); + } + return asList; + } + + public Solution(int[] nums) { + array = nums; + original = nums.clone(); + } + + public int[] reset() { + array = original; + original = original.clone(); + return array; + } + + public int[] shuffle() { + List aux = getArrayCopy(); + + for (int i = 0; i < array.length; i++) { + int removeIdx = rand.nextInt(aux.size()); + array[i] = aux.get(removeIdx); + aux.remove(removeIdx); + } + + return array; + } +} \ No newline at end of file diff --git "a/src/\346\225\260\347\273\204\346\223\215\344\275\234/q581_\346\234\200\347\237\255\346\227\240\345\272\217\350\277\236\347\273\255\345\255\220\346\225\260\347\273\204/Solution.java" "b/src/\346\225\260\347\273\204\346\223\215\344\275\234/q581_\346\234\200\347\237\255\346\227\240\345\272\217\350\277\236\347\273\255\345\255\220\346\225\260\347\273\204/Solution.java" new file mode 100644 index 0000000..491b5ae --- /dev/null +++ "b/src/\346\225\260\347\273\204\346\223\215\344\275\234/q581_\346\234\200\347\237\255\346\227\240\345\272\217\350\277\236\347\273\255\345\255\220\346\225\260\347\273\204/Solution.java" @@ -0,0 +1,32 @@ +package 数组操作.q581_最短无序连续子数组; + +import java.util.Arrays; + +/** + * 利用排序 o(n*log(n)) + */ +public class Solution { + + public int findUnsortedSubarray(int[] nums) { + if (nums == null || nums.length < 1) { + return 0; + } + + int[] cloneNums = nums.clone(); + Arrays.sort(nums); + + int begin = Integer.MAX_VALUE; + int end = 0; + for (int i = 0; i < nums.length; i++) { + if (nums[i] != cloneNums[i]) { + begin = Math.min(begin, i); + end = Math.max(end, i); + } + } + return Math.max(end - begin + 1, 0); + } + + public static void main(String[] args) { + new Solution().findUnsortedSubarray(new int[]{2, 6, 4, 8, 10, 9, 15}); + } +} diff --git "a/src/\346\225\260\347\273\204\346\223\215\344\275\234/q78_\345\255\220\351\233\206/Solution.java" "b/src/\346\225\260\347\273\204\346\223\215\344\275\234/q78_\345\255\220\351\233\206/Solution.java" new file mode 100644 index 0000000..d47219e --- /dev/null +++ "b/src/\346\225\260\347\273\204\346\223\215\344\275\234/q78_\345\255\220\351\233\206/Solution.java" @@ -0,0 +1,24 @@ +package 数组操作.q78_子集; + +import java.util.ArrayList; +import java.util.List; + +/** + * 向子集中添加子集合 o(n*2^n) + */ +public class Solution { + + public List> subsets(int[] nums) { + List> result = new ArrayList<>(); + result.add(new ArrayList<>()); + for (int i = 0; i < nums.length; i++) { + int size = result.size(); + for (int j = 0; j < size; j++) { + List temp = new ArrayList<>(result.get(j)); + temp.add(nums[i]); + result.add(temp); + } + } + return result; + } +} diff --git "a/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/f1/MyQueue.java" "b/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/f1/MyQueue.java" new file mode 100644 index 0000000..9a6f4cf --- /dev/null +++ "b/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/f1/MyQueue.java" @@ -0,0 +1,51 @@ +package 栈相关.q232_用栈实现队列.f1; + +import java.util.Stack; + +/** + * 双栈 入队o(n) 出队o(1) + */ +class MyQueue { + + private Stack s1 = new Stack<>(); + private Stack s2 = new Stack<>(); + private Integer front; + + /** Initialize your data structure here. */ + public MyQueue() { + + } + + /** Push element x to the back of queue. */ + public void push(int x) { + if (s1.empty()){ + front = x; + } + while (!s1.isEmpty()){ + s2.push(s1.pop()); + } + s2.push(x); + while (!s2.isEmpty()){ + s1.push(s2.pop()); + } + } + + /** Removes the element from in front of queue and returns that element. */ + public int pop() { + int value = s1.pop(); + if (!s1.empty()){ + front = s1.peek(); + } + return value; + } + + /** Get the front element. */ + public int peek() { + return front; + } + + /** Returns whether the queue is empty. */ + public boolean empty() { + return s1.isEmpty(); + } +} diff --git "a/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/f2/MyQueue.java" "b/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/f2/MyQueue.java" new file mode 100644 index 0000000..22a6e97 --- /dev/null +++ "b/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/f2/MyQueue.java" @@ -0,0 +1,49 @@ +package 栈相关.q232_用栈实现队列.f2; + +import java.util.Stack; + +/** + * 双栈 入队o(1) 出队平均o(1),最坏o(n) + */ +class MyQueue { + + private Stack s1 = new Stack<>(); + private Stack s2 = new Stack<>(); + private Integer front; + + /** Initialize your data structure here. */ + public MyQueue() { + + } + + /** Push element x to the back of queue. */ + public void push(int x) { + if (s1.empty()){ + front = x; + } + s1.push(x); + } + + /** Removes the element from in front of queue and returns that element. */ + public int pop() { + if (s2.isEmpty()) { + while (!s1.isEmpty()){ + s2.push(s1.pop()); + } + } + return s2.pop(); + } + + /** Get the front element. */ + public int peek() { + if (!s2.isEmpty()) { + return s2.peek(); + } + return front; + } + + /** Returns whether the queue is empty. */ + public boolean empty() { + return s1.isEmpty() && s2.isEmpty(); + } +} diff --git "a/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/\345\220\253\346\234\211\346\234\200\345\244\247\345\200\274\347\232\204\351\230\237\345\210\227/MaxQueue.java" "b/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/\345\220\253\346\234\211\346\234\200\345\244\247\345\200\274\347\232\204\351\230\237\345\210\227/MaxQueue.java" new file mode 100644 index 0000000..32562be --- /dev/null +++ "b/src/\346\240\210\347\233\270\345\205\263/q232_\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227/\345\220\253\346\234\211\346\234\200\345\244\247\345\200\274\347\232\204\351\230\237\345\210\227/MaxQueue.java" @@ -0,0 +1,34 @@ +package 栈相关.q232_用栈实现队列.含有最大值的队列; + +import java.util.LinkedList; +import java.util.Queue; + +public class MaxQueue { + + private Queue queue; + private LinkedList max; + + public MaxQueue() { + queue = new LinkedList<>(); + max = new LinkedList<>(); + } + + public int max_value() { + return max.size() == 0 ? -1 : max.getFirst(); + } + + public void push_back(int value) { + queue.add(value); + while (max.size() != 0 && max.getLast() < value) { + max.removeLast(); + } + max.add(value); + } + + public int pop_front() { + if (max.size() != 0 && queue.peek().equals(max.getFirst())) { + max.removeFirst(); + } + return queue.size() == 0 ? -1 : queue.poll(); + } +} diff --git "a/src/\346\240\221\347\232\204\351\201\215\345\216\206/q103_\344\272\214\345\217\211\346\240\221\347\232\204\351\224\257\351\275\277\345\275\242\345\261\202\345\272\217\351\201\215\345\216\206/Solution.java" "b/src/\346\240\221\347\232\204\351\201\215\345\216\206/q103_\344\272\214\345\217\211\346\240\221\347\232\204\351\224\257\351\275\277\345\275\242\345\261\202\345\272\217\351\201\215\345\216\206/Solution.java" new file mode 100644 index 0000000..dff5487 --- /dev/null +++ "b/src/\346\240\221\347\232\204\351\201\215\345\216\206/q103_\344\272\214\345\217\211\346\240\221\347\232\204\351\224\257\351\275\277\345\275\242\345\261\202\345\272\217\351\201\215\345\216\206/Solution.java" @@ -0,0 +1,44 @@ +package 树的遍历.q103_二叉树的锯齿形层序遍历; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +/** + * 和层序遍历相同,额外加一个标记控制插入队列的位置 + */ +public class Solution { + + public List> zigzagLevelOrder(TreeNode root) { + List> ans = new ArrayList<>(); + if (root == null) { + return ans; + } + Queue queue = new LinkedList<>(); + queue.add(root); + boolean flag = true; + while (!queue.isEmpty()) { + List list = new ArrayList<>(); + int size = queue.size(); + while (size > 0) { + TreeNode tn = queue.poll(); + if (flag) { + list.add(tn.val); + } else { + list.add(0, tn.val); + } + if (tn.left != null) { + queue.add(tn.left); + } + if (tn.right != null) { + queue.add(tn.right); + } + size--; + } + flag = !flag; + ans.add(list); + } + return ans; + } +} diff --git "a/src/\346\240\221\347\232\204\351\201\215\345\216\206/q103_\344\272\214\345\217\211\346\240\221\347\232\204\351\224\257\351\275\277\345\275\242\345\261\202\345\272\217\351\201\215\345\216\206/TreeNode.java" "b/src/\346\240\221\347\232\204\351\201\215\345\216\206/q103_\344\272\214\345\217\211\346\240\221\347\232\204\351\224\257\351\275\277\345\275\242\345\261\202\345\272\217\351\201\215\345\216\206/TreeNode.java" new file mode 100644 index 0000000..931d109 --- /dev/null +++ "b/src/\346\240\221\347\232\204\351\201\215\345\216\206/q103_\344\272\214\345\217\211\346\240\221\347\232\204\351\224\257\351\275\277\345\275\242\345\261\202\345\272\217\351\201\215\345\216\206/TreeNode.java" @@ -0,0 +1,11 @@ +package 树的遍历.q103_二叉树的锯齿形层序遍历; + +public class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int x) { + val = x; + } +} diff --git "a/src/\351\200\222\345\275\222/q1325_\345\210\240\351\231\244\347\273\231\345\256\232\345\200\274\347\232\204\345\217\266\345\255\220\350\212\202\347\202\271/Solution.java" "b/src/\351\200\222\345\275\222/q1325_\345\210\240\351\231\244\347\273\231\345\256\232\345\200\274\347\232\204\345\217\266\345\255\220\350\212\202\347\202\271/Solution.java" new file mode 100644 index 0000000..88e89d9 --- /dev/null +++ "b/src/\351\200\222\345\275\222/q1325_\345\210\240\351\231\244\347\273\231\345\256\232\345\200\274\347\232\204\345\217\266\345\255\220\350\212\202\347\202\271/Solution.java" @@ -0,0 +1,21 @@ +package 递归.q1325_删除给定值的叶子节点; + +/** + * 递归 o(n) + */ +public class Solution { + + public TreeNode removeLeafNodes(TreeNode root, int target) { + if (root == null) { + return null; + } + + root.left = removeLeafNodes(root.left, target); + root.right = removeLeafNodes(root.right, target); + + if (root.val == target && root.left == null && root.right == null) { + return null; + } + return root; + } +} diff --git "a/src/\351\200\222\345\275\222/q1325_\345\210\240\351\231\244\347\273\231\345\256\232\345\200\274\347\232\204\345\217\266\345\255\220\350\212\202\347\202\271/TreeNode.java" "b/src/\351\200\222\345\275\222/q1325_\345\210\240\351\231\244\347\273\231\345\256\232\345\200\274\347\232\204\345\217\266\345\255\220\350\212\202\347\202\271/TreeNode.java" new file mode 100644 index 0000000..3101161 --- /dev/null +++ "b/src/\351\200\222\345\275\222/q1325_\345\210\240\351\231\244\347\273\231\345\256\232\345\200\274\347\232\204\345\217\266\345\255\220\350\212\202\347\202\271/TreeNode.java" @@ -0,0 +1,20 @@ +package 递归.q1325_删除给定值的叶子节点; + +public class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode() { + } + + TreeNode(int val) { + this.val = val; + } + + TreeNode(int val, TreeNode left, TreeNode right) { + this.val = val; + this.left = left; + this.right = right; + } +} diff --git "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q160_\347\233\270\344\272\244\351\223\276\350\241\250/ListNode.java" "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q160_\347\233\270\344\272\244\351\223\276\350\241\250/ListNode.java" new file mode 100644 index 0000000..f3da319 --- /dev/null +++ "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q160_\347\233\270\344\272\244\351\223\276\350\241\250/ListNode.java" @@ -0,0 +1,11 @@ +package 链表操作.q160_相交链表; + +public class ListNode { + int val; + ListNode next; + + ListNode(int x) { + val = x; + next = null; + } +} diff --git "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q160_\347\233\270\344\272\244\351\223\276\350\241\250/Solution.java" "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q160_\347\233\270\344\272\244\351\223\276\350\241\250/Solution.java" new file mode 100644 index 0000000..8009847 --- /dev/null +++ "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q160_\347\233\270\344\272\244\351\223\276\350\241\250/Solution.java" @@ -0,0 +1,28 @@ +package 链表操作.q160_相交链表; + +import java.util.HashSet; +import java.util.Set; + +/** + * 哈希存储 + * + * 方法二:两个链表相连,快慢指针判断是否有环(省略) + */ +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + Set visited = new HashSet<>(); + ListNode temp = headA; + while (temp != null) { + visited.add(temp); + temp = temp.next; + } + temp = headB; + while (temp != null) { + if (visited.contains(temp)) { + return temp; + } + temp = temp.next; + } + return null; + } +} diff --git "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f1/Solution.java" "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f1/Solution.java" index 8e21915..bcd4a1c 100644 --- "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f1/Solution.java" +++ "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f1/Solution.java" @@ -1,28 +1,19 @@ package 链表操作.q206_反转链表.f1; -import java.util.ArrayList; -import java.util.List; - /** - * 暴力法舍弃空间 o(n) + * 遍历直接反向修改next指针 o(n) */ class Solution { + public ListNode reverseList(ListNode head) { - if (head == null || head.next == null) { - return head; - } - List list = new ArrayList<>(); + ListNode pre = null; ListNode temp = head; while (temp != null) { - list.add(temp.val); - temp = temp.next; - } - ListNode rs = new ListNode(list.get(list.size() - 1)); - ListNode t1 = rs; - for (int i = list.size() - 2; i >= 0; i--) { - t1.next = new ListNode(list.get(i)); - t1 = t1.next; + ListNode t = temp.next; + temp.next = pre; + pre = temp; + temp = t; } - return rs; + return pre; } } diff --git "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f2/Solution.java" "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f2/Solution.java" index d65acaf..479ed78 100644 --- "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f2/Solution.java" +++ "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q206_\345\217\215\350\275\254\351\223\276\350\241\250/f2/Solution.java" @@ -1,18 +1,17 @@ package 链表操作.q206_反转链表.f2; /** - * 遍历直接反向修改next指针 o(n) + * 递归法 o(n) */ class Solution { + public ListNode reverseList(ListNode head) { - ListNode pre = null; - ListNode temp = head; - while (temp != null) { - ListNode t = temp.next; - temp.next = pre; - pre = temp; - temp = t; + if (head == null || head.next == null) { + return head; } - return pre; + ListNode p = reverseList(head.next); + head.next.next = head; + head.next = null; + return p; } } diff --git "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q25_k\344\270\252\344\270\200\347\273\204\347\277\273\350\275\254\351\223\276\350\241\250/ListNode.java" "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q25_k\344\270\252\344\270\200\347\273\204\347\277\273\350\275\254\351\223\276\350\241\250/ListNode.java" new file mode 100644 index 0000000..8c6845a --- /dev/null +++ "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q25_k\344\270\252\344\270\200\347\273\204\347\277\273\350\275\254\351\223\276\350\241\250/ListNode.java" @@ -0,0 +1,12 @@ +package 链表操作.q25_k个一组翻转链表; + + +public class ListNode { + int val; + ListNode next; + + ListNode(int x) { + val = x; + } +} + diff --git "a/src/\351\223\276\350\241\250\346\223\215\344\275\234/q25_k\344\270\252\344\270\200\347\273\204\347\277\273\350\275\254\351\223\276\350\241\250/Solution.java" "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q25_k\344\270\252\344\270\200\347\273\204\347\277\273\350\275\254\351\223\276\350\241\250/Solution.java" new file mode 100644 index 0000000..ab4fe58 --- /dev/null +++ "b/src/\351\223\276\350\241\250\346\223\215\344\275\234/q25_k\344\270\252\344\270\200\347\273\204\347\277\273\350\275\254\351\223\276\350\241\250/Solution.java" @@ -0,0 +1,45 @@ +package 链表操作.q25_k个一组翻转链表; + +/** + * 难点在于返回每个部分被修改的头节点,新建一个头节点的前置节点 o(n) + */ +public class Solution { + + public ListNode reverseKGroup(ListNode head, int k) { + ListNode hair = new ListNode(0); + hair.next = head; + + ListNode pre = hair; + ListNode end = hair; + + while (end.next != null) { + for (int i = 0; i < k && end != null; i++){ + end = end.next; + } + if (end == null){ + break; + } + ListNode start = pre.next; + ListNode next = end.next; + end.next = null; + pre.next = reverse(start); + start.next = next; + pre = start; + + end = pre; + } + return hair.next; + } + + private ListNode reverse(ListNode head) { + ListNode pre = null; + ListNode curr = head; + while (curr != null) { + ListNode next = curr.next; + curr.next = pre; + pre = curr; + curr = next; + } + return pre; + } +}