[C++] Lista di numeri

Messaggioda ncant » 29/01/2024, 16:19

Salve a tutti.

Per esercitarmi, ho deciso di fare una struttura dati composta da due sottoliste: una contenente solo numeri pari ed un'altra contenente solo numeri dispari. Gli elementi sono gestiti secondo la strategia FIFO (first in, first out. La funzione di inserimento accetta come solo argomento il numero da inserire nella struttura, assumendosi la responsabilità di inserirlo nella sottolista adeguata, mentre, nel caso dell'estrazione, sarà necessario specificare la sottolista in questione. Di seguito riporto quanto ho scritto. Spero sia abbastanza comprensibile sia nelle mie intenzioni, sia nelle funzioni che deve svolgere.

Codice:
#include <iostream>
using namespace std;

enum tipo_numero { PARI, DISPARI };

class ListaNumeri {
   friend ostream& operator<<(ostream&, const ListaNumeri&);

   class Lista {
      struct elem {
         int num;
         elem* prox;
      };

      elem *testa;
      void copia(const Lista&);
      void elimina();

   public:
      Lista() { testa = NULL; }
      Lista(const Lista& src) { copia(src); }
      Lista& operator=(const Lista&);
      void inserisci(int);
      int estrai(int&);
      int somma() const;
      int operator!() const;
      ~Lista() { elimina(); }
   };

   // Crea due sottoliste: una per i numeri pari, una per i numeri dispari.
   Lista liste[2];

public:
   ListaNumeri() {}
   ListaNumeri(const ListaNumeri&);
   ListaNumeri& operator=(const ListaNumeri&);
   int inserisci(int);
   int estrai(tipo_numero, int&);
   int operator!() const;
   int operator%(tipo_numero) const;
   int operator[](tipo_numero) const;
};

//
// Classe Lista
//

void ListaNumeri::Lista::copia(const ListaNumeri::Lista& src)
{
   testa = NULL;

   if (src.testa != NULL) {
      testa = new elem;
      testa->num = src.testa->num;
      elem *temp = testa, *aux = src.testa;

      while (temp != NULL) {
         temp->prox = new elem;
         temp = temp->prox;
         temp->num = aux->num;
         aux = aux->prox;
      }

      temp->prox = NULL;
   }
}

void ListaNumeri::Lista::elimina()
{
   elem *temp = testa;

   while (temp != NULL) {
      testa = testa->prox;
      delete temp;
      temp = testa;
   }
}

ListaNumeri::Lista& ListaNumeri::Lista::operator=(const Lista& src)
{
   if (this != &src) {
      elimina();
      copia(src);
   }

   return *this;
}

// Inserzione in coda.
void ListaNumeri::Lista::inserisci(int n)
{
   elem *temp = new elem;
   temp->num = n;
   temp->prox = NULL;

   if (testa == NULL) {
      testa = temp;
   } else {
      elem *ultimo = testa;

      while (ultimo->prox != NULL)
         ultimo = ultimo->prox;

      ultimo->prox = temp;
   }
}

// Estrazione in testa.
int ListaNumeri::Lista::estrai(int& num_estratto)
{
   if (testa != NULL) {
      elem *temp = testa;
      testa = testa->prox;
      num_estratto = temp->num;
      delete temp;

      return 1;
   }

   return 0;
}

// Restituisce il numero di elementi presenti in una lista.
int ListaNumeri::Lista::operator!() const
{
   int conta = 0;
   for (elem *temp = testa; temp != NULL; temp = temp->prox)
      conta++;

   return conta;
}

// Restituisce la somma di tutti i valori memorizzati nella lista.
int ListaNumeri::Lista::somma() const
{
   int somma = 0;
   elem *aux = testa;

   while (aux != NULL) {
      somma += aux->num;
      aux = aux->prox;
   }

   return somma;
}

//
// Classe ListaNumeri
//

int ListaNumeri::inserisci(int n)
{
#ifndef ZERO_OP
   if (n == 0) return 0;
#endif

   if (n % 2)
      liste[DISPARI].inserisci(n);
   else
      liste[PARI].inserisci(n);

   return 1;
}

inline int ListaNumeri::estrai(tipo_numero tn, int& num_estratto)
{
   return liste[tn].estrai(num_estratto);
}

// Restituisce il numero di elementi contenuti in una determinata sottolista.
inline int ListaNumeri::operator%(tipo_numero tn) const
{
   return !(liste[tn]);
}

// Restituisce la somma dei valori degli elementi contenuti in una
// delle due sottoliste.
inline int ListaNumeri::operator[](tipo_numero tn) const
{
   return liste[tn].somma();
}

// Restituisce il numero totale di elementi contenuti nella lista di numeri.
inline int ListaNumeri::operator!() const
{
   return ((!liste[PARI]) + (!liste[DISPARI]));
}

ostream& operator<<(ostream& os, const ListaNumeri& src)
{
   os << '<' << src % PARI << ", " << src % DISPARI << '>';
   return os;
}


Qualche consiglio/opinione in merito?
Grazie e scusate per il disturbo.
Il mio rapporto con la matematica è come quello tra Dante e Beatrice: la amo, ma è un'amore non corrisposto.
Avatar utente
ncant
Junior Member
Junior Member
 
Messaggio: 66 di 102
Iscritto il: 16/12/2023, 15:57
Località: qualche volta a Roma, qualche volta a Pisa

Re: [C++] Lista di numeri

Messaggioda Quinzio » 29/01/2024, 20:43

Quinzio
Cannot live without
Cannot live without
 
Messaggio: 5809 di 10547
Iscritto il: 24/08/2010, 06:50

Re: [C++] Lista di numeri

Messaggioda utente__medio » 30/01/2024, 11:52

ncant ha scritto:Qualche consiglio/opinione in merito?

Ciao, la risposta dipende da qual è lo scopo del codice.
Se l'intento era quello di esercitarsi sulle classi in generale facendo esperimenti sulle classi nidificate e sull'overloading degli operatori, allora mi sembra inutile andare a fare le pulci al codice su dettagli implementativi e sull'impostazione logica delle strutture; in questo caso l'importante è che "funzioni", e dei test come quelli fatti da @Quinzio sei sicuramente in grado di farli anche da solo.
Se invece lo scopo era quello di esercitarsi sulle liste, allora i margini di miglioramento sono ampi. In tal caso il mio consiglio è quello di implementare una classe sulle liste semplici fatta per bene, da utilizzare poi per la struttura ListaNumeri da te ideata.
"Ci abbaiano, Sancho; segno che stiamo cavalcando!"
utente__medio
Junior Member
Junior Member
 
Messaggio: 320 di 394
Iscritto il: 02/11/2021, 12:48
Località: Draghistan

Re: [C++] Lista di numeri

Messaggioda apatriarca » 30/01/2024, 12:25

In generale mi sembra che il codice sia corretto. Ho inserito alcuni commenti su miglioramenti che immagino possano esserti utili per il futuro. Ho inserito una versione con tutti questi commenti alla fine e anche una funzione main per il test.

Se inserisci questo codice all'interno di un file header è necessario fare uso di inclusion guards oppure di #pragma once come descritto nella stessa pagina del link. Inoltre tutte le funzioni dovrebbe essere dichiarate inline se la loro definizione è presente nel file header.

È considerata una cattiva pratica quella di usare using namespace std;, soprattutto in file che devono essere inclusi in altri perché potresti causare dei problemi nel caso qualcuno dichiarasse delle funzioni, classi o variabili con lo stesso nome di funzioni, classi o variabili definiti nelle librerie standard.

Sinceramente dichiarerei la classe Lista e la classe ListaNumeri separatamente. Invece l'enumerazione tipo_numero sembra maggiormente legata alla classe ListaNumeri in quanto non ha granché senso da sola e la vedrei più come qualcosa di interna.

In versioni moderne del C++ puoi inizializzare le variabili membro nel costruttore come segue:
Codice:
Lista() : testa(nullptr) { }

In versioni ancora più moderne puoi scrivere invece direttamente
Codice:
Elem* testa = nullptr;

Puoi avere maggiori informazioni sui costruttori per esempio in questa pagina mentre in quest'altra pagina si parla della inizializzazione delle variabili membro. La tua versione non è sbagliata comunque, questo commento è più che altro informativo sulle diverse opzioni disponibili. Nota che ho fatto uso di nullptr invece di NULL. Guarda qui perché.

Il codice della copia mi sembra corretto, tuttavia gestirei al suo interno l'eliminazione del contenuto corrente della lista invece di delegarlo alla funzione che la chiama. Questo perché la funzione copia causerebbe memory leak se non chiamata su una lista vuota. Discorso simile vale per la copia di se stessi.

Puoi inizializzare e creare una struttura come:
Codice:
// BEFORE
elem* temp = new elem;
temp->num = n;
temp->prox = NULL;
// AFTER
elem* temp = new elem{ n, nullptr };


Non è chiaro perchè estrai restituisce un intero invece di un valore booleano. Se fai uso di una versione recente del C++, l'alternativa di solito preferibile per estrai è quella di usare std::optional.

La tua scelta di operatori da usare è un po' strana. Per esempio operator! è normalmente usato come NOT LOGICO e ci si aspetta quindi restituisca un valore booleano. Per una lista di solito questo è interpretato come test per vedere se la lista è vuota. Tu invece restituisci la lunghezza della lista. Questo significa che se scrivi:
Codice:
if (!lista) {
    // ...
}

si entra quando lista->testa != nullptr che è l'opposto di quanto uno si aspetta.

Il costruttore di copia di ListaNumeri non ha alcuna definizione. Neanche l'assegnazione.

Puoi usare il n % 2 per accedere direttamente alla lista corretta come segue:
Codice:
liste[n % 2].inserisci(n);


Per quale ragione inserisci restituisce un valore in ListaNumeri se restituisce sempre lo stesso valore?

Non mi sembra ci sia bisogno di avere operator<< come friend della classe.

Stilisticamente credo sarebbe meglio essere più consistenti nel modo in cui dai i nomi ai tipi. Alcuni sono in CamelCase mentre altri sono in snake_case. Farei uso dello stesso stile ovunque. Non importa quale.

In generale, piuttosto che scrivere:
Codice:
if (condizione) {
    // resto della funzione
}

sarebbe meglio scrivere
Codice:
if (!condizione) {
    return;
}

// resto della funzione

È comunque più che altro una scelta stilistica.

Codice:
#pragma once

#include <iostream>
#include <optional>

//
// Classe Lista
//

class Lista {
    struct Elem {
        int num;
        Elem* prox;
    };

    Elem* testa = nullptr;

    void copia(const Lista&);
    void elimina();

public:
    Lista() = default;

    Lista(const Lista& src) { copia(src); }

    virtual ~Lista() { elimina(); }

    Lista& operator=(const Lista&);

    void inserisci(int);
   
    std::optional<int> estrai();
   
    int somma() const;
   
    int lunghezza() const;
};

inline void Lista::copia(const Lista& src)
{
    // Niente da fare se copiamo noi stessi
    if (this == &src) {
        return;
    }

    // Rimuoviamo il contenuto corrente della lista
    elimina();

    // Niente da fare se stiamo copiando una lista vuota
    if (src.testa == nullptr) {
        return;
    }

    // Crea il nodo di testa
    testa = new Elem{ src.testa->num, nullptr };
   
    // Copia il resto della lista
    Elem* temp = testa;
    Elem* aux = src.testa;

    while (temp != nullptr) {
        temp->prox = new Elem{ aux->num, nullptr };
       
        temp = temp->prox;
        aux = aux->prox;
    }
}

inline void Lista::elimina()
{
    Elem* temp = testa;

    while (temp != nullptr) {
        testa = testa->prox;
        delete temp;
        temp = testa;
    }
}

inline Lista& Lista::operator=(const Lista& src)
{
    copia(src);
    return *this;
}

// Inserzione in coda.
inline void Lista::inserisci(int n)
{
    Elem* temp = new Elem{ n, nullptr };

    if (testa == nullptr) {
        testa = temp;
    } else {
        Elem* ultimo = testa;
        while (ultimo->prox != nullptr) {
            ultimo = ultimo->prox;
        }

        ultimo->prox = temp;
    }
}

// Estrazione in testa.
inline std::optional<int> Lista::estrai()
{
    if (testa == nullptr) {
        return {};
    }

    Elem* temp = testa;
    testa = testa->prox;
    int num_estratto = temp->num;
    delete temp;

    return { num_estratto };
}

// Restituisce il numero di elementi presenti in una lista.
inline int Lista::lunghezza() const
{
    int conta = 0;

    for (Elem* temp = testa; temp != nullptr; temp = temp->prox) {
        conta++;
    }

    return conta;
}

// Restituisce la somma di tutti i valori memorizzati nella lista.
inline int Lista::somma() const
{
    int somma = 0;
   
    Elem* aux = testa;
    while (aux != nullptr) {
        somma += aux->num;
        aux = aux->prox;
    }

    return somma;
}

//
// Classe ListaNumeri
//

class ListaNumeri {
    // Crea due sottoliste: una per i numeri pari, una per i numeri dispari.
    Lista liste[2];

public:

    enum TipoNumero { PARI, DISPARI };

    ListaNumeri() {}

    ListaNumeri(const ListaNumeri& src)
        : liste{ src.liste[0], src.liste[1] }
    {}
   
    ListaNumeri& operator=(const ListaNumeri& src)
    {
        liste[0] = src.liste[0];
        liste[1] = src.liste[1];
        return *this;
    }

    void inserisci(int);

    std::optional<int> estrai(TipoNumero);

    int lunghezza() const;

    int lunghezza(TipoNumero) const;

    int somma(TipoNumero) const;
};

void ListaNumeri::inserisci(int n)
{
    liste[n % 2].inserisci(n);
}

inline std::optional<int> ListaNumeri::estrai(TipoNumero tn)
{
    return liste[tn].estrai();
}

// Restituisce il numero di elementi contenuti in una determinata sottolista.
inline int ListaNumeri::lunghezza(TipoNumero tn) const
{
    return liste[tn].lunghezza();
}

// Restituisce la somma dei valori degli elementi contenuti in una
// delle due sottoliste.
inline int ListaNumeri::somma(TipoNumero tn) const
{
    return liste[tn].somma();
}

// Restituisce il numero totale di elementi contenuti nella lista di numeri.
inline int ListaNumeri::lunghezza() const
{
    return liste[PARI].lunghezza() + liste[DISPARI].lunghezza();
}

std::ostream& operator<<(std::ostream& os, const ListaNumeri& src)
{
    os << '<' << src.lunghezza(ListaNumeri::PARI) << ", " << src.lunghezza(ListaNumeri::DISPARI) << '>';
    return os;
}


Il sorgente di test.
Codice:
#include <iostream>
#include "listaNumeri.hpp"

int main(int argc, char* argv[])
{
    ListaNumeri lista;

    lista.inserisci(10);
    lista.inserisci(12);
    lista.inserisci(15);
    lista.inserisci(1);
    lista.inserisci(34);

    std::cout << "EXPECTED: <3, 2>\n";
    std::cout << "GOT: " << lista << "\n";

    std::cout << "EXPECTED: 56, 16\n";
    std::cout << "EXPECTED: " << lista.somma(ListaNumeri::PARI) << ", " << lista.somma(ListaNumeri::DISPARI) << "\n";

    lista.estrai(ListaNumeri::DISPARI);

    std::cout << "EXPECTED: <3, 1>\n";
    std::cout << "GOT: " << lista << "\n";

    std::cout << "EXPECTED: 56, 1\n";
    std::cout << "EXPECTED: " << lista.somma(ListaNumeri::PARI) << ", " << lista.somma(ListaNumeri::DISPARI) << "\n";

    lista.estrai(ListaNumeri::PARI);
    lista.estrai(ListaNumeri::PARI);
    lista.inserisci(4);
    lista.inserisci(3);

    std::cout << "EXPECTED: <2, 2>\n";
    std::cout << "GOT: " << lista << "\n";

    std::cout << "EXPECTED: 38, 4\n";
    std::cout << "EXPECTED: " << lista.somma(ListaNumeri::PARI) << ", " << lista.somma(ListaNumeri::DISPARI) << "\n";
}
apatriarca
Moderatore
Moderatore
 
Messaggio: 5784 di 10436
Iscritto il: 08/12/2008, 20:37
Località: Madrid

Re: [C++] Lista di numeri

Messaggioda apatriarca » 30/01/2024, 12:36

Ovviamente una lista concatenata semplice per implementare una logica FIFO non è ideale. L'inserimento ha infatti complessità lineare O(n) perché devi visitare tutti gli elementi per arrivare alla fine. Le liste concatenate sono pensate per essere usate con logica LIFO perché in questa caso sia l'inserimento che l'estrazione avvengono in tempo costante.

Una lista doppia concatenata sarebbe meglio. Oppure puoi fare uso di strutture dati migliori. Le liste di questo tipo sono raramente la struttura dati migliore da usare. Sono più che altro utili per imparare a lavorare con i puntatori, ma la loro utilità in linguaggi come il C++ è limitata. Sono principalmente utili in linguaggi funzionali, ma in quel caso il linguaggio semplifica fortemente il loro utilizzo.
apatriarca
Moderatore
Moderatore
 
Messaggio: 5785 di 10436
Iscritto il: 08/12/2008, 20:37
Località: Madrid

Re: [C++] Lista di numeri

Messaggioda ncant » 05/02/2024, 16:23

Ok, grazie mille a tutti (e scusate per il ritardo nella risposta). Siete stati utilissimi!

Come già accennato da @apatriarca, sarebbe mooolto più facile creare una lista costituita da un nodo di testa ed uno di coda, così da facilitare le cose con l'inserzione in coda.

Eh sì, prima bisogna fare la gavetta, poi puoi usare std::list !!
Il mio rapporto con la matematica è come quello tra Dante e Beatrice: la amo, ma è un'amore non corrisposto.
Avatar utente
ncant
Junior Member
Junior Member
 
Messaggio: 68 di 102
Iscritto il: 16/12/2023, 15:57
Località: qualche volta a Roma, qualche volta a Pisa

Re: [C++] Lista di numeri

Messaggioda apatriarca » 06/02/2024, 10:57

Piuttosto puoi ignorare completamente le liste concatenate e usare std::vector... Nella maggioranza dei casi è la struttura dati migliore (spesso anche quando std::list sembra avere performance migliori sulla carta).

Sinceramente, al di fuori dei corsi di studio, non ho mai dovuto implementare qualcosa di simile ad una lista o strutture dati complesse. Mi è capitato di realizzare qualche albero, ma nella maggior parte dei casi ho fatto uso di strutture dati fornite dal linguaggio (o da altre librerie di cui facevo uso). Quindi non sono del tutto convinto sulla tua teoria della gavetta. Meglio fare uso di tutto quello che puoi al di fuori dei corsi e imparare a risolvere problemi reali di programmazione, piuttosto che creare soluzioni mediocri a problemi che non esistono.
apatriarca
Moderatore
Moderatore
 
Messaggio: 5787 di 10436
Iscritto il: 08/12/2008, 20:37
Località: Madrid


Torna a Informatica

Chi c’è in linea

Visitano il forum: Google [Bot] e 1 ospite