CS 250: Basic Data Structures using C++ (Spring 2024 version)

Table of Contents

\newpage

topic-corecompsci.png

Rachel Wil Sha Singh's Basic Data Structures Course Β© 2024 by Rachel Wil Sha Singh is licensed under CC BY 4.0. To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/

These course documents are written in emacs orgmode and the files can be found here: https://gitlab.com/moosadee/courses

Dedicated to a better world, and those who work to try to create one.

\newpage

l1-contents.png

This section contains each of the units we will be covering in the class, including the reading material, review, and assignment documentation. Add your own notes and highlight things as you go through!

WEEK 1 - JAN 16

topic-setup.png

UNIT 00: Welcome & setup

πŸ“–οΈ Reading - Welcome (U00.READ)

  • Welcome!

    It feels weird to start a collection of notes (or a "textbook") without some sort of welcome, though at the same time I know that people are probably not going to read the introduction (Unless I put some cute art and interesting footnotes, maybe.)

    I think that I will welcome you to my notes by addressing anxiety.

    Belonging

    Unfortunately there is a lot of bias in STEM fields and over decades there has been a narrative that computer science is for a certain type of person - antisocial, nerdy, people who started coding when they were 10 years old.

    Because of this, a lot of people who don't fit this description can be hesitant to get into computers or programming because they don't see people like themselves in media portrayals. Or perhaps previous professors or peers have acted like you're not a real programmer if you didn't start programming as a child

    If you want to learn about coding, then you belong here.

    There are no prerequisites.

    I will say from my own experience, I know developers who fell in love with programming by accident as an adult after having to take a computer class for a different degree. I know developers who are into all sorts of sports, or into photography, or into fashion. There is no specific "type" of programmer. You can be any religion, any gender, any color, from any country, and be a programmer.

    u00_WelcomeAndSetup_gatekeepingbaby.png

    Figure 1: Don't be like Gatekeeper Baby. You can begin coding at any age!

    Challenge

    Programming can be hard sometimes. There are many aspects of learning to write software (or websites, apps, games, etc.) and you will get better at it with practice and with time. But I completely understand the feeling of hitting your head against a wall wondering why won't this work?! and even wondering am I cut out for this?! - Yes, you are.

    u00_WelcomeAndSetup_justwork.png

    I will tell you right now, I have cried over programming assignments, over work, over software. I have taken my laptop with me to a family holiday celebration because I couldn't figure out this program and I had to get it done!!

    All developers struggle. Software is a really unique field. It's very intangible, and there are lots of programming languages, and all sorts of tools, and various techniques. Nobody knows everything, and there's always more to learn.

    Just because something is hard doesn't mean that it is impossible.

    It's completely natural to hit roadblocks. To have to step away from your program and come back to it later with a clear head. It's natural to be confused. It's natural to not know.

    But some skills you will learn to make this process smoother are how to plan out your programs, test and verify your programs, how to phrase what you don't know as a question, how to ask for help. These are all skills that you will build up over time. Even if it feels like you're not making progress, I promise that you are, and hopefully at the end of our class you can look back to the start of it and realize how much you've learned and grown.

    Learning

    First and foremost, I am here to help you learn.

    My teaching style is influenced on all my experiences throughout my learning career, my software engineer career, and my teaching career.

    I have personally met teachers who have tried to scare me away from computers, I've had teachers who really encouraged me, I've had teachers who barely cared, I've had teachers who made class really fun and engaging.

    I've worked professionally in software and web development, and independently making apps and video games. I know what it's like to apply for jobs and work with teams of people and experience a software's development throughout its lifecycle.

    And as a teacher I'm always trying to improve my classes - making the learning resources easily available and accessible, making assignments help build up your knowledge of the topics, trying to give feedback to help you design and write good programs.

    As a teacher, I am not here to trick you with silly questions or decide whether you're a real programmer or not; I am here to help guide you to learn about programming, learn about design, learn about testing, and learn how to teach yourself.

    u00_WelcomeAndSetup_buildingblocks.png

    Roadmap

    u00_WelcomeAndSetup_roadmap.png

    When you're just starting out, it can be hard to know what all you're going to be learning about. I have certainly read course descriptions and just thought to myself "I have no idea what any of that meant, but it's required for my degree, so I guess I'm taking it!"

    Here's kind of my mental map of how the courses I teach work:

    CS 200: Concepts of Programming with C++
    You're learning the language. Think of it like actually learning a human language; I'm teaching you words and the grammar, and at first you're just parroting what I say, but with practice you'll be able to build your own sentences.
    CS 235 Object-Oriented Programming with C++
    You're learning more about software development practices, design, testing, as well as more advanced object oriented concepts.
    CS 250 Basic Data Structures with C++
    You're learning about data, how to store data, how to assess how efficient algorithms are. Data data data.

    In addition to learning about the language itself, I also try to sprinkle in other things I've learned from experience that I think you should know as a software developer (or someone who codes for whatever reason), like

    • How do you validate that what you wrote actually works? (Spoilers: How to write tests, both manual and automated.)
    • What tools can you use to make your programming life easier? (And are used in the professional world?)
    • How do you design a solution given just some requirements?
    • How do you network in the tech field?
    • How do you find jobs?
    • What are some issues facing tech fields today?

    Something to keep in mind is that, if you're studying Computer Science as a degree (e.g., my Bachelor's degree is in Computer Science), technically that field is about "how do computers work?", not about "how do I write software good?" but I still find these topics important to go over.

    u00_WelcomeAndSetup_search.png

    That's all I can really think of to write here. If you have any questions, let me know. Maybe I'll add on here.

    \vspace{0.2cm}


  • What is this weird webpage/"book"?

    In the past I've had all my course content available on the web on separate webpages. However, maintaining the HTML, CSS, and JS for this over time is cumbersome. Throughout 2023 I've been adapting my course content to emacs orgmode documents, which allows me to export the course content to HTML and PDF files. I have a few goals with this:

    1. All course information is in one singular place
    2. At the end of the semester, you can download the entire course's "stuff" in a single PDF file for easy referencing later on
    3. Hopefully throughout this semester I'll get everything "moved over" to orgmode, and in Summer/Fall 2024 I can have a physical textbook printed for the courses, which students can use and take notes in so as to have most of their course stuff in one place as well
    4. It's important to me to make sure that you have access to the course content even once you're not my student anymore - this resource is available publicly online, whether you're in the course or not. You can always reference it later.

    I know a lot of text can be intimidating at first, but hopefully it will be less intimidating as the semester goes and we learn our way around this page.

    \vspace{0.2cm}


  • What are "data structures"?

    hit-computer.png

    A data structure is an object (a class, a struct - some type of structured thing) that holds data. In particular, the entire job of a data structure object is to store data and provide an interface for adding, removing, and accessing that data.

    "Don't all the classes we write hold data? How is this different from other objects I've defined?"

    In the past, you may have written classes to represent objects, like perhaps a book. A book could have member variables like its title, isbn, and year published, and some methods that help us interface with a book…

    Book  
    - title : string
    - isbn : string
    - year : int
    + Setup( ... ) : void
    + Update( ... ) : void
    + Display() : void

    However - this is not a data structure. We could, however, use a data structure to store a list of books:

    BookArray  
    - bookArray : Book[]
    - arraySize : int
    - totalBooks : int
    + AddBook( newBook: Book ) : void
    + Update( index: int, newBook: Book ) : void
    + RemoveBook( index: int ) : void
    + GetBook( index: int ) : Book&
    +DisplayAll() : void
    +GetSize() : int

    A data structure will store a series of some sort of data. Often, it will use an array or a linked list as a base structure and functionality will be built on top.

    Ideally, the user doesn't care about how the data structure works, they just care that they can add, remove, and access data they want to store.

    Ideally, when we are writing a data structure, it should be:

    • Generic, so you could store any data type in
    • Reusable, so that the data structure can be used in many different programs.
    • Robust, offering exception handling to prevent the program from crashing when something goes wrong with it.
    • Encapsulated, handling the inner-workings of dealing with the data, without the user (or other programmers working outside the data structure) having to write special code to perform certain operations.

    The way I try to conceptualize the work I'm doing on a data structure is to pretend that I'm a developer that is going to create and sell a C++ library of data structures that other developers at other companies can use in their own, completely separate, software projects. If I'm selling my data structures package to other businesses, my code should be dependable, stable, efficient, and relatively easy to use.

    c4_u00_data-structures-ad.png

    \vspace{0.2cm}


  • What is "algorithm analysis"?

    c4_u00_complexity.png

    Algorithm Analysis is the process of figuring out how efficient a function is and how it scales over time, given more and more data to operate on.

    This is another important part of dealing with data structures, as different structures will offer different trade offs when it comes to the efficiency of data access functions, data searching functions, data adding functions, and data removal functions.

    Every operation takes a little bit of processing time, and as you iterate over data, that processing time is multiplied by the amount of times you go through a loop.

    Example: Scalability

    Let's say we have a sorting algorithm that, for every \(n\) records, it takes \(n^2\) program units to find an object. If we had 100 records, then it would take \(100^2 = 10,000\) time-units to sort the set of data.

    What the time-units are could vary - an older machine might take longer to execute one instruction, and a newer computer might process an instruction much more quickly, but we think of algorithm complexity in this sort of generic form.

    c4_u00_search-sort-ad.png

    Example: Efficiency of an Array-based structure

    0 1 2 3 4
    "kansas" "missouri" "arkansas" "ohio" "oklahoma"

    In an array-based structure, we have a series of elements in a row, and each element is accessible via its index (its position in the array). Arrays allow for random-access, so accessing element #2 is instant:

    cout << arr[2];
    

    No matter how many elements there are (\(n\)), we can access the element at index 2 without doing any looping. We state that this is \(O(1)\) ("Big-O of 1") time complexity for an access operation on an *array.

    However, if we were searching through the unsorted array for an item, we would have to start at the beginning and look at each item, one at a time, until we either found what we're looking for, or hit the end of the array:

    for ( int i = 0; i < ARR_SIZE; i++ )
    {
        if ( arr[i] == searchTerm )
        {
            return i; // found at this position
        }
    }
    return -1;      // not found
    

    For search, the worst-case scenario is having to look at all elements of the array to ensure what we're looking for isn't there. Given \(n\) items in the array, we have to iterate through the loop \(n\) times. This would end up being \(O(n)\) ("Big-O of \(n\)") time complexity for a search operation on an array.

    We can build our data structures on top of an array, but there is also a type of structure called a linked structure, which offers its own pros and cons to go with it. We will learn more about algorithm analysis and types of structures later on.

    \vspace{0.2cm}


  • Why is there a whole class dedicated to "data structures"?

    Of course, plenty of data structures have already been written and are available out there for you to use. C++ even has the Standard Template Library full of structures already built and optimized!

    "So why are we learning to write data structures if they've already been written for us?"

    While you generally won't be rolling your own linked list for projects or on the job, it is important to know how the inner-workings of these structures operate. Knowing how each structure works, its tradeoffs in efficiency, and how it structures its data will help you choose what structures to use when faced with a design decision for your software.

    c4_u00_which-structure.png


  • Review questions

    Answer these questions in your notes, then check your work by completing the related Concept Intro assignment on Canvas.

    Review questions:

    1. Why should a data structure be generic?
    2. What does it mean for a data structure to be robust?
    3. Most data structures have what kind of core functionality?

πŸ”Ž Concept Intro - Welcome/setup (U00.CIN)

πŸ§‘β€πŸ”¬ Lab - Set up (U00.LAB)

About:
Source control is an important tool in software development. GitLab is a service that allows us to host git repositories. Our own computer will use the git software to interact with the GitLab server.
Goals:
  • Create a GitLab account
  • Set up git software on your computer
  • Clone a repository
  • Make changes to a repository
  • Deal with merge conflicts
  • Create branches and merge requests
Turn-in:
  • 1. Tell me what your GitLab username is using the "❓ Unit 00 Setup - GitLab Username" assignment on Canvas. Then I can give you access to your repository for the semester.
  • 2. Turn in a URL to a merge request in the "πŸ§‘β€πŸ”¬ Unit 00 Lab - Set up (U00.LAB.202401CS250)" assignment on Canvas.
    • Should contain an example program and your project files.
Links (CS 235)
Links (CS 250)

  • Installing an IDE

    Download and install an IDE (Integrated Development Environment) on your system. Here are some suggested ones, but you can use whatever tools you'd like.

    IDE Link Platforms Info
    Visual Studio Community https://visualstudio.microsoft.com/vs/community/ Windows only (C++ version) During install, make sure to select Desktop development with C++!!
    Code::Blocks https://www.codeblocks.org/downloads/binaries/ Windows/Linux Lightweight, better for older machines. (WINDOWS: Download the mingw-setup version!)
    XCode https://developer.apple.com/xcode/ Mac  

    We'll use our IDE more once we set up git and clone our repository…


  • Installing git

    Starting off, you will need to install the git program to your computer. Go to https://git-scm.com/ and select Download for your OS. Make sure to read through the setup settings here to prevent headaches in the future. (In particular, not setting Vim as the default text editor… unless that's something you want.)

    Most of the default settings are fine, but take note:

    • Select Components: Make sure that "Git Bash Here" is checked.
    • Choosing the default editor used by Git: Choose something like Notepad or if you have a preferred text editor, use that.

  • Configuring git

    After git is installed, we will need to do a one-time configuration. Open up Git Bash (in Windows) or use git from your terminal (Linux/Mac).

    You will need to configure your name:

    git config --global user.email "you@example.com"
    

    Replace "you@example.com" with your email (such as your student email).

    And your email address:

    git config --global user.name "Your Name"
    

    Replace "Your Name" with your name, which is what will show up when you submit code file edits.

    Finally, we're setting up a default merge resolution scheme, which you don't really need to worry about the details right now:

    git config --global pull.rebase false
    

  • Registering a GitLab account

    1. Go to the sign up page: Start by going to https://gitlab.com/users/sign_up and fill out the form to create your account.

    creating_a_gitlab_account_register.png

    Fill out your first and last name, a username, email address, and password. Note that if you want to maintain your privacy you might use an alternative email/name, but make sure to let the instructor know who you are!

    2. Navigate to your profile page: After registering, going to https://gitlab.com/ should take you to a "Projects" page. You will be able to view your repositories here later on. On the right-hand sidebar, click on your profile image (it might be some default picture), then click on the top section to go to your profile.

    creating_a_gitlab_account_gotoprofile.png

    3. Bookmark your profile page: Once you're on your profile page, the URL will look something like this: https://gitlab.com/YOURUSERNAME Take note of this URL and bookmark this page. You may also need to give this link to the instructor for further setup.

    creating_a_gitlab_account_viewprofileurl.png


  • Make sure the instructor knows your GitLab username

    Post your GitLab username in the "❓ Unit 00 Setup - GitLab Username" assignment. Once you have access to your repository, you can continue.


  • Cloning the repository

    Your operating system: Navigate to the folder where you're storing your class projects. From here, right-click in the empty space and select Git Bash Here to open Git Bash.

    Repository webpage: On the repository webpage (gitlab.com) click on the blue Clone button and copy the URL listed under the Clone with HTTPS header. (You can use SSH if you know how to set it up, but we aren't going to cover that here.)

    c3_u00_clone.png

    Git Bash: Use the git clone URL command in Git Bash. This will clone the repository within the folder you opened Git Bash in. (Windows: To use PASTE, right-click in Git Bash… CTRL+V will not work here.)

    $ git clone https://gitlab.com/rsingh13-student-repos/2024-01_cs250/YOURSSO-cs250.git
    Cloning into 'YOURSSO-cs250'...
    remote: Enumerating objects: 3, done.
    remote: Counting objects: 100% (3/3), done.
    remote: Compressing objects: 100% (2/2), done.
    remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
    Receiving objects: 100% (3/3), done.
    

    (This will be "cs235" or "cs250" based on the course you're in.)

    Now that the folder is on your computer, you will need to change directory into the folder. Use cd FOLDERNAME (in the example above, the folder name would be YOURSSO-cs235 or YOURSSO-cs250) to enter the repository folder in Git Bash.

    Changing directory in Git Bash:

    c3_u00_clonedrepo_bash.png

    Viewing the folder in Windows Explorer:

    c3_u00_clonedrepo.png


  • Creating a branch
    Command Description
    git branch View available branches
    git checkout -b BRANCHNAME Create a new branch with the given BRANCHNAME
    git checkout BRANCHNAME Checkout an existing branch whose name matches the BRANCHNAME

    When working on an assignment, you will need to create a new branch each time.

    To view available branches, use the git branch command:

    $ git branch
    * main
    

    To create a new branch, use the git checkout -b BRANCHNAME command. The branch name needs to be unique. I suggest putting the Unit # in the branch name at least.

    $ git checkout -b u00_setup
    Switched to a new branch 'u00_setup'
    

    Now if you type in git branch again you will see two branches with the asterisk marking which branch you're currently on:

    $ git branch
      main
    * u00_setup
    

    Each branch basically has its own code space. If you have work-in-progress code happening on separate branches, they will only be available on that specific branch. If you switch to another branch you'll only see code for that branch, but you can always keep switching between to get the code back. (When starting a new assignment, make sure to go back to the main branch and use git pull to grab latest code before then creating a new branch. I'll try to put a reminder in future assignments.)

    If you want to switch between existing branches, use git checkout BRANCHNAME:

    $ git checkout main
    Switched to branch 'main'
    Your branch is up to date with 'origin/main'.
    
    $ git checkout u00_setup
    Switched to branch 'u00_setup'.
    

    (Note: The main branch might be named "master" so if your git branch shows "master" instead of "main", use that.)

    Stay on your new branch for now.


    Command Description
    ls List out the files and folders in the current directory
    touch FILENAME Create a new file
    echo "text" >> FILENAME Append text to a file
    git status View files that have changed
    git add FILENAME Add a specific file to the changeset
    git add *.cpp Add all files that end with ".cpp" to the changeset
    git add . Add all changed files to the changeset
    git commit Create a snapshot of currently added changes
    git commit -m "text" Create a snapshot of currently added changes, setting commit message at the same time
    git push -u origin BRANCHNAME Push the changes in the branch to the server

    Starting off, your repository may only have a README file in the folder:

    $ ls
    README.md
    

    In Windows you can create a new text file in here by opening notepad and saving a new file in this directory.

    In Git Bash you can create a file by using the touch command: Use the touch FILENAME command to create a new file. Name your file YOURNAME.txt . You can open the file from your OS and edit the text, or you can use the following command to put the text "Hello, I'm NAME!" in place:

    $ echo "Hello, I'm Rachel!" >> u00_hello.txt
    

    Next, use the git status command to view the changed file:

    $ git status
    On branch u00_setup
    Untracked files:
      (use "git add <file>..." to include in what will be committed)
      u00_hello.txt
    
    nothing added to commit but untracked files present (use "git add" to track)
    

    We need to use the git add FILENAME command to mark the file for the changeset.

    $ git add u00_hello.txt
    

    It won't show any confirmation message if there is no error.

    Next, we use the git commit -m "COMMIT MESSAGE" command to make a snapshot of the file's changes at this point:

    $ git commit -m "Created my text file"
    [rsingh13_u04ex 9eb8e5c] Created my text file
     1 file changed, 1 insertion(+)
     create mode 100644 u00_hello.txt
    

    Finally, use the git push -u origin BRANCHNAME command to push your changes to the GitLab server:

    $ git push -u origin u00_setup
    Enumerating objects: 4, done.
    Counting objects: 100% (4/4), done.
    Delta compression using up to 12 threads
    Compressing objects: 100% (2/2), done.
    Writing objects: 100% (3/3), 303 bytes | 303.00 KiB/s, done.
    Total 3 (delta 1), reused 0 (delta 0)
    remote:
    remote: To create a merge request for u00_setup, visit:
    remote:   (A URL)
    remote:
    To gitlab.com:(REPOSITORY URL).git
     * [new branch]      u00_setup -> u00_setup
    Branch 'u00_setup' set up to track remote branch 'u00_setup' from 'origin'.
    

  • Pulling remote changes
    Command Description
    git pull Pull latest changes from the server

    GitLab webpage: On the webpage your branch will now show up in the dropdown menu.

    c3_u00_gitlab_branch.png

    Select your branch and you'll be able to see the last commit and your file here:

    c3_u00_gitlab_files.png

    Open up your text file here and click the blue Edit button and select an editor. Add another line to the text file, then click the Commit changes button at the bottom of the page.

    c3_u00_gitlab_commit.png

    Git Bash: Use the git pull command here to pull all changes, which will grab the changes that you just made from the server.

    $ git pull
    remote: Enumerating objects: 5, done.
    remote: Counting objects: 100% (5/5), done.
    remote: Compressing objects: 100% (2/2), done.
    remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
    Unpacking objects: 100% (3/3), 517 bytes | 517.00 KiB/s, done.
    From gitlab.com:(URL)
       9eb8e5c..90394af  u00_setup -> origin/u00_setup
    Updating 9eb8e5c..90394af
    Fast-forward
     u00_hello.txt | 2 ++
     1 file changed, 2 insertions(+)
    

    When you open the text file in Notepad or a text editor, you will now see the second line of text.


  • Dealing with merge conflicts

    This time we're going to purposefully create a merge conflict. This happens when changes are made to the same file from different locations (such as on your computer and the web, or between different developers) and git can't figure out how to automatically merge the changes together. It can be intimidating to deal with, but it isn't too bad.

    Computer: Edit your text file to add some text in the middle. Then use git add FILENAME , git commit -m "Edited file", but don't push yet.

    GitLab: Edit your text file to add a different line of text in the middle.

    c3_u00_gitlab_twochanges.png

    Git Bash: Now that you have two sets of changes, try to push your changes to the server. It will be rejected:

    $ git push
    To gitlab.com:(URL).git
     ! [rejected]        u00_welcome -> u00_welcome (fetch first)
    error: failed to push some refs to 'git@gitlab.com:(URL).git'
    hint: Updates were rejected because the remote contains work that you do
    hint: not have locally. This is usually caused by another repository pushing
    hint: to the same ref. You may want to first integrate the remote changes
    hint: (e.g., 'git pull ...') before pushing again.
    hint: See the 'Note about fast-forwards' in 'git push --help' for details.
    

    It gives you a hint - you need to use git pull to grab any latest changes from the server:

    $ git push
    To gitlab.com:(URL).git
     ! [rejected]        u00_welcome -> u00_welcome (non-fast-forward)
    error: failed to push some refs to 'git@gitlab.com:(URL).git'
    hint: Updates were rejected because the tip of your current branch is behind
    hint: its remote counterpart. Integrate the remote changes (e.g.
    hint: 'git pull ...') before pushing again.
    hint: See the 'Note about fast-forwards' in 'git push --help' for details.
    

    Use git pull, which will work, but result in a merge conflict:

    $ git pull
    Auto-merging u00_welcome.txt
    CONFLICT (content): Merge conflict in u00_welcome.txt
    Automatic merge failed; fix conflicts and then commit the result.
    

    Open the text file in your editor and you'll see some additions to the file:

    Hello, I'm Rachel!
    <<<<<<< HEAD
    This is my third edit.
    =======
    Fourth update!!!
    >>>>>>> a0c7bd54e2c5c19d1ebd41a84e7a6b8820573cf9
    This is a second update!
    

    There are two regions, and we need to manually decide how we want to merge the text. You could just remove the markers if you wanted both changes. Make the update and save the file:

    Hello, I'm Rachel!
    This is my third edit.
    Fourth update!!!
    This is a second update!
    

    Then do your add, commit, and push to push the merged updates to the server.

    Protip: You can combine multiple commands together with &&:

    $ git add . && git commit -m "manual merge" && git push

    GitLab: On the website, you can select the "Commits" link under the "Code" section to view all the changes you've made to the code:

    c3_u00_gitlab_commitlist.png


  • Creating an example program to test our IDE

    Lastly we're going to create a basic C++ project and make sure everything builds correctly. The project and code you create here should be stored within your repository.

    • If you're using VS Code, please check the reference portion of the book for the section labeled VS Code - Open projects, building, and running.
    • If you're using another code editor and wish to build from the terminal using the Makefile, look in the reference portion of the book for the section labeled Terminal - Building and running

    Visual Studio

    1. From the starting page, select "Create a new project".

      vs_newproject_01_splash.png

    2. Select "Empty project" and click "Next".

      vs_newproject_02_emptyproject.png

    3. Set your Project name. Make sure to set the project Location to somewhere within your repository directory.

      vs_newproject_03_namelocation.png

    4. Right-click on your empty project and select Add then New item…

      vs_newproject_04_addfile.png

    5. Type in "main.cpp" then click "Add".

      vs_newproject_04_addfile2.png

    6. Add a simple starter C++ program into main.cpp.

      vs_newproject_05_program.png

    7. Go to the "Build" dropdown menu and select "Build Solution".

      vs_newproject_06_build.png

    8. It should show a success in the Output window.

      vs_newproject_07_buildsuccess.png

    9. Then, click the "(play) Local Windows Debugger" button to run the program.

      vs_newproject_08_run.png

    10. Your program text will show up in a console.

      vs_newproject_08_run2.png

    11. Your files will be in the location you set up. The .cpp file is your source code, and the .vcxproj is the project file.

      vs_newproject_09_files.png

    Code::Blocks

    1. From the starting page, select "Create a new project".

      cb_newproject_01_splash.png

    2. Select "Empty project" and click "Go".

      cb_newproject_02_emptyproject.png

    3. Set your Project name. Make sure to set the project Location to somewhere within your repository directory.

      cb_newproject_03_namelocation.png

    4. Go to the "File" dropdown menu and select "New", then "Empty file".

      cb_newproject_04_addfile.png

    5. It will ask you to save the file first, name it main.cpp. Default options are fine.

      cb_newproject_04_addfile2.png

    6. Add a simple starter C++ program into main.cpp.

      cb_newproject_05_program.png

    7. Go to the "Build" dropdown menu and select "Build".

      cb_newproject_06_build.png

    8. The program should build without any errors.

      cb_newproject_07_buildsuccess.png

    9. Click on the "play" button in the toolbar to run the program.

      cb_newproject_08_run.png

    10. Your program text should show up in the console.

      cb_newproject_08_run2.png

    11. Your files will be in the location you set up. The .cpp file is your source code and the .cbp file is the project file.

      cb_newproject_09_files.png

    Afterwards, add, commit, and push your changes:

    git add .
    git commit -m "Example project"
    git push -u origin u00_setup
    

  • Creating a merge request

    Once changes are made, you'll create a merge request. This is how you can have your code reviewed, and the instructor will sign off on it and merge it to the main branch, where all the code will live together. This is similar to in software development, where everybody will work on their own features on their own branches, create a merge request, get their code reviewed, then a senior developer will merge the code into the main project.

    GitLab: On the main repository page you will see a message saying that there are changes in your branch, with a button "Create merge request":

    c3_u00_gitlab_mergenotif.png

    You can mostly leave the defaults unless you want to add notes. Scroll down and click the blue "Create merge request" button.

    There will be a merge request page generated:

    c3_u00_gitlab_mergerequest2.png

    The URL of this page is what you will usually turn in as your Lab assignments

    Once you've created a merge request, copy the URL of the merge request on the GitLab.com webpage:

    merge_request_url.png

    Back on the Canvas page, locate the assignment and click the link:

    canvas_assignment.png

    Then click on "Start Assignment":

    canvas_start_assignment.png

    Paste in the URL to your Merge Request in the "Website URL:" textbox, then click "Submit Assignment" to finish the turn in.

    canvas_submit_mergerequest.png

UNIT 01: Exploring data

πŸ“–οΈ Reading - Exploring data (U01.READ)

reading_u01_ExploringData_shopkeep.png

For a moment, let's forget about computers and think about an old timey shop ran by one fella. Let's call this enthusiastic entrepreneur "Stan", and he sells snacks. What kind of data does he have access to, as he sells items to customers?

He probably keeps a log of inventory - how much of each item he's bought. He can see from how much has sold, which items are popular and what he needs to order more of. Perhaps the Butterscotch Bars are very popular so he needs to make sure to order double the stock to keep up with demand.

If it's just ole' Stan here manning the shoppe, perhaps he notices patterns as well - little Sally tends to buy Nickel Candies after school, so whenever he sees her he knows what she's going to order.

Larry the Lad likes buying Cinnamon Firecrackers, he buys Lemondrop Llamas on Fridays. Good ole' Stan asks Larry the Lad, "So when do you have a hankerin' for these Llamas?" and Larry the Lad tells Stan, "I visit my grand pappy's house on Fridays, and he sure likes them Lemondrops!". With or without that context, Stan can still see the pattern of Larry the Lady purchasing Cinnamon Firecrackers on Monday and Wednesday, and Lemondrop Llamas on Friday.

The longer he runs the shoppe and the more regular customers he gets, he keeps a mental note of the patterns of his customers. And for a small olde timey shoppe like his, this can create good customer service, perhaps he offers a more customized experience than his competitor, Big City Candy Co.

Back to the modern day…

reading_u01_ExploringData_shopkeep2.png

Virtually everything we do on the internet generates data, whether we are actively providing data or not. Merely just browsing websites generates data that companies use to optimize sales or ad revenue. Some pages track where your mouse cursor sits while you visit a page, because people often point their mouse cursor to what they're reading - this data creates a heat map that companies can use to figure out the best layout for their pages to funnel potential customers to where they want them.

Even in-person systems we use are digitized and recording data. When you purchase something with a card at a store, they can keep a record for you as a customer and relate your purchases to that card - even better if you have a "super saver" type membership card as well.

Scrolling through social media? Which ads do you pause over? Maybe you got distracted by your dog, but the data shows potential interest - you spent a few seconds longer looking at this ad over the other ads. (And if you click the ad, then they really have a lead!) And of course your order history can be used to figure out what to market to you and when.

As you carry your phone around with you, location data is often tracked. When you get together with a friend, you might start seeing ads influenced by your friend - you're hanging out, so perhaps their ads will appeal to you.

With the speed of the internet and relatively cheap cost of storage, data is being tracked all the time, and in large quantities. "Big Data", as well as using Machine Learning to do what Good Ole' Stan does, are part of our modern tech landscape.


  • OK but how does that relate to this class?

    Data needs to be stored, and while long-term storage means putting it in some kind of database, receiving data to save, or pulling data to crunch, requires software at some level. We need to be able to store the data in a data structure, and we need to choose the structure that is the most efficient for the goals we have - do we prioritize access speed or insert speed? Do we want a structure that auto-sorts incoming data so we can find records more quickly? What kind of sorting algorithm do we use to sort the unsorted data?

    In this class we'll be focusing on common data structures you'll see as a software developer - array-based structures, link-based structures, binary search trees, hash tables, stacks, and queues. We're going to learn how they work, and the efficiency of their functionality. While you probably won't be writing these structures from scratch after this course, knowing how they work helps you make an informed design decision on what is the best tool for the job.


  • Example: Spotify

    We can get some kind of idea of the underlying structure of a system by looking at the API (Application Programming Interface) that it exposes to the outside world. Many modern websites offer APIs in order to allow third parties to create helper programs and services that work with the data stored by the original service.

    Looking at the Spotify API documentation (https://developer.spotify.com/documentation/web-api), we can see several main "objects" it provides:

    1. Albums
    2. Artists
    3. Audiobooks
    4. Categories
    5. Chapters
    6. Episodes
    7. Genres
    8. Markets
    9. Player
    10. Playlists
    11. Search
    12. Shows
    13. Tracks
    14. Users

    If you click on one of the objects, it will show an example of requests that an external program can call, such as Get Album, Get Album Tracks, Save Albums for Current User, and so on.

    Clicking on a request type will also show a Response Sample, which shows what kind of information may be returned with a call, such as Get Album:

    {
      "album_type": "compilation",
      "total_tracks": 9,
      "available_markets": [
        "CA",
        "BR",
        "IT"
      ],
      "id": "2up3OPMp9Tb4dAKM2erWXQ",
      "images": [
        {
          "url": "https://i.scdn.co/image/ab67616d00001e02ff9ca10b55ce82ae553c8228",
          "height": 300,
          "width": 300
        }
      ],
      "release_date": "1981-12",
      "type": "album",
      "artists": [
        {
          "id": "string",
          "name": "string",
        }
      ],
      "tracks": {
        "total": 4,
        "href": "https://api.spotify.com/v1/me/shows?offset=0&limit=20",
        "next": "https://api.spotify.com/v1/me/shows?offset=1&limit=1",
        "previous": "https://api.spotify.com/v1/me/shows?offset=1&limit=1"
      },
      "copyrights": [
        {
          "text": "string",
          "type": "string"
        }
      ],
      "genres": [
        "Egg punk",
        "Noise rock"
      ],
      "popularity": 0
    }
    

    The data and the form it's returned in via API response isn't necessarily how the data is stored in the database itself, or how the data is stored in objects when worked on by the Spotify servers themselves. Usually, API systems pulls data from multiple sources and bundles it together to return as a response. But, this can at least give us some idea of how the actual system may be organized.

    reading_u01_ExploringDesign_Spotify.png

    This is just a small example, containing a "Music" section (Artist, Album, Track) and a "User" section (User, Playlist). The arrows show relationships between the objects. But this diagram is also lacking any of the back-end functionality that helps everything actually do something. On their own, it's just data stored somewhere. A program has to be written around it.


  • Additional APIs

    In the Tech Literacy assignment you will brainstorm about data used for specific types of software. You may want to reference the API pages for these common apps for ideas:

    \newpage

🧠 Tech Literacy - Exploring data (U01.TEC)

Locate the Discussion Board assignment "🧠 Unit 01 Tech Literacy - Exploring data (U01.TEC.202401CS250)" Refer back to the reading on exploring data ("πŸ“–οΈ Unit 01 Reading - Exploring data (U01.READ.202401CS250)") for ideas.

Example software:

  1. A Door Dash style food delivery app where a Customer can place an Order for some Food items from a Restaraunt, a Driver might pick up the items.
  2. A Social Media style app where Users can make friends, create Posts, and mark Posts as favorites.
  3. A Class Learning Management System where Teachers and Students can access a course, Assignments can be created/accessed, Grades can be saved.

You can come up with your own example program to post about as well.

Canvas assignment: https://canvas.jccc.edu/courses/68320/modules/items/3810715

UNIT 02: CS 200 Review - Basics

πŸ“–οΈ Reading - CS 200 review: Basics (U02.READ)

Make sure to also view the "C++ Quick Reference" part of this textbook for a reference guide on basic C++ syntax.

  • Includes
    Include Features
    #include <iostream> Use of cout (console output), cin (console input), getline
    #include <string> Use of the string data type and its functions
    #include <fstream> Use of ofstream (output file stream), ifstream (input file stream), and related functions
    #include <array> Use of array from the Standard Template Library
    #include <vector> Use of vector from the Standard Template Library
    #include <cstdlib> C standard libraries, usually used for rand().
    #include <ctime> C time libraries, usually used for srand( time( NULL ) ); to seed random # generator
    #include <cmath> C math libraries, such as sqrt, trig functions, etc.
    #include "file.h Use "" to include .h files within your own project. DON'T USE INCLUDE ON .cpp FILES!!
    About the using command

    The using namespace std; command states that we're using the std namespace. If this is left off, then we need to prefix any C++ types and functions with std::, such as std::cout << "Hello!" << std::endl;

    Best practices:

    • If your program ONLY uses C++ standard library includes, use using namespace std; for brevity.
    • If your program uses MULTIPLE LIBRARIES, avoid using namespace std; and prefix each type/function with the library it belongs to (e.g., std::string from the STD library, sf::vector2f from the SFML library)

  • Bare minimum C++ programs

    Without arguments:

    #include <iostream>
    using namespace std;
    
    int main()
    {
      cout << "Hello, world!" << endl;
      return 0;
    }
    

    With arguments:

    #include <string>
    #include <iostream>
    using namespace std;
    
    int main( int argCount, char* args[] )
    {
      if ( argCount < 4 ) { cout << "Not enough arguments!" << endl; return 1; }
    
      int my_int = stoi( args[1] );
      float my_float = stof( args[2] );
      string my_string = string( args[3] );
    
      return 0;
    }
    

  • Building and running from the command line

    Building a single source file:

    g++ SOURCEFILE.cpp -o PROGRAMNAME.out
    

    Building multiple source files:

    g++ *.cpp *.h -o PROGRAMNAME.out
    

    (There's a different command to use the Visual Studio compiler to build from command line.)

    Running a program without arguments:

    ./PROGRAMNAME.out
    

    Running a program with arguments:

    ./PROGRAMNAME.out arg1 arg2 arg3
    

  • Variable declaration
    1. Declaring a variable: DATATYPE VARIABLENAME;
    2. Declaring a variable and assigning a value: DATATYPE VARIABLENAME = VALUE;
    3. Declaring and assigning a named constant: const DATATYPE NAME = VALUE;
    4. Assigning a new value to an existing variable: VARIABLENAME = VALUE;
    5. Copying a value from one variable to another: UPDATEDVAR = COPYME;
    Data type Value examples
    int -5, 0, 100
    float 0.99, -3.25, 1.0
    string "Hello world!"
    char 'a', '$'
    bool true, false

    Increment/Decrement statements:

    • a++, a--
    • ++a, --a
    • a+=5;, a-=5;
    • a = a + 5;, a = a - 5;

    Notes:

    • LHS = RHS; copies FROM RHS TO LHS; make sure you have the right order!
    • A hard-coded value, like "Hello", is known as a literal.

  • Console input and output
    1. Output a variable's value: cout << VARIABLENAME;
    2. Output a string literal: cout << "Hello";
    3. Chain together multiple items: cout << "Label: " << VARIABLENAME << endl;
    4. Input to a variable: cin >> VARIABLENAME;
    5. Input a whole line to a string variable: getline( cin, STRINGVARIABLE );

    Notes:

    • The << operator is called the output stream operator and is used on cout statements.
    • The >> operator is called the input stream operator and is used on cin statements.
    • endl is only to be used with cout statements, not cin!
    • getline can only be used with strings!! Use cin >> for other data types.
    • You need a cin.ignore() ONLY in between cin >> ... and getline( cin, ... ).
      • If your program is skipping an input then you're missing a cin.ignore();.
      • If your program is getting input but not storing the first letter then you have too many cin.ignore(); statements / they're in the wrong place.

  • Boolean expressions
    • AND (&&)
      • Expression is TRUE if all sub-expressions are TRUE
      • Expression is FALSE if at least one sub-expression is FALSE
    • OR (||)
      • Expression is TRUE if at least one sub-expression is TRUE
      • Expression is FALSE if all sub-expressions are FALSE
    • NOT (!)
      • Expression is TRUE if sub-expression is FALSE
      • Expression is FALSE if sub-expression is TRUE
    a b a AND b a OR b
    T T T T
    T F F T
    F T F T
    F F F F
    a NOT a
    T F
    F T

  • If statements
    • if statement:
    if ( CONDITION_A )
    {
      // Action A
    }
    
    • if/else statement:
    if ( CONDITION_A )
    {
      // Action A
    }
    else
    {
      // Action Z
    }
    
    • if/else if statement:
    if ( CONDITION_A )
    {
      // Action A
    }
    else if ( CONDITION_B )
    {
      // Action B
    }
    else if ( CONDITION_C )
    {
      // Action C
    }
    
    • if/else if/else statement:
    if ( CONDITION_A )
    {
      // Action A
    }
    else if ( CONDITION_B )
    {
      // Action B
    }
    else if ( CONDITION_C )
    {
      // Action C
    }
    else
    {
      // Action Z
    }
    

    Condition types can be a boolean variable:

    bool done = true;
    if ( done )
    {
      cout << "Goodbye!" << endl;
    }
    

    Or a boolean expression:

    int a = 10, b = 5;
    if ( a > b )
    {
      cout << "a is bigger!" << endl;
    }
    

    Notes:

    • else statements NEVER TAKE A CONDITION
    • Whichever condition evaluates to true, all future else if and else statements are skipped.

  • Switch statements
    switch( myNumber )
    {
      case 1:
        cout << "It's one!" << endl;
        break;
    
      case 2:
        cout << "It's two!" << endl;
        break;
    
      default:
        cout << "I don't know what it is!" << endl;
    }
    

    Notes:

    • You can leave off break; from a case statement. In this case, fallthrough occurs, where the following case's code will be executed, up until it hits a break; statement.
    • Switch statements like the one above can be replaced with if (myNumber = 1)=, else if (myNumber = 2)=.
    • In C++, switch doesn't work with string, only primitive data types like int, float, char.

    Fallthrough example:

    char choice;
    cout << "Enter a choice: (y/n): ";
    cin >> choice;
    
    switch( choice )
    {
      case 'y':
      case 'Y':
        cout << "YES" << endl;
        break;
    
      case 'n':
      case 'N':
        cout << "NO" << endl;
        break;
    
      default:
        cout << "INVALID INPUT!" << endl;
    }
    

  • While loops

    while ( CONDITION ) { }

    while ( a < b )
    {
      cout << a << endl;
      a++;
    }
    

    Notes:

    • While loops use CONDITIONS like if statements do.
    • While loops are susceptible to infinite loop errors if nothing within the loop causes the CONDITION to evaluate to false eventually. Be careful!

  • For loops

    Normal for loop:

    for ( INIT; CONDITION; UPDATE ) { }

    for ( int i = 0; i < 10; i++ )
    {
      cout << i << endl;
    }
    

    Notes:

    • When using STL arrays or vectors, size_t or unsigned int should be used instead of int for i. This will remove the warning relating to testing .size() (which returns size_t) against a signed integer.

    Range-based for loop:

    In versions of C++ past C++98 (from 1998) you can use range-based for loops to iterate over a range of items:

    for ( INIT : RANGE ) { }

    vector<int> myVec = { 1, 2, 3, 4 };
    for ( int element : myVec )
    {
      cout << element << endl;
    }
    

  • Arrays and vectors
    1. Declare a C-style array: DATATYPE ARRAYNAME[ SIZE ];
    2. Declare a C++ STL array: array<DATATYPE, SIZE> ARRAYNAME;
    3. Declare a C++ STL vector: vector<DATATYPE> VECTORNAME;
    4. Declare a dynamic array via pointer: DATATYPE * PTR = NEW DATATYPE[ SIZE ];
    5. Destroy a dynamic array: delete [] PTR;
    6. Initialize an array with an initializer list: DATATYPE ARRAY[] = { VAL1, VAL2, VAL3 } (also works for array and vector).

    Notes:

    • The position of an item within an array is called its index.
    • The variable within the array at some position is called its element.
    • A vector is a dynamic array, but you don't have to worry about the memory management. :) This means that it can be resized.
    • A array is a fixed size, just like the C-style array.

    Iterate over an array/vector

    // C-style array:
    for ( int i = 0; i < TOTAL_STUDENTS; i++ )
    {
      cout << "index: " << i << ", value: " << students[i] << endl;
    }
    
    // STL Array and STL Vector:
    for ( size_t i = 0; i < bankBalances.size(); i++ )
    {
      cout << "index: " << i << ", value: " << students[i] << endl;
    }
    

    size_t is another name for an unsigned int, which allows values of 0 and above - no negatives.


  • File I/O

    Create an output file and write

    ofstream output;
    output.open( "file.txt" );
    
    // Write to text file
    output << "Hello, world!" << endl;
    

    Create an input file and read

    ifstream input;
    input.open( "file.txt" );
    if ( input.fail() )
    {
      cout << "ERROR: could not load file.txt!" << endl;
    }
    string buffer;
    
    // read a word
    input >> buffer;
    
    // read a line
    getline( input, buffer );
    

    Notes:

    • An ifstream open will fail if the file requested does not exist or cannot be found. Use if ( input.fail() ) to check for a failure scenario.
    • An ofstream open will create a file if it doesn't already exist.
    • The default path where files are written to or read from is wherever your project file is (.vcxproj for Visual Studio, .cbp for Code::Blocks). You can also set a default working directory in your project settings.
      • Mac users may need to specify an absolute path, like "~/myfile.txt", which will put the file in your home directory. I don't know how to use XCode but we can work through it together if you need help.

  • Review questions:

    Answer these questions in your notes, then check your work by completing the related Concept Intro assignment on Canvas.

    1. The starting point of all C++ programs is…
    2. Given two variables, vip and player3, how do you copy the name from the player3 variable into the vip variable?
    3. << is known as …, >> is known as…
    4. getline( cin, … ) can only be used with…
    5. An if or else if statement's contents are executed when…
    6. The three parts of a for loop's header is…
    7. … can be resized, but … cannot.

πŸ”Ž Concept Intro - CS 200 review: Basics (U02.CIN)

πŸ§‘β€πŸ”¬ Lab - CS 200 review: Basics (U02.LAB)

About: This assignment will cover some basic C++ concepts. This should give you a chance to refresh your skills and recognize what you may need to review.

pixel-goals.png Goals:

  • Practice with variables
  • Practice with branching
  • Practice with loops
  • Practice with vectors
  • Practice with file output
  • Practice with file input

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Set up: Creating a new branch in your repository

    git-harddrive-opengitbash.png

    First, Open up Git Bash (Windows) or your Terminal (Linux/Mac) from within your repository directory. Make sure you're inside the repository folder, which should be named something like YOURSSO-CLASS.

    Next, create a new branch in the repository with the checkout command: git checkout -b u02_review

    $ git checkout -b u02_review
    Switched to a new branch 'u02_review'
    

    git_newbranch.png

    Now your folder is on the new branch and you can start working on the assignment.


  • Download the starter code

    Download the following starter code and unzip it to your repository folder.

    lab_u02_CPPBasicsReview_extractedfiles.png

    All starter files:

    .
    ├── INFO.h
    ├── main.cpp
    ├── Project_CodeBlocks
    │   ├── CPPReviewBasics.cbp
    │   ├── CPPReviewBasics.depend
    │   └── CPPReviewBasics.layout
    ├── Project_Makefile
    │   └── Makefile
    ├── Project_VisualStudio
    │   ├── CPPBasicsReview.sln
    │   ├── CPPBasicsReview.vcxproj
    │   ├── CPPBasicsReview.vcxproj.filters
    │   ├── results.txt
    │   ├── test1.txt
    │   └── test2.txt
    └── u02_Review_CPPBasics
        ├── P1_variables
        │   └── U02_P1_PizzaSlices.cpp
        ├── P2_branching
        │   └── U02_P2_LetterGrade.cpp
        ├── P3_looping
        │   └── U02_P3_SalaryRaise.cpp
        ├── P4_vector
        │   └── U02_P4_GPA.cpp
        ├── P5_ofstream
        │   └── U02_P5_SalaryRaise.cpp
        ├── P6_ifstream
        │   ├── lyricsB.txt
        │   └── U02_P6_Lyrics.cpp
        ├── U02_Headers.cpp
        └── U02_Headers.h
    

    Visual Studio users:

    Open the Project_VisualStudio folder and open the CPPBasicsReview.sln file to get started.

    Code::Blocks users:

    Open the Project_CodeBlocks folder and open the CPPReviewBasics.cbp file to get started.

    Build from Terminal/VSCode:

    In the Project_Makefile directory you'll find a makefile you can use to build the project. Use the command make in the directory, and it will generate an executable file that you can run like this: ./CPPReviewBasics_out

    DO NOT CREATE YOUR OWN VISUAL STUDIO OR CODE::BLOCKS PROJECT.

    DO NOT REORGANIZE THE CODE.

    KEEP YOUR REPOSITORY DIRECTORY AND FILES CLEAN!!


  • How the program and tests work

    (These are not steps to do yet, just background information on the program. Instructions continue below.)

    Once you run the program, you will see two options: Run the AUTOMATED TESTS or run the PROGRAMS manually.

    1. Run AUTOMATED TESTS
    2. Run PROGRAMS
    >>
    

    You can use the AUTOMATED TESTS to verify your work. Tests will either be marked as PASS or FAIL, and show the input passed in and the expected result:

    [FAIL]  TEST 1, StudentCode(AABC)
       EXPECTED OUTPUT: [3.25]
       ACTUAL OUTPUT:   [3.5]
    
    [PASS]  TEST 2, StudentCode(ABCCCDF) = 2.07143
    

    You can also MANUALLY RUN THE PROGRAMS to test them yourself:

    1. Run AUTOMATED TESTS
    2. Run PROGRAMS
    >> 2
    UNIT 02: C++ Basics Review -----------------------------------------------------
    
    Program 1: Pizza slices
    Program 2: Letter grade
    Program 3: Salary raise v1
    Program 4: GPA
    Program 5: Salary raise v2
    Program 6: Lyrics
    
    Run program #: 4
    Enter another letter grade (A, B, C, D, F) or X to stop: A
    Enter another letter grade (A, B, C, D, F) or X to stop: B
    Enter another letter grade (A, B, C, D, F) or X to stop: C
    Enter another letter grade (A, B, C, D, F) or X to stop: X
    Your GPA is: 3
    

    To begin with, the functionality won't work properly - you will need to implement the functions themselves. We will step through that in a moment.


  • File: INFO.h

    This file contains a string constant. Replace "Your Name" with your actual name. This is to help me with grading.

    const std::string STUDENT_NAME = "Your Name";
    

    As you work through the coding assignments, if you have any questions or get stuck, feel free to ask for hints from the instructor.


  • File: U02_P1_PizzaSlices.cpp

    This code file is under u02_Review_CPPBasics/P1_variables/U02_P1_PizzaSlices.cpp.

    PIZZA PARTY PLANNER
    How many guests will you have at your party? 20
    How many slices of pizza are available? 100
    Total guests: 20
    Total slices of pizza: 100
    Each guest gets 5 slices of pizza
    Each guest can have 5 slices of pizza
    

    You will be editing the function int U02_Program1_Function( int guest_count, int pizza_slices ) for this program.

    • Input parameters:
      1. guest_count, the amount of guests attending the party
      2. pizza_slices, the total amount of pizza slices at the party
    • Return output: The amount of slices of pizza each guest gets
    • Requirements: In this function, calculate the slices per person. You can add cout statements to display each piece of information, or you can also just do the calculation and return it at the end.
    • Math: Given \(s\) total slices and \(g\) total guests, slices per person \(p\) is \[p = \frac{s}{g}\].

  • File: U02_P2_LetterGrade.cpp

    This code file is under u02_Review_CPPBasics/P2_branching/U02_P2_LetterGrade.cpp.

    LETTER GRADE PROGRAM
    How many points was the assignment worth? 75
    How many points did you score? 65
    
    GRADE INFORMATION
    My score ......... 75
    Points possible ...65
    Grade % ...........115.385
    Letter grade ......A
    
    Letter grade result: A
    

    You will be editing the function char U02_Program2_Function( float points_possible, float my_score ) for this program.

    • Input parameters:
      1. points_possible, the amount of total points an assignment is worth
      2. my_score, the amount of points the student earned on the assignment
    • Return output: The letter grade they received on the assignment
    • Requirements: Calculate the percent result the student earned on the assignment
    • Math: Given \(p\) points possible and \(s\) score earned, the percentage grade the student earned \(g\) is: \[ g = \frac{s}{p} \cdot 100 \]

    Letter grade: Use the following criteria to decide which grade the student receives.

    Letter Range
    'A' 89.5% and above
    'B' 79.5% - 89.5%
    'C' 69.5% - 79.5%
    'D' 59.5% - 69.5%
    'F' Below 59.5%

  • File: U02_P3_SalaryRaise.cpp

    This code file is under u02_Review_CPPBasics/P3_looping/U02_P3_SalaryRaise.cpp.

    SALARY RAISE PROGRAM v2
    What is your starting salary? $60000
    What is the raise you get per year? %2
    How many years to calculate? 10
    Starting salary: $60000.00
    Year: 1, Salary: $61200.00
    Year: 2, Salary: $62424.00
    Year: 3, Salary: $63672.48
    Year: 4, Salary: $64945.93
    Year: 5, Salary: $66244.85
    Year: 6, Salary: $67569.75
    Year: 7, Salary: $68921.15
    Year: 8, Salary: $70299.57
    Year: 9, Salary: $71705.56
    Year: 10, Salary: $73139.67
    Salary after 10 years: $73139.67
    

    You will be editing the function float U02_Program3_Function( float starting_salary, float raise_per_year, int years ) for this program.

    • Input parameters:
      1. starting_salary, the salary the user starts out with
      2. raise_per_year the decimal value of the raise per year (e.g., 0.05 instead of 5%)
      3. years, how many years of raises they want to calculate
    • Return output: The salary after years amount of years.
    • Requirements: Use a loop to calculate and display the updated salary each year.
    • Math: Given the last year's salary \(s\) and the decimal raise per year \(d\), the updated salary for the year \(u\) is: \[ u = s + sd \]

  • File: U02_P4_GPA.cpp

    This code file is under u02_Review_CPPBasics/P4_vector/U02_P4_GPA.cpp.

    Enter another letter grade (A, B, C, D, F) or X to stop: A
    Enter another letter grade (A, B, C, D, F) or X to stop: A
    Enter another letter grade (A, B, C, D, F) or X to stop: B
    Enter another letter grade (A, B, C, D, F) or X to stop: C
    Enter another letter grade (A, B, C, D, F) or X to stop: X
    Your GPA is: 3.25
    

    You will be editing the function float U02_Program4_Function( std::vector<char> course_grades ) for this program.

    • Input parameters:
      1. course_grades, a vector of letter grades
    • Return output: The GPA the student received
    • Requirements: Calculate the decimal equivalent of each letter grade, then use that to calculate the GPA.
    • Math: Given \(n\) total decimal scores \(s_1\), \(s_2\), …, \(s_n\), the GPA is: \[ \frac{ s_1 + s_2 + ... + s_n }{ n } \]
    Letter Decimal equivalent
    'A' 4.0
    'B' 3.0
    'C' 2.0
    'D' 1.0
    'F' 0.0

  • File: U02_P5_SalaryRaise.cpp

    This code file is under u02_Review_CPPBasics/P5_ofstream/U02_P5_SalaryRaise.cpp.

    SALARY RAISE PROGRAM v2
    What is your starting salary? $40000
    What is the raise you get per year? %5
    How many years to calculate? 5
    Salary after 5 years: $51051.26
    Result saved to results.txt
    

    Saved file example:

    Starting salary: $40000
    Year: 1, Salary: $42000
    Year: 2, Salary: $44100
    Year: 3, Salary: $46305
    Year: 4, Salary: $48620.2
    Year: 5, Salary: $51051.3
    

    You will be editing the function float U02_Program5_Function( float starting_salary, float raise_per_year, int years ) for this program.

    • Input parameters:
      1. starting_salary, the salary the user starts out with
      2. raise_per_year the decimal value of the raise per year (e.g., 0.05 instead of 5%)
      3. years, how many years of raises they want to calculate
    • Return output: The salary after years amount of years.
    • Requirements: Besides calculating the updated salary, create an ofstream output file and store the following data to the text file:
      • The starting salary
      • In the loop, each year and the updated salary.

  • File: U02_P6_Lyrics.cpp

    This code file is under u02_Review_CPPBasics/P6_ifstream/U02_P6_Lyrics.cpp.

    Get which line # from test2.txt? 1
    Text at that line: we can leave your friends behind
    

    Example input file:

    we can dance if we want to
    we can leave your friends behind
    cuz your friends don't dance
    and if they don't dance
    well then they're no friends of mine.
    

    You will be editing the function std::string U02_Program6_Function( std::string filename, int line_number ) for this program.

    • Input parameters:
      1. filename, the file of lyrics to open
      2. line_number, which line # of the song to display
    • Return output: The lyric at the given line_number
    • Requirements: Use an ifstream object to open the filename given. Read each line of the input file until you hit the correct line number, return the string read in at that line number.
      • Error check: If opening the file fails, then display an error message and return an empty string, "".
      • Default: If the loop completes and we haven't hit the line_number requested (i.e., the file is fewer than that many lines), then return an empty string. "".

    File input hints:

    You can read one line of the file at a time with the getline function:

    // input is our ifstream object
    // buffer is a string, each line of the file is stored in it
    while ( getline( input, buffer ) )
    {
      // Do something with buffer
    }
    

  • Turning in the assignment

    Once you're done with the assignment, you need to add, commit, and push your changes to the server.

    lab_u02_CPPBasicsReview_gitserver.png

    Save your changes to the server:

    1. Open git bash from within your repository folder on your harddrive. git-harddrive-opengitbash.png
    2. Use git status to review what files have changed since your last commit.
    3. Use git add . to add all changed files to a changeset. git_add.png
    4. Use git commit -m "MESSAGE" to create a snapshot of your current changes. (Replace "MESSAGE" with something more descriptive.) git_commit.png
    5. Use git push -u origin u02_review to push all your commits to the GitLab server, on the u02_review branch. git_push.png

    You can continue making changes and do additional add, commit, and push commands as well.

    Get everything ready for next assignment:

    1. Use git checkout main to change back to the main branch to prepare for the next assignment. (You can always use git checkout u02_review to return to this branch and code.)
    2. Use git pull to pull latest from the server. (This might happen if the instructor adds new starter code or makes modifications.)

    git_newbranch.png

    Create a Merge Request on the GitLab website:

    After pushing your changes to your branch, the GitLab webpage will show an update:

    gitlab_web_mergerequest1.png

    Click the blue "Create merge request" button.

    gitlab_web_mergerequest2.png

    Give your Merge Request a descriptive name, such as the unit the assignment belongs to.

    At the bottom of the page, click on the blue "Create merge request" button.

    gitlab_web_mergerequest3.png

    Your merge request will now be created. You don't need to do anything further on this page.

    • Approve button: The instructor may mark the merge request as approved.
    • Merge button: You won't be able to press the Merge button; the instructor will merge the code once they've reviewed your work and approved it.

    Turn in the Merge Request URL on Canvas:

    Once you've created a merge request, copy the URL of the merge request on the GitLab.com webpage:

    merge_request_url.png

    Back on the Canvas page, locate the assignment and click the link:

    canvas_assignment.png

    Then click on "Start Assignment":

    canvas_start_assignment.png

    Paste in the URL to your Merge Request in the "Website URL:" textbox, then click "Submit Assignment" to finish the turn in.

    canvas_submit_mergerequest.png

🧠 Tech Literacy - Command Line programs (U02.TEC)

  • A brief history of interfacing with computers

    Early computers were a shared resource, with one mainframe computer shared by many people at a university, business, or elseware. People often wrote programs on paper punchcards using dedicated hardware separate from the computer itself. (See: 1964 IBM 029 Keypunch Card Punching Demonstration (CuriousMarc))

    Used_Punchcard_(5151286161).jpg

    Figure 2: By Pete Birkinshaw from Manchester, UK - Used Punchcard, CC BY 2.0, index.php?curid=49758093Used_Punchcard_(5151286161).jpg

    Similarly, early computers didn't necessarily utilize video screens or monitors for output. Since the computer was shared, you might drop off your batch of cards for your program and come back later for the result output. If you think about it, a paper-based printout was probably the most cost-effective way for this. (See: YouTube: The IBM 1401 Mainframe Computer Wishes you a Merry Christmas (CuriousMarc) - Feeding a mainframe punchcards and getting printed output)

    Even when video monitors were available for these mainframe computers, the computers were still a shared resource being used by multiple people sharing computer time. In cases like these, users would work at a terminal. These look like what we think of as computers, with a video display and a keyboard, but they don't do any processing on their own. These terminals were connected to the mainframe computer, allowing users in other locations to interact with it via a simple text interface.

    Even as Personal Computers began taking root in households in the 1980s, only rudimentary graphics were available (if any), such as drawing large blocks with some basic colors, or drawing pixels at specific positions on a low resolution screen. For daily use, typed commands were still largely how people interacted with the operating system of their computer, and for many of their programs as well.

    vic20.jpg

    Figure 4: A photo of a Vic-20 BASIC program displayed on a CRT television.

    If a program did offer some kind of graphical user interface, at the time there were no standards and programs by different developers would often have wildly different interfaces, keyboard shortcuts, and styles.

  • Who uses command line programs these days?

    In a way, there are still challenges around different types of interfaces. We have computers running different operating systems like Windows, Mac OS, Linux, and Unix, we have smartphones running their own operating systems as well. Beyond that, people might install different types of software or configure their personal computers differently. Under the surface, however, there is still some form of command line, though the commands may differ between a Windows PowerShell and a Linux/UNIX Bash system. So, what do these non-GUIs offer in utility?

    Consistency
    Sometimes I try to help out a person on a Mac computer, though I have almost no experience with that operating system. Often, I find it more comfortable to navigate through the Mac Terminal to utilize the UNIX-like commands that are in common with Linux - such as ls to list files and folders in a directory. Rather than have to learn a GUI for a system I'll never use on my own, I can interact with it with a "common tongue" - the command line. :)
    Scriptability
    Think of the effort it takes to automate doing anything with a GUI - you'll need some kind of program or library that allows moving the mouse to some (x,y) position, hit mouse clicks, type keys, etc. And even then, windows in a GUI might move around. It's much easier to write a program script to automate running commands from a command line instead - it's just text commands!
    Speed
    Command lines generally can be more responsive than a GUI. I find that even on computers that aren't too old, Windows slogs along. Command lines don't necessarily use your computer's graphics card to make the windows semi-transparent and animate when they perform some operation. And in some cases it can be faster to type something like mkdir cs200 cs235 cs250 to create three folders than to go through the steps to move mouse to "File", move mouse to "New Folder", click, type "cs200", repeat twice more.
    Portability
    Being able to navigate a command line also makes it much easier to access remote computers. While you can run a computer remotely and work with the graphical operating system, it's still usually pretty sluggish. Instead, you could SSH into a computer and work with it from the command line and quickly perform some operations that you need, which could be remotely launching a build, restarting a server, or even writing some notes.

    Most average computer users aren't using the command line in their personal lives, but it is still a hugely useful tool for people who work in IT and software. With IT, imagine you have to configure 20 computers per classroom, for all the classrooms in a building - would it be easier to manually go configure those computers from their GUIs, or to write a script and run that script on each computer?

  • Why Computer Science courses don't (usually) teach GUI creation
    GUI libraries change over time
    Java and C# have both gone through various different systems for creating GUIs, such as WPF for old Windows programs, Swing for Java-based programs. But eventually they go out of date and stop being used.
    C++ isn't "owned" by a company like Microsoft or Oracle
    so there's no company to package some kind of UI library as the de-facto standard for C++ GUI development. There are open source tools (wxWidgets, Qt) and proprietary tools, but who knows what any given company uses for their software - if they even write desktop software at all anymore, anyway. Most things are HTML/CSS/JS based now. :)
    The study of Computer Science is more about how computers work rather than the design of front-ends.
    Adding a GUI layer on top of learning core C++ or about data structures can make the course content less focused and make it hard to understand the important parts on its own.
  • Command line programs on your own computer

    If you're on Windows open up the PowerShell. If you're on Mac open up the Terminal. If you're on Linux open up your favorite Terminal emulator or whatever. :) Let's look at some basic programs that might be useful.

    • ping URL - The ping program allows us to send multiple packets to some URL. I usually just use this to see if my internet has gone down or if a site is just down. I still, by habit, use ping yahoo.com even though I haven't used Yahoo in years. (FYI: Use CTRL+C to end a command line program if it continues looping.)
    rachelwil@rachelwil-GF63-Thin-9SC:~$ ping yahoo.com
    PING yahoo.com (98.137.11.163) 56(84) bytes of data.
    64 bytes from media-router-fp74.prod.media.vip.gq1.yahoo.com (98.137.11.163): icmp_seq=1 ttl=49 time=60.6 ms
    64 bytes from media-router-fp74.prod.media.vip.gq1.yahoo.com (98.137.11.163): icmp_seq=2 ttl=49 time=63.1 ms
    64 bytes from media-router-fp74.prod.media.vip.gq1.yahoo.com (98.137.11.163): icmp_seq=3 ttl=49 time=85.4 ms
    64 bytes from media-router-fp74.prod.media.vip.gq1.yahoo.com (98.137.11.163): icmp_seq=4 ttl=49 time=69.7 ms
    ^C
    --- yahoo.com ping statistics ---
    4 packets transmitted, 4 received, 0% packet loss, time 3004ms
    
    • mkdir FOLDER1 FOLDER2 FOLDER3 - The mkdir (make-directory) program allows you to create 1 or more folders within the directory that you're currently in.
    • ls - The ls (list) program shows you all the files and folders in the directory that you're currently in.
    • pwd - The pwd (present working directory) program shows you the path of the directory that you're currently in.
    rachelwil@rachelwil-GF63-Thin-9SC:~/test-folder$ mkdir folderA folderB folderC
    
    rachelwil@rachelwil-GF63-Thin-9SC:~/test-folder$ ls
    folderA  folderB  folderC
    
    rachelwil@rachelwil-GF63-Thin-9SC:~/test-folder$ pwd
    /home/rachelwil/test-folder
    

    Commands that you can use from the terminal can also be called from C++ by using the system() function.

  • See also

😺 Unit 00/01/02 Status Update - Welcome, Setup, Exploring Data, CS 200 Basics Review (U01.SUP)

WEEK 2 - JAN 22

UNIT 03: CS 200 Review - Functions and classes

πŸ“–οΈ Reading - CS 200 Review - Functions and classes (U03.READ)

Make sure to also view the "C++ Quick Reference" part of this textbook for a reference guide on basic C++ syntax.


  • Functions
    • Function headers

      A function header contains the following information:

      RETURNTYPE FUNCTIONNAME( PARAMETERLIST )

      • The return type is the type of data returned (like an int), or void for functions that don't need to return any data.
      • The function name follows the same naming rules as a variable - letters, numbers, and underscores allowed, no spaces or other special characters.
      • The parameter list is a series of variable declarations to be used within the function. These parameters are assigned values during the function call, when arguments are provided.

    • Function declarations

      Function declarations should go in .h files. A function declaration is the function header, with a semicolon at the end:

      int Sum( int a, int b );
      void DisplayMenu();
      

    • Function definitions

      Function definitions should go in .cpp files. A function definition is the function header, plus a code block, starting and ending with curly braces {}:

      int Sum( int a, int b )
      {
        int result = a + b;
        return result;
      }
      
      void DisplayMenu()
      {
        cout << "1. Add items" << endl;
        cout << "2. Subtract items" << endl;
        cout << "3. Quit" << endl;
      }
      

    • Function calls

      Function calls will happen within other functions. A function call includes the function's name, input arguments to be passed in, and the return data will need to be stored in a variable, if applicable.

      int main()
      {
        DisplayMenu(); // Function call
        int choice;
        cin >> choice;
      
        if ( choice == 1 )
        {
          int num1, num2, result;
          cout << "Enter num1: ";
          cin >> num1;
          cout << "Enter num2: ";
          cin >> num2;
      
          result = Sum( num1, num2 ); // Function call
          cout << "Result: " << result << endl;
        }
        // ... etc ...
      
        return 0;
      }
      

    • Header files

      Function declarations should go in header files (.h files).

      Header files must have file guards, this prevents the .h file from being "copied" into multiple files, which will cause a "duplicate code" build error.

      #ifndef _FILENAME_H
      #define _FILENAME_H
      
      // Code goes here
      
      #endif
      

      For the file guard, a label like _FILENAME_H is used. This must be unique for each file!

      Visual Studio supports using #pragma once for .h files, but this is not cross platform, so you should use these preprocessor file guards!


    • Source files

      Function definitions should go in source files (.cpp files).

      File guards are not needed in .cpp files.


    • Using your functions in other files

      Any file that utilizes your function must include the .h file:

      #include "MyFunctions.h"
      

      Note that something like #include <iostream> is used for including libraries that are not inside the project. Using "" is for including files that are in your project.

      NEVER INCLUDE .cpp FILES, THIS WILL GENERATE ERRORS!


    • Function overloading

      Multiple functions can have the same name as long as their function signatures are different. This means that two functions with the same name either need a different number of parameters, or different parameter data types, so that they can be unambiguously identified.

      AMBIGUOUS:

      void MyFunction( int a, int b );
      
      void MyFunction( int num1, int num2 );
      

      UNAMBIGUOUS:

      void MyFunction( int num1, int num2 );
      
      void MyFunction( string name1, string name2 );
      
      void MyFunction( int oneNum );
      

  • Structs and classes
    • Accessibility levels

      We can specify accessibility levels for struct and class members. This information dictates where a struct/class' internal contents can be accessed from:

      Accessibility level Class' functions? Child's functions? External functions?
      private βœ… ❌ ❌
      protected βœ… βœ… ❌
      public βœ… βœ… βœ…
      • private members can only be accessed from the class' own functions.
      • protected members can only be accessed from the class' own functions, and also the functions of any other classes that INHERIT from this class.
      • public members can be accessed from anywhere in the program, including functions that are not a part of the class.
    • Structs

      Structs are usually used to store very basic structures, often with just variable data and no functions.

      struct Coordinate
      {
        float x, y;
      };
      

      Struct declarations should go in their own .h file, usually the filename will match the name of the class, such as "Coordinate.h".

      All .h files need file guards!

      Also note that at the closing curly brace } of the struct declaration there is a semicolon - this is required!

      Members of a struct are public level accessibility by default.


    • Classes

      Classes are meant for more complex structures.

      class CLASSNAME
      {
          public:
          // Public members
      
          protected:
          // Protected members
      
          private:
          // Private members
      };
      

      Class declarations should go in their own .h file, and class function definitions should go in a corresponding .cpp file!

      It is best practice to make member variables of a class private, and only provide indirect access to this data via functions.

      Example: Product.h

      #ifndef _PRODUCT_H  // File guards
      #define _PRODUCT_H  // File guards
      
      #include <string>
      using namespace std;
      
      class Product
      {
          public:
          // Constructors:
          Product();
          Product( string newName, float newPrice );
      
          // Destructor:
          ~Product();
      
          // Setters:
          void SetName( string newName );
          void SetPrice( float newPrice );
      
          // Getters:
          string GetName() const;
          float GetPrice() const;
      
          private:
          string m_name;
          float m_price;
      }
      
      #endif
      

      Example: Product.cpp

      #include "Product.h"
      
      // Constructors:
      Product::Product()
      {
        m_name = "unset";
        m_price = 0;
      }
      
      Product::Product( string newName, float newPrice )
      {
        SetName( newName );
        SetPrice( newPrice );
      }
      
      // Destructor:
      Product::~Product()
      {
        cout << "Bye." << endl;
      }
      
      // Setters:
      void Product::SetName( string newName )
      {
        m_name = newName;
      }
      
      void Product::SetPrice( float newPrice )
      {
        if ( newPrice >= 0 )
        {
          m_price = newPrice;
        }
      }
      
      // Getters:
      string Product::GetName() const
      {
        return m_name;
      }
      
      float Product::GetPrice() const
      {
        return m_price;
      }
      

      Accessor/Getter functions:

      • Don't take in any input data (no parameters).
      • Return the value of a private member variable (has a return).
      • Should be marked const to prevent data from changing within the function. (This is "read only")

      Mutator/Setter functions:

      • Take in an input value of the new data to be stored (has a parameter).
      • Generally doesn't return any data (no return, void return type).

      Constructor functions:

      • Called automatically when a new object of that class type is created.
      • Can overload.

      Destructor functions:

      • Called automatically when an object of that class type is destroyed/loses scope.
      • Cannot overload.

    • Class objects / instantiating a class object

      Once we've declared a class we can then declare variables whose data types are that class:

      PlayerCharacter bario;
      NonPlayerCharacter boomba;
      

      In this example, bario is a PlayerCharacter object, aka an "instantiation of the PlayerCharacter object".


    • Class inheritance

      A class (called a subclass or a child class) can inherit from another class (called a superclass or a parent class). Doing this means that any protected and public members are inherited by the child class. This can be useful for creating more "specialized" versions of something, storing the shared attributes of a set of items in a common "parent" class.

      Example:

      class Character
      {
        void SetPosition( float x, float y );
        void Move( float xAmount, float yAmount );
      
        protected:
        float x, y;
      };
      
      class PlayerCharacter : Public Character
      {
        public:
        void GetKeyboardInput();
      
        private:
        int totalLives;
      };
      
      class NonPlayerCharacter : public Character
      {
        public:
        void ComputerDecideMove();
      
        private:
        bool attackPlayer;
      };
      

    • Class composition

      Class composition is where a class contains class objects as member variables. This is another form of object oriented design where a class might "contain" traits that could be inherited, but instead encapsulates them into a sub-object.

      Example:

      class MovableObject
      {
        void SetPosition( float x, float y );
        void Move( float xAmount, float yAmount );
      
        private:
        float x, y;
      };
      
      class PlayerCharacter
      {
        public:
        void GetKeyboardInput();
      
        private:
        int totalLives;
        MovableObject mover;
      };
      

  • Review questions:
    1. What is included in a function signature?
    2. Build the function signature given the following specs:
      • The function is naemd "GetValidInput".
      • The function returns an integer.
      • The function takes an int for the minimum valid value, and an int for the maximum valid value.
    3. #include <THING> is used for … ? and #include "THING" is used for … ?
    4. Best practice is to put file guards in every .h file you create. (true/false)
    5. Arguments are … ? and Parameters are … ?
    6. What type of code goes in each type of file? (Function declaration/function definition) vs. (.cpp file/.h file)
    7. Class members are (private/protected/public) by default.
    8. What kind of member function is the following?

      void Pet::XYZ( int age )
      {
        m_age = age;
      }
      
    9. What kind of member function is the following?

      int Pet::XYZ()
      {
        return m_age;
      }
      
    10. This kind of class function will be called automatically when an object of that class type is instantiated…
    11. This kind of class function will be called automatically when an object of that class type is destroyed…

πŸ”Ž Concept Intro - CS 200 review: Functions and classes (U03.CIN)

πŸ§‘β€πŸ”¬ Lab - CS 200 Review - Functions and classes (U03.LAB)

About: This assignment has you working with functions, structs, and classes.

pixel-goals.png Goals:

  • Practice with functions
  • Practice with structs
  • Practice with classes

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Set up: Creating a new branch in your repository

    git-harddrive-opengitbash.png

    First, Open up Git Bash (Windows) or your Terminal (Linux/Mac) from within your repository directory. Make sure you're inside the repository folder, which should be named something like YOURSSO-CLASS.

    Next, create a new branch in the repository with the checkout command: git checkout -b u03_review

    $ git checkout -b u03_review
    Switched to a new branch 'u03_review'
    

    git_newbranch.png

    Now your folder is on the new branch and you can start working on the assignment.


  • Download the starter code

    Download the following starter code and unzip it to your repository folder.

    lab_u02_CPPBasicsReview_extractedfiles.png

    All starter files:

    .
    ├── INFO.h
    ├── main.cpp
    ├── Project_CodeBlocks
    │   ├── FunctionsClasses.cbp
    │   ├── FunctionsClasses.depend
    │   └── FunctionsClasses.layout
    ├── Project_Makefile
    │   └── Makefile
    ├── Project_VisualStudio
    │   ├── FunctionsAndClasses.sln
    │   ├── FunctionsAndClasses.vcxproj
    │   └── FunctionsAndClasses.vcxproj.filters
    ├── u03_Review_FuncClass
    │   ├── P1_functions
    │   │   └── U03_P1_Sale.cpp
    │   ├── P2_struct
    │   │   └── U03_P2_Room.cpp
    │   ├── P3_class
    │   │   └── U03_P3_Employee.cpp
    │   ├── U03_Headers.cpp
    │   └── U03_Headers.h
    └── Utilities
        ├── Colors.hpp
        ├── Helper.cpp
        └── Helper.hpp
    

    Visual Studio users:

    Open the Project_VisualStudio folder and open the FunctionsAndClasses.sln file to get started.

    Code::Blocks users:

    Open the Project_CodeBlocks folder and open the FunctionsClasses.cbp file to get started.

    Build from Terminal/VSCode:

    In the Project_Makefile directory you'll find a makefile you can use to build the project. Use the command make in the directory, and it will generate an executable file that you can run like this: ./CPPReviewClasses_out

    DO NOT CREATE YOUR OWN VISUAL STUDIO OR CODE::BLOCKS PROJECT.

    DO NOT REORGANIZE THE CODE.

    KEEP YOUR REPOSITORY DIRECTORY AND FILES CLEAN!!


  • How the program and tests work

    Once you run the program, you will see two options: Run the AUTOMATED TESTS or run the PROGRAMS manually.

    1. Run AUTOMATED TESTS
    2. Run PROGRAMS
    >>
    

    You can use the AUTOMATED TESTS to verify your work. Tests will either be marked as PASS or FAIL.

    Note that the first "automated" test in the set requires user input once the program is implemented. The test text will describe what inputs it is expecting, the user can set those prices, and then use checkout to finish the test.

    You can also MANUALLY RUN THE PROGRAMS to test them yourself:

    EMPLOYEE LOOKUP
    INDEX     ID        NAME                          WAGE
    --------------------------------------------------------------------------------
    0         123       Ochoa                         16.65
    1         234       Bakalar                       16.65
    2         353       Grubb                         16.65
    
    Enter Employee ID to find index of: 234
    RESULT: Index = 1
    

    To begin with, the functionality won't work properly - you will need to implement the functions themselves. We will step through that in a moment.


  • File: INFO.h

    This file contains a string constant. Replace "Your Name" with your actual name. This is to help me with grading.

    const std::string STUDENT_NAME = "Your Name";
    

    As you work through the coding assignments, if you have any questions or get stuck, feel free to ask for hints from the instructor.


  • File: U03_P1_Sale.cpp

    This code file is under u03_Review_FuncClass/P1_functions/PizzaSlices.cpp.

    ########################################
    MAIN MENU
    
    Current transaction amount: $8.52
    1. Add item
    2. Checkout
    Enter a number between 1 and 2: 2
    
    
    ########################################
    RECEIPT
    
    Transaction total: $8.52
    Tax rate: %9.61
    After tax: $9.34
    RESULT: 9.34
    

    The U03_P1_Sale.cpp file contains several functions that you'll implement for this program.

    void DisplayMenu()

    • This function is responsible for showing the numbered menu, with 1 for "Add item" and 2 for "Checkout".
    • This function does not get any input! Just use cout statements here.

    void FormatUSD( float price )

    • This function takes in the price and formats it as USD. You can set up the precision like this first: std::cout << std::fixed << std::setprecision( 2 );
    • Afterwards, use cout to display "$" and then the price .

    float GetTaxPercent()

    • This function only returns the tax percent. Return a value of 9.61.

    float GetPricePlusTax( float original_price, float tax_percent )

    • Calculate the price plus tax given the inputs.
    • Math: Given original price \(o\) and tax percent \(t\), the new price \(p\) is: \(p = o + ( o \cdot t )\)

    float GetNewPrice()

    This function needs to ask theuser to enter the price of a new item. Use cout and cin statements here, and return the new price they entered afterwards.


    int GetChoice( int min, int max )

    • This function should ask the user to enter a number between min and max.
    • Get the user's input, then use a while loop to check for invalid input: While input is invalid, display an error and have the user enter their choice again.
      • Input is invalid if user_choice < min OR user_choice > max.
    • After the while loop we know the user's input is valid, so we return that value.

    float SaleProgram()

    1. Create a transaction_total float variable and initialize it to 0.
    2. Create a is_running boolean variable and initialize it to true.
    3. Create a program loop: while( is_running ). Within the loop, do the following:
      1. Display the current transaction_total, formatted as USD.
      2. Display the main menu (call the DisplayMenu function).
      3. Get the user's choice (call the GetChoice function, store result in an integer).
      4. Use if/else if statements or a switch statement to check the user's choice:
        • If their choice is 1, then call the GetNewPrice function. Store its result in a float variable, then add the result onto the transaction_total.
        • Otherwise if their choice is 2, then set is_running to false.
    4. After the program while loop, display the RECEIPT. This should include:
      • The transaction_total formatted as USD,
      • the tax rate,
      • the price after tax formatted as USD.
    5. Before the end of the function, return the final_price that was calculated.

    Note that your program output does not need to perfectly match mine. The important thing is the correct information, and readability.

    ########################################
    MAIN MENU
    
    Current transaction amount: $8.52
    1. Add item
    2. Checkout
    Enter a number between 1 and 2: 2
    
    ########################################
    RECEIPT
    
    Transaction total: $8.52
    Tax rate: %9.61
    After tax: $9.34
    RESULT: 9.34
    

    The "automated tests" for this one will require that you manually input information. It will tell you what inputs it is expecting, and then tell you if it passes or not.

    MANUAL TEST, check functionality:
     * DisplayMenu() - main menu shown in loop
     * FormatUSD( p ) - make sure dollar amounts are formatted
     * GetTaxPercent() - Make sure on checkout the tax % shows up
     * GetPricePlusTax( p, t ) - Make sure After Tax price is correct
     * GetChoice( min, max ) - Make sure it disallows numbers less than min / greater than max
     * GetNewPrice() - Make sure function is called to get float values
    --------------------------------------------------------------------------------
    
    Enter prices:     2.25, 4.00, 5.30    <<<<<<<<< This tells you what to enter
    Expected output: 12.66
    

  • File: U03_P2_Room.cpp

    This code file is under u03_Review_FuncClass/P2_struct/U03_P2_Room.cpp.

    ROOM BUILDER
    Enter room length: 15
    Enter room width: 20
    RESULT:
     * width:     20
     * length:    15
     * area:      300
     * perimeter: 70
    

    Within this file, a Room struct has been declared:

    struct Room
    {
      float width;
      float length;
      float area;
      float perimeter;
    };
    

    You will need to implement two functions here:

    Room BuildRoom( float width, float length )

    float width Input parameter The width of a room
    float length Input parameter The length of a room
    Room Return output The built Room struct with all the data set up

    In this function, set the newRoom's member variables, utilizing the input parameters.

    • newRoom.width: Copy the input parameter width.
    • newRoom.length: Copy the input parameter length.
    • newRoom.area: Calculate the area (\(w \cdot l\))
    • newRoom.perimeter: Calculate the perimeter (\(2w + 2l\))

    Make sure the newRoom is being returned at the end of the function.


    void U03_Program2_Program()

    In this function ask the user to enter a room length and a room width. Store their inputs in float variables. Call the BuildRoom function, passing in the data, and storing the returned result in a new Room variable. Finally, use cout to display all of the Room's information.


    Afterwards, the automated tests will pass:

    [PASS]  TEST 1, StudentCode(5.00, 7.00) = [width=5.00, length=7.00, area=35.00, perimeter=24.00]
    [PASS]  TEST 2, StudentCode(3.00, 11.00) = [width=3.00, length=11.00, area=33.00, perimeter=28.00]
    

  • File: U03_P3_Employee.cpp

    This code file is under u03_Review_FuncClass/P3_class/U03_P3_Employee.cpp.

    EMPLOYEE LOOKUP
    INDEX     ID        NAME                          WAGE
    --------------------------------------------------------------------------------
    0         123       Ochoa                         16.65
    1         234       Bakalar                       16.65
    2         353       Grubb                         16.65
    
    Enter Employee ID to find index of: 353
    RESULT: Index = 2
    

    This program has a list of employees, each with an employee_id, name, and hourly_wage. The user can enter in an employee_id in this program, and the associated array index will be returned.

    Within the file is a class Employee declaration and empty member functions. You will implement these and the basic GetEmployeeIndex search function.

    Note: I have all the class functions declared here inside the class declaration, for program simplicity. When implementing class functions, generally the class declaration goes in a .h file and the function definitions go in a .cpp file, and look like int Employee::GetId() const.

    Employee( int new_id, std::string new_name, float new_wage )

    This is the constructor function for the Employee class. Within this function, initialize each of the private member variables (employee_id, name, hourly_wage) to the values passed in as input parameters (new_id, etc.)


    int GetId() const

    Within this function return the value of the private member variable, employee_id.

    Note that this function is const, which means that changes to member variables are not permitted within.


    std::string GetName() const

    Within this function return the value of the private member variable, name.


    float GetWage() const

    Within this function return the value of the private member variable, hourly_wage.


    int GetEmployeeIndex( std::vector<Employee> employee_list, int lookup_id )

    std::vector<Employee> employee_list Input parameter The list of all employees to search through
    int lookup_id Input parameter The employee ID of the person we're looking for
    int Return output The associated index of the employee with the matching ID.
    • Within this function use a for loop to iterate over all the elements of the employee_list, using the index as the looping variable. Within the loop:
      • If the employee at this index's ID matches the lookup_id, then return the current index.
    • At the end of the function, after the for loop, if nothing has been returned yet that means there's no matches - return a -1 here.

    For the for loop:

    • Initialization: size_t index = 0
    • Condition: index < employee_list.size()
    • Update: index++

    To access a certain element of the array: employee_list[index] , then you can also call its functions: employee_list[index].GetId()


    Afterwards, the automated tests should pass:

    [PASS]  TEST 1, StudentCode([11/100/AAA],22/200/BBB],33/300/CCC],44/400/DDD]], 44) = 3
    [PASS]  TEST 2, StudentCode([100/11/aardvark],200/12/bear]], 200) = 1
    [PASS]  TEST 3, StudentCode([100/11/aardvark],200/12/bear]], 444) = -1
    

  • Turning in the assignment

    Once you're done with the assignment, you need to add, commit, and push your changes to the server.

    lab_u02_CPPBasicsReview_gitserver.png

    Save your changes to the server:

    1. Open git bash from within your repository folder on your harddrive. git-harddrive-opengitbash.png
    2. Use git status to review what files have changed since your last commit. git_status.png
    3. Use git add . to add all changed files to a changeset. git_add.png
    4. Use git commit -m "MESSAGE" to create a snapshot of your current changes. (Replace "MESSAGE" with something more descriptive.) git_commit.png
    5. Use git push -u origin u03_review to push all your commits to the GitLab server, on the u03_review branch. git_push.png

    You can continue making changes and do additional add, commit, and push commands as well.

    Get everything ready for next assignment:

    1. Use git checkout main to change back to the main branch to prepare for the next assignment. (You can always use git checkout u03_review to return to this branch and code.)
    2. Use git pull to pull latest from the server. (This might happen if the instructor adds new starter code or makes modifications.)

    git_newbranch.png

    Create a Merge Request on the GitLab website:

    After pushing your changes to your branch, the GitLab webpage will show an update:

    gitlab_web_mergerequest1.png

    Click the blue "Create merge request" button.

    gitlab_web_mergerequest2.png

    Give your Merge Request a descriptive name, such as the unit the assignment belongs to.

    At the bottom of the page, click on the blue "Create merge request" button.

    gitlab_web_mergerequest3.png

    Your merge request will now be created. You don't need to do anything further on this page.

    • Approve button: The instructor may mark the merge request as approved.
    • Merge button: You won't be able to press the Merge button; the instructor will merge the code once they've reviewed your work and approved it.

    Turn in the Merge Request URL on Canvas:

    Once you've created a merge request, copy the URL of the merge request on the GitLab.com webpage:

    merge_request_url.png

    Back on the Canvas page, locate the assignment and click the link:

    canvas_assignment.png

    Then click on "Start Assignment":

    canvas_start_assignment.png

    Paste in the URL to your Merge Request in the "Website URL:" textbox, then click "Submit Assignment" to finish the turn in.

    canvas_submit_mergerequest.png

    "What happens if I make mistakes?"

    lab_u03_CPPClassReview_Mistakes.png

    If you make mistakes, I will be able to mark certain lines of code in the merge request and add a comment as to what isn't correct, similar to how your coworkers would also review your code on the job.

    After receiving feedback, you can go back and make updates to your code and resubmit it. You will need to do the following:

    1. Check out the branch you made the changes on: git checkout BRANCHNAME
    2. Make the fixes to the code.
    3. Make a new add/commit/push:
      1. git add .
      2. git commit -m "MESSAGE"
      3. git push -u origin BRANCHNAME
    4. The merge request automatically gets updated; re-submit the merge request link in Canvas to mark it as ready to be re-graded.

πŸ‹οΈ Exercise - Debugging functions and classes (U03.EXE)

Answer these questions on the related Canvas assignment to check your work.

  1. It's important to know what I'm talking about when I use certain terms. Please identify what each of these pieces of code are called. Identify whether each of the following is a function declaration, function definition, or a function call.

    • (Declaration / Definition / Call?)

      void DisplayMenu()
      {
        cout << "1. Save" << endl;
        cout << "2. Load" << endl;
      }
      
    • (Declaration / Definition / Call?)
    DisplayMenu();
    
    • (Declaration / Definition / Call?)
    void DisplayMenu();
    
  2. Given the following function definition:

    int Sum( int a, int b )
    {
      return a + b;
    }
    

    And elseware in the program the function is called:

    int num1, num2;
    
    cout << "Enter num1: " << num1 << endl;
    cin >> num1;
    cout << "Enter num2: " << num2 << endl;
    cin >> num2;
    
    int result = Sum( int num1, int num2 );
    cout << "Result: " << result << endl;
    

    When trying to build the program, we get compile errors: expected primary-expression before 'int'. It points to the "int result = Sum( int num1, int num2 );" line. The error isn't very descriptive, but it's complaining about an "int" keyword. What is the error with the highlighted line of code?

  3. The following functions are defined:

    int Sum( int a, int b )
    {
      return a + b;
    }
    
    float Sum( float a, float b )
    {
      return a + b;
    }
    

    And is used elseware in the program. But when building, we get an error: error: call of overloaded β€˜Sum(int&, float&)’ is ambiguous| What is causing this error to occur?

  4. The following program has been started:

    #include <iostream>
    using namespace std;
    
    struct Vector
    {
      float x, y;
    }
    
    int main()
    {
      Vector my_vec;
    
      return 0;
    }
    

    But even at this early point, there is a build error: error: expected β€˜;’ after struct definition| What is the cause of this build error?

  5. In "Student.h", this class is declared:

    class Student
    {
      public:
      void Setup( string newName );
    
      private:
      string m_name;
    };
    

    And in "Student.cpp", this function is defined:

    void Setup( string newName )
    {
      m_name = newName;
    }
    

    However, a build error occurs: In function β€˜void Setup(std::string)’:| error: β€˜m_name’ was not declared in this scope; did you mean β€˜tzname’?| Ignore the "tzname" part of the error message, it doesn't help us. There is something missing from the code which is causing this build error. What is it?

UNIT 04: Debugging and testing

πŸ“–οΈ Reading - Debugging and testing (U04.READ)

reading_u04_DebugTest_tests.png

  • Testing: Validating your work

    How do you know that your program actually works?

    Sure, you may have manually tested your code, running the program and typing in test data and checking the output.

    After a while, manual testing becomes a chore. Maybe you start entering "asdjhfklq" as the test data and just make sure nothing breaks while running the program.

    But are you sure your program runs, doesn't crash, and gives the correct output for all reasonable cases?

    reading_u04_DebugTest_hopeitworks.png

    A skill that is good to develop as a software developer is how to test and validate your work. You can break down parts of your code into little transactions of "inputs" and "outputs", and then develop test cases.

    Test case: A test case is a single test. You specify some input(s) that you will give your program, and the expected output(s) it should return.

    When you run the actual program with your inputs, it will return actual output(s). Compare the actual output with the expected output to validate whether the program worked as expected.

    Test cases can be built without any of the program built so far. In fact, it can be handy to write your tests ahead of time so you have a better understanding of how things are supposed to work.

    Example: Bank withdrawals In this example, the program keeps track of a bank balance and the amount the user wants to withdraw, but it shouldn't let the balance fall below 0.

    Test case Input(s) Expected output Actual output
    1 balance = 100, withdraw = 10 balance is now 90 (enter after testing)
    2 balance = 0, withdraw = 100 can't withdraw!  
    3 balance = 100, withdraw = 100 balance is now 0  

    You would make a list of inputs and what should be the result for each case, and then run your program and check each scenario. If something doesn't match, it could mean that there's an error in a calculation somewhere, or other logic in the program. (And yes, sometimes tests can be wrong, too.)


    • The role of testing in software development

      Companies that write big software products will often have multiple types of testing that they do to ensure the validity of their software.

      QA (Quality Assurance) people generally will be responsible for writing test cases and running through *manual tests to validate the software works as intended. They might also write test scripts that automate going through the UI of the software, or automate other parts of testing.

      Software Engineers will regularly need to be able to write unit tests along with whatever features they're working on in order to validate that their new feature works as intended and they will run older unit tests to ensure that their new feature doesn't break anything already there.

      Testing is an important part of software development and, from a student perspective, it is also a handy tool in ensuring that your programming projects work as intended before turning them in.


    • Designing tests
    • Test cases

      When writing out test cases, it's important to try to think of valid inputs and invalid inputs that could be entered in, and account for how the program will respond to that input.

      There are three main pieces to defining a test case:

      1. Inputs entered in.
      2. What the expected output of the program is.
      3. What the program actually did.

      By comparing the expected result with the actual result, you can validate whether your program is working as intended. If they match - the test passes. If they don't match - the test failed.

      If a test fails, you can investigate the expected output and the actual output to help you get an idea of perhaps where it went wrong - perhaps a formula is wrong somewhere.

      reading_u04_DebugTest_validation.png

      Example: Calculate price plus tax When ringing up a product at a store, there will be a price for that product and a sales tax that gets added onto it. Using a test case can help us make sure that the program's calculations are correct.

      Let's say that the programmer implemented the formula like this: total = price + tax

      We could validate the program's actual output vs. the expected output, and see that there's an error.

      Test case Input(s) Expected output Actual output
      1 price = $10.00, tax = 0.09 total = $10.90 total = $10.09 (FAIL)
      2 price = $5.45, tax = 0.06 total = $5.78 total = $5.51 (FAIL)
      3 price = $2.25, tax = 0.095 total = $2.46 total = $2.35 (FAIL)

      What is the problem with the formula? Well, we don't just add tax as a flat number; "0.09" is supposed to represent 9%, not 9 cents!

      The correct formula would be: total = price + (price * tax)


    • Manual testing

      reading_u04_DebugTest_dontexplode.png

      You can manually test your program effectively by documenting your test cases and running through each of them each time you decide to test your program. Of course it can take a while, but by using your test cases instead of entering in gibberish or only testing one scenario, you make sure your program works, even as you keep adding onto it.


    • Automated unit tests

      reading_u04_DebugTest_inputoutput.png

      Our test cases can be a handy roadmap to follow when running through your program's various features and validating it all works manually, but you can speed up the process by letting the computer do these checks for you.

      After all, when you manually validate outputs yourself, you're basically asking yourself,

      "If the actual output DOES NOT match the expected output, then the test failed."

      A test that only validates one function is known as a unit test, testing the smallest possible unit of a program. You could also write automated tests that checks several functions, but unit tests are good to validate each function on its own, independently of the rest.

      • Unit tests in the real world

        "Tests just mean more code to maintain. We don't keep tests."

        I was told this at a job interview once. I didn't take the job.

        At companies that maintain tests, the software engineers will usually write unit tests to validate their work as they're adding in new functionality or making modifications to the existing code.

        This means that ideally each feature of the program has one or more test to validate that it works.

        This also means that, when adding or modifying features, you can run the entire test suite to make sure that all features still work.

        Many software companies have something called Continuous Integration (CI), which is an automated system that kicks off a build of the software and runs all the tests each time a developer commits code to the repository.

        Example: Area function Let's say we have a function with the header

        int Area( int width, int length )

        and we don't necessarily know what's in it (we don't need that to test). We can write a series of test cases with inputs we specify and outputs we know to be correct in order to check the logic of the Area function…

        Test case Input(s) Expected output Actual output
        1 width = 10, length = 20 120  
        2 width = 2, length = 5 10  

        …and so on.

        We could use this to manually test by calling Area and checking the output it gives you, but you could also write a function to validate it for us

        reading_u04_DebugTest_tedious.png

        Here's some example code of a function that tests Area() for us.

        void Test_Area()
        {
          // Variables used for each test
          int width, length;
          int expectedOutput;
          int actualOutput;
        
          // Test 1
          width = 10;
          length = 20;
          expectedOutput = 200;
          actualOutput = Area( width, length );
        
          if ( actualOutput != expectedOutput ) {
            cout << "Test failed!"
                 << "\n width: " << width
                 << "\n length: " << length
                 << "\n expected output: " << expectedOutput
                 << "\n actual output: " << actualOutput 
                 << endl;
          }
          else {
            cout << "Test 1 passed" << endl;
          }
        }
        

        You can add as many tests as you'd like inside one function, testing different permutations of inputs against outputs to make sure that any reasonable scenarios are covered. To test, just call the function in your program, and check the output:

        Test failed!
        width: 10
        length: 20
        expected output: 200
        actual output: 30
        

        If the test fails, you can cout your variables' values to help you figure out what might be wrong with the logic.

      • Other automated tests

        Unit tests test the smallest unit of code - a single function.

        (It should be the smallest "unit", if your function is really long you should probably split it up!)

        There are other types of automated tests as well, and software tools to help developers create and run those tests.

        An integration test is when you test multiple units together. A unit might work fine on its own, but when multiple units are put together, the resulting system may not work as intended.

        (Look up "integration test unit test" online for some good gif examples. :))

        reading_u04_DebugTest_integrationtest.png

        Tests can also be written to test user interfaces of a program, running a script to drive the cursor around the screen, or defining text that should be typed into fields, running through features of the program from the user's perspective, and validating that data updates and is displayed when some action is performed from the UI.


  • Using debugging tools

    errorinmain.png

    Utilizing debugging tools are an important part of software development. Especially as you begin working in large codebases on the job, being able to step through the execution of a program to find variable values, how the program flows, and where exceptions are thrown is something you will have to do frequently.

    The lab contains more of the steps for learning these tools with your IDE, but here is some general information for now.


    Breakpoints

    reading_u04_DebugTest_setbreakpoint.png

    A breakpoint can be toggled in your code by clicking to the left of the code screen. The exact placement differs between IDEs, but usually a red circle pops up once clicked to show that the program execution will stop here once we reach it.

    You can set breakpoints in your program at various places to begin stepping through to see where the program goes, if it's "flowing" the way you expect it to, and to navigate into other function calls as they're hit.

    reading_u04_DebugTest_stepover.png

    The "next line" or "step over" button will let you move to the next line of code execution. If your pause line is at a function call, it will not enter the function, and just continue on the next line after the function call is done.

    reading_u04_DebugTest_stepinto.png

    The "step into" button will take you to inside a function if you're currently paused on a line that is a function call.


    Watch/Local/Auto window

    reading_u04_DebugTest_watchwindow.png

    Watch, Local, and Auto windows do similar things - they all display a variable's name and its current value (while paused in the program execution).

    Visual Studio has (and other IDEs may have) an auto view, which shows variables that are around the current statement we're paused on. The locals view shows variables that are currently in scope. (Information from https://learn.microsoft.com/en-us/visualstudio/debugger/autos-and-locals-windows?view=vs-2022)


    Call stack

    reading_u04_DebugTest_callstack.png

    The call stack will show you all the functions that have been called, leading up until the point where you are currently paused at. The current function is listed on the top, with the function immediately below it being the one that made the call to the function you're in. The function listed at the bottom is the "oldest" function, or the first one called, usually main().


  • Review questions:
    1. Given some input and expected output defined, a test would pass if…
    2. Let's say I've written a program that takes in two numbers as inputs and returns the sum as the output. For our first test case, let's say the inputs are 2 and 3. What would the expected output be?
    3. Let's say you need to write a program that will update a person's account balance after they deposit some money. What might be the input(s) / output(s)?
    4. Let's say your program is crashing somewhere. How might setting breakpoints be useful?
    5. We are paused in a program at this point:

      int a = 10;
      int b = 20;
      int result = Sum( a, b ); << Break here
      cout << result << endl;
      

      If we hit the step over button, what will the next line the debugger is paused at be?

    6. We are paused in a program at this point:

      int a = 10;
      int b = 20;
      int result = Sum( a, b ); << Break here
      cout << result << endl;
      

      If we hit the step into button, what will the next line the debugger is paused at be?

    7. Given the following call stack:

      Product::GetPrice()
      Store::PurchaseProduct()
      Store::MainMenu()
      main()

      What function are we currently paused in?

    8. Given the following call stack:

      Product::GetPrice()
      Store::PurchaseProduct()
      Store::MainMenu()
      main()

      Once the current function returns, which function will then be at the top of the call stack?

.πŸ”Ž Concept Intro - Debugging and testing (U04.CIN)

πŸ§‘β€πŸ”¬ Lab - Debugging and testing (U04.LAB)

pixel-goals.png Goals:

  • Practice with debugging tools:
    • Watch window
    • Call stack
    • Breakpoints
    • Step
  • Practice writing unit tests

pixel-turnin.png Turn-in:

  • 1. For the DEBUG assignment you'll be creating a text document to log your findings. (You can save it in your repository to submit via the merge request as well.)
  • 2. You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Set up: Creating a new branch in your repository
    1. Navigate to your class repository folder. Right-click the empty space and select GitBash Here (Windows). (Other platforms: Open this directory in your Terminal.)
    2. Create a new branch: git checkout -b u04_debugtest

  • Download the starter code

    Download the following starter code and unzip it to your repository folder.

    All starter files:

    .
    ├── Part1_Debug
    │   ├── engine
    │   │   ├── Helper.cpp
    │   │   ├── Helper.h
    │   │   ├── Log.cpp
    │   │   ├── Log.h
    │   │   ├── PpmImage.cpp
    │   │   ├── PpmImage.h
    │   │   ├── ScreenDrawer.cpp
    │   │   └── ScreenDrawer.h
    │   ├── Game.cpp
    │   ├── Game.h
    │   ├── images
    │   │   ├── food.ppm
    │   │   ├── kansascity.ppm
    │   │   ├── medical.ppm
    │   │   ├── olathe.ppm
    │   │   ├── overlandpark.ppm
    │   │   ├── raytown.ppm
    │   │   ├── safehouse.ppm
    │   │   ├── scavenger.ppm
    │   │   ├── zombie1.ppm
    │   │   └── zombie2.ppm
    │   ├── main.cpp
    │   ├── Player.cpp
    │   ├── Player.h
    │   ├── Project_CodeBlocks
    │   │   ├── log.txt
    │   │   ├── ZombieApoc.cbp
    │   │   ├── ZombieApoc.depend
    │   │   └── ZombieApoc.layout
    │   ├── Project_Makefile
    │   │   └── Makefile
    │   └── Project_VisualStudio
    │       ├── log.txt
    │       ├── ZombieApoc.sln
    │       ├── ZombieApoc.vcxproj
    │       └── ZombieApoc.vcxproj.filters
    └── Part2_Test
        ├── Functions.cpp
        ├── Functions.h
        ├── Project_CodeBlocks
        │   ├── WritingTests.cbp
        │   ├── WritingTests.depend
        │   └── WritingTests.layout
        ├── Project_Makefile
        │   └── Makefile
        └── Project_VisualStudio
            ├── WritingTests.sln
            ├── WritingTests.vcxproj
            └── WritingTests.vcxproj.filters
    
    • Please use the provided Visual Studio or Code::Blocks project files, don't make your own!
    • If you want to build the code from the terminal, go to the Project_Makefile directory and use the command make to generate an out file.

    DO NOT CREATE YOUR OWN VISUAL STUDIO OR CODE::BLOCKS PROJECT.

    DO NOT REORGANIZE THE CODE.

    KEEP YOUR REPOSITORY DIRECTORY AND FILES CLEAN!!


  • Part 1: Debug

    For this assignment you'll run an existing program. You don't need to do any programming - you will be utilizing the debugging tools of your IDE to answer the questions. You will need to create a text document (a MS Word / LibreOffice Write / Google Docs document).


    • Running the game as-is - Learning the controls

      lab_u04_DebugAndTest_gamerunning.png

      Run the game as-is and play a few rounds of it. You can choose to scavenge in the area you're currently at, or travel to another location. For this step, you're just getting used to how the game controls.


    • Using breakpoints - Pause program execution at some line

      Task:

      1. Set breakpoints at the start of Player::Player() in Player.cpp, and at the start of Game::Game() in Game.cpp.
      2. In the watch window, add the following (for the Player class):
        1. food
        2. maxHealth
        3. health
        4. location
      3. In the watch windwo, add the following (for the Game class):
        1. m_done
        2. m_day

      Observe the current values of those variables. Refer to the following documentation on the steps to do this.

      Video: https://youtu.be/BNaTN9KWmu4

      • Setting the breakpoint:

        Step 1: Set a breakpoint at the start of Player::Player():

        Player::Player()
        {
            food = 5;
        

        Set a breakpoint at the start of Game::Game():

        Game::Game()
        {
            m_day = 1;
        

        To set a breakpoint, you can locate a line of code and click on a region in your editor next to the line number that will enable a breakpoint. A breakpoint is a debugging tool that will cause the program execution to pause when that line of code is reached. While the program is paused, you can investigate values and step through the program flow one line at a time.

        lab_u04_DebugAndTest_00setbreakpoint.png

        Depending on your IDE you might need to click before or after the line number to set the breakpoint. You can also toggle breakpoints through the menu or keyboard shortcuts:

        IDE Menu Shortcut Image
        Visual Studio Debug, Toggle breakpoint F9 visualstudio_breakpoint.png
        Code::Blocks Debug, Toggle breakpoint F5 codeblocks_breakpoint.png
        XCode     xcode_breakpoint.png

        Next you'll need to run your program in debug mode:

        IDE Toolbar Menu Shortcut
        Visual Studio visualstudio_debug_toolbar.png visualstudio_debug_dropdownmenu.png F5
        Code::Blocks codeblocks_debug_toolbar.png codeblocks_debug_dropdownmenu.png F8
        XCode xcode_debug_toolbar.png    

        The program will build (if it hasn't already) and start running the program. When the program starts, it will hit the breakpoint and pause execution. Go back to your IDE to view where your program has paused.

      • Opening the variable investigation panes:

        At the bottom of your screen should be some debug panels. If they don't show up, you can usually open these under the Debug drop down menu.

        IDE Steps
        Visual Studio Debug, Windows, Watch window
        Code::Blocks Debug, Debugging wnidows, Watches
        XCode View, Debug area, Activate console

        Now you should have the watch windows open, which is a table that displays a Name (variable name) column and a Value column.

        lab_u04_DebugAndTest_01locals.png

      • Enter the names of the variables we want to track:

        In these watch windows, type in the names of the following variables:

        • Player member variables:
          • food
          • maxHealth
          • health
          • location
        • Game member variables:
          • m_day
          • m_done

        While the program is still paused, we will use the step features to step through each line of code one at a time and watch the variables as they're assigned values.

        Use the Step Over or Next Line to step down one line at a time until you get to the end of the function. As you go down each line, notice how the variable value changes in the watch window once its declaration line is finished.

        lab_u04_DebugAndTest_02stepover.png


    • Step into, step out - Investigating program flow

      Task: Set a breakpoint at the start of Game::Menu_Scavenge() and then use the Continue button to resume program execution. (Or if you closed the program, run it again and enter a name.)

      Select the "1. Scavenge here" option in the game and make sure the program execution pauses within this function.

      void Game::Menu_Scavenge()
      {
          bool actionSuccessful = true;
          int random = rand() % 5;
          RandomEvent_Scavenging( random );
      }
      

      Use the Stepover/Next line button to move the cursor to the line with RandomEvent_Scavenging( random ); Take note of the value of the random variable.

      We are now sitting at a function call. We can go into the function call using the Step into option.

      lab_u04_DebugAndTest_02stepinto.png

      This will take us into the function.

      void Game::RandomEvent_Scavenging( int event )
      {
          ScreenDrawer::Fill( '~', "black", "red" );
          ScreenDrawer::DrawWindow( "", 1, 1, 78, 13 );
          ScreenDrawer::Set( 3, 2, "DAY: " + Helper::ToString( m_day ), "red", "black" );
      
          int mod;
          if ( event == 0 )
          {
              mod = rand() % 3 + 2;
              ScreenDrawer::Set( 3, 4, "You find a stash of food.", "green", "black" );
      // ... etc ...
      

      This function takes the event variable and figures out what kind of event to make happen in the game.

      Use the Step Over / Next Line button to see which if/else if statement is executed: event = 0=? event = 1=? And so on…

      Take note of which kind of event got executed:

      • You find a stash of food.
      • A zombie surprises you!
      • You find some medical supplies.
      • Another scavenger ambushes you!
      • You don't find anything.

      Don't stop the program execution, we are going to keep working with where we've paused.


    • Call Stack - Looking at the function call history

      Another important debugging tool is the Call Stack, which tells you the order of functions called to get to where the program is currently at.

      Task: Use the Call Stack to take note of what functions were called leading up to your Game::RandomEvent_Scavenging breakpoint.

      The call stack should look like this:

      lab_u04_DebugAndTest_03callstack.png

      If the call stack pane isn't showing up, you can open them through the menus:

      IDE Menu
      Visual Studio Debug, Windows, Call Stack
      Code::Blocks Debug, Debugging windows, Call Stack

      The top-most function displayed is the one currently running, and all functions below it are functions that were active previously. If you step all the way down, you can trace all the functions that were called to get to where we're at.

      Task: Set a breakpoint in the Game::EndOfDayStats() function and use the Call Stack to take note of what functions were called leading up to it.

      Task: Set a breakpoint in the Player::Player() function (in Player.cpp) and use the Call Stack to take note of what functions were called leading up to it. You will probably have to close and restart the game to hit this breakpoint.


    • When to use debugging tools…?

      Debugging tools are extremely helpful and the better you get at utilizing them to diagnose your programs' issues, the more well prepared you will be for software development in the "real world".


  • Part 2: IDE navigation techniques

    Don’t close the zombie game yet – now we’re going to use various IDE features in this project.

    Take a screenshot once you've completed each one and paste it into your document.

    • Viewing code split-screen

      In most IDEs you can click-and-drag a code window (via its tab) to another part of the screen, allowing to have a side-by-side view of two (or more) different code files at once.

      Visual Studio:

      Click and drag the tab to the center of the screen. Then, a placement box will show up. Drag the tab to one of these 5 options for different views.

      lab_u04_DebugAndTest_04draggingA.png

      Code::Blocks:

      Click and drag the tab to an edge of the code window. A slight highlight will appear in the place where it will be placed. Let go of the mouse button to place the tab to the side.

      lab_u04_DebugAndTest_04draggingB.png

    • Go to definition

      Go to the .h file and right-click on one of the functions, then select the Go to definition (VS) or Go to implementation (CB) to go directly to the definition of that function.

      lab_u04_DebugAndTest_05definition.png

    • Go to declaration

      Right-click on a function call somewhere, and select Go to declaration to go to where the function is declared.

      lab_u04_DebugAndTest_06declarations.png

    • Find references of

      Right-click on any function and select Find references of and you will get a search result with every place where the function is used throughout the program, including function calls.

      lab_u04_DebugAndTest_07references.png

    • Indenting/unindenting chunks of code

      Highlight a chunk of code and use "tab" to indent it all forward, or "shift+tab" to un-indent it all backward.

      lab_u04_DebugAndTest_08tab.png

    • Autoformatting

      When your indentation gets messed up you can have the IDE automatically format it for you.

      lab_u04_DebugAndTest_09format.png

      IDE Steps
      Visual Studio Edit, Advanced, Format Document
      Code::Blocks Right-click in the code window, select Format use AStyle
    • Comment/uncomment code blocks

      Highlight a section of code. You can comment out a whole block at once, or uncomment out a block, in the following ways:

      lab_u04_DebugAndTest_10commentout.png

      IDE Comment-out Uncomment-out
      Visual Studio Hold CTRL, then press K and then C Hold CTRL, then press K and then U
      Code::Blocks Press CTRL+SHIFT+C Press CTRL+SHIFT+X

  • Part 3: Test

    This program contains four simple functions and four test functions:

    1. int GetArea( int width, int length );
    2. void Test_GetArea();
    3. int GetStringLength( string text );
    4. void Test_GetStringLength();
    5. int GetPerimeter( int width, int length );
    6. void Test_GetPerimeter();
    7. float Withdraw( float balance, float amount );
    8. void Test_Withdraw();

    We are going to approach this from a Test Driven Development standpoint where we will write our TESTS before implementing the functionality. This way, we ensure our tests "work" because they first fail - then, when we implement the functionaltiy, the tests passing should be how we verify our logic is correct.

    • Set 1: Stub functions

      These functions contain placeholder information being returned. They return data because some C++ compilers will complain if a non-void function doesn't have a return statement. Before you implement the function itself, we will be implementing the unit tests for each function first.

      Task: Implement void Test_GetArea() and void Test_GetStringLength() first, then implement int GetArea( int width, int length ) and int GetStringLength( string text );

      Read the following instructions for a guide.

      • GetArea

        First off we need to come up with test cases - at least two for these simple functions that just do computations. Why two? Because if you only have one test that checks "The area of a 2x5 yard is 10", then implementing the function as return 10 would cause it to pass… we need at least two tests to make sure the function has proper logic.

        Here are some test cases we can use for GetArea:

        # Inputs Expected outputs
        1 width=5, height=3 returned area is 15
        2 width=7, height=11 returned area is 77

        So… how do we implement this in code?

        We have two inputs and an expected output, and in the program itself we will have an actual output. We can create variables to represent each of these pieces of information:

        • int input_width = 5;
        • int input_height = 3;
        • int expected_result = 15;
        • int actual_result;

        To get a value for the actual_result, we just call the function itself, passing in our inputs:

        actual_result = GetArea( input_width, input_height );

        We can tell if the test passed if the actual_result matches the expected_result. Or, the test failed if they do not match.

        It would be useful to output the results to the console as well, so we can write out something like this:

        if ( actualResult != expectedResult ) { result = "[FAIL]\t"; }
        else                                  { result = "[PASS]\t"; }
        
        cout << result << testName << endl;
        cout << "* input_width:     " << input_width << endl;
        cout << "* input_height:    " << input_height << endl;
        cout << "* expectedResult:  " << expectedResult << endl;
        cout << "* actualResult:    " << actualResult << endl;
        cout << endl;
        

        If you run the program now, we should get a nice print out of the test result:

        [FAIL]  GetArea - pass in 5 and 3 and should get 15 back
        * input_width:     5
        * input_height:    3
        * expectedResult:  15
        * actualResult:    -1
        

        Implement the second test case in the code before continuing.

        Once they're both implemented, you can now implement the int GetArea( int width, int height ) function. To calculate the area of a rectangle, we use the formula:

        area = width x height

        (But write it in C++ code :)

        Implement the function and once run the tests should now pass. Here's how I've formatted my tests:

        --------------------------------------------------------------------------------
        Test_GetArea
        --------------------------------------------------------------------------------
        [PASS]  GetArea - pass in 5 and 3 and should get 15 back
        * input_width:     5
        * input_height:    3
        * expectedResult:  15
        * actualResult:    15
        
        [PASS]  GetArea - pass in 7 and 11 and should get 77 back
        * input_width:     7
        * input_height:    11
        * expectedResult:  77
        * actualResult:    77
        

      • GetStringLength

        Implement the following test cases as unit tests:

        # Inputs Expected outputs
        1 text="Hello" returned length is 5
        2 text="abc def" returned length is 7

        Once the tests are implemented and they fail when you run the program, now implement the GetStringLength function. (Hint: You can get the length of the text string with text.size().)


    • Set 2: Broken functions

      These functions have already been implemented, but with incorrect logic. You're going to write unit tests before doing any fixes to help ensure that your fixes are valid once implemented.

      Task: Implement void Test_GetPerimeter() and void Test_Withdraw() first, then fix the functions -int GetPerimeter( int width, int length ) and float Withdraw( float balance, float amount )

      Read the following instructions for a guide.

      • GetPerimeter

        For this function, come up with your own TWO test cases and implement them as unit tests. After implementing the unit tests, fix the function logic.


      • Withdraw

        For this function, come up with your own TWO test cases and implement them as unit tests. After implementing the unit tests, fix the function logic.


  • Turning in the assignment

    Once you're done with the assignment, you need to add, commit, and push your changes to the server.

    lab_u02_CPPBasicsReview_gitserver.png

    Save your changes to the server:

    1. Open git bash from within your repository folder on your harddrive.
    2. Use git status to review what files have changed since your last commit.
    3. Use git add . to add all changed files to a changeset.
    4. Use git commit -m "MESSAGE" to create a snapshot of your current changes. (Replace "MESSAGE" with something more descriptive.)
    5. Use git push -u origin u04_debugtest to push all your commits to the GitLab server, on the u04_debugtest branch.

    You can continue making changes and do additional add, commit, and push commands as well.

    Get everything ready for next assignment:

    1. Use git checkout main to change back to the main branch to prepare for the next assignment. (You can always use git checkout u04_debugtest to return to this branch and code.)
    2. Use git pull to pull latest from the server. (This might happen if the instructor adds new starter code or makes modifications.)

    Create a Merge Request / turn in on Canvas:

    1. Navigate to your GitLab repository webpage.
    2. Click on the blue "Create merge request" button.
    3. Add a descriptive Title, then click "Create merge request" at the bottom of the page.
    4. Copy the URL of this merge request.
    5. Find the assignment on Canvas. Click "Start Assignment".
    6. Paste in the Merge Request URL here. Click "Submit Assignment".

🧠 Tech Literacy - Git (U04.TEC)

  • Without Source Control…

    Let's imagine a workplace that doesn't use any source control software. Multiple people have shared files on a network drive and can edit them at any time. Two different users are working on a file at the same time, but what happens when they save?

    tec_Git_SharedFile.png

    If User A saves their file first and then User B saves their file second, the network file will only be the last saved version - User B's version. It won't automatically merge the files together, meaning User A's work gets lost. Imagine working on a lot of code and having this happen - making sure work doesn't get overwritten would be a nightmare to manage manually.


  • Types of Source Control software…

    Various source control systems have been developed over the years to help manage versioning, storing, and merging source code. There are pros and cons to each one, but the most common ones these days are probably Git and TFS (Team Foundation Server). Others you might hear about are SVN (Subversion) and Mercurial. We will be specifically talking about Git here.


  • Basics of Git

    tec_Git_SourceControl.png

    There are two main parts of source control: The Server, where the code is all brought together and stored, and the Clients - or, the user PCs. A person, company, or other organization can set up a Server on their own computers or they can utilize a service like GitLab, GitHub, SourceForge, Bitbucket, or other services to host their code for them.

    Either way, for a project, a repository is created on the server. This is basically a folder, except the Git program is told that it is a special Git folder, and the Git software knows how to process different commands to help users add, update, and manage the code stored within.


    • The clone command

      tec_Git_Clone.png

      When a new developer joins the team, or a new project is created to work on, the repository will be created and then each teammate on that project can use the clone command, which looks like this:

      git clone URL.git
      

      This command tells the Git program to access the git URL given and download the repository folder. Git handles the download and setting up the folder and files within, and on the user's local computer they'll see a folder with the repository name. The clone command only needs to be done once per computer.


    • Branches

      tec_Git_Branch.png

      In our project repository, by default there will be a main branch (Note: sometimes the default is a master branch). The main branch should represent the current, stable version of the program. The goal is to make sure the code on main can build.

      When somebody wants to add a new feature or is working on a bug, they will create a new branch to work on. This starts at the main branch, but gives the user their own workspace. They can make many backups of their code on their own branch without affecting the main branch, and without breaking code on other peoples' machines.

      The checkout command is used with the -b flag to create a new branch:

      git checkout -b BRANCHNAME
      

      Even if you're on a different branch, you can switch back to main, or other branches, any time with git checkout BRANCHNAME. On your local computer you may see files disappear and changes be undone, but those changes will come back once you're on the branch where you created those files/changes.


    • The status command

      While working on a branch you can use the git status command to see a list of files that you've added, removed, and changed since the last commit. Items marked in red haven't yet been marked as "to back up", and items marked in green are ready to be backed up by using the commit command. To mark a file as "ready to backup", we use the add command.

      git status
      

    • The add command

      tec_Git_Add.png

      When you're ready to make a backup of your work, you can use a git add command to add your files. Sometimes you only want to add one or two files, sometimes you want to add only a certain type of files, and sometimes you want to add everything that has been updated.

      Add one specific file:

      git add FILENAME
      

      Add all files ending with ".cpp":

      git add *.cpp
      

      Add all changed files:

      git add .
      

    • The commit command

      tec_Git_Commit.png

      After using the add command, we "bundle" all the changes together in one commit. We use the commit command, and a message is required - this is to help us keep track of what we changed where. Try to add a descriptive message so you can track your changes later if you need to.

      Commits are initially only stored on your own computer, they won't be shown on the repository server until you push everything.

      git commit -m "Fixed bug"
      

    • The push command

      tec_Git_Push.png

      After you have one or more commits ready to go, you can use the push command to send your changes to the server under your branch. This will allow others to view the changes you've made, while still keeping your code separate from the main branch… for now. The overall goal is to eventually get it merged into the main branch, but we aren't doing that quite yet.

      git push -u origin BRANCHNAME
      

    • The pull command

      tec_Git_Pull.png

      Besides just pushing your own changes to the server, you will periodically want to use the pull command to get any changes from the server - such as completed features from other developers, or merged in code.

      The pull will grab the latest version of files, so you want to make sure to first add and commit your changes before using pull.

      git checkout main
      git pull
      

    • Merge requests

      gitlab_web_mergerequest3.png

      The Merge Request is the final part of a work-in-progress feature's life. You create a merge request, and it will show a list of all changes you've made under the changes tab. Usually, you would send this Merge Request to other developers to review your work, but for this class you'll submit your Merge Request as your assignment turn-ins.

      tec_Git_MergeRequest.png

      Other developers can leave comments on specific lines of code in the merge request. Those reviewing your merge request should look out for obvious potential errors, typos, logical fallacies, and so on. If comments are left asking you to fix something, you can go back to your computer, make changes, then add/commit/push again and ask others to re-review your updates.

      Finally, once it has been approved, the code can be merged to the main branch. In this class, the instructor will merge the code. In the work world, someone like the system architect or your team lead might handle this step.


      There's no assignment to turn in for this item.

UNIT 05: Algorithm efficiency

πŸ“–οΈ Reading - Algorithm efficiency (U05.READ)

  • Introduction: Algorithm Efficiency (and why we care)

    Processing power continues increasing as time moves forward. Many things process so quickly that we barely notice it, if at all. That doesn't mean we can just write code as inefficiently as possible and just let it run because "computers are fast enough, right?" - as technology evolves, we crunch more and more data, and as "big data" becomes a bigger field, we need to work with this data efficiently - because it doesn't matter how powerful a machine is, processing large quantities of data with an inefficient algorithm can still take a long time.

    And some computers today are slow. Not usually the computers that the average person is using (Even though it might feel that way if they're running Windows). Some computers that handle systems are pretty basic, or they're small, and don't have the luxury of having great specs. Fitness trackers, dedicated GPS devices, a thermostat, etc. For systems like these, processing efficiency is important.

    Let's say our algorithm's time to execute increases quadratically. That means, as we increase the amount of items to process (\(n\)), the time to process that data goes up quadratically.

    reading_u05_AlgorithmEfficiency_quadratic.png

    For processing 1 piece of data, it takes 1 unit of time (we aren't focused so much on the actual unit of time, since each machine runs at different speeds) - that's fine. Processing 2 pieces of data? 4 units of time. 8 pieces of data? 64 units of time. We don't just double the amount of time it takes when we double the data - we square it.

    How much data do you think is generated every day on a social media website that has millions of users? Now imagine the processing time for an algorithm with a quadratic increase…

    Data \(n\) Time units
    1 1
    2 4
    3 9
    4 16
    5 25
    100 10,000
    1,000 1,000,000
    10,000 100,000,000
    1,000,000 1,000,000,000,000

    From a design standpoint we also need to know what the efficiency of different algorithms are (such as the algorithm to find data in a Linked List or the algorithm to resize a dynamic array) in order to make design decisions on what is the best option for our software.


  • Example: Fibonacci sequence, iterative and recursive

    As an example, you shouldn't use a recursive algorithm to generate a Fibonacci sequence (Each number \(F_n\) in the sequence is equal to the sum of the two previous items: \(F_n = F_{n-1} + F_{n-2}\)). It's just not as efficient! Let's look at generating the string of numbers:

    1, 1, 2, 3, 5, 8, 13, 21, 34, 55

    The most efficient way to do this would be with an iterative approach using a loop. However, we could also figure it out with a recursive approach, though the recursive approach is much less efficient.

    Fibonacci sequence, iterative:

    int GetFib_Iterative(int n)
    {
      if (n == 0 || n == 1) { return 1; }
    
      int n_prev = 1;
      int n_prevprev = 1;
      int result = 0;
    
      for (int i = 2; i <= n; ++i)
        {
          result = n_prev + n_prevprev;
          n_prevprev = n_prev;
          n_prev = result;
        }
    
      return result;
    }
    

    This algorithm has a simple loop that iterates \(n\) times to find a given number of the Fibonacci sequence. The amount of time the loop iterates based on \(n\) is:

    n 3 4 5 6 7 8 9 10 20
    iterations 2 3 4 5 6 7 8 9 19

    Fibonacci sequence, recursive:

    int GetFib_Recursive(int n)
    {
      if (n == 0 || n == 1) { return 1; }
    
      return GetFib_Recursive(n - 1) + GetFib_Recursive(n - 2);
    }
    

    The way this version is written, any time we call this function with any value \(n\), it has to compute the Fibonacci number for \(Fib(n-1)\), \(Fib(n-2)\), \(Fib(n-3)\), … \(Fib(1)\), \(Fib(0)\) … twice. This produces duplicate work b ecause it effectively doesn't "remember" what \(Fib(3)\) was after it computes it, so for \(Fib(n)\) it has to recompute \(Fib(n-1)\) and \(Fib(n-2)\) each time…

    n 3 4 5 6 7 8 9 10 20
    iterations 2 4 7 12 20 33 54 88 10,945

    The growth rate of the iterative version ends up being linear: As \(n\) rises linearly, the amount of iterations also goes up linearly.

    The growth rate of the recursive function is exponential: As \(n\) rises linearly, the amount of iterations goes up exponentially.

    reading_u05_AlgorithmEfficiency_fib.png

    • Red/solid: Recursive growth rate
    • Blue/dashed: Iterative growth rate
    • Y scale from 0 to 25 (the 0th, to 25th Fibonacci number to generate).

    For small amounts this might not be too bad. However, if we were generating a Fibonacci number further in the list, it would continue getting even slower…

    reading_u05_AlgorithmEfficiency_fib2.png

    • Red/solid: Recursive growth rate
    • Blue/dashed: Iterative growth rate
    • Y scale from 0 to 11,000 (the 11,000th Fibonacci number to generate).

    If we are trying to generate the 11,000th item in the sequence, the iterative approach requires 11,000 iterations in a loop. That linear increase is still much faster than each \(Fib(n)\) calling \(Fib(n-1)\) and \(Fib(n-2)\) recursively.


  • Big-O Notation and Growth Rates

    For the most part, we don't care much about the exact amount of times a loop runs. After all, while the program is running, \(n\) could be changing depending on user input, or how much data is stored, or other scenarios. Instead, we are more interested in looking at the big picture: the generalized growth rate of the algorithm.

    We use something called "Big-O notation" to indicate the growth rate of an algorithm. Some of the rates we care about are:

    \(O(1)\) \(O(log(n))\) \(O(n)\) \(O(n^2)\) \(O(n^3)\) \(O(2^n)\)
    Constant Logarithmic Linear Quadratic Cubic Exponential
    Constant time, \(O(1)\)

    We think of any single command as being constant time. The operation \[ a = b + c \] will take the same amount of computing time no matter what the values of \(a\), \(b\), and \(c\) are.

    int F(int x)
    {
      return 3 * x + 2;
    }
    

    reading_u05_AlgorithmEfficiency_constant.png

    Logarithmic time, \(O(log(n))\)

    Having an algorithm that halves its range each iteration of the loop will result in a logarithmic growth rate.

    int Search(int l, int r,
               int search, int arr[])
    {
      while ( l <= r )
        {
          int m = l + (r-l) / 2;
          if ( arr[m] == search )
            { return m; }
          else if ( arr[m] < search )
            { l = m+1; }
          else if ( arr[m] > search )
            { r = m-1; }
        }
      return -1;
    }
    

    reading_u05_AlgorithmEfficiency_log.png

    Linear time, \(O(n)\)

    Having a single loop that iterates over a range will be a linear time increase.

    int Sum( int n )
    {
      int sum = 0;
      for (int i=1; i<=n; i++)
        {
          sum += n;
        }
      return sum;
    }
    

    reading_u05_AlgorithmEfficiency_linear.png

    If there is some scenario that causes the loop to halve its range each time, then its growth rate would be less than linear: as \(O(log(n))\).

    Quadratic time, \(O(n^2)\)

    Quadratic time comes into play when you have one loop iterating \(n\) times nested within an other loop that also iterates \(n\) times. For example, if we were writing out times tables from 1 to 10 (\(n = 10\)), then we would need 10 rows and 10 columns, giving us \(10^2 = 100\) cells.

    void TimesTables(int n)
    {
      for (int y=1; y<=n; y++)
        {
          for (int x=1; x<=n; x++)
            {
              cout << x << "*"
                   << y << "="
                   << x*y << "\t";
            }
          cout << endl;
        }
    }
    

    reading_u05_AlgorithmEfficiency_quadraticB.png

    Cubic time, \(O(n^3)\)

    Just like nesting two loops iterating \(n\) times each gives us a \(n^2\) result, having three nested loops iterating \(n\) times each gives us a \(O(n^3)\) growth rate.

    void CubicThing(int n)
    {
      for (int z=1; z<=n; z++)
        {
          for (int y=1; y<=n; y++)
            {
              for (int x=1; x<=n; x++)
                {
                  cout << x << " "
                       << y << " "
                       << z << endl;
                }
            }
        }
    }
    

    reading_u05_AlgorithmEfficiency_cubic.png

    Exponential time, \(O(2^n)\)

    With an exponential function, each step increases the complexity of the operation. The Fibonacci example is a good illustration: Figuring out Fib(0) and Fib(1) are constant, but Fib(2) requires calling Fib(0) and Fib(1), and Fib(3) calls Fib(1) and Fib(2), with Fib(2) calling Fib(0) and Fib(1), and each iteration adds that many more operations.

    int Fib(int n)
    {
      if (n == 0 || n == 1)
        return 1;
    
      return
        Exponential_Fib(n-2)
        + Exponential_Fib(n-1);
    }
    

    reading_u05_AlgorithmEfficiency_exponential.png

    Growth rate comparisons:

    reading_u05_AlgorithmEfficiency_comparisons.png


  • Review questions:
    1. Let's say an algorithm's time complexity is constant. For this example algorithm we can model the efficiency like this: \[ T(n) = 5 \] where \(n\) is the amount of items to process and \(T\) is the time it takes.
      1. Processing 1 item would cost how many time units?
      2. Processing 10 items would cost how many time units?
      3. Processing 100 items would cost how many time units?
    2. Let's say an algorithm's time complexity is linear. For this example algorithm we can model the efficiency like this: \[ T(n) = 2n \]
      1. Processing 1 item would cost how many time units?
      2. Processing 10 items would cost how many time units?
      3. Processing 100 items would cost how many time units?
    3. Let's say an algorithm's time complexity is quadratic. For this example algorithm we can model the efficiency like this: \[ T(n) = n^2 \]
      1. Processing 1 item would cost how many time units?
      2. Processing 10 items would cost how many time units?
      3. Processing 100 items would cost how many time units?
    4. Given this function:

      int Func( int n )
      {
        return 3 * n; // +1 command
      }
      

      How many commands are executed when…

      1. \(n = 1\)?
      2. \(n = 10\)?
      3. \(n = 100\)?
    5. Given this function:

      int Func( int n )
      {
        n += 1; // +1 command
      
        if ( n > 100 )
          return n+100; // +1 command, or
        else if ( n > 10 )
          return n+10; // +1 command, or
        else
          return n; // +1 command
      }
      

      How many commands are executed when…

      1. \(n = 1\)?
      2. \(n = 10\)?
      3. \(n = 100\)?
    6. Given this function:

      void Func( int n )
      {
        for ( int i = 0; i < n; i++ )
        {
          cout << i << " "; // +1 command each iteration
        }
      }
      

      How many commands are executed when…

      1. \(n = 1\)?
      2. \(n = 10\)?
      3. \(n = 100\)?
      4. The increase is… (constant/linear/quadratic?)
    7. Given this function:

      void Func( int n )
      {
        int iterations = 0; // Don't count me
        for ( int i = 0; i < n; i++ )
        {
          for ( int j = 0; j < n; j++ )
          {
            cout << i << "," << j << endl; // +1 command each iteration
            iterations++; // Don't count me
          }
        }
        cout << "Iterations: " << iterations << endl; // Don't count me
      }
      

      How many commands are executed when…

      1. \(n = 1\)?
      2. \(n = 10\)?
      3. \(n = 100\)?
      4. The increase is… (constant/linear/quadratic?)

πŸ”Ž Concept Intro - Algorithm efficiency (U05.CIN)

🧠 Tech Literacy - Algorithm efficiency (U05.TEC)

Watch from 26:46 - 32:19 from the "The Secret Rules of Modern Living Algorithms" - Documentary: https://youtu.be/kiFfp-HAu64?si=0274JcEj7vIogCtk&t=1606

Locate the 🧠 Unit 05 Tech Literacy - Algorithm efficiency (U05.TEC.202401CS235) or 🧠 Unit 05 Tech Literacy - Algorithm efficiency (U05.TEC.202401CS250) assignment and add to the discussion!

\newpage

WEEK 3 - JAN 29

UNIT 06: CS 200 Review - Pointers

πŸ“–οΈ Reading - Pointers and memory (U06.READ)

reading_u06_Pointers_pointer.png

  • Memory addresses

    When we declare a variable, what we're actually doing is telling the computer to set aside some memory (in RAM) to hold some information. Depending on what data type we declare, a different amount of memory will need to be reserved for that variable.

    Data type Size
    boolean 1 byte
    character 1 byte
    integer 4 bytes
    float 4 bytes
    double 8 bytes

    A bit is the smallest unit, storing just 0 or 1. A byte is a set of 8 bits. With a byte, we can store numbers from 0 to 255, for an unsigned number (only 0 and positive numbers, no negatives).

    Minimum value, 0

    (Decimal value = \(128 \cdot 0 + 64 \cdot 0 + 32 \cdot 0 + 8 \cdot 0 + 4 \cdot 0 + 2 \cdot 0 + 1 \cdot 0\))

    place 128 64 32 16 8 4 2 1
    value 0 0 0 0 0 0 0 0

    Maximum value, 255

    (Decimal value = \(128 \cdot 1 + 64 \cdot 1 + 32 \cdot 1 + 8 \cdot 1 + 4 \cdot 1 + 2 \cdot 1 + 1 \cdot 1\))

    place 128 64 32 16 8 4 2 1
    value 1 1 1 1 1 1 1 1

    A char only needs one byte to store a single letter because we represent letters with the ASCII or UTF8 codes 65 - 90 for upper-case, and 97 - 122 for lower-case.

    A B C D E F G H I J K L M
    65 66 67 68 69 70 71 72 73 74 75 76 77
    N O P Q R S T U V W X Y Z
    78 79 80 81 82 83 84 85 86 87 88 89 90
    a b c d e f g h i j k l m
    97 98 99 100 101 102 103 104 105 106 107 108 109
    n o p q r s t u v w x y z
    110 111 112 113 114 115 116 117 118 119 120 121 122

    Any of these numbers can be stored with 8 bits:

    A = 65…

    (Decimal value = \(128 \cdot 0 + 64 \cdot 1 + 32 \cdot 0 + 8 \cdot 0 + 4 \cdot 0 + 2 \cdot 0 + 1 \cdot 1\))

    place 128 64 32 16 8 4 2 1
    value 0 1 0 0 0 0 0 1

    z = 122…

    (Decimal value = \(128 \cdot 0 + 64 \cdot 1 + 32 \cdot 1 + 8 \cdot 1 + 4 \cdot 0 + 2 \cdot 1 + 1 \cdot 0\))

    place 128 64 32 16 8 4 2 1
    value 0 1 1 1 1 0 1 0

    So when we declare a variable, the computer finds an available space in memory and reserves the appropriate amount of bytes in memory. For example, our char could be assigned the memory address 0xabc008 in memory, and its value being stored would look like this:

    …007 …008 …009 …00a …00b …00c …00d …00e …00f …010
    0 1 1 1 1 0 1 0

    (…007 and …010 are spaces in memory taken by something else)

    We can view the addresses of variables in our program by using the address-of operator &. Note that we have used the ampersand symbol before to declare pass-by-reference parameters, but this is a different symbol used in a different context.

    #include <iostream>
    using namespace std;
    
    int main()
    {
      int number1 = 10;
      int number2 = 20;
    
      cout << &number1 << "\t"
           << &number2 << endl;
    
      return 0;
    }
    

    Running the program, it would display the memory addresses for these two variables. Notice that they're 4 bytes apart in memory (one is at …70 and one is at …74):

    0x7ffd3a24cc70	0x7ffd3a24cc74
    

    Each time we run the program, we will get different memory addresses, since the operating system reclaims that memory when the program ends, and gives us new memory spaces to allocate next time we run the program.

    0x7ffe0e708a80	0x7ffe0e708a84
    

    When we declare an array of integers of size \(n\), the program asks for \(n \times 4\) bytes of memory to work with. The two variables above didn't have to be side-by-side in memory; they just happened to be because they were declared close together. With an array, however, all elements of the array will be contiguous (side-by-side) in memory.

    Here we have an array of integers:

    int arr[3];
    
    for ( int i = 0; i < 3; i++ )
      {
        cout << &arr[i] << "\t";
      }
    

    And the output:

    0x7ffd09a130c0	0x7ffd09a130c4	0x7ffd09a130c8
    

    Because the elements of an array must be contiguous in memory, we cannot resize a normal array. After our array is declared, chances are the memory addresses right after it will be put to use elseware on the computer and will be unavailable to our program and our array.

    But, after we learn about pointers, we will learn how we can dynamically allocate as much memory as we need at any time during our program's execution - giving us the ability to "resize" arrays by allocating space for a new array, copying the data over to the larger chunk of memory, and deallocating the old chunk of memory from the smaller array.


  • Pointers
    • Creating pointer variables

      We can declare special variables that store memory addresses rather than storing an int or float or char value. These variables are called pointers.

      We can declare a pointer like this: int* ptrNumber; Or like this: int * ptrNumber; Or like this: int *ptrNumber;

      But note that doing this declares one pointer and several integers: int * ptrNumber, notAPointer1, notAPointer2; To avoid confusion, \underline{declare multiple pointers on separate lines}.

      If we declare a pointer as an int* type, then it will only be able to point to the addresses of integer variables, and likewise for any other data type.

      Context: Safety with pointers!

      Remember how variables in C++ store garbage in them initially? The same is true with pointers - it will store a garbage memory address. This can cause problems if we try to work with a pointer while it's storing garbage.

      To play it safe, any pointer that is not currently in use should be initialized to NULL or nullptr.

      Declaring a pointer and initializing it to nullptr:

      #include <iostream>
      using namespace std;
      
      int main()
      {
        int * ptr = nullptr;
      
        return 0;
      }
      
    • Assigning pointers to addresses

      Once we have a pointer, we can point it to the address of any variable with a matching data type. To do this, we have to use the address-of operator to access the variable's address - this is what gets stored as the pointer's value.

      • Assigning an address during declaration: int * ptr = &somevariable;
      • Assigning an address after declaration: ptr = \&somevariable;

      After assigning an address to a pointer, if we cout the pointer it will display the memory address of the pointed-to variable - just like if we had used cout to display the address-of that variable.

      // Shows the same address
      cout << ptr << endl;
      cout << &somevariable;
      

      Here's a simple program that has an integer var with a value of 10, and a pointer ptr that points to var's address.

      #include <iostream>
      using namespace std;
      
      int main()
      {
        int * ptr;
        int var = 10;
      
        ptr = &var;
      
        cout << "var address: " << &var << endl;
        cout << "ptr address: " << &ptr << endl;
        cout << "var value:   " << var << endl;
        cout << "ptr value:   " << ptr << endl;
      
        return 0;
      }
      

      The output would look like:

      var address: 0x7ffc3d6028b0
      ptr address: 0x7ffc3d6028b4
      var value:   10
      ptr value:   0x7ffc3d6028b0
      

      Some things to note:

      • var stores its data at its address 0x7ffc3d6028b0.
      • ptr stores its data at its address 0x7ffc3d6028b4.
      • var's value is 10, since it's an integer.
      • ptr's value is the address of var, since it's a pointer-to-an-integer.

      c2_u08_ptrmemory.png

    • Dereferencing pointers to get values

      Once the pointer is pointing to the address of a variable, we can access that pointed-to variable's value by dereferencing our pointer. This gives us the ability to read the value stored at that memory address, or overwrite the value stored at that memory address.

      We dereference the pointer by prefixing the pointer's name with a * - again, another symbol being reused but in a different context.

      In this code, we point ptr to the address of var. Outputting ptr will give us var's address, and outputting *ptr (ptr dereferenced) will give us the value stored at var's address.

      int * ptr;
      int var = 10;
      
      ptr = &var;
      
      cout << "ptr value:        " << ptr << endl;
      cout << "ptr dereferenced: " << *ptr << endl;
      

      Output:

      ptr value:        0x7ffd21de775c
      ptr dereferenced: 10
      

      Then, we could also overwrite the value stored at var's address by again dereferencing the pointer and writing an assignment statement:

      *ptr = 20;
      

      When we output the value that var is storing, either directly through var or through the ptr, we can see that the value of var has been overwritten:

      cout << "var value:        " << var << endl;
      cout << "*ptr value:       " << *ptr << endl;
      
      var value:        20
      *ptr value:       20
      

  • Pointer cheat sheet
    Declare a pointer int* ptrInt;
      float *ptrFloat;
    Assign pointer to address ptrInt = &intVar;
      ptrFloat = &floatVar;
    Dereference a pointer cout << *ptrChar;
      *ptrInt = 100;
    Assign to nullptr float *ptrFloat = nullptr;
      ptrChar = nullptr;

  • Invalid memory access with pointers

    Remember that when you declare a variable in C++, it will initially store garbage. This is true of pointers as well. When you declare a pointer without initializing it to nullptr, it will be storing random garbage that it will try to interpret as a memory address. If you dereference a pointer that is pointing to garbage, your program is going to run into a problem - a segmentation fault.

    A pointer pointing to garbage:

    int main()
    {
      int * bob;
      cout << *bob << endl;
    
      return 0;
    }
    

    Program output:

    Segmentation fault (core dumped)
    
    How do we check whether memory address is valid?

    In short, we can't check if an address stored in a pointer is valid. This is why we initialize our pointers to nullptr when they're not in use - we know nullptr means that the pointer is not pointing to anything.


  • Dynamically allocating memory

    One handy use of pointers is to point at the addresses of other variables that were already declared. However, there is a second main use of pointers: To dynamically allocate memory any time we need new variables that we don't have to pre-write in the program.


    • Dynamic variables

      At the moment, dynamic variables might seem pointless, but they are really handy for linked data structures - a type of structure you can use to store a series of data in that is an alternative to an array.

      • new and delete

        We use the new keyword to allocate memory for the size of one variable data type (as opposed to an array of the variables), and delete to free up that memory.

        Once we've allocated memory through the pointer, we can use the pointer as normal. The only difference is that we're now accessing an address that was allocated differently from the address of a "normal" variable.

        • Allocating memory for a single item: int * ptr = new int;
        • Deallocating memory for a single item: delete ptr;

        Small program using a dynamic variable:

        int main()
        {
          int * myNumber;             // Declare pointer
        
          myNumber = new int;         // Allocate memory
        
          *myNumber = 10;             // Assign value
        
          cout << *myNumber << endl;  // Print value
        
          delete myNumber;            // Free memory
        
          return 0;
        }
        

    • Dynamic arrays

      For our normal arrays, we are unable to resize them during the program's run time because an array requires that all elements of the array are contiguous (side-by-side) in memory.

      However, if we can allocate and deallocate memory any time we want with pointers, we can create a dynamic array. If we ever need to resize it, we allocate a new array of a bigger size and copy the data from the old array to the new one.

      • new[ ] and delete[ ]

        When we're allocating space for an array of items, we use a slightly different set of keywords:

        • Allocate memory for an array: int * arr = new int[100];
        • Deallocate memory for an array: delete [] arr;

        Example program that lets the user specify how big the array will be.

        int main()
        {
          int arraySize;
          float * pricesArray;
        
          cout << "How many items? ";
          cin >> arraySize;
        
          pricesArray = new float[arraySize]; // Allocate mem
        
          for ( int i = 0; i < arraySize; i++ )
            {
              cout << "Enter price for item " << i << ": ";
              cin >> pricesArray[i];
            }
        
          delete [] pricesArray;      // Deallocate memory
        
          return 0;
        }
        

        When you've used a pointer to allocate space for an array, you will access items in the dynamic array the same way you would do for a normal array. In other words, you would use the subscript operator [ ] to access elements at particular indices, and you don't need to prefix the pointer with the dereference operator *.


      • "Resizing" a dynamic array

        Let's say we're writing a program that stores the user's library of movies they own. It saves a file so they can run the program any time, add or delete movies, close the program and open it again sometime later.

        We can't realistically predict how many movies a person would own - some people might be collectors and have tons of movies. At the same time, if we were to over-estimate and make an array with, say, 10,000 elements, it would be a huge waste of memory if we had such a big array for someone who only had a few dozen movies.

        A dynamic array would work perfectly here because we can initially create an array that's fairly small - maybe 10 items - and if the array fills up, we can then allocate more memory and copy the movies to the new array and deallocating the old array. If it fills up again, we just repeat the process, allocating more memory, copying the data over, and freeing the old array's memory.

        Resizing steps

        The resizing process works like this:

        1. Create a pointer and use it to allocate a new, bigger array.
        2. Use a for loop to copy all the elements from the old-small-array to the new-big-array.
        3. Free the memory for the old-small-array.
        4. Update the array's pointer, which was pointing to old-small-array, to now point to the address of the new-big-array.

        Normally, you would store a dynamic array inside of a class that acts as the interface for adding and removing data, where it will check if there's enough space before adding new information and resizing the array if not.

        Here's an example of resizing a movie array within a MovieLibrary class:

        void MovieLibrary::Resize()
        {
          // Allocate more memory
          int newSize = m_arraySize + 10;
          string * newArray = new string[newSize];
        
          // Copy data from old array to new array
          for ( int i = 0; i < m_arraySize; i++ )
            {
              newArray[i] = m_movieArray[i];
            }
        
          // Free memory of old array
          delete [] m_movieArray;
        
          // Update the pointer to point at new array
          m_movieArray = newArray;
        
          // Update the array size
          m_arraySize = newSize;
        }
        

        Dynamic array example: Movie Library

        Here's a small example of a class that contains a dynamic array, that allows users to add additional information, and handles checking for a full array and resizing.

        MovieLibrary.h:
        #ifndef _MOVIE_LIBRARY_HPP
        #define _MOVIE_LIBRARY_HPP
        
        #include <string>
        using namespace std;
        
        class MovieLibrary
        {
        public:
          MovieLibrary();
          ~MovieLibrary();
        
          void ViewAllMovies() const;
          void ClearAllMovies();
          void AddMovie( string newTitle );
        
        private:
          bool IsFull();
          void Resize();
        
          string * m_movieArray;
          int m_arraySize;
          int m_itemsStored;
        };
        
        #endif
        

        The methods IsFull() and Resize() are set as private because outside users don't need to know anything about how the data is stored. The outside users shouldn't be able to recall Resize whenever they want.

        MovieLibrary.cpp:

        Constructor:

        MovieLibrary::MovieLibrary()
        {
          m_arraySize = 10;
          m_movieArray = new string[m_arraySize];
          m_itemsStored = 0;
        }
        

        In the constructor, we initialize our m_movieArray member pointer variable by allocating some memory to start with.

        If we weren't going to allocate memory in the constructor, then we should initialize m_movieArray by setting it to nullptr.

        We also have two separate size variables - m_arraySize keeps track of how many spaces are available in the array, and m_itemsStored keeps track of how many items the user has added to the array.


        Destructor:

        MovieLibrary::~MovieLibrary()
        {
          if ( m_movieArray != nullptr )
            {
              delete [] m_movieArray;
            }
        }
        

        Before the MovieLibrary item is destroyed (e.g., when the program ends and it goes out of scope), we need to make sure to *free the memory that we allocated}. First, we need to check if the pointer m_movieArray is pointing to nullptr - if it is, we can assume it's not in use and there's nothing to do. But, if m_movieArray is pointing to some address, we assume that memory has been allocated here. In this case, we free that memory.


        ViewAllMovies:

        void MovieLibrary::ViewAllMovies() const
        {
          for ( int i = 0; i < m_itemsStored; i++ )
            {
              cout << i << ". " << m_movieArray[i] << endl;
            }
        }
        

        This is just a simple for loop that goes from index 0 to m_itemsStored - 1, displaying each element to the screen.

        We aren't iterating until i < m_arraySize because if our array size is 10 and the user has only stored 5 movies, we don't need to display 5 empty slots.


        ClearAllMovies:

        void MovieLibrary::ClearAllMovies()
        {
          delete [] m_movieArray;
          m_movieArray = nullptr;
          m_arraySize = 0;
          m_itemsStored = 0;
        }
        

        If the user decides to clear out their movie array, we could just deallocate all memory reserved for the movie list and reset our size variables. We just need to make sure we're checking to make sure the array is allocated before we try to access elements from it or add to it.


        IsFull:

        bool MovieLibrary::IsFull()
        {
          return m_itemsStored == m_arraySize;
        }
        

        The array is full if the amount of items the user has stored, m_itemsStored is equal to the amount of spaces we have allocated for the array, m_arraySize.


        AddMovie:

        void MovieLibrary::AddMovie( string newTitle )
        {
          if ( m_movieArray == nullptr )
            {
              m_arraySize = 10;
              m_movieArray = new string[m_arraySize];
            }
          if ( IsFull() )
            {
              Resize();
            }
        
          m_movieArray[ m_itemsStored ] = newTitle;
          m_itemsStored++;
        }
        

        Before we add a movie, we need to make sure m_movieArray is set up and ready.

        First, we check to see if it is pointing to nullptr. If it is, we allocate memory.

        Next, we check to see if the array is full. If it is, we call Resize().

        Finally, once those two checks have been done and the array has been prepared, we can add a new item to the array and increase hte m_itemsStored count.


        Resize:

        void MovieLibrary::Resize()
        {
          int newSize = m_arraySize + 10;
          string * newArray = new string[newSize];
        
          for ( int i = 0; i < m_arraySize; i++ )
            {
              newArray[i] = m_movieArray[i];
            }
        
          delete [] m_movieArray;
        
          m_movieArray = newArray;
          m_arraySize = newSize;
        }
        

        This is the same Resize() method as was shown before as an example of resizing a dynamic array.


  • Memory Management
    • Types of memory errors

      Working with pointers and dealing with memory can lead to writing code that can mess up our program or even slow down our computer (until the next reboot).

      Invalid memory access:

      Invalid memory access happens when a pointer isn't set to nullptr when it's no longer in use. If this doesn't happen, the memory address it's pointing to could be invalid, such as if delete was used on the pointer, or if the pointer were declared and not initialized.

      An invalid memory access would cause a problem once you try to dereference the pointer pointing to an invalid address - causing a segfault and your program to crash.

      Memory leak:

      A memory leak occurs when you've allocated memory (via the new command) but you never deallocate it. C++ won't automatically deallocate this memory for you, and what happens is that chunk of memory ends up being "called for" even after the program has finished running. This memory block will be unavailable to all programs until the user restarts their computer.

      If you had many items allocating memory but never freeing them, the resulting memory leaks could cause your computer to slow down over time (until the next reboot).

      Missing allocation:

      Missing allocation occurs if you try to delete memory that has already previously been freed. If this happens, the progrma will crash.


    • The Stack and the Heap

      What is the difference between a normal variable…

      int myNum;

      … and a dynamically allocated variable?…

      int * myNum = new int;

      Well, there are different types of memory, and each is stored in a different space.

      The Stack:

      All of our non-dynamic variables we have been declaring, including function parameters and class member variables, get allocated to the Stack. The Stack part of memory has a fixed size, and it contains a sequence of memory addresses. The Stack automatically handles the memory management for these variables - when we declare a int count; variable, it will be pushed onto the Stack, and when it's not in use anymore (when it goes out of scope), it will be removed from (popped off) the Stack.

      A "stack overflow" error occurs when so many variables have been declared that the Stack runs out of room. This usually occurs while writing recursive functions that have a logic error, causing them to repeat until we run out of Stack space.

      The Heap:

      The heap is where dynamically allocated data gets stored. The Heap does not automatically take care of memory management for us, so that is why we have to deal with new and delete ourselves (unless we're using a smart pointer). Any data in the Heap must be accessed via a pointer. There is no size restriction for the Heap.


  • Smart Pointers

    In modern versions of C++ we have access to smart pointers, which allow us to utilize pointer functionality but without having to worry about the memory management side of things. It's important to see both the original style of pointers and the smart pointers so that you're acquainted with both in case you experience them in other classes or on the job.


    • shared_ptr

      reading_u06_Pointers_SharedPointers.png

      Documentation: https://cplusplus.com/reference/memory/shared_ptr/

      Shared pointers are meant for spaces in memory that will be pointed to by multiple pointers. The memory allocated at that address stays active up until the last pointer is destroyed - that triggers the deallocation of the memory once nobody is pointing to it anymore.

      To illustrate this, let's create a City struct:

      struct City
      {
        City( string value )
        {
          name = value;
        }
      
        string name;
        shared_ptr<City> west_neighbor;
        shared_ptr<City> east_neighbor;
        shared_ptr<City> south_neighbor;
        shared_ptr<City> north_neighbor;
      };
      

      Each City can have a north, south, east, and/or west neighbor City. Instead of using a City* traditional pointer, we're using the shared_ptr object that's part of the <memory> C++ library.

      Within our main program, we can create some cities. Instead of our dynamic variable allocation like City lenexa = new City( "Lenexa" );, it will look like this:

      shared_ptr<City> overland_park  = shared_ptr<City>( new City( "Overland Park" ) );
      shared_ptr<City> lenexa         = shared_ptr<City>( new City( "Lenexa" ) );
      shared_ptr<City> olathe         = shared_ptr<City>( new City( "Olathe" ) );
      shared_ptr<City> shawnee        = shared_ptr<City>( new City( "Shawnee" ) );
      

      For each of these shared pointers, this is the first reference to its memory address. When we set up neighbors for each city, we create additional references:

      // Set up neighbors
      lenexa->east_neighbor         = overland_park;
      overland_park->west_neighbor  = lenexa;
      lenexa->south_neighbor        = olathe;
      olathe->north_neighbor        = lenexa;
      lenexa->north_neighbor        = shawnee;
      shawnee->north_neighbor       = lenexa;
      

      If these were traditional pointers, the syntax would look the same.

      The shared_ptr class gives us access to a function called use_count(), which we can use to see how many shared pointers are currently pointing to a given memory address. (https://cplusplus.com/reference/memory/shared_ptr/) Using this, we can build out a table that displays each city, its address, and how many references are going to that address:

      cout << setw( 20 ) << "CITY"              << setw( 20 ) << "ADDRESS"      << setw( 20 ) << "USE COUNT"               << endl << string( 80, '-' ) << endl;
      cout << setw( 20 ) << overland_park->name << setw( 20 ) << overland_park  << setw( 20 ) << overland_park.use_count() << endl;
      cout << setw( 20 ) << lenexa->name        << setw( 20 ) << lenexa         << setw( 20 ) << lenexa.use_count()        << endl;
      cout << setw( 20 ) << olathe->name        << setw( 20 ) << olathe         << setw( 20 ) << olathe.use_count()        << endl;
      cout << setw( 20 ) << shawnee->name       << setw( 20 ) << shawnee        << setw( 20 ) << shawnee.use_count()       << endl;
      
      CITY                ADDRESS             USE COUNT
      --------------------------------------------------------------------------------
      Overland Park       0x55f775311eb0      2
      Lenexa              0x55f775311f40      4
      Olathe              0x55f775311fd0      2
      Shawnee             0x55f775312060      2
      

      At the end of the program we don't have to include code like delete lenexa; to free the allocated memory. Since the shared_ptr is a class, the actual pointer is stored within that class, and utilizing the destructor, it will take care of cleaning up the memory for us.


    • unique_ptr

      Documentation: https://cplusplus.com/reference/memory/unique_ptr/

      With a unique_ptr, we create a pointer that is the only "owner" of the memory block it points to. We can move around "ownership" of that address, but only one pointer may point to it. When the owning unique_ptr goes out of scope and is destroyed, it will automatically free the space allocated at the memory address it owns.

      This can be useful for creating a dynamic array but without having to deal with the memory management. Instead of allocating memory in the traditional way like: string* arr = new string[ array_size ];, we can use the unique_ptr:

      unique_ptr<string[]> arr = unique_ptr<string[]>( new string[ array_size ] );
      

      or you could use the auto keyword to make this a little nicer looking:

      auto arr = unique_ptr<string[]>( new string[ array_size ] );
      

      Otherwise, we use this dynamic array similar to with traditional pointers, but we don't need the delete [] arr; at the end of the program:

      for ( size_t i = 0; i < array_size; i++ )
      {
        cout << "Enter value for #" << i << ": ";
        getline( cin, arr[i] );
      }
      

      If you need to change who owns the pointed-to address, the move command can be used:

      // Previously declared:
      // auto arr = unique_ptr<string[]>( new string[3] );
      // auto biggerArray = unique_ptr<string[]>( new string[ newSize ] );
      arr = move( biggerArray );
      

  • Review questions:
    1. Given the variable declaration: int num = 10; What kind of memory is the num variable stored in?
    2. Given the variable declaration: int* num = new int; What kind of memory is the num variable stored in?
    3. How do you declare a pointer?
    4. How do you access the address of a variable?
    5. How do you assign the address of a variable to a pointer?
    6. How do you dereference a pointer to display the value of the address it's pointing to?
    7. How do you dereference a pointer to store a new value in the memory block pointed to?
    8. When a pointer is not currently in use, it should be initialized to…

πŸ”Ž Concept Intro - CS 200 review: Pointers (U06.CIN)

πŸ§‘β€πŸ”¬ Lab - CS 200 Review - Pointers (U06.LAB)

pixel-goals.png Goals:

  • Practice with getting addresses
  • Practice with pointers
  • Practice with allocating memory for variables via pointer
  • Practice with allocating memory for array via pointer
  • Practice using unique pointers
  • Practice using shared pointers

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-practice.png Practice programs and graded program:

  • This assignment contains several "practice" program folders, and a "graded" program folder. Only the "graded" program is required.
  • The practice programs are there to help you practice with the topics before tackling the larger graded program. If you feel confident in the topic already, you may go ahead and work on the graded program. If you feel like you need additional practice first, that's what the practice assignments are for.
  • The graded program assignment contains unit tests to help verify your code.
  • You can score up to 100% turning in just the graded program assignment.
  • You can turn in the practice assignments for extra credit points.

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.
  • If you're stuck on a practice program, try working on a different one and come back to it later.
  • If you're stuck on the graded program, try the practice programs first.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Setup: Starter code and new branch

    For this assignment, I've already added the code to your repository.

    1. Pull the starter code:
      1. Check out the main branch: git checkout main
      2. Pull latest: git pull
    2. Create a new branch to start working from: git checkout -b BRANCHNAME

    The u06_ReviewPointers folder contains the following items:

    .
    ├── graded_program
    │   ├── Array.cpp
    │   ├── Array.h
    │   ├── INFO.h
    │   ├── main.cpp
    │   ├── Project_CodeBlocks
    │   │   ├── DynamicArray.cbp
    │   │   ├── DynamicArray.depend
    │   │   └── DynamicArray.layout
    │   ├── Project_Makefile
    │   │   └── Makefile
    │   └── Project_VisualStudio2022
    │       ├── DynamicArray
    │       │   ├── DynamicArray.vcxproj
    │       │   └── DynamicArray.vcxproj.filters
    │       └── DynamicArray.sln
    ├── practice1_addresses
    │   └── u06_p1_addresses.cpp
    ├── practice2_pointers
    │   └── u06_p2_pointers.cpp
    ├── practice3_dynamic_variables
    │   └── u06_p3_dynvars.cpp
    ├── practice4_dynamic_arrays
    │   └── u06_p4_dynarr.cpp
    ├── practice5_unique_pointers
    │   └── u06_p5_uniqueptr.cpp
    └── practice6_shared_pointers
        └── u06_p6_sharedptr.cpp
    
    • Practice program projects:

      I have only provided a Visual Studio and Code::Blocks project for the graded assignment. For the practice assignments you will need to create a project and add the file to that project. You can follow the steps in this video:


  • practice1_addresses
    EXPLORING ADDRESSES
    
    Original Variables!
    VARIABLE            ADDRESS             VALUE
    --------------------------------------------------------------------------------
    num1                0x7ffd286acffc      10
    num2                0x7ffd286ad000      15
    num3                0x7ffd286ad004      -5
    
    Pointer!
    Address: 0
    Don't dereference the nullptr!!
    
    ptr is pointing to address: 0x7ffd286acffc
    The value at that address is: 10
    Enter a new value: 500
    
    ptr is pointing to address: 0x7ffd286ad000
    The value at that address is: 15
    Enter a new value: 200
    
    ptr is pointing to address: 0x7ffd286ad004
    The value at that address is: -5
    Enter a new value: 300
    
    Changed Variables!
    VARIABLE            ADDRESS             VALUE
    --------------------------------------------------------------------------------
    num1                0x7ffd286acffc      500
    num2                0x7ffd286ad000      200
    num3                0x7ffd286ad004      300
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    You will need to create an integer pointer and assign it to various addresses throughout the program. Once finished, the output should look similar to the above one.


  • practice2_pointers
    POINTERS AND OBJECTS
    
    PRODUCTS:
    0    Pencil              1.59
    1    Markers             2.39
    2    Eraser              0.59
    Enter # of product to modify: 0
    Enter new price: $9.99
    Enter new name: Super Pencil
    
    UPDATED PRODUCTS:
    0    Super Pencil        9.99
    1    Markers             2.39
    2    Eraser              0.59
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    This program will use pointers to objects, so you will need to also access member variables of the given class. Use the -> operator to access members of an object, when it's being pointed to via a pointer.


  • practice3_dynamic_variables
    LINKED NODES
    
    DYNAMICALLY ALLOCATING MEMORY FOR VARIABLES
    --------------------------------------------------------------------------------
     CREATE NODES AND LINK THEM:
     * Create firstNode (A)
     * Create firstNode->ptrNext (B)
     * Create firstNode->ptrNext->ptrNext (C)
    
     ITERATE THROUGH NODES:
    
     FREE MEMORY:
     * Delete firstNode->ptrNext->ptrNext
     * Delete firstNode->ptrNext
     * Delete firstNode
    

    Look out for the comment lines in the code that give instructions on what to do.

    Follow the steps of the program to create a basic "linked" structure.


  • practice4_dynamic_arrays
    DYNAMICALLY ALLOCATING MEMORY FOR ARRAYS
    
    ----------------------------------------
    ARRAY SIZE: 3, ITEM COUNT: 0
    ARRAY CONTENTS:
    
    Enter a new item to add, or QUIT to quit: cat
    
    ----------------------------------------
    ARRAY SIZE: 3, ITEM COUNT: 1
    ARRAY CONTENTS:
    0. cat
    
    Enter a new item to add, or QUIT to quit: dog
    
    ----------------------------------------
    ARRAY SIZE: 3, ITEM COUNT: 2
    ARRAY CONTENTS:
    0. cat
    1. dog
    
    Enter a new item to add, or QUIT to quit: rat
     * NEED TO RESIZE
     * COPY DATA FROM OLD ARRAY TO NEW ARRAY
     * FREE THE OLD MEMORY
     * UPDATE POINTER TO NEW MEMORY
     * UPDATE ARRAYSIZE
    
    ----------------------------------------
    ARRAY SIZE: 6, ITEM COUNT: 3
    ARRAY CONTENTS:
    0. cat
    1. dog
    2. rat
    
    Enter a new item to add, or QUIT to quit: panda
    
    ----------------------------------------
    ARRAY SIZE: 6, ITEM COUNT: 4
    ARRAY CONTENTS:
    0. cat
    1. dog
    2. rat
    3. panda
    
    Enter a new item to add, or QUIT to quit: QUIT
    
    FREE REMAINING MEMORY BEFORE LEAVING!!
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Follow along with the instructions to implement the core functionality of a dynamic array, which will also be part of the graded program.


  • practice5_unique_pointers
    UNIQUE POINTERS
    
    ----------------------------------------
    ARRAY SIZE: 3, ITEM COUNT: 0
    ARRAY CONTENTS:
    
    Enter a new item to add, or QUIT to quit: eggs
    
    ----------------------------------------
    ARRAY SIZE: 3, ITEM COUNT: 1
    ARRAY CONTENTS:
    0: eggs
    
    Enter a new item to add, or QUIT to quit: bagels
    
    ----------------------------------------
    ARRAY SIZE: 3, ITEM COUNT: 2
    ARRAY CONTENTS:
    0: eggs
    1: bagels
    
    Enter a new item to add, or QUIT to quit: cereal
     * NEED TO RESIZE
     * COPY DATA FROM OLD ARRAY TO NEW ARRAY
     * MOVE BIG ARRAY OWNERSHIP TO ORIGINAL ARRAY
     * UPDATE ARRAYSIZE
    
    ----------------------------------------
    ARRAY SIZE: 6, ITEM COUNT: 3
    ARRAY CONTENTS:
    0: eggs
    1: bagels
    2: cereal
    
    Enter a new item to add, or QUIT to quit: QUIT
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Follow along to practice with unique pointers. This program is similar to the dynamic array program except without the memory management.


  • practice6_shared_pointers
    SHARED POINTERS
    1. Lemon	 2. Lime
    
    Person 1 enter your vote: 1
    Person 2 enter your vote: 2
    Person 3 enter your vote: 2
    
    RESULTS:
    OPTION 1: Lemon, USE COUNT: 2
    OPTION 2: Lime, USE COUNT: 3
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    The shared pointers vote1, vote2, and vote3 will point to either option1 or option2. At the end of the program, we check their use_count() s to see the total amount of references to each address.


  • graded_program
    DYNAMIC ARRAY
    1. Create array
    2. Allocate space
       AllocateSpace
    3. Add items to array, trigger resize
       AddItem
       AddItem
       AddItem
       AddItem
       ResizeArray
       AddItem
    4. Display array
    0: BASIC
    1: COBOL
    2: Java
    3: C#
    4: C++
    5. Deallocate space
       DeallocateSpace
       DeallocateSpace
    

    For this program you will be implementing a dynamic array inside the Array.cpp file.

    The class has already been declared as:

    class Array
    {
    public:
        Array();
        Array( int arraySize );
        ~Array();
    
        void AllocateSpace( int arraySize );
        void DeallocateSpace();
    
        void ResizeArray();
        void AddItem( string value );
        void Display();
    
    private:
        int m_arraySize = 3;
        int m_itemCount = 0;
        string * m_ptr = nullptr;
    
    
        friend void Test_Array_Constructor();
        friend void Test_Array_AllocateSpace();
        friend void Test_Array_DeallocateSpace();
        friend void Test_Array_ResizeArray();
        friend void Test_Array_AddItem();
    };
    

    You can ignore the friend functions - these are used with the unit tests. Basically, marking a function as a friend gives that function access to the private member variables of a class, which helps me with implementing the unit tests.

    The functions of the Array will need to be implemented. You will only need to update Array.cpp.


    • Array::Array()

      This is the default constructor function.

      Initialize the m_ptr pointer to nullptr for safety.


    • Array::Array( int arraySize )

      This is the parameterized constructor function.

      Initialize the m_ptr pointer to nullptr for safety and call the AllocateSpace function, passing in the arraySize.


    • Array::~Array()

      This is the destructor function.

      Call the DeallocateSpace function.


    • void Array::AllocateSpace( int arraySize )

      If m_ptr is NOT pointing to nullptr this means that it is currently in use. Display an error message and return early (you can leave void functions with return;.)

      Otherwise, allocate space for a new array via the m_ptr pointer - we are creating a dynamic array. Use the arraySize given.

      Make sure to also initialize m_arraySize to the arraySize and initialize m_itemCount to 0.

      QUESTION: How do I create a new dynamic array with m_ptr?

      m_ptr = new string[ arraySize ];


    • void Array::DeallocateSpace()

      If m_ptr is NOT pointing to nullptr then we need to free the memory. delete the array at the m_ptr address, then assign nullptr to the m_ptr for safety.

      QUESTION: How do I free the memory that m_ptr is pointing to?

      delete [] m_ptr;

      QUESTION: How do I point m_ptr to nullptr?

      m_ptr = nullptr;


    • void Array::ResizeArray()
      1. Create a new dynamic array with a larger array size. Let's call this newArray.
      2. Copy the data from the old array (m_ptr) to the new array (newArray). You'll use a for loop, and iterate from i=0 to the m_itemCount (not-inclusive).
      3. After copying the data over, free the memory at the m_ptr location using the delete command.
      4. Update the m_ptr pointer to point to the same address as newArray.
      5. Update the value of the m_arraySize to the new array size.

      QUESTION: How do I create a new dynamic array with a larger size?

      string * newArray = new string[ m_arraySize * 2 ]; or some other larger size.

      QUESTION: How do I copy the data from one array to another?

      for ( int i = 0; i < m_itemCount; i++ ) { newArray[i] = m_ptr[i]; }

      QUESTION: How do I free the memory that m_ptr is pointing to?

      delete [] m_ptr;

      QUESTION: How do I point m_ptr to the same address as newArray?

      m_ptr = newArray;


    • void Array::AddItem( string value )

      If the array is full (m_itemCount = marraySize=) then call the ResizeArray function and then continue on.

      Assign the element of m_ptr at the index m_itemCount to the value given.

      Increment m_itemCount afterward.

      QUESTION: How do I assign an item to the m_ptr array?

      m_ptr[INDEX] = value;

      QUESTION: What is the index going to be?

      m_ptr[m_itemCount] = value;


    • void Array::Display()

      Note: This function does not have a unit test.

      Use a for loop to iterate over all the elements of the m_ptr array. Display each item's index and element value.

      QUESTION: What is an index?

      The index is the position of an element in the array. The lowest valid index is 0, and for an array of size size, the highest valid index is size-1.

      QUESTION: How do I iterate over all the elements of the array?

      for ( int i = 0; i < m_itemCount; i++ ) { cout << i << ". " << m_ptr[i] << endl; }


      Once everything is implemented, all the tests should pass. You can also run the program itself to see your dynamic array in use.


  • Turning in the assignment

    Screenshot: Before finishing up, run the automated tests and take a screenshot of all of your tests passing. Save the file somewhere as a .png.

    Back up your code: Open Git Bash in the base directory of your repository folder.

    1. Use git status to view all the changed items (in red).
    2. Use git add . to add all current changed items.
    3. Use git commit -m "INFO" to commit your changes, change "INFO" to a more descriptive message.
    4. Use git push -u origin BRANCHNAME to push your changes to the server.
    5. On GitLab.com, go to your repository webpage. There should be a button to Create a new Merge Request. You can leave the default settings and create the merge request.
    6. Copy the URL of your merge request.

    Turn in on Canvas:

    1. Find the assignment on Canvas and click "Start assignment".
    2. Select Upload file and upload your screenshot.
    3. Paste your GitLab URL into the Comments box.
    4. Click "Submit assignment" to finish.

UNIT 07: The Standard Template Library

πŸ“Š Presentation - The Standard Template Library (U07.PRES.202401CS235)

πŸ“–οΈ Reading - The Standard Template Library (U07.READ)

C++ comes with a bunch of data structures we can use to store sets of data. Each of these have different uses and work better for different designs. They all have documentation available online so you can learn more about how these structures work there as well.

  • Without the STL: Traditional fixed-length array

    One of the first ways you may have learned to store a sequence of data is with a traditional array in C++. These aren't "smart" at all, cannot be resized, and you either have to manually keep track of the size of the array or do a calculation whenever you want to find the size.

    Declaration

    They come in this form:

    const int ARRAY_SIZE = 100;
    int itemCount = 0;
    int myArray[ ARRAY_SIZE ];
    

    The ARRAY_SIZE named constant and the itemCount variable are optional, however, we have to manually keep track of the size of the array and how many items are stored within it - it won't take care of that for us.

    Reading and writing values to this array is simple:

    cout << "Enter a number: ";
    cin >> myArray[ 0 ];
    cout << "Item #0 is " << myArray[0] << endl;
    

    And iterating through it requires, again, keeping track of the amount of items we've stored in the array with some kind of variable:

    Iterating over elements
    for ( int i = 0; i < itemCount; i++ )
    {
      cout << i << " = " << myArray[i] << endl;
    }
    

  • Without the STL: Dynamic array

    We can create dynamic arrays by using pointers. This allows us to define the size of the array at runtime, instead of defining the size at compile-time like with our traditional fixed-length array. The downside is manually managing the memory - making sure to free whatever we have allocated.

    Declaration
    int itemCount = 0;
    int arraySize;
    
    cout << "Enter an array size: ";
    cin >> arraySize;
    
    int* myArray = new int[ arraySize ];
    

    Reading and writing values to the array is the same as with the traditional array. We just have to make sure to free the memory before the program ends:

    Iterating over elements
    for ( int i = 0; i < itemCount; i++ )
    {
      cout << i << " = " << myArray[i] << endl;
    }
    
    Freeing memory

    You need to free the memory you allocate before its pointer loses scope - if this happens, you lose the address and that memory is now taken up and cannot be freed.

    delete [] myArray;
    

  • STL std::array

    Documentation: https://cplusplus.com/reference/array/array/

    he array from the STL is basically a class wrapping our traditional array. It allows us to use the array as an object with basic functions, so that we don't have to keep track of as much.

    Declaration
    array<int, 100> myArray;
    
    Accessing the size
    cout << "Size: " << myArray.size();
    
    Accessing elements
    cout << "Enter item 0: ";
    cin >> myArray[0];
    
    Iterating over elements
    for ( int i = 0; i < myArray.size(); i++ )
    {
      cout << i << " = " << myArray[i] << endl;
    }
    

  • STL std::vector

    Documentation: https://cplusplus.com/reference/vector/vector/

    Vectors are dynamic arrays so you can add items to it and it will resize - no worries on your part regarding resizing and managing memory.

    Declaration
    vector<int> myVector;
    
    Accessing the size
    cout << "Size: " << myVector.size();
    
    Accessing elements

    Because you're generally not pre-allocating space for a vector, you need to use the push_back function to add additional items to the end of the vector's internal array.

    cout << "Enter item 0: ";
    int item;
    cin >> item;
    myVector.push_back( item );
    
    Iterating over elements
    for ( int i = 0; i < myVector.size(); i++ )
    {
      cout << i << " = " << myVector[i] << endl;
    }
    

  • STL std::list

    Documentation: https://cplusplus.com/reference/list/list/

    The list is similar to a vector in that you can store any amount of items in it, however you can only ever access the front and the back of the list - not random items in the middle. You can still iterate over all the items, though.

    Declaration
    list<int> myList;
    
    Accessing the size
    cout << "Size: " << myList.size();
    
    Adding data
    myList.push_front( 1 ); // Stored at the start of the list
    myList.push_back( 10 ); // Stored at the end of the list
    
    Accessing data
    cout << "Front item: " << myList.get_front() << endl;
    cout << "Back item:  " << myList.get_back() << endl;
    
    Removing data
    myList.pop_front();
    myList.pop_back();
    
    Iterating over elements
    for ( auto& item : myList )
    {
      cout << item << endl;
    }
    

  • STL std::stack

    stack.png

    Documentation: https://cplusplus.com/reference/stack/stack/

    A stack is a type of restricted-access data type. It can store a series of items, but you can only add and remove items from the top.

    Declaration
    stack<char> myStack;
    
    Accessing the size
    cout << "Size: " << myStack.size();
    
    Adding data
    myStack.push( 'A' );
    myStack.push( 'B' );
    myStack.push( 'C' );
    
    Accessing data
    cout << "Top item is: " << myStack.top() << endl;
    
    Removing data
    myStack.pop(); // Remove top item
    

  • STL std::queue

    queue.png

    Documentation: https://cplusplus.com/reference/queue/queue/

    A queue is another type of restricted-access data type. It stores a series of items, with items being added at the back of the queue, and being removed from the front of the queue, similar to waiting in line at the store.

    Declaration
    queue<char> myQueue;
    
    Accessing the size
    cout << "Size: " << myQueue.size() << endl;
    
    Adding data
    myQueue.push( 'A' );
    myQueue.push( 'B' );
    myQueue.push( 'C' );
    
    Accessing data
    cout << "The front item is: " << myQueue.front() << endl;
    
    Removing data
    myQueue.pop(); // Remove front item
    

  • STL std::map

    hashtable.png

    Documentation: https://cplusplus.com/reference/map/map/

    Our traditional arrays have index numbers (0, 1, 2, …) and elements stored at that position in the array.

    With a map, we can have any data type as a unique identifier for an element. This could be an integer, but it doesn't have to be 0, 1, 2, and so on - it could be an employee ID, a phone number, etc., but the data type of the identifier (called a key) can be any data type.

    Declaration
    map<int, string> area_codes;
    
    Adding data
    area_codes[913] = "Northeastern Kansas";
    area_codes[816] = "Northwestern Missouri";
    
    Accessing the size
    cout << "Size: " << area_codes.size();
    
    Accessing data
    int key;
    cout << "Enter an area code: ";
    cin >> key;
    
    cout << "The region for that is: " << area_codes[ key ];
    
    Iterating over elements
    for ( auto& item : area_codes )
    {
      cout << "Key: " << item.first << ", Value: " << item.second << endl;
    }
    

  • Review questions:
    1. Identify what "type" each of the following is… (traditional array / traditional dynamic array / array object / vector object)
      1. vector<int> arr;
      2. int * arr = new int[100];
      3. array<int, 100> arr;
      4. int arr[100];
    2. Which of these std data types can be resized (store any amount of data after declaration)? (array / vector / list)
    3. You can only add items to the … of a stack, and you can only remove items from the … of a stack.
    4. You can only add items to the … of a queue, and you can only remove items from the … of a queue.
    5. True or false: A map can use any data type for its unique key item.

πŸ”Ž Concept Intro - The Standard Template Library (U07.CIN)

πŸ§‘β€πŸ”¬ Lab - The Standard Template Library (U07.LAB)

pixel-goals.png Goals:

  • Practice with STL array
  • Practice with STL vector
  • Practice with STL list
  • Practice with STL map
  • Practice with STL queue
  • Practice with STL stack

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-practice.png Practice programs and graded program:

  • This assignment contains several "practice" program folders, and a "graded" program folder. Only the "graded" program is required.
  • The practice programs are there to help you practice with the topics before tackling the larger graded program. If you feel confident in the topic already, you may go ahead and work on the graded program. If you feel like you need additional practice first, that's what the practice assignments are for.
  • The graded program assignment contains unit tests to help verify your code.
  • You can score up to 100% turning in just the graded program assignment.
  • You can turn in the practice assignments for extra credit points.

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.
  • If you're stuck on a practice program, try working on a different one and come back to it later.
  • If you're stuck on the graded program, try the practice programs first.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Setup: Starter code and new branch

    For this assignment, I've already added the code to your repository.

    1. Pull the starter code:
      1. Check out the main branch: git checkout main
      2. Pull latest: git pull
    2. Create a new branch to start working from: git checkout -b BRANCHNAME

    The u07_StandardTemplateLibrary folder contains the following items:

    .
    ├── examples
    │   ├── array_example.cpp
    │   ├── list_example.cpp
    │   ├── map_example.cpp
    │   ├── queue_example.cpp
    │   ├── stack_example.cpp
    │   └── vector_example.cpp
    ├── graded_program
    │   ├── Customer.h
    │   ├── events.txt
    │   ├── GroceryStoreProgram.cpp
    │   ├── GroceryStoreProgram.h
    │   ├── INFO.h
    │   ├── main.cpp
    │   ├── Product.h
    │   ├── Project_CodeBlocks
    │   │   ├── STL.cbp
    │   │   ├── STL.depend
    │   │   └── STL.layout
    │   ├── Project_Makefile
    │   │   └── Makefile
    │   ├── Project_VisualStudio2022
    │   │   ├── StandardTemplateLibraryProject.sln
    │   │   ├── StandardTemplateLibraryProject.vcxproj
    │   │   └── StandardTemplateLibraryProject.vcxproj.filters
    │   └── Tester.cpp
    ├── practice1_array
    │   └── u07_p1_array.cpp
    ├── practice2_vector
    │   └── u07_p2_vector.cpp
    ├── practice3_list
    │   └── u07_p3_list.cpp
    ├── practice4_map
    │   └── u07_p4_map.cpp
    ├── practice5_stack
    │   └── u07_p5_stack.cpp
    └── practice6_queue
        └── u07_p6_queue.cpp
    

    The examples folder contains examples of each of the Standard Template Library items we're using in this assignment.


  • practice1_array
    ARRAY PROGRAM
    
    COURSES:
    0: CS 134
    1: CS 200
    2: CS 235
    3: CS 250
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Utilize the STL array class (https://cplusplus.com/reference/array/array/) for this program. Set the array's size and fill it up with items, then iterate over the array, displaying each index and element value.

    QUESTION: How do I iterate over an array?

    for ( size_t i = 0; i < ARRAYNAME.size(); i++ ) for the loop.

    QUESTION: How do I get the index?

    Within the loop example above, i is the index.

    QUESTION: How do I get the element?

    Within the loop example above, ARRAYNAME[i] is the element.


  • practice2_vector
    VECTOR PROGRAM
    Enter a new course, or STOP to finish: CS134
    Enter a new course, or STOP to finish: CS200
    Enter a new course, or STOP to finish: CS235
    Enter a new course, or STOP to finish: STOP
    
    COURSES:
    0: CS134
    1: CS200
    2: CS235
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Utilize the STL vector class (https://cplusplus.com/reference/vector/vector/) for this program. Use the push_back function to add new items to the vector, and use a normal for loop to display each index and element value.

    QUESTION: How do I iterate over a vector?

    for ( size_t i = 0; i < VECTORNAME.size(); i++ ) for the loop.

    QUESTION: How do I get the index?

    Within the loop example above, i is the index.

    QUESTION: How do I get the element?

    Within the loop example above, VECTORNAME[i] is the element.


  • practice3_list
    LIST PROGRAM
    
    Enter a new course, or STOP to finish: ASL120
    Insert at (F) FRONT or (B) BACK? F
    Enter a new course, or STOP to finish: ASL121
    Insert at (F) FRONT or (B) BACK? B
    Enter a new course, or STOP to finish: CS134
    Insert at (F) FRONT or (B) BACK? F
    Enter a new course, or STOP to finish: CS250
    Insert at (F) FRONT or (B) BACK? B
    Enter a new course, or STOP to finish: STOP
    
    COURSES:
    F
    F
    B
    B
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Utilize the STL list class (https://cplusplus.com/reference/list/list/) for this program. As you add items to the list, the user can choose to put the new item at the FRONT or the BACK of the list. Afterwards, use a range-based for loop to iterate over all the items in the list to display each one.

    QUESTION: How do I iterate over a list to display each value?

    for ( auto& item : LISTNAME ) for the loop.


  • practice4_map
    MAP PROGRAM
    
    Item: burrito, Price: $1.29
    Item: quesadilla, Price: $2.36
    Item: taco, Price: $1.29
    
    Enter a food: burrito
    The price is: $1.29
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Utilize the STL map class (https://cplusplus.com/reference/map/map/) for this program. You'll add items to the map, access items via their keys, and utilize a range-based for loop to iterate over all the items in the map.

    QUESTION: How do I iterate over a map and display all keys/values?

    for ( auto& item : product_prices ) for the loop, then you can use item.first to get the key, and item.second to get the value.


  • practice5_stack
    STACK PROGRAM
    --------------------------------------------------
    STACK IS EMPTY
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 1
    Enter new text to push on stack: A
    --------------------------------------------------
    TOP ITEM IN STACK: A
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 1
    Enter new text to push on stack: B
    --------------------------------------------------
    TOP ITEM IN STACK: B
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 1
    Enter new text to push on stack: C
    --------------------------------------------------
    TOP ITEM IN STACK: C
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 2
    Popped top item off stack
    --------------------------------------------------
    TOP ITEM IN STACK: B
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 0
    
    --------------------------------------------------
    TOP OF STACK: B
    TOP OF STACK: A
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Utilize the STL stack class (https://cplusplus.com/reference/stack/stack/) for this program. You'll create a stack, use push, pop, and top functions to interface with it.


  • practice6_queue
    QUEUE PROGRAM
    --------------------------------------------------
    QUEUE IS EMPTY
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 1
    Enter new text to push on queue: A
    --------------------------------------------------
    FRONT ITEM IN QUEUE: A
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 1
    Enter new text to push on queue: B
    --------------------------------------------------
    FRONT ITEM IN QUEUE: A
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 1
    Enter new text to push on queue: C
    --------------------------------------------------
    FRONT ITEM IN QUEUE: A
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 2
    Popped top item off queue
    --------------------------------------------------
    FRONT ITEM IN QUEUE: B
    --------------------------------------------------
    0. Quit
    1. PUSH item
    2. POP item
    >> 0
    
    --------------------------------------------------
    FRONT OF QUEUE: B
    FRONT OF QUEUE: C
    
    THE END
    

    Look out for the comment lines in the code that give instructions on what to do.

    Utilize the STL queue class (https://cplusplus.com/reference/queue/queue/) for this program. You'll create a queue, use push, pop, and front functions to interface with it.


  • graded_program
    (8:00) Added apple @ $0.76 to shelves.
    (8:05) Added avocado @ $2.14 to shelves.
    (8:10) Added banana @ $0.24 to shelves.
    (8:15) Added watermelon @ $12.59 to shelves.
    (8:20) Customer Maribel entered store.
    (8:25) Customer Maribel adds apple to their cart.
    (8:30) Customer Maribel adds apple to their cart.
    (8:35) Customer Maribel decides to put apple back.
    (8:40) Customer Maribel adds watermelon to their cart.
    (8:45) Customer Sara entered store.
    (8:50) Customer Sara adds apple to their cart.
    (8:55) Customer Sara queues up in the checkout line.
    (9:00) Customer Cody entered store.
    (9:05) Customer Cody adds watermelon to their cart.
    (9:10) Customer Cody adds watermelon to their cart.
    (9:15) Customer Cody adds watermelon to their cart.
    (9:20) Customer Cody decides to put watermelon back.
    (9:25) Customer Cody queues up in the checkout line.
    (9:30) Customer Rai entered store.
    (9:35) Customer Rai adds avocado to their cart.
    (9:40) Customer Rai adds apple to their cart.
    (9:45) Customer Rai adds watermelon to their cart.
    (9:50) Customer Maribel queues up in the checkout line.
    (9:55) Customer Rai queues up in the checkout line.
    (10:00) Customer Sara is at the register.
    (10:05)   >> apple ($0.76) is their next item.
    (10:10)   >> Checkout done. Total: $0.76... Customer Sara leaves.
    (10:15) Customer Cody is at the register.
    (10:20)   >> watermelon ($12.59) is their next item.
    (10:25)   >> watermelon ($12.59) is their next item.
    (10:30)   >> Checkout done. Total: $25.18... Customer Cody leaves.
    (10:35) Customer Maribel is at the register.
    (10:40)   >> watermelon ($12.59) is their next item.
    (10:45)   >> apple ($0.76) is their next item.
    (10:50)   >> Checkout done. Total: $13.35... Customer Maribel leaves.
    (10:55) Customer Rai is at the register.
    (11:00)   >> watermelon ($12.59) is their next item.
    (11:05)   >> apple ($0.76) is their next item.
    (11:10)   >> avocado ($2.14) is their next item.
    (11:15)   >> Checkout done. Total: $15.49... Customer Rai leaves.
    (11:20) Done processing line!
    

    This program simulates events in a grocery store. The events.txt contains a series of commands, though you don't have to worry about parsing the file. You only need to implement functionality in the GroceryStoreProgram.cpp file.

    You will need to keep in mind these structures as you work on your class:

    Product.h:

    struct Product
    {
        string name;
        float price;
    };
    

    Customer.h: Each Customer has a name and a stack of products_in_cart. Sometimes, customers will put something in their cart and then change their mind. When this happens, the item at the top of the stack will be popped to be removed.

    struct Customer
    {
        string name;
        stack<string> products_in_cart;
    };
    

    GroceryStoreProgram.h:

    class GroceryStoreProgram
    {
    public:
        GroceryStoreProgram();
    
        void Stock( string product, float price );
        void CustomerEnterStore( string customer );
        void CustomerCartAdd( string customer, string product );
        void CustomerOops( string customer );
        void CustomerLineup( string customer );
        void Process();
        void PrintTimestamp();
    
    private:
        queue<Customer> checkout_queue;
        map<string, float> product_prices;
        map<string, Customer> customers_in_store;
        int minutes, hours;
    
        friend void Tester_GroceryStoreProgram();
    };
    

    The grocery store program has several important member variables:

    • queue<Customer> checkout_queue is the queue of customers in line waiting to check out. The customer at the front of the line gets helped first.
    • map<string, float> product_prices is a map of product names -> product prices. This can be used to look up the price of a product.
    • map<string, Customer> customers_in_store is a map of all customers in the store. The customer's name is the key, and then a Customer object is the value.

    • void GroceryStoreProgram::Stock( string product, float price )

      Use the product as the key and the price as the value, adding the data to the product_prices map.


    • void GroceryStoreProgram::CustomerEnterStore( string customer )

      Use the customer as the key for a new item in the customers_in_store map.


    • void GroceryStoreProgram::CustomerCartAdd( string customer, string product )

      Access the customer at the customer key. The Customer object contains a products_in_cart stack, push the new product onto that stack.


    • void GroceryStoreProgram::CustomerOops( string customer )

      Access the customer at the customer key. The Customer object contains a products_in_cart stack, use the pop function to remove the last-added item from their cart.


    • void GroceryStoreProgram::CustomerLineup( string customer )

      Access the customer at the customer key in the customers_in_store map. Push this customer into the checkout_queue.


    • void GroceryStoreProgram::Process()

      This function will process everybody who is currently in the checkout_queue.

      1. While the checkout queue is not empty:
        1. Display the name of the customer at the front of the checkout queue.
        2. Create a float variable to store the total cost of the transaction, initialize to 0.
        3. For the front customer, while their products_in_cart stack is not empty:
          1. Get the price of the next item - use the product_prices and the key of the product name.
          2. Add the product price to your total variable.
          3. Display the front item in the customer's cart and the price.
          4. Pop the top item from the products_in_cart.
        4. After the while loop, display that checkout is done and the total amount of money.
        5. Pop the customer from the checkout queue.

      Hints:

      • You can get the next customer in line with: checkout_queue.front().
      • You can access Customer member variables further: checkout_queue.front().name
      • You can access the next customer's next item in cart with: checkout_queue.front().products_in_cart.top()
      • You can get the price of an item with product_prices[ KEY ]. The key will be the product name, which corresponds to checkout_queue.front().products_in_cart.top().

      Once everything is implemented, all the tests should pass. You can also run the program itself to see the "simulation" run.


  • Turning in the assignment

    Screenshot: Before finishing up, run the automated tests and take a screenshot of all of your tests passing. Save the file somewhere as a .png.

    Back up your code: Open Git Bash in the base directory of your repository folder.

    1. Use git status to view all the changed items (in red).
    2. Use git add . to add all current changed items.
    3. Use git commit -m "INFO" to commit your changes, change "INFO" to a more descriptive message.
    4. Use git push -u origin BRANCHNAME to push your changes to the server.
    5. On GitLab.com, go to your repository webpage. There should be a button to Create a new Merge Request. You can leave the default settings and create the merge request.
    6. Copy the URL of your merge request.

    Turn in on Canvas:

    1. Find the assignment on Canvas and click "Start assignment".
    2. Select Upload file and upload your screenshot.
    3. Paste your GitLab URL into the Comments box.
    4. Click "Submit assignment" to finish.

    \newpage

WEEK 4 - FEB 5

UNIT 08: Recursion

πŸ“–οΈ Reading - Recursion (U08.READ.202401CS235)

reading_u08_Recursion_recursionB.png

  • Examples of recursion in math

    Recursion is a manner of solving a problem by breaking it down into smaller pieces - and those smaller pieces are solved in the same way as the big-picture version.

    • Summation:

      A summation can be broken down into smaller chunks but using the same structure as the original.

      Let's say we have \[ \sum_{i=1}^{6} { i } = 1 + 2 + 3 + 4 + 5 + 6 \]

      The summation can be redefined recursively:

      \[ \sum_{i=1}^{6} { i } = 6 + \sum_{i=1}^{5} { i } \]

      The summation from \(i = 1\) to \(6\) is equivalent to whatever the sum is at \(i=6\), plus the sum from \(i = 1\) to \(5\). We can continue this way until we get to a case that we know (e.g., \(\sum_{i=1}^{1} { i }\)).

      • \(\sum_{i=1}^{6} { i } = 6 + \sum_{i=1}^{5} { i }\)
      • \(\sum_{i=1}^{5} { i } = 5 + \sum_{i=1}^{4} { i }\)
      • \(\sum_{i=1}^{4} { i } = 4 + \sum_{i=1}^{3} { i }\)
      • \(\sum_{i=1}^{3} { i } = 3 + \sum_{i=1}^{2} { i }\)
      • \(\sum_{i=1}^{2} { i } = 2 + \sum_{i=1}^{1} { i }\)
      • We know that \(\sum_{i=1}^{1} { i } = 1\), then we move back up to sub out this value.
      Recursive problem Finding the solution
      A. \[\sum^{6}_{i=1} { i } = 6 + \sum_{i=1}^{5} { i }\] But what is \(\sum_{i=1}^{5} { i }\)? \(\downarrow\) K. DONE! \[\sum_{i=1}^{6} { i } = 6 + \sum_{i=1}^{5} { i } = 6 + 15 = 21\]
      B. \[\sum_{i=1}^{5} { i } = 5 + \sum_{i=1}^{4} { i }\] But what is \(\sum_{i=1}^{4} { i }\)? \(\downarrow\) J. \(\uparrow\) \[\sum_{i=1}^{5} { i } = 5 + \sum_{i=1}^{4} { i } = 5 + 10 = 15\]
      C. \[\sum_{i=1}^{4} { i } = 4 + \sum_{i=1}^{3} { i }\] But what is \(\sum_{i=1}^{3} { i }\)? \(\downarrow\) I. \(\uparrow\) \[\sum_{i=1}^{4} { i } = 4 + \sum_{i=1}^{3} { i } = 4 + 6 = 10\]
      D. \[\sum_{i=1}^{3} { i } = 3 + \sum_{i=1}^{2} { i }\] But what is \(\sum_{i=1}^{2} { i }\)? \(\downarrow\) H. \(\uparrow\) \[\sum_{i=1}^{3} { i } = 3 + \sum_{i=1}^{2} { i } = 3 + 3 = 6\]
      E. \[\sum_{i=1}^{2} { i } = 2 + \sum_{i=1}^{1} { i }\] But what is \(\sum_{i=1}^{1} { i }\)? \(\downarrow\) G. \(\uparrow\) \[\sum_{i=1}^{2} { i } = 2 + \sum_{i=1}^{1} { i } = 2 + 1 = 3\]
      F. \[\sum_{i=1}^{1} { i } = 1\] \(\uparrow\)
      Now we start substituting this value, from bottom-up… \(\rightarrow\) \(\uparrow\)

    • Factorials:

      With a factorial of \(n\), written \(n!\) the formula to solve this is:

      \[ n! = n \cdot (n-1) \cdot ... \cdot 3 \cdot 2 \cdot 1 \]

      So,

      • \(2!\) is \(2 \cdot 1\),
      • \(3!\) is \(3 \cdot 2 \cdot 1\),
      • \(4!\) is \(4 \cdot 3 \cdot 2 \cdot 1\), and so on.

      Additionally, we can break down each of these equations: \(3!\) is equivalent to \(3 \cdot 2!\) \(4!\) is equivalent to \(4 \cdot 3!\) … \(n!\) is equivalent to \(n \cdot (n-1)!\)

      Thinking of breaking down the problem here in this way is looking at it recursively.

      reading_u08_Recursion_recursivefactorial.png


  • Recursion in programming

    In programming, we usually approach problems iteratively, using a for-loop or a while-loop:

    // Iterative solution
    int Sum( int n )
    {
        int result = 0;
        for ( int i = 1; i <= n; i++ )
        {
            result += i;
        }
        return result;
    }
    

    Which solves this summation:

    \[ \sum_{i=1}^n {i} \]

    But some types of problems lend themselves better to a recursive solution. To be fair, though, many problems are better solved iteratively. So how do we know which method is better?


    • Recursion basics

      When defining a problem recursively in programming, we need two things:

      1. A terminating case: A case that ends our recursing. Often, this is some known data, something hard-coded. For example with our summation, the terminating case would be that \[ \sum_{i=1}^1 {i} = 1 \] or for a factorial, \(1! = 1\) and \(0! = 1\).
      2. A recursive case: A recursive case is what happens otherwise - if we're not to a solution yet (via the terminating case), we call the same function again, but with updated arguments. For example:
        • Factorial( 4 ) = 4 * Factorial( 3 )
        • Factorial( 3 ) = 3 * Factorial( 2 )
        • Factorial( 2 ) = 2 * Factorial( 1 )
        • Factorial( 1 ) = 1

      We can solve these basic math operations both iteratively and recursively:

      // Iterative solution
      int FactorialI( int n )
      {
          int result = 1;
          for ( int i = 1; i <= n; i++ )
          {
              result *= i;
          }
          return result;
      }
      
      // Recursive solution
      int FactorialR( int n )
      {
          if ( n == 1 || n == 0 ) { return 1; }
          return n * FactorialR( n-1 );
      }
      

    • Breaking down problems into recursive solutions

      reading_u08_Recursion_recursionstress.png

      One of the most challenging parts of recursion, at least for me, is trying to break away from thinking of something in terms of "looping" and figuring out how to think of it "recursively". It's not as natural-feeling, so don't worry if it's confusing at first.

      Let's tackle some basic design problems to practice.

      Summation:
      Try to convert the Summation function to be recursive. Think about what the terminating case would be and the recursive case. Use the Factorial function for reference.
      int SumI( int n )
      {
          int result = 0;
          for ( int i = 1; i <= n; i++ )
          {
              result += i;
          }
          return result;
      }
      
      int SumR( int n )
      {
          // Terminating case?
          // Recursive case?
      }
      
      Solution for recursive summation:
      int SumR( int n )
      {
          if ( n == 1 ) { return 1; }  // Terminating case
          return n + SumR( n-1 );      // Recursive case
      }
      

      Draw a line:
      Now let's make a function that will draw a line of symbols, with a parameter being the length. Iteratively, it could look like this:
      void DrawLineI( int amount )
      {
          for ( int i = 0; i < amount; i++ )
          {
              cout << "-";
          }
      }
      

      How would we repeat this behavior recursively? How do we have a "count up" sort of functionality? What would be the terminating case?

      We're going to think of it a little differently: The recursive function will only output one "-" before it recurses. Each time it recurses, it draws one more dash…

      void DrawLine_Recursive( int amount )
      {
          cout << "-";
          // Recursive case
          DrawLineR( amount );
      }
      

      However, we don't have a terminating case… it will continue looping, but it won't go forever like a bad while loop. We will eventually run out of stack space and the program will encounter a stack overflow and end.

      So what would the terminating case be? How do we adjust the amount each time? Since amount is the one parameter we have, let's have the recursion stop once it is 0. Each time we recurse, we can pass in amount-1 to the next call…

      void DrawLine_Recursive( int amount )
      {
          cout << "-";
      
          // Terminating case
          if ( amount == 0 ) { return; }
      
          // Recursive case
          DrawLineR( amount - 1 );
      }
      

      Counting Up:
      How can we write a function that takes a start and end integer, and outputs each number between them (including the start and end)?

      Iteratively, it could look like this:

      void CountUpI( int start, int end )
      {
          for ( int i = start; i <= end; i++ )
          {
              cout << i << "\t";
          }
      }
      

      Try to fill in this function to build a recursive solution:

      void CountUpR( int start, int end )
      {
      }
      
      
      Solution for recursive count up:
      void CountUpR( int start, int end )
      {
          cout << start << "\t";
      
          // Terminating case
          if ( start == end ) { return; }
      
          // recursive case
          CountUp_Recursive( start+1, end );
      }
      

    • A case for recursion

      Although there are a lot of problems we could convert from an iterative solution to a recursive solution, there are some types of problems that really are better suited to recursion.

      • Searching a File System

        reading_u08_Recursion_filesystem.png

        On a harddrive, we generally have files and folders. Folders can contain files, but they will also contain subfolders as well. And subfolders can each contain their own subfolders.

        When you don't know the exact layout of the filesystem, how would you even begin to iteratively search for a specific file?

        Instead, it is good to think of it recursively. For example, say we're searching for a folder where you store your Recursion homework. We will begin searching at the top-most folder of the computer. The algorithm would then run like…

        1. folder = "C:", 2. folder = "work", 3. folder = "C:"
        reading_u08_Recursion_traverse1.png reading_u08_Recursion_traverse2.png reading_u08_Recursion_traverse1.png
        Is this "Recursion"? No. Is this "Recursion"? No. Are there subfolders? Yes.
        Are there subfolders? Yes. Are there subfolders? No. Then, search next subfolder.
        Then, search first subfolder. Return.  
        4. folder = "school" 5. folder = "cs210" 6. folder = "school"
        reading_u08_Recursion_traverse3.png reading_u08_Recursion_traverse4.png reading_u08_Recursion_traverse5.png
        Is this "Recursion"? No. Is this "Recursion"? No. Are there subfolders? Yes.
        Are there subfolders? Yes. Are there subfolders? No. Then, search next subfolder.
        Then, search next subfolder. Return.  
        7. folder = "cs235" 8. 9.
        reading_u08_Recursion_traverse6.png reading_u08_Recursion_traverse8.png reading_u08_Recursion_traverse9.png
        Is this "Recursion"? No. folder = "Recursion" folder = "cs235"
        Are there subfolders? Yes. Is this "Recursion"? Yes. Return Recursion.
        Then, search the first subfolder. Return Recursion.  
        10. folder = "school" 11. folder = "C:"  
        reading_u08_Recursion_traverse10.png reading_u08_Recursion_traverse11.png  
        Return Recursion. Return Recursion.  

        For find functionality, terminating cases would be:

        1. Have we found the item? Return it.
        2. Are we out of places to search? Return nothing.

        And the recursive case would be:

        1. Has a subfolder? Call Find() on that folder.

  • Review questions:
    1. Recursion of a means of problem solving that…
    2. Each recursive function needs at least one … case and at least one … case.
    3. We are designing a recursive function CountUp( start, end )

      • The function displays start and CountUp( start+1, end )
      • The function returns when start is greater than end.

      What is the terminating case?

    4. We are designing a recursive function to figure out the result of \(n!\)…

      • The result of \(n!\) is equal to \(n\) times \((n-1)!\)
      • The result of \(0!\) is 1.

      What is the terminating case?

πŸ”Ž Concept Intro - Recursion (U08.CIN.202401CS250)

πŸ§‘β€πŸ”¬ Lab - Recursion (U08.LAB)

pixel-goals.png Goals:

  • Practice writing recursive functions for several different problems.

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-practice.png Practice programs and graded program:

  • This assignment contains several "practice" program folders, and a "graded" program folder. Only the "graded" program is required.
  • The practice programs are there to help you practice with the topics before tackling the larger graded program. If you feel confident in the topic already, you may go ahead and work on the graded program. If you feel like you need additional practice first, that's what the practice assignments are for.
  • The graded program assignment contains unit tests to help verify your code.
  • You can score up to 100% turning in just the graded program assignment.
  • You can turn in the practice assignments for extra credit points.

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.
  • If you're stuck on a practice program, try working on a different one and come back to it later.
  • If you're stuck on the graded program, try the practice programs first.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Setup: Starter code and new branch

    For this assignment, I've already added the code to your repository.

    1. Pull the starter code:
      1. Check out the main branch: git checkout main
      2. Pull latest: git pull
    2. Create a new branch to start working from: git checkout -b BRANCHNAME

    The u08_Recursion folder contains the following items:

    .
    ├── graded_program
    │   ├── FilesystemNode.h
    │   ├── Functions.cpp
    │   ├── Functions.h
    │   ├── INFO.h
    │   ├── main.cpp
    │   ├── Project_CodeBlocks
    │   │   ├── FilesystemProject.cbp
    │   │   ├── FilesystemProject.depend
    │   │   └── FilesystemProject.layout
    │   ├── Project_Makefile
    │   │   └── Makefile
    │   └── Project_VisualStudio2022
    │       ├── FilesystemProject.sln
    │       ├── FilesystemProject.vcxproj
    │       └── FilesystemProject.vcxproj.filters
    ├── practice1_countup
    │   └── countup.cpp
    ├── practice2_abc
    │   └── abc.cpp
    ├── practice3_countletter
    │   └── countletter.cpp
    └── practice4_factorial
        └── factorial.cpp
    

  • practice1_countup

    CountUp: Displays each number from start to end (inclusive).

    I've already implemented the iterative solution to this using a for loop. You'll be implementing the recursive solution.

    This program doesn't have any unit tests, but the program itself is a manual test - you can visually check if the Recursive output matches the Iterative output.

    CountUp_Iterative( 2, 5 ): 2 3 4 5
    CountUp_Recursive( 2, 5 ): 2 3 4 5
    
    CountUp_Iterative( 10, 20 ): 10 11 12 13 14 15 16 17 18 19 20
    CountUp_Recursive( 10, 20 ): 10 11 12 13 14 15 16 17 18 19 20
    

    Iterative version:

    void CountUp_Iterative( int start, int end )
    {
      for ( int i = start; i <= end; i++ )
      {
        cout << i << " ";
      }
    }
    

    We use a for loop to start a counter i at the start position, loop while i is less than or equal to the end position, and add 1 to i each time. Within the loop, we display the current value of i to the screen.

    Recursive version:

    With a recursive solution we solve a problem by breaking a big problem into a sequence of identical smaller problems. We approach the question

    "What is CountUp( start , end )?"

    with the answer:

    "Well, it's going to be "start" then… CountUp( start+1 , end )!"…

    "Well, what is CountUp( start+1, end )?"

    "It's start+1 then CountUp( start+2 to end )!"…

    and so on until finally the start value is past the end value, then we're done - we terminate.

    We don't need a counter variable i here, instead we move start closer to the end each time we call the CountUp_Recursive function again.

    • Terminating case: If start is greater than end, return - we're done.
    • Recursive case: Display the current value of start, then call CountUp again, passing in start+1 as the beginning and end as the end.

    lab_u08_Recursion_countup.png


  • practice2_abc
    MANUAL TESTS:
    Abc_Iterative( 'A', 'G' ): ABCDEFG
    Abc_Recursive( 'A', 'G' ): ABCDEFG
    
    Abc_Iterative( 'm', 'p' ): mnop
    Abc_Recursive( 'm', 'p' ): mnop
    
    AUTOMATED TESTS:
    [PASS]  TEST 1, Abc_Recursive(a, f, "") = [abcdef]
    [PASS]  TEST 2, Abc_Recursive(C, G, "") = [CDEFG]
    

    string Abc_Recursive( char start, char end, string built_string )

    This function takes in a start, end, and a built_string. The function is responsible for filling the built_string with the letters from start to end (inclusive). A char can be treated like an integer, where adding 1 to a letter (like 'a') will give you the next letter.

    • Terminating case: If the start is greater than the end, then we're done - return the built_string.
    • Recursive case: Add the start character to the built_string string, then call the function again with start+1 to move forward, the same end, and the built_string. (Make sure to put return before the function call, since a value must be returned!)

    lab_u08_Recursion_abc.png


  • practice3_countletter
    MANUAL TESTS:
    CountLetter_Iterative( "Hello dello!", 'e' ): 2
    CountLetter_Recursive( "Hello dello!", 'e' ): 2
    
    CountLetter_Iterative( "abcd", 'z' ): 0
    CountLetter_Recursive( "abcd", 'z' ): 0
    
    AUTOMATED TESTS:
    [PASS]  TEST 1, CountLetter_Recursive("aabbcc", 'a', 0) = [2]
    [PASS]  TEST 2, CountLetter_Recursive("onethree", 'e', 0) = [3]
    [PASS]  TEST 3, CountLetter_Recursive("xyz", 'X', 0) = [0]
    

    lab_u08_Recursion_countletter.png

    int CountLetter_Recursive( string text, char letter_to_count, size_t i )

    This function takes in a line of text and has a letter_to_count. When it finds that letter in the string, it will add one to the result. The i variable is used as a counter, the current letter we're looking at in the text string.

    • Terminating case: If i is greater than or equal to text.size(), then we're done with the string. Return 0 in this case.
    • Recursive case:
      • a. If text[i] is equal to the letter_to_count, then return 1, plus another call to the function, passing in text, letter, and i+1.
      • b. Otherwise, (text[i] is not equal to the letter_to_count) return the function call, passing in text, letter, and i+1. (I like to put a 0 + at the start to mirror the other case and to be explicit that no additional letter is being counted, but the 0 + is not required. I just think it helps readability.)

  • practice4_factorial
    MANUAL TESTS:
    Factorial_Iterative( 0 ): 1
    Factorial_Recursive( 0 ): 1
    
    Factorial_Iterative( 1 ): 1
    Factorial_Recursive( 1 ): 1
    
    Factorial_Iterative( 3 ): 6
    Factorial_Recursive( 3 ): 6
    
    Factorial_Iterative( 5 ): 120
    Factorial_Recursive( 5 ): 120
    
    AUTOMATED TESTS:
    [PASS]  TEST 1, Factorial_Recursive(0) = [1]
    [PASS]  TEST 2, Factorial_Recursive(1) = [1]
    [PASS]  TEST 3, Factorial_Recursive(3) = [6]
    [PASS]  TEST 4, Factorial_Recursive(5) = [120]
    

    int Factorial_Recursive( int n )

    This function takes in n, and returns the result of \(n!\), where \[n! = n \cdot (n-1) \cdot (n-2) \cdot ... \cdot 3 \cdot 2 \cdot 1\].

    Note that \(n! = 0\) and \(n! = 1\). These make up our terminating case.

    • Terminating case: If n is 0 or 1, then return 1.
    • Recursive case: Return n times the function call for Factorial(n-1).

    lab_u08_Recursion_factorial.png


  • Graded program
    /
     home/
      school/
       english/
        essay1.txt
        essay2.txt
       compsci/
        homeworkA.cpp
        homeworkB.cpp
      projects/
       my_game/
        game.cpp
    
    Enter name of FILE or FOLDER to find, or QUIT to stop: game.cpp
    Path: /home/projects/my_game/game.cpp
    

    This program creates a representation of a filesystem on your computer. I've created a FilesystemNode object, which contains the following:

    enum class NodeType {
      FOLDER,
      FILE
    };
    
    struct FilesystemNode
    {
      FilesystemNode() { }
      FilesystemNode( string new_name, NodeType new_node_type )
      {
        name = new_name;
        node_type = new_node_type;
      }
    
      //! Name of this file/folder
      string name;
      //! Is this a folder or file?
      NodeType node_type;
      //! If this is a folder it may contain files and folders
      vector<shared_ptr<FilesystemNode>> contents;
    };
    

    Within Functions.cpp, a basic filesystem is created like what is shown in the example output above.

    The program displays the filesystem using a recursive function, void DisplayContents( shared_ptr<FilesystemNode> current_node, int tab ), which I've already implemented.

    The program will also allow the user to search the filesystem for a folder or file that matches the search text they enter. This function is also recursive, and you will be implementing it.

    string FindFile( shared_ptr<FilesystemNode> current_node, string find_me )

    Input parameter Description
    current_node Pointer to the current file/folder node we're looking at
    find_me Partial name of file/folder we're looking for

    Returns the path to the file/folder we're searching for, uses recursion.

    1. If the current node's name has a partial match with the find_me parameter, then RETURN this current node's name as the result. To do a partial string match:

      if ( current_node->name.find( find_me ) != string::npos )
      
    2. Afterwards, use a loop to iterate over all of the current node's contents. Within the loop: RECURSE to the FindFile function, passing in this CHILD node and the find_me data. STORE the result in a string variable. IF the result is NOT an empty string "", then we've found the file: Return current_node->name + result
    3. After the for loop, this means nothing was found in this recursive branch. In this case, just return an empty string "".

    Once finished, run the Automated tests to verify that your function works properly. You can also run the Program itself to test the functionality manually.


  • Turning in the assignment

    Screenshot: Before finishing up, run the automated tests and take a screenshot of all of your tests passing. Save the file somewhere as a .png.

    Back up your code: Open Git Bash in the base directory of your repository folder.

    1. Use git status to view all the changed items (in red).
    2. Use git add . to add all current changed items.
    3. Use git commit -m "INFO" to commit your changes, change "INFO" to a more descriptive message.
    4. Use git push -u origin BRANCHNAME to push your changes to the server.
    5. On GitLab.com, go to your repository webpage. There should be a button to Create a new Merge Request. You can leave the default settings and create the merge request.
    6. Copy the URL of your merge request.

    Turn in on Canvas:

    1. Find the assignment on Canvas and click "Start assignment".
    2. Select Upload file and upload your screenshot.
    3. Paste your GitLab URL into the Comments box.
    4. Click "Submit assignment" to finish.

πŸ‹οΈ Exercise - Debugging arrays and pointers (U08.EXE)

UNIT 09: Searching and sorting

πŸ“Š Presentation - Searching and sorting (U09.PRES.202401CS235)

πŸ“–οΈ Reading - Searching and sorting (U09.READ.202401CS235)

  • Introduction to searching and sorting

    reading_u09_SearchSort_algorithms.png

    Searching and sorting algorithms are things that we'll study in college, and perhaps see during job interviews, but for the most part in the daily life of an average developer we may not be implementing these algorithms from scratch.

    Like data structures, these algorithms have already been implemented and are available in various forms, usually already tested and optimized by someone else, and you can use them in your programs by adding in third party libraries.

    I'm just saying this because a some of this functionality of sorting algorithms may not make "intuitive" sense, and I find the code pretty ugly for a lot of these, but you won't have to worry about these in the real world.


  • Searching algorithms

    reading_u09_SearchSort_goose.png

    When we're working with structures of data - such as an array - we often need to find information stored within. Perhaps we know the element or a piece of data we want to find, but we need to get the positional index of where it's located in the array. This is where our search algorithms come in handy.

    • Linear search

      Pseudocode:

      LinearSearch (
          Inputs: array, findme
          Outputs: index of found item, or -1 if not found
        )
      
      begin function:
      
        for i begins at 0, loop while i < array size, update i++ each loop
          if array[i] matches findme, return i
        end for
      
        return -1
      
      end function
      

      When we're searching for items in an unsorted linear structure there's not much we can do to speed up the process. We can basically either start at the beginning and move forward checking each item in the structure for what you're looking for.

      We begin at the first index 0 and iterate until we hit the last index. Within the loop, if the element at index i matches what we're looking for, we return this index.

      If the loop completes and we haven't returned an index yet that means we've searched the entire structure and have not found the item. In this case, it is not in the structure and we can throw an exception to be dealt with elseware or return something like -1 to symbolize "no valid index".

      This search algorithm's growth rate is \(O(n)\) – the more items in the structure, the time linearly increases to search through it. Not much we can do about that, which is why we have different types of data structures that sort data as it is inserted - more on those later on.

      Additional documentation: https://en.wikipedia.org/wiki/Linear_search


    • Binary search

      Pseudocode:

      BinarySearch (
          Inputs: array, findme
          Outputs: index of found item, or -1 if not found
          Note: array MUST BE SORTED
        )
      
      begin function:
      
        left = 0
        right = array size - 1
      
        while left <= right:
          mid = left + ( right - left ) / 2
      
          if ( array[mid] == findme ): return mid
          else if ( array[mid] < findme): left = mid+1;
          else if ( array[mid] > findme): right = mid-1;
        end while
      
        return -1
      
      end function
      

      A binary search allows for a faster search - if the array passed in is sorted (otherwise it won't work!). The binary search cuts its search range in half each iteration through the loop, thus giving us a \(log(n)\) growth rate.

      Additional documentation: https://en.wikipedia.org/wiki/Binary_search_algorithm


  • Sorting algorithms
    • Selection sort

      Pseudocode:

      SelectionSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        for i begins at 0, loop while i < array size - 1, update i++ each loop:
          minindex = i
      
          for j begins at i+1, loop while j < array size, update j++ each loop:
            if ( array[j] < array[minindex] ): minindex = j
          end for
      
          if ( minindex != i ): swap( array[i], array[minindex] )
      
        end for
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Selection_sort


    • Bubble sort

      Pseudocode:

      BubbleSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        for i begins at 0, loop while i < array size - 1, update i++ each loop:
      
          for j begins at 0, loop while j < array size - i - 1, update j++ each loop:
            if ( array[j] > array[j+1] ): swap( array[j], array[j+1] )
          end for
      
        end for
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Bubble_sort


    • Insertion sort

      Pseudocode:

      InsertionSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        i = 1
        while ( i < array size ):
          j = i
      
          while ( j > 0 and array[j-1] > array[j] ):
            swap( array[j], array[j-1] )
            j--;
          end while
      
          i++;
        end while
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Insertion_sort


    • Quick sort

      Pseudocode:

      Partition (
          Inputs: array, lowindex, highindex
          Outputs: an index value
        )
      
      begin function:
      
        pivotvalue = array[high]
        i = low - 1
      
        for j begins at low, loop while j <= high - 1, update j++ each loop:
          if ( array[j] <= pivotvalue ) then:
            i++
            swap( array[i], array[j] )
          end if
        end for
      
        swap( array[i+1], array[high] )
        return i+1
      
      end function
      
      ------------------------------------------------------
      
      QuickSort_Recursive (
          Inputs: array, lowindex, highindex
          Outputs: void
        )
      
      begin function:
      
        if ( low < high ) then:
          partitionindex = Partition( array, low, high )
          QuickSort_Recursive( array, low, partitionindex - 1 )
          QuickSort_Recursive( array, partitionindex + 1, high )
        end if
      
      end function
      
      ------------------------------------------------------
      
      QuickSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        QuickSort_Recursive( array, 0, array size - 1 )
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Quicksort


    • Merge sort

      Pseudocode: (Adapted from https://www.programiz.com/dsa/merge-sort)

      MergeParts (
          Inputs: array, left, mid, right
          Outputs: void
        )
      
      begin function:
        n1 = mid - left + 1
        n2 = right - mid
      
        leftarr is a new array
        rightarr is a new array
      
        for i begins at 0, loops while i < n1, updates i++ each loop:
          leftarr push array[ left + i ]
        end for
      
        for j begins at 0, loops while j < n2, updates j++ each loop:
          rightarr push array[ mid + 1 + j ]
        end for
      
        i = 0
        j = 0
        k = left
      
        while ( i < n1 and j < n2 ):
          if ( leftarr[i] <= rightarr[j] ) then:
            array[k] = leftarr[i]
            i++
          else:
            array[k] = rightarr[j]
            j++
          end if
      
          k++
        end while
      
        while ( i < n1 ):
          array[k] = leftarr[i]
          i++
          k++
        end while
      
        while ( j < n2 ):
          array[k] = rightarr[j]
          j++
          k++
        end while
      
      
      end function
      
      ------------------------------------------------------
      
      MergeSort_Recursive (
          Inputs: array, left, right
          Outputs: void
        )
      
      begin function:
      
        if ( left < right ) then:
          mid = floor( left + right ) / 2
      
          MergeSort_Recursive( array, left, mid )
          MergeSort_Recursive( array, mid+1, right )
          Merge( array, left, mid, right )
        end if
      
      end function
      
      ------------------------------------------------------
      
      MergeSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        MergeSort_Recursive( array, 0, array size - 1 )
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Merge_sort


    • Heap sort

      Pseudocode: (Adapted from https://en.wikipedia.org/wiki/Heapsort)

      LeftChildIndex (
          Inputs: index
          Outputs: where this index's left child is
        )
      
      begin function:
        return 2 * index + 1
      end function
      
      ------------------------------------------------------
      
      RightChildIndex (
          Inputs: index
          Outputs: where this index's right child is
        )
      
      begin function:
        return 2 * index + 2
      end function
      
      ------------------------------------------------------
      
      ParentIndex (
          Inputs: index
          Outputs: where this index's parent is
        )
      
      begin function:
        return floor( ( index - 1 ) / 2 )
      end function
      
      ------------------------------------------------------
      
      SiftDown (
          Inputs: array, root, end
          Outputs: void
        )
      
      begin function:
      
        while ( LeftChildIndex( root ) < end ):
      
          child = LeftChildIndex( root )
          if ( child + 1 < end and array[child] < array[child+1] ): child++
      
          if ( array[root] < array[child] ) then:
            swap( array[root], array[child] )
            root = child
          else:
            return
          end if
      
        end while
      
      end function
      
      ------------------------------------------------------
      
      Heapify (
          Inputs: array, end
          Outputs: void
        )
      
      begin function:
      
        start = ParentIndexx( end-1 ) + 1
      
        while ( start > 0 ):
          start--
          SiftDown( array, start, end )
        end while
      
      end function
      
      ------------------------------------------------------
      
      HeapSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        Heapify( array, array size )
        end = array size
      
        while ( end > 1 ):
          end--
          swap( array[end], array[0] )
          SiftDown( array, 0, end )
        end while
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Heapsort


    • Radix sort

      Pseudocode: (Adapted from https://www.geeksforgeeks.org/radix-sort/)

      CountSort (
          Inputs: array, n, exp
          Outputs: void
        )
      
      begin function:
        output is an int array of size n (create as dynamic array)
        count is an int array of size 10
        count[0] = 0
      
        for i starts at 0, loops while i < n, updates i++ each loop:
          count[ ( array[i] / exp ) % 10 ]++
        end for
      
        for i starts at 1, loops while i < 10, updates i++ each loop:
          count[i] += count[i-1]
        end for
      
        for i starts at n-1, loops while i >= 0, updates i-- each loop:
          output[ count[ ( array[i] / exp ) % 10 ] - 1 ] = array[i]
          count[ ( array[i] / exp ) % 10 ]--
        end for
      
        for i starts at 0, loops while i < n, updates i++ each loop:
          array[i] = output[i]
        end for
      end function
      
      ------------------------------------------------------
      
      GetMax (
          Inputs: array
          Outputs: the value of the largest item in the array
        )
      
      begin function:
      
        maxvalue = array[0]
      
        for i starts at 1, loops while i < array size, updates i++ each loop:
          if ( array[i] > maxvalue ): maxvalue = array[i]
        end for
      
        return maxvalue
      
      end function
      
      ------------------------------------------------------
      
      RadixSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        maxvalue = GetMax( array )
      
        for exp starts at 1, loop while maxvalue / exp > 0, update exp *= 10 each loop:
          CountSort( array, array size, exp )
        end for
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Radix_sort

πŸ§‘β€πŸ”¬ Lab - Searching and sorting (U09.LAB)

pixel-goals.png Goals:

  • Practice with the searching algorithm: Linear Search
  • Practice with the sorting algorithms: Selection Sort, Bubble Sort, Insertion Sort
  • Implement the searching algorithm: Binary Search
  • Implement the sorting algorithms: Heap Sort, Merge Sort, Quick Sort, Radix Sort
  • Compare speeds of each algorithm

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-practice.png Practice programs and graded program:

  • This assignment contains several "practice" program folders, and a "graded" program folder. Only the "graded" program is required.
  • The practice programs are there to help you practice with the topics before tackling the larger graded program. If you feel confident in the topic already, you may go ahead and work on the graded program. If you feel like you need additional practice first, that's what the practice assignments are for.
  • The graded program assignment contains unit tests to help verify your code.
  • You can score up to 100% turning in just the graded program assignment.
  • You can turn in the practice assignments for extra credit points.

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.
  • If you're stuck on a practice program, try working on a different one and come back to it later.
  • If you're stuck on the graded program, try the practice programs first.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Setup: Starter code and new branch

    For this assignment, I've already added the code to your repository.

    1. Pull the starter code:
      1. Check out the main branch: git checkout main
      2. Pull latest: git pull
    2. Create a new branch to start working from: git checkout -b BRANCHNAME

    The u09_SearchingSorting folder contains the following items:

    .
    ├── graded_program
    │   ├── Functions.cpp
    │   ├── Functions.h
    │   ├── INFO.h
    │   ├── main.cpp
    │   ├── Project_CodeBlocks
    │   │   ├── SearchSortProject.cbp
    │   │   ├── SearchSortProject.depend
    │   │   ├── SearchSortProject_exe
    │   │   └── SearchSortProject.layout
    │   ├── Project_Makefile
    │   │   └── Makefile
    │   ├── Searching
    │   │   ├── BinarySearch.cpp
    │   │   └── LinearSearch.cpp
    │   ├── SearchSortProject
    │   │   ├── SearchSortProject.sln
    │   │   ├── SearchSortProject.vcxproj
    │   │   └── SearchSortProject.vcxproj.filters
    │   └── Sorting
    │       ├── HeapSort.cpp
    │       ├── MergeSort.cpp
    │       ├── QuickSort.cpp
    │       ├── RadixSort.cpp
    │       └── SelectionSort.cpp
    ├── practice1_linearsearch
    │   └── linearsearch.cpp
    ├── practice2_selectionsort
    │   └── selectionsort.cpp
    ├── practice3_bubblesort
    │   └── bubblesort.cpp
    └── practice4_insertionsort
        └── insertionsort.cpp
    

  • practice1_linearsearch
    MANUAL TESTS:
     Array:	{ 0="ice cream", 1="cake", 2="cookies", 3="pie" }
     Search for "cookies":  2
     Search for "rasgulla": -1
    
    
    AUTOMATED TESTS:
    [PASS]  TEST 1, LinearSearch( { 0="dog", 1="rat", 2="cat" }, "cat" ) = 2
    [PASS]  TEST 2, LinearSearch( { 0="Anuj", 1="Rai", 2="Asha" }, "Rai" ) = 1
    [PASS]  TEST 3, LinearSearch( { 0="One", 1="Two", 2="Three" }, "Four" ) = -1
    

    For this practice you'll implement the LinearSearch function.

    • Inputs: This function takes in arr, a vector of strings to search through,

    and find_me, the item we're looking for.

    • Output: Returns the index of the found item, or -1 if not found.

    A linear search is a simple search. Use a loop to iterate over all the letters of the arr vector. For each element, check to see if arr[i] is equal to find_me. If it matches, return i, the index it was found at. (No ELSE case is needed INSIDE the for loop!)

    If the for loop finishes, that means we searched the entire vector and no match was found. In this case, before the function ends, return -1 to represent "not found".


  • practice2_selectionsort
    
    
    MANUAL TESTS:
     Original:	{ 0="ice cream", 1="cake", 2="cookies", 3="pie" }
    
     After sort:	{ 0="cake", 1="cookies", 2="ice cream", 3="pie" }
    
    
    AUTOMATED TESTS:
    [PASS]  TEST 1, SelectionSort( { 0="c", 1="a", 2="t" } ) =
      { 0="a", 1="c", 2="t" }
    [PASS]  TEST 2, SelectionSort( { 0="h", 1="e", 2="l", 3="l", 4="o" } ) =
      { 0="e", 1="h", 2="l", 3="l", 4="o" }
    

    Additional information: https://en.wikipedia.org/wiki/Selection_sort#Implementations

    Pseudocode:

    SelectionSort (
        Inputs: array
        Outputs: void
      )
    
    begin function:
    
      for i begins at 0, loop while i < array size - 1, update i++ each loop:
        minindex = i
    
        for j begins at i+1, loop while j < array size, update j++ each loop:
          if ( array[j] < array[minindex] ): minindex = j
        end for
    
        if ( minindex != i ): swap( array[i], array[minindex] )
    
      end for
    
    end function
    

    Translate the pseudocode into C++. You can also view other implementations of each sorting algorithm for reference.


  • practice3_bubblesort
    MANUAL TESTS:
     Original:	{ 0="ice cream", 1="cake", 2="cookies", 3="pie" }
    
     After sort:	{ 0="cake", 1="cookies", 2="ice cream", 3="pie" }
    
    
    AUTOMATED TESTS:
    [PASS]  TEST 1, BubbleSort( { 0="c", 1="a", 2="t" } ) =
      { 0="a", 1="c", 2="t" }
    [PASS]  TEST 2, BubbleSort( { 0="h", 1="e", 2="l", 3="l", 4="o" } ) =
      { 0="e", 1="h", 2="l", 3="l", 4="o" }
    

    Additional information: https://en.wikipedia.org/wiki/Bubble_sort

    Pseudocode:

    BubbleSort (
        Inputs: array
        Outputs: void
      )
    
    begin function:
    
      for i begins at 0, loop while i < array size - 1, update i++ each loop:
    
        for j begins at 0, loop while j < array size - i - 1, update j++ each loop:
          if ( array[j] > array[j+1] ): swap( array[j], array[j+1] )
        end for
    
      end for
    
    end function
    

  • practice4_insertionsort
    MANUAL TESTS:
     Original:	{ 0="ice cream", 1="cake", 2="cookies", 3="pie" }
    
     After sort:	{ 0="cake", 1="cookies", 2="ice cream", 3="pie" }
    
    
    AUTOMATED TESTS:
    [PASS]  TEST 1, InsertionSort( { 0="c", 1="a", 2="t" } ) =
      { 0="a", 1="c", 2="t" }
    [PASS]  TEST 2, InsertionSort( { 0="h", 1="e", 2="l", 3="l", 4="o" } ) =
      { 0="e", 1="h", 2="l", 3="l", 4="o" }
    

    Additional information: https://en.wikipedia.org/wiki/Insertion_sort

    Pseudocode:

    InsertionSort (
        Inputs: array
        Outputs: void
      )
    
    begin function:
    
      i = 1
      while ( i < array size ):
        j = i
    
        while ( j > 0 and array[j-1] > array[j] ):
          swap( array[j], array[j-1] )
          j--;
        end while
    
        i++;
      end while
    
    end function
    

  • Graded program
    2. Run PROGRAMS
    >> 2
    Generating 100000 pieces of data... 4 milliseconds
    
    Running Merge sort, please wait...
    Completed in 114 milliseconds
    
    Running Quick sort, please wait...
    Completed in 22 milliseconds
    
    Running Radix sort, please wait...
    Completed in 13 milliseconds
    
    Running Heap sort, please wait...
    Completed in 43 milliseconds
    
    Running Selection sort, please wait...
    Completed in 25686 milliseconds
    
    Running Linear search, please wait...
    Completed in 0 milliseconds
    
    Running Binary search, please wait...
    Completed in 0 milliseconds
    

    This program creates a bunch of data to sort and running it will show you how long each algorithm takes to complete. Every computer will have different results. You can also use the automated tests to verify your function implementations as well.

    You'll be editing the following files:

    • Sorting/
      • HeapSort.cpp
      • MergeSort.cpp
      • QuickSort.cpp
      • RadixSort.cpp
    • Searching/
      • BinarySearch.cpp

    Some sorting algorithms contain multiple functions for different steps of the process. Pseudocode will be provided for each.

    (You do NOT need to have these algorithms memorized or really to understand them. You might encounter them later on in other bachelors level CS classes so it's good to be familiar with them. You can look these algorithms up anywhere, it's not like they're secret knowledge.)


    • QuickSort

      Pseudocode:

      Partition (
          Inputs: array, lowindex, highindex
          Outputs: an index value
        )
      
      begin function:
      
        pivotvalue = array[high]
        i = low - 1
      
        for j begins at low, loop while j <= high-1, update j++ each loop:
          if ( array[j] <= pivotvalue ) then:
            i++
            swap( array[i], array[j] )
          end if
        end for
      
        swap( array[i+1], array[high] )
        return i+1
      
      end function
      
      ------------------------------------------------------
      
      QuickSort_Recursive (
          Inputs: array, lowindex, highindex
          Outputs: void
        )
      
      begin function:
      
        if ( low < high ) then:
          partitionindex = Partition( array, low, high )
          QuickSort_Recursive( array, low, partitionindex - 1 )
          QuickSort_Recursive( array, partitionindex + 1, high )
        end if
      
      end function
      
      ------------------------------------------------------
      
      QuickSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        QuickSort_Recursive( array, 0, array size - 1 )
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Quicksort


    • MergeSort

      Pseudocode: (Adapted from https://www.programiz.com/dsa/merge-sort)

      MergeParts (
          Inputs: array, left, mid, right
          Outputs: void
        )
      
      begin function:
        n1 = mid - left + 1
        n2 = right - mid
      
        leftarr is a new array
        rightarr is a new array
      
        for i begins at 0, loops while i < n1, updates i++ each loop:
          leftarr push array[ left + i ]
        end for
      
        for j begins at 0, loops while j < n2, updates j++ each loop:
          rightarr push array[ mid + 1 + j ]
        end for
      
        i = 0
        j = 0
        k = left
      
        while ( i < n1 and j < n2 ):
          if ( leftarr[i] <= rightarr[j] ) then:
            array[k] = leftarr[i]
            i++
          else:
            array[k] = rightarr[j]
            j++
          end if
      
          k++
        end while
      
        while ( i < n1 ):
          array[k] = leftarr[i]
          i++
          k++
        end while
      
        while ( j < n2 ):
          array[k] = rightarr[j]
          j++
          k++
        end while
      
      
      end function
      
      ------------------------------------------------------
      
      MergeSort_Recursive (
          Inputs: array, left, right
          Outputs: void
        )
      
      begin function:
      
        if ( left < right ) then:
          mid = floor( left + right ) / 2
      
          MergeSort_Recursive( array, left, mid )
          MergeSort_Recursive( array, mid+1, right )
          Merge( array, left, mid, right )
        end if
      
      end function
      
      ------------------------------------------------------
      
      MergeSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        MergeSort_Recursive( array, 0, array size - 1 )
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Merge_sort


    • HeapSort

      Pseudocode: (Adapted from https://en.wikipedia.org/wiki/Heapsort)

      LeftChildIndex (
          Inputs: index
          Outputs: where this index's left child is
        )
      
      begin function:
        return 2 * index + 1
      end function
      
      ------------------------------------------------------
      
      RightChildIndex (
          Inputs: index
          Outputs: where this index's right child is
        )
      
      begin function:
        return 2 * index + 2
      end function
      
      ------------------------------------------------------
      
      ParentIndex (
          Inputs: index
          Outputs: where this index's parent is
        )
      
      begin function:
        return floor( ( index - 1 ) / 2 )
      end function
      
      ------------------------------------------------------
      
      SiftDown (
          Inputs: array, root, end
          Outputs: void
        )
      
      begin function:
      
        while ( LeftChildIndex( root ) < end ):
      
          child = LeftChildIndex( root )
          if ( child + 1 < end and array[child] < array[child+1] ): child++
      
          if ( array[root] < array[child] ) then:
            swap( array[root], array[child] )
            root = child
          else:
            return
          end if
      
        end while
      
      end function
      
      ------------------------------------------------------
      
      Heapify (
          Inputs: array, end
          Outputs: void
        )
      
      begin function:
      
        start = ParentIndexx( end-1 ) + 1
      
        while ( start > 0 ):
          start--
          SiftDown( array, start, end )
        end while
      
      end function
      
      ------------------------------------------------------
      
      HeapSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        Heapify( array, array size )
        end = array size
      
        while ( end > 1 ):
          end--
          swap( array[end], array[0] )
          SiftDown( array, 0, end )
        end while
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Heapsort


    • RadixSort

      Radix sort

      Pseudocode: (Adapted from https://www.geeksforgeeks.org/radix-sort/)

      CountSort (
          Inputs: array, n, exp
          Outputs: void
        )
      
      begin function:
        output is an int array of size n (might need to be a dynamic array)
        count is an int array of size 10
        count[0] = 0
      
        for i starts at 0, loops while i < n, updates i++ each loop:
          count[ ( array[i] / exp ) % 10 ]++
        end for
      
        for i starts at 1, loops while i < 10, updates i++ each loop:
          count[i] += count[i-1]
        end for
      
        for i starts at n-1, loops while i >= 0, updates i-- each loop:
          output[ count[ ( array[i] / exp ) % 10 ] - 1 ] = array[i]
          count[ ( array[i] / exp ) % 10 ]--
        end for
      
        for i starts at 0, loops while i < n, updates i++ each loop:
          array[i] = output[i]
        end for
      end function
      
      ------------------------------------------------------
      
      GetMax (
          Inputs: array
          Outputs: the value of the largest item in the array
        )
      
      begin function:
      
        maxvalue = array[0]
      
        for i starts at 1, loops while i < array size, updates i++ each loop:
          if ( array[i] > maxvalue ): maxvalue = array[i]
        end for
      
        return maxvalue
      
      end function
      
      ------------------------------------------------------
      
      RadixSort (
          Inputs: array
          Outputs: void
        )
      
      begin function:
      
        maxvalue = GetMax( array )
      
        for exp starts at 1, loop while maxvalue / exp > 0, update exp *= 10 each loop:
          CountSort( array, array size, exp )
        end for
      
      end function
      

      Additional documentation: https://en.wikipedia.org/wiki/Radix_sort


  • Turning in the assignment

    Screenshot: Before finishing up, run the automated tests and take a screenshot of all of your tests passing. Save the file somewhere as a .png.

    Back up your code: Open Git Bash in the base directory of your repository folder.

    1. Use git status to view all the changed items (in red).
    2. Use git add . to add all current changed items.
    3. Use git commit -m "INFO" to commit your changes, change "INFO" to a more descriptive message.
    4. Use git push -u origin BRANCHNAME to push your changes to the server.
    5. On GitLab.com, go to your repository webpage. There should be a button to Create a new Merge Request. You can leave the default settings and create the merge request.
    6. Copy the URL of your merge request.

    Turn in on Canvas:

    1. Find the assignment on Canvas and click "Start assignment".
    2. Select Upload file and upload your screenshot.
    3. Paste your GitLab URL into the Comments box.
    4. Click "Submit assignment" to finish.

🧠 Tech Literacy - Problem solving (U09.TEC)

Programming can be confusing. We have to worry about design, implementation, debugging, and maintaining code over an extended period of time. No matter what skill level you are at programming, building up your problem solving skills will help you immensely.

Here are a few ways that I approach challenges to help me figure out a solution:

  1. Sketch it on paper - Diagramming parts of a program, or writing down data, or otherwise organizing my thoughts on paper help me visualize the problem. It can also be helpful to step through the code and writing down the values of each variable on paper as you go, to help you figure out the program flow and variable values.
  2. Ask myself "What is the program doing that it shouldn't be doing… or… what is the program not doing that I want it to do?" - Being able to define what exactly is wrong with your program will help you in solving it, or at least talking to someone else to get ideas for how to solve it.
  3. Rubber duck debugging - This is a technique where you talk through the issue with an object, a pet, or another person. Sometimes, just talking through the problem can help you figure out a solution.
  4. Step away - When you feel like you're hitting your head against a wall and just can't think through anything it is time to step away. Your brain is done and you won't come up with any solutions, but even a half hour break can help you come back with a new perspective. Don't try to force yourself through this kind of roadblock - give yourself a break.
  5. Write out the steps in pseudocode - When trying to design something, I will usually write down the items it needs to accomplish as pseudocode, or as a series of function calls or actions that need to occur. Implementation details can come later once I know where I need to go.

Locate the 🧠 Tech Literacy - Problem solving assignment and add to the discussion!

Assignment link: https://canvas.jccc.edu/courses/68320/modules/items/3911082

WEEK 5 - FEB 12

UNIT 10: Templates

πŸ“–οΈ Reading - Templates (U10.READ)

  • Before Templates

    Templates don't exist in C++'s precursor, C. Because of this, if you had a function like - for example - SumTwoNumbers

    • that you wanted to work with different data types, you would have to define different functions for each version. C also doesn't have function overloading, so they would have to have different names as well.

    As a real-world example, OpenGL is a cross-platform graphics library that can be used to create 3D graphics. OpenGL was written in C, and you could tell it a set of vertices to draw in order to create one polygon or quad or other shape.

    There were different functions you could use to define points (vertices) in a shape, like:

    • glVertex2f( 0, 0 );
    • glVertex3f( 0, 0, 0 );

    And, in particular, there are a bunch of "glVertex" functions: glVertex2d, glVertex2dv, glVertex2f, glVertex2fv, glVertex2i, and so on… (Don't you wish you were programming in C?)

    reading_u10_Templates_ogl.png


  • What are Templates?

    With C++ and other languages like C# and Java, we can now use Templates with our functions and classes. A Template allows us to specify a placeholder for a data type which will be filled in later.

    In the C++ Standard Template Library, there are objects like the vector that is essentially a dynamic array, but it can store any data type - we just have to tell it what it's storing when we declare a vector object:

    vector<int> listOfQuantities;
    vector<float> listOfPrices;
    vector<string> listOfNames;
    

    We can also define our own functions and even classes with templated functions and member variables ourselves, leading to much more reusable code.


    • Templated functions

      We can write a standalone function with templated parameters or a templated return type or both. For example, here's a simple function to add two items together:

      template <typename T>
      T Sum( T numA, T numB )
      {
          return numA + numB;
      }
      

      This function can be called with any data type, so long as the data type has the + operator defined for it - so, if it were a custom class you wrote, you would have to overload the operator+ function.

      What this means is that we can call Sum with integers and floats, but also with someting like a string, since strings use the + operator to combine two strings together.

      Calling the templated function:
      int main()
      {
          int intA = 4, intB = 6;
          float floatA = 3.9, floatB = 2.5;
          string strA = "alpha", strB = "bet";
      
          cout << intA << " + " << intB
              << " = " << Sum( intA, intB ) << endl;
      
          cout << floatA << " + " << floatB
              << " = " << Sum( floatA, floatB ) << endl;
      
          cout << strA << " + " << strB
              << " = " << Sum( strA, strB ) << endl;
      }
      
      Program output:
      4 + 6 = 10
      3.9 + 2.5 = 6.4
      alpha + bet = alphabet
      

    • Templated classes

      More frequently, you will be using templates to create classes for data structures that can store any kind of data. The C++ Standard Template Library has data structures like vector, list, and map, but we can also write our own.

      When creating our templated class, there are a few things to keep in mind:

      1. We need to use template <typename T> at the beginning of the class declaration.
      2. Method definitions must be in the header file - in this case, we won't be putting the method definitions in a separate .cpp file. You can either define the functions inside the class declaration, or immediately after it.
      3. Method definitions also need to be prefixed with template <typename T>.

      If you try to create a "TemplatedArray.h" file and a "TemplatedArray.cpp" file and put your method definitions in the .cpp file, then you're going to get compile errors:

      reading_u10_Templates_undefined-reference.png

      You might think, "Well, that's weird." - and yes, it is. C++ is a strange language with weird behaviors. In this case in particular, you can read about why this is for templates here: https://isocpp.org/wiki/faq/templates%5C#templates-defn-vs-decl

      In short, the template command is used to generate classes, and while our class declaration looks normal, this is actually special code that is just telling the compiler how it's going to generate a family of classes. Because of this, the compiler needs to see the function definitions as well.


  • Example TemplatedArray (Full):

    This is all in one file - TemplatedArray.hpp. I have the class declaration on top, with all the definitions below.

    #ifndef _TEMPLATED_ARRAY
    #define _TEMPLATED_ARRAY
    
    #include <stdexcept>
    using namespace std;
    
    template <typename T>
    class TemplatedArray
    {
    public:
      TemplatedArray();
      TemplatedArray( int size );
      ~TemplatedArray();
    
      void PushToBack( T item );
      void RemoveFromBack();
    
      bool IsFull();
      bool IsEmpty();
    
      void Display();
      int Size();
    
    private:
      void AllocateMemory( int size );
      void DeallocateMemory();
    
      int m_arraySize;
      int m_storedItems;
      T* m_array;
    };
    
    // Constructors/Destructor
    template <typename T>
    TemplatedArray<T>::TemplatedArray()
    {
      m_arraySize = 0;
      m_storedItems = 0;
      // Be safe: Initialize pointers to nullptr.
      m_array = nullptr;
    }
    
    template <typename T>
    TemplatedArray<T>::TemplatedArray( int size )
    {
      m_array = nullptr;
      AllocateMemory( size );
    }
    
    template <typename T>
    TemplatedArray<T>::~TemplatedArray()
    {
      DeallocateMemory();
    }
    
    // Other functionality
    template <typename T>
    void TemplatedArray<T>::PushToBack( T item )
    {
      if ( IsFull() )
        {
          throw runtime_error( "Array is full!" );
        }
      if ( m_array == nullptr )
        {
          AllocateMemory( 10 );
        }
    
      m_array[ m_storedItems ] = item;
      m_storedItems++;
    }
    
    template <typename T>
    void TemplatedArray<T>::RemoveFromBack()
    {
      if ( IsEmpty() )
        {
          throw runtime_error( "Array is empty!" );
        }
    
      // Lazy deletion
      m_storedItems--;
    }
    
    template <typename T>
    bool TemplatedArray<T>::IsFull()
    {
      return ( m_arraySize == m_storedItems );
    }
    
    template <typename T>
    bool TemplatedArray<T>::IsEmpty()
    {
      return ( m_storedItems == 0 );
    }
    
    template <typename T>
    void TemplatedArray<T>::Display()
    {
      for ( int i = 0; i < m_storedItems; i++ )
        {
          cout << i << ". " << m_array[i] << endl;
        }
    }
    
    template <typename T>
    int TemplatedArray<T>::Size()
    {
      return m_storedItems;
    }
    
    // Private methods
    template <typename T>
    void TemplatedArray<T>::AllocateMemory( int size )
    {
      // Clear out any memory currently stored
      DeallocateMemory();
    
      m_array = new T[ size ];
      m_arraySize = size;
      m_storedItems = 0;
    }
    
    template <typename T>
    void TemplatedArray<T>::DeallocateMemory()
    {
      // Free the memory allocated
      if ( m_array != nullptr )
        {
          delete [] m_array;
          m_array = nullptr;
          m_arraySize = 0;
          m_storedItems = 0;
        }
    }
    #endif
    

    This basic templated data structure is now ready to store any kind of data. In this case, the only requirement is that the object being stored has the ostream<< operator function overloaded, since it is used in the Display() function.

    Using the TemplatedArray
    Within main(), we can then use this templated array to store any kind of data:
    #include "TemplatedArray.hpp"
    
    #include <iostream>
    using namespace std;
    
    int main()
    {
        TemplatedArray<string> myList( 10 );
    
        myList.PushToBack( "cat" );
        myList.PushToBack( "rat" );
        myList.PushToBack( "bat" );
    
        myList.Display();
    
        myList.RemoveFromBack();
        myList.Display();
    
        return 0;
    }
    

  • Review questions:
    1. A templated standalone function that uses the template placeholder type T as a parameter looks like…
    2. The declaration of a templated class looks like…
    3. The definition of a templated class' member function looks like…
    4. Declaring an object whose data type is a templated class will look like…

πŸ”Ž Concept Intro - Templates (U10.CIN)

UNIT 11: Exceptions

πŸ“–οΈ Reading - Exceptions (U11.READ)

reading_u11_Exceptions_exceptions.png

  • Writing robust software

    As a software developer, you will often be writing software that other developers will be using. This could be other developers at the company working on the same software product, or perhaps you might write a library that gets licensed out to other companies, or anything else. Your code will need to have checks in place for errors and be able to manage those errors gracefully, allowing the software to continue running instead of letting the program crash and restart.

    A long time ago, lots of developers used numeric error codes to track errors. If you've ever seen something like "Error 12943" with no other useful information, that is an example of these error codes - useless for the end-user, but meant so that the programmer can search the code for that number and find where it broke. This is also why we use return 0 at the end of our C++ programs - technically, you could return anything else, but returning 0 is meant to show that there were no errors. If you ran into an error, you /could return 1 or 2 or 3 instead to mark errors.

    Modern languages have exception handling built in, usually working with a try/catch style. You write in logic to check for errors, and when you find a problem you throw an exception, and that exception can be caught elseware in the code.

    What kind of errors can we listen for?

    • Trying to open a file that doesn't exist
    • Memory access violations
    • Invalid math (dividing by 0)
    • Not enough memory
    • Receiving unexpected inputs
    • Trying to delete from an empty data structure
    • Problem converting one data type to another

  • The C++ Exception object

    C++ has an exception family of objects that we can use when trying to classify what kind of exception has happened. If none of the existing exception objects is appropriate, you can also create your own exception type.

    You can find documentation for the exception object here: http://www.cplusplus.com/reference/exception/exception/

    Exceptions: (From http://www.cplusplus.com/reference/exception/exception/)

    Exception class Description
    bad_alloc Exception thrown on failure allocating memory
    bad_cast Exception thrown on failure to dynamic cast
    bad_exception Exception thrown by unexpected handler
    bad_function_call Exception thrown on bad call
    bad_typeid Exception thrown on typeid of null pointer
    bad_weak_ptr Bad weak pointer
    ios_base::failure Base class for stream exceptions
    logic_error Logic error exception
    runtime_error Runtime error exception
    domain_error Domain error exception
    future_error Future error exception
    invalid_argument Invalid argument exception
    length_error Length error exception
    out_of_range Out-of-range exception
    overflow_error Overflow error exception
    range_error Range error exception
    system_error System error exception\
    underflow_error Underflow error exception
    bad_array_new_length Exception on bad array length

    • Detecting errors and throwing exceptions

      The first step of dealing with exceptions is identifying a place where an error may occur - such as a place where we might end up dividing by zero. We write an if statement to check for the error case, and then throw an exception. We choose an exception type and we can also pass an error message as a string.

      int ShareCookies( int cookies, int kids )
      {
        if ( kids == 0 )
        {
          throw runtime_error( "Cannot divide by zero!" );
        }
      
        return cookies / kids;
      }
      
      • Listening for exceptions with try

        Now we know that the function ShareCookies could possibly throw an exception. Any time we call that function, we need to listen for any thrown exceptions by using try.

        try
        {
          // Calling the function
          cookiesPerKid = ShareCookies( c, k );
        }
        
      • Dealing with exceptions with catch

        Immediately following the try, we can write one or more catch blocks for different types of exceptions and then decide how we want to handle it.

        try
        {
          // Calling the function
          cookiesPerKid = ShareCookies( c, k );
        }
        catch( runtime_error ex )
        {
          // Display the error message
          cout << "Error: " << ex.what() << endl;
        
          // Handling it by setting a default value
          cookiesPerKid = 0;
        }
        
        cout << "The kids get " << cookiesPerKid << " each" << endl;
        

        A function could possibly throw different types of exceptions for different errors, and you can have multiple catch blocks for each type.

        Handling the error: Once you catch an error, it's up to you to decide what to do with it. For example, you could…

        • Ignore the error and just keep going
        • Write some different logic for a "plan B" backup
        • End the program because now the data is invalid

        Coding with others' code: While writing software and utilizing others' code, you will want to pay attention to which functions you're calling that could throw exceptions. Often code libraries will contain documentation that specify possible exceptions thrown.


    • Common error in student implementations

      reading_u11_Exceptions_trycatch.png

      A common error I see students make is to put the try, catch, and throw statements all in one function. This defeats the point of even using exceptions.

      Don't do this:

      float Divide( float num, float denom )
      {
        try
        {
          if ( denom == 0 )
          {
            throw Exception;
          }
      
          return num / denom;
        }
        catch( Exception& ex )
        {
          // ...
        }
      }
      

      Remember that the throw belongs within a function that could cause an error, and the try/catch belongs with the location that calls the potentially exception-causing function.

      // FUNCTION THAT COULD CAUSE ERROR - Responsible for THROW
      float Divide( float num, float denom )
      {
        if ( denom == 0 )
        {
          throw Exception;
        }
      
        return num / denom;
      }
      
      
      int main()
      {
        float n, d;
        cout << "Enter num: ";
        cin >> n;
      
        cout << "Enter denom: ";
        cin >> d;
      
        float result = 0;
        try
        {
          // CALLING "IFFY" FUNCTION - Wrap the CALL in try/catch
          result = Divide( n, d );
        }
        catch( Exception& ex )
        {
          cout << "ERROR OCCURRED!" << endl;
          return 1234;
        }
      
        cout << "Result: " << result << endl;
      
        return 0;
      }
      

  • Review questions:
    1. When is the throw keyword used?
    2. When is the catch keyword used?
    3. When is the try keyword used?

πŸ”Ž Unit 11 Concept Intro - Exceptions (U11.CIN)

πŸ§‘β€πŸ”¬ Lab - Templates and exceptions (U11.LAB)

pixel-goals.png Goals:

  • Practice with the searching algorithm: Linear Search
  • Practice with the sorting algorithms: Selection Sort, Bubble Sort, Insertion Sort
  • Implement the searching algorithm: Binary Search
  • Implement the sorting algorithms: Heap Sort, Merge Sort, Quick Sort, Radix Sort
  • Compare speeds of each algorithm

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-practice.png Practice programs and graded program:

  • This assignment contains several "practice" program folders, and a "graded" program folder. Only the "graded" program is required.
  • The practice programs are there to help you practice with the topics before tackling the larger graded program. If you feel confident in the topic already, you may go ahead and work on the graded program. If you feel like you need additional practice first, that's what the practice assignments are for.
  • The graded program assignment contains unit tests to help verify your code.
  • You can score up to 100% turning in just the graded program assignment.
  • You can turn in the practice assignments for extra credit points.

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.
  • If you're stuck on a practice program, try working on a different one and come back to it later.
  • If you're stuck on the graded program, try the practice programs first.

pixel-dual.png Dual enrollment: If you are in both my CS 235 and CS 250 this semester, these instructions are the same. You only need to implement it once and upload it to one repository, then turn in the link for the one merge request. Please turn in on both Canvas assignments so they're marked done.


  • Setup: Starter code and new branch

    For this assignment, I've already added the code to your repository.

    1. Pull the starter code:
      1. Check out the main branch: git checkout main
      2. Pull latest: git pull
    2. Create a new branch to start working from: git checkout -b BRANCHNAME

    The u11_TemplatesExceptions folder contains the following items:

    .
    ├── graded_program
    │   ├── DataStructure
    │   │   └── SmartFixedArray.h
    │   ├── Exception
    │   │   ├── InvalidIndexException.h
    │   │   ├── ItemNotFoundException.h
    │   │   ├── NotImplementedException.h
    │   │   ├── StructureEmptyException.h
    │   │   └── StructureFullException.h
    │   ├── main.cpp
    │   ├── Project_CodeBlocks
    │   │   ├── TemplateProgram.cbp
    │   │   ├── TemplateProgram.depend
    │   │   └── TemplateProgram.layout
    │   ├── Project_Makefile
    │   │   └── Makefile
    │   ├── Project_VisualStudio2022
    │   │   ├── TemplateProgram.sln
    │   │   ├── TemplateProgram.vcxproj
    │   │   └── TemplateProgram.vcxproj.filters
    │   └── Tester
    │       ├── SmartFixedArrayTester.cpp
    │       └── SmartFixedArrayTester.h
    ├── practice1_functions
    │   └── templfunctions.cpp
    ├── practice2_structs
    │   └── templstruct.cpp
    ├── practice3_stdexcept
    │   └── stdexcept.cpp
    └── practice4_customexcept
        └── customexcept.cpp
    

  • practice1_functions
    FIELD               DATA
    --------------------------------------------------------------------------------
    product             USB-C cable pack
    price               9.99
    quantity            5
    sold_out            0
    

    For this practice program we are looking at templated functions. Starting off, we have three functions for three different data types, even though they do basically the same thing:

    void DisplayValue( string variable_name, string variable_value )
    {
      cout << setw( 20 ) << variable_name << setw( 20 ) << variable_value << endl;
    }
    
    void DisplayValue( string variable_name, int variable_value )
    {
      cout << setw( 20 ) << variable_name << setw( 20 ) << variable_value << endl;
    }
    
    void DisplayValue( string variable_name, float variable_value )
    {
      cout << setw( 20 ) << variable_name << setw( 20 ) << variable_value << endl;
    }
    

    Convert one of these functions into a templated function so that the variable_value can be any data type T. Remove the other two duplicates.

    The code in main() should continue working as usual, and you can add more variables with different data types and display their values now, too.


  • practice2_structs
    NODE      DATA      ADDR                PREV                NEXT
    --------------------------------------------------------------------------------
    Ptr 1     a         0x7fffe480ad18      0                   0x561efe87eef0
    Ptr 2     b         0x7fffe480ad10      0x561efe87eeb0      0x561efe87ef30
    Ptr 3     c         0x7fffe480ad08      0x561efe87eef0      0
    

    This program starts with a Node class that can only contain string data:

    struct Node
    {
      Node()
      {
        ptr_next = nullptr;
        ptr_prev = nullptr;
      }
    
      string data;
      Node* ptr_next;
      Node* ptr_prev;
    };
    

    As well as a PrintTable function that only works with this sort of Node:

    void PrintTable( Node* ptr1, Node* ptr2, Node* ptr3 )
    {
      cout << setw( 10 ) << "NODE" << setw( 10 ) << "DATA" << setw( 20 ) << "ADDR" << setw( 20 ) << "PREV" << setw( 20 ) << "NEXT" << endl;
      cout << string( 80, '-' ) << endl;
      cout << setw( 10 ) << "Ptr 1"  << setw( 10 ) <<  ptr1->data << setw( 20 ) << &ptr1 << setw( 20 ) <<  ptr1->ptr_prev  << setw( 20 ) <<  ptr1->ptr_next << endl;
      cout << setw( 10 ) << "Ptr 2" << setw( 10 ) <<  ptr2->data << setw( 20 ) << &ptr2 << setw( 20 ) <<  ptr2->ptr_prev  << setw( 20 ) <<  ptr2->ptr_next << endl;
      cout << setw( 10 ) << "Ptr 3" << setw( 10 ) <<  ptr3->data << setw( 20 ) << &ptr3 << setw( 20 ) <<  ptr3->ptr_prev  << setw( 20 ) <<  ptr3->ptr_next << endl;
    }
    

    Update the Node struct to be a templated struct. Change the string data to a T data type.

    You'll also have to update the PrintTable to support templated Nodes as its parameters.

    Further below in main(), update the set of Nodes already created to be Node<string> types. For additional practice, create another set of Nodes with a different data type and see that everything still works for the other data type as well.


  • practice3_stdexcept
    --------------------------------------------------------------------------------
    DIVISION EXAMPLE
    Enter a numerator and denominator, separated by a space: 5 0
    invalid_argument Exception: Division by 0 not allowed!
    
    --------------------------------------------------------------------------------
    DISPLAY EXAMPLE
    Enter an index between 0 and 4: 600
    out_of_range Exception: Invalid index!
    
    --------------------------------------------------------------------------------
    POINTER EXAMPLE
    Display good_ptr...
    Item being pointed to is: 600
    
    Display bad_pointer...
    invalid_argument Exception: ptr is null!
    

    For this program we have a few "risky functions" that could encounter errors. Currently, they have no error checking, and main() is not listening for any errors, either.

    For the Divide, Display, and PtrDisplay functions, add if statements to check for certain error scenarios and throw the appropriate exception if found:

    Function Error check Type of exception to throw
    Divide if denom is 0 invalid_argument
    Display if index is less than 0 or greater than/equal to arr size out_of_range
    PtrDisplay if arr is pointing to nullptr invalid_argument

    When throwing an exception, add a message string in the constructor so that the exception has a description to help programmers figure out waht went wrong…

    throw out_of_range( "Invalid index!" );

    Down in main(), I've marked certain functions as "!! Risky function call !!". These functions need to be surrounded in a try {} block (one try for each function), and then have a catch statement following, listening for the type of exception that the risky function might throw - so when calling Divide, use catch( const invalid_argument& ex ), and so on.

    When an exception is caught, use cout to display the exception information (ex.what()).


  • practice4_customexcept
    --------------------------------------------------------------------------------
    PIZZA PARTY
    How many pizza slices at pizza party? 50
    How many friends at party? 0
    
    You should really make more friends.
    Exception: Zero friends at party!
    

    For this program, we're going to inherit from an exception class and create our own exception.

    Custom exceptions:

    Use this as a template for the custom exceptions:

    class EXCEPTIONTYPE : public std::runtime_error
    {
    public:
        EXCEPTIONTYPE(std::string message)
            : std::runtime_error(message) {
            // Additional custom info for this type of error:
            std::cout << "You should really make more friends." << std::endl;
        }
    };
    

    Replace "EXCEPTIONTYPE" with the name of the exception you're creating. This is just creating a new class, inheriting from runtime_error, and setting up its constructor. The : std::runtime_error(message) line is calling the parent class' constructor, passing in the message. The parent class will handle dealing with the message, but we could add additional functionality in our constructor if needed. In this case, I just have an additional message for this exception type.

    SlicesPerPerson function:

    In the SlicesPerPerson function, create two if statements:

    • If friends is 0, then throw the NotEnoughFriendsException with an error message.
    • If pizzaSlices is 0, then throw the NotEnoughPizzaException with an error message.

    main():

    In main, wrap the "risky function call" (SlicesPerPerson( friendCount, pizzaSliceCount )) with a try {} block. Create two catch blocks afterwards, one listening for the NotEnoughFriendsException exception, and one listening for the NotEnoughPizzaException exception. Display each exception's error message (ex.what()) within each catch statement.


  • Graded program

    For the graded program there are only automated tests and no program right now. We will be editing the SmartFixedArray class, located within the DataStructure folder.

    The SmartFixedArray class is a templated class that stores a fixed-length array within it. We are sort of implementing part of the STL array class, just fewer features.

    Keep in mind the member variables that belong to this class, which will be important as you implement the SmartFixedArray's functionality:

    Member variable Description
    T data[100] An array of 100 items, of T data type
    ARRAY_SIZE The hard-coded array size, fixed at 100
    item_count How many items are currently stored in the array

    Custom exceptions:

    These exceptions are provided already, and you will use them in various functions:

    Exception Description
    InvalidIndexException  
    ItemNotFoundException Thrown by Search when an item requested is not found
    NotImplementedException All the functions have this by default to mark them as incomplete
    StructureEmptyException Thrown when trying to Pop or Get data from an empty array
    StructureFullException Thrown when trying to Push data to an array that is full

    Automated testing: Make sure to run the automated tests to verify that your data structure fully works!


    • SmartFixedArray<T>::SmartFixedArray()

      lab_u11_TemplatesExceptions_array_empty.png

      When we start off, we mark the array as empty. To do this, we just set this->item_count to 0.

      Even though the amount of memory allocated doesn't get changed throughout the lifetime of our array, we marked "filled in" spaces by adjusting the item count.


    • SmartFixedArray<T>::Clear()

      lab_u11_TemplatesExceptions_array_empty.png

      This function "deletes" all the items in the array… however, we're going to do lazy deletion. Basically, by setting this->item_count to 0, we effectively say "there's nothing in this array!". All the old data will be overwritten sooner or later when new data is added.


    • SmartFixedArray<T>::Size() const

      lab_u11_TemplatesExceptions_array_3items.png

      Return
      The current amount of items stored in the array

      This function is responsible for telling us how many pieces of data are currently stored in the array. This is reflected by the this->item_count variable (NOT the total this->ARRAY_SIZE, which marks the maximum capacity of the array!)


    • SmartFixedArray<T>::IsFull() const

      lab_u11_TemplatesExceptions_array_full.png

      Return
      true if the array is full, or false otherwise.

      This function will tell us if the array is currently full. The maximum capacity of the array is stored in this->ARRAY_SIZE, so if this->item_count matches that, then it means we've filled up all possible spaces.


    • SmartFixedArray<T>::IsEmpty() const

      lab_u11_TemplatesExceptions_array_empty.png

      Return
      true if the array is empty, or false otherwise.

      The this->item_count represents how many items are currently stored in the array, so if it is 0, then that means the array is empty.


    • SmartFixedArray<T>::Search( T item ) const

      lab_u11_TemplatesExceptions_array_search.png

      Parameters
      • item, the data we're searching for.
      Return
      • Returns the index of the item found
      Exception
      • If the item is not found in the array, then throw a ItemNotFoundException.

      Use a basic Linear Search to iterate over all the used elements of the array (0 to this->item_count), looking at each item. If the item asked for is found within your loop, then return the index of this item (usually i in a for loop).

      After the for loop is over, this means the item wasn't found, so in this case throw the ItemNotFoundException. (Don't throw this INSIDE THE FOR LOOP! Logic error…)


    • SmartFixedArray<T>::ShiftLeft( sizet index )

      lab_u11_TemplatesExceptions_array_shiftleft.png

      Parameters
      • index, all items to the right of this index value will be shifted to the left.
      Exception
      • If the index is invalid then throw an InvalidIndexException. The index is invalid if index is less than 0 or greater than or equal to this->item_count, as we want to keep our items contiguous in the array.

      Use a for loop to shift everything to the right of the index position to the left.

      (Note: DON'T adjust the item count here! This is just a helper function and the function that calls it will handle adjusting item count as needed.)

      QUESTION: How do I shift an item over to the left?

      With an assignment statement, you can move things over from its neighbor: this->data[i] = this->data[i+1]


    • SmartFixedArray<T>::ShiftRight( sizet index )

      lab_u11_TemplatesExceptions_array_shiftright.png

      Parameters
      • index, all items to the right of this index value will be shifted to the left.
      Exception
      • If the index is invalid then throw an InvalidIndexException. The index is invalid if index is less than 0 or greater than or equal to this->item_count, as we want to keep our items contiguous in the array.
      • If the array is full (call IsFull() to check) then throw a StructureFullException, as there is no more space in the array.

      Use a for loop to take everything at index position and to its right and shift these items further to the right.

      (Note: DON'T adjust the item count here! This is just a helper function and the function that calls it will handle adjusting item count as needed.)

      QUESTION: How do I shift an item over to the right?

      With an assignment statement, you can move things over from its neighbor: this->data[i] = this->data[i-1]. You'll want to make sure to start your loop at the end of the range and go towards the index.


    • SmartFixedArray<T>::PushBack( T newItem )

      lab_u11_TemplatesExceptions_array_pushback.png

      Parameters
      • newItem is a new piece of data to eb added to the back (end) of the array.
      Exception
      • If the array is full (call IsFull() to check) then throw a StructureFullException.

      The this->item_count contains the amount of items that are currently stored in the array, but this number ALSO corresponds to the next location where you need to add your data.

      Insert the newItem into your this->data array at the this->item_count position, then make sure to increment this->item_count.


    • SmartFixedArray<T>::PopBack()

      lab_u11_TemplatesExceptions_array_popback.png

      Exception
      • If the array is empty (call IsEmpty() to check) then throw StructureEmptyException.

      This function "deletes" the item at the current end of hte items in the array. However, we just do a "lazy deletion" here by decrementing this->item_count. We don't need to do anything with the data itself, because it will be overwritten by the next Push call.


    • SmartFixedArray<T>::GetBack()

      lab_u11_TemplatesExceptions_array_getback.png

      Exception
      • If the array is empty (call IsEmpty() to check) then throw StructureEmptyException.
      Return
      • Return the element of this->data[] at the this->item_count-1 position.

      This function returns the last item in the contiguous list of elements in the array… Basically, whatever is at size-1, or this->item_count-1.


  • Turning in the assignment

    Screenshot: Before finishing up, run the automated tests and take a screenshot of all of your tests passing. Save the file somewhere as a .png.

    Back up your code: Open Git Bash in the base directory of your repository folder.

    1. Use git status to view all the changed items (in red).
    2. Use git add . to add all current changed items.
    3. Use git commit -m "INFO" to commit your changes, change "INFO" to a more descriptive message.
    4. Use git push -u origin BRANCHNAME to push your changes to the server.
    5. On GitLab.com, go to your repository webpage. There should be a button to Create a new Merge Request. You can leave the default settings and create the merge request.
    6. Copy the URL of your merge request.

    Turn in on Canvas:

    1. Find the assignment on Canvas and click "Start assignment".
    2. Select Upload file and upload your screenshot.
    3. Paste your GitLab URL into the Comments box.
    4. Click "Submit assignment" to finish.

    \newpage

WEEK 6 - FEB 19

UNIT 12: Intro to data structures

πŸ“–οΈ Reading - Intro to data structures (U12.READ)

  • Introduction to Data Structures and Algorithm Analysis

    reading_u12_IntroDataStructures_structures.png

    • What are data structures?

      A data structure is an object (a class, a struct - some type of structured thing) that holds data. In particular, the entire job of a data structure object is to store data and provide an interface for adding, removing, and accessing that data.

      "Don't all the classes we write hold data? How is this different from other objects I've defined?"

      In the past, you may have written classes to represent objects, like perhaps a book. A book could have member variables like its title, isbn, and year published, and some methods that help us interface with a book.

      Book
      - title : string
      - isbn : string
      - year : int
      + Setup() : void
      + Display() : void

      However - this is not a data structure. We could, however, use a data structure to store a list of books.

      A data structure will store a series of some sort of data. Often, it will use an array or a linked list as a base structure and functionality will be built on top.

      Ideally, the user doesn't care about how the data structure works, they just care that they can add, remove, and access data they want to store.

      Ideally, when we are writing a data structure, it should be:

      • Generic, so you could store any data type in it.
      • Reusable, so that the data structure can be used in many different programs.
      • Robust, offering exception handling to prevent the program from crashing when something goes wrong with it.
      • Encapsulated, handling the inner-workings of dealing with the data, without the user (or other programmers working outside the data structure) having to write special code to perform certain operations.

      SmartBookArray
      - bookArray : Book[]
      - arraySize : int
      - bookCount : int
      + AddBook( newBook : Book ) : void
      + RemoveBook( index : int ) : void
      + RemoveBook( title : string ) : void
      + GetBook( index : int ) : Book&
      + FindBookIndex( title : string ) : int
      + DisplayAll() : void
      + Size() : int
      + IsEmpty() : bool

      The way I try to conceptualize the work I'm doing on a data structure is to pretend that I'm a developer that is going to create and sell a C++ library of data structures that other developers at other companies can use in their own, completely separate, software projects. If I'm selling my data structures package to other businesses, my code should be dependable, stable, efficient, and relatively easy to use.

      reading_u12_IntroDataStructures_structures_ad.png


    • What is algorithm analysis?

      Algorithm Analysis is the process of figuring out how efficient a function is and how it scales over time, given more and more data to operate on.

      reading_u12_IntroDataStructures_structures_complexity.png

      This is another important part of dealing with data structures, as different structures will offer different trade offs when it comes to the efficiency of data access functions, data searching functions, data adding functions, and data removal functions. Every operation takes a little bit of processing time, and as you iterate over data, that processing time is multiplied by the amount of times you go through a loop.

      Example: Scalability

      Let's say we have a sorting algorithm that, for every \(n\) records, it takes \(n^2\) program units to find an object. If we had 100 records, then it would take \(100^2 = 10,000\) time-units to sort the set of data.

      What the time-units are could vary - an older machine might take longer to execute one instruction, and a newer computer might process an instruction much more quickly, but we think of algorithm complexity in this sort of generic form.

      reading_u12_IntroDataStructures_structures_ad2.png

      Example: Efficiency of an array-based structure
      value: kansas missouri arkansas ohio oklahoma
      index: 0 1 2 3 4

      In an array-based structure, we have a series of elements in a row, and each element is accessible via its index (its position in the array). Arrays allow for random-access, so accessing element #2 is instant:

      cout << arr[2];
      

      No matter how many elements there are (\(n\)), we can access the element at index 2 without doing any looping. We state that this is \(O(1)\) ("Big-O of 1") time complexity for an access operation on an array.

      However, if we were searching through the unsorted array for an item, we would have to start at the beginning and look at each item, one at a time, until we either found what we're looking for, or hit the end of the array:

      for ( int i = 0; i < ARR_SIZE; i++ )
      {
          if ( arr[i] == searchTerm )
          {
                  return i; // found at this position
          }
      }
      return -1;      // not found
      

      For search, the worst-case scenario is having to look at all elements of the array to ensure what we're looking for isn't there. Given \(n\) items in the array, we have to iterate through the loop \(n\) times. This would end up being \(O(n)\) ("Big-O of \(n\)") time complexity for a search operation on an array.

      We can build our data structures on top of an array, but there is also a type of structure called a linked structure, which offers its own pros and cons to go with it. We will learn more about algorithm analysis and types of structures later on.


    • The point of a Data Structures class

      Of course, plenty of data structures have already been written and are available out there for you to use. C++ even has the Standard Template Library full of structures already built and optimized!

      vector http://www.cplusplus.com/reference/vector/vector/
      list http://www.cplusplus.com/reference/list/list/
      map http://www.cplusplus.com/reference/map/map/
      stack https://cplusplus.com/reference/stack/stack/
      queue https://cplusplus.com/reference/queue/queue/

      "So why are we learning to write data structures if they've already been written for us?"

      While you generally won't be rolling your own linked list for projects or on the job, it is important to know how the inner-workings of these structures operate. Knowing how each structure works, its tradeoffs in efficiency, and how it structures its data will help you choose what structures to use when faced with a design decision for your software.

      reading_u12_IntroDataStructures_structures_which-structure.png


  • Review questions:
    1. asdfasdf

UNIT 13: Smart Array Structures

πŸ“–οΈ Reading - Smart arrays (U13.READ)3

arrays.png

  • Smart fixed array structure

    In previous programming courses you've probably worked with arrays to store data. You've probably encountered out-of-bounds errors and had to deal with array indices, moving items around, and generally micro-managing your array's data as the program goes.

    What we're going to do here is wrap our array - and everything we need to work with it - inside of a class, creating our first data structure: A slightly smarter fixed-length array.

    • Functionality for our data type

      What kind of things do we want to \textit{do} to a data structure? What is some functionality that others will need to store and retrieve data, without having to see inside the class? How do we keep the data stored?

      Creating the array: For example, if we weren't storing our array inside a class, we might declare it in a program like this:

      // Create an array + helper variables
      const int ARR_SIZE = 100;
      int totalItems = 0;
      string data[ARR_SIZE];
      

      With a fixed-length array, we have to keep track of the array size ARRAY_SIZE to ensure we don't go out-of-bounds in the array… valid indices will be 0 until ARRAY_SIZE - 1.

      We are also keeping track of the totalItems in the array, because even though we have an array with some size, it could be useful to know that currently no data is stored in it. Then, when new data is added, we could increment totalItems by 1.

      An empty array:

      index 0 1 2 3 4 5
      value "" "" "" "" "" ""

      totalItems = 0


      Adding onto the array: Adding on to the array would mean choosing an index to put new data. If we were wanting to fill the array from \textbf{left-to-right}, we would start at index 0, then 1, then 2, then 3, and so on.

      The totalItems variable begins at 0. Once the first item is added, it is then 1. Once a second item is added, it is then 2. Basically, the totalItems corresponds to the next index to store new data at

      // Add new item
      data[totalItems] = myNewData;
      totalItems++;
      

      One item added:

      index 0 1 2 3 4 5
      value "Cats" "" "" "" "" ""

      totalItems = 1


      Removing items from the array: We may also want to remove an item at a specific index from the array. There's not really a way to "delete" an item from an array, but we can overwrite it, if we wanted to…

      // Remove an item
      int index;
      cout << "Remove which index? ";
      cin >> index;
      
      data[index] = "";
      totalItems--;
      

      But if we did it this way we would create an array with gaps in it. Additionally now totalItems doesn't align to the next index to insert at.

      Before removing an item:

      index 0 1 2 3 4 5
      value "Cats" "Dogs" "Mice" "Birds" "Snakes" ""

      totalItems = 6

      After removing an item:

      index 0 1 2 3 4 5
      value "Cats" "Dogs" "Mice" "" "Snakes" ""

      totalItems = 5

      We could design our array data structure this way, but to find a new place to add an item we'd have to use a for loop to look for an empty spot to use - this means every time we add a new item it would be less efficient.

      Instead, we could design our arrays so that after a remove, we shift elements backwards to "overwrite" the empty spot. This also solves our issue with totalItems not aligning to the next-index-to-insert-at.

      After removing an item with a "Shift Left":

      index 0 1 2 3 4 5
      value "Cats" "Dogs" "Mice" "Snakes" "" ""

      totalItems = 5


      Searching for an item: Another thing a user of this data structure might want to do is search for an item - is this thing stored in the array? If so, what is the index? If the array is unsorted (we're not going to do sorting yet), then really the only way to search for the item is to start at the beginning and keep searching until the end.

      If we find a match, we return that index number.

      If we get to the end of the loop and nothing has been returned, that means it isn't in the array so we would have to return something to symbolize "it's not in the arrayI – such as returning -1, or perhaps throwing an exception.

      Populated array:

      index 0 1 2 3 4 5
      value "Cats" "Dogs" "Mice" "Birds" "Snakes ""

      totalItems = 5

      Searching through the array would look like this:

      // Search for an item
      string findme;
      cout << "Find what? ";
      getline( cin, findme );
      
      int foundIndex;
      
      for ( int i = 0; i < totalItems; i++ )
      {
          if ( data[i] == findme )
          {
              foundIndex = i;   // Found it
              break;
          }
      }
      
      // Did not find it
      foundIndex = -1;
      

      Once we implement this structure within a class, we will turn this into a function that returns data - this is just an example as if we were writing a program in main().


      Retrieving an item at some index: Besides adding, removing, and searching for items, users will want to retrieve the data located at some index. This would be a simple return once we're writing our class, but generally accessing item at index looks like:

      // Display element at index
      int index;
      cout << "Which index? ";
      cin >> index;
      cout << data[index] << endl;
      

      Visualizing the array: Another feature could be to display all elements of the array, which might look something like this:

      // Display all items
      for ( int i = 0; i < totalItems; i++ )
      {
          cout << i << ". " << data[i] << endl;
      }
      

    • Predicting errors/exceptions

      Invalid indices: When working with arrays one of the most common errors encountered are out-of-bounds errors: When we have an index that is less than 0, or equal to or greater than the size of the array…

      // Out of bounds!
      cout << data[-1]        << endl;    // error!
      cout << data[ARR_SIZE]  << endl;    // error!
      

      Additionally, if the user tries to access an index \(\geq\) totalItems then they would retrieve data that is invalid, though this wouldn't crash the program if it is still less than ARR_SIZE.


      Array is full: With the fixed-length array, we could also run out of space, and we would have to design a way to deal with it. For example, if totalItems was equal to ARR_SIZE, then we are out of space and doing this would also cause an out-of-bounds error.

      data[totalItems] = "Ferrets";       // error if full!
      totalItems++;
      

      As part of designing our data structure, we need to make sure we account for reasonable scenarios that would cause the program to crash and guard against logic errors. This would all be part of how we implement the functionality.


    • Creating a class to wrap the array

      For our wrapper class we will need the array and array size / item count integers as part of the private member variables. The public functions would include the "interface" of what the user will want to do with the array, and we could also add private/protected helper functions.

      Class Name  
      - m_array : TYPE[100]
      - m_itemCount : int
      - m_arraySize : const int
      + SmartFixedArray()  
         
      + PushFront( newData : TYPE ) : void
      + PushBack( newData : TYPE ) : void
      + PushAtIndex( index : int, newData : TYPE ) : void
         
      + PopFront() : void
      + PopBack() : void
      + PopAtIndex( index : int ) : void
         
      + GetFront() : TYPE&
      + GetBack() : TYPE&
      + GetAtIndex( index : int ) : TYPE&
         
      + Search( item : TYPE ) : int
      + Clear() : void
      + IsEmpty() : bool
      + IsFull() : bool
      + Size() : int
         
      - ShiftLeft() : int
      - ShiftRight() : int
      - IsValidIndex( index : int ) : bool

      The class declaration could look like this:

      template <typename T>
      class SmartFixedArray
      {
      public:
          SmartFixedArray();
      
          void PushBack( T newItem );
          void PushFront( T newItem );
          void PushAt( T newItem, int index );
      
          void PopBack();
          void PopFront();
          void PopAt( int index );
      
          T GetBack() const;
          T GetFront() const;
          T GetAt( int index ) const;
      
          int Search( T item ) const;
          void Display() const;
          int Size() const;
          bool IsEmpty() const;
          bool IsFull() const;
      
      private:
          const int m_arraySize;
          T m_array[100];
          int m_itemCount;
      
          void ShiftLeft( int index );
          void ShiftRight( int index );
          bool IsValidIndex( int index );
      };
      

      For this example data structure, we are hard-coding the array size to 100 elements. It is unlikely that anyone would use this data structure for anything, but I wanted to start off with a basic fixed-length array. Next, we will move to a dynamic array, which will be more useful.

      With this class structure set up, lets look into how to actually implement the functionality.


      • Constructor - Preparing the array to be used

        When the constructor is called, we need to set the value of m_arraySize as part of the initializer list since it is a const member and needs to be initialized right away.

        Otherwise, we can initialize m_itemCount to 0 as well, since are creating an empty array.

        template <typename T>
        SmartFixedArray<T>::SmartFixedArray()
            : m_arraySize( 100 )
        {
            m_itemCount = 0;
        }
        

      • Helper functions

        The helper functions here are easier to implement, and we will be using them in the main interface functions.

        • int Size() const

          This function returns the value from m_itemCount - the current amount of items in the array.

        • bool IsEmpty() const

          Returns true if the array is empty and false if not. The array is empty if m_itemCount is 0.

        • bool IsFull() const

          Returns true if the array is full and false if not. The array is full if m_itemCount is equal to m_arraySize.

        • bool IsValidIndex( int index )

          Returns true if the index is valid and false otherwise.

          • Valid index: The index is valid if $0 ≤ $ index and index $ < $ m_itemCount.
          • Invalid index: The index is invalid if index \(<\) 0 or index \(\geq\) m_itemCount.

            bool SmartFixedArray<T>::IsValidIndex( int index )
            {
                return ( 0 <= index && index < m_itemCount );
            }
            
        • void ShiftRight( int index )

          Error checks:

          • If the array is full, then throw an exception.
          • If the index is invalid, then throw an exception.

          Functionality:

          • Iterate over the array, starting at the index of the first empty space. Moving backwards, copy each item over from the previous index. Loop until hitting the index.

            void SmartFixedArray<T>::ShiftRight( int index )
            {
                if ( !IsValidIndex( index ) )
                {
                    throw out_of_range( "Index is out of bounds!" );
                }
                else if ( IsFull() )
                {
                    throw length_error( "Array is full!" );
                }
            
                for ( int i = m_itemCount; i > index; i-- )
                {
                    m_array[i] = m_array[i-1];
                }
            }
            
        • void ShiftLeft( int index )

          Error checks:

          • If the index is invalid, then throw an exception.

          Functionality:

          • Iterate over the array, starting at the index given and going until the last element. Copy each neighbor from the right to the current index until we get to the end.

            void SmartFixedArray<T>::ShiftLeft( int index )
            {
                if ( !IsValidIndex( index ) )
                {
                    throw out_of_range( "Index is out of bounds!" );
                }
            
                for ( int i = index; i < m_itemCount-1; i++ )
                {
                    m_array[i] = m_array[i+1];
                }
            }
            
        • void Display() const

          Iterates over all the elements of the array displaying each item.

          void SmartFixedArray<T>::Display() const
          {
              for ( int i = 0; i < m_itemCount; i++ )
              {
                  cout << i << ". " << m_array[i] << endl;
              }
          }
          
        • int Search( T item ) const

          Iterates over all the elements of the array searching for the \texttt{item}. If a match is found, it returns the index of that element. Otherwise, if it finishes looping over the array and has not yet returned, it means that nothing was found. Return -1 in this case, or throw an exception (this is a design decision).

          int SmartFixedArray<T>::Search( T item ) const
          {
              for ( int i = 0; i < m_itemCount; i++ )
              {
                  if ( m_array[i] == item )
                  {
                      return i;
                  }
              }
              throw runtime_error( "Could not find item!" );
          }
          

      • Push - adding items to the array

        We have three different Push functions in order to add an item to different parts of the array, which will each require slightly different logic:

        PushBack: Add a new item to the end of the list of elements…

        BEFORE: totalItems = 4

        index 0 1 2 3 4 5
        value "Cats" "Dogs" "Mice" "Birds" "" ""

        AFTER: totalItems = 5

        index 0 1 2 3 4 5
        value "Cats" "Dogs" "Mice" "Birds" "Ferrets" ""

        PushFront: Add a new item to the beginning of the list of elements. This requires shifting everything forward to make space for the new item…

        BEFORE: totalItems = 4

        index 0 1 2 3 4 5
        value "Cats" "Dogs" "Mice" "Birds" "" ""

        AFTER: totalItems = 5

        index 0 1 2 3 4 5
        value "Ferrets" "Cats" "Dogs" "Mice" "Birds" ""

        PushAt: Add a new item to the index given. This requires shifting everything forward to make space for the new item…

        BEFORE: totalItems = 4

        index 0 1 2 3 4 5
        value "Cats" "Dogs" "Mice" "Birds" "" ""

        AFTER: totalItems = 5

        index 0 1 2 3 4 5
        value "Cats" "Dogs" "Ferrets" "Mice" "Birds" ""

        Additional design considerations:

        The PushFront and PushAt functions require shifting other elements over, so I've added a helper function called ShiftRight so that we don't write the same functionality in both of these functions.

        Additionally, for each of these functions we will need to check to see if the array is full prior to adding any new items or shifting things. Implementing a IsFull function can help with this as well.

        • Implementing void PushBack( T newItem )

          Error checks:

          • If the array is full, then throw an exception.
          if ( IsFull() )
              throw length_error( "Array is full!" );
          

          Functionality:

          • Add the new item to the first empty space.
          • Increment the item count.
          m_array[ m_itemCount ] = newItem;
          m_itemCount++;
          

        • Implementing void PushFront( T newItem )

          Error checks:

          • If the array is full, then throw an exception.

          Preparation:

          • Shift everything to the right, starting at index 0.
          ShiftRight( 0 );
          

          Functionality:

          • Add the new item at position 0.
          • Increment the item count.

        • Implementing void PushAt( T newItem )

          DRY (Don't Repeat Yourself) checks:

          • If the index is 0, call PushFront instead.
          • If the index is m_itemCount, call PushBack instead.

          Error checks:

          • If the array is full, then throw an exception.
          • If the index is invalid, throw an exception.

          Preparation:

          • Shift everything to the right, starting at the index.
          ShiftRight( index );
          

          Functionality:

          • Add the new item at position index.
          • Increment the item count.

      • Pop - removing items from the array
        • Implementing void PopBack()

          Error checks: If the array is empty, throw exception.

          Functionality: Decrement m_itemCount.


        • Implementing void PopFront()

          Error checks: If the array is empty, throw exception.

          Functionality: Call ShiftLeft at 0. Decrement m_itemCount.


        • Implementing void PopAt( int index )

          Error checks: If the array is empty, throw exception. If the index is invalid, throw an exception.

          Functionality: Call ShiftLeft at index. Decrement m_itemCount.


      • Get - accessing items in the array
        • Implementing T GetBack()

          Error checks: If the array is empty, throw exception.

          Functionality: Return the item in the array at position m_itemCount-1.


        • Implementing T GetFront()

          Error checks: If the array is empty, throw exception.

          Functionality: Return the item in the array at position 0.


        • Implementing T GetAt( int index )

          Error checks: If the array is empty, throw exception. If the index is invalid, throw an exception.

          Functionality: Return the item in the array at position index.


      • Search

        Use a for loop to iterate over all elements of the array (0 to m_itemCount). If the item is found, return its index. Otherwise, after the loop has concluded, throw an exception if nothing was found.

        int SmartFixedArray<T>::Search( T item ) const
        {
            for ( int i = 0; i < m_itemCount; i++ )
            {
                if ( m_array[i] == item )
                {
                    return i;
                }
            }
            throw ItemNotFoundException( "SmartFixedArray<T>::Search", "Could not find item!" );
        }
        

      • SmartFixedArray code
        template <typename T>
        class SmartFixedArray
        {
        public:
          SmartFixedArray();
          ~SmartFixedArray();
        
          void PushBack( T newItem );
          void PushFront( T newItem );
          void PushAt( T newItem, int index );
        
          void PopBack();
          void PopFront();
          void PopAt( int index );
        
          T GetBack() const;
          T GetFront() const;
          T GetAt( int index ) const;
        
          int Search( T item ) const;
          void Display() const;
          void Display( ostream& outstream ) const;
          int Size() const;
          bool IsEmpty() const;
          bool IsFull() const;
          void Clear();
        
        private:
          T m_array[100];
          const int ARRAY_SIZE;
          int m_itemCount;
        
          void ShiftLeft( int index );
          void ShiftRight( int index );
        };
        
        template <typename T>
        SmartFixedArray<T>::SmartFixedArray()
          : ARRAY_SIZE( 100 )
        {
          Clear();
        }
        
        template <typename T>
        SmartFixedArray<T>::~SmartFixedArray()
        {
          Clear();
        }
        
        template <typename T>
        void SmartFixedArray<T>::Clear()
        {
          m_itemCount = 0;
        }
        
        template <typename T>
        int SmartFixedArray<T>::Size() const
        {
          return m_itemCount;
        }
        
        template <typename T>
        bool SmartFixedArray<T>::IsFull() const
        {
          return ( m_itemCount == ARRAY_SIZE );
        }
        
        template <typename T>
        bool SmartFixedArray<T>::IsEmpty() const
        {
          return ( m_itemCount == 0 );
        }
        
        template <typename T>
        void SmartFixedArray<T>::Display() const
        {
          Display( cout );
        }
        
        template <typename T>
        void SmartFixedArray<T>::Display( ostream& outstream ) const
        {
          for ( int i = 0; i < m_itemCount; i++ )
          {
            outstream << i << ". " << m_array[i] << endl;
          }
        }
        
        template <typename T>
        void SmartFixedArray<T>::ShiftLeft( int index )
        {
          if ( index < 0 || index >= m_itemCount )
          {
            // THROW EXCEPTION
          }
        
          for ( int i = index; i < m_itemCount-1; i++ )
          {
            m_array[i] = m_array[i+1];
          }
        }
        
        template <typename T>
        void SmartFixedArray<T>::ShiftRight( int index )
        {
          if ( index < 0 || index >= m_itemCount )
          {
            // THROW EXCEPTION
          }
          else if ( m_itemCount == ARRAY_SIZE )
          {
            // THROW EXCEPTION
          }
        
          for ( int i = m_itemCount; i > index; i-- )
          {
            m_array[i] = m_array[i-1];
          }
        }
        
        template <typename T>
        void SmartFixedArray<T>::PushBack( T newItem )
        {
          if ( IsFull() )
          {
            // THROW EXCEPTION
          }
        
          m_array[ m_itemCount ] = newItem;
          m_itemCount++;
        }
        
        template <typename T>
        void SmartFixedArray<T>::PushFront( T newItem )
        {
          if ( IsFull() )
          {
            // THROW EXCEPTION
          }
          if ( !IsEmpty() )
          {
            ShiftRight( 0 );
          }
        
          m_array[ 0 ] = newItem;
          m_itemCount++;
        }
        
        template <typename T>
        void SmartFixedArray<T>::PushAt( T newItem, int index )
        {
          if ( index == 0 )
          {
            PushFront( newItem );
            return;
          }
          else if ( index == m_itemCount )
          {
            PushBack( newItem );
            return;
          }
        
          if ( index < 0 || index > m_itemCount )
          {
            // THROW EXCEPTION
          }
        
          if ( IsFull() )
          {
            // THROW EXCEPTION
          }
        
          ShiftRight( index );
          m_array[ index ] = newItem;
          m_itemCount++;
        }
        
        template <typename T>
        void SmartFixedArray<T>::PopBack()
        {
          if ( IsEmpty() )
          {
            // THROW EXCEPTION
          }
          m_itemCount--;
        }
        
        template <typename T>
        void SmartFixedArray<T>::PopFront()
        {
          if ( IsEmpty() )
          {
            // THROW EXCEPTION
          }
          ShiftLeft( 0 );
          m_itemCount--;
        }
        
        template <typename T>
        void SmartFixedArray<T>::PopAt( int index )
        {
          if ( IsEmpty() )
          {
            // THROW EXCEPTION
          }
          else if ( index < 0 || index >= m_itemCount )
          {
            // THROW EXCEPTION
          }
          ShiftLeft( index );
          m_itemCount--;
        }
        
        template <typename T>
        T SmartFixedArray<T>::GetBack() const
        {
          if ( IsEmpty() )
          {
            // THROW EXCEPTION
          }
        
          return m_array[m_itemCount - 1];
        }
        
        template <typename T>
        T SmartFixedArray<T>::GetFront() const
        {
          if ( IsEmpty() )
          {
            // THROW EXCEPTION
          }
        
          return m_array[0];
        }
        
        template <typename T>
        T SmartFixedArray<T>::GetAt( int index ) const
        {
          if ( IsEmpty() )
          {
            // THROW EXCEPTION
          }
          else if ( index < 0 || index >= m_itemCount )
          {
            // THROW EXCEPTION
          }
        
          return m_array[index];
        }
        
        template <typename T>
        int SmartFixedArray<T>::Search( T item ) const
        {
          for ( int i = 0; i < m_itemCount; i++ )
          {
            if ( m_array[i] == item )
            {
              return i;
            }
          }
          // THROW EXCEPTION
        }
        

  • Smart dynamic array structure

    With the dynamic array structure, most of it will be similar to the Fixed Array Structure, except we now have to deal with memory management and pointers.

    Instead of throwing exceptions if the array is full, with a dynamic array we can resize it instead. However, we also now need to check for the array pointer (m_array in this example) pointing to nullptr.

    The class declaration could look like this:

    class SmartDynamicArray
    {
    public:
        SmartDynamicArray();
        ~SmartDynamicArray();
    
        void PushBack( T newItem );
        void PushFront( T newItem );
        void PushAt( T newItem, int index );
    
        void PopBack();
        void PopFront();
        void PopAt( int index );
    
        T GetBack() const;
        T GetFront() const;
        T GetAt( int index ) const;
    
        int Search( T item ) const;
    
        void Display() const;
        int Size() const;
        bool IsEmpty() const;
        void Clear();
    
    private:
        T* m_array;
        int m_arraySize;
        int m_itemCount;
    
        void ShiftLeft( int index );
        void ShiftRight( int index );
    
        void AllocateMemory( int size );
        void Resize( int newSize );
    
        bool IsFull() const;
    };
    

    • Constructor - Preparing the array to be used

      With the constructor here, it is important to assign nullptr to the pointer so that we don't try to dereference a garbage address. The array size and item count variables should also be assigned to 0.

      template <typename T>
      SmartDynamicArray<T>::SmartDynamicArray()
      {
        m_array = nullptr;
        m_itemCount = 0;
        m_arraySize = 0;
      }
      

    • Destructor - Cleaning up the array

      Having a destructor is also very important since we're working with pointers. Before our data structure is destroyed, we need to make sure that we free any allocated memory.

      template <typename T>
      SmartDynamicArray<T>::~SmartDynamicArray()
      {
        if ( m_array != nullptr )
        {
          delete [] m_array;
          m_array = nullptr;
        }
        m_arraySize = 0;
        m_itemCount = 0;
      }
      

      We can also put this functionality into the Clear() function, and call Clear() from the destructor.


    • void AllocateMemory( int size )

      When the array is not in use, the m_array pointer will be pointing to nullptr. In this case, we need to allocate space for a new array before we can begin putting items into it.

      Error checks:

      • If the m_array pointer is NOT pointing to nullptr, throw an exception.

      Functionality:

      1. Allocate space for the array via the m_array pointer.
      2. Assign m_arraySize to the size passed in as a parameter.
      3. Set m_itemCount to 0.
      template <typename T>
      void SmartDynamicArray<T>::AllocateMemory( int size )
      {
        if ( m_array != nullptr )
        {
          throw logic_error( "Can't allocate memory, m_array is already pointing to a memory address!" );
        }
      
        m_array = new T[ size ];
        m_arraySize = size;
        m_itemCount = 0;
      }
      

      For this function, if the array is already pointing somewhere, we don't want to just erase all that data and allocate space for a new array. We want to try to prevent data loss, so it would be better to throw an exception instead so that the caller can decide how they want to handle it.


    • void Resize( int newSize )

      We can't technically resize an array that has been created. We can, however, allocate more space for a bigger array and copy all the data over and then update the pointer to point to the new array. And that's exactly how we "resize" our dynamic array.

      Error checks:

      • If the new size is smaller than the old size, throw an exception.

      Functionality:

      1. If m_array is pointing to nullptr, then just call AllocateMemory instead.
      2. Otherwise:
        1. Allocate space for a new array with the new size using a new pointer.
        2. Iterate over all the elements of the old array, copying each element to the new array.
        3. Free space from the old array.
        4. Update the m_array pointer to point to the new array address.
        5. Update the m_arraySize to the new size.
      template <typename T>
      void SmartDynamicArray<T>::Resize( int newSize )
      {
        if ( newSize < m_arraySize )
        {
          throw logic_error( "Invalid size!" );
        }
        else if ( m_array == nullptr )
        {
          AllocateMemory( newSize );
        }
        else
        {
          T* newArray = new T[newSize]; // New array
      
          // Copy values over
          for ( int i = 0; i < m_itemCount; i++ )
          {
            newArray[i] = m_array[i];
          }
      
          delete [] m_array;      // Deallocate old space
          m_array = newArray;     // Update pointer
          m_arraySize = newSize;  // Update size
        }
      }
      

    • Updating other functions
      • ShiftLeft: If m_array is pointing to nullptr, throw an exception.
      • ShiftRight:
        • If m_array is pointing to nullptr, throw an exception.
        • If the array is full, call Resize.
      • PushBack, PushFront, PushAt:
        • If m_array is pointing to nullptr call AllocateMemory.
        • If the array is full, call Resize.

      \newpage

WEEK 7 - FEB 26

UNIT 14: Linked List Structure

πŸ“–οΈ Reading - Linked lists (U14.READ) (WORK IN PROGRESS)

reading_u14_LinkedList_linked-list.png

  • Coming from arrays…

    Before taking a Data Structures course, you've probably only managed data in your program via arrays.

    When we declare an array in C++, we must give it a size (whether we're making a dynamic array or a vanilla array). This ensures that memory can be allocated so that each element of the array is side-by-side with other elements.

    • Investigating arrays and memory addresses:

      Let's write a simple program to look at how arrays are stored in memory, in case you don't remember.

      First, how big is one element? We can use the sizeof function to find out how big, in bytes, a given data type is:

      cout << "The size of a int is: "
           << sizeof( int ) << " bytes." << endl;
      

      The size of a int is: 4 bytes.
      

      One integer is 4 bytes. Next, we can declare an array of integers and look at how many bytes that takes up:

      const int SIZE = 10;
      int intArr[SIZE];
      cout << "Size of a int array with "
           << SIZE << " elements: "
           << sizeof( intArr ) << " bytes." << endl;
      
      Size of a int array with 10 elements: 40 bytes.
      

      Now, let's look at the memory addresses of each element:

      cout << "Memory addresses for the int array:" << endl;
      for ( int i = 0; i < SIZE; i++ )
      {
          cout << "Element " << i << ": " << &(intArr[i]) << endl;
      }
      
      Memory addresses for the int array:
      Element 0: 0x7ffff9867d00
      Element 1: 0x7ffff9867d04
      Element 2: 0x7ffff9867d08
      Element 3: 0x7ffff9867d0c
      Element 4: 0x7ffff9867d10
      Element 5: 0x7ffff9867d14
      Element 6: 0x7ffff9867d18
      Element 7: 0x7ffff9867d1c
      Element 8: 0x7ffff9867d20
      Element 9: 0x7ffff9867d24
      

      Each time this program runs, we will have different memory addresses for the intArr, but those addresses will always be contiguous in memory, 4 bytes apart.

      reading_u14_LinkedList_contiguousarray.png

      Sidenote 1: Hexadecimal
      When we write in hexadecimal, we want each number to only take up one character space. So, we have 0-9 to be 0 through 9, but then 10 is A, 11 is B, 12 is C, 13 is D, 14 is E, and 15 is F.
      Sidenote 2: Address of the beginning of the array
      The address of intArr is the same as the address of intArr[0].

      arrays.png


    • Pros and Cons of arrays for storing data:
      Pros:
      • Random access is instant: This means, we can access an element at any arbitrary index instantaneously. How? Well, we have the address of the 0th element, and to get an item at position \(i\), we just have to add i * sizeof(int). A simple math operation is virtually instant. AddressOf( i ) = AddressOf( 0 ) + i * sizeof( dataType )
      Cons:
      • Resizing a dynamic array is costly: Any time our array is full, to resize it we have to allocate memory for a new array of a bigger size. Then, we have to copy each element from the old array to the new array. This takes time any time resize occurs.
      • We have to over-estimate the array size: We don't want to have to resize the entire array every time a new item is inserted, so we generally will resize it by some quantity each time (not just adding 1 to the size of the array). This means we're allocating more space than we're actually using if the array isn't full.

      The study of Data Structures is all about the trade-offs between different data types: Some are faster to access data but slower to insert new data and vice versa. Instead of storing data in an array-based structure, we can make a tradeoff of less memory used, instant insertion of new data in exchange for slower access using a linked structure.

      A dynamic array:

      reading_u14_LinkedList_dynamicarray.png

      A linked list:

      reading_u14_LinkedList_linkedlist.png


  • Building a Linked List
    • Anatomy of a Node class

      reading_u14_LinkedList_nodes.png

      The Node is one of the two structures we build when implementing a linked list or other linked type structure. The node is responsible for storing the data itself, and storing one or more pointers.


      Singly-linked List's Nodes

      reading_u14_LinkedList_singly-linked-nodes.png

      With a singly-linked list, the nodes only point to the next Node after them. These nodes don't know what comes before each of themselves.

      SinglyLinkedNode  
      - data : TYPE
      - ptrNext Node<TYPE>*

      Doubly-linked List's Nodes

      reading_u14_LinkedList_doubly-linked-nodes.png

      With a doubly-linked list, the nodes point to both the next Node and the previous node.

      DoublyLinkedNode  
      - data : TYPE
      - ptrPrev Node<TYPE>*
      - ptrNext Node<TYPE>*

      Warning! Remember that any class with pointers in it should be setting those pointers to nullptr in its constructor!


      A Node is simple enough that you could implement it as a struct instead of a class, but it's up to you. A Node is only used by the List (or linked structure) itself, the user wouldn't be accessing the Node or know about it.

      A doubly-linked Node:

      template <typename T>
      struct Node
      {
      public:
          Node();
      
          Node<T>* ptrNext;
          Node<T>* ptrPrev;
      
          T data;
      };
      

      A singly-linked Node:

      template <typename T>
      struct Node
      {
      public:
          Node();
      
          Node<T>* ptrNext;
      
          T data;
      };
      

      Single vs. Double? I generally prefer to teach how to implement a doubly-linked list first, because the functionality is easier to implement when each Node is aware of what comes before itself.

      Once you understand how a doubly-linked list works, you can figure out a singly-linked list; it just means having to iterate through the list more often.


    • Anatomy of a List class

      reading_u14_LinkedList_list-and-node.png

      The List class itself is what handles interfacing with the "user" (or other programmers using your List). Data Structures generally will have Access, Add, Remove, and Search functionality, and those functions (and helper functions) are stored in our List.

      The List will also keep track of the first Node in the list, and often also the last Node by storing Node* pointers. (You could get by without the last Node pointer, but it's just easier, dude.)

      The list is also responsible for the memory management - it will allocate memory for a new Node when it's needed, and free the memory of a Node when it's to be removed.

      reading_u14_LinkedList_delete-node.png

      Generally, a List will have functionality like:

      DoublyLinkedList  
      - ptrFirst : Node<TYPE>*
      - ptrLast : Node<TYPE>*
      - itemCount : int
      + DoublyLinkedList()  
      + ~DoublyLinkedList()  
         
      + PushFront( newData: T ) : void
      + PopFront() : void
      + GetFront() : TYPE&
         
      + PushBack( newData: T ) : void
      + PopBack() : void
      + GetBack() : TYPE&
         
      + PushAt( index: int, newData: T ) : void
      + PopAt( index: int ) : void
      + GetAt( index: int ) : TYPE&

      The Push functions are to add new items. There are three varieties: Add to the start of the list (PushFront), Add to the end of the list (PushBack), and Add to somewhere in the middle of the list (PushAtIndex).

      The Pop functions are to remove items.

      And the Get functions are to access data stored in Nodes.

      Depending on the design of the List, sometimes the "AtIndex" functions won't be implemented.

      Exapmle Linked List declaration:
      template <typename T>
      class LinkedList
      {
      private:
          Node<T>* ptrFirst;
          Node<T>* ptrLast;
          int itemCount;
      
      public:
          LinkedList();
          ~LinkedList();
      
          void PushFront( T newData );
          void PushBack( T newData );
          void PushAtIndex( int index, T newData );
      
          void PopFront() noexcept;
          void PopBack() noexcept;
          void PopAtIndex( int index );
      
          T& GetFront();
          T& GetBack();
          T& GetAtIndex( int index );
      
          void Clear();
          bool IsEmpty();
          int Size();
      };
      

    • Functionality of a Doubly-Linked List

      reading_u14_LinkedList_list-juggle.png

      We're going to look at the way functions work, assuming we're using doubly-linked Nodes. This is because it's more straightforward, and once you understand how doubly-linked lists work, you can figure out how a singly-linked list works (it just means more traversing).


      • Constructor and destructor
        • Constructor

          reading_u14_LinkedList_emptylist.png

          First and foremost - remember to set your pointers to nullptr! You will also want to set your m_itemCount to 0.


        • Destructor

          The destructor should make sure that all memory is freed when the LinkedList is destroyed. I've moved this functionality into the Clear() function, so just make sure to call Clear() within the destructor.


      • Walking the list

        reading_u14_LinkedList_initiallist.png

        The STL List only allows you to access items at the front and back of the list, but with our list we are going to also be able to work with the middle - but to do so, we need to add some logic to walk the list. All of the "At" functions below will use this functionality to get to a certain index.

        reading_u14_LinkedList_walker.png

        First, we need to create a Node pointer. We aren't going to allocate space for this pointer, we will just use it to point at each item we're currently investigating.

        When we want to get to somewhere, we will have an index position. This will be our goal location.

        We will need to create a counter variable as well to count how many steps we've taken to that position.

        Node<TYPE>* walker = this->ptrFirst; // Point to list's first item
        int counter = 0;
        

        reading_u14_LinkedList_walkA.png

        We begin by pointing our walker Node pointer to wherever the list's ptrFirst is pointing to, and we set our counter to 0.

        while counter is not the index we want… walk forward. To walk forward, we set our walker node to point to the current location's ptrNext.

        while ( counter != index )
        {
          walker = walker->ptrNext;
        }
        

        reading_u14_LinkedList_walkB.png

        We keep moving forward…

        reading_u14_LinkedList_walkC.png

        …And once our loop ends, we have arrived at the index desired.


        Keep this functionality in mind as we continue, it will be needed for the "PushAt", "PopAt", and "GetAt" functions.


      • Adding data

        The Push functions are to add data to our list. We will be passing in some data and it will be added at the front, back, or maybe in the middle.

        We're going to have slightly different instructions to follow based on whether our list is starting out empty, or if we already have some items stored in it, so make sure to take notice of the multiple scenarios.


        • Adding data to an EMPTY LIST

          An empty list is one that has its ptrFirst and ptrLast both pointing to nullptr.

          reading_u14_LinkedList_emptylist.png

          First, we allocate space for a new Node and set its data:

          reading_u14_LinkedList_newnode.png

          Then, we update the LinkedList's pointers - the new node is now our new first and last item:

          reading_u14_LinkedList_newnode2.png


        • PushFront - Add a new item to the beginning of the list

          Scenario 1: If the list is currently empty, then follow the "Adding data to an EMPTY LIST" steps above.

          Scenario 2: If the list contains at least one item, then follow this.

          First, the list begins in a state with nodes already stored:

          reading_u14_LinkedList_initiallist.png

          Next, we create a new node and set its data:

          Node<TYPE>* newNode = new Node<TYPE>;
          newNode->data = data_from_parameter;
          

          Then we update the pointers:

          reading_u14_LinkedList_pushfront.png

          • The newNode's ptrNext is the list's current ptrFirst.
          • The list's current ptrFirst's new ptrPrev is the newNode.
          • The list's new ptrFirst is the newNode.

        • PushBack - Add a new item to the end of the list

          Scenario 1: If the list is currently empty, then follow the "Adding data to an EMPTY LIST" steps above.

          Scenario 2: If the list contains at least one item, then follow this.

          First, the list begins in a state with nodes already stored:

          reading_u14_LinkedList_initiallist.png

          Next, we create a new node and set its data:

          Node<TYPE>* newNode = new Node<TYPE>;
          newNode->data = data_from_parameter;
          

          Then we update the pointers:

          reading_u14_LinkedList_pushback.png

          • The newNode's ptrPrev is the list's current ptrLast.
          • The list's current ptrLast's new ptrNext is the newNode.
          • The list's new ptrLast is the newNode.

        • PushAt - Add a new item to the middle of the list

          Scenario 1: If the list is currently empty, then follow the "Adding data to an EMPTY LIST" steps above.

          Scenario 2: If the list contains at least one item, then follow this.

          First, the list begins in a state with nodes already stored:

          reading_u14_LinkedList_initiallist.png

          Next, we create a new node and set its data:

          Node<TYPE>* newNode = new Node<TYPE>;
          newNode->data = data_from_parameter;
          

          Now we need to walk the list to find the proper location:

          reading_u14_LinkedList_pushat2.png

          Then we update the pointers:

          reading_u14_LinkedList_pushat.png

          The walker node will be pointing at the item that will go before the newNode, so we can use it to adjust pointers. I also like to make some temporary pointers to "previous" and "next" to keep the code clean.

          Node<TYPE>* previous = walker;
          Node<TYPE>* next = walker->ptrNext;
          
          newNode->ptrPrev = previous;
          newNode->ptrNext = next;
          
          next->ptrPrev = newNode;
          previous->ptrNext = newNode;
          

          If you didn't create these "aliases" for the previous/next nodes, it would look like this instead:

          newNode->ptrPrev = walker;
          newNode->ptrNext = walker->ptrNext;
          
          walker->ptrNext->ptrPrev = newNode;
          walker->ptrNext = newNode;
          

          reading_u14_LinkedList_node-parade.png


      • Removing data

        We will also be able to remove data from the front, back, or in the middle of our list. We will again have two scenarios: If we're removing the last item in the list, or if there is more than one item in the list.

        We will need an error check as well: If the list is empty, we can't remove any data!


        • Removing the LAST ITEM in the list

          A list with 1 item remaining in it means that its ptrFirst and ptrLast are both pointing at the same node.

          reading_u14_LinkedList_lastitem.png

          In this case, we delete the node via one of these pointers:

          delete this->ptrFirst;
          

          And then we reset the list's ptrFirst and ptrLast to point to nullptr.

          reading_u14_LinkedList_emptylist.png


        • PopFront - Remove the item at the front of the list

          Scenario 1: If there's only 1 item in the list, then follow the "Removing the LAST ITEM in the list" steps above.

          Scenario 2: If the list has more than 1 item in it, then follow this.

          First, the list begins in a state with nodes already stored:

          reading_u14_LinkedList_initiallist.png

          We want to assign a new ptrFirst - whatever node is after the first one becomes the new first:

          this->ptrFirst = this->ptrFirst->ptrNext;
          

          Then we can delete the "old first" item, and set the "new first"'s ptrPrev to nullptr:

          delete this->ptrFirst->ptrPrev;
          this->ptrFirst->ptrPrev = nullptr;
          

          reading_u14_LinkedList_popfront.png


        • PopBack - Remove the item at the end of the list

          Scenario 1: If there's only 1 item in the list, then follow the "Removing the LAST ITEM in the list" steps above.

          Scenario 2: If the list has more than 1 item in it, then follow this.

          First, the list begins in a state with nodes already stored:

          reading_u14_LinkedList_initiallist.png

          We want to assign a new ptrLast - whatever node is before the last one becomes the new last:

          this->ptrLast = this->ptrLast->ptrPrev;
          

          Then we can delete the "old last" item, and set the "new last"'s ptrNext to nullptr:

          delete this->ptrLast->ptrNext;
          this->ptrLast->ptrNext = nullptr;
          

          reading_u14_LinkedList_popback.png


        • PopAt - Remove an item from the middle of the list

          Scenario 1: If there's only 1 item in the list, then follow the "Removing the LAST ITEM in the list" steps above.

          Scenario 2: If the list has more than 1 item in it, then follow this.

          First, the list begins in a state with nodes already stored:

          reading_u14_LinkedList_initiallist.png

          We need to use a walker to get to the index that we want to remove. Then, we can update the item before and after the walker to point to each other.

          With aliases:

          Node<T>* before = walker->ptrPrev;
          Node<T>* after = walker->ptrNext;
          before->ptrNext = after;
          after->ptrPrev = before;
          

          Without aliases:

          walker->ptrPrev->ptrNext = walker->ptrNext;
          walker->ptrNext->ptrPrev = walker->ptrPrev;
          

          We then delete the Node at the walker location and we're done.

          reading_u14_LinkedList_popat.png

          reading_u14_LinkedList_new-node2.png


      • Accessing data
        • GetFront - Retrieving the data at the beginning of the list

          Return the data stored in the ptrFirst node.

        • GetBack - Retrieving the data at the end of the list

          Return the data stored in the ptrLast node.

        • GetAt - Retrieving the data in the middle of the list

          Use a walker to get to a certain index, and return the data at the walker node.


      • Helper functions
        • Clear
          void Clear();
          

          The Clear function will be responsible to freeing the memory of all the nodes. You can implement this function by writing a while loop that continues looping while there are still items in the list, calling a Pop function until it is finally empty.

          while ( !IsEmpty() )
          {
              PopFront();
          }
          
        • IsEmpty
          bool IsEmpty();
          

          This should simply return true if itemCount is 0. Otherwise, return false.

        • Size
          int Size();
          

          Return the value of itemCount.

UNIT 15: Stack and Queue Structure

πŸ“–οΈ Reading - Stacks and Queues (U15.READ)

  • Queues

    reading_u15_StackAndQueue_Queue_queue.png

    • What are Queues?

      Queues are a type of structure that only allows new items to be added to the end of the queue, and only allows items to be removed at the beginning of the queue. It is commonly called FIFO: First In First Out. A queue is basically any line you have to stand in at a store.

      reading_u15_StackAndQueue_Stack_groceryline.png

      Remember that a data structure will have functionality to add, remove, and access (and sometimes search) the structure, but some data structures are for special types of applications. A queue has add, remove, and access, but these are all restricted - you can't insert in the middle or beginning of the queue, or remove from the middle or end of the queue.

      A Queue, in and of itself, is just an idea. We can use our dynamic array structure or our linked list structure to build a Queue, and only allowing certain functions to be called and others to be unavailable. We could simply do this by writing a Queue class that inherits from or contains a LinkedList or Vector, and only providing access to certain methods.

      Since a Queue only has one place items can be added, we would only have a simple Push function instead of having a PushFront and PushBack, and same for the rest of the functions.

      Available functionality:

      Vector List Queue Stack
      PushFront PushFront - -
      PushBack PushBack Push Push
      PushAt - - -
      PopFront PopFront Pop -
      PopBack PopBack - Pop
      PopAt - - -
      GetFront GetFront Front -
      GetBack GetBack - Top
      GetAt - - -
      • The Stack and Queue only has a PushBack function, so we just call it "Push" because it's the only one.
      • The Queue only removes items from the front, so it has PopFront… but since it's the only Pop function we just say "Pop".
      • The Stack only removes itesm from the back, so it has PopBack… but we just call it "Pop".
      • The Queue only allows us to access the front-most item… Instead of being called "Get", this is usually labeled as "Front" or "GetFront".
      • The Stack only allows us to access the "top-most" item… We usually label this as "Top".

      For the most part we've been visualizing our arrays and structures horizontally, from left to right:

      Value: "Cats" "Dogs" "Mice" "Birds" "Snakes" ""
      Index: 0 1 2 3 4 5

      With a Stack, we think of it as having a Top and a Bottom. For the way we're working with Stacks in my class, "Front" will be "Bottom" and "Back" will be "Top".

      We will cover Stacks in another chapter, but it is a cousin of Queues - they also have restricted access, but they're LIFO (Last In First Out).

      reading_u15_StackAndQueue_Queue_stackqueue.png


    • Adding to a Queue (Push/Enqueue)

      The Queue will add items using PushBack functionality, adding each item to the end of the list. Let's say we're running the following commands in order:

      1. Push("A");
      2. Push("B");
      3. Push("C");
      4. Push("D");

      We start off with an empty Queue:

               
      0 1 2 3 4
      FRONT       BACK

      After Push("A");, the item enters the BACK and makes its way to the first available spot closest to the front - in this case, the front itself:

      "A"        
      0 1 2 3 4
      FRONT       BACK

      Next, Push("B");, which also enters from the BACK and goes to the next available spot:

      "A" "B"      
      0 1 2 3 4
      FRONT       BACK

      With each item filling in from left-to-right:

      "A" "B" "C"    
      0 1 2 3 4
      FRONT       BACK

      And finally:

      "A" "B" "C" "D"  
      0 1 2 3 4
      FRONT       BACK

    • Removing from a Queue (Pop/Dequeue)

      When we remove items from the Queue, the front item will be removed, and each item will be shifted one space toward the front.

      1. Pop();
      2. Pop();
      3. Pop();

      Let's say this is our starting Queue state:

      "A" "B" "C" "D"  
      0 1 2 3 4
      FRONT       BACK

      After our first Pop() the item at the front of the Queue ("A") is removed and everybody else moves forward:

      "B" "C" "D"    
      0 1 2 3 4
      FRONT       BACK

      The second pop then removes "B":

      "C" "D"      
      0 1 2 3 4
      FRONT       BACK

      The the third pop removes "C":

      "D"        
      0 1 2 3 4
      FRONT       BACK

    • Access with a Queue (Get/Peek)

      When we call the function to access from a Queue, we can only access one item: the front-most item.

      1. Front()

      The Front function is our GetFront() equivalent. This is how we get the next item to work with in our program. If we were thinking of customers in a store line, while checking out the current customer, we would be using the "queue.Front()" to work with that customer until they're done. (Then they get removed with Pop()).

      "A" "B" "C" "D"  
      0 1 2 3 4
      FRONT       BACK

      For this queue, accessing queue.Front() would return "A".


    • Use of a Queue in software

      With this single-minded, "access the front item of the Queue, then discard that and access the next front item", what is a Queue structure used for?

      Similarly to in real life, it's a way to make items wait their turn for some form of processing. At the grocery store, there are limited resources (cash registers). At each register, people queue up, waiting for their time until they can go through the process of having their items rung up and paying for them. And, we wait in a "first come, first served" order (people will generally get angry if you try to enter at the front of the line).

      The same is true of a Queue. Perhaps we have many different things we want to process, but a limited amount of resources. So, as these items come in, if we're busy processing something, everything else queues up and waits its turn.

      reading_u15_StackAndQueue_Queue_boxqueue.png


    • Implementing a Queue

      A Queue will be simplest to implement if you build the class on top of an existing structure. If your underlying Array or List has PushBack, PopFront, and GetFront functions, you can set it up like this:

      Array version

      template <typename T>
      class ArrayQueue
      {
      public:
        void Push(const T& newData );
        void Pop();
        T& Front();
        int Size();
        bool IsEmpty();
      
      private:
        SmartDynamicArray<T> m_vector;
      };
      

      This one contains m_vector.

      • The Push function will call m_vector.PushBack( newData );
      • The Pop function will call m_vector.PopFront();
      • The Front function will call m_vector.GetFront();

      Linked version

      template <typename T>
      class LinkedQueue
      {
      public:
        void Push(const T& newData );
        void Pop();
        T& Front();
        int Size();
        bool IsEmpty();
      
      private:
        LinkedList<T> m_list;
      };
      

      This one contains m_list.

      • The Push function will call m_list.PushBack( newData );
      • The Pop function will call m_list.PopFront();
      • The Front function will call m_list.GetFront();

  • Stacks

    reading_u15_StackAndQueue_Stack_stack.png

    • What are Stacks?

      reading_u15_StackAndQueue_Stack_stackofchips.png

      Stacks are another type of restricted-access data structure. In a way, it's a cousin to a Queue, since they both only allow access to certain items stored within and are used for specialized operations.

      The Stack, however, is LIFO: Last In First Out. The stack can be visualized as a can of Pringles chips, where you only have access to whatever is on the top of the internal stack of chips.

      Of course, we don't usually think of arrays or linked lists in a vertical manner, so it might also help to see the stack structure like this:

      reading_u15_StackAndQueue_Stack_horizstack.png

      While similar to a Queue, which allows add data to the back and accessing/removing data from the front, a Stack only allows adding, accessing, and removing data all from the "top".

      Queue Stack
      reading_u15_StackAndQueue_Stack_queueA.png reading_u15_StackAndQueue_Stack_stackA.png

      Just like with a Queue, we can implement a Stack on top of an array or linked structure, taking advantage of code we've already previously written to create our new data structure, the Stack:


    • Adding to a Stack (Push)

      The Stack will add items using PushBack functionality, adding each item to the top of the list. Let's say we're running the following commands in order:

      1. Push("A");
      2. Push("B");
      3. Push("C");
      4. Push("D");

      We start off with an empty Stack:

               
      0 1 2 3 4
      BOTTOM       TOP

      After Push("A");, we insert the "A" at the "TOP" (or the "back"), and it "falls down" as far as it can to the "BOTTOM" (or "front"):

      "A"        
      0 1 2 3 4
      BOTTOM       TOP

      Next, Push("B") will drop in "B" from the top, and it will land "on top of" "A".

      "A" "B"      
      0 1 2 3 4
      BOTTOM       TOP

      And so on, building a Stack of items:

      "A" "B" "C"    
      0 1 2 3 4
      BOTTOM       TOP
      "A" "B" "C" "D"  
      0 1 2 3 4
      BOTTOM       TOP

    • Removing from a Stack (Pop)

      When we remove items from the Stack, the top item will be removed.

      1. Pop()
      2. Pop()
      3. Pop()

      With the Pop function, we will only be able to access the "top-most" item in the stack. So, items are removed the same way they came in.

      Starting Stack:

      "A" "B" "C" "D"  
      0 1 2 3 4
      BOTTOM       TOP

      After first Pop():

      "A" "B" "C"    
      0 1 2 3 4
      BOTTOM       TOP

      After second Pop():

      "A" "B"      
      0 1 2 3 4
      BOTTOM       TOP

      After third Pop():

      "A"        
      0 1 2 3 4
      BOTTOM       TOP

    • Access with a Stack (Get/Peek)

      When we call the function to access from a Stack, we can only access one item: the top-most item.

      1. Top()

      The Top function is our GetBack() equivalent. The next item we work with is going to be the one sitting at the top of the pile. This also happens to be the "newest" item in a Stack.

      "A" "B" "C" "D"  
      0 1 2 3 4
      BOTTOM       TOP

      For this Stack, accessing stack.Top() would return "D".


    • Use of a Stack in software

      Stacks are a handy instruction in computer science, allowing us to essentially "pause" where we're at with something when we push a new task to the top of the stack, and simply*pop* that task off and return directly to the previous item. But how, in particular, are they applied?


      Screen navigation

      reading_u15_StackAndQueue_Stack_viewstack.png

      In a program that generally only displays one screen at a time - such as a video game, or a small computer device like a GPS - we can Push new view states onto a "view stack" any time we navigate to a new screen.

      Maybe the first screen of the program is the main menu (Push(mainMenu);). When the user clicks the "options" button, they go to the options menu (Push(options);). Then, they may click the "sound settings" tab, taking them to the sound settings menu (Push(soundSettings);).

      Once they're done adjusting the sound, they click the back button. Instead of having to write logic like "what comes before sound settings?" we simply Pop() the current view off the stack, which returns us to the options menu.


      Undo functionality

      Let's say you're typing some text, and behind-the-scenes, the computer is storing all the text you've typed in a buffer like this…

      "C" "a" "r" "s"  
      0 1 2 3 4
      BOTTOM       TOP

      But oops, you meant to type "Cats", not "Cars". The backspace button is essentially a Pop(), and each time you hit backspace, the "top-most" (i.e., most recent) keystroke is removed from the buffer. We hit backspace twice (Pop(); Pop();) and we have gone backwards twice:

      Pop()

      "C" "a" "r"    
      0 1 2 3 4
      BOTTOM       TOP

      Pop()

      "C" "a"      
      0 1 2 3 4
      BOTTOM       TOP

      Now as we type in our correction, each new keystroke is a Push() onto the buffer stack. We fix our error (Push("t"); Push("s");)…

      Push("t")

      "C" "a" "t"    
      0 1 2 3 4
      BOTTOM       TOP

      Push("s")

      "C" "a" "t" "s"  
      0 1 2 3 4
      BOTTOM       TOP

      Call stack

      A stack is utilized any time we make a function call.

      When debugging, you should notice that there is a Call Stack pane, which lists all the functions that have been called, leading up to where the program is currently paused (when using breakpoints).

      reading_u15_StackAndQueue_Stack_lab11-callstack2.png

      The top-most item in the call stack is the most recently called function. The bottom-most is the first function called (such as main().)

      When a function is called, it is Pushed onto the call stack. When the function ends, it is Popped off of the call stack, returning us to whatever previous function was active before it.


    • Implementing a Stack

      Same as with the Queue, we can also build a Stack on top of an Array or Linked List structure. In this case, we use PushBack, PopBack, and GetBack for these classes in order to implement our Stack's Push, Pop, and Top.

      Array version

      template <typename T>
      class ArrayStack
      {
      public:
        void Push(const T& newData );
        void Pop();
        T& Top();
        int Size();
        bool IsEmpty();
      
      private:
        SmartDynamicArray<T> m_vector;
      };
      

      This one contains m_vector.

      • The Push function will call m_vector.PushBack( newData );
      • The Pop function will call m_vector.PopBack();
      • The Top function will call m_vector.GetBack);

      Linked version

      template <typename T>
      class LinkedStack
      {
      public:
        void Push(const T& newData );
        void Pop();
        T& Top();
        int Size();
        bool IsEmpty();
      
      private:
        LinkedList<T> m_list;
      };
      

      This one contains m_list.

      • The Push function will call m_list.PushBack( newData );
      • The Pop function will call m_list.PopBack();
      • The Top function will call m_list.GetBack();

πŸ§‘β€πŸ”¬ Lab - Linked lists, stacks, and queues (U15.LAB)

pixel-goals.png Goals:

  • Implement a Linked List
  • Implement a Stack
  • Implement a Queue
  • Look at the data structures in use in a program

pixel-turnin.png Turn-in:

  • You'll commit your code to your repository, create a merge request, and submit the merge request URL in Canvas. (Instructions in document.)

pixel-practice.png Practice programs and graded program:

  • This assignment contains several "practice" program folders, and a "graded" program folder. Only the "graded" program is required.
  • The practice programs are there to help you practice with the topics before tackling the larger graded program. If you feel confident in the topic already, you may go ahead and work on the graded program. If you feel like you need additional practice first, that's what the practice assignments are for.
  • The graded program assignment contains unit tests to help verify your code.
  • You can score up to 100% turning in just the graded program assignment.
  • You can turn in the practice assignments for extra credit points.

pixel-fight.png Stuck on the assignment? Not sure what to do next?

  • Continue scrolling down in the documentation and see if you're missing anything.
  • Try skimming through the entire assignment before getting started, to get a high-level overview of what we're going to be doing.

  • Setup: Starter code and new branch

    For this assignment, I've already added the code to your repository.

    1. Pull the starter code:
      1. Check out the main branch: git checkout main
      2. Pull latest: git pull
    2. Create a new branch to start working from: git checkout -b BRANCHNAME

    The folder cs250_program is added:

    =^o.o^=
    
    

  • About the program

    lab_u14_LinkedList_program.png

    The main menu of the program contains several options:

    • [1] Shopazon Store
    • [2] Grillezy Sandwiches
    • [11] Smart dynamic array tests
    • [12] Linked list tests
    • [13] Stack tests
    • [14] Queue tests

    Options 1 and 2 are programs that utilize the data structures you'll be implementing. You don't need to implement the programs themselves, but you can look at their usage within the code.

    As you work on the data structures, run the automated tests to check your work. These automated tests generate output .html files with detailed information about the tests. Make sure to check these for information on why a test fails.

    lab_u14_LinkedList_tests.png

    The test file will be generated in your project directory, either Project_VisualStudio2022, Project_CodeBlocks, or Project_Makefile.

    lab_u14_LinkedList_testsfolder.png

    For this program the SmartDynamicArray is already provided and you will implement the LinkedList. There is also two variants of the Stack and the Queue: ArrayStack, LinkedStack, ArrayQueue, LinkedQueue. The array versions are already done, and you'll implement the linked versions on top of the LinkedList. More information later.


  • Implementing the data structures
    Data structure Path
    Smart Dynamic Array (already done) DataStructures/SmartDynamicArray/SmartDynamicArray.h
    Linked List DataStructures/LinkedList/LinkedList.h
    Queue (Array version, already done) DataStructures/Queue/ArrayQueue.h
    Queue (Linked version) DataStructures/Queue/LinkedQueue.h
    Stack (Array version, already done) DataStructures/Stack/ArrayStack.h
    Stack (Linked version) DataStructures/Stack/LinkedStack.h

    Refer to the textbook, slides, and class video for the implementation specifics.


  • Looking at the programs

    We will also be looking at the implementations of the Shopazon Store and Grillezy Sandwiches during class, so watch the archived lecture video if you miss it.


  • Turning in the assignment

    Screenshot: Before finishing up, run the automated tests and take a screenshot of all of your tests passing. Save the file somewhere as a .png.

    Back up your code: Open Git Bash in the base directory of your repository folder.

    1. Use git status to view all the changed items (in red).
    2. Use git add . to add all current changed items.
    3. Use git commit -m "INFO" to commit your changes, change "INFO" to a more descriptive message.
    4. Use git push -u origin BRANCHNAME to push your changes to the server.
    5. On GitLab.com, go to your repository webpage. There should be a button to Create a new Merge Request. You can leave the default settings and create the merge request.
    6. Copy the URL of your merge request.

    Turn in on Canvas:

    1. Find the assignment on Canvas and click "Start assignment".
    2. Select Upload file and upload your screenshot.
    3. Paste your GitLab URL into the Comments box.
    4. Click "Submit assignment" to finish.

    \newpage

WEEK 8 - MAR 4

UNIT 16: Intro to Trees

πŸ“–οΈ Reading - Intro to trees (U16.READ)

reading_u16_IntroToTrees_tree.png

  • Linear structures vs. tree structure

    reading_u16_IntroToTrees_node-parade.png

    Lists, Queues, and Stacks are all examples of linear structures. Now we will work with trees, which are hierarchical.

    reading_u16_IntroToTrees_tree-example2.png

    A tree structure could be used to model a filesystem, moves or progress in a game, a family tree. And with a binary search tree, you could store any kind of data and keep it organized as each new item is inserted.

  • Basic terminology

    A tree is a type of graph that is completely connected and has no cycles. A tree is made up of nodes/vertices and edges.

    reading_u16_IntroToTrees_tree-terminology.png

    • Node: A node is a point in a tree; in the code, it will be a structure that contains data.
    • Edge: An edge is a line that connects two nodes; in the code, it will be the pointer from one node to another.

    reading_u16_IntroToTrees_tree-terminology2.png

    • Root: Each tree will have exactly one root, which all other nodes branch out from. When drawing a tree, we will generally make it the top-most node. The root is the only node that has no parent, and every other node has exactly one parent.
    • Leaf: A leaf is a node with no children.

    reading_u16_IntroToTrees_tree-parent.png

    • Parent: In a tree, each node has exactly one parent, except for the root node, which has no parent. The parent of node \(n\) is the node directly "above" \(n\), on the path back towards the root.

    reading_u16_IntroToTrees_tree-children.png

    • Child: A node in a tree can have any amount of children. A child of node \(n\) are the nodes directly under \(n\), branching off from it.

    reading_u16_IntroToTrees_tree-siblings.png

    • Sibling: Nodes that are children of the same parent node are siblings of each other.

    reading_u16_IntroToTrees_tree-descendants.png

    • Ancestors: The ancestor of node \(n\) is any node between \(n\) and the root (including the root).
    • Descendants: The descendants of node \(n\) is any node derived from node \(n\), going all the way down to the leaves.

    reading_u16_IntroToTrees_tree-subtree.png

    • Subtree: The subtree of node \(n\) is a tree with node \(n\) as the root, derived from an original tree that contains \(n\). Each node in a tree is essentially the root of its own subtree.

    reading_u16_IntroToTrees_tree-height.png

    • Height of a node \(n\): The height of some node \(n\) is the amount of edges on the longest path from that node \(n\) to any descendant leaf.
    • Height of a tree: The height of a tree is the number of edges on the longest path from the root to a leaf.

    reading_u16_IntroToTrees_tree-depth.png

    • Depth of a node \(n\): The length of the path (amount of edges) from that node \(n\) to the root of the tree.

  • Types of trees

    reading_u16_IntroToTrees_tree-generic.png

    • General tree: A general tree contains a root and subtrees that branch off from that root. There are no restrictions on the way it must be filled, or how many children each node can have.
    • n-ary tree: An $n$-ary tree is a type of tree where each node can have no more than \(n\) children.

    reading_u16_IntroToTrees_tree-binary.png

    Binary tree:
    A tree where each node can have no more than 2 children. This means a node can have 0, 1, or 2 children.

    reading_u16_IntroToTrees_tree-binarysearchtree.png

    • Binary search tree: A binary seach tree is a binary tree that is sorted. For any given node, its left child has a lesser value, and its right child has a greater value.

    • Tree fullness, completeness, and balance

      reading_u16_IntroToTrees_tree-full.png

      • Full binary tree: A full binary tree is one where every node either has 0 children or 2 children.

      reading_u16_IntroToTrees_tree-complete.png

      • Complete binary tree: A complete binary tree is one where all nodes have 2 children, except possibly in the lowest level. Additionally:
        • In the 2nd-to-lowest level, if a node has children, then all its siblings to its left must have 2 children each.
        • If a node in the 2nd-to-lowest level has only one node, it must be a left child.
        • This mean the tree fills from left-to-right.

      reading_u16_IntroToTrees_tree-balanced.png

      • Balanced binary tree: A binary tree is height balanced when, for each node of the tree, each node's left subtree and right subtree differ in height by no more than 1 level.

  • Binary tree traversals

    reading_u16_IntroToTrees_traversal.png

    When working with a list, it's easy to just output all the contents from beginning-to-end and call it a day - simple! But how do you output the contents of a binary tree?


    • Preorder traversal

      reading_u16_IntroToTrees_traverse-preorder.png

      We begin at the root node. The algorithm here is:

      1. Display current node's value
      2. Traverse to the left subtree
      3. Traverse to the right subtree

    • Inorder traversal

      reading_u16_IntroToTrees_traverse-inorder.png

      We begin at the root node. The algorithm here is:

      1. Traverse to the left subtree
      2. Display current node's value
      3. Traverse to the right subtree

    • Postorder traversal

      reading_u16_IntroToTrees_traverse-postorder.png

      We begin at the root node. The algorithm here is:

      1. Traverse to the left subtree
      2. Traverse to the right subtree
      3. Display current node's value

  • Review questions:
    1. reading_u16_IntroToTrees_review-tree.png
      • Identify the leaves and the roots
    2. reading_u16_IntroToTrees_review-protoindoeuropean.png
      • Identify the children of Persian.
      • Identify the parent of French.
      • Identify the ancestors of Spanish.
      • Identify the descendants of German.
    3. reading_u16_IntroToTrees_review-heights1.png
      • Identify the height of this tree.
      • Identify the height of the Help menu subtree.
      • Identify the height of the Play menu subtree.
      • Identify the height of the Exit subtree.
    4. reading_u16_IntroToTrees_review-tree.png
      • Identify the Preorder traversal
      • Identify the Inorder traversal
      • Identify the Postorder traversal
  • πŸ”Ž Unit 16 Concept Intro - Intro to trees (U16.CIN)

    πŸ‹οΈ Unit 16 Exercise - Intro to Heaps (U16.EXE)

    UNIT 17: Binary Search Tree Structure

    πŸ“–οΈ Reading - Binary search trees (U17.READ)

    • Binary Search Trees

      binarysearchtree.png

      Binary search trees are a type of data structure that keep the data it contains ordered. The ordering process happens when a new piece of data is entered by finding a location for the data that adheres to the ordering rules. With a binary search tree, smaller values are stored to the left and larger values are stored to the right.

      This means that when we're searching for data, when we land at a node we can figure out whether to traverse left or right by comparing the node to what we're searching for.


      • Architecture of a Binary Search Tree

        reading_u17_BinarySearchTree_linkedlistdiagram.png

        With a Linked List, we need to implement a Node structure and a LinkedList class, where the Node stores the data and the LinkedList provides an interface for users to add, remove, and search for data and stores a pointer to the first and last elements.

        reading_u17_BinarySearchTree_binarysearchtreeB.png

        Similarly for a Binary Search Tree, we need another type of Node to store the data, as well as the BinarySearchTree structure that acts as an interface and keeps a pointer to the root node.

        We might also think of a Binary Search Tree as being ordered based on the data's key - some sort of unique identifier we assign to each node - and then containing additional data (a value) within the node.

        For example, if we create our Node with two templated types like this:

        reading_u17_BinarySearchTree_bstnode.png

        our key could be a unique lookup (e.g., "employee ID"), and the *data*/value could be another structure that stores more employee data (name, department, etc.).

        reading_u17_BinarySearchTree_bst-employees.png

        What's the significance of the hierarchy in this tree? Nothing, really - the point of a binary search tree is that we're assuming the keys we will be pushing into the tree will be in a somewhat random order, and using the BST structure will help keep things ordered and somewhat faster to search through.

        We could use a Binary Search Tree in another application where the order of the keys does have some significance, but that's all a design decision.


        BinarySearchTreeNode in C++:

        template <typename TK, typename TD>
        class Node
        {
        public:
            Node()
            {
                ptrLeft = nullptr;
                ptrRight = nullptr;
            }
        
            Node( TK newKey, TD newData )
            {
                key = newKey;
                data = newData;
                ptrLeft = nullptr;
                ptrRight = nullptr;
            }
        
            ~Node()
            {
                if ( ptrLeft != nullptr  ) { delete ptrLeft; }
                if ( ptrRight != nullptr ) { delete ptrRight; }
            }
        
            Node<TK, TD>* ptrLeft;
            Node<TK, TD>* ptrRight;
        
            TD data;
            TK key;
        };
        

        The node I've written here contains a key, which nodes will be ordered by, and data, which can contain more information.

        As with any structure utilizing pointers, the pointers should be initialized to nullptr in any constructors.

        The destructor here will trigger the deletion of any child nodes, creating a chain reaction to clean up the entire tree if the root node is deleted.


        BinarySearchTree in C++:

        template <typename TK, typename TD>
        class BinarySearchTree
        {
        public:
            BinarySearchTree();
            ~BinarySearchTree();
        
            // Basic functionality
            void Push( const TK& newKey, const TD& newData );
            bool Contains( const TK& key );
            TD& GetData( const TK& key );
            void Delete( const TK& key );
        
            // Traversal functions
            string GetInOrder();
            string GetPreOrder();
            string GetPostOrder();
        
            // Additional functionality
            TK& GetMinKey();
            TK& GetMaxKey();
            int GetCount();
            int GetHeight();
        
        private:
            // (more here)
        
        private:
            Node<TK, TD>* m_ptrRoot;
            int m_nodeCount;
        };
        

        A Binary Search Tree, just like other data structures, can store more functionality than this, or less if needed.

        There are additional private methods that would be implemented. This declaration is just showing the public (interface) methods and the private member variables. I will talk about the private helper methods in depth later.


      • Efficiency of a Binary Search Tree

        The Binary Search Tree ends up being a good compromise between choosing faster random access but slow search/insert/delete (like with a dynamic array) and faster inserts/deletes but slow search/access (like with a linked list).

        The Binary Search Tree ends up being slower than \(O(1)\) (instant) but faster than \(O(N)\) (going through the entire list, where \(N\) is the number of nodes in the tree).

        Structure Random access Search Insert Delete
        Dynamic array \(O(1)\) \(O(n)\) \(O(n)\) \(O(n)\)
        Linked list \(O(n)\) \(O(n)\) \(O(1)\) \(O(1)\)
        Binary search tree \(O(log(n))\) \(O(log(n))\) \(O(log(n))\) \(O(log(n))\)

        Why is this? By the nature of its tree structure, as we traverse the tree we're essentially cutting out half the tree each time we choose to go left or right. Halfing the nodes to search each cycle means we have the opposite of exponential growth: A logarithmic function.

        reading_u17_BinarySearchTree_growthdiagrams.png


      • The Binary Search Tree and Recursion

        Many of the BinarySearchTree functions will be recursive, starting at the root node and recursing down. Because of this, the Push function (and many others) that would actually do the work would look like this:

        void RecursivePush(
            TK newKey,
            TD newData,
            Node<TK, TD>* ptrCurrent );
        

        However, we don't want the user outside of the BinarySearchTree to have to call Push and pass in the tree's node. They shouldn't even have access to any Node objects…

        myTree.Push( 'a', "apple", ???? ); // What do I pass in?
        

        That's why we have the public Push function

        void Push( TK newKey, TD newData );
        

        And a private RecursivePush function

        void RecursivePush( TK newKey, TD newData, Node<TK, TD>* ptrCurrent );
        

        Where the user calls the public Push and that function makes the first call to RecursivePush, passing in the root node to begin operations on.

        void Push( TK newKey, TD newData )
        {
            RecursivePush( newKey, newData, m_ptrRoot );
        }
        

      • Functionality of a Binary Search Tree
        • The Constructor:
          emplate <typename TK, typename TD>
          BinarySearchTree<TK,TD>::BinarySearchTree()
          {
              m_ptrRoot = nullptr;
              m_nodeCount = 0;
          }
          

          The constructor of the BinarySearchTree should set the m_ptrRoot pointer to nullptr and initialize the m_nodeCount to 0.


        • The Destructor:
          template <typename TK, typename TD>
          BinarySearchTree<TK,TD>::~BinarySearchTree()
          {
              if ( m_ptrRoot != nullptr )
              {
                  delete m_ptrRoot;
              }
          }
          

          The destructor of the BinarySearchTree will check to see if m_ptrRoot is not null - if it's not null, we will free that memory with the delete command. Since our BinarySearchTreeNode destructor deletes its left and right children, we don't have to do anything special to clear out the whole tree - that will happen automatically.


        • Push:
          template <typename TK, typename TD>
          void BinarySearchTree<TK,TD>::Push( const TK& newKey, const TD& newData )
          {
              if ( Contains( newKey ) )
              {
                  throw runtime_error( "Key is not unique!" );
              }
              else
              {
                  RecursivePush( newKey, newData, m_ptrRoot );
              }
          }
          

          Remember that the Push function is just a public-facing starting point that will begin the recursive process by calling the RecursivePush function, starting at the root node.

          First, before starting the recursion, use the Contains method to see if the tree already contains the =newKey passed in.

          If this key is already in the tree, we'll have to decide how to handle that. In our example, we will just throw a runtime_error exception, because for this design we're assuming that each key is a unique identifier.

          If the key is not already in the tree, then we are going to call the RecursivePush function to get started. RecursivePush takes in the key, data, and a node, so we pass on the newKey and newData parameters and we start at m_ptrRoot.

          If the root is already storing some data, then we call RecursivePush, passing forward the newKey, newData, and the m_ptrRoot as the starting point.


          RecursivePush:

          Within the RecursivePush function, we are looking for a position to place our new node at. While we haven't found an appropriate position, we will continue recursing left or right to ensure our keys are sorted.


          Terminating cases:
          • If ptrCurrent is nullptr, then the node can be created here.

            ptrCurrent = new Node<TK,TD>(newKey, newData);
            m_nodeCount++;
            
          • If newKey is less than ptrCurrent's key, AND if ptrCurrent DOES NOT HAVE a left child, then we can create the new node to the left of ptrCurrent.

            ptrCurrent->ptrLeft = new Node<TK,TD>(newKey, newData);
            m_nodeCount++;
            
          • If newKey is greater than ptrCurrent's key, AND if ptrCurrent DOES NOT HAVE a right child, then we can create the new node to the right of ptrCurrent.

            ptrCurrent->ptrRight = new Node<TK,TD>(newKey, newData);
            m_nodeCount++;
            

          Recursive cases:
          • If the newKey is less than ptrCurrent's key, AND ptrCurrent HAS a left child, then we recurse to the left.

            // Recurse left
            RecursivePush(newKey, newData, ptrCurrent->ptrLeft);
            
          • If the newKey is greater than ptrCurrent's key, AND ptrCurrent HAS a right child, then we recurse to the right.

            // Recurse right
            RecursivePush(newKey, newData, ptrCurrent->ptrRight);
            

        • Contains
          template <typename TK, typename TD>
          bool BinarySearchTree<TK,TD>::Contains( const TK& key )
          {
              return RecursiveContains( key, m_ptrRoot );
          }
          

          The Contains method also is a public entry-point that will start the RecursiveContains method at the m_ptrRoot.

          RecursiveContains: RecursiveContains will look at the current node ptrCurrent. If the ptrCurrent's key matches the key we're searching for, then we'll return true. Otherwise, we will recurse left or right searching for the key. If we run out of nodes to search, then the key is not found and we return false.


          Terminating cases:
          • If ptrCurrent is nullptr, then we've run out of nodes to search - return false.
          • If ptrCurrent's key matches the key parameter, then we've found the key we're looking for - return true.

          Recursive cases:
          • If key is less than ptrCurrent's key, then recurse left.

            return RecursiveContains(key, ptrCurrent->ptrLeft);
            
          • If key is greater than ptrCurrent's key, then recurse right.

            return RecursiveContains(key, ptrCurrent->ptrRight);
            

        • GetData
          template <typename TK, typename TD>
          TD& BinarySearchTree<TK,TD>::GetData( const TK& key )
          {
              Node<TK, TD>* node = FindNode( key );
              if ( node == nullptr ) { throw runtime_error( "Key not found!" ); }
              return &(node->data);
          }
          

          The GetData function takes in a key and returns the data for the given node. If that node does not exist in the tree, then an exception will be thrown.


        • FindNode
          template <typename TK, typename TD>
          Node<TK, TD>* BinarySearchTree<TK,TD>::FindNode( const TK& key )
          {
              return RecursiveFindNode( key, m_ptrRoot );
          }
          

          The FindNode function is the starting point that will begin the RecursiveFindNode at the m_ptrRoot.

          RecursiveFindNode: RecursiveFindNode will search the tree for the key given, stopping only once found or once there are no more nodes to search.


          Terminating cases:
          • If ptrCurrent is nullptr, then we've run out of nodes to search - return nullptr.
          • If ptrCurrent's key matches the key parameter, then we've found the key we're looking for - return the ptrCurrent.

          Recursive cases:
          • If key is less than ptrCurrent's key, then recurse left.

            return RecursiveFindNode(key, ptrCurrent->ptrLeft);
            
          • If key is greater than ptrCurrent's key, then recurse right.

            return RecursiveFindNode(key, ptrCurrent->ptrRight);
            

        • GetMinKey
          template <typename TK, typename TD>
          TK& BinarySearchTree<TK,TD>::GetMinKey()
          {
              return RecursiveGetMin( m_ptrRoot );
          }
          

          The GetMinKey function starts the recursive search for the minimum key, starting at m_ptrRoot.


          RecursiveGetMin:

          Terminating cases:
          • If ptrCurrent is nullptr, throw a runtime_error.
          • If ptrCurrent has no left children, then we're at the min node. Return ptrCurrent's key.

          Recursive cases:
          • Recurse to the left.

            return RecursiveGetMin(ptrCurrent->ptrLeft);
            

        • GetMaxKey
          template <typename TK, typename TD>
          TK& BinarySearchTree<TK,TD>::GetMaxKey()
          {
              return RecursiveGetMax( m_ptrRoot );
          }
          

          The GetMaxKey function starts the recursive search for the maximum key, starting at m_ptrRoot.


          RecursiveGetMax:

          Terminating cases:
          • If ptrCurrent is nullptr, throw a runtime_error.
          • If ptrCurrent has no right children, then we're at the max node. Return ptrCurrent's key.

          Recursive cases:
          • Recurse to the right.

            return RecursiveGetMax(ptrCurrent->ptrRight);
            

        • GetHeight
          template <typename TK, typename TD>
          int BinarySearchTree<TK,TD>::GetHeight()
          {
              if ( m_ptrRoot == nullptr ) { return 0; }
              return RecursiveGetHeight( m_ptrRoot );
          }
          

          The GetHeight function starts recursively analyzing the heights of each left-subtree vs. each right-subtree in order to find the maximum height of the tree.


          RecursiveGetHeight:

          Execute the commands in the following order:

          1. If ptrCurrent is nullptr, return 0.
          2. Create two integer variables, leftHeight and rightHeight, and initialize them to 0.
          3. If ptrCurrent has a left child, then recurse to the left. Store the result in leftHeight, and add +1.

            leftHeight = RecursiveGetHeight(ptrCurrent->ptrLeft)+1;
            
          4. If ptrCurrent has a right child, then recurse to the right. Store the result in rightHeight, and add +1.

            rightHeight = RecursiveGetHeight(ptrCurrent->ptrRight)+1;
            
          5. If leftHeight is greater than rightHeight, then return the leftHeight.
          6. Otherwise, return the rightHeight.

        • GetInOrder
          template <typename TK, typename TD>
          string BinarySearchTree<TK,TD>::GetInOrder()
          {
              stringstream stream;
              RecursiveGetInOrder( m_ptrRoot, stream );
              return stream.str();
          }
          

          The GetInOrder function starts recursively building a stream of nodes of the tree, ordered in-order.


          RecursiveGetInOrder:

          Terminating case:
          • If ptrCurrent is nullptr, just return.

          The following lines will be executed as part of the recursive case:


          Recursive cases:
          • Recurse to the left child.

            RecursiveGetInOrder(ptrCurrent->ptrLeft, stream);
            
          • Add the ptrCurrent's key to the stream.

            stream << ptrCurrent->key;
            
          • Recurse to the right child.

            RecursiveGetInOrder(ptrCurrent->ptrRight, stream);
            

        • GetPreOrder
          template <typename TK, typename TD>
          string BinarySearchTree<TK,TD>::GetPreOrder()
          {
              stringstream stream;
              RecursiveGetPreOrder( m_ptrRoot, stream );
              return stream.str();
          }
          

          The GetPreOrder function starts recursively building a stream of nodes of the tree, ordered pre-order.


          RecursiveGetPreOrder:

          Terminating case:
          • If ptrCurrent is nullptr, just return.

          The following lines will be executed as part of the recursive case:


          Recursive cases:
          • Add the ptrCurrent's key to the stream.

            stream << ptrCurrent->key;
            
          • Recurse to the left child.

            RecursiveGetPreOrder(ptrCurrent->ptrLeft, stream);
            
          • Recurse to the right child.

            RecursiveGetPreOrder(ptrCurrent->ptrRight, stream);
            

        • GetPostOrder
          template <typename TK, typename TD>
          string BinarySearchTree<TK,TD>::GetPostOrder()
          {
              stringstream stream;
              RecursiveGetPostOrder( m_ptrRoot, stream );
              return stream.str();
          }
          

          The GetPostOrder function starts recursively building a stream of nodes of the tree, ordered post-order.


          RecursiveGetPostOrder:

          Terminating case:
          • If ptrCurrent is nullptr, just return.

          The following lines will be executed as part of the recursive case:


          Recursive cases:
          • Recurse to the left child.

            RecursiveGetPostOrder(ptrCurrent->ptrLeft, stream);
            
          • Recurse to the right child.

            RecursiveGetPostOrder(ptrCurrent->ptrRight, stream);
            
          • Add the ptrCurrent's key to the stream.

            stream << ptrCurrent->key;
            

    πŸ§‘β€πŸ”¬ Unit 17 Lab - Binary search tree structures (U17.LAB)

    Refer to the class videos and reading materials

    🧠 Unit 17 Tech Literacy - Building your resume and portfolio

    😺 Unit 16/17 Status Update - Trees (U17.SUP)

    WEEK 9 - MAR 11 - SPRING BREAK

    Take a break!

    \newpage

    WEEK 10 - MAR 18

    UNIT 17: Binary Search Tree Structure (continued)

    No new content, just working on this data structure more. You can move onto Hash Tables early if you'd like to.

    \newpage

    WEEK 11 - MAR 25

    UNIT 18: Hash Table Structure

    πŸ“–οΈ Reading - Hash tables (U18.READ)

    • Hash tables

      hash-table.png

      • Back to Arrays…

        So far we've looked at array-based structures and link-based structures and their tradeoffs in efficiency. Recall that:

        Arrays Linked Lists
        Access to an arbitrary index is instant - \((O(1))\) Access to an arbitrary index is \((O(n))\)
        Inserting is instant if the array isn't full, if the array is full, we need to resize and copy all of the data, which is \((O(n))\) Inserting is instant - \((O(1))\) - just creating a node and updating pointers.
        Searching is \((O(n))\) in an unsorted array - start at the beginning and search until the end. Searching is \((O(n))\) in an unsorted linked list - start at the beginning and search until the end.

        What makes an array so fast to access is that we do a simple math operation to find the address of a given index in the array:

        \(address_i = address_0 + i \cdot sizeof(data)\)

        With this next structure, the Hash Table (aka associative array, map, or dictionary), we're going to leverage the speed of an array, and also make searching essentially \(O(1)\) as well, with a little extra math.


      • Concepts
        • Key-value pairs

          Dictionaries are often thought of as "key-value pairs," where we have some data to store (this is the value), and we can look it up by its key (some searchable identifier).

          When designing a class around this, we can reuse some piece of information as the key, as long as it's going to be unique in the table.

          Employee ID (key) Employee object (value)
          1024 Name: Amina, Employee ID: 1024, Department: Dev
          2048 Name: Benita, Employee ID: 2048, Department: Sales
          1280 Name: Caiyun, Employee ID: 1280, Department: QA

          The key should be a datatype that can be easily put into a mathematical function - such as an integer. Strings can also be used, but a complex data type like a class wouldn't work very well as a key.

          The value can be any data type, usually a specific class that stores more data.

          Some examples of key-value uses are:

          • People lookup - Associating an employee ID, student ID, etc. (key) to an Employee or Student object (value).
          • Address book - Associating a phone number (key) with a contact entry (value).
          • Dictionaries - searching for a word (key), finding the definition (value).
          • Book lookup - Associating an ISBN (key) to a Book object (value).
          • Word counter - Associating each word (key) with the amount of times it has shown up in a document (value).

        • Hashing the Key to get the index

          With our Hash Tables, each element in the array will be associated with a key - similar to our Binary Search Trees. The key is a unique identifier, allowing us to access a set of data mapped to the key.

          reading_u18_hash-employees.png

          The way we do this is we use this key as the input of a function that converts the key into an index - somewhere from 0 (the beginning of the array) to \(n-1\) (the end of the array). This is known as the hash function. A simple hash function could be taking the key and using the modulus operator with the size of the array to get an index.

          reading_u18_hash-function.png

          Because of this hash function, the efficiency of both inserting and searching is \(O(1)\) - unless there are collisions, where multiple keys map to the same index. This also means that, with minimal collisions, the speed to insert/search is going to be near instantaneous whether we have 10 items or 10,000 items.


        • Hashing functions

          The simplest hashing function will just take the key and the size of the array and use modulus to find a resulting index:

          \[ Hash(key) = key \% ARRAY\_SIZE \]

          Using the modulus operator restricts the resulting indices so that they will be between 0 and ARRAY_SIZE-1. For example, if we had an array of size 5 and were generating indices, everything would be between 0 and 4:

          index 0 1 2 3 4
          key 0 1 2 3 4
            5 6 7 8 9
            10 11 12 13 14
            15 16 17 18 19

          Looking at the table above, if we had something with the key 5 and the key 10, this would cause a collision - they would both map to the index position 0. If we do encounter a collision, we will need a strategy to find a new index – more on that later.

          As an example of a hash function, let’s say we’re going to store employees in a Hash Table by their unique employee IDs. Employee IDs begin at 1000 (not 0 like the array index) and can be any 4-digit number. They’re not sequential. We want to assign an array index to each employee ID so we can quickly search for an employee by their ID. For this, we need a hash function. The simple hash function would look like this:

          // Input: employee ID integer (key)
          // Output: array index 
          int Hash(int employeeId) {
              return employeeId % ARRAY_SIZE;
          }
          

        • Collisions

          Sooner or later, a collision will occur, and you will need a way to take care of it - basically, to generate a different index than the one that our hash function gives us. There are several ways we can design our hash tables and collision strategies.

          • Linear Probing (Open Addressing)

            With linear probing, the solution to a collision is simply to go forward by one index and see if that position is free. If not, continue stepping forward by 1 until an available space is found.

            1. Initial hash table:

              index 0 1 2 3 4
              value   6 12    
            2. Insert new employee ID 16…

              H(16) = 16 % 5 = 1

              COLLISION: Index 1 is already taken!

            3. Move forward by 1…

              H(16) + 1 = (16 % 5) + 1 = (1) + 1 = 2

              COLLISION: Index 2 is already taken!

            4. Move forward by 1…

              H(16) + 2 = (16 % 5) + 2 = (1) + 2 = 3

              Index 3 is available!

              index 0 1 2 3 4
              value   6 12 16  

            As an example of a hash function, let’s say we’re going to store employees in a Hash Table by their unique employee IDs. Employee IDs begin at 1000 (not 0 like the array index) and can be any 4 digit number. They’re not sequential. We want to assign an array index to each employee ID so we can quickly search for an employee by their ID. For this, we need a hash function. The simple hash function would look like this:

            // Input: employee ID integer (key)
            // Output: array index
            int Hash( int employeeId )
            {
                return employeeId % ARRAY_SIZE;
            }
            

          • Quadratic Probing (Open Addressing)

            With quadratic probing, we will keep stepping forward until we find an available spot, just like with linear probing. However, the amount we step forward by changes each time we hit a collision on an insert.

            1. Initial hash table:

              index 0 1 2 3 4 5 6 7 8 9 10 11 12
              value   14 41     18              
            2. Insert new employee ID 27…

              H(27) = 27 % 13 = 1

              COLLISION: Index 1 is already taken!

            3. Collisions = 1, shift over by \(collisions^2\)…

              H(27) + 1^2 = (27 % 13) + 1^2 = (1) + 1^2 = 2

              COLLISION: Index 2 is already taken!

            4. Collisions = 2, shift over by \(collisions^2\)…

              H(27) + 2^2 = (27 % 13) + 2^2 = (1) + 2^2 = 5

              COLLISION: Index 5 is already taken!

            5. Collisions = 3, shift over by \(collisions^2\)…

              H(27) + 3^2 = (27 % 13) + 3^2 = (1) + 3^2 = 10

              Index 10 is available!

            Each time there's a collision, we search further from the original hashed index: +1 space, then +4 spaces, then +9 spaces.

            reading_u18_HashTable_collisions.png

            The probing function would look something like this:

            int QuadraticProbe( int originalIndex, int collisionCount )
            {
                return originalIndex + pow( collisionCount, 2 );
            }
            

          • Double Hashing (Open Addressing)

            With double hashing, we have two hash functions. If the first hash function returns an index that is already taken, then we use the second hash function on the key to generate an offset, instead of just using a linear or quadratic formula.

            With this method, our new index after a collision will be:

            \(newIndex=Hash(key)+collisionCountβ‹…Hash2(key)\)

            It is up to our design to figure out a second hash function, but generally it should also work with prime numbers. For example:

            int Hash( int key )
            {
            return key % ARRAY_SIZE;
            }
            
            int Hash2( int key )
            {
            return 7 - ( key % 7 );
            }
            

          • Additional considerations

            Index wrap-around Note that with all of these operations, we will also do an additional operation of index % ARRAY_SIZE to make sure the index stays within the bounds of the array.


            The problem of clustering Clustering is a problem that arises, particularly when using linear probing. If we’re only stepping forward by 1 index every time there’s a collision, then we tend to get a Dictionary table where all the elements are clustered together in groups.

            index 0 1 2 3 4 5 6 7 8 9
            value   A B C     X Y Z  

            Because of this, the more full the Hash Table gets, the less efficient it becomes when using linear probing.


            Sizing a Hash Table Array When we’re writing a hash function, we will want to take into account how likely a collision will be. A general design rule is that making your array size a prime number helps reduce collisions.

        • Hash Table Efficiency

          In an ideal world with no collisions, a Hash Table would have an \(O(1)\) growth rate for its Search, Insert, and Deletion no matter how many items it stores. However, since collisions may occur, the efficiency of the Hash Table can vary.

          Pros
          • Searching and Inserting is relatively quick: Since we use hash functions and collision functions to find the index to insert at and to access, it is just one math operation (at best) to find an index, and with collisions, hopefully just a few more calculations.
          • Mapping a key to a value is useful: With a normal Array vs. Linked List, there's not really additional logic that goes into the storage - we just need to store data and it is thrown into a structure. With a Hash Table, we can have a more intentional design for our data.
          Cons
          • Many things affect Hash Table efficiency: The efficiency of finding an index can vary based on the size of the array, how full it is, the quality of the hashing function and collision strategy, and even the data being stored in it.
          • A bad hash function can cause more collisions: Different hash functions can affect how many collisions occur, so different functions can cause different speeds.
          • They are difficult to resize: If we were to resize a hash table, not only would we have to copy all the data over (which would be \(O(n)\)), we would also need to rehash all of the keys to get new indices based on the new table size.

          Because resizing a hash table is expensive, if we can reasonably estimate the needed size of the hash table ahead of time, that only helps us out. If we under-estimate the size, that can be costly in having to resize things.


      • See Also

        You can find out about more hashing techniques by looking on Wikipedia.

        • Separate chaining
        • Coalesced hashing
        • Robin-hood hashing

    πŸ§‘β€πŸ”¬ Lab - Hash tables (U18.LAB)

    Refer to the class videos and reading materials

    \newpage

    WEEK 12 - APR 1 - SEMESTER PROJECT V1

    Project v1

    About:

    For version 1 of this project you will build out a basic program, similar to what we've seen in class, that contains a Program class with various menus. The program should also contain data structures to store sets of objects, and there should be 2 objects that model items in a problem domain. See below for information and requirements.

    • Timeline: You will work on v1 of the project for a week and then turn in your work as-is at that time. The instructor will assess and give feedback and a partial grade, then give additional requirements for v2. You will then have another week to work on the program and get in the final version of the project.
    • Grading: There is just one "Project" item in Canvas, and your grade on the project will be based on the final turned-in version.


    Coding requirements:

    • You should use one data structure in your program: Linked List or Binary Search Tree.
      • Use your own implementation for full credit.
      • Use an STL implementation of vector, list, or map for 90% credit.
    • There should be two classes to represent objects in some problem domain.
      • Examples: Store AND Product, Course AND Student.
      • The "Program" class doesn't count as being part of the problem domain.
    • The program should allow storage and maintenance on data relating to the problem domain models/classes.
      • Examples: Add course, remove course, update course, register student for course, drop student from course.
    • Program should contain search OR sort functionality; any search/sort algorithm may be used.
      • Examples: Filter student by grade letter, filter courses by department.
    • Exceptions should be utilized for error checking.
      • Examples: Invalid index provided, bad input provided, trying to work with an empty data structure.
    • Additional features may be requested for v2 of the project.

    Project ideas:

    You can choose one of these ideas to work on or design your own.

    1. A system for a college could include objects like: Campus, Building, Department, Employee, Student, Course, and so on.
      • Could create Courses and Students, then register Students for Courses.
      • Could create Courses and Buildings, then mark Courses dates/times/location to some Building and room.
      • Use Linked List or Binary Search Tree to store these sets of data.
    2. A system for an online store could include objects like: Store, Inventory, Product, Order, Return, Review, and so on.
      • An Order could include multiple Products. The order could have a status like "processing", "shipped", "delivered".
      • A Store could have multiple Products and you could edit the Stores and Products from your project program.
    3. A music streaming program could include objects like: Track, Playlist, Artist, Album, and so on.
      • An Album or a Playlist could have a list of Tracks associated with it. The user could add tracks to their playlist in the program.

    Project approval:

    Before working on your project you need to have the instructor review and sign off on your idea. This is mostly to make sure that you're staying in scope and not over-complicating the assignment.


    Helper code:

    cout << BLACK_ON_YELLOW;
    cout << "Hello" << endl;
    cout << CLEAR;
    
    • Program class:

    Build out your program within a Program class to simplify the implementation; data stored in the program should just be a member of the Program class, and menu functions can be added to the Program class to help streamline navigation of the program.

    Example Program.h:

    #ifndef _PROGRAM
    #define _PROGRAM
    
    class Program
    {
    public:
      void Run(); // Begins the program
    
      Program();
      ~Program();
    
      void Setup();
      void Teardown();
      void SaveData();
      void LoadData();
    
      void Menu_Main();
      void Menu_Course_Create();
      void Menu_Course_Update();
      void Menu_Course_Delete();
    
      void Menu_Student_Create();
      void Menu_Student_Update();
      void Menu_Student_RegisterForClass();
      void Menu_Student_DropFromClass();
    
    private:
      void PressEnterToContinue();
      int GetValidChoice( int min, int max );
    
      DoublyLinkedList<Course> courses;    // Problem domain object 1 is Course
      DoublyLinkedList<Student> students;  // Problem domain object 2 is Student
    };
    
    #endif
    

    WEEK 13 - APR 8 - (Teacher grading week/Student catch up week)

    (This class only) - A break week to catch up on labs and previous work before working on the semester project.

    I WILL STILL BE IN CLASS AND YOU CAN STILL COME TO CLASS TO ASK QUESTIONS AND WORK ON THINGS.

    WEEK 14 - APR 15 - SEMESTER PROJECT V2

    Project v2

    • About:

      For v2 of this project we will be cleaning up any issues with the v1 code and adding some additional features. Make sure to read any feedback in your v1 merge request for notes on what to fix up or clean. If you have any questions about your program, let the instructor know.


    • Assignment links:
    • Note:

      MAKE SURE TO READ THROUGH THE "Code and UI Style Guide" SECTION OF THE BOOK UNDER THE "Additional/Reference" AREA.


    • Requirements:
      • If any features/requirements were missing in v1, implement those now:
        • Error checking and handling - Program should not crash when given invalid input or when exceptions occur.
        • Two models - Two classes to represent items in the problem domain
        • Add/remove/update functionality on models
        • Search or sort functionality - This should be exposed as a feature for the user
      • Linking functionality - A relationship should be present between two models, if not already.
      • Display linked information - User should be able to see records from [MODELA] as well as any linked information from [MODELB].
      • Save/load data - Saving and loading program data; can save in a format like .csv or create your own parser for a .txt file.
      • Export report - Saving program data in a human readable format (.txt, .html, .md, etc.)
      • Documentation - Basic documentation should be provided, including:
        • Doc for general user:
          • List of features available in the program (e.g., add/update/remove student, register student for course, etc.)
        • Doc for client:
          • List of all data models and what attributes they have (e.g., Student has name, gpa).
        • Doc for tech lead:
          • Development postmortem: What went well? What could be improved for next time (future projects :)?
        • Documentation should be saved in your repository and there should at least be a PDF version available to read.
        • Documentation does not have to be very long!

    • Examples:

    WEEK 15 - APR 22 - (Teacher grading week/Student catch up week)

    (This class only) - A break week where I can grade the turned in projects so far.

    I WILL STILL BE IN CLASS AND YOU CAN STILL COME TO CLASS TO ASK QUESTIONS AND WORK ON THINGS.

    WEEK 16 - APR 29 - LAST WEEK OF CLASS

    Catch-up week, last chance to get your assignments in.

    I USE FINALS WEEK TO GRADE ALL REMAINING WORK AND I DO NOT ACCEPT LATE WORK DURING FINALS WEEK.

    WEEK 17 - MAY 7 - MAY 13 - FINALS WEEK

    No final exam for this course; make sure to do the exams that are posted on Canvas.

    Professors submit grades by Tuesday, May 14th.

    Grades available online to students by noon, Thursday, May 16th.

    \newpage

    Additional

    l1-about.png

    About

    About this book

    book-cute-center.png

    This "course book" contains the concept notes, assignment documentation, reference material, and other resources, all of which make up the author's Core C++ course. The goal is to be a one-stop location for all the resources you need for this course, and to give you something to keep for reference later on - in other classes, for review before a job interview, or just as a reference manual. I encourage you to write in this book, keep your notes in it, highlight it, and use it as your guide as you work through the course content.

    About this course

    about-course-center.png

    My course design ideals

    • I am interested in training you how to be a good software developer, which includes how the language works, designing software and good interfaces, diagnosing and fixing issues and bugs, and so on. I am not interested in making sure you're a cookie-cutter "best-student", which usually just translates to "a traditional student with the opportunity to give school 150% of their attention". I've been a non-traditional student, I've had to work full time my entire adult life, I understand that many of my students are also adults with jobs, families, and other responsibilities, and I am not interested in penalizing "non-traditional" students for not being able to make perfect attendance or get everything in by the due date 100% of the time.
    • I try to build my courses in a way like a video game - start by tutorializing and easy challenges, and as you get more experience you are faced with harder challenges, but everything's designed to help you build the skills you need to tackle everything.
    • I think that education should be free and available to everybody. (But also I need to pay bills so I am fortunate that I get to work as a teacher, which I enjoy doing.)
    • All the (good) course content I create is available online for free for anyone to access. (GitLab, my webpage, YouTube). Anything that I think you need to know to be successful will be available in some form online and outside of class; I'm not going to penalize you for not making it to class.
    • I want to help you build up your problem solving skills so I may not always answer your assignment questions directly (more in a guided, "here's a hint" way), but I am always here to answer questions and help guide you to where you need to go.
    • I miss teaching from home where I could wear sweat pants all day. We should all be able to do what we do while being comfy in sweat pants or whatever.

    About the author

    rachel1.png

    I've been in the KC Metro area since I was around 11 years old. I started programming because I was in love with video games, and I spent my free time in my teen years (badly) programming video games. :)

    \vspace{0.2cm}

    Education: Homeschooled, GED, Associates in Computer Science from MCCKC, Bachelors in Computer Science from UMKC, dropped out of gradschool multiple times (UMKC, MS&T, KU) cuz money/time.

    \vspace{0.2cm}

    Development Work: Mostly C# (web development) and C++ (software development) professionally, also with PHP, HTML, CSS, JavaScript. Independent mobile app development for Android, independent game development with Lua or C++.

    \vspace{0.2cm}

    Teaching Work: Tutoring math @ MCCKC (2004), creating programming tutorials on YouTube (since 2008), adjunct teacher @ UMKC (2013, various other years), adjunct teacher @ JCCC (2016-2018), full time teacher @ JCCC (2020 - current).

    \vspace{0.2cm}

    Free time things: Playing video games, making video games, studying human languages, gardening, arts and crafts, journaling and organization, animating, video editing, playing piano, writing music, TTRPGs, drinking coffee, making zines, volunteering.

    \vspace{0.2cm}

    Other names: In the work system I'm R.W., but my full name is Rachel Wil Sha Singh. Around many friends I go by "Moose". I am nonbinary and go by various names in various contexts. I relate more to computers than I do to genders.

    \vspace{0.2cm}

    Links 'n' stuff:

    \newpage

    Reference

    Basic Computer Skills

    • Basic computer skills

      How to take a screenshot

      computer_skills_printscreen.jpg

      In Windows
      Press PRINTSCREEN on your keyboard, then go to an image editing application (Paint, if you have nothing else) and use EDIT > PASTE to paste the image in.
      In Mac
      CTRL+Z, COMMAND+SHIFT+3 to take a screenshot in Mac
    • Navigating the filesystem

      Identifying file extensions

      You should be aware of the extensions of our files. You will need to be able to identify source code files, such as .cpp and .h files for C++, or .py files for Python. Additionally, most executable (runnable) files end with the .exe extension in Windows.

      Turning on viewing file extensions in Windows

      computer_skills_extensions.png

      By default, Windows hides file extensions from you. However, it is important to know what kind of files you're working with in a programming class. To view file extensions, search for "File Explorer Options". Select the "View" tab, and UNCHECK the option "Hide extensions for known file types". Hit OK when done.

      Project working directory

      Visual Studio: computer_skills_workingdir_visualstudio.png

      Code::Blocks: computer_skills_workingdir_codeblocks.png

      It is important to keep in mind that the default working directory of our programs is the location where the project file is. For Visual Studio, that's a .vcxproj file. For Code::Blocks, that's a .cbp file.

      Navigating quickly

      Selecting multiple files with CTRL or SHIFT

    • Editing text

      Cut (CTRL+X), copy (CTRL+C), paste (CTRL+V)

      Double-click to highlight word

      CTRL+arrow keys to scroll a word at a time

      CTRL+HOME/CTRL+END to go to start/end of a document

      SHIFT to select regions of text

    • IDE tips and tricks

      Splitting code views

      Go to declaration

      Go to definition

      Find all references

      Refactor - Rename


    Code and UI Style Guide

    • Repository management
      Your merge request should only contain files related to the current feature or project you're working on
      It shouldn't contain code from multiple assignments.

    • User Interface design
      • Prompts before inputs

        Before using an input statement (e.g., cin), use an output statement (e.g., cout) to display a message to the user. Make sure you're clear about what kind of data you're expecting them to enter.

        No:

        _
        

        If you use an input statement it doesn't show anything on the screen, so it looks like your program has crashed.

        Yes:

        Please enter pet name: _
        

        Use an output statement to display a message before getting the user's input.


    • C++ Style Guide
      • Naming conventions
        Variables

        lowerCamelCase - Variable names should begin with a lower-case letter and each subsequent letter use an Upper Case, in a camel-case format.

        Additionally:

        • Variable names should be descriptive.
        • Avoid using single-letter variable names except with for loops.
        Member variables
        m_lowerCamelCase - Variables that belong to a class should be prefixed with m_ to signify "member".
        Functions
        CamelCase - Function names should begin with an upper-case letter and be in CamelCase form. In some cases, using an underscore might be okay to group like-functions together, if it makes things more clear.
        Classes
        CamelCase - Class names should begin with an upper-case letter and be in CamelCase form.

      • Best practices
        • Basics
          • You must have int main, not void main.
          • Don't put an entire program in main() - Split out the program into separate menu functions; ideally use a Program class that contains functions within it.
        • Naming conventions
          • Variable, function, and class names should be descriptive.
        • Arrays
          • Do not use a variable for an array's size; some compilers might support this but not all do. Traditional C arrays and STL arrays must have hard-coded integer literals or named constant integers as its size.
        • Functions
          • Functions should perform the minimal amount of operations - a function should have a specific purpose.
        • Structs and Classes
          • Structs should be used for structures that only contain a few amount of variables and no functions. Classes should be used for more sophisticated structures that contain functions and variables.
          • Prefer making member variables private with member functions as an interface for those items.
        • try/catch
          • The try{} statement should wrap the most minimal amount of code (NOT THE ENTIRE FUNCTION BODY!) - generally, try should wrap the one function that could throw an exception.
        • NEVER USE GLOBAL VARIABLES.
        • NEVER USE goto STATEMENTS.

    • Cross platform compatibility
      Treat everything (including files) as case-sensitive
      When using an #include statement for a file in your project, make sure your casing matches the file itself. E.g., if the file is Product.h, use #include "Product.h", not #include "product.h".
      Use #ifndef fileguards, not #pragma once
      Use the #ifndef _LABEL, #define _LABEL, #endif style of file guards as they are supported by more compilers.
      • Fileguards ONLY go in .h files.
      system("cls"); only works on Windows

      Prefer creating a cross-platform function like this instead:

      void ClearScreen()
      {
      #if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
        system( "cls" );
      #else
        system( "clear" );
      #endif
      }
      
      system("pause"); only works on Windows.

      Prefer creating a cross-platform function like this instead:

      // It's klugey but it's the simplest.
      void PressEnterToContinue()
      {
        cout << endl << "PRESS ENTER TO CONTINUE" << endl;
        string temp;
        getline( cin, temp );
        getline( cin, temp );
      }
      
      File guards
      Always use ifndef instead of pragma once for your file guards.

      Yes:

      #ifndef _MYFILE_H
      #define _MYFILE_H
      
      // My code
      
      #endif
      

      No:

      #pragma once
      
      // My code
      

    • File organization
      Function/class declarations
      Function and class declarations should go in a header (.h) file.
      Function definitions
      Function definitions should go in a source (.cpp) file, EXCEPT TEMPLATED FUNCTIONS (those all go in .h).
      Each class/struct should have its own set of .h and .cpp (as needed) files
      • Don't put multiple class declarations in one file.
      Function definitions must go in .cpp file
      Don't define class functions within the class or in the .h file.
      • Except for templated classes - Everything goes in the .h file for templated classes.
      Don't define structs/classes inside other classes
      Each struct/class should have its own .h file/set of .h and .cpp files.]
      Never include .cpp files!

      Includes should only include .h or .hpp header files.

      If a project has many files, separate them into folders
      Logically separate related files into subfolders, such as Exceptions, DataStructures, Menus, etc.

    • Clean code
      Erase unused codeblocks
      If you want to make sure to be able to reference old code later, back it up to GitLab, then delete the comment. Don't leave commented out code blocks in turn-in assignments.
      Be consistent with naming conventions
      Choose either UpperCaseFunctionNames or lowerCaseFunctionNames but don't interchange them. Same with variable and class naming. Be consistent.
      Whitespace
      Adequate whitespace should be used to differentiate logical sections of code. At the same time, don't use too much whitespace. The goal is to have readable code.
      If ( true ) return true; else return false;
      If you're writing a condition to check if something is true in order to return true, just return the result of the condition instead.

      No:

      if ( a == b )
        return true;
      else
        return false;
      

      Yes:

      return ( a == b );
      

      • Curly braces
        Follow the existing code base
        • If you're writing code from scratch you can follow whatever style you'd like as long as it is consistent.
        • When working with existing code-bases, use the same style as the existing code.
        • See https://en.wikipedia.org/wiki/Indentation_style for indentation styles

        Preferred style:

        if ( a == b )
        {
          DoThing();
        }
        

        GNU styling:

        if ( a == b )
          {
            DoThing();
          }
        

        K&R Styling ("JavaScript" style):

        if ( a == b ) {
          DoThing();
        }
        
        One-line contents
        Always surround your if statement and while loop contents within curly braces { } even though it's not required for one-line internals.

        No:

        if ( CONDITION )
          DoThingA();
        else if ( CONDITION2 )
          DoThingB();
        else
          DoThingC();
        

        Yes:

        if ( CONDITION )
        {
          DoThingA();
        }
        else if ( CONDITION2 )
        {
          DoThingB();
        }
        else
        {
          DoThingC();
        }
        

        Or:

        if      ( CONDITION )  { DoThingA(); }
        else if ( CONDITION2 ) { DoThingB(); }
        else                   { DoThingC(); }
        

    C++ Quick Reference

    • Starter C++ programs

      Without arguments:

      int main()
      {
        return 0;
      }
      

      With arguments:

      int main( int argCount, char* args[] )
      {
        return 0;
      }
      
    • Variables and named constants

      Declare a variable

      DATATYPE VARIABLENAME;

      int myNumber;
      string myString;
      float myFloat;
      

      Declare a variable and initialize it

      DATATYPE VARIABLENAME = VALUE;

      int myNumber = 10;
      string myString = "Hello!";
      float myFloat = 9.99;
      char myChar = 'a';
      bool myBool = true;
      

      Declare a named constant

      const DATATYPE NAME = VALUE;

      const int NUMBER = 10;
      const string STATE = "Missouri";
      

      Assign a value to a variable that has already been declared

      VARIABLENAME = VALUE;

      myNumber = 100;
      myString = "Goodbye";
      

      Copy a value from one variable to another

      UPDATEDVAR = COPYME;

      vipStudent = student3;
      
    • Console input and output

      Output text to the console

      cout << ITEM;

      You can mix variables and string literals, as long as the stream operator << is between each item.

      cout << "Text";
      cout << myVariable;
      cout << "Label: " << myVariable << endl;
      

      Get input from the keyboard and store it in a variable

      cin >> VARIABLENAME;

      cin >> myInteger;
      cin >> myString;
      

      Get a full line of text (with spaces) and store in a variable (string only)

      getline( cin, VARIABLENAME );

      getline( cin, myString );
      

      Note that if you have a cin >> statement immediately before the getline statement, your input will get skipped. In this case you need to add a cin.ignore() statement:

      cin >> myNumber;
      cin.ignore();
      getline( cin, myString );
      
    • Branching

      If statement

      If the condition evaluates to true, then execute the code within the if statement. Otherwise, skip that code completely.

      if ( a == 1 )
      {
      }
      

      If/else statement

      If the condition from if statement results to false, then the code in the else case is executed instead. No condition is written with the else case.

      if ( a == 1 )
      {
      }
      else
      {
      }
      

      If/else if statement

      Checks each condition in order. If one is true, then subsequent else if statements are ignored.

      if ( a == 1 )
      {
      }
      else if ( a == 2 )
      {
      }
      

      If/else if/else statement

      Similar to above, but if all if and else if statements are false, then else is executed.

      if ( a == 1 )
      {
      }
      else if ( a == 2 )
      {
      }
      else
      {
      }
      
    • Switch

      switch( VARIABLE ) { case VALUE: break; }

      switch( myNumber )
      {
        case 1:
          cout << "It's one!" << endl;
          break;
      
        case 2:
          cout << "It's two!" << endl;
          break;
      
        default:
          cout << "I don't know what it is!" << endl;
      }
      
    • Loops

      Looping with While Loops

      Runs 0 or more times, depending on whether the CONDITION is true.

      while ( CONDITION ) { }

      while ( a < b )
      {
        cout << a << endl;
        a++;
      }
      

      Looping with Do…While Loops

      Runs at least once and then continues looping if the condition is true.

      do { } while ( CONDITION );

      do {
        cout << "Enter a number: ";
        cin >> input;
       } while ( input < 0 || input > max );
      

      Looping with For Loops

      Iterate some amount of times through the loop based on your counter variable and range.

      for ( INIT; CONDITION; UPDATE ) { }

      for ( int i = 0; i < 10; i++ )
      {
        cout << i << endl;
      }
      

      Looping with Range-Based For Loops

      Iterates over all elements in a collection. Only moves forward, doesn't give access to index

      for ( INIT : RANGE ) { }

      vector<int> myVec = { 1, 2, 3, 4 };
      for ( int element : myVec )
      {
        cout << element << endl;
      }