Como criar dicionários case insensitive

Dicionários são sempre listados entre as melhores estruturas de dados pois o custo de acesso a um dado registro é O(1), ou seja, não é necessário percorrer todo dataset para encontrar um elemento, basta acessar o seu índice diretamente. Mas como criar dicionários case insensitive? Ou em outras palavras, como criar dicionários que ignorem a forma como uma palavra foi escrita?

O código abaixo armazena o estoque de produtos para uma determinada loja. Sempre que um produto é adicionado ele verifica se o produto já existe, caso exista aumenta a quantidade em estoque, quando não existe cria um novo registro.

namespace Dictionary {
   public class Store {
      public Dictionary<string, int> Items = new();

      public void Add(string product, int quant) {
         if (Items.ContainsKey(product)) {
            Items[product] += quant;
         }
         else {
            Items.Add(product, quant);
         }
      }
   }
   public static class DictionaryExample {
      public static void Main() {
         var store = new Store();
         store.Add("Bike", 10);
         store.Add("bike", 15);
         store.Add("Skate", 8);

         foreach (var item in store.Items) {
            Console.WriteLine($"{item.Key} - {item.Value}");
         }
         Console.ReadKey();
      }
   }
}

O problema é que, por padrão, o runtime trata strings como case sensitive, então “Bike”, “BIKE”, …. e “bike” são palavras diferentes, resultando em dois diferentes produtos:

Bike - 10
bike - 15
Skate - 8

Uma solução simples

Uma solução simples para o problema é converter todas as letras do nome do produto para minúsculas ou maiúsculas, usando ToLower ou ToUpper no método Add():

public void Add(string product, int quant) {
   product = product.ToLower();
   if (Items.ContainsKey(product)) {
      Items[product] += quant;
   }
   else {
      Items.Add(product, quant);
   }
}

O que trará como resultado:

bike - 25
skate - 8

Funciona, mas não é o ideal.

Uma solução eficaz

Uma solução melhor para o problema é especificar ao runtime como ele deve tratar as strings armazenadas no dicionário usando a classe StringComparer no momento que o dicionário é instanciado.

Então, ao invés de instanciar o objeto simplesmente usando new(), basta passar o tipo de comparação desejada no construtor no mesmo. A opção InvariantCultureIgnoreCase fará com que as strings “Bike” e “bike” sejam consideradas a mesma sem a necessidade do uso do método ToLower e produzirá exatamente a mesma saída.

public Dictionary<string, int> Items = new(StringComparer.InvariantCultureIgnoreCase);

Quer saber mais sobre os diferentes tipos de comparação, então acesse a documentação oficial da Microsoft: StringComparer Class

 

Teste de código: contando os vales percorridos

Enunciado em inglês. Desde já, tenha em mente que o texto foi parcialmente alterado para diminuir a chance da solução seja encontrada por alguém que esteja fazendo a prova:

A hiker keeps records of their hikes registering when is uphill, sea level, or downhill. Hikes always start and end at sea level, and each step up or down represents a  unit change in altitude. We define the following terms:

    • A hill is a sequence of consecutive steps above sea level, starting with a step up from sea level and ending with a step down to sea level.
    • valley is a sequence of consecutive steps below sea level, starting with a step down from sea level and ending with a step up to sea level.

Given the sequence of up and down steps during a hike, find and print the number of valleys walked through.

Sample Input: 8, UDDDUDUU

Expected result: 1

Explanation: If we represent _ as sea level, a step up as /, and a step down as \, the hike can be drawn as:

_/\      _
   \    /
    \/\/

Explicando

Um caminhante registra todos seus passos durante suas caminhadas (a redundância está no texto original). Ele sempre começa as caminhadas no nível do mar, sempre que sobe registra um U (up) e sempre que desce registra um D (down).

No exemplo dado, o caminhante começa subindo um nível uma colina (UDDDUDUU), imediatamente após desce um nível (UDDDUDUU), o que faz com que ele volte ao nível do mar e depois começa a descer um vale (UDDDUDUU), sobre um nível (UDDDUDUU), volta a descer (UDDDUDUU) e depois sobe novamente até o nível do mar (UDDDUDUU). O exercício pede que seja retornado o número de vales pelo qual o caminhante passou (neste caso: 1).

Resolvendo

Há várias formas de resolver o problema, uma delas é ir contado o número de passos para cima e para baixo. Quando eles são iguais e o próximo passo for para baixo, está começando a descer um vale. Uma vez que o  número de passos volte a ser iguais, parou de descer e voltou ao nível do mar. Apesar de funcionar, não é uma solução das mais eficiente.

Outra forma, com melhor performance, é comparar o nível em que o caminhante se encontra. Considere zero como sendo o nível do mar, cada vez que subir adicione uma unidade ao nível, quando ele descer, subtraia uma unidade. Desta forma, quando o nível for menor que zero, o caminhante se encontra num vale., quando o nível voltar a zero, ele saiu do vale. Então, basta contar quantas vezes ele entrou no vale.  Abaixo o código devidamente comentado:

int contandoVales(int passos, string caminho) {
    int nivel = 0; //nivel do mar
    int numeroVales = 0;
    bool estaNumVale = false; //comeca no nivel do mar
    char desce = 'D';
 
    //percorre toda a string "caminho"
    for (int i = 0; i < passos; i++)
    {
        //se a posicao for igual a "D" subtrai uma unidade do "nivel"
        //senao adiciona uma unidade (este é um IF ternário)
        nivel = caminho[i] == desce ? --nivel : ++nivel;

        //se o nível for menor que zero e nao está no vale        
        if (nivel < 0 && !estaNumVale)
        {
            numeroVales++; //incrementa o número de vales
        }

        //se nível for menor que zero está no vale
        estaNumVale = nivel < 0;
    }

    return numeroVales;
}
Origem da questão
País: Bélgica Tipo: Teste de Código Assunto: Manipulação de strings
Ramo de negócio da empresa: Banco Grau de Dificuldade: Fácil

Teste de código: encontrar o maior número possível

O texto e valores do enunciado em inglês foram ligeiramente alterados para diminuir a chance da solução seja encontrada por alguém que esteja fazendo a prova:

Write a function that, given an integer N, returns the maximum possible value obtainable by deleting one ‘3’ digit from the decimal representation of N.

Examples: Given N = 13938, the function should return 1938.  Given N = -3839, the function should return -389. Given N = -30, the function should return 0. After deleting the ‘3’, the only digits in the number are zeroes, so its value is 0.

Assume that:

    • N is an integer within the range -99999999 to 99999999;
    • N contains at least one ‘3’ digit in its decimal representation;
    • N consists of at least two digits in its decimal representation.

In your solution, focus on correctness. The performance of your solution will not be the focus of the assessment.

Enunciado (resumido) em português

Dado um número inteiro N, retorne o maior valor possível ao remover o dígito 3 do número. Por exemplo: dado N= 393, deve retornar 93. Se N = 3153, retorna 315. Se N= -323, retorna -23. Por fim, se N = -30, retorna 0 (zero).

Assuma que sempre haverá um 3 no valor informado e este valor sempre terá pelo menos duas casas decimais.

Apesar de receber um número inteiro, trata-se de uma questão de parse de strings, uma vez que o valor precisa ser convertido para string para ser alterado (é possível trabalhar apenas com números, mas isto torna a questão muito mais complexa).

Para resolver o problema vamos pegar o valor 3153. Se remover o primeiro “3”, retorna 153, se remover o segundo 315, que é a resposta esperada.

Uma das formas de resolver o problema é identificar as posições da string que possuem o 3 e depois eliminar estas posições (indexes) uma a uma, comparando o resultado, o que gera o seguinte algoritmo em português estruturado

1. Converta o número para string
2. Percora a string identificando as posições onde o valor = 3
3. Armaze as posições num vetor 
4. Inicialize uma variável maiorValor com o menor valor possível
5. Para cada uma das posições do vetor faça
   6. Remova o caracter da posição na string original
   7. Compara o valor de maiorValor com o novo valor gerado
   8. Se o novo valor for maior, armaze no valor em maiorValor
9. Fim do para
10. Retorne o maior valor

A solução em C#

using System;
using System.Collections.Generic;

class Solution {
    //retorna um vetor com as posicoes da string "number" 
    //ocupadas pelo numero 3
    //ex: 635131 retorna [1,4] => a contagem comeca em 0
    private static List<int> GetIndexes(string number) {
        //cria uma lista (que sera retornada)
        var indexes = new List<int>();

        //percore todos os caracteres da string
        //number.Length = 6, para o numero 635131 
        for(int i = 0; i< number.Length; i++) {
            //se caracter = 3, adiciona o indice na lista
            if (number[i] == '3') {
                indexes.Add(i);
            }
        }
        return indexes;
    }

    public int solution(int N) {
        //conver o numero N para uma string
        var originalNumber = N.ToString();
        //pega a lista de indices
        var indexes = GetIndexes(originalNumber);
        //seta o maior valor como o menor inteiro existe
        //assim qualquer valor gerado serah maior q ele
        var maxValue = int.MinValue;

        //percore todos os elementos do vetor
        foreach(var index in indexes) {
           //remove o caracter da posicao armazenada em GetIndexes()
            var number = originalNumber.Remove(index,1);
            //transforma a string num numero inteiro
            var newNumber = int.Parse(number);

            //se o "valor maximo" for menor que o "novo valor"
            //seta o "valor maximo" como sendo o "novo valor"
            if (maxValue < newNumber) {
                maxValue = newNumber;
            }
        }
        return maxValue;
    }
}

 

Origem da questão
País: Irlanda Tipo: Teste de Código Assunto: manipulação de strings
Ramo de negócio da empresa: Pagamentos Grau de Dificuldade: Fácil