Wykład 6. Operacje na łańcuchach znakowych

Transkrypt

Wykład 6. Operacje na łańcuchach znakowych
Wykład 6. Operacje na łańcuchach znakowych
1. Łańcuch znaków jako tablica
Do przechowania łańcucha znaków służy tablica znakowa.
W kolejnych pozycjach tej tablicy są pamiętane kolejne znaki.
Ostatnim znakiem musi być znak końca, czyli znak ASCII
o numerze 0.
Poniżej pokazano przykład deklaracji tablicy znakowej,
która może przechowywać łańcuch o maksymalnej długości 9
znaków. Tablica ma 10 elementów, bo musi jeszcze pomieścić
znak końca.
char s[10];
Deklarując taka tablicę, można do niej od razu wpisać jakiś
łańcuch, na przykład
char s[10]={'p','r','o','g','r','a','m','\0'};
Nie wolno wtedy pominąć końcowego znaku '\0'.
Prostszy sposób polega na przypisaniu do deklarowanej tablicy
odpowiedniej stałej łańcuchowej:
char s[R]= "program";
Końcowy znak ‘\0’ wpisze się teraz automatycznie.
char s[10];
s[0]
p
s[1]
r
s[2]
o
s[3]
g
s[4]
r
s[5]
a
s[6]
m
s[7]
\0
s[8]
s[9]
Rys. 1. Przykład tablicy przechowującej łańcuch znakowy
2
2. Pisanie i czytanie łańcuchów
2.1. Użycie printf i scanf
Łańcuch można wyprowadzić na ekran za pomocą instrukcji
printf. Jeżeli chcemy wyprowadzić tylko jawny tekst, to
umieszczamy go pomiędzy apostrofami, bez kodu formatującego. Do tekstu można dokleić znak, na przykład znak przejścia na nowy wiersz:
printf(”\nPodaj imie i nazwisko: ”);
Pisząc zawartość tablicy pamiętającej łańcuch, stosuje się kod
formatujący "%s", jak poniżej:
char wezwanie[40]=”Podaj nazwisko: ”;
printf(”%s”, wezwanie);
Łańcuch można odczytać z klawiatury za pomoc instrukcji
scanf z kodem formatującym "%s". Przy tym:
1. Przed nazwą tablicy, w której jest zapamiętywany łańcuch, nie ma operatora adresu, ponieważ nazwa tablicy
jest jej adresem.
2. Jeżeli czytamy łańcuch zawierający spację, to zostanie
zapamiętana tylko ta jego część, która poprzedza spację,
ponieważ spacja zostanie potraktowana jako separator.
Przykład użycia funkcji printf i scanf do wczytania danej,
którą jest nazwisko osoby:
char name[40];
printf(”Podaj nazwisko: ”);
scanf(”%s”,name)
2.2. Użycie puts i gets
Funkcje biblioteczne puts i gets służą do wypisywania i wczytywania łańcuchów.
Funkcje puts i gets nie wymagają kodów formatujących.
3
Funkcja puts wyprowadza na ekran łańcuch będący jej argumentem; następnie przesuwa kursor na początek następnej
linii ekranu.
Argument funkcji puts może być jawną stałą łańcuchową,
lub nazwą tablicy, która przechowuje łańcuch.
Funkcja gets odczytuje łańcuch z klawiatury i zapamiętuje
go w tablicy, której nazwa jest jej argumentem.
Funkcja gets odczytuje bez problemu także łańcuchy, zawierające spacje.
Przykład:
char name[40];
puts(„Podaj nazwisko: „);
gets(name);
Uwaga: Funkcji gets oraz scanf nie należy używać jedna po
drugiej. Może to powodować błędny odczyt danych.
3. Odwoływanie się do znaków łańcucha
Do elementów łańcucha odwołujemy się tak, jak do elementów
tablicy (przez indeks lub wskaźnik).
Długość łańcucha pamiętanego w tablicy może być mniejsza,
niż rozmiar tablicy. Do określenia długości łańcucha aktualnie
pamiętanego w tablicy służy funkcja biblioteczna strlen, opisana w zbiorze nagłówkowym <string.h>.
Przykładzie poniżej pokazuje dwie różne realizacje funkcji,
która zlicza liczbę wystąpień znaku z w łańcuchu s.
Funkcja ile_z1 stosuje pętlę for ze zmienną licznikową j,
która służy jako indeks kolejnych znaków łańcucha. Liczba
kroków jest wyznaczona przez funkcję strlen.
Funkcja ile_z2 wykorzystuje pętlę while i wskaźnikowe
odwołania do elementów. Warunek działania pętli to po prostu
*(s+j). Gdy j-ty znak jest znakiem końca o wartości liczbowej
zero, instrukcja while przestaje działać i stan licznika ile zostaje przekazany przez return.
4
Przykład: Funkcje, zwracające liczbę znaków z w łańcuchu s.
int ile_z1(char z, char s[])
{
int j, ile=0;
for(j=0;j<strlen(s);j++)
if(s[j]==z) ile++;
return ile;
}
int ile_z2(char z, char *s)
{
int j=0, ile=0;
while(*(s+j))
{
if(*(s+j)==z) ile++;
j++;
{
return ile;
}
4. Kopiowanie łańcuchów
W języku C++ jest specjalna funkcja biblioteczna strcpy, przeznaczona do kopiowania łańcuchów (zbiór naglówkowy
<string.h>).
Instrukcja wywołania funkcji strcpy ma dwa argumenty.
Pierwszy argument to nazwa zmiennej tablicowej, do której
kopiujemy łańcuch. Drugi argument to kopiowany łańcuch.
Ten drugi argument może mieć postać jawnej stałej łańcuchowej, lub nazwy tablicy, która pamięta kopiowany łańcuch.
Na przykład, chcąc zapisać w tablicy name1 imię ”Jacek”, a
w tablicy name2 imię ”Beata”, użyjemy instrukcji:
char name1[20],name2[20],bufor[20];
strcpy(name1,”Jacek”);
strcpy(name2,”Beata”);
Zamieńmy teraz miejscami zawartość tablic name1 oraz
name2. Będzie to wymagało trzech instrukcji:
5
strcpy(bufor,name1);
strcpy(name1,name2);
strcpy(name2,bufor);
5. Funkcje biblioteczne operujące na łańcuchach
W języku C++ występuje kilka funkcji bibliotecznych, zdefiniowanych w zbiorze nagłówkowym <string.h>, które są przeznaczone do operacji na łańcuchach znakowych. Zestawiono je
tabeli 1.
Tabela 1. Funkcje biblioteczne operujące na łańcuchach
Instrukcja wywołania
char *p=strcat(s1,s2);
strcpy(s,s1);
strncpy(s,s1,n);
int d=strlen(s);
int d=strcmp(s1,s2);
int d=stricmp(s1,s2);
int d=strncmp(s1,s2,n);
char *p=strstr(s,s1);
Działanie funkcji
Skleja łańcuch s1 z łańcuchem s2.
Zwraca wskaźnik na łańcuch wynikowy
Kopiuje łańcuch s1 do łańcucha s.
Argument s1 może być także stałą łańcuchową.
Kopiuje n początkowych znaków łańcucha s1
do łańcucha s.
Zwraca aktualną długość łańcucha s,
bez uwzględnienia znaku końca.
Porównuje łańcuchy s1 i s2. Zwraca wartość:
= = 0, gdy łańcuchy są jednakowe,
> 0, gdy s1 jest alfabetycznie większy od s2,
< 0, gdy s2 jest alfabetycznie mniejszy od s1.
Porównuje łańcuchy s1 i s2 bez rozróżniania
dużych i małych liter.
Porównuje tylko n początkowych znaków
łańcuchów s1 i s2.
Zwraca wskaźnik na początek pierwszego
wystąpienia podłańcucha s1 w łańcuchu s.
Gdy s nie zawiera s1, zwraca wartość NULL.
Następujące dwa przykłady ilustrują niektóre możliwe zastosowania funkcji łańcuchowych.
W przykładzie poniżej pokazano program z funkcją własną,
która z łańcucha usuwa wszystkie wystąpienia danego podłańcucha.
Opis działania funkcji usun: W każdym kroku pętli while instrukcja strstr(p,p1) sprawdza, czy w łańcuchu p występuje
podłańcuch p1. Jeżeli tak nie jest, pętla ulega przerwaniu za
6
pomocą instrukcji break i funkcja kończy
W przeciwnym przypadku, za pomocą instrukcji
strcpy(pos,pos+d);
działanie.
jest usuwane kolejne wystąpienie podłańcucha p1 (d jest długością podłańcucha p1). Mechanizm działania funkcji strcpy
przy usuwaniu fragmentu łańcucha wyjaśnia rysunek 9.2.
Pierwszy argument funkcji nie zawsze musi być nazwą łańcucha, czyli wskaźnikiem na początek łańcucha s[0]. Na rys. 9.2
wskaźnik pos wskazuje na element s[4], natomiast wskaźnik
pos+3 wskazuje na element s[7]. Ponieważ pos jest pierwszym
argumentem funkcji strcpy, a pos+3 jest jej drugim argumentem, zostaną przekopiowane wszystkie znaki z pozycji nr 7, 8,
9, 10, 11 na pozycje nr 4, 5, 6, 7, 8. Poprzednia zawartość pozycji 4, 5, 6, które zajmował usuwany podłańcuch ”big”, zostanie tym samym wymazana.
Stan początkowy
s
s[0]
T
s[1]
s[2]
h
e
s[3]
pos
s[4]
s[5]
b
i
s[6]
pos+3
s[7]
g
s[8]
s[9]
s[10]
s[11]
c
a
t
\0
Stan po wykonaniu instrukcji: strcpy(pos,pos+3);
T
h
e
c
a
t
\0
Rys. 2. Przykład użycia funkcji strcpy do usunięcia fragmentu łańcucha
Przykład: Usuwanie z łańcucha wszystkich wystąpień
podłańcucha
#include <stdio.h>
#include <conio.h>
#include <string.h> //dla strstr,strcpy,strlen
void usun(char[],char[]);
7
int main ()
{
char s1[81];
char s[81]=
"Jak sie nie ma, co sie lubi, to sie lubi, co sie ma.";
printf("\nPierwotna postac lancucha:\n%s\n",s);
printf("\nPodaj usuwany podlancuch: ");
gets(s1);
printf("\nKolejne postacie lancucha:\n");
usun(s,s1);
printf("\nKoncowa postac lancucha:\n%s\n",s);
getch();
return 0;
}
void usun(char s[],char s1[])
{
char * pos;
int d=strlen(s1);
while(1)
{
pos=strstr(s,s1);
if(pos==NULL) break;
strcpy(pos,pos+d);
puts(s);
}
}
Poniżej pokazano wydruk uzyskany przy usuwaniu podłańcucha ”sie”. Dzięki umieszczeniu w pętli while instrukcji puts
można obserwować, jak w kolejnych krokach algorytmu zmienia się postać łańcucha.
Pierwotna postac lancucha:
Jak sie nie ma, co sie lubi, to sie lubi, co sie ma.
Podaj szukany podlancuch: sie
Kolejne postacie lancucha:
Jak nie ma, co sie lubi, to sie lubi, co sie ma.
Jak nie ma, co lubi, to sie lubi, co sie ma.
Jak nie ma, co lubi, to lubi, co sie ma.
Jak nie ma, co lubi, to lubi, co ma.
Koncowa postac lancucha:
Jak nie ma, co lubi, to
lubi, co
ma.
8
Następną ilustrację zastosowania bibliotecznych funkcji
łańcuchowych stanowi pokazana w przykładzie poniżej funkcja
własna licz, która znajduje liczbę wystąpień podłańcucha
w łańcuchu.
Przykład: Znajdowanie liczby wystąpień podłańcucha w łańcuchu.
int licz(char *p,char *p1)
{
int n=strlen(p),n1=strlen(p1);
int j=0,w=0;
while(j<n-n1+1)
if(strncmp(p+j,p1,n1)) j++;
else
{
w++;
j+=n1;
}
return w;
}
Opis działania funkcji licz: Funkcja ma dwa argumenty: Łańcuch p i szukany podłańcuch p1. Jako licznik wystąpień zastosowano zmienną w, a jako licznik indeksów – zmienną j.
W pętli while za pomocą instrukcji:
strncmp(p+j,p1,n1)
porównujemy n1 pozycji łańcucha p, poczynając od pozycji o
adresie p+j (a więc fragment p, rozpoczynający się od znaku
p[j]), z podłańcuchem p1, przy czym n1 jest długością p1. Jeżeli nie ma zgodności, to funkcja zwraca wartość różną od zera, więc warunek po if jest spełniony. Następuje inkrementacja
indeksu j++, dzięki czemu w następnym kroku porównywany
fragment łańcucha będzie przesunięty o jedną pozycję
w prawo. W przypadku, gdy badany fragment łańcucha p jest
identyczny z szukanym podłańcuchem p1, funkcja strncmp
zwraca zero, więc warunek po if nie jest spełniony, wobec czego stan licznika w zwiększa się o jeden. Jednocześnie licznik
9
indeksów j zwiększa stan o wartość n1, aby następny badany
fragment p rozpoczynał się za znalezionym podłańcuchem.
Warunkiem zakończenia działania pętli jest osiągnięcie
przez zmienną indeksową wartości j=n-n1. Dalsze zwiększanie
indeksu nie miałoby sensu, bo szukany podłańcuch nie mógłby
się zmieścić w krótszym od tej wartości granicznej końcowym
fragmencie łańcucha.
O
N
N
I
N
E
I
N
N
I
E
E
I
N
E
I
E
N
W
I
I
N
E
I
N
E
E
I
N
,
E
I
N
E
I
N
N
I
E
E
I
N
E
I
E
.
\0
Rys. 3. Ilustracja metody zliczania wystąpień podłańcucha w łańcuchu
6. Tablice łańcuchów
6.1. Przykładowe operacje na tablicy łańcuchów
W programie poniżej pokazano sposób definiowania tablicy
łańcuchów. Łańcuch jest tablicą, więc tablica t, która zawiera
M łańcuchów N-znakowych, jest tablicą dwuwymiarową:
char t[M][N];
Tablicę taką można wstępnie zapełnić przy deklarowaniu,
wpisując wartości poszczególnych wierszy w cudzysłowach
jako stałe łańcuchowe.
Pokazany program korzysta z dwóch funkcji własnych,
operujących na tablicy łańcuchów.
Opis funkcji sortuj: Funkcja porządkuje wiersze w kolejności
alfabetycznej. Zastosowano znany algorytm bąbelkowy, jednak
10
w jego implementacji łańcuchowej występują dwie ważne cechy, różniące ją od postaci używanej do sortowania tablic liczbowych:
1. Do porównania wierszy nie wolno stosować operatorów
relacyjnych > lub <, które nie działają na tablicach. Zamiast tego stosujemy funkcję strcmp.
2. Do kopiowania wierszy tablicy przy ich zamianie miejscami nie można użyć operatora przypisania, bo nie
działa on na tablicach. Zamiast tego stosujemy funkcję
biblioteczną strcpy.
Opis funkcji pisz_tab: Funkcja jest przeznaczona do wyprowadzania zawartości tablicy na ekran. Łańcuchy pamiętane w tablicy są drukowane jako kolejne wiersze tekstu na
ekranie. W zależności od wartości argumentu c, funkcja
może wyprowadzać pamiętany w tablicy tekst na trzy sposoby: a/ Dokładnie tak, jak jest pamiętany, b/ używając wyłącznie małych liter, c/ używając wyłącznie dużych liter.
Do zmiany liter z małych na duże lub odwrotnie służą
funkcje biblioteczne toupper, tolower, zdefiniowane
w zbiorze nagłówkowym <ctype.h>. Ponadto, za pomocą
argumentu p określamy, czy tekst ma być pisany znak po
znaku, czy też szeroko, z dodatkową spacją po każdym
znaku. Przykładowy wydruk programu przedstawia rys. 4.
Ze względu na konieczność modyfikowania pojedynczych znaków, tablica jest tutaj wyprowadzana znak po
znaku, za pomocą dwóch zagnieżdżonych instrukcji for. Na
ogół jednak wystarcza do tego pojedyncza pętla for, która
wyprowadza tablicę łańcuchów wiersz po wierszu, jak poniżej:
void pisz_tab(char t[M][N])
{
for(j=0;j<M;j++)
printf(”%s\n”,t[j]);
}
11
Przykład: Sortowanie i wypisywanie tablicy łańcuchów
#include
#include
#include
#include
<stdio.h>
<conio.h>
<string.h>
<ctype.h>
//dla toupper, tolower
const int M=4, N=12;
void pisz_tab(char[][N],int,int);
void sortuj(char[][N]);
int main(){
char t[M][N]={"Tomek",
"Jarek",
"Zbyszek",
"Agata"};
puts("Tablica przed sortowaniem:\n");
pisz_tab(t,0,0);
sortuj(t);
puts("\n\nTablica po sortowaniu:\n");
pisz_tab(t,0,0);
puts("\n\nTablica malymi literami:\n");
pisz_tab(t,-1,0);
puts("\n\nTablica wielkimi literami:\n");
pisz_tab(t,1,0);
puts("\nTablica szeroko:\n");
pisz_tab(t,0,1);
getch();
return 0;
}
void sortuj(char t[][N])
{
int i,j;
char buf[N];
for (j=0; j<M-1; j++)
for (i=j+1; i<M; i++)
if (strcmp(t[j],t[i])>0)
{
strcpy(buf,t[i]);
strcpy(t[i],t[j]);
strcpy(t[j],buf);
}
}
12
void pisz_tab(char s[][N],int c,int p){
int j,k;
for (j=0;j<M;j++)
{
for (k=0; k<strlen(s[j]); k++)
{
if (c==0) printf("%c",s[j][k]);
else if (c>=1)
printf("%c",toupper(s[j][k]));
else
printf("%c",tolower(s[j][k]));
if (p) printf("%c",'\x20');
}
printf("\n");
}
}
Tablica przed sortowaniem:
Tomek
Jarek
Zbyszek
Agata
Tablica po sortowaniu:
Agata
Jarek
Tomek
Zbyszek
Tablica malymi literami:
agata
jarek
tomek
zbyszek
Tablica wielkimi literami:
AGATA
JAREK
TOMEK
ZBYSZEK
Tablica szeroko:
A
J
T
Z
g
a
o
b
a
r
m
y
t
e
e
s
a
k
k
z e k
Rys. 4. Wydruk uzyskany po wykonaniu programu
13
6.2. Tablica wskaźników na łańcuchy znakowe
Użycie znakowej tablicy dwuwymiarowej nie jest jedynym
sposobem pamiętania łańcuchów znakowych. Można posłużyć
się również tablicą wskaźników na znaki, inicjując ją tak, jak
poniżej:
char *tab[6]={"ADAM",
"TADEUSZ",
"EWA",
"GRZEGORZ",
"IGNACY",
"ZENON"};
Po takiej deklaracji, kompilator umieści inicjowane łańcuchy
w pamięci bezpośrednio po sobie, niezależnie od ich długości.
Po znaku końca pierwszego łańcucha znajdzie się początkowy
znak drugiego łańcucha itd. Dzięki temu nie ma niewykorzystanych miejsc i znacznie zmniejsza się rozmiar pamięci, potrzebnej do zapamiętania tekstu. Jednocześnie z lokowaniem
łańcuchów w pamięci, ich adresy są umieszczane w kolejnych
pozycjach tablicy tab. Dlatego za pomocą elementów tej tablicy można odwoływać się do kolejnych łańcuchów w pamięci.
Ilustracją opisanej metody jest rysunek 5. Dla porównania pokazano, w jaki sposób są zapamiętane łańcuchy w dwuwymiarowej tablicy znakowej. W rozpatrywanym przykładzie użycie
tablicy wskaźników zmniejszyło zajęty obszar pamięci o 33
bajty w porównaniu z obszarem pamięci zajmowanym przez
tablicę dwuwymiarową z rysunku 6.
Program pokazany w przykładzie poniżej operuje na grupie
łańcuchów, posługując się tablicą wskaźników na te łańcuchy.
W przedstawionym programie zdefiniowano dwie funkcje własne: pisztab oraz sortuj.
14
Przed
sortow.
Po
sortow.
tab[0]
Adr0
Adr2
Adr0
A
D
A
M
\0
tab[1]
Adr1
Adr0
Adr1
T
A
D
E
U
S
Z
\0
tab[2]
Adr2
Adr5
Adr2
E
W
A
\0
tab[3]
Adr3
Adr4
Adr3
G
R
Z
E
G
O
R
Z
tab[4]
Adr4
Adr1
Adr4
I
G
N
A
C
Y
\0
tab[5]
Adr5
Adr3
Adr5
Z
E
N
O
N
\0
\0
Rys. 5. Tablica łańcuchów deklarowana jako char * tab[6]
tab[0]
A
D
A
M
\0
tab[1]
T
A
D
E
U
S
Z
\0
tab[2]
E
W
A
\0
tab[3]
G
R
Z
E
G
O
R
Z
tab[4]
I
G
N
A
C
Y
\0
tab[5]
Z
E
N
O
N
\0
\0
Rys. 6. Tablica łańcuchów deklarowana jako char tab [6][12]
Opis funkcji pisz_tab:
W pętli for odwołujemy się przez indeks j do kolejnych pozycji
tablicy tab, Adres j–tego łańcucha tab[j] jest argumentem
funkcji puts, która wyprowadza ten łańcuch na ekran, po czym
przenosi kursor na początek następnego wiersza ekranu.
Opis funkcji sortuj:
Funkcja ta porządkuje łańcuchy według ich długości, stosując
algorytm bąbelkowy. ale łańcuchy w czasie sortowania nie
zmieniają swojego położenia w pamięci. Zmiany dotyczą tylko
położenia elementów tablicy wskaźników. Jak widać na rysunku 5, po zakończeniu sortowania kolejne pozycje tablicy tab
wskazują na coraz dłuższe łańcuchy. Ponieważ zamieniamy
miejscami nie łańcuchy, lecz wskaźniki, możemy posłużyć się
operatorem przypisania. W warunku po if porównujemy ze so-
15
bą nie łańcuchy, lecz długości łańcuchów. Te długości, zwracane przez funkcję strlen, są liczbami, dlatego do porównania
można użyć operatora większości.
Obie opisane funkcje mają argument tablicowy zapisany
jako char ** tab. Wymaga to krótkiego wyjaśnienia. Ponieważ
nie pracujemy na tablicy znaków, ale na tablicy wskaźników na
znaki, argument tablicowy zamiast postaci char tab[ ] musi
mieć postać char * tab[ ]. Tę postać argumentu można by z
powodzeniem zastosować w definicjach obu funkcji. Jednak
jak pamiętamy, tab[ ] można równoważnie zapisać jako *tab.
W ten sposób powstał użyty w przykładzie 9.5 zapis argumentu
tablicowego jako char ** tab. Druga gwiazdka pokazuje, że
mamy argument tablicowy, a pierwsza – że chodzi o tablicę
wskaźników na znaki.
Słowo const użyte przed argumentem w funkcji pisz_tab
powoduje, że argument nie będzie mógł ulegać zmianom
w czasie działania funkcji – w ten sposób często zabezpiecza
się argumenty wejściowe. W przypadku tej konkretnej funkcji
nie ma to znaczenia.
Na rysunku 7 widać wydruk, uzyskany na ekranie po uruchomieniu opisanego programu.
Przykład: Operacje na łańcuchach za pośrednictwem
tablicy wskaźników
#include <stdio.h>
#include <conio.h>
#include <string.h>
const int M=6;
void pisz_tab(const char **);
void sortuj(char **);
16
int main()
{
char *tab[M]={ "ADAM",
"TADEUSZ",
"EWA",
"GRZEGORZ",
"IGNACY",
"ZENON"};
pisz_tab(tab);
sortuj(tab);
pisz_tab(tab);
getch();
return 0;
}
void pisz_tab(const char **tab)
{
int j;
for (j=0; j<M; j++)
puts(tab[j]);
puts("\n");
}
void sortuj(char **tab)
{
int j,k;
char *buf;
for (j=0; j<M-1; j++)
for (k=j+1; k<M; k++)
if
(strlen(tab[j])>strlen(tab[k]))
{
buf=tab[k];
tab[k]=tab[j];
tab[j]=buf;
}
}
17
ADAM
TADEUSZ
EWA
GRZEGORZ
IGNACY
ZENON
EWA
ADAM
ZENON
IGNACY
TADEUSZ
GRZEGORZ
Rys. 7. Wydruk otrzymany w wyniku działania programu

Podobne dokumenty