Implementazione di un Albero Binario di Ricerca Concorrente in C++

Implementazione di un Albero Binario di Ricerca Concorrente in C++

Introduzione

Questo articolo presenta un’implementazione concorrente di un albero binario di ricerca in C++, progettata per essere concisa e facile da capire. Discuteremo la definizione dell’interfaccia, seguita dall’implementazione delle operazioni concorrenti di ricerca, inserimento, eliminazione e aggiornamento. Infine, forniremo un esempio di esecuzione di queste operazioni in modo concorrente. Gli alberi binari di ricerca (BST) sono una struttura dati ampiamente utilizzata grazie alle loro capacità efficienti di ricerca, inserimento ed eliminazione. Un BST è una struttura dati simile a un albero in cui ogni nodo ha al massimo due figli e, per tutti i nodi, il figlio sinistro è minore del nodo genitore e il figlio destro è maggiore del nodo genitore.

Implementazione di un Albero Binario di Ricerca Concorrente in C++

La complessità temporale per le operazioni di ricerca, inserimento ed eliminazione è generalmente O(h), dove h è l’altezza dell’albero. In un albero bilanciato, l’altezza è O(log n), dove n è il numero di nodi. Nella programmazione concorrente, più thread vengono eseguiti simultaneamente e l’accesso concorrente a strutture dati condivise, come un albero binario di ricerca, può portare a risultati imprevedibili se non gestiti correttamente. Un albero binario di ricerca concorrente garantisce che queste operazioni siano thread-safe, consentendo a più thread di accedere e modificare l’albero senza causare corruzione dei dati.

Definizione dell’interfaccia di un albero binario di ricerca concorrente in C++

Per implementare un albero binario di ricerca concorrente in C++, definiamo prima una classe template ConcurrentBinarySearchTree con un parametro di tipo T che supporta l’operatore minore di (<). Questo vincolo garantisce che gli elementi memorizzati nell’albero possano essere confrontati, che è un requisito fondamentale per le operazioni di un albero binario di ricerca. La classe espone quattro metodi principali: ricerca, inserimento, eliminazione e aggiornamento.

#include <memory>
#include <mutex>

using std::unique_ptr;
using std::mutex;

template <typename T>
class ConcurrentBinarySearchTree {
public:
    bool search(const T& item) const;
    bool insert(const T& item);
    bool remove(const T& item);
    bool update(const T& oldItem, const T& newItem);

private:
    struct Node {
        T value;
        unique_ptr<Node> left;
        unique_ptr<Node> right;
    };

    unique_ptr<Node> root;
    mutable mutex mtx;
};
  • L’operazione di ricerca verifica se un elemento esiste all’interno dell’albero e restituisce true se trovato, altrimenti false. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero.
  • L’operazione di inserimento aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.
  • L’operazione di eliminazione rimuove un elemento dall’albero e riassegna l’albero per mantenere le sue proprietà. La complessità temporale dell’operazione di eliminazione è O(h), dove h è l’altezza dell’albero.
  • L’operazione di aggiornamento aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca. La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero.

Le sezioni successive di questo articolo spiegheranno le implementazioni concorrenti di queste operazioni, garantendo sicurezza del thread e prestazioni efficienti pur mantenendo le proprietà dell’albero binario di ricerca.

Implementazione dell’operazione di ricerca concorrente in un albero binario di ricerca

L’operazione di ricerca concorrente consente a più thread di cercare contemporaneamente elementi all’interno dell’albero binario di ricerca senza causare corruzione dei dati. L’operazione di ricerca non modifica la struttura dell’albero, quindi la sicurezza del thread viene raggiunta senza bisogno di blocchi o altri meccanismi di sincronizzazione. Nell’operazione di ricerca, attraversiamo l’albero partendo dal nodo radice, confrontando l’elemento target con il valore di ogni nodo. Se l’elemento target è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Quando raggiungiamo un riferimento nullo, l’elemento non è nell’albero e restituiamo false. Se troviamo un nodo con lo stesso valore dell’elemento target, restituiamo true. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero. Nel caso peggiore, dobbiamo attraversare l’intera altezza dell’albero.

template <typename T>
bool ConcurrentBinarySearchTree<T>::search(const T& item) const {
    // Traverses the tree, comparing each node's value with the target item.
    // If found, it returns true. Otherwise, it returns false.
    const Node* current = root.get();

    while (current != nullptr) {
        if (item == current->value) {
            return true;
        }
        current = (item < current->value) ? current->left.get() : current->right.get();
    }

    return false;
}

Implementazione dell’operazione di inserimento concorrente in un albero binario di ricerca

L’operazione di inserimento concorrente aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un lock_guard per bloccare il mutex durante la modifica della struttura dell’albero. Ciò impedisce a più thread di inserire nodi contemporaneamente, il che potrebbe portare alla corruzione dei dati.

Nell’operazione di inserimento, attraversiamo l’albero per trovare la posizione del nuovo elemento. Confrontiamo il nuovo elemento con il valore di ogni nodo durante l’attraversamento. Se il nuovo elemento è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Se troviamo un nodo con lo stesso valore del nuovo elemento, l’inserimento è un duplicato e restituiamo false. Una volta raggiunto un riferimento nullo, abbiamo trovato la posizione per il nuovo elemento. Quindi, blocchiamo l’albero per impedire ad altri thread di modificarlo mentre inseriamo il nuovo nodo. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.

template <typename T>
bool ConcurrentBinarySearchTree<T>::insert(const T& item) {
    // Inserts a new node by finding its position in the tree.
    // Uses a lock_guard to ensure thread-safety during the process.
    std::unique_lock<std::mutex> lock(mtx);
    Node* parent = nullptr;
    Node* current = root.get();

    while (current != nullptr) {
        parent = current;
        if (item == current->value) {
            return false;
        }
        current = (item < current->value) ? current->left.get() : current->right.get();
    }

    std::unique_ptr<Node> newNode = std::make_unique<Node>();
    newNode->value = item;
    newNode->left = nullptr;
    newNode->right = nullptr;

    if (parent == nullptr) {
        root = std::move(newNode);
    } else if (item < parent->value) {
        parent->left = std::move(newNode);
    } else {
        parent->right = std::move(newNode);
    }

    return true;
}

Implementazione dell’operazione di eliminazione concorrente

L’operazione di eliminazione concorrente rimuove un elemento dall’albero e riassegna l’albero per mantenere la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un lock_guard per bloccare il mutex durante la modifica della struttura dell’albero. L’operazione di eliminazione coinvolge tre scenari principali:

  1. Il nodo da eliminare non ha figli: in questo caso, rimuoviamo semplicemente il nodo dall’albero e aggiorniamo il riferimento del genitore.
  2. Il nodo da eliminare ha un solo figlio: rimuoviamo il nodo dall’albero e aggiorniamo il riferimento del genitore per puntare al singolo figlio del nodo.
  3. Il nodo da eliminare ha due figli: troviamo il nodo con il valore minimo nel sottoalbero destro (il successore in ordine), sostituiamo il nodo da eliminare con il valore del successore in ordine e poi eliminiamo il successore in ordine.
  4. La complessità temporale dell’operazione di eliminazione è O(h), dove h è l’altezza dell’albero.
template <typename T>
bool ConcurrentBinarySearchTree<T>::deleteItem(const T& item) {
    // Deletes an item from the tree and handles three scenarios:
    // 1. The node has no children.
    // 2. The node has one child.
    // 3. The node has two children.
    // Uses a lock_guard to ensure thread-safety during the process.
    std::unique_lock<std::mutex> lock(mtx);
    Node* parent = nullptr;
    Node* current = root.get();

    while (current != nullptr && current->value != item) {
        parent = current;
        current = (item < current->value) ? current->left.get() : current->right.get();
    }

    if (current == nullptr) {
        return false;
    }

    // Case 1: Node with no children
    if (current->left == nullptr && current->right == nullptr) {
        deleteNode(parent, current);
    }
    // Case 2: Node with one child
    else if (current->left == nullptr || current->right == nullptr) {
        Node* child = (current->left != nullptr) ? current->left.get() : current->right.get();
        deleteNode(parent, current, child);
    }
    // Case 3: Node with two children
    else {
        Node* inorderSuccessorParent = current;
        Node* inorderSuccessor = current->right.get();
        while (inorderSuccessor->left != nullptr) {
            inorderSuccessorParent = inorderSuccessor;
            inorderSuccessor = inorderSuccessor->left.get();
        }
        current->value = inorderSuccessor->value;
        deleteNode(inorderSuccessorParent, inorderSuccessor);
    }

    return true;
}

Implementazione dell’operazione di aggiornamento concorrente

L’operazione di aggiornamento concorrente aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un lock_guard per bloccare il mutex durante la modifica della struttura dell’albero. Nell’operazione di aggiornamento, prima eliminiamo il vecchio elemento dall’albero e poi inseriamo il nuovo elemento. Utilizzando un lock_guard, ci assicuriamo che nessun altro thread possa modificare l’albero tra le operazioni di eliminazione e inserimento. La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero. Poiché l’operazione di aggiornamento coinvolge un’eliminazione seguita da un inserimento, la complessità rimane la stessa delle singole operazioni di eliminazione e inserimento.

template <typename T>
bool ConcurrentBinarySearchTree<T>::update(const T& oldItem, const T& newItem) {
    // Updates a node by deleting the old item and inserting the new one.
    // Uses a lock_guard to ensure thread-safety.
    std::unique_lock<std::mutex> lock(mtx);
    if (deleteItem(oldItem)) {
        return insert(newItem);
    }

    return false;
}

Conclusione

In conclusione, questo articolo ha presentato un’implementazione concorrente di un albero binario di ricerca in C++, progettata per essere chiara e facile da capire. Abbiamo discusso la definizione della classe e fornito un’implementazione thread-safe delle operazioni concorrenti di ricerca, inserimento, eliminazione e aggiornamento, mantenendo le proprietà dell’albero binario di ricerca. Utilizzando una combinazione di mutex, lock_guard e un’attenta considerazione delle aree che richiedono sincronizzazione, abbiamo ottenuto una manipolazione efficiente e sicura della struttura dati in un ambiente multithread. L’albero binario di ricerca concorrente offre complessità temporali di O(h) per ciascuna operazione, dove h è l’altezza dell’albero, rendendolo una struttura dati preziosa per l’utilizzo in scenari di programmazione concorrente.

Risorse Correlate

Implementazione di un Albero Binario di Ricerca Concorrente in Python

Implementazione di un Albero Binario di Ricerca Concorrente in Python

Introduzione

Questo articolo presenta un’implementazione concorrente di un albero binario di ricerca in Python, progettata per essere concisa e facile da capire. Discuteremo la definizione della classe, seguita dall’implementazione delle operazioni concorrenti di ricerca, inserimento, eliminazione e aggiornamento. Infine, forniremo un esempio di esecuzione di queste operazioni in modo concorrente. Gli alberi binari di ricerca (BST) sono una struttura dati ampiamente utilizzata grazie alle loro efficienti capacità di ricerca, inserimento ed eliminazione. Un BST è una struttura dati simile ad un albero in cui ogni nodo ha al massimo due figli, e per tutti i nodi, il figlio sinistro è minore del nodo padre, e il figlio destro è maggiore del nodo padre.

Implementazione di un Albero Binario di Ricerca Concorrente in Python

La complessità temporale per le operazioni di ricerca, inserimento ed eliminazione è generalmente O(h), dove h è l’altezza dell’albero. In un albero bilanciato, l’altezza è O(log n), dove n è il numero di nodi. Nella programmazione concorrente, più thread vengono eseguiti simultaneamente e l’accesso concorrente a strutture dati condivise, come un albero binario di ricerca, può portare a risultati imprevedibili se non gestito correttamente. Un albero binario di ricerca concorrente garantisce che queste operazioni siano sicure per i thread, consentendo a più thread di accedere e modificare l’albero senza causare corruzione dei dati.

Definizione della classe Concurrent Binary Search Tree in Python

Per implementare un albero binario di ricerca concorrente in Python, definiamo prima una classe ConcurrentBinarySearchTree con un parametro di tipo generico T. Questa classe richiede che gli elementi memorizzati nell’albero siano confrontabili, il che è un requisito fondamentale per le operazioni di un albero binario di ricerca. La classe espone quattro metodi principali: ricerca, inserimento, eliminazione e aggiornamento.

  • L’operazione di ricerca verifica se un elemento è presente nell’albero e restituisce True se trovato, altrimenti False. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero.
  • L’operazione di inserimento aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.
  • L’operazione di eliminazione rimuove un elemento dall’albero e riallinea l’albero per mantenere le sue proprietà. La complessità temporale dell’operazione di eliminazione è O(h), dove h è l’altezza dell’albero.
  • L’operazione di aggiornamento aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca. La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero.

Le sezioni successive di questo articolo spiegheranno le implementazioni concorrenti di queste operazioni, garantendo la sicurezza dei thread e prestazioni efficienti mantenendo le proprietà dell’albero binario di ricerca.

from threading import Lock

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

class ConcurrentBinarySearchTree:
    def __init__(self):
        self.root = None
        self.lock = Lock()
        # Scroll down for methods implementation
        # ...

Implementazione dell’operazione di ricerca concorrente nell’albero binario di ricerca

L’operazione di ricerca concorrente consente a più thread di cercare elementi all’interno dell’albero binario di ricerca simultaneamente senza causare corruzione dei dati. L’operazione di ricerca non modifica la struttura dell’albero, quindi la sicurezza del thread è garantita senza la necessità di blocchi o altri meccanismi di sincronizzazione. Nell’operazione di ricerca, attraversiamo l’albero partendo dal nodo radice, confrontando l’elemento target con il valore di ciascun nodo. Se l’elemento target è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Quando raggiungiamo un riferimento nullo, l’elemento non è nell’albero e restituiamo False. Se troviamo un nodo con lo stesso valore dell’elemento target, restituiamo True. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero. Nel caso peggiore, dobbiamo attraversare l’intera altezza dell’albero.

def search(self, item):
    # Traverses the tree, comparing each node's value with the target item.
    # If found, it returns True. Otherwise, it returns False.
    current = self.root

    while current is not None:
        comparison = self._compare(item, current.value)

        if comparison == 0:
            return True
        current = current.left if comparison < 0 else current.right

    return False

Implementazione dell’operazione di inserimento concorrente nell’albero binario di ricerca

L’operazione di inserimento concorrente aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco quando modifichiamo la struttura dell’albero. Ciò impedisce a più thread di inserire nodi contemporaneamente, il che potrebbe portare alla corruzione dei dati. Nell’operazione di inserimento, attraversiamo l’albero per trovare la posizione del nuovo elemento. Confrontiamo il nuovo elemento con il valore di ciascun nodo durante l’attraversamento. Se il nuovo elemento è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Se troviamo un nodo con lo stesso valore del nuovo elemento, l’inserimento è un duplicato e restituiamo False. Una volta raggiunto un riferimento nullo, abbiamo trovato la posizione per il nuovo elemento. Quindi, blocciamo l’albero per impedire ad altri thread di modificarlo mentre inseriamo il nuovo nodo. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.

def insert(self, item):
    # Inserts a new node by finding its position in the tree.
    # Uses a lock to ensure thread-safety during the process.
    new_node = Node(item)
    parent = None
    current = self.root

    while current is not None:
        parent = current
        comparison = self._compare(item, current.value)

        if comparison == 0:
            return False
        current = current.left if comparison < 0 else current.right

    with self.lock:
        if parent is None:
            self.root = new_node
        elif self._compare(item, parent.value) < 0:
            parent.left = new_node
        else:
            parent.right = new_node

    return True

Implementazione dell’operazione di eliminazione concorrente nell’albero binario di ricerca

L’operazione di eliminazione concorrente rimuove un elemento dall’albero e riorganizza l’albero per mantenere la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco quando modifichiamo la struttura dell’albero. L’operazione di eliminazione prevede tre scenari principali:

  1. Il nodo da eliminare non ha figli: in questo caso, rimuoviamo semplicemente il nodo dall’albero e aggiorniamo il riferimento del nodo padre.
  2. Il nodo da eliminare ha un solo figlio: rimuoviamo il nodo dall’albero e aggiorniamo il riferimento del nodo padre per puntare al singolo figlio del
  3. nodo.
  4. Il nodo da eliminare ha due figli: troviamo il nodo con il valore minimo nel sottoalbero destro (il successore in ordine), sostituiamo il nodo da eliminare con il valore del successore in ordine e poi eliminiamo il successore in ordine.

La complessità temporale dell’operazione di eliminazione è O(h), dove h è l’altezza dell’albero.

def delete(self, item):
    # Deletes a node with the given item from the tree.
    # Uses a lock to ensure thread-safety during the process.
    parent, node = self._find_node_and_parent(item)
    if node is None:
        return False

    with self.lock:
        self._delete_node(node, parent)

    return True

def _delete_node(self, node, parent):
    if node.left is None and node.right is None:
        self._replace_node_in_parent(parent, node, None)
    elif node.left is not None and node.right is not None:
min_right = self._find_min_node(node.right)
node.value = min_right.value
self._delete_node(min_right, node)
else:
child = node.left if node.left is not None else node.right
self._replace_node_in_parent(parent, node, child)

def _replace_node_in_parent(self, parent, node, new_node):
if parent is None:
self.root = new_node
elif parent.left == node:
parent.left = new_node
else:
parent.right = new_node

def _find_min_node(self, node):
while node.left is not None:
node = node.left
return node

Implementazione dell’operazione di aggiornamento concorrente

L’operazione di aggiornamento concorrente aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco quando modifichiamo la struttura dell’albero. Nell’operazione di aggiornamento, prima eliminiamo il vecchio elemento dall’albero e poi inseriamo il nuovo elemento. Utilizzando un blocco, ci assicuriamo che nessun altro thread possa modificare l’albero tra le operazioni di eliminazione e inserimento. La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero. Poiché l’operazione di aggiornamento coinvolge un’eliminazione seguita da un inserimento, la complessità rimane la stessa delle singole operazioni di eliminazione e inserimento.

def update(self, old_item, new_item):
    # Updates a node by deleting the old item and inserting the new one.
    # Uses a lock to ensure thread-safety.
    with self.lock:
        if self.delete(old_item):
            return self.insert(new_item)

    return False

Testing the Concurrent Binary Search Tree

import threading

def main():
    tree = ConcurrentBST()
    element_count = 50000

    # Concurrent insertions
    t1 = threading.Thread(target=insert_elements, args=(tree, 1, element_count // 2))
    t2 = threading.Thread(target=insert_elements, args=(tree, element_count // 2 + 1, element_count))

    # Concurrent deletions
    t3 = threading.Thread(target=delete_elements, args=(tree, 1, element_count // 4))
    t4 = threading.Thread(target=delete_elements, args=(tree, element_count // 4 + 1, element_count // 2))

    # Concurrent updates
    t5 = threading.Thread(target=update_elements, args=(tree, 1, element_count // 4, element_count))
    t6 = threading.Thread(target=update_elements, args=(tree, element_count // 4 + 1, element_count // 2, element_count))

    t1.start()
    t2.start()
    t3.start()
    t4.start()
    t5.start()
    t6.start()

    t1.join()
    t2.join()
    t3.join()
    t4.join()
    t5.join()
    t6.join()

    print("Processing completed.")

def insert_elements(tree, start, end):
    for i in range(start, end + 1):
        tree.insert(i)

def delete_elements(tree, start, end):
    for i in range(start, end + 1):
        tree.delete(i)

def update_elements(tree, start, end, offset):
    for i in range(start, end + 1):
        tree.update(i, i + offset)

if __name__ == "__main__":
    main()

Conclusione

Questo articolo ha presentato un’implementazione concorrente di un albero binario di ricerca in Python, progettata per essere chiara e facile da capire. Abbiamo discusso la definizione della classe e fornito un’implementazione sicura per i thread delle operazioni concorrenti di ricerca, inserimento, eliminazione e aggiornamento mantenendo le proprietà dell’albero binario di ricerca. Utilizzando una combinazione di blocchi e un’attenta considerazione delle aree che richiedono sincronizzazione, abbiamo ottenuto una manipolazione efficiente e sicura della struttura dati in un ambiente multithread. L’albero binario di ricerca concorrente offre complessità temporali di O(h) per ciascuna operazione, dove h è l’altezza dell’albero, rendendolo una struttura dati preziosa per l’uso in scenari di programmazione concorrente. Con l’implementazione fornita, gli sviluppatori possono sfruttare il potere della concorrenza per ottimizzare le prestazioni delle loro applicazioni, soprattutto quando si lavora con grandi set di dati e più thread che accedono alla stessa struttura dati. L’albero binario di ricerca concorrente è un’ottima scelta per gli scenari in cui i dati devono essere acceduti, modificati o eliminati da più thread senza causare corruzione dei dati.

Risorse Correlate

Implementazione di albero binario di ricerca concorrente in Java

Implementazione di Albero Binario di Ricerca Concorrente in Java

Introduzione

Questo articolo presenta un’implementazione di un albero binario di ricerca concorrente in Java, progettata per essere concisa e facile da comprendere. Discuteremo la definizione dell’interfaccia, seguita dall’implementazione delle operazioni concorrenti di ricerca, inserimento, eliminazione e aggiornamento. Infine, forniremo un esempio di esecuzione di queste operazioni in modo concorrente. Gli alberi binari di ricerca (BST) sono una struttura dati ampiamente utilizzata grazie alle loro capacità efficienti di ricerca, inserimento ed eliminazione. Un BST è una struttura dati ad albero in cui ogni nodo ha al massimo due figli e, per tutti i nodi, il figlio sinistro è minore del nodo padre e il figlio destro è maggiore del nodo padre.

La complessità temporale per le operazioni di ricerca, inserimento ed eliminazione è generalmente O(h), dove h è l’altezza dell’albero. In un albero bilanciato, l’altezza è O(log n), dove n è il numero di nodi. Nella programmazione concorrente, più thread vengono eseguiti simultaneamente e l’accesso concorrente a strutture dati condivise, come un albero binario di ricerca, può portare a risultati imprevedibili se non gestito correttamente. Un albero binario di ricerca concorrente garantisce che queste operazioni siano protette dai thread, consentendo a più thread di accedere e modificare l’albero senza causare corruzione dei dati o condizioni di gara.

Implementazione di albero binario di ricerca concorrente in Java

Definizione dell’interfaccia Java Concurrent Binary Search Tree

Per implementare un albero binario di ricerca concorrente in Java, definiamo prima un’interfaccia ConcurrentBinarySearchTree con un parametro di tipo generico T che estende l’interfaccia Comparable per il tipo T. Questo vincolo garantisce che gli elementi memorizzati nell’albero possano essere confrontati, il che è un requisito fondamentale per le operazioni di un albero binario di ricerca. L’interfaccia espone quattro metodi principali: search, insert, delete e update.

public interface ConcurrentBinarySearchTree<T extends Comparable<T>> {
    boolean search(T item);
    boolean insert(T item);
    boolean delete(T item);
    boolean update(T oldItem, T newItem);
}

public class ConcurrentBST<T extends Comparable<T>> implements ConcurrentBinarySearchTree<T> {
    private Node<T> root;
    private final Object lock = new Object();
    // Scroll down for methods implementation
    // ...
}
  • L’operazione di ricerca verifica se un elemento esiste all’interno dell’albero e restituisce true se trovato, altrimenti false. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero.
  • L’operazione di inserimento aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.
  • L’operazione di eliminazione rimuove un elemento dall’albero e riorganizza l’albero per mantenerne le proprietà. La complessità temporale dell’operazione di eliminazione è O(h), dove h è l’altezza dell’albero.
  • L’operazione di aggiornamento aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca.

La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero. Le sezioni successive di questo articolo spiegheranno le implementazioni concorrenti di queste operazioni, garantendo la sicurezza dei thread e un’efficace performance mantenendo le proprietà dell’albero binario di ricerca.

Implementazione dell’operazione di ricerca concorrente nell’albero binario di ricerca

L’operazione di ricerca concorrente consente a più thread di cercare elementi all’interno dell’albero binario di ricerca contemporaneamente senza causare corruzione dei dati. L’operazione di ricerca non modifica la struttura dell’albero, quindi la sicurezza del thread è garantita senza la necessità di blocchi o altri meccanismi di sincronizzazione. Nell’operazione di ricerca, attraversiamo l’albero a partire dal nodo radice, confrontando l’elemento target con il valore di ciascun nodo. Se l’elemento target è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Quando raggiungiamo un riferimento nullo, l’elemento non è nell’albero e restituiamo false. Se troviamo un nodo con lo stesso valore dell’elemento target, restituiamo true. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero. Nel caso peggiore, dobbiamo attraversare l’intera altezza dell’albero.

public boolean search(T item) {
    // Traverses the tree, comparing each node's value with the target item.
    // If found, it returns true. Otherwise, it returns false.
    Node<T> current = root;

    while (current != null) {
        int comparison = item.compareTo(current.value);

        if (comparison == 0)
            return true;
        current = comparison < 0 ? current.left : current.right;
    }

    return false;
}

Implementazione dell’operazione di inserimento concorrente nell’albero binario di ricerca

L’operazione di inserimento concorrente aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco sincronizzato durante la modifica della struttura dell’albero. Ciò impedisce a più thread di inserire nodi contemporaneamente, il che potrebbe portare alla corruzione dei dati. Nell’operazione di inserimento, attraversiamo l’albero per trovare la posizione del nuovo elemento. Confrontiamo il nuovo elemento con il valore di ciascun nodo durante l’attraversamento. Se il nuovo elemento è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Se troviamo un nodo con lo stesso valore del nuovo elemento, l’inserimento è un duplicato e restituiamo false. Una volta raggiunto un riferimento nullo, abbiamo trovato la posizione del nuovo elemento. Quindi blocchiamo l’albero per impedire ad altri thread di modificarlo mentre inseriamo il nuovo nodo. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.

public boolean insert(T item) {
    // Inserts a new node by finding its position in the tree.
    // Uses a synchronized block to ensure thread-safety during the process.
    Node<T> newNode = new Node<>(item);
    Node<T> parent = null;
    Node<T> current = root;

    while (current != null) {
        parent = current;
        int comparison = item.compareTo(current.value);

        if (comparison == 0)
            return false;
        current = comparison < 0 ? current.left : current.right;
    }

    synchronized (lock) {
        if (parent == null)
            root = newNode;
        else if (item.compareTo(parent.value) < 0)
            parent.left = newNode;
        else
            parent.right = newNode;
    }

    return true;
}

Implementazione dell’operazione di eliminazione concorrente

L’operazione di eliminazione concorrente rimuove un elemento dall’albero e riorganizza l’albero per mantenere la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco sincronizzato durante la modifica della struttura dell’albero. L’operazione di eliminazione coinvolge tre scenari principali:

  1. Il nodo da eliminare non ha figli: in questo caso, rimuoviamo semplicemente il nodo dall’albero e aggiorniamo il riferimento del nodo padre.
  2. Il nodo da eliminare ha un figlio: rimuoviamo il nodo dall’albero e aggiorniamo il riferimento del nodo padre per puntare al singolo figlio del nodo.
  3. Il nodo da eliminare ha due figli: troviamo il nodo con il valore minimo nel sottoalbero destro (il successore inorder), sostituiamo il nodo da eliminare con il valore del successore inorder e quindi eliminiamo il successore inorder.
private void deleteNode(Node<T> node, Node<T> parent) {
    // Deletes a node from the tree and handles three scenarios:
    // 1. The node has no children.
    // 2. The node has one child.
    // 3. The node has two children.
    if (node.left == null && node.right == null) {
        replaceNodeInParent(parent, node, null);
} else if (node.left != null && node.right != null) {
Node<T> minRight = findMinNode(node.right);
node.value = minRight.value;
deleteNode(minRight, node);
} else {
Node<T> child = node.left != null ? node.left : node.right;
replaceNodeInParent(parent, node, child);
}
}

private void replaceNodeInParent(Node<T> parent, Node<T> node, Node<T> newNode) {
if (parent == null) {
root = newNode;
} else if (parent.left == node) {
parent.left = newNode;
} else {
parent.right = newNode;
}
}

private Node<T> findMinNode(Node<T> node) {
while (node.left != null) {
node = node.left;
}

return node;
}

Implementazione dell’operazione di aggiornamento concorrente

L’operazione di aggiornamento concorrente aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco sincronizzato durante la modifica della struttura dell’albero. Nell’operazione di aggiornamento, prima eliminiamo il vecchio elemento dall’albero, poi inseriamo il nuovo elemento. Utilizzando un blocco sincronizzato, ci assicuriamo che nessun altro thread possa modificare l’albero tra le operazioni di eliminazione e inserimento. La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero. Poiché l’operazione di aggiornamento coinvolge un’eliminazione seguita da un inserimento, la complessità rimane la stessa delle operazioni di eliminazione e inserimento individuali.

public boolean update(T oldItem, T newItem) {
    // Updates a node by deleting the old item and inserting the new one.
    // Uses a synchronized block to ensure thread-safety.
    synchronized (lock) {
        if (delete(oldItem))
            return insert(newItem);
    }

    return false;
}

Testare l’albero binario di ricerca concorrente

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ConcurrentBinarySearchTree<Integer> tree = new ConcurrentBinarySearchTree<>();
        int elementCount = 50000;

        // Concurrent insertions
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(() -> insertElements(tree, 1, elementCount / 2));
        executor.submit(() -> insertElements(tree, elementCount / 2 + 1, elementCount));

        // Concurrent deletions
        executor.submit(() -> deleteElements(tree, 1, elementCount / 4));
        executor.submit(() -> deleteElements(tree, elementCount / 4 + 1, elementCount / 2));

        // Concurrent updates
        executor.submit(() -> updateElements(tree, 1, elementCount / 4, elementCount));
        executor.submit(() -> updateElements(tree, elementCount / 4 + 1, elementCount / 2, elementCount));

        executor.shutdown();

        System.out.println("Processing completed.");
    }

    static void insertElements(ConcurrentBinarySearchTree<Integer> tree, int start, int end) {
        for (int i = start; i <= end; i++) {
            tree.insert(i);
        }
    }

    static void deleteElements(ConcurrentBinarySearchTree<Integer> tree, int start, int end) {
        for (int i = start; i <= end; i++) {
            tree.delete(i);
        }
    }

    static void updateElements(ConcurrentBinarySearchTree<Integer> tree, int start, int end, int offset) {
        for (int i = start; i <= end; i++) {
            tree.update(i, i + offset);
        }
    }
}

Conclusione

In conclusione, questo articolo ha presentato un’implementazione di un albero binario di ricerca concorrente in Java, progettata per essere chiara e facile da comprendere. Abbiamo discusso la definizione dell’interfaccia e fornito un’implementazione protetta dai thread delle operazioni concorrenti di ricerca, inserimento, eliminazione e aggiornamento mantenendo le proprietà dell’albero binario di ricerca. Utilizzando una combinazione di blocchi sincronizzati e una attenta considerazione delle aree che richiedono sincronizzazione, abbiamo ottenuto una manipolazione efficiente e sicura della struttura dati in un ambiente multi-thread. L’albero binario di ricerca concorrente offre complessità temporali di O(h) per ciascuna operazione, dove h è l’altezza dell’albero, rendendolo una struttura dati preziosa per l’uso in scenari di programmazione concorrente.

Risorse Correlate

Implementazione di un Albero Binario di Ricerca Concorrente in C#

Implementazione di un Albero Binario di Ricerca Concorrente in C#

Introduzione

Questo articolo presenta un’implementazione di un albero binario di ricerca concorrente in C#, progettato per essere conciso e facile da capire. Discuteremo la definizione dell’interfaccia, seguita dall’implementazione delle operazioni concorrenti di ricerca, inserimento, cancellazione e aggiornamento. Infine, forniremo un esempio di esecuzione di queste operazioni in modo concorrente. Gli alberi binari di ricerca (BST) sono una struttura dati ampiamente utilizzata grazie alle loro capacità efficienti di ricerca, inserimento e cancellazione. Un BST è una struttura dati simile a un albero in cui ogni nodo ha al massimo due figli e, per tutti i nodi, il figlio sinistro è minore del nodo padre, mentre il figlio destro è maggiore del nodo padre.

La complessità temporale per le operazioni di ricerca, inserimento e cancellazione è generalmente O(h), dove h è l’altezza dell’albero. In un albero bilanciato, l’altezza è O(log n), dove n è il numero di nodi. Nella programmazione concorrente, più thread vengono eseguiti simultaneamente e l’accesso concorrente a strutture dati condivise, come un albero binario di ricerca, può portare a risultati imprevedibili se non gestiti correttamente. Un albero binario di ricerca concorrente garantisce che queste operazioni siano thread-safe, consentendo a più thread di accedere e modificare l’albero senza causare corruzione dei dati o condizioni di gara.

Implementazione di un Albero Binario di Ricerca Concorrente in C#

Definizione dell’interfaccia dell’albero binario di ricerca concorrente in C#

Per implementare un albero binario di ricerca concorrente in C#, definiamo prima un’interfaccia IConcurrentBinarySearchTree<T> che estende l’interfaccia IComparable per il tipo generico T. Questo vincolo garantisce che gli elementi memorizzati nell’albero possano essere confrontati, il che è un requisito fondamentale per le operazioni di un albero binario di ricerca. L’interfaccia espone quattro metodi principali: Search (Ricerca), Insert (Inserimento), Delete (Cancellazione) e Update (Aggiornamento).

public interface IConcurrentBinarySearchTree<T> where T : IComparable<T>
{
    bool Search(T item);
    bool Insert(T item);
    bool Delete(T item);
    bool Update(T oldItem, T newItem);
}

/// <summary>
/// Concurrent Binary Search Tree implementation.
/// </summary>
public class ConcurrentBinarySearchTree<T> : IConcurrentBinarySearchTree<T> where T : IComparable<T>
{
    private Node<T> _root;
    private readonly object _lock = new object();
// Scroll down for methods implementation
// ...
}


  • L’operazione Search verifica se un elemento è presente nell’albero e restituisce true se trovato, altrimenti false. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero.
  • L’operazione Insert aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.
  • L’operazione Delete rimuove un elemento dall’albero e riorganizza l’albero per mantenere le sue proprietà. La complessità temporale dell’operazione di cancellazione è O(h), dove h è l’altezza dell’albero.
  • L’operazione Update aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca. La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero.

Le sezioni successive di questo articolo spiegheranno le implementazioni concorrenti di queste operazioni, garantendo la sicurezza dei thread e un’elevata efficienza mantenendo le proprietà dell’albero binario di ricerca.

Implementazione dell’operazione di ricerca concorrente nell’albero binario di ricerca

L’operazione di ricerca concorrente consente a più thread di cercare elementi all’interno dell’albero binario di ricerca simultaneamente senza causare corruzione dei dati. L’operazione di ricerca non modifica la struttura dell’albero, quindi la sicurezza del thread viene raggiunta senza la necessità di blocchi o altri meccanismi di sincronizzazione. Nell’operazione di ricerca, attraversiamo l’albero, partendo dal nodo radice, confrontando l’elemento target con il valore di ogni nodo. Se l’elemento target è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Quando raggiungiamo un riferimento nullo, l’elemento non è nell’albero e restituiamo false. Se troviamo un nodo con lo stesso valore dell’elemento target, restituiamo true. La complessità temporale dell’operazione di ricerca è O(h), dove h è l’altezza dell’albero. Nel caso peggiore, dobbiamo attraversare l’intera altezza dell’albero.

public bool Search(T item)
    {
        // Traverses the tree, comparing each node's value with the target item.
        // If found, it returns true. Otherwise, it returns false.
        Node<T> current = _root;

        while (current != null)
        {
            int comparison = item.CompareTo(current.Value);

            if (comparison == 0)
                return true;
            current = comparison < 0 ? current.Left : current.Right;
        }

        return false;
    }

Implementazione dell’operazione di inserimento concorrente nell’albero binario di ricerca

L’operazione di inserimento concorrente aggiunge un nuovo elemento all’albero mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco quando modifichiamo la struttura dell’albero. Ciò impedisce a più thread di inserire nodi simultaneamente, il che potrebbe portare alla corruzione dei dati. Nell’operazione di inserimento, attraversiamo l’albero per trovare la posizione per il nuovo elemento. Confrontiamo il nuovo elemento con il valore di ogni nodo durante l’attraversamento. Se il nuovo elemento è minore del valore del nodo corrente, ci spostiamo sul figlio sinistro; altrimenti, ci spostiamo sul figlio destro. Se troviamo un nodo con lo stesso valore del nuovo elemento, l’inserimento è un duplicato e restituiamo false. Una volta raggiunto un riferimento nullo, abbiamo trovato la posizione per il nuovo elemento. Quindi blocchiamo l’albero per evitare che altri thread lo modifichino mentre inseriamo il nuovo nodo. La complessità temporale dell’operazione di inserimento è O(h), dove h è l’altezza dell’albero.

public bool Insert(T item)
    {
        // Inserts a new node by finding its position in the tree.
        // Uses a lock to ensure thread-safety during the process.
        Node<T> newNode = new Node<T>(item);
        Node<T> parent = null;
        Node<T> current = _root;

        while (current != null)
        {
            parent = current;
            int comparison = item.CompareTo(current.Value);

            if (comparison == 0)
                return false;
            current = comparison < 0 ? current.Left : current.Right;
        }

        lock (_lock)
        {
            if (parent == null)
                _root = newNode;
            else if (item.CompareTo(parent.Value) < 0)
                parent.Left = newNode;
            else
                parent.Right = newNode;
        }

        return true;
    }

Implementazione dell’operazione di cancellazione concorrente

L’operazione di cancellazione concorrente rimuove un elemento dall’albero e riorganizza l’albero per mantenere la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco quando modifichiamo la struttura dell’albero. L’operazione di cancellazione prevede tre scenari principali:

  • Il nodo da eliminare non ha figli: in questo caso, rimuoviamo semplicemente il nodo dall’albero e aggiorniamo il riferimento del nodo padre.
  • Il nodo da eliminare ha un solo figlio: rimuoviamo il nodo dall’albero e aggiorniamo il riferimento del nodo padre per puntare al singolo figlio del nodo.
  • Il nodo da eliminare ha due figli: troviamo il nodo con il valore minimo nel sottoalbero destro (il successore in ordine), sostituiamo il nodo da eliminare con il valore del successore in ordine e poi eliminiamo il successore in ordine.

La complessità temporale dell’operazione di cancellazione è O(h), dove h è l’altezza dell’albero.

private void DeleteNode(Node<T> node, Node<T> parent)
{
    // Deletes a node from the tree and handles three scenarios:
    // 1. The node has no children.
    // 2. The node has one child.
    // 3. The node has two children.
    if (node.Left == null && node.Right == null)
    {
        ReplaceNodeInParent(parent, node, null);
    }
    else if (node.Left != null && node.Right != null)
    {
        Node<T> minRight = FindMinNode(node.Right);
        node.Value = minRight.Value;
        DeleteNode(minRight, node);
    }
    else
    {
        Node<T> child = node.Left ?? node.Right;
        ReplaceNodeInParent(parent, node, child);
    }
}

private void ReplaceNodeInParent(Node<T> parent, Node<T> node, Node<T> newNode)
{
    if (parent == null)
    {
        _root = newNode;
    }
    else if (parent.Left == node)
    {
        parent.Left = newNode;
    }
    else
    {
        parent.Right = newNode;
    }
}

private Node<T> FindMinNode(Node<T> node)
{
    while (node.Left != null)
    {
        node = node.Left;
    }

    return node;
}

Implementazione dell’operazione di aggiornamento concorrente

L’operazione di aggiornamento concorrente aggiorna un elemento esistente nell’albero con un nuovo elemento mantenendo la proprietà dell’albero binario di ricerca. Per garantire la sicurezza del thread, utilizziamo un blocco quando modifichiamo la struttura dell’albero. Nell’operazione di aggiornamento, prima eliminiamo il vecchio elemento dall’albero e poi inseriamo il nuovo elemento. Utilizzando un blocco, garantiamo che nessun altro thread possa modificare l’albero tra le operazioni di eliminazione e inserimento. La complessità temporale dell’operazione di aggiornamento è O(h), dove h è l’altezza dell’albero. Poiché l’operazione di aggiornamento coinvolge una cancellazione seguita da un inserimento, la complessità rimane la stessa delle singole operazioni di cancellazione e inserimento.

public bool Update(T oldItem, T newItem)
{
    // Updates a node by deleting the old item and inserting the new one.
    // Uses locks to ensure thread-safety.
    lock (_lock)
    {
        if (Delete(oldItem))
            return Insert(newItem);
    }

    return false;
}
}

Testare l’albero binario di ricerca concorrente

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        IConcurrentBinarySearchTree<int> tree = new ConcurrentBinarySearchTree<int>();
        int elementCount = 50000;

        // Concurrent insertions
        var insertTask1 = Task.Run(() => InsertElements(tree, 1, elementCount / 2));
        var insertTask2 = Task.Run(() => InsertElements(tree, elementCount / 2 + 1, elementCount));

        Task.WaitAll(insertTask1, insertTask2);

        // Concurrent deletions
        var deleteTask1 = Task.Run(() => DeleteElements(tree, 1, elementCount / 4));
        var deleteTask2 = Task.Run(() => DeleteElements(tree, elementCount / 4 + 1, elementCount / 2));

        Task.WaitAll(deleteTask1, deleteTask2);

        // Concurrent updates
        var updateTask1 = Task.Run(() => UpdateElements(tree, 1, elementCount / 4, elementCount));
        var updateTask2 = Task.Run(() => UpdateElements(tree, elementCount / 4 + 1, elementCount / 2, elementCount));

        Task.WaitAll(updateTask1, updateTask2);

        Console.WriteLine("Processing completed.");
    }

    static void InsertElements(IConcurrentBinarySearchTree<int> tree, int start, int end)
    {
        for (int i = start; i <= end; i++)
        {
            tree.Insert(i);
        }
    }

    static void DeleteElements(IConcurrentBinarySearchTree<int> tree, int start, int end)
    {
        for (int i = start; i <= end; i++)
        {
            tree.Delete(i);
        }
    }

    static void UpdateElements(IConcurrentBinarySearchTree<int> tree, int start, int end, int offset)
    {
        for (int i = start; i <= end; i++)
        {
            tree.Update(i, i + offset);
        }
    }
}

Conclusione

In questo articolo, abbiamo presentato un’implementazione di un albero binario di ricerca concorrente in C#, progettata per essere concisa e facile da capire. Abbiamo discusso la definizione dell’interfaccia e fornito un’implementazione thread-safe delle operazioni concorrenti di ricerca, inserimento, cancellazione e aggiornamento, mantenendo le proprietà dell’albero binario di ricerca. L’albero binario di ricerca concorrente consente una manipolazione efficiente e sicura della struttura dati in un ambiente multithread, con complessità temporali di O(h) per ciascuna operazione, dove h è l’altezza dell’albero.

Risorse Correlate