Zamień dwa elementy w List <T>

82

Czy istnieje sposób LINQ do zamiany pozycji dwóch elementów w list<T>?

Tony Lew
źródło
57
Dlaczego ma znaczenie, dlaczego chce to zrobić. Osoby, które szukają w Google „elementów listy wymiany c #”, będą chcieli prostej odpowiedzi na to konkretne pytanie.
Daniel Macias
10
@DanielMacias To jest takie prawdziwe. Te odpowiedzi są w stylu „ale dlaczego to robisz?” są takie irytujące. Myślę, że przed podjęciem próby argumentowania dlaczego należy udzielić przynajmniej realnej odpowiedzi.
julealgon
dlaczego chcesz użyć LINQ, aby to zrobić? jeśli LINQ jest specyficzny, dlaczego nie zmienić tytułu, aby dodać LINQ
ina

Odpowiedzi:

119

Sprawdź odpowiedź Marca z C #: Dobra / najlepsza implementacja metody Swap .

public static void Swap<T>(IList<T> list, int indexA, int indexB)
{
    T tmp = list[indexA];
    list[indexA] = list[indexB];
    list[indexB] = tmp;
}

który może być podobny do linq-i-fied

public static IList<T> Swap<T>(this IList<T> list, int indexA, int indexB)
{
    T tmp = list[indexA];
    list[indexA] = list[indexB];
    list[indexB] = tmp;
    return list;
}

var lst = new List<int>() { 8, 3, 2, 4 };
lst = lst.Swap(1, 2);
Jan Jongboom
źródło
10
Dlaczego ta metoda rozszerzenia musi zwracać listę? Modyfikujesz listę lokalnie.
vargonian,
13
Następnie możesz połączyć metody.
Jan Jongboom
nie zapomnij upublicznić metody rozszerzenia, jeśli chcesz jej używać w Unity3D! (Jedność nie może go znaleźć, jeśli tego nie zrobisz)
col000r
Miło i łatwo. Dzięki
fnc12
5
Ostrzeżenie: wersja „LINQified” nadal zmienia oryginalną listę.
Philipp Michalski
32

Może ktoś wymyśli sprytny sposób na zrobienie tego, ale nie powinieneś. Zamiana dwóch elementów na liście jest z natury obciążona efektem ubocznym, ale operacje LINQ powinny być wolne od skutków ubocznych. Dlatego po prostu użyj prostej metody rozszerzenia:

static class IListExtensions {
    public static void Swap<T>(
        this IList<T> list,
        int firstIndex,
        int secondIndex
    ) {
        Contract.Requires(list != null);
        Contract.Requires(firstIndex >= 0 && firstIndex < list.Count);
        Contract.Requires(secondIndex >= 0 && secondIndex < list.Count);
        if (firstIndex == secondIndex) {
            return;
        }
        T temp = list[firstIndex];
        list[firstIndex] = list[secondIndex];
        list[secondIndex] = temp;
    }
}
Jason
źródło
Skutki uboczne? Czy możesz rozwinąć?
Tony The Lion,
12
Mówiąc o efektach ubocznych ma na myśli, że zmieniają listę i prawdopodobnie pozycje na liście - w przeciwieństwie do przeszukiwania danych, które niczego nie modyfikują
saret
robi "T temp = list [firstIndex];" utworzyć głęboką kopię obiektu na liście?
Paul McCarthy
1
@PaulMcCarthy Nie, tworzy nowy wskaźnik do oryginalnego obiektu, bez kopiowania.
NetMage
12

List<T>ma Reverse()metodę, jednak odwraca tylko kolejność dwóch (lub więcej) kolejnych pozycji.

your_list.Reverse(index, 2);

Tam, gdzie 2wskazuje drugi parametr , odwracamy kolejność 2 pozycji, zaczynając od pozycji podanej index.

Źródło: https://msdn.microsoft.com/en-us/library/hf2ay11y(v=vs.110).aspx

user1920925
źródło
1
Chociaż Google przywiózł mnie tutaj, tego właśnie szukałem.
Jnr
1
Funkcja .Reverse () nie jest Linq i nie nazywa się Swap, ale wydaje się, że celem pytania jest znalezienie „wbudowanej” funkcji, która wykonuje zamianę. To jedyna odpowiedź, która to zapewnia.
Americus
Ale przydatne tylko do zamiany sąsiednich elementów, więc nie jest to odpowiedź.
NetMage
10

Nie ma istniejącej metody Swap, więc musisz ją stworzyć samodzielnie. Oczywiście możesz to zrobić linqify, ale należy to zrobić mając na uwadze jedną (niepisaną?) Regułę: operacje LINQ nie zmieniają parametrów wejściowych!

W innych odpowiedziach "linqify" lista (wejściowa) jest modyfikowana i zwracana, ale to działanie łamie tę regułę. Jeśli byłoby dziwne, gdybyś miał listę z nieposortowanymi pozycjami, wykonaj operację LINQ „OrderBy” i odkryj, że lista wejściowa jest również posortowana (tak jak wynik). To nie może się zdarzyć!

Więc jak to robimy?

Moją pierwszą myślą było przywrócenie kolekcji po zakończeniu iteracji. Ale to jest brudne rozwiązanie, więc nie używaj go:

static public IEnumerable<T> Swap1<T>(this IList<T> source, int index1, int index2)
{
    // Parameter checking is skipped in this example.

    // Swap the items.
    T temp = source[index1];
    source[index1] = source[index2];
    source[index2] = temp;

    // Return the items in the new order.
    foreach (T item in source)
        yield return item;

    // Restore the collection.
    source[index2] = source[index1];
    source[index1] = temp;
}

To rozwiązanie jest brudny, ponieważ robi zmodyfikować listę wejściowego, nawet jeśli przywraca go do stanu pierwotnego. Może to spowodować kilka problemów:

  1. Lista może być tylko do odczytu, co spowoduje zgłoszenie wyjątku.
  2. Jeśli lista jest współdzielona przez wiele wątków, lista zmieni się dla innych wątków w czasie trwania tej funkcji.
  3. Jeśli podczas iteracji wystąpi wyjątek, lista nie zostanie przywrócona. (Można to rozwiązać, aby napisać próbę w końcu wewnątrz funkcji Swap i umieścić kod przywracania wewnątrz bloku last).

Jest lepsze (i krótsze) rozwiązanie: po prostu zrób kopię oryginalnej listy. (Umożliwia to również użycie IEnumerable jako parametru zamiast IList):

static public IEnumerable<T> Swap2<T>(this IList<T> source, int index1, int index2)
{
    // Parameter checking is skipped in this example.

    // If nothing needs to be swapped, just return the original collection.
    if (index1 == index2)
        return source;

    // Make a copy.
    List<T> copy = source.ToList();

    // Swap the items.
    T temp = copy[index1];
    copy[index1] = copy[index2];
    copy[index2] = temp;

    // Return the copy with the swapped items.
    return copy;
}

Wadą tego rozwiązania jest to, że kopiuje całą listę, co zużywa pamięć, co powoduje, że rozwiązanie jest raczej powolne.

Możesz rozważyć następujące rozwiązanie:

static public IEnumerable<T> Swap3<T>(this IList<T> source, int index1, int index2)
{
    // Parameter checking is skipped in this example.
    // It is assumed that index1 < index2. Otherwise a check should be build in and both indexes should be swapped.

    using (IEnumerator<T> e = source.GetEnumerator())
    {
        // Iterate to the first index.
        for (int i = 0; i < index1; i++)
            yield return source[i];

        // Return the item at the second index.
        yield return source[index2];

        if (index1 != index2)
        {
            // Return the items between the first and second index.
            for (int i = index1 + 1; i < index2; i++)
                yield return source[i];

            // Return the item at the first index.
            yield return source[index1];
        }

        // Return the remaining items.
        for (int i = index2 + 1; i < source.Count; i++)
            yield return source[i];
    }
}

A jeśli chcesz, aby parametr wejściowy był IEnumerable:

static public IEnumerable<T> Swap4<T>(this IEnumerable<T> source, int index1, int index2)
{
    // Parameter checking is skipped in this example.
    // It is assumed that index1 < index2. Otherwise a check should be build in and both indexes should be swapped.

    using(IEnumerator<T> e = source.GetEnumerator())
    {
        // Iterate to the first index.
        for(int i = 0; i < index1; i++) 
        {
            if (!e.MoveNext())
                yield break;
            yield return e.Current;
        }

        if (index1 != index2)
        {
            // Remember the item at the first position.
            if (!e.MoveNext())
                yield break;
            T rememberedItem = e.Current;

            // Store the items between the first and second index in a temporary list. 
            List<T> subset = new List<T>(index2 - index1 - 1);
            for (int i = index1 + 1; i < index2; i++)
            {
                if (!e.MoveNext())
                    break;
                subset.Add(e.Current);
            }

            // Return the item at the second index.
            if (e.MoveNext())
                yield return e.Current;

            // Return the items in the subset.
            foreach (T item in subset)
                yield return item;

            // Return the first (remembered) item.
            yield return rememberedItem;
        }

        // Return the remaining items in the list.
        while (e.MoveNext())
            yield return e.Current;
    }
}

Swap4 tworzy również kopię (podzbioru) źródła. Tak więc najgorszy scenariusz jest tak powolny i pochłania pamięć jak funkcja Swap2.

Martin Mulder
źródło
0

Jeśli porządek ma znaczenie, powinieneś zachować właściwość na obiektach „T” na swojej liście, która oznacza sekwencję. Aby je zamienić, po prostu zamień wartość tej właściwości, a następnie użyj jej w .Sort ( porównanie z właściwością sequence )

CaffGeek
źródło
Dzieje się tak, jeśli koncepcyjnie możesz zgodzić się, że Twoje T ma nieodłączną „kolejność”, ale nie jeśli chcesz, aby było sortowane w dowolny sposób bez nieodłącznego „porządku”, na przykład w interfejsie użytkownika.
Dave Van den Eynde
@DaveVandenEynde, jestem dość początkującym programistą, więc proszę mi wybaczyć, jeśli to nie jest świetne pytanie: jakie jest znaczenie zamiany pozycji na liście, jeśli sytuacja nie wiąże się z koncepcją porządku?
Mathieu K.
@ MathieuK.well, chciałem powiedzieć, że ta odpowiedź sugeruje, że kolejność jest wyprowadzona z właściwości obiektów, na których można posortować sekwencję . Zwykle tak nie jest.
Dave Van den Eynde
Nie rozumiem (być może źle) tej odpowiedzi, mówiąc: „Numeruj obiekty, przechowując liczby we właściwości. Następnie, aby zamienić dwa elementy, zamień ich numery. Użyj listy posortowanej według tej właściwości”.
Mathieu K.