Merge branch 'youngyangyang04:master' into master

This commit is contained in:
lingyin
2022-04-22 11:11:59 +08:00
committed by GitHub
57 changed files with 1034 additions and 170 deletions

View File

@ -420,6 +420,40 @@ var letterCombinations = function(digits) {
};
```
## TypeScript
```typescript
function letterCombinations(digits: string): string[] {
if (digits === '') return [];
const strMap: { [index: string]: string[] } = {
1: [],
2: ['a', 'b', 'c'],
3: ['d', 'e', 'f'],
4: ['g', 'h', 'i'],
5: ['j', 'k', 'l'],
6: ['m', 'n', 'o'],
7: ['p', 'q', 'r', 's'],
8: ['t', 'u', 'v'],
9: ['w', 'x', 'y', 'z'],
}
const resArr: string[] = [];
function backTracking(digits: string, curIndex: number, route: string[]): void {
if (curIndex === digits.length) {
resArr.push(route.join(''));
return;
}
let tempArr: string[] = strMap[digits[curIndex]];
for (let i = 0, length = tempArr.length; i < length; i++) {
route.push(tempArr[i]);
backTracking(digits, curIndex + 1, route);
route.pop();
}
}
backTracking(digits, 0, []);
return resArr;
};
```
## C
```c

View File

@ -392,7 +392,34 @@ var combinationSum = function(candidates, target) {
};
```
## TypeScript
```typescript
function combinationSum(candidates: number[], target: number): number[][] {
const resArr: number[][] = [];
function backTracking(
candidates: number[], target: number,
startIndex: number, route: number[], curSum: number
): void {
if (curSum > target) return;
if (curSum === target) {
resArr.push(route.slice());
return
}
for (let i = startIndex, length = candidates.length; i < length; i++) {
let tempVal: number = candidates[i];
route.push(tempVal);
backTracking(candidates, target, i, route, curSum + tempVal);
route.pop();
}
}
backTracking(candidates, target, 0, [], 0);
return resArr;
};
```
## C
```c
int* path;
int pathTop;

View File

@ -532,6 +532,7 @@ var combinationSum2 = function(candidates, target) {
};
```
**使用used去重**
```js
var combinationSum2 = function(candidates, target) {
let res = [];
@ -562,6 +563,37 @@ var combinationSum2 = function(candidates, target) {
};
```
## TypeScript
```typescript
function combinationSum2(candidates: number[], target: number): number[][] {
candidates.sort((a, b) => a - b);
const resArr: number[][] = [];
function backTracking(
candidates: number[], target: number,
curSum: number, startIndex: number, route: number[]
) {
if (curSum > target) return;
if (curSum === target) {
resArr.push(route.slice());
return;
}
for (let i = startIndex, length = candidates.length; i < length; i++) {
if (i > startIndex && candidates[i] === candidates[i - 1]) {
continue;
}
let tempVal: number = candidates[i];
route.push(tempVal);
backTracking(candidates, target, curSum + tempVal, i + 1, route);
route.pop();
}
}
backTracking(candidates, target, 0, 0, []);
return resArr;
};
```
## C
```c

View File

@ -331,6 +331,34 @@ var permute = function(nums) {
```
## TypeScript
```typescript
function permute(nums: number[]): number[][] {
const resArr: number[][] = [];
const helperSet: Set<number> = new Set();
backTracking(nums, []);
return resArr;
function backTracking(nums: number[], route: number[]): void {
if (route.length === nums.length) {
resArr.push(route.slice());
return;
}
let tempVal: number;
for (let i = 0, length = nums.length; i < length; i++) {
tempVal = nums[i];
if (!helperSet.has(tempVal)) {
route.push(tempVal);
helperSet.add(tempVal);
backTracking(nums, route);
route.pop();
helperSet.delete(tempVal);
}
}
}
};
```
### C
```c

View File

@ -292,6 +292,34 @@ var permuteUnique = function (nums) {
```
### TypeScript
```typescript
function permuteUnique(nums: number[]): number[][] {
nums.sort((a, b) => a - b);
const resArr: number[][] = [];
const usedArr: boolean[] = new Array(nums.length).fill(false);
backTracking(nums, []);
return resArr;
function backTracking(nums: number[], route: number[]): void {
if (route.length === nums.length) {
resArr.push(route.slice());
return;
}
for (let i = 0, length = nums.length; i < length; i++) {
if (i > 0 && nums[i] === nums[i - 1] && usedArr[i - 1] === false) continue;
if (usedArr[i] === false) {
route.push(nums[i]);
usedArr[i] = true;
backTracking(nums, route);
usedArr[i] = false;
route.pop();
}
}
}
};
```
### Swift
```swift

View File

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

View File

@ -272,7 +272,28 @@ var subsets = function(nums) {
};
```
## TypeScript
```typescript
function subsets(nums: number[]): number[][] {
const resArr: number[][] = [];
backTracking(nums, 0, []);
return resArr;
function backTracking(nums: number[], startIndex: number, route: number[]): void {
resArr.push(route.slice());
let length = nums.length;
if (startIndex === length) return;
for (let i = startIndex; i < length; i++) {
route.push(nums[i]);
backTracking(nums, i + 1, route);
route.pop();
}
}
};
```
## C
```c
int* path;
int pathTop;

View File

@ -318,6 +318,28 @@ var subsetsWithDup = function(nums) {
```
### TypeScript
```typescript
function subsetsWithDup(nums: number[]): number[][] {
nums.sort((a, b) => a - b);
const resArr: number[][] = [];
backTraking(nums, 0, []);
return resArr;
function backTraking(nums: number[], startIndex: number, route: number[]): void {
resArr.push(route.slice());
let length: number = nums.length;
if (startIndex === length) return;
for (let i = startIndex; i < length; i++) {
if (i > startIndex && nums[i] === nums[i - 1]) continue;
route.push(nums[i]);
backTraking(nums, i + 1, route);
route.pop();
}
}
};
```
### C
```c
@ -387,7 +409,7 @@ int** subsetsWithDup(int* nums, int numsSize, int* returnSize, int** returnColum
}
```
## Swift
### Swift
```swift
func subsetsWithDup(_ nums: [Int]) -> [[Int]] {

View File

@ -455,6 +455,45 @@ var restoreIpAddresses = function(s) {
};
```
## TypeScript
```typescript
function isValidIpSegment(str: string): boolean {
let resBool: boolean = true;
let tempVal: number = Number(str);
if (
str.length === 0 || isNaN(tempVal) ||
tempVal > 255 || tempVal < 0 ||
(str.length > 1 && str[0] === '0')
) {
resBool = false;
}
return resBool;
}
function restoreIpAddresses(s: string): string[] {
const resArr: string[] = [];
backTracking(s, 0, []);
return resArr;
function backTracking(s: string, startIndex: number, route: string[]): void {
let length: number = s.length;
if (route.length === 4 && startIndex >= length) {
resArr.push(route.join('.'));
return;
}
if (route.length === 4 || startIndex >= length) return;
let tempStr: string = '';
for (let i = startIndex + 1; i <= Math.min(length, startIndex + 3); i++) {
tempStr = s.slice(startIndex, i);
if (isValidIpSegment(tempStr)) {
route.push(s.slice(startIndex, i));
backTracking(s, i, route);
route.pop();
}
}
}
};
```
## Go
回溯(对于前导 0的IP特别注意s[startIndex]=='0'的判断不应该写成s[startIndex]==0因为s截取出来不是数字

View File

@ -408,6 +408,27 @@ class Solution:
return True
```
```python
# 遵循Carl的写法只添加了节点判断的部分
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
# method 2
que, pre = [], None
while root or que:
while root:
que.append(root)
root = root.left
root = que.pop()
# 对第一个节点只做记录,对后面的节点进行比较
if pre is None:
pre = root.val
else:
if pre >= root.val: return False
pre = root.val
root = root.right
return True
```
## Go
```Go

View File

@ -437,41 +437,6 @@ class Solution:
return True
```
层序遍历
```python
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
if not root: return True
que, cnt = [[root.left, root.right]], 1
while que:
nodes, tmp, sign = que.pop(), [], False
for node in nodes:
if not node:
tmp.append(None)
tmp.append(None)
else:
if node.left:
tmp.append(node.left)
sign = True
else:
tmp.append(None)
if node.right:
tmp.append(node.right)
sign = True
else:
tmp.append(None)
p1, p2 = 0, len(nodes) - 1
while p1 < p2:
if (not nodes[p1] and nodes[p2]) or (nodes[p1] and not nodes[p2]): return False
elif nodes[p1] and nodes[p2] and nodes[p1].val != nodes[p2].val: return False
p1 += 1
p2 -= 1
if sign: que.append(tmp)
cnt += 1
return True
```
## Go
```go

View File

@ -790,7 +790,7 @@ func findRootIndex(target int,inorder []int) int{
```javascript
var buildTree = function(inorder, postorder) {
if (!preorder.length) return null;
if (!inorder.length) return null;
const rootVal = postorder.pop(); // 从后序遍历的数组中获取中间节点的值, 即数组最后一个值
let rootIndex = inorder.indexOf(rootVal); // 获取中间节点在中序遍历中的下标
const root = new TreeNode(rootVal); // 创建中间节点

View File

@ -355,6 +355,7 @@ func sortedArrayToBST(nums []int) *TreeNode {
```
## JavaScript
递归
```javascript
var sortedArrayToBST = function (nums) {
@ -372,7 +373,44 @@ var sortedArrayToBST = function (nums) {
return buildTree(nums, 0, nums.length - 1);
};
```
迭代
```JavaScript
var sortedArrayToBST = function(nums) {
if(nums.length===0){
return null;
}
let root=new TreeNode(0); //初始根节点
let nodeQue=[root]; //放遍历的节点,并初始化
let leftQue=[0]; //放左区间的下标,初始化
let rightQue=[nums.length-1]; // 放右区间的下标
while(nodeQue.length){
let curNode=nodeQue.pop();
let left=leftQue.pop();
let right=rightQue.pop();
let mid=left+Math.floor((right-left)/2);
curNode.val=nums[mid]; //将下标为mid的元素给中间节点
// 处理左区间
if(left<=mid-1){
curNode.left=new TreeNode(0);
nodeQue.push(curNode.left);
leftQue.push(left);
rightQue.push(mid-1);
}
// 处理右区间
if(right>=mid+1){
curNode.right=new TreeNode(0);
nodeQue.push(curNode.right);
leftQue.push(mid+1);
rightQue.push(right);
}
}
return root;
};
```
## TypeScript
```typescript

View File

@ -148,8 +148,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n × 5)$
* 时间复杂度O(n)
* 空间复杂度O(n × 5)
当然大家可以看到力扣官方题解里的一种优化空间写法我这里给出对应的C++版本:
@ -173,8 +173,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
* 时间复杂度O(n)
* 空间复杂度O(1)
大家会发现dp[2]利用的是当天的dp[1]。 但结果也是对的。

View File

@ -134,7 +134,29 @@ public int ladderLength(String beginWord, String endWord, List<String> wordList)
```
## Python
```
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
wordSet = set(wordList)
if len(wordSet)== 0 or endWord not in wordSet:
return 0
mapping = {beginWord:1}
queue = deque([beginWord])
while queue:
word = queue.popleft()
path = mapping[word]
for i in range(len(word)):
word_list = list(word)
for j in range(26):
word_list[i] = chr(ord('a')+j)
newWord = "".join(word_list)
if newWord == endWord:
return path+1
if newWord in wordSet and newWord not in mapping:
mapping[newWord] = path+1
queue.append(newWord)
return 0
```
## Go
## JavaScript

View File

@ -450,6 +450,38 @@ var partition = function(s) {
};
```
## TypeScript
```typescript
function partition(s: string): string[][] {
function isPalindromeStr(s: string, left: number, right: number): boolean {
while (left < right) {
if (s[left++] !== s[right--]) {
return false;
}
}
return true;
}
function backTracking(s: string, startIndex: number, route: string[]): void {
let length: number = s.length;
if (length === startIndex) {
resArr.push(route.slice());
return;
}
for (let i = startIndex; i < length; i++) {
if (isPalindromeStr(s, startIndex, i)) {
route.push(s.slice(startIndex, i + 1));
backTracking(s, i + 1, route);
route.pop();
}
}
}
const resArr: string[][] = [];
backTracking(s, 0, []);
return resArr;
};
```
## C
```c

View File

@ -239,6 +239,30 @@ class Solution {
### Python
```python
# 解法1
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
n = len(gas)
cur_sum = 0
min_sum = float('inf')
for i in range(n):
cur_sum += gas[i] - cost[i]
min_sum = min(min_sum, cur_sum)
if cur_sum < 0: return -1
if min_sum >= 0: return 0
for j in range(n - 1, 0, -1):
min_sum += gas[j] - cost[j]
if min_sum >= 0:
return j
return -1
```
```python
# 解法2
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
start = 0

View File

@ -207,7 +207,7 @@ public:
};
```
* 时间复杂度O(n^3)因为substr返回子串的副本是$O(n)$的复杂度这里的n是substring的长度
* 时间复杂度O(n^3)因为substr返回子串的副本是O(n)的复杂度这里的n是substring的长度
* 空间复杂度O(n)

View File

@ -106,6 +106,21 @@ class Solution:
## Go
```go
func hasCycle(head *ListNode) bool {
if head==nil{
return false
} //空链表一定不会有环
fast:=head
slow:=head //快慢指针
for fast.Next!=nil&&fast.Next.Next!=nil{
fast=fast.Next.Next
slow=slow.Next
if fast==slow{
return true //快慢指针相遇则有环
}
}
return false
}
```
### JavaScript

View File

@ -79,7 +79,7 @@ void removeExtraSpaces(string& s) {
逻辑很简单从前向后遍历遇到空格了就erase。
如果不仔细琢磨一下erase的时间复杂读还以为以上的代码是$O(n)$的时间复杂度呢。
如果不仔细琢磨一下erase的时间复杂读还以为以上的代码是O(n)的时间复杂度呢。
想一下真正的时间复杂度是多少一个erase本来就是O(n)的操作erase实现原理题目[数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html)最优的算法来移除元素也要O(n)。
@ -222,7 +222,44 @@ public:
效率:
<img src='https://code-thinking.cdn.bcebos.com/pics/151_翻转字符串里的单词.png' width=600> </img></div>
```CPP
//版本二:
//原理同版本1更简洁实现。
class Solution {
public:
void reverse(string& s, int start, int end){ //翻转,区间写法:闭区间 []
for (int i = start, j = end; i < j; i++, j--) {
swap(s[i], s[j]);
}
}
void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。
int slow = 0; //整体思想参考Leetcode: 27. 移除元素https://leetcode-cn.com/problems/remove-element/
for (int i = 0; i < s.size(); ++i) { //
if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。
if (slow != 0) s[slow++] = ' '; //手动控制空格给单词之间添加空格。slow != 0说明不是第一个单词需要在单词前添加空格。
while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。
s[slow++] = s[i++];
}
}
}
s.resize(slow); //slow的大小即为去除多余空格后的大小。
}
string reverseWords(string s) {
removeExtraSpaces(s); //去除多余空格,保证单词之间之只有一个空格,且字符串首尾没空格。
reverse(s, 0, s.size() - 1);
int start = 0; //removeExtraSpaces后保证第一个单词的开始下标一定是0。
for (int i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。
reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转。
start = i + 1; //更新下一个单词的开始下标start
}
}
return s;
}
};
```
## 其他语言版本

View File

@ -124,6 +124,19 @@ class Solution:
## Go
```go
func rotate(nums []int, k int) {
l:=len(nums)
index:=l-k%l
reverse(nums)
reverse(nums[:l-index])
reverse(nums[l-index:])
}
func reverse(nums []int){
l:=len(nums)
for i:=0;i<l/2;i++{
nums[i],nums[l-1-i]=nums[l-1-i],nums[i]
}
}
```
## JavaScript

View File

@ -315,5 +315,75 @@ class Solution {
}
```
C:
```C
typedef struct HashNodeTag {
int key; /* num */
struct HashNodeTag *next;
}HashNode;
/* Calcualte the hash key */
static inline int hash(int key, int size) {
int index = key % size;
return (index > 0) ? (index) : (-index);
}
/* Calculate the sum of the squares of its digits*/
static inline int calcSquareSum(int num) {
unsigned int sum = 0;
while(num > 0) {
sum += (num % 10) * (num % 10);
num = num/10;
}
return sum;
}
#define HASH_TABLE_SIZE (32)
bool isHappy(int n){
int sum = n;
int index = 0;
bool bHappy = false;
bool bExit = false;
/* allocate the memory for hash table with chaining method*/
HashNode ** hashTable = (HashNode **)calloc(HASH_TABLE_SIZE, sizeof(HashNode));
while(bExit == false) {
/* check if n has been calculated */
index = hash(n, HASH_TABLE_SIZE);
HashNode ** p = hashTable + index;
while((*p) && (bExit == false)) {
/* Check if this num was calculated, if yes, this will be endless loop */
if((*p)->key == n) {
bHappy = false;
bExit = true;
}
/* move to next node of the same index */
p = &((*p)->next);
}
/* put n intot hash table */
HashNode * newNode = (HashNode *)malloc(sizeof(HashNode));
newNode->key = n;
newNode->next = NULL;
*p = newNode;
sum = calcSquareSum(n);
if(sum == 1) {
bHappy = true;
bExit = true;
}
else {
n = sum;
}
}
return bHappy;
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -396,6 +396,30 @@ var combinationSum3 = function(k, n) {
};
```
## TypeScript
```typescript
function combinationSum3(k: number, n: number): number[][] {
const resArr: number[][] = [];
function backTracking(k: number, n: number, sum: number, startIndex: number, tempArr: number[]): void {
if (sum > n) return;
if (tempArr.length === k) {
if (sum === n) {
resArr.push(tempArr.slice());
}
return;
}
for (let i = startIndex; i <= 9 - (k - tempArr.length) + 1; i++) {
tempArr.push(i);
backTracking(k, n, sum + i, i + 1, tempArr);
tempArr.pop();
}
}
backTracking(k, n, 0, 1, []);
return resArr;
};
```
## C
```c

View File

@ -506,6 +506,50 @@ var findItinerary = function(tickets) {
```
### TypeScript
```typescript
function findItinerary(tickets: string[][]): string[] {
/**
TicketsMap 实例:
{ NRT: Map(1) { 'JFK' => 1 }, JFK: Map(2) { 'KUL' => 1, 'NRT' => 1 } }
这里选择Map数据结构的原因是与Object类型的一个主要差异是Map实例会维护键值对的插入顺序。
*/
type TicketsMap = {
[index: string]: Map<string, number>
};
tickets.sort((a, b) => {
return a[1] < b[1] ? -1 : 1;
});
const ticketMap: TicketsMap = {};
for (const [from, to] of tickets) {
if (ticketMap[from] === undefined) {
ticketMap[from] = new Map();
}
ticketMap[from].set(to, (ticketMap[from].get(to) || 0) + 1);
}
const resRoute = ['JFK'];
backTracking(tickets.length, ticketMap, resRoute);
return resRoute;
function backTracking(ticketNum: number, ticketMap: TicketsMap, route: string[]): boolean {
if (route.length === ticketNum + 1) return true;
const targetMap = ticketMap[route[route.length - 1]];
if (targetMap !== undefined) {
for (const [to, count] of targetMap.entries()) {
if (count > 0) {
route.push(to);
targetMap.set(to, count - 1);
if (backTracking(ticketNum, ticketMap, route) === true) return true;
targetMap.set(to, count);
route.pop();
}
}
}
return false;
}
};
```
### Swift
直接迭代tickets数组
@ -626,5 +670,77 @@ for line in tickets {
}
```
### Go
```Go
// 先排序,然后找到第一条路径即可返回
func findItinerary(tickets [][]string) []string {
var path []string // 用来保存搜索的路径
data := make(map[string]ticketSlice) // 用来保存tickets排序后的结果
var search func(airport string) bool
search = func(airport string) bool {
if len(path) == len(tickets) {
path = append(path, airport)
return true
}
to := data[airport]
for _, item := range to {
if item.Count == 0 {
// 已用完
continue
}
path = append(path, airport)
item.Count--
if search(item.To) { return true }
item.Count++
path = path[:len(path) - 1]
}
return false
}
// 排序
// 感觉这段代码有点啰嗦,不知道能不能简化一下
tmp := make(map[string]map[string]int)
for _, ticket := range tickets {
if to, ok := tmp[ticket[0]]; ok {
if _, ok2 := to[ticket[1]]; ok2 {
to[ticket[1]]++
} else {
to[ticket[1]] = 1
}
} else {
tmp[ticket[0]] = map[string]int{
ticket[1]: 1,
}
}
}
for from, to := range tmp {
var tmp ticketSlice
for to, num := range to {
tmp = append(tmp, &ticketStat{To: to, Count: num})
}
sort.Sort(tmp)
data[from] = tmp
}
search("JFK")
return path
}
type ticketStat struct {
To string
Count int
}
type ticketSlice []*ticketStat
func (p ticketSlice) Len() int { return len(p) }
func (p ticketSlice) Less(i, j int) bool { return strings.Compare(p[i].To, p[j].To) == -1 }
func (p ticketSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

View File

@ -119,8 +119,8 @@ public:
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$
* 时间复杂度O(n^2)
* 空间复杂度O(n)
### 贪心

View File

@ -281,6 +281,38 @@ impl Solution {
}
}
```
C:
```C
int* intersection1(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){
int nums1Cnt[1000] = {0};
int lessSize = nums1Size < nums2Size ? nums1Size : nums2Size;
int * result = (int *) calloc(lessSize, sizeof(int));
int resultIndex = 0;
int* tempNums;
int i;
/* Calculate the number's counts for nums1 array */
for(i = 0; i < nums1Size; i ++) {
nums1Cnt[nums1[i]]++;
}
/* Check if the value in nums2 is existing in nums1 count array */
for(i = 0; i < nums2Size; i ++) {
if(nums1Cnt[nums2[i]] > 0) {
result[resultIndex] = nums2[i];
resultIndex ++;
/* Clear this count to avoid duplicated value */
nums1Cnt[nums2[i]] = 0;
}
}
* returnSize = resultIndex;
return result;
}
```
## 相关题目
* 350.两个数组的交集 II

View File

@ -208,6 +208,75 @@ class Solution {
}
```
```java
public class Solution {
public static void main(String[] args) {
int num[] = {1,5,11,5};
canPartition(num);
}
public static boolean canPartition(int[] nums) {
int len = nums.length;
// 题目已经说非空数组,可以不做非空判断
int sum = 0;
for (int num : nums) {
sum += num;
}
// 特判:如果是奇数,就不符合要求
if ((sum %2 ) != 0) {
return false;
}
int target = sum / 2; //目标背包容量
// 创建二维状态数组,行:物品索引,列:容量(包括 0
/*
dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数
每个数只能用一次,使得这些数的和恰好等于 j。
*/
boolean[][] dp = new boolean[len][target + 1];
// 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满 这里的dp[][]数组的含义就是“恰好”,所以就算容积比它大的也不要)
if (nums[0] <= target) {
dp[0][nums[0]] = true;
}
// 再填表格后面几行
//外层遍历物品
for (int i = 1; i < len; i++) {
//内层遍历背包
for (int j = 0; j <= target; j++) {
// 直接从上一行先把结果抄下来,然后再修正
dp[i][j] = dp[i - 1][j];
//如果某个物品单独的重量恰好就等于背包的重量那么也是满足dp数组的定义的
if (nums[i] == j) {
dp[i][j] = true;
continue;
}
//如果某个物品的重量小于j那就可以看该物品是否放入背包
//dp[i - 1][j]表示该物品不放入背包,如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true
//dp[i - 1][j - nums[i]]表示该物品放入背包。如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。
if (nums[i] < j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
for (int i = 0; i < len; i++) {
for (int j = 0; j <= target; j++) {
System.out.print(dp[i][j]+" ");
}
System.out.println();
}
return dp[len - 1][target];
}
}
//dp数组的打印结果
false true false false false false false false false false false false
false true false false false true true false false false false false
false true false false false true true false false false false true
false true false false false true true false false false true true
```
二维数组版本(易于理解):
```Java
class Solution {

View File

@ -51,7 +51,7 @@ if (root == nullptr) return root;
* 确定单层递归的逻辑
这里就把平衡二叉树中删除节点遇到的情况都搞清楚。
这里就把二叉搜索树中删除节点遇到的情况都搞清楚。
有以下五种情况:

View File

@ -396,7 +396,35 @@ var findSubsequences = function(nums) {
```
## TypeScript
```typescript
function findSubsequences(nums: number[]): number[][] {
const resArr: number[][] = [];
backTracking(nums, 0, []);
return resArr;
function backTracking(nums: number[], startIndex: number, route: number[]): void {
let length: number = nums.length;
if (route.length >= 2) {
resArr.push(route.slice());
}
const usedSet: Set<number> = new Set();
for (let i = startIndex; i < length; i++) {
if (
nums[i] < route[route.length - 1] ||
usedSet.has(nums[i])
) continue;
usedSet.add(nums[i]);
route.push(nums[i]);
backTracking(nums, i + 1, route);
route.pop();
}
}
};
```
### C
```c
int* path;
int pathTop;

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

@ -277,7 +277,30 @@ int fib(int n){
return fib(n-1) + fib(n-2);
}
```
### Rust
动态规划:
```Rust
pub fn fib(n: i32) -> i32 {
let n = n as usize;
let mut dp = vec![0; 31];
dp[1] = 1;
for i in 2..=n {
dp[i] = dp[i - 1] + dp[i - 2];
}
dp[n]
}
```
递归实现:
```Rust
pub fn fib(n: i32) -> i32 {
//若n小于等于1返回n
f n <= 1 {
return n;
}
//否则返回fib(n-1) + fib(n-2)
return fib(n - 1) + fib(n - 2);
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>

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)的区别。

View File

@ -276,7 +276,7 @@ func search(nums []int, target int) int {
```
**JavaScript:**
(版本一)左闭右闭区间
(版本一)左闭右闭区间 [left, right]
```js
/**
@ -285,10 +285,12 @@ func search(nums []int, target int) int {
* @return {number}
*/
var search = function(nums, target) {
// right是数组最后一个数的下标num[right]在查找范围内,是左闭右闭区间
let left = 0, right = nums.length - 1;
// 使用左闭右闭区间
// 当left=right时由于nums[right]在查找范围内,所以要包括此情况
while (left <= right) {
let mid = left + Math.floor((right - left)/2);
// 如果中间数大于目标值要把中间数排除查找范围所以右边界更新为mid-1如果右边界更新为mid那中间数还在下次查找范围内
if (nums[mid] > target) {
right = mid - 1; // 去左面闭区间寻找
} else if (nums[mid] < target) {
@ -300,7 +302,7 @@ var search = function(nums, target) {
return -1;
};
```
(版本二)左闭右开区间
(版本二)左闭右开区间 [left, right)
```js
/**
@ -309,10 +311,13 @@ var search = function(nums, target) {
* @return {number}
*/
var search = function(nums, target) {
// right是数组最后一个数的下标+1nums[right]不在查找范围内,是左闭右开区间
let left = 0, right = nums.length;
// 使用左闭右开区间 [left, right)
// left=right由于nums[right]不在查找范围,所以不必包括此情况
while (left < right) {
let mid = left + Math.floor((right - left)/2);
// 如果中间值大于目标值,中间值不应在下次查找的范围内,但中间值的前一个值应在;
// 由于right本来就不在查找范围内所以将右边界更新为中间值如果更新右边界为mid-1则将中间值的前一个值也踢出了下次寻找范围
if (nums[mid] > target) {
right = mid; // 去左区间寻找
} else if (nums[mid] < target) {

View File

@ -38,8 +38,8 @@
使用贪心算法的性能是
* 时间复杂度$O(n)$
* 空间复杂度$O(1)$
* 时间复杂度O(n)
* 空间复杂度O(1)
那么我们再来看看是使用动规的方法如何解题
@ -87,8 +87,8 @@ public:
};
```
* 时间复杂度$O(n)$
* 空间复杂度$O(n)$
* 时间复杂度O(n)
* 空间复杂度O(n)
## 其他语言版本

View File

@ -71,7 +71,7 @@ public:
}
};
```
* 时间复杂度:O(n + m)n为S的长度m为T的长度 ,也可以理解是$O(n)$的时间复杂度
* 时间复杂度:O(n + m)n为S的长度m为T的长度 也可以理解是O(n)的时间复杂度
* 空间复杂度:O(n + m)
当然以上代码大家可以发现有重复的逻辑处理S处理T可以把这块公共逻辑抽离出来代码精简如下
@ -185,6 +185,36 @@ class Solution {
}
```
双指针:
```java
class Solution {
public static boolean backspaceCompare(String s, String t) {
char[] sarray = s.toCharArray();
char[] tarray = t.toCharArray();
return generate(sarray).equals(generate(tarray));
}
public static String generate(char[] a){
int slow = -1;
int fast = 0;
if(a.length == 1){
return new String(a);
} else{
for(fast = 0; fast < a.length; fast++){
if(a[fast] != '#')
a[++slow] = a[fast];
else{
if(slow >= 0)
slow--;
}
}
return new String(a,0,slow + 1);
}
}
}
```
### python

View File

@ -112,7 +112,7 @@ public:
* 时间复杂度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

@ -148,7 +148,7 @@ O(nlogn)的算法1s内大概计算机可以运行 2 * (10^7)次计算,符
![程序超时1](https://img-blog.csdnimg.cn/20201208231559175.png)
至于$O(\log n)$和$O(n^3)$ 等等这些时间复杂度在1s内可以处理的多大的数据规模大家可以自己写一写代码去测一下了。
至于O(log n)O(n^3) 等等这些时间复杂度在1s内可以处理的多大的数据规模大家可以自己写一写代码去测一下了。
# 完整测试代码

View File

@ -24,7 +24,7 @@
算法导论给出的解释:**大O用来表示上界的**,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。
同样算法导论给出了例子:拿插入排序来说,插入排序的时间复杂度我们都说是$O(n^2)$
同样算法导论给出了例子拿插入排序来说插入排序的时间复杂度我们都说是O(n^2) 。
输入数据的形式对程序运算时间是有很大影响的在数据本来有序的情况下时间复杂度是O(n)但如果数据是逆序的话插入排序的时间复杂度就是O(n^2)也就对于所有输入情况来说最坏是O(n^2) 的时间复杂度所以称插入排序的时间复杂度为O(n^2)。
@ -44,7 +44,7 @@
![时间复杂度,不同数据规模的差异](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)是更优的,所花费的时间也是最少的。
@ -125,7 +125,7 @@ O(2 × n^2 + 10 × n + 1000) < O(3 × n^2),所以说最后省略掉常数项
如果是暴力枚举的话时间复杂度是多少呢是O(n^2)
这里一些同学会忽略了字符串比较的时间消耗这里并不像int 型数字做比较那么简单除了n^2次的遍历次数外字符串比较依然要消耗m次操作m也就是字母串的长度所以时间复杂度是$O(m × n × n)$
这里一些同学会忽略了字符串比较的时间消耗这里并不像int 型数字做比较那么简单除了n^2次的遍历次数外字符串比较依然要消耗m次操作m也就是字母串的长度所以时间复杂度是O(m × n × n)。
接下来再想一下其他解题思路

View File

@ -9,7 +9,7 @@
* 什么是大O
* 不同数据规模的差异
* 复杂表达式的化简
* $O(\log n)$中的log是以什么为底
* O(log n)中的log是以什么为底
* 举一个例子
@ -23,21 +23,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(n\log n)$,但是当数据已经有序情况下,快速排序的时间复杂度是$O(n^2)$ 的,**所以严格从大O的定义来讲快速排序的时间复杂度应该是$O(n^2)$**。
同样的同理再看一下快速排序都知道快速排序是O(nlogn)但是当数据已经有序情况下快速排序的时间复杂度是O(n^2) 的,**所以严格从大O的定义来讲快速排序的时间复杂度应该是O(n^2)**。
**但是我们依然说快速排序是$O(n\log n)$的时间复杂度这个就是业内的一个默认规定这里说的O代表的就是一般情况而不是严格的上界**。如图所示:
**但是我们依然说快速排序是O(nlogn)的时间复杂度这个就是业内的一个默认规定这里说的O代表的就是一般情况而不是严格的上界**。如图所示:
![时间复杂度4一般情况下的时间复杂度](https://img-blog.csdnimg.cn/20200728185745611.png)
我们主要关心的还是一般情况下的数据形式。
@ -51,11 +51,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就是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度这个数据量也就是常数项系数已经不起决定性作用的数据量**。
@ -63,13 +63,13 @@
**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示**
O(1)常数阶 < $O(\log n)$对数阶 < $O(n)$线性阶 < $O(n^2)$平方阶 < $O(n^3)$立方阶 < $O(2^n)$指数阶
O(1)常数阶 < O(logn)对数阶 < 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)
@ -95,19 +95,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(\log n)$中的log是以什么为底
## O(logn)中的log是以什么为底
平时说这个算法的时间复杂度是logn的那么一定是log 以2为底n的对数么
@ -130,21 +130,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(n\log n)$依然要考虑字符串的长度是m那么快速排序每次的比较都要有m次的字符比较的操作就是$O(m × n × \log n)$
那看看这种算法的时间复杂度快速排序时间复杂度为O(nlogn)依然要考虑字符串的长度是m那么快速排序每次的比较都要有m次的字符比较的操作就是O(m × n × log n)
之后还要遍历一遍这n个字符串找出两个相同的字符串别忘了遍历的时候依然要比较字符串所以总共的时间复杂度是 $O(m × n × \log n + n × m)$
之后还要遍历一遍这n个字符串找出两个相同的字符串别忘了遍历的时候依然要比较字符串所以总共的时间复杂度是 O(m × n × logn + n × m)。
我们对$O(m × n × \log n + n × m)$ 进行简化操作$m × n$提取出来变成 $O(m × n × (\log n + 1)$)再省略常数项最后的时间复杂度是 $O(m × n × \log n)$
我们对O(m × n × log n + n × m) 进行简化操作把m × n提取出来变成 O(m × n × (logn + 1))再省略常数项最后的时间复杂度是 O(m × n × log n)。
最后很明显$O(m × n × \log n)$ 要优于$O(m × n × n)$
最后很明显O(m × n × logn) 要优于O(m × n × n)
所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快

View File

@ -3,14 +3,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来表示。利用程序的空间复杂度可以对程序运行中需要多少内存有个预先估计。
@ -41,11 +41,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++) {
@ -53,9 +53,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(\log n)$呢?**
其他的 O(n^2) O(n^3) 我想大家应该都可以以此例举出来了,**那么思考一下 什么时候空间复杂度是 O(logn)呢?**
空间复杂度是logn的情况确实有些特殊其实是在**递归的时候会出现空间复杂度为logn的情况**。

View File

@ -13,7 +13,7 @@
- 左孩子和右孩子的下标不太好理解。我给出证明过程:
如果父节点在第$k$层,第$m,m \in [0,2^k]$个节点,则其左孩子所在的位置必然为$k+1$层,第$2*(m-1)+1$个节点。
如果父节点在第k层,第$m,m \in [0,2^k]$个节点,则其左孩子所在的位置必然为$k+1$层,第$2*(m-1)+1$个节点。
- 计算父节点在数组中的索引:
$$

View File

@ -26,7 +26,7 @@ int fibonacci(int i) {
在讲解递归时间复杂度的时候,我们提到了递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归的时间复杂度**
可以看出上面的代码每次递归都是$O(1)$的操作。再来看递归了多少次这里将i为5作为输入的递归过程 抽象成一棵递归树,如图:
可以看出上面的代码每次递归都是O(1)的操作。再来看递归了多少次这里将i为5作为输入的递归过程 抽象成一棵递归树,如图:
![递归空间复杂度分析](https://img-blog.csdnimg.cn/20210305093200104.png)
@ -36,7 +36,7 @@ int fibonacci(int i) {
我们之前也有说到一棵深度按根节点深度为1为k的二叉树最多可以有 2^k - 1 个节点。
所以该递归算法的时间复杂度为$O(2^n)$这个复杂度是非常大的随着n的增大耗时是指数上升的。
所以该递归算法的时间复杂度为O(2^n)这个复杂度是非常大的随着n的增大耗时是指数上升的。
来做一个实验,大家可以有一个直观的感受。
@ -85,7 +85,7 @@ int main()
* n = 40耗时837 ms
* n = 50耗时110306 ms
可以看出,$O(2^n)$这种指数级别的复杂度是非常大的。
可以看出O(2^n)这种指数级别的复杂度是非常大的。
所以这种求斐波那契数的算法看似简洁,其实时间复杂度非常高,一般不推荐这样来实现斐波那契。
@ -119,14 +119,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个斐波那契数的话递归调用栈的深度就是n。
那么每次递归的空间复杂度是$O(1)$ 调用栈深度为n所以这段递归代码的空间复杂度就是$O(n)$
那么每次递归的空间复杂度是O(1) 调用栈深度为n所以这段递归代码的空间复杂度就是O(n)。
```CPP
int fibonacci(int i) {
@ -233,24 +233,24 @@ int binary_search( int arr[], int l, int r, int x) {
}
```
都知道二分查找的时间复杂度是$O(\log n)$,那么递归二分查找的空间复杂度是多少呢?
都知道二分查找的时间复杂度是O(logn),那么递归二分查找的空间复杂度是多少呢?
我们依然看 **每次递归的空间复杂度和递归的深度**
每次递归的空间复杂度可以看出主要就是参数里传入的这个arr数组但需要注意的是在C/C++中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入的数组首元素地址。
**也就是说每一层递归都是公用一块数组地址空间的**,所以 每次递归的空间复杂度是常数即:$O(1)$
**也就是说每一层递归都是公用一块数组地址空间的**,所以 每次递归的空间复杂度是常数即O(1)。
再来看递归的深度二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 $1 * logn = O(logn)$
再来看递归的深度二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * logn = O(logn)。
大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是$O(n\log n)$
大家要注意自己所用的语言在传递函数参数的时是拷贝整个数值还是拷贝地址如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)。
## 总结
本章我们详细分析了递归实现的求斐波那契和二分法的空间复杂度,同时也对时间复杂度做了分析。
特别是两种递归实现的求斐波那契数列,其时间复杂度截然不容,我们还做了实验,验证了时间复杂度为$O(2^n)$是非常耗时的。
特别是两种递归实现的求斐波那契数列其时间复杂度截然不容我们还做了实验验证了时间复杂度为O(2^n)是非常耗时的。
通过本篇大家应该对递归算法的时间复杂度和空间复杂度有更加深刻的理解了。

View File

@ -5,13 +5,13 @@
相信很多同学对递归算法的时间复杂度都很模糊,那么这篇来给大家通透的讲一讲。
**同一道题目,同样使用递归算法,有的同学会写出了$O(n)$的代码,有的同学就写出了$O(\log n)$的代码**
**同一道题目同样使用递归算法有的同学会写出了O(n)的代码有的同学就写出了O(logn)的代码**
这是为什么呢?
如果对递归的时间复杂度理解的不够深入的话,就会这样!
那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了$O(n)$的代码。
那么我通过一道简单的面试题模拟面试的场景来带大家逐步分析递归算法的时间复杂度最后找出最优解来看看同样是递归怎么就写成了O(n)的代码。
面试题求x的n次方
@ -26,7 +26,7 @@ int function1(int x, int n) {
return result;
}
```
时间复杂度为$O(n)$,此时面试官会说,有没有效率更好的算法呢。
时间复杂度为O(n),此时面试官会说,有没有效率更好的算法呢。
**如果此时没有思路,不要说:我不会,我不知道了等等**
@ -44,11 +44,11 @@ int function2(int x, int n) {
```
面试官问:“那么这个代码的时间复杂度是多少?”。
一些同学可能一看到递归就想到了$O(\log n)$,其实并不是这样,递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归中的操作次数**
一些同学可能一看到递归就想到了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)。
这个时间复杂度就没有达到面试官的预期。于是又写出了如下的递归算法的代码:
@ -81,11 +81,11 @@ int function3(int x, int n) {
![递归求时间复杂度](https://img-blog.csdnimg.cn/20200728195531892.png)
**时间复杂度忽略掉常数项`-1`之后,这个递归算法的时间复杂度依然是$O(n)$**。对,你没看错,依然是$O(n)$的时间复杂度!
**时间复杂度忽略掉常数项`-1`之后这个递归算法的时间复杂度依然是O(n)**。对你没看错依然是O(n)的时间复杂度!
此时面试官就会说:“这个递归的算法依然还是$O(n)$啊”, 很明显没有达到面试官的预期。
此时面试官就会说“这个递归的算法依然还是O(n)啊”, 很明显没有达到面试官的预期。
那么$O(\log n)$的递归算法应该怎么写呢?
那么O(logn)的递归算法应该怎么写呢?
想一想刚刚给出的那份递归算法的代码,是不是有哪里比较冗余呢,其实有重复计算的部分。
@ -108,7 +108,7 @@ int function4(int x, int n) {
依然还是看他递归了多少次可以看到这里仅仅有一个递归调用且每次都是n/2 所以这里我们一共调用了log以2为底n的对数次。
**每次递归了做都是一次乘法操作,这也是一个常数项的操作,那么这个递归算法的时间复杂度才是真正的$O(\log n)$**
**每次递归了做都是一次乘法操作这也是一个常数项的操作那么这个递归算法的时间复杂度才是真正的O(logn)**
此时大家最后写出了这样的代码并且将时间复杂度分析的非常清晰,相信面试官是比较满意的。
@ -116,11 +116,11 @@ int function4(int x, int n) {
对于递归的时间复杂度,毕竟初学者有时候会迷糊,刷过很多题的老手依然迷糊。
**本篇我用一道非常简单的面试题目求x的n次方来逐步分析递归算法的时间复杂度注意不要一看到递归就想到了$O(\log n)$**
**本篇我用一道非常简单的面试题目求x的n次方来逐步分析递归算法的时间复杂度注意不要一看到递归就想到了O(logn)**
同样使用递归,有的同学可以写出$O(\log n)$的代码,有的同学还可以写出$O(n)$的代码。
同样使用递归有的同学可以写出O(logn)的代码有的同学还可以写出O(n)的代码。
对于function3 这样的递归实现,很容易让人感觉这是$O(\log n)$的时间复杂度,其实这是$O(n)$的算法!
对于function3 这样的递归实现很容易让人感觉这是O(log n)的时间复杂度其实这是O(n)的算法!
```CPP
int function3(int x, int n) {

View File

@ -86,7 +86,7 @@ public:
# 题外话
一些同学热衷于使用substr来做这道题。
其实使用substr 和 反转 时间复杂度是一样的 ,都是$O(n)$但是使用substr申请了额外空间所以空间复杂度是$O(n)$,而反转方法的空间复杂度是$O(1)$
其实使用substr 和 反转 时间复杂度是一样的 都是O(n)但是使用substr申请了额外空间所以空间复杂度是O(n)而反转方法的空间复杂度是O(1)。
**如果想让这套题目有意义,就不要申请额外空间。**

View File

@ -89,7 +89,7 @@ for (int i = 0; i < array.size(); i++) {
# 总结
本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目除了链表一些题目一定要使用双指针其他题目都是使用双指针来提高效率一般是将$O(n^2)$的时间复杂度,降为$O(n)$。
本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目除了链表一些题目一定要使用双指针其他题目都是使用双指针来提高效率一般是将O(n^2)的时间复杂度,降为$O(n)$。
建议大家可以把文中涉及到的题目在好好做一做,琢磨琢磨,基本对双指针法就不在话下了。

View File

@ -44,7 +44,7 @@ a->right = NULL;
在介绍前中后序遍历的时候有递归和迭代非递归还有一种牛逼的遍历方式morris遍历。
morris遍历是二叉树遍历算法的超强进阶算法morris遍历可以将非递归遍历中的空间复杂度降为$O(1)$,感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。
morris遍历是二叉树遍历算法的超强进阶算法morris遍历可以将非递归遍历中的空间复杂度降为O(1),感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。
## 周二

View File

@ -76,7 +76,7 @@
* 空间复杂度:$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!)$这个可以从排列的树形图中很明显发现每一层节点为n第二层每一个分支都延伸了n-1个分支再往下又是n-2个分支所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。每个叶子节点都会有一个构造全排列填进数组的操作(对应的代码:`result.push_back(path)`),该操作的复杂度为$O(n)$。所以最终时间复杂度为n * n!,简化为$O(n!)$。
* 空间复杂度:$O(n)$,和子集问题同理。
组合问题分析:

View File

@ -41,7 +41,7 @@
一些录友不清楚[贪心算法:分发饼干](https://programmercarl.com/0455.分发饼干.html)中时间复杂度是怎么来的?
就是快排$O(n\log n)$,遍历$O(n)$,加一起就是还是$O(n\log n)$
就是快排O(nlog n)遍历O(n)加一起就是还是O(nlogn)。
## 周三

View File

@ -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(\log n)$的代码。
此时大家就会发现同一道题目同样使用递归算法有的同学会写出了O(n)的代码,有的同学就写出了$O(\log n)$的代码。
其本质是要对递归的时间复杂度有清晰的认识,才能运用递归来有效的解决问题!

View File

@ -8,7 +8,7 @@
在[贪心算法:加油站](https://programmercarl.com/0134.加油站.html)中给出每一个加油站的汽油和开到这个加油站的消耗,问汽车能不能开一圈。
这道题目咋眼一看,感觉是一道模拟题,模拟一下汽车从每一个节点出发看看能不能开一圈,时间复杂度是$O(n^2)$
这道题目咋眼一看感觉是一道模拟题模拟一下汽车从每一个节点出发看看能不能开一圈时间复杂度是O(n^2)。
即使用模拟这种情况,也挺考察代码技巧的。

View File

@ -211,8 +211,8 @@ public:
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$
* 时间复杂度O(n^2)
* 空间复杂度O(1)
贪心解法代码如下:
@ -233,8 +233,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
* 时间复杂度O(n)
* 空间复杂度O(1)
动规解法,版本一,代码如下:
@ -256,8 +256,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$
* 时间复杂度O(n)
* 空间复杂度O(n)
从递推公式可以看出dp[i]只是依赖于dp[i - 1]的状态。
@ -282,8 +282,8 @@ public:
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$
* 时间复杂度O(n)
* 空间复杂度O(1)
建议先写出版本一,然后在版本一的基础上优化成版本二,而不是直接就写出版本二。

View File

@ -22,7 +22,7 @@
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是$O(n)$,但如果使用哈希表的话, 只需要$O(1)$就可以做到。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
@ -88,17 +88,17 @@
|集合 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率|
|---|---| --- |---| --- | --- | ---|
|std::set |红黑树 |有序 |否 |否 | $O(\log n)$|$O(\log n)$ |
|std::multiset | 红黑树|有序 |是 | 否| $O(\log n)$ |$O(\log n)$ |
|std::unordered_set |哈希表 |无序 |否 |否 |$O(1)$ | $O(1)$|
|std::set |红黑树 |有序 |否 |否 | O(log n)|O(log n) |
|std::multiset | 红黑树|有序 |是 | 否| O(logn) |O(logn) |
|std::unordered_set |哈希表 |无序 |否 |否 |O(1) | O(1)|
std::unordered_set底层实现为哈希表std::set 和std::multiset 的底层实现是红黑树红黑树是一种平衡二叉搜索树所以key值是有序的但key不可以修改改动key值会导致整棵树的错乱所以只能删除和增加。
|映射 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率|
|---|---| --- |---| --- | --- | ---|
|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::map |红黑树 |key有序 |key不可重复 |key不可修改 | O(logn)|O(logn) |
|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也是有序的这个问题也经常作为面试题考察对语言容器底层的理解

View File

@ -302,7 +302,7 @@ 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)的空间啊?

View File

@ -365,6 +365,87 @@ class Solution:
return res
```
TypeScript
**90.子集II**
```typescript
function subsetsWithDup(nums: number[]): number[][] {
nums.sort((a, b) => a - b);
const resArr: number[][] = [];
backTraking(nums, 0, []);
return resArr;
function backTraking(nums: number[], startIndex: number, route: number[]): void {
resArr.push(route.slice());
const helperSet: Set<number> = new Set();
for (let i = startIndex, length = nums.length; i < length; i++) {
if (helperSet.has(nums[i])) continue;
helperSet.add(nums[i]);
route.push(nums[i]);
backTraking(nums, i + 1, route);
route.pop();
}
}
};
```
**40. 组合总和 II**
```typescript
function combinationSum2(candidates: number[], target: number): number[][] {
candidates.sort((a, b) => a - b);
const resArr: number[][] = [];
backTracking(candidates, target, 0, 0, []);
return resArr;
function backTracking(
candidates: number[], target: number,
curSum: number, startIndex: number, route: number[]
) {
if (curSum > target) return;
if (curSum === target) {
resArr.push(route.slice());
return;
}
const helperSet: Set<number> = new Set();
for (let i = startIndex, length = candidates.length; i < length; i++) {
let tempVal: number = candidates[i];
if (helperSet.has(tempVal)) continue;
helperSet.add(tempVal);
route.push(tempVal);
backTracking(candidates, target, curSum + tempVal, i + 1, route);
route.pop();
}
}
};
```
**47. 全排列 II**
```typescript
function permuteUnique(nums: number[]): number[][] {
const resArr: number[][] = [];
const usedArr: boolean[] = [];
backTracking(nums, []);
return resArr;
function backTracking(nums: number[], route: number[]): void {
if (nums.length === route.length) {
resArr.push(route.slice());
return;
}
const usedSet: Set<number> = new Set();
for (let i = 0, length = nums.length; i < length; i++) {
if (usedArr[i] === true || usedSet.has(nums[i])) continue;
usedSet.add(nums[i]);
route.push(nums[i]);
usedArr[i] = true;
backTracking(nums, route);
usedArr[i] = false;
route.pop();
}
}
};
```
Go

View File

@ -102,7 +102,7 @@
本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将$O(n^2)$的暴力解法降为$O(n)$。**
**滑动窗口的精妙之处在于根据当前子序列和大小的情况不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。**
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。

View File

@ -151,7 +151,7 @@ public:
大家应该发现了编程语言中一个普通容器的insertdelete的使用都可能对写出来的算法的有很大影响
如果抛开语言谈算法,除非从来不用代码写算法纯分析,**否则的话语言功底不到位O(n)的算法可以写出$O(n^2)$的性能**,哈哈。
如果抛开语言谈算法,除非从来不用代码写算法纯分析,**否则的话语言功底不到位O(n)的算法可以写出O(n^2)的性能**,哈哈。
相信在这里学习算法的录友们,都是想在软件行业长远发展的,都是要从事编程的工作,那么一定要深耕好一门编程语言,这个非常重要!

View File

@ -127,9 +127,9 @@ Carl个人认为如果找出局部最优并可以推出全局最优就是
贪心专题汇聚为一张图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211110121605.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/贪心总结water.png)
这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[](https://wx.zsxq.com/dweb2/index/footprint/185251215558842)所画,总结的非常好,分享给大家。
这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412)所画,总结的非常好,分享给大家。
很多没有接触过贪心的同学都会感觉贪心有啥可学的,但只要跟着「代码随想录」坚持下来之后,就会发现,贪心是一种很重要的算法思维而且并不简单,贪心往往妙的出其不意,触不及防!
@ -145,18 +145,6 @@ Carl个人认为如果找出局部最优并可以推出全局最优就是
**一个系列的结束,又是一个新系列的开始,我们将在明年第一个工作日正式开始动态规划,来不及解释了,录友们上车别掉队,我们又要开始新的征程!**
## 其他语言版本
Java
Python
Go
-----------------------