diff --git a/assignments/A1/a1.tex b/assignments/A1/a1.tex index 9d651a3..d20a0b1 100644 --- a/assignments/A1/a1.tex +++ b/assignments/A1/a1.tex @@ -2,9 +2,11 @@ \usepackage{amsmath} \usepackage[utf8]{inputenc} \usepackage[margin=0.75in]{geometry} +\usepackage{amsfonts} +\usepackage{amssymb} \title{CSC111 Assignment 1: Linked Lists} -\author{Azalea Gui \& Peter Lin} +\author{Azalea Gui and Peter Lin} \date{\today} \begin{document} @@ -26,14 +28,27 @@ Do \textbf{not} include your solution in this file. \begin{enumerate} \item[(a)] - TODO: Write your answer here. + Let $n, m \in \mathbb Z^+$. Because \texttt{\_\_contains\_\_} doesn't mutate the \texttt{LinkedList}, each function call is independent from each other. Therefore, we can just find the total running time of one call to \texttt{\_\_contains\_\_} and multiply it with $m$.\\ + Because $n - 1$ is the $n^{th}$ element in the \texttt{LinkedList}, the loop must iterate $n$ times. Everything else other than the loop are constant time operations and can be counted as 1. + So the total running time is $m(n + 1) = nm + m$ or $\Theta(nm + m)$. \item[(b)] - TODO: Write your answer here. + The first search operation moves the element to the front, and because that's the only one being searched for, it will stay there for the rest of the searches. So the first search takes $n$ iterations and some constant time, while the rest of the searches are constant time because it will be the first element. Therefore the total running time is $n + m + 1$ or $\Theta(n + m)$.\\ + + The second search operation moves the element closer to the beginning by 1 element. So if $m < n$, the number of loop iterations is $n + (n - 1) + ... + (n - m + 1) = \frac{m(2n - m + 1)}{2}$, adding constant time operations to be a total running time of $\frac{m(2n - m + 3)}{2}$ or $\Theta(m(2n - m))$. Because $n < 2n - m \leq 2n$, this is also $\Theta(mn)$.\\ + + If $m \geq n$, then there will be constant time searches as the element will already be at the very beginning. So the total number of loop iterations is $\frac{n(n + 1)}{2}$, adding constant time operations to get a total running time of $\frac{n(n + 1)}{2} + m$ or $\Theta(n^2 + m)$.\\ + + The last search operation is equivalent to the first. Because all elements start with a count of 0, the first search will bring the element to the beginning with $n$ loop iterations, and every other search becomes constant time. Therefore the answer is the same as the first search operation, having a total running time of $n + m + 1$ or $\Theta(n + m)$. \end{enumerate} \item[4.] -TODO: Write your answer here. +Assume $n > 3$. Also assume $m > n$. +Consider a sequence $[n - 1, n - 2, ..., 0, n - 1, n - 2, ...]$. Each time a search is performed in the \texttt{MoveToFrontLinkedList}, it must iterate to the last element each time. However, the \texttt{SwapLinkedList} does not have to do that. The first 2 times it will access the $n^{th}$ element, however the next 2 times it will access the $n-2^{th}$, and the next 2 will access the $n-4^{th}$, etc.\\ + +Thus the total running time for \texttt{MoveToFrontLinkedList} is $T_1 = mn$ and the total running time for \texttt{SwapLinkedList} is $T_2 = n + n + (n - 2) + (n - 2) + ...$. Then $T_1 - T_2 = 0 + 0 + 2 + 2 + 4 + 4 + ... + 0 + 0 + 2 + ...$. Each element of this sum is less than $n$, and there are $m$ elements in the sum. Thus $T_1 - T_2 \in \mathcal O(mn)$.\\ + +To find a lower bound, first find the running time for searching $n$ elements. This would be $0 + 0 + 2 + 2 + ... + n - 4 + n - 4 + n - 2$ if $n$ is odd, and $0 + 0 + 2 + 2 + ... + n - 2 + n - 2$ if $n$ is even. If $n$ is even, then the sum is $n\frac{n - 2}{2}$. If $n$ is odd, then $n - 1$ is even, therefore $(n - 1)\frac{n - 3}{2}$ is a lower bound. Let $L = (n - 1)\frac{n - 3}{2}$ be this lower bound. Now consider a sequence very similar to $T_1 - T_2$, $T_3 = 0 + 0 + ... + L + 0 + 0 + ... + L + 0 + ...$, where the first $n - 1$ elements are 0 and the next element is $L$, and then it repeats. The partial sum of $T_3$ is smaller or equal to the partial sum for $T_2 - T_1$, so this is a lower bound for $T_1 - T_2$. Also, $T_3 \in \Omega (m\frac{L}{n})$, because $m > n$, a constant factor of $\frac{1}{2}$ can be chosen, and then $T_3$ will always be greater or equal to $m\frac{L}{2n}$. So $T_1 - T_2 \in \Omega (m\frac{L}{n}) = \Omega (m\frac{(n - 1)(n - 3)}{n} = \Omega (mn)$. Therefore $T_1 - T_2 \in \Theta (mn)$. \end{enumerate} diff --git a/assignments/A1/a1_part1.py b/assignments/A1/a1_part1.py index 7202a13..b90fa25 100644 --- a/assignments/A1/a1_part1.py +++ b/assignments/A1/a1_part1.py @@ -167,7 +167,6 @@ class CountLinkedList(LinkedList): cur = cur.next cur.next = _CountNode(item, cur.next, count) # insert node - print(count) return True def append(self, item: Any) -> None: diff --git a/assignments/A1/a1_part1_test.py b/assignments/A1/a1_part1_test.py index efaceb1..27db764 100644 --- a/assignments/A1/a1_part1_test.py +++ b/assignments/A1/a1_part1_test.py @@ -28,75 +28,108 @@ import pytest from a1_part1 import MoveToFrontLinkedList, SwapLinkedList, CountLinkedList -def test_all_lists_true() -> None: - movetofront = [MoveToFrontLinkedList(range(i)) for i in range(100)] - swap = [SwapLinkedList(range(i)) for i in range(100)] - count = [CountLinkedList(range(i)) for i in range(100)] - assert(all(all(x in movetofront[i] for x in range(i)) for i in range(100))) - assert(all(all(x in swap[i] for x in range(i)) for i in range(100))) - assert(all(all(x in count[i] for x in range(i)) for i in range(100))) +def test_movetofront_true() -> None: + """Test MoveToFrontLinkedList contains with elements in the linked list""" + lst = [MoveToFrontLinkedList(range(i)) for i in range(100)] + assert(all(all(x in lst[i] for x in range(i)) for i in range(100))) -def test_all_lists_false() -> None: - movetofront = [MoveToFrontLinkedList(range(i)) for i in range(100)] - swap = [SwapLinkedList(range(i)) for i in range(100)] - count = [CountLinkedList(range(i)) for i in range(100)] - assert(all(all(x not in movetofront[i] for x in range(i, i + 100)) for i in range(100))) - assert(all(all(x not in swap[i] for x in range(i, i + 100)) for i in range(100))) - assert(all(all(x not in count[i] for x in range(i, i + 100)) for i in range(100))) +def test_swap_true() -> None: + """Test SwapLinkedList contains with elements in the linked list""" + lst = [SwapLinkedList(range(i)) for i in range(100)] + assert(all(all(x in lst[i] for x in range(i)) for i in range(100))) -def generate_ordering(n: int): - """generates a pseudorandom ordering of n integers from 0 to n-1, inclusive +def test_count_true() -> None: + """Test CountLinkedList contains with elements in the linked list""" + lst = [CountLinkedList(range(i)) for i in range(100)] + assert(all(all(x in lst[i] for x in range(i)) for i in range(100))) + + +def test_movetofront_false() -> None: + """Test MoveToFrontLinkedList contains with elements not in the linked list""" + lst = [MoveToFrontLinkedList(range(i)) for i in range(100)] + assert(all(all(x not in lst[i] for x in range(i, i + 100)) for i in range(100))) + + +def test_swap_false() -> None: + """Test SwapLinkedList contains with elements not in the linked list""" + lst = [SwapLinkedList(range(i)) for i in range(100)] + assert(all(all(x not in lst[i] for x in range(i, i + 100)) for i in range(100))) + + +def test_count_false() -> None: + """Test CountLinkedList contains with elements not in the linked list""" + lst = [CountLinkedList(range(i)) for i in range(100)] + assert(all(all(x not in lst[i] for x in range(i, i + 100)) for i in range(100))) + + +def generate_ordering(n: int) -> list[int]: + """Generates a pseudorandom ordering of n integers from 0 to n-1, inclusive Preconditions: - n is prime """ arr = [(2 << i) % n for i in range(1, n)] arr.append(0) - assert set(arr) == set(range(n)) + # assert set(arr) == set(range(n)) return arr def test_movetofront_mutation() -> None: + """Tests the mutation of a MoveToFrontLinkedList by searching for every number from 0-100.""" lst = MoveToFrontLinkedList(range(101)) arr = list(range(101)) operations = generate_ordering(101) + bools = [] # test results of mutation after each iteration for x in operations: i = arr.index(x) # find index arr.insert(0, arr.pop(i)) # simulate moving to front - x in lst - assert lst.to_list() == arr + lst.__contains__(x) + bools.append(lst.to_list() == arr) + assert all(bools) def test_swap_mutation() -> None: + """Tests the mutation of a SwapLinkedList by searching for every number from 0-100.""" lst = SwapLinkedList(range(101)) arr = list(range(101)) operations = generate_ordering(101) + bools = [] # test results of mutation after each iteration for x in operations: i = arr.index(x) # find index if i > 0: # simulate swapping temp = arr[i] arr[i] = arr[i - 1] arr[i - 1] = temp - x in lst - assert lst.to_list() == arr + lst.__contains__(x) + bools.append(lst.to_list() == arr) + assert all(bools) def test_count_mutation() -> None: + """Tests the mutation of a CountLinkedList by searching for every number from 0-100, + and then every number from 0-49 inclusive.""" lst = CountLinkedList(generate_ordering(101)) - operations = list(range(101)) - for x in operations: - x in lst - assert lst.to_list() == operations + for x in list(range(101)): # reorder the linked list to natural ordering + lst.__contains__(x) - for x in reversed(range(50)): - x in lst + for x in reversed(range(50)): # reverse the first 50 numbers + lst.__contains__(x) assert lst.to_list() == list(reversed(range(50))) + list(range(50, 101)) if __name__ == '__main__': pytest.main(['a1_part1_test.py', '-v']) + + import python_ta + + python_ta.check_all(config={ + 'max-line-length': 100, + 'disable': ['E1136'], + 'extra-imports': ['a1_linked_list'], + 'max-nested-blocks': 4 + })