Esercizio - Array dinamico ordinato come classe template (quarto appello 2022-23, programmazione)
Prova di programmazione (60 minuti) del quarto appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template
Testo. Implementare una classe template che gestisce un array dinamico in cui gli elementi sono sempre ordinati. La classe non può fare uso di contenitori standard STL. La classe deve avere: una funzione che inserisce un nuovo elemento nell'array; una funzione che cerca un elemento nell'array e, se è presente, ritorna l'indice; le altre funzioni necessarie per inizializzazione, copia e spostamento.
Progetto
Dati e invariante. T* data_, size_, capacity_ (come in un vettore con buffer sovradimensionato). Invariante: data_[0] <= data_[1] <= ... <= data_[size_-1] (ordine crescente rispetto a operator<). Ogni funzione che modifica il contenuto deve conservarlo; per questo l'accesso agli elementi è in sola lettura (const T& operator[](size_t) const): una scrittura diretta potrebbe rompere l'ordine.
Funzioni.
| funzione | scelta |
|---|---|
insert(const T&) |
trova con una ricerca binaria la posizione del primo elemento maggiore del valore, sposta a destra gli elementi successivi, scrive; se il buffer è pieno reserve(2 * capacity_) prima. I duplicati sono ammessi (il nuovo va dopo gli uguali: inserimento stabile). Costo: per la posizione + per lo spostamento |
find(const T&) const |
ricerca binaria (sfrutta l'ordine): ; restituisce l'indice di un elemento uguale, oppure npos (costante static constexpr size_t npos = -1). L'uguaglianza si ricava solo da < (!(a<b) && !(b<a)): così T deve fornire solo operator< |
| costruttore di default | vuoto, nessuna allocazione |
| distruttore | delete[] data_ |
| copia (costruttore e assegnamento) | profonda, con capacity_ = size_; l'assegnamento alloca prima, libera dopo, controlla this != &o |
| spostamento (costruttore e assegnamento) | noexcept, ruba il buffer e svuota la sorgente |
size(), operator[] const, reserve privato |
accessori e ingrandimento (raddoppio da 4) |
Requisiti su T: costruttore di default (per new T[n]), assegnamento di copia e operator<.
Soluzione
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <string>
#include <utility>
using namespace std;
// Array dinamico template i cui elementi sono SEMPRE ordinati (crescenti). Niente contenitori STL.
// Invarianti: data_[0..size_-1] e' ordinato; 0 <= size_ <= capacity_; data_ == nullptr se capacity_ == 0.
// Requisiti su T: costruttore di default, assegnamento di copia, operator< (e operator== non serve).
template <typename T>
class SortedArray {
public:
static constexpr size_t npos = static_cast<size_t>(-1); // "non trovato"
SortedArray() : size_{0}, capacity_{0}, data_{nullptr} {}
~SortedArray() { delete[] data_; }
SortedArray(const SortedArray& o) : size_{o.size_}, capacity_{o.size_}, data_{o.size_ ? new T[o.size_] : nullptr}
{
copy(o.data_, o.data_ + size_, data_);
}
SortedArray& operator=(const SortedArray& o)
{
if (this != &o) {
T* nuovo = o.size_ ? new T[o.size_] : nullptr;
copy(o.data_, o.data_ + o.size_, nuovo);
delete[] data_;
data_ = nuovo;
size_ = capacity_ = o.size_;
}
return *this;
}
SortedArray(SortedArray&& o) noexcept : size_{o.size_}, capacity_{o.capacity_}, data_{o.data_}
{
o.size_ = o.capacity_ = 0;
o.data_ = nullptr;
}
SortedArray& operator=(SortedArray&& o) noexcept
{
if (this != &o) {
delete[] data_;
size_ = o.size_; capacity_ = o.capacity_; data_ = o.data_;
o.size_ = o.capacity_ = 0;
o.data_ = nullptr;
}
return *this;
}
// inserisce mantenendo l'ordine: cerca la posizione con una ricerca binaria, sposta a destra, scrive
void insert(const T& value)
{
if (size_ == capacity_) reserve(capacity_ == 0 ? 4 : 2 * capacity_);
size_t pos = primo_maggiore(value); // posizione del primo elemento > value
for (size_t i = size_; i > pos; --i) data_[i] = data_[i - 1]; // fa posto
data_[pos] = value;
++size_;
}
// ricerca binaria: O(log n). Restituisce l'indice di un elemento uguale a value, oppure npos
size_t find(const T& value) const
{
size_t lo = 0, hi = size_; // intervallo [lo, hi)
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (data_[mid] < value) lo = mid + 1;
else if (value < data_[mid]) hi = mid;
else return mid;
}
return npos;
}
size_t size() const { return size_; }
const T& operator[](size_t i) const { return data_[i]; } // SOLA lettura: scrivere romperebbe l'ordine
private:
size_t primo_maggiore(const T& value) const // indice del primo elemento > value (o size_)
{
size_t lo = 0, hi = size_;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (value < data_[mid]) hi = mid;
else lo = mid + 1;
}
return lo;
}
void reserve(size_t nuova)
{
T* nuovo = new T[nuova];
copy(data_, data_ + size_, nuovo);
delete[] data_;
data_ = nuovo;
capacity_ = nuova;
}
size_t size_;
size_t capacity_;
T* data_;
};
template <typename T>
void stampa(const SortedArray<T>& a)
{
for (size_t i = 0; i < a.size(); ++i) cout << a[i] << ' ';
cout << '\n';
}
int main()
{
SortedArray<int> a;
for (int x : {42, 7, 19, 7, 88, 3, 56}) // inseriti disordinati
a.insert(x);
cout << "ordinato: ";
stampa(a);
cout << "find(19) = " << a.find(19) << ", find(88) = " << a.find(88)
<< ", find(5) e' npos? " << (a.find(5) == SortedArray<int>::npos) << '\n';
SortedArray<int> b = a; // copia profonda
b.insert(1);
cout << "a ha " << a.size() << " elementi, b ne ha " << b.size() << ": ";
stampa(b);
SortedArray<int> c = std::move(a); // spostamento
cout << "dopo lo spostamento: a ha " << a.size() << " elementi, c ne ha " << c.size() << '\n';
SortedArray<string> s; // anche con le stringhe (ordine alfabetico)
for (string w : {"pera", "mela", "uva", "banana"}) s.insert(w);
stampa(s);
cout << "find(\"uva\") = " << s.find("uva") << '\n';
return 0;
}ordinato: 3 7 7 19 42 56 88
find(19) = 3, find(88) = 6, find(5) e' npos? 1
a ha 7 elementi, b ne ha 8: 1 3 7 7 19 42 56 88
dopo lo spostamento: a ha 0 elementi, c ne ha 7
banana mela pera uva
find("uva") = 3Lettura: gli inserimenti disordinati (42, 7, 19, 7, 88, 3, 56) producono 3 7 7 19 42 56 88; find(19) indice 3 (4º elemento), find(88) 6 (l'ultimo), find(5) npos. La copia b è indipendente (b.insert(1) dà 8 elementi e lascia a a 7); dopo std::move(a), a è vuoto e c ha i 7 elementi. Con string l'ordine è alfabetico (banana mela pera uva) e find("uva") dà 3.
Passi di insert(19) in {3, 7, 7, 42, ...} (esempio): primo_maggiore(19) fa una ricerca binaria dell'indice del primo elemento (qui la posizione del 42); gli elementi da quella posizione in poi si spostano di una posizione a destra, partendo dall'ultimo (altrimenti si sovrascrivono: ciclo for (i = size_; i > pos; --i) data_[i] = data_[i-1];); si scrive 19 in pos.
Versione ripasso
- Invariante:
data_[0] <= ... <= data_[size_-1](crescente con<); accesso agli elementi solo in lettura (const T& operator[](size_t) const) per non romperlo. Dati:T* data_,size_,capacity_; nessun contenitore STL. insert(v):reserve(raddoppio da 4) se pieno;pos = primo_maggiore(v)con ricerca binaria; sposta a destra dall'ultimo (for (i = size_; i > pos; --i) data_[i] = data_[i-1];);data_[pos] = v; ++size_. Duplicati ammessi. Costo .find(v) const: ricerca binaria su :data_[mid] < vlo = mid + 1;v < data_[mid]hi = mid; altrimentimid;nposse assente. ; serve solooperator<.
size_t find(const T& value) const
{ size_t lo = 0, hi = size_;
while (lo < hi) { size_t mid = lo + (hi - lo) / 2;
if (data_[mid] < value) lo = mid + 1;
else if (value < data_[mid]) hi = mid;
else return mid; }
return npos; }- Inizializzazione, copia, spostamento: default vuoto; distruttore
delete[] data_; copia profonda (capacity_ = size_; assegnamento: alloca, copia, libera,this != &o); spostamentonoexcept, sorgente svuotata. Requisiti suT: default, assegnamento,operator<. - Prova: inserendo
42, 7, 19, 7, 88, 3, 563 7 7 19 42 56 88;find(19) = 3,find(88) = 6,find(5) =npos; copia indipendente (b8 elementi,a7); dopomoveavuoto,c7;string:banana mela pera uva,find("uva") = 3. - Errori tipici: spostare gli elementi dall'inizio (si sovrascrivono); ricerca lineare () invece di binaria;
operator[]che restituisceT&e rompe l'ordine;mid = (lo + hi) / 2(overflow);findcon==(impone un requisito in più); dimenticarereserve. primo_maggiore(v)(ricerca binaria del punto di inserimento):lo = 0, hi = size_; while (lo < hi) { mid = lo + (hi - lo) / 2; if (value < data_[mid]) hi = mid; else lo = mid + 1; } return lo;il nuovo elemento va dopo gli uguali (inserimento stabile).- Costo:
insertper trovare la posizione + per spostare;find; una ricerca lineare sarebbe .