/ 杂七杂八  

【基本完结】LeetCode自用刷题记录

LeetCode自用刷题思路

自用的刷题记录,以防后续不记得之前自己怎么想的~ 正式的题解还是看官方题解和**“代码随想录”**吧

明天就要华为面试了,但我已经什么都不记得了,祝我好运吧!随缘了

——2024年10月13日

数组

704. 二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

注意开闭区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int search(int* nums, int numsSize, int target) {
int left = 0;
int right = numsSize - 1;
int middle = 0;
while (left <= right){
middle = (left + right) / 2;
if (nums[middle] == target) {
return middle;
}
else if (nums[middle] > target) {
right = middle - 1;
}
else {
left = middle + 1;
}
}
return -1;
}

27. 移除元素

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

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

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

快指针指向原始数组,慢指针指向新数组。如果等于val,slow不加;不等于val,fast指向的值赋给slow指向的值

1
2
3
4
5
6
7
8
9
10
11
int removeElement(int* nums, int numsSize, int val) {
int slow = 0;
for (int fast = 0; fast < numsSize; fast++) {
// 快指针指向原始数组,慢指针指向新数组。如果等于val,slow不加;不等于val,fast指向的值赋给slow指向的值
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}

977. 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

动态数组不行!

中途比较,两头的数字一定是最大的,其平方一定在平方数组的末尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
*returnSize = numsSize;

int left = 0;
int right = numsSize - 1;

int* ans = (int*)malloc(sizeof(int) * numsSize);

// 中途比较,两头的数字一定是最大的,其平方一定在平方数组的末尾

for (int i = numsSize - 1; i >= 0; i--) {
if (nums[left] * nums[left] >= nums[right] * nums[right]) {
ans[i] = nums[left] * nums[left];
left++;
}
else {
ans[i] = nums[right] * nums[right];
right--;
}
}
return ans;
}

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

暴力解法过不了

张开的窗口之和至少要能够装下target。首先移动end(必须到结尾),至少要装下target。接着,start向右,找最小的长度,装不下了,end向右。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int minSubArrayLen(int target, int* nums, int numsSize) {
int length = 100001;
int start = 0;
int sum = 0;
for(int end = 0; end < numsSize; end++) {
sum += nums[end];
while (sum >= target) { //这个while是精髓!!! 滑动初始窗口
if ((end - start + 1) < length) {
length = end - start + 1;
}
sum -= nums[start];
start++;
}
}
if (length < 100001) {
return length;
}
else {
return 0;
}
}

59. 螺旋矩阵 II

给你一个正整数 n ,生成一个包含 1n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

找规律,注意边界

找规律,第一步先把第一行填满,后续每次转弯向右,每两次需要步长减一。注意,横纵坐标有没有超出范围,且二维数组第一位为行,第二位为列

image-20240326200626892
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
int** generateMatrix(int n, int* returnSize, int** returnColumnSizes) {
// 初始化返回的结果数组
*returnSize = n;
*returnColumnSizes = (int*)malloc(sizeof(int) * n);
int** ans = (int**)malloc(sizeof(int*) * n);
int i;
for (int i = 0; i < n; i++) {
ans[i] = (int*)malloc(sizeof(int) * n);
(*returnColumnSizes)[i] = n;
}

int shiftX = 0;
int shiftY = 0;
int inputNum = 1;
int directionX = 0; // 0往左,1往右
int directionY = 0; // 0往下,1往上

for (int step = n; step > 0; step-- ) {
if (step == n) {
for (int i = 0; i < step; i++) {
ans[shiftY][shiftX] = inputNum;
inputNum++;
shiftX++;
}
shiftX--;
}
else {
// 先移动Y方向
if (directionY == 0) {
for (int i = 0; i < step; i++) {
shiftY++;
ans[shiftY][shiftX] = inputNum;
inputNum++;
}
directionY = 1;
}
else {
for (int i = 0; i < step; i++) {
shiftY--;
ans[shiftY][shiftX] = inputNum;
inputNum++;
}
directionY = 0;
}
//再移动X方向
if (directionX == 0) {
for (int i = 0; i < step; i++) {
shiftX--;
ans[shiftY][shiftX] = inputNum;
inputNum++;
}
directionX = 1;
}
else {
for (int i = 0; i < step; i++) {
shiftX++;
ans[shiftY][shiftX] = inputNum;
inputNum++;
}
directionX = 0;
}
}
}

return ans;
}

链表

链表和数组对比

插入/删除 查询 使用场景
数组 O(n)O(n) O(1)O(1) 数据量固定,频繁查询,较少增删
链表 O(1)O(1) O(n)O(n) 数据量不固定,频繁增删,较少查询
  • C:——《数据结构与算法/软件技术基础》周大为版

    1
    2
    3
    4
    typedef struct node {
    int data;
    struct node *next;
    } linklist;
  • C++:

    1
    2
    3
    4
    5
    struct ListNode {
    int val; // 节点上存储的元素
    ListNode *next; // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
    };

——代码随想录

203. 移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

定义虚拟头节点dummyHead,以解决第一个节点就是val值的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode* dummyHead = malloc(sizeof(struct ListNode));
dummyHead->next = head;
struct ListNode* p = dummyHead;
while(p->next != NULL) {
if (p->next->val == val) {
p->next = p->next->next;
}
else {
p = p->next;
}
}
return dummyHead->next;

}

707. 设计链表

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

找准指向的元素。前一个还是后一个。另外需要分类分析头尾的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
typedef struct MyLinkedList{
int val;
struct MyLinkedList* next;
} MyLinkedList;


MyLinkedList* myLinkedListCreate() {
// 定义虚拟头节点
MyLinkedList* head = (MyLinkedList*)malloc(sizeof(MyLinkedList));
head->next = NULL;
return head;
}

int myLinkedListGet(MyLinkedList* obj, int index) {
MyLinkedList* p = obj->next; //定义工作指针
for (int i = 0; p != NULL; i++) {
if (i == index) {
return p->val;
}
else {
p = p->next;
}
}
return -1;
}

void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
MyLinkedList* newHead = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newHead->next = obj->next;
newHead->val = val;
obj->next = newHead;
}

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
MyLinkedList* p = obj->next; //定义工作指针
MyLinkedList* newTail = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newTail->val = val;
newTail->next = NULL;
if (p != NULL) {
while(p->next != NULL) {
p = p->next;
}
p->next = newTail;
}
else {
obj->next = newTail;
}

}

void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
MyLinkedList* p = obj->next; //工作指针
if (index == 0) {
myLinkedListAddAtHead(obj, val);
}
MyLinkedList* newAdd = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newAdd->val = val;
for(int i = 0; p != NULL; i++) {
if (i == index - 1) {
newAdd->next = p->next;
p->next = newAdd;
break;
}
else {
p = p->next;
}
}
}

void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
MyLinkedList* p = obj->next;
if (index == 0){
if (p != NULL) {
obj->next = p->next;
}
}
else {
for (int i = 0; p->next != NULL; i++) {
if (i == index - 1) {
p->next = p->next->next;
if (p->next == NULL) {
break;
}
else {
p = p->next;
}
}
else {
p = p->next;
}
}
}
}

void myLinkedListFree(MyLinkedList* obj) {
while(obj != NULL) {
MyLinkedList* temp = obj;
obj = obj->next;
free(temp);
}
}

/**
* Your MyLinkedList struct will be instantiated and called as such:
* MyLinkedList* obj = myLinkedListCreate();
* int param_1 = myLinkedListGet(obj, index);

* myLinkedListAddAtHead(obj, val);

* myLinkedListAddAtTail(obj, val);

* myLinkedListAddAtIndex(obj, index, val);

* myLinkedListDeleteAtIndex(obj, index);

* myLinkedListFree(obj);
*/

cpp版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class MyLinkedList {
public:
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
ListNode* _dummyHead;
int _size;

MyLinkedList() {
_dummyHead = new ListNode(0);
_size = 0;
}

int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
ListNode *p = _dummyHead->next;
for (int i = 0; i < index; i++) {
p = p->next;
}
return p->val;
}

void addAtHead(int val) {
ListNode* p = new ListNode(val);
p->next = _dummyHead->next;
_dummyHead->next = p;
_size++;
}

void addAtTail(int val) {
ListNode *p = _dummyHead;
while (p->next != nullptr) {
p = p->next;
}
p->next = new ListNode(val);
_size ++;
}

void addAtIndex(int index, int val) {
if (index > _size) {
return;
}
ListNode *p = _dummyHead;
for (int i = 0; i < index; i++) {
p = p->next;
}
ListNode *newNode = new ListNode(val);
newNode->next = p->next;
p->next = newNode;
_size++;
}

void deleteAtIndex(int index) {
if (index < 0 || index >= _size) {
return;
}
ListNode *p = _dummyHead;
for (int i = 0; i < index; i++) {
p = p->next;
}
p->next = p->next->next;
_size--;
}
};

/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

【双指针】注意链表不带头节点。所以双指针的工作和带头结点的不同。

image-20240326200013905

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head) {
//head 不带头节点
if (head == NULL || head->next == NULL) {
return head;
}
struct ListNode *p = NULL;
struct ListNode *q = head;
while (q != NULL) {
struct ListNode *r = q->next;
q->next = p;
p = q;
q = r;
}
// head->next = NULL;
head = p;
return head;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (head == NULL || head->next == NULL) {
return head;
}
ListNode *p = NULL;
ListNode *q = head;
while (q) {
ListNode *temp;
temp = q->next;
q->next = p;
p = q;
q = temp;
}
return p;
}
};

24. 两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

image-20240326195630531

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* swapPairs(struct ListNode* head) {
typedef struct ListNode ListNode;
ListNode *dummyHead = (ListNode*)malloc(sizeof(ListNode));
ListNode *p = (ListNode*)malloc(sizeof(ListNode)); // 定义头节点和工作指针
dummyHead->next = head;
p = dummyHead;
while(p->next != NULL && p->next->next != NULL) {
ListNode *temp = (ListNode*)malloc(sizeof(ListNode));
temp = p->next;
p->next = p->next->next;
temp->next = p->next->next;
p->next->next = temp;
p = p->next->next;
}
return dummyHead->next;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode *dummyHead = new ListNode(0, head);
if (dummyHead->next == nullptr || dummyHead->next->next == nullptr) {
return dummyHead->next;
}
ListNode *p = dummyHead;
ListNode *temp;
while (p->next != nullptr && p->next->next != nullptr) {
temp = p->next;
p->next = p->next->next;
temp->next = p->next->next;
p->next->next = temp;
p = p->next->next;
}
return dummyHead->next;
}
};

19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

解法一——暴力解法,两次遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
typedef struct ListNode ListNode;
ListNode *dummyNode = (ListNode*)malloc(sizeof(ListNode));
dummyNode->next = head;
ListNode *p = dummyNode;
int count = 0;
while (p->next != 0) {
count++;
p = p->next;
}
p = dummyNode;
for (int i = 0; i < count - n; i++) {
p = p->next;
}
p->next = p->next->next;
return dummyNode->next;
}

解法二——双指针,让快慢指针间隔n位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
typedef struct ListNode ListNode;
ListNode *dummyNode = (ListNode*)malloc(sizeof(ListNode));
dummyNode->next = head;
ListNode *fast = dummyNode;
ListNode *slow = dummyNode;
int count = 0;
for (int i = 0; i < n; i++) {
fast = fast->next;
}
while(fast-> next != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummyNode->next;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 定义虚拟头节点,方便统一处理
ListNode *dummyHead = new ListNode(0, head);
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
for (int i = 0; i < n; i++) {
fast = fast->next;
}
while (fast->next != nullptr) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummyHead->next;
}
};

160. 相交链表

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交**:**

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

计算链表长度的差,移动到剩余相同长度,一个一个节点比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
typedef struct ListNode ListNode;

ListNode *p = headA;
ListNode *q = headB;

// 计算链表长度的差
int lenA = 0, lenB = 0;
int gap = 0;
while (p != NULL) {
lenA++;
p = p->next;
}
while (q != NULL) {
lenB++;
q = q->next;
}

// 移动到相同长度
p = headA;
q = headB;
if (lenA > lenB) {
gap = lenA - lenB;
while(gap != 0) {
gap--;
p = p->next;
}
}
else {
gap = lenB - lenA;
while(gap != 0) {
gap--;
q = q->next;
}
}

// 一个一个比较
while (p != NULL && q != NULL){
if (p == q) {
return p;
}
else {
p = p->next;
q = q->next;
}
}
return NULL;

}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lengthA = 0;
int lengthB = 0;
ListNode* p = headA;
while(p != NULL) {
lengthA++;
p = p->next;
}
ListNode* q = headB;
while(q != NULL) {
lengthB++;
q = q->next;
}
p = headA;
q = headB;
if (lengthA > lengthB) {
for (int i = 0; i < lengthA - lengthB; i++) {
p = p->next;
}
}
else {
for (int i = 0; i < lengthB - lengthA; i++) {
q = q->next;
}
}
for (int i = 0; i < min(lengthB, lengthA); i++) {
if(p == q) {
return p;
}
else {
p = p->next;
q = q->next;
}
}
return NULL;
}
};

官方题解——双指针的思路

两者长度分别为m,nm,n,假设公共部分为后cc,则m=a+cm=a+c, n=b+cn=b+c

开始先指向自己,走完自己全程指向对方

  • 两链表相交:
    • 长度相等:两个指针会同时到达两个链表相交的节点
    • 长度不等:走到第一个公共节点的距离是相同的,第一个走的是a+c+ba+c+b,第二个走的是b+c+ab+c+a
  • 两个链表不相交
    • 长度相等:同时到达两个链表自己的尾节点变成NULL
    • 长度不等:两个指针都会遍历完两个链表(自己加对方),变成NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *p = headA;
ListNode *q = headB;

while (p != NULL && q != NULL) {
p = p->next;
q = q->next;
}
if (p == NULL) {
p = headB;
while (q != NULL) {
p = p->next;
q = q->next;
}
q = headA;
while (p != NULL && q != NULL) {
if(p == q) {
return p;
}
p = p->next;
q = q->next;
}
}
else {
q = headA;
while (p != NULL) {
p = p->next;
q = q->next;
}
p = headB;
while (p != NULL && q != NULL) {
if(p == q) {
return p;
}
p = p->next;
q = q->next;
}
}
return NULL;
}
};

142. 环形链表 II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

img

慢指针每次走一步,快指针每次走两部

相遇时

  • slow: x+yx+y

  • fast:x+y+n(y+z)x+y+n(y+z)​ 比slow多走了n圈

  • 重要的等式!!!

    2(x+y)=x+y+n(y+z)2(x+y)=x+y+n(y+z)

    于是可以推导出x=n(y+z)y=(n1)(y+z)+zx=n(y+z)-y = (n-1)(y+z)+z,至少多走了一圈,所以n1n\ge 1合理.

  • 所以x和z的关系就是差了整数圈的关系!

  • 所以从头节点走到环形入口的距离等于整数圈+相遇节点到入口。

    • 所以从相遇点、头节点出发的两个指针,每次走一步,相遇的位置一定是环形入口。
    • 代码注意后续这么动的时候,slow和fast在不在变,会导致判断条件有问题!
  1. 为何慢指针第一圈走不完一定会和快指针相遇? 可以认为快指针和慢指针是相对运动的,假设慢指针的速度是 1节点/秒,快指针的速度是 2节点/秒,当以慢指针为参考系的话(即慢指针静止),快指针的移动速度就是 1节点/秒,所以肯定会相遇。
  2. 为什么在第一圈就会相遇呢? 设环的长度为 L,当慢指针刚进入环时,慢指针需要走 L 步(即 L 秒)才能走完一圈,此时快指针距离慢指针的最大距离为 L-1,我们再次以慢指针为参考系,如上所说,快指针在按照1节点/秒的速度在追赶慢指针,所以肯定能在 L 秒内追赶到慢指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
typedef struct ListNode ListNode;
ListNode *slow = head, *fast = head;
while (fast != NULL && fast->next != NULL) {
// 先追及
slow = slow->next;
fast = fast->next->next;

// 后从头开始
if (slow == fast) {
ListNode *p = head;
ListNode *q = slow;
while (p != q) {
p = p->next;
q = q->next;
}
return p;
}
}
return NULL;
}

以下开始以C艹为主

哈希表

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map (映射)

这里数组就没啥可说的了,我们来看一下set。

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

集合 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
std::set 红黑树 有序 O(logn)O(\log n) O(logn)O(\log n)
std::multiset 红黑树 有序 O(logn)O(\log n) O(logn)O(\log n)
std::unordered_set 哈希表 无序 O(1)O(1) O(1)O(1)`

std::unordered_set底层实现为哈希表,std::setstd::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

映射 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
std::map 红黑树 key有序 key不可重复 key不可修改 O(logn)O(\log n) O(logn)O(\log n)
std::multimap 红黑树 key有序 key可重复 key不可修改 O(logn)O(\log n) O(logn)O(\log n)
std::unordered_map 哈希表 key无序 key不可重复 key不可修改 O(1)O(1) O(1)O(1)

std::unordered_map 底层实现为哈希表,std::mapstd::multimap 的底层实现是红黑树。同理,std::mapstd::multimap 的key也是有序的。

  • 当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
  • 再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

——代码随想录

242. 有效的字母异位词

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的字母异位词。

**注意:**若 st 中每个字符出现的次数都相同,则称 st 互为字母异位词。

把小写字母表看作哈希表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return 0;
}

vector <int> alphabetS(26);
vector <int> alphabetT(26);

for (int i = 0; i < s.length(); i++) {
alphabetS[s[i] - 'a'] += 1;
alphabetT[t[i] - 'a'] += 1;
}

if (alphabetS == alphabetT) {
return 1;
}
else {
return 0;
}

}
};

或者通过是不是都是0来判断,减少空间需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool isAnagram(char* s, char* t) {

if (strlen(s) != strlen(t)) {
return 0;
}

int *alphabet = (int*)malloc(sizeof(int) * 26);
for (int i = 0; i < 26; i++) {
alphabet[i] = 0;
}

for (int i = 0; i < strlen(s); i++) {
alphabet[s[i] - 'a'] += 1;
alphabet[t[i] - 'a'] -= 1;
}

for (int i = 0; i < 26; i++) {
if (alphabet[i] != 0){
return 0;
}
}
return 1;

}

349. 两个数组的交集

给定两个数组 nums1nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

解法一——建立数组hash表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
vector <int> hash(1001);
unordered_set <int> output;

for (int num:nums1) {
hash[num] += 1;
}
for (int num:nums2) {
if (hash[num] != 0) {
output.insert(num);
}
}

vector <int> output_vec;
output_vec.assign(output.begin(), output.end());
return output_vec;

}
};

解法二——用无序set做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set <int> nums1_set(nums1.begin(), nums1.end());
unordered_set <int> output_set;

for (int num:nums2) {
if (nums1_set.find(num) != 0) {
output_set.insert(num);
}
}

return vector <int> (output_set.begin(), output_set.end());
}
};

202. 快乐数

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

无限循环是重点!!!

重点是可能会陷入无限循环。
需要先判断在不在之前存储的集合里面,再将sum添加进集合里面。
同时集合.find的判定是和end()比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
bool isHappy(int n) {
unordered_set <int> results;
int sum = n;
int mod = n;

while (results.find(sum) == results.end()) {
results.insert(sum);
mod = sum;
sum = 0;
// 下列循环求mod的各位平方和
while (mod != 0) {
sum += (mod % 10) * (mod % 10);
mod /= 10;
}
// cout << sum << " ";
}
if (sum == 1) {
return true;
}
else {
return false;
}
}
};

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

解法一——暴力解法

复杂度O(n2)\mathcal O(n^2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int n = nums.size();
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n; j++){
if( nums[i] + nums[j] == target) {
return {i, j};
}
}
}
return {};
}
};

解法二——利用哈希表的思路

利用键值对,在之前保存的键值对中找有没有能够匹配的元素。注意find的时候,是find的键值对的键,而非值,返回的是键值对的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map <int,int> nums_map;

for (int i = 0; i < nums.size(); i++) {
if (nums_map.find(target - nums[i]) != nums_map.end()) {
return {nums_map.find(target - nums[i])->second, i};
}
nums_map[nums[i]] = i;
}
return {};
}
};

454. 四数相加 II

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

首先是无序的,但是需要次数,所以需要通过unordered_map

分组的思想降低for循环的次数

map新增值不需要判断是否存在键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map <int, int> sumTwo;
int counts = 0;

for (int num1:nums1) {
for (int num2:nums2) {
sumTwo[num1 + num2] += 1;
}
}

for (int num3:nums3) {
for (int num4:nums4) {
if (sumTwo.find(- (num3 + num4)) != sumTwo.end()) {
counts += sumTwo[- (num3 + num4)];
}
}
}

return counts;
}
};

383. 赎金信

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

小写字母建立hash表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int alphabet[26];
if (magazine.length() < ransomNote.length()) {
return 0;
}
for (int i = 0; i < magazine.length(); i++) {
alphabet[magazine[i] - 'a'] += 1;
}
for (int j = 0; j < ransomNote.length(); j++) {
alphabet[ransomNote[j] - 'a'] -= 1;
if (alphabet[ransomNote[j] - 'a'] < 0) {
return 0;
}
}
return 1;
}
};

15. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

**注意:**答案中不可以包含重复的三元组。

去重很困难。考虑双指针法

去重的位置非常重要!同时要考虑使用双指针而非哈希表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector <vector <int>> output;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) { // 最左边的元素大于0,那么不可能和为0
return output;
}
if (i > 0 && nums[i] == nums[i - 1]) { //第一个数,去重
continue;
}
// 双指针启动!
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
if (nums[i] + nums[left] + nums[right] < 0) {
left++;
}
else if (nums[i] + nums[left] + nums[right] > 0) {
right--;
}
else {
output.push_back({nums[i], nums[left], nums[right]});
while (left < right && nums[left] == nums[left + 1]) {
left++; // 第二个数,去重,要求左右顺序不能错。如果这个数和后一个一样,那么会导致第二个数有重复,所以右移
}
while (left < right && nums[right] == nums[right - 1]) {
right--;// 第三个数,去重
}
//找到答案,同时收缩
left++;
right--;
}
}
}
return output;
}
};

18. 四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1]target-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。

这边的重点是第二个数要求j > i+1而非j>0,不然数相等时候会往右缩。

同时注意不加(long)可能会溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector <vector <int>> output;
//先要从小到大排
sort(nums.begin(), nums.end());

for (int i = 0; i < nums.size(); i++) {
// if (nums[i] > target && nums[i] >= 0) {
// break; //剪枝处理,类似之前的首元素大于0,那么必然不行
// }
if (i > 0 && nums[i] == nums[i-1]) {
continue;
}
for (int j = i + 1; j < nums.size(); j++) {
// if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) {
// break; //剪枝处理,类似之前的首元素大于0,那么必然不行
// }
if (j > i+1 && nums[j] == nums[j-1]) { // 这边的重点是j > i+1而非j>0
continue;
}
// 双指针
int left = j + 1;
int right = nums.size() - 1;
while(left < right) {
if ((long) nums[i] + nums[j] + nums[left] + nums[right] < target) { //会溢出
left++;
}
else if ((long) nums[i] + nums[j] + nums[left] + nums[right] > target) {
right--;
}
else {
output.push_back({nums[i], nums[j], nums[left], nums[right]});
while (left < right && nums[left] == nums[left+1]) {
left++;
}
while (left < right && nums[right] == nums[right-1]) {
right--;
}
left++;
right--;
}
}
}
}
return output;
}
};

204. 计数质数

给定整数 n ,返回 所有小于非负整数 n 的质数的数量

埃氏筛

枚举没有考虑到数与数的关联性,因此难以再继续优化时间复杂度。

我们设 isPrime[i] 表示数 i 是不是质数,如果是质数则为 1,否则为 0。从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即 0,这样在运行结束的时候我们即能知道质数的个数。

这种方法的正确性是比较显然的:这种方法显然不会将质数标记成合数;另一方面,当从小到大遍历到数 x 时,倘若它是合数,则它一定是某个小于 x 的质数 y 的整数倍,故根据此方法的步骤,我们在遍历到 y 时,就一定会在此时将 x 标记为 isPrime[x]=0。因此,这种方法也不会将合数标记为质数。

当然这里还可以继续优化,对于一个质数 x,如果按上文说的我们从 2x 开始标记其实是冗余的,应该直接从 x⋅x 开始标记,因为 2x,3x,… 这些数一定在 x 之前就被其他数的倍数标记过了,例如 2 的所有倍数,3 的所有倍数等。

作者:力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int countPrimes(int n) {
vector<int> isPrime(n, 1);
int output = 0;
for (int i = 2; i < n; i++) {
if (isPrime[i]) {
output++;
if ((long long)i * i < n) {
for (int j = i * i ; j < n ; j += i) {
isPrime[j] = 0;
}
}
}
}
return output;
}
};

字符串

344. 反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

不要给另外的数组分配额外的空间,你必须**原地修改输入数组**、使用 O(1) 的额外空间解决这一问题。

注意right的其实位置是length-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
void reverseString(vector<char>& s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
int temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
};

541. 反转字符串 II

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。

  • 如果剩余字符少于 k 个,则将剩余字符全部反转。
  • 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

还是写的有点复杂了,可以把两个大于等于k的条件再精简一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
string reverseStr(string s, int k) {
int loop = s.length() / (2 * k) + 1;
for (int i = 0; i < loop; i++) {
// 如果不是最后一个loop,则反转当前loop前k个
if (i != loop - 1) { //和下面last_loop >= k合并
int left = 2 * i * k;
int right = 2 * i * k + (k - 1);
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
}
else {
int last_loop = s.length() % (2 * k);
int left = 2 * i * k;
int right;
if (last_loop < k) { //0的情况也没事,不会进入while循环。
right = s.length() - 1;
}
else { //和上面i != loop - 1合并
right = 2 * i * k + (k - 1);
}
while (left < right) {
swap(s[left], s[right]);
left++;
right--;
}
}
}
return s;
}
};

54. 替换数字(kamacoder)

题目描述

给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。 例如,对于输入字符串 a1b2c3,函数应该将其转换为 anumberbnumbercnumber

输入描述

输入一个字符串 s,s 仅包含小写字母和数字字符。

输出描述

打印一个新的字符串,其中每个数字字符都被替换为了number

从后往前替换字符,复杂度低,左边不用管。

img

注意处理完之后要左移一下,且判断条件注意边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# include <iostream> 

using namespace std;

int main() {
string s;
while (cin >> s) {
int countNum = 0;
int oldSize = s.length();
for (int i = 0; i < oldSize; i++) {
if (s[i] >= '0' && s[i] <= '9') {
countNum++;
}
}
// cout << countNum << endl;
s.resize(s.size() + 5 * countNum);
for (int left = oldSize - 1, right = s.size() - 1; left >= 0; left--){
if (s[left] < '0' || s[left] > '9') { // 不应该有等号
s[right] = s[left];
right--;
}
else {
s[right--] = 'r';
s[right--] = 'e';
s[right--] = 'b';
s[right--] = 'm';
s[right--] = 'u';
s[right--] = 'n'; // 注意先减还是后减去
}
}
cout << s << endl;
}
return 0;
}

151. 反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

**注意:**输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

fig

反转整个字符串。
同时操作反转单词和去除空格,要定义一个变量表示当前单词头,然后快慢指针删除空格. (代码随想录中分开处理。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
void reverseString (string &s, int left, int right) {
while (left < right) {
swap(s[left++], s[right--]);
}
}
string reverseWords(string s) {
// 反转整个字符串
reverseString (s, 0, s.length() - 1);

// 同时操作反转单词和去除空格,要定义一个变量表示当前单词头,然后快慢指针删除空格.
// 慢指针指向单词尾部时,从单词头到慢指针反转。
// 处理完一个单词后,注意单词之间有空格
int wordHead = 0;
int slow = 0;
for (int fast = 0; fast < s.length(); fast++) {
/*三种情况(前两种可以合并):
① fast=0 且 s[fast] 不为空,要传给slow
② fast!=0 且 s[fast] 不为空,要传给slow
(这样不好处理最后一个单词)③ fast!=0 且 s[fast] 为空,s[fast-1] 不为空,要传给slow,同时从wordHead到slow-1逆转,wordHead移动到空格后
③ fast!=0 且 s[fast] 不为空,s[fast+1] 为空或者句子结尾,要传给slow后,同时从wordHead到slow逆转,slow补齐空格,wordHead移动到空格后
所以先处理情况三*/
if (s[fast] != ' ') {
s[slow] = s[fast];
if (s[fast+1] == ' ' || fast+1 >= s.length()) {
reverseString(s, wordHead, slow);
s[++slow] = ' ';
wordHead = slow + 1;
}
slow++;
}
}
// 重置string长度,注意slow移向上一个单词的后一个格了
s.resize(slow-1);
return s;
}
};

55. 右旋字符串 (kamacoder)

题目描述

字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。

例如,对于输入字符串 “abcdefg” 和整数 2,函数应该将其转换为 “fgabcde”。

输入描述

输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。

输出描述

输出共一行,为进行了右旋转操作后的字符串。

解法一——直接字符串提取拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <queue>
using namespace std;

int main() {
int k;
string s;

cin >> k;
cin >> s;

int length = s.length();

string s1 = s.substr(0, length - k);
string s2 = s.substr(length - k, k);

cout << s2 << s1;
return 0;
}

解法二——“整体反转字符串”+“两个反转单词”

在原本字符串中处理,变成“整体反转字符串”+“两个反转单词”的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <algorithm>
using namespace std;

// void reverseString (string &s, int left, int right) {
// while (left < right) {
// swap (s[left++], s[right--]);
// }
// }

int main() {
int k;
string s;

cin >> k;
cin >> s;

// int length = s.length();
// reverseString(s, 0, length-1);
// reverseString(s, 0, k-1);
// reverseString(s, k, length-1);

reverse(s.begin(), s.end()); //左闭右开
reverse(s.begin(), s.begin() + k);
reverse(s.begin() + k, s.end());


cout << s;
return 0;
}

28. 找出字符串中第一个匹配项的下标

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

  1. 注意:如果单纯通过一层for循环判断时候,当flag倒了的时候,可能前面部分字符还是重复了needle的少部分,所以需要回溯。

解法一—— 暴力解法也能解,取所有的n长字串即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int strStr(string haystack, string needle) {
int flag = 0;
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
if (needle[j] == haystack[i]) {
flag++;
j++;
if (j == needle.length()) {
return i - j + 1;
}
}
else {
flag = 0;
i = i - j;
j = 0;
}
}
return -1;
}
};

解法二——KMP算法

  1. KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

  2. 前缀表

    • 为什么需要?不想完全回溯。
      image-20240329202856323

    • 当前位置既然不能完全满足匹配串,那么至多能匹配上多少?也就是说可以少回溯多少呢?也就知道子串(匹配串)需要回溯到什么位置——这由当前位置的上一个前缀表值来确定。

      image-20240329202910993

  3. next数组的定义

    • 初始化(前缀末尾(即最长相等前后缀长度)j、后缀末尾i

    • 前后缀不相等(防止越界j>0s[i]!=s[j]时,j要连续回溯到next[j-1]

    • 前后缀相等(j可以递增,同时后缀i指向位置的前缀表值为j

    • 请注意这里最后一个为什么是2:这是由于B的时候发现匹配不上了,那么j=3也就要回溯到j=next[j-1]=next[2]=1这个位置。这里需要注意为什么明明是后缀的问题,要用前缀来看呢?因为后缀和前缀相等,所以后缀要回溯的值等于前缀需要回溯到的值(好乱啊55555555)
      image-20240329203055212

      image-20240329202935587

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
void getNext (int* next, string &s) {
int j = 0; //前缀末尾,即最长相等前后缀长度
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j-1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.length() == 0) {
return 0;
}

int next[needle.length()];
getNext(next, needle); //获取前缀表

int j = 0; //指向匹配串
for (int i = 0; i < haystack.length(); i++) { //i指向文本串
while (j > 0 && haystack[i] != needle[j]){ //注意这里也要连续回溯
j = next[j - 1];
}
if(haystack[i] == needle[j]) {
j++;
if (j == needle.length()) {
return i - j + 1;
}
}
}
return -1;
}
};

459. 重复的子字符串

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

解法一——移动匹配

那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s

图二
——来自代码随想录

代码也来自代码随想录代码如下:

1
2
3
4
5
6
7
8
9
class Solution {
public:
bool repeatedSubstringPattern(string s) {
string t = s + s;
t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾
if (t.find(s) != std::string::npos) return true; // r
return false;
}
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数。 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。

解法二——优化的KMP算法

(不是很能看懂最后一步)

假设字符串s使用多个重复子串构成(这个子串是最小重复单位),重复出现的子字符串长度是x,所以s是由n * x组成。

因为字符串s的最长相同前后缀的长度一定是不包含s本身,所以 最长相同前后缀长度必然是m * x,而且n - m = 1,(这里如果不懂,看上面的推理)

所以如果 nx % (n - m)x = 0,就可以判定有重复出现的子字符串。

最长相等前后缀的长度为:next[len - 1]

数组长度为:len。

如果len % (len - next[len - 1] ) == 0 ,则说明数组的长度正好可以被 (数组长度-最长相等前后缀的长度) 整除 ,说明该字符串有重复的子字符串。

数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
bool repeatedSubstringPattern(string s) {
int j = 0;
int next[s.length()];
next[0] = 0;
int maxLoop = 1;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s[j] != s[i]) {
j = next[j - 1];
}
if (s[j] == s[i]) {
j++;
}
next[i] = j;
if (next[i] > maxLoop) {
maxLoop = next[i];
}
}
for (int i = 0; i < s.length(); i++) {
cout << next[i] << ' ';
}
if(s.length() % (s.length() - maxLoop) == 0) {
return true;
}
else{
return false;
}

}
};

双指针

  • 数组

    • [移除元素](#27. 移除元素):通过两个指针在一个for循环下完成两个for循环的工作。
  • 字符串

    • [反转字符串](#344. 反转字符串):使用双指针法,定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。时间复杂度是O(n)。
    • [替换数字](#54. 替换数字(kamacoder)):首先扩充数组到每个空格替换成"number"之后的大小。然后双指针从后向前替换空格。
    • [反转字符串中的单词](#151. 反转字符串中的单词):两次反转,同时要去除冗余空格,注意erase操作也是O(n)的操作
  • 链表

    • [反转链表](#206. 反转链表):只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。

    • [删除链表的倒数第 N 个结点](#19. 删除链表的倒数第 N 个结点):快指针多走N个节点

    • [相交链表](#160. 相交链表):计算链表长度的差,移动到剩余相同长度,一个一个节点比较

    • [环形链表II](#142. 环形链表 II):

      如何通过双指针判断是否有环,而且还要找到环的入口。

      使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

  • N数之和

    • 使用了哈希法解决了两数之和,但是哈希法并不适用于三数之和!去重不好操作
    • [三数之和](#15. 三数之和):①先排序;②双指针的移动原则;③通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。
    • [四数之和](#18. 四数之和):在三数之和的基础上再套一层for循环,依然是使用双指针法。
    • 对于三数之和使用双指针法就是将原本暴力O(n3)O(n^3)的解法,降为O(n2)O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n4)O(n^4)的解法,降为O(n3)O(n^3)的解法。

栈、队列

我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。

deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。

SGI STL中 队列底层实现缺省情况下一样使用deque实现的。

STL 队列不被归类为容器,而被归类为container adapter( 容器适配器)

队列是先进先出的数据结构,不允许有遍历行为,stack和queue不提供迭代器

——代码随想录

232. 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

第2种实现的效率远高于第1种实现。原因是:

  • 在第2种实现中,元素只会在 queueInqueueOut 之间转移一次,不会反复移动。
  • 第1种实现的入队每次都要移动所有元素两次,这种重复操作极大降低了效率。

双栈——操作均集中在push

注意pop只会去掉顶上的元素,不会把顶上元素值返回回来。

image-20240401104414302

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MyQueue {
public:
stack <int> left;
stack <int> right;
MyQueue() {
/* 双栈操作,定义两个栈
进队列时,先把右栈所有元素pop出压入左栈,再把加入元素压入左栈栈顶;接着左栈依次出栈,右栈依次入栈;
出队即右栈出栈pop即可
是否为空即判断右栈是否为空 */
}

void push(int x) {
while (right.size() != 0) {
left.push(right.top());
right.pop();
}
left.push(x);
while (left.size() != 0) {
right.push(left.top());
left.pop();
}
}

int pop() {
int output = right.top();
right.pop();
return output;
}

int peek() {
return right.top();
}

bool empty() {
if (right.size() == 0) {
return true;
}
return false;
}
};

/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/

双栈的官方题解——输入栈、输出栈

将一个栈当作输入栈,用于压入 push传入的数据;另一个栈当作输出栈,用于 pop和 peek操作。

每次 pop或 peek 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。

其实,在这个代码逻辑中即使 queueOut 中有数据,入队(push)操作依然是可以正常工作的。你的疑问可能源于对双栈模拟队列的工作机制的理解,我们一起来梳理一下这个逻辑。

双栈模拟队列的核心逻辑

  • queueIn:用于存储新入队的元素(入栈操作)。
  • queueOut:用于实现出队操作(栈顶弹出,模拟队列的队首)。

双栈实现队列的原理是:

  1. 入队时,只把数据压入 queueIn
  2. 出队时
    • 只有在 queueOut 为空时,才把 queueIn 中所有元素倒入 queueOut
    • 这样 queueOut 中的栈顶元素就对应队列的队首元素(先进先出)。

为什么入队不会出错?

  1. 每次 入队(push 只会把新元素加入到 queueIn 中,而不会影响 queueOut
  2. 出队(pop 时,只有当 queueOut 为空时,才会将 queueIn 的数据转移到 queueOut
    • 如果 queueOut 中已有元素,出队操作会直接弹出 queueOut 栈顶的元素,无需转移数据。

所以,即便 queueOut 中已有数据,push 新元素只会影响 queueIn,不会破坏现有的队列顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class MyQueue {
public:
stack<int> queueIn;
stack<int> queueOut;
MyQueue() {

}

void push(int x) {
queueIn.push(x);
}

int pop() {
if (queueOut.empty()) {
while (!queueIn.empty()) {
int x = queueIn.top();
queueOut.push(x);
queueIn.pop();
}
}
int x = queueOut.top();
queueOut.pop();
return x;
}

int peek() {
int x = this->pop();
queueOut.push(x);
return x;
}

bool empty() {
if (queueIn.empty() && queueOut.empty()) {
return true;
} else {
return false;
}
}
};

225. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

注意:

  • 你只能使用队列的标准操作 —— 也就是 push to backpeek/pop from frontsizeis empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

两个队列

——将pop出来的前面的元素存在另一个queue里面

fig1

——官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MyStack {
public:
queue <int> queue1;
queue <int> queue2;
MyStack() {

}

void push(int x) {
queue1.push(x);
}

int pop() {
while (queue1.size() > 1) {
queue2.push(queue1.front());
queue1.pop();
}
int output = queue1.front();
queue1.pop();
swap(queue1, queue2);
return output;
}

int top() {
return queue1.back();
}

bool empty() {
if (queue1.size() == 0) {
return true;
}
else {
return false;
}
}
};

/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/

一个队列

——将前面pop出来的继续push回去即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class MyStack {
public:
queue <int> queue1;
MyStack() {

}

void push(int x) {
queue1.push(x);
}

int pop() {
int queueSize = queue1.size();
for (int i = 0; i < queueSize - 1; i++) {
queue1.push(queue1.front());
queue1.pop();
}
int output = queue1.front();
queue1.pop();
return output;
}

int top() {
return queue1.back();
}

bool empty() {
if (queue1.size() == 0) {
return true;
}
else {
return false;
}
}
};

/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/

20. 有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

注意右括号的情况要判断一下栈是否为空。注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 False,省去后续的遍历判断过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Solution {
public:
bool isValid(string s) {
if (s.length() % 2 != 0) {
return false;
}
stack <char> bracks;
for (int i = 0; i < s.length(); i++) {
if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
bracks.push(s[i]);
}
else {
switch (s[i]) {
case ')':
if (!bracks.empty() && bracks.top() == '(') {
bracks.pop();
break;
}
else {
return false;
}
case ']':
if (!bracks.empty() && bracks.top() == '[') {
bracks.pop();
break;
}
else {
return false;
}
case '}':
if (!bracks.empty() && bracks.top() == '{') {
bracks.pop();
break;
}
else {
return false;
}
default:
return false;
}
}
}
if (bracks.size() == 0) {
return true;
}
else {
return false;
}
}
};

少写一点判断的话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
bool isValid(string s) {
stack<char> S;
for (int i = 0; i < s.length(); i++) {
if (s[i] == '(' || s[i] == '{' || s[i] == '[') {
S.push(s[i]);
}
else if (!S.empty() && ((s[i] == ')' && S.top() == '(') || (s[i] == ']' && S.top() == '[') || (s[i] == '}' && S.top() == '{'))) {
S.pop();
}
else {
return false;
}
}
if (S.empty()) {
return true;
}
else {
return false;
}
}
};

1047. 删除字符串中的所有相邻重复项

给出由小写字母组成的字符串 S重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

字符串拼接的代价

  • 在 C++ 中,字符串的拼接操作(+=)会动态调整内存。如果你每次都将字符插入到字符串的

    最前面,会导致:

    • 多次内存重新分配。
    • 数据从旧位置到新位置的频繁拷贝。
  • 因此,第一种实现的时间复杂度实际上更高。

第二种实现的优化

  • 在第二种代码中,字符逐个添加到 sNew 的末尾(+= 操作),这种方式更符合字符串的内存模型,避免了频繁的拷贝。
  • 最后再调用 reverse,将整体反转,代价是一次 O(n) 的操作,比第一种实现中频繁的插入效率高得多。

解法一——stack

注意size会变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
string removeDuplicates(string s) {
stack <char> words;
for (int i = 0; i < s.length(); i++) {
if (!words.empty() && s[i] == words.top()) {
words.pop();
}
else {
words.push(s[i]);
}
}
string sNew;
int wordsSize = words.size(); //注意size会变
for (int j = 0; j < wordsSize; j++) {
sNew = words.top() + sNew; //在前面加上字母
words.pop();
}
return sNew;
}
};

貌似每次在前面加字母会带来大量的时间消耗和内存消耗,所以下面那个循环可以改成这个。

image-20240401144455896

1
2
3
4
5
while (!words.empty()) {
sNew += words.top() ; //在前面加上字母
words.pop();
}
reverse(sNew.begin(), sNew.end());

解法二——以字符串为栈

进一步的以字符串为栈,也是比较快的,消耗内存也比较小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
string removeDuplicates(string s) {
string sNew;
for (int i = 0; i < s.length(); i++) {
if (!sNew.empty() && s[i] == sNew.back()) {
sNew.pop_back();
}
else {
sNew.push_back(s[i]);
}
}
return sNew;
}
};

150. 逆波兰表达式求值

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 '+''-''*''/'
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

用栈来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack <int> nums;
int result;
for (string token:tokens) {
if (token == "+" ) {
result = nums.top();
nums.pop();
result = nums.top() + result;
nums.pop();
nums.push(result);
}
else if (token == "-") {
result = nums.top();
nums.pop();
result = nums.top() - result;
nums.pop();
nums.push(result);
}
else if (token == "*") {
result = nums.top();
nums.pop();
result = nums.top() * result;
nums.pop();
nums.push(result);
}
else if (token == "/") {
result = nums.top();
nums.pop();
result = nums.top() / result;
nums.pop();
nums.push(result);
}
else { // 数字
nums.push(stoi(token));
}
}
return nums.top();
}
};

239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

1
2
3
4
5
6
7
8
9
10
11
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

单调队列问题

只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。

那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列

设计单调队列的时候,pop,和push操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

在单调队列中,我们维护的是非严格单调序列(即非递增非递减),确保重复值不会一次性全部被移除。具体来说,每次只会移除队列中的一部分元素,而不会影响同一值的其他副本。同时每次pop时候只会pop比较最前面的元素,不会对重复值造成影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int numsSize = nums.size();
deque <int> window; //这个双向队列里面存的是索引,而非数字本身,防止有重复,被误pop(?)
vector <int> output;

for (int i = 0; i < numsSize; i++) {
// push操作①: 如果该值比之前的大,那么就pop掉之前的直到剩下比它大的,确保队列递减
while (!window.empty() && nums[i] > nums[window.back()]) {
window.pop_back();
}
window.push_back(i); //push操作②:此时队列中剩下的只有比它大的,且在队头

// 将之前的序号pop
if (!window.empty() && window.front() < i - k + 1) {
window.pop_front();
}

// 取出当前窗口的最大值
if (i >= k - 1) {
output.push_back(nums[window.front()]);
}
}
return output;
}
};

### 347. 前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

解法一——对map的值进行排序

注意力扣不能直接用cmp,得包装一层结构体,同时要将map转化为vector组才能排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
struct cmp{
bool operator ()(pair<int,int> &x, pair<int,int> &y) {
return x.second > y.second;
}
};

vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map <int, int> numsMap; //无序即可,后续要排序
vector <int> output(k);
for (int num:nums) {
numsMap[num] ++;
}
cmp cp;
vector <pair<int,int>> numsMap_vector(numsMap.begin(), numsMap.end());
sort(numsMap_vector.begin(), numsMap_vector.end(), cp);
int i = 0; // output序号
for (auto p = numsMap_vector.begin(); p < numsMap_vector.begin() + k; p++) {
output[i++] = p->first; //这一这里p是一个指针
}
return output;
}
};

解法二——优先级序列

要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。建立一个小顶堆,然后遍历「出现次数数组」:

  • 如果堆的元素个数小于 k,就可以直接插入堆中。
  • 如果堆的元素个数等于 k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有 k 个数字的出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中。
  • 堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

优先级队列的基本操作与普通队列类似,不同的是每次获得队内的元素是优先级最高的元素(要从堆的顶部开始),因此使用的是top()方法,而不是front()方法。如下:

  1. push() :入队。向队列添加一个元素,无返回值;
  2. pop() :将队列中优先级最高的元素出队。将队列中优先级最高的元素删除(出队),无返回值;
  3. top() :获得队列优先级最高的元素。此函数返回值为队列中优先级最高的元素,常与pop()函数一起,先通过top()获得队列中优先级最高的元素,然后将其从队列中删除;
  4. size() :获得队列大小。此函数返回队列的大小,返回值是“size_t”类型的数据,“size_t”是“unsigned int”的别名。
  5. empty() :判断队列是否为空。此函数返回队列是否为空,返回值是bool类型。队列空:返回true;不空:返回false。

——C++——优先级队列(priority_queue)_c++优先队列-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public:
struct cmp_greater{
bool operator ()(pair<int,int> &x, pair<int,int> &y) {
return x.second > y.second;
}
};
struct cmp_less{
bool operator ()(pair<int,int> &x, pair<int,int> &y) {
return x.second < y.second;
}
};

vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map <int, int> numsMap;
vector <int> output;
for (int num:nums) {
numsMap[num] ++;
}

// // 建立优先级序列(用小顶堆, 大于当前节点的要下沉,大小为k)
// priority_queue <pair<int,int>, vector<pair<int,int>>, cmp_greater> pri_que;
// // 扫描所有频率值
// for (auto p = numsMap.begin(); p != numsMap.end(); p++) {
// pri_que.push(*p);
// if (pri_que.size() > k) {
// pri_que.pop();
// }
// }

// 建立优先级序列(用大顶堆, 小于当前节点的要下沉,大小不限制)
priority_queue <pair<int,int>, vector<pair<int,int>>, cmp_less> pri_que;
// 扫描所有频率值
for (auto p = numsMap.begin(); p != numsMap.end(); p++) {
pri_que.push(*p);
}

// 可以按照任意顺序输出, 注意优先级序列也没有begin和end
for (int i = 0; i < k; i++) {
output.push_back(pri_que.top().first); // 取数值
pri_que.pop();
}

return output;
}
};

二叉树

二叉树的种类:

  • 满二叉树
  • 完全二叉树
  • 二叉搜索树(二叉排序树):二叉搜索树是一个有序树,左子树(若非空)上所有结点的值均小于它的根结点的值,右子树(若非空)上所有结点的值均大于它的根结点的值,左右子树也分别为二叉排序树。
  • 平衡二叉搜索树[AVL(Adelson-Velsky and Landis)树]:它是一棵空树或它的左右两个子树的高度(深度)差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
    • C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是O(logn)O(\log n)
    • 红黑树就是一种二叉平衡搜索树
  • 辨析
    1. 平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?——是的,是二叉搜索树和平衡二叉树的结合。
    2. 平衡二叉树与完全二叉树的区别在于底层节点的位置?——是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。
    3. 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?——堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树

二叉树的遍历:

  • 深度优先遍历
    • 前序遍历DLR(递归法,迭代法)
    • 中序遍历LDR(递归法,迭代法)
    • 后序遍历LRD(递归法,迭代法)
  • 广度优先遍历
    • 层次遍历(迭代法)

二叉树的定义:

  • C++:

    1
    2
    3
    4
    5
    6
    struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
    };
  • C:——《数据结构与算法/软件技术基础》周大为版

    1
    2
    3
    4
    5
    typedef struct node {
    int data;
    struct node *lchild, *rchild;
    } bitree;
    bitree *root; //root指向根节点指针
二叉树大纲

——代码随想录

二叉树的深度优先DFS遍历(144前序/145后序/94中序

方法一——递归

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

前序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void preorder (TreeNode *p, vector <int> &output) {
if (p != NULL) {
output.push_back(p->val);
preorder(p->left, output);
preorder(p->right, output);
}
}
vector<int> preorderTraversal(TreeNode* root) {
vector <int> output;
preorder(root, output);
return output;
}
};

中序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
void inorder (TreeNode *p, vector <int> &output) {
if (p != NULL){
inorder(p->left, output);
output.push_back(p->val);
inorder(p->right, output);
}
}

vector<int> inorderTraversal(TreeNode* root) {
vector <int> output;
inorder(root, output);
return output;
}
};

后序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
void postorder(TreeNode *p, vector<int> &output) {
if (p != NULL) {
postorder(p->left, output);
postorder(p->right, output);
output.push_back(p->val);
}
}
vector<int> postorderTraversal(TreeNode* root) {
vector <int> output;
postorder(root, output);
return output;
}
};

方法二——非递归迭代

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!

前序——先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子

二叉树前序遍历(迭代法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector <int> output;
stack <TreeNode*> S;
if (root == NULL) {
return output;
}
S.push(root); // 放入根节点
while (!S.empty()) {
TreeNode *p = S.top();
output.push_back(p->val); //根的值
S.pop();
if (p->right != NULL) { //先压入右子树, 先入栈后出
S.push(p->right);
}
if (p->left != NULL) { //后压入左子树
S.push(p->left);
}
}
return output;
}
};

中序——在遍历左子树之前先把根节点入栈;当左子树遍历完成,根节点出栈,遍历右子树。借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector <int> output;
stack <TreeNode*> S;
TreeNode *p = root;
while (p != NULL || !S.empty()) { // 第一个条件是为了第一次能够满足,以能够先押入根节点
if (p != NULL) { // 指针访问到左边的最底层的
S.push(p);
p = p->left; // 左子树
}
else { // p为空,那么栈里面的就是他的祖先节点(如果目前是右子树的话甚至不一定是父节点),
p = S.top();
S.pop();
output.push_back(p->val); // 根节点
p = p->right; // 右子树
}
}
return output;
}
};

后序

DLR(先序)调整左右顺序DRL(逆先序)reverseLRD(后序)\text{DLR(先序)} \xrightarrow{\text{调整左右顺序}}\text{DRL(逆先序)}\xrightarrow{\text{reverse}}\text{LRD(后序)}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector <int> output;
stack <TreeNode*> S;
if (root == NULL) {
return output;
}
S.push(root);
while (!S.empty()) {
TreeNode *p = S.top();
S.pop();
output.push_back(p->val); // 压入根节点
if (p->left) {
S.push(p->left); //压入左节点,后出
}
if (p->right) {
S.push(p->right); //压入右节点,先出
}
}
reverse(output.begin(), output.end());
return output;
}
};

方法三——二叉树的统一迭代法

将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。

如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

二叉树的层序(广度优先)BFS遍历(102/107/199/637/429/515/116/117

102.二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

基本思想:在上层先被访问的节点,他的下层孩子在该层也会被先访问到。因此使用队列,当一个元素出队,他的孩子将会进入队列。

力扣的题目返回的是二维数组,需要对层也包裹一层,所以需要定义qSize来确定每层的大小,将每层的遍历结果输出到output中。

解法一——非递归迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector <vector<int>> output;
if (root == NULL) {
return output;
}
queue <TreeNode*> Q;
Q.push(root);
while (!Q.empty()) {
int qSize = Q.size(); // 为了返回二维数组,需要知道每层有多少个
vector <int> outputLayer;
for (int i = 0; i < qSize; i++) {
TreeNode *p = Q.front();
outputLayer.push_back(p->val); //根节点出队
Q.pop();
if (p->left) {
Q.push(p->left); //push左孩子入队
}
if (p->right) {
Q.push(p->right); //push右孩子入队
}
}
output.push_back(outputLayer);
}
return output;
}
};
解法二——递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
void levelorderTraversal (TreeNode *p, vector<vector<int>> &output, int depth) {
if (p == NULL) {
return ; //空指针返回父节点。为什么不把判定条件放在访问孩子前呢?因为无法判断是否为空树
}
if (output.size() == depth) {
output.push_back(vector<int>()); //当前层还没有加入过,创建对应层的数组
}
output[depth].push_back(p->val);
// 访问孩子,在孩子对应层的vector尾部加入
levelorderTraversal(p->left, output, depth+1);
levelorderTraversal(p->right, output, depth+1);
}
vector<vector<int>> levelOrder(TreeNode* root) {
vector <vector<int>> output;
int depth = 0;
levelorderTraversal(root, output, depth);
return output;
}
};

107.二叉树的层次遍历II

给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)

对102的正向层次遍历reverse。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> output;
queue <TreeNode*> Q;
if (root == NULL) {
return output;
}
Q.push(root);
while (!Q.empty()) {
int qSize = Q.size();
vector <int> outputLayer;
for (int i = 0; i < qSize; i++) {
outputLayer.push_back(Q.front()->val);
if (Q.front()->left) {
Q.push(Q.front()->left);
}
if (Q.front()->right) {
Q.push(Q.front()->right);
}
Q.pop();
}
output.push_back(outputLayer);
}
reverse(output.begin(),output.end());
return output;
}
};

199.二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

每层层次遍历的最后一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
vector <int> output;
queue <TreeNode*> Q;
if (root == NULL) {
return output;
}
Q.push(root);
while (!Q.empty()) {
int qSize = Q.size();
for (int i = 0; i < qSize; i++) {
if (i == qSize - 1) {// 每层的最后一个元素需要保存值
output.push_back(Q.front()->val);
}
if (Q.front()->left) {
Q.push(Q.front()->left);
}
if (Q.front()->right) {
Q.push(Q.front()->right);
}
Q.pop();
}
}
return output;
}
};

637.二叉树的层平均值

给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10510^{-5}以内的答案可以被接受。

注意与答案相差10510^{-5}以内的答案,所以要用double了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<double> averageOfLevels(TreeNode* root) {
vector<double> output;
queue <TreeNode*> Q;
if (root == NULL) {
return output;
}
Q.push(root);
while (!Q.empty()) {
int qSize = Q.size();
double sumLayer = 0.0;
for (int i = 0; i < qSize; i++) {
sumLayer += Q.front()->val;
if (Q.front()->left) {
Q.push(Q.front()->left);
}
if (Q.front()->right) {
Q.push(Q.front()->right);
}
Q.pop();
}
output.push_back(sumLayer/qSize);
}
return output;
}
};

429.N叉树的层序遍历

给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;

Node() {}

Node(int _val) {
val = _val;
}

Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/

class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
vector <vector<int>> output;
queue <Node*> Q;
if(root != NULL) {
Q.push(root);
}
while (!Q.empty()) {
int qSize = Q.size();
vector <int> outputLayer;
for (int i = 0; i < qSize; i++) {
Node* p = Q.front();
outputLayer.push_back(p->val);
for (int j = 0; j < p->children.size(); j++) {
if (p->children[j] != NULL) {
Q.push(p->children[j]);
}
}
Q.pop();
}
output.push_back(outputLayer);
}
return output;
}
};

515.在每个树行中找最大值

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

注意可能有负数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
vector<int> largestValues(TreeNode* root) {
vector<int> output;
queue <TreeNode*> Q;
if (root != NULL) {
Q.push(root);
}
while (!Q.empty()) {
int qSize = Q.size();
int maxLayer = INT_MIN; //注意有负数
for (int i = 0; i < qSize; i++) {
TreeNode *p = Q.front();
if (p->val > maxLayer) {
maxLayer = p->val;
}
Q.pop();
if (p->left) {
Q.push(p->left); //push左孩子入队
}
if (p->right) {
Q.push(p->right); //push右孩子入队
}
}
output.push_back(maxLayer);
}
return output;
}
};

116.填充每个节点的下一个右侧节点指针

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

1
2
3
4
5
6
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

初始状态下,所有 next 指针都被设置为 NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;

Node() : val(0), left(NULL), right(NULL), next(NULL) {}

Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}

Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
*/

class Solution {
public:
Node* connect(Node* root) {
queue <Node*> Q;
if (root != NULL) {
Q.push(root);
}
while (!Q.empty()) {
int qSize = Q.size();
for (int i = 0; i < qSize; i++) {
Node *p = Q.front();
Q.pop();
if (i != qSize - 1) {
p->next = Q.front();
}
if (p->left) {
Q.push(p->left);
}
if (p->right) {
Q.push(p->right);
}
}
}
return root;
}
};

117.填充每个节点的下一个右侧节点指针II

给定一个二叉树:

1
2
3
4
5
6
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

初始状态下,所有 next 指针都被设置为 NULL

代码同116

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;

Node() : val(0), left(NULL), right(NULL), next(NULL) {}

Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}

Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
*/

class Solution {
public:
Node* connect(Node* root) {
queue <Node*> Q;
if (root != NULL) {
Q.push(root);
}
while (!Q.empty()) {
int qSize = Q.size();
for (int i = 0; i < qSize; i++) {
Node *p = Q.front();
Q.pop();
if (i != qSize - 1) {
p->next = Q.front();
}
if (p->left) {
Q.push(p->left);
}
if (p->right) {
Q.push(p->right);
}
}
}
return root;
}
};

另见

226. 翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

解法一——递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
TreeNode* temp;
if (root != NULL) {
temp = root->left;
root->left = root->right;
root->right = temp;
invertTree(root->left);
invertTree(root->right);
}
return root;
}
};

解法二——迭代(深度优先)

DLR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == NULL) {
return root;
}
stack <TreeNode*> S;
S.push(root);
while (!S.empty()) {
TreeNode *p = S.top();
S.pop();
swap(p->left, p->right); //就这一步不一样
if (p->left) {
S.push(p->left);
}
if (p->right) {
S.push(p->right);
}
}
return root;
}
};

解法三——迭代(广度优先)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
queue <TreeNode*> Q;
if (root != NULL) {
Q.push(root);
}
while (!Q.empty()) {
int qSize = Q.size();
for (int i = 0; i < qSize; i++) {
TreeNode *p = Q.front();
Q.pop();
swap(p->left, p->right); //就这一步不一样
if (p->left) {
Q.push(p->left);
}
if (p->right) {
Q.push(p->right);
}
}
}
return root;
}
};

101. 对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

把NULL存进去的层序遍历。,遍历每一层时候,前半段用stack存进去,后半段pop对比。一个很垃圾的逻辑,一定程度上NULL==0,所以这里使用了非常丑陋的76777777来表示这个位置是NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Solution {
public:
bool isSymmetric(TreeNode* root) {
bool flagSymmetric = true;
int depth = 0;
bool flagNULL = false; // 判断下层是否全空
queue <TreeNode*> Q;
if (root != NULL) {
Q.push(root);
}
while(!Q.empty() && flagSymmetric && !flagNULL) {
int qSize = Q.size();
stack <int> S;
flagNULL = true; // 判断下层是否全空
// cout << qSize << " ";
for (int i = 0; i < qSize; i++) {
TreeNode *p = Q.front();
Q.pop();
if (i < qSize / 2 || depth == 0) { // 第二个条件为了第一层,防止没有东西可pop
if (p == NULL) {
S.push(76777777);
}
else {
S.push(p->val);
}
}
else if (p == NULL) {
if (S.top() != 76777777) {
flagSymmetric = false;
break;
}
S.pop();
}
else if (S.top() != p->val) { // 大于一半,且top和目前的val不等,那么不对称
flagSymmetric = false;
break;
}
else { //大于一半,且目前还对称
S.pop();
}
// 将孩子加入队列,由于要对称,不需要考虑NULL的情况
if (p != NULL) {
Q.push(p->left);
Q.push(p->right);
if (p->left || p->right){ //只要有一个不为0,下一层就不会全空
flagNULL = false;
}
}
}
depth++;
}
return flagSymmetric;
}
};

解法一——递归法

总体思路就是左子树的左边要和右子树的右边相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
bool checkSymmetric(TreeNode *p, TreeNode *q) {
if (!p && !q) { // 都空
return true;
}
else if (!p || !q) { //有一个空
return false;
}
else if (p->val != q->val) { // 非空且不等
return false;
}
// 左子树的左和右子树的右 && 左子树的右和右子树的左
return checkSymmetric(p->left, q->right) && checkSymmetric(p->right, q->left) ;
}

bool isSymmetric(TreeNode* root) {
if (root == NULL) {
return true;
}
return checkSymmetric(root->left, root->right);
}
};

解法二——迭代法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == NULL) {
return true;
}
queue <TreeNode*> Q;
Q.push(root->left);
Q.push(root->right);
while (!Q.empty()) {
TreeNode *p = Q.front(); // 左子树
Q.pop();
TreeNode *q = Q.front(); // 右子树
Q.pop();
if (p == NULL && q == NULL) { //都空
continue;
}
else if (p == NULL || q == NULL) { //有一个空
return false;
}
else if (p->val != q->val) {
return false;
}
Q.push(p->left); //左子树左
Q.push(q->right); //右子树右
Q.push(p->right); //左子树右
Q.push(q->left); //右子树左
}
return true;
}
};

这两道题目基本和本题是一样的,只要稍加修改就可以AC。

100.相同的树

给你两棵二叉树的根节点 pq ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (!p && !q) {
return true;
}
else if (!p || !q) {
return false;
}
else if (p->val != q->val) {
return false;
}
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);

}
};

572.另一个树的子树

给你两棵二叉树 rootsubRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false

二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

先进行层次遍历,遍历到相同的节点值时候,开始比较是否是同一棵树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
bool isSametree (TreeNode *p, TreeNode *q) {
if (!p && !q) {
return true;
}
else if (!p || !q || p->val != q->val){
return false;
}
return isSametree(p->left, q->left) && isSametree(p->right, q->right);
}
bool isSubtree(TreeNode* root, TreeNode* subRoot) {
// 先进行层次遍历,遍历到相同的节点值时候,开始比较是否是同一棵树
queue <TreeNode*> Q;
bool flagSubtree = false;
if (root && subRoot) {
Q.push(root);
}
while (!Q.empty() && !flagSubtree) {
TreeNode *p = Q.front();
Q.pop();
if (p->val == subRoot->val) {
flagSubtree = isSametree(p, subRoot);
}
// cout << p->val << " ";
if (p->left) {
Q.push(p->left);
}
if (p->right) {
Q.push(p->right);
}
}
return flagSubtree;
}
};

三种官方题解:

  • 方法一:深度优先搜索暴力匹配
  • 方法二:深度优先搜索序列上做串匹配
  • 方法三:树哈希

104.二叉树的最大深度

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

递归求解——深度优先遍历

时间复杂度:O(n)O(n),其中nn为二叉树节点的个数。每个节点在递归中只被遍历一次。

空间复杂度:O(height)O(\text{height}),其中height 表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。

——力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int maxDepth(TreeNode* root) {
// 如果根节点为空,则深度为0
if (root == NULL) {
return 0;
}

// 递归计算左右子树的深度
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);

// 返回左右子树深度的最大值加上根节点本身的深度1
return max(leftDepth, rightDepth) + 1;
}
};

非递归迭代——广度优先遍历

  • 时间复杂度:O(n)O(n),其中 n 为二叉树的节点个数。与方法一同样的分析,每个节点只会被访问一次。
  • 空间复杂度:此方法空间的消耗取决于队列存储的元素数量,其在最坏情况下会达到O(n)O(n)​。

——力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
int maxDepth(TreeNode* root) {
queue <TreeNode*> Q;
int depth = 0;
if (root != NULL) {
Q.push(root);
}
while (!Q.empty()) {
int qSize = Q.size();
depth++;
for (int i = 0; i < qSize; i++) {
TreeNode *p = Q.front();
Q.pop();
if (p->left) {
Q.push(p->left);
}
if (p->right) {
Q.push(p->right);
}
}
}
return depth;
}
};

559. N 叉树的最大深度

给定一个 N 叉树,找到其最大深度。

最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。

N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。

递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;

Node() {}

Node(int _val) {
val = _val;
}

Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/

class Solution {
public:
int maxDepth(Node* root) {
if (root == NULL) {
return 0;
}
int depth = 0;
for (int i = 0; i < root->children.size(); i++) {
depth = max(depth, maxDepth(root->children[i]));
}
return depth + 1;
}
};
迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int maxDepth(Node* root) {
queue <Node*> Q;
if (root != NULL) {
Q.push(root);
}
int depth = 0;
while (!Q.empty()) {
int qSize = Q.size();
depth++;
for (int i = 0; i < qSize; i++){
Node* p = Q.front() ;
Q.pop();
for (int j = 0; j < p->children.size(); j++) {
if (p->children[j]) {
Q.push(p->children[j]);
}
}
}
}
return depth;
}
};

111.二叉树的最小深度

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

**说明:**叶子节点是指没有子节点的节点。

注意要考虑一条路到底的情况,即存在单边树的情况。左右孩子都为空的节点才是叶子节点!

image-20240402191845973

非递归迭代——广度优先遍历

注意判断左右为空的时候是“且”,而非“或” 只有当左右孩子都为空的时候,才说明遍历到最低点了。如果其中一个孩子不为空则不是最低点

时间复杂度:O(N)O(N),其中 NN 是树的节点数。对每个节点访问一次。

空间复杂度:O(N)O(N),其中 NN 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。

——力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
int minDepth(TreeNode* root) {
queue <TreeNode*> Q;
int depth = 0;
bool flag = true;
if (root != NULL) {
Q.push(root);
}
while (!Q.empty() && flag) {
int qSize = Q.size();
depth++;
for (int i = 0; i < qSize; i++) {
TreeNode *p = Q.front();
Q.pop();
if (p->left == NULL && p->right == NULL) { //注意这里是且而非或
flag = false;
break;
}
if (p->left != NULL) {
Q.push(p->left);
if (p->right != NULL) {
Q.push(p->right);
}
}
else {
Q.push(p->right); // flag为真时,left为空,right必然不为空,不为真时Q无意义
}
}
}
return depth;
}
};

递归——深度优先遍历

必须要考虑单边树不存在的问题——需要分别考虑根节点左右孩子

时间复杂度:O(N)O(N),其中NN是树的节点数。对每个节点访问一次。

空间复杂度:O(H)O(H),其中 HH 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为O(N)O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logN)O(\log N)

——力扣官方题解

遍历的顺序为后序(左右中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int minDepth(TreeNode* root) {
if (root == NULL) {
return 0;
}
// 必须要考虑单边树不存在的问题——需要分别考虑根节点左右孩子
else if (root->left == NULL) {
return minDepth(root->right) + 1;
}
else if (root->right == NULL) {
return minDepth(root->left) + 1;
}
return min(minDepth(root->left),minDepth(root->right)) + 1;
}
};

222. 完全二叉树的节点个数

给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。

完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。

解法一——层次遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
int countNodes(TreeNode* root) {
queue <TreeNode*> Q;
bool flagEnd = false;
int nums = 0;
if (root != NULL) {
Q.push(root);
nums++;
}
while (!Q.empty() && !flagEnd) {
int qSize = Q.size();
for (int i = 0; i < qSize; i++) {
TreeNode *p = Q.front();
Q.pop();
if (p->left) {
Q.push(p->left);
nums++;
}
else {
flagEnd = true;
break;
}
if (p->right) {
Q.push(p->right);
nums++;
}
else {
flagEnd = true;
break;
}
}
}
return nums;
}
};

解法二——完全二叉树+二分法

这个思路有个问题,二分法的判断标准是大了,小了,这里的判断是是否为NULL。

通过每一次二分查找可以从上到下确定一层是往左走还是往右走。for循环包含depth次,每一次当前层指向right,下面的层都指向left,如果有值,那么该层应该指向right,为NULL则该层指向left,在进行下一层。当指向right的时候得注意nums要增加多少,我感觉我现在就在凑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public:
int countNodes(TreeNode* root) {
int depth = 0;
if (root == NULL) {
return 0;
}
// 先找出二叉树的最大深度
TreeNode *p = root;
while(p != NULL) {
p = p->left;
depth++;
}
vector <bool> direction(depth); //每一次指向的方向,右为0,左为1。
int nums = pow(2, depth - 1) - 1;//前(depth-1)层
// 二分查找最后一层的最后一个顶点
for (int i = 0; i < depth - 1; i++) { //depth次才能判断每次向左向右
TreeNode *p = root;
for (int j = 0; j < i; j++) { // 前i层已经确定方向
if (direction[j] == 1) {
p = p->left;
}
else {
p = p->right;
}
}
p = p->right;
for (int k = i + 1; k < depth - 1; k++) { //后面的层往左
p = p->left;
}
if (p == NULL) { //为NULL,该层指向left
direction[i] = 1;
// cout << " NULL ";
}
else { //指向right
nums += pow(2, depth - i - 2);
// cout << p->val << " ";
}
}
// cout << endl;
// for (int i = 0 ;i < depth-1; i++) {
// cout << direction[i] << " ";
// }
return nums + 1;
}
};

原来官方题解也是这个办法啊,一定分析的比我好。。。

具体做法是,根据节点个数范围的上下界得到当前需要判断的节点个数 kkk,如果第 kkk 个节点存在,则节点个数一定大于或等于 kkk,如果第 kkk 个节点不存在,则节点个数一定小于 kkk,由此可以将查找的范围缩小一半,直到得到节点个数。

如何判断第 kkk 个节点是否存在呢?如果第 kkk 个节点位于第 hhh 层,则 kkk 的二进制表示包含 h+1h+1h+1 位,其中最高位是 111,其余各位从高到低表示从根节点到第 kkk 个节点的路径,000 表示移动到左子节点,111 表示移动到右子节点。通过位运算得到第 kkk 个节点对应的路径,判断该路径对应的节点是否存在,即可判断第 kkk 个节点是否存在。

——力扣官方题解

fig1

完全二叉树的性质

完全二叉树总能拆分成许多的满二叉树。

222.完全二叉树的节点个数1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == NULL) {
return 0;
}
TreeNode *p = root->left;
TreeNode *q = root->right;
int leftDepth = 0;
int rightDepth = 0;
while (p != NULL) {
p = p->left;
leftDepth++;
}
while (q != NULL) {
q = q->right;
rightDepth++;
}
if (leftDepth == rightDepth) { //满二叉树
return (2 << leftDepth) - 1; //位运算,移位运算
}
return countNodes(root->left) + countNodes(root->right) + 1; // 加上根节点
}
};

110. 平衡二叉树

给定一个二叉树,判断它是否是 平衡二叉树

本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

自顶向下的递归

首先计算左右子树的高度,如果左右子树的高度差是否不超过 1,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int height (TreeNode* p) {
if (p == NULL){
return 0;
}
else {
return max(height(p->left), height(p->right)) + 1;
}
}
bool isBalanced(TreeNode* root) {
if (root == NULL) {
return true;
}
else {
return abs(height(root->left) - height(root->right)) <= 1 && isBalanced(root->left) && isBalanced(root->right);
}
}
};

自底向上的递归

自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回 −1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
int height (TreeNode* p) {
if (p == NULL){
return 0;
}
int leftHeight = height(p->left);
int rightHeight = height(p->right);
if (leftHeight == -1 || rightHeight == -1 || abs(leftHeight - rightHeight) > 1) {
return -1;
}
return max(height(p->left), height(p->right)) + 1;

}
bool isBalanced(TreeNode* root) {
if (root == NULL) {
return true;
}
if (height(root) == -1) {
return false;
}
else {
return true;
}
}
};

257. 二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

注意要回溯!!!所以要把之前的节点值存起来

257.二叉树的所有路径

解法一——递归+使用栈

函数参数我就使用了引用,即 vector<int>& path ,这是会拷贝地址的,所以 本层递归逻辑如果有path.push_back(cur->val); 就一定要有对应的 path.pop_back()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
void preorder(TreeNode *p, vector<int> &path,vector <string> &output) {
if (!p->left && !p->right){ //左右为空,终止,记录path
string s;
for (int i = 0; i < path.size(); i++){
s += to_string(path[i]);
s += "->";
}
s += to_string(p->val);
output.push_back(s);
return;
}

if (p->left) {
path.push_back(p->val);
preorder(p->left, path, output);
path.pop_back();// 回溯
}
if (p->right) {
path.push_back(p->val);
preorder(p->right, path, output);
path.pop_back();// 回溯
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector <string> output;
if (root == NULL) {
return output;
}
vector <int> path;
preorder(root, path, output);
return output;
}
};

解法二——递归+使用字符串+隐式回溯

注意path是不变的,隐式回溯了。

使用的是 string path,这里并没有加上引用& ,即本层递归中,path + 该节点数值,但该层递归结束,上一层path的数值并不会受到任何影响。 如图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
void preorder(TreeNode *p, string path,vector <string> &output) {
if (!p->left && !p->right){ //左右为空,终止,记录path
output.push_back(path + to_string(p->val));
return;
}

if (p->left) {
preorder(p->left, path + to_string(p->val) + "->", output); //注意path是不变的,隐式回溯了
}
if (p->right) {
preorder(p->right, path + to_string(p->val) + "->", output);
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector <string> output;
if (root == NULL) {
return output;
}
string path;
preorder(root, path, output);
return output;
}
};

解法三——迭代

使用两个栈,一个存节点,一个存路径。因为路径拼接不易,所以直接存入整体路径。为了防止路径的top被污染,所以和取S的栈顶工作节点p一样,也把路径pathS先取出来,方便后续进一步使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector <string> output;
stack <string> path; // 存放路径
stack <TreeNode*> S; // 前序遍历
if (root != NULL) {
S.push(root);
path.push (to_string(root->val));
}
while (!S.empty()) {
TreeNode *p = S.top();
S.pop();
string pathS = path.top(); //取出路径防止后续拼接时候多拼接一部分
path.pop();
if (!p->left && !p->right) {
output.push_back(pathS);
}
if (p->right){
S.push(p->right);
path.push(pathS + "->" + to_string(p->right->val)); //考虑到stack出来拼接不方便,把存进去的时候直接把之前的也拼上
}
if (p->left){
S.push(p->left);
path.push(pathS + "->" + to_string(p->left->val));
}

}
return output;
}
};

404. 左叶子之和

给定二叉树的根节点 root ,返回所有左叶子之和。

注意是左叶子,不是左节点。

解法一——层次遍历BFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
lass Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
queue <TreeNode*> Q;
int leftSum = 0;
if (root != NULL) {
Q.push(root);
}
while (!Q.empty()) {
int qSize = Q.size();
for (int i = 0; i < qSize; i++){
TreeNode *p = Q.front();
Q.pop();
if (p->left){
Q.push(p->left);
if (!p->left->left && !p->left->right){
leftSum += p->left->val;
}
}
if (p->right) {
Q.push(p->right);
}
}
}
return leftSum;
}
};

解法二——深度优先DFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int preorder (TreeNode* p) {
int ans = 0;
if (p->left) {
if (!p->left->left && !p->left->right) {
ans += p->left->val;
}
else {
ans += preorder(p->left);
}
}
if (p->right) {
ans += preorder(p->right);
}
return ans;
}
int sumOfLeftLeaves(TreeNode* root) {
if (root == NULL) {
return 0;
}
return preorder(root);
}
};

513. 找树左下角的值

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

解法一——BFS+flag

欸,我傻了,这个flag一点用也没有啊。删了也一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue <TreeNode*> Q;
int bottomLeftValue = root->val;
Q.push(root);
bool flagNULL = false;
while (!Q.empty() && !flagNULL){
int qSize = Q.size();
flagNULL = true;
for (int i = 0; i < qSize; i++){
TreeNode *p = Q.front();
Q.pop();
if (i == 0){
bottomLeftValue = p->val;
}
if (p->left){
Q.push(p->left);
flagNULL = false;
}
if (p->right) {
Q.push(p->right);
flagNULL = false;
}
}
}
return bottomLeftValue;
}
};

解法二——BFS,左右节点相反

在遍历一个节点时,需要先把它的非空右子节点放入队列,然后再把它的非空左子节点放入队列,这样才能保证从右到左遍历每一层的节点。广度优先搜索所遍历的最后一个节点的值就是最底层最左边节点的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue <TreeNode*> Q;
int bottomLeftValue = root->val;
Q.push(root);
while (!Q.empty() ){
TreeNode *p = Q.front();
Q.pop();
if (p->right){
Q.push(p->right);
}
if (p->left) {
Q.push(p->left);
}
bottomLeftValue = p->val;
}
return bottomLeftValue;
}
};

DFS

太难了,绕不清

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
void preorder (TreeNode *p, int depth, int &bottomLeftValue, int &maxDepth){ // DLR最先遍历到的就是最左节点
int output = 0;
if(!p->left && !p->right){ //全空
if (depth > maxDepth) {
maxDepth = depth;
bottomLeftValue = p->val;
}
}
if (p->left) {
preorder(p->left, depth + 1, bottomLeftValue, maxDepth);
}
if (p->right) {
preorder(p->right, depth + 1, bottomLeftValue, maxDepth);
}
}

int findBottomLeftValue(TreeNode* root) {
int depth = 0;
int maxDepth = 0;
int bottomLeftValue = 0;
preorder(root, depth + 1, bottomLeftValue, maxDepth);
return bottomLeftValue;
}
};

路径总和(112/113

112. 路径总和

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false

叶子节点 是指没有子节点的节点。

迭代

注意深度优先遍历回溯时候,可能无法减去双亲结点的值,所以得把双亲的值保存,类似于“[257. 二叉树的所有路径](#257. 二叉树的所有路径)”中的保存方式。此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
stack <pair<TreeNode*,int>> S;
if (root != NULL){
S.push(pair<TreeNode*,int>(root, root->val)); //根节点和为0
}
while (!S.empty()){
pair<TreeNode*,int> p = S.top();
S.pop();
if (!p.first->left && !p.first->right) { //叶子
if (p.second == targetSum) {
return true;
}
}
if (p.first->right) {
S.push(pair<TreeNode*,int>(p.first->right, p.second + p.first->right->val));
}
if (p.first->left) {
S.push(pair<TreeNode*,int>(p.first->left, p.second + p.first->left->val));
}
}
return false;
}
};
递归
  1. 确定递归函数的参数和返回类型

    参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。

    再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

    • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
    • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 (opens new window)中介绍)
    • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
  2. 确定终止条件

    不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。

    如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。

  3. 确定单层递归的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
stack <pair<TreeNode*,int>> S;
if (root == NULL){ // 递归时候没有判断左右节点是否非空,所以这里开始的时候要判断
return false;
}
int count = targetSum - root->val;
if (!root->left && !root->right && count == 0) { // 利用减代替加可以少传递一个参数
return true;
}
if (hasPathSum(root->left, count)) { // 如果是true 立刻返回
return true;
}
if (hasPathSum(root->right, count)) {
return true;
}
return false;
}
};

另一个版本——代码随想录

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
if (!root) return false;
if (!root->left && !root->right && sum == root->val) {
return true;
}
return hasPathSum(root->left, sum - root->val) || hasPathSum(root->right, sum - root->val);
}
};

113. 路径总和 II

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

迭代

思路类似“[257. 二叉树的所有路径](#257. 二叉树的所有路径)”中的保存方式,知识这里路径的保存利用stack套vector,而非之前的stack套string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
vector<vector<int>> output;
stack <pair<TreeNode*, int>> S;
stack <vector<int>> pathAll;
if (root) {
S.push(pair<TreeNode*, int>(root, root->val));
pathAll.push(vector<int>(1, root->val)); // 注意这里的vector写法
}
while(!S.empty()) {
pair<TreeNode*, int> p = S.top() ;
S.pop();
vector<int> path = pathAll.top();
pathAll.pop();
// for (int num:path){
// cout << num << " ";
// }
// cout << endl;
if (!p.first->left && !p.first->right && p.second == targetSum){
output.push_back(path);
}
if (p.first->right){
S.push(pair<TreeNode*,int>(p.first->right, p.second + p.first->right->val));
path.push_back(p.first->right->val);
pathAll.push(path);
path.pop_back();
}
if (p.first->left){
S.push(pair<TreeNode*,int>(p.first->left, p.second + p.first->left->val));
path.push_back(p.first->left->val);
pathAll.push(path);
path.pop_back();
}
}
return output;
}
};
递归

不需要返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
vector<vector<int>> output;
vector<int> path;

void findPath(TreeNode* p, int targetSum) {
if (p == NULL) {
return;
}
path.push_back(p->val);
if (!p->left && !p->right && targetSum == 0) {
output.push_back(path);
}
if (p->left) {
findPath(p->left, targetSum - p->left->val);
}
if (p->right) {
findPath (p->right, targetSum - p->right->val);
}
path.pop_back();
}
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
if (root == NULL) {
return output;
}
findPath (root, targetSum - root->val);
return output;
}
};

从遍历序列恢复二叉树

  • 由DLR和LDR的遍历序列可以唯一地确定一棵二叉树
  • 由LRD和LDR的遍历序列可以唯一地确定一棵二叉树
  • 通过DLR或者LRD的遍历序列确定二叉树或子树的根节点,通过LDR确定左右子树的序列。

106. 从中序与后序遍历序列构造二叉树

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

一层一层切割

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间

img106.从中序与后序遍历序列构造二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
// 递归
// 数组大小为0,空节点
if (inorder.empty() && postorder.empty()) {
return NULL;
}

//不为空,postorder的最后一个元素是根节点
TreeNode* p = new TreeNode (postorder[postorder.size() - 1]);

// 找到中序的切割点
int cutPoint;
for (cutPoint = 0; cutPoint < inorder.size(); cutPoint++) {
if (inorder[cutPoint] == postorder[postorder.size() - 1]) {
break;
}
}

// 分割中序 (左闭右开)
vector<int> inorder_left(inorder.begin(), inorder.begin() + cutPoint);
vector<int> inorder_right(inorder.begin() + cutPoint + 1, inorder.end()); //不包含分割点

// 分割后序 (左闭右开)中序数组大小一定是和后序数组的大小相同的
vector<int> postorder_left(postorder.begin(), postorder.begin() + inorder_left.size());
vector<int> postorder_right(postorder.begin() + inorder_left.size(), postorder.end() - 1); //不包含分割点,最后一个

// 对左右子树递归
p->left = buildTree(inorder_left, postorder_left);
p->right = buildTree(inorder_right, postorder_right);

return p;
}
};

为了减少vector的查找时间,官方题解建立(元素,下标)键值对的哈希

这里注意一个细节:为什么可以用下标减一来表示后序的每次根节点呢?因为他先构造右子树,每次的根节点刚好是最后一个,而当右子树构造完以后正好下标来到左子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
int post_idx;
unordered_map<int, int> idx_map;
public:
TreeNode* helper(int in_left, int in_right, vector<int>& inorder, vector<int>& postorder){
// 如果这里没有节点构造二叉树了,就结束
if (in_left > in_right) {
return nullptr;
}

// 选择 post_idx 位置的元素作为当前子树根节点
int root_val = postorder[post_idx];
TreeNode* root = new TreeNode(root_val);

// 根据 root 所在位置分成左右两棵子树
int index = idx_map[root_val];

// 下标减一
post_idx--;
// 构造右子树
root->right = helper(index + 1, in_right, inorder, postorder);
// 构造左子树
root->left = helper(in_left, index - 1, inorder, postorder);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
// 从后序遍历的最后一个元素开始
post_idx = (int)postorder.size() - 1;

// 建立(元素,下标)键值对的哈希表
int idx = 0;
for (auto& val : inorder) {
idx_map[val] = idx++;
}
return helper(0, (int)inorder.size() - 1, inorder, postorder);
}
};

——官方题解

105. 从前序与中序遍历序列构造二叉树

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的先序遍历inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

利用无序映射做

这里利用pre_index++来表示前序的根节点也是和上述同理。更好理解的话还是把先序、中序的头尾都放在helper函数的输入里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
int pre_index;
unordered_map <int, int> idx_map;
public:
TreeNode* helper (int in_begin, int in_end, vector<int> & preorder, vector<int>& inorder) {
if (in_begin > in_end) {
return NULL;
}

// 找到分割点
int cutPoint = idx_map[preorder[pre_index]]; // 这是分割点在inorder中的下标索引

// 建立节点
TreeNode *p = new TreeNode(preorder[pre_index]); // 分割点的值是preorder[begin]

// 前序的分割点递增
pre_index++;

//左右子树
p->left = helper(in_begin, cutPoint - 1, preorder, inorder);
p->right = helper(cutPoint + 1, in_end, preorder, inorder);

return p;
}

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
pre_index = 0;
int index = 0;
for (int num:inorder) {
idx_map[num] = index++;
}
return helper(0, inorder.size() - 1, preorder, inorder);
}
};

654. 最大二叉树

给定一个不重复的整数数组 nums最大二叉树 可以用下面的算法从 nums 递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值 左边子数组前缀上 构建左子树。
  3. 递归地在最大值 右边子数组后缀上 构建右子树。

返回 nums 构建的最大二叉树

思路非常类似于“从遍历序列恢复二叉树”,但是这里需要注意的是不需要建立哈希表存储索引和值的关系,因为你每次要在序列中找到最大值,还是需要通过for循环来寻找,因此unordered_map在这里作用不大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
TreeNode* helper (vector<int>& nums, int begin, int end) {
if (begin > end) {
return NULL;
}
// 递归求解,每次先找最大
int maxNumIndex = begin;
for(int i = begin + 1; i <= end; i++){ //从begin+1开始可以减少工作量哈
if (nums[i] > nums[maxNumIndex]) {
maxNumIndex = i;
}
}
TreeNode* root = new TreeNode(nums[maxNumIndex]);
root->left = helper(nums, begin, maxNumIndex - 1);
root->right = helper(nums, maxNumIndex + 1, end);
return root;
}
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
return helper(nums, 0, nums.size() - 1);
}
};

617. 合并二叉树

给你两棵二叉树: root1root2

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。

返回合并后的二叉树。

注意: 合并过程必须从两个树的根节点开始。

解法一——迭代+层次遍历+列举所有情况

非常朴素的想法,层次遍历+列举所有情况,将左右操作都从第二个子树合并到第一个子树上,所有的节点不存在问题,都通过创建0节点解决。这里需要注意的是不光要将0节点加到队列中以保证两子树的队列长度一致,同时子树1是要返回的,那么他的具体节点也需要有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
// 层次遍历,在root1上操作
queue <TreeNode*> Q1;
queue <TreeNode*> Q2;
if (root1 != NULL || root2 != NULL) {
if (root1 == NULL) {
root1 = new TreeNode(0);
}
if (root2 == NULL) {
root2 = new TreeNode(0);
}
Q1.push(root1);
Q2.push(root2);
}
while (!Q1.empty() || !Q2.empty()) {
TreeNode *p1 = Q1.front();
Q1.pop();
TreeNode *p2 = Q2.front();
Q2.pop();
if (p1 && p2) {
p1->val += p2->val;
}
// left、right四种情况
// if (p1->left) {
// Q1.push(p1->left);
// if (p2->left) {
// Q2.push(p2->left);
// }
// else {
// Q2.push(new TreeNode(0));
// }
// }
// if (p1->right) {
// Q1.push(p1->right);
// if (p2->right) {
// Q2.push(p2->right);
// }
// else {
// Q2.push(new TreeNode(0));
// }
// }
// if (!p1->left && p2->left) {
// p1->left = new TreeNode(0);
// Q1.push(p1->left);
// Q2.push(p2->left);
// }
// if (!p1->right && p2->right) {
// p1->right = new TreeNode(0);
// Q1.push(p1->right);
// Q2.push(p2->right);
// }
// 其余的p1、p2左右子树均不存在的情况不需要考虑
// 综合一下只有两种情况,都不存在,和其他
if (p1->left || p2->left){
if(!p1->left){
p1->left = new TreeNode(0);
}
if(!p2->left){
p2->left = new TreeNode(0);
}
Q1.push(p1->left);
Q2.push(p2->left);
}
if (p1->right || p2->right){
if(!p1->right){
p1->right = new TreeNode(0);
}
if(!p2->right){
p2->right = new TreeNode(0);
}
Q1.push(p1->right);
Q2.push(p2->right);
}
}
return root1;
}
};

解法二——递归+深度优先

如果两个二叉树的对应节点都为空,则合并后的二叉树的对应节点也为空;

如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点;且即使其中一个有子树也不需要管了,毕竟另外一个直接空了

如果两个二叉树的对应节点都不为空,则合并后的二叉树的对应节点的值为两个二叉树的对应节点的值之和,此时需要显性合并两个节点。

——力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if (!root1 && !root2){
return NULL;
}
if (!root1 && root2) {
return root2;
}
if (root1 && !root2) {
return root1;
}
TreeNode *merged = new TreeNode (root1->val + root2->val);
merged->left = mergeTrees(root1->left, root2->left);
merged->right = mergeTrees(root1->right, root2->right);
return merged;
}
};

700. 二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

二叉搜索树(二叉排序树)是一个有序树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉搜索树

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if (!root) {
return NULL;
}
TreeNode *p = root;
while (p != NULL) {
if (p->val == val) {
return p;
}
if (p->val > val) {
p = p->left;
}
else {
p = p->right;
}
}
return NULL;
}
};

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if (!root || root->val == val){
return root;
}
if (root->val > val) {
return searchBST(root->left, val);
}
else {
return searchBST(root->right, val);
}
}
};

98. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了,而是左子树都小于中间节点,右子树都大于中间节点。

要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。

迭代——中序遍历

定义flag,防止第一次比较order超范围有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
bool isValidBST(TreeNode* root) {
// 中序遍历
stack <TreeNode*> S;
vector<int> order;
bool flag = true;
TreeNode *p = root;
while (p != NULL || !S.empty()){
if (p != NULL) {
S.push(p);
p = p->left;
}
else {
p = S.top();
S.pop();
if (flag) {
order.push_back(p->val);
flag = false;
// cout << p->val << " ";
}
else if (p->val > order[order.size() - 1]) {
order.push_back(p->val);
// cout << p->val << " ";
}
else {
return false;
}
p = p->right;
}
}
return true;
}
};

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
TreeNode *pre = NULL; //记录前一个,中序的前一个总比后一个小
bool isValidBST(TreeNode* root) {
if (root == NULL) {
return true;
}
bool validLeft = isValidBST(root->left);

if (pre != NULL && pre->val >= root->val) {
return false;
}
pre = root;

bool validRight = isValidBST(root->right);

return validLeft && validRight;
}
};

530. 二叉搜索树的最小绝对差

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值

差值是一个正数,其数值等于两值之差的绝对值。

在有序数组求任意两数最小值差等价于相邻两数的最小值差

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
int getMinimumDifference(TreeNode* root) {
stack <TreeNode*> S;
TreeNode *p = root;
int pre = -1;
int minDistance = INT_MAX;
while(p || !S.empty()){
if(p){
S.push(p);
p = p->left;
}
else {
p = S.top();
S.pop();
if((pre != -1) && (p->val - pre) < minDistance) {
minDistance = p->val - pre;
}
pre = p->val;
p = p->right;
}
}
return minDistance;
}
};

501. 二叉搜索树中的众数

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。

假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • 左子树和右子树都是二叉搜索树

几个可以利用的特点:

  1. 中序遍历是有序的
  2. 遍历一次就可以找到所有的众数——如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),不仅要更新maxCount,而且要清空结果集

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public:
int count;
int maxCount;
vector<int> output;
TreeNode* pre = NULL; //不能直接定义int类型,否则,NULL==0
void inorder(TreeNode* p){
if(p->left) {
inorder(p->left);
}
// 判断、计数
if (pre == NULL){
count = 1;
}
else if (pre->val == p->val) {
count++;
}
else { //不相等清零
count = 1;
}
pre = p; //更新节点
//比较
if (count > maxCount) {
maxCount = count;
output.clear();
output.push_back(p->val);
}
else if (count == maxCount) {
output.push_back(p->val);
}
if(p->right) {
inorder(p->right);
}
}

vector<int> findMode(TreeNode* root) {
if (root == NULL) {
return output;
}
int maxCount = 0;
inorder(root);
return output;
}
};

236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

236.二叉树的最近公共祖先2

  • 如何从底向上遍历?
  • 遍历整棵树,还是遍历局部树?
  • 如何把结果传到根节点的?

思路有点难想,一共分为两种情况:

  • 情况一:p、q分属在一个顶点的左右子树上
  • 情况二:p在q的子树下,q在p的子树下

首先考虑“情况一”,在递归寻找的过程中,如果我能找到这个其中一个节点,我就回传(此时不需要考虑是否为叶子节点,为什么呢?此时其实就是“情况二”)。如果一个顶点的左右都有值,那就说明这个节点是公共节点,又因为我们是从下到上回溯的,所以碰到的一定是最近公共祖先(也就是最深的,再往上的一定至少有一边为NULL)。

下面考虑“情况二”的问题。如果p在q的子树下或q在p的子树下,题目中又保证pq 均存在于给定的二叉树中,且互不相同,那么是不是就可以说假设我现在找到了p,如果我在后续的递归遍历中再也没有找到q,那么q一定就在p的子树中,所以这就解决了一个节点属于另一个元素的孩子的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) {
return root; //NULL就直接回传,如果遇到需要找的p、q也回传,注意如果q(p)在p(q)的子树下就不往下递归了,虽然我没递归到,但是一定在这个下面。
}
// 左右子树递归寻找
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
if (left && right) {
return root; // 如果左右子树都有,那他就是最近公共祖先
}
else if (left && !right) {
return left;
}
else if (right && !left) {
return right;
}
else { //都为空
return NULL;
}

}
};

235. 二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

  • 公共祖先:因为是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的值 一定是在[p,q][p, q]​区间的。即 中节点->val > p->val && 中节点->val < q->val 或者 中节点->val > q->val && 中节点->val < p->val
  • 最近公共祖先:如果一个节点值大于根节点,一个节点值小于根节点,说明他们他们一个在根节点的左子树上一个在根节点的右子树上,那么根节点就是他们的最近公共祖先节点。【反证法】此时,如果最近公共祖先在左子树中,说明p、q必须都得在左子树中,和“一个结点值大于根节点”矛盾;如果最近公共祖先在右子树中,说明p、q必须都得在右子树中,和“一个结点值小于根节点”矛盾;只剩最后一种情况:根节点就是最近公共祖先呗

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root->val > p->val && root->val > q->val) { //p,q都在左子树
return lowestCommonAncestor(root->left, p, q);
}
if (root->val < p->val && root->val < q->val) { //p,q都在右子树
return lowestCommonAncestor(root->right, p, q);
}
if ((root->val <= p->val && root->val >= q->val) || (root->val >= p->val && root->val <= q->val)) { // 分叉点或者就是其中一个节点,其实这个就是上两个以外的else
return root;
}
return NULL;
}
};

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
while (root) {
if (root->val > p->val && root->val > q->val) { //p,q都在左子树
root = root->left;
}
else if (root->val < p->val && root->val < q->val) { //p,q都在右子树
root = root->right;
}
else { // 分叉点或者就是其中一个节点,其实这个就是上两个以外的else
return root;
}
}
return NULL;
}
};

701. 二叉搜索树中的插入操作

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果

根据定义完成。

  • 注意root因为要返回,所以不能直接对他操作
  • 需要考虑空root的问题

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == NULL) { // 考虑空节点的问题
return new TreeNode(val);
}
TreeNode *p = root;
while (p) {
if (val > p->val) {
if (p->right) {
p = p->right;
}
else {
p->right = new TreeNode(val);
break;
}
}
else {
if (p->left) {
p = p->left;
}
else {
p->left = new TreeNode(val);
break;
}
}
}
return root; // 注意root不能变
}
};

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == NULL) { // 考虑空节点的问题,递归中其实就是最后一步
return new TreeNode(val);
}
if (val > root->val) {
root->right = insertIntoBST(root->right, val); //接收到的就是返回的原始root,只是新增了子节点
}
else {
root->left = insertIntoBST(root->left, val);
}
return root;
}
};

450. 删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。
  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • 找到删除的节点
    • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。

其实第五种情况简单来说就是,删除节点的左子树的任意一个元素,一定小于右子树的任意元素,那我直接放到右子树的最小元素的左子树上就好啦!

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == NULL) {
return root;
}
// 先要找到删除元素
TreeNode *p = root;
TreeNode *pParent = NULL;
bool direction = true; // 布尔值判断是在parent的左子树(true)还是右子树(false)
while (p) {
if (key > p->val) {
pParent = p;
p = p->right;
direction = false;
}
else if(key < p->val) {
pParent = p;
p = p->left;
direction = true;
}
else { //相等
break;
}
}
if (p == NULL) { //没找到
return root;
}
// 对删除元素操作
TreeNode *q = NULL; // q用于指示pParent后续指向何方
if (!p->left && !p->right) { // 全空
q = NULL;
}
else if (!p->left && p->right) { // 左空右不空
q = p->right;
}
else if (p->left && !p->right) { // 左不空右空
q = p->left;
}
else {// 左右都不空
q = p->right; //指向右子树,并将原来的左子树放在原来的右子树的最左下方
TreeNode *pLeft = p->left; //原来的左子树
p = p->right;
while (p->left) {
p = p->left; // 指向原来的右子树的最左下方
}
//此时p->left为空
p->left = pLeft;
}
// 将新子树节点放置在原来pParent下方,这里需要注意,万一删除的节点就是根节点的情况
if (pParent) { //删除的节点非根节点
if(direction) {
pParent->left = q;
}
else {
pParent->right = q;
}
return root;
}
else { //删除的节点就是根节点
return q;
}
}
};

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == NULL) {
return root;
}

// 对删除元素操作
if (root->val == key) {
if (!root->left && !root->right) { // 全空
return NULL;
}
else if (!root->left && root->right) { // 左空右不空
return root->right;
}
else if (root->left && !root->right) { // 左不空右空
return root->left;
}
else {// 左右都不空
TreeNode *rootRight = root->right;
while (rootRight->left) {
rootRight = rootRight->left; // 指向原来的右子树的最左下方
}
//此时rootRight->left为空
rootRight->left = root->left;
// cout << rootRight->val << " ";
return root->right;
}
}

// 递归寻找左右
if (root->val > key) {
root->left = deleteNode(root->left, key);
}
else {
root->right = deleteNode(root->right, key);
}
return root;
}
};

669. 修剪二叉搜索树

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

先找到两个临界点值。

  • low:工作节点p满足p->val >= low && p->left->val < low, 则p = p->right
  • high:工作节点p满足p->val <= high && p->right->val > high, 则p = p->left

例如对于low的临界点来说,他一定是左孩子,双亲一定大于他,他本身的右孩子也一定大于他。**但是要注意右孩子可能还是会有小于low的情况出现!!!**所以要用while

此外处理根节点不在范围内的问题时候,不能分开来考虑,例如先low,后high,否则可能有问题。

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
while (root && (root->val > high || root->val < low)) { // 不能分开来,先左后右
if (root->val > high) {
root = root->left;
}
else {
root = root->right;
}
}
if (!root) {
return root;
}
TreeNode *p = root;
// 找low的临界点
while (p) {
while (p->val >= low && p->left && p->left->val < low) { // 注意这里是while
p->left = p->left->right;
}
p = p->left; // 还没到临界点
}
p = root;
// 找high的临界点
while (p) {
while (p->val <= high && p->right && p->right->val > high) {// 注意这里是while
p->right = p->right->left;
}
p = p->right; // 还没到临界点
}
return root;
}
};

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (root == NULL) {
return root;
}
if (root->val < low) {
return trimBST(root->right, low, high);
}
if (root->val > high) {
return trimBST(root->left, low, high);
}

root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);

return root;
}
};

108. 将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。

平衡二叉树 是指该树所有节点的左右子树的深度相差不超过 1。

注意是所有节点的左右子树的深度相差不超过 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
TreeNode* helper (vector<int>&nums, int left, int right){
if (left > right) {
return NULL;
}
TreeNode* p = new TreeNode(nums[(left + right) / 2]);
p->left = helper(nums, left, (left + right) / 2 - 1);
p->right = helper(nums, (left + right) / 2 + 1, right);
return p;
}

TreeNode* sortedArrayToBST(vector<int>& nums) {
return helper(nums, 0, nums.size() - 1);
}
};

538. 把二叉搜索树转换为累加树

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。

逆中序遍历

迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
TreeNode* convertBST(TreeNode* root) {
stack <TreeNode*> S;
TreeNode *p = root;
int sum = 0;
while(p || !S.empty()){
if (p) {
S.push(p);
p = p->right;
}
else {
p = S.top();
S.pop();
sum += p->val;
p->val = sum;
p = p->left;
}
}
return root;
}
};

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int sum = 0;
TreeNode* convertBST(TreeNode* root) {
if (root == NULL) {
return root;
}
if (root->right) {
root->right = convertBST(root->right);
}
sum += root->val;
root->val = sum;
if (root->left) {
root->left = convertBST(root->left);
}
return root;
}
};

回溯

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合

    • 剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。
    • 一个集合来求组合的话,就需要startIndex;多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
    • 去重问题——used
  • 切割问题:一个字符串按一定规则有几种切割方式

  • 子集问题:一个N个数的集合里有多少符合条件的子集

    • 在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果。
  • 排列问题:N个数按一定规则全排列,有几种排列方式

  • 棋盘问题:N皇后,解数独等等

组合是不强调元素顺序的,排列是强调元素顺序

回溯法解决的问题都可以抽象为树形结构!因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。for循环横向遍历,递归纵向遍历,回溯不断调整结果集
回溯算法理论基础

  • 在for循环上做剪枝操作是回溯法剪枝的常见套路!

模板

1
2
3
4
5
6
7
8
9
10
11
12
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
回溯算法大纲

以下来自回溯算法入门级详解 + 练习(持续更新)

「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性。

回溯算法用于 搜索一个问题的所有的解 ,通过深度优先遍历的思想实现。

与动态规划的区别:

  • 共同点——用于求解多阶段决策问题。多阶段决策问题即:

    • 求解一个问题分为很多步骤(阶段);

    • 每一个步骤(阶段)可以有多种选择。

  • 不同点

    • 动态规划只需要求我们评估最优解是多少,最优解对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果;
    • 回溯算法可以搜索得到所有的方案(当然包括最优解),但是本质上它是一种遍历算法,时间复杂度很高。

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。

77.组合1

解法一——暴力回溯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;
void backtracking(int n, int k, int start) {
if (temp.size() == k) {
output.push_back(temp);
return;
}

for (int i = start; i <= n; i++) {
temp.push_back(i);
backtracking(n, k, i + 1); //递归
temp.pop_back(); //回溯
}
}

vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return output;
}
};

解法二——回溯+剪枝

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

  1. 已经选择的元素个数:path.size();
  2. 还需要的元素个数为: k - path.size();
  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;
void backtracking(int n, int k, int start) {
if (temp.size() == k) {
output.push_back(temp);
return;
}

for (int i = start; i <= n - (k - temp.size()) + 1; i++) {
temp.push_back(i);
backtracking(n, k, i + 1); //递归
temp.pop_back(); //回溯
}
}

vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return output;
}
};

216. 组合总和 III

找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

隐含sum的回溯,通过两个方法剪枝

  • i的范围,要保证后续有这么多的树可以取
  • sum和target的关系,如果加上当前数target已经炸了,那么就不往下了,这里隐含了一点,i是从小到大排列的,加上这个比较小的都不行,后面的就都不用加了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(int k, int target, int start, int sum) {
if (temp.size() == k && sum == target) {
output.push_back(temp);
return;
}

for (int i = start; i <= 9 - (k - temp.size()) + 1; i++) {
if (sum + i > target) {
break;
}
temp.push_back(i);
backtracking (k, target, i + 1, sum + i); // 隐含sum的回溯
temp.pop_back();
}
}

vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 1, 0);
return output;
}
};

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。img

注意alphabet的定义方式是大括号。此外digitsStart不能使用stoi,这个也不知道是为什么。此外需要注意为空的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<string> output;
string temp;
vector<string> alphabet = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
void backtracking(string digits, int start) {
if (start == digits.length()) {
output.push_back(temp);
return ;
}
int digitsStart = digits[start] - '0';
for (int i = 0; i < alphabet[digitsStart].length(); i++) {
temp.push_back(alphabet[digitsStart][i]);
backtracking(digits, start + 1);
temp.pop_back();
}
}

vector<string> letterCombinations(string digits) {
if (digits.length() == 0) {
return output;
}
backtracking(digits, 0);
return output;
}
};

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

  • 组合没有数量要求
  • 元素可无限重复选取

有两点需要注意:

  • 递归的时候下一次的start是i,不是i+1,也不是start。不是start(因为是这个数及其以后的可选),也不是i+1(因为可以重复选取)
  • 剪枝时候的判定条件sum + candidates[i] > targetbreak,数组需要有序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;
void backtracking (vector<int>& candidates, int target, int sum, int start) {
if (sum == target) {
output.push_back(temp);
return;
}

for (int i = start; i < candidates.size(); i++) {
if (sum + candidates[i] > target) {
break;
}
temp.push_back(candidates[i]);
backtracking(candidates, target, sum + candidates[i], i); // 隐含sum的回溯,注意这里是i不是start(因为是这个数及其以后的可选),也不是i+1(因为可以重复选取)
temp.pop_back();
}
}

vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 注意需要有序的时候,我们的break策略才是成立的
backtracking(candidates, target, 0, 0);
return output;
}
};

差不多的写法,只是把target直接改了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(vector<int>& candidates, int target, int startIdx) {
if (target < 0) {
return;
}
if (target == 0) {
output.emplace_back(temp);
return;
}
for (int i = startIdx; i < candidates.size(); i++) {
temp.emplace_back(candidates[i]);
backtracking(candidates, target - candidates[i], i);
temp.pop_back();
}
}

vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0);
return output;
}
};

40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

**注意:**解集不能包含重复的组合。

candidates中有重复的数字,这个重复数字可以在同一个解里面使用,但是解集不能包含重复的元素。例如:

1
2
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:[[1,1,6],[1,2,5],[1,7],[2,6]]

所以不能直接通过以下方式将输入candidates数组去重来实现

1
2
3
sort(candidates.begin(), candidates.end());
auto end_unique = unique(candidates.begin(), candidates.end());
candidates.erase(end_unique, candidates.end());

我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重

40.组合总和II

与39题的区别:

  • 同意元素不能无限取,所以递归时候是i+1
  • 加入了这一个判断i > start && candidates[i] == candidates[i-1],以防止出现一摸一样的结果。第二部分很好理解,就是说如果这一个和上一个值是一样的,那我就跳过。但是注意到同一个树枝上面的元素是可以重复的,所以需要i > start这一约束,防止直接不让重复了,也就保证了示例中的[1,1,6]这组解还是能够出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;
void backtracking (vector<int>& candidates, int target, int sum, int start) {
if (sum == target) {
output.emplace_back(temp);
return;
}
for (int i = start; i < candidates.size(); i++) {
if (i > start && candidates[i] == candidates[i-1]) {
continue;
}
if (sum + candidates[i] > target) {
break;
}
temp.emplace_back(candidates[i]);
backtracking(candidates, target, sum + candidates[i], i + 1);
temp.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return output;
}
};

131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是向前和向后读都相同的字符串。

解法一——暴力回溯

检测到是回文串送入temp

  • 在处理组合问题的时候,递归参数需要传入start,表示下一轮递归遍历的起始位置,这个start就是切割线。
  • 为了方便处理回文串,也要传入start和end,就可以减少切割。
  • start == s.length() - 1时,最后一个值还没有传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
vector<vector<string>> output;
vector<string> temp;

bool isPalindrome (string s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}

void backtracking (string s, int start) {
if (start > s.length() - 1) { // 注意这里是>,而非==,因为最后一个start也要存入
output.emplace_back(temp);
return;
}

for (int i = start; i < s.length(); i++) {
if (isPalindrome(s, start, i)) {
temp.emplace_back(s.substr(start, i - start + 1));
}
else {
continue;
}
backtracking(s, i + 1);
temp.pop_back();
}
}

vector<vector<string>> partition(string s) {
backtracking(s, 0);
return output;
}
};

解法二——回溯+优化回文串的识别

给定字符串"abcde", 在已知"bcd"不是回文字串时, 不再需要去双指针操作"abcde"而可以直接判定它一定不是回文字串。

具体来说, 给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]s[1:n-1]是回文字串。

image-20240410155458022

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public:
vector<vector<string>> output;
vector<string> temp;
vector<vector<bool>> isPalindrome; //放事先计算好的是否回文子串的结果

void computePalindrome (string &s) {
// isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串
isPalindrome.resize(s.length(), vector<bool> (s.length(), true)); // 创建二维数组,长度为s.length() x s.length(),全部赋值为true
for (int i = s.length() - 1; i >= 0; i--) {
for(int j = i + 1; j < s.length(); j++) {
// 需要保证计算[i, j]区间时候,[i+1, j-1]区间已经计算完毕
isPalindrome[i][j] = (s[i] == s[j]) && isPalindrome[i+1][j-1];
// if (j == i) {isPalindrome[i][j] = true;} // 等于的时候必然是true,不需要重新赋值
// else if (j - i == 1) {isPalindrome[i][j] = (s[i] == s[j]);} //差一位的时候,第二个条件相当于已经满足了,因为是true
// else {isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i+1][j-1]);}
}
}
}

void backtracking (string s, int start) {
if (start > s.length() - 1) { // 注意这里是>,而非==,因为最后一个start也要存入
output.emplace_back(temp);
return;
}

for (int i = start; i < s.length(); i++) {
if (isPalindrome[start][i]) {
temp.emplace_back(s.substr(start, i - start + 1));
backtracking(s, i + 1);
temp.pop_back();
}
}
}

vector<vector<string>> partition(string s) {
computePalindrome(s);
backtracking(s, 0);
return output;
}
};

93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201""192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

先考虑暴力回溯,在考虑剪枝

解法一——暴力回溯,temp是字符串数组

注意这里不需要代码中对i的for循环,这个本来就是递归要做的循环,还是没有理解透啊,加入这个循环以后会导致缺数。

这里也考虑了一点剪枝的问题

  • 比如j < start + 3,就是每个segment值不会大于255,那么就不会大于3.
  • 此外if (segment > 4) {continue;},就是对于segment分段总数的限制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Solution {
public:
vector<string> output;
vector<string> temp;
bool isValidSegment (string sSegment) { // 左右皆为闭
if (sSegment.size() > 1 && sSegment[0] == '0') { // 多位前导0的情况
return false;
}
// cout << sSegment << ",";
int num = stoi(sSegment);
if (num < 0 || num > 255){
return false;
}
else {
return true;
}
}

void backtracking(string s, int start, int segment) {
if (start == s.length() && segment == 4){
// string tempOutput = temp[0] + "." + temp[1] + "." + temp[2] + "." + temp[3];
// cout << temp[0] + "." + temp[1] + "." + temp[2] + "." + temp[3] << endl;
output.emplace_back(temp[0] + "." + temp[1] + "." + temp[2] + "." + temp[3]);
return;
}


// for (int i = start; i < s.length(); i++){
for (int j = start; j < start + 3 && j < s.length(); j++) {
string sSegment = s.substr(start, j - start + 1);
if (segment > 4) {
continue;
}
if (isValidSegment(sSegment)) {
temp.emplace_back(sSegment);
backtracking(s, j + 1, segment + 1);
temp.pop_back();
}
}
// }
}

vector<string> restoreIpAddresses(string s) {
backtracking(s, 0, 0);
return output;
}
};

解法二——temp就是对源字符串加点

代码随想录的方法

93.复原IP地址

1
2
3
4
5
6
7
8
9
for (int i = startIndex; i < s.size(); i++) {
if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法
s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点
pointNum++;
backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2
pointNum--; // 回溯
s.erase(s.begin() + i + 1); // 回溯删掉逗点
} else break; // 不合法,直接结束本层循环
}

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

数组的 子集 是从数组中选择一些元素(可能为空)。

其实还是一个组合问题,和顺序无关。

子集是收集树形结构中树的所有节点的结果

而组合问题、分割问题是收集树形结构中叶子节点的结果

解法一——递归

思路一——每个元素有选和不选两个情况

递归的过程就是每次去掉一个数,再求子集。而每个元素又选和不选两个情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking (vector<int> nums, int start) {
if (start == nums.size()) {
output.emplace_back(temp);
return;
}

temp.emplace_back(nums[start]);
backtracking(nums, start + 1);
temp.pop_back();
backtracking(nums, start + 1);

}

vector<vector<int>> subsets(vector<int>& nums) {
// vector<int> empty;
// output.emplace_back(empty);
backtracking(nums, 0);
return output;
}
};
思路二——遍历整个树的所有节点

78.子集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking (vector<int> nums, int start) {
output.emplace_back(temp);
if (start >= nums.size()) {
return;
}

for (int i = start; i < nums.size(); i++) {
temp.emplace_back(nums[i]);
backtracking(nums, i + 1);
temp.pop_back();
}

// backtracking(nums, start + 1);

}

vector<vector<int>> subsets(vector<int>& nums) {
// vector<int> empty;
// output.emplace_back(empty);
backtracking(nums, 0);
return output;
}
};

解法二——枚举迭代

创建一个0,1的mask实现枚举,相当于一个二进制的数组和当前数组取

其中,<<是左移运算,每次乘2.

  • 1 << numsSize创建02numsSize10\sim 2^{numsSize-1}的二进制序列长度。
  • 1 << i判断该mask下,这个数字要不要push进去。注意1 << 0出来的是0011 << 1出来的是0101 << 2出来的是100,刚好与mask的各个位置吻合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> output;
vector<int> temp;
int numsSize = nums.size();
for (int mask = 0; mask < (1 << numsSize); mask++) {
temp.clear();
for (int i = 0; i < numsSize; i++) {
if (mask & (1 << i)) {
temp.emplace_back(nums[i]);
}
}
output.emplace_back(temp);
}
return output;
}
};

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

解法一——递归

套路类似“40. 组合总和 II”,先排序,如果和上一个相同的话,那就continue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(vector<int> nums, int start) {
output.emplace_back(temp);

for (int i = start; i < nums.size(); i++) {
if (i > start && nums[i] == nums[i-1]) {
continue;
}
temp.emplace_back(nums[i]);
backtracking(nums, i + 1);
temp.pop_back();
}
}

vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
backtracking(nums, 0);
return output;
}
};

解法二——枚举迭代

mask需要考虑去重,i不需要考虑去重。

要加入一个判断if (i > 0 && (mask >> (i - 1) & 1) == 0 && nums[i] == nums[i - 1]),这个不好想啊!

对于当前选择的数x,若前面有与其相同的数 y,且没有选择 y,此时包含 x 的子集,必然会出现在包含 y 的所有子集中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

vector<vector<int>> subsetsWithDup(vector<int>& nums) {
int numSize = nums.size();
sort(nums.begin(), nums.end());
for (int mask = 0; mask < (1 << numSize); mask++) {
temp.clear();
bool flag = true;
for (int i = 0; i < numSize; i++) {
if (mask & (1 << i)) {
if(i > 0 && (mask >>(i-1) & 1) == 0 && nums[i] == nums[i-1]) {
flag = false;
break;
}
temp.emplace_back(nums[i]);
}
}
if (flag) {
output.emplace_back(temp);
}
}
return output;
}
};

491. 非递减子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以和子集问题并不一样!!!

解法一——深搜

定义之前的最大值last

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(vector<int> nums, int start, int last) {
if (start == nums.size()) {
if (temp.size() >= 2) {
output.emplace_back(temp);
}
return;
}

if (nums[start] >= last) { // 输入并递归
temp.emplace_back(nums[start]);
backtracking(nums, start + 1, nums[start]);
temp.pop_back();
}
if (nums[start] != last) {
backtracking(nums, start + 1, last);
}
}

vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0, INT_MIN);
return output;
}
};

解法二——回溯+set去重

以下写法if (i > start && nums[i] == nums[i-1])错误的,因为可能重复值并不出现在相邻元素,即使改成if (i > start && nums[i] == last)也不对,需要判断之前的所有元素才行。如测试用例[1,2,3,4,5,6,7,8,9,10,1,1,1,1,1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void backtracking(vector<int> nums, int start, int last) {
if (temp.size() >= 2) {
output.emplace_back(temp);
}

for(int i = start; i < nums.size(); i++){
if (i > start && nums[i] == nums[i-1]) {
continue;
}
if (nums[i] >= last) {
temp.emplace_back(nums[i]);
backtracking(nums, i+1, nums[i]);
temp.pop_back();
}
// else {
// backtracking(nums, i+1, last);
// }
}
}

所以需要用到set来记录之前的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void backtracking(vector<int> nums, int start, int last) {
if (temp.size() >= 2) {
output.emplace_back(temp);
}

unordered_set<int> thisLayer;
for(int i = start; i < nums.size(); i++){ // 使用set对本层元素进行去重
if (i > start && thisLayer.find(nums[i]) != thisLayer.end()) {
continue;
}
if (nums[i] >= last) {
temp.emplace_back(nums[i]);
thisLayer.insert(nums[i]);
backtracking(nums, i+1, nums[i]);
temp.pop_back();
}
}
}

或者用数组来作为哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void backtracking(vector<int> nums, int start, int last) {
if (temp.size() >= 2) {
output.emplace_back(temp);
}

int thisLayer[201] = {0};
for(int i = start; i < nums.size(); i++){
if (i > start && thisLayer[nums[i] + 100] != 0) {
continue;
}
if (nums[i] >= last) {
temp.emplace_back(nums[i]);
thisLayer[nums[i] + 100]++;
backtracking(nums, i+1, nums[i]);
temp.pop_back();
}
}
}

此外可以不定义last元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(vector<int> nums, int start) {
if (temp.size() >= 2) {
output.emplace_back(temp);
}

int thisLayer[201] = {0};
for(int i = start; i < nums.size(); i++){
if ((!temp.empty() && nums[i] < temp.back()) || thisLayer[nums[i] + 100] != 0) {
continue;
}
temp.emplace_back(nums[i]);
thisLayer[nums[i] + 100]++;
backtracking(nums, i+1);
temp.pop_back();
}
}

vector<vector<int>> findSubsequences(vector<int>& nums) {
// sort(nums.begin(), nums.end());
backtracking(nums, 0);
return output;
}
};

46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

排列问题

排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:

46.全排列

因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次

这里的used数组和非递减子集中的set含义非常像,虽然这边是用于标记是否使用过,上文是用于是否有重复元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(vector<int> nums, vector<bool> &used) {
if (temp.size() == nums.size()) { // 终止条件
output.emplace_back(temp);
return;
}

for (int i = 0; i < nums.size(); i++) {
if (used[i]) {
continue;
}
used[i] = true;
temp.emplace_back(nums[i]);
backtracking(nums, used);
temp.pop_back();
used[i] = false;
}
}

vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used (nums.size(), false);
backtracking(nums, used);
return output;
}
};

47. 全排列 II

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

解法一——利用unordered_map

相当于定义了一个hash表,存储每个数字(first,键)能用多少次(second,值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(vector<int> nums, unordered_map<int,int> &used) {
if (temp.size() == nums.size()) {
output.emplace_back(temp);
return;
}

for (auto p = used.begin(); p != used.end(); p++) {
if (p->second == 0 ) {
continue;
}
p->second--;
temp.emplace_back(p->first);
backtracking(nums, used);
temp.pop_back();
p->second++;
}
}

vector<vector<int>> permuteUnique(vector<int>& nums) {
unordered_map<int,int> used;
for (int num:nums) {
used[num] ++;
}
backtracking(nums, used);
return output;
}
};

解法二——利用used数组,并对其限制条件

只要设定一个规则,保证在填第idx 个数的时候重复数字只会被填入一次即可。而在本题解中,我们选择对原数组排序,保证相同的数字都相邻,然后每次填入的数一定是这个数所在重复数集合中「从左往右第一个未被填过的数字」

这里涉及两个条件,关系为“或”,第二个条件很好理解,用过了就不能用了,第一个条件有点难理解:

  • i > 0 && nums[i] == nums[i-1] && used[i-1] == false
  • used[i] == true

i > 0 && nums[i] == nums[i-1] && used[i-1] == false限制了从左到右填入的顺序,属于是“同层(树层)剪枝”

image-20240411144113495

47.全排列II2

如果该作i > 0 && nums[i] == nums[i-1] && used[i-1] == true,则变成“不同层(树枝)剪枝”,情况复杂很多

image-20240411144439089

47.全排列II3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
vector<vector<int>> output;
vector<int> temp;

void backtracking(vector<int> nums, vector<bool> &used) {
if (temp.size() == nums.size()) {
output.emplace_back(temp);
// cout << "return " << endl;
return;
}

for (int i = 0; i < nums.size(); i++) {
// for (bool jj:used) {
// cout << jj << " ";
// }
if ((i > 0 && nums[i] == nums[i-1] && used[i-1] == false) || used[i] == true) {
// cout << "continue "<< endl;
continue;
}
// cout << endl;
temp.emplace_back(nums[i]);
used[i] = true;
backtracking(nums, used);
used[i] = false;
temp.pop_back();
}
}

vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return output;
}
};

332. 重新安排行程

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"]["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

一个机场映射多个机场,同时由于字典顺序小的行程,所以需要里面套的是有序的map:两种表示

  • unordered_map<string, multiset> targetsunordered_map<出发机场, 到达机场的集合> targets遍历multiset不能删除元素
  • unordered_map<string, map<string, int>> targetsunordered_map<出发机场, map<到达机场, 航班次数>> targets可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。

有几个点需要注意:

  • 最终的路径长度应该是比ticket长度要大1的
  • 在使用迭代器遍历元素时候,如果要改变元素值,需要使用&,即for (auto &p : airports[last])
  • vector,map元素遍历时候的写法要注意。难还难在容器的选择和使用上
  • 该题目的情况下,应该采用有返回值的回溯比较好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
vector<string> output;
unordered_map <string, map<string, int>> airports;

bool backtracking (string last, int ticketsSize) {
if (output.size() == ticketsSize + 1) {
return true;
}

for (auto &p : airports[last]) {
if (p.second != 0) {
p.second--;
output.emplace_back(p.first);
if (backtracking(p.first, ticketsSize)) {
return true;
}
output.pop_back();
p.second++;
}
}
return false;
}

vector<string> findItinerary(vector<vector<string>>& tickets) {
int ticketsSize = tickets.size();
// 建立unordered_map存储一对多的机场关系
for (auto p: tickets) {
airports[p[0]][p[1]] ++;
}
output.emplace_back("JFK");
backtracking(output.back(), ticketsSize);
return output;
}
};

51. N 皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

根据题意暴力回溯即可,和代码随想录不同。

  • 利用 columns 数组记录每个皇后的位置,最后再构建棋盘
  • 这样判断是否合理时候比较简单,只需要考虑列不同、左斜、右斜等几种情况即可。
  • 其实还可以进一步简化,当第一行只需要一半即可,因为偶数时候必定有对称的情况,奇数时候除了第一行刚好为中间,其余也有对称情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
vector<vector<string>> output;
vector<int> columns; //n个位置,表示第n个皇后在第n行的第几个格子
void backtracking (int n, int row) {
if (row == n) {
vector<string> temp(n, string(n, '.'));
for (int i = 0; i < n; i++) {
// if (!(n % 2) && columns[0] == n / 2) { // 奇数列情况,中间的情况是不能反转的
temp[i][columns[i]] = 'Q';
}
output.emplace_back(temp);
// cout << endl;
return;
}
bool flag = false; // flag检测列标是否和前面元素冲突
for(int column = 0; column < n; column++) { //第row元素的列标, 一定有一个反转的情况成立
flag = false;
for (int j = 0; j < row; j++) { // j表示前面的行
if (columns[j] == column || (row - j) == (columns[j] - column) || (row - j) == - (columns[j] - column)) { // 列标重合,斜线
flag = true;
break;
}
}
if (!flag) { // 没问题,回溯
columns.emplace_back(column);
backtracking(n, row + 1);
columns.pop_back();
}
}

}

vector<vector<string>> solveNQueens(int n) {
backtracking(n, 0);
return output;
}
};

37. 解数独

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Solution {
public:
bool backtracking(vector<vector<char>>& board, int row, int column) {
if (row >= 9 || column >= 9) {
return true;
}
if (board[row][column] < '1' || board[row][column] > '9') { // 注意这里是或的关系
int num = 1;
for (num = 1; num < 10; num++) {
// cout << row << " " << column << " " << num << endl;
if (isValid(board, row, column, num)) {
board[row][column] = num + '0';
// cout << row << " " << column << " " << num << endl;
if (column == 9 - 1) {
if (backtracking(board, row + 1, 0)) {
return true;
} // 换行
}
else {
if (backtracking(board, row, column + 1)) {
return true;
} // 继续
}
board[row][column] = '.';
}
}
if (num == 10 && board[row][column] == '.') {
return false; // 不能让一元素为.,说明前面是有错的, 注意已经自增过了,所以这里是10
}
}
if (column == 9 - 1) {
if (backtracking(board, row + 1, 0)) {
return true;
} // 换行
}
else {
if (backtracking(board, row, column + 1)) {
return true;
} // 继续
}
return false;
}

bool isValid (vector<vector<char>>& board, int row, int column, int num) {
// cout << row << " " << column << " " << num << endl;
for (int i = 0; i < 9; i++) { // 检查同一行
if (board[row][i] - '0' == num) {
return false;
}
}
for (int j = 0; j < 9; j++) { // 检查同一列
if (board[j][column] - '0' == num) {
return false;
}
}
int blockLeft = (column / 3) * 3;
int blockTop = (row / 3) * 3;
for (int i = blockLeft; i < blockLeft + 3; i++) {
for (int j = blockTop; j < blockTop + 3; j++) {
if (board[j][i] - '0' == num) {
return false;
}
}
}
return true;
}

void solveSudoku(vector<vector<char>>& board) {
backtracking(board, 0, 0);
}
};

代码随想录的解答略有区别,主要是他直接把每个顶点的循环放在backtracking里面了,这样里面每一次跑都会进入两层for循环,如果有值就continue;此外循环可以直接通过for (char k = '1'; k <= '9'; k++)来进行,就不需要补+'0'的操作了。

贪心算法

贪心的本质是选择每一阶段的局部最优,从而达到全局最优

如何验证可不可以用贪心算法呢?最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

贪心算法大纲

区间类问题——先排序

455. 分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

解法一——两重for循环

优先满足胃口大的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int output = 0;
int gStart = g.size() - 1;
for (int i = s.size() - 1; i >= 0; i--) { // 优先满足胃口大的
for (int j = gStart; j >= 0; j--) {
// cout << s[i] << " " << g[j] << endl;
if (s[i] >= g[j]) {

output++;
gStart = j - 1; // 从目前满足的下一个开始
break;
}
}
}
return output;
}
};

解法二——一层for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int output = 0;
int sStart = s.size() - 1;
for (int i = g.size() - 1; i >= 0; i--) { // 优先满足胃口大的
if (sStart >= 0 && s[sStart] >= g[i]) {
output++;
sStart--;
}
}
return output;
}
};

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

  • 局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
  • 其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)。这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点

考虑的因素

  • 单调坡度只记录首尾
  • 需要考虑平坡(两种情况)
    img
  • 只有一个或两个元素——两个元素判断是否是平坡

解法一——贪心

起点终点、平坡比较难判断。。。(还是没太想清楚,后面仔细想想)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() <= 1) {
return nums.size();
}

int index0, index1;
int output = (nums[1] - nums[0] == 0) ? 1 : 2;
for (index0 = 0, index1 = 1; index1 < nums.size() - 1; index1++) {
cout << " index0: " << nums[index0] << " index1: " << nums[index1] << " index2:" << nums[index1+1];
cout << " prev: " << (nums[index1] - nums[index0]) << " curr: " << (nums[index1+1] - nums[index1])<< endl;
if (((nums[index1] - nums[index0]) * (nums[index1 + 1] - nums[index1]) < 0) || ((nums[index1] - nums[index0]) == 0) && (nums[index1 + 1] - nums[index1]) != 0) { //满足摆动
output++;
index0 = index1;
}
}
return output;
}
};

解法二——动态规划

考虑用动态规划的思想来解决这个问题。

  1. up[i] 表示以前 i 个元素中的某一个为结尾的最长的「上升摆动序列」的长度。
  2. down[i] 表示以前 i 个元素中的某一个为结尾的最长的「下降摆动序列」的长度。

up[i]={up[i1], nums [i]nums[i1]max(up[i1], down [i1]+1), nums [i]> nums [i1]down[i]={down[i1], nums [i] nums [i1]max(up[i1]+1, down [i1]), nums [i]< nums [i1]\begin{aligned} & u p[i]= \begin{cases}u p[i-1], & \text { nums }[i] \leq n u m s[i-1] \\ \max (u p[i-1], \text { down }[i-1]+1), & \text { nums }[i]>\text { nums }[i-1]\end{cases} \\ & \operatorname{down}[i]= \begin{cases}\operatorname{down}[i-1], & \text { nums }[i] \geq \text { nums }[i-1] \\ \max (u p[i-1]+1, \text { down }[i-1]), & \text { nums }[i]<\text { nums }[i-1]\end{cases} \end{aligned}

仅需要前一个状态来进行转移,所以我们维护两个变量即可。

——官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int numSize = nums.size();
if (numSize < 2) {
return numSize;
}
vector <int> up(numSize) ;
vector <int> down(numSize);
up[0] = down[0] = 1; //长度为 1 的序列,它既是「上升摆动序列」,也是「下降摆动序列」。
for(int i = 1; i < numSize; i++) {
if (nums[i] > nums[i-1]) { // 比上一个大
up[i] = max(up[i-1], down[i-1] + 1); //上摆情况是在“上一元素上摆”和“上一元素下摆的基础上加上这个元素上摆”
down[i] = down[i-1]; // 加入这个元素不能组成下摆
}
else if (nums[i] < nums[i-1]) { // 比上一个元素小,不可能上摆
down[i] = max(down[i-1], up[i-1] + 1);
up[i] = up[i-1];
}
else { // 相等
up[i] = up[i-1];
down[i] = down[i-1];
}
}
return max(up[numSize-1], down[numSize-1]);
}
};

仅需要前一个状态来进行转移,所以我们维护两个变量即可。每有一个「峰」到「谷」的下降趋势,down 值才会增加,每有一个「谷」到「峰」的上升趋势,up 值才会增加。且过程中 downup 的差的绝对值值恒不大于 1,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int numSize = nums.size();
if (numSize < 2) {
return numSize;
}
int up = 1, down = 1; //长度为 1 的序列,它既是「上升摆动序列」,也是「下降摆动序列」 之和
for(int i = 1; i < numSize; i++) {
if (nums[i] > nums[i-1]) { // 比上一个大
up = down + 1); //上摆情况是在“上一元素上摆”和“上一元素下摆的基础上加上这个元素上摆”
}
else if (nums[i] < nums[i-1]) { // 比上一个元素小,不可能上摆
down = up + 1;
}
}
return max(up, down);
}
};

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。子数组 是数组中连续的 非空 元素序列。

解法一——贪心

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

局部最优的情况下,并记录最大的“连续和”,可以推出全局最优

注意“连续和”为负数的时候前一个数字一定为负数,所以应该要舍弃。其关键在于:不能让“连续和”为负数的时候加上下一个元素,而不是 不让“连续和”加上一个负数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int sum = 0;
int output = INT_MIN;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
if (sum > output) {
output = sum;
}
if (sum <= 0) {
sum = 0;
}
}
return output;
}
};

解法二——动态规划

其实和贪心的思路很类似,只是把sum变成了dp数组。但是思路比贪心好想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 0) {
return 0;
}
vector<int> dp(nums.size());
dp[0] = nums[0];
int output = dp[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i-1] + nums[i], nums[i]);
if (dp[i] > output) {
output = dp[i];
}
}
return output;
}
};

122. 买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

解法一——贪心

其实最终利润是可以分解的,那么本题就很容易了!

假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!

但是不同的天的是可以叠加的。所以可以一天一天的利润考虑,只要有利润>0就买。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int maxProfit(vector<int>& prices) {
int output = 0;
int profitDay = 0;
for (int i = 1; i < prices.size(); i++) {
profitDay = prices[i] - prices[i-1];
if (profitDay > 0) {
output += profitDay;
}
}
return output;
}
};

解法二——动态规划

  • dp[i][0] 表示第i天持有股票所得现金。
  • dp[i][1] 表示第i天不持有股票所得最多现金

如果第ii天持有股票即dp[i][0], 那么可以由两个状态推出来

  • i1i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • ii天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int> (2));
dp[0][0] -= prices[0]; // 第0天持有股票目前的利润
dp[0][1] = 0; // 第0天不持有股票
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]); // 第i-1天有股票不动,或第i-1天没有股票买进
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]); // 第i-1天没有股票不动,或第i-1天有股票卖出
}
return dp[prices.size()-1][1];
}
};

55. 跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

**转化为跳跃覆盖范围究竟可不可以覆盖到终点!**每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxIndex = 0;
int numSize = nums.size();
for (int i = 0; i <= maxIndex && i < numSize; i++) {
maxIndex = max(maxIndex, i + nums[i]);
if (maxIndex >= numSize - 1) {
return true;
}
}
return false;
}
};

45. 跳跃游戏 II

给定一个长度为 n0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

生成到每个位置所需的最少跳跃次数。每个位置的最少跳跃次数是连续的。只要每一次在这个最小跳跃次数覆盖范围内找下一次的覆盖范围即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int jump(vector<int>& nums) {
int minJump = 0; //记录当前位置需要最少几次跳跃到达
int maxIndex = 0; //记录当前能到达的最远位置
int premaxIndex = 0;
for (int i = 0; i < nums.size(); i++) {
if(premaxIndex < 0) {
minJump++;
premaxIndex = maxIndex - i;
}
if (nums[i] + i > maxIndex) { // 可以更新maxIndex
maxIndex = nums[i] + i;
}
premaxIndex--;
// cout << i << " " << minJump << " " << maxIndex << endl;
}
return minJump;
}
};

另一个思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int jump(vector<int>& nums) {
int maxJump = 0; // 最远到
int jumpTimes = 0; // 目前的位置要跳几次
int preMaxJump = 0; // 上一次最远到

for(int i = 0; i < nums.size(); i++) {
if (i <= preMaxJump) {
if (nums[i] + i > maxJump) { // 能跳的比目前的远
maxJump = nums[i] + i;
}
}
else {
jumpTimes++;
preMaxJump = maxJump;
if (nums[i] + i > maxJump) { // 能跳的比目前的远
maxJump = nums[i] + i;
}
}
}
return jumpTimes;
}
};

1005. K 次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]

重复这个过程恰好 k 次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和

解法一——逻辑复杂的分类讨论

分三种情况:

  • 负数:①负数个数>=k,②负数个数<k
  • 0:有无0
  • 正数

其实0和负数可以统一考虑,当非正数<k时,倒数的那个数字反转剩余次数即可。

先排序,优先反转小的负数,这里考虑的一些情况:

  • 负数或0:k用完没有?用完了那就只能是负数了
  • 正数:k还有没有没用完,只需要考虑模2为1的情况,也就是得翻转一个数的情况,没用完有两种情况
    • 前面有负数,且那个负数绝对值比正数最小的要小,反转最大的负数
    • 前面没有负数了,或者负数绝对值比正数最小的要大,反转最小的正数
  • 还有一个情况,全是负数,那么可能出现出循环k还没用完的情况,那需要考虑是否需要反转最大的负数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
bool flagNotPositive = true; //非正数
int output = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] <= 0) {
if (k > 0) {
output -= nums[i];
}
else {
output += nums[i];
}
k--;
}
else {
if (flagNotPositive == true) {
flagNotPositive = false;
if (k > 0 && k % 2 == 1) {
if (i > 0 && -nums[i-1] < nums[i] ) {
output += 2 * nums[i-1];
}
else {
output -= 2 * nums[i];
}
}
}
output += nums[i];
}
// cout << nums[i] << " " << output << endl;
}
if (k > 0 && k % 2 == 1 && nums[nums.size()-1] < 0) {
output += 2 * nums[nums.size()-1];
}
return output;
}
};

解法二——贪心

这里思路和我最大的不同就是按照绝对值大小排序。。。能减轻很多负担

  • 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
  • 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
  • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
  • 第四步:求和

两次贪心:

  • 局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
  • 一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大,全局最优:整个 数组和 达到最大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
static bool cmp(int a, int b) {
return abs(a) > abs(b);
}
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort (nums.begin(), nums.end(), cmp);
int output = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] < 0 && k-- > 0) {
nums[i] = - nums[i];
}
output += nums[i];
}
if (k > 0 && k % 2 == 1) {
output -= 2 * nums[nums.size()-1]; // 不管最后一个数原本是正负均成立,因为转成正数了
}
return output;
}
};

134. 加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

解法一——暴力解法+一点点优化

如果x到达不了y+1,那么x-y之间的点也不可能到达y+1,因为中间任何一点的油都是拥有前面的余量的,所以下次遍历直接从y+1开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
// if (accumulate(gas.begin(), gas.end(), 0) < accumulate(gas.begin(), gas.end(), 0)) {
// return -1;
// }
for (int i = 0; i < n; i++) {
int gasSum = 0;
int j = 0;
for (j = 0; j < n; j++) {
int station = (i + j) % n;
gasSum = gasSum + gas[station] - cost[station];
if (gasSum < 0) {
// cout << "break" << endl;
break;
}
// cout << station << " " << gasSum << endl;
}
if (j == n) { // 说明转完一圈了
return i;
}
else {
i = i + j;
}
}
return -1;
}
};

解法二——贪心

首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。

局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int totalSum = 0;
int start = 0;
for (int i = 0; i < gas.size(); i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if (curSum < 0) { // 当前累加rest[i]和 curSum一旦小于0
start = i + 1; // 起始位置更新为i+1
curSum = 0; // curSum从0开始
}
}
if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
return start;
}
};

——代码随想录

135. 分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

解法一——两次贪心

两次贪心的策略:

  • 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
  • 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candys(ratings.size(), 1);
for (int i = 1; i < ratings.size(); i++) {//从左到右贪心
if (ratings[i] > ratings[i-1]) {
candys[i] = candys[i-1] + 1;
}
}
for (int j = ratings.size() - 2; j >= 0; j--) {// 从右往左贪心,注意更新值的时候比较的值要已经更新过了
if (ratings[j] > ratings[j+1]) {
candys[j] = max(candys[j] , candys[j+1] + 1);
}
}
return accumulate(candys.begin(), candys.end(), 0);
}
};

解法二——常数空间遍历

我们从左到右枚举每一个同学,记前一个同学分得的糖果数量为 pre:

  • 如果当前同学比上一个同学评分高,说明我们就在最近的递增序列中,直接分配给该同学 pre+1 个糖果即可。
    fig1
  • 否则我们就在一个递减序列中,我们直接分配给当前同学一个糖果,并把该同学所在的递减序列中所有的同学都再多分配一个糖果,以保证糖果数量还是满足条件。
    • 我们无需显式地额外分配糖果,只需要记录当前的递减序列长度,即可知道需要额外分配的糖果数量。
      fig2
    • 同时注意当当前的递减序列长度和上一个递增序列等长时,需要把最近的递增序列的最后一个同学也并进递减序列中。
      fig3

我们只要记录当前递减序列的长度 dec,最近的递增序列的长度 inc 和前一个同学分得的糖果数量 pre 即可。

——力扣官方题解

可以同时考虑两边的问题,只是右边的问题通过前面递减序列加1得到。(真的想不到啊)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
int candy(vector<int>& ratings) {
int pre = 1;
int dec = 0;
int inc = 1;
int output = 1;
for (int i = 1; i < ratings.size(); i++) {
if (ratings[i] > ratings[i-1]) {
dec = 0;
output += ++pre;
inc = pre;
}
else if (ratings[i] == ratings[i-1]) { //相等=》归一
dec = 0;
pre = 1;
output += pre;
inc = 1;
}
else { //小于
// cout << " A " << dec << " B " << inc << " ";
dec ++;
if (dec == inc) { // 合并入最后一个上升节点
dec ++;
}
output += dec; //而非pre,因为最新加入的这个元素一定是1,但是递减序列中前面的元素还需要+1
pre = 1;
}
// cout << output << " ";
}
return output;
}
};

860. 柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

账单是20的情况,为什么要优先消耗一个10和一个5呢?因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!

局部最优:遇到账单20,优先消耗美元10,完成本次找零。

全局最优:完成全部账单的找零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int note5 = 0;
int note10 = 0;
for (int i = 0; i < bills.size(); i++) {
if (bills[i] == 5) {
note5 ++;
}
else if (bills[i] == 10) {
note5 --;
note10 ++;
if (note5 < 0) {
return false;
}
}
else { // 20 dollar
if (note10 > 0 && note5 > 0) {
note10--;
note5--;
}
else if (note5 >= 3) {
note5 = note5 - 3;
}
else {
return false;
}
}
}
return true;
}
};

406. 根据身高重建队列

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

在按照身高从大到小排序后:

局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性

全局最优:最后都做完插入操作,整个队列满足题目队列属性

解法一——插入用数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
static bool cmp (vector<int> x, vector<int> y) {
return x[0] > y[0] || (x[0] == y[0] && x[1] < y [1]);
// 保证这个元素前面的值比他大或相等
}

vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
int peopleSize = people.size();
// 先考虑身高,前面的都比他大或者相等,相等的时候k越小越靠前
sort (people.begin(), people.end(), cmp);
// for (int i = 0; i < peopleSize; i++) {
// cout << "[" << people[i][0] << "," << people[i][1] << "],";
// }
// cout << endl;
// 只需要按照k为下标重新插入队列即可,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点
vector<vector<int>> Q;
for (int i = 0; i < peopleSize; i++) {
Q.insert(Q.begin() + people[i][1], people[i]);
}

return Q;
}
};

解法二——插入用链表

list内部使用链表实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
static bool cmp (vector<int> x, vector<int> y) {
return x[0] > y[0] || (x[0] == y[0] && x[1] < y [1]);
// 保证这个元素前面的值比他大或相等
}

vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
int peopleSize = people.size();
// 先考虑身高,前面的都比他大或者相等,相等的时候k越小越靠前
sort (people.begin(), people.end(), cmp);

// 只需要按照k为下标重新插入队列即可,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点
list<vector<int>> Q;
for (int i = 0; i < peopleSize; i++) {
int position = people[i][1];
list<vector<int>>::iterator it = Q.begin();
while (position--) {
it++;
}
Q.insert(it, people[i]);
}
return vector<vector<int>> (Q.begin(), Q.end());
}
};

452. 用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstartxend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

思路就是先排序,先按照start从小到大排序,如果start相等则按照end从小到大排序。

然后遍历每个point,找当前遍历过的所有点里哪个end最小,一旦一个点的start超过了这个最小的end,那就不得不射一箭了。射完箭以后minEnd需要重置。

最后要补射一箭,因为最后一个区间没有射箭呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
static bool cmp (vector<int> x, vector<int> y) {
if (x[0] == y[0]) {return x[1] < y[1];}
return x[0] < y[0]; // 小的在前
}
int findMinArrowShots(vector<vector<int>>& points) {
sort (points.begin(), points.end());
int arrow = 0;
int minEnd = INT_MAX;
for (int i = 0; i < points.size(); i++) {
if (points[i][1] < minEnd) { // 当前的end比之前的end还要小
minEnd = points[i][1];
}
if (points[i][0] > minEnd) { //start坐标大于之前的最小的end,必须射箭
arrow++;
minEnd = points[i][1];
}
}
return arrow + 1; // 最后一部分没有射箭
}
};

435. 无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

思路——排序加哈希表

先按照每个区间的长度排序,区间长度一致按照start从小到大。然后再建立哈希表保存左闭右开区间的元素。

这个思路是错误的,并不是说区间长度越小越不容易造成重叠的。

【反例】万一有这么一组数字[[-100,1],[1,2],[2,3],[3,100],[-3,7]]。按照以上的逻辑会首先排成[[1,2],[2,3],[-3,7],[-100,1],[3,100]],然后得到的结果应该去除[-100,1],[3,100]这两个,保留以下三个[1,2],[2,3],[-3,7]。然后正确的应该是保留四个[1,2],[2,3],[-100,1],[3,100],去除一个[-3,7]

想复杂了,其实和452是一样的!!!

解法一——贪心+左边缘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
static bool cmp (const vector<int> &x, const vector<int> &y) {
// if (x[0] == y[0]) {
// return x[1] < y[1];
// }
return x[0] < y[0];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
int erase = 0;
int minEnd = intervals[0][1];
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] < minEnd) {
erase++;
if (intervals[i][1] < minEnd) {
minEnd = intervals[i][1];
}
}
else {//大于之前的区间了,重新计数
minEnd = intervals[i][1];
}
}
return erase;
}
};

解法二——贪心+右边缘

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
static bool cmp (const vector<int> &x, const vector<int> &y) {
// if (x[0] == y[0]) {
// return x[1] < y[1];
// }
return x[1] < y[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
int count = 1; //非重叠区间至少有一个
int minEnd = intervals[0][1];
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] >= minEnd) {
count++;
minEnd = intervals[i][1];
}
}
return intervals.size() - count;
}
};

763. 划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

返回一个表示每个字符串片段的长度的列表。

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。

记录下每个字母出现的end位置,从前往后遍历,当前字母的end位置更加靠后了,更新maxEnd,如果end更前则不用管。当走到maxEnd的时候,分割这个片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
vector<int> partitionLabels(string s) {
int alphabet[26] = {0}; // 记录每个字母出现的end位置
for (int i = 0; i < s.size(); i++) {
alphabet[s[i] - 'a'] = i;
}
int maxEnd = alphabet[s[0] - 'a']; //当前片段目前能够分割的最后位置
int prevEnd = -1; // 上一次的end(不含)
vector <int> output;
for (int i = 0; i < s.size(); i++) {
if (alphabet[s[i] - 'a'] > maxEnd) {
maxEnd = alphabet[s[i] - 'a'];
}
if (i == maxEnd) {
output.emplace_back(maxEnd - prevEnd);
prevEnd = maxEnd;
}
}
return output;
}
};

56. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
static bool cmp (const vector<int> &x, const vector<int> &y){
return x[0] < y[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort (intervals.begin(), intervals.end(), cmp);
vector<int> temp(2,0);
temp[0] = intervals[0][0];//当前区间左边缘
temp[1] = intervals[0][1];//当前区间右边缘(含)
vector<vector<int>> output;
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] > temp[1]) { // 保存上一个区间
output.emplace_back(temp);
temp = intervals[i];
}
else { // 左边缘小于等于当前右边缘
temp[1] = max(temp[1], intervals[i][1]);
}
}
output.emplace_back(temp); // 保存最后一个区间
return output;
}
};

738. 单调递增的数字

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

n / 10 * 10 - 1 最后一位必然是9,比别的大或等于,所以可以不考虑最后一位。只考虑前几位即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
bool isIncreaseingDigits(int n) {
int prev = n % 10;
n /= 10;
while (n != 0) {
// cout << n%10 << " " << prev <<" ";
if (n % 10 > prev) {
return false;
}
prev = n % 10;
n /= 10;
}
return true;
}
int monotoneIncreasingDigits(int n) {
if (n < 10 || isIncreaseingDigits(n)) {
return n;
}
int prev = n; // 前缀部分,每次只需要判断前缀是否为单调递增即可,因为最后一位必然是9.
int divideTime = 0;
while (!isIncreaseingDigits(prev - 1)) {
// n / 10 * 10 - 1 最后一位必然是9,比别的大或等于,所以可以不考虑最后一位。
prev = prev / 10;
divideTime++;
}
while(divideTime != 0){
prev *= 10;
divideTime--;
}

return prev - 1;
}
};

代码随想录的方法使用字符串操作。

968. 监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

深度优先,后序遍历

两个注意点:

  • 空节点命名为哪个状态,应当是1已经覆盖,因为叶子节点实际上并不需要单独的摄像头
  • 根节点返回0的情况,即根节点还没有摄像头覆盖,需要+1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int inorder (TreeNode* p, int &numCamera) {
// 0: 未覆盖; 1: 已覆盖; 2: 摄像头
if (p == NULL) {
return 1;
}

int left = inorder(p->left, numCamera);
int right = inorder(p->right, numCamera);

if (left == 0 || right == 0) { // 包含00,01,10,02,20
numCamera++;
return 2; // 摄像头
}
else if (left == 2 || right == 2) { // 包含22,21,12
return 1; // 已经覆盖
}
else { //11
return 0;
}

}
int minCameraCover(TreeNode* root) {
int numCamera = 0;
if (root == nullptr ) {
return 0;
}
if (inorder(root, numCamera) == 0) {
return numCamera + 1;
}
return numCamera;
}
};

动态规划

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。

动态规划问题:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组
img
  • 只是求排列组合数时候可以用动规,但是要给出所有答案得用回溯。

背包问题

416.分割等和子集1

509. 斐波那契数

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

1
2
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

解法一——直接递推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
int prev0 = 0;
int prev1 = 1;
int temp;
while (n != 1) {
temp = prev0 + prev1;
prev0 = prev1;
prev1 = temp;
n--;
}
return temp;
}
};

解法二——动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int fib(int n) {
if (n <= 1) {
return n;
}
vector<int> dp(n + 1);
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int climbStairs(int n) {
int dp[46] = {0};
dp[1] = 1; // 上1楼只有一种方式
dp[2] = 2; //上2楼有两种方式
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 上到这个楼层有两个方式,一个台阶、两个台阶
}
return dp[n];
}
};

746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp[3] = {0};
dp[0] = cost[0];
dp[1] = cost[1];

for (int i = 2; i < cost.size(); i++) {
// 到第二层有两种方式:下标0直接2步上来,下标1一步上来
dp[2] = min(dp[0], dp[1]) + cost[i];
dp[0] = dp[1];
dp[1] = dp[2];
}
return min(dp[0], dp[1]);
}
};

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

img

解法一——动态规划硬解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> roadMap (m, vector<int> (n,0));
roadMap[0][0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) {
continue;
}
else if (j == 0) {
roadMap[i][j] = roadMap[i - 1][j];
}
else if (i == 0) {
roadMap[i][j] = roadMap[i][j - 1];
}
else {
roadMap[i][j] = roadMap[i - 1][j] + roadMap[i][j - 1];
}
}
}
return roadMap[m - 1][n - 1];
}
};

改一下,更简单、更快一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> roadMap (m, vector<int> (n,0));
roadMap[0][0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {
roadMap[i][j] = 1;
}
else {
roadMap[i][j] = roadMap[i - 1][j] + roadMap[i][j - 1];
}
}
}
return roadMap[m - 1][n - 1];
}
};

解法二——数论方法

组合问题

无论怎么走,走到终点都需要 m + n - 2 步,一定有m - 1要往下走,那么就需要Cm+n2m1\mathbf C_{m+n-2}^{m-1}​次

Anm=n!(nm)!Cnm=n!(nm)!m!\begin{gathered} \mathbf A_n^m=\frac{n!}{(n-m)!}\\ \mathbf C_n^m = \frac{n!}{(n-m)!m!} \end{gathered}

则计算

Cm+n2m1=(m+n2)!(n1)!(m1)!=(m+n2)(n+1)n(m1)1\mathbf C_{m+n-2}^{m-1} = \frac{(m+n-2)!}{(n-1)!(m-1)!} = \frac{(m+n-2)\cdots(n+1)n}{(m-1)\cdots1}

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
int uniquePaths(int m, int n) {
long long output = 1;
for (int numerator = n, denominator = 1; numerator < m + n - 1; numerator++, denominator++ ) {
output = output * numerator / denominator;
}
return output;
}
};

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。
img

解法一——动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int temp = -1;
// 初始化 第一行 第一列
for (int i = 0; i < obstacleGrid.size(); i++) {
if (obstacleGrid[i][0] == 1) {
temp = 0;
}
obstacleGrid[i][0] = temp;
}
temp = -1;
for (int j = 1; j < obstacleGrid[0].size(); j++) {
if (obstacleGrid[0][0] == 0 || obstacleGrid[0][j] == 1) {
temp = 0;
}
obstacleGrid[0][j] = temp;
}
for (int i = 1; i < obstacleGrid.size(); i++) {
for (int j = 1; j < obstacleGrid[0].size(); j++) {
if (obstacleGrid[i][j] == 1) {
obstacleGrid[i][j] = 0;
}
else { // 不需要分类讨论了。障碍和两边都挡住的就是0啊,那加上去也没影响
obstacleGrid[i][j] = obstacleGrid[i-1][j] + obstacleGrid[i][j-1];
}
}
}
// for (int i = 0; i < obstacleGrid.size(); i++) {
// for (int j = 0; j < obstacleGrid[0].size(); j++) {
// cout << obstacleGrid[i][j] << " ";
// }
// cout << endl;
// }
return - obstacleGrid[obstacleGrid.size() - 1][obstacleGrid[0].size() - 1];
}
};

343. 整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

解法一——数论?技巧?

我们知道正方形面积最大,所以分出来的数字一定比较接近。所以我直接遍历了分n种的情况,每次算当前的成绩比较。

但是这里需要注意,因为不是整除,所以可能需要考虑向上向下取整两种情况。

向下取整的情况最后一个比较好考虑,就是普通的因子+余数。

向上取整的要稍微想一想,是普通的因子+余数-之前i-1个因子每个都多考虑了1。或者利用以下这组关系

(n/i+1)(i1)+x=(n/i)i+n%i=n(n/i+1)\cdot(i-1)+x = (n/i)\cdot i+ n\%i = n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
int integerBreak(int n) {
long long maxNum = 0;
for (int i = 2; i <= n; i++) {
long long prod = 1;
if (n % i > i - n % i){ // 说明要进位
int factor = n / i + 1;
for (int j = 0; j < i - 1; j++) {
prod *= factor;
}
prod *= n % i + n / i - (i - 1);
}
else {
int factor = n / i;
for (int j = 0; j < i - 1; j++) {
prod *= factor;
}
prod *= (factor + n % i);
}
if (maxNum < prod) {
maxNum = prod;
}
}
return maxNum;
}
};

解法二——动态规划

dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。

dp[i]最大乘积是怎么得到的呢?其实可以从1遍历j,然后有两种渠道得到dp[i].

  • 一个是j * (i - j) 直接相乘。
  • 一个是j * dp[i - j],相当于是拆分(i - j),对这个拆分不理解的话,可以回想dp数组的定义。

注意第一种情况表示i-j不拆;第二种情况会拆分i-j,因为我们保存在数组中的值必定是拆分过的,那么也就没有保存i-j这个本身的值,例如dp[2]=1,但是如果一个数乘上2,反而还要比1大呢。

在遍历过程中会j和i-j的地位是完全对等的,也就是说拆j和拆i-j是完全一致的。

所以递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

1
2
3
4
5
6
7
8
9
10
11
12
13
1为头结点的时候,其右子树有两个节点,看这两个节点的布局,是不是和 n 为2的时候两棵树的布局是一样的啊!class Solution {
public:
int integerBreak(int n) {
int dp[59] = {0};
dp[2] = 1;
for (int i = 3; i <= n; i++) {
for (int j = 1; j <= i/2; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};

96. 不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

96.不同的二叉搜索树1

当1为头结点的时候,其右子树有两个节点,这两个节点的布局,和n为2的时候两棵树的布局是一样的!

当3为头结点的时候,其左子树有两个节点,这两个节点的布局,和n为2的时候两棵树的布局也是一样的!

当2为头结点的时候,其左右子树都只有一个节点,布局和n为1的时候只有一棵树的布局也是一样的!

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量

元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量

元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

有2个元素的搜索树数量就是dp[2]。

有1个元素的搜索树数量就是dp[1]。

有0个元素的搜索树数量就是dp[0]。

所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

意思是当有3个元素的时候,1、2、3每个元素都有可能当根节点,而每个元素作为根节点的时候的,左右子树所有有可能的序列类型。

在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]

j相当于是头结点的元素,从1遍历到i为止。

所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量

——代码随想录

代码中的递推公式使用的是

dp[i]+=dp[j]dp[ij1],j=0,1,,i1dp[i] +=dp[j]*dp[i-j-1],\quad j = 0,1,\cdots, i-1

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int numTrees(int n) {
int dp[20];
dp[0] = 1; //为0的子树有一种
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
dp[i] += dp[j] * dp[i - j - 1]; // 左子树从0到i-1个元素,右子树从i-1到0个元素;
}
}
return dp[n];
}
};

0-1背包问题

0-1背包问题——二维数组

  1. 确定dp数组以及下标的含义

    dp[i][j]表示从下标为[0i][0-i]的物品里任意取,放进容量为jj​的背包,价值总和最大是多少,即二维数组中的数字是最大价值

    image-20240417111706222
  2. 确定递推公式:dp[i][j]有两种情况得到

    1. 不放物品idp[i][j]=dp[i1][j]dp[i][j] = dp[i-1][j]
    2. 放物品idp[i][j]=dp[i1][jweight[i]]+value[i]dp[i][j] = dp[i-1][j-\text{weight}[i]]+\text{value}[i]

    即递推公式为dp[i][j]=max{dp[i1][j],dp[i1][jweight[i]]+value[i]}dp[i][j] = \max\,\{dp[i-1][j],dp[i-1][j-\text{weight}[i]]+\text{value}[i]\}

  3. dp数组如何初始化

    1. 第一列:背包重量为0的情况,必然为0
    2. 第一行(递推需要i-1状态):背包重量小于weight[0]的为0,大于等于的为value[0]
    1
    2
    3
    4
    5
    // 初始化 dp
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
    for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
    }
  4. 确定遍历顺序:有两个遍历的维度:物品与背包重量

    1. 那么我先给出先遍历物品,然后遍历背包重量的代码。

      1
      2
      3
      4
      5
      6
      7
      8
      // weight数组的大小 就是物品个数
      for(int i = 1; i < weight.size(); i++) { // 遍历物品
      for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
      if (j < weight[i]) dp[i][j] = dp[i - 1][j];
      else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

      }
      }
    2. 先遍历背包,再遍历物品,也是可以的!(注意这里使用的二维dp数组)

      1
      2
      3
      4
      5
      6
      7
      // weight数组的大小 就是物品个数
      for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
      for(int i = 1; i < weight.size(); i++) { // 遍历物品
      if (j < weight[i]) dp[i][j] = dp[i - 1][j];
      else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
      }
      }
  5. 举例推导dp数组

0-1背包问题——滚动数组

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

  • 遍历顺序问题:

  • 举一个例子(物品i=0时,背包重量j从0开始):物品0的重量weight[0] = 1,价值value[0] = 15

    如果正序遍历

    dp[1] = dp[1 - weight[0]] + value[0] = 15

    dp[2] = dp[2 - weight[0]] + value[0] = 30 (因为这里用的是用到了dp[1],而这个dp[1]已经被上一步更新过了)

    此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

  • 所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

1
2
3
4
5
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}

46. 携带研究材料 (kamacoder)

题目描述

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。

小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。

二维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# include <iostream>
# include <vector>
using namespace std;

int main() {
int itemNum;
int maxWeight;
cin >> itemNum >> maxWeight;
vector<int> weight(itemNum);
vector<int> value(itemNum);

for (auto &weighti:weight) {
cin >> weighti;
}
for(auto &valuei:value) {
cin >> valuei;
}

// 初始化二维矩阵
vector<vector<int>> dp(itemNum, vector<int>(maxWeight + 1, 0)); // 注意这里要加1
for (int j = weight[0]; j <= maxWeight; j++) { // 注意这里有等号
dp[0][j] = value[0];
}


// 开始遍历, 先遍历物品,后遍历背包大小
for (int i = 1; i < itemNum; i++) {
for (int j = 0; j <= maxWeight; j++) { // 注意这里有等号
if (weight[i] > j) { // 这个物品比目前能承载的最大重量还要大
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
// 这边的j - weight[i]是不会超范围的,因为他不是在前一个基础上加,而是直接找过去到前面的
}
}
}
cout << dp[itemNum - 1][maxWeight] << endl;
return 0;
}
滚动数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 include <iostream>
# include <vector>
using namespace std;

int main() {
int itemNum;
int maxWeight;
cin >> itemNum >> maxWeight;
vector<int> weight(itemNum);
vector<int> value(itemNum);

for (auto &weighti:weight) {
cin >> weighti;
}
for(auto &valuei:value) {
cin >> valuei;
}

// 初始化二维矩阵
vector<int> dp(maxWeight + 1, 0); // 注意这里要加1



// 开始遍历, 先遍历物品,后遍历背包大小
for (int i = 0; i < itemNum; i++) {
for (int j = maxWeight; j >= weight[i]; j--) { // 注意这里有等号
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[maxWeight] << endl;
return 0;
}

416. 分割等和子集

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 != 0) {
return false;
}
int maxWeight = sum / 2;
vector <int> dp(maxWeight + 1, 0);

for (int i = 0; i < nums.size(); i++) {
for (int j = maxWeight; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[maxWeight] == maxWeight;
}
};

01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。

或者采用true/false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
bool canPartition(vector<int>& nums) {
// 0-1背包
int sum = 0;
for (int num:nums) {
sum += num;
}
if (sum % 2) {// 奇数
return false;
}
vector<bool> dp(sum/2 + 1,false);
dp[0] = true;

for (int i = 0; i < nums.size(); i++) {
for (int j = sum/2; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[sum/2];

}
};

1049. 最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

因为是“最小”的可能重量,所以分组由我们决定。

用归纳法可以证明,无论按照何种顺序粉碎石头,最后一块石头的重量总是可以表示成

i=1n1ki×stonesi,ki{1,1}\sum_{i=1}^{n-1}k_i\times stones_i, k_i\in\{-1,1\}

我们将这组{ki}\{k_i\}对应的石头划分成两堆,ki=1k_i=1的石头分至一堆,ki=1k_i=-1的石头分至另一堆。由于这是最小非负值所对应的{ki}\{k_i\},这两堆石头重量之差的绝对值也是所有划分当中最小的。

所以问题就转化成了分成两组,是他们的绝对值最小。那么可以利用“416.分割等和子集”的思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = accumulate(stones.begin(), stones.end(), 0);
int maxWeight = sum / 2;

vector<int> dp(maxWeight + 1, 0);

for (int i = 0; i < stones.size(); i++) {
for (int j = maxWeight; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return (sum - dp[maxWeight]) - dp[maxWeight];
}
};

494. 目标和

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

分成两堆,一堆取正,一堆取负数。那么有

{positive+negtive=targetpositivenegetive=sum\left\{ \begin{array}{l} positive +negtive = target\\ positive - negetive = sum \end{array} \right.

所以postive之和就要等于(target+sum)/2。

dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法。只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

  • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
  • 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
  • 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

所以求组合类问题的公式,都是类似这种:

1
dp[j] += dp[j - nums[i]]

——代码随想录

j是背包大小,dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if ((sum + target) % 2 != 0 || abs(target) > sum) {
return 0;
}
int positiveSum = (sum + target) / 2;

vector<int> dp(positiveSum + 1, 0);
dp[0] = 1;

for (int i = 0; i < nums.size(); i++) {
for (int j = positiveSum; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[positiveSum];
}
};

474. 一和零

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

递推关系:有上一个字符串推出结果,假设当前字符串种有zeroNum个0,oneNum个1,上一个字符串至多有dp[i - zeroNum][j - oneNum]个子集,那么当前的可以保存下的子集的数量就要加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (auto s:strs) {
int one = 0;
int zero = 0;
for (auto c:s) {
if (c == '0') {
zero++;
}
else {
one++;
}
}
for (int i = m; i >= zero; i--) {
for (int j = n; j >= one; j--) {
dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1);
}
}
}
return dp[m][n];
}
};

完全背包问题

完全背包问题

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

首先再回顾一下01背包的核心代码

1
2
3
4
5
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

1
2
3
4
5
6
7
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

}
}

——代码随想录

求组合数:动态规划:518.零钱兑换II (opens new window)

求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)动态规划:70. 爬楼梯进阶版(完全背包) (opens new window)

求最小数(组合问题):动态规划:322. 零钱兑换 (opens new window)动态规划:279.完全平方数

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

52. 携带研究材料(kamacoder)

题目描述

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。

小明的行李箱所能承担的总重量为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>

using namespace std;

int main() {
int N, V;
cin >> N >> V;

vector<int> weight(N);
vector<int> value(N);
vector<int> dp(V + 1, 0);

for (int i = 0; i < N; i++) {
cin >> weight[i] >> value[i];
for (int j = weight[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}

cout << dp[V] << endl;

return 0;
}

518. 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

求组合方式。

本题要求凑成总和的组合数,元素之间明确要求没有顺序。

所以纯完全背包是能凑成总和就行,不用管怎么凑的。

本题是求凑出来的方案个数,且每个方案个数是为组合数。

那么本题,两个for循环的先后顺序可就有说法了。

我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。

代码如下:

1
2
3
4
5
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}

假设:coins[0] = 1,coins[1] = 5。

那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。

所以这种遍历顺序中dp[j]里计算的是组合数!

如果把两个for交换顺序,代码如下:

1
2
3
4
5
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}

背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。

此时dp[j]里算出来的就是排列数!

可能这里很多同学还不是很理解,建议动手把这两种方案的dp数组数值变化打印出来,对比看一看!(实践出真知)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1);
dp[0] = 1;

for (int i = 0; i < coins.size(); i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};

377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!

所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历

——代码随想录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1);
dp[0] = 1;

for (int j = 0; j <= target; j++) {
for (int i = 0; i < nums.size(); i++) {
if (nums[i] <= j && dp[j] < INT_MAX - dp[j - nums[i]]) { //C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
dp[j] += dp[j - nums[i]];
}
}
}

return dp[target];
}
};

57. 爬楼梯 (kamacoder)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数

求排列数。背包体积是n,物品大小是1到m。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
#include<vector>

using namespace std;

int main() {
int n, m;
cin >> n >> m;

vector<int> dp(n + 1);
dp[0] = 1;

for (int j = 0; j <= n; j++) {
for (int i = 1; i <= m; i++){
if (i <= j) {
dp[j] += dp[j - i];
}
}
}

cout << dp[n];
return 0;
}

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

首先这是个组合问题。 初始值怎么取很重要!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) {
for (int j = coins[i]; j <= amount; j++) {
if (dp[j - coins[i]] != INT_MAX) {
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) {
return -1;
}
return dp[amount];
}
};

279. 完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

组合问题。dp[j]表示的是最小数量。j就表示序列嘛,i就是完全平方数啊,或者说完全平方数的根。背包的元素就是i2i^2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;

for (int i = 1; i * i <= n; i++) {
for (int j = i * i; j <= n; j++) {
if (dp[j - i*i] != INT_MAX) {
dp[j] = min(dp[j], dp[j - i * i] + 1);
}
}
}
return dp[n];
}
};

官方题解的好像更好理解

f[i]=1+minj=1if[ij2]f[i] = 1 +\min_{j=1}^{\lfloor\sqrt{i}\rfloor} f[i-j^2]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
dp[1] = 1;
for (int i = 1; i <= n; i++) {
int minN = INT_MAX;
for (int j = 1; j * j <= i; j++) {
minN = min(minN, dp[i - j * j]);
}
dp[i] = minN + 1;
}
return dp[n];
}
};

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

完全背包,排序问题。

dp[j]表示s从0开始截取的长度是否能够通过字典中的值组成。

dp[j]为true的前提是dp[k]为true且从k+1到j区间的字符串在字典中出现。

解法一——遍历物品时候直接遍历字典列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.length() + 1, false);
dp[0] = true;

for (int j = 1; j <= s.length(); j++) {
for (int i = 0; i < wordDict.size(); i++) {
if (j - (int) wordDict[i].length() >= 0) { // 这里需要加一个int 否则他出不来负数
string sSub = s.substr(j - wordDict[i].length(), wordDict[i].length());
if (dp[j - wordDict[i].length()] && wordDict[i] == sSub){
dp[j] = true;
break;
}
}
}
}
return dp[s.length()];
}
};

解法二——遍历物品时候,遍历截取的字符串长度

——代码随想录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.size(); i++) { // 遍历背包
for (int j = 0; j < i; j++) { // 遍历物品
string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
if (wordSet.find(word) != wordSet.end() && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.size()];
}
};

多重背包问题

多重背包问题

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

实现方式——把每种商品遍历的个数放在01背包里面在遍历一遍。

56. 携带矿石资源 (kamacoder)

你是一名宇航员,即将前往一个遥远的行星。在这个行星上,有许多不同类型的矿石资源,每种矿石都有不同的重要性和价值。你需要选择哪些矿石带回地球,但你的宇航舱有一定的容量限制。

给定一个宇航舱,最大容量为 C。现在有 N 种不同类型的矿石,每种矿石有一个重量 w[i],一个价值 v[i],以及最多 k[i] 个可用。不同类型的矿石在地球上的市场价值不同。你需要计算如何在不超过宇航舱容量的情况下,最大化你所能获取的总价值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <vector>

using namespace std;

int main() {
int C, N;
cin >> C >> N;

vector<int> weights(N), values(N), maxNums(N);
for (auto &weight:weights) {
cin >> weight;
}
for (auto &value:values) {
cin >> value;
}
for (auto &maxNum:maxNums) {
cin >> maxNum;
}

vector<int> dp(C + 1);

for (int i = 0; i < N; i++) { // 遍历物品
for (int j = C; j >= weights[i]; j--) {
for (int k = 1; k <= maxNums[i] && (j - k * weights[i]) >= 0; k++) {
dp[j] = max(dp[j], dp[j - k * weights[i]] + k * values[i]);
}
}
}
cout << dp[C] << endl;
return 0;
}

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

决定dp[i]的因素就是第i房间偷还是不偷。两种情况,不偷和偷,不偷那么就是之前任何一个位置的最大值,偷只能在间隔一个未知的最大值再加上当前的金额

如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。

如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,

然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) {
return nums[0];
}

vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);

for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
// 两种情况,不偷和偷,不偷那么就是之前任何一个位置的最大值,偷只能在间隔一个未知的最大值再加上当前的金额
}

return dp[nums.size() - 1];
}
};

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

解法一——建立二维数组

建立二维数组,分两种情况,偷0【1】和不偷0【0】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) {
return nums[0];
}
else if (nums.size() == 2) {
return max(nums[0], nums[1]);
}

vector<vector<int>> dp(nums.size(),vector<int>(2));
// 分两种情况,偷0【1】和不偷0【0】
dp[0][0] = 0;
dp[0][1] = nums[0];
dp[1][0] = nums[1];
dp[1][1] = max(nums[0], nums[1]);

for (int i = 2; i < nums.size() - 1; i++) { // 结尾单独考虑
dp[i][0] = max(dp[i - 1][0], dp[i - 2][0] + nums[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 2][1] + nums[i]);
}
// 考虑最后一家
dp[nums.size() - 1][0] = max(dp[nums.size() - 2][0], dp[nums.size() - 3][0] + nums[nums.size() - 1]); // 不偷0
dp[nums.size() - 1][1] = dp[nums.size() - 2][1]; // 偷0

return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);
}
};

解法二——利用上一题的代码,分两个子函数

三种情况:

  • 情况一:考虑不包含首尾元素
  • 情况二:考虑包含首元素,不包含尾元素
  • 情况三:考虑包含尾元素,不包含首元素

情况二、三 包括情况一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
int robRange(vector<int>& nums, int start, int end) {
if (start == end) {
return nums[start];
}
vector<int> dp(nums.size(), 0);
dp[start] = nums[start];
dp[start + 1] = max(nums[start], nums[start + 1]);

for (int i = start + 2; i <= end; i++) {
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
// 两种情况,不偷和偷,不偷那么就是之前任何一个位置的最大值,偷只能在间隔一个未知的最大值再加上当前的金额
}

return dp[end];
}
int rob(vector<int>& nums) {
if (nums.size() == 1) {
return nums[0];
}
int situation2 = robRange(nums, 0, nums.size() - 2);
int situation3 = robRange(nums, 1, nums.size() - 1);

return max(situation2, situation3);
}
};

337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

解法一——二维数组记录

递归,得知道当前节点的子节点有没有没抢劫。采用后序遍历。

这里处理起来就复杂在:

  • 左右子树:4种情况——有无。
  • 左右偷不偷:4种情况——各自偷不偷。只有左右孩子都不偷,这次才能偷。而当前不偷则应该上一轮四种情况都可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> dp;
vector<int> temp; // 索引0表示不偷, 索引1表示偷

void robPostOrder (TreeNode *p) {
if (p->left) {
robPostOrder(p->left);
}
if (p->right) {
robPostOrder(p->right);
}

// 当前节点的四种情况:左右子树都有、有一个、都没有
if (p->left && p->right) {
// 这里有四个情况啊:左偷右不偷、左不偷右偷、左偷右偷、左不偷右不偷。四种情况这次都可以不偷,但是只有最后一种情况这次可以偷
// 当前不偷:那就是左右索引上一次的偷的和(这里应该是加而不是取最大值)当前不偷上一次也可以不偷啊,
temp[0] = max(dp[dp.size() - 2][1] + dp[dp.size() - 1][1], max(dp[dp.size() - 2][0] + dp[dp.size() - 1][0], max(dp[dp.size() - 2][1] + dp[dp.size() - 1][0], dp[dp.size() - 2][0] + dp[dp.size() - 1][1])));
// 当前偷:那就是左右索引上一次的不偷的和(这里应该是加而不是取最大值)(相当于是当前索引上上次可能偷的和)
temp[1] = dp[dp.size() - 2][0] + dp[dp.size() - 1][0] + p->val;
dp.pop_back(); // 需要把子树的pop掉,不然根节点找不到左子树的
dp.pop_back();
}
else if (p->left || p->right) { // 有一个非空
// 当前不偷:那就是非空子树上一次的偷的值
temp[0] = max(dp[dp.size() - 1][1], dp[dp.size() - 1][0]);
// 当前偷:那就是非空子树上一次的不偷的值(相当于是当前索引上上次可能偷的值)
temp[1] = dp[dp.size() - 1][0] + p->val;
dp.pop_back();
}
else { // 根节点
temp[0] = 0; // 不偷
temp[1] = p->val; // 偷
}
cout << temp[0] << " " << temp[1] << endl;
dp.emplace_back(temp);
}

int rob(TreeNode* root) {
if(root == nullptr) {
return 0;
}
temp.emplace_back(0);
temp.emplace_back(0);
robPostOrder(root);
// for (auto p:dp) {
// cout << p[0] << " " << p[1] << endl;
// }
return max(dp[dp.size() - 1][0], dp[dp.size() - 1][1]);
}
};

解法二——一个数组即可

——代码随想录

做了几点简化:

  • 它不需要我的二维数组才保存dp,直接每次返回当前节点偷或者不偷的情况
  • 不偷的情况不需要我的四个和取max,只需要两边各取max即可。
  • 空节点也不需要单独考虑,直接返回0,0即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
// 长度为2的数组,0:不偷,1:偷
vector<int> robTree(TreeNode* cur) {
if (cur == NULL) return vector<int>{0, 0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0];
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};

121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

解法一——贪心

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
int maxProfit(vector<int>& prices) {
int low = INT_MAX;
int output = 0;
for (int i = 0; i < prices.size(); i++) {
low = min(low, prices[i]);
output = max(output, prices[i] - low);
}
return output;
}
};

解法二——动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(2));
dp[0][0] = - prices[0]; // 第0天买入股票,手头剩下
dp[0][1] = 0; // 第0天抛出股票
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i-1][0], - prices[i]);
dp[i][1] = max(dp[i-1][1], prices[i] + dp[i-1][0]);
}
return dp[prices.size() - 1][1];
}
};

从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。只需要记录 当前天的dp状态和前一天的dp状态就可以了,可以使用滚动数组来节省空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(2, vector<int>(2)); // 注意这里只开辟了一个2 * 2大小的二维数组
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]);
dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
}
return dp[(len - 1) % 2][1];
}
};

122. 买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

贪心见前一部分,给出动规解法

与买卖股票1的区别——dp[i][0]dp[i][0]不止和上次也没持有比较,还和上次持有但是买了有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(2));
dp[0][0] = - prices[0]; // 持有
dp[0][1] = 0; // 不持有
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
return dp[prices.size() - 1][1];
}
};

123. 买卖股票的最佳时机 III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

分四个状态:第一笔持有、第一笔卖出、第二笔持有、第二笔卖出

如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。

在动态规划结束后,由于我们可以进行不超过两笔交易,因此最终的答案在 0,sell1,sell2中,且为三者中的最大值。然而我们可以发现,由于在边界条件中 sell1和 sell2的值已经为 0,并且在状态转移的过程中我们维护的是最大值,因此 sell1和 sell2最终一定大于等于 0。同时,如果最优的情况对应的是恰好一笔交易,那么它也会因为我们在转移时允许在同一天买入并且卖出这一宽松的条件,从 sell1转移至 sell2,因此最终的答案即为 sell2

——力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(4));
dp[0][0] = - prices[0]; // 第一次持有
dp[0][1] = 0; // 第一次卖出
dp[0][2] = - prices[0]; // 第二次持有,第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了
dp[0][3] = 0; // 第二次卖出
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(- prices[i], dp[i-1][0]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
dp[i][2] = max(dp[i][1] - prices[i], dp[i-1][2]); // 之前错在这里,应该要在这里引入第一次卖出的结果
dp[i][3] = max(dp[i-1][3], dp[i-1][2] + prices[i]);
}
// cout << dp[prices.size() - 1][1] << " " << dp[prices.size() - 1][3];
return dp[prices.size() - 1][3];
}
};

188. 买卖股票的最佳时机 IV

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

和上一题一致,只是换成了循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int> (2 * k));
for (int j = 0; j < k; j++) {
dp[0][2 * j] = - prices[0]; // 2*j的位置是持有,2*j+1的位置是不持有
}
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(- prices[i], dp[i-1][0]); // 2*j的位置是持有
dp[i][1] = max(dp[i-1][0] + prices[i], dp[i-1][1]); //2*j+1的位置是不持有
for (int j = 1; j < k; j++) {
dp[i][2 * j] = max(dp[i-1][2 * j - 1] - prices[i], dp[i-1][2 * j]); // 2*j的位置是持有
dp[i][2 * j + 1] = max(dp[i-1][2 * j] + prices[i], dp[i-1][2 * j + 1]); //2*j+1的位置是不持有
}
}
return dp[prices.size() - 1][2 * k - 1];
}
};

309. 买卖股票的最佳时机含冷冻期

给定一个整数数组prices,其中第 prices[i] 表示第 *i* 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(3));
dp[0][0] = - prices[0]; // 持有
dp[0][1] = 0; // 不持有且不在不在冷冻期
dp[0][2] = 0; // 冷冻期

for(int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]); // 持有只有两种:上一期持有,上次不持有-price[i]
dp[i][1] = max(dp[i-1][1], dp[i-1][2]); // 上一次在冷冻期,或者上一次不持有
dp[i][2] = dp[i-1][0] + prices[i]; // 上一期持有,本期抛售
// cout << dp[i][0] << " " << dp[i][1] << " " << dp[i][2] << endl;
}
return max(dp[prices.size() - 1][1], dp[prices.size() - 1][2]);

}
};

714. 买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

**注意:**这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
vector<vector<int>> dp(prices.size(), vector<int>(2));
dp[0][0] = - prices[0]; // 持有
dp[0][1] = 0;// 未持有
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
}
return max(dp[prices.size() - 1][0], dp[prices.size() - 1][1]);
}
};

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(), 1); // 考虑当前元素加入时候的最长值
int output = 1;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) { // 通过之前的遍历
dp[i] = max(dp[i], dp[j] + 1);
}
}
if (dp[i] > output) {
output = dp[i];
}
}
return output;
}
};

674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

这道题看上去和动规没啥关系啊!原来我这是贪心的解法啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int temp = 1;
int output = 1;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > nums[i-1]) {
temp++;
}
else {
temp = 1;
}
if (temp > output) {
output = temp;
}
}
return output;
}
};

718. 最长重复子数组

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

解法一——二维数组

dp[i][j]:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]

减一是为了方便初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1));
// dp[i+1][j+1]表示截至nums1的i位置,nums2的j位置最长重复子数组是多少
int output = 0;
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums2.size(); j++) {
if (nums1[i] == nums2[j]) {
dp[i+1][j+1] = dp[i][j] + 1; // 注意这里是和谁比
}
if (output < dp[i+1][j+1]) {
output = dp[i+1][j+1];
}
}
}
return output;
}
};

解法二——滚动数组

718.最长重复子数组

dp[i][j]都是由dp[i - 1][j - 1]推出。那么压缩为一维数组,也就是dp[j]都是由dp[j - 1]推出。也就是相当于可以把上一层dp[i - 1][j]拷贝到下一层dp[i][j]来继续用。此时遍历B数组的时候,就要从后向前遍历,这样避免重复覆盖

注意,不相等要赋零初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<int> dp(nums2.size() + 1);
int output = 0;

for (int i = 0; i < nums1.size(); i++) {
for (int j = nums2.size() - 1; j >= 0 ; j--) {
if (nums1[i] == nums2[j]) {
dp[j+1] = dp[j] + 1;
}
else {
dp[j+1] = 0; // 不相等要附零
}
if (output < dp[j+1]) {
output = dp[j+1];
}
}
}
return output;
}
};

1143. 最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

滚动数组太难想了。

解法一——二维数组

每行对应不同的text1序列,到text1的第几位了,之前的元素最多能和text2重合几位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size() + 1, vector<int> (text2.size() + 1));

for (int i = 0; i < text1.size(); i++) {
for (int j = 0; j < text2.size(); j++) {
if (text1[i] == text2[j]) {
dp[i+1][j+1] = dp[i][j] + 1;
}
else {
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]); // 注意这里不是dp[i][j]
}
}
}
return dp[text1.size()][text2.size()];
}
};

解法二——滚动数组

太绕了,还是二维数组吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<int> dp(text2.size() + 1);
int output = 0;
for (int i = 0; i < text1.size(); i++) {
int temp = 0; // 存储上一位没有+1的值
for (int j = 0; j < text2.size(); j++) {
/* 这里会存在一个矛盾,当text1[i] == text2[j]时候,我要加1,我希望这里的dp[j]是没有加1过的
但是当text1[i] != text2[j]时候,我需要在两种情况下(①i和j+1;②i+1和j)取最大,希望这里的dp[j]是加1过的
*/
if (text1[i] == text2[j]) {
int old = dp[j+1];
dp[j+1] = temp + 1;
temp = max(old, temp); // 这里需要比较的是dp[j+1]原来的值和原来的temp,
}
else {
dp[j+1] = max(dp[j+1], temp);
temp = dp[j+1];
}
// int newlen = max(temp, dp[j+1]);
// if (text1[i] == text2[j]) {
// dp[j+1] = temp+1;
// }
// temp = newlen;
if (dp[j+1] > output) {
output = dp[j+1];
}
}
}
return output;
}
};

1035. 不相交的线

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i]nums2[j] 的直线,这些直线需要同时满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

难就难在这一步抽象!: 直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。

就转换成了上一道题的问题。这里我还是用滚动数组来试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
vector<int> dp(nums2.size() + 1);
int output = 0;
for (int i = 0; i < nums1.size(); i++) {
int temp = 0;
for (int j = 0; j < nums2.size(); j++) {
int newlen = max(dp[j+1], temp); // 先存一下到当前位置最多连线数量是多少,不考虑该位置相等加一的问题。
if (nums1[i] == nums2[j]) { // 相等了也是在temp的基础上加
dp[j+1] = temp + 1;
}
temp = newlen;
if (output < dp[j+1]) {
output = dp[j+1];
}
}
}
return output;
}
};

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

子数组 是数组中连续的 非空 元素序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0] = nums[0];
int output = nums[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i-1] + nums[i], nums[i]); // 如果之前的加上这个数反而还不如这个数字,那就没意义了
if (dp[i] > output) {
output = dp[i];
}
}
return output;
}
};

392. 判断子序列

给定字符串 st ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

解法一——双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
bool isSubsequence(string s, string t) {
if (s.length() > t.length()) {
return false;
}
int start = 0;
for (int i = 0; i < s.length(); i++) {
for (int j = start; j < t.length(); j++) {
cout << i << " " << j << endl;
if (s[i] == t[j]) {
start = j + 1;
if (start == t.length() && i != s.length() - 1) { // 防止超出去
return false;
}
break;
}
if (j == t.length() - 1 && s[i] != t[j]) {
return false;
}
}
}
return true;
}
};

解法二——动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
bool isSubsequence(string s, string t) {
vector<vector<int>> dp(s.length() + 1, vector<int>(t.length() + 1));

for (int i = 0; i < s.length(); i++) {
for(int j = 0; j < t.length(); j++) {
if (s[i] == t[j]) {
dp[i+1][j+1] = dp[i][j] + 1;
}
else {
dp[i+1][j+1] = dp[i+1][j]; // 未匹配,保留上一个匹配的值
}
}
}
return dp[s.length()][t.length()] == s.length();
}
};

115. 不同的子序列

给你两个字符串 st ,统计并返回在 s子序列t 出现的个数,结果需要对 109+710^9 + 7​ 取模。

或者也可以把i为1的情况放进去一起考虑,后来发现不需要乘了,而是加的关系。但是第0行还是需要初始化的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
int numDistinct(string s, string t) {
vector<vector<unsigned long long>> dp(t.length() + 1, vector<unsigned long long> (s.length() + 1)); // t才是要找的子序列
// dp[i+1][j+1]表示截至t的第i个字母,s的第j个字母有多少种方案。
int temp = 0;
for (int j = 0; j < s.length(); j++) {
if (t[0] == s[j]) {
dp[1][j+1] = ++temp;
}
else {
dp[1][j+1] = dp[1][j]; // 不等那么就保留前面的
}
// cout << 1 << " " << j+1 << " " << dp[1][j+1] << " " << temp << endl;
}
if (temp == 0) {
return 0;
}
for (int i = 1; i < t.length(); i++) {
temp = 0; // 当前字母i有几种
for (int j = i; j < s.length(); j++) {
if(t[i] == s[j]) {
temp++;
dp[i+1][j+1] = dp[i+1][j] + dp[i][j]; // 因为这里要考虑乘法,所以需要单独考虑s的第0个字母。
}
else {
dp[i+1][j+1] = dp[i+1][j];
}
// cout << i+1 << " " << j+1 << " " << dp[i+1][j+1] << " " << temp << endl;
}
if (temp == 0) {
return 0;
}
}
return dp[t.length()][s.length()];
}
};

583. 两个字符串的删除操作

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

求最大公共子串?然后再减一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));

for (int i = 0; i < word1.size(); i++) {
for (int j = 0; j < word2.size(); j++) {
if (word1[i] == word2[j]) {
dp[i+1][j+1] = dp[i][j] + 1;
}
else {
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]);
}
}
}
return (word1.size() + word2.size() - 2 * dp[word1.size()][word2.size()]);
}
};

72. 编辑距离

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

我们可以发现,如果我们有单词 A 和单词 B:

对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;

同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;

对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。

这样以来,本质不同的操作实际上只有三种:

  • 在单词 A 中插入一个字符;
  • 在单词 B 中插入一个字符;
  • 修改单词 A 的一个字符

——力扣官方题解

两个单词都可以操作!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
// 边界条件需要初始化,毕竟你一个单词万一是空的这么说直接啥也不需要了?
// dp[0][0] = 0;
for (int i = 0; i < word1.size(); i++) {
dp[i+1][0] = i+1;
}
for (int j = 0; j < word2.size(); j++) {
dp[0][j+1] = j+1;
}
for (int i = 0; i < word1.size(); i++) {
for (int j = 0; j < word2.size(); j++) {
if (word1[i] == word2[j]) {
dp[i+1][j+1] = dp[i][j];
}
else {
// 否则三种操作,A加一个单词,B加一个单词,A换一个字符
dp[i+1][j+1] = min({dp[i][j+1], dp[i+1][j], dp[i][j]}) + 1;
}
}
}
return dp[word1.size()][word2.size()];
}
};

647. 回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

解法一——双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
int countSubstrings(string s) {
int left, right;
int output = 1; // 0位置的

for (int center = 1; center < s.length(); center++) {
output ++; // 独自的
// 区分奇偶,偶数往左找
if (s[center] == s[center - 1]) { // 偶
left = center - 1;
right = center;
while (left >= 0 && right < s.length() && s[left] == s[right]) {
output++;
left--;
right++;
}
}
// 奇数是必然有的情况,不需要else
left = center - 1;
right = center + 1;
while (left >= 0 && right < s.length() && s[left] == s[right]) {
output++;
left--;
right++;
}
}
return output;
}
};

解法二——动态规划

布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

遍历时候,需要的元素出现在左下角。

647.回文子串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.length(), vector<bool>(s.length(), false));
int output = 0;

for (int i = s.length() - 1; i >= 0; i--) {
for (int j = i; j < s.length(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i+1][j-1])) {
output++;
dp[i][j] = true;
}
}
}
return output;
}
};

不如双指针好理解。

516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.length(), vector<int>(s.length()));
// 保存的是区间内回文子串最长是多少
for (int i = s.length() - 1; i >= 0; i--) {
for (int j = i; j < s.length(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) {
dp[i][j] = j - i + 1;
}
else {
dp[i][j] = dp[i+1][j-1] + 2;
}
}
else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
}
return dp[0][s.length() - 1];
}
};

单调栈

739. 每日温度

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

只要存下标就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> S; //栈底元素对应的温度最大(只要存下标!!!) 栈里面的元素递增
int numSize = temperatures.size();
vector<int> output(numSize);
S.push(numSize - 1);
for (int i = numSize - 2; i >= 0; i--) {
while (!S.empty() && temperatures[i] >= temperatures[S.top()]) {
S.pop();
}
if (S.empty()) {
output[i] = 0;
}
else {
output[i] = S.top() - i;
}
S.push(i);
}
return output;
}
};

496. 下一个更大元素 I

nums1 中数字 x下一个更大元素 是指 xnums2 中对应位置 右侧第一个x 大的元素。

给你两个 没有重复元素 的数组 nums1nums2 ,下标从 0 开始计数,其中nums1nums2 的子集。

对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j]下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1

返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素

单调栈+哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
map<int, int> nums1Index;
stack<int> S; // 这道题可以直接存数值了
vector<int> output(nums1.size());
for (int i = 0; i < nums1.size(); i++) {
nums1Index[nums1[i]] = i + 1;
}
for (int j = nums2.size() - 1; j >= 0; j--) {
if (nums1Index[nums2[j]] == 0) {
S.push(nums2[j]);
continue;
}
while (!S.empty() && nums2[j] >= S.top()) {
S.pop();
}
// cout << nums1Index[nums2[j]] - 1 << endl;
if (S.empty()) {
output[nums1Index[nums2[j]] - 1] = -1;
}
else {
output[nums1Index[nums2[j]] - 1] = S.top();
}
S.push(nums2[j]);
}
return output;
}
};

503. 下一个更大元素 II

给定一个循环数组 numsnums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素

数字 x下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1

模一下?做两遍,第一遍不操作output;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
stack<int> S; // 先存一遍,但不操作output;
vector<int> output(nums.size());

for (int i = nums.size() - 1; i >= 0; i--) {
while (!S.empty() && (nums[i] >= S.top())) {
S.pop();
}
S.push(nums[i]);
}
for (int i = nums.size() - 1; i >= 0; i--) {
while (!S.empty() && (nums[i] >= S.top())) {
S.pop();
}
if (S.empty()) {
output[i] = -1;
}
else {
output[i] = S.top();
}
S.push(nums[i]);
}
return output;
}
};

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

img

(PS:怎么想到了注水法功控,虽然好像不完全是一回事情!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int trap(vector<int>& height) {
int output = 0;
stack<int> S; // 还是应该存下标
for (int i = 0; i < height.size(); i++) {
while (!S.empty() && height[i] >= height[S.top()]) {
int last = height[S.top()];
// cout << i << " " << S.top() << " " ;
S.pop(); //只有在每pop一次才相当于积攒了一部分水
if (!S.empty()) { // S空了左边是挡不住水的
int length = i - S.top() - 1; // 因为减完包含了最左边挡住的柱子
output += length * (min(height[S.top()] , height[i]) - last); // 距离*高度
}
// cout << output << endl;
}
S.push(i);
}
return output;
}
};

84. 柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

img

“42. 接雨水” 是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子。

太绕了,吐血

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack <int> S; // 保存下标;
heights.insert(heights.begin(), 0);
heights.push_back(0);
S.push(0);
int output = 0;
int temp = 0;
int last = 0;
for (int i = 1; i < heights.size(); i++) {
while (!S.empty() && heights[i] < heights[S.top()]) {
last = heights[S.top()];
S.pop();
if (!S.empty()) {
temp = (i - S.top() - 1) * last; // 不在S里面的值都比边缘高
}
else {
temp = (i + 1) * heights[i];
}
if (temp > output) {
output = temp;
}
}
// if (S.empty()) {
// cout << i << " " << output << " " << last << endl;
// }
// else {
// cout << i << " " << output << " " << last << " " << S.top() << endl;
// }
S.push(i);
}
return output;
}
};

图论

深搜(DFS)和广搜(BFS)

  • dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
  • bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。

DFS

回溯算法,其实就是dfs的过程,这里给出dfs的代码框架:

1
2
3
4
5
6
7
8
9
10
11
12
void dfs(参数) {
if (终止条件) {
存放结果;
return;
}

for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}

BFS——一圈一圈搜索

图三

仅仅需要一个容器,能保存我们要遍历过的元素就可以,那么用队列,还是用栈,甚至用数组,都是可以的

广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
queue<pair<int, int>> que; // 定义队列
que.push({x, y}); // 起始节点加入队列
visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
while(!que.empty()) { // 开始遍历队列里的元素
pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
int curx = cur.first;
int cury = cur.second; // 当前节点坐标
for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过
if (!visited[nextx][nexty]) { // 如果节点没被访问过
que.push({nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点
visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
}
}
}

}

——代码随想录

797. 所有可能的路径

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序

graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
vector<int> temp;
vector<vector<int>> output;
void dfs(vector<vector<int>>& graph, int node) {
if (node == graph.size() - 1) {
output.emplace_back(temp);
return;
}
for (auto nodeNext:graph[node]) {
temp.emplace_back(nodeNext);
dfs(graph, nodeNext);
temp.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
if (graph.empty()) {
return output;
}
temp.emplace_back(0);
dfs(graph, 0);
temp.pop_back();
return output;
}
};

200. 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

解法一——DFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};
void dfs (vector<vector<char>> &grid, vector<vector<bool>> &visited, int x, int y) {
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()) {
continue; //超范围
}
if (!visited[xNext][yNext] && grid[xNext][yNext] == '1') {
// cout << xNext << " " << yNext << " " << endl;
visited[xNext][yNext] = true;
dfs(grid, visited, xNext, yNext);
}
}
}

int numIslands(vector<vector<char>>& grid) {
int n = grid.size(); // 行
int m = grid[0].size(); // 列
vector<vector<bool>> visited(n, vector<bool> (m, false));
int output = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!visited[i][j] && grid[i][j] == '1') {
// cout << i << " " << j << " " << output << endl;
// 未访问过,且是陆地
visited[i][j] = true;
output++;
dfs(grid, visited, i, j); // 将所有链接到的陆地标记为true
}
}
}
return output;
}
};

解法二——BFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};
void bfs (vector<vector<char>> &grid, vector<vector<bool>> &visited, int x, int y) {
queue<pair<int, int >> Q;
visited[x][y] = true; // 以起始点为圆心广搜
Q.push({x, y});
while(!Q.empty()) {
pair<int, int> cur = Q.front();
Q.pop();
int xCur = cur.first;
int yCur = cur.second;
for (int i = 0; i < 4; i++) {
int xNext = xCur + dir[i][0];
int yNext = yCur + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()) {
continue; //超范围
}
if (!visited[xNext][yNext] && grid[xNext][yNext] == '1') {
visited[xNext][yNext] = true;
Q.push({xNext, yNext});
}
}

}
}

int numIslands(vector<vector<char>>& grid) {
int n = grid.size(); // 行
int m = grid[0].size(); // 列
vector<vector<bool>> visited(n, vector<bool> (m, false));
int output = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!visited[i][j] && grid[i][j] == '1') {
// cout << i << " " << j << " " << output << endl;
// 未访问过,且是陆地
// visited[i][j] = true;
output++;
bfs(grid, visited, i, j); // 将所有链接到的陆地标记为true
}
}
}
return output;
}
};

695. 岛屿的最大面积

给你一个大小为 m x n 的二进制矩阵 grid

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0

考虑使用广搜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};

int bfs(vector<vector<int>> &grid, vector<vector<bool>> &visited, int x, int y) {
queue <pair<int, int>> Q;
Q.push({x, y});
visited[x][y] = true;
int areaSize = 1;
while (!Q.empty()) {
pair<int, int> cur = Q.front();
Q.pop();
for (int i = 0; i < 4; i++) {
int xNext = cur.first + dir[i][0];
int yNext = cur.second + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()) {
continue;
}
if (!visited[xNext][yNext] && grid[xNext][yNext] == 1) {
visited[xNext][yNext] = true;
Q.push({xNext, yNext});
areaSize++;
}
}
}
return areaSize;
}

int maxAreaOfIsland(vector<vector<int>>& grid) {
vector<vector<bool>> visited(grid.size(), vector<bool>(grid[0].size(), false));
int output = 0;
for (int i = 0; i < grid.size(); i++) {
for(int j = 0; j < grid[0].size(); j++) {
if (!visited[i][j] && grid[i][j] == 1) {
int areaSize = bfs(grid, visited, i, j);
// cout << i << " " << j << " " << areaSize << endl;
output = max(areaSize, output);
}
}
}
return output;
}
};

1020. 飞地的数量

给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。

一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。

返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。

解法一——深搜+visited数组

思路:通过深搜,标记边缘点能够联通的位置。再统计内部其余未被visit的点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Solution {
public:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};
void dfs (vector<vector<int>>& grid, vector<vector<bool>> &visited, int x, int y) {
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()){
continue;
}
if (!visited[xNext][yNext] && grid[xNext][yNext] == 1) {
visited[xNext][yNext] = true;
dfs(grid, visited, xNext, yNext);
}
}
}
int numEnclaves(vector<vector<int>>& grid) {
vector<vector<bool>> visited(grid.size(), vector<bool> (grid[0].size()));
for (int j = 0; j < grid[0].size(); j++) { // 第一行和最后一行
if (!visited[0][j] && grid[0][j] == 1){
visited[0][j] = true;
dfs(grid, visited, 0, j);
}
if (!visited[grid.size() - 1][j] && grid[grid.size() - 1][j] == 1){
visited[grid.size() - 1][j] = true;
dfs(grid, visited, grid.size() - 1, j);
}
}
for (int i = 1; i < grid.size() - 1; i++) { // 第一列和最后一列
if (!visited[i][0] && grid[i][0] == 1){
visited[i][0] = true;
dfs(grid, visited, i, 0);
}
if (!visited[i][grid[0].size() - 1] && grid[i][grid[0].size() - 1] == 1){
visited[i][grid[0].size() - 1] = true;
dfs(grid, visited, i, grid[0].size() - 1);
}
}
int count = 0;
for (int i = 1; i < grid.size() - 1; i++) {
for(int j = 1; j < grid[0].size() - 1; j++) {
if (!visited[i][j] && grid[i][j] == 1) {
count++;
}
}
}
return count;
}
};

解法二——深搜+优化空间

不使用visited数组,直接对grid进行操作。visit过就将对应位置置零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution {
public:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};
void dfs (vector<vector<int>>& grid, int x, int y) {
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()){
continue;
}
if (grid[xNext][yNext] == 1) {
grid[xNext][yNext] = 0;
dfs(grid, xNext, yNext);
}
}
}
int numEnclaves(vector<vector<int>>& grid) {
for (int j = 0; j < grid[0].size(); j++) { // 第一行和最后一行
if (grid[0][j] == 1){
grid[0][j] = 0;
dfs(grid, 0, j);
}
if (grid[grid.size() - 1][j] == 1){
grid[grid.size() - 1][j] = 0;
dfs(grid, grid.size() - 1, j);
}
}
for (int i = 1; i < grid.size() - 1; i++) { // 第一列和最后一列
if (grid[i][0] == 1){
grid[i][0] = 0;
dfs(grid, i, 0);
}
if (grid[i][grid[0].size() - 1] == 1){
grid[i][grid[0].size() - 1] = 0;
dfs(grid, i, grid[0].size() - 1);
}
}
int count = 0;
for (int i = 1; i < grid.size() - 1; i++) {
for(int j = 1; j < grid[0].size() - 1; j++) {
if (grid[i][j] == 1) {
count++;
}
}
}
return count;
}
};

130. 被围绕的区域

给你一个 m x n 的矩阵 board ,由若干字符 'X''O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O''X' 填充。

与边缘相连的陆地不能沉没,用’T’表示。最后对中心处理,没有和边缘相连的陆地’O’沉没掉变成’X’,而相连的’T’改成’O’。

解法一——广搜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
void bfs(vector<vector<char>> &board, int x, int y) {
queue <pair<int, int>> Q;
Q.push({x, y});
while (!Q.empty()) {
pair<int,int> cur = Q.front();
Q.pop();
for (int i = 0; i < 4; i++) {
int xNext = cur.first + dir[i][0];
int yNext = cur.second + dir[i][1];
if (xNext <= 0 || yNext <= 0 || xNext >= board.size() - 1 || yNext >= board[0].size() - 1) {
continue;
}
if (board[xNext][yNext] == 'O') {
board[xNext][yNext] = 'T';
Q.push({xNext, yNext});
}
}
}

}
void solve(vector<vector<char>>& board) {
for (int j = 0; j < board[0].size(); j++) {// 首尾行
if (board[0][j] == 'O') {
bfs(board, 0, j);
}
if (board[board.size() - 1][j] == 'O') {
bfs(board, board.size() - 1, j);
}
}
for (int i = 1; i < board.size() - 1; i++) {// 首尾列
if (board[i][0] == 'O') {
bfs(board, i, 0);
}
if (board[i][board[0].size() - 1] == 'O') {
bfs(board, i, board[0].size() - 1);
}
}
for (int i = 1; i < board.size() - 1; i++) {
for (int j = 1; j < board[0].size() - 1; j++) {
// cout << board[i][j] << " ";
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
else if (board[i][j] == 'T') {
board[i][j] = 'O';
}
}
// cout << endl;
}
}
};

解法二——深搜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
void dfs (vector<vector<char>>& grid, int x, int y) {
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext <= 0 || yNext <= 0 || xNext >= grid.size() - 1 || yNext >= grid[0].size() - 1){
continue;
}
if (grid[xNext][yNext] == 'O') {
grid[xNext][yNext] = 'T';
dfs(grid, xNext, yNext);
}
}
}
void solve(vector<vector<char>>& board) {
for (int j = 0; j < board[0].size(); j++) {// 首尾行
if (board[0][j] == 'O') {
dfs(board, 0, j);
}
if (board[board.size() - 1][j] == 'O') {
dfs(board, board.size() - 1, j);
}
}
for (int i = 1; i < board.size() - 1; i++) {// 首尾列
if (board[i][0] == 'O') {
dfs(board, i, 0);
}
if (board[i][board[0].size() - 1] == 'O') {
dfs(board, i, board[0].size() - 1);
}
}
for (int i = 1; i < board.size() - 1; i++) {
for (int j = 1; j < board[0].size() - 1; j++) {
// cout << board[i][j] << " ";
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
else if (board[i][j] == 'T') {
board[i][j] = 'O';
}
}
// cout << endl;
}
}
};

417. 太平洋大西洋水流问题

有一个 m × n 的矩形岛屿,与 太平洋大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heightsheights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度

岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回网格坐标 result2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋

想法:建一个表格表示每个位置能够流到哪里。建立表格的原则是低的能向高的覆盖。

原来想一并把output结果处理的,结果还是不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
vector<vector<int>> output;
void dfs(vector<vector<int>>& heights, vector<vector<int>>& flows, int x, int y) {
// vector<int> temp(2);
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= heights.size() || yNext >= heights[0].size()) {
continue;
}
if (heights[xNext][yNext] >= heights[x][y]) {
// next位置能流到当前位置
// cout << x << " " << y << " " << flows[x][y] << "|| " << xNext << " " << yNext << " " << flows[xNext][yNext] ;
if (flows[xNext][yNext] == flows[x][y]) { // 33,22,11,00
continue;
}
else if (flows[xNext][yNext] + flows[x][y] >= 3 && flows[xNext][yNext] < 3) { // 32,23,31,13,30,03,12,21
// temp[0] = xNext;
// temp[1] = yNext;
// output.emplace_back(temp);
flows[xNext][yNext] = 3;
}
else { //01,10,02,20
flows[xNext][yNext] = max(flows[xNext][yNext], flows[x][y]);
}
// cout << " || " << flows[xNext][yNext] << endl;
dfs(heights, flows, xNext, yNext);
}
}
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
vector<int> temp(2);
// // 两个角落的先传进去
// temp[0] = 0; temp[1] = heights[0].size() - 1; output.emplace_back(temp);
// if (heights[0].size() == 1 && heights.size() == 1) {
// return output;
// }
// temp[1] = 0; temp[0] = heights.size() - 1; output.emplace_back(temp);

vector<vector<int>> flows(heights.size(), vector<int>(heights[0].size()));
// 初始化左上为1(太平洋)、右下为2(大西洋)、右上角、左下角角落为3(全都可以)
for (int i = 0; i < heights.size(); i++) {// 首尾列
flows[i][0] += 1;
flows[i][heights[0].size() - 1] += 2;
}
for (int j = 1; j < heights[0].size() - 1; j++) {// 首尾行
flows[0][j] += 1;
flows[heights.size() - 1][j] += 2;
}
flows[0][heights[0].size() - 1] = 3;
flows[heights.size() - 1][0] = 3;
// 遍历
for (int i = 0; i < heights.size(); i++) {// 首尾列
// if (flows[i][0] == 3) {
// temp[0] = i; temp[1] = 0; output.emplace_back(temp);
// }
dfs(heights, flows, i, 0);
// if (heights[0].size() == 1){
// continue;
// }
// if (flows[i][heights[0].size() - 1] == 3) {
// temp[0] = i; temp[1] = heights[0].size() - 1; output.emplace_back(temp);
// }
dfs(heights, flows, i, heights[0].size() - 1);
}
for (int j = 1; j < heights[0].size() - 1; j++) {// 首尾行
// if (flows[0][j] == 3) {
// temp[0] = 0; temp[1] = j; output.emplace_back(temp);
// }
dfs(heights, flows, 0, j);
// if (heights.size() == 1){
// continue;
// }
// if (flows[heights.size() - 1][j] == 3) {
// temp[0] = heights.size() - 1; temp[1] = j; output.emplace_back(temp);
// }
dfs(heights, flows, heights.size() - 1, j);
}
for (int i = 0; i < heights.size() ; i++) {
for (int j = 0; j < heights[0].size() ; j++) {
if (flows[i][j] == 3){
temp[0] = i;
temp[1] = j;
output.emplace_back(temp);
}
}
}
return output;
}
};

改了一下能够一遍处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
vector<vector<int>> output;
void dfs(vector<vector<int>>& heights, vector<vector<int>>& flows, int x, int y) {
vector<int> temp(2);
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= heights.size() || yNext >= heights[0].size()) {
continue;
}
if (heights[xNext][yNext] >= heights[x][y]) {
// next位置能流到当前位置
// cout << x << " " << y << " " << flows[x][y] << "|| " << xNext << " " << yNext << " " << flows[xNext][yNext] ;
if (flows[xNext][yNext] == flows[x][y]) { // 33,22,11,00
continue;
}
else if (flows[xNext][yNext] + flows[x][y] >= 3 && flows[xNext][yNext] < 3) { // 32,23,31,13,30,03,12,21
temp[0] = xNext;
temp[1] = yNext;
output.emplace_back(temp);
flows[xNext][yNext] = 3;
}
else { //01,10,02,20
flows[xNext][yNext] = max(flows[xNext][yNext], flows[x][y]);
}
// cout << " || " << flows[xNext][yNext] << endl;
dfs(heights, flows, xNext, yNext);
}
}
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
vector<int> temp(2);
// // 两个角落的先传进去
// temp[0] = 0; temp[1] = heights[0].size() - 1; output.emplace_back(temp);
// if (heights[0].size() == 1 && heights.size() == 1) {
// return output;
// }
// temp[1] = 0; temp[0] = heights.size() - 1; output.emplace_back(temp);

vector<vector<int>> flows(heights.size(), vector<int>(heights[0].size()));
// 初始化左上为1(太平洋)、右下为2(大西洋)、右上角、左下角角落为3(全都可以)
for (int i = 0; i < heights.size(); i++) {// 首尾列
flows[i][0] += 1;
flows[i][heights[0].size() - 1] += 2;
}
for (int j = 1; j < heights[0].size() - 1; j++) {// 首尾行
flows[0][j] += 1;
flows[heights.size() - 1][j] += 2;
}
flows[0][heights[0].size() - 1] = 3;
flows[heights.size() - 1][0] = 3;
for (int i = 0; i < heights.size(); i++) {// 首尾列
if (flows[i][0] == 3) {
temp[0] = i; temp[1] = 0; output.emplace_back(temp);
}
if (heights[0].size() != 1 && flows[i][heights[0].size() - 1] == 3) {
temp[0] = i; temp[1] = heights[0].size() - 1; output.emplace_back(temp);
}
}
for (int j = 1; j < heights[0].size() - 1; j++) {// 首尾行
if (flows[0][j] == 3) {
temp[0] = 0; temp[1] = j; output.emplace_back(temp);
}
if (heights.size() != 1 && flows[heights.size() - 1][j] == 3) {
temp[0] = heights.size() - 1; temp[1] = j; output.emplace_back(temp);
}
}
// 遍历
for (int i = 0; i < heights.size(); i++) {// 首尾列
dfs(heights, flows, i, 0);
if (heights[0].size() == 1){
continue;
}
dfs(heights, flows, i, heights[0].size() - 1);
}
for (int j = 1; j < heights[0].size() - 1; j++) {// 首尾行
dfs(heights, flows, 0, j);
if (heights.size() == 1){
continue;
}
dfs(heights, flows, heights.size() - 1, j);
}
// for (int i = 0; i < heights.size() ; i++) {
// for (int j = 0; j < heights[0].size() ; j++) {
// if (flows[i][j] == 3){
// temp[0] = i;
// temp[1] = j;
// output.emplace_back(temp);
// }
// }
// }
return output;
}
};

827. 最大人工岛

给你一个大小为 n x n 二进制矩阵 grid最多 只能将一格 0 变成 1

返回执行此操作后,grid 中最大的岛屿面积是多少?

岛屿 由一组上、下、左、右四个方向相连的 1 形成。

暴力思路——每次改一个,看结果怎么变化。

思路:一遍深搜先把每个位置对应的岛屿大小保存。第二遍只检测原来为0的位置,通过四个方向的岛屿面积累加得到。这里保存岛屿大小时候需要把岛屿的编号也保存,以保证独立性。由于统计岛屿大小时候无法直接录入最终的岛屿面积,所以只保存编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
int count;
void dfs (vector<vector<int>>& grid,int x, int y, int islandIndex) {
// cout << x << " " << y << " " << count << endl;
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()) {
continue;
}
if (grid[xNext][yNext] == 1) {
grid[xNext][yNext] = islandIndex;
count ++;
dfs(grid, xNext, yNext, islandIndex);
}
}
}

// int islandAreaSum (vector<vector<int>> areaIndex, int x, int y, unordered_map <int, int> areaMap) {
// int area = 0;
// for (int i = 0; i < 4; i++) {
// int xNext = x + dir[i][0];
// int yNext = y + dir[i][1];
// if (xNext < 0 || yNext < 0 || xNext >= areaIndex.size() || yNext >= areaIndex[0].size()) {
// continue;
// }
// if (areaIndex[xNext][yNext] != 0) {
// // cout << xNext << " | " << yNext << " " <<areaMap[areaIndex[xNext][yNext]] << endl;
// area += areaMap[areaIndex[xNext][yNext]];
// areaMap[areaIndex[xNext][yNext]] = 0; // 置零只能加一次,同时这个局部变量不影响全局
// }
// }
// // cout << x << " " << y << " " << area << endl;
// return area + 1;
// }

int largestIsland(vector<vector<int>>& grid) {
// vector<vector<int>> areaIndex(grid.size(), vector<int>(grid[0].size())); // 保存每个位置的编号
unordered_map <int, int> areaMap; // 保存编号和面积的关系
bool allOne = true;

// 一遍深搜先把每个位置对应的岛屿大小保存。
int islandIndex = 2;
for (int i = 0; i < grid.size(); i++) {
for(int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 0) {
allOne = false;
}
if (grid[i][j] == 1) {
grid[i][j] = islandIndex;
count = 1;
dfs(grid, i, j, islandIndex);
areaMap[islandIndex] = count;
islandIndex++;
// cout << count << " ";
}
}
}

if (allOne) {
return grid.size() * grid[0].size();
}

// 二次深搜搜原来为0的位置,累加能得到的最大面积
int maxArea = 0;


for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 0) {
// 检查四个方向陆地面积的和
unordered_set<int> temp;
int area = 0;
for (int k = 0; k < 4; k++) {
int xNext = i + dir[k][0];
int yNext = j + dir[k][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()) {
continue;
}
if (temp.find(grid[xNext][yNext]) == temp.end()) {
// cout << xNext << " | " << yNext << " " <<areaMap[areaIndex[xNext][yNext]] << endl;
area += areaMap[grid[xNext][yNext]];
temp.insert(grid[xNext][yNext]);
// areaMap[areaIndex[xNext][yNext]] = 0;
}
}
maxArea = max(area + 1, maxArea);
// maxArea = max(islandAreaSum(grid, i, j, areaMap), maxArea);
}
}
}
return maxArea;
}
};

127. 单词接龙

字典 wordList 中从单词 beginWordendWord转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  • 对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
  • sk == endWord

给你两个单词 beginWordendWord 和一个字典 wordList ,返回 beginWordendWord最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0

  • 图中的线是如何连在一起的
  • 起点和终点的最短路径长度

这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> wordSet(wordList.begin(), wordList.end());
if (wordSet.find(endWord) == wordSet.end()) {
return 0;
}
unordered_map<string, int> isVisited; // 保存到这个单词的路径长度
// 使用广搜
queue<string> Q;
Q.push(beginWord);
isVisited[beginWord] = 1;

while (!Q.empty()) {
string cur = Q.front();
Q.pop();
for (int i = 0; i < cur.length(); i++) {
string newWord = cur;
for (int j = 0; j < 26; j++) {
newWord[i] = 'a' + j;
if (newWord == endWord) {
return isVisited[cur] + 1;
}
if (wordSet.find(newWord) != wordSet.end() && isVisited[newWord] == 0) {
isVisited[newWord] = isVisited[cur] + 1;
Q.push(newWord);
}
}
}
}
return 0;
}
};

841. 钥匙和房间

n 个房间,房间按从 0n - 1 编号。最初,除 0 号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。

当你进入一个房间,你可能会在里面找到一套不同的钥匙,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。

给你一个数组 rooms 其中 rooms[i] 是你进入 i 号房间可以获得的钥匙集合。如果能进入 所有 房间返回 true,否则返回 false

和上一题的思路类似,利用广度搜索。但是可以不定义isVisited为int了,bool就够了,外面放一个count计数满为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
bool canVisitAllRooms(vector<vector<int>>& rooms) {
int count = 1; // 0号房间一定能够进入
queue <int> Q;
vector<bool> visited(rooms.size(), false);
visited[0] = true;
for (auto room:rooms[0]) {
Q.push(room);
visited[room] = true;
count++;
if (count == rooms.size()) {
return true;
}
}

while(!Q.empty()) {
int cur = Q.front();
Q.pop();
for (int i = 0; i < rooms[cur].size(); i++) {
if (visited[rooms[cur][i]] == false) { // 说明还没有过
Q.push(rooms[cur][i]);
visited[rooms[cur][i]] = true;
count++;
if (count == rooms.size()) {
return true;
}
}
}
}

return false;
}
};

463. 岛屿的周长

给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。

网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。

岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

解法一——深搜

深搜把

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
int count = 0;
void dfs(vector<vector<int>>& grid, int x, int y) {
for(int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size()) {
count++;
continue;
}
if (grid[xNext][yNext] == 1 ) {
grid[xNext][yNext] = 2;
dfs(grid, xNext, yNext);
}
else if (grid[xNext][yNext] == 0){
count++;
}
}
}
int islandPerimeter(vector<vector<int>>& grid) {
// vector<vector<bool>> visited(grid.size(), vector<bool>(grid[0].size(), false)) ;
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 1) {
grid[i][j] = 2;
dfs(grid, i, j);
return count;
}
}
}
return 0;
}
};

解法二——遍历

遍历每一个空格,遇到岛屿,计算其上下左右的情况,遇到水域或者出界的情况,就可以计算边了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int islandPerimeter(vector<vector<int>>& grid) {
// vector<vector<bool>> visited(grid.size(), vector<bool>(grid[0].size(), false)) ;
int count = 0;
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
for (int i = 0; i < grid.size(); i++) {
for (int j = 0; j < grid[0].size(); j++) {
if (grid[i][j] == 1) {
for(int k = 0; k < 4; k++) {
int xNext = i + dir[k][0];
int yNext = j + dir[k][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size() || grid[xNext][yNext] == 0) {
count++;
}
}
}
}
}
return count;
}

并查集

模板

大白话就是当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。

并查集主要有两个功能:

  • 将两个元素添加到一个集合中。
  • 判断两个元素在不在同一个集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构

// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}

// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}

通过模板,我们可以知道,并查集主要有三个功能。

  1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
  2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
  3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点

2492. 两个城市间路径的最小分数

给你一个正整数 n ,表示总共有 n 个城市,城市从 1n 编号。给你一个二维数组 roads ,其中 roads[i] = [ai, bi, distancei] 表示城市 aibi 之间有一条 双向 道路,道路距离为 distancei 。城市构成的图不一定是连通的。

两个城市之间一条路径的 分数 定义为这条路径中道路的 最小 距离。

城市 1 和城市 n 之间的所有路径的 最小 分数。

注意:

  • 一条路径指的是两个城市之间的道路序列。
  • 一条路径可以 多次 包含同一条道路,你也可以沿着路径多次到达城市 1 和城市 n
  • 测试数据保证城市 1 和城市n 之间 至少 有一条路径。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public:
vector<int> parent = vector<int> (100005,0);

void init() {
for (int i = 0; i < 100005; ++i) {
parent[i] = i;
}
}
int findparent( int x) {
// if (parent[x] != x) {
// parent[x] = findparent(parent, parent[x]);
// }
// return parent[x];
return x == parent[x] ? x : (parent[x] = findparent(parent[x])); // 路径压缩
}
void add(int x, int y) {
int fx = findparent(x);
int fy = findparent(y);
parent[fx] = fy;
}
int minScore(int n, vector<vector<int>>& roads) {

// vector<int> parent(n + 1);
init();

for (auto& rd : roads) {
int x = rd[0];
int y = rd[1];
add(x, y);
}
int res = INT_MAX;
int f0 = findparent(1);
cout << f0 << " ";
for (auto& rd : roads) {
int x = rd[0];
int fx = findparent( x);
// int fy = findparent(parent, rd[1]);
cout << fx << " ";
if (fx == f0 ) {
res = min(res, rd[2]);
}
}
return res;
}
};

1971. 寻找图中是否存在路径

有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0n - 1(包含 0n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。

请你确定是否存在从顶点 source 开始,到顶点 destination 结束的 有效路径

给你数组 edges 和整数 nsourcedestination,如果从 sourcedestination 存在 有效路径 ,则返回 true,否则返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
public:
int parent[200001];

void init() {
for (int i = 0; i < 200001; i++) {
parent[i] = i;
}
}

int findParent(int u) {
return u == parent[u] ? u : (parent[u] = findParent(parent[u]));
}

bool isSame(int u, int v) {
u = findParent(u);
v = findParent(v);
return u == v;
}

void join(int u, int v) {
u = findParent(u);
v = findParent(v);
if (u == v) {
return;
}
parent[u] = v;
}

bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
init();
for (auto edge:edges) {
join(edge[0], edge[1]);
}
// for(int i = 0; i < edges.size(); i++) {
// join(edges[i][0], edges[i][1]);
// }
return isSame(source, destination);
}
};

684. 冗余连接

树可以看成是一个连通且 无环无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edgesedges[i] = [ai, bi] 表示图中在 aibi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的那个。

大致思路就是后面的加入得边的两个端点不能是相同的祖先。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
int parent[1001];
void init() {
for (int i = 0; i < 1001; i++) {
parent[i] = i;
}
}

int findParent(int u) {
return u == parent[u] ? u : (parent[u] = findParent(parent[u]));
}

bool isSame(int u, int v) {
u = findParent(u);
v = findParent(v);
return u == v;
}

void join(int u, int v) {
u = findParent(u);
v = findParent(v);
parent[u] = v;
}

vector<int> findRedundantConnection(vector<vector<int>>& edges) {
init();
for (auto edge:edges) {
if (isSame(edge[0], edge[1])) {
return edge;
}
join(edge[0], edge[1]);
}
return {0, 0};
}
};

685. 冗余连接 II

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1n)的树及一条附加的有向边构成。附加的边包含在 1n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 uivi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

三种情况:

  • 出现入度为2的节点

    img

  • 存在有向环
    img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class Solution {
public:
int parent[1001];
void init() {
for(int i = 0; i < 1001; i++) {
parent[i] = i;
}
}

int findParent (int u) {
return u == parent[u] ? u : (parent[u] = findParent(parent[u]));
}
bool isSame (int u, int v) {
u = findParent(u);
v = findParent(v);
return u == v;
}
void join (int u, int v) {
u = findParent(u);
v = findParent(v);
// u是v的根节点;
parent[v] = u;
}

bool isTreeRemove (vector<vector<int>> &edges, vector<int> removeEdge) {
init();
for (auto edge:edges) {
// if (edge[0] == removeEdge[0] && edge[1] == removeEdge[1]) {
if(edge == removeEdge) {
continue;
}
if (isSame(edge[0], edge[1])) {
return false;
}
join(edge[0], edge[1]);
}
return true;
}

vector<int> getRemove (vector<vector<int>> &edges) {
init();
for (auto edge:edges) {
if (isSame(edge[0], edge[1])) {
return edge;
}
join(edge[0], edge[1]);
}
return {0, 0};
}

vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int inDegree[1001];
stack <vector<int>> S; // 保存入度大于2的节点
for (auto edge:edges) {
inDegree[edge[1]] ++;
}
for (auto edge:edges) {
if (inDegree[edge[1]] == 2) {
S.push(edge);
}

}
// 存在入度为2的节点
if (!S.empty()) {
if (isTreeRemove(edges, S.top())) {
return S.top();
}
else {
S.pop();
return S.top();
}
}
// 存在有向环
return getRemove(edges);
}
};class Solution {
public:
int parent[1001];
void init() {
for(int i = 0; i < 1001; i++) {
parent[i] = i;
}
}

int findParent (int u) {
return u == parent[u] ? u : (parent[u] = findParent(parent[u]));
}
bool isSame (int u, int v) {
u = findParent(u);
v = findParent(v);
return u == v;
}
void join (int u, int v) {
u = findParent(u);
v = findParent(v);
// u是v的根节点;
parent[v] = u;
}

bool isTreeRemove (vector<vector<int>> &edges, vector<int> removeEdge) {
init();
for (auto edge:edges) {
// if (edge[0] == removeEdge[0] && edge[1] == removeEdge[1]) {
if(edge == removeEdge) {
continue;
}
if (isSame(edge[0], edge[1])) {
return false;
}
join(edge[0], edge[1]);
}
return true;
}

vector<int> getRemove (vector<vector<int>> &edges) {
init();
for (auto edge:edges) {
if (isSame(edge[0], edge[1])) {
return edge;
}
join(edge[0], edge[1]);
}
return {0, 0};
}

vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int inDegree[1001];
stack <vector<int>> S; // 保存入度大于2的节点
for (auto edge:edges) {
inDegree[edge[1]] ++;
}
for (auto edge:edges) {
if (inDegree[edge[1]] == 2) {
S.push(edge);
}

}
// 存在入度为2的节点
if (!S.empty()) {
if (isTreeRemove(edges, S.top())) {
return S.top();
}
else {
S.pop();
return S.top();
}
}
// 存在有向环
return getRemove(edges);
}
};

增补——热题100

49. 字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> output;
unordered_map <string, vector<string>> hashMap;
vector<int> count(26);
for (int i = 0; i < strs.size(); i++) {
string str = strs[i];
sort(str.begin(), str.end());
hashMap[str].emplace_back(strs[i]);
}
for(auto str:hashMap) {
output.emplace_back(str.second);
}
return output;
}
};

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

  • 怎么判断呢,就是用哈希表查找这个数前面一个数是否存在,即num-1在序列中是否存在。存在那这个数肯定不是开头,直接跳过。
  • 因此只需要对每个开头的数进行循环,直到这个序列不再连续,因此复杂度是O(n)。 以题解中的序列举例:
    [100,4,200,1,3,4,2]
    去重后的哈希序列为:[100,4,200,1,3,2]

——力扣题解评论

我写的和题解反的,但是结果都一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> hashSet;
// 先去重
for(auto num:nums) {
hashSet.insert(num);
}
int output = 0;
for(auto num:hashSet) {
// 要防止后续的数字继续遍历,所以要确保开始遍历一定是结尾!!!题解的写法是保一定是开头
if (hashSet.find(num + 1) == hashSet.end()) {
int count = 1;
int cur = num;
while (hashSet.find(cur - 1) != hashSet.end()) {
// cout << num << " " << cur << " " << count << endl;
count++;
cur--;
}
output = max(output, count);
}
}
return output;
}
};

283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0;
int right = 0;
for (int right = 0; right < nums.size(); right++) {
if (nums[right] != 0 ) {
nums[left++] = nums[right];
}
}
for (right = 0; left < nums.size(); left++) {
nums[left] = 0;
}
}
};

11. 盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

**说明:**你不能倾斜容器。

解法一——单调栈

很慢,能过

单调栈,栈内元素从小到大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxArea(vector<int>& height) {
vector <int> increasingVector;
increasingVector.emplace_back(0);
int output = 0;
for (int i = 1; i < height.size(); i++) {
for (int j = 0; j < increasingVector.size(); j++) {
int temp = min(height[i], height[increasingVector[j]]) * (i - increasingVector[j]);
output = max(temp, output);
}
if (height[i] > height[increasingVector[increasingVector.size() - 1]]) {
increasingVector.emplace_back(i);
}
}
return output;
}
};

解法二——双指针

左右两个指针总是移动较小的那个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int maxArea(vector<int>& height) {
int output = 0;
int left = 0;
int right = height.size() - 1;
int h;
while(left < right) {
if (height[left] < height[right]) {
h = height[left];
left++;
}
else {
h = height[right];
right--;
}
int temp = (right - left + 1) * h;
output = max(temp, output);
}
return output;
}
};

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

还以为是KMP算法。不需要KMP,使用滑动窗口就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if (s.length() <= 1) {
return s.length();
}
vector<bool> hashMap(128, false);
int left = 0;
hashMap[s[left] - ' '] = true;
int output = 1;
for (int right = 1; right < s.length(); right++) {
if (hashMap[s[right] - ' ']) { // 之前存过了
while (s[left] != s[right]) {
hashMap[s[left] - ' '] = false;
left++;
}
// 此时正好检测到s[left] == s[right], 哈希表不需要动
left++;
}
else {
hashMap[s[right] - ' '] = true;
}
// cout << right << " " << left << " " << endl;
output = max(output, right - left + 1);
}
return output;
}
};

438. 找到字符串中所有字母异位词

给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)

想复杂了!滑动窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> output;
if (s.length() < p.length()) {
return output;
}
vector<int> sCount(26);
vector<int> pCount(26);

for(int i = 0; i < p.length(); i++) {
sCount[s[i] - 'a'] ++;
pCount[p[i] - 'a'] ++;
}

if (sCount == pCount) {
output.emplace_back(0);
}

for (int j = 0; j < s.length() - p.length(); j++) {
sCount[s[j] - 'a'] --;
sCount[s[j + p.length()] - 'a'] ++ ;
if (sCount == pCount) {
output.emplace_back(j + 1);
}
}
return output;
}
};

560. 和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

连续,那好像好办一点!

解法一——暴力解法,但是超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int output = 0;
vector<int> preSum(nums.size()); // 记录到当前位置之前能够组成的数字大小
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j <= i; j++) {
preSum[j] += nums[i];
if (preSum[j] == k) {
output++;
}
}
}
return output;
}
};

解法二——前缀和建表

定义pre[i]表示[0,,i][0,\dots, i]的和,那么pre[i]可以有pre[i-1]递推得到,pre[i] = pre[i-1] + nums[i]。要使得[j,,i][j,\dots, i]的和为kkpre[j-1] + k == pre[i]。也就是说到i位置时候,要找和为k的子序列,只需要找到之前和为pre[i]-k的子序列有多少个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> preSum;
preSum[0] = 1;
int output = 0;
int pre = 0;
for (auto num:nums) {
pre += num;
output += preSum[pre - k];
preSum[pre] ++;
}
return output;
}
};

76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

很凌乱的做法,5555

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Solution {
public:
string minWindow(string s, string t) {
string output;

vector<int> sCount(100); // 计数用
vector<int> tCount(100); // 计数用
vector<bool> originMap(100, false);
vector<bool> tMap(100, false); // 判断对应字母有没有满足
vector<int> leftRight(2);

if (s.length() < t.length()) {
return output;
}
for (int i = 0; i < t.length(); i++) {
tCount[t[i] - 'A'] ++;
tMap[t[i] - 'A'] = true;
}
int left = 0;
int right = 0;
int len = INT_MAX;
for (right = 0; right < s.length(); right++) {
if (tCount[s[right] - 'A'] != 0) {
sCount[s[right] - 'A'] ++;
// cout << right << " " << sCount[s[right] - 'A'] << " " << tCount[s[right] - 'A'] << endl;
if (sCount[s[right] - 'A'] >= tCount[s[right] - 'A']) {
tMap[s[right] - 'A'] = false;
}
// 如果都清空了
if (tMap == originMap) {
// cout << "C" << " ";
// 先让left右移,看看能右移到什么程度还能满足要求
while (tMap == originMap && left < right) {
// cout << "B" << " ";
if (tCount[s[left] - 'A'] == 0) {
left++;
continue;
}
else {
if (sCount[s[left] - 'A'] > tCount[s[left] - 'A']) {
sCount[s[left] - 'A'] --;
left++;
// cout << "A" << " ";
}
else {
break;
}
}
}
// 全false,说明满足
if (right - left + 1 < len) {
leftRight[0] = left;
leftRight[1] = right;
len = right - left + 1;
}
}
}

}
if (len == INT_MAX) {
return "";
}
else {
return s.substr(leftRight[0], len);
}

}
};

189. 轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

解法一——队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
void rotate(vector<int>& nums, int k) {
queue <int> Q;
k = k % nums.size();

for (int i = 0; i < k; i++) {
Q.push(nums[i]);
}
for (int i = k; i < nums.size() + k; i++) {
Q.push(nums[i % nums.size()]);
nums[i % nums.size()] = Q.front();
Q.pop();
}
}
};

解法二——两次反转

类似卡码网55

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
void reverseVector(vector<int> & nums, int start, int end) {
while (start < end) {
swap(nums[start++],nums[end--]);
}
}
void rotate(vector<int>& nums, int k) {
reverseVector(nums, 0, nums.size() - 1);
k = k % nums.size();
reverseVector(nums, 0, k - 1);
reverseVector(nums, k, nums.size() - 1);
}
};

238. 除自身以外数组的乘积

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

请 **不要使用除法,**且在 O(n) 时间复杂度内完成此题。

不要使用除法咋做啊。注意本来除法也不行,有0的问题

解法一——左右乘积列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> answer(nums.size());
vector<int> left(nums.size(), 1);
vector<int> right(nums.size(), 1);
left[0] = nums[0];
right[nums.size() - 1] = nums[nums.size() - 1];
for (int i = 1; i < nums.size();i++) {
left[i] = left[i - 1] * nums[i];
right[nums.size() - 1 - i] = right[nums.size() - i] * nums[nums.size() - 1 - i];
}
answer[0] = right[1];
answer[nums.size() - 1] = left[nums.size() - 2];
for (int i = 1; i < nums.size() - 1;i++) {
answer[i] = left[i - 1] * right[i + 1];
}
return answer;
}
};

解法二——空间复杂度为O(1)\mathcal O(1)

右侧不用一个vector来存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> answer(nums.size(), 1);

for (int i = 1; i < nums.size(); i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}

int right = nums[nums.size() - 1];
for (int i = nums.size() - 2; i >= 0; i--) {
answer[i] *= right;
right *= nums[i];
}
return answer;
}
};

41. 缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

排序的时间复杂度是o(n2)o(n^2)啊,而且要没有额外的空间的话哈希表好像也不好使。

解法一——原始数组作为hash表

我们为什么要使用哈希表?这是因为哈希表是一个可以支持快速查找的数据结构:给定一个元素,我们可以在O(1) 的时间查找该元素是否在哈希表中。

对于一个长度为 NN 的数组,其中没有出现的最小正整数只能在 [1,N+1][1, N+1] 中。这是因为如果 [1,N][1, N] 都出现了,那么答案是 N+1N+1,否则答案是 [1,N][1, N]​ 中没有出现的最小正整数。

【怎么标记呢?】

  • 我们将数组中所有小于等于 0 的数修改为 N+1N+1
  • 我们遍历数组中的每一个数 xx,它可能已经被打了标记,因此原本对应的数为 x|x|,其中 |\,| 为绝对值符号。如果 x[1,N]|x| \in [1, N],那么我们给数组中的第 x1|x| - 1 个位置的数添加一个负号。注意如果它已经有负号,不需要重复添加;
  • 在遍历完成之后,如果数组中的每一个数都是负数,那么答案是 N+1N+1N+1,否则答案是第一个正数的位置加 111。

用下标的正负作为bool类型的hash表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++) {
if (nums[i] <= 0) {
nums[i] = nums.size() + 1; // 如果要出现N+1的情况,里面存的数字应该是1~N,所以这边要避开N
}
}
for (int i = 0; i < nums.size(); i++) {
if (abs(nums[i]) <= nums.size()) {
nums[abs(nums[i]) - 1] = - abs(nums[abs(nums[i]) - 1]); // 这里要防止反转两次
}
}
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) {
return i + 1;
}
}
return nums.size() + 1;
}
};

解法二——类似排序?直接交换到对应下标位置去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++) {
while (nums[i] >=1 && nums[i] <=nums.size() && nums[i] != nums[nums[i] - 1]) {
// 第三个条件防止死循环,另外这里是while!换回去的那个元素还得接着换
swap(nums[i], nums[nums[i] - 1]);
}
}

for (int i = 0; i < nums.size(); i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return nums.size() + 1;
}
};

73. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。**

解法一——记录行列

一个简单的思路,先遍历一遍记录哪些行列需要标记为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
vector<bool> row(matrix.size(), false);
vector<bool> column(matrix[0].size(), false);
for (int i = 0; i < matrix.size(); i++) {
for (int j = 0; j < matrix[0].size(); j++) {
if (matrix[i][j] == 0) {
row[i] = true;
column[j] = true;
}
}
}
for (int i = 0; i < matrix.size(); i++) {
for (int j = 0; j < matrix[0].size(); j++) {
if (row[i] == true || column[j] == true) {
matrix[i][j] = 0;
}
}
}
}
};

解法二——使用标记变量

这两个解法真的太离谱了,不想写

【两个标记变量】

我们可以用矩阵的第一行和第一列代替方法一中的两个标记数组,以达到 O(1) 的额外空间。但这样会导致原数组的第一行和第一列被修改,无法记录它们是否原本包含 0。因此我们需要额外使用两个标记变量分别记录第一行和第一列是否原本包含 0。

【一个标记变量】

这样,第一列的第一个元素即可以标记第一行是否出现 0。但为了防止每一列的第一个元素被提前更新,我们需要从最后一行开始,倒序地处理矩阵元素。

54. 螺旋矩阵

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

写的一坨shit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> output;
int m = matrix.size();
int n = matrix[0].size();
int length = m * n;
int x = 0;
int y = 0;
int count = n;
int dir[4][2] = {0,1,1,0,0,-1,-1,0}; // 01右;10下;0-1左;-10上
int countDir = 0;

for (int i = 0; i < length; i++) {
output.emplace_back(matrix[x][y]);
// cout << count << " || ";
if(count <= 1) { // count = 1 换方向
countDir = (countDir + 1) % 4;
m -= abs(dir[countDir][0]);
n -= abs(dir[countDir][1]);
count = abs(dir[countDir][0]) * m + abs(dir[countDir][1]) * n + 1;
}
// cout << i <<" " <<count << " || " << x << " " << y << " || " << countDir << " " << dir[countDir][0] << " " << dir[countDir][1] << endl;
x += dir[countDir][0];
y += dir[countDir][1];
count --;
}
return output;
}
};

48. 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

解法一——旋转矩阵的思路,一圈一圈来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
int length = n;
int dir[4][2] = {0,1,1,0,0,-1,-1,0};
queue <int> Q;
int x, y;
while(length >= 1) {
Q = queue<int> (); // 记得要清空
y = (n - length) / 2;
x = n - (n - length) / 2 - 1;
// cout << x << " " << y << endl;

for (int i = 0; i < length - 1; i++) { // 往上
Q.push(matrix[x][y]);
x += dir[3][0];
y += dir[3][1];
// cout << x << " " << y << endl;
}

//绕圈
for (int i = 0; i < (length - 1) * 4; i ++) {
// length-1为一组
Q.push(matrix[x][y]);
matrix[x][y] = Q.front();
Q.pop();
x += dir[i/(length - 1)][0];
y += dir[i/(length - 1)][1];
// cout << x << " " << y << endl;
}
length -= 2;
}
}
};

也可以不用这么多空间,点对点来旋转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n/2; i++) { // 一共n/2圈向下取整
for (int j = i; j < n - i - 1; j++) {
int temp = matrix[i][j];
// 对应的四个位置坐标分别为
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
};

解法二——用翻转代替旋转

旋转90°实际上就是线上下翻转,在主对角线反转。

image-20240527170107807

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 上下翻转
for(int i = 0; i < n / 2; i++) {
for (int j = 0; j < n; j++) {
swap(matrix[i][j], matrix[n - i - 1][j]);
}
}
// 主对角线反转
for (int i = 0; i < n; i++) {
for(int j = 0; j < i; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
};

240. 搜索二维矩阵 II

编写一个高效的算法来搜索 m × n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

【Z 字形查找】

我们可以从矩阵 matrix\textit{matrix} 的右上角 (0,n1)(0, n-1)进行搜索。在每一步的搜索过程中,如果我们位于位置(x,y)(x, y),那么我们希望在以 matrix\textit{matrix} 的左下角为左下角、以(x,y)(x, y) 为右上角的矩阵中进行搜索,即行的范围为 [x,m1][x, m - 1],列的范围为 [0,y][0, y]

  • 如果 matrix[x,y]=target\textit{matrix}[x, y] = \textit{target},说明搜索完成;

  • 如果 matrix[x,y]>target\textit{matrix}[x, y] > \textit{target},由于每一列的元素都是升序排列的,那么在当前的搜索矩阵中,所有位于第 yy 列的元素都是严格大于 target\textit{target} 的,因此我们可以将它们全部忽略,即将 yy 减少 1;

  • 如果 matrix[x,y]<target\textit{matrix}[x, y] < \textit{target},由于每一行的元素都是升序排列的,那么在当前的搜索矩阵中,所有位于第 xxx 行的元素都是严格小于target\textit{target} 的,因此我们可以将它们全部忽略,即将 xx 增加 1。

作者:力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int x = 0, y = n - 1;
while(x < m && y >= 0) {
if(matrix[x][y] == target) {
return true;
}
if (matrix[x][y] > target) {
y--;
}
else{
x++;
}
}
return false;
}
};

234. 回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

回文 序列是向前和向后读都相同的序列。

解法一——栈保存数值

空间复杂度O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
stack<int> S;
ListNode *p = head;
while (p != NULL) {
S.push(p->val);
p = p->next;
}
p = head;
while (p != NULL) {
if(p->val != S.top()) {
return false;
}
S.pop();
p = p->next;
}
return true;
}
};

解法二——快慢指针反转后面的链表

  1. 找到前半部分链表的尾节点。
  2. 反转后半部分链表。
  3. 判断是否回文。
  4. 恢复链表。
  5. 返回结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (head == nullptr) {
return true;
}
ListNode *p = head;
ListNode *q = head;
while (q->next != nullptr && q->next->next != nullptr) {
p = p->next;
q = q->next->next;
}
// 反转链表
ListNode *p1 = nullptr;
ListNode *q1 = p->next;
while (q1 != nullptr) {
ListNode * temp = q1->next;
q1->next = p1;
p1 = q1;
q1 = temp;
}
// ListNode *r = head;
// while (r != nullptr) {
// cout << r->val;
// r = r->next;
// }
// 比较
q = head;
p = p1;
while (p != nullptr) {
if (p->val != q->val) {
return false;
}
p = p->next;
q = q->next;
}
return true;
}
};

141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

快慢指针,能够重合就是有环,快指针走到头了就是没环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if (head == nullptr) {
return false;
}
ListNode *slow = head;
ListNode *fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
if(fast == slow) {
return true;
}
}
return false;
}
};

21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* p1 = list1;
ListNode *p2 = list2;
if (list1 == nullptr ) {
return list2;
}
else if (list2 == nullptr) {
return list1;
}
ListNode *p;
if (list1->val < list2->val) {
p= p1;
p1 = p1->next;
}
else {
p = p2;
p2 = p2->next;
}
ListNode *head = p;
while (p1 != nullptr && p2 != nullptr) {
if (p1->val >= p2->val) {
p->next = p2;
p = p->next;
p2 = p2->next;
}
else {
p->next = p1;
p = p->next;
p1 = p1->next;
}
}
if (p1 == nullptr) {
p->next = p2;
}
else {
p->next = p1;
}
return head;
}
};

2. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

img

进位标志(Carry Flag, CF)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
if(l1 == nullptr) {
return l2;
}
else if (l2 == nullptr){
return l1;
}
ListNode *p1 = l1;
ListNode *p2 = l2;
bool CF = false;
p1->val = p1->val + p2->val;
if(p1->val >= 10) {
p1->val -= 10;
CF = true;
}
while (p1->next != nullptr || p2->next != nullptr) {
if (p1->next == nullptr) {
p1->next = new ListNode(0);
}
else if (p2->next == nullptr) {
p2->next = new ListNode(0);
}
p1 = p1->next;
p2 = p2->next;
p1->val = p1->val + p2->val;
if (CF) {
p1->val += 1;
}
if(p1->val >= 10) {
p1->val -= 10;
CF = true;
}
else {
CF = false;
}
}
if (CF) {
p1->next = new ListNode(1);
}
return l1;
}
};

25. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
img

img

  • 双指针

hair–dummyHead

pre-slow

tail–fast

nex–fast->next

绕也绕不清

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode *dummyHead = new ListNode (0, head);
ListNode *slow = dummyHead;
ListNode *fast = dummyHead;
// bool isEnd = false;
int i;
ListNode *pNext ;
while (fast != nullptr) {
for (i = 0; i < k && fast != nullptr; i++) {
fast = fast->next;
}
if (fast == nullptr) {
return dummyHead->next;
}
// cout << fast ->val << endl;
pNext = fast->next;
//反转链表
ListNode *p = slow;
ListNode *q = slow->next;
while (q != pNext) { // 这个判断条件!!是pNext,不是fast,不然会导致最后一个fast对应位置没指回去,不用担心q已经跑到前面去了怎么办,还有p比他慢一步
ListNode *temp = q->next;
q->next = p;
p = q;
q = temp;
}
ListNode* tmp;
// 这边往下是真的很难想清。可以想象第一次
slow->next->next = pNext; // 这是让原来的head的下一个指向下一次的起点,而非dummyHead
// cout<< slow->next->val << endl;
// cout << pNext->val << endl;
tmp = slow->next;
// cout << tmp->val << endl;
slow->next = p; // 这里用p就好了,这个是为了让dummyHead指向新的head
// cout << slow->next->val << endl;
slow = tmp;
fast = tmp;
// ListNode *qqq = dummyHead;
// while(qqq->next != NULL) {
// cout << qqq->next->val << " ";
// qqq = qqq->next;
// }
// cout << endl;
}
return dummyHead->next;


}
};

再贴一个官方题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
public:
// 翻转一个子链表,并且返回新的头与尾
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
ListNode* prev = tail->next;
ListNode* p = head;
while (prev != tail) {
ListNode* nex = p->next;
p->next = prev;
prev = p;
p = nex;
}
return {tail, head};
}

ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* pre = hair;

while (head) {
ListNode* tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail->next;
if (!tail) {
return hair->next;
}
}
ListNode* nex = tail->next;
// 这里是 C++17 的写法,也可以写成
// pair<ListNode*, ListNode*> result = myReverse(head, tail);
// head = result.first;
// tail = result.second;
tie(head, tail) = myReverse(head, tail);
// 把子链表重新接回原链表
pre->next = head;
tail->next = nex;
pre = tail;
head = tail->next;
}

return hair->next;
}
};

138. 随机链表的复制

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点

例如,如果原链表中有 XY 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 xy ,同样有 x.random --> y

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0n-1);如果不指向任何节点,则为 null

你的代码 接受原链表的头节点 head 作为传入参数。

解法一——回溯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;

Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/

class Solution {
public:
unordered_map<Node*, Node*> connectedNode;
Node* copyRandomList(Node* head) {
if (head == nullptr) {
return nullptr;
}
if (!connectedNode.count(head)) {
Node* newHead = new Node(head->val);
connectedNode[head] = newHead;
newHead->next = copyRandomList(head->next);
newHead->random = copyRandomList(head->random);
}
return connectedNode[head];
}
};

解法二——迭代 + 节点拆分

注意到方法一需要使用哈希表记录每一个节点对应新节点的创建情况,而我们可以使用一个小技巧来省去哈希表的空间。

我们首先将该链表中每一个节点拆分为两个相连的节点,例如对于链表 A→B→C,我们可以将其拆分为 A→A′→B→B′→C→C′ 。对于任意一个原节点 S,其拷贝节点 S′ 即为其后继节点。

——力扣官方题解
img

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
Node* copyRandomList(Node* head) {
if (head == nullptr) {
return nullptr;
}
// 新节点连进来
for (Node* p = head; p != nullptr; p = p->next->next) {
Node* newP = new Node(p->val);
newP->next = p->next;
p->next = newP;
}
// 新节点的指针域
for (Node* p = head; p != nullptr; p = p->next->next) {
Node* newP = p->next;
if (p->random != nullptr) {
newP->random = p->random->next;
}
else {
newP = nullptr;
}
}
//断开
Node* newHead = head->next;
for (Node* p = head; p != nullptr; p = p->next) {
Node* newP = p->next;
p->next = p->next->next;
if (newP->next != nullptr) {
newP->next = newP->next->next;
}
}
return newHead;
}
};

148. 排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

对链表自顶向下归并排序的过程如下。

找到链表的中点,以中点为分界,将链表拆分成两个子链表。寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2 步,慢指针每次移动 1 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。

  1. 对两个子链表分别排序。
  2. 将两个排序后的子链表合并,得到完整的排序后的链表。
  3. 上述过程可以通过递归实现。递归的终止条件是链表的节点个数小于或等于 1,即当链表为空或者链表只包含 1 个节点时,不需要对链表进行拆分和排序。

作者:力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:

ListNode* sortList(ListNode* head) {
return sortTwoList (head, nullptr);
}

ListNode* sortTwoList(ListNode* head, ListNode* tail) {
if (head == nullptr) { // 链表为空
return head;
}
if (head->next == tail) { // 只剩一个元素
head->next = nullptr;
return head;
}
ListNode* slow = head;
ListNode* fast = head;
while (fast != tail) {
slow = slow->next;
fast = fast->next;
if (fast != tail) {
fast = fast->next;
}
}
ListNode* mid = slow;
return mergeTwoList(sortTwoList(head, mid), sortTwoList(mid, tail));
}

ListNode *mergeTwoList(ListNode* head1, ListNode* head2) {
if (head1 == nullptr) {
return head2;
}
else if (head2 == nullptr) {
return head1;
}
ListNode *p1 = head1;
ListNode *p2 = head2;

ListNode *p;
if (head1->val < head2->val) {
p = p1;
p1 = p1->next;
}
else {
p = p2;
p2 = p2->next;
}
ListNode * head = p;
while (p1 != nullptr && p2 != nullptr) {
if (p1->val < p2->val) {
p->next = p1;
p = p->next;
p1 = p1->next;
}
else{
p->next = p2;
p = p->next;
p2 = p2->next;
}
}
if (p1 == nullptr) {
p->next = p2;
}
else {
p->next = p1;
}
return head;
}
};

23. 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

最naive的思想——存起来呗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
int findMin(vector<int> minNum) {
int tempMin = INT_MAX;
int tempMinPos = -1;
for (int i = 0; i < minNum.size(); i++) {
if (minNum[i] < tempMin) {
tempMin = minNum[i];
tempMinPos = i;
}
}
return tempMinPos;
}

ListNode* mergeKLists(vector<ListNode*>& lists) {
int K = lists.size();

if (K == 0) {
return nullptr;
}

vector<int> minNum(K);

vector<ListNode*> p;
int headIndex = -1;
int headNum = INT_MAX;
for(int i = 0; i < K; i++) {
p.emplace_back(lists[i]);
if (lists[i] != nullptr) {
minNum[i] = lists[i]->val;
if (headIndex == -1 || headNum > minNum[i]) {
headIndex = i;
headNum = minNum[i];
}
}
else {
minNum[i] = INT_MAX;
}
}
if (headIndex == -1) {
return nullptr;
}
ListNode* head = p[headIndex];
p[headIndex] = p[headIndex]->next;
if (p[headIndex] != nullptr) {
minNum[headIndex] = p[headIndex]->val;
}
else {
minNum[headIndex] = INT_MAX;
}
// 在minNum中最小值,直到最小值为INT_MAX
ListNode* temp = head;
int minPos = findMin(minNum);
while (minPos != -1) {
temp->next = p[minPos];
temp = temp->next;
p[minPos] = p[minPos]->next;
if (p[minPos] != nullptr) {
minNum[minPos] = p[minPos]->val;
}
else {
minNum[minPos] = INT_MAX;
}
minPos = findMin(minNum);
}

return head;
}
};

分治合并

img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
return merge(lists, 0, lists.size() - 1);
}

ListNode* merge(vector <ListNode*> &lists, int left, int right) {
if (left == right) {
return lists[left];
}
if (left > right) {
return nullptr;
}
int mid = (left + right) / 2;
return mergeTwoLists(merge(lists, left, mid), merge(lists, mid + 1, right));
}

ListNode* mergeTwoLists(ListNode* head1, ListNode* head2) {
if((!head1) || (!head2)) {
return head1 ? head1 : head2;
}
ListNode *p1 = head1;
ListNode *p2 = head2;

ListNode *p;
if (head1->val < head2->val) {
p = p1;
p1 = p1->next;
}
else {
p = p2;
p2 = p2->next;
}
ListNode * head = p;
while (p1 != nullptr && p2 != nullptr) {
if (p1->val < p2->val) {
p->next = p1;
p = p->next;
p1 = p1->next;
}
else{
p->next = p2;
p = p->next;
p2 = p2->next;
}
}
if (p1 == nullptr) {
p->next = p2;
}
else {
p->next = p1;
}
return head;
}
};

146. LRU 缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
  • 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:

  • 对于 get 操作,首先判断 key 是否存在:

    • 如果 key 不存在,则返回 −1;

    • 如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

  • 对于 put 操作,首先判断 key 是否存在:

    • 如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
    • 如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

——力扣官方题解

想不到双向链表,纯粹抄——主要要想到双向链表,这样比较好处理头尾的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
struct DLinkedNode {
int key, value;
DLinkedNode *prev;
DLinkedNode *next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};


class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacityA;

public:
LRUCache(int capacity) {
capacityA = capacity;
size = 0;
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}

int get(int key) {
if (!cache.count(key)) { // key不存在
return -1;
}
else {
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
}

void put(int key, int value) {
//如果不存在
if (!cache.count(key)) {
DLinkedNode* node = new DLinkedNode(key, value);
cache[key] = node;
addToHead(node);
size++;
if (size > capacityA) {
// 超出容量
DLinkedNode *removed = removeTail();
cache.erase(removed->key);
delete removed;
size--;
}
}
else {
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}

void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}

void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}

void addToHead(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}

DLinkedNode* removeTail() {
DLinkedNode *node = tail->prev;
removeNode(node);
return node;
}
};

/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/

543. 二叉树的直径

给你一棵二叉树的根节点,返回该树的 直径

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root

两节点之间路径的 长度 由它们之间边数表示。

每个节点的直径为(左子树深度+右子树深度)+1,而他的父节点的节点深度等于max(左子树深度,右子树深度)+1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int diameter;

int depth(TreeNode* p) {
if (p == NULL) {
return 0;
}
int depthL = depth(p->left);
int depthR = depth(p->right);
diameter = max(diameter, depthL + depthR + 1);
return max(depthL, depthR) + 1;
}

int diameterOfBinaryTree(TreeNode* root) {
diameter = 1;
depth(root);
return diameter - 1;
}
};

230. 二叉搜索树中第 K 小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。

二叉搜索树的中序排列是有序的——中序排列即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
stack <TreeNode*> S;
TreeNode *p = root;
int count = 0;
while (p || !S.empty()){
if (p != nullptr) {
S.push(p);
p = p->left;
}
else{
count++;
p = S.top();
// cout << count << " ";
if (count == k) {
return p->val;
}
S.pop();
p = p->right;
}
}
return 0;
}
};

114. 二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

解法一——遍历+一个数组(非原地)

不知道为啥我写的超出内存限制,他写的就不超出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
void flatten(TreeNode* root) {
// TreeNode *dummyHead = new TreeNode(0, nullptr, root);
// dummyHead->right = root;
// TreeNode* p = dummyHead->right;
stack<TreeNode*> S;
vector<TreeNode*> V;
if (root == nullptr) {
return ;
}
S.push(root);
// V.emplace_back(dummyHead);
while(!S.empty()) {
TreeNode *p = S.top();
V.emplace_back(p);
if (p->right) {
S.push(p->right);
}
if (p->left) {
S.push(p->left);
}
}
// TreeNode *q = V[0];
for(int i = 1; i < V.size(); i++) {
TreeNode *prev = V[i - 1];
TreeNode *curr = V[i];
prev->right = curr;
prev->left = nullptr;
// q = Q.front();
// Q.pop();
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
void flatten(TreeNode* root) {
auto v = vector<TreeNode*>();
auto stk = stack<TreeNode*>();
TreeNode *node = root;
while (node != nullptr || !stk.empty()) {
while (node != nullptr) {
v.push_back(node);
stk.push(node);
node = node->left;
}
node = stk.top(); stk.pop();
node = node->right;
}
int size = v.size();
for (int i = 1; i < size; i++) {
auto prev = v.at(i - 1), curr = v.at(i);
prev->left = nullptr;
prev->right = curr;
}
}
};

chatgpt的分析结果:你在第一个代码中同时使用了栈(stack<TreeNode*> S)和向量(vector<TreeNode*> V)。栈用于深度优先遍历,而向量用于存储访问顺序,这使得你在遍历每个节点时需要额外存储两次节点信息。这会增加内存使用,特别是在树很大的时候。

第二个代码将遍历结果直接存入向量,而栈只用于辅助遍历,这样减少了节点的重复存储。

解法二——寻找前驱节点

注意到前序遍历访问各节点的顺序是根节点、左子树、右子树。如果一个节点的左子节点为空,则该节点不需要进行展开操作。如果一个节点的左子节点不为空,则该节点的左子树中的最后一个节点被访问之后,该节点的右子节点被访问。该节点的左子树中最后一个被访问的节点是左子树中的最右边的节点,也是该节点的前驱节点。因此,问题转化成寻找当前节点的前驱节点。

具体做法是,对于当前节点,如果其左子节点不为空,则在其左子树中找到最右边的节点,作为前驱节点,将当前节点的右子节点赋给前驱节点的右子节点,然后将当前节点的左子节点赋给当前节点的右子节点,并将当前节点的左子节点设为空。对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束。

作者:力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void flatten(TreeNode* root) {
if (!root) {
return;
}
subflatten(root);
}
TreeNode* subflatten(TreeNode* p) {
TreeNode* left = p->left;
TreeNode* right = p->right;
TreeNode* last = p;
p->left = nullptr;
if (left != nullptr) {
p->right = left;
last = subflatten(left);
}
if (right != nullptr) {
last->right = right; // 注意这里是last的右孩子
last = subflatten(right);
}
return last; // 链表的最后一位
}
};

437. 路径总和 III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

迭代隐式回溯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int rootSum(TreeNode* root, long targetSum) {
if (root == nullptr) {
return 0;
}
int output = 0;
if (root->val == targetSum) {
output++;
}
output += rootSum(root->left, targetSum - root->val);
output += rootSum(root->right, targetSum - root->val);
return output;
}
int pathSum(TreeNode* root, int targetSum) {
if (root == nullptr) {
return 0;
}

int output = rootSum(root, targetSum);
output += pathSum(root->left, targetSum);
output += pathSum(root->right, targetSum);
return output;
}
};

124. 二叉树中的最大路径和

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和

思路和“[543. 二叉树的直径]”类似,但是注意这里需要考虑当贡献大于0时候在加上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int pathSum;
int maxPath(TreeNode * p) {
if (p == nullptr) {
return 0;
}
int leftSum = maxPath(p->left);
int rightSum = maxPath(p->right);


// 注意下列和直径的区别,需要考虑贡献度
// int leftSum = maxPath(p->left);
// int rightSum = maxPath(p->right);
// pathSum = max(pathSum, max(leftSum + rightSum + p->val, max(rightSum + p->val, max(leftSum + p->val, p->val))));
// return max(max(leftSum, 0), max(rightSum, 0)) + p->val;

int leftSum = max(maxPath(p->left), 0);
int rightSum = max(maxPath(p->right), 0);

pathSum = max(pathSum, leftSum + rightSum + p->val);
return max(leftSum, rightSum) + p->val;

}

int maxPathSum(TreeNode* root) {
pathSum = root->val;
maxPath(root);
return pathSum;
}
};

994. 腐烂的橘子

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

  • 0 代表空单元格;
  • 1 代表新鲜橘子;
  • 2 代表腐烂的橘子。

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1

BFS

碰到1不动,碰到2开始广搜。

注意对于如下的例子,两头一起烂才是最快的

1
2
3
[[2,1,1],
[1,1,1],
[0,1,2]]

所以不能只考虑每个点的,然后再找最小的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
int bfs (vector<vector<int>>& grid, int x, int y) {
int time = 0;
queue<pair<int, int>> Q;
vector<vector<bool>> visited(grid.size(), vector<bool>(grid[0].size(), false));
visited[x][y] = true;
Q.push({x, y});

while (!Q.empty()) {
int currLoopNum = Q.size();
for (int i = 0; i < currLoopNum; i++) {
pair<int, int> curr = Q.front();
Q.pop();
int xCurr = curr.first;
int yCurr = curr.second;
for (int i = 0; i < 4; i++) {
int xNext = xCurr + dir[i][0];
int yNext = yCurr + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size() || grid[xNext][yNext] == 2) { // 2说明就是中心烂橘子,不需要重新计算
continue;
}
if ((grid[xNext][yNext] == 1 || grid[xNext][yNext] == 3) && visited[xNext][yNext] == false) {
Q.push({xNext, yNext});
visited[xNext][yNext] = true;
grid[xNext][yNext] = 3;
// cout << xNext << " " << yNext;
}
}
}
time ++;
// cout << time << endl;
}
// cout << "A";
return time - 1;
}

int orangesRotting(vector<vector<int>>& grid) {
int row = grid.size();
int column = grid[0].size();
int time = 0;
queue<pair<int, int>> Q;
bool flagOne = false;

for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
if (grid[i][j] == 2) {
Q.push({i, j});
}
if (grid[i][j] == 1) {
flagOne = true;
}
}
}
if (Q.size() == 0 && flagOne == false) { // 考虑没有烂橘子
return 0;
}

while (!Q.empty()) {
int currLoopNum = Q.size();
for (int i = 0; i < currLoopNum; i++) {
pair<int, int> curr = Q.front();
Q.pop();
int xCurr = curr.first;
int yCurr = curr.second;
for (int i = 0; i < 4; i++) {
int xNext = xCurr + dir[i][0];
int yNext = yCurr + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= grid.size() || yNext >= grid[0].size() || grid[xNext][yNext] == 2) { // 2说明就是中心烂橘子,不需要重新计算
continue;
}
if (grid[xNext][yNext] == 1 ) {
Q.push({xNext, yNext});
grid[xNext][yNext] = 2;
// cout << xNext << " " << yNext;
}
}
}
time ++;
// cout << time << endl;
}

for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
if (grid[i][j] == 1) {
// cout << i << " " << j;
return -1;
}
}
}
return time - 1;
}
};

为了确认是否所有新鲜橘子都被腐烂,可以记录一个变量 cnt 表示当前网格中的新鲜橘子数,广度优先搜索的时候如果有新鲜橘子被腐烂,则 cnt=cnt−1 ,最后搜索结束时如果 cnt 大于 0 ,说明有新鲜橘子没被腐烂,返回 −1 ,否则返回所有新鲜橘子被腐烂的时间的最大值即可,也可以在广度优先搜索的过程中把已腐烂的新鲜橘子的值由 1 改为 2,最后看网格中是否有值为 1 即新鲜的橘子即可。

207. 课程表

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai必须 先学习课程 bi

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

不能成环?
结果答案是拓扑排序,太难了

image-20241006105916273

用一个栈来存储所有已经搜索完成的节点。假设我们当前搜索到了节点 u,如果它的所有相邻节点都已经搜索完成,那么这些节点都已经在栈中了,此时我们就可以把 u 入栈。可以发现,如果我们从栈顶往栈底的顺序看,由于 u 处于栈顶的位置,那么 u 出现在所有 u 的相邻节点的前面。因此对于 u 这个节点而言,它是满足拓扑排序的要求的。

方法一: 从入度思考(从前往后排序), 入度为0的节点在拓扑排序中一定排在前面, 然后删除和该节点对应的边, 迭代寻找入度为0的节点。

方法二: 从出度思考(从后往前排序), 出度为0的节点在拓扑排序中一定排在后面, 然后删除和该节点对应的边, 迭代寻找出度为0的节点。

深度优先——栈

对于图中的任意一个节点,它在搜索的过程中有三种状态,即:

  • 「未搜索」:我们还没有搜索到这个节点;

  • 「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成);

  • 「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。

通过上述的三种状态,我们就可以给出使用深度优先搜索得到拓扑排序的算法流程,在每一轮的搜索搜索开始时,我们任取一个「未搜索」的节点开始进行深度优先搜索。

  • 我们将当前搜索的节点 u 标记为「搜索中」,遍历该节点的每一个相邻节点 v:

    • 如果 v 为「未搜索」,那么我们开始搜索 v,待搜索完成回溯到 u;

    • 如果 v 为「搜索中」,那么我们就找到了图中的一个环,因此是不存在拓扑排序的;

    • 如果 v 为「已完成」,那么说明 v 已经在栈中了,而 u 还不在栈中,因此 u 无论何时入栈都不会影响到 (u,v) 之前的拓扑关系,以及不用进行任何操作。

  • 当 u 的所有相邻节点都为「已完成」时,我们将 u 放入栈中,并将其标记为「已完成」。

作者:力扣官方题解

image-20241006134104937

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
bool valid = true;

void dfs(vector<vector<int>>& edges, vector<int>& visited, int u) {
visited[u] = 1; // 1 搜索中
for (int v:edges[u]) {
if (visited[v] == 0) {
dfs(edges, visited, v);
if (valid == false) {
return;
}
}
if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2; // 搜索完成
}

bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> edges(numCourses);
vector<int> visited(numCourses, 0); // 0 未搜索

for (int i = 0; i < prerequisites.size(); i++) {
vector<int> prerequisite = prerequisites[i];
edges[prerequisite[1]].emplace_back(prerequisite[0]);
}
for (int i = 0; i < numCourses && valid; i++) {
if (visited[i] == 0) {
dfs(edges, visited, i);
}
}
return valid;
}
};

广度优先——队列

我们考虑拓扑排序中最前面的节点,该节点一定不会有任何入边,也就是它没有任何的先修课程要求。当我们将一个节点加入答案中后,我们就可以移除它的所有出边,代表着它的相邻节点少了一门先修课程的要求。如果某个相邻节点变成了「没有任何入边的节点」,那么就代表着这门课可以开始学习了。按照这样的流程,我们不断地将没有入边的节点加入答案,直到答案中包含所有的节点(得到了一种拓扑排序)或者不存在没有入边的节点(图中包含环)。

作者:力扣官方题解

image-20241006134142872

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> edges(numCourses);
vector<int> indeg(numCourses);

for (int i = 0; i < prerequisites.size(); i++) {
vector<int> prerequisite = prerequisites[i];
edges[prerequisite[1]].emplace_back(prerequisite[0]); // 1是0的先修课
indeg[prerequisite[0]] ++;
}

queue <int> Q;
for (int i = 0; i < numCourses; i++) {
if (indeg[i] == 0) { // 入度为0,加入队列删除边
Q.push(i);
}
}
int visited = 0;
while (!Q.empty()) {
visited++;
int u = Q.front();
Q.pop();
for (int v:edges[u]) {
indeg[v] --;
if (indeg[v] == 0) {
Q.push(v);
}
}
}
return visited == numCourses; // 如果没环,应该就都结束了
}
};

210. 课程表 II

现在你总共有 numCourses 门课需要选,记为 0numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai必须 先选修 bi

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1]

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
vector<int> output; // 数组模拟栈
bool valid = true;

void dfs(vector<vector<int>>& edges, vector<int>& visited, int u) {
visited[u] = 1; // 1 搜索中
for (int v:edges[u]) {
if (visited[v] == 0) {
dfs(edges, visited, v);
if (valid == false) {
return;
}
}
if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2; // 搜索完成
output.emplace_back(u);
}

vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> edges(numCourses);
vector<int> visited(numCourses, 0); // 0 未搜索

for (int i = 0; i < prerequisites.size(); i++) {
vector<int> prerequisite = prerequisites[i];
edges[prerequisite[1]].emplace_back(prerequisite[0]);
}
for (int i = 0; i < numCourses && valid; i++) {
if (visited[i] == 0) {
dfs(edges, visited, i);
}
}
if (!valid) {
return {};
}
reverse(output.begin(), output.end());
return output;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> edges(numCourses);
vector<int> indeg(numCourses);
vector<int> output;

for (int i = 0; i < prerequisites.size(); i++) {
vector<int> prerequisite = prerequisites[i];
edges[prerequisite[1]].emplace_back(prerequisite[0]); // 1是0的先修课
indeg[prerequisite[0]] ++;
}

queue <int> Q;
for (int i = 0; i < numCourses; i++) {
if (indeg[i] == 0) { // 入度为0,加入队列删除边
Q.push(i);
}
}
int visited = 0;
while (!Q.empty()) {
visited++;
int u = Q.front();
Q.pop();
output.emplace_back(u);
for (int v:edges[u]) {
indeg[v] --;
if (indeg[v] == 0) {
Q.push(v);
}
}
}
if ( visited == numCourses ) {
return output;
}
else {
return {};
}

}
};

208. 实现 Trie (前缀树)

Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false

Trie 是一颗非典型的多叉树模型,多叉好理解,即每个结点的分支数量可能为多个。非典型指的是节点设计不同。一次建树,多次查询

1
2
3
4
struct TrieNode {
bool isEnd; //该结点是否是一个串的结束
TrieNode* next[26]; //字母映射表
};

字母映射表next TrieNode* next[26]中保存了对当前结点而言下一个可能出现的所有字符的链接,因此我们可以通过一个父结点来预知它所有子结点的值.

  1. Trie 的形状和单词的插入或删除顺序无关,也就是说对于任意给定的一组单词,Trie 的形状都是唯一的。
  2. 查找或插入一个长度为 L 的单词,访问 next 数组的次数最多为 L+1,和 Trie 中包含多少个单词无关。
  3. Trie 的每个结点中都保留着一个字母表,这是很耗费空间的。如果 Trie 的高度为 n,字母表的大小为 m,最坏的情况是 Trie 中还不存在前缀相同的单词,那空间复杂度就为 O(mn)O(m^n)

作者:路漫漫我不畏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Trie {
public:
bool isEnd;
Trie* next[26];

Trie() {
isEnd = false;
memset(next, 0, sizeof(next));
}

void insert(string word) { // 这个操作和构建链表很像。首先从根结点的子结点开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。

Trie* node = this;
for (auto c:word) {
if (node->next[c - 'a'] == NULL) {
node->next[c - 'a'] = new Trie();
}
node = node->next[c - 'a'];
}
node->isEnd = true;
}

bool search(string word) { // 从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回 false
Trie* node = this;
for (auto c:word) {
node = node->next[c - 'a'];
if (node == NULL) {
return false;
}
}
return node->isEnd;
}

bool startsWith(string prefix) { // 和search的区别只在最后的判断
Trie* node = this;
for (auto c:prefix) {
node = node->next[c - 'a'];
if (node == NULL) {
return false;
}
}
return true;
}
};

/**
* Your Trie object will be instantiated and called as such:
* Trie* obj = new Trie();
* obj->insert(word);
* bool param_2 = obj->search(word);
* bool param_3 = obj->startsWith(prefix);
*/

在你的Trie实现中:

  • Trie* node = this;是用来从当前对象(即根节点)开始操作的。
  • 如果你用new Trie(),那意味着你创建了一个新的Trie对象,这个对象和原来的Trie无关,并且是一个全新的空的前缀树。所以这里的区别在于,你是想要操作当前已有的前缀树,还是想要创建一个新的空的前缀树。

总结

  • this指针是对已有对象的引用。
  • 新建对象(使用new)是创建一个新的实例,它和原始对象没有直接关系。
  • 使用this的场景是想操作已有对象的内部,而使用new则是为了创建一个新对象来存储新的数据或状态。

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

有效括号对数,必须要再之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
vector<string> output;
string temp;

void backtracking(int n, int usedLeft, int usedRight) {
if (usedLeft == n) {
for (int i = 0; i < n - usedRight; i++) { // 补齐右括号
temp.push_back(')');
}
output.emplace_back(temp);
for (int i = 0; i < n - usedRight; i++) { // 不要忘记这边要pop掉
temp.pop_back();
}
return;
}

if (usedLeft < n) {
// 选择加入‘(’, 不管什么情况都可以,只要还有n
temp.push_back('(');
backtracking(n, usedLeft + 1, usedRight);
temp.pop_back();
}
if (usedLeft > usedRight) {
temp.push_back(')');
backtracking(n, usedLeft, usedRight + 1);
temp.pop_back();
}
}

vector<string> generateParenthesis(int n) {
backtracking(n, 0, 0);
return output;
}
};

79. 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

图论解法——广度优先好像有问题,比如之前visited过的元素,那条路径被废弃了,万一在后面需要visit,就不行了。无法回溯回去——所以下面的解法是错的。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Solution {
public:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};

bool bfs(vector<vector<char>>& board, string word, int x, int y){
queue <pair<int, int>> Q;
vector<vector<bool>> visited(board.size(), vector<bool>(board[0].size(), false));
Q.push({x, y});
visited[x][y] = true;
int len = 1;

cout << x << " " << y << endl;

while(!Q.empty()) {
int qSize = Q.size();
cout << qSize << " ";
for (int j = 0; j < qSize; j++) {
pair<int, int> curr = Q.front();
int xCurr = curr.first;
int yCurr = curr.second;
Q.pop();
for (int i = 0; i < 4; i++) {
int xNext = xCurr + dir[i][0];
int yNext = yCurr + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= board.size() || yNext >= board[0].size() || board[xNext][yNext] != word[len] || visited[xNext][yNext] == true) {
continue;
}
Q.push({xNext, yNext});
visited[xNext][yNext] = true;
}
}
len++;
cout << len << endl;
}
return len - 1 == word.size();
}


bool exist(vector<vector<char>>& board, string word) {
// bool output = false;
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
if (board[i][j] == word[0]) {
if (bfs(board, word, i, j) == true) {
return true;
}
}
}
}
return false;
}
};

下面是正确写法——深搜,虽然好像写的又臭又长,len和判断逻辑应该写在dfs里面的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};

bool dfs(vector<vector<char>>& board, string word, vector<vector<bool>>& visited, int x, int y, int len){


if (len == word.size()) {
return true;
}

bool output = false;
for (int i = 0; i < 4; i++) {
int xNext = x + dir[i][0];
int yNext = y + dir[i][1];
if (xNext < 0 || yNext < 0 || xNext >= board.size() || yNext >= board[0].size() || board[xNext][yNext] != word[len] || visited[xNext][yNext] == true) {
continue;
}
visited[xNext][yNext] = true;
output = output || dfs(board, word, visited, xNext, yNext, len+1);
visited[xNext][yNext] = false;
}
return output;

}


bool exist(vector<vector<char>>& board, string word) {
// bool output = false;
vector<vector<bool>> visited(board.size(), vector<bool>(board[0].size(), false));
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
if (board[i][j] == word[0]) {
visited[i][j] = true;
if (dfs(board, word, visited, i, j, 1) == true) {
return true;
}
visited[i][j] = false;
}
}
}
return false;
}
};

35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

注意开闭区间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int output = nums.size();
while (left <= right) {
int mid = (left + right) / 2 ;
if (target <= nums[mid]) {
output = mid;
right = mid - 1;
}
else {
left = mid + 1;
}
}
return output;
}
};

74. 搜索二维矩阵

给你一个满足下述两条属性的 m x n 整数矩阵:

  • 每行中的整数从左到右按非严格递增顺序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false

非严格递增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int row = matrix.size();
int column = matrix[0].size();
int left = 0;
int right = row * column - 1;

while (left <= right) {
int mid = (left + right) / 2;
int midX = mid / column; // 看看清啊,这里要除和模的是谁
int midY = mid % column;

if (matrix[midX][midY] == target) {
return true;
}
else if (target > matrix[midX][midY]) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
return false;
}
};

34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

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

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] < target) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid - 1;
}
else {
int leftBound = mid;
while (leftBound >= 0 && nums[leftBound] == target) {
leftBound--;
}
int rightBound = mid;
while (rightBound < nums.size() && nums[rightBound] == target) {
rightBound++;
}
return {leftBound+1, rightBound-1};
}
}
return {-1, -1};
}
};

官方题解在二分查找上做文章,加入了一个选项,确定是不是最小的/最大的范围。

33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

先找分界点,再二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
int search(vector<int>& nums, int target) {
// 先找分界点
int min = 0;
int left = 1;
int right = nums.size() - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[0] < nums[mid]) {
left = mid + 1;
}
else {
right = mid - 1;
min = mid;
}
}
left = min;
right = left + nums.size() - 1;
while (left <= right) {
int mid = (left + right) / 2;
int i = mid % nums.size();
if (target < nums[i]) right = mid - 1;
else if (target > nums[i]) left = mid + 1;
else return i;
}
return -1;
}
};

153. 寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findMin(vector<int>& nums) {
int min = 0;
int left = 1;
int right = nums.size() - 1;
while(left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > nums[0]) {
left = mid + 1;
}
else {
right = mid - 1;
min = mid;
}
}
return nums[min];
}
};

4. 寻找两个正序数组的中位数

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

解法一——不满足时间复杂度要求

直接找中间的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
double output;
int p1 = 0;
int p2 = 0;
int prev = -1;
int curr = -1;
for (int i = 0; i <= (nums1.size() + nums2.size()) / 2; i++) {
prev = curr;
if (p1 < nums1.size() && (p2 >= nums2.size() || nums1[p1] < nums2[p2])) {
curr = nums1[p1++];
}
else {
curr = nums2[p2++];
}
}
if ((nums1.size() + nums2.size()) % 2 == 0) { //偶数
return (prev + curr) / 2.0;
}
else {
return curr;
}
}
};

解法二——第k小数

要求O(log(m+n))O(\log(m+n))的复杂度

题目是求中位数,其实就是求第 k 小数的一种特殊情况

更一般的情况 A[1] ,A[2] ,A[3],A[k/2] … ,B[1],B[2],B[3],B[k/2] … ,如果 A[k/2]<B[k/2] ,那么A[1],A[2],A[3],A[k/2]都不可能是第 k 小的数字。

不断地去找第k小的数字,排除的去掉。

作者:windliang

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
int getKthElement(vector<int>& nums1, vector<int>& nums2, int k) {
int p1 = 0, p2 = 0;
while(1) {
int nums1Size = nums1.size();
int nums2Size = nums2.size();
if (p1 == nums1Size) {
return nums2[p2 + k - 1];
}
if (p2 == nums2Size) {
return nums1[p1 + k - 1];
}
if (k == 1) {
return min(nums1[p1], nums2[p2]) ;
}
int p1Next = min(p1 + k/2 - 1, nums1Size - 1);
int p2Next = min(p2 + k/2 - 1, nums2Size - 1);
if (nums1[p1Next] <= nums2[p2Next]) {
k -= p1Next - p1 + 1;
p1 = p1Next + 1;
}
else {
k -= p2Next - p2 + 1;
p2 = p2Next + 1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if ((nums1.size() + nums2.size()) % 2) { // 奇数
return getKthElement(nums1, nums2, ((nums1.size() + nums2.size()) / 2 + 1));
}
else {
return (getKthElement(nums1, nums2, ((nums1.size() + nums2.size()) / 2)) + getKthElement(nums1, nums2, ((nums1.size() + nums2.size()) / 2) + 1)) /2.0;
}
}
};

155. 最小栈

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

再找一个辅助栈,专门存最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MinStack {
public:
stack<int> S;
stack<int> minS;

MinStack() {
minS.push(INT_MAX);
}

void push(int val) {
S.push(val);
if (val < minS.top()) {
minS.push(val);
}
else {
minS.push(minS.top());
}
}

void pop() {
S.pop();
minS.pop();
}

int top() {
return S.top();
}

int getMin() {
return minS.top();
}
};

/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/

394. 字符串解码(▲)

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a2[4] 的输入。

GPT的修改结果,思路更加清晰,我原来想得太乱了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
string decodeString(string s) {
stack<int> times; // 栈存储重复次数
stack<string> strings; // 栈存储中间结果
string temp;
string output;

for (int i = 0; i < s.size(); i++) {
if (isdigit(s[i])) { // 判断是否为数字
int num = 0;
while (isdigit(s[i])) {
num = num * 10 + (s[i] - '0');
i++;
}
times.push(num);
i--; // 回退一个位置,以便后续的 '[' 可以正确处理
} else if (s[i] == '[') {
strings.push(output); // 将当前的 output 保存
output = ""; // 清空 output,准备存储新的子字符串
} else if (s[i] == ']') {
string tempTimes = output;
output = strings.top(); // 取出上一次的字符串
strings.pop();
int repeat = times.top();
times.pop();
for (int j = 0; j < repeat; j++) {
output += tempTimes; // 拼接重复的字符串
}
} else { // 字母
output.push_back(s[i]);
}
}

return output;
}
};

215. 数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

解法一——基于快排和快速选择

首先我们来回顾一下快速排序,这是一个典型的分治算法。我们对数组 a[l⋯r] 做快速排序的过程是(参考《算法导论》):

分解: 将数组 a[l⋯r] 「划分」成两个子数组 a[l⋯q−1]、a[q+1⋯r],使得 a[l⋯q−1] 中的每个元素小于等于 a[q],且 a[q] 小于等于 a[q+1⋯r] 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分。
解决: 通过递归调用快速排序,对子数组 a[l⋯q−1] 和 a[q+1⋯r] 进行排序。
合并: 因为子数组都是原址排序的,所以不需要进行合并操作,a[l⋯r] 已经有序。
上文中提到的 「划分」 过程是:从子数组 a[l⋯r] 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, x 的最终位置就是 q。

所以只要某次划分的 q为倒数第k个下标的时候,我们就已经找到了答案。

作者:力扣官方题解

还是没完全看懂,为啥要do while啊,防止多加吗—— 帮助程序在有大量重复数字时快速收敛边界。 快速收敛就是让j尽可能接近当前区间中间位置。

在特别用例中,存在大量在xnums[i]nums[j]都相等的情况。

所以会有许多次i++,j--,这让j`更进一步地接近中间位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
int quickSelect(vector<int>& nums, int l, int r, int k) {
if (l == r) {
return nums[k];
}
int partition = nums[l];
int low = l -1;
int high = r +1;
while (low < high) {
// while(nums[low] < partition) {
// low++;
// }
// while(nums[high] > partition) {
// high--;
// }
do {low++;} while (nums[low] < partition);
do {high--;} while (nums[high] > partition);
if (low < high) {
swap(nums[low], nums[high]);
}

}
if (k <= high) {
return quickSelect(nums, l, high, k);
}
else {
return quickSelect(nums, high + 1, r, k);
}
}
int findKthLargest(vector<int>& nums, int k) {
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
};

解法二——堆

小根堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<>> pq;

for(int i = 0; i < k; i++) {
pq.push(nums[i]);
}
for(int i = k; i < nums.size(); i++) {
if (pq.top() < nums[i]) {
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
};

295. 数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5

实现 MedianFinder 类:

  • MedianFinder()初始化 MedianFinder 对象。
  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

用两个优先队列,分别存最大和最小?

当我们尝试添加一个数 num 到数据结构中,我们需要分情况讨论:

num≤max{queMin}

此时 num 小于等于中位数,我们需要将该数添加到 queMin 中。新的中位数将小于等于原来的中位数,因此我们可能需要将 queMin 中最大的数移动到 queMax 中。

num>max{queMin}

此时 num 大于中位数,我们需要将该数添加到 queMin 中。新的中位数将大于等于原来的中位数,因此我们可能需要将 queMax 中最小的数移动到 queMin 中。

作者:力扣官方题解

奇数,qMin比qMax多存一个。偶数相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MedianFinder {
public:
priority_queue<int, vector<int>, less<>> Qmin;
priority_queue<int, vector<int>, greater<>> Qmax;

MedianFinder() {

}

void addNum(int num) {
if (Qmin.empty() || num <= Qmin.top()) {
Qmin.push(num);
if (Qmax.size() + 1 < Qmin.size()) {
Qmax.push(Qmin.top());
Qmin.pop();
}
}
else {
Qmax.push(num);
if (Qmax.size() > Qmin.size()) {
Qmin.push(Qmax.top());
Qmax.pop();
}
}
}

double findMedian() {
if (Qmin.size() > Qmax.size()) {
return Qmin.top();
}
else {
return (Qmax.top() + Qmin.top()) / 2.0;
}
}
};

/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/

118. 杨辉三角

给定一个非负整数 *numRows,*生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> output(numRows);

for (int i = 0; i < numRows; i++) {
output[i].resize(i + 1);
output[i][0] = output[i][i] = 1;
for (int j = 1; j < i; j++) {
output[i][j] = output[i - 1][j - 1] + output[i - 1][j];
}
}
return output;
}
};

152. 乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

子数组 是数组中连续非空 元素序列。

测试用例的答案是一个 32-位 整数。

考虑和300题类似的思路,每次回退计算。但是超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int maxProduct(vector<int>& nums) {
vector<int> dp(nums.size()); // 包含该数的最长子数组乘积
dp[0] = nums[0];
int output = nums[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = nums[i];
int temp = 1;
for (int j = i; j >= 0; j--) {
temp *= nums[j];
dp[i] = max(dp[i], temp);
if (temp == 0) {
break;
}
}
if (dp[i] > output) {
output = dp[i];
}
}
return output;
}
};

用两个正负两个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxProduct(vector<int>& nums) {
vector<int> dpPlus(nums.size()); // 正序列(绝对值最大)
vector<int> dpMinus(nums.size()); // 负序列(绝对值最大)
dpPlus[0] = nums[0];
dpMinus[0] = nums[0];
int output = nums[0];
for (int i = 1; i < nums.size(); i++) {
dpPlus[i] = max(nums[i], max(dpPlus[i - 1] * nums[i], dpMinus[i - 1] * nums[i]));
dpMinus[i] = min(nums[i], min(dpPlus[i - 1] * nums[i], dpMinus[i - 1] * nums[i]));
if (dpPlus[i] > output) {
output = dpPlus[i];
}
}
return output;
}
};

32. 最长有效括号

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

考虑有"()(()"的情况,所以单纯的记数左括号作为dp是不对的

所以考虑以下的思路:

  • (均为0
  • )分上一个是((增加2)还是)(还得继续往前找)
    • ((增加2),dp[i]=dp[i2]+2dp[i] = dp[i-2]+2
    • )(继续往前找),要把上一个元素序列扣掉(它的长度是dp[i1]dp[i-1]),去看dp[idp[i1]1]dp[i-dp[i-1]-1]是不是(,是的话dp[i]=dp[i1]+dp[idp[i1]2]+2dp[i] = dp[i-1]+ dp[i-dp[i-1]-2]+2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
int longestValidParentheses(string s) {
vector<int> dp(s.size(), 0);
int output = 0;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') {
dp[i] = 0;
}
else if (i >= 1 && s[i - 1] == '(') {
if (i >= 2) {
dp[i] = dp[i-2] + 2;
}
else {
dp[i] = 2;
}
}
else if (i >= 1 && s[i - 1] == ')') {
if ((i - dp[i-1] - 1 >= 0) && (s[i - dp[i-1] - 1] == '(')) {
if (i - dp[i-1] - 2 >= 0) {
dp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2;
}
else {
dp[i] = dp[i-1] + 2;
}
}
}
output = max(output, dp[i]);
}
return output;
}
};

官方题解更加简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int longestValidParentheses(string s) {
int maxans = 0, n = s.length();
vector<int> dp(n, 0);
for (int i = 1; i < n; i++) {
if (s[i] == ')') {
if (s[i - 1] == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = max(maxans, dp[i]);
}
}
return maxans;
}
};

64. 最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

**说明:**每次只能向下或者向右移动一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int row = grid.size();
int column = grid[0].size();
vector<int> dp(column, 0);
int temp = 0;

for(int j = 0; j < column; j++) {
temp += grid[0][j];
dp[j] = temp;
}

for (int i = 1; i < row; i++) {
dp[0] = dp[0] + grid[i][0];
for (int j = 1; j < column; j++) {
dp[j] = min(dp[j] , dp[j - 1]) + grid[i][j];
}
}
return dp[column - 1];
}
};

5. 最长回文子串

给你一个字符串 s,找到 s 中最长的 回文子串。

和32挺像的。但是区别就是不光两个元素啊!所以会有其他的问题,比如一个滚动数组好像是不够的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
string longestPalindrome(string s) {
vector<vector<int>> dp(s.size(), vector<int> (s.size(), 0));
int len = 0;
string output;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i+1][j-1])) {
dp[i][j] = true;
if (j - i >= len) {
len = j - i;
output = s.substr(i, j - i + 1);
}
}
}
}
return output;
}
};

136. 只出现一次的数字

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

解法一——不考虑线性复杂度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int singleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
// int output = nums[0];
if (nums.size() == 1) {
return nums[0];
}
for (int i = 1; i < nums.size(); i++) {
if (nums[i-1] != nums[i]) {
return nums[i-1];
}
else {
i++;
}
}
return nums[nums.size() - 1];
}
};

解法二——位运算

真的想不到啊。。。

对于这道题,可使用异或运算 ⊕。异或运算有以下三个性质。

  • 任何数和 0 做异或运算,结果仍然是原来的数,即 a0=aa⊕0=a
  • 任何数和其自身做异或运算,结果是 0,即 aa=0a⊕a=0
  • 异或运算满足交换律和结合律,即 aba=baa=b(aa)=b0=ba⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b

作者:力扣官方题解

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
int singleNumber(vector<int>& nums) {
int single = 0;
for(int num:nums) {
single ^= num;
}
return single;
}
};

169. 多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

解法一——不考虑复杂度

① 先排序再找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int majorityElement(vector<int>& nums) {
if(nums.size() == 1){
return nums[0];
}
sort(nums.begin(), nums.end());
int count = 1;
for (int i = 1; i < nums.size(); i++) {
if(nums[i] == nums[i-1]) {
count++;
if (count > (nums.size() / 2)) {
return nums[i];
}
}
else {
count = 1;
}
}
return 0;
}
};

更简单的写法:

1
2
3
4
5
6
7
class Solution {
public:
int majorityElement(vector<int>& nums) {
sort(nums.begin(), nums.end());
return nums[nums.size()/2];
}
};

② 哈希表

解法二——Boyer-Moore 投票算法

如果我们把众数记为 +1,把其他数记为 −1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多。

太形象了——“同归于尽消杀法” :

由于多数超过50%, 比如100个数,那么多数至少51个,剩下少数是49个。

  1. 第一个到来的士兵,直接插上自己阵营的旗帜占领这块高地,此时领主 winner 就是这个阵营的人,现存兵力 count = 1。
  2. 如果新来的士兵和前一个士兵是同一阵营,则集合起来占领高地,领主不变,winner 依然是当前这个士兵所属阵营,现存兵力 count++;
  3. 如果新来到的士兵不是同一阵营,则前方阵营派一个士兵和它同归于尽。 此时前方阵营兵力count --。(即使双方都死光,这块高地的旗帜 winner 依然不变,因为已经没有活着的士兵可以去换上自己的新旗帜)
  4. 当下一个士兵到来,发现前方阵营已经没有兵力,新士兵就成了领主,winner 变成这个士兵所属阵营的旗帜,现存兵力 count ++。

就这样各路军阀一直以这种以一敌一同归于尽的方式厮杀下去,直到少数阵营都死光,那么最后剩下的几个必然属于多数阵营,winner 就是多数阵营。(多数阵营 51个,少数阵营只有49个,死剩下的2个就是多数阵营的人)

https://leetcode.cn/problems/majority-element/solution/javashi-pin-jiang-jie-xi-lie-majority-element-by-s/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int majorityElement(vector<int>& nums) {
int count = 1;
int curr = nums[0];
for (int i = 1; i < nums.size(); i++) {
if (curr == nums[i]) {
count++;
}
else if (count == 0) {
curr = nums[i];
count = 1;
}
else {
count--;
}
}
return curr;
}
};

75. 颜色分类

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

解法一——排序(冒泡)

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
void sortColors(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++) {
for (int j = 1; j < nums.size() - i; j++) {
if (nums[j - 1] > nums[j]) {
swap(nums[j - 1], nums[j]);
}
}
}
}
};

解法二——直接统计,反正就三个颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
void sortColors(vector<int>& nums) {
int color[3] = {0};
for(int i = 0; i < nums.size(); i++) {
color[nums[i]]++;
}
for(int i = 0; i < color[0]; i++) {
nums[i] = 0;
}
for(int i = color[0]; i < color[0] + color[1]; i++) {
nums[i] = 1;
}
for(int i = color[0] + color[1]; i < nums.size(); i++) {
nums[i] = 2;
}
}
};

解法三——单指针

遍历两边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
void sortColors(vector<int>& nums) {
int p = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == 0) {
swap(nums[i], nums[p]);
p++;
}
}
for (int i = p; i < nums.size(); i++) {
if (nums[i] == 1) {
swap(nums[i], nums[p]);
p++;
}
}
}
};

解法四——双指针(▲)

两个指针一个指向0,一个指向1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
void sortColors(vector<int>& nums) {
int p0 = 0, p1 = 0;
for (int i = 0; i < nums.size(); i++) {
if(nums[i] == 1) {
swap(nums[i], nums[p1]);
p1++;
}
else if (nums[i] == 0) {
swap(nums[i], nums[p0]);
if (p0 < p1) {
swap(nums[i], nums[p1]);
}
p0++;
p1++;
}
}

}
};

31. 下一个排列(▲)

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须** 原地 **修改,只允许使用额外常数空间。

image-20241010160245605

如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:

  1. 我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
  2. 我们还希望下一个数 增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:
    1. 尽可能靠右的低位 进行交换,需要 从后向前 查找
    2. 将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
    3. 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列

作者:Imageslr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
void nextPermutation(vector<int>& nums) {
if (nums.size() <= 1) {
return;
}
int i = nums.size() - 2;
// 找到从后往前的第一个相邻升序对
while (i >=0 && nums[i] >= nums[i+1]) {
i--;
}

if (i >= 0) {// 非最后一个排列
//找到尽可能小的「大数」
int k = nums.size() - 1;
while (k >= 0 && nums[i] >= nums[k]) {
k--;
}
swap(nums[i], nums[k]);
}
reverse(nums.begin() + i + 1, nums.end());
}
};

287. 寻找重复数(▲)

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

二分法:

定义cnt[i]cnt[i]是数组中小于i的数的个数,cnt[]cnt[]随数字增大有单调性(targetcnt[i]≤itargetcnt[i]>i

  1. 如果测试用例的数组中 target 出现了两次,其余的数各出现了一次,这个时候肯定满足上文提及的性质,因为小于 target 的数 i 满足 cnt[i]=i,大于等于 target 的数 j 满足 cnt[j]=j+1。

  2. 如果测试用例的数组中 target 出现了三次及以上,那么必然有一些数不在 nums 数组中了,这个时候相当于我们用 target 去替换了这些数,我们考虑替换的时候对 cnt[] 数组的影响。如果替换的数 i 小于 target ,那么 [i,target−1] 的 cnt 值均减一,其他不变,满足条件。如果替换的数 j 大于等于 target,那么 [target,j−1] 的 cnt 值均加一,其他不变,亦满足条件。

作者:力扣官方题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int left = 1;
int right = nums.size() - 1;
int output = -1;
while (left <= right) {
int mid = (left + right) / 2;
int cnt = 0;
for (int i = 0; i < nums.size(); i++) {
cnt += (nums[i] <= mid);
}
if (cnt <= mid) {
left = mid + 1;
}
else {
right = mid - 1;
output = mid;
}
}
return output;
}
};

华为手撕准备

54. 螺旋矩阵

  1. visited矩阵
  2. 模拟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
vector<int> output;
int row = matrix.size();
int column = matrix[0].size();
int x = 0;
int y = 0;
int upBound = 0, downBound = row - 1, leftBound = 0, rightBound = column - 1;


while (1) {
for (int j = leftBound; j <= rightBound; j++) {
output.push_back(matrix[upBound][j]);
}
upBound++;
if (upBound > downBound) {
break;
}
for (int i = upBound; i <= downBound; i++) {
output.push_back(matrix[i][rightBound]);
}
rightBound--;
if (rightBound < leftBound) {
break;
}
for (int j = rightBound; j >= leftBound; j--) {
output.push_back(matrix[downBound][j]);
}
downBound--;
if (downBound < upBound) {
break;
}
for (int i = downBound; i >= upBound; i--) {
output.push_back(matrix[i][leftBound]);
}
leftBound++;
if (rightBound < leftBound) {
break;
}
}
return output;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> output;
int m = matrix.size();
int n = matrix[0].size();
int length = m * n;
int x = 0;
int y = 0;
int count = n;
int dir[4][2] = {0,1,1,0,0,-1,-1,0}; // 01右;10下;0-1左;-10上
int countDir = 0;

for (int i = 0; i < length; i++) {
output.emplace_back(matrix[x][y]);
// cout << count << " || ";
if(count <= 1) { // count = 1 换方向
countDir = (countDir + 1) % 4;
m -= abs(dir[countDir][0]);
n -= abs(dir[countDir][1]);
count = abs(dir[countDir][0]) * m + abs(dir[countDir][1]) * n + 1;
}
// cout << i <<" " <<count << " || " << x << " " << y << " || " << countDir << " " << dir[countDir][0] << " " << dir[countDir][1] << endl;
x += dir[countDir][0];
y += dir[countDir][1];
count --;
}
return output;
}
};

买卖股票

  1. 只能买卖一次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Solution {
    public:
    int maxProfit(vector<int>& prices) {
    int len = prices.size();
    vector<vector<int>> dp(2, vector<int>(2)); // 注意这里只开辟了一个2 * 2大小的二维数组
    dp[0][0] -= prices[0];
    dp[0][1] = 0;
    for (int i = 1; i < len; i++) {
    dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]);
    dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
    }
    return dp[(len - 1) % 2][1];
    }
    };
  2. 可以买卖无数次:dp[i][0]dp[i][0]不止和上次也没持有比较,还和上次持有但是卖了有关。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Solution {
    public:
    int maxProfit(vector<int>& prices) {
    vector<vector<int>> dp(2, vector<int> (2));
    dp[0][0] = -prices[0];
    dp[0][1] = 0;
    for(int i = 1; i < prices.size(); i++) {
    dp[i % 2][0] = max(dp[(i-1) % 2][0], dp[(i-1) % 2][1]-prices[i]); // 持有股票
    dp[i % 2][1] = max(dp[(i-1) % 2][1], dp[(i-1) % 2][0]+prices[i]); // 不持有股票
    }
    return dp[(prices.size() - 1)%2][1];
    }
    };
  3. 最多可以完成 两笔 交易。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Solution {
    public:
    int maxProfit(vector<int>& prices) {
    int n = prices.size();
    int buy1 = -prices[0], sell1 = 0;
    int buy2 = -prices[0], sell2 = 0;
    for (int i = 1; i < n; ++i) {
    buy1 = max(buy1, -prices[i]);
    sell1 = max(sell1, buy1 + prices[i]);
    buy2 = max(buy2, sell1 - prices[i]);
    sell2 = max(sell2, buy2 + prices[i]);
    }
    return sell2;
    }
    };
  4. 最多可以完成k笔交易的通解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Solution {
    public:
    int maxProfit(int k, vector<int>& prices) {
    vector buy(k+1, INT_MIN), sel(k+1, 0);
    for (int i : prices) {
    for (int j = 1; j < k+1; j++) {
    buy[j] = max(buy[j], sel[j - 1] - i);
    sel[j] = max(sel[j], buy[j] + i);
    }
    }
    return sel[k];
    }
    };
  5. 无限次交易,但带冷静期

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Solution {
    public:
    int maxProfit(vector<int>& prices) {
    vector<vector<int>> dp(prices.size(), vector<int>(3));
    dp[0][0] = - prices[0]; // 持有
    dp[0][1] = 0; // 不持有且不在不在冷冻期
    dp[0][2] = 0; // 冷冻期

    for(int i = 1; i < prices.size(); i++) {
    dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]); // 持有只有两种:上一期持有,上次不持有-price[i]
    dp[i][1] = max(dp[i-1][1], dp[i-1][2]); // 上一次在冷冻期,或者上一次不持有
    dp[i][2] = dp[i-1][0] + prices[i]; // 上一期持有,本期抛售
    // cout << dp[i][0] << " " << dp[i][1] << " " << dp[i][2] << endl;
    }
    return max(dp[prices.size() - 1][1], dp[prices.size() - 1][2]);

    }
    };
  6. 无限次交易,但含手续费

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Solution {
    public:
    int maxProfit(vector<int>& prices, int fee) {
    vector<vector<int>> dp(prices.size(), vector<int> (2));
    dp[0][0] = -prices[0];
    dp[0][1] = 0;
    for (int i = 1; i < prices.size(); i++) {
    dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
    dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
    }
    return dp[prices.size() - 1][1];
    }
    };

公共子串

回文子串

动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.length(), vector<bool>(s.length(), false));
int output = 0;

for (int i = s.length() - 1; i >= 0; i--) {
for (int j = i; j < s.length(); j++) {
if (s[i] == s[j] && (j - i <= 1 || dp[i+1][j-1])) {
output++;
dp[i][j] = true;
}
}
}
return output;
}

双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
int countSubstrings(string s) {
int left, right;
int output = 1; // 0位置的

for (int center = 1; center < s.length(); center++) {
output ++; // 独自的
// 区分奇偶,偶数往左找
if (s[center] == s[center - 1]) { // 偶
left = center - 1;
right = center;
while (left >= 0 && right < s.length() && s[left] == s[right]) {
output++;
left--;
right++;
}
}
// 奇数是必然有的情况,不需要else
left = center - 1;
right = center + 1;
while (left >= 0 && right < s.length() && s[left] == s[right]) {
output++;
left--;
right++;
}
}
return output;
}
};

括号匹配

链表成环


持续更新中

本文标题:【基本完结】LeetCode自用刷题记录

文章作者:Levitate_

发布时间:2024年03月29日 - 21:38:44

原始链接:https://levitate-qian.github.io/2024/03/30/LeetCode-problems/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。