本文系 AI 生成,仅供参考,不保证准确性,实战中务必将结果与专业软件中的数据进行对比。
在量化交易和股票数据分析中,“复权”是一个绕不开的核心概念。很多新手在搭建自己的数据库或编写策略时,常常因为复权处理不当,导致回测结果失真、技术指标错位,甚至引发系统性的计算灾难。
本文将从底层逻辑出发,用大白话为你讲透复权的本质,并提供一套成熟、高效的数据库与指标设计方案,帮你完美避开各种“坑”。
一、股票中的“复权”是什么意思?
简单来说,复权是为了消除股票因分红、送股、配股等除权除息事件造成的K线价格“断崖式”缺口。
二、如何计算复权因子?
复权因子本质上就是除权前后的价格比例,但不同数据源的定义方向恰好相反:
• BaoStock 算法:复权因子 = 除权后价格 ÷ 除权前价格(结果小于 1,如 10 ÷ 20 = 0.5)。
• 掘金量化算法:复权因子 = 除权前价格 ÷ 除权后价格(结果大于 1,如 20 ÷ 10 = 2)。
无论哪种定义,其核心作用都是作为一个乘数,用于在不同历史价格之间进行等比换算。
三、将股票历史价格保存到数据库中时,建议使用真实价格还是复权价格?
四、以MA5为例,如何计算和保存指标价格?
• 计算与存储:永远使用“真实价格”来计算 MA5,并将结果存入数据库。这样历史指标是客观事实,无需因复权而重算。
• 展示与应用:在看盘或跑策略时,将数据库中的真实 MA5 乘以“当前的复权因子”,即可动态映射出完美的复权 MA5。
举个例子:某 6 天的真实价格是 20,21,22,23,24,12,其中最后一天进行了除权,且假设这天没涨没跌,掘金因子是24/12=2,这样的话,倒数据第 2 天的 MA5 是 (20+21+22+23+24)/5=22,最后一天的 MA5 是 (21/2+22/2+23/2+24/2+12)/5=11.4,所以数据库中这两天的 MA5 值分别是 22 和 11.4 。复权因子同样适用于 MA5 值,即从最后一天看,其前一天的前复权 MA5 值是 22/2=11。
五、如何解决复权因子与四舍五入叠加产生的偏差问题?
核心原则是 “真实价格负责算钱,复权价格负责算趋势” (实盘中):
• 交易与风控层:涉及真实盈亏、涨跌停价计算、下单挂单等,必须使用精确到两位小数的“真实价格”。
• 研究与回测层:涉及画K线图、计算 MA 等趋势指标时,才使用复权价格。此时允许存在极微小的尾数误差(如 1 分钱),因为技术指标看的是宏观趋势,微小偏差对信号毫无影响。
举个例子:真实价格 12.34(2位小数),复权因子 0.965678,前复权价格 12.34 × 0.965678 = 11.91646652(8 位小数)
如果你用这个 11.91646652 去算涨停价:11.91646652 × 1.1 = 13.108113172,四舍五入变成 13.11。
但是,真实的涨停价是 13.57,它的前复权价应该是:13.57 × 0.965678 = 13.10429046,四舍五入变成 13.10。
六、前复权与后复权在实际应用中该如何选择?
• 前复权:以当前最新价格为基准,向下平移历史价格。优点是最新价格与真实交易价格一致,适合看近期K线走势、分析短期技术形态。
• 后复权:以历史最早价格为基准,向上放大最新价格。优点是能真实反映上市以来的累计涨幅,适合计算长期累计收益率、评估长线投资价值。
附:复权因子计算实战案例(小白秒懂版)
T 日公司宣布“10 送 10”,股价直接“腰斩”变成 10元。假设 T 日当天市场不涨不跌,实际收盘价依然是 10元。
1. 复权因子怎么算?
• BaoStock算法:10 ÷ 20 = 0.5
• 掘金量化算法:20 ÷ 10 = 2
2. 前复权怎么算?
前复权的核心是:T 日的 10元 保持不变,把 T-1 日的价格乘以 BaoStock 的因子(0.5)。
• T-1 日前复权价 = 20 × 0.5 = 10元
• T 日前复权价 = 10元(因为T日是基准日,保持不变)最终结果:T-1 日是 10元,T 日是 10元。K 线图完美连续,没有缺口。
3. 后复权怎么算?
后复权的核心是:T-1 日的 20元 保持不变,把 T 日的价格乘以掘金的因子(2)。
• T-1 日后复权价 = 20元
• T 日后复权价 = 10 × 2 = 20元最终结果:T-1 日是 20元,T 日是 20元。K 线图同样连续,反映了你“虽然股价没涨,但手里股票翻倍了,总资产没变”的真实情况。
4. 技术指标(以 MA2 为例)怎么算?
• 数据库里存什么? 存真实价格算出来的指标。
◦ T-1 日前复权价格 = 20 × 0.5 = 10
◦ T 日前复权价格 = 10(前复权以当日为基准,保持真实价格不变)
◦ T 日真实 MA2 = (10 + 10) ÷ 2 = 10
复权只是数学游戏。真实价格负责算钱,复权价格负责看趋势;数据库里只存真实指标,看盘时再乘因子映射。
(AI 生成)
个人整理,仅供参考。具体规格请以官方发布为准。
表格于 2025 年 11 月整理更新。
个人整理,仅供参考。具体规格请以官方发布为准。
|
|
|
|
|
|
|
|
|
|---|---|---|---|---|---|---|---|---|
| 名称 | BE10000 | BE7000 | BE6500 Pro | BE6500 | BE3600 Pro 套装 | BE3600 Pro 网线版 | BE10000 Pro | BE7200 Pro |
| 型号 |
主:RN04 子:RN01 |
主:RP01 子:RP03 |
||||||
| 上市时间 | 2022.10 | 2023.5 | 2023.10 | 2024.8 | 2024.10 | 2025.5 | 2025.9 | 2026.5 |
| 处理器 | Qualcomm 四核 A73 2.2GHz | Qualcomm 四核 A73 1.5GHz | Qualcomm 四核 A53 1.5GHz | Qualcomm 四核 A53 1.1GHz |
主/子: 高通 IPQ5312 四核 1.1GHz |
主/子: Qualcomm Dragonwing N7 |
Qualcomm A7 四核 1.8GHz | Qualcomm 四核 A55 1.8GHz |
| 内存 | 2GB | 1GB | 1GB | 512MB |
主/子:512MB 一说子是 128MB |
主:512MB 子:256MB |
2GB | 1G DRAM + 512MB Flash |
| 频段 | 2.4GHz、5.2GHz、5.8GHz | 2.4GHz、5GHz | 2.4GHz、5GHz | 2.4GHz、5GHz | 2.4GHz、5GHz | 2.4GHz、5GHz | 2.4GHz、5.2GHz、5.8GHz | 2.4GHz、5GHz |
| 组网 | 混合 Mesh | 混合 Mesh | 混合 Mesh | 混合 Mesh | 混合 Mesh | AC + AP | AI Mesh | AI Mesh |
| 天线 | 12根高增益天线 + 12路信号放大器 + NFC内置天线 | 7根外置高增益WiFi天线 + 1根内置高增益WiFi天线 + NFC内置天线 | 6根高增益WiFi内置天线 + 1根蓝牙内置天线 + 1根NFC内置天线 | 6根外置高增益Wi-Fi天线 | 主/子:4根内置天线 |
主:无 子:2根内置双频天线 |
12根高增益天线 + 12路信号放大器 | 8根高增益天线 + 8路信号放大器 |
| 中枢网关 | 不支持 | 不支持 | 支持 | 不支持 |
主:支持 子:不支持 |
主:支持 子:不支持 |
支持 | 支持 |
| 蓝牙网关 | 不支持 | 不支持 |
蓝牙 Mesh 1.0 100 台 + 蓝牙 100 台 升级固件后支持蓝牙 Mesh 2.0 |
蓝牙 Mesh 1.0 |
蓝牙 Mesh 1.0 200 台 + 蓝牙 100 台 |
主:不支持 子:蓝牙 Mesh 2.0 |
蓝牙 Mesh 2.0 200 台 + 蓝牙 100 台 |
蓝牙 Mesh 2.0 200 台 + 蓝牙 100 台 |
| 散热 | 主动散热 | 自然散热 | 自然散热 | 自然散热 | 自然散热 |
主:自然散热 子:主动散热 |
主动散热 | 自然散热 |
| 接口 |
4×2.5G 1×10G 1×10G SFP+ 1×USB 3.0 |
4×2.5G 1×USB 3.0 |
4×2.5G | 4×2.5G |
主/子: 1×2.5G 3×1G |
主:5×2.5G 子:2×2.5G |
4×2.5G 2×10G 1×M.2 1×USB 3.0 |
4×2.5G |
| Wi-Fi | Wi-Fi 7 | Wi-Fi 7 | Wi-Fi 7 | Wi-Fi 7 | Wi-Fi 7 |
主:无 子:Wi-Fi 7 |
Wi-Fi 7 | Wi-Fi 7 |
| MLO 多链路聚合 |
双频 4.3Gbps 5 GHz 和 5 GHz-Game |
双频 4.3Gbps 5 GHz 和 5 GHz-Game |
双频 3.6Gbps 2.4 GHz 和 5 GHz |
双频 3.57Gbps 2.4 GHz 和 5 GHz |
双频 3.57Gbps 2.4 GHz 和 5 GHz |
双频 2.4 GHz 和 5 GHz |
双频 4.3Gbps 5 GHz 和 5 GHz-Game |
双频 3.6Gbps 2.4 GHz 和 5 GHz |
|
价格 |
最新价格 | 最新价格 | 最新价格 | 最新价格 | 最新价格 | 最新价格 | 最新价格 | 最新价格 |
-
表格于 2025 年 10 月整理更新。
-
如果只考虑支持蓝牙 Mesh 2.0,那么有 BE3600 Pro 网线版 和 BE10000 Pro 可选,搭配其它 Mesh 路由器实现全屋 Wi-Fi 7 覆盖,搭配其它中枢网关或从网关设备实现全屋蓝牙 Mesh 2.0 覆盖。
-
如果考虑用 Xiaomi 中枢网关 来部署独立的中枢架构,那么选择路由器就没有限制了。
-
名称中带有“全屋”字样的通常以子母套装形式出售,子母路由配置通常不同。购买两台一模一样的普通 BE 路由器,就相当于组建了一套“不分子母”的 Mesh 套装。
-
名称中带有“Pro”字样的通常具备中枢网关功能。
驾驶自动化等级与划分要素的关系
| 分级 | 名称 | 车辆横向和纵向运动控制 | 目标和事件探测与响应 | 动态驾驶任务接管 | 设计运行条件 |
| 0 级 | 应急辅助 | 驾驶员 | 驾驶员及系统 | 驾驶员 | 有限制 |
| 1 级 | 部分驾驶辅助 | 驾驶员和系统 | 驾驶员及系统 | 驾驶员 | 有限制 |
| 2 级 | 组合驾驶辅助 | 系统 | 驾驶员及系统 | 驾驶员 | 有限制 |
| 3 级 | 有条件自动驾驶 | 系统 | 系统 | 动态驾驶任务接管用户(接管后成为驾驶员) | 有限制 |
| 4 级 | 高度自动驾驶 | 系统 | 系统 | 系统 | 有限制 |
| 5 级 | 完全自动驾驶 | 系统 | 系统 | 系统 | 无限制* |
| * 排除商业和法规因素等限制。 | |||||
摘自:《汽车驾驶自动化分级》(GB/T 40429-2021)
部分车企驾驶安全与辅助、智能领航和泊车辅助系统简介
问界
新M5 全系搭载华为高阶智能驾驶系统,包含智驾领航辅助(NCA)、车道巡航辅助(LCC)、车道巡航辅助增强(LCC Plus)、全向防碰撞系统(CAS)、智能泊车辅助(APA)、遥控泊车辅助(RPA)、代客泊车辅助(AVP)、哨兵模式等功能。部分高阶智驾功能需付费开通,如城区智驾领航辅助(City NCA)、城区车道巡航辅助增强(City LCC Plus)、代客泊车辅助(AVP)等。
阿维塔
驾驶安全与辅助:
前向碰撞预警(FCW)、自动紧急制动(AEB)、异形障碍物自动紧急制动(GAEB)、低速自动紧急制动(LAEB)、前向横穿碰撞预警(FCTA)、前向横穿碰撞制动(FCTB)、后向碰撞预警(RCW)、后向横穿碰撞预警(RCTA)、后向横穿碰撞制动(RCTB)、后向自动紧急制动(RAEB)、交通标志识别(TSR)、交通信号灯识别(TLR)、超速告警(TSA)、开门预警(DOW)、车道偏离预警(LDW)、盲区监测预警(BSD)、车道保持辅助(LKA)、紧急车道保持辅助(ELKA)、侧向障碍物防碰撞(LOCP)、自适应巡航辅助(ACC)、高速车道巡航辅助 (Highway LCC)
智能领航系统:
高速智驾领航辅助 (Highway NCA)、城区车道巡航辅助 (City LCC)、城区智驾领航辅助 (City NCA)
智能泊车辅助系统:
智能泊车辅助(APA)、遥控泊车辅助(RPA)、代客泊车辅助(AVP)
其中,ADS高阶功能包需付费购买,包含城区智驾领航辅助 (City NCA)与代客泊车辅助(AVP)。
参考:阿维塔 12 参数配置表
蔚来
智能驾驶-安全辅助:
前向碰撞预警 (FCW)、自动紧急制动 (AEB)、车辆盲区监测 (BSD)、变道盲区预警 (LCA)、侧方开门预警 (DOW)、后方穿行预警带制动 (RCTA-B)、前方穿行预警带制动 (FCTA-B)、车道偏离预警 (LDW)、车道保持辅助 (LKA)、紧急主动停车(EAS)、紧急车道保持 (ELK)、全场景误加速抑制辅助 (MAI+)、增强型驾驶员感知系统 (ADMS)、通用障碍物预警及辅助 (GOA)、增强型自动紧急转向 (AES)
智能驾驶-泊车辅助:
视觉融合泊车辅助 (S-APA with Fusion)、换电站泊车辅助 (PSAP)、车辆近距召唤 (NBS)、遥控泊车辅助 (RPA)
智能驾驶-辅助驾驶:
车道居中辅助 (LCC)、转向灯控制变道 (ALC)、智能自适应巡航 (i-ACC)、道路标识识别 (TSR)、动态环境模拟显示 (ESD)、视觉融合起步提醒 (AGN)
蔚来智能驾驶NAD服务:
智能驾驶NAD服务(覆盖部分城区道路、封闭高速道路的智能驾驶体验)、低速及泊车智能驾驶NAD服务(支持领航泊车、智能召唤等功能体验)、全域领航辅助 (NOP+)(支持高速、城市快速路、城区道路、高速服务区领航换电等体验)
小米
Xiaomi Pilot Pro:高速领航辅助(NOA)、智能泊车辅助(APA)、车道居中辅助(LCC)、代客泊车辅助(AVP)
Xiaomi Pilot Max:城市领航辅助(NOA)
特斯拉
基本版 Autopilot 自动辅助驾驶:
主动巡航控制、自动辅助转向
增强版 Autopilot 自动辅助驾驶:
自动变道、自动辅助导航驾驶、自动泊车、召唤功能、智能召唤功能
完全自动驾驶功能 FSDFull-Self Driving(简称“FSD”):
交通信号灯和停车标志控制、在城市街道自动辅助转向等。2024年3月31日,特斯拉向美国部分用户推送FSD V12(Supervised)版本。
今天在控制台应用程序中删除和覆盖文件时,遇到指定路径的文件被禁止操作:
Access to the path is denied.
在网上找到的答案都是给所在目录添加 Everyone 的权限,尝试无果。
后来发现原因是被操作的文件被勾选了“只读”权限,取消就正常了。

错误 TS5055 无法写入文件“***.js”,因为它会覆盖输入文件。
添加 tsconfig.json 文件有助于组织包含 TypeScript 和 JavaScript 文件的项目。有关详细信息,请访问 https://aka.ms/tsconfig。
检查“输出”窗口,找到“error”的行,并修复该错误。
一种可能的情况是,使用了 vue3 的 computed 计算属性。原因未知,尝试又定义了一个变量并使用 watch 侦听原变量来给它赋值,同样报错。
本文使用 ASP.NET 6 版本 Senparc.Weixin.Sample.MP 示例项目改造。
第一步 注册多公众号
方法一:打开 appsettings.json 文件,在 SenparcWeixinSetting 节点内添加数组节点 Items,该对象类型同 SenparcWeixinSetting。
//Senparc.Weixin SDK 设置
"SenparcWeixinSetting": {
"IsDebug": true,
"Token": "",
"EncodingAESKey": "",
"WeixinAppId": "",
"WeixinAppSecret": "",
"Items": [
{
"IsDebug": true,
"Token": "a",
"EncodingAESKey": "a",
"WeixinAppId": "a",
"WeixinAppSecret": "a"
},
{
"IsDebug": true,
"Token": "b",
"EncodingAESKey": "b",
"WeixinAppId": "b",
"WeixinAppSecret": "b"
}
]
}方法二:修改 Program.cs 文件,在 UseSenparcWeixin 方法中注册多个公众号信息。
var registerService = app.UseSenparcWeixin(app.Environment,
null /* 不为 null 则覆盖 appsettings 中的 SenparcSetting 配置*/,
null /* 不为 null 则覆盖 appsettings 中的 SenparcWeixinSetting 配置*/,
register => { /* CO2NET 全局配置 */ },
(register, weixinSetting) =>
{
//注册公众号信息(可以执行多次,注册多个公众号)
//register.RegisterMpAccount(weixinSetting, "【盛派网络小助手】公众号");
foreach (var mp in 从数据库或配置文件中获取的公众号列表)
{
register.RegisterMpAccount(new SenparcWeixinSetting
{
//IsDebug = true,
WeixinAppId = mp.AppId,
WeixinAppSecret = mp.AppSecret,
Token = mp.Token,
EncodingAESKey = mp.EncodingAeskey,
}, mp.Name);
}
});完成后,我们可以从 Config.SenparcWeixinSetting.Items 获取这些信息。
第二步 接入验证与消息处理
打开 WeixinController 控制器,将构造函数改写为:
public WeixinController(IHttpContextAccessor httpContextAccessor)
{
AppId = httpContextAccessor.HttpContext!.Request.Query["appId"];
var MpSetting = Services.MPService.MpSettingByAppId(AppId);
Token = MpSetting.Token;
EncodingAESKey = MpSetting.EncodingAESKey;
}示例中 Services.MPService.MpSettingByAppId() 方法实现从 Config.SenparcWeixinSetting.Items 返回指定 appId 的公众号信息。
为使 IHttpContextAccessor 注入生效,打开 Program.cs,在行
builder.Services.AddControllersWithViews();下方插入
builder.Services.AddHttpContextAccessor();这样,我们就可以在构造函数中直接获取地址栏中的 appId 参数,找到对应的公众号进行消息处理。
第三步 消息自动回复中的 AppId
打开 CustomMessageHandler.cs,将
private string appId = Config.SenparcWeixinSetting.MpSetting.WeixinAppId;
private string appSecret = Config.SenparcWeixinSetting.MpSetting.WeixinAppSecret;替换为:
private string appId = null!;
private string appSecret = null!;并在构造函数中插入
appId = postModel.AppId;
appSecret = Services.MPService.MpSettingByAppId(postModel.AppId).WeixinAppSecret;这样,本页中使用的 appId / appSecret 会从 postModel 中获取,而非默认公众号。postModel 已在 WeixinController 中赋值当前的 appId。
改造后,我们可以在 OnTextRequestAsync() 等处理消息的方法中可以判断 appId 来处理不同的消息。
第四步 其它功能和接口
其它功能和接口均可用指定的 AppId 和对应的 AppSecret 进行调用。
自 2023 年 4 月起,Windows 11 已经重新支持显示“秒”,无需第三方工具来实现,点击查看详情。
ElevenClock 下载地址:GitHub,开源软件放心使用
效果:

* 该软件不会影响右下角的显示桌面和系统通知功能。
v3.3 设置方法:
√ 在主屏幕上时钟区显示本程序的时钟样式
ElevenClock 不直接修改任务栏上的时钟区域,而是将时钟覆盖在系统时钟区域的上方。
√ 不要在辅助监视器上显示时钟
此项按实际需求勾选
√ 时间与日期设置 - Set a custom date and time format (for advanced users only)
填写以下内容并 Apply
%H:%M:%S
%Y/%#m/%#d %a
如果不想显示星期,把 %a 去掉即可。
√ 使用自定义字体大小
因分辨率缩放设置不同可能导致显示的字体大小与系统时钟不同,会导致覆盖面过大或过小,从而使系统托盘中的其它图标显示不完整。所以应选择一个与系统时间差不多的字体大小。当显示“周序号”时 ElevenClock 时钟区域可能会远宽于系统时钟区域,可以设置系统时钟显示“星期”(方法见文末)。
v3.2 设置方法:
√ 在主屏幕上时钟区显示本程序的时钟样式
ElevenClock 不直接修改任务栏上的时钟区域,而是将时钟覆盖在系统时钟区域的上方。
√ 不要在辅助监视器上显示时钟
此项按实际需求勾选
√ 显示秒数
这是我们的最终目的。
√ 使用自定义字体大小
因分辨率缩放设置不同可能导致显示的字体大小与系统时钟不同,会导致覆盖面过大或过小,从而使系统托盘中的其它图标显示不完整。所以应选择一个与系统时间差不多的字体大小。当显示“周序号”时 ElevenClock 时钟区域可能会远宽于系统时钟区域,可以设置系统时钟显示“星期”(方法见文末)。
如何显示系统时间“星期”:
打开“更改日期和时间”,在“日期”选项卡的“短日期”中添加“ddd”。

SshNet 向一个已存在的文件 WriteAllText 的时候,是在原内容的基础上从第一个字节依次覆盖新内容。
因此,如果新内容的长度小于原内容,则最终的文件内容将是新内容与结尾部分原内容的结合。
若新内容的长度大于等于原内容,则不存在问题。
例:原文件内容:"abc",WriteAllText 的内容为:“de”,那么最终的文件内容是:“dec”。
前言:本文源于前几天看到的一条微博:

对于这种言论我并不赞同。我大学学的是化学,没有学习过计算机专业的课程,但我认为至少这个问题并不需要多么高端的计算机专业知识,只要中学数学没有全还给老师,就应该能给出至少一种解法。比如说,我就随便涂了一个多边形和一个点,现在我要找出一种通用的方法来判断这个点是不是在多边形内部(别告诉我用肉眼观察……)。

首先想到的一个解法是从这个点做一条射线,计算它跟多边形边界的交点个数,如果交点个数为奇数,那么点在多边形内部,否则点在多边形外。

这个结论很简单,那它是怎么来的?其实,对于平面内任意闭合曲线,我们都可以直观地认为,曲线把平面分割成了内、外两部分,其中“内”就是我们所谓的多边形区域。

基于这一认识,对于平面内任意一条直线,我们可以得出下面这些结论:
直线穿越多边形边界时,有且只有两种情况:进入多边形或穿出多边形。
在不考虑非欧空间的情况下,直线不可能从内部再次进入多边形,或从外部再次穿出多边形,即连续两次穿越边界的情况必然成对。
直线可以无限延伸,而闭合曲线包围的区域是有限的,因此最后一次穿越多边形边界,一定是穿出多边形,到达外部。

现在回到我们最初的题目。假如我们从一个给定的点做射线,还可以得出下面两条结论:
如果点在多边形内部,射线第一次穿越边界一定是穿出多边形。
如果点在多边形外部,射线第一次穿越边界一定是进入多边形。

把上面这些结论综合起来,我们可以归纳出:
当射线穿越多边形边界的次数为偶数时,所有第偶数次(包括最后一次)穿越都是穿出,因此所有第奇数次(包括第一次)穿越为穿入,由此可推断点在多边形外部。

当射线穿越多边形边界的次数为奇数时,所有第奇数次(包括第一次和最后一次)穿越都是穿出,由此可推断点在多边形内部。

到这里,我们已经了解这个解法的思路了,下面接着看算法实现的一些具体问题和边界条件的处理。
点在多边形的边上
上面我们讲到,这个解法的主要思路就是计算射线穿越多边形边界的次数,那么对于点在多边形的边上这种特殊情况,射线出发的这一次,是否应该算作穿越呢?

看了上面的图就会发现,不管算不算穿越,都会陷入两难的境地——同样落在多边形边上的点,可能会得到相反的结果。这显然是不正确的,因此对这种特殊情况需要特殊处理。
点和多边形的顶点重合
这其实是第一种情况的一个特例。

射线经过多边形顶点
射线刚好经过多边形顶点的时候,应该算一次还是两次穿越?这种情况比前两种复杂,也是实现中的难点。

射线刚好经过多边形的一条边
这是上一种情况的特例,也就是说,射线连续经过了多边形的两个相邻顶点。

解决方案:
判断点是否在线上的方法有很多,比较简单直接的就是计算点与两个多边形顶点的连线斜率是否相等,中学数学都学过。
点和多边形顶点重合的情况更简单,直接比较点的坐标就行了。
顶点穿越看似棘手,其实我们换一个角度,思路会大不相同。先来回答一个问题,射线穿越一条线段需要什么前提条件?没错,就是线段两个端点分别在射线两侧。只要想通这一点,顶点穿越就迎刃而解了。这样一来,我们只需要规定被射线穿越的点都算作其中一侧。

如上图,假如我们规定射线经过的点都属于射线以上的一侧,显然点 D 和发生顶点穿越的点 C 都位于射线 Y 的同一侧,所以射线 Y 其实并没有穿越 CD 这条边。而点 C 和点 B 则分别位于射线 Y 的两侧,所以射线 Y 和 BC 发生了穿越,由此我们可以断定点 Y 在多边形内。同理,射线 X 分别与 AD 和 CD 都发生了穿越,因此点 X 在多边形外,而射线 Z 没有和多边形发生穿越,点 Z 位于多边形外。
解决了第三点,这一点就毫无难度了。根据上面的假设,射线连续经过的两个顶点显然都位于射线以上的一侧,因此这种情况看作没有发生穿越就可以了。由于第三点的解决方案实际上已经覆盖到这种特例,因此不需要再做特别的处理。
这种简单直观的算法通常叫做射线法或奇偶法,下面给出 JavaScript 的算法实现。
/**
* @description 射线法判断点是否在多边形内部
* @param {Object} p 待判断的点,格式:{ x: X 坐标, y: Y 坐标 }
* @param {Array} poly 多边形顶点,数组成员的格式同 p
* @return {String} 点 p 和多边形 poly 的几何关系
*/function rayCasting(p, poly) {
var px = p.x,
py = p.y,
flag = false
for(var i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
var sx = poly[i].x,
sy = poly[i].y,
tx = poly[j].x,
ty = poly[j].y
// 点与多边形顶点重合
if((sx === px && sy === py) || (tx === px && ty === py)) {
return 'on'
}
// 判断线段两端点是否在射线两侧
if((sy < py && ty >= py) || (sy >= py && ty < py)) {
// 线段上与射线 Y 坐标相同的点的 X 坐标
var x = sx + (py - sy) * (tx - sx) / (ty - sy)
// 点在多边形的边上
if(x === px) {
return 'on'
}
// 射线穿过多边形的边界
if(x > px) {
flag = !flag
}
}
}
// 射线穿过多边形边界的次数为奇数时点在多边形内
return flag ? 'in' : 'out'}除了射线法还有很多其他的方法,下面再介绍一种回转数法。
平面中的闭合曲线关于一个点的回转数(又叫卷绕数),代表了曲线绕过该点的总次数。下面这张图动态演示了回转数的概念:图中红色曲线关于点(人所在位置)的回转数为 2。

回转数是拓扑学中的一个基本概念,具有很重要的性质和用途。本文并不打算在这一点上展开论述,这需要具备相当的数学知识,否则会非常乏味和难以理解。我们暂时只需要记住回转数的一个特性就行了:当回转数为 0 时,点在闭合曲线外部(回转数大于 0 时所代表的含义,大家可以自己想一想,还是很有趣的)。
对于给定的点和多边形,回转数应该怎么计算呢?
用线段分别连接点和多边形的全部顶点。

计算所有点与相邻顶点连线的夹角。

计算所有夹角和。注意每个夹角都是有方向的,所以有可能是负值。

最后根据角度累加值计算回转数。看过前面的介绍,很容易理解 360°(2π)相当于一次回转。
思路介绍完了,下面两点是实现中需要留意的问题。
JavaScript 的数只有 64 位双精度浮点这一种。对于三角函数产生的无理数,浮点数计算不可避免会造成一些误差,因此在最后计算回转数时需要做取整操作。
通常情况下,平面直角坐标系内一个角的取值范围是 -π 到 π 这个区间,这也是 JavaScript 三角函数
Math.atan2()返回值的范围。但 JavaScript 并不能直接计算任意两条线的夹角,我们只能先计算两条线与 X 正轴夹角,再取两者差值。这个差值的结果就有可能超出 -π 到 π 这个区间,因此我们还需要处理差值超出取值区间的情况。
这里也给出回转数法的 JavaScript 实现。
/**
* @description 回转数法判断点是否在多边形内部
* @param {Object} p 待判断的点,格式:{ x: X 坐标, y: Y 坐标 }
* @param {Array} poly 多边形顶点,数组成员的格式同 p
* @return {String} 点 p 和多边形 poly 的几何关系
*/function windingNumber(p, poly) {
var px = p.x,
py = p.y,
sum = 0
for(var i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
var sx = poly[i].x,
sy = poly[i].y,
tx = poly[j].x,
ty = poly[j].y
// 点与多边形顶点重合或在多边形的边上
if((sx - px) * (px - tx) >= 0 && (sy - py) * (py - ty) >= 0 && (px - sx) * (ty - sy) === (py - sy) * (tx - sx)) {
return 'on'
}
// 点与相邻顶点连线的夹角
var angle = Math.atan2(sy - py, sx - px) - Math.atan2(ty - py, tx - px)
// 确保夹角不超出取值范围(-π 到 π)
if(angle >= Math.PI) {
angle = angle - Math.PI * 2
} else if(angle <= -Math.PI) {
angle = angle + Math.PI * 2
}
sum += angle
}
// 计算回转数并判断点和多边形的几何关系
return Math.round(sum / Math.PI) === 0 ? 'out' : 'in'}也有人问到像下面这种复杂多边形有没有办法?答案是肯定的。至于为什么,就留给大家思考吧。





















