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

Jak rozwiązywać problemy w C# — 4 kroki

Dostajesz zadanie: „Sprawdź, czy w tablicy liczb istnieją dwie wartości, które sumują się do zadanego celu”. Patrzysz na treść i… nie wiesz od czego zacząć pisać kod. To nie jest problem znajomości składni C# — to brak metody rozkładania nieznanego problemu na kroki, które faktycznie da się zakodować.

Rozwiązywanie problemów to metoda, nie talent

Dobra wiadomość: to powtarzalny proces, ten sam niezależnie od konkretnego zadania. Cztery kroki: zrozum dokładnie wejście i wyjście, napisz najprostsze możliwe rozwiązanie, sprawdź je ręcznie na małym przykładzie, dopiero potem szukaj czegoś lepszego. Prześledźmy to na konkretnym zadaniu — dokładnie takim, jakie dostajesz na rekrutacji.

Krok 1 — Zrozum dokładnie wejście i wyjście

Zanim napiszesz jakikolwiek kod, odpowiedz precyzyjnie na dwa pytania: co dostajesz i co masz zwrócić. Dla naszego zadania:

  • Wejście: tablica liczb całkowitych int[] oraz liczba cel
  • Wyjście: true, jeśli istnieją dwie różne liczby z tablicy sumujące się do cel; w przeciwnym razie false

To pozornie oczywisty krok, ale to właśnie tutaj większość początkujących traci najwięcej czasu — próbują pisać kod, zanim jasno zdefiniują, co ten kod ma zwracać. Jeśli to pomaga, zapisz to w punktach na kartce albo jako pseudokod — nie musisz jeszcze myśleć w składni C#.

Krok 2 — Napisz najprostsze możliwe rozwiązanie

Nie szukaj od razu optymalnego algorytmu. Najprostsze podejście do „czy istnieje para sumująca się do celu” to sprawdzenie każdej możliwej pary liczb:

static bool IstniejePara(int[] liczby, int cel)
{
    for (int i = 0; i < liczby.Length; i++)
    {
        for (int j = i + 1; j < liczby.Length; j++)
        {
            if (liczby[i] + liczby[j] == cel)
            {
                return true; // ✅ znaleziono parę
            }
        }
    }

    return false; // przeszliśmy wszystkie pary i nic — poprawny wynik, nie błąd
}

To rozwiązanie sprawdza każdą parę liczb osobno — dla tablicy o długości n to n × (n-1) / 2 porównań. Nieefektywne dla dużych tablic, ale działające. Na tym etapie to ważniejsze niż szybkość.

Działający, nieefektywny kod, który rozumiesz, jest wart więcej niż optymalny kod, którego jeszcze nie potrafisz napisać. Optymalizację zostaw na krok 4.

Krok 3 — Sprawdź ręcznie na małym przykładzie

Zanim uruchomisz cokolwiek w Visual Studio, prześledź kod na kartce dla małego przykładu — np. liczby = [2, 7, 11, 15], cel = 9. Pętla sprawdza kolejno: 2 + 7 = 9 — zgadza się, funkcja zwraca true już po drugim porównaniu. Teraz sprawdź przypadek brzegowy: liczby = [4], cel = 8. Pętla wewnętrzna nigdy się nie wykona (bo j zaczyna się od i + 1, a jest tylko jeden element), więc funkcja poprawnie zwraca false — jedna czwórka nie może „sparować się sama ze sobą”.

Ten krok wyłapuje większość błędów logicznych, zanim jeszcze napiszesz test jednostkowy. Kompilator sprawdzi Ci składnię — nie sprawdzi, czy Twoja logika ma sens dla przypadku brzegowego.

Krok 4 — Szukaj lepszego rozwiązania dopiero teraz

Rozwiązanie z kroku 2 działa, ale dla tablicy 10 000 elementów to około 50 milionów porównań. Skoro rozumiesz już problem (krok 1) i masz działający punkt odniesienia (krok 2), możesz bezpiecznie szukać czegoś szybszego — i sprawdzić, czy nowa wersja wciąż daje ten sam wynik co stara.

static bool IstniejePara(int[] liczby, int cel)
{
    var widziane = new HashSet<int>();

    foreach (int liczba in liczby)
    {
        int brakujaca = cel - liczba;

        if (widziane.Contains(brakujaca)) // sprawdź PRZED dodaniem
        {
            return true;
        }

        widziane.Add(liczba);
    }

    return false;
}

Zamiast porównywać każdą parę, dla każdej liczby sprawdzamy, czy wcześniej widzieliśmy jej „brakującą połówkę” (cel - liczba). Wyszukiwanie w HashSet jest w praktyce natychmiastowe, więc cała funkcja sprawdza tablicę jednym przejściem — n operacji zamiast n × (n-1) / 2. Dla 10 000 elementów to różnica między 10 000 a 50 milionami sprawdzeń.

Szybsze rozwiązanie ma sens dopiero wtedy, gdy masz z czym je porównać. Bez działającej wersji z kroku 2 nie wiedziałbyś, czy HashSet w ogóle daje ten sam wynik.

Pułapki, które łapią początkujących przy rozwiązywaniu problemów

Pułapka #1: Kolejność sprawdź-potem-zapisz w HashSecie

Zamień miejscami dwie linijki z kroku 4 — najpierw Add, potem Contains — i kod dalej się kompiluje, dalej działa dla większości przypadków... i cicho zwraca zły wynik dla jednego konkretnego przypadku:

// ❌ zła kolejność: dodajemy PRZED sprawdzeniem
widziane.Add(liczba);
if (widziane.Contains(brakujaca)) // liczba może "sparować się" sama ze sobą
{
    return true;
}

Dla liczby = [4], cel = 8 ta wersja zwróci true, mimo że w tablicy jest tylko jedna czwórka — nie da się jej dodać do samej siebie. To najgorszy rodzaj błędu: bez wyjątku, bez czerwonej linijki w edytorze, po prostu zły wynik. Dokładnie dlatego krok 3 (ręczne sprawdzenie na przypadku brzegowym) jest obowiązkowy, nie opcjonalny.

Pułapka #2: Zmiana typu zwracanego bez przemyślenia konsekwencji

Załóżmy, że rekruter prosi o zmianę: zamiast true/false, metoda ma zwracać indeksy pasującej pary jako int[]. Naturalny odruch to dopisanie return null; w miejscu, gdzie wcześniej było return false;:

static int[] ZnajdzIndeksyPary(int[] liczby, int cel)
{
    // ...
    return null; // ⚠️ CS8603: Possible null reference return
}

Kompilator z włączonym <Nullable>enable</Nullable> (domyślne w nowych projektach) zgłosi ostrzeżenie CS8603, bo sygnatura int[] deklaruje, że metoda zawsze zwraca tablicę — nigdy null. To nie jest kaprys kompilatora: to on wymusza jawną decyzję z kroku 1. Rozwiązanie: zmień sygnaturę na int[]? i spraw, żeby wywołujący kod musiał obsłużyć przypadek braku wyniku.

Pułapka #3: Optymalizacja zanim cokolwiek działa

Kuszące jest, żeby od razu napisać wersję z HashSetem — wygląda „bardziej profesjonalnie”. Problem: gdy się pomylisz (a przy pierwszym podejściu do nowego problemu prędzej czy później się pomylisz), debugujesz jednocześnie logikę problemu i szczegóły implementacji struktury danych. Rozdzielenie tego na krok 2 (działające, proste) i krok 4 (szybkie, na sprawdzonym fundamencie) to nie strata czasu — to najszybsza droga do poprawnego kodu.

Checklist — 5 pytań zanim napiszesz pierwszą linijkę kodu

  • Co dokładnie dostaję na wejściu i w jakim typie danych?
  • Co dokładnie mam zwrócić i co się dzieje, gdy nie ma rozwiązania?
  • Czy potrafię rozwiązać to ręcznie, na kartce, dla jednego małego przykładu?
  • Jakie jest najprostsze możliwe (nawet nieefektywne) rozwiązanie, które na pewno zadziała?
  • Jakie dane wejściowe mogą złamać to rozwiązanie — pusta tablica, jeden element, duplikaty, brak pasującej pary?

Podsumowanie

Rozwiązywanie nieznanego problemu programistycznego to nie kwestia talentu ani znajomości sztuczek — to czteroetapowy proces, który stosujesz do każdego zadania: zrozum wejście i wyjście, napisz najprostsze działające rozwiązanie, sprawdź je ręcznie na przykładzie, dopiero potem optymalizuj. Ta sama metoda działa identycznie przy prostszych zadaniach jak FizzBuzz czy prosty kalkulator — zmienia się tylko problem, nie sposób jego rozkładania. Jeśli chcesz zobaczyć, jak ta sama dyscyplina przekłada się na strukturę całego programu, a nie tylko pojedynczej funkcji, zobacz też mental model kodu jako fabryki.

👨‍💻
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.

1 comment

  1. Świetny artykuł! Zdecydowanie zgadzam się z tym, że rozwiązywanie problemów to jedna z najważniejszych umiejętności dla programisty. To coś więcej niż tylko pisanie kodu – to umiejętność myślenia analitycznego, kreatywnego i znajdowania rozwiązań problemów, które często nie są oczywiste.

    Szczególnie podoba mi się metafora porównywania programistów do detektywów. Myślę, że to bardzo trafne porównanie, ponieważ obaj muszą umieć gromadzić informacje, analizować je i wyciągać wnioski, aby rozwiązać zagadkę.

    Jestem przekonany, że każdy, kto chce zostać programistą, powinien skupić się na rozwijaniu swoich umiejętności rozwiązywania problemów. Istnieje wiele sposobów na ćwiczenie tych umiejętności, na przykład poprzez udział w konkursach programistycznych lub pracę nad własnymi projektami.

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ę →