ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.10.2023
Просмотров: 440
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
78 Глава 3
Эти первичные и вторичные свойства определяют то, каким образом используются массивы. Работая с любой разновидностью агрегированной структурой данных, хорошо иметь в голове набор базовых операций при рассмотрении вариантов решения задачи.
Воспринимайте эти базовые операции как инструменты: молотки, отвертки и гаечные ключи структур данных. Не всякая механическая проблема может быть решена с помощью общедоступных инстру- ментов, но вам всегда следует смотреть, можно ли решить пробле- му с помощью этих инструментов прежде, чем отправляться искать новый. Далее приведен мой список базовых операций для массивов.
1 ... 5 6 7 8 9 10 11 12 ... 34
Сохранение
Это самая базовая операция. Массив — это набор переменных и мы можем присвоить значение каждой из них. Чтобы присвоить целое число 5 первому элементу (элементу 0) ранее объявленного массива, мы просто сообщаем:
tenIntegerArray[0] = 5;
Как и в случае с любой переменной, значения элементов внутри массива будут просто случайным «мусором» до тех пор, пока им не присвоят конкретное значение, таким образом, массивы следует инициализировать перед использованием. В некоторых случаях, особенно для тестирования, нам потребуется присвоить конкретное значение каждому элементу массива. Мы можем сделать это с помо- щью инициализатора сразу после объявления массива.
int tenIntegerArray[10] = {4, 5 , 9, 12, -4, 0, -57, 30987, -287, 1};
Вскоре вы увидите хороший пример использования инициа- лизатора. Иногда вместо присваивания разных значений каждому элементу массива нам может потребоваться инициализировать все элементы массива одним значением. Существует несколько вари- антов сокращений, позволяющих присвоить нули всем элементам массива в зависимости от ситуации или используемого компилято- ра (например, компилятор С++ в среде разработки Microsoft Visual
Studio инициализирует все элементы массива нулем, если иное не указано пользователем). На данном этапе, однако, я бы всегда явно инициализировал массив вне зависимости от того, требуется ли та- кая инициализация, так как это повысит удобочитаемость кода, как в следующем примере кода, который устанавливает все элементы
10-элементного массива равными –1:
int tenIntegerArray[10];
for (int i = 0; i < 10; i++) tenIntegerArray[i] = -1;
Копирование
Мы можем сделать копию массива. Существует две часто встреча- ющиеся ситуации, в которых это может оказаться полезным. Во-
Решение задач с массивами 79
первых, нам может потребоваться очень сильно изменить массив, но при этом иметь такой же массив в его первозданной форме для последующих операций. Восстановить исходный вид массива после проведения операций с ним может быть очень сложно или вовсе не- возможно, если мы изменили хотя бы одно значение. Скопировав весь массив, мы можем работать с копией, не изменяя исходный мас- сив. Все что нам нужно, чтобы скопировать весь массив, — это цикл и инструкция присваивания, точно так же, как и в случае с кодом инициализации:
int tenIntegerArray[10] = {4, 5 , 9, 12, -4, 0, -57, 30987, -287, 1};
int secondArray[10];
for (int i = 0; i < 10; i++) secondArray[i] = tenIntegerArray[i];
Эта операция доступна для большинства агрегированных струк- тур данных. Вторая ситуация более специфична для массивов. Ино- гда нам может потребоваться скопировать часть данных из одного массива во второй, либо мы можем захотеть скопировать элементы одного массива в другой в рамках метода реорганизации порядка элементов. Если вы изучали алгоритм сортировки слиянием, то вы видели эту ситуацию в действии. Далее по тексту главы мы увидим несколько примеров копирования.
Извлечение и поиск
Имея возможность поместить значения в массив, мы также должны быть способны получать данные из массива. Извлечение значения из конкретной позиции выполняется прямолинейно:
int num = tenIntegerArray[0];
Поиск определенного значения
Обычно ситуация не так проста. Часто нам неизвестна нужная по- зиция, и требуется произвести поиск в массиве, чтобы определить по- зицию конкретного значения. Если элементы массива не упорядоче- ны особым образом, то самое лучшее, что мы можем сделать, — это произвести последовательный поиск, в процессе которого мы будем перебирать все элементы массива, пока не найдем нужное значение.
Ниже представлена базовая версия кода.
X const int ARRAY_SIZE = 10;
Y int intArray[ARRAY_SIZE] = {4, 5, 9, 12, -4, 0, -57, 30987, -287, 1};
Z int targetValue = 12;
[ int targetPos = 0;
while ((intArray[targetPos] != targetValue) && (targetPos <
\ ARRAY_SIZE))
targetPos++;
В этом коде присутствует константа, в которой хранится размер массива
X
, сам массив
Y
, переменная для хранения значения, кото- рое мы ищем в массиве
Z
, а также переменная для хранения пози- ции этого значения
[
. В данном примере мы используем константу
ARRAY_SIZE
для ограничения количества итераций по нашему мас-
80 Глава 3
сиву
\
, чтобы не выйти за его пределы, если среди его элементов не обнаружится целевое значение targetValue. Вы могли бы «жестко за- кодировать» значение 10 вместо константы, однако использование константы делает код более общим, что облегчает его модифика- цию и повторное использование. Мы будем использовать констан- ту ARRAY_SIZE в большей части фрагментов кода, приведенных в дан- ной главе. Обратите внимание на то, что, если значение targetValue не будет обнаружено в массиве intArray, то после завершения цикла значение targetPos будет равно значению ARRAY_SIZE. Этого доста- точно для обозначения данного события, поскольку значение ARRAY_
SIZE
не является допустимым номером элемента. Тем не менее про- верить это предстоит коду, следующему далее. Также заметьте, что этот код не предусматривает никаких действий на случай, если целе- вое значение встретится более одного раза. При первом появлении целевого значения цикл завершится.
Поиск по критерию
Иногда искомое значение является не фиксированным, а основан- ным на отношении с другими значениями массива. Например, нам требуется найти максимальное значение в массиве. Механизм для решения этой задачи я называю «Царь горы» по аналогии с извест- ной игрой. Пусть у вас будет переменная, представляющая наиболь- шее значение из обнаруженных в массиве до сих пор. Вы перебираете все элементы массива с помощью цикла, и каждый раз, когда сталки- ваетесь со значением, превышающим предыдущее наибольшее зна- чение, новое значение сбрасывает предыдущего царя с горы, зани- мая его место:
const int ARRAY_SIZE = 10;
int intArray[ARRAY_SIZE] = {4, 5, 9, 12, -4, 0, -57, 30987, -287, 1};
X int highestValue = intArray[0];
Y for (int i = 1; i < ARRAY_SIZE; i++) {
Z if (intArray[i] [ > highestValue) highestValue = intArray[i];
}
В переменной highestValue хранится наибольшее значение, най- денное в массиве до сих пор. При ее объявлении ей присваивается значение первого элемента массива
X
, что позволяет запустить цикл на втором элементе массива (это позволяет нам начать со значения i
равного 1, а не 0)
Y
. Внутри цикла мы сравниваем значение в те- кущей позиции со значением highestValue, при необходимости за- меняя значение highestValue
Z
. Обратите внимание на то, что для нахождения наименьшего значения вместо наибольшего достаточ- но просто заменить оператор сравнения «больше чем» оператором сравнения «меньше чем» (и изменить название переменной, чтобы не запутаться). Эта базовая структура может применяться ко всем видам ситуаций, в которых нам требуется рассмотреть каждый эле- мент массива, чтобы найти значение, в наибольшей степени иллю- стрирующее конкретное качество.
Решение задач с массивами 81
Сортировка
Сортировка означает расположение данных в определенном поряд- ке. Вам, вероятно, уже встречались алгоритмы сортировки масси- вов. Это классическая область для применения анализа эффектив- ности, поскольку существует так много конкурирующих алгоритмов сортировки, каждый из которых имеет различные характеристики производительности, зависящие от особенностей базовых данных.
Изучение различных алгоритмов сортировки могло бы стать пред- метом отдельной книги, поэтому мы не будем глубоко исследовать данную область. Вместо этого сосредоточимся на том, что можно применить на практике. В большинстве ситуаций вы сможете обой- тись двумя алгоритмами сортировки — быстрым, простым в исполь- зовании, и хорошим, легким для понимания, которые вы сможете, при необходимости, с уверенностью модифицировать. Для быстрой и простой сортировки мы будем использовать стандартную библи- отечную функцию qsort, а когда понадобится что-то настроить — сорти ровку методом вставок.
Быстрая и простая сортировка с помощью функции qsort
Для программистов C/C++ алгоритмом быстрой сортировки по умол- чанию является функция стандартной библиотеки qsort (название предполагает применение быстрой сортировки (quicksort), однако реализатор библиотеки не обязан использовать этот алгоритм). Для использования функции qsort мы должны написать функцию сравне- ния. Ее будет вызывать функция qsort, когда понадобится сравнить два элемента в массиве, чтобы решить, какой из них должен отобра- жаться раньше в отсортированном порядке. В эту функцию передает- ся два указателя типа void. В данной книге мы еще не обсуждали указа- тели, однако пока вам достаточно знать то, что вы должны привести эти указатели void к указателям на тип элемента в вашем массиве. За- тем функция должна возвратить значение int, которое может быть положительным, отрицательным или равным нулю, в зависимости от того, является ли значение первого элемента большим, меньшим или равным значению второго элемента. Конкретное возвращаемое зна- чение не важно, главное то, положительное оно, отрицательное или равное нулю. Давайте проясним это с помощью небольшого приме- ра сортировки массива из 10 целых чисел с использованием функции qsort
. Наша функция сравнения выглядит следующим образом: int compareFunc(
Xconst void * voidA, const void * voidB) {
Y int * intA = (int *)(voidA);
int * intB = (int *)(voidB);
Z return *intA — *intB;
}
Список параметров состоит из двух указателей const void
X
Так бывает всегда в случае с компаратором. Код внутри функции начинается с объявления двух указателей int
Y
и приведения двух
82 Глава 3
указателей void к указателям на тип int. Мы могли бы написать функ- цию без двух временных переменных; я использовал их здесь для яс- ности. Дело в том, что, как только мы закончим с этими объявлени- ями, intA и intB будут указывать на два элемента в нашем массиве, а *intA и *intB будут двумя целыми числами, подлежащими сравне- нию. Наконец мы возвращаем результат вычитания второго целого значения из первого
Z
. Это позволяет получить нужный результат.
Например, если *intA > *intB, нам нужно возвратить положитель- ное число, а результат вычисления выражения *intA — *intB будет положительным, если *intA > *intB. Точно так же результат вычис- ления выражения *intA — *intB будет отрицательным, если *intB >
*intA
, и будет равен нулю в случае их равенства.
После добавления функции компаратора пример использования функции qsort выглядит следующим образом:
const int ARRAY_SIZE = 10;
int intArray[ARRAY_SIZE] = {87, 28, 100, 78, 84, 98, 75, 70, 81, 68};
qsort(
XintArray, YARRAY_SIZE, Zsizeof(int), [compareFunc);
Как вы можете заметить, функция qsort принимает четыре па- раметра: массив, который должен быть отсортирован
X
; количество элементов в этом массиве
Y
; размер одного элемента в массиве, кото- рый, как правило, определяется, как и в данном случае, оператором sizeof
Z
; наконец, функция компаратора
[
. Если у вас нет большого опыта передачи функций в качестве параметров в другие функции, обратите внимание на синтаксис, используемый для последнего па- раметра. Мы передаем непосредственно саму функцию, а не вызы- ваем ее с последующей передачей результата этого вызова. Поэтому мы просто указываем имя функции без списка параметров или кру- глых скобок.
Легко модифицируемый алгоритм сортировки — сортировка
вставками
В некоторых случаях вам может потребоваться написать собственный код сортировки. Иногда встроенная функция сортировки просто не подходит для решения стоящей перед вами задачи. Например, вам нуж- но отсортировать массив данных, основываясь на данных в другом мас- сиве. При необходимости написания собственного кода вам нужен про- стой алгоритм сортировки, в котором вы будете уверены и который при необходимости можете без труда реализовать. Таким алгоритмом может стать сортировка методом вставок. Сортировка вставками напоми- нает сортировку карт при игре в бридж: игроки берут карты по одной за раз и размещают их в руке, поддерживая общий порядок и сдвигая другие карты, чтобы освободить для них место. Ниже представлена ба- зовая реализация нашего целочисленного массива:
Xint start = 0;
Yint end = ARRAY_SIZE — 1;
Zfor (int i = start + 1; i <= end; i++) {