整理科研数据文件

题目分析

给定 个文件名,要求按照"自然排序"规则排序输出。自然排序与普通字典序排序的区别在于:数字部分按数值大小比较而非逐字符比较,例如 Run2 应排在 Run10 前面。

排序规则:

  1. 将文件名拆分为交替的非数字段和数字段,如 ts010tc12 拆分为 ["ts", "010", "tc", "12"]
  2. 逐段比较:两个非数字段按字典序比较;两个数字段按数值比较;类型不同时数字段排在前面。
  3. 前缀优先:一个文件名是另一个的前缀时,较短的排在前面。
  4. 稳定性:数值相等的项(如 Run01Run1)保持原始输入顺序。

思路

分段 + 自定义比较器

核心思想非常直接:先对每个文件名做一次预处理,将其拆分成若干段,然后用自定义比较函数对所有文件名做稳定排序。

拆分过程: 从左到右扫描字符串,遇到连续数字就提取为一个数字段(同时记录其数值),遇到连续非数字就提取为一个字符串段。

比较过程: 对两个文件名的段序列,从第一段开始逐段比较:

  • 若两段类型相同:数字段比数值,字符串段比字典序。
  • 若类型不同:数字段优先(排在前面)。
  • 若所有公共段都相等,段数少的排在前面。

由于题目要求等值项保持输入顺序,使用稳定排序即可。

以样例 1 验证: 三个文件名拆分结果:

  • ts1tc1 -> ["ts", 1, "tc", 1]
  • ts1tc01 -> ["ts", 1, "tc", 1]
  • ts0tc1 -> ["ts", 0, "tc", 1]

比较 ts0tc1ts1tc1:第一段 "ts" 相同,第二段 ,所以 ts0tc1 排前面。比较 ts1tc1ts1tc01:所有段的值都相等,保持原始顺序,ts1tc1ts1tc01 前面。最终输出 ts0tc1, ts1tc1, ts1tc01,与预期一致。

复杂度

  • 时间复杂度:,其中 为文件名的平均长度,排序进行 次比较,每次比较
  • 空间复杂度:,存储所有文件名的分段结果

代码

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    scanf("%d", &n);
    vector<string> names(n);
    for (int i = 0; i < n; i++) {
        char buf[1001];
        scanf("%s", buf);
        names[i] = buf;
    }

    // 将字符串拆分为交替的非数字段和数字段
    auto parse = [](const string& s) -> vector<pair<string, long long>> {
        vector<pair<string, long long>> segs;
        int i = 0, len = s.size();
        while (i < len) {
            if (isdigit(s[i])) {
                int j = i;
                while (j < len && isdigit(s[j])) j++;
                segs.push_back({s.substr(i, j - i), stoll(s.substr(i, j - i))});
                i = j;
            } else {
                int j = i;
                while (j < len && !isdigit(s[j])) j++;
                segs.push_back({s.substr(i, j - i), -1}); // -1 表示非数字段
                i = j;
            }
        }
        return segs;
    };

    vector<int> idx(n);
    iota(idx.begin(), idx.end(), 0);
    vector<vector<pair<string, long long>>> parsed(n);
    for (int i = 0; i < n; i++) parsed[i] = parse(names[i]);

    stable_sort(idx.begin(), idx.end(), [&](int a, int b) {
        auto& sa = parsed[a];
        auto& sb = parsed[b];
        int m = min(sa.size(), sb.size());
        for (int i = 0; i < m; i++) {
            bool aDigit = (sa[i].second >= 0);
            bool bDigit = (sb[i].second >= 0);
            if (aDigit != bDigit) return aDigit; // 数字段排前面
            if (aDigit) {
                if (sa[i].second != sb[i].second)
                    return sa[i].second < sb[i].second;
            } else {
                if (sa[i].first != sb[i].first)
                    return sa[i].first < sb[i].first;
            }
        }
        return sa.size() < sb.size();
    });

    for (int i = 0; i < n; i++) printf("%s\n", names[idx[i]].c_str());
    return 0;
}
import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        String[] names = new String[n];
        for (int i = 0; i < n; i++) names[i] = br.readLine().trim();

        // 预处理:将每个文件名拆分为段
        List<List<String>> segs = new ArrayList<>();
        List<List<Boolean>> types = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            List<String> seg = new ArrayList<>();
            List<Boolean> typ = new ArrayList<>();
            int j = 0, len = names[i].length();
            while (j < len) {
                if (Character.isDigit(names[i].charAt(j))) {
                    int k = j;
                    while (k < len && Character.isDigit(names[i].charAt(k))) k++;
                    seg.add(names[i].substring(j, k));
                    typ.add(true);
                    j = k;
                } else {
                    int k = j;
                    while (k < len && !Character.isDigit(names[i].charAt(k))) k++;
                    seg.add(names[i].substring(j, k));
                    typ.add(false);
                    j = k;
                }
            }
            segs.add(seg);
            types.add(typ);
        }

        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; i++) idx[i] = i;

        Arrays.sort(idx, (a, b) -> {
            List<String> sa = segs.get(a), sb = segs.get(b);
            List<Boolean> ta = types.get(a), tb = types.get(b);
            int m = Math.min(sa.size(), sb.size());
            for (int i = 0; i < m; i++) {
                boolean ad = ta.get(i), bd = tb.get(i);
                if (ad != bd) return ad ? -1 : 1;
                if (ad) {
                    long va = Long.parseLong(sa.get(i));
                    long vb = Long.parseLong(sb.get(i));
                    if (va != vb) return Long.compare(va, vb);
                } else {
                    int cmp = sa.get(i).compareTo(sb.get(i));
                    if (cmp != 0) return cmp;
                }
            }
            return sa.size() - sb.size();
        });

        StringBuilder out = new StringBuilder();
        for (int i = 0; i < n; i++) out.append(names[idx[i]]).append('\n');
        System.out.print(out);
    }
}
import sys
from functools import cmp_to_key

def parse(s):
    segs = []
    i = 0
    while i < len(s):
        if s[i].isdigit():
            j = i
            while j < len(s) and s[j].isdigit():
                j += 1
            segs.append((True, int(s[i:j]), s[i:j]))
            i = j
        else:
            j = i
            while j < len(s) and not s[j].isdigit():
                j += 1
            segs.append((False, 0, s[i:j]))
            i = j
    return segs

def compare(a, b):
    sa, sb = a[1], b[1]
    m = min(len(sa), len(sb))
    for i in range(m):
        ad, av, at = sa[i]
        bd, bv, bt = sb[i]
        if ad != bd:
            return -1 if ad else 1  # 数字段排前面
        if ad:  # 都是数字段,比数值
            if av != bv:
                return -1 if av < bv else 1
        else:  # 都是字符串段,比字典序
            if at != bt:
                return -1 if at < bt else 1
    if len(sa) != len(sb):
        return -1 if len(sa) < len(sb) else 1
    return 0

input_data = sys.stdin.read().split('\n')
n = int(input_data[0])
names = []
for i in range(1, n + 1):
    s = input_data[i].strip()
    names.append((s, parse(s)))

names.sort(key=cmp_to_key(compare))
print('\n'.join(name for name, _ in names))
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin });
const lines = [];

rl.on('line', line => lines.push(line.trim()));
rl.on('close', () => {
    const n = parseInt(lines[0]);
    const names = [];
    for (let i = 1; i <= n; i++) names.push(lines[i]);

    function parse(s) {
        const segs = [];
        let i = 0;
        while (i < s.length) {
            if (s[i] >= '0' && s[i] <= '9') {
                let j = i;
                while (j < s.length && s[j] >= '0' && s[j] <= '9') j++;
                segs.push({ isDigit: true, num: parseInt(s.substring(i, j)), text: s.substring(i, j) });
                i = j;
            } else {
                let j = i;
                while (j < s.length && !(s[j] >= '0' && s[j] <= '9')) j++;
                segs.push({ isDigit: false, num: 0, text: s.substring(i, j) });
                i = j;
            }
        }
        return segs;
    }

    const parsed = names.map(s => parse(s));
    const idx = Array.from({ length: n }, (_, i) => i);

    idx.sort((a, b) => {
        const sa = parsed[a], sb = parsed[b];
        const m = Math.min(sa.length, sb.length);
        for (let i = 0; i < m; i++) {
            if (sa[i].isDigit !== sb[i].isDigit) return sa[i].isDigit ? -1 : 1;
            if (sa[i].isDigit) {
                if (sa[i].num !== sb[i].num) return sa[i].num - sb[i].num;
            } else {
                if (sa[i].text !== sb[i].text) return sa[i].text < sb[i].text ? -1 : 1;
            }
        }
        return sa.length - sb.length;
    });

    const result = [];
    for (let i = 0; i < n; i++) result.push(names[idx[i]]);
    console.log(result.join('\n'));
});