Merge branch 'master' of github.com:Tiffany-yuan/leetcode-master

This commit is contained in:
mengyuan
2021-12-20 19:45:11 +08:00
128 changed files with 1370 additions and 608 deletions

View File

@ -22,6 +22,14 @@
<a href="https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ"><img src="https://img.shields.io/badge/知识星球-代码随想录-blue" alt=""></a>
</p>
<p align="center"><strong>《代码随想录》正式出版啦!!录友专属福利,点击下方可以享五折优惠!详细可以<a href="programmercarl.com/other/publish.html">点击这里</a></strong></p>
<p align="center">
<a href="https://union-click.jd.com/jdc?e=&p=JF8BAMQJK1olXg8EUVhVCkkWAV8IGV8WVAICU24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYAUF1UDEsQHDZNRwYlX0B9A1cfakpyYBkSRj4QKFBUEEAfaEcbM244GFIXWQYAUV5VOHsXBF9edVsUXAcDVVtdDUgnAl8IHFkdXw8KUl5fDkgRM2gIEmtIFVpKAxVtOHsUM184G2sWbURsVApfAR8XA2sLSw8cWA8LUw1ZCElHAmhdTAxGW1YBUlxtCkoWB2Y4" target="_blank">
<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20211213180559.png" width="400"/>
</a>
</p>
# 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)

View File

@ -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 的底层实现是红黑树。

View File

@ -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)$

View File

@ -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++代码代码如下

View File

@ -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<string> 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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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)

View File

@ -62,6 +62,7 @@ public:
}
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
@ -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)
@ -85,7 +86,7 @@ public:
## 其他语言版本
C:
```
```c
/**
* Definition for singly-linked list.
* struct ListNode {

View File

@ -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++){

View File

@ -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)
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -11,7 +11,7 @@
如果数组中不存在目标值 target返回 [-1, -1]。
进阶你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
进阶:你可以设计并实现时间复杂度为 $O(\log n)$ 的算法解决此问题吗?
示例 1

View File

@ -73,8 +73,8 @@ public:
};
```
* 时间复杂度O(n)
* 空间复杂度O(1)
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
效率如下:
@ -82,7 +82,7 @@ public:
### 二分法
既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。
既然暴力解法的时间复杂度是$O(n)$,就要尝试一下使用二分查找法。
![35_搜索插入位置4](https://img-blog.csdnimg.cn/202012162326354.png)
@ -90,7 +90,7 @@ public:
以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。**
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下可能不是唯一的。
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下可能不是唯一的。
大体讲解一下二分法的思路这里来举一个例子例如在这个数组中使用二分法寻找元素为5的位置并返回其下标。
@ -140,8 +140,9 @@ public:
}
};
```
* 时间复杂度O(logn)
* 时间复杂度O(1)
* 时间复杂度:$O(\log n)$
* 时间复杂度:$O(1)$
效率如下:
![35_搜索插入位置2](https://img-blog.csdnimg.cn/2020121623272877.png)
@ -183,8 +184,8 @@ public:
};
```
* 时间复杂度O(logn)
* 时间复杂度O(1)
* 时间复杂度:$O(\log n)$
* 时间复杂度:$O(1)$
## 总结

View File

@ -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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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)$
-----------------------

View File

@ -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<vector<int>> result;
vector<int> path;
void backtracking (vector<int>& nums, vector<bool>& used)
@ -72,7 +72,7 @@ void backtracking (vector<int>& nums, vector<bool>& 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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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)$
## 总结

View File

@ -79,8 +79,9 @@ public:
}
};
```
* 时间复杂度O(n)
* 间复杂度O(n)
* 间复杂度:$O(n)$
* 空间复杂度:$O(n)$
## 总结

View File

@ -112,8 +112,8 @@ public:
};
```
* 时间复杂度O(nlogn) 有一个快排
* 空间复杂度O(1)我没有算result数组返回值所需容器占的空间
* 时间复杂度$O(n\log n)$ 有一个快排
* 空间复杂度$O(1)$我没有算result数组返回值所需容器占的空间
## 总结

View File

@ -10,7 +10,7 @@
[力扣题目链接](https://leetcode-cn.com/problems/spiral-matrix-ii/)
给定一个正整数 n生成一个包含 1 到 n^2 所有元素且元素按顺时针顺序螺旋排列的正方形矩阵。
给定一个正整数 n生成一个包含 1 到 $n^2$ 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:

View File

@ -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. 遍历顺序 一行一行遍历

View File

@ -152,8 +152,9 @@ public:
}
};
```
* 时间复杂度O(n * m) n m 分别为obstacleGrid 长度和宽度
* 空间复杂度O(n * m)
*间复杂度$O(n × m)$n、m 分别为obstacleGrid 长度和宽度
* 空间复杂度:$O(n × m)$
## 总结

View File

@ -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)$
后面将讲解的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,**但我个人认为面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化**。

View File

@ -109,7 +109,7 @@ for (int i = 1; i <= n; i++) {
代码如下:
```
```cpp
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
```
@ -132,7 +132,7 @@ vector<int> path; // 用来存放符合条件结果
那么整体代码如下:
```
```cpp
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> 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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -57,13 +57,13 @@
代码如下:
```
```cpp
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
```
* 递归终止条件
递归终止条件
从图中可以看出:
@ -75,7 +75,7 @@ void backtracking(vector<int>& 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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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()]) {

View File

@ -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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -66,10 +66,10 @@ startIndex一定是需要的因为不能重复分割记录下一层递归
所以代码如下:
```
vector<string> result;// 记录结果
// startIndex: 搜索的起始位置pointNum:添加逗点的数量
void backtracking(string& s, int startIndex, int pointNum) {
```cpp
vector<string> 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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -136,8 +136,9 @@ public:
}
};
```
* 时间复杂度O(n^2)
* 间复杂度O(n)
* 间复杂度$O(n^2)$
* 空间复杂度:$O(n)$
大家应该发现了,我们分析了这么多,最后代码却如此简单!

View File

@ -272,7 +272,7 @@ public:
**此时应该发现了如上的代码性能并不好应为每层递归定定义了新的vector就是数组既耗时又耗空间但上面的代码是最好理解的为了方便读者理解所以用如上的代码来讲解。**
下面给出用下索引写出的代码版本思路是一样的只不过不用重复定义vector了每次用下索引来分割)
下面给出用下索引写出的代码版本思路是一样的只不过不用重复定义vector了每次用下索引来分割)
### C++优化版本
```CPP

View File

@ -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<TreeNode*> nodeQue; // 放遍历的节点
queue<int> leftQue; // 保存左区间下
queue<int> rightQue; // 保存右区间下
queue<int> leftQue; // 保存左区间下
queue<int> 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()) {

View File

@ -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。

View File

@ -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)$
## 总结

View File

@ -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)

View File

@ -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]。 但结果也是对的。

View File

@ -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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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)$
**说这种解法为贪心算法,才是是有理有据的,因为全局最优解是根据局部最优推导出来的**

View File

@ -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)$
## 总结

View File

@ -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;
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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)是如何移除元素的。

View File

@ -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)$ 的 原地 算法**
那么我来提供一种旋转的方式哈。

View File

@ -20,7 +20,7 @@
## 暴力解法
这道题目暴力解法当然是 两个for循环然后不断的寻找符合条件的子序列时间复杂度很明显是O(n^2)
这道题目暴力解法当然是 两个for循环然后不断的寻找符合条件的子序列时间复杂度很明显是$O(n^2)$
代码如下:
@ -80,7 +80,7 @@ public:
![leetcode_209](https://img-blog.csdnimg.cn/20210312160441942.png)
可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况不断调节子序列的起始位置。从而将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)$。
## 相关题目推荐

View File

@ -57,7 +57,7 @@
至于为什么取名为path从上面树形结构中可以看出结果其实就是一条根节点到叶子节点的路径。
```
```cpp
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
```
@ -71,7 +71,7 @@ vector<int> path; // 符合条件的结果
所以代码如下:
```
```cpp
vector<vector<int>> result;
vector<int> 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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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)$
# 其他语言版本

View File

@ -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

View File

@ -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
}
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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++ 代码如下:

View File

@ -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就可以了**

View File

@ -147,8 +147,8 @@ public:
};
```
* 时间复杂度O(n)
* 空间复杂度O(n)
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$
当然空间复杂度可以优化定义一个dp[2][4]大小的数组就可以了,就保存前一天的当前的状态,感兴趣的同学可以自己去写一写,思路是一样的。

View File

@ -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)$算上递推系统栈的空间
## 总结

View File

@ -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)$
## 总结

View File

@ -14,7 +14,7 @@
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 $O(1)$ 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
@ -63,7 +63,7 @@
如果对数组和链表原理不清楚的同学,可以看这两篇,[关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html)[必须掌握的数组理论知识](https://programmercarl.com/数组理论基础.html)。
对于字符串,我们定义两个指针(也可以说是索引下),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
对于字符串,我们定义两个指针(也可以说是索引下),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。
以字符串`hello`为例,过程如下:

View File

@ -25,7 +25,7 @@
提示:
* 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
* 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
* 你的算法的时间复杂度必须优于 $O(n \log n)$ , n 是数组的大小。
* 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
* 你可以按任意顺序返回答案。

View File

@ -28,7 +28,7 @@
注意题目特意说明:**输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序**
这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。
这道题用暴力的解法时间复杂度是$O(n^2)$,那来看看使用哈希法进一步优化。
那么用数组来做哈希表也是不错的选择,例如[242. 有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html)

View File

@ -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)$
## 总结

View File

@ -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)$
## 总结

View File

@ -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)$
大家可以把两个版本的代码提交一下试试就可以发现其差别了

View File

@ -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数组大小为一个常数但是大常数
## 总结

View File

@ -92,8 +92,8 @@ public:
}
};
```
* 时间复杂度O(nlogn) ,有一个快排
* 空间复杂度O(1)
* 时间复杂度:$O(n\log n)$ ,有一个快排
* 空间复杂度:$O(1)$
大家此时会发现如此复杂的一个问题,代码实现却这么简单!

View File

@ -17,7 +17,7 @@
首先找到需要删除的节点;
如果找到了,删除它。
说明: 要求算法时间复杂度为 O(h)h 为树的高度。
说明: 要求算法时间复杂度为 $O(h)$h 为树的高度。
示例:

View File

@ -105,8 +105,8 @@ public:
};
```
* 时间复杂度O(nlogn)因为有一个快排
* 空间复杂度O(1)
* 时间复杂度$O(n\log n)$因为有一个快排
* 空间复杂度$O(1)$
可以看出代码并不复杂

View File

@ -59,7 +59,7 @@ public:
int findContentChildren(vector<int>& g, vector<int>& 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]) {

View File

@ -56,7 +56,7 @@
代码如下:
```
```cpp
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex)
@ -68,7 +68,7 @@ void backtracking(vector<int>& 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<int> 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<Int>()
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
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -221,8 +221,8 @@ public:
};
```
* 时间复杂度O(n * m)n为正数个数m为背包容量
* 空间复杂度O(m) m为背包容量
* 时间复杂度$O(n × m)$n为正数个数m为背包容量
* 空间复杂度:$O(m)$m为背包容量
## 总结

View File

@ -70,7 +70,7 @@ C++中当我们要使用集合来解决哈希问题的时候优先使用un
那么预处理代码如下:
```CPP
unordered_map<int, int> umap; // key:下元素value
unordered_map<int, int> 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<int> result(nums1.size(), -1);
if (nums1.size() == 0) return result;
unordered_map<int, int> umap; // key:下元素value
unordered_map<int, int> 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<int> result(nums1.size(), -1);
if (nums1.size() == 0) return result;
unordered_map<int, int> umap; // key:下元素value
unordered_map<int, int> 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]
}

View File

@ -68,7 +68,7 @@ public:
这种写法确实比较直观但做了很多无用操作例如修改了nums数组而且最后还要把result数组resize回去。
resize倒是不费时间是O(1)的操作但扩充nums数组相当于多了一个O(n)的操作。
resize倒是不费时间$O(1)$的操作但扩充nums数组相当于多了一个$O(n)$的操作。
其实也可以不扩充nums而是在遍历的过程中模拟走了两边nums。

View File

@ -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)

View File

@ -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)$
## 其他语言版本

View File

@ -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<int> newVec(nums.begin(), nums.begin() + maxValueIndex);
node->left = constructMaximumBinaryTree(newVec);
}
// 最大值所在的下右区间 构造右子树
// 最大值所在的下右区间 构造右子树
if (maxValueIndex < (nums.size() - 1)) {
vector<int> 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<int>& 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这个问题我在最后也给出了解释。

View File

@ -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 (十年前的陈年老文了)
# 其他语言版本

View File

@ -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.递推公式 ifnums[i+1]>nums[i] dp[i+1] = dp[i]+1
* 3.初始化 都为1
* 4.遍历方向,从其那往后

View File

@ -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;
}

View File

@ -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)$
## 总结

View File

@ -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)$
## 其他语言版本

View File

@ -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)$
## 其他语言版本

View File

@ -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)$需要一个字符串,转化为字符串操作更方便
## 总结

View File

@ -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)$
精简的代码是直接把情况一二三都合并到了一起其实这种代码精简是精简但思路不是很清晰

View File

@ -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)$
**当然我不建议这么写,能写出版本一就可以了,直观简洁!**

View File

@ -68,8 +68,8 @@ public:
};
```
* 时间复杂度O(n)
* 空间复杂度O(1) 使用的hash数组是固定大小
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$使用的hash数组是固定大小
## 总结

View File

@ -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和Ti初始为S末尾j初始为T末尾记录#的数量,模拟消除的操作,如果#用完了就开始比较S[i]和S[j]。
@ -150,8 +151,8 @@ public:
};
```
* 时间复杂度O(n + m)
* 空间复杂度O(1)
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(1)$
# 其他语言版本

View File

@ -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{

View File

@ -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<int> sortArrayByParityII(vector<int>& A) {
vector<int> 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的关系
## 其他语言版本

View File

@ -90,8 +90,8 @@ public:
```
时间复杂度O(n)
空间复杂度O(1)
时间复杂度:$O(n)$
空间复杂度:$O(1)$
# 其他语言版本

View File

@ -43,7 +43,7 @@
**注意这里还是有一些细节,例如如下两点:**
* 因为left和right是数组下,移动的过程中注意不要数组越界
* 因为left和right是数组下,移动的过程中注意不要数组越界
* 如果left或者right没有移动说明是一个单调递增或者递减的数组依然不是山峰
C++代码如下:

View File

@ -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上执行用时打败多少多少用户这个就是一个玩具非常不准确。**

View File

@ -40,7 +40,7 @@ words[i] 由小写英文字母组成
这道题目一眼看上去,就是用哈希法,**“小写字符”,“出现频率”, 这些关键字都是为哈希法量身定做的啊**
首先可以想到的是暴力解法一个字符串一个字符串去搜时间复杂度是O(n^m)n是字符串长度m是有几个字符串。
首先可以想到的是暴力解法,一个字符串一个字符串去搜,时间复杂度是$O(n^m)$n是字符串长度m是有几个字符串。
可以看出这是指数级别的时间复杂度,非常高,而且代码实现也不容易,因为要统计 重复的字符,还要适当的替换或者去重。

View File

@ -136,8 +136,8 @@ public:
```
* 时间复杂度O(m * n) , m是石头总重量准确的说是总重量的一半n为石头块数
* 空间复杂度O(m)
* 时间复杂度:$O(m × n)$ , m是石头总重量准确的说是总重量的一半n为石头块数
* 空间复杂度:$O(m)$
## 总结

View File

@ -41,7 +41,7 @@
# 思路
两层for循环暴力查找时间复杂度明显为O(n^2)。
两层for循环暴力查找时间复杂度明显为$O(n^2)$
那么我们来看一下如何优化
@ -110,7 +110,7 @@ public:
};
```
可以排序之后加哈希时间复杂度为O(nlogn)
可以排序之后加哈希,时间复杂度为$O(n\log n)$
# 其他语言版本

View File

@ -3,7 +3,7 @@
<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20210924105952.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 程序提交之后为什么会超时O(n)的算法会超时n究竟是多大
# 程序提交之后为什么会超时?$O(n)$的算法会超时n究竟是多大
一些同学可能对计算机运行的速度还没有概念就是感觉计算机运行速度应该会很快那么在leetcode上做算法题目的时候为什么会超时呢
@ -18,9 +18,9 @@
也就是说程序运行的时间超过了规定的时间一般OJonline 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年MacProCPU配置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() {
![程序超时2](https://img-blog.csdnimg.cn/20200729200018460.png)
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)开根号,实验数据如下。
![程序超时3](https://img-blog.csdnimg.cn/2020072919590970.png)
O(n^2)的算法1s内大概计算机可以运行 22500次计算验证了刚刚的推测。
在推测一下O(nlogn)的话, 1s可以处理的数据规模是什么呢
在推测一下$O(n\log n)$的话, 1s可以处理的数据规模是什么呢
理论上应该是比 O(n)少一个数量级因为logn的复杂度 其实是很快,看一下实验数据。
理论上应该是比 $O(n)$少一个数量级,因为$\log n$的复杂度 其实是很快,看一下实验数据。
![程序超时4](https://img-blog.csdnimg.cn/20200729195729407.png)
O(nlogn)的算法1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。
$O(n \logn)$的算法1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。
这是在我个人PC上测出来的数据不能说是十分精确但数量级是差不多的大家也可以在自己的计算机上测一下。
@ -148,7 +148,7 @@ O(nlogn)的算法1s内大概计算机可以运行 2 * (10^7)次计算,符
![程序超时1](https://img-blog.csdnimg.cn/20201208231559175.png)
至于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的大小。
建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。

View File

@ -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<T> {
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
}
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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)
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -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代表的就是一般情况而不是严格的上界**。如图所示:
![时间复杂度4一般情况下的时间复杂度](https://img-blog.csdnimg.cn/20200728185745611.png)
我们主要关心的还是一般情况下的数据形式。
@ -44,11 +44,11 @@
![时间复杂度,不同数据规模的差异](https://img-blog.csdnimg.cn/20200728191447384.png)
在决定使用哪些算法的时候不是时间复杂越低的越好因为简化后的时间复杂度忽略了常数项等等要考虑数据规模如果数据规模很小甚至可以用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)$
所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。

View File

@ -45,7 +45,21 @@
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20210914223147.png)
那么此时大家是不是应该知道了,数组如何转化成 二叉树了。**如果父节点的数组下标是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`
那么这里又有同学疑惑了,这些我都懂了,但我还是不知道 应该 怎么构造。

View File

@ -20,9 +20,9 @@
也就是说程序运行的时间超过了规定的时间一般OJonline 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年MacProCPU配置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() {
![程序超时2](https://img-blog.csdnimg.cn/20200729200018460.png)
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)开根号,实验数据如下。
![程序超时3](https://img-blog.csdnimg.cn/2020072919590970.png)
O(n^2)的算法1s内大概计算机可以运行 22500次计算验证了刚刚的推测。
在推测一下O(nlogn)的话, 1s可以处理的数据规模是什么呢
在推测一下$O(n\log n)$的话, 1s可以处理的数据规模是什么呢
理论上应该是比 O(n)少一个数量级因为logn的复杂度 其实是很快,看一下实验数据。
理论上应该是比 $O(n)$少一个数量级,因为$\log n$的复杂度 其实是很快,看一下实验数据。
![程序超时4](https://img-blog.csdnimg.cn/20200729195729407.png)
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)次计算,符
![程序超时1](https://img-blog.csdnimg.cn/20201208231559175.png)
至于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的大小。
建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。

View File

@ -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++编程规范,就可以领取。(涉及到微信后台的回复,没更改)
**具体的规范要以自己团队风格为主**,融入团队才是最重要的。

View File

@ -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代表的就是一般情况而不是严格的上界**。如图所示:
![时间复杂度4一般情况下的时间复杂度](https://img-blog.csdnimg.cn/20200728185745611.png)
我们主要关心的还是一般情况下的数据形式。
@ -57,11 +57,11 @@
![时间复杂度,不同数据规模的差异](https://img-blog.csdnimg.cn/20200728191447384.png)
在决定使用哪些算法的时候不是时间复杂越低的越好因为简化后的时间复杂度忽略了常数项等等要考虑数据规模如果数据规模很小甚至可以用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)$
所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快

View File

@ -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的情况**。

View File

@ -13,11 +13,11 @@
然后录友就问了:如何打日志呢?
其实在力扣上打日志也挺方便的,我一般调试就是直接在力扣上打日志,偶尔需要把代码粘到本来运行添加日志debug一下。
其实在力扣上打日志也挺方便的,我一般调试就是直接在力扣上打日志,偶尔需要把代码粘到本来运行添加日志debug一下。
在力扣上直接打日志这个就不用讲C++的话想打啥直接cout啥就可以了。
我来说一说力扣代码如何在本运行。
我来说一说力扣代码如何在本运行。
毕竟我们天天用力扣刷题,也应该知道力扣上的代码如何在本地编译运行。

View File

@ -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`

Some files were not shown because too many files have changed in this diff Show More