Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e12257e
修改错别字“下表”->“下标”
bqlin Dec 10, 2021
e662df7
面试题02.07.链表相交:增加C语言实现
bqlin Dec 10, 2021
973582c
0142.环形链表II:添加C语言实现
bqlin Dec 10, 2021
90638af
优化排版,把复杂度标记为公式
bqlin Dec 10, 2021
05b7350
修正复杂度公式
bqlin Dec 10, 2021
f7db9fd
0017.电话号码的字母组合:优化排版,补充Swift版本
bqlin Dec 11, 2021
f702f04
0216.组合总和III:优化排版,补充Swift版本
bqlin Dec 11, 2021
1a15ed0
0077.组合优化:补充Swift版本
bqlin Dec 11, 2021
a1359e0
0077.组合:优化排版,补充Swift版本
bqlin Dec 11, 2021
dda0b20
0017.电话号码的字母组合:更新Swift实现
bqlin Dec 11, 2021
a5c51b7
0216.组合总和III:更新Swift实现
bqlin Dec 11, 2021
6a5c3ae
0039.组合总和:优化排版,补充Swift版本
bqlin Dec 11, 2021
9a577c7
0040.组合总和II:优化排版,补充Swift版本
bqlin Dec 12, 2021
c82bf13
0131.分割回文串:优化排版,补充Swift版本
bqlin Dec 12, 2021
c29f74d
0093.复原IP地址:优化排版,补充Swift版本
bqlin Dec 12, 2021
458a37e
0078.子集:优化排版,补充Swift版本
bqlin Dec 12, 2021
b884383
0090.子集II:优化排版,补充Swift版本
bqlin Dec 12, 2021
cf59acd
0491.递增子序列:优化排版,补充Swift版本
bqlin Dec 12, 2021
39a233f
0046.全排列:优化排版,补充Swift版本
bqlin Dec 13, 2021
a953eca
0047.全排列II:优化排版,补充Swift版本
bqlin Dec 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@

* 算法性能分析
* [关于时间复杂度,你不知道的都在这里!](./problems/前序/关于时间复杂度,你不知道的都在这里!.md)
* [O(n)的算法居然超时了,此时的n究竟是多大?](./problems/前序/On的算法居然超时了,此时的n究竟是多大?.md)
* [$O(n)$的算法居然超时了,此时的n究竟是多大?](./problems/前序/On的算法居然超时了,此时的n究竟是多大?.md)
* [通过一道面试题目,讲一讲递归算法的时间复杂度!](./problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md)
* [本周小结!(算法性能分析系列一)](./problems/周总结/20201210复杂度分析周末总结.md)
* [关于空间复杂度,可能有几个疑问?](./problems/前序/关于空间复杂度,可能有几个疑问?.md)
Expand Down
12 changes: 6 additions & 6 deletions problems/0001.两数之和.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

## 思路

很明显暴力的解法是两层for循环查找,时间复杂度是O(n^2)。
很明显暴力的解法是两层for循环查找,时间复杂度是$O(n^2)$

建议大家做这道题目之前,先做一下这两道
* [242. 有效的字母异位词](https://www.programmercarl.com/0242.有效的字母异位词.html)
Expand All @@ -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 的底层实现是红黑树。

Expand Down
10 changes: 5 additions & 5 deletions problems/0005.最长回文子串.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

两层for循环,遍历区间起始位置和终止位置,然后判断这个区间是不是回文。

时间复杂度:O(n^3)
时间复杂度:$O(n^3)$

## 动态规划

Expand Down Expand Up @@ -205,8 +205,8 @@ public:

```

* 时间复杂度:O(n^2)
* 空间复杂度:O(n^2)
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

## 双指针

Expand Down Expand Up @@ -253,8 +253,8 @@ public:

```

* 时间复杂度:O(n^2)
* 空间复杂度:O(1)
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$



Expand Down
6 changes: 3 additions & 3 deletions problems/0015.三数之和.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

去重的过程不好处理,有很多小细节,如果在面试中很难想到位。

时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
时间复杂度可以做到$O(n^2)$,但还是比较费时的,因为不好做剪枝操作。

大家可以尝试使用哈希法写一写,就知道其困难的程度了。

Expand Down Expand Up @@ -85,7 +85,7 @@ public:

**其实这道题目使用哈希法并不十分合适**,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。

而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。
而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是$O(n^2)$,也是可以在leetcode上通过,但是程序的执行时间依然比较长 。

接下来我来介绍另一个解法:双指针法,**这道题目使用双指针法 要比哈希法高效一些**,那么来讲解一下具体实现的思路。

Expand All @@ -101,7 +101,7 @@ public:

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

时间复杂度:O(n^2)。
时间复杂度:$O(n^2)$

C++代码代码如下:

Expand Down
54 changes: 48 additions & 6 deletions problems/0017.电话号码的字母组合.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

可以使用map或者定义一个二位数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下:

```
```cpp
const string letterMap[10] = {
"", // 0
"", // 1
Expand Down Expand Up @@ -79,7 +79,7 @@ const string letterMap[10] = {

代码如下:

```
```cpp
vector<string> result;
string s;
void backtracking(const string& digits, int index)
Expand All @@ -95,7 +95,7 @@ void backtracking(const string& digits, int index)

代码如下:

```
```cpp
if (index == digits.size()) {
result.push_back(s);
return;
Expand Down Expand Up @@ -281,7 +281,7 @@ class Solution {

## Python
**回溯**
```python3
```python
class Solution:
def __init__(self):
self.answers: List[str] = []
Expand Down Expand Up @@ -317,7 +317,7 @@ class Solution:
self.answer = self.answer[:-1] # 回溯
```
**回溯简化**
```python3
```python
class Solution:
def __init__(self):
self.answers: List[str] = []
Expand Down Expand Up @@ -420,7 +420,8 @@ var letterCombinations = function(digits) {
};
```

C:
## C

```c
char* path;
int pathTop;
Expand Down Expand Up @@ -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
}
```


-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>
8 changes: 4 additions & 4 deletions problems/0018.四数之和.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,21 @@

但是有一些细节需要注意,例如: 不要判断`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,同时四元组不能重复。

而[454.四数相加II](https://programmercarl.com/0454.四数相加II.html)是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少!

我们来回顾一下,几道题目使用了双指针法。

双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下:
双指针法将时间复杂度:$O(n^2)$的解法优化为 $O(n)$的解法。也就是降一个数量级,题目如下:

* [27.移除元素](https://programmercarl.com/0027.移除元素.html)
* [15.三数之和](https://programmercarl.com/0015.三数之和.html)
Expand Down
5 changes: 3 additions & 2 deletions problems/0024.两两交换链表中的节点.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public:
}
};
```

* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

Expand All @@ -73,7 +74,7 @@ public:

上面的代码我第一次提交执行用时8ms,打败6.5%的用户,差点吓到我了。

心想应该没有更好的方法了吧,也就O(n)的时间复杂度,重复提交几次,这样了:
心想应该没有更好的方法了吧,也就$O(n)$的时间复杂度,重复提交几次,这样了:

![24.两两交换链表中的节点](https://code-thinking.cdn.bcebos.com/pics/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.png)

Expand All @@ -85,7 +86,7 @@ public:
## 其他语言版本

C:
```
```c
/**
* Definition for singly-linked list.
* struct ListNode {
Expand Down
8 changes: 4 additions & 4 deletions problems/0027.移除元素.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并**原地**修改输入数组。
不要使用额外的数组空间,你必须仅使用 $O(1)$ 额外空间并**原地**修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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++){
Expand Down
4 changes: 2 additions & 2 deletions problems/0028.实现strStr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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为模式串。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
进阶:你可以设计并实现时间复杂度为 $O(\log n)$ 的算法解决此问题吗?


示例 1:
Expand Down
17 changes: 9 additions & 8 deletions problems/0035.搜索插入位置.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,24 @@ public:
};
```

* 时间复杂度:O(n)
* 空间复杂度:O(1)
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

效率如下:

![35_搜索插入位置](https://img-blog.csdnimg.cn/20201216232127268.png)

### 二分法

既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。
既然暴力解法的时间复杂度是$O(n)$,就要尝试一下使用二分查找法。

![35_搜索插入位置4](https://img-blog.csdnimg.cn/202012162326354.png)

大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。

以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。**

同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的

大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。

Expand Down Expand Up @@ -140,8 +140,9 @@ public:
}
};
```
* 时间复杂度:O(logn)
* 时间复杂度:O(1)

* 时间复杂度:$O(\log n)$
* 时间复杂度:$O(1)$

效率如下:
![35_搜索插入位置2](https://img-blog.csdnimg.cn/2020121623272877.png)
Expand Down Expand Up @@ -183,8 +184,8 @@ public:
};
```

* 时间复杂度:O(logn)
* 时间复杂度:O(1)
* 时间复杂度:$O(\log n)$
* 时间复杂度:$O(1)$

## 总结

Expand Down
29 changes: 28 additions & 1 deletion problems/0039.组合总和.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ func backtracking(startIndex,sum,target int,candidates,trcak []int,res *[][]int)
}
```

## JavaScript
## JavaScript

```js
var combinationSum = function(candidates, target) {
Expand Down Expand Up @@ -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
}
```

-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>
Loading