Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Полный+текст.doc
Скачиваний:
0
Добавлен:
01.04.2025
Размер:
245.76 Кб
Скачать

2.4 Односвязные списки

Односвязные или однонаправленные списки строятся с помощью структуры данных следующего вида

struct spis { char name[20];

spis *next;};

Д

*begin

анная структура может быть использована для создания списка фамилий. На рис.5 предсавлен однонаправленный связный список

name[20]

name[20]

name[20]

next

next

next

NULL

Рис.5

Поле next служит для связи элементов списка между собой в порядке следования, причем значение поля next последнего элемента списка равно NULL. Адрес начального элемента списка содержится в указателе begin.

Ниже представлена функция создания списка из n фамилий.

Void create_stek(spis **begin, int n)

{

spis *q;

char fam[20];

*begin = NULL;

for(int i = 0;i<n;i++)

{ cout<<”Введите фамилию: ”;

cin << fam;

q = new(spis)//выделение памяти под узел списка

strcpy(q->name,fam);

q->next = *begin;

*begin = q;}

}

Список, создаваемый функцией create_stek(), называется стеком. В нем реализован принцип LIFO – последним пришел – первым ушел (last in – first out). Действительно, просмотр этого списка осуществляется с последнего добавленного элементы с использованием следующей функции:

void look_stek(spis **p)

{

while(*p!=NULL)

{

cout << (*p)->name<<’\n’;//печать информационного поля узла

p = (*p)->next;

}}

Другой однонаправленный список – очередь реализует принцип FIFO -«первым пришел – первым ушел». Для создания этого списка необходимо хранить два указателя – на начало списка и на его конец. Элемент добавляется в начало списка, а просмотр производится с его конца (см. Рис.6). Ниже представлена функция для создания списка типа очередь из n элемнтов.

void create_quar(spis **begin,spis **end,int n)

{

spis *q;

char fam[20];

cout<<”Введите 1-ю фамилию ”;

cin>>fam;

q = new(spis);//выделяется память под 1-й элемент списка

strcpy(q->name,fam);

q->next = NULL;

*begin = *end = q;//создан 1-й элемент списка

for(int i=1;i<n;i++)

{

q = new(spis) );//выделяется память под i-й элемент списка

cout<<”Введите фамилию ”;

cin>>fam;

strcpy(q->name,fam);

q->next = NULL;

(*begin)->next=q;

*begin = q;

}

*begin

*end

}

name[20]

name[20]

name[20]

next

next

next

NULL

Рис.6

3. Двоичные деревья.

Двоичное дерево можно определить следующим образом:

  • имеется набор вершин, соединенных стрелками;

  • из каждой вершины выходит не более двух ветвей ;

  • существует только одна вершина, в которую не входит ни одна из ветвей, и она называется корнем дерева. Остальные вершины называются узлами;

  • в каждый узел входит только одна ветвь.

На рисунке 7 схематично показано двоичное дерево.

Рис.7

Двоичное дерево может быть определено рекурсивно, в терминах самого дерева.

Двоичное (бинарное) дерево - это конечное множество узлов, которое либо пусто, либо состоит из корня и двух непересекающихся двоичных деревьев, называемых левым и правым поддеревьями данного корня. Левая ветвь, исходящая из произвольного узла, ведет в корень левого поддерева ( если оно не пусто ), а правая ветвь - в корень правого поддерева этого узла.

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

При работе с бинарным деревом необходимо уметь его создавать. В качестве примера рассмотрим случайную последовательность чисел: 7, 3, 9, 8, 10, 11, 5, 4, 6, 3, 2, 1. Данной последовательности будет соответствовать бинарное дерево, показанное на рис.8.

Данное дерево является сбалансированным, т.е. числа равномерно распределены по ветвям дерева. При поиске в сбалансированном дереве просматривается не более log 2 N вершин дерева (N – число вершин дерева). Однако, если числа будут поступать не в случайном порядке, а ,например, в порядке убывания, то получится не сбалансированное дерево – числа будут «попадать» только в одну – левую ветвь дерева. В результате бинарное дерево «выродится» в однонаправленный список. Время поиска в таком – не сбалансированном – дереве будет таким же, как в однонаправленном списке, т.е. в среднем равным N/2 вершин. Существуют способы балансировки деревьев, но они являются трудоемкими и здесь не рассматриваются.

Двоичные деревья в программах создаются с помощью связных списков, которые объявляются следующим образом:

struct bintree_st

{ char data;

bintree_st *left;

bintree_st *right;

};

Структура содержит две ссылки: на левый от узла элемент списка и на правый элемент от этого же узла.

На рисунке 9 показан связный список в виде бинарного дерева. Как показано на рисунке, все указатели, как left, так и right, которые ни на что не указывают должны иметь значение NULL. Об этом необходимо помнить при написании программы, создающей подобный связный список.

Программа Binary_Tree позволяет создать бинарное дерево и выводит на экран самую левую и самую правую ветви.

include<stdio.h>

include<conio.h>

include<stdlib.h>

//объявление структуры бинарного дeрева

struct bintree_st

{ char data;

bintree_st *left;

bintree_st *right;

}

//функция, добавляющая узел к дереву

void AddTree( int data_key, bintree_st *First, bintree_st *New )

{//локальные переменные: p указывает на текущий узел, q – на предыдущий

bintree_st *p = First, *q;

//переменная - для исключения записи в дерево повторяющихся значений

int logical = 0;

if ( First != NULL ) {

do {

if ( p->data = = data_key ) {

logical = 1; // значение найдено

printf(“Значение повторяется \n”);

}

else {// переход на следующий узел

q = p; //сохранение указателя на предыдущий узел

if ( data_key < p->data )

p = p -> left;

else

p = p -> right;

}

} while (( logical != 1) && (p != NULL));

}//if

else printf(“ Null tree\n”);

//добавление нового элемента

if ( logical != 1 ) {

if ( data_key < q -> data )

q->left = New;

else

q->right = New;

}

return;

}//AddTree

int main(void)

{ // объявление указателей

bintree_st *First, *Next, *New;

int ch, data_key;

// создание корня дерева – указатель First

if ((First = (bintree_st *)malloc(sizeof( bintree_st))) = = NULL)

{ printf(“Not enough memory to allocate buffer\n”);

exit(1); //завершение программы из-за отсутствия памяти }

//обнуление указателей нового узла

First -> left = First ->right = NULL;

printf(“ Введите первое значение – “);

scanf(“%d”, &(First -> data ));

//для надежности работы программы не изменяем указатель First, а присваиваем

// значение указателя First указателю Next и функциям передаем Next

Next = First;

do {

printf(“Введите значение для записи в бинарное дерево – “);

scanf(“%d”, &data_key );

if ((New = ( bintree_st *) malloc ( sizeof ( bintree_st )))==NULL)

{ printf(“Not enough memory to allocate buffer\n”);

exit(1); //завершение программы из-за отсутствия памяти }

New -> left = New -> right = NULL;

AddTree( data_key, Next, New); //вызов функции добавления узла

printf(“Continue – press – y\n”);

} while ((ch = getch()) == ‘y’ );

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

//вывод самого левого поддерева бинарного дерева

printf(“ Левое поддерево\n”);

Next = First;

do {

printf(“%d” , Next-> data);

Next = Next -> left;

} while ( Next != NULL );

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

//вывод самого правого поддерева бинарного дерева

printf(“ Правое поддерево\n”);

Next = First;

do {

printf(“ %d”, Next -> data );

Next = Next -> right;

} while ( Next != NULL );

return 0;

}//main

Для вывода всего бинарного дерева необходимо использовать более сложный прием.

Корень и каждый промежуточный узел открывают два пути для обхода. Если обходить дерево от корня по левым ссылкам до ссылки, равной NULL ( см. предыдущую программу ), то будут выведены узлы только одной самой левой ветви дерева. Обход всего дерева можно выполнить, если запоминать указатели на узлы, в которых произошло разделение на две ветви. Это необходимо для того, чтобы по завершении вывода одной ветви поддерева перейти к выводу второй ветви поддерева.

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

Вывод на экран всего дерева можно выполнить с помощью следующих четырех функций, которые назовем Stack_Push, Stack_Pop, Throw_Left и AllTree, которая вызывает функции Throw_Left и Stack_Pop. Функции используют те же библиотечные функции, что и предыдущая программа. Текст перечисленных функций и вызов AllTree из функции main приведен ниже ( в тексте не показано: подключение библиотек и объявление структуры бинарного дерева).

//объявление указателя на узел бинарного дерева

struct bintree_st *F;

//объявление структуры для стека

struct stack

{

bintree_st *point; //указатель на узел дерева

stack *next;

}*top; //указатель на вершину стека

//функция записи в стек

void Stack_Push(void)

{

stack *elem;

//создание нового элемента стека

if ((elem = (stack*) malloc(sizeof( stack ))) == NULL)

{ printf(“Not enough memory to allocate buffer\n”);

exit(1); //завершение программы из-за отсутствия памяти }

elem ->point = F;

elem ->next = top;

top = elem;

return;

}; //Stack_Push

void Stack_Pop(void)

{

F = top -> point;

top = top -> next;

return;

}; //Stack_Pop

void Throw_Left(void)

{

while ( F != NULL ) { Stack_Push();

F = F -> left; }

return;

}; //Trow_Left

void AllTree(void)

{ // создание указателя на вершину стека

if (( top = (stack*) malloc (sizeof( stack ))) == NULL)

{ printf(“Not enough memory to allocate buffer\n”);

exit(1); //завершение программы из-за отсутствия памяти }

top -> next = NULL;

printf(“Вывод бинарного дерева, начиная с самого левого узла\n”);

while ( top != NULL )

{

Trow_Left();

Stack_Pop();

if ( top != NULL ) printf( “ % d\n”, F -> data);

F = F -> right; // переход на следующий правый узел

}

return;

}; // AllTree

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

int main ( void )

{

. . .

F = First; //присваиваем переменной F значение указателя First на корень дерева

AllTree(); //далее все функции работают с указателем F

return 0;

}//main

Использование рекурсии для написания функций, решающих те же задачи, что и функции AddTree() и AllTree(), позволяет получить очень компактный код. Успешное применение рекурсии при работе с бинарными деревьями обусловлено рекурсивным определением бинарного дерева. Однако, использование рекурсии чревато появлением ряда проблем. Одной из основных проблем, сопровождающих рекурсию, является не достаточный размер стека и его переполнение, что при неумелом использовании стека может привести к потере результата. Функция Add_Knot рекурсивно добавляет узел в дерево.

struct bintree_st *Add_Knot( bintree_st *New, int data_key )

{

if ( New == NULL ) //значение встречается впервые

{ //создается новый узел

if ((New = (bintree_st *) malloc (sizeof( bintree_st)))==NULL)

{ printf(“Not enough memory to allocate buffer\n”);

exit(1); }

New -> data = data_key;

New -> left = New -> right = NULL;

}

else if ( New -> data = = data_key ) printf(“Значение повторяется\n”);

else if ( New -> data > data_key )

New -> left = Add_Knot( New -> left, data_key ); //рекурсивный вызов

else

New -> right = Add_Knot( New -> right, data_key); //рекурсивный вызов

return New;

}

Вызов функции Add_Knod выполняется из main() следующим образом:

int main( void )

{

bintree_st *root = NULL;

. . .

root = Add_Knot( root, data_key );

return 0;

}

Функция Tree_Print выводит на экран все данные из узлов бинарного дерева, упорядоченными по возрастанию.

void Tree_Print( struct bintree_st *F )

{

if ( F != NULL ) {

Tree_Print( F -> left ); //рекурсивный вызов

printf(“%6d”, F -> data );

Tree_Print( F -> right ); //рекурсивный вызов

}

return;

}

Функции передается указатель на корень дерева.

int main( void )

{

bintree_st *root = NULL;

. . .

root = Add_Knot( root, data_key );

Tree_Print(root);

return 0;

}

При удалении элемента из дерева следует рассмотреть несколько вариантов. Если элемент последний или из него выходит только одна ветвь, то удаление не представляет сложности. На рисунке 10 удаляемый узел заштрихован.

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

На рисунке 11 схематично показано исключение узла с ключом 49:

101 101

19 123 19 123

15 49 125 15 35 125

30 54 30 54

28 35 28 33

33

Рис.11

Процедура исключения узла из двоичного дерева различает три случая:

1) узла с заданным значением ключа нет;

2) узел с заданным ключом имеет не более одной ветви;

3) узел - имеет две ветви.

Н.Виртом предложен рекурсивный алгоритм удаления узла бинарного дерева. При реализации алгоритма, учитывались особенности языка С, в частности особенности работы с указателями. Здесь приведен текст основной функции удаления узла из бинарного дерева DelTreeKnod и вспомогательной рекурсивной функции Del_Knod.

bintree_st *q; //дополнительная переменная

bintree_st *Del_Knod ( bintree_st *Del )

{

if ( Del->right = = NULL ) {

q->data = Del->data;

q = Del;

Del = Del->left;

}

else Del->right = Del_Knod( Del->right ); //рекурсивный вызов

return Del;

}

//основная рекурсивная функция удаления

bintree_st *DelTreeKnod ( bintree_st *Next, int key )

{

if ( Next = = NULL )

printf(" Значение отсутствует \n");

else

{

if ( key = = Next->data )

{ //второй случай

q=Next;

if ( q->right = = NULL ) Next = q->left;

else if (q->left = = NULL) Next = q->right;

else Next->left = Del_Knod ( Next->left) ; //третий случай

}

else if ( key > Next->data )

Next -> left = DelTreeKnod ( Next -> left,key );

else

Next -> right = DelTreeKnod ( Next -> right,key );

}

return Next;

}

int main(void)

{

bintree_st *Next, *root = NULL;

int ch, data_key;

do

{

Next = q = root; //указатель на корень дерева

printf("Введите значение для удаления: ");

scanf( "%d", &data_key );

Next = DelTreeKnod(Next, data_key); //вызов функции для удаления узла

Next = root; //указатель на корень дерева

Tree_Print(Next);

printf( "Continue – press “y”\n" );

} while ((ch = getch()) = = 'y');

return 0;

}//main

Вспомогательная рекурсивная функция Del_Knod вызывается в третьем случае. Она “cспускается” вниз по правой ветви левого поддерева удаляемого элемента q, а затем заменяет значение поля data. После этого узел, на который указывает переменная-указатель Del, можно удалить. Удаление выполняется присваиванием Del = Del -> left .

Не нужный элемент можно удалить из памяти при помощи стандартной функции free (). Функции передается указатель на удаляемый блок void free( void *block ).