diff options
Add Seminar05
4 files changed, 854 insertions, 0 deletions
| -rw-r--r-- | Seminar04/reorder-linked-list.cpp | 160 | ||||
| -rw-r--r-- | Seminar05/LinkedList.hpp | 306 | ||||
| -rw-r--r-- | Seminar05/README.md | 316 | ||||
| -rw-r--r-- | Seminar05/list-cheat-sheet.cpp | 72 |
diff --git a/Seminar04/reorder-linked-list.cpp b/Seminar04/reorder-linked-list.cpp new file mode 100644 index 0000000..6259a36 --- /dev/null +++ b/Seminar04/reorder-linked-list.cpp @@ -0,0 +1,160 @@ +#include"linked-list-utils.h" +#include<deque> + +// O(n) time +// O(n) complexity +void reorderWithDeque(LinkedListNode<int>* head) { + if(!head || !head->next) + return; + + std::deque<LinkedListNode<int>*> dq; + + LinkedListNode<int>* it = head; + + while(it) { + dq.push_back(it); + it = it->next; + } + + // Винаги пазим края на вече готовия списък + // Ако не е ясно как го използвам прочети обяснението над втория метод + // идеята е същата. + LinkedListNode<int>* backOfNewList = nullptr; + + while(!dq.empty()) { + // Ако размерът е 1 то имаме нечетен брой елементи и просто добавяме последния в края + if(dq.size() == 1) { + backOfNewList->next = dq.front(); + backOfNewList = backOfNewList->next; + break; + } + + // Първия елемент останал в дека започва да сочи към последния. + // Генерираме коректна двойка. + dq.front()->next = dq.back(); + + // Ако края на списъка не е nullptr следващите два елемента + // в списъка са dq.front()->dq.front()->next (което е dq.back()) + if(backOfNewList) + backOfNewList->next = dq.front(); // Сложи следващите два елемента накрая + + backOfNewList = dq.front()->next; // Вече края на списъка е друг. Преместваме се там. + + dq.pop_back(); + dq.pop_front(); + } + + // next на последния елемент от списъка е nullptr, този ред е важен. + backOfNewList->next = nullptr; +} + +// Това ще ни помогне за второто решение +template<class T> +void reverseLinkedList(LinkedListNode<T>*& ll) { + LinkedListNode<T>* previous = nullptr; + LinkedListNode<T>* next = nullptr; + LinkedListNode<T>* iterate = ll; + + while(iterate) { + next = iterate->next; + iterate->next = previous; + previous = iterate; + iterate = next; + } + + ll = previous; +} + +// Това също +template<class T> +LinkedListNode<T>* findMiddle(LinkedListNode<T>* ll) { + LinkedListNode<T>* fast = ll; + LinkedListNode<T>* slow = ll; + + while(fast && fast->next) { + fast = fast->next->next; + slow = slow->next; + } + + return slow; +} + +/* +Тук идеята е да намерим средата и да я обърнем. Две неща, които вече знаем как да правим. Как ще протече алгоритъма: + +Вход: 1 -> 2 -> 3 -> 4 -> 5 +Среда: 3 -> 4 -> 5 + +Обърната среда: 5 -> 4 -> 3 -> nullptr +Как изглежда първоначалния масив: 1 -> 2 -> 3 -> nullptr + +Дали е бъг, че и в двата списъка имаме еднакъв последен елемент? Малко неточно е, но за да го оправя трябва да променя функциите. +С цел да са разбираеми нещата го оставям така. + +В случая става нещо подобно: +1-> 2 -> 3 -> nullptr + ^ +5 -> 4 / + +Виждаме, че последователно трябва да вземаме двойки (L_0 -> L_n) -> (L_1 -> L_{n-1}) -> (L_2 -> L_{n - 2}) -> ... и да ги поставяме една след друга. + +* firstHalf -> първата половина от списъка +* mid -> Втората половина от списъка +* currentPair -> текущата двойка (L_i -> L_{n - i}) +* backOfList -> Края на коректния списък от двойки + +Сега имаме този сценарий: +[някакъв коректен списък] [коректна двойка] +* Насочи края на този коректен списък да сочи към коректната двойка. Това става лесно понеже му пазя края! +[някакъв вече готов списък] -> [коректна двойка] +* Сега насочи указателя който пази края на коректния списък да сочи към края на този списък, понеже този списък е корекрен. +[стария коректен списък -> коректната двойка] - готов за още коректни двойки! + +Така добавямe коректни двойки докато не стигна края на списъка съдържащ обърнатата дясна част. +*/ +void reorderWithTwoPointers(LinkedListNode<int>* ll) { + if(!ll || !ll->next) + return; + + LinkedListNode<int>* mid = findMiddle(ll); + reverseLinkedList(mid); + + LinkedListNode<int>* firstHalf = ll; + LinkedListNode<int>* currentPair = nullptr; + LinkedListNode<int>* backOfList = nullptr; + + while(mid) { + currentPair = firstHalf; + firstHalf = firstHalf->next; + + // Случая в който имаме нечетен брой елементи + if(!firstHalf) { + backOfList->next = currentPair; + backOfList = backOfList->next; + break; + } + + currentPair->next = mid; + mid = mid->next; + + if(backOfList) + backOfList->next = currentPair; // Пренасочи края към коректната двойка. Ако е nullptr нямаме какво да пренасочваме. + + backOfList = currentPair->next; // И един бърз ъпдейт на края - вече края на коректния лист е стария лист + новата двойка + } + + backOfList->next = nullptr; +} + +int main() { + LinkedListNode<int>* ll = new LinkedListNode<int> + (1, new LinkedListNode<int> + (2, new LinkedListNode<int> + (3, new LinkedListNode<int> + (4, new LinkedListNode<int> + (5))))); + + reorderWithTwoPointers(ll); + printLinkedList(ll); + freeList(ll); +}
\ No newline at end of file diff --git a/Seminar05/LinkedList.hpp b/Seminar05/LinkedList.hpp new file mode 100644 index 0000000..fc74b21 --- /dev/null +++ b/Seminar05/LinkedList.hpp @@ -0,0 +1,306 @@ +#ifndef _SINGLY_LINKED_LIST_IMPL +#define _SINGLY_LINKED_LIST_IMPL + +#include<stdexcept> + +template<class T> +class LinkedList { +private: + struct LinkedListNode { + T data; + LinkedListNode* next; + + LinkedListNode(const T& elem, LinkedListNode* n = nullptr) : data{ elem }, next { n } {} + }; + + void free(); + void copy(const LinkedList<T>&); + void move(LinkedList<T>&&); +public: + class Iterator { + private: + // Текущия възел в обхождането + LinkedListNode* currentNode; + + Iterator(LinkedListNode* arg) : currentNode { arg } {} + public: + + Iterator& operator++() { + if(currentNode == nullptr) + return *this; + currentNode = currentNode->next; + return *this; + } + + Iterator operator++(int) { + Iterator temp = *this; + ++(*this); + return temp; + } + + T& operator*() { + return currentNode->data; + } + + bool operator==(const Iterator& other) const { + return other.currentNode == currentNode; + } + + bool operator!=(const Iterator& other) const { + return !(*this == other); + } + + friend class LinkedList; + }; + + 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; + }; + + LinkedList(); + + LinkedList(const LinkedList<T>&); + LinkedList(LinkedList<T>&&); + + LinkedList& operator=(const LinkedList<T>&); + LinkedList& operator=(LinkedList<T>&&); + + Iterator begin() { + return Iterator(head); + } + + Iterator end() { + return Iterator(nullptr); + } + + ConstIterator begin() const { + return Iterator(head); + } + + ConstIterator end() const { + return Iterator(nullptr); + } + + ConstIterator cbegin() { + return ConstIterator(head); + } + + ConstIterator cend() { + return ConstIterator(nullptr); + } + + void pushBack(const T&); + void popBack(); + + void pushFront(const T&); + void popFront(); + + const T& front() const; + const T& back() const; + + bool empty() const; + size_t size() const; + + ~LinkedList(); + +private: + LinkedListNode* head; + LinkedListNode* tail; + size_t sizeOfList; +}; + +template<class T> +LinkedList<T>::LinkedList() : head {nullptr}, tail {nullptr}, sizeOfList { 0 } {} + +template<class T> +void LinkedList<T>::free() { + while(head) { + LinkedListNode* toDelete = head; + head = head->next; + delete toDelete; + } + sizeOfList = 0; +} + +template<class T> +void LinkedList<T>::copy(const LinkedList<T>& other) { + LinkedListNode* iter = other.head; + + while(iter) { + pushBack(iter->data); + iter = iter->next; + } + + sizeOfList = other.sizeOfList; +} + +template<class T> +void LinkedList<T>::move(LinkedList<T>&& other) { + head = other.head; + tail = other.tail; + sizeOfList = other.sizeOfList; + + other.tail = other.head = nullptr; + other.sizeOfList = 0; +} + +template<class T> +LinkedList<T>::LinkedList(const LinkedList<T>& other) { + copy(other); +} + +template<class T> +LinkedList<T>::LinkedList(LinkedList<T>&& other) { + move(other); +} + +template<class T> +LinkedList<T>& LinkedList<T>::operator=(const LinkedList<T>& other) { + if(this != & other) { + free(); + copy(other); + } + return *this; +} + +template<class T> +LinkedList<T>& LinkedList<T>::operator=(LinkedList<T>&& other) { + if(this != &other) { + move(other); + } + return *this; +} + +template<class T> +void LinkedList<T>::pushBack(const T& elem) { + LinkedListNode* toInsert = new LinkedListNode(elem); + ++sizeOfList; + + if(empty()) { + head = tail = toInsert; + } + else { + tail->next = toInsert; + tail = toInsert; + } +} + +template<class T> +void LinkedList<T>::popBack() { + if(empty()) { + throw std::runtime_error("Tried to pop back on empty list!"); + } + --sizeOfList; + + if(head == tail) { + delete tail; + head = tail = nullptr; + } + else { + LinkedListNode* newTail = head; + + // O(n) за n дължината на свързания списък + while (newTail->next != tail) { + newTail = newTail->next; + } + + delete tail; + tail = newTail; + tail->next = nullptr; + } +} + +template<class T> +void LinkedList<T>::pushFront(const T& elem) { + LinkedListNode* toInsert = new LinkedListNode(elem, head); + + if(empty()) + tail = toInsert; + + head = toInsert; + + ++sizeOfList; +} + +template<class T> +void LinkedList<T>::popFront() { + if(empty()) + throw std::runtime_error("Cannot pop front from empty list!"); + + if(head == tail) { + delete tail; + head = tail = nullptr; + } + else { + LinkedListNode* toDelete = head; + head = head->next; + delete toDelete; + } + + --sizeOfList; +} + +template<class T> +bool LinkedList<T>::empty() const { + return head == nullptr; +} + +template<class T> +size_t LinkedList<T>::size() const { + return sizeOfList; +} + +template<class T> +const T& LinkedList<T>::front() const { + if(empty()) + throw std::runtime_error("Empty list!"); + + return head->data; +} + +template<class T> +const T& LinkedList<T>::back() const { + if(empty()) + throw std::runtime_error("Empty list!"); + + return tail->data; +} + +template<class T> +LinkedList<T>::~LinkedList() { + free(); +} + +#endif
\ No newline at end of file diff --git a/Seminar05/README.md b/Seminar05/README.md new file mode 100644 index 0000000..fd8a62a --- /dev/null +++ b/Seminar05/README.md @@ -0,0 +1,316 @@ +# Свързани списъци втора част + +## Малко за представянията +Миналия семинар работихме с един класически начин да представим свързан списък, а именно просто чрез указател към първия му елемент. Днес ще разгледаме представянето на свързания списък като клас. +Разбира се, какво може да е толкова трудното на това да представим свързания списък като клас? Вместо функция *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 +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), " "); + } +} +``` + +## Задача първа (Контролно 2021): + +Даден е списък l с n елемента. + +Да се дефинира функция shuffle(), която получава **адреса на първия елемент на списъка**. + +Функцията да пренарежда възлите на списъка така, че елементите от втората половина на списъка да се преместят в началото на списъка, но в обратен ред (при списъци с нечетен брой елементи считаме средния елемент за принадлежащ към първата половина на списъка). +Пример: + +Вход: L1 → L2 → L3 → L4 → L5 + +Изход: L5 → L4 → L1 → L2 → L3 + +## Задача втора (Контролно 2021): + +Даден е списък 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 +```
\ No newline at end of file diff --git a/Seminar05/list-cheat-sheet.cpp b/Seminar05/list-cheat-sheet.cpp new file mode 100644 index 0000000..ee5f906 --- /dev/null +++ b/Seminar05/list-cheat-sheet.cpp @@ -0,0 +1,72 @@ +#include<list> +#include<iostream> + +void printList(const std::list<int>& myList, const char* msg) { + if(msg != "") + std::cout << msg << std::endl; + + // range based for loop който извежда всички елементи на списъка + for(const int& elementInList : myList) + std::cout << elementInList << " "; + std::cout << std::endl; +} + +int main() { + std::list<int> myList; + + const auto f = myList.begin(); + + // Добави елемент в края на листа + for (int i = 0; i < 10; i++) + myList.push_back(i); + + // Добави елемент в началото на листа + for(int i = 10; i < 20; i++) + myList.push_front(i); + + printList(myList, "Added the elements from [0..9] to the back and [10..19] to the front"); + + // Можем да изтриваме елементи на позиция + myList.erase(myList.begin()); + printList(myList, "Erased the first element"); + + // Удобен начин да обърнем списък + myList.reverse(); + printList(myList, "Reversed the linked list"); + + // Можем да премахнем елемент + myList.remove(10); + printList(myList, "After removing 10 from the list"); + + // Можем да премахнем всички елементи, които отговарят на някакъв предикат + // Аргумента на remove_if е булева функция. Ще останат само тези за които условието е лъжа. + myList.remove_if([](const int& currentElement) -> bool { return currentElement % 2; }); + printList(myList, "Filter all odd elements"); + + // myList.begin(); begin iterator + // myList.end(); end iterator + + // myList.cbegin(); constang begin iterator + // myList.cend(); constant end iterator + + // Мога да създавам списъци така + std::list<int> second{1, 2, 3, 4, 5}; + printList(second, "Create second list with initializer list"); + + // Премества елементи от един лист в друг + myList.splice(myList.begin(), second); + printList(myList, "First list after splice"); + printList(second, "second list after splice"); + + // Да си върнем малко елементи във втория списък + second.assign({1, 2, 3, 4, 5}); + printList(second, "Second list after .assign({1, 2, 3, 4, 5})"); + + // Можем да сортираме листа + myList.sort(); + printList(myList, "First list after beigg sorted"); + + // Можем и да го сортираме наобратно + myList.sort([](const int& fst, const int& snd) -> bool { return fst > snd; }); + printList(myList, "First list after being sorted with >"); +}
\ No newline at end of file |
