Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Algoritmi di Ricerca e Ordinamento, Appunti di Programmazione Orientata agli Oggetti

Una panoramica sugli algoritmi di ricerca e ordinamento, come la ricerca lineare, la ricerca binaria, l'ordinamento a selezione, l'ordinamento per inserimento e l'ordinamento rapido (quicksort). Vengono discussi i principi di base di questi algoritmi, la loro complessità computazionale e alcuni esempi di implementazione in c++. Inoltre, il documento introduce i concetti di programmazione orientata agli oggetti, come le classi, i costruttori, i distruttori e il polimorfismo. Vengono presentati anche i template di funzioni e classi come strumenti per la programmazione generica. Complessivamente, il documento offre una solida introduzione agli argomenti di algoritmi e programmazione orientata agli oggetti, utile per studenti universitari di corsi di informatica e ingegneria.

Tipologia: Appunti

2023/2024

Caricato il 16/08/2024

alicebo3
alicebo3 🇮🇹

1 documento

1 / 25

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
La complessità di un algoritmo misura le risorse necessarie per eseguire l'algoritmo, solitamente in
termini di tempo (complessità temporale) e spazio (complessità spaziale)
Il costo, il "prezzo da pagare" per ottenere l'output dall'algoritmo stesso, di un algoritmo è in funzione di
n
,
ossia della dimensione dei dati in input
Il costo può essere associato:
Cos'è il costo computazionale o complessità?
Si può rappresentare:
n
i
=0
ci
ovvero la sommatoria dei costi delle istruzioni
Si definiscono anche:
Il tempo impiegato per risolvere un problema dipende sia dall'algoritmo utilizzato sia dalla
"dimensione" dei dati a cui si applica l'algoritmo
La complessità temporale è:
L'obbiettivo è valutare il comportamento di un algortimo quando le dimensioni
n
dell'input tendono ad infinito
Esempio:
N.B.: non si valuta dunque l'algoritmo in sé ma la dimensioni delle strutture dati che si utilizzano
Programmazione II - Teoria
Complessità
alla guardia
al corpo
tempo, come il numero di operazioni RAM eseguite
spazio, come il numero di celle di memoria occupate (escluse l'input)
Complessità Temporale
proporzionale al numero di istruzioni dell'intero programma eseguite
indipendente dall'hardware considerato
dipendente dalla dimensione dell'input
for(int i = 0; i < N; i++) {
swap(i, i + 1);
swap(i. i + 2);
v[i] = -4;
}
l'interesse è valutare il costo dell'istruzione che dipende dalla dimensione
N
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19

Anteprima parziale del testo

Scarica Algoritmi di Ricerca e Ordinamento e più Appunti in PDF di Programmazione Orientata agli Oggetti solo su Docsity!

La complessità di un algoritmo misura le risorse necessarie per eseguire l'algoritmo, solitamente in termini di tempo (complessità temporale) e spazio (complessità spaziale)

Il costo , il "prezzo da pagare" per ottenere l'output dall'algoritmo stesso, di un algoritmo è in funzione di n , ossia della dimensione dei dati in input

Il costo può essere associato:

Cos'è il costo computazionale o complessità? Si può rappresentare:

ni =

ci

ovvero la sommatoria dei costi delle istruzioni

Si definiscono anche:

Il tempo impiegato per risolvere un problema dipende sia dall'algoritmo utilizzato sia dalla "dimensione" dei dati a cui si applica l'algoritmo

La complessità temporale è:

L'obbiettivo è valutare il comportamento di un algortimo quando le dimensioni n dell'input tendono ad infinito

Esempio :

N.B. : non si valuta dunque l'algoritmo in sé ma la dimensioni delle strutture dati che si utilizzano

Programmazione II - Teoria

Complessità

alla guardia al corpo

tempo , come il numero di operazioni RAM eseguite spazio , come il numero di celle di memoria occupate (escluse l'input)

Complessità Temporale

proporzionale al numero di istruzioni dell'intero programma eseguite indipendente dall'hardware considerato dipendente dalla dimensione dell'input

for(int i = 0; i < N; i++) { swap(i, i + 1); swap(i. i + 2); v[i] = -4; }

l'interesse è valutare il costo dell'istruzione che dipende dalla dimensione N

Per valutare la complessità di un algoritmo è necessario sapere il suo comportamento asintotico , in modo tale da stimare in quanto il tempo aumenta al crescere dell'input

Esistono tre tipi di notazione asintotica:

Definisce il limite asintotico superiore e si applica a delle funzioni f con un limite asintotico superiore costituito dalla funzione di costo g ( n ):

N.B. : O ( g ( n )) è l'insieme di funzioni f ( n ) tali che esistano le costanti c , n 0 tale che f ( n ) sia sempre maggiore o uguale a 0 e minore o uguale a cg ( n ) per ogni nn 0 N.B. 2 : x 0 equivale ad n 0

Si può stabilire una relazione tra f ( n ) e O ( g ( n )):

Nella valutazione degli algoritmi si va a valutare la notazione O dando un limite superiore ad uno dei tre casi

Attraverso la notazione O () gli algoritmi vengono suddivisi in classi di equivalenza. Si hanno così algoritmi (funzioni) di complessità di ordine :

Caso Migliore e Peggiore

 il caso migliore ( costo minimo ) è quello che, dipendentemente dalla natura del problema, viene eseguito più velocemente  il caso peggiore ( costo massimo ) è il viceversa  il caso medio ( costo medio )

Notazione Asintotica

O

θ

Notazione O

si denota con O ( g ( n )) = { f ( n ) : ∃ c , n 0 | 0 ≤ f ( n ) ≤ cg ( n ) ∀ nn 0 }

f ( n ) = O ( g ( n )) → " f(n) è O(g(n)) "

costante O (1) ossia la complessità di una funzione o blocco istruzioni ciascuna di costo O (1), che non contengono cicli, ricorsione o chiamate ad altre funzioni non costanti logaritmica O (log n ), ossia la complessità di un ciclo quando le sue variabili sono incrementate o decrementate moltiplicandole o dividendole per una costante

Si stabilisce anche qui una relazione tra f ( n ) e Ω( g ( n )):

In definitiva , se un algoritmo A prende almeno tempo t ( n ), allora Ω( t ( n )) è un limite inferiore.

Definisce il limite asintotico stretto e viene così descritta:

Si stabilisce una relazione tra f ( n ) e Θ( g ( n )) leggermente più complessa:

Esercizio :

f ( n ) = Ω( g ( n ))

Notazione Θ

Θ( g ( n )) = { f ( n ) : ∃ c 1 , c 2 , n 0 | 0 ≤ c 1 ⋅ g ( n ) ≤ f ( n ) ≤ c 2 ⋅ g ( n ) ∀ nn 0 }

f ( n ) = Θ( g ( n )) ⟺ f ( n ) = Ω( g ( n )) e f ( n ) = O ( g ( n ))

Gli algoritmi di ricerca lineare hanno una complessità di circa O ( n ). Un esempio di algoritmo di ricerca lineare:

Un tipo di ricerca più efficiente è la ricerca dicotomica o ricerca binaria spiegata in seguito:

Si effettua una ricerca su un array già ordinato. Questa è un informazione importante dato che permette di:

Algoritmo :

Algoritmi di Ricerca e Ordinamento

Ricerca

bool linearSearch(int array[], int n, int key) { for(int i = 0; i < n; i++) { if(array[i] == key) { return true; } }

return false; }

Ricerca Dicotomica o Binaria

suddividere l'array a metà e controllare se la key è >, =, < dell'elemento presente nel mezzo ripetere le suddivisioni fino a che non si avranno due elementi per il quale scegliere quello richiesto

L'algoritmo di ordinamento classico che si utilizza computa eseguendo un confronto tra due elementi in cui: se l'elemento j , successivo all'elemento i , è più piccolo allora si effettua lo scambio tra i due

Algoritmo :

if (array[midpoint] == key) { found = true; } else if (key < array[midpoint]) { end = midpoint; } else { start = midpoint + 1; } }

if(found) { cout << key << " found."; } else { cout << key << " not found."; } }

int main() {

int *a = init_Array(); cout << "Unsorted Array:\n"; print_Array(a);

cout << "Sorted Array:\n"; sort_Array(a); print_Array(a);

int key; cin >> key;

binary_Search(a, key);

delete [] a; a = nullptr;

return 0; }

Algoritmi Iterativi o di Ordinamento

#include #include

#define N 10

using namespace std;

template void my_swap(T& a, T&b) {

T t = a; a = b; b = t; }

template void sorting(T* array, int n) {

for(int i = 0; i < n; i++) { for(int j = i + 1; j < n; j++) { if(array[j] < array[i]) { my_swap(array[i], array[j]); } } } }

int main(){

int array[N]; // double array[N];

random_device rd; mt19937 gen(rd()); uniform_int_distribution<> dis(1, 10);

for(int i = 0; i < N; i++) array[i] = dis(gen);

cout << "Unsorted array:\n"; for(int i = 0; i < N; i++) { cout << array[i] << " "; }

sorting(array, N);

cout << "\nSorted array:\n"; for(int i = 0; i < N; i++) { cout << array[i] << " "; }

return 0; }

#include

#define N 5

using namespace std;

void swap(int& a, int& b) {

int t = a; a = b; b = t; }

void selection_sort(int array[]) {

for(size_t i = 0; i < N; i++) { size_t index_min = i;

for(size_t j = i + 1; j < N; j++) { if(array[j] < array[i]) { index_min = j; } }

swap(array[i], array[index_min]); } }

int main() {

int array[N];

cout << "Enter array elements:\n"; for(size_t i = 0; i < N; i++) { cin >> array[i]; }

cout << "Unsorted Array:\n"; for(size_t i = 0; i < N; i++) { cout << array[i] << " "; }

selection_sort(array);

cout << "\nSorted Array:\n"; for(size_t i = 0; i < N; i++) { cout << array[i] << " "; }

return 0; }

Quest'algoritmo prevede l'utilizzo di una variabile temporanea che immagazina un elemento dello array per confrontarlo con tutti quelli che lo precedono: se uno degli elementi che lo precede è più grande dell'elemento nella variabile temporanea, si effettua uno scambio

Si ripete questa logica e queste operazioni finché l'array non è ordinato

Insertion Sort

#include

#define N 5

using namespace std;

void insertion_sort(int array[]) {

for(size_t i = 1; i < N; i++) { int temp = array[i];

size_t j = i; while((j > 0) && (temp < array[j - 1])) { array[j] = array[j - 1]; j--; }

array[j] = temp; } }

int main() {

int array[N];

for(size_t i = 0; i < N; i++) { cin >> array[i]; }

cout << "Unsorted Array:\n"; for(size_t i = 0; i < N; i++) { cout << array[i] << " "; }

insertion_sort(array);

cout << "\n\nSorted Array:\n"; for(size_t i = 0; i < N; i++) { cout << array[i] << " "; }

return 0; }

Complessità : O ( n log n ) e complessità spaziale maggiore rispetto ai classici algoritmi di ordinamento Vantaggio : per grandi n , è la soluzione ottimale rispetto a insertion sort , selection sort e bubble sort

Algoritmo :

stessa procedura per il sotto-array di destra : ... combinazione finale :

#include

using namespace std;

void merge(int* A, size_t p, size_t q, size_t r) {

size_t n1 = q-p+1; // Dimensione sotto-array L size_t n2 = r-q; // Dimensione sotto-array R

int* L = new int [n1+1]; int* R = new int [n2+1];

size_t i = 0, j = 0;

// Inizializzo L con gli elementi di sinistra di A for(i = 0; i < n1; i++) { L[i] = A[p+i]; }

// Inizializzo R con gli elementi di destra di A for(j = 0; j < n2; j++) { R[j] = A[q+1+j]; }

i = j = 0;

L[n1] = R[n2] = INT_MAX; // Elementi sentinella

for(size_t k = p; k <= r; k++) { if(L[i] < R[j]) { A[k] = L[i]; i++; } else { A[k] = R[j]; j++; } }

delete [] L; L = nullptr;

delete [] R; R = nullptr; }

void mergeSort(int* A, size_t p, size_t r) {

if(p < r) { size_t q = (p+r)/2; // suddivido l'array in due parti

mergeSort(A, p, q); // applico il merge sort fino a quando i sotto-array hanno dimensione = 1 mergeSort(A, q+1, r);

#include

using namespace std;

void swap(int& a, int& b) {

int t = a; a = b; b = t; }

int partition(int* A, size_t p, size_t r) {

size_t i = p-1; size_t j = p;

int& pivot = A[r];

while(j < r) { if(A[j] <= pivot) { i++; swap(A[i], A[j]); }

j++; }

swap(pivot, A[i+1]);

return i+1; }

void quickSort(int* A, size_t p, size_t r) {

if(p < r) { int q = partition(A, p, r);

quickSort(A, p, q-1); quickSort(A, q+1, r); } }

int main() {

constexpr size_t N = 8;

int array[N];

cout << "Fill array:\n"; for(size_t i = 0; i < N; i++) cin >> array[i];

cout << "Unsorted Array:\n";

Complessità : nel caso medio O ( n log n ) mentre nel caso peggiore O ( n^2 ) Vantaggio : l'algoritmo è più efficiente con n molto grandi dato che prendere un elemento che sia il minimo o il massimo è molto meno probabile

I principi di base di questo paradigma di programmazione sono:

E' un procedimento che consente di semplificare la realtà che si vuole modellare. La semplificazione avviene concentrando l'attenzione solo sugli elementi importanti del sistema complesso preso in considerazione

Terminologia:

E' la capacità di una classe di incapsulare sia le caratteristiche (attributi) che i comportamenti (metodi) degli oggetti che rappresenta

Limita di molto gli errori rispetto alla programmazione strutturata

Una classe è un insieme di oggetti che condividono struttura e comportamento

Essa contiene:

Definizione :

for(size_t i = 0; i < N; i++) cout << array[i] << " ";

cout << "\nSorted Array:\n"; quickSort(array, N-N, N); for(size_t i = 0; i < N; i++) cout << array[i] << " ";

return 0; }

OOP (Object Oriented Programming)

astrazione incapsulamento (o information hiding ) ereditarietà polimorfismo

1. Astrazione

Astrarre significa semplificare delle entità complesse in oggetti caratterizzati dalle caratteristiche e dalle funzionalità essenziali per gli scopi preposti

2. Information Hiding

Classe

attributi , specifica dei dati che descrivono ogni oggetto che ne fa parte metodi , descrizione delle azioni che l'oggetto stesso è capace di eseguire

E' un metodo speciale che viene chiamato automaticamente quando si distrugge un oggetto; serve per liberare la memoria assegnata dal costruttore

Caratteristiche:

Separare in files diversi il codice "cliente" della classe dal codice della classe

Separare il codice sorgente di una classe in due files con lo stesso nome della classe

Così come una classe è un modello per istanziare oggetti a tempo di esecuzione, un template è un modello per istanziare classi o funzioni a tempo di compilazione

Esempio :

Esempio :

Distruttore

stesso nome della classe preceduta dal simbolo ~ non ha tipo di ritorno non accetta parametri non può essercene più di uno se non si dichiara, C ne crea uno in automatico

Header File

Intestazione di classe

uno con la definizione della classe (solo prototipi dei metodi) ed estensione .h un altro con le implementazioni dei metodi della classe ed estensione .cpp

Template di classi e funzioni

Template di Funzioni

// swap function using a template

template void swap(int& x, int& y) { T t = x; x = y; y = t; }

Template di Classi

Una classe derivata eredita attributi e metodi dalla classe base già esistente

E' un ulteriore livello di astrazione che permette di avere un livello più alto rispetto a un interfaccia comune ( classe base ), grazie al al quale tutte le proprietà di questa classe vengono estese alle classi derivate (in quest'ultime si possono aggiungere nuove specifiche e funzionalità)

Il polimorfismo permette di avere funzioni che hanno la stessa firma ma assumono diversi comportamenti a seconda del chiamante

Esempio :

template class MyClass { private: T data;

public: // Costruttore MyClass(T data) : data(data) {}

// Metodo per ottenere il valore dei dati T getData() const { return data; }

// Metodo per impostare il valore dei dati void setData(T data) { this->data = data; }

// Metodo per stampare i dati void display() const { cout << "Data: " << data << endl; } };

int main() { // Creazione di oggetti della classe template con tipi diversi MyClass intObj(42); MyClass doubleObj(3.14); MyClass stringObj("Hello, World!");

// Utilizzo dei metodi della classe template intObj.display(); doubleObj.display(); stringObj.display();

return 0; }

3. Ereditarietà e 4. Polimorfismo