1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
|
# Свързани списъци втора част
## Малко за представянията
Миналия семинар работихме с един класически начин за представяне на свързан списък, а именно просто чрез указател към първия му елемент. Днес ще разгледаме представянето на свързания списък като клас.
Разбира се, какво може да е толкова трудното на това да представим свързания списък като клас? Правим подходящи конструктори, вместо функция *freeLinkedList* ще имаме деструктор, всички нужни функции ще ги направим като член-функции на новосъздадения ни клас и готово.
Проблемът тук е, че доста често ни се налага да обхождаме списъка. Когато го представяхме чрез указател към първия му елемент нещата бяха лесни:
```cpp
void map(LinkedListNode<int>* ll, void (*func)(int&)) {
while(ll) {
func(ll->data);
ll = ll->next;
}
}
```
Можем да направим нещо подобно:
```cpp
LinkedListNode* getHead();
```
Да вземем началото и да обхождаме отново с while цикъл. Когато представяме свързания списък като клас е желателно да държим логиката по представянето на елемента private. Потребителя работи с интерфейса на свързания списък. Това, че елементът на свързания списък е структура LinkedListNode която има указател към следващ елемент и тн остава скрито.
## Итератори
Тук на помощ идват итераторите. Итераторите са обекти, които са отговорни за обхождане на контейнери (динамичен масив, свързан списък и тн). В *C++* голяма част от STL използва итератори за постигане на целите си.
Примерната реализация, с която ще работим, се намира във файла LinkedList.hpp. Представяме свързания списък с два указателя, където
* head сочи към първия (най-левия) елемент
* tail сочи към последния (най-десния) елемент
* Също така пазим още една допълнителна променлива от тип size_t за дължината на списъка.
Разгледайте реализацията, особено алгоритмите за добавяне и премахване в началото и в края. Забележете, че всеки път разглеждаме три случая:
* Дали листа е празен
* Дали листа има един елемент
* Дали листа има повече от един елемент
Вече имаме някаква примерна реализация на свързан списък. Хайде да си направим итератор и да видим колко точно сложно ще е.
Ще направим итератора **вътрешен и публичен клас за класа LinkedList**.
```cpp
// ...
public:
class Iterator {
private:
public:
};
// ...
```
Сега, какво трябва да имаме в този клас? Той е отговорен за предвижване по възлите на свързания списък. Когато се придвижваме във всеки момент имаме някой текущ елемент. Следователно ще ни трябва текущ елемент. Също така, ще ни трябва начин да създадем итератор към някой елемент.
След курса по ООП това е лесна задача:
```cpp
class Iterator {
private:
// Текущия възел в обхождането
LinkedListNode* currentNode;
Iterator(LinkedListNode* arg) : currentNode { arg } {}
public:
};
```
Искаме да обхождаме, значи ни трябва някакъв начин да се придвижим напред. Конвенцията е тази задача да се изпълнява от operator++.
Това става по следния начин:
```cpp
// Забележете, че връщаме итератор!
Iterator& operator++() {
// Ако сме стигнали края не правим нищо
if(currentNode == nullptr)
return *this;
// Преместваме се една позиция надясно
currentNode = currentNode->next;
return *this;
}
```
Супер, вече имаме ++it. Но ще е добре и да си направим it++, не знаем за какво ще ни потрябва.
```cpp
// Защо този път връщаме Iterator а не Iterator&?
// Защото този път връщаме temporary object наречен temp. Той е заделен статично и ще се унищожи когато стигнем края на скоупа
Iterator operator++(int) {
Iterator temp = *this;
++(*this);
return temp;
}
```
Напомняме, че разликата между префиксния и постфиксния operator++ е просто един "служебен" int като аргумент на постфиксния.
Супер, вече можем да се движим, но това движение става безполезно ако нямаме начин да видим каква информация имаме в currentNode. Тази задача по конвенция се изпълнява от operator\*. Супер, нека го предефинираме:
```cpp
T& operator*() {
return currentNode->data;
}
```
Също ни трябва начин да разберем дали два итератора са равни или не. Тази задача се изпълнява от *operator==* и *operator!=*
```cpp
bool operator==(const Iterator& other) const {
// Сравняваме ги по адреси!
return other.currentNode == currentNode;
}
bool operator!=(const Iterator& other) const {
return !(*this == other);
}
```
Повечето контейнери в STL имате методи begin() и end(), които връщат итератори към началото и края. Нека последваме примера им:
```cpp
Iterator begin() {
return Iterator(head);
}
Iterator end() {
return Iterator(nullptr);
}
```
Ние направихме конструктора на итератора private. За да извикаме Iterator(head) обаче трябва някак да имаме достъп до private конструктора на Iterator.
Това ще направим като декларираме класа LinkedList като приятелски за Iterator!
```cpp
/// ...
bool operator!=(const Iterator& other) const {
return !(*this == other);
}
friend class LinkedList;
/// ...
```
Вече сме готови. Нека да проверим дали нашия метод работи:
```cpp
int main() {
LinkedList<int> ll;
for (size_t i = 0; i < 10; i++)
ll.pushBack(i);
// Тук има значение дали викаме ++it или it++!
for(LinkedList<int>::Iterator it = ll.begin(); it != ll.end(); ++it)
std::cout << *it << " ";
}
```
## Const и итераторите
Нека разгледаме следния код:
```cpp
int main() {
const LinkedList<int> ll;
LinkedList<int>::Iterator beg = ll.begin();
}
```
В случая имаме грешка, понеже нямаме декларирана константна функция begin(). Нека си напишем константни begin() и end():
```cpp
const Iterator begin() const {
return Iterator(head);
}
const Iterator end() const {
return Iterator(nullptr);
}
```
Горния фрагмент код се компилира, но вече имаме друг проблем:
```cpp
void hack(const LinkedList<int>& ll) {
LinkedList<int>::Iterator beg = ll.begin();
*beg = 10;
}
int main() {
LinkedList<int> ll;
ll.pushBack(1);
hack(ll);
std::cout << ll.back();
}
```
В случая, въпреки че ll е const, отново променяме стойността му. Но защо?
В случая **beg е константен**. Той е съставен от един пойнтър, който обаче **не е константен**. В някакъв смисъл **това, че стойността на паметта към която сочи пойнтъра се променя не чупи константността на итератора**. За това ще се наложи да дефинираме още един клас, ConstIterator!
```cpp
class ConstIterator {
private:
// Тук вече е указател към константна памет
const LinkedListNode* currentNode;
ConstIterator(LinkedListNode* arg) : currentNode { arg } {}
public:
ConstIterator& operator++() {
if(currentNode == nullptr)
return *this;
currentNode = currentNode->next;
return *this;
}
ConstIterator operator++(int) {
Iterator temp = *this;
++(*this);
return temp;
}
const T& operator*() const {
return currentNode->data;
}
bool operator==(const ConstIterator& other) const {
return other.currentNode == currentNode;
}
bool operator!=(const ConstIterator& other) const {
return !(*this == other);
}
friend class LinkedList;
};
```
Нещата са доста подобни, единствената разлика тук е, че имаме константен указател като тип на текущия елемент.
Реализираме следните:
```cpp
// Връща константен итератор към началото
ConstIterator cbegin() const {
return ConstIterator(head);
}
// Връща константен итератор към края
ConstIterator cend() const {
return ConstIterator(nullptr);
}
```
Сега, ако искам да имам итератор за константен списък, просто ще създавам ConstIterator. Примерно, искаме да итерираме константен свързан списък:
```cpp
void iterate(const LinkedList<int>& ll){
for(LinkedList<int>::ConstIterator it = ll.cbegin(); it != ll.cend(); ++it) {
std::cout << *it << " ";
}
}
```
## auto
Най - вероятно вече сте чували за ключовата дума *auto*. Чрез нея карате компилатора сам да прецени какъв тип да използва за променливата. Тя е удобна когато имате дълги типове. Прави кодът по-четим и разбираем. Като всяко хубаво нещо обаче **с auto не е добре да се прекалява.**
```cpp
// Автоматично разбирам типа на it
for(auto it = ll.begin(); it != ll.end(); ++it) {
//...
}
auto x = 5; // х е променлива от тип int
const auto y = 10u; // y e променлива от тип const unsigned
auto f = 10.0f; // f е променлива от тип float
const auto& r = f; // r е променлива от тип const float&
std::list<int> ll;
auto it = ll.begin(); // it e променлива от тип std::list<int>::iterator
const auto its = ll.end(); // it е променлива от тип ?
auto itss = ll.cend(); // it е променлива от тип ?
```
## range based for loops
От c++11 езикът ни предлага начин, чрез който да пишем подобни for цикли още по - бързо.
```cpp
int main() {
LinkedList<int> ll;
for(const int& elem : ll) {
std::cout << elem << " ";
}
}
```
Този цикъл е еквивалентен на горния, просто е синтактична захар.
За по - любопитните компилатора развива този приятен for цикъл до следното:
```cpp
{
LinkedList<int> & __range1 = ll;
LinkedList<int>::Iterator __begin1 = __range1.begin();
LinkedList<int>::Iterator __end1 = __range1.end();
for(; __begin1.operator!=(__end1); __begin1.operator++()) {
const int & elem = __begin1.operator*();
std::operator<<(std::cout.operator<<(elem), " ");
}
}
```
## Задача първа:
Даден е списък `l` с `n` елемента.
Да се дефинира функция shuffle(), която получава **адреса на първия елемент на списъка**.
Функцията да пренарежда възлите на списъка така, че елементите от втората половина на списъка да се преместят в началото на списъка, но в обратен ред (при списъци с нечетен брой елементи считаме средния елемент за принадлежащ към първата половина на списъка).
Пример:
Вход: L1 → L2 → L3 → L4 → L5
Изход: L5 → L4 → L1 → L2 → L3
## Задача втора:
Даден е списък l с елементи стекове.
Да се дефинира функция equalize(l), която размества елементите на стековете така, че да няма два стека в l с разлика в броя на елементите, по-голяма от 1.
```
Вход Изход
5 -> 4 -> 3 -> 2 -> 1 5 -> 4 -> 3
| |
v v
7 -> 6 7 -> 6 -> 1
| |
v v
11 -> 10 -> 9 -> 8 11 -> 10 -> 9
| |
v v
12 12 -> 2 -> 8
```
## Задача трета - Linked List Partition
Да се създаде функция
```cpp
LinkedListNode<int>* partition(LinkedListNode<int>* head, int x);
```
която пренарежда списъка така, че всички възли със стойност по-малка от **х** са преди всички възли със стойност по-голяма или равна от **х**.
Пример:
Вход: head -> 1 -> 4 -> 3 -> 2 -> 5 -> 2, x = 3
Изход: 1 -> 2 -> 2 -> 4 -> 3 -> 5
Вход: head -> 2 -> 1, x = 2
Изход: 1 -> 2
**HINT:**
Можете да си направите два указателя, чрез които да строите някакви полезни помощни списъци. После, след като построите тези полезни помощни списъци, можете да пренасочите правилните указатели и да получите коректен отговор.
|