🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Zliczanie liter w tekście w C# (Dictionary)

Czego szuka rekruter: Dictionary<T,int>, LINQ GroupBy, obsługa wielkości liter i znaków
Poziom: Junior


Zadanie

Napisz funkcję, która liczy wystąpienia każdego znaku (lub litery) w podanym tekście i zwraca wyniki posortowane od najczęstszego.


Czego szuka rekruter

To zadanie o wzorcu zliczania z Dictionary — fundamentalny pattern w algorytmice. Pełne API Dictionary<TKey, TValue> omawiam w osobnym artykule Dictionary w C# — tutaj skupiamy się na jednym, konkretnym, bardzo częstym wzorcu jego użycia. Rekruter sprawdza:

  • Znajomość Dictionary<char, int> z GetValueOrDefault
  • Czy kandydat pyta o szczegóły: tylko litery czy wszystkie znaki? Wielkość liter?
  • Czy zna LINQ GroupBy jako alternatywę
  • Co zwrócić — Dictionary, posortowane pary, lista wynikowa?

Decyzje projektowe do omówienia z rekruterem

1. Litery tylko czy wszystkie znaki (spacje, cyfry, interpunkcja)?
2. Case-sensitive? ("A" i "a" liczone razem czy osobno?)
3. Wynik posortowany po częstości czy alfabetycznie?
4. Co dla null/pustego stringa?

✅ Rozwiązanie z Dictionary

static Dictionary<char, int> CountCharacters(string text, bool ignoreCase = true)
{
    if (string.IsNullOrEmpty(text))
        return new Dictionary<char, int>();

    var counts = new Dictionary<char, int>();

    foreach (char c in text)
    {
        // Opcjonalnie: pomijaj spacje i interpunkcję
        // if (!char.IsLetter(c)) continue;

        char key = ignoreCase ? char.ToLowerInvariant(c) : c;
        counts[key] = counts.GetValueOrDefault(key) + 1;
    }

    return counts;
}

// Posortowane wyniki:
static IEnumerable<(char Char, int Count)> CountSorted(string text)
    => CountCharacters(text)
        .OrderByDescending(kv => kv.Value)
        .ThenBy(kv => kv.Key)
        .Select(kv => (kv.Key, kv.Value));

// Użycie:
foreach (var (ch, count) in CountSorted("programowanie"))
    Console.WriteLine($"'{ch}': {count}");
// 'a': 3
// 'o': 2
// 'e': 1
// 'g': 1
// ...

GetValueOrDefault(key) zwraca 0 jeśli klucz nie istnieje — eliminuje potrzebę sprawdzania ContainsKey.


Zaawansowana wersja — jedno odpytanie słownika zamiast dwóch

counts.GetValueOrDefault(key) + 1 a potem counts[key] = ... to w rzeczywistości dwa odpytania wewnętrznej tablicy haszującej słownika — raz przy odczycie, raz przy zapisie. Dla milionów znaków w gorącej ścieżce wykonania to zauważalny narzut. Od .NET 6 CollectionsMarshal.GetValueRefOrAddDefault pozwala zrobić to w jednym odpytaniu, zwracając referencję do wartości w słowniku, którą można modyfikować bezpośrednio:

using System.Runtime.InteropServices;

static Dictionary<char, int> CountCharactersFast(string text, bool ignoreCase = true)
{
    var counts = new Dictionary<char, int>();

    foreach (char c in text ?? string.Empty)
    {
        char key = ignoreCase ? char.ToLowerInvariant(c) : c;

        ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(counts, key, out _);
        count++; // domyślna wartość int to 0, więc od razu inkrementujemy referencję
    }

    return counts;
}

To mikrooptymalizacja — dla typowych tekstów (pojedyncze zdania, akapity) różnica jest niemierzalna, a czytelniejszy GetValueOrDefault wygrywa. Warto ją jednak znać jako odpowiedź na pytanie “a jak przyspieszyć to jeszcze bardziej?”, które czasem pada jako pytanie pogłębiające na rozmowie z bardziej doświadczonym zespołem.


Alternatywa — LINQ GroupBy

static IEnumerable<(char Char, int Count)> CountWithLinq(string text, bool ignoreCase = true)
{
    if (string.IsNullOrEmpty(text))
        return Enumerable.Empty<(char, int)>();

    return text
        .Select(c => ignoreCase ? char.ToLowerInvariant(c) : c)
        .GroupBy(c => c)
        .OrderByDescending(g => g.Count())
        .Select(g => (g.Key, g.Count()));
}

Czytelniejsze ale mniej wydajne — Count() wywołane wielokrotnie. Możesz użyć g.Count() raz i zapisać.


Wersja tylko dla liter (pomijanie cyfr, spacji, interpunkcji)

static Dictionary<char, int> CountLettersOnly(string text)
{
    var counts = new Dictionary<char, int>();

    foreach (char c in text ?? string.Empty)
    {
        if (!char.IsLetter(c)) continue;

        char lower = char.ToLowerInvariant(c);
        counts[lower] = counts.GetValueOrDefault(lower) + 1;
    }

    return counts;
}

Testy jednostkowe

[Fact]
public void CountCharacters_IgnoresCase_MergesLetters()
{
    var result = CountCharacters("AaB");
    Assert.Equal(2, result['a']);
    Assert.Equal(1, result['b']);
}

[Fact]
public void CountCharacters_EmptyString_ReturnsEmptyDictionary()
{
    Assert.Empty(CountCharacters(""));
}

[Fact]
public void CountCharacters_CountsCorrectly()
{
    var result = CountCharacters("aabbbc");
    Assert.Equal(2, result['a']);
    Assert.Equal(3, result['b']);
    Assert.Equal(1, result['c']);
}

[Fact]
public void CountSorted_ReturnsOrderedByFrequency()
{
    var sorted = CountSorted("aabbc").ToList();
    Assert.Equal('a', sorted[0].Char);
    Assert.Equal(2, sorted[0].Count);
}

Co powiedzieć na rozmowie

Dictionary<char, int> z GetValueOrDefault jest wzorcem zliczania — prosty i O(n). Przed napisaniem pytam o szczegóły: czy liczymy wszystkie znaki czy tylko litery, czy case-sensitive, jak sortować wyniki. To pokazuje że myślę o wymaganiach zanim zacznę kodować.”


Podsumowanie

Wzorzec: counts[key] = counts.GetValueOrDefault(key) + 1. Stary idiom ContainsKey + Add/increment to dwie operacje zamiast jednej. LINQ GroupBy jest alternatywą, ale warto wiedzieć jak to działa pod spodem.

Powiązane: zobacz pokrewne zadanie Usuwanie duplikatów oraz podejście z LINQ (GroupBy).

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

18 comments

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →