Files
uni/year2/semester2/CT2109/Assignments/Assignment-03/latex/CT2109-Assignment-03.tex

215 lines
14 KiB
TeX

\documentclass[a4paper]{article}
\usepackage{listings}
\usepackage{xcolor}
\definecolor{codegreen}{rgb}{0,0.6,0}
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
\definecolor{codeorange}{rgb}{1,0.49,0}
\definecolor{backcolour}{rgb}{0.95,0.95,0.96}
\usepackage{pgfplots}
\pgfplotsset{width=\textwidth,compat=1.9}
\lstdefinestyle{mystyle}{
backgroundcolor=\color{backcolour},
commentstyle=\color{codegray},
keywordstyle=\color{codeorange},
numberstyle=\tiny\color{codegray},
stringstyle=\color{codegreen},
basicstyle=\ttfamily\footnotesize,
breakatwhitespace=false,
breaklines=true,
captionpos=b,
keepspaces=true,
numbers=left,
numbersep=5pt,
showspaces=false,
showstringspaces=false,
showtabs=false,
tabsize=2,
xleftmargin=10pt,
}
\lstset{style=mystyle}
\input{head}
\begin{document}
%-------------------------------
% TITLE SECTION
%-------------------------------
\fancyhead[C]{}
\hrule \medskip % Upper rule
\begin{minipage}{0.295\textwidth}
\begin{raggedright}
\footnotesize
Andrew Hayes \hfill\\
21321503 \hfill\\
a.hayes18@nuigalway.ie
\end{raggedright}
\end{minipage}
\begin{minipage}{0.4\textwidth}
\begin{centering}
\large
CT2109 Assignment 3\\
\normalsize
\end{centering}
\end{minipage}
\begin{minipage}{0.295\textwidth}
\begin{raggedleft}
\footnotesize
\today \hfill\\
\end{raggedleft}
\end{minipage}
\medskip\hrule
\medskip
\begin{centering}
Double-Base Palindromes \& Complexity Analysis\\
\end{centering}
\medskip\hrule
\bigskip
\section{Problem Statement}
The overall purpose of this assignment is to perform some basic complexity analysis on four distinct algorithms, each of which aim to achieve the same result: determining whether a String is palindromic.
More specifically, each algorithm will be fed the binary \& decimal representations of each integer between $0_{10}$ and $1,000,000_{10}$, in String form.
Each algorithm will then determine whether the String is palindromic, returning either \verb|true| or \verb|false|.
Each method will have three counter variables to record the number of palindromes found: one for the numbers whose decimal form is palindromic, one for the numbers whose binary form is palindromic, and one
for the numbers which are palindromic in both decimal \& binary form.
\\\\
The number of primitive operations will be recorded using a separate global variable for each method.
Additionally, the real time taken by each method (in milliseconds) will be recorded.
These measurements will form the basis of the complexity analysis performed.
The number of primitive operations will be recorded for each method and plotted against the size of the problem (the number of values being checked for palindromicity, $n$).
This graph should reflect the ``order'' of the problem, i.e. it should match the big-O representation derived from a time-complexity analysis of the algorithm.
For example, an exponential curve on the graph would indicate that the algorithm is of $O(n^2)$ complexity, while a straight line through the origin would indicate that the algorithm is of $O(n)$ complexity.
\section{Analysis \& Design Notes}
The first thing that we'll want to do in this program is declare the variables that we'll use to record the data for each method being tested.
We will have seven arrays of size four, with one index for each method being tested.
These arrays will be:
\begin{itemize}
\item \verb|long[] operations| - To count the number of primitive operations for each method.
\item \verb|int[] decCount| - To count how many numbers that are palindromic in decimal form are found using each method.
\item \verb|int[] binCount| - To count how many numbers that are palindromic in binary form are found using each method.
\item \verb|int[] bothCount| - To count how many numbers that are palindromic in both decimal \& binary form are found using each method.
\item \verb|long[] startTime| - To record the start time (in Unix epoch form) of the testing of each of the four methods.
\item \verb|long[] totalTime| - To record the total time (in milliseconds) by the testing of each of the four methods.
\item \verb|StringBuilder[] data| - This will be be used to create a String of CSV data for each method, which will be output to a \verb|.csv| file at the end of testing.
Doing this saves us from having to open, write to, \& close a \verb|.csv| file every time we want to record some data, which would be very inefficient.
\end{itemize}
The first thing that we'll do in the main method is initialise all the indices in the \verb|StringBuilder[] data| array to have the appropriate headings from each column, one column for the number of primitive operations
and one for the size of the problem that used that number of primitive operations, i.e. \verb|"operations,size\n"|.
\\\\
We'll also want to generate a two-dimensional array of all the Strings that we'll be testing for palindromicity, with one dimension for decimal Strings and one dimension for binary Strings, both going up to
$1,000,000_{10}$.
Generating this once in the beginning will save us from having to re-generate the same Strings four times, which would be very inefficient.
\\\\
Each method that we will be testing will have its own class that implements the interface \verb|PalindromeChecker|.
This interface will contain a single method with the signature \verb|public boolean checkPalindrome(String str)|.
The reason for doing this will be to follow OOP principles \& the DRY principle so that we don't have unnecessary repetition of code.
This will allow us to have one generic loop through each of the numbers from $0_{10}$ to $1,000,000_{10}$ instead of four separate ones.
Each of the methods that we will be testing will be overridden implementations of \verb|checkPalindrome|.
We will have four classes that implement \verb|PalindromeChecker|:
\begin{itemize}
\item \verb|ReverseVSOriginal| - This class will contain ``Method One'' outlined in the assignment specification, which checks if a String is palindromic by comparing the String to a reversed copy of itself, hence the name.
\item \verb|IVersusIMinusN| - This will contain ``Method Two'' outlined in the assignment specification, which checks if a String is palindromic by looping through each character in the String using an iterator
\verb|i| and comparing the character at index \verb|i| to the character at index \verb|n-i|, where \verb|n| is the last index in the String, i.e., comparing the first character to the last, the second character to the second-last, etc.
\item \verb|StackVSQueue| - This will contain ``Method Three'' outlined in the assignment specification, which checks if a String is palindromic using essentially the same technique as ``Method Two''
but instead of simply iterating through the String, each character of the String will be put onto both a Stack \& a Queue, and then items will be removed from the Stack \& Queue and compared to each other.
The LIFO nature of the Stack and the FIFO nature of the Queue result in this comparison being first character versus last, second character versus second-last, and so on.
\item \verb|RecursiveReverse| - This will contain ``Method Four'' outlined in the assignment specification, which checks if a String is palindromic using essentially the same technique as ``Method One''
but using recursion to reverse the String instead of iteration.
\end{itemize}
An array called \verb|PalindromeCheckers[]| will be initialised to contain an instance of each of these four classes.
This array will be iterated over (using iterator \verb|j|) to test each method, which prevents code repetition.
In said loop, firstly, the \verb|startTime[j]| will be recorded using \verb|System.getCurrentTimeMillis();|.
Then, a loop will be entered that iterators over each number between $0_{10}$ to $1,000,000_{10}$.
Both the decimal \& binary Strings at index \verb|i| of the \verb|strings[]| array will be passed to \verb|palindromeCheckers[j].checkPalindrome()| to be checked for palindromicity.
Then, \verb|decCount[j]|, \verb|binCount[j]|, \& \verb|bothCount[j]| will be iterated or will remain unchanged, as appropriate.
\\\\
The count of primitive operations for each method will be iterated as they are executed.
I won't count the \verb|return| statements at the end of methods or accessing a variable or array index as primitive operations, as these (especially the \verb|return| statements) are computationally
insignificant, and certainly aren't on the same level as something like creating a new variable in memory.
Unfortunately, it's not really possible to say with accuracy what is \& isn't a ``primitive operation'' in a language so high-level \& abstract as Java, but I feel that this is a reasonable approximation.
We want to record the count of primitive operations at regular intervals of 50,000 during the process, so we will append the operations count and the current \verb|i| to \verb|data[j]| if \verb|i| is
divisible by 50,000.
\\\\
Once the loop using iterator \verb|i| has ended, the total time taken will be recorded by subtracting the current time from the start time.
The number of palindromes found in decimal, binary, and both decimal \& binary will be printed out to the screen, along with the total time taken \& the total number of primitive operations for that method.
Finally, the data for that method will be written to a \verb|.csv| file.
This will be repeated for all four methods in the \verb|palindromeCheckers| array.
\section{Code}
\lstinputlisting[language=Java, breaklines=true, title={NewPalindrome.java}]{../code/NewPalindrome.java}
\lstinputlisting[language=Java, breaklines=true, title={PalindromeChecker.java}]{../code/PalindromeChecker.java}
\lstinputlisting[language=Java, breaklines=true, title={ReverseVSOriginal.java}]{../code/ReverseVSOriginal.java}
\lstinputlisting[language=Java, breaklines=true, title={IVersusIMinusN.java}]{../code/IVersusNMinusI.java}
\lstinputlisting[language=Java, breaklines=true, title={StackVSQueue.java}]{../code/StackVSQueue.java}
\lstinputlisting[language=Java, breaklines=true, title={RecursiveReverse.java}]{../code/RecursiveReverse.java}
\newpage
\section{Testing}
\begin{center}
\begin{tikzpicture}
\begin{axis}[
title={Number of Primitive Operations per Method},
xlabel={Size of Problem},
ylabel={Number of Operations}
]
\addplot table [x=size, y=operations, col sep=comma] {../code/method0.csv};
\addplot table [x=size, y=operations, col sep=comma] {../code/method1.csv};
\addplot table [x=size, y=operations, col sep=comma] {../code/method2.csv};
\addplot table [x=size, y=operations, col sep=comma] {../code/method3.csv};
\legend{Iterative Reverse vs Original,$i$ vs $n-i$,Stack vs Queue,Recursive Reverse vs Original}
\end{axis}
\end{tikzpicture}
\end{center}
\begin{figure}[h]
\centering
\includegraphics[width=0.7\textwidth]{images/output.png}
\caption{Output of the Main Method}
\end{figure}
The output from the program shows that all the methods agreed on the number of palindromes found in each category, which shows us
that they did indeed work as intended.
\\\\
We can see from the graph that $i$ versus $n-i$ or \verb|IVersusNMinusI| method was the most efficient, as it used the fewest
primitive operations out of the four methods (37,839,177) and the complexity grew relatively slowly as the size of the problem increased,
demonstrated by the fact that its curve has quite a gentle slope.
This is reflected in the testing times as it was by far the fastest method (referred to in the screenshot as ``Method 1'', as
indexing was done from 0), taking only 39 milliseconds to complete.
This makes sense, as the method consisted of just one loop and some basic operations, without using any fancy data structures.
Furthermore, this method quickly detects non-palindromic Strings, and returns \verb|false|, saving computation.
\\\\
The next most efficient method was the iterative reverse versus original or \verb|ReverseVSOriginal| method.
Despite being the next most efficient method, it took almost ten times the time that \verb|IVersusNMinusI| took to complete, taking 366
milliseconds, which, all things considered, is still quite fast.
The number of primitive operations and the rate of growth of this method were also accordingly higher than the previous method.
\\\\
The third most efficient method was the recursive reverse versus original or \verb|RecursiveReverse| method.
This makes sense, as it uses a very similar approach to the second most efficient method, but instead did it recursively.
Despite this solution appearing somewhat more elegant from a lines of code perspective, it was in practice less efficient than
its iterative counterpart, both in terms of memory (as the recursive function calls had to remain in memory until all the sub-calls
were complete) and in terms of operations, taking approximately 20 million more primitive operations to complete the same task
as the iterative approach.
It was also significantly slower than its iterative counterpart, taking around 200 milliseconds more to complete the task.
We can also tell from looking at the graph that at low problem sizes that this approach is comparable \& rather similar in terms
of efficiency to the iterative approach, but they quickly diverge, with the recursive approach having a significantly steeper
slope.
We can extrapolate from this that this method would be even less efficient at very large problem sizes, as its rate of growth
is quite large.
\\\\
By far the least efficient method was the Stack versus Queue or \verb|StackVSQueue| method.
It took by far the greatest number of primitive operations, and the rate of growth was ridiculously large, rapidly diverging from
the other four techniques.
Its rate of growth is so large that it would likely quickly become unusable for any significantly large problem.
This is reinforced by the fact that it took 3,519 milliseconds to complete the task, being the only method that took more than one
second to do so, and taking almost 100 times what the best-performing method took.
\end{document}