首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
1
「今日早报」 2026年6月13日, 农历四月廿八, 星期六
2
「今日早报」 2026年6月12日, 农历四月廿七, 星期五
3
「今日早报」 2026年6月11日, 农历四月廿六, 星期四
4
「今日早报」 2026年6月10日, 农历四月廿五, 星期三
5
「今日早报」 2026年6月9日, 农历四月廿四, 星期二
沙漠渔
把過去的累積,善用到當下
累计撰写
2,795
篇文章
累计创建
385
个标签
累计收到
997
条评论
栏目
首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
搜索
标签搜索
代理服务
winsw
override
VMware
api
popai
拉取镜像
人工智能
copilot
chatgpt
openai
coze
objdump
ldd
日志
版本
latest
批处理
bat
节能模式
iwconfig
排序
du
设计模式
hostname
面板
cockpit
版本不兼容
npm
统计
烟花
新春
leveldb
Java heap space
堆内存
harbor
utf8mb4
网络聚合
IPV6
nmtui
测速
带宽
千兆
路由器
nmcli
nmlci
orangepi
motd
中文乱码
webdav
香橙派
代码折叠
享元模式
单例模式
解锁
锁定
无法安装
Xshell
并发编程
ScheduledThreadPoolExecutor
ThreadPoolExecutor
线程池
Fock-Join
并发
ExecutorService
nextcloud
alist
panic
hung task
时间戳
ping
tail
dd
嵌入式
点灯
mount
共享
NFS
curl
全屏
ChannelOption
C++
comparator
桥头堡
开源
varchar
char
StringBuilder
StringBuffer
String
命令行工具
网络配置
netsh
建议
专家
离谱
2022
设备管理器
虚拟网卡
环回适配器
安全
攻击
CC
DDOS
跳槽
过年
年假
春节
2023
优站计划
鼻塞
咳嗽
大号流感
阳
新冠病毒
循环冗余校验
CRC-16/XMODEM
crc校验
stream
DMZ主机
域名解析
CDN
七牛云
DDNS
类加载器
双亲委派
加载机制
删除
搜索
变化时间
修改时间
访问时间
响应异常
超时
jsoup
用法详解
压缩命令
打包压缩
zip
优缺点
打包
数据压缩
密码
sudoers
索引量
权重
瞬间
魔鬼洞
沙漠鱼
沙漠渔
面试
引用传递
值传递
即时编译器
机器码
JIT
旗舰版
ISO
原版镜像
win7
参数
配置文件
8小时
时间错误
报告
coverity
jetbrains
idea
谷歌翻译
rest
403
ERR_UNKNOWN_URL_SCHEME
DevTools
优先级
location
FTP
挂载
curlftpfs
提示
自定义参数
springboot
配置
死锁
超级密码
桥接
联通光猫
Dockerfile
构建
命令行
eclipse
光猫
路由模式
桥接模式
bash-4.2
sudo
oracle帐号
JDK下载
百度收录
定时发布
哨兵模式
内存管理
JVM
登录
免密
不一致
服务器时间
BIO,NIO,AIO
ByteBuffer
FileWriter
BufferedOutputStream
流水线
pipeline
环境变量
重启
任务消失
jenkins
卸载
snap
20.04
Ubuntu
容器
大数据传输
InputStream
学习笔记
CSS
最佳实践
DOM操作
原型链
原型
No such file or Directory
SSH配置
SSH免密
tar
命令
网站优化
取消快照
百度快照
全局配置
修改密码
控制台
gitlab
解码器
Netty
文件传输
sz
rz
Jar
打铁花
羊毛沟
容器日志
docker
规范
博客
rejected
Gerrit
wireshark
代理
Go
http-server
nodejs
package-lock.json
996加班
删除标签
钟薛高
60s新闻接口
每日新闻接口
百度福利
自动回复
localstorage
QQ机器人
redis
近乎
客户端
JuiceSSH
乔恩
加菲猫
新浪邮箱
joe
公告
父亲节
图标
每日新闻
51la
服务器
腾讯云
插件
罗永浩
编码
avatar
小虎墩大英雄
端午劫
端午节
中石化
中国石化
儿童节
win10
激活
shell
金门大桥
旧金山
产品经理
免疫力
熬夜
云旅游
云游
招聘
海尔
halo
校友会
创新
哈工大
鸿蒙
王成录
标题
主题
北京
疫情
域名
沙漠渔溏
方法声明
jpa
表情包
emoji
JavaAgent
姓名测试
姓名打分
起名
百度
收录
SEO
group by
distinct
去重
JDBC
validationQuery
兄弟元素
点击事件
$event
9600
传输延时
波特率
串口
站点统计
站长工具
cnzz
生命周期
refs
vue
replaceAll
replace
JavaScript
软件
FRP
内网外入
内网穿透
学习
解题
leetcode
云计算
中国医药
神思电子
股票
Java
报错
数据块
关键词
风筝
清明节
注解
spring
clang-format
格式化
反向代理
nginx
索引
linux
vim
数据库
仓库管理
git
压缩
winrar
mysql
测试
markdown
目 录
CONTENT
以下是
网络聚合
相关的文章
2024-01-19
前端报表如何实现无预览打印解决方案或静默打印
在前端开发中,除了将数据呈现后,我们往往需要为用户提供,打印,导出等能力,导出是为了存档或是二次分析,而打印则因为很多单据需要打印出来作为主要的单据来进行下一环节的票据支撑, 而前端打印可以说是非常令人头疼的一件事。 为什么令大家头疼呢? 因为前端打印,要强依赖与浏览器的打印预览页面,会天然存在以下弊端: 每一次打印都要弹出来打印预览对话框,如果前端需要批量打印,那么意味着客户要点击无数个关闭按钮,才能实现批量打印,如果一次性打印几百张上千张的报表,则会成为“NightMare”。 前端打印强依赖于浏览器,主流的思路是先将内容转换为PDF文件,再调用浏览器的打印功能进行打印,而生成PDF文件是依赖于浏览器对于字体,边线等的处理,因此浏览器的异同则直接导致打印出来的效果差距很大,有的边线加粗,有的1页数据,打印出来呈现2页,也是让开发者十分苦恼的事情,对于一些打印要求比较高的行业,这就是灾难。 因此如何在前端实现无预览打印,也就是用户点击打印之后直接就使用默认打印机打印出来。针对这个需求,我们验证了一个解决该问题的方案,本贴就来介绍该方案如何实现。 实现思路如下: 后端实现一个接口,接收Blob类型PDF流,然后调用系统默认打印机,将PDF进行静默打印。 前端利用ACTIVEREPORTSJS自带的导出PDF,导出Blob类型,然后通过POST请求调用后端接口将Blob流传给后端进行打印。 具体实现步骤: 前端实现方法: 前端利用ActivereportsJS的PDF.exportDocument无预览导出PDF,该接口返回的result包含data属性和download方法,然后调用后端接口,将result.data传递给后端。 function printPDF() { var ACTIVEREPORTSJS = GC.ActiveReports.Core; var PDF = GC.ActiveReports.PdfExport; var settings = { info: { title: "test", author: "GrapeCity inc.", }, pdfVersion: "1.7", }; var pageReport = new ACTIVEREPORTSJS.PageReport(); pageReport .load("1.rdlx-json") .then(function () { return pageReport.run(); }) .then(function (pageDocument) { return PDF.exportDocument(pageDocument, settings); }) .then(function (result) { let formData = new FormData(); formData.append("file", result.data); fetch("http://localhost:8088/print", { method: 'POST', mode: 'cors', body: formData }) }); } 具体PDF.exportDocument可以参考文档: https://demo.grapecity.com.cn/activereportsjs/demos/api/export/purejs 后端实现方式: 我这边是采用python实现了一个接口,接收前端传递的Blob文件流,然后调用后端部署的服务器默认打印机直接进行静默打印。 后端程序可以部署到服务器上,如果是windows服务器,可以直接下载exe,在服务器上运行。 下载链接: https://pan.baidu.com/s/1De2VdhrGTqX9tHub8gYrSg 提取码: 569c 下载下来是2个exe程序,需要放在同一个文件夹,然后运行PrintAgent.exe,切记这两个程序需要放在同一个文件夹。 注意:如果exe只给服务器上部署,那么前端在打印时调用服务器地址接口打印,最终都会从服务器上连接的打印机打出来。 如果exe给客户端部署了,那么前端打印就可以代码调用localhost地址去打印,最终就会从客户端所连接的默认打印机打印出来; 切换打印机的话,就调整windows的默认打印机就可以。 Linux服务器的话需要将源码拷贝到服务器去运行。 源码如下,也可以根据自己需要进行调整和修改: https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjUzNTMyfGE0YTE2ZDY5fDE2NzM0MTk2ODZ8NjI2NzZ8OTk3MTg%3D 另附 前端100张数据可视化大屏模板,按需取用: https://www.grapecity.com.cn/solutions/wyn/demo
2024-01-19
392
0
0
网络聚合
2024-01-19
对比 elasticsearch 和 mysql
最近阅读了elasticsearch的官方文档,学习了它的很多特性,发现elasticsearch和mysql有很多地方类似,也有很多地方不同。这里做一个对比,帮助大家加深对elasticsearch的理解。 特性 elasticsearch mysql 备注 场景 全文搜索,日志处理,空间数据分析 表结构存储 es 不适合做join操作,mysql 不适合做全文检索 扩展性 动态扩展,能够通过添加node快速提升性能 mysql cluster master 选举 bully 算法,比较id选出master master-slave结构,无需选举 es中master选举可能会出现脑裂问题,配置 minimum_master_nodes参数确保过半选举决定机制 路由算法 routing_factor = num_routing_shards / num_primary_shards shard_num = (hash(_routing) % num_routing_shards) / routing_factor 指定路由分片: my-index-000001/_doc/1?routing=user1&refresh=true 手动路由,或者使用路由组件sharding-jdbc 可靠性 Cross-cluster replication (CCR), 双集群设计 主从复制,双数据中心 内存配置 heap size 推荐 32g,但不要超过内存的一半, 其他需要用到堆外内存的地方,网络,文件缓存,jvm的栈 物理内存的80% 单独的服务器 缓存 filesystem cache, request cahce, query cache 所有cache都是基于node query cache (deprecated) 数据块大小 分片大小 几g ~ 几十g, time based data, 20g ~ 40g 分片数量,每g内存小于20分片 shard越多,维护索引成本越高 shard越大,rebalance越慢 单表数据不超过2kw,3层b+树能存储的数据大概是2kw,如果b+层级变高,查询速度会显著降低 数据结构 json,底层是lucene table,底层是b+ tree 索引 倒排表,fst 正向文件,分块 + 压缩 DocValues, 映射文件 + 压缩 b+数,聚簇/非聚簇索引 定义数据结构的方式 mapping (dynamic mapping & static mapping) schema 支持自动创建数据结构 是 否 事务 near real-time,需要refresh才可以查询到 reaptable read,高级事务 锁 Index blocks,比如 index.blocks.read_only,索引只读 丰富的锁机制,表锁,行锁,间隙锁 文件系统 默认mmapfs,采用内存映射方式访问文件,也支持其他的文件系统,比如fs, niofs, hybirdfs fs 数据恢复 es在写入之前会先将数据写入到translog,用来对异常情况进恢复 flush,lucene 进行提交,并且同时重新开启一段 translog index.translog.sync_interval,持久化translog 间隔,5s index.translog.flush_threshold_size, flush translog阈值大小,512m redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求 es 和 mysql 处理数据恢复的模式基本一致 flush机制 从内存缓存写入磁盘缓存memorybuffer -> filesystem cache(refresh) 刷盘,filesystem cache -> disk ( flush) 定时触发或者 translog > 512M buffer pool -> disk 当redo log满了,或者buffer pool空间不足 es 和 mysql 刷盘模式基本一致 备份 snapshot mysqldump -u root -h host -p --all-databases > backdb.sql 慢日志 比如 index.search.slowlog.threshold.query.warn: 10s long_query_time=10 服务调用方式 rest api mysql connection + sql 数据类型 较为丰富的数据类型,boolean, keyword, long, data, object, nested, range, ip, text, arrays int, data, varchar es 提供了非常多的数据类型,一些是为了支持全文检索,一些能够方便查询,比如range,ip 数据属性 analyzer,分词器 index,是否被索引,没有被索引的字段不可查询 fielddata,如果想对text类型的字段进行聚合,排序,或者执行脚本,就必须设置fielddata属性 doc_values,将_source 转化为表结构放在磁盘上,方便聚合,排序,或者脚本操作,默认支持除了text类型的所有类型 ... 主键索引, 可空,唯一值,自增,默认值 es的数据属性更复杂 查询超时 设置 query timeout set wait_timeout = 10 context es查询需要区分query context, 还是 filter context,前者会进行打分,后者只进行过滤 不需要区分 打分查询 比如match,match_phrase 不支持 runtime field 使用script 创建临时字段 语法支持 select concat (a, b) as c script更灵活,但是性能会降低 精确查询 比如term, terms, ids, exists 语法支持 mysql使用起来更方便 分组聚合查询 比如histogram aggs,terms aggs group by es支持的类型稍微丰富一些,方便开发 指标聚合查询 avg, max, min, sum ,count, cardinality aggs,percentile aggs 语法支持, count(*), distinct es是分布式的,聚合的时候存在一些精度问题 分页 from + size (不适合深分页,有去重问题) search_after + PIT (推荐) scroll (不适合深分页) limit + size 或者进行条件关联,书签 在深分页上的处理方案上基本一致 profile { "profile": true, "query" : { "match" : { "message" : "GET /search" } } } explain script支持 painless script 不支持
2024-01-19
251
0
0
网络聚合
2024-01-19
遗传算法求TSP问题
一、实验内容及目的 本实验以遗传算法为研究对象,分析了遗传算法的选择、交叉、变异过程,采用遗传算法设计并实现了商旅问题求解,解决了商旅问题求解最合适的路径,达到用遗传算法迭代求解的目的。选择、交叉、变异各实现了两种,如交叉有顺序交叉和部分交叉。 二、实验环境 Windows10 开发环境Python 3/Flask 三、实验设计与实现 图1软件结构图 图1软件结构图 Flask.py是后端核心代码,里面是遗传算法实现,index.html为首页,即第一次进入网页的页面,进入之后可以进行参数设置,之后点击开始,参数会传到Flask.py中进行解析和算法运行,最终将迭代结果存到result(存储迭代结果图)和result_path(存储最短路径图)在返回给display.html页面显示。 图2系统界面图 图2系统界面图 输入种群规模、迭代次数、变异概率、选择比例、交叉概率并选择变异方法、选择个体方法、交叉方法。点击开始即可运行该系统。 具体算法流程图: 图3核心算法流程图 流程图描述:首先根据参数城市数量和种群规模初始一个城市坐标矩阵的列表并计算城市间的距离存到矩阵,最后生成一个路径矩阵,这样就可以进入下一步计算适应度,每一条路径都有其路径距离值和适应度,接下来一次进行选择,交叉,变异操作,循环往复,直至达到了参数中的迭代次数限制。 选择—轮盘赌:(这里我的算法选出的种群数量不一定就恰好是根据比例算出的数量) 图3核心算法流程图 图4轮盘赌流程图 选择—锦标赛: 图5三元锦标赛流程图 交叉—顺序交叉: 1、 选切点X,Y 2、 交换中间部分 3、 从第二个切点Y后第一个基因起列出原顺序,去掉已有基因 4、 从第二个切点Y后第一个位置起,将获得的无重复顺序填入 图6顺序交叉动态图 图7顺序交叉静态图 交叉—部分交叉: 1、 选切点oop 2、 选取oop到oop+3部分交换(我这里就是三个,你可以做成随机的几个) 3、 判断是否有重复的,若重复则进行映射,保证形成的新一对子代基因无冲突。 图8部分交叉动态图 变异—两点交换 1、 随机选取两点 2、 两点进行交换 变异—相邻交换 1、 随机选取一点 2、 和该点的后面点进行交换 适应度函数:经过测试得A取5,B取0效果好,所以实验中直接取了A=5,B=0运行 借鉴了sigmoid函数的形式,并对数据做了最大最小标准化,A、B是人为给定的常系数mean、max、min是种群所有个体的目标函数值的均值、最大值、最小值图像如下A=5,B=0 适应值较大的更容易进入下一代种群中 图9适应度函数算术表达式 四、实验结果与测试 表1 遗传算法解决TSP问题的测试用例 测试内容 测试用例 预期结果 实际结果 种群规模 1.不输入2.输入除数字其他3.输入整数数字4.输入小数或者负数 失败失败成功失败 与预期相同 迭代次数 5.不输入6.输入除数字其他7.输入整数数字8.输入小数或者负数 失败失败成功失败 与预期相同 变异方法 9.选择两点交换10.选择相邻交换 成功成功 与预期相同 选择个体方法 11.选择轮盘赌12.选择锦标赛 成功成功 与预期相同 交叉方法 13.选择部分交叉14.选择顺序交叉 成功成功 与预期相同 变异概率 15.不输入16.输入除数字其他17.输入小于1的小数18.输入非小于1的小数或者整数 失败失败成功失败 与预期相同 选择比例 19.不输入20.输入除数字其他21.输入小于1的小数22.输入非小于1的小数或者整数 失败失败成功失败 与预期相同 交叉概率 23.不输入24.输入除数字其他25.输入小于1的小数26.输入非小于1的小数或者整数 失败失败成功失败 与预期相同 随机产生多少个城市 27.不输入28.输入除数字其他29.输入整数数字30. 输入小数或者负数 失败失败成功失败 与预期相同 图10参数设置图 在上述参数设置好之后,即可开始运行系统,最后产生如图11的迭代结果图,最上面是自己的参数设置和最后生成的最小路径min_dist,图示整体为每次迭代的路径距离,可见随着迭代次数增加,路径距离一直减小最后趋于稳定。图12为用python画的路径图,图中横轴纵轴为城市位置的X,Y坐标。 图11 迭代结果图 图12最短路径图 接下来重新选择其他参数来运行一下,看一下有没有区别。 图13参数设置图 图14迭代结果图 图15最短路径图 可以从迭代图像看出,参数不同会导致迭代中结果的不同,第一次参数设置的迭代中在前段迭代不稳定,忽上忽下,之后稳定,而第二次参数设置后迭代很快就稳定,没有忽上忽下的现象,所以不同的选择、变异、交叉方法会使迭代结果不同。所以可以根据随机设定让计算机找到最合适的参数设置。 欢迎关注我的知乎平台,我将持续为您解答一系列问题!
2024-01-19
180
0
0
网络聚合
2024-01-19
concurrent-map 和 sync.Map,我该选择哪个?
concurrent-map 和 sync.Map,我该选择哪个? 官方的map并不是线程安全的,如果我们在多线程中并发对一个map进行读写操作,是会引发panic的。解决方案除了使用锁来对map进行保护外,还有两种方式: 一,开源项目 concurrent-map 提供了可以用来做并发安全的map 二,Go1.9之后,标准库提供了一个sync.Map 这两种并发安全的map,我们应该怎么选择呢? 在concurrent-map我看到这么一段话: 标准库中的sync.Map是专为append-only场景设计的。因此,如果您想将Map用于一个类似内存数据库,那么使用我们的版本可能会受益。你可以在golang repo上读到更多,这里 and 这里 译注:sync.Map在读多写少性能比较好,否则并发性能很差 concurrent-map为什么会有这种表述呢?这篇文章就来庖丁解牛下。 concurrent-map concurrent-map是Golang中一个流行的并发安全的哈希表库,它允许多个goroutine同时对哈希表进行读写操作,而不需要使用显式的锁或同步原语。 该库的核心原理是使用分片锁,将哈希表分成多个小的哈希表片段,并为每个片段分配一个独立的锁。当多个goroutine尝试同时读写同一个片段时,只有该片段上的锁会被锁住,而其他片段的锁则不受影响,从而避免了整个哈希表被锁住的情况。 当进行写操作时,只需要锁住要写入的片段的锁,以确保原子性操作。当进行读操作时,则不需要锁住片段的锁,只需要对该片段上的读取操作进行同步即可。 此外,concurrent-map库还使用了一些优化策略,如缓存哈希值和桶的地址,以减少计算和查找时间,从而提高并发读写性能。 总之,concurrent-map库的原理是基于分片锁和其他优化策略来实现高效的并发安全哈希表。 我们先看它的使用方式: // 创建一个新的 map. m := cmap.New[string]() // 设置变量m一个键为“foo”值为“bar”键值对 m.Set("foo", "bar") // 从m中获取指定键值. bar, ok := m.Get("foo") // 删除键为“foo”的项 m.Remove("foo") 它的New方法创建了一个ConcurrentMap结构 type ConcurrentMap[K comparable, V any] struct { shards []*ConcurrentMapShared[K, V] sharding func(key K) uint32 } 我们看ConcurrentMap结构中的shards,是用来代表map分片之后的这些存储分片ConcurrentMapShared。 而sharing这个匿名函数代表的是分配的hash函数。 而存储分片是一个基础的,带有互斥锁的map type ConcurrentMapShared[K comparable, V any] struct { items map[K]V sync.RWMutex } 所以看到这里我们其实心里明白了个七七八八了,再看下它的New/Set/Get的流程如下: flowchart LR cmap.New --> 创建一个ConcurrentMap --> 初始化ConcurrentMapShared cmap.Set --> 根据需要设置的key查找对应的ConcurrentMapShared --> 加锁写分片中的map cmap.Get --> 根据需要查找的key找出对应分片ConcurrentMapShared --> 加读锁读取分片中的map 是的,基本原理就是如上图所示。concurrent-map就是将一个大map拆分成若干个小map,然后用若干个小mutex 对这些小map进行保护。这样,通过降低锁的粒度提升并发程度。毕竟嘛,一个诸葛亮不如十个臭皮匠。 sync.Map sync.Map是Golang标准库中提供的一个并发安全的哈希表,它与常规的map相比,可以在多个goroutine并发访问时,保证数据的安全性和一致性。 理解sync.Map,最关键就是理解Map结构。 type Map struct { mu Mutex //互斥锁,用于锁定dirty map //优先读map,支持原子操作,注释中有readOnly不是说read是只读,而是它的结构体。read实际上有写的操作 read atomic.Value // readOnly // dirty是一个当前最新的map,允许读写 dirty map[any]*entry // 主要记录read读取不到数据加锁读取read map以及dirty map的次数,当misses等于dirty的长度时,会将dirty复制到read misses int } 这里的sync.Map的逻辑还是比较复杂的。我们再看它的Store函数和Load函数。 func (m *Map) Store(key, value any) func (m *Map) Load(key any) (value any, ok bool) 我们先把Store的代码流程图画出来 flowchart TD Store-->判断read中是否有key{判断read中是否有key} 判断read中是否有key{判断read中是否有key}--有key-->在read中tryStore-->CompareAndSwapPointer-->原子替换read中对应指针 判断read中是否有key{判断read中是否有key}--没有key-->加锁-->判断key的位置 判断key的位置--在read中存在-->dirty中存入这对keyvalue-->read中原子替换指针-->解锁 判断key的位置--在read中不存在\n在dirty中存在-->dirty中原子替换指针-->解锁 判断key的位置--在read中不存在\n在dirty中不存在-->read中所有元素复制到dirty一份-->read中增加这个keyvalue-->dirty中增加这个keyvalue-->解锁 我们看下,这里面有几个步骤是非常有细节的。 首先,第一次判断read中是否有key的时候是没有加锁的,所以当第一次判断结束后,一旦明确read中没有key,要做后续的操作之前,先做一次加锁操作,做完加锁操作之后,又判断了一次key是否在read中。这是为什么呢?其实是由于在加锁这个操作的前后,map还是有可能有变化的,人不可能两次踏入同一个河流,map也不可能在加锁前后两次都不变,所以这里必须进行二次判断,这里可以说是非常细节了。 其次,在判断read或者dirty中已经有key的时候,Store做的操作不是复制一份value到目标结构,而是使用原子替换atomic.StorePointer 来将目标map中key对应的value指针替换为参数value。为什么呢? - 这是极致的性能优化写法,原子替换能减少一次值拷贝操作,做一次指针赋值就能替换拷贝内存操作。从这里我们也能理解为什么这个并发map会放在atomic包中,因为它的实现大量依赖atomic的原子操作。 同样,我们将Load的代码转化为流程图如下, flowchart TD Load --> 判断read中是否有key{判断read中是否有key} 判断read中是否有key{判断read中是否有key}--有key-->直接返回对应的value 判断read中是否有key{判断read中是否有key}--没有key-->加锁-->再次判断read中是否有key{再次判断read中是否有key} 再次判断read中是否有key{再次判断read中是否有key} --有key-->直接返回对应的value 再次判断read中是否有key{再次判断read中是否有key} --没有key-->返回dirty中是否有key-->标记map的miss值加一-->如果miss值大于dirty的个数-->将dirty中的map通过指针切换到read-->dirty置空-->标记map的miss值为0 从Load中我们大致能看出sync.Map的思路。 sync.Map内部使用两个map,read和dirty。其实read的map的作用是挡在读写操作的第一个屏障。如果读写在这个read中能直接操作的话,我们就直接在read中读写,那么就可以完全避免使用锁,性能自然就提升了。 而dirty的作用就相当于是一个缓冲区,一旦要写的key在read中找不到,我们就会先写dirty中。这个好处是什么?也是不去影响读read的操作,不会出现并发读写一个数据结构的情况。 而什么时候dirty的缓存清空同步到read中呢?就是“当map的miss标记大于dirty的个数的时候”。 这里我读的时候也确实有这个疑问,为什么是“当miss标记个数大于dirty个数”。而不是当miss标记个数大于某个值呢?我是这么理解,miss是代表读操作在read中失效的数量,而dirty个数代表写操作在read中失效的数量。如果使用固定值来比对miss个数,那么这个固定值是不好定的,比如一个有10个key的map和一个有10000个key的map如果都是一样的固定值,那是明显不合适的。所以就找了这么个“浮动阈值”。 concurrent-map和sync.map的比较 我们再回到最开始的那一段话: 标准库中的sync.Map是专为append-only场景设计的。因此,如果您想将Map用于一个类似内存数据库,那么使用我们的版本可能会受益。你可以在golang repo上读到更多,这里 and 这里 译注:sync.Map在读多写少性能比较好,否则并发性能很差 通过以上的代码分析,我们看出sync.Map的这个机制,是一个想追求无锁读写的结构,它最好的运行方式是读永远都命中read,写只命中dirty,这用能不用任何锁机制就能做到map读写。而它最差的运行状态是read和dirty不断做替换和清理动作,性能就无法达到预期。而什么时候可能出现最差运行状态呢?- 大量的写操作和大量的读操作。大量读写会导致“map的miss标记大于dirty的个数”。 这个时候sync.Map中第一层屏障会失效,dirty就会频繁变动。 而current-map就相当于是一个比较中等中规中矩的方案。它的每次读写都会用到锁,只是这个锁的粒度比较小。它的最优运行方式是我们的所有并发读写都是分散在不同的hash切片中。它的最差运行方式就是我们所有的并发读写都集中在一个hash切片。但是按照实际运行逻辑,这两种极端情况都不会发生。 所以总结下来,concurrent-map 的这段话确实没有骗我们: sync.Map在读多写少性能比较好,而concurrent-map 在key的hash度高的情况下性能比较好。 在无法确定读写比的情况下,建议使用 concurrent-map。 最后说一句:世上本没有烦恼,选择多了,便有了幸福的烦恼。 参考 https://segmentfault.com/a/1190000015242373
2024-01-19
285
0
0
网络聚合
2024-01-19
《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(6)-Charles安卓手机抓包大揭秘
1.简介 Charles和Fiddler一样不但能截获各种浏览器发出的 HTTP 请求,也可以截获各种智能手机发出的HTTP/ HTTPS 请求。 Charles也能截获 Android 和 Windows Phone 等设备发出的 HTTP/HTTPS 请求。 今天宏哥讲解和分享Charles如何截获安卓移动端发出的 HTTP/HTTPS 请求。 2.环境准备 Charles如果想要实现手机抓包,需要先满足下面 3 个条件: (1)电脑上安装有Charles抓包工具。 (2)安装有Charles的电脑必须跟手机处在同一个网络里,并且手机网络代理必须设置为Charles,当我们的手机发送数据时必须经过Charles这一层服务。 (3)在Charles中设置好捕获 HTTPS。 1.宏哥的环境是Windows 10版本 64位系统。如下图所示: 2.夜神模拟器的安卓版本是:5.1.1 。 如下图所示: 3.大致思路步骤 1.电脑本地安装charles证书 2.查看电脑charles的IP和端口号 3.手机连接charles,抓取简单的http 4.charles设置ssl proxy setting 5.手机安装charles证书 4.为什么需要安装Charles的CA证书呢? 1)先理清一些概念的东西: a)简单来说,https是http的安全版本,超文本传输协议http是以明文发送数据,而https是具有安全性的ssl加密传输协议,可以这么认为https=http+ssl。 b)采用https的服务器必须从CA申请一个用于证明服务器用途类型的证书,证书是唯一性,只用于对应的服务器。客户端要认可这个服务器是否是安全的,可以进行访问或者交易等操作,则需要进行对服务端的验证。 下图是客户端对服务器的验证过程: c)ssl证书,遵循了ssl协议,在客户端和服务器之间建立了一条ssl安全通道,一般ssl证书都是在验证服务器身份后颁发给客户端。 d)由于ssl技术已建立在所有主要的浏览器和web服务器程序中,因此,仅需安装服务器证书就可以激活ssl协议,所以客户端通过信任该证书,就相当于信任了该主机(服务器)。 下图是客户端和服务端加密通讯的流程: 2)通过以上一个简单的理顺之后,这也就为什么当我们在使用Charles进行抓包的时候需要安装证书,可以通过ssl数字证书中的私用密钥来解译加密的信息,展示在Charles中,但是Charles有一个特殊的地方,就是实际上客户端安装的是Charles的CA证书,然后Charles安装服务器的CA证书,实际上流程还是一样的。 5.安卓手机抓包配置 5.1Charles PC端http配置 http是默认配置好的,要是默认安装完没配置好,我们就自己配置一下,默认的端口号是“8888”我们后面设置手机代理的的时候也要注意这个端口要保持一致。 Charles的配置:这是要打开代理功能,具体操作步骤如下: 1.启动Charles,点击“Proxy-->Proxy Settings”,然后在Proxy Settings中填好端口(8888),并且勾选上“Enable transparent HTTP proxying”,最后点击“OK”。如下图所示: 2.查看运行Charles的电脑的IP地址,可以在命令行中运行ipconfig或者直接查看网络配置,如下图所示: 3.可以对照一下当前所安装的Charles中的ip地址是否一致,你可以直接在Charles上可以直接查看Charles的IP和端口,点击“Help-->SSL Proxying”,然后点击“Install Charles Root Certificate on a Mobile Device or Remote Browser”,如下图所示: 5.2Charles PC端SSL(https)配置 无论电脑端还是手机端进行抓取Https的包,都需要先安装对应证书才能使用。具体操作步骤如下: 1.打开charles,点击help-->SSL Proxying-->Install Charles root Certificate 安装证书,如下图所示: 2.点击完“Install Charles root Certificate”后,然后点击“安装证书”,如下图所示: 3.点击“安装证书”后,选择存储位置“本地计算机”,点击“下一步”,如下图所示: 4.证书存储位置选择‘将所有的证书都放入下列存储’,然后点击证书存储后的“浏览”,证书存储选择“受信任的根证书颁发机构”,点击“确定”,如下图所示: 5.点击“下一步”,如下图所示: 6.点击“完成”,提示导入成功。如下图所示: 7.点击proxy》 SSL Proxyng Settings。如下图所示: 8.打开界面如下 弄到跟我一样就可以 如果没有*;443自己按add手动添host :*,port:443 。如下图所示: 这样就可以抓取PC端的http和https类型的包了,接下来我们就来进行安卓手机端代理的配置。 5.3安卓手机设置 本节内容适合所有的 Android设备。下面以夜神模拟器为例进行讲解,其他品牌的模拟器和真实的手机操作方法与此差不多。具体操作步骤如下: 1.在确定了手机和Charles在同一局域网下之后, 那么我们来到Android手机的设置选项下,找到夜神模拟器手机当前连接的WLAN(一些 Android 手机是单击右边的箭头,有的是长按弹出对话框),如下图所示: 2.看到有一个wifi信号,长按这个信号,出现修改网络的弹窗。如下图所示: 3.点击修改网络,选中高级选项,打开高级选项,将代理设为手动,代理服务器主机名填写电脑的IP,端口号填写为主机抓包工具的监听端口。如下图所示: 4.点击保存,就成功完成代理的设置了。如下图所示: 5.电脑出现允许代理的提示,点击Allow即可,如下图所示: 那么到此Android手机的网络代理设置就到此为止,其他不同型号的Android模拟器和真机设置大同小异,以此类推,实在不会的自己可以百度一下。到此处表示已经可以抓http的手机包了。 5.4测试Charles捕获手机发出的 HTTP 1.打开手机上的浏览器,在浏览器中输入链接:http://open.vipexam.org。中科VIPExam考试学习资源数据库网站用的是 HTTP 协议而不是 HTTPS 协议,查看 Fiddler 是否捕获到了 HTTP 数据包。如下图所示: 2.打开手机上的 APP,在 APP 中进行一些操作,查看 Fiddler 是否能捕获到 HTTP 数据包。如下图所示: 如果抓不到 HTTP 的包,很可能是 Windows 防火墙的问题,到控制面板中关闭防火墙后再试试。 5.5测试Fiddler捕获手机发出的HTTPS 1.打开手机上的浏览器,在浏览器中输入HTTPS协议,查看Charles是否捕获到了HTTPS数据包。一直在报证书安全警告错误,无法抓取,因此需要我们安装证书,原因宏哥在抓取PC端Web页面包已经说过了,这里就不做赘述了。如下图所示: 2.打开手机上的APP,在APP中进行一些操作,查看Fiddler是否能捕获到HTTPS数据包。又出现了Unknown,如下图所示: 到此,我们知道了要想抓取手机端Https的数据,还的配置证书,证书不用问了,还是Charles下发的。 5.6Android手机配置证书 通过前边宏哥的测试,我们知道在抓取Android手机数据包的时候 跟web端也是一样,都需要配置证书,否则是无法正常进行抓包的。之前已经在我们的android手机上配置好了Charles的代理服务了,那么现在就可以通过ip+port的方式来访问Charles从而下载对应的证书。具体操作步骤如下: 1.在Android手机上打开(自带)的浏览器,输入:http://chls.pro/ssl 来下载证书。如果不出意外的话就会出现如下界面: 2.给证书命名为:CharlesRoot,点击“确定”,如下图所示: 3.点击“确定”后,需要输入凭据存储的密码。如下图所示: 4.再次点击“确定”,提示需要设置锁屏密码(注:选择安装的文件后,需要输入手机的锁屏密码。Android一定要有锁屏密码才能安装证书),如下图所示: 5.点击“确定”。按要求设置一个手机密码,自己设置一个,记住密码就行,最后不用了去系统-安全-密码中去掉即可,如下图所示: 6.完成锁屏密码后,提示证书已安装,证书安装成功后,如果你的手机系统没有设置密码或者锁屏图案,则系统会提示你设置锁屏图案或者密码。如下图所示: 7.证书安装好后,查看已信任证书:具体位置在【设置--->安全--->信任的凭据--->用户】,如下图所示: 6.开始Android抓包 通过前边的配置,我们现在可以开始抓安卓手机的https的包了。 1.打开手机上的浏览器,在浏览器中输入HTTPS协议的网站,例如:百度。如下图所示: 7.小结 Charles和Fiddler一样,一个手机可以安装多个证书,但是每安装的一个证书里面都设置有IP地址,所以:安装的证书和电脑IP是一一对应的,当前的这个证书只能针对某一台电脑使用,更换电脑后,该证书将不能使用,只能重新安装与更换的电脑的IP相同的证书才能使用。 对了,关于Android7.0的版本在Fiddler那里已经详细地介绍了,只不过是工具换了一下,原理都差不多,这里和后边就不再做介绍了。而且这里介绍的和Fiddler抓包安卓手机的设置也基本一致,种种原因这里又啰嗦水了一遍。凑合看吧。
2024-01-19
267
0
0
网络聚合
2024-01-19
SpringBoot项目动态定时任务之 ScheduledTaskRegistrar(解决方案一)
前言 在做SpringBoot项目的过程中,有时客户会提出按照指定时间执行一次业务的需求。 如果客户需要改动业务的执行时间,即动态地调整定时任务的执行时间,那么可以采用SpringBoot自带的ScheduledTaskRegistrar类作为解决方案来实现。 在单一使用ScheduledTaskRegistrar类解决定时任务问题的时候,可能会达不到预期的动态调整定时任务的效果。 如果灵活配合使用对应的工具类(ThreadPoolTaskScheduler类),则可以方便地对动态调整定时任务进行管理。 本文会从问题出发,详细介绍ScheduledTaskRegistrar类是如何解决动态调整定时任务的思路,并给出关键的代码示例,帮助大家快速地上手学习。 目录 一、问题背景 在指定的某一时刻执行业务; 可以手动地更改执行时间。 在实际项目中,很少会有傻瓜式地去指定某一时间就触发某个业务的场景,执行业务的时间不是一成不变的,而是动态地随着客户所指定的时间进行调整的。 二、痛点所在 如果单一地使用SpringBoot自带的ScheduledTaskRegistrar去实现,那么可能会有以下问题: 只能按照指定的时间去执行,更改执行时间需要重启服务; 无法删除该定时任务,或者删除后无法再启动新的定时任务。 业务逻辑与触发器的代码耦合度太高,无法将业务代码从ScheduledTaskRegistrar类中抽离出去。 /** * @author Created by zhuzqc on 2023/1/30 15:28 */ @Slf4j @Component @EnableScheduling public class ScheduleTaskDemo implements SchedulingConfigurer { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { //Runnable线程注册任务 Runnable taskOne = () -> { //需要执行的业务逻辑,一般会在这里封装好 logger.info("----------业务执行结束----------"); }; //任务的触发时间,一般使用 cron 表达式 Trigger triggerOne = triggerContext -> { Date nextExecTime = null; try { // 此处指定 cron 表达式 String cron = "0 00 12 ? * *"; if (StringUtils.isBlank(cron)) { // 提示参数为空 logger.info("trigger定时器的 cron 参数为空!"); // 如果为空则赋默认值,每天中午12点 cron = "0 00 12 ? * *"; } logger.info("---------->定时任务执行中<---------"); CronTrigger cronTrigger = new CronTrigger(cron); nextExecTime = cronTrigger.nextExecutionTime(triggerContext); } catch (Exception e) { e.printStackTrace(); log.info(e.getMessage()); } return nextExecTime; }; taskRegistrar.addTriggerTask(taskOne, triggerOne); } } 上述代码只能实现在指定的时间去触发定时任务,无法对 cron 表达式进行更改,如果更改则需要重新启动服务,非常地“傻瓜”。 而在实际的编码过程中,业务逻辑代码需要单独地剥离开(解耦),如何做到业务逻辑代码和触发器代码都能访问到外部业务数据,是设计过程中需要考虑到的关键。 三、解决思路 //TODO:如果要在此处将业务逻辑和时间触发器进行捆绑,那么在这个实现类中无法获取到来自该类外部的业务数据; //TODO:要解决这样的问题,就要找到一个办法:既能将两者抽离,又能实现灵活触发定时任务。 在这里介绍一个名为ThreadPoolTaskScheduler类,通过源码得知,该类实现了SchedulingTaskExecutor和TaskScheduler接口。 该类中schedule(Runnable task, Trigger trigger)方法,通过分别传入线程任务(业务逻辑)和Trigger触发器对象作为参数,支持动态创建指定 cron 表达式的定时任务。 该方法源码如下: @Override @Nullable public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) { ScheduledExecutorService executor = getScheduledExecutor(); try { ErrorHandler errorHandler = this.errorHandler; if (errorHandler == null) { errorHandler = TaskUtils.getDefaultErrorHandler(true); } return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule(); } catch (RejectedExecutionException ex) { throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); } } 以下部分是对该方法的具体使用,核心思路如下 : 实例化ThreadPoolTaskScheduler类对象; 实例化ScheduledFuture类对象,用于初始化调用schedule()后的值。 将携带有Runnable和Trigger的ScheduledFuture类对象作为Map的value进行装配。 根据Map的key对定时任务进行管理,达到添加和删除的目的。 四、代码示例 代码示例分为两部分: 第一部分是关于ThreadPoolTaskScheduler类和schedule()方法的使用; /** * @author @author Created by zhuzqc on 2023/1/30 15:39 * 任务线程池管理工具 */ public class TaskSchedulerUtil { private static final Logger logger = LoggerFactory.getLogger(TaskSchedulerUtil.class); /** * 线程调度工具对象,作为该类的成员变量 */ private static ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); /** *初始化 map 对象,装配 schedule 方法的返回对象为 value 值 */ private static Map<String, ScheduledFuture<?>> scheduledFutureMap = new HashMap<String, ScheduledFuture<?>>(); static { threadPoolTaskScheduler.initialize(); } /** * 将Runnable对象和Trigger对象作为参数传入该静态方法 * @param runnable * @param trigger * @param 定时任务id */ public static void put(Runnable runnable, Trigger trigger, String id) { // 将携带有Runnable和Trigger的ScheduledFuture类对象作为 Map 的 value 进行装配 ScheduledFuture<?> scheduledFuture = threadPoolTaskScheduler.schedule(runnable, trigger); // 放入 Map 中作为 value scheduledFutureMap.put(id, scheduledFuture); logger.info("---添加定时任务--->" + id); } /** * 通过上述 put 方法的参数id(定时任务id)标识,将定时任务移除出 map * @param id */ public static void delete(String id) { ScheduledFuture<?> scheduledFuture = scheduledFutureMap.get(id); // 条件判断 if (scheduledFuture != null && scheduledFuture.isCancelled()) { scheduledFuture.cancel(true); } scheduledFutureMap.remove(id); logger.info("---取消定时任务--->" + id); } } 第二部分是关于结合实际业务,引入实际业务数据的代码demo。 /** * @author Created by zhuzqc on 2023/1/30 15:58 */ @Slf4j @Component @EnableScheduling public class TaskScheduleDemo{ @Resource private AAAMapper aaaMapper; @Resource private BBBService bbbService; private Logger logger = LoggerFactory.getLogger(this.getClass()); // 引入外部的业务数据对象 public void putHiredTask(CCCEntity cccEntity){ //TODO: 将业务线程和定时触发器交由线程池工具管理:创建业务线程对象,并对属性赋初始化值(有参构造) TaskSchedulerUtil.put(new TaskThreadDemo(cccEntity,aaaMapper,bbbService), new Trigger() { @Override public Date nextExecutionTime(TriggerContext triggerContext) { //获取定时触发器,这里可以获取页面的更新记录,实现定时间隔的动态调整 Date nextExecTime = TaskTransUtils.StringToDateTime(cccEntity.getSendTime()); //cron 表达式转换工具类 String cron = TaskTransUtils.getDateCronTime(nextExecTime); try { if (StringUtils.isBlank(cron)) { // 提示参数为空 logger.info("trackScheduler定时器的 cron 参数为空!"); // 如果为空则赋默认值,每天早上9:00 cron = "0 00 09 ? * *"; } logger.info("-------定时任务执行中:" + cron + "--------"); CronTrigger cronTrigger = new CronTrigger(cron); nextExecTime = cronTrigger.nextExecutionTime(triggerContext); } catch (Exception e) { e.printStackTrace(); log.info(e.getMessage()); } return nextExecTime; } },"该定时任务的id"); } } 五、文章小结 动态定时任务的总结如下: 单一使用ScheduledTaskRegistrar类,无法达到预期动态调整定时任务的效果; 实际的开发场景中,需要业务逻辑代码和触发器代码都能访问到外部业务数据; 配合ThreadPoolTaskScheduler类和该类中的schedule()方法可以达到动态调整定时任务的效果。 如果大家有遇到这样类似的问题,并且为此感到困惑时,希望以上文章的介绍可以帮助到大家。 最后,欢迎大家的指正和交流!
2024-01-19
237
0
0
网络聚合
2024-01-19
前端Linux部署命令与流程记录
以前写过一篇在Linux上从零开始部署前后端分离的Vue+Spring boot项目,但那时候是部署自己的个人项目,磕磕绊绊地把问题解决了,后来在公司有了几次应用到实际生产环境的经验,发现还有很多可以补充的地方,很多指令和下载地址每次用到的时候再找就相对麻烦,通过这篇文章可以做一个记录。 另外,之前漏掉了很重要的Linux版本,因为以前不太了解,一直使用的都是CentOS 7,这次选择系统的时候看到CentOS后续会停止维护,所以决定换一个版本学习一下,Linux版本非常多,通常我们可以选择CentOS、Debian、Ubuntu等,具体的区别可以自己去查下,这里我选择了Debian 11.1作为新的系统环境。 npm和node(通过NVM安装) 前端部署最先想到的就是NPM和Node,但是Node的版本切换可能是个问题,所以使用了NVM,这里之前也写过一篇NVM、NPM、Node.js的安装选择,不过是针对Windows系统的,实际在Linux上运行还有些许不同。 写文档时NVM的最新版本为0.39.3,使用时可以按需要更改版本。 1. 安装 1.1 在线安装 NVM的GitHub地址 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash 1.2 手动安装 也可以下载GitHub的Release版本文件上传到服务器手动安装,安装目录是/root/.nvm。 创建安装目录 mkdir /root/.nvm 解压文件到安装目录 tar -zxvf nvm-0.39.3.tar.gz --strip-components 1 -C /root/.nvm -z:有gzip属性的 -x:解压 -v:显示所有过程 -f: 使用档案名字,切记,这个参数是最后一个参数,后面只能接档案名。 –strip-component=1 代表解压出来的文件,剥离前一个路径 -C, --directory=DIR 改变至目录 DIR 2. 配置环境变量 如果选择了手动安装,需要自己配置一下环境变量才能在全局使用nvm指令。 #编辑文件 vim ~/.bashrc #按“i”进入insert模式,将下面两行代码写入文件,按“esc”退出insert模式,按“:”进入底行模式,输入“wq!”回车,即保存并退出 export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm #刷新配置 source ~/.bashrc 3. 判断是否安装成功 nvm -v 4.安装node和npm #查看线上版本的指令与Windows版不同 nvm ls-remote #选择一个LTS版本,如下图所示18.13.0,关于版本选择的问题以前也说过 nvm install 18.13.0 5.查看和切换版本 #查看npm和node版本 npm -v node -v #如果提示没有找到指令,则查看已安装的node版本 nvm ls #切换到18.13.0版本 nvm use 18.13.0 nrm nrm(npm registry manager)是npm的镜像源管理工具,直接使用连接国外资源下载有时会慢,可以切换至其他镜像源。 #全局安装 npm install -g nrm #查看可选的源 nrm ls #切换至淘宝源 nrm use taobao Nginx Debian安装 如果使用的是Debian的系统,可以通过如下代码直接安装,但实际生产环境多半要添加模块,还是需要手动下载源码编译,参考下文的步骤。 sudo apt update sudo apt install nginx 下载 Nginx下载 选择稳定版本下载,上传到服务器 或者通过远程仓库下载 wget http://nginx.org/download/nginx-1.22.1.tar.gz 安装 # 1.解压文件 tar -zxvf nginx-1.22.1.tar.gz 2.进入目录 cd nginx-1.22.1 按需编译 没有特殊需求的话,在nginx的解压目录里执行 make && make install 就可以编译安装nginx了,但是实际的线上环境还需要添加一些模块来满足线上的业务需求,我们的项目中用到了两个插件: http_realip_module(真实IP) http_ssl_module(SSL协议) 编译前要先安装依赖,虽然两个系统要安装的库不同,但是功能类似,从上到下依次是: gcc编译器:用于 make 编译 正则库:用于在配置文件内进行目录匹配 zlib库:用于对HTTP包的内容做gzip格式的压缩,Nginx编译过程和Http请求过程中需要gzip格式的压缩 OpenSSL库:提供SSL协议的编译环境 # CentOS yum install gcc yum install pcre-devel yum install zlib zlib-devel yum install openssl openssl-devel # Debian apt install -y build-essential apt install -y libpcre3 libpcre3-dev apt install -y zlib1g-dev apt install -y openssl libssl-dev 编译操作如下: #查看可用模块 ./configure --help #配置 ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_realip_module 参数说明: --prefix 用于指定nginx编译后的安装目录 --add-module 为添加的第三方模块 --with..._module 表示启用的nginx模块,如此处启用了两个模块 没有安装依赖的话这里会提示错误,正确执行的结果如下: 接下来就是编译安装: #编译 make #安装 make install #可以简写成 make && make install 创建软链接 安装完成后,通过 nginx -V 查看一下已安装的模块信息,会提示没有这样的指令,这是因为指令只能在对应目录下执行,对于需要频繁使用的工具来说并不方便,想要在全局使用,可以创建一个软链接: sudo ln -s /usr/local/nginx/sbin/nginx /usr/sbin/nginx 之后就可以在任意目录执行nginx指令,执行 nginx -V 可以看到编译安装的模块: 日志 nginx日志默认记录在安装目录的 logs 文件夹下,访问日志全部写入在 access.log 文件中,随着时间推移,日志文件体积会逐渐增加,单个文件过大会导致可读性变差。通过修改配置的方式,我们可以让nginx日志自动按日期分割。 #日志时间变量 map $time_iso8601 $logdate { '~^(?<ymd>\d{4}-\d{2}-\d{2})' $ymd; default 'date-not-found'; } log_format json_log escape=json '{"host":"$http_host $request","time":"$time_local","timestamp":"$msec","from_ip":"$remote_addr","real_ip":"$http_x_forwarded_for","user_agent":"$http_user_agent"}'; access_log logs/access-$logdate.log json_log; 加在下图位置: 在更新配置之前,还要授予文件夹权限,要按日期创建日志文件需要有在目录中写入的权限。 chmod -R 777 /usr/local/nginx/logs 在查找无权限创建文件这个问题时候,看到一些回答是修改运行nginx的用户为root,但是我觉得这样在安全上可能会有问题,虽然777也是文件夹的最高权限,任何用户都可以修改,但相对会好一些,可能还有更好的方法,欢迎评论留言。 需要的话,error.log 也可以以同样的方式按时间分割。 可以创建一个软链接方便查看日志 ln -s /usr/local/nginx/logs /root/logs 启动 无论是启动还是重启,都要先测试配置,没有问题再启动。 #测试配置 nginx -t #配置没有问题后启动 nginx #重启 nginx -s reload pm2 #安装 npm install pm2@latest -g #查看 pm2 -h 如果提示命令未找到,也是与之前一样在bin目录下建立软链接
2024-01-19
220
0
0
网络聚合
2024-01-19
2023年计划,步入工作的第6年
好几年没写计划了,有些浑浑噩噩,过上了一段躺平的生活。 因为上篇博客收到了很多评论,有人评论:想让我写一下躺平后的生活,也有人评论:为啥分手的?(PS上篇博客《工作5年的老程序员的年终总结》地址:https://www.cnblogs.com/liudw-0215/p/16997452.html),所以我准备写了这篇博客。 疫情三年,终于放开了,感觉这三年一直被压抑着,放开之后心情大好,我刚“阳康”,大家也都好了吧?祝大家身体健康、工作顺利! 回复上篇博客的评论 第一个评论:想让我写一下躺平后的生活。也不是完全躺平,但经历上段工作的严重加班,对工作已经提不起兴趣,就是不想工作!但做了很多其他的事,提几个来说说。 读书 内向只是我的一个性格特点,更特别的是我能坐在那一天一句话不说都可以(PS我旁边的同事可以作证,哈哈哈)。 我也是最近两年开始喜欢上读书的,之前完全不知道读书的好处,可能因为中国的应试教育,给中国孩纸太大压力了,所以上大学之后,都逃避学习和读书。 怎么喜欢上读书的 要追溯到2020年11月,那时股市经历19/20年的牛市,市场异常火爆,大量散户跑步进场,我也是作为一颗新鲜的韭菜在2020年11月进场的,正是牛市尾巴,涨了最后3个月,到2021年2月达到最高点,然后一路下跌,今年2022年更不用说了,超级大熊市,作为一颗韭菜当然逃不了被割的命运,到现在还亏1万。 从来不歌颂苦难,但苦难、挫折是最能让人成长的,当暴跌之后,我难受极了,没有刚开始赚点小钱的那种盲目自信了(PS那时心里想着哥就是中国巴菲特),玩基金真的让我体会到很多人性,比如巴特特很经典的一句话:别人贪婪时我恐惧,别人恐惧时我贪婪! 亏钱就要想办法,然后开始去看各种消息、各种大V,发现一点用都没有,然后开始看书吧,看能不能有用,然后慢慢的看了很多书,最后发现也没啥用。但养成我读书的习惯和爱好,之后就开始看很多其他类型的书,心理学、哲学等。 考公 有段时间在准备考公,之前我对体制内很多抵触,想着就是一帮混吃等死之辈,但一句话打破我这种想法:你讨厌里面的人,因为你没在其中! 第二个评论:为啥分手的? 我们是异地恋,因为疫情三年,我见面机会太少了,感情淡了 2023年计划 提高技术 其实还没具体想好提高哪些方面,我是linux C/C++服务器开发,我第一份工作是纯C语言的,没有用到C++,C++都是后来自学的,学的不够深入,很多语言特性没有掌握,而且更新换代太快了,工作中主要还是用c++11,但现在都出C++20了! 所以,第一个计划,就是深入学习一下C++ 第二计划,多学一些python,脚本语言对于做服务端的来说,也算是必需掌握的,什么shell、python,但python只会写一些简单的脚本,学了一点爬虫,并没有更加深入的学习。 第三个计划,学习协程,多掌握一些架构思想。 培养业余技能 1、学习手机摄影,有人可能会想这有啥可以学的?拍出照片和拍出好看的照片是有很大的区别的。 2、学习视频剪辑 3、学习文案,提高自己的文笔,多写一些东西 旅游计划 1、过完年之后,准备带父母来北京溜达一圈,他们还没有来过北京,再让他们坐一回飞机。 2、去北欧或其他国外城市,我还没坐过飞机,推荐一本书《这么慢,那么美》,是讲北欧的,看完之后我就决定要去北欧了,我觉得北欧更像“理想国” 3、国内暂时没有想去的地方,主要人多的地方我不想去,去国内旅游,去哪都排队,就超级烦,说几个还有点兴趣的地方吧,云南、三亚、西藏 希望以上计划能实现吧,2023年底,我再来复盘!
2024-01-19
201
0
0
网络聚合
2024-01-19
[数据结构] 二分查找 (四种写法)
二分查找 二分查找 二分查找(Binary Search)也叫作折半查找,前提是查找的顺序结构是有序的,我们一般在数组上进行二分查找。 二分查找就好像猜数字大小游戏一样。假设要数字目标值属于 [1, 1000] 范围内,当我们猜的数字小于这个目标值时("Too low"),我们需要往大去猜;反之大于这个目标值时("Too high"),我们需要往小去猜。当然这里猜的方式并不是盲目的,我们每次都取中间值去猜,每猜一次可以缩小当前一半的范围,这样可以大大提高效率。二分查找本质上也是这样的过程,时间复杂度为 O(logn) ,在较大数据情况下相比线性查找要快非常多。 我们定义一个左指针 left 标记当前查找区间的左边界,一个右指针 right 标记当前查找范围的右边界。每次取 mid 来判断当前取的值是否等于目标值 target。如果等于 target 就直接返回 mid ;如果小于目标值 target ,那么将左边界变为 mid + 1,缩小区间范围继续在 [mid + 1, right] 范围内进行二分查找,如果大于目标值 target ,那么将右边界变为 mid - 1,缩小区间范围继续在 [left, mid - 1] 范围内进行二分查找。 假如最后出现了 left > right 的情况,说明区间范围大小缩小到 0 都无法找到该目标值,那么很明显数组中不存在这个目标值 target,此时退出循环,返回 -1 。 二分查找图解 (1) (2) (3) 二分查找代码 //二分查找 int BinarySearch(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left <= right){ int mid = (left + right) >> 1; if(v[mid] == target) return mid; if(v[mid] < target) left = mid + 1; else right = mid - 1; } return -1; } 二分查找 递归 上面二分查找也可以写成递归的形式。大致步骤为: (1)当前层 mid 位置元素等于目标值 target,直接 return mid; (2)如果小于目标值,递归搜索 [mid + 1, right] 范围; (3)如果大于目标值,递归搜索 [left, mid - 1] 范围。 //二分查找法递归写法 int BinarySearchRec(vector<int> &a, int left, int right, int target) { int mid = (left + right) >> 1; if(left <= right){ if(a[mid] == target) return mid; if(a[mid] < target) return BinarySearchRec(a, mid + 1, right, target); else return BinarySearchRec(a, left, mid - 1, target); } return -1; } 二分查找 元素起始位置 查找等于目标值的第一个位置 数组中可能存在连续的多个位置元素都等于目标值 target ,当我们要查找第一个出现的位置,我们需要保证查找到位置的左边所有元素都满足小于 target,右边所有元素都满足大于等于 target。 当出现 a[mid] < target 时,说明我们要查找的位置一定在 [mid + 1, right] 范围内,当然也可以写成 (mid, right] ;当出现 a[mid] >= target 时,说明要查找的位置有可能是当前的 mid,也有可能是当前 mid 左边的某个位置,所以此时要查找的位置一定在 [left, mid] 范围内。 因此,当 a[mid] < target,将 left = mid + 1 ;当 a[mid] >= target,将 right = mid。 当最后 left == right 即两个指针相遇时退出循环,最后要判断一下相遇位置处的元素是否等于目标值 target。如果等于目标值,就返回 left 或者 right,如果不等于目标值,说明不存在该元素,那么就返回 -1 。 查找等于目标值起始位置图解 (1) (2) (3) (4) 查找等于目标值起始位置代码 int BinarySearchfirst(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right) >> 1; if(v[mid] < target) left = mid + 1; else right = mid; } return v[left] == target ? left : -1; } 二分查找 元素终止位置 查找等于目标值的最后一个位置 当我们要查找最后一个出现的位置,我们需要保证查找到位置的左边所有元素都满足小于等于 target,右边所有元素都满足大于 target。 当出现 a[mid] > target 时,说明我们要查找的位置一定在 [left, mid - 1] 范围内,当然也可以写成 [left, mid) ;当出现 a[mid] <= target 时,说明要查找的位置有可能是当前的 mid,也有可能是当前 mid 右边的某个位置,所以此时要查找的位置一定在 [mid, right] 范围内。 因此,当 a[mid] > target,将 right = mid - 1 ;当 a[mid] <= target,将 left = mid。 当最后 left == right 即两个指针相遇时退出循环,最后要判断一下相遇位置处的元素是否等于目标值 target。如果等于目标值,就返回 left 或者 right,如果不等于目标值,说明不存在该元素,那么就返回 -1 。 但是要注意的是,在这里取 mid 时,我们不能和之前一样取 (left + right) >> 1,而要采取 (left + right + 1) >> 1 的形式。我们可以假设只有数组两个元素,两个元素都等于目标值,显然此时我们要找的最后一个位置为下标1。我们模拟一下,初始情况下 left 为 0,right 为 1,如果采用 (left + right) >> 1,那么此时的 mid 就等于 0,这个时候出现了 left 依旧等于之前 left 的情况,那么显然这个时候区间无法进行缩小,left 会一直等于 0,这个时候就陷入死循环了。 我们仔细看一下,当前这种情况的特点是 left + 1 == right,那么我们取 mid 时: mid = (left + right) >> 1 = (2 * left + 1) >> 1 = left,很明显left 会一直等于 mid。 如果我们能够让 left 在这种 left + 1 == right 情况下使得 left 取到 right 即往后一位,那么我们的区间范围就得以缩小,也不会陷入死循环。所以我们采用 (left + right + 1) >> 1 取 mid,问题就得以解决了。此时: mid = (left + right + 1) >> 1 = (2 * left + 2) >> 1 = left + 1 = right,可以看出 left 往后了一位。 归根究底还是因为整形数据除 2 会自动进行向下取整的问题,进行 +1 操作后向上取整就可以解决这个问题。 查找等于目标值终止位置图解(取mid向上取整) (1) (2) (3) (4) 查找等于目标值终止位置图解(取mid向下取整)* (1) (2) (3) (4) 查找等于目标值终止位置代码 int BinarySearchlast(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right + 1) >> 1; if(v[mid] > target) right = mid - 1; else left = mid; } return v[left] == target ? left : -1; } 相关试题及完整程序 相关试题 Acwing 789.数的范围 Leetcode 34.在排序数组中查找元素的第一个和最后一个位置 完整程序 #include<iostream> #include<vector> using namespace std; //二分查找 int BinarySearch(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left <= right){ int mid = (left + right) >> 1; if(v[mid] == target) return mid; if(v[mid] < target) left = mid + 1; else right = mid - 1; } return -1; } //二分查找法递归写法 int BinarySearchRec(vector<int> &a, int left, int right, int target) { int mid = (left + right) >> 1; if(left <= right){ if(a[mid] == target) return mid; if(a[mid] < target) return BinarySearchRec(a, mid + 1, right, target); else return BinarySearchRec(a, left, mid - 1, target); } return -1; } //查找元素起始位置 int BinarySearchfirst(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right) >> 1; if(v[mid] < target) left = mid + 1; else right = mid; } return v[left] == target ? left : -1; } //查找元素终止位置 int BinarySearchlast(vector<int> &v, int target){ int left = 0, right = v.size() - 1; while(left < right){ int mid = (left + right + 1) >> 1; if(v[mid] > target) right = mid - 1; else left = mid; } return v[left] == target ? left : -1; } int main(){ vector<int> v = {7,7,7,7}; cout<<BinarySearchFirst(v, 7)<<endl; cout<<BinarySearchLast(v, 7)<<endl; cout<<BinarySearchRec(v, 0, v.size() - 1, 7)<<endl; cout<<BinarySearch(v, 7)<<endl; }
2024-01-19
367
0
0
网络聚合
2024-01-19
Python绘制神经网络模型图
本文介绍基于Python语言,对神经网络模型的结构进行可视化绘图的方法。 最近需要进行神经网络结构模型的可视化绘图工作。查阅多种方法后,看到很多方法都比较麻烦,例如单纯利用graphviz模块,就需要手动用DOT语言进行图片描述,比较花时间;最终,发现利用第三方的ann_visualizer模块,可以实现对已有神经网络的直接可视化,过程较为方便,本文对此加以详细介绍。 此外,如果需要在MATLAB中实现神经网络构建与简单的可视化,大家可以查看MATLAB人工神经网络ANN代码;如果要借助软件或在线工具进行不需要代码的神经网络可视化,可以查看我们后期的博客。 相关环境的版本信息:Anaconda Navigator:1.10.0;Python:3.8.5。 首先,下载与安装必要的模块ann_visualizer。打开Anaconda Prompt (Soft)。 在弹出的界面中输入: pip install ann_visualizer 即可完成ann_visualizer模块的安装。 接下来,我们就可以借助以下仅仅一句代码对神经网络模型进行可视化了。 ann_viz(DNNModel,view=True,filename='G:/CropYield/02_CodeAndMap/01_SavedPicture/MyANN.gv',title='ANN') 其中,DNNModel就是我们已经建立好的神经网络模型,任意神经网络模型均可——可以是一个简单的浅层人工神经网络,也可以是一个相对复杂的全连接深度神经网络;view表示是否在代码执行后直接显示绘图结果;filename是绘图结果的保存位置,需要以.gv结尾;title就是神经网络图片的名称。 在这里,我就直接以Python TensorFlow深度神经网络回归:keras.Sequential中介绍并建立的深度神经网络加以可视化。 第一次运行代码时发现,出现以下报错: 报错提示我没有安装graphviz模块,但其实之前在进行随机森林决策树的可视化(也就是Python实现随机森林RF并对比自变量的重要性)时,早已经将这一模块安装过了,并且当时用到graphviz这一模块的代码也没有报错。通过查阅,发现这里需要重新安装一下python-graphviz这个新的模块。因此我们打开Anaconda Prompt (Soft),输入代码: conda install python-graphviz 如下图所示: 安装之后这里就不报错啦~ 结果紧接着又报出了新的错误,说我的keras模块没有安装: 这就不对了,明明在进行深度神经网络构建时都没有出现问题,甚至在这一句报错的下方连深度神经网络的误差绘制曲线都能显示(误差曲线的精度的确很差,大家不用在意~因为这里我们仅仅是做一个示范,所以Epoch次数就调得很小),说明keras模块应该是没问题的。 随后考虑到,这里报错的keras是在ann_visualizer的文件环境下,可能是环境不同导致的。打开Anaconda Navigator,在base (root)环境下确实找不到keras: 那么我这里就图方便,直接在base (root)环境下再安装一个keras。安装方法同上,输入代码即可: pip install keras 然后这里就不报错啦~ 接下来,经过多次尝试发现,这一方法进行神经网络可视化时,一是不能存在正则化层与BatchNormalization层;二是LeakyReLU层与Dropout层的总数量不能过多,否则绘图结果会出现问题——这就显得这一可视化方法稍微有点鸡肋了,但是其对于基本的神经网络绘图而言其实也已经很不错了。因此,我就将Python TensorFlow深度神经网络回归:keras.Sequential中的神经网络上述对应的层删除或注释掉。 如下图,首先,将当初我的代码对应的LeakyReLU层与Dropout层注释掉: 然后执行代码,即可进行神经网络的可视化。且绘制出的图将会自动打开在PDF阅读软件中,如下图(版面有限,这里就只是绘图结果的一部分)。 还是很不错的~我们还可以直接将其转换为图片格式,看起来就更直观了: 如果再取消Dropout层的注释,即绘图时加上Dropout层,也还是很不错的: 如果我们再加上LeakyReLU层,就成了这个乱七八糟、不太正确的样子(原图实在太大了,就只给大家截取图片的一部分): 可以看到,这样的话就有些问题了。 最后,我们看一下这个ann_visualizer第三方库的源代码,可以看到该库支持绘图的不同种类神经网络层;如果大家的神经网络包含这些层,就可以用ann_visualizer这一第三方库进行绘图。 至此,大功告成。
2024-01-19
279
0
0
网络聚合
2024-01-19
新项目决定用 JDK 17了
大家好,我是风筝,公众号「古时的风筝」,专注于 Java技术 及周边生态。 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面。 最近在调研 JDK 17,并且试着将之前的一个小项目升级了一下,在测试环境跑了一段时间。最终,决定了,新项目要采用 JDK 17 了。 JDK 1.8:“不是说好了,他发任他发,你用 Java 8 吗?” 不光是我呀,连 Spring Boot 都开始要拥护 JDK 17了,下面这一段是 Spring Boot 3.0 的更新日志。 Spring Boot 3.0 requires Java 17 as a minimum version. If you are currently using Java 8 or Java 11, you'll need to upgrade your JDK before you can develop Spring Boot 3.0 applications. Spring Boot 3.0 需要 JDK 的最低版本就是 JDK 17,如果你想用 Spring Boot 开发应用,你需要将正在使用的 Java 8 或 Java 11升级到 Java 17。 选用 Java 17,概括起来主要有下面几个主要原因: 1、JDK 17 是 LTS (长期支持版),可以免费商用到 2029 年。而且将前面几个过渡版(JDK 9-JDK 16)去其糟粕,取其精华的版本; 2、JDK 17 性能提升不少,比如重写了底层 NIO,至少提升 10% 起步; 3、大多数第三方框架和库都已经支持,不会有什么大坑; 4、准备好了,来吧。 拿几个比较好玩儿的特性来说一下 JDK 17 对比 JDK 8 的改进。 密封类 密封类应用在接口或类上,对接口或类进行继承或实现的约束,约束哪些类型可以继承、实现。例如我们的项目中有个基础服务包,里面有一个父类,但是介于安全性考虑,值允许项目中的某些微服务模块继承使用,就可以用密封类了。 没有密封类之前呢,可以用 final关键字约束,但是这样一来,被修饰的类就变成完全封闭的状态了,所有类都没办法继承。 密封类用关键字 sealed修饰,并且在声明末尾用 permits表示要开放给哪些类型。 下面声明了一个叫做 SealedPlayer的密封类,然后用关键字 permits将集成权限开放给了 MarryPlayer类。 public sealed class SealedPlayer permits MarryPlayer { public void play() { System.out.println("玩儿吧"); } } 之后 MarryPlayer 就可以继承 SealedPlayer了。 public non-sealed class MarryPlayer extends SealedPlayer{ @Override public void play() { System.out.println("不想玩儿了"); } } 继承类也要加上密封限制。比如这个例子中是用的 non-sealed,表示不限制,任何类都可以继承,还可以是 sealed,或者 final。 如果不是 permits 允许的类型,则没办法继承,比如下面这个,编译不过去,会给出提示 "java: 类不得扩展密封类:org.jdk17.SealedPlayer(因为它未列在其 'permits' 子句中)" public non-sealed class TomPlayer extends SealedPlayer { @Override public void play() { } } 空指针异常 String s = null; String s1 = s.toLowerCase(); JDK1.8 的版本下运行: Exception in thread "main" java.lang.NullPointerException at org.jdk8.App.main(App.java:10) JDK17的版本(确切的说是14及以上版本) Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "s" is null at org.jdk17.App.main(App.java:14) 出现异常的具体方法和原因都一目了然。如果你的一行代码中有多个方法、多个变量,可以快速定位问题所在,如果是 JDK1.8,有些情况下真的不太容易看出来。 yield关键字 public static int calc(int a,String operation){ var result = switch (operation) { case "+" -> { yield a + a; } case "*" -> { yield a * a; } default -> a; }; return result; } 换行文本块 如果你用过 Python,一定知道Python 可以用 'hello world'、"hello world"、''' hello world '''、""" hello world """ 四种方式表示一个字符串,其中后两种是可以直接支持换行的。 在 JDK 1.8 中,如果想声明一个字符串,如果字符串是带有格式的,比如回车、单引号、双引号,就只能用转义符号,例如下面这样的 JSON 字符串。 String json = "{\n" + " \"name\": \"古时的风筝\",\n" + " \"age\": 18\n" + "}"; 从 JDK 13开始,也像 Python 那样,支持三引号字符串了,所以再有上面的 JSON 字符串的时候,就可以直接这样声明了。 String json = """ { "name": "古时的风筝", "age": 18 } """; record记录类 类似于 Lombok 。 传统的Java应用程序通过创建一个类,通过该类的构造方法实例化类,并通过getter和setter方法访问成员变量或者设置成员变量的值。有了record关键字,你的代码会变得更加简洁。 之前声明一个实体类。 public class User { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } 使用 Record类之后,就像下面这样。 public record User(String name) { } 调用的时候像下面这样 RecordUser recordUser = new RecordUser("古时的风筝"); System.out.println(recordUser.name()); System.out.println(recordUser.toString()); 输出结果 Record 类更像是一个实体类,直接将构造方法加在类上,并且自动给字段加上了 getter 和 setter。如果一直在用 Lombok 或者觉得还是显式的写上 getter 和 setter 更清晰的话,完全可以不用它。 G1 垃圾收集器 JDK8可以启用G1作为垃圾收集器,JDK9到 JDK 17,G1 垃圾收集器是默认的垃圾收集器,G1是兼顾老年代和年轻代的收集器,并且其内存模型和其他垃圾收集器是不一样的。 G1垃圾收集器在大多数场景下,其性能都好于之前的垃圾收集器,比如CMS。 ZGC 从 JDk 15 开始正式启用 ZGC,并且在 JDK 16后对 ZGC 进行了增强,控制 stop the world 时间不超过10毫秒。但是默认的垃圾收集器仍然是 G1。 配置下面的参数来启用 ZGC 。 -XX:+UseZGC 可以用下面的方法查看当前所用的垃圾收集器 JDK 1.8 的方法 jmap -heap 8877 JDK 1.8以上的版本 jhsdb jmap --heap --pid 8877 例如下面的程序采用 ZGC 垃圾收集器。 其他一些小功能 1、支持 List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法实例化对象; 2、Stream API 有一些改进,比如 .collect(Collectors.toList())可以直接写成 .toList()了,还增加了 Collectors.teeing(),这个挺好玩,有兴趣可以看一下; 3、HttpClient重写了,支持 HTTP2.0,不用再因为嫌弃 HttpClient 而使用第三方网络框架了,比如OKHTTP; 升级 JDK 和 IDEA 安装 JDK 17,这个其实不用说,只是推荐一个网站,这个网站可以下载各种系统、各种版本的 JDK 。地址是 https://adoptium.net/。 还有,如果你想在 IDEA 上使用 JDK 17,可能要升级一下了,只有在 2021.02版本之后才支持 JDK 17。 如果觉得还不错的话,给个推荐吧! 公众号「古时的风筝」,Java 开发者,专注 Java 及周边生态。坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,跟我一起变优秀!
2024-01-19
338
0
0
网络聚合
2024-01-19
Rust中的智能指针:Box
Rc
Arc
Cell
RefCell
Weak
Rust中的智能指针是什么 智能指针(smart pointers)是一类数据结构,是拥有数据所有权和额外功能的指针。是指针的进一步发展 指针(pointer)是一个包含内存地址的变量的通用概念。这个地址引用,或 ” 指向”(points at)一些其 他数据 。引用以 & 符号为标志并借用了他们所 指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以在Rust中应用得最多。 智能指针是Rust中一种特殊的数据结构。它与普通指针的本质区别在于普通指针是对值的借用,而智能指针通常拥有对数据的所有权。并且可以实现很多额外的功能。 Rust智能指针有什么用,解决了什么问题 它提供了许多强大的抽象来帮助程序员管理内存和并发。其中一些抽象包括智能指针和内部可变性类型,它们可以帮助你更安全、更高效地管理内存,例如Box<T> 用于在堆上分配值。Rc<T> 是一种引用计数类型,可以实现数据的多重所有权。RefCell<T> 提供内部可变性,可用于实现对同一数据的多个可变引用 它们在标准库中定义,可以用来更灵活地管理内存,智能指针的一个特点就是实现了Drop和Deref这两个trait。其中Drop trait中提供了drop方法,在析构时会去调用。Deref trait提供了自动解引用的能力,让我们在使用智能指针的时候不需要再手动解引用了 Rust有哪些常用智能指针 Box<T>是最简单的智能指针,它允许你在堆上分配值并在离开作用域时自动释放内存。 Rc<T>和Arc<T>是引用计数类型,它们允许多个指针指向同一个值。当最后一个指针离开作用域时,值将被释放。Rc<T>不是线程安全的,而Arc<T>是线程安全的。 内部可变性类型允许你在不可变引用的情况下修改内部值。Rust中有几种内部可变性类型,包括Cell<T>,RefCell<T>和UnsafeCell<T>。 Cell<T>是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。Cell<T>只能用于Copy类型,因为它通过复制值来实现内部可变性。 RefCell<T>也是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>不同,RefCell<T>可以用于非Copy类型。它通过借用检查来确保运行时的安全性。 UnsafeCell<T>是一个底层的内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>和RefCell<T>不同,UnsafeCell<T>不提供任何运行时检查来确保安全性。因此,使用UnsafeCell<T>可能会导致未定义行为。 此外,Rust还提供了一种弱引用类型Weak<T>,它可以与Rc<T>或Arc<T>一起使用来创建循环引用。Weak<T>不会增加引用计数,因此它不会阻止值被释放。 Box<T> Box<T>是最简单的智能指针,它允许你在堆上分配值并在离开作用域时自动释放内存。 Box<T>通常用于以下情况: 当你有一个类型,但不确定它的大小时,可以使用Box<T>来在堆上分配内存。例如,递归类型通常需要使用Box<T>来分配内存。 当你有一个大型数据结构并希望在栈上分配内存时,可以使用Box<T>来在堆上分配内存。这样可以避免栈溢出的问题。 当你希望拥有一个值并只关心它的类型而不是所占用的内存时,可以使用Box<T>。例如,当你需要将一个闭包传递给函数时,可以使用Box<T>来存储闭包。 总之,当你需要在堆上分配内存并管理其生命周期时,可以考虑使用Box<T>。 下面是一个简单的例子: fn main() { let b = Box::new(5); println!("b = {}", b); } 复制代码 这里定义了变量 b,其值是一个指向被分配在堆上的值 5 的 Box。这个程序会打印出 b = 5;在这个例子 中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上) 和它所指向的数据(位于堆上)。 但是Box<T>无法同时在多个地方对同一个值进行引用 enum List { Cons(i32, Box), Nil, } use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); } 复制代码 编译会得出如下错误: error[E0382]: use of moved value: a,因为b和c无法同时拥有a的所有权,这个时候我们要用Rc<T> Rc<T> Reference Counted 引用计数 Rc<T>是一个引用计数类型,它允许多个指针指向同一个值。当最后一个指针离开作用域时,值将被释放。Rc<T>不是线程安全的,因此不能在多线程环境中使用。 Rc<T>通常用于以下情况: 当你希望在多个地方共享数据时,可以使用Rc<T>。解决了使用Box<T>共享数据时出现编译错误 当你希望创建一个循环引用时,可以使用Rc<T>和Weak<T>来实现。 下面是一个简单的例子,演示如何使用Rc<T>来共享数据: use std::rc::Rc; fn main() { let data = Rc::new(vec![<span class="hljs-number">1, <span class="hljs-number">2, <span class="hljs-number">3]); let data1 = data.clone(); let data2 = data.clone(); println!("data: {:?}", data); println!("data1: {:?}", data1); println!("data2: {:?}", data2); } 复制代码 这个例子中,我们使用Rc::new来创建一个新的Rc<T>实例。然后,我们使用clone方法来创建两个新的指针,它们都指向同一个值。由于Rc<T>实现了引用计数,所以当最后一个指针离开作用域时,值将被释放。 但是Rc<T>在多线程中容易引发线程安全问题,为了解决这个问题,又有了Arc<T> Arc<T> Atomically Reference Counted 原子引用计数 Arc<T>是一个线程安全的引用计数类型,它允许多个指针在多个线程之间共享同一个值。当最后一个指针离开作用域时,值将被释放。 Arc<T>通常用于以下情况: 当你希望在多个线程之间共享数据时,可以使用Arc<T>,是Rc<T>的多线程版本。 当你希望在线程之间传递数据时,可以使用Arc<T>来实现。 下面是一个简单的例子,演示如何使用Arc<T>来在线程之间共享数据: use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(vec![<span class="hljs-number">1, <span class="hljs-number">2, <span class="hljs-number">3]); let data1 = data.clone(); let data2 = data.clone(); let handle1 = thread::spawn(move || { println!("data1: {:?}", data1); }); let handle2 = thread::spawn(move || { println!("data2: {:?}", data2); }); handle1.join().unwrap(); handle2.join().unwrap(); } 复制代码 这个例子中,我们使用Arc::new来创建一个新的Arc<T>实例。然后,我们使用clone方法来创建两个新的指针,它们都指向同一个值。接着,我们在线程中使用这些指针来访问共享数据。由于Arc<T>实现了线程安全的引用计数,所以当最后一个指针离开作用域时,值将被释放。 Weak<T> 弱引用类型 Weak<T>是一个弱引用类型,它可以与Rc<T>或Arc<T>一起使用来创建循环引用。Weak<T>不会增加引用计数,因此它不会阻止值被释放。 当你希望创建一个循环引用时,可以使用Rc<T>或Arc<T>和Weak<T>来实现。 Weak<T>通常用于以下情况: 当你希望观察一个值而不拥有它时,可以使用Weak<T>来实现。由于Weak<T>不会增加引用计数,所以它不会影响值的生命周期。 下面是一个简单的例子,演示如何使用Rc<T>和Weak<T>来创建一个循环引用: use std::rc::{Rc, Weak}; struct Node { value: i32, next: Option<Rc<Node>>, prev: Option<Weak<Node>>, } fn main() { let first = Rc::new(Node { value: 1, next: None, prev: None }); let second = Rc::new(Node { value: 2, next: None, prev: Some(Rc::downgrade(&first)) }); first.next = Some(second.clone()); } 复制代码 这个例子中,我们定义了一个Node结构体,它包含一个值、一个指向下一个节点的指针和一个指向前一个节点的弱引用。然后,我们创建了两个节点first和second,并使用Rc::downgrade方法来创建一个弱引用。最后,我们将两个节点连接起来,形成一个循环引用。 需要注意的是,由于Weak<T>不会增加引用计数,所以它不会阻止值被释放。当你需要访问弱引用指向的值时,可以使用upgrade方法来获取一个临时的强引用。如果值已经被释放,则upgrade方法会返回None。 UnsafeCell<T> UnsafeCell<T>是一个底层的内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>和RefCell<T>不同,UnsafeCell<T>不提供任何运行时检查来确保安全性。因此,使用UnsafeCell<T>可能会导致未定义行为。 由于UnsafeCell<T>是一个底层类型,它通常不直接用于应用程序代码。相反,它被用作其他内部可变性类型(如Cell<T>和RefCell<T>)的基础。 下面是一个简单的例子,演示如何使用UnsafeCell<T>来修改内部值: use std::cell::UnsafeCell; fn main() { let x = UnsafeCell::new(1); let y = &x; let z = &x; unsafe { *x.get() = 2; *y.get() = 3; *z.get() = 4; } println!("x: {}", unsafe { *x.get() }); } 复制代码 这个例子中,我们使用UnsafeCell::new来创建一个新的UnsafeCell<T>实例。然后,我们创建了两个不可变引用y和z,它们都指向同一个值。接着,在一个unsafe块中,我们使用get方法来获取一个裸指针,并使用它来修改内部值。由于UnsafeCell<T>实现了内部可变性,所以我们可以在不可变引用的情况下修改内部值。 需要注意的是,由于UnsafeCell<T>不提供任何运行时检查来确保安全性,所以使用它可能会导致未定义行为。因此,在大多数情况下,你应该使用其他内部可变性类型(如Cell<T>和RefCell<T>),而不是直接使用UnsafeCell<T>。 Cell<T> Cell<T>是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。Cell<T>只能用于Copy类型,因为它通过复制值来实现内部可变性。 Cell<T>通常用于以下情况: 当你需要在不可变引用的情况下修改内部值时,可以使用Cell<T>来实现内部可变性。 当你需要在结构体中包含一个可变字段时,可以使用Cell<T>来实现。 下面是一个简单的例子,演示如何使用Cell<T>来修改内部值: use std::cell::Cell; fn main() { let x = Cell::new(1); let y = &x; let z = &x; x.set(2); y.set(3); z.set(4); println!("x: {}", x.get()); } 复制代码 这个例子中,我们使用Cell::new来创建一个新的Cell<T>实例。然后,我们创建了两个不可变引用y和z,它们都指向同一个值。接着,我们使用set方法来修改内部值。由于Cell<T>实现了内部可变性,所以我们可以在不可变引用的情况下修改内部值。 需要注意的是,由于Cell<T>通过复制值来实现内部可变性,所以它只能用于Copy类型。如果你需要在不可变引用的情况下修改非Copy类型的值,可以考虑使用RefCell<T>。 RefCell<T> RefCell<T>是一个内部可变性类型,它允许你在不可变引用的情况下修改内部值。与Cell<T>不同,RefCell<T>可以用于非Copy类型。 RefCell<T>通过借用检查来确保运行时的安全性。当你尝试获取一个可变引用时,RefCell<T>会检查是否已经有其他可变引用或不可变引用。如果有,则会发生运行时错误。 RefCell<T>通常用于以下情况: 当你需要在不可变引用的情况下修改内部值时,可以使用RefCell<T>来实现内部可变性。 当你需要在结构体中包含一个可变字段时,可以使用RefCell<T>来实现。 下面是一个简单的例子,演示如何使用RefCell<T>来修改内部值: use std::cell::RefCell; fn main() { let x = RefCell::new(vec![<span class="hljs-number">1, <span class="hljs-number">2, <span class="hljs-number">3]); let y = &x; let z = &x; x.borrow_mut().push(4); y.borrow_mut().push(5); z.borrow_mut().push(6); println!("x: {:?}", x.borrow()); } 复制代码 这个例子中,我们使用RefCell::new来创建一个新的RefCell<T>实例。然后,我们创建了两个不可变引用y和z,它们都指向同一个值。接着,我们使用borrow_mut方法来获取一个可变引用,并使用它来修改内部值。由于RefCell<T>实现了内部可变性,所以我们可以在不可变引用的情况下修改内部值。 需要注意的是,由于RefCell<T>通过借用检查来确保运行时的安全性,所以当你尝试获取一个可变引用时,如果已经有其他可变引用或不可变引用,则会发生运行时错误。from刘金,转载请注明原文链接。感谢!
2024-01-19
300
0
0
网络聚合
2024-01-19
Educational Codeforces Round 143 (Rated for Div. 2) A-E
比赛链接 A 题意 有两座塔由红蓝方块组成,分别有 \(n,m\) 个方块,一次操作可以把一座塔塔顶的方块移动到另一座塔的塔顶,问通过操作是否能使每座塔中没有颜色相同的相邻方块。 题解 知识点:贪心。 注意到,操作最多能拆掉一对相邻的方块,因此统计两座塔不合法的对数。 如果超过 \(1\) 对,那么无解。 如果只有 \(1\) 对,那么操作一定使得塔顶相对,塔顶若颜色一样就无解,否则有解。 如果没有不合法的方块,也有解。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { int n, m; cin >> n >> m; string s, t; cin >> s >> t; int mis = 0; for (int i = 1;i < n;i++) if (s[i] == s[i - 1]) { mis++; } for (int i = 1;i < m;i++) if (t[i] == t[i - 1]) { mis++; } if (mis >= 2 || mis == 1 && s.back() == t.back()) return false; else cout << "YES" << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << "NO" << '\n'; } return 0; } B 题意 坐标轴上覆盖了 \(n\) 个线段,指定一个点坐标为 \(k\) ,能否通过删除一些线段使得覆盖指定点的线段严格最多。 题解 知识点:贪心。 直接去除没有覆盖 \(k\) 的线段,此时若 \(k\) 覆盖数严格最多,那么有解;否则,去除任意一条线段,都会使 \(k\) 和一些点同时减 \(1\) ,不会使得 \(k\) 覆盖数严格最多,所以无解。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { int n, k; cin >> n >> k; vector<int> vis(57); for (int i = 1;i <= n;i++) { int l, r; cin >> l >> r; if (l <= k && k <= r) vis[l], vis[r + 1]--; } for (int i = 1;i <= 50;i) vis[i] += vis[i - 1]; bool ok = 1; for (int i = 1;i <= 50;i++) if (i != k) ok &= vis[k] > vis[i]; if (ok) cout << "YES" << '\n'; else return false; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << "NO" << '\n'; } return 0; } C 题意 有 \(n\) 杯茶,第 \(i\) 杯茶有 \(a_i\) 毫升。有 \(n\) 个人,每个人每次最多能喝 \(b_i\) 毫升。 第一轮,第 \(i\) 个人喝第 \(i\) 杯茶,第 \(i\) 杯茶减少 \(\min(a_i,b_i)(1\leq i\leq n)\) 毫升。 第二轮,第 \(i\) 个人喝第 \(i-1\) 杯茶,第 \(1\) 个人不喝,第 \(i\) 杯茶减少 \(\min(a_i,b_{i+1})(1\leq i\leq n-1)\) 毫升。 以此类推,问最后每个人喝了多少毫升茶。 题解 知识点:枚举,前缀和,差分,二分。 考虑枚举每杯茶的贡献。第 \(i\) 杯茶会被 \([i,n]\) 内的人喝,但茶会被喝完,所以设 \(sumb\) 为 \(b\) 的前缀和,找到最大的位置 \(j\) 满足 \(sumb_j - sumb_{i-1}\leq a_i \iff sum_{j} \leq sum_{i-1} + a_i\) ,即 \([i,j]\) 的人能喝到自己的上限,第 \(j+1\) 个人能喝到剩下的 \(a_i - (sum_j - sum_{i-1})\) 。 我们用差分数组 \(cnt\) 完成 \([i,j]\) 的区间加 \(1\) ,最后前缀和就能得到第 \(i\) 个人能喝到自己的上限多少次。再用数组 \(delta\) 记录第 \(i\) 个人喝了多少剩下的茶,最后 \(cnt_ib_i + delta_i\) 即第 \(i\) 个人的毫升数。 时间复杂度 \(O(n\log n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; int a[200007], b[200007]; ll sumb[200007]; int cnt[200007]; ll delta[200007]; bool solve() { int n; cin >> n; for (int i = 1;i <= n;i++) cin >> a[i]; for (int i = 1;i <= n;i++) cin >> b[i]; for (int i = 1;i <= n;i++) sumb[i] = sumb[i - 1] + b[i], delta[i] = 0, cnt[i] = 0; for (int i = 1;i <= n;i++) { int id = upper_bound(sumb + i, sumb + n + 1, a[i] + sumb[i - 1]) - sumb - 1; cnt[i]; cnt[id + 1]--; delta[id + 1] += a[i] - (sumb[id] - sumb[i - 1]); } for (int i = 1;i <= n;i) { cnt[i] += cnt[i - 1]; cout << 1LL * cnt[i] * b[i] + delta[i] << ' '; } cout << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; } D 题意 给定一个有 \(n\) 个点 \(n\) 条边的无向带权图,保证 \(n\) 为 \(6\) 的倍数,组成 \(\dfrac{n}{3}\) 个三元环: \((1,2,3),(4,5,6),\cdots\) 。 现在给每个点染上红或蓝两种颜色,要求红色有 \(\dfrac{n}{2}\) 个点、蓝色也有 \(\dfrac{n}{2}\) 个点 。 定义一种染色方案的价值为,两端颜色不同的边的权值总和。 设所有染色方案种价值最大为 \(W\) ,问有多少种染色方案的价值为 \(W\) 。 题解 知识点:贪心,排列组合。 显然,一个三元环的最多能贡献两条边,即两红一蓝或两蓝一红,刚好我们可以使得所有三元环都能贡献出两条边,我们对每个三元环贪心地选最大的两条边即可达到最大价值。因此,染色方案必然是每个三元环两红一蓝或两蓝一红,且最大的两条边端点颜色不同。 考虑三元环的分配方案,我们需要一半的两红一蓝另一半两蓝一红,因此方案数是 \(\dbinom{n/3}{n/6}\) 。再考虑每一种分配方案的不同染色方案,显然三边权值相同的三元组能贡献三种方案,而两边权值相同的三元组当且仅当三边中不同的权值是最大值时会贡献两种方案,分别记为 \(cnt_1,cnt_2\) ,则总方案数为 \(3^{cnt_1} \cdot 2^{cnt_2}\cdot \dbinom{n/3}{n/6}\) 。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; const int P = 998244353; const int N = 3e5 + 7; namespace CNM { int qpow(int a, ll k) { int ans = 1; while (k) { if (k & 1) ans = 1LL * ans * a % P; k >>= 1; a = 1LL * a * a % P; } return ans; } int fact[N], invfact[N]; void init(int n) { fact[0] = 1; for (int i = 1;i <= n;i++) fact[i] = 1LL * i * fact[i - 1] % P; invfact[n] = qpow(fact[n], P - 2); for (int i = n;i >= 1;i--) invfact[i - 1] = 1LL * invfact[i] * i % P; } int C(int n, int m) { if (n == m && m == -1) return 1; //* 隔板法特判 if (n < m || m < 0) return 0; return 1LL * fact[n] * invfact[n - m] % P * invfact[m] % P; } } int w[300007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; CNM::init(n / 3); for (int i = 1;i <= n;i++) cin >> w[i]; int cnt1 = 0, cnt2 = 0; for (int i = 3;i <= n;i += 3) { if (w[i - 2] == w[i - 1] && w[i - 1] == w[i]) cnt1++; else if (w[i] == w[i - 1] || w[i - 1] == w[i - 2] || w[i - 2] == w[i]) { int mx = max({ w[i - 2],w[i - 1],w[i] }); if (count(w + i - 2, w + i + 1, mx) == 1) cnt2++; } } cout << 1LL * CNM::qpow(3, cnt1) * CNM::qpow(2, cnt2) % P * CNM::C(n / 3, n / 6) % P << '\n'; return 0; } E 题意 有 \(n\) 个怪物,第 \(i\) 个怪物的血量有 \(h_i\) ,血量降为 \(0\) 时立刻死亡,你有两种攻击方式: 指定一个怪物,对其普通攻击,造成 \(1\) 点伤害,消耗 \(1\) 点魔法。普通攻击可以使用无限次。 指定一个怪物,对其释放爆炸魔法,造成的伤害取决于注入的魔法,如果想要消耗 \(x\) 点魔法注入其中,会造成 \(x\) 点伤害。 爆炸魔法具有连锁性,如果被魔法击中的怪物死了,假设是第 \(i\) 个怪物,那么 \(i-1,i+1\) 两个怪物会受到 \(h_i-1\) 的伤害,以此类推,直至没有怪物死掉。这个魔法只能使用一次。 问,最少需要多少魔法能消灭所有怪物。 题解 知识点:枚举,贪心,单调栈。 显然,我们可以枚举释放爆炸魔法的位置,取造成伤害最多的即可。 对于每个位置,都可以向两侧扩展,不妨先考虑左侧部分。 我们设 \(l_i\) 为在第 \(i\) 个位置释放爆炸魔法后,左侧(包括自己)能造成的最大伤害。我们考虑在 \(i\) 处依次向左扩展的两种情况: 对于 \(j<i\) ,若 \(h_j \geq h_i - (i-j)\) ,则说明 \(j\) 不能被消灭,但我们可以在之前通过普通攻击把血量降到可以消灭的血量,因此还是可以造成 \(h_i-(i-j)\) 的伤害。 对于 \(j<i\) ,一旦出现 \(h_j < h_i-(i-j)\) ,则说明 \(j\) 虽然能被消灭,但会降低之后的魔法伤害,后续计算会由 \(h_j\) 直接支配,我们通过 \(l_i\) 直接转移即可,并停止扩展。 注意到,如此操作的复杂度是平方的,我们考虑优化复杂度。我们可以将条件中的变量转换为 \(h_j - j,h_i -i\) ,就可以绑定成一个值了。我们发现,满足情况1造成的伤害都是连续减 \(1\) 的,而满足情况 \(2\) 后直接加 \(f_j\) 后停止,所以我们只要求左侧第一个满足 \(h_j - j < h_i-i\) 的位置 \(j\) ,就可以直接得到造成的伤害 \(l_j + \dfrac{(i-j)(h_i-(i-j)+1 + h_i)}{2}\) 。 显然,这个过程可以用单调递增栈实现的,其可以维护一个字典序最小的极长递增子序列(极长递增子序列指,一个不能继续递增的递增子序列),而子序列相邻两元素之间的在原数组的其他元素,一定都大于等于这两个元素,因此是不需要比较的。于是,对于一个值,我们比较维护的子序列,就可以跳过一些不需要比较的位置。整个过程,每个元素只会出入一次维护的子序列,因此复杂度是线性的。 对于右侧,我们把 \(h\) 反转,重新算一遍 \(h_i-i\) 做相同的事情得到 \(r_i\) ,再将 \(r_i\) 反转,\(h\) 复原。最后,设血量总和为 \(sum\) ,枚举每个位置 \(i\) 得到 \(sum - (l_i + r_i - h_i) + h_i\) 取最小值。 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; int n; int h[300007], hi[300007]; ll l[300007], r[300007]; void calc(ll *f) { stack<int> stk; stk.push(0); for (int i = 1;i <= n;i++) { while (stk.size() > 1 && hi[stk.top()] >= hi[i]) stk.pop(); int len = min(h[i], i - stk.top()); f[i] = f[stk.top()] + 1LL * len * (h[i] + (h[i] - len + 1)) / 2; stk.push(i); } } bool solve() { cin >> n; ll sum = 0; for (int i = 1;i <= n;i++) cin >> h[i], sum += h[i]; for (int i = 1;i <= n;i++) hi[i] = h[i] - i; calc(l); reverse(h + 1, h + n + 1); for (int i = 1;i <= n;i++) hi[i] = h[i] - i; calc(r); reverse(h + 1, h + n + 1); reverse(r + 1, r + n + 1); ll ans = 1e18; for (int i = 1;i <= n;i++) ans = min(ans, sum - (l[i] + r[i] - h[i]) + h[i]); cout << ans << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; }
2024-01-19
288
0
0
网络聚合
2024-01-19
设计模式实践---策略+简单工厂对大量计算公式的处理
业务流程: 1.用户根据需要选择的实验方案,每个实验方案对应一种计算公式,计算公式例如下面这种 2.将带有实验数据的PDF文件上传到特定位置,对PDF文件进行解析后将数据数据保存到数据库。 3.遍历所有方案,对每种方案使用特定的公式对数据库中的数据进行 重构前实现: 遍历方案,使用IF语句对使用的公式进行判断,而后在IF块里对数据进行处理 IF(Formula=='F1'){ //F1的处理... } IF(Formula=='F2'){ //F2的处理... } IF(Formula=='F3'){ //F2的处理... } 这样实现的问题就是程序太过庞大,八十多个公式就要有八十多个判断语句,并且判断内部的处理现也是很庞大的,这就导致了这个程序可读性很差,维护以及调试都十分困难。 重构 这里考虑使用策略模式+简单工厂进行重构 策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化。 使用策略模式重构后的程序结构: 定义一个AbstractFormula抽象类,在抽象类中定义一个Calculation计算方法,这个方法返回经过计算的结果的集合,在各个实现类(即具体公式)中实现Calculation方法 定义上下文类,在上下文类中指定要使用的公式,定义Caclute方法用于返回结果。 实现代码 /// <summary> /// 简易的方案信息 /// </summary> internal class Schemeinformation { //方案ID public string SchemeID { get; set; } //方案名称 public string SchemeName { get; set; } //公式 public string Formula { get; set; } } /// <summary> /// 单个结果 /// </summary> internal class Result { public Result(Schemeinformation schemeinformation, string Result) { Schemeinformation = schemeinformation; FinalResult=Result; } public string FinalResult { get; set; } </span><span style="color: rgba(0, 0, 255, 1)">public</span> Schemeinformation Schemeinformation { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; } }</span></pre> </div> <div class="cnblogs_code code-toolbar" style="overflow-x: scroll;padding: 20px;"> <pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">抽象的公式类 </span> internal abstract class AbstractFormula { //公式计算后的结果集 public List<string>? tempResultList; public abstract List<string> Caclution(Schemeinformation schemeinformation); } //具体实现 internal class Formula1 : AbstractFormula { public override List<string> Caclution(Schemeinformation schemeinformation) { tempResultList = new List<string>(); //计算过程... 对tempResultList赋值 return tempResultList; } } internal class Formula2 : AbstractFormula { public override List<string> Caclution(Schemeinformation schemeinformation) { tempResultList = new List<string>(); //计算过程...其中需要使用到Schemeinformation中的信息 //对tempResultList赋值 return tempResultList; } } /// <summary> /// 上下文类 使用公式,并管理结果集 /// </summary> internal class Context { AbstractFormula formula; public void SetFromula(AbstractFormula formula) { this.formula = formula; } public List<string> Caclute(Schemeinformation schemeinformation) { return formula.Caclution(schemeinformation); } } /// <summary> /// 创建公式的简单工厂 /// </summary> internal class FormulaFactory { public static AbstractFormula GetFormula(string Formula) { if(Formula == "F1") { return new Formula1(); } else if(Formula == "F2") { return new Formula2(); } //以下若干..... <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">找不到这个公式,抛出异常</span> <span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ArgumentNullException(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">value</span><span style="color: rgba(128, 0, 0, 1)">"</span>, $<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">公式{Formula}不存在</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">); ; } } }</span></pre> </div> <div class="cnblogs_code code-toolbar" style="overflow-x: scroll;padding: 20px;"> <pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">实际使用 </span> static void Main(string[] args) { Context context= new Context(); //获取所有方案信息 List<Schemeinformation> schemeinformationList = new List<Schemeinformation>(); //总结果集 List<Result> results= new List<Result>(); </span><span style="color: rgba(0, 0, 255, 1)">foreach</span>(Schemeinformation schemeinformation <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> schemeinformationList) { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">使用简单工厂获得公式对象</span> context.SetFromula(FormulaFactory.GetFormula(schemeinformation.Formula)); //获取当前公式的计算结果集 List<string>tempResults=context.Caclute(schemeinformation); //遍历结果,将所有结果放入到总结果集中 foreach(string tempResult in tempResults) { results.Add(new Result(schemeinformation,tempResult)); } } //下面就可以输出总结果集了 Console.WriteLine(results.Count); }
2024-01-19
278
0
0
网络聚合
2024-01-19
超详细讲解如何搭建自己的文件服务器
Linux上安装文件服务器FTP 由于FTP、HTTP、Telnet等协议的数据都是使用明文进行传输的,因此从设计上就是不可靠的。人们为了满足以密文方式传输文件的需求,发明了vsftpd服务程序。vsftpd(very secure ftp daemon,非常安全的FTP守护进程)是一款运行在Linux操作系统上的FTP服务程序,不仅完全开源而且免费。此外,它还具有很高的安全性、传输速度,以及支持虚拟用户验证等其他FTP服务程序不具备的特点。在不影响使用的前提下,管理者可以自行决定客户端是采用匿名开放、本地用户还是虚拟用户的验证方式来登录vsftpd服务器。这样即便黑客拿到了虚拟用户的账号密码,也不见得能成功登录vsftpd服务器。 安装VSFTP 下载dnf [root@chenstudy ~]# yum install epel-release [root@chenstudy ~]# yum install dnf 下载VSFTP [root@chenstudy ~]# dnf install vsftpd 清除防火墙的iptables缓存 iptables防火墙管理工具默认禁止了FTP协议的端口号,因此在正式配置vsftpd服务程序之前,为了避免这些默认的防火墙策略“捣乱”,还需要清空iptables防火墙的默认策略,并把当前已经被清理的防火墙策略状态保存下来: [root@chenstudy ~]# iptables -F [root@chenstudy ~]# iptables-save 然后再把FTP协议添加到firewalld服务的允许列表中(前期准备工作一定要做充足): [root@chenstudy ~]# firewall-cmd --permanent --zone=public --add-service=ftp success [root@chenstudy ~]# firewall-cmd --reload success 查看vsftpd服务程序的主配置文件(/etc/vsftpd/vsftpd.conf): [root@chenstudy ~]# mv /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd.conf_bak [root@chenstudy ~]# grep -v "#" /etc/vsftpd/vsftpd.conf_bak > /etc/vsftpd/vsftpd.conf [root@chenstudy ~]# cat /etc/vsftpd/vsftpd.conf anonymous_enable=YES local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=NO listen_ipv6=YES pam_service_name=vsftpd userlist_enable=YES tcp_wrappers=YES [root@chenstudy ~]# **vsftpd服务程序常用的参数以及作用** 参数 作用 listen=[YES|NO] 是否以独立运行的方式监听服务 listen_address=IP地址 设置要监听的IP地址 listen_port=21 设置FTP服务的监听端口 download_enable=[YES|NO] 是否允许下载文件 userlist_enable=[YES|NO] userlist_deny=[YES|NO] 设置用户列表为“允许”还是“禁止”操作 max_clients=0 最大客户端连接数,0为不限制 max_per_ip=0 同一IP地址的最大连接数,0为不限制 anonymous_enable=[YES|NO] 是否允许匿名用户访问 anon_upload_enable=[YES|NO] 是否允许匿名用户上传文件 anon_umask=022 匿名用户上传文件的umask值 anon_root=/var/ftp 匿名用户的FTP根目录 anon_mkdir_write_enable=[YES|NO] 是否允许匿名用户创建目录 anon_other_write_enable=[YES|NO] 是否开放匿名用户的其他写入权限(包括重命名、删除等操作权限) anon_max_rate=0 匿名用户的最大传输速率(字节/秒),0为不限制 local_enable=[YES|NO] 是否允许本地用户登录FTP local_umask=022 本地用户上传文件的umask值 local_root=/var/ftp 本地用户的FTP根目录 chroot_local_user=[YES|NO] 是否将用户权限禁锢在FTP目录,以确保安全 local_max_rate=0 本地用户最大传输速率(字节/秒),0为不限制 下载FTP vsftpd作为更加安全的文件传输协议服务程序,允许用户以3种认证模式登录FTP服务器。 匿名开放模式:是最不安全的一种认证模式,任何人都可以无须密码验证而直接登录到FTP服务器。 本地用户模式:是通过Linux系统本地的账户密码信息进行认证的模式,相较于匿名开放模式更安全,而且配置起来也很简单。但是如果黑客破解了账户的信息,就可以畅通无阻地登录FTP服务器,从而完全控制整台服务器。 虚拟用户模式:更安全的一种认证模式,它需要为FTP服务单独建立用户数据库文件,虚拟出用来进行密码验证的账户信息,而这些账户信息在服务器系统中实际上是不存在的,仅供FTP服务程序进行认证使用。这样,即使黑客破解了账户信息也无法登录服务器,从而有效降低了破坏范围和影响。 ftp是Linux系统中以命令行界面的方式来管理FTP传输服务的客户端工具。我们首先手动安装这个ftp客户端工具: [root@chenstudy ~]# dnf install ftp 匿名访问模式 vsftpd服务程序中,匿名开放模式是最不安全的一种认证模式。任何人都可以无须密码验证而直接登录FTP服务器。这种模式一般用来访问不重要的公开文件(在生产环境中尽量不要存放重要文件)。当然,如果采用第8章中介绍的防火墙管理工具(如TCP Wrapper服务程序)将vsftpd服务程序允许访问的主机范围设置为企业内网,也可以提供基本的安全性。 vsftpd服务程序默认关闭了匿名开放模式,我们需要做的就是开放匿名用户的上传、下载文件的权限,以及让匿名用户创建、删除、更名文件的权限。需要注意的是,针对匿名用户放开这些权限会带来潜在危险,我们只是为了在Linux系统中练习配置vsftpd服务程序而放开了这些权限,不建议在生产环境中如此行事。表11-2罗列了可以向匿名用户开放的权限参数以及作用。 向匿名用户开放的权限参数以及作用 参数 作用 anonymous_enable=YES 允许匿名访问模式 anon_umask=022 匿名用户上传文件的umask值 anon_upload_enable=YES 允许匿名用户上传文件 anon_mkdir_write_enable=YES 允许匿名用户创建目录 anon_other_write_enable=YES 允许匿名用户修改目录名称或删除目录 配置vsftp配置文件: [root@chenstudy ~]# vim /etc/vsftpd/vsftpd.conf # 重启vsftp [root@chenstudy ~]# systemctl restart vsftpd # 把vsftp加入开机自启动 [root@chenstudy ~]# systemctl enable vsftpd Created symlink from /etc/systemd/system/multi-user.target.wants/vsftpd.service to /usr/lib/systemd/system/vsftpd.service. [root@chenstudy ~]# 在linux中采用匿名访问ftp [root@chenstudy ~]# ftp 192.168.200.130 Connected to 192.168.200.130 (192.168.200.130). 220 (vsFTPd 3.0.2) Name (192.168.200.130:root): anonymous 331 Please specify the password. Password: 敲回车 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp> cd pub 250 Directory successfully changed. ftp> mkdir files 550 Create directory operation failed. ftp> # 退出ftp客户端,修改所有者身份 [root@chenstudy ~]# ls -ld /var/ftp/pub drwxr-xr-x. 2 root root 6 Jun 10 2021 /var/ftp/pub [root@chenstudy ~]# chown -R ftp /var/ftp/pub [root@chenstudy ~]# ls -ld /var/ftp/pub drwxr-xr-x. 2 ftp root 6 Jun 10 2021 /var/ftp/pub [root@chenstudy ~]# 系统提示“创建目录的操作失败”(Create directory operation failed),我猜应该是SELinux服务在“捣乱” [root@chenstudy ~]# getsebool -a | grep ftp ftpd_anon_write --> off ftpd_connect_all_unreserved --> off ftpd_connect_db --> off ftpd_full_access --> off ftpd_use_cifs --> off ftpd_use_fusefs --> off ftpd_use_nfs --> off ftpd_use_passive_mode --> off httpd_can_connect_ftp --> off httpd_enable_ftp_server --> off tftp_anon_write --> off tftp_home_dir --> off [root@chenstudy ~]# setsebool -P ftpd_full_access=on SELinux域策略就可以顺利执行文件的创建、修改及删除等操作了: 本地用户模式 本地用户模式要更安全,而且配置起来也很简单 本地用户模式使用的权限参数以及作用 参数 作用 anonymous_enable=NO 禁止匿名访问模式 local_enable=YES 允许本地用户模式 write_enable=YES 设置可写权限 local_umask=022 本地用户模式创建文件的umask值 userlist_deny=YES 启用“禁止用户名单”,名单文件为ftpusers和user_list userlist_enable=YES 开启用户作用名单文件功能 修改vsftp的配置文件: [root@chenstudy ~]# vim /etc/vsftpd/vsftpd.conf anonymous_enable=NO local_enable=YES write_enable=YES local_umask=022 dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=NO listen_ipv6=YES pam_service_name=vsftpd userlist_enable=YES tcp_wrappers=YES 在我们输入root管理员的密码之前,就已经被系统拒绝访问了。这是因为vsftpd服务程序所在的目录中默认存放着两个名为“用户名单”的文件(ftpusers和user_list):vsftpd服务程序目录中的这两个文件也有类似的功能—只要里面写有某位用户的名字,就不再允许这位用户登录到FTP服务器上。 [root@chenstudy ~]# cat /etc/vsftpd/user_list # vsftpd userlist # If userlist_deny=NO, only allow users in this file # If userlist_deny=YES (default), never allow users in this file, and # do not even prompt for a password. # Note that the default vsftpd pam config also checks /etc/vsftpd/ftpusers # for users that are denied. root bin daemon adm lp sync shutdown halt mail news uucp operator games nobody [root@chenstudy ~]# cat /etc/vsftpd/ftpusers # Users that are not allowed to login via ftp root bin daemon adm lp sync shutdown halt mail news uucp operator games nobody [root@chenstudy ~]# 我们可以使用普通用户登录vsftp服务器: [root@chenstudy ~]# ftp 192.168.200.130 Connected to 192.168.200.130 (192.168.200.130). 220 (vsFTPd 3.0.2) Name (192.168.200.130:root): chen 331 Please specify the password. Password: 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp>
2024-01-19
423
0
0
网络聚合
66
67
68
69
70