0

I'm trying to write a method to synchronize a list with another under some constraints, and so far I haven't managed to think of a proper algorithm to do that.

Here are all the details:

  • After the method call, the target list will have the exact same elements of the source list, in the same order
  • Some elements in the target list could be removed, others could be added, others could have a different position
  • I can't sort the target list nor can I use swap operations (as the UI is shown to the user and I don't want the UI to flicker)

Edit: to add some details, the list is an ObservableCollection object, and I don't want to just assign the second list to the first one as that'd make the whole ListView control (UWP app) refresh the content. Instead, I want the "right" items to remain where they are, and I'd like the other items to nicely being added/removed at the right positions. Also, the items in the wrong positions should just "slide" in the right position where possible, for example if I'm removing an item that was before an item that was one position ahead of its right final position.

As a result, I can only do the following operations on the target list:

  • Insert an item at a given index
  • Remove an item from a given index

I need the algorithm to perform as few operations as possible on the target list.

Example:

target = { "tree", "house", "beach" }
source = { "tree", "beach", "house", "peach" }

In this case I'd remove the "house" item from the first list, so that "beach" would slide in the right position, then add "house" again (in the right position) and finally "peach" at the end of the list.

Here's the general prototype I thought about:

/// <summary>
/// Synchronizes a list with the items in the second list
/// </summary>
/// <typeparam name="T">The type of the items in the lists</typeparam>
/// <param name="target">The target list to edit</param>
/// <param name="source">The source list (ordered)</param>
/// <param name="comparer">A comparer function to check if two items are the same, and if a list contains a given item (by using the Any LINQ)</param>
/// <param name="indexer">An action used to assign the right final index to an element in the target list</param>
public static void Synchronize<T>([NotNull] IList<T> target, [NotNull] IReadOnlyList<T> source, 
    [NotNull] Func<T, T, bool> comparer, [NotNull] Action<T, int> indexer)
{
    // Edge case
    if (target.Count == 0)
    {
        foreach (T item in source)
            target.Add(item);
        return;
    }

    if (target.Count < source.Count)
    {
        /* At least a new item has been added, but there's
         * no guarantee it'll be at the end of the source list */
    }
    else if (target.Count > source.Count)
    {
        /* One or more items have been removed from the target list,
         * but at the same time the source list could have one or more new items
         * that weren't present in the target list before */
    }
    else
    {
        /* The two lists have the same length, but I can make no assumptions
         * on their content. Every item could be different, or they could have the same
         * items but in different positions, or some items right, some in the wrong 
         * positions and some new items as well */
    }
}

Note: each list will not have more than 20 items, so I don't care about the algorithm cost, it could be O(n^2) and it'd still be absolutely fine.

Note #2: the indexer function is needed as at the end of the synchronization, each item in the target list will have to know its position inside the list, so I can use that action on all the items that ended up in a different position (and on all the new items) to let them know their final position.

I need a help with the pseudocode, I didn't just started writing the code as I wanted to come up with a decent algorithm for the problem first, I've been thinking about it for a while but I'm not sure I know the right approach to solve this.

Thanks for your help!


Solution: here's the final implementation I wrote (tested, it works great)

/// <summary>
/// Synchronizes a list with the items in the second list
/// </summary>
/// <typeparam name="T">The type of the items in the lists</typeparam>
/// <param name="target">The target list to edit</param>
/// <param name="source">The source list (ordered)</param>
/// <param name="comparer">A comparer function to check if two items are the same, and if a list contains a given item (by using the Any LINQ)</param>
/// <param name="indexer">An action used to assign the right final index to an element in the target list</param>
public static void Synchronize<T>([NotNull] this IList<T> target, [NotNull] IReadOnlyList<T> source, 
    [NotNull] Func<T, T, bool> comparer, [NotNull] Action<T, int> indexer)
{
    // Edge case
    if (target.Count == 0)
    {
        foreach (T item in source)
            target.Add(item);
        return;
    }

    // Step 1
    for (int i = 0; i < target.Count; i++)
    {
        // Remove all the items in target that are not in source
        if (!source.Any(item => comparer(item, target[i])))
        {
            target.RemoveAt(i--);
        }
    }

    // Step 2
    List<T> copy = source.Where(item => target.Any(test => comparer(test, item))).ToList();
    List<(T, int, int)> lookup = new List<(T, int, int)>();
    for (int i = 0; i < target.Count; i++)
    {
        // Check if the item is out of place
        T current = target[i];
        if (!comparer(current, copy[i]))
        {
            // Store the item and its target index in the lookup table
            int index = copy.IndexOf(item => comparer(item, current));
            lookup.Add((current, i, index));
        }
    }

    // Adjust the items in the wrong positions
    lookup.Sort(tuple => -(tuple.Item3 - tuple.Item2).Abs());
    while (lookup.Count > 0)
    {
        // Move the items in the right position
        (T item, int current, int desired) = lookup.First();
        lookup.RemoveAt(0);

        // Adjust the current index if the element has shifted
        if (!comparer(target[current], item))
        {
            current = target.IndexOf(pick => comparer(pick, item));
        }

        // Skip if the element has already been shifted into its right place
        if (current == desired) continue;

        // Adjust the current item
        target.RemoveAt(current);
        target.Insert(desired, item);
    }

    // Step 3
    for (int i = 0; i < source.Count; i++)
    {
        // Insert the missing elements
        if (!target.Any(item => comparer(item, source[i])))
        {
            target.Insert(i, source[i]);
        }
    }

    // Adjust the indexes
    for (int i = 0; i < target.Count; i++)
    {
        indexer(target[i], i);
    }
}
7
  • I can't sort the target list nor can I use swap operations (as the UI is shown to the user and I don't want the UI to flicker) - that makes no sense to me. If by list you mean List<T>, then why would UI flicker, and if you mean Windows.Forms.Listbox then only refresh it once after you're doing sorting your internal list. Commented Apr 30, 2017 at 14:01
  • Also it's not clear would you wouldn't simply clone the source list and save the clone in target. Commented Apr 30, 2017 at 14:03
  • 2
    Yes, it certainly sounds like you are trying to solve a tricky computer science problem that you don't need to solve. If a UI is flickering under some operations then turn off UI updates, make your changes, turn updates back on, and invalidate the element. Commented Apr 30, 2017 at 14:05
  • That said, the algorithm is easy. Compute the longest common subsequence of the two sequences using the standard memoized algorithm. Now iterate through the LCS sequence and the final sequence; when you encounter an element not present in both, either insert it or delete it as appropriate. Commented Apr 30, 2017 at 14:10
  • @GSerg I've edited the question and added some more info as to why I don't want to assign the source list to the target Commented Apr 30, 2017 at 14:12

2 Answers 2

1

The algorithm you ended up seem too overcomplicated to me. Since you are ok with O(N^2) time complexity, the following simple algorithm should do the job:

For each position in the source list, check if the target item exists in that position and if it's the same as the source item in that position, i.e. if the target item is in its final position. If yes, do nothing. Otherwise, search the rest of the target (remember that each step ensures the target item is in its final position, so it cannot be before the current position) for the corresponding item of the source item at that position and if found, just remove it. Then simply insert the source item in target at that position.

After finishing the above pass, remove the target items with position >= source.Count.

And that's all. No edge cases are needed because it handles them smoothly.

public static void Synchronize<T>([NotNull] IList<T> target, [NotNull] IReadOnlyList<T> source,
    [NotNull] Func<T, T, bool> comparer, [NotNull] Action<T, int> indexer)
{
    for (int pos = 0; pos < source.Count; pos++)
    {
        if (pos >= target.Count || !comparer(target[pos], source[pos]))
        {
            // Either no target item at that position or the target item at that position
            // is different from the source item at that position.
            // Remove the corresponding target item (if any) from its original position. 
            for (int i = pos + 1; i < target.Count; i++)
            {
                if (comparer(target[i], source[pos]))
                {
                    target.RemoveAt(i);
                    break;
                }
            }
            // Insert the source item at that position
            target.Insert(pos, source[pos]);
        }
        // The target item is in its final position
        indexer(target[pos], pos);
    }

    // Remove the remaining target items if any
    for (int i = target.Count - 1; i >= source.Count; i--)
        target.RemoveAt(i);
}
Sign up to request clarification or add additional context in comments.

Comments

0

Step 1: Iterate over the target list, and remove any element that is not in the source list.

Step 2: Iterate over the target list again, noting how far each element is from its correct place (not counting missing elements). Remove the most out-of-place element, and insert it in its correct place. Repeat until the elements are in the correct order.

Step 3: Iterate over both lists, inserting the elements that are missing.

I think that's minimal.

3 Comments

As for step 2: if I haven't already added the new elements from the source list, doesn't that mean that it's possible that some elements won't ever be able to reach their "right" position while I'm still at that step (the 2nd one)? Also, the step 2 sounds like it could get stuck if I keep trying to place two or more items in their right position if I'm actually missing a new item in between their positions
@Sergio0694: Don't count missing elements in step 2. The idea is to get the target list into the right order.
Thanks, I've implemented your algorithm (see my updated question) and it works great! :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.