web-dev-qa-db-fra.com

Détecter les différences entre deux chaînes

J'ai 2 cordes

string a = "foo bar";
string b = "bar foo";

et je veux détecter les changements de a à b. Quels caractères dois-je changer pour passer de a à b?

Je pense qu'il doit y avoir une itération sur chaque personnage et détecter s'il a été ajouté, supprimé ou est resté égal. Voici donc mon résultat exprimé

'f' Remove
'o' Remove
'o' Remove
' ' Remove
'b' Equal
'a' Equal
'r' Equal
' ' Add
'f' Add
'o' Add
'o' Add

class et enum pour le résultat:

public enum Operation { Add,Equal,Remove };
public class Difference
{
    public Operation op { get; set; }
    public char c { get; set; }
}

Voici ma solution, mais le cas "Supprimer" ne me permet pas de savoir à quoi doit ressembler le code

public static List<Difference> CalculateDifferences(string left, string right)
{
    int count = 0;
    List<Difference> result = new List<Difference>();
    foreach (char ch in left)
    {
        int index = right.IndexOf(ch, count);
        if (index == count)
        {
            count++;
            result.Add(new Difference() { c = ch, op = Operation.Equal });
        }
        else if (index > count)
        {
            string add = right.Substring(count, index - count);
            result.AddRange(add.Select(x => new Difference() { c = x, op = Operation.Add }));
            count += add.Length;
        }
        else
        {
            //Remove?
        }
    }
    return result;
}

À quoi le code doit-il ressembler pour les caractères supprimés?


Mise à jour - ajout de quelques exemples supplémentaires

exemple 1:

string a = "foobar";
string b = "fooar";

résultat attendu:

'f' Equal
'o' Equal
'o' Equal
'b' Remove
'a' Equal
'r' Equal

exemple 2:

string a = "asdfghjk";
string b = "wsedrftr";

résultat attendu:

'a' Remove
'w' Add
's' Equal
'e' Add
'd' Equal
'r' Add
'f' Equal
'g' Remove
'h' Remove
'j' Remove
'k' Remove
't' Add
'r' Add

Mise à jour:

Voici une comparaison entre la réponse de Dmitry et d'ingen: https://dotnetfiddle.net/MJQDAO

31
Dr. Snail

Vous recherchez (minimum) la distance d'édition / (minimum) la séquence d'édition . Vous pouvez trouver la théorie du processus ici:

https://web.stanford.edu/class/cs124/lec/med.pdf

Implémentons (le plus simple) l'algorithme de distance/séquence de Levenstein (pour plus de détails, voir https://en.wikipedia.org/wiki/Levenshtein_distance ). Commençons par les classes d'assistance (j'ai un peu changé leur implémentation):

  public enum EditOperationKind : byte {
    None,    // Nothing to do
    Add,     // Add new character
    Edit,    // Edit character into character (including char into itself)
    Remove,  // Delete existing character
  };

  public struct EditOperation {
    public EditOperation(char valueFrom, char valueTo, EditOperationKind operation) {
      ValueFrom = valueFrom;
      ValueTo = valueTo;

      Operation = valueFrom == valueTo ? EditOperationKind.None : operation;
    }

    public char ValueFrom { get; }
    public char ValueTo {get ;}
    public EditOperationKind Operation { get; }

    public override string ToString() {
      switch (Operation) {
        case EditOperationKind.None:
          return $"'{ValueTo}' Equal";
        case EditOperationKind.Add:
          return $"'{ValueTo}' Add";
        case EditOperationKind.Remove:
          return $"'{ValueFrom}' Remove";
        case EditOperationKind.Edit:
          return $"'{ValueFrom}' to '{ValueTo}' Edit";
        default:
          return "???";
      }
    }
  }

D'après ce que je peux voir dans les exemples fournis, nous n'avons aucune opération d'édition , mais add + remove ; c'est pourquoi j'ai mis editCost = 2 quand insertCost = 1, int removeCost = 1 (en cas de égalité : insert + remove vs edit nous mettons insert + remove). Nous sommes maintenant prêts à implémenter l'algorithme de Levenstein:

public static EditOperation[] EditSequence(
  string source, string target, 
  int insertCost = 1, int removeCost = 1, int editCost = 2) {

  if (null == source)
    throw new ArgumentNullException("source");
  else if (null == target)
    throw new ArgumentNullException("target");

  // Forward: building score matrix

  // Best operation (among insert, update, delete) to perform 
  EditOperationKind[][] M = Enumerable
    .Range(0, source.Length + 1)
    .Select(line => new EditOperationKind[target.Length + 1])
    .ToArray();

  // Minimum cost so far
  int[][] D = Enumerable
    .Range(0, source.Length + 1)
    .Select(line => new int[target.Length + 1])
    .ToArray();

  // Edge: all removes
  for (int i = 1; i <= source.Length; ++i) {
    M[i][0] = EditOperationKind.Remove;
    D[i][0] = removeCost * i;
  }

  // Edge: all inserts 
  for (int i = 1; i <= target.Length; ++i) {
    M[0][i] = EditOperationKind.Add;
    D[0][i] = insertCost * i;
  }

  // Having fit N - 1, K - 1 characters let's fit N, K
  for (int i = 1; i <= source.Length; ++i)
    for (int j = 1; j <= target.Length; ++j) {
      // here we choose the operation with the least cost
      int insert = D[i][j - 1] + insertCost;
      int delete = D[i - 1][j] + removeCost;
      int edit = D[i - 1][j - 1] + (source[i - 1] == target[j - 1] ? 0 : editCost);

      int min = Math.Min(Math.Min(insert, delete), edit);

      if (min == insert) 
        M[i][j] = EditOperationKind.Add;
      else if (min == delete)
        M[i][j] = EditOperationKind.Remove;
      else if (min == edit)
        M[i][j] = EditOperationKind.Edit;

      D[i][j] = min;
    }

  // Backward: knowing scores (D) and actions (M) let's building edit sequence
  List<EditOperation> result = 
    new List<EditOperation>(source.Length + target.Length);

  for (int x = target.Length, y = source.Length; (x > 0) || (y > 0);) {
    EditOperationKind op = M[y][x];

    if (op == EditOperationKind.Add) {
      x -= 1;
      result.Add(new EditOperation('\0', target[x], op));
    }
    else if (op == EditOperationKind.Remove) {
      y -= 1;
      result.Add(new EditOperation(source[y], '\0', op));
    }
    else if (op == EditOperationKind.Edit) {
      x -= 1;
      y -= 1;
      result.Add(new EditOperation(source[y], target[x], op));
    }
    else // Start of the matching (EditOperationKind.None)
      break;
  }

  result.Reverse();

  return result.ToArray();
}

Démo:

var sequence = EditSequence("asdfghjk", "wsedrftr"); 

Console.Write(string.Join(Environment.NewLine, sequence));

Résultat:

'a' Remove
'w' Add
's' Equal
'e' Add
'd' Equal
'r' Add
'f' Equal
'g' Remove
'h' Remove
'j' Remove
'k' Remove
't' Add
'r' Add
20
Dmitry Bychenko

Je vais me lancer ici et fournir un algorithme qui n'est pas le plus efficace, mais qui est facile à raisonner.

Voyons d'abord quelques points:

1) Questions d'ordre

string before = "bar foo"
string after = "foo bar"

Même si "bar" et "foo" apparaissent dans les deux chaînes, "bar" devra être supprimé et ajouté à nouveau plus tard. Cela nous indique également que c'est la chaîne after qui nous donne l'ordre des caractères qui nous intéresse, nous voulons d'abord "foo".

2) Commander plus que compter

Une autre façon de voir les choses est que certains caractères peuvent ne jamais avoir leur tour.

string before = "abracadabra"
string after = "bar bar"

Seuls les caractères gras de "bar b a r", ont leur mot à dire dans "a b r a cadab ra ". Même si nous avons deux b dans les deux chaînes, seule la première compte . Au moment où nous arrivons au deuxième b dans "ba rb ar" le deuxième b dans "abracada br a "a déjà été passé, lorsque nous cherchions la première occurrence de 'r'.

3) Obstacles

Les obstacles sont les caractères qui existent dans les deux chaînes, en tenant compte de l'ordre et du compte. Cela suggère déjà qu'un ensemble pourrait ne pas être la structure de données la plus appropriée, car nous perdrions le compte.

Pour une entrée

string before = "pinata"
string after = "accidental"

Nous obtenons (pseudocode)

var barriers = { 'a', 't', 'a' }

"pin ata"

"a cciden ta l"

Suivons le flux d'exécution:

  • 'a' est la première barrière, c'est aussi le premier caractère de after donc tout ce qui précède le premier 'a' dans before peut être supprimé. "pin a ta" -> "a ta"
  • la deuxième barrière est 't', elle n'est pas à la position suivante dans notre chaîne after, nous pouvons donc tout insérer entre les deux. "a t a" -> "acciden t a"
  • la troisième barrière "a" est déjà à la position suivante, nous pouvons donc passer à la barrière suivante sans faire de travail réel.
  • il n'y a plus de barrières, mais notre longueur de chaîne est toujours inférieure à celle de after, donc il y aura du post-traitement. "accidenta" -> "accidenta l "

Notez que 'i' et 'n' ne jouent pas, encore une fois, commandez plus que compte.


La mise en oeuvre

Nous avons établi que l'ordre et le nombre comptent, un Queue vient à l'esprit.

static public List<Difference> CalculateDifferences(string before, string after)
{
    List<Difference> result = new List<Difference>();
    Queue<char> barriers = new Queue<char>();

    #region Preprocessing
    int index = 0;
    for (int i = 0; i < after.Length; i++)
    {
        // Look for the first match starting at index
        int match = before.IndexOf(after[i], index);
        if (match != -1)
        {
            barriers.Enqueue(after[i]);
            index = match + 1;
        }
    }
    #endregion

    #region Queue Processing
    index = 0;
    while (barriers.Any())
    {
        char barrier = barriers.Dequeue();
        // Get the offset to the barrier in both strings, 
        // ignoring the part that's already been handled
        int offsetBefore = before.IndexOf(barrier, index) - index;
        int offsetAfter = after.IndexOf(barrier, index) - index;
        // Remove prefix from 'before' string
        if (offsetBefore > 0)
        {
            RemoveChars(before.Substring(index, offsetBefore), result);
            before = before.Substring(offsetBefore);
        }
        // Insert prefix from 'after' string
        if (offsetAfter > 0)
        {
            string substring = after.Substring(index, offsetAfter);
            AddChars(substring, result);
            before = before.Insert(index, substring);
            index += substring.Length;
        }
        // Jump over the barrier
        KeepChar(barrier, result);
        index++;
    }
    #endregion

    #region Post Queue processing
    if (index < before.Length)
    {
        RemoveChars(before.Substring(index), result);
    }
    if (index < after.Length)
    {
        AddChars(after.Substring(index), result);
    }
    #endregion

    return result;
}

static private void KeepChar(char barrier, List<Difference> result)
{
    result.Add(new Difference()
    {
        c = barrier,
        op = Operation.Equal
    });
}

static private void AddChars(string substring, List<Difference> result)
{
    result.AddRange(substring.Select(x => new Difference()
    {
        c = x,
        op = Operation.Add
    }));
}

static private void RemoveChars(string substring, List<Difference> result)
{
    result.AddRange(substring.Select(x => new Difference()
    {
        c = x,
        op = Operation.Remove
    }));
}
8
ingen

J'ai testé avec 3 exemples ci-dessus, et cela renvoie le résultat attendu correctement et parfaitement.

        int flag = 0;
        int flag_2 = 0;

        string a = "asdfghjk";
        string b = "wsedrftr";

        char[] array_a = a.ToCharArray();
        char[] array_b = b.ToCharArray();

        for (int i = 0,j = 0, n= 0; i < array_b.Count(); i++)
        {   
            //Execute 1 time until reach first equal character   
            if(i == 0 && a.Contains(array_b[0]))
            {
                while (array_a[n] != array_b[0])
                {
                    Console.WriteLine(String.Concat(array_a[n], " : Remove"));
                    n++;
                }
                Console.WriteLine(String.Concat(array_a[n], " : Equal"));
                n++;
            }
            else if(i == 0 && !a.Contains(array_b[0]))
            {
                Console.WriteLine(String.Concat(array_a[n], " : Remove"));
                n++;
                Console.WriteLine(String.Concat(array_b[0], " : Add"));
            }


            else
            {
                if(n < array_a.Count())
                {
                    if (array_a[n] == array_b[i])
                    {
                        Console.WriteLine(String.Concat(array_a[n], " : Equal"));
                        n++;
                    }
                    else
                    {
                        flag = 0;
                        for (int z = n; z < array_a.Count(); z++)
                        {                              
                            if (array_a[z] == array_b[i])
                            {
                                flag = 1;
                                break;
                            }                                                              
                        }

                        if (flag == 0)
                        {
                            flag_2 = 0;
                            for (int aa = i; aa < array_b.Count(); aa++)
                            {
                                for(int bb = n; bb < array_a.Count(); bb++)
                                {
                                    if (array_b[aa] == array_a[bb])
                                    {
                                        flag_2 = 1;
                                        break;
                                    }
                                }
                            }

                            if(flag_2 == 1)
                            {
                                Console.WriteLine(String.Concat(array_b[i], " : Add"));
                            }
                            else
                            {
                                for (int z = n; z < array_a.Count(); z++)
                                {
                                    Console.WriteLine(String.Concat(array_a[z], " : Remove"));
                                    n++;
                                }
                                 Console.WriteLine(String.Concat(array_b[i], " : Add"));
                            }

                        }
                        else
                        {
                            Console.WriteLine(String.Concat(array_a[n], " : Remove"));
                            i--;
                            n++;
                        }

                    }
                }
                else
                {
                    Console.WriteLine(String.Concat(array_b[i], " : Add"));
                }

            }

        }//end for


        MessageBox.Show("Done");


    //OUTPUT CONSOLE:
    /*
    a : Remove
    w : Add
    s : Equal
    e : Add
    d : Equal
    r : Add
    f : Equal
    g : Remove
    h : Remove
    j : Remove
    k : Remove
    t : Add
    r : Add
    */  
3
Nguyen Thanh Binh

Voici peut-être une autre solution, le code complet et commenté. Cependant le résultat de votre premier exemple original est inversé:

class Program
{
    enum CharState
    {
        Add,
        Equal,
        Remove
    }

    struct CharResult
    {
        public char c;
        public CharState state;
    }

    static void Main(string[] args)
    {
        string a = "asdfghjk";
        string b = "wsedrftr";
        while (true)
        {
            Console.WriteLine("Enter string a (enter to quit) :");
            a = Console.ReadLine();
            if (a == string.Empty)
                break;
            Console.WriteLine("Enter string b :");
            b = Console.ReadLine();

            List<CharResult> result = calculate(a, b);
            DisplayResults(result);
        }
        Console.WriteLine("Press a key to exit");
        Console.ReadLine();
    }

    static List<CharResult> calculate(string a, string b)
    {
        List<CharResult> res = new List<CharResult>();
        int i = 0, j = 0;

        char[] array_a = a.ToCharArray();
        char[] array_b = b.ToCharArray();

        while (i < array_a.Length && j < array_b.Length)
        {
            //For the current char in a, we check for the equal in b
            int index = b.IndexOf(array_a[i], j);
            if (index < 0) //not found, this char should be removed
            {
                res.Add(new CharResult() { c = array_a[i], state = CharState.Remove });
                i++;
            }
            else
            {
                //we add all the chars between B's current index and the index
                while (j < index)
                {
                    res.Add(new CharResult() { c = array_b[j], state = CharState.Add });
                    j++;
                }
                //then we say the current is the same
                res.Add(new CharResult() { c = array_a[i], state = CharState.Equal });
                i++;
                j++;
            }
        }

        while (i < array_a.Length)
        {
            //b is now empty, we remove the remains
            res.Add(new CharResult() { c = array_a[i], state = CharState.Remove });
            i++;
        }
        while (j < array_b.Length)
        {
            //a has been treated, we add the remains
            res.Add(new CharResult() { c = array_b[j], state = CharState.Add });
            j++;
        }

        return res;
    }

    static void DisplayResults(List<CharResult> results)
    {
        foreach (CharResult r in results)
        {
            Console.WriteLine($"'{r.c}' - {r.state}");
        }
    }
}
3
Martin Verjans

Si vous voulez avoir une comparaison précise entre deux chaînes, vous devez lire et comprendre Levenshtein Distance. en utilisant cet algorithme, vous pouvez calculer précisément le taux de similitude entre deux chaînes et vous pouvez également revenir en arrière sur l'algorithme pour obtenir la chaîne de changement sur la deuxième chaîne. cet algorithme est également une métrique importante pour le traitement du langage naturel.

il y a d'autres avantages et il faut du temps pour apprendre.

dans ce lien, il existe une version C # de Levenshtein Distance:

https://www.dotnetperls.com/levenshtein

1
RezaNoei