MIT 6.
0001 Comp Sci and Programming in Python Lecture Notes
Lecture 1: Introduction to Python Programming
1. Introduction:
● The lecture begins with an analogy of a recipe to explain the concept of
algorithms.
● Key components of a recipe:
Sequence of steps.
Flow of control (decisions and repetitions).
A way to stop.
2. Algorithms:
● A recipe in scientific terms is an algorithm.
● An algorithm is a set of instructions to achieve a particular task.
3. History of Computers:
● Two types of historical computers:
Fixed Program Computers: Can only perform specific tasks. E.g.,
calculators.
Stored Program Computers: Can store and execute a sequence of
instructions.
4. Basic Machine Architecture:
● The Central Processing Unit (CPU) is the heart of the computer.
● Four main components:
Memory
Input/Output
Arithmetic Logic Unit (ALU): Performs basic operations.
Control Unit: Contains the program counter.
5. Expressions and Primitives:
● Alan Turing introduced six primitives: Move left, move right, read/write, scan, and
do nothing.
● Expressions combine primitives in a programming language.
● In English, primitive constructs are words. In Python, they are data types like
integers, floats, booleans, etc.
6. Syntax and Semantics:
● Syntax refers to the structure of the language.
● Semantics refers to the meaning of the language.
● In programming, a set of instructions has only one meaning, but it might not be
the intended meaning by the programmer.
7. Python Programming:
● Python programs are sequences of definitions and commands.
● Everything in Python is an object.
● Objects have types, which determine the operations that can be performed on
them.
8. Scalar Objects in Python:
● Scalar objects are indivisible.
● Types of scalar objects: Integers, floats, booleans, and none type.
● Use the type() function to determine the type of an object.
9. Printing in Python:
● Use the print() function to display values.
● The shell provides immediate feedback, but for more complex programs, use the
editor.
10. Variables and Assignments:
● Variables store values for later use.
● Use the = sign for assignments.
● Variables can rebound to new values.
● The value of a variable doesn't change unless explicitly changed.
11. Conclusion:
● The lecture concludes with a reminder that computers only execute the
instructions given to them.
● The next lecture will delve into control flow in programming.
Lecture 2: Core Elements of Programs
1. Control Flow and Decision Making
● Example: Navigating MIT campus for free food.
● Algorithm: Keep right hand on a wall and navigate.
● Steps:
If no wall on the right, turn right.
If wall on the right but path forward, move forward.
If walls on right and front, turn left.
If surrounded by walls, turn around.
● Keywords in Programming:
● If, else, elif (else-if)
● Computers don't make decisions. Programmers build decisions into
programs.
2. Basic Control Structures
● If Statement:
● Checks a condition. If true, executes a block of code.
● Syntax:
if condition:
# code block
● If-Else Statement:
● Checks a condition. If true, executes one block of code, otherwise
executes another.
● Syntax:
if condition:
# code block 1
else:
# code block 2
● If-Elif-Else Statement:
● Multiple decision points.
● Syntax:
if condition1:
# code block 1
elif condition2:
# code block 2
else:
# code block 3
3. Loops
● Example: Legend of Zelda's Lost Woods.
● Problem: Infinite loop if user keeps moving in one direction.
● Solution: Use loops to handle repetitive tasks.
● While Loop:
● Repeats a block of code as long as a condition is true.
● Syntax:
while condition:
# code block
● Can lead to infinite loops if not careful.
● For Loop:
● Iterates over a sequence (range, list, etc.).
● Syntax:
for variable in range(start, stop, step):
# code block
● range() function: Generates a sequence of numbers.
● range(stop): Generates numbers from 0 to stop-1.
● range(start, stop): Generates numbers from start to stop-1.
● range(start, stop, step): Generates numbers from start to stop-1,
incrementing by step.
● Break Statement:
● Exits the loop prematurely.
● Syntax:
if condition:
break
4. Comparison between For and While Loops
● For Loops:
● Used when the number of iterations is known.
● Has an inherent counter.
● While Loops:
● Useful for unpredictable tasks, like user input.
● Can use a counter, but must be initialized and incremented manually.
● Can be rewritten as a for loop, but the reverse isn't always true.
Lecture 3: Simple Algorithms and Debugging Techniques
1. Robot Cheerleaders:
● Example of a program that acts as a cheerleader.
● User inputs a word and a level of enthusiasm.
● The program spells out the word like a cheerleader and repeats it based on the
enthusiasm level.
● Demonstrates the use of loops and string manipulation.
2. Toolbox Expansion:
● Added knowledge:
● Integers, floats, booleans.
● String manipulation.
● Math operations.
● Conditionals and branching.
● Loops: for and while.
3. Algorithms:
● Don't be intimidated by the term "algorithm".
● Three algorithms to be discussed:
Guess and Check
Approximation Algorithm
Bisection Search
4. Guess and Check Method:
● Also known as exhaustive enumeration.
● Start with a guess and check if it's correct.
● If not, guess another value systematically.
● Continue until a solution is found or all possible values are exhausted.
5. Approximate Solutions:
● Useful when an exact solution isn't necessary.
● Start with a guess and increment by a small value.
● Continue until a "good enough" solution is found.
● Two factors to consider:
Step size: Smaller step size = more accurate but slower.
Epsilon: Determines how close is "good enough".
6. Bisection Search:
● A more efficient method than guess and check.
● Divide the search space in half with each guess.
● Choose a guess halfway between the current high and low boundaries.
● Adjust boundaries based on whether the guess is too high or too low.
● Continue until the guess is within an acceptable range (epsilon).
● Converges on the order of log base n, making it powerful for large search
spaces.
7. Practical Demonstration:
● A game was played to guess a number between 0 and 100.
● Using bisection search, the number was guessed in fewer steps than traditional
methods.
● Demonstrated the efficiency of bisection search.
8. Limitations and Adjustments:
● The bisection search code shown only works for positive cubes.
● For numbers less than 1, the cube root will be outside the initial boundary.
● A small adjustment can be made to the code to account for this.
Conclusion:
● Algorithms are essential tools in computer science and programming.
● Different algorithms have their strengths and weaknesses.
● Bisection search is a powerful method for quickly narrowing down a search
space.
Lecture 4: Iterative and Recursive Algorithms
1. Functions:
● Functions are like mini-programs within a program.
● They allow for code reuse and modularization.
● Functions can take parameters and return values.
Example:
python
Copy code
def is_even(i):
"""
Input: i, a positive integer
Returns True if i is even, otherwise False
"""
return i % 2 == 0
2. Scope:
● Scope refers to the environment in which variables exist and can be accessed.
● Each function has its own scope.
● Variables defined inside a function are not accessible outside that function.
3. Function Calls and Return Values:
● Functions can be called from other parts of the program.
● The return statement determines the value that the function sends back to the
caller.
● If no return statement is provided, Python will return None.
4. Nested Functions:
● Functions can be defined inside other functions.
● Nested functions have access to variables from the enclosing function.
Example:
python
Copy code
def outer_function(x):
def inner_function(y):
return y + x
return inner_function
5. Global vs. Local Variables:
● Variables defined inside a function are local to that function.
● Variables defined outside any function are global.
● Local variables cannot be accessed outside their function.
● Global variables can be accessed from any function, but it's generally not
recommended.
6. Passing Functions as Parameters:
● Since everything in Python is an object, functions can be passed as parameters
to other functions.
7. Importance of Decomposition and Abstraction:
● Decomposition involves breaking a problem down into smaller, more manageable
pieces.
● Abstraction involves hiding the complex reality while exposing only the necessary
parts.
● Using functions allows for both decomposition and abstraction, making code
more readable and maintainable.
8. Debugging:
● Debugging functions can be easier because once a function is verified to work
correctly, it can be reused multiple times without worry.
Conclusion:
● Functions are crucial for writing clean, efficient, and maintainable code.
● Understanding scope is essential to avoid variable access errors.
● Decomposition and abstraction are foundational concepts in computer science
and are facilitated by the use of functions.
Lecture 5: Object-Oriented Programming in Python
1. Introduction to Lists
● Lists are a sequence of values, similar to strings and tuples.
● Lists are mutable, meaning their contents can be changed after they are created.
● Lists are defined using square brackets [] and can contain elements of different
types.
● Example: L = [2, 1, 3]
2. Indexing and Operations on Lists
● Lists can be indexed and sliced like strings.
● L[0] gives the first element (2 in the example).
● L[2] gives the third element (3 in the example).
● L[2] + 1 gives 4.
● L[3] would give an error since the list has only 3 elements.
● Lists are similar to strings and tuples in terms of operations, but they are
mutable.
3. Mutability of Lists
● Lists can be modified after they are created.
● Example: If L = [2, 1, 3], then executing L[1] = 5 would change L to [2, 5, 3].
● Multiple variables can point to the same list. If the list is modified, all variables
pointing to that list will reflect the change.
4. Iterating Over Lists
● It's more Pythonic to iterate over the elements of a list directly rather than using
indices.
● Example:
● python
● Copy code
for i in L:
print(i)
●
5. List Operations
● Lists offer a variety of operations due to their mutability.
● L.append(e) adds element e to the end of the list.
● L1 + L2 combines two lists but doesn't mutate them.
● L1.extend(L2) adds elements of L2 to L1, mutating L1.
● del(L[index]) deletes an element at a specific index.
● L.pop() removes the last element.
● L.remove(e) removes the first occurrence of element e.
6. Converting Strings to Lists and Vice Versa
● list(s) converts string s into a list where each character becomes an element.
● s.split(delimiter) splits a string into a list based on a delimiter.
● 'delimiter'.join(L) joins a list into a string using a delimiter.
7. Sorting and Reversing Lists
● L.sort() sorts the list in place.
● sorted(L) returns a sorted version of the list without modifying the original.
● L.reverse() reverses the list in place.
8. Cloning Lists
● To create a copy of a list, use the slicing notation: L[:].
● This is useful to avoid unintended side effects when modifying lists.
9. Nested Lists and Side Effects
● Lists can contain other lists.
● Modifying a nested list will reflect in all variables pointing to that list.
● Be cautious when modifying lists during iteration to avoid unexpected behaviors.
10. Conclusion
● Lists are versatile and mutable sequences in Python.
● They offer a wide range of operations and methods for manipulation.
● Care should be taken to avoid unintended side effects, especially when working
with aliases or during iteration.
Lecture 6: Algorithmic Complexity and Big O Notation
1. Recursion:
● A method of solving problems by reducing them to smaller versions of the same
problem.
● Example: Towers of Hanoi problem.
2. Towers of Hanoi:
● Problem: Move a stack of disks from one pole to another, with the constraint that
you can only move one disk at a time and you can't place a larger disk on top of
a smaller disk.
● Recursive solution:
● Move n-1 disks to the spare pole.
● Move the nth disk to the target pole.
● Move the n-1 disks from the spare pole to the target pole.
3. Fibonacci Sequence:
● A sequence where each number is the sum of the two preceding ones.
● Recursive solution:
● Base cases:
● Fib(0) = 1
● Fib(1) = 1
● Recursive case:
● Fib(n) = Fib(n-1) + Fib(n-2)
4. Palindromes:
● Words or phrases that read the same backward as forward.
● Recursive solution:
● Check if the first and last characters are the same.
● Check if the substring without the first and last characters is a palindrome.
5. Dictionaries:
● A data structure that pairs keys with values.
● Unlike lists, which are indexed by numbers, dictionaries are indexed by keys.
● Keys must be unique and immutable.
● Values can be any type and can be duplicated.
6. Using Dictionaries:
● Example: Analyzing song lyrics.
● Convert lyrics to frequencies.
● Find the most common words.
● Remove words from the dictionary once counted.
7. Memoization:
● A method of improving the efficiency of recursive algorithms by storing previously
computed results in a dictionary.
● Example: Improving the efficiency of the Fibonacci sequence calculation.
● Store previously computed Fibonacci numbers in a dictionary.
● Check the dictionary before computing a Fibonacci number to see if it has
already been calculated.
Conclusion:
● Recursion is a powerful tool for breaking down complex problems into simpler
versions of the same problem.
● Dictionaries are versatile data structures that can be used to store and retrieve
data efficiently.
● Memoization can significantly improve the efficiency of recursive algorithms.
Lecture 7: Advanced Algorithmic Complexity Analysis
1. Debugging:
● Learning Curve: Debugging is a skill that improves with experience.
● Goal: Achieve a bug-free program.
● Tools:
● Anaconda built-in tools.
● Python tutor.
● Print statements.
● Systematic Approach:
● Use print statements to test hypotheses.
● Place print statements inside functions, loops, and check return values.
● Use the bisection method: Place a print statement approximately halfway
in your code to narrow down the source of the error.
● Study the program code, not just what's wrong.
● Use the scientific method: Observe data, form a hypothesis, conduct
repeatable experiments, and test with simple cases.
2. Error Messages:
● Types of Errors:
● IndexError: Accessing beyond list limits.
● TypeError: Incorrect data type conversion.
● NameError: Accessing undeclared variables.
● SyntaxError: Missing parentheses, colons, etc.
● Logic Errors: These are harder to spot and require more time to debug.
3. Rubber Ducky Debugging:
● Explaining your code to someone else (or even a rubber ducky) can help you
understand and identify issues in your code.
4. Debugging Dos and Don'ts:
● Don't: Write the entire program, then test and debug all at once.
● Do: Use unit testing. Write a function, test it, debug it, and then move on to the
next function.
Exceptions:
● Definition: An error that occurs during program execution.
● Handling Exceptions:
● Use try and except blocks.
● try block contains code that might raise an exception.
● except block handles the exception.
● Can catch specific exceptions, e.g., ValueError or ZeroDivisionError.
● Use else block to execute code when no exception occurs.
● Use finally block to execute code regardless of whether an exception
occurred.
Example:
python
Copy code
try:
val = int(input("Enter a number: "))
except ValueError:
print("That's not a number!")
Assertions:
● Definition: A statement that a certain condition is true.
● Usage: Ensure that preconditions (input specifications) and postconditions
(output specifications) are met.
● Benefits: Spot bugs as soon as they're introduced.
● Syntax: assert condition, "Error message"
Example:
python
Copy code
def avg(grades):
assert len(grades) != 0, "No grades data"
return sum(grades)/len(grades)
Summary:
● Debugging is a crucial skill in programming.
● Use tools and systematic approaches to identify and fix bugs.
● Understand different types of errors and how to handle them.
● Use exceptions to handle unexpected conditions in your code.
● Assertions ensure that conditions are met, helping to spot bugs early.
Lecture 8: Sorting Algorithms and Computational Thinking
1. Defining Classes:
● Classes are defined using the class keyword.
● The __init__ method is a special method used to initialize an object.
● self is a reference to the instance of the object itself. It's used to access variables
that belong to the class.
Example:
python
Copy code
class Coordinate:
def __init__(self, x, y):
self.x = x
self.y = y
2. Using the self Parameter:
● self represents a specific instance of the class.
● It's a placeholder for any instance of the class.
● Always use self. to access attributes and methods of the class.
3. Creating Objects:
● Objects (instances) are created by calling the class.
● The __init__ method is automatically invoked when an object is created.
Example:
python
Copy code
c = Coordinate(3, 4)
origin = Coordinate(0, 0)
4. Accessing Object Attributes:
● Use the dot notation to access object attributes.
Example:
python
Copy code
print(c.x) # Outputs: 3
print(origin.x) # Outputs: 0
5. Methods:
● Methods are functions defined within a class.
● They allow interaction with objects.
● Always include self as the first parameter of any method.
Example:
python
Copy code
def distance(self, other):
return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5
6. Using Methods:
● Methods are invoked using the dot notation.
Example:
python
Copy code
dist = c.distance(origin)
7. Special Methods:
● Methods with double underscores before and after their name are special
methods.
● They allow customization of default Python operations.
Example:
python
Copy code
def __str__(self):
return "<" + str(self.x) + "," + str(self.y) + ">"
8. Printing Objects:
● By default, printing an object provides its memory location.
● The __str__ method allows customization of the string representation of an
object.
9. Arithmetic with Objects:
● Special methods like __add__ and __sub__ allow objects to be added or
subtracted.
Example:
python
Copy code
def __add__(self, other):
return Coordinate(self.x + other.x, self.y + other.y)
10. Fraction Class:
● A class to represent fractions with a numerator and denominator.
Example:
python
Copy code
class Fraction:
def __init__(self, top, bottom):
self.num = top
self.den = bottom
11. Power of OOP:
● OOP allows bundling of objects with the same type.
● Objects have a consistent data representation and methods.
● Builds layers of abstraction, from basic Python objects to custom object types.
Lecture 9: Advanced Sorting Techniques and Analysis
Class Hierarchies and Inheritance
● Hierarchies:
● Classes can be organized in hierarchies.
● Example: All animals have an age, but specific animals like cats, rabbits,
and humans have additional attributes and behaviors.
● A student is a type of person and also an animal. They might have
attributes like a major or favorite subject.
● Parent classes are at the top of the hierarchy, and child classes inherit
from them.
● Inheritance:
● Child classes inherit all attributes and methods from their parent classes.
● They can add new attributes and methods or override existing ones.
● Example: An animal might have a basic speak method, but a cat's speak
method might make it say "meow".
Class Variables vs. Instance Variables
● Instance Variables:
● Specific to each instance of a class.
● Example: self.name or self.age in an animal class.
● Class Variables:
● Shared across all instances of a class.
● If one instance modifies a class variable, all other instances will see the
modified value.
● Example: A tag variable in a rabbit class that keeps track of how many
rabbit instances have been created.
Implementing Class Behavior
● Overriding Methods:
● Child classes can override methods from their parent classes.
● Example: A student's speak method might make them say "I have
homework" instead of just "hello".
● Using Class Variables:
● Class variables can be used to keep track of shared data across
instances.
● Example: A tag class variable in a rabbit class that increments each time a
new rabbit is created.
● Special Methods:
● Python classes can implement special methods to define custom behavior
for built-in operations.
● Example: The __add__ method can be defined to specify what happens
when two instances of a class are added together.
Working with Objects
● Creating and Using Objects:
● Objects can be created from classes and used to store data and perform
operations.
● Example: Creating rabbit objects and using their methods to get their
names, ages, and parents.
● Comparing Objects:
● Objects can be compared based on their attributes.
● Example: Checking if two rabbits have the same parents by comparing
their parent IDs.
Conclusion:
● Object-oriented programming allows for the creation of custom data structures
with specific attributes and behaviors.
● It promotes decomposition and abstraction in programming, making code more
organized and easier to manage.
Lecture 10: Data Structures and Efficiency
Introduction
● Understanding the efficiency of algorithms is essential.
● Asymptotic notation (Big O notation) is used to describe the efficiency of
algorithms.
Big O Notation
● Describes how the runtime of an algorithm grows relative to the input size.
● Focuses on the term that grows most rapidly.
● Ignores additive and multiplicative constants.
Complexity Classes
Constant Time (O(1)): Time taken is independent of the input size.
Logarithmic Time (O(log n)): Time taken increases logarithmically with input size.
Linear Time (O(n)): Time taken increases linearly with input size.
Log Linear (O(n log n)): Between linear and quadratic.
Quadratic Time (O(n^2)): Time taken is proportional to the square of the input
size.
Exponential Time (O(2^n)): Time taken doubles with each additional element in
the input.
Analyzing Algorithms
● Count the number of basic operations an algorithm takes.
● Focus on the worst-case scenario.
Examples
Linear Search:
● Searching for an element in an unsorted list.
● Worst-case scenario: O(n).
Nested Loops:
● Typically quadratic behavior (O(n^2)).
● For each element in one list, the algorithm checks all elements in another
list.
Subset Check:
● Check if one list is a subset of another.
● Nested loops result in O(n^2) complexity.
Intersection of Two Lists:
● Find common elements in two lists.
● Nested loops and list membership checks lead to O(n^2) complexity.
Key Takeaways
● Simple loops typically result in linear complexity.
● Nested loops often lead to quadratic complexity.
● Asymptotic notation helps in understanding the efficiency of algorithms,
especially for large inputs.
Conclusion
● Algorithmic complexity is crucial for designing efficient algorithms.
● Understanding the order of growth helps in predicting the performance of
algorithms on large datasets.
Lecture 11: Advanced Data Structures and Applications
1. Towers of Hanoi:
● The recursive solution for the Towers of Hanoi problem has exponential growth.
● The order of growth is
● 2�
● 2
● n
● .
● The story of the Towers of Hanoi: When the priests in the temple move the entire
stack from one peg to another, the world ends. For
● �=64
● n=64, the number is
● 264
● 2
● 64
● , which is a huge number. Even if one move is made per second, it would take an
incredibly long time to finish.
2. Power Set:
● Definition: For a given set of integers (without repeats), the power set is the
collection of all possible subsets.
● Example: For the set {1, 2, 3, 4}, the power set includes subsets with 0, 1, 2, 3,
and 4 elements.
● Recursive solution:
● Base case: If the list is empty, return a list containing an empty list.
● Recursive case: Generate the power set for the list excluding the last
element. Then, for each subset in that power set, create a new subset that
includes the last element.
● The size of the power set doubles with each additional element in the original set.
3. Code Analysis:
● The code for generating the power set is elegant and concise.
● The recursive solution is more intuitive and cleaner than an iterative solution with
nested loops.
● Complexity analysis:
● The code has a recursive call, which is called
● �
● n times (linear).
● However, there's an inner loop that grows exponentially with the size of
the "smaller" list. This leads to an exponential growth in complexity.
4. Fibonacci Sequence:
● Definition: The nth Fibonacci number is the sum of the (n-1)th and (n-2)th
Fibonacci numbers.
● Iterative solution:
● Complexity:
● �(�)
● O(n) (linear).
● Recursive solution:
● Two recursive calls for each Fibonacci number.
● Complexity: Exponential growth, approximately the golden ratio to the nth
power.
5. Big O Notation:
● A way to compare the efficiency of algorithms.
● Recognizing common patterns in code can help determine the complexity of an
algorithm.
● Aim for algorithms with lower complexity (e.g., linear or logarithmic) and avoid
exponential algorithms when possible.
6. Built-in Python Functions:
● Python provides several built-in functions for lists and dictionaries.
● The complexity of these functions varies:
● For lists: Indexing is
● �(1)
● O(1) (constant time), while operations like comparison or removal are
● �(�)
● O(n) (linear time).
● For dictionaries: Many operations are
● �(1)
● O(1), but some can be
● �(�)
● O(n) in the worst case.
Conclusion:
● Understanding the complexity of algorithms is crucial for efficient programming.
● Recognizing patterns in code can help in predicting the order of growth and
making better algorithmic choices.
Lecture 12: Computational Thinking and Problem Solving
1. Selection Sort:
● Overview: Iteratively selects the smallest element from the unsorted portion and
swaps it with the first unsorted element.
● Mechanism:
● Start with the first element.
● Search for the smallest element in the list.
● Swap the smallest element with the first element.
● Move to the next element and repeat until the entire list is sorted.
● Complexity: Quadratic (O(n^2)).
2. Merge Sort:
● Overview: A divide and conquer algorithm that splits a list into halves, sorts each
half, and then merges the sorted halves.
● Mechanism:
● If the list has 0 or 1 element, it's already sorted.
● Otherwise, split the list into two halves.
● Recursively sort both halves.
● Merge the two halves to produce a single sorted list.
● Visualization:
● Start with a large unsorted list.
● Continuously split the list until you have lists of size 0 or 1.
● Merge these small lists pairwise into larger sorted lists.
● Complexity: Log-linear (O(n log n)).
3. Merging Process:
● Overview: Given two sorted lists, the merging process combines them into a
single sorted list.
● Mechanism:
● Compare the smallest elements of each list.
● Add the smaller element to the result list.
● Move the pointer in the list from which the smaller element was taken.
● Repeat until one list is empty, then append the remaining elements from
the other list.
● Complexity: Linear (O(n)).
4. Merge Sort Visualization:
● Divide: Continuously split the list until you reach lists of size 0 or 1.
● Conquer: Merge these small lists to form larger sorted lists.
● Example: Sorting a group of students by height. Split the group into pairs, sort
each pair, then merge the pairs.
5. Computational Thinking:
● Abstraction: Capture computation inside a procedure. Delegate tasks and trust
the procedure to deliver the expected results.
● Classes & Methods: Modularize systems to combine data and operations.
● Algorithm Complexity: Understand the efficiency of algorithms and their impact
on performance.
6. Computational Modes of Thinking:
● Abstraction: Identify the core essence of a problem.
● Algorithmic Thinking: Design a sequence of steps to solve a problem.
● Automated Execution: Implement the algorithm in a programming language for
automated execution.
7. Conclusion:
● Computational thinking is becoming a fundamental skill for well-educated
individuals.
● The ability to think and act like a computer scientist involves understanding
abstractions, algorithms, and automated execution.
● Recursion is a powerful tool that appears in various domains, and thinking
recursively can help in breaking down complex problems.