
Л10_Сортировка файлов.pptx
- Количество слайдов: 47
Cортировка файлов Лекция 12
Файлы в Си (ANSI) Файлом называют способ хранения информации на физическом устройстве. Файл ― это понятие, которое применимо ко всему : от файла на диске до терминала. Файл представляется потоком байт. В языке Си отсутствуют операторы для работы с файлами. Все необходимые действия выполняются с помощью функций, включенных в стандартную библиотеку
Текстовые и бинарные файлы Текстовый файл — это файл, содержащий текст, разбитый на строки при помощи некоторого разделяющего символа окончания строки или последовательности в Unix — одиночный символ перевода строки; в Microsoft Windows за символом перевода строки следует знак возврата каретки: 0 x 0 D 0 x 0 A 13 10 в десятичной системе счисления. Двоичный (бинарный) файл — файл, из которого байты считываются и выводятся в «сыром» виде без какого-либо связывания (подстановки). В текстовом файле символ "n" переводится в "rn" при записи в файл. При считывании производится обратная замена: "rn" "n". С бинарными файлами этого не происходит.
Описание файла Логическое имя представляет собой указатель на файл, который используется операционной системой для поддержки операций с этим файлом. Оно определяется так: FILE *fp; FILE ― имя типа, описанное в стандартном заголовочном файле
Библиотечные функции, используемые при работе с файлами. Функция открытия файла fopen( ) FILE * fopen (const char* name, const char* mode ); (спецификация файла, способ использования файла) В случае удачного открытия файла, функция fopen() возвращает дескриптор файла, иначе – константу NULL, которая определена в файле
Способ использования файла rw - а - r+ - w+ - a+ - rb - wb аb - r+b w+b а+b - открыть существующий файл для чтения; создать новый файл для записи (если файл с указанным именем существует, то он будет переписан); дополнить файл (открыть существующий файл для записи информации, начиная с конца файла, или создать файл, если он не существует); открыть существующий файл для чтения и записи; создать новый файл для чтения и записи; дополнить или создать файл с возможностью чтения и записи; открыть двоичный файл для чтения; создать двоичный файл для записи; дополнить двоичный файл; открыть двоичный файл для чтения и записи; создать двоичный файл для чтения и записи; дополнить двоичный файл с предоставлением возможности чтения и записи
Способ использования файла rt wt at - r+t - w+t - a+t - открыть текстовой файл для чтения; создать текстовый файл для записи; дополнить текстовый файл; открыть текстовой файл для чтения и записи; создать текстовый файл для чтения и записи; дополнить текстовый файл с предоставлением возможности записи и чтения. Строки вида r+b можно записывать и в другой форме: rb+ Если режим t или b не задан, то он определяется значением глобальной переменной _fmode.
Функция fclose() После окончания работы с файлом он должен быть закрыт. Это делается с помощью библиотечной функции fclose( ). Прототип: int fclose(FILE *fp); При успешном завершении операции функция fclose( ) возвращает значение ноль. Любое другое значение свидетельствует об ошибке.
Буферизация Функции fopen( ) и fclose( ) работают с файлами с "буферизацией". Под буферизацией мы понимаем, что вводимые и выводимые данные запоминаются во временной области памяти, называемой буфером. Если буфер заполнился, содержимое его передается в файл (или затирается), и процесс буферизации начинается снова. Одна из основных задач fclose( ) заключается в том, чтобы освободить любые частично заполненные буферы при закрытии файла.
Функция fflush() Если заданный файл открыт для вывода, то содержимое буфера, записывается в него. Если файл открыт для ввода, то функция fflush очищает содержимое буфера. После вызова функции файл остается открытым. Прототип: int fflush(FILE *fp); Функция fflush возвращает 0, если буфер успешно обновлен. Это же значение возвращается, когда файл открыт только для чтения. В случае возникновения ошибки возвращается значение EOF ( 1). Буферы автоматически обновляются, когда они полны, когда файл закрывается или произошло нормальное окончание работы программы без закрытия файла.
Функция fflush() Например, способ освобождения от нежелательных символов во входном файле: printf("Введите возраст"); scanf("%d", &age); /*получение возраста*/ printf("Введите размер обуви: "); fflush(stdin); scanf("%d", &shoesize); Отмена буферизации канала stdout: setbuf(stdout, NULL); Канал вывода сообщений об ошибках stderr не буферизован, поэтому выдаваемые в него сообщения печатаются немедленно.
Функция fprintf( ) выполняет те же действия, что и функция printf( ), но работает с файлом. Прототип: int fprintf(FILE *fp, const char *format, . . . ); Возвращаемое значение равно количеству реально выведенных символов. Если при выводе возникла ошибка, возвращается отрицательное число. Пример: fprintf(fp, "%х", а);
Функция fscanf( ) выполняет те же действия, что и функция scanf(), но работает с файлом. Прототип: int fscanf(FILE *fp, char *format, . . . ); Возвращает количество считанных параметров. При попытке считывания конца файла возвращается значение EOF. Пример: fscanf(fp, "a = %х", &a);
Пример #include
Функция feof( ) определяет конец файла при чтении двоичных данных. Прототип: int feof(FILE *fp); fp - указатель на файл, возвращенный функцией fopen( ). При достижении конца файла возвращается ненулевое значение, в противном случае возвращается 0. while (!feof(f)) fscanf (f, “%d”, &x); Если в конце последовательности стоит пробел, то конец файла еще не достигнут, но очередное число прочитать невозможно.
Функция fputc() Функция fputc( ) записывает символ в файл. Прототип: int fputc(int с, FILE *fp); fp - указатель на файл, возвращенный функцией fopen(), с - символ. При успешном завершении fputc() возвращает записанный символ, в противном случае возвращается константа EOF. int ch; . . fputc(ch, fo);
Функция fgetc() Прототип: int fgetc(FILE *fp); fp - указатель на файл, возвращенный функцией fopen( ). Эта функция возвращает прочитанный из файла символ. Соответствующее значение имеет тип int, но старший байт равен нулю. Если достигнут конец файла, то fgetc( ) возвращает значение ЕОF. int ch; . . . ch=fgetc(fi); читает символ из файла, на который указывает fi
Функция fputs( ) записывает строку символов в файл. Она отличается от функции puts( ) только тем, что в качестве второго параметра должен быть записан указатель на переменную файлового типа. Символ конца строки (‘ ') не записывается. Прототип: int fputs(const char *str, FILE *fo); Например: l = fputs("Ехаmple", fo); При успешном выполнении функция fputs() возвращает неотрицательное значение (последний записанный символ), а при неудачном — значение EOF. В отличие от puts функция fputs( ) не добавляет в конец строки символ перехода на новую строку.
Функция fgets( ) читает строку символов из файла. Она отличается от функции gets( ) тем, что имеет три параметра, третий - указатель на переменную файлового типа. Прототип: char *fgets(char *str, int n, FILE *fi); Строка считывается целиком, если ее длина не превышает указанного числа символов, в противном случае функция возвращает только заданное число символов. Пример: … fgets(string, n, fi); Функция возвращает указатель на строку string при успешном завершении и константу NULL в случае ошибки либо достижения конца файла.
Пример /* Программа считывает файл строка за строкой */ #include
Функция remove( ) удаляет файл. Прототип: int remove(const char *file_name); file_name - указатель на строку со спецификацией файла. При успешном завершении возвращается ноль, в противном случае возвращается ненулевое значение.
Функция rewind( ) устанавливает указатель текущей позиции в начало файла. Прототип: void rewind(FILE *fp);
Функция fseek( ) позволяет выполнять чтение и запись с произвольным доступом. Прототип: int fseek(FILE *fp, long count, int access); fp - указатель на файл, возвращенный функцией fopen( ), count - номер байта относительно заданной начальной позиции, начиная с которого будет выполняться операция, access - способ задания начальной позиции. Переменная access может принимать следующие значения: SEEK-SET (0) - начальная позиция задана в начале файла; SEEK-CUR (1) - начальная позиция считается текущей; SEEK-END (2) - начальная позиция задана в конце файла. При успешном завершении возвращается ноль, при ошибке - ненулевое значение Вызов fseek(fp, OL, 0) означает, что мы идем в файл, на который ссылается указатель fp, и находим байт, отстоящий на 0 байт от начала, т. е. первый байт.
Функция fread( ) предназначена для чтения блоков данных из потока. Прототип: unsigned fread(void *ptr, unsigned size, unsigned n, FILE * fp); Она читает n элементов данных, длиной size байт каждый, в блок памяти, на который указывает указатель ptr, Общее число прочитанных байтов равно произведению n*size. При успешном завершении функция fread( ) возвращает число прочитанных элементов данных, при ошибке - 0.
Функция fwrite( ) предназначена для записи в файл блоков данных. Прототип: unsigned fwrite(void *ptr, unsigned size, unsigned n, FILE *fp); Она добавляет n элементов данных, длиной size байт каждый, в заданный выходной файл fp. При успешном завершении операции функция fwrite() возвращает число записанных элементов данных, при ошибке ― неверное число элементов данных.
Пять стандартных файлов stdin - для ввода данных из стандартного входного потока (по умолчанию - c клавиатуры); stdout - для вывода данных в стандартный выходной поток (по умолчанию - на экран дисплея); stderr - файл для вывода сообщений об ошибках (всегда связан с экраном дисплея); stdprn - для вывода данных на принтер; stdaus - для ввода и вывода данных в коммуникационный канал.
Слияние последовательностей Под слиянием будем понимать объединение двух или более упорядоченных последовательностей в одну упорядоченную. Это можно сделать следующим образом: сравнить наименьшие элементы из упорядоченных последовательностей и наименьший из них перенести в готовую последовательность. Далее снова сравнить начала последовательностей и наименьший из этих элементов добавить в готовую последовательность и т. д. Как только одна из последовательностей закончится, она исключается из рассмотрения. Когда остается только одна последовательность, ее «хвост» можно просто переместить в готовую.
Объединим две последовательности в третью (позиция считывания отмечена чертой) 8 38 40 51 75 1 15 63 89 101 107 Сравним первые элементы отсортированных последовательностей, наименьший из них запишем в выходную последовательность: 8 38 40 51 75 1 15 63 89 101 107 1 Следующий шаг 8 38 40 51 75 1 15 63 89 101 107 1 8
Этот процесс продолжится до тех пор, пока все элементы первой и второй последовательности не будут переписаны в третью в заданном порядке. В результате получим отсортированную по возрастанию последовательность: 1 8 15 38 40 51 63 75 89 101 107
Метод слияния — один из самых первых методов, который естественным образом можно применить к сортировке файлов, а именно два отсортированных файла слить в третий отсортированный. Данный метод слияния был предложен фон Нейманом в 1945 г. и предназначался именно для сортировки файлов.
Джон фон Нейман англ. John von Neumann Джон фон Нейман в 1940 -е Дата рождения: 28 декабря 1903 Место рождения: Будапешт Дата смерти: 8 февраля 1957 (53 года) Место смерти: Вашингтон Научная сфера: математик, физик
Сортировка массива простым двухпутевым слиянием Идея метода сортировки слиянием такова: разделим входную последовательность на две части, отсортируем каждую из них по отдельности, результаты сольем, как описано выше. Исходная задача сводится к двум аналогичным задачам с меньшим объемом данных, применим рекурсию: — на фазе рекурсивного спуска каждая из образующихся последовательностей делится на две части до тех пор, пока не образуются последовательности длины 0 или 1, которые сортировать не надо; — на фазе возврата из рекурсии пары уже отсортированных подпоследовательностей сливаются.
Пример 13 86 71 52 99 21 37 45 66 4 75 80 31 4 разделение 3 разделение 2 разделение 1 разделение
Слияние элементарных последовательностей начинается из глубины рекурсии и дает следующие подпоследовательности: 86 71 99 21 45 71 86 54 21 99 37 46 4 75 80 31 слияние 4 ур: 13 слияние 3 ур: 13 71 86 21 54 99 45 66 4 75 37 45 66 4 31 31 75 80 80 слияние 2 ур: 13 21 54 71 86 99 4 31 37 45 66 75 80 слияние 1 ур: 4 13 21 31 37 45 54 66 71 75 80 86 99
Следующая пара функций реализует сортировку слиянием для массивов: void merge (key al[], int lenl, key a 2[], int len 2, key ar[]) /* Слияние отсортированных массивов al длины lenl и а 2 длины len 2 в массив аr */ { int i=0, j=0, k=0; key x; while ((i
static key aw[N]; /* вспомогательный глобальный массив для слияния */ void sort_merging (key a[], int L, int R) /* L, R - границы сортируемой части массива а */ { int i, M; М = (L+R)/2; if (L < M) sort_merging(a, L, M); if(M+l < R) sort_merging(a, M+l, R); /* слияние частей в aw */ merge(&a[L], M-L+l, &a[M+l], R-M, &aw[L]); /* копирование в исход. фрагмент */ for (i=L; i<=R; i++) a[i] = aw[i]; }`
Анализ Для слияния двух отсортированных частей необходим третий массиврезультат aw. Однако, поскольку упорядоченные данные должны накапливаться для последующего слияния в исходном массиве, приходится дополнительно переписывать результат на место исходной подпоследовательности. Нам было бы достаточно иметь в качестве aw локальный рабочий массив длины R – L + 1, но в Си невозможно описать массив переменной длины (без обращения к более медленным средствам динамической памяти). Введение же локального массива максимальной длины N, используемого лишь частично привело бы к затратам памяти до N log 2 N записей, так как на каждом из log 2 N уровней рекурсии в памяти хранился бы отдельный рабочий массив длины N.
Использование глобального массива приводит к оценке затрат памяти в данном методе ~ 2 N записей и в данном случае организовано корректно: ни один элемент массива aw, записанный в функции слияния, не может быть изменен до переписи его в массив а в функции сортировки, а после переписи он становится не нужен.
Сортировка файла простым двухпутевым слиянием Пусть теперь вместо массива а дан файл f, который нужно отсортировать. Заметим, что в функции слияния merge доступ к элементам частей массива и к массиву-результату исключительно последовательный: индексы-указатели текущего доступа сдвигаются только на единицу вперед, без возвратов и скачков. Поэтому операции вида a[i + +] для массива можно заменить на типовые операции чтения и записи элемента файла с продвижением к позиции следующего элемента.
При разделении массива нам не приходилось явно отводить память под образуемые части и переписывать в них элементы. Вместо этого мы устанавливали и перемещали два указателя. Однако файл читать можно только по одному указателю, поэтому разделяемые части придется явно переписывать в отдельные файлы. Таким образом, нужна процедура split, выполняющая физическое разделение.
Для разделения массива пополам мы пользовались знанием его длины. Для файла число его записей не всегда известно и определение длины требует дополнительного холостого считывания. Это препятствие мы устраним так: поскольку разделяются еще неотсортированные файлы, разделение можно организовать подобно тому, как сдается колода карт на двух игроков: элементы разделяемого файла по мере считывания переписываются в два новых файла поочередно. Концом «раздачи» является достижение конца входного файла, при этом количество элементов в новых файлах отличается максимум на единицу, что и требуется.
1 разделение 2 разделение 3 разделение 4 разделение 13 86 71 52 99 21 37 45 66 4 75 80 31 13 71 99 37 66 75 31 86 52 21 45 4 80 13 99 66 31 71 37 75 86 21 4 52 45 80 13 66 99 31 71 75 37 86 4 21 52 80 45
Следующие процедуры реализуют все описанные модификации. Мы пользуемся стандартными файловыми функциями библиотеки Си, в том числе средствами создания промежуточных рабочих файлов, для которых не нужно беспокоиться о выборе уникальных имен. /* упрощенные вызовы файловых функций С */ #define fget(f, x) fread(&x, sizeof(x), 1, f) #define fput(f, x) fwrite(&x, sizeof(x), 1, f)
int split (FILE *f, FILE *fl, FILE * f 2) /* Разделение f: перепись элементов нечетных позиций в f 1, четных - в f 2 */ { key x; int n=0; /* счетчик длины файла */ rewind (f); /* возврат к началу разделяемого файла */ fget (f, x); while (!feof(f)) { /* (feof срабатывает ПОСЛЕ попытки чтения!) */ fput (f 1, х); fget (f, x); if (!feof(f)) { fput (f 2, x); fget (f, x); } n++; } return n>l; /* 0 (длина 0 или 1) сигнализирует о прекращении разделения */ }
void merge (FILE *fl, FILE *f 2, FILE *fr) /* Слияние fl и f 2 в fr */ { key xl, x 2; rewind(f 1); /* перемотка к началу всех файлов */ rewind(f 2); rewind(fr); fget(fl, xl); fget(f 2, x 2); while (!feof(fl) || !feof(f 2)) { if (feof(fl)) { fput(fr, x 2); fget(f 2, x 2); } else if (feof(f 2)) { fput(fr, xl); fget(fl, xl); } else if (xl
void sort_merge (FILE *f) /* Главная процедура сортировки, входной файл должен быть открыт */ { /* создание временных файлов для частей */ FILE *fl = tmpfile(), *f 2 = tmpfile(); /* разделение на фазе спуска в рекурсию */ if (split(f, fl, f 2)) { sort_merge(f 1); sort_merge(f 2); } /* слияние на фазе возврата из рекурсии */ merge(f 1, f 2, f); /* закрытие и удаление рабочих файлов */ fclose(f 1); fclose(f 2); }
int main () { int key x; FILE * f = fopen("inputfile", "r+b"); /* открытие файла */ sort_merge(f); /* сортировка открытого файла */ fclose(f); /* закрытие выходного файла */ return 0; }