Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Практикум по программированию на языке Си

..pdf
Скачиваний:
25
Добавлен:
12.11.2023
Размер:
3.53 Mб
Скачать

В параметрах функция получает указатель на начало списка (struct cell * list) и указатель на открытый для чтения (режим "r") файл (FILE * testFile). Вспомогательный указатель struct cell * p "настраивается" на начало списка, а затем последовательно адресует все его элементы. Если p==NULL, то достигнут конец списка. В противном случае вызывается библиотечная функция позиционирования файла:

int fseek(указатель_на_файл, смещение, начало_отсчета);

В нашем случае началом отсчета является начало файла (это задает предопределенная константа SEEK_SET). Смещение выбирается как значение компонента position из структуры (из элемента списка). После этой установки позиции чтения из файла на начало очередной строки вызывается функция fgets(). Ее результат из строки char line[] функцией puts() выводится в стандартный выходной поток. Указателю p присваивается адрес следующего элемента списка структур, и цикл повторяется.

Основная функция программы:

/* 12_07.c - строки файла по возрастанию их длин */ #include <stdlib.h>

#include <stdio.h> #include <string.h> #define MAXLEN 2000

/* Структурный тип "Звено списка": */ struct cell {

int lenStr;

struct cell * next; long position;

};

#include "addCell.c" #include "printFile.c"

int main (int nArg, char * arg[])

{

FILE * testFile; char line[MAXLEN]; long pos = 0L;

struct cell * list = NULL;

541

int len;

char * pLF = NULL; if (nArg == 1)

{ printf("\nThe name of test file is absent!"); return 0;

}

testFile = fopen(arg[1], "r"); if (testFile == NULL)

/* Файл не открылся */

{ printf("\nThe file \"%s\" is absent!", arg[1]);

return 0;

}

/* Последовательное чтение строк из файла */ while (fgets(line, MAXLEN, testFile) != NULL)

{len = strlen(line); pLF=strchr(line,'\n'); if(pLF != NULL) len--;

list = addCell(list, pos, len); pos = ftell(testFile);

}

printFile(list, testFile);

}

В тексте функции main() проверяется наличие аргумента – имени анализируемого файла. Затем файл с заданным именем открывается и связывается с указателем FILE * testFile. Последовательное чтение строк из файла выполняется функцией fgets(). Она вернет значение NULL по достижении конца файла. Отметим, что проверку достижения конца файла можно выполнить и с помощью библиотечной функции feof(), как это сделано в функции openPrograms() программы 12_06.с. Наиболее интересно для нас получение позиции начала очередной строки. Позиция определяется как смещение (в байтах) от начала файла. Соответствующая переменная long pos инициализируется нулевым значением. Оно используется при первом обращении к функции addCell(). Когда в список, адресуемый указателем struct cell * list, занесена информация об очередной строке, определяется новое значение pos. Для этого используется библиотечная функция

long ftell(FILE *);

542

Она возвращает значение смещения от начала потока до позиции чтения (записи). Зная предыдущее значение позиции чтения (переменная pos) и длину прочитанной строки (len), то же самое можно получить и с помощью оператора

pos+=len+1;

Остальные детали не должны вызывать затруднений у читателя, добравшегося до этой темы. Обратим только внимание на роль указателя char * pLF (см. функцию openPrograms() из программы

12_06.с).

ЗАДАЧА 12-08. Решите предыдущую задачу о печати строк из файла в порядке возрастания их длин, используя вместо связного списка массив структур и упорядочивая его элементы с помощью библиотечной функции быстрой сортировки:

void qsort(void * base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

Функция qsort() сортирует массив из nmemb элементов, начиная с элемента, адресованного указателем base. Параметр size определяет размер каждого элемента массива. Правила сортировки определяет дополнительная функция, адресуемая параметром (указателем) compar. Эта дополнительная функция при каждом обращении к ней сравнивает два объекта, адресуемых указателями (ее аргументами). Названное обращение к дополнительной функции выполняется в теле библиотечной функции qsort() и "не видимо" при применении функции qsort(). Правила сравнения полностью зависят от тела дополнительной функции. И только ее автор решает, как ввести упорядочение для объектов сортируемого массива. Функция должна вернуть значение меньшее нуля, если объект, адресуемый его первым аргументом, считается меньшим, нежели объект, адресуемый вторым аргументом. Функция возвращает нулевое значение, если объекты равны. Функция возвращает положительное значение, если первый объект больше второго. Обратите внимание, что для указания типа элементов сортируемого массива использовано служебное слово void. Это делает библиотечную функцию qsort() универсальной, так

543

как вместо формального параметра типа void * может быть подставлен указатель на объект любого другого типа.

В качестве элементов сортируемого массива используем структуры типа:

struct arrayCell {

//длина строки

int lenStr;

long position; //начало строки в файле

};

Для сравнения элементов массива определим такую функцию:

/* Сравнение длин

двух строк */

int compare(const

struct

arrayCell * c1,

const

struct

arrayCell * c2)

{

 

 

if (c1 -> lenStr < c2 -> lenStr) return -1; if (c1 -> lenStr == c2 -> lenStr) return 0; if (c1 -> lenStr > c2 -> lenStr) return +1;

}

Чтобы воспользоваться библиотечной функцией qsort(), необходимо сортируемые объекты представить в виде массива с фиксированным числом элементов. Размер этого массива (т.е. число его элементов) должен в нашей задаче соответствовать количеству строк в анализируемом файле. Открыв файл, подсчитаем число его строк и создадим динамический массив с этим количеством элементов. Затем функцией fseek() переведем позицию чтения в начало файла и вторично прочитаем его строки, занося соответствующие данные (длина очередной строки и ее начало в файле) в элементы (структуры) массива. Далее обратимся к функции сортировки qsort() и упорядочим элементы массива структур по возрастанию длин строк. Затем, последовательно перебирая отсортированный массив структур, выведем строки анализируемого файла, используя позиции их начал. Следующая программа соответствует приведенному описанию:

// 12_08.c – применение библиотечной функции qsort() #include <stdlib.h>

#include <stdio.h> #include <string.h>

544

#define MAXLEN 2000

/* Структурный тип "Элемент массива": */ struct arrayCell {

int lenStr; long position; };

/* Сравнение длин двух строк */

int compare(const struct arrayCell * c1, const struct arrayCell * c2)

{

if (c1 -> lenStr < c2 -> lenStr) return -1; if (c1 -> lenStr == c2 -> lenStr) return 0; if (c1 -> lenStr > c2 -> lenStr) return +1;

}

/* Напечатать упорядоченно строки файла */ void printFile(struct arrayCell * array,

int lenFile, FILE * testFile)

{

char line[MAXLEN]; char * pLF = NULL; int i;

for (i=0; i < lenFile; i++)

{fseek(testFile, array[i].position, SEEK_SET); fgets(line, MAXLEN, testFile); pLF=strchr(line,'\n');

if (pLF != NULL) *pLF='\0'; puts(line);

}

}

int main (int nArg, char * arg[])

{

FILE * testFile; char line[MAXLEN]; char * pLF = NULL; long pos = 0L;

struct arrayCell * array = NULL; int lenFile = 0;

int lenLine; int i;

if (nArg == 1)

545

{printf("\nThe name of test file is absent!"); return 0;

}

testFile = fopen(arg[1], "r"); if (testFile == NULL)

/* Файл не открылся */

{ printf("\nThe file \"%s\" is absent!", arg[1]);

return 0;

}

/* Подсчет строк файла */ for (lenFile=0;

fgets(line, MAXLEN, testFile) != NULL; lenFile++);

/* Создание динамического массива */ array = (struct arrayCell *)

calloc (lenFile, sizeof(struct arrayCell)); if (array == NULL)

/* Нет памяти */

{printf("\nThe calloc() error!"); return 0;

}

/* Позиционирование на начало файла: */ fseek(testFile, 0L, SEEK_SET);

/* Последовательное чтение строк из файла: */ for (i=0; ; i++)

{ if (fgets(line, MAXLEN, testFile) == NULL) break;

lenLine = strlen(line); pLF=strchr(line,'\n'); if(pLF != NULL) lenLine--; array[i].lenStr = lenLine; array[i].position = pos; pos = ftell(testFile);

}

/* Сортировка массива сведений о строках файла */ qsort(array, lenFile, sizeof(array[0]), compare); printFile(array, lenFile, testFile);

}

В тексте программы комментарии поясняют основные особенности реализации.

546

12.4."Крупный" проект с файлами

ЗАДАЧА 12-09. Разработайте структуры данных и программы ведения англо-русского словаря. Предусмотрите два режима обработки – режим пополнения словаря и режим его использования. В режиме пополнения вводится английское слово и несколько русских переводных эквивалентов. В режиме использования словаря для введенного с клавиатуры английского термина выдаются все его переводы, имеющиеся в словаре.

Как в режиме заполнения словаря, так и в режиме его использования необходимо по заданному английскому термину (символьная строка) определить его размещение в файловой системе. В традиционных (печатных) словарях словарные статьи (термин плюс переводные эквиваленты) размещаются в алфавитном порядке терминов. Однако в "электронном" словаре такое размещение удобно применять, когда словарные статьи заранее (до начала заполнения словаря) уже собраны и упорядочены. Ввод словарных статей в произвольном порядке требует (при вставках) дополнительных затрат на упорядочение уже накопленной информации. Такие алгоритмы вставки новых терминов и сортировки файлов несложно разработать, но мы воспользуемся другим подходом – методом хэширования (см., например, [4]).

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

547

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

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

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

all – всякий, весь, все, всецело, вполне;

run – бегать, бежать, работать, распространяться, течь; sky – небо, небеса, высоко забросить (мяч);

room – комната, место.

Таким образом, при построении файловой системы нужно учитывать возможность появления коллизий при хэшировании терминов

548

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

Спроектируем схему взаимосвязей данных словаря вначале на бумаге. Предположим, что значение HASH_LEN так мало, что все слова, начинающиеся на одну букву, получают одно хэш-значение. Изобразим заполнение такого словаря приведенными выше словарными статьями (рис. 12.1). На рисунке изображена одна коллизия – термины run и room получили одно хэш-значение, и их словарные статьи объединены в хэш-цепочку (связный список). Переводные эквиваленты одного термина на рисунке объединены в список (хэшцепочку) его переводов.

Рис. 12.1. Взаимосвязи данных в словаре

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

549

В основной программе будем вводить английский термин, разыскивать его в словаре и в зависимости от результата поиска либо выводить список переводных эквивалентов (когда термин есть в словаре), либо предлагать пользователю ввести переводы (русские эквиваленты термина).

Данные словаря разместим в двух файлах:

"hash" – совокупность адресов (позиций начал цепочек) из файла словарных статей, организованных в виде списков (файл хэшзначений);

"articles" – совокупность списков словарных статей, каждая из которых содержит английский термин и все его переводы (файл хэшцепочек).

Количество элементов в файле hash будет равно HASH_LEN+1. Все элементы будут иметь одинаковый размер. Первый элемент будет сохранять значение HASH_LEN, остальные элементы будут по мере заполнения словаря принимать значения адресов (позиций) начал соответствующих хэш-цепочек в файле articles. До заполнения словаря всем элементам файла hash (кроме начального) необходимо присвоить некоторые начальные значения. Запишем туда значение –1L.

Вфайле articles будем хранить собственно словарные статьи

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

Будем создавать оба файла как текстовые. Это упростит отладку программы, так как содержимое файлов будет доступно для просмотра в любом текстовом редакторе (с учетом различия кодировок русских букв в MS-DOS и MS Windows).

Первый крупный шаг выполнения программы – подготовка фай-

лов hash и articles. В результате этой подготовки файлы должны быть открыты для последующих изменений (открыты в режимах "a+" и "r+"). Связанные с файлами указатели FILE *hashFile, FILE *articles должны быть доступны в следующих частях программы. При выполнении возможны два условия (две ситуации): файлы не существовали и их необходимо создать заново и инициализировать; файлы уже созданы при предыдущих исполнениях программы – их нужно открыть и прочитать из них данные об их состояниях. Прежде чем проектировать названный алгоритм подготовки файлов, следует разработать структуру файлов.

550