diff options
pls no more merge conficts
9 files changed, 40 insertions, 33 deletions
| -rw-r--r-- | Seminar01/README.md | 12 | ||||
| -rw-r--r-- | Seminar02/README.md | 18 | ||||
| -rw-r--r-- | Seminar03/README.md | 2 | ||||
| -rw-r--r-- | Seminar04/README.md | 8 | ||||
| -rw-r--r-- | Seminar05/README.md | 6 | ||||
| -rw-r--r-- | Seminar06/README.md | 4 | ||||
| -rw-r--r-- | Seminar08/README.md | 8 | ||||
| -rw-r--r-- | Seminar09/README.md | 12 | ||||
| -rw-r--r-- | Seminar11/README.md | 3 |
diff --git a/Seminar01/README.md b/Seminar01/README.md index 79424b2..47f9f83 100644 --- a/Seminar01/README.md +++ b/Seminar01/README.md @@ -42,7 +42,7 @@ void redirect(int*& ptr, int& dest) { } ``` то вече функцията върши желаната работа. Когато приемаме обект по референция **копие не се прави**. Това важи за всички типове - включително и за указателите. -Можем да правим указатели към всякакви типове включително и към указатели. Типът **int**** не е нищо повече от указател към указател. +Можем да правим указатели към всякакви типове включително и към указатели. Типът **int\*\*** не е нищо повече от указател към указател. Тогава не можем ли просто да вземем указател към указателя и да го променим по този начин? ```c++ @@ -63,12 +63,12 @@ int main() { ``` Този код също върши работа. Тогава каква е разликата между указател и референция? -Разликата е, че указателите са по - гъвкави. Не можем да пренасочим референция. Също така не можем да имаме референция която не сочи към обект. При указателите имаме специален тип наречен nullptr, който сочи към "нищото". Често референцията е представяна като указател, който не може да се пренасочва, вика * вместо теб и винаги сочи към обект. +Разликата е, че указателите са по - гъвкави. Не можем да пренасочим референция. Също така не можем да имаме референция която не сочи към обект. При указателите имаме специален тип наречен nullptr, който сочи към "нищото". Често референцията е представяна като указател, който не може да се пренасочва, вика \* вместо теб и винаги сочи към обект. ## const и указателите За *const* важи следното правило - *const* винаги модифицира това което е в негово ляво, освен когато няма нищо отляво, тогава модифицира това което е отдясно. -* const int* -> Указател, който сочи към константен *int*. Можем да пренасочваме, но не можем да променяме стойността. -* int const* -> Абсолютно същото. -* int * const -> Константен указател, не можем да го пренасочваме. -* const int* const -> Константен указател към константен *int*. Нито можем да пренасочваме нито да променяме стойността.
\ No newline at end of file +* const int\* -> Указател, който сочи към константен *int*. Можем да пренасочваме, но не можем да променяме стойността. +* int const\* -> Абсолютно същото. +* int \* const -> Константен указател, не можем да го пренасочваме. +* const int\* const -> Константен указател към константен *int*. Нито можем да пренасочваме нито да променяме стойността. diff --git a/Seminar02/README.md b/Seminar02/README.md index 97d7a86..d1779b2 100644 --- a/Seminar02/README.md +++ b/Seminar02/README.md @@ -1,10 +1,10 @@ # Задачи за стекове 12.10.22 ## Задача първа -Имаме низ състоящ се от английски букви и символа *. Всеки път когато видим * трябва да премахнем символът отляво както и самата звезда. +Имаме низ състоящ се от английски букви и символа \*. Всеки път когато видим \* трябва да премахнем символът отляво както и самата звезда. Пример: -* aa* -> "a" -* erase***** -> "" +* aa\* -> "a" +* erase\*\*\*\*\* -> "" * thi\*s i\*s exampl\*e -> "ths s exampe" Задачата да се реализира за линейно време с линейна памет. @@ -69,15 +69,15 @@ void monotonicStackExample(const std::vector<int>& v) { Защо това е вярно? Нека -$$ -arr = [a_1 \dots a_k \dots a_m \dots a_n] -$$ -и нека в цикъла i = m. Твърдя, че ако a_k е първото по - голямо число от a_m то ще бъде задължително на върха на стека след приключване на while цикъла. +$$arr = [a_1 \dots a_k \dots a_m \dots a_n]$$ -Ако има други числа на върха на стека, то по предположение те са по - малки от a_m и ще бъдат премахнати. +и нека в цикъла i = m. Твърдя, че ако $a_k$ е първото по - голямо число от $a_m$ то ще бъде задължително на върха на стека след приключване на while цикъла. -Ами ако a_k не е в стека? Ако то не е в стека то някое по - голямо от него число, намиращо се между a_k и a_m, го е премахнало. Тоест то не е първото по - голямо число намиращо се вляво. + +Ако има други числа на върха на стека, то по предположение те са по - малки от $a_m$ и ще бъдат премахнати. + +Ами ако $a_k$ не е в стека? Ако то не е в стека то някое по - голямо от него число, намиращо се между $a_k$ и $a_m$, го е премахнало. Тоест то не е първото по - голямо число намиращо се вляво. ## Бонус задачи 1. Да се сортира стек като използвате само един допълнителен стек. diff --git a/Seminar03/README.md b/Seminar03/README.md index 3dad403..dc36329 100644 --- a/Seminar03/README.md +++ b/Seminar03/README.md @@ -138,7 +138,7 @@ int main() { } ``` Тук компилаторът няма да ни се скара, понеже maxSize задължително трябва да е константа. -Едно хубаво свойство на нетиповите параметри е, че staticArray<T, 50> и staticArray<T, 100> са два различни типа. +Едно хубаво свойство на нетиповите параметри е, че `staticArray<T, 50>` и `staticArray<T, 100>` са два различни типа. # Задачи diff --git a/Seminar04/README.md b/Seminar04/README.md index 18eae0d..eb35f37 100644 --- a/Seminar04/README.md +++ b/Seminar04/README.md @@ -64,7 +64,7 @@ class DynArray { size_t capacity = 8; }; ``` -Нека в началото *capacity* да е 8. Кога ще имаме алокация? Първо, ще алокираме памет при създаването на масива. След това, когато се опитаме да добавим девети елемент ще алокираме 16 * sizeof(T) байта. След това когато се опитаме да добавим седемнадесети елемент ще алокираме 32 * sizeof(T) байта. +Нека в началото *capacity* да е 8. Кога ще имаме алокация? Първо, ще алокираме памет при създаването на масива. След това, когато се опитаме да добавим девети елемент ще алокираме 16 \* sizeof(T) байта. След това когато се опитаме да добавим седемнадесети елемент ще алокираме 32 \* sizeof(T) байта. Тоест, когато въртя for цикъла във втория пример, всеки път когато *i = 2^k + 1* ще имам алокация на памет. Това за наше щастие има как да се оправи. Когато знаем колко елемента ще пазим в динамичния масив можем да извикаме *resize(size_t)*. @@ -99,7 +99,7 @@ std::vector<int> filter(const std::vector<int>& arr, bool (*pred)(int)) { ``` ## Сложността на push_back -До момента разгледахме няколко структури от данни и се стремяхме главно към O(1) сложност. Виждоме, че push_back() е доста използвана операция, каква е нейната сложност? Ами тя доста често е константна, но когато ни се наложи да направим алокация е линейна. Тоест, в най - лошия случай тя е O(n). Но това не е достатъчно добра оценка. +До момента разгледахме няколко структури от данни и се стремяхме главно към O(1) сложност. Виждаме, че push_back() е доста използвана операция, каква е нейната сложност? Ами тя доста често е константна, но когато ни се наложи да направим алокация е линейна. Тоест, в най - лошия случай тя е O(n). Но това не е достатъчно добра оценка. В случая worst case сложността е твърде песимистична. Интересува ни по - скоро колко време ще отнеме цялото изпълнение на алгоритъма използващ push_back() а не колко време ще отнеме на 17 стъпка, примерно. Приемаме по - оптимистичен подход - нека не се интересуваме от това колко е най - лошия случай на операцията а да разгледаме колко време ни отнема да направим всички операции. Това неформално е целта на така наречения амортизиран анализ на сложност. С него няма да се занимаваме особено много, но е добре да го знаем от обща култура. @@ -187,9 +187,11 @@ LL = 5 -> 4 -> 3 -> 2 -> 1 Да се напише функция, която открива средата на свързан списък. Пример: +``` 1 -> 2 -> 3 -> 4 -> 5 => 3 1 -> 2 -> 3 -> 4 => 3 +``` ## Задача четвърта да се напише функция която пренарежда свързания списък по следния начин: @@ -204,4 +206,4 @@ LL = 1 -> 5 -> 2 -> 4 -> 3 * Имате празен списък * Имате списък с един елемент * Имате списък и четни елементи -* Имате списък с нечетни елементи
\ No newline at end of file +* Имате списък с нечетни елементи diff --git a/Seminar05/README.md b/Seminar05/README.md index 865e784..1a493aa 100644 --- a/Seminar05/README.md +++ b/Seminar05/README.md @@ -88,7 +88,7 @@ Iterator operator++(int) { ``` Напомняме, че разликата между префиксния и постфиксния operator++ е просто един "служебен" int като аргумент на постфиксния. -Супер, вече можем да се движим, но това движение става безполезно ако нямаме начин да видим каква информация имаме в currentNode. Тази задача по конвенция се изпълнява от operator*. Супер, нека го предефинираме: +Супер, вече можем да се движим, но това движение става безполезно ако нямаме начин да видим каква информация имаме в currentNode. Тази задача по конвенция се изпълнява от operator\*. Супер, нека го предефинираме: ```cpp T& operator*() { return currentNode->data; @@ -288,7 +288,7 @@ int main() { ## Задача първа: -Даден е списък l с n елемента. +Даден е списък `l` с `n` елемента. Да се дефинира функция shuffle(), която получава **адреса на първия елемент на списъка**. @@ -339,4 +339,4 @@ LinkedListNode<int>* partition(LinkedListNode<int>* head, int x); **HINT:** -Можете да си направите два указателя, чрез които да строите някакви полезни помощни списъци. После, след като построите тези полезни помощни списъци, можете да пренасочите правилните указатели и да получите коректен отговор.
\ No newline at end of file +Можете да си направите два указателя, чрез които да строите някакви полезни помощни списъци. После, след като построите тези полезни помощни списъци, можете да пренасочите правилните указатели и да получите коректен отговор. diff --git a/Seminar06/README.md b/Seminar06/README.md index bfd184f..e333dc2 100644 --- a/Seminar06/README.md +++ b/Seminar06/README.md @@ -31,7 +31,7 @@ void insertSorted(std::list<T>& ll, const T& elem); която приема сортиран свързан списък и добавя *elem* на правилното място. ## Задача втора -Нека имаме двусвързан списък от опашки и символи. Във всяка опашка от налачото към края е записан валиден израз в обратна полска нотация. Операторите са [+, -, *, /] а операндите за улеснение са **едноцифрени числа**. +Нека имаме двусвързан списък от опашки и символи. Във всяка опашка от началото към края е записан валиден израз в обратна полска нотация. Операторите са [+, -, \*, /] а операндите за улеснение са **едноцифрени числа**. Да се напише функция, която приема такъв списък, изчислява стойността на всички опашки, и връща дали списъка от резултатите е симетричен спрямо средата. @@ -70,4 +70,4 @@ void insertSorted(std::list<T>& ll, const T& elem); Вход: ``` 1 -> 2 -> 3 ``` -Изход: false
\ No newline at end of file +Изход: false diff --git a/Seminar08/README.md b/Seminar08/README.md index 2243f09..a61b2bd 100644 --- a/Seminar08/README.md +++ b/Seminar08/README.md @@ -39,7 +39,7 @@ Има ли долна граница за височината на дърво? Ако нищо не знаем за дървото, то височината му най - малко може да е 1. Това се случва когато всички възли, различни от корена, са негови наследници (т.е. когато нямаме вътрешни върхове). Доста често обаче, когато работим с дървета, знаем тяхната разклоненост. -Нека имаме дърво с *n* върха и разклоненост *k*. Тогава то не може да стане по - ниско от floor(log<sub>k</sub>(n)). +Нека имаме дърво с *n* върха и разклоненост *k*. Тогава то не може да стане по - ниско от $\lfloor \log_k n \rfloor$. Опитайте се да начертаете дърво с разклоненост 2 и 8 върха. Възможно ли е височината му да е по - малка от 3? @@ -86,7 +86,7 @@ struct Tree { } ``` -От тук всеки указател от типа *Tree\<T>** е валидно дърво. Ако то е празно стойността му би била nullptr. Ако е непразно, данните записани във възела му достъпваме чрез data полето, а лявото и дясното поддърво достъпваме съответно чрез left и right. +От тук всеки указател от типа *Tree\<T>\** е валидно дърво. Ако то е празно стойността му би била nullptr. Ако е непразно, данните записани във възела му достъпваме чрез data полето, а лявото и дясното поддърво достъпваме съответно чрез left и right. Дървото от пример 1, представено по този начин, би изглеждало така: @@ -181,10 +181,10 @@ struct Tree { Да се реализира функция, която добавя елемент в двоично наредено дърво. ## Задача втора -Да се напише функция *contains(Tree\<T>* tree, const T& elem)* която връща дали елемента се съдържа в двоично дърво. Да се реализира същата функция за двоично наредено дърво. +Да се напише функция *contains(Tree\<T>\* tree, const T& elem)* която връща дали елемента се съдържа в двоично дърво. Да се реализира същата функция за двоично наредено дърво. ## Задача трета -Да се напише функция *max((Tree\<T>* tree)* която връща най - големия елемент в двоично дърво. Да се реализира същата функция за двоично наредено дърво. Да се реализира функция намираща най - малкия елемент. +Да се напише функция *max((Tree\<T>\* tree)* която връща най - големия елемент в двоично дърво. Да се реализира същата функция за двоично наредено дърво. Да се реализира функция намираща най - малкия елемент. ## Задача четвърта Да се реализира функция, която връща височината на двоично дърво. diff --git a/Seminar09/README.md b/Seminar09/README.md index 92a4fdb..617232d 100644 --- a/Seminar09/README.md +++ b/Seminar09/README.md @@ -20,7 +20,7 @@ T maxElem(const Tree<T>* t) { Това решение работи почти винаги. Какво става ако t e nullptr? Ако знаехме, че дървото е от тип int, лесно можехме да връщаме INT_MAX, но в случая не знаем какъв тип е скрит зад Т. -Един от начините да се справим с този проблем е да връщаме T* вместо Т. Така, ако дървото е nullptr ние също ще връщаме nullptr. Това е валиден начин, и може би стандартния начин по който бихме подходили в С или във версиите на С++ преди С++17. +Един от начините да се справим с този проблем е да връщаме T\* вместо Т. Така, ако дървото е nullptr ние също ще връщаме nullptr. Това е валиден начин, и може би стандартния начин по който бихме подходили в С или във версиите на С++ преди С++17. ## std::optional\<T> std::optional\<T> е клас който в някакъв смисъл "обвива" елемент от тип Т. Както името подсказва той или има стойност.. или няма. @@ -71,7 +71,7 @@ int main() { Също така дърво ще наричаме **идеално балансирано** ако за всеки негов възел броя на възлите в лявото поддърво и броя на възлите в дясното поддърво се различават най - много с единица. -В балансираните и идеално балансираните дървета сложността на операциите добавяне, търсене, изтриване стават O(log<sub>2</sub>(n)). +В балансираните и идеално балансираните дървета сложността на операциите добавяне, търсене, изтриване стават $\mathcal{O} (\log_2 n)$. С балансирани дървета ще се занимаваме много подробно следващия път. Засега само дефинираме неща, които ще ни бъдат от голяма полза при реализацията на едно такова дърво. @@ -105,11 +105,15 @@ int main() { Да се реализират ляво и дясна ротация за произволно дърво. ## Задача шеста (За домашно) -Нека имаме двоично наредено дърво T от цели числа. +Нека имаме двоично наредено дърво T от цели числа. + +``` pred(x) = max{y | y е ключ на възел в Т и y < x} // Предшественик succ(x) = min{y | y е ключ на възел в T и y > x} // Наследник +``` + Да се реализира pred(t, x) и succ(t, x). Бележка: Не е нужно х да се съдържа в дървото. ## Задача седма (Контролно) -Да се напише функция, която намира сумата на всички елементи на двоично наредено дърво от цели числа, които имат нечетен брой наследници в интервала [x, y] (т.е. сумата на броя елементи в лявото поддърво и броя елементи в дясното поддърво в интервала [x, y] е нечетно число). Нека x и y са параметри на функцията.
\ No newline at end of file +Да се напише функция, която намира сумата на всички елементи на двоично наредено дърво от цели числа, които имат нечетен брой наследници в интервала [x, y] (т.е. сумата на броя елементи в лявото поддърво и броя елементи в дясното поддърво в интервала [x, y] е нечетно число). Нека x и y са параметри на функцията. diff --git a/Seminar11/README.md b/Seminar11/README.md index 7f28913..8ef5c54 100644 --- a/Seminar11/README.md +++ b/Seminar11/README.md @@ -30,7 +30,7 @@ ### Тегловен граф -Често е полезно да "закачим" някакви тегла на ребрата. Примерно, ако върходете на графа представляват някакви забележителности, а ребрата са пътища между тях, теглата могат да представляват времето в минути, което отнема да стигнем от v1 до v2. +Често е полезно да "закачим" някакви тегла на ребрата. Примерно, ако върховете на графа представляват някакви забележителности, а ребрата са пътища между тях, теглата могат да представляват времето в минути, което отнема да стигнем от v1 до v2. Примерно, това е **рисунка** на тегловен граф: @@ -143,6 +143,7 @@ matrix[7][5] = false Предимството на тази реализация е, че бързо можем да добавяме върхове и ребра. Също така паметта ни вече не е O(|V|^2) а O(|V| + |E|). Част от материалите са взаимствани от [тук](https://github.com/MariaGrozdeva/Data_structures_and_algorithms_FMI/tree/main/Sem_10) +<<<<<<< HEAD ## BFS Breadth-first search е сравнително прост алгоритъм за търсене в граф, но върху идеята, която той носи се базират други алгоритми върху графи. Примерно, алгоритъмът на Прим за минимално покриващо дърво, алгоритъмът на Dijkstra за най - къси пътища се базират на идеи подобни на тази на BFS. |
