核心思路是将非零判断转为布尔数组后用np.diff找跳变点:值为1处对应非零段起始(索引+1),-1处对应结束;需手动补首尾边界(mask[0]或mask[-1]为True时),再合并索引返回片段区间。

用 np.diff 找非零片段的起始和结束位置
核心思路是:把非零判断转成布尔数组,再对它求差分——np.diff 会把布尔数组隐式转为整数(True→1,False→0),于是跳变点就暴露了:np.diff(arr != 0) 中值为 1 的位置对应「从 0 到非零」的起始,值为 -1 对应「从非零到 0」的结束。
注意:差分结果比原数组少一个元素,所以起始索引要 +1,结束索引直接对应差分中 -1 的位置(即原数组中该位置就是最后一个非零值)。
实操建议:
- 先生成布尔掩码:
mask = arr != 0,避免重复计算 - 用
np.diff(mask),再用np.where提取跳变索引 - 起始点 =
np.where(np.diff(mask) == 1)[0] + 1;若首元素非零,需手动补上索引0 - 结束点 =
np.where(np.diff(mask) == -1)[0];若末元素非零,需手动补上len(arr)-1
处理边界情况:首尾连续非零段容易漏掉
差分天然丢失首尾信息。比如 arr = [5, 3, 0, 1, 2],mask = [True, True, False, True, True],np.diff(mask) 是 [0, -1, 1, 0],能捕获中间的 0→1 起始(索引 3),但漏了开头的 [0:2] 和结尾的 [3:5]。
正确做法是显式检查首尾:
- 如果
mask[0]为True,则第一个片段起始于0 - 如果
mask[-1]为True,则最后一个片段结束于len(arr)-1 - 合并时用
np.concatenate拼接手动补充的索引,别用 Python 列表 +,否则类型易出错
封装成可复用函数:支持 1D 和 2D 数组沿指定轴处理
实际中常需处理列向量或按行找片段,不能只写死 1D。关键在用 axis 参数配合 np.diff 和布尔操作。
要点:
- 对 2D 数组,先用
arr != 0得到同形布尔数组,再用axis=指定方向做差分 -
np.diff(mask, axis=1)行内差分(每行独立找连续非零段) - 起始/结束索引返回的是元组(如
(rows, cols)),需用np.unravel_index或直接保留坐标对 - 避免在循环里反复调用
np.where,可一次性对整个差分数组操作
示例(1D 封装):
def nonzero_runs(arr):
mask = arr != 0
d = np.diff(mask)
starts = np.where(d == 1)[0] + 1
ends = np.where(d == -1)[0]
if mask[0]: starts = np.insert(starts, 0, 0)
if mask[-1]: ends = np.append(ends, len(arr)-1)
return np.array(list(zip(starts, ends)))
性能陷阱:np.diff 在大数组上不慢,但拼接逻辑可能拖慢
np.diff 本身是 C 实现,很快;真正卡顿往往来自多次 np.where + np.concatenate / np.append,尤其在循环中反复构建小数组。
优化方向:
- 预分配结果数组(如用
np.empty((max_possible, 2), dtype=int)),用计数器填入,最后切片 - 避免
np.append——它每次都会复制整个数组;改用列表收集索引,最后一次性转np.array - 若只需长度而非位置,可用
scipy.ndimage.label更快(但引入额外依赖) - 对超长稀疏序列,考虑先用
np.flatnonzero提取所有非零下标,再用np.split切分间隔 >1 的组
最易被忽略的一点:布尔掩码的内存开销。对亿级 float64 数组,arr != 0 会额外占用同等大小的布尔内存(约 1 byte/元素)。若内存敏感,可改用 np.sign(arr, out=...) 复用缓冲区,或分块处理。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











