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

Czego szuka rekruter:
Dictionary<T,int>, LINQGroupBy, 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>zGetValueOrDefault - Czy kandydat pyta o szczegóły: tylko litery czy wszystkie znaki? Wielkość liter?
- Czy zna LINQ
GroupByjako 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).
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
18 comments
Dodaj komentarz
Musisz się zalogować, aby móc dodać komentarz.
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ę →
Very nice post. I absolutely appreciate this site. Keep writing!
bardzo dobry artykuł, dzieki
dzieki
takiego materiału szukałem!!!!
I am continually invstigating online for tips that can assist me. Thank you!
Thank you for this nice article
Thank you for this nice article
Jestem pod wrażeniem Twojej umiejętności przekazywania trudnych koncepcji w przystępny sposób
Świetny balans między teorią a praktyką. Dzięki temu artykułowi czuję, że naprawdę zrozumiałem temat
Nice blog! Thanks you all star articles good story!
good job keep it up
Dobra robota, Twój sposób myślenia naprawdę mnie inspiruje. Dzięki za dzielenie się swoją perspektywą
Dzięki za przypomnienie o ważnym zagadnieniu. Czasem łatwo o tym zapomnieć, a to naprawdę istotny aspekt programowania
Masz zdolność tłumaczenia skomplikowanych koncepcji na prosty język. Świetne
Twoje doświadczenie widać w każdym zdaniu. Inspirujące
Bardzo klarownie wyjaśnione! Dzięki za świetny artykuł
Masz talent do tłumaczenia skomplikowanych koncepcji na prosty język. To cecha, której wielu czytelników szuka