Оптимизация алгоритма WordWrap - PullRequest
0 голосов
/ 17 марта 2011

У меня есть алгоритм переноса слов, который в основном генерирует строки текста, которые соответствуют ширине текста. К сожалению, это происходит медленно, когда я добавляю слишком много текста.

Мне было интересно, наблюдал ли я за какой-либо серьезной оптимизацией, которую можно было бы сделать. Кроме того, если у кого-то есть дизайн, который по-прежнему допускает строки строк или строковые указатели строк, то я был бы готов переписать алгоритм.

Спасибо

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 ]

2 голосов
/ 17 марта 2011

Проблемы с алгоритмами часто сопровождаются проблемами с структурами данных.

Сначала сделаем несколько замечаний:

  • абзацы можно обрабатывать независимо
  • редактированиепри заданном индексе делает недействительным только текущее слово и те, которые следуют за
  • , нет необходимости копировать целые слова, когда их индекса будет достаточно для их извлечения, и только их длина имеет значение для вычисления

Абзац

Я бы начал с введения понятия абзаца, которое определяется введенными пользователем переносами строк.Когда происходит издание, вам нужно найти соответствующий абзац, для которого требуется структура поиска.

«Идеальной» структурой здесь будет дерево Фенвика для небольшого текстового поля, однако это кажетсяизлишество.Мы просто сохраним в каждом абзаце количество отображаемых строк, которые составляют его представление, и вы будете считать с начала.Обратите внимание, что доступ к последней отображаемой строке - это доступ к последнему абзацу.

Таким образом, абзацы сохраняются в виде непрерывной последовательности, в терминах C ++, вполне вероятно, принимают удар косвенного действия (т.е. хранят указатели)чтобы сохранить их перемещение при удалении абзаца в середине.

Каждый абзац будет хранить:

  • его содержимое, простейшим из которых будет std::string для его представления.
  • его отображение в редактируемой форме (которую нам нужно определить еще)

Каждый абзац будет кэшировать свое отображение, этот кэш абзаца будет аннулирован всякий раз, когда редактирование

Фактический рендеринг будет выполняться только для пары абзацев за раз (и, что лучше, для пары отображаемых строк): видимых.

ОтображаетсяСтрока

Абзац может отображаться как минимум с одной строкой, но максимума нет.Нам нужно хранить «дисплей» в редактируемой форме, то есть форме, подходящей для издания.

Один кусок символов с добавленным \n не подходит.Изменения подразумевают перемещение большого количества символов, и пользователи должны изменять текст, поэтому нам нужно лучше.

Используя длины вместо символов, мы можем на самом деле хранить только 4байт (если строка занимает более 3 ГБ ... Я не гарантирую многого об этом алгоритме).

Моей первой идеей было использование индекса символов, однако в случае редактирования все последующие индексы изменяются,и распространение подвержено ошибкам.Длина смещена, поэтому у нас есть индекс относительно позиции предыдущего слова.Это ставит вопрос о том, что такое слово (или токен).В частности, вы сверните несколько пробелов?Как вы справляетесь с ними?Здесь я предполагаю, что слова отделяются друг от друга одним пробелом.

Для «быстрого» поиска я также сохраню длину всей отображаемой строки.Это позволяет быстро пропустить первые отображаемые строки, когда редактирование выполняется с помощью символа 503. Абзац

Таким образом, отображаемая строка будет состоять из:

  • общей длины (уступаетмаксимальная отображаемая длина поля после завершения вычисления)
  • последовательность слов (токенов) длина

Эта последовательность должна эффективно редактироваться на обоих концах (поскольку для переноса мыБуду вставлять / вставлять слова на обоих концах в зависимости от того, добавлены или удалены слова редактированияЭто не так важно, если посередине мы не настолько эффективны, потому что в середине редактируется только одна строка за раз.

В C ++, vector или deque должно быть хорошо.Хотя теоретически 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 голосов
/ 17 марта 2011

Есть две основные вещи, которые делают это медленнее, чем могло бы быть, я думаю.

Первое и, возможно, менее важное: когда вы строите каждую строку, вы добавляете слова в строку. Каждая такая операция может потребовать перераспределения строки и копирования ее старого содержимого. Для длинных линий это неэффективно. Однако, я предполагаю, что при реальном использовании ваши строки довольно короткие (скажем, 60-100 символов), и в этом случае стоимость вряд ли будет огромной. Тем не менее, там, вероятно, есть некоторая эффективность, которую можно выиграть.

Второе, и, вероятно, гораздо более важное: вы, очевидно, используете это для текстовой области в каком-то графическом интерфейсе, и я предполагаю, что он вводится. Если вы пересчитываете для каждого напечатанного символа, это действительно будет больно, когда текст станет длинным.

Пока пользователь добавляет только символы в конце - что, безусловно, является наиболее распространенным случаем - вы можете эффективно использовать тот факт, что с вашим «жадным» изменением алгоритма переноса строк ничего не влияет на ранее линии: поэтому просто пересчитайте с начала последней строки.

Если вы хотите сделать это быстро, даже когда пользователь печатает (или удаляет, или что-то еще) где-то в середине текста, ваш код должен будет выполнять больше работы и хранить больше информации. Например: всякий раз, когда вы строите строку, помните: «если вы начинаете строку с этим словом, оно заканчивается этим словом и этим всей полученной строкой ». Признать недействительной эту информацию, когда что-либо меняется в этой строке. Теперь, после небольшого редактирования, большинство изменений не потребуют большого пересчета. Вы должны проработать детали этого для себя, потому что (1) это хорошее упражнение и (2) мне нужно идти спать.

(Для экономии памяти вы можете предпочесть вообще не хранить целые строки - независимо от того, реализуете ли вы только что описанный трюк. Вместо этого просто сохраняйте здесь информацию о разрыве строки и сборку вверх строк, поскольку ваш пользовательский интерфейс должен отображать их.)

Это, вероятно, более сложная задача, чем вы хотели бы принять прямо сейчас, но вам также следует взглянуть на алгоритм переноса строк, основанный на динамическом программировании Дональда Кнута. Это существенно сложнее, чем у вас, но все еще может быть сделано довольно быстро, и это дает значительно лучшие результаты. См., Например, http://defoe.sourceforge.net/folio/knuth-plass.html.

0 голосов
/ 17 марта 2011

Общее изменение алгоритма -

  1. потренируйтесь, если вам нужна полоса прокрутки как можно дешевле, т.е. посчитайте число \ n в тексте и, если оно больше, чем vheight, включите прокрутку, проверьте длины и так далее.
  2. подготовьте текст в соответствующие строки для элемента управления теперь, когда вы знаете, что вам нужна полоса прокрутки или нет.

Это позволяет вам удалить / уменьшить тест if(!isVscroll && isVScrollNeeded()), так как он запускается почти для каждого символа - isVScroll, вероятно, не подходит, пример кода, похоже, не передает знание строк в функцию, поэтому не может понять, как он говорит, если это необходимо.

Предполагая, что textRows - это vector<string> - textrows.back() += довольно дорого, поиск спины не так сильно, как + = на строке, не эффективен для строк. Я бы использовал ostrstream для сбора строки и вставил ее, когда это будет сделано.

getFont (). GetWidth (), вероятно, будут дорогими - шрифт меняется? насколько отличается ширина между самым маленьким и самым большим ярлыками для шрифтов фиксированной ширины.

Используйте нативные методы, где это возможно, чтобы получить размер слова, поскольку вы не хотите их разбивать - GetTextExtentPoint32

Часто при переключении между ними будет достаточно места для VScroll. Перезапуск с начала измерения может стоить вам вдвое больше времени. Сохраняйте ширину линии для каждой строки, чтобы вы могли пропустить те, которые все еще подходят. Или не создавайте строковые строки напрямую, оставляйте слова отдельно от размера.

Насколько точным оно должно быть? Применить прагматизм ...
Просто предположим, что VScroll понадобится, в основном перенос не сильно изменится, даже если это не так (1 буква слова в конце / начале строки)

старайтесь больше работать со словами, чем с буквами - проверка оставшегося места для каждой буквы может тратить время. Предположим, что каждая буква в строке является самой длинной буквой, буквы х самой длинной <пробел, затем вставьте ее. </p>

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...