Computer Science Notes
Contents
Basics
Some good development practices:
- being resourceful (Learn to solve problems, develop critical thinking, discuss issues with teammates, read documentation)
- version control (advanced concepts in GIT and GITHUB)
- data structure & code efficiency ( learn how to manipulate data in a creative way so that you can save time and storage)
- scripting and automation (learn how to speed making scripts of tasks that are repetitive and that can make you save a ton of time)
- Asynchronous programming (learn how run programs in “parallel” )
- CI & CD : continuous integration and continuous deployment (take at least the basic concepts)
- clear and precise and accurate communication
Bits/Bytes
0
s and1
s are known as bits–binary digits- Bytes are
8
bits
Computer
input/output device
: peripherals, keyboards, screens.storage
: non-volatile. Magnetic particles orientated in a0
/1
position. Disk vs memory. Disk applies a head that pulses electricity. Other method tunnel electrons into special circuits on memory’s chip, and removes it with a ‘flash’ of electrcity.memory
: RAM. volatile. Gives the ability to access memory in arbitrary order in a few ticks over hundreds from disks.processor
: executes read/write instructions, accesses memory as well as memory from cache (a mini-RAM, accessible in 1 clock tick). Initiates BIOS, then OS.clock
: The clock speed is typically measured in Hertz (Hz) and represents the number of clock cycles a CPU can execute in one second. Common units for clock speed include Megahertz (MHz) and Gigahertz (GHz), where 1 MHz equals one million cycles per second, and 1 GHz equals one billion cycles per second
8 Patterns Everyone Should Know
Creational, Structural, Behvaioral Patterns (in Python
)
Factory (Creational)
Create a ‘factory’ that instantiates an object for us using parameters as input for what ’type’ of object we want.
class Dog:
def __init__(self, name):
self._name = name
def speak(self):
return "Woof!"
class Cat:
def __init__(self, name):
self._name = name
def speak(self):
return "Meow!"
def get_pet(pet="dog"):
pets = dict(dog=Dog("Hope"), cat=Cat("Peace"))
return pets[pet]
my_pet = get_pet("dog")
print(my_pet.speak()) # Output: Woof!
Builder (Creational)
Create a builder that has methods to “build” our object instead of passing parameters during instantiation.
class Burger:
def __init__(self, size, cheese=False, pepperoni=False, lettuce=False):
self.size = size
self.cheese = cheese
self.pepperoni = pepperoni
self.lettuce = lettuce
class BurgerBuilder:
def __init__(self, size):
self.burger = Burger(size)
def add_cheese(self):
self.burger.cheese = True
return self
def add_pepperoni(self):
self.burger.pepperoni = True
return self
def add_lettuce(self):
self.burger.lettuce = True
return self
def build(self):
return self.burger
builder = BurgerBuilder(4)
burger = builder.add_cheese().add_pepperoni().build()
Singleton (Creational)
An object which only has a single instance instantiated, which is referenced by other objects. AKA a shared mutable state.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # Output: True
Observer (Behavioral)
A subject/publisher can maintain a list of observers to which one action may be called and act on observers.
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self, message):
for observer in self._observers:
observer.update(message)
class Observer:
def __init__(self, name):
self._name = name
def update(self, message):
print(f"{self._name} received message: {message}")
# Usage
subject = Subject()
observer1 = Observer("Observer 1")
observer2 = Observer("Observer 2")
subject.attach(observer1)
subject.attach(observer2)
subject.notify("Hello!")
# Output:
# Observer 1 received message: Hello!
# Observer 2 received message: Hello!
Iterator (Behavioral)
Defines how the values of an object can be iterated through.
class MyIterator:
def __init__(self, data):
self._data = data
self._index = 0
def __iter__(self):
return self
def __next__(self):
if self._index < len(self._data):
result = self._data[self._index]
self._index += 1
return result
else:
raise StopIteration
# Usage
my_list = [1, 2, 3, 4, 5]
my_iter = MyIterator(my_list)
for item in my_iter:
print(item)
# Output:
# 1
# 2
# 3
# 4
# 5
Strategy (Behavioral)
Fancy version of using an interface. Uses Open/Closed principle. You create an interface and define logic under it.
“programming at an interface level”
class PaymentStrategy:
def pay(self, amount):
pass
class CreditCardPayment(PaymentStrategy):
def pay(self, amount):
print(f"Paid ${amount} with Credit Card")
class PayPalPayment(PaymentStrategy):
def pay(self, amount):
print(f"Paid ${amount} with PayPal")
class ShoppingCart:
def __init__(self, payment_strategy):
self._items = []
self._payment_strategy = payment_strategy
def add_item(self, item):
self._items.append(item)
def checkout(self):
total = sum(item.price for item in self._items)
self._payment_strategy.pay(total)
# Usage
cart = ShoppingCart(CreditCardPayment())
cart.add_item(Item("Item 1", 25))
cart.add_item(Item("Item 2", 15))
cart.checkout()
# Output: Paid $40 with Credit Card
Adapter (Structural)
Adapting a class into another s.t. it allows two incompatible interfaces to work together. It acts as a bridge between two existing classes, making them work together without changing their source code.
# Existing class with an incompatible interface
class OldSystem:
def do_old_stuff(self):
return "Old System is doing its stuff."
# Adapter class that adapts the OldSystem to a new interface
class Adapter:
def __init__(self, old_system):
self._old_system = old_system
def do_new_stuff(self):
# Delegate the call to the old system's method
return self._old_system.do_old_stuff()
# Client code
def client_code(target):
result = target.do_new_stuff()
print(result)
# Usage
old_system = OldSystem() # Existing class instance
adapter = Adapter(old_system) # Adapter wraps the existing class
client_code(adapter) # Client interacts with the adapter
Facade (Structural)
Design pattern that provides a simplified, higher-level interface to a complex system, a set of interfaces, or a group of classes. It hides the complexities of the underlying components and provides a unified interface that is easier to use. The facade pattern is often used to improve the usability and maintainability of a system by reducing the dependencies between clients and subsystems.
class SubsystemA:
def operation_a(self):
return "Subsystem A is doing its operation."
class SubsystemB:
def operation_b(self):
return "Subsystem B is doing its operation."
class Facade:
def __init__(self):
self._subsystem_a = SubsystemA()
self._subsystem_b = SubsystemB()
def do_complex_operation(self):
result_a = self._subsystem_a.operation_a()
result_b = self._subsystem_b.operation_b()
return f"Complex operation: {result_a} {result_b}"
# Usage
facade = Facade()
result = facade.do_complex_operation()
Complexity
Time Complexity (TC)
Time complexity measures time taken to execute each statement of code in an algorithm. It is going to give information about the variation (increase or decrease) in execution time when the number of operations (increase or decrease) in an algorithm. Time complexity is a function of input length $l$, where output is time $t$ or: $$ f(l) = t $$
or more commonly: $$ O(x), x \in 1, n, n^2, \log n, n \log n, 2^n, n! … $$
Where $O(x)$ denotes O-notation, or “Order” notation. See Algorithm Efficiency.
A time-complexity analysis will require evaluating run time on each step of a program.
Time complexity deals with finding out how the computational time of an algorithm changes with the change in size of the input.
Space Complexity (SC)
While Space Complexity may also be described by O-notation, they are unrelated and not dependent on each other. For example, an $O(1)$ space complexity denotes the same amount of space usage per input data of any size.
A program that uses a variable to swap values, while swapping multiple values for the variable, will still only use one variable.
Space complexity deals with finding out how much (extra) space would be required by the algorithm with change in the input size.
References: Differences between time complexity and space complexity?
Data Structures and Algorithms
Algorithms
See section header link for detailed notes.
Dynamic Programming & Memoization
A way of making algorithms more efficient by storing the intermediary results.
- Memoization: In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again
Examples:
fib(n)
Algorithm Efficiency: Big $O/\Omega$-Notation
- Space complexity: relationship between growth of input size and growth of space needed
O-notation
Ranked closeness from Elements to Operations (Also good to bad)
$O(1)$, $O(log n)$, $O(n)$, $O(n log n)$, $O(n^2)$, $O(2^n)$, $O(n!)$
- denotes maximum bound for runtime.
$\Omega$-notation
$\Omega (n)$
- denotes minimum bound for runtime.