优化WordWrap算法

时间:2011-03-16 23:11:24

标签: c++ algorithm optimization

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

我想知道我是否监督了可以做出的任何重大优化。此外,如果任何人的设计仍然允许更好的线条或字符串指针的线条,我将打开以重写算法。

由于

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();
}

3 个答案:

答案 0 :(得分:2)

我认为,有两个主要因素会让它变慢。

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

第二个,也许更重要的是:你显然在某种GUI中使用它作为文本区域,我猜它正在被输入。如果你为每个输入的字符重新计算,一旦文本变长,这真的会受到伤害。

只要用户只在最后添加字符 - 这肯定是最常见的情况 - 你可以有效地利用这样一个事实,即你的“贪婪”换行算法改变永远不会影响更早的事情lines:所以只需从最后一行的开头重新计算。

如果您希望即使用户在文本中间的某个位置键入(或删除或其他)时也要快速,您的代码将需要做更多的工作并存储更多信息。例如:无论何时构建一条线,请记住“如果您使用字开始一行,则以 字开头,而是整个结果线“。当该行内的任何变化时,使此信息无效。现在,经过一些编辑后,大多数更改都不需要重新计算。你应该自己弄清楚这个细节,因为(1)这是一个很好的练习,(2)我现在需要上床睡觉。

(为了节省内存,你可能根本不想存储整行 - 无论你是否实现我刚才描述的那种技巧。相反,只需存储这里的下一行换行信息和构建你的UI需要渲染它们。)

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

答案 1 :(得分:2)

算法问题通常会带来数据结构问题。

首先让我们做一些观察:

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

段落

我首先介绍段落的概念,它由用户引入的换行符确定。在进行编辑时,您需要找到哪个是相关段落,这需要查找结构。

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

这些段落因此以C ++术语存储为连续序列,很可能采用间接命中(即存储指针)来保存在中间的段落被移除时移动它们。

每个段落都会存储:

  • 其内容,最简单的是表示它的单个std::string
  • 以可编辑的形式显示(我们还需要确定)

每个段落都会缓存其显示,只要进行编辑,段落缓存就会失效。

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

显示行

段落可能至少显示一行,但没有最大值。我们需要以可编辑的形式存储“显示”,这是一种适合版本的形式。

投入\n的单个字符块不适合。更改意味着移动大量字符,用户假设正在更改文本,因此我们需要更好。

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

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

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

显示的行将由以下内容组成:

  • 总长度(低于盒子的最大显示长度,一旦计算结束)
  • 一系列单词(代币)长度

这个序列应该可以在两端有效地编辑(因为对于包装,我们将在两端按下/弹出单词,具体取决于编辑是否添加或删除了单词)。如果在中间我们效率不高,那就不那么重要了,因为在中间只编辑一行。

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

渲染

对于渲染,选择一个已经足够长度的缓冲区(std::string调用reserve即可)。通常,你只需要clear并重写缓冲区,所以不会发生内存分配。

您无需显示无法看到的内容,但需要知道有多少行,以获取正确的段落。

一旦你得到段落:

  • offset设为0
  • 对于隐藏的每一行,按其长度增加offset(后面的空格为+ 1)
  • 将一个字作为_content的子字符串进行访问,您可以使用insert上的buffer方法: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;
};

使用这种结构,实现应该是直截了当的,并且在内容增长时不应该减慢速度。

答案 2 :(得分:0)

算法的一般更改 -

  1. 如果您需要尽可能便宜的滚动条,即可以解决问题。计算文本中\ n的数量,如果它更大,那么打开滚动的vheight,检查长度等等。
  2. 在您知道需要滚动条的情况下,将文本准备到控件的相应行中。
  3. 这允许你移除/减少几乎每个字符上运行的测试if(!isVscroll && isVScrollNeeded()) - isVScroll可能不会吱吱作响,示例代码似乎没有将行知识传递给函数所以不能看看它是否需要它。

    假设textRowsvector<string> - textrows.back() +=有点贵,查看后面的内容不是+ =字符串上的字符串效率不高。我将更改为使用ostrstream收集行并在完成后将其推入。

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

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

    当你在两者之间进行切换时,通常会有足够的空间来容纳VScroll。从测量开始重新启动可能会花费您两倍的时间。存储每行的行宽,以便跳过仍然适合的行。 或者不要直接构建线条,保持单词大小分开。

    它真正需要的准确度是多少? 应用一些实用主义......
    只是假设需要VScroll,大部分包装都不会有太大变化,即使它不是(在一行的结尾/开头有1个字母的单词)

    尝试使用单词而不是字母更多地工作 - 检查每个字母的剩余空间可能会浪费时间。假设字符串中的每个字母是最长的字母,字母x最长&lt;空间然后把它放进去。