From b55659b6d092b5381aaf60f21f4c64679a05c80a Mon Sep 17 00:00:00 2001 From: lihaibineric Date: Sat, 3 Feb 2024 14:57:04 +0800 Subject: [PATCH] Site updated: 2024-02-03 14:57:04 --- .../go-\345\237\272\347\241\200/index.html" | 10 +- 2024/01/01/leetcode/index.html | 3 +- 2024/01/23/go-kit/index.html | 6 + 2024/01/30/deepl/index.html | 619 +++++++++++++ 2024/02/02/consul/index.html | 847 ++++++++++++++++++ archives/2023/10/index.html | 2 +- archives/2023/11/index.html | 2 +- archives/2023/12/index.html | 2 +- archives/2023/index.html | 2 +- archives/2024/01/index.html | 8 +- archives/2024/02/index.html | 338 +++++++ archives/2024/index.html | 14 +- archives/index.html | 26 +- archives/page/2/index.html | 14 +- categories/index.html | 59 +- .../index.html" | 8 +- index.html | 192 ++-- local-search.xml | 44 +- page/2/index.html | 128 +++ search.xml | 180 ++++ "tags/go\345\274\200\345\217\221/index.html" | 338 +++++++ tags/index.html | 2 +- .../index.html" | 8 +- 23 files changed, 2685 insertions(+), 167 deletions(-) create mode 100644 2024/01/30/deepl/index.html create mode 100644 2024/02/02/consul/index.html create mode 100644 archives/2024/02/index.html create mode 100644 "tags/go\345\274\200\345\217\221/index.html" diff --git "a/2023/11/27/go-\345\237\272\347\241\200/index.html" "b/2023/11/27/go-\345\237\272\347\241\200/index.html" index ed1e790..d6ba350 100644 --- "a/2023/11/27/go-\345\237\272\347\241\200/index.html" +++ "b/2023/11/27/go-\345\237\272\347\241\200/index.html" @@ -42,7 +42,7 @@ - + @@ -227,7 +227,7 @@ - 24k words + 25k words @@ -280,7 +280,7 @@

【后端开发】Golang语法基础

- Last updated on January 27, 2024 pm + Last updated on February 3, 2024 am

@@ -426,6 +426,8 @@

GO111MODULE

执行以下命令开启go mod管理

1
go env -w GO111MODULE=on
+

Go mod操作

+
1
go mod init github.com/hub/project

Go基本语法

如何编译并运行一个Go文件

对于已经写好的go文件,这里以hello.go作为例子,直接使用以下语句进行编译并运行

@@ -851,7 +853,7 @@

结构体标签

Updated on
-
January 27, 2024
+
February 3, 2024
diff --git a/2024/01/01/leetcode/index.html b/2024/01/01/leetcode/index.html index f7fa417..ee2143a 100644 --- a/2024/01/01/leetcode/index.html +++ b/2024/01/01/leetcode/index.html @@ -37,7 +37,7 @@ - + @@ -1186,6 +1186,7 @@

每日温度

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

思路:

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

+

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
vector<int> res(temperatures.size(),0);
st.push(0);
for(int i=1;i<temperatures.size();i++){
if(temperatures[i]<=temperatures[st.top()]){
st.push(i);
}else{
while (!st.empty()&& temperatures[i]>temperatures[st.top()]){
res[st.top()]=i-st.top();
st.pop();
}
st.push(i);
}
}
return res;
}
};

图论

diff --git a/2024/01/23/go-kit/index.html b/2024/01/23/go-kit/index.html index 55e94d1..be3e96b 100644 --- a/2024/01/23/go-kit/index.html +++ b/2024/01/23/go-kit/index.html @@ -494,6 +494,12 @@

实际运行结果

+ + + deepl + Previous + +
diff --git a/2024/01/30/deepl/index.html b/2024/01/30/deepl/index.html new file mode 100644 index 0000000..baf7f84 --- /dev/null +++ b/2024/01/30/deepl/index.html @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + deepl - LIHAIBIN'S BLOG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

deepl

+ +

+ + + Last updated on February 2, 2024 am + + +

+ + + + +
+
+ + + + + + +
+
+
deepl
+
https://lihaibineric.github.io/2024/01/30/deepl/
+
+
+ +
+
Author
+
Haibin Li
+
+ + +
+
Posted on
+
January 30, 2024
+
+ + +
+
Updated on
+
February 2, 2024
+
+ + +
+
Licensed under
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+ + +
+ +
+ + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2024/02/02/consul/index.html b/2024/02/02/consul/index.html new file mode 100644 index 0000000..0cf4836 --- /dev/null +++ b/2024/02/02/consul/index.html @@ -0,0 +1,847 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 【后端开发】Consul服务与配置 - LIHAIBIN'S BLOG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ + + +
+
+
+ + +
+ +
+
+
+
+

【后端开发】Consul服务与配置

+ +

+ + + Last updated on February 3, 2024 pm + + +

+ + +
+ + +

Consul介绍

+

Consul是hashicorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 +内置了服务注册与发现框架、分布一致性协议实现、健康检查、key/value存储、多数据中心方案,不再需要依赖其它工具。

+

Consul是一个服务网络解决方案,它使团队能够管理服务之间以及跨多云环境和运行时的安全网络连接。Consul提供服务发现、基于身份的授权、L7流量管理和服务到服务加密。

+

Consul示意图

+

Consul角色

+

服务发现和注册

+
    +
  • dev:开发模式
  • +
  • client:客户端,接受请求转达给服务端集群,将http和dns接口请求转发给局域网内的服务端集群,它只是一个代理的角色
  • +
  • server:服务端,保存配置信息,高可用集群,每个数据中心的server数据推荐为3个或5个
  • +
+

Consul内部角色介绍

+
    +
  • 不管是server还是client,统称为agent
  • +
  • consul client是相对无状态的,只负责转发rpcserver,资源开销很少
  • +
  • server是一个有一组扩展功能的代理,这些功能包括参与raft选举,维护集群状态,响应rpc查询,与其它数据中心交互wan gossip和转发查询给leader或远程数据中心。
  • +
  • 每个数据中心,clientserver是混合的,一般建有3-5台server
  • +
+

安装Consul

+

安装地址:https://www.consul.io/,这里建议安装终端版本

+
1
2
consul agent -dev -clinent=0.0.0.0
#-dev表示开发模式运行, -server表示服务模式运行
+
1
2
#默认端口
localhost:8500
+

登陆这个默认的端口就能看见可视化的界面

+

Consul工作原理

+

producer:服务提供者

+

consumer:服务消费者

+

image-20240202230444248

+

服务发现与注册

+
    +
  • producer启动时,会将自己的ip/host等信息通过发送请求告知consul
  • +
  • consul接收到producer的注册信息后,每隔10秒(默认)会向producer发送一个健康检查的请求,检查producer是否处于可用状态
  • +
  • post +服务注册 /health健康定期检查
  • +
+

服务调用

+
    +
  • consumer请求product时,会先从consul中拿存储的producer服务的ip和port的临时表(temp table),从表中任选一个producer的ip和port
  • +
  • 根据这个ipport,发送访问请求
  • +
  • 此表只包含通过健康检查的producer信息,并且每隔10秒更新
  • +
  • Temp table 拉取服务列表 +从临时表中拿producer的ip和端口发送请求
  • +
+

Consul+Go实现

+

下面给出一个例子对于在GO项目中是如何结合Consul进行使用的

+

consul中间件目录

+

首先给出consul中间件的结构体图,这里主要对consul进行了sdk的封装,包含了以下的内容:

+
    +
  1. consulclient:服务注册与发现的板块
  2. +
  3. consulconfig:主要是中心化配置的内容
  4. +
  5. consulsdk:主要是对以上的两个板块进行了封装
  6. +
  7. main_test:是给出了一个例子对上述的内容进行测试
  8. +
+

Consulclient

+

这部分着重来写如何实现对服务注册和服务发现的功能

+
    +
  • 定义 ConsulClient 结构体
  • +
+
1
2
3
4
5
goCopy code
type ConsulClient struct {
client *consulapi.Client
serverPort int
}
+

ConsulClient 结构体包含了一个 Consul +客户端指针和一个服务端口。

+
    +
  • 创建新的 Consul 客户端
  • +
+
1
2
3
4
5
6
7
8
9
10
func NewConsulClient(consulAddress string, serverPort int) (*ConsulClient, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulClient{client: client, serverPort: serverPort}, nil
}

+

NewConsulClient 函数用于创建一个新的 Consul +客户端实例。它接收 Consul 服务器的地址和服务端口作为参数,并返回一个 +ConsulClient 实例以及可能的错误。

+
    +
  • 注册服务
  • +
+
1
2
3
4
5
6
7
8
9
10
func (c *ConsulClient) RegisterService(serviceID, serviceName, serviceHost string, servicePort int) error {
service := &consulapi.AgentServiceRegistration{
ID: serviceID,
Name: serviceName,
Address: serviceHost,
Port: servicePort,
}
return c.client.Agent().ServiceRegister(service)
}

+

RegisterService 方法用于向 Consul +注册服务。它接收服务的ID、名称、主机和端口作为参数,并向 Consul +注册该服务。

+
    +
  • 服务发现
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
services, _, err := c.client.Health().Service(serviceName, "", true, nil)
if err != nil {
return "", err
}
if len(services) == 0 {
return "", fmt.Errorf("service not found")
}

// 随机选择一个服务实例
if len(services) > 0 {
index := rand.Intn(len(services))
service := services[index].Service
address := fmt.Sprintf("%v:%v", service.Address, service.Port)
return address, nil
}

return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}

+

DiscoverService 方法用于从 Consul +中发现服务实例。它接收服务名称作为参数,并返回一个服务实例的地址。在内部,它通过健康检查来获取可用的服务实例,并随机选择一个健康的实例返回其地址。

+

Consulconfig

+

这段代码实现了一个基本的 Consul 配置中心客户端,用于从 Consul +中获取、设置和删除键值对的配置信息。

+
    +
  • 定义 ConsulConfigCenter 结构体
  • +
+
1
2
3
type ConsulConfigCenter struct {
client *consulapi.Client
}
+
    +
  • 创建新的 Consul 配置中心客户端
  • +
+
1
2
3
4
5
6
7
8
9
func NewConsulConfigCenter(consulAddress string) (*ConsulConfigCenter, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulConfigCenter{client: client}, nil
}
+

NewConsulConfigCenter 函数用于创建一个新的 Consul +配置中心客户端实例。它接收 Consul 服务器的地址作为参数,并返回一个 +ConsulConfigCenter 实例以及可能的错误

+
    +
  • 获取特定键对应的值
  • +
+
1
2
3
4
5
6
7
8
9
10
11
func (cc *ConsulConfigCenter) GetValue(key string) (string, error) {
kv := cc.client.KV()
pair, _, err := kv.Get(key, nil)
if err != nil {
return "", err
}
if pair == nil {
return "", fmt.Errorf("key '%s' not found", key)
}
return string(pair.Value), nil
}
+

GetValue 方法用于从 Consul +中获取特定键对应的值。它接收键名作为参数,并返回键对应的值以及可能的错误。

+
    +
  • 设置键值对
  • +
+
1
2
3
4
5
6
func (cc *ConsulConfigCenter) SetValue(key, value string) error {
kv := cc.client.KV()
p := &consulapi.KVPair{Key: key, Value: []byte(value)}
_, err := kv.Put(p, nil)
return err
}
+

SetValue 方法用于在 Consul +的键值存储中设置一个键值对。它接收键名和值作为参数,并将其设置到 Consul +中。

+
    +
  • 删除键值对
  • +
+
1
2
3
4
5
func (cc *ConsulConfigCenter) DeleteValue(key string) error {
kv := cc.client.KV()
_, err := kv.Delete(key, nil)
return err
}
+

DeleteValue 方法用于从 Consul +的键值存储中删除指定的键值对。它接收键名作为参数,并将对应的键值对从 +Consul 中删除。

+

ConsulSDK

+

实现了一个基于单例模式的 Consul SDK,用于管理 Consul +客户端和配置中心。让我们逐段解释代码的功能

+
    +
  • 定义 ConsulSDK 结构体
  • +
+
1
2
3
4
type ConsulSDK struct {
Client *ConsulClient
ConfigCenter *ConsulConfigCenter
}
+

ConsulSDK 结构体包含了 Consul 客户端和配置中心的实例

+
    +
  • 定义全局变量和 sync.Once 实例
  • +
+
1
2
3
4
var (
instance *ConsulSDK
once sync.Once
)
+

定义了 instance 变量用于保存 ConsulSDK +实例,once 变量用于确保 GetInstance() +函数只被执行一次

+
    +
  • 实现 NewConsulSDK 函数
  • +
+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func NewConsulSDK(consulAddress string, serverPort int) (*ConsulSDK, error) {
// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
return nil, err
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
return nil, err
}

// 返回 ConsulSDK 实例
return &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}, nil
}
+

NewConsulSDK 函数用于创建一个新的 ConsulSDK 实例,它接收 +Consul 服务器地址和端口作为参数,并返回一个 ConsulSDK +实例以及可能的错误

+
    +
  • 实现 GetInstance() 函数
  • +
+
1
2
3
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
func GetInstance() *ConsulSDK {
// 保证只执行一次
once.Do(func() {
consulAddress := "127.0.0.1:8500"
serverPort := 8080

// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
fmt.Println("Failed to create Consul client:", err)
os.Exit(1)
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
fmt.Println("Failed to create Consul config center:", err)
os.Exit(1)
}

// 初始化 ConsulSDK 实例
instance = &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}
})
return instance
}
+

GetInstance() 函数用于获取 ConsulSDK 的单例实例。它通过 +once.Do() 确保只执行一次,创建 Consul +客户端和配置中心,并将其保存到全局变量 instance +中,然后返回该实例

+

Main_test.go

+

这个部分主要对上述的sdk接口给出了一个具体的测试用例

+

注意在终端运行的时候的运行为:go test +go默认对_test会进行测试

+
1
2
3
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
package consul

import (
"fmt"
"os"
"testing"
)

func TestConsul(t *testing.T) {
//创建新的 consul sdk
sdk := GetInstance()

// 注册服务
serviceID := "my_service"
serviceName := "my_service"
serviceHost := "127.0.0.1"
servicePort := 8080

err := sdk.Client.RegisterService(serviceID, serviceName, serviceHost, servicePort)
if err != nil {
fmt.Printf("Error registering service: %v\n", err)
os.Exit(1)
}

// 发现服务
serviceAddress, err := sdk.Client.DiscoverService(serviceName)
if err != nil {
fmt.Printf("Error discovering service: %v\n", err)
os.Exit(1)
}
fmt.Printf("Discovered service address: %s\n", serviceAddress)

//使用 ConsulConfig 获取键对应的值
value, err := sdk.ConfigCenter.GetValue("test")
if err != nil {
fmt.Println("Failed to get value:", err)
return
}
fmt.Println("Value:", value)
}

+ + +
+ +
+
+ + + + + + +
+
+
【后端开发】Consul服务与配置
+
https://lihaibineric.github.io/2024/02/02/consul/
+
+
+ +
+
Author
+
Haibin Li
+
+ + +
+
Posted on
+
February 2, 2024
+
+ + +
+
Updated on
+
February 3, 2024
+
+ + +
+
Licensed under
+
+ + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+ + +
+ +
+ +
+ + +
+
+
+
+ +
+ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/10/index.html b/archives/2023/10/index.html index dba1ca3..aff0a60 100644 --- a/archives/2023/10/index.html +++ b/archives/2023/10/index.html @@ -204,7 +204,7 @@
-

15 posts in total

+

17 posts in total


diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index 198c445..ad81383 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -204,7 +204,7 @@
-

15 posts in total

+

17 posts in total


diff --git a/archives/2023/12/index.html b/archives/2023/12/index.html index 8f9bee7..5e6f342 100644 --- a/archives/2023/12/index.html +++ b/archives/2023/12/index.html @@ -204,7 +204,7 @@
-

15 posts in total

+

17 posts in total


diff --git a/archives/2023/index.html b/archives/2023/index.html index 6b876ba..7c6184d 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -204,7 +204,7 @@
-

15 posts in total

+

17 posts in total


diff --git a/archives/2024/01/index.html b/archives/2024/01/index.html index 9a3acab..f36713d 100644 --- a/archives/2024/01/index.html +++ b/archives/2024/01/index.html @@ -204,7 +204,7 @@
-

15 posts in total

+

17 posts in total


@@ -212,6 +212,12 @@

2024

+ + +
deepl
+
+ +
【后端开发】Go-kit与Gin框架
diff --git a/archives/2024/02/index.html b/archives/2024/02/index.html new file mode 100644 index 0000000..7f1d426 --- /dev/null +++ b/archives/2024/02/index.html @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archives - LIHAIBIN'S BLOG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

17 posts in total

+
+ + + + +

2024

+ + + +
【后端开发】Consul服务与配置
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2024/index.html b/archives/2024/index.html index d7bcdb7..16249f3 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -204,7 +204,7 @@
-

15 posts in total

+

17 posts in total


@@ -212,6 +212,18 @@

2024

+ + +
【后端开发】Consul服务与配置
+
+ + + + +
deepl
+
+ +
【后端开发】Go-kit与Gin框架
diff --git a/archives/index.html b/archives/index.html index 83fd46f..9e5841e 100644 --- a/archives/index.html +++ b/archives/index.html @@ -204,7 +204,7 @@
diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 3a8296b..4958439 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -204,7 +204,7 @@
-

15 posts in total

+

17 posts in total


@@ -212,6 +212,18 @@

2023

+ + +
【基础工具】Linux指令集
+
+ + + + +
【多智能体强化学习】基于自动分组的价值函数分解
+
+ +
【基础工具】Git命令集
diff --git a/categories/index.html b/categories/index.html index faa357c..a079daf 100644 --- a/categories/index.html +++ b/categories/index.html @@ -353,22 +353,22 @@
- + - 3 + 4 -
+
@@ -377,28 +377,37 @@ - - 【多智能体强化学习】Pymarl代码分析 + 【后端开发】Consul服务与配置 - - 【多智能体强化学习】基于自动分组的价值函数分解 + 【后端开发】Go-kit与Gin框架 - - 【多智能体强化学习】Pymarl环境配置 + 【后端开发】Golang协程与Channel + + + + + + + 【后端开发】Golang语法基础 @@ -413,22 +422,22 @@
- + 3 -
+
@@ -437,28 +446,28 @@ - - 【后端开发】Go-kit与Gin框架 + 【多智能体强化学习】Pymarl代码分析 - - 【后端开发】Golang协程与Channel + 【多智能体强化学习】基于自动分组的价值函数分解 - - 【后端开发】Golang语法基础 + 【多智能体强化学习】Pymarl环境配置 diff --git "a/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" "b/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" index 92d364a..c92b205 100644 --- "a/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" +++ "b/categories/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" @@ -204,7 +204,7 @@
-

3 posts in total

+

4 posts in total


@@ -212,6 +212,12 @@

2024

+ + +
【后端开发】Consul服务与配置
+
+ +
【后端开发】Go-kit与Gin框架
diff --git a/index.html b/index.html index d41db35..c12bd39 100644 --- a/index.html +++ b/index.html @@ -210,15 +210,15 @@

- - 【后端开发】Go-kit与Gin框架 + + 【后端开发】Consul服务与配置

- +
- Go-kit框架 Go-kit 是一个功能丰富、易于使用的分布式微服务框架,旨在帮助开发者构建健壮、可维护和可测试的分布式系统。它通过提供一系列可组合的组件,解决了分布式系统中的常见问题,使开发者能够专注于业务逻辑。Go-kit 的核心理念是通过可插拔的组件来实现微服务的功能,这些组件包括服务发现、负载均衡、请求追踪、日志记录和监控等。 Go-kit基本架构 Go-kit 包含了一些基本的组件, + Consul介绍 Consul是hashicorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 内置了服务注册与发现框架、分布一致性协议实现、健康检查、key/value存储、多数据中心方案,不再需要依赖其它工具。 Consul是一个服务网络解决方案,它使团队能够管理服务之间以及跨多云环境和运行时的安全网络连接。Consul提供服务发现、基于身份的授权、L7流量管理和服务到服务加密。
@@ -226,8 +226,8 @@

@@ -259,6 +259,8 @@

#后端开发 + #go开发 +

@@ -271,15 +273,15 @@

- - 【多智能体强化学习】Pymarl代码分析 + + deepl

- +
- Pymarl代码结构 本文章主要介绍多智能体强化学习中的PyMarl框架的代码结构以及训练流程 Main Pymarl的主文件(main.py)主要的作用是构建一个 sacred.Experiment 类的对象 ex ,ex 包含三个重要的内置变量: _run:表示当前实验运行时的 run 对象,_run.info 可用于记录实验中产生的结果,实验初始时是空字典{}; _c + https://blog.csdn.net/weixin_42693876/article/details/120345924 https://zhuanlan.zhihu.com/p/560482252 https://zhuanlan.zhihu.com/p/667048896
@@ -287,44 +289,12 @@

- - - -

@@ -336,15 +306,15 @@

- - 【基础工具】Docker基础功能 + + 【后端开发】Go-kit与Gin框架

- +
- 参考文档 Docker介绍 Docker 是一个应用打包、分发、部署的工具,也可以把它理解为一个轻量的虚拟机,它只虚拟你软件需要的运行环境,多余的一点都不要,而普通虚拟机则是一个完整而庞大的系统,包含各种不管你要不要的软件。 特性 普通虚拟机 Docker 跨平台 通常只能在桌面级系统运行,例如 Windows/Mac,无法在不带图形界面的服务器上运行 支持的系统非常多,各类 + Go-kit框架 Go-kit 是一个功能丰富、易于使用的分布式微服务框架,旨在帮助开发者构建健壮、可维护和可测试的分布式系统。它通过提供一系列可组合的组件,解决了分布式系统中的常见问题,使开发者能够专注于业务逻辑。Go-kit 的核心理念是通过可插拔的组件来实现微服务的功能,这些组件包括服务发现、负载均衡、请求追踪、日志记录和监控等。 Go-kit基本架构 Go-kit 包含了一些基本的组件,
@@ -352,8 +322,8 @@

@@ -368,7 +338,7 @@

- 基础工具 + 后端开发 @@ -397,15 +367,15 @@

- - 【深度学习】推荐系统 + + 【多智能体强化学习】Pymarl代码分析

- +
- 推荐系统学习资料 从零单排推荐系统文章汇总目录 深度推荐系统 王喆 推荐系统算法实战课程 推荐系统简介 搜索弓|擎需要用户主动输入自己的意图,有时候,用户并不知道自己需要什么,有些需求、意愿,是用户自己都意识不到的。因为用户提不出需求,就"无所事事",显然这是对宝贵流量的巨大浪费,不利于建立用户粘性。将自己拥有的、用户可能喜欢的内容主动展示给用户,从而留住用户花费更多的时间与金钱。这就是推荐 + Pymarl代码结构 本文章主要介绍多智能体强化学习中的PyMarl框架的代码结构以及训练流程 Main Pymarl的主文件(main.py)主要的作用是构建一个 sacred.Experiment 类的对象 ex ,ex 包含三个重要的内置变量: _run:表示当前实验运行时的 run 对象,_run.info 可用于记录实验中产生的结果,实验初始时是空字典{}; _c
@@ -413,8 +383,8 @@

@@ -429,7 +399,7 @@

- 深度学习 + 强化学习 @@ -448,6 +418,8 @@

#深度学习 + #强化学习 + @@ -460,15 +432,15 @@

- - 【深度学习】图神经网络 + + 【基础工具】Docker基础功能

- +
- 图神经网络 GNN全称----图神经网络,它是一种直接作用于图结构上的神经网络。我们可以把图中的每一个节点 \(V\) 当作个体对象,而每一条边 \(E\) 当作个体与个体间的某种联系,所有节点组成的关系网就是最后的图 \(U\) GNN的输入一般是每个节点的起始特征向量和表示节点间关系的邻接矩阵,有了这两个输入信息,接下来就是聚合操作了。所谓的聚合,其实就是将周边与节点 V i ViVi 有关 + 参考文档 Docker介绍 Docker 是一个应用打包、分发、部署的工具,也可以把它理解为一个轻量的虚拟机,它只虚拟你软件需要的运行环境,多余的一点都不要,而普通虚拟机则是一个完整而庞大的系统,包含各种不管你要不要的软件。 特性 普通虚拟机 Docker 跨平台 通常只能在桌面级系统运行,例如 Windows/Mac,无法在不带图形界面的服务器上运行 支持的系统非常多,各类
@@ -476,8 +448,8 @@

@@ -492,7 +464,7 @@

- 深度学习 + 基础工具 @@ -507,7 +479,7 @@

@@ -521,15 +493,15 @@

- - 【算法题】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循环 + 推荐系统学习资料 从零单排推荐系统文章汇总目录 深度推荐系统 王喆 推荐系统算法实战课程 推荐系统简介 搜索弓|擎需要用户主动输入自己的意图,有时候,用户并不知道自己需要什么,有些需求、意愿,是用户自己都意识不到的。因为用户提不出需求,就"无所事事",显然这是对宝贵流量的巨大浪费,不利于建立用户粘性。将自己拥有的、用户可能喜欢的内容主动展示给用户,从而留住用户花费更多的时间与金钱。这就是推荐
@@ -537,8 +509,8 @@

@@ -553,7 +525,7 @@

- LeetCode算法 + 深度学习 @@ -568,9 +540,9 @@

@@ -584,15 +556,15 @@

- - 【后端开发】Golang协程与Channel + + 【深度学习】图神经网络

- +
- Golang进阶 这一部分主要介绍golang中的并发,并发是 golang 的优势之一,使用关键字 go 可以很方便的开启一个协程. go 语言中,常常用 go、chan、select 及 sync 库完成并发操作,处理同步、异步、阻塞、非阻塞任务 go 语言的并发编程,以下是需要了解的基础知识点 阻塞: 阻塞是进程(也可以是线程、协程)的状态之一(新建、就绪、运行、阻塞、终止). 指的是当 + 图神经网络 GNN全称----图神经网络,它是一种直接作用于图结构上的神经网络。我们可以把图中的每一个节点 \(V\) 当作个体对象,而每一条边 \(E\) 当作个体与个体间的某种联系,所有节点组成的关系网就是最后的图 \(U\) GNN的输入一般是每个节点的起始特征向量和表示节点间关系的邻接矩阵,有了这两个输入信息,接下来就是聚合操作了。所谓的聚合,其实就是将周边与节点 V i ViVi 有关
@@ -600,8 +572,8 @@

@@ -616,7 +588,7 @@

- 后端开发 + 深度学习 @@ -631,7 +603,7 @@

@@ -645,15 +617,15 @@

- - 【后端开发】Golang语法基础 + + 【算法题】LeetCode算法汇总

- +
- Golang基础 参考学习资料: 8小时转职Golang工程师 Go语言101 Go程序员面试笔试宝典 Golang简介 go语言的优势 部署简单 可以直接编译成机器码可执行 不依赖其他库 直接运行即可部署 静态类型语言 编译的时候就能查出来大多数的问题 语言层面的并发 能够成分利用多核 强大的标准库 runtime系统调度机制 丰富的标准库 简单易学 25个关键字,内 + 语言细节 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循环
@@ -661,8 +633,8 @@

@@ -677,7 +649,7 @@

- 后端开发 + LeetCode算法 @@ -692,7 +664,9 @@

@@ -706,15 +680,15 @@

- - 【基础工具】Linux指令集 + + 【后端开发】Golang协程与Channel

- +
- Linux命令集 查看当前路径的位置 pwd 查看命令历史 终端中输入 1history 指定查找关键字 通过增加grep来增加关键字的筛选 1| grep GOPROXY 当前目录下的文件 当前目录下的文件 ls ls -l;ls-h;都是用来显示文件的详细信息 ls -a 来显示所有的文件(包含隐藏文件) 组合使用 ls -lha 显示所有并包含的隐藏文件且显示详细信息 使用dir显示当前文 + Golang进阶 这一部分主要介绍golang中的并发,并发是 golang 的优势之一,使用关键字 go 可以很方便的开启一个协程. go 语言中,常常用 go、chan、select 及 sync 库完成并发操作,处理同步、异步、阻塞、非阻塞任务 go 语言的并发编程,以下是需要了解的基础知识点 阻塞: 阻塞是进程(也可以是线程、协程)的状态之一(新建、就绪、运行、阻塞、终止). 指的是当
@@ -722,8 +696,8 @@

@@ -738,7 +712,7 @@

- 基础工具 + 后端开发 @@ -753,8 +727,6 @@

@@ -769,15 +741,15 @@

- - 【多智能体强化学习】基于自动分组的价值函数分解 + + 【后端开发】Golang语法基础

- +
- Automatic Grouping for MARL 论文标题: 《Vast: Value function factorization with variable agent sub-teams》 《Automatic Grouping for Efficient Cooperative Multi-Agent Reinforcement Learning》 论文代码:https://git + Golang基础 参考学习资料: 8小时转职Golang工程师 Go语言101 Go程序员面试笔试宝典 Golang简介 go语言的优势 部署简单 可以直接编译成机器码可执行 不依赖其他库 直接运行即可部署 静态类型语言 编译的时候就能查出来大多数的问题 语言层面的并发 能够成分利用多核 强大的标准库 runtime系统调度机制 丰富的标准库 简单易学 25个关键字,内
@@ -785,8 +757,8 @@

@@ -801,7 +773,7 @@

- 强化学习 + 后端开发 @@ -816,11 +788,7 @@

diff --git a/local-search.xml b/local-search.xml index 159421c..50c0226 100644 --- a/local-search.xml +++ b/local-search.xml @@ -3,6 +3,46 @@ + + 【后端开发】Consul服务与配置 + + /2024/02/02/consul/ + +

Consul介绍

Consul是hashicorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。内置了服务注册与发现框架、分布一致性协议实现、健康检查、key/value存储、多数据中心方案,不再需要依赖其它工具。

Consul是一个服务网络解决方案,它使团队能够管理服务之间以及跨多云环境和运行时的安全网络连接。Consul提供服务发现、基于身份的授权、L7流量管理和服务到服务加密。

Consul示意图

Consul角色

服务发现和注册

  • dev:开发模式
  • client:客户端,接受请求转达给服务端集群,将http和dns接口请求转发给局域网内的服务端集群,它只是一个代理的角色
  • server:服务端,保存配置信息,高可用集群,每个数据中心的server数据推荐为3个或5个

Consul内部角色介绍

  • 不管是server还是client,统称为agent
  • consul client是相对无状态的,只负责转发rpcserver,资源开销很少
  • server是一个有一组扩展功能的代理,这些功能包括参与raft选举,维护集群状态,响应rpc查询,与其它数据中心交互wan gossip和转发查询给leader或远程数据中心。
  • 每个数据中心,clientserver是混合的,一般建有3-5台server

安装Consul

安装地址:https://www.consul.io/,这里建议安装终端版本

1
2
consul agent -dev -clinent=0.0.0.0
#-dev表示开发模式运行, -server表示服务模式运行
1
2
#默认端口
localhost:8500

登陆这个默认的端口就能看见可视化的界面

Consul工作原理

producer:服务提供者

consumer:服务消费者

image-20240202230444248

服务发现与注册

  • producer启动时,会将自己的ip/host等信息通过发送请求告知consul
  • consul接收到producer的注册信息后,每隔10秒(默认)会向producer发送一个健康检查的请求,检查producer是否处于可用状态
  • post服务注册 /health健康定期检查

服务调用

  • consumer请求product时,会先从consul中拿存储的producer服务的ip和port的临时表(temp table),从表中任选一个producer的ip和port
  • 根据这个ipport,发送访问请求
  • 此表只包含通过健康检查的producer信息,并且每隔10秒更新
  • Temp table 拉取服务列表从临时表中拿producer的ip和端口发送请求

Consul+Go实现

下面给出一个例子对于在GO项目中是如何结合Consul进行使用的

consul中间件目录

首先给出consul中间件的结构体图,这里主要对consul进行了sdk的封装,包含了以下的内容:

  1. consulclient:服务注册与发现的板块
  2. consulconfig:主要是中心化配置的内容
  3. consulsdk:主要是对以上的两个板块进行了封装
  4. main_test:是给出了一个例子对上述的内容进行测试

Consulclient

这部分着重来写如何实现对服务注册和服务发现的功能

  • 定义 ConsulClient 结构体
1
2
3
4
5
goCopy code
type ConsulClient struct {
client *consulapi.Client
serverPort int
}

ConsulClient 结构体包含了一个 Consul客户端指针和一个服务端口。

  • 创建新的 Consul 客户端
1
2
3
4
5
6
7
8
9
10
func NewConsulClient(consulAddress string, serverPort int) (*ConsulClient, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulClient{client: client, serverPort: serverPort}, nil
}

NewConsulClient 函数用于创建一个新的 Consul客户端实例。它接收 Consul 服务器的地址和服务端口作为参数,并返回一个ConsulClient 实例以及可能的错误。

  • 注册服务
1
2
3
4
5
6
7
8
9
10
func (c *ConsulClient) RegisterService(serviceID, serviceName, serviceHost string, servicePort int) error {
service := &consulapi.AgentServiceRegistration{
ID: serviceID,
Name: serviceName,
Address: serviceHost,
Port: servicePort,
}
return c.client.Agent().ServiceRegister(service)
}

RegisterService 方法用于向 Consul注册服务。它接收服务的ID、名称、主机和端口作为参数,并向 Consul注册该服务。

  • 服务发现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
services, _, err := c.client.Health().Service(serviceName, "", true, nil)
if err != nil {
return "", err
}
if len(services) == 0 {
return "", fmt.Errorf("service not found")
}

// 随机选择一个服务实例
if len(services) > 0 {
index := rand.Intn(len(services))
service := services[index].Service
address := fmt.Sprintf("%v:%v", service.Address, service.Port)
return address, nil
}

return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}

DiscoverService 方法用于从 Consul中发现服务实例。它接收服务名称作为参数,并返回一个服务实例的地址。在内部,它通过健康检查来获取可用的服务实例,并随机选择一个健康的实例返回其地址。

Consulconfig

这段代码实现了一个基本的 Consul 配置中心客户端,用于从 Consul中获取、设置和删除键值对的配置信息。

  • 定义 ConsulConfigCenter 结构体
1
2
3
type ConsulConfigCenter struct {
client *consulapi.Client
}
  • 创建新的 Consul 配置中心客户端
1
2
3
4
5
6
7
8
9
func NewConsulConfigCenter(consulAddress string) (*ConsulConfigCenter, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulConfigCenter{client: client}, nil
}

NewConsulConfigCenter 函数用于创建一个新的 Consul配置中心客户端实例。它接收 Consul 服务器的地址作为参数,并返回一个ConsulConfigCenter 实例以及可能的错误

  • 获取特定键对应的值
1
2
3
4
5
6
7
8
9
10
11
func (cc *ConsulConfigCenter) GetValue(key string) (string, error) {
kv := cc.client.KV()
pair, _, err := kv.Get(key, nil)
if err != nil {
return "", err
}
if pair == nil {
return "", fmt.Errorf("key '%s' not found", key)
}
return string(pair.Value), nil
}

GetValue 方法用于从 Consul中获取特定键对应的值。它接收键名作为参数,并返回键对应的值以及可能的错误。

  • 设置键值对
1
2
3
4
5
6
func (cc *ConsulConfigCenter) SetValue(key, value string) error {
kv := cc.client.KV()
p := &consulapi.KVPair{Key: key, Value: []byte(value)}
_, err := kv.Put(p, nil)
return err
}

SetValue 方法用于在 Consul的键值存储中设置一个键值对。它接收键名和值作为参数,并将其设置到 Consul中。

  • 删除键值对
1
2
3
4
5
func (cc *ConsulConfigCenter) DeleteValue(key string) error {
kv := cc.client.KV()
_, err := kv.Delete(key, nil)
return err
}

DeleteValue 方法用于从 Consul的键值存储中删除指定的键值对。它接收键名作为参数,并将对应的键值对从Consul 中删除。

ConsulSDK

实现了一个基于单例模式的 Consul SDK,用于管理 Consul客户端和配置中心。让我们逐段解释代码的功能

  • 定义 ConsulSDK 结构体
1
2
3
4
type ConsulSDK struct {
Client *ConsulClient
ConfigCenter *ConsulConfigCenter
}

ConsulSDK 结构体包含了 Consul 客户端和配置中心的实例

  • 定义全局变量和 sync.Once 实例
1
2
3
4
var (
instance *ConsulSDK
once sync.Once
)

定义了 instance 变量用于保存 ConsulSDK实例,once 变量用于确保 GetInstance()函数只被执行一次

  • 实现 NewConsulSDK 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func NewConsulSDK(consulAddress string, serverPort int) (*ConsulSDK, error) {
// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
return nil, err
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
return nil, err
}

// 返回 ConsulSDK 实例
return &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}, nil
}

NewConsulSDK 函数用于创建一个新的 ConsulSDK 实例,它接收Consul 服务器地址和端口作为参数,并返回一个 ConsulSDK实例以及可能的错误

  • 实现 GetInstance() 函数
1
2
3
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
func GetInstance() *ConsulSDK {
// 保证只执行一次
once.Do(func() {
consulAddress := "127.0.0.1:8500"
serverPort := 8080

// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
fmt.Println("Failed to create Consul client:", err)
os.Exit(1)
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
fmt.Println("Failed to create Consul config center:", err)
os.Exit(1)
}

// 初始化 ConsulSDK 实例
instance = &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}
})
return instance
}

GetInstance() 函数用于获取 ConsulSDK 的单例实例。它通过once.Do() 确保只执行一次,创建 Consul客户端和配置中心,并将其保存到全局变量 instance中,然后返回该实例

Main_test.go

这个部分主要对上述的sdk接口给出了一个具体的测试用例

注意在终端运行的时候的运行为:go testgo默认对_test会进行测试

1
2
3
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
package consul

import (
"fmt"
"os"
"testing"
)

func TestConsul(t *testing.T) {
//创建新的 consul sdk
sdk := GetInstance()

// 注册服务
serviceID := "my_service"
serviceName := "my_service"
serviceHost := "127.0.0.1"
servicePort := 8080

err := sdk.Client.RegisterService(serviceID, serviceName, serviceHost, servicePort)
if err != nil {
fmt.Printf("Error registering service: %v\n", err)
os.Exit(1)
}

// 发现服务
serviceAddress, err := sdk.Client.DiscoverService(serviceName)
if err != nil {
fmt.Printf("Error discovering service: %v\n", err)
os.Exit(1)
}
fmt.Printf("Discovered service address: %s\n", serviceAddress)

//使用 ConsulConfig 获取键对应的值
value, err := sdk.ConfigCenter.GetValue("test")
if err != nil {
fmt.Println("Failed to get value:", err)
return
}
fmt.Println("Value:", value)
}

]]>
+ + + + + 后端开发 + + + + + + + 后端开发 + + go开发 + + + +
+ + + + + deepl + + /2024/01/30/deepl/ + + https://blog.csdn.net/weixin_42693876/article/details/120345924

https://zhuanlan.zhihu.com/p/560482252

https://zhuanlan.zhihu.com/p/667048896

]]>
+ + + +
+ + + 【后端开发】Go-kit与Gin框架 @@ -139,7 +179,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;
}
};

接雨水

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

示例 1:

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

示例 2:

1
2
输入:height = [4,2,0,3,2,5]
输出:9

思路:

找到最大的左边和最大的右边并相减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
int trap(vector<int>& height) {
if(height.size()<=2) return 0;
vector<int> maxLeft(height.size(), 0);
vector<int> maxRight(height.size(), 0);
int size = maxRight.size();
//记录每个柱子左边柱子的最大高度
maxLeft[0] = height[0];
for(int i=1;i<size;i++){
maxLeft[i] = max(height[i],maxLeft[i-1]);
}
//记录每个柱子右边柱子的最大高度
maxRight[size-1] = height[size-1];
for(int i=size-2;i>=0;i--){
maxRight[i] = max(height[i],maxRight[i+1]);
}
int sum=0;
for(int i=0;i<size;i++){
int count = min(maxLeft[i], maxRight[i])-height[i];
if(count > 0) sum+=count;
}
return sum;
}
};

柱形图中的最大矩形

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

最大矩形问题

1
2
3
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeft(heights.size());
vector<int> minRight(heights.size());
int size = heights.size();

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

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

return res;
}
};

二叉树

深搜回溯

深度优先搜索的三部曲:

  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/

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

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

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

示例:

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

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

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

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

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

电话号码组合问题

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

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

电话号码的按键

示例 :

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> result;
string s;
void backtracking(const string digits,int index, string s){
if(digits.size()==0){
return;
}
if(index==digits.size()){
result.push_back(s);
return;
}
int digit = digits[index]-'0';
string letters = letterMap[digit];
for(int i= 0;i<letters.size();i++){
s.push_back(letters[i]);
backtracking(digits, index+1, s);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
backtracking(digits,0,"");
return result;
}
};

组合总和

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

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

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

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

示例 :

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

思路:

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

因此需要调整startindex的开始的位置是在backtracking(candidates,target,sum,i);注意,这个时候从i开始保证还能用到自己的元素重复使用,还有最重要的sort(candidates.begin(), candidates.end()); // 需要排序

排序之后能够很好的进行剪枝,将一些加了之后元素大于目标的删掉直接跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>&candidates, int target, int sum, int startindex){
if(sum==target){
res.push_back(path);
return;
}
for(int i=startindex;i<candidates.size();i++){
if(sum>target){
return;
}
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0,0);
return res;
}
};

组合总和II

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

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

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

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

示例 :

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

思路:

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

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

首先需要在backtracking中定一个continue,这个地方是为了筛选不是重复的部分,那么如何区分开是否是同一个数组中重复的元素而不是重复利用的元素呢?

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

整数拆分

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

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

示例 :

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

思路:

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

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

背包问题解题框架

0-1背包

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

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

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

递推公式:

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

初始化:

全部初始化为0

遍历顺序:

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

整体的代码结构是

1
2
3
4
5
6
7
8
9
10
11
12
13
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}

完全背包问题

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}

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

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

分割等和子集

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

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

1
2
3
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5][11]

单调栈

每日温度

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

示例 1:

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

思路:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
vector<int> res(temperatures.size(),0);
st.push(0);
for(int i=1;i<temperatures.size();i++){
if(temperatures[i]<=temperatures[st.top()]){
st.push(i);
}else{
while (!st.empty()&& temperatures[i]>temperatures[st.top()]){
res[st.top()]=i-st.top();
st.pop();
}
st.push(i);
}
}
return res;
}
};

图论

深度优先搜索理论

广度优先搜索理论

所有可能的路径

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

图路径遍历

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

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

思路:

深度优先搜索

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void dfs(vector<vector<int>>& graph, int x){
if(x == graph.size()-1){
res.push_back(path);
return;
}
for(int i=0;i<graph[x].size();i++){
path.push_back(graph[x][i]);
dfs(graph,graph[x][i]);
path.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
path.push_back(0);
dfs(graph, 0);
return res;
}
};

岛屿数量

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

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

示例 :

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

深度优先搜索版本:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
int dir[4][2] ={0,1,1,0,-1,0,0,-1};//四个方向
void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y){
for(int i=0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx < 0||nextx>=grid.size()||nexty<0 ||nexty>=grid[0].size()) continue;
if(!visited[nextx][nexty]&&grid[nextx][nexty]=='1'){
visited[nextx][nexty]=true;
dfs(grid, visited , nextx, nexty);
}
}
}
int numIslands(vector<vector<char>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n,vector<bool>(m,false));
int res = 0;
for(int i=0;i<n;i++){
for(int j = 0;j<m;j++){
if(!visited[i][j] && grid[i][j]=='1'){
visited[i][j]=true;
res++;
dfs(grid, visited, i,j);
}
}
}
return res;
}
};
]]>
+

语言细节

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

接雨水

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

示例 1:

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

示例 2:

1
2
输入:height = [4,2,0,3,2,5]
输出:9

思路:

找到最大的左边和最大的右边并相减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
int trap(vector<int>& height) {
if(height.size()<=2) return 0;
vector<int> maxLeft(height.size(), 0);
vector<int> maxRight(height.size(), 0);
int size = maxRight.size();
//记录每个柱子左边柱子的最大高度
maxLeft[0] = height[0];
for(int i=1;i<size;i++){
maxLeft[i] = max(height[i],maxLeft[i-1]);
}
//记录每个柱子右边柱子的最大高度
maxRight[size-1] = height[size-1];
for(int i=size-2;i>=0;i--){
maxRight[i] = max(height[i],maxRight[i+1]);
}
int sum=0;
for(int i=0;i<size;i++){
int count = min(maxLeft[i], maxRight[i])-height[i];
if(count > 0) sum+=count;
}
return sum;
}
};

柱形图中的最大矩形

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

最大矩形问题

1
2
3
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> minLeft(heights.size());
vector<int> minRight(heights.size());
int size = heights.size();

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

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

return res;
}
};

二叉树

深搜回溯

深度优先搜索的三部曲:

  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/

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

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

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

示例:

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

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

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

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

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

电话号码组合问题

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

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

电话号码的按键

示例 :

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> result;
string s;
void backtracking(const string digits,int index, string s){
if(digits.size()==0){
return;
}
if(index==digits.size()){
result.push_back(s);
return;
}
int digit = digits[index]-'0';
string letters = letterMap[digit];
for(int i= 0;i<letters.size();i++){
s.push_back(letters[i]);
backtracking(digits, index+1, s);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
backtracking(digits,0,"");
return result;
}
};

组合总和

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

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

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

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

示例 :

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

思路:

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

因此需要调整startindex的开始的位置是在backtracking(candidates,target,sum,i);注意,这个时候从i开始保证还能用到自己的元素重复使用,还有最重要的sort(candidates.begin(), candidates.end()); // 需要排序

排序之后能够很好的进行剪枝,将一些加了之后元素大于目标的删掉直接跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>&candidates, int target, int sum, int startindex){
if(sum==target){
res.push_back(path);
return;
}
for(int i=startindex;i<candidates.size();i++){
if(sum>target){
return;
}
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i);
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0,0);
return res;
}
};

组合总和II

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

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

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

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

示例 :

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

思路:

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

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

首先需要在backtracking中定一个continue,这个地方是为了筛选不是重复的部分,那么如何区分开是否是同一个数组中重复的元素而不是重复利用的元素呢?

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

整数拆分

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

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

示例 :

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

思路:

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

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

背包问题解题框架

0-1背包

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

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

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

递推公式:

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

初始化:

全部初始化为0

遍历顺序:

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

整体的代码结构是

1
2
3
4
5
6
7
8
9
10
11
12
13
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}

完全背包问题

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}

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

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

分割等和子集

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

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

1
2
3
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5][11]

单调栈

每日温度

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

示例 1:

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

思路:

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
vector<int> res(temperatures.size(),0);
st.push(0);
for(int i=1;i<temperatures.size();i++){
if(temperatures[i]<=temperatures[st.top()]){
st.push(i);
}else{
while (!st.empty()&& temperatures[i]>temperatures[st.top()]){
res[st.top()]=i-st.top();
st.pop();
}
st.push(i);
}
}
return res;
}
};

图论

深度优先搜索理论

广度优先搜索理论

所有可能的路径

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

图路径遍历

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

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

思路:

深度优先搜索

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void dfs(vector<vector<int>>& graph, int x){
if(x == graph.size()-1){
res.push_back(path);
return;
}
for(int i=0;i<graph[x].size();i++){
path.push_back(graph[x][i]);
dfs(graph,graph[x][i]);
path.pop_back();
}
}
vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
path.push_back(0);
dfs(graph, 0);
return res;
}
};

岛屿数量

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

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

示例 :

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

深度优先搜索版本:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
int dir[4][2] ={0,1,1,0,-1,0,0,-1};//四个方向
void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y){
for(int i=0;i<4;i++){
int nextx = x+dir[i][0];
int nexty = y+dir[i][1];
if(nextx < 0||nextx>=grid.size()||nexty<0 ||nexty>=grid[0].size()) continue;
if(!visited[nextx][nexty]&&grid[nextx][nexty]=='1'){
visited[nextx][nexty]=true;
dfs(grid, visited , nextx, nexty);
}
}
}
int numIslands(vector<vector<char>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n,vector<bool>(m,false));
int res = 0;
for(int i=0;i<n;i++){
for(int j = 0;j<m;j++){
if(!visited[i][j] && grid[i][j]=='1'){
visited[i][j]=true;
res++;
dfs(grid, visited, i,j);
}
}
}
return res;
}
};
]]>
@@ -191,7 +231,7 @@ /2023/11/27/go-%E5%9F%BA%E7%A1%80/ -

Golang基础

参考学习资料:

  • 8小时转职Golang工程师
  • Go语言101
  • Go程序员面试笔试宝典

Golang简介

go语言的优势

  1. 部署简单
    • 可以直接编译成机器码可执行
    • 不依赖其他库
    • 直接运行即可部署
  2. 静态类型语言
    • 编译的时候就能查出来大多数的问题
  3. 语言层面的并发
    • 能够成分利用多核
  4. 强大的标准库
    • runtime系统调度机制
    • 丰富的标准库
  5. 简单易学
    • 25个关键字,内嵌C语法支持
    • 面向对象的特征,能够跨平台
    • go语言没有异常,全部都用ERROR来表示

go应用方向

  1. 云计算基础设施建设
    • Docker, kubernetes
    • Consul, cloudflare CDN
  2. 基础后端软件:tide, influxdb, cockroachdb
  3. 微服务:go-kit, micro
  4. 互联网基础设施: 以太坊,hyperledger

Go的环境安装

下载官网

go的官网下载网站,选择合适的系统版本进行安装https://go.dev/dl/

安装步骤

  1. 下载安装包并按照安装包的指引下载相关的内容

  2. 对于Mac系统会直接配置好环境变量,根据官网的安装手册进行安装 https://go.dev/doc/install

  3. 测试GO的版本

    1
    go version

    go version

  4. 测试GO的环境变量

    1
    go env

    go env

GO环境变量

GOROOT路径

GOROOT 表示的是安装包所在的位置,一般不需要修改

GOPATH路径

GOPATH表示的是运行文件所在的位置,表示的是workspace的文件位置,GOPATH是我们的工作空间,保存go项目代码和第三方依赖包GOPATH可以设置多个,其中,第一个将会是默认的包目录,使用go get 下载的包都会在第一个path中的src目录下,使用 goinstall时,在哪个GOPATH中找到了这个包,就会在哪个GOPATH下的bin目录生成可执行文件

修改GOPATH的路径

1
export GOPATH="/Users/lihaibin/workspace/golang"

将文件查找的路径设置为GOROOT和GOPATH的并集合

1
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

将两个部分并在一起之后,就能从两个地方开始寻找定义的包

首先会从GOROOT进行搜索,接着从GOPATH进行搜索。

GOPATH是开发时的工作目录。用于:

  1. 保存编译后的二进制文件。
  2. go getgo install命令会下载go代码到GOPATH。
  3. import包时的搜索路径

使用GOPATH时,GO会在以下目录中搜索包:

  1. GOROOT/src:该目录保存了Go标准库代码。
  2. GOPATH/src:该目录保存了应用自身的代码和第三方依赖的代码。

GOPATH的弊端

在 GOPATH 的 $GOPATH/src 下进行 .go文件或源代码的存储,我们可以称其为 GOPATH的模式,这个模式拥有一些弊端.

  • 无版本控制概念.在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。

  • 无法同步一致第三方版本号. 在运行 Go应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。

  • 无法指定当前项目引用的第三方版本号. 你没办法处理v1、v2、v3 等等不同版本的引用问题,因为 GOPATH模式下的导入路径都是一样的,都是github.com/foo/bar

GOPROXY设置

这个环境变量主要是用于设置 Go 模块代理(Go moduleproxy),其作用是用于使 Go在后续拉取模块版本时直接通过镜像站点来快速拉取

GOPROXY的默认值是:https://proxy.golang.org,directproxy.golang.org国内访问不了,需要设置国内的代理

  • 阿里云 https://mirrors.aliyun.com/goproxy/
  • 七牛云 https://goproxy.cn,direct

并通过以下的命令进行设置

1
go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY 的值是一个以英文逗号 “,” 分割的 Go模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为“off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。

1
go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct

GO111MODULE

GO111MODULE 有三个值:off, on和auto(默认值)。

  • GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
  • GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
  • GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:当前目录在GOPATH/src之外且该目录包含go.mod文件当前文件在包含go.mod文件的目录下面。

执行以下命令开启go mod管理

1
go env -w GO111MODULE=on

Go基本语法

如何编译并运行一个Go文件

对于已经写好的go文件,这里以hello.go作为例子,直接使用以下语句进行编译并运行

1
go run hello.go

或者将编译和运行两个过程分开,先编译后运行:

1
2
go build hello.go
./ hello

写一个hello.go

首先给出基本框架

1
2
3
4
5
6
7
8
9
10
11
package main

import(
"fmt",
"time"
)

func main(){
fmt.Println("hello world!")
time.Sleep(1*time.Second)
}

程序的第一行声明了名为main的package。一个package会包含一个或多个.go源代码文件。每一个源文件都是以package开头。比如我们的例子里是packagemain。这行声明语句表示该文件是属于哪一个package。

  • 第一行代码packagemain定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:packagemain。package main表示一个可独立执行的程序,每个 Go应用程序都包含一个名为 main 的包。
  • 下一行import "fmt"告诉 Go 编译器这个程序需要使用fmt 包(的函数,或其他元素),fmt 包实现了格式化IO(输入/输出)的函数
  • .和没有.导入包的区别,如果一开始引入的时候有.那么就不需要指定哪个包的来调用函数,否则需要再调用函数的时候指定对应的包package
  • 下一行func main()是程序开始执行的函数。main函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有init() 函数则会先执行该函数)。

一个程序的main入口函数必须不带任何输入参数和返回结果。而且go语言的语法,定义函数的时候,‘{’必须和函数名在同一行,不能另起一行

变量的声明

声明变量的一般形式是使用 var 关键字

第一种声明:

指定变量类型,声明后若不赋值,使用默认值0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var v_name v_type
v_name = value
package main


import "fmt"


func main() {
var a int
fmt.Printf(" = %d\n", a)
}


$go run test.go
a = 0

第二种声明:

根据值自行判定变量类型。

1
var v_name = value

第三种声明:

省略var, 注意:=左侧的变量不应该是已经声明过的,就是:=只能用于没有被声明的变量赋值上,否则会编译错误

1
2
3
4
5
6
7
v_name := value


// 例如
var a int = 10
var b = 10
c : = 10

几种声明类型的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
func main() {
//第一种 使用默认值
var a int
fmt.Printf("a = %d\n", a)


//第二种
var b int = 10
fmt.Printf("b = %d\n", b)


//第三种 省略后面的数据类型,自动匹配类型
var c = 20
fmt.Printf("c = %d\n", c)


//第四种 省略var关键字
d := 3.14
fmt.Printf("d = %f\n", d)
}

全局变量声明

和一般的定义变量的方式一样

1
2
3
var x, y int
var c, d int = 1, 2
var e, f = 123, "lihaibin"

特殊的定义全局变量的方式,而且:=的定义方式不能够用于定义全局变量

1
2
3
4
5
6
7
8
9
10

var ( //这种分解的写法,一般用于声明全局变量
a int
b bool
)

//这种不带声明格式的只能在函数体内声明
//g, h := 123, "需要在func函数体内实现"

func main() {}

多变量声明

:=不能用于已经被初始化之后的变量的赋值,如果对于_的情况是不具备可读性,相当于忽略

1
2
3
4
5
6
7
8
9
10
11
func main() {
g, h := 123, "需要在func函数体内实现"
fmt.Println(x, y, a, b, c, d, e, f, g, h)

//不能对g变量再次做初始化声明
//g := 400

_, value := 7, 5 //实际上7的赋值被废弃,变量 _ 不具备读特性
//fmt.Println(_) //_变量的是读不出来的
fmt.Println(value) //5
}

常量

常量的声明方式

常量是一个简单值的标识符,在程序运行时,不会被修改的量。常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

常量的定义格式:

1
2
const identifier [type] = value
const b string = "abc"

隐式定义类型方法:

1
const b = "abc"

多重赋值

1
const a, b, c = 1, false, "str" //多重赋值

枚举类型

1
2
3
4
5
const (
Unknown = 0
Female = 1
Male = 2
)

常量可以用len(), cap(),unsafe.Sizeof()常量计算表达式的值。常量表达式中,函数必须是内置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main


import "unsafe"
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)


func main(){
println(a, b, c)
}

输出结果为:abc, 3, 16

unsafe.Sizeof(a) = 16

字符串类型在 go 里是个结构,包含指向底层数组的指针和长度,这两部分每部分都是 8个字节,所以字符串类型大小为 16 个字节。

常量中的iota标识符

在 golang中,一个方便的习惯就是使用iota标示符,简化了常量用于增长数字的定义。

下面的代码中,当第一行赋值了iota之后,那么相当于初始化位置是0,后面的依次增加是1,2

1
2
3
4
5
const (
CategoryBooks = iota // 0
CategoryHealth // 1
CategoryClothing // 2
)

如果对iota进行运算,其实相当于是选择当前的行作为iota的取值进行运算,如果中间不对运算加以改变,那么会一直持续按照当前的运算规则执行下去

1
2
3
4
5
6
7
8
9
const (
//关键字 iota
/* BEIJING=0
SHANGHAI=1
SHENGZHENG=2 */
BEIJING = 10 * iota // 默认为0 输出0
SHANGHAI //输出10
HANGZHOU //输出20
)

同样的在同一个const中去定义不同的iota的计算方式也可以,iota的取值就是选择当前的行,从哪个地方开始改变,那么就改成不同的计算方式

1
2
3
4
5
6
7
8
9
const (
//这个iota的值表示的是和行数有关的数值,因此计算的时候用行来表示
a, b = iota + 1, iota + 2 //每次新开一个关于iota的计算 那么后续全部会跟着这个计算方式下去
c, d
e, f
g, h = iota * 2, iota * 3
//iota只能在const之中使用
i, k
)

以下是输出的内容:

1
a= 1 b= 2 c= 2 d= 3 e= 3 f= 4 g= 6 h= 9 i= 8 k= 12

函数

基本函数的定义

多个返回值初始化设置了函数的形参之后,初始值是0

go每次设置一个变量值之后都有初始值,如果是数据就是0,如果是字符串那么就是空,防止出现一些野指针的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func swap(x, y string) (string, string) {
return y, x
}


func main() {
a, b := swap("Mahesh", "Kumar")
fmt.Println(a, b)
}

输出的结果是

1
Kumar Mahesh

import和init

所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,执行main包中的init函数,最后执行main函数。下图详细地解释了整个执行过程:

分别建不同的文件夹对应的就是package的名字,相应的在.go文件内部声明package的名字

main 函数只能在package main中

注意:在包中设置接口的时候,函数名称必须第一个字母是大写,如果是小写的话将无法识别

  • 如果函数名第一个是大写就是对外开放的函数,认为是public
  • 如果函数名第一个是小写的话就认为是私有的函数,认为是private

接口函数大小写的区别

init函数的调用过程,首先会对包中的init进行初始化再进行调用接口

init()调用顺序和过程

如果你导入了包比如lib1,但是没有使用这个包里面的接口函数,仍然会报错

导入包但是没有使用接口函数

以下是一个import包的例子,首先定义两个不同包以及对应的接口函数和初始化函数的实现

1
2
3
4
5
6
7
package InitLib1

import "fmt"

func init() {
fmt.Println("lib1")
}
1
2
3
4
5
6
7
package InitLib2

import "fmt"

func init() {
fmt.Println("lib2")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"GolangTraining/InitLib1"
"GolangTraining/InitLib2"
)

func init() {
fmt.Println("libmain init")
}

func main() {
fmt.Println("libmian main")
}

"GolangTraining/InitLib1""GolangTraining/InitLib2"是两个包的地址,go会默认从GOROOT和GOPATH两个默认的位置进行寻找,首先要保证地址的正确性

代码的输出:

1
2
3
4
lib1
lib2
libmain init
libmian main

匿名导包方式

如果我不想调用lib1的函数接口,但是想使用lib1的init()函数怎么办呢,如果这个时候直接导入了包但是不调用接口,就会出现上述的错误

导入包但是没有使用接口函数

在导入的包前面加上下划线来认为这个包是匿名的,这样就能知进行init操作

1
2
3
4
import(
"fmt"
_"lib2"
)

那么这个时候就只会调用init()函数同时不会出错

除了能够匿名导包之外,还能给新导入的包起个别的名字,比如叫mylib作为新的别名

给导入的包换个名字

或者直接使用·来进行调用

换别名进行导包

最好别使用这种,如果两个包的函数名称一样那么可能会导致出现歧义的情况

函数值传递

函数如果使用参数,该变量可称为函数的形参。

形参就像定义在函数体内的局部变量。调用函数,可以通过两种方式来传递参数:值传递和指针传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go语言使用的是值传递,即在调用过程中不会影响到实际参数。

1
2
3
4
5
6
7
8
9
10
11
12
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int


temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/


return temp;
}

接下来,让我们使用值传递来调用 swap() 函数:

1
2
3
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
package main


import "fmt"


func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200


fmt.Printf("交换前 a 的值为 : %d\n", a )
fmt.Printf("交换前 b 的值为 : %d\n", b )


/* 通过调用函数来交换值 */
swap(a, b)


fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}


/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int


temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/


return temp;
}

运行的结果为:

1
2
3
4
交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200

GO指针

和C++以及C中的是一样的,对go中的指针定义的时候 *int传递变量的地址&

指针和引用的传递例子

在对一个指针赋值的时候,传递的是某一个变量的地址,就是传递这个变量的引用,引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

defer

defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数,主要作用:

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。

1
2
3
4
5
6
7
8
9
func Demo(){
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
defer fmt.Println("4")
}
func main() {
Demo()
}

输出的内容

1
2
3
4
4
3
2
1

数组与切片

Go 语言切片 slice 是对数组的抽象

静态数组

通过这种方式进行初始化数组以及进行切片操作,通过range关键字进行遍历数组,并给出index和value进行给出不同的下标和数值

初始化数组并进行切片操作

固定数组传递的是一个值拷贝

动态数组 slice

切片不需要说明长度

1
2
3
4
5
6
7
8
9
10
11
12
/*
声明切片之后的长度是3,同时初始化的值是1,2,3
*/
slice1 := []int{1, 2, 3}
s := arr[:]
//但是这个时候就只会重新赋予空间并复制0
slice1 = make([]int, 4)

/*
声明slice是切片,但是没有分配空间
*/
var slice2 []int

也可以指定容量,其中capacity为可选参数。

1
make([]T, length, capacity)

将arr中从下标startIndex到endIndex-1下的元素创建为一个新的切片

1
s := arr[startIndex:endIndex]

缺省endIndex时将表示一直到arr的最后一个元素,缺省startIndex时将表示从arr的第一个元素开始

1
2
s := arr[startIndex:]
s := arr[:endIndex]

通过切片s初始化切片s1

1
s1 := s[startIndex:endIndex]

通过内置函数 make()初始化切片s,[]int标识为其元素类型为int的切片

同时动态数组传递的过程中的参数形式是一致的,能够适配所有的slice参数类型,但是对于

动态数组传递引用同时不会因为长度不一样而改变形参

动态数组的初始化和打印

这里面的下划线表示的是不需要考虑的index的数值,可以忽略,这里是关于切片slice的声明和打印

1
2
3
4
5
//声明切片之后的长度是3,同时初始化的值是1,2,3
slice1 :=[]int{1,2,3}
//%v表示的是打印出全部的表示信息
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
}

打印的结果是

输出的结果

声明slice但是不一定声明了空间,因此需要注意的是声明的同时并给出空间大小,同时没办法中途增加空间

1
2
3
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
func  main(){
/*
声明切片之后的长度是3,同时初始化的值是1,2,3
*/
slice1 :=[]int{1,2,3}
//%v表示的是打印出全部的表示信息
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
//直接对没有赋予空间的位置修改会出现位置越界
//slice1[3]=999
slice1 = make([]int,4)
//但是这个时候就只会重新赋予空间并复制0
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
/*
声明slice是切片,但是没有分配空间
*/
var slice2 []int
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
//slice2[0]=2 //直接赋予数值会出现错误,越界
slice2 = make([]int,3)
//开辟空间,但是默认值都是0
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
slice2[0]=1000
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
//此时就修改成功
/*
声明slice类型,同时分配空间
*/
var slice3 =make([]int,5)
fmt.Printf("len = %d, slice =%v\n", len(slice3),slice3)


判断一个切片是不是空的

1
2
3
4
5
6
7
8
9
/* 
判断一个slice切片是不是为0
*/
if slice1 == nil{
fmt.Println("slice1 is null")
}else{
fmt.Println("slice1 is not null")
}
}

注意if-else的格式有要求,{必须是出现在else和if紧接着的位置,不能换行写

判断当前的动态数组是不是空的

append()和copy()

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来

追加元素的操作显示

注意,如果append超过了当前的空间,那么slice就会继续增加空间,增加的大小是cap的大小增加

拷贝copy()操作

1
2
3
4
5
6
7
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)


/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)

关于切片的截取操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
切片的截取
*/
var slice6 = make([]int,3)
slice6[0]=1
slice6[2]=33
fmt.Printf("len = %d, cap = %d, slice =%v\n", len(slice6), cap(slice6), slice6)
//截取的过程中是左闭右开
s1:=slice6[0:2]
// s1:=slice6[:]//表示截取全部
fmt.Println(s1)
//注意此时的s1的地址和slice6的位置一样了

//copy
var s2 = make([]int,3)
copy(s2,slice6)
fmt.Println(s2)// 只会从头开始截取s2长度的,如果长了那么就是0,
//否则就是选择slice6中相同长度的元素,从左到右边

map

map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main
import (
"fmt"
)

func main() {
//第一种声明
var test1 map[string]string
//在使用map前,需要先make,make的作用就是给map分配数据空间
test1 = make(map[string]string, 10)
test1["one"] = "php"
test1["two"] = "golang"
test1["three"] = "java"
fmt.Println(test1) //map[two:golang three:java one:php]


//第二种声明
test2 := make(map[string]string)
test2["one"] = "php"
test2["two"] = "golang"
test2["three"] = "java"
fmt.Println(test2) //map[one:php two:golang three:java]

//第三种声明
test3 := map[string]string{
"one" : "php",
"two" : "golang",
"three" : "java",
}
fmt.Println(test3) //map[one:php two:golang three:java]



language := make(map[string]map[string]string)
language["php"] = make(map[string]string, 2)
language["php"]["id"] = "1"
language["php"]["desc"] = "php是世界上最美的语言"
language["golang"] = make(map[string]string, 2)
language["golang"]["id"] = "2"
language["golang"]["desc"] = "golang抗并发非常good"

fmt.Println(language) //map[php:map[id:1 desc:php是世界上最美的语言] golang:map[id:2 desc:golang抗并发非常good]]


//增删改查
// val, key := language["php"] //查找是否有php这个子元素
// if key {
// fmt.Printf("%v", val)
// } else {
// fmt.Printf("no");
// }

//language["php"]["id"] = "3" //修改了php子元素的id值
//language["php"]["nickname"] = "啪啪啪" //增加php元素里的nickname值
//delete(language, "php") //删除了php子元素
fmt.Println(language)
}

面向对象结构体

定义一个结构体

1
2
3
4
//定义一个结构体
type T struct {
name string
}

分别定义不同的拷贝和引用的函数

1
2
3
4
5
6
7
func (t T) method1() {
t.name = "new name1"
}

func (t *T) method2() {
t.name = "new name2"
}

结果是使用值拷贝的输出的name没有改变,只有使用引用的才发生了改变

关于结构体定义的细节,内部的成员变量和结构体本身的大小写就是蕴含了是不是私有和公有的关系,大写标识公有,小写表示私有

1
2
3
4
5
6
// 如果这个类如果是大写,那么其他的Package也能够访问
type Human struct {
Name string //其中变量名称表示大写说明是能够对外界可见的public
Age int
//如果是小写那么就是private的属性
}

对于结构体内部的成员函数,必须是传递了引用的地址才能够修改,否则就是默认的值传递

1
2
3
4
5
6
7
8
9
10
11
func (this *Human) GetName() {
fmt.Println(this.Name)
}
func (this *Human) SetName(newname string) {
//只有是引用地址的传递的时候才是能够修改的
this.Name = newname
}
func (this Human) SetName1(newname string) {
//默认是值传递
this.Name = newname
}

类的继承性

如果新定义的类继承了某个类,那么只需要在内部写上所继承的类的名称,同时这里没有C++中的公有保护等其他类型的继承,公有私有的设定保持一致

1
2
3
4
5
type Superman struct {
Human //表示Superman继承了Human,同时这里没有C++中的公有保护等其他类型的继承
//在子类中重新增加变量
Level int
}

对继承类中的方法重写,同样传递的还是引用和指针

1
2
3
4
5
6
// 对于父类方法进行重写
func (this *Superman) GetName() {
fmt.Println(this.Name)
fmt.Println(this.Level)
}

重新定义新的方法

1
2
3
4
5
6
// 子类中的新方法
func (this *Superman) LevelUp() {
fmt.Println("level up")
this.Level = this.Level + 1
fmt.Println(this)
}

关于主函数中的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
human := Human{Name: "zhang", Age: 99}
human.SetName1("li")
fmt.Println(human) //{zhang 99}
human.SetName("li")
fmt.Println(human) //{li 99}
human.GetName()
fmt.Println("-------------")
superman := Superman{Human{"li4", 18}, 99}
//父类方法的重写
superman.GetName()
//子类新方法
superman.LevelUp()
/* level up
&{{li4 18} 100} */
//父类方法原封不动
superman.SetName("wang5")
fmt.Println(superman) //{{wang5 18} 100}

fmt.Println("+++++++++++++")
//第二种继承类对象的声明
var super Superman
super.Name = "zhangmazi"
super.Level = 100
super.Age = 19
fmt.Println(super)
}

Interface与类型断言

在继承和多态上,一系列家族定义的接口,每个子类能够重写方法,实现同一个方法有多个接口表现形式

类的多态性

本质上利用interface来实现类的多态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 本质是一个指针多态
type Animal interface {
//给出接口包含的多态的函数
Sleep()
GetColor() string
GetType() string
}

// 定义一个具体的类
type Cat struct {
/*如果继承一个接口interface 那么就不需要直接写出来继承,
只需要进行实现就可以认为继承了这个接口inerface
*/
color string
}

// 第二个多态的类
type Dog struct {
//同样需要进行继承这个interface
color string
}

那怎么认为这个cat继承了这个animal类呢?只需要对animal中的所有函数重写即可认为是继承了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 必须要完全重写所有的接口才能认为是多态满足 */
func (this *Cat) Sleep() {
fmt.Println("cat sleep...")
}

func (this *Cat) GetColor() string {
fmt.Printf("the cat color is %v\n", this.color)
return this.color
}

func (this *Cat) GetType() string {
fmt.Printf("the type is cat\n")
return "Cat"
}

同理对于dog也是一样

1
2
3
4
5
6
7
8
9
10
11
12
func (this *Dog) Sleep() {
fmt.Println("dog sleep...")
}
func (this *Dog) GetColor() string {
fmt.Printf("the dog color is %v\n", this.color)
return this.color
}

func (this *Dog) GetType() string {
fmt.Printf("the type is dog\n")
return "Dog"
}

主函数中如何实现不同的多态调用呢?注意哦,这个地方传递的是继承类的引用进去来实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func showanimal(animal Animal) {
animal.Sleep()
// fmt.Println("color = ",animal.GetColor())
// fmt.Println("type = ",animal.GetType())
animal.GetType()
animal.GetColor()
}


cat := Cat{"Green"}
dog := Dog{"Yellow"}

//通过传递指针和引用来实现多态性
showanimal(&cat)
showanimal(&dog)

万能类型interface

golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。

1
2
3
func funcName(a interface{}) string {
return string(a)
}

interface{}相当于是一个万能的数据类型,适用于对任何的函数的参数传递中的使用

  • 直接断言使用
1
2
var a interface{}
fmt.Println("Where are you,Jonny?", a.(string))

如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断

1
value, ok := a.(string)

如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。

interface{}的例子

定义一个断言类型的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func myFunc(arg interface{}) {
fmt.Println("myfunc is called...")
fmt.Println(arg)

//interface{}如何确定类型是什么呢?

//类型断言机制 用于判断是什么类型
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not a string")
fmt.Printf("the value is %T\n", arg) //the value is main.Book
} else {
fmt.Println("arg is string type,is = ", value)
}
}

主函数的调用关系如下:

1
2
3
4
5
6
7
8
9
type Book struct {
auth string
}
func main() {
book := Book{"golang"}
myFunc(book) //{golang}
myFunc(100) //100
myFunc("goland")
}

输出的内容是:

1
2
3
4
5
6
7
8
9
10
11
#输出的值
myfunc is called...
{golang}
arg is not a string
the value is main.Book
myfunc is called...
100
arg is not a string
the value is int
myfunc is called...
goland

Reflect反射

在讲反射之前,先来看看Golang关于类型设计的一些原则

  • 变量包括(type, value)两部分
  • type 包括 static typeconcrete type.简单来说static type是你在编码是看见的类型(如int、string),concrete typeruntime系统看见的类型
  • 类型断言能否成功,取决于变量的concrete type,而不是static type.因此,一个reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.

反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是statictype),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concretetype),只有interface类型才有反射一说

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型

1
(value, type)

value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型concrete type,另外一个指针指向实际的值对应value

reflect的基本功能

reflect的反射类型对象:TypeOf和ValueOf

那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢?它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf()reflect.TypeOf()

1
2
3
4
5
6
7
8
9
10
11
12
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}

//ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"reflect"
)

func main() {
var num float64 = 1.2345

fmt.Println("type: ", reflect.TypeOf(num))
fmt.Println("value: ", reflect.ValueOf(num))
}

运行结果:
type: float64
value: 1.2345

说明:

  1. reflect.TypeOf:直接给到了我们想要的type类型,如float64、int、各种pointer、struct等等真实的类型
  2. reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1"Allen.Wu" 25} 这样的结构体struct的值
  3. 反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Typereflect.Value这两种

reflet例子1:

1
2
3
4
func reflectNum(arg interface{}) {
fmt.Println("type:", reflect.TypeOf(arg))
fmt.Println("value:", reflect.ValueOf(arg))
}

主函数的调用

1
2
3
4
func main() {
var num float64 = 1.2345
reflectNum(num)
}

注意,在使用反射之前需要引入reflect的包

1
2
3
4
import (
"fmt"
"reflect"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var num float64 = 1.2345

pointer := reflect.ValueOf(&num)
value := reflect.ValueOf(num)

// 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
// Golang 对类型要求非常严格,类型一定要完全符合
// 如下两个,一个是*float64,一个是float64,如果弄混,则会panic
convertPointer := pointer.Interface().(*float64)
convertValue := value.Interface().(float64)

fmt.Println(convertPointer)
fmt.Println(convertValue)
}

运行结果:
0xc42000e238
1.2345

reflet例子2:

  • 首先定义一个类以及关于这个类的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type User struct {
    Id int
    Name string
    Age int
    }

    func (this User) Call() { //为什么这个地方返回类型不是*
    fmt.Println("user is called ..")
    fmt.Printf("%v\n", this)
    }
  • 再定义一个利用反射选择类中值和方法的函数

1
2
3
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
func DoFiledAndMethod(input interface{}) {
//获取输入的类型type
inputType := reflect.TypeOf(input)
fmt.Println("input type is: ", inputType.Name())
//output: input type is: User

//获取input的value
inputValue := reflect.ValueOf(input)
fmt.Println("input value is: ", inputValue)
//output: input value is: {1 eric 19}

//通过type获取其中的字段
/*
1、获取interface中的reflect的type, 通过type得到numfield,进行遍历
2、得到每个filed,就是数据类型
3、通过filed中有一个interface()方法得到对应的value
*/
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
// value:=inputType.Field{i}.interface()
value := inputValue.Field(i).Interface()
// fmt.Println(field) //{Id int 0 [0] false}、{Name string 8 [1] false}
/*
每个field表示的就是一行的元素内容,其中Name表示了这一行的变量名, Type表示的是这一行的类型
通过value单独存在field里面,根据索引值寻找通过Interface()调用
*/
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)

}

/* 如何遍历选择方法呢
也是根据inputType来进行划分
*/
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}

注意点:

  1. reflect.TypeOf(input)得到类型
  2. reflect.ValueOf(input)得到对应的值
  3. reflect.TypeOf(input).NumField()的方法是获得interface()中的所有的字段
  4. 如果选择字段中的类型: inputType.Field(i) =reflect.TypeOf(input).Field(i)
  5. 如果选择字段中的值:inputType.Field(i) =reflect.TypeOf(input).Field(i).Interface()
  6. 如果想便利interface中的方法:reflect.TypeOf(input).NumMethod(),其中具体的方法是:reflect.TypeOf(input).Method(i)

通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumMethod进行遍历
  2. 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
  3. 最后对结果取其Name和Type得知具体的方法名
  4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是一样的判断处理方式
通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

1
2
3
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
package main

import (
"fmt"
"reflect"
)

func main() {

var num float64 = 1.2345
fmt.Println("old value of pointer:", num)

// 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
pointer := reflect.ValueOf(&num)
newValue := pointer.Elem()

fmt.Println("type of pointer:", newValue.Type())
fmt.Println("settability of pointer:", newValue.CanSet())

// 重新赋值
newValue.SetFloat(77)
fmt.Println("new value of pointer:", num)

////////////////////
// 如果reflect.ValueOf的参数不是指针,会如何?
pointer = reflect.ValueOf(num)
//newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

运行结果:
old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77
  1. 需要传入的参数是*float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针
  2. 如果传入的参数不是指针,而是变量,那么
    • 通过Elem获取原始值对应的对象则直接panic
    • 通过CanSet方法查询是否可以设置返回false
  1. newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
  2. reflect.Value.Elem()表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的,是指只能修改原是对象的值的大小,不能修改地址
  3. newValue.SetFloat(77)重新设置值的操作,传递引用来修改interface中的值

反射的基本原理

结构体标签

本质上还是利用了反射,通过以下形式给结构体中的变量添加标签作用:其他包在调用这个当前包的时候对于某个属性的一个说明,指示某个包在具体使用中的作用。

作用:能够将结构体转化为json格式

1
2
3
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
package main

import (
"fmt"
"reflect"
)
type resume struct{
/*
通过以下形式给结构体中的变量添加标签
作用: 其他包在调用这个当前包的时候对于某个属性的一个说明,指示某个包在具体使用中的作用
*/
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}

func findtag(str interface{}){
t :=reflect.TypeOf(str).Elem()

for i:=0;i<t.NumField();i++{
tagstring:=t.Field(i).Tag.Get("info")
tagdoc:=t.Field(i).Tag.Get("doc")
fmt.Println("info:",tagstring,"doc",tagdoc)
}
}

func main(){
var re resume
findtag(&re) //注意这里传递的是引用
}

go printf的占位符格式

go printf的占位符表示

输出之后在json格式转换中可以看到如下,注意可以看到的是输出的内容是根据给定的tag来进行标题的命名的

转化为json格式的例子

利用反射取出元素查询

利用编码和解码对struct 和json之间的转化

1
2
3
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
package main

import (
"fmt"
"encoding/json"
)

//如何将结构体转化成json
type Movie struct{
Title string `json:"title"` //就是告诉json库对应的变量的标签名字是这个
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"` //slice `json:"actors"`
}
func main(){
movie :=Movie{"喜剧之王",2000,10,[]string{"zhouxingchi","zhangbozhi"}}

//编码的过程就是将 struct-->json

jsonStr,err:=json.Marshal(movie)

//返回两个字段

if err!=nil{
fmt.Println("json marshal error",err)
return
}
fmt.Printf("jsonStr=%s\n",jsonStr )
/* jsonStr={"title":"喜剧之王","year":2000,"rmb":10,"Actors":["zhouxingchi","zhangbozhi"]} */

//解码过程:json-->struct
//jsonStr={"title":"喜剧之王","year":2000,"rmb":10,"actors":["zhouxingchi","zhangbozhi"]}

myMovie:=Movie{}
//注意这个地方传递的参量是引用
err=json.Unmarshal(jsonStr,&myMovie)
if err!=nil{
fmt.Println("json unmashal error",err)
return
}
fmt.Printf("%v\n",myMovie)
//返回结构体
//{喜剧之王 2000 10 [zhouxingchi zhangbozhi]}
}
]]> +

Golang基础

参考学习资料:

  • 8小时转职Golang工程师
  • Go语言101
  • Go程序员面试笔试宝典

Golang简介

go语言的优势

  1. 部署简单
    • 可以直接编译成机器码可执行
    • 不依赖其他库
    • 直接运行即可部署
  2. 静态类型语言
    • 编译的时候就能查出来大多数的问题
  3. 语言层面的并发
    • 能够成分利用多核
  4. 强大的标准库
    • runtime系统调度机制
    • 丰富的标准库
  5. 简单易学
    • 25个关键字,内嵌C语法支持
    • 面向对象的特征,能够跨平台
    • go语言没有异常,全部都用ERROR来表示

go应用方向

  1. 云计算基础设施建设
    • Docker, kubernetes
    • Consul, cloudflare CDN
  2. 基础后端软件:tide, influxdb, cockroachdb
  3. 微服务:go-kit, micro
  4. 互联网基础设施: 以太坊,hyperledger

Go的环境安装

下载官网

go的官网下载网站,选择合适的系统版本进行安装https://go.dev/dl/

安装步骤

  1. 下载安装包并按照安装包的指引下载相关的内容

  2. 对于Mac系统会直接配置好环境变量,根据官网的安装手册进行安装 https://go.dev/doc/install

  3. 测试GO的版本

    1
    go version

    go version

  4. 测试GO的环境变量

    1
    go env

    go env

GO环境变量

GOROOT路径

GOROOT 表示的是安装包所在的位置,一般不需要修改

GOPATH路径

GOPATH表示的是运行文件所在的位置,表示的是workspace的文件位置,GOPATH是我们的工作空间,保存go项目代码和第三方依赖包GOPATH可以设置多个,其中,第一个将会是默认的包目录,使用go get 下载的包都会在第一个path中的src目录下,使用 goinstall时,在哪个GOPATH中找到了这个包,就会在哪个GOPATH下的bin目录生成可执行文件

修改GOPATH的路径

1
export GOPATH="/Users/lihaibin/workspace/golang"

将文件查找的路径设置为GOROOT和GOPATH的并集合

1
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

将两个部分并在一起之后,就能从两个地方开始寻找定义的包

首先会从GOROOT进行搜索,接着从GOPATH进行搜索。

GOPATH是开发时的工作目录。用于:

  1. 保存编译后的二进制文件。
  2. go getgo install命令会下载go代码到GOPATH。
  3. import包时的搜索路径

使用GOPATH时,GO会在以下目录中搜索包:

  1. GOROOT/src:该目录保存了Go标准库代码。
  2. GOPATH/src:该目录保存了应用自身的代码和第三方依赖的代码。

GOPATH的弊端

在 GOPATH 的 $GOPATH/src 下进行 .go文件或源代码的存储,我们可以称其为 GOPATH的模式,这个模式拥有一些弊端.

  • 无版本控制概念.在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。

  • 无法同步一致第三方版本号. 在运行 Go应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。

  • 无法指定当前项目引用的第三方版本号. 你没办法处理v1、v2、v3 等等不同版本的引用问题,因为 GOPATH模式下的导入路径都是一样的,都是github.com/foo/bar

GOPROXY设置

这个环境变量主要是用于设置 Go 模块代理(Go moduleproxy),其作用是用于使 Go在后续拉取模块版本时直接通过镜像站点来快速拉取

GOPROXY的默认值是:https://proxy.golang.org,directproxy.golang.org国内访问不了,需要设置国内的代理

  • 阿里云 https://mirrors.aliyun.com/goproxy/
  • 七牛云 https://goproxy.cn,direct

并通过以下的命令进行设置

1
go env -w GOPROXY=https://goproxy.cn,direct

GOPROXY 的值是一个以英文逗号 “,” 分割的 Go模块代理列表,允许设置多个模块代理,假设你不想使用,也可以将其设置为“off” ,这将会禁止 Go 在后续操作中使用任何 Go 模块代理。

1
go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct

GO111MODULE

GO111MODULE 有三个值:off, on和auto(默认值)。

  • GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
  • GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
  • GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:当前目录在GOPATH/src之外且该目录包含go.mod文件当前文件在包含go.mod文件的目录下面。

执行以下命令开启go mod管理

1
go env -w GO111MODULE=on

Go mod操作

1
go mod init github.com/hub/project

Go基本语法

如何编译并运行一个Go文件

对于已经写好的go文件,这里以hello.go作为例子,直接使用以下语句进行编译并运行

1
go run hello.go

或者将编译和运行两个过程分开,先编译后运行:

1
2
go build hello.go
./ hello

写一个hello.go

首先给出基本框架

1
2
3
4
5
6
7
8
9
10
11
package main

import(
"fmt",
"time"
)

func main(){
fmt.Println("hello world!")
time.Sleep(1*time.Second)
}

程序的第一行声明了名为main的package。一个package会包含一个或多个.go源代码文件。每一个源文件都是以package开头。比如我们的例子里是packagemain。这行声明语句表示该文件是属于哪一个package。

  • 第一行代码packagemain定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:packagemain。package main表示一个可独立执行的程序,每个 Go应用程序都包含一个名为 main 的包。
  • 下一行import "fmt"告诉 Go 编译器这个程序需要使用fmt 包(的函数,或其他元素),fmt 包实现了格式化IO(输入/输出)的函数
  • .和没有.导入包的区别,如果一开始引入的时候有.那么就不需要指定哪个包的来调用函数,否则需要再调用函数的时候指定对应的包package
  • 下一行func main()是程序开始执行的函数。main函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有init() 函数则会先执行该函数)。

一个程序的main入口函数必须不带任何输入参数和返回结果。而且go语言的语法,定义函数的时候,‘{’必须和函数名在同一行,不能另起一行

变量的声明

声明变量的一般形式是使用 var 关键字

第一种声明:

指定变量类型,声明后若不赋值,使用默认值0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var v_name v_type
v_name = value
package main


import "fmt"


func main() {
var a int
fmt.Printf(" = %d\n", a)
}


$go run test.go
a = 0

第二种声明:

根据值自行判定变量类型。

1
var v_name = value

第三种声明:

省略var, 注意:=左侧的变量不应该是已经声明过的,就是:=只能用于没有被声明的变量赋值上,否则会编译错误

1
2
3
4
5
6
7
v_name := value


// 例如
var a int = 10
var b = 10
c : = 10

几种声明类型的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
func main() {
//第一种 使用默认值
var a int
fmt.Printf("a = %d\n", a)


//第二种
var b int = 10
fmt.Printf("b = %d\n", b)


//第三种 省略后面的数据类型,自动匹配类型
var c = 20
fmt.Printf("c = %d\n", c)


//第四种 省略var关键字
d := 3.14
fmt.Printf("d = %f\n", d)
}

全局变量声明

和一般的定义变量的方式一样

1
2
3
var x, y int
var c, d int = 1, 2
var e, f = 123, "lihaibin"

特殊的定义全局变量的方式,而且:=的定义方式不能够用于定义全局变量

1
2
3
4
5
6
7
8
9
10

var ( //这种分解的写法,一般用于声明全局变量
a int
b bool
)

//这种不带声明格式的只能在函数体内声明
//g, h := 123, "需要在func函数体内实现"

func main() {}

多变量声明

:=不能用于已经被初始化之后的变量的赋值,如果对于_的情况是不具备可读性,相当于忽略

1
2
3
4
5
6
7
8
9
10
11
func main() {
g, h := 123, "需要在func函数体内实现"
fmt.Println(x, y, a, b, c, d, e, f, g, h)

//不能对g变量再次做初始化声明
//g := 400

_, value := 7, 5 //实际上7的赋值被废弃,变量 _ 不具备读特性
//fmt.Println(_) //_变量的是读不出来的
fmt.Println(value) //5
}

常量

常量的声明方式

常量是一个简单值的标识符,在程序运行时,不会被修改的量。常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

常量的定义格式:

1
2
const identifier [type] = value
const b string = "abc"

隐式定义类型方法:

1
const b = "abc"

多重赋值

1
const a, b, c = 1, false, "str" //多重赋值

枚举类型

1
2
3
4
5
const (
Unknown = 0
Female = 1
Male = 2
)

常量可以用len(), cap(),unsafe.Sizeof()常量计算表达式的值。常量表达式中,函数必须是内置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main


import "unsafe"
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)


func main(){
println(a, b, c)
}

输出结果为:abc, 3, 16

unsafe.Sizeof(a) = 16

字符串类型在 go 里是个结构,包含指向底层数组的指针和长度,这两部分每部分都是 8个字节,所以字符串类型大小为 16 个字节。

常量中的iota标识符

在 golang中,一个方便的习惯就是使用iota标示符,简化了常量用于增长数字的定义。

下面的代码中,当第一行赋值了iota之后,那么相当于初始化位置是0,后面的依次增加是1,2

1
2
3
4
5
const (
CategoryBooks = iota // 0
CategoryHealth // 1
CategoryClothing // 2
)

如果对iota进行运算,其实相当于是选择当前的行作为iota的取值进行运算,如果中间不对运算加以改变,那么会一直持续按照当前的运算规则执行下去

1
2
3
4
5
6
7
8
9
const (
//关键字 iota
/* BEIJING=0
SHANGHAI=1
SHENGZHENG=2 */
BEIJING = 10 * iota // 默认为0 输出0
SHANGHAI //输出10
HANGZHOU //输出20
)

同样的在同一个const中去定义不同的iota的计算方式也可以,iota的取值就是选择当前的行,从哪个地方开始改变,那么就改成不同的计算方式

1
2
3
4
5
6
7
8
9
const (
//这个iota的值表示的是和行数有关的数值,因此计算的时候用行来表示
a, b = iota + 1, iota + 2 //每次新开一个关于iota的计算 那么后续全部会跟着这个计算方式下去
c, d
e, f
g, h = iota * 2, iota * 3
//iota只能在const之中使用
i, k
)

以下是输出的内容:

1
a= 1 b= 2 c= 2 d= 3 e= 3 f= 4 g= 6 h= 9 i= 8 k= 12

函数

基本函数的定义

多个返回值初始化设置了函数的形参之后,初始值是0

go每次设置一个变量值之后都有初始值,如果是数据就是0,如果是字符串那么就是空,防止出现一些野指针的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func swap(x, y string) (string, string) {
return y, x
}


func main() {
a, b := swap("Mahesh", "Kumar")
fmt.Println(a, b)
}

输出的结果是

1
Kumar Mahesh

import和init

所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,执行main包中的init函数,最后执行main函数。下图详细地解释了整个执行过程:

分别建不同的文件夹对应的就是package的名字,相应的在.go文件内部声明package的名字

main 函数只能在package main中

注意:在包中设置接口的时候,函数名称必须第一个字母是大写,如果是小写的话将无法识别

  • 如果函数名第一个是大写就是对外开放的函数,认为是public
  • 如果函数名第一个是小写的话就认为是私有的函数,认为是private

接口函数大小写的区别

init函数的调用过程,首先会对包中的init进行初始化再进行调用接口

init()调用顺序和过程

如果你导入了包比如lib1,但是没有使用这个包里面的接口函数,仍然会报错

导入包但是没有使用接口函数

以下是一个import包的例子,首先定义两个不同包以及对应的接口函数和初始化函数的实现

1
2
3
4
5
6
7
package InitLib1

import "fmt"

func init() {
fmt.Println("lib1")
}
1
2
3
4
5
6
7
package InitLib2

import "fmt"

func init() {
fmt.Println("lib2")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"GolangTraining/InitLib1"
"GolangTraining/InitLib2"
)

func init() {
fmt.Println("libmain init")
}

func main() {
fmt.Println("libmian main")
}

"GolangTraining/InitLib1""GolangTraining/InitLib2"是两个包的地址,go会默认从GOROOT和GOPATH两个默认的位置进行寻找,首先要保证地址的正确性

代码的输出:

1
2
3
4
lib1
lib2
libmain init
libmian main

匿名导包方式

如果我不想调用lib1的函数接口,但是想使用lib1的init()函数怎么办呢,如果这个时候直接导入了包但是不调用接口,就会出现上述的错误

导入包但是没有使用接口函数

在导入的包前面加上下划线来认为这个包是匿名的,这样就能知进行init操作

1
2
3
4
import(
"fmt"
_"lib2"
)

那么这个时候就只会调用init()函数同时不会出错

除了能够匿名导包之外,还能给新导入的包起个别的名字,比如叫mylib作为新的别名

给导入的包换个名字

或者直接使用·来进行调用

换别名进行导包

最好别使用这种,如果两个包的函数名称一样那么可能会导致出现歧义的情况

函数值传递

函数如果使用参数,该变量可称为函数的形参。

形参就像定义在函数体内的局部变量。调用函数,可以通过两种方式来传递参数:值传递和指针传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go语言使用的是值传递,即在调用过程中不会影响到实际参数。

1
2
3
4
5
6
7
8
9
10
11
12
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int


temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/


return temp;
}

接下来,让我们使用值传递来调用 swap() 函数:

1
2
3
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
package main


import "fmt"


func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200


fmt.Printf("交换前 a 的值为 : %d\n", a )
fmt.Printf("交换前 b 的值为 : %d\n", b )


/* 通过调用函数来交换值 */
swap(a, b)


fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}


/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int


temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/


return temp;
}

运行的结果为:

1
2
3
4
交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200

GO指针

和C++以及C中的是一样的,对go中的指针定义的时候 *int传递变量的地址&

指针和引用的传递例子

在对一个指针赋值的时候,传递的是某一个变量的地址,就是传递这个变量的引用,引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

defer

defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数,主要作用:

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。

1
2
3
4
5
6
7
8
9
func Demo(){
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
defer fmt.Println("4")
}
func main() {
Demo()
}

输出的内容

1
2
3
4
4
3
2
1

数组与切片

Go 语言切片 slice 是对数组的抽象

静态数组

通过这种方式进行初始化数组以及进行切片操作,通过range关键字进行遍历数组,并给出index和value进行给出不同的下标和数值

初始化数组并进行切片操作

固定数组传递的是一个值拷贝

动态数组 slice

切片不需要说明长度

1
2
3
4
5
6
7
8
9
10
11
12
/*
声明切片之后的长度是3,同时初始化的值是1,2,3
*/
slice1 := []int{1, 2, 3}
s := arr[:]
//但是这个时候就只会重新赋予空间并复制0
slice1 = make([]int, 4)

/*
声明slice是切片,但是没有分配空间
*/
var slice2 []int

也可以指定容量,其中capacity为可选参数。

1
make([]T, length, capacity)

将arr中从下标startIndex到endIndex-1下的元素创建为一个新的切片

1
s := arr[startIndex:endIndex]

缺省endIndex时将表示一直到arr的最后一个元素,缺省startIndex时将表示从arr的第一个元素开始

1
2
s := arr[startIndex:]
s := arr[:endIndex]

通过切片s初始化切片s1

1
s1 := s[startIndex:endIndex]

通过内置函数 make()初始化切片s,[]int标识为其元素类型为int的切片

同时动态数组传递的过程中的参数形式是一致的,能够适配所有的slice参数类型,但是对于

动态数组传递引用同时不会因为长度不一样而改变形参

动态数组的初始化和打印

这里面的下划线表示的是不需要考虑的index的数值,可以忽略,这里是关于切片slice的声明和打印

1
2
3
4
5
//声明切片之后的长度是3,同时初始化的值是1,2,3
slice1 :=[]int{1,2,3}
//%v表示的是打印出全部的表示信息
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
}

打印的结果是

输出的结果

声明slice但是不一定声明了空间,因此需要注意的是声明的同时并给出空间大小,同时没办法中途增加空间

1
2
3
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
func  main(){
/*
声明切片之后的长度是3,同时初始化的值是1,2,3
*/
slice1 :=[]int{1,2,3}
//%v表示的是打印出全部的表示信息
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
//直接对没有赋予空间的位置修改会出现位置越界
//slice1[3]=999
slice1 = make([]int,4)
//但是这个时候就只会重新赋予空间并复制0
fmt.Printf("len = %d, slice =%v\n", len(slice1),slice1)
/*
声明slice是切片,但是没有分配空间
*/
var slice2 []int
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
//slice2[0]=2 //直接赋予数值会出现错误,越界
slice2 = make([]int,3)
//开辟空间,但是默认值都是0
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
slice2[0]=1000
fmt.Printf("len = %d, slice =%v\n", len(slice2),slice2)
//此时就修改成功
/*
声明slice类型,同时分配空间
*/
var slice3 =make([]int,5)
fmt.Printf("len = %d, slice =%v\n", len(slice3),slice3)


判断一个切片是不是空的

1
2
3
4
5
6
7
8
9
/* 
判断一个slice切片是不是为0
*/
if slice1 == nil{
fmt.Println("slice1 is null")
}else{
fmt.Println("slice1 is not null")
}
}

注意if-else的格式有要求,{必须是出现在else和if紧接着的位置,不能换行写

判断当前的动态数组是不是空的

append()和copy()

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来

追加元素的操作显示

注意,如果append超过了当前的空间,那么slice就会继续增加空间,增加的大小是cap的大小增加

拷贝copy()操作

1
2
3
4
5
6
7
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)


/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)

关于切片的截取操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
切片的截取
*/
var slice6 = make([]int,3)
slice6[0]=1
slice6[2]=33
fmt.Printf("len = %d, cap = %d, slice =%v\n", len(slice6), cap(slice6), slice6)
//截取的过程中是左闭右开
s1:=slice6[0:2]
// s1:=slice6[:]//表示截取全部
fmt.Println(s1)
//注意此时的s1的地址和slice6的位置一样了

//copy
var s2 = make([]int,3)
copy(s2,slice6)
fmt.Println(s2)// 只会从头开始截取s2长度的,如果长了那么就是0,
//否则就是选择slice6中相同长度的元素,从左到右边

map

map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main
import (
"fmt"
)

func main() {
//第一种声明
var test1 map[string]string
//在使用map前,需要先make,make的作用就是给map分配数据空间
test1 = make(map[string]string, 10)
test1["one"] = "php"
test1["two"] = "golang"
test1["three"] = "java"
fmt.Println(test1) //map[two:golang three:java one:php]


//第二种声明
test2 := make(map[string]string)
test2["one"] = "php"
test2["two"] = "golang"
test2["three"] = "java"
fmt.Println(test2) //map[one:php two:golang three:java]

//第三种声明
test3 := map[string]string{
"one" : "php",
"two" : "golang",
"three" : "java",
}
fmt.Println(test3) //map[one:php two:golang three:java]



language := make(map[string]map[string]string)
language["php"] = make(map[string]string, 2)
language["php"]["id"] = "1"
language["php"]["desc"] = "php是世界上最美的语言"
language["golang"] = make(map[string]string, 2)
language["golang"]["id"] = "2"
language["golang"]["desc"] = "golang抗并发非常good"

fmt.Println(language) //map[php:map[id:1 desc:php是世界上最美的语言] golang:map[id:2 desc:golang抗并发非常good]]


//增删改查
// val, key := language["php"] //查找是否有php这个子元素
// if key {
// fmt.Printf("%v", val)
// } else {
// fmt.Printf("no");
// }

//language["php"]["id"] = "3" //修改了php子元素的id值
//language["php"]["nickname"] = "啪啪啪" //增加php元素里的nickname值
//delete(language, "php") //删除了php子元素
fmt.Println(language)
}

面向对象结构体

定义一个结构体

1
2
3
4
//定义一个结构体
type T struct {
name string
}

分别定义不同的拷贝和引用的函数

1
2
3
4
5
6
7
func (t T) method1() {
t.name = "new name1"
}

func (t *T) method2() {
t.name = "new name2"
}

结果是使用值拷贝的输出的name没有改变,只有使用引用的才发生了改变

关于结构体定义的细节,内部的成员变量和结构体本身的大小写就是蕴含了是不是私有和公有的关系,大写标识公有,小写表示私有

1
2
3
4
5
6
// 如果这个类如果是大写,那么其他的Package也能够访问
type Human struct {
Name string //其中变量名称表示大写说明是能够对外界可见的public
Age int
//如果是小写那么就是private的属性
}

对于结构体内部的成员函数,必须是传递了引用的地址才能够修改,否则就是默认的值传递

1
2
3
4
5
6
7
8
9
10
11
func (this *Human) GetName() {
fmt.Println(this.Name)
}
func (this *Human) SetName(newname string) {
//只有是引用地址的传递的时候才是能够修改的
this.Name = newname
}
func (this Human) SetName1(newname string) {
//默认是值传递
this.Name = newname
}

类的继承性

如果新定义的类继承了某个类,那么只需要在内部写上所继承的类的名称,同时这里没有C++中的公有保护等其他类型的继承,公有私有的设定保持一致

1
2
3
4
5
type Superman struct {
Human //表示Superman继承了Human,同时这里没有C++中的公有保护等其他类型的继承
//在子类中重新增加变量
Level int
}

对继承类中的方法重写,同样传递的还是引用和指针

1
2
3
4
5
6
// 对于父类方法进行重写
func (this *Superman) GetName() {
fmt.Println(this.Name)
fmt.Println(this.Level)
}

重新定义新的方法

1
2
3
4
5
6
// 子类中的新方法
func (this *Superman) LevelUp() {
fmt.Println("level up")
this.Level = this.Level + 1
fmt.Println(this)
}

关于主函数中的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
human := Human{Name: "zhang", Age: 99}
human.SetName1("li")
fmt.Println(human) //{zhang 99}
human.SetName("li")
fmt.Println(human) //{li 99}
human.GetName()
fmt.Println("-------------")
superman := Superman{Human{"li4", 18}, 99}
//父类方法的重写
superman.GetName()
//子类新方法
superman.LevelUp()
/* level up
&{{li4 18} 100} */
//父类方法原封不动
superman.SetName("wang5")
fmt.Println(superman) //{{wang5 18} 100}

fmt.Println("+++++++++++++")
//第二种继承类对象的声明
var super Superman
super.Name = "zhangmazi"
super.Level = 100
super.Age = 19
fmt.Println(super)
}

Interface与类型断言

在继承和多态上,一系列家族定义的接口,每个子类能够重写方法,实现同一个方法有多个接口表现形式

类的多态性

本质上利用interface来实现类的多态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 本质是一个指针多态
type Animal interface {
//给出接口包含的多态的函数
Sleep()
GetColor() string
GetType() string
}

// 定义一个具体的类
type Cat struct {
/*如果继承一个接口interface 那么就不需要直接写出来继承,
只需要进行实现就可以认为继承了这个接口inerface
*/
color string
}

// 第二个多态的类
type Dog struct {
//同样需要进行继承这个interface
color string
}

那怎么认为这个cat继承了这个animal类呢?只需要对animal中的所有函数重写即可认为是继承了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 必须要完全重写所有的接口才能认为是多态满足 */
func (this *Cat) Sleep() {
fmt.Println("cat sleep...")
}

func (this *Cat) GetColor() string {
fmt.Printf("the cat color is %v\n", this.color)
return this.color
}

func (this *Cat) GetType() string {
fmt.Printf("the type is cat\n")
return "Cat"
}

同理对于dog也是一样

1
2
3
4
5
6
7
8
9
10
11
12
func (this *Dog) Sleep() {
fmt.Println("dog sleep...")
}
func (this *Dog) GetColor() string {
fmt.Printf("the dog color is %v\n", this.color)
return this.color
}

func (this *Dog) GetType() string {
fmt.Printf("the type is dog\n")
return "Dog"
}

主函数中如何实现不同的多态调用呢?注意哦,这个地方传递的是继承类的引用进去来实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func showanimal(animal Animal) {
animal.Sleep()
// fmt.Println("color = ",animal.GetColor())
// fmt.Println("type = ",animal.GetType())
animal.GetType()
animal.GetColor()
}


cat := Cat{"Green"}
dog := Dog{"Yellow"}

//通过传递指针和引用来实现多态性
showanimal(&cat)
showanimal(&dog)

万能类型interface

golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。

1
2
3
func funcName(a interface{}) string {
return string(a)
}

interface{}相当于是一个万能的数据类型,适用于对任何的函数的参数传递中的使用

  • 直接断言使用
1
2
var a interface{}
fmt.Println("Where are you,Jonny?", a.(string))

如果断言失败一般会导致panic的发生。所以为了防止panic的发生,我们需要在断言前进行一定的判断

1
value, ok := a.(string)

如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。

interface{}的例子

定义一个断言类型的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func myFunc(arg interface{}) {
fmt.Println("myfunc is called...")
fmt.Println(arg)

//interface{}如何确定类型是什么呢?

//类型断言机制 用于判断是什么类型
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not a string")
fmt.Printf("the value is %T\n", arg) //the value is main.Book
} else {
fmt.Println("arg is string type,is = ", value)
}
}

主函数的调用关系如下:

1
2
3
4
5
6
7
8
9
type Book struct {
auth string
}
func main() {
book := Book{"golang"}
myFunc(book) //{golang}
myFunc(100) //100
myFunc("goland")
}

输出的内容是:

1
2
3
4
5
6
7
8
9
10
11
#输出的值
myfunc is called...
{golang}
arg is not a string
the value is main.Book
myfunc is called...
100
arg is not a string
the value is int
myfunc is called...
goland

Reflect反射

在讲反射之前,先来看看Golang关于类型设计的一些原则

  • 变量包括(type, value)两部分
  • type 包括 static typeconcrete type.简单来说static type是你在编码是看见的类型(如int、string),concrete typeruntime系统看见的类型
  • 类型断言能否成功,取决于变量的concrete type,而不是static type.因此,一个reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.

反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是statictype),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concretetype),只有interface类型才有反射一说

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型

1
(value, type)

value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型concrete type,另外一个指针指向实际的值对应value

reflect的基本功能

reflect的反射类型对象:TypeOf和ValueOf

那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢?它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf()reflect.TypeOf()

1
2
3
4
5
6
7
8
9
10
11
12
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}

//ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"reflect"
)

func main() {
var num float64 = 1.2345

fmt.Println("type: ", reflect.TypeOf(num))
fmt.Println("value: ", reflect.ValueOf(num))
}

运行结果:
type: float64
value: 1.2345

说明:

  1. reflect.TypeOf:直接给到了我们想要的type类型,如float64、int、各种pointer、struct等等真实的类型
  2. reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1"Allen.Wu" 25} 这样的结构体struct的值
  3. 反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Typereflect.Value这两种

reflet例子1:

1
2
3
4
func reflectNum(arg interface{}) {
fmt.Println("type:", reflect.TypeOf(arg))
fmt.Println("value:", reflect.ValueOf(arg))
}

主函数的调用

1
2
3
4
func main() {
var num float64 = 1.2345
reflectNum(num)
}

注意,在使用反射之前需要引入reflect的包

1
2
3
4
import (
"fmt"
"reflect"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
var num float64 = 1.2345

pointer := reflect.ValueOf(&num)
value := reflect.ValueOf(num)

// 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
// Golang 对类型要求非常严格,类型一定要完全符合
// 如下两个,一个是*float64,一个是float64,如果弄混,则会panic
convertPointer := pointer.Interface().(*float64)
convertValue := value.Interface().(float64)

fmt.Println(convertPointer)
fmt.Println(convertValue)
}

运行结果:
0xc42000e238
1.2345

reflet例子2:

  • 首先定义一个类以及关于这个类的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type User struct {
    Id int
    Name string
    Age int
    }

    func (this User) Call() { //为什么这个地方返回类型不是*
    fmt.Println("user is called ..")
    fmt.Printf("%v\n", this)
    }
  • 再定义一个利用反射选择类中值和方法的函数

1
2
3
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
func DoFiledAndMethod(input interface{}) {
//获取输入的类型type
inputType := reflect.TypeOf(input)
fmt.Println("input type is: ", inputType.Name())
//output: input type is: User

//获取input的value
inputValue := reflect.ValueOf(input)
fmt.Println("input value is: ", inputValue)
//output: input value is: {1 eric 19}

//通过type获取其中的字段
/*
1、获取interface中的reflect的type, 通过type得到numfield,进行遍历
2、得到每个filed,就是数据类型
3、通过filed中有一个interface()方法得到对应的value
*/
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
// value:=inputType.Field{i}.interface()
value := inputValue.Field(i).Interface()
// fmt.Println(field) //{Id int 0 [0] false}、{Name string 8 [1] false}
/*
每个field表示的就是一行的元素内容,其中Name表示了这一行的变量名, Type表示的是这一行的类型
通过value单独存在field里面,根据索引值寻找通过Interface()调用
*/
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)

}

/* 如何遍历选择方法呢
也是根据inputType来进行划分
*/
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}

注意点:

  1. reflect.TypeOf(input)得到类型
  2. reflect.ValueOf(input)得到对应的值
  3. reflect.TypeOf(input).NumField()的方法是获得interface()中的所有的字段
  4. 如果选择字段中的类型: inputType.Field(i) =reflect.TypeOf(input).Field(i)
  5. 如果选择字段中的值:inputType.Field(i) =reflect.TypeOf(input).Field(i).Interface()
  6. 如果想便利interface中的方法:reflect.TypeOf(input).NumMethod(),其中具体的方法是:reflect.TypeOf(input).Method(i)

通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumMethod进行遍历
  2. 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
  3. 最后对结果取其Name和Type得知具体的方法名
  4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是一样的判断处理方式
通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

1
2
3
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
package main

import (
"fmt"
"reflect"
)

func main() {

var num float64 = 1.2345
fmt.Println("old value of pointer:", num)

// 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
pointer := reflect.ValueOf(&num)
newValue := pointer.Elem()

fmt.Println("type of pointer:", newValue.Type())
fmt.Println("settability of pointer:", newValue.CanSet())

// 重新赋值
newValue.SetFloat(77)
fmt.Println("new value of pointer:", num)

////////////////////
// 如果reflect.ValueOf的参数不是指针,会如何?
pointer = reflect.ValueOf(num)
//newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

运行结果:
old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77
  1. 需要传入的参数是*float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,注意一定要是指针
  2. 如果传入的参数不是指针,而是变量,那么
    • 通过Elem获取原始值对应的对象则直接panic
    • 通过CanSet方法查询是否可以设置返回false
  1. newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
  2. reflect.Value.Elem()表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的,是指只能修改原是对象的值的大小,不能修改地址
  3. newValue.SetFloat(77)重新设置值的操作,传递引用来修改interface中的值

反射的基本原理

结构体标签

本质上还是利用了反射,通过以下形式给结构体中的变量添加标签作用:其他包在调用这个当前包的时候对于某个属性的一个说明,指示某个包在具体使用中的作用。

作用:能够将结构体转化为json格式

1
2
3
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
package main

import (
"fmt"
"reflect"
)
type resume struct{
/*
通过以下形式给结构体中的变量添加标签
作用: 其他包在调用这个当前包的时候对于某个属性的一个说明,指示某个包在具体使用中的作用
*/
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}

func findtag(str interface{}){
t :=reflect.TypeOf(str).Elem()

for i:=0;i<t.NumField();i++{
tagstring:=t.Field(i).Tag.Get("info")
tagdoc:=t.Field(i).Tag.Get("doc")
fmt.Println("info:",tagstring,"doc",tagdoc)
}
}

func main(){
var re resume
findtag(&re) //注意这里传递的是引用
}

go printf的占位符格式

go printf的占位符表示

输出之后在json格式转换中可以看到如下,注意可以看到的是输出的内容是根据给定的tag来进行标题的命名的

转化为json格式的例子

利用反射取出元素查询

利用编码和解码对struct 和json之间的转化

1
2
3
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
package main

import (
"fmt"
"encoding/json"
)

//如何将结构体转化成json
type Movie struct{
Title string `json:"title"` //就是告诉json库对应的变量的标签名字是这个
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"` //slice `json:"actors"`
}
func main(){
movie :=Movie{"喜剧之王",2000,10,[]string{"zhouxingchi","zhangbozhi"}}

//编码的过程就是将 struct-->json

jsonStr,err:=json.Marshal(movie)

//返回两个字段

if err!=nil{
fmt.Println("json marshal error",err)
return
}
fmt.Printf("jsonStr=%s\n",jsonStr )
/* jsonStr={"title":"喜剧之王","year":2000,"rmb":10,"Actors":["zhouxingchi","zhangbozhi"]} */

//解码过程:json-->struct
//jsonStr={"title":"喜剧之王","year":2000,"rmb":10,"actors":["zhouxingchi","zhangbozhi"]}

myMovie:=Movie{}
//注意这个地方传递的参量是引用
err=json.Unmarshal(jsonStr,&myMovie)
if err!=nil{
fmt.Println("json unmashal error",err)
return
}
fmt.Printf("%v\n",myMovie)
//返回结构体
//{喜剧之王 2000 10 [zhouxingchi zhangbozhi]}
}
]]> diff --git a/page/2/index.html b/page/2/index.html index 3b75f1d..7ea37c0 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -204,6 +204,134 @@ + + + +
diff --git a/search.xml b/search.xml index 955f802..e304dbe 100644 --- a/search.xml +++ b/search.xml @@ -1701,6 +1701,8 @@ proxy),其作用是用于使 Go

执行以下命令开启go mod管理

go env -w GO111MODULE=on
+

Go mod操作

+
go mod init github.com/hub/project

Go基本语法

如何编译并运行一个Go文件

对于已经写好的go文件,这里以hello.go作为例子,直接使用以下语句进行编译并运行

@@ -3234,6 +3236,7 @@ href="https://leetcode.cn/problems/partition-equal-subset-sum/description/">http
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

思路:

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

+

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

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

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

图论

@@ -5661,4 +5664,181 @@ Gin 路由器实例中

后端开发 + + 【后端开发】Consul服务与配置 + /2024/02/02/consul/ + +

Consul介绍

+

Consul是hashicorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 +内置了服务注册与发现框架、分布一致性协议实现、健康检查、key/value存储、多数据中心方案,不再需要依赖其它工具。

+

Consul是一个服务网络解决方案,它使团队能够管理服务之间以及跨多云环境和运行时的安全网络连接。Consul提供服务发现、基于身份的授权、L7流量管理和服务到服务加密。

+

Consul示意图

+

Consul角色

+

服务发现和注册

+
    +
  • dev:开发模式
  • +
  • client:客户端,接受请求转达给服务端集群,将http和dns接口请求转发给局域网内的服务端集群,它只是一个代理的角色
  • +
  • server:服务端,保存配置信息,高可用集群,每个数据中心的server数据推荐为3个或5个
  • +
+

Consul内部角色介绍

+
    +
  • 不管是server还是client,统称为agent
  • +
  • consul client是相对无状态的,只负责转发rpcserver,资源开销很少
  • +
  • server是一个有一组扩展功能的代理,这些功能包括参与raft选举,维护集群状态,响应rpc查询,与其它数据中心交互wan gossip和转发查询给leader或远程数据中心。
  • +
  • 每个数据中心,clientserver是混合的,一般建有3-5台server
  • +
+

安装Consul

+

安装地址:https://www.consul.io/,这里建议安装终端版本

+
consul agent -dev -clinent=0.0.0.0
#-dev表示开发模式运行, -server表示服务模式运行
+
#默认端口
localhost:8500
+

登陆这个默认的端口就能看见可视化的界面

+

Consul工作原理

+

producer:服务提供者

+

consumer:服务消费者

+

image-20240202230444248

+

服务发现与注册

+
    +
  • producer启动时,会将自己的ip/host等信息通过发送请求告知consul
  • +
  • consul接收到producer的注册信息后,每隔10秒(默认)会向producer发送一个健康检查的请求,检查producer是否处于可用状态
  • +
  • post +服务注册 /health健康定期检查
  • +
+

服务调用

+
    +
  • consumer请求product时,会先从consul中拿存储的producer服务的ip和port的临时表(temp table),从表中任选一个producer的ip和port
  • +
  • 根据这个ipport,发送访问请求
  • +
  • 此表只包含通过健康检查的producer信息,并且每隔10秒更新
  • +
  • Temp table 拉取服务列表 +从临时表中拿producer的ip和端口发送请求
  • +
+

Consul+Go实现

+

下面给出一个例子对于在GO项目中是如何结合Consul进行使用的

+

consul中间件目录

+

首先给出consul中间件的结构体图,这里主要对consul进行了sdk的封装,包含了以下的内容:

+
    +
  1. consulclient:服务注册与发现的板块
  2. +
  3. consulconfig:主要是中心化配置的内容
  4. +
  5. consulsdk:主要是对以上的两个板块进行了封装
  6. +
  7. main_test:是给出了一个例子对上述的内容进行测试
  8. +
+

Consulclient

+

这部分着重来写如何实现对服务注册和服务发现的功能

+
    +
  • 定义 ConsulClient 结构体
  • +
+
goCopy code
type ConsulClient struct {
client *consulapi.Client
serverPort int
}
+

ConsulClient 结构体包含了一个 Consul +客户端指针和一个服务端口。

+
    +
  • 创建新的 Consul 客户端
  • +
+
func NewConsulClient(consulAddress string, serverPort int) (*ConsulClient, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulClient{client: client, serverPort: serverPort}, nil
}

+

NewConsulClient 函数用于创建一个新的 Consul +客户端实例。它接收 Consul 服务器的地址和服务端口作为参数,并返回一个 +ConsulClient 实例以及可能的错误。

+
    +
  • 注册服务
  • +
+
func (c *ConsulClient) RegisterService(serviceID, serviceName, serviceHost string, servicePort int) error {
service := &consulapi.AgentServiceRegistration{
ID: serviceID,
Name: serviceName,
Address: serviceHost,
Port: servicePort,
}
return c.client.Agent().ServiceRegister(service)
}

+

RegisterService 方法用于向 Consul +注册服务。它接收服务的ID、名称、主机和端口作为参数,并向 Consul +注册该服务。

+
    +
  • 服务发现
  • +
+
func (c *ConsulClient) DiscoverService(serviceName string) (string, error) {
services, _, err := c.client.Health().Service(serviceName, "", true, nil)
if err != nil {
return "", err
}
if len(services) == 0 {
return "", fmt.Errorf("service not found")
}

// 随机选择一个服务实例
if len(services) > 0 {
index := rand.Intn(len(services))
service := services[index].Service
address := fmt.Sprintf("%v:%v", service.Address, service.Port)
return address, nil
}

return "", fmt.Errorf("no healthy instances found for service %s", serviceName)
}

+

DiscoverService 方法用于从 Consul +中发现服务实例。它接收服务名称作为参数,并返回一个服务实例的地址。在内部,它通过健康检查来获取可用的服务实例,并随机选择一个健康的实例返回其地址。

+

Consulconfig

+

这段代码实现了一个基本的 Consul 配置中心客户端,用于从 Consul +中获取、设置和删除键值对的配置信息。

+
    +
  • 定义 ConsulConfigCenter 结构体
  • +
+
type ConsulConfigCenter struct {
client *consulapi.Client
}
+
    +
  • 创建新的 Consul 配置中心客户端
  • +
+
func NewConsulConfigCenter(consulAddress string) (*ConsulConfigCenter, error) {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulConfigCenter{client: client}, nil
}
+

NewConsulConfigCenter 函数用于创建一个新的 Consul +配置中心客户端实例。它接收 Consul 服务器的地址作为参数,并返回一个 +ConsulConfigCenter 实例以及可能的错误

+
    +
  • 获取特定键对应的值
  • +
+
func (cc *ConsulConfigCenter) GetValue(key string) (string, error) {
kv := cc.client.KV()
pair, _, err := kv.Get(key, nil)
if err != nil {
return "", err
}
if pair == nil {
return "", fmt.Errorf("key '%s' not found", key)
}
return string(pair.Value), nil
}
+

GetValue 方法用于从 Consul +中获取特定键对应的值。它接收键名作为参数,并返回键对应的值以及可能的错误。

+
    +
  • 设置键值对
  • +
+
func (cc *ConsulConfigCenter) SetValue(key, value string) error {
kv := cc.client.KV()
p := &consulapi.KVPair{Key: key, Value: []byte(value)}
_, err := kv.Put(p, nil)
return err
}
+

SetValue 方法用于在 Consul +的键值存储中设置一个键值对。它接收键名和值作为参数,并将其设置到 Consul +中。

+
    +
  • 删除键值对
  • +
+
func (cc *ConsulConfigCenter) DeleteValue(key string) error {
kv := cc.client.KV()
_, err := kv.Delete(key, nil)
return err
}
+

DeleteValue 方法用于从 Consul +的键值存储中删除指定的键值对。它接收键名作为参数,并将对应的键值对从 +Consul 中删除。

+

ConsulSDK

+

实现了一个基于单例模式的 Consul SDK,用于管理 Consul +客户端和配置中心。让我们逐段解释代码的功能

+
    +
  • 定义 ConsulSDK 结构体
  • +
+
type ConsulSDK struct {
Client *ConsulClient
ConfigCenter *ConsulConfigCenter
}
+

ConsulSDK 结构体包含了 Consul 客户端和配置中心的实例

+
    +
  • 定义全局变量和 sync.Once 实例
  • +
+
var (
instance *ConsulSDK
once sync.Once
)
+

定义了 instance 变量用于保存 ConsulSDK +实例,once 变量用于确保 GetInstance() +函数只被执行一次

+
    +
  • 实现 NewConsulSDK 函数
  • +
+
func NewConsulSDK(consulAddress string, serverPort int) (*ConsulSDK, error) {
// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
return nil, err
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
return nil, err
}

// 返回 ConsulSDK 实例
return &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}, nil
}
+

NewConsulSDK 函数用于创建一个新的 ConsulSDK 实例,它接收 +Consul 服务器地址和端口作为参数,并返回一个 ConsulSDK +实例以及可能的错误

+
    +
  • 实现 GetInstance() 函数
  • +
+
func GetInstance() *ConsulSDK {
// 保证只执行一次
once.Do(func() {
consulAddress := "127.0.0.1:8500"
serverPort := 8080

// 创建 Consul 客户端
client, err := NewConsulClient(consulAddress, serverPort)
if err != nil {
fmt.Println("Failed to create Consul client:", err)
os.Exit(1)
}

// 创建 Consul 配置中心
configCenter, err := NewConsulConfigCenter(consulAddress)
if err != nil {
fmt.Println("Failed to create Consul config center:", err)
os.Exit(1)
}

// 初始化 ConsulSDK 实例
instance = &ConsulSDK{
Client: client,
ConfigCenter: configCenter,
}
})
return instance
}
+

GetInstance() 函数用于获取 ConsulSDK 的单例实例。它通过 +once.Do() 确保只执行一次,创建 Consul +客户端和配置中心,并将其保存到全局变量 instance +中,然后返回该实例

+

Main_test.go

+

这个部分主要对上述的sdk接口给出了一个具体的测试用例

+

注意在终端运行的时候的运行为:go test +go默认对_test会进行测试

+
package consul

import (
"fmt"
"os"
"testing"
)

func TestConsul(t *testing.T) {
//创建新的 consul sdk
sdk := GetInstance()

// 注册服务
serviceID := "my_service"
serviceName := "my_service"
serviceHost := "127.0.0.1"
servicePort := 8080

err := sdk.Client.RegisterService(serviceID, serviceName, serviceHost, servicePort)
if err != nil {
fmt.Printf("Error registering service: %v\n", err)
os.Exit(1)
}

// 发现服务
serviceAddress, err := sdk.Client.DiscoverService(serviceName)
if err != nil {
fmt.Printf("Error discovering service: %v\n", err)
os.Exit(1)
}
fmt.Printf("Discovered service address: %s\n", serviceAddress)

//使用 ConsulConfig 获取键对应的值
value, err := sdk.ConfigCenter.GetValue("test")
if err != nil {
fmt.Println("Failed to get value:", err)
return
}
fmt.Println("Value:", value)
}

+]]>
+ + 后端开发 + + + 后端开发 + go开发 + +
+ + deepl + /2024/01/30/deepl/ + https://blog.csdn.net/weixin_42693876/article/details/120345924

+

https://zhuanlan.zhihu.com/p/560482252

+

https://zhuanlan.zhihu.com/p/667048896

+]]>
+
diff --git "a/tags/go\345\274\200\345\217\221/index.html" "b/tags/go\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000..4d50360 --- /dev/null +++ "b/tags/go\345\274\200\345\217\221/index.html" @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tags - go开发 - LIHAIBIN'S BLOG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+

1 posts in total

+
+ + + + +

2024

+ + + +
【后端开发】Consul服务与配置
+
+ +
+ + + + + +
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/index.html b/tags/index.html index 9872d96..eeb291d 100644 --- a/tags/index.html +++ b/tags/index.html @@ -204,7 +204,7 @@
diff --git "a/tags/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" "b/tags/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" index 13c8cab..5f51c6e 100644 --- "a/tags/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" +++ "b/tags/\345\220\216\347\253\257\345\274\200\345\217\221/index.html" @@ -204,7 +204,7 @@
-

6 posts in total

+

7 posts in total


@@ -212,6 +212,12 @@

2024

+ + +
【后端开发】Consul服务与配置
+
+ +
【后端开发】Go-kit与Gin框架