Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CS 402 - Algorithms in Practice

Welcome to the class! I am excited to have you. Throughout this website, you’ll find all the relevant information needed for the course.

On this page, I’ll post important announcements, as well as important information. If you have any questions about the course, please use Ed Discussion. If you need to email me, please include the subject [CS 402 Fall 2026] so it does not end up in spam!

Announcements

  • [September 19, 2026] Added in-class code from Lectures 7 and 8. Added Lecture notes for Lecture 7. Updated schedule to reflect swapping Trees and Graphs (Trees are now before Graphs).
  • [September 13, 2026] Added in-class code from Lectures 5 and 6. Added Lecture notes for Lecture 6.
  • [September 7, 2026] Added in-class code from Lectures 3 and 4, posted at Resources->In-Class Code. Project 2 posted; see Project 2.
  • [September 2, 2026] Added Lecture notes for Lecture 3.
  • [September 1, 2026] Updated Project 1 to specifically say all students are expected to implement Radix Sort. Also made Binary Radix Sort extra credit.
  • [August 27, 2026] Added a Lecture Notes section to Resources. In-class code from today posted at Resources->In-Class Code.
  • [August 25, 2026] Drop-in Office hours posted. In-class code from today posted under Resources.

Important Information

Instructor: Alexander R. Block

Email: arblock [at] uic [dot] edu

  • Please only use email as a last resort. Please use Ed Discussion as the primary method of communication with the instructor.
  • Please include the subject line [CS 402 Fall 2026].

Course Modality and Schedule: In-person only, CDRLC 2406, 11:00am - 12:15pm Chicago Local Time (Central Time), Tuesday & Thursday.

Drop-in Office Hours:

  • Time: Wednesdays, 1:00pm-2:00pm; Thursdays, 12:30pm-1:30pm.
  • Location: CDLRC 2404 (drop-in).

Student Guide for CDRLC: Google Doc

Syllabus

Instructor and Course Details

Instructor: Alexander R. Block
Email: arblock [at] uic [dot] edu

Drop-in Office Hours

  • Time: Wednesdays, 1:00pm-2:00pm; Thursdays, 12:30pm-1:30pm.
  • Location: CDLRC 2404 (drop-in).

Course Websites

Note

Students are expected to regularly check the above course sites to learn about any developments related to the course, upload assignments, and communicate with classmates and the instructor.

Course Modality and Schedule
This course is taught ON CAMPUS, IN PERSON ONLY.

  • Tuesdays and Thursdays, 11:00am–12:15pm Central Time.
  • Room: CDRLC 2406.

Course Information

Catalog Course Description and Prerequisite/Corequisite Statement

Design, implementation and presentation of algorithms and data structures emphasizing dynamic programming and both exact and heuristic approaches to NP-hard problems; problem-solving sessions, programming projects and presentations. Course Information: 3 undergraduate hours. 4 graduate hours.

Prerequisites: CS 401; and consent of the instructor.

Growth Mindset
Course materials and assignments can be complex and challenging, but they are crucial to your intellectual and personal growth and development. There are times you may need extra help. Students who attend class consistently, complete all assignments, thoughtfully engage with feedback on work, develop good study strategies, visit the tutoring center, and contact faculty when struggling can develop a thorough understanding of the course material and ultimately succeed in the course!

Course Goals and Learning Outcomes

Important

Note: this is an elective course.

CS 402 is a joint undergrad/graduate level course. The course is intended to be an applied follow-up course to CS 401. The goal of this course is to reinforce many of the skills you have accumulated as a CS student here at UIC. In particular, I view this course as a way to help prepare you for the job market and actual positions in industry or elsewhere. As such, I have consulted many people (friends and colleagues) to try and give students of this course a broad range of skills that will be useful in the wild.

Course Structure. This course will emphasize your skills as a CS student. Crucial to this, in my view, are the following skills:

  • Designing and implementing algorithms/data structures to solve problems;
  • Presenting these designs and implementations to others; and
  • Continual improvement of skills.

As such, the aim of the course structure is to help you with these three skills. There will be an emphasis on practice of all of these skills. In order to achieve this, lectures will be sparse, and I will try to only present reviews of concepts needed to help solve the problems that will be given in-class and for projects. In my opinion (and many others), students learn best by practicing, practicing, practicing!

Roughly speaking, we will divide up our in-class time as follows.

  • Some brief review and/or introduction of materials/problems for the current section.
  • Ample in-class time to attempt problems and understand materials.
  • Reserved time for in-class presentations of the problems.

For in-class assignments, you will not be evaluated on whether you get a solution correctly the first time, but rather how you approach solving a problem, how well you explain and justify your approach and present it to others, and how well you improve over the course of this class. Your soft skills as a computer scientist are just as important as the hard skills (i.e., designing and implementing), and I will be evaluating you on all such skills and looking for improvement. However, all projects and exams in this course will expect you to find the correct solution to the best of your ability.

You will be allowed to work individually or in groups during our in-class sessions, as well as for projects. It is highly encouraged to work in groups of 2–3 people to help build on each other’s skills. Note that it is easy for an instructor to tell if a single person in a group is doing all the work (or one of three in a larger group is doing no work). All exams will be individual.

The goal of this course is to reinforce many of the skills you have accumulated as a CS student here at UIC. In particular, I view this course as a way to help prepare you for the job market and actual positions in industry or elsewhere. As such, I have consulted many people (friends and colleagues) to try and give students of this course a broad range of skills that will be useful in the wild.

ABET Learning Outcomes

  • (1) an ability to identify, formulate, and solve complex engineering problems by applying principles of engineering, science, and mathematics.
  • (3) an ability to communicate effectively with a range of audiences.
  • (5) an ability to function effectively on a team whose members together provide leadership, create a collaborative environment, establish goals, plan tasks, and meet objectives.
  • (7) an ability to acquire and apply new knowledge as needed, using appropriate learning strategies.

Course Learning Outcomes

  • Designing and implementing algorithms/data structures to solve problems;
  • Presenting these designs and implementations to others; and
  • Continual improvement of skills.

Brief list of topics to be covered (subject to change)

  • Sorting
  • Hashing
  • Graphs
  • Trees
  • Dynamic Programming
  • SAT Solvers (time permitting)

No textbook is required for this course. Lecture will contain overviews of the ideas and concepts needed for the topic at hand, but much of this course is intended for you to practice skills you’ve accumulated thus far as a student.

When relevant or helpful, I will provide links to free materials to help study topics and concepts in this course. Please keep an eye out for changes to the website with these resources. Office hours and Ed discussion threads will also help you greatly during this course.

Required Technology

Access to a computer and internet is required for this course. Students will be expected to make and give in-class presentations using presentation software.

Respect for Copyright

Please protect the copyright integrity of all course materials and content. Please do not upload course materials not created by you onto third-party websites or share content with anyone not enrolled in our course.

Course Policies and Classroom Expectations

Course Announcements

Course information will primarily be conveyed using this website (see here) and Ed Discussion. Course discussion will happen on Ed Discussion. All course assignments and grades will be collected and returned through Gradescope. I will also send email notifications to the class with announcements.

You are responsible for checking this website and emails for any and all updates and information regarding the course, including homework assignments and schedule changes. You are also responsible for keeping up to date on the course webpage, Canvas, and Ed Discussion for any corrections and/or clarifications regarding assignments, or other important information.

Communication Expectations

Students are responsible for all information instructors send to your UIC email. Faculty messages should be regularly monitored and read in a timely fashion.

Please use Ed Discussion private messages shared with the instructors (not just the professor or TA by name) if you wish to communicate with us directly. Please only use email for something that explicitly should be kept private only to that person.

Please email me if you face an unexpected situation that may impede your attendance, participation in class and exam sessions, or timely completion of assignments.

For all emails, please include [CS 402 Fall 2026] in the subject line to ensure it does not get sent to spam.

Grades

Grades may or may not be curved based on an aggregate course score; grade cut-offs are not defined ahead of time. The score cut-offs for A, B, C, etc., will be set after the end of the course.

The course will have the following grade breakdown (subject to change):

Task% of total grade
In-class Presentation and Discussion20%
In-class Reports10%
Projects25%
Midterm Exam20%
Final Exam25%

Information on Assignments and Exams

In-class Presentations and Discussions

Each week, a group (or groups) of students will be asked to present their ideas and solutions to the class. Essentially, you will lead the discussion and answer questions/comments from other students, and myself. You will be evaluated on your presentation, your (attempted) solution, and your discussion.

For this reason, it is essential you attend class. Not every student/group will be required to present each week, but every student/group will be expected to participate in the discussion that comes with the presentations. Full details about in-class presentations and discussions can be found under the In-class Work section of this website.

In-class Reports

To conclude every week of class, all students will individually submit a report. This report will discuss the problem(s) we tackled during class, what the student did to tackle this problem (individually or in a group), and how the discussion helped confirm your solution or helped you find the right solution/clarify your answer.

This report is not meant to be a super formal write-up, but rather to help track your progress and understanding during the semester. Moreover, you are allowed to miss/drop 2 of these reports throughout the semester, no questions asked. Full details about in-class reports can be found under the In-class Work section of this website.

Projects

As this course is meant to give you ample practice reinforcing your skills, there will be a series of programming projects throughout the semester. The current expectation is 4–6 projects depending on the course pacing; i.e., at most one project for each topic covered, maybe a project that incorporates more topics (note this does not mean course concepts will not overlap between topics and projects).

All projects will use the C++ programming language, unless otherwise stated. Your lowest project grade will be automatically dropped. Projects will also require a report and/or office hours presentations alongside the submitted code. Full details about course projects can be found under the Projects section of this website.

Midterm Exam

There will be an in-class midterm exam, covering topics from (roughly) the first half of the course. The tentative date for the midterm exam is Thursday, October 15, 2026, with an in-class review planned for the previous lecture on Tuesday, October 13, 2026. Please plan to attend class on this day for the midterm exam. I will notify all students once the midterm exam date is finalized, and will do so as soon as possible.

Final Exam

There will also be an in-person, written final exam. You are required to take this exam, barring extraneous circumstances. Students who do not take the final exam will be unable to receive a passing grade in this course. Moreover, do not expect to get a passing grade if you simply show up, write your name on the exam, and don’t do the exam (or some variation of this).

Policy for Missed or Late Work

Each assignment (Projects and In-class Reports) will have a set due date. You may submit assignments late, with a 25% point reduction per day of being late. On the fourth or later day of being late, you will receive zero points on the assignment, but if you submit the assignment, it will still be graded in order to give you feedback.

Attendance / Participation Policy

As this class emphasizes many soft skills and in-class participation, it is highly encouraged that you attend class. Much of your grade relies on in-class participation.

You are expected to attend class every week, stay for the entire duration of class, and be an active participant. Lack of attendance, leaving early, and not participating will be detrimental to your grade.

However, I do understand that things happen (important meeting conflicts with class, unexpected sickness, etc.). So, for this course, you are allowed to have 3 unexcused absences from class, no questions asked. For all other absences and/or if you need to leave early, you will need to notify me with supporting documentation for your reasons.

Please email me AS SOON AS POSSIBLE if you face an unexpected situation that may impede your attendance, participation in required class and exam sessions, or timely completion of assignments.

Other Course Policies

Regrade Policy

You are allowed to request one single regrade per project/non-final exam. Moreover, with every regrade request, you must submit the following information:

  • Which problems you are requesting a regrade for; and
  • The exact reason you are requesting a regrade.

I will be strict with this policy to ensure there are no frivolous regrade requests; i.e., do not request a regrade to try and argue for more points. You must have a specific and articulate reason for why you believe something was graded incorrectly. Finally, note that any regrade request can result in a score reduction if additional errors are discovered.

Final Grade Assessment

My goal is to ensure that the assessment of your learning in this course is comprehensive, fair, and equitable. Your grade in the class will be based on the number of points you earn out of the total number of points possible, and is not based on your rank relative to other students. There are no set limits to the number of grades given (e.g., everyone can get an A if everyone does well).

Under no circumstances will grades be adjusted down (except in cases of course policy violation). You can use this straight grading scale as an indicator of your minimum grade in the course at any time during the course. You should keep track of your own points so that at any time during the semester you may calculate your minimum grade based on the total number of points possible at that particular time. If and when, for any reason, you have concerns about your grade in the course, please email me to schedule a time for you to speak with me so that we can discuss study techniques or alternative strategies to help you.

Academic Integrity

Consulting with your classmates on assignments is encouraged, except where noted. However, turn-ins are individual or group-based, and copying code/text from your classmates is considered plagiarism (in the case of individual reports, copying your group member’s report is also plagiarism). You should never look at someone else’s writing/code, or show someone else your writing/code, unless otherwise directed by the instructor. Either of these actions are considered academic dishonesty (cheating) and will be prosecuted as such.

To avoid suspicion of plagiarism, you must specify your sources together with all turned-in materials. List classmates you discussed your homework with and webpages/resources (this includes AI usage) from which you got inspiration and help. Plagiarism and cheating, as in copying the work of others, paying others to do your work, etc., is obviously prohibited, and will be reported (this includes asking questions and copying answers from forums such as Stack Overflow and Reddit).

I report all suspected academic integrity violations to the dean of students. If it is your first time, the dean of students may provide the option to informally resolve the case – this means the student agrees that my description of what happened is accurate, and the only repercussions on an institutional level are that it is noted that this happened in your internal, UIC files (i.e., the dean of students can see that this happened, but no professors or other people can, and it is not in your transcript). If this is not your first academic integrity violation in any of your classes, a formal hearing is held and the dean of students decides on the institutional consequences. After multiple instances of academic integrity violations, students may be suspended or expelled. For all cases, the student has the option to go through a formal hearing if they believe that they did not actually violate the academic integrity policy. If the dean of students agrees that they did not, then I revert their grade back to the original grade, and the matter is resolved.

If you are found responsible for violating the academic integrity policy, the penalty can range from receiving a zero on the assignment in question, receiving a grade deduction, or receiving an F in the class, depending on the severity of the violation.

As a student and member of the UIC community, you are expected to adhere to the Community Standards of academic integrity, accountability, and respect. Please review the UIC Student Disciplinary Policy for additional information.

AI Usage

Usage of AI is allowed in this course. However, you are expected to use AI as a tool to help you, and not as a tool to do the work for you. These are two completely different things. This means you should not be submitting any assignment in this course that is completely written by AI. Doing so constitutes a violation of the course conduct policy and will be appropriately punished.

Failure to adhere to this policy will result in the following consequences:

  • First use: You will lose 50% of the points available on the assignment.
  • Second use: You will fail the assignment.
  • Third use: You will fail the course.

Unfortunately, due to how advanced AI has become, it is increasingly difficult to identify code that has been completely written by AI. To account for this, most of your grade in this course will be based on in-class assessments. You should therefore view the programming projects as a way to practice and study for the in-person assessments.

AI has helped and hindered new and old programmers alike, and it is in your best interest to use GenAI properly to help you solve problems, not solve them for you (as they will often include extra/incorrect/bloated code). See this article exploring the harm GenAI has done to new programmers.

Course Schedule

Please check the Schedule for information regarding the schedule.

Disclaimer

This syllabus is intended to give the student guidance on what may be covered during the semester and will be followed as closely as possible. However, as the instructor, I reserve the right to modify, supplement, and make changes as course needs arise. I will communicate such changes in advance through in-class announcements and in writing via email, the course website, Ed Discussion, and Canvas.

Accommodations

Disability Accommodation Procedures

UIC is committed to full inclusion and participation of people with disabilities in all aspects of university life. If you face or anticipate disability-related barriers while at UIC, please connect with the Disability Resource Center (DRC) at drc.uic.edu, via email at drc@uic.edu, or call (312) 413-2183 to create a plan for reasonable accommodations. To receive accommodations, you will need to disclose the disability to the DRC, complete an interactive registration process with the DRC, and provide me with a Letter of Accommodation (LOA). Upon receipt of an LOA, I will gladly work with you and the DRC to implement approved accommodations.

Religious Accommodations

Following campus policy, if you wish to observe religious holidays, you must notify me by the tenth day of the semester. If the religious holiday is observed on or before the tenth day of the semester, you must notify me at least five days before you will be absent. Please submit this form by email with the subject heading: “[CS 402 Fall 2025] YOUR NAME: Requesting Religious Accommodation.”

Classroom Environment

Inclusive Community

UIC values diversity and inclusion. Regardless of age, disability, ethnicity, race, gender, gender identity, sexual orientation, socioeconomic status, geographic background, religion, political ideology, language, or culture, we expect all members of this class to contribute to a respectful, welcoming, and inclusive environment for every other member of our class. If aspects of this course result in barriers to your inclusion, engagement, accurate assessment, or achievement, please notify me as soon as possible.

Name and Pronoun Use

If your name does not match the name on my class roster, please let me know as soon as possible. My pronouns are [she/her; he/him; they/them]. I welcome your pronouns if you would like to share them with me. For more information about pronouns, see this page: https://www.mypronouns.org/what-and-why.

Community Agreement/Classroom Conduct Policy

  • Be present by removing yourself from distractions, whether they be phone notifications, entire devices, conversations, or anything else.
  • Be respectful of the learning space and community. For example, no side conversations or unnecessary disruptions.
  • Use preferred names and gender pronouns.
  • Assume goodwill in all interactions, even in disagreement.
  • Facilitate dialogue and value the free and safe exchange of ideas.
  • Try not to make assumptions, have an open mind, seek to understand, and not judge.
  • Approach discussion, challenges, and different perspectives as an opportunity to “think out loud,” learn something new, and understand the concepts or experiences that guide other people’s thinking.
  • Debate the concepts, not the person.
  • Be gracious and open to change when your ideas, arguments, or positions do not work or are proven wrong.
  • Be willing to work together and share helpful study strategies.
  • Be mindful of one another’s privacy, and do not invite outsiders into our classroom.

Furthermore, our class (in person and online) will follow the CS Code of Conduct. If you are not adhering to our course norms, a case of behavior misconduct will be submitted to the Dean of Students and to the Director of Undergraduate Studies in the department of Computer Science. If you are not adhering to our course norms, you will not get full credit for your work in this class. For extreme cases of violating the course norms, credit for the course will not be given.

Content Notices and Trigger Warnings

Our classroom provides an open space for a critical and civil exchange of ideas, inclusive of a variety of perspectives and positions. Some readings and other content may expose you to ideas, subjects, or views that may challenge you, cause you discomfort, or recall past negative experiences or traumas. I intend to discuss all subjects with dignity and humanity, as well as with rigor and respect for scholarly inquiry. If you would like me to be aware of a specific topic of concern, please email or visit my Student Drop-In Hours.

Student Parents

I know well how exhausting balancing school, childcare, and work can be. I would like to help support you and accommodate your family’s needs, so please don’t keep me in the dark. I hope you will feel safe disclosing your student-parent status to me so that I can help you anticipate and solve problems in a way that makes you feel supported. Unforeseen disruptions in childcare often put parents in the position of having to choose between missing classes to stay home with a child or leaving them with a less desirable backup arrangement. While this is not meant to be a long-term childcare solution, occasionally bringing a child to class in order to cover gaps in care is perfectly acceptable. If your baby or young child comes to class with you, please plan to sit close to the door so that you can step outside without disrupting learning for other students if your child needs special attention. Non-parents in the class, please reserve seats near the door for your parenting classmates or others who may need to step out briefly.

Resources: Academic Success, Wellness, and Safety

We all need the help and the support of our UIC community. Please visit my drop-in hours for course consultation and other academic or research topics. For additional assistance, please contact your assigned college advisor and visit the support services available to all UIC students.

Academic Success

Wellness

  • Counseling Services : You may seek free and confidential services from the Counseling Center at https://counseling.uic.edu/.

  • Access U&I Care Program for assistance with personal hardships.

  • Campus Advocacy Network : Under Title IX, you have the right to an education that is free from any form of gender-based violence or discrimination. To make a report, email TitleIX@uic.edu. For more information or confidential victim services and advocacy, visit UIC’s Campus Advocacy Network at http://can.uic.edu/.

Safety

Schedule

This schedule is tentative and subject to change. Any changes will be announced.

Week (Dates)TopicsAnnouncementsAdditional Resources
Week 1 (08/25, 08/27)
  • Syllabus
  • Sorting
Project 1 released (08/24).
Week 2 (09/01, 09/03)
  • Sorting
  • Hashing
Project 1 due (09/06).
Week 3 (09/08, 09/10)
  • Hashing
Project 2 released (09/07).
Week 4 (09/15, 09/17)
  • Trees
Week 5 (09/22, 09/24)
  • Trees
Project 2 due (09/27).
Week 6 (09/29, 10/01)
  • Trees
Project 3 released (09/30).
Week 7 (10/06, 10/08)
  • Graphs
Week 8 (10/13, 10/15)
  • Midterm Review (10/13)
  • Midterm Exam (10/15)
Week 9 (10/20, 10/22)
  • Graphs
Week 10 (10/27, 10/29)
  • Graphs
Week 11 (11/03, 11/05)
  • Dynamic Programming
Week 12 (11/10, 11/12)
  • Dynamic Programming
Week 13 (11/17, 11/19)
  • Dynamic Programming
Week 14 (11/24)
  • NP Hard Problems
  • No class on 11/26 (Thanksgiving)
Week 15 (12/01, 12/03)
  • NP Hard Problems
  • Final Exam Review
Week 16 (Finals Week)
  • Final Exam (Date and Time TBD)

In-class Work

One of the primary assessments in this course is in-class work (constituting 30% of your final grade). Attending and participating in class is essential to your success in this course. The two portions of in-class work are Presentations and Discussions, and Reports.

In-class Presentations and Discussions

During each class session, students will be given problems to work on, focusing on certain topics. Students will work either individually or in groups, working on these problems (designing, implementing, and analyzing algorithms to solve these problems). After some time has passed, we will shift to a problem discussion phase.

A student or group (or groups of students) will then be asked to lead a presentation and discussion about the problem session. You will be expected to do (at a minimum) the following:

  • Present the problem;
  • Present your solution;
  • Justify why you think your solution works;
  • Analyze your solution; and
  • Lead a discussion / answer questions about your solution.

Note that if you (or your group) was not able to solve the problem, that is okay! Your presentation will then be what you did to try to solve the problem, what issues you were running into, and then open the discussion to your peers to see if you can get the problem resolved.

Evaluation

This portion of class is meant to give you practice with the soft skills you need to be a successful computer scientist. As such, there is no rigid evaluation for the presentations and discussions. Giving your best effort towards solving, presenting, and discussing the problems will give you full credit. However, any half-attempts are easily identifiable and will result in point deductions. Just do your best and you will get full points.

In-class Reports

To conclude each week of class, students are expected to individually submit a report of the work done this week. This includes the at least (but not limited to) following:

  • Problems given in class;
  • How the student and/or their group approached solving the problem;
  • Challenges or insights found while solving these problems; and
  • The final solution you came up with to solve the problem (if applicable).

This is not meant to be a rigorous report; it is meant to help you track your progress and help you identify your strengths and weaknesses. You might notice a pattern in the challenges you faced when trying to solve a problem, or a pattern of solving specific types of problems easily. It is in your best interest to be thorough with these reports to help you help yourself.

These reports must be submitted individually, even if you worked in groups. You are required to identify all group members and any other students you practiced with to get to your final solution. You need not acknowledge the in-class discussion in your report.

Evaluation

Reports should be 2–5 pages, save for weeks when material is sparse (e.g., the first week, Thanksgiving week, etc.). Be thorough with your report and findings, but you do not need to be overly formal.

Submissions are required to be digital and in .pdf format. LaTeX\LaTeX is preferred but not required.

Templates

Please find a .docx and a .tex template for your reports below.

Projects

Programming projects constitute 25% of your final grade, broken down into two parts. My goal is to give you 5 projects throughout the semester. Your lowest project grade will be automatically dropped.

General Guidelines

Projects in this course will be in C++ unless otherwise stated. You are expected to track project progress using GitHub. Projects will ask you to solve one or a series of problems, with varying difficulty.

My goal is for projects to help you focus on the concepts you are practicing in class, but also have you handle other tasks that often come with building programs (e.g., handling I/O, handling different requirements, etc.). As such, you will often be given only a handful of files that you are expected to write in. You can include other files (e.g., other .h and .cpp files), but do not go overboard (if you submit a project with 100 extra files when you do not need them, we are going to have a problem).

Project Submission

You will submit your projects to me in the form of a GitHub repository, where I can see any and all commits made during your project. You are expected to know how to use Git and GitHub. My general rule of thumb:

Tip

Commit early, commit often.

If you give me a GitHub repo with a single commit of thousands of lines of code, this is not good conduct and signals to me you are likely using AI to write large portions of your codebase. The projects in this course should not require such large codebases; please keep your code contained and free of bloat.

Autograding

Each project will include an autograder with some tests you can run yourself. Instructions will be included in each project repository.

Project Grades

The code you submit for your project will consist of 15% of the 25% of your final grade. The autograder will be used to test your code and output your grades.

Project Evaluation

Each project will have a project evaluation. This evaluation occurs after you have submitted the code for your projects. Evaluations will be in-class, written assessments of the projects you have submitted. The evaluation will test your understanding of the code you submitted. This portion of your projects will account for the remaining 10% of the total 25% project grade.

Warning

You should never be submitting code you do not understand. This is bad practice and can cause devastating issues down the line in real life scenarios.

GitHub Repository for all Projects

Please see my GitHub repo for this course here. It contains the relevant information you need to set up your projects and any other code I add to the repository (e.g., adding code we did in class).

Project 1

Date Assigned: Monday, August 24th, 2026.
Due Date: Sunday, September 6th, 2026, by 11:59pm Central Time.
Project Instructions:https://github.com/arblock-uic/uic-cs-402-fall-2026/tree/project-1/project-1#readme
Direct link to GitHub branch: https://github.com/arblock-uic/uic-cs-402-fall-2026/tree/project-1/ (this includes instructions for how to set up your repository).

Project 2

Date Assigned: Monday, September 7th, 2026.
Due Date: Sunday, September 27th, 2026 by 11:59pm Central Time.
Project Instructions:https://github.com/arblock-uic/uic-cs-402-fall-2026/blob/project-2/project-2/README.md
Direct link to GitHub branch:https://github.com/arblock-uic/uic-cs-402-fall-2026/tree/project-2

Project 3

Date Assigned: Wednesday, September 30th, 2026.
Due Date: Sunday, October 25th, 2026, by 11:59pm Central Time.
Project Instructions:https://github.com/arblock-uic/uic-cs-402-fall-2026/blob/project-3/project-3/README.md
Direct link to GitHub branch:https://github.com/arblock-uic/uic-cs-402-fall-2026/tree/project-3

Resources

Throughout the semester, I will fill this webpage with useful resources. Most (if not all) of these resources will be freely available online.

Lecture Notes

For the few times I give lectures in class, I will post lecture notes here, along with other relevant materials.

Lecture 10 (Sept. 24, 2026) Notes from LeetCode Problem 105

We began this lecture by re-visiting the problem given in Lecture 9 (see below). Associated in-class drawings can be found here.

You are given two lists L1 and L2 of the same size. L1 represents the preorder traversal of a binary tree, and L2 is the inorder traversal. From this information, you are tasked with reconstructing the actual binary tree as a TreeNode data structure.

The high-level idea is simple for this problem. Given the root of a binary tree root, a preorder traversal does the following:

  1. visits root,
  2. recursively visits the left subtree root.left, and
  3. recursively visits the right subtree root.right.

Note here that “visits” is a catch-all term for “do something at this tree node”. By definition, all null or nil nodes are visited.

Then, given root, an inorder traversal does the following:

  1. recursively visits the left subtree root.left, and
  2. visits root,
  3. recursively visits the right subtree root.right.

From this, the algorithm for the specified problem is easy to state at a high level.

  1. L1[0] is the root of the binary tree.

  2. Search L2 for index i such that L1[0] == L2[i].

  3. Now, from the definition of inorder traversal, we know that L2[:i] (note here i is exclusive, so this is L2[0], ..., L2[i-1]) are all nodes that appear in the left subtree of the binary tree rooted at L1[0], and L2[i+1:] are all nodes that appear in the right subtree of the binary tree rooted at L1[0].

  4. Let l = len(L2[:i]) and r = len(L2[i+1:]). Then, we know that:

    • L1[1:l+1] is the preorder traversal of the left subtree of the binary tree rooted at L1[0], and
    • L1[l+1:] is the preorder traversal of the right subtree of the binary tree rooted at L1[0].
  5. From here, we can recurse on the left subtree with preorder list L1[1:l+1] and inorder list L2[:i+1], and the right subtree with preorder list L1[l+1:] and inorder list L2[i+1:].

Lecture 7 (Sept. 15, 2026) Binary Trees and Merkle Trees

In this lecture, I tried to give some insight behind Merkle Trees, which you are asked to implement for your Project 2. Handwritten notes can be found here.

Review: Binary Trees

In general, trees are one of the most important data structures in computer science; they are used everywhere. Here, we focus on Binary trees, which are trees such that every node has at most 2 children.

Suppose we have the following TreeNode data structure to represent a binary tree.

struct TreeNode {
    // three basic things you'd want in a TreeNode
    string node_id; // this can also be an integer/char/short/a custom datatype/etc.
    TreeNode* left_child;
    TreeNode* right_child;
    // may contain other useful data
    // ...
}

Clearly from the above, trees are recursively defined data structures: a tree is either a leaf node, which has no children (i.e., left_child == right_child == null), or it is a rooted tree root which has two subtrees: a left subtree with root left_child and a right subtree with root right_child. Thinking about trees as recursive data structures greatly helps with understanding a variety of algorithms which use trees.

Traversing Trees. One of the most basic things you’d like to do when given a tree (besides trying to build a tree) is to perform some type of tree traversal: go through and “visit” every node in the tree. Here, “visiting” a node can mean a variety of things, including:

  • Print node_id;
  • Mark the node as visited; e.g., modify the struct TreeNode to include a boolean visited variable initially set to false.
  • Update some node_distance value;
  • etc.

Intuitively, “visiting” a node is a catch-all term for “do something at this node in the tree”. Now, the important question in any traversal is: what order do you visit each node in the tree?

Pre-order Traversal. A pre-order traversal is one of several types of depth-first traversals. You can think of these as recursive traversals. Intuitively, a pre-order traversal:

  1. Begins at the root of the tree and immediately visits the root,
  2. Then, the traversal recurses on the left subtree (i.e., go back to (1) above, but treat the left child as the new root to visit), followed by the right subtree.

As pseudocode, this traversal is done as follows.

void pre_order(TreeNode* root) {
    if(root == null) return;
    visit(root); // e.g., std::cout << root->node_id << std::endl;
    pre_order(root->left_child);
    pre_order(root->right_child);
}

Again, it is helpful to think about trees as recursive data structures: a pre-order traversal visits the root of a tree, then recurses on the left subtree (which has its own root), and on the right subtree (which also has its own root). The base case is either a leaf node (no children), or a null node (which is handled in the above pseudocode).

An example tree is given below.

graph TB
    a((a))-->b((b))
    a-->c((c))
    b-->d((d))
    b-->e((e))
    c-->f((f))
    c-->g((g))

The pre-order traversal of the tree would then be [a, b, d, e, c, f, g].

Post-order Traversal. Post-order traversal is essentially the reverse of a pre-order traversal. Again, thinking recursively: from the root of a tree,

  1. Traverse the left subtree;
  2. Traverse the right subtree;
  3. Visit the root (i.e., the root is only visited after all nodes in the left and right subtrees are visited)

Now, as pseudocode, this traversal is done as follows.

void post_order(TreeNode* root) {
    if(root == null) return;
    post_order(root->left_child);
    post_order(root->right_child);
    visit(root); // e.g., std::cout << root->node_id << std::endl;
}

The post-order traversal of the previous tree would then be [d, e, b, f, g, c, a].

Back to Merkle Trees

This digression to trees and tree traversals was meant to get you thinking about trees so you can get some intuition behind Merkle Trees. A Merkle tree is an implicitly defined complete binary tree: given a list of 2n2^n node_id’s, a Merkle tree asks you to label a complete binary tree1 as follows. Let vector<string> L be a list/vector of size 2n2^n, and let H be a hash function which hashes strings to strings.

  1. For each i in range(len(L)): leaf node i has label H( L[i] + "i"), where + denotes string concatenation, and "i" denotes converting integer i into a string.
  2. For each non-leaf node v, label v as H( v.left_child.node_id + v.right_child.node_id ).

The final output of the Merkle tree commitment is the label of the root node in this complete binary tree. Now, this is a bit of a different tree problem you are given: the complete binary tree is only implicitly defined with respect to the input list L. However, from this list, you can label every node in a complete binary tree whose leaf nodes all have node_id = H( L[i] + "i" ).

Thinking in terms of tree traversals, imagine for a moment that you are given the root of this complete binary tree, and that all the leaf labels were given to you. Then, the output of a Merkle tree commitment is the root label after you’ve done a post_order labeling of the tree!

Given as pseudocode, this traversal is done as follows.

void post_order_label(TreeNode* root, HashFunction H) {
    // Assumption is that the leaves are already labeled.
    if(root->left_child == null && root->right_child == null) return; 
    post_order_label(root->left_child, H);
    post_order_label(root->right_child, H);
    root->node_id = H( root->left_child->node_id + root->right_child->node_id);
}

Suppose that you have TreeNode* myRoot as your root node. Then, you can run post_order_label(myRoot, H), and then the Merkle tree commitment simply outputs myRoot->node_id.

The key challenge with a Merkle tree is that you are not given myRoot! You are instead given the list L, which tells you how to label the leaf nodes in the above algorithm. From L and H, you can still recover myRoot->node_id if you were given myRoot instead.

Merkle Proofs. The amazing feature of a Merkle tree is that once you give someone the value of myRoot->node_id, they can later ask the following question: “What is item 5 in your list L?” Of course, if you trust the person who created the Merkle tree, they can reply with “Item 5 is L[5]”, and you both move on with your days. However, Merkle trees are used in situations where you do not trust the person who gave you myRoot->node_id! For example, they may be malicious and give you some other value instead of L[5] used in computing myRoot->node_id.

So then, you ask instead the question: “What is item 5 in your list L? Can you prove that your answer is consistent/agrees with the value myRoot->node_id which you gave me earlier?” This is a lot of words to say that a Merkle tree allows you to prove the authenticity of your data, and that it is consistent with the value myRoot->node_id you computed earlier.

As an example, suppose you begin with the list L = ['a', 'b', 'c', 'd']. First, let’s compute the Merkle tree below.

graph TB
    0(("('a',0)"))-->a
    1(("('b',1)"))-->b
    2(("('c',2)"))-->c
    3(("('d',3)"))-->d
    a(("h_a0 = H('a0')"))
    b(("h_b1 = H('b1')"))
    c(("h_c2 = H('c2')"))
    d(("h_d3 = H('d3')"))
    a-->e(("h_ab = H( h_a0+h_b1  )"))
    b-->e 
    c-->f(("h_cd = H( h_c2+h_d3 )"))
    d-->f
    e-->g(("h_abcd = H( h_ab+h_cd )"))
    f-->g
    linkStyle default stroke-width:2pt;

The output of the Merkle commitment would be h_abcd, the root label of the above tree.

Suppose you are given the string h_abcd, the output of the root label from the above tree. Now, in the Merkle proof, let’s say you ask for index i = 2. We provide the value L[2] = 'c', and now we also need to provide additional values to the proof so that you can verify ('c', 2) is consistent with h_abcd. Let proof = [] denote the proof string. We first have proof.append(L[2]), which gives us proof = [ 'c' ].

Now, intuitively, the proof proof needs to contain the minimum amount of information you need to compute h_abcd given you have h_abcd and you have information c and i=2. How do you figure out this information? Well, in the above tree, let’s highlight in red the root-to-leaf path from h_abcd to node ('c',2), given below as a red path.

graph TB
    0(("('a',0)"))-->a
    1(("('b',1)"))-->b
    2(("('c',2)"))-->c
    3(("('d',3)"))-->d
    a(("h_a0 = H('a0')"))
    b(("h_b1 = H('b1')"))
    c(("h_c2 = H('c2')"))
    d(("h_d3 = H('d3')"))
    a-->e(("h_ab = H( h_a0+h_b1  )"))
    b-->e 
    c-->f(("h_cd = H( h_c2+h_d3 )"))
    d-->f
    e-->g(("h_abcd = H( h_ab+h_cd )"))
    f-->g
    linkStyle default stroke-width:2pt;
    linkStyle 2 stroke:red,stroke-width:3pt;
    linkStyle 6 stroke:red,stroke-width:3pt;
    linkStyle 9 stroke:red,stroke-width:3pt;

Notice that given ('c',2), you can compute h_c2 as H=('c2'). Next, you must be able to compute h_cd. You can compute h_c2, but you cannot compute h_d3! So, instead, we ask the creator of the Merkle tree to give us this value in the proof. Notice that if you have h_d3, then you can compute h_cd = H(h_c2 + h_d3)! So proof.append(h_d3), and now the proof is proof = ['c', h_d3].

Now, with the proof so far, you can compute h_cd, but you cannot compute h_ab! And without h_ab, you cannot compute h_abcd, which is supposed to be the label of the root in the Merkle tree. So again, we ask the creator of the Merkle tree to give us h_ab so we can compute this value! Thus, we have proof.append(h_ab), and the final proof is proof = ['c', h_d3, h_ab]

You now have all the information needed to check if ('c', 2) is consistent with h_abcd. You compute h_c2 = H('c2'), then compute h_cd = H( h_cd + proof[1]), and finally check if h_abcd == H( proof[2] + h_cd). Done!

Lecture 6 (Sept. 10, 2026) Birthday Attacks

In this lecture, we discussed finding collisions in a hash function using something called a Birthday Attack, named after the Birthday Paradox.

In principle, a hash function HH is a compressing map from arbitrary-length strings, represented as {0,1}∗∗{{0,1}^{}}\vphantom{}, to fixed-length strings. For positive integer λ\lambda, a hash function is a map H ⁣:{0,1}∗→{0,1}λH \colon {0,1}^* \rightarrow {0,1}^\lambda. It takes any binary string as input and outputs some binary string of length λ\lambda. Usually, we only think about hash functions with fixed-length inputs, such as H:{0,1}2λ→{0,1}λH : {0,1}^{2\lambda} \rightarrow {0,1}^\lambda, which maps bit-strings of length 2λ2\lambda to bit-strings of length λ\lambda (it is a 2-to-1 compressing map).

Ideally, it should be very difficult to find collisions in a hash function. That is, finding two inputs a≠ba \neq b such that H(a)=H(b)H(a) = H(b) should be hard. Indeed, if HH behaves like a random function, then the probability you find a collision is at most 12λ\frac{1}{2^\lambda}. However, HH is compressing, so collisions are guaranteed to exist by the Pigeonhole Principle.

Birthday Attacks

A birthday attack exploits the Birthday paradox to find collisions in a hash function. It is a very simple attack and works as follows.

  1. Randomly choose 2λ=2λ/2\sqrt{2^\lambda} = 2^{\lambda/2} inputs to the hash function. Let these inputs be named x1,…,x2λ/2x_1, \dotsc, x_{2^{\lambda/2}}.
  2. For each i≠ji \neq j, check if H(xi)=H(xj)H(x_{i}) = H(x_{j}).

The above algorithm takes O(2λ/2)O(2^{\lambda/2}) time and space to execute. Note that step (2) above takes O(2λ)O(2^\lambda) time if you check all possible pairs naively (e.g., you can use a hash-table to check if you have a collision!). Now, this is bad in practice, where λ=256\lambda = 256 is typical. This would be approximately 4×10224 \times 10^{22} petabytes. Next lecture, we’ll see a much more space-efficient birthday attack algorithm.

More Reading and Resources

Additional reading can be found here: https://people.cs.uchicago.edu/~davidcash/284-autumn-21/12-hash.pdf. Calculator for the Birthday Problem found here: https://www.bdayprob.com/

Lecture 3 (Sept. 01, 2026) Counting Sort and Radix Sort

In this lecture, we learned about Counting Sort and Radix Sort. Handwritten notes can be found here.

Counting Sort

Suppose you are tasked with sorting integers in a given list LL of size nn. Suppose further you are guaranteed that every integer ℓ∈L\ell \in L satisfies 0≤ℓ≤K0 \leq \ell \leq K, where K≥1K\geq 1 is some positive integer. That is, everything in your dataset is guaranteed to be at least 00 and at most the value KK. Given this fact, can we sort LL in faster than O(nlog⁡(n))O(n\log(n)) time?

All comparison-based sorts we saw in last lecture have a Ω(nlog⁡(n))\Omega(n \log(n)) lower bound on their run time (specifically, the number of comparisons they make) when given any arbitrary list as input. In this problem, you are given a somewhat arbitrary list, but you have guarantees about the data set. We will use these guarantees to sort LL faster.

The simplest idea is to count how many times each element ℓ∈L\ell \in L occurs. Since you know that 0≤ℓ≤K0 \leq \ell \leq K, we can sort the list as follows (using pseudocode).

def countingSort(L):
    # Allocates an array of size K+1 integers; we will use this to count.
    A = [ 0 for i in range(K+1) ] 
    for l in L:
        A[l] = A[l] + 1 # count how many times element l occurs in list L

    L.clear() # empty the list so we can use it again

    # add 0, 1, ..., K back to the list depending on the number of
    # times we saw it before
    for i in range(K+1): 
        # using the count we computed, add each element back to 
        # list L
        while size(A[i]) > 0:
            L.append(i) # append element i to L
            A[i] = A[i]-1 # decrement the count

    return L

The idea with counting sort above is to simply count the occurrence of each ℓ∈{0,1,…,K}\ell \in {0,1,\dotsc, K} that appear in the original list LL. We use the array AA to track these occurrences. Once we have counted, we simply have to scan AA in order from index 00 to index KK, adding index ii exactly A[i]A[i] times to the list LL (which we have cleared).

This algorithm runs in time O(K+n)O(K + n), where nn is the size of your input list LL and KK is the upper bound on the values taken by your list LL. The space is O(K+n)O(K + n) as well. So long as K=O(n)K = O(n), this algorithm works pretty well. However, it is quite likely that KK can be quite large when handling arbitrary data; for example, K=232−1K = 2^{32}-1 for all possible unsigned integers, and you are allocating an array of size 2322^{32}, which can be quite large. This is even worse if K=264−1K = 2^{64}-1 (unsigned integers), or if you need to handle larger integer types. Can we sort a list of nn integers in linear time without counting sort?

See also https://www.w3schools.com/dsa/dsa_algo_countingsort.php

Radix Sort

Radix sort essentially says “let’s use counting sort, digit by digit, for our integer inputs”. We will discuss LSB radix sort, which performs sorting in order from the least significant bit/digit to the most significant.

As an example, consider L=[123,52,1677,17]L = [123, 52, 1677, 17]. Scanning from the start to end of LL, for each digit starting with the least significant, perform counting sort on those digits while preserving the original numbers. This means we have an array A=[[],...,[]]A = [ [], …, [] ] of size 1010. Sorting by LSB, we see that A[2]=[52]A[2] = [ 52 ], A[3]=[123]A[3] = [ 123 ], and A[7]=[1677,17]A[7] = [1677, 17], with all other lists being empty. Now, we output a new list L=[52,123,1677,17]L = [ 52, 123, 1677, 17 ], in sorted order of the LSB.

Continuing again to the second digits, we have A[1]=[123,17]A[1] = [ 123, 17 ], A[5]=[52]A[5] = [ 52 ], and A[7]=[1677]A[7] = [ 1677 ], and obtain the new sorted list L=[123,17,52,1677]L = [ 123, 17, 52, 1677 ]. With the third digits, we can write 017017 and 052052, and we get A[0]=[17,52]A[0] = [ 17, 52 ], A[1]=[123]A[1] = [ 123 ], and A[6]=[1677]A[6] = [1677], giving us L=[17,52,123,1677]L = [17, 52, 123, 1677 ]. Though we are already in sorted order, the algorithm would still run one more pass and have A[0]=[17,52,123]A[0] = [ 17, 52, 123] and A[1]=1677A[1] = 1677 and output L=[17,52,123,1677]L = [17, 52, 123, 1677 ].

You can generalize this to any integer base B≥2B \geq 2 (e.g., binary, ternary, octal, hexadecimal, etc.). The pseudocode for the algorithm is given below.

# List L, base B
def radix_sort(L, B):
    max_val = max(L)
    d = 0;
    # Here, // denotes integer division, or floor( max_val / B^d ) 
    # where / is float division
    while(max_val // (B^d) > 0): 
        d = d+1

    A = [ [] for i in range(B) ] # A list of empty lists; size(A) = B
    for(int i = 0; i < d; ++i):
        for item in L:
            digit = (item // B^i) % B # Extract the ith digits, where 0 is the least significant
            A[digits].append(item)
        L.clear()
        for sublist in A:
            if(!sublist.empty()):
                L.append(sublist)
                sublist.clear()
    return L

See also https://www.w3schools.com/dsa/dsa_algo_radixsort.php

Lecture 2 (Aug. 27, 2026) Sorting Algorithms Review

In this lecture, we reviewed a variety of sorting algorithms. My handwritten notes for this lecture can be found here.

Bubble Sort

Given an array AA of size nn, iterate through the array and compare adjacent elements, swapping them if they are in the incorrect order. Repeat until no swaps are performed. Worst-case runtime: O(n2)O(n^2); Best-case runtime: Ω(n)\Omega(n).

Bubble sort example from Wikipedia.
Bubble sort example, courtesy of Wikipedia.

See also: https://www.w3schools.com/dsa/dsa_algo_bubblesort.php

Selection Sort

Given an array AA of size nn, partition the list into “sorted” (initially nothing) and “unsorted” (initially the entire list) parts. Within the unsorted part, find the minimum element and swap it with the first element of the unsorted portion. Then, expand the sorted portion to include this new element. Worst-case runtime: O(n2)O(n^2); Best-case runtime: Ω(n2)\Omega(n^2).

Selection sort example from Wikipedia.
Selection sort example, courtesy of Wikipedia.

See also: https://www.w3schools.com/dsa/dsa_algo_selectionsort.php

Insertion Sort

Given an array AA of size nn, partition the list into “sorted” (initially nothing) and “unsorted” (initially the entire list) parts. Next, get the first element of the unsorted portion of the list; let cc be this value. Then, scan through the sorted portion of the list until you find an index ii such that A[i]≤c<A[i+1]A[i] \leq c < A[i+1], and insert cc in between these two values (shifting the values to the right), expanding the sorted portion of the list and shrinking the unsorted portion. Worst-case runtime: O(n2)O(n^2); Best-case runtime: Ω(n)\Omega(n).

Insertion sort example from Wikipedia.
Insertion sort example, courtesy of Wikipedia.

See also: https://www.w3schools.com/dsa/dsa_algo_insertionsort.php

Merge Sort

Given an array AA of size nn, split the array in half. Recursively split each sub-array until arriving at arrays of size 11. Arrays of size 11 are trivially sorted. Now, when going down the recursion stack, the left and right sub-arrays are sorted, and you merge these sorted sub-arrays into a new sorted array. Runtime (Best and Worst): Θ(n)\Theta(n).

Merge sort example from Wikipedia.
Merge sort example, courtesy of Wikipedia.

See also: https://www.w3schools.com/dsa/dsa_algo_mergesort.php

Quicksort

Quicksort is actually a family of algorithms which sort a list as follows. Given an array AA of size nn:

  • Pick a pivot index ii and a pivot value pivot = A[i].
  • Partition AA into three buckets: all elements of AA that are less than pivot, all element of AA that are equal to pivot, and all elements of AA that are greater than pivot.
  • Recursively sort the “less than” and “greater than” buckets.
  • Merge the sorted buckets together. In class, we discussed picking ii (i.e., pivot) uniformly random from all possible values in the current list. This gives O(nlog⁡(n))O(n \log(n)) time on average, but can have O(n2)O(n^2) time in the worst case (if you are unlucky). Other pivot selection strategies include (non-exhaustive):
  • Always pick the first/middle/last element of the list;
  • Pick the median of the first, middle, and last elements.
Quicksort example using last element of list as pivot, from Wikipedia.
Quicksort example where the pivot is always chosen to be the last element of the array, courtesy of Wikipedia.

See also: https://www.w3schools.com/dsa/dsa_algo_quicksort.php

Lecture 1 (Aug. 25, 2026) Sorting Algorithms Review

In this lecture, we briefly reviewed some sorting algorithms (with a more thorough review postponed until Lecture 2). We also reviewed the following important concepts for sorting algorithms.

  • Stable vs. Unstable Sort. A stable sorting algorithm preserves the relative order of the list given as input. This is important for sorting data with many attributes. An example is given below.
    • Given a list of tuples of the form ("name", age) as input, sort the list by age. Example list: list = [("Alice", 25), ("Bob", 28), ("Charlie", 25)].
      • Stable sort: outputs the list [("Alice", 25), ("Charlie", 25), ("Bob", 28)]. This is stable because the tuple ("Alice", 25) appeared before ("Charlie", 25) in the original list.
      • Unstable sort: can output either [("Alice", 25), ("Charlie", 25), ("Bob", 28)] or [("Charlie", 25), ("Alice", 25), ("Bob", 28)].
  • In-place vs. Not In-place Sort. An in-place sorting algorithm only uses a constant amount of memory to sort the list. Note that this means the sorting algorithm cannot copy the data (as given a list of size nn, this gives O(n)O(n) extra space used). Generally speaking, in-place sorting algorithms are more difficult to implement and often have larger overheads when compared to not in-place sorting, but this can vary.

In-Class Code

In this section, I will post the problems we have tackled in class, as well as the code we wrote to solve the problems. This section will continue to grow throughout the semester. Note that it is posted in reverse chronological order (most recent problems are posted first).

(Sept. 29, 2026) Check Completeness of a Binary Tree

Topic: Trees
Link

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    # Idea: DFS only down the leftmost path. This is the
    # max possible depth of the tree.
    def max_depth(self, root: TreeNode | None) -> int:
        if(root == None):
            return 0
        return (1+self.max_depth(root.left))
    
    def isCompleteTree(self, root: TreeNode | None) -> bool:
        depth = self.max_depth(root)-1 # Making the root be depth 0

        queue = [root]
        level = 0
        num_nodes = 2**level
        while(len(queue)>0):
            # If we are not at the deepest level of the tree and 
            # we do not have the full amount of nodes, return False
            if(len(queue) != num_nodes and level < depth):
                return False

            # Get all nodes from the current level out of the queue
            curr_level = []
            while(len(queue)>0):
                curr_level.append(queue.pop(0))
            
            # If we are not at the level right above depth, add
            # all children to the queue as in a level-order 
            # traversal or BFS
            if(level < depth-1):
                for node in curr_level:
                    # only add children if they are NOT null/None
                    if(node.left):
                        queue.append(node.left)
                    if(node.right):
                        queue.append(node.right)
            # Now, if we are at depth-1, add EVERY child to the
            # queue, even if they are null/none
            elif(level == depth-1): 
                for node in curr_level:
                    queue.append(node.left)
                    queue.append(node.right)
            else: # level == depth
                # Flag to handle the case that the last level is partially filled.
                # If true, it means that we should only see null/None nodes in the
                # remainder of the queue. Becomes "True" upon seeing the first null/
                # None node.
                noMoreNodes = False
                # Iterate through the current level in-order
                for node in curr_level:
                    if(noMoreNodes):
                        # If we shouldn't see any non-null/-None nodes, and we
                        # see one, return False
                        if(node):
                            return False
                    else:
                        # If the node is None/null, we should ONLY see null/None
                        # nodes in the remainder of the queue
                        if(not node):
                            noMoreNodes = True
                        else:
                            # If any node at level==depth has a child, the tree is
                            # ill-structured and we return false.
                            if(node.left or node.right):
                                return False

            level = level+1
            num_nodes = num_nodes*2

        return True

(Sept. 22, 2026) Construct Binary Tree from Preorder and Inorder Traversal

Topic: Trees.
Link

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def buildTree(self, preorder: list[int], inorder: list[int]) -> TreeNode | None:
        if(len(inorder)==0):
            return None
        if(len(inorder)==1):
            return TreeNode(inorder[0])
        rootval = preorder[0]
        root = TreeNode(rootval)
        root_idx = inorder.index(rootval)
        left_inorder_slice = inorder[:root_idx]
        right_inorder_slice = inorder[root_idx+1:]
        left_preorder_slice = preorder[1:1+len(left_inorder_slice)]
        right_preorder_slice = preorder[1+len(left_inorder_slice):]

        root.left = self.buildTree(left_preorder_slice, left_inorder_slice)
        root.right = self.buildTree(right_preorder_slice, right_inorder_slice)

        return root

(Sept. 17, 2026) Binary Tree Traversals II

Topic: Trees.
Level Order Traversal
Level Order Traversal II
Pre-order Traversal

Level Order Traversal: (1) visit myself, (2) visit children in-order from left to right.
Level Order Traversal II: (1) visit all leaf nodes in order from left to right, (2) visit all parents in order from left to right.

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def levelOrder(self, root: TreeNode | None) -> list[list[int]]:
        if(root == None):
            return []
        queue = [root]
        res = []
        while(queue):
            lvl = []
            for i in range(len(queue)):
                node = queue.pop(0)
                lvl.append(node.val)
                if(node.left != None):
                    queue.append(node.left)
                if(node.right != None):
                    queue.append(node.right)
            res.append(lvl)

        # Another approach here
        # while len(queue)>0:
        #     lvl = []
        #     for ele in queue:
        #         lvl.append(ele)
        #     for i in range(len(lvl)):
        #         queue.pop(0)
        #     for ele in lvl:
        #         if(ele.left != None):
        #             queue.append(ele.left)
        #         if(ele.right != None):
        #             queue.append(ele.right)
        #     res.append([ node.val for node in lvl ])

        # return reverse(res) -- for Level Order Traversal II
        return res

Pre-order Traversal: in this version of the problem, you were tasked with coming up with a non-recursive algorithm to perform this traversal.

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def preorderTraversal(self, root: TreeNode | None) -> list[int]:
        if(root == None):
            return []
        res = []
        stack = [root]
        while(len(stack)>0):
            curr = stack.pop()
            res.append(curr.val)
            if(curr.right):
                stack.append(curr.right)
            if(curr.left):
                stack.append(curr.left)
        return res

(Sept. 15, 2026) Binary Tree Traversals

Topic: Trees.
In-order Traversal
Post-order Traversal

In-order Traversal: (1) visit left subtree, (2) visit self, (3) visit right subtree

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def inorderTraversal(self, root: TreeNode | None) -> list[int]:
        if(root==None):
            print("null")
            return []
        L = self.inorderTraversal(root.left)
        print(root.val)
        R = self.inorderTraversal(root.right)
        return L + [root.val] + R

Post-order Traversal: (1) visit left subtree, (2) visit right subtree, (3) visit self

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def postorderTraversal(self, root: TreeNode | None) -> list[int]:
        if(root==None):
            print("null")
            return []
        
        L = self.postorderTraversal(root.left)
        R = self.postorderTraversal(root.right)
        print(root.val)
        return L  + R + [root.val]

(Sept. 10, 2026) Majority Element

Topic: Hashing.
Link

Solution with a hashmap

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        if(len(nums) == 1):
            return nums[0]
        majority = (len(nums)//2)+1
        hashtable = {}
        for num in nums:
            if num in hashtable:
                hashtable[num] += 1
                if(hashtable[num] >= majority):
                    return num
            else:
                hashtable[num] = 1

More clever solution just by smartly counting.

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int output = 0;
        int majority = 0;
        for(int num: nums) {
            if(majority == 0) {
                output = num;
                majority++;
            }
            else if(num == output) {
                majority++;
            }
            else {
                majority--;
            }
        }
        return output;
    }
};

(Sept. 8, 2026) Two Sum, Longest Substring without Repeating Characters

Topic: Hashing.
Link 1, Link 2

Two Sum

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        # key-value pair (num, index)
        # nums[index] = num
        seen = {} 
        for i in range(len(nums)):
            num = nums[i]
            comp = target-num
            if(comp in seen): # check if comp is a key in seen
                return [i, seen[comp]]
            seen[num] = i
        return []
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> seen;

        vector<int> indices {-1,-1};

        for(int i = 0; i < nums.size(); ++i) {
            seen[nums[i]] = i;
        }

        for(int i = 0; i < nums.size(); ++i) {
            int comp = target-nums[i];
            if(seen[comp] && seen[comp] != i) {
                indices[0] = i;
                indices[1] = seen[comp];
                break;
            }
        }
        return indices;

    }
};

Longest Substring Without Repeating Characters

Slow solution

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // Idea: 2 pointers
        int max_len = 0;

        unordered_set<char> char_set;
        int left = 0;
        for(int right = 0; right < s.size(); ++right) {
            if(char_set.count(s[right]) == 0) {
                char_set.insert(s[right]);
                max_len = max(max_len, (right-left+1));
            }
            else {
                while(char_set.count(s[right])) {
                    char_set.erase(s[left]);
                    ++left;
                }
                char_set.insert(s[right]);
            }
        }

        // Solution using pointers
        // auto left = s.begin();
        // for(auto right = s.begin(); right != s.end(); right++) {
        //     if(char_set.count(*right)==0) {
        //         char_set.insert(*right);
        //         max_len = max(max_len, static_cast<int>(right-left+1));
        //     }
        //     else {
        //         while(char_set.count(*right)) {
        //             char_set.erase(*left);
        //             left++;
        //         }
        //         char_set.insert(*right);
        //     }
        // }

        return max_len;
    }
};

Faster solution

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // Idea: 2 pointers
        int max_len = 0;
        int seen_char[256] {};
        int left = 0;
        int diff = 0;

        for(int right = 0; right < s.size(); ++right) {
            ++diff;
            if(!seen_char[s[right]]) {
                ++seen_char[s[right]];
                max_len = (diff > max_len) ? diff : max_len;
            }
            else {
                while(seen_char[s[right]]) {
                    --seen_char[s[left]];
                    ++left;
                    --diff;
                }
                ++seen_char[s[right]];
            }
        }

        return max_len;
    }
};

(Sept. 03, 2026) Wriggle Sort II

Topic: Sorting.
Link

We give both a C++ and a Python solution. In class, the Python solution was faster and used less memory.

class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<int> temp(nums.size());
        int end = nums.size()-1;
        for(int i = 1; i < nums.size(); i=i+2) {
            temp[i] = nums[end];
            --end;
        }

        for(int i = 0; i < nums.size(); i=i+2) {
            temp[i] = nums[end];
            --end;
        }
        for(int i = 0; i < nums.size(); ++i) {
            nums[i] = temp[i];
        }
    }
};

class Solution:
    def wiggleSort(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        nums.sort(reverse=True)
        pivot = len(nums) // 2
        smaller_nums = nums[pivot:]
        larger_nums = nums[:pivot]
        nums.clear()
        for i in range(len(larger_nums)):
            nums.append(smaller_nums[i])
            nums.append(larger_nums[i])
        if(len(smaller_nums) > len(larger_nums)):
            nums.append(smaller_nums[-1])

Implementing the above Python code in C++ gives us a solution that beats 100% of all other solutions, while using less memory than ~71% of all other solutions.

class Solution {
public:
    void wiggleSort(vector<int>& nums) {
        int n = nums.size();
        if(n <= 1) return;
        sort(nums.begin(), nums.end(), greater<int>());
        int pivot = nums.size() / 2;
        vector<int> larger(nums.begin(), nums.begin()+pivot);
        vector<int> smaller(nums.begin()+pivot, nums.end());
        nums.clear();
        int i = 0;
        while(i < larger.size()) {
            nums.push_back(smaller[i]);
            nums.push_back(larger[i]);
            ++i;
        }
        if(n%2 == 1) {
            nums.push_back(smaller[i]);
        }
    }
};

(Sept. 01 and 03, 2026) Sort the Jumbled Numbers

Topic: Sorting.
Link

The code for this problem is much easier to implement in Python.

class Solution:
    def sortJumbled(self, mapping: List[int], nums: List[int]) -> List[int]:
        d = 9 # Problem statement tells us that the maximum number of digits is 9
        temp = []
        # Below, we will translate each item num in nums into a tuple 
        # (num, newnum), where newnum is num converted using mapping
        for num in nums:
            if(num == 0): # need to handle the edge case of item == 0
                temp.append((mapping[num], 0))
            else:
                i = 0
                newnum = 0
                while(num // (10**i) > 0):
                    digit = (num // (10**i)) % 10
                    newnum = newnum + mapping[digit]*(10**i)
                    i = i+1
                temp.append((newnum, item))

        # here, we perform radix sort
        buckets = [[] for i in range(10)]
        for i in range(d):
            for tup in temp:
                digit = (tup[0] // (10**i)) % 10
                buckets[digit].append(tup)
            temp.clear()
            for j in range(10):
                size = len(buckets[j])
                for k in range(size):
                    temp.append(buckets[j][k])
                buckets[j].clear()
        return [ tup[1] for tup in temp ]

(Aug. 27, 2026) Two Sum

Topic: Sorting.
Link

/*
Given an unsorted integer array, find a pair with the given sum in it.
• Each input can have multiple solutions. The output should match with 
    either one of them.
• The solution can return pair in any order. If no pair with the given 
    sum exists, the solution should return the pair (-1, -1).
*/

class Solution
{
public:
	pair<int,int> findPair(vector<int> const &nums, int target)
	{
		// Write your code here...
		// Method 1: O(n^2) via checking all possible pairs
		for(int i = 0; i < nums.size(); ++i) {
			for(int j = i+1; j < nums.size(); ++j) {
				int sum = nums[i]+nums[j];
				if(sum == target) {
					return pair(nums[i], nums[j]);
				}
			}
		}
		return pair(-1,-1);
		
		// Method 2: O(n log n) via Sorting
		vector<int> numsCopy(nums);
		std::sort(numsCopy.begin(), numsCopy.end());
		int low = 0;
		int high = numsCopy.size()-1;
		while(low < high) {
			int sum = numsCopy[low]+numsCopy[high];
			if(sum == target) {
				return pair(numsCopy[low], numsCopy[high]);
			}
			else if(sum < target) {
				++low;
			}
			else --high;
		}
		return pair(-1,-1);
	}
};

(Aug. 25, 2026) Sorting Binary Array and Dutch National Flag Problem

Topic: Sorting.
Link 1, Link 2

/*
Given a binary array, in-place sort it in linear time and 
constant space. The output should contain all zeroes, followed by all ones.
*/

class Solution
{
public:
	void sortArray(vector<int> &nums)
	{
        // Method 1: Counting Sort
        // Algorithm:
        //     Count the number of 0s, followed by the number of 1s.
        //     Add the correct number of 0s to the front of the vector,
        //     followed by the correct number of 1s.
        
        int num_0 = 0;
        int num_1 = 1;

        for(int i = 0; i < nums.size(); ++i) {
            if(nums[i] == 0) {
                nums_0++;
            }
            else {
                nums_1++;
            }
        }
        for(int i = 0; i < nums.size(); ++i) {
            if(i < num_0) {
                nums[i] = 0;
            }
            else nums[i] = 1;
        }


        // Method 2: Two Pointers
        // Algorithm:
        //     Have a pointer at the start (left) and end (right) 
        //     of the vector. If start is 1 and end is 0, swap 
        //     and move pointers closer. If left is 1 and right
        //     is 1, decrement right until a 0 is found. Swap for
        //     left = 0 and right = 0.

        int left = 0;
        int right = nums.size();
        while(left < right) {
            if(nums[left] > nums[right]) {
                nums[left] = 0;
                nums[right] = 1;
                ++left;
                --right;
            }
            else if(nums[left] == 0 && nums[right] == 0) {
                ++left;
            }
            else if(nums[left] == 1 && nums[right] == 1) {
                --right;
            }
            else {
                ++left;
                --right;
            }
        }
	}
};
/*
Given an array containing only 0’s, 1’s, and 2’s, 
in-place sort it in linear time and using constant space.
*/

class Solution
{
public:
	void sortArray(vector<int> &nums)
	{
		// Method 1: Counting Sort

        int num_0;
        int num_1;
        int num_2;
        for(int i = 0; i < nums.size(); ++i) {
            switch(nums[i]) {
                case 0:
                    ++num_0;
                    break;
                case 1:
                    ++num_1;
                    break;
                default:
                    ++num_2;
                    break;
            }
        }
        for(int i = 0; i < nums.size(); ++i) {
            if(i < num_0) {
                nums[i] = 0;
            }
            else if(i < num_0 + num_1) {
                nums[i] = 1;
            }
            else nums[i] = 2;
        }


        // Method 2: "Quick" sort
        int pivot = 1;
        int start = 0;
        int end = nums.size()-1;
        int mid = 0;

        while(start <= end) {
            if(nums[mid] < pivot) {
                int tmp = nums[mid];
                nums[mid] = nums[start];
                nums[start] = tmp;
                ++start;
                ++mid;
            }
            else if(nums[mid] > pivot) {
                int tmp = nums[end];
                nums[end] = nums[mid];
                nums[mid] = tmp;
                --end;
            }
            else ++mid;
        }
	}
};

  1. A complete binary tree is a binary tree where every non-leaf node has exactly 2 children. If there are 2n2^n leaf nodes, then a complete binary tree has 2n+1−12^{n+1}-1 nodes. ↩