0%

注册Servlet三大组件

简介

Servlet三大组件Servlet、Filter、Listener我们听说的比较多,在springboot当中也可以添加这三大组件,使用起来也比较方便

讲解

编写三大组件

添加三大组件之前我们需要先编写三大组件

MyServlet:

1
2
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
public class MyServlet extends HttpServlet {

/**
* 处理get请求
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("MyServlet 被调用");
resp.getWriter().write("Hello MyServlet");
}

/**
* 处理post请求
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("MyServlet 被调用");
resp.getWriter().write("Hello MyServlet");
}
}

MyFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("MyFilter 被调用");
chain.doFilter(request,response);

}

@Override
public void destroy() {

}
}

MyListener:

1
2
3
4
5
6
7
8
9
10
11
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("contextInitialized...web应用启动");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("contextDestroyed...当前web项目销毁");
}
}

进行相应的配置

servlet要添加进容器需要以ServletRegistrationBean的形式,fliter要添加进容器需要以FilterRegistrationBean的形式,Listener添加进容器需要以ServletListenerRegistrationBean的形式

1
2
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
/**
* 配置三大组件
*
* @author zhouning
*/
@Configuration
public class MyServerConfig {

@Bean
public ServletRegistrationBean myServlet(){
//和 “/myServlet”想映射
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}

@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new MyFilter());
//对于 “/hello”,“/myServlet”进行拦截
registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
return registrationBean;
}

@Bean
public ServletListenerRegistrationBean myListener(){
//
ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
return registrationBean;
}

}

最终效果

访问http://localhost:8080/hello和http://localhost:8080/myServlet

总结

springboot继续学习当中

springboot当中整合Druid

简介

jdbc是连接数据库的基础,springboot当中也可以直接使用jdbc进行相应的访问,并且在学习当中springboot整合jdbc过程中学习了一下Druid,所以记录一下。

讲解

1.jdbc配置

使用jdbc我们需要在application.yml或者application.properties进行配置,我使用的是mysql,配置如下,其中springboot是对应的数据库

1
2
3
4
5
6
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/springboot
driver-class-name: com.mysql.jdbc.Driver

2.自动建表

springboot中可以程序运行的时候运行建表和插入数据的sql语句,具体内容在DataSourceInitializer当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
boolean createSchema() {
//调用了getScripts
List<Resource> scripts = this.getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");
if (!scripts.isEmpty()) {
if (!this.isEnabled()) {
logger.debug("Initialization disabled (not running DDL scripts)");
return false;
}

String username = this.properties.getSchemaUsername();
String password = this.properties.getSchemaPassword();
//运行脚本
this.runScripts(scripts, username, password);
}

return !scripts.isEmpty();
}

void initSchema() {
//调用了getScripts
List<Resource> scripts = this.getScripts("spring.datasource.data", this.properties.getData(), "data");
if (!scripts.isEmpty()) {
if (!this.isEnabled()) {
logger.debug("Initialization disabled (not running data scripts)");
return;
}

String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword();
//运行脚本
this.runScripts(scripts, username, password);
}

}

private List<Resource> getScripts(String propertyName, List<String> resources, String fallback) {
if (resources != null) {
return this.getResources(propertyName, resources, true);
} else {
//这里得到platform
String platform = this.properties.getPlatform();
List<String> fallbackResources = new ArrayList();
//platform默认为all,可以点进去看
fallbackResources.add("classpath*:" + fallback + "-" + platform + ".sql");
fallbackResources.add("classpath*:" + fallback + ".sql");
return this.getResources(propertyName, fallbackResources, false);
}
}

总结规则就是默认运行schema.sql,schema‐all.sql,data.sql,data-all.sql脚本,但是也可以通过自定进行配置,如在apllication.yml中进行配置:

1
2
3
4
5
6
7
8
9
10
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/springboot
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initialization-mode: always
schema:
- classpath:sql/department.sql

其中initialization-mode: always在springboot2中需要进行设置,它有三种选项

1
2
3
4
5
6
7
8
public enum DataSourceInitializationMode {
ALWAYS,//每次
EMBEDDED,//仅嵌入式数据源时使用
NEVER;//从不

private DataSourceInitializationMode() {
}
}

遇到的问题:

Property spring.datasource.schema with value 'class path resource [department.sql]' is invalid: The specified resource does not exist.

我开始将department.sql直接放在resources目录下检测不到department.sql的存在,最终没有办法才创建了sql文件夹,放在下面

最终效果:

3.整合Druid数据源

Druid是阿里巴巴开源的一个数据源,主要用于java数据库连接池,可以后台对sql进行监控,是目前使用比较多的数据源之一

首先我们需要去maven仓库搜索出对应的依赖,进行加入

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>

在application.yml中的配置

1
2
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
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/springboot
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
schema:
- classpath:sql/department.sql
initialization-mode: always

initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,logback
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

编写配置文件

1
2
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
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid(){
return new DruidDataSource();
}

/**
* 配置一个管理后台的Servlet
*
*/
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(),
"/druid/*");
Map<String,String> initParams = new HashMap<>();
initParams.put("loginUsername","admin");
initParams.put("loginPassword","123456");
//默认就是允许所有访问
initParams.put("allow","");
initParams.put("deny","192.168.15.21");
bean.setInitParameters(initParams);
return bean;
}

/**
* 配置一个web监控的filter
*/
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
Map<String,String> initParams = new HashMap<>();
initParams.put("exclusions","*.js,*.css,/druid/*");
bean.setInitParameters(initParams);
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}

}

然后我们就可以以在

http://localhost:8080/druid进行后台登录然后进行查看

比如我在代码当中对建的department表进行查询,在sql监控上可以看到

总结

学习jdbc是学习springboot数据访问当中的基础,继续学习加油。

bd4442aed16acafc54c7943d34abff0edadfa74c

RESTful API 是每个程序员都应该了解并掌握的基本知识,我们在开发过程中设计 API 的时候也应该至少要满足 RESTful API 的最基本的要求(比如接口中尽量使用名词,使用 POST 请求创建资源,DELETE 请求删除资源等等,示例:GET /notes/id:获取某个指定 id 的笔记的信息)。

如果你看 RESTful API 相关的文章的话一般都比较晦涩难懂,包括我下面的文章也会提到一些概念性的东西。但是,实际上我们平时开发用到的 RESTful API 的知识非常简单也很容易概括!举个例子,如果我给你下面两个 url 你是不是立马能知道它们是干什么的!这就是 RESTful API 的强大之处!

RESTful API 可以你看到 url + http method 就知道这个 url 是干什么的,让你看到了 http 状态码(status code)就知道请求结果如何。

1
2
GET    /classes:列出所有班级
POST /classes:新建一个班级

下面的内容只是介绍了我觉得关于 RESTful API 比较重要的一些东西,欢迎补充。

一、重要概念

REST,即 REpresentational State Transfer 的缩写。这个词组的翻译过来就是”表现层状态转化”。这样理解起来甚是晦涩,实际上 REST 的全称是 Resource Representational State Transfe ,直白地翻译过来就是 “资源”在网络传输中以某种“表现形式”进行“状态转移” 。如果还是不能继续理解,请继续往下看,相信下面的讲解一定能让你理解到底啥是 REST 。

我们分别对上面涉及到的概念进行解读,以便加深理解,不过实际上你不需要搞懂下面这些概念,也能看懂我下一部分要介绍到的内容。不过,为了更好地能跟别人扯扯 “RESTful API”我建议你还是要好好理解一下!

  • 资源(Resource) :我们可以把真实的对象数据称为资源。一个资源既可以是一个集合,也可以是单个个体。比如我们的班级 classes 是代表一个集合形式的资源,而特定的 class 代表单个个体资源。每一种资源都有特定的 URI(统一资源定位符)与之对应,如果我们需要获取这个资源,访问这个 URI 就可以了,比如获取特定的班级:/class/12。另外,资源也可以包含子资源,比如 /classes/classId/teachers:列出某个指定班级的所有老师的信息
  • 表现形式(Representational):”资源”是一种信息实体,它可以有多种外在表现形式。我们把”资源”具体呈现出来的形式比如 json,xml,image,txt 等等叫做它的”表现层/表现形式”。
  • 状态转移(State Transfer) :大家第一眼看到这个词语一定会很懵逼?内心 BB:这尼玛是啥啊? 大白话来说 REST 中的状态转移更多地描述的服务器端资源的状态,比如你通过增删改查(通过 HTTP 动词实现)引起资源状态的改变。ps:互联网通信协议 HTTP 协议,是一个无状态协议,所有的资源状态都保存在服务器端。

综合上面的解释,我们总结一下什么是 RESTful 架构:

  1. 每一个 URI 代表一种资源;
  2. 客户端和服务器之间,传递这种资源的某种表现形式比如 json,xml,image,txt 等等;
  3. 客户端通过特定的 HTTP 动词,对服务器端资源进行操作,实现”表现层状态转化”。

二、REST 接口规范

1、动作

  • GET :请求从服务器获取特定资源。举个例子:GET /classes(获取所有班级)
  • POST :在服务器上创建一个新的资源。举个例子:POST /classes(创建班级)
  • PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /classes/12(更新编号为 12 的班级)
  • DELETE :从服务器删除特定的资源。举个例子:DELETE /classes/12(删除编号为 12 的班级)
  • PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。

2、路径(接口命名)

路径又称”终点”(endpoint),表示 API 的具体网址。实际开发中常见的规范如下:

  1. 网址中不能有动词,只能有名词,API 中的名词也应该使用复数。 因为 REST 中的资源往往和数据库中的表对应,而数据库中的表都是同种记录的”集合”(collection)。如果 API 调用并不涉及资源(如计算,翻译等操作)的话,可以用动词。 比如:GET /calculate?param1=11&param2=33
  2. 不用大写字母,建议用中杠 - 不用下杠 _ 比如邀请码写成 invitation-code而不是 invitation_code

Talk is cheap!来举个实际的例子来说明一下吧!现在有这样一个 API 提供班级(class)的信息,还包括班级中的学生和教师的信息,则它的路径应该设计成下面这样。

接口尽量使用名词,禁止使用动词。 下面是一些例子:

1
2
3
4
5
6
7
8
9
GET    /classes:列出所有班级
POST /classes:新建一个班级
GET /classes/classId:获取某个指定班级的信息
PUT /classes/classId:更新某个指定班级的信息(一般倾向整体更新)
PATCH /classes/classId:更新某个指定班级的信息(一般倾向部分更新)
DELETE /classes/classId:删除某个班级
GET /classes/classId/teachers:列出某个指定班级的所有老师的信息
GET /classes/classId/students:列出某个指定班级的所有学生的信息
DELETE classes/classId/teachers/ID:删除某个指定班级下的指定的老师的信息

反例:

1
2
3
/getAllclasses
/createNewclass
/deleteAllActiveclasses

理清资源的层次结构,比如业务针对的范围是学校,那么学校会是一级资源:/schools,老师: /schools/teachers,学生: /schools/students 就是二级资源。

3、过滤信息(Filtering)

如果我们在查询的时候需要添加特定条件的话,建议使用 url 参数的形式。比如我们要查询 state 状态为 active 并且 name 为 guidegege 的班级:

1
GET    /classes?state=active&name=guidegege

比如我们要实现分页查询:

1
GET    /classes?page=1&size=10 //指定第1页,每页10个数据

4、状态码(Status Codes)

状态码范围:

2xx:成功 3xx:重定向 4xx:客户端错误 5xx:服务器错误
200 成功 301 永久重定向 400 错误请求 500 服务器错误
201 创建 304 资源未修改 401 未授权 502 网关错误
403 禁止访问 504 网关超时
404 未找到
405 请求方法不对

三 HATEOAS

RESTful 的极致是 hateoas ,但是这个基本不会在实际项目中用到。

上面是 RESTful API 最基本的东西,也是我们平时开发过程中最容易实践到的。实际上,RESTful API 最好做到 Hypermedia,即返回结果中提供链接,连向其他 API 方法,使得用户不查文档,也知道下一步应该做什么。

比如,当用户向 api.example.com 的根目录发出请求,会得到这样一个文档。

1
2
3
4
5
6
{"link": {
"rel": "collection https://www.example.com/classes",
"href": "https://api.example.com/classes",
"title": "List of classes",
"type": "application/vnd.yourformat+json"
}}

上面代码表示,文档中有一个 link 属性,用户读取这个属性就知道下一步该调用什么 API 了。rel 表示这个 API 与当前网址的关系(collection 关系,并给出该 collection 的网址),href 表示 API 的路径,title 表示 API 的标题,type 表示返回类型 Hypermedia API 的设计被称为HATEOAS

在 Spring 中有一个叫做 HATEOAS 的 API 库,通过它我们可以更轻松的创建除符合 HATEOAS 设计的 API。

转载自:https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/system-design/restful-api.md

git stash保存本地修改

远程分支发生修改,本地分支修改未添加到暂存区,可以使用如下命令更新

第一步: git stash 将本地的改动暂存

第二步: git pull 获取最新更新

第三步: git stash pop 将改动还原

bug分支当中讲解了git stash

多人协作当中讲解了我们平时需要的内容

git pull、git fetch、git merge

https://www.oschina.net/translate/git-fetch-and-merge?lang=chs&p=1

查看分支

git brach 查看本地分支

git brach -r 查看远程分支

参考:

买卖股票的最佳时机

121.买卖股票的最佳时机

题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

示例 1:

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

示例 2:

1
2
3
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这里只能进行一次交易,那我们选择相对来说差距最大的两个才行,有一种动态规划的思想在里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int maxProfit(int[] prices) {
if(prices==null)
return 0;
int minPrice = Integer.MAX_VALUE;
int maxNum = 0;

for (int i = 0; i < prices.length; i++) {

minPrice = Math.min(minPrice, prices[i]);
maxNum = Math.max(maxNum, prices[i]-minPrice);
}
return maxNum;
}
}

122.买卖股票的最佳时机II

题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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

示例 2:

1
2
3
4
5
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

1
2
3
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 3 * 10 ^ 4
  • 0 <= prices[i] <= 10 ^ 4

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

可以进行多次股票买卖

思想一:贪心,贪心算法的决策是:只加正数

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int maxProfit(int[] prices) {
int profit = 0;
for(int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i -1]) {
profit = profit + prices[i] - prices[i - 1];
}
}
return profit;
}
}

思想二:峰谷法

假设给定的数组为:

[7, 1, 5, 3, 6, 4]

如果我们在图表上绘制给定数组中的数字,我们将会得到:

如果我们分析图表,那么我们的兴趣点是连续的峰和谷。

其实最大值就算波峰剪对应的波谷的累加,其实用单调栈也是同样可以做出来,思想类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int maxProfit(int[] prices) {
int i = 0;
int valley = prices[0];
int peak = prices[0];
int maxprofit = 0;
while (i < prices.length - 1) {
while (i < prices.length - 1 && prices[i] >= prices[i + 1])
i++;
valley = prices[i];
while (i < prices.length - 1 && prices[i] <= prices[i + 1])
i++;
peak = prices[i];
maxprofit += peak - valley;
}
return maxprofit;
}
}

思想三:动态规划,能用贪心的一定能够使用动态规划,只是这题的动态规划不好想,直接转载https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/solution/tan-xin-suan-fa-by-liweiwei1419-2/

想到动态规划的原因是:可以用贪心算法解决的问题,一般情况下都可以用动态规划。因此,不妨从 “状态”、“状态转移方程” 的角度考虑一下,使用动态规划的思路解决这道问题。

根据 「力扣」第 121 题的思路,需要设置一个二维矩阵表示状态。

第 1 步:定义状态
状态 dp[i][j] 定义如下

第一维 i 表示索引为 i 的那一天(具有前缀性质,即考虑了之前天数的收益)能获得的最大利润;
第二维 j 表示索引为 i 的那一天是持有股票,还是持有现金。这里 0 表示持有现金(cash),1 表示持有股票(stock)。
第 2 步:思考状态转移方程
状态从持有现金(cash)开始,到最后一天我们关心的状态依然是持有现金(cash);
每一天状态可以转移,也可以不动。状态转移用下图表示:

(状态转移方程写在代码中)

说明:

因为不限制交易次数,除了最后一天,每一天的状态可能不变化,也可能转移;
写代码的时候,可以不用对最后一天单独处理,输出最后一天,状态为 0 的时候的值即可。
第 3 步:确定起始
起始的时候:

如果什么都不做,dp[0][0] = 0;
如果买入股票,当前收益是负数,即 dp[0][1] = -prices[i];
第 4 步:确定终止
终止的时候,上面也分析了,输出 dp[len - 1][0],因为一定有 dp[len - 1][0] > dp[len - 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
public class Solution {

public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}

// 0:持有现金
// 1:持有股票
// 状态转移:0 → 1 → 0 → 1 → 0 → 1 → 0
int[][] dp = new int[len][2];

dp[0][0] = 0;
dp[0][1] = -prices[0];

for (int i = 1; i < len; i++) {
// 这两行调换顺序也是可以的
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0];
}
}

感觉十分难想象到,然后可以进行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Solution {

public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}

// cash:持有现金
// hold:持有股票
// 状态转移:cash → hold → cash → hold → cash → hold → cash

int cash = 0;
int hold = -prices[0];

int preCash = cash;
int preHold = hold;
for (int i = 1; i < len; i++) {
cash = Math.max(preCash, preHold + prices[i]);
hold = Math.max(preHold, preCash - prices[i]);

preCash = cash;
preHold = hold;
}
return cash;
}
}

123.买卖股票的最佳时机 III

题目描述

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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

示例 2:

1
2
3
4
5
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

1
2
3
输入: [7,6,4,3,1] 
输出: 0
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这次限制了交易的次数为2,这样看来前三题其实只是次数上的不同,有了上题的基础后就比较好理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int maxProfit(int[] prices) {
int T_i10 = 0, T_i11 = Integer.MIN_VALUE;
int T_i20 = 0, T_i21 = Integer.MIN_VALUE;

for (int price : prices) {
T_i20 = Math.max(T_i20, T_i21 + price);
T_i21 = Math.max(T_i21, T_i10 - price);
T_i10 = Math.max(T_i10, T_i11 + price);
T_i11 = Math.max(T_i11, -price);
}

return T_i20;
}
}

188.买卖股票的最佳时机IV

题目描述

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

1
2
3
输入: [2,4,1], k = 2
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

思路看下面思路总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public int maxProfit(int k, int[] prices) {
if (k >= prices.length >>> 1) {
//相当于k为无限的情况
int T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
int T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price);
T_ik1 = Math.max(T_ik1, T_ik0_old - price);
}

return T_ik0;
}

int[] T_ik0 = new int[k + 1];
int[] T_ik1 = new int[k + 1];
Arrays.fill(T_ik1, Integer.MIN_VALUE);

for (int price : prices) {
for (int j = k; j > 0; j--) {
T_ik0[j] = Math.max(T_ik0[j], T_ik1[j] + price);
T_ik1[j] = Math.max(T_ik1[j], T_ik0[j - 1] - price);
}
}

return T_ik0[k];
}
}

309.最佳买卖股票时机含冷冻期

题目描述

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:

1
2
3
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int maxProfit(int[] prices) {
int T_ik0_pre = 0, T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
int T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price);
T_ik1 = Math.max(T_ik1, T_ik0_pre - price);
T_ik0_pre = T_ik0_old;
}

return T_ik0;
}
}

714.买卖股票的最佳时机含手续费

题目描述

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

1
2
3
4
5
6
7
8
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

注意:

  • 0 < prices.length <= 50000.
  • 0 < prices[i] < 50000.
  • 0 <= fee < 50000.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int maxProfit(int[] prices, int fee) {
int T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
int T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price);
T_ik1 = Math.max(T_ik1, T_ik0_old - price - fee);
}

return T_ik0;
}
}

思路总结

做了k=1,k=2和k=无穷多后,我参考别人的博客,发现其实这题有一个同一的解法。翻译题解:https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems

一、分析

首先,让我们阐明表示法以简化我们的分析。令价格为长度为n的股票价格数组pricesi表示第i天(i0n-1),k表示允许完成的最大交易数,T[i][k]为在第i天结束时最多可进行k笔交易可获得的最大利润。显然,我们有基本情况:T[-1][k]= T[i][0] = 0,即没有股票或没有交易产生任何利润(请注意,第一天的i = 0,所以i = -1表示没有库存)。现在,如果我们能够以某种方式将T[i][k]与其子问题相关联,例如T [i-1] [k],T [i] [k-1],T [i-1] [k-1],...。 ..,我们将有一个有效的递归关系,并且可以递归解决该问题。那么我们如何实现呢?

最直接的方法是查看第i天采取的措施。我们有几种选择?答案是三个:买,卖,休息。我们应该选哪一个?答案是:我们并不真正知道,但是找出哪一个很容易。如果没有其他限制,我们可以尝试每一种选择,然后选择使我们的利润最大化的选择。但是,我们确实有一个额外的限制,即不允许同时进行多次交易,这意味着如果我们决定在第i天进行购买,那么在购买之前,我们手中应该持有0只股票;如果我们决定在第i天卖出,则在我们卖出之前,我们手中应该只持有1只股票。我们手中持有的股票数量是上述隐藏的因素,它将影响第i天的操作,从而影响最大利润。

因此,我们对T[i][k]的定义实际上应该分为两部分:T[i][k][0]T[i][k][1],其中前者表示在第i天交易股票获得的利润最大,k笔交易后手头有0只股票,而后者表示第i天结束时的最大利润,最多交易数k笔交易中有1只股票采取行动后,伸出一只手。现在,基本案例和递归关系可以写成:

  1. 基本情况

    T[-1][k][0] = 0, T[-1][k][1] = -Infinity
    T[i][0][0] = 0, T[i][0][1] = -Infinity

  2. 递归关系:

    T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
    T[i][k][1] = max(T[i-1][k][1], T[i-1][k-1][0] - prices[i])

    这个关系在前面有例子之后更好理解

对于基本情况,T [-1] [k] [0] = T [i] [0] [0] = 0具有与以前相同的含义,而T[-1] [k] [1] = T[i] [0][1] = -Infinity强调一个事实,即如果没有可用的库存或不允许进行交易,那么我们手头就不可能有1只库存。

对于循环关系中的T [i] [k] [0],在第i天采取的行动只能是休息和出售,因为在一天结束时我们手中的股票为0。如果采取行动休息,T [i-1] [k] [0]是最大利润,而采取行动卖出的则T[i-1][k][1] + prices[i]。请注意,由于交易包含两个成对出现的交易(即买入和卖出),因此允许交易的最大数量保持不变。只有行动购买会改变允许的最大交易数量。

对于递归关系中的T [i] [k] [1],在第i天采取的行动只能休息或者购买,因为在一天结束时我们手中只有1只股票。如果采取行动休息,T[i-1][k][1]是最大利润,如果采取行动买入, T[i-1][k-1][0] - prices[i] 是最大利润。采取。请注意,允许交易的最大数量减少了一个,因为在第i天进行购买将使用一次交易,如上所述。

要找到最后一天结束时的最大利润,我们可以简单地遍历价格数组并根据上述重复关系更新T [i] [k] [0]T [i] [k] [1]。最终答案将是T [i] [k] [0](如果最终手头有0只股票,我们总是有更大的利润)。

另外需要一提的是虽然上面是三维表达式,但是可以通过滚动数组等方式进行降维

二、实际例子

案例一:k=1,也就是第一题

对于这种情况,我们实际上每天都有两个未知变量:T[i][1][0]T[i][1][1],递归关系说:

1
2
T[i][1][0] = max(T[i-1][1][0], T[i-1][1][1] + prices[i])
T[i][1][1] = max(T[i-1][1][1], T[i-1][0][0] - prices[i]) = max(T[i-1][1][1], -prices[i])

在这里,我们针对第二个方程利用了基本情况T [i] [0] [0] = 0。所以最终变成

1
2
3
4
5
6
7
8
9
10
public int maxProfit(int[] prices) {
int T_i10 = 0, T_i11 = Integer.MIN_VALUE;

for (int price : prices) {
T_i10 = Math.max(T_i10, T_i11 + price);
T_i11 = Math.max(T_i11, -price);
}

return T_i10;
}

现在,让我们尝试对上述解决方案有所了解。如果我们更仔细地检查循环中的部分,则T_i11实际上只是表示第i天之前所有股价的负值的最大值,或者等效地表示所有股价的最小值。至于T_i10,我们只需要决定哪个动作产生更高的利润,即卖出还是休息。如果采取卖空行动,我们买入股票的价格为T_i11,即第i天之前的最小值。如果我们想获得最大的利润,这正是我们在现实中会做的事情。我应该指出,这不是解决这种情况的唯一方法。您可能会在这里找到其他不错的解决方案。

案例二:k=+Infinity,也就是第二题

如果k为正无穷大,则kk-1之间实际上没有任何区别,这意味着T[i-1][k-1][0] = T [i -1] [k] [0]T [i-1] [k-1] [1] = T [i-1] [k] [1]。因此,我们每天仍有两个未知变量:T [i] [k] [0]和T [i] [k] [1],其中k = + Infinity,递归关系说:

1
2
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-1][k-1][0] - prices[i]) = max(T[i-1][k][1], T[i-1][k][0] - prices[i])

这里我们利用了第二个方程T [i-1] [k-1] [0] = T [i-1] [k] [0]的事实。 O(n)时间和O(1)空间解如下:

1
2
3
4
5
6
7
8
9
10
public int maxProfit(int[] prices) {
int T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
int T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price);
T_ik1 = Math.max(T_ik1, T_ik0_old - price);
}

return T_ik0;

(注意:不需要缓存T_ik0的旧值,即变量T_ik0_old。特别感谢0x0101和elvina对此进行了说明。) 该解决方案提出了一种获取最大利润的贪婪策略:尽可能长的时间,在每个局部最小值处购买股票,并在紧随其后的局部最大值处出售。这等同于找到价格上涨的子阵列(股票价格阵列),并以每个子阵列的起始价格购买,同时以其最终价格出售。很容易证明这与累积利润相同,只要这样做是有利可图的,正如本文所展示的。

案例三:k=2,也就是第三题

类似于k = 1的情况,除了现在我们有四个变量而不是每天有两个变量:T [i] [1] [0],T [i] [1] [1],T [i] [2 ] [0],T [i] [2] [1]和递归关系为:

1
2
3
4
T[i][2][0] = max(T[i-1][2][0], T[i-1][2][1] + prices[i])
T[i][2][1] = max(T[i-1][2][1], T[i-1][1][0] - prices[i])
T[i][1][0] = max(T[i-1][1][0], T[i-1][1][1] + prices[i])
T[i][1][1] = max(T[i-1][1][1], -prices[i])

在这里,我们再次利用了基本情况T [i] [0] [0] = 0的最后一个方程式。O(n)时间和O(1)空间解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int maxProfit(int[] prices) {
int T_i10 = 0, T_i11 = Integer.MIN_VALUE;
int T_i20 = 0, T_i21 = Integer.MIN_VALUE;

for (int price : prices) {
T_i20 = Math.max(T_i20, T_i21 + price);
T_i21 = Math.max(T_i21, T_i10 - price);
T_i10 = Math.max(T_i10, T_i11 + price);
T_i11 = Math.max(T_i11, -price);
}

return T_i20;
}

这与此处给出的基本相同。

案例四:k是任意的

这是最普遍的情况,因此每天我们都需要在一天结束时用不同的k值(对应于0或1只股票)更新所有最大利润。但是,如果k超过某个临界值,我们可以做一个较小的优化,超过这个临界值,最大利润将不再取决于允许的交易数量,而是由可用股票的数量(价格数组的长度)限制。让我们弄清楚这个临界值是什么。 有利可图的交易至少需要两天的时间(一天买入,另一天卖出,前提是买入价低于卖出价)。如果价格数组的长度为n,则获利交易的最大数量为n / 2(整数除法)。之后,将无法进行任何有利可图的交易,这意味着最大利润将保持不变。因此,k的临界值为n / 2。如果给定的k不小于该值,即k> = n / 2,我们可以将k扩展到正无穷大,并且问题等同于情况II。 以下是O(kn)时间和O(k)空间解。如果不进行优化,则TLE会满足较大k值的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public int maxProfit(int k, int[] prices) {
if (k >= prices.length >>> 1) {
//相当于k为无限的情况
int T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
int T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price);
T_ik1 = Math.max(T_ik1, T_ik0_old - price);
}

return T_ik0;
}
//也可以由一个三维数组表示
int[] T_ik0 = new int[k + 1];
int[] T_ik1 = new int[k + 1];
Arrays.fill(T_ik1, Integer.MIN_VALUE);

for (int price : prices) {
for (int j = k; j > 0; j--) {
T_ik0[j] = Math.max(T_ik0[j], T_ik1[j] + price);
T_ik1[j] = Math.max(T_ik1[j], T_ik0[j - 1] - price);
}
}

return T_ik0[k];
}

案例五:k=+Infinity但是有冷冻期

案例与案例II非常相似,因为它们具有相同的k值,不同之处在于现在必须对递归关系进行一些修改以解决“冷却”要求。案例二的原始递归关系由下式给出

1
2
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-1][k][0] - prices[i])

但是,由于“冷却”,如果在第(i-1)天卖出股票,我们将无法在第i天购买股票。因此,在上述第二个公式中,如果要在第i天购买,我们实际上应该使用T [i-2] [k] [0]代替T [i-1] [k] [0] 。其他所有内容保持不变,并且新的重复关系是

1
2
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-2][k][0] - prices[i])

这是O(n)时间和O(1)空间解:

1
2
3
4
5
6
7
8
9
10
11
12
public int maxProfit(int[] prices) {
int T_ik0_pre = 0, T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
int T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price);
T_ik1 = Math.max(T_ik1, T_ik0_pre - price);
T_ik0_pre = T_ik0_old;
}

return T_ik0;
}

案例六:k=+Infinity但是有手续费

再次,此案例与案例II非常相似,因为它们具有相同的k值,只是现在需要对递归关系进行一些修改以解决“交易费用”的要求。案例二的原始递归关系由下式给出

1
2
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-1][k][0] - prices[i])

由于现在我们需要为每笔交易支付一定的费用(表示为费用),因此应该将第i天买卖股票后的利润减去该金额,因此新的重复关系将是

1
2
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-1][k][0] - prices[i] - fee)

或者

1
2
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i] - fee)
T[i][k][1] = max(T[i-1][k][1], T[i-1][k][0] - prices[i])

请注意,关于何时扣除费用,我们有两种选择。这是因为(如上所述)每笔交易的特点是两个动作成对出现-买卖。可以在我们购买股票(对应于第一组方程式)时或当我们出售它(对应于第二组方程式)时支付费用。以下是对应于这两个选项的O(n)时间和O(1)空间解,其中对于第二个解,我们需要注意可能的溢出。

1
2
3
4
5
6
7
8
9
10
11
public int maxProfit(int[] prices, int fee) {
int T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
int T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price);
T_ik1 = Math.max(T_ik1, T_ik0_old - price - fee);
}

return T_ik0;
}

或者

1
2
3
4
5
6
7
8
9
10
11
public int maxProfit(int[] prices, int fee) {
long T_ik0 = 0, T_ik1 = Integer.MIN_VALUE;

for (int price : prices) {
long T_ik0_old = T_ik0;
T_ik0 = Math.max(T_ik0, T_ik1 + price - fee);
T_ik1 = Math.max(T_ik1, T_ik0_old - price);
}

return (int)T_ik0;
}

二维数组中的查找

题目描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

例如:在下面这个数组当中查找数字7,返回true;查找数字5返回false

【【1 2 8 9】

​ 【3 4 9 12】

​ 【4 7 10 13】

​ 【6 8 11 15】】

解题思路

  • 首先可以想到的是直接遍历,但是想想如果直接遍历数组肯定浪费了从左到右,从上到下增大的条件所以排除
  • 然后想到的是想到的是利用给定的条件进行相对应的排除,但是怎么样才能进行对应的排除呢?发现数组的右上角和数组的左下角成为了链接的重要部分。以右上角为例。首先将目标target和右上角的这个数array[i][j](其中i=0,j=array[0].length-1)进行比较就会有三种情况产生
    • target==array[i][j],直接返回true
    • target>array[i][j],因为array[i][j]是这一行最大的,所以这一行直接排除掉了,所以搜索剩下的行就行
    • target<array[i][j],因为array[i][j]是这一列最小的,所以这一列直接排除掉了,所以搜索剩下的列就行

代码实现

java实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Solution {
public boolean Find(int target, int [][] array) {
if(array==null)
return false;
int n = array.length;
int m = array[0].length;
if(n==0||m==0)
return false;
int i = 0;
int j = m-1;
//查看最右上角的位置
while(i<n&&i>=0 && j<m && j>=0){
if(target==array[i][j])
return true;
else if(target>array[i][j])//不要写成 if 结构
i++;
else if(target<array[i][j])
j--;
}
return false;
}
}

需要注意的点:

  • 特殊输入测试:null

  • if else if不要写出if,因为当i++时,可能导致了i>=n

    下面再进行判断可能会造成越界,开始这个问题就导致我失败了,差之毫厘,谬以千里,需要注意

总结

这题是《剑指offer》上的面试题4,其实开始也不会写,看了答案才会,希望慢慢刷题后面会有进步,加油。

位运算题目小结

在刷Leetcode的时候发现许多位运算的题目,总结在一起看看能不能发现规律

78.子集

题目描述

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subsets
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这题其实属于排列组合小结里面总结过的一题,不过这里面解释另外一种思路,通过位运算的思路来解决。但是其实本质上还是排列组合里面的选取和不选取的思想。

举例:假设nums=[1,2,3,4],二进制的0可以写成0000,代表一个数也不取,1=0001表示去第一个数也就是[1],2=0010,表示取第二个数[2],3=0011表示取1和2位[1,2],4=0100表示[3]….15=1111表示[1,2,3,4]

1
2
3
4
5
6
7
8
9
10
11
public  List<List<Integer>> binaryBit(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
for (int i = 0; i < (1 << nums.length); i++) {
List<Integer> sub = new ArrayList<Integer>();
for (int j = 0; j < nums.length; j++)
//第i种情况取第j位的值,判断是不是1,是则选取
if (((i >> j) & 1) == 1) sub.add(nums[j]);
res.add(sub);
}
return res;
}

这个代码有个问题,就是nums.length超过32就处理不了,不过这个目前不用担心(因为如果超过长度超过32那用其他方法进行子集提取,估计也会非常消耗时间),这题的测试用例并没有超过32位

136.只出现一次的数字

题目描述

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

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

示例 2:

1
2
输入: [4,1,2,1,2]
输出: 4

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/single-number
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这题做过很多次了,可以使用哈希表进行处理,但是最好的方法是使用异或^进行处理,这里利用到了异或的一个特性,两个相同的数异或为0(相同为0不同为1)

1
2
3
4
5
6
7
8
9
class Solution {
public int singleNumber(int[] nums) {
int res = nums[0];
for (int i = 1; i < nums.length; i++) {
res = res ^ nums[i];
}
return res;
}
}

137.只出现一次的数字II

题目描述

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

示例 1:

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

示例 2:

1
2
输入: [0,1,0,1,0,1,99]
输出: 99

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/single-number-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这次是一个元素出现一次,其他元素出现三次。这题可以使用哈希表进行处理,但是位了训练位运算,我们采取使用位运算的思想处理。

这题看了题解之后,我想到了计算机组成原理里面的真值表法,所以我们可以使用该方法进行处理。

首先我们想到需要一个逻辑运算实现 1*1*1=01∗1∗1=0 且 01=10=10∗1=1∗0=1 ,其中 *为这种新逻辑操作符。根据这个,我们可以想到

  • 出现0次为0,出现1次为1,出现2次的值无所谓,出现3次就又回到0,也就是说,我们一共需要记录3种状态:0次,1次,2次,之后次数都是这三种状态的循环。其实这也就是一个模三运算。

  • 记录两个状态需要的是一位二进制0/1,那么记录三种状态需要的是至少两位二进制,可以是00, 01, 10, 11,这里我们只需要选其中任意三个状态即可,例如:00,01,10,分别代表0次1次2次。

  • 用00代表0次,01代表出现1次是因为刚好对应数字原本那位上0代表0次,1代表1次,这样可以方便写程序,不这么选也可以,但最后你自己要有个映射规则。至于2次的编码无所谓,10和11都可以,反正最后答案也不要它,只是个中间状态,辅助我们计算的。

  • 那么对于输入数字的每一位二进制位,都可以用这三种状态表示。如果再输入一个数字,对于每一位上,我们的操作可以化为:

    • 新输入的是0(即00),三种状态都维持不变,00$\rightarrow $00,01$\rightarrow$01,10$\rightarrow$10

    • 新输入的是1(即01),00$\rightarrow$01,01$\rightarrow$10,10$\rightarrow$00

所以可以得出真值表

当前状态XY 输入Z 输出X 输出Y
00 0 0 0
01 0 0 1
10 0 1 0
00 1 0 1
01 1 1 0
10 1 0 0

XY是指两位数表示的状态,因为1位只能表示2个状态。后面的XY表式输出后的状态,其实6个状态对应着前来说的输入为0和输入为1,比如第一行其实就是00*0$\rightarrow $00

根据真值表的求法这里,可以得到

然后我们可以根据布尔表达式化简一下

同理可得

所以答案可以是这样y

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {

public int singleNumber(int[] nums) {
int y = 0, x = 0;
for (int z : nums) {
int _y = y;
y = ~x & (y ^ z);
x = x&(~_y)&(~z)|(~x)&(_y)&(z);
}
return y;
}
}

标准答案是这样,应该是最终化简后的结果

1
2
3
4
5
6
7
8
9
10
class Solution {
int seenOnce = 0, seenTwice = 0;
public int singleNumber(int[] nums) {
for (int num : nums) {
seenOnce = ~seenTwice & (seenOnce ^ num);
seenTwice = ~seenOnce & (seenTwice ^ num);
}
return seenOnce;
}
}

思考:最终为啥返回y而不是返回x

因为最终返回的是出现次数1次的数字,只有Y为1的时候才代表该数出现一次

191.位数1的个数

题目描述

编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。

示例 1:

1
2
3
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

示例 2:

1
2
3
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。

示例 3:

1
2
3
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

提示:

  • 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
  • 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/number-of-1-bits
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

方法思路比较简单,就是n&(n-1)

1
2
3
n   = 0010
n-1 = 0001
n&(n-1) = 0000

这样就取出了末尾的1

1
2
3
4
5
6
7
8
9
10
11
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
count++;
n = n & (n - 1);
}
return count;
}
}

201.数字范围按位与

题目描述

给定范围 [m, n],其中 0 <= m <= n <= 2147483647,返回此范围内所有数字的按位与(包含 m, n 两端点)。

示例 1:

1
2
输入: [5,7]
输出: 4

示例 2:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/bitwise-and-of-numbers-range
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

参考标准答案:https://leetcode-cn.com/problems/bitwise-and-of-numbers-range/solution/shu-zi-fan-wei-an-wei-yu-by-leetcode/

其实就是找n和m的公共前缀

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int rangeBitwiseAnd(int m, int n) {
int shift = 0;
// find the common 1-bits
while (m < n) {
m >>= 1;
n >>= 1;
++shift;
}
return m << shift;
}
}

利用n&(n-1)

1
2
3
4
5
6
7
8
9
class Solution {
public int rangeBitwiseAnd(int m, int n) {
while (m < n) {
// turn off rightmost 1-bit
n = n & (n - 1);
}
return n;
}
}

总结

总结一些位运算可能使用到的规律:

  • 两个相同的数异或值为0,用于筛选出独特的数,eg:136.只出现一次的数字
  • 有时候可以使用真值表法
  • 使用n&(n-1)取出末尾的1

删除排序数组或者链表当中的重复项

在刷leetcode当中发现了一个删除重复项的问题,总结一下

删除排序数组中的重复项

题目描述

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用O(1) 额外空间的条件下完成。

示例 1:

1
2
3
4
5
给定数组 nums = [1,1,2], 

函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。

你不需要考虑数组中超出新长度后面的元素。

示例 2:

1
2
3
4
5
给定 nums = [0,0,1,1,1,2,2,3,3,4],

函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

1
2
3
4
5
6
7
8
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这种题目一看就是使用双指针的方法进行编写,但是其实我在想的时候还是没有那么直观的想出答案,不得不说还是有些巧妙的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int removeDuplicates(int[] nums) {
if(nums==null||nums.length==0)
return 0;

int last = 0; //记录上个数字的位置
int temp = nums[0];
for(int i=1;i<nums.length;i++){
if(temp!=nums[i]){
nums[last+1] = nums[i];
last++;
temp =nums[i];
}

}
return last+1;
}
}

删除排序数组中的重复项

题目描述

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

示例 1:

1
2
3
4
5
给定 nums = [1,1,1,2,2,3],

函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。

你不需要考虑数组中超出新长度后面的元素。

示例 2:

1
2
3
4
5
给定 nums = [0,0,1,1,1,1,2,3,3],

函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

1
2
3
4
5
6
7
8
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

代码思路和上面一题其实类似,但是有一个容忍度

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

public int removeDuplicates(int[] nums) {
//初始化指针
int j = 1, count = 1;


for (int i = 1; i < nums.length; i++) {

//
// If the current element is a duplicate, increment the count.
//
if (nums[i] == nums[i - 1]) {

count++;

} else {

//
// Reset the count since we encountered a different element
// than the previous one.
//
count = 1;
}

//
// For a count <= 2, we copy the element over thus
// overwriting the element at index "j" in the array
//
if (count <= 2) {
nums[j++] = nums[i];
}
}
return j;
}
}

参考链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/solution/shan-chu-pai-xu-shu-zu-zhong-de-zhong-fu-xiang-i-7/

方法还是挺妙的

删除排序链表当中的重复元素

题目描述

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

示例 1:

1
2
输入: 1->1->2
输出: 1->2

示例 2:

1
2
输入: 1->1->2->3->3
输出: 1->2->3

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这个比较简单,我是使用了两个指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head==null)return null;
if(head.next==null)return head;
ListNode curent = head;
ListNode next = head.next;

while (next!=null) {
if (curent.val==next.val) {
//如果 curent = next
while(next!=null && curent.val==next.val) {
next = next.next;
}
curent.next = next;
curent = next;
if(next!=null) next =next.next;
}else{
curent = next;
next = next.next;
}
}
return head;
}
}

官方答案比我的要简洁、优雅一些,也贴出来

1
2
3
4
5
6
7
8
9
10
11
public ListNode deleteDuplicates(ListNode head) {
ListNode current = head;
while (current != null && current.next != null) {
if (current.next.val == current.val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}

删除排序链表当中的重复元素II

题目描述

给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。

示例 1:

1
2
输入: 1->2->3->3->4->4->5
输出: 1->2->5

示例 2:

1
2
输入: 1->1->1->2->3
输出: 2->3

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

我的思路是使用三个指针进行删除,因为这个删除是必须删除掉所有重复的元素,和上面还是有一些不相同,所以规定了一个last指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head==null)return null;
if(head.next==null)return head;
ListNode last = null;
ListNode curent = head;
ListNode next = head.next;
//新的头指针
ListNode newHead = null;

while (next!=null) {
if (curent.val==next.val) {
//如果 curent = next

while(next!=null && curent.val==next.val) {
curent = curent.next;
next = next.next;
}
//需要判断 last,如果等于null
//说明从头开始就重复,如 1->1->1->2->2->3
if (last!=null) {
last.next = next;
}
curent.next = null;
curent = next;
if(next!=null) next =next.next;
if (next==null &&newHead==null ) {
//当next==null时,说明要退出
//但是此时还是没有给newHead赋值,前面连带head都是重复的
//直接赋值curent即可
//eg:1->1->1->2
newHead = curent;
}

}else{
//不相等
last = curent;
curent = next;
next = next.next;
if (newHead==null) {
newHead = last;
}
}
}
return newHead;
}
}

单调栈小结

刷Leetcode的时候遇到了一些求柱状面积之类的题目,可以使用单调栈的方法进行求解。单调栈的定义为栈中存放的数据出栈应该是有序的所以单调栈也分为单调递增栈单调递减栈

  • 单调递增栈:栈中数据出栈的序列为单调递增序列
  • 单调递减栈:栈中数据出栈的序列为单调递减序列

例子:现在有一组数10,3,7,4,12。从左到右依次入栈,则如果栈为空或入栈元素值小于栈顶元素值,则入栈;否则,如果入栈则会破坏栈的单调性,则需要把比入栈元素小的元素全部出栈。单调递减的栈反之。

1
2
3
4
5
6
7
8
9
10入栈时,栈为空,直接入栈,栈内元素为10。

3入栈时,栈顶元素10比3大,则入栈,栈内元素为10,3。

7入栈时,栈顶元素3比7小,则栈顶元素出栈,此时栈顶元素为10,比7大,则7入栈,栈内元素为10,7。

4入栈时,栈顶元素7比4大,则入栈,栈内元素为10,7,4。

12入栈时,栈顶元素4比12小,4出栈,此时栈顶元素为7,仍比12小,栈顶元素7继续出栈,此时栈顶元素为10,仍比12小,10出栈,此时栈为空,12入栈,栈内元素为12。

具体的内容可以参考链接:https://blog.csdn.net/lucky52529/article/details/89155694

接雨水

题目描述

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

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

示例:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/trapping-rain-water
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这题的解题方法还挺多种多样的,暴力、动态规划、双指针,为了切题我们先展示单调栈的做法(虽然我一个都不会),参考题解的是https://leetcode-cn.com/problems/trapping-rain-water/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-8/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int trap6(int[] height) {
int sum = 0;
Stack<Integer> stack = new Stack<>();
int current = 0;
while (current < height.length) {
//如果栈不空并且当前指向的高度大于栈顶高度就一直循环
while (!stack.empty() && height[current] > height[stack.peek()]) {
int h = height[stack.peek()]; //取出要出栈的元素
stack.pop(); //出栈
if (stack.empty()) { // 栈空就出去
break;
}
int distance = current - stack.peek() - 1; //两堵墙之前的距离。
int min = Math.min(height[stack.peek()], height[current]);
sum = sum + distance * (min - h);
}
stack.push(current); //当前指向的墙入栈
current++; //指针后移
}
return sum;
}

除了上述方法之外还有暴力法、动态规划、双指针的解法,这三种解法思想相同,都是求每一列的水,我们只需要关注当前列,以及左边最高的墙,右边最高的墙就够了。装水的多少,当然根据木桶效应,我们只需要看左边最高的墙和右边最高的墙中较矮的一个就够了。都可以查看上面那个链接进行学习

柱状图中最大的矩形

题目描述

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为[2,1,5,6,2,3]

图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。

示例:

1
2
输入: [2,1,5,6,2,3]
输出: 10

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

这题就是比较标准的单调栈了,参考题解为

两者代码其实差不多,但是一个讲解了一下单调栈+哨兵的技巧

没有加哨兵的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {

public int largestRectangleArea(int[] heights) {
int len = heights.length;
if (len == 0) {
return 0;
}
if (len == 1) {
return heights[0];
}

int res = 0;
Deque<Integer> stack = new ArrayDeque<>(len);
for (int i = 0; i < len; i++) {
// 这个 while 很关键,因为有可能不止一个柱形的最大宽度可以被计算出来
while (!stack.isEmpty() && heights[i] < heights[stack.peekLast()]) {
int curHeight = heights[stack.pollLast()];
while (!stack.isEmpty() && heights[stack.peekLast()] == curHeight) {
stack.pollLast();
}

int curWidth;
if (stack.isEmpty()) {
curWidth = i;
} else {
curWidth = i - stack.peekLast() - 1;
}

// System.out.println("curIndex = " + curIndex + " " + curHeight * curWidth);
res = Math.max(res, curHeight * curWidth);
}
stack.addLast(i);
}
//未加哨兵,所以最后怕栈里面不干净,还需要计算再计算
while (!stack.isEmpty()) {
int curHeight = heights[stack.pollLast()];
while (!stack.isEmpty() && heights[stack.peekLast()] == curHeight) {
stack.pollLast();
}
int curWidth;
if (stack.isEmpty()) {
curWidth = len;
} else {
curWidth = len - stack.peekLast() - 1;
}
res = Math.max(res, curHeight * curWidth);
}
return res;
}
}

加哨兵,发现代码简洁很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {

public int largestRectangleArea(int[] heights) {
int len = heights.length;
if (len == 0) {
return 0;
}

if (len == 1) {
return heights[0];
}

int res = 0;
//两边加0
int[] newHeights = new int[len + 2];
newHeights[0] = 0;
System.arraycopy(heights, 0, newHeights, 1, len);
newHeights[len + 1] = 0;
len += 2;
heights = newHeights;

Deque<Integer> stack = new ArrayDeque<>(len);
// 先放入哨兵,在循环里就不用做非空判断
stack.addLast(0);

for (int i = 1; i < len; i++) {
while (heights[i] < heights[stack.peekLast()]) {
int curHeight = heights[stack.pollLast()];
int curWidth = i - stack.peekLast() - 1;
res = Math.max(res, curHeight * curWidth);
}
stack.addLast(i);
}
return res;
}

public static void main(String[] args) {
// int[] heights = {2, 1, 5, 6, 2, 3};
int[] heights = {1, 1};
Solution solution = new Solution();
int res = solution.largestRectangleArea(heights);
System.out.println(res);
}
}