In che modo questo algoritmo di ordinamento Θ (n³) e non Θ (n²), nel caso peggiore?


52

Ho appena iniziato a seguire un corso su Strutture di dati e algoritmi e il mio assistente di insegnamento ci ha fornito il seguente pseudo-codice per ordinare una matrice di numeri interi:

void F3() {
    for (int i = 1; i < n; i++) {
        if (A[i-1] > A[i]) {
            swap(i-1, i)
            i = 0
        }
    }
}

Potrebbe non essere chiaro, ma qui è la dimensione dell'array che stiamo cercando di ordinare.nA

In ogni caso, l'assistente didattico ha spiegato alla classe che questo algoritmo è in tempo di (nel peggiore dei casi, credo), ma non importa quante volte lo attraversi con un array in ordine inverso, mi sembra che dovrebbe essere e non .Θ(n3)Θ(n2)Θ(n3)

Qualcuno potrebbe spiegarmi perché questo è Θ(n3) e non Θ(n2) ?


Potresti essere interessato ad un approccio strutturato all'analisi ; prova tu stesso a trovare una prova!
Raffaello

Implementalo e misura per convincerti. Un array con 10.000 elementi in ordine inverso dovrebbe richiedere molti minuti e un array con 20.000 elementi in ordine inverso dovrebbe richiedere circa otto volte di più.
gnasher729,

@ gnasher729 Non ti sbagli, ma la mia soluzione è diversa: se provi a dimostrare il tuo limite fallirai invariabilmente, il che ti dirà che qualcosa non va. (Certo, si possono fare entrambe le cose. Tracciare / adattare è decisamente più veloce per rifiutare l'ipotesi, ma meno affidabile . Fintanto che fai qualche tipo di analisi formale / strutturata, nessun danno fatto. Affidarsi alle trame è dove iniziano i problemi.)O(n2)
Raffaello

1
a causa della i = 0dichiarazione
njzk2,

Risposte:


60

Questo algoritmo può essere riscritto in questo modo

  1. Scansione Afino a trovare un'inversione .
  2. Se ne trovi uno, scambia e ricomincia.
  3. Se non ce n'è nessuno, terminare.

Ora possono esserci al massimo e hai bisogno di una scansione a tempo lineare per trovarle, quindi il tempo di esecuzione peggiore è . Un bell'esempio di insegnamento mentre si avvicina all'approccio del pattern-matching a cui molti soccombono!(n2)Θ(n2)Θ(n3)

Nota bene: bisogna stare un po 'attenti: alcune inversioni appaiono in anticipo, altre in ritardo, quindi non è di per sé banale che i costi si sommino come richiesto (per il limite inferiore). È inoltre necessario osservare che gli swap non introducono mai nuove inversioni. Un'analisi più dettagliata del caso con la matrice ordinata inversamente produrrà quindi qualcosa come il caso quadratico della formula di Gauss.

Come giustamente commenta @ gnasher729, è facile vedere il tempo di esecuzione nel caso peggiore è analizzando il tempo di esecuzione durante l'ordinamento dell'input (sebbene questo input non sia probabilmente il caso peggiore).Ω(n3)[1,2,,n,2n,2n1,,n+1]

Attenzione: non dare per scontato che un array ordinato in modo inverso sia necessariamente l'input nel caso peggiore per tutti gli algoritmi di ordinamento. Dipende dall'algoritmo. Esistono alcuni algoritmi di ordinamento in cui una matrice ordinata in modo inverso non è il caso peggiore e potrebbe persino essere vicina al caso migliore.


14
Se prendi un array in cui la prima metà è composta dai numeri da 1 a n / 2 in ordine crescente e la seconda metà è da n a n / 2 + 1 in ordine inverso, allora è ovvio che hai bisogno di almeno n / 2 passi per trovare ogni inversione, e ci saranno circa (n / 2) ^ 2/2 di essi. E molto probabilmente non è il caso peggiore.
gnasher729,

@AnthonyRossello È un risultato standard (in combinatoria di permutazioni). In breve, conta il numero di inversioni nell'array ordinato in modo inverso (è ovvio che quello è il caso peggiore?); è una somma di Gauss.
Raffaello

Bisogna ricordare che, indipendentemente da cosa, le somme parziali di sono sempre , è solo il coefficiente che scende rapidamente: (nota il coefficiente abbastanza grande ). Il problema è che non si preoccupa dei coefficienti. Θ(nα)Θ(nα+1)k=0nkα1α+1nα+11α+1Θ
yo'

2
@yo 'E questo si riferisce alla risposta (o alla domanda) come?
Raffaello

7

Un modo alternativo di pensare a questo è quello che idiventa il valore massimo prima che venga resettato. Questo, a quanto pare, rende più semplice ragionare su come il precedente ordinamento Ainfluenzi il tempo di esecuzione dell'algoritmo.

In particolare, osserva che quando iimposta il suo nuovo valore massimo, chiamiamolo N, l'array [A[0], ..., A[N-1]]viene ordinato in ordine crescente.

Quindi cosa succede quando aggiungiamo l'elemento A[N]al mix?

La matematica:

Bene, diciamo che si adatta alla posizione . Quindi abbiamo bisogno di iterazioni in loop (che indicheremo ) per spostarlo per posizionare , iterazioni per spostarlo in posizione , e in generale:pNNstepsN1N+(N1)N2

stepsN(pN)=N+(N1)+(N2)++(pN+1)=12(N(N+1)pN(pN+1))

Per una matrice ordinata casualmente, assume la distribuzione uniforme su per ogni , con:pN{0,1,,N}N

E(stepsN(pN))=a=1NP(pN=a)stepsN(a)=a=1N1N12(N(N+1)a(a+1))=12(N(N+1)13(N+1)(N+2))=13(N21)=Θ(N2)

la somma può essere mostrata usando la formula di Faulhaber o il link Wolfram Alpha in basso.

Per una matrice ordinata inversamente, per tutte le e otteniamo:pN=0N

stepsN(pN)=12N(N+1)

esattamente, impiegando strettamente più tempo di qualsiasi altro valore di .pN

Per una matrice già ordinata, e , con i termini di ordine inferiore che diventano rilevanti.pN=NstepsN(pN)=0

Tempo totale:

Per ottenere il tempo totale, sommiamo i gradini su tutta la . (Se stessimo facendo molta attenzione, riassumemmo gli swap e le iterazioni di loop e ci occuperemo delle condizioni di inizio e fine, ma è ragionevolmente facile vedere che non contribuiscono alla complessità nella maggior parte dei casi) .N

E ancora, usando la linearità delle aspettative e la Formula di Faulhaber:

Expected Total Steps=E(N=1nstepsN(pN))=N=1nE(stepsN(pN))=Θ(n3)

Naturalmente, se per qualche motivo non è (ad es. La distribuzione degli array che stiamo guardando sono già molto vicini all'ordinamento), allora non è necessario che sempre essere il caso. Ma ci vogliono distribuzioni molto specifiche su per raggiungere questo obiettivo!stepsN(pN)Θ(N2)pN

Lettura pertinente:


@Raphael - grazie per i miglioramenti suggeriti, ho aggiunto un po 'più di dettagli. Bene, le variabili casuali sono il (da , il set di ordinamenti di ), quindi le aspettative sono tecnicamente fatte supiΩAΩ
David E

Diverso ; Intendevo quello di Landau. Ω
Raffaello

3

Clausola di esclusione della responsabilità:

Questa non è una prova (sembra che alcune persone pensino che l'abbia pubblicato come se lo fosse). Questo è solo un piccolo esperimento che l'OP potrebbe eseguire per risolvere i suoi dubbi sull'incarico:

non importa quante volte lo attraversi con una matrice ordinata in modo inverso, mi sembra che dovrebbe essere e non .Θ(n2)Θ(n3)

Con un codice così semplice, la differenza tra e non dovrebbe essere difficile da individuare e in molti casi pratici questo è un approccio utile per verificare intuizioni o adeguare le aspettative.Θ(n2)Θ(n3)


@Raphael ha già risposto alla tua domanda, ma solo per i calci, adattando l' output di questo programma a usando questo script gnuplot ha riportato valori esponenti di e e ha prodotto i seguenti grafici ( la prima è la scala normale e la seconda è la scala log-log):f(x)=axb+cx2.997961668332222.99223727692339

normale loglog

Spero che questo aiuti¨


2
È possibile adattare qualsiasi funzione a questi valori. Vedi anche qui .
Raffaello

3
@Raphael Se non vuoi fare il nitpick in questo modo, allora no, non puoi adattare nessuna funzione (ad esempio non sarai in grado di adattare una funzione costante a una precisione ragionevole). Questa non è una prova, ma esiste già una risposta che fornisce uno schizzo. Per quanto riguarda l'utilità, posso citare il tuo post che hai collegato: "Devo concordare sul fatto che questo è un approccio molto utile che a volte è anche sottoutilizzato". Inoltre, l'OP ha detto che pensava che dovesse essere piuttosto che , quindi perché non sperimentare e vedere se il suo sospetto era corretto? Cont. Θ(n2)Θ(n3)
dtldarek,

2
Ciò fornisce la prova che l'algoritmo è ma la domanda chiede perché . Sta chiedendo una spiegazione del fenomeno, non una sua conferma. Θ(n3)
David Richerby,

2
@DavidRicherby Questo significa che questa risposta non è utile?
dtldarek,

3
@Magicsowon È un sito di domande e risposte, non un forum. Siamo alla ricerca di risposte alla domanda, non di discussioni al riguardo.
David Richerby,

3

Supponiamo di avere un array.

array a[10] = {10,8,9,6,7,4,5,2,3,0,1}

Il tuo algoritmo procede come segue

Scan(1) - Swap (10,8) => {8,10,9,6,7,4,5,2,3,0,1}  //keep looking at "10"
Scan(2) - Swap (10,9) => {8,9,10,6,7,4,5,2,3,0,1}
...
Scan(10) - Swap(10,1) => {8,9,6,7,4,5,2,3,0,1,10}

Fondamentalmente sposta alla fine dell'array l'elemento più alto, e nel fare ciò ricomincia da capo ad ogni scansione facendo effettivamente delle O(n^2)mosse ... solo per quell'elemento. Tuttavia, ci sono n elementi quindi dovremo ripetere questa nvolta. Questa non è una prova formale, ma aiuta a capire in modo "non formale" perché il tempo di esecuzione è O(n^3).


4
Cosa aggiunge questo alle altre risposte? È stata già fornita una spiegazione di ciò che l'algoritmo fa e il tuo ragionamento per il runtime è al massimo impreciso. (Il caso peggiore non si comporta in modo lineare!)
Raffaello

2
A volte è utile spiegare la stessa idea in molti modi (con formalismo; con un semplice esempio per "pompare l'intuizione"), specialmente quando la persona che pone la domanda è nuova sul campo. Quindi mi sembra che ciò aggiunga che è presentato in un modo che potrebbe aiutare l'intuizione.
DW

Da quando ho ricevuto una risposta al mio commento in una bandiera (non farlo!): "Il caso peggiore non si comporta in modo lineare!" - Intendo le proprietà algebriche dell'operatore nel caso peggiore. In parole povere, stai usando WorstCase (1 + ... + n) "=" WorstCase (1) + ... + WorstCase (n) ma questa identità non è valida.
Raffaello

1
Sono nuovo in questo campo e fornire una spiegazione con un esempio concreto e esplicito mi ha sicuramente aiutato a ottenere un'intuizione sul problema. Ora la soluzione accettata ha più senso per me.
vaer-k,

0

La logica sembra ordinare gli elementi nell'array in ordine crescente.

Supponiamo che il numero più piccolo sia alla fine dell'array (a [n]). Perché arrivi al posto giusto - sono necessarie le operazioni (n + (n-1) + (n-2) + ... 3 + 2 + 1). = O (n2).

Per un singolo elemento nell'array O (n2) sono richieste operazioni. Quindi, per gli elementi è O (n3).


5
Cosa aggiunge questo alle altre risposte? È stata già fornita una spiegazione di ciò che l'algoritmo fa e il tuo ragionamento per il runtime è al massimo impreciso. (Il caso peggiore non si comporta in modo lineare!)
Raffaello

Ottima spiegazione Ciò fornisce una prospettiva diversa, più intuitiva sul problema, non spiegata in altre risposte. (Per non parlare molto breve e facile da capire.)
2501

1
@ 2501 No, è sbagliato. Prova a usare questa "intuizione" sull'algoritmo di Dijkstra e otterrai runtime quadratico (nel numero di nodi), che è sbagliato.
Raffaello

@Raphael No, è vero, come spiegato nella risposta. Questa spiegazione funziona per questo algoritmo, non per altri. Sebbene possa essere sbagliato per loro, questa affermazione non dimostra che sia sbagliata per questo.
2501

@Raphael Non ho capito la spiegazione nella risposta accettata. Quindi, ho risolto questo e ho cercato di spiegarlo in termini semplici senza termini tecnici .. quindi, questo è per i membri come me che non sono stati in grado di capire la risposta accettata .. Sono contento che qualcuno lo trovi utile.
mk ..
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.