-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.json
More file actions
1 lines (1 loc) · 417 KB
/
index.json
File metadata and controls
1 lines (1 loc) · 417 KB
1
[{"categories":["AI","必知必会"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 大语言模型的下半场——Agent介绍 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:0:0","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"什么是Agent? 我们都知道大模型很强大,能够像人类大脑一样思考和推理。但是大模型没有与世界互动的能力,就像人类只有大脑没有四肢,无法使用工具。如何让大模型能够使用工具,完成超越大模型能力边界的事情呢?答案是Agent,也叫做智能体。 OpenAI应用研究主管LilianWeng在LLM Powered Autonomous Agents中总结:Agent = Memory + Planning + Tools + Action。 记忆(Memory):记忆分为短期记忆和长期记忆,能够为大模型决策是提供上下文。 思考和决策(Planning):根据用户输入结合上下文进行思考和决策。 工具(Tools):决策如何使用工具,工具可以是任意的API调用结果。 行动(Action):执行决策选择的工具进行行动。 一个简单的Agent就是在不断地执行执行思考、调用工具、观测结果,最终返回结果。 以查询今天广州的天气为例子,假设我们为大模型提供两种工具: 查询当前时间:get_current_time() -\u003e time,无参数输入,返回当前时间。 查询某地天气:get_location_weather(city, time) -\u003e weather_describe,输入城市与时间,返回该城市的天气预报。 那么Agent可能的思考过程如下: 理解输入,知道是期望查询今天广州的天气。 查看已有的工具,发现get_location_weather工具可以查询天气。 观测到get_location_weather工具需要两个参数输入——city和time。 观测输入,理解city参数为广州,time参数为今天,无具体日期。 思考得知需要查询当前具体时间。 查看已有的工具,发现get_current_time工具可以查询当前时间,且无需参数输入。 调用get_current_time工具获取当前时间。 将city=广州和time=time作为参数调用get_location_weather工具。 观测get_location_weather工具结果,整理天气预报。 思考已经得到最终答案。 将最终答案返回给用户。 我们可以发现Agent的思考过程是一个循环调用的过程,这是一种称之为ReAct模式的思考链,其理论基础来源于一篇论文:ReAct: Synergizing Reasoning and Acting in Language Models。 ReAct是Reason和Action的结合。ReAct思考链遵循如下模式: 思考和推理问题。 制定行动计划。 执行行动。 观测行动结果。 循环直到任务完成。 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:1:0","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"Multi-Agent 通过两个或更多不同角色的Agent,让这些Agent协调完成复杂的工作,这种方式称之为Multi-Agent。 通过角色的定义,还可以让Agent之间拥有对话、竞争以及上下级的概念。 一个注明的例子是由斯坦福大学和Google共同推出的项目——虚拟小镇。小镇由25个Agent组成,各自拥有不同的角色和性格,在虚拟小组中模拟人类世界的爱恨情仇。 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:2:0","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"Agent前沿应用 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:3:0","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"Auto-GPT 可视化的AI Agent平台,使用流程图的方式来构建Agent。 项目地址:https://github.com/Significant-Gravitas/AutoGPT ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:3:1","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"BabyAGI BabyAGI是一个脚本,是使用另一种称之为Plan-and-Executor思考链的方式实现的Agent。通过不断将任务拆解为更小的任务,最终为目标提供一系列可行的步骤。 项目地址:https://github.com/yoheinakajima/babyagi ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:3:2","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"ChatDev 清华大学发布的应用,由多个Agent共同经营一家虚拟软件公司,每个Agent负责包括销售、产品、研发、测试、实施等不同角色。 项目地址:https://github.com/OpenBMB/ChatDev ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:3:3","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"虚拟小镇 斯坦福和Google共同推出的Multi-Agent项目,用于模拟和研究人类的交互行为。 论文地址:https://arxiv.org/pdf/2304.03442v1.pdf 项目地址:https://github.com/joonspk-research/generative_agents ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:3:4","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"大语言模型的开发框架 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:4:0","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"LangChain 由OpenAI推出的大语言模型开发框架,涵盖了基础设施(LangChain-Core)、开发框架(LangChain/LangGraph)、部署平台(LangServe)、观测平台(LangSmith)、社区插件(LangChain-Community)等一系列工具和平台,帮助开发者更好的进行AI应用的开发。 官方文档:https://python.langchain.com/v0.2/docs/introduction/ ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:4:1","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"AutoGen 由微软推出的开源框架,允许开发人员通过多个Agent构建AI应用。 项目地址:https://github.com/microsoft/autogen ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:4:2","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"什么限制了Agent的发展? ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:5:0","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"有限的上下文 在不断地思考和工具调用中,Prompt的长度会急剧膨胀,知道超过可输入的上下文上限。 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:5:1","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"LLM模型的理解能力 Agent会不断地思考、推理和决策,这非常依赖大模型本身的理解能力。如果大模型无法做出正确的决策,那么后续的工具调用都是空中楼阁。 就目前来看,至少需要70b及以上参数的模型,才能够输出比较质量的结果。 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:5:2","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"LLM模型的可靠输出 大模型的输出是随机的,这是大模型的魅力,也是限制。如何获取相对可靠的输出,是调用工具的关键。 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:5:3","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"多模态的感知能力 如何将多模态的感知输入到Agent是更高阶段需要考虑的。我们当然不希望大模型只能理解文本,还希望他能够有更多模的感知能力。比如物理世界中:视觉、听觉、触觉、嗅觉和精神世界中的:情绪等等。 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:5:4","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"想象力 虽然说现在Agent的应用难点主要是技术无法匹配上人类的想象力,但是在将如何应用Agent上,人类的想象力还是相当匮乏。 ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:5:5","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["AI","必知必会"],"content":"参考 动画科普AI Agent:大模型之后为何要卷它? ","date":"2024-08-25","objectID":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/:6:0","tags":["AI","Agent"],"title":"大语言模型的下半场(一)——Agent介绍","uri":"/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E7%9A%84%E4%B8%8B%E5%8D%8A%E5%9C%BA%E4%B8%80agent%E4%BB%8B%E7%BB%8D/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com Jenkins介绍 Jenkins是一款开源CI\u0026CD软件,用于自动化各种任务,包括构建、测试和部署软件。Jenkins本身并没有多少功能,而是使用插件来支持绝大多数功能。 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:0:0","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"Jenkins部署 官方地址:https://www.jenkins.io/zh/download/ ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:1:0","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"Jenkins核心功能 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:2:0","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"CICD Jenkins最主要的功能就是CICD,CICD主要有三个阶段:拉取/编译/打包/部署。 Jenkins本身并没有这些功能,而是通过插件的方式来灵活支持不同的系统。 拉取阶段:支持从git/github/gitlab/svn等代码托管仓库拉取代码。 编译阶段:根据编译代码的语言不同,来选择该语言的编译环境/代码审计/单元测试。 打包阶段:根据不同语言的特点,可以通过插件将代码打包成二进制/jar/war/docker。 部署阶段:通过ssh等方式将编译好的包部署到服务器上,或是推送docker镜像到镜像仓库中,再触发k8s更新资源。 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:2:1","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"运维管理 Jenkins通过Role-based Authorization Strategy插件,来支持细粒度的权限管理。比如可以控制某个角色能访问的项目和Job。 Jenkins通过Credentials Binding插件,来存储需要密文保护的密码/token。比如拉取阶段需要用到的github/gitlab token,编译阶段需要用到的数据库密码,部署阶段需要用到的ssh key/ssh密码等。 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:2:2","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"使用Jenkins构建github上的项目 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:0","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"使用Freestyle Project构建 使用Freestyle Project,适用于构建较简单的项目。官方推荐使用Pipeline进行构建,但是Pipeline有学习成本,需要学习Jenkinsfile的语法。 使用Freestyle Project进行项目构建,需要在创建item时,选择如下选项。 这个配置用于显示描述信息,创建完成之后的Job详情页面,可以跳转到github仓库。 这里进行github的配置,包括仓库url/拉取用户账户密码或token/仓库分支等。这里Credentials使用了Credentials Binding插件的能力,不需要明文写上账户密码了。 这些都配置完之后保存,在首页启动这个Job。这里可以看到执行状态这一栏,有两个node,意味着可以同时执行两个构建任务。 构建结束后,可以点击#1查看构建过程的输出日志,因为国内拉取github仓库经常失败,所以这里进行了多次构建,直到第三次才成功,所以可以看到版本已经到了#4。 点击构建历史可以查看之前的构建记录,查看#1的输出,可以看到就是与github建立连接失败所导致的。 再查看一下#4成功的构建记录,输出中显示拉取代码成功,仓库最后一次Commit的信息是Update Jenkinsfile。 因为在创建Job时并没有对Build阶段进行配置,因此这个Job执行完之后,就只是简单的将代码拉取到本地的workspace中,可以在Job详情的工作空间中看到拉取的代码,也可以在本地中查看。 接下来加上编译操作,将代码编译成二进制。点击配置,在Build Step中增加构建步骤。 这里需要选择执行脚本的解释器,比如windows系统的就要选择windows batch command,linux选择shell。脚本内容就是编译代码的脚本。 修改完成后保存,重新执行构建,构建成功后就可以在workspace中看到编译好的二进制程序。 重新查看构建输出,可以看到执行的脚本内容和执行过程。 构建成功后就到了分发部署的阶段,一般来说会将构建好的包通过ssh分发到测试环境或生产环境,或是打包成镜像push到镜像仓库,这种能力对应ssh插件:Publish Over SSH,打包成镜像也是通过shell脚本的方式打包并推送的。 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:1","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"使用Pipeline构建 使用Freestyle Project适合构建步骤比较简单的项目,如果构建步骤变得多而复杂,Freestyle Project这种图形化的管理无法直观的展示每一个构建的步骤和过程,也就是不够扁平化。 因此,对于构建步骤多而复杂的项目,Jenkins官方推荐使用Pipeline。Pipeline是一套插件,用于实现和集成持续发布的流水线到Jenkins中。 官方推荐做法是,将构建步骤组织成一个Jenkinsfile文件,存放在代码仓库中。当代码拉取下来时,Jenkins Pipeline会执行Jenkinsfile文件的内容进行构建。 Jenkinsfile文件使用一种叫做Groovy的编程语言的语法来编写构建步骤,官方定义一个最简单的构建分为三大部分:Build/Test/Deploy,因此一个最简单的Jenkinsfile文件如下所示。 pipeline { // 指示Jenkins为流水线任意分配一个执行器和工作区 agent any stages { stage('Build') { steps { // 编写脚本内容,脚本命令执行返回非0就结束 sh 'echo build' } } stage('Test') { steps { // 编写脚本内容 sh 'echo test' } } stage('Deploy') { steps { // 编写脚本内容 sh 'echo deploy' } } } } Jenkinsfile中可以定义变量,还可以读取环境变量/内置变量/参数变量等等。 pipeline { agent any environment { // 定义一个变量,全局生效 name = 'Jay' } // 选项参数 // 选项参数会自动注入到shell环境的env中,通过env | grep 变量名的方式可以过滤出来 parameters { choice(name: \"k8s_cluster\", choices: [\"\"], description: \"k8s集群名称\") choice(name: \"k8s_namespace\", choices: [\"\"], description: \"k8s命名空间\") } stages { stage('Hello') { environment { // 定义一个变量,stage内生效 age = '18' } steps { echo \"I'm ${name}, i'm ${age} year old.\" } } stage('Env') { steps { // 过滤选项参数 echo \"选项参数:\" sh \"env | grep k8s_*\" // 内置变量通过env来访问 echo \"JOB_NAME: ${env.JOB_NAME}\" // 内置的env变量,查看所有支持的env变量:${YOUR_JENKINS_URL}/pipeline-syntax/globals#env echo \"BUILD_ID: ${BUILD_ID}\" // 当前项目构建的id,比如第13次构建,就为13 echo \"BUILD_NUMBER: ${BUILD_NUMBER}\" // 与BUILD_ID相同 echo \"BUILD_URL: ${BUILD_URL}\" // 构建完之后的结果url,example http://buildserver/jenkins/job/MyJobName/17/ echo \"JOB_NAME: ${JOB_NAME}\" // 项目的名称 echo \"BUILD_TAG: ${BUILD_TAG}\" // 等于${JOB_NAME}-${BUILD_NUMBER} // 参数变量通过params来访问,params是项目配置好的所需参数 // 比如项目配置了一个字符参数key1和一个选项参数key2,就可以通过以下方式获取 // 如果使用了改参数,但是项目并没有设置该参数,那么会直接报错中断运行 echo \"${key1}\" echo \"${key2}\" } } } } parameters的用法 pipeline { parameters { choice( name: \"app_git_url\", choices: params.app_git_url ? params.app_git_url : [\"\"], description: \"应用gitlab仓库地址\" ) gitParameter( name: \"branch_tag\", type: \"PT_BRANCH_TAG\", defaultValue: \"master\", description: \"分支或标签\" ) string( name: \"dockerfile\", defaultValue: params.dockerfile ? params.dockerfile : \"\", description: \"dockerfile文件路径\", trim: true ) choice( name: \"jdk_version\", choices: [\"\", \"/usr/local/java-se-1.7.0\"], description: \"使用的jdk路径,默认使用jdk1.8\" ) string( name: \"mvn_arg\", defaultValue: params.mvn_arg ? params.mvn_arg : \"-s /var/lib/jenkins/.m2/settings-hd.xml clean deploy -U -Dmaven.test.skip=true -Dmaven.test.skip=true\", description: \"mvn命令参数\", trim: true ) choice( name: \"k8s_cluster\", choices: params.k8s_cluster ? params.k8s_cluster : [\"\"], description: \"k8s集群名称\" ) choice( name: \"k8s_namespace\", choices: params.k8s_namespace ? params.k8s_namespace : [\"\"], description: \"k8s命名空间\" ) choice( name: \"k8s_app_name\", choices: params.k8s_app_name ? params.k8s_app_name : [\"\"], description: \"k8s应用名称\" ) choice( name: \"k8s_pod_number\", choices: params.k8s_pod_number ? params.k8s_pod_number : [\"1\"], description: \"k8s pod数量\" ) choice( name: \"target_dir\", choices: params.target_dir ? params.target_dir : [\"\"], description: \"打包后jar包target目录,默认为工程根目录的target\" ) } } when的用法 pipeline { stages { stage(\"Pre Check\") { when { expression { fileExists(params.dockerfile) } } steps { echo \"预检查通过\" } } } } environment的值,可以动态从params获得 Jenkins Pipeline的一些示例:https://www.jenkins.io/doc/pipeline/examples/ Pipeline所用编程语言Groovy文档:http://groovy-lang.org/semantics.html ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:2","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"权限管理 下载Role-Based Strategy插件,在配置-\u003eSecurity-\u003eAuthorization-\u003eAuthorization-,选择Role-Based Strategy,然后在Manage Jenkins中就会多出一个Manage and Assign Roles配置。 role分为三种,global role(常用),item role(常用),agent role(不常用)。 给一个用户指定查看某些项目的限制: 创建用户 创建一个全局角色——read,只在Overall下拥有Read权限。(注意Job、View这两个块都不能设置Read权限,不然会覆盖Item role Pattern的配置) 创建item role——hello,并配置希望匹配的pattern 给用户赋予全局角色read和item role角色hello 然后就能看到该用户只有指定项目的权限 特殊用户 在给用户添加global role时,可以添加authenticated用户,这个特殊用户不需要创建,给他global role相当于给所有用户。 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:3","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"API访问及构建 Jenkins提供了REST API来操作各个功能,比如job的crud等等。 使用REST API需要经过认证,可以在用户设置中的API Token生成Token,给后续的api使用。 例子 列出所有可用API curl -u hetiansheng:11b446b0d5a5b503f19f095cb290c3f718 http://xxx.miniso.com:8080/api/json?pretty=true 对指定job进行构建 # job没有参数 curl -u hetiansheng:11b446b0d5a5b503f19f095cb290c3f718 http://jenkins1.miniso.com:8080/job/${JOB_NAME}/build # job有参数 # -d中的参数要满足需求,构建才会开始 # 比如key2是个选项参数,只有选中已有选项才合法 curl -u hetiansheng:11b446b0d5a5b503f19f095cb290c3f718 -d \"key1=test1\u0026key2=value-2\" http://jenkins1.miniso.com:8080/job/hello-api/buildWithParameters 使用默认参数进行构建 curl -u hetiansheng:11b446b0d5a5b503f19f095cb290c3f718 http://jenkins1.miniso.com:8080/job/hello-api/buildWithParameters pipeline进阶 ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:4","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"根据tag来构建 在参数化构建选项中,添加git参数选项,需要git插件的支持。 添加完之后可能会出现拉取不到tag的情况。解决这个问题的方法: 检查git仓库地址是否正确 检查pipeline中使用的credientalId是否正确 如果配置没问题,可以先手动配置默认值为最新tag,然后开始构建,后续就能自动拉取tag了 不同插件在pipeline中有不同表现,以GitSCM插件为例,在pipeline中根据tag来拉取代码。 pipeline{ // 定义本次构建使用哪个标签的构建环境,本示例中为 “slave-pipeline” agent any // agent { // node { // label 'master' // } // } // colorful message options { ansiColor('xterm') } // 定义groovy脚本中使用的环境变量 environment{ // 将构建任务中的构建参数转换为环境变量 // 目标应用仓库地址 LOCAL_APP_GIT_URL = sh(returnStdout: true, script: 'echo $app_git_url').trim() // 目标应用仓库分支 LOCAL_BRANCH = sh(returnStdout: true, script: 'if [ -z \"${branch}\" ]; then echo master; else echo ${branch}; fi').trim() // GIT仓库秘钥ID LOCAL_CREDENTIALS_ID = \"ci-jenkins\" // maven参数 LOCAL_MVN_ARG = sh(returnStdout: true,script: 'if [ -z \"${mvn_arg}\" ] ;then echo \"clean package -U -B -DskipTests\" ;else echo ${mvn_arg} ;fi').trim() } // \"stages\"定义项目构建的多个模块,可以添加多个 “stage”,可以多个 “stage” 串行或者并行执行 stages { // 定义第一个stage, 完成克隆源码的任务 stage('Git') { steps { // git branch: '${branch}', credentialsId: env.LOCAL_CREDENTIALS_ID, url: env.LOCAL_APP_GIT_URL checkout([ $class: 'GitSCM', branches: [[name: \"refs/tags/${branch}\"]], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: env.LOCAL_CREDENTIALS_ID, url: env.LOCAL_APP_GIT_URL]], ]) } } // 添加第二个stage,运行源码打包命令,并由maven统一推送到镜像仓库 stage('Package') { steps { script { sh \"JAVA_HOME=${jdk_version} mvn ${LOCAL_MVN_ARG}\" } } } } } ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:5","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"常见函数 sh sh '${params.test}'无法获取到参数,需要使用sh \"${params.test}\"。也支持另一种格式sh(returnStdout: true, script: \"echo ${params.test}\") ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:6","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Jenkins","CICD","运维","必知必会"],"content":"常用插件 build user vars 用法 pipeline { stages { stage('Display Build User') { steps { wrap([$class: 'BuildUser']) { echo \"$env.BUILD_USER_ID\" } } } } } ","date":"2023-07-05","objectID":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:7","tags":["Jenkins","CICD","运维"],"title":"Jenkins必知必会","uri":"/jenkins%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Kubernetes","运维","必知必会"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com Kubernetes架构 K8S使用CS架构,也就是Server-Client架构。客户端通过WebUI、CLI等工具与Kubernetes Master连接下达命令,Master再将这些命令下发给对应Node进行执行。 K8S Master包含四个核心组件: API Server:处理API请求,Kubernetes中所有组件都会与API Server进行连接 Controller:进行集群状态管理,比如容器故障恢复、自动水平扩容等 Scheduler:进行调度管理,比如观察和计算节点与容器负载,决定将容器调度到那个节点 ETCD:分布式存储系统,存储Kubernetes集群所需的元信息 K8S Node是真正运行业务负载的地方,需要运行的容器会经过Scheduler计算决定运行在哪个Node上,计算完之后通知API Server,API Server通知对应Node上的Kubelet组件运行对应的容器。 apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 Kubernetes元信息 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:0:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Labels 标签是Kubernetes中最重要的元数据,它使用Key:Value Pair的方式来标识资源对象,以便后续Selector能够筛选和组合资源。Selector支持与或非、集合的的逻辑组合来筛选资源。 # 给资源打上标签,\u003cresource\u003e表示资源的类型,\u003cresource-name\u003e表示具体资源名称 kubectl label \u003cresource\u003e \u003cresource-name\u003e key=value # 重写已有标签 kubectl label \u003cresource\u003e \u003cresource-name\u003e key=value --overwrite # 查看资源标签 kubectl get \u003cresource\u003e --show-labels # 给资源删除标签,key后面加 '-' 表示删除改标签 kubectl label \u003cresource\u003e \u003cresource-name\u003e key- # 筛选资源时指定标签 kubectl get \u003cresource\u003e --show-labels -l 'key=value' kubectl get \u003cresource\u003e --show-labels -l 'key in (value1, value2)' ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:1:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Annotation 注解用于记录资源的非标示性信息,扩展资源的描述。 # 给资源加上注解,\u003cresource\u003e表示资源的类型,\u003cresource-name\u003e表示具体资源名称 kubectl annotate \u003cresource\u003e \u003cresource-name\u003e describe='some describe for resource.' # 查看资源注解信息,在annotation中可以看到加上的注解 kubectl get \u003cresource\u003e \u003cresource-name\u003e -o yaml # kubectl工具apply文件创建资源时,会创建一个独特的annotation叫:kubectl.kubernetes.io/last-configuration,里面记录了apply是文件的内容,是一个json串 kubectl get deployments nginx -o yaml | grep -A 5 annotations ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:2:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"OwnerReference 所有者用于标识资源的所有者/创建者。比如有些资源是一个集合,比如Pod集合会由replicaset或statefulset创建,而想要删除整个集合资源时,就可以用到OwnerReference来找到属于同一集合的资源进行统一删除。 # Deployment类型的资源是Replicaset更高级的封装,因此查看Replicaset类型的资源也可以看到Deployment的资源,因此也可以看到Deployment资源的OwnerReference kubectl get replicasets nginx -o yaml 控制器模式 控制器模式指的是在一个控制循环中,通过控制器、被控系统及观测传感器这三个逻辑组件,驱使被控系统达到期望状态。 Kubernetes中的声明式API就是声明资源期望达到的状态(spec),通过控制模式不断地将当前状态(status)向期望状态逼近。这在资源扩容或故障恢复时,有明显的逻辑优势。在扩容时,声明式API理解的是当前状态与期望状态不一致,为了达到状态一致而触发扩容,而命令式API理解的是收到一条命令,该命令要求扩容机器。 这两种逻辑在出错后重试时,会产生巨大的差异。试想一下扩容成功了,但是扩容程序认为自己失败了,会怎么样?声明式API重试时,会观测到当前状态与期望状态其实已经一致了,从而停止扩容,就算这个时间错开,扩容了两次,在下个观测周期到来,传感器依然会发现状态不一致而触发缩容,这种设计下当前状态总是同期望状态一起变化的。 命令式API如果发生两次扩容,系统很难理解和避免这种错误,特别是期望状态不断变化时,程序必须非常小心的处理每一个可能发生错误的地方,加上大量的判断和回滚,这严重影响性能和故障恢复时间,而且也无法完全保证多次故障发生后,状态仍然与期望的一致。究其根源在于命令式API只有一次任务的过程,如果该过程发生错误,必须通过强一致的事务来保证回滚,然后再继续尝试。 Kubernetes Schedule ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:3:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"优先级 priorityclass和priority ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:4:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"污点 NoSchedule和NoExecute区别: NoSchedule:尽量不将Pod调度到该Node上,已在该Node上的Pod不受影响 NoExecute:除非Pod容忍该污点,否则不会调度到该Node上,已在该Node上没有容忍污点的Pod会被驱逐 spec: ... template: ... spec: tolerations: - effect: NoSchedule key: app-name operator: Equal value: aaa-job ... ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:5:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"亲和性 分为node亲和性、pod亲和性和pod反亲和性。 node亲和性——nodeAffinity:类似nodeSelector,限制pod尽量或必须调度到匹配的node上。 spec: ... template: metadata: labels: app: nginx-hts0000 spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - preference: matchExpressions: - key: kubernetes.io/hostname operator: In values: - cn-shenzhen.10.77.89.38 weight: 100 ... pod亲和性——podAffinity: pod反亲和性——podAntiAffinity: Kubernetes中的资源 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:6:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Deployment Deployment是对Replicaset的高级封装,Replicaset控制Pod的副本数量,Deployment负责控制使用哪个Replicaset。创建或修改Deployment就会新创建一个Replicaset,每个Replicaset都记录了管理的Pod的副本、容器等等信息。Deployment回滚就是回滚使用历史版本的Replicaset信息来创建Pod。 Deployment资源文件模板 apiVersion: app/v1 kind: Deployment # 声明资源元信息:名称、标签、注解、命名空间等 metadata: name: nginx-deployment labels: app: nginx # 声明资源期望的状态 spec: # 具体配置需要根据资源类型进行配置 # 期望Pod数量 replicas: 3 # Pod的选择器 selector: # 配置标签匹配 matchLabels: app: nginx # 每个Pod的模板 template: # Pod元信息 metadata: labels: app: nginx # Pod期望状态 spec: # Pod中的containers containers: - name: nginx image: nginx:1.18 ports: - containerPort: 80 查看Deployment资源的字段解释 # 查看Deployment资源 # READY:就绪Pod个数 # UP-TO-DATE:达到最新版本Pod个数 # AVAILABLE:可以Pod个数 kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE nginx-depoloyment 4/4 4 4 23h nginx-depoloyment-2 2/2 2 2 22h web 3/3 3 3 12d 更新Deployment资源Pod的镜像 # 使用kubectl工具更新镜像 # deployment.v1.apps:指定资源类型和资源组,可以忽略 # nginx-deployment:资源名称 # nginx=nginx:1.19.1:nginx指的是Pod中具体哪一个container,以及修改成什么镜像 kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.19.1 # 也可以修改资源的yaml文件,重新apply来修改 # 具体操作为打印资源的yaml格式配置到文件中,修改指定容器的镜像版本,重新apply kubectl get deployments nginx-deployment -o yaml \u003e deployment-update.yaml kubectl apply -f deployment-update.yaml # 其实每一次变更,Kubernetes都会保留历史的信息,方便回滚,默认会保留10次的变更历史 kubectl get deployments nginx-deployment -o yaml | grep -A revisionHistoryLimit # 因为Deployment是对Replicaset的高级封装,多次修改Deployment会保留多份Replicaset # 因为保存了历史信息,所以可以很快回退 # 回退时会设置当前版本的Replicaset副本数为0,增加回退版本Replicaset副本数,当然会控制两边达到一个平滑过渡。 回滚操作 # 查看历史版本 kubectl rollout history deployments.apps/nginx-deployment # 回退上一版本 kubectl rollout undo deployments.apps/nginx-deployment # 回退指定版本 kubectl rollout undo deployments.apps/nginx-deployment --to-revision=2 Deployment状态流转图 每一个资源都有其对应的当前状态(status)。 spec字段解释 MinReadySeconds:Deployment 会根据 Pod ready 来看 Pod 是否可用,但是如果我们设置了 MinReadySeconds 之后,比如设置为 30 秒,那 Deployment 就一定会等到 Pod ready 超过 30 秒之后才认为 Pod 是 available 的。Pod available 的前提条件是 Pod ready,但是 ready 的 Pod 不一定是 available 的,它一定要超过 MinReadySeconds 之后,才会判断为 available revisionHistoryLimit:保留历史 revision,即保留历史 ReplicaSet 的数量,默认值为 10 个。这里可以设置为一个或两个,如果回滚可能性比较大的话,可以设置数量超过 10 paused:paused 是标识,Deployment 只做数量维持,不做新的发布,这里在 Debug 场景可能会用到 progressDeadlineSeconds:前面提到当 Deployment 处于扩容或者发布状态时,它的 condition 会处于一个 processing 的状态,processing 可以设置一个超时时间。如果超过超时时间还处于 processing,那么 controller 将认为这个 Pod 会进入 failed 的状态 升级策略字段解析 MaxUnavailable:滚动过程中最多百分之几 Pod 不可用 MaxSurge:滚动过程中最多存在百分之几 Pod 超过预期 replicas 数量 Deployment发布策略 灰度发布:按百分比逐步升级,阶段性 滚动发布:持续完成升级,每次升级部分 蓝绿发布:分组升级 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:7:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Service Service用于为一组相同的资源提供统一的访问入口,比如一个Depolyment中有十个Pod副本,每个Pod提供的应用是一样的,希望对用户暴露一个统一的入口,Service就是用来干这个的。 Service配置文件中,使用selector.matchLabels来匹配一组Pod,为这组Pod提供一个统一入口。 Service通过type来配置类型,支持的类型及其功能: ClusterIP:默认值,将Service暴露在Service网段上,只能集群内部访问 NodePort:将Service暴露在Node网段上,可以通过Node IP来访问 LoadBalancer:外接云厂商产品来做负载均衡 配置文件模板 apiVersion: v1 kind: Service metadata: name: hello-world-service labels: app: hello-world spec: selector: matchLabels: # 根据标签选中所有匹配到的pod app: hello-world # clusterIp: 10.110.211.92 # 指定service网段的ip,不知道就从service网段中获取一个可用的 type: ClusterIP # Service的类型 ports: - protocol: TCP port: 8080 # 暴露到集群内部的端口 targetPort: 80 # 对应pod暴露的端口,从集群内部或外部访问的请求最终会请求这个端口 nodePort: 80 # 暴露到集群外部的端口 查看Service资源字段解释 kubectl describe services Name: demo-deployment Namespace: demo Labels: app=demo Annotations: \u003cnone\u003e Selector: app=demo Type: NodePort IP Family Policy: SingleStack IP Families: IPv4 IP: 10.110.211.92 IPs: 10.110.211.92 Port: \u003cunset\u003e 18080/TCP TargetPort: 18080/TCP NodePort: \u003cunset\u003e 32581/TCP # Endpoints:通过标签匹配到的所有Pod Endpoints: 10.244.169.162:18080,10.244.169.164:18080,10.244.36.77:18080 Session Affinity: None External Traffic Policy: Cluster Events: \u003cnone\u003e 值得一提的是,Service的type为NodePort时,无论访问哪台Node的这个端口,都能访问,但是查看开放的端口,又没有看到监听。其实这是Kubernetes的网络插件来管理的,通常情况下时直接修改iptables,在内核层面完成转发。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:8:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Job Job就是一个任务,对于资源文件指定的任务启动一个对应的Pod执行,如果该任务要求执行10次,而且并发执行,那么将会创建10个Pod并发来执行同一个任务。 配置文件模板 apiVersion: batch/v1 kind: Jon metadata: name: hello-world labels: type: bash # 描述Job spec: # 该Job需要执行多少次 completions: 10 # 并发执行的Pod数 parallelism: 2 # 重试次数 backoffLimit: 4 # 描述执行Job的Pod template: spec: # 有三种策略 # Never: # OnFailure: # Always: restartPolicy: Never conatiners: - name: hello-world image: busybox command: [\"sh\", \"-c\", \"echo hello world \u0026\u0026 sleep 60\"] 查看Job资源字段解释 # COMPLETIONS:一共几次任务,完成了几次 # DURATION:Pod中业务运行具体时长 # AGE: kubectl get jobs NAME COMPLETIONS DURATION AGE hello-world 1/1 80s 33m hello-world-parallelism 8/8 5m20s 6m44s 查看Job执行日志 kubectl logs \u003cpod-id\u003e ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:9:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"CronJob CronJob与Job类似,增加了定时执行功能,使用习惯和语法与Linux的crontab类似。 配置文件模板 apiVersion: batch/v1 kind: CronJob metadata: name: hello-world-cron labels: type: bash spec: schedule: \"*/1 * * * *\" # 超过指定时间Job未启动,就停止这个Job startingDeadlineSecond: 10 # 是否允许并行运行 concurrencyPolicy: Allow # 允许留存历史Job个数 successfulJobsHistoryLimit: 10 jobTemplate: spec: template: spec: # 有三种策略 # Never: # OnFailure: # Always: restartPolicy: OnFailure containers: - name: hello-world-cron image: busybox command: - /bin/sh - -c - echo hello world from CronJob; sleep 60 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:10:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"DaemonSet DaemonSet叫做守护进程控制器,主要作用如下: 保证每一个Node上面运行同一个Pod 新增或移除Node时能动态感知到并自动添加或删除 跟踪每一个Pod的状态,在Pod异常时尝试恢复 配置文件模板 apiVersion: apps/v1 kind: DaemonSet metadata: name: fluentd-elasticsearch namespace: kube-system labels: k8s-app: fluentd-logging spec: selector: matchLabels: name: fluentd-elasticsearch template: metadata: labels: name: fluentd-elasticsearch spec: tolerations: # 这些容忍度设置是为了让该守护进程集在控制平面节点上运行 # 如果你不希望自己的控制平面节点运行 Pod,可以删除它们 - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule - key: node-role.kubernetes.io/master operator: Exists effect: NoSchedule containers: - name: fluentd-elasticsearch image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2 resources: limits: memory: 200Mi requests: cpu: 100m memory: 200Mi volumeMounts: - name: varlog mountPath: /var/log terminationGracePeriodSeconds: 30 volumes: - name: varlog hostPath: path: /var/log ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:11:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Ingress ingress需要额外安装,通常安装ingress-nginx 配置文件模板 # 快速输出一个模板 kubectl create ingress test-ingress -n default --class=nginx --rule=test.domain.com/path1*=test1-svc:80 --rule=test.domain.com/path2*=test2-svc:80 --dry-run=client -o yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: creationTimestamp: null name: test-ingress namespace: default spec: ingressClassName: nginx rules: - host: test.domain.com http: paths: - backend: service: name: test1-svc port: number: 80 path: /path1 pathType: Prefix - backend: service: name: test2-svc port: number: 80 path: /path2 pathType: Prefix status: loadBalancer: {} 通过注解的方式,可以动态修改nginx ingress的配置,比如,加上注解:nginx.ingress.kubernetes.io/proxy-read-timeout: \"300s\",可以动态修改连接读超时的时间。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:12:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Ingress的访问控制 nginx ingress可以通过注解的方式来控制nginx的配置,因此我们可以使用xxx注解,来实现配置allow和deny。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:12:1","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Nginx Ingress配置跨域 需要加上如下注解: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress namespace: default annotations: nginx.ingress.kubernetes.io/enable-cors: \"true\" # 启用CORS。 nginx.ingress.kubernetes.io/cors-allow-origin: \"*\" # 允许所有域访问。 nginx.ingress.kubernetes.io/cors-allow-methods: \"GET, PUT, POST, DELETE, PATCH, OPTIONS\" # 允许的HTTP方法。 # 允许的自定义请求头。 nginx.ingress.kubernetes.io/cors-allow-headers: \"DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range\" nginx.ingress.kubernetes.io/cors-expose-headers: \"Content-Length,Content-Range\" # 暴露的响应头。 nginx.ingress.kubernetes.io/cors-max-age: \"86400\" # 预检请求缓存时间。 ... 坑 nginx.ingress.kubernetes.io/cors-allow-headers: *不意味着允许所有header,而是预设了一组默认的header,参考文档。 不在预设之外的header,仍然需要单独手动加上,不然依然会有跨域问题。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:12:2","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Network Policy 网络策略用于限制同集群内同namespace、同集群内跨namespace、跨集群的Pod之间的通信。 可视化的网络策略编辑器:https://editor.networkpolicy.io/?id=8izZAPuYL6eFiitL # networkpolicy1 # restricts all Pods in Namespace space1 to only have outgoing traffic to Pods in Namespace space2. # incoming traffic not affected. # and also allow outgoing dns traffic on port 53 TCP and UDP. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: np namespace: space1 spec: podSelector: {} policyTypes: - Egress egress: - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - port: 53 protocol: UDP - port: 53 protocol: TCP - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: space2 # networkpolicy2 # restricts all Pods in Namespace space2 to only have incoming traffic from Pods in Namespace space1. # outgoing traffic not affected. # and also allow outgoing dns traffic on port 53 TCP and UDP. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: np namespace: space2 spec: podSelector: {} ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: space1 egress: - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - port: 53 protocol: UDP - port: 53 protocol: TCP # 获取networkpolicy kubectl get networkpolicy --all-namespaces ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:13:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"RBAC ClusterRole/Role定义了一组权限,以及这组权限在哪些地方可用(整个集群或单个namespace)。 ClusterRoleBinding/RoleBinding将一组权限与Account联系起来,并定义它的应用位置(整个集群或单个namespace)。 # 创建sa kubectl create serviceaccount my-sa -n ns-1 kubectl create serviceaccount my-sa -n ns-2 # 创建clusterrolebinding kubectl create clusterrolebinding my-sa-view --clusterrole view --serviceaccount ns1:my-sa --serviceaccount ns2:my-sa # 查看clusterrolebinding kubectl get clusterrolebinding my-sa-view -o wide # 创建clusterrole kubectl create clusterrole my-clusterrole --verb=create,delete --resource deployments k -n ns1 create rolebinding pipeline-deployment-manager --clusterrole pipeline-deployment-manager --serviceaccount ns1:pipeline k -n ns2 create rolebinding pipeline-deployment-manager --clusterrole pipeline-deployment-manager --serviceaccount ns2:pipeline # 检查当前账号权限 kubectl auth can-i delete deployments --as system:serviceaccount:ns1:pipeline -n ns1 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:14:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"PriorityClass PriorityClass是Kubernetes中的一种资源,用于定义pod的优先级。PriorityClass可以被分配给pod,以确定它们在资源竞争中的优先级。 apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: level4 preemptionPolicy: PreemptLowerPriority value: 400000000 --- apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: important name: important namespace: lion spec: priorityClassName: level4 containers: - image: nginx:1.21.6-alpine name: important resources: requests: memory: 1Gi dnsPolicy: ClusterFirst restartPolicy: Always Kubernetes应用配置管理 首先看一下需求来源。大家应该都有过这样的经验,就是用一个容器镜像来启动一个container。要启动这个容器,其实有很多需要配套的问题待解决: 第一,比如说一些可变的配置。因为我们不可能把一些可变的配置写到镜像里面,当这个配置需要变化的时候,可能需要我们重新编译一次镜像,这个肯定是不能接受的; 第二就是一些敏感信息的存储和使用。比如说应用需要使用一些密码,或者用一些 token; 第三就是我们容器要访问集群自身。比如我要访问 kube-apiserver,那么本身就有一个身份认证的问题; 第四就是容器在节点上运行之后,它的资源需求; 第五个就是容器在节点上,它们是共享内核的,那么它的一个安全管控怎么办? 最后一点我们说一下容器启动之前的一个前置条件检验。比如说,一个容器启动之前,我可能要确认一下 DNS 服务是不是好用?又或者确认一下网络是不是联通的?那么这些其实就是一些前置的校验。 Kubernetes中对这些配置进行管理,有配套的工具和资源: 可变配置就用 ConfigMap; 敏感信息是用 Secret; 身份认证是用 ServiceAccount 这几个独立的资源来实现的; 资源配置是用 Resources; 安全管控是用 SecurityContext; 前置校验是用 InitContainers 这几个在 spec 里面加的字段,来实现的这些配置管理。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:15:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"ConfigMap 存储Pod的配置,用于实现Pod与配置的解耦。 配置文件模板 apiVersion: v1 kind: ConfigMap metadata: name: hello-world-config labels: hello: world # 具体的配置以key:value的方式存储 data: # 简单的标量类型key:value形式 MAX_CONNECTION: \"10\" FILE_NAME: hello_world player: hts0000 # 文件名和内容形式的key:value hello-world-config.json: | { \"name\": \"hts0000\", \"age\": 10, \"sex\": boy } # 类文件键 game.properties: | enemy.types=aliens,monsters player.maximum-lives=5 user-interface.properties: | color.good=purple color.bad=yellow allow.textmode=true 通过命令行创建 kubectl create configmap hello-config --from-literal=hello=world --from-literal=MaxConnection=10 使用ConfigMap 可以在Deployment等资源中使用ConfigMap,下面以Deployment为例,打印ConfigMap中定义的各项配置。 apiVersion: apps/v1 kind: Deployment metadata: name: hello-world-deployment labels: hello: world spec: replicas: 3 selector: matchLabels: hello: world template: metadata: labels: hello: world spec: volumes: # 将ConfigMap作为卷,Pod内就可以将这个卷挂载到容器内部 - name: hello-world-config-volumes configMap: name: hello-world-config # ConfigMap的名字 items: # 来自ConfigMap的一组键,将被创建为文件 - key: hello-world-config.json path: hello-world-config.json - key: game.properties path: game.properties - key: user-interface.properties path: user-interface.properties containers: - name: say-hello image: busybox env: # 从hello-world-config中获取配置,写入容器的环境变量 - name: MAX_CONNECTION # 这个名字可以和ConfigMap中不同 valueFrom: configMapKeyRef: # value的来源为ConfigMap name: hello-world-config key: MAX_CONNECTION - name: FILE_NAME valueFrom: configMapKeyRef: name: hello-world-config key: FILE_NAME - name: MY_NAME valueFrom: configMapKeyRef: name: hello-world-config key: player volumeMounts: # 将hello-world-config-volumes挂载到容器内部 - name: hello-world-config-volumes mountPath: \"/config\" readOnly: true command: # 打印上面加载进来的环境变量和文件 - /bin/sh - -c - echo hello world; echo MAX_CONNECTION $(MAX_CONNECTION); \\ echo FILE_NAME $(FILE_NAME); echo player $(MY_NAME); \\ echo '########################################'; \\ cat /config/hello-world-config.json; \\ echo '########################################'; \\ cat /config/game.properties; \\ echo '########################################'; \\ cat /config/user-interface.properties; \\ echo '########################################'; \\ sleep 10000000000 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:16:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Secret Secret是一个主要用来存储密码token等一些敏感信息的资源对象,里面的敏感信息是采用 base-64编码保存起来的。 配置文件模板 apiVersion: v1 kind: Secret metadata: name: mysecret namespace: default # 指定Secret的一个类型 # Opaque:用户定义的任意数据 # kubernetes.io/service-account-token:服务账号令牌 # kubernetes.io/dockercfg:~/.dockercfg文件的序列化形式 # kubernetes.io/dockerconfigjson:~/.docker/config.json文件的序列化形式 # kubernetes.io/basic-auth:用于基本身份认证的凭据 # kubernetes.io/ssh-auth:用于 SSH 身份认证的凭据 # kubernetes.io/tls:用于 TLS 客户端或者服务器端的数据 # bootstrap.kubernetes.io/token:启动引导令牌数据 type: Opaque data: aHRzMDAwMAo=: MTIzNDU2Cg== # 敏感信息经过base64加密后存储在Secret中 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:17:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"ServiceAccount ServiceAccount用于解决Pod在集群里面的身份认证问题,ServiceAccount会去关联一个底层的Secret,也就是说身份认证信息实际存储于Secret里面。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:18:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Resources Resources用于配置容器使用的资源,比如配置使用的CPU时间、内存大小、硬盘大小或GPU算力等,也支持自定义实现资源限制。 apiVersion: v1 kind: Pod metadata: name: memory-demo namespace: mem-example spec: containers: - name: memory-demo-ctr image: polinux/stress resources: requests: cpu: \"250m\" memory: \"100Mi\" limits: cpu: \"300m\" memory: \"200Mi\" command: [\"stress\"] args: [\"--vm\", \"1\", \"--vm-bytes\", \"150M\", \"--vm-hang\", \"1\"] ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:19:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"SecurityContext SecurityContext主要是用于限制容器的一个行为,它能保证系统和其他容器的安全。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:20:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"InitContainers InitContainer会在普通的Container之前启动,如果定义了多个InitContainer,那么他们会按定义顺序依次启动。InitContainer常用与为普通的Container进行初始化。 实践 本实践通过helm在Kubernetes上部署Prometheus,采集自定义的Demo应用的指标数据,并通过Grafana展示出来。 实践使用到了Kubernetes的Deployment、ConfigMap、服务发现、Service等一系列能力,结合Prometheus和Grafana实现监控系统的搭建。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:21:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"自定义Exporter 根据Prometheus提供的SDK,实现一个可以向外暴露Metrics指标的应用。 官方教程:https://prometheus.io/docs/guides/go-application/ 实现demo:https://github.com/hts0000/exporter-demo ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:22:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"打包成Docker镜像 使用以下Dockerfile文件将Demo打包成镜像。打包是使用多阶段编译的方式减小镜像的体积。 FROM golang:1.20.3-alpine AS builder RUN go env -w GO111MODULE=on RUN go env -w GOPROXY=https://goproxy.cn,direct COPY . /go/src/exporter-demo WORKDIR /go/src/exporter-demo RUN go build -o exporter-demo . FROM alpine COPY --from=builder /go/src/exporter-demo/exporter-demo /bin/exporter-demo EXPOSE 18080 ENTRYPOINT [ \"/bin/exporter-demo\" ] 执行docker build -t demo:v1 .命令进行打包。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:23:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"将Demo在K8S上运行起来 以Deployment的方式,运行三个Demo副本,同时使用ConfigMap定义副本将会使用到的配置。 demo-deployment.yaml文件配置 # 副本使用到的配置存储在demo-configmap这个ConfigMap里 apiVersion: v1 kind: ConfigMap metadata: name: demo-configmap namespace: demo data: ADDR: ':18080' --- # demo副本信息 apiVersion: apps/v1 kind: Deployment metadata: name: demo-deployment labels: app: demo namespace: demo spec: replicas: 3 selector: matchLabels: app: demo template: metadata: labels: app: demo annotation: desc: 'simple app, expose metric from :18080/metrics url' spec: containers: - name: demo image: demo:v1 ports: - containerPort: 18080 # 定义程序向外暴露的端口,需要跟ConfigMap中的配置对应 env: - name: ADDR # 程序会读取ADDR环境变量获取程序启动监听地址 valueFrom: configMapKeyRef: name: demo-configmap # 此处为ConfigMap的名称 key: ADDR 镜像仓库认证 如果镜像仓库配置了需要认证,可以配置一个Secret资源,记录认证的用户名和密码。 apiVersion: v1 kind: Secret metadata: name: mysecret namespace: default type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: \u003e- eyJhdXRocyI6eyJtaW5pc28tcmVnaXN0cnktdnBjLmNuLXNoZW56aGVuLmNyLmFsaXl1bmNzLmNvbSI6eyJ1c2VybmFtZSI6ImRvY2tlci1yb0BtaW5pc28xIiwicGFzc3dvcmQiOiJWM2liZEZ3a2xNMnlQMzF6IiwiYXV0aCI6IlpHOWphMlZ5TFhKdlFHMXBibWx6YnpFNlZqTnBZbVJHZDJ0c1RUSjVVRE14ZWc9PSJ9fX0= 然后在Deployment中加上imagePullSecrets配置项,引用Secret的配置,即可完成镜像仓库的认证。 apiVersion: apps/v1 kind: Deployment ... spec: ... spec: # 配置拉取使用的Secret imagePullSecrets: - name: miniso-repo-acr ... 执行kubectl apply -f demo-deployment.yaml命令,创建Deployment。 执行kubectl get deployments -n demo查看Deployments信息。-n选项指定命名空间,因为我们将demo-deployment创建在demo这个命名空间里的。 执行kubectl get pods -n demo查看Pods信息。 确保Pods已经正常运行。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:24:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"使用Helm安装Prometheus、Altermanager和Grafana Helm是Kubernetes的包管理工具,可以方便的在Kubernetes上部署软件。 如果我们要手动部署Prometheus到Kubernetes上可能需要如下步骤: 使用Prometheus官方镜像包甚至自行给Prometheus打包 根据产品特性,选择部署资源。比如单实例的部署为Pod,集群的部署为Deployment或DaemonSet等等 抽离其中可定制化的配置,比如监听地址、采集间隔、监控服务器信息等等,根据Kubernetes的最佳实践,这些配置最好写到ConfigMap中去 还需要考虑访问权限和安全性的问题,如果不希望其他用户访问Prometheus这个资源,还要单独配置ServiceAccount等 … 需要做的事情很多,而且要按照最佳实践来部署,有很高的学习成本。因此Helm就是来解放这一过程的,用户只需要一行命令,就可以安装官方提供的Chart(Helm里一个包称之为Chart),官方提供的无疑是最贴合最佳实践的,用户只需要简单的配置,即可在Kubernetes上启动一个实例,甚至一个集群。 Helm官方文档:https://helm.sh/zh/docs/ 需要注意的是,Helm与Kubernetes有版本支持的要求,不同版本的Helm支持的Kubernetes版本参考文档:https://helm.sh/zh/docs/topics/version_skew/。 首先安装Helm很简单,下载下来就是个二进制包,放到/usr/bin目录即可。下载地址:https://github.com/helm/helm/releases 前往Helm Charts Hub查找Prometheus Chart。 Charts Hub地址:https://artifacthub.io/。 一般来说里面的教程会提示增加一个仓库,但是这些仓库下载拉取镜像时,经常会失败,所以我们需要配置Helm的下载仓库,Helm支持配置多个仓库,下载的时候需要加上指定仓库的前缀。 # 添加仓库地址 helm repo add bitnami https://charts.bitnami.com/bitnami helm repo add stable http://mirror.azure.cn/kubernetes/charts helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts helm repo add incubator https://charts.helm.sh/incubator 执行helm repo list查看所有仓库。 我们在bitnami仓库和官方提供的prometheus-community仓库分别查找promethues,看看他们的版本差异。 可以看到两个仓库的版本是同步的。为了下载安装顺利,我们选择使用bitnami仓库。 Helm在下载安装Chart时,需要指定仓库作为前缀。 # 在bitnami/prometheus仓库中下载prometheus helm install prometheus bitnami/prometheus # 如法炮制下载grafana helm install grafana bitnami/grafana 下载完之后查看,altermanager也安装上了。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:25:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"暴露Grafana和Prometheus访问地址 默认情况下Grafana和Prometheus的Service Type为ClusterIP,只能通过Service网段访问,也就是只有集群内可访问。我们希望集群外也能访问,就需要将Service Type修改为NodePort,并给资源分配一个nodePort,即Node上的Port。 首先查看所有的Service。 执行kubectl edit services prometheus-server实时修改配置,将type修改为NodePort,增加nodePort配置。grafana也是一样的操作。 然后就可以通过Node IP来访问Grafana和Prometheus了。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:26:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"Prometheus访问K8S动态发现Pod 现在Prometheus默认只有Prometheus自己和Altermanager这两个采集源。我们增加上demo-deployment中的所有pod。但是pod ip是不固定的,如果pod重启或滚动升级,ip地址会变化。这时就需要将Prometheus配置为主动与K8S读取Pod配置。 Prometheus的配置存储在ConfigMap中,我们先查看所有ConfigMap。 执行kubectl edit configmap prometheus-server命令进行编辑。增加针对demo这个namespace的指标采集。 Prometheus启动后配置就固定了,需要重启,我们可以把prometheus这个pod删掉,让Kubernetes Deployment自动帮我们重启。重启之后就能看到三个demo副本的监控信息了。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:27:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"配置Grafana 在Grafana中配置Prometheus数据源,并展示指标的数据。 配置数据源时,地址就是Prometheus所在Node的地址,以及nodePort配置的端口。 添加一个Panel。 在Options和侧边栏里还可以配置指标和Panel的名称。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:28:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Kubernetes","运维","必知必会"],"content":"访问测试 使用ab工具访问这些Pod,就可以在Grafana中看到指标的变化了。需要注意执行ab命令的机器,必须是Kubernetes集群内的机器,ab访问的地址为Pod在集群内的地址,如果希望集群外访问,可以创建一个Services,通过向外暴露demo-deployment的统一入口。 ","date":"2023-06-12","objectID":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/:29:0","tags":["Kubernetes","运维"],"title":"Kubernetes必知必会(一)","uri":"/kubernetes%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E4%B8%80/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com Prometheus介绍 开源的监控和告警工具,2016年加入CNCF,是继Kubernetes之后第二个CNCF托管的项目。 Prometheus收集和存储称为Metrics(指标)的时序数据,Metrics由记录时的时间戳和可选的key-value pair(称之为labels)组成。 Prometheus主要特性: metrics Name和key-value pair实现的多维数据时间序列 使用PromQL语言来查询这个多维时间序列 不依赖分布式存储 通过HTTP实现拉取(pull)时序数据 通过中间网关实现推送(push)时序数据 通过服务发现或静态配置获取目标端 支持丰富的制图方式,自带数据看板 Prometheus架构图: Metrics介绍 Metrics由两部分组成,Metrics名称和用大括号包围的多个key-value pair组成的多维数据,也叫labels。 Metrics格式:\u003cmetric name\u003e{\u003clabel name\u003e=\u003clabel value\u003e, ...} 比如想存储应用各个接口调用的次数,可以使用以下命名和labels,表示请求的方法和路由。 api_http_requests_total{method=\"POST\", handler=\"/messages\"} ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:0:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"四种指标类型 Counter:只增不减的计数器,常用于存储HTTP请求总数/PV/UV等单调递增的指标 Gauge:可增可减的仪表盘,侧重于反应系统的当前状态,常用于存储可用内存/可用连接数等 Histogram和Summary:主用用于统计和分析样本的分布情况 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:1:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"Counter Counter计数器在重启时会重置,但是Prometheus假设这个值是递增的,因此会拿到上一次落盘时存储的值,在这个值的基础上累加。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:1:1","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"Gauge ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:1:2","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"Histogram ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:1:3","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"Summary PromQL介绍 PromQL是Prometheus内置的查询语言,用来选择、查询、计算、聚合时间序列数据,Prometheus的可视化和可视化都是基于PromQL来实现的。 PromQL是一种嵌套的函数式语言,要查找的数据由多层嵌套的表达式组成: histogram_quantile( # 查询的根,最终结果表示一个近似分位数。 0.9, # histogram_quantile() 的第一个参数,分位数的目标值 # histogram_quantile() 的第二个参数,聚合的直方图 sum by(le, method, path) ( # sum() 的参数,直方图过去5分钟每秒增量。 rate( # rate() 的参数,过去5分钟的原始直方图序列 demo_api_request_duration_seconds_bucket{job=\"demo\"}[5m] ) ) ) 每个表达式执行的结果只会是以下四种类型中的一种: 字符串(string) 标量(scalar) 瞬时向量(instant vector) 区间向量(range vector) 其中瞬时向量,是一组时间序列中每条时间序列的最新的值,执行结果为每一条时间序列对应单个值。 下图展示的结果为promhttp_metric_handler_requests_total{code=\"200\"}这组有两条时间序列,它们分别对应一个值。 而区间向量,则是一组时间序列中每条时间序列的最新时间区间内的值,执行结果为每一条时间序列区域时间内采样的所有值,使用[\u003ctime\u003e]指定时间区间,\u003ctime\u003e支持y/w/d/h/m/s/ms组合使用。 下图展示promhttp_metric_handler_requests_total{code=\"200\"}[1m30s]这组有两条时间序列,它们在[1m30s]这个区间内分别有六个值。区间内具体有多少值跟采样间隔有关,本文配置采样间隔为15s。 瞬时向量和区间向量,默认情况下都是从最近时间点开始取值,使用offset \u003ctime\u003e可以让取值时间偏移\u003ctime\u003e。这通常用于当前状态与历史状态做对比。 可以通过指定指标名称来选择时间序列,也可以组合指标名称和标签,或使用正则,来定位符合条件的时间序列。下面通过组合指标名和标签,查询localhost:9104的最大连接数。 除了相等匹配(=)之外,Prometheus还支持其他匹配方式: !=:匹配不相等 =~:正则匹配相等 !~:正则匹配不相等 如果相对指标名做正则匹配,可以使用内置__name__的特性标签,下面查询过滤所有以mysql_global_status开头的指标。 {__name__=~\"mysql_global_status.*\"} 所有指标都有instance和job这两个标签,可以通过下面两种方法拿到所有的时间序列。 {job!=\"\"} {__name__=~\".+\"} PromQL有很多内置函数,以下是一些常见函数的介绍: count(\u003cexpression\u003e):统计符合表达式的metric的数量 rate(\u003cexpression\u003e):计算区间变化率,选择区间内最后一个和第一个点 irate(\u003cexpression\u003e):计算区间变化率,选择区间内最后两个点 delta(\u003cexpression\u003e):计算区间差值,选择区间内最后一个和第一个点 predict_linear(\u003cexpression\u003e):通过线性回归进行预测 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:1:4","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"rate() rate()是Prometheus中非常重要的函数,它用于计算指标在时间区间内的变化率。Counter类型指标会不断地增长,有时我们可能不关心它增长到多少,而关心它在某段时间内增长的速率,rate()就是为此准备的。 rate()接收一个区间向量,返回一个瞬时向量,返回这段区间内每秒平均变化率,公式为:(区间内最后一个采样点的值 - 区间内第一个采样点的值) / (区间内最后一个采样时间点 - 区间内第一个采样时间点)。 假设想查询Prometheus Metric页面访问成功总数在一分钟内的变化率,如下查询在Prometheus Table中得到一个瞬时向量0.06666518521810627。 rate(promhttp_metric_handler_requests_total{code=\"200\", instance=\"localhost:9090\"}[1m]) 把区间向量打印出来,手动计算一下是否与rate()公式计算的结果一致。 Prometheus设置的采样间隔时间为15s,因此[1m]区间内有四个瞬时向量,代入公式计算一下:(10363 - 10360) / (1686106626.286 - 1686106581.286) = 0.0666666666666667,符合rate()计算结果。 使用Prometheus Graph绘制一段时间内每个采样点的变化率。下图中指定查询了30m内,每个采样点的变化率,相当于将30m内的每个采样点都丢给rate(promhttp_metric_handler_requests_total{code=\"200\", instance=\"localhost:9090\"}[1m])表达式进行执行,将得到的一连串结果连线绘制图形。 其实也可以手动获取这一连串的结果,通过offset来指定获取之前时间的值,如下所示,取满30分钟或者说1800秒,如果说采样间隔是15s的话,那么30m内最多可以计算出120个瞬时向量,将这120个瞬时向量连起来,就得到Prometheus Graph展示的图形了。当然了,实际Prometheus并不会这样做,而是通过其他更加高效的方法来得到Prometheus Graph展示的图形。 rate(promhttp_metric_handler_requests_total{code=\"200\", instance=\"localhost:9090\"}[1m] offset) rate(promhttp_metric_handler_requests_total{code=\"200\", instance=\"localhost:9090\"}[1m] offset 15s) rate(promhttp_metric_handler_requests_total{code=\"200\", instance=\"localhost:9090\"}[1m] offset 30s) ... rate(promhttp_metric_handler_requests_total{code=\"200\", instance=\"localhost:9090\"}[1m] offset 1800s) rate()计算的结果是与区间选择高度相关的,如果选择的时间区间太短,小于采样间隔,那么区间内将没有两个采样点用于计算,则无法的到计算结果。通常来说区间的选择为采样间隔的四倍。比如采样间隔15s,则时间区间选择为1m。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:2:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"irate() irate()与rate()不同点在于它选取的是时间区间内最后两个点。因为是相邻的两个点,所以irate()更加灵敏,能更好的反应两个点之间的瞬时变化,也就是说irate()得到的图像更加尖锐。注意一下,时间区间与函数没啥关系。时间区间就像一个窗口,框选了一个范围,得到一系列的瞬时向量,而选择哪些向量来使用,是各个函数的事情。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"increase() increase()使用方法和呈现图像都与rate()相似,区别在于Y轴数据是数值而非百分比。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:4:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"delta() rate()会假定传入的指标类型都是Counter类型的。如果一个函数假定传入的指标是Counter类型的,但是数据并不是单调递增,出现了数据值减小的情况,那么这类函数会认为是抓取指标的进程出问题(比如宕机导致该Counter值被重置了),而进行自动调整。比如时间序列的值为[5,10,4,6],则将其视为[5,10,14,16]。这就导致对于Gauge这种数据可能会增加或减少的指标而言,rate()是无法正确处理的。 也就是说,其实各个函数需要指定正确的数据类型的指标,才能正确工作。 delta()函数可以正确的处理Gauge类型的指标。delta()计算时间区间内最后一个采样点和第一个采样点的差值,这个差值是可以为负数的。 下图查询go语言堆上对象的数量。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:5:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"predict_linear() predict_linear()根据一个区间向量来预测未来该值的变化情况,这种预测是线性的,如果数据是非线性的,那么预测的值可能毫无意义。 线性数据的典型例子是硬盘空间的使用情况,因为硬盘使用空间大多数情况下都是线性上升的,那么就可以通过一元线性回归方程来预测合适达到阈值,从而提前告警。 线性预测一般来说是通过历史数据的分布情况,通过梯度下降算法来拟合出一条直线,该直线可对应一个方程,将未来的时间戳输入该方程,即可得到预测的使用空间。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:6:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"常用语句 字符串正则匹配:=~ 目标端发现 Exporter Exporter是应用与Prometheus之间的\"适配器\"。有了Exporter应用就无需在业务代码中实现指标采集的逻辑,而是由Exporter来采集并将其修改为Metric的格式上传给Prometheus。 Exporter是一个开放的标准接口,任何应用的指标都可以通过适配一个Exporter来接入Prometheus。 一些常见的Exporter: node_exporter:实现Linux主机的指标采集 mysqld_exporter:实现MySQL的指标采集,支持Linux和Windows blackbox_exporte`:支持TCP/UDP/HTTP等网络探针的方式来探测网站,收集各个指标,如访问时间等 windows_exporter:实现Windows主机的指标采集 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:7:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"自定义Exporter Prometheus采集和存储的指标,都是Metrics类型的,因此应用想要接入Prometheus有两种方法,一种是云原生,也就是在应用设计实现之处就考虑使用k8s及其相关生态的能力,比如拆分成微服务,使用k8s的水平扩展弹性扩缩容能力,而监控方面使用k8s生态的Prometheus,采集应用本身的指标为Metrics类型并暴露接口给Promethues采集。第二种方法是将自定义指标转换成Metrics类型的指标,适合应用实现时未考虑使用现在希望使用Prometheus的应用,这种方法可以不入侵业务代码,通过增加一层数据转换层的方式实现指标采集。 下面以阿里云的账单数据为例,写一个Exporter通过阿里云API获取账单信息,再转换成Metrics类型的指标,暴露接口给Prometheus采集,最后通过Grafana进行展示。 最后期望实现如下效果: Exporter采集阿里云账单数据 Exporter将账单数据区分出各个产品使用金额,各个产品购买数量,各个项目使用的产品金额,每个月使用总金额的变化,暴露指标,方便后续聚合和计算 定制Grafana模板,对数据进行聚合和计算,比如费用变化的折线图,各个产品线使用金额的饼图等等 每个月或每个季度将各个产品线使用的金额通过邮件或钉钉或微信等方式,发送给具体的业务负责人 结合Altermanager对即将超出预算的产品线发出告警 Grafana介绍 Alertmanager Altermanager是一个独立的组件,负责接收来自Prometheus的告警信息,然后对这些告警信息进行统一管理。Altermanager支持多种功能,核心功能如下: 告警路由 告警静默 告警抑制 邮件通知 告警分组 Webhook 高可用 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:8:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"分组(Grouping) 根据标签名来分组,多个相同的告警只会触发一次通知。比如一个集群的多个副本,因为数据库宕机了同时报数据库连接错误的告警,那么只需要知道一次即可。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:9:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"抑制(Inhibition) 匹配到某个critical的标签时,抑制其他标签的告警。常用于关键节点宕机,而引发的告警风暴,只需要处理critical标签的告警即可,其他都是因为这个告警而引发的。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:10:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"静默(Silences) 直接给某些标签配置一个静默时间,在静默时间内不会收到这些标签的告警信息。静默是在web页面上配置的,属于实时调整告警的一种手段。比如某些已知问题正在修复的,可以手动静默这些告警。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:11:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"告警触发流程 Prometheus根据配置文件的evaluation_interval计算间隔对rule文件中的告警规则进行计算 如果expr规则表达式条件满足会发送status=pending的告警信息,表示告警静默中。如果条件满足且持续了for设置的时长,会发送status=firing的告警信息,表示告警触发。如果条件满足,但未持续for设置的时长,发送status=inactive的告警信息,表示告警已恢复。 所有类型的告警都会推送至Prometheus配置文件中配置一个或多个的alertmanagers Altermanager收到告警根据告警信息状态、抑制器和静默配置选择发送、抑制或静默 需要发送告警信息的将会根据group_by分组,根据route进行标签匹配,将告警路由到指定receiver receiver根据配置决定发送至邮件还是webhook 最后再根据send_resolved配置决定告警恢复后是否发送恢复信息 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:12:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"配置文件 文档地址:https://prometheus.io/docs/alerting/latest/configuration/ global: smtp_smarthost: 'smtp.sina.com:25' smtp_from: 'hts_0000@sina.com' smtp_auth_username: 'hts_0000@sina.com' smtp_auth_password: 'c0106ffddcafca6e' # 使用网易邮箱的授权码 smtp_hello: 'sina.com' smtp_require_tls: false wechat_api_url: \"https://qyapi.weixin.qq.com/cgi-bin/\" wechat_api_secret: \u003csecret\u003e wechat_api_corp_id: \u003cstring\u003e # Files from which custom notification template definitions are read. # The last component may use a wildcard matcher, e.g. 'templates/*.tmpl'. templates: # The root node of the routing tree. route: # 这里的标签列表是接收到报警信息后的重新分组标签,例如,接收到的报警信息里面有许多具有 cluster=A 和 alertname=LatncyHigh 这样的标签的报警信息将会批量被聚合到一个分组里面 group_by: ['alertname', 'cluster'] # 当一个新的报警分组被创建后,需要等待至少 group_wait 时间来初始化通知,这种方式可以确保您能有足够的时间为同一分组来获取多个警报,然后一起触发这个报警信息。 group_wait: 30s # 相同的group之间发送告警通知的时间间隔 group_interval: 5m # 如果一个报警信息已经发送成功了,等待 repeat_interval 时间来重新发送他们,不同类型告警发送频率需要具体配置 repeat_interval: 1h # 默认的receiver:如果一个报警没有被一个route匹配,则发送给默认的接收器 receiver: 'sina.email' # 上面所有的属性都由所有子路由继承,并且可以在每个子路由上进行覆盖。 routes: - receiver: 'frontend-pager' matchers: - severity=\"critical\" - receiver: sina.email group_wait: 10s match: team: node # All alerts with the team=frontend label match this sub-route. # They are grouped by product and environment rather than cluster # and alertname. - receiver: 'frontend-pager' group_by: [product, environment] matchers: - team=\"frontend\" # All alerts with the service=inhouse-service label match this sub-route. # the route will be muted during offhours and holidays time intervals. # even if it matches, it will continue to the next sub-route - receiver: 'dev-pager' matchers: - service=\"inhouse-service\" mute_time_intervals: # 指定时间期间不接受告警信息 - offhours - holidays continue: true # All alerts with the service=inhouse-service label match this sub-route # the route will be active only during offhours and holidays time intervals. - receiver: 'on-call-pager' matchers: - service=\"inhouse-service\" - foo=~\"foo\" active_time_intervals: # oncall 放假也得上班! - offhours - holidays # A list of notification receivers. receivers: - name: 'web.hook' webhook_configs: - url: 'http://127.0.0.1:5001/' - name: 'sina.email' email_configs: - to: 'hts_0000@sina.com' send_resolved: true # 接受告警恢复的通知 - name: 'frontend-pager' email_configs: - to: 'hts_0000@sina.com' send_resolved: true # 接受告警恢复的通知 - name: 'on-call-pager' email_configs: - to: 'hts_0000@sina.com' send_resolved: true # 接受告警恢复的通知 - name: 'dev-pager' email_configs: - to: 'hts_0000@sina.com' send_resolved: true # 接受告警恢复的通知 # - name: 'wechat' # wechat_configs: # - send_resolved: false # Whether to notify about resolved alerts. # api_secret: global.wechat_api_secret # The API key to use when talking to the WeChat API. # api_url: global.wechat_api_url # The WeChat API URL. # corp_id: global.wechat_api_corp_id # The corp id for authentication. # message: '{{ template \"wechat.default.message\" . }}' # API request data as defined by the WeChat API. # message_type: 'text' # Type of the message type, supported values are `text` and `markdown`. # agent_id: '{{ template \"wechat.default.agent_id\" . }}' # to_user: '{{ template \"wechat.default.to_user\" . }}' # to_party: '{{ template \"wechat.default.to_party\" . }}' # to_tag: '{{ template \"wechat.default.to_tag\" . }}' # A list of inhibition rules. inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' equal: ['alertname', 'dev', 'instance'] # A list of time intervals for muting/activating routes. time_intervals: - name: 'offhours' # 停机时间 time_intervals: - times: - start_time: '02:00' end_time: '05:00' weekdays: - 'monday:friday' # 工作日 - 'sunday' # 周日 days_of_month: - '5' # 每个月5号 - '-5:-1' # 每个月最后5天 months: - '1:12' # 1~12月 years: - '2023:3202' # 2023~3202年 # location: 'Asia/Shanghai' # 指定时区 - name: 'holidays' # 假期时间 time_intervals: - months: - '1:2' # 春节 # 其他的不指定则匹配所有,也就是这个时间为每年1到2月的每天每时每刻 # times: # - start_time: 02:00 # end_time: 05:00 # weekdays: # - 'monday:friday' # 工作日 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:13:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"下载Prometheus 下载地址:https://prometheus.io/download/ ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:14:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"配置Prometheus prometheus配置文件为:prometheus.yml。初始的配置文件如下所示。 # my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. scrape_timeout: 15s # scrape_timeout is set to the global default (10s). # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: # - alertmanager:9093 # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. rule_files: # - \"first_rules.yml\" # - \"second_rules.yml\" # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=\u003cjob_name\u003e` to any timeseries scraped from this config. - job_name: \"prometheus\" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: [\"localhost:9090\"] 重点关注配置文件中global和scrape_configs这两个block。 global block中配置含义如下: scrape_interval:抓取数据间隔 evaluation_interval:告警间隔 scrape_configs block中配置含义如下: job_name:目标端的自定义名称 static_configs:指示连接目标端使用静态配置的方式,Prometheus还支持目标发现 targets:抓取metric默认采用的是http pull的方式,需要指定ip和端口,默认将从/metrics路由拉取metric格式的数据 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:15:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"启动Prometheus 启动命令:prometheus --config.file=prometheus.yml。 在prometheus.yml配置文件中,scrape_configs block默认配置了prometheus本身的监控。prometheus通过localhost:9090/metrics地址向外暴露自身的metric格式的指标数据,prometheus server则通过这个接口采集并存储相关数据。如果我们直接访问这个接口,将得到如图所示的metric数据。 在众多metrics中有一个名为promhttp_metric_handler_requests_total的metric,记录了prometheus的/metrics接口各个状态的访问次数。 也可以访问http://localhost:9090/graph,通过可视化的方式展示这个时序数据。 promhttp_metric_handler_requests_total这个metric只有code一个维度,它包含了200/500/503这3个值,使用PromQL语言可以查询灵活的查询指定值。 使用promhttp_metric_handler_requests_total{code=\"200\"}来查询接口状态正常的时序数据。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:16:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"安装MySQL Exporter 下载地址:https://prometheus.io/download/ 启动MySQL Exporter前需要给Exporter配置一个只读账户。 CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'XXXXXXXX' WITH MAX_USER_CONNECTIONS 3; GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost'; MySQL Exporter启动需要指定.cnf类型的配置文件读取用户名和密码,可以将配置写在MySQL的配置文件my.cnf中,也可以单独写一个文件。需要添加的配置如下。 [client] user = exporter password = XXXXXXXX 启动MySQL Exporter:mysqld_exporter --config.my-cnf=\"my.cnf\" --web.listen-address=\":9104\"。 然后就可以通过访问http://localhost:9104/metrics来获取MySQL的metric了。 一些重要的metric: mysql_global_variables_max_connections:允许最大连接数 mysql_global_status_threads_connected:当前连接数 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:17:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"配置Prometheus拉取MySQL Exporter的metric 在Prometheus配置文件prometheus.yml的scrape_configs中添加一个job: scrape_configs: # The job name is added as a label `job=\u003cjob_name\u003e` to any timeseries scraped from this config. - job_name: \"prometheus\" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: [\"localhost:9090\"] - job_name: \"mysql\" static_configs: - targets: [\"localhost:9104\"] 重启Prometheus,在Graph页面就可以查询到MySQL Exporter采集的metric了。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:18:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"安装Grafana 下载地址:https://grafana.com/grafana/download ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:19:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"启动Grafana Grafana启动后默认运行在:3000端口,默认登录用户名和密码为:admin/admin。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:20:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"为Grafana添加Prometheus数据源 Grafana需要从数据源中读取并展示数据,Grafana支持多种数据源。 点击Data sources来进入添加数据源界面。 选择Prometheus添加数据源,配置其地址和端口,在底部选择Save \u0026 test保存。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:21:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"配置DashBoard DashBoard是一整个页面,里面可以有多个不同类型的Panel(看板)。Panel支持任意拖拽和改变大小,存在多个看板是非常方便修改界面。鼠标在看板顶部可以拖拽,在右下角可以伸缩大小。 下面来为DashBoard创建几个Panel。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:22:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"时间序列看板 回到首页,点击右上角的Add按钮,为这个DashBoard配置监控看板。 选择监控看板的数据源,监控看板展示通过PromQL查询出的metric数据。下面配置将以Time series的方式展示promhttp_metric_handler_requests_total指标的数据。 在这个DashBoard页面就能看到刚刚配置的时间序列看板。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:22:1","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"导入导出DashBoard 如果监控指标很多,需要配置很多看板,如果有多套Grafana还需重复配置,很麻烦。好消息是我们可以导出配置,并在其他Grafana中导入配置。 也可以导入配置,像一些常用的如主机监控、MySQL监控、网络监控等等,都有通用的模板可以直接导入使用,而各个云厂商也会为云主机等云产品定制模板,方便使用。 模板下载地址:https://grafana.com/grafana/dashboards/ 在DashBoards中导入MySQL的监控模板。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:23:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"配置告警 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:24:0","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"安装Altermanager 下载地址:https://prometheus.io/download/ ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:24:1","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"配置Altermanager 配置文件为:altermanager.yml,配置示例如下: global: smtp_smarthost: 'smtp.sina.com:25' smtp_from: 'hts_0000@sina.com' smtp_auth_username: 'hts_0000@sina.com' smtp_auth_password: 'xxxxxxxxxx' # 使用邮箱的授权码 smtp_hello: 'sina.com' smtp_require_tls: false route: # 这里的标签列表是接收到报警信息后的重新分组标签, # 例如,接收到的报警信息里面有许多具有 cluster=A 和 alertname=LatncyHigh 这样的标签的报警信息将会批量被聚合到一个分组里面 group_by: ['alertname', 'cluster'] # 当一个新的报警分组被创建后,需要等待至少 group_wait 时间来初始化通知, # 这种方式可以确保您能有足够的时间为同一分组来获取多个警报,然后一起触发这个报警信息。 group_wait: 30s # 相同的group之间发送告警通知的时间间隔 group_interval: 5m # 如果一个报警信息已经发送成功了,等待 repeat_interval 时间来重新发送他们, # 不同类型告警发送频率需要具体配置 repeat_interval: 1h # 默认的receiver:如果一个报警没有被一个route匹配,则发送给默认的接收器 receiver: 'web.hook' # 上面所有的属性都由所有子路由继承,并且可以在每个子路由上进行覆盖。 routes: - receiver: sina.email group_wait: 10s match: team: node receivers: - name: 'web.hook' webhook_configs: - url: 'http://127.0.0.1:5001/' - name: 'sina.email' email_configs: - to: 'hts_0000@sina.com' send_resolved: true # 接受告警恢复的通知 inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' equal: ['alertname', 'dev', 'instance'] 在Altermanager上面配置了告警路由、告警分组、静默时间、恢复通知、接收者——邮箱信息、接收者——webhook信息等等信息。这些配置指示Altermanager在收到来自Prometheus的告警信息后,如何对告警进行控制。而控制何时告警是在Prometheus中配置的。 在Prometheus的配置文件的增加Altermanager和告警规则的相关配置。 # my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. # 配置告警规则计算间隔 evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. scrape_timeout: 15s # scrape_timeout is set to the global default (10s). # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: # 配置访问Altermanager的地址 - localhost:9093 # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. rule_files: # - \"first_rules.yml\" # - \"second_rules.yml\" # 配置告警规则文件路径 - \"rule.yml\" # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=\u003cjob_name\u003e` to any timeseries scraped from this config. - job_name: \"prometheus\" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: [\"localhost:9090\"] - job_name: \"mysql\" static_configs: - targets: [\"localhost:9104\"] - job_name: \"windows\" static_configs: - targets: [\"localhost:9182\"] 而rule.yml文件里面配置告警规则及触发告警时应该通知那个Altermanager。 groups: - name: example rules: # Alert for any instance that is unreachable for \u003e10s. - alert: InstanceDown expr: up == 0 for: 10s labels: project: demo1 environment: production annotations: summary: \"Instance {{ $labels.instance }} down\" description: \"{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes.\" # Alert for any instance that has a median request latency \u003e1s. - alert: APIHighRequestLatency expr: api_http_request_latencies_second{quantile=\"0.5\"} \u003e 1 for: 10m annotations: summary: \"High request latency on {{ $labels.instance }}\" description: \"{{ $labels.instance }} has a median request latency above 1s (current value: {{ $value }}s)\" ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:24:2","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"邮件告警 把Mysql Exporter停掉模拟宕机,规则成立时告警信息状态为PENDING,还未触发告警。 当持续10s后,告警信息状态变为FIRING,Altermanager收到告警信息,并发送告警信息到指定邮箱。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:24:3","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Prometheus","监控","运维","必知必会"],"content":"钉钉机器人告警 使用webhook功能,可以将告警信息发送到顶顶机器人,顶顶机器人再通知到具体人或群。 创建机器人。创建时安全设置选择加签选项,记录生成的密钥和生成的url,还需要把url中带上的token取出来。 下载钉钉和企微webhook插件:https://github.com/cnych/promoter 这种插件没有官方的,该插件的文档:https://p8s.io/docs/alertmanager/receiver/ 插件的文档: global: prometheus_url: http://localhost:9090 wechat_api_secret: \u003csecret\u003e wechat_api_corp_id: \u003csecret\u003e # 配置这两块,token从url中提取一下,secret就是密钥 dingtalk_api_token: 0d45e829fb1a90f9ec4123bed7f542627dc967b98898a314d08f37b7ae6a7ee1 dingtalk_api_secret: SECb40c229b6d4220c640aac8052d0f900e7585e4a4882d87a398abc6739f41db72 # 云存储的配置信息,用来存储生成的图片,s3是亚马逊云,但是也支持阿里云 s3: access_key: LTAI5t9qFmk1LPxxNU6KrAiy secret_key: NdRxYjVpSW1LbscdW68J4FbkH0PcHz endpoint: oss-cn-guangzhou.aliyuncs.com region: cn-guangzhou bucket: sdlifudfh receivers: # 此处的名字要记住,在Prometheus中要用到 - name: rcv1 # wechat_configs: # - agent_id: \u003cagent_id\u003e # to_user: \"@all\" # message_type: markdown # message: '{{ template \"wechat.default.message\" . }}' dingtalk_configs: - message_type: markdown markdown: title: '{{ template \"dingtalk.default.title\" . }}' text: '{{ template \"dingtalk.default.content\" . }}' at: atMobiles: [ \"123456\" ] isAtAll: false 在Prometheus中加上webhook的配置。 receivers: - name: 'web.hook' webhook_configs: # 这里的rcv1就是上面配置的名字 - url: 'http://localhost:8080/rcv1/send' send_resolved: true 上面为啥要这样配置,是根据插件来的,也可以自己写一个插件。功能简单来说就是监听等待Prometheus的消息推送,然后再根据配置的钉钉密钥/token等信息,将告警信息转发给钉钉。自己处理的好处在于可以定制告警的内容。 关掉Mysql Exporter触发告警看看钉钉上能否收到。 能显示图片,一个原因是用了云的对象存储,可以实时预览图片。插件定制了转发的内容为markdown格式,钉钉也支持markdown格式的内容。插件调用Prometheus的接口生成告警时的监控图,再将图片上传到对象存储,然后将对象存储生成的文件填入模板中,发送给钉钉,最终呈现的效果就是这样。 ","date":"2023-06-05","objectID":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:24:4","tags":["Prometheus","监控","运维"],"title":"Prometheus必知必会","uri":"/prometheus%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 数位统计DP 数位DP强调分情况讨论 ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:0:0","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"计数问题 package main import ( \"bufio\" \"fmt\" \"os\" ) var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) ) func power10(x int) int { ans := 1 for x \u003e 0 { ans *= 10 x-- } return ans } // 返回 1~n 中 x 出现次数 func count(n, x int) int { ans := 0 cnt := 0 // 统计 n 有多少位 for m := n; m \u003e 0; m /= 10 { cnt++ } // 从右往左枚举每一位上的 x 总数 for i := 1; i \u003c= cnt; i++ { // 我们计算第四位上 x 出现的次数 // 假设 n = abcdefg,i = 4 指向 d 这一位 // 情况1:高三位为 000~abc-1 // d 右边可以取到 000~999 共 power10(i - 1) 个数 r := power10(i - 1) // d 左边可以取到 000 ~abc-1 共 abc 种情况 l := n / (r * 10) // abc = n / power10(i) = n / (r * 10) // 当 x == 0 时,则为 001~abc-1 种情况 if x \u003e 0 { ans += l * r } else { ans += (l - 1) * r } // n / r = abcd, abcd % 10 = d d := (n / r) % 10 // 高三位等于 abc 的情况,只需要考虑 d \u003e= x,因为 d \u003c x 就不符合条件 // 前四位 abcd 均相同,后三位可取 0~efg 共 efg + 1 种情况 if d == x { ans += n % r + 1 // 此时后三位可取 000~999 共 power10(i - 1) 种情况 } else if d \u003e x { ans += r } } return ans } func main() { defer out.Flush() for { var a, b int fmt.Fscan(in, \u0026a, \u0026b) if a == 0 \u0026\u0026 b == 0 { break } if a \u003e b { a, b = b, a } for i := 0; i \u003c 10; i++ { fmt.Fprintf(out, \"%d \", count(b, i) - count(a - 1, i)) } fmt.Fprintln(out) } } ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:1:0","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"经典模板题 338. 计数问题 233. 数字 1 的个数 状态压缩DP 状态压缩就是把一个集合的状态,压缩成一个数,用这个数的二进制表示集合的每一种情况。再使用位运算、逻辑运算等操作方便的计算状态。 ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:1:1","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"蒙德里安的梦想 package main import ( \"bufio\" \"fmt\" \"os\" ) const ( N = 12 // 以便计算最后一列 d[m][0] M = 1\u003c\u003cN ) var reader = bufio.NewReader(os.Stdin) var writer = bufio.NewWriter(os.Stdout) var st [M]bool // 字典表,存储所有位置的合法情况 func initStatus(n int){ for i:=0;i\u003c1\u003c\u003cn;i++{ cnt:=0 st[i] = true for j:=0;j\u003cn;j++{ // 遇到1,被占, 且统计的空格数为奇数,无法填充竖着的长方形 if i\u003e\u003ej \u0026 1 \u003e 0 { if cnt\u00261\u003e0{ st[i] = false break } cnt=0 continue } cnt++ } // 判断剩下的统计数是否奇数 if st[i] \u0026\u0026 cnt\u00261\u003e0{ st[i] = false } } } // - **数位统计DP**: 所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩 // - 计数问题: 蒙德里安的梦想, 求把N*M的棋盘分割成若干个1*2的的长方形,有多少种方案。 // - 核心: 找到所有横放 1 X 2 小方格的方案数,因为所有横放确定了,那么竖放方案是唯一的。 // - 状态定义: // - 集合: 用`f[i][j]` 记录第i列第j个状态的所有合法方案。j二进制中1表示对应行的上一列有横放格子并捅出到本列中。 // - 属性: 方案数 // - 状态计算: `f[i][j] += f[i - 1][k]` // - i 列和 i - 1 列同一行不同时捅出来 ; 本列捅出来的状态j和上列捅出来的状态k求或,得到上列是否存在连续奇数空行状态,奇数空行不转移。 // - `f[m][0]` 表示没有m列没有涌出。 var f [N][M]int func solution(n,m int) int{ initStatus(n) f[0][0] = 1 for i:=1;i\u003c=m;i++{ for j:=0;j\u003c1\u003c\u003cn;j++{ f[i][j] = 0 // 重置统计 for k:=0;k\u003c1\u003c\u003cn;k++{ if j\u0026k==0 \u0026\u0026 st[j|k] { f[i][j] +=f[i-1][k] } } } } return f[m][0] // 最后一列 } func main() { var n,m int for { fmt.Fscan(reader, \u0026n, \u0026m) if n | m == 0 { break } fmt.Fprintln(writer, solution(n,m)) } writer.Flush() } ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:2:0","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"91. 最短Hamilton路径 package main import \"fmt\" /* 举例说明:如果是4个点 0-\u003e1-\u003e2-\u003e3 0-\u003e2-\u003e1-\u003e3 ...总方案是3!,剩下的4种方法略... 假设第一种走法的方案,距离是10,第二种走法是20, 则第二种走法后面的点再怎么走的方案也就不被采纳,并且后面点的方法是可以直接套在第一种方案后的。 所以我们就可以只关注两点: 1、那些点是走过的 2、现在走在了哪个点上 状态表示: f[status][j] 第一维表示走过的所有点,第二维度表示现在落在哪个点上 状态转移: f[status][j]=f[status_k-j][k]+weight[k][j] k表示当前走到的j点的前一步k点,并且k点的所有状态集合status_k中不包含j点 status的状态使用二进制状态压缩,1,0分别表示走过和没走过 */ const ( N = 20 M = 1 \u003c\u003c 20 ) var ( n int f [M][N]int weight [N][N]int ) func main() { fmt.Scan(\u0026n) // 输入 for i := 0; i \u003c n; i++ { for j := 0; j \u003c n; j++ { fmt.Scan(\u0026weight[i][j]) } } // 初始化f数组 for i := 0; i \u003c M; i++ { for j := 0; j \u003c N; j++ { f[i][j] = 0x3f3f3f3f } } f[1][0] = 0 // 在起点的状态,还没走,所以距离是0 for i := 0; i \u003c 1\u003c\u003cn; i++ { for j := 0; j \u003c n; j++ { if i\u003e\u003ej\u00261 \u003e 0 { // 查看集合i中的j点有没有走过,只有走过的点的集合才是可以状态转移的合法状态 for k := 0; k \u003c n; k++ { //枚举所有的整数 if i-(1\u003c\u003cj)\u003e\u003ek\u00261 \u003e 0 { //当前状态i集合还不包括j点,所以集合中要减去,减去后的集合也得满足合法的状态转移条件 // 所以k点是已经走过的,二进制1表示。 f[i][j] = min(f[i][j], f[i-(1\u003c\u003cj)][k]+weight[k][j]) } } } } } fmt.Println(f[(1\u003c\u003cn)-1][n-1]) } func min(a, b int) int { if a \u003c b { return a } return b } ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:3:0","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"经典模板题 291. 蒙德里安的梦想 最短Hamilton路径 树形DP ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:3:1","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"没有上司的舞会 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 6010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) h, e, ne [N]int idx int happy [N]int f [N][2]int hasFather [N]bool n int ) func add(a, b int) { e[idx] = b ne[idx] = h[a] h[a] = idx idx++ } func dfs(u int) { f[u][1] = happy[u] for i := h[u]; i != -1; i = ne[i] { j := e[i] dfs(j) f[u][1] += f[j][0] f[u][0] += max(f[j][0], f[j][1]) } } func main() { defer out.Flush() fmt.Fscan(in, \u0026n) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026happy[i]) } for i := 0; i \u003c N; i++ { h[i] = -1 } for i := 0; i \u003c n - 1; i++ { var a, b int fmt.Fscan(in, \u0026a, \u0026b) add(b, a) hasFather[a] = true } root := 1 for hasFather[root] { root++ } dfs(root) fmt.Fprintln(out, max(f[root][0], f[root][1])) } func max(a, b int) int { if a \u003e b { return a } return b } 记忆化搜索 ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:4:0","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"滑雪问题 ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:5:0","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"content":"滑雪 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 310 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) g [N][N]int f [N][N]int dx = [4]int{-1, 0, 1, 0} dy = [4]int{0, 1, 0, -1} n, m int ) func dp(x, y int) int { if f[x][y] != -1 { return f[x][y] } f[x][y] = 1 for i := 0; i \u003c 4; i++ { a := x + dx[i] b := y + dy[i] if a \u003e= 1 \u0026\u0026 a \u003c= n \u0026\u0026 b \u003e= 1 \u0026\u0026 b \u003c= m \u0026\u0026 g[x][y] \u003e g[a][b] { f[x][y] = max(f[x][y], dp(a, b) + 1) } } return f[x][y] } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { fmt.Fscan(in, \u0026g[i][j]) } } for i := 0; i \u003c N; i++ { for j := 0; j \u003c N; j++ { f[i][j] = -1 } } ans := 0 for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { ans = max(ans, dp(i, j)) } } fmt.Fprintln(out, ans) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-14","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/:5:1","tags":["算法","算法整理","动态规划","数位DP","状态压缩DP","树形DP","记忆化搜索"],"title":"基础算法整理(十二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%BA%8C/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 线性DP 递推方程有明显的线性关系 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:0:0","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"数字三角形 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:1:0","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"从上到下 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 510 const INF int = 1e9 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存放数字三角形 nums [N][N]int // dp 数组 // f[i][j] 表示从起点走到 [i,j] 点的最大路径和 f [N][N]int // n 行的数字三角形 n int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n) for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= i; j++ { fmt.Fscan(in, \u0026nums[i][j]) } } // 初始化为 -INF // 因为是从上往下遍历,会用到 [i-1][j] // 因此每一行需要多初始化一列,给下一行的使用 for i := 0; i \u003c= n; i++ { for j := 0; j \u003c= i + 1; j++ { f[i][j] = -INF } } // 初始化起点,表示从起点走到起点的最大路径和 f[1][1] = nums[1][1] for i := 2; i \u003c= n; i++ { for j := 1; j \u003c= i; j++ { f[i][j] = max(f[i-1][j-1], f[i-1][j]) + nums[i][j] } } ans := -INF // 遍历最后一行,获取最大路径和 for i := 1; i \u003c= n; i++ { ans = max(ans, f[n][i]) } fmt.Fprintln(out, ans) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:1:1","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"从下往上 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 510 const INF int = 1e9 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存放数字三角形 nums [N][N]int // dp 数组 // f[1][1] 表示从最下面一层走到起点的最大路径和 f [N][N]int // n 行的数字三角形 n int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n) for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= i; j++ { fmt.Fscan(in, \u0026nums[i][j]) } } // 从下往上遍历,只会用到三角形内的值,因此不用初始化 for i := n; i \u003e= 1; i-- { for j := i; j \u003e= 1; j-- { f[i][j] = max(f[i+1][j], f[i+1][j+1]) + nums[i][j] } } fmt.Fprintln(out, f[1][1]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:1:2","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"经典模板题 898. 数字三角形 剑指 Offer II 100. 三角形中最小路径之和 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:1:3","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"最长上升子序列 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:2:0","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"最长上升子序列朴素 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储输入 a [N]int // dp 数组,f[i] 表示以 i 结尾的最长子序列长度 f [N]int n int ) func main() { defer out.Flush() // 处理输入 fmt.Fscan(in, \u0026n) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026a[i]) } // 初始化,每个以 i 结尾的子序列长度至少为1 for i := 1; i \u003c= n; i++ { f[i] = 1 } // 枚举每个数字结尾 for i := 1; i \u003c= n; i++ { // 遍历 i 前面所有数字 for j := 1; j \u003c= i; j++ { // 题目要求严格小于 if a[j] \u003c a[i] { // f[j] 表示以 j 结尾的最长子序列长度,加1表示加上 i 的长度 f[i] = max(f[i], f[j] + 1) } } } // 遍历一遍 dp 数组获取最大值 ans := 0 for i := 1; i \u003c= n; i++ { ans = max(ans, f[i]) } fmt.Fprintln(out, ans) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:2:1","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"最长上升子序列优化 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 100010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) a [N]int f [N]int n int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n) for i := 0; i \u003c n; i++ { fmt.Fscan(in, \u0026a[i]) } length := 0 for i := 0; i \u003c n; i++ { l, r := 0, length for l \u003c r { mid := (l + r + 1) \u003e\u003e 1 if f[mid] \u003c a[i] { l = mid } else { r = mid - 1 } } length = max(length, r + 1) f[r + 1] = a[i] } fmt.Fprintln(out, length) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:2:2","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"经典模板题 895. 最长上升子序列 896. 最长上升子序列 II ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:2:3","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"最长公共子序列 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:3:0","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"最长公共子序列 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) a, b string // f[i][j] 表示在 a 字符串前 i 个字符中出现,且在 b 字符串前 j 个字符中出现的最长公共子序列长度 f [N][N]int n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) fmt.Fscan(in, \u0026a, \u0026b) // 让字符串从下标1开始 a = \" \" + a b = \" \" + b for i := 1; i \u003c= n; i ++ { for j := 1; j \u003c= m; j ++ { f[i][j] = max(f[i-1][j], f[i][j-1]) if a[i] == b[j] { f[i][j] = max(f[i][j], f[i-1][j-1] + 1) } } } fmt.Fprintln(out, f[n][m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:3:1","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"经典模板题 897. 最长公共子序列 剑指 Offer II 095. 最长公共子序列 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:3:2","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"编辑距离 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:4:0","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"最短编辑距离 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) a, b string // f[i][j] 表示将 a 的前 i 个字符变成 b 的前 j 个字符最少需要多少步 f [N][N]int // 字符串长度 n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026a) fmt.Fscan(in, \u0026m, \u0026b) a = \" \" + a b = \" \" + b // 表示将 a 的前0个字符变成 b 的前 i 个字符最少需要多少步 for i := 1; i \u003c= m; i++ { f[0][i] = i } // 表示将 a 的前 i 个字符变成 b 的前 0 个字符最少需要多少步 for i := 1; i \u003c= n; i++ { f[i][0] = i } for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { // f[i][j-1]+1 表示 a 的前 i 个字符与 b 的前 j-1 个字符相同需要的最小步骤, // +1 表示为 a 字符串增加一个字符,步骤数 +1 // f[i-1][j]+1 表示 a 的前 i-1 个字符与 b 的前 j 个字符相同需要的最小步骤, // +1 表示为 a 字符串删除一个字符,步骤数 +1 f[i][j] = min(f[i][j-1]+1, f[i-1][j]+1) // 当前字符不相同,需要修改当前字符 if a[i] != b[j] { // f[i-1][j-1] 表示 a 的前 i-1 个字符与 b 的前 j-1 个字符相同需要的最小步骤, // +1 表示将 a 的 i 字符修改为 b 的 j 字符,步骤数 +1 f[i][j] = min(f[i][j], f[i-1][j-1]+1) // 如果当前字符相同,那么当前步骤数就是使得 a 的前 i-1 个字符与 b 的前 j-1 个字符相同需要的最小步骤 } else { f[i][j] = min(f[i][j], f[i-1][j-1]) } } } fmt.Fprintln(out, f[n][m]) } func min(a, b int) int { if a \u003c b { return a } return b } ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:4:1","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"经典模板题 902. 最短编辑距离 72. 编辑距离 899. 编辑距离 区间DP ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:4:2","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"石子合并 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:5:0","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"石子合并 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 310 const INF int = 1e9 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储输入和前缀和 s [N]int // f[i][j] 表示合并 [i~j] 堆石子所需最小代价 f [N][N]int n int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n) // 读取输入 for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026s[i]) } // 处理前缀和 for i := 1; i \u003c= n; i++ { s[i] += s[i-1] } // f[i][i] 单独一堆的石子,合并单独一堆石子代价为0 for i := 1; i \u003c= n; i++ { f[i][i] = 0 } // length 枚举区间范围,从 2 开始是因为上面已经初始化了区间范围为 1 的情况 for length := 2; length \u003c= n; length++ { // 枚举区间左端点 for i := 1; i + length - 1 \u003c= n; i++ { // l, r 表示每一个 [l,r] 小区间,最终枚举 [1,n] 整个区间 // [1,2], [2,3], [3,4], ... // [1,3], [2,4], ... // [1,4], ... l, r := i, i + length - 1 // 表示合并 [l,r] 这个区间代价一开始为无穷 f[l][r] = INF // 以 k 为中点,划分为左右两块区间, // f[i][j] 的值就为合并左区间的最小代价 f[l][k] + 合并右区间的最小代价 f[k+1][r] + 合并左右区间的代价 s[r] - s[l-1] // s[r] - s[l-1] 用前缀和快速求 [l,r] 这段区间的值 // 为什么要加上 [l,r] 这段区间的值?因为最后还需要合并左右两堆石子 for k := l; k \u003c r; k++ { f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]) } } } fmt.Fprintln(out, f[1][n]) } func min(a, b int) int { if a \u003c b { return a } return b } 计数类DP ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:5:1","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"整数划分 ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:6:0","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"content":"整数划分 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 const MOD int= 1e9 + 7 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) f [N]int n int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n) f[0] = 1 // 将整数划分看成是完全背包问题 // f[j] 表示 1~n 当中选,体积恰好是 j 的方案数 for i := 1; i \u003c= n; i++ { for j := i; j \u003c= n; j++ { f[j] = (f[j] + f[j-i]) % MOD } } fmt.Fprintln(out, f[n]) } ","date":"2022-11-13","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/:6:1","tags":["算法","算法整理","动态规划","线性DP","区间DP","计数DP"],"title":"基础算法整理(十一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81%E4%B8%80/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 背包问题 有 N 件物品和一个容量为 V 的背包。物品可能多有个属性——费用Ci、价值Wi、数量Si。求解将哪些物品装入背包可使价值总和最大。 01背包:每个物品只有一个 完全背包:每个物品有无限个 多重背包:每个物品有给定数量 分组背包:有N组物品,每组物品中只能选一个物品 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:0:0","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"01背包 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:1:0","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"01背包朴素 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储第 i 件物品的体积和价值 v, w [N]int // dp 数组,第一维表示物品,第二维表示容量 // f[i][j] 表示把物品 i 放进容量为 j 的背包的最大价值 f [N][N]int // n 是物品的个数,m 是背包容量 n, m int ) func main() { defer out.Flush() // 处理输入 fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026v[i], \u0026w[i]) } // 初始化,表示0件物品放进容量为 i 的背包里的最大价值为0 for i := 0; i \u003c= m; i++ { f[0][i] = 0 } // i 枚举物品 for i := 1; i \u003c= n; i++ { // j 枚举容量 for j := 1; j \u003c= m; j++ { // 如果背包容量放不下物品 i 了,最大价值等于不放物品 i 的最大价值 if j \u003c v[i] { f[i][j] = f[i-1][j] // 能放下物品 i,则比较放与不放哪种价值大 // f[i-1][j] 表示不放物品 i 的最大价值 // f[i-1][j-v[i]] + w[i] 表示放物品 i 的最大价值 } else { f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]) } } } fmt.Fprintln(out, f[n][m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:1:1","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"01背包优化 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储第 i 件物品的体积和价值 v, w [N]int // dp 数组 f [N]int // n 是物品的个数,m 是背包容量 n, m int ) func main() { defer out.Flush() // 处理输入 fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026v[i], \u0026w[i]) } // 初始化,表示0件物品放进容量为 i 的背包里的最大价值为0 for i := 0; i \u003c= m; i++ { f[i] = 0 } // i 枚举物品 for i := 1; i \u003c= n; i++ { // j 枚举容量 for j := m; j \u003e= v[i]; j-- { // f[j] 表示不选第 i 件物品的最大价值,f[j-v[i]] + w[i] 表示选择第 i 件物品的最大价值 f[j] = max(f[j], f[j-v[i]] + w[i]) } } fmt.Fprintln(out, f[m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:1:2","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"经典模板题 2. 01背包问题 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:1:3","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"完全背包 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:2:0","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"完全背包朴素 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储第 i 件物品的体积和价值 v, w [N]int // dp 数组,第一维表示物品,第二维表示容量 f [N][N]int // n 是物品的数量,m 是背包容量 n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026v[i], \u0026w[i]) } // 枚举物品 for i := 1; i \u003c= n; i++ { // 枚举容量 for j := 1; j \u003c= m; j++ { // 枚举物品个数 for k := 0; k * v[i] \u003c= j; k++ { // f[i][j] 表示第 i 个物品取 k 个放进容量为 j 的背包中的最大价值 f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k) } } } fmt.Fprintln(out, f[n][m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:2:1","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"完全背包优化 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储第 i 件物品的体积和价值 v, w [N]int // dp 数组 f [N]int // n 是物品的数量,m 是背包容量 n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026v[i], \u0026w[i]) } for i := 1; i \u003c= n; i++ { for j := v[i]; j \u003c= m; j++ { f[j] = max(f[j], f[j-v[i]] + w[i]) } } fmt.Fprintln(out, f[m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:2:2","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"经典模板题 3. 完全背包问题 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:2:3","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"多重背包 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:3:0","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"多重背包朴素 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 110 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储第 i 件物品的体积、价值和数量 v, w, s [N]int // dp 数组, // f[i][j] 表示数量 s[i] 的物品 i,存入容量为 j 的背包中的最大价值 f [N][N]int // n 是物品的数量,m 是背包容量 n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026v[i], \u0026w[i], \u0026s[i]) } // 枚举物品 for i := 1; i \u003c= n; i++ { // 枚举容量 for j := 1; j \u003c= m; j++ { // 枚举物品数量从 0 ~ s[i],且总体积不超过 j for k := 0; k \u003c= s[i] \u0026\u0026 k * v[i] \u003c= j; k++ { // f[i][j] 表示第 i 个物品取 k 个放进容量为 j 的背包中的最大价值,0 \u003c= k \u003c= s[i] f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k) } } } fmt.Fprintln(out, f[n][m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:3:1","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"多重背包优化 多重背包二进制优化 把物品 i 的数量 s[i],按照二进制分成若干个,$2^0 + 2^1 + … + 2^{k} + s[i] - 2^{k} = s[i]$,将拆分的每一个数看成一个单独的物品。把所有 s[i] 拆分之后,对所有拆分的物品求一次01背包问题。 package main import ( \"bufio\" \"fmt\" \"os\" ) // 物品个数 N \u003c= 1000 // 物品数量 S \u003c= 2000 // 每个物品都按照二进制拆分成了 logS 个 // 所以总的物品个数是 N * logS 上取整 const N int = 25010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储第 i 件物品的体积、价值和数量 v, w [N]int // dp 数组, // f[i][j] 表示数量 s[i] 的物品 i,存入容量为 j 的背包中的最大价值 f [N]int // n 是物品的数量,m 是背包容量 n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) // 将物品 i 的数量按照二进制拆成若干个 cnt := 0 for i := 0; i \u003c n; i++ { var a, b, s int fmt.Fscan(in, \u0026a, \u0026b, \u0026s) k := 1 // 二进制拆分 for k \u003c= s { cnt++ v[cnt] = a * k w[cnt] = b * k s -= k k *= 2 } // 2^k 次方是小于 s[i] 的最大二进制数,剩下的数为 s[i] - 2^k if s \u003e 0 { cnt++ v[cnt] = a * s w[cnt] = b * s } } // 物品个数为拆分了多少个 n = cnt // 对所有拆分的物品做01背包 for i := 1; i \u003c= n; i++ { for j := m; j \u003e= v[i]; j-- { f[j] = max(f[j], f[j-v[i]] + w[i]) } } fmt.Fprintln(out, f[m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:3:2","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"经典模板题 4. 多重背包问题 I 5. 多重背包问题 II ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:3:3","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"分组背包 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:4:0","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"分组背包朴素 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 110 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // v[i][j] 表示第 i 组里的物品 j 的体积是多少 v, w [N][N]int // s[i] 表示第 i 组里有多少种物品 s [N]int // f[i][j] 表示把每 i 组物品选一个,放进容量为 j 的背包中的最大价值 f [N][N]int // 物品组数和背包容量 n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) // 读取输入 for i := 1; i \u003c= n; i++ { // 第 i 组有多少种物品 fmt.Fscan(in, \u0026s[i]) // 第 i 组中每个物品的体积和价值 for j := 1; j \u003c= s[i]; j++ { fmt.Fscan(in, \u0026v[i][j], \u0026w[i][j]) } } // 枚举物品组 for i := 1; i \u003c= n; i++ { // 枚举容量 for j := 0; j \u003c= m; j++ { // 不选第 i 组的最大价值 f[i][j] = f[i-1][j] // 枚举物品组中每一个物品 for k := 1; k \u003c= s[i] ; k++ { // 容量能够放下第 i 组的物品 k if v[i][k] \u003c= j { // 第 i 组选择物品 k 放入容量为 j 的背包中的最大价值 f[i][j] = max(f[i][j], f[i-1][j-v[i][k]] + w[i][k]) } } } } fmt.Fprintln(out, f[n][m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:4:1","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"分组背包优化 只优化了空间 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 110 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // v[i][j] 表示第 i 组里的物品 j 的体积是多少 v, w [N][N]int // s[i] 表示第 i 组里有多少种物品 s [N]int // f[j] 表示把每一组物品各选一个,放进容量为 j 的背包中的最大价值 f [N]int // 物品组数和背包容量 n, m int ) func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) // 读取输入 for i := 1; i \u003c= n; i++ { // 第 i 组有多少种物品 fmt.Fscan(in, \u0026s[i]) // 第 i 组中每个物品的体积和价值 for j := 1; j \u003c= s[i]; j++ { fmt.Fscan(in, \u0026v[i][j], \u0026w[i][j]) } } // 枚举物品组 for i := 1; i \u003c= n; i++ { // 枚举容量 for j := m; j \u003e= 0; j-- { // 枚举物品组中每一个物品 for k := 1; k \u003c= s[i] ; k++ { // 容量能够放下第 i 组的物品 k if v[i][k] \u003c= j { // 第 i 组选择物品 k 放入容量为 j 的背包中的最大价值 f[j] = max(f[j], f[j-v[i][k]] + w[i][k]) } } } } fmt.Fprintln(out, f[m]) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:4:2","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","动态规划","背包问题"],"content":"经典模板题 9. 分组背包问题 ","date":"2022-11-01","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/:4:3","tags":["算法","算法整理","动态规划","背包问题"],"title":"基础算法整理(十)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%8D%81/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 最小生成树 最小生成树问题,对应的图都是无向图。 最小生成树问题允许有负边 不能有环 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:0:0","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"Prim算法(普里姆算法) Prim算法和Dijkstra算法很相似 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:1:0","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"朴素版Prim算法 稠密图用朴素版Prim算法,时间复杂度O(n^2) 算法步骤: 将所有点初始化为正无穷 迭代n次 每次找到不在集合当中的,距离集合最小的点t。集合表示已经加入生成树的点,距离集合最小定义为——点i到集合内任意一点的距离,是所有不在集合中的点中最小的。点i到集合的距离定义为——点i到集合内点的所有边中的最小值 用t更新其他点到集合的距离 把t加入集合 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:1:1","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"朴素版Prim算法代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 510 const INF int = 0x3f3f3f3f var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 邻接矩阵存储图 g [N][N]int // 点i到集合的距离 dist [N]int // 点i是否在集合中 st [N]bool n, m int ) func prim() int { // 初始化距离 for i := 1; i \u003c= n; i++ { dist[i] = INF } // res表示最小生成树各边权值和 res := 0 // 遍历n次 for i := 0; i \u003c n; i++ { t := -1 // 找到不在集合当中的,距离集合最近的点t for j := 1; j \u003c= n; j++ { // j不在集合中 if ! st[j] \u0026\u0026 (t == -1 || dist[t] \u003e dist[j]) { t = j } } // 把t点加入集合 st[t] = true // 如果不是第一次找,而且不在集合中的点到集合最小距离是INF了,那么该图不是连通图 if i != 0 \u0026\u0026 dist[t] == INF { return INF } // 累加权值和 if i != 0 { res += dist[t] } // 用t更新其他点到集合的距离 for j := 1; j \u003c= n; j++ { dist[j] = min(dist[j], g[t][j]) } } return res } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= n; j++ { if i == j { g[i][j] = 0 } else { g[i][j] = 0x3f3f3f3f } } } for ; m \u003e 0; m-- { var a, b, c int fmt.Fscan(in, \u0026a, \u0026b, \u0026c) // 无向图,建边的时候两个方向都建一次 g[a][b] = min(g[a][b], c) g[b][a] = min(g[b][a], c) } // 如果不存在最小生成树,返回INF // 存在则返回最小生成树各边的权值和 t := prim() if t == INF { fmt.Fprintln(out, \"impossible\") } else { fmt.Fprintln(out, t) } } func min(a, b int) int { if a \u003c b { return a } return b } ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:1:2","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"经典模板题 858. Prim算法求最小生成树 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:1:3","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"堆优化版Prim算法 稀疏图用堆优化版Prim算法,时间复杂度O(mlogn),堆优化版Prim算法不如Kruskal算法简单好写,因此稀疏图一般使用Kruskal算法实现。 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:1:4","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"Kruskal(克鲁斯卡尔算法) 时间复杂度O(mlogm) Kruskal算法是排序+并查集的应用,算法性能瓶颈主要在排序部分。 算法步骤: 对所有边排序O(mlogm) 枚举每条边a-\u003eb,权重c 如果a-\u003eb不连通,把这条边加入集合中,这一步用并查集来做O(m) ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:2:0","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"Kruskal算法代码模板 package main import ( \"fmt\" \"bufio\" \"os\" \"sort\" ) const N int = 100010 const M int = 200010 // Kruskal算法只要能遍历到所有边即可 type Edge struct { a, b, w int } var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 并查集中的p数组,存储所有点 // 用于快速判断两个点是否在同一集合中 p [N]int n, m int ) // 寻找x的父节点,并做状态压缩,并查集的内容 func find(x int) int { if x != p[x] { p[x] = find(p[x]) } return p[x] } // 给定一个所有边的集合,返回是否存在最小生成树及最小生成树对应的权值和 func kruskal(edges []*Edge) (bool, int) { var ( // 记录最小生成树对应的权值和 res int // 记录已经联通的边数 // 连通n个点只需要n-1条边,用cnt来判断是否有最小生成树 cnt int ) // 先对所有边根据边权从小到大排序 sort.Slice(edges, func(i, j int) bool { return edges[i].w \u003c edges[j].w }) // 从小到大遍历所有边,得到的生成树就是最小的 for i := 0; i \u003c m; i++ { a := edges[i].a b := edges[i].b w := edges[i].w // 用并查集判断a、b点是否在同一集合中 // 在同一集合中意味着,a、b点连通了 a, b = find(a), find(b) // 如果a、b不连通 if a != b { // 连通a、b p[b] = a // 累加边权和 res += w // 记录生成树边数 cnt++ } } // 连接n个点,至少需要n-1条边,边数少于n - 1条,则图不存在最小生成树 return cnt \u003c n - 1, res } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { p[i] = i } edges := make([]*Edge, 0, M) for i := 0; i \u003c m; i++ { var a, b, w int fmt.Fscan(in, \u0026a, \u0026b, \u0026w) edges = append(edges, \u0026Edge{a, b, w}) } flag, res := kruskal(edges) if flag { fmt.Fprintln(out, \"impossible\") } else { fmt.Fprintln(out, res) } } ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:2:1","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"经典模板题 859. Kruskal算法求最小生成树 二分图 二分图指的是图中所有点能划分到两个集合中,所有的边都在两个集合之间连接,集合内部没有边。 二分图的题目通常是判断一个图是否是二分图。 一个图是二分图,当且仅当这个图不含有奇数环。反之,一个图不含有奇数环,那么它一定是一个二分图。 环是从一个点出发经过m条边后,能回到自身。奇数环指的是m为奇数。 当一个点i属于集合a时,那么所有与它连接的点j必须属于集合b。 由于图中不含奇数环,所以划分点到不同集合中的步骤一定不会矛盾。 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:2:2","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"染色法判断图是否为二分图 就是DFS,时间复杂度O(n + m) 染色过程就是把一个点确认颜色,比如白色,然后深度搜索把该点相连的其他点染为黑色。 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:3:0","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"染色法代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const ( N int = 100010 M int = 200010 ) var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 邻接表存储图 h [N]int e, ne [M]int idx int // 存储改点染的什么颜色,1为白色,2为黑色 color [N]int n, m int ) func add(a, b int) { e[idx] = b ne[idx] = h[a] h[a] = idx idx++ } func dfs(n, c int) bool { // 先把当前点染为颜色c color[n] = c // 遍历该点所有邻边,将其他点染为另一种颜色 for i := h[n]; i != -1; i = ne[i] { j := e[i] if color[j] == 0 { // 3 - c,当前颜色是1,其他点就染为2;当前颜色是2,其他点就染为1; // 递归下去染色,任意一个连通块染色失败,就返回false if ! dfs(j, 3 - c) { return false } // 当前点与它连通的点颜色一样,发生矛盾 } else if color[j] == color[n] { return false } } return true } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { h[i] = -1 } for ; m \u003e 0; m-- { var a, b int fmt.Fscan(in, \u0026a, \u0026b) // 无向图 add(a, b); add(b, a) } flag := true // 图不一定是连通图,所以需要遍历所有点,把每个连通块都尝试染色 // 如果有连通块染色失败,说明该图无法二分,也说明有奇数环 for i := 1; i \u003c= n; i++ { if color[i] == 0 { if ! dfs(i, 1) { flag = false break } } } if flag { fmt.Fprintln(out, \"Yes\") } else { fmt.Fprintln(out, \"No\") } } ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:3:1","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"经典模板题 860. 染色法判定二分图 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:3:2","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"匈牙利算法——二分图最大匹配问题 求二分图的最大匹配,时间复杂度最坏情况O(mn),实际运行情况远小于这个时间复杂度,效果很好。 二分图最大匹配问题指的是,二分图中的集合a和集合b中的点,一一匹配的最大可能。 如图两个集合Boys和Girls,为每个男生匹配一个女生,最多能匹配多少个。 算法运行步骤: 遍历集合Boys 遍历Boys[i]的所有可匹配女生,任选一个匹配(如果是有权图,那么选权值最大的,就成为二分图的最优匹配) 如果Boys[i]的所有可匹配女生都已经分配了 那么尝试让这些已匹配的人去匹配其他人,空出位置 图中B2只能匹配G2,但是G2已经被分配给B1了,那么尝试让B1匹配其他人,以增加匹配数。 算法运行完之后的情侣数量就是最大匹配数。 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:4:0","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const ( N int = 510 M int = 100010; ) var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 邻接表存储 h [N]int e, ne [M]int idx int // 集合b的点匹配集合a的哪个点 match [N]int // 集合a每个点遍历的时候,标记集合b那些点被访问了,避免递归时发生重复选择同一目标 // 递归子树去重 st [N]bool // n1表示a集合有多少点,n2表示b集合有多少点,m表示边的数量 n1, n2, m int ) func add(a, b int) { e[idx] = b ne[idx] = h[a] h[a] = idx idx++ } // 寻找x能否匹配到一个集合b中的点 func find(x int) bool { // 遍历集合a中点x能匹配的所有集合b中的点 for i := h[x]; i != -1; i = ne[i] { // j是集合b中的点 j := e[i] // 同一颗递归树中,j必须没被用过才能选 if ! st[j] { // 这里把j标记为true,在下一层递归时,点j对应的点就不能再次匹配点j了,避免死循环 // 在同一颗递归树里面,点x不能再次匹配点j,必须找别的点,如果找不到,那么下面的判断就失败,不会赋值 // 回溯算法中的子树去重 st[j] = true // j点没有匹配集合a中的点 // 或者点j对应的点,能匹配其他点 if match[j] == 0 || find(match[j]) { // 实际记录时,记录的是集合b中哪个点与集合a中的点匹配 match[j] = x return true } } } return false } func main() { defer out.Flush() fmt.Fscan(in, \u0026n1, \u0026n2, \u0026m) for i := 1; i \u003c= n1; i++ { h[i] = -1 } for ; m \u003e 0; m-- { var a, b int fmt.Fscan(in, \u0026a, \u0026b) // 虽然是无向图,但是算法遍历过程中只会从集合a找向集合b // 所以b-\u003ea的边用不到 add(a, b) } // 匹配数量 res := 0 for i := 1; i \u003c= n1; i++ { // 每个点匹配开始都要把st清空,st只负责子树去重 // 是否匹配成功还要看match for i := 1; i \u003c N; i++ { st[i] = false } // i能否匹配到一个点 if find(i) { res++ } } fmt.Fprintln(out, res) } ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:4:1","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"content":"经典模板题 861. 二分图的最大匹配 ","date":"2022-10-31","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/:4:2","tags":["算法","算法整理","最小生成树","Prim","Kruskal","二分图","二分图最大匹配","染色法","匈牙利算法"],"title":"基础算法整理(九)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B9%9D/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 最短路 最短路问题分为两类: 单源最短路:求一个点到其他所有点的最短距离 多源汇最短路:源点就是起点,汇点就是终点,可能有多个询问,每次询问一个点到另一个点的最短距离 单源最短路根据边权值的正负,可以使用不同的算法: n为图的点的数量,m为图的边的数量 边权值为正 朴素版Dijkstra算法,时间复杂度O(n^2),与边数量没有关系,比较适合稠密图 堆优化版Dijkstra算法,时间复杂度O(mlogn),如果是稀疏图,或n数量比较大,应该使用堆优化版 边权值存在负数 Bellman-Ford算法,时间复杂度O(nm),求经过不超过k条边的最短路,只能用Bellman-Ford算法来做 SPFA算法,时间复杂度一般情况下为O(m),最坏情况为O(nm),是对Bellman-Ford算法的优化,SPFA算法一般情况下优于Bellman-Ford算法,但是有的情况只能用Bellman-Ford算法来做 多源汇最短路只有一种算法:Floyd算法,时间复杂度为O(n^3) ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:0:0","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"单源最短路 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:0","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"朴素Dijkstra算法代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 510 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 用邻接矩阵存储稠密图 g [N][N]int // 存储起点到各个点的最短距离 d [N]int // 存储是否已经确定1-\u003ei的最短路 st [N]bool n, m int ) func dijkstra() int { // 初始化d为正无穷,表示所有点都不可达 for i := 1; i \u003c= n; i++ { d[i] = 0x3f3f3f3f } // 1号点为起点,初始化起点到起点距离为0 d[1] = 0 for i := 0; i \u003c n; i++ { t := -1 // 寻找还未更新距离的节点中的最小距离的节点 for j := 1; j \u003c= n; j++ { if !st[j] \u0026\u0026 (t == -1 || d[t] \u003e d[j]) { t = j } } // 标记已寻到1-\u003et的最短路 st[t] = true // 从t点出发更新其余点的最短路 for j := 1; j \u003c= n; j++ { d[j] = min(d[j], d[t] + g[t][j]) } } // n点不可达 if d[n] == 0x3f3f3f3f { return -1 } return d[n] } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) // 初始化g为正无穷,表示一开始所有边都没建立连接 for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= n; j++ { g[i][j] = 0x3f3f3f3f } } for ; m \u003e 0; m-- { var x, y, z int fmt.Fscan(in, \u0026x, \u0026y, \u0026z) // 为x-\u003ey点建立连接,权重为z // 因为存在重边,所有需要存储最小值 // 不用处理自环,因为求1-\u003en的最短路自环没有影响 g[x][y] = min(g[x][y], z) } // 返回1-\u003en的最短距离 fmt.Fprintln(out, dijkstra()) } func min (a, b int) int { if a \u003c b { return a } return b } ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:1","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"经典模板题 849. Dijkstra求最短路 I ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:2","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"用堆来优化朴素版Dijkstra算法 朴素版的Dijkstra算法中,寻找没确认最短路的点中的距离最小的点这一步,时间复杂度是O(n^2)的。 for i := 0; i \u003c n; i++ { t := -1 // 寻找还未更新距离的节点中的最小距离的节点 // 这一步总共执行n * n次 for j := 1; j \u003c= n; j++ { if !st[j] \u0026\u0026 (t == -1 || d[t] \u003e d[j]) { t = j } } ... } 寻找一个集合中的最小值,我们可以用堆来优化。因此我们可以用堆来存储所有点到起点的最短距离d,这样每次寻找最小值只需要O(1)的时间复杂度,需要寻找n次,因此这一步的时间复杂度降到了O(n)。 // d是一个小根堆,或叫优先队列 // 寻找最小距离点操作改为 t := heap.Pop(d) 除此之外,我们还需要用最小距离节点——t更新到其他点的距离。朴素算法中,这一步时间复杂度是O(m)的。因为我们每次只会更新节点t能到达的节点,访问内存操作最多只有m次。 for i := 0; i \u003c n; i++ { ... // 从t点出发更新其余点的最短路 // 每次循环,只有t能到达的点有可能被更新 // n次循环加起来,有效操作次数\u003c=m次 for j := 1; j \u003c= n; j++ { d[j] = min(d[j], d[t] + g[t][j]) } } 堆优化后,因为需要更新堆元素,需要调整堆,因此这一步时间复杂度为O(mlogn)。 for i := 0; i \u003c n; i++ { ... // 从t点出发更新其余点的最短路 // 每次循环,只有t能到达的点有可能被更新 // n次循环加起来,有效操作次数\u003c=m次 for j := 1; j \u003c= n; j++ { d[j] = min(d[j], d[t] + g[t][j]) // 这里调整堆是O(logn)的 heap.Push(d, j) } } 最后,朴素算法就被优化成了O(mlogn) ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:3","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"堆优化版Dijkstra算法代码模板 package main import ( \"fmt\" \"bufio\" \"os\" \"container/heap\" ) const N int = 1000010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 邻接表存储图 h, e, ne [N]int idx int // 存储起点到i点的最短距离 d [N]int // 存储i-\u003ej的边权 w [N]int // 记录点i是否已经寻到最短路 st [N]bool n, m int ) // 存储no节点到起点的距离 type Pair struct { dis, no int } type PriorityQueue []*Pair func (pq PriorityQueue) Len() int { return len(pq) } func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] } func (pq PriorityQueue) Less(i, j int) bool { return pq[i].dis \u003c pq[j].dis } func (pq *PriorityQueue) Push(x interface{}) { *pq = append(*pq, x.(*Pair)) } func (pq *PriorityQueue) Pop() interface{} { old := *pq n := len(old) x := old[n-1] old[n-1] = nil // avoid memory leak *pq = old[0:n-1] return x } func add(a, b, c int) { e[idx] = b w[idx] = c ne[idx] = h[a] h[a] = idx idx++ } func dijkstra() int { // 初始化小根堆,并存入起点到起点的最短路 pq := \u0026PriorityQueue{ { 0, 1 } } d[1] = 0 for pq.Len() \u003e 0 { t := heap.Pop(pq).(*Pair) // t.no是目前最短距离节点编号,t.dis是最短距离节点到起点的距离 // 因为存在重边,再遇到相同节点,就跳过 if st[t.no] { continue } st[t.no] = true // 更新最短距离节点能到达的所有节点 for i := h[t.no]; i != -1; i = ne[i] { j := e[i] if d[j] \u003e d[t.no] + w[i] { d[j] = d[t.no] + w[i] heap.Push(pq, \u0026Pair{ d[j], j }) } } } if d[n] == 0x3f3f3f3f { return -1 } return d[n] } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 0; i \u003c N; i++ { h[i] = -1 d[i] = 0x3f3f3f3f } for ; m \u003e 0; m-- { var a, b, c int fmt.Fscan(in, \u0026a, \u0026b, \u0026c) add(a, b, c) } fmt.Fprintln(out, dijkstra()) } ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:4","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"经典模板题 850. Dijkstra求最短路 II ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:5","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"Bellman-Ford算法 两重循环,第一重循环节点次数n,第二重循环所有边a-\u003eb及权重w,并更新dist[b] = min(dist[b], dist[a] + w)。这个更新的过程叫做松弛操作。 Bellman-Ford算法证明了,所有循环结束之后,所有的边都满足dist[b] \u003c= dist[a] + w,这个等式也叫作三角不等式 负权回路是回路的权值加起来小于0 如果存在负权回路,那么Bellman-Ford求出的最短路不一定存在(最短路为负无穷时不存在) Bellman-Ford算法可以求出是否存在负权回路 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:6","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"Bellman-Ford算法代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const ( N int = 510 M int = 10010 ) // 存储所有边,a -\u003e b 权值为 w 的边 type Edge struct { a, b, w int } var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // Bellman-Ford算法只要能遍历到所有边就可以求最短路 edges [M]Edge // dist 存储 起点 到 点i 的最短距离 dist [N]int // backup 存储上一次 dist 的值,避免本次更新时用到被覆盖的值 // 跟 背包问题 中从后往前遍历,避免使用被覆盖值一个道理 backup [N]int // 标识是否找到最短路 flag bool = true; n, m, k int ) func bellmanFord() int { // 初始化 dist 所有点到起点距离为无穷 for i := 1; i \u003c= n; i++ { dist[i] = 0x3f3f3f3f } // 起点到起点距离为0 dist[1] = 0 for i := 0; i \u003c k; i++ { // 保存 dist 状态 copy(backup[:], dist[:]) for j := 0; j \u003c m; j++ { a := edges[j].a b := edges[j].b w := edges[j].w // 使用 backup 的状态,而不是 dist dist[b] = min(dist[b], backup[a] + w) } } // 因为存在负权,距离可能被更新为0x3f3f3f3f - 某个负权w,所以不能简单判断 dist[n] == 0x3f3f3f3f // 但是负权最多为 10000,k为500,最多为0x3f3f3f3f - 500 * 10000 if dist[n] \u003e 0x3f3f3f3f / 2 { flag = false; return -1 } return dist[n] } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m, \u0026k) for i := 0; i \u003c m; i++ { var a, b, w int fmt.Fscan(in, \u0026a, \u0026b, \u0026w) edges[i] = Edge{a, b, w} } t := bellmanFord() if ! flag \u0026\u0026 t == -1 { fmt.Fprintln(out, \"impossible\") } else { fmt.Fprintln(out, t) } } func min(a, b int) int { if a \u003c b { return a } return b } ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:7","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"经典模板题 853. 有边数限制的最短路 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:8","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"SPFA算法 SPFA算法求最短路问题的限制最少,只要图中没有负环的存在,就可以用SPFA算法来求解最短路问题,而99%的最短路问题,都没有负环。 SPFA算法是对Bellman-Ford算法的优化。 SPFA算法的第一重循环是所有被更新过距离的点,用一个队列来存放。一开始队列中只有起点,用起点更新起点的所有邻边,能更新的点加入队列中,用这些变小了的点去更新其他点,才有会使其他点的距离缩小,才有意义。 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:9","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"SPFA算法代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 100010 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 稀疏图,用邻接表来存 h, e, ne, w [N]int idx int // 数组模拟队列,或者直接用slice也行 que [N]int qh, qt int = 0, -1 // 存储 点1 到 点i 的最短距离 dist [N]int // 标识 点i 是否在队列中,在队列中的点是距离缩小了的点, // 用这些点去更新其他点才有意义 st [N]bool flag bool = true n, m int ) // 邻接表建图 func add(a, b, c int) { e[idx] = b ne[idx] = h[a] w[idx] = c h[a] = idx idx++ } func spfa() int { // 初始化所有点为无穷 for i := 1; i \u003c= n; i++ { dist[i] = 0x3f3f3f3f } // 起点 到 起点 的距离为0 dist[1] = 0 qt++ que[qt] = 1; st[1] = true for qh \u003c= qt { t := que[qh] qh++ // 改点出队,标记为不在队列中 st[t] = false // 遍历t的所有边 for i := h[t]; i != -1; i = ne[i] { j := e[i] // 用t更新它的所有邻边,如果能缩小距离,将点j也加入队列 if dist[j] \u003e dist[t] + w[i] { dist[j] = dist[t] + w[i] // 只有不在队列中的点才加入队列 if ! st[j] { qt++ que[qt] = j st[j] = true } } } } if dist[n] == 0x3f3f3f3f { flag = false return -1 } return dist[n] } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { h[i] = -1 } for ; m \u003e 0; m-- { var a, b, c int fmt.Fscan(in, \u0026a, \u0026b, \u0026c) add(a, b, c) } t := spfa() if ! flag \u0026\u0026 t == - 1 { fmt.Fprintln(out, \"impossible\") } else { fmt.Fprintln(out, t) } } ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:10","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"经典模板题 851. spfa求最短路 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:11","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"SPFA算法判断是否存在负环 dist[i]的含义是起点到i点的最短路距离,我们新加一个cnt[i],表示**起点到i点最短路经过的边数**。 dist[i]更新的条件是,与i连接的其他点j,起点-\u003ej的距离+j-\u003ei的距离,小于当前i记录的最短路距离。也就是当前i的状态能被它连接的其他点更新(DP思想)。dist[i] = min(dist[i], dist[j] + w)。 cnt[i]跟着dist[i]一起更新,更新为起点-\u003ej的边数 + 1,因为起点-\u003ej-\u003ei是新的最短路,所以当前i点最短路的边数为,能更新i点最短路的点j所经过的边数加1,可以理解为DP中当前状态由上一个状态推导出来。 判断是否存在负环的条件是,cnt[i] \u003e= n,其中n为图中点的个数。如果最短路径等于n,意味这经过了n+1个点,但图中只有n个点,根据抽屉原理,则图中某一个点被经过了两次,则图中有环。因为SPFA是最短路算法,因此正环实际上不会循环(走正环一定没有不走正环短),只有负环会一直更新(走负环会一直得到更小的值,产生负无穷)。所以当cnt[i] \u003e= n时,就可以停止循环,返回找到负环了,如果没有负环,就是正常的找最短路的过程。 抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理。 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:12","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"SPFA算法判断是否存在环代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const ( N int = 2010 M int = 10010 ) var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 邻接表存储图 h [N]int e, ne, w [M]int idx int // 存储起点到点i的最短距离 dist [N]int // 存储起点到点i最短路经过的边数 cnt [N]int // 记录点i是否在队列中 st [N]bool n, m int ) // 邻接表建图 func add(a, b, c int) { e[idx] = b ne[idx] = h[a] w[idx] = c h[a] = idx idx++ } // SPFA算法判断是否存在负环 func spfa() bool { que := make([]int, 0, n) for i := 1; i \u003c= n; i++ { // 初始化所有距离为正无穷 dist[i] = 0x3f3f3f3f // 这里与spfa求最短路不一样,要把所有点入队 // 因为负环可能从起点到不了 que = append(que, i) st[i] = true } // 起点到起点的距离为0 dist[1] = 0 for len(que) \u003e 0 { t := que[0] que = que[1:] st[t] = false for i := h[t]; i != -1; i = ne[i] { j := e[i] // 点j可以被点t更新距离 if dist[j] \u003e dist[t] + w[i] { // 更新dist[j]的同时更新cnt[j] dist[j] = dist[t] + w[i] cnt[j] = cnt[t] + 1 // 如果经过了n条边,意味这经过了n+1个点,存在环 // 最短路中正环不会循环,只有负环会循环,因此可以判断存在负环 if cnt[j] \u003e= n { return true } // j不在队列中才加入队列 if ! st[j] { que = append(que, j) st[j] = true } } } } return false } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { h[i] = -1 } for ; m \u003e 0; m-- { var a, b, c int fmt.Fscan(in, \u0026a, \u0026b, \u0026c) add(a, b, c) } if spfa() { fmt.Fprintln(out, \"Yes\") } else { fmt.Fprintln(out, \"No\") } } ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:13","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"经典模板题 852. spfa判断负环 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:14","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"Floyd算法求最短路 Floyd算法是基于DP的,时间复杂度O(n^3)。用邻接矩阵来存储图——d[i][j]表示从i点到j点的最短路是多少。 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:15","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"Floyd算法求最短路代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const ( N int = 210 INF int = 1e9 ) var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 用邻接矩阵存储图 d [N][N]int // n个点,m条边,q个询问 n, m, q int ) func floyd() { for k := 1; k \u003c= n; k++ { for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= n; j++ { // DP思想 d[i][j] = min(d[i][j], d[i][k] + d[k][j]) } } } } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m, \u0026q) // 初始化邻接矩阵,图存在重边和自环 // floyd要求图中没有负环,自环自己到自己初始化为0 for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= n; j++ { if i == j { d[i][j] = 0 } else { d[i][j] = INF } } } // 读入m条边 for ; m \u003e 0; m-- { var a, b, w int fmt.Fscan(in, \u0026a, \u0026b, \u0026w) // 重边用取min的方式解决 d[a][b] = min(d[a][b], w) } // Floyd算法会将d[N][N]变成存储点i到点j的最短路距离 floyd() // q个询问 for ; q \u003e 0; q-- { var a, b int fmt.Fscan(in, \u0026a, \u0026b) // 因为存在负权值,最大值可能会被更新 if d[a][b] \u003e INF / 2 { fmt.Fprintln(out, \"impossible\") } else { fmt.Fprintln(out, d[a][b]) } } } func min(a, b int) int { if a \u003c b { return a } return b } ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:16","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"content":"经典模板题 854. Floyd求最短路 ","date":"2022-10-26","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/:1:17","tags":["算法","算法整理","最短路","Dijkstra","Bellman-Ford","SPFA","Floyd"],"title":"基础算法整理(八)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AB/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 搜索 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:0:0","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"BFS 使用数据结构:queue。 使用空间:因要存储每一层所有节点,因此使用的空间是O(2^h)指数级,h是树的高度。 BFS搜索具有最短路的性质。BFS搜索两个点的距离,一定是最短的。 BFS是树中的层序遍历。 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:1:0","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"代码模板 走迷宫问题 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 110 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) // 存储迷宫 maze [N][N]int // 存储距离 dist [N][N]int n, m int ) type pair struct { first int second int } func bfs(start pair) int { queue := make([]pair, 1) queue[0] = start dist[0][0] = 0 // 上右下左 四个方向 var dx = [4]int{-1, 0, 1, 0} var dy = [4]int{0, 1, 0, -1} for len(queue) \u003e 0 { t := queue[0] queue = queue[1:] // 枚举四个方向 for j := 0; j \u003c 4; j++ { x := t.first + dx[j] y := t.second + dy[j] // 将合法的位置加入队列中 if x \u003e= 0 \u0026\u0026 x \u003c n \u0026\u0026 y \u003e= 0 \u0026\u0026 y \u003c m \u0026\u0026 maze[x][y] == 0 \u0026\u0026 dist[x][y] == -1 { queue = append(queue, pair{x, y}) dist[x][y] = dist[t.first][t.second] + 1 } } } return dist[n-1][m-1] } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 0; i \u003c n; i++ { for j := 0; j \u003c m; j++ { fmt.Fscan(in, \u0026maze[i][j]) // 没走过的点初始化为-1 dist[i][j] = -1 } } fmt.Fprintln(out, bfs(pair{0, 0})) } ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:1:1","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"经典模板题 844. 走迷宫 845. 八数码 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:1:2","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"DFS 使用数据结构:stack。 使用空间:只需要存储当前路径上的所有节点,因此使用的空间是O(h),h是树的高度。 DFS的一些概念: DFS搜索不具有最短路性质。 DFS是树的中序遍历(左中右)。 DFS最重要的是画出递归树。 DFS两个重要概念——回溯和剪枝。 回溯的难点在于递归过程中的去重问题。常见去重有: 同层去重 子树去重 路径去重 深度优先搜索可以求出某个节点为根的子树上节点的数量。 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:2:0","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"代码模板 N皇后问题 package main import ( \"fmt\" \"bufio\" \"os\" ) var in = bufio.NewReader(os.Stdin) var out = bufio.NewWriter(os.Stdout) const N int = 10 // 棋盘 var chessbord [N][N]byte var ( // 记录列是否有皇后 col [N]bool // 记录45度对角是否有皇后,有n行n列,因此需要开2*N的空间 udg [2*N]bool // 记录135度对角是否有皇后,有n行n列,因此需要开2*N的空间 dg [2*N]bool ) var n int func dfs(row int) { // 找到了一种解决方案,输出棋盘 if row == n { for i := 0; i \u003c n; i++ { for j := 0; j \u003c n; j++ { fmt.Fprintf(out, \"%c\", chessbord[i][j]) } fmt.Fprintln(out) } fmt.Fprintln(out) return } // 枚举每一列,能放下皇后的进入下一行 for c := 0; c \u003c n; c++ { // 如果这一列、135度对角和45度对角上都没有皇后,就在c列放下皇后,并进入下一行 // row - c + n 和 row + c 如何理解?每条对角线可以看成是一个 y = x + b 或 y = -x + b 的函数 // 唯一的 x和y 可以确认唯一的 b,我们可以用这个 b 来代表y行x列的对角线 // 135度对角线y = x + b,45度对角线y = -x + b // 变形后135度对角线 b = y - x,45度对角线b = y + x // 因为 y - x 可能为负,我们可以加上一个n,将整体结果映射到0~n这个区间 if !col[c] \u0026\u0026 !udg[row - c + n] \u0026\u0026 !dg[row + c] { // 放下皇后 chessbord[row][c] = 'Q' // 标记 c 列有皇后;标记 row 行 -c 列有皇后;标记 row 行 c 列有皇后 col[c] = true; udg[row - c + n] = true; dg[row + c] = true // 进入下一行 dfs(row + 1) // 回溯状态,为下一列枚举提供可能 chessbord[row][c] = '.' col[c] = false; udg[row - c + n] = false; dg[row + c] = false } } } func main() { defer out.Flush() fmt.Fscan(in, \u0026n) // 初始化棋盘 for i := 0; i \u003c n; i++ { for j := 0; j \u003c n; j++ { chessbord[i][j] = '.' } } dfs(0) } ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:2:1","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"经典模板题 842. 排列数字 843. n-皇后问题 51. N 皇后 图论 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:2:2","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"树与图的存储 树是一种特殊的图,因此我们只讨论图即可。 图的形式 图分为有向图和无向图,无向图是一种特殊的有向图,其两个顶点有两条相互连接的边,因此在讨论图的遍历时,只讨论有向图即可。 图的存储形式 图的存储形式有: 邻接矩阵 邻接表 邻接矩阵是一个N*N的二维数组。x行y列存储值为1,表示x顶点与y顶点有一条边。邻接矩阵使用比较少,因为他需要O(n^2)的存储空间,比较适合存储稠密图。 邻接表是一个元素是链表的数组,下标x存储的是一条链表,链表所有节点是x顶点能到达的所有其他顶点。 重边:指的是点i到点j之间存在不止一条边 自环:指的是点i有一条边指向自己 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:3:0","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"图的遍历 深度有限搜索可以方便的获取子树的节点数量。 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:4:0","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"深度优先搜索遍历代码模板 const N int = 100010 var ( // 邻接表的存储方式 h [N]int e, ne [2*N]int idx int // 记录哪些点被访问过了 st [N]bool ) // 为 a,b 点建立连接 func add(a, b int) { e[idx] = b ne[idx] = h[a] h[a] = idx idx++ } // 深度有限搜索遍历图 func dfs(u int) { st[u] = true for i := h[u]; i != -1; i = ne[i] { j := e[i] if !st[j] { dfs(j) } } } ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:4:1","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"经典模板题 846. 树的重心 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:4:2","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"广度优先搜索代码模板 const N int = 100010 var ( // 存储邻接表 h, e, ne [N]int idx int // 存储点是否被访问过 st [N]bool ) func add(a, b int) { e[idx] = b ne[idx] = h[a] h[a] = idx idx++ } func bfs() { queue := make([]int, 1, N) queue[0] = 1 st[1] = true for len(queue) \u003e 0 { t := queue[0] queue = queue[1:] // 当前点的邻接表 for i := h[t]; i != -1; i = ne[i] { j := e[i] if !st[j] { st[j] = true queue = append(queue, j) } } } } ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:4:3","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"经典模板题 847. 图中点的层次 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:4:4","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"拓扑排序 拓扑排序是指,将有向图中的所有顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素。 拓扑排序是针对有向图而言,无向图没有拓扑序列。 拓扑排序的一个典型应用是——有前导课程的课程表,所有后面的课程指向前面的先导课程。 一个有环图不存在拓扑排序,因为必定有一个前面的节点指向后面的节点。反之,一个有向无环图必定存在拓扑排序,因此有向无环图又称为拓扑图。 图的入度与出度: 入度:有多少条边指向该节点 出度:该节点有多少条边出去 求拓扑排序的步骤 寻找入度为0的点入队,入度为0意味着没有其他节点指向它。 取出队头元素,将队头元素所有的出边删掉 将出边指向节点中,入度为0的点入队 拓扑图必定存在一个入度为0的点。如果存在环,那么将不会有元素入队,循环结束。 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:5:0","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 1e5 + 10 var ( in = bufio.NewReader(os.Stdin) out = bufio.NewWriter(os.Stdout) h, e, ne [N]int idx int // d 存储所有节点的入度 queue, d [N]int hh, tt int = 0, -1 n, m int ) func add(a, b int) { e[idx] = b ne[idx] = h[a] h[a] = idx idx++ } func topsort() bool { // 先将所有入度为0的点入队 for i := 1; i \u003c= n; i++ { if d[i] == 0 { tt++ queue[tt] = i } } // 取出队头元素,将其所有出边删除 for hh \u003c= tt { t := queue[hh] hh++ for i := h[t]; i != -1; i = ne[i] { j := e[i] // 将入度为0的点入队 d[j]-- if d[j] == 0 { tt++ queue[tt] = j } } } // 如果是拓扑图,那么所有点都会入队 return tt == n - 1 } func main() { defer out.Flush() fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { h[i] = -1 } var a, b int for i := 1; i \u003c= m; i++ { fmt.Fscan(in, \u0026a, \u0026b) add(a, b) d[b]++ } if topsort() { for i := 0; i \u003c hh; i++ { fmt.Fprintf(out, \"%d \", queue[i]) fmt.Fprintln(out) } } else { fmt.Fprintln(out, \"-1\") } } ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:5:1","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"content":"经典模板题 848. 有向图的拓扑序列 ","date":"2022-10-20","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/:5:2","tags":["算法","算法整理","搜索算法","BFS","DFS","广度优先搜索","深度优先搜索","图论","最短路","拓扑排序"],"title":"基础算法整理(七)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%83/"},{"categories":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com Hash表/哈希表 Hash表通过Hash函数映射的方式,将一个稀疏的集合存储到一个紧凑的集合中。比如一个集合的数据范围是1~10^9次方,但里面的数只有10^5个,我们就可以通过一个Hash函数(通常是取模)的方式将集合中的数映射到一个10^5的集合中,减少存储空间的浪费。 但是将一个大集合映射到小集合,必然造成信息的损失,表现到Hash表中就是Hash冲突。通过Hash后,大集合中的某些数必定被映射到了同一个点上。因此我们无法确认该点是否存在某些值,因为有多个值同时被映射过来了。 解决Hash冲突的经典解决方式(都需要额外判断): 拉链法 开放寻址法 拉链法 每一个点存储的是一个链式结构,冲突时将新值链接到前面或后面。确认是否存在该值时,可以依次判断链上的每一个节点。 开放寻址法 如果该点存在冲突,则往下或往上寻找空的位置,将新值写入。确认是否存在该值时,先找到映射的位置,然后向上或向下依次判断每一个值。 ","date":"2022-10-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/:0:0","tags":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"title":"基础算法整理(六)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/"},{"categories":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"content":"代码模板 拉链法 package main import ( \"fmt\" \"bufio\" \"os\" ) // 拉链法 const N int = 100003 var h, e, ne [N]int var idx int func insert(x int) { // x 为负数时 % N 结果为负数,加上 N 变为正数,再 % N k := (x % N + N) % N e[idx] = x ne[idx] = h[k] h[k] = idx idx++ } func query(x int) bool { k := (x % N + N) % N for i := h[k]; i != -1; i = ne[i] { if e[i] == x { return true } } return false } func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() // 将h所有槽位初始化为-1,表示空节点 for i := 0; i \u003c N; i++ { h[i] = -1 } var n int fmt.Fscan(in, \u0026n) for ; n \u003e 0; n-- { var op string var x int fmt.Fscan(in, \u0026op, \u0026x) if op == \"I\" { insert(x) } else { if query(x) { fmt.Fprintln(out, \"Yes\") } else { fmt.Fprintln(out, \"No\") } } } } 开放寻址法 package main import ( \"fmt\" \"bufio\" \"os\" ) // 开放寻址法通常开数据范围2~3倍的空间 const N int = 200003 // 用一个不存在于数据范围内的数来表示该点没有值 const null int = 0x3f3f3f3f var h [N]int // 在 h 中寻找 x 应该插入的位置 func find(x int) int { k := (x % N + N) % N // 在h中找一个x能插入的位置,如果x已经插入过了,返回插入的位置 for h[k] != null \u0026\u0026 h[k] != x { k++ // 如果到最后了,尝试从头开始查找插入的问题 if k == N { k = 0 } } return k } func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() // 初始化将所有点标记为空 for i := 0; i \u003c N; i++ { h[i] = null } var n int fmt.Fscan(in, \u0026n) for ; n \u003e 0; n-- { var op string var x int fmt.Fscan(in, \u0026op, \u0026x) k := find(x) if op == \"I\" { h[k] = x } else { if h[k] != null { fmt.Fprintln(out, \"Yes\") } else { fmt.Fprintln(out, \"No\") } } } } ","date":"2022-10-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/:0:1","tags":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"title":"基础算法整理(六)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/"},{"categories":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"content":"经典模板题 840. 模拟散列表 字符串Hash 字符串hash是一种快速判断字符串是否相等的方法。Golang中strings.Index等库均用到了字符串Hash——RK算法。详细可以看Golang中的字符串匹配——RK算法。 ","date":"2022-10-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/:0:2","tags":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"title":"基础算法整理(六)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/"},{"categories":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 100010 // P表示P进制,题目提到字符串中只包含大小写英文字母和数字,因此128可以完全存下 // ASCII码总共128,131是大于128的最小素数,用素数是因为可以有效的降低冲突的概率 // 这里还有个细节是用到了uint64 // 当字符串足够长时,产生的hash值可能会非常大,int可能会存不下,但是uint64也不一定存的下 // 经验告诉我们,当P取131或13331时,hash值mod一个2^64,在99.99%的情况下不会发生冲突 // 因此用uint64还有个好处,当uint64发生溢出时,就相当于mod了2^64,可以减少许多次计算 // 因此这个字符串hash方法,是假定不会发生冲突的 const P uint64 = 131 // h存储的是s所有前缀的hash值,预处理好之后,求某一段的hash就只需要O(1)的时间 // p存储的是P^i次方数,方便O(1)时间计算hash var h, p [N]uint64 func get(l, r int) uint64 { // 为什么是 h[r] - h[l-1] * p[r-l+1]? // 这个公式的作用是求出 r ~ l 这一段的hash值 // h[l-1] * p[r-l+1] 是把 1 ~ l - 1 这一段放大到与 1 ~ r 对齐同一位 // 比如 ABCDE 与 ABC 前三个字符是一样的,只差两位 // ABC 的 hash 值乘上 P^2 变成 ABC00,再用 ABCDE - ABC00,得到 DE 的 hash 值 // 然后再相减 return h[r] - h[l-1] * p[r-l+1] } func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var n, m int fmt.Fscan(in, \u0026n, \u0026m) var s string fmt.Fscan(in, \u0026s) s = \" \" + s // 预处理前缀hash和P进制每一位数 p[0] = 1 for i := 1; i \u003c= n; i++ { p[i] = p[i-1] * P h[i] = h[i-1] * P + uint64(s[i]) } for ; m \u003e 0; m-- { var l1, r1, l2, r2 int fmt.Fscan(in, \u0026l1, \u0026r1, \u0026l2, \u0026r2) // 因为hash冲突极大概率不存在,因此判断hash值相同就认为字符串相同 if get(l1, r1) == get(l2, r2) { fmt.Fprintln(out, \"Yes\") } else { fmt.Fprintln(out, \"No\") } } } ","date":"2022-10-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/:0:3","tags":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"title":"基础算法整理(六)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/"},{"categories":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"content":"经典模板题 841. 字符串哈希 求大于x的最小素数 ","date":"2022-10-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/:0:4","tags":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"title":"基础算法整理(六)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/"},{"categories":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"content":"代码模板 // 快速求比128数大的最小素数 for i := 128; ; i++ { flag := true for j := 2; j * j \u003c= i; j++ { if i % j == 0 { flag = false break } } if flag { fmt.Fprintln(out, i) break } } ","date":"2022-10-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/:0:5","tags":["算法","算法整理","hash","哈希表","字符串hash","RK算法"],"title":"基础算法整理(六)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%85%AD/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com Trie树/字典树 Trie是一种高效存储和查找字符串集合的数据结构。其存储形式如下图所示: 红色星号标记了存在以该词结尾的单词。 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:0:0","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"代码模板 package main import ( \"bufio\" \"fmt\" \"os\" ) const N int = 1e5 + 10 var ( // 指示son存储到哪了 idx int // 第一个维度表示 节点i // 第二个维度表示 节点i的子节点的下标 // 比如先存储了一个 只有a字母 长度为20的字符串,那么idx值为20 // 表示已经使用了20个节点 // 再存储新的字符串,将会从idx开始 // son[0][26]表示头结点的子节点 son [N][26]int // 以某个节点结尾的单词数量 cnt [N]int ) func insert(s string) { p := 0 for i := 0; i \u003c len(s); i++ { u := s[i] - 'a' if son[p][u] == 0 { // 如果没有存储过该字符,则新开一个节点存储,idx+1的含义 son[p][u] = idx + 1 idx++ } p = son[p][u] } cnt[p]++ // 以节点p结尾的单词数量 } func query(s string) int { p := 0 for i := 0; i \u003c len(s); i++ { u := s[i] - 'a' if son[p][u] == 0 { return 0 } p = son[p][u] } return cnt[p] } func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var n int fmt.Scan(\u0026n) for ; n \u003e 0; n-- { var op, s string fmt.Fscan(in, \u0026op, \u0026s) if op == \"I\" { insert(s) } else { fmt.Fprintln(out, query(s)) } } } ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:0:1","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"经典模板题 835. Trie字符串统计 208. 实现 Trie (前缀树) 143. 最大异或对 421. 数组中两个数的最大异或值 并查集 并查集通常用来: 将两个集合合并 询问两个元素是否在一个集合当中 并查集能在近乎O(1)的时间内完成这两个操作。 并查集是树形的数据结构,用数根来表示这个集合的编号,其余每个节点存储它的父节点是谁 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:0:2","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"朴素并查集 问题1:如何判断树根? 集合中只有树根的父节点等于自身,因此判断是否与父节点相同即可:if x == p[x] 问题2:如何求一个元素属于那个集合? 从num不断地向上走,一直走到树根:for x := x; x != p[x]; x = p[x] {} 问题3:如何合并两个集合? 假设有两个集合分别用编号x和y表示,则将集合x合并到y的操作为,将x的根节点指向y:p[x] = y 可以发现,并查集的时间复杂度集中在问题2上。并查集的路径压缩降低了这个时间复杂度。 有了路径压缩后,并查集查询两个元素是否在同一个集合中的时间复杂度,可以近乎看成O(1)。 在一次求元素属于那个集合的操作中,将查找路径上的所有节点直接指向根节点,本质上是降低了树的高度。 在代码实现时,一般会直接带上路径压缩。 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:1:0","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"代码模板 var p [N]int func init() { // 一开始每个元素都是一个单独的集合,自己就是自己的祖先节点 for i := 1; i \u003c= n; i++ { p[i] = i } } // 核心实现,寻找 x 的祖先节点,并且加上路径压缩 func find(x int) int { // 如果x的根节点不是,则递归寻找根节点, // 并将这条路径上的节点的父节点都赋值为根节点 if p[x] != x { p[x] = find(p[x]) } return p[x] } ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:1:1","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"经典模板题 836. 合并集合 剑指 Offer II 118. 多余的边 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:1:2","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"维护每个集合数量 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:2:0","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"代码模板 // 额外使用一个cnt,来维护这个集合的数量 var p, cnt [N]int func init() { // 一开始每个元素都是一个单独的集合,自己就是自己的祖先节点 for i := 1; i \u003c= n; i++ { p[i] = i // 初始化每个集合数量为1 cnt[i] = 1 } } // 核心实现,寻找 x 的祖先节点,并且加上路径压缩 func find(x int) int { // 如果x的根节点不是,则递归寻找根节点, // 并将这条路径上的节点的父节点都赋值为根节点 if p[x] != x { p[x] = find(p[x]) } return p[x] } // 将b加入a集合,并且更新a集合数量 func merge(a, b int) { a, b = find(a), find(b) if a != b { p[b] = a cnt[a] += cnt[b] } } ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:2:1","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"经典模板题 837. 连通块中点的数量 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:2:2","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"记录偏移量 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:3:0","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"代码模板 const N int = 50010 var p, d [N]int func find(x int) int { if x != p[x] { t := find(p[x]) d[x] += d[p[x]] p[x] = t } return p[x] } ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:3:1","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"经典模板题 240. 食物链 堆 堆是一颗完全二叉树,根据节点与其左右节点的性质,可以分为大根堆和小根堆。对于每一个节点,都小于其左右节点的堆称为小根堆,对于每一个节点,都大于其左右节点的堆称为大根堆。 堆通常需要支持如下操作: 插入一个数 求集合当中的最值 删除最值 删除任意一个元素(不常用) 修改任意一个元素(不常用) 要求执行完这些操作之后,集合仍然是一个堆。 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:3:2","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"实现细节 我们使用一维数组来存储集合,将下标为1的位置定义为根节点,那么每个节点i与其左右节点的关系为:$左节点 = 2i,右节点 = 2i+1$。 我们定义两种操作:down(u int)和up(u int),分别从u向下调整堆,和从u向上调整堆。 down会判断u是否小于其左右节点(对于小根堆而言),如果不是,则交换u与最小者,再递归调整最小者。 up会判断u是否小于其父节点(对于小根堆而言),如果不是,则交换u与其父节点,再循环调整其父节点。 我们可以组合down和up操作,来实现上述需要支持的5个操作。 堆的初始化,我们可以不断的将数插到最后,然后向上调整堆,这样的时间复杂度是O(nlogn)的。有另一种方法是O(n)的。 对于一个数组,我们从n/2 ~ 1执行down操作,$n/2$其实就是倒数第二层,这一层的节点数量为$n/4$,只需要往下down一次,因此时间复杂度是$(n/4)*1$。往上每一层的节点数量为$n/8$,需要往下down两次,时间复杂度是$(n/8)*2$,以此类推,那么总的时间复杂度就是他们的总和: $$sum((n/4)*1, (n/8)*2, (n/16)*3, …) = n$$ go语言中heap.Init函数也是采用这个实现。 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:3:3","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"代码模板 // 堆 var h [N]int // 堆存储长度 var size int // 堆初始化 O(n) func initHeap() { for i := n / 2; i \u003e 0; i-- { down(i) } } // 从u开始向上调整堆 O(logn) func up(u int) { for u / 2 \u003e 0 \u0026\u0026 h[u] \u003c h[u / 2] { h[u], h[u / 2] = h[u / 2], h[u] u /= 2 } } // 从u开始向下调整堆 O(logn) func down(u int) { t := u // 拿到 u 及左右儿子中的最小值 if u * 2 \u003c= size \u0026\u0026 h[u * 2] \u003c h[t] { t = u * 2 } if u * 2 + 1 \u003c= size \u0026\u0026 h[u * 2 + 1] \u003c h[t] { t = u * 2 + 1 } // 如果 u 已经是 三者中最小,则不需要调整堆 // 否则交换 u 与最小者,递归调整堆 if u != t { h[u], h[t] = h[t], h[u] down(t) } } ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:3:4","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"content":"经典模板题 838. 堆排序 839. 模拟堆 912. 排序数组 ","date":"2022-10-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/:3:5","tags":["算法","算法整理","Trie","字典树","并查集","heap","堆"],"title":"基础算法整理(五)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%94/"},{"categories":["算法","算法整理","质数"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com ","date":"2022-09-23","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/:0:0","tags":["算法","算法整理","质数"],"title":"基础算法整理-质数","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/"},{"categories":["算法","算法整理","质数"],"content":"质数筛 n \u003c= 10^6 时,埃氏筛要快于线性筛。 可以在预处理函数init()中先求出所需的所有质数,再进行计算。 ","date":"2022-09-23","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/:1:0","tags":["算法","算法整理","质数"],"title":"基础算法整理-质数","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/"},{"categories":["算法","算法整理","质数"],"content":"埃氏筛O(nloglogn) func getPrimes(n int) []int { np := make([]bool, n + 1) primes := make([]int, 0) for i := 2; i \u003c= n; i++ { if !np[i] { primes = append(primes, i) for j := i + i; j \u003c= n; j += i { np[j] = true } } } return primes } 求一个范围内的质数 // 筛出 [l~r] 范围内的所有质数 func getPrimes(l, r int) []int { np := make([]bool, r + 1) primes := make([]int, 0) for i := 2; i \u003c= r; i++ { if !np[i] { primes = append(primes, i) for j := i + i; j \u003c= r; j += i { np[j] = true } } } ans := make([]int, 0) for _, prime := range primes { if prime \u003e= l { ans = append(ans, prime) } } return ans } ","date":"2022-09-23","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/:1:1","tags":["算法","算法整理","质数"],"title":"基础算法整理-质数","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/"},{"categories":["算法","算法整理","质数"],"content":"线性筛O(n) // [0 ~ n] func getPrimes(n int) []int { np := make([]bool, n + 1) primes := make([]int, 0) for i := 2; i \u003c= n; i++ { if !np[i] { primes = append(primes, i) } for j := 0; primes[j] \u003c= n / i; j ++ { np[i * primes[j]] = true if i % primes[j] == 0 { break } } } return primes } ","date":"2022-09-23","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/:1:2","tags":["算法","算法整理","质数"],"title":"基础算法整理-质数","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86-%E8%B4%A8%E6%95%B0/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 链表与邻接表 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:0:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"单链表 链表通常使用结构体加指针的方式来构建: type Node struct { Val int Next *Node } 但是这种方式创建链表很慢,笔试算法中链表长度可能达到10^5甚至10^6级别,只是初始化链表就超时了。所以笔试算法通常使用数组来模拟链表,用数组模拟链表也被称为静态链表。 用数组模拟链表: // 存储节点i的值 var e = [N]int{0, 1, 2, 3, 4, 5} // 存储节点i的next,这里存储的实际是数组下标,-1表示nil var ne = [N]int{3, 1, 2, 5, 4, -1} // [0]-\u003e[3]-\u003e[1]-\u003e[2]-\u003e[5]-\u003e[4]-\u003enil 单链表最大的用途是用来写邻接表,存储树和图。 双链表通常用于优化某些问题。 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:1:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N = 100010 // 指向头节点,指示链表长度 var head, length int var e, ne [N]int func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var m int fmt.Fscan(in, \u0026m) initLinkList() for ; m \u003e 0; m-- { var op string var k, x int fmt.Fscan(in, \u0026op) if op == \"H\" { fmt.Fscan(in, \u0026x) addToHead(x) } else if op == \"D\"{ fmt.Fscan(in, \u0026k) // 如果删除的节点是头结点,直接将head指向下一位 if k == 0 { head = ne[head] } else { remove(k - 1) } } else { fmt.Fscan(in, \u0026k, \u0026x) add(k - 1, x) } } for i := head; i != -1; i = ne[i] { fmt.Fprintf(out, \"%d \", e[i]) } } // 初始化链表 func initLinkList() { head = -1 length = 0 } // 将x插到头结点 func addToHead(x int) { e[length] = x ne[length] = head head = length length++ } // 将x插到下标是k的后面 func add(k, x int) { e[length] = x ne[length] = ne[k] ne[k] = length length++ } // 删除下标k后面的一个节点 func remove(k int) { ne[k] = ne[ne[k]] } ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:1:1","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"经典模板题 826. 单链表 707. 设计链表 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:1:2","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"双链表 双链表比单链表多存储了一个前节点指针,能够在O(1)时间找到上一个节点。 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:2:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N = 100010 // l存放i节点的左节点;r存放i节点的右节点 var e, l, r [N]int var length int func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() initDuLinkNode() var m int fmt.Fscan(in, \u0026m) for ; m \u003e 0; m-- { var op string fmt.Fscan(in, \u0026op) var k, x int if op == \"R\" { fmt.Fscan(in, \u0026x) // 1表示最右端点 // l[1]表示最右端点的左侧 add(l[1], x) } else if op == \"L\" { fmt.Fscan(in, \u0026x) // 0表示最左端点 add(0, x) } else if op == \"D\" { fmt.Fscan(in, \u0026k) // 删除第k个插入的数 // 因为预先插入了两个左右端点,所以需+2 // 又因为下标从0开始,插入数从1开始,所以-1 remove(k + 1) } else if op == \"IR\" { fmt.Fscan(in, \u0026k, \u0026x) // 在第k个插入数的右侧插入x add(k + 1, x) } else if op == \"IL\" { fmt.Fscan(in, \u0026k, \u0026x) // 在第k个插入数的左侧插入x // 等价于在 第k个插入数的左节点的右侧 插入x add(l[k + 1], x) } } for i := r[0]; i != 1; i = r[i] { fmt.Fprintf(out, \"%d \", e[i]) } } func initDuLinkNode() { // 定义第一个和第二个节点为左右端点 r[0] = 1 l[1] = 0 length = 2 } // 在下标k的右边插入x func add(k, x int) { e[length] = x r[length] = r[k] l[length] = k l[r[k]] = length r[k] = length length++ } // 删除下标是k的点 func remove(k int) { r[l[k]] = r[k] l[r[k]] = l[k] } ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:2:1","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"经典模板题 827. 双链表 707. 设计链表 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:2:2","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"邻接表 邻接表存储的是一个节点和它的边。存储形式为一个矩阵,每个元素代表一个节点及它的所有边。 邻接表多用于存储树和图。 栈与队列 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:3:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"栈 一种先进后出的数据结构。 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:4:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 100010 var st [N]int var tt int = -1 func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var m int fmt.Fscan(in, \u0026m) for ; m \u003e 0; m-- { var op string fmt.Fscan(in, \u0026op) if op == \"push\" { var x int fmt.Fscan(in, \u0026x) push(x) } else if op == \"pop\" { _ = pop() } else if op == \"empty\" { fmt.Fprintln(out, empty()) } else { fmt.Fprintln(out, query()) } } } func push(x int) { tt++ st[tt] = x } func pop() int { x := st[tt] tt-- return x } func empty() string { if tt == -1 { return \"YES\" } return \"NO\" } func query() int { return st[tt] } ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:4:1","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"经典模板题 828. 模拟栈 3302. 表达式求值 剑指 Offer II 036. 后缀表达式 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:4:2","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"单调栈 通常用于解决,寻找每一个数左边离它最近的比它小的数,或寻找每一个数左边离它最近的比它大的数,或右边最近比它小小,诸如此类的问题。 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:5:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 100010 var st [N]int var tt int = -1 func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var m int fmt.Fscan(in, \u0026m) for i := 0; i \u003c m; i++ { var x int fmt.Fscan(in, \u0026x) for tt \u003e -1 \u0026\u0026 st[tt] \u003e= x { tt-- } if tt \u003e - 1 { fmt.Fprintf(out, \"%d \", st[tt]) } else { fmt.Fprint(out, \"-1 \") } tt++ st[tt] = x } } ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:5:1","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"经典模板题 830. 单调栈 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:5:2","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"队列 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:6:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"代码模板 package main import \"fmt\" const N int = 100010 var que [N]int var qh, qt int = 0, -1 func main() { var m int fmt.Scan(\u0026m) for ; m \u003e 0; m-- { var op string fmt.Scan(\u0026op) if op == \"push\" { var x int fmt.Scan(\u0026x) push(x) } else if op == \"pop\" { _ = pop() } else if op == \"empty\" { fmt.Println(empty()) } else { fmt.Println(query()) } } } func push(x int) { qt++ que[qt] = x } func pop() int { x := que[qh] qh++ return x } func empty() string { if qh \u003e qt { return \"YES\" } return \"NO\" } func query() int { return que[qh] } ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:6:1","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"经典模板题 829. 模拟队列 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:6:2","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"单调队列 经典应用是求滑动窗口中的最值。 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:7:0","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 1000010 var a [N]int var deque [N]int func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var n, k int fmt.Fscan(in, \u0026n, \u0026k) for i := 0; i \u003c n; i++ { fmt.Fscan(in, \u0026a[i]) } // 求窗口中的最小值 hh, tt int := 0, -1 for i := 0; i \u003c n; i++ { // 队头元素已经超出窗口范围,将队头元素弹出 if hh \u003c= tt \u0026\u0026 i - k + 1 \u003e deque[hh] { hh++ } // 重新维护单调队列,从队尾弹出比当前元素大的元素 for hh \u003c= tt \u0026\u0026 a[deque[tt]] \u003e= a[i] { tt-- } tt++; deque[tt] = i // 队列存储的是下标 if i \u003e= k - 1 { fmt.Fprintf(out, \"%d \", a[deque[hh]]) } } fmt.Fprintln(out) // 求窗口中的最大值 hh, tt = 0, -1 for i := 0; i \u003c n; i++ { // 队头元素已经超出窗口范围 if hh \u003c= tt \u0026\u0026 i - k + 1 \u003e deque[hh] { hh++ } // 重新维护单调队列,从队尾弹出比当前元素小的元素 for hh \u003c= tt \u0026\u0026 a[deque[tt]] \u003c= a[i] { tt-- } tt++; deque[tt] = i if i \u003e= k - 1 { fmt.Fprintf(out, \"%d \", a[deque[hh]]) } } } ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:7:1","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"content":"经典模板题 154. 滑动窗口 239. 滑动窗口最大值 kmp算法 kmp算法 ","date":"2022-09-22","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/:7:2","tags":["算法","算法整理","链表","栈","队列","单调栈","单调队列","kmp"],"title":"基础算法整理(四)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E5%9B%9B/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 双指针 双指针算法一般有两种场景: 两个指针分别指向两个不同的集合 两个指针指向同一个集合,本质是维护该集合上的一段区间 双指针一般用于将暴力穷举O(n^2)的算法,优化到O(n)。 ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:0:0","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"代码模板 package main import ( \"fmt\" ) func main() { var n int fmt.Scan(\u0026n) nums := make([]int, n) for i := 0; i \u003c n; i++ { fmt.Scan(\u0026nums[i]) } m := make([]int, 1e5 + 10) ans := 0 for i, j := 0, 0; i \u003c n; i++ { m[nums[i]]++ for m[nums[i]] \u003e 1 { m[nums[j]]-- j++ } ans = max(ans, i - j + 1) } fmt.Print(ans) } func max(a, b int) int { if a \u003e b { return a } return b } ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:0:1","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"经典模板题 799. 最长连续不重复子序列 800. 数组元素的目标和 3. 无重复字符的最长子串 2816. 判断子序列 位运算 常用的位运算操作: 取出n的二进制表示下第k位是多少——(n\u003e\u003ek)\u00261 返回n的二进制表示下最后一位1,也叫lowbit操作——n\u0026-n ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:0:2","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"取出第k位 用0表示第一位。 ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:1:0","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"代码模板 package main import \"fmt\" func main() { n := 10 for k := 3; k \u003e= 0; k-- { fmt.Print(n\u003e\u003ek\u00261) } } ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:1:1","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"lowbit 公式为:x \u0026 -x。 原理为: 负数在计算机中使用补码的方式表示,因此-x = ^x + 1,当x取反之后,x的最后的1会变成0,而最后的1后面的0会变成1,再加上1的话,会进位一直到最后的1处。因此-x的二进制表达形——最后一个1前与x全部取反,最后一个1及后面全部一致,然后x \u0026 -x就会得到最后一个1及后面的0组成的二进制数。 一般形式: x = 0b(10101010000) ^x = 0b(01010101111) ^x + 1 = 0b(01010110000) 计算过程: x \u0026 -x = 0b(10101010000) \u0026 \\ 0b(01010110000) = 0b(00000010000) lowbit的用处有很多,常见的有:快速计算一个数有多少个位为1。 ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:2:0","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"代码模板 package main import \"fmt\" // 计算二进制表示中1的个数 func main() { var n int fmt.Scan(\u0026n) for i := 0; i \u003c n; i++ { var num, res int fmt.Scan(\u0026num) for num \u003e 0 { num -= lowbit(num) res++ } fmt.Printf(\"%d \", res) } } func lowbit(x int) int { return x \u0026 -x } ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:2:1","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"经典模板题 801. 二进制中1的个数 191. 位1的个数 离散化 这里单独指整数离散化。离散化是指将一个稀疏的区间离散到一个紧凑的区间中。比如有一个区间范围为[-10^9 ~ 10^9],但是里面只有的数只有10^5范围,那么显然这个区间是稀疏的,有很多重复或空的值。离散化就是把这个稀疏空间映射到紧凑空间上,降低操作的时空间复杂度。 ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:2:2","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"代码模板 func main() { // 存储所有需要操作的下标 alls := make([]int, 0, N) // 对所有需要操作的下标排序去重 quickSort(\u0026alls, 0, len(alls) - 1) alls = unique(alls) // 二分求出离散化后的值 find(alls, x) } func unique(a []int) []int { j := 0 for i := 0; i \u003c len(a); i++ { if i == 0 || a[i] != a[i - 1] { a[j] = a[i] j++ } } // 不应该直接返回a的切片,这样会导致底层大数组的引用没有消失,gc就无法回收 // return a[:j] // 返回一个新开辟的切片,底层数组不同,gc可以将a回收掉 temp := make([]int, j) copy(temp, a[:j]) return temp } func find(a []int, x int) int { l, r := 0, len(a) - 1 for l \u003c r { mid := (l + r) \u003e\u003e 1 if a[mid] \u003e= x { r = mid } else { l = mid + 1 } } return r + 1 } ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:2:3","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"经典模板题 802. 区间和 区间合并 合并满足某种条件的区间,本质是贪心算法。 经典例题是合并有交集的区间,排序后判断左右端点重合则重新维护最小和最大端点。 ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:2:4","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"代码模板 func merge(segs []pair) []pair { // 首先从小到大排序区间 sort.Slice(segs, func(i, j int) bool { return segs[i].first \u003c segs[j].first }) temp := make([]pair, 0) // 赋予开始和结束端点最小值,避免重复 var st, ed int = math.MinInt64, math.MinInt64 // 遍历所有区间 for _, seg := range segs { // 当有区间与开始和结束端点重叠时,更新最大结束端点 if seg.first \u003c= ed { ed = max(ed, seg.second) // 如果没有重叠,说明是一个新区间,更新开始和结束端点 } else { // 避免重复 if st != math.MinInt64 { temp = append(temp, pair{st, ed}) } st = seg.first ed = seg.second } } // 避免重复,再加入最后一个区间 if st != math.MinInt64 { temp = append(temp, pair{st, ed}) } return temp } ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:2:5","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","双指针","位运算","离散化","区间合并"],"content":"经典模板题 803. 区间合并 56. 合并区间 ","date":"2022-09-19","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/:2:6","tags":["算法","算法整理","双指针","位运算","离散化","区间合并"],"title":"基础算法整理(三)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%89/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 高精度 高精度是指,不能直接用一个变量来存储的数据,这种数据有几千位甚至几万位。存储这种数据必须使用数组。高精度运算就是为这种数据做算术运算。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:0:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"1.1 大整数相加 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:1:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import \"fmt\" func main() { a, b := \"\", \"\" fmt.Scanf(\"%s\", \u0026a) fmt.Scanf(\"%s\", \u0026b) A := make([]int, 0, len(a)) B := make([]int, 0, len(b)) for i := len(a) - 1; i \u003e= 0; i-- { A = append(A, int(a[i] - '0')) } for i := len(b) - 1; i \u003e= 0; i-- { B = append(B, int(b[i] - '0')) } C := add(A, B) for i := len(C) - 1; i \u003e= 0; i-- { fmt.Print(C[i]) } } // C = A + B func add(A, B []int) (C []int) { t := 0 for i := 0; i \u003c len(A) || i \u003c len(B); i++ { if i \u003c len(A) { t += A[i] } if i \u003c len(B) { t += B[i] } C = append(C, t % 10) t /= 10 } if t == 1 { C = append(C, 1) } return C } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:1:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 791. 高精度加法 剑指 Offer II 025. 链表中的两数相加 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:1:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"1.2 大整数相减 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:2:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import \"fmt\" func main() { var a, b string fmt.Scanf(\"%s\", \u0026a) fmt.Scanf(\"%s\", \u0026b) A := make([]int, 0, len(a)) B := make([]int, 0, len(b)) for i := len(a) - 1; i \u003e= 0; i-- { A = append(A, int(a[i] - '0')) } for i := len(b) - 1; i \u003e= 0; i-- { B = append(B, int(b[i] - '0')) } if cmp(A, B) { C := sub(A, B) for i := len(C) - 1; i \u003e= 0; i-- { fmt.Print(C[i]) } } else { // A \u003c B 的情况,要加上负号 fmt.Print(\"-\") C := sub(B, A) for i := len(C) - 1; i \u003e= 0; i-- { fmt.Print(C[i]) } } } // 判断是否有 A \u003e= B func cmp(A, B []int) bool { if len(A) != len(B) { return len(A) \u003e len(B) } for i := len(A) - 1; i \u003e= 0; i-- { if A[i] != B[i] { return A[i] \u003e B[i] } } return true } // C = A - B // 应该保证 A \u003e= B func sub(A, B []int) (C []int) { t := 0 for i := 0; i \u003c len(A); i++ { t = A[i] - t if i \u003c len(B) { t -= B[i] } // 考虑了需要进位和不需要进位的情况 C = append(C, (t + 10) % 10) if t \u003c 0 { t = 1 } else { t = 0 } } // 去除前导零 for len(C) \u003e 1 \u0026\u0026 C[len(C) - 1] == 0 { C = C[:len(C) - 1] } return C } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:2:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 792. 高精度减法 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:2:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"1.3 大整数乘法 一个高精度的正整数A,乘上一个低精度的正整数b。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:3:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import \"fmt\" func main() { var a string var b int fmt.Scanf(\"%s\", \u0026a) fmt.Scanf(\"%d\", \u0026b) if b == 0 { fmt.Print(0) } else { A := make([]int, 0, len(a)) for i := len(a) - 1; i \u003e= 0; i-- { A = append(A, int(a[i] - '0')) } C := mul(A, b) for i := len(C) - 1; i \u003e= 0; i-- { fmt.Print(C[i]) } } } // C = A * b // 0 \u003c= b \u003c= 10000 func mul(A []int, b int) (C []int) { for i, t := 0, 0; i \u003c len(A) || t \u003e 0; i++ { if i \u003c len(A) { t += A[i] * b } C = append(C, t % 10) t /= 10 } return C } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:3:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 793. 高精度乘法 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:3:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"1.4 大整数除法 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:4:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import \"fmt\" func main() { var a string var b int fmt.Scanf(\"%s\", \u0026a) fmt.Scanf(\"%d\", \u0026b) A := make([]int, 0, len(a)) for i := len(a) - 1; i \u003e= 0; i-- { A = append(A, int(a[i] - '0')) } C, r := div(A, b) for i := len(C) - 1; i \u003e= 0; i-- { fmt.Print(C[i]) } fmt.Printf(\"\\n%d\", r) } // C = A / b, r = A % b // 1 \u003c= b \u003c= 10000 func div(A []int, b int) (C []int, r int) { // 高精度除法需要从最高位开始处理, // 与其他算术运算不同 for i := len(A) - 1; i \u003e= 0 ; i-- { r = r * 10 + A[i] C = append(C, r / b) r %= b } // 反转C,与其他算术运算输出逻辑保持一致 reverse(C, 0, len(C) - 1) // 去除前导零 for len(C) \u003e 1 \u0026\u0026 C[len(C) - 1] == 0 { C = C[:len(C) - 1] } return C, r } func reverse(a []int, l, r int) { for l \u003c r { a[l], a[r] = a[r], a[l] l++; r-- } } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:4:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 794. 高精度除法 前缀和与差分 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:4:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"一维前缀和 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:5:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"什么是前缀和? 设有长度为n的原数组a,则其前缀和si为原数组前i项的和——si=a1+a2+...+ai,当i=0时,s0=0。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:5:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"如何求前缀和数组? 从前往后遍历原数组,s[i]=s[i-1]+a[i]。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:5:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"前缀和的作用 前缀和一般用来在O(1)时间复杂度内求解区间和。比如求解原数组a[3]~a[10]的区间和,用前缀和求解为s[10]-s[2]。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:5:3","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 100010 var a[N]int var b[N]int func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var n, m int fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026a[i]) } for i := 1; i \u003c= n; i++ { b[i] = b[i - 1] + a[i] } for ; m \u003e 0; m-- { var l, r int fmt.Fscan(in, \u0026l, \u0026r) fmt.Fprintln(out, b[r] - b[l-1]) } } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:5:4","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 795. 前缀和 303. 区域和检索 - 数组不可变 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:5:5","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"二维前缀和 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:6:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() const N int = 1010 var a, s [N][N]int var n, m, q int fmt.Fscan(in, \u0026n, \u0026m, \u0026q) for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { fmt.Fscan(in, \u0026a[i][j]) } } for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { // 求前缀和 s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j] } } for ; q \u003e 0; q-- { var x1, y1, x2, y2 int fmt.Fscan(in, \u0026x1, \u0026y1, \u0026x2, \u0026y2) // 求子矩阵的和 fmt.Fprintln(out, s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]) } } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:6:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 796. 子矩阵的和 304. 二维区域和检索 - 矩阵不可变 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:6:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"一维差分 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:7:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"什么是差分? 差分是前缀和的逆运算,对一个差分数组求前缀和,得到原数组。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:7:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"如何求差分数组? 构造这样一个数据,对于原数组a而言,差分数组b中每一个元素,都等于b[i] = a[i] - a[i-1],其中b[1] = a[1]。 这样构造完之后,我们对其求前缀和可以发现,得到的答案就是原数组。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:7:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"差分的作用 差分数组用于解决区间加值的问题。比如给区间[l:r]中每一个数都加上C,利用差分数组可以在O(1)时间复杂度完成。 实现过程为,在差分数组b[l]处加上C,在b[r+1]处减去C,这样在还原为原数组时,区间[l:r]就施加上C的影响,在[r+1:n]减去C的影响。 公式为:b[l] += C; b[r+1] -= C。 图例——为[4:7]这个区间加上3。 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:7:3","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N = 100010 var a [N]int var b [N]int func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var n, m int fmt.Fscan(in, \u0026n, \u0026m) for i := 1; i \u003c= n; i++ { fmt.Fscan(in, \u0026a[i]) insert(i, i, a[i]) } for ; m \u003e 0; m-- { var l, r, c int fmt.Fscan(in, \u0026l, \u0026r, \u0026c) insert(l, r, c) } for i := 1; i \u003c= n; i++ { b[i] += b[i-1] fmt.Fprintf(out, \"%d \", b[i]) } } func insert(l, r, c int) { b[l] += c b[r+1] -= c } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:7:4","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 797. 差分 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:7:5","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"二维差分 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:8:0","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"代码模板 package main import ( \"fmt\" \"bufio\" \"os\" ) const N int = 1010 var a [N][N]int var b [N][N]int func main() { in := bufio.NewReader(os.Stdin) out := bufio.NewWriter(os.Stdout) defer out.Flush() var n, m, q int fmt.Fscan(in, \u0026n, \u0026m, \u0026q) // 读取原数组数据,并构建差分数组 for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { fmt.Fscan(in, \u0026a[i][j]) insert(i, j, i, j, a[i][j]) } } // q次矩阵增加c值操作 for ; q \u003e 0; q-- { var x1, y1, x2, y2, c int fmt.Fscan(in, \u0026x1, \u0026y1, \u0026x2, \u0026y2, \u0026c) insert(x1, y1, x2, y2, c) } // 构建二维前缀和数组 for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { b[i][j] += b[i-1][j] + b[i][j-1] - b[i-1][j-1] } } // q次矩阵增加c之后的答案输出 for i := 1; i \u003c= n; i++ { for j := 1; j \u003c= m; j++ { fmt.Fprintf(out, \"%d \", b[i][j]) } fmt.Fprintln(out) } } func insert(x1, y1, x2, y2, c int) { b[x1][y1] += c b[x2+1][y1] -= c b[x1][y2+1] -= c b[x2+1][y2+1] += c } ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:8:1","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","高精度","前缀和","差分"],"content":"经典模板题 798. 差分矩阵 ","date":"2022-09-15","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/:8:2","tags":["算法","算法整理","高精度","前缀和","差分"],"title":"基础算法整理(二)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%BA%8C/"},{"categories":["算法","算法整理","排序","二分"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 1. 排序 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:0:0","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"1.1 快速排序 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:1:0","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"代码模板 func sortArray(nums []int) []int { qucikSort(nums, 0, len(nums) - 1) return nums } func qucikSort(nums []int, l, r int) { if l \u003e= r { return } pivot := nums[(l + r) / 2] i, j := l - 1, r + 1 for i \u003c j { for i++; nums[i] \u003c pivot; i++ {} for j--; nums[j] \u003e pivot; j-- {} if i \u003c j { nums[i], nums[j] = nums[j], nums[i] } } qucikSort(nums, l, j) qucikSort(nums, j + 1, r) } ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:1:1","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"经典模板题 912. 排序数组 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:1:2","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"1.2 归并排序 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:2:0","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"代码模板 递归版 func sortArray(nums []int) []int { mergeSort(nums, 0, len(nums) - 1) return nums } func mergeSort(nums []int, left, right int) { if left \u003e= right { return } mid := (left + right) \u003e\u003e 1 mergeSort(nums, left, mid) mergeSort(nums, mid + 1, right) temp := merge(nums[left:mid+1], nums[mid+1:right+1]) copy(nums[left:], temp) } func merge(left, right []int) []int { temp := make([]int, len(left) + len(right)) i, j, k := 0, 0, 0 for i \u003c len(left) || j \u003c len(right) { if i \u003e= len(left) { copy(temp[k:], right[j:]) return temp } if j \u003e= len(right) { copy(temp[k:], left[i:]) return temp } if left[i] \u003c= right[j] { temp[k] = left[i] k++; i++ } else { temp[k] = right[j] k++; j++ } } return temp } 迭代版 func sortArray(nums []int) []int { mergeSort(nums) return nums } func mergeSort(nums []int) { for step := 1; step \u003c len(nums); step += step { for i := 0; i + step \u003c len(nums); i += 2 * step { temp := merge(nums[i:i+step], nums[i+step:min(i+2*step, len(nums))]) copy(nums[i:], temp) } } } func merge(left, right []int) []int { temp := make([]int, len(left) + len(right)) i, j, k := 0, 0, 0 for i \u003c len(left) || j \u003c len(right) { if i \u003e= len(left) { copy(temp[k:], right[j:]) break } if j \u003e= len(right) { copy(temp[k:], left[i:]) break } if left[i] \u003c= right[j] { temp[k] = left[i] k++; i++ } else { temp[k] = right[j] k++; j++ } } return temp } func min(a, b int) int { if a \u003c b { return a } return b } ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:2:1","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"经典模板题 912. 排序数组 剑指 Offer 51. 数组中的逆序对 2. 二分 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:2:2","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"2.1 整数二分 二分的本质是将一个区间分为两块,一块满足要求,另一块不满足要求,然后就可以寻找这两块区间的边界。 每次缩小的时候,都要确保剩余区间内有答案。 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:3:0","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"代码模板 // 左闭右开区间 // lowerBound 返回最小的满足 nums[i] \u003e= target 的 i // 如果数组为空,或者所有数都 \u003c target,则返回 nums.length // 要求 nums 是非递减的,即 nums[i] \u003c= nums[i + 1] func lowerBound(a []int, x int) int { // [l : r) // 循环不变量: // a[l - 1] \u003c x // a[r] \u003e= x l, r := 0, len(a) for l \u003c r { m := (l + r) \u003e\u003e 1 if a[m] \u003c x { l = m + 1 // 范围缩小到 [m + 1 : r) } else { r = m // 范围缩小到 [l : m) } } return l // 或者返回 r } golang的标准库中内置了二分查找的API——sort.SearchInts(),其实现代码跟模板代码类似,也是lower_bound的写法,但是进行了抽象。 把if a[m] \u003c x这一句代码进行了抽象,改为传入一个闭包函数,当条件不满足时,缩小左区间,否则缩小右区间。 // src/sort/search.go;l=124 func SearchInts(a []int, x int) int { // 闭包函数把 x 包进去了 return Search(len(a), func(i int) bool { return a[i] \u003e= x }) } func Search(n int, f func(int) bool) int { // 定义循环不变量 // Define f(-1) == false and f(n) == true. // Invariant: f(i-1) == false, f(j) == true. i, j := 0, n for i \u003c j { h := int(uint(i+j) \u003e\u003e 1) // avoid overflow when computing h // i ≤ h \u003c j if !f(h) { i = h + 1 // preserves f(i-1) == false } else { j = h // preserves f(j) == true } } // i == j, f(i-1) == false, and f(j) (= f(i)) == true =\u003e answer is i. return i } 一些常用的使用方法 寻找x的左边界: l := sort.SearchInts(nums, x),如果不存在x,则l = len(nums) 寻找x的右边界: r := sort.SearchInts(nums, x + 1) - 1,如果不存在x,则r = len(nums) 寻找第一个\u003ex的: sort.SearchInts(nums, x + 1) 寻找第一个\u003cx的: sort.SearchInts(nums, x) - 1 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:3:1","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"关于 lower_bound 和 upper_bound lower_bound是寻找第一个\u003e=x的下标 upper_bound是寻找第一个\u003ex的下标 golang实现lower_bound和upper_bound // 如果不存在 x,返回的下标是 0 func lowerBound(a []int, x int) int { return sort.Search(len(a), func(i int) bool { return a[i] \u003e= x }) } func upperBound(a []int, x int) int { return sort.Search(len(a), func(i int) bool { return a[i] \u003e x }) } lower_bound() - 1是\u003cx的最大下标,-1表示不存在 upper_bound() - 1是\u003c=x的最大值下标,-1表示不存在 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:3:2","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"关于循环不变量 循环不变量指的是在循环中,性质不变的量。 比如我们定义L的左侧是\u003cx的,R的右侧是\u003e=x的,保证这两个变量的性质,在循环时和循环后不改变,这就叫循环不变量。 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:3:3","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"模板实践 34. 在排序数组中查找元素的第一个和最后一个位置 func searchRange(nums []int, target int) []int { // 寻找左边界 l := lowerBound(nums, target) if l == len(nums) || nums[l] != target { return []int{-1, -1} } // 左边界存在,右边界肯定存在 // 寻找右边界,第一个 \u003e=target+1 的下标,这个下标-1 就是 target 的右边界 r := lowerBound(nums, target + 1) - 1 return []int{l, r} } func lowerBound(a []int, x int) int { l, r := 0, len(a) for l \u003c r { m := (l + r) \u003e\u003e 1 if a[m] \u003c x { l = m + 1 } else { r = m } } return l } 用标准库函数 func searchRange(nums []int, target int) []int { l := sort.SearchInts(nums, target) if l == len(nums) || nums[l] != target { return []int{-1, -1} } r := sort.SearchInts(nums, target + 1) - 1 return []int{l, r} } ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:3:4","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"经典模板题 704. 二分查找 34. 在排序数组中查找元素的第一个和最后一个位置 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:3:5","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"2.2 浮点数二分 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:4:0","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"代码模板 func mySqrt() { var x float64 fmt.Scanf(\"%f\", \u0026x) var l, r float64 = 0, x for r - l \u003e 1e-8 { mid := (l + r) / 2 if mid * mid \u003e= x { r = mid } else { l = mid } } fmt.Printf(\"%f\", l) } ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:4:1","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["算法","算法整理","排序","二分"],"content":"经典模板题 69. x 的平方根 790. 数的三次方根 ","date":"2022-09-12","objectID":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/:4:2","tags":["算法","算法整理","排序","二分"],"title":"基础算法整理(一)","uri":"/%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95%E6%95%B4%E7%90%86%E4%B8%80/"},{"categories":["Redis","必知必会"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com Redis ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:0:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Redis如何通过Key找到对应的Value ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:1:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Key的设计原则 Key不宜太长,大于1024字节会显著影响Key的查找和匹配性能 Key不宜太短,短Key能减少内存消耗和提升查找性能,需要在可读性和性能直接寻找平衡点 Key采用:区分层级,如user:1:info,表示用户编号为1的信息,用-或.来连接多词,如comment:4321:reply-to或comment:4321:reply.to表示评论编号4321回复的对象 Key最大允许的size为512MB ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:2:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Redis支持的数据结构 字符串(String) 列表(List) 集合(Set) 有序集合(Sorted Set) 哈希表(Hash) 数据流(Stream) 地理空间(Geospatial) 基数统计(HyperLogLog) 位图(Bitmaps) 位域(Bitfields) ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:3:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Redis命令执行的结果 执行结果的几种可能 成功,返回值1 失败,返回值0 错误,报错并打印错误信息 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:4:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"字符串操作 document: https://redis.io/commands/?group=string 字符串是Redis中绝大多数数据结构的底层类型 查看使用说明 help @string 增/改 SET user:1:info '{\"username\": \"joy\", \"age\": 12}' # 后面加上EX选项,可以设置Key的过期时间,单位秒 SET user:1:info '{\"username\": \"joy\", \"age\": 12}' EX 10 # 后面加上PX选项,可以设置Key的过期时间,单位毫秒 SET user:1:info '{\"username\": \"joy\", \"age\": 12}' PX 1000 # 后面加上EXAT选项,可以设置Key的在指定的时间戳过期,单位秒 # 下面设置Key在2023-5-23 11:52:10过期 SET user:1:info '{\"username\": \"joy\", \"age\": 12}' EXAT 1684813930 # 后面加上PXAT选项,可以设置Key的在指定的时间戳过期,单位毫秒 # 下面设置Key在2023-05-23 11:53:53.364过期 SET user:1:info '{\"username\": \"joy\", \"age\": 12}' PXAT 1684814033364 # 后面加上NX选项,当Key不存在时创建 SET user:1:info '{\"username\": \"joy\", \"age\": 12}' NX EX 10 # 后面加上XX选项,当Key存在时覆盖 SET user:1:info '{\"username\": \"joy\", \"age\": 12}' XX EX 10 # 加上GET选项,当key存在时,返回旧值,不存在时返回nil SET user:1:info '{\"username\": \"joy\", \"age\": 18}' GET XX EX 10 # KEEPTTL选项,在修改value时,保持其原有的过期时间 SET user:1:info '{\"username\": \"bob\", \"age\": 18}' GET XX KEEPTTL # 设置多个KeyValue对 MSET user:1:age 18 user:2:age 19 user:3:age 20 查 GET user:1:info # 获取多个Key的value MGET user:1:info user:2:info user:3:info # 检查Key是否存在,1-存在 0-不存在 EXISTS user:1:info # 检查Key对应的类型 TYPE user:1:age # 返回value的长度 STRLEN user:1:info 删 # 支持删除多个 DEL user:1:info user:2:info user:3:info 自增/增加任意大小 # 如果不存在这个key,先创建并设置value为0,然后自增1 # 如果value不为整数或者超过int64的范围,则报错 INCR views:page:2 INCRBY views:page:2 10 自减/减少任意大小 # 如果不存在这个key,先创建并设置value为0,然后自减1 # 如果value不为整数或者超过int64的范围,则报错 DECR views:page:2 DECRBY views:page:2 10 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:5:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"使用Redis实现分布式锁 Redis的SET命令有个选项是NX,表示当Key不存在时才创建,又因为Redis是单线程模型,同一时间点只有一条语句在执行,天然具有原子性,因此可以用来实现分布式锁。 我们用SET lock-key lock-value NX命令来创建一个lock-key表示一个锁,拿到锁的用户才能继续执行逻辑,拿不到锁的用户自旋等待获取锁。然而这种设计下,拿到锁的程序突然宕机,会形成死锁。 要解决这个问题我们可以加上一个过期时间,SET lock-key lock-value NX EX 10命令表示lock-key这个锁10秒后会自动释放,这样就避免了因获取到锁的程序宕机无法释放锁形成的死锁问题。 然而又有新的问题。如果过期时间到了,程序1仍然没有执行完主动释放锁,而程序2获取到锁执行的时候,程序1执行完了,然后去释放锁,程序3又获取到了锁,程序2执行完了释放锁。。。如此反复下去,锁根本无法保证数据安全。 要解决这个问题,我们可以给锁加上唯一标识,释放锁时判断锁还是不是自己的,如果不是就不执行释放。SET lock-key unique-id NX EX 10命令,表示lock-key这个锁由unique-id这个用户持有,释放锁时可以根据unique-id来判断锁是不是自己的。 然而判断锁是否是自己的,和释放锁是两条指令,不是原子操作。极端情况下,程序1判断锁是自己的,发送释放锁请求,如果释放锁请求到的比较晚,这个期间内锁到期被自动释放了,程序2获取到了锁,此时程序1释放锁的请求才到达,就会造成释放的锁是程序2的。 要解决这个问题,我们必须让判断锁和是否锁这两个操作是原子的。Redis中有事务和Lua脚本来保证多条指令的原子性。下面以Lua脚本为例。 // 释放锁时,先比较 unique_id 是否相等,避免锁的误释放 if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end 然而还有问题。如果程序1没运行完,锁被自动释放了,程序2拿到锁开始运行,这不就变成两个程序并行执行了吗?并行情况下保证不了数据竞争啊。这种情况下,光靠Redis解决不了了,需要引入监控机制,如果持有锁的程序还在正常执行,那就不断更新过期时间,直到该程序执行完成或者宕机。Redisson框架就是Redis官方推荐的分布式锁方案。 上面谈论的还只限于单机情况,如果是Redis集群,涉及到集群间数据同步问题复杂度又上一个数量级。好在Redisson框架仍然支持集群,是一个非常成熟的分布式锁框架。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:5:1","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"列表操作 document: https://redis.io/commands/?group=list Redis的列表通过链表实现,插入时间O(1) 增加元素 # 从右边追加元素 rpush user:1:follower 4 5 6 # 从左边追加元素 lpush my-list 1 2 \"hello\" \"done\" 获取元素 # 获取列表长度 LLEN user:1:follower # 获取全部元素,-1表示最后一个元素,闭区间 LRANGE user:1:follower 0 -1 # 获取指定范围的元素 LRANGE user:1:follower 0 3 LRANGE user:1:follower -5 -1 # pop操作,如果没有则元素返回nil LPOP user:1:follower RPOP user:1:follower # 阻塞式的pop操作,没有元素时阻塞 # 如果给定多个列表,那么会顺序的从第一个非空列表中获取值 # timeout 表示阻塞时间,接受双精度的值,单位为秒,0表示无限 BLPOP list1 list2 ... timeout # POPPUSH操作,Redis计划使用LMOVE指令代替RPOPLPUSH指令 # 把源列表——user:1:follower左边的元素POP,添加到目标列表——user:1:follower的右边 # 实现了一个循环队列的功能 # LMOVE指令还会返回POP出来的值 LMOVE user:1:follower user:1:follower LEFT RIGHT # 阻塞式的LMOVE,当源列表没有元素时,阻塞 BLMOVE user:1:follower user:1:follower LEFT RIGHT timeout 删除元素 # 移除[0~3]这个闭区间之外的元素 LTRIM user:1:follower 0 3 # 移除等于value的元素 # count支持3种值 # count=0 表示查找整个列表,移除所有等于value的元素 # count\u003e0 表示从列表头开始查找,移除count个等于value的元素 # count\u003c0 表示从列表尾开始查找,移除count个等于value的元素 LREM user:1:follower count value ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:6:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"集合 集合是无序集合,Redis集合支持进行集合运算,比如:交集、并集和差集 # 新增元素 SADD user:1:follow 10 11 12 SADD user:2:follow 11 13 15 # 检查 user1 是否关注 11 号用户,返回1表示关注 SISMEMBER user:1:follow 11 # 获取 user1 和 user2 的共同关注 # 集合运算非常耗时,大集合进行操作时会阻塞Redis很长时间 SINTER user:1:follow user:2:follow # 多个集合的交集存放到新集合dist中 SINTERSTORE dist user:1:follow user:2:follow user:3:follow # 获取集合元素个数 SCARD user:1:follow # 获取集合所有元素,一次性返回所有元素,结果集庞大 SMEMBERS user:1:follow # 集合迭代器 # cursor 表示游标开始的位置,pattern 表示匹配值得表达式,支持Linux中的匹配,count 表示匹配几个值,默认10个 # 每次调用 SSCAN 命令会返回两个值 # 第一个值是游标下次开始的位置,第二个值是扫描到的值得列表 SSCAN user:1:follow cursor MATCH pattern COUNT count # 删除集合内元素,成功删除至少一个元素返回1,无元素删除返回0 SREM user:1:follow 10 20 30 # 随机弹出count个元素 SPOP user:1:follow count # 随机返回count个元素 SRANDMEMBER user:1:follow count ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:7:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"哈希 # 添加元素 HSET user:1:info username joy age 18 city LA email xxx@xx.com # 获取元素 HGET user:1:info username HMGET user:1:info username age # 获取所有键值对 HGETALL user:1:info # 获取所有键 HKEYS user:1:info # 获取所有值 HVALS user:1:info # 获取键值对个数 HLEN user:1:info # 增加数值value的计数 # 给年龄增加10 HINCRBY user:1:info age 10 # 删除键值对 HDEL user:1:info email city ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:8:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"有序集合 有序集合类似于集合,但是支持给每个元素设置分数,根据分数来排序,如果分数一致,则根据元素的字典序排序 # 添加元素 ZADD players:rank 100 play1 97 play2 88 play3 # NX子选项,要添加的元素不存在才添加 ZADD players:rank NX 200 play4 # XX子选项,要添加的元素存在才更新 ZADD players:rank XX 150 play1 # LT子选项,要添加的元素存在且score小于200才更新 ZADD players:rank XX LT 200 play1 # GT子选项,要添加的元素存在且score大于100才更新 ZADD players:rank XX GT 100 play1 # 获取元素,基于排名 # 获取前三名的元素和其分数 ZRANGE players:rank 0 2 WITHSCORES # 获取元素,基于排名 # 获取所有名次的元素和其分数 ZRANGE players:rank 0 -1 WITHSCORES # 获取元素,基于分数 # 获取分数在 [100 ~ 200] 之间的元素及其分数 ZRANGE players:rank 100 200 BYSCORE WITHSCORES # 获取元素,基于分数 # 获取分数在 [-inf ~ +inf] 之间的元素及其分数 ZRANGE players:rank -inf +inf BYSCORE WITHSCORES # REV 子选项,可以反转排序 # 默认从小到大,反转为从大到小 ZRANGE players:rank 0 -1 REV WITHSCORES # 删除元素 ZREM players:rank play1 play2 # 返回集合元素个数 ZCARD players:rank # 并集计算(相同元素分值相加) # numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积 ZUNIONSTORE destkey numberkeys key [key...] # 交集计算(相同元素分值相加) # numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积 ZINTERSTORE destkey numberkeys key [key...] ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:9:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"流 专门用于消息队列的数据结构,支持消息持久化,消息唯一id,消费确认,消费组,重复消费等功能。 # 往流中添加消息,*表示使用自动生成的id,也可以自己指定 # 添加成功返回消息id,自动生成的消息id已 '-' 分割可以分为两部分 # 前半部分为以毫秒为单位的当前服务器时间戳 # 后半部分为在当前毫秒的第几条数据,0表示当前毫秒的第0条数据 XADD user:1:messages * chat-username user2 chat-user-id 2 chat-user-icon http://chat-user-icon/user/2 # 使用NOMKSTREAM子选项,阻止流不存在时的自动创建 XADD user:2:messages NOMKSTREAM * chat-username user2 chat-user-id 2 chat-user-icon http://chat-user-icon/user/2 # 使用MAXLEN子选项,可以对流进行裁剪 # 如果使用 = 运算符,那么流的长度为min(流原始长度, 指定长度) XADD user:2:messages MAXLEN = 10 * chat-username user2 chat-user-id 2 chat-user-icon http://chat-user-icon/user/2 # 如果使用 ~ 运算符,那么流的长度,会在保证性能的情况下尽可能\u003e=指定长度 XADD user:2:messages MAXLEN ~ 10 * chat-username user2 chat-user-id 2 chat-user-icon http://chat-user-icon/user/2 # 使用MINID子选项,可以对流进行裁剪,MINID的裁剪基于消息ID进行裁剪 # 如果使用 = 运算符,那么流的最老id为min(流原始最老id, 指定id) XADD user:2:messages MINID = 1684924529514-0 * chat-username user2 chat-user-id 2 chat-user-icon http://chat-user-icon/user/2 # 读取流中的数据 # 从指定id开始读取后面的所有数据,0表示从头开始 XREAD STREAMS user:2:messages 0 # 使用COUNT子选项,可以指定读取条数 XREAD COUNT 2 STREAMS user:2:messages 0 # 使用BLOCK子选择,可以阻塞指定毫秒时长,等待读取指定消息id之后的数据 XREAD BLOCK 100000 STREAMS user:2:messages 1684925552237-0 # 读取多个流的消息 # 阻塞100000ms,最多读取100条消息,从user:1:messages和user:2:messages两个流的最新消息开始读,'$' 表示最新消息 XREAD BLOCK 100000 COUNT 100 STREAMS user:1:messages user:2:messages $ $ # 读取流中的所有数据 XRANGE user:2:messages - + # 使用COUNT子选项,实现迭代器的功能 # 第一次迭代,范围为[- ~ +],取出第一条数据 # 第二次迭代,范围为(第一条数据id ~ +],取出第二条数据 # Redis中用'('表示开区间 XRANGE user:2:messages (第一条数据id + COUNT 1 # 支持从某个时间点开始 # 因为数据id是:毫秒时间戳-序号 的格式,因此可以给定时间,来获取时间段内的数据 XRANGE user:2:messages 1684924529514 1684925302364 COUNT 10 # 获取单条数据,两个参数指定一样的数据id即可 XRANGE user:2:messages 1684925552237-0 1684925552237-0 # 流的长度 XLEN user:2:messages # 删除数据 XDEL user:2:messages 1684925552237-0 1684925552237-1 消费组,创建一个从流指定id开始读取的组,组内可以有多个消费者,消费方式为扇出(fan-out),也就是轮流消费。可以创建多个消费组,不同消费组内的消费者可以重复消费同一条数据。 # 创建一个从user:2:messages流的1684925552237-0数据id之后开始消费的消费组user:2:consumer-group:1 XGROYP CREATE user:2:messages user:2:consumer-group:1 1684925552237-0 # 创建一个从头开始消费的消费组 XGROYP CREATE user:2:messages user:2:consumer-group:1 0 # 创建一个从最后一条数据id之后开始消费的消费组 XGROUP CREATE user:2:messages user:2:consumer-group:1 $ # 消费者consumer:1从消费组中第一条未被消费的数据开始读取 # '\u003e' 表示第一条未被消费的数据,也可以指定数据id # COUNT子选项指定读取的条数,不指定则一直读取到最后 # 一个消费者消费完数据之后,这个消费组内的数据会被打上\"已读取\"的标志 # 其他消费这就无法再消费这些数据 XREADGROUP GROUP user:2:consumer-group:1 consumer:1 COUNT 10 STREAMS user:2:messages \u003e # BLOCK子选项,可以指定阻塞时长,单位ms XREADGROUP GROUP user:2:consumer-group:1 consumer:1 COUNT 10 BLOCK 10000 STREAMS user:2:messages \u003e # NOACK子选项 # 消费组中的数据,被读取后会打上已读取的标志。 # 被标记为已读取的消息会被记录在该消费组的PENDING List中,并没有真正的删除。 # 要等到消费者显式的ACK,才会将PENDING List中该数据id删除。 # 这种设计是为了防止消费者读取了数据,但是在进行数据处理时程序崩溃了,数据并没有被真正的消费掉 # 程序恢复后还能够继续消费PENDING List中未ACK的数据 # NOACK子选项的作用是,消费者读取到数据,就立马ACK。使用于允许消息丢失的场景 XREADGROUP GROUP user:2:consumer-group:1 consumer:1 COUNT 10 NOACK STREAMS user:2:messages \u003e # ACK消息已消费 # user:2:messages流的user:2:consumer-group:1组中给定的数据id已消费 # 这些数据将会从PENDING List中被删除 XACK user:2:messages user:2:consumer-group:1 1684925552237-0 1684925552237-1 1684925552237-2 # 查看PENDING List中未ACK的数据,还能看到是哪个消费者读取了这条数据 XPENDING user:2:messages user:2:consumer-group:1 - + 10 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:10:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"地理空间 专门用于存储和计算地理坐标的数据结构。存储经纬度坐标,计算并给出指定范围内的坐标有哪些 # 存储坐标 GEOADD locations:ca NX -122.27652 37.805186 station:1 -122.2674626 37.8062344 station:2 # 计算给定经纬度坐标5km半径内的所有坐标,还支持m为单位 # WITHDIST指示返回坐标距离中心点的距离 # WITHCOORD指示返回搜索到的坐标的经纬度 # ASC|DESC指示按照离给定坐标为中心的距离进行排序,ASC从近到远,DESC反之 GEOSEARCH locations:ca FROMLONLAT -122.2612767 37.7936847 BYRADIUS 5 km ASC WITHDIST WITHCOORD # 计算给定已存储坐标500m半径内的所有坐标 GEOSEARCH locations:ca FROMMEMBER station:2 BYRADIUS 500 m WITHDIST # BYBOX子选项,支持在给定长宽的矩形内进行搜索 GEOSEARCH locations:ca FROMMEMBER station:2 BYBOX 400 400 km WITHDIST # 查询指定坐标的经纬度 GEOPOS locations:ca station:1 station:2 # 查询两个坐标之间的km距离,支持m GEODIST locations:ca station:1 station:2 KM ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:11:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"位图 # 设置值 SETBIT users:login-status 10002 1 # 获取值 GETBIT users:login-status 10002 # 统计值为1的个数,[0 ~ -1]表示全区间 BITCOUNT users:login-status 0 -1 # 统计第一个0/1出现的位置,支持指定区间[0 ~ -1] # 返回的定位总是从0开始算的绝对定位 # BIT子选项表示以BIT位来计数,100 200 BIT 表示[100 ~ 200]这个区间内第一个0/1出现的位置 # 还支持BYTE以字节来计数,BYTE为默认选项 BITPOS users:login-status 1 100 200 BIT ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:12:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"设置Key的过期时间 EXPIRE指令可以给存在的Key设置过期时间,如果Key不存在或者为空,那么直接返回0 # 给列表设置10.10秒过期时间 LPUSH users \"bob\" \"joy\" \"may\" EXPIRE users 10.10 # [NX | XX | GT | LT] 子选项 # NX 子选项表示当Key没有过期时间时,设置才生效 # XX 子选项表示当Key没有过期时,设置才生效 # GT 子选项新过期时间大于Key过期时间时,设置才生效 # LT 子选项新过期时间小于Key过期时间时,设置才生效 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:13:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"位图、布隆过滤器和布谷鸟过滤器 位图只能存储数值,布隆过滤器则可以存储字符串 布隆过滤器是把要存储的值进行多次Hash取模,映射到位图的某个bit位。下次检查该值是否存在时,同样进行多次Hash取模,如果每一个bit位都存在,则该值可能存在,反之则一定不存在。 布隆过滤器可以用较少的空间来过滤值是否存在,缺点是存在误判率,而且值不能删除,使用时间长了之后,过滤器中大多数位置都被填充上了值,误判率上升。 布隆过滤器在Redis中以Module的形式存在,需要单独安装,下载地址: https://redis.io/resources/modules/ ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:14:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Redis用途 缓存:单纯作为缓存使用,减少DB的压力 消息中间件:使用Pub/Sub功能作为消息中间件使用 内存数据库:把Redis当作数据库使用,需要配合AOF和RDB的数据持久化,以及哨兵或集群来实现 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:15:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Key删除策略 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:16:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"过期删除策略 过期删除策略是当Key达到过期时间后,如何对过期的Key进行删除的策略。 定时删除 Key过期时间一到就删除。 优点:对内存友好,存活的Key都是有用的 缺点:对CPU不友好,频繁的执行删除任务,特别是内存还很空闲而CPU繁忙时,有大批量的删除任务 惰性删除 不主动删除,当访问该Key过期时,将其删除。 优点:对CPU友好,只会占用很少的CPU资源 缺点:对内存不友好,很多过期Key无法及时删除,如果一个Key过期后永远不访问了,会造成内存泄漏 定期删除 每隔一段时间删除过期Key,配置文件中默认配置为hz 10,表示每隔10s取出一批量的Key,删除其中过期的。Redis保证了删除这一批量的过期Key不超过25ms,防止线程卡顿 优点:平均了内存负载和CPU负载 缺点:两头都不占优 Redis过期删除策略 Redis选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:16:1","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"内存淘汰策略 内存淘汰策略是当Redis使用的内存超过配置文件设置的最大内存时,如何选择淘汰哪些Key的策略。 Redis默认内存淘汰策略为不淘汰,最大允许使用内存为: 32位系统——\u003e3G 64位系统——\u003e无限制 设置Redis最大允许使用内存:配置文件中配置maxmemory 4GB/4096MB # 查看已使用的内存 INFO MEMORY # 查看最大允许使用内存 CONFIG GET MAXMEMORY # 查看内存淘汰策略 CONFIG GET MAXMEMORY-POLICY Redis支持的所有内存淘汰策略 noeviction:不淘汰,默认的淘汰策略 volatile-random:在设置了过期时间的键值中,随机淘汰任意键值 volatile-ttl:在设置了过期时间的键值中,优先淘汰更早过期的键值 volatile-lru:在设置了过期时间的键值中,淘汰最久未使用的键值 volatile-lfu:在设置了过期时间的键值中,淘汰最少使用的键值 allkeys-random:不关心是否设置过期时间,随机淘汰任意键值 allkeys-lru:不关心是否设置过期时间,淘汰最久未使用的键值 allkeys-lfu:不关心是否设置过期时间,淘汰最少使用的键值 设置Redis内存淘汰策略:配置文件中配置maxmemory-policy allkeys-lfu ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:16:2","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"阿里云Redis产品选型 分为社区版和企业版。社区版完全兼容开源Redis,企业版增强了很多能力和性能。 产品可选: 标准架构:主从架构 集群架构:Redis的集群模式,数据分片,高可用,主从自动切换,使用于读写高QPS场景 读写分离架构:写节点1个,读节点多个,只读节点采用链式复制,使用于读多写少场景 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:17:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Redis事务 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:18:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"数据持久化、恢复及迁移 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:19:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"数据持久化 AOF 只追加(Append Only File),在Redis执行每一条写操作后,将该操作的命令追加到文件中,恢复时就读取并执行该文件的命令,这样就能够完全恢复Redis的数据。 启用AOF AOF持久化默认不开启,在配置文件中添加如下配置,开启AOF dir /var/lib/redis appendonly yes appenddirname appendonlydir appendfilename appendonly.aof # always|everysec|no appendfsync everysec AOF的三种回写机制 always:写操作完成同步写盘,最大限度保证数据不丢,性能差 everysec:写操作完成将写指令写入缓冲区,每秒落一次盘,性能适中,可能丢失1s的数据 no:写操作完成将写指令写入缓冲区,由操作系统控制合适将缓冲区刷入磁盘,性能好,可能丢失很多数据 AOF重写机制 官方文档:https://redis.io/docs/management/persistence/ AOF因为所有写指令都会写入文件,随着时间文件会越来越大,AOF文件过大会导致恢复Redis时时间过长。 Redis提供AOF重写机制,当达到设定的重写阈值(auto-aof-rewrite-min-size 64mb)时,触发重写任务,来压缩AOF文件。 Redis7.0之前的重写任务会读取当前所有的Key,将所有Key和其Value写入到新的AOF文件,等到全部Key记录完成,替换之前的AOF文件,这个替换是原子操作。先写入新文件是为了避免重写失败,造成对原有文件的破坏。 Redis7.0之后的重写任务,主进程会新开一个.incrAOF文件继续执行写入,子进程执行重写生成新的.base快照文件,写入完成后会执行原子性的替换操作。 AOF重写任务是非常耗时的,所以是非阻塞的,会启用一个新的进程来执行重写任务。 使用进程而非线程的原因在于,线程之间是共享内存的,在修改内存数据时,需要加锁保护,而进程的共享内存是只读的,如果修改会发生写时复制,生成新的副本。 执行BGREWRITEAOF命令来主动触发AOF重写程序。 AOF文件格式 Redis7.0之前AOF是以单文件的形式存在的,7.0之后改成了多AOF文件,这是不向下兼容的。总共有三类AOF文件: appendonly.aof.1.base.rdb/aof:代表初始快照,格式可能是AOF或者RDB的 appendonly.aof.1.incr.aof:表示从初始快照开始的增量变化aof文件,该文件可能有多个 appendonly.aof.manifest:以上的文件可以分开存放,manifest清单用于追踪他们的位置 AOF文件修复 追加命令被截断了 如果因为某些原因(比如磁盘满了),导致AOF文件追加写入时失败了,只写入了半截的命令。可以尝试执行redis-check-aof --fix \u003cfilename\u003e命令生成自动修复的AOF文件,再使用diff命令比较两个文件的差异。 文件损坏了 执行redis-check-aof --fix \u003cfilename\u003e命令查看损坏的地方,尝试手动修复。 RDB RDB文件是快照文件,将Redis当前内存存储的数据用二进制的方式存储下来,RDB文件相比AOF文件更加紧凑,只存储了当前数据的最终状态,因此使用RDB文件来恢复数据是非常快的。 启用RDB RDB默认配置为:save 3600 1 300 100 60 10000,其含义如下 3600 1:3600s内至少有1个数据改变,就保存快照 300 100:300s内是少有100个数据改变,就保存快照 60 10000:60s内至少有10000个数据改变,就保存快照 如果希望关闭快照,使用配置:save \"\" RDB快照生成流程 基于Redis进程生成子进程 子进程将数据写入临时RDB文件 子进程完成数据写入后将新文件替换旧RDB文件 子进程写入时同样使用到了写入复制技术。 主动执行快照生成 主动保存快照文件有两个命令,SAVE和BGSAVE,他们的区别如下: SAVE:同步的保存,会阻塞主进程执行 BGSAVE:后台子进程执行保存,不会阻塞主进程 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:19:1","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"数据恢复 如果想要建立一个当前Redis实例的副本,应该使用主从的方式建立。当然也可以使用当前实例的RDB文件来快速启动一个非同步的副本。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:19:2","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"数据迁移 拷贝RDB或AOF文件来迁移。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:19:3","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"高可用方案 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:20:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"主从 主从能完成数据备份的功能。 从库与主库建立联系,可以在配置文件中加上:replicaof 192.168.1.1 6379命令,也可以直接在命令行中执行。主从建立流程和具体步骤如下: 从服务器给主服务器发送psync ? -1命令,表示进行数据同步 因为是第一次同步,不知道主服务器的runID,所以用?代替,-1则表示同步点 主服务器响应FULLRESYNC \u003crunID\u003e \u003coffset\u003e 响应内容包括主服务器runID和偏移量 主服务器执行BGSAVE命令后台生成RDB文件,这期间的写操作同时记录在buffer中 RDB文件生成后发送给从服务器,从服务器清空自己的数据,使用RDB文件来重建 从服务器回复重建完成,主服务器将buffer中的写指令发送给从服务器,第一次同步完成,建立长连接 后面的每次同步都通过这个长连接将buffer中的数据同步过去,这个过程是异步的 增量复制 如果长连接断开了,需要重连,重连期间的数据如何同步?Redis2.8之后的版本采用的是增量复制。 从服务器执行psync \u003crunID\u003e \u003coffset\u003e命令,这次会带上曾经记录的服务器id和同步偏移量,从偏移位置开始增量同步。 之前说过主服务器要同步的指令会先写在buffer中,再异步同步过去。而这个buffer——repl_backlog_buffer缓冲区的大小是有限制的,默认1mb,他是一个环形缓冲区。也就是说超过1mb的部分会循环覆盖旧数据,如果要断开的时间太长,要恢复的offset以及被覆盖了,那就会进行全量同步。为了避免这种情况,应该调大repl_backlog_buffer配置的值。 主从架构是没有自动切换的功能的,自动切换需要使用哨兵。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:20:1","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"哨兵 哨兵在主从的基础上增加故障切换的功能。基于分布式共识算法的高可用方案。 哨兵服务已经集成在redis-server二进制文件中,使用redis-server /path/to/sentinel.conf --sentinel命令来启动一个哨兵。哨兵默认运行端口为26379。一个最小的配置文件如下: # 该哨兵将会监听mymaster这个master是否异常, # 如果2个哨兵认为其异常了,将会切换到从节点 # 这里并没有配置其他哨兵节点和Redis从节点的信息, # 这是因为哨兵会通过Pub/Sub能力,从__sentinel__:hello这个Channel中获取其他哨兵的信息,所以mymaster这个名字必须是唯一的,以便服务发现时多个哨兵达成共识建立通信 # 而主从服务器的信息,则会主动与主服务器通信获取,这些配置将会自动的保存到配置文件中,也就是说这些配置都是实时获取的 sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 60000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1 # 该哨兵将会监听resque这个master是否异常, # 如果4个哨兵认为其异常了,将会切换到从节点 sentinel monitor resque 192.168.1.3 6380 4 sentinel down-after-milliseconds resque 10000 sentinel failover-timeout resque 180000 sentinel parallel-syncs resque 5 主从如何切换 首先哨兵如何知道节点真的挂掉了?这里有两个概念: 主观下线:当前哨兵节点认为其下线了 客观下线:哨兵集群超过半数认为其下线了 所有哨兵节点每隔1s与主从节点发一次心跳包确认其存活,超过down-after-milliseconds这个配置规定的时间未回复的节点,该哨兵就将其标记为主观下线。此时有两种情况,1.该节点真的挂了无法回复,2.哨兵节点与该节点的网络出问题了,为了避免第二种情况导致的误判,所以需要客观下线。 当一个哨兵认为该节点主观下线了,就会向其他哨兵发起投票,判断该节点是否下线,如果多数都认为该节点主观下线了,那么就会将该节点标记为客观下线。这里的多数是可以在配置文件中配置的,称之为quorum。 确定该主节点下线之后,需要从其从节点中选择一个升级为主节点。如何选择合适的从节点?有如下考量: 历史网络情况:检查每个从节点主观下线的次数,超过10次的将会被过滤掉 优先级:如果从节点配置了优先级slave-priority,那么优先级小的将会被选择 复制进度:优先级一样的情况下,谁复制的进度更快选择谁 节点id:都一样则选择节点id小的那个 选择好主节点了,还需要通知其他从节点切换主节点。哨兵会向所有从节点发送SLAVEOF命令,切换主节点。 最后通过Pub/Sub通知客户端发生了主从切换事件,客户端可以通过Sub该Channel来完成事件监听,以便与告警。其实不止主从切换事件,主观下线等事件都可以监听到。事件列表如下: 旧主节点重新恢复了,重新上线后哨兵会将其恢复为从节点。 哨兵集群的高可用 哨兵集群的高可用使用分布式算法来保证,需要保证哨兵集群为奇数个,以便于投票时不发生平票而导致脑裂。哨兵集群要高可用起码需要三个节点,最多容忍(n-1)/2个节点宕机。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:20:2","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"集群 集群在前面两者的基础上通过数据分片实现横向扩展的功能。 slot,称之为插槽,Redis集群最重要的概念。一个最小的Redis集群由三主三从组成,所有写入集群的Key将会经过Hash,分配到16384个slot上,而每个主节点将会分配到等量的slot,以达到均衡写入提高写入性能的目的。16384个slot,意味着Redis集群最多支持16384个主从节点。 如果某个主节点宕机了,那么他的从节点将会升级为主节点。 横向扩展 横向扩展,将一个新的主从节点加入集群,需要执行称之为Rehard(重新分片)的操作,来保证各个节点的负载均衡。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:20:3","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"读写分离架构 阿里云上的读写分离架构,本地盘方案采用链式复制,数据同步存在延迟,数据可能不一致,如果要求数据一致,那么应该使用云盘方案的星型复制,或者使用集群。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:20:4","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"可观测能力 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:21:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"监控指标 大Key监控 大Key指的是某个Key对应的Value很大,单线程的Redis读取或操作这个Key时时间过长,影响了其他请求的执行,导致总体QPS下降。 如果在集群中遇到大Key问题,还会发生数据倾斜的现象,大Key所在的节点CPU、内存和带宽的负载明显比其他节点要高。 监控大Key可以有如下方法 redis-cli –bigkeys:列出各个数据结构Top1的大Key 离线分析RDB文件:使用redis-rdb-tools工具离线扫描RDB持久化文件,对线上性能无影响 TopKey:阿里云上支持实时获取大Key和热Key 如何解决大Key 拆分:将存储在一个Hash中的成员,分散存储到多个Hash中 删除:清除大Key应该使用UNLINK \u003ckey\u003e命令,该命令是非阻塞式的 查询历史热Key 热Key指的是某个Key的访问量非常高,其占用了大量的QPS、CPU时间和带宽。热Key同样会导致其他请求的QPS下降,还会发生访问倾斜的现象。如果Key太热,超过Redis实例处理能力,还会出现缓存击穿甚至超卖的情况。 热Key会出现上述问题,原因在于读请求的数量,超过了Redis实例的处理能力,也就是服务器的性能不够。 redis-cli –hotkeys:列出hotkey TopKey:阿里云上支持实时获取大Key和热Key 如何解决大Key 提高服务器性能:升级硬件,使用更好的CPU、内存 架构升级:使用读写分离或者集群架构,提升整体架构处理请求的能力 监控是否发生数据倾斜 监控集群各个节点同一时间段CPU、内存和带宽使用量是否平衡。 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:21:1","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"日志 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:21:2","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"访问量倾斜 某个节点的访问量或QPS明显高于其他节点,导致该节点的CPU使用率和带宽使用率高于其他节点 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:21:3","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"数据量倾斜 某个节点的Key大小大于其他节点的Key,就说明发生数据量倾斜,大多少没有个具体的值,一般是大于20%就认为发生倾斜了。 发生倾斜的原因一般是大Key 热Key或者一些高消耗的命令O(n)复杂度的命令,或者是使用了错误的Key和Hash方式,导致过多的Key被分配到同一节点上 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:21:4","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"缓存击穿 缓存穿透 缓存雪崩 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:22:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"Benchmark Redis-benchmark ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:23:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"禁用高危命令 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:24:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"调优 ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:25:0","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"参数调优 https://help.aliyun.com/document_detail/98726.html?spm=a2c4g.65001.0.nextDoc.55804630W2a2ik ","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:25:1","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["Redis","必知必会"],"content":"内核调优","date":"2022-05-22","objectID":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/:25:2","tags":["Redis"],"title":"Redis必知必会","uri":"/redis%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/"},{"categories":["草稿"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com ","date":"2022-05-15","objectID":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/:0:0","tags":["草稿"],"title":"页面置换算法","uri":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/"},{"categories":["草稿"],"content":"LRU(Least Recently Used) 计算机中内存是有限的,因此我们不可能将所有数据都存在内存中。根据程序的时空间局限性,我们总是希望内存中存储的是最新最近访问过的数据。因此我们需要一种页面置换算法,用来维护内存中始终是最新最近访问过的数据。 LRU就是一种常见的页面置换算法,其作用在于操作系统发生缺页中断时,将内存中最近最少使用的页面置换成需要读取内存的页面。 ","date":"2022-05-15","objectID":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/:1:0","tags":["草稿"],"title":"页面置换算法","uri":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/"},{"categories":["草稿"],"content":"如何简单实现LRU算法 数据结构的设计 LRU缓存可以用哈希表+双向链表实现。 type LRUCache struct { size int // cache已使用的大小 capacity int // cache容量大小 cache map[int]*Node head, tail *Node } type Node struct { val int pre *Node next *Node } 哈希表用来存储存储值到双向链表节点的映射,双向链表用来动态更新最近使用过的值。 大概示意如下图所示: 双向链表中越靠近头节点的,表示越近时间内访问过。设置head和tail节点标记边界,可以避免添加或删除节点时判断相邻节点是否存在,简化代码逻辑。 对于缓存,我们希望有如下操作,并且它们都在O(1)时间复杂度内完成: Get(key),根据输入的key找到对应的val Push(key, val),存储key/val的映射。如果cache已满,我们希望它自动淘汰最近最少使用的数据,再存储新数据。 ","date":"2022-05-15","objectID":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/:1:1","tags":["草稿"],"title":"页面置换算法","uri":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/"},{"categories":["草稿"],"content":"Golang简单实现 type LRUCache struct { size int capacity int // 值到双向链表节点的映射 cache map[int]*Node // 双向链表 head, tail *Node // 虚拟头尾节点 } type Node struct { key, val int pre *Node next *Node } func NewNode(key, val int) *Node { return \u0026Node{ key: key, val: val, } } func Constructor(capacity int) LRUCache { lru := LRUCache{ size: 0, capacity: capacity, cache: make(map[int]*Node, capacity), head: NewNode(-1, -1), tail: NewNode(-1, -1), } lru.head.next = lru.tail lru.tail.pre = lru.head return lru } func (this *LRUCache) Get(key int) int { if _, ok := this.cache[key]; !ok { return -1 } node := this.cache[key] // 更新该元素到链表头部,表示最近访问过 this.moveToHead(node) return node.val } func (this *LRUCache) Put(key int, value int) { // 如果缓存中没有该值,则将其添加到缓存中 if _, ok := this.cache[key]; !ok { node := NewNode(key, value) this.cache[key] = node this.addToHead(node) this.size++ // 如果超过缓存大小了,删除链表最尾元素,表示淘汰最近最近未使用的元素 if this.size \u003e this.capacity { removed := this.removeTail() delete(this.cache, removed.key) this.size-- } } else { // 有该值则更新值和链表中的位置, node := this.cache[key] node.val = value this.moveToHead(node) } } // 以下操作都是O(1)时间复杂度 // 以下操作如果没有虚拟头尾节点,要增加很多判断 func (this *LRUCache) addToHead(node *Node) { node.pre = this.head node.next = this.head.next this.head.next.pre = node this.head.next = node } func (this *LRUCache) removeNode(node *Node) { node.pre.next = node.next node.next.pre = node.pre } func (this *LRUCache) moveToHead(node *Node) { this.removeNode(node) this.addToHead(node) } func (this *LRUCache) removeTail() *Node { node := this.tail.pre this.removeNode(node) return node } ","date":"2022-05-15","objectID":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/:1:2","tags":["草稿"],"title":"页面置换算法","uri":"/%E9%A1%B5%E9%9D%A2%E7%BD%AE%E6%8D%A2%E7%AE%97%E6%B3%95/"},{"categories":["MySQL","SQL","必知必会"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com MySQL必知必会-SQL篇 本篇主要记录SQL相关的内容 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:0:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"检索 # 查找并去重,如果有多列,则要一整行完全一样才会去重 SELECT DISTINCT prod_name FROM prod_table; # limit限制输出的行数,第一个参数指示从第几行开始(从0开始算),后面参数指示输出几行 SELECT prod_name FROM prod_table LIMIT 5, 3; SELECT prod_name FROM prod_table LIMIT 3 OFFSET 5; # 一样的意思 # 根据当前列进行排序 SELECT prod_name FROM prod_table ORDER BY prod_name; # 根据其他列排序当前列 SELECT prod_name FROM prod_table ORDER BY id; # 根据多列进行排序,先根据prod_name列排序,再根据id列排序(只有prod_name列的值重复时,才会使用id列排序) SELECT prod_name FROM prod_table ORDER BY prod_name, id; # 倒序排序,想根据多个列进行降序排序,必须在每个列后面都跟上DESC SELECT prod_name FROM prod_table ORDER BY prod_name DESC, id DESC; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:1:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"过滤数据 # 过滤price=2.2的列,支持的操作符有:=, !=, \u003c, \u003e, \u003c=, \u003e=, BETWEEN SELECT price FROM prices WHERE price=2.2; # BETWEEN过滤,范围[2.2 ~ 3.2] SELECT price FROM prices WHERE price BETWEEN 2.2 AND 3.2; # 过滤非空值 SELECT prod_name FROM prod_table WHERE prod_name IS NOT NULL; # 多过滤条件组合,OR, AND, NOT, IN,符号优先级AND \u003e OR,可以加上括号()来明确优先级 SELECT prod_name FROM prod_table WHERE (prod_name = \"apple\" OR prod_name = \"mongo\") AND prod_price \u003e 2.2 AND prod_price \u003c 4.4; # IN操作符,过滤条件是否在集合中 SELECT prod_name FROM prod_table WHERE prod_name IN (\"apple\", \"mongo\"); # 模糊匹配,%匹配多个任意字符,_匹配任意单个字符 # 模糊匹配性能很差 SELECT prod_name FROM prod_table WHERE prod_name LIKE \"%apple%\"; SELECT prod_name FROM prod_table WHERE prod_name LIKE \"_ppl_\"; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:2:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"正则匹配REGEXP SELECT prod_id, prod_name FROM products WHERE prod_id REGEXP '' 一些特殊字符:-,|,[], ^的双重用途 当在集合中[^],表示否定集合的条件 当在集合外^[],表示以该集合条件开头 MYSQL中快速测试正则表达式是否可以正确工作:SELECT 'hts_0000@sina.com' REGEXP '^[:alnum:]{4,15}@[sina|qq|163]\\\\.[com|cn]$', ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:3:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"拼接字段 # 把输出内容格式化,有点像printf SELECT Concat(prod_name, \"(\", prod_price, \")\") FROM prod_table; # 去除字段左右多余空格,RTrim去除右边空格,LTrim去除左边空格,Trim去除左右空格 SELECT Concat(RTrim(prod_name), \"|\", LTrim(prod_name), \"|\", Trim(prod_name)) FROM prod_table; # 计算price*num的值,并将计算结果展示为expanded_price列 SELECT price, num, price*num AS expanded_price FROM prices; MYSQL中快速测试函数调用和计算表达式的值:SELECT 2*3;或SELECT Trim(' hello world '); ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:4:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"函数 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:5:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"文本处理函数 RTrim, LTrim, Trim # Upper, Lower SELECT price_name, Upper(price_name) as upper_price_name, Lower(price_name) as lower_price_name FROM prices; # Length, Locate, Soundex, SubString ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:5:1","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"时间函数 MYSQL中存储时间的数据类型为datatime,其会将时间存储为YYYY-MM-DD HH:mm::ss,因此下面的时间匹配是不可靠的,因为它只匹配了日期,而没有时间。可靠的做法是,用Date()函数,提取order_date这一列数组中的日期部分,再做匹配 # 不可靠的做法 SELECT cust_id, order_num FROM orders WHERE order_date = '2020-01-01'; # 可靠的做法 SELECT cust_id, order_num FROM orders WHERE Date(order_date) = '2020-01-01'; # 匹配2005年9月的所有数据 SELECT cust_id, order_num FROM orders WHERE Year(order_date) = 2005 AND Month(order_date) = 9; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:5:2","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"数值处理函数 补充几个浮点数处理的 # 四舍五入,第二个参数控制保留几位小数 SELECT ROUND(3.15, 1) -- 3.2 SELECT ROUND(3.15, 0) -- 3 # 上取整 SELECT CEIL(3.14) -- 4 # 下取整 SELECT FLOOR(3.99) -- 3 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:5:3","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"聚合函数 汇总数据,而不是检索出来。常用的汇总有: 汇总行数 行数据组之和 列数据之和、最大值、最小值、平均值等 # AVG, COUNT, MAX, MIN, SUM SELECT MAX(price), MIN(price), SUM(price), COUNT(price), AVG(price) FROM prices; MAX()一般用来找出数值或时间类型的最大值,但它也能用来找文本类型的最大值。MIN()它也能用来找文本类型的最小值 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:5:4","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"系统函数 # 查询系统版本 SELECT VERSION(); # 也可以通过全局变量的方式查看 SELECT @@GLOBAL.VERSION; # 或者用show查看 SHOW VARIABLES LIKE 'VERSION'; # 以下是一些常用的系统全局变量 @@GLOBAL.read_only: 一个布尔值,表示 MySQL 是否处于只读模式。 @@GLOBAL.version_comment: MySQL 版本的注释信息。 @@SESSION.sql_select_limit: 限制 SELECT 查询返回的行数。 @@SESSION.transaction_isolation: 设置事务隔离级别。 @@GLOBAL.max_connections: MySQL 服务器的最大连接数。 @@GLOBAL.innodb_buffer_pool_size: InnoDB 缓冲池的大小。 @@GLOBAL.key_buffer_size: MyISAM 键缓存的大小。 @@GLOBAL.max_allowed_packet: MySQL 允许的最大数据包大小。 @@ERROR_MESSAGE: 最近一次出现的错误信息。 @@ERROR_NUMBER: 最近一次出现的错误编号。 @@ERROR_STATE: 最近一次出现的错误状态。 # 查看所有变量和全局变量 SHOW VARIABLES; SHOW GLOBAL VARIABLES; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:5:5","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"聚合数据 COUNT(*)、COUNT(column)、COUNT(1)的区别 COUNT(*)统计行数,不在乎列值是否为NULL COUNT(column)统计传入的列,列值为NULL时跳过,如果该列设置了不为NULL,则与COUNT(*)结果一致 COUNT(1)传入的是一个表达式,当表达式不为NULL时,统计该行 COUNT(NULL)始终返回0 COUNT(IF(id \u003e 3, 1, NULL))统计id大于3的行数,然而这种方式并不好,因为无法利用索引 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:6:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"分组数据 分组允许把数据分为多个逻辑组,以便对每个组做聚合计算。 # 根据vend_id创建分组,再对每一组做聚合计算(这里的聚合是COUNT(*)) SELECT vend_id, COUNT(*) AS num_prods FROM products GROUP BY vend_id; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:7:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"GROUP BY 创建分组 # 根据id把ids表排序分组,COUNT(*)是对每个分组的数据进行统计 SELECT id, count(*) AS num_id FROM ids GROUP BY id; GROUP BY和WHERE执行顺序的问题 WHERE会先于GROUP BY执行,首先使用WHERE的条件将数据过滤出来,再使用GROUP BY的条件将数据分组。 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:7:1","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"HAVING 过滤分组 HAVING功能跟WHERE类似,HAVING仅仅是因为分组查询出来的数据WHERE不能用过滤分组,仅此而已。HAVING基本上能代替WHERE所有功能。 注意,用HAVING过滤的时候,不能写别名,必须用前面聚合的表达式。如下面的例子就必须用COUNT(*),而不能用num_id # 分组的过滤不能使用WHERE,必须使用HAVING SELECT id, COUNT(*) AS num_id FROM ids GROUP BY id HAVING COUNT(*) \u003e 2; # HAVING和WHERE合用的例子 # 返回有2个物品价格\u003e=10的供应商id SELECT vend_id, COUNT(*) AS num_id FROM products WHERE prod_price \u003e= 10 GROUP BY vend_id HAVING COUNT(*) \u003e= 2; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:7:2","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"窗口函数 窗口函数,也叫OLAP函数或分析函数,用于对数据进行分析。 基本语法 \u003c窗口函数\u003e over (partition by \u003c用于分组的列名\u003e order by \u003c用于排序的列名\u003e) 报表函数 使用聚合函数(max, min, avg, sum, count)和专业窗口函数(lag, lead)来聚合数据,但是不使用GROUP BY来分组,而是使用over子句。 # 查询支付金额在10美元以上的月度订单金额和年度订单金额 # over () 子句指示所有记录不需要分组,所有数据丢给SUM()进行聚合 # over (partition by monthname(order_date)) 子句指示所有记录按照月份来分组,分组的数据丢给SUM()进行聚合 SELECT monthname(order_date) month, -- 月份 amount, -- 金额 sum(amount) over () amount_total, -- 年度总金额 sum(amount) over (partition by monthname(order_date)) -- 月度总金额 FROM payment WHERE amount \u003e 10 ORDER BY 1; -- 根据第一列排序 # 查询各个月的订单金额,并且顺序输出每个月累加的总金额 # 首先是 GROUP BY monthname(payment_date),将所有订单按照月份分组 # 单纯的 sum(amount) 拿到的是每个分组的数据汇总 # over (order by monthname(payment_date) rows unbounded preceding) # 首先是 order by monthname(payment_date) 将各个分组按月份排序 # rows unbounded preceding 则指示结果集从开始到当前行 # 正是因为进行了排序,从开始到当前行拿到的结果才是预期的 # 如果 GROUP BY 总共分了两组,over rows unbounded preceding 执行到第二组时 # 就能拿到从开始到第二组的数据,然后再丢到 sum() 中进行聚合 # 从而第二行拿到的累加金额就是当前月金额加上之前所有月的金额 SELECT monthname(payment_date) month, -- 月份 sum(amount) month_amount, -- 月度金额 sum(sum(amount)) over (order by monthname(payment_date) rows unbounded preceding) rolling_amount -- 顺序的月度累加金额 FROM payment GROUP BY monthname(payment_date) -- 按照月份来分组 ORDER BY 1; # 查询各个月的订单金额,并输出当前月对比上个月的百分比差异 # lag(\u003ccol\u003e, \u003cnum\u003e) 函数,能拿到当前行的前\u003cnum\u003e行,lead(\u003ccol\u003e, \u003cnum\u003e) 函数,能拿到当前行的后\u003cnum\u003e行 # 对月份进行排序,再通过这两个函数,我们可以拿到当前月前一个月和后一个月的月度订单金额 SELECT monthname(payment_date) month, -- 月份 sum(amount) month_amount, -- 月度金额 lag(sum(amount), 1) over (order by monthname(payment_date)) prev_amount, -- 上一个月的订单金额 lead(sum(amount), 1) over (order by monthname(payment_date)) prev_amount -- 下一个月的订单金额 FROM payment GROUP BY monthname(payment_date) -- 按照月份来分组 ORDER BY 1; # 然后再加上数值计算,就能知道当前月对比上一个的百分比差异 SELECT monthname(payment_date) month, -- 月份 sum(amount) month_amount, -- 月度金额 round( (sum(amount) - lag(sum(amount), 1) over (order by monthname(payment_date))) -- 当前月减去上个月 / lag(sum(amount), 1) over (order by monthname(payment_date)) -- 上个月 * 100 , 1) pct_diff FROM payment GROUP BY monthname(payment_date) -- 按照月份来分组 ORDER BY 1; 排名函数 排名函数有三个: rank: 分配的排名不唯一,如果出现并列的,返回相同排名,后面就n个相同的次序 row_number: 为每一行分配唯一的排名,如果出现并列的,随机分配 dense_rank: 分配的排名不唯一,出现并列的情况会重复 # 3种排序函数的区别,desc指示倒序,因为越大排名越高 SELECT monthname(payment_date) month, row_number() over (order by count(*) desc) row_number, rank() over (order by count(*) desc) rank, dense_rank() over (order by count(*) desc) dense_rank FROM payment GROUP BY monthname(payment_date) -- 按照月份来分组 ORDER BY 1; # 生成各个分组内的排名 # 同样通过 partition by 将结果集再次分组 # 然后 count() 计算行数 # 最后丢给RANK() 进行排名 SELECT monthname(payment_date) month, rank() over (partition by monthname(payment_date) order by count(*) desc) rank_level FROM payment GROUP BY monthname(payment_date) -- 按照月份来分组 ORDER BY 1; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:7:3","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"子查询 就是嵌套查询 # 先查询购买了TNT2的所有订单号,再根据订单号过滤出客户id SELECT cust_id FROM orders WHERE order_num IN (SELECT order_num FROM orderitems WHERE prod_id = \"TNT2\"); # 为计算列使用子查询,将customers.cust_id列与子查询中的orders.cust_id列一一比较,n*m的嵌套比较关系 SELECT cust_id, cust_name, cust_state, (SELECT count(*) FROM orders WHERE orders.cust_id = customers.cust_id) AS orders FROM customers ORDER BY cust_name; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:8:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"联结表 JOIN 联结表时,实际上就是把第一个表中的每一行与第二个表的每一行配对。相当于做了一个笛卡尔积。 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:9:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"内联结 只列出ON匹配的行。 # 将vendors表和products表联结起来,配对vendors和products表的每一行数据,当id对应时,就输出供应商名称,产品名称和产品价格,通过这种方法可以找到产品对应的供应商 SELECT vend_name, prod_name, prod_price FROM vendors INNER JOIN products ON vendors.vend_id = products.vend_id ORDER BY vend_name, prod_name; # 或者直接写查询两张表,效果与内联结一致 SELECT vend_name, prod_name, prod_price FROM vendors, products WHERE vendors.vend_id = products.vend_id; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:9:1","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"自联结 表a联结表a,自己联结自己。使用场景是,我想要在当前表中找到生产了’DTNTR’这个产品ID的供应商生产的所有产品。 自联结需要用到表别名 SELECT p1.prod_id, p1.prod_name FROM products AS p1 JOIN products AS p2 ON p2.prod_id = 'DTNTR' AND p1.vend_id = p2.vend_id; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:9:2","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"左联结 选择LEFT OUTER JOIN子句左边的所有行去匹配右边的所有行,如果右边有不匹配的行,就是NULL,ON后面的条件不匹配也会列出来 # 检索所有客户及他们的订单,包括没有订单的客户,如果用内联结,就无法检索出没有订单的客户了 SELECT c.cust_id, o.order_num FROM customers AS c LEFT OUTER JOIN orders AS o ON c.cust_id = o.cust_id; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:9:3","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"右联结 选择RIGHT OUTER JOIN子句右边的所有行去匹配左边的所有行,如果左边有不匹配的行,就是NULL,ON后面的条件不匹配也会列出来 左右联结可以互换使用,只需要调整WHERE或FROM子句中表的顺序。 # 检索所有客户及他们的订单,包括没有订单的客户,如果用内联结,就无法检索出没有订单的客户了 SELECT c.cust_id, o.order_num FROM orders AS o RIGHT OUTER JOIN customers AS c ON c.cust_id = o.cust_id; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:9:4","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"组合查询 用UNION操作将多条SELECT语句组合成一个结果集。 UNION操作与多个WHERE条件查询结果完成的工作一样。但是执行性能不一样,需要试试才知道哪个性能好。 UNION并上的SELECT必须有相同的列,列的顺序可以不同。 UNION会自动去重,与WHERE结果一样。如果不想去重,用UNION ALL,UNION ALL与WHERE就不一样了,它能做到WHERE做不到的事。 # 查找产品价格小于等于5的产品,并上vend_id为1001或1002的 SELECT vend_id, prod_id, prod_price FROM products WHERE prod_price \u003c= 5 UNION SELECT vend_id, prod_id, prod_price FROM products WHERE vend_id IN (1001, 1002); # 与上面等价的WHERE SELECT vend_id, prod_id, prod_price FROM products WHERE prod_price \u003c= 5 OR vend_id IN (1001, 1002); ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:10:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"插入 # 依赖表定义定义的次寻,每个值必须与表中列的定义对应上,如果定义允许为空,才能使用NULL # 主键都是填NULL,让MYSQL自动生成 INSERT INTO Customers VALUES(NULL, 'PeP E. La', '100'); # 也可以指定列名,这样的好处是,表结构发生变化该SQL也能工作,因为它不强依赖列的次寻 # 不必总是填上所有列,没有指定的列,会被填上NULL INSERT INTO Customers(cust_name, cust_contace, cust_email) VALUES('PeP E. La', NULL, 'pepela@gla.com'); # 插入多行 INSERT INTO Customers( cust_name, cust_contace, cust_email) VALUES( 'PeP E. La', NULL, 'pepela@gla.com' ), ( 'PeP E. La', NULL, 'pepela@gla.com' ); ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:11:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"更新 UPDATE customers SET cust_email = 'test@google.com' WHERE cust_id = 10005; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:12:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"删除 DELETE FROM customers WHERE cust_id = 10005; TRUNCATE TABLE; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:13:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"创建表 CREATE TABLE `test` ( `cust_id` int(11) NOT NULL AUTO_INCREMENT, `cust_name` char(50) NOT NULL, `cust_address` char(50) DEFAULT NULL, `cust_city` char(50) DEFAULT NULL, `cust_state` char(5) DEFAULT NULL, `cust_zip` char(10) DEFAULT NULL, `cust_country` char(50) DEFAULT NULL, `cust_contact` char(50) DEFAULT NULL, `cust_email` char(255) DEFAULT NULL, PRIMARY KEY (`cust_id`) ) ENGINE=InnoDB AUTO_INCREMENT=10006 DEFAULT CHARSET=utf8mb4; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:14:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"更新表 更新表的定义 # 增加列 ALTER TABLE Customers ADD describes CHAR(50); # 删除列 ALTER TABLE Customers DROP COLUMN describes; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:15:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"删除表 DROP TABLE customers; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:16:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"重命名表 RENAME TABLE old_table TO new_table; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:17:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"视图 视图是虚拟的表,虚拟表不包含数据,它包含的是一个SQL查询。 为什么要使用视图: 重用SQL语句 简化SQL操作 使用表的组成部分,而不是整个表 保护数据,可以只给到用户视图,而不是整个表 视图创建后,可以像使用表一样使用它,包括SELECT、过滤、排序、JOIN其他表或视图,甚至添加和更新数据。 视图仅仅是用来查看存储在别处的数据的一种设施,视图本身不包含数据,因此它返回的数据是从其他表中检索出来的。 视图最重要的功能就是,将一些常用的、重复的基础查询虚拟成一张表,我们可以在建视图的时候就格式化好里面的数据,使其更有用,减少重复劳动。 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:18:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"使用视图 # 创建视图 CREATE VIEW productcustomers AS SELECT cust_name, cust_contact, prod_id FROM customers, orders, orderitems WHERE customers.cust_id = orders.cust_id AND orderitems.order_num = orders.order_num; # 查看创建视图的语句 SHOW CREATE VIEW viewname; # 删除视图 DELETE VIEW viewname; # 用视图重新格式化检索出来的数据 CREATE VIEW vendorlocation AS SELECT Concat(RTrim(vend_name), ' (', RTrim(vend_country), ')') AS vend_title FROM vendors ORDER BY vend_name; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:18:1","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"存储过程 存储过程简单来说是一条或多条SQL语句的集合,可将其视为批处理文件,但不仅限于批处理。本质就是一个函数。 存储过程简单、安全并且性能高。 存储过程创建之后,会一直存在,直至被显式删除。 存储过程不易于版本管理和调试,阿里开发规范中不建议使用存储过程。 但是存储过程可以封装数据处理、提高性能、减少数据传输带宽,具体使用要视情况而定。 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:19:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"执行/调用存储过程 # 调用productpricing这个存储过程,@pricelow表示用pricelow这个变量来存储结果,MySQL中的变量必须以@开头 CALL productpricing(@pricelow, @pricehight, @priceaverage); # 如果是要传入变量给存储过程 CALL productpricing(1000, @pricelow, @pricehight, @priceaverage); ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:19:1","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"创建存储过程 在MySQL命令行中创建存储过程,需要把语句分隔符;改掉,不然会报语法错误。在其他数据库工具中应该不需要这样改。 一般来说,存储过程不显示结果,而是把结果返回给你指定的变量。 # 修改分隔符为// DELIMITER // # 创建一个名为productpricing的无参数存储过程 CREATE PROCEDURE productpricing() BEGIN SELECT Avg(prod_price) AS priceaverage FROM products; END// # 有返回参数的存储过程 # BEGIN上面的这一块是定义入参和出参的地方 # BEGIN和END中间这一段是具体逻辑 CREATE PROCEDURE productpricing( OUT pl DECIMAL(8, 2), # 返回pl给调用者 OUT ph DECIMAL(8, 2), OUT pa DECIMAL(8, 2) ) BEGIN SELECT Min(prod_price) INTO pl # 表示将结果传递给pl变量 FROM products; SELECT Max(prod_price) INTO ph FROM products; SELECT Avg(prod_price) INTO pa FROM products; END// # 调用上面的存储过程 CALL productpricing(@pl, @ph, @pa)// # 使用上面的存储过程,不会产生输出,而是会返回pl、ph、pa变量,访问变量来获取结果 SELECT @pl as pricelow// SELECT @pl as pricelow, @ph as pricehight, @pa as priceaverage// # 定义一个具有入参的存储过程 CREATE PROCEDURE ordertotal( IN onumber INT, # 接受一个INT型的参数 OUT ototal DECIMAL(8, 2) ) BEGIN SELECT Sum(item_price*quantity) FROM orderitems WHERE order_num = onumber # 在这里用上了onumber INTO ototal; END// # 调用上面的存储过程 CALL ordertotal(20005, @total)// # 修改分隔符为; DELIMITER ; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:19:2","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"删除存储过程 DROP PROCEDUER IF EXISTS productpricing; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:19:3","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"更高级更复杂的存储过程 下面这个存储过程功能为,给出是否需要加税taxable,计算给定客户编号onumber的价格总计ototal。 -- 这些都是注释 -- Name: ordertotal -- Parameters: onumber = order number -- taxable = 0 if not taxable, 1 if taxable -- ototal = order total variable CREATE PROCEDURE ordertotal( IN onumber INT, IN taxable BOOLEAN, # 传入的是一个布尔值 OUT ototal DECIMAL(8, 2) ) COMMENT '订单收益总计,是否附加税' # 这条也是注释,用SHOW PROCEDURE STATUS可以看到 BEGIN -- Declare variable for total DECLARE total DECIMAL(8, 2); # 这里定义的就是局部变量 -- Declare tax percentage DECIMAL taxrate INT DEFAULT 6; # 定义税率 -- Get the order total SELECT Sum(item_price*quantity) FROM orderitems WHERE ordr_num = onumber INTO total; -- Is this taxable? IF taxable THE # 传入的布尔值为真,就加上税率 -- Yes, so add taxrate to the total SELECT total+(total/100*taxrate) INTO total; END IF; -- And finally, save to out variable SELECT total INTO ototal; # 将最终计算结果传出去 END; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:19:4","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"游标 从一组结果中获取数据的指针,可以任取结果集中前几行后几行和某几行,像一个迭代器。 MySQL中的游标只能在存储过程或函数中使用。 游标只能定义在存储过程和函数中,定义好之后还要显式的打开和关闭它,以启用和释放游标占用的资源。 CREATE PROCEDURE processorders() BEGIN -- Declare local variables DECLARE o INT; -- Declare the cursor -- 从SELECT查询出来的一组数据集中定义一个游标 DECLARE ordernumbers CURSOR FOR SELECT order_num FROM orders; -- Declare continue handler -- 当出现02000错误时,done被设置为1 -- 02000错误会发生在REPEAT无法提供更多循环时 DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1; -- Open the cursor -- 显式的打开游标 OPEN ordernumbers; -- Get order number -- 从游标中获取一个数据 FETCH ordernumers INTO o; -- Loop through all rows REPEAT -- Get order number FETCH ordernumers INTO o; -- End of loop -- 当没有更多数据可迭代了,会发生02000错误,done被置为1 -- UNTIL 意思为直到 done 为真了,才停止 UNTIL done END REPEAT; --Close the cursor -- 显式的关闭游标 CLOSE ordernumbers; END// ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:20:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"触发器 在DELTE、UPDATE、INSERT操作执行之后触发一系列操作,这三种操作都支持操作前——BEFORE和操作后——AFTER两个时间点触发。 触发器中有两个特殊的值——NEW.{col_name}和OLD.{col_name}。 NEW.{col_name}是执行操作后产生的新纪录,对于INSERT操作而言,只有NEW.{col_name}而没有OLD.{col_name},通过NEW.id可以拿到插入的新纪录的主键id。对于UPDATE操作而言,NEW.{col_name}获取的是更新之后的新纪录。而DELETE操作则没有NEW.{col_name}这个值。 OLD.{col_name}是执行操作之后的旧纪录,对于DELETE操作而言,只有OLD.{col_name}而没有NEW.{col_name},通过OLD.id可以拿到删除的旧纪录的主键id。对于UPDATE操作而言,OLD.{col_name}获取的是更新之前的旧纪录。而INSERT操作则没有OLD.{col_name}这个值。 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:21:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"INSERT触发器 不仅仅是INSERT语句会触发,所有增加行记录的操作都会触发 # INSERT BEFORE 统计总行数 SET @total_count = 0; DELIMITER $$ CREATE TRIGGER total_count BEFORE INSERT ON account FOR EACH ROW BEGIN @total_count = @total_count + 1; END$$ DELIMITER ; SELECT @total_count; # INSERT AFTER 插入成功后打印新记录主键id SET @last_id = 0; DELIMITER $$ CREATE TRIGGER new_id AFTER INSERT ON account FOR EACH ROW BEGIN SELECT NEW.id INTO @last_id; END$$ DELIMITER ; SELECT @last_id; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:21:1","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"UPDATE触发器 DELIMITER $$ CREATE TRIGGER old_amount BEFORE UPDATE ON account FOR EACH ROW BEGIN SELECT NEW.amount, OLD.amount INTO @old_amount, @new_amount; END$$ DELIMITER ; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:21:2","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"DELETE触发器 DELIMITER $$ CREATE TRIGGER old_id BEFORE DELETE ON account FOR EACH ROW BEGIN SELECT OLD.id INTO @old_id; END$$ DELIMITER ; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:21:3","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"查看触发器 # 查看所有数据库的触发器 SHOW TRIGGERS; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:21:4","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"删除触发器 DROP TRIGGER total_count; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:21:5","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"事务 MySQL常用的引擎中有InnoDB引擎是支持事务的,MyISAM则不支持。 事务,用来保证一批SQL操作的原子性,要么全部成功,要么全部失败。 如果事务中某条SQL语句执行出错了,那么整个事务会被自动回滚。 当事务中执行了ROLLBACK或COMMIT,事务会自动关闭。 MySQL命令行中,所有操作都是默认自动提交的,执行的DELETE、INSERT等操作,本来应该是需要显式的COMMIT才会修改,但是因为设置了默认提交,所以会立即生效,如果想改变默认提交行为,可以使用下面的命令。 # 默认不提交 SET autocommit=0; # 默认提交 SET autocommit=1; 事务处理的术语: 事务(transaction) 指一批SQL语句 回退(rollback) 回退事务,撤销未commit前的所有事务操作,回退不能回退CREATE和DROP操作 提交(commit) 将事务执行结果写入数据库表 保留点(savepoint) 设置一个储存点,回退到这个储存点,而不是整个事务 # 开始事务 START TRANSACTION SELECT * FROM orders; DELETE FROM orders; # 删除orders表中的所有行 SELECT * FROM orders; # 回退这个事务,其实就是回退了所有改变数据的操作,上面只有DELETE,回退SELECT是没有意义的 # 执行完整ROLLBACK之后事务就隐形关闭了 ROLLBACK; # 需要重新开启事务 START TRANSACTION SELECT * FROM orders; DELETE FROM orders; # 设置一个保留点,可以ROLLBACK到这个位置,上面的操作不会被回滚 SAVEPOINT delete1; ROLLBACK TO delete1; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:22:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"字符集管理 两个概念,字符集(CHARACTER)和其对应的校对(COLLATION)。 字符集负责字符串的编码规则,常见的有 utf8 和 utf8mb4。 utf8 和 utf8mb4 的区别: utf8是变长字节编码,会用1~4字节去编码,MySQL中最多使用3字节,因此无法支持emoji表情等需要4字节才能表示的字符 utf8mb4是固定长度的编码,统一使用4字节,完全涵盖世上所有字符 校对是对应的字符集的排序字符集。字符串除了存储还需要排序和比较,校对就是干这件事的。 校对字符集也有两类常见的,utf8mb4_general_ci 和 utf8mb4_unicode_ci。 utf8mb4_general_ci 和 utf8mb4_unicode_ci 的区别: utf8mb4_general_ci没有实现基于标准Unicode编码的排序,对于一些特殊的字符和emoji排序的结果可能不是期望值 utf8mb4_unicode_ci实现了基于标准Unicode编码的排序,能够精确的排序所有字符,但是相对更加耗时 MySQL8.0中默认字符集是utf8mb4,默认校对集是utf8mb4_0900_ai_ci。 在8.0之前,应该选择utf8mb4和utf8mb4_unicode_ci或者是utf8_unicode_520_ci如果有的话。 # 查看所有字符集及其默认校对 SHOW CHARACTER SET; # 查看所有可用校对 SHOW COLLATION; # 查看创建数据库或表时使用的字符集和校对 SHOW VARIABLES LIKE 'character%'; SHOW VARIABLES LIKE 'collation%'; # MySQL允许对数据库、表和每一列单独设置 # 单独设置数据库 CREATE DATABASE test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE test_table( id INT, name VARCHAR(20), email VARCHAR(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci # 单独设置列 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 单独设置表 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:23:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"安全管理 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:24:0","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"用户管理 MySQL用户账户信息存储在mysql.user表中。 有三种方法可以创建用户: CREATE语句 GRANT语句,MySQL8.0中不在支持 在mysql.user表中INSERT一条记录 CREATE语句创建用户的语义是最清晰的。GRANT语句是用来给用户赋予权限的。直接INSERT则是强烈不建议使用。 # 查看所有用户信息 SELECT * FROM mysql.user; # 创建一个用户,基于给定的密码 CREATE USER xiaoming IDENTIFIED BY 'p@$$w0rd'; # 重命名用户,仅支持5.0之后的版本,5.0之前使用UPDATE更新mysql.user表 RENAME USER xiaoming TO xiaoli; # 更改用户的密码,不指定用户名时,更新当前用户的密码 SET PASSWORD FOR xiaoli = Password('n3e p@$$w0rd'); # 删除用户 DROP USER xiaoli; ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:24:1","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["MySQL","SQL","必知必会"],"content":"用户权限管理 管理用户权限,使用GRANT语句,GRANT语句要求至少提供如下信息: 要授予的权限 授予访问权限的库或表 要授予权限的用户名 GRANT的反操作的REVOKE,用来撤销权限。 # 新建用户没有访问权限,能登录数据库,但看不到任何数据,也不能操作任何数据 # 查看用户的权限 SHOW GRANTS FOR xiaoli; # 输出结果为 # +------------------------------------+ # | Grants for xiaoli@% | # +------------------------------------+ # | GRANT USAGE ON *.* TO 'xiaoli'@'%' | # +------------------------------------+ # 其中USAGE ON *.* 表示该用户对任何库和表没有任何权限(USAGE表示没有权限) # 'xiaoli'@'%' 表示 xiaoli用户可以从任意一个ip登录上来,'%'匹配任意ip或主机名 # 给xiaoli添加对orders表的查询和插入权限 GRANT SELECT, INSERT ON crashcourse.orders TO xiaoli; # 撤销xiaoli的插入权限 REVOKE INSERT ON crashcourse.orders TO xiaoli; 可以被授予或撤销的权限 ","date":"2022-05-01","objectID":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/:24:2","tags":["MySQL","SQL"],"title":"MySQL必知必会-SQL篇","uri":"/mysql%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A-sql%E7%AF%87/"},{"categories":["算法","树状数组"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 参考 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:0:0","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"树状数组是什么? 树状数组,或称Binary Indexed Tree, Fenwick Tree,是一种用于高效处理对一个存储数字的列表进行更新及求前缀和的数据结构。 树状数组所能解决的典型问题就是存在一个长度为n的数组,我们如何高效进行如下操作: update(idx, delta):将num加到位置idx的数字上。 prefixSum(idx):求从数组第一个位置到第idx(含idx)个位置所有数字的和。 rangeSum(from_idx, to_idx):求从数组第from_idx个位置到第to_idx个位置的所有数字的和 如果用暴力求解数组区间和,时间复杂度为O(n),更新数组值为O(1)。 // [left,right] func rangeSum(left, right int) int { // 时间复杂度O(n),与for left\u003c= right 没有数量级上的差距 return sum(right) - sum(left - 1) } // [0,index] func sum(index int) int { if index \u003c 0 || index \u003e= len(nums) { return 0 } sum := 0 for i := index; i \u003e= 0; i-- { sum += nums[i] } return sum } // O(1) func update(index, delta int) { nums[index] += delta } 用前缀和求解区间和的时间复杂度为O(1),更新数组值为O(n) type Presum struct { nums []int } func NewPreSum(nums []int) Presum { pre := make([]int, len(nums)) pre[0] = nums[0] for i := 1; i \u003c len(nums); i++ { pre[i] = pre[i-1] + nums[i] } return Presum{ nums: pre, } } func (p *Presum) Update(index, val int) { // O(n) for i := index; i \u003c len(p.nums); i++ { p.nums[i] += val } } func (p *Presum) SumRange(left, right int) int { // O(1) return p.nums[right] - p.nums[left-1] } 树状数组求解区间和的时间复杂度为O(logn),更新数组值为O(logn)。因为修改值之后需要重新维护树状数组。 type BinaryIndTree struct { Nums []int Tree []int } func NewBinaryIndTree(nums []int) BinaryIndTree { // 用前缀和快速构造树状数组 length := len(nums) tree := make([]int, length+1) preSum := make([]int, length+1) for i := 1; i \u003c= length; i++ { preSum[i] = preSum[i-1] + nums[i-1] tree[i] = preSum[i] - preSum[i-lowbit(i)] } return BinaryIndTree{ Nums: nums, Tree: tree, } } func lowbit(i int) int { return i \u0026 (-i) } func (b *BinaryIndTree) Add(index, delta int) { // O(logn) for i := index; i \u003c len(b.Tree); i += lowbit(i) { b.Tree[i] += delta } } func (b *BinaryIndTree) Update(index, val int) { b.Add(index+1, val-b.Nums[index]) b.Nums[index] = val } func (b *BinaryIndTree) Sum(index int) int { sum := 0 // O(logn) for i := index; i \u003e 0; i -= lowbit(i) { sum += b.Tree[i] } return sum } func (b *BinaryIndTree) SumRange(left, right int) int { return b.Sum(right+1) - b.Sum(left) } 树状数组适用于对数组查询和修改都有性能要求的场景。 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:1:0","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"树状数组如何高效? 看上面这张图,可以把树状数组想象成一棵树。所有奇数下标的都是独立子节点。子节点通过i+lowbit(i)来找到其父节点,父节点存储的就是它和它所有子节点的和。通过这样一棵树,我们可以用O(logn)的时间复杂度来获得一个区间的和。 为了方便表示,tree被画成了不连续的样子,实际上tree在内存中仍是连续的数组。 下面这样图可以比较容易理解树状数组在更新节点值时,如何知道那些节点值是应该更新的。 比如我们要对下标i的值进行更新,除了对i本身,还有其上面覆盖了i(或者说管辖i)的所有节点进行更新。比如i=1,则需要更新的节点为1、2、4、8 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:2:0","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"lowbit原理 lowbit(i)用一句话来解释就是——截断i在二进制表示中最低位1前面的数。 比如:i=6=b(110),截断最低位1前面的数得到b(10)=2 lowbit实现很简单 func lowbit(i int) int { // return i \u0026 ((~i)+1) 两者等价 return i \u0026 (-i) } 为什么\u0026上自己的负数可以截断最低位1前面的数呢?因为负数在计算机中是用补码来表示的,所以我们需要对补码、原码有一些了解。 原码:用最高位表示符号位,如int8(-8) = 原(10001000) 补码:对原码除符号位之外的所有位取反并加1,如int8(-8) = 补(11111000) 反码:对原码除符号位之外的所有位取反,如int8(-8) = 反(11110111) 这里我们以6和-6为例,假设他们都是有符号的8位数。 可以发现一个数的正数与负数,它们除了最低位的1及后面的数之外,每一位的数都不相同,因此它们相\u0026可以得到我们想要的结果——截断i在二进制表示中最低位1前面的数。 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:2:1","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"树状数组功能一览 根据节点维护的数据含义不同,树状数组可以提供不同的功能来满足各种区间场景。 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:3:0","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"1. 单点增减+区间求和 LeetCode-307. 区域和检索 - 数组可修改 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:3:1","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"2. 区间增减+单点查询 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:3:2","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"3. 区间增减+区间求和 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:3:3","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"4. 单点增减+区间最值 HDU-1754 I Hate It ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:3:4","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"5. 区间叠加+单点最值 LeetCode-218. 天际线问题 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:3:5","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"树状数组常见应用 求逆序对 求区间逆序对 求树上逆序对 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:4:0","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"二维树状数组 ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:5:0","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","树状数组"],"content":"完整代码 type BinaryIndTree struct { Nums []int Tree []int } func NewBinaryIndTree(nums []int) BinaryIndTree { // 用前缀和快速构造树状数组 length := len(nums) tree := make([]int, length+1) preSum := make([]int, length+1) for i := 1; i \u003c= length; i++ { preSum[i] = preSum[i-1] + nums[i-1] tree[i] = preSum[i] - preSum[i-lowbit(i)] } return BinaryIndTree{ Nums: nums, Tree: tree, } } func (b *BinaryIndTree) update(index, delta int) { for i := index; i \u003c len(b.Tree); i += lowbit(i) { b.Tree[i] += delta } } func (b *BinaryIndTree) Update(index, val int) { b.update(index+1, val-b.Nums[index]) b.Nums[index] = val } func (b *BinaryIndTree) Sum(index int) int { sum := 0 for i := index; i \u003e 0; i -= lowbit(i) { sum += b.Tree[i] } return sum } func (b *BinaryIndTree) SumRange(left, right int) int { return b.Sum(right+1) - b.Sum(left) } func lowbit(i int) int { return i \u0026 (-i) } ","date":"2022-04-04","objectID":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/:6:0","tags":["算法","树状数组"],"title":"树状数组","uri":"/%E6%A0%91%E7%8A%B6%E6%95%B0%E7%BB%84/"},{"categories":["算法","查找算法"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com ","date":"2022-04-03","objectID":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/:0:0","tags":["算法","查找算法"],"title":"二分查找","uri":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"},{"categories":["算法","查找算法"],"content":"二分查找 对于任何有序集合,我们可以使用二分查找的方式来快速定位到想要的元素。 二分查找的时间复杂度为O(logn) 二分查找的实现难点在于边界条件的判断,要注意集合的闭合区间。实现时闭合区间分为两种:[left,right]和[left,right),两种不同的闭合区间的代码实现也不同。 ","date":"2022-04-03","objectID":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/:1:0","tags":["算法","查找算法"],"title":"二分查找","uri":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"},{"categories":["算法","查找算法"],"content":"二分查找的边界条件 [left,right] // BinarySearch 查找nums中是否存在target,并返回其下标,如果不存在返回-1 func BinarySearch(nums []int, target int) int { length := len(nums) left, right := 0, length - 1 for left \u003c= right { mid := left + (right - left) / 2 if nums[mid] \u003c target { left = mid + 1 } else if nums[mid] \u003e target { right = mid - 1 } else { return mid } } return -1 } [left,right) // BinarySearch 查找nums中是否存在target,并返回其下标,如果不存在返回-1 func BinarySearch(nums []int, target int) int { length := len(nums) left, right := 0, length for left \u003c right { mid := left + (right - left) / 2 if nums[mid] \u003c target { left = mid + 1 } else if nums[mid] \u003e target { right = mid } else { return mid } } return -1 } ","date":"2022-04-03","objectID":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/:1:1","tags":["算法","查找算法"],"title":"二分查找","uri":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"},{"categories":["算法","查找算法"],"content":"二分查找其他应用 二分查找不仅可以用于有序序列,还可以用于部分有序序列,比如leetcode 153.寻找旋转排序数组中的最小值也可以使用二分法。 由此我们可知,二分查找只是一种查找的思想。对于查找一个值,我们要首先想到如何能跳过一些无意义的比较,收缩左右边界,以达到加快搜索的过程。 ","date":"2022-04-03","objectID":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/:1:2","tags":["算法","查找算法"],"title":"二分查找","uri":"/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/"},{"categories":["bit数组","bitmap"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 什么是bit数组? bit数组通常为一个无符号数的数组,用每一个元素的每一个bit位来存储数据。 var bitSlice []uint64{0, 0, 0} bit数组 当我们存储数据,比如存储2时,对64取商和模为0和2,可知需要将2存储在bitSlice[0]的第2个bit位中,因此我们把bitSlice[0] \u0026 1\u003c\u003c2即可标识2被存储了 把2存储到bitSlice中 对于更大的数字,比如66,对64取商和模为1和2,可知需要将66存储在bitSlice[1]的第2个bit位中,因此我们把bitSlice[1] \u0026 1\u003c\u003c2即可标识66被存储了 把66存储到bitSlice中 bit数组有什么用? bit数组可以使用很小的存储空间,存储更多的数据。对于uint64而言,如果直接存储,一个数要使用8byte空间,64个数则要64*8byte=512byte,而用bit来存,一个uint64的变量可以存储64个数,空间节省了64倍。 如果我们要对40亿条数据进行排序去重,但机器可用内存只有4G,要怎么做呢? 我们先来计算一下,4,000,000,000对64取商和模为62,500,000和0,可知数组最大需要62,500,000个元素,而62,500,000 * 8byte = 500,000,000byte ≈ 480M 如果用[]uint32切片直接存储,则需要4,000,000,000 * 4byte = 16,000,000,000byte ≈ 15G 使用bit数组,存储时只会将数所在的bit位 置1,所以只要按顺序取出数据,就是排序去重后的数据。 bit数组应用 bit数组在很多地方都有应用,比如redis中的bitmap就是使用了bit数组的方式实现的。 bit数组实现 package bitmap import ( \"bytes\" \"fmt\" ) // 参考go语言圣经6.5章节 // UINTSIZE 判断当前机器是32位还是64位 // 64位机器^uint(0) = 64位全1,\u003e\u003e 63之后变成只有第一位为1,32 \u003c\u003c 1 = 64 // 32位机器\u003e\u003e 63之后全部位变为0,32 \u003c\u003c 0 = 32 const UINTSIZE = 32 \u003c\u003c (^uint(0) \u003e\u003e 63) // An IntSet is a set of small non-negative integers. // Its zero value represents the empty set. type IntSet struct { words []uint } // Has reports whether the set contains the non-negative value x. func (s *IntSet) Has(x int) bool { word, bit := x/UINTSIZE, uint(x%UINTSIZE) // words长度大于数据所在元素下标,且所在bit位不为0 return word \u003c len(s.words) \u0026\u0026 s.words[word]\u0026(1\u003c\u003cbit) != 0 } // Add adds the non-negative value x to the set. func (s *IntSet) Add(x int) { // 对要存储的数取商和模 word, bit := x/UINTSIZE, uint(x%UINTSIZE) // 如果words数组长度小于商,则扩容至能存下为止 for word \u003e= len(s.words) { s.words = append(s.words, 0) } // 将数存储到指定bit位 s.words[word] |= 1 \u003c\u003c bit } // UnionWith sets s to the union of s and t. // 设置s为s和t的并集 func (s *IntSet) UnionWith(t *IntSet) { for i, tword := range t.words { if i \u003c len(s.words) { // 取并集 // 101 | 010 = 111 s.words[i] |= tword } else { s.words = append(s.words, tword) } } } // IntersectWith sets s to the intersect of s and t // 设置s为s和t的交集 // 元素在s集合t集合均出现 func (s *IntSet) IntersectWith(t *IntSet) { // 如果s比t长,把s截取到t的长度 if len(s.words) \u003e len(t.words) { s.words = s.words[:len(t.words)] } // 如果t比s长,也只会比较s中有的部分 for i, tword := range t.words { if i \u003c len(s.words) { s.words[i] \u0026= tword } } } // DifferenceWith sets s to the difference of s and t // 设置s为s和t的差集 // 元素出现在s集合,未出现在t集合 func (s *IntSet) DifferenceWith(t *IntSet) { for i, tword := range t.words { if i \u003c len(s.words) { // ^: 异或运算,值不同则为1,相同为0 // B A // 100011010 ^ 110101001 = 010110011 // 010110011 \u0026 110101001 = 010100001 // 正好是A中有而B中没有的 s.words[i] \u0026= s.words[i] ^ tword } } } // SymmetricDifference sets s to the symmetric difference of s and t // 设置s为s和t的并查集 // 元素出现在s但没有出现在t,或者出现在t没有出现在s func (s *IntSet) SymmetricDifference(t *IntSet) { for i, tword := range t.words { if i \u003c len(s.words) { s.words[i] ^= tword } else { // 如果s元素个数小于t,则s要把t中多的部分补上 s.words = append(s.words, tword) } } } // String returns the set as a string of the form \"{1 2 3}\". // String方法,fmt.Printf()函数对实现了String方法的类型会直接调用其String()方法进行输出 func (s *IntSet) String() string { var buf bytes.Buffer buf.WriteByte('{') for i, word := range s.words { if word == 0 { continue } // 逐位遍历,如果为1,则将其写入缓冲区 for j := 0; j \u003c UINTSIZE; j++ { if word\u0026(1\u003c\u003cuint(j)) != 0 { if buf.Len() \u003e len(\"{\") { buf.WriteByte(' ') } // UINTSIZE*i+j还原数实际大小 // 比如2存储在[0]的第2个比特位 // UINTSIZE*0+2=2 fmt.Fprintf(\u0026buf, \"%d\", UINTSIZE*i+j) } } } buf.WriteByte('}') return buf.String() } // Len return the number of elements // 返回当前存储了多少个数 func (s *IntSet) Len() int { l := 0 for _, word := range s.words { if word == 0 { continue } for j := 0; j \u003c UINTSIZE; j++ { if word\u0026(1\u003c\u003cuint(j)) != 0 { l++ } } } return l } // Elems returns a slice containing the elements of the set func (s *IntSet) Elems() []int { res := make([]int, s.Len()) k := 0 for i, word := range s.words { if word == 0 { continue } for j := 0; j \u003c UINTSIZE; j++ { if word\u0026(1\u003c\u003cj) != 0 { res[k] = UINTSIZE*i + j k++ } } } return res } // Remove remove x from the set // 删除指定数 func (s *IntSet) Remove(x int","date":"2022-02-15","objectID":"/bit%E6%95%B0%E7%BB%84/:0:0","tags":["bit数组","bitmap"],"title":"Bit数组/bitmap","uri":"/bit%E6%95%B0%E7%BB%84/"},{"categories":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 什么是RK算法? RK算法是一种字符串匹配算法。核心思想是将字符串映射为其字符集长度的数值(hash值),将字符串比较变为数值比较,从而可以获得较好的性能。需要注意的是,当hash相同时,字符串任然有小概率不相同,hash不同则不可能相同。 对于文本串s和模式串p,我们计算p的hash值,然后再计算s中长度为len(p)的所有子串的hash值,对比他们是否相同,如果相同还需再比较一次字符串是否相同。 ","date":"2022-02-11","objectID":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/:0:0","tags":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"title":"Golang中的字符串匹配——RK算法","uri":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/"},{"categories":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"content":"什么是hash值? 通过一个hash函数,将输入内容转化为数字,这个数字就是hash值。对于输入的内容相同,hash函数输出的值必须相同且应该尽量唯一。 ","date":"2022-02-11","objectID":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/:1:0","tags":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"title":"Golang中的字符串匹配——RK算法","uri":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/"},{"categories":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"content":"如何计算字符串的hash值? 对于一串数字字符串\"1234\",我们可以把它看做是10进制数,通过计算:'1'*10^(4-1) + '2'*10^(4-2) + '3'*10^(4-3)+'4'*10^(4-4)将它hash为1234(10^2表示10的2次方,4为字符串长度)。 对于小写字母字符串\"abcd\",我们可以把它看做是26进制数,通过计算:'a'*26^(4-1) + 'b'*26^(4-2) + 'c'*26^(4-3) + 'd'*26^(4-4)将它hash为19010(‘a’=1,‘b’=2,‘c’=3,’d’=4)。 对于大小写字符混合字符串\"AbCd\",我们可以把它看做是128进制(ASCII码128个字符),通过计算:'A'*128^(4-1) + 'b'*128^(4-2) + 'C'*128^(4-3) + 'd'*128^(4-4),将它们hash为137,929,188(‘A’=65,‘b’=98,‘C’=67,’d’=100)。 通过上述例子可以发现,我们可以把一串字符hash为它们字符集进制的数,那么我们是否可以用Unicode字符集作为进制数?不就可以表示所有字符了?答案是可以的。Unicode字符集所有字符个数为1114112个。 ","date":"2022-02-11","objectID":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/:2:0","tags":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"title":"Golang中的字符串匹配——RK算法","uri":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/"},{"categories":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"content":"如何使用hash进行字符串匹配? 对于文本串s\"12345\",我们希望知道模式串p\"45\"是否被s包含要如何做? 以10进制为例。 首先对p进行hash,'4'*10+'5'=45,对s[0:2]进行hash,'1'*10+'2'=12,12不等于45,进行下一轮计算s[1:3]。下一轮计算难道又要'2'*10+'3'=23这样遍历s[1:3]的每一个字符吗?其实不用,我们可以把(12-10)*10+3,这意味着我们总共只需要遍历s串一次,而p串也是一次,这就是rk算法时间复杂度O(m+n)的原因。 ","date":"2022-02-11","objectID":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/:3:0","tags":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"title":"Golang中的字符串匹配——RK算法","uri":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/"},{"categories":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"content":"常用的字符串hash函数 BKDRHash ","date":"2022-02-11","objectID":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/:4:0","tags":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"title":"Golang中的字符串匹配——RK算法","uri":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/"},{"categories":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"content":"如何解决hash值相同但字符串不相同的情况 字符串hash难免出现hash值相同的情况,当相同时我们需要额外遍历一遍以确保相同。 Golang中的RK算法应用 // PrimeRK相当于我们上面提到的进制 const PrimeRK = 16777619 // HashStr returns the hash and the appropriate multiplicative // factor for use in Rabin-Karp algorithm. func HashStr(sep string) (uint32, uint32) { hash := uint32(0) for i := 0; i \u003c len(sep); i++ { // 将模式串hash为PrimeRK进制的数 hash = hash*PrimeRK + uint32(sep[i]) } // var pow, sq uint32 = 1, PrimeRK for i := len(sep); i \u003e 0; i \u003e\u003e= 1 { if i\u00261 != 0 { pow *= sq } sq *= sq } return hash, pow } // IndexRabinKarp uses the Rabin-Karp search algorithm to return the index of the // first occurrence of substr in s, or -1 if not present. func IndexRabinKarp(s, substr string) int { if len(s) \u003c len(substr) { return -1 } // Rabin-Karp search hashss, pow := HashStr(substr) n := len(substr) var h uint32 for i := 0; i \u003c n; i++ { h = h*PrimeRK + uint32(s[i]) } if h == hashss \u0026\u0026 s[:n] == substr { return 0 } for i := n; i \u003c len(s); { h *= PrimeRK h += uint32(s[i]) h -= pow * uint32(s[i-n]) i++ if h == hashss \u0026\u0026 s[i-n:i] == substr { return i - n } } return -1 } 参考:https://www.cnblogs.com/golove/p/3234673.html ","date":"2022-02-11","objectID":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/:5:0","tags":["算法","hash","字符串hash","RK算法","字符串匹配算法"],"title":"Golang中的字符串匹配——RK算法","uri":"/golang%E4%B8%AD%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%8C%B9%E9%85%8Drk%E7%AE%97%E6%B3%95/"},{"categories":["算法","kmp","字符串匹配算法"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 什么是KMP算法? kmp算法是一种用于高效匹配字符串子串是否存在的算法,比如想知道文本串s中是否存在模式串p,就可以使用kmp算法。 如何实现KMP算法? 实现kmp算法分两步 基于模式串初始化next数组 使用next数组加速字符串匹配 ","date":"2022-02-11","objectID":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/:0:0","tags":["算法","kmp","字符串匹配算法"],"title":"如何实现kmp算法","uri":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/"},{"categories":["算法","kmp","字符串匹配算法"],"content":"什么是next数组? next数组,又称为prefix数组、前缀表,其作用是记录给定字符串的所有子串中相等的前后缀长度。 ","date":"2022-02-11","objectID":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/:1:0","tags":["算法","kmp","字符串匹配算法"],"title":"如何实现kmp算法","uri":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/"},{"categories":["算法","kmp","字符串匹配算法"],"content":"什么是前缀后缀? 对于字符串cbccbcbccc,其前缀为包含首字符不包含尾字母的任意子串,其后缀为包含尾字符不包含首字符的任意子串。 前缀 c cb cbc cbcc ... cbccbcbcc 后缀 c cc ccc bccc ... bccbcbccc 对于字符串cbccbcbccc的每一个子串,我们求出它们的最长相等前后缀长度,其中子串c初始化为0。 最长相等前后缀 ","date":"2022-02-11","objectID":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/:1:1","tags":["算法","kmp","字符串匹配算法"],"title":"如何实现kmp算法","uri":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/"},{"categories":["算法","kmp","字符串匹配算法"],"content":"next数组具体实现 求next数组的代码如下 func getNext(s string) []int { sLen := len(s) next := make([]int, sLen) next[0] = 0 j := 0 for i := 1; i \u003c sLen; i++ { // j要保证大于0,因为下面有取j-1作为数组下标的操作 for j \u003e 0 \u0026\u0026 s[i] != s[j] { // 回退前一位 j = next[j - 1] } if s[i] == s[j] { j++ } next[i] = j } return next } 求出next数组后,我们就可以利用它加速字符串的匹配。 当遇到不匹配时,就往前找一个最长相等前后缀,并移动模式串指针到他的后面,再尝试匹配 不匹配 匹配 当i==j==5时,文本串s[5] != 模式串p[5],这时我们在next数组中往前找一位,next数组中记录了p[0:5]这个子串最长相等前后缀长度为2,意味着我们j指针只需要回退到下标为2的位置就可以继续匹配了,跳过了前面相同的部分 kmp具体实现 func kmp(s string, p string) int { m, n := len(s), len(p) next := make([]int, n) // 构建next数组 for i, j := 1, 0; i \u003c n; i++ { for j \u003e 0 \u0026\u0026 p[i] != p[j] { j = next[j-1] } if p[i] == p[j] { j++ } next[i] = j } // 根据next数组加速字符串匹配 for i, j := 0, 0; i \u003c m; i++ { for j \u003e 0 \u0026\u0026 s[i] != p[j] { j = next[j-1] } if s[i] == p[j] { j++ } if j == n { return i - n + 1 } } // 全部匹配失败,返回-1 return -1 } ","date":"2022-02-11","objectID":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/:2:0","tags":["算法","kmp","字符串匹配算法"],"title":"如何实现kmp算法","uri":"/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0kmp%E7%AE%97%E6%B3%95/"},{"categories":["分享"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 1 引言 本文用于记录日常使用hugo的优化建议,阅读时长10分钟。 适用于以下人员: 已经搭建好Hugo博客 希望使用图床功能 希望自己的博客能在百度和谷歌上搜索到 本文将从以下几点进行优化: 使用GitHub作为博文的图床 让百度和谷歌收录站点 2 使用GitHub作为图床储存图片 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:0:0","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"什么是图床 图床是一个专门存放图片的服务器,使用图床可以让我们的博客直接从网上获取图片,而无需管理本地图片路径,让我们获得更好的博文书写体验。 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:1:0","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"优雅的使用图床 方案为:VsCode+PicGo插件+GitHub。 VsCode用于书写博客文章,PicGo插件用于快速生成网络图片链接,GitHub作为图片服务器存放图片。 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:0","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"操作步骤如下: ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:1","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"1.安装VsCode 点击VsCode即可前往下载界面。安装十分简单,不再赘述。 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:2","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"2.下载PicGo插件 打开VsCode,在左侧的扩展管理中输入PicGo下载插件。 下载PicGo插件 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:3","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"3.创建GitHub图床仓库 创建一个名字为\u003c你github账户名\u003e.github.io的仓库,我这里的是博客使用的仓库,并使用images目录作为图片存放的目录。 创建GitHub图床仓库 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:4","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"4.生成令牌(Token) 接下来需要生成令牌,让PicGo插件能通过令牌免密上传图片到图床目录。 生成令牌 生成令牌 生成令牌 生成令牌 Token生成之后要及时复制,之后就无法复制了。 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:5","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"5.配置PicGo插件 打开VsCode上的PicGo插件,点击小齿轮进行配置。 在配置中找到GitHub的相关配置项,从上到下分别配置:图床目录(必须加上/),github仓库,Token令牌。 配置PicGo插件 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:6","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"6.测试图床 VsCode上的PicGo插件配置好之后,使用Ctrl+Alt+u可以把剪切版中的图片上传到图床中,并且会在当前VsCode打开的文件中生产一个图片的网络链接。同时我们看GitHub上的images目录时,会发现图片被自动上传了。 测试图床 2 在百度和谷歌上添加网站 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:2:7","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"为什么要添加网站 在百度和谷歌添加博客的网站之后,就可以通过百度和谷歌的搜索引擎找到自己的博客。 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:3:0","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"收录和站点管理 站点收录:让百度或谷歌知道有这个站点的存在,添加之后需要等1~7天(不同搜索引擎不一样)的时间后才能找到自己的博客。 站点管理:向百度或谷歌证明某个站点是属于你的,即你是网站的管理员。可以通过如下方式进行证明: 在网站首页中增加一个校验值标签 在网页目录中增加一个校验页面 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:4:0","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"添加收录 测试网址是否被收录:site:hts0000.github.io。 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:5:0","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"百度 检查收录 添加之后百度大概需要一星期的时间进行收录。 检查收录 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:5:1","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"谷歌 检查收录 添加之后谷歌大概需要1~2天的时间来收录网站。 检查收录 收录成功后在谷歌中搜索:site:hts0000.github.io,就可以发现我们的网站可以通过浏览器搜索到了。 检查收录 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:5:2","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"站点管理 通过百度账号和谷歌账号,可以绑定网站,通过百度和谷歌的统计功能,可以看到网站的访问次数和访问情况。 让账号和网站绑定,需要向百度和谷歌证明你是网站的管理员,证明的方法有很多种,这次选择校验页面的方式进行验证。 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:6:0","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"百度 首先在账号中添加网站 站点管理 站点管理 把下载好的校验页面放到网页目录下,向百度证明你是网站的管理员。 站点管理 校验成功后可以在站点管理中看到刚刚添加的站点。 站点管理 点击站点可以看到百度为我们统计的站点信息,如点击率搜索量等等。 站点管理 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:6:1","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"谷歌 谷歌的操作与百度类似,也是在网页的目录下添加校验页面,向谷歌证明你是网站的管理员。 站点管理 添加完成之后也可以通过谷歌统计的站点信息查看网站的点击量等等。 站点管理 ","date":"2020-02-16","objectID":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/:6:2","tags":["hugo","分享"],"title":"Hugo日常使用优化","uri":"/hugo%E6%97%A5%E5%B8%B8%E4%BD%BF%E7%94%A8%E4%BC%98%E5%8C%96/"},{"categories":["分享"],"content":"转载请注明出处:https://hts0000.github.io/ 欢迎与我联系:hts_0000@sina.com 1 引言 本文用于记录基于HUGO搭建个人博客的步骤和注意事项,阅读时长大概30分钟。 适用于以下人员: 想搭建博客 想了解hugo 闲着没事 2 准备阶段 在开始搭建博客前,希望你已经准备好以下工具,以便能快速的跟上本篇文章。 Git Bash Notepad++ VSCode 文章将会从如下三个大步骤进行展开。 基于HUGO的本地博客 定制本地博客 使用GitHub展示博客内容 3 基于HUGO的本地博客 想要在本地搭建一个基于HUGO的博客,大致有如下三步。 安装hugo 获取一个hugo主题 启动hugo 第一步,安装hugo。 前往hugo的github仓库获取对应操作系统的hugo执行程序。 完成之后打开cmd终端,输入hugo version,如能正确显示hugo的版本即为安装成功。 hugo安装成功 安装成功后在cmd终端执行如下命令,在D盘生成hugo博客站点MyBlog。 hugo new site D:\\MyBlog 第二步,获取HUGO主题。 hugo的主题是博客的门面,博客的风格和样式都基于主题。如果有前端编程的能力,也可以自行设计属于自己的主题,或者前往hugo主题商店下载现成的主题。这里以LoveIt 主题为例。 点击Download前往LoveIt的github仓库进行下载。 下载LoveIt主题 下载LoveIt主题 将下载好的压缩包解压到D:\\MyBlog\\themes目录下,并将目录重命名为LoveIt。 将D:\\MyBlog\\themes\\LoveIt\\exampleSite\\zh目录下的所有文件和D:\\MyBlog\\themes\\LoveIt\\exampleSite\\static目录复制到D:\\MyBlog\\目录下。 zh目录:LoveIt主题的中文配置及界面内容 zh\\config.toml:LoveIt主题的集中配置文件 zh\\content目录:LoveIt主题的默认页面内容 static目录:LoveIt主题的图包 使用Notepad++或其他文本编辑工具打开config.toml文件,将 staticDir = [\"../static\", “../../assets/others”] 改成staticDir = [\"/static\", “../assets/others”]。 第三步,启动本地博客。 在cmd终端执行如下命令,启动监听。 hugo server -D --config D:\\MyBlog\\config.toml 在浏览器访问URL:localhost:1313,可以查看本地博客。 下载LoveIt主题 4 定制本地博客 定制一个充满个人元素的博客大概分为如下三步。 个性化的博客标题和个人介绍 个性化的头像和图标 个性化的博客内容 第一步,定制个人的博客标题和介绍。 关于如何修改博客的标题等,D:\\MyBlog\\config.toml文件内作者有非常完善的注释,大家多修改多尝试(先备份),这里不细讲。 第二步,个性化头像和图标。 点击Favicon Generator,打开Select your Favicon image上传一张你喜欢的图片。 Favicon Generator生成图标 上传之后成功之后来到页面最下方点击Generate your Favicons and HTML code,Favicon Generator会自动分辨率生成符合一系列PC和移动终端的图标。 Favicon Generator生成图标 点击Favicon package下载生成好的图标包。 Favicon Generator下载图标 将下载好的图标包解压,把里面的文件全部复制到D:\\MyBlog\\static\\目录下。 如果想要替换主页的头像图标,需要替换D:\\MyBlog\\static\\images\\avatar.png,推荐分辨率为528*560。 最后整体效果如下图所示。 博客展示 第三步,个性化的博客内容。 博客的最终目的是展示内容,hugo能非常方便的根据文章的设定生成标签(tags)和分类(categories)。下面教大家如何设置文章的模板。 设置博客文章的模板 LoveIt主题中作者已经写好了一个文章模板,这里直接套用这个模板进行修改即可。 首先将D:\\MyBlog\\themes\\LoveIt\\archetypesdefault.md文件复制到D:\\MyBlog\\archetypes目录下,使用VsCode等markdown编辑工具打开。 模板文件 生成一篇博客文章 打开cmd终端,执行如下命令,根据文章模板生成第一篇文章。 # 进入D盘 D: # 进入MyBlog目录 cd MyBlog # 生成博客文章,文章会生成在content/posts目录下 hugo new posts/我的第一篇博客.md # 启动hugo,开启监听 hugo server -D --config D:\\MyBlog\\config.toml 再次访问URL:localhost:1313即可看到刚刚添加的文章。 5 用GitHub展示博客文章 让GitHub仓库展示博客页面,大致有如下四步。 创建一个GitHub账号 创建一个同名仓库 hugo build生成页面 推送到GitHub远程仓库 第一步,创建一个GitHub账号。 网上有很多教程,不再赘述。 第二步,创建一个同名仓库 网上也有很多教程,只强调一点,仓库名称为username.github.io。如:你的用户名叫zhangsan000,则你的仓库名必须为zhangsan000.github.io。 GitHub仓库 第三步,hugo生成静态页面。 打开cmd终端,执行如下命令,生成静态页面。 # 进入D盘 D: # 进入MyBlog目录 cd MyBlog # 生成静态页面 hugo hugo生成静态页面之后在MyBlog目录下会生成public目录存放静态页面。 将静态页面推送到GitHub 打开cmd终端,将GitHub仓库clone到本地。 # 查看git是否正常安装,能正常显示git版本即为安装正确 git version D: cd MyBlog/pubilc # 将你的仓库clone下来 git clone repoURL 然后将所有文件移动到clone下来的仓库目录下,再次打开cmd终端,输入如下命令,将静态页面推送到远程仓库。 D: # 进入git本地仓库 cd MyBlog\\public\\\u003c你的仓库目录\u003e # 将所有的文件添加到本地暂存区 git add . # 提交暂存区的内容 git commit -m 'init MyBlog' # 将本地提交推送到远程 git push 在GitHub上查看提交。 GitHub仓库 最后在浏览器上访问URL:\u003c仓库名称\u003e.github.io即可通过网络访问到静态页面。 GitHub仓库 ","date":"2020-02-15","objectID":"/%E5%9F%BA%E4%BA%8Ehugo%E5%BF%AB%E9%80%9F%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/:0:0","tags":["hugo","分享"],"title":"基于Hugo快速搭建个人博客","uri":"/%E5%9F%BA%E4%BA%8Ehugo%E5%BF%AB%E9%80%9F%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/"},{"categories":null,"content":"Hts0000的个人博客 主要分享如下内容 Linux运维 网络基础 ","date":"2019-08-02","objectID":"/about/:0:0","tags":null,"title":"About LoveIt","uri":"/about/"}]