Spis treści Poprzednia strona: Pętle Następna strona: Funkcje, czyli metody
Tablica, to taka zmienna, która potrafi przechowywać określoną ilość zmiennych tego samego typu pod jedną nazwą. Dostęp do poszczególnych przechowywanych zmiennych uzyskuje się podając ich numery (indeksy) w nawiasach kwadratowych umieszczonych za nazwą zmiennej typu tablicowego (bez odstępu między nimi, chociaż wstawienie tam spacji nie jest błędem, tzn. może być tablica [5] zamiast tablica[5] oraz double [] zamiast double[]). Nawiasy kwadratowe za nazwą tablicy to tzw. operator indeksacji [indexer]. Używany on jest do uzyskania wartości jednej ze zmiennych przechowanych w tablicy, tej o podanym indeksie. Zmienne w tablicy (tzw. elementy tablicy) numerowane (indeksowane) są zwykle od zera, do n-1, gdzie n jest rozmiarem tablicy, czyli liczbą jej elementów, albo inaczej jej długością [length]. Każda tablica jest obiektem (typem referencyjnym), który niejawnie dziedziczy po typie System.Array. Co to dokładnie oznacza, dowiemy się później. Tablica to złożony typ danych, jedna z prostszych struktur danych pozwalająca na przechowywanie wcześniej znanej ilości zmiennych jednego typu.
Zdefiniujemy teraz tablicę 10-ciu liczb całkowitych.
int[] moja_pierwsza_tablica = new int[10]; { int[] moja_druga_tablica; // tu nie można używać moja_druga_tablica moja_druga_tablica = new int[73]; // przydział pamięci // tu już można używać moja_druga_tablica } // nie można używać moja_druga_tablica, bo jest już poza zakresem
Zwróćmy uwagę na zakres widoczności zmiennej tablicowej w powyższym listingu. Poniżej przykład jak będzie wyglądać odwołanie do wartości piątej zmiennej w tablicy (piątego elementu tablicy, czyli elementu o indeksie 4 (bo pierwszy ma indeks zerowy)):
moja_pierwsza_tablica[4] = 2345; int k = moja_pierwsza_tablica[4] - 5; moja_pierwsza_tablica[4] *= k; Console.WriteLine(moja_pierwsza_tablica[4]); // 5487300
Zapisu a[n] można używać tak, jak każdej innej zmiennej typu int, a więc można jej przypisywać wartość i odczytywać z niej wartość, można używać wartości w bardziej skomplikowanych wyrażeniach, można też używać na elementach tablic operatorów inkrementacji i dekrementacji (np. a[n]++ zwiększa a[n] o jeden po użyciu wartości a[n] w wyrażeniu). Indeks n nie musi być stałą ani zmienną, ale może być całym wyrażeniem o wartości całkowitej w zakresie dozwolonym dla indeksów, czyli od zera do rozmiaru tablicy minus jeden.
Z elementu tablicy można odczytać wartość nawet wtedy, kiedy nie została ona nigdy jawnie przypisana (będzie to domyślnie wartość zero). Zmienne w tablicach, bezpośrednio po utworzeniu tablicy przy pomocy słowa kluczowego new, są zawsze niejawnie inicjalizowane poprzez domyślne wartości (dla liczb całkowitych taką wartością domyślną jest właśnie default(int), czyli zero). Inicjalizacja zmiennej, to właśnie przypisywanie jej wartości początkowej. W przypadku zwykłych zmiennych musimy to zrobić my, a w przypadku zmiennych strukturalnych (tablic i struktur) zajmuje się tym sam .NET Framework i nie musimy się obawiać, że przed wykorzystaniem tablicy znajdziemy w niej jakieś przypadkowe wartości pozostawione w pamięci przez inne programy (jak na przykład było w języku C++ – tzw. wycieki pamięci [memory leaks] w kodzie zarządzanym po prostu nie zdarzają się). Nie trzeba także dbać o zwalnianie pamięci przydzielonej na tablicę. Ale nie wszystko jest takie wesołe, bo częste przydzielanie dużych tablic może być czasochłonne właśnie przez tą domyślną inicjalizację (oraz energochłonne, bo usuwanie grzeje elektronikę i jest to ciepło do odprowadzenia, a więc energia stracona). Jeśli znamy maksymalne wymagania pamięciowe danego zagadnienia, to lepiej będzie od razu przydzielić największą tablicę, i osobno pamiętać która jej część jest aktualnie faktycznie używana. Jeśli jednak takie własnoręczne zarządzanie pamięcią staje się zbyt skomplikowane to lepiej go nie robić i powierzyć tą funkcję .NET, który zawiera automatyczny odśmiecacz [garbage collector].
Można śmiało zakładać, że tablica jest niszczona wtedy, kiedy wychodzi z zakresu widzialności [scope], czyli kiedy sterowanie wychodzi z bloku kodu, w którym zmienna tablicowa została zdefiniowana i kiedy nie istnieją inne kopie referencji do danych w tej tablicy. Jak kopie referencji mogą istnieć po zakończeniu bloku kodu który tę referencję stworzył? Mogą zostać zwrócone na zewnątrz tego bloku. Będzie o tym w następnym rozdziale pod tytułem Funkcje, czyli metody.
Tablica vs. Lista
Dla tablic istotne jest to, że tablica pozwala zapamiętać w jednym miejscu (pod jedną nazwą zmiennej) wiele podobnych rzeczy (tego samego typu) pod warunkiem, że z góry wiemy ile ich będzie (albo znamy ich maksymalną możliwą ilość) i nie jest to ilość zbyt wielka (nie więcej niż – powiedzmy miliard). Jeśli nie wiemy z góry ile będzie elementów tablicy, ani nie znamy ich maksymalnej liczby, to zamiast tablicy używamy listy (która też jest wewnętrznie tablicą, ale taką, że sama zwiększa swój rozmiar wtedy, kiedy tylko jest to potrzebne, co praktycznie oznacza, że o jej rozmiar nie musimy dbać).
Zarówno tablica, jak i lista to przykłady struktur danych, a konkretnie kolekcji. Znane są też inne struktury danych, jak stos, kolejka, kolejka priorytetowa, różne rodzaje drzew i najbardziej ogólnych struktur, czyli grafów. Wszystkie one mają właściwe sobie zastosowania i implementacje (nie wszystkie są jednak zawarte w FCL). Często okazuje się, że nawet skomplikowane logicznie struktury danych można wewnętrznie przechowywać po prostu w tablicach, właśnie ze względu na ich szybkość i prostotę obsługi.
Klasa System.Array
Pomimo, że w kodzie języka C# nie deklarujemy tego faktu jawnie, klasa System.Array jest domyślnie klasą bazową dla każdego typu tablicowego (podobnie jak System.Enum jest domyślną klasą bazową dla typu wyliczeniowego). Oznacza to, że każda tablica dziedziczy coś po typie Array. Oto lista dziedziczonych metod i właściwości:
Length | Właściwość zwracająca ilość elementów w tablicy |
Clone |
Klonowanie tablicy (tablice są klonowalne, bo implementują interfejs ICloneable), czyli tworzenie referencji do nowej tablicy o takich samych elementach. Potem elementy tych tablic zmieniają się niezależnie.
var a = new int[] { 3, 4, 5 }; int[] b = a; // reference copy int[] c = (int[])a.Clone(); // shallow copy Console.WriteLine(Object.ReferenceEquals(b, a)); // True Console.WriteLine(Object.ReferenceEquals(c, a)); // False |
CopyTo | Kopiowanie danych do innej tablicy… |
Tablice wielowymiarowe
Zwykła tablica ma jeden wymiar. Matematycznie odpowiada wektorowi. Można też tworzyć tablice o 2, 3 i więcej wymiarach. Dwuwymiarowe tablice odpowiadają macierzom.
Tablice tablic
Z jednej strony wiemy, że każda tablica jest obiektem. Z drugiej strony wiemy, że tablice zawierają obiekty. Czy tymi obiektami mogą być też tablice? Oczywiście że tak.
Ćwiczenie 1
Przejdź przy pomocy pętli po elementach tablicy dwuwymiarowej w podanej na poniższym rysunku kolejności wstawiając do nich kolejne liczby naturalne.
1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 |
Rozwiązanie 1
var t = new int[6, 6]; int n = 1; for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { t[i, j] = n++; } }
Zwróć uwagę, że przy takim zapisie indeks i odpowiada za numer wiersza, a indeks j za numer kolumny. Równie dobrze moglibyśmy się umówić odwrotnie. To, co wybierzemy zależy od nas, ale musimy się tej konwencji trzymać konsekwentnie w całym dalszym programie. Poniżej pokazujemy jak w tym przypadku wyglądają indeksy elementów.
[0,0] | [0,1] | [0,2] | [0,3] | [0,4] | [0,5] |
[1,0] | [1,1] | [1,2] | [1,3] | [1,4] | [1,5] |
[2,0] | [2,1] | [2,2] | [2,3] | [2,4] | [2,5] |
[3,0] | [3,1] | [3,2] | [3,3] | [3,4] | [3,5] |
[4,0] | [4,1] | [4,2] | [4,3] | [4,4] | [4,5] |
[5,0] | [5,1] | [5,2] | [5,3] | [5,4] | [5,5] |
Aby zobaczyć efekt wypiszemy tablicę t na konsoli:
for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { Console.Write(t[i, j]); Console.Write(' '); } Console.WriteLine(); }
Ćwiczenie 2
Przejdź przy pomocy pętli po elementach tablicy dwuwymiarowej w podanej na poniższym rysunku kolejności wstawiając do nich kolejne liczby naturalne.
1 | 7 | 13 | 19 | 25 | 31 |
2 | 8 | 14 | 20 | 26 | 32 |
3 | 9 | 15 | 21 | 27 | 33 |
4 | 10 | 16 | 22 | 28 | 34 |
5 | 11 | 17 | 23 | 29 | 35 |
6 | 12 | 18 | 24 | 30 | 36 |
Rozwiązanie 2
var t = new int[6, 6]; int n = 1; for (int j = 0; j < 6; j++) { for (int i = 0; i < 6; i++) { t[i, j] = n++; } }
albo inaczej (oba sposoby są równoważne w tym sensie, że uzyskujemy ten sam efekt)
var t = new int[6, 6]; int n = 1; for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { t[j, i] = n++; } }
Ćwiczenie 3 (transpozycja macierzy)
Przechodząc po elementach tablicy z ćwiczenia 2 uzyskaj tablicę z ćwiczenia 1.
Rozwiązanie 3
for (int i = 0; i < 5; i++) { for (int j = i + 1; j < 6; j++) { int z = t[i, j]; t[i, j] = t[j, i]; t[j, i] = z; } }
Ćwiczenie 4 (po skosie)
Przejdź przy pomocy pętli po elementach tablicy dwuwymiarowej w podanej na poniższym rysunku kolejności wstawiając do nich kolejne liczby naturalne.
1 | 3 | 6 | 10 | 15 | 21 |
2 | 5 | 9 | 14 | 20 | 26 |
4 | 8 | 13 | 19 | 25 | 30 |
7 | 12 | 18 | 24 | 29 | 33 |
11 | 17 | 23 | 28 | 32 | 35 |
16 | 22 | 27 | 31 | 34 | 36 |
Rozwiązanie 4 (przykładowe)
var t = new int[6, 6]; int n = 1; for (int i = 0; i < 6; i++) { for (int j = 0; j <= i; j++) { t[i - j, j] = n++; } } for (int j = 1; j < 6; j++) { for (int i = j; i < 6; i++) { t[5 + j - i, i] = n++; } }
Spróbuj wypełnić tablicę w podanej kolejności innym sposobem.
Ćwiczenie 5 (spirala)
Przejdź przy pomocy pętli po elementach tablicy dwuwymiarowej w podanej na poniższym rysunku kolejności wstawiając do nich kolejne liczby naturalne.
21 | 22 | 23 | 24 | 25 | 26 |
20 | 7 | 8 | 9 | 10 | 27 |
19 | 6 | 1 | 2 | 11 | 28 |
18 | 5 | 4 | 3 | 12 | 29 |
17 | 16 | 15 | 14 | 13 | 30 |
36 | 35 | 34 | 33 | 32 | 31 |
Rozwiązanie 5
var t = new int[6, 6]; int n = 1, x = 2, y = x; for (int k = 0; k < 3; k++) { for (int j = 0; j < 2 * k + 1; j++) t[x--, y] = n++; x++; y++; for (int j = 0; j < 2 * k + 1; j++) t[x, y++] = n++; x++; y--; for (int j = 0; j < 2 * k + 1; j++) t[x++, y] = n++; x--; y--; for (int j = 0; j < 2 * k + 1; j++) t[x, y--] = n++; }
Czy potrafisz wymyślić algorytm używający tylko trzech pętli? A tylko jednej pętli?
Ćwiczenie 6 (zygzak)
Przejdź przy pomocy jednej pętli po elementach tablicy dwuwymiarowej w podanej na poniższym rysunku kolejności wstawiając do nich kolejne liczby naturalne.
1 | 2 | 6 | 7 | 15 | 16 |
3 | 5 | 8 | 14 | 17 | 26 |
4 | 9 | 13 | 18 | 25 | 27 |
10 | 12 | 19 | 24 | 28 | 33 |
11 | 20 | 23 | 29 | 32 | 34 |
21 | 22 | 30 | 31 | 35 | 36 |
Rozwiązanie 6 (jedna pętla!)
var t = new int[6, 6]; int i = 0, j = 0, d = -1, start = 1, end = 6 * 6; do { t[i, j] = start++; t[5 - i, 5 - j] = end--; i += d; j -= d; if (i < 0) { i++; d = -d; } else if (j < 0) { j++; d = -d; } } while (start < end); if (start == end) { t[i, j] = start; }
Spróbuj samodzielnie uogólnić ten algorytm dla tablic niekwadratowych (tj. t.GetLength(0) != t.GetLength(1)).
Spis treści Poprzednia strona: Pętle Następna strona: Funkcje, czyli metody