Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2177 / 0 | Длительность: 63:16:00
Лекция 3:

Элементарные структуры данных

Строки

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

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

Язык C++ наследует из C эффективную реализацию на основе массивов, которая рассматривается в этом разделе, а также предоставляет более обобщенную реализацию из стандартной библиотеки. В "Абстрактные типы данных" будут рассмотрены и другие реализации.

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

Для строки необходимо зарезервировать память либо во время компиляции, объявив массив символов фиксированной длины, либо во время выполнения, с помощью вызова new[]. После выделения памяти массиву его можно заполнять символами с начала и до символа завершения строки. Без символа завершения строка представляет собой обыкновенный массив символов. Символ завершения строки позволяет применять более высокий уровень абстракции, когда только часть массива (от начала до символа завершения) считается содержащей значимую информацию. Символ завершения имеет значение 0, или '\0'.

Например, чтобы найти длину строки, можно подсчитать количество символов от ее начала до символа завершения. В таблица 3.2 перечислены простые операции, которые часто выполняются со строками. Все они предусматривают просмотр строк от начала и до конца. Многие из этих функций содержатся в библиотеках, объявленных в файле <string.h>. Однако для простых приложений программисты часто используют слегка измененные версии прямо в коде программы. Надежные функции, реализующие те же операции, должны содержать дополнительный код проверки на ошибки. Код в таблица 3.2 представлен не только для иллюстрации его простоты, но и для наглядной демонстрации характеристик производительности.

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

Программа 3.15 является реализацией простой задачи обработки строк. Она выводит те позиции внутри длинной строки текста, где содержится короткая строка-образец. Для этой задачи разработано несколько сложных алгоритмов, а данный простой алгоритм демонстрирует несколько соглашений, используемых при обработке строк в C++.

В этой таблице представлены реализации основных операций обработки строк, использующие два различных примитива языка C++. Применение указателей делает код более компактным, а использование индексированного массива служит более естественным методом выражения алгоритмов и делает код более простым для понимания. Операция объединения для версии с указателями совпадает с операцией для версии с индексированным массивом. Операция сравнения первых символов для версии с указателями получается таким же образом, как и для версии с индексированным массивом, поэтому она опущена. Время выполнения всех реализаций пропорционально длине строк.

Таблица 3.2. Элементарные операции со строками
Версии с индексированным массивом
Вычисление длины строки (strlen(a)) for (i = 0; a[i] != 0; i++) ; return i ;
Копирование (strcpy(a, b)) for (i = 0; (a[i] = b[i]) != 0; i++) ;
Сравнение (strcmp(a, b)) for (i = 0; a[i] == b[i]; i++)
if (a[i] == 0) return 0;
return a[i] - b[i];
Сравнение (первых символов) (strncmp(a, b, n)) for (i = 0; i < n && a[i] != 0; i++)
if (a[i] != b[i]) return a[i] - b[i];
return 0;
Присоединение (strcat(a, b)) strcpy(a+strlen(a), b)
Эквивалентные версии с указателями
Вычисление длины строки (strlen(a)) b = a; while (*b++) ; return b-a-1;
Копирование (strcpy(a, b)) while (*a++ = *b++) ;
Сравнение (strcmp(a, b)) while (*a++ = *b++)
if (*(a-1) == 0) return 0;
return *(a-1) - *(b-1);

Обработка строк служит убедительным примером необходимости сведений о быстродействии библиотечных функций. Дело в том, что библиотечные функции могут работать медленнее, чем мы ожидаем. Например, время определения длины строки пропорционально ее длине. Игнорирование этого факта может привести к серьезным проблемам, связанным с быстродействием. Например, после краткого знакомства с библиотекой можно реализовать сравнение с образцом из программы 3.15 следующим образом:

for (i = 0; i < strlen(a); i++)
  if (strncmp(&a[i], p, strlen(p)) == 0)
    cout << i << " ";
        

К сожалению, время выполнения этого фрагмента пропорционально, как минимум, квадрату длины a независимо от кода тела цикла, поскольку для определения длины строки a каждый раз выполняется ее полный просмотр. Эти затраты времени велики и даже неприемлемы: выполнение программы для проверки наличия определенного слова в данной книге (содержащей более миллиона символов) потребует триллионов операций. Подобные проблемы трудно обнаружить, поскольку программа может хорошо работать с небольшими строками на этапе отладки, но сильно тормозить, если не полностью зависать, при решении реальных задач. Более того, таких проблем можно избежать, только зная о них!

Программа 3.15. Поиск строки

Эта программа обнаруживает все вхождения введенного из командной строки слова в строке текста (предположительно намного большей длины). Строка текста объявляется в виде массива символов фиксированной длины (можно с помощью операции new[], как в программе 3.6). Чтение строки выполняется из стандартного ввода с помощью функции cin.get() . Память для слова (аргумента командной строки) выделяется системой перед вызовом программы, а указатель строки содержится в элементе argv[1]. Для каждой начальной позиции i в строке a выполняется попытка сравнения подстроки, которая начинается с этой позиции, со словом p. Равенство проверяется символ за символом. При достижении конца слова p выводится начальная позиция i вхождения этого слова в текст.

#include <iostream.h>
#include <string.h>
static const int N = 10000;
int main(int argc, char *argv[])
  { int i; char t;
    char a[N], *p = argv[1];
    for (i = 0; i < N-1; a[i] = t, i++)
      if (!cin.get(t)) break;
    a[i] = 0;
    for (i = 0; a[i] != 0; i++)
      { int j;
        for (j = 0; p[j] != 0; j++)
          if (a[i+j] ! = p[j]) break;
        if (p[j] == 0) cout << i << " ";
      }
    cout << endl;
  }
        

Этот вид ошибок называется потерей быстродействия (performance bug), поскольку код может быть корректным, но его выполнение окажется не настолько эффективным, как ожидалось. Прежде чем приступить к изучению эффективных алгоритмов, необходимо устранить подобные потери быстродействия. Стандартные библиотеки обладают многими достоинствами, однако следует учитывать и потенциальные недостатки их использования для реализации таких простых функций.

Одна из важных концепций, к которой мы время от времени возвращаемся, гласит, что различные реализации одного и того же абстрактного понятия могут существенно различаться по производительности. Например, класс string стандартной библиотеки C++ постоянно отслеживает длину строки и может возвращать это значение за фиксированное время, хотя остальные операции выполняются медленнее. Для разных приложений могут быть более удобными различные реализации.

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

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

while (*a++ = *b++) ;
        

вместо

for (i = 0; a[i] != 0; i++) a[i] = b[i];
        

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

Распределение памяти для строк сложнее, чем для связных списков, поскольку строки имеют различный размер. Вообще-то максимально обобщенный механизм резервирования памяти для строк - это просто системные функции new[] и delete[]. Как было сказано в разделе 3.6, для решения этой задачи разработаны различные алгоритмы. Их характеристики производительности зависят от системы и компьютера. Часто распределение памяти при работе со строками является не такой сложной проблемой, как это может показаться, поскольку используются указатели на строки, а не сами символы. И обычно мы не предполагаем, что все строки занимают конкретно выделенные блоки памяти. Как правило, мы считаем, что каждая строка занимает область памяти с неопределенным адресом, но достаточно большую, чтобы вмещать строку и ее символ завершения. При выполнении операций создания либо удлинения строк следует очень внимательно отнестись к выделению памяти. В качестве примера мы рассмотрим в разделе 3.7 программу, которая читает строки и обрабатывает их.

Упражнения

  • 3.55. Напишите программу, которая принимает строку в качестве аргумента и выводит таблицу со всеми имеющимися в строке символами и частотой появления каждого из них.
  • 3.56. Напишите программу, которая определяет, является ли данная строка палиндромом (одинаково читается в прямом и обратном направлениях), если игнорировать пробелы. Например, программа должна давать положительный ответ для строки "if i had a hifi".
  • 3.57. Предположим, что память для строк выделяется индивидуально. Напишите версии функций strcpy и strcat, которые выделяют память и возвращают указатель на новую строку-результат.
  • 3.58. Напишите программу, которая принимает строку в качестве аргумента и читает ряд слов (последовательностей символов, разделенных пробелами) из стандартного ввода, выводя те из них, которые входят как подстроки в строку аргумента.
  • 3.59. Напишите программу, которая заменяет в данной строке подстроки, состоящие из нескольких пробелов, одним пробелом.
  • 3.60. Реализуйте версию программы 3.15, в которой будут использоваться указатели.
  • 3.61. Напишите эффективную программу, которая определяет длину самой большой последовательности пробелов в данной строке, просматривая как можно меньшее количество символов. Подсказка: с возрастанием длины последовательности пробелов программа должна выполняться быстрее.
Бактыгуль Асаинова
Бактыгуль Асаинова

Здравствуйте прошла курсы на тему Алгоритмы С++. Но не пришел сертификат и не доступен.Где и как можно его скаачат?

Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?