diff --git a/README.md b/README.md
index 12781e77..d283d9ba 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,14 @@
+《代码随想录》正式出版啦!!录友专属福利,点击下方可以享五折优惠!详细可以点击这里
+
+
+
+
+
+
+
# LeetCode 刷题攻略
@@ -115,7 +123,7 @@
* 算法性能分析
* [关于时间复杂度,你不知道的都在这里!](./problems/前序/关于时间复杂度,你不知道的都在这里!.md)
- * [O(n)的算法居然超时了,此时的n究竟是多大?](./problems/前序/On的算法居然超时了,此时的n究竟是多大?.md)
+ * [$O(n)$的算法居然超时了,此时的n究竟是多大?](./problems/前序/On的算法居然超时了,此时的n究竟是多大?.md)
* [通过一道面试题目,讲一讲递归算法的时间复杂度!](./problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md)
* [本周小结!(算法性能分析系列一)](./problems/周总结/20201210复杂度分析周末总结.md)
* [关于空间复杂度,可能有几个疑问?](./problems/前序/关于空间复杂度,可能有几个疑问?.md)
diff --git a/problems/0001.两数之和.md b/problems/0001.两数之和.md
index 4f9dbbad..f612a110 100644
--- a/problems/0001.两数之和.md
+++ b/problems/0001.两数之和.md
@@ -24,7 +24,7 @@
## 思路
-很明显暴力的解法是两层for循环查找,时间复杂度是O(n^2)。
+很明显暴力的解法是两层for循环查找,时间复杂度是$O(n^2)$。
建议大家做这道题目之前,先做一下这两道
* [242. 有效的字母异位词](https://www.programmercarl.com/0242.有效的字母异位词.html)
@@ -35,17 +35,17 @@
本题呢,则要使用map,那么来看一下使用数组和set来做哈希法的局限。
* 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
-* set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下表位置,因为要返回x 和 y的下表。所以set 也不能用。
+* set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
-此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下表。
+此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。
C++中map,有三种类型:
|映射 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率|
|---|---| --- |---| --- | --- | ---|
-|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | O(logn)|O(logn) |
-|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|O(logn) |O(logn) |
-|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |O(1) | O(1)|
+|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | $O(\log n)$|$O(\log n)$ |
+|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|$O(\log n)$ |$O(\log n)$ |
+|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |$O(1)$ | $O(1)$|
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。
diff --git a/problems/0005.最长回文子串.md b/problems/0005.最长回文子串.md
index cb022ac3..c54de039 100644
--- a/problems/0005.最长回文子串.md
+++ b/problems/0005.最长回文子串.md
@@ -38,7 +38,7 @@
两层for循环,遍历区间起始位置和终止位置,然后判断这个区间是不是回文。
-时间复杂度:O(n^3)
+时间复杂度:$O(n^3)$
## 动态规划
@@ -205,8 +205,8 @@ public:
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(n^2)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n^2)$
## 双指针
@@ -253,8 +253,8 @@ public:
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(1)$
diff --git a/problems/0015.三数之和.md b/problems/0015.三数之和.md
index 1e675254..38158bc6 100644
--- a/problems/0015.三数之和.md
+++ b/problems/0015.三数之和.md
@@ -39,7 +39,7 @@
去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
-时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
+时间复杂度可以做到$O(n^2)$,但还是比较费时的,因为不好做剪枝操作。
大家可以尝试使用哈希法写一写,就知道其困难的程度了。
@@ -85,7 +85,7 @@ public:
**其实这道题目使用哈希法并不十分合适**,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。
-而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。
+而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是$O(n^2)$,也是可以在leetcode上通过,但是程序的执行时间依然比较长 。
接下来我来介绍另一个解法:双指针法,**这道题目使用双指针法 要比哈希法高效一些**,那么来讲解一下具体实现的思路。
@@ -101,7 +101,7 @@ public:
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
-时间复杂度:O(n^2)。
+时间复杂度:$O(n^2)$。
C++代码代码如下:
diff --git a/problems/0017.电话号码的字母组合.md b/problems/0017.电话号码的字母组合.md
index 0f99c879..b3bbf122 100644
--- a/problems/0017.电话号码的字母组合.md
+++ b/problems/0017.电话号码的字母组合.md
@@ -39,7 +39,7 @@
可以使用map或者定义一个二位数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下:
-```
+```cpp
const string letterMap[10] = {
"", // 0
"", // 1
@@ -79,7 +79,7 @@ const string letterMap[10] = {
代码如下:
-```
+```cpp
vector result;
string s;
void backtracking(const string& digits, int index)
@@ -95,7 +95,7 @@ void backtracking(const string& digits, int index)
代码如下:
-```
+```cpp
if (index == digits.size()) {
result.push_back(s);
return;
@@ -281,7 +281,7 @@ class Solution {
## Python
**回溯**
-```python3
+```python
class Solution:
def __init__(self):
self.answers: List[str] = []
@@ -317,7 +317,7 @@ class Solution:
self.answer = self.answer[:-1] # 回溯
```
**回溯简化**
-```python3
+```python
class Solution:
def __init__(self):
self.answers: List[str] = []
@@ -420,7 +420,8 @@ var letterCombinations = function(digits) {
};
```
-C:
+## C
+
```c
char* path;
int pathTop;
@@ -481,6 +482,47 @@ char ** letterCombinations(char * digits, int* returnSize){
}
```
+## Swift
+
+```swift
+func letterCombinations(_ digits: String) -> [String] {
+ // 按键与字母串映射
+ let letterMap = [
+ "",
+ "", "abc", "def",
+ "ghi", "jkl", "mno",
+ "pqrs", "tuv", "wxyz"
+ ]
+ // 把输入的按键字符串转成Int数组
+ let baseCode = ("0" as Character).asciiValue!
+ let digits = digits.map { c in
+ guard let code = c.asciiValue else { return -1 }
+ return Int(code - baseCode)
+ }.filter { $0 >= 0 && $0 <= 9 }
+ guard !digits.isEmpty else { return [] }
+
+ var result = [String]()
+ var s = ""
+ func backtracking(index: Int) {
+ // 结束条件:收集结果
+ if index == digits.count {
+ result.append(s)
+ return
+ }
+
+ // 遍历当前按键对应的字母串
+ let letters = letterMap[digits[index]]
+ for letter in letters {
+ s.append(letter) // 处理
+ backtracking(index: index + 1) // 递归,记得+1
+ s.removeLast() // 回溯
+ }
+ }
+ backtracking(index: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0018.四数之和.md b/problems/0018.四数之和.md
index cf9ba9c1..25350b7a 100644
--- a/problems/0018.四数之和.md
+++ b/problems/0018.四数之和.md
@@ -33,13 +33,13 @@
但是有一些细节需要注意,例如: 不要判断`nums[k] > target` 就返回了,三数之和 可以通过 `nums[i] > 0` 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。(大家亲自写代码就能感受出来)
-[15.三数之和](https://programmercarl.com/0015.三数之和.html)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下表作为双指针,找到nums[i] + nums[left] + nums[right] == 0。
+[15.三数之和](https://programmercarl.com/0015.三数之和.html)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] + nums[left] + nums[right] == 0。
-四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下表作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。
+四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是$O(n^2)$,四数之和的时间复杂度是$O(n^3)$ 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。
-对于[15.三数之和](https://programmercarl.com/0015.三数之和.html)双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。
+对于[15.三数之和](https://programmercarl.com/0015.三数之和.html)双指针法就是将原本暴力$O(n^3)$的解法,降为$O(n^2)$的解法,四数之和的双指针解法就是将原本暴力$O(n^4)$的解法,降为$O(n^3)$的解法。
之前我们讲过哈希表的经典题目:[454.四数相加II](https://programmercarl.com/0454.四数相加II.html),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。
@@ -47,7 +47,7 @@
我们来回顾一下,几道题目使用了双指针法。
-双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下:
+双指针法将时间复杂度:$O(n^2)$的解法优化为 $O(n)$的解法。也就是降一个数量级,题目如下:
* [27.移除元素](https://programmercarl.com/0027.移除元素.html)
* [15.三数之和](https://programmercarl.com/0015.三数之和.html)
diff --git a/problems/0024.两两交换链表中的节点.md b/problems/0024.两两交换链表中的节点.md
index 4f1f7956..aa1e1118 100644
--- a/problems/0024.两两交换链表中的节点.md
+++ b/problems/0024.两两交换链表中的节点.md
@@ -62,6 +62,7 @@ public:
}
};
```
+
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
@@ -73,7 +74,7 @@ public:
上面的代码我第一次提交执行用时8ms,打败6.5%的用户,差点吓到我了。
-心想应该没有更好的方法了吧,也就O(n)的时间复杂度,重复提交几次,这样了:
+心想应该没有更好的方法了吧,也就$O(n)$的时间复杂度,重复提交几次,这样了:

@@ -85,7 +86,7 @@ public:
## 其他语言版本
C:
-```
+```c
/**
* Definition for singly-linked list.
* struct ListNode {
diff --git a/problems/0027.移除元素.md b/problems/0027.移除元素.md
index 9e8e2c33..b0376f51 100644
--- a/problems/0027.移除元素.md
+++ b/problems/0027.移除元素.md
@@ -11,7 +11,7 @@
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
-不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并**原地**修改输入数组。
+不要使用额外的数组空间,你必须仅使用 $O(1)$ 额外空间并**原地**修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
@@ -58,7 +58,7 @@ public:
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
- i--; // 因为下表i以后的数值都向前移动了一位,所以i也向前移动一位
+ i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
@@ -184,8 +184,8 @@ func removeElement(nums []int, val int) int {
JavaScript:
```javascript
-//时间复杂度O(n)
-//空间复杂度O(1)
+//时间复杂度:O(n)
+//空间复杂度:O(1)
var removeElement = (nums, val) => {
let k = 0;
for(let i = 0;i < nums.length;i++){
diff --git a/problems/0028.实现strStr.md b/problems/0028.实现strStr.md
index 1c654dd4..cc73420b 100644
--- a/problems/0028.实现strStr.md
+++ b/problems/0028.实现strStr.md
@@ -229,9 +229,9 @@ next数组就可以是前缀表,但是很多实现都是把前缀表统一减
# 时间复杂度分析
-其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
+其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是$O(n)$,之前还要单独生成next数组,时间复杂度是$O(m)$。所以整个KMP算法的时间复杂度是$O(n+m)$的。
-暴力的解法显而易见是O(n * m),所以**KMP在字符串匹配中极大的提高的搜索的效率。**
+暴力的解法显而易见是$O(n × m)$,所以**KMP在字符串匹配中极大的提高的搜索的效率。**
为了和力扣题目28.实现strStr保持一致,方便大家理解,以下文章统称haystack为文本串, needle为模式串。
@@ -894,7 +894,58 @@ var strStr = function (haystack, needle) {
};
```
+Swift 版本
+> 前缀表统一减一
+
+```swift
+func strStr(_ haystack: String, _ needle: String) -> Int {
+
+ let s = Array(haystack), p = Array(needle)
+ guard p.count != 0 else { return 0 }
+
+ // 2 pointer
+ var j = -1
+ var next = [Int](repeating: -1, count: needle.count)
+ // KMP
+ getNext(&next, needle: p)
+ for i in 0 ..< s.count {
+ while j >= 0 && s[i] != p[j + 1] {
+ //不匹配之后寻找之前匹配的位置
+ j = next[j]
+ }
+ if s[i] == p[j + 1] {
+ //匹配,双指针同时后移
+ j += 1
+ }
+ if j == (p.count - 1) {
+ //出现匹配字符串
+ return i - p.count + 1
+ }
+ }
+ return -1
+}
+
+//前缀表统一减一
+func getNext(_ next: inout [Int], needle: [Character]) {
+
+ var j: Int = -1
+ next[0] = j
+
+ // i 从 1 开始
+ for i in 1 ..< needle.count {
+ while j >= 0 && needle[i] != needle[j + 1] {
+ j = next[j]
+ }
+ if needle[i] == needle[j + 1] {
+ j += 1;
+ }
+ next[i] = j
+ }
+ print(next)
+}
+
+```
-----------------------
diff --git a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md b/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md
index f3967253..44f13295 100644
--- a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md
+++ b/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md
@@ -11,7 +11,7 @@
如果数组中不存在目标值 target,返回 [-1, -1]。
-进阶:你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
+进阶:你可以设计并实现时间复杂度为 $O(\log n)$ 的算法解决此问题吗?
示例 1:
diff --git a/problems/0035.搜索插入位置.md b/problems/0035.搜索插入位置.md
index 0b902c5b..1f66e6bd 100644
--- a/problems/0035.搜索插入位置.md
+++ b/problems/0035.搜索插入位置.md
@@ -73,8 +73,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
效率如下:
@@ -82,7 +82,7 @@ public:
### 二分法
-既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。
+既然暴力解法的时间复杂度是$O(n)$,就要尝试一下使用二分查找法。

@@ -90,7 +90,7 @@ public:
以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。**
-同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的。
+同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。
大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。
@@ -140,8 +140,9 @@ public:
}
};
```
-* 时间复杂度:O(logn)
-* 时间复杂度:O(1)
+
+* 时间复杂度:$O(\log n)$
+* 时间复杂度:$O(1)$
效率如下:

@@ -183,8 +184,8 @@ public:
};
```
-* 时间复杂度:O(logn)
-* 时间复杂度:O(1)
+* 时间复杂度:$O(\log n)$
+* 时间复杂度:$O(1)$
## 总结
diff --git a/problems/0039.组合总和.md b/problems/0039.组合总和.md
index c2c7530d..81be41fd 100644
--- a/problems/0039.组合总和.md
+++ b/problems/0039.组合总和.md
@@ -365,7 +365,7 @@ func backtracking(startIndex,sum,target int,candidates,trcak []int,res *[][]int)
}
```
-## JavaScript:
+## JavaScript
```js
var combinationSum = function(candidates, target) {
@@ -447,5 +447,32 @@ int** combinationSum(int* candidates, int candidatesSize, int target, int* retur
}
```
+## Swift
+
+```swift
+func combinationSum(_ candidates: [Int], _ target: Int) -> [[Int]] {
+ var result = [[Int]]()
+ var path = [Int]()
+ func backtracking(sum: Int, startIndex: Int) {
+ // 终止条件
+ if sum > target { return }
+ if sum == target {
+ result.append(path)
+ return
+ }
+
+ let end = candidates.count
+ guard startIndex < end else { return }
+ for i in startIndex ..< end {
+ path.append(candidates[i]) // 处理
+ backtracking(sum: sum + candidates[i], startIndex: i) // sum这里用新变量完成回溯,i不用+1以重复访问
+ path.removeLast() // 回溯
+ }
+ }
+ backtracking(sum: 0, startIndex: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0040.组合总和II.md b/problems/0040.组合总和II.md
index b5e511e7..ac1970e1 100644
--- a/problems/0040.组合总和II.md
+++ b/problems/0040.组合总和II.md
@@ -381,7 +381,7 @@ class Solution:
sum_ -= candidates[i] # 回溯,为了下一轮for loop
```
-## Go:
+## Go
主要在于如何在回溯中去重
```go
@@ -424,7 +424,7 @@ func backtracking(startIndex,sum,target int,candidates,trcak []int,res *[][]int,
}
```
-## javaScript:
+## javaScript
```js
/**
@@ -550,5 +550,37 @@ int** combinationSum2(int* candidates, int candidatesSize, int target, int* retu
}
```
+## Swift
+
+```swift
+func combinationSum2(_ candidates: [Int], _ target: Int) -> [[Int]] {
+ // 为了方便去重复,先对集合排序
+ let candidates = candidates.sorted()
+ var result = [[Int]]()
+ var path = [Int]()
+ func backtracking(sum: Int, startIndex: Int) {
+ // 终止条件
+ if sum == target {
+ result.append(path)
+ return
+ }
+
+ let end = candidates.count
+ guard startIndex < end else { return }
+ for i in startIndex ..< end {
+ if i > startIndex, candidates[i] == candidates[i - 1] { continue } // 去重复
+ let sum = sum + candidates[i] // 使用局部变量隐藏回溯
+ if sum > target { continue } // 剪枝
+
+ path.append(candidates[i]) // 处理
+ backtracking(sum: sum, startIndex: i + 1) // i+1避免重复访问
+ path.removeLast() // 回溯
+ }
+ }
+ backtracking(sum: 0, startIndex: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0042.接雨水.md b/problems/0042.接雨水.md
index 3c1577a8..2022f6cc 100644
--- a/problems/0042.接雨水.md
+++ b/problems/0042.接雨水.md
@@ -129,8 +129,8 @@ public:
};
```
-因为每次遍历列的时候,还要向两边寻找最高的列,所以时间复杂度为O(n^2)。
-空间复杂度为O(1)。
+因为每次遍历列的时候,还要向两边寻找最高的列,所以时间复杂度为$O(n^2)$。
+空间复杂度为$O(1)$。
@@ -778,8 +778,9 @@ int trap(int* height, int heightSize) {
return ans;
}
```
-时间复杂度 O(n)
-空间复杂度 O(1)
+
+* 时间复杂度 $O(n)$
+* 空间复杂度 $O(1)$
-----------------------
diff --git a/problems/0046.全排列.md b/problems/0046.全排列.md
index 18005961..a37f6da3 100644
--- a/problems/0046.全排列.md
+++ b/problems/0046.全排列.md
@@ -44,7 +44,7 @@
* 递归函数参数
-**首先排列是有序的,也就是说[1,2] 和[2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方**。
+**首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方**。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
@@ -54,7 +54,7 @@
代码如下:
-```
+```cpp
vector> result;
vector path;
void backtracking (vector& nums, vector& used)
@@ -72,7 +72,7 @@ void backtracking (vector& nums, vector& used)
代码如下:
-```
+```cpp
// 此时说明找到了一组
if (path.size() == nums.size()) {
result.push_back(path);
@@ -90,7 +90,7 @@ if (path.size() == nums.size()) {
代码如下:
-```
+```cpp
for (int i = 0; i < nums.size(); i++) {
if (used[i] == true) continue; // path里已经收录的元素,直接跳过
used[i] = true;
@@ -179,7 +179,7 @@ class Solution {
}
}
}
-```
+```
```java
// 解法2:通过判断path中是否存在数字,排除已经选择的数字
@@ -331,7 +331,8 @@ var permute = function(nums) {
```
-C:
+### C
+
```c
int* path;
int pathTop;
@@ -398,6 +399,35 @@ int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes)
}
```
+### Swift
+
+```swift
+func permute(_ nums: [Int]) -> [[Int]] {
+ var result = [[Int]]()
+ var path = [Int]()
+ var used = [Bool](repeating: false, count: nums.count) // 记录path中已包含的元素
+ func backtracking() {
+ // 结束条件,收集结果
+ if path.count == nums.count {
+ result.append(path)
+ return
+ }
+
+ for i in 0 ..< nums.count {
+ if used[i] { continue } // 排除已包含的元素
+ used[i] = true
+ path.append(nums[i])
+ backtracking()
+ // 回溯
+ path.removeLast()
+ used[i] = false
+ }
+ }
+ backtracking()
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0047.全排列II.md b/problems/0047.全排列II.md
index d5d1528b..167e4b76 100644
--- a/problems/0047.全排列II.md
+++ b/problems/0047.全排列II.md
@@ -97,14 +97,15 @@ public:
大家发现,去重最为关键的代码为:
-```
+```cpp
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
```
**如果改成 `used[i - 1] == true`, 也是正确的!**,去重代码如下:
-```
+
+```cpp
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}
@@ -131,13 +132,13 @@ if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
## 总结
这道题其实还是用了我们之前讲过的去重思路,但有意思的是,去重的代码中,这么写:
-```
+```cpp
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
```
和这么写:
-```
+```cpp
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
continue;
}
@@ -291,7 +292,36 @@ var permuteUnique = function (nums) {
```
+### Swift
+```swift
+func permuteUnique(_ nums: [Int]) -> [[Int]] {
+ let nums = nums.sorted() // 先排序,以方便相邻元素去重
+ var result = [[Int]]()
+ var path = [Int]()
+ var used = [Bool](repeating: false, count: nums.count)
+ func backtracking() {
+ if path.count == nums.count {
+ result.append(path)
+ return
+ }
+
+ for i in 0 ..< nums.count {
+ // !used[i - 1]表示同一树层nums[i - 1]使用过,直接跳过,这一步很关键!
+ if i > 0, nums[i] == nums[i - 1], !used[i - 1] { continue }
+ if used[i] { continue }
+ used[i] = true
+ path.append(nums[i])
+ backtracking()
+ // 回溯
+ path.removeLast()
+ used[i] = false
+ }
+ }
+ backtracking()
+ return result
+}
+```
-----------------------
diff --git a/problems/0053.最大子序和.md b/problems/0053.最大子序和.md
index 6f75935c..8c62440e 100644
--- a/problems/0053.最大子序和.md
+++ b/problems/0053.最大子序和.md
@@ -21,8 +21,9 @@
暴力解法的思路,第一层for 就是设置起始位置,第二层for循环遍历数组寻找最大值
-时间复杂度:O(n^2)
-空间复杂度:O(1)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(1)$
+
```CPP
class Solution {
public:
@@ -96,8 +97,9 @@ public:
}
};
```
-时间复杂度:O(n)
-空间复杂度:O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
当然题目没有说如果数组为空,应该返回什么,所以数组为空的话返回啥都可以了。
@@ -126,8 +128,8 @@ public:
};
```
-时间复杂度:O(n)
-空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 总结
diff --git a/problems/0053.最大子序和(动态规划).md b/problems/0053.最大子序和(动态规划).md
index 9e9f5511..e88f9961 100644
--- a/problems/0053.最大子序和(动态规划).md
+++ b/problems/0053.最大子序和(动态规划).md
@@ -79,8 +79,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 总结
diff --git a/problems/0056.合并区间.md b/problems/0056.合并区间.md
index 2732f5ee..b7b05699 100644
--- a/problems/0056.合并区间.md
+++ b/problems/0056.合并区间.md
@@ -112,8 +112,8 @@ public:
};
```
-* 时间复杂度:O(nlogn) ,有一个快排
-* 空间复杂度:O(1),我没有算result数组(返回值所需容器占的空间)
+* 时间复杂度:$O(n\log n)$ ,有一个快排
+* 空间复杂度:$O(1)$,我没有算result数组(返回值所需容器占的空间)
## 总结
diff --git a/problems/0059.螺旋矩阵II.md b/problems/0059.螺旋矩阵II.md
index f90fce1a..306b917b 100644
--- a/problems/0059.螺旋矩阵II.md
+++ b/problems/0059.螺旋矩阵II.md
@@ -10,7 +10,7 @@
[力扣题目链接](https://leetcode-cn.com/problems/spiral-matrix-ii/)
-给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
+给定一个正整数 n,生成一个包含 1 到 $n^2$ 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
diff --git a/problems/0062.不同路径.md b/problems/0062.不同路径.md
index 5d789332..25dc7ad8 100644
--- a/problems/0062.不同路径.md
+++ b/problems/0062.不同路径.md
@@ -80,7 +80,7 @@ public:
那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已)
-所以上面深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的。
+所以上面深搜代码的时间复杂度为$O(2^{m + n - 1} - 1)$,可以看出,这是指数级别的时间复杂度,是非常大的。
### 动态规划
@@ -142,8 +142,9 @@ public:
}
};
```
-* 时间复杂度:O(m * n)
-* 空间复杂度:O(m * n)
+
+* 时间复杂度:$O(m × n)$
+* 空间复杂度:$O(m × n)$
其实用一个一维数组(也可以理解是滚动数组)就可以了,但是不利于理解,可以优化点空间,建议先理解了二维,在理解一维,C++代码如下:
@@ -162,8 +163,9 @@ public:
}
};
```
-* 时间复杂度:O(m * n)
-* 空间复杂度:O(n)
+
+* 时间复杂度:$O(m × n)$
+* 空间复杂度:$O(n)$
### 数论方法
@@ -222,8 +224,8 @@ public:
};
```
-时间复杂度:O(m)
-空间复杂度:O(1)
+* 时间复杂度:$O(m)$
+* 空间复杂度:$O(1)$
**计算组合问题的代码还是有难度的,特别是处理溢出的情况!**
@@ -243,7 +245,7 @@ public:
Java:
```java
/**
- * 1. 确定dp数组下表含义 dp[i][j] 到每一个坐标可能的路径种类
+ * 1. 确定dp数组下标含义 dp[i][j] 到每一个坐标可能的路径种类
* 2. 递推公式 dp[i][j] = dp[i-1][j] dp[i][j-1]
* 3. 初始化 dp[i][0]=1 dp[0][i]=1 初始化横竖就可
* 4. 遍历顺序 一行一行遍历
diff --git a/problems/0063.不同路径II.md b/problems/0063.不同路径II.md
index 0f0d10b3..73186314 100644
--- a/problems/0063.不同路径II.md
+++ b/problems/0063.不同路径II.md
@@ -152,8 +152,9 @@ public:
}
};
```
-* 时间复杂度O(n * m) n m 分别为obstacleGrid 长度和宽度
-* 空间复杂度O(n * m)
+
+* 时间复杂度:$O(n × m)$,n、m 分别为obstacleGrid 长度和宽度
+* 空间复杂度:$O(n × m)$
## 总结
diff --git a/problems/0070.爬楼梯.md b/problems/0070.爬楼梯.md
index 4b7f60f4..cf8f2fe7 100644
--- a/problems/0070.爬楼梯.md
+++ b/problems/0070.爬楼梯.md
@@ -123,8 +123,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
当然依然也可以,优化一下空间复杂度,代码如下:
@@ -146,8 +147,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
后面将讲解的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,**但我个人认为面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化**。
diff --git a/problems/0077.组合.md b/problems/0077.组合.md
index 16313cb8..ef7031e3 100644
--- a/problems/0077.组合.md
+++ b/problems/0077.组合.md
@@ -109,7 +109,7 @@ for (int i = 1; i <= n; i++) {
代码如下:
-```
+```cpp
vector> result; // 存放符合条件结果的集合
vector path; // 用来存放符合条件结果
```
@@ -132,7 +132,7 @@ vector path; // 用来存放符合条件结果
那么整体代码如下:
-```
+```cpp
vector> result; // 存放符合条件结果的集合
vector path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex)
@@ -152,7 +152,7 @@ path这个数组的大小如果达到k,说明我们找到了一个子集大小
所以终止条件代码如下:
-```
+```cpp
if (path.size() == k) {
result.push_back(path);
return;
@@ -248,7 +248,7 @@ void backtracking(参数) {
在遍历的过程中有如下代码:
-```
+```cpp
for (int i = startIndex; i <= n; i++) {
path.push_back(i);
backtracking(n, k, i + 1);
@@ -405,16 +405,16 @@ class Solution:
if len(path) == k:
res.append(path[:])
return
- for i in range(StartIndex, n-(k-len(path)) + 2):
+ for i in range(StartIndex, n + 1):
path.append(i)
backtrack(n, k, i+1)
path.pop()
backtrack(n, k, 1)
return res
-```
+```
剪枝:
-```python3
+```python
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res=[] #存放符合条件结果的集合
@@ -423,7 +423,7 @@ class Solution:
if len(path) == k:
res.append(path[:])
return
- for i in range(startIndex,n-(k-len(path))+2): #优化的地方
+ for i in range(startIndex,n - (k - len(path)) + 2): #优化的地方
path.append(i) #处理节点
backtrack(n,k,i+1) #递归
path.pop() #回溯,撤销处理的节点
@@ -454,7 +454,7 @@ const combineHelper = (n, k, startIndex) => {
path.pop()
}
}
-```
+```
@@ -621,5 +621,35 @@ int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
}
```
+## Swift
+
+```swift
+func combine(_ n: Int, _ k: Int) -> [[Int]] {
+ var path = [Int]()
+ var result = [[Int]]()
+ func backtracking(_ n: Int, _ k: Int, _ startIndex: Int) {
+ // 结束条件,并收集结果
+ if path.count == k {
+ result.append(path)
+ return
+ }
+
+ // 单层逻辑
+ // let end = n
+ // 剪枝优化
+ let end = n - (k - path.count) + 1
+ guard startIndex <= end else { return }
+ for i in startIndex ... end {
+ path.append(i) // 处理结点
+ backtracking(n, k, i + 1) // 递归
+ path.removeLast() // 回溯
+ }
+ }
+
+ backtracking(n, k, 1)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0077.组合优化.md b/problems/0077.组合优化.md
index 1d725bc5..d7f7bc38 100644
--- a/problems/0077.组合优化.md
+++ b/problems/0077.组合优化.md
@@ -294,5 +294,35 @@ int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
}
```
+Swift:
+
+```swift
+func combine(_ n: Int, _ k: Int) -> [[Int]] {
+ var path = [Int]()
+ var result = [[Int]]()
+ func backtracking(_ n: Int, _ k: Int, _ startIndex: Int) {
+ // 结束条件,并收集结果
+ if path.count == k {
+ result.append(path)
+ return
+ }
+
+ // 单层逻辑
+ // let end = n
+ // 剪枝优化
+ let end = n - (k - path.count) + 1
+ guard startIndex <= end else { return }
+ for i in startIndex ... end {
+ path.append(i) // 处理结点
+ backtracking(n, k, i + 1) // 递归
+ path.removeLast() // 回溯
+ }
+ }
+
+ backtracking(n, k, 1)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0078.子集.md b/problems/0078.子集.md
index c5dec795..133c55ac 100644
--- a/problems/0078.子集.md
+++ b/problems/0078.子集.md
@@ -57,13 +57,13 @@
代码如下:
-```
+```cpp
vector> result;
vector path;
void backtracking(vector& nums, int startIndex) {
```
-* 递归终止条件
+递归终止条件
从图中可以看出:
@@ -75,7 +75,7 @@ void backtracking(vector& nums, int startIndex) {
就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下:
-```
+```cpp
if (startIndex >= nums.size()) {
return;
}
@@ -253,7 +253,7 @@ func Dfs(temp, nums []int, start int){
}
```
-## Javascript:
+## Javascript
```Javascript
var subsets = function(nums) {
@@ -329,6 +329,29 @@ int** subsets(int* nums, int numsSize, int* returnSize, int** returnColumnSizes)
}
```
+## Swift
+
+```swift
+func subsets(_ nums: [Int]) -> [[Int]] {
+ var result = [[Int]]()
+ var path = [Int]()
+ func backtracking(startIndex: Int) {
+ // 直接收集结果
+ result.append(path)
+
+ let end = nums.count
+ guard startIndex < end else { return } // 终止条件
+ for i in startIndex ..< end {
+ path.append(nums[i]) // 处理:收集元素
+ backtracking(startIndex: i + 1) // 元素不重复访问
+ path.removeLast() // 回溯
+ }
+ }
+ backtracking(startIndex: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0084.柱状图中最大的矩形.md b/problems/0084.柱状图中最大的矩形.md
index 0c49c581..7dc20af2 100644
--- a/problems/0084.柱状图中最大的矩形.md
+++ b/problems/0084.柱状图中最大的矩形.md
@@ -51,7 +51,7 @@ public:
};
```
-如上代码并不能通过leetcode,超时了,因为时间复杂度是O(n^2)。
+如上代码并不能通过leetcode,超时了,因为时间复杂度是$O(n^2)$。
## 动态规划
@@ -140,9 +140,9 @@ public:
heights.push_back(0); // 数组尾部加入元素0
st.push(0);
int result = 0;
- // 第一个元素已经入栈,从下表1开始
+ // 第一个元素已经入栈,从下标1开始
for (int i = 1; i < heights.size(); i++) {
- // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下表
+ // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下标
if (heights[i] > heights[st.top()]) {
st.push(i);
} else if (heights[i] == heights[st.top()]) {
@@ -251,9 +251,9 @@ class Solution {
st.push(0);
int result = 0;
- // 第一个元素已经入栈,从下表1开始
+ // 第一个元素已经入栈,从下标1开始
for (int i = 1; i < heights.length; i++) {
- // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下表
+ // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下标
if (heights[i] > heights[st.peek()]) {
st.push(i);
} else if (heights[i] == heights[st.peek()]) {
diff --git a/problems/0090.子集II.md b/problems/0090.子集II.md
index 95b6080b..17612801 100644
--- a/problems/0090.子集II.md
+++ b/problems/0090.子集II.md
@@ -156,14 +156,12 @@ public:
当然本题去重的逻辑,也可以这么写
-```
+```cpp
if (i > startIndex && nums[i] == nums[i - 1] ) {
- continue;
+ continue;
}
```
-
-
## 其他语言版本
@@ -359,6 +357,31 @@ int** subsetsWithDup(int* nums, int numsSize, int* returnSize, int** returnColum
}
```
+## Swift
+
+```swift
+func subsetsWithDup(_ nums: [Int]) -> [[Int]] {
+ let nums = nums.sorted()
+ var result = [[Int]]()
+ var path = [Int]()
+ func backtracking(startIndex: Int) {
+ // 直接收集结果
+ result.append(path)
+
+ let end = nums.count
+ guard startIndex < end else { return } // 终止条件
+ for i in startIndex ..< end {
+ if i > startIndex, nums[i] == nums[i - 1] { continue } // 跳过重复元素
+ path.append(nums[i]) // 处理:收集元素
+ backtracking(startIndex: i + 1) // 元素不重复访问
+ path.removeLast() // 回溯
+ }
+ }
+ backtracking(startIndex: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0093.复原IP地址.md b/problems/0093.复原IP地址.md
index 2277aa60..5118810d 100644
--- a/problems/0093.复原IP地址.md
+++ b/problems/0093.复原IP地址.md
@@ -66,10 +66,10 @@ startIndex一定是需要的,因为不能重复分割,记录下一层递归
所以代码如下:
-```
- vector result;// 记录结果
- // startIndex: 搜索的起始位置,pointNum:添加逗点的数量
- void backtracking(string& s, int startIndex, int pointNum) {
+```cpp
+vector result;// 记录结果
+// startIndex: 搜索的起始位置,pointNum:添加逗点的数量
+void backtracking(string& s, int startIndex, int pointNum) {
```
* 递归终止条件
@@ -82,7 +82,7 @@ pointNum表示逗点数量,pointNum为3说明字符串分成了4段了。
代码如下:
-```
+```cpp
if (pointNum == 3) { // 逗点数量为3时,分隔结束
// 判断第四段子字符串是否合法,如果合法就放进result中
if (isValid(s, startIndex, s.size() - 1)) {
@@ -96,7 +96,7 @@ if (pointNum == 3) { // 逗点数量为3时,分隔结束
在[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)中已经讲过在循环遍历中如何截取子串。
-在`for (int i = startIndex; i < s.size(); i++)`循环中 [startIndex, i]这个区间就是截取的子串,需要判断这个子串是否合法。
+在`for (int i = startIndex; i < s.size(); i++)`循环中 [startIndex, i] 这个区间就是截取的子串,需要判断这个子串是否合法。
如果合法就在字符串后面加上符号`.`表示已经分割。
@@ -531,6 +531,53 @@ char ** restoreIpAddresses(char * s, int* returnSize){
}
```
+## Swift
+
+```swift
+// 判断区间段是否合法
+func isValid(s: [Character], start: Int, end: Int) -> Bool {
+ guard start <= end, start >= 0, end < s.count else { return false } // 索引不合法
+ if start != end, s[start] == "0" { return false } // 以0开头的多位数字不合法
+ var num = 0
+ for i in start ... end {
+ let c = s[i]
+ guard c >= "0", c <= "9" else { return false } // 非数字不合法
+ let value = c.asciiValue! - ("0" as Character).asciiValue!
+ num = num * 10 + Int(value)
+ guard num <= 255 else { return false } // 大于255不合法
+ }
+ return true
+}
+func restoreIpAddresses(_ s: String) -> [String] {
+ var s = Array(s) // 转换成字符数组以便于比较
+ var result = [String]() // 结果
+ func backtracking(startIndex: Int, pointCount: Int) {
+ guard startIndex < s.count else { return } // 索引不合法
+ // 结束条件
+ if pointCount == 3 {
+ // 最后一段也合法,则收集结果
+ if isValid(s: s, start: startIndex, end: s.count - 1) {
+ result.append(String(s))
+ }
+ return
+ }
+
+ for i in startIndex ..< s.count {
+ // 判断[starIndex, i]子串是否合法,合法则插入“.”,否则结束本层循环
+ if isValid(s: s, start: startIndex, end: i) {
+ s.insert(".", at: i + 1) // 子串后面插入“.”
+ backtracking(startIndex: i + 2, pointCount: pointCount + 1) // 注意这里时跳2位,且通过pointCount + 1局部变量隐藏了pointCount的回溯
+ s.remove(at: i + 1) // 回溯
+ } else {
+ break
+ }
+ }
+ }
+ backtracking(startIndex: 0, pointCount: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0096.不同的二叉搜索树.md b/problems/0096.不同的二叉搜索树.md
index 0567beda..1f6d0962 100644
--- a/problems/0096.不同的二叉搜索树.md
+++ b/problems/0096.不同的二叉搜索树.md
@@ -136,8 +136,9 @@ public:
}
};
```
-* 时间复杂度O(n^2)
-* 空间复杂度O(n)
+
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n)$
大家应该发现了,我们分析了这么多,最后代码却如此简单!
diff --git a/problems/0106.从中序与后序遍历序列构造二叉树.md b/problems/0106.从中序与后序遍历序列构造二叉树.md
index 6b55b10e..41e15a79 100644
--- a/problems/0106.从中序与后序遍历序列构造二叉树.md
+++ b/problems/0106.从中序与后序遍历序列构造二叉树.md
@@ -272,7 +272,7 @@ public:
**此时应该发现了,如上的代码性能并不好,应为每层递归定定义了新的vector(就是数组),既耗时又耗空间,但上面的代码是最好理解的,为了方便读者理解,所以用如上的代码来讲解。**
-下面给出用下表索引写出的代码版本:(思路是一样的,只不过不用重复定义vector了,每次用下表索引来分割)
+下面给出用下标索引写出的代码版本:(思路是一样的,只不过不用重复定义vector了,每次用下标索引来分割)
### C++优化版本
```CPP
diff --git a/problems/0108.将有序数组转换为二叉搜索树.md b/problems/0108.将有序数组转换为二叉搜索树.md
index 04f62e60..3d0f7b52 100644
--- a/problems/0108.将有序数组转换为二叉搜索树.md
+++ b/problems/0108.将有序数组转换为二叉搜索树.md
@@ -70,7 +70,7 @@
那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。
-再来看参数,首先是传入数组,然后就是左下表left和右下表right,我们在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下表来操作原数组。
+再来看参数,首先是传入数组,然后就是左下标left和右下标right,我们在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
所以代码如下:
@@ -144,7 +144,7 @@ public:
## 迭代法
-迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下表,一个队列放右区间下表。
+迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。
模拟的就是不断分割的过程,C++代码如下:(我已经详细注释)
@@ -156,11 +156,11 @@ public:
TreeNode* root = new TreeNode(0); // 初始根节点
queue nodeQue; // 放遍历的节点
- queue leftQue; // 保存左区间下表
- queue rightQue; // 保存右区间下表
+ queue leftQue; // 保存左区间下标
+ queue rightQue; // 保存右区间下标
nodeQue.push(root); // 根节点入队列
- leftQue.push(0); // 0为左区间下表初始位置
- rightQue.push(nums.size() - 1); // nums.size() - 1为右区间下表初始位置
+ leftQue.push(0); // 0为左区间下标初始位置
+ rightQue.push(nums.size() - 1); // nums.size() - 1为右区间下标初始位置
while (!nodeQue.empty()) {
TreeNode* curNode = nodeQue.front();
@@ -267,9 +267,9 @@ class Solution {
// 根节点入队列
nodeQueue.offer(root);
- // 0为左区间下表初始位置
+ // 0为左区间下标初始位置
leftQueue.offer(0);
- // nums.size() - 1为右区间下表初始位置
+ // nums.size() - 1为右区间下标初始位置
rightQueue.offer(nums.length - 1);
while (!nodeQueue.isEmpty()) {
diff --git a/problems/0121.买卖股票的最佳时机.md b/problems/0121.买卖股票的最佳时机.md
index 486e08bd..b299e5ae 100644
--- a/problems/0121.买卖股票的最佳时机.md
+++ b/problems/0121.买卖股票的最佳时机.md
@@ -46,8 +46,8 @@ public:
};
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(1)$
当然该方法超时了。
@@ -71,8 +71,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
### 动态规划
@@ -155,8 +156,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。
@@ -185,8 +187,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
这里能写出版本一就可以了,版本二虽然原理都一样,但是想直接写出版本二还是有点麻烦,容易自己给自己找bug。
diff --git a/problems/0122.买卖股票的最佳时机II.md b/problems/0122.买卖股票的最佳时机II.md
index f6d5906a..5f917983 100644
--- a/problems/0122.买卖股票的最佳时机II.md
+++ b/problems/0122.买卖股票的最佳时机II.md
@@ -90,8 +90,9 @@ public:
}
};
```
-* 时间复杂度O(n)
-* 空间复杂度O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
### 动态规划
@@ -116,8 +117,9 @@ public:
}
};
```
-* 时间复杂度O(n)
-* 空间复杂度O(n)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 总结
diff --git a/problems/0122.买卖股票的最佳时机II(动态规划).md b/problems/0122.买卖股票的最佳时机II(动态规划).md
index 8f03e88e..17a96b36 100644
--- a/problems/0122.买卖股票的最佳时机II(动态规划).md
+++ b/problems/0122.买卖股票的最佳时机II(动态规划).md
@@ -88,8 +88,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
大家可以本题和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)的代码几乎一样,唯一的区别在:
@@ -121,8 +121,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
@@ -135,7 +135,7 @@ Java:
class Solution
// 实现1:二维数组存储
// 可以将每天持有与否的情况分别用 dp[i][0] 和 dp[i][1] 来进行存储
- // 时间复杂度:O(n),空间复杂度O(n)
+ // 时间复杂度:O(n),空间复杂度:O(n)
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2]; // 创建二维数组存储状态
@@ -204,7 +204,7 @@ class Solution:
Go:
```go
// 买卖股票的最佳时机Ⅱ 动态规划
-// 时间复杂度O(n) 空间复杂度O(n)
+// 时间复杂度:O(n) 空间复杂度:O(n)
func maxProfit(prices []int) int {
dp := make([][]int, len(prices))
status := make([]int, len(prices) * 2)
diff --git a/problems/0123.买卖股票的最佳时机III.md b/problems/0123.买卖股票的最佳时机III.md
index 8fa3a8e0..4ab5f53c 100644
--- a/problems/0123.买卖股票的最佳时机III.md
+++ b/problems/0123.买卖股票的最佳时机III.md
@@ -146,8 +146,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n * 5)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n × 5)$
当然,大家可以看到力扣官方题解里的一种优化空间写法,我这里给出对应的C++版本:
@@ -171,8 +171,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
大家会发现dp[2]利用的是当天的dp[1]。 但结果也是对的。
diff --git a/problems/0131.分割回文串.md b/problems/0131.分割回文串.md
index 075734ea..47734f6c 100644
--- a/problems/0131.分割回文串.md
+++ b/problems/0131.分割回文串.md
@@ -450,7 +450,8 @@ var partition = function(s) {
};
```
-##C
+## C
+
```c
char** path;
int pathTop;
@@ -546,5 +547,48 @@ char*** partition(char* s, int* returnSize, int** returnColumnSizes){
}
```
+## Swift
+
+```swift
+func partition(_ s: String) -> [[String]] {
+ // 把字符串转为字符数组以便于通过索引访问和取子串
+ let s = Array(s)
+ // 使用双指针法判断子串是否回文
+ func isPalindrome(start: Int, end: Int) -> Bool {
+ var start = start, end = end
+ while start < end {
+ if s[start] != s[end] { return false }
+ start += 1
+ end -= 1
+ }
+ return true
+ }
+
+ var result = [[String]]()
+ var path = [String]() // 切割方案
+ func backtracking(startIndex: Int) {
+ // 终止条件,收集结果
+ guard startIndex < s.count else {
+ result.append(path)
+ return
+ }
+
+ for i in startIndex ..< s.count {
+ // 回文则收集,否则跳过
+ if isPalindrome(start: startIndex, end: i) {
+ let substring = String(s[startIndex ... i])
+ path.append(substring)
+ } else {
+ continue
+ }
+ backtracking(startIndex: i + 1) // 寻找下一个起始位置的子串
+ if !path.isEmpty { path.removeLast() } // 回溯
+ }
+ }
+ backtracking(startIndex: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0134.加油站.md b/problems/0134.加油站.md
index c274bd78..f6e8596c 100644
--- a/problems/0134.加油站.md
+++ b/problems/0134.加油站.md
@@ -48,7 +48,7 @@
## 暴力方法
-暴力的方法很明显就是O(n^2)的,遍历每一个加油站为起点的情况,模拟一圈。
+暴力的方法很明显就是$O(n^2)$的,遍历每一个加油站为起点的情况,模拟一圈。
如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。
@@ -76,8 +76,9 @@ public:
}
};
```
-* 时间复杂度O(n^2)
-* 空间复杂度O(n)
+
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n)$
C++暴力解法在leetcode上提交也可以过。
@@ -119,8 +120,8 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
**其实我不认为这种方式是贪心算法,因为没有找出局部最优,而是直接从全局最优的角度上思考问题**。
@@ -173,8 +174,8 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
**说这种解法为贪心算法,才是是有理有据的,因为全局最优解是根据局部最优推导出来的**。
diff --git a/problems/0139.单词拆分.md b/problems/0139.单词拆分.md
index 0ae5c5f2..58d54afc 100644
--- a/problems/0139.单词拆分.md
+++ b/problems/0139.单词拆分.md
@@ -66,8 +66,8 @@ public:
};
```
-* 时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割
-* 空间复杂度:O(n),算法递归系统调用栈的空间
+* 时间复杂度:$O(2^n)$,因为每一个单词都有两个状态,切割和不切割
+* 空间复杂度:$O(n)$,算法递归系统调用栈的空间
那么以上代码很明显要超时了,超时的数据如下:
@@ -115,7 +115,7 @@ public:
};
```
-这个时间复杂度其实也是:O(2^n)。只不过对于上面那个超时测试用例优化效果特别明显。
+这个时间复杂度其实也是:$O(2^n)$。只不过对于上面那个超时测试用例优化效果特别明显。
**这个代码就可以AC了,当然回溯算法不是本题的主菜,背包才是!**
@@ -207,8 +207,9 @@ public:
}
};
```
-* 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度)
-* 空间复杂度:O(n)
+
+* 时间复杂度:$O(n^3)$,因为substr返回子串的副本是$O(n)$的复杂度(这里的n是substring的长度)
+* 空间复杂度:$O(n)$
## 总结
diff --git a/problems/0142.环形链表II.md b/problems/0142.环形链表II.md
index 60bb10b7..fb2172c6 100644
--- a/problems/0142.环形链表II.md
+++ b/problems/0142.环形链表II.md
@@ -33,11 +33,11 @@
### 判断链表是否有环
-可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
+可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
-首先第一点: **fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。**
+首先第一点:**fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。**
那么来看一下,**为什么fast指针和slow指针一定会相遇呢?**
@@ -328,5 +328,26 @@ extension ListNode: Equatable {
}
```
+C:
+
+```c
+ListNode *detectCycle(ListNode *head) {
+ ListNode *fast = head, *slow = head;
+ while (fast && fast->next) {
+ // 这里判断两个指针是否相等,所以移位操作放在前面
+ slow = slow->next;
+ fast = fast->next->next;
+ if (slow == fast) { // 相交,开始找环形入口:分别从头部和从交点出发,找到相遇的点就是环形入口
+ ListNode *f = fast, *h = head;
+ while (f != h) f = f->next, h = h->next;
+ return h;
+ }
+ }
+ return NULL;
+}
+```
+
+
+
-----------------------
diff --git a/problems/0151.翻转字符串里的单词.md b/problems/0151.翻转字符串里的单词.md
index a6669147..d4477885 100644
--- a/problems/0151.翻转字符串里的单词.md
+++ b/problems/0151.翻转字符串里的单词.md
@@ -36,7 +36,7 @@
一些同学会使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。
-所以这里我还是提高一下本题的难度:**不要使用辅助空间,空间复杂度要求为O(1)。**
+所以这里我还是提高一下本题的难度:**不要使用辅助空间,空间复杂度要求为$O(1)$。**
不能使用辅助空间之后,那么只能在原字符串上下功夫了。
@@ -79,13 +79,13 @@ void removeExtraSpaces(string& s) {
逻辑很简单,从前向后遍历,遇到空格了就erase。
-如果不仔细琢磨一下erase的时间复杂读,还以为以上的代码是O(n)的时间复杂度呢。
+如果不仔细琢磨一下erase的时间复杂读,还以为以上的代码是$O(n)$的时间复杂度呢。
-想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作,erase实现原理题目:[数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html),最优的算法来移除元素也要O(n)。
+想一下真正的时间复杂度是多少,一个erase本来就是$O(n)$的操作,erase实现原理题目:[数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html),最优的算法来移除元素也要$O(n)$。
-erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。
+erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为$O(n^2)$。
-那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。
+那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到$O(n)$的时间复杂度。
如果对这个操作比较生疏了,可以再看一下这篇文章:[数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html)是如何移除元素的。
diff --git a/problems/0189.旋转数组.md b/problems/0189.旋转数组.md
index 2a60d245..b2619c4d 100644
--- a/problems/0189.旋转数组.md
+++ b/problems/0189.旋转数组.md
@@ -12,7 +12,7 @@
进阶:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
-你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
+你可以使用空间复杂度为 $O(1)$ 的 原地 算法解决这个问题吗?
示例 1:
@@ -41,7 +41,7 @@
本题其实和[字符串:剑指Offer58-II.左旋转字符串](https://programmercarl.com/剑指Offer58-II.左旋转字符串.html)就非常像了,剑指offer上左旋转,本题是右旋转。
-注意题目要求是**要求使用空间复杂度为 O(1) 的 原地 算法**
+注意题目要求是**要求使用空间复杂度为 $O(1)$ 的 原地 算法**
那么我来提供一种旋转的方式哈。
diff --git a/problems/0209.长度最小的子数组.md b/problems/0209.长度最小的子数组.md
index 8c539ff7..a289c22e 100644
--- a/problems/0209.长度最小的子数组.md
+++ b/problems/0209.长度最小的子数组.md
@@ -20,7 +20,7 @@
## 暴力解法
-这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是O(n^2) 。
+这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是$O(n^2)$。
代码如下:
@@ -80,7 +80,7 @@ public:

-可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。**
+可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将$O(n^2)$的暴力解法降为$O(n)$。**
C++代码如下:
@@ -110,9 +110,9 @@ public:
时间复杂度:$O(n)$
空间复杂度:$O(1)$
-**一些录友会疑惑为什么时间复杂度是O(n)**。
+**一些录友会疑惑为什么时间复杂度是$O(n)$**。
-不要以为for里放一个while就以为是$O(n^2)$啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是2 * n 也就是$O(n)$。
+不要以为for里放一个while就以为是$O(n^2)$啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是 2 × n 也就是$O(n)$。
## 相关题目推荐
diff --git a/problems/0216.组合总和III.md b/problems/0216.组合总和III.md
index fa2ce37f..adb11676 100644
--- a/problems/0216.组合总和III.md
+++ b/problems/0216.组合总和III.md
@@ -57,7 +57,7 @@
至于为什么取名为path?从上面树形结构中,可以看出,结果其实就是一条根节点到叶子节点的路径。
-```
+```cpp
vector> result; // 存放结果集
vector path; // 符合条件的结果
```
@@ -71,7 +71,7 @@ vector path; // 符合条件的结果
所以代码如下:
-```
+```cpp
vector> result;
vector path;
void backtracking(int targetSum, int k, int sum, int startIndex)
@@ -168,7 +168,7 @@ public:
那么剪枝的地方一定是在递归终止的地方剪,剪枝代码如下:
-```
+```cpp
if (sum > targetSum) { // 剪枝操作
return;
}
@@ -301,25 +301,32 @@ class Solution {
```py
class Solution:
- def combinationSum3(self, k: int, n: int) -> List[List[int]]:
- res = [] #存放结果集
- path = [] #符合条件的结果
- def findallPath(n,k,sum,startIndex):
- if sum > n: return #剪枝操作
- if sum == n and len(path) == k: #如果path.size() == k 但sum != n 直接返回
- return res.append(path[:])
- for i in range(startIndex,9-(k-len(path))+2): #剪枝操作
- path.append(i)
- sum += i
- findallPath(n,k,sum,i+1) #注意i+1调整startIndex
- sum -= i #回溯
- path.pop() #回溯
-
- findallPath(n,k,0,1)
- return res
+ def __init__(self):
+ self.res = []
+ self.sum_now = 0
+ self.path = []
+
+ def combinationSum3(self, k: int, n: int) -> [[int]]:
+ self.backtracking(k, n, 1)
+ return self.res
+
+ def backtracking(self, k: int, n: int, start_num: int):
+ if self.sum_now > n: # 剪枝
+ return
+ if len(self.path) == k: # len(path)==k时不管sum是否等于n都会返回
+ if self.sum_now == n:
+ self.res.append(self.path[:])
+ return
+ for i in range(start_num, 10 - (k - len(self.path)) + 1):
+ self.path.append(i)
+ self.sum_now += i
+ self.backtracking(k, n, i + 1)
+ self.path.pop()
+ self.sum_now -= i
+ return
```
-## Go:
+## Go
回溯+减枝
@@ -351,7 +358,7 @@ func backTree(n,k,startIndex int,track *[]int,result *[][]int){
}
```
-## javaScript:
+## javaScript
```js
// 等差数列
@@ -390,7 +397,8 @@ var combinationSum3 = function(k, n) {
};
```
-C:
+## C
+
```c
int* path;
int pathTop;
@@ -448,5 +456,37 @@ int** combinationSum3(int k, int n, int* returnSize, int** returnColumnSizes){
}
```
+## Swift
+
+```swift
+func combinationSum3(_ count: Int, _ targetSum: Int) -> [[Int]] {
+ var result = [[Int]]()
+ var path = [Int]()
+ func backtracking(sum: Int, startIndex: Int) {
+ // 剪枝
+ if sum > targetSum { return }
+ // 终止条件
+ if path.count == count {
+ if sum == targetSum {
+ result.append(path)
+ }
+ return
+ }
+
+ // 单层逻辑
+ let endIndex = 9
+ guard startIndex <= endIndex else { return }
+ for i in startIndex ... endIndex {
+ path.append(i) // 处理
+ backtracking(sum: sum + i, startIndex: i + 1)
+ path.removeLast() // 回溯
+ }
+ }
+
+ backtracking(sum: 0, startIndex: 1)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0222.完全二叉树的节点个数.md b/problems/0222.完全二叉树的节点个数.md
index 2e964dfc..3b5ca4d8 100644
--- a/problems/0222.完全二叉树的节点个数.md
+++ b/problems/0222.完全二叉树的节点个数.md
@@ -105,8 +105,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(logn),算上了递归系统栈占用的空间
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(\log n)$,算上了递归系统栈占用的空间
**网上基本都是这个精简的代码版本,其实不建议大家照着这个来写,代码确实精简,但隐藏了一些内容,连遍历的顺序都看不出来,所以初学者建议学习版本一的代码,稳稳的打基础**。
@@ -138,8 +138,8 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 完全二叉树
@@ -185,8 +185,8 @@ public:
};
```
-* 时间复杂度:O(logn * logn)
-* 空间复杂度:O(logn)
+* 时间复杂度:$O(\log n × \log n)$
+* 空间复杂度:$O(\log n)$
# 其他语言版本
diff --git a/problems/0236.二叉树的最近公共祖先.md b/problems/0236.二叉树的最近公共祖先.md
index 4182999f..6d26e376 100644
--- a/problems/0236.二叉树的最近公共祖先.md
+++ b/problems/0236.二叉树的最近公共祖先.md
@@ -221,42 +221,30 @@ public:
## Java
+
```Java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
- return lowestCommonAncestor1(root, p, q);
- }
- public TreeNode lowestCommonAncestor1(TreeNode root, TreeNode p, TreeNode q) {
- if (root == null || root == p || root == q) {
+ if (root == null || root == p || root == q) { // 递归结束条件
return root;
}
- TreeNode left = lowestCommonAncestor1(root.left, p, q);
- TreeNode right = lowestCommonAncestor1(root.right, p, q);
- if (left != null && right != null) {// 左右子树分别找到了,说明此时的root就是要求的结果
- return root;
- }
- if (left == null) {
+
+ // 后序遍历
+ TreeNode left = lowestCommonAncestor(root.left, p, q);
+ TreeNode right = lowestCommonAncestor(root.right, p, q);
+
+ if(left == null && right == null) { // 若未找到节点 p 或 q
+ return null;
+ }else if(left == null && right != null) { // 若找到一个节点
return right;
+ }else if(left != null && right == null) { // 若找到一个节点
+ return left;
+ }else { // 若找到两个节点
+ return root;
}
- return left;
}
}
-```
-
-```java
-// 代码精简版
-class Solution {
- public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
- if (root == null || root.val == p.val ||root.val == q.val) return root;
- TreeNode left = lowestCommonAncestor(root.left,p,q);
- TreeNode right = lowestCommonAncestor(root.right,p,q);
- if (left != null && right != null) return root;
- else if (left == null && right != null) return right;
- else if (left != null && right == null) return left;
- else return null;
- }
-}
```
## Python
diff --git a/problems/0239.滑动窗口最大值.md b/problems/0239.滑动窗口最大值.md
index 8a8d3a52..ba362222 100644
--- a/problems/0239.滑动窗口最大值.md
+++ b/problems/0239.滑动窗口最大值.md
@@ -36,7 +36,7 @@
难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。
-暴力方法,遍历一遍的过程中每次从窗口中在找到最大的数值,这样很明显是O(n * k)的算法。
+暴力方法,遍历一遍的过程中每次从窗口中在找到最大的数值,这样很明显是$O(n × k)$的算法。
有的同学可能会想用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。**
@@ -183,13 +183,13 @@ public:
};
```
-在来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。
+在来看一下时间复杂度,使用单调队列的时间复杂度是 $O(n)$。
-有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。
+有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的$O(n)$。
-其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。
+其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 $O(n)$。
-空间复杂度因为我们定义一个辅助队列,所以是O(k)。
+空间复杂度因为我们定义一个辅助队列,所以是$O(k)$。
# 扩展
@@ -418,6 +418,112 @@ var maxSlidingWindow = function (nums, k) {
};
```
+Swift:
+```Swift
+/// 双向链表
+class DoublyListNode {
+ var head: DoublyListNode?
+ var tail: DoublyListNode?
+ var next: DoublyListNode?
+ var pre: DoublyListNode?
+ var value: Int = 0
+ init(_ value: Int = 0) {
+ self.value = value
+ }
+ func isEmpty() -> Bool {
+ return self.head == nil
+ }
+ func first() -> Int? {
+ return self.head?.value
+ }
+ func last() -> Int? {
+ return self.tail?.value
+ }
+ func removeFirst() {
+ if isEmpty() {
+ return
+ }
+ let next = self.head!.next
+ self.head?.next = nil// 移除首节点
+ next?.pre = nil
+ self.head = next
+ }
+ func removeLast() {
+ if let tail = self.tail {
+ if let pre = tail.pre {
+ self.tail?.pre = nil
+ pre.next = nil
+ self.tail = pre
+ } else {
+ self.head = nil
+ self.tail = nil
+ }
+ }
+ }
+ func append(_ value: Int) {
+ let node = DoublyListNode(value)
+ if self.head != nil {
+ node.pre = self.tail
+ self.tail?.next = node
+ self.tail = node
+ } else {
+ self.head = node
+ self.tail = node
+ self.pre = nil
+ self.next = nil
+ }
+ }
+}
+// 单调队列, 从大到小
+class MyQueue {
+// var queue: [Int]!// 用数组会超时
+ var queue: DoublyListNode!
+ init() {
+// queue = [Int]()
+ queue = DoublyListNode()
+ }
+ // 滑动窗口时弹出第一个元素, 如果相等再弹出
+ func pop(x: Int) {
+ if !queue.isEmpty() && front() == x {
+ queue.removeFirst()
+ }
+ }
+ // 滑动窗口时添加下一个元素, 移除队尾比 x 小的元素 始终保证队头 > 队尾
+ func push(x: Int) {
+ while !queue.isEmpty() && queue.last()! < x {
+ queue.removeLast()
+ }
+ queue.append(x)
+ }
+ // 此时队头就是滑动窗口最大值
+ func front() -> Int {
+ return queue.first() ?? -1
+ }
+}
+
+class Solution {
+ func maxSlidingWindow(_ nums: [Int], _ k: Int) -> [Int] {
+ // 存放结果
+ var res = [Int]()
+ let queue = MyQueue()
+ // 先将前K个元素放入队列
+ for i in 0 ..< k {
+ queue.push(x: nums[i])
+ }
+ // 添加当前队列最大值到结果数组
+ res.append(queue.front())
+ for i in k ..< nums.count {
+ // 滑动窗口移除最前面元素
+ queue.pop(x: nums[i - k])
+ // 滑动窗口添加下一个元素
+ queue.push(x: nums[i])
+ // 保存当前队列最大值
+ res.append(queue.front())
+ }
+ return res
+ }
+}
+```
-----------------------
diff --git a/problems/0242.有效的字母异位词.md b/problems/0242.有效的字母异位词.md
index 61182758..69b5df67 100644
--- a/problems/0242.有效的字母异位词.md
+++ b/problems/0242.有效的字母异位词.md
@@ -27,7 +27,7 @@
## 思路
-先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。
+先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 $O(n^2)$。
暴力的方法这里就不做介绍了,直接看一下有没有更优的方式。
@@ -45,7 +45,7 @@
定义一个数组叫做record用来上记录字符串s里字符出现的次数。
-需要把字符映射到数组也就是哈希表的索引下表上,**因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下表0,相应的字符z映射为下表25。**
+需要把字符映射到数组也就是哈希表的索引下标上,**因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。**
再遍历 字符串s的时候,**只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。** 这样就将字符串s中字符出现的次数,统计出来了。
@@ -55,7 +55,7 @@
最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
-时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
+时间复杂度为$O(n)$,空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为$O(1)$。
C++ 代码如下:
diff --git a/problems/0283.移动零.md b/problems/0283.移动零.md
index 8ba3ff3b..de3b1e89 100644
--- a/problems/0283.移动零.md
+++ b/problems/0283.移动零.md
@@ -30,7 +30,7 @@
好了,我们说一说双指针法,大家如果对双指针还不熟悉,可以看我的这篇总结[双指针法:总结篇!](https://programmercarl.com/双指针总结.html)。
-双指针法在数组移除元素中,可以达到O(n)的时间复杂度,在[27.移除元素](https://programmercarl.com/0027.移除元素.html)里已经详细讲解了,那么本题和移除元素其实是一个套路。
+双指针法在数组移除元素中,可以达到$O(n)$的时间复杂度,在[27.移除元素](https://programmercarl.com/0027.移除元素.html)里已经详细讲解了,那么本题和移除元素其实是一个套路。
**相当于对整个数组移除元素0,然后slowIndex之后都是移除元素0的冗余元素,把这些元素都赋值为0就可以了**。
diff --git a/problems/0309.最佳买卖股票时机含冷冻期.md b/problems/0309.最佳买卖股票时机含冷冻期.md
index 48106bd8..1da766da 100644
--- a/problems/0309.最佳买卖股票时机含冷冻期.md
+++ b/problems/0309.最佳买卖股票时机含冷冻期.md
@@ -147,8 +147,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
当然,空间复杂度可以优化,定义一个dp[2][4]大小的数组就可以了,就保存前一天的当前的状态,感兴趣的同学可以自己去写一写,思路是一样的。
diff --git a/problems/0337.打家劫舍III.md b/problems/0337.打家劫舍III.md
index e8e95cc4..4264728f 100644
--- a/problems/0337.打家劫舍III.md
+++ b/problems/0337.打家劫舍III.md
@@ -50,8 +50,8 @@ public:
};
```
-* 时间复杂度:O(n^2) 这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
-* 空间复杂度:O(logn) 算上递推系统栈的空间
+* 时间复杂度:$O(n^2)$,这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
+* 空间复杂度:$O(\log n)$,算上递推系统栈的空间
当然以上代码超时了,这个递归的过程中其实是有重复计算了。
@@ -83,8 +83,9 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(logn) 算上递推系统栈的空间
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(\log n)$,算上递推系统栈的空间
### 动态规划
@@ -197,8 +198,8 @@ public:
}
};
```
-* 时间复杂度:O(n) 每个节点只遍历了一次
-* 空间复杂度:O(logn) 算上递推系统栈的空间
+* 时间复杂度:$O(n)$,每个节点只遍历了一次
+* 空间复杂度:$O(\log n)$,算上递推系统栈的空间
## 总结
diff --git a/problems/0343.整数拆分.md b/problems/0343.整数拆分.md
index 9f070e24..1bab99ca 100644
--- a/problems/0343.整数拆分.md
+++ b/problems/0343.整数拆分.md
@@ -120,8 +120,8 @@ public:
};
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n)$
### 贪心
@@ -148,8 +148,9 @@ public:
}
};
```
-* 时间复杂度O(n)
-* 空间复杂度O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
## 总结
diff --git a/problems/0344.反转字符串.md b/problems/0344.反转字符串.md
index cfd536de..39da7307 100644
--- a/problems/0344.反转字符串.md
+++ b/problems/0344.反转字符串.md
@@ -14,7 +14,7 @@
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
-不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
+不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 $O(1)$ 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
@@ -63,7 +63,7 @@
如果对数组和链表原理不清楚的同学,可以看这两篇,[关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html),[必须掌握的数组理论知识](https://programmercarl.com/数组理论基础.html)。
-对于字符串,我们定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
+对于字符串,我们定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
以字符串`hello`为例,过程如下:
diff --git a/problems/0347.前K个高频元素.md b/problems/0347.前K个高频元素.md
index c0f194fa..6f3a65fe 100644
--- a/problems/0347.前K个高频元素.md
+++ b/problems/0347.前K个高频元素.md
@@ -25,7 +25,7 @@
提示:
* 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
-* 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
+* 你的算法的时间复杂度必须优于 $O(n \log n)$ , n 是数组的大小。
* 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
* 你可以按任意顺序返回答案。
diff --git a/problems/0349.两个数组的交集.md b/problems/0349.两个数组的交集.md
index 35d0cb7f..b5cf31c5 100644
--- a/problems/0349.两个数组的交集.md
+++ b/problems/0349.两个数组的交集.md
@@ -28,7 +28,7 @@
注意题目特意说明:**输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序**
-这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。
+这道题用暴力的解法时间复杂度是$O(n^2)$,那来看看使用哈希法进一步优化。
那么用数组来做哈希表也是不错的选择,例如[242. 有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html)
diff --git a/problems/0376.摆动序列.md b/problems/0376.摆动序列.md
index b7293635..3c32dd52 100644
--- a/problems/0376.摆动序列.md
+++ b/problems/0376.摆动序列.md
@@ -87,8 +87,9 @@ public:
}
};
```
-时间复杂度O(n)
-空间复杂度O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
## 思路2(动态规划)
@@ -137,9 +138,8 @@ public:
};
```
-时间复杂度O(n^2)
-
-空间复杂度O(n)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n)$
**进阶**
@@ -149,9 +149,9 @@ public:
* 每次更新`dp[i][1]`,则在`tree2`的`nums[i]`位置值更新为`dp[i][1]`
* 则dp转移方程中就没有必要j从0遍历到i-1,可以直接在线段树中查询指定区间的值即可。
-时间复杂度O(nlogn)
+时间复杂度:$O(n\log n)$
-空间复杂度O(n)
+空间复杂度:$O(n)$
## 总结
diff --git a/problems/0392.判断子序列.md b/problems/0392.判断子序列.md
index f71940f6..e157b439 100644
--- a/problems/0392.判断子序列.md
+++ b/problems/0392.判断子序列.md
@@ -31,7 +31,7 @@
## 思路
-(这道题可以用双指针的思路来实现,时间复杂度就是O(n))
+(这道题可以用双指针的思路来实现,时间复杂度就是$O(n)$)
这道题应该算是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。
@@ -122,8 +122,8 @@ public:
};
```
-* 时间复杂度:O(n * m)
-* 空间复杂度:O(n * m)
+* 时间复杂度:$O(n × m)$
+* 空间复杂度:$O(n × m)$
## 总结
diff --git a/problems/0406.根据身高重建队列.md b/problems/0406.根据身高重建队列.md
index 1d79209a..40756878 100644
--- a/problems/0406.根据身高重建队列.md
+++ b/problems/0406.根据身高重建队列.md
@@ -116,12 +116,12 @@ public:
}
};
```
-* 时间复杂度O(nlogn + n^2)
-* 空间复杂度O(n)
+* 时间复杂度:$O(n\log n + n^2)$
+* 空间复杂度:$O(n)$
但使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。
-所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。
+所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是$O(n^2)$了,甚至可能拷贝好几次,就不止$O(n^2)$了。
改成链表之后,C++代码如下:
@@ -150,8 +150,8 @@ public:
};
```
-* 时间复杂度O(nlogn + n^2)
-* 空间复杂度O(n)
+* 时间复杂度:$O(n\log n + n^2)$
+* 空间复杂度:$O(n)$
大家可以把两个版本的代码提交一下试试,就可以发现其差别了!
diff --git a/problems/0416.分割等和子集.md b/problems/0416.分割等和子集.md
index b661a766..2c33842a 100644
--- a/problems/0416.分割等和子集.md
+++ b/problems/0416.分割等和子集.md
@@ -68,7 +68,7 @@
* 背包的体积为sum / 2
* 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
-* 背包如何正好装满,说明找到了总和为 sum / 2 的子集。
+* 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
* 背包中每一个元素是不可重复放入。
以上分析完,我们就可以套用01背包,来解决这个问题了。
@@ -168,8 +168,8 @@ public:
};
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(n),虽然dp数组大小为一个常数,但是大常数
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n)$,虽然dp数组大小为一个常数,但是大常数
## 总结
diff --git a/problems/0435.无重叠区间.md b/problems/0435.无重叠区间.md
index 9b843581..1ca8083e 100644
--- a/problems/0435.无重叠区间.md
+++ b/problems/0435.无重叠区间.md
@@ -92,8 +92,8 @@ public:
}
};
```
-* 时间复杂度:O(nlogn) ,有一个快排
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n\log n)$ ,有一个快排
+* 空间复杂度:$O(1)$
大家此时会发现如此复杂的一个问题,代码实现却这么简单!
diff --git a/problems/0450.删除二叉搜索树中的节点.md b/problems/0450.删除二叉搜索树中的节点.md
index 4c433899..c9be2c02 100644
--- a/problems/0450.删除二叉搜索树中的节点.md
+++ b/problems/0450.删除二叉搜索树中的节点.md
@@ -17,7 +17,7 @@
首先找到需要删除的节点;
如果找到了,删除它。
-说明: 要求算法时间复杂度为 O(h),h 为树的高度。
+说明: 要求算法时间复杂度为 $O(h)$,h 为树的高度。
示例:
diff --git a/problems/0452.用最少数量的箭引爆气球.md b/problems/0452.用最少数量的箭引爆气球.md
index 93361056..326680e1 100644
--- a/problems/0452.用最少数量的箭引爆气球.md
+++ b/problems/0452.用最少数量的箭引爆气球.md
@@ -105,8 +105,8 @@ public:
};
```
-* 时间复杂度O(nlogn),因为有一个快排
-* 空间复杂度O(1)
+* 时间复杂度:$O(n\log n)$,因为有一个快排
+* 空间复杂度:$O(1)$
可以看出代码并不复杂。
diff --git a/problems/0455.分发饼干.md b/problems/0455.分发饼干.md
index 4c7c817a..be842083 100644
--- a/problems/0455.分发饼干.md
+++ b/problems/0455.分发饼干.md
@@ -59,7 +59,7 @@ public:
int findContentChildren(vector& g, vector& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
- int index = s.size() - 1; // 饼干数组的下表
+ int index = s.size() - 1; // 饼干数组的下标
int result = 0;
for (int i = g.size() - 1; i >= 0; i--) {
if (index >= 0 && s[index] >= g[i]) {
diff --git a/problems/0491.递增子序列.md b/problems/0491.递增子序列.md
index 231ea494..ee7180dd 100644
--- a/problems/0491.递增子序列.md
+++ b/problems/0491.递增子序列.md
@@ -56,7 +56,7 @@
代码如下:
-```
+```cpp
vector> result;
vector path;
void backtracking(vector& nums, int startIndex)
@@ -68,7 +68,7 @@ void backtracking(vector& nums, int startIndex)
但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下:
-```
+```cpp
if (path.size() > 1) {
result.push_back(path);
// 注意这里不要加return,因为要取树上的所有节点
@@ -82,7 +82,7 @@ if (path.size() > 1) {
那么单层搜索代码如下:
-```
+```cpp
unordered_set uset; // 使用set来对本层元素进行去重
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back())
@@ -431,6 +431,36 @@ int** findSubsequences(int* nums, int numsSize, int* returnSize, int** returnCol
}
```
+## Swift
+
+```swift
+func findSubsequences(_ nums: [Int]) -> [[Int]] {
+ var result = [[Int]]()
+ var path = [Int]()
+ func backtracking(startIndex: Int) {
+ // 收集结果,但不返回,因为后续还要以此基础拼接
+ if path.count > 1 {
+ result.append(path)
+ }
+
+ var uset = Set()
+ let end = nums.count
+ guard startIndex < end else { return } // 终止条件
+ for i in startIndex ..< end {
+ let num = nums[i]
+ if uset.contains(num) { continue } // 跳过重复元素
+ if !path.isEmpty, num < path.last! { continue } // 确保递增
+ uset.insert(num) // 通过set记录
+ path.append(num) // 处理:收集元素
+ backtracking(startIndex: i + 1) // 元素不重复访问
+ path.removeLast() // 回溯
+ }
+ }
+ backtracking(startIndex: 0)
+ return result
+}
+```
+
-----------------------
diff --git a/problems/0494.目标和.md b/problems/0494.目标和.md
index 01554b02..1b107c95 100644
--- a/problems/0494.目标和.md
+++ b/problems/0494.目标和.md
@@ -221,8 +221,8 @@ public:
};
```
-* 时间复杂度O(n * m),n为正数个数,m为背包容量
-* 空间复杂度:O(m) m为背包容量
+* 时间复杂度:$O(n × m)$,n为正数个数,m为背包容量
+* 空间复杂度:$O(m)$,m为背包容量
## 总结
diff --git a/problems/0496.下一个更大元素I.md b/problems/0496.下一个更大元素I.md
index deeae2a6..0ca899fb 100644
--- a/problems/0496.下一个更大元素I.md
+++ b/problems/0496.下一个更大元素I.md
@@ -70,7 +70,7 @@ C++中,当我们要使用集合来解决哈希问题的时候,优先使用un
那么预处理代码如下:
```CPP
-unordered_map umap; // key:下表元素,value:下表
+unordered_map umap; // key:下标元素,value:下标
for (int i = 0; i < nums1.size(); i++) {
umap[nums1[i]] = i;
}
@@ -108,7 +108,7 @@ for (int i = 0; i < nums1.size(); i++) {
```CPP
while (!st.empty() && nums2[i] > nums2[st.top()]) {
if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素
- int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下表
+ int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标
result[index] = nums2[i];
}
st.pop();
@@ -128,7 +128,7 @@ public:
vector result(nums1.size(), -1);
if (nums1.size() == 0) return result;
- unordered_map umap; // key:下表元素,value:下表
+ unordered_map umap; // key:下标元素,value:下标
for (int i = 0; i < nums1.size(); i++) {
umap[nums1[i]] = i;
}
@@ -141,7 +141,7 @@ public:
} else { // 情况三
while (!st.empty() && nums2[i] > nums2[st.top()]) {
if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素
- int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下表
+ int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标
result[index] = nums2[i];
}
st.pop();
@@ -166,7 +166,7 @@ public:
vector result(nums1.size(), -1);
if (nums1.size() == 0) return result;
- unordered_map umap; // key:下表元素,value:下表
+ unordered_map umap; // key:下标元素,value:下标
for (int i = 0; i < nums1.size(); i++) {
umap[nums1[i]] = i;
}
@@ -174,7 +174,7 @@ public:
for (int i = 1; i < nums2.size(); i++) {
while (!st.empty() && nums2[i] > nums2[st.top()]) {
if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素
- int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下表
+ int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标
result[index] = nums2[i];
}
st.pop();
@@ -264,7 +264,7 @@ func nextGreaterElement(nums1 []int, nums2 []int) []int {
top := stack[len(stack)-1]
if _, ok := mp[nums2[top]]; ok { // 看map里是否存在这个元素
- index := mp[nums2[top]]; // 根据map找到nums2[top] 在 nums1中的下表
+ index := mp[nums2[top]]; // 根据map找到nums2[top] 在 nums1中的下标
res[index] = nums2[i]
}
diff --git a/problems/0503.下一个更大元素II.md b/problems/0503.下一个更大元素II.md
index 0634e932..2d09c527 100644
--- a/problems/0503.下一个更大元素II.md
+++ b/problems/0503.下一个更大元素II.md
@@ -68,7 +68,7 @@ public:
这种写法确实比较直观,但做了很多无用操作,例如修改了nums数组,而且最后还要把result数组resize回去。
-resize倒是不费时间,是O(1)的操作,但扩充nums数组相当于多了一个O(n)的操作。
+resize倒是不费时间,是$O(1)$的操作,但扩充nums数组相当于多了一个$O(n)$的操作。
其实也可以不扩充nums,而是在遍历的过程中模拟走了两边nums。
diff --git a/problems/0509.斐波那契数.md b/problems/0509.斐波那契数.md
index 3cdd2c52..b866cbe2 100644
--- a/problems/0509.斐波那契数.md
+++ b/problems/0509.斐波那契数.md
@@ -101,8 +101,8 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
当然可以发现,我们只需要维护两个数值就可以了,不需要记录整个序列。
@@ -126,8 +126,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
### 递归解法
@@ -145,8 +145,8 @@ public:
};
```
-* 时间复杂度:O(2^n)
-* 空间复杂度:O(n) 算上了编程语言中实现递归的系统栈所占空间
+* 时间复杂度:$O(2^n)$
+* 空间复杂度:$O(n)$,算上了编程语言中实现递归的系统栈所占空间
这个递归的时间复杂度大家画一下树形图就知道了,如果不清晰的同学,可以看这篇:[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)
diff --git a/problems/0647.回文子串.md b/problems/0647.回文子串.md
index 89046eed..9aa44cad 100644
--- a/problems/0647.回文子串.md
+++ b/problems/0647.回文子串.md
@@ -32,7 +32,7 @@
两层for循环,遍历区间起始位置和终止位置,然后判断这个区间是不是回文。
-时间复杂度:O(n^3)
+时间复杂度:$O(n^3)$
## 动态规划
@@ -171,8 +171,8 @@ public:
};
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(n^2)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n^2)$
## 双指针法
@@ -212,8 +212,9 @@ public:
}
};
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(1)
+
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(1)$
## 其他语言版本
diff --git a/problems/0654.最大二叉树.md b/problems/0654.最大二叉树.md
index 361a92d1..d5209b5d 100644
--- a/problems/0654.最大二叉树.md
+++ b/problems/0654.最大二叉树.md
@@ -63,7 +63,7 @@ if (nums.size() == 1) {
这里有三步工作
-1. 先要找到数组中最大的值和对应的下表, 最大的值构造根节点,下表用来下一步分割数组。
+1. 先要找到数组中最大的值和对应的下标, 最大的值构造根节点,下标用来下一步分割数组。
代码如下:
```CPP
@@ -79,7 +79,7 @@ TreeNode* node = new TreeNode(0);
node->val = maxValue;
```
-2. 最大值所在的下表左区间 构造左子树
+2. 最大值所在的下标左区间 构造左子树
这里要判断maxValueIndex > 0,因为要保证左区间至少有一个数值。
@@ -91,7 +91,7 @@ if (maxValueIndex > 0) {
}
```
-3. 最大值所在的下表右区间 构造右子树
+3. 最大值所在的下标右区间 构造右子树
判断maxValueIndex < (nums.size() - 1),确保右区间至少有一个数值。
@@ -114,7 +114,7 @@ public:
node->val = nums[0];
return node;
}
- // 找到数组中最大的值和对应的下表
+ // 找到数组中最大的值和对应的下标
int maxValue = 0;
int maxValueIndex = 0;
for (int i = 0; i < nums.size(); i++) {
@@ -124,12 +124,12 @@ public:
}
}
node->val = maxValue;
- // 最大值所在的下表左区间 构造左子树
+ // 最大值所在的下标左区间 构造左子树
if (maxValueIndex > 0) {
vector newVec(nums.begin(), nums.begin() + maxValueIndex);
node->left = constructMaximumBinaryTree(newVec);
}
- // 最大值所在的下表右区间 构造右子树
+ // 最大值所在的下标右区间 构造右子树
if (maxValueIndex < (nums.size() - 1)) {
vector newVec(nums.begin() + maxValueIndex + 1, nums.end());
node->right = constructMaximumBinaryTree(newVec);
@@ -141,7 +141,7 @@ public:
以上代码比较冗余,效率也不高,每次还要切割的时候每次都要定义新的vector(也就是数组),但逻辑比较清晰。
-和文章[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中一样的优化思路,就是每次分隔不用定义新的数组,而是通过下表索引直接在原数组上操作。
+和文章[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中一样的优化思路,就是每次分隔不用定义新的数组,而是通过下标索引直接在原数组上操作。
优化后代码如下:
@@ -152,7 +152,7 @@ private:
TreeNode* traversal(vector& nums, int left, int right) {
if (left >= right) return nullptr;
- // 分割点下表:maxValueIndex
+ // 分割点下标:maxValueIndex
int maxValueIndex = left;
for (int i = left + 1; i < right; ++i) {
if (nums[i] > nums[maxValueIndex]) maxValueIndex = i;
@@ -212,7 +212,7 @@ root->right = traversal(nums, maxValueIndex + 1, right);
这道题目其实和 [二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) 是一个思路,比[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) 还简单一些。
-**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下表索引直接在原数组上操作,这样可以节约时间和空间上的开销。**
+**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。**
一些同学也会疑惑,什么时候递归函数前面加if,什么时候不加if,这个问题我在最后也给出了解释。
diff --git a/problems/0673.最长递增子序列的个数.md b/problems/0673.最长递增子序列的个数.md
index d15ecbb9..1bfaa7d0 100644
--- a/problems/0673.最长递增子序列的个数.md
+++ b/problems/0673.最长递增子序列的个数.md
@@ -216,10 +216,10 @@ public:
};
```
-* 时间复杂度O(n^2)
-* 空间复杂度O(n)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(n)$
-还有O(nlogn)的解法,使用树状数组,今天有点忙就先不写了,感兴趣的同学可以自行学习一下,这里有我之前写的树状数组系列博客:https://blog.csdn.net/youngyangyang04/category_871105.html (十年前的陈年老文了)
+还有$O(n\log n)$的解法,使用树状数组,今天有点忙就先不写了,感兴趣的同学可以自行学习一下,这里有我之前写的树状数组系列博客:https://blog.csdn.net/youngyangyang04/category_871105.html (十年前的陈年老文了)
# 其他语言版本
diff --git a/problems/0674.最长连续递增序列.md b/problems/0674.最长连续递增序列.md
index 740b0ce7..4c1f6453 100644
--- a/problems/0674.最长连续递增序列.md
+++ b/problems/0674.最长连续递增序列.md
@@ -107,8 +107,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
### 贪心
@@ -135,12 +135,12 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
## 总结
-本题也是动规里子序列问题的经典题目,但也可以用贪心来做,大家也会发现贪心好像更简单一点,而且空间复杂度仅是O(1)。
+本题也是动规里子序列问题的经典题目,但也可以用贪心来做,大家也会发现贪心好像更简单一点,而且空间复杂度仅是$O(1)$。
在动规分析中,关键是要理解和[动态规划:300.最长递增子序列](https://programmercarl.com/0300.最长上升子序列.html)的区别。
@@ -156,7 +156,7 @@ public:
Java:
```java
/**
- * 1.dp[i] 代表当前下表最大连续值
+ * 1.dp[i] 代表当前下标最大连续值
* 2.递推公式 if(nums[i+1]>nums[i]) dp[i+1] = dp[i]+1
* 3.初始化 都为1
* 4.遍历方向,从其那往后
diff --git a/problems/0701.二叉搜索树中的插入操作.md b/problems/0701.二叉搜索树中的插入操作.md
index 5d63ce58..1e6ab47e 100644
--- a/problems/0701.二叉搜索树中的插入操作.md
+++ b/problems/0701.二叉搜索树中的插入操作.md
@@ -236,16 +236,13 @@ class Solution {
```java
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
- return buildTree(root, val);
- }
-
- public TreeNode buildTree(TreeNode root, int val){
if (root == null) // 如果当前节点为空,也就意味着val找到了合适的位置,此时创建节点直接返回。
return new TreeNode(val);
+
if (root.val < val){
- root.right = buildTree(root.right, val); // 递归创建右子树
+ root.right = insertIntoBST(root.right, val); // 递归创建右子树
}else if (root.val > val){
- root.left = buildTree(root.left, val); // 递归创建左子树
+ root.left = insertIntoBST(root.left, val); // 递归创建左子树
}
return root;
}
diff --git a/problems/0714.买卖股票的最佳时机含手续费.md b/problems/0714.买卖股票的最佳时机含手续费.md
index f7ddeaf7..ae0ee047 100644
--- a/problems/0714.买卖股票的最佳时机含手续费.md
+++ b/problems/0714.买卖股票的最佳时机含手续费.md
@@ -84,8 +84,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,**所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!**
@@ -117,8 +117,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
当然可以对空间经行优化,因为当前状态只是依赖前一个状态。
@@ -140,8 +140,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
## 总结
diff --git a/problems/0714.买卖股票的最佳时机含手续费(动态规划).md b/problems/0714.买卖股票的最佳时机含手续费(动态规划).md
index 700b8cde..4a5b8cf1 100644
--- a/problems/0714.买卖股票的最佳时机含手续费(动态规划).md
+++ b/problems/0714.买卖股票的最佳时机含手续费(动态规划).md
@@ -37,8 +37,9 @@
在讲解贪心专题的时候,我们已经讲过本题了[贪心算法:买卖股票的最佳时机含手续费](https://programmercarl.com/0714.买卖股票的最佳时机含手续费.html)
使用贪心算法,的性能是:
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
那么我们再来看看是使用动规的方法如何解题。
@@ -86,8 +87,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 其他语言版本
diff --git a/problems/0718.最长重复子数组.md b/problems/0718.最长重复子数组.md
index ec2a4366..b2738ac1 100644
--- a/problems/0718.最长重复子数组.md
+++ b/problems/0718.最长重复子数组.md
@@ -111,8 +111,8 @@ public:
};
```
-* 时间复杂度O(n * m) n 为A长度,m为B长度
-* 空间复杂度O(n * m)
+* 时间复杂度:$O(n × m)$,n 为A长度,m为B长度
+* 空间复杂度:$O(n × m)$
## 滚动数组
@@ -145,8 +145,8 @@ public:
};
```
-* 时间复杂度O(n * m) n 为A长度,m为B长度
-* 空间复杂度O(m)
+* 时间复杂度:$O(n × m)$,n 为A长度,m为B长度
+* 空间复杂度:$O(m)$
## 其他语言版本
diff --git a/problems/0738.单调递增的数字.md b/problems/0738.单调递增的数字.md
index 6baaa827..cb21965a 100644
--- a/problems/0738.单调递增的数字.md
+++ b/problems/0738.单调递增的数字.md
@@ -54,8 +54,8 @@ public:
}
};
```
-* 时间复杂度:O(n * m) m为n的数字长度
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n × m)$ m为n的数字长度
+* 空间复杂度:$O(1)$
## 贪心算法
@@ -108,8 +108,8 @@ public:
```
-* 时间复杂度:O(n) n 为数字长度
-* 空间复杂度:O(n) 需要一个字符串,转化为字符串操作更方便
+* 时间复杂度:$O(n)$,n 为数字长度
+* 空间复杂度:$O(n)$,需要一个字符串,转化为字符串操作更方便
## 总结
diff --git a/problems/0739.每日温度.md b/problems/0739.每日温度.md
index fe9f9f7a..9fdcafc0 100644
--- a/problems/0739.每日温度.md
+++ b/problems/0739.每日温度.md
@@ -18,7 +18,7 @@
## 思路
-首先想到的当然是暴力解法,两层for循环,把至少需要等待的天数就搜出来了。时间复杂度是O(n^2)
+首先想到的当然是暴力解法,两层for循环,把至少需要等待的天数就搜出来了。时间复杂度是$O(n^2)$
那么接下来在来看看使用单调栈的解法。
@@ -26,13 +26,13 @@
**通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了**。
-时间复杂度为O(n)。
+时间复杂度为$O(n)$。
例如本题其实就是找找到一个元素右边第一个比自己大的元素。
此时就应该想到用单调栈了。
-那么单调栈的原理是什么呢?为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?
+那么单调栈的原理是什么呢?为什么时间复杂度是$O(n)$就可以找到每一个元素的右边第一个比它大的元素位置呢?
单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素的元素,优点是只需要遍历一次。
@@ -165,8 +165,8 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
精简的代码是直接把情况一二三都合并到了一起,其实这种代码精简是精简,但思路不是很清晰。
diff --git a/problems/0746.使用最小花费爬楼梯.md b/problems/0746.使用最小花费爬楼梯.md
index ce7b310c..6bd3d858 100644
--- a/problems/0746.使用最小花费爬楼梯.md
+++ b/problems/0746.使用最小花费爬楼梯.md
@@ -113,8 +113,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
还可以优化空间复杂度,因为dp[i]就是由前两位推出来的,那么也不用dp数组了,C++代码如下:
@@ -136,8 +136,8 @@ public:
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
**当然我不建议这么写,能写出版本一就可以了,直观简洁!**
diff --git a/problems/0763.划分字母区间.md b/problems/0763.划分字母区间.md
index 40318726..b55f4b2b 100644
--- a/problems/0763.划分字母区间.md
+++ b/problems/0763.划分字母区间.md
@@ -68,8 +68,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1) 使用的hash数组是固定大小
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$,使用的hash数组是固定大小
## 总结
diff --git a/problems/0844.比较含退格的字符串.md b/problems/0844.比较含退格的字符串.md
index 95cf69c8..deb4a433 100644
--- a/problems/0844.比较含退格的字符串.md
+++ b/problems/0844.比较含退格的字符串.md
@@ -36,7 +36,7 @@
# 思路
-本文将给出 空间复杂度O(n)的栈模拟方法 以及空间复杂度是O(1)的双指针方法。
+本文将给出 空间复杂度$O(n)$的栈模拟方法 以及空间复杂度是$O(1)$的双指针方法。
## 普通方法(使用栈的思路)
@@ -71,8 +71,8 @@ public:
}
};
```
-* 时间复杂度:O(n + m), n为S的长度,m为T的长度 ,也可以理解是O(n)的时间复杂度
-* 空间复杂度:O(n + m)
+* 时间复杂度:$O(n + m)$,n为S的长度,m为T的长度 ,也可以理解是$O(n)$的时间复杂度
+* 空间复杂度:$O(n + m)$
当然以上代码,大家可以发现有重复的逻辑处理S,处理T,可以把这块公共逻辑抽离出来,代码精简如下:
@@ -96,12 +96,13 @@ public:
};
```
性能依然是:
-* 时间复杂度:O(n + m)
-* 空间复杂度:O(n + m)
+
+* 时间复杂度:$O(n + m)$
+* 空间复杂度:$O(n + m)$
## 优化方法(从后向前双指针)
-当然还可以有使用 O(1) 的空间复杂度来解决该问题。
+当然还可以有使用 $O(1)$ 的空间复杂度来解决该问题。
同时从后向前遍历S和T(i初始为S末尾,j初始为T末尾),记录#的数量,模拟消除的操作,如果#用完了,就开始比较S[i]和S[j]。
@@ -150,8 +151,8 @@ public:
};
```
-* 时间复杂度:O(n + m)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n + m)$
+* 空间复杂度:$O(1)$
# 其他语言版本
diff --git a/problems/0860.柠檬水找零.md b/problems/0860.柠檬水找零.md
index feb2f5a0..ceb4e5ee 100644
--- a/problems/0860.柠檬水找零.md
+++ b/problems/0860.柠檬水找零.md
@@ -183,7 +183,7 @@ class Solution:
```golang
func lemonadeChange(bills []int) bool {
- //left表示还剩多少 下表0位5元的个数 ,下表1为10元的个数
+ //left表示还剩多少 下标0位5元的个数 ,下标1为10元的个数
left:=[2]int{0,0}
//第一个元素不为5,直接退出
if bills[0]!=5{
diff --git a/problems/0922.按奇偶排序数组II.md b/problems/0922.按奇偶排序数组II.md
index 71b13de1..36d0c2cc 100644
--- a/problems/0922.按奇偶排序数组II.md
+++ b/problems/0922.按奇偶排序数组II.md
@@ -26,11 +26,11 @@
## 思路
-这道题目直接的想法可能是两层for循环再加上used数组表示使用过的元素。这样的的时间复杂度是O(n^2)。
+这道题目直接的想法可能是两层for循环再加上used数组表示使用过的元素。这样的的时间复杂度是$O(n^2)$。
### 方法一
-其实这道题可以用很朴实的方法,时间复杂度就就是O(n)了,C++代码如下:
+其实这道题可以用很朴实的方法,时间复杂度就就是$O(n)$了,C++代码如下:
```CPP
class Solution {
@@ -57,8 +57,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
### 方法二
@@ -69,8 +69,8 @@ class Solution {
public:
vector sortArrayByParityII(vector& A) {
vector result(A.size());
- int evenIndex = 0; // 偶数下表
- int oddIndex = 1; // 奇数下表
+ int evenIndex = 0; // 偶数下标
+ int oddIndex = 1; // 奇数下标
for (int i = 0; i < A.size(); i++) {
if (A[i] % 2 == 0) {
result[evenIndex] = A[i];
@@ -86,8 +86,8 @@ public:
};
```
-* 时间复杂度O(n)
-* 空间复杂度O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
### 方法三
@@ -109,10 +109,10 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
-这里时间复杂度并不是O(n^2),因为偶数位和奇数位都只操作一次,不是n/2 * n/2的关系,而是n/2 + n/2的关系!
+这里时间复杂度并不是$O(n^2)$,因为偶数位和奇数位都只操作一次,不是n/2 * n/2的关系,而是n/2 + n/2的关系!
## 其他语言版本
diff --git a/problems/0925.长按键入.md b/problems/0925.长按键入.md
index 603040e6..dd3679ba 100644
--- a/problems/0925.长按键入.md
+++ b/problems/0925.长按键入.md
@@ -90,8 +90,8 @@ public:
```
-时间复杂度:O(n)
-空间复杂度:O(1)
+时间复杂度:$O(n)$
+空间复杂度:$O(1)$
# 其他语言版本
diff --git a/problems/0941.有效的山脉数组.md b/problems/0941.有效的山脉数组.md
index 24a2906b..9c346c53 100644
--- a/problems/0941.有效的山脉数组.md
+++ b/problems/0941.有效的山脉数组.md
@@ -43,7 +43,7 @@
**注意这里还是有一些细节,例如如下两点:**
-* 因为left和right是数组下表,移动的过程中注意不要数组越界
+* 因为left和right是数组下标,移动的过程中注意不要数组越界
* 如果left或者right没有移动,说明是一个单调递增或者递减的数组,依然不是山峰
C++代码如下:
diff --git a/problems/0977.有序数组的平方.md b/problems/0977.有序数组的平方.md
index 6616ba8c..ac300a7d 100644
--- a/problems/0977.有序数组的平方.md
+++ b/problems/0977.有序数组的平方.md
@@ -40,7 +40,7 @@ public:
};
```
-这个时间复杂度是 O(n + nlogn), 可以说是O(nlogn)的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 O(n + nlogn)。
+这个时间复杂度是 $O(n + n\log n)$, 可以说是$O(n\log n)$的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 $O(n + n\log n)$。
## 双指针法
@@ -83,7 +83,7 @@ public:
};
```
-此时的时间复杂度为O(n),相对于暴力排序的解法O(n + nlogn)还是提升不少的。
+此时的时间复杂度为$O(n)$,相对于暴力排序的解法$O(n + n\log n)$还是提升不少的。
**这里还是说一下,大家不必太在意leetcode上执行用时,打败多少多少用户,这个就是一个玩具,非常不准确。**
diff --git a/problems/1002.查找常用字符.md b/problems/1002.查找常用字符.md
index 093bfdde..63a15751 100644
--- a/problems/1002.查找常用字符.md
+++ b/problems/1002.查找常用字符.md
@@ -40,7 +40,7 @@ words[i] 由小写英文字母组成
这道题目一眼看上去,就是用哈希法,**“小写字符”,“出现频率”, 这些关键字都是为哈希法量身定做的啊**
-首先可以想到的是暴力解法,一个字符串一个字符串去搜,时间复杂度是O(n^m),n是字符串长度,m是有几个字符串。
+首先可以想到的是暴力解法,一个字符串一个字符串去搜,时间复杂度是$O(n^m)$,n是字符串长度,m是有几个字符串。
可以看出这是指数级别的时间复杂度,非常高,而且代码实现也不容易,因为要统计 重复的字符,还要适当的替换或者去重。
diff --git a/problems/1049.最后一块石头的重量II.md b/problems/1049.最后一块石头的重量II.md
index 842e63aa..79cc69e1 100644
--- a/problems/1049.最后一块石头的重量II.md
+++ b/problems/1049.最后一块石头的重量II.md
@@ -136,8 +136,8 @@ public:
```
-* 时间复杂度:O(m * n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
-* 空间复杂度:O(m)
+* 时间复杂度:$O(m × n)$ , m是石头总重量(准确的说是总重量的一半),n为石头块数
+* 空间复杂度:$O(m)$
## 总结
diff --git a/problems/1365.有多少小于当前数字的数字.md b/problems/1365.有多少小于当前数字的数字.md
index 5e1e875c..c7f9d0f9 100644
--- a/problems/1365.有多少小于当前数字的数字.md
+++ b/problems/1365.有多少小于当前数字的数字.md
@@ -41,7 +41,7 @@
# 思路
-两层for循环暴力查找,时间复杂度明显为O(n^2)。
+两层for循环暴力查找,时间复杂度明显为$O(n^2)$。
那么我们来看一下如何优化。
@@ -110,7 +110,7 @@ public:
};
```
-可以排序之后加哈希,时间复杂度为O(nlogn)
+可以排序之后加哈希,时间复杂度为$O(n\log n)$
# 其他语言版本
diff --git a/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md b/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md
index 4c368ef8..9e36092c 100644
--- a/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md
+++ b/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md
@@ -3,7 +3,7 @@
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!
-# 程序提交之后为什么会超时?O(n)的算法会超时,n究竟是多大?
+# 程序提交之后为什么会超时?$O(n)$的算法会超时,n究竟是多大?
一些同学可能对计算机运行的速度还没有概念,就是感觉计算机运行速度应该会很快,那么在leetcode上做算法题目的时候为什么会超时呢?
@@ -18,9 +18,9 @@
也就是说程序运行的时间超过了规定的时间,一般OJ(online judge)的超时时间就是1s,也就是用例数据输入后最多要1s内得到结果,暂时还不清楚leetcode的判题规则,下文为了方便讲解,暂定超时时间就是1s。
-如果写出了一个O(n)的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。
+如果写出了一个$O(n)$的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。
-如果n的规模已经足够让O(n)的算法运行时间超过了1s,就应该考虑log(n)的解法了。
+如果n的规模已经足够让$O(n)$的算法运行时间超过了1s,就应该考虑log(n)的解法了。
# 从硬件配置看计算机的性能
@@ -63,7 +63,7 @@
测试硬件:2015年MacPro,CPU配置:2.7 GHz Dual-Core Intel Core i5
-实现三个函数,时间复杂度分别是 O(n) , O(n^2), O(nlogn),使用加法运算来统一测试。
+实现三个函数,时间复杂度分别是 $O(n)$ , $O(n^2)$, $O(n\log n)$,使用加法运算来统一测试。
```CPP
// O(n)
@@ -128,19 +128,19 @@ int main() {

-O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下O(n^2) 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。
+O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下$O(n^2)$ 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。

O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。
-在推测一下O(nlogn)的话, 1s可以处理的数据规模是什么呢?
+在推测一下$O(n\log n)$的话, 1s可以处理的数据规模是什么呢?
-理论上应该是比 O(n)少一个数量级,因为logn的复杂度 其实是很快,看一下实验数据。
+理论上应该是比 $O(n)$少一个数量级,因为$\log n$的复杂度 其实是很快,看一下实验数据。

-O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。
+$O(n \logn)$的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。
这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。
@@ -148,7 +148,7 @@ O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符

-至于O(logn) 和O(n^3) 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。
+至于$O(\log n)$和$O(n^3)$ 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。
# 完整测试代码
@@ -209,7 +209,7 @@ int main() {
# 总结
-本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看O(n)的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。
+本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看$O(n)$的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。
建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。
diff --git a/problems/二叉树理论基础.md b/problems/二叉树理论基础.md
index dab434e6..37b200af 100644
--- a/problems/二叉树理论基础.md
+++ b/problems/二叉树理论基础.md
@@ -93,7 +93,7 @@
用数组来存储二叉树如何遍历的呢?
-**如果父节点的数组下表是i,那么它的左孩子就是i * 2 + 1,右孩子就是 i * 2 + 2。**
+**如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。**
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
@@ -227,6 +227,21 @@ function TreeNode(val, left, right) {
}
```
+Swift:
+```Swift
+class TreeNode {
+ var value: T
+ var left: TreeNode?
+ var right: TreeNode?
+ init(_ value: T,
+ left: TreeNode? = nil,
+ right: TreeNode? = nil) {
+ self.value = value
+ self.left = left
+ self.right = right
+ }
+}
+```
-----------------------
diff --git a/problems/二叉树的递归遍历.md b/problems/二叉树的递归遍历.md
index 875e6169..0468b79b 100644
--- a/problems/二叉树的递归遍历.md
+++ b/problems/二叉树的递归遍历.md
@@ -409,5 +409,56 @@ int* postorderTraversal(struct TreeNode* root, int* returnSize){
}
```
+Swift:
+前序遍历:(144.二叉树的前序遍历)
+```Swift
+func preorderTraversal(_ root: TreeNode?) -> [Int] {
+ var res = [Int]()
+ preorder(root, res: &res)
+ return res
+}
+func preorder(_ root: TreeNode?, res: inout [Int]) {
+ if root == nil {
+ return
+ }
+ res.append(root!.val)
+ preorder(root!.left, res: &res)
+ preorder(root!.right, res: &res)
+}
+```
+
+中序遍历:(94. 二叉树的中序遍历)
+```Swift
+func inorderTraversal(_ root: TreeNode?) -> [Int] {
+ var res = [Int]()
+ inorder(root, res: &res)
+ return res
+}
+func inorder(_ root: TreeNode?, res: inout [Int]) {
+ if root == nil {
+ return
+ }
+ inorder(root!.left, res: &res)
+ res.append(root!.val)
+ inorder(root!.right, res: &res)
+}
+```
+
+后序遍历:(145. 二叉树的后序遍历)
+```Swift
+func postorderTraversal(_ root: TreeNode?) -> [Int] {
+ var res = [Int]()
+ postorder(root, res: &res)
+ return res
+}
+func postorder(_ root: TreeNode?, res: inout [Int]) {
+ if root == nil {
+ return
+ }
+ postorder(root!.left, res: &res)
+ postorder(root!.right, res: &res)
+ res.append(root!.val)
+}
+```
-----------------------
diff --git a/problems/关于时间复杂度,你不知道的都在这里!.md b/problems/关于时间复杂度,你不知道的都在这里!.md
index c36c8b61..f05af6bc 100644
--- a/problems/关于时间复杂度,你不知道的都在这里!.md
+++ b/problems/关于时间复杂度,你不知道的都在这里!.md
@@ -16,21 +16,21 @@
那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认CPU的每个单元运行消耗的时间都是相同的。
-假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 O(f(n))。
+假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 $O(f(n))$。
## 什么是大O
-这里的大O是指什么呢,说到时间复杂度,**大家都知道O(n),O(n^2),却说不清什么是大O**。
+这里的大O是指什么呢,说到时间复杂度,**大家都知道$O(n)$,$O(n^2)$,却说不清什么是大O**。
算法导论给出的解释:**大O用来表示上界的**,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。
-同样算法导论给出了例子:拿插入排序来说,插入排序的时间复杂度我们都说是O(n^2) 。
+同样算法导论给出了例子:拿插入排序来说,插入排序的时间复杂度我们都说是$O(n^2)$ 。
-输入数据的形式对程序运算时间是有很大影响的,在数据本来有序的情况下时间复杂度是O(n),但如果数据是逆序的话,插入排序的时间复杂度就是O(n^2),也就对于所有输入情况来说,最坏是O(n^2) 的时间复杂度,所以称插入排序的时间复杂度为O(n^2)。
+输入数据的形式对程序运算时间是有很大影响的,在数据本来有序的情况下时间复杂度是$O(n)$,但如果数据是逆序的话,插入排序的时间复杂度就是$O(n^2)$,也就对于所有输入情况来说,最坏是$O(n^2)$ 的时间复杂度,所以称插入排序的时间复杂度为$O(n^2)$。
-同样的同理再看一下快速排序,都知道快速排序是O(nlogn),但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的,**所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)**。
+同样的同理再看一下快速排序,都知道快速排序是$O(n\log n)$,但是当数据已经有序情况下,快速排序的时间复杂度是$O(n^2)$ 的,**所以严格从大O的定义来讲,快速排序的时间复杂度应该是$O(n^2)$**。
-**但是我们依然说快速排序是O(nlogn)的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界**。如图所示:
+**但是我们依然说快速排序是$O(n\log n)$的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界**。如图所示:

我们主要关心的还是一般情况下的数据形式。
@@ -44,11 +44,11 @@

-在决定使用哪些算法的时候,不是时间复杂越低的越好(因为简化后的时间复杂度忽略了常数项等等),要考虑数据规模,如果数据规模很小甚至可以用O(n^2)的算法比O(n)的更合适(在有常数项的时候)。
+在决定使用哪些算法的时候,不是时间复杂越低的越好(因为简化后的时间复杂度忽略了常数项等等),要考虑数据规模,如果数据规模很小甚至可以用$O(n^2)$的算法比$O(n)$的更合适(在有常数项的时候)。
-就像上图中 O(5n^2) 和 O(100n) 在n为20之前 很明显 O(5n^2)是更优的,所花费的时间也是最少的。
+就像上图中 $O(5n^2)$ 和 $O(100n)$ 在n为20之前 很明显 $O(5n^2)$是更优的,所花费的时间也是最少的。
-那为什么在计算时间复杂度的时候要忽略常数项系数呢,也就说O(100n) 就是O(n)的时间复杂度,O(5n^2) 就是O(n^2)的时间复杂度,而且要默认O(n) 优于O(n^2) 呢 ?
+那为什么在计算时间复杂度的时候要忽略常数项系数呢,也就说$O(100n)$ 就是$O(n)$的时间复杂度,$O(5n^2)$ 就是$O(n^2)$的时间复杂度,而且要默认$O(n)$ 优于$O(n^2)$ 呢 ?
这里就又涉及到大O的定义,**因为大O就是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度,这个数据量也就是常数项系数已经不起决定性作用的数据量**。
@@ -56,13 +56,13 @@
**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示**:
-O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n^2)平方阶 < O(n^3)(立方阶) < O(2^n) (指数阶)
+O(1) 常数阶 < $O(\log n)$ 对数阶 < $O(n)$ 线性阶 < $O(n^2)$ 平方阶 < $O(n^3)$ 立方阶 < $O(2^n)$指数阶
但是也要注意大常数,如果这个常数非常大,例如10^7 ,10^9 ,那么常数就是不得不考虑的因素了。
## 复杂表达式的化简
-有时候我们去计算时间复杂度的时候发现不是一个简单的O(n) 或者O(n^2), 而是一个复杂的表达式,例如:
+有时候我们去计算时间复杂度的时候发现不是一个简单的$O(n)$ 或者$O(n^2)$, 而是一个复杂的表达式,例如:
```
O(2*n^2 + 10*n + 1000)
@@ -88,19 +88,19 @@ O(n^2 + n)
O(n^2)
```
-如果这一步理解有困难,那也可以做提取n的操作,变成O(n(n+1)) ,省略加法常数项后也就别变成了:
+如果这一步理解有困难,那也可以做提取n的操作,变成$O(n(n+1))$,省略加法常数项后也就别变成了:
```
O(n^2)
```
-所以最后我们说:这个算法的算法时间复杂度是O(n^2) 。
+所以最后我们说:这个算法的算法时间复杂度是$O(n^2)$ 。
-也可以用另一种简化的思路,其实当n大于40的时候, 这个复杂度会恒小于O(3 * n^2),
-O(2 * n^2 + 10 * n + 1000) < O(3 * n^2),所以说最后省略掉常数项系数最终时间复杂度也是O(n^2)。
+也可以用另一种简化的思路,其实当n大于40的时候, 这个复杂度会恒小于$O(3 × n^2)$,
+$O(2 × n^2 + 10 × n + 1000)$ < $O(3 × n^2)$,所以说最后省略掉常数项系数最终时间复杂度也是$O(n^2)$。
-## O(logn)中的log是以什么为底?
+## $O(\log n)$中的log是以什么为底?
平时说这个算法的时间复杂度是logn的,那么一定是log 以2为底n的对数么?
@@ -123,21 +123,21 @@ O(2 * n^2 + 10 * n + 1000) < O(3 * n^2),所以说最后省略掉常数项系
通过这道面试题目,来分析一下时间复杂度。题目描述:找出n个字符串中相同的两个字符串(假设这里只有两个相同的字符串)。
-如果是暴力枚举的话,时间复杂度是多少呢,是O(n^2)么?
+如果是暴力枚举的话,时间复杂度是多少呢,是$O(n^2)$么?
-这里一些同学会忽略了字符串比较的时间消耗,这里并不像int 型数字做比较那么简单,除了n^2 次的遍历次数外,字符串比较依然要消耗m次操作(m也就是字母串的长度),所以时间复杂度是O(m * n * n)。
+这里一些同学会忽略了字符串比较的时间消耗,这里并不像int 型数字做比较那么简单,除了$n^2$次的遍历次数外,字符串比较依然要消耗m次操作(m也就是字母串的长度),所以时间复杂度是$O(m × n × n)$。
接下来再想一下其他解题思路。
先排对n个字符串按字典序来排序,排序后n个字符串就是有序的,意味着两个相同的字符串就是挨在一起,然后在遍历一遍n个字符串,这样就找到两个相同的字符串了。
-那看看这种算法的时间复杂度,快速排序时间复杂度为O(nlogn),依然要考虑字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是O(m * n * logn) 。
+那看看这种算法的时间复杂度,快速排序时间复杂度为$O(n\log n)$,依然要考虑字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是$O(m × n × \log n)$。
-之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 O(m * n * logn + n * m)。
+之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 $O(m × n × \log n + n × m)$。
-我们对O(m * n * logn + n * m) 进行简化操作,把m * n提取出来变成 O(m * n * (logn + 1)),再省略常数项最后的时间复杂度是 O(m * n * logn)。
+我们对$O(m × n × \log n + n × m)$进行简化操作,把$m × n$提取出来变成$O(m × n × (\log n + 1)$),再省略常数项最后的时间复杂度是$O(m × n × \log n)$。
-最后很明显O(m * n * logn) 要优于O(m * n * n)!
+最后很明显$O(m × n × \log n)$ 要优于$O(m × n × n)$!
所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。
diff --git a/problems/前序/ACM模式如何构建二叉树.md b/problems/前序/ACM模式如何构建二叉树.md
index 682e18f5..d0cf2341 100644
--- a/problems/前序/ACM模式如何构建二叉树.md
+++ b/problems/前序/ACM模式如何构建二叉树.md
@@ -45,7 +45,21 @@

-那么此时大家是不是应该知道了,数组如何转化成 二叉树了。**如果父节点的数组下标是i,那么它的左孩子下标就是i * 2 + 1,右孩子下标就是 i * 2 + 2**。
+那么此时大家是不是应该知道了,数组如何转化成 二叉树了。**如果父节点的数组下标是i,那么它的左孩子下标就是i * 2 + 1,右孩子下标就是 i * 2 + 2**。计算过程为:
+
+如果父节点在第$k$层,第$m,m \in [0,2^k]$个节点,则其左孩子所在的位置必然为$k+1$层,第$2*(m-1)+1$个节点。
+
+- 计算父节点在数组中的索引:
+ $$
+ index_{father}=(\sum_{i=0}^{i=k-1}2^i)+m-1=2^k-1+m-1
+ $$
+
+- 计算左子节点在数组的索引:
+ $$
+ index_{left}=(\sum_{i=0}^{i=k}2^i)+2*m-1-1=2^{k+1}+2m-3
+ $$
+
+- 故左孩子的下表为$index_{left}=index_{father}\times2+1$,同理可得到右子孩子的索引关系。也可以直接在左子孩子的基础上`+1`。
那么这里又有同学疑惑了,这些我都懂了,但我还是不知道 应该 怎么构造。
diff --git a/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md b/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md
index d9cc8d45..2019e691 100644
--- a/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md
+++ b/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md
@@ -20,9 +20,9 @@
也就是说程序运行的时间超过了规定的时间,一般OJ(online judge)的超时时间就是1s,也就是用例数据输入后最多要1s内得到结果,暂时还不清楚leetcode的判题规则,下文为了方便讲解,暂定超时时间就是1s。
-如果写出了一个O(n)的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。
+如果写出了一个$O(n)$的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。
-如果n的规模已经足够让O(n)的算法运行时间超过了1s,就应该考虑log(n)的解法了。
+如果n的规模已经足够让$O(n)$的算法运行时间超过了1s,就应该考虑log(n)的解法了。
# 从硬件配置看计算机的性能
@@ -65,7 +65,7 @@
测试硬件:2015年MacPro,CPU配置:2.7 GHz Dual-Core Intel Core i5
-实现三个函数,时间复杂度分别是 O(n) , O(n^2), O(nlogn),使用加法运算来统一测试。
+实现三个函数,时间复杂度分别是 $O(n)$ , $O(n^2)$, $O(n\log n)$,使用加法运算来统一测试。
```CPP
// O(n)
@@ -130,19 +130,19 @@ int main() {

-O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下O(n^2) 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。
+O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下$O(n^2)$ 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。

O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。
-在推测一下O(nlogn)的话, 1s可以处理的数据规模是什么呢?
+在推测一下$O(n\log n)$的话, 1s可以处理的数据规模是什么呢?
-理论上应该是比 O(n)少一个数量级,因为logn的复杂度 其实是很快,看一下实验数据。
+理论上应该是比 $O(n)$少一个数量级,因为$\log n$的复杂度 其实是很快,看一下实验数据。

-O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。
+$O(n\log n)$的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。
这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。
@@ -150,7 +150,7 @@ O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符

-至于O(logn) 和O(n^3) 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。
+至于 $O(\log n)$ 和 $O(n^3)$ 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。
# 完整测试代码
@@ -211,7 +211,7 @@ int main() {
# 总结
-本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看O(n)的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。
+本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看$O(n)$的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。
建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。
diff --git a/problems/前序/代码风格.md b/problems/前序/代码风格.md
index a71ac420..b48665e5 100644
--- a/problems/前序/代码风格.md
+++ b/problems/前序/代码风格.md
@@ -13,7 +13,7 @@
最近看了很多录友在[leetcode-master](https://mp.weixin.qq.com/s/wZRTrA9Rbvgq1yEkSw4vfQ)上提交的代码,发现很多录友的代码其实并不规范,这一点平时在交流群和知识星球里也能看出来。
-很多录友对代码规范应该不甚了解,代码看起来并不舒服。
+很多录友对代码规范应该了解得不多,代码看起来并不舒服。
所以呢,我给大家讲一讲代码规范,我主要以C++代码为例。
@@ -27,7 +27,7 @@
现在一些小公司,甚至大公司里的某些技术团队也不注重代码规范,赶进度撸出功能就完事,这种情况就要分两方面看:
-* 第一种情况:这个项目在业务上具有巨大潜力,需要抢占市场,只要先站住市场就能赚到钱,每年年终好几十万,那项目前期还关心啥代码风格,赶进度把功能撸出来,赚钱就完事了,例如12年的微信,15年的王者荣耀。这些项目都是后期在不断优化的。
+* 第一种情况:这个项目在业务上具有巨大潜力,需要抢占市场,只要先站住市场就能赚到钱,每年年终好几十万,那项目前期还关心啥代码风格,赶进度把功能撸出来,赚钱就完事了,例如12年的微信,15年的王者荣耀。这些项目都是后期再不断优化的。
* 第二种情况:这个项目没赚到钱,半死不活的,代码还没有设计也没有规范,这样对技术人员的伤害就非常大了。
@@ -39,7 +39,7 @@
这里我简单说一说规范问题。
-**权威的C++规范以Google为主**,我给大家下载了一份中文版本,在公众号「代码随想录」后台回复:googlec++编程规范,就可以领取。
+**权威的C++规范以Google为主**,我给大家下载了一份中文版本,在公众号「代码随想录」后台回复:googlec++编程规范,就可以领取。(涉及到微信后台的回复,没更改)
**具体的规范要以自己团队风格为主**,融入团队才是最重要的。
diff --git a/problems/前序/关于时间复杂度,你不知道的都在这里!.md b/problems/前序/关于时间复杂度,你不知道的都在这里!.md
index d4869b14..cfcbed1a 100644
--- a/problems/前序/关于时间复杂度,你不知道的都在这里!.md
+++ b/problems/前序/关于时间复杂度,你不知道的都在这里!.md
@@ -15,7 +15,7 @@
* 什么是大O
* 不同数据规模的差异
* 复杂表达式的化简
-* O(logn)中的log是以什么为底?
+* $O(\log n)$中的log是以什么为底?
* 举一个例子
@@ -29,21 +29,21 @@
那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认CPU的每个单元运行消耗的时间都是相同的。
-假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 O(f(n))。
+假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 $O(f(n)$)。
## 什么是大O
-这里的大O是指什么呢,说到时间复杂度,**大家都知道O(n),O(n^2),却说不清什么是大O**。
+这里的大O是指什么呢,说到时间复杂度,**大家都知道$O(n)$,$O(n^2)$,却说不清什么是大O**。
算法导论给出的解释:**大O用来表示上界的**,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。
-同样算法导论给出了例子:拿插入排序来说,插入排序的时间复杂度我们都说是O(n^2) 。
+同样算法导论给出了例子:拿插入排序来说,插入排序的时间复杂度我们都说是$O(n^2)$ 。
-输入数据的形式对程序运算时间是有很大影响的,在数据本来有序的情况下时间复杂度是O(n),但如果数据是逆序的话,插入排序的时间复杂度就是O(n^2),也就对于所有输入情况来说,最坏是O(n^2) 的时间复杂度,所以称插入排序的时间复杂度为O(n^2)。
+输入数据的形式对程序运算时间是有很大影响的,在数据本来有序的情况下时间复杂度是$O(n)$,但如果数据是逆序的话,插入排序的时间复杂度就是$O(n^2)$,也就对于所有输入情况来说,最坏是$O(n^2)$ 的时间复杂度,所以称插入排序的时间复杂度为$O(n^2)$。
-同样的同理再看一下快速排序,都知道快速排序是O(nlogn),但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的,**所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)**。
+同样的同理再看一下快速排序,都知道快速排序是$O(n\log n)$,但是当数据已经有序情况下,快速排序的时间复杂度是$O(n^2)$ 的,**所以严格从大O的定义来讲,快速排序的时间复杂度应该是$O(n^2)$**。
-**但是我们依然说快速排序是O(nlogn)的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界**。如图所示:
+**但是我们依然说快速排序是$O(n\log n)$的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界**。如图所示:

我们主要关心的还是一般情况下的数据形式。
@@ -57,11 +57,11 @@

-在决定使用哪些算法的时候,不是时间复杂越低的越好(因为简化后的时间复杂度忽略了常数项等等),要考虑数据规模,如果数据规模很小甚至可以用O(n^2)的算法比O(n)的更合适(在有常数项的时候)。
+在决定使用哪些算法的时候,不是时间复杂越低的越好(因为简化后的时间复杂度忽略了常数项等等),要考虑数据规模,如果数据规模很小甚至可以用$O(n^2)$的算法比$O(n)$的更合适(在有常数项的时候)。
-就像上图中 O(5n^2) 和 O(100n) 在n为20之前 很明显 O(5n^2)是更优的,所花费的时间也是最少的。
+就像上图中 $O(5n^2)$ 和 $O(100n)$ 在n为20之前 很明显 $O(5n^2)$是更优的,所花费的时间也是最少的。
-那为什么在计算时间复杂度的时候要忽略常数项系数呢,也就说O(100n) 就是O(n)的时间复杂度,O(5n^2) 就是O(n^2)的时间复杂度,而且要默认O(n) 优于O(n^2) 呢 ?
+那为什么在计算时间复杂度的时候要忽略常数项系数呢,也就说$O(100n)$ 就是$O(n)$的时间复杂度,$O(5n^2)$ 就是$O(n^2)$的时间复杂度,而且要默认$O(n)$ 优于$O(n^2)$ 呢 ?
这里就又涉及到大O的定义,**因为大O就是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度,这个数据量也就是常数项系数已经不起决定性作用的数据量**。
@@ -69,13 +69,13 @@
**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示**:
-O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n^2)平方阶 < O(n^3)(立方阶) < O(2^n) (指数阶)
+O(1)常数阶 < $O(\log n)$对数阶 < $O(n)$线性阶 < $O(n^2)$平方阶 < $O(n^3)$立方阶 < $O(2^n)$指数阶
但是也要注意大常数,如果这个常数非常大,例如10^7 ,10^9 ,那么常数就是不得不考虑的因素了。
## 复杂表达式的化简
-有时候我们去计算时间复杂度的时候发现不是一个简单的O(n) 或者O(n^2), 而是一个复杂的表达式,例如:
+有时候我们去计算时间复杂度的时候发现不是一个简单的$O(n)$ 或者$O(n^2)$, 而是一个复杂的表达式,例如:
```
O(2*n^2 + 10*n + 1000)
@@ -101,19 +101,19 @@ O(n^2 + n)
O(n^2)
```
-如果这一步理解有困难,那也可以做提取n的操作,变成O(n(n+1)) ,省略加法常数项后也就别变成了:
+如果这一步理解有困难,那也可以做提取n的操作,变成$O(n(n+1)$) ,省略加法常数项后也就别变成了:
```
O(n^2)
```
-所以最后我们说:这个算法的算法时间复杂度是O(n^2) 。
+所以最后我们说:这个算法的算法时间复杂度是$O(n^2)$ 。
-也可以用另一种简化的思路,其实当n大于40的时候, 这个复杂度会恒小于O(3 * n^2),
-O(2 * n^2 + 10 * n + 1000) < O(3 * n^2),所以说最后省略掉常数项系数最终时间复杂度也是O(n^2)。
+也可以用另一种简化的思路,其实当n大于40的时候, 这个复杂度会恒小于$O(3 × n^2)$,
+$O(2 × n^2 + 10 × n + 1000) < O(3 × n^2)$,所以说最后省略掉常数项系数最终时间复杂度也是$O(n^2)$。
-## O(logn)中的log是以什么为底?
+## $O(\log n)$中的log是以什么为底?
平时说这个算法的时间复杂度是logn的,那么一定是log 以2为底n的对数么?
@@ -136,21 +136,21 @@ O(2 * n^2 + 10 * n + 1000) < O(3 * n^2),所以说最后省略掉常数项系
通过这道面试题目,来分析一下时间复杂度。题目描述:找出n个字符串中相同的两个字符串(假设这里只有两个相同的字符串)。
-如果是暴力枚举的话,时间复杂度是多少呢,是O(n^2)么?
+如果是暴力枚举的话,时间复杂度是多少呢,是$O(n^2)$么?
-这里一些同学会忽略了字符串比较的时间消耗,这里并不像int 型数字做比较那么简单,除了n^2 次的遍历次数外,字符串比较依然要消耗m次操作(m也就是字母串的长度),所以时间复杂度是O(m * n * n)。
+这里一些同学会忽略了字符串比较的时间消耗,这里并不像int 型数字做比较那么简单,除了n^2 次的遍历次数外,字符串比较依然要消耗m次操作(m也就是字母串的长度),所以时间复杂度是$O(m × n × n)$。
接下来再想一下其他解题思路。
先排对n个字符串按字典序来排序,排序后n个字符串就是有序的,意味着两个相同的字符串就是挨在一起,然后在遍历一遍n个字符串,这样就找到两个相同的字符串了。
-那看看这种算法的时间复杂度,快速排序时间复杂度为O(nlogn),依然要考虑字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是O(m * n * logn) 。
+那看看这种算法的时间复杂度,快速排序时间复杂度为$O(n\log n)$,依然要考虑字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是$O(m × n × \log n)$ 。
-之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 O(m * n * logn + n * m)。
+之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 $O(m × n × \log n + n × m)$。
-我们对O(m * n * logn + n * m) 进行简化操作,把m * n提取出来变成 O(m * n * (logn + 1)),再省略常数项最后的时间复杂度是 O(m * n * logn)。
+我们对$O(m × n × \log n + n × m)$ 进行简化操作,把$m × n$提取出来变成 $O(m × n × (\log n + 1)$),再省略常数项最后的时间复杂度是 $O(m × n × \log n)$。
-最后很明显O(m * n * logn) 要优于O(m * n * n)!
+最后很明显$O(m × n × \log n)$ 要优于$O(m × n × n)$!
所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。
diff --git a/problems/前序/关于空间复杂度,可能有几个疑问?.md b/problems/前序/关于空间复杂度,可能有几个疑问?.md
index 510979c9..95ffe597 100644
--- a/problems/前序/关于空间复杂度,可能有几个疑问?.md
+++ b/problems/前序/关于空间复杂度,可能有几个疑问?.md
@@ -11,14 +11,14 @@
# 空间复杂度分析
* [关于时间复杂度,你不知道的都在这里!](https://programmercarl.com/前序/关于时间复杂度,你不知道的都在这里!.html)
-* [O(n)的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html)
+* [$O(n)$的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html)
* [通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)
那么一直还没有讲空间复杂度,所以打算陆续来补上,内容不难,大家可以读一遍文章就有整体的了解了。
什么是空间复杂度呢?
-是对一个算法在运行过程中占用内存空间大小的量度,记做S(n)=O(f(n)。
+是对一个算法在运行过程中占用内存空间大小的量度,记做$S(n)=O(f(n)$。
空间复杂度(Space Complexity)记作S(n) 依然使用大O来表示。利用程序的空间复杂度,可以对程序运行中需要多少内存有个预先估计。
@@ -40,7 +40,7 @@
同样在工程实践中,计算机的内存空间也不是无限的,需要工程师对软件运行时所使用的内存有一个大体评估,这都需要用到算法空间复杂度的分析。
-来看一下例子,什么时候的空间复杂度是O(1)呢,C++代码如下:
+来看一下例子,什么时候的空间复杂度是$O(1)$呢,C++代码如下:
```CPP
int j = 0;
@@ -49,11 +49,11 @@ for (int i = 0; i < n; i++) {
}
```
-第一段代码可以看出,随着n的变化,所需开辟的内存空间并不会随着n的变化而变化。即此算法空间复杂度为一个常量,所以表示为大 O(1)。
+第一段代码可以看出,随着n的变化,所需开辟的内存空间并不会随着n的变化而变化。即此算法空间复杂度为一个常量,所以表示为大$O(1)$。
-什么时候的空间复杂度是O(n)?
+什么时候的空间复杂度是$O(n)$?
-当消耗空间和输入参数n保持线性增长,这样的空间复杂度为O(n),来看一下这段C++代码
+当消耗空间和输入参数n保持线性增长,这样的空间复杂度为$O(n)$,来看一下这段C++代码
```CPP
int* a = new int(n);
for (int i = 0; i < n; i++) {
@@ -61,9 +61,9 @@ for (int i = 0; i < n; i++) {
}
```
-我们定义了一个数组出来,这个数组占用的大小为n,虽然有一个for循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,随着n的增大,开辟的内存大小呈线性增长,即 O(n)。
+我们定义了一个数组出来,这个数组占用的大小为n,虽然有一个for循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,随着n的增大,开辟的内存大小呈线性增长,即 $O(n)$。
-其他的 O(n^2), O(n^3) 我想大家应该都可以以此例举出来了,**那么思考一下 什么时候空间复杂度是 O(logn)呢?**
+其他的 $O(n^2)$, $O(n^3)$ 我想大家应该都可以以此例举出来了,**那么思考一下 什么时候空间复杂度是 $O(\log n)$呢?**
空间复杂度是logn的情况确实有些特殊,其实是在**递归的时候,会出现空间复杂度为logn的情况**。
diff --git a/problems/前序/力扣上的代码想在本地编译运行?.md b/problems/前序/力扣上的代码想在本地编译运行?.md
index 970af7ff..c4899a20 100644
--- a/problems/前序/力扣上的代码想在本地编译运行?.md
+++ b/problems/前序/力扣上的代码想在本地编译运行?.md
@@ -13,11 +13,11 @@
然后录友就问了:如何打日志呢?
-其实在力扣上打日志也挺方便的,我一般调试就是直接在力扣上打日志,偶尔需要把代码粘到本例来运行添加日志debug一下。
+其实在力扣上打日志也挺方便的,我一般调试就是直接在力扣上打日志,偶尔需要把代码粘到本地来运行添加日志debug一下。
在力扣上直接打日志,这个就不用讲,C++的话想打啥直接cout啥就可以了。
-我来说一说力扣代码如何在本题运行。
+我来说一说力扣代码如何在本地运行。
毕竟我们天天用力扣刷题,也应该知道力扣上的代码如何在本地编译运行。
diff --git a/problems/前序/编程素养部分的吹毛求疵.md b/problems/前序/编程素养部分的吹毛求疵.md
new file mode 100644
index 00000000..3f18f9d1
--- /dev/null
+++ b/problems/前序/编程素养部分的吹毛求疵.md
@@ -0,0 +1,31 @@
+## 代码风格
+
+- `不甚了解`是不能更了解的意思,这个地方应该使用存疑。
+- `后期在不断优化`,'在'应为'再'。
+- `googlec++编程规范`,Google拼写错误
+
+## 代码本地编译
+
+- `粘到本例来运行`存疑,应为本地
+- `本题运行`存疑,应为本地
+
+## ACM二叉树
+
+- 左孩子和右孩子的下标不太好理解。我给出证明过程:
+
+ 如果父节点在第$k$层,第$m,m \in [0,2^k]$个节点,则其左孩子所在的位置必然为$k+1$层,第$2*(m-1)+1$个节点。
+
+ - 计算父节点在数组中的索引:
+ $$
+ index_{father}=(\sum_{i=0}^{i=k-1}2^i)+m-1=2^k-1+m-1
+ $$
+
+ - 计算左子节点在数组的索引:
+ $$
+ index_{left}=(\sum_{i=0}^{i=k}2^i)+2*m-1-1=2^{k+1}+2m-3
+ $$
+
+ - 故左孩子的下表为$index_{left}=index_{father}\times2+1$,同理可得到右子孩子的索引关系。也可以直接在左子孩子的基础上`+1`。
+
+
+
diff --git a/problems/前序/递归算法的时间与空间复杂度分析.md b/problems/前序/递归算法的时间与空间复杂度分析.md
index 63376d11..8025e8e6 100644
--- a/problems/前序/递归算法的时间与空间复杂度分析.md
+++ b/problems/前序/递归算法的时间与空间复杂度分析.md
@@ -34,7 +34,7 @@ int fibonacci(int i) {
在讲解递归时间复杂度的时候,我们提到了递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归的时间复杂度**。
-可以看出上面的代码每次递归都是O(1)的操作。再来看递归了多少次,这里将i为5作为输入的递归过程 抽象成一颗递归树,如图:
+可以看出上面的代码每次递归都是$O(1)$的操作。再来看递归了多少次,这里将i为5作为输入的递归过程 抽象成一颗递归树,如图:

@@ -44,7 +44,7 @@ int fibonacci(int i) {
我们之前也有说到,一棵深度(按根节点深度为1)为k的二叉树最多可以有 2^k - 1 个节点。
-所以该递归算法的时间复杂度为 O(2^n) ,这个复杂度是非常大的,随着n的增大,耗时是指数上升的。
+所以该递归算法的时间复杂度为$O(2^n)$,这个复杂度是非常大的,随着n的增大,耗时是指数上升的。
来做一个实验,大家可以有一个直观的感受。
@@ -93,7 +93,7 @@ int main()
* n = 40,耗时:837 ms
* n = 50,耗时:110306 ms
-可以看出,O(2^n)这种指数级别的复杂度是非常大的。
+可以看出,$O(2^n)$这种指数级别的复杂度是非常大的。
所以这种求斐波那契数的算法看似简洁,其实时间复杂度非常高,一般不推荐这样来实现斐波那契。
@@ -127,14 +127,14 @@ int fibonacci(int first, int second, int n) {
这里相当于用first和second来记录当前相加的两个数值,此时就不用两次递归了。
-因为每次递归的时候n减1,即只是递归了n次,所以时间复杂度是 O(n)。
+因为每次递归的时候n减1,即只是递归了n次,所以时间复杂度是 $O(n)$。
-同理递归的深度依然是n,每次递归所需的空间也是常数,所以空间复杂度依然是O(n)。
+同理递归的深度依然是n,每次递归所需的空间也是常数,所以空间复杂度依然是$O(n)$。
代码(版本二)的复杂度如下:
-* 时间复杂度: O(n)
-* 空间复杂度: O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
此时再来测一下耗时情况验证一下:
@@ -198,7 +198,7 @@ int main()
因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。
-此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是O(1)。
+此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是$O(1)$。
在看递归的深度是多少呢?如图所示:
@@ -206,7 +206,7 @@ int main()
递归第n个斐波那契数的话,递归调用栈的深度就是n。
-那么每次递归的空间复杂度是O(1), 调用栈深度为n,所以这段递归代码的空间复杂度就是O(n)。
+那么每次递归的空间复杂度是$O(1)$, 调用栈深度为n,所以这段递归代码的空间复杂度就是$O(n)$。
```CPP
int fibonacci(int i) {
@@ -241,24 +241,24 @@ int binary_search( int arr[], int l, int r, int x) {
}
```
-都知道二分查找的时间复杂度是O(logn),那么递归二分查找的空间复杂度是多少呢?
+都知道二分查找的时间复杂度是$O(\log n)$,那么递归二分查找的空间复杂度是多少呢?
我们依然看 **每次递归的空间复杂度和递归的深度**
每次递归的空间复杂度可以看出主要就是参数里传入的这个arr数组,但需要注意的是在C/C++中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入的数组首元素地址。
-**也就是说每一层递归都是公用一块数组地址空间的**,所以 每次递归的空间复杂度是常数即:O(1)。
+**也就是说每一层递归都是公用一块数组地址空间的**,所以 每次递归的空间复杂度是常数即:$O(1)$。
-再来看递归的深度,二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * logn = O(logn)。
+再来看递归的深度,二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 $1 * logn = O(logn)$。
-大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)。
+大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是$O(n\log n)$。
## 总结
本章我们详细分析了递归实现的求斐波那契和二分法的空间复杂度,同时也对时间复杂度做了分析。
-特别是两种递归实现的求斐波那契数列,其时间复杂度截然不容,我们还做了实验,验证了时间复杂度为O(2^n)是非常耗时的。
+特别是两种递归实现的求斐波那契数列,其时间复杂度截然不容,我们还做了实验,验证了时间复杂度为$O(2^n)$是非常耗时的。
通过本篇大家应该对递归算法的时间复杂度和空间复杂度有更加深刻的理解了。
diff --git a/problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md b/problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md
index b3aef43c..dd998c53 100644
--- a/problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md
+++ b/problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md
@@ -15,13 +15,13 @@
相信很多同学对递归算法的时间复杂度都很模糊,那么这篇来给大家通透的讲一讲。
-**同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码**。
+**同一道题目,同样使用递归算法,有的同学会写出了$O(n)$的代码,有的同学就写出了$O(\log n)$的代码**。
这是为什么呢?
如果对递归的时间复杂度理解的不够深入的话,就会这样!
-那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了O(n)的代码。
+那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了$O(n)$的代码。
面试题:求x的n次方
@@ -36,7 +36,7 @@ int function1(int x, int n) {
return result;
}
```
-时间复杂度为O(n),此时面试官会说,有没有效率更好的算法呢。
+时间复杂度为$O(n)$,此时面试官会说,有没有效率更好的算法呢。
**如果此时没有思路,不要说:我不会,我不知道了等等**。
@@ -54,11 +54,11 @@ int function2(int x, int n) {
```
面试官问:“那么这个代码的时间复杂度是多少?”。
-一些同学可能一看到递归就想到了O(logn),其实并不是这样,递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归中的操作次数**。
+一些同学可能一看到递归就想到了$O(\log n)$,其实并不是这样,递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归中的操作次数**。
那再来看代码,这里递归了几次呢?
-每次n-1,递归了n次时间复杂度是O(n),每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项O(1),所以这份代码的时间复杂度是 n * 1 = O(n)。
+每次n-1,递归了n次时间复杂度是$O(n)$,每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项$O(1)$,所以这份代码的时间复杂度是 $n × 1 = O(n)$。
这个时间复杂度就没有达到面试官的预期。于是又写出了如下的递归算法的代码:
@@ -91,11 +91,11 @@ int function3(int x, int n) {

-**时间复杂度忽略掉常数项`-1`之后,这个递归算法的时间复杂度依然是O(n)**。对,你没看错,依然是O(n)的时间复杂度!
+**时间复杂度忽略掉常数项`-1`之后,这个递归算法的时间复杂度依然是$O(n)$**。对,你没看错,依然是$O(n)$的时间复杂度!
-此时面试官就会说:“这个递归的算法依然还是O(n)啊”, 很明显没有达到面试官的预期。
+此时面试官就会说:“这个递归的算法依然还是$O(n)$啊”, 很明显没有达到面试官的预期。
-那么O(logn)的递归算法应该怎么写呢?
+那么$O(\log n)$的递归算法应该怎么写呢?
想一想刚刚给出的那份递归算法的代码,是不是有哪里比较冗余呢,其实有重复计算的部分。
@@ -118,7 +118,7 @@ int function4(int x, int n) {
依然还是看他递归了多少次,可以看到这里仅仅有一个递归调用,且每次都是n/2 ,所以这里我们一共调用了log以2为底n的对数次。
-**每次递归了做都是一次乘法操作,这也是一个常数项的操作,那么这个递归算法的时间复杂度才是真正的O(logn)**。
+**每次递归了做都是一次乘法操作,这也是一个常数项的操作,那么这个递归算法的时间复杂度才是真正的$O(\log n)$**。
此时大家最后写出了这样的代码并且将时间复杂度分析的非常清晰,相信面试官是比较满意的。
@@ -126,11 +126,11 @@ int function4(int x, int n) {
对于递归的时间复杂度,毕竟初学者有时候会迷糊,刷过很多题的老手依然迷糊。
-**本篇我用一道非常简单的面试题目:求x的n次方,来逐步分析递归算法的时间复杂度,注意不要一看到递归就想到了O(logn)!**
+**本篇我用一道非常简单的面试题目:求x的n次方,来逐步分析递归算法的时间复杂度,注意不要一看到递归就想到了$O(\log n)$!**
-同样使用递归,有的同学可以写出O(logn)的代码,有的同学还可以写出O(n)的代码。
+同样使用递归,有的同学可以写出$O(\log n)$的代码,有的同学还可以写出$O(n)$的代码。
-对于function3 这样的递归实现,很容易让人感觉这是O(logn)的时间复杂度,其实这是O(n)的算法!
+对于function3 这样的递归实现,很容易让人感觉这是$O(\log n)$的时间复杂度,其实这是$O(n)$的算法!
```CPP
int function3(int x, int n) {
diff --git a/problems/剑指Offer05.替换空格.md b/problems/剑指Offer05.替换空格.md
index 115ed1e7..eed31557 100644
--- a/problems/剑指Offer05.替换空格.md
+++ b/problems/剑指Offer05.替换空格.md
@@ -29,7 +29,7 @@ i指向新长度的末尾,j指向旧长度的末尾。
有同学问了,为什么要从后向前填充,从前向后填充不行么?
-从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
+从前向后填充就是$O(n^2)$的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
**其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。**
@@ -72,10 +72,10 @@ public:
return s;
}
};
-
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
此时算上本题,我们已经做了七道双指针相关的题目了分别是:
diff --git a/problems/剑指Offer58-II.左旋转字符串.md b/problems/剑指Offer58-II.左旋转字符串.md
index cd09b690..a7ffb931 100644
--- a/problems/剑指Offer58-II.左旋转字符串.md
+++ b/problems/剑指Offer58-II.左旋转字符串.md
@@ -86,7 +86,7 @@ public:
# 题外话
一些同学热衷于使用substr,来做这道题。
-其实使用substr 和 反转 时间复杂度是一样的 ,都是O(n),但是使用substr申请了额外空间,所以空间复杂度是O(n),而反转方法的空间复杂度是O(1)。
+其实使用substr 和 反转 时间复杂度是一样的 ,都是$O(n)$,但是使用substr申请了额外空间,所以空间复杂度是$O(n)$,而反转方法的空间复杂度是$O(1)$。
**如果想让这套题目有意义,就不要申请额外空间。**
diff --git a/problems/动态规划-股票问题总结篇.md b/problems/动态规划-股票问题总结篇.md
index ff16db36..e630eb44 100644
--- a/problems/动态规划-股票问题总结篇.md
+++ b/problems/动态规划-股票问题总结篇.md
@@ -72,8 +72,8 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
使用滚动数组,代码如下:
@@ -95,8 +95,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
## 买卖股票的最佳时机II
@@ -120,8 +120,9 @@ public:
}
};
```
-* 时间复杂度O(n)
-* 空间复杂度O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
【动态规划】
@@ -161,8 +162,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 买卖股票的最佳时机III
@@ -223,8 +224,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n * 5)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n × 5)$
当然,大家可以看到力扣官方题解里的一种优化空间写法,我这里给出对应的C++版本:
@@ -248,8 +249,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
**这种写法看上去简单,其实思路很绕,不建议大家这么写,这么思考,很容易把自己绕进去!** 对于本题,把版本一的写法研究明白,足以!
@@ -401,8 +402,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 买卖股票的最佳时机含手续费
@@ -453,8 +454,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
## 总结
diff --git a/problems/双指针总结.md b/problems/双指针总结.md
index e67015e8..68be65ef 100644
--- a/problems/双指针总结.md
+++ b/problems/双指针总结.md
@@ -22,7 +22,7 @@ for (int i = 0; i < array.size(); i++) {
}
```
-这个代码看上去好像是O(n)的时间复杂度,其实是O(n^2)的时间复杂度,因为erase操作也是O(n)的操作。
+这个代码看上去好像是$O(n)$的时间复杂度,其实是$O(n^2)$的时间复杂度,因为erase操作也是$O(n)$的操作。
所以此时使用双指针法才展现出效率的优势:**通过两个指针在一个for循环下完成两个for循环的工作。**
@@ -30,7 +30,7 @@ for (int i = 0; i < array.size(); i++) {
在[字符串:这道题目,使用库函数一行代码搞定](https://programmercarl.com/0344.反转字符串.html)中讲解了反转字符串,注意这里强调要原地反转,要不然就失去了题目的意义。
-使用双指针法,**定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。**,时间复杂度是O(n)。
+使用双指针法,**定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。**,时间复杂度是$O(n)$。
在[替换空格](https://programmercarl.com/剑指Offer05.替换空格.html) 中介绍使用双指针填充字符串的方法,如果想把这道题目做到极致,就不要只用额外的辅助空间了!
@@ -38,13 +38,13 @@ for (int i = 0; i < array.size(); i++) {
有同学问了,为什么要从后向前填充,从前向后填充不行么?
-从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
+从前向后填充就是$O(n^2)$的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。
**其实很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。**
-那么在[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中,我们使用双指针法,用O(n)的时间复杂度完成字符串删除类的操作,因为题目要产出冗余空格。
+那么在[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中,我们使用双指针法,用$O(n)$的时间复杂度完成字符串删除类的操作,因为题目要产出冗余空格。
-**在删除冗余空格的过程中,如果不注意代码效率,很容易写成了O(n^2)的时间复杂度。其实使用双指针法O(n)就可以搞定。**
+**在删除冗余空格的过程中,如果不注意代码效率,很容易写成了$O(n^2)$的时间复杂度。其实使用双指针法$O(n)$就可以搞定。**
**主要还是大家用erase用的比较随意,一定要注意for循环下用erase的情况,一般可以用双指针写效率更高!**
@@ -74,22 +74,22 @@ for (int i = 0; i < array.size(); i++) {
去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
-时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
+时间复杂度可以做到$O(n^2)$,但还是比较费时的,因为不好做剪枝操作。
所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,**通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。**
-只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。
+只用双指针法时间复杂度为$O(n^2)$,但比哈希法的$O(n^2)$效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。
在[双指针法:一样的道理,能解决四数之和](https://programmercarl.com/0018.四数之和.html)中,讲到了四数之和,其实思路是一样的,**在三数之和的基础上再套一层for循环,依然是使用双指针法。**
-对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。
+对于三数之和使用双指针法就是将原本暴力$O(n^3)$的解法,降为$O(n^2)$的解法,四数之和的双指针解法就是将原本暴力$O(n^4)$的解法,降为$O(n^3)$的解法。
同样的道理,五数之和,n数之和都是在这个基础上累加。
# 总结
-本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为O(n)。
+本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将$O(n^2)$的时间复杂度,降为$O(n)$。
建议大家可以把文中涉及到的题目在好好做一做,琢磨琢磨,基本对双指针法就不在话下了。
diff --git a/problems/周总结/20200927二叉树周末总结.md b/problems/周总结/20200927二叉树周末总结.md
index ff8f67d4..60f02205 100644
--- a/problems/周总结/20200927二叉树周末总结.md
+++ b/problems/周总结/20200927二叉树周末总结.md
@@ -44,7 +44,7 @@ a->right = NULL;
在介绍前中后序遍历的时候,有递归和迭代(非递归),还有一种牛逼的遍历方式:morris遍历。
-morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为O(1),感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。
+morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为$O(1)$,感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。
## 周二
diff --git a/problems/周总结/20201010二叉树周末总结.md b/problems/周总结/20201010二叉树周末总结.md
index 913ad963..215452bd 100644
--- a/problems/周总结/20201010二叉树周末总结.md
+++ b/problems/周总结/20201010二叉树周末总结.md
@@ -67,7 +67,7 @@
知道了如何构造二叉树,那么使用一个套路就可以解决文章[二叉树:构造一棵最大的二叉树](https://programmercarl.com/0654.最大二叉树.html)中的问题。
-**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下表索引直接在原数组上操作,这样可以节约时间和空间上的开销。**
+**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。**
文章中我还给出了递归函数什么时候加if,什么时候不加if,其实就是控制空节点(空指针)是否进入递归,是不同的代码实现方式,都是可以的。
diff --git a/problems/周总结/20201112回溯周末总结.md b/problems/周总结/20201112回溯周末总结.md
index 3d019cf2..f363b9f7 100644
--- a/problems/周总结/20201112回溯周末总结.md
+++ b/problems/周总结/20201112回溯周末总结.md
@@ -72,16 +72,16 @@
**所以这块就说一说我个人理解,对内容持开放态度,集思广益,欢迎大家来讨论!**
子集问题分析:
-* 时间复杂度:O(n * 2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n),构造每一组子集都需要填进数组,又有需要O(n),最终时间复杂度:O(n * 2^n)
-* 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
+* 时间复杂度:$O(n × 2^n)$,因为每一个元素的状态无外乎取与不取,所以时间复杂度为$O(2^n)$,构造每一组子集都需要填进数组,又有需要$O(n)$,最终时间复杂度:$O(n × 2^n)$
+* 空间复杂度:$O(n)$,递归深度为n,所以系统栈所用空间为$O(n)$,每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为$O(n)$
排列问题分析:
-* 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。
-* 空间复杂度:O(n),和子集问题同理。
+* 时间复杂度:$O(n!)$,这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。
+* 空间复杂度:$O(n)$,和子集问题同理。
组合问题分析:
-* 时间复杂度:O(n * 2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
-* 空间复杂度:O(n),和子集问题同理。
+* 时间复杂度:$O(n × 2^n)$,组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
+* 空间复杂度:$O(n)$,和子集问题同理。
**一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!**
diff --git a/problems/周总结/20201126贪心周末总结.md b/problems/周总结/20201126贪心周末总结.md
index 5671cdbd..02fccc25 100644
--- a/problems/周总结/20201126贪心周末总结.md
+++ b/problems/周总结/20201126贪心周末总结.md
@@ -41,7 +41,7 @@
一些录友不清楚[贪心算法:分发饼干](https://programmercarl.com/0455.分发饼干.html)中时间复杂度是怎么来的?
-就是快排O(nlogn),遍历O(n),加一起就是还是O(nlogn)。
+就是快排$O(n\log n)$,遍历$O(n)$,加一起就是还是$O(n\log n)$。
## 周三
diff --git a/problems/周总结/20201203贪心周末总结.md b/problems/周总结/20201203贪心周末总结.md
index 23b0b7cf..10e2d1bb 100644
--- a/problems/周总结/20201203贪心周末总结.md
+++ b/problems/周总结/20201203贪心周末总结.md
@@ -79,7 +79,7 @@
[贪心算法:K次取反后最大化的数组和](https://programmercarl.com/1005.K次取反后最大化的数组和.html)中的代码,最后while处理K的时候,其实直接判断奇偶数就可以了,文中给出的方式太粗暴了,哈哈,Carl大意了。
-例外一位录友留言给出一个很好的建议,因为文中是使用快排,仔细看题,**题目中限定了数据范围是正负一百,所以可以使用桶排序**,这样时间复杂度就可以优化为O(n)了。但可能代码要复杂一些了。
+例外一位录友留言给出一个很好的建议,因为文中是使用快排,仔细看题,**题目中限定了数据范围是正负一百,所以可以使用桶排序**,这样时间复杂度就可以优化为$O(n)$了。但可能代码要复杂一些了。
## 总结
diff --git a/problems/周总结/20201210复杂度分析周末总结.md b/problems/周总结/20201210复杂度分析周末总结.md
index 02bfbd83..1b404bf0 100644
--- a/problems/周总结/20201210复杂度分析周末总结.md
+++ b/problems/周总结/20201210复杂度分析周末总结.md
@@ -53,11 +53,11 @@
文中涉及如下问题:
-* 究竟什么是大O?大O表示什么意思?严格按照大O的定义来说,快排应该是O(n^2)的算法!
-* O(n^2)的算法为什么有时候比O(n)的算法更优?
+* 究竟什么是大O?大O表示什么意思?严格按照大O的定义来说,快排应该是$O(n^2)$的算法!
+* $O(n^2)$的算法为什么有时候比$O(n)$的算法更优?
* 什么时间复杂度为什么可以忽略常数项?
* 如何简化复杂的时间复杂度表达式,原理是什么?
-* O(logn)中的log究竟是以谁为底?
+* $O(\log n)$中的log究竟是以谁为底?
这些问题大家可能懵懵懂懂的了解一些,但一细问又答不上来。
@@ -70,9 +70,9 @@
# 周三
-在[O(n)的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html)中介绍了大家在leetcode上提交代码经常遇到的一个问题-超时!
+在[$O(n)$的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html)中介绍了大家在leetcode上提交代码经常遇到的一个问题-超时!
-估计很多录友知道算法超时了,但没有注意过 O(n)的算法,如果1s内出结果,这个n究竟是多大?
+估计很多录友知道算法超时了,但没有注意过 $O(n)$的算法,如果1s内出结果,这个n究竟是多大?
文中从计算机硬件出发,分析计算机的计算性能,然后亲自做实验,整理出数据如下:
@@ -95,7 +95,7 @@
文中给出了四个版本的代码实现,并逐一分析了其时间复杂度。
-此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码。
+此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了$O(n)$的代码,有的同学就写出了$O(\log n)$的代码。
其本质是要对递归的时间复杂度有清晰的认识,才能运用递归来有效的解决问题!
diff --git a/problems/周总结/20201217贪心周末总结.md b/problems/周总结/20201217贪心周末总结.md
index 4d12f92a..e9d22d6e 100644
--- a/problems/周总结/20201217贪心周末总结.md
+++ b/problems/周总结/20201217贪心周末总结.md
@@ -8,7 +8,7 @@
在[贪心算法:加油站](https://programmercarl.com/0134.加油站.html)中给出每一个加油站的汽油和开到这个加油站的消耗,问汽车能不能开一圈。
-这道题目咋眼一看,感觉是一道模拟题,模拟一下汽车从每一个节点出发看看能不能开一圈,时间复杂度是O(n^2)。
+这道题目咋眼一看,感觉是一道模拟题,模拟一下汽车从每一个节点出发看看能不能开一圈,时间复杂度是$O(n^2)$。
即使用模拟这种情况,也挺考察代码技巧的。
diff --git a/problems/周总结/20210225动规周末总结.md b/problems/周总结/20210225动规周末总结.md
index ae8b4800..4ea92266 100644
--- a/problems/周总结/20210225动规周末总结.md
+++ b/problems/周总结/20210225动规周末总结.md
@@ -211,8 +211,8 @@ public:
};
```
-* 时间复杂度:O(n^2)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n^2)$
+* 空间复杂度:$O(1)$
贪心解法代码如下:
@@ -232,8 +232,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
动规解法,版本一,代码如下:
@@ -254,8 +255,9 @@ public:
}
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(n)
+
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(n)$
从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。
@@ -280,8 +282,8 @@ public:
};
```
-* 时间复杂度:O(n)
-* 空间复杂度:O(1)
+* 时间复杂度:$O(n)$
+* 空间复杂度:$O(1)$
建议先写出版本一,然后在版本一的基础上优化成版本二,而不是直接就写出版本二。
diff --git a/problems/周总结/二叉树阶段总结系列一.md b/problems/周总结/二叉树阶段总结系列一.md
index 595f367d..f65f5497 100644
--- a/problems/周总结/二叉树阶段总结系列一.md
+++ b/problems/周总结/二叉树阶段总结系列一.md
@@ -49,7 +49,7 @@ a->right = NULL;
在介绍前中后序遍历的时候,有递归和迭代(非递归),还有一种牛逼的遍历方式:morris遍历。
-morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为O(1),感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。
+morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为$O(1)$,感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。
## [二叉树的递归遍历](https://programmercarl.com/二叉树的递归遍历.html)
diff --git a/problems/哈希表总结.md b/problems/哈希表总结.md
index 885dc59a..80534b85 100644
--- a/problems/哈希表总结.md
+++ b/problems/哈希表总结.md
@@ -84,7 +84,7 @@ std::set和std::multiset底层实现都是红黑树,std::unordered_set的底
* 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
* set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
-map是一种``的结构,本题可以用key保存数值,用value在保存数值所在的下表。所以使用map最为合适。
+map是一种``的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。
C++提供如下三种map::(详情请看[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html))
diff --git a/problems/哈希表理论基础.md b/problems/哈希表理论基础.md
index 445bfed6..64642954 100644
--- a/problems/哈希表理论基础.md
+++ b/problems/哈希表理论基础.md
@@ -14,7 +14,7 @@
这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。
-哈希表中关键码就是数组的索引下表,然后通过下表直接访问数组中的元素,如下图所示:
+哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:

@@ -22,7 +22,7 @@
例如要查询一个名字是否在这所学校里。
-要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1) 就可以做到。
+要枚举的话时间复杂度是$O(n)$,但如果使用哈希表的话, 只需要$O(1)$就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
@@ -42,13 +42,13 @@
此时问题又来了,哈希表我们刚刚说过,就是一个数组。
-如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下表的位置。
+如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来**哈希碰撞**登场
### 哈希碰撞
-如图所示,小李和小王都映射到了索引下表 1的位置,**这一现象叫做哈希碰撞**。
+如图所示,小李和小王都映射到了索引下标 1 的位置,**这一现象叫做哈希碰撞**。

@@ -88,17 +88,17 @@
|集合 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率|
|---|---| --- |---| --- | --- | ---|
-|std::set |红黑树 |有序 |否 |否 | O(logn)|O(logn) |
-|std::multiset | 红黑树|有序 |是 | 否| O(logn) |O(logn) |
-|std::unordered_set |哈希表 |无序 |否 |否 |O(1) | O(1)|
+|std::set |红黑树 |有序 |否 |否 | $O(\log n)$|$O(\log n)$ |
+|std::multiset | 红黑树|有序 |是 | 否| $O(\log n)$ |$O(\log n)$ |
+|std::unordered_set |哈希表 |无序 |否 |否 |$O(1)$ | $O(1)$|
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
|映射 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率|
|---|---| --- |---| --- | --- | ---|
-|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | O(logn)|O(logn) |
-|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|O(logn) |O(logn) |
-|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |O(1) | O(1)|
+|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | $O(\log n)$|$O(\log n)$ |
+|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|$O(\log n)$ |$O(\log n)$ |
+|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |$O(1)$ | $O(1)$|
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
diff --git a/problems/回溯总结.md b/problems/回溯总结.md
index 7f8a312f..424d6947 100644
--- a/problems/回溯总结.md
+++ b/problems/回溯总结.md
@@ -302,11 +302,11 @@ if (startIndex >= nums.size()) { // 终止条件可以不加
**而使用used数组在时间复杂度上几乎没有额外负担!**
-**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。
+**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)中分析过,组合,子集,排列问题的空间复杂度都是$O(n)$,但如果使用set去重,空间复杂度就变成了$O(n^2)$,因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。
-那有同学可能疑惑 用used数组也是占用O(n)的空间啊?
+那有同学可能疑惑 用used数组也是占用$O(n)$的空间啊?
-used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。
+used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是$O(n + n)$,最终空间复杂度还是$O(n)$。
# 重新安排行程(图论额外拓展)
@@ -380,24 +380,24 @@ used数组可是全局变量,每层与每层之间公用一个used数组,所
以下在计算空间复杂度的时候我都把系统栈(不是数据结构里的栈)所占空间算进去。
子集问题分析:
-* 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
-* 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
+* 时间复杂度:$O(2^n)$,因为每一个元素的状态无外乎取与不取,所以时间复杂度为$O(2^n)$
+* 空间复杂度:$O(n)$,递归深度为n,所以系统栈所用空间为$O(n)$,每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为$O(n)$
排列问题分析:
-* 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。
-* 空间复杂度:O(n),和子集问题同理。
+* 时间复杂度:$O(n!)$,这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。
+* 空间复杂度:$O(n)$,和子集问题同理。
组合问题分析:
-* 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
-* 空间复杂度:O(n),和子集问题同理。
+* 时间复杂度:$O(2^n)$,组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
+* 空间复杂度:$O(n)$,和子集问题同理。
N皇后问题分析:
-* 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * .... * 1。
-* 空间复杂度:O(n),和子集问题同理。
+* 时间复杂度:$O(n!)$ ,其实如果看树形图的话,直觉上是$O(n^n)$,但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * .... * 1。
+* 空间复杂度:$O(n)$,和子集问题同理。
解数独问题分析:
-* 时间复杂度:O(9^m) , m是'.'的数目。
-* 空间复杂度:O(n^2),递归的深度是n^2
+* 时间复杂度:$O(9^m)$ , m是'.'的数目。
+* 空间复杂度:$O(n^2)$,递归的深度是n^2
**一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!**
diff --git a/problems/回溯算法去重问题的另一种写法.md b/problems/回溯算法去重问题的另一种写法.md
index 99fff630..d79f985a 100644
--- a/problems/回溯算法去重问题的另一种写法.md
+++ b/problems/回溯算法去重问题的另一种写法.md
@@ -226,11 +226,11 @@ public:
**而使用used数组在时间复杂度上几乎没有额外负担!**
-**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。
+**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)中分析过,组合,子集,排列问题的空间复杂度都是$O(n)$,但如果使用set去重,空间复杂度就变成了$O(n^2)$,因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。
-那有同学可能疑惑 用used数组也是占用O(n)的空间啊?
+那有同学可能疑惑 用used数组也是占用$O(n)$的空间啊?
-used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。
+used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是$O(n + n)$,最终空间复杂度还是$O(n)$。
## 总结
diff --git a/problems/字符串总结.md b/problems/字符串总结.md
index 6c336158..46df61fe 100644
--- a/problems/字符串总结.md
+++ b/problems/字符串总结.md
@@ -57,15 +57,15 @@ for (int i = 0; i < a.size(); i++) {
在[344.反转字符串](https://programmercarl.com/0344.反转字符串.html) ,我们使用双指针法实现了反转字符串的操作,**双指针法在数组,链表和字符串中很常用。**
-接着在[字符串:替换空格](https://programmercarl.com/剑指Offer05.替换空格.html),同样还是使用双指针法在时间复杂度O(n)的情况下完成替换空格。
+接着在[字符串:替换空格](https://programmercarl.com/剑指Offer05.替换空格.html),同样还是使用双指针法在时间复杂度$O(n)$的情况下完成替换空格。
**其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。**
那么针对数组删除操作的问题,其实在[27. 移除元素](https://programmercarl.com/0027.移除元素.html)中就已经提到了使用双指针法进行移除操作。
-同样的道理在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中我们使用O(n)的时间复杂度,完成了删除冗余空格。
+同样的道理在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中我们使用$O(n)$的时间复杂度,完成了删除冗余空格。
-一些同学会使用for循环里调用库函数erase来移除元素,这其实是O(n^2)的操作,因为erase就是O(n)的操作,所以这也是典型的不知道库函数的时间复杂度,上来就用的案例了。
+一些同学会使用for循环里调用库函数erase来移除元素,这其实是$O(n^2)$的操作,因为erase就是$O(n)$的操作,所以这也是典型的不知道库函数的时间复杂度,上来就用的案例了。
# 反转系列
@@ -93,7 +93,7 @@ KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之
KMP的精髓所在就是前缀表,在[KMP精讲](https://programmercarl.com/0028.实现strStr.html)中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。
-前缀表:起始位置到下表i之前(包括i)的子串中,有多大长度的相同前缀后缀。
+前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。
那么使用KMP可以解决两类经典问题:
diff --git a/problems/数组总结篇.md b/problems/数组总结篇.md
index 8d52db26..39f9aa5e 100644
--- a/problems/数组总结篇.md
+++ b/problems/数组总结篇.md
@@ -67,8 +67,8 @@
可以使用暴力解法,通过这道题目,如果追求更优的算法,建议试一试用二分法,来解决这道题目
-暴力解法时间复杂度:O(n)
-二分法时间复杂度:O(logn)
+* 暴力解法时间复杂度:$O(n)$
+* 二分法时间复杂度:$O(\log n)$
在这道题目中我们讲到了**循环不变量原则**,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
@@ -81,8 +81,8 @@
双指针法(快慢指针法):**通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。**
-暴力解法时间复杂度:O(n^2)
-双指针时间复杂度:O(n)
+* 暴力解法时间复杂度:$O(n^2)$
+* 双指针时间复杂度:$O(n)$
这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点:
@@ -97,12 +97,12 @@
本题介绍了数组操作中的另一个重要思想:滑动窗口。
-暴力解法时间复杂度:O(n^2)
-滑动窗口时间复杂度:O(n)
+* 暴力解法时间复杂度:$O(n^2)$
+* 滑动窗口时间复杂度:$O(n)$
本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
-**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。**
+**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将$O(n^2)$的暴力解法降为$O(n)$。**
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。
@@ -129,19 +129,5 @@
最后,大家周末愉快!
-## 其他语言版本
-
-
-Java:
-
-
-Python:
-
-
-Go:
-
-
-
-
-----------------------
diff --git a/problems/栈与队列总结.md b/problems/栈与队列总结.md
index 2faa27c4..485d0cf6 100644
--- a/problems/栈与队列总结.md
+++ b/problems/栈与队列总结.md
@@ -141,7 +141,7 @@ cd a/b/c/../../
本题就要**使用优先级队列来对部分频率进行排序。** 注意这里是对部分数据进行排序而不需要对所有数据排序!
-所以排序的过程的时间复杂度是O(logk),整个算法的时间复杂度是O(nlogk)。
+所以排序的过程的时间复杂度是$O(\log k)$,整个算法的时间复杂度是$O(n\log k)$。
# 总结
diff --git a/problems/根据身高重建队列(vector原理讲解).md b/problems/根据身高重建队列(vector原理讲解).md
index b2a9cf3a..09fe6476 100644
--- a/problems/根据身高重建队列(vector原理讲解).md
+++ b/problems/根据身高重建队列(vector原理讲解).md
@@ -33,7 +33,7 @@ public:
耗时如下:

-其直观上来看数组的insert操作是O(n)的,整体代码的时间复杂度是O(n^2)。
+其直观上来看数组的insert操作是$O(n)$的,整体代码的时间复杂度是$O(n^2)$。
这么一分析好像和版本二链表实现的时间复杂度是一样的啊,为什么提交之后效率会差距这么大呢?
```CPP
@@ -97,7 +97,7 @@ for (int i = 0; i < vec.size(); i++) {
**同时也注意此时capicity和size的变化,关键的地方我都标红了**。
-而在[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中,我们使用vector来做insert的操作,此时大家可会发现,**虽然表面上复杂度是O(n^2),但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是O(n^2 + t * n)级别的,t是底层拷贝的次数**。
+而在[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中,我们使用vector来做insert的操作,此时大家可会发现,**虽然表面上复杂度是$O(n^2)$,但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是$O(n^2 + t × n)$级别的,t是底层拷贝的次数**。
那么是不是可以直接确定好vector的大小,不让它在动态扩容了,例如在[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中已经给出了有people.size这么多的人,可以定义好一个固定大小的vector,这样我们就可以控制vector,不让它底层动态扩容。
@@ -133,7 +133,7 @@ public:

-这份代码就是不让vector动态扩容,全程我们自己模拟insert的操作,大家也可以直观的看出是一个O(n^2)的方法了。
+这份代码就是不让vector动态扩容,全程我们自己模拟insert的操作,大家也可以直观的看出是一个$O(n^2)$的方法了。
但这份代码在leetcode上统计的耗时甚至比版本一的还高,我们都不让它动态扩容了,为什么耗时更高了呢?
@@ -151,7 +151,7 @@ public:
大家应该发现了,编程语言中一个普通容器的insert,delete的使用,都可能对写出来的算法的有很大影响!
-如果抛开语言谈算法,除非从来不用代码写算法纯分析,**否则的话,语言功底不到位O(n)的算法可以写出O(n^2)的性能**,哈哈。
+如果抛开语言谈算法,除非从来不用代码写算法纯分析,**否则的话,语言功底不到位$O(n)$的算法可以写出$O(n^2)$的性能**,哈哈。
相信在这里学习算法的录友们,都是想在软件行业长远发展的,都是要从事编程的工作,那么一定要深耕好一门编程语言,这个非常重要!
diff --git a/problems/知识星球精选/刷力扣用不用库函数.md b/problems/知识星球精选/刷力扣用不用库函数.md
index 07db8564..73f2a2d5 100644
--- a/problems/知识星球精选/刷力扣用不用库函数.md
+++ b/problems/知识星球精选/刷力扣用不用库函数.md
@@ -27,7 +27,7 @@
使用库函数最大的忌讳就是不知道这个库函数怎么实现的,也不知道其时间复杂度,上来就用,这样写出来的算法,时间复杂度自己都掌握不好的。
-例如for循环里套一个字符串的insert,erase之类的操作,你说时间复杂度是多少呢,很明显是O(n^2)的时间复杂度了。
+例如for循环里套一个字符串的insert,erase之类的操作,你说时间复杂度是多少呢,很明显是$O(n^2)$的时间复杂度了。
在刷题的时候本着我说的标准来使用库函数,详细对大家回有所帮助!
diff --git a/problems/背包理论基础01背包-1.md b/problems/背包理论基础01背包-1.md
index 16be4c67..ae645d93 100644
--- a/problems/背包理论基础01背包-1.md
+++ b/problems/背包理论基础01背包-1.md
@@ -40,7 +40,7 @@ leetcode上没有纯01背包的问题,都是01背包应用方面的题目,
这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?
-每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。
+每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是$O(2^n)$,这里的n表示物品数量。
**所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!**
diff --git a/problems/背包问题理论基础多重背包.md b/problems/背包问题理论基础多重背包.md
index 5f62ccd6..1b24168b 100644
--- a/problems/背包问题理论基础多重背包.md
+++ b/problems/背包问题理论基础多重背包.md
@@ -89,7 +89,7 @@ int main() {
```
-* 时间复杂度:O(m * n * k) m:物品种类个数,n背包容量,k单类物品数量
+* 时间复杂度:$O(m × n × k)$,m:物品种类个数,n背包容量,k单类物品数量
也有另一种实现方式,就是把每种商品遍历的个数放在01背包里面在遍历一遍。
@@ -125,7 +125,7 @@ int main() {
}
```
-* 时间复杂度:O(m * n * k) m:物品种类个数,n背包容量,k单类物品数量
+* 时间复杂度:$O(m × n × k)$,m:物品种类个数,n背包容量,k单类物品数量
从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。
diff --git a/problems/链表理论基础.md b/problems/链表理论基础.md
index 8891c665..500ff213 100644
--- a/problems/链表理论基础.md
+++ b/problems/链表理论基础.md
@@ -70,7 +70,7 @@
这里我给出C/C++的定义链表节点方式,如下所示:
-```
+```cpp
// 单链表
struct ListNode {
int val; // 节点上存储的元素
@@ -85,13 +85,13 @@ struct ListNode {
通过自己定义构造函数初始化节点:
-```
+```cpp
ListNode* head = new ListNode(5);
```
使用默认构造函数初始化节点:
-```
+```cpp
ListNode* head = new ListNode();
head->val = 5;
```
@@ -120,9 +120,9 @@ head->val = 5;

-可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
+可以看出链表的增添和删除都是$O(1)$操作,也不会影响到其他节点。
-但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
+但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是$O(n)$。
# 性能分析
diff --git a/problems/面试题02.07.链表相交.md b/problems/面试题02.07.链表相交.md
index e3c284c0..9d399ef9 100644
--- a/problems/面试题02.07.链表相交.md
+++ b/problems/面试题02.07.链表相交.md
@@ -249,6 +249,45 @@ var getIntersectionNode = function(headA, headB) {
};
```
+C:
+
+```c
+ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
+ ListNode *l = NULL, *s = NULL;
+ int lenA = 0, lenB = 0, gap = 0;
+ // 求出两个链表的长度
+ s = headA;
+ while (s) {
+ lenA ++;
+ s = s->next;
+ }
+ s = headB;
+ while (s) {
+ lenB ++;
+ s = s->next;
+ }
+
+ // 求出两个链表长度差
+ if (lenA > lenB) {
+ l = headA, s = headB;
+ gap = lenA - lenB;
+ } else {
+ l = headB, s = headA;
+ gap = lenB - lenA;
+ }
+
+ // 尾部对齐
+ while (gap--) l = l->next;
+ // 移动,并检查是否有相同的元素
+ while (l) {
+ if (l == s) return l;
+ l = l->next, s = s->next;
+ }
+
+ return NULL;
+}
+```
+