[{"title":"Use gRPC with SpringBoot","url":"/2022/05/29/Use-gRPC-with-SpringBoot/","content":"gRPC相比于REST HTTP请求在接口规范、传输性能和流式通信方面更好,因此广泛应用分布式系统和微服务的通信过程中. 本文主要是针对目前gRPC-Java和SpringBoot的入门级教程不够统一而写,也是使用过程的记录.感谢grpc-spring-boot-starter提供了开箱即用的gRPC服务端和客户端的接口实现,因此我们可以专注业务实现.\n\n\n项目简述该Demo主要实现一个gRPC服务端和一个gRPC客户端,服务端提供一个api接收请求参数name,返回消息Hello ==&gt; &lt;name&gt;. 本项目基于maven构建的多module工程实现.\n预备为了避免出现版本兼容问题,我们在parent pom文件中定义好springboot、java、grpc和对应grpc-spring-boot-starter版本,如下:\n&lt;parent&gt;    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;    &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;    &lt;version&gt;2.5.8&lt;/version&gt;    &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;&lt;/parent&gt;&lt;properties&gt;    &lt;java.version&gt;11&lt;/java.version&gt;    &lt;grpc.version&gt;1.42.2&lt;/grpc.version&gt;    &lt;grpc.starter.version&gt;2.13.1.RELEASE&lt;/grpc.starter.version&gt;&lt;/properties&gt;\n\ngrpc interface1. proto文件定义我们新建一个子module为grpc, 作为公用proto文件和对应protobuf生成的文件的存放处(后续的gRPC server和client项目都以此module作为依赖),其中proto文件放在src/main/proto目录下.进行相关定义.\nsyntax = &quot;proto3&quot;;package service;option java_multiple_files = true;option java_package = &quot;com.fengzw.minimall.minimalluser.service&quot;;option java_outer_classname = &quot;HelloWorldProto&quot;;// The greeting service definition.service MyService &#123;  // Sends a greeting  rpc SayHello (HelloRequest) returns (HelloReply) &#123;&#125;&#125;// The request message containing the user&#x27;s name.message HelloRequest &#123;  string name = 1;&#125;// The response message containing the greetingsmessage HelloReply &#123;  string message = 1;&#125;\n\n2. maven配置同时在module的pom文件中配置好maven插件和相关依赖,如下:\n&lt;dependency&gt;    &lt;groupId&gt;io.grpc&lt;/groupId&gt;    &lt;artifactId&gt;grpc-netty-shaded&lt;/artifactId&gt;    &lt;version&gt;$&#123;grpc.version&#125;&lt;/version&gt;    &lt;scope&gt;runtime&lt;/scope&gt;&lt;/dependency&gt;&lt;dependency&gt;    &lt;groupId&gt;io.grpc&lt;/groupId&gt;    &lt;artifactId&gt;grpc-protobuf&lt;/artifactId&gt;    &lt;version&gt;$&#123;grpc.version&#125;&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;    &lt;groupId&gt;io.grpc&lt;/groupId&gt;    &lt;artifactId&gt;grpc-stub&lt;/artifactId&gt;    &lt;version&gt;$&#123;grpc.version&#125;&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt; &lt;!-- necessary for Java 9+ --&gt;    &lt;groupId&gt;org.apache.tomcat&lt;/groupId&gt;    &lt;artifactId&gt;annotations-api&lt;/artifactId&gt;    &lt;version&gt;6.0.53&lt;/version&gt;    &lt;scope&gt;provided&lt;/scope&gt;&lt;/dependency&gt;&lt;build&gt;    &lt;extensions&gt;        &lt;extension&gt;            &lt;groupId&gt;kr.motd.maven&lt;/groupId&gt;            &lt;artifactId&gt;os-maven-plugin&lt;/artifactId&gt;            &lt;version&gt;1.6.2&lt;/version&gt;        &lt;/extension&gt;    &lt;/extensions&gt;    &lt;plugins&gt;        &lt;plugin&gt;            &lt;groupId&gt;org.xolstice.maven.plugins&lt;/groupId&gt;            &lt;artifactId&gt;protobuf-maven-plugin&lt;/artifactId&gt;            &lt;version&gt;0.6.1&lt;/version&gt;            &lt;configuration&gt;                &lt;protocArtifact&gt;com.google.protobuf:protoc:3.19.2:exe:$&#123;os.detected.classifier&#125;&lt;/protocArtifact&gt;                &lt;pluginId&gt;grpc-java&lt;/pluginId&gt;                &lt;pluginArtifact&gt;io.grpc:protoc-gen-grpc-java:1.45.1:exe:$&#123;os.detected.classifier&#125;&lt;/pluginArtifact&gt;                &lt;protoSourceRoot&gt;src/main/proto&lt;/protoSourceRoot&gt;                &lt;outputDirectory&gt;src/main/java&lt;/outputDirectory&gt;                &lt;clearOutputDirectory&gt;false&lt;/clearOutputDirectory&gt;            &lt;/configuration&gt;            &lt;executions&gt;                &lt;execution&gt;                    &lt;goals&gt;                        &lt;goal&gt;compile&lt;/goal&gt;                        &lt;goal&gt;compile-custom&lt;/goal&gt;                    &lt;/goals&gt;                &lt;/execution&gt;            &lt;/executions&gt;        &lt;/plugin&gt;        &lt;plugin&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;        &lt;/plugin&gt;    &lt;/plugins&gt;&lt;/build&gt;\n通过编译该module可以自动生成对应的gRPC文件到src/main/java的目录下.\n服务端module1. maven配置新建一个module作为gRPC服务端,配置好对应的pom文件:(包括grpc module和server-starter依赖)\n&lt;dependency&gt;    &lt;groupId&gt;com.fengzw.minimall&lt;/groupId&gt;    &lt;artifactId&gt;grpc&lt;/artifactId&gt;    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;    &lt;groupId&gt;net.devh&lt;/groupId&gt;    &lt;artifactId&gt;grpc-server-spring-boot-starter&lt;/artifactId&gt;    &lt;version&gt;$&#123;grpc.starter.version&#125;&lt;/version&gt;&lt;/dependency&gt;\n\n2. 定义服务service构建一个service提供gRPC server服务,如下:\n@GrpcServicepublic class GrpcServerService extends MyServiceGrpc.MyServiceImplBase &#123;    @Override    public void sayHello(HelloRequest request, StreamObserver&lt;HelloReply&gt; responseObserver) &#123;        System.out.println(&quot;Hello ==&gt; &quot; + request.getName());        HelloReply reply = HelloReply.newBuilder().setMessage(&quot;Hello ==&gt; &quot; + request.getName()).build();        responseObserver.onNext(reply);        responseObserver.onCompleted();    &#125;    @Override    public void sayHelloAgain(HelloRequest request, StreamObserver&lt;HelloReply&gt; responseObserver) &#123;        super.sayHelloAgain(request, responseObserver);    &#125;&#125;\n如果你顺利编译了grpc module,那么MyServiceGrpc文件和相应的model文件会出现在grpc的src/main/java目录下. 启动该module,你会在SpringBoot的启动日志见到如下信息:\n2022-05-29 20:47:18.223  INFO 16841 --- [  restartedMain] n.d.b.g.s.s.AbstractGrpcServerFactory    : Registered gRPC service: service.MyService, bean: grpcServerService, class: com.fengzw.minimall.minimalluser.service.GrpcServerService2022-05-29 20:47:18.223  INFO 16841 --- [  restartedMain] n.d.b.g.s.s.AbstractGrpcServerFactory    : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl2022-05-29 20:47:18.223  INFO 16841 --- [  restartedMain] n.d.b.g.s.s.AbstractGrpcServerFactory    : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService2022-05-29 20:47:18.281  INFO 16841 --- [  restartedMain] n.d.b.g.s.s.GrpcServerLifecycle          : gRPC Server started, listening on address: *, port: 9091\n此时通过idea的http request你可以调用grpc请求来测试该grpc service的状态.\nGRPC localhost:9090/service.MyService/SayHello&#123;  &quot;name&quot;: &quot;request&quot;&#125;\n你会看到如下输出.\n&#123;  &quot;message&quot;: &quot;Hello \\u003d\\u003d\\u003e request&quot;&#125;\n这样我们的服务端就实现好了.\n客户端module1. maven配置新建一个module作为grpc 客户端, 如服务端一样, 我们同样需要在maven pom文件中依赖grpc module, 并依赖client-starter依赖如下:\n&lt;dependency&gt;    &lt;groupId&gt;com.fengzw.minimall&lt;/groupId&gt;    &lt;artifactId&gt;gprc&lt;/artifactId&gt;    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;    &lt;groupId&gt;net.devh&lt;/groupId&gt;    &lt;artifactId&gt;grpc-client-spring-boot-starter&lt;/artifactId&gt;    &lt;version&gt;$&#123;grpc.starter.version&#125;&lt;/version&gt;&lt;/dependency&gt;\n\n2. springboot配置文件中配置grpc server信息首先我们需要在配置文件application.properties中定义好grpc server的相关信息:\ngrpc.client.user-server.address=static://127.0.0.1:9091grpc.client.user-server.enable-keep-alive=truegrpc.client.user-server.keep-alive-without-calls=truegrpc.client.user-server.negotiation-type=plaintext\n\nWARNING: 注意如果是同一台服务器来进行服务端和客户端的测试, 那么服务端需要设置grpc server的端口在除了9090之外的端口, 否则会引起调用时报错INTERNAL: http2 exceptionCaused by: io.netty.handler.codec.http2.Http2Exception: First received frame was not SETTINGS. Hex dump for first 5 bytes: 485454502f\n\n3. 定义客户端方法然后我们在一个spring service组件下定义我们的客户端方法,如下:\n@Servicepublic class DemoService &#123;    @GrpcClient(&quot;user-server&quot;)    private MyServiceGrpc.MyServiceBlockingStub myServiceStub;    public String hello(String name) &#123;        HelloRequest request = HelloRequest.newBuilder().setName(name).build();        return myServiceStub.sayHello(request).getMessage();    &#125;&#125;\n这里@GrpcClient中的value表示grpc server的名称, 在配置文件中进行了定义. 我们可以通过定义一个controller来对外提空一个rest api来进行grpc客户端调用, 如下:\n@Autowiredprivate DemoService service;@GetMapping(&quot;/hello&quot;)public String hello() &#123;    return service.hello(&quot;demo&quot;);&#125;\n这样客户端就全部配置完成了.\n总结可以看到相比于Go语言中对gRPC的使用, grpc-spring-boot-starter极大地简化了非业务相关的代码.\n","categories":["Java"],"tags":["SpringBoot","gRPC"]},{"title":"slice使用append的小坑","url":"/2021/04/22/be-careful-when-use-append-to-add-slice-to-slice/","content":"今天在刷一道算法题的时候遇到了一个关于slice在使用append的小细节问题。这个算法题可以参考路径总和II。题意很简单，就是从一个二叉树中所有由根到叶子节点的路径中找到所有的满足路径和等于target的路径。\n\n\n问题在参考官方题解的golang版本的时候，发现了有一段代码是我一开始没弄明白的。完整代码可参考\nfunc pathSum(root *TreeNode, targetSum int) (ans [][]int) &#123;    path := []int&#123;&#125;    var dfs func(*TreeNode, int)    dfs = func(root *TreeNode, left int) &#123;        if root == nil &#123;            return        &#125;        left -= root.Val        path = append(path, root.Val)        defer func() &#123; path = path[:len(path)-1] &#125;()        if left == 0 &amp;&amp; root.Left == nil &amp;&amp; root.Right == nil &#123;            ans = append(ans, append([]int&#123;&#125;, path...))            return        &#125;        dfs(root.Left, left)        dfs(root.Right, left)    &#125;    dfs(root, targetSum)    return&#125;\n其中的ans = append(ans, append([]int&#123;&#125;, path...))则是我关注的地方，一开始我疑惑为什么不直接ans = append(ans, path))，但是实验证明结果会非常奇怪，比如对于这样一个例子我们想要的是[[5,4,11,2],[5,8,4,5]]，但是实际上是[[5,8,4,1],[5,8,4,1]].  \n解析这里其实是我忘了golang中的slice底层实现如下图所示\ntype slice struct &#123;    array unsafe.Pointer    len   int    cap   int&#125;\n因此如果像ans = append(ans, path))这样做，其实在ans存放的是path这个slice，在后续代码运行中的改变会导致ans的结果改变，从而产生一个错误的结果。而ans = append(ans, append([]int&#123;&#125;, path...))则是在ans中存放了一个path的副本，且该副本不会被其他代码修改到。因此结果才是正确的。\n\n一行代码就揭示golang中slice的本质~，真实妙啊。\n\nREFERENCE[1] leetcode 113题[2] 深入解析 Go 中 Slice 底层实现\n","categories":["Golang"],"tags":["go","指针"]},{"title":"bugfix of pytorch out of memory","url":"/2022/04/21/bugfix-of-pytorch-out-of-memory/","content":"记录一次模型推理过程中显存不断增加直至Out-of-Memory的解决过程.\n\n\n场景当我在算法中加入了一个运算量不大的操作后, 模型推理过程直接从原来的显存占用不到11G直接爆了3090的显存.\n显存增加定位首先需要判断是什么地方造成了显存的增加. 这里推荐一个库Pytorch-Memory-Utils.该库可以通过侵入式的track方法记录两个track方法之间的显存变化. 其记录的格式如下\nAt main.py line 18: &lt;module&gt;  Total Tensor Used Memory:466.4  Mb Total Allocated Memory:466.4  Mb+ | 1 * Size:(120, 3, 512, 512)   | Memory: 360.0 M | &lt;class &#x27;torch.Tensor&#x27;&gt; | torch.float32+ | 1 * Size:(80, 3, 512, 512)    | Memory: 240.0 M | &lt;class &#x27;torch.Tensor&#x27;&gt; | torch.float32At main.py line 23: &lt;module&gt;  Total Tensor Used Memory:1066.4 Mb Total Allocated Memory:1066.4 Mb\n\n最终是发现在模型decoder部分导致了显存的激增, 并且随着推理的进行,不断累加. \n解决解决这里参考了Pytorch模型测试时显存一直上升导致爆显存, 原因应该是decoder中的梯度在一直计算并累积导致显存的不合理累加. 因此解决方案也比较直接:\nwith torch.no_grad():    y = torch.sigmoid(self.refiner(s, features, im_size))\n将decoder的过程用torch.no_grad() wrap起来. bug就解决好了.\n","categories":["pytorch"],"tags":["pytorch","gpu","oom"]},{"title":"Gorm加悲观锁的最新用法","url":"/2021/11/30/gorm-for-update/","content":"因为google了“gorm for update”或者是“gorm 开启排他锁”出来的文章清一色的使用着如下用法来开启表的行/表锁  \ntx.Set(&quot;gorm:query_option&quot;, &quot;FOR UPDATE&quot;).First(&amp;employee, id)\n\n但是经过测试，我加了没有作用，搜索gorm官方文档，结果用法已经变成如下\ndb.Clauses(clause.Locking&#123;Strength: &quot;UPDATE&quot;&#125;).Find(&amp;users)// SELECT * FROM `users` FOR UPDATEdb.Clauses(clause.Locking&#123;  Strength: &quot;SHARE&quot;,  Table: clause.Table&#123;Name: clause.CurrentTable&#125;,&#125;).Find(&amp;users)// SELECT * FROM `users` FOR SHARE OF `users`\n\n果然有问题先找官方文档。😂\n","categories":["Golang"],"tags":["go","gorm"]},{"title":"[6.824系列] Outline of lab1 solution","url":"/2022/04/10/outline-of-6-824-lab1-solution/","content":"本文主要总结了如何实现6.824 lab1的大纲, 方便自己和读者能够迅速理解MapReduce具体要实现些什么.\n\n概览首先整个MapReduce的过程可以参考下图, 分为两个阶段: Map和Reduce阶段.从这个图中我们可以总结出MapReduce整体的工作流程如下\n\n将任务的输入文件拆分多个split文件\n创建多个Map任务, 每个任务对应输入若干个split\n在多个worker服务器上执行Map任务, 并将输出写入到Intermediate files中\n当所有Map完成后, MapReduce进入Reduce阶段, 为多个worker服务器分配Reduce任务\n每个Reduce任务读取对应的Intermediate files完成Reduce任务, 并将输出写入对应的output files\n\n问题Q1: 什么时候由谁来对任务输入文件进行split? 如何split?\n\nA: 由Master在MapReduce任务初始化的过程完成, split的目的是为了将一个任务拆分成多个子任务给到多个worker机器运行, 实现并发加速, 因此split文件的数量应该等同于worker机器数量来设置. 同时split应该按照预设好的文件的大小来进行划分(比如一个split文件为1KB大小). \n\nQ2: Map是什么?\n\nA: 接收一个key/value的pair, 产生一系列的key/value中间结果的过程就是Map, 比如说下面是一个wordcount的map例&gt; 子\nmap(String key, String value):    // key: document name    // value: document contents    for each word w in value:        EmitIntermediate(w, &quot;1&quot;);\n\nQ3: Reduce怎么知道自己对应的是哪些Intermediate files?\n\nA: 我们把对Map产生的每一个key/value, 对key进行hash后分配到一个Reduce任务, 具体来说:\n\n给定一个Map任务X以及产生的key/value pair\nReduce Y = hash(key) % nReduce\nwrite to “mr-X-Y” file因此, 对于给定的Reduce任务Y, 它去读取所有的mr-?-Y文件作为输入.\n\n\nQ4: 如何处理Map任务或者Reduce任务执行超时或失败的问题\n\nA: 利用超时机制和Master被动分配任务来实现超时重试, 对每一个任务都记录其开始的时间, 当worker请求分配任务的时候, 遍历所有的任务判断是否超时, 如果超时则是一个可以被分配的任务.\n\n实现大纲大纲可以分为两个部分: Master和Worker\n1. Worker对于每一个Worker, 它的执行过程都可以概括为:无限循环请求任务分配并执行直至MapReduce全部任务完成. 过程如下:\n\n向Master申请分配任务(由Master决定分配Map还是Reduce任务)\n如果Master返回MapReduce已经全部完成则退出循环\n根据返回的任务类型(Map or Reduce)执行对应的处理过程\n重复上述\n\n1.1 Map处理Map的输入: taskId(任务ID), nReduce(预设的Reduce个数), fileName(输入文件的访问路径)\n\n这里需要注意fileName的定义, 在这个作业当中, 我们可以直接使用文件名作为fileName, 这是因为程序跑在单机环境下, 通过多个进程的形式模拟分布式环境, 因此默认情况下相关文件都存放在主程序的当前目录下, 因此可以通过文件名即可以访问到, 但是在生产环境, 我们往往会依托一个分布式文件系统来存放这些文件(比如GFS), 因此此时fileName应该是一个文件访问URL, 可以通过URL去分布式文件系统读取到对应文件. \n\nMap的过程\n\n读取文件内容并经map函数处理得到对应kv对集合kva\n创建nReduce个临时文件\n将kva按照key hash值写入到对应临时文件\n重命名临时文件为mr-X-Y, X为taskId(mapId), Y为reduceId.\n通知Master, 该任务完成\n\n\n这里采用约定大于配置思想, 因此不需要通知Master该任务输出文件的文件名, 但是如果是分布式文件系统的话需要给出系统接口返回的文件URL给Master.\n\n1.2 Reduce处理过程和Map处理类似Reduce的输入: taskId, intermediateFileNames(中间文件访问路径的集合)\nReduce的过程\n\n遍历读取中间文件, 并保存到一个内存的列表中\n按照key来排序列表\n将列表中每一个key和对应的value集合交给reduce函数处理\n结果写入到临时文件并重命名\n通知Master任务完成\n\n2. MasterMaster作为MapReduce的协调者, 需要对所有Map、Reduce任务的状态进行管理, 同时在对应的状态下进行任务分配. 总的来说需要具备以下功能:\n\n任务初始化\n任务分配\n任务完成处理\n\n2.1 任务初始化这一过程实际上也是Master的初始化过程. 这一过程, Master需要设定好需要多少个Reduce和Map, 同时对相应的Map和Reduce任务进行初始化, 同时进入整个MapReduce进入Mapping状态.\n2.2 任务分配这个过程核心在于判断当前MapReduce(Master)处于什么状态从而分配相应的任务:\n\n如果处于Mapping状态, 遍历MapTasks列表判断每一个任务是否可以被分配\n判断该任务是否超时, 如果超时则需要将其状态修改为可被分配\n\n\n如果处于Reducing状态, 遍历ReduceTasks列表进行类似的分配\n如果处于Done状态, 则返回Exit状态码给worker告知整个MapReduce已经结束\n\n2.3 任务完成处理\n给定完成的任务ID: taskId\n根据阶段Mapping/Reducing找到对应的Task\n修改Task的状态为Finished\n如果该阶段所有任务都已经完成,则推进Master的状态到下一阶段\n\n\n\n","categories":["6.824"],"tags":["go","分布式"]},{"title":"[6.824系列] Outline of lab2A solution","url":"/2022/04/26/outline-of-6-824-lab2A-solution/","content":"本文主要总结了如何实现6.824 lab2A.\n\n主要任务如下:\n\n实现Raft Leader Election\n实现leader和follower之间的心跳机制(不带log)这个lab关键是要处理好多个goroutine对Raft状态机的并发写.\n处理不好, 容易造成死锁/有限时间内选不出leader/选出多个leader等多种问题.\n\n\n\n主要实现参考论文中的Figure 2\nRaft状态机我们首先需要定义出 leader election 和 heartbeat 两个过程中Raft需要维护的数据, 这一点在Raft论文中的Figure 2已经说明了, 我们在它的基础上加上一些运行时数据.具体如下:\ntype Raft struct &#123;    ... // other    currentTerm     int             // latest term server has seen    votedFor        int             // 选举过程投给谁一票, 初始化或reset后为-1表示未投票    roleState       int             // 表示server当前处于三角色之一: leader, candidate, follower    electionTimeout time.Duration   // 当前term所设置的选举超时, 同时也是心跳超时    lastTickTime    time.Time       // 上一次刷新心跳的时间&#125;\n\nRPC调用AppendEntries在Lab2A中, AppendEntries主要用于心跳机制, 因此主要用于通知follower当前的term是什么, 以便于follower进行term的更新和timeout的重置.具体来说, AppendEntries调用处理的过程分为下面几个部分:\n\n上锁, 因为后续需要读写raft状态和数据\n判断心跳传递过来的term是否比当前节点的term新, 如果不新的话, 表示该心跳无效, 返回false\n有效则刷新 lastTickTime\n满足以下三种情况则重置节点状态\n[leader with older term]\n[candidate with older or same term]\n[follower with older term]\n\n\n\n\n重置节点状态实际上就是\n\n更新term到最新\nvotedFor=-1\nroleState=Follower\n\n\nRequestVoteRequestVote用于当节点提升为candidate的时候, 向其他节点索要投票时调用. 具体来说, candidate需要将自己的term和id告知其他节点, 而其他节点收到RequestVote后, 需要对term的合法性(newer)进行验证, 如果term合法且节点未投票给其他节点或者已经投票给candidate, 则投票给该candidate. 大致处理过程有以下几个部分:\n\n加锁\n如果发送过来的term更早, 说明该选举过程已过时, 直接返回false\n如果term更新, 说明是新一轮的选举, 此时节点需要重置状态\n判断是否未投过票或者该请求的candidateId是已选过的候选者, 则给该候选者投票\n刷新 lastTickTime\n\n\n\n心跳机制每个raft节点如果是leader状态的话,会周期性地向其他节点发送心跳. 在Lab2A中, 心跳机制在发送方其实不需要做什么事情, 唯一需要做的是在心跳ack的term比自己的term大的情况下, 需要重置自己的状态, 这可能是因为网络等原因导致其他节点超时收不到心跳而发起了新的选举.\n\n各个节点维护一个共同的peers数组, 表示集群所有节点的网络访问地址\n\n选举每个raft节点会在不是leader的状态且选举超时的情况下发起选举, 我们可以将这个过程转变为一个周期性过程: 即每隔 election timeout 就尝试发起新的选举, 如果已经是leader状态(上一次选举成功)或者选举还未超时(其他leader刷新了节点的lastTickTime)则sleep, 否则发起选举. 具体选举过程如下:\n\n向其他节点索要投票\n检查返回结果和当前状态\n如果不是candidate(说明有新的leader重置了当前节点的状态)或者当前的term已经被其他选举更新过, 则直接退出选举\n返回的term更新, 则说明当前candidate状态无效, 重置当前状态并退出选举\n\n\n返回结果为true, 则记一票, 超过半数的票的情况, 当前节点当选leader并立刻发起心跳给其他节点同步状态.\n\n注意点\n任何需要查询和修改raft状态的操作都需要加锁\n避免在持有锁的过程中请求其他的锁, 极易发生死锁\n避免在持有锁的过程中进行RPC调用, 应在rpc调用之前释放锁. 接收返回结果后在加锁处理后续逻辑\n\n\n这是因为在这个作业中, 被调方和调用方实际上共享锁, 这会导致其中一个协程A持有锁进行RPC调用而被调用方处于协程B由于锁被占用而阻塞.\n\n","categories":["6.824"],"tags":["go","分布式"]},{"title":"面试16种代码模式总结(1) - 滑动窗口","url":"/2021/04/04/sliding-window/","content":"本系列文章是对Grokking the Coding Interview: Patterns for Coding Questions课程的总结，编程语言使用Go。读者如果想要更细致的了解，请自行购买课程学习。\n问题背景在处理数组和链表的时候，我们经常会需要找出满足某些条件的连续子列, 比如子列的和最大等，这个时候就可以使用滑动窗口的思想来进行解答。这里注意的是子列可以是固定大小也可以是可变大小，两种情况有相应的处理方式。\n\n\n下面先给出一个例子，比如在给定一个数组，找出其中每个size为K的连续子列的平均值。  \nArray: [1, 3, 2, 6, -1, 4, 1, 8, 2], K=5Output: [2.2, 2.8, 2.4, 3.6, 2.8]\n\n滑动窗口的解决方案如下:\nfunc findAveragesOfSubArraysBySlidingWindow(K int, arr []int) []float64 &#123;    results := make([]float64, len(arr)-K+1)    windowSum := 0    windowStart := 0    for windowEnd := 0; windowEnd &lt; len(arr); windowEnd++ &#123;        // 把下个元素加上        windowSum += arr[windowEnd]        // 滑动窗口，特别地，如果没有达到K个则不滑动        if windowEnd &gt;= K-1 &#123;            // 当前windowSum计算了整个窗口的和            results[windowStart] = float64(windowSum) / float64(K)            // 当前窗口计算完后，移动窗口需要先减去原来窗口头部的元素            windowSum -= arr[windowStart]            windowStart++        &#125;    &#125;    return results&#125;\n\n解法总结根据窗口大小是否固定，分为固定大小和可变大小\n固定大小这里拿 找出size=k的最大连续子序列和 作为例子\nGiven an array of positive numbers and a positive number k,find the maximum sum of any contiguous subarray of size k.Input: [2, 1, 5, 1, 3, 2], k=3Output: 9Explanation: Subarray with maximum sum is [5, 1, 3].Input: [2, 3, 4, 1, 5], k=2Output: 7Explanation: Subarray with maximum sum is [3, 4].\n\n解决方案\nfunc max(x, y int) int &#123;    if x &gt; y &#123;        return x    &#125; else &#123;        return y    &#125;&#125;func solution(arr []int, k int) int &#123;    var (        windowStart = 0        windowSum = 0 // 用于记录当前窗口的状态        maxSum = 0 // 用于记录最好的状态    )    for windowEnd:=0;windowEnd&lt;len(arr);windowEnd++&#123;        // 这一步是窗口的右侧进行一格扩张，同时更新当前窗口的状态        windowSum += arr[windowEnd]        // 判断当前窗口是否满足相应条件        if windowEnd &gt;= k-1&#123;            // 更新最好的状态            maxSum = max(maxSum, windowSum)            // 收缩窗口左侧            windowSum -= arr[windowStart]            windowStart--        &#125;    &#125;    return maxSum&#125;\n\n\n相关练习: [1][2][3]\n\n可变大小这里通过长度最小的子数组来说明\nfunc min(i, j int) int &#123;    if i&lt;j &#123;        return i    &#125;else&#123;        return j    &#125;&#125;func minSubArrayLen(target int, nums []int) int &#123;    var(        windowStart = 0        windowSum = 0        minSize = -1    )    for windowEnd:=0;windowEnd&lt;len(nums);windowEnd++&#123;        // 窗口右侧扩张，这个部分和固定大小是一样的        windowSum+=nums[windowEnd]        // 这个部分是与固定大小类型最大的区别        // 不同于固定大小每次的右侧扩张和左侧收缩是同步的        // 可变大小的左侧收缩需要循环判断窗口是否满足条件        // 可能右侧扩张了很多次，但是左侧才收缩一回        // 也可能右侧扩张一次，左侧需要收缩几回        for windowSum&gt;=target&#123;            if minSize==-1&#123;                minSize = windowEnd-windowStart+1            &#125;else&#123;                minSize = min(minSize, windowEnd-windowStart+1)            &#125;             windowSum-=nums[windowStart]            windowStart++        &#125;        // 对于一些情况条件需要进行后处理，比如当收缩窗口直到窗口内单一字符数量&lt;k        // 然后再比较当前窗口的大小    &#125;    if minSize==-1&#123;        return 0    &#125;    return minSize&#125;\n\n\n相关练习题: [1][2][3][4][5][6][7]\n\n特殊例子最小覆盖子串原题见leetcode\nfunc findSubstring(str, pattern string) string &#123;    // 边界     if len(pattern)&gt;len(str)&#123;        return &quot;&quot;    &#125;    var (        windowStart = 0        matched     = 0        minLength   = len(str) + 1        subStrStart = 0        patternMap  = make(map[byte]int)    )    for i := 0; i &lt; len(pattern); i++ &#123;        patternMap[pattern[i]]++    &#125;    for windowEnd := 0; windowEnd &lt; len(str); windowEnd++ &#123;        wright := str[windowEnd]        if _, ok := patternMap[wright]; ok &#123;            patternMap[wright]--             // 关键点1, 不再是==0，而是&gt;=0，因为包含所有的字符            if patternMap[wright] &gt;= 0 &#123;                matched++            &#125;        &#125;        // 如果当前window包含pattern所有的字符，则从左收缩窗口至不完全包含状态         // 注意这里等号右侧不是len(patternMap)，因为是要匹配所有字符        for matched == len(pattern) &#123;             if minLength &gt; windowEnd-windowStart+1 &#123;                minLength = windowEnd - windowStart + 1                subStrStart = windowStart            &#125;            wleft := str[windowStart]            if _, ok := patternMap[wleft]; ok &#123;                // 关键点2，只要有一个匹配的字符移除了窗口，则停止收缩                if patternMap[wleft] == 0 &#123;                    matched--                &#125;                patternMap[wleft]++            &#125;            windowStart++        &#125;    &#125;    if minLength &gt; len(str) &#123;        return &quot;&quot;    &#125; else &#123;        return str[subStrStart : subStrStart+minLength]    &#125;&#125;\nFAQ: 这里对上述代码做一定的解释，主要困惑点在于两个关键点Q: 为什么关键点1处是&gt;=0？A: 因为是要匹配所有的字符，因为每遇到一个需要匹配的字符，我们都需要对patternMap中对应值做减法并matched++, 直到我们匹配完了pattern所有该字符，此时对于多余该字符我们只需要更新patternMap，但是不必再更新matched。\nQ: 为什么关键点2处是==0？A: 这里注意，如果窗口同一个字符没有冗余，那么移除了一个该字符则意味着匹配不完全，但是如果窗口内该字符有冗余，比如pattern是aab，而窗口内有3个a，此时a有冗余，如果收缩窗口只移出了一个a，那么此时窗口依然是满足匹配完全条件的，反映到代码上，收缩前patternMap[&#39;a&#39;] = -1 (在2的基础-3)，收缩后patternMap[&#39;a&#39;]=0，因此如果收缩掉下一个a，则需要matched–了。\n串联所有单词的子串原题见leetcode这个题目主要要对题意理解清楚，首先目标子串需要满足几个条件：\n\n长度等于单词列表中的每个单词长度*单词个数\n子串不可以出现不在列表上的其他单词\n\nfunc findWordConcatenation(str string, words []string) []int &#123;    if len(words)==0||len(str)==0&#123;        return []int&#123;&#125;    &#125;    var (        wordFreqMap = make(map[string]int)        wordsCount  = len(words)        wordLength  = len(words[0])        // 存储满足条件的index        startIndex  = make([]int, 0, 10)     )    // 记录每个单词出现的频率    for _, v := range words &#123;        wordFreqMap[v]++    &#125;    // 注意这个不是常规的sliding window pattern     // i表示的是一个子串的起始位置，从条件可知每个子串是固定长度的     // 所以i最大为len(str)-wordLength*wordsCount    for i := 0; i &lt;= len(str)-wordLength*wordsCount; i++ &#123;        wordsSeenMap := make(map[string]int) // key1: 保存看过的word的数量        // 对从当前index i开始的长度为wordLength*wordsCount的string进行判定        for j := 0; j &lt; wordsCount; j++ &#123;            nextWordIndex := i + j*wordLength // 当前word的开始index            word := str[nextWordIndex : nextWordIndex+wordLength]            // 如果出现不在words数组中的word，则直接跳出            if _, ok := wordFreqMap[word]; !ok &#123;                break            &#125;            wordsSeenMap[word]++             // 如果words数组中某个word数量出现次数过多意味着一定会有其他单词不在，则也直接跳出            if wordsSeenMap[word] &gt; wordFreqMap[word] &#123;                break            &#125;            // 如果遍历到该string的最后一个字符都没跳出循环，意味该string是满足条件的            if j == wordsCount-1 &#123;                startIndex = append(startIndex, i)            &#125;        &#125;        // ===end===    &#125;    return startIndex&#125;\n\n\nREFERENCE[1] https://www.educative.io/courses/grokking-the-coding-interview[2] https://github.com/zhiwei-Feng/Golang-Grokking-the-Coding-Interview-Patterns-for-Coding-Questions\n","categories":["面试","16种代码模式"],"tags":["go","algorithm","sliding window"]},{"title":"Leetcode 组合总和问题总结","url":"/2022/04/18/summary-of-combination-sum/","content":"组合总和系列题总结.\n\n39. 组合总和\n40. 组合总和II\n216. 组合总和III\n377. 组合总和IV\n\n组合总和本题注意candidates是无重复元素,且元素都大于0, 利用backtrace的思想求解即可\nfunc combinationSum(candidates []int, target int) [][]int &#123;    ans := [][]int&#123;&#125;    path := []int&#123;&#125;    pathSum := 0    var backtrace func(idx int)    backtrace = func(idx int) &#123;        if idx == len(candidates) || pathSum &gt;= target &#123;            if pathSum == target &#123;                tmp := make([]int, len(path))                copy(tmp, path)                ans = append(ans, tmp)            &#125;            return        &#125;        backtrace(idx+1)        for i:=1;i&lt;=target/candidates[idx];i++&#123;            if candidates[idx]*i+pathSum&lt;=target &#123;                pathSum+=candidates[idx]*i                for k:=0;k&lt;i;k++&#123;                    path = append(path, candidates[idx])                &#125;                backtrace(idx+1)                pathSum-=candidates[idx]*i                path = path[:len(path)-i]            &#125;else &#123;                break            &#125;        &#125;    &#125;    backtrace(0)    return ans&#125;\n\n组合总和II本题的关键是如何去重, 思路依旧是backtrace. 具体去重的思想如下:\n\n对于candidates和target以及当前索引idx, 我们保证candidates[idx]只能被选取一次来扣减target, 从而保证当前情况下没有重复, 并将问题转化为candidates[idx+1:]凑成target-candicates[idx]的子问题.\n为了能够方便的避免重复candidates的选取, 我们将candidates首先进行排序\n\nfunc combinationSum2(candidates []int, target int) [][]int &#123;    var (        ans = make([][]int, 0)        path = make([]int, 0)        backtrace func(int, int)        n = len(candidates)    )    sort.Ints(candidates)    backtrace = func(begin int, remain int) &#123;        if remain==0 &#123;            ans = append(ans, append([]int&#123;&#125;, path...))            return        &#125;        for i:=begin;i&lt;n;i++&#123;            if candidates[i]&gt;remain&#123;                // 当前候选值太大，剪枝                break            &#125;            if i&gt;begin&amp;&amp;candidates[i]==candidates[i-1]&#123;                // 因为candidates[i-1]存在被选取的情况, 为避免重复跳过后续的重复元素                continue            &#125;            path = append(path, candidates[i])            backtrace(i+1, remain-candidates[i])            path = path[:len(path)-1]        &#125;    &#125;    backtrace(0, target)    return ans&#125;\n\n组合总和III注意条件中需要组合列表个数等于k, 因此回溯的返回条件因加入判断.\nfunc combinationSum3(k int, n int) (ans [][]int) &#123;    path := []int&#123;&#125;    var backtrace func(idx, remain int)    backtrace = func(idx, remain int) &#123;        if idx &gt; 9 || remain == 0 || len(path) == k &#123;            // idx表示当前遍历到的数字, 大于9则过界            // 同时如果remain等于0, 或者path的长度达到k了都属于到达边界的情况            if remain == 0 &amp;&amp; len(path) == k &#123;                ans = append(ans, append([]int&#123;&#125;, path...))            &#125;            return        &#125;        for i:=idx;i&lt;=9;i++&#123;            if i&gt;remain &#123;                break            &#125;            path = append(path, i)            backtrace(i+1, remain-i)            path = path[:len(path)-1]        &#125;    &#125;    backtrace(1, n)    return&#125;\n\n组合总和IV(*)本题使用backtrace会超时, 因此需要使用dp来解, 本题的dp解法类似于完全背包问题, 但需要注意的是对于target=7, nums=[1,2,3]的输入, 完全背包认为[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]六种情况是一种方案, 而本题认为是六种方案, 这一点是本题解法的关键.\nfunc combinationSum4(nums []int, target int) int &#123;    n := target    // dp的含义, dp[i][j]对于长度i的组合, 总和为j的方案数, 本题由于nums全部为正数, 因此i最大为target    dp := make([][]int, n+1)    for i:=range dp &#123;        dp[i] = make([]int, target+1)    &#125;    dp[0][0]=1    ans := 0    for i:=1;i&lt;=n;i++&#123;        for j:=0;j&lt;=target;j++&#123;            for _, u := range nums &#123;                if j&gt;=u &#123;                    dp[i][j]+=dp[i-1][j-u]                &#125;            &#125;        &#125;        // 对于不同长度的组合, 总和为target的方案, 我们累加        ans += dp[i][target]    &#125;    return ans&#125;\n\n数组降维, 减少空间复杂度\nfunc combinationSum4(nums []int, target int) int &#123;    n := target    dp := make([]int, target+1)    dp[0]=1    ans := 0    for i:=1;i&lt;=n;i++&#123;        for j:=target;j&gt;=0;j--&#123;            dp[j] = 0            for _, u := range nums &#123;                if j&gt;=u &#123;                    dp[j]+=dp[j-u]                &#125;            &#125;        &#125;        ans += dp[target]    &#125;    return ans&#125;\n\n进一步减少时间复杂度\nfunc combinationSum4(nums []int, target int) int &#123;    // 这个解法类似于爬楼梯, 对于每个总和j而言, nums为每次攀爬的楼梯数    // 所以 dp[j] = sum of &#123; dp[j-nums[i]] for each i belong to 0..len(nums) &#125;    dp := make([]int, target+1)    dp[0]=1    for j:=1;j&lt;=target;j++&#123;        for _, u := range nums &#123;            if j&gt;=u &#123;                dp[j]+=dp[j-u]            &#125;        &#125;    &#125;    return dp[target]&#125;\n","categories":["算法"],"tags":["algorithm","leetcode","backtrace"]},{"title":"从6.824的kv.go理解tcp协议","url":"/2021/04/11/socket-analysis-for-6-824-kv-go/","content":"本文是在学习6.824课程时，对Lec 2的kv.go产生的困惑的解释和总结。\n\n\nkv.go首先对kv.go的情况进行解释。这里只会取一些重点片段进行解释，需要\b了解完整代码细节的，读者可自行选择文章最后的参考条目进行详细了解。\n主逻辑func main() &#123;    //启动rpc服务，一个kv存储    server()     //写入一个kv键值对    put(&quot;subject&quot;, &quot;6.824&quot;)    fmt.Printf(&quot;Put(subject, 6.824) done\\n&quot;)    //读出刚才存储的key的value    fmt.Printf(&quot;get(subject) -&gt; %s\\n&quot;, get(&quot;subject&quot;))&#125;\n主逻辑非常简单，启动一个kv存储的rpc服务，然后运行简单的读写功能测试。\nserver()func server() &#123;    kv := new(KV)    kv.data = map[string]string&#123;&#125;    rpcs := rpc.NewServer()    rpcs.Register(kv)    l, e := net.Listen(&quot;tcp&quot;, &quot;:1234&quot;)    if e != nil &#123;        log.Fatal(&quot;listen error:&quot;, e)    &#125;    go func() &#123;        for &#123;            conn, err := l.Accept()            if err == nil &#123;                go rpcs.ServeConn(conn)            &#125; else &#123;                break            &#125;        &#125;        l.Close()    &#125;()&#125;\n启动服务，监听1234端口，并开始接受客户端的连接\nclientfunc get(key string) string &#123;    client := connect()    args := GetArgs&#123;key&#125;    reply := GetReply&#123;&#125;    err := client.Call(&quot;KV.Get&quot;, &amp;args, &amp;reply)    if err != nil &#123;        log.Fatal(&quot;error:&quot;, err)    &#125;    client.Close()    return reply.Value&#125;func put(key string, val string) &#123;    client := connect()    args := PutArgs&#123;key, val&#125;    reply := PutReply&#123;&#125;    err := client.Call(&quot;KV.Put&quot;, &amp;args, &amp;reply)    if err != nil &#123;        log.Fatal(&quot;error:&quot;, err)    &#125;    client.Close()&#125;\n客户端方法总体逻辑是一致，首先进行tcp连接的建立，然后进行rpc方法调用，最后关闭连接\n解析过程如果你运行该程序，会发现打印如下：\n$ go run kv.goPut(subject, 6.824) doneget(subject) -&gt; 6.824$\n\n为什么程序最后会退出，而不是因为server在监听阻塞住。我自己的解释是：首先是因为server()中的无限接收请求的循环是运行在goroutine当中，所以之后的客户端请求可以并发的运行；其次因为主线程没有其他方法的阻塞，所以会在所有逻辑结束后退出，也意味该进程退出了，这样其中的所有线程都会结束，包括其中创建的goroutine。\n如果读者有更好的解释欢迎留言讨论。\n\n在该程序的主逻辑中，实际发生了两次tcp连接，我们知道一次tcp协议的运行过程有三个阶段：连接创建、数据传送和连接终止。server()下开启的rpc服务监听着1234端口，而get和put()中的connect()则完成一次tcp连接创建的三次握手，如图下\n\n此时通过netstat命令查看会出现有两个连接以及一个监听  \n\n当数据传送完毕，get和put()调用Close()方法进入连接终止阶段（四次握手）后，客户端状态则会进入TIME_WAIT状态\n\n\n等待2MSL后，客户端Close。\n\n如果希望对TCP的设计想要更深入了解，可以参考  \n\n为什么 TCP 协议有 TIME_WAIT 状态  \n为什么 TCP 建立连接需要三次握手  \nWhy is the TCP connection terminated in a 4-way handshake?\n\n\nREFERENCE[1] 6.824 kv.go[2] 维基百科  \n","categories":["计算机网络"],"tags":["go","network","socket","tcp"]},{"title":"Leetcode 最大矩形问题总结","url":"/2022/05/27/summary-of-largest-rectangle/","content":"概述该节涉及的Leetcode题目为:\n\n84. 柱状图中最大的矩形\n85. 最大矩形\n\n\n\n实际上, 85题可以看作是基于84题来拓展了一维的问题.首先来看84题,如下图,可以看到题目的意思就是给定一组宽度为1的矩形块,求出其中可以构成的最大矩形的面积.\n84.柱状图中最大的矩形不难看出,这里面有个隐含的提示,及对于某个矩形块(高度为x)而言,要想以它作为最大矩形的高度,那么就必须保证构成矩阵的相邻高度都大于等于x.\n比如说上图中,我们如果要以矩形高度为5的块作为最大矩形的高,则左侧已经没有块高度大于等于5,所以左侧无法拓展,而右侧因为有高度6的块,所以可以拓展一次,最终构造出来的矩形面积为2*5=10.\n因此解法即为遍历每一个块高度,并以它为中心向两侧拓展并计算其拓展后的矩形面积,取其中最大值.但是显然这种做法时间复杂度较高,因为它重复遍历两侧的元素.\n上述这种解法换个角度看可以认为是找到某个高度为x的矩形块左侧第一个高度低于x的块i和右侧第一个高度低于x块j,则构成的矩形面积为area = x*(j-i-1).启发自单调栈的思想,我们可以维护一个严格递增的栈,该栈里面存储矩形块的索引index,栈中元素满足从底到顶的矩形块高度严格递增.\n\n单调栈如何实现上述解法?\n\n保证栈单调的同时入栈当前的矩阵块索引i, 这样可以保证左侧第一个高度低于heights[i]的矩形块索引就是底下的那个元素\n另一方面,单调栈入栈前需要弹出其中高度大于等于当前块i高度的索引,这一步实际上对于每个弹出的矩阵块k是找到了右侧第一个高度低于heights[k]的矩形块索引,即i.\n\n\n因此,解法就很直接,维护单调栈的过程中,每弹出一个矩形块索引i,则以其高度heights[i]为矩形高度,宽度为(i-peek()-1)来计算当前面积,如此其中的最大值即为答案.具体代码过程如下:\npublic int largestRectangleArea(int[] heights) &#123;    Deque&lt;Integer&gt; stk = new ArrayDeque&lt;&gt;();    var ans = 0;    for (int i = 0; i &lt; heights.length; i++) &#123;        while (!stk.isEmpty() &amp;&amp; heights[stk.peek()] &gt;= heights[i]) &#123;            var curIdx = stk.pop();            var leftIdx = stk.isEmpty() ? -1 : stk.peek();            ans = Math.max(ans, heights[curIdx] * (i - leftIdx - 1));        &#125;        stk.push(i);    &#125;    var rightIdx = heights.length;    while (!stk.isEmpty()) &#123;        var curIdx = stk.pop();        var leftIdx = stk.isEmpty() ? -1 : stk.peek();        ans = Math.max(ans, heights[curIdx] * (rightIdx - leftIdx - 1));    &#125;    return ans;&#125;\n\n稍微注意一下边界问题, 当弹出栈顶的时候, 如果左侧没有元素,即栈空的情况下,左侧索引取-1. 而在遍历完成后,栈不为空,则说明栈中每个元素的右侧索引为数组长度n(n=heights.length).\n\n85.最大矩形这一题我们将其每一行看作一个84题中的heights数组,因此只需要遍历每一行的字符,如果为’1’则更新heights中对应索引的高度(+1),如果为’0’,则置0.然后应用84的算法计算该行的最大矩形面积.最终取每一行计算的面积最大值为答案. 代码如下:\npublic int maximalRectangle(char[][] matrix) &#123;    if (matrix.length == 0) &#123;        return 0;    &#125;    var heights = new int[matrix[0].length];    var ans = 0;    for (int i = 0; i &lt; matrix.length; i++) &#123;        for (int j = 0; j &lt; matrix[i].length; j++) &#123;            if (matrix[i][j] == &#x27;1&#x27;) &#123;                heights[j]++;            &#125; else &#123;                heights[j] = 0;            &#125;        &#125;        ans = Math.max(ans, maxRec(heights));    &#125;    return ans;&#125;// 84.int maxRec(int[] heights) &#123;    var ans = 0;    Deque&lt;Integer&gt; stk = new ArrayDeque&lt;&gt;();    for (int i = 0; i &lt; heights.length; i++) &#123;        while (!stk.isEmpty() &amp;&amp; heights[stk.peek()] &gt;= heights[i]) &#123;            var curIdx = stk.pop();            var leftIdx = stk.isEmpty() ? -1 : stk.peek();            ans = Math.max(ans, heights[curIdx] * (i - leftIdx - 1));        &#125;        stk.push(i);    &#125;    var rightIdx = heights.length;    while (!stk.isEmpty()) &#123;        var curIdx = stk.pop();        var leftIdx = stk.isEmpty() ? -1 : stk.peek();        ans = Math.max(ans, heights[curIdx] * (rightIdx - leftIdx - 1));    &#125;    return ans;&#125;\n","categories":["算法"],"tags":["algorithm","leetcode","单调栈"]},{"title":"Leetcode 旋转排序数组搜索问题总结","url":"/2022/04/14/summary-of-search-in-rotated-sorted-array/","content":"本篇文章主要对leetcode上关于旋转排序数组搜索问题的二分解法一个总结, 具体题目集有以下几个题目\n\n33. 搜索旋转排序数组\n81. 搜索旋转排序数组 II\n153. 寻找旋转排序数组中的最小值\n154. 寻找旋转排序数组中的最小值 II\n\n我们可以根据排序数组是否允许重复元素将其分为元素不可重复和可重复两类.\n元素不可重复找目标值对于每个mid位置,我们都根据他左侧有序还是右侧有序来考虑是否缩减左边界还是右边界,比如说对于mid的左侧有序(满足nums[l]&lt;=nums[mid])则判断target是否落在这个有序区间中,如果是则收缩右边界,否则收缩左边界,其他情况同理.\n// [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/)func search(nums []int, target int) int &#123;\tl, r := 0, len(nums)-1\tfor l &lt; r &#123;\t\tmid := l + (r-l)/2\t\tif nums[mid]==target &#123;\t\t\treturn mid\t\t&#125;\t\tif nums[l]&lt;=nums[mid] &#123;\t\t\tif target&lt;nums[mid]&amp;&amp;target&gt;=nums[l] &#123;\t\t\t\tr = mid-1\t\t\t&#125;else &#123;\t\t\t\tl = mid+1\t\t\t&#125;\t\t&#125;else &#123;\t\t\tif nums[mid]&lt;target&amp;&amp;target&lt;=nums[r]&#123;\t\t\t\tl = mid+1\t\t\t&#125;else &#123;\t\t\t\tr = mid-1\t\t\t&#125;\t\t&#125;\t&#125;\tif target!=nums[l] &#123;\t\treturn -1\t&#125;\treturn l&#125;\n\n找最小值与找目标值不同,找最小值我们应该将数组看作为山,沿着下坡的方向进行移动,对于一个mid,如果左侧有序,左侧和右侧都可能是最小值存在的区间,因此我们应该通过判断右侧是否有序,如果有序,则当前mid及左侧为最小值存在的区间,因此收缩右边界即可.反之当前mid及左侧必定不可能为最小值存在的区间.\n// [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)func findMin(nums []int) int &#123;    l, r := 0, len(nums)-1    for l &lt; r &#123;        mid := l + (r-l)/2        if nums[mid]&lt;nums[r] &#123;            r = mid        &#125; else &#123;            l = mid+1        &#125;    &#125;    return nums[l]&#125;\n\n元素可重复找目标值在不可重复的基础上只需要多判断一下mid, l, r三者位置的值是不是一样, 当一样的时候无法判断收缩左侧还是右侧, 但是可以确定l, r两个位置肯定不是target, 因此直接左移l和右移r即可\n// [81. 搜索旋转排序数组 II](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/)func search(nums []int, target int) bool &#123;    l, r := 0, len(nums)-1    for l &lt; r &#123;        mid := l + (r-l)/2        if nums[mid]==target &#123;            return true        &#125;        if nums[mid]==nums[l]&amp;&amp;nums[mid]==nums[r] &#123;            l++            r--        &#125;else if nums[l]&lt;=nums[mid] &#123;            if nums[l]&lt;=target &amp;&amp; target &lt; nums[mid] &#123;                r = mid-1            &#125;else&#123;                l = mid+1            &#125;        &#125;else &#123;            if nums[mid]&lt;target&amp;&amp;target&lt;=nums[r] &#123;                l = mid+1            &#125;else &#123;                r = mid-1            &#125;        &#125;    &#125;    return nums[l]==target&#125;\n\n找最小值和找目标值类似,在不可重复的基础,加一个mid, l, r三者的判断即可.\n// [154. 寻找旋转排序数组中的最小值 II](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/)func findMin(nums []int) int &#123;    l, r := 0, len(nums)-1    for l &lt; r &#123;        mid := l + (r-l)/2        if nums[l]==nums[mid]&amp;&amp;nums[r]==nums[mid]&#123;            l++            r--        &#125;else if nums[mid]&lt;=nums[r]&#123;            r = mid        &#125;else &#123;            l = mid+1        &#125;    &#125;    return nums[l]&#125;","categories":["算法"],"tags":["algorithm","leetcode","binary search"]},{"title":"两种翻转slice的方式对比","url":"/2021/04/16/two-way-to-reverse-slice/","content":"刷题时，遇到一个需求如下：对一个slice进行翻转。在实现的过程使用了两种方式：\n\n每次插入新元素时，使用append左插入的方式\n先正常append右插入，最后再对这个slice进行翻转\n\n\n\n两种方式的实现及时间和空间消耗的对比实现下面两个方法实现的功能是一致的\n\n方法1使用左插入法来实现翻转，如下所示\nfunc method1() &#123;    result := make([][]int, 0)    for i := 0; i &lt; 100; i++ &#123;        item := make([]int, 0)        for j := 0; j &lt; 10; j++ &#123;            item = append(item, i*j)        &#125;        // 左插入        join := [][]int&#123;item&#125;        result = append(join, result...)    &#125;&#125;\n方法2先正常append，再翻转\nfunc method2() &#123;    result := make([][]int, 0)    for i := 0; i &lt; 100; i++ &#123;        item := make([]int, 0)        for j := 0; j &lt; 10; j++ &#123;            item = append(item, i*j)        &#125;        result = append(result, item)    &#125;    tmp := result    result = make([][]int, 0)    for i := len(tmp) - 1; i &gt;= 0; i-- &#123;        result = append(result, tmp[i])    &#125;&#125;\n\n性能对比\n时间\ngoos: darwingoarch: amd64BenchmarkMethod1-4         17006             69558 ns/opBenchmarkMethod2-4         43002             27851 ns/op\n时间上，方法2要好于方法1\n\n空间\n\n\n\n空间上，方法2也要优于方法1\n分析通过pprof工具对其源码进行分析如下\n从上图我们发现，result = append(join, result...)语句的内存消耗非常严重，同时这种方法进行append，会使得地址重新分配（因为首地址改变了）致使多余内存和时间的消耗。\n补充方法2的翻转可以有两种方法实现\n\n如上面所示，通过slice反向遍历插入完成翻转\n还可以通过双指针法来翻转\n\n这里通过一个简单例子比较下双方的性能\nfunc method1() &#123;    var input = make([]int, 0, 100)    for i := 0; i &lt; len(input); i++ &#123;        input = append(input, i)    &#125;    // method1    for i := 0; i &lt; len(input)/2; i++ &#123;        j := len(input) - 1 - i        input[i], input[j] = input[j], input[i]    &#125;&#125;func method2() &#123;    var input = make([]int, 0, 100)    for i := 0; i &lt; len(input); i++ &#123;        input = append(input, i)    &#125;    // method2    tmp := input    input = make([]int, 0, 100)    for i := len(tmp) - 1; i &gt;= 0; i-- &#123;        input = append(input, tmp[i])    &#125;&#125;\n结果表示，双指针法会更好一些，理由很简单，因为双指针是O(N/2)的\n$ go test -run=. -bench=. -benchmemgoos: darwingoarch: amd64BenchmarkMethod1-4      71840024                16.9 ns/op             0 B/op          0 allocs/opBenchmarkMethod2-4      26362216                39.7 ns/op             0 B/op          0 allocs/opPASS\n\n\n结论这种情况下推荐使用方法2，同时对于方法1(左插入)的使用要格外谨慎，不当的使用会使得程序的时/空间消耗加剧。\n","categories":["Golang"],"tags":["go"]},{"title":"unexpected error in pytorch backward","url":"/2022/05/18/unexpected-error-in-pytorch-backward/","content":"在写模型训练的时候遇到奇怪的报错:\nRuntimeError: Trying to backward through the graph a second time, but the saved intermediate results have already been freed. Specify retain_graph=True when calling backward the first time.\n\n\n参考一些网上的解答和报错提示,意思就是执行backward的时候,中间结果已经被释放了.一开始比较纳闷在于,我仅仅增加了一些卷积层和操作在train方法里面,怎么突然就会进行该报错呢,并且中间结果都是在train方法中,不会出现backward的时候已经被释放.\n经过仔细审查,有个地方存在比较大的问题, 在我的代码出有这样一个逻辑\nfor j, batch in enumerate(loader, 1):    self.optimizer.zero_grad()    # pre-handle some thing    for i in range(1, len(images)):        s = self.model(images[i])        y = label[i]        acc = self.compute_accuracy(s.detach(), y)        loss = self.compute_loss(s, y)        loss.backward()    self.optimizer.step()\n上面这段代码看起来没啥问题, 但是注意如果pre-handle部分存在有计算图的梯度计算, 这一部分中间结果在loss.backward的时候就已经free掉了,由此会导致一开始的那个报错,而我正好在这一部分作了一个类似下面的操作:\nfor j, batch in enumerate(loader, 1):    self.optimizer.zero_grad()    # pre-handle some thing    # warning: 注意这一步    self.prev_ft = self.feature_extractor(images[0])    for i in range(1, len(images)):        s = self.model(images[i])        y = label[i]        acc = self.compute_accuracy(s.detach(), y)        loss = self.compute_loss(s, y)        loss.backward()    self.optimizer.step()\n上述使用feature_extractor抽取images[0]的特征, 因为不是处于torch.no_grad()的wrap环境下,因此会进行梯度计算. 解决方案也很简单, 将pre-handle的部分用torch.no_grad()包括起来即可.\nfor j, batch in enumerate(loader, 1):    self.optimizer.zero_grad()    # pre-handle some thing    with torch.no_grad():        self.prev_ft = self.feature_extractor(images[0])    for i in range(1, len(images)):        s = self.model(images[i])        y = label[i]        acc = self.compute_accuracy(s.detach(), y)        loss = self.compute_loss(s, y)        loss.backward()    self.optimizer.step()\n","categories":["pytorch"],"tags":["pytorch","python"]}]