14

最近,有人询问了一种在 C 中反转字符串的算法。大多数提出的解决方案在处理非单字节字符串时都会遇到麻烦。所以,我想知道什么是专门处理 utf-8 字符串的好算法。

我想出了一些代码,我将其作为答案发布,但我很高兴看到其他人的想法或建议。我更喜欢使用实际代码,所以我选择了 C#,因为它似乎是这个网站上最流行的语言之一,但我不介意你的代码是另一种语言,只要它可以合理任何熟悉命令式语言的人都能理解。而且,因为这是为了了解如何在低级别实现这样的算法(低级别我只是指处理字节),所以这个想法是避免将库用于核心代码。

笔记:

我对算法本身、它的性能以及如何优化它感兴趣(我的意思是算法级优化,不是用 ++i 替换 i++ 等;我对实际的基准测试也不感兴趣)。

我并不是要在生产代码中实际使用它或“重新发明轮子”。这只是出于好奇和练习。

我正在使用 C# 字节数组,所以我假设您可以在不运行字符串的情况下获取字符串的长度,直到找到 NUL。也就是说,我没有考虑找到字符串长度的复杂性。但是,如果您使用的是 C,例如,您可以在调用核心代码之前使用 strlen() 将其排除在外。

编辑:

正如 Mike F 所指出的,我的代码(以及此处发布的其他人的代码)没有处理复合字符。关于这里的一些信息。我不熟悉这个概念,但如果这意味着存在“组合字符”,即仅与其他“基本”字符/代码点组合有效的字符/代码点,则此类查找表字符可用于在反转时保留“全局”字符(“基”+“组合”字符)的顺序。

4

5 回答 5

13

我会进行一次反转字节,然后第二次将任何多字节字符(在 UTF8 中很容易检测到)中的字节反转回正确的顺序。

您绝对可以一次性处理此问题,但除非例程成为瓶颈,否则我不会打扰。

于 2008-10-13T22:34:52.180 回答
8

此代码假定输入的 UTF-8 字符串有效且格式正确(即每个多字节字符最多 4 个字节):

#include "string.h"

void utf8rev(char *str)
{
    /* this assumes that str is valid UTF-8 */
    char    *scanl, *scanr, *scanr2, c;

    /* first reverse the string */
    for (scanl= str, scanr= str + strlen(str); scanl < scanr;)
        c= *scanl, *scanl++= *--scanr, *scanr= c;

    /* then scan all bytes and reverse each multibyte character */
    for (scanl= scanr= str; c= *scanr++;) {
        if ( (c & 0x80) == 0) // ASCII char
            scanl= scanr;
        else if ( (c & 0xc0) == 0xc0 ) { // start of multibyte
            scanr2= scanr;
            switch (scanr - scanl) {
                case 4: c= *scanl, *scanl++= *--scanr, *scanr= c; // fallthrough
                case 3: // fallthrough
                case 2: c= *scanl, *scanl++= *--scanr, *scanr= c;
            }
            scanr= scanl= scanr2;
        }
    }
}

// quick and dirty main for testing purposes
#include "stdio.h"

int main(int argc, char* argv[])
{
    char buffer[256];
    buffer[sizeof(buffer)-1]= '\0';

    while (--argc > 0) {
        strncpy(buffer, argv[argc], sizeof(buffer)-1); // don't overwrite final null
        printf("%s → ", buffer);
        utf8rev(buffer);
        printf("%s\n", buffer);
    }
    return 0;
}

如果您编译此程序(示例名称so199260.c:)并在 UTF-8 环境(本例中为 Linux 安装)上运行它:

$ so199260 γεια και χαρά français АДЖИ a♠♡♢♣b
a♠♡♢♣b → b♣♢♡♠a
АДЖИ → ИЖДА
français → siaçnarf
χαρά → άραχ
και → ιακ
γεια → αιεγ

如果代码太神秘,我会很乐意澄清。

于 2008-10-13T23:44:36.220 回答
6

同意您的方法是就地执行此操作的唯一合理方法。

就个人而言,我不喜欢在处理它的每个函数中重新验证 UTF8,并且通常只做避免崩溃所需的事情;它加起来的代码要少得多。不知道多少 C#,所以这里是 C:

编辑以消除 strlen

void reverse( char *start, char *end )
{
    while( start < end )
    {
        char c = *start;
        *start++ = *end;
        *end-- = c;
    }
}

char *reverse_char( char *start )
{
    char *end = start;
    while( (end[1] & 0xC0) == 0x80 ) end++;
    reverse( start, end );
    return( end+1 );
}

void reverse_string( char *string )
{
    char *end = string;
    while( *end ) end = reverse_char( end );
    reverse( string, end-1 );
}
于 2008-10-13T23:14:00.930 回答
5

我最初的方法可以这样总结:

1) 天真地反转字节

2) 向后运行字符串并随时修复 utf8 序列。

非法序列在第二步中处理,在第一步中,我们检查字符串是否处于“同步”状态(即,它是否以合法的前导字节开头)。

编辑:改进了 Reverse() 中前导字节的验证

class UTF8Utils {


    public static void Reverse(byte[] str) {
        int len = str.Length;
        int i   = 0;
        int j   = len - 1;

        //  first, check if the string is "synced", i.e., it starts
        //  with a valid leading character. Will check for illegal 
        //  sequences thru the whole string later.
        byte leadChar = str[0];

        //  if it starts with 10xx xxx, it's a trailing char...
        //  if it starts with 1111 10xx or 1111 110x 
        //  it's out of the 4 bytes range.
    //  EDIT: added validation for 7 bytes seq and 0xff
        if( (leadChar & 0xc0) == 0x80 ||
            (leadChar & 0xfc) == 0xf8 ||
            (leadChar & 0xfe) == 0xfc ||
        (leadChar & 0xff) == 0xfe ||
        leadChar == 0xff) {

            throw new Exception("Illegal UTF-8 sequence");

        }

        //  reverse bytes in-place naïvely
        while(i < j) {
            byte tmp = str[i];
            str[i]  = str[j];
            str[j]  = tmp;
            i++;
            j--;
        }
        //  now, run the string again to fix the multibyte sequences
        UTF8Utils.ReverseMbSequences(str);

    }

    private static void ReverseMbSequences(byte[] str) {
        int i = str.Length - 1;
        byte leadChar = 0;
        int nBytes  = 0;

        //  loop backwards thru the reversed buffer
        while(i >= 0) {
            //  since the first byte in the unreversed buffer is assumed to be
            //  the leading char of that byte, it seems safe to assume that the  
            //  last byte is now the leading char. (Given that the string is
            //  not out of sync -- we checked that out already)
            leadChar = str[i];

            //  check how many bytes this sequence takes and validate against
            //  illegal sequences
            if(leadChar < 0x80) {
                nBytes = 1;
            } else if((leadChar & 0xe0) == 0xc0) {
                if((str[i-1] & 0xc0) != 0x80) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 2;
            } else if ((leadChar & 0xf0) == 0xe0) {
                if((str[i-1] & 0xc0) != 0x80 ||
                    (str[i-2] & 0xc0) != 0x80 ) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 3;
            } else if ((leadChar & 0xf8) == 0xf0) {
                if((str[i-1] & 0xc0) != 0x80 ||
                    (str[i-2] & 0xc0) != 0x80 ||
                    (str[i-3] & 0xc0) != 0x80  ) {
                    throw new Exception("Illegal UTF-8 sequence");
                }
                nBytes = 4;
            } else {
                throw new Exception("Illegal UTF-8 sequence");
            }

            //  now, reverse the current sequence and then continue
            //  whith the next one
            int back    = i;
            int front   = back - nBytes + 1;

            while(front < back) {
                byte tmp = str[front];
                str[front] = str[back];
                str[back] = tmp;
                front++;
                back--;
            }
            i -= nBytes;
        }
    }
} 
于 2008-10-13T22:35:08.497 回答
-3

最佳解决方案:

  1. 转换为宽字符字符串
  2. 反转新字符串

从不,从不,从不,从不将单个字节视为字符。

于 2008-10-13T22:36:16.013 回答