优化 WordWrap 算法

Posted

技术标签:

【中文标题】优化 WordWrap 算法【英文标题】:Optimizing WordWrap Algorithm 【发布时间】:2011-03-16 23:11:23 【问题描述】:

我有一个自动换行算法,它基本上可以生成适合文本宽度的文本行。不幸的是,当我添加太多文本时它会变慢。

我想知道我是否监督了可以进行的任何重大优化。此外,如果有人的设计仍然允许行的字符串或行的字符串指针更好,我愿意重写算法。

谢谢

void AguiTextBox::makeLinesFromWordWrap()

    textRows.clear();
    textRows.push_back("");
    std::string curStr;
    std::string curWord;

    int curWordWidth = 0;
    int curLetterWidth = 0;
    int curLineWidth = 0;

    bool isVscroll = isVScrollNeeded();
    int voffset = 0;
    if(isVscroll)
    
        voffset = pChildVScroll->getWidth();
    
    int AdjWidthMinusVoffset = getAdjustedWidth() - voffset;
    int len = getTextLength();
    int bytesSkipped = 0;
    int letterLength = 0;
    size_t ind = 0;

    for(int i = 0; i < len; ++i)
    

        //get the unicode character
        letterLength = _unicodeFunctions.bringToNextUnichar(ind,getText());
        curStr = getText().substr(bytesSkipped,letterLength);


        bytesSkipped += letterLength;

        curLetterWidth = getFont().getTextWidth(curStr);

        //push a new line
        if(curStr[0] == '\n')
        
            textRows.back() += curWord;
            curWord = "";
            curLetterWidth = 0;
            curWordWidth = 0;
            curLineWidth = 0;
            textRows.push_back("");
            continue;
        



            //ensure word is not longer than the width
            if(curWordWidth + curLetterWidth >= AdjWidthMinusVoffset && 
                curWord.length() >= 1)
            
                textRows.back() += curWord;

                textRows.push_back("");
                curWord = "";
                curWordWidth = 0;
                curLineWidth = 0;
            

            //add letter to word
            curWord += curStr;
            curWordWidth += curLetterWidth;


        //if we need a Vscroll bar start over
        if(!isVscroll && isVScrollNeeded())
        
            isVscroll = true;
            voffset = pChildVScroll->getWidth();
            AdjWidthMinusVoffset = getAdjustedWidth() - voffset;
            i = -1;
            curWord = "";
            curStr = "";
            textRows.clear();
            textRows.push_back("");
            ind = 0;

            curWordWidth = 0;
            curLetterWidth = 0;
            curLineWidth = 0;

            bytesSkipped = 0;
            continue;
        

        if(curLineWidth + curWordWidth >= 
            AdjWidthMinusVoffset && textRows.back().length() >= 1)
        
            textRows.push_back("");
            curLineWidth = 0;
        

        if(curStr[0] == ' ' || curStr[0] == '-')
        
            textRows.back() += curWord;
            curLineWidth += curWordWidth;
            curWord = "";
            curWordWidth = 0;
        
    

    if(curWord != "")
    
        textRows.back() += curWord;
    

    updateWidestLine();

【问题讨论】:

到目前为止,为了加快速度,您做了哪些尝试?当要求优化时,提供代码是有帮助的。附上代码后,将这个问题放在codereview.stackexchange.com上可能更有益 关键优化在第 42 行。 有一个示例输入和输出可能会有所帮助,“生成适合宽度的线条”的描述对我来说没有意义。您的目标是最方形的文本块,还是基于新闻纸的列,或者最适合消息框,或者最适合表格中的单元格或... @Greg Domjan 只是一个类似于 Windows 的文本框,您可以在其中将单词添加到一行中,直到下一个单词超出文本框的宽度,就像我上面所说的那样 @Milo:getText() 是否返回副本或引用?如果是副本,则每次循环迭代都会调用它两次,这会很昂贵。 【参考方案1】:

我认为,主要有两个原因使这比它可能的速度慢。

第一个,可能不太重要:当你构建每一行时,你正在将单词附加到该行。每个这样的操作都可能需要重新分配行并复制其旧内容。对于长线,这是低效的。但是,我猜在实际使用中你的行很短(比如 60-100 个字符),在这种情况下成本不太可能很大。不过,在那里可能会获得一些效率。

第二个,可能更重要:您显然在某种 GUI 中将它用于文本区域,我猜它正在被输入。如果您要为输入的每个字符重新计算,那么一旦文本变长,那真的会受到伤害。

只要用户只在末尾添加字符(这肯定是最常见的情况),您就可以有效地利用“贪婪”换行算法更改不会影响之前的任何内容这一事实行:所以只需从最后一行的开头重新计算。

如果您想在用户在文本中间某处键入(或删除或其他任何内容)的情况下使其快速运行,您的代码将需要做更多的工作并存储更多信息。例如:每当你构建一行时,请记住“如果你以 this 单词开始一行,它以 that 单词结尾,而 this 是整个结果线”。当该行内发生任何变化时,此信息无效。现在,经过一点编辑,大多数更改都不需要重新计算。你应该自己弄清楚这个细节,因为(1)这是一个很好的练习,(2)我现在需要睡觉了。

(为了节省内存,您可能根本不希望存储整行——无论您是否实现我刚刚描述的那种技巧。相反,只需存储此处的下一个换行符信息并构建你的 UI 需要渲染它们。)

这可能比您现在想了解的更复杂,但您还应该查看 Donald Knuth 的基于动态编程的换行算法。它比你的要复杂得多,但仍然可以很快完成,并且产生明显更好的结果。参见,例如,http://defoe.sourceforge.net/folio/knuth-plass.html

【讨论】:

【参考方案2】:

算法问题往往伴随着数据结构问题。

让我们先做一些观察:

段落可以独立处理 在给定索引处编辑只会使当前单词和后面的单词无效 当它们的索引足以检索它们并且只有它们的长度对计算有影响时,没有必要复制整个单词

段落

我将首先介绍段落的概念,它由用户引入的换行符决定。当一个版本发生时,你需要定位哪个是相关段落,这需要一个查找结构。

这里的“理想”结构是 Fenwick 树,但对于一个小文本框来说,这似乎有点过头了。我们将让每个段落存储构成其表示的显示行数,您将从头开始计数。请注意,访问最后显示的行就是访问最后一段。

因此,这些段落被存储为一个连续的序列,用 C++ 术语来说,很可能会受到间接(即存储指针)的影响,以便在删除中间的段落时避免移动它们。

每个段落都将存储:

它的内容,最简单的就是用一个std::string 来表示它。 它的显示,以可编辑的形式(我们仍然需要确定)

每个段落都会缓存其显示,段落缓存将在每次编辑时失效。

一次只对几个段落进行实际渲染(最好是显示几行):那些可见的。

显示线

一个段落可以至少显示一行,但没有最大值。我们需要将“显示”存储为可编辑的形式,即适合编辑的形式。

带有\n 的单个字符块是不合适的。更改意味着移动大量字符,并且用户应该更改文本,所以我们需要更好的。

使用长度而不是字符,我们实际上可能只存储 4 个字节(如果字符串占用超过 3GB ......我对这个算法不能保证太多)。

我的第一个想法是使用字符索引,但是在版本的情况下,所有后续索引都会更改,并且传播容易出错。长度是偏移量,所以我们有一个相对于前一个单词位置的索引。它确实提出了单词(或标记)是什么的问题。值得注意的是,您是否折叠多个空格?你如何处理它们?在这里,我假设单词之间用一个空格隔开。

为了“快速”检索,我还将存储整个显示行的长度。这允许在段落的字符 503 处进行编辑时快速跳过显示的第一行。

因此显示的行将由以下部分组成:

总长度(低于框的最大显示长度,计算结束后) 单词(标记)长度的序列

这个序列应该在两端都可以有效地编辑(因为为了换行,我们将在两端推送/弹出单词,具体取决于编辑是添加还是删除单词)。如果在中间我们效率不高,这并不那么重要,因为在中间一次只编辑一行。

在 C++ 中,vectordeque 应该没问题。虽然理论上list 将是“完美的”,但实际上它较差的内存局部性和高内存开销将抵消其渐近保证。一行由几个单词组成,所以渐近行为无关紧要,高常数才是。

渲染

对于渲染,选取一个长度已经足够的缓冲区(std::string 调用 reserve 就可以了)。通常情况下,您会clear 并每次都重写缓冲区,因此不会发生内存分配。

您不需要显示看不见的内容,但需要知道有多少行才能选择正确的段落。

一旦你得到段落:

offset 设置为0 对于隐藏的每一行,将 offset 的长度递增(后面的空格加 1) 一个词作为_content的子串被访问,你可以在buffer上使用insert方法:buffer.insert(buffer.end(), _content[offset], _content[offset+length])

困难在于维护offset,但这正是算法高效的原因。

结构

struct LineDisplay: private boost::noncopyable

  Paragraph& _paragraph;
  uint32_t _length;
  std::vector<uint16_t> _words; // copying around can be done with memmove
;

struct Paragraph:

  std::string _content;
  boost::ptr_vector<LineDisplay> _lines;
;

使用这种结构,实现应该简单明了,并且在内容增长时不应该放慢速度。

【讨论】:

【参考方案3】:

算法的一般变化 -

    确定是否需要尽可能便宜的滚动条,即。计算文本中 \n 的数量,如果它大于 vheight 则打开滚动条,检查长度等等。 既然您知道是否需要滚动条,请将文本准备到控件的适当行中。

这允许您删除/减少测试 if(!isVscroll &amp;&amp; isVScrollNeeded()) 几乎在每个字符上运行 - isVScroll 可能并不便宜,示例代码似乎没有将行的知识传递给函数所以看不到如何它会告诉您是否需要。

假设textRowsvector&lt;string&gt; - textrows.back() += 有点贵,查找后面不如 += 对字符串无效。我会改为使用ostrstream 来收集行并在完成后将其推入。

getFont().getWidth() 可能很昂贵 - 字体会改变吗?固定宽度字体的快捷方式的最小和最大宽度之间的差异有多大。

尽可能使用本地方法来获取单词的大小,因为您不想破坏它们 - GetTextExtentPoint32

当您在两者之间切换时,通常会有足够的空间来容纳 VScroll。从头开始测量可能会花费您两倍的时间。将每条线的线宽存储起来,这样您就可以跳过仍然合适的线。 或者不要直接建行字符串,保持单词与大小分开。

它真的需要多准确? 应用一些实用主义...... 假设需要 VScroll,即使不需要,大部分换行也不会发生太大变化(行尾/行首的 1 个字母单词)

尝试更多地使用单词而不是字母 - 检查每个字母的剩余空间会浪费时间。假设字符串中的每个字母都是最长的字母,字母x最长

【讨论】:

以上是关于优化 WordWrap 算法的主要内容,如果未能解决你的问题,请参考以下文章

UTF-8 的多字节安全 wordwrap() 函数

php Wordwrap示例

wordwrap 一个很长的字符串

wordwrap — 打断字符串为指定数量的字串

wordwrap — 打断字符串为指定数量的字串

QT Text.WordWrap 在 ColumnLayout 中不起作用