1
Current Location:
>
Algorithms
Common Algorithms and Data Structures in Python: From Beginner to Expert
Release time:2024-11-11 06:05:02 Number of reads: 12
Copyright Statement: This article is an original work of the website and follows the CC 4.0 BY-SA copyright agreement. Please include the original source link and this statement when reprinting.

Article link: https://junyayun.com/en/content/aid/1426?s=en%2Fcontent%2Faid%2F1426

Hello, dear readers! Today, let's talk about common algorithms and data structures in Python programming. This is both an important and interesting topic! Whether you're a beginner or a seasoned programmer, mastering these basics can make your code more efficient and elegant. So, let's embark on this challenging and fun learning journey together!

The Magic of Arrays

Arrays are arguably the most basic and commonly used data structure. Do you often use them to store a set of related data? For example, storing a class's students' grades or a week's temperature changes. But did you know that arrays have many clever uses?

Dynamic Arrays

In Python, we use lists to implement dynamic arrays. Their characteristic is the ability to resize dynamically as needed, making them very flexible. Let's look at an example:

my_list = []


my_list.append(1)
my_list.append(2)
my_list.append(3)

print(my_list)  # Output: [1, 2, 3]


my_list.pop()

print(my_list)  # Output: [1, 2]

See? We can add and remove elements freely without worrying about the array's size. This is especially useful when dealing with datasets of unknown size.

Multidimensional Arrays

Sometimes, we need to handle more complex data structures, like images or matrices. Here, multidimensional arrays come into play. In Python, we can use nested lists to implement multidimensional arrays:

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]


print(matrix[1][1])  # Output: 5


matrix[0][2] = 10
print(matrix)  # Output: [[1, 2, 10], [4, 5, 6], [7, 8, 9]]

Isn't it amazing? We can use this method to represent and handle complex data structures. For instance, you could use it to implement a tic-tac-toe game board!

Array Slicing

Python's list slicing feature is very powerful, allowing us to easily obtain subarrays. Check out this example:

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


print(numbers[:5])  # Output: [0, 1, 2, 3, 4]


print(numbers[-5:])  # Output: [5, 6, 7, 8, 9]


print(numbers[::2])  # Output: [0, 2, 4, 6, 8]


print(numbers[::-1])  # Output: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

These slicing operations allow us to conveniently handle parts of the array without writing complex loops. Do you find this feature useful?

Playing with Linked Lists

Linked lists are another common linear data structure. Unlike arrays, elements in a linked list are not stored continuously in memory but are connected through "links." What advantages does this structure have? Let's explore!

Singly Linked List

A singly linked list is the most basic type of linked list. Each node contains data and a pointer to the next node. Let's implement a simple singly linked list:

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

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

    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        current = self.head
        while current.next:
            current = current.next
        current.next = new_node

    def display(self):
        current = self.head
        while current:
            print(current.data, end=" -> ")
            current = current.next
        print("None")


my_list = LinkedList()
my_list.append(1)
my_list.append(2)
my_list.append(3)
my_list.display()  # Output: 1 -> 2 -> 3 -> None

Looks a bit complex, right? But the linked list's structure allows us to conveniently insert or delete elements at any position without moving other elements. This can be advantageous over arrays in certain scenarios.

Doubly Linked List

Doubly linked lists are more flexible than singly linked lists. Each node has not only a pointer to the next node but also a pointer to the previous node. This allows us to traverse the list in both directions. Let's see how to implement it:

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

class DoublyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = self.tail = new_node
        else:
            new_node.prev = self.tail
            self.tail.next = new_node
            self.tail = new_node

    def display_forward(self):
        current = self.head
        while current:
            print(current.data, end=" <-> ")
            current = current.next
        print("None")

    def display_backward(self):
        current = self.tail
        while current:
            print(current.data, end=" <-> ")
            current = current.prev
        print("None")


my_list = DoublyLinkedList()
my_list.append(1)
my_list.append(2)
my_list.append(3)
my_list.display_forward()   # Output: 1 <-> 2 <-> 3 <-> None
my_list.display_backward()  # Output: 3 <-> 2 <-> 1 <-> None

Doubly linked lists allow us to traverse data in both directions. This is useful in scenarios requiring bidirectional operations, such as a browser's forward and back functions. Can you think of other application scenarios?

Circular Linked List

A circular linked list is a special linked list where the last node points to the first node, forming a loop. This structure is very useful in certain algorithms, like the Josephus problem. Let's implement a simple circular linked list:

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

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

    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            new_node.next = self.head
        else:
            current = self.head
            while current.next != self.head:
                current = current.next
            current.next = new_node
            new_node.next = self.head

    def display(self):
        if not self.head:
            return
        current = self.head
        while True:
            print(current.data, end=" -> ")
            current = current.next
            if current == self.head:
                break
        print("(Back to head)")


my_list = CircularLinkedList()
my_list.append(1)
my_list.append(2)
my_list.append(3)
my_list.display()  # Output: 1 -> 2 -> 3 -> (Back to head)

A circular linked list has the interesting feature that you can start from any node, traverse the entire list, and eventually return to the starting point. This is useful in scenarios requiring cyclic data processing. Can you think of practical applications?

The Magic of Stacks

A stack is a data structure that follows the Last In First Out (LIFO) principle. Imagine stacking plates: you always put the new plate on top, and when you take a plate, it's also from the top. This is how a stack works! Let's see how to implement a stack in Python and some of its interesting applications.

Basic Implementation

We can use Python's list to implement a simple stack:

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

    def is_empty(self):
        return len(self.items) == 0

    def push(self, item):
        self.items.append(item)

    def pop(self):
        if not self.is_empty():
            return self.items.pop()

    def peek(self):
        if not self.is_empty():
            return self.items[-1]

    def size(self):
        return len(self.items)


stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)

print(stack.pop())  # Output: 3
print(stack.peek()) # Output: 2
print(stack.size()) # Output: 2

This implementation is simple yet powerful. You can easily add (push) and remove (pop) elements, and also view the top element (peek) without removing it.

Parenthesis Matching

A classic application of stacks is checking if parentheses are balanced. This is common in syntax checking of programming languages. Let's implement a simple parenthesis matcher:

def is_balanced(expression):
    stack = []
    opening = "({["
    closing = ")}]"
    pairs = {")": "(", "}": "{", "]": "["}

    for char in expression:
        if char in opening:
            stack.append(char)
        elif char in closing:
            if not stack or stack.pop() != pairs[char]:
                return False

    return len(stack) == 0


print(is_balanced("({[]})"))  # Output: True
print(is_balanced("([)]"))    # Output: False
print(is_balanced("(("))      # Output: False

This algorithm cleverly uses a stack to track opening brackets and checks matching when encountering closing brackets. Do you think this method is smart?

Evaluating Reverse Polish Notation

Reverse Polish Notation (RPN), also known as postfix notation, is a mathematical notation where operators follow their operands. Stacks are ideal for evaluating such expressions. Let's see how to implement it:

def evaluate_rpn(tokens):
    stack = []
    operators = {'+': lambda x, y: x + y,
                 '-': lambda x, y: x - y,
                 '*': lambda x, y: x * y,
                 '/': lambda x, y: int(x / y)}

    for token in tokens:
        if token in operators:
            b, a = stack.pop(), stack.pop()
            stack.append(operators[token](a, b))
        else:
            stack.append(int(token))

    return stack.pop()


expression = ["2", "1", "+", "3", "*"]
print(evaluate_rpn(expression))  # Output: 9 ((2 + 1) * 3)

expression = ["4", "13", "5", "/", "+"]
print(evaluate_rpn(expression))  # Output: 6 (4 + (13 / 5))

This algorithm uses a stack to store operands and performs operations on the top two elements upon encountering an operator. Can you explain why this method is effective?

The Secrets of Queues

A queue is another basic data structure that follows the First In First Out (FIFO) principle. Imagine lining up to buy tickets: the first person in line is the first to get a ticket, and the last is last. This is how a queue works! Let's explore the implementation and applications of queues.

Basic Implementation

We can use Python's list to implement a simple queue:

from collections import deque

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

    def is_empty(self):
        return len(self.items) == 0

    def enqueue(self, item):
        self.items.append(item)

    def dequeue(self):
        if not self.is_empty():
            return self.items.popleft()

    def front(self):
        if not self.is_empty():
            return self.items[0]

    def size(self):
        return len(self.items)


queue = Queue()
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)

print(queue.dequeue())  # Output: 1
print(queue.front())    # Output: 2
print(queue.size())     # Output: 2

Here, we use collections.deque to implement a queue because it has O(1) complexity for operations at both ends. What advantages do you think this has over using a regular list?

Breadth-First Search

Queues play an important role in the Breadth-First Search (BFS) algorithm for graphs. BFS is used to traverse or search tree or graph data structures. Let's implement a simple BFS:

from collections import deque

def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)

    while queue:
        vertex = queue.popleft()
        print(vertex, end=" ")

        for neighbor in graph[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)


graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

bfs(graph, 'A')  # Output: A B C D E F

This algorithm uses a queue to store nodes to be visited, ensuring the graph is traversed level by level. Can you think of some real-world applications of BFS?

Task Scheduling

Queues are also very useful in task scheduling systems. For example, we can implement a simple print task scheduler:

import time
from collections import deque

class PrinterQueue:
    def __init__(self):
        self.queue = deque()

    def add_task(self, document):
        self.queue.append(document)
        print(f"Added '{document}' to the print queue.")

    def print_next(self):
        if self.queue:
            document = self.queue.popleft()
            print(f"Printing '{document}'...")
            time.sleep(1)  # Simulate print time
            print(f"Finished printing '{document}'.")
        else:
            print("No documents in the queue.")


printer = PrinterQueue()
printer.add_task("Report.pdf")
printer.add_task("Image.jpg")
printer.add_task("Letter.doc")

for _ in range(4):
    printer.print_next()

This example simulates a print queue where tasks are handled on a first-come, first-served basis. Can you think of how to expand this system, such as adding priority features?

The World of Trees

Trees are a very important data structure with applications in many scenarios. They are a hierarchical structure, like a family tree or a company's organizational chart. Let's explore the implementation of trees and some common applications.

Binary Tree Implementation

A binary tree is the most common tree structure, with each node having at most two children. Let's implement a simple binary tree:

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)
        else:
            self._insert_recursive(self.root, value)

    def _insert_recursive(self, node, value):
        if value < node.value:
            if node.left is None:
                node.left = TreeNode(value)
            else:
                self._insert_recursive(node.left, value)
        else:
            if node.right is None:
                node.right = TreeNode(value)
            else:
                self._insert_recursive(node.right, value)

    def inorder_traversal(self):
        self._inorder_recursive(self.root)

    def _inorder_recursive(self, node):
        if node:
            self._inorder_recursive(node.left)
            print(node.value, end=" ")
            self._inorder_recursive(node.right)


tree = BinaryTree()
tree.insert(5)
tree.insert(3)
tree.insert(7)
tree.insert(1)
tree.insert(9)

print("Inorder traversal:")
tree.inorder_traversal()  # Output: 1 3 5 7 9

This implementation includes inserting nodes and inorder traversal. An interesting property of inorder traversal is that for binary search trees, it outputs node values in ascending order. Can you explain why?

Binary Search Tree

A Binary Search Tree (BST) is a special kind of binary tree that maintains an important property: for any node, all node values in its left subtree are smaller, and all node values in its right subtree are larger. This makes search operations very efficient. Let's implement a BST:

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

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

    def insert(self, value):
        self.root = self._insert_recursive(self.root, value)

    def _insert_recursive(self, node, value):
        if node is None:
            return BSTNode(value)
        if value < node.value:
            node.left = self._insert_recursive(node.left, value)
        else:
            node.right = self._insert_recursive(node.right, value)
        return node

    def search(self, value):
        return self._search_recursive(self.root, value)

    def _search_recursive(self, node, value):
        if node is None or node.value == value:
            return node
        if value < node.value:
            return self._search_recursive(node.left, value)
        return self._search_recursive(node.right, value)

    def inorder_traversal(self):
        self._inorder_recursive(self.root)

    def _inorder_recursive(self, node):
        if node:
            self._inorder_recursive(node.left)
            print(node.value, end=" ")
            self._inorder_recursive(node.right)


bst = BinarySearchTree()
bst.insert(5)
bst.insert(3)
bst.insert(7)
bst.insert(1)
bst.insert(9)

print("Inorder traversal:")
bst.inorder_traversal()  # Output: 1 3 5 7 9

print("
Searching for 7:")
result = bst.search(7)
print("Found" if result else "Not found")

The average time complexity of search operations in a BST is O(log n), which is much faster than linear search in an unsorted array. Can you think of where BSTs might be used in real applications?

Balanced Trees

Although BSTs perform well on average, in the worst case (e.g., inserting sorted data), they can degenerate into a linked list, reducing search efficiency. To solve this problem, we have

Journey Through Python Algorithms: From Bubble Sort to Dynamic Programming for More Efficient Code
Previous
2024-11-10 22:07:01
The Magic of Python Algorithms: A Wonderful Journey from Beginner to Expert
2024-11-12 04:07:02
Next
Related articles