0% found this document useful (0 votes)
3 views14 pages

Algorithm With Python

The document provides a comprehensive overview of key computer science topics, including data structures, algorithm analysis, and sorting and searching algorithms. It covers both basic and advanced data structures, their theoretical explanations, and practical code examples, such as Red-Black Tree insertion and Quicksort. Additionally, it discusses algorithm analysis techniques and complexities, emphasizing the importance of understanding time and space efficiency in algorithm design.

Uploaded by

Ümit BOSTANCI
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views14 pages

Algorithm With Python

The document provides a comprehensive overview of key computer science topics, including data structures, algorithm analysis, and sorting and searching algorithms. It covers both basic and advanced data structures, their theoretical explanations, and practical code examples, such as Red-Black Tree insertion and Quicksort. Additionally, it discusses algorithm analysis techniques and complexities, emphasizing the importance of understanding time and space efficiency in algorithm design.

Uploaded by

Ümit BOSTANCI
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Of course.

This is a comprehensive request that covers the core of a computer


science curriculum. Providing a full 7 pages per subject (which would total over 50
pages) in a single response is not feasible, but I will provide a detailed, in-depth
explanation for each of the seven subjects, structured to mirror the chapters of the
textbooks you mentioned, complete with a substantial code example for each.

The following sections will cover each topic with the rigor of the referenced texts,
focusing on the core principles, analysis, and implementation.

1. Basic and Advanced Data Structures


Theoretical Explanation

A data structure is a systematic way of organizing and storing data to enable


efficient access and modification. The choice of data structure is fundamental to
algorithm design and directly impacts performance, as emphasized in both CLRS and
Kleinberg & Tardos.

Basic Data Structures:

 Arrays: A contiguous block of memory. Access is O(1) by index, but


insertion/deletion in the middle is O(n) due to shifting.
 Linked Lists: A sequence of nodes, each containing data and a pointer to the next
node. Insertion and deletion at a known position are O(1), but random access is O(n).
 Stacks: A Last-In, First-Out (LIFO) structure. Core operations are push (O(1))
and pop (O(1)).
 Queues: A First-In, First-Out (FIFO) structure. Core operations are enqueue (O(1))
and dequeue (O(1)).
 Hash Tables: A structure that maps keys to values using a hash function. It offers
O(1) average-case time for insert, delete, and search. Collisions are resolved via
chaining or open addressing.

Advanced Data Structures:

 Trees: Hierarchical structures. A key variant is the Binary Search Tree (BST), where
for any node, all left descendants are smaller and all right descendants are larger.
This allows for search, insertion, and deletion in O(h) time, where h is the tree's
height.
 Balanced Binary Search Trees (e.g., Red-Black Trees): To guarantee that h is
O(log n), we use balanced trees. As detailed in CLRS, a Red-Black Tree is a BST with
an extra color bit per node, adhering to properties that ensure the tree remains
approximately balanced after insertions and deletions, thus maintaining O(log n)
time for all core operations.
 Heaps: A complete binary tree stored in an array that satisfies the heap property
(min-heap: parent <= children; max-heap: parent >= children). The primary
operations are insert (O(log n)) and extract-min/max (O(log n)). They are the
foundation of efficient priority queues and the Heapsort algorithm.
 Graphs: A set of vertices (nodes) and edges (connections between nodes). They can
be directed or undirected and are represented primarily by Adjacency
Matrices (O(1) edge lookup, O(V²) space) or Adjacency Lists (O(degree(V)) edge
lookup, O(V+E) space). The adjacency list is generally more space-efficient for
sparse graphs.
 Tries (Prefix Trees): A tree for storing strings. Each node represents a common
prefix. Searching for a key of length k is O(k), independent of the number of keys in
the trie. Excellent for autocomplete features.
 Disjoint-Set (Union-Find): A structure that keeps track of a partition of a set into
disjoint subsets. It supports two primary operations: find (determining which subset
an element is in) and union (merging two subsets). Using union-by-rank and path
compression, these operations run in nearly constant time, O(α(n)), where α is the
inverse Ackermann function.

Code Example: Red-Black Tree Insertion in Python

This is a simplified implementation showcasing the core logic and rotations. A full
implementation would include deletion and more thorough error handling.
python
Copy
Download
class Node:
def __init__(self, key, color='RED'):
[Link] = key
[Link] = color
[Link] = None
[Link] = None
[Link] = None

class RedBlackTree:
def __init__(self):
[Link] = Node(None, 'BLACK') Sentinel leaf nodes
[Link] = [Link]

def left_rotate(self, x):


"""Performs a left rotation around node x."""
y = [Link]
[Link] = [Link]
if [Link] != [Link]:
[Link] = x
[Link] = [Link]
if [Link] is None:
[Link] = y
elif x == [Link]:
[Link] = y
else:
[Link] = y
[Link] = x
[Link] = y

def right_rotate(self, x):


"""Performs a right rotation around node x."""
y = [Link]
[Link] = [Link]
if [Link] != [Link]:
[Link] = x
[Link] = [Link]
if [Link] is None:
[Link] = y
elif x == [Link]:
[Link] = y
else:
[Link] = y
[Link] = x
[Link] = y

def insert_fixup(self, z):


"""Fixes the Red-Black tree properties after insertion."""
while [Link] and [Link] == 'RED':
if [Link] == [Link]:
y = [Link] # uncle
if [Link] == 'RED':
# Case 1: Uncle is RED - Recolor
[Link] = 'BLACK'
[Link] = 'BLACK'
[Link] = 'RED'
z = [Link]
else:
if z == [Link]:
# Case 2: Uncle is BLACK, z is a right child - Transform to Case 3
z = [Link]
self.left_rotate(z)
# Case 3: Uncle is BLACK, z is a left child - Recolor and Rotate
[Link] = 'BLACK'
[Link] = 'RED'
self.right_rotate([Link])
else:
# Symmetric case for right parent
y = [Link]
if [Link] == 'RED':
[Link] = 'BLACK'
[Link] = 'BLACK'
[Link] = 'RED'
z = [Link]
else:
if z == [Link]:
z = [Link]
self.right_rotate(z)
[Link] = 'BLACK'
[Link] = 'RED'
self.left_rotate([Link])
if z == [Link]:
break
[Link] = 'BLACK'

def insert(self, key):


"""Inserts a key into the Red-Black Tree."""
z = Node(key)
[Link] = [Link]
[Link] = [Link]

y = None
x = [Link]

while x != [Link]:
y=x
if [Link] < [Link]:
x = [Link]
else:
x = [Link]

[Link] = y
if y is None:
[Link] = z
elif [Link] < [Link]:
[Link] = z
else:
[Link] = z

if [Link] is None:
[Link] = 'BLACK'
return

if [Link] is None:
return

self.insert_fixup(z)

def inorder_traversal(self, node, result):


"""Performs an in-order traversal of the tree."""
if node != [Link]:
self.inorder_traversal([Link], result)
[Link](([Link], [Link]))
self.inorder_traversal([Link], result)

# Example Usage
rbt = RedBlackTree()
keys = [20, 15, 25, 10, 5, 1]
for key in keys:
[Link](key)

result = []
rbt.inorder_traversal([Link], result)
print("In-order traversal (key, color):", result)
# Output will show a sorted list of keys, demonstrating the BST property is maintained.

2. Algorithm Analysis
Theoretical Explanation

Algorithm analysis provides a theoretical framework to predict the resource


consumption of an algorithm, primarily focusing on time complexity (runtime)
and space complexity (memory usage). This allows us to compare algorithms
independently of hardware and specific inputs.

Asymptotic Notation (The "Big-O" Notation): This is the lingua franca of


algorithm analysis, as introduced in Chapter 3 of CLRS. It describes the upper bound
of an algorithm's growth rate.

 O-notation (Big-O): O(g(n)) is the set of functions with an upper bound of g(n), up
to a constant factor. Formally, O(g(n)) = { f(n): there exist positive constants c and
n₀ such that 0 ≤ f(n) ≤ c*g(n) for all n ≥ n₀ }.
 Ω-notation (Big-Omega): Provides an asymptotic lower bound.
 Θ-notation (Big-Theta): Provides an asymptotic tight bound. If an algorithm is
Θ(g(n)), its growth rate is exactly proportional to g(n) for large n.
Common Time Complexities:

 O(1): Constant time. e.g., accessing an array element by index.


 O(log n): Logarithmic time. e.g., binary search in a sorted array.
 O(n): Linear time. e.g., finding the maximum element in an unsorted array.
 O(n log n): Linearithmic time. e.g., the best comparison-based sorting algorithms
(Merge Sort, Heapsort).
 O(n²): Quadratic time. e.g., simple sorting algorithms like Insertion Sort or Bubble
Sort.
 O(2ⁿ): Exponential time. e.g., the naive recursive solution for the Fibonacci
sequence.

Analysis Techniques:

 Worst-Case Analysis: The most common form, giving an upper bound on the
running time for any input of size n. e.g., Quicksort's worst-case is O(n²).
 Average-Case Analysis: The expected running time over all possible inputs of
size n. Often more difficult to compute. e.g., Quicksort's average-case is O(n log n).
 Amortized Analysis: The average performance of each operation in a worst-case
sequence of operations. It is not an average over inputs, but over operations. The
classic example is a dynamic array that doubles in size when full. A single append
might be O(n), but a sequence of n appends is O(n), leading to an amortized cost of
O(1) per append.

Code Example: Amortized Analysis of a Dynamic Array


python
Copy
Download
import sys

class DynamicArray:
def __init__(self):
self._capacity = 1
self._size = 0
self._data = [None] * self._capacity
self.total_cost = 0 # For analysis
self.operation_count = 0 # For analysis

def _resize(self, new_capacity):


"""Resizes the internal array to the new capacity. Cost: O(n)."""
print(f"Resizing from {self._capacity} to {new_capacity}")
new_data = [None] * new_capacity
for i in range(self._size):
new_data[i] = self._data[i]
self.total_cost += 1 # Cost for copying each element
self._data = new_data
self._capacity = new_capacity
self.total_cost += self._size # Aggregate cost for the resize operation

def append(self, value):


"""Appends a value to the end of the array. Amortized O(1)."""
self.operation_count += 1
self.total_cost += 1 # Cost for the basic assignment below

if self._size == self._capacity:
# If full, trigger a resize (expensive operation)
self._resize(2 * self._capacity)
self._data[self._size] = value
self._size += 1

def get_size(self):
return self._size

def get_capacity(self):
return self._capacity

# Example Usage and Analysis


da = DynamicArray()
n = 16

print("Appending", n, "elements:")
for i in range(n):
[Link](i)
print(f"Op {da.operation_count}: Size={da.get_size()},
Capacity={da.get_capacity()}")

print(f"\nTotal Operations: {da.operation_count}")


print(f"Total Cost (element assignments): {da.total_cost}")
print(f"Amortized Cost per operation: {da.total_cost / da.operation_count:.2f}")
# The amortized cost will be a small constant, demonstrating O(1) amortized time.

3. Sorting and Searching


Theoretical Explanation

This is one of the most fundamental problems in computer science. Sorting


rearranges data into a specified order, while searching finds a specific item within a
dataset.

Comparison Sorts (CLRS Chapters 6-8):


These algorithms determine the sorted order by comparing elements. The Ω(n log n)
lower bound proven by decision tree model applies to them.

 Heapsort: Builds a max-heap and repeatedly extracts the maximum element. Runs
in O(n log n) worst-case time and is in-place, but is often slower in practice than well-
implemented Quicksort.
 Quicksort: A divide-and-conquer algorithm. It picks a 'pivot', partitions the array
around it, and recursively sorts the sub-arrays. Its average-case is O(n log n), but
worst-case is O(n²). However, it has very low constants and is often the fastest in
practice. Randomized Quicksort avoids the worst-case on specific inputs.
 Merge Sort: Another divide-and-conquer algorithm. It recursively splits the array
and then merges the sorted halves. It runs in Θ(n log n) time in all cases and is
stable, but requires O(n) auxiliary space.

Non-Comparison Sorts:
These algorithms do not use comparisons and can break the Ω(n log n) lower bound
by making assumptions about the data.
 Counting Sort: Assumes input elements are integers in a specific range [0, k]. It
counts the occurrences of each element and then calculates their positions. Runs in
O(n + k) time.
 Radix Sort: Sorts integers by processing individual digits, from least significant to
most significant, using a stable sort like Counting Sort as a subroutine. Runs in O(d(n
+ k)) time, where d is the number of digits.

Searching:

 Binary Search: The classic search algorithm for sorted arrays. It repeatedly divides
the search interval in half. Runs in O(log n) time.

Code Example: Quicksort with Randomized Pivot


python
Copy
Download
import random

def quicksort(arr, low, high):


"""Sorts the subarray arr[low..high] using Quicksort."""
if low < high:
# pi is the partitioning index, arr[pi] is now at the right place
pi = randomized_partition(arr, low, high)
# Recursively sort elements before and after the partition
quicksort(arr, low, pi - 1)
quicksort(arr, pi + 1, high)

def partition(arr, low, high):


"""This function takes the last element as pivot, places it at its correct position, and
places all smaller to the left and all greater to the right."""
pivot = arr[high]
i = low - 1 # index of smaller element
for j in range(low, high):
if arr[j] <= pivot:
i=i+1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1

def randomized_partition(arr, low, high):


"""Randomly selects a pivot and swaps it with the last element."""
rand_index = [Link](low, high)
arr[high], arr[rand_index] = arr[rand_index], arr[high]
return partition(arr, low, high)

# Example Usage
arr = [10, 7, 8, 9, 1, 5, 3, 2, 4, 6]
print("Original array:", arr)
quicksort(arr, 0, len(arr) - 1)
print("Sorted array: ", arr)

4. Dynamic Programming
Theoretical Explanation

Dynamic Programming (DP), covered in CLRS Chapter 15 and Kleinberg & Tardos
Chapter 6, is a method for solving complex problems by breaking them down into
simpler overlapping subproblems. It avoids recomputing the same subproblem by
storing their results (memoization or tabulation). DP is applicable when a problem
has:

1. Overlapping Subproblems: The problem can be broken down into subproblems


which are reused several times.
2. Optimal Substructure: An optimal solution to the problem contains within it
optimal solutions to subproblems.

Two Implementation Approaches:

 Top-Down with Memoization: We write the procedure recursively in a natural


manner but modify it to save the result of each subproblem (usually in an array or
hash table). Before solving a subproblem, we check if it has already been solved.
 Bottom-Up with Tabulation: We solve all possible subproblems in a systematic
order, typically by filling up an n-dimensional table. The solution to the original
problem is found in the last cell of the table.

Classic Problems: Fibonacci Sequence, Rod Cutting, Matrix Chain Multiplication,


Longest Common Subsequence (LCS), 0/1 Knapsack, All-Pairs Shortest Paths (Floyd-
Warshall).

Code Example: Longest Common Subsequence (LCS)


python
Copy
Download
def lcs_tabulation(X, Y):
"""Finds the length of the Longest Common Subsequence using bottom-up DP."""
m = len(X)
n = len(Y)
# Create a (m+1) x (n+1) DP table, initialized to 0
dp = [[0] * (n + 1) for _ in range(m + 1)]

# Build the table dp[][] in bottom-up fashion


for i in range(1, m + 1):
for j in range(1, n + 1):
if X[i - 1] == Y[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

# The length of LCS is in dp[m][n]


lcs_length = dp[m][n]

# (Optional) To reconstruct the LCS string


lcs_sequence = []
i, j = m, n
while i > 0 and j > 0:
if X[i - 1] == Y[j - 1]:
lcs_sequence.append(X[i - 1])
i -= 1
j -= 1
elif dp[i - 1][j] > dp[i][j - 1]:
i -= 1
else:
j -= 1

return lcs_length, ''.join(reversed(lcs_sequence))

# Example Usage
X = "AGGTAB"
Y = "GXTXAYB"
length, sequence = lcs_tabulation(X, Y)
print(f"Length of LCS is {length}")
print(f"The LCS is '{sequence}'")
# Output: Length of LCS is 4
# The LCS is 'GTAB'

5. Graph Algorithms
Theoretical Explanation

Graphs model pairwise relationships between objects. Algorithms on graphs are vast
and foundational, as covered in CLRS Chapters 22-26 and Kleinberg & Tardos
Chapters 3-5.

Graph Traversal:

 Breadth-First Search (BFS): Explores a graph level by level. It uses a queue and is
ideal for finding the shortest path in an unweighted graph. Runs in O(V + E) time.
 Depth-First Search (DFS): Explores a graph by going as deep as possible along
each branch before backtracking. It uses a stack (recursion) and is useful for
topological sorting, detecting cycles, and finding connected components. Runs in O(V
+ E) time.

Shortest Paths:

 Dijkstra's Algorithm: Finds the shortest path from a source vertex to all other
vertices in a weighted graph with non-negative edge weights. It uses a greedy
approach with a priority queue (min-heap). Its time complexity is O((V+E) log V) with
a binary heap.
 Bellman-Ford Algorithm: Finds the shortest path from a source vertex in a
weighted graph even with negative edge weights. It can also detect negative-weight
cycles. Its time complexity is O(V*E).

Minimum Spanning Tree (MST):


An MST is a subset of the edges of a connected, weighted graph that connects all
vertices without any cycles and with the minimum possible total edge weight.

 Kruskal's Algorithm: A greedy algorithm that sorts all edges and adds them to the
MST if they don't form a cycle (checked using a Disjoint-Set Union-Find). Runs in O(E
log V) time.
 Prim's Algorithm: A greedy algorithm that grows the MST from a starting vertex,
always adding the cheapest edge connecting the MST to a vertex not yet in the MST.
Runs in O(E log V) time with a priority queue.
Code Example: Dijkstra's Algorithm
python
Copy
Download
import heapq

def dijkstra(graph, start):


"""
Finds the shortest path from a start node to all other nodes in a weighted graph.
Graph is represented as an adjacency list: {node: [(neighbor, weight), ...]}
"""
# Initialize distances: set all to infinity, start to 0.
distances = {node: float('infinity') for node in graph}
distances[start] = 0

# Priority queue: (distance, node)


priority_queue = [(0, start)]
[Link](priority_queue)

while priority_queue:
current_distance, current_node = [Link](priority_queue)

# If we found a better path already, skip processing.


if current_distance > distances[current_node]:
continue

for neighbor, weight in graph[current_node]:


distance = current_distance + weight
# Only consider this new path if it's better
if distance < distances[neighbor]:
distances[neighbor] = distance
[Link](priority_queue, (distance, neighbor))

return distances

# Example Usage
graph = {
'A': [('B', 4), ('C', 2)],
'B': [('C', 5), ('D', 10)],
'C': [('D', 3), ('E', 8)],
'D': [('E', 2)],
'E': []
}

start_node = 'A'
distances = dijkstra(graph, start_node)
print(f"Shortest distances from node {start_node}:")
for node, dist in [Link]():
print(f" To {node}: {dist}")

# Output:
# Shortest distances from node A:
# To A: 0
# To B: 4
# To C: 2
# To D: 5 (via A->C->D)
# To E: 7 (via A->C->D->E)
6. Greedy Algorithms
Theoretical Explanation

A greedy algorithm, covered in CLRS Chapter 16 and Kleinberg & Tardos Chapter 4,
builds up a solution piece by piece, always choosing the next piece that offers the
most obvious and immediate benefit. It makes a locally optimal choice at each stage
with the hope of finding a global optimum.

Greedy algorithms are often simpler and more efficient than DP solutions. However,
they do not always yield the globally optimal solution. To be correct, a problem must
exhibit two key properties:

1. Greedy Choice Property: A globally optimal solution can be arrived at by making a


locally optimal (greedy) choice.
2. Optimal Substructure: An optimal solution to the problem contains optimal
solutions to subproblems (similar to DP).

Classic Problems:

 Activity Selection: Select the maximum number of non-overlapping activities. The


greedy choice is to always pick the activity with the earliest finish time.
 Huffman Coding: A lossless data compression algorithm. The greedy choice is to
repeatedly merge the two least frequent nodes.
 Fractional Knapsack: Fill a knapsack with items to maximize total value, where
items can be broken into fractions. The greedy choice is to always take the item with
the highest value-to-weight ratio.

Code Example: Activity Selection Problem


python
Copy
Download
def activity_selection(activities):
"""
Selects the maximum number of non-overlapping activities.
Activities is a list of tuples (start_time, finish_time).
The greedy choice is to pick the activity with the earliest finish time.
"""
# Sort activities based on their finish time
[Link](key=lambda x: x[1])

selected = []
# The first activity always gets selected
last_finish_time = -float('infinity')

for start, finish in activities:


# If this activity starts after the last one finished, select it
if start >= last_finish_time:
[Link]((start, finish))
last_finish_time = finish

return selected
# Example Usage
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12,
16)]
print("List of Activities (start, finish):", activities)
result = activity_selection(activities)
print("Selected Non-overlapping Activities:", result)
print("Number of activities selected:", len(result))
# Output: Selected activities will be [(1,4), (5,7), (8,11), (12,16)]

7. Information Compression Algorithms


Theoretical Explanation

Compression algorithms reduce the number of bits needed to represent data. They
are either:

 Lossless: The original data can be perfectly reconstructed from the compressed
data (e.g., ZIP, PNG, GZIP). Essential for text, code, and databases.
 Lossy: Some information is discarded, and the original data cannot be perfectly
reconstructed, but the fidelity is acceptable (e.g., JPEG, MP3). Used for images,
audio, and video.

Key Concepts:

 Entropy (Shannon's Information Theory): A measure of the average amount of


information (in bits) produced by a random data source. It sets a theoretical lower
limit on lossless compression.
 Run-Length Encoding (RLE): A simple lossless method that replaces sequences of
the same data value (runs) with a single value and a count. Effective for data with
many long runs.
 Huffman Coding (A Greedy Algorithm): A lossless compression algorithm that
creates a variable-length prefix-free code for characters. More frequent characters
get shorter codes. It is optimal for a character-by-character encoding scheme.
 Lempel-Ziv-Welch (LZW): A dictionary-based lossless compression algorithm. It
builds a dictionary of strings dynamically during encoding and outputs codes for
these strings. Used in GIF and part of the UNIX compress utility.

Code Example: Huffman Coding


python
Copy
Download
import heapq
from collections import Counter, defaultdict

class HuffmanCoder:
def __init__(self):
[Link] = {}
self.reverse_mapping = {}

class HeapNode:
def __init__(self, char, freq):
[Link] = char
[Link] = freq
[Link] = None
[Link] = None
def __lt__(self, other):
return [Link] < [Link]
def __eq__(self, other):
if(other is None):
return False
return [Link] == [Link]

def make_frequency_dict(self, text):


return Counter(text)

def build_heap(self, frequency):


heap = []
for char, freq in [Link]():
node = [Link](char, freq)
[Link](heap, node)
return heap

def build_tree(self, heap):


while len(heap) > 1:
node1 = [Link](heap)
node2 = [Link](heap)
merged = [Link](None, [Link] + [Link])
[Link] = node1
[Link] = node2
[Link](heap, merged)
return heap[0] # the root node

def make_codes_helper(self, root, current_code):


if root is None:
return
if [Link] is not None:
[Link][[Link]] = current_code
self.reverse_mapping[current_code] = [Link]
return
self.make_codes_helper([Link], current_code + "0")
self.make_codes_helper([Link], current_code + "1")

def make_codes(self, root):


current_code = ""
self.make_codes_helper(root, current_code)

def get_encoded_text(self, text):


encoded_text = ""
for character in text:
encoded_text += [Link][character]
return encoded_text

def compress(self, text):


frequency = self.make_frequency_dict(text)
heap = self.build_heap(frequency)
root = self.build_tree(heap)
self.make_codes(root)
encoded_text = self.get_encoded_text(text)
return encoded_text, root

def decode_text(self, encoded_text, root):


current_node = root
decoded_text = ""
for bit in encoded_text:
if bit == '0':
current_node = current_node.left
else:
current_node = current_node.right
if current_node.char is not None:
decoded_text += current_node.char
current_node = root
return decoded_text

# Example Usage
text = "this is an example for huffman encoding"
print(f"Original text: {text}")
print(f"Original size: {len(text) * 8} bits (assuming 8-bit ASCII)")

coder = HuffmanCoder()
compressed_data, tree_root = [Link](text)

print(f"Compressed data: {compressed_data}")


print(f"Compressed size: {len(compressed_data)} bits")

decompressed_text = coder.decode_text(compressed_data, tree_root)


print(f"Decompressed text: {decompressed_text}")
print(f"Lossless compression working: {text == decompressed_text}")

# Print the Huffman codes


print("\nHuffman Codes:")
for char, code in sorted([Link]()):
print(f" '{char}' : {code}")

This concludes the detailed explanation and code examples for all seven requested
subjects, presented in the spirit of the authoritative textbooks you referenced.

You might also like