Tablice – podstawowe operacje na tablicach

Transkrypt

Tablice – podstawowe operacje na tablicach
27.03.2014r.
Tablice –
podstawowe
operacje na
tablicach
Tablica - definicja
Tablica (ang. array) lub wektor (ang. vector) jest
złożoną strukturą danych (ang. compound data
structure) zbudowaną z ciągu elementów tego
samego typu. W pamięci komputera elementy
tablicy są ułożone kolejno jeden obok drugiego.
Dostęp do elementu odbywa się poprzez numer
zwany indeksem. Na podstawie indeksu, rozmiaru
elementu oraz adresu początku tablicy komputer
oblicza adres elementu i w ten sposób uzyskujemy
do niego dostęp.
We współczesnych językach programowania
tablice są stosowane powszechnie do
przechowywania danych podobnego rodzaju.
Przy ich pomocy można zapisywać ciągi
liczbowe, wyniki pomiarów różnych wielkości
oraz tworzyć złożone bazy danych. Liczba
zastosowań tablic jest w zasadzie
ograniczona naszą wyobraźnią. Podstawową
zaletą tablic jest prostota przetwarzania ich
elementów. Dzięki dostępowi poprzez
indeksy, elementy tablic daje się łatwo
przetwarzać w pętlach iteracyjnych.
Deklarowanie tablic
Deklarację tablicy umieszczamy w języku C++ na liście
deklaracji zmiennych. Składnia jest następująca:
typ_danych nazwa_tablicy[liczba_elementów];
typ_danych
–
określa rodzaj informacji
przechowywanych przez deklarowane zmienne
nazwa_tablicy –
tworzona jest wg zwykłych reguł tworzenia
nazw zmiennych w języku C++
Liczba_elementów
– określa, ile elementów danego typu
przechowuje tablica
W języku C++ indeksy tablic rozpoczynają się
od 0. Ma to sens, ponieważ nazwa tablicy jest
traktowana zawsze jak adres początku
obszaru pamięci, w którym tablica
przechowuje swoje elementy. Naturalne
zatem jest, iż pierwszy element leży właśnie
pod adresem tablicy. Stąd jego indeks wynosi
0, czyli nic nie musimy dodawać do adresu
początku tablicy, aby uzyskać dostęp do jej
pierwszego elementu.
Przykłady deklaracji tablic w C++ :
int a[3]; // tablica zawierająca 3 elementy typu int
double x[10]; // tablica przechowująca 10 liczb typu double
char c[6]; // tablica przechowująca 6 wartości znakowych
W powyższym przykładzie zadeklarowano trzy tablice a, x
oraz c. Posiadają one elementy o następujących indeksach:
Tablica a : a[0] a[1] a[2] - 3 elementy typu integer
Tablica x : x[0] x[1] x[2] x[3] x[4] x[5] x[6] x[7] x[8] x[9] 10 elementów typu double
Tablica c : c[0] c[1] c[2] c[3] c[4] c[5] - 6 elementów typu
char
Zwróć uwagę, iż tablica nie posiada elementu o indeksie
równym ilości elementów. Zatem jeśli zadeklarujemy np.
tablicę:
double Tlk[168];
to jej ostatnim elementem jest Tlk[167], a nie Tlk[168].
Odwołanie się w programie do Tlk[168] jest błędem,
Inicjalizacja tablic
Często zdarza się, iż chcemy utworzyć tablicę z zadaną z góry
zawartością (np. tablica zawierająca początkowe liczby pierwsze).
Składnia inicjalizacji tablicy w języku C++ jest następująca:
typ_elementów nazwa_tablicy =
{lista_wartości_dla_kolejnych_elementów};
Zwróć uwagę, iż nie musimy podawać liczby elementów. Kompilator
utworzy tyle elementów, ile podamy dla nich wartości na liście
inicjalizacyjnej. Poniższy przykład tworzy tablicę 10 liczb
całkowitych i wypełnia ją kolejnymi liczbami Fibonacciego.
int fib[] = (0,1,1,2,3,5,8,13,21,33);
Tablice dynamiczne
Zdarza się, iż w trakcie pisania programu nie wiemy, ile dokładnie
elementów będzie zawierała używana w tym programie tablica. W takim
przypadku problem tworzenia tablicy możemy rozwiązać na dwa
sposoby:
1. Utworzyć tablicę o maksymalnej, przewidywanej liczbie elementów.
Rozwiązanie nieefektywne ze względu na wykorzystanie pamięci. Jeśli w
typowych przypadkach wykorzystujemy małą liczbę elementów tablicy,
to i tak musimy rezerwować założoną ilość komórek dla przypadku
pesymistycznego, który pojawia się bardzo rzadko, ale jest
prawdopodobny.
2. Utworzyć tablicę dynamicznie o tylu komórkach, ile w danej chwili
jest nam potrzebne. Po wykorzystaniu, tablicę dynamiczną usuwamy,
zwalniając w ten sposób zajmowany przez nią obszar pamięci, który
teraz można wykorzystać do innych celów – np. dla nowej tablicy
dynamicznej.
W celu utworzenia w języku C++ tablicy dynamicznej, tworzymy
zmienną wskaźnikową na typ danych, które mają być przechowywane
w tablicy:
typ_elementów * nazwa_tablicy_dynamicznej;
Zmienna wskaźnikowa (ang. pointer variable) nie przechowuje
danych tylko adres obszaru pamięci komputera, w którym te dane
się znajdują. Deklarację zmiennej wskaźnikowej zawsze
poprzedzamy znakiem gwiazdki. W poniższym przykładzie tworzymy
trzy wskaźniki a, b i c do danych typu double (czyli do obszaru
pamięci, w którym będą przechowywane liczby zmiennoprzecinkowe
o podwójnej precyzji):
double * a, * b, * c;
Pamięć rezerwujemy operatorem new i adres zarezerwowanego obszaru
umieszczamy w zmiennej wskaźnikowej:
nazwa_tablicy_dynamicznej = new typ_elementów[liczba_elementów];
Poniższy przykład tworzy trzy tablice dynamiczne, w których będzie można
przechowywać odpowiednio 10, 100 i 1000 elementów typu double:
a = new double[10]; // elementy od a[0] do a[9]
b = new double[100]; // elementy od b[0] do b[99]
c = new double[1000]; // elementy od c[0] do c[999]
Po tej operacji do elementów tablic a, b i c odwołujemy się w
zwykły sposób za pomocą indeksów. Istnieje również
alternatywna metoda, wykorzystująca fakt, iż zmienne a, b i c
są wskaźnikami. W języku C++ dodanie do wskaźnika liczby
całkowitej powoduje obliczenie adresu elementu o indeksie
równym dodawanej liczbie. Zatem wynik takiej operacji jest
również wskaźnikiem:
Tablica
a[2] = 10.54;
cout << a[2] << endl;
Wskaźnik
*(a + 2) = 10.54;
cout << * (a + 2) << endl;
#include <iostream>
using namespace std;
int main()
{
double * a[2];
wskaźnik
// definiujemy
a[2] = new double; // przydzielamy
pamięć
* a[2] = 10.54; // w przydzielonej
pamięci umieszczamy dane
cout << * a[2] << endl;
delete a[2];
// zwalniamy
przydzielony obszar pamięci
}
return 0;
W rzeczywistości zapis a[i] kompilator i tak przekształca
sobie na zapis * (a + i). Forma tablicowa jest tylko
uproszczeniem zapisu wskaźnikowego.
Tablice dynamiczne nie są automatycznie usuwane z pamięci,
jeśli utworzono je w funkcji. Dlatego po zakończeniu
korzystania z tablicy program powinien zwolnić zajmowaną
przez tablicę pamięć. Dokonujemy tego poleceniem delete w
sposób następujący:
delete [] nazwa_tablicy_dynamicznej;
W poniższym przykładzie zwalniamy pamięć
zarezerwowaną wcześniej na elementy tablic b i
c.
delete [] b; // usuwamy obszar wskazywany
przez b
delete [] c; // usuwamy obszar wskazywany
przez c
Należy również wspomnieć, iż Code::Blocks dopuszcza konstrukcję:
typ_elementów nazwa_tablicy[zmienna];
co pozwala na tworzenie statycznych tablic o liczbie elementów
podanej w zmiennej. Na przykład poniższa konstrukcja programowa
tworzy statyczną tablicę a o liczbie elementów odczytanej ze
strumienia wejściowego konsoli znakowej:
int n;
cin >> n;
double a[n];
Jednakże nie jest to zbyt standardowe rozwiązanie i może nie być
przenośne na inne kompilatory C++, dlatego odradzam używania go –
lepiej zastosować tablicę dynamiczną.
Tablice dynamiczne o dynamicznym
rozmiarze
Gdy operujemy na dynamicznych strukturach
danych, to często okazuje się, że taką strukturę
należy zwiększyć lub zmniejszyć w czasie działania
programu. Np. utworzyliśmy na początku algorytmu
dynamiczną tablicę 100 elementową, lecz dalej
okazało się, że w tablicy tej należy dodatkowo
umieścić jeszcze 50 kolejnych elementów. Co
możemy zrobić? Odpowiedź jest prosta –
dynamicznie zmienić rozmiar naszej struktury.
Zasada jest następująca:
Oprócz wskazania do tablicy tworzymy
dodatkową zmienną, która przechowuje
jej maksymalny rozmiar. Oznaczmy tę
zmienną przez nn. Każdą tablicę tworzymy
z pewnym zapasem komórek, oznaczmy go
przez z. Zmienna n przechowuje
informację o ilości zajętych komórek.
Gdy dodajemy element do tablicy, to najpierw zwiększamy n o
1 i sprawdzamy, czy warunek n >= nn jest spełniony. Jeśli tak,
to tablica zawiera za mało komórek, i należy zwiększyć jej
rozmiar. Operację tę przeprowadzamy następująco:
1. Rezerwujemy nowy obszar pamięci o rozmiarze nn + z i
zapamiętujemy jego adres w tymczasowej zmiennej.
2. Przepisujemy zawartość starego obszaru tablicy do
nowego.
3. Usuwamy stary obszar – w ten sposób system odzyska
pamięć.
4. Do wskaźnika tablicy przepisujemy adres ze zmiennej
tymczasowej.
5. Zmienną nn zwiększamy o z
Zwróć uwagę, że cała tablica zmieniła swoje
położenie w pamięci. Jeśli zatem posiadałeś
jakieś zmienne, które przechowywały adresy jej
elementów (wskaźniki), to teraz przestaną one
być aktualne, ponieważ wskazują stary obszar,
który już do tej tablicy nie należy (chociaż
przez pewien czas może zachowywać swoją
zawartość aż system zapisze go innymi danymi).
Należy to wziąć pod uwagę, gdy planujesz
wykorzystywanie dynamicznych tablic o
dynamicznym rozmiarze.
if(++n >= nn)
{
typ_danych * T = new typ_danych[nn+z];
for(int i = 0; i < nn; i++) T[i] = A[i];
delete [] A;
A = T;
nn += z;
}
Przy usuwaniu elementów z tablicy postępujemy
podobnie. Gdy usuniemy element, to rozmiar
tablicy n zostanie zmniejszony o 1. Sprawdzamy,
czy jest spełniony warunek n ≤ nn - 2z. Jeśli tak,
to tablica zajmuje zbyt duży obszar i
powinniśmy zmniejszyć jej rozmiar. Dlaczego w
nierówności użyliśmy 2z a nie po prostu z?
Chodzi o to, aby po zmniejszeniu rozmiaru
tablica wciąż posiadała zapas z komórek, co
zmniejszy ryzyko częstych przeładowań pamięci,
które są przecież kosztowne czasowo. Zmianę
rozmiaru tablicy wykonujemy podobnie jak przy
zwiększaniu liczby elementów:
if(--n <= nn-z-z)
{
typ_danych * T = new typ_danych[nn-z];
for(int i = 0; i < n; i++) T[i] = A[i];
delete [] A;
A = T;
nn -= z;
}
Wprowadzanie/wyprowadzanie
danych
Dane dla programu zwykle muszą być odczytywane ze
zewnętrznego źródła – konsoli lub pliku. W takim przypadku
nie wiemy z góry (tzn. w trakcie pisania programu) ile ich
będzie. Narzucającym się rozwiązaniem jest zastosowanie
tablic dynamicznych. Ze źródła danych odczytujemy
rozmiar tablicy, tworzymy tablicę dynamiczną o
odpowiednim rozmiarze, a następnie wczytujemy do jej
komórek poszczególne dane.
Poniżej podajemy sposoby odczytu zawartości tablicy z
konsoli. Sposób ten jest bardzo ogólny. Wykorzystanie
standardowego wejścia jako źródła danych daje nam kilka
możliwości wprowadzania danych:
I sposób
Dane podajemy bezpośrednio z
klawiatury. Sposób skuteczny i prosty
dla niedużego zbioru danych.
Jednakże przy większej ich liczbie
staje się bardzo uciążliwy.
II sposób
Skopiowanie danych poprzez schowek. Procedura
postępowania jest następująca:
– tworzymy w notatniku Windows (aplikacja zawsze pod
ręką) odpowiedni zbiór danych
– zbiór kopiujemy do schowka (zaznaczamy całość Ctrl-A
i naciskamy Ctrl-C)
– uruchamiamy program
– klikamy prawym przyciskiem myszki w pasek tytułowy
okna konsoli
– z menu kontekstowego wybieramy polecenie Edytuj →
Wklej
– gotowe
III sposób
Przekierowanie standardowego wejścia z konsoli na plik na dysku. W tym
przypadku program będzie pobierał dane z pliku, a nie z klawiatury. Aby to
uzyskać uruchamiamy program w oknie konsoli następująco:
nazwa_programu < nazwa_pliku_wejściowego
Na przykład nasz program nazywa się szukaj.exe, a plik nosi nazwę
dane.txt. Odpowiednie polecenie odczytu danych z pliku przez nasz
program wygląda następująco:
szukaj < dane.txt
To rozwiązanie umożliwia również zapis danych wynikowych nie na ekran
konsoli, lecz do pliku na dysku. W tym celu wystarczy wydać polecenie:
nazwa_programu > nazwa_pliku_wynikowego
Wejście i wyjście można przekierować w jednym poleceniu. Np. nasz
program szukaj może odczytać dane wejściowe z pliku dane.txt, a wyniki
swojej pracy umieścić w pliku wyniki.txt. W tym celu wydajemy takie oto
polecenie:
szukaj < dane.txt > wyniki.txt
Program
Program z pierwszego wiersza odczytuje
liczbę n określającą ilość danych. Z
następnych n wierszy odczytywane są
dane i umieszczane w tablicy dynamicznej.
Odczytane dane zostają następnie
wyświetlone jedna obok drugiej. Wypróbuj
z tym programem podane powyżej trzy
opcje dostarczania danych i
wyprowadzania wyników.
#include <iostream>
using namespace std;
int main()
{
int * T,n,i;
cin >> n;
T = new int[n];
for(i = 0; i < n; i++) cin >> T[i];
cout << endl;
for(i = 0; i < n; i++) cout << T[i] << " ";
cout << endl << endl;
delete [] T;
return 0;
}
Wypełniane tablicy
Czasami algorytm musi wstępnie wypełnić tablicę
określoną zawartością. Operację taką przeprowadza
się w pętli iteracyjnej, której zmienna licznikowa
przebiega przez wszystkie kolejne indeksy elementów.
Następnie wykorzystuje się zmienną licznikową jako
indeks elementu tablicy, w którym umieszczamy
określoną zawartość.
W poniższych przykładach zakładamy, iż w programie
zadeklarowano tablicę T o 100 elementach typu
integer. Indeksy elementów tablicy T są w zakresie od
0 do 99.
Wypełnianie liczbami parzystymi
począwszy od 2
...
for(i = 0; i < 100; i++)
T[i] = (i + 1) << 1;
...
Wypełnianie liczbami
nieparzystymi począwszy od 1
...
for(i = 0; i < 100; i++)
T[i] = 1 + (i << 1);
...
Wypełnianie liczbami
pseudolosowymi z przedziału <a,b>
#include <cstdlib>
#include <time.h>
...
srand((unsigned)time(NULL));
...
for(i = 0; i < 100; i++)
T[i] = a + rand() % (b - a + 1);
...

Podobne dokumenty