Ein Doppel-Axel in C (1) -- qsort, bsearch, lsearch
Ein Doppel-Axel in C (1) -- qsort, bsearch, lsearch
Ein Doppel-Axel in C (1) -- qsort, bsearch, lsearch
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
<strong>E<strong>in</strong></strong> <strong>Doppel</strong>-<strong>Axel</strong> <strong>in</strong> C (1) — <strong>qsort</strong>,<br />
<strong>bsearch</strong>, <strong>lsearch</strong><br />
<strong>Axel</strong>-Tobias Schre<strong>in</strong>er, Universität Ulm<br />
Kernighan und Ritchie’s fünftes Kapitel 1 ist für jeden C Programmierer natürlich<br />
Pflicht. Die Kür besteht bei System V offensichtlich dar<strong>in</strong>, Suchfunktionen nicht<br />
mehr selbst zu programmieren, sondern die verschiedenen Lösungen zu diesem<br />
Thema aus der Standard-Bücherei korrekt e<strong>in</strong>setzen zu können. In dieser und der<br />
nächsten Ausgabe der Sprechstunde möchte ich Beispiele für die Verwendung der<br />
Funktionen qsor t(), <strong>bsearch</strong>(), hsearch(), <strong>lsearch</strong>() und tsearch() zeigen und e<strong>in</strong><br />
bißchen ihre Stärken und Schwächen diskutieren.<br />
Das Problem wird durch die System V Interface Def<strong>in</strong>ition nicht unbed<strong>in</strong>gt<br />
vere<strong>in</strong>facht: zu den Suchfunktionen gibt es Beispiele, aber die s<strong>in</strong>d nicht immer<br />
korrekt programmiert. Bei ähnlicher Funktionalität ist außerdem die Benutzung der<br />
verschiedenen Funktionen ziemlich verschieden, und die Beschreibung gibt ke<strong>in</strong>en<br />
H<strong>in</strong>weis, welche Lösung und Problemstellung am besten harmonieren. Die<br />
eigentliche Schwierigkeit liegt aber dar<strong>in</strong>, daß die Funktionen zur Manipulation<br />
nahezu beliebiger Information e<strong>in</strong>gerichtet s<strong>in</strong>d, und daß dies natürlich mit Hilfe e<strong>in</strong>er<br />
recht ‘‘flexiblen’’ Interpretation von Zeigern erreicht wird.<br />
1 ...über Zeiger und Vektoren.
Sor tieren — ‘‘qsor t()’’<br />
qsor t() gab es ‘‘schon immer’’: mit dieser Funktion kann man die Elemente e<strong>in</strong>es<br />
beliebigen Vektors nach beliebigen Kriterien sortieren. Zu beachten ist, daß die<br />
Elemente selbst vertauscht werden (je nach Größe ist das aufwendig, denn es<br />
f<strong>in</strong>den nicht nur die m<strong>in</strong>imal notwendige Anzahl Tauschoperationen statt), und daß<br />
die Sortierung nicht stabil ist, das heißt, daß die ursprüngliche Reihenfolge von zwei<br />
gleichen Elementen nicht notwendigerweise erhalten bleibt — auch gleiche<br />
Elemente werden möglicherweise vertauscht. qsor t() hat folgende Deklaration:<br />
void <strong>qsort</strong>(base, nel, width, compar)<br />
char * base; /* Anfang des Vektors */<br />
unsigned nel, width; /* Anzahl Elemente, Groesse */<br />
<strong>in</strong>t (* compar)(); /* Vergleichsfunktion */<br />
Oft ist base der Name e<strong>in</strong>es Vektors — praktisch nie mit Elementen vom Typ char<br />
— und width ist die Elementgröße. <strong>E<strong>in</strong></strong>e e<strong>in</strong>fachere Schnittstelle, die auch die<br />
(unnötigen) Umwandlungen erzw<strong>in</strong>gt, könnte man zum Beispiel so def<strong>in</strong>ieren:<br />
void <strong>qsort</strong>();<br />
#def<strong>in</strong>e Qsort(base, nel, compar) \<br />
(<strong>qsort</strong>((char *) (base), /* Anfang der Tabelle */ \<br />
(unsigned) (nel), /* Anzahl Elemente */ \<br />
(unsigned) sizeof *(base), /* Elementgroesse */ \<br />
(<strong>in</strong>t (*)()) (compar))) /* Vergleichsfunktion */<br />
Bei e<strong>in</strong>em Vektor könnte man zwar die Anzahl der Elemente vom C Compiler<br />
berechnen lassen und so auf den Parameter nel auch noch verzichten, aber es wird<br />
sich bei unseren Beispielen zeigen, daß wir oft nur teilweise gefüllte oder mit Hilfe<br />
von Zeigern dynamisch angelegte Vektoren sortieren, bei denen der Compiler die
tatsächliche Anzahl der Elemente nicht feststellen kann.<br />
Im Makro Qsor t() wird die Elementgröße vom Compiler mit sizeof aus dem Typ<br />
der Anfangsadresse bestimmt. Die expliziten Umwandlungen provozieren zum<br />
Beispiel e<strong>in</strong>e Fehlermeldung, wenn weder e<strong>in</strong> Vektorname noch e<strong>in</strong> Zeiger als erstes<br />
Argument übergeben wird, oder wenn ke<strong>in</strong> Funktionsname oder Zeiger als drittes<br />
Argument steht. Sollte die Anzahl der Elemente versehentlich als long-Wert<br />
angegeben se<strong>in</strong>, wird sie stillschweigend umgewandelt. (Ähnliche Effekte werden<br />
<strong>in</strong> Zukunft die Funktions-Prototypen von ANSI-Standard-C erzielen.)<br />
Betrachten wir sicherheitshalber e<strong>in</strong> e<strong>in</strong>faches Beispiel auch für qsor t(): e<strong>in</strong>e<br />
kle<strong>in</strong>e Tabelle aus Namen und Zahlen soll je nach Wunsch sortiert werden. Hier ist<br />
das Hauptprogramm:<br />
#<strong>in</strong>clude <br />
#<strong>in</strong>clude "ma<strong>in</strong>.h" /* unix/mail 3/84 */<br />
#def<strong>in</strong>e DIM(x) (sizeof (x) / sizeof *(x)) /* Elemente <strong>in</strong> Vektor */<br />
typedef struct { /* Beispiel fuer Objekt */<br />
char str<strong>in</strong>g[30]; /* Name */<br />
<strong>in</strong>t number; /* Zahl */<br />
<strong>in</strong>t <strong>in</strong>fo; /* e<strong>in</strong>deutige, andere Information */<br />
} Object;<br />
/*** hierher gehoeren die Vergleichsfunktionen —— siehe unten ***/<br />
MAIN<br />
{ Object table[100]; register <strong>in</strong>t t = 0, i;<br />
while (scanf("%s %d", table[t].str<strong>in</strong>g, & table[t].number) == 2)<br />
{ table[t].<strong>in</strong>fo = t;<br />
if (++ t >= DIM(table))<br />
break;<br />
}
Mit scanf() lesen wir e<strong>in</strong>fach abwechselnd e<strong>in</strong>en Namen und e<strong>in</strong>e Zahl jeweils <strong>in</strong> e<strong>in</strong><br />
Tabellenelement e<strong>in</strong>. Läuft die Tabelle über, oder f<strong>in</strong>den wir ke<strong>in</strong> geeignetes Paar<br />
mehr, wird der Lesevorgang beendet. In der .<strong>in</strong>fo-Komponente speichern wir noch<br />
die <strong>E<strong>in</strong></strong>gabeposition des Tabellenelements, damit wir anschließend gewisse Effekte<br />
beim Sortieren demonstrieren können.<br />
Worte wie MAIN wurden <strong>in</strong> der ersten Sprechstunde <strong>in</strong> unix/mail 3/1984 def<strong>in</strong>iert.<br />
Unser Hauptprogramm soll mit der Option -n die .number-Komponenten, mit der<br />
Option -s die .str<strong>in</strong>g-Komponenten und mit der Option -= beide Komponenten<br />
sortieren:<br />
}<br />
OPT<br />
ARG ’n’: Qsort(table, t, cmpn); /* —n .number sortieren */<br />
ARG ’s’: Qsort(table, t, cmps); /* —s .str<strong>in</strong>g sortieren */<br />
ARG ’=’: Qsort(table, t, cmp); /* —= komb<strong>in</strong>iert sortieren */<br />
OTHER fputs("q [—ns=] < testdata\n", stderr), exit(1);<br />
ENDOPT<br />
for (i = 0; i < t; ++ i)<br />
pr<strong>in</strong>tf("%d %s %d\n",<br />
table[i].<strong>in</strong>fo, table[i].str<strong>in</strong>g, table[i].number);<br />
table[] ist unsere Tabelle, e<strong>in</strong> Vektor von Object-Strukturen, und <strong>in</strong> t haben wir die<br />
e<strong>in</strong>gelesenen Elemente gezählt — dieser Wert kann Null se<strong>in</strong>. Die verschiedenen<br />
Aufrufe von qsor t() unterscheiden sich nur <strong>in</strong> den Vergleichsfunktionen: cmpn() und<br />
cmps() sortieren <strong>in</strong> bezug auf e<strong>in</strong>e Komponente, cmp() <strong>in</strong> bezug auf zwei. Aus der<br />
Beschreibung von qsor t() weiß man, daß die Vergleichsfunktion jeweils mit zwei<br />
Zeigern auf Tabellenelemente aufgerufen wird und sich dann wie strcmp() verhalten<br />
muß. Man kann es sich auch so merken: jeder der beiden Parameter der<br />
Vergleichsfunktion hat den Typ, den auch das erste Argument — der Zeiger auf den
Tabellenanfang — von qsor t() hat. Hier s<strong>in</strong>d die Vergleichsfunktionen für unser<br />
Beispiel:<br />
<strong>in</strong>t cmps(a, b) /* .str<strong>in</strong>g vergleichen */<br />
register Object * a, * b;<br />
{<br />
return strcmp(a—>str<strong>in</strong>g, b—>str<strong>in</strong>g);<br />
}<br />
<strong>in</strong>t cmpn(a, b) /* .number vergleichen */<br />
register Object * a, * b;<br />
{<br />
return a—>number < b—>number ? —1 : a—>number > b—>number;<br />
}<br />
<strong>in</strong>t cmp(a, b) /* komb<strong>in</strong>ierter Vergleich */<br />
register Object * a, * b;<br />
{ register <strong>in</strong>t cond;<br />
return (cond = cmps(a, b)) ? cond : cmpn(a, b);<br />
}<br />
Str<strong>in</strong>gs vergleicht man natürlich mit strcmp(). Bei Zahlen muß man aufpassen: nicht<br />
e<strong>in</strong>mal für zwei <strong>in</strong>t-Werte sollte man die Differenz als Resultat des Vergleichs<br />
liefern! Auf e<strong>in</strong>er 16-Bit Masch<strong>in</strong>e hat 32767 - (-2) angeblich den Wert -32767, und<br />
der repräsentiert nicht gerade die Tatsache, daß 32767 größer als -2 ist. cmp()<br />
vergleicht zuerst die .str<strong>in</strong>g-Komponenten; s<strong>in</strong>d sie gleich, hängt das Resultat von<br />
den .number-Komponenten ab.<br />
Das Beispiel sollte vor allem demonstrieren, daß man mit qsor t() beliebige<br />
Vektoren sortieren kann. Außer den Sortierschlüsseln können die Vektorelemente<br />
auch andere Information enthalten — das Sortierkriterium ist völlig unter Kontrolle<br />
der Vergleichsfunktion. Betrachten wir noch e<strong>in</strong> paar Probeläufe:
$ q < data $ q -s < data $ q -n < data<br />
0 str<strong>in</strong>g 10 5 Str<strong>in</strong>g 20 0 str<strong>in</strong>g 10<br />
1 str<strong>in</strong>g 20 4 Str<strong>in</strong>g 10 2 str<strong>in</strong>g 10<br />
2 str<strong>in</strong>g 10 0 str<strong>in</strong>g 10 4 Str<strong>in</strong>g 10<br />
3 str<strong>in</strong>g 20 3 str<strong>in</strong>g 20 3 str<strong>in</strong>g 20<br />
4 Str<strong>in</strong>g 10 1 str<strong>in</strong>g 20 5 Str<strong>in</strong>g 20<br />
5 Str<strong>in</strong>g 20 2 str<strong>in</strong>g 10 1 str<strong>in</strong>g 20<br />
L<strong>in</strong>ks wird die <strong>E<strong>in</strong></strong>gabe unsortiert ausgegeben. Rechts wird numerisch, <strong>in</strong> der Mitte<br />
werden die Namen sortiert. Man sieht <strong>in</strong> beiden Beispielen, daß die<br />
<strong>E<strong>in</strong></strong>gabereihenfolge ‘‘gleicher’’ Elemente zerstört werden kann.<br />
$ q -= < data $ q -sn < data $ q -nn < data<br />
4 Str<strong>in</strong>g 10 0 str<strong>in</strong>g 10 0 str<strong>in</strong>g 10<br />
5 Str<strong>in</strong>g 20 4 Str<strong>in</strong>g 10 2 str<strong>in</strong>g 10<br />
2 str<strong>in</strong>g 10 2 str<strong>in</strong>g 10 4 Str<strong>in</strong>g 10<br />
0 str<strong>in</strong>g 10 3 str<strong>in</strong>g 20 3 str<strong>in</strong>g 20<br />
1 str<strong>in</strong>g 20 5 Str<strong>in</strong>g 20 1 str<strong>in</strong>g 20<br />
3 str<strong>in</strong>g 20 1 str<strong>in</strong>g 20 5 Str<strong>in</strong>g 20<br />
L<strong>in</strong>ks wird mit cmp() nach Namen und sekundär nach Zahlen sortiert. Das ist völlig<br />
anders, als wenn (wie <strong>in</strong> der Mitte) erst e<strong>in</strong>mal ganz nach Namen und dann das<br />
Resultat nach Zahlen sortiert wird. Im mittleren Beispiel wird eigentlich die mittlere<br />
Ausgabe von vorher nochmals neu nach der .number-Komponente sortiert. Man<br />
sieht auch hier sehr deutlich, daß qsor t() <strong>in</strong>stabil ist.<br />
Im rechten Beispiel wird zweimal numerisch sortiert, also die vorhergehende<br />
rechte Ausgabe nochmals neu nach dem gleichen Sortierkriterium. Man sieht, daß<br />
sogar bei e<strong>in</strong>er ursprünglich sortierten Tabelle noch zwei <strong>E<strong>in</strong></strong>träge vertauscht
werden!<br />
Tausch-Puzzle — sortieren mit Index-Vektor<br />
In dieser Ausgabe der unix/mail habe ich auch über e<strong>in</strong> SpreadSheet berichtet, das<br />
e<strong>in</strong>e Sortierfunktion besitzt. Im Spreadsheet kann das Vertauschen von Teilzeilen<br />
oder -spalten e<strong>in</strong>e sehr teure Operation se<strong>in</strong>. Me<strong>in</strong>e Lösung besteht dar<strong>in</strong>,<br />
stellvertretend für den SpreadSheet-Ausschnitt e<strong>in</strong>en Vektor p[] mit Indizes der<br />
betroffenen SpreadSheet-Zeilen oder -Spalten von qsor t() sortieren zu lassen:<br />
<strong>in</strong>t n; /* Anzahl zu sortierende Objekte */<br />
<strong>in</strong>t low; /* Index des ersten Objekts */<br />
<strong>in</strong>t (* cmp)(); /* zustaendige Vergleichsfunktion */<br />
short * p; /* dynamisch angelegter Vektor der Indizes */<br />
<strong>in</strong>t i, j, k;<br />
if (! (p = (short *) malloc(n * sizeof(short))))<br />
fatal("no room");<br />
for (j = 0; j < n; ++ j)<br />
p[j] = j+low;<br />
<strong>qsort</strong>(p, n, sizeof(short), cmp);<br />
Zuerst werden n, low und cmp aus dem Aufruf im SpreadSheet decodiert. Der<br />
Vektor p[] wird so <strong>in</strong>itialisiert, daß p[j] der Index (Zeilen- oder Spaltennummer) des<br />
Objekts im SpreadSheet ist, das sozusagen auf Platz j <strong>in</strong> der zu sortierenden Tabelle<br />
steht. Der durch cmp festgelegten Vergleichsfunktion werden Zeiger auf zwei<br />
solche Indexwerte angeboten, und die Funktion kann damit im SpreadSheet die<br />
Objekte f<strong>in</strong>den und vergleichen.<br />
Ist qsor t() fertig, wurden die Indexwerte <strong>in</strong> p[] so vertauscht, daß <strong>in</strong> p[j] jetzt der<br />
Index des Objekts im SpreadSheet steht, das eigentlich <strong>in</strong> Indexposition low+j im
SpreadSheet stehen sollte um das Sortierkriterium zu erfüllen. Gilt (wie das vor<br />
Aufruf von qsor t() ja der Fall war) p[j] == low+j, sitzt das Objekt schon richtig; gilt<br />
das nicht, müssen wir Objekte tauschen. Das Puzzle besteht dar<strong>in</strong>, wie man<br />
Ordnung mit möglichst wenig Tauschoperationen erreicht:<br />
for (i = 0; i < n; ++ i)<br />
if (p[i] != i+low)<br />
for (j = i; k = j, j = p[k]—low, p[k] = k+low, p[j] != j+low; )<br />
/* Objekte j+low, k+low tauschen */<br />
Zugegeben, Thomas Mandry und ich haben Tage gebraucht, um diese Lösung zu<br />
erf<strong>in</strong>den und zu formulieren, aber sie ist dafür doch auch sehr leicht zu verstehen —<br />
oder? 2<br />
B<strong>in</strong>är suchen — ‘‘<strong>bsearch</strong>()’’<br />
Muß man e<strong>in</strong> Element <strong>in</strong> e<strong>in</strong>em sortierten Vektor f<strong>in</strong>den, kann man <strong>bsearch</strong>()<br />
verwenden. Diese Funktion vergleicht e<strong>in</strong> Suchobjekt mit dem mittleren Element<br />
e<strong>in</strong>es Vektorbereichs, und setzt je nach Resultat die Suche <strong>in</strong> der l<strong>in</strong>ken oder rechten<br />
Hälfte fort. Mit höchstens zehn Vergleichen kann man so e<strong>in</strong>en Wert unter tausend<br />
f<strong>in</strong>den. <strong>bsearch</strong>() verwendet Argumente, die an qsor t() er<strong>in</strong>nern, und liefert als<br />
Resultat e<strong>in</strong>en Zeiger auf das gesuchte Objekt <strong>in</strong> der Tabelle oder e<strong>in</strong>en Nullzeiger:<br />
char * <strong>bsearch</strong>();<br />
#def<strong>in</strong>e Bsearch(key, base, nel, compar) \<br />
2 Fans sei soviel verraten: Die äußere Schleife behandelt jedes Element <strong>in</strong> p[]<br />
e<strong>in</strong>mal. Wenn etwas nicht stimmt, löst die <strong>in</strong>nere Schleife e<strong>in</strong>en der<br />
Permutationszyklen auf, aus denen p[] besteht.
((Object *) <strong>bsearch</strong>((char *) (key), /* gesuchtes Object */ \<br />
(char *) (base), /* Anfang der Tabelle */ \<br />
(unsigned) (nel), /* Anzahl Elemente */ \<br />
(unsigned) sizeof *(base), /* Elementgroesse */ \<br />
(<strong>in</strong>t (*)()) (compar))) /* Vergleichsfunktion */<br />
Als erstes Argument erwartet <strong>bsearch</strong>() e<strong>in</strong>en Zeiger auf das Suchobjekt; die<br />
restlichen vier Argumente s<strong>in</strong>d exakt die gleichen wie bei qsor t(). Der Makro<br />
Bsearch() vermeidet wieder die Angabe der Größe e<strong>in</strong>es Elements und sorgt mit<br />
expliziten Umwandlungen für e<strong>in</strong>e gewisse Typenprüfung. Als Resultat liefert<br />
<strong>bsearch</strong>() e<strong>in</strong>en Zeigerwert; er hat den gleichen Typ wie das erste Argument — der<br />
Zeiger auf das Suchobjekt — und das zweite Argument — der Zeiger auf den<br />
Tabellenanfang — und wie die Zeiger, die an die Vergleichsfunktion übergeben<br />
werden.<br />
In unserem früheren Beispiel können wir das Hauptprogramm so abändern, daß<br />
bei jeder Option noch e<strong>in</strong> Wert übergeben wird, der dann entsprechend gesucht<br />
wird:<br />
Object table[100]; <strong>in</strong>t t; /* Beispiel e<strong>in</strong>er Tabelle */<br />
static look(str, num, cmp)<br />
char * str; <strong>in</strong>t num; /* Suchargument */<br />
<strong>in</strong>t (* cmp)(); /* Vergleichsfunktion */<br />
{ Object key, * kp; /* Suchobjekt, Resultat */<br />
Qsort(table, t, cmp);<br />
strcpy(key.str<strong>in</strong>g, str), key.number = num;<br />
if (kp = Bsearch(& key, table, t, cmp))<br />
pr<strong>in</strong>tf("%d %s %d\n", kp—><strong>in</strong>fo, kp—>str<strong>in</strong>g, kp—>number);<br />
}<br />
MAIN<br />
{ char * str;
}<br />
/*** e<strong>in</strong>lesen, wie im <strong>qsort</strong>()—Beispiel ***/<br />
OPT<br />
ARG ’n’: PARM look("", atoi(*argv), cmpn); NEXTOPT<br />
ARG ’s’: PARM look(*argv, 0, cmps); NEXTOPT<br />
ARG ’=’: PARM str = *argv; NEXTOPT<br />
PARM look(str, atoi(*argv), cmp); NEXTOPT<br />
OTHER fputs("b [—n num] [—s str] [—= str num] < testdata\n",<br />
stderr), exit(1);<br />
ENDOPT<br />
Object und die drei Vergleichsfunktionen stammen aus dem ersten Abschnitt.<br />
Diesmal muß table[] global def<strong>in</strong>iert werden, damit die Tabelle auch für look()<br />
implizit zur Verfügung steht. look() erhält die zu suchende Information und die<br />
nötige Vergleichsfunktion. Zuerst wird die Tabelle mit qsor t() entsprechend sortiert,<br />
dann wird <strong>bsearch</strong>() verwendet.<br />
Unsere Tabellenelemente enthalten wieder mehr Information als nur das<br />
Suchargument. Man sieht aber, daß an <strong>bsearch</strong>() als Suchgegenstand nicht etwa<br />
nur der gesuchte Name oder die Zahl übergeben wird, sondern e<strong>in</strong> Zeiger auf e<strong>in</strong><br />
ganzes Tabellenelement. In dem Tabellenelement muß natürlich nur der Teil<br />
<strong>in</strong>itialisiert se<strong>in</strong>, den die Vergleichsfunktion tatsächlich betrachtet. Wir <strong>in</strong>itialisieren<br />
nur deshalb immer die .str<strong>in</strong>g- und die .number-Komponente, da wir look() für alle<br />
drei möglichen Suchen verwenden.<br />
$ b < data -n 10; $ b < data -n 20 $ b < data -n 10 -n 20<br />
4 Str<strong>in</strong>g 10 4 Str<strong>in</strong>g 10<br />
5 Str<strong>in</strong>g 20 1 str<strong>in</strong>g 20<br />
Bei diesen Beispielen werden die gleichen Daten wie für die Beispiele zu qsor t()<br />
verwendet. L<strong>in</strong>ks wird jeweils e<strong>in</strong>mal numerisch sortiert und dann gesucht.
Vergleicht man mit der entsprechenden früheren Ausgabe, sieht man, daß <strong>bsearch</strong>()<br />
von mehreren ‘‘gleichen’’ Tabellenelementen eben das liefert, das durch die<br />
fortgesetzte Halbierung zuerst erreicht wird; das ist nicht unbed<strong>in</strong>gt das erste oder<br />
letzte e<strong>in</strong>er Folge gleicher Elemente. Rechts wird für 20 positionell das gleiche<br />
Tabellenelement wie im l<strong>in</strong>ken Beispiel gefunden; look() hat aber dann zweimal<br />
numerisch sortiert und, wie bei qsor t() schon demonstriert, hat damit 1 an die Stelle<br />
von 5 gebracht.<br />
Die eigentliche Falle besteht bei <strong>bsearch</strong>() wohl dar<strong>in</strong>, daß man die falschen<br />
Zeigertypen <strong>in</strong> der Vergleichsfunktion erwartet. Hier ist noch e<strong>in</strong> Beispiel, bei dem<br />
Str<strong>in</strong>gs, nämlich die Argumente des Kommandos, verwendet werden:<br />
typedef char * Object; /* Objekte s<strong>in</strong>d Str<strong>in</strong>gs */<br />
static <strong>in</strong>t cmp(a, b) Object * a, * b; /* Zeiger auf Objekte,... */<br />
{<br />
return strcmp(*a, *b); /* ...erst *a ist e<strong>in</strong> Str<strong>in</strong>g! */<br />
}<br />
ma<strong>in</strong>(argc, argv) Object argv[]; /* (Object[]) ist (char **) */<br />
{ char buf[100];<br />
Object key = buf;<br />
Qsort(argv, argc, cmp);<br />
while (gets(buf))<br />
if (! Bsearch(& key, argv, argc, cmp))<br />
puts("not found");<br />
}<br />
argv[] ist der Vektor, der sortiert wird, und <strong>in</strong> dem gesucht wird. Das erste<br />
Argument zu <strong>bsearch</strong>() muß den gleichen Typ besitzen wie das zweite Argument,<br />
argv, und dieser Typ wird auch an cmp() geliefert. buf[] muß wirklich e<strong>in</strong> Vektor<br />
se<strong>in</strong>, denn gets() h<strong>in</strong>terlegt dort die <strong>E<strong>in</strong></strong>gabezeichen. buf ist zwar e<strong>in</strong> Zeigerwert,
kann aber nicht erstes Argument für <strong>bsearch</strong>() se<strong>in</strong>, denn buf hat den Typ char *<br />
und argv ist char **. Fehlt e<strong>in</strong> Sternchen, kann man pr<strong>in</strong>zipiell mit & e<strong>in</strong>s erzeugen:<br />
& buf müßte nach diesem Rezept den richtigen Typ, e<strong>in</strong> Sternchen mehr, also char<br />
**, besitzen — als Vektorname ist buf aber e<strong>in</strong>e Adreßkonstante, und von der kann<br />
und darf man ke<strong>in</strong>e Adresse bilden. Die korrekte Lösung besteht, wie gezeigt, <strong>in</strong> der<br />
Verwendung von key, e<strong>in</strong>er Variablen, <strong>in</strong> der die konstante Adresse buf h<strong>in</strong>terlegt ist,<br />
und deren Adresse wieder an <strong>bsearch</strong>() übergeben werden kann.<br />
L<strong>in</strong>ear suchen — ‘‘<strong>lsearch</strong>()’’ und ‘‘lf<strong>in</strong>d()’’<br />
lf<strong>in</strong>d() hat fast die gleichen Argumente wie <strong>bsearch</strong>() und exakt den gleichen Effekt:<br />
<strong>E<strong>in</strong></strong> Objekt wird <strong>in</strong> e<strong>in</strong>em Vektor unter Kontrolle e<strong>in</strong>er Vergleichsfunktion gesucht. Ist<br />
das Objekt vorhanden, liefert lf<strong>in</strong>d() e<strong>in</strong>en Zeiger als Resultat, ist’s nicht da, gibt’s<br />
e<strong>in</strong>en Nullzeiger. Der kle<strong>in</strong>e Unterschied ist, daß <strong>bsearch</strong>() die Anzahl der<br />
Tabellenelemente als drittes Argument erhält, lf<strong>in</strong>d() aber e<strong>in</strong>en Zeiger auf diese<br />
Anzahl. Der große Unterschied ist, daß lf<strong>in</strong>d() l<strong>in</strong>ear sucht, also vom ersten<br />
Tabellenelement sequentiell bis zum letzten. Das kann extrem lang dauern, aber<br />
dafür braucht die Tabelle nicht sortiert zu se<strong>in</strong>.<br />
Außer lf<strong>in</strong>d() gibt’s <strong>in</strong> der Bücherei auch noch <strong>lsearch</strong>(), mit den gleichen<br />
Argumenten, der gleichen Suchtechnik, und fast dem gleichen Resultat:<br />
char * <strong>lsearch</strong>(), * lf<strong>in</strong>d();<br />
#def<strong>in</strong>e Lsearch(key, base, nelp, compar) \<br />
((Object *) <strong>lsearch</strong>((char *) (key), /* gesuchtes Object */ \<br />
(char *) (base), /* Anfang der Tabelle */ \<br />
(unsigned *) (nelp), /* Anzahl Elemente */ \<br />
(unsigned) sizeof *(base), /* Elementgroesse */ \<br />
(<strong>in</strong>t (*)()) (compar))) /* Vergleichsfunktion */
#def<strong>in</strong>e Lf<strong>in</strong>d(key, base, nelp, compar) \<br />
((Object *) lf<strong>in</strong>d((char *) (key), /* gesuchtes Object */ \<br />
(char *) (base), /* Anfang der Tabelle */ \<br />
(unsigned *) (nelp), /* Anzahl Elemente */ \<br />
(unsigned) sizeof *(base), /* Elementgroesse */ \<br />
(<strong>in</strong>t (*)()) (compar))) /* Vergleichsfunktion */<br />
Leider wurden die Funktionen nicht so arg logisch benannt: lf<strong>in</strong>d() sucht, muß aber<br />
nicht unbed<strong>in</strong>gt f<strong>in</strong>den, und liefert unter Umständen e<strong>in</strong>en Nullzeiger. <strong>lsearch</strong>()<br />
sucht, f<strong>in</strong>det bestimmt, und liefert nie e<strong>in</strong>en Nullzeiger: ist das gesuchte Objekt nicht<br />
<strong>in</strong> der Tabelle, trägt es <strong>lsearch</strong>() am Ende des Vektors e<strong>in</strong>, korrigiert die Anzahl der<br />
Elemente entsprechend (deshalb muß e<strong>in</strong> Zeiger auf diese Anzahl übergeben<br />
werden!), und liefert schließlich e<strong>in</strong>en Zeiger auf das gefundene — neu e<strong>in</strong>getragene<br />
— Objekt. Das Wortspiel zum ‘‘gefundenen Fressen’’ liegt nahe: <strong>lsearch</strong>() hat nicht<br />
genügend Information um das verfügbare Ende der Tabelle zu erkennen. Es bleibt<br />
dem Progammierer überlassen, <strong>lsearch</strong>() nur dann aufzurufen, wenn wenigstens<br />
noch e<strong>in</strong> Element <strong>in</strong> den angebotenen Vektor paßt.<br />
<strong>lsearch</strong>() eignet sich auch nicht zum <strong>E<strong>in</strong></strong>satz <strong>in</strong> kle<strong>in</strong>en Compilern: zur<br />
Implementierung e<strong>in</strong>er Blockstruktur für den Geltungsbereich von Namen fügt man<br />
neue Namen jeweils zu e<strong>in</strong>er l<strong>in</strong>earen Liste h<strong>in</strong>zu, die aber wie e<strong>in</strong> Stack rückwärts<br />
durchsucht werden muß, damit man neue (<strong>in</strong>nere) Namen zuerst f<strong>in</strong>det. <strong>lsearch</strong>()<br />
sucht vorwärts...
B<strong>in</strong>är e<strong>in</strong>fügen — ‘‘b<strong>in</strong>ary()’’<br />
<strong>lsearch</strong>() hat gegenüber <strong>bsearch</strong>() den Vorteil, daß e<strong>in</strong> nicht vorhandenes Element <strong>in</strong><br />
die Tabelle e<strong>in</strong>gefügt wird. Die l<strong>in</strong>eare Suche ist jedoch e<strong>in</strong> gravierender Nachteil:<br />
<strong>bsearch</strong>() merkt nach 10 Vergleichen bei 1000 Elementen, daß e<strong>in</strong> gesuchtes<br />
Element fehlt; <strong>lsearch</strong>() merkt das erst nach 1000 Vergleichen. Die folgende<br />
Funktion b<strong>in</strong>ary() komb<strong>in</strong>iert die Vorteile der beiden anderen Funktionen: sie sucht<br />
b<strong>in</strong>är und fügt bei Bedarf das gesuchte Element so e<strong>in</strong>, daß die Tabelle sortiert<br />
bleibt:<br />
char * b<strong>in</strong>ary(key, base, nelp, width, compar)<br />
char * key, * base; unsigned * nelp, width; <strong>in</strong>t (* compar)();<br />
{ unsigned nel = * nelp; <strong>in</strong>t c;<br />
char * lim = base + nel * width, * mid, * high;<br />
for (high = lim — width; base >= 1)<br />
{ mid = base + (nel >> 1) * width;<br />
if ((c = (* compar)(key, mid)) < 0)<br />
high = mid — width;<br />
else if (c > 0)<br />
base = mid + width, —— nel;<br />
else<br />
return mid;<br />
}<br />
for (high = lim, lim += width; high > base; )<br />
*——lim = *——high;<br />
for (c = width; c——; )<br />
base[c] = key[c];<br />
++ *nelp;<br />
return base;<br />
}
lim wird auf den Punkt unmittelbar nach den vorhandenen Elementen e<strong>in</strong>gestellt.<br />
Die erste for-Schleife realisiert die konventionelle b<strong>in</strong>äre Suche. Die e<strong>in</strong>zige<br />
Schwierigkeit liegt dar<strong>in</strong>, daß mid nicht nur auf den Mittelpunkt zwischen base und<br />
high — also dem ersten und letzten Element im aktuellen Vektorbereich — zeigen<br />
kann, sondern möglichst genau dort auf e<strong>in</strong> Element zeigen muß. nel ist die Anzahl<br />
der Elemente im Vektorbereich. Ist sie gerade, zeigt mid auf das Element, das am<br />
Mittelpunkt beg<strong>in</strong>nt, und nach dem Element bei mid folgen weniger Elemente als<br />
davor. Ist die Anzahl der Elemente ungerade, zeigt mid auf das Element, das das<br />
Byte am Mittelpunkt enthält, und davor und danach s<strong>in</strong>d gleich viele Elemente.<br />
Wechselt man <strong>in</strong> die rechte ‘‘Hälfte’’ über, muß nel sorgfältig korrigiert werden,<br />
sonst zeigt man am Schluß möglicherweise <strong>in</strong>s Leere!<br />
Geht die erste for-Schleife erfolglos zu Ende, zeigt base gerade auf den Punkt, wo<br />
das gesuchte Element se<strong>in</strong> sollte. Die zweite for-Schleife geht von rechts nach l<strong>in</strong>ks<br />
und schiebt den Rest des Vektors nach rechts, und die dritte for-Schleife kopiert die<br />
gesuchte Information an die richtige Stelle <strong>in</strong> der nach wie vor sortierten Tabelle.<br />
Messen — ‘‘bench()’’<br />
<strong>E<strong>in</strong></strong> typisches Problem zum Vergleichen von Suchfunktionen ist die<br />
Häufigkeitsanalyse von Worten <strong>in</strong> e<strong>in</strong>em Text. Als Treiber für die nachfolgenden<br />
Vergleiche verwenden wir folgendes Programm:<br />
<strong>in</strong>t <strong>in</strong>c; /* Parameter fuer Speicherverwaltung */<br />
MAIN<br />
{ char buf[100]; register char * cp;<br />
OPT<br />
ARG ’i’: PARM <strong>in</strong>c = atoi(*argv); NEXTOPT
}<br />
OTHER fputs("[—i <strong>in</strong>c]\n", stderr), exit(1);<br />
ENDOPT<br />
bench("");<br />
while (scanf("%s", buf) == 1)<br />
if (cp = word(buf))<br />
count(cp);<br />
bench("count");<br />
alpha();<br />
bench("alpha");<br />
frequency();<br />
bench("frequency");<br />
scanf() extrahiert Worte, also Zeichenfolgen, die nicht aus Zwischenraumzeichen<br />
bestehen, aus der Standard-<strong>E<strong>in</strong></strong>gabe. word() kann dazu dienen, zum Beispiel nur<br />
alphanumerische Worte oder nur genügend lange Worte zu selektieren. count()<br />
muß e<strong>in</strong> Wort mit Hilfe e<strong>in</strong>er der Suchfunktionen f<strong>in</strong>den und zählen; neue Worte<br />
müssen jeweils dynamisch gespeichert werden. alpha() soll zum Schluß alle Worte<br />
sortiert ausgeben, frequency() soll sie nach Häufigkeit sortiert ausgeben. bench()<br />
dient dazu, die Ausführungszeit der verschiedenen Phasen des Tests und den<br />
tatsächlichen dynamischen Speicherverbrauch zu messen:<br />
#<strong>in</strong>clude <br />
#<strong>in</strong>clude <br />
#<strong>in</strong>clude <br />
bench(s) register char * s; /* Zeitstempel ausgeben */<br />
{ static struct tms told; static long old; /* letzte Marke */<br />
struct tms tnew; long new, times(); /* neue Marke */<br />
static char * bold; char * bnew, * sbrk(); /* Speicher */<br />
new = times(& tnew); bnew = sbrk(0);
}<br />
if (old)<br />
fpr<strong>in</strong>tf(stderr,<br />
"%s: %d/%d/%d (user/sys/real <strong>in</strong> ticks), %u (heap bytes)\n",<br />
s, (<strong>in</strong>t) (tnew.tms_utime — told.tms_utime),<br />
(<strong>in</strong>t) (tnew.tms_stime — told.tms_stime),<br />
(<strong>in</strong>t) (new — old), (unsigned) bnew — bold);<br />
bold = bnew; old = times(& told);<br />
times() ist e<strong>in</strong> Systemaufruf, der als Resultat jeweils e<strong>in</strong>e Uhrzeit liefert. In struct<br />
tms werden außerdem die CPU-Zeiten h<strong>in</strong>terlegt, die der aufrufende Prozeß (mit<br />
etwa vorhandenen Abkömml<strong>in</strong>gen) bisher selbst und durch Systemaufrufe<br />
verbraucht hat. sbrk(0) zeigt die Adresse, die der Systemkern bei der nächsten<br />
3<br />
Anforderung von dynamischem Speicherplatz liefern würde. Verwendet man für<br />
count(), alpha() und frequency() leere Funktionen, liefert unser Programm etwa<br />
folgendes:<br />
$ f0 < spreadsheet.ms<br />
count: 87/6/135 (user/sys/real <strong>in</strong> ticks), 0 (heap bytes)<br />
alpha: 0/0/0 (user/sys/real <strong>in</strong> ticks), 0 (heap bytes)<br />
frequency: 0/0/0 (user/sys/real <strong>in</strong> ticks), 0 (heap bytes)<br />
spreadsheet.ms ist dabei die Quelle zu me<strong>in</strong>em SpreadSheet-Artikel <strong>in</strong> diesem Heft:<br />
word() selektiert 966 verschiedene Worte mit etwa 8 KB Zeichen; der Text enthält<br />
<strong>in</strong>sgesamt etwa 2750 ‘‘Worte’’ mit 18 KB Text.<br />
Die Bedeutung der clock ticks bleibt eigentlich undef<strong>in</strong>iert. Bei XENIX enthält die<br />
Environment-Variable HZ die Anzahl der clock ticks pro Sekunde; auf dem hier<br />
3 Das ist nicht unbed<strong>in</strong>gt das nächste Resultat von malloc(), denn diese Funktion<br />
verwaltet e<strong>in</strong>e freie Liste und fordert außerdem Speicher bei sbrk() nur <strong>in</strong> größeren<br />
Blöcken an.
verwendeten System s<strong>in</strong>d das 50. Der Systemkern mißt auch die CPU-Zeiten nicht<br />
vollkommen exakt; die gezeigten Zeiten sollten also nur als qualitative Aussage<br />
betrachtet werden.<br />
(Fortsetzung folgt)