如何在字符串中交换两个字符?

8
我希望写一个函数,具体如下:
  • 输入:字符串A,整数i,其中0 < i < len(A)
  • 输出:交换位置i - 1和i处的字符后的字符串A。
想要一种优雅简洁的解决方案来实现这个功能。目前我的解决方案是:
let mut swapped = input_str[0..i].to_string();
swapped.push(input_str.char_at(i));
swapped.push(input_str.char_at(i - 1));
swapped.push_str(&query[i..input_str.len()]);

但这只适用于ASCII字符串。我可以想到其他解决方案,比如将其转换为UTF-32向量,进行交换,然后再转换回字符串,但这似乎需要额外的工作。


我认为你的代码有误——如果我读对了,它会将ii-1处的字符推入两次。 - Lambda Fairy
2个回答

6
这里有一个优美的解决方案:
use std::str::CharRange;

fn swap_chars_at(input_str: &str, i: usize) -> String {
    // Pre-allocate a string of the correct size
    let mut swapped = String::with_capacity(input_str.len());
    // Pluck the previous character
    let CharRange { ch: prev_ch, next: prev } = input_str.char_range_at_reverse(i);
    // Pluck the current character
    let CharRange { ch, next } = input_str.char_range_at(i);
    // Put them back
    swapped.push_str(&input_str[..prev]);
    swapped.push(ch);
    swapped.push(prev_ch);
    swapped.push_str(&input_str[next..]);
    // Done!
    swapped
}

#[test]
fn smoke_test() {
    let s = swap_chars_at("lyra", 2);
    assert_eq!(s, "lrya");
}

#[test]
fn unicode() {
    // 'ç' takes up 2 bytes in UTF-8
    let s = swap_chars_at("ça va?", 2);
    assert_eq!(s, "aç va?");
}

文档中得知:
  • fn char_range_at(&self, start: usize) -> CharRange
    • 从字符串中取出一个字符并返回下一个字符的索引。
  • fn char_range_at_reverse(&self, start: usize) -> CharRange
    • 给定一个字节位置和一个字符串,返回前一个字符及其位置。
这两种方法一起使用可以让我们向前或向后查看字符串——这正是我们想要的。
但等等,还有更多!DK指出了上面代码的一个特殊情况。如果输入包含任何组合字符,它们可能会与它们结合的字符分离。
现在,这个问题是关于Rust而不是Unicode的,所以我不会详细介绍具体如何工作。你现在需要知道的是,Rust提供了此方法
  • fn grapheme_indices(&self, is_extended: bool) -> GraphemeIndices
    • 返回迭代器,遍历该字符串的语素簇及其字节偏移量。
通过大量使用.find().rev(),我们得到了这个(希望是)正确的解决方案:
#![allow(unstable)]  // `GraphemeIndices` is unstable

fn swap_graphemes_at(input_str: &str, i: usize) -> String {
    // Pre-allocate a string of the correct size
    let mut swapped = String::with_capacity(input_str.len());
    // Find the grapheme at index i
    let (_, gr) = input_str.grapheme_indices(true)
        .find(|&(index, _)| index == i)
        .expect("index does not point to a valid grapheme");
    // Find the grapheme just before it
    let (prev, prev_gr) = input_str.grapheme_indices(true).rev()
        .find(|&(index, _)| index < i)
        .expect("no graphemes to swap with");
    // Put it all back together
    swapped.push_str(&input_str[..prev]);
    swapped.push_str(gr);
    swapped.push_str(prev_gr);
    swapped.push_str(&input_str[i+gr.len()..]);
    // Done!
    swapped
}

#[test]
fn combining() {
    // Ensure that "c\u{327}" is treated as a single unit
    let s = swap_graphemes_at("c\u{327}a va?", 3);
    assert_eq!(s, "ac\u{327} va?");
}

诚然,这有点复杂。首先,它遍历输入,在位置提取出字形簇。然后,它向后迭代(.rev())通过输入,在索引< i处选择最右侧的簇(即上一个簇)。最后,它将所有内容重新组合在一起。
如果你非常严谨,还有更多特殊情况需要处理。例如,如果字符串包含Windows换行符("\r\n"),那么我们可能不想交换它们。在希腊语中,当字母西格玛(σ)位于单词末尾时,它的书写方式不同(ς),因此更好的算法应根据需要在它们之间进行转换。别忘了那些双向控制字符...
但为了我们的理智,我们就到这里吧。

1
我不相信这是正确的:你正在处理代码点,而不是字形,这才是你应该处理的。也就是说,如果你有组合码点,它们不会与你要交换的码点交换位置。 - DK.
@DK 很好的观点。我本来想说标准库没有字形分割器,但事实证明它确实有。已经注意并编辑过了。 - Lambda Fairy
4
华丽的编辑。真正的问题在于,原始问题的思路完全错误:如果你正在做任何依赖“字符”具体概念的事情,那么你可能只是给自己惹麻烦。要做到正确非常困难。 - DK.

0

从头开始构建字符切换字符串:

fn swap_chars_at(idx: usize, s: &str) -> String {
    let mut rslt = "".to_string();
    let mut rm = '\0';
    for (i, c) in s.chars().enumerate() {
        if idx == i {
            rm = c;
        } else {
            rslt.push(c);
            if idx + 1 == i {
                rslt.push(rm);
            }
        }
    }
    rslt
}

游乐场


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接