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

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

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

У функции addWord() два параметра: tree – указатель на корень дерева, word – указатель на строку с добавляемым словом. Алгоритм функции addWord() неформально мы уже описали выше. В ее тексте принято, что функция должна выполняться только для уже существующего дерева. Создание дерева, т.е. включение в пустое дерево первого элемента, тривиально и сведется к присваиванию указателю на корень дерева (например, root) значения, возвращаемого функцией newNode():

root=newNodde(newWord);

Алгоритм функции addWord():

Цикл перебора вершин дерева Сравнить добавляемое слово со словом вершины Если слова совпали

Увеличить счетчик слова в вершине Освободить память добавляемого слова Закончить обработку

Если добавляемое слово меньше слова из вершины (выше по алфавиту)

Если левая ссылка вершины равна NULL

Создать новую структуру с добавляемым словом Присвоить левой ссылке вершины адрес структуры Закончить обработку

Настроиться на обработку левого поддерева Иначе (добавляемое слово ниже по алфавиту)

Если правая ссылка вершины равна NULL

Создать новую структуру с добавляемым словом Присвоить левой ссылке вершины адрес структуры Закончить обработку

Настроиться на обработку правого поддерева Все-если

Конец-цикла перебора вершин

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

501

Если указатель на вершину равен NULL Завершить выполнение

Выполнить алгоритм для левой ветви Напечатать информацию из вершины Выполнить алгоритм для правой ветви

Напишем для выполнения обхода дерева и печати информации соответствующую функцию:

/* printTree.c - обход дерева и печать слов */ void printTree(Node * tree)

{

if (tree == NULL) return;

printTree(tree -> left);

printf("\n%s - %d", tree -> nWord, tree -> counter);

printTree(tree -> right);

}

Перечисленные функции позволяют решить поставленную задачу следующим образом:

/* 11_16.c - упорядочить слова из входного потока по алфавиту */

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

/* Структурный тип "вершина двоичного дерева": */ struct node {

int counter;

struct node * left; struct node * right; char * nWord;

};

typedef struct node Node;

/* Функция для чтения слова. */ #include "getWord.c"

/* Формирование новой структуры (вершины дерева) */

502

#include "newNode.c"

/* Добавление слова (новая вершина или учет) */ #include "addWord.c"

/* Обход дерева и печать слов */ #include "printTree.c"

#define LEN_AR 400 int main()

{

Node * root = NULL; char * newWord;

puts("Input text (EOF - end of run):"); while(1)

{newWord = getWord();

if (newWord == NULL) break; if (root == NULL)

root = newNode(newWord);

else

addWord(root, newWord);

}

printTree(root); return 0;

}

Запуск программы на исполнение с анализом текста файла printTree.c:

C:\...>test<printTree.c>res

Результат выполнения программы:

Input text (EOF - end of run):

c - 1 counter - 1 d - 1

if - 1 left - 1 n - 2 ode - 1 ord - 1

print - 4

503

printf - 1 ree - 4 return - 1 right - 1 s - 1

tree - 6 void - 1

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

В основной программе, т.е. в функции main(), определены два указателя root – на структуры-вершины и newWord – на читаемые из входного потока слова. Чтение слов происходит в бесконечном цикле, который завершается оператором break по достижении конца файла, на который "назначен" стандартный входной поток.

Указателю root присваивается адрес вершины, сформированной для первого введенного слова (когда root==NULL). Для последующих слов вызывается функция addWord(), формирующая дерево с корнем root. По завершении чтения вызывается функция печати printTree(), которой передается адрес корня дерева.

Отметим теоретически важный факт. Сбалансированность формируемого дерева существенно зависит от распределения (относительно алфавитного порядка) слов из входного потока. Наиболее неблагоприятный случай – чтение уже отсортированного списка слов. Наиболее удачный – равномерное размещение вводимых слов по алфавиту. Подробнее с этими вопросами читатель может познакомиться по книге Б. Кернигана и Р. Пайка [4], а также по фундаментальной монографии Д. Кнута [12].

ЗАДАНИЕ. Перепишите функцию addWord() таким образом, чтобы она реализовала рекурсивный алгоритм вставки слова в дерево.

Возможное решение (см. также программу 11_16_2.с):

504

/* addWordR.c – рекурсивная функция включения слова (новая вершина или учет) */

void addWordR(Node * tree, char * word)

{

int comp;

if (tree == NULL)

{ puts("The tree is empty!"); exit(1);

}

comp = strcmp(word, tree -> nWord); if (comp == 0)

{tree -> counter++; free(word); return;

}

if (comp < 0)

{if (tree -> left == NULL)

{tree -> left = newNode(word);

return;

}

else addWordR(tree -> left, word);

}

else

{if (tree -> right == NULL)

{tree -> right = newNode(word); return;

}

else addWordR(tree -> right, word);

}

}

Функция addWordR() специально написана с минимальными изменениями по сравнению с нерекурсивным вариантом. В ней отсутствует цикл, а используются рекурсивные вызовы "ее самой" для левого или правого поддеревьев в зависимости от результата сравнения слов. В функции предусмотрено четыре выхода из последовательности рекурсивных вызовов:

!аварийный выход при пустом дереве;

!выход после увеличения счетчика найденного слова;

!выход после подключения нового узла в левую ветвь;

!выход после подключения нового узла в правую ветвь.

505

Коротко о важном

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

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

(11_01.с).

!Имея в программе структурный тип, невозможно оперировать с его элементами, не определив конкретную структуру.

!Имя структурного типа обязательно включает служебное слово struct (11_01.c, 11_02.с и др.).

!Чтобы вычислить размер памяти, выделяемой для структуры, операцию sizeof применяют к имени структуры или к имени структурного типа (11_01.с, 11_02_1.с, 11_04.с).

!Невозможно вывести или ввести значения элементов структуры, не обращаясь непосредственно к ее элементам, например, используя только ее имя (11_01_2.с).

!Структуры можно определять одновременно с определением структурного типа. При этом структурный тип может быть введен как безымянный (11_01_1.с).

!Инициализация элементов структур выполняется всегда подряд, начиная с первого элемента (11_02.с).

!Структуры одного типа можно присваивать друг другу одной операцией. При этом значения элементов копируются (11_02.с,

11_03_1.с).

!Структура, элемент которой адресует строку, никогда не включает эту строку. При присваивании таких структур копируется только адрес, но адресуемые их элементами строки "не размно-

жаются" (11_02_1.с).

!Структура с элементом-массивом включает этот массив. При присваивании таких структур элемент-массив копируется

(11_03.с).

!Структура, элемент которой адресует массив, не включает этот массив (11_04.с).

!При присваивании структур с элементами, адресующими некоторые объекты (массивы, строки, структуры...), копируются только значения элементов, а не адресуемых ими объектов

(11_04_1.с).

506

!Элементом структуры может быть другая структура (11_05.с).

!Имя структуры не зависит от значений ее элементов. Значение элемента может совпадать с именем структуры (11_05_1.с).

!Структуры одного типа могут быть объединены в один массив структур (11_06.с, 11_09.с).

!Зная адрес структуры, доступ к ее элементам можно получить с помощью операции "->" (11_07.c).

!Выражение "указатель -> элемент" эквивалентно выражению

(*указатель).элемент (11_07.с, 11_07_1.с).

!Указатель на объекты структурного типа незаменим при динамическом создании структур (11_07.с) и динамических массивов структур (11_08.с).

!При создании динамических массивов (не только массивов структур) необходимо "запоминать" их длину.

!В отличие от массива имя структуры, использованное в качестве параметра, не позволяет изменять значения элементов структуры операторам тела функции (11_10_4.с).

!Возвращаемым значением функции может быть структура (11_10.с, readTerm()). Копирование при таком возврате выполняется по правилам присваивания.

!Возвращаемым значением функции может быть указатель на структуру (11_11.с, analyse(), 11_15.c, cellCreate(), inList()). При этом необходимо, чтобы указатель адресовал доступную в точке вызова структуру.

!Битовые поля не существуют вне структур и объединений.

!Используя битовые поля, можно уменьшить количество разрядов, выделяемых по умолчанию компилятором для целочисленных значений (11_12.с).

!Набор битовых полей в структуре позволяет компактно разместить в ней ("упаковать") несколько целочисленных значений

(11_12.с).

!Нельзя ввести в битовое поле значение из входного потока (11_12.с), так как нельзя получить адрес битового поля.

!Включение в объединение структуры с битовыми полями и переменной позволяет получить доступ к любым разрядам внутреннего представления переменной (11_13.с, 11_14.с).

!Структура с элементом-указателем на структуру того же типа – основа построения таких динамических информационных конструкций, как связанные списки, стеки, деревья и др. (11_15.с,

11_16.с).

Тема 12

Работа с файлами

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

Б. Страуструп. Язык программирования С++

Основные вопросы темы

!Структурный тип "файловая структура" (FILE).

!Открытие файла для чтения в текстовом режиме.

!Изменение структуры типа FILE при открытии файла.

!Чтение информации из файла по строкам.

!Различие между именем файла и указателем типа FILE.

!Посимвольное чтение из файла и запись в файл.

!Копирование файлов в текстовом и двоичных режимах.

!"Конкатенация" файлов.

!Изменение режимов обработки файлов.

!Форматные "обмены" с файлами.

!Запись массива структур в файл и восстановление массива структур по данным из файла.

!Поиск в файле заданной последовательности (строки, слова).

!Применение функции feof().

!Сохранение в файле информации, представленной в программе структурой с элементами-указателями на данные.

!Информационные обмены между функциями программы с помощью глобальных объектов (переменных, массивов, структур) и файлов.

!Автономная трансляция функций программы и ее компоновка из объектных модулей.

!Взаимосвязи файлов в одной программе.

!Записи фиксированной длины в файле и обработка этих записей в произвольном порядке.

!Строки (в стиле Си) как записи файла.

!Хеширование – уникальный метод упорядочения данных.

!Организация связных списков записей файла.

!Нисходящее проектирование программ.

508

12.1.Основы работы с файлами

ЗАДАЧА 12-01. Напишите функцию для печати значений элементов файловой структуры, тип которой FILE предопределен в тексте заголовочного файла stdio.h. Используя функцию, выведите на экран сведения о файловых структурах, поддерживающих стандартные потоки stdin, stdout, stderr.

Известно, что перечисленные имена стандартных потоков вводавывода являются указателями на структуры типа FILE. Чтобы решить поставленную задачу, нужно знать компоненты (элементы) этих структур и типы компонентов.

ЗАДАНИЕ. Используя любой текстовый редактор, просмотрите текст файла stdio.h вашего компилятора. Найдите в файле определение структурного типа FILE и напечатайте его.

Для компилятора DJGPP определение структурного типа FILE таково:

/* FILE.txt - предопределенный структурный тип. */ typedef struct {

int _cnt; char *_ptr; char *_base; int _bufsiz; int _flag; int _file;

char *_name_to_remove; } FILE;

Набор элементов структур типа FILE не определен Стандартом языка Си и существенно зависит от среды реализации компилятора. Поэтому ограничимся только несложными экспериментами, чтобы показать, какие значения принимают элементы структуры типа FILE при ее "настройке" на те или иные файлы (т.е. при открытии файлов). При необходимости более глубокого изучения средств для управления файлами читатель может обратиться к специальной литературе. Например, в [15] подробно описаны особенности файловой системы

509

MS-DOS и средства Turbo С, позволяющие гибко управлять файлами.

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

/* printFSTR.c - функция структуры типа FILE */ #include <stdio.h>

#define PRINTI(EXPRESSION) \ printf(#EXPRESSION"=%d\n",EXPRESSION)

#define PRINTS(EXPRESSION) \ printf(#EXPRESSION"=%s\n",EXPRESSION)

void printFSTR(FILE * fstr)

{

if (fstr == NULL)

{ puts("File is not define."); return;

}

PRINTI(fstr -> _cnt);

PRINTS(fstr -> _ptr);

PRINTS(fstr -> _base); PRINTI(fstr -> _bufsiz); PRINTI(fstr -> _flag); PRINTI(fstr -> _file); PRINTS(fstr -> _name_to_remove);

}

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

/* 12_01.c - сведения о стандартных потоках ввода/вывода */

#include <stdio.h> #include "printFSTR.c"

510