An Introduction to the Art of Computer Programming Using Python in the Age of Generative AI

XVIII. Data Structures

Introduction to Data Structures

Data structures are fundamental computer science concepts for storing, organizing, and managing data efficiently. Choosing the right data structure for a problem is crucial, as it can significantly affect the performance and efficiency of your program. We introduced the hashed table data structure in Chapter VI. Arrays are fixed-size sequences of elements, all of which are stored contiguously in memory. In Python, lists are dynamic arrays that allow for efficient indexing and dynamic resizing, while tuples are immutable sequences that also provide efficient indexing. Linked lists, trees, stacks, and queues are additional important data structures that offer various advantages depending on the use case. Trees are hierarchical structures useful for representing hierarchical data, stacks and queues are linear structures with specific access rules (LIFO and FIFO respectively), and graphs are versatile structures for representing complex relationships.

Linked Lists

A linked list is a linear collection of nodes, where each node contains two parts: the data value and a reference to the next node. Linked lists provide dynamic sizing and efficient insertion/deletion, but require sequential traversal. They are particularly useful when you need frequent insertions and deletions from the list.


class Node:
    def __init__(self, value):
        self.value = value
        self.next = None


class LinkedList:
    def __init__(self):
        self.head = None

    @staticmethod
    def create_node(value):
        return Node(value)

    def append(self, value):
        new_node = self.create_node(value)
        if not self.head:
            self.head = new_node
        else:
            current = self.head
            while current.next:
                current = current.next
            current.next = new_node

    def prepend(self, value):
        new_node = self.create_node(value)
        new_node.next = self.head
        self.head = new_node

    def delete(self, value):
        if not self.head:
            return
        if self.head.value == value:
            self.head = self.head.next
            return

        current = self.head
        next_node = current.next
        while next_node and next_node.value != value:
            current = next_node
            next_node = current.next
        if next_node:
            current.next = next_node.next

    def __str__(self):
        values = []
        current = self.head
        while current:
            values.append(str(current.value))
            current = current.next
        return ' -> '.join(values) + " -> None"
        

linked_list = LinkedList()
linked_list.append(42)
linked_list.append("Mango")
linked_list.prepend(3.14)
print(linked_list)
linked_list.delete(3.14)
print(linked_list)
        

Arrays

An array is a collection of elements stored in contiguous memory locations. Arrays allow for efficient indexing but have a fixed size. In Python, arrays can be implemented using the `array` module, though lists are more commonly used despite not being true arrays. Lists in Python are dynamic arrays that allow for dynamic resizing and offer efficient access by index.


import array

# Create an array of integers
arr = array.array('i', [1, 2, 3, 4, 5])

# Access elements
print(arr[0])  # Output: 1

# Modify elements
arr[1] = 10

# Append elements
arr.append(6)

# Delete elements
arr.remove(3)

print(arr)
        

Stacks

A stack is a linear data structure that follows the Last In, First Out (LIFO) principle. Elements can be added to the top of the stack and removed from the top as well. Stacks are useful in scenarios such as undo mechanisms, expression evaluation, and backtracking algorithms.


class Stack:
    def __init__(self):
        self.items = []

    def is_empty(self):
        return not self.items

    def push(self, item):
        self.items.append(item)
        print(f'Pushed {item} to stack.')

    def pop(self):
        if not self.is_empty():
            item = self.items.pop()
            print(f'Popped {item} from stack.')
            return item
        print('Stack is empty.')
        return None

    def peek(self):
        if not self.is_empty():
            return self.items[-1]
        print('Stack is empty.')
        return None

    def __str__(self):
        return 'Stack: ' + ' -> '.join(map(str, self.items)) + ' -> None'

# Example usage
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack)
stack.pop()
print(stack)
        

Queues

A queue is a linear data structure that follows the First In, First Out (FIFO) principle. Elements are added to the rear of the queue and removed from the front. Queues are useful in scenarios such as task scheduling, breadth-first search algorithms, and handling asynchronous data.


from collections import deque

class Queue:
    def __init__(self):
        self.items = deque()

    def is_empty(self):
        return not self.items

    def enqueue(self, item):
        self.items.append(item)
        print(f'Enqueued {item} to queue.')

    def dequeue(self):
        if not self.is_empty():
            item = self.items.popleft()
            print(f'Dequeued {item} from queue.')
            return item
        print('Queue is empty.')
        return None

    def peek(self):
        if not self.is_empty():
            return self.items[0]
        print('Queue is empty.')
        return None

    def __str__(self):
        return 'Queue: ' + ' -> '.join(map(str, self.items)) + ' -> None'

# Example usage
queue = Queue()
queue.enqueue('A')
queue.enqueue('B')
queue.enqueue('C')
print(queue)
queue.dequeue()
print(queue)
        

Trees

A tree is a hierarchical data structure consisting of nodes, with each node containing a value and references to child nodes. Trees are widely used in various applications such as databases, file systems, and search algorithms. A binary tree is a common type of tree where each node has at most two children, referred to as the left child and the right child.


class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class BinaryTree:
    def __init__(self):
        self.root = None

    def insert(self, value):
        if not self.root:
            self.root = TreeNode(value)
            print(f'Inserted root node with value {value}.')
        else:
            self._insert_recursive(self.root, value)

    def _insert_recursive(self, current, value):
        if value < current.value:
            if current.left:
                self._insert_recursive(current.left, value)
            else:
                current.left = TreeNode(value)
                print(f'Inserted node with value {value} to the left of {current.value}.')
        else:
            if current.right:
                self._insert_recursive(current.right, value)
            else:
                current.right = TreeNode(value)
                print(f'Inserted node with value {value} to the right of {current.value}.')

    def inorder_traversal(self):
        elements = []
        self._inorder_recursive(self.root, elements)
        return elements

    def _inorder_recursive(self, current, elements):
        if current:
            self._inorder_recursive(current.left, elements)
            elements.append(current.value)
            self._inorder_recursive(current.right, elements)

    def __str__(self):
        return 'Inorder Traversal: ' + ' -> '.join(map(str, self.inorder_traversal())) + ' -> None'

# Example usage
tree = BinaryTree()
tree.insert(10)
tree.insert(5)
tree.insert(15)
tree.insert(3)
tree.insert(7)
tree.insert(12)
tree.insert(18)
print(tree)
        

Prompting AI for Implementing Data Structures

Start by defining the data structure you need and the context in which it will be used. For example, instead of asking "How do I use data structures?", ask "Can you help me implement a linked list in Python with methods for adding, deleting, and displaying nodes?" Provide detailed requirements, such as "I need a dynamic data structure that allows efficient insertion and deletion of elements." In addition, if you need to understand different types of data structures, ask for comparisons and use cases, such as "What are the differences between linked lists and arrays, and when should each be used?"

Example Prompt:
Write a functional program to manage a linked list, including functions to add, delete, and display nodes.

Resulting AI-generated code:


def create_node(value, next_node=None):
    return {'value': value, 'next': next_node}

def append(head, value):
    if head is None:
        return create_node(value)
    current = head
    while current['next'] is not None:
        current = current['next']
    current['next'] = create_node(value)
    return head

def prepend(head, value):
    new_node = create_node(value, head)
    return new_node

def delete_node(head, value):
    if head is None:
        return None
    if head['value'] == value:
        return head['next']
    current = head
    while current['next'] is not None and current['next']['value'] != value:
        current = current['next']
    if current['next'] is not None:
        current['next'] = current['next']['next']
    return head

def display(head):
    current = head
    values = []
    while current is not None:
        values.append(str(current['value']))
        current = current['next']
    return ' -> '.join(values) + ' -> None'

# Example usage
head = None
head = append(head, 42)
head = append(head, "Mango")
head = prepend(head, 3.14)
print(display(head))
head = delete_node(head, 3.14)
print(display(head))
        
Back to Home