go语言:实现natural sort自然排序算法(附带源码)

一、项目背景详细介绍

在实际开发中,我们经常会遇到这样一种排序需求:

file1.txt
file2.txt
file10.txt
file20.txt

如果使用普通的字符串字典序排序,结果会变成:

file1.txt
file10.txt
file2.txt
file20.txt

这是因为字符串比较是逐字符比较的:

  • "1" < "2"
  • 但 "10" < "2"(因为比较的是字符 '1' 和 '2')

这显然不符合人类直觉。

人类的排序逻辑是:

当遇到连续数字时,应按数值大小比较,而不是逐字符比较。

这种排序方式被称为:

Natural Sort(自然排序)

它广泛应用于:

  • 文件管理系统排序
  • Windows 文件名排序
  • Linux GUI 文件排序
  • 图像序列排序(img1, img2, img10)
  • 版本号排序
  • 日志编号排序

在操作系统层面:

  • Windows 使用自然排序
  • macOS Finder 使用自然排序
  • Linux 多数桌面环境也使用自然排序

Go 标准库并未内置自然排序,因此我们需要自行实现。


二、项目需求详细介绍

本项目目标:

功能需求

  1. 实现字符串数组自然排序
  2. 支持任意位置数字比较
  3. 支持多个数字段比较
  4. 支持前导零
  5. 支持大小写控制
  6. 可扩展为版本号排序

示例

输入:

[]string{
"file1",
"file10",
"file2",
"file20",
}

输出:

file1
file2
file10
file20


三、相关技术详细介绍

1️⃣ 字典序排序(Lexicographical Order)

Go 中:

sort.Strings(arr)

底层是逐字符比较:

  • 从左到右比较
  • 遇到不同字符即决定大小

缺点:

  • 数字会按字符比较

2️⃣ 自然排序核心思想

自然排序的关键:

将字符串拆分为“数字段”和“非数字段”进行比较

例如:

file20.txt

拆分为:

["file", 20, ".txt"]

比较规则:

  1. 同为数字段 → 数值比较
  2. 同为字符串段 → 字典序比较
  3. 数字段优先于字符串段(可自定义)

3️⃣ Go排序接口

Go排序核心接口:

sort.Interface

需要实现:

  • Len()
  • Less(i, j int)
  • Swap(i, j int)

我们只需实现 Less() 中的自然比较逻辑。


4️⃣ 算法时间复杂度

  • 拆分复杂度:O(k)
  • 排序复杂度:O(n log n)
  • 总复杂度:O(n log n * k)

k 为字符串平均长度。


四、实现思路详细介绍

整体思路

对于两个字符串 a 和 b:

  1. 使用双指针 i, j 遍历
  2. 如果当前位置都是数字:
    • 提取完整数字
    • 转为整数
    • 数值比较
  3. 如果当前位置不是数字:
    • 逐字符比较
  4. 如果完全相同:
    • 长度短的优先

关键点分析

1️⃣ 如何识别数字段?

使用:

unicode.IsDigit(rune)

2️⃣ 如何提取完整数字?

当遇到数字:

  • 记录起始位置
  • 直到不是数字为止
3️⃣ 如何处理前导零?

例如:

file001
file1

策略:

  • 数值相同
  • 比较数字长度(可选规则)

本实现:

  • 数值优先
  • 长度短优先

五、完整实现代码

// =============================== // 文件名:main.go // =============================== package main import ( "fmt" "sort" "strconv" "unicode" ) // =============================== // 自然排序结构体定义 // =============================== // NaturalSort 实现 sort.Interface type NaturalSort struct { data []string } // Len 返回元素数量 func (n NaturalSort) Len() int { return len(n.data) } // Swap 交换元素 func (n NaturalSort) Swap(i, j int) { n.data[i], n.data[j] = n.data[j], n.data[i] } // Less 核心自然比较逻辑 func (n NaturalSort) Less(i, j int) bool { return naturalLess(n.data[i], n.data[j]) } // =============================== // 核心自然比较函数 // =============================== // naturalLess 比较两个字符串是否 a < b func naturalLess(a, b string) bool { i, j := 0, 0 la, lb := len(a), len(b) for i < la && j < lb { // 如果当前都是数字 if unicode.IsDigit(rune(a[i])) && unicode.IsDigit(rune(b[j])) { // 提取完整数字 startI := i for i < la && unicode.IsDigit(rune(a[i])) { i++ } startJ := j for j < lb && unicode.IsDigit(rune(b[j])) { j++ } numA := a[startI:i] numB := b[startJ:j] // 转整数比较 intA, _ := strconv.Atoi(numA) intB, _ := strconv.Atoi(numB) if intA != intB { return intA < intB } // 数值相同,长度短优先(处理前导零) if len(numA) != len(numB) { return len(numA) < len(numB) } } else { // 非数字直接字符比较 if a[i] != b[j] { return a[i] < b[j] } i++ j++ } } // 长度短优先 return la < lb } // =============================== // 对外排序函数 // =============================== // NaturalSortStrings 对字符串数组进行自然排序 func NaturalSortStrings(arr []string) { sort.Sort(NaturalSort{data: arr}) } // =============================== // 测试代码 // =============================== func main() { arr := []string{ "file1.txt", "file10.txt", "file2.txt", "file20.txt", "file3.txt", "file001.txt", "file02.txt", "file100.txt", } fmt.Println("排序前:") fmt.Println(arr) NaturalSortStrings(arr) fmt.Println("\n排序后:") fmt.Println(arr) // 版本号测试 versionArr := []string{ "v1.2.10", "v1.2.2", "v1.10.1", "v1.2.1", } fmt.Println("\n版本排序前:") fmt.Println(versionArr) NaturalSortStrings(versionArr) fmt.Println("\n版本排序后:") fmt.Println(versionArr) }

六、代码详细解读(仅解读方法作用)

NaturalSort 结构体

封装字符串数组,实现 sort.Interface。


Len

返回数组长度。


Swap

交换两个元素。


Less

调用核心自然比较函数。


naturalLess

核心算法:

  1. 双指针遍历两个字符串
  2. 如果是数字:
    • 提取完整数字
    • 转整数比较
  3. 如果不是数字:
    • 直接字符比较
  4. 全部相同:
    • 长度短优先

NaturalSortStrings

对外暴露排序函数。


main

测试:

  • 文件名排序
  • 版本号排序

七、项目详细总结

本项目实现了:

  • Go语言自然排序
  • 支持多数字段
  • 支持版本号排序
  • 支持前导零
  • 兼容 sort.Interface

优点:

  • 符合人类直觉
  • 可扩展
  • 易集成

缺点:

  • 性能略低于纯字符串排序
  • 需要解析数字

八、项目常见问题及解答

Q1:为什么不用正则?

正则会增加开销,双指针更高效。


Q2:支持大整数吗?

当前使用 strconv.Atoi,受 int 限制。

可以改为:

  • big.Int
  • 字符串长度优先比较

Q3:是否稳定?

Go 默认 sort 不是稳定排序。

如需稳定排序:

sort.SliceStable


Q4:支持 Unicode 数字吗?

当前使用 unicode.IsDigit,可支持。


Q5:时间复杂度?

O(n log n * k)

k 为字符串长度。


九、扩展方向与性能优化

1️⃣ 使用 sort.Slice

简化结构体实现。


2️⃣ 使用 big.Int 支持超大整数


3️⃣ 实现稳定版本

使用:

sort.SliceStable


4️⃣ 并发排序

对大数据分块排序。


5️⃣ 忽略大小写版本

在比较前:

strings.ToLower()


结语

通过本篇文章,你已经掌握:

  • 自然排序原理
  • Go排序接口机制
  • 数字段解析技巧
  • 版本号排序实现
  • 工程级排序扩展能力

自然排序是:

文件系统、版本控制、日志分析中的核心排序能力。

掌握它,说明你已经具备工程级算法实现能力。

Read more

Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案

Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 shelf_router 的适配 鸿蒙Harmony 实战 - 驾驭官方标准路由器架构、实现鸿蒙端 HTTP 流量精密分发与逻辑路由审计方案 前言 在鸿蒙(OpenHarmony)生态的分布式业务中继、政务级内嵌 API 管理平台以及需要承载大规模高频交互请求的各类全栈式应用开发中,“路由的精确支配与逻辑安全性”是决定系统架构稳健性的命门所在。面对包含上百个 RESTful 端点的复杂服务模型、需要动态解析包含 UUID、日期等多种格式的 URL 参数,或者是需要针对鸿蒙手机与智慧大屏执行差异化的路由匹配。如果仅仅依靠原始的字符串拆分或低性能的手写拦截逻辑。不仅会导致路由解析执行效率的低下,更会因为缺乏一套工业级的“官方契约”规范。引发鸿蒙端微服务接口在面对异常报文时的逻辑脆弱性风险。 我们需要一种“官方背书、匹配闭环”的路由艺术。 shelf_router 是一套由 Dart 官方团队维护的、

By Ne0inhk
【MYSQL】MYSQL学习的一大重点:数据库基础

【MYSQL】MYSQL学习的一大重点:数据库基础

🎬 个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬 艾莉丝的简介: 文章目录 * 1 ~> 数据库概念 * 2 ~> 当前主流的数据库 * 3 ~> MYSQL的基本使用 * 3.1 MYSQL的安装 * 3.2 连接服务器 * 3.3 服务器管理 * 3.4 服务器,数据库,表关系 * 3.5 使用案例(文章最后有详细流程) * 3.6

By Ne0inhk
【MySQL数据库基础】(六)MySQL 表的约束详解:从基础到实战,拿捏数据合法性!

【MySQL数据库基础】(六)MySQL 表的约束详解:从基础到实战,拿捏数据合法性!

前言         在 MySQL 数据库开发中,我们总希望存入表中的数据是合法、规范、符合业务逻辑的。虽然数据类型能对字段做基础限制,但面对复杂的业务需求,仅靠数据类型远远不够。比如要求邮箱唯一、用户名不能为空、学生的班级必须是已存在的班级…… 这些需求都需要靠表的约束来实现。         表的约束是数据库保证数据完整性的核心手段,它能从业务逻辑层面过滤无效数据,避免脏数据进入数据库。今天这篇文章就带大家全面吃透 MySQL 中最常用的表约束,包括null/not null、default、comment、zerofill、primary key、auto_increment、unique key、foreign key,从基础概念到实操案例,手把手教你用约束拿捏数据合法性!下面就让我们正式开始吧! 一、为什么需要表的约束?         先看一个简单的例子:如果我们创建一个班级表,只定义字段和数据类型,不添加任何约束,会发生什么? -- 无约束的班级表 create table myclass( class_

By Ne0inhk
绿联云NAS配置webdav

绿联云NAS配置webdav

前言         zotero使用webdav服务时使用绿联自带的webdav服务只能使用http协议,并且只能在局域网内传输,故而尝试自行配置,以期实现公网文献同步。 注:非专业,自己在配置的时候也是根据前人的分享实现的,可能有很多不准确的地方,请见谅。 1. 大致思路         购买域名(腾讯云)→配置DDNS-go(docker)→获取SSL证书(乐此加密)→配置natfrp(docker) ①域名:固定域名,后续内网穿透时可以使用自定义域名; ②DDNS-go:自动更新域名解析到公网IP; ③SSL证书:https协议需要; ④natfrp:内网穿透需要,这里使用的是Sakura Frp。 2.参考文献 (31 封私信 / 80 条消息) 绿联 NAS 域名直连 DDNS-Go+IPv6 内网穿透并开启 HTTPS - 知乎https://zhuanlan.zhihu.com/p/

By Ne0inhk