web-dev-qa-db-fra.com

Maintenir un tableau trié dans O (1)?

Nous avons un tableau trié et nous aimerions augmenter la valeur d'un index de 1 unité seulement (tableau [i] ++), de sorte que le tableau résultant soit toujours trié. Est-ce possible dans O (1)? Il est bon d’utiliser toute structure de données possible en LIST et C++.

Dans un cas plus spécifique, si le tableau est initialisé avec toutes les valeurs 0 et qu'il est toujours construit de manière incrémentielle uniquement en augmentant la valeur d'un index de un, existe-t-il une solution O(1)?

28
Farshid

Je n'ai pas encore complètement résolu le problème, mais je pense que l'idée générale pourrait aider au moins pour les nombres entiers. Au prix de davantage de mémoire, vous pouvez conserver une structure de données distincte qui conserve l'index de fin d'une série de valeurs répétées (puisque vous souhaitez échanger votre valeur incrémentée avec l'index de fin de la valeur répétée). En effet, c’est avec des valeurs répétées que vous rencontrez le cas le plus défavorable du runtime O(n): supposons que vous avez [0, 0, 0, 0] et que vous incrémentez la valeur à l’emplacement 0. Ensuite, O(n) recherche le dernier emplacement (3).

Mais disons que vous maintenez la structure de données que j'ai mentionnée (une carte fonctionnerait car elle a la fonction O(1) lookup). Dans ce cas, vous auriez quelque chose comme ceci:

0 -> 3

Vous avez donc une série de valeurs 0 qui se terminent à l'emplacement 3. Lorsque vous incrémentez une valeur, par exemple à l'emplacement i, vous vérifiez si la nouvelle valeur est supérieure à celle de i + 1. Si ce n'est pas le cas, vous allez bien. Mais si c'est le cas, vous regardez s'il existe une entrée pour cette valeur dans la structure de données secondaire. S'il n'y en a pas, vous pouvez simplement échanger. Si est une entrée, vous recherchez l'index de fin puis permutez avec la valeur à cet emplacement. Vous apportez ensuite les modifications nécessaires à la structure de données secondaire pour refléter le nouvel état du tableau.

Un exemple plus complet:

[0, 2, 3, 3, 3, 4, 4, 5, 5, 5, 7]

La structure de données secondaire est:

3 -> 4
4 -> 6
5 -> 9

Supposons que vous incrémentez la valeur à l'emplacement 2. Donc, vous avez incrémenté 3, à 4. Le tableau ressemble maintenant à ceci:

[0, 2, 4, 3, 3, 4, 4, 5, 5, 5, 7]

Vous regardez l'élément suivant, qui est 3. Vous recherchez ensuite l'entrée de cet élément dans la structure de données secondaire. L'entrée est 4, ce qui signifie qu'il y a une exécution de 3 qui se termine par 4. Cela signifie que vous pouvez échanger la valeur de l'emplacement actuel avec la valeur à l'index 4:

[0, 2, 3, 3, 4, 4, 4, 5, 5, 5, 7]

Maintenant, vous devrez également mettre à jour la structure de données secondaire. Plus précisément, la séquence de 3 termine un index à l’avance, vous devez donc décrémenter cette valeur:

3 -> 3
4 -> 6
5 -> 9

Une autre vérification consiste à vérifier si la valeur est répétée. Vous pouvez vérifier cela en consultant les emplacements i - 1th et i + 1th pour voir s'ils sont identiques à la valeur en question. Si aucune n'est égale, vous pouvez supprimer l'entrée de cette valeur de la carte.

Encore une fois, ceci est juste une idée générale. Je vais devoir le coder pour voir si cela fonctionne comme je le pensais. 

S'il vous plaît, n'hésitez pas à faire des trous.

UPDATE

J'ai une implémentation de cet algorithme ici en JavaScript. J'ai utilisé JavaScript pour pouvoir le faire rapidement. De plus, comme je l'ai codé assez rapidement, il peut probablement être nettoyé. J'ai des commentaires cependant. Je ne fais rien d'ésotérique non plus, donc ça devrait être facilement portable en C++.

L'algorithme comporte essentiellement deux parties: l'incrémentation et la permutation (si nécessaire) et la comptabilité effectuée sur la carte qui garde la trace de nos indices de fin pour les exécutions de valeurs répétées.

Le code contient un faisceau de test qui commence par un tableau de zéros et incrémente des emplacements aléatoires. À la fin de chaque itération, un test est effectué pour vérifier que le tableau est trié.

var array = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
var endingIndices = {0: 9};

var increments = 10000;

for(var i = 0; i < increments; i++) {
    var index = Math.floor(Math.random() * array.length);    

    var oldValue = array[index];
    var newValue = ++array[index];

    if(index == (array.length - 1)) {
        //Incremented element is the last element.
        //We don't need to swap, but we need to see if we modified a run (if one exists)
        if(endingIndices[oldValue]) {
            endingIndices[oldValue]--;
        }
    } else if(index >= 0) {
        //Incremented element is not the last element; it is in the middle of
        //the array, possibly even the first element

        var nextIndexValue = array[index + 1];
        if(newValue === nextIndexValue) {
            //If the new value is the same as the next value, we don't need to swap anything. But
            //we are doing some book-keeping later with the endingIndices map. That code requires
            //the ending index (i.e., where we moved the incremented value to). Since we didn't
            //move it anywhere, the endingIndex is simply the index of the incremented element.
            endingIndex = index;
        } else if(newValue > nextIndexValue) {
            //If the new value is greater than the next value, we will have to swap it

            var swapIndex = -1;
            if(!endingIndices[nextIndexValue]) {
                //If the next value doesn't have a run, then location we have to swap with
                //is just the next index
                swapIndex = index + 1;
            } else {
                //If the next value has a run, we get the swap index from the map
                swapIndex = endingIndices[nextIndexValue];
            }

            array[index] = nextIndexValue;
            array[swapIndex] = newValue;

            endingIndex = swapIndex;

        } else {
        //If the next value is already greater, there is nothing we need to swap but we do
        //need to do some book-keeping with the endingIndices map later, because it is
        //possible that we modified a run (the value might be the same as the value that
        //came before it). Since we don't have anything to swap, the endingIndex is 
        //effectively the index that we are incrementing.
            endingIndex = index;
        }

        //Moving the new value to its new position may have created a new run, so we need to
        //check for that. This will only happen if the new position is not at the end of
        //the array, and the new value does not have an entry in the map, and the value
        //at the position after the new position is the same as the new value
        if(endingIndex < (array.length - 1) &&
           !endingIndices[newValue] &&
           array[endingIndex + 1] == newValue) {
            endingIndices[newValue] = endingIndex + 1;
        }

        //We also need to check to see if the old value had an entry in the
        //map because now that run has been shortened by one.
        if(endingIndices[oldValue]) {
            var newEndingIndex = --endingIndices[oldValue];

            if(newEndingIndex == 0 ||
               (newEndingIndex > 0 && array[newEndingIndex - 1] != oldValue)) {
                //In this case we check to see if the old value only has one entry, in
                //which case there is no run of values and so we will need to remove
                //its entry from the map. This happens when the new ending-index for this
                //value is the first location (0) or if the location before the new
                //ending-index doesn't contain the old value.
                delete endingIndices[oldValue];
            }
        }
    }

    //Make sure that the array is sorted   
    for(var j = 0; j < array.length - 1; j++) {
        if(array[j] > array[j + 1]) {        
            throw "Array not sorted; Value at location " + j + "(" + array[j] + ") is greater than value at location " + (j + 1) + "(" + array[j + 1] + ")";
        }
    }
}
22
Vivin Paliath

Dans un cas plus spécifique, si le tableau est initialisé avec toutes les valeurs 0 et qu'il est toujours construit de manière incrémentielle uniquement en augmentant la valeur d'un index de un, existe-t-il une solution O(1)?

N ° Étant donné un tableau de tous les 0: [0, 0, 0, 0, 0]. Si vous incrémentez la première valeur en donnant [1, 0, 0, 0, 0], vous devrez alors effectuer 4 échanges pour vous assurer qu’elle reste triée.

Étant donné un tableau trié sans doublons, la réponse est oui. Mais après la première opération (c’est-à-dire la première fois que vous incrémentez), vous pouvez potentiellement avoir des doublons. Plus vous augmentez d'incréments, plus il y a de chances que vous ayez des doublons et plus il sera probable que vous aurez besoin de O(n) pour garder ce tableau trié.

Si tout ce que vous avez est le tableau, il est impossible de garantir moins de O(n) temps par incrément. Si vous recherchez une structure de données prenant en charge l'ordre trié et la recherche indexée, vous souhaiterez probablement un arbre stastique order .

10
Jim Mischel

Si les valeurs sont petites, le tri par comptage fonctionnera. Représentez le tableau [0,0,0,0] sous la forme {4}. Incrémenter n'importe quel zéro donne {3,1}: 3 zéros et un un. En général, pour incrémenter une valeur x, déduisez un du nombre de x et incrémentez le nombre de {x + 1}. L'efficacité spatiale est cependant O (N), N étant la valeur la plus élevée.

3
MSalters

Donc, vous prenez un tableau trié et hashtable. Vous allez sur tableau pour déterminer les zones «plates» - où les éléments ont la même valeur. Pour chaque surface plane, vous devez déterminer trois choses 1) où elle commence (index du premier élément) 2) quelle est sa valeur 3) quelle est la valeur de l'élément suivant (le prochain élément le plus grand). Ensuite, mettez ce tuple dans la table de hachage, où la clé sera la valeur de l'élément. C'est une condition préalable et sa complexité n'a pas vraiment d'importance.

Ensuite, lorsque vous augmentez un élément (index i), vous recherchez une table pour l'index du prochain élément le plus grand (appelez-le j) et permutez i avec i - 1. Puis 1) ajoutez une nouvelle entrée à la table de hachage 2) mettez à jour une entrée existante pour sa valeur précédente.

Avec une table de hachage parfaite (ou une plage limitée de valeurs possibles), il sera presque égal à O (1). L'inconvénient: ce ne sera pas stable.

Voici du code:

#include <iostream>
#include <unordered_map>
#include <vector>

struct Range {
    int start, value, next;
};

void print_ht(std::unordered_map<int, Range>& ht)
{
    for (auto i = ht.begin(); i != ht.end(); i++) {
        Range& r = (*i).second;
        std::cout << '(' << r.start << ", "<< r.value << ", "<< r.next << ") ";
    }
    std::cout << std::endl;
}

void increment_el(int i, std::vector<int>& array, std::unordered_map<int, Range>& ht)
{
    int val = array[i];
    array[i]++;
    //Pick next bigger element
    Range& r = ht[val];
    //Do the swapping, so last element of that range will be first
    std::swap(array[i], array[ht[r.next].start - 1]);
    //Update hashtable
    ht[r.next].start--;
}

int main(int argc, const char * argv[])
{
    std::vector<int> array = {1, 1, 1, 2, 2, 3};
    std::unordered_map<int, Range> ht;

    int start = 0;
    int value = array[0];

    //Build indexing hashtable
    for (int i = 0; i <= array.size(); i++) {
        int cur_value = i < array.size() ? array[i] : -1;
        if (cur_value > value || i == array.size()) {
            ht[value] = {start, value, cur_value};
            start = i;
            value = cur_value;
        }
    }

    print_ht(ht);

    //Now let's increment first element
    increment_el(0, array, ht);
    print_ht(ht);
    increment_el(3, array, ht);
    print_ht(ht);

    for (auto i = array.begin(); i != array.end(); i++)
        std::cout << *i << " ";


    return 0;
}
1
Andrey

Cela dépend du nombre d'éléments pouvant avoir la même valeur. Si plusieurs éléments peuvent avoir la même valeur, il n'est pas possible d'avoir O(1) avec des tableaux ordinaires.

Faisons un exemple: supposons array [5] = 21, et vous voulez faire array [5] ++:

  • Incrémenter l'article:

    array[5]++
    

    (qui est O(1) parce que c'est un tableau).

    Donc, maintenant tableau [5] = 22.

  • Vérifiez l'élément suivant (c'est-à-dire le tableau [6]):

    Si array [6] == 21, vous devez continuer à vérifier les nouveaux éléments (ie array [7] et ainsi de suite) jusqu'à ce que vous trouviez une valeur supérieure à 21 . À ce stade, vous pouvez échanger les valeurs. Cette recherche est pas O(1) car vous devez potentiellement analyser tout le tableau.

Au lieu de cela, si les éléments ne peuvent pas avoir la même valeur, alors vous avez:

  • Incrémenter l'article:

    array[5]++
    

    (qui est O(1) parce que c'est un tableau).

    Donc, maintenant tableau [5] = 22.

  • L'élément suivant ne peut pas être 21 (car deux éléments ne peuvent pas avoir la même valeur), il doit donc avoir une valeur> 21 et le tableau est déjà trié.

1
Claudio

Je pense que c'est possible sans utiliser de hashtable. J'ai une implémentation ici:

#include <cstdio>
#include <vector>
#include <cassert>

// This code is a solution for http://stackoverflow.com/questions/19957753/maintain-a-sorted-array-in-o1
//
// """We have a sorted array and we would like to increase the value of one index by only 1 unit
//    (array[i]++), such that the resulting array is still sorted. Is this possible in O(1)?"""


// The obvious implementation, which has O(n) worst case increment.
class LinearIncrementor
{
public:
    LinearIncrementor(int numElems);
    int valueAt(int index) const;
    void incrementAt(int index);
private:
    std::vector<int> m_values;
};

// Free list to store runs of same values
class RunList
{
public:
    struct Run
    {
        int m_end;   // end index of run, inclusive, or next object in free list
        int m_value; // value at this run
    };

    RunList();
    int allocateRun(int endIndex, int value);
    void freeRun(int index);
    Run& runAt(int index);
    const Run& runAt(int index) const;
private:
    std::vector<Run> m_runs;
    int m_firstFree;
};

// More optimal implementation, which increments in O(1) time
class ConstantIncrementor
{
public:
    ConstantIncrementor(int numElems);
    int valueAt(int index) const;
    void incrementAt(int index);
private:
    std::vector<int> m_runIndices;
    RunList m_runs;
};

LinearIncrementor::LinearIncrementor(int numElems)
    : m_values(numElems, 0)
{
}

int LinearIncrementor::valueAt(int index) const
{
    return m_values[index];
}

void LinearIncrementor::incrementAt(int index)
{
    const int n = static_cast<int>(m_values.size());
    const int value = m_values[index];
    while (index+1 < n && value == m_values[index+1])
        ++index;
    ++m_values[index];
}

RunList::RunList() : m_firstFree(-1)
{
}

int RunList::allocateRun(int endIndex, int value)
{
    int runIndex = -1;
    if (m_firstFree == -1)
    {
        runIndex = static_cast<int>(m_runs.size());
        m_runs.resize(runIndex + 1);
    }
    else
    {
        runIndex = m_firstFree;
        m_firstFree = m_runs[runIndex].m_end;
    }
    Run& run = m_runs[runIndex];
    run.m_end = endIndex;
    run.m_value = value;
    return runIndex;
}

void RunList::freeRun(int index)
{
    m_runs[index].m_end = m_firstFree;
    m_firstFree = index;
}

RunList::Run& RunList::runAt(int index)
{
    return m_runs[index];
}

const RunList::Run& RunList::runAt(int index) const
{
    return m_runs[index];
}

ConstantIncrementor::ConstantIncrementor(int numElems) : m_runIndices(numElems, 0)
{
    const int runIndex = m_runs.allocateRun(numElems-1, 0);
    assert(runIndex == 0);
}

int ConstantIncrementor::valueAt(int index) const
{
    return m_runs.runAt(m_runIndices[index]).m_value;
}

void ConstantIncrementor::incrementAt(int index)
{
    const int numElems = static_cast<int>(m_runIndices.size());

    const int curRunIndex = m_runIndices[index];
    RunList::Run& curRun = m_runs.runAt(curRunIndex);
    index = curRun.m_end;
    const bool freeCurRun = index == 0 || m_runIndices[index-1] != curRunIndex;

    RunList::Run* runToMerge = NULL;
    int runToMergeIndex = -1;
    if (curRun.m_end+1 < numElems)
    {
        const int nextRunIndex = m_runIndices[curRun.m_end+1];
        RunList::Run& nextRun = m_runs.runAt(nextRunIndex);
        if (curRun.m_value+1 == nextRun.m_value)
        {
            runToMerge = &nextRun;
            runToMergeIndex = nextRunIndex;
        }
    }

    if (freeCurRun && !runToMerge) // then free and allocate at the same time
    {
        ++curRun.m_value;
    }
    else
    {
        if (freeCurRun)
        {
            m_runs.freeRun(curRunIndex);
        }
        else
        {
            --curRun.m_end;
        }

        if (runToMerge)
        {
            m_runIndices[index] = runToMergeIndex;
        }
        else
        {
            m_runIndices[index] = m_runs.allocateRun(index, curRun.m_value+1);
        }
    }
}

int main(int argc, char* argv[])
{
    const int numElems = 100;
    const int numInc = 1000000;

    LinearIncrementor linearInc(numElems);
    ConstantIncrementor constInc(numElems);
    srand(1);
    for (int i = 0; i < numInc; ++i)
    {
        const int index = Rand() % numElems;
        linearInc.incrementAt(index);
        constInc.incrementAt(index);
        for (int j = 0; j < numElems; ++j)
        {
            if (linearInc.valueAt(j) != constInc.valueAt(j))
            {
                printf("Error: differing values at increment step %d, value at index %d\n", i, j);
            }
        }
    }
    return 0;
}
0
myavuzselim

Oui et non.

Oui si la liste ne contient que des entiers uniques, cela signifie que vous ne devez vérifier que la valeur suivante. Non dans aucune autre situation. Si les valeurs ne sont pas uniques, incrémenter la première des N valeurs en double signifie qu'il doit se déplacer de N positions. Si les valeurs sont à virgule flottante, vous pouvez avoir des milliers de valeurs comprises entre x et x + 1

0
MSalters

Il est important d'être très clair sur les exigences. le moyen le plus simple consiste à exprimer le problème sous forme de type ADT (type de données abstrait), en répertoriant les opérations et les complexités requises.

Voici ce que je pense que vous recherchez: un type de données fournissant les opérations suivantes:

  • Construct(n): Créez un nouvel objet de taille n dont toutes les valeurs sont 0.

  • Value(i): Retourne la valeur à l'index i.

  • Increment(i): incrémente la valeur dans index i.

  • Least(): Retourne l'index de l'élément avec la plus petite valeur (ou un tel élément s'il y en a plusieurs).

  • Next(i): Renvoie l'index du prochain élément après l'élément i dans une traversée triée commençant à Least(), de sorte que la traversée renvoie tous les éléments.

À part le constructeur, nous voulons que chacune des opérations ci-dessus ait une complexité O(1). Nous voulons également que l'objet occupe O(n) space.

L'implémentation utilise une liste de compartiments; chaque compartiment a une value et une liste d'éléments. Chaque élément a un index, un pointeur sur le compartiment dont il fait partie. Enfin, nous avons un tableau de pointeurs sur les éléments. (En C++, j'utiliserais probablement des itérateurs plutôt que des pointeurs; dans un autre langage, j'utiliserais probablement des listes intrusives.) Les invariants sont qu'aucun seau n'est jamais vide, et la value des seaux augmente strictement de façon monotone.

Nous commençons avec un seul compartiment avec la valeur 0, qui contient une liste d'éléments n.

Value(i) est implémenté en renvoyant la valeur du compartiment de l'élément référencé par l'itérateur à l'élément i du tableau. Least() est l'index du premier élément du premier compartiment. Next(i) est l'index du prochain élément après celui référencé par l'itérateur à l'élément i, à moins que cet itérateur pointe déjà à la fin de la liste, auquel cas il s'agit du premier élément du compartiment suivant, à moins que le compartiment de l'élément ne soit le dernier compartiment, auquel cas nous sommes à la fin de la liste des éléments.

La seule interface intéressante est Increment(i), qui est la suivante:

  • Si l'élément i est le seul élément de son compartiment (c'est-à-dire qu'il n'y a pas d'élément suivant dans la liste de compartiments et que l'élément i est le premier élément de la liste de compartiments):

    • Incrémente la valeur du compartiment associé.
    • Si le compartiment suivant a la même valeur, ajoutez la liste d'éléments du compartiment suivant à la liste d'éléments de ce compartiment (il s'agit de O(1), quelle que soit la taille de la liste, car il ne s'agit que d'un échange de pointeur), puis supprimez le compartiment suivant.
  • Si l'élément i n'est pas le seul élément de son compartiment, alors:

    • Supprimez-le de sa liste de seau.
    • Si le compartiment suivant a la valeur séquentielle suivante, ajoutez l'élément i à la liste du compartiment suivant.
    • Sinon, la valeur du compartiment suivant est plus grande, puis créez un nouveau compartiment avec la valeur séquentielle suivante et uniquement l'élément i et insérez-le entre ce compartiment et le suivant.
0
rici

il suffit de parcourir le tableau à partir de l'élément modifié jusqu'à ce que vous trouviez le bon emplacement, puis permutez. La complexité moyenne des affaires est de O(N), où N est le nombre moyen de doublons. Le cas le plus défavorable est O(n) où n est la longueur du tableau. Tant que N n’est pas grand et que l’échelle ne va pas mal avec n, tout va bien et vous pouvez probablement prétendre que c’est O(1) à des fins pratiques.

Si les doublons sont la norme et/ou s’agrandissent fortement avec n, il existe de meilleures solutions, voir autres réponses.

0
John_C