You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust中CharRange与ByteRange高效转换方案问询

Rust中字符范围与字节范围的高效转换方案

问题背景

我使用的一个外部库,其字符串表示等价于&[char],该库的部分编辑接口接受type CharRange = Range<usize>类型的范围输入(基于char的偏移量);而其他Rust库采用type ByteRange = Range<usize>(基于u8的偏移量)。

目前我用的是O(n)复杂度的转换算法,但存在性能瓶颈,想知道有没有高效的数据结构能实现二者间的转换。原转换代码如下:

type CharRange = Range<usize>;
type ByteRange = Range<usize>;

fn byte_range_to_char_range(text: &str, byte_range: ByteRange) -> CharRange {
    let start = text[..byte_range.start].chars().count();
    let end = text[..byte_range.end].chars().count();
    start..end
}

fn char_range_to_byte_range(text: &str, char_range: CharRange) -> ByteRange {
    let start = text.char_indices().nth(char_range.start).map(|(i, _)| i).unwrap_or(0);
    let end = text.char_indices().nth(char_range.end).map(|(i, _)| i).unwrap_or(text.len());
    start..end
}

高效转换方案

核心思路是预构建偏移映射表,利用字符串内容固定时(或修改频率远低于转换频率时)的特性,将O(n)的单次转换开销分摊到初始化阶段,后续转换操作可做到O(1)或O(log n)时间复杂度。

1. 全映射数组版本(O(1)转换,O(n)内存)

预先遍历字符串一次,构建两个数组:记录每个字节位置对应的字符索引,以及每个字符起始的字节位置。

use std::ops::Range;

type CharRange = Range<usize>;
type ByteRange = Range<usize>;

struct StringOffsetMap {
    // 索引=字节位置,值=对应字符索引
    byte_to_char: Vec<usize>,
    // 索引=字符索引,值=对应起始字节位置
    char_to_byte: Vec<usize>,
    total_bytes: usize,
    total_chars: usize,
}

impl StringOffsetMap {
    fn new(text: &str) -> Self {
        let mut byte_to_char = vec![0; text.len()];
        let mut char_to_byte = Vec::new();
        let mut current_char_idx = 0;

        for (char_start_byte, c) in text.char_indices() {
            char_to_byte.push(char_start_byte);
            // 填充当前字符覆盖的所有字节位置对应的字符索引
            let char_len = c.len_utf8();
            for offset in 0..char_len {
                if let Some(entry) = byte_to_char.get_mut(char_start_byte + offset) {
                    *entry = current_char_idx;
                }
            }
            current_char_idx += 1;
        }

        StringOffsetMap {
            byte_to_char,
            char_to_byte,
            total_bytes: text.len(),
            total_chars: current_char_idx,
        }
    }

    fn byte_range_to_char_range(&self, byte_range: ByteRange) -> CharRange {
        let start = if byte_range.start >= self.total_bytes {
            self.total_chars
        } else {
            self.byte_to_char[byte_range.start]
        };
        let end = if byte_range.end >= self.total_bytes {
            self.total_chars
        } else {
            self.byte_to_char[byte_range.end]
        };
        start..end
    }

    fn char_range_to_byte_range(&self, char_range: CharRange) -> ByteRange {
        let start = self.char_to_byte.get(char_range.start).copied().unwrap_or(self.total_bytes);
        let end = self.char_to_byte.get(char_range.end).copied().unwrap_or(self.total_bytes);
        start..end
    }
}

2. 紧凑前缀和版本(O(log n)转换,O(k)内存,k为字符数量)

如果字符串超大、内存紧张,可改用前缀和数组存储每个字符的结束字节位置,通过二分查找实现转换,大幅降低内存占用:

use std::ops::Range;

type CharRange = Range<usize>;
type ByteRange = Range<usize>;

struct CompactStringOffsetMap {
    // 每个字符的结束字节位置(即下一个字符的起始位置)
    char_end_bytes: Vec<usize>,
    total_bytes: usize,
    total_chars: usize,
}

impl CompactStringOffsetMap {
    fn new(text: &str) -> Self {
        let mut char_end_bytes = Vec::new();
        let mut current_byte = 0;
        for c in text.chars() {
            current_byte += c.len_utf8();
            char_end_bytes.push(current_byte);
        }
        CompactStringOffsetMap {
            char_end_bytes,
            total_bytes: text.len(),
            total_chars: char_end_bytes.len(),
        }
    }

    fn byte_to_char(&self, byte_pos: usize) -> usize {
        if byte_pos >= self.total_bytes {
            return self.total_chars;
        }
        // 二分查找第一个大于byte_pos的结束位置,索引即为字符索引
        self.char_end_bytes.partition_point(|&end| end <= byte_pos)
    }

    fn byte_range_to_char_range(&self, byte_range: ByteRange) -> CharRange {
        let start = self.byte_to_char(byte_range.start);
        let end = self.byte_to_char(byte_range.end);
        start..end
    }

    fn char_to_byte(&self, char_pos: usize) -> usize {
        match char_pos {
            0 => 0,
            pos if pos >= self.total_chars => self.total_bytes,
            _ => self.char_end_bytes[char_pos - 1],
        }
    }

    fn char_range_to_byte_range(&self, char_range: CharRange) -> ByteRange {
        let start = self.char_to_byte(char_range.start);
        let end = self.char_to_byte(char_range.end);
        start..end
    }
}

使用说明

  • 若字符串内容固定,初始化一次映射表后,所有转换操作均为常数或对数时间,适合高频转换场景。
  • 若字符串频繁修改,需权衡初始化开销与转换收益:当转换操作远多于修改操作时,仍能显著提升性能。

内容的提问来源于stack exchange,提问作者Aster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 09:20:24