Връзки и указатели

Цел: изследване на фундаменталната абстрактна структура - свързани списъци.

В днешния урок ще разгледаме фундаменталната абстрактна структура на свързан списък.

Свързаният списък е колекция от последователно организирани данни. Свързаните списъци се дефинират като примитиви в някои езици за програмиране (напр. Lisp, Delphi), но не и в Pascal. Pascal обаче предоставя някои основни примитивни операции, за да улесни използването на свързани списъци.

Основното предимство на свързаните списъци пред масивите е, че те могат да растат или да се свиват по време на изпълнение на програмата. По-специално, не е необходимо да знаем максималния му размер предварително.

Второто предимство е, че осигуряват гъвкавост за пренареждане на елементите си. Тази гъвкавост идва за сметка на скоростта на достъп до произволен елемент от списъка. Това ще стане по-ясно по-късно, след като разгледаме някои от основните свойства на свързаните списъци и основните операции, дефинирани върху тях.

Списъкът може да бъде представен като връзки във верига.

В масив се предоставя последователна организация директно (според индекса).

Свързаният списък използва специален начин за организиране на данни, според който неговият елемент се съдържа в „възел“, съдържащ данни, както и „връзка“ към следващия „възел“.

Възел и връзка могат да бъдат описани по следния начин:

Тип Връзка = ^Възел; Възел = запис Данни: цяло число; Следва: Връзка; Край;

За да опишем списъка, ще използваме два допълнителни помощни възела head и z. Главният възел ще сочи към първия елемент от списъка, а z ще сочи към последния елемент от списъка. Глава понякогасе нарича "глава" на списъка, а z се нарича "опашка".

Тогава списъкът може да бъде представен в следната форма.

Това представяне на данни ви позволява да извършвате някои операции с много по-ефективни методи, отколкото с масиви. Например, ако искаме да преместим елемент 1 от края към началото, тогава, за да извършим такава операция в масив, ще трябва да преместим всички негови елементи, за да освободим място. В свързания списък променяме само връзките. И двете версии на фигурата са еквивалентни; те просто са нарисувани по различен начин. Ние просто нулираме връзката на възела, съдържащ 1, към възела, съдържащ 2, и връзката на празния главен възел към възела, съдържащ 1. Дори ако списъкът беше много голям, пак ще ни трябват само три пермутации на връзката за такъв обмен.

списъка

По-важното е, че вече можем да говорим за "вмъкване" на елемент в свързан списък (което увеличава дължината му с един елемент) - операция, която е толкова неестествена и неудобна за масиви. Фигурата показва как да вмъкнете нов възел в списъка. Първо трябва да добавите този възел, след това да създадете връзка към възел q и след това да промените връзката на възел p към новия възел. Това изисква само две препратки да бъдат променени, независимо от дължината на списъка.

списъка

По подобен начин можем да говорим за "премахване" на елемент от списъка (което намалява размера му с един елемент). Просто трябва да промените връзката към възела, който следва възела q и след това възелът, съдържащ q, трябва да бъде унищожен.

връзки

Pascal ни предоставя някои примитивни операции, които ни позволяват да създаваме директно свързани списъци. Следният кодов фрагмент е пример за изпълнение на основните функции,които току-що обсъдихме.

Тип Връзка = ^Възел; Възел = запис Данни: цяло число; Следва: Връзка; Край;

Var Head, z: връзка;

процедура list_initialize; започнете ново(глава); ново(z); head^.next := z; z^.next := нула; край;

процедура insert_after( v : цяло число; t : връзка ); вар. x : връзка; начало ново(x); x^.data := v; x^.next := t^.next; t^.next := x; край;

процедура delete_next( t : връзка ); var del: връзка; започнете дел := t^.next; t^.next := t^.next^.next; изхвърляне(дел); край;

Нека ги разгледаме по-подробно.

Link = ^Node; - тук се създава нов тип Link, който е въведен указател към типа Node. Типът възел е показан по-долу.

Нека разгледаме по-отблизо това определение.

Компютърната памет може да бъде представена по следния начин. Той е разделен на блокове, всеки такъв блок се нарича сегмент. В Dos всеки номер на сегмент може да съдържа максимум 16 бита (числото $FFFF, когато всички битове са равни

списъка

1) (символът $ е знак на шестнадесетичната система), т.е. всеки сегмент може да има номер [0; $FFFF]. Да приемем, че разглеждаме област на паметта със сегменти $592B, $592C, $592D. За да намерите конкретна клетка от паметта във всеки сегмент, има така нареченото отместване, което също може да бъде номерирано [0; $FFFF]. Например, нека отместването вътре в сегмента $592C е $B401, тогава указателят към това място в паметта ще бъде $592CB401.

procedure list_initialize;

head^.next := z; - тук въвеждаме $592CB401 в клетката, започвайки от 3 байта (тъй като първите два са заети от ключ) в областта, наречена Next, числото $592D000A.

процедураinsert_after( v : цяло число; t : връзка);

процедура delete_next(t: връзка);

Защо използването на празни възли е толкова полезно?

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

Второ, конвенцията за празния възел z предотвратява ненужната проверка на процедурата за изтриване за изтриване от празния списък.

И тези проверки значително влияят на времето за изпълнение на програмата.

Израз катоhead^.next^.key ни дава първия елемент от списъка, докатоhead^. Next^.next^.key е вторият.

Нека създадем програма, която обработва информация за автомобили.

програмна кола; използва crt; тип namestr = низ[20]; Връзка = ^Възел; Възел = запис име: namestr; скорост: цяло число; следващ: връзка; край; var head, z: връзка; namefind:namestr; v: 0..4; крайно меню: boolean;

процедура list_initialize; започнете ново(глава); ново(z); head^.next:=z; z^.next:=nil; край;

процедура list_destroy; започнете изхвърляне (глава); изхвърляне(z); край;

процедура insert_after(име1: иместр; скорост1: цяло число; t: връзка); вар. x : връзка; начало ново(x); x^.име := име1; x^.speed:= скорост1; x^.next := t^.next; t^.next := x; край;

процедура delete_next(t: връзка); var del: връзка; започнете дел := t^.next; t^.next := t^.next^.next; изхвърляне(дел); край;

процедура InpAuto; var nam:namestr; sp: цяло число; begin write('Въведете марка на кола: '); readln(име); write('Максимална скорост: '); readln(sp); insert_after(име, sp, глава); край;

процедура Mylist; вар Връзка: Връзка; започнете Curr:=head^.next; Докато Curr^.next <> nil do begin writeln('Марка: ', Curr^.name, ' Скорост: ', Curr^.Speed); curr:=curr^.next; край; write('Извеждането на списъка е завършено, натиснете Enter.'); readln; край;

функция findname(fn: namestr): връзка; var Curr: Връзка; започнете Curr:=head; Докато Curr<>Nil направете if Curr^.name=fn тогава започнете findname:=curr; изход; край друго curr:=curr^.next; findName:=Nil; край;

начало списък_инициализиране; крайно меню:=false; повтаряне clrscr; writeln('Посочете типа работа:'); writeln('1. Пишете първи в списъка'); writeln('2. Изтриване на първия обект от списъка'); writeln('3. Вижте целия списък'); writeln('4. Изтриване на обекта, следващ посочения в списъка'); writeln('0. Край на работа'); readln(v); случай v от 1: inpauto; 2: delete_next(head); 3: mylist; 4: begin writeln('Въведете марката на колата, последвана от тази за премахване от списъка'); readln(NamFind); delete_next(FindName(namfind)); край; друго крайно меню:=вярно; край; до края на менюто; списък_унищожаване; край.

Следващият вид списъци са цикличните списъци. Цикличният списък е списък, чийто последен елемент сочи към първия. Този списък позволява на програмата да обикаля списъка отново и отново толкова дълго, колкото е необходимо.

Като пример ще разгледаме така наречения "проблем на Йосиф". Той е следният: представете си, че N души решат да извършат масово самоубийство, като се наредят в кръг и убият всеки M-ти човек и постепенно стесняват кръга. Необходимо е да се намери човекът, който ще умре последен (макар че в крайна сметка той ще промени решението си по този въпрос), или, с други думи, да се намерисмъртен ред на тези хора. Например, ако N=9 и M=5, тогава хората ще бъдат убити в следния ред: 5 1 7 4 3 6 9 2 8. Тази програма чете стойностите на N и M и след това отпечатва този ред.

Тази програма използва цикличен свързан списък, за да симулира директно поръчка за самоубийство. Първо се създава списък с ключове от 1 до N: променливата head сочи към началото на списъка по време на неговото създаване, а след това програмата прави последния елемент в списъка да сочи към главата. След това програмата преминава през този цикличен списък, като пропуска M-1 елементи и унищожава следващия възел след тях, докато в списъка остане само един елемент (който сочи към себе си).

тип връзка=^възел; възел=запис данни:дума; следващ:връзка; край; VarN,M:дума; head:link; Процедура Init; Променлива q,l:връзка; i:дума; Започнете Write('Въведете N: '); Четене(N); Ново (глава); l:=глава; Head^.data:=1; Head^.next:=head; За i:=2 до n направете begin New(q); q^.data:=i; l^.next:=q; q^.next:=head; l:=q; край; Край; Процедура Del; Променлива i,k:дума; q,p:връзка; Започнете Write('Въведете M: '); Четене(M); k:=0; i:=1; q:=глава; Докато k

И така, в днешния урок разгледахме връзки и указатели, линейни и циклични списъци.

списъка