diff --git a/2024/01/01/leetcode/index.html b/2024/01/01/leetcode/index.html index 93d9958..def80f6 100644 --- a/2024/01/01/leetcode/index.html +++ b/2024/01/01/leetcode/index.html @@ -26,6 +26,9 @@ + + + @@ -55,7 +58,7 @@ - + @@ -241,7 +244,7 @@ - 109k words + 113k words @@ -252,7 +255,7 @@ - 905 mins + 940 mins @@ -294,7 +297,7 @@

【算法题】LeetCode算法汇总

- Last updated on May 23, 2024 pm + Last updated on June 6, 2024 pm

@@ -434,6 +437,59 @@

螺旋矩阵

1
2
输入:n = 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
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> result(n, vector<int>(n,0));
int is=0,ie=n-1,js=0,je=n-1;
int k = 1;
while(is<=ie&&js<=je){
for(int j=js;j<=je;j++)
{
result[is][j] = k++;
}
is++;
for(int i =is;i<=ie;i++)
{
result[i][je] = k++;
}
je--;
for(int j=je;j>=js;j--)
{
result[ie][j] = k++;
}
ie--;
for(int i=ie;i>=is;i--)
{
result[i][js] = k++;
}
js++;
}
return result;
}
};
+

螺旋矩阵2

+

https://leetcode.cn/problems/spiral-matrix/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

+

给你一个 mn 列的矩阵 +matrix ,请按照 顺时针螺旋顺序 +,返回矩阵中的所有元素。

+

螺旋矩阵2

+

思路:

+

主要的解题方法就是模拟,但是需要注意的是每次改变is/ie/js/je之后都需要进行一次判断一旦不满足循环的条件立马退出不进行模拟

+
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<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
int is = 0, js = 0, ie = m-1, je = n-1;
vector<int>res;
while(is<=ie && js<=je){
for(int j=js;j<=je;j++){
res.push_back(matrix[is][j]);
}
is++;
if(is>ie || js>je) break;
for(int i = is;i<=ie;i++){
res.push_back(matrix[i][je]);
}
je--;
if(is>ie || js>je) break;
for(int j=je;j>=js;j--){
res.push_back(matrix[ie][j]);
}
ie--;
if(is>ie || js>je) break;
for(int i =ie;i>=is;i--){
res.push_back(matrix[i][js]);
}
js++;
if(is>ie || js>je) break;
}
return res;
}
};
+

生命游戏

+

https://leetcode.cn/problems/game-of-life/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

+

根据 百度百科 +, 生命游戏 ,简称为 生命 +,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

+

给定一个包含 m × n +个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: +1 即为 活细胞 (live),或 0 +即为 死细胞 +(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

+
    +
  1. 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
  2. +
  3. 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
  4. +
  5. 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
  6. +
  7. 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
  8. +
+

下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 +m x n 网格面板 board +的当前状态,返回下一个状态。

+

image-20240606214825990

+

思路:

+
    +
  1. 给出8个方位的计算公式,-1,0,1横纵两个方向来调整
  2. +
  3. 复制一份相同的地图保证不会每次调整的时候更换初始的值
  4. +
  5. 模拟操作即可,注意每次考虑边界条件
  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
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public:
void gameOfLife(vector<vector<int>>& board) {
int step[3] = {-1,0,1};
vector<vector<int>>origin_board(board.size(), vector<int>(board[0].size(),0));
for(int i =0;i<board.size();i++){
for(int j = 0;j<board[0].size();j++){
origin_board[i][j] = board[i][j];
}
}
for(int r = 0;r<board.size();r++){
for(int c = 0;c<board[0].size();c++){
//计算周围的活细胞数量
int alive_cell = 0;

//遍历8个方向
for(int i = 0;i<3;i++){
for(int j=0;j<3;j++){
if(!(step[i]==0 && step[j]==0)){ //表示不同时为0的情况
if(((r+step[i]>=0)&&(r+step[i]<board.size()))&&((c+step[j]>=0)&&(c+step[j]<board[0].size()))&&(origin_board[r+step[i]][c+step[j]]==1)){
alive_cell++;
}
}
}
}

if(origin_board[r][c]==1){
if ((alive_cell<2)||(alive_cell>3)){
board[r][c]=0;
}
}else{
if(alive_cell==3){
board[r][c]=1;
}
}
}
}
}
};
+

旋转矩阵

+

给定一个 n × n 的二维矩阵 matrix +表示一个图像。请你将图像顺时针旋转 90 度。

+

你必须在原地 +旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 +使用另一个矩阵来旋转图像。

+

旋转矩阵示意图

+

思路:

+
    +
  1. 顺时针旋转的思路先水平方向翻转
  2. +
  3. 再中心对称反转即可
  4. +
+
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]);
}
}
}
};

快速排序

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
#include <stdio.h>
#include <iostream>
#include <math.h>
#include <algorithm>
using namespace std;
int part(int* r, int low, int hight) //划分函数
{
int i = low, j = hight, pivot = r[low]; //基准元素
while (i < j)
{
while (i<j && r[j]>pivot) //从右向左开始找一个 小于等于 pivot的数值
{
j--;
}
if (i < j)
{
swap(r[i++], r[j]); //r[i]和r[j]交换后 i 向右移动一位
}
while (i < j && r[i] <= pivot) //从左向右开始找一个 大于 pivot的数值
{
i++;
}
if (i < j)
{
swap(r[i], r[j--]); //r[i]和r[j]交换后 i 向左移动一位
}
}
return i; //返回最终划分完成后基准元素所在的位置
}
void Quicksort(int* r, int low, int hight)
{
int mid;
if (low < hight)
{
mid = part(r, low, hight); // 返回基准元素位置
Quicksort(r, low, mid - 1); // 左区间递归快速排序
Quicksort(r, mid+1, hight); // 右区间递归快速排序
}
}

数组中的第K大元素

@@ -669,6 +725,39 @@

移动零

思路:

使用双指针,向后遍历的过程中一旦遇到非0的元素就将其与左边指针互换并左边下标+1,凡事遇到需要交换位置的这类方法都建议能使用双指针来实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0;
for(int right = 0; right<nums.size();right++){
if(nums[right]){
swap(nums[left],nums[right]);
left++;
}
}
}
};
+

删除有序数组中的重复元素

+

https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

+

给你一个 非严格递增排列 的数组 nums +,请你原地 +删除重复出现的元素,使每个元素 只出现一次 +,返回删除后数组的新长度。元素的 相对顺序 应该保持 +一致 。然后返回 nums +中唯一元素的个数。

+

考虑 nums 的唯一元素的数量为 k +,你需要做以下事情确保你的题解可以被通过:

+ +

示例 1:

+
1
2
3
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
+

示例 2:

+
1
2
3
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
+

思路:

+

这道题技巧在于抓住有序的数组,可以用双指针来对数组进行遍历,定义两个指针 +fast 和 +slow分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标 +1。

+

假设数组 nums的长度为 n。将快指针 fast 依次遍历从 1到 +n−1的每个位置,对于每个位置,如果 nums[fast]≠nums[fast−1],说明 +nums[fast]和之前的元素都不同,因此将 nums[fast]的值复制到 +nums[slow],然后将 slow的值加 1,即指向下一个位置。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n = nums.size();
if(n==0){
return 0;
}
int slow = 1, fast = 1;
while(fast<n){
if(nums[fast]!=nums[fast-1]){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};

反转字符串

https://leetcode.cn/problems/reverse-string/description/

@@ -2224,7 +2313,7 @@

冗余连接II

Updated on
-
May 23, 2024
+
June 6, 2024
diff --git a/local-search.xml b/local-search.xml index 650d3e5..31eebbc 100644 --- a/local-search.xml +++ b/local-search.xml @@ -276,7 +276,7 @@ /2024/01/01/leetcode/ -

语言细节

vector的长度:

初始化数组:

构造vector:

for循环:

数组

二分查找

题目描述

链接:https://leetcode.cn/problems/binary-search/description/

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

示例 1:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

思路

题目表示的是有序数组,而且题目没有重复元素。在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 定义target在左闭右闭的区间里,[left, right]

while left <= right:
middle = left + (right - left) // 2

if nums[middle] > target:
right = middle - 1 # target在左区间,所以[left, middle - 1]
elif nums[middle] < target:
left = middle + 1 # target在右区间,所以[middle + 1, right]
else:
return middle # 数组中找到目标值,直接返回下标
return -1 # 未找到目标值

注意这里给出的题解法:当left <= right的时候,以下的条件中全部都不取到等号nums[middle] > target nums[middle] < target

需要注意的是:right=nums.size()-1

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
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
while(left<=right)
{
// int middle = (left+right)/2; 这样写会溢出
int middle = left + ((right - left) / 2);
if(nums[middle]>target)
{
right = middle-1;
}
else if(nums[middle]<target)
{
left = middle+1;
}
else{
return middle;
}
}
return -1;
}
};

Go版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func search(nums []int, target int) int {
right:=len(nums)-1
left:=0
for left<=right{
middle:= left+(right-left)/2
if nums[middle]<target{
left = middle+1
}else if nums[middle]>target{
right = middle-1
}else{
return middle
}
}
return -1
}

移除元素

https://leetcode.cn/problems/remove-element/description/

题目描述

示例 1:

1
2
3
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

1
2
3
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

思路

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针

双指针题解

C++版本

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

python版本

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def removeElement(self, nums, val):
slowindex=0
fastindex=0
while fastindex<len(nums):
if val!=nums[fastindex]:
nums[slowindex]=nums[fastindex]
slowindex = slowindex+1
fastindex+=1
return slowindex

GO版本:

1
2
3
4
5
6
7
8
9
10
func removeElement(nums []int, val int) int {
slow:=0
for i:=0;i<len(nums);i++{
if nums[i]!=val{
nums[slow]=nums[i]
slow++
}
}
return slow
}

有序数组的平方

https://leetcode.cn/problems/squares-of-a-sorted-array/

题目描述

示例 1:

1
2
3
4
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

1
2
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

思路

双指针法,首尾遍历比较并存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result(nums.size(),0);
int j = nums.size()-1;
int k =j;
for(int i = 0 ;i<=j;)
{
if(nums[i]*nums[i]>nums[j]*nums[j]){
result[k--]= nums[i]*nums[i];
i++;
}else{
result[k--]= nums[j]*nums[j];
j--;
}
}
return result;
}
};

Python:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution(object):
def sortedSquares(self, nums):
l, r, i = 0, len(nums)-1, len(nums)-1
res = [float('inf')] * len(nums) # 需要提前定义列表,存放结果
while l<=r :
if nums[l]*nums[l] < nums[r]*nums[r] :
res[i--]=nums[r]*nums[r]
r--
else:
res[i--]=nums[l]*nums[l]
l++
return

GO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sortedSquares(nums []int) []int {
n := len(nums)
i, j, k := 0, n-1, n-1
ans := make([]int, n)
for i <= j {
lm, rm := nums[i]*nums[i], nums[j]*nums[j]
if lm > rm {
ans[k] = lm
i++
} else {
ans[k] = rm
j--
}
k--
}
return ans
}

长度最小的子数组

https://leetcode.cn/problems/minimum-size-subarray-sum/description/

题目描述

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

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

示例 1:

1
2
3
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

1
2
输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

1
2
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

思路

滑动窗口法

滑动窗口法

滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动本题中实现滑动窗口,主要确定如下三点:

窗口就是 满足其和 ≥ 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 minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};

螺旋矩阵

https://leetcode.cn/problems/spiral-matrix-ii/

题目描述

螺旋矩阵

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

1
2
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

示例 2:

1
2
输入:n = 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
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> result(n, vector<int>(n,0));
int is=0,ie=n-1,js=0,je=n-1;
int k = 1;
while(is<=ie&&js<=je){
for(int j=js;j<=je;j++)
{
result[is][j] = k++;
}
is++;
for(int i =is;i<=ie;i++)
{
result[i][je] = k++;
}
je--;
for(int j=je;j>=js;j--)
{
result[ie][j] = k++;
}
ie--;
for(int i=ie;i>=is;i--)
{
result[i][js] = k++;
}
js++;
}
return result;
}
};

快速排序

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
#include <stdio.h>
#include <iostream>
#include <math.h>
#include <algorithm>
using namespace std;
int part(int* r, int low, int hight) //划分函数
{
int i = low, j = hight, pivot = r[low]; //基准元素
while (i < j)
{
while (i<j && r[j]>pivot) //从右向左开始找一个 小于等于 pivot的数值
{
j--;
}
if (i < j)
{
swap(r[i++], r[j]); //r[i]和r[j]交换后 i 向右移动一位
}
while (i < j && r[i] <= pivot) //从左向右开始找一个 大于 pivot的数值
{
i++;
}
if (i < j)
{
swap(r[i], r[j--]); //r[i]和r[j]交换后 i 向左移动一位
}
}
return i; //返回最终划分完成后基准元素所在的位置
}
void Quicksort(int* r, int low, int hight)
{
int mid;
if (low < hight)
{
mid = part(r, low, hight); // 返回基准元素位置
Quicksort(r, low, mid - 1); // 左区间递归快速排序
Quicksort(r, mid+1, hight); // 右区间递归快速排序
}
}

数组中的第K大元素

https://leetcode.cn/problems/xx4gT2/description/

给定整数数组 nums 和整数 k,请返回数组中第**k** 个最大的元素。

请注意,你需要找的是数组排序后的第 k个最大的元素,而不是第 k 个不同的元素。

示例 1:

1
2
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

1
2
输入: [3,2,3,1,2,4,5,5,6] 和 k = 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
class Solution {
public:
int partition(vector<int>& nums, int low, int high){
int key = nums[low];
while(low< high){
while(low<high && nums[high]>= key) high--;
nums[low] = nums[high];
while(low<high&& nums[low]<=key) low++;
nums[high] = nums[low];
}
nums[low] = key;
return low;
}
void quicksort(vector<int>& nums, int low, int high){
if(low>=high) return;
int mid = partition(nums, low, high);
quicksort(nums, low, mid-1);
quicksort(nums, mid+1, high);
}
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
quicksort(nums, 0, n-1);
return nums[n-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
class Solution {
public:
void BuildHeap(vector<int>& arr, int i, int n){
int p = i, c=2*i+1;
while(c<=n){
if(c<n && arr[c]>arr[c+1]) c++;
if(arr[p]>arr[c]){
swap(arr[p], arr[c]);
p = c;
}
c = 2*c+1;
}
}

void Heapsort(vector<int>& arr){
//建立一个堆
for(int i=arr.size()/2-1;i>=0;i--){
BuildHeap(arr, i,arr.size()-1);
}
for(int i=arr.size()-1; i>0;i--){
swap(arr[0], arr[i]);
BuildHeap(arr,0,i-1);
}
}
int findKthLargest(vector<int>& nums, int k) {
Heapsort(nums);
return nums[k-1];
}
};

和为K的子数组

https://leetcode.cn/problems/subarray-sum-equals-k/description/?envType=study-plan-v2&envId=top-100-liked

给你一个整数数组 nums 和一个整数 k,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列

示例 1:

1
2
输入:nums = [1,1,1], k = 2
输出:2

示例 2:

1
2
输入:nums = [1,2,3], k = 3
输出:2

思路:

这道题用前缀和+哈希表来解决

和为K的子数组

主要的思路是,首先维护一个map数组来存每个元素的前缀和,以及出现的次数,当每次到一个位置的时候来判断当前的map中是否有pre-k的元素以及对应的值,如果有那么就可以将个数加上,这个map对应的键是加和的元素,值是出现的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int res = 0;
unordered_map<int, int>pre;
pre[0] = 1;
int p = 0;
for(int i =0;i<nums.size();i++){
p+=nums[i];
if(pre.find(p-k)!=pre.end()){
res+=pre[p-k];
}
pre[p]++;
}
return res;
}
};

哈希表

一般哈希表都是用来快速判断一个元素是否出现集合里

只需要初始化把所有元素都存在哈希表里,在查询的时候通过索引直接就可以知道元素在不在这哈希表里了

建立索引:哈希函数

有效的字母异位词

https://leetcode.cn/problems/valid-anagram/description/

题目描述

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

示例 1: 输入: s = "anagram", t = "nagaram" 输出: true

示例 2: 输入: s = "rat", t = "car" 输出: false

思路

暴力的方法可能时间复杂度会很高

判断有没有异位词的本质就是查看当前的字母是不是有出现过,那么思路就是选择哈希表

定义一个数组叫做record用来上记录字符串s里字符出现的次数。

需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。

再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。这样就将字符串s中字符出现的次数,统计出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
// record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
// record数组所有元素都为零0,说明字符串s和t是字母异位词
return true;
}
};

两个数组的交集

https://leetcode.cn/problems/intersection-of-two-arrays/description/

题目描述

示例 1:

1
2
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

1
2
3
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

思路

使用哈希表存储,但是用set(unordered_set)

std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表,使用unordered_set读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};

快乐数

https://leetcode.cn/problems/happy-number/description/

题目描述

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

「快乐数」 定义为:

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

示例 1:

1
2
3
4
5
6
7
输入:n = 19
输出:true
解释:
1**2 + 9**2 = 82
8**2 + 2**2 = 68
6**2 + 8**2 = 100
1**2 + 0**2 + 0**2 = 1

思路:

注意,题目中提到一个点是无限循环,说明计算的结果sum是有限的只需要在哈希表中将这部分的结果存储进去,并每次比较是不是出现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 getSum(int n){
int sum=0;
while(n){
sum+=(n%10)*(n%10);
n/=10;
}
return sum;
}
bool isHappy(int n) {
//首先建立哈希表来存储是不是出现了无限循环的结果
unordered_set<int>sum_set;
//无限循环 直到出现1或者无限循环且不是快乐数
while(1){
n=getSum(n);
if(sum_set.find(n)!=sum_set.end()){
return false;
}else{
sum_set.insert(n);
}
if(n==1){
return true;
}
}
}
};

两数之和

题目描述

https://leetcode.cn/problems/two-sum/submissions/495021134/

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

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

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

示例 1:

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

示例 2:

1
2
输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

1
2
输入:nums = [3,3], target = 6
输出:[0,1]

思路:

构建一个哈希表,然后遍历一遍就行了在哈希表中找n-a的值是否存在,但是最大的问题是数组中同一个元素在答案里不能重复出现,所以不能简单考虑unordered_set

这里提供一种新的思路,就是用unordered_map来存储数组中的数据内容和下标的数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};

四数相加

https://leetcode.cn/problems/4sum-ii/description/

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

示例 1:

1
2
3
4
5
6
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:

1
2
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

思路

  1. 首先定义 一个unordered_map,key放a和b两数之和,value放a和b两数之和出现的次数
  2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
  4. 在遍历大C和大D数组,找到如果 0-(c+d)在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
  5. 最后返回统计值 count 就可以了
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 fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
std::unordered_map<int,int>nm;
int res=0;

for(int i=0;i<nums1.size();i++){
for(int j=0;j<nums2.size();j++){
int s = nums1[i]+nums2[j];
nm[s]++;
}
}
for(int i=0;i<nums3.size();i++){
for(int j=0;j<nums4.size();j++){
if(nm.find(0-nums3[i]-nums4[j])!=nm.end()){
res+=nm[0-(nums3[i]+nums4[j])];
}
}
}
return res;
}
};

赎金信

https://leetcode.cn/problems/ransom-note/description/

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

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

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

示例 1:

1
2
输入:ransomNote = "a", magazine = "b"
输出:false

示例 2:

1
2
输入:ransomNote = "aa", magazine = "ab"
输出:false

思路:

用哈希表unordered_map来存储次数,对于ransomNote来减去次数

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) {
unordered_map<int,int>umap;
if(ransomNote.size()>magazine.size()){return false;}
for(int i=0;i<magazine.size();i++){
umap[magazine[i]-'a']++;
}
for(int i=0;i<ransomNote.size();i++){
if(umap.find(ransomNote[i]-'a')!=umap.end()){
umap[ransomNote[i]-'a']--;
if(umap[ransomNote[i]-'a']<0) {return false;}
}else{
return false;
}
}
return true;
}
};

三数之和

https://leetcode.cn/problems/3sum/description/

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

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

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

示例 1:

1
2
3
4
5
6
7
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1][-1,-1,2]

思路

其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码,而且使用哈希法在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2)

这道题可以用双指针法求解

拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left定义在i+1的位置上,定义下标right 在数组结尾的位置上

依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a =nums[i],b = nums[left],c = nums[right]。

接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right]> 0 就说明此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时三数之和小了,left就向右移动,才能让三数之和大一些,直到left与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
32
33
34
35
36
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}

}
return result;
}
};

字母异位词分组

https://leetcode.cn/problems/group-anagrams/description/?envType=study-plan-v2&envId=top-100-liked

给你一个字符串数组,请你将 字母异位词组合在一起。可以按任意顺序返回结果列表。

字母异位词是由重新排列源单词的所有字母得到的一个新单词。

示例 1:

1
2
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:

1
2
输入: strs = [""]
输出: [[""]]

示例 3:

1
2
输入: strs = ["a"]
输出: [["a"]]

思路:

对每个字符串进行计数,将每个字符串中出现的字母和数字进行排序作为key存储,利用map数据结构来进行存储上述的内容,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
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> map;
for(string str: strs){
int count_letter[26] = {0};
for(char c: str){
count_letter[c-'a']++;
}
string key = "";
for (int i =0;i<26;i++){
if(count_letter[i]>0){
key.push_back(i-'a');
key.push_back(count_letter[i]);
}
}
map[key].push_back(str);
}
vector<vector<string>> res;
for(auto& p:map) {
res.push_back(p.second);
}
return res;
}
};

最长连续序列

https://leetcode.cn/problems/longest-consecutive-sequence/submissions/531637826/?envType=study-plan-v2&envId=top-100-liked

题目描述:

给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为O(n) 的算法解决此问题。

示例 1:

1
2
3
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4

示例 2:

1
2
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

思路:

用哈希集合来存储上述的数,本质上还是需要比较是否存在下一个数x+1,但是可以在条件判断上进行约束,如果x-1这个数不存在集合中那么说明可以从x开始遍历!num_set.count(num - 1)){ //count用来计数是否存在数在集合中

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 longestConsecutive(vector<int>& nums) {
unordered_set<int> num_set;
for(auto num : nums){
num_set.insert(num);
}
int maxlong = 0;
for(auto num: num_set){
if (!num_set.count(num - 1)){ //count用来计数是否存在数在集合中
int curnum = num;
int curlong = 1;
while(num_set.count(curnum+1)){
curnum+=1;
curlong+=1;
}
maxlong = max(maxlong, curlong);
}
}
return maxlong;
}
};

双指针

移除元素

https://leetcode.cn/problems/remove-element/description/

示例 1:

1
2
3
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

1
2
3
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

思路:

使用快慢指针来实现两个指针之间的移动,对于找到了和val数值一样的就进行替换

移动零

https://leetcode.cn/problems/move-zeroes/description/?envType=study-plan-v2&envId=top-100-liked

给定一个数组 nums,编写一个函数将所有 0移动到数组的末尾,同时保持非零元素的相对顺序。

请注意,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

1
2
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

1
2
输入: nums = [0]
输出: [0]

思路:

使用双指针,向后遍历的过程中一旦遇到非0的元素就将其与左边指针互换并左边下标+1,凡事遇到需要交换位置的这类方法都建议能使用双指针来实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0;
for(int right = 0; right<nums.size();right++){
if(nums[right]){
swap(nums[left],nums[right]);
left++;
}
}
}
};

反转字符串

https://leetcode.cn/problems/reverse-string/description/

示例 1:

1
2
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

示例 2:

1
2
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]

思路:

采用两个指针之间互相交换,首尾交换

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
void reverseString(vector<char>& s) {
for(int a=0, b = s.size()-1;a<b;){
char tmp;
tmp=s[a];
s[a]=s[b];
s[b]=tmp;
a++;
b--;
}
}
};

反转字符串中的单词

https://leetcode.cn/problems/reverse-words-in-a-string/description/

示例 1:

1
2
输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

1
2
3
输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

1
2
3
输入:s = "a good   example"
输出:"example good 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
string reverseWords(string s) {
for(int i = s.size()-1;i>0;i--){
if(s[i]==s[i-1]&&s[i]==' '){
s.erase(s.begin()+i);
}
}
if(s.size()>0&&s[s.size()-1]==' '){
s.erase(s.begin()+s.size()-1);
}
if(s.size()>0&&s[0]==' '){
s.erase(s.begin());
}

//完成全局的交换
for(int i = 0, j=s.size()-1;i<j;i++,j--){
char tmp;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
cout<<s;
//进行局部的交换
int i=0;
int j=1;
while(j<=s.size()){
if(s[j]==' '||j==s.size()){
for(int k =i, q =j-1;k<q;k++,q--){
char tmp;
tmp = s[k];
s[k] = s[q];
s[q] = tmp;
}
i=j+1;
j=i+1;
}else{
j++;
}
}
return s;
}
};

反转链表

https://leetcode.cn/problems/reverse-linked-list/description/

image-20240118151406885

1
2
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

思路:本质上就是利用了两个链表指针实现对元素的转向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp;
ListNode* cur = head;
ListNode* pre = nullptr;
while(cur){
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};

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

https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/

image-20240118152000575

1
2
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

1
2
输入:head = [1], n = 1
输出:[]

思路:

遍历,用两个指针分别来记录

如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了

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:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;

// ListNode *tmp = slow->next; C++释放内存的逻辑
// slow->next = tmp->next;
// delete nth;

return dummyHead->next;
}
};

链表相交

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

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

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

示例 1:

链表相交图

1
2
3
4
5
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 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
27
28
29
30
31
32
33
34
35
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while(curA != NULL){
lenA++;
curA = curA ->next;
}
while(curB != NULL){
lenB++;
curB = curB ->next;
}
curA = headA;
curB = headB;
if(lenB> lenA){
swap(lenA,lenB);
swap(curA, curB);
}

int gap = lenA - lenB;
while(gap--){
curA = curA->next;
}
while(curA!=NULL){
if(curA == curB){
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};

环形链表

https://leetcode.cn/problems/linked-list-cycle-ii/description/

判断是否是有还存在,如果有那么返回开始入环的第一个节点的下标

环形链表

1
2
3
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

思路:

这道题用快慢指针的思路,就是慢指针每次只走一步,快指针每次走两步,如果在到达null之前出现快慢指针指向了同一个地方,说明这个链表有环存在,那么怎么判断下标的位置呢?

具体的证明过程:

相遇时slow指针走过的节点数为: x + y,fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针,(y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以fast指针走过的节点数 = slow指针走过的节点数 * 2:

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

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个(y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z注意这里n一定是大于等于1的,因为fast指针至少要多走一圈才能相遇slow指针

所以可以得到的规律是:从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点,那么当这两个指针相遇的时候就是 环形入口的节点

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:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast!=NULL&& fast->next!=NULL){
slow = slow->next;
fast = fast->next->next;
//说明找到了
if(slow==fast){
ListNode* index1 = fast;
ListNode* index2 = head;
while(index1!=index2){
index1 = index1->next;
index2 = index2 ->next;
}
return index2;
}
}
return NULL;
}
};

找到字符串中所有字幕的异位词

https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/?envType=study-plan-v2&envId=top-100-liked

给定两个字符串 sp,找到 s中所有 p异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

1
2
3
4
5
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:

1
2
3
4
5
6
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

思路:

这道题可以考虑用滑动窗口的方法来考虑,对于不同位置但是组成一样的一对字符串可以用哈希表来记录,这个地方就用哈希数组来存储,每次按照一定的顺序进行滑动

所以我们可以在字符串 s 中构造一个长度为与字符串 p的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;当窗口中每种字母的数量与字符串p 中每种字母的数量相同时,则说明当前窗口为字符串 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
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int m = s.size(), n = p.size();
if(m<n){
return vector<int>();
}
vector<int> res;
vector<int> scount(26);
vector<int> pcount(26);
for(int i =0;i<n;i++){
scount[s[i]-'a']++;
pcount[p[i]-'a']++;
}
if(scount == pcount){
res.push_back(0);
}
for(int i=0;i<m-n;i++){
scount[s[i]-'a']--;
scount[s[i+n]-'a']++;
if(scount==pcount){
res.push_back(i+1);
}
}
return res;
}
};

盛最多水的容器

https://leetcode.cn/problems/container-with-most-water/?envType=study-plan-v2&envId=top-100-liked

给定一个长度为 n 的整数数组 height 。有n 条垂线,第 i 条线的两个端点是(i, 0)(i, height[i])。找出其中的两条线,使得它们与 x轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。说明:你不能倾斜容器。

最多水的容器

1
2
3
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49

示例 2:

1
2
输入:height = [1,1]
输出:1

思路:

双指针来解决这个题,主要的思路就是首尾都放一个指针,然后依次向中间移动,向中间移动意味着x轴的长度变短那么需要高度要长,因此需要舍弃掉短的那边--或者++来滑动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int maxArea(vector<int>& height) {
int l = 0, r = height.size()-1;
int maxa = 0;
while(l<r){
int water = min(height[r], height[l]) * (r-l);
if(water>maxa){
maxa = water;
}
if(height[l]<height[r]){
l++;
}else{
r--;
}
}
return maxa;
}
};

接雨水

给定 n 个非负整数表示每个宽度为 1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

1
2
3
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

1
2
输入:height = [4,2,0,3,2,5]
输出: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
class Solution {
public:
int trap(vector<int>& height) {
if(height.size()<=2) return 0;
vector<int> maxLeft(height.size(), 0);
vector<int> maxRight(height.size(), 0);
int size = maxRight.size();
//记录每个柱子左边柱子的最大高度
maxLeft[0] = height[0];
for(int i=1;i<size;i++){
maxLeft[i] = max(height[i],maxLeft[i-1]);
}
//记录每个柱子右边柱子的最大高度
maxRight[size-1] = height[size-1];
for(int i=size-2;i>=0;i--){
maxRight[i] = max(height[i],maxRight[i+1]);
}
int sum=0;
for(int i=0;i<size;i++){
int count = min(maxLeft[i], maxRight[i])-height[i];
if(count > 0) sum+=count;
}
return sum;
}
};

柱形图中的最大矩形

https://leetcode.cn/problems/largest-rectangle-in-histogram/description/

最大矩形问题

1
2
3
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 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
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeft(heights.size());
vector<int> minRight(heights.size());
int size = heights.size();

//记录每个柱子左边第一个小于该柱子的下标
minLeft[0] = -1;
for(int i=1;i<size;i++){
int t= i-1;
while(t>=0&&heights[t]>=heights[i]) t=minLeft[t];
minLeft[i]=t;
}
//记录每个柱右边第一个小于该柱子的下标
minRight[size-1]=size;
for(int i=size -2;i>=0;i--){
int t=i+1;
while(t<size&&heights[t]>=heights[i]) t=minRight[t];
minRight[i]=t;
}

int res=0;
for(int i=0;i<size;i++){
int sum=heights[i]*(minRight[i]-minLeft[i]-1);
res = max(sum,res);
}

return res;
}
};

二叉树

二叉树的中序遍历

https://leetcode.cn/problems/binary-tree-inorder-traversal/description/?envType=study-plan-v2&envId=top-100-liked

给定一个二叉树的根节点 root ,返回 它的中序 遍历

1
2
输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

1
2
输入:root = []
输出:[]

示例 3:

1
2
输入:root = [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
/**
* 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 inorder(TreeNode* root, vector<int>& res){
if(!root){
return;
}
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right,res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int>res;
inorder(root, res);
return res;
}
};

二叉树的最大深度

https://leetcode.cn/problems/maximum-depth-of-binary-tree/description/?envType=study-plan-v2&envId=top-100-liked

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

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

思路:

利用递归和深度优先搜索来求解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 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 maxDepth(TreeNode* root) {
if(!root){
return 0;
}
return max(maxDepth(root->left), maxDepth(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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<iostream>
#include<vector>
using namespace std;

struct TreeNode{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(): val(0), left(nullptr), right(nullptr){}
TreeNode(int val): val(val), left(nullptr), right(nullptr)()
};

TreeNode* search(vector<int> nums){
TreeNode* root = new TreeNode(nums[0]);
for(int i=0;i<nums.size();i++){
TreeNode* n = new TreeNode(nums[i]);
TreeNode* cur = root;
while(cur!=nullptr){
if(nums[i]<cur->val){
if(cur->left==nullptr){cur->left = n; break;}
else(cur = cur->left;)
}else{
if(cur->right==nullptr){cur->right = n; break;}
else(cur = cur->right;)
}
}
}
return root;
}

void inorder(TreeNode* root){
if(root == nullptr) return;
inorder(root->left);
printf("%d ",root->val);
inorder(root->right);
}


int main(){
vector<int> nums=({2,3,4,5,2,1});
TreeNode* st = search(nums);
inorder(root);
return 0;
}

验证二叉搜索树

思路:对这个树进行中序遍历放进一个数组中如果是从大到小的顺序,那么就认为是二叉搜索树

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> nums;
void inorder(TreeNode* root){
if(root == nullptr){
return;
}
inorder(root->left);
nums.push_back(root->val);
inorder(root->right);
}
bool isValidBST(TreeNode* root) {
inorder(root);
for(int i = 1; i < nums.size(); i++){
if(nums[i] <= nums[i - 1]){
return false;
}
}
return true;
}
};

二叉搜索树中第K小的元素

思路:一样的思路,将二叉搜索树进行存储进一个一维的数组中,然后输出第k-1个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> nums;
void inorder(TreeNode* root){
if(root == nullptr){
return;
}
inorder(root->left);
nums.push_back(root->val);
inorder(root->right);
}
int kthSmallest(TreeNode* root, int k) {
inorder(root);
return nums[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
class Solution {
public:
//内部实现
TreeNode* build(vector<int>& preorder, vector<int>& inorder, int ps, int pe, int is, int ie){
if(ps >= pe || is >= ie){
return nullptr;
}
//找到第一个元素就是前序遍历的第一个元素
TreeNode* r = new TreeNode(preorder[ps]);
//find inorder partition
//找到中序遍历中的处于位置中间的那个元素
int i;
for(i = is; i < ie; i++){
if(inorder[i] == preorder[ps]){
break;
}
}
r->left = build(preorder, inorder, ps + 1, ps + 1 + (i - is), is, i);
r->right = build(preorder, inorder, ps + 1 + i - is, pe, i + 1, ie);
return r;
}
//最外层的调用
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
return build(preorder, inorder, 0, preorder.size(), 0, inorder.size());
}
};

深搜回溯

深度优先搜索的三部曲:

  1. 确定搜索函数的返回值以及搜索函数的参数分别是什么
  2. 确定每次找到叶子结点的终止条件
  3. 确定for单层搜索的逻辑,包含push,backtracking,pop

别忘了最开始的初始化步骤

组合问题

https://leetcode.cn/problems/combinations/description/

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

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

示例 1:

1
2
3
4
5
6
7
8
9
10
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

示例 2:

1
2
输入:n = 1, k = 1
输出:[[1]]

思路,使用深度优先搜索算法进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
private:
vector<vector<int>> result;
vector<int>path;
void backtracking(int n,int k, int startindex){
if(path.size()==k){
result.push_back(path);
return;
}
for(int i = startindex;i<=n;i++){
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k){
backtracking(n,k,1);
return result;
}
};

组合问题III

https://leetcode.cn/problems/combination-sum-iii/submissions/496823507/

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

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

示例:

1
2
3
4
5
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

思路:简单的深度优先搜索,但需要注意的是可以适当采用减枝操作和必要的时候添加sum变量进行记录

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 {
private:
vector<vector<int>> result;
vector<int> path;
void backtacking(int k, int n,int startindex, int sum){
if(path.size()==k){
if(sum == n) result.push_back(path);
return;
}
for(int i= startindex;i<=9;i++){
sum+=i;
path.push_back(i);
backtacking(k,n,i+1,sum);
sum-=i;
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtacking(k,n,1,0);
return result;
}
};

为了优化可以做一个剪枝操作

1
2
3
if (sum > targetSum) { // 剪枝操作
return;
}

电话号码组合问题

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

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

电话号码的按键

示例 :

1
2
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

这道题需要注意的地方是,首先第一步做好map字符的映射

第二步最关键是要写清楚回溯函数的参数可能包含index,就是第几位置的字符,同时需要区分backtracking函数的for循环的内容是相当于横向的遍历,而函数体内部的实现是纵向的遍历

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 {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> result;
string s;
void backtracking(const string digits,int index, string s){
if(digits.size()==0){
return;
}
if(index==digits.size()){
result.push_back(s);
return;
}
int digit = digits[index]-'0';
string letters = letterMap[digit];
for(int i= 0;i<letters.size();i++){
s.push_back(letters[i]);
backtracking(digits, index+1, s);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
backtracking(digits,0,"");
return result;
}
};

组合总和

https://leetcode.cn/problems/combination-sum/

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

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

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

示例 :

1
2
3
4
5
6
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7
仅有这两种组合。

思路:

题目最关键的点在于能重复使用元素但是不能重复元素的组合不能被重复输出

因此需要调整startindex的开始的位置是在backtracking(candidates,target,sum,i);注意,这个时候从i开始保证还能用到自己的元素重复使用,还有最重要的sort(candidates.begin(), candidates.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:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>&candidates, int target, int sum, int startindex){
if(sum==target){
res.push_back(path);
return;
}
for(int i=startindex;i<candidates.size();i++){
if(sum>target){
return;
}
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0,0);
return res;
}
};

组合总和II

https://leetcode.cn/problems/combination-sum-ii/description/

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

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

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

示例 :

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

思路:

这个地方最大的困难在于每个数字在每个组合中只能使用一次,同时集合中的元素存在重复的元素,那么这个时候有一个问题是如何才能对元素进行去重处理呢,就是让每个元素只能被使用一次

去重的操作就在于vector<bool> used(candidates.size(),false); sort(candidates.begin(), candidates.end());

首先需要在backtracking中定一个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
25
26
27
28
29
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startindex, vector<bool>used){
if(sum==target){
res.push_back(path);
return;
}
for(int i= startindex;i<candidates.size();i++){
if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false){
continue;
}
sum+=candidates[i];
used[i]=true;
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i+1, used);
used[i]=false;
path.pop_back();
sum-=candidates[i];
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(),false);
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return res;
}
};

分割回文串

https://leetcode.cn/problems/palindrome-partitioning/description/

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

回文串 是正着读和反着读都一样的字符串。

示例:

1
2
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

思路:

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:
vector<vector<string>> res;
vector<string> path;
bool ishuiwen(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 startindex){
if(startindex>=s.size()){
res.push_back(path);
return;
}
for(int i=startindex;i<s.size();i++){
if(ishuiwen(s,startindex,i)){
string str = s.substr(startindex, i-startindex+1);
path.push_back(str);
}else{
continue;
}
backtracking(s,i+1);
path.pop_back();
}
}
vector<vector<string>> partition(string s) {
backtracking(s,0);
return res;
}
};

复原IP地址

https://leetcode.cn/problems/restore-ip-addresses/description/

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

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

示例 :

1
2
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

思路:

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:
vector<string> res;
bool isvalid(string s, int start, int end){
if(start>end){
return false;
}
if(s[start]=='0'&&start!=end){
return false;
}
int num = 0;
for(int i=start;i<=end;i++){
if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
return false;
}
num=num*10+(s[i]-'0');
if(num>255){
return false;
}
}
return true;
}
void backtracking(string s, int startindex, int pointnum){
if(pointnum==3){
if (isvalid(s, startindex, s.size() - 1)) {
res.push_back(s);
}
return;
}
for(int i=startindex;i<s.size();i++){
if(isvalid(s,startindex,i)){
s.insert(s.begin()+i+1,'.');
pointnum++;
backtracking(s,i+2,pointnum);
pointnum--;
s.erase(s.begin()+i+1);
}else break;
}
}
vector<string> restoreIpAddresses(string s) {
backtracking(s,0,0);
return res;
}
};

子集

https://leetcode.cn/problems/subsets/description/

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

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

示例:

1
2
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

思路:

这道题比较简单,就是简单的遍历就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums,int startindex){
res.push_back(path);
for(int i=startindex;i<nums.size();i++){
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};

子集II

https://leetcode.cn/problems/subsets-ii/description/

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

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

示例:

1
2
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[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
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int> nums, int startindex, vector<bool> used){
res.push_back(path);

for(int i=startindex;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums,0,used);
return res;
}
};

非递减子序列

https://leetcode.cn/problems/non-decreasing-subsequences/description/

给你一个整数数组 nums,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素 。你可以按 任意顺序返回答案。数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

1
2
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

1
2
输入:nums = [4,4,3,2,1]
输出:[[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
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, int startindex){
if(path.size()>1){
res.push_back(path);
}
unordered_set<int> uset;
for(int i =startindex;i<nums.size();i++){
if ((!path.empty() && nums[i] < path.back())
|| uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};

全排列

https://leetcode.cn/problems/permutations/description/

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

示例 1:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

1
2
输入:nums = [0,1]
输出:[[0,1],[1,0]]

思路:

要求解全排列,因此回溯退出的条件是当path的长度和nums的长度一样的时候就达到了退出的条件

因为这道题没有重复的元素,求解全排列需要每次都从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
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool>& used){
if(path.size()==nums.size()){
res.push_back(path);
}
for(int i=0;i<nums.size();i++){
if(used[i]==true){
continue;
}
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
};

全排列II

https://leetcode.cn/problems/permutations-ii/

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

示例 1:

1
2
3
4
5
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

示例 2:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,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:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool>& used){
if(path.size()==nums.size()){
res.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
if(used[i]==false){
used[i]=true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i]=false;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
};

重新安排行程

https://leetcode.cn/problems/reconstruct-itinerary/description/

给你一份航线列表 tickets ,其中tickets[i] = [fromi, toi]表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。所有这些机票都属于一个从JFK(肯尼迪国际机场)出发的先生,所以该行程必须从JFK开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

安排行程

思路:

【困难】

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<string> res;
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketnum, vector<string>& res){
if(res.size()==ticketnum+1){
return true;
}
for(pair<const string, int>& target: targets[res[res.size()-1]]){
if(target.second>0){
res.push_back(target.first);
target.second--;
if(backtracking(ticketnum, res)) return true;
res.pop_back();
target.second++;
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {

for(const vector<string>& vec: tickets){
targets[vec[0]][vec[1]]++;
}
res.push_back("JFK");
backtracking(tickets.size(), res);

return res;
}
};

N皇后

https://leetcode.cn/problems/n-queens/description/

n 皇后问题 研究的是如何将 n个皇后放置在 n×n的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题的棋子放置方案,该方案中 'Q''.'分别代表了皇后和空位。

1
2
3
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

思路:

这道题关键在于用好数据结构和写好合法性的判断

关键在于定义好chessboard第二步是把合法性位置判断写好

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<vector<string>> res;
bool isvalid(int row, int col, vector<string>& chessboard,int n){
//检查列
for(int j=0;j<n;j++){
if(chessboard[row][j]=='Q') return false;
}
//检查行
for(int i=0;i<n;i++){
if(chessboard[i][col]=='Q') return false;
}
//检查对角线45
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
//检查对角线135
for(int i = row-1,j=col+1;i>=0&&j<n;i--,j++){
if(chessboard[i][j]=='Q'){
return false;
}
}
return true;
}
void backtracking(vector<string>& chessboard, int row, int n){
if(row==n)
{
res.push_back(chessboard);
return;
}
for(int col = 0;col<n;col++){
if(isvalid(row,col,chessboard,n)){
chessboard[row][col]='Q';
backtracking(chessboard,row+1,n);
chessboard[row][col]='.';
}
}
}

vector<vector<string>> solveNQueens(int n) {
std::vector<std::string> chessboard(n, std::string(n, '.'));
backtracking(chessboard,0,n);
return res;
}
};

解数独

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

1
2
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","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
35
36
37
38
39
40
class Solution {
public:
bool isvalid(vector<vector<char>>& board, int row, int col, char a){
//行遍历
for(int j=0;j<9;j++){
if(board[row][j]==a) return false;
}
//列遍历
for(int i=0;i<9;i++){
if(board[i][col]==a) return false;
}
//方框内判断:关键找到起始的方框对角
for(int i= (row/3)*3;i<(row/3)*3+3;i++){
for(int j=(col/3)*3; j<(col/3)*3+3;j++){
if(board[i][j]==a) return false;
}
}
return true;
}
bool backtracking(vector<vector<char>>& board){
for(int i=0;i<board.size();i++){
for(int j=0;j<board[0].size();j++){
if(board[i][j]=='.'){
for(char a='1';a<='9';a++){
if(isvalid(board,i,j,a)){
board[i][j]=a;
if(backtracking(board)) return true;
board[i][j]='.';
}
}
return false;
}
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};

贪心算法

分发饼干

https://leetcode.cn/problems/assign-cookies/description/

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子i,都有一个胃口值g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j,都有一个尺寸 s[j] 。如果s[j] >= g[i],我们可以将这个饼干 j分配给孩子 i,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 :

1
2
3
4
5
6
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 :

1
2
3
4
5
6
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

思路:

这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。

然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。

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(s.begin(),s.end());
sort(g.begin(),g.end());
int index=s.size()-1;
int num= 0;
for(int i=g.size()-1; i>=0;i--){
if(index>=0&&s[index]>=g[i]){
num++;
index--;
}
}
return num;
}
};

摆动序列

https://leetcode.cn/problems/wiggle-subsequence/description/

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

给你一个整数数组 nums ,返回 nums 中作为摆动序列最长子序列的长度

示例 :

1
2
3
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3)
1
2
3
4
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8)

思路:

本题异常情况的本质,就是要考虑平坡,平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图

同时需要注意的是在判断条件语句的时候,不能简单的用判断相乘法小于0作为判断,因为存在平坡的情况

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 wiggleMaxLength(vector<int>& nums) {
if(nums.size()<=1){
return nums.size();
}
int num=1;
vector<int> differ;
for(int i=1;i<nums.size();i++){
differ.push_back(nums[i]-nums[i-1]);
}
int preDiff=0;
for(int i=0;i<differ.size();i++){
if((preDiff<=0&& differ[i]>0)||(differ[i]<0&&preDiff>=0)){
num++;
preDiff = differ[i];
}
}
return num;
}
};

最大子数组和

https://leetcode.cn/problems/maximum-subarray/description/

给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例:

1
2
3
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

思路:

这道题使用的是局部的最优贪心的思路,如果遇到让总的值小于0,那么久立刻让总的值变成0,那么下一轮就从头开始记了,同时max会每一轮进行判断是否有比当前的最大值大,如果有那么就进行替换

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

买卖股票的最佳时机

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

给你一个整数数组 prices ,其中 prices[i]表示某支股票第 i天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多 只能持有 一股股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

1
2
3
4
5
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3
总利润为 4 + 3 = 7

思路:

把利润分解为每天为单位的维度,而不是从 0 天到第 3天整体去考虑!

那么根据 prices可以得到每天的利润序列:(prices[i] - prices[i - 1]).....(prices[1] - prices[0])

相当于是每天的利润之差和0的比较,只选择为正的值,负数的情况直接忽略

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for(int i=1;i<prices.size();i++){
res+=max(prices[i]-prices[i-1],0);
}
return res;
}
};

动态规划

状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。

对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

注意:动态规划的问题一般只会输出最后的一个结果,不会输出比如中间的路径等相关的值

斐波那契数列

https://leetcode.cn/problems/fibonacci-number/

斐波那契数 (通常用 F(n)表示)形成的序列称为 斐波那契数列 。该数列由01开始,后面的每一项数字都是前面两项数字的和。也就是:

1
2
F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

示例:

1
2
3
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

思路:

因为这道题给出了递推公式:F(n) = F(n - 1) + F(n - 2)

动规五部曲:

这里我们要用一个一维dp数组来保存递归的结果

  1. 确定dp数组以及下标的含义:dp[i]的定义为:第i个数的斐波那契数值是dp[i]

  2. 确定递推公式F(n) = F(n - 1) + F(n - 2)

  3. dp数组如何初始化

    1
    2
    dp[0] = 0;
    dp[1] = 1;
  4. 确定遍历顺序

    从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i- 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的

  5. 举例推导dp数组

1
2
3
4
5
6
7
8
9
10
11
12
13
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];
}
};

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬12个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

1
2
3
4
5
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1
2. 2

思路:

动态规划简单题,递推公式:dp[i] = dp[i-2]+dp[i-1];

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int climbStairs(int n) {
if(n<=2) return n;
vector<int> dp(n+1);
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i] = dp[i-2]+dp[i-1];
}
return dp[n];
}
};

最小费用爬楼梯

https://leetcode.cn/problems/min-cost-climbing-stairs/description/

给你一个整数数组 cost ,其中 cost[i]是从楼梯第 i个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为0 或下标为 1的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。

示例 1:

1
2
3
4
5
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15

思路:

动态规划可以有两个途径得到dp[i],一个是dp[i-1]一个是dp[i-2]

dp[i - 1] 跳到 dp[i] 需要花费dp[i - 1] + cost[i - 1]

dp[i - 2] 跳到 dp[i] 需要花费dp[i - 2] + cost[i - 2]

那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢?

一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n+1);
dp[0]= 0;
dp[1] = 0;
for(int i=2;i<=n;i++){
dp[i]=min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
}
return dp[n];
}
};

不同路径

一个机器人位于一个 m x n 网格的左上角(起始点在下图中标记为 “Start”)。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish” )。问总共有多少条不同的路径?

机器人路径问题

思路:

简单的动态规划问题,只需要保证每次迭代都从上面和左边进行叠加

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>>dp(m, vector<int>(n, 0));
for(int i=0;i<m;i++){
dp[i][0]=1;
}
for(int i=0;i<n;i++){
dp[0][i]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};

不同路径II

https://leetcode.cn/problems/unique-paths-ii/description/

一个机器人位于一个 m x n 网格的左上角(起始点在下图中标记为 “Start”)。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用10 来表示。

障碍物的机器人路径

思路:

和上一题的思路一样,都是需要遍历路径就行,但是这里加入了一个新的数组用来存储有障碍物的位置,因此需要额外进行标记&&obstacleGrid[i][0]==0的信息,同时遇到障碍物就不改变对应的值,直接continue就好

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

整数拆分

https://leetcode.cn/problems/integer-break/description/

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2),并使这些整数的乘积最大化。返回 你可以获得的最大乘积

示例 :

1
2
3
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

思路:

给出递推公式一个是j * (i - j)直接相乘。一个是j * dp[i - j],相当于是拆分(i - j),在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j取最大的。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
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];
}
};

香槟塔问题

我们把玻璃杯摆成金字塔的形状,其中 第一层1 个玻璃杯, 第二层2个,依次类推到第 100 层,每个玻璃杯 (250ml) 将盛有香槟。

从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。(当最底层的玻璃杯满了,香槟会流到地板上)

香槟塔问题

1
2
3
4
5
6
7
8
9
示例 1:
输入: poured(倾倒香槟总杯数) = 1, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.00000
解释: 我们在顶层(下标是(0,0))倒了一杯香槟后,没有溢出,因此所有在顶层以下的玻璃杯都是空的。

示例 2:
输入: poured(倾倒香槟总杯数) = 2, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.50000
解释: 我们在顶层(下标是(0,0)倒了两杯香槟后,有一杯量的香槟将从顶层溢出,位于(1,0)的玻璃杯和(1,1)的玻璃杯平分了这一杯香槟,所以每个玻璃杯有一半的香槟。

思路:

线性DP,令 pouredkquery_rowquery_glass分别为 nm

思路题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
double champagneTower(int poured, int query_row, int query_glass) {
double dp[110][110]={0};
dp[0][0]= (double) poured;
for(int i =0;i<=query_row;i++){
for(int j = 0;j<=i;j++){
if(dp[i][j]>1){
dp[i+1][j]+=(dp[i][j]-1)/2;
dp[i+1][j+1]+=(dp[i][j]-1)/2;
dp[i][j] = 1;
}
}
}
return dp[query_row][query_glass];
}
};

最大正方形

在一个由 '0''1'组成的二维矩阵内,找到只包含 '1'的最大正方形,并返回其面积。

示例 1:

image-20240310155051578

1
2
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4

示例 2:

1
2
输入:matrix = [["0","1"],["1","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:
int maximalSquare(vector<vector<char>>& matrix) {
if(matrix.size()==0||matrix[0].size()==0){return 0;}
int maxsize = 0;
int rows = matrix.size(), colums = matrix[0].size();
vector<vector<int>> dp(rows, vector<int>(colums));
for(int i= 0;i<rows;i++){
for(int j = 0;j<colums;j++){
if(matrix[i][j]=='1'){
if(i==0||j==0){dp[i][j]=1;}
else{
dp[i][j] = min(min(dp[i-1][j], dp[i][j-1]),dp[i-1][j-1])+1;
}
maxsize = max(maxsize, dp[i][j]);
}
}
}
return maxsize* maxsize;
}
};

戳气球

n 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的i - 1i + 1 代表和 i相邻的两个气球的序号。如果 i - 1i + 1超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:

1
2
3
4
5
输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167

思路:

这道题是一个区间DP的题目,下面的解题思路

https://leetcode.cn/problems/burst-balloons/solutions/337630/zhe-ge-cai-pu-zi-ji-zai-jia-ye-neng-zuo-guan-jian-/?envType=featured-list&envId=2cktkvj%3FenvType%3Dfeatured-list&envId=2cktkvj

关键点在于

DP的状态转移方程只和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<vector<int>> rec;
vector<int> val;
int solve(int left, int right){
if(left>=right-1) return 0;
if(rec[left][right]!=-1){return rec[left][right];}
for(int i=left+1;i<right;i++){
int sum = val[left]*val[i]* val[right];
sum+=solve(left,i)+solve(i, right);
rec[left][right] = max(rec[left][right], sum);
}
return rec[left][right];
}
int maxCoins(vector<int>& nums) {
n = nums.size();
val.resize(n+2);
for(int i = 1;i<=n;i++){
val[i] = nums[i-1];
}
val[0] = val[n+1] = 1;
rec.resize(n+2, vector<int>(n+2, -1));
return solve(0, n+1);
}
};

背包问题解题框架

0-1背包

dp[j]为容量为j的背包所背的最大价值,那么如何推导dp[j]呢?dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

dp[j - weight[i]] + value[i] 表示 容量为 j- 物品i重量 的背包 加上物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,

递推公式:

1
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

初始化:

全部初始化为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]);
}
}

整体的代码结构是

1
2
3
4
5
6
7
8
9
10
11
12
13
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
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]);
}
}
cout << dp[bagWeight] << endl;
}

完全背包问题

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

在代码层面的区别在于背包遍历的时候是从头开始到尾遍历,int j = weight[i]; j <= bagWeight; j++,因为所有的背包内部都是无限的

1、先遍历物品再遍历背包

1
2
3
4
5
6
7
8
9
10
11
12
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
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]);
}
}
cout << dp[bagWeight] << endl;
}

2、先遍历背包再遍历物品

1
2
3
4
5
6
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for(int i = 0; i < weight.size(); i++) { // 遍历物品
if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
cout << endl;
}

分割等和子集

https://leetcode.cn/problems/partition-equal-subset-sum/description/

给你一个只包含正整数非空 数组nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等示例:

1
2
3
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5][11]
1
2
3
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

思路:因为这个地方只需要写出是否能够划分,而不是具体的划分结果,因此可以考虑利用动态规划的方法来进行写,可以利用背包法来进行求解,具体步骤:

  1. 首先判断数组的和是否为偶数,是的话那么背包的大小就是总和取一半,不是的话就直接返回false
  2. 0-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:
bool canPartition(vector<int>& nums) {
int sum =0;
vector<int> dp(10001, 0);
for(int i =0;i< nums.size();i++){
sum+=nums[i];
}

if(sum%2==1) return false;
int target = sum/2;
for(int i=0;i<nums.size();i++){
for(int j=target; j>=nums[i];j--){
dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);
}
}

if(dp[target]==target) return true;
return false;

}
};

最后一块石头的重量

https://leetcode.cn/problems/last-stone-weight-ii/description/

有一堆石头,用整数数组 stones 表示。其中stones[i] 表示第 i块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为xy,且x <= y。那么粉碎的可能结果如下:

最后,最多只会剩下一块 石头。返回此石头最小的可能重量 。如果没有石头剩下,就返回0

1
2
3
4
5
6
7
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1]
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1]
组合 2 和 1,得到 1,所以数组转化为 [1,1,1]
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

思路:

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了本题物品的重量为stones[i],物品的价值也为stones[i]。对应着01背包里的物品重量weight[i]和物品价值value[i]。

这道题的关键在于如何将这堆石头尽可能平均的划分成两堆(只有这样才能让剩下的石头的重量最小),大小就是总和除以2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int> dp(15001, 0);
int sum = 0;
for(int i =0; i<stones.size();i++){
sum+=stones[i];
}
int target = sum/2;
for(int i = 0;i<stones.size();i++){
for(int j= target;j>=stones[i];j--){
dp[j] = max(dp[j], dp[j-stones[i]]+stones[i]);
}
}
return sum - dp[target]- dp[target];
}
};

目标和

https://leetcode.cn/problems/target-sum/description/

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-',然后串联起所有整数,可以构造一个 表达式

返回可以通过上述方法构造的、运算结果等于 target 的不同表达式 的数目。

1
2
3
4
5
6
7
8
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,装满容量为x的背包,有几种方法

这里的x,就是bagSize,也就是我们后面要求的背包容量。

大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响

动态规划:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int i=0;i<nums.size();i++) sum+=nums[i];
if(abs(target)>sum) return 0;
if((target+sum)%2 == 1) return 0;
int bagsize = (target+sum)/2;
vector<int> dp(bagsize+1,0);
dp[0]=1;
for(int i=0;i<nums.size();i++){
for(int j=bagsize;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[bagsize];
}
};

一和零

https://leetcode.cn/problems/ones-and-zeroes/description/

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中最多m0n1

如果 x 的所有元素也是 y 的元素,集合x 是集合 y子集

1
2
3
4
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5031 的最大子集是 {"10","0001","1","0"} ,因此答案是 4
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 41 ,大于 n 的值 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for(string str: strs){
int onenum=0, zeronum=0;
for(char c :str){
if(c=='0')zeronum++;
else onenum++;
}
for(int i=m;i>=zeronum;i--){
for(int j=n; j>=onenum; j--){
dp[i][j] = max(dp[i][j], dp[i-zeronum][j- onenum]+1);
}
}
}
return dp[m][n];
}
};

零钱兑换II

https://leetcode.cn/problems/coin-change-ii/description/

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回0

假设每一种面额的硬币有无限个。

1
2
3
4
5
6
7
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

思路:

完全背包问题

注意在用完全背包问题的时候,遍历背包这个地方是从++开始,就是从coins[i]开始遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1, 0);
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];
}
};

组合总结IV

https://leetcode.cn/problems/combination-sum-iv/description/

给你一个由 不同 整数组成的数组 nums,和一个目标整数 target 。请你从 nums中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

1
2
3
4
5
6
7
8
9
10
11
12
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

思路:

题目中说这个问题每个元素可以被用好多次,因此想到完全背包问题

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

本题中的结果是需要考虑到不同数字之间的排列顺序的,所以这个地方要用到排列,先背包再物品

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

零钱兑换

https://leetcode.cn/problems/coin-change/description/

给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

1
2
3
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

确定dp数组以及下标的含义,dp[j]:凑足总额为j所需钱币的最少个数为dp[j]

确定递推公式,凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]dp[j - coins[i]] + 1就是dp[j](考虑coins[i])

所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。

递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);

完全背包问题,且不是排列问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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], dp[j-coins[i]]+1);
}
}
}
if(dp[amount]==INT_MAX) return -1;
return dp[amount];
}
};

完全平方数

https://leetcode.cn/problems/perfect-squares/

给你一个整数 n ,返回 和为 n的完全平方数的最少数量

完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311不是。

1
2
3
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

思路:

完全背包问题,注意这个地方求解的是最小数量,因此初始化的时候是用INT_MAX来进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
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++){
dp[j]= min(dp[j], dp[j-i*i]+1);
}
}
return dp[n];
}
};

打家劫舍

https://leetcode.cn/problems/house-robber/description/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

1
2
3
4
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4

思路:

当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。

递推公式为: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
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[1],nums[0]);
for(int i=2;i<nums.size();i++){
dp[i]=max(dp[i-2]+nums[i], dp[i-1]);
}
return dp[nums.size()-1];
}
};

打家劫舍II

https://leetcode.cn/problems/house-robber-ii/description/

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,今晚能够偷窃到的最高金额。

1
2
3
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

思路:

如果是一个环的情况,那么需要考虑的是如何转换,既然是首尾相连的情况,那么注意的是分成两种情况,第一种是指考虑开头和倒数第二个,第二种是指考虑第二个到最后一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
int res1 = robrange(nums, 0, nums.size()-2);
int res2 = robrange(nums, 1, nums.size()-1);
return max(res1, res2);
}
int robrange(vector<int>& nums, int start, int end){
if(end==start) return nums[start];
vector<int> dp(nums.size());
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-2]+nums[i], dp[i-1]);
}
return dp[end];
}
};

买卖股票的最佳时机

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/

给定一个数组 prices ,它的第 i 个元素prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0

1
2
3
4
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

思路:

贪心

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 res = 0;
for(int i =0;i<prices.size(); i++){
low = min(low, prices[i]);
res = max(res, prices[i]-low);
}
return res;
}
};

动态规划

1

乘积最大子数组

https://leetcode.cn/problems/maximum-product-subarray/description/

给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列。

1
2
3
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

思路:

因为是乘积的问题,所以会存在如果某个值特别小或者是特别大再负负得正之后仍然会出现最大的情况,因此我们在用动态规划记录的时候不仅要记录最大值还需要记录最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int maxProduct(vector<int>& nums) {
vector<int> maxp(nums);
vector<int> minp(nums);
int res = nums[0];
for(int i=1;i<nums.size();i++){
maxp[i]=max(maxp[i-1]*nums[i], max(nums[i], minp[i-1]*nums[i]));
res = max(maxp[i], res);
minp[i]=min(minp[i-1]*nums[i], min(nums[i], maxp[i-1]*nums[i]));
}
return res;
}
};

最长有效括号

https://leetcode.cn/problems/longest-valid-parentheses/description/

给你一个只包含 '('')'的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

1
2
3
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

示例 2:

1
2
3
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

思路:

动态规划,分两类进行讨论,分别考虑最后结尾的元素是不是')'再分成两类进行讨论

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

最长连续递增序列

https://leetcode.cn/problems/longest-continuous-increasing-subsequence/

给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个l <= i < r,都有nums[i] < nums[i + 1] ,那么子序列[nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]就是连续递增子序列。

示例 1:

1
2
3
4
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

示例 2:

1
2
3
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

思路:

简单的遍历模拟并用dp数组来存储到目前为止的最长递增序列的长度,本质上也是一种贪心

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if(nums.size()<=1) return nums.size();
vector<int> dp(nums.size(),1);
int s =1;
for(int i =1;i<nums.size();i++){
if(nums[i-1]<nums[i]){dp[i] = dp[i-1]+1;}
s = max(s,dp[i]);
}
return s;
}
};

无重复字符的最长子串

https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/

给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

思路:

滑动窗口法,用一个left来记录对应的左侧没有出现重复的元素,一直进行erase()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size()<=1) return s.size();
std::unordered_set<char> lookup;
int maxstr = 0;
int left = 0;
for(int i=0;i<s.size();i++){
while(lookup.find(s[i])!=lookup.end()){
lookup.erase(s[left]);
left++;
}
maxstr = max(maxstr, i-left+1);
lookup.insert(s[i]);
}
return maxstr;
}
};

最长递增子序列

https://leetcode.cn/problems/longest-increasing-subsequence/description/

给你一个整数数组 nums,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

1
2
3
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4

思路:

既然涉及到非连续的子序列,那么在处理的时候就需要用两个指针来标记两个位置遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()<=1) return nums.size();
vector<int> dp(nums.size(),0);
for(int i =0;i<nums.size();i++){
dp[i]=1;
for(int j = 0;j<i;j++){
if(nums[j]<nums[i]){
dp[i] = max(dp[i], dp[j]+1);
}
}
}
return *max_element(dp.begin(), dp.end());;
}
};

最长重复子数组

https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/

给两个整数数组 nums1nums2 ,返回两个数组中 公共的 、长度最长的子数组的长度

示例 1:

1
2
3
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1]

思路:

在比较i,j对的时候,当然希望能够利用到前面的i-1和j-1的结果,因此用二维的动态规划来提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0));
int ans = 0;
for(int i= nums1.size()-1;i>=0;i--){
for(int j= nums2.size()-1;j>=0;j--){
dp[i][j] = nums1[i]==nums2[j]?dp[i+1][j+1]+1:0;
ans = max(ans, dp[i][j]);
}
}
return ans;
}
};

回文子串

https://leetcode.cn/problems/palindromic-substrings/description/

给你一个字符串 s ,请你统计并返回这个字符串中回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

1
2
3
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

1
2
3
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

思路:

计算有多少个回文子串的最朴素方法就是枚举出所有的回文子串,而枚举出所有的回文字串又有两种思路,分别是:

因此我们这里可以用中心拓展法来解决这个问题

中心拓展问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int countSubstrings(string s) {
int n = s.size(), ans = 0;
for(int i=0;i<2*n-1;i++){
int l = i/2, r=i/2+i%2;
while(l>=0 && r<n && s[l]==s[r]){
--l;
++r;
++ans;
}
}
return ans;
}
};

最长公共子序列

https://leetcode.cn/problems/longest-common-subsequence/

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列的长度。如果不存在 公共子序列 ,返回 0。一个字符串的 子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

示例 1:

1
2
3
输入:text1 = "abcde", text2 = "ace" 
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3

示例 2:

1
2
3
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3

思路:

使用动态规划进行求解,其中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 longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector<vector<int>> dp(m+1, vector<int>(n+1));
for(int i=1;i<=m;i++){
char c1 = text1.at(i-1);
for(int j=1; j<=n; j++){
char c2 = text2.at(j-1);
if(c1==c2){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
};

编辑距离

https://leetcode.cn/problems/edit-distance/description/

给你两个单词 word1word2请返回将word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

示例 1:

1
2
3
4
5
6
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

思路:

题目给定了两个单词,设为 A 和 B,这样我们就能够六种操作方法。

但我们可以发现,如果我们有单词 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 的一个字符。

我们用 D[i][j] 表示 A 的前 i个字母和 B 的前 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
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size();
int m = word2.size();

//其中有一个子字符串为空字符串
if (n*m==0) return n+m;

//DP数组
vector<vector<int>> dp(n+1, vector<int>(m+1));

//边界状态初始化
for(int i =0;i<n+1;i++){
dp[i][0] = i;
}
for(int j =0;j<m+1;j++){
dp[0][j] = j;
}

//循环计算DP
for(int i=1; i<n+1;i++){
for(int j=1; j<m+1; j++){
int left = dp[i-1][j]+1;
int down = dp[i][j-1]+1;
int left_down = dp[i-1][j-1];
if(word1[i-1]!=word2[j-1]) left_down +=1;
dp[i][j] = min(left, min(down, left_down));
}
}
return dp[n][m];
}
};

正则表达式匹配

https://leetcode.cn/problems/regular-expression-matching/description/

给你一个字符串 s 和一个字符规律p,请你来实现一个支持 '.''*'的正则表达式匹配。

所谓匹配,是要涵盖 整个 字符串s的,而不是部分字符串。

示例 1:

1
2
3
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

1
2
3
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

1
2
3
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

思路:

  1. 定义状态: 创建一个二维数组 dp,其中dp[i][j] 表示 s 的前 i 个字符与p 的前 j 个字符是否匹配。
  2. 初始化:
    1. dp[0][0]true,因为两个空字符串是匹配的。
    2. 初始化 dp[0][j],处理模式 p 的前j 个字符与空字符串 s 的匹配情况。这通常依赖于'*' 字符,因为 '*' 可以表示重复前面的字符 0次。
  3. 状态转移方程:
    1. s[i-1] == p[j-1]p[j-1] == '.'时,dp[i][j] = dp[i-1][j-1]
    2. p[j-1] == '*' 时,需要分两种情况考虑:
      • 如果 p[j-2] 不匹配 s[i-1],则'*' 表示它前面的字符出现 0次,dp[i][j] = dp[i][j-2]
      • 如果 p[j-2] 匹配s[i-1],则存在多种可能性('*'表示前面的字符出现 0 次、1次或多次),dp[i][j] = dp[i][j-2] || dp[i][j-1] || dp[i-1][j]
  4. 循环顺序: 从左到右,从上到下遍历 dp数组。
  5. 返回结果: dp[len(s)][len(p)] 表示整个sp 是否匹配

栈&队列&单调栈

有效括号

https://leetcode.cn/problems/valid-parentheses/description/

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

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

示例 1:

1
2
输入:s = "()"
输出:true

思路:简单的判断栈

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 ismatch(char a, char b){
if((a==')'&&b=='(')||(a=='}'&&b=='{')||(a==']'&&b=='[')){
return true;
}else{
return false;
}
}
bool isValid(string s) {
stack<char>st;
if(s.size()%2!=0) return false;
for(int i=0;i<s.size();i++){
if(s[i]==')'||s[i]=='}'||s[i]==']'){
if(!st.empty()&&ismatch(s[i],st.top())){
st.pop();
}else{
return false;
}
}else{
st.push(s[i]);
}
}
return st.empty();
}
};

删除相邻重复

示例:

1
2
3
4
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"

https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
string removeDuplicates(string s) {
stack<char>st;
for(char c:s){
if(st.empty()||c!=st.top()){
st.push(c);
}else{
st.pop();
}
}
string res="";
while(!st.empty()){
res+=st.top();
st.pop();
}
reverse(res.begin(), res.end()); //注意这个地方需要反转一下字符串
return res;
}
};

逆波兰表达式求值

https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。

示例 1:

1
2
3
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

1
2
3
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

思路:

利用栈的思想模拟运算的过程,按照tokens的顺序将数字放进栈中,遇到运算符那么就拿出栈中的数字,运算结束之后再放进栈中

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 evalRPN(vector<string>& tokens) {
stack<int>st;
for(int i=0;i<tokens.size();i++){
if(tokens[i]!="/"&&tokens[i]!="+"&&tokens[i]!="-"&&tokens[i]!="*"){
st.push(stoi(tokens[i]));
}else{
int num1 = st.top();
st.pop();
int num2 = st.top();
st.pop();
if (tokens[i] == "+") st.push(num2 + num1);
if (tokens[i] == "-") st.push(num2 - num1);
if (tokens[i] == "*") st.push(num2 * num1);
if (tokens[i] == "/") st.push(num2 / num1);
}
}
int result = st.top();
return result;
}
};

前K个高频元素

https://leetcode.cn/problems/top-k-frequent-elements/description/

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

示例 1:

1
2
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

前k个高频元素的思路图

这道题目主要涉及到如下三块内容:

  1. 要统计元素出现频率
  2. 对频率排序
  3. 找出前K个高频元素

首先统计元素出现的频率,这一类的问题可以使用map来进行统计。

然后是对频率进行排序,这里我们可以使用一种容器适配器就是优先级队列

优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的completebinary 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
36
class Solution {
public:
class mycomparison{
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs){
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
//统计元素出现的频率
unordered_map<int, int>map;
for(int i=0;i<nums.size();i++){
map[nums[i]]++;
}

//对频率进行排序
//定义一个小顶堆,大小为k
priority_queue<pair<int,int>, vector<pair<int, int>>, mycomparison> pri_que;

//用固定大小为k的小顶堆,扫面所有频率的数值
for(unordered_map<int, int>::iterator it=map.begin(); it!=map.end(); it++){
pri_que.push(*it);
if(pri_que.size()>k){
pri_que.pop();
}
}

//找出前面k个高频元素,因为小顶堆先弹出的是最小的,所以倒序输出到数组
vector<int> res(k);
for(int i=k-1;i>=0;i--){
res[i]=pri_que.top().first;
pri_que.pop();
}
return res;
}
};

每日温度

给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer ,其中answer[i] 是指对于第 i天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用0 来代替。

示例 1:

1
2
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

思路:

可以选择使用单调栈的方法来求解,具体的思路是设置一个栈,遍历数组的时候和栈顶元素进行比较,小于栈顶元素的时候就需要将当前元素放入栈中

首先这道题必须有一个向量数组来存储对应位置的元素的值,vector<int> res(temperatures.size(),0)方便修改对应的元素

如果大于当前的栈顶元素的值,那么就要进行比较while循环,只要还是大于当前栈顶的元素都需要对栈顶的元素进行pop()

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> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
vector<int> res(temperatures.size(),0);
st.push(0);
for(int i=1;i<temperatures.size();i++){
if(temperatures[i]<=temperatures[st.top()]){
st.push(i);
}else{
while (!st.empty()&& temperatures[i]>temperatures[st.top()]){
res[st.top()]=i-st.top();
st.pop();
}
st.push(i);
}
}
return res;
}
};

图论

深度优先搜索理论

深度优先搜索和回溯的思路大体上是一样的

就地递归函数的下面,例如如下代码:

1
2
3
4
5
void dfs(参数) {
处理节点
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}

可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的

深度优先搜索的三部曲:

  1. 确认递归函数,参数

    深搜需要二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。

  2. 确认终止条件

    1
    2
    3
    4
    if (终止条件) {
    存放结果;
    return;
    }

    终止添加不仅是结束本层递归,同时也是我们收获结果的时候。

  3. 处理目前搜索节点出发的路径

一般这里就是一个for循环的操作,去遍历 目前搜索节点所能到的所有节点。

1
2
3
4
5
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}

广度优先搜索理论

广搜的搜索方式就适合于解决两个点之间的最短路径问题。

因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。

广度优先搜索代码模版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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>>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; //立刻标记,避免重复访问
}
}
}
}

所有可能的路径

https://leetcode.cn/problems/all-paths-from-source-to-target/description/

图路径遍历

给你一个有 n 个节点的有向无环图(DAG),请你找出所有从节点 0到节点 n-1的路径并输出(不要求按特定顺序graph[i]是一个从节点 i 可以访问的所有节点的列表(即从节点i 到节点 graph[i][j]存在一条有向边)。

1
2
3
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 30 -> 2 -> 3

思路:

深度优先搜索

注意在用dfs做题的时候需要初始化path.push_back(0)每一次都需要初始化输入这个数值起点

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<vector<int>> res;
vector<int> path;
void dfs(vector<vector<int>>& graph, int x){
if(x == graph.size()-1){
res.push_back(path);
return;
}
for(int i=0;i<graph[x].size();i++){
path.push_back(graph[x][i]);
dfs(graph,graph[x][i]);
path.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
path.push_back(0);
dfs(graph, 0);
return res;
}
};

岛屿数量

https://leetcode.cn/problems/number-of-islands/description/

给你一个由 '1'(陆地)和'0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

示例 :

1
2
3
4
5
6
7
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1

深度优先搜索版本:

思路在于利用dfs来对岛屿中的数量进行标记是否能visited,必须是联通的才能继续标记为res++

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 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 nextx = x +dir[i][0];
int nexty = y +dir[i][1];
if(nextx<0||nexty>grid.size()||nexty<0||nexty>grid[0].size) continue;
if(!visited[nextx][nexty] && grid[nextx][nexty]=='1'){
visited[nextx][nexty] = true;
dfs(grid, visited, nextx, nexty);
}
}
}
int numIslands(vector<vector<char>>& grid){
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for(int i=0;i< n;i++){
for(int j=0;j<m;j++){
if(!visited[i][j]&&grid[i][j]=='1'){
visited[i][j]=true;
res++;
dfs(grid, visited, i, j);
}
}
}
return res;
}
};

广度优先搜索版本

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 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>>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||nextt>grid[0].size()) continue;
if(!visited[nextx][nexty]&&grid[nextx][nexty]=='1'){
que.push({nextx,nexty});
visited[nextx][nexty] = true;
}
}
}
}
int numIslands(vector<vector<char>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(!visited[i][j]&&gird[i][j]=='1'){
res++;
bfs(grid, visited, i, j);
}
}
}
return res;
}
};

岛屿的最大面积

https://leetcode.cn/problems/max-area-of-island/description/

给你一个大小为 m x n 的二进制矩阵 grid

岛屿 是由一些相邻的 1 (代表土地)构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直的四个方向上 相邻。你可以假设grid 的四个边缘都被 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
class Solution {
public:
int count;
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 nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
if(!visited[nextx][nexty]&& grid[nextx][nexty]==1){
visited[nextx][nexty]=true;
count++;
dfs(grid, visited, nextx, nexty);
}
}
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int n = grid.size(), m=grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!visited[i][j] && grid[i][j] == 1) {
count = 1;
visited[i][j] = true;
dfs(grid, visited, i, j);
res = max(res, count);
}
}
}
return res;
}
};

广度优先搜索

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 Solution {
public:
int count = 0;
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
void bfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y){
queue<int> que;
que.push(x);
que.push(y);
visited[x][y]=true;
count++;
while(!que.empty()){
int xx = que.front();que.pop();
int yy = que.front();que.pop();
for(int i=0;i<4;i++){
int nextx = xx+dir[i][0];
int nexty = yy+dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
if(!visited[nextx][nexty]&& grid[nextx][nexty]==1){
count++;
visited[nextx][nexty]=true;
que.push(nextx);
que.push(nexty);
}
}
}
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int n= grid.size(), m=grid[0].size();
vector<vector<bool>>visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(!visited[i][j]&&grid[i][j]==1){
count =0;
bfs(grid, visited, i, j);
res = max(res , count);
}
}
}
return res;
}
};

飞地的数量

https://leetcode.cn/problems/number-of-enclaves/description/

给你一个大小为 m x n 的二进制矩阵 grid,其中 0 表示一个海洋单元格、1表示一个陆地单元格。一次 移动是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过grid 的边界。返回网格中 无法在任意次数的移动中离开网格边界的陆地单元格的数量。

飞地的数量

思路:

利用dfs或者是bfs的方法先把边界周围的岛屿全部去除为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
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){
grid[x][y] =0;
count++;
for(int i= 0;i<4;i++){
int nextx = x +dir[i][0];
int nexty = y +dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
if(grid[nextx][nexty]==0) continue;

dfs(grid, nextx, nexty);
}
}
int numEnclaves(vector<vector<int>>& grid) {
int n = grid.size(), m=grid[0].size();
for(int i=0;i<n;i++){
if(grid[i][0]==1) dfs(grid, i, 0);
if(grid[i][m-1]==1) dfs(grid, i, m-1);
}
for(int j=0;j<m;j++){
if(grid[0][j]==1) dfs(grid, 0, j);
if(grid[n-1][j]==1) dfs(grid, n-1, j);
}

count = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]==1) dfs(grid, i, j);
}
}
return count;
}
};

被围绕的区域

https://leetcode.cn/problems/surrounded-regions/description/

给你一个 m x n 的矩阵 board ,由若干字符'X''O' ,找到所有被 'X'围绕的区域,并将这些区域里所有的 'O''X'填充。

被围绕的区域

思路:

利用深度优先搜索的方法先将边界的联通区域全部设置为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
27
28
29
30
31
32
33
34
35
class Solution {
public:
int dir[4][2]={0,1,1,0,-1,0,0,-1};
void dfs(vector<vector<char>>& board, int x, int y){
board[x][y]='A';
for(int i=0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx<0||nextx>=board.size()||nexty<0||nexty>=board[0].size()) continue;
if(board[nextx][nexty]=='X'||board[nextx][nexty]=='A') continue;
dfs(board, nextx, nexty);
}
return;
}
void solve(vector<vector<char>>& board) {
int n= board.size(), m=board[0].size();
for(int i=0;i<n;i++){
if(board[i][0]=='O') dfs(board, i,0);
if(board[i][m-1]=='O') dfs(board, i, m-1);
}

for(int j=0;j<m;j++){
if(board[0][j]=='O') dfs(board, 0, j);
if(board[n-1][j]=='O') dfs(board, n-1, j);
}

for(int i =0; i<n ;i++){
for(int j=0;j<m;j++){
if(board[i][j]=='O') board[i][j]='X';;
if(board[i][j]=='A') board[i][j]='O';
}
}

}
};

太平洋大西洋水流问题

https://leetcode.cn/problems/pacific-atlantic-water-flow/description/

有一个 m × n 的矩形岛屿,与 太平洋大西洋 相邻。 “太平洋”处于大陆的左边界和上边界,而 “大西洋”处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个m x n 的整数矩阵 heightsheights[r][c] 表示坐标 (r, c) 上单元格高于海平面的高度

岛上雨水较多,如果相邻单元格的高度 小于或等于当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回网格坐标 result2D 列表 ,其中result[i] = [ri, ci] 表示雨水从单元格 (ri, ci)流动 既可流向太平洋也可流向大西洋

太平洋大西洋水流问题

思路:

这道题本质上是在考虑连通图能不能到太平洋和大西洋,给出两个函数,dfs是对每一个点能够到的位置进行标记,isResult是遍历每一个节点是不是能够通过联通到达太平洋和大西洋

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
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
void dfs(vector<vector<int>>& heights, vector<vector<bool>>& visited, int x, int y){
if (visited[x][y]) return;
visited[x][y] = true;
for(int i =0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx<0||nextx>=heights.size()||nexty<0||nexty>=heights[0].size()) continue;
if (heights[x][y] < heights[nextx][nexty]) continue;

dfs(heights, visited, nextx, nexty);

}
}
bool isResult(vector<vector<int>>& heights, int x, int y){
vector<vector<bool>> visited =vector<vector<bool>>(heights.size(), vector<bool>(heights[0].size(), false));
// dfs选择当前这个点能到达的全部点
dfs(heights, visited, x, y);
bool isPacific = false;
bool isAtlantic = false;
for(int j=0;j<heights[0].size();j++){
if(visited[0][j]){
isPacific = true;
break;
}
}
for(int i=0;i<heights.size();i++){
if(visited[i][0]){
isPacific = true;
break;
}
}

for(int j=0;j<heights[0].size();j++){
if(visited[heights.size()-1][j]){
isAtlantic = true;
break;
}
}

for(int i =0;i<heights.size();i++){
if(visited[i][heights[0].size()-1]){
isAtlantic = true;
break;
}
}
if (isAtlantic && isPacific) return true;
return false;
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
vector<vector<int>> result;
// 遍历每一个点,看是否能同时到达太平洋和大西洋
for (int i = 0; i < heights.size(); i++) {
for (int j = 0; j < heights[0].size(); j++) {
if (isResult(heights, i, j)) result.push_back({i, j});
}
}
return result;

}
};

最大人工岛

https://leetcode.cn/problems/making-a-large-island/description/

给你一个大小为 n x n 二进制矩阵 grid最多 只能将一格 0 变成 1

返回执行此操作后,grid 中最大的岛屿面积是多少?

岛屿 由一组上、下、左、右四个方向相连的1 形成。

示例 1:

1
2
3
输入: grid = [[1, 0], [0, 1]]
输出: 3
解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。

示例 2:

1
2
3
输入: grid = [[1, 1], [1, 0]]
输出: 4
解释: 将一格0变成1,岛屿的面积扩大为 4

示例 3:

1
2
3
输入: grid = [[1, 1], [1, 1]]
输出: 4
解释: 没有0可以让我们变成1,面积依然为 4

思路:

当完成编号之后,第二步骤就是对每个没有编号的节点进行相邻岛屿的面积遍历增加

注意这一步完成之后要对每个不同岛屿的面积数量进行记录,这个地方可以用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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Solution {
public:
int count;
int dir[4][2]={0,1,1,0,-1,0,0,-1};
//第一步dfs对地图已经有的岛屿数量进行标记
void dfs(vector<vector<int>>&grid, vector<vector<bool>>& visited, int x, int y, int mark){
if(visited[x][y]||grid[x][y]==0) return;
visited[x][y]=true;
grid[x][y] = mark;
count++;
for(int i=0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y + dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
dfs(grid, visited, nextx, nexty, mark);
}
}
int largestIsland(vector<vector<int>>& grid) {
int n =grid.size(), m=grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
unordered_map<int, int>gridNum;
int mark = 2; //对每个岛屿进行编号
bool isALLgrid = true;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]==0) isALLgrid = false;
if(!visited[i][j]&&grid[i][j]==1){
count = 0;
dfs(grid, visited, i, j, mark);
gridNum[mark] = count; //利用map来对所有岛屿的信息进行编号并记录
mark++;
}
}
}
if (isALLgrid) return n*m; // 如果全部是岛屿的情况,那么这个时候就不需要增加新的面积

//以下的逻辑是对需要增添岛屿的情况进行记录
int res = 0;
unordered_set<int> visitedGrid; //标记访问过的岛屿
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
int count =1;
visitedGrid.clear(); //每次都需要将这个已经访问过的岛屿进行清除
if(grid[i][j]==0){
for(int k=0;k<4;k++){
int neari = i+dir[k][0];
int nearj = j+dir[k][1];
if(neari<0||nearj<0||neari>=grid.size()||nearj>=grid[0].size()) continue;
if(visitedGrid.count(grid[neari][nearj])) continue;
count+=gridNum[grid[neari][nearj]];
visitedGrid.insert(grid[neari][nearj]);
}
}
res = max(res, count);
}
}
return res;
}
};

单词接龙

https://leetcode.cn/problems/word-ladder/description/

字典 wordList 中从单词 beginWordendWord转换序列是一个按下述规格形成的序列beginWord -> s1 -> s2 -> ... -> sk

给你两个单词 beginWordendWord和一个字典 wordList ,返回 beginWordendWord最短转换序列 中的单词数目 。如果不存在这样的转换序列,返回0

示例 1:

1
2
3
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5

示例 2:

1
2
3
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
输出:0
解释:endWord "cog" 不在字典中,所以无法进行转换。

思路:

这道题要解决两个问题:

首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个,所以判断点与点之间的关系,要自己判断是不是差一个字符,如果差一个字符,那就是有链接。

然后就是求起点和终点的最短路径长度,这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。

本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路。而广搜只要达到终点,一定是最短路。

广度优先搜索一定是能找到最短路径

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) {
//将vector转化成unordered_set提高查询速度
unordered_set<string> wordSet(wordList.begin(), wordList.end());
// 如果endWorld没有在worldSet里面出现,直接返回0
if(wordSet.find(endWord)==wordSet.end()) return 0;

unordered_map<string, int>visitMap;
queue<string> que;
que.push(beginWord);
visitMap.insert(pair<string, int>(beginWord, 1)); //map是用于记录路径的长度

while(!que.empty()){
string word = que.front();
que.pop();
int path = visitMap[word]; //这个word的路径长度
for(int i=0;i<word.size();i++){
string newWord = word;
for(int j=0;j<26;j++){
newWord[i]=j+'a';
if(newWord ==endWord) return path+1;

if(wordSet.find(newWord)!=wordSet.end()&& visitMap.find(newWord)==visitMap.end()){
visitMap.insert(pair<string, int>(newWord, path+1));
que.push(newWord);
}
}
}
}
return 0;
}
};

钥匙和房间

https://leetcode.cn/problems/keys-and-rooms/description/

n 个房间,房间按从 0n - 1 编号。最初,除 0号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。

当你进入一个房间,你可能会在里面找到一套不同的钥匙,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。

给你一个数组 rooms 其中 rooms[i] 是你进入i 号房间可以获得的钥匙集合。如果能进入所有 房间返回 true,否则返回false

示例 1:

1
2
3
4
5
6
7
8
输入:rooms = [[1],[2],[3],[]]
输出:true
解释:
我们从 0 号房间开始,拿到钥匙 1
之后我们去 1 号房间,拿到钥匙 2
然后我们去 2 号房间,拿到钥匙 3
最后我们去了 3 号房间。
由于我们能够进入每个房间,我们返回 true

示例 2:

1
2
3
输入:rooms = [[1,3],[3,0,1],[2],[0]]
输出:false
解释:我们不能进入 2 号房间。

思路:

这是在找一个有向图,如果出现有孤立的问题那么就不能开门进入,一定要全部连通,除此之外还需要注意的是这个是一个有向图的问题,还需要注意方向

有向量但不连通

因此需要用DFS来求解

其实本质上就是在利用深度优先搜索来进行遍历查找,如果能找到的话那么就进行标记,最后再进行筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
void dfs(const vector<vector<int>>& rooms, int key, vector<bool>& visited){
if(visited[key]) return;
visited[key] = true;
vector<int> keys = rooms[key];
for(int key :keys){
dfs(rooms, key, visited);
}
}
bool canVisitAllRooms(vector<vector<int>>& rooms) {
vector<bool> visited(rooms.size(),false);
dfs(rooms, 0, visited);
for(int i:visited){
if(i==false) return false;
}
return true;
}
};

岛屿的周长

https://leetcode.cn/problems/island-perimeter/description/

给定一个 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
class Solution {
public:
int dir[4][2]={0,1,1,0,-1,0,0,-1};
int islandPerimeter(vector<vector<int>>& grid) {
int res = 0;
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 x= i+dir[k][0];
int y = j+dir[k][1];
if(x<0||y<0||x>=grid.size()||y>=grid[0].size()||grid[x][y]==0){
res++;
}
}
}
}
}
return res;
}
};

并查集理论基础

并查集常用来解决连通性问题。当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集,并查集主要有两个功能:

基础理论知识

将两条边加入并查集

1
2
3
4
5
6
7
// 将v,u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}

判断两个边是不是在同一个集合中寻根

给出A元素,就可以通过 father[A] = B,father[B] = C,找到根为 C。

给出B元素,就可以通过 father[B] = C,找到根也为为 C,说明 A 和 B是在同一个集合里。大家会想第一段代码里find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根过程,代码如下:

1
2
3
4
5
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u; // 如果根就是自己,直接返回
else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}
1
2
3
4
5
6
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}

最后我们如何判断两个元素是否在同一个集合里,如果通过 find函数 找到两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下:

1
2
3
4
5
6
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}

路径压缩就一行代码:

1
2
3
4
5
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u;
else return father[u] = find(father[u]); // 路径压缩
}

以上代码在C++中,可以用三元表达式来精简一下,代码如下:

1
2
3
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}

join 函数里的这段代码:

1
2
3
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回

与 isSame 函数的实现是不是重复了? 如果抽象一下呢,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 判断 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) {
if (isSame) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;

这样写可以吗? 好像看出去没问题,而且代码更精简了。

其实这么写是有问题的,在join函数中 我们需要寻找 u 和v 的根,然后再进行连线在一起,而不是直接 用 u 和 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
int n =1005;
vector<int> father = vector<int>(n, 0);
// 并查集初始化
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]);
}

// 判uv是不是同一个根
bool isSame(int u, int v){
u=find(u);
v=find(v);
return u==v;
}

void join(int u, int v){
u=find(u);
v=find(v);
//如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
if(u==v) return;
//否则就将其中一个当成另一个的父亲节点
father[v]=u;
}

寻找图中是否存在路径

https://leetcode.cn/problems/find-if-path-exists-in-graph/description/

有一个具有 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
class Solution {
public:
int n=200005;
vector<int> father = vector<int>(n,0);

//并查集初始化
void init(){
for(int i=0;i<n;i++){
father[i] = i;
}
}
//并查集里面的寻根过程
int find(int u){
if(u==father[u]) return u;
else return father[u] = find((father[u]));
}
//判断是不是在同一个根上
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u==v;
}
//将新的内容加入到并查集中
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
init();
for(int i=0;i<edges.size();i++){
join(edges[i][0],edges[i][1]);
}
return isSame(source, destination);
}
};

冗余连接

https://leetcode.cn/problems/redundant-connection/description/

树可以看成是一个连通且 无环无向图。

给定往一棵 n 个节点 (节点值 1~n)的树中添加一条边后的图。添加的边的两个顶点包含在 1n中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为n 的二维数组 edgesedges[i] = [ai, bi] 表示图中在 aibi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n个节点的树。如果有多个答案,则返回数组 edges中最后出现的那个。

冗余连接图解

思路:

那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。

如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了

已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和节点B 连在一起就一定会出现环。

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 n =1005;
vector<int> father = vector<int>(n, 0);
void init(){
for(int i=0;i<n;i++){
father[i]=i;
}
}
int find(int u){
if(u==father[u]) return u;
else return father[u] = find(father[u]);
}
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u==v;
}
void join(int u, int v){
u = find(u);
v = find(v);
if(u==v) return;
father[v]=u;
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
init();
for(int i=0;i<edges.size();i++){
if(isSame(edges[i][0], edges[i][1])) return edges[i];
else (join(edges[i][0], edges[i][1]));
}
return {};
}
};

冗余连接II

https://leetcode.cn/problems/redundant-connection-ii/description/

在本问题中,有根树指满足以下条件的 有向图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n个节点(节点值不重复,从 1n)的树及一条附加的有向边构成。附加的边包含在1n中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对[ui, vi],用以表示 有向 图中连接顶点ui 和顶点 vi 的边,其中 uivi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

冗余连接2的题目解释

思路:

题目中的表述中提到该图由一个有着N个节点 (节点值不重复1, 2,..., 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Solution {
public:
static const int N =1010;
int father[N];
int n;
void init(){
for(int i =1;i<=n;i++){
father[i]=i;
}
}
int find(int u){
if(father[u]==u) return u;
else return father[u]=find(father[u]);
}
void join(int u, int v){
u = find(u);
v = find(v);
if(u==v) return;
else father[v] = u;
}
bool same(int u, int v){
u=find(u);
v = find(v);
return u==v;
}
vector<int> getRemoveEdge(const vector<vector<int>>& edges){
init();
for(int i =0;i<n;i++){
if(same(edges[i][0], edges[i][1])){ //这个表示有环的情况,需要进行删除
return edges[i];
}
join(edges[i][0], edges[i][1]);
}
return {};
}
// 删掉一条边之后判断是不是树
bool isTree(const vector<vector<int>>& edges, int deleteEdges){
init();
for(int i=0;i<n;i++){
if(i==deleteEdges) continue;
if(same(edges[i][0], edges[i][1])) return false; //出现环了因此不是树
join(edges[i][0], edges[i][1]);
}
return true;
}
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int inDegree[N]={0};
n = edges.size();
for(int i=0;i<n;i++){
inDegree[edges[i][1]]++;
}
vector<int> vec; //记录入度为2的边
for(int i=n-1;i>=0;i--){
if(inDegree[edges[i][1]]==2){
vec.push_back(i);
}
}

//如果有入度为2的节点,那么就需要删除
if(vec.size()>0){
if(isTree(edges, vec[0])){
return edges[vec[0]];
}else{
return edges[vec[1]];
}
}

return getRemoveEdge(edges);
}
};
]]>
+

语言细节

vector的长度:

初始化数组:

构造vector:

for循环:

数组

二分查找

题目描述

链接:https://leetcode.cn/problems/binary-search/description/

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

示例 1:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

1
2
3
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

思路

题目表示的是有序数组,而且题目没有重复元素。在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 定义target在左闭右闭的区间里,[left, right]

while left <= right:
middle = left + (right - left) // 2

if nums[middle] > target:
right = middle - 1 # target在左区间,所以[left, middle - 1]
elif nums[middle] < target:
left = middle + 1 # target在右区间,所以[middle + 1, right]
else:
return middle # 数组中找到目标值,直接返回下标
return -1 # 未找到目标值

注意这里给出的题解法:当left <= right的时候,以下的条件中全部都不取到等号nums[middle] > target nums[middle] < target

需要注意的是:right=nums.size()-1

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
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0;
int right=nums.size()-1;
while(left<=right)
{
// int middle = (left+right)/2; 这样写会溢出
int middle = left + ((right - left) / 2);
if(nums[middle]>target)
{
right = middle-1;
}
else if(nums[middle]<target)
{
left = middle+1;
}
else{
return middle;
}
}
return -1;
}
};

Go版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func search(nums []int, target int) int {
right:=len(nums)-1
left:=0
for left<=right{
middle:= left+(right-left)/2
if nums[middle]<target{
left = middle+1
}else if nums[middle]>target{
right = middle-1
}else{
return middle
}
}
return -1
}

移除元素

https://leetcode.cn/problems/remove-element/description/

题目描述

示例 1:

1
2
3
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

1
2
3
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

思路

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针

双指针题解

C++版本

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

python版本

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def removeElement(self, nums, val):
slowindex=0
fastindex=0
while fastindex<len(nums):
if val!=nums[fastindex]:
nums[slowindex]=nums[fastindex]
slowindex = slowindex+1
fastindex+=1
return slowindex

GO版本:

1
2
3
4
5
6
7
8
9
10
func removeElement(nums []int, val int) int {
slow:=0
for i:=0;i<len(nums);i++{
if nums[i]!=val{
nums[slow]=nums[i]
slow++
}
}
return slow
}

有序数组的平方

https://leetcode.cn/problems/squares-of-a-sorted-array/

题目描述

示例 1:

1
2
3
4
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

1
2
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

思路

双指针法,首尾遍历比较并存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result(nums.size(),0);
int j = nums.size()-1;
int k =j;
for(int i = 0 ;i<=j;)
{
if(nums[i]*nums[i]>nums[j]*nums[j]){
result[k--]= nums[i]*nums[i];
i++;
}else{
result[k--]= nums[j]*nums[j];
j--;
}
}
return result;
}
};

Python:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution(object):
def sortedSquares(self, nums):
l, r, i = 0, len(nums)-1, len(nums)-1
res = [float('inf')] * len(nums) # 需要提前定义列表,存放结果
while l<=r :
if nums[l]*nums[l] < nums[r]*nums[r] :
res[i--]=nums[r]*nums[r]
r--
else:
res[i--]=nums[l]*nums[l]
l++
return

GO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func sortedSquares(nums []int) []int {
n := len(nums)
i, j, k := 0, n-1, n-1
ans := make([]int, n)
for i <= j {
lm, rm := nums[i]*nums[i], nums[j]*nums[j]
if lm > rm {
ans[k] = lm
i++
} else {
ans[k] = rm
j--
}
k--
}
return ans
}

长度最小的子数组

https://leetcode.cn/problems/minimum-size-subarray-sum/description/

题目描述

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

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

示例 1:

1
2
3
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

1
2
输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

1
2
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

思路

滑动窗口法

滑动窗口法

滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动本题中实现滑动窗口,主要确定如下三点:

窗口就是 满足其和 ≥ 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 minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};

螺旋矩阵

https://leetcode.cn/problems/spiral-matrix-ii/

题目描述

螺旋矩阵

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

1
2
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

示例 2:

1
2
输入:n = 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
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> result(n, vector<int>(n,0));
int is=0,ie=n-1,js=0,je=n-1;
int k = 1;
while(is<=ie&&js<=je){
for(int j=js;j<=je;j++)
{
result[is][j] = k++;
}
is++;
for(int i =is;i<=ie;i++)
{
result[i][je] = k++;
}
je--;
for(int j=je;j>=js;j--)
{
result[ie][j] = k++;
}
ie--;
for(int i=ie;i>=is;i--)
{
result[i][js] = k++;
}
js++;
}
return result;
}
};

螺旋矩阵2

https://leetcode.cn/problems/spiral-matrix/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

给你一个 mn 列的矩阵matrix ,请按照 顺时针螺旋顺序,返回矩阵中的所有元素。

螺旋矩阵2

思路:

主要的解题方法就是模拟,但是需要注意的是每次改变is/ie/js/je之后都需要进行一次判断一旦不满足循环的条件立马退出不进行模拟

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<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
int is = 0, js = 0, ie = m-1, je = n-1;
vector<int>res;
while(is<=ie && js<=je){
for(int j=js;j<=je;j++){
res.push_back(matrix[is][j]);
}
is++;
if(is>ie || js>je) break;
for(int i = is;i<=ie;i++){
res.push_back(matrix[i][je]);
}
je--;
if(is>ie || js>je) break;
for(int j=je;j>=js;j--){
res.push_back(matrix[ie][j]);
}
ie--;
if(is>ie || js>je) break;
for(int i =ie;i>=is;i--){
res.push_back(matrix[i][js]);
}
js++;
if(is>ie || js>je) break;
}
return res;
}
};

生命游戏

https://leetcode.cn/problems/game-of-life/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

根据 百度百科, 生命游戏 ,简称为 生命,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

给定一个包含 m × n个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态:1 即为 活细胞 (live),或 0即为 死细胞(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

  1. 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
  2. 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
  3. 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
  4. 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;

下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你m x n 网格面板 board的当前状态,返回下一个状态。

image-20240606214825990

思路:

  1. 给出8个方位的计算公式,-1,0,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
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public:
void gameOfLife(vector<vector<int>>& board) {
int step[3] = {-1,0,1};
vector<vector<int>>origin_board(board.size(), vector<int>(board[0].size(),0));
for(int i =0;i<board.size();i++){
for(int j = 0;j<board[0].size();j++){
origin_board[i][j] = board[i][j];
}
}
for(int r = 0;r<board.size();r++){
for(int c = 0;c<board[0].size();c++){
//计算周围的活细胞数量
int alive_cell = 0;

//遍历8个方向
for(int i = 0;i<3;i++){
for(int j=0;j<3;j++){
if(!(step[i]==0 && step[j]==0)){ //表示不同时为0的情况
if(((r+step[i]>=0)&&(r+step[i]<board.size()))&&((c+step[j]>=0)&&(c+step[j]<board[0].size()))&&(origin_board[r+step[i]][c+step[j]]==1)){
alive_cell++;
}
}
}
}

if(origin_board[r][c]==1){
if ((alive_cell<2)||(alive_cell>3)){
board[r][c]=0;
}
}else{
if(alive_cell==3){
board[r][c]=1;
}
}
}
}
}
};

旋转矩阵

给定一个 n × n 的二维矩阵 matrix表示一个图像。请你将图像顺时针旋转 90 度。

你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

旋转矩阵示意图

思路:

  1. 顺时针旋转的思路先水平方向翻转
  2. 再中心对称反转即可
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]);
}
}
}
};

快速排序

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
#include <stdio.h>
#include <iostream>
#include <math.h>
#include <algorithm>
using namespace std;
int part(int* r, int low, int hight) //划分函数
{
int i = low, j = hight, pivot = r[low]; //基准元素
while (i < j)
{
while (i<j && r[j]>pivot) //从右向左开始找一个 小于等于 pivot的数值
{
j--;
}
if (i < j)
{
swap(r[i++], r[j]); //r[i]和r[j]交换后 i 向右移动一位
}
while (i < j && r[i] <= pivot) //从左向右开始找一个 大于 pivot的数值
{
i++;
}
if (i < j)
{
swap(r[i], r[j--]); //r[i]和r[j]交换后 i 向左移动一位
}
}
return i; //返回最终划分完成后基准元素所在的位置
}
void Quicksort(int* r, int low, int hight)
{
int mid;
if (low < hight)
{
mid = part(r, low, hight); // 返回基准元素位置
Quicksort(r, low, mid - 1); // 左区间递归快速排序
Quicksort(r, mid+1, hight); // 右区间递归快速排序
}
}

数组中的第K大元素

https://leetcode.cn/problems/xx4gT2/description/

给定整数数组 nums 和整数 k,请返回数组中第**k** 个最大的元素。

请注意,你需要找的是数组排序后的第 k个最大的元素,而不是第 k 个不同的元素。

示例 1:

1
2
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

1
2
输入: [3,2,3,1,2,4,5,5,6] 和 k = 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
class Solution {
public:
int partition(vector<int>& nums, int low, int high){
int key = nums[low];
while(low< high){
while(low<high && nums[high]>= key) high--;
nums[low] = nums[high];
while(low<high&& nums[low]<=key) low++;
nums[high] = nums[low];
}
nums[low] = key;
return low;
}
void quicksort(vector<int>& nums, int low, int high){
if(low>=high) return;
int mid = partition(nums, low, high);
quicksort(nums, low, mid-1);
quicksort(nums, mid+1, high);
}
int findKthLargest(vector<int>& nums, int k) {
int n = nums.size();
quicksort(nums, 0, n-1);
return nums[n-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
class Solution {
public:
void BuildHeap(vector<int>& arr, int i, int n){
int p = i, c=2*i+1;
while(c<=n){
if(c<n && arr[c]>arr[c+1]) c++;
if(arr[p]>arr[c]){
swap(arr[p], arr[c]);
p = c;
}
c = 2*c+1;
}
}

void Heapsort(vector<int>& arr){
//建立一个堆
for(int i=arr.size()/2-1;i>=0;i--){
BuildHeap(arr, i,arr.size()-1);
}
for(int i=arr.size()-1; i>0;i--){
swap(arr[0], arr[i]);
BuildHeap(arr,0,i-1);
}
}
int findKthLargest(vector<int>& nums, int k) {
Heapsort(nums);
return nums[k-1];
}
};

和为K的子数组

https://leetcode.cn/problems/subarray-sum-equals-k/description/?envType=study-plan-v2&envId=top-100-liked

给你一个整数数组 nums 和一个整数 k,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列

示例 1:

1
2
输入:nums = [1,1,1], k = 2
输出:2

示例 2:

1
2
输入:nums = [1,2,3], k = 3
输出:2

思路:

这道题用前缀和+哈希表来解决

和为K的子数组

主要的思路是,首先维护一个map数组来存每个元素的前缀和,以及出现的次数,当每次到一个位置的时候来判断当前的map中是否有pre-k的元素以及对应的值,如果有那么就可以将个数加上,这个map对应的键是加和的元素,值是出现的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int res = 0;
unordered_map<int, int>pre;
pre[0] = 1;
int p = 0;
for(int i =0;i<nums.size();i++){
p+=nums[i];
if(pre.find(p-k)!=pre.end()){
res+=pre[p-k];
}
pre[p]++;
}
return res;
}
};

哈希表

一般哈希表都是用来快速判断一个元素是否出现集合里

只需要初始化把所有元素都存在哈希表里,在查询的时候通过索引直接就可以知道元素在不在这哈希表里了

建立索引:哈希函数

有效的字母异位词

https://leetcode.cn/problems/valid-anagram/description/

题目描述

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

示例 1: 输入: s = "anagram", t = "nagaram" 输出: true

示例 2: 输入: s = "rat", t = "car" 输出: false

思路

暴力的方法可能时间复杂度会很高

判断有没有异位词的本质就是查看当前的字母是不是有出现过,那么思路就是选择哈希表

定义一个数组叫做record用来上记录字符串s里字符出现的次数。

需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。

再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。这样就将字符串s中字符出现的次数,统计出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
// record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
// record数组所有元素都为零0,说明字符串s和t是字母异位词
return true;
}
};

两个数组的交集

https://leetcode.cn/problems/intersection-of-two-arrays/description/

题目描述

示例 1:

1
2
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

1
2
3
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

思路

使用哈希表存储,但是用set(unordered_set)

std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表,使用unordered_set读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};

快乐数

https://leetcode.cn/problems/happy-number/description/

题目描述

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

「快乐数」 定义为:

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

示例 1:

1
2
3
4
5
6
7
输入:n = 19
输出:true
解释:
1**2 + 9**2 = 82
8**2 + 2**2 = 68
6**2 + 8**2 = 100
1**2 + 0**2 + 0**2 = 1

思路:

注意,题目中提到一个点是无限循环,说明计算的结果sum是有限的只需要在哈希表中将这部分的结果存储进去,并每次比较是不是出现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 getSum(int n){
int sum=0;
while(n){
sum+=(n%10)*(n%10);
n/=10;
}
return sum;
}
bool isHappy(int n) {
//首先建立哈希表来存储是不是出现了无限循环的结果
unordered_set<int>sum_set;
//无限循环 直到出现1或者无限循环且不是快乐数
while(1){
n=getSum(n);
if(sum_set.find(n)!=sum_set.end()){
return false;
}else{
sum_set.insert(n);
}
if(n==1){
return true;
}
}
}
};

两数之和

题目描述

https://leetcode.cn/problems/two-sum/submissions/495021134/

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

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

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

示例 1:

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

示例 2:

1
2
输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

1
2
输入:nums = [3,3], target = 6
输出:[0,1]

思路:

构建一个哈希表,然后遍历一遍就行了在哈希表中找n-a的值是否存在,但是最大的问题是数组中同一个元素在答案里不能重复出现,所以不能简单考虑unordered_set

这里提供一种新的思路,就是用unordered_map来存储数组中的数据内容和下标的数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};

四数相加

https://leetcode.cn/problems/4sum-ii/description/

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

示例 1:

1
2
3
4
5
6
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:

1
2
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

思路

  1. 首先定义 一个unordered_map,key放a和b两数之和,value放a和b两数之和出现的次数
  2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
  4. 在遍历大C和大D数组,找到如果 0-(c+d)在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
  5. 最后返回统计值 count 就可以了
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 fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
std::unordered_map<int,int>nm;
int res=0;

for(int i=0;i<nums1.size();i++){
for(int j=0;j<nums2.size();j++){
int s = nums1[i]+nums2[j];
nm[s]++;
}
}
for(int i=0;i<nums3.size();i++){
for(int j=0;j<nums4.size();j++){
if(nm.find(0-nums3[i]-nums4[j])!=nm.end()){
res+=nm[0-(nums3[i]+nums4[j])];
}
}
}
return res;
}
};

赎金信

https://leetcode.cn/problems/ransom-note/description/

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

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

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

示例 1:

1
2
输入:ransomNote = "a", magazine = "b"
输出:false

示例 2:

1
2
输入:ransomNote = "aa", magazine = "ab"
输出:false

思路:

用哈希表unordered_map来存储次数,对于ransomNote来减去次数

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) {
unordered_map<int,int>umap;
if(ransomNote.size()>magazine.size()){return false;}
for(int i=0;i<magazine.size();i++){
umap[magazine[i]-'a']++;
}
for(int i=0;i<ransomNote.size();i++){
if(umap.find(ransomNote[i]-'a')!=umap.end()){
umap[ransomNote[i]-'a']--;
if(umap[ransomNote[i]-'a']<0) {return false;}
}else{
return false;
}
}
return true;
}
};

三数之和

https://leetcode.cn/problems/3sum/description/

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

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

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

示例 1:

1
2
3
4
5
6
7
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1][-1,-1,2]

思路

其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码,而且使用哈希法在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2)

这道题可以用双指针法求解

拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left定义在i+1的位置上,定义下标right 在数组结尾的位置上

依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a =nums[i],b = nums[left],c = nums[right]。

接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right]> 0 就说明此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时三数之和小了,left就向右移动,才能让三数之和大一些,直到left与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
32
33
34
35
36
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}

}
return result;
}
};

字母异位词分组

https://leetcode.cn/problems/group-anagrams/description/?envType=study-plan-v2&envId=top-100-liked

给你一个字符串数组,请你将 字母异位词组合在一起。可以按任意顺序返回结果列表。

字母异位词是由重新排列源单词的所有字母得到的一个新单词。

示例 1:

1
2
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:

1
2
输入: strs = [""]
输出: [[""]]

示例 3:

1
2
输入: strs = ["a"]
输出: [["a"]]

思路:

对每个字符串进行计数,将每个字符串中出现的字母和数字进行排序作为key存储,利用map数据结构来进行存储上述的内容,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
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> map;
for(string str: strs){
int count_letter[26] = {0};
for(char c: str){
count_letter[c-'a']++;
}
string key = "";
for (int i =0;i<26;i++){
if(count_letter[i]>0){
key.push_back(i-'a');
key.push_back(count_letter[i]);
}
}
map[key].push_back(str);
}
vector<vector<string>> res;
for(auto& p:map) {
res.push_back(p.second);
}
return res;
}
};

最长连续序列

https://leetcode.cn/problems/longest-consecutive-sequence/submissions/531637826/?envType=study-plan-v2&envId=top-100-liked

题目描述:

给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为O(n) 的算法解决此问题。

示例 1:

1
2
3
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4

示例 2:

1
2
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

思路:

用哈希集合来存储上述的数,本质上还是需要比较是否存在下一个数x+1,但是可以在条件判断上进行约束,如果x-1这个数不存在集合中那么说明可以从x开始遍历!num_set.count(num - 1)){ //count用来计数是否存在数在集合中

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 longestConsecutive(vector<int>& nums) {
unordered_set<int> num_set;
for(auto num : nums){
num_set.insert(num);
}
int maxlong = 0;
for(auto num: num_set){
if (!num_set.count(num - 1)){ //count用来计数是否存在数在集合中
int curnum = num;
int curlong = 1;
while(num_set.count(curnum+1)){
curnum+=1;
curlong+=1;
}
maxlong = max(maxlong, curlong);
}
}
return maxlong;
}
};

双指针

移除元素

https://leetcode.cn/problems/remove-element/description/

示例 1:

1
2
3
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

1
2
3
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

思路:

使用快慢指针来实现两个指针之间的移动,对于找到了和val数值一样的就进行替换

移动零

https://leetcode.cn/problems/move-zeroes/description/?envType=study-plan-v2&envId=top-100-liked

给定一个数组 nums,编写一个函数将所有 0移动到数组的末尾,同时保持非零元素的相对顺序。

请注意,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

1
2
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

1
2
输入: nums = [0]
输出: [0]

思路:

使用双指针,向后遍历的过程中一旦遇到非0的元素就将其与左边指针互换并左边下标+1,凡事遇到需要交换位置的这类方法都建议能使用双指针来实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0;
for(int right = 0; right<nums.size();right++){
if(nums[right]){
swap(nums[left],nums[right]);
left++;
}
}
}
};

删除有序数组中的重复元素

https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

给你一个 非严格递增排列 的数组 nums,请你原地删除重复出现的元素,使每个元素 只出现一次,返回删除后数组的新长度。元素的 相对顺序 应该保持一致 。然后返回 nums中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k,你需要做以下事情确保你的题解可以被通过:

示例 1:

1
2
3
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

1
2
3
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

思路:

这道题技巧在于抓住有序的数组,可以用双指针来对数组进行遍历,定义两个指针fast 和slow分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标1。

假设数组 nums的长度为 n。将快指针 fast 依次遍历从 1到n−1的每个位置,对于每个位置,如果 nums[fast]≠nums[fast−1],说明nums[fast]和之前的元素都不同,因此将 nums[fast]的值复制到nums[slow],然后将 slow的值加 1,即指向下一个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n = nums.size();
if(n==0){
return 0;
}
int slow = 1, fast = 1;
while(fast<n){
if(nums[fast]!=nums[fast-1]){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};

反转字符串

https://leetcode.cn/problems/reverse-string/description/

示例 1:

1
2
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

示例 2:

1
2
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]

思路:

采用两个指针之间互相交换,首尾交换

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
void reverseString(vector<char>& s) {
for(int a=0, b = s.size()-1;a<b;){
char tmp;
tmp=s[a];
s[a]=s[b];
s[b]=tmp;
a++;
b--;
}
}
};

反转字符串中的单词

https://leetcode.cn/problems/reverse-words-in-a-string/description/

示例 1:

1
2
输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

1
2
3
输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

1
2
3
输入:s = "a good   example"
输出:"example good 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
string reverseWords(string s) {
for(int i = s.size()-1;i>0;i--){
if(s[i]==s[i-1]&&s[i]==' '){
s.erase(s.begin()+i);
}
}
if(s.size()>0&&s[s.size()-1]==' '){
s.erase(s.begin()+s.size()-1);
}
if(s.size()>0&&s[0]==' '){
s.erase(s.begin());
}

//完成全局的交换
for(int i = 0, j=s.size()-1;i<j;i++,j--){
char tmp;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
cout<<s;
//进行局部的交换
int i=0;
int j=1;
while(j<=s.size()){
if(s[j]==' '||j==s.size()){
for(int k =i, q =j-1;k<q;k++,q--){
char tmp;
tmp = s[k];
s[k] = s[q];
s[q] = tmp;
}
i=j+1;
j=i+1;
}else{
j++;
}
}
return s;
}
};

反转链表

https://leetcode.cn/problems/reverse-linked-list/description/

image-20240118151406885

1
2
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

思路:本质上就是利用了两个链表指针实现对元素的转向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp;
ListNode* cur = head;
ListNode* pre = nullptr;
while(cur){
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};

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

https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/

image-20240118152000575

1
2
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

1
2
输入:head = [1], n = 1
输出:[]

思路:

遍历,用两个指针分别来记录

如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了

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:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;

// ListNode *tmp = slow->next; C++释放内存的逻辑
// slow->next = tmp->next;
// delete nth;

return dummyHead->next;
}
};

链表相交

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

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

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

示例 1:

链表相交图

1
2
3
4
5
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 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
27
28
29
30
31
32
33
34
35
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while(curA != NULL){
lenA++;
curA = curA ->next;
}
while(curB != NULL){
lenB++;
curB = curB ->next;
}
curA = headA;
curB = headB;
if(lenB> lenA){
swap(lenA,lenB);
swap(curA, curB);
}

int gap = lenA - lenB;
while(gap--){
curA = curA->next;
}
while(curA!=NULL){
if(curA == curB){
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};

环形链表

https://leetcode.cn/problems/linked-list-cycle-ii/description/

判断是否是有还存在,如果有那么返回开始入环的第一个节点的下标

环形链表

1
2
3
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

思路:

这道题用快慢指针的思路,就是慢指针每次只走一步,快指针每次走两步,如果在到达null之前出现快慢指针指向了同一个地方,说明这个链表有环存在,那么怎么判断下标的位置呢?

具体的证明过程:

相遇时slow指针走过的节点数为: x + y,fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针,(y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以fast指针走过的节点数 = slow指针走过的节点数 * 2:

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

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个(y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z注意这里n一定是大于等于1的,因为fast指针至少要多走一圈才能相遇slow指针

所以可以得到的规律是:从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点,那么当这两个指针相遇的时候就是 环形入口的节点

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:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast!=NULL&& fast->next!=NULL){
slow = slow->next;
fast = fast->next->next;
//说明找到了
if(slow==fast){
ListNode* index1 = fast;
ListNode* index2 = head;
while(index1!=index2){
index1 = index1->next;
index2 = index2 ->next;
}
return index2;
}
}
return NULL;
}
};

找到字符串中所有字幕的异位词

https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/?envType=study-plan-v2&envId=top-100-liked

给定两个字符串 sp,找到 s中所有 p异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

1
2
3
4
5
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

示例 2:

1
2
3
4
5
6
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

思路:

这道题可以考虑用滑动窗口的方法来考虑,对于不同位置但是组成一样的一对字符串可以用哈希表来记录,这个地方就用哈希数组来存储,每次按照一定的顺序进行滑动

所以我们可以在字符串 s 中构造一个长度为与字符串 p的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;当窗口中每种字母的数量与字符串p 中每种字母的数量相同时,则说明当前窗口为字符串 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
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int m = s.size(), n = p.size();
if(m<n){
return vector<int>();
}
vector<int> res;
vector<int> scount(26);
vector<int> pcount(26);
for(int i =0;i<n;i++){
scount[s[i]-'a']++;
pcount[p[i]-'a']++;
}
if(scount == pcount){
res.push_back(0);
}
for(int i=0;i<m-n;i++){
scount[s[i]-'a']--;
scount[s[i+n]-'a']++;
if(scount==pcount){
res.push_back(i+1);
}
}
return res;
}
};

盛最多水的容器

https://leetcode.cn/problems/container-with-most-water/?envType=study-plan-v2&envId=top-100-liked

给定一个长度为 n 的整数数组 height 。有n 条垂线,第 i 条线的两个端点是(i, 0)(i, height[i])。找出其中的两条线,使得它们与 x轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。说明:你不能倾斜容器。

最多水的容器

1
2
3
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49

示例 2:

1
2
输入:height = [1,1]
输出:1

思路:

双指针来解决这个题,主要的思路就是首尾都放一个指针,然后依次向中间移动,向中间移动意味着x轴的长度变短那么需要高度要长,因此需要舍弃掉短的那边--或者++来滑动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int maxArea(vector<int>& height) {
int l = 0, r = height.size()-1;
int maxa = 0;
while(l<r){
int water = min(height[r], height[l]) * (r-l);
if(water>maxa){
maxa = water;
}
if(height[l]<height[r]){
l++;
}else{
r--;
}
}
return maxa;
}
};

接雨水

给定 n 个非负整数表示每个宽度为 1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

1
2
3
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

1
2
输入:height = [4,2,0,3,2,5]
输出: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
class Solution {
public:
int trap(vector<int>& height) {
if(height.size()<=2) return 0;
vector<int> maxLeft(height.size(), 0);
vector<int> maxRight(height.size(), 0);
int size = maxRight.size();
//记录每个柱子左边柱子的最大高度
maxLeft[0] = height[0];
for(int i=1;i<size;i++){
maxLeft[i] = max(height[i],maxLeft[i-1]);
}
//记录每个柱子右边柱子的最大高度
maxRight[size-1] = height[size-1];
for(int i=size-2;i>=0;i--){
maxRight[i] = max(height[i],maxRight[i+1]);
}
int sum=0;
for(int i=0;i<size;i++){
int count = min(maxLeft[i], maxRight[i])-height[i];
if(count > 0) sum+=count;
}
return sum;
}
};

柱形图中的最大矩形

https://leetcode.cn/problems/largest-rectangle-in-histogram/description/

最大矩形问题

1
2
3
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 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
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeft(heights.size());
vector<int> minRight(heights.size());
int size = heights.size();

//记录每个柱子左边第一个小于该柱子的下标
minLeft[0] = -1;
for(int i=1;i<size;i++){
int t= i-1;
while(t>=0&&heights[t]>=heights[i]) t=minLeft[t];
minLeft[i]=t;
}
//记录每个柱右边第一个小于该柱子的下标
minRight[size-1]=size;
for(int i=size -2;i>=0;i--){
int t=i+1;
while(t<size&&heights[t]>=heights[i]) t=minRight[t];
minRight[i]=t;
}

int res=0;
for(int i=0;i<size;i++){
int sum=heights[i]*(minRight[i]-minLeft[i]-1);
res = max(sum,res);
}

return res;
}
};

二叉树

二叉树的中序遍历

https://leetcode.cn/problems/binary-tree-inorder-traversal/description/?envType=study-plan-v2&envId=top-100-liked

给定一个二叉树的根节点 root ,返回 它的中序 遍历

1
2
输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

1
2
输入:root = []
输出:[]

示例 3:

1
2
输入:root = [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
/**
* 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 inorder(TreeNode* root, vector<int>& res){
if(!root){
return;
}
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right,res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int>res;
inorder(root, res);
return res;
}
};

二叉树的最大深度

https://leetcode.cn/problems/maximum-depth-of-binary-tree/description/?envType=study-plan-v2&envId=top-100-liked

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

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

思路:

利用递归和深度优先搜索来求解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 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 maxDepth(TreeNode* root) {
if(!root){
return 0;
}
return max(maxDepth(root->left), maxDepth(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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<iostream>
#include<vector>
using namespace std;

struct TreeNode{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(): val(0), left(nullptr), right(nullptr){}
TreeNode(int val): val(val), left(nullptr), right(nullptr)()
};

TreeNode* search(vector<int> nums){
TreeNode* root = new TreeNode(nums[0]);
for(int i=0;i<nums.size();i++){
TreeNode* n = new TreeNode(nums[i]);
TreeNode* cur = root;
while(cur!=nullptr){
if(nums[i]<cur->val){
if(cur->left==nullptr){cur->left = n; break;}
else(cur = cur->left;)
}else{
if(cur->right==nullptr){cur->right = n; break;}
else(cur = cur->right;)
}
}
}
return root;
}

void inorder(TreeNode* root){
if(root == nullptr) return;
inorder(root->left);
printf("%d ",root->val);
inorder(root->right);
}


int main(){
vector<int> nums=({2,3,4,5,2,1});
TreeNode* st = search(nums);
inorder(root);
return 0;
}

验证二叉搜索树

思路:对这个树进行中序遍历放进一个数组中如果是从大到小的顺序,那么就认为是二叉搜索树

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> nums;
void inorder(TreeNode* root){
if(root == nullptr){
return;
}
inorder(root->left);
nums.push_back(root->val);
inorder(root->right);
}
bool isValidBST(TreeNode* root) {
inorder(root);
for(int i = 1; i < nums.size(); i++){
if(nums[i] <= nums[i - 1]){
return false;
}
}
return true;
}
};

二叉搜索树中第K小的元素

思路:一样的思路,将二叉搜索树进行存储进一个一维的数组中,然后输出第k-1个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<int> nums;
void inorder(TreeNode* root){
if(root == nullptr){
return;
}
inorder(root->left);
nums.push_back(root->val);
inorder(root->right);
}
int kthSmallest(TreeNode* root, int k) {
inorder(root);
return nums[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
class Solution {
public:
//内部实现
TreeNode* build(vector<int>& preorder, vector<int>& inorder, int ps, int pe, int is, int ie){
if(ps >= pe || is >= ie){
return nullptr;
}
//找到第一个元素就是前序遍历的第一个元素
TreeNode* r = new TreeNode(preorder[ps]);
//find inorder partition
//找到中序遍历中的处于位置中间的那个元素
int i;
for(i = is; i < ie; i++){
if(inorder[i] == preorder[ps]){
break;
}
}
r->left = build(preorder, inorder, ps + 1, ps + 1 + (i - is), is, i);
r->right = build(preorder, inorder, ps + 1 + i - is, pe, i + 1, ie);
return r;
}
//最外层的调用
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
return build(preorder, inorder, 0, preorder.size(), 0, inorder.size());
}
};

深搜回溯

深度优先搜索的三部曲:

  1. 确定搜索函数的返回值以及搜索函数的参数分别是什么
  2. 确定每次找到叶子结点的终止条件
  3. 确定for单层搜索的逻辑,包含push,backtracking,pop

别忘了最开始的初始化步骤

组合问题

https://leetcode.cn/problems/combinations/description/

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

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

示例 1:

1
2
3
4
5
6
7
8
9
10
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

示例 2:

1
2
输入:n = 1, k = 1
输出:[[1]]

思路,使用深度优先搜索算法进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
private:
vector<vector<int>> result;
vector<int>path;
void backtracking(int n,int k, int startindex){
if(path.size()==k){
result.push_back(path);
return;
}
for(int i = startindex;i<=n;i++){
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k){
backtracking(n,k,1);
return result;
}
};

组合问题III

https://leetcode.cn/problems/combination-sum-iii/submissions/496823507/

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

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

示例:

1
2
3
4
5
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

思路:简单的深度优先搜索,但需要注意的是可以适当采用减枝操作和必要的时候添加sum变量进行记录

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 {
private:
vector<vector<int>> result;
vector<int> path;
void backtacking(int k, int n,int startindex, int sum){
if(path.size()==k){
if(sum == n) result.push_back(path);
return;
}
for(int i= startindex;i<=9;i++){
sum+=i;
path.push_back(i);
backtacking(k,n,i+1,sum);
sum-=i;
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtacking(k,n,1,0);
return result;
}
};

为了优化可以做一个剪枝操作

1
2
3
if (sum > targetSum) { // 剪枝操作
return;
}

电话号码组合问题

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

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

电话号码的按键

示例 :

1
2
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

这道题需要注意的地方是,首先第一步做好map字符的映射

第二步最关键是要写清楚回溯函数的参数可能包含index,就是第几位置的字符,同时需要区分backtracking函数的for循环的内容是相当于横向的遍历,而函数体内部的实现是纵向的遍历

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 {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> result;
string s;
void backtracking(const string digits,int index, string s){
if(digits.size()==0){
return;
}
if(index==digits.size()){
result.push_back(s);
return;
}
int digit = digits[index]-'0';
string letters = letterMap[digit];
for(int i= 0;i<letters.size();i++){
s.push_back(letters[i]);
backtracking(digits, index+1, s);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
backtracking(digits,0,"");
return result;
}
};

组合总和

https://leetcode.cn/problems/combination-sum/

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

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

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

示例 :

1
2
3
4
5
6
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7
仅有这两种组合。

思路:

题目最关键的点在于能重复使用元素但是不能重复元素的组合不能被重复输出

因此需要调整startindex的开始的位置是在backtracking(candidates,target,sum,i);注意,这个时候从i开始保证还能用到自己的元素重复使用,还有最重要的sort(candidates.begin(), candidates.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:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>&candidates, int target, int sum, int startindex){
if(sum==target){
res.push_back(path);
return;
}
for(int i=startindex;i<candidates.size();i++){
if(sum>target){
return;
}
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0,0);
return res;
}
};

组合总和II

https://leetcode.cn/problems/combination-sum-ii/description/

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

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

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

示例 :

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

思路:

这个地方最大的困难在于每个数字在每个组合中只能使用一次,同时集合中的元素存在重复的元素,那么这个时候有一个问题是如何才能对元素进行去重处理呢,就是让每个元素只能被使用一次

去重的操作就在于vector<bool> used(candidates.size(),false); sort(candidates.begin(), candidates.end());

首先需要在backtracking中定一个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
25
26
27
28
29
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startindex, vector<bool>used){
if(sum==target){
res.push_back(path);
return;
}
for(int i= startindex;i<candidates.size();i++){
if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false){
continue;
}
sum+=candidates[i];
used[i]=true;
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i+1, used);
used[i]=false;
path.pop_back();
sum-=candidates[i];
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(),false);
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return res;
}
};

分割回文串

https://leetcode.cn/problems/palindrome-partitioning/description/

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

回文串 是正着读和反着读都一样的字符串。

示例:

1
2
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

思路:

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:
vector<vector<string>> res;
vector<string> path;
bool ishuiwen(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 startindex){
if(startindex>=s.size()){
res.push_back(path);
return;
}
for(int i=startindex;i<s.size();i++){
if(ishuiwen(s,startindex,i)){
string str = s.substr(startindex, i-startindex+1);
path.push_back(str);
}else{
continue;
}
backtracking(s,i+1);
path.pop_back();
}
}
vector<vector<string>> partition(string s) {
backtracking(s,0);
return res;
}
};

复原IP地址

https://leetcode.cn/problems/restore-ip-addresses/description/

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

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

示例 :

1
2
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

思路:

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:
vector<string> res;
bool isvalid(string s, int start, int end){
if(start>end){
return false;
}
if(s[start]=='0'&&start!=end){
return false;
}
int num = 0;
for(int i=start;i<=end;i++){
if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
return false;
}
num=num*10+(s[i]-'0');
if(num>255){
return false;
}
}
return true;
}
void backtracking(string s, int startindex, int pointnum){
if(pointnum==3){
if (isvalid(s, startindex, s.size() - 1)) {
res.push_back(s);
}
return;
}
for(int i=startindex;i<s.size();i++){
if(isvalid(s,startindex,i)){
s.insert(s.begin()+i+1,'.');
pointnum++;
backtracking(s,i+2,pointnum);
pointnum--;
s.erase(s.begin()+i+1);
}else break;
}
}
vector<string> restoreIpAddresses(string s) {
backtracking(s,0,0);
return res;
}
};

子集

https://leetcode.cn/problems/subsets/description/

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

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

示例:

1
2
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

思路:

这道题比较简单,就是简单的遍历就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums,int startindex){
res.push_back(path);
for(int i=startindex;i<nums.size();i++){
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};

子集II

https://leetcode.cn/problems/subsets-ii/description/

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

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

示例:

1
2
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[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
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int> nums, int startindex, vector<bool> used){
res.push_back(path);

for(int i=startindex;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums,0,used);
return res;
}
};

非递减子序列

https://leetcode.cn/problems/non-decreasing-subsequences/description/

给你一个整数数组 nums,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素 。你可以按 任意顺序返回答案。数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

1
2
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

1
2
输入:nums = [4,4,3,2,1]
输出:[[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
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, int startindex){
if(path.size()>1){
res.push_back(path);
}
unordered_set<int> uset;
for(int i =startindex;i<nums.size();i++){
if ((!path.empty() && nums[i] < path.back())
|| uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};

全排列

https://leetcode.cn/problems/permutations/description/

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

示例 1:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

1
2
输入:nums = [0,1]
输出:[[0,1],[1,0]]

思路:

要求解全排列,因此回溯退出的条件是当path的长度和nums的长度一样的时候就达到了退出的条件

因为这道题没有重复的元素,求解全排列需要每次都从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
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool>& used){
if(path.size()==nums.size()){
res.push_back(path);
}
for(int i=0;i<nums.size();i++){
if(used[i]==true){
continue;
}
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
};

全排列II

https://leetcode.cn/problems/permutations-ii/

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

示例 1:

1
2
3
4
5
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

示例 2:

1
2
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,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:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool>& used){
if(path.size()==nums.size()){
res.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
if(used[i]==false){
used[i]=true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i]=false;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<bool> used(nums.size(),false);
backtracking(nums,used);
return res;
}
};

重新安排行程

https://leetcode.cn/problems/reconstruct-itinerary/description/

给你一份航线列表 tickets ,其中tickets[i] = [fromi, toi]表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。所有这些机票都属于一个从JFK(肯尼迪国际机场)出发的先生,所以该行程必须从JFK开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

安排行程

思路:

【困难】

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<string> res;
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketnum, vector<string>& res){
if(res.size()==ticketnum+1){
return true;
}
for(pair<const string, int>& target: targets[res[res.size()-1]]){
if(target.second>0){
res.push_back(target.first);
target.second--;
if(backtracking(ticketnum, res)) return true;
res.pop_back();
target.second++;
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {

for(const vector<string>& vec: tickets){
targets[vec[0]][vec[1]]++;
}
res.push_back("JFK");
backtracking(tickets.size(), res);

return res;
}
};

N皇后

https://leetcode.cn/problems/n-queens/description/

n 皇后问题 研究的是如何将 n个皇后放置在 n×n的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题的棋子放置方案,该方案中 'Q''.'分别代表了皇后和空位。

1
2
3
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

思路:

这道题关键在于用好数据结构和写好合法性的判断

关键在于定义好chessboard第二步是把合法性位置判断写好

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<vector<string>> res;
bool isvalid(int row, int col, vector<string>& chessboard,int n){
//检查列
for(int j=0;j<n;j++){
if(chessboard[row][j]=='Q') return false;
}
//检查行
for(int i=0;i<n;i++){
if(chessboard[i][col]=='Q') return false;
}
//检查对角线45
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
//检查对角线135
for(int i = row-1,j=col+1;i>=0&&j<n;i--,j++){
if(chessboard[i][j]=='Q'){
return false;
}
}
return true;
}
void backtracking(vector<string>& chessboard, int row, int n){
if(row==n)
{
res.push_back(chessboard);
return;
}
for(int col = 0;col<n;col++){
if(isvalid(row,col,chessboard,n)){
chessboard[row][col]='Q';
backtracking(chessboard,row+1,n);
chessboard[row][col]='.';
}
}
}

vector<vector<string>> solveNQueens(int n) {
std::vector<std::string> chessboard(n, std::string(n, '.'));
backtracking(chessboard,0,n);
return res;
}
};

解数独

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

1
2
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","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
35
36
37
38
39
40
class Solution {
public:
bool isvalid(vector<vector<char>>& board, int row, int col, char a){
//行遍历
for(int j=0;j<9;j++){
if(board[row][j]==a) return false;
}
//列遍历
for(int i=0;i<9;i++){
if(board[i][col]==a) return false;
}
//方框内判断:关键找到起始的方框对角
for(int i= (row/3)*3;i<(row/3)*3+3;i++){
for(int j=(col/3)*3; j<(col/3)*3+3;j++){
if(board[i][j]==a) return false;
}
}
return true;
}
bool backtracking(vector<vector<char>>& board){
for(int i=0;i<board.size();i++){
for(int j=0;j<board[0].size();j++){
if(board[i][j]=='.'){
for(char a='1';a<='9';a++){
if(isvalid(board,i,j,a)){
board[i][j]=a;
if(backtracking(board)) return true;
board[i][j]='.';
}
}
return false;
}
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};

贪心算法

分发饼干

https://leetcode.cn/problems/assign-cookies/description/

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子i,都有一个胃口值g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j,都有一个尺寸 s[j] 。如果s[j] >= g[i],我们可以将这个饼干 j分配给孩子 i,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 :

1
2
3
4
5
6
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 :

1
2
3
4
5
6
输入: g = [1,2], s = [1,2,3]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

思路:

这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。

然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。

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(s.begin(),s.end());
sort(g.begin(),g.end());
int index=s.size()-1;
int num= 0;
for(int i=g.size()-1; i>=0;i--){
if(index>=0&&s[index]>=g[i]){
num++;
index--;
}
}
return num;
}
};

摆动序列

https://leetcode.cn/problems/wiggle-subsequence/description/

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

给你一个整数数组 nums ,返回 nums 中作为摆动序列最长子序列的长度

示例 :

1
2
3
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3)
1
2
3
4
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8)

思路:

本题异常情况的本质,就是要考虑平坡,平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图

同时需要注意的是在判断条件语句的时候,不能简单的用判断相乘法小于0作为判断,因为存在平坡的情况

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 wiggleMaxLength(vector<int>& nums) {
if(nums.size()<=1){
return nums.size();
}
int num=1;
vector<int> differ;
for(int i=1;i<nums.size();i++){
differ.push_back(nums[i]-nums[i-1]);
}
int preDiff=0;
for(int i=0;i<differ.size();i++){
if((preDiff<=0&& differ[i]>0)||(differ[i]<0&&preDiff>=0)){
num++;
preDiff = differ[i];
}
}
return num;
}
};

最大子数组和

https://leetcode.cn/problems/maximum-subarray/description/

给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例:

1
2
3
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

思路:

这道题使用的是局部的最优贪心的思路,如果遇到让总的值小于0,那么久立刻让总的值变成0,那么下一轮就从头开始记了,同时max会每一轮进行判断是否有比当前的最大值大,如果有那么就进行替换

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

买卖股票的最佳时机

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

给你一个整数数组 prices ,其中 prices[i]表示某支股票第 i天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多 只能持有 一股股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

1
2
3
4
5
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3
总利润为 4 + 3 = 7

思路:

把利润分解为每天为单位的维度,而不是从 0 天到第 3天整体去考虑!

那么根据 prices可以得到每天的利润序列:(prices[i] - prices[i - 1]).....(prices[1] - prices[0])

相当于是每天的利润之差和0的比较,只选择为正的值,负数的情况直接忽略

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0;
for(int i=1;i<prices.size();i++){
res+=max(prices[i]-prices[i-1],0);
}
return res;
}
};

动态规划

状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。

对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

注意:动态规划的问题一般只会输出最后的一个结果,不会输出比如中间的路径等相关的值

斐波那契数列

https://leetcode.cn/problems/fibonacci-number/

斐波那契数 (通常用 F(n)表示)形成的序列称为 斐波那契数列 。该数列由01开始,后面的每一项数字都是前面两项数字的和。也就是:

1
2
F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

示例:

1
2
3
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

思路:

因为这道题给出了递推公式:F(n) = F(n - 1) + F(n - 2)

动规五部曲:

这里我们要用一个一维dp数组来保存递归的结果

  1. 确定dp数组以及下标的含义:dp[i]的定义为:第i个数的斐波那契数值是dp[i]

  2. 确定递推公式F(n) = F(n - 1) + F(n - 2)

  3. dp数组如何初始化

    1
    2
    dp[0] = 0;
    dp[1] = 1;
  4. 确定遍历顺序

    从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i- 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的

  5. 举例推导dp数组

1
2
3
4
5
6
7
8
9
10
11
12
13
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];
}
};

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬12个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

1
2
3
4
5
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1
2. 2

思路:

动态规划简单题,递推公式:dp[i] = dp[i-2]+dp[i-1];

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int climbStairs(int n) {
if(n<=2) return n;
vector<int> dp(n+1);
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i] = dp[i-2]+dp[i-1];
}
return dp[n];
}
};

最小费用爬楼梯

https://leetcode.cn/problems/min-cost-climbing-stairs/description/

给你一个整数数组 cost ,其中 cost[i]是从楼梯第 i个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为0 或下标为 1的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。

示例 1:

1
2
3
4
5
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15

思路:

动态规划可以有两个途径得到dp[i],一个是dp[i-1]一个是dp[i-2]

dp[i - 1] 跳到 dp[i] 需要花费dp[i - 1] + cost[i - 1]

dp[i - 2] 跳到 dp[i] 需要花费dp[i - 2] + cost[i - 2]

那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢?

一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n+1);
dp[0]= 0;
dp[1] = 0;
for(int i=2;i<=n;i++){
dp[i]=min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
}
return dp[n];
}
};

不同路径

一个机器人位于一个 m x n 网格的左上角(起始点在下图中标记为 “Start”)。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish” )。问总共有多少条不同的路径?

机器人路径问题

思路:

简单的动态规划问题,只需要保证每次迭代都从上面和左边进行叠加

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>>dp(m, vector<int>(n, 0));
for(int i=0;i<m;i++){
dp[i][0]=1;
}
for(int i=0;i<n;i++){
dp[0][i]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};

不同路径II

https://leetcode.cn/problems/unique-paths-ii/description/

一个机器人位于一个 m x n 网格的左上角(起始点在下图中标记为 “Start”)。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用10 来表示。

障碍物的机器人路径

思路:

和上一题的思路一样,都是需要遍历路径就行,但是这里加入了一个新的数组用来存储有障碍物的位置,因此需要额外进行标记&&obstacleGrid[i][0]==0的信息,同时遇到障碍物就不改变对应的值,直接continue就好

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

整数拆分

https://leetcode.cn/problems/integer-break/description/

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2),并使这些整数的乘积最大化。返回 你可以获得的最大乘积

示例 :

1
2
3
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

思路:

给出递推公式一个是j * (i - j)直接相乘。一个是j * dp[i - j],相当于是拆分(i - j),在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j取最大的。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
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];
}
};

香槟塔问题

我们把玻璃杯摆成金字塔的形状,其中 第一层1 个玻璃杯, 第二层2个,依次类推到第 100 层,每个玻璃杯 (250ml) 将盛有香槟。

从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。(当最底层的玻璃杯满了,香槟会流到地板上)

香槟塔问题

1
2
3
4
5
6
7
8
9
示例 1:
输入: poured(倾倒香槟总杯数) = 1, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.00000
解释: 我们在顶层(下标是(0,0))倒了一杯香槟后,没有溢出,因此所有在顶层以下的玻璃杯都是空的。

示例 2:
输入: poured(倾倒香槟总杯数) = 2, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.50000
解释: 我们在顶层(下标是(0,0)倒了两杯香槟后,有一杯量的香槟将从顶层溢出,位于(1,0)的玻璃杯和(1,1)的玻璃杯平分了这一杯香槟,所以每个玻璃杯有一半的香槟。

思路:

线性DP,令 pouredkquery_rowquery_glass分别为 nm

思路题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
double champagneTower(int poured, int query_row, int query_glass) {
double dp[110][110]={0};
dp[0][0]= (double) poured;
for(int i =0;i<=query_row;i++){
for(int j = 0;j<=i;j++){
if(dp[i][j]>1){
dp[i+1][j]+=(dp[i][j]-1)/2;
dp[i+1][j+1]+=(dp[i][j]-1)/2;
dp[i][j] = 1;
}
}
}
return dp[query_row][query_glass];
}
};

最大正方形

在一个由 '0''1'组成的二维矩阵内,找到只包含 '1'的最大正方形,并返回其面积。

示例 1:

image-20240310155051578

1
2
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4

示例 2:

1
2
输入:matrix = [["0","1"],["1","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:
int maximalSquare(vector<vector<char>>& matrix) {
if(matrix.size()==0||matrix[0].size()==0){return 0;}
int maxsize = 0;
int rows = matrix.size(), colums = matrix[0].size();
vector<vector<int>> dp(rows, vector<int>(colums));
for(int i= 0;i<rows;i++){
for(int j = 0;j<colums;j++){
if(matrix[i][j]=='1'){
if(i==0||j==0){dp[i][j]=1;}
else{
dp[i][j] = min(min(dp[i-1][j], dp[i][j-1]),dp[i-1][j-1])+1;
}
maxsize = max(maxsize, dp[i][j]);
}
}
}
return maxsize* maxsize;
}
};

戳气球

n 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的i - 1i + 1 代表和 i相邻的两个气球的序号。如果 i - 1i + 1超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:

1
2
3
4
5
输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167

思路:

这道题是一个区间DP的题目,下面的解题思路

https://leetcode.cn/problems/burst-balloons/solutions/337630/zhe-ge-cai-pu-zi-ji-zai-jia-ye-neng-zuo-guan-jian-/?envType=featured-list&envId=2cktkvj%3FenvType%3Dfeatured-list&envId=2cktkvj

关键点在于

DP的状态转移方程只和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<vector<int>> rec;
vector<int> val;
int solve(int left, int right){
if(left>=right-1) return 0;
if(rec[left][right]!=-1){return rec[left][right];}
for(int i=left+1;i<right;i++){
int sum = val[left]*val[i]* val[right];
sum+=solve(left,i)+solve(i, right);
rec[left][right] = max(rec[left][right], sum);
}
return rec[left][right];
}
int maxCoins(vector<int>& nums) {
n = nums.size();
val.resize(n+2);
for(int i = 1;i<=n;i++){
val[i] = nums[i-1];
}
val[0] = val[n+1] = 1;
rec.resize(n+2, vector<int>(n+2, -1));
return solve(0, n+1);
}
};

背包问题解题框架

0-1背包

dp[j]为容量为j的背包所背的最大价值,那么如何推导dp[j]呢?dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

dp[j - weight[i]] + value[i] 表示 容量为 j- 物品i重量 的背包 加上物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,

递推公式:

1
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

初始化:

全部初始化为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]);
}
}

整体的代码结构是

1
2
3
4
5
6
7
8
9
10
11
12
13
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
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]);
}
}
cout << dp[bagWeight] << endl;
}

完全背包问题

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

在代码层面的区别在于背包遍历的时候是从头开始到尾遍历,int j = weight[i]; j <= bagWeight; j++,因为所有的背包内部都是无限的

1、先遍历物品再遍历背包

1
2
3
4
5
6
7
8
9
10
11
12
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
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]);
}
}
cout << dp[bagWeight] << endl;
}

2、先遍历背包再遍历物品

1
2
3
4
5
6
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for(int i = 0; i < weight.size(); i++) { // 遍历物品
if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
cout << endl;
}

分割等和子集

https://leetcode.cn/problems/partition-equal-subset-sum/description/

给你一个只包含正整数非空 数组nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等示例:

1
2
3
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5][11]
1
2
3
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

思路:因为这个地方只需要写出是否能够划分,而不是具体的划分结果,因此可以考虑利用动态规划的方法来进行写,可以利用背包法来进行求解,具体步骤:

  1. 首先判断数组的和是否为偶数,是的话那么背包的大小就是总和取一半,不是的话就直接返回false
  2. 0-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:
bool canPartition(vector<int>& nums) {
int sum =0;
vector<int> dp(10001, 0);
for(int i =0;i< nums.size();i++){
sum+=nums[i];
}

if(sum%2==1) return false;
int target = sum/2;
for(int i=0;i<nums.size();i++){
for(int j=target; j>=nums[i];j--){
dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);
}
}

if(dp[target]==target) return true;
return false;

}
};

最后一块石头的重量

https://leetcode.cn/problems/last-stone-weight-ii/description/

有一堆石头,用整数数组 stones 表示。其中stones[i] 表示第 i块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为xy,且x <= y。那么粉碎的可能结果如下:

最后,最多只会剩下一块 石头。返回此石头最小的可能重量 。如果没有石头剩下,就返回0

1
2
3
4
5
6
7
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1]
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1]
组合 2 和 1,得到 1,所以数组转化为 [1,1,1]
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

思路:

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了本题物品的重量为stones[i],物品的价值也为stones[i]。对应着01背包里的物品重量weight[i]和物品价值value[i]。

这道题的关键在于如何将这堆石头尽可能平均的划分成两堆(只有这样才能让剩下的石头的重量最小),大小就是总和除以2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int> dp(15001, 0);
int sum = 0;
for(int i =0; i<stones.size();i++){
sum+=stones[i];
}
int target = sum/2;
for(int i = 0;i<stones.size();i++){
for(int j= target;j>=stones[i];j--){
dp[j] = max(dp[j], dp[j-stones[i]]+stones[i]);
}
}
return sum - dp[target]- dp[target];
}
};

目标和

https://leetcode.cn/problems/target-sum/description/

给你一个非负整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-',然后串联起所有整数,可以构造一个 表达式

返回可以通过上述方法构造的、运算结果等于 target 的不同表达式 的数目。

1
2
3
4
5
6
7
8
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,装满容量为x的背包,有几种方法

这里的x,就是bagSize,也就是我们后面要求的背包容量。

大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响

动态规划:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int i=0;i<nums.size();i++) sum+=nums[i];
if(abs(target)>sum) return 0;
if((target+sum)%2 == 1) return 0;
int bagsize = (target+sum)/2;
vector<int> dp(bagsize+1,0);
dp[0]=1;
for(int i=0;i<nums.size();i++){
for(int j=bagsize;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[bagsize];
}
};

一和零

https://leetcode.cn/problems/ones-and-zeroes/description/

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中最多m0n1

如果 x 的所有元素也是 y 的元素,集合x 是集合 y子集

1
2
3
4
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5031 的最大子集是 {"10","0001","1","0"} ,因此答案是 4
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 41 ,大于 n 的值 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for(string str: strs){
int onenum=0, zeronum=0;
for(char c :str){
if(c=='0')zeronum++;
else onenum++;
}
for(int i=m;i>=zeronum;i--){
for(int j=n; j>=onenum; j--){
dp[i][j] = max(dp[i][j], dp[i-zeronum][j- onenum]+1);
}
}
}
return dp[m][n];
}
};

零钱兑换II

https://leetcode.cn/problems/coin-change-ii/description/

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回0

假设每一种面额的硬币有无限个。

1
2
3
4
5
6
7
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

思路:

完全背包问题

注意在用完全背包问题的时候,遍历背包这个地方是从++开始,就是从coins[i]开始遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1, 0);
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];
}
};

组合总结IV

https://leetcode.cn/problems/combination-sum-iv/description/

给你一个由 不同 整数组成的数组 nums,和一个目标整数 target 。请你从 nums中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

1
2
3
4
5
6
7
8
9
10
11
12
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

思路:

题目中说这个问题每个元素可以被用好多次,因此想到完全背包问题

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

本题中的结果是需要考虑到不同数字之间的排列顺序的,所以这个地方要用到排列,先背包再物品

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

零钱兑换

https://leetcode.cn/problems/coin-change/description/

给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

1
2
3
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

确定dp数组以及下标的含义,dp[j]:凑足总额为j所需钱币的最少个数为dp[j]

确定递推公式,凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]dp[j - coins[i]] + 1就是dp[j](考虑coins[i])

所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。

递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);

完全背包问题,且不是排列问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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], dp[j-coins[i]]+1);
}
}
}
if(dp[amount]==INT_MAX) return -1;
return dp[amount];
}
};

完全平方数

https://leetcode.cn/problems/perfect-squares/

给你一个整数 n ,返回 和为 n的完全平方数的最少数量

完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311不是。

1
2
3
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

思路:

完全背包问题,注意这个地方求解的是最小数量,因此初始化的时候是用INT_MAX来进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
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++){
dp[j]= min(dp[j], dp[j-i*i]+1);
}
}
return dp[n];
}
};

打家劫舍

https://leetcode.cn/problems/house-robber/description/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

1
2
3
4
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4

思路:

当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。

递推公式为: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
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[1],nums[0]);
for(int i=2;i<nums.size();i++){
dp[i]=max(dp[i-2]+nums[i], dp[i-1]);
}
return dp[nums.size()-1];
}
};

打家劫舍II

https://leetcode.cn/problems/house-robber-ii/description/

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,今晚能够偷窃到的最高金额。

1
2
3
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

思路:

如果是一个环的情况,那么需要考虑的是如何转换,既然是首尾相连的情况,那么注意的是分成两种情况,第一种是指考虑开头和倒数第二个,第二种是指考虑第二个到最后一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0) return 0;
if(nums.size()==1) return nums[0];
int res1 = robrange(nums, 0, nums.size()-2);
int res2 = robrange(nums, 1, nums.size()-1);
return max(res1, res2);
}
int robrange(vector<int>& nums, int start, int end){
if(end==start) return nums[start];
vector<int> dp(nums.size());
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-2]+nums[i], dp[i-1]);
}
return dp[end];
}
};

买卖股票的最佳时机

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/

给定一个数组 prices ,它的第 i 个元素prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0

1
2
3
4
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

思路:

贪心

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 res = 0;
for(int i =0;i<prices.size(); i++){
low = min(low, prices[i]);
res = max(res, prices[i]-low);
}
return res;
}
};

动态规划

1

乘积最大子数组

https://leetcode.cn/problems/maximum-product-subarray/description/

给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列。

1
2
3
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

思路:

因为是乘积的问题,所以会存在如果某个值特别小或者是特别大再负负得正之后仍然会出现最大的情况,因此我们在用动态规划记录的时候不仅要记录最大值还需要记录最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int maxProduct(vector<int>& nums) {
vector<int> maxp(nums);
vector<int> minp(nums);
int res = nums[0];
for(int i=1;i<nums.size();i++){
maxp[i]=max(maxp[i-1]*nums[i], max(nums[i], minp[i-1]*nums[i]));
res = max(maxp[i], res);
minp[i]=min(minp[i-1]*nums[i], min(nums[i], maxp[i-1]*nums[i]));
}
return res;
}
};

最长有效括号

https://leetcode.cn/problems/longest-valid-parentheses/description/

给你一个只包含 '('')'的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

1
2
3
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

示例 2:

1
2
3
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

思路:

动态规划,分两类进行讨论,分别考虑最后结尾的元素是不是')'再分成两类进行讨论

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

最长连续递增序列

https://leetcode.cn/problems/longest-continuous-increasing-subsequence/

给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个l <= i < r,都有nums[i] < nums[i + 1] ,那么子序列[nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]就是连续递增子序列。

示例 1:

1
2
3
4
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

示例 2:

1
2
3
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

思路:

简单的遍历模拟并用dp数组来存储到目前为止的最长递增序列的长度,本质上也是一种贪心

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if(nums.size()<=1) return nums.size();
vector<int> dp(nums.size(),1);
int s =1;
for(int i =1;i<nums.size();i++){
if(nums[i-1]<nums[i]){dp[i] = dp[i-1]+1;}
s = max(s,dp[i]);
}
return s;
}
};

无重复字符的最长子串

https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/

给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

思路:

滑动窗口法,用一个left来记录对应的左侧没有出现重复的元素,一直进行erase()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size()<=1) return s.size();
std::unordered_set<char> lookup;
int maxstr = 0;
int left = 0;
for(int i=0;i<s.size();i++){
while(lookup.find(s[i])!=lookup.end()){
lookup.erase(s[left]);
left++;
}
maxstr = max(maxstr, i-left+1);
lookup.insert(s[i]);
}
return maxstr;
}
};

最长递增子序列

https://leetcode.cn/problems/longest-increasing-subsequence/description/

给你一个整数数组 nums,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

1
2
3
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4

思路:

既然涉及到非连续的子序列,那么在处理的时候就需要用两个指针来标记两个位置遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()<=1) return nums.size();
vector<int> dp(nums.size(),0);
for(int i =0;i<nums.size();i++){
dp[i]=1;
for(int j = 0;j<i;j++){
if(nums[j]<nums[i]){
dp[i] = max(dp[i], dp[j]+1);
}
}
}
return *max_element(dp.begin(), dp.end());;
}
};

最长重复子数组

https://leetcode.cn/problems/maximum-length-of-repeated-subarray/description/

给两个整数数组 nums1nums2 ,返回两个数组中 公共的 、长度最长的子数组的长度

示例 1:

1
2
3
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1]

思路:

在比较i,j对的时候,当然希望能够利用到前面的i-1和j-1的结果,因此用二维的动态规划来提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0));
int ans = 0;
for(int i= nums1.size()-1;i>=0;i--){
for(int j= nums2.size()-1;j>=0;j--){
dp[i][j] = nums1[i]==nums2[j]?dp[i+1][j+1]+1:0;
ans = max(ans, dp[i][j]);
}
}
return ans;
}
};

回文子串

https://leetcode.cn/problems/palindromic-substrings/description/

给你一个字符串 s ,请你统计并返回这个字符串中回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

1
2
3
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

1
2
3
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

思路:

计算有多少个回文子串的最朴素方法就是枚举出所有的回文子串,而枚举出所有的回文字串又有两种思路,分别是:

因此我们这里可以用中心拓展法来解决这个问题

中心拓展问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int countSubstrings(string s) {
int n = s.size(), ans = 0;
for(int i=0;i<2*n-1;i++){
int l = i/2, r=i/2+i%2;
while(l>=0 && r<n && s[l]==s[r]){
--l;
++r;
++ans;
}
}
return ans;
}
};

最长公共子序列

https://leetcode.cn/problems/longest-common-subsequence/

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列的长度。如果不存在 公共子序列 ,返回 0。一个字符串的 子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

示例 1:

1
2
3
输入:text1 = "abcde", text2 = "ace" 
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3

示例 2:

1
2
3
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3

思路:

使用动态规划进行求解,其中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 longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector<vector<int>> dp(m+1, vector<int>(n+1));
for(int i=1;i<=m;i++){
char c1 = text1.at(i-1);
for(int j=1; j<=n; j++){
char c2 = text2.at(j-1);
if(c1==c2){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
};

编辑距离

https://leetcode.cn/problems/edit-distance/description/

给你两个单词 word1word2请返回将word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

示例 1:

1
2
3
4
5
6
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

思路:

题目给定了两个单词,设为 A 和 B,这样我们就能够六种操作方法。

但我们可以发现,如果我们有单词 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 的一个字符。

我们用 D[i][j] 表示 A 的前 i个字母和 B 的前 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
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size();
int m = word2.size();

//其中有一个子字符串为空字符串
if (n*m==0) return n+m;

//DP数组
vector<vector<int>> dp(n+1, vector<int>(m+1));

//边界状态初始化
for(int i =0;i<n+1;i++){
dp[i][0] = i;
}
for(int j =0;j<m+1;j++){
dp[0][j] = j;
}

//循环计算DP
for(int i=1; i<n+1;i++){
for(int j=1; j<m+1; j++){
int left = dp[i-1][j]+1;
int down = dp[i][j-1]+1;
int left_down = dp[i-1][j-1];
if(word1[i-1]!=word2[j-1]) left_down +=1;
dp[i][j] = min(left, min(down, left_down));
}
}
return dp[n][m];
}
};

正则表达式匹配

https://leetcode.cn/problems/regular-expression-matching/description/

给你一个字符串 s 和一个字符规律p,请你来实现一个支持 '.''*'的正则表达式匹配。

所谓匹配,是要涵盖 整个 字符串s的,而不是部分字符串。

示例 1:

1
2
3
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

示例 2:

1
2
3
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

1
2
3
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

思路:

  1. 定义状态: 创建一个二维数组 dp,其中dp[i][j] 表示 s 的前 i 个字符与p 的前 j 个字符是否匹配。
  2. 初始化:
    1. dp[0][0]true,因为两个空字符串是匹配的。
    2. 初始化 dp[0][j],处理模式 p 的前j 个字符与空字符串 s 的匹配情况。这通常依赖于'*' 字符,因为 '*' 可以表示重复前面的字符 0次。
  3. 状态转移方程:
    1. s[i-1] == p[j-1]p[j-1] == '.'时,dp[i][j] = dp[i-1][j-1]
    2. p[j-1] == '*' 时,需要分两种情况考虑:
      • 如果 p[j-2] 不匹配 s[i-1],则'*' 表示它前面的字符出现 0次,dp[i][j] = dp[i][j-2]
      • 如果 p[j-2] 匹配s[i-1],则存在多种可能性('*'表示前面的字符出现 0 次、1次或多次),dp[i][j] = dp[i][j-2] || dp[i][j-1] || dp[i-1][j]
  4. 循环顺序: 从左到右,从上到下遍历 dp数组。
  5. 返回结果: dp[len(s)][len(p)] 表示整个sp 是否匹配

栈&队列&单调栈

有效括号

https://leetcode.cn/problems/valid-parentheses/description/

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

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

示例 1:

1
2
输入:s = "()"
输出:true

思路:简单的判断栈

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 ismatch(char a, char b){
if((a==')'&&b=='(')||(a=='}'&&b=='{')||(a==']'&&b=='[')){
return true;
}else{
return false;
}
}
bool isValid(string s) {
stack<char>st;
if(s.size()%2!=0) return false;
for(int i=0;i<s.size();i++){
if(s[i]==')'||s[i]=='}'||s[i]==']'){
if(!st.empty()&&ismatch(s[i],st.top())){
st.pop();
}else{
return false;
}
}else{
st.push(s[i]);
}
}
return st.empty();
}
};

删除相邻重复

示例:

1
2
3
4
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"

https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
string removeDuplicates(string s) {
stack<char>st;
for(char c:s){
if(st.empty()||c!=st.top()){
st.push(c);
}else{
st.pop();
}
}
string res="";
while(!st.empty()){
res+=st.top();
st.pop();
}
reverse(res.begin(), res.end()); //注意这个地方需要反转一下字符串
return res;
}
};

逆波兰表达式求值

https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。

示例 1:

1
2
3
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

1
2
3
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

思路:

利用栈的思想模拟运算的过程,按照tokens的顺序将数字放进栈中,遇到运算符那么就拿出栈中的数字,运算结束之后再放进栈中

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 evalRPN(vector<string>& tokens) {
stack<int>st;
for(int i=0;i<tokens.size();i++){
if(tokens[i]!="/"&&tokens[i]!="+"&&tokens[i]!="-"&&tokens[i]!="*"){
st.push(stoi(tokens[i]));
}else{
int num1 = st.top();
st.pop();
int num2 = st.top();
st.pop();
if (tokens[i] == "+") st.push(num2 + num1);
if (tokens[i] == "-") st.push(num2 - num1);
if (tokens[i] == "*") st.push(num2 * num1);
if (tokens[i] == "/") st.push(num2 / num1);
}
}
int result = st.top();
return result;
}
};

前K个高频元素

https://leetcode.cn/problems/top-k-frequent-elements/description/

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

示例 1:

1
2
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

前k个高频元素的思路图

这道题目主要涉及到如下三块内容:

  1. 要统计元素出现频率
  2. 对频率排序
  3. 找出前K个高频元素

首先统计元素出现的频率,这一类的问题可以使用map来进行统计。

然后是对频率进行排序,这里我们可以使用一种容器适配器就是优先级队列

优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的completebinary 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
36
class Solution {
public:
class mycomparison{
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs){
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
//统计元素出现的频率
unordered_map<int, int>map;
for(int i=0;i<nums.size();i++){
map[nums[i]]++;
}

//对频率进行排序
//定义一个小顶堆,大小为k
priority_queue<pair<int,int>, vector<pair<int, int>>, mycomparison> pri_que;

//用固定大小为k的小顶堆,扫面所有频率的数值
for(unordered_map<int, int>::iterator it=map.begin(); it!=map.end(); it++){
pri_que.push(*it);
if(pri_que.size()>k){
pri_que.pop();
}
}

//找出前面k个高频元素,因为小顶堆先弹出的是最小的,所以倒序输出到数组
vector<int> res(k);
for(int i=k-1;i>=0;i--){
res[i]=pri_que.top().first;
pri_que.pop();
}
return res;
}
};

每日温度

给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer ,其中answer[i] 是指对于第 i天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用0 来代替。

示例 1:

1
2
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

思路:

可以选择使用单调栈的方法来求解,具体的思路是设置一个栈,遍历数组的时候和栈顶元素进行比较,小于栈顶元素的时候就需要将当前元素放入栈中

首先这道题必须有一个向量数组来存储对应位置的元素的值,vector<int> res(temperatures.size(),0)方便修改对应的元素

如果大于当前的栈顶元素的值,那么就要进行比较while循环,只要还是大于当前栈顶的元素都需要对栈顶的元素进行pop()

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> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
vector<int> res(temperatures.size(),0);
st.push(0);
for(int i=1;i<temperatures.size();i++){
if(temperatures[i]<=temperatures[st.top()]){
st.push(i);
}else{
while (!st.empty()&& temperatures[i]>temperatures[st.top()]){
res[st.top()]=i-st.top();
st.pop();
}
st.push(i);
}
}
return res;
}
};

图论

深度优先搜索理论

深度优先搜索和回溯的思路大体上是一样的

就地递归函数的下面,例如如下代码:

1
2
3
4
5
void dfs(参数) {
处理节点
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}

可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的

深度优先搜索的三部曲:

  1. 确认递归函数,参数

    深搜需要二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。

  2. 确认终止条件

    1
    2
    3
    4
    if (终止条件) {
    存放结果;
    return;
    }

    终止添加不仅是结束本层递归,同时也是我们收获结果的时候。

  3. 处理目前搜索节点出发的路径

一般这里就是一个for循环的操作,去遍历 目前搜索节点所能到的所有节点。

1
2
3
4
5
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}

广度优先搜索理论

广搜的搜索方式就适合于解决两个点之间的最短路径问题。

因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。

广度优先搜索代码模版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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>>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; //立刻标记,避免重复访问
}
}
}
}

所有可能的路径

https://leetcode.cn/problems/all-paths-from-source-to-target/description/

图路径遍历

给你一个有 n 个节点的有向无环图(DAG),请你找出所有从节点 0到节点 n-1的路径并输出(不要求按特定顺序graph[i]是一个从节点 i 可以访问的所有节点的列表(即从节点i 到节点 graph[i][j]存在一条有向边)。

1
2
3
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 30 -> 2 -> 3

思路:

深度优先搜索

注意在用dfs做题的时候需要初始化path.push_back(0)每一次都需要初始化输入这个数值起点

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<vector<int>> res;
vector<int> path;
void dfs(vector<vector<int>>& graph, int x){
if(x == graph.size()-1){
res.push_back(path);
return;
}
for(int i=0;i<graph[x].size();i++){
path.push_back(graph[x][i]);
dfs(graph,graph[x][i]);
path.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
path.push_back(0);
dfs(graph, 0);
return res;
}
};

岛屿数量

https://leetcode.cn/problems/number-of-islands/description/

给你一个由 '1'(陆地)和'0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

示例 :

1
2
3
4
5
6
7
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1

深度优先搜索版本:

思路在于利用dfs来对岛屿中的数量进行标记是否能visited,必须是联通的才能继续标记为res++

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 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 nextx = x +dir[i][0];
int nexty = y +dir[i][1];
if(nextx<0||nexty>grid.size()||nexty<0||nexty>grid[0].size) continue;
if(!visited[nextx][nexty] && grid[nextx][nexty]=='1'){
visited[nextx][nexty] = true;
dfs(grid, visited, nextx, nexty);
}
}
}
int numIslands(vector<vector<char>>& grid){
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for(int i=0;i< n;i++){
for(int j=0;j<m;j++){
if(!visited[i][j]&&grid[i][j]=='1'){
visited[i][j]=true;
res++;
dfs(grid, visited, i, j);
}
}
}
return res;
}
};

广度优先搜索版本

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 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>>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||nextt>grid[0].size()) continue;
if(!visited[nextx][nexty]&&grid[nextx][nexty]=='1'){
que.push({nextx,nexty});
visited[nextx][nexty] = true;
}
}
}
}
int numIslands(vector<vector<char>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(!visited[i][j]&&gird[i][j]=='1'){
res++;
bfs(grid, visited, i, j);
}
}
}
return res;
}
};

岛屿的最大面积

https://leetcode.cn/problems/max-area-of-island/description/

给你一个大小为 m x n 的二进制矩阵 grid

岛屿 是由一些相邻的 1 (代表土地)构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直的四个方向上 相邻。你可以假设grid 的四个边缘都被 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
class Solution {
public:
int count;
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 nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
if(!visited[nextx][nexty]&& grid[nextx][nexty]==1){
visited[nextx][nexty]=true;
count++;
dfs(grid, visited, nextx, nexty);
}
}
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int n = grid.size(), m=grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!visited[i][j] && grid[i][j] == 1) {
count = 1;
visited[i][j] = true;
dfs(grid, visited, i, j);
res = max(res, count);
}
}
}
return res;
}
};

广度优先搜索

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 Solution {
public:
int count = 0;
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
void bfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y){
queue<int> que;
que.push(x);
que.push(y);
visited[x][y]=true;
count++;
while(!que.empty()){
int xx = que.front();que.pop();
int yy = que.front();que.pop();
for(int i=0;i<4;i++){
int nextx = xx+dir[i][0];
int nexty = yy+dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
if(!visited[nextx][nexty]&& grid[nextx][nexty]==1){
count++;
visited[nextx][nexty]=true;
que.push(nextx);
que.push(nexty);
}
}
}
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
int n= grid.size(), m=grid[0].size();
vector<vector<bool>>visited = vector<vector<bool>>(n, vector<bool>(m, false));
int res = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(!visited[i][j]&&grid[i][j]==1){
count =0;
bfs(grid, visited, i, j);
res = max(res , count);
}
}
}
return res;
}
};

飞地的数量

https://leetcode.cn/problems/number-of-enclaves/description/

给你一个大小为 m x n 的二进制矩阵 grid,其中 0 表示一个海洋单元格、1表示一个陆地单元格。一次 移动是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过grid 的边界。返回网格中 无法在任意次数的移动中离开网格边界的陆地单元格的数量。

飞地的数量

思路:

利用dfs或者是bfs的方法先把边界周围的岛屿全部去除为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
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){
grid[x][y] =0;
count++;
for(int i= 0;i<4;i++){
int nextx = x +dir[i][0];
int nexty = y +dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
if(grid[nextx][nexty]==0) continue;

dfs(grid, nextx, nexty);
}
}
int numEnclaves(vector<vector<int>>& grid) {
int n = grid.size(), m=grid[0].size();
for(int i=0;i<n;i++){
if(grid[i][0]==1) dfs(grid, i, 0);
if(grid[i][m-1]==1) dfs(grid, i, m-1);
}
for(int j=0;j<m;j++){
if(grid[0][j]==1) dfs(grid, 0, j);
if(grid[n-1][j]==1) dfs(grid, n-1, j);
}

count = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]==1) dfs(grid, i, j);
}
}
return count;
}
};

被围绕的区域

https://leetcode.cn/problems/surrounded-regions/description/

给你一个 m x n 的矩阵 board ,由若干字符'X''O' ,找到所有被 'X'围绕的区域,并将这些区域里所有的 'O''X'填充。

被围绕的区域

思路:

利用深度优先搜索的方法先将边界的联通区域全部设置为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
27
28
29
30
31
32
33
34
35
class Solution {
public:
int dir[4][2]={0,1,1,0,-1,0,0,-1};
void dfs(vector<vector<char>>& board, int x, int y){
board[x][y]='A';
for(int i=0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx<0||nextx>=board.size()||nexty<0||nexty>=board[0].size()) continue;
if(board[nextx][nexty]=='X'||board[nextx][nexty]=='A') continue;
dfs(board, nextx, nexty);
}
return;
}
void solve(vector<vector<char>>& board) {
int n= board.size(), m=board[0].size();
for(int i=0;i<n;i++){
if(board[i][0]=='O') dfs(board, i,0);
if(board[i][m-1]=='O') dfs(board, i, m-1);
}

for(int j=0;j<m;j++){
if(board[0][j]=='O') dfs(board, 0, j);
if(board[n-1][j]=='O') dfs(board, n-1, j);
}

for(int i =0; i<n ;i++){
for(int j=0;j<m;j++){
if(board[i][j]=='O') board[i][j]='X';;
if(board[i][j]=='A') board[i][j]='O';
}
}

}
};

太平洋大西洋水流问题

https://leetcode.cn/problems/pacific-atlantic-water-flow/description/

有一个 m × n 的矩形岛屿,与 太平洋大西洋 相邻。 “太平洋”处于大陆的左边界和上边界,而 “大西洋”处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个m x n 的整数矩阵 heightsheights[r][c] 表示坐标 (r, c) 上单元格高于海平面的高度

岛上雨水较多,如果相邻单元格的高度 小于或等于当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回网格坐标 result2D 列表 ,其中result[i] = [ri, ci] 表示雨水从单元格 (ri, ci)流动 既可流向太平洋也可流向大西洋

太平洋大西洋水流问题

思路:

这道题本质上是在考虑连通图能不能到太平洋和大西洋,给出两个函数,dfs是对每一个点能够到的位置进行标记,isResult是遍历每一个节点是不是能够通过联通到达太平洋和大西洋

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
class Solution {
public:
int dir[4][2] = {0,1,1,0,-1,0,0,-1};
void dfs(vector<vector<int>>& heights, vector<vector<bool>>& visited, int x, int y){
if (visited[x][y]) return;
visited[x][y] = true;
for(int i =0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx<0||nextx>=heights.size()||nexty<0||nexty>=heights[0].size()) continue;
if (heights[x][y] < heights[nextx][nexty]) continue;

dfs(heights, visited, nextx, nexty);

}
}
bool isResult(vector<vector<int>>& heights, int x, int y){
vector<vector<bool>> visited =vector<vector<bool>>(heights.size(), vector<bool>(heights[0].size(), false));
// dfs选择当前这个点能到达的全部点
dfs(heights, visited, x, y);
bool isPacific = false;
bool isAtlantic = false;
for(int j=0;j<heights[0].size();j++){
if(visited[0][j]){
isPacific = true;
break;
}
}
for(int i=0;i<heights.size();i++){
if(visited[i][0]){
isPacific = true;
break;
}
}

for(int j=0;j<heights[0].size();j++){
if(visited[heights.size()-1][j]){
isAtlantic = true;
break;
}
}

for(int i =0;i<heights.size();i++){
if(visited[i][heights[0].size()-1]){
isAtlantic = true;
break;
}
}
if (isAtlantic && isPacific) return true;
return false;
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
vector<vector<int>> result;
// 遍历每一个点,看是否能同时到达太平洋和大西洋
for (int i = 0; i < heights.size(); i++) {
for (int j = 0; j < heights[0].size(); j++) {
if (isResult(heights, i, j)) result.push_back({i, j});
}
}
return result;

}
};

最大人工岛

https://leetcode.cn/problems/making-a-large-island/description/

给你一个大小为 n x n 二进制矩阵 grid最多 只能将一格 0 变成 1

返回执行此操作后,grid 中最大的岛屿面积是多少?

岛屿 由一组上、下、左、右四个方向相连的1 形成。

示例 1:

1
2
3
输入: grid = [[1, 0], [0, 1]]
输出: 3
解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。

示例 2:

1
2
3
输入: grid = [[1, 1], [1, 0]]
输出: 4
解释: 将一格0变成1,岛屿的面积扩大为 4

示例 3:

1
2
3
输入: grid = [[1, 1], [1, 1]]
输出: 4
解释: 没有0可以让我们变成1,面积依然为 4

思路:

当完成编号之后,第二步骤就是对每个没有编号的节点进行相邻岛屿的面积遍历增加

注意这一步完成之后要对每个不同岛屿的面积数量进行记录,这个地方可以用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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Solution {
public:
int count;
int dir[4][2]={0,1,1,0,-1,0,0,-1};
//第一步dfs对地图已经有的岛屿数量进行标记
void dfs(vector<vector<int>>&grid, vector<vector<bool>>& visited, int x, int y, int mark){
if(visited[x][y]||grid[x][y]==0) return;
visited[x][y]=true;
grid[x][y] = mark;
count++;
for(int i=0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y + dir[i][1];
if(nextx<0||nextx>=grid.size()||nexty<0||nexty>=grid[0].size()) continue;
dfs(grid, visited, nextx, nexty, mark);
}
}
int largestIsland(vector<vector<int>>& grid) {
int n =grid.size(), m=grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
unordered_map<int, int>gridNum;
int mark = 2; //对每个岛屿进行编号
bool isALLgrid = true;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]==0) isALLgrid = false;
if(!visited[i][j]&&grid[i][j]==1){
count = 0;
dfs(grid, visited, i, j, mark);
gridNum[mark] = count; //利用map来对所有岛屿的信息进行编号并记录
mark++;
}
}
}
if (isALLgrid) return n*m; // 如果全部是岛屿的情况,那么这个时候就不需要增加新的面积

//以下的逻辑是对需要增添岛屿的情况进行记录
int res = 0;
unordered_set<int> visitedGrid; //标记访问过的岛屿
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
int count =1;
visitedGrid.clear(); //每次都需要将这个已经访问过的岛屿进行清除
if(grid[i][j]==0){
for(int k=0;k<4;k++){
int neari = i+dir[k][0];
int nearj = j+dir[k][1];
if(neari<0||nearj<0||neari>=grid.size()||nearj>=grid[0].size()) continue;
if(visitedGrid.count(grid[neari][nearj])) continue;
count+=gridNum[grid[neari][nearj]];
visitedGrid.insert(grid[neari][nearj]);
}
}
res = max(res, count);
}
}
return res;
}
};

单词接龙

https://leetcode.cn/problems/word-ladder/description/

字典 wordList 中从单词 beginWordendWord转换序列是一个按下述规格形成的序列beginWord -> s1 -> s2 -> ... -> sk

给你两个单词 beginWordendWord和一个字典 wordList ,返回 beginWordendWord最短转换序列 中的单词数目 。如果不存在这样的转换序列,返回0

示例 1:

1
2
3
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5

示例 2:

1
2
3
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
输出:0
解释:endWord "cog" 不在字典中,所以无法进行转换。

思路:

这道题要解决两个问题:

首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个,所以判断点与点之间的关系,要自己判断是不是差一个字符,如果差一个字符,那就是有链接。

然后就是求起点和终点的最短路径长度,这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。

本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路。而广搜只要达到终点,一定是最短路。

广度优先搜索一定是能找到最短路径

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) {
//将vector转化成unordered_set提高查询速度
unordered_set<string> wordSet(wordList.begin(), wordList.end());
// 如果endWorld没有在worldSet里面出现,直接返回0
if(wordSet.find(endWord)==wordSet.end()) return 0;

unordered_map<string, int>visitMap;
queue<string> que;
que.push(beginWord);
visitMap.insert(pair<string, int>(beginWord, 1)); //map是用于记录路径的长度

while(!que.empty()){
string word = que.front();
que.pop();
int path = visitMap[word]; //这个word的路径长度
for(int i=0;i<word.size();i++){
string newWord = word;
for(int j=0;j<26;j++){
newWord[i]=j+'a';
if(newWord ==endWord) return path+1;

if(wordSet.find(newWord)!=wordSet.end()&& visitMap.find(newWord)==visitMap.end()){
visitMap.insert(pair<string, int>(newWord, path+1));
que.push(newWord);
}
}
}
}
return 0;
}
};

钥匙和房间

https://leetcode.cn/problems/keys-and-rooms/description/

n 个房间,房间按从 0n - 1 编号。最初,除 0号房间外的其余所有房间都被锁住。你的目标是进入所有的房间。然而,你不能在没有获得钥匙的时候进入锁住的房间。

当你进入一个房间,你可能会在里面找到一套不同的钥匙,每把钥匙上都有对应的房间号,即表示钥匙可以打开的房间。你可以拿上所有钥匙去解锁其他房间。

给你一个数组 rooms 其中 rooms[i] 是你进入i 号房间可以获得的钥匙集合。如果能进入所有 房间返回 true,否则返回false

示例 1:

1
2
3
4
5
6
7
8
输入:rooms = [[1],[2],[3],[]]
输出:true
解释:
我们从 0 号房间开始,拿到钥匙 1
之后我们去 1 号房间,拿到钥匙 2
然后我们去 2 号房间,拿到钥匙 3
最后我们去了 3 号房间。
由于我们能够进入每个房间,我们返回 true

示例 2:

1
2
3
输入:rooms = [[1,3],[3,0,1],[2],[0]]
输出:false
解释:我们不能进入 2 号房间。

思路:

这是在找一个有向图,如果出现有孤立的问题那么就不能开门进入,一定要全部连通,除此之外还需要注意的是这个是一个有向图的问题,还需要注意方向

有向量但不连通

因此需要用DFS来求解

其实本质上就是在利用深度优先搜索来进行遍历查找,如果能找到的话那么就进行标记,最后再进行筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
void dfs(const vector<vector<int>>& rooms, int key, vector<bool>& visited){
if(visited[key]) return;
visited[key] = true;
vector<int> keys = rooms[key];
for(int key :keys){
dfs(rooms, key, visited);
}
}
bool canVisitAllRooms(vector<vector<int>>& rooms) {
vector<bool> visited(rooms.size(),false);
dfs(rooms, 0, visited);
for(int i:visited){
if(i==false) return false;
}
return true;
}
};

岛屿的周长

https://leetcode.cn/problems/island-perimeter/description/

给定一个 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
class Solution {
public:
int dir[4][2]={0,1,1,0,-1,0,0,-1};
int islandPerimeter(vector<vector<int>>& grid) {
int res = 0;
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 x= i+dir[k][0];
int y = j+dir[k][1];
if(x<0||y<0||x>=grid.size()||y>=grid[0].size()||grid[x][y]==0){
res++;
}
}
}
}
}
return res;
}
};

并查集理论基础

并查集常用来解决连通性问题。当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集,并查集主要有两个功能:

基础理论知识

将两条边加入并查集

1
2
3
4
5
6
7
// 将v,u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}

判断两个边是不是在同一个集合中寻根

给出A元素,就可以通过 father[A] = B,father[B] = C,找到根为 C。

给出B元素,就可以通过 father[B] = C,找到根也为为 C,说明 A 和 B是在同一个集合里。大家会想第一段代码里find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根过程,代码如下:

1
2
3
4
5
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u; // 如果根就是自己,直接返回
else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}
1
2
3
4
5
6
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}

最后我们如何判断两个元素是否在同一个集合里,如果通过 find函数 找到两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下:

1
2
3
4
5
6
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}

路径压缩就一行代码:

1
2
3
4
5
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u;
else return father[u] = find(father[u]); // 路径压缩
}

以上代码在C++中,可以用三元表达式来精简一下,代码如下:

1
2
3
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}

join 函数里的这段代码:

1
2
3
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回

与 isSame 函数的实现是不是重复了? 如果抽象一下呢,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 判断 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) {
if (isSame) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;

这样写可以吗? 好像看出去没问题,而且代码更精简了。

其实这么写是有问题的,在join函数中 我们需要寻找 u 和v 的根,然后再进行连线在一起,而不是直接 用 u 和 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
int n =1005;
vector<int> father = vector<int>(n, 0);
// 并查集初始化
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]);
}

// 判uv是不是同一个根
bool isSame(int u, int v){
u=find(u);
v=find(v);
return u==v;
}

void join(int u, int v){
u=find(u);
v=find(v);
//如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
if(u==v) return;
//否则就将其中一个当成另一个的父亲节点
father[v]=u;
}

寻找图中是否存在路径

https://leetcode.cn/problems/find-if-path-exists-in-graph/description/

有一个具有 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
class Solution {
public:
int n=200005;
vector<int> father = vector<int>(n,0);

//并查集初始化
void init(){
for(int i=0;i<n;i++){
father[i] = i;
}
}
//并查集里面的寻根过程
int find(int u){
if(u==father[u]) return u;
else return father[u] = find((father[u]));
}
//判断是不是在同一个根上
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u==v;
}
//将新的内容加入到并查集中
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
init();
for(int i=0;i<edges.size();i++){
join(edges[i][0],edges[i][1]);
}
return isSame(source, destination);
}
};

冗余连接

https://leetcode.cn/problems/redundant-connection/description/

树可以看成是一个连通且 无环无向图。

给定往一棵 n 个节点 (节点值 1~n)的树中添加一条边后的图。添加的边的两个顶点包含在 1n中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为n 的二维数组 edgesedges[i] = [ai, bi] 表示图中在 aibi 之间存在一条边。

请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n个节点的树。如果有多个答案,则返回数组 edges中最后出现的那个。

冗余连接图解

思路:

那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。

如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了

已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和节点B 连在一起就一定会出现环。

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 n =1005;
vector<int> father = vector<int>(n, 0);
void init(){
for(int i=0;i<n;i++){
father[i]=i;
}
}
int find(int u){
if(u==father[u]) return u;
else return father[u] = find(father[u]);
}
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u==v;
}
void join(int u, int v){
u = find(u);
v = find(v);
if(u==v) return;
father[v]=u;
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
init();
for(int i=0;i<edges.size();i++){
if(isSame(edges[i][0], edges[i][1])) return edges[i];
else (join(edges[i][0], edges[i][1]));
}
return {};
}
};

冗余连接II

https://leetcode.cn/problems/redundant-connection-ii/description/

在本问题中,有根树指满足以下条件的 有向图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n个节点(节点值不重复,从 1n)的树及一条附加的有向边构成。附加的边包含在1n中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对[ui, vi],用以表示 有向 图中连接顶点ui 和顶点 vi 的边,其中 uivi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

冗余连接2的题目解释

思路:

题目中的表述中提到该图由一个有着N个节点 (节点值不重复1, 2,..., 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Solution {
public:
static const int N =1010;
int father[N];
int n;
void init(){
for(int i =1;i<=n;i++){
father[i]=i;
}
}
int find(int u){
if(father[u]==u) return u;
else return father[u]=find(father[u]);
}
void join(int u, int v){
u = find(u);
v = find(v);
if(u==v) return;
else father[v] = u;
}
bool same(int u, int v){
u=find(u);
v = find(v);
return u==v;
}
vector<int> getRemoveEdge(const vector<vector<int>>& edges){
init();
for(int i =0;i<n;i++){
if(same(edges[i][0], edges[i][1])){ //这个表示有环的情况,需要进行删除
return edges[i];
}
join(edges[i][0], edges[i][1]);
}
return {};
}
// 删掉一条边之后判断是不是树
bool isTree(const vector<vector<int>>& edges, int deleteEdges){
init();
for(int i=0;i<n;i++){
if(i==deleteEdges) continue;
if(same(edges[i][0], edges[i][1])) return false; //出现环了因此不是树
join(edges[i][0], edges[i][1]);
}
return true;
}
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int inDegree[N]={0};
n = edges.size();
for(int i=0;i<n;i++){
inDegree[edges[i][1]]++;
}
vector<int> vec; //记录入度为2的边
for(int i=n-1;i>=0;i--){
if(inDegree[edges[i][1]]==2){
vec.push_back(i);
}
}

//如果有入度为2的节点,那么就需要删除
if(vec.size()>0){
if(isTree(edges, vec[0])){
return edges[vec[0]];
}else{
return edges[vec[1]];
}
}

return getRemoveEdge(edges);
}
};
]]>
diff --git a/search.xml b/search.xml index 12a86f3..4c06a1b 100644 --- a/search.xml +++ b/search.xml @@ -135,6 +135,59 @@ href="https://leetcode.cn/problems/spiral-matrix-ii/">https://leetcode.cn/proble
输入:n = 1
输出:[[1]]

思路:大模拟循环遍历

class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> result(n, vector<int>(n,0));
int is=0,ie=n-1,js=0,je=n-1;
int k = 1;
while(is<=ie&&js<=je){
for(int j=js;j<=je;j++)
{
result[is][j] = k++;
}
is++;
for(int i =is;i<=ie;i++)
{
result[i][je] = k++;
}
je--;
for(int j=je;j>=js;j--)
{
result[ie][j] = k++;
}
ie--;
for(int i=ie;i>=is;i--)
{
result[i][js] = k++;
}
js++;
}
return result;
}
};
+

螺旋矩阵2

+

https://leetcode.cn/problems/spiral-matrix/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

+

给你一个 mn 列的矩阵 +matrix ,请按照 顺时针螺旋顺序 +,返回矩阵中的所有元素。

+

螺旋矩阵2

+

思路:

+

主要的解题方法就是模拟,但是需要注意的是每次改变is/ie/js/je之后都需要进行一次判断一旦不满足循环的条件立马退出不进行模拟

+
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
int is = 0, js = 0, ie = m-1, je = n-1;
vector<int>res;
while(is<=ie && js<=je){
for(int j=js;j<=je;j++){
res.push_back(matrix[is][j]);
}
is++;
if(is>ie || js>je) break;
for(int i = is;i<=ie;i++){
res.push_back(matrix[i][je]);
}
je--;
if(is>ie || js>je) break;
for(int j=je;j>=js;j--){
res.push_back(matrix[ie][j]);
}
ie--;
if(is>ie || js>je) break;
for(int i =ie;i>=is;i--){
res.push_back(matrix[i][js]);
}
js++;
if(is>ie || js>je) break;
}
return res;
}
};
+

生命游戏

+

https://leetcode.cn/problems/game-of-life/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

+

根据 百度百科 +, 生命游戏 ,简称为 生命 +,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

+

给定一个包含 m × n +个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: +1 即为 活细胞 (live),或 0 +即为 死细胞 +(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

+
    +
  1. 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
  2. +
  3. 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
  4. +
  5. 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
  6. +
  7. 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
  8. +
+

下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 +m x n 网格面板 board +的当前状态,返回下一个状态。

+

image-20240606214825990

+

思路:

+
    +
  1. 给出8个方位的计算公式,-1,0,1横纵两个方向来调整
  2. +
  3. 复制一份相同的地图保证不会每次调整的时候更换初始的值
  4. +
  5. 模拟操作即可,注意每次考虑边界条件
  6. +
+
class Solution {
public:
void gameOfLife(vector<vector<int>>& board) {
int step[3] = {-1,0,1};
vector<vector<int>>origin_board(board.size(), vector<int>(board[0].size(),0));
for(int i =0;i<board.size();i++){
for(int j = 0;j<board[0].size();j++){
origin_board[i][j] = board[i][j];
}
}
for(int r = 0;r<board.size();r++){
for(int c = 0;c<board[0].size();c++){
//计算周围的活细胞数量
int alive_cell = 0;

//遍历8个方向
for(int i = 0;i<3;i++){
for(int j=0;j<3;j++){
if(!(step[i]==0 && step[j]==0)){ //表示不同时为0的情况
if(((r+step[i]>=0)&&(r+step[i]<board.size()))&&((c+step[j]>=0)&&(c+step[j]<board[0].size()))&&(origin_board[r+step[i]][c+step[j]]==1)){
alive_cell++;
}
}
}
}

if(origin_board[r][c]==1){
if ((alive_cell<2)||(alive_cell>3)){
board[r][c]=0;
}
}else{
if(alive_cell==3){
board[r][c]=1;
}
}
}
}
}
};
+

旋转矩阵

+

给定一个 n × n 的二维矩阵 matrix +表示一个图像。请你将图像顺时针旋转 90 度。

+

你必须在原地 +旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 +使用另一个矩阵来旋转图像。

+

旋转矩阵示意图

+

思路:

+
    +
  1. 顺时针旋转的思路先水平方向翻转
  2. +
  3. 再中心对称反转即可
  4. +
+
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]);
}
}
}
};

快速排序

#include <stdio.h>
#include <iostream>
#include <math.h>
#include <algorithm>
using namespace std;
int part(int* r, int low, int hight) //划分函数
{
int i = low, j = hight, pivot = r[low]; //基准元素
while (i < j)
{
while (i<j && r[j]>pivot) //从右向左开始找一个 小于等于 pivot的数值
{
j--;
}
if (i < j)
{
swap(r[i++], r[j]); //r[i]和r[j]交换后 i 向右移动一位
}
while (i < j && r[i] <= pivot) //从左向右开始找一个 大于 pivot的数值
{
i++;
}
if (i < j)
{
swap(r[i], r[j--]); //r[i]和r[j]交换后 i 向左移动一位
}
}
return i; //返回最终划分完成后基准元素所在的位置
}
void Quicksort(int* r, int low, int hight)
{
int mid;
if (low < hight)
{
mid = part(r, low, hight); // 返回基准元素位置
Quicksort(r, low, mid - 1); // 左区间递归快速排序
Quicksort(r, mid+1, hight); // 右区间递归快速排序
}
}

数组中的第K大元素

@@ -370,6 +423,39 @@ href="https://leetcode.cn/problems/move-zeroes/description/?envType=study-plan-v

思路:

使用双指针,向后遍历的过程中一旦遇到非0的元素就将其与左边指针互换并左边下标+1,凡事遇到需要交换位置的这类方法都建议能使用双指针来实现

class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0;
for(int right = 0; right<nums.size();right++){
if(nums[right]){
swap(nums[left],nums[right]);
left++;
}
}
}
};
+

删除有序数组中的重复元素

+

https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/?envType=study-plan-v2&envId=2024-spring-sprint-100

+

给你一个 非严格递增排列 的数组 nums +,请你原地 +删除重复出现的元素,使每个元素 只出现一次 +,返回删除后数组的新长度。元素的 相对顺序 应该保持 +一致 。然后返回 nums +中唯一元素的个数。

+

考虑 nums 的唯一元素的数量为 k +,你需要做以下事情确保你的题解可以被通过:

+ +

示例 1:

+
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
+

示例 2:

+
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
+

思路:

+

这道题技巧在于抓住有序的数组,可以用双指针来对数组进行遍历,定义两个指针 +fast 和 +slow分别为快指针和慢指针,快指针表示遍历数组到达的下标位置,慢指针表示下一个不同元素要填入的下标位置,初始时两个指针都指向下标 +1。

+

假设数组 nums的长度为 n。将快指针 fast 依次遍历从 1到 +n−1的每个位置,对于每个位置,如果 nums[fast]≠nums[fast−1],说明 +nums[fast]和之前的元素都不同,因此将 nums[fast]的值复制到 +nums[slow],然后将 slow的值加 1,即指向下一个位置。

+
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n = nums.size();
if(n==0){
return 0;
}
int slow = 1, fast = 1;
while(fast<n){
if(nums[fast]!=nums[fast-1]){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};

反转字符串

https://leetcode.cn/problems/reverse-string/description/