如何在awk数组遍历中实现GNU sort -V风格的字母数字混合排序
awk实现chr序列ID自然排序方案(兼容GNU sort -V效果)
核心逻辑
自然排序和普通字符串/数值排序的核心差异在于对混合字母数字的字段拆分规则:将序列ID拆分为交替的纯字符串段、纯数字段,按位置逐个比较:
- 字符串段按ASCII码字典序比较
- 数字段按数值大小比较
- 主chr编号比较完成后,
_alt、_random等后缀按同样规则继续比较,最终排序结果和GNUsort -V完全一致
完整实现代码
#!/usr/bin/awk -f # 自定义chr ID比较函数,返回值规则:a<b返回-1,a==b返回0,a>b返回1 function cmp_chr(a, b, a_len, b_len, a_seg, b_seg, a_is_num, b_is_num, i) { # 拆分a为交替的数字/非数字段 a_len = 0 while (match(a, /([^0-9]+)|([0-9]+)/, seg)) { a_seg[++a_len] = seg[0] a = substr(a, RSTART + RLENGTH) } # 拆分b为交替的数字/非数字段 b_len = 0 while (match(b, /([^0-9]+)|([0-9]+)/, seg)) { b_seg[++b_len] = seg[0] b = substr(b, RSTART + RLENGTH) } # 逐个段比较 for (i = 1; i <= a_len && i <= b_len; i++) { a_is_num = (a_seg[i] ~ /^[0-9]+$/) b_is_num = (b_seg[i] ~ /^[0-9]+$/) if (a_is_num && b_is_num) { # 都是数字按数值比较 if (a_seg[i] + 0 < b_seg[i] + 0) return -1 if (a_seg[i] + 0 > b_seg[i] + 0) return 1 } else { # 有一个不是数字按字符串比较 if (a_seg[i] < b_seg[i]) return -1 if (a_seg[i] > b_seg[i]) return 1 } } # 前面段都相同,短的排前面 if (a_len < b_len) return -1 if (a_len > b_len) return 1 return 0 } # 读入所有行,第一列是序列ID,存入数组 { lines[NR] = $0 ids[NR] = $1 } # 所有行读入完成后,自定义排序输出 END { # 冒泡排序实现(数据量小的场景可用,数据量大可替换为快速排序实现) for (i = 1; i < NR; i++) { for (j = i + 1; j <= NR; j++) { if (cmp_chr(ids[i], ids[j]) > 0) { # 交换ID和对应行 tmp_id = ids[i] ids[i] = ids[j] ids[j] = tmp_id tmp_line = lines[i] lines[i] = lines[j] lines[j] = tmp_line } } } # 输出排序后结果 for (i = 1; i <= NR; i++) { print lines[i] } }
效果验证
输入样例
chr10_random
chr1
chr2
chrM
chr10
chr22
chr1_alt
chrUn_gl000220
chr1_random
输出结果(和sort -V结果完全一致)
chr1
chr1_alt
chr1_random
chr2
chr10
chr10_random
chr22
chrM
chrUn_gl000220
注意事项
- 代码使用了GNU awk的
match()捕获组特性,需使用gawk运行 - 示例中用冒泡排序仅为简化实现,处理万级以上行时建议替换为更高效的快速排序实现
- 若序列ID不在第一列,只需修改
ids[NR] = $1为对应的列号即可
内容的提问来源于stack exchange,提问作者jeffpkamp
相关产品推荐
相关产品推荐

