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

C# для чайников

.pdf
Скачиваний:
186
Добавлен:
27.03.2015
Размер:
15.52 Mб
Скачать

Связанные списки идеально подходят для хранения последовательностей данных, особенно если программа не знает заранее их точное количество (тем не менее следует серьезно подумать о возможном применении обобщенного класса List<T>, который был описан в главе 15, "Обобщенное программирование". Если вам нужен именно свя­ занный список, можно воспользоваться встроенным связанным списком из С# 2.0, а не тем, который разрабатывается в данном разделе. Обратитесь к справочной системе за информацией о пространстве имен System. Collections .Generic).

Другие пространства имен коллекций, которыми вы можете захотеть воспользовать­ с я — System. Collections и System. Collections . Specialized. Поищите информацию о них в справочной системе, но в первую очередь следует искать подходя­ щую коллекцию именно в пространстве имен System. Collections . Generic.

Пример связанного списка

Приведенная далее демонстрационная программа иллюстрирует создание

ииспользование связанного списка.

//LinkedListContainer - демонстрация "самодельного"

//связанного списка. Этот контейнер реализует интерфейс

//IEnumerable для поддержки таких операторов, как foreach.

//Этот пример включает также итератор, который реализует

//интерфейс IEnumerator

using System;

using System.Collections;

namespace LinkedListContainer

{

//LLNode - каждый LLNode образует узел списка. Каждый

//узел LLNode содержит ссылку на целевые данные,

//встроенные в список

public class LLNode

{

//Это данные, которые хранятся в узле списка internal object linkedData = null;

//Указатели на следующий и предыдущий узлы в списке internal LLNode forward = null; // Следующий узел internal LLNode backward = null; // Предыдущий узел

internal LLNode(object linkedData)

{

this.linkedData = linkedData;

}

// Получение данных, хранящихся в узле public object Data

{

 

get

 

{

 

return

linkedData;

}

 

}

 

452

Часть VII. Дополнительные главы

}

II LinkedList - реализация дважды связанного списка public class LinkedList : IEnumerable

{

//Концы связанного списка. Спецификатор internal

//позволяет итераторам обращаться к ним непосредственно internal LLNode head = null; // Начало списка

internal LLNode tail = null; // Конец списка

public IEnumerator GetEnumerator()

return new LinkedListlterator(this);

// AddObject - добавление объекта в конец списка public LLNode AddObject(object objectToAdd)

{

return AddObject(tail, objectToAdd);

}

// AddObjectдобавление объекта в список public LLNode AddObject(LLNode previousNode,

object objectToAdd)

{

//Создание нового узла с добавляемым объектом LLNode newNode = new LLNode(objectToAdd);

//Начнем с простейшего случая — пустого списка.

if (head == null && tail == null)

{

// ...теперь в нем один элемент

head = newNode; tail = newNode;

return newNode;

}

// Добавляем ли мы новый узел в средину списка? if (previousNode != null &&

previousNode.forward != null)

{

// Просто изменяем указатели

LLNode nextNode = previousNode.forward;

//Указатель на следующий узел newNode.forward = nextNode; previousNode.forward = newNode;

//Указатель на предыдущий узел nextNode.backward = newNode; newNode.backward = previousNode;

return newNode;

}

// Добавление в начало списка? if (previousNode == null)

Глава 20. Работа с коллекциями

453

{

// Делаем его головой списка LLNode nextNode = head; newNode.forward = nextNode; nextNode.backward = newNode; head = newNode;

return newNode;

}

// Добавление в конец списка newNode.backward = previousNode; previousNode.forward = newNode; tail = newNode;

return newNode;

}

// RemoveObject - удаление объекта из списка public void RemoveObject(LLNode currentNode)

{

// Получаем соседей

удаляемого узла

LLNode

previousNode

=

currentNode.backward;

LLNode

nextNode

=

currentNode.forward;

//Обнуляем указатели удаляемого объекта currentNode.forward = currentNode.backward = null;

//Был ли это последний элемент списка?

if (head == currentNode && tail == currentNode)

head = tail = null; return;

// Это узел в средине списка?

if (head != currentNode && tail != currentNode)

previousNode.forward = nextNode; nextNode.backward = previousNode; return;

// Это узел в начале списка?

if (head •== currentNode && tail != currentNode)

head = nextNode; nextNode.backward = null; return;

// Это узел в конце списка...

tail = previousNode; previousNode.forward = null;

// LinkedListlterator - дает приложению доступ к спискам

454

Часть VII. Дополнительные главы

// LinkedList

public class LinkedListlterator : IEnumerator

{

//Итерируемый связанный список private LinkedList linkedList;

//"Текущий" и "следующий" элементы связанного списка.

//Объявлены как private для предотвращения

//непосредственного обращения извне

private LLNode currentNode = null; private LLNode nextNode = null;

//LinkedListlterator - конструктор

public LinkedListlterator(LinkedList linkedList)

{

this.linkedList = linkedList; Reset ();

}

// Currentвозвращаем объект данных в текущей позиции public object. Current

{

get

{

if (currentNode == null)

{

return null;

}

return currentNode.1inkedData;

//Reset - перемещение итератора назад, в позицию,

//непосредственно предшествующую первому узлу списка public void Reset()

{

currentNode = null; nextNode = linkedList.head;

}

//MoveNext - переход к следующему элементу списка, пока

//не будет достигнут его конец

public bool MoveNext()

{

currentNode = nextNode; if (currentNode == null)

{

return false;

}

nextNode = nextNode.forward; return true;

public class Program

Глава 20. Работа с коллекциями

455

public static void Main (string [] args)

{

//Создаем контейнер и добавляем в него три элемента LinkedList 11с = new LinkedList();

LLNode first = 11c.AddObject("Первая строка"); LLNode second = 11c.AddObject("Вторая строка"); LLNode third = 11c.AddObject("Последняя строка");

//Добавляем элементы в начале и средине списка LLNode newfirst

=11с.AddObject(null,"Перед первой строкой"); LLNode newmiddle

=11с.AddObject(second, "Между второй и " +

"третьей строкой");

// Итератором можно управлять "вручную" Console.WriteLine("Проход по контейнеру вручную"); LinkedListlterator H i

= (LinkedListlterator)11с.GetEnumerator(); H i . Reset () ;

while ( H i .MoveNext () )

{

string s = (string) Hi . Current ; Console.WriteLine(s);

}

// Либо использовать цикл foreach Console.WriteLine("\пОбход с использованием foreach"); foreach(string s in 11c)

{

Console.WriteLine(s);

}

// Ожидаем подтверждения пользователя Console.WriteLine("Нажмите <Enter> для " +

"завершения программы...");

Console.Read();

}

}

Классы LinkedList и LLNode образуют фундамент приведенной демонстрацион­ ной программы. На рис. 20.1 показан связанный список с тремя узлами, каждый из кото­ рых указывает на (или "содержит") отдельную строку, так что LinkedList вполне за­ служивает названия "контейнер". Лично я, впрочем, как и большинство в мире .NET, предпочитаю термин коллекция.

Узлы в связанном списке представлены объектами LLNode. Для каждого данно­ го узла член forward указывает на следующий узел в списке, а член backward — на предыдущий. Класс LinkedList представляет сам список. Член head указыва­ ет на первый узел списка, член tail — на последний. По сути, это все, что есть в данном классе.

456

Часть VII. Дополнительные главы

Рис. 20.1. Классы L i n k e d L i s t и LLNode совместно создают связанный список

Добавление объекта в связанный список

Основные сложности содержатся в методах AddObject О и RemoveObject (). Сначала рассмотрим метод AddOb j ect ().

Для того чтобы добавить объект в список, вы должны знать, куда именно его нужно поместить. По существу, имеется четыре ситуации.

1. Вы добавляете новый объект в пустой список. Это простейшая из всех ситуаций; она показана на рис. 20.2. Указатель head равен null, как и указатель tail. Следует просто заставить указывать их на добавляемый элемент, и на этом все — вы получите список с одним узлом.

Рис. 20.2. Добавление нового узла в пустой связанный список выполняется в два счета

2. Наиболее сложная ситуация — это добавление элемента в середину списка. В этом случае предыдущий и следующий узлы не равны null. На рис. 20.3 пока­ зана такая ситуация. Чтобы вставить объект в середину списка, следует настроить указатели forward и backward как вставляемого объекта, так и объектов, рас-

Глава 20. Работа с коллекциями

457

положенных по сторонам от места, куда он будет вставлен. На рис. 20.4 пред ставлен результат (показанные шаги очень важно делать в определенном порядке, чтобы не "потерять" часть списка из-за того, что у вас не останется ссылки на нее Вы можете следовать порядку, показанному в приведенном исходном тексте де­ монстрационной программы).

Рис. 20.3. Перед вставкой в список объект не связан ни с чем

Рис. 20.4. После вставки объекта в список он становится частью команды

458

Часть VII. Дополнительные главы

Рис. 20.5. Добавление объекта в голову списка делает его первым

всписке

3.Объект может быть добавлен и в голову списка. Это гибрид первых двух случаев.

Указатель на голову списка после вставки должен указывать на новый объект, а старый первый объект списка после вставки должен указывать на вставленный узел, как на предыдущий в списке. Все это продемонстрировано на рис. 20.5.

4.И наконец, новый объект может быть вставлен в конец списка. Это ситуация, об­ ратная случаю 3.

Удаление объекта из связанного списка

Метод R e m o v e O b j e c t () рассматривает те же четыре ситуации, но в обратном на­ правлении.

Единственный способ следовать A d d O b j e c t () и R e m o v e Ob j e c t () — нари­ совать рисунки наподобие рис. 20.2-20.5 и аккуратно пройти каждый шаг. Не стесняйтесь — не родился еще программист, который в подобной ситуации ни разу не рисовал бы рисунка для того, чтобы разобраться во всем. Сложные ве­ щи остаются сложными независимо от того, кто с ними работает.

Реализация перечислителя связанного списка

Обратите внимание, что

демонстрационная программа L i n k e d L i s t C o n t a i n e r

в действительности содержит

три класса: L L N o d e , L i n k e d L i s t и L i n k e d L i s t l t -

e r a t o r . Класс L i n k e d L i s t l t e r a t o r — сопутствующий связанному списку класс со специальными привилегиями. Он в деталях знаком с внутренним устройством связанно­ го списка, которое недоступно никому во внешнем мире. Внешние клиенты используют его для итерирования связанного списка.

Класс L i n k e d L i s t l t e r a t o r работает путем отслеживания текущего и следующе­ го за ним узла. Изначально c u r r e n t N o d e равен n u l l , a n e x t N o d e — первому эле­ менту связанного списка. После каждого вызова M o v e N e x t () c u r r e n t N o d e указывает на то, что перед этим было следующим узлом, a n e x t N o d e перемещается к следующему за ним узлу. После того как M o v e N e x t () достигает конца списка, c u r r e n t N o d e равно

Глава 20. Работа с коллекциями

459

n u l l , и функция отвергает все дальнейшие вызовы, возвращая для каждого из них зна­ чение f a l s e . Лучше не оказываться за концом списка.

Функция M a i n () демонстрирует использование связанного списка, сначала добавляя в него три строки s t r i n g . Затем одна строка добавляется в начало списка, и еще од­ на — в его середину.

Первый цикл вывода создает итератор для связанного списка. Программа проходит по всему списку путем вызова M o v e N e x t () до тех пор, пока функция не вернет значе­ ние f a l s e , указывающее, что достигнут конец списка. Для каждого узла программа по­ лучает его объект и преобразует его обратно в строку, из которой он был создан.

Затем функция M a i n () делает все то же, но с использованием цикла f o r e a c h . Цикл f o r e a c h точно так же проходит по всем узлам связанного списка, как функция

M a i n () делала это вручную.

Предпочтительно использовать

цикл f o r e a c h . Он дает лучший

код, прост

и меньше подвержен ошибкам.

Однако он имеет два ограничения:

в нем нет

счетчика i n t i, но можно объявить собственный счетчик перед циклом и са­ мостоятельно увеличивать его в теле цикла. Также нельзя удалять элементы из коллекции внутри цикла f o r e a c h . В этом случае вам нужна коллекция re­ m o v e d l t e m s , в которой вы сохраняете индексы или ссылки на элементы, най­ денные в цикле f o r e a c h и которые должны быть удалены из исходной кол­ лекции. Затем используйте цикл f o r e a c h еще раз для прохода по коллекции r e m o v e d l t e m s и удалите указанные в нем элементы из исходной коллекции,

Лучше

один раз

увидеть,

чем

сто — услышать, так что вот как это выглядит

в исходном тексте:

 

 

L i s t < s t r i n g >

r e m o v e d l t e m s

=

n e w L i s t < s t r i n g > ( ) ;

/ / Ц и к л n o

o r i g i n a l C o l l e c t i o n

f o r e a c h ( s t r i n g s

i n o r i g i n a l C o l l e c t i o n )

{

/ / Е с л и s т р е б у е т с я у д а л и т ь , с о х р а н я е м с с ы л к у и л и и н д е к с в / / r e m o v e d l t e m s

r e m o v e d l t e m s . A d d ( s ) ;

}

f o r e a c h ( s t r i n g s i n r e m o v e d l t e m s ) / / Ц и к л n o r e m o v e d l t e m s

{

o r i g i n a l C o l l e c t i o n . R e m o v e ( s ) ;

}

Вывод программы L i n k e d L i s t C o n t a i n e r выглядит следующим образом: П р о х о д п о к о н т е й н е р у в р у ч н у ю П е р е д п е р в о й с т р о к о й П е р в а я с т р о к а

В т о р а я

с т р о к а

Между

в т о р о й и т р е т ь е й с т р о к о й

П о с л е д н я я с т р о к а

О б х о д с и с п о л ь з о в а н и е м f o r e a c h П е р е д п е р в о й с т р о к о й П е р в а я с т р о к а

460

Часть VII. Дополнительные главы

Глав

В т о р а я

с т р о к а

 

Между в т о р о й и

т р е т ь е й с т р о к о й

П о с л е д н я я с т р о к а

Нажмите

< E n t e r >

д л я з а в е р ш е н и я п р о г р а м м ы . . .

Обобщенную версию этого связанного списка можно найти в демонстраци­ онной программе G e n e r i c L i n k e d L i s t C o n t a i n e r на прилагаемом ком­ пакт-диске. Обратите внимание, что G e n e r i c L i n k e d L i s t C o n t a i n e r продолжает использовать интерфейс I E n u m e r a t o r , который будет рас­ смотрен немного позже, но в некоторых ситуациях следует применять вме­ сто него новую обобщенную версию I E n u m e r a t o r < T > . Однако пока не стоит начинать анализировать обобщенные классы. Кроме того, обратитесь к встроенному обобщенному классу L i n k e d L i s t , который, несомненно, превосходит написанный здесь.

Зачем нужен связанный список

Связанный список может показаться пустыми хлопотами. Его основное преимущест­ во заключается в большой скорости вставки и удаления узлов. "Ну хорошо, — можете сказать вы. — Но ведь добавление s t r i n g в массив из четырех или пяти строк не слож­ нее перемещения нескольких ссылок для освобождения места." А что вы скажете, если этот массив будет содержать несколько сотен тысяч строк, и вы должны выполнять мас­ су вставок и удалений из него?

Второе преимущество связанного списка в том, что он может расти и уменьшаться. Если вы думаете, что вам будут нужны 1000 объектов, то вы должны создать массив на 1000 элементов, независимо от того, будете вы их использовать или нет. Что еще хуже, если вы в действительности создадите 2000 объектов, то можете считать, что в этот раз вам крупно не повезло. (Да, конечно, можно создать второй массив с большей емкостью и скопировать в него содержимое первого, но что при этом можно сказать об эффектив­ ности и затратах памяти?)

На этом принципе основаны многие распространенные вирусы. Например, неко­ торый исполненный благих намерений программист решает, что 256 символов будет достаточно для любого имени файла, и объявляет массив c h a r [ 2 5 6 ] . Ес­ ли программист забудет убедиться, что имя на самом деле не длиннее, чем ожи­ дается, то у хакера появляется шанс сломать программу, передав ей неправдопо­ добно длинное имя, и переписать тем самым часть кода за массивом (это называ­ ется переполнением буфера). Впрочем, это проблема в первую очередь C/C++: С# автоматически проверяет выход за границы массива.

В оставшейся части главы будут проанализированы три разных подхода к общей за­ даче итерирования коллекции. В этом разделе будет продолжено обсуждение наиболее традиционного (как минимум, для программистов на С#) подхода с использованием ите­ раторов, которые реализуют интерфейс I E n u m e r a t o r . В качестве примера рассматри­ вается итератор для связанного списка из предыдущего раздела.

Глава 20. Работа с коллекциями

461

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]