web-dev-qa-db-fra.com

Pourquoi le runtime de l'algorithme de sélection est-il O (n)?

Selon Wikipedia , les algorithmes de sélection basés sur des partitions tels que quickselect ont une exécution de O(n), mais je n'en suis pas convaincu. Quelqu'un peut-il expliquer pourquoi c'est O(n)?

Dans le tri rapide normal, le runtime est O(n log n). Chaque fois que nous partitionnons la branche en deux branches (plus grande que le pivot et plus petite que le pivot), nous devons continuer le processus dans les deux branches, alors que quickselect n'a besoin que de traiter une branche. Je comprends parfaitement ces points. Cependant, si vous pensez dans l'algorithme de recherche binaire, après avoir choisi l'élément du milieu, nous recherchons également uniquement un côté de la branche. Cela fait-il donc de l'algorithme O(1)? Non, bien sûr, l'algorithme de recherche binaire est toujours O(log N) au lieu de O(1). C'est également la même chose que l'élément de recherche dans un arbre de recherche binaire. Nous cherchons seulement un côté, mais nous considérons toujours O(log n) au lieu de O(1).

Quelqu'un peut-il expliquer pourquoi dans quickselect, si nous continuons la recherche dans one côté du pivot, il est considéré O(1) au lieu de O(log n)? Je considère que l'algorithme est O(n log n), O(N) pour le partitionnement, et O(log n) pour le nombre de fois pour continuer à trouver.

32
user926958

Il existe plusieurs algorithmes de sélection différents, de la sélection rapide beaucoup plus simple (attendu O (n), le pire des cas O (n2)) à l'algorithme plus complexe de la médiane des médianes (Θ (n)). Ces deux algorithmes fonctionnent en utilisant une étape de partitionnement rapide (time O(n)) pour réorganiser les éléments et positionner un élément dans sa bonne position. Si cet élément est à l'index en question, nous avons terminé et nous pouvons simplement renvoyer cet élément, sinon nous déterminons de quel côté récurer et y revenir.

Faisons maintenant une hypothèse très forte - supposons que nous utilisons quickselect (choisissez le pivot au hasard) et à chaque itération, nous parvenons à deviner le milieu exact du tableau. Dans ce cas, notre algorithme fonctionnera comme ceci: nous faisons une étape de partition, jetons la moitié du tableau, puis traitons récursivement la moitié du tableau. Cela signifie qu'à chaque appel récursif, nous finissons par effectuer un travail proportionnel à la longueur du tableau à ce niveau, mais que cette longueur continue de diminuer d'un facteur deux à chaque itération. Si nous calculons le calcul (en ignorant les facteurs constants, etc.), nous obtenons le temps suivant:

  • Travaillez au premier niveau: n
  • Travailler après un appel récursif: n/2
  • Travail après deux appels récursifs: n/4
  • Travail après trois appels récursifs: n/8
  • ...

Cela signifie que le travail total effectué est donné par

n + n/2 + n/4 + n/8 + n/16 + ... = n (1 + 1/2 + 1/4 + 1/8 + ...)

Notez que ce dernier terme est n fois la somme de 1, 1/2, 1/4, 1/8, etc. Si vous calculez cette somme infinie, malgré le fait qu'il existe une infinité de termes, la somme totale est exactement 2. Cela signifie que le travail total est

n + n/2 + n/4 + n/8 + n/16 + ... = n (1 + 1/2 + 1/4 + 1/8 + ...) = 2n

Cela peut sembler étrange, mais l'idée est que si nous faisons un travail linéaire à chaque niveau mais continuons à couper le tableau en deux, nous finissons par ne faire qu'environ 2n travail.

Un détail important ici est qu'il y a effectivement O (log n) différentes itérations ici, mais toutes ne font pas le même travail. En effet, chaque itération fait deux fois moins de travail que l'itération précédente. Si nous ignorons le fait que le travail diminue, vous pouvez conclure que le travail est O (n log n), ce qui est correct mais pas une limite étroite. Cette analyse plus précise, qui utilise le fait que le travail effectué ne cesse de diminuer à chaque itération, donne le runtime O(n).

Bien sûr, c'est une hypothèse très optimiste - nous n'obtenons presque jamais un partage 50/50! - mais en utilisant une version plus puissante de cette analyse, vous pouvez dire que si vous pouvez garantir n'importe quel facteur de division constant, le travail total effectué n'est qu'une constante multiple de n. Si nous choisissons un élément totalement aléatoire à chaque itération (comme nous le faisons dans quickselect), alors nous avons seulement besoin de choisir deux éléments avant de finir par choisir un élément pivot au milieu de 50% du tableau, ce qui signifie que, sur attente, seulement deux tours de sélection d'un pivot sont nécessaires avant de finir par choisir quelque chose qui donne un partage 25/75. C'est de là que vient le temps d'exécution attendu de O(n) pour quickselect.

Une analyse formelle de l'algorithme de la médiane des médianes est beaucoup plus difficile car la récurrence est difficile et pas facile à analyser. Intuitivement, l'algorithme fonctionne en effectuant une petite quantité de travail pour garantir qu'un bon pivot est choisi. Cependant, comme il y a deux appels récursifs différents, une analyse comme celle ci-dessus ne fonctionnera pas correctement. Vous pouvez soit utiliser un résultat avancé appelé théorème d'Akra-Bazzi , soit utiliser la définition formelle de big-O pour prouver explicitement que le runtime est O (n). Pour une analyse plus détaillée, consultez "Introduction aux algorithmes, troisième édition" de Cormen, Leisserson, Rivest et Stein.

J'espère que cela t'aides!

67
templatetypedef

Laissez-moi essayer d'expliquer la différence entre la sélection et la recherche binaire.

L'algorithme de recherche binaire à chaque étape fait O(1) opérations. Totalement, il y a des étapes de log (N) et cela le rend O (log (N))

L'algorithme de sélection à chaque étape effectue O(n) opérations. Mais ce 'n' continue de diminuer de moitié à chaque fois. Il y a totalement log (N) étapes. Cela fait N + N/2 + N/4 + ... + 1 (log (N) fois) = 2N = O (N)

Pour la recherche binaire, c'est 1 + 1 + ... (log (N) fois) = O (logN)

11
Rajendran T

Dans Quicksort, l'arborescence de récursivité a une profondeur de lg (N) niveaux et chacun de ces niveaux nécessite O(N) quantité de travail. Donc, la durée totale d'exécution est O (NlgN)).

Dans Quickselect, l'arbre de récurrence a une profondeur de lg (N) niveaux et chaque niveau ne nécessite que la moitié du travail du niveau supérieur. Cela produit ce qui suit:

N * (1/1 + 1/2 + 1/4 + 1/8 + ...)

ou

N * Summation(1/i^2)
    1 < i <= lgN

La chose importante à noter ici est que je passe de 1 à lgN, mais pas de 1 à N et pas non plus de 1 à l'infini.

La sommation vaut 2. Par conséquent Quickselect = O (2N).

2
amrish

Quicksort n'a pas de gros O de nlogn - son exécution dans le pire des cas est n ^ 2.

Je suppose que vous posez des questions sur l'algorithme de sélection de Hoare (ou quickselect) et non sur l'algorithme de sélection naïf qui est O (kn). Comme quicksort, quickselect a un temps d'exécution dans le pire des cas de O (n ^ 2) (si de mauvais pivots sont choisis), pas O (n). Il peut s'exécuter dans le temps d'attente n car il ne trie qu'un seul côté, comme vous le faites remarquer.

0
Kane

Trier = réorganiser les éléments est O (n log n), mais la sélection est quelque chose comme prendre le ième élément = indexation. Et pour cela à la fois dans une liste chaînée, ou dans un arbre binaire, c'est O (n).

0
Joop Eggen

Parce que pour la sélection, vous ne triez pas forcément. Vous pouvez simplement compter le nombre d'éléments qui ont une valeur donnée. Ainsi, une médiane O(n) peut être effectuée en comptant le nombre de fois où chaque valeur apparaît, et en choisissant la valeur qui a 50% d'éléments au-dessus et en dessous. C'est un passage dans le tableau. , incrémentant simplement un compteur pour chaque élément du tableau, donc c'est O (n).

Par exemple, si vous avez un tableau "a" de nombres 8 bits, vous pouvez effectuer les opérations suivantes:

int histogram [ 256 ];
for (i = 0; i < 256; i++)
{
    histogram [ i ] = 0;
}
for (i = 0; i < numItems; i++)
{
    histogram [ a [ i ] ]++;
}
i = 0;
sum = 0;
while (sum < (numItems / 2))
{
    sum += histogram [ i ];
    i++;
}

A la fin, la variable "i" contiendra la valeur 8 bits de la médiane. Il s'agissait d'environ 1,5 passage à travers le tableau "a". Une fois dans tout le tableau pour compter les valeurs, et à nouveau à moitié pour obtenir la valeur finale.

0
user1118321