From c7f4d7fc3d4f26638769c6c0394e45dc4ae00f99 Mon Sep 17 00:00:00 2001 From: lihaibineric Date: Sun, 28 Jan 2024 14:37:28 +0800 Subject: [PATCH] Site updated: 2024-01-28 14:37:27 --- 2024/01/01/leetcode/index.html | 100 +++++++++++++++++++++++++-- 2024/01/19/docker/index.html | 33 ++++++--- local-search.xml | 4 +- search.xml | 119 ++++++++++++++++++++++++++++++--- 4 files changed, 228 insertions(+), 28 deletions(-) diff --git a/2024/01/01/leetcode/index.html b/2024/01/01/leetcode/index.html index e0eaa50..95d94a3 100644 --- a/2024/01/01/leetcode/index.html +++ b/2024/01/01/leetcode/index.html @@ -32,8 +32,10 @@ + + - + @@ -219,7 +221,7 @@ - 41k words + 44k words @@ -230,7 +232,7 @@ - 342 mins + 370 mins @@ -272,7 +274,7 @@

【算法题】LeetCode算法汇总

- Last updated on January 23, 2024 pm + Last updated on January 28, 2024 pm

@@ -999,6 +1001,94 @@

买卖股票的最佳时机

相当于是每天的利润之差和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. 确定递推公式
  4. +
  5. dp数组如何初始化
  6. +
  7. 确定遍历顺序
  8. +
  9. 举例推导dp数组
  10. +
+

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

+

斐波那契数列

+

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. +
  3. 确定递推公式F(n) = F(n - 1) + F(n - 2)

  4. +
  5. dp数组如何初始化

    +
    1
    2
    dp[0] = 0;
    dp[1] = 1;
  6. +
  7. 确定遍历顺序

    +

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

  8. +
  9. 举例推导dp数组

  10. +
+
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];
}
};
@@ -1065,7 +1155,7 @@

动态规划

Updated on
-
January 23, 2024
+
January 28, 2024
diff --git a/2024/01/19/docker/index.html b/2024/01/19/docker/index.html index 5567f7e..bed4c63 100644 --- a/2024/01/19/docker/index.html +++ b/2024/01/19/docker/index.html @@ -26,7 +26,7 @@ - + @@ -338,11 +338,14 @@

Docker主要概念

Docker的安装

版本的安装

镜像加速源

+ @@ -352,35 +355,43 @@

镜像加速源

- + - + - + - + - + - + - + - +
镜像加速器
Docker 中国官方镜像https://registry.docker-cn.comhttps://registry.docker-cn.com
DaoCloud 镜像站http://f1361db2.m.daocloud.iohttp://f1361db2.m.daocloud.io
Azure 中国镜像https://dockerhub.azk8s.cnhttps://dockerhub.azk8s.cn
科大镜像站https://docker.mirrors.ustc.edu.cnhttps://docker.mirrors.ustc.edu.cn
阿里云https://ud6340vz.mirror.aliyuncs.comhttps://ud6340vz.mirror.aliyuncs.com
七牛云https://reg-mirror.qiniu.comhttps://reg-mirror.qiniu.com
网易云https://hub-mirror.c.163.comhttps://hub-mirror.c.163.com
腾讯云https://mirror.ccs.tencentyun.comhttps://mirror.ccs.tencentyun.com
diff --git a/local-search.xml b/local-search.xml index 3d0d017..ad24a35 100644 --- a/local-search.xml +++ b/local-search.xml @@ -62,7 +62,7 @@ /2024/01/19/docker/ -

参考文档

Docker介绍

Docker是一个应用打包、分发、部署的工具,也可以把它理解为一个轻量的虚拟机,它只虚拟你软件需要的运行环境,多余的一点都不要,而普通虚拟机则是一个完整而庞大的系统,包含各种不管你要不要的软件。

Docker

特性普通虚拟机Docker
跨平台通常只能在桌面级系统运行,例如Windows/Mac,无法在不带图形界面的服务器上运行支持的系统非常多,各类 windows 和 Linux 都支持
性能性能损耗大,内存占用高,因为是把整个完整系统都虚拟出来了性能好,只虚拟软件所需运行环境,最大化减少没用的配置
自动化需要手动安装所有东西一个命令就可以自动部署好所需环境
稳定性稳定性不高,不同系统差异大稳定性好,不同系统都一样部署方式

Docker 主要功能

Docker 用途介绍

Docker主要概念

Docker的安装

版本的安装

镜像加速源

镜像加速器镜像加速器地址
Docker 中国官方镜像https://registry.docker-cn.com
DaoCloud 镜像站http://f1361db2.m.daocloud.io
Azure 中国镜像https://dockerhub.azk8s.cn
科大镜像站https://docker.mirrors.ustc.edu.cn
阿里云https://ud6340vz.mirror.aliyuncs.com
七牛云https://reg-mirror.qiniu.com
网易云https://hub-mirror.c.163.com
腾讯云https://mirror.ccs.tencentyun.com

加入以下命令:=="registry-mirrors":["https://registry.docker-cn.com,https://hub-mirror.c.163.com"]==

Docker镜像下载与软件安装

Docker 安装的优点

Docker 安装 Redis

Docker 官方镜像仓库查找 Redis :https://hub.docker.com/

查找redis镜像

找到如下的命令:

1
$ docker run --name some-redis -d redis

Docker安装ubuntu

首先先从远端拉取镜像,可以指定版本或者是最新的版本

接着本地进行run

1
2
- docker pull ubuntu:18.04
- docker run -it ubuntu /bin/bash

Docker参考文档

命令参考:https://docs.docker.com/engine/reference/commandline/run/

Docker的常见命令

Docker创建镜像

Dockerfile是用于创建自定义Docker镜像的文件,其主要作用包括配置文件、挂载点、对外暴露的端口和设置环境变量

容器其实是在镜像的最上面加了一层读写层,在运行容器里做的任何文件改动,都会写到这个读写层。如果删除了容器,也就删除了其最上面的读写层,文件改动也就丢失了。Docker使用存储驱动管理镜像每层内容及可读写层的容器层。

Dockerfile 操作常用指令

  1. FROM 镜像

    指定新镜像所基于的基础镜像,第一条指令必须为FROM指令,每创建一个镜像就需要一条 FROM 指令

    1
    2
    #指定了go的镜像:这个时候会从网上下载
    FROM golang:1.20-alpine
  2. MAINTAINER 名字

    说明新镜像的维护人信息

    1
    2
    #指定维护者的信息
    LABEL maintainer="XXXXX@gmail.com"
  3. WORKDIR 路径 为后续的 RUN、CMD、ENTRYPOINT指定工作目录

    这里定义的目录相当于是在docker的文件夹下定义新的目录并会将本地的文件内容上传上去

    1
    2
    # 定义工作目录
    WORKDIR /Workspace/ApiService
  4. RUN 命令

    在所基于的镜像上执行命令,并提交到新的镜像中

    1
    2
    #编译应用程序
    RUN go build -o api_service main.go
  5. CMD ["要运行的程序", "参数1", "参数2"]

    Dockerfile只能有一条CMD命令。如果指定多条命令,只执行最后一条命令。

    1
    2
    # 运行应用程序
    CMD ["./api_service"]
  6. EXPOSE 端口号

    指定新镜像加载到 Docker 时要开启的端口

    1
    2
    # 暴露端口
    EXPOSE 8080
  7. ENV 环境变量 变量值

    设置一个环境变量的值,会被后面的 RUN 使用

    1
    2
    3
    # 设置环境变量
    ENV BUILD_ENV local
    ENV TZ=Asia/Shanghai #容器内部的时间设置与上海所在的时区一致
  8. ADD 源文件/目录 目标文件/目录

    将源文件复制到镜像中,源文件要与 Dockerfile位于相同目录

    例子本地的文件在/Workspace/ApiService中,这个时候会将本地的/Workspace/ApiService目录下所有的文件送到docker中的/Workspace/ApiService/docker

    1
    2
    3
    4
    WORKDIR /Workspace/ApiService

    ADD docker .
    #ADD . . #这里表示相同文件夹目录直接复制
  9. VOLUME [“目录”] 在容器中创建一个挂载点

Build镜像并运行

构建dockerfile镜像并运行

要运行 Dockerfile 文件,需要使用 Docker 工具。Docker可以在不同的操作系统上运行,包括 Windows、macOS 和各种 Linux发行版。以下是一些运行 Dockerfile 的步骤:

  1. 安装 Docker 工具

在官网上下载并安装 Docker工具,根据你的操作系统选择对应版本。安装完成后,可以在终端中运行docker 命令来检查是否安装成功。

  • 进入 Dockerfile 所在目录

打开终端,进入 Dockerfile 所在目录。可以使用cd 命令切换目录。

  • 构建 Docker 镜像

使用以下命令构建 Docker 镜像:

1
2
docker build -t <镜像名称> .
#docker build -t api_service .

其中 -t参数用于指定镜像的名称,. 表示 Dockerfile所在目录。执行该命令后,Docker 会根据 Dockerfile文件中的指令创建镜像。

  • 运行 Docker 容器

使用以下命令运行 Docker 容器:

1
2
3
docker run -p <主机端口>:<容器端口> <镜像名称>
docker run -d --name my_container -p<主机端口>:<容器端口> <镜像名称>
#docker run -d --name api-container -p 8080:8080 api_service

其中 -p参数用于指定主机端口和容器端口的映射关系,<镜像名称>是在第 3 步中指定的镜像名称。执行该命令后,Docker会在后台启动一个容器,并将容器的标准输出输出到终端中。如果 Dockerfile中没有指定端口,需要在运行容器时指定 -p参数,并将容器端口映射到主机上的一个端口。否则,容器会启动失败。

-d表示的当前的容器可以在后台运行

--name表示给指定的容器起个名字,否则会随机生成名字

停止docekr容器

使用如下命令停止:

1
docker stop <container-id>

其中,<container-id>是要停止的容器的 ID。可以使用以下命令查看正在运行的容器列表:

1
docker ps

如果要停止所有容器,可以使用以下命令:

1
docker stop $(docker ps -aq)

其中,docker ps -aq命令用于列出所有容器的 ID。

Scratch最小镜像

使用scratch镜像时,确保应用程序是静态的可执行文件或者不需要额外的操作系统层,这样可以最大程度地减少镜像的大小

我们这里给出一个例子

首先在创建一个最小的镜像的时候需要一个应用程序,这个程序可以通过dockerfile生成或者是从其他地方引入,那么这里给出一个golang镜像构建应用程序api_service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 第一阶段:使用 golang 镜像构建应用程序
FROM golang:1.20-alpine AS builder

# 定义工作目录
WORKDIR /workspace

# 复制源代码到工作目录
COPY . .

# 设置环境变量
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

# 编译应用程序
RUN go build -o api_service main.go

接着第二阶段就建一个最小的轻量级的docker镜像,首先需要按照构建镜像的基本步骤确定工作目录,环境变量、端口等信息,接着通过从已经编译好的环境中获取应用程序,最后执行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 第二阶段:创建一个新的轻量级镜像
FROM scratch

# 设置工作目录
WORKDIR /app

# 从 builder 镜像中复制编译好的应用程序
COPY --from=builder /workspace/api_service .

# 设置环境变量
ENV BUILD_ENV local
ENV TZ=Asia/Shanghai

# 暴露端口
EXPOSE 8080

# 运行应用程序
CMD ["./api_service"]

写好dockerfile之后就可以构建并运行了

1
2
docker build -t api_scratch .
docker run -d --name scratch_container -p 8080:8080 api_scratch

注意:通过这个方式只会产生一个scratch镜像,仅包含一个应用程序

Docker-Compose

如果项目依赖更多的第三方软件,我们需要管理的容器就更加多,每个都要单独配置运行,指定网络。使用docker-compose把项目的多个服务集合到一起,一键运行。要把项目依赖的多个服务集合到一起,我们需要编写一个docker-compose.yml文件,描述依赖哪些服务

Wordpress安装

在dockerhub在安装wordpress的时候,所需要的服务非常多,所以需要把项目依赖的多个服务集合到一起

1
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
version: '3.1'

services:

wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html

db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql

volumes:
wordpress:
db:

首先编写一个:docker-compose.yml

接着在docker-compose.ym==l文件所在的位置==执行:docker-compose up就可以了

注意:执行完docker-composeup之后就已经生成容器并已经成功run起来了

]]>
+

参考文档

Docker介绍

Docker是一个应用打包、分发、部署的工具,也可以把它理解为一个轻量的虚拟机,它只虚拟你软件需要的运行环境,多余的一点都不要,而普通虚拟机则是一个完整而庞大的系统,包含各种不管你要不要的软件。

Docker

特性普通虚拟机Docker
跨平台通常只能在桌面级系统运行,例如Windows/Mac,无法在不带图形界面的服务器上运行支持的系统非常多,各类 windows 和 Linux 都支持
性能性能损耗大,内存占用高,因为是把整个完整系统都虚拟出来了性能好,只虚拟软件所需运行环境,最大化减少没用的配置
自动化需要手动安装所有东西一个命令就可以自动部署好所需环境
稳定性稳定性不高,不同系统差异大稳定性好,不同系统都一样部署方式

Docker 主要功能

Docker 用途介绍

Docker主要概念

Docker的安装

版本的安装

镜像加速源

镜像加速器镜像加速器地址
Docker 中国官方镜像https://registry.docker-cn.com
DaoCloud 镜像站http://f1361db2.m.daocloud.io
Azure 中国镜像https://dockerhub.azk8s.cn
科大镜像站https://docker.mirrors.ustc.edu.cn
阿里云https://ud6340vz.mirror.aliyuncs.com
七牛云https://reg-mirror.qiniu.com
网易云https://hub-mirror.c.163.com
腾讯云https://mirror.ccs.tencentyun.com

加入以下命令:=="registry-mirrors":["https://registry.docker-cn.com,https://hub-mirror.c.163.com"]==

Docker镜像下载与软件安装

Docker 安装的优点

Docker 安装 Redis

Docker 官方镜像仓库查找 Redis :https://hub.docker.com/

查找redis镜像

找到如下的命令:

1
$ docker run --name some-redis -d redis

Docker安装ubuntu

首先先从远端拉取镜像,可以指定版本或者是最新的版本

接着本地进行run

1
2
- docker pull ubuntu:18.04
- docker run -it ubuntu /bin/bash

Docker参考文档

命令参考:https://docs.docker.com/engine/reference/commandline/run/

Docker的常见命令

Docker创建镜像

Dockerfile是用于创建自定义Docker镜像的文件,其主要作用包括配置文件、挂载点、对外暴露的端口和设置环境变量

容器其实是在镜像的最上面加了一层读写层,在运行容器里做的任何文件改动,都会写到这个读写层。如果删除了容器,也就删除了其最上面的读写层,文件改动也就丢失了。Docker使用存储驱动管理镜像每层内容及可读写层的容器层。

Dockerfile 操作常用指令

  1. FROM 镜像

    指定新镜像所基于的基础镜像,第一条指令必须为FROM指令,每创建一个镜像就需要一条 FROM 指令

    1
    2
    #指定了go的镜像:这个时候会从网上下载
    FROM golang:1.20-alpine
  2. MAINTAINER 名字

    说明新镜像的维护人信息

    1
    2
    #指定维护者的信息
    LABEL maintainer="XXXXX@gmail.com"
  3. WORKDIR 路径 为后续的 RUN、CMD、ENTRYPOINT指定工作目录

    这里定义的目录相当于是在docker的文件夹下定义新的目录并会将本地的文件内容上传上去

    1
    2
    # 定义工作目录
    WORKDIR /Workspace/ApiService
  4. RUN 命令

    在所基于的镜像上执行命令,并提交到新的镜像中

    1
    2
    #编译应用程序
    RUN go build -o api_service main.go
  5. CMD ["要运行的程序", "参数1", "参数2"]

    Dockerfile只能有一条CMD命令。如果指定多条命令,只执行最后一条命令。

    1
    2
    # 运行应用程序
    CMD ["./api_service"]
  6. EXPOSE 端口号

    指定新镜像加载到 Docker 时要开启的端口

    1
    2
    # 暴露端口
    EXPOSE 8080
  7. ENV 环境变量 变量值

    设置一个环境变量的值,会被后面的 RUN 使用

    1
    2
    3
    # 设置环境变量
    ENV BUILD_ENV local
    ENV TZ=Asia/Shanghai #容器内部的时间设置与上海所在的时区一致
  8. ADD 源文件/目录 目标文件/目录

    将源文件复制到镜像中,源文件要与 Dockerfile位于相同目录

    例子本地的文件在/Workspace/ApiService中,这个时候会将本地的/Workspace/ApiService目录下所有的文件送到docker中的/Workspace/ApiService/docker

    1
    2
    3
    4
    WORKDIR /Workspace/ApiService

    ADD docker .
    #ADD . . #这里表示相同文件夹目录直接复制
  9. VOLUME [“目录”] 在容器中创建一个挂载点

Build镜像并运行

构建dockerfile镜像并运行

要运行 Dockerfile 文件,需要使用 Docker 工具。Docker可以在不同的操作系统上运行,包括 Windows、macOS 和各种 Linux发行版。以下是一些运行 Dockerfile 的步骤:

  1. 安装 Docker 工具

在官网上下载并安装 Docker工具,根据你的操作系统选择对应版本。安装完成后,可以在终端中运行docker 命令来检查是否安装成功。

  • 进入 Dockerfile 所在目录

打开终端,进入 Dockerfile 所在目录。可以使用cd 命令切换目录。

  • 构建 Docker 镜像

使用以下命令构建 Docker 镜像:

1
2
docker build -t <镜像名称> .
#docker build -t api_service .

其中 -t参数用于指定镜像的名称,. 表示 Dockerfile所在目录。执行该命令后,Docker 会根据 Dockerfile文件中的指令创建镜像。

  • 运行 Docker 容器

使用以下命令运行 Docker 容器:

1
2
3
docker run -p <主机端口>:<容器端口> <镜像名称>
docker run -d --name my_container -p<主机端口>:<容器端口> <镜像名称>
#docker run -d --name api-container -p 8080:8080 api_service

其中 -p参数用于指定主机端口和容器端口的映射关系,<镜像名称>是在第 3 步中指定的镜像名称。执行该命令后,Docker会在后台启动一个容器,并将容器的标准输出输出到终端中。如果 Dockerfile中没有指定端口,需要在运行容器时指定 -p参数,并将容器端口映射到主机上的一个端口。否则,容器会启动失败。

-d表示的当前的容器可以在后台运行

--name表示给指定的容器起个名字,否则会随机生成名字

停止docekr容器

使用如下命令停止:

1
docker stop <container-id>

其中,<container-id>是要停止的容器的 ID。可以使用以下命令查看正在运行的容器列表:

1
docker ps

如果要停止所有容器,可以使用以下命令:

1
docker stop $(docker ps -aq)

其中,docker ps -aq命令用于列出所有容器的 ID。

Scratch最小镜像

使用scratch镜像时,确保应用程序是静态的可执行文件或者不需要额外的操作系统层,这样可以最大程度地减少镜像的大小

我们这里给出一个例子

首先在创建一个最小的镜像的时候需要一个应用程序,这个程序可以通过dockerfile生成或者是从其他地方引入,那么这里给出一个golang镜像构建应用程序api_service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 第一阶段:使用 golang 镜像构建应用程序
FROM golang:1.20-alpine AS builder

# 定义工作目录
WORKDIR /workspace

# 复制源代码到工作目录
COPY . .

# 设置环境变量
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

# 编译应用程序
RUN go build -o api_service main.go

接着第二阶段就建一个最小的轻量级的docker镜像,首先需要按照构建镜像的基本步骤确定工作目录,环境变量、端口等信息,接着通过从已经编译好的环境中获取应用程序,最后执行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 第二阶段:创建一个新的轻量级镜像
FROM scratch

# 设置工作目录
WORKDIR /app

# 从 builder 镜像中复制编译好的应用程序
COPY --from=builder /workspace/api_service .

# 设置环境变量
ENV BUILD_ENV local
ENV TZ=Asia/Shanghai

# 暴露端口
EXPOSE 8080

# 运行应用程序
CMD ["./api_service"]

写好dockerfile之后就可以构建并运行了

1
2
docker build -t api_scratch .
docker run -d --name scratch_container -p 8080:8080 api_scratch

注意:通过这个方式只会产生一个scratch镜像,仅包含一个应用程序

Docker-Compose

如果项目依赖更多的第三方软件,我们需要管理的容器就更加多,每个都要单独配置运行,指定网络。使用docker-compose把项目的多个服务集合到一起,一键运行。要把项目依赖的多个服务集合到一起,我们需要编写一个docker-compose.yml文件,描述依赖哪些服务

Wordpress安装

在dockerhub在安装wordpress的时候,所需要的服务非常多,所以需要把项目依赖的多个服务集合到一起

1
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
version: '3.1'

services:

wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html

db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql

volumes:
wordpress:
db:

首先编写一个:docker-compose.yml

接着在docker-compose.ym==l文件所在的位置==执行:docker-compose up就可以了

注意:执行完docker-composeup之后就已经生成容器并已经成功run起来了

]]>
@@ -139,7 +139,7 @@ /2024/01/01/leetcode/ -

语言细节

vector的长度:

  • C++:nums.size()
  • Python:len(nums)
  • GO:len(nums)

初始化数组:

  • C++:array[n]={0} 让所有元素都是0

构造vector:

  • C++:vector result(长度,元素)
  • Python:res = [float('inf')] * len(nums)
  • GO:=make([]int,n)

for循环:

  • C++:条件小括号+循环体中括号
  • Python:冒号且不需要小括号包条件
  • GO:循环体中括号,条件按照C++写但是不需要小括号

数组

二分查找

题目描述

链接: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;
}
};

哈希表

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

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

建立索引:哈希函数

有效的字母异位词

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 是不是快乐数。

「快乐数」 定义为:

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

如果 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) 能满足:

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

示例 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/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/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;
}
};

二叉树

深搜回溯

深度优先搜索的三部曲:

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

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

  • 首先要区分private和public这两个部分分别做的内容,private主要就是写出回溯的函数主体并且可能需要的数据结构
  • public中就对函数进行跳用以及数据结构的使用
  • 第一步就是确定函数的类型和返回,这里用了一个 startindex用来存储下一次进行选择的位置点这样能够避免重复
  • 同时函数的返回类型是二位的向量结构
  • 同时定义终止条件和单层循环的内容
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/

思路:简单的深度优先搜索,但需要注意的是可以适当采用减枝操作和必要的时候添加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,这个地方是为了筛选不是重复的部分,那么如何区分开是否是同一个数组中重复的元素而不是重复利用的元素呢?

  • i>0&&candidates[i]==candidates[i-1]这个地方表明了对元素相邻之间进行比较
  • used[i-1]==false如果这个地方是false,那么说明这个元素是同一层的元素(同一个数组中的元素)
  • 注意i<candidates.size()&&sum + candidates[i] <= target为了避免出现超出时间限制的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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"]]

思路:

  • 首先给出回文字符的判断方法:首尾指针来回比较
  • 回溯算法中最关键的点在于startindex的使用,利用这个来移动s.substr(startindex, i-startindex+1)来截取并筛选出相应的字符串的值进行回文比较
  • 回溯的终止条件是startindex>=s.size()如果超出了范围那么久说明到终点了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
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),整数之间用 '.' 分隔。

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

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

示例 :

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

思路:

  • 回溯三部曲第一步:如何确定函数的类型以及参数呢?首先参数肯定包含了s、开始的位置(因为要一直往后移动并选择)、以及一个标记用于是否已经有三个点
  • 写好判断是否合法的函数,这里比较多的陷阱需要注意
  • 注意,当放进去3个点之后别忘了判断最后一位是否满足合法性的要求,容易忽略最后一位的情况
  • 注意字符串的插入 s.inset(n,'.') 和删除s.erase(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
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开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

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

安排行程

思路:

【困难】

  • 首先第一步:确定终止条件,遇到的机场个数,如果达到了(航班数量+1)

  • 记录航班的数量,使用unordered_map<string, map<string, int>> targets;来记录航班的映射关系,我定义为全局变量。

    当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。参数里还需要ticketNum,表示有多少个航班

  • 回溯的过程中,如何遍历一个机场所对应的所有机场呢?

    这里刚刚说过,在选择映射函数的时候,不能选择unordered_map<string, multiset<string>> targets,因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。

    可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效

    所以我选择了unordered_map<string, map<string, int>> targets来做机场之间的映射

1
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/

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

  • 例如, [1, 7, 4, 9, 2, 5] 是一个摆动序列 ,因为差值 (6, -3, 5, -7, 3)是正负交替出现的。

给你一个整数数组 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;
}
};

动态规划

]]>
+

语言细节

vector的长度:

  • C++:nums.size()
  • Python:len(nums)
  • GO:len(nums)

初始化数组:

  • C++:array[n]={0} 让所有元素都是0

构造vector:

  • C++:vector result(长度,元素)
  • Python:res = [float('inf')] * len(nums)
  • GO:=make([]int,n)

for循环:

  • C++:条件小括号+循环体中括号
  • Python:冒号且不需要小括号包条件
  • GO:循环体中括号,条件按照C++写但是不需要小括号

数组

二分查找

题目描述

链接: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;
}
};

哈希表

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

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

建立索引:哈希函数

有效的字母异位词

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 是不是快乐数。

「快乐数」 定义为:

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

如果 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) 能满足:

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

示例 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/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/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;
}
};

二叉树

深搜回溯

深度优先搜索的三部曲:

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

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

  • 首先要区分private和public这两个部分分别做的内容,private主要就是写出回溯的函数主体并且可能需要的数据结构
  • public中就对函数进行跳用以及数据结构的使用
  • 第一步就是确定函数的类型和返回,这里用了一个 startindex用来存储下一次进行选择的位置点这样能够避免重复
  • 同时函数的返回类型是二位的向量结构
  • 同时定义终止条件和单层循环的内容
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/

思路:简单的深度优先搜索,但需要注意的是可以适当采用减枝操作和必要的时候添加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,这个地方是为了筛选不是重复的部分,那么如何区分开是否是同一个数组中重复的元素而不是重复利用的元素呢?

  • i>0&&candidates[i]==candidates[i-1]这个地方表明了对元素相邻之间进行比较
  • used[i-1]==false如果这个地方是false,那么说明这个元素是同一层的元素(同一个数组中的元素)
  • 注意i<candidates.size()&&sum + candidates[i] <= target为了避免出现超出时间限制的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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"]]

思路:

  • 首先给出回文字符的判断方法:首尾指针来回比较
  • 回溯算法中最关键的点在于startindex的使用,利用这个来移动s.substr(startindex, i-startindex+1)来截取并筛选出相应的字符串的值进行回文比较
  • 回溯的终止条件是startindex>=s.size()如果超出了范围那么久说明到终点了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
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),整数之间用 '.' 分隔。

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

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

示例 :

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

思路:

  • 回溯三部曲第一步:如何确定函数的类型以及参数呢?首先参数肯定包含了s、开始的位置(因为要一直往后移动并选择)、以及一个标记用于是否已经有三个点
  • 写好判断是否合法的函数,这里比较多的陷阱需要注意
  • 注意,当放进去3个点之后别忘了判断最后一位是否满足合法性的要求,容易忽略最后一位的情况
  • 注意字符串的插入 s.inset(n,'.') 和删除s.erase(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
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开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

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

安排行程

思路:

【困难】

  • 首先第一步:确定终止条件,遇到的机场个数,如果达到了(航班数量+1)

  • 记录航班的数量,使用unordered_map<string, map<string, int>> targets;来记录航班的映射关系,我定义为全局变量。

    当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。参数里还需要ticketNum,表示有多少个航班

  • 回溯的过程中,如何遍历一个机场所对应的所有机场呢?

    这里刚刚说过,在选择映射函数的时候,不能选择unordered_map<string, multiset<string>> targets,因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。

    可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效

    所以我选择了unordered_map<string, map<string, int>> targets来做机场之间的映射

1
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/

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

  • 例如, [1, 7, 4, 9, 2, 5] 是一个摆动序列 ,因为差值 (6, -3, 5, -7, 3)是正负交替出现的。

给你一个整数数组 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];
}
};
]]>
diff --git a/search.xml b/search.xml index 3be341d..4b120fe 100644 --- a/search.xml +++ b/search.xml @@ -1156,11 +1156,14 @@ hub中进行管理

Docker的安装

版本的安装

镜像加速源

+ @@ -1170,35 +1173,43 @@ hub中进行管理 - + - + - + - + - + - + - + - +
镜像加速器
Docker 中国官方镜像https://registry.docker-cn.comhttps://registry.docker-cn.com
DaoCloud 镜像站http://f1361db2.m.daocloud.iohttp://f1361db2.m.daocloud.io
Azure 中国镜像https://dockerhub.azk8s.cnhttps://dockerhub.azk8s.cn
科大镜像站https://docker.mirrors.ustc.edu.cnhttps://docker.mirrors.ustc.edu.cn
阿里云https://ud6340vz.mirror.aliyuncs.comhttps://ud6340vz.mirror.aliyuncs.com
七牛云https://reg-mirror.qiniu.comhttps://reg-mirror.qiniu.com
网易云https://hub-mirror.c.163.comhttps://hub-mirror.c.163.com
腾讯云https://mirror.ccs.tencentyun.comhttps://mirror.ccs.tencentyun.com
@@ -3040,6 +3051,94 @@ href="https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/descriptio

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

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. 确定递推公式
  4. +
  5. dp数组如何初始化
  6. +
  7. 确定遍历顺序
  8. +
  9. 举例推导dp数组
  10. +
+

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

+

斐波那契数列

+

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

+

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

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

给定 n ,请计算 F(n)

+

示例:

+
输入: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. +
  3. 确定递推公式F(n) = F(n - 1) + F(n - 2)

  4. +
  5. dp数组如何初始化

    +
    dp[0] = 0;
    dp[1] = 1;
  6. +
  7. 确定遍历顺序

    +

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

  8. +
  9. 举例推导dp数组

  10. +
+
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:

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

思路:

+

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

+
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:

+
输入: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]);

+
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” )。问总共有多少条不同的路径?

+

机器人路径问题

+

思路:

+

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

+
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就好

+
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];
}
};
]]> LeetCode算法