Algorithm Fundamentals
Core Data Structure Categories
Data structures can be classified into several fundamental types:
- Linear Structures: Basic arrangements including arrays, linked lists, stacks, queues, and hash tables
- Tree Structures: Hierarchical organizations like binary trees and heaps
- Graph Structures: Complex networks representing many-to-many relationships
- Advanced Structures: Skip lists, hash-linked structures, and bitmaps
Time Complexity Analysis
Time complexity T(n) represents the relative execution time as a function of input size n. For common algorithmic patterns:
Asymptotic Notation: When a function f(n) exists such that lim(T(n)/f(n)) approaches a non-zero constant as n approaches infinity, we denote T(n) = O(f(n)).
Derivation Prinicples:
- Represent constant-time operations with coefficient 1
- Retain only the highest-order term in polynomial expressions
- Eliminate leading coefficients from dominant terms
Space Complexity Metrics
Space requirements vary by algorithm design:
- Constant Space: O(1) for fixed memory allocation
- Linear Space: O(n) scaling with input size
- Quadratic Space: O(n²) for two-dimensional structures
- Recursive Space: Stack depth proportional to recursion levels
Fundamental Data Operations
Array Manipulation
Arrays excel in read-heavy scenarios due to sequential storage and O(1) access/update times. Dynamic resizing accommodates overflow conditions.
class DynamicArray:
def __init__(self, initial_capacity=4):
self.data = [None] * initial_capacity
self.length = 0
def add_element(self, position, value):
if position < 0 or position > self.length:
raise IndexError("Position exceeds current bounds")
if self.length >= len(self.data):
self._expand_storage()
for idx in range(self.length - 1, position - 1, -1):
self.data[idx + 1] = self.data[idx]
self.data[position] = value
self.length += 1
def _expand_storage(self):
new_storage = [None] * (len(self.data) * 2)
for i in range(self.length):
new_storage[i] = self.data[i]
self.data = new_storage
def display_contents(self):
for i in range(self.length):
print(self.data[i])
container = DynamicArray(4)
container.add_element(0, 100)
container.add_element(0, 110)
container.add_element(0, 150)
container.display_contents()
Deletion and insertion operations maintain O(n) complexity due to element shifting requirements.
Linked List Implementation
Linked structures consist of nodes with data and references, enabling dynamic memory allocation without contiguous storage.
class ListNode:
def __init__(self, content):
self.content = content
self.successor = None
class ChainStructure:
def __init__(self):
self.node_count = 0
self.initial_node = None
self.final_node = None
def retrieve_node(self, position):
if position < 0 or position >= self.node_count:
raise IndexError('Index beyond structure limits')
current = self.initial_node
for _ in range(position):
current = current.successor
return current
def insert_item(self, content, position):
if position < 0 or position > self.node_count:
raise IndexError('Invalid insertion point')
fresh_node = ListNode(content)
if self.node_count == 0:
self.initial_node = fresh_node
self.final_node = fresh_node
elif position == 0:
fresh_node.successor = self.initial_node
self.initial_node = fresh_node
elif position == self.node_count:
self.final_node.successor = fresh_node
self.final_node = fresh_node
else:
previous = self.retrieve_node(position - 1)
fresh_node.successor = previous.successor
previous.successor = fresh_node
self.node_count += 1
def delete_item(self, position):
if position < 0 or position >= self.node_count:
raise IndexError('Removal index invalid')
if position == 0:
removed = self.initial_node
self.initial_node = self.initial_node.successor
elif position == self.node_count - 1:
previous = self.retrieve_node(position - 1)
removed = previous.successor
previous.successor = None
self.final_node = previous
else:
previous = self.retrieve_node(position - 1)
subsequent = previous.successor.successor
removed = previous.successor
previous.successor = subsequent
self.node_count -= 1
return removed
def show_elements(self):
pointer = self.initial_node
while pointer:
print(pointer.content)
pointer = pointer.successor
sequence = ChainStructure()
sequence.insert_item(30, 0)
sequence.insert_item(40, 0)
sequence.insert_item(90, 2)
sequence.insert_item(50, 3)
sequence.insert_item(60, 1)
sequence.delete_item(0)
sequence.show_elements()
Node access requires O(n) time, while insertion/deletion after position identification takes O(1).
Stack Operations
Stacks follow Last-In-First-Out (LIFO) principle with O(1) push/pop operations:
class StackContainer:
def __init__(self):
self.elements = []
def push_item(self, item):
self.elements.append(item)
def pop_item(self):
if not self.elements:
raise IndexError("Empty stack")
return self.elements.pop()
def peek_top(self):
if not self.elements:
raise IndexError("Empty stack")
return self.elements[-1]
Queue Management
Queues implement First-In-First-Out (FIFO) behavior:
class QueueSystem:
def __init__(self, max_capacity):
self.items = [None] * max_capacity
self.front_index = 0
self.rear_index = 0
self.max_size = max_capacity
def enqueue_item(self, item):
if (self.rear_index + 1) % self.max_size == self.front_index:
raise OverflowError("Queue full")
self.items[self.rear_index] = item
self.rear_index = (self.rear_index + 1) % self.max_size
def dequeue_item(self):
if self.front_index == self.rear_index:
raise IndexError("Queue empty")
removed = self.items[self.front_index]
self.front_index = (self.front_index + 1) % self.max_size
return removed
Full queue condition: (rear_index + 1) % array_length = front_index
Hash Table Functionality
Hash tables provide near-constant O(1) key-value mapping through hash functions:
class HashMapping:
def __init__(self, capacity=16):
self.capacity = capacity
self.buckets = [[] for _ in range(capacity)]
def _compute_hash(self, key):
return hash(key) % self.capacity
def store_pair(self, key, value):
bucket_index = self._compute_hash(key)
bucket = self.buckets[bucket_index]
for i, (existing_key, _) in enumerate(bucket):
if existing_key == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
def retrieve_value(self, key):
bucket_index = self._compute_hash(key)
bucket = self.buckets[bucket_index]
for existing_key, value in bucket:
if existing_key == key:
return value
raise KeyError(f"Key {key} not found")
mapping = HashMapping()
mapping.store_pair("identifier_1", "data_value")
result = mapping.retrieve_value("identifier_1")
Collision resolution uses chaining methodology where multiple entries share the same hash index.