Algorithmen & Datenstrukturen
Programmieren 1


von Prof. Jürgen Plate

8 Ausgewählte Algorithmen (Teil 1)

8.1 Verarbeitung von Zeichenketten (Strings)

Einfache Stringoperationen

Bibliothek zum Test von Zeichen: <ctype.h>

Diese Bilbliothek wurde bereits kurz vorgestellt, hier folgen Beispiele zur Anwendung. Zur Erinnerung:
Das Argument der folgenden Funktionen ist jeweils ein int dessen Wert entweder durch EOF oder als unsigned char darstellbar sein muß. Der Rückgabewert ist ein int, wobei dieser gleich Null ist, wenn das Argument c die Bedingung nicht erfüllt, ansonsten ist er ungleich Null. Wenn sie Probleme mit char-Variablen haben, kann das in seltenen Fällen am zugrundeliegenden System liegen. Ersetzen sie dann char durch unsigned char.
Achtung: Die deutschen Umlaute und das "ß" sind keine Buchstaben im Sinne der C-Funktionen!!

islower(c) Kleinbuchstabe
isupper(c) Großbuchstabe
isalpha(c) Klein- oder Großbuchstabe
isdigit(c) Dezimalzahl
isalnum(c) Klein- oder Großbuchstabe oder Dezimalzahl
iscntrl(c) Control-Zeichen
isgraph(c) druckbares Zeichen außer space
isprint(c) druckbares Zeichen mit space
ispunct(c) druckbares Zeichen außer space, Buchstabe und Ziffern
isspace(c) space, formfeed, newline, carriage return, tab, vertical tab
isxdigit(c) Hexadezimalzahl
tolower(int c) konvertiert c zu Kleinbuchstaben
toupper(int c) konvertiert c zu Großbuchstaben

#include <stdio.h>
#include <ctype.h>

void main (void)
  {
  char c;

  c = getchar();
  if (isalpha(c)) printf ( "%c ist ein Buchstabe\n",c);
  if (isalnum(c)) printf ( "%c ist ein Buchstabe oder eine Ziffer\n",c);
  if (isupper(c)) printf ( "%c ist ein Großbuchstabe\n",c);
  if (islower(c)) printf ( "%c ist ein Kleinbuchstabe\n",c);
  if (isdigit(c)) printf ( "%c ist eine Ziffer\n",c);
  if (isspace(c)) printf ( "%c ist ein Leerzeichen\n",c);
  }
Das folgende Beispiel zeigt die Anwendung der Konvertierfunktionen:
#include <stdio.h>
#include <ctype.h>

void main (void)
{
  char c, d;

  c = getchar();
  if (! isalpha(c)) 
    {
    printf ("%c ist kein Buchstabe!\n",c);
    }
  else
    {
    if (islower(c))
      {
      d = toupper (c);
      printf ( "%c wurde umgwandelt in %c\n",c,d );
      }
    if (isupper(c)) 
      {
      d = tolower (c);
      printf ( "%c wird umgwandelt in %c\n",c,d );
      }
    }
  }

Ausdrucken einer einfachen ASCII-Tabelle:

#include <stdio.h>
#include <ctype.h>

void main(void)
  {
  int i, j;
  for (i=0; i < 4; i++) printf("|dec hex Char ");
  printf("|\n");
  for (i=0; i < 128/4; i++)
    {
    printf("\n| ");
    for (j=0; j < 128; j+=128/4)
      {
      printf("%3d %2X ", i+j, i+j);
      if (isgraph(j)) printf("  %c  | ", i+j);
      else printf("  .  | ");
      }
    }
  }

Länge einer konstanten Zeichenkette ermitteln:

#include <stdio.h>

int stringlaenge(char *s);

int main(void)
  { 
  char kette[] = "Elvis is alive!";
  printf("DDer String hat %d Zeichen.\n", stringlaenge(kette));
  } 

int stringlaenge(char s[])
  { 
  int i = 0;
  for(i=0; kette[i] != '\0'; i++);
  return (i)
  }

Stringfunktionen

Neben den einfachen Zuweisungen und Abfragen gibt es eine Reihe von komplexen Befehlen für Zeichenketten. DIe Prototypen befinden sich in der Standardbibliothek <string.h>. In den Beispielen gelten folgende Variablenvereinbarungen:
char a[100];
char b[100];
char c;
int n;
int m;

strcat ( char *a , char *b )

Es wird an a die Zeichenfolge von b angehängt.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] = "Algorithmen & "; 
  char b[100] = "Datenstrukturen"; 

  printf ("a = %s\nb = %s\n",a,b); 
  strcat ( a, b ); 
  printf("\na = %s\n",a);
  }

strncat ( char *a , char *b , int n )

Es wird wie bei strcat b an a angehängt. Jedoch nur maximal n Zeichen von b werden angehängt.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] = "Algorithmen & "; 
  char b[100] = "Datenstrukturen"; 
  int n = 5; 
  printf ("\na = %s\nb = %s\n",a,b); 
  strncat ( a, b ); 
  printf("\na = %s\n",a);
  }

strcpy ( char *a , char *b )

Der Inhalt von b wird nach a kopiert. Das Beispielprogramm dazu:
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] = "Alter Text"; 
  char b[100] = "Neuer Text";

  printf ("a = %s",a);
  printf ("b = %s\n\n",b);
  strcpy (a,b); 
  printf ("a = %s\n",a);
  }
strcpy könnte so implementiert werden:
void strcpy(char *a, char *b)
  { 
  int i = 0;
  while ((a[i] = b[i]) != '\0')
     i++;
  }

strncpy ( char *a , char *b , int n )

Der Inhalt von b wird nach a kopiert. Dabei werden höchstens n Zeichen von b nach a kopiert. Der Rest des Strings bleibt erhalten.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] = "Alter Text"; 
  char b[100] = "Neuer Text";
  int n = 2;

  printf ("a = %s",a);
  printf ("b = %s\n\n",b);
  strcpy (a,b); 
  printf ("a = %s\n",a);
  strncpy (b,a,n); 
  printf ("a = %s\n",a);
  }

int n = strlen ( char *a )

Es wird die Länge der Zeichenkette ( ohne das abschließende '/0') zurückgegeben.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  int n; 
  char a[100] ="Na, wie lang bin ich denn?"; 

  n = strlen (a); 
  printf (",,aïï ist %d Zeichen lang\n",n);
  }

Zeichenposition und Teilstring suchen im String

char *b = strchr ( char *a , char c )

Es wird String a nach dem Buchstaben c durchsucht und ein Zeiger auf die Stelle in a zurückgegeben, an der sich der Buchstabe c zuerst befindet. Existiert kein Buchstabe, so wird NULL zurückgegeben. Im Beispiel ist dies das 'i' von 'ist'.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  int n; 
  char a[100] ="Na, wo ist denn das Zeichen?"; 

  printf ("%s" , strchr (a , 'i') );
  }

char *b = strrchr ( char *a , char c )

Es wird String a nach dem Buchstaben c durchsucht und ein Zeiger auf die Stelle in a zurückgegeben, an der sich der Buchstabe c zuletzt befindet. Existiert kein Buchstabe, so wird NULL zurückgegeben. Im Beispiel ist dies das 'i' von 'Zeichen'.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] ="Na, wo ist denn das Zeichen?"; 
  printf ("%s" , strrchr (a , 'i') );
  }

int m = strcmp ( char *a , char *b )

Die beiden Zeichenketten werden miteinander verglichen. Der Rückgabewert hängt vom Vergleichsergebnis ab:
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] = "Vergleichswort 1"; 
  char b[100] = "Vergleichswort 2"; 

  printf ("a = %s",a);
  printf ("b = %s\n\n",b);
  printf ("strcmp (a,b) = %d\n", strcmp (a,b) ); 
  printf ("strcmp (a,a) = %d\n", strcmp (a,a) ); 
  printf ("strcmp (b,a) = %d\n", strcmp (b,a) ); 
  }

Die Vergleichsfunktion kann man sich auch selbst schreiben:

strcmp(char s1[], char s2[]) 
  {	
  int i=0;
  while (s1[i] == s2[i])
    {
    if (s1[i] == 0) 
      return(0);
    else
      i++;
    }
  return (s1[i] - s2[i]);
}

int m = strncmp ( char *a , char *b , int n )

Die Funktion arbeitet wie strcmp mit dem Unterschied, das nur die ersten n Zeichen verglichen werden. Alle nachfolgenden Zeichen werden ignoriert.Der Rückgabewert hängt vom Vergleichsergebnis ab:
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] = "Vergleichswort 1"; 
  char b[100] = "Vergleichswort 2"; 

  printf ("a = %s",a);
  printf ("b = %s\n\n",b);
  printf ("strncmp (a,b,3) = %d\n", strncmp (a,b,3) ); 
  printf ("strncmp (a,a,3) = %d\n", strncmp (a,a,3) ); 
  printf ("strncmp (b,a,3) = %d\n", strncmp (b,a,3) ); 
  }

size_t strlen( char *string)

liefert Länge von string ohne das Nullbyte am Schluß
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[100] = "Irgend ein String"; 

  int len = strlen(a);
  printf (",,%sïï hat %d Zeichen", a, len);
  }

void delete(char str[], int pos, int anz)

Die folgende Funktion löscht n Zeichen ab einer vorgegebenen Position aus einem String:
void delete(char str[],int pos, int anz)
  /* anz Zeichen aus str ab pos l”schen */
  {
  int i = pos + anz;
  while ((str[pos++] = str[i++]) != '\0');
  }

char *strupper(char *string)

Die folgende Funktion wandelt alle Kleinbuchstaben in Großbuchstaben um:
char *strupper(char *string)
  {
  char *s;
  if (string != NULL)
    for (s = string; *s; ++s)
      *s = toupper(*s);
  return string;
  } 

char *strlower(char *string)

Die folgende Funktion wandelt alle Großbuchstaben in Kleinbuchstaben um:
char *strlower(char *string)
  {
  char *s;
  if (string != NULL)
    for (s = string; *s; ++s)
      *s = tolower(*s);
  return string;
  }

Suchen und Ersetzen von Strings

char *strstr( char *such, char *vergleich )

liefert einen Zeiger auf das erste Auftreten des Strings such im String vergleich.

Das folgende Beispiel zeigt, wie man das 'Enthaltensein' eines Strings im anderen ermitteln kann. Zur Veranschaulichung ohne Bibliotheksfunktionen und ohne Pointer

int sstr( char such[], char vergleich[] )
  /* Feststellen, ob such in vergleich enthalten ist */
  {
  int i, j;			/* Laufvariablen */

  int lsuch = strlen(such);
  int lvergl = strlen(vergleich);

  for (i = 0; i < (lvergl - lsuch); i++)
    {
    j = 0;
    while (vergleich[i+j] == such[j++])
      if (j == lsuch) return (i);
    }
    return -1;
  }

Variante in der Programmierung:

int sstr( char such[], char vergleich[] )
  /* Feststellen, ob such in vergleich enthalten ist */
  {
  int i, j, k;			/* Laufvariablen */
  for (i = 0; vergleich[i] != '\0'; i++)
    {
    for (j = i, k = 0; such[k] != '\0' && such[k] == vergleich[j]; j++, k++)
      /* tu nix */;
    if (such[k] == '\0') return (i);
    }
  return -1;
  }
Die gleiche Funktion, nur daß diesman die Groß-/Kleinschreibung ignoriert wird:
int isstr( char such[], char vergleich[] )
  /* Feststellen, ob such in vergleich enthalten ist, 
     Gross-/Kleinschreibung ignorieren */
  {
  int i, j;			/* Laufvariablen */

  int lsuch = strlen(such);
  int lvergl = strlen(vergleich);

  for (i = 0; i < (lvergl - lsuch); i++)
    {
    j = 0;
    while (tolower(vergleich[i+j]) == tolower(such[j++])) 
      if (j == lsuch) return (i);
    }
    return -1;
  }

int index (char s[],char t[])

Suchen und die Position zurückliefern:
# include <stdio.h>
# define MAXLINE 1000

int index (char s[],char t[])    /* Position von t in s liefern,
                                    -1 falls nicht da */
  { 
  int i, j, k;
  for (i=0; s[i] != '\0'; i++) 
    {
    for (j=i,k=0; t[k]!='\0' && s[j]==t[k]; j++,k++)
      ;
    if (t[k] == '\0')
      return(i);
    }
  return(-1);
  } 

void replace( char such[], char ersatz[], char original[] )

Die folgende Funktion ersetzt such durch ersatz im String original. Falls such nicht im String enthalten ist, passiert nichts.
void replace( char such[], char ersatz[], char original[] )
  {
  int pos, i, j;			/* Laufvariablen */

  pos = sstr(such, original);
  if (pos >= 0)
    {
    /* Suchstring entfernen */
    i = pos + strlen(such);
    while ((original[pos++] = original[i++]) != '\0');
    /* Platz machen fuer ersatz */
    len = strlen(ersatz), j = pos;
    while ((original[j+len] = original[j]) != '\0')
      j++;
    /* ersatz einbauen */
    for (j = 0; ersatz[j] != '\0'; j++)
      original[pos+j] = ersatz[j];
    }
  }

Leerzeichen entfernen

Die folgende Funktion trim entfernt Leerzeichen aus einer Zeichenkette:
#include <ctype.h>

char *trim (char *str)
  {
  char *ibuf, *obuf;
  if (str)
    {
    for (ibuf = obuf = str; *ibuf; )
      {
      while (*ibuf && (isspace (*ibuf)))
         ibuf++;
      if (*ibuf && (obuf != str))
        *(obuf++) = ' ';
      while (*ibuf && (!isspace (*ibuf)))
        *(obuf++) = *(ibuf++);
      }
    *obuf = NUL;
    }
  return (str);
  }

Umlaute konvertieren

Die folgende Funktion konvertiert Umlaute und Szett in die Ersatzdarstellung (ae, oe, ue, Ae, Oe, Ue, ss):

char* uml(char* eingabe)
  {
  int i = 0, j = 0;
  char *tmp1 = NULL;
  char *tmp2 = NULL;

  /* Wir sparen uns das Ermitteln der Laenge des     */
  /* Eingabestrings und nehmen mal den worst case an */
  tmp1= malloc(2*strlen(eingabe) + 1);
  if(tmp1 == NULL) return(NULL);

  /*Kopieren und Ersetzen */
  while(eingabe[i] != '\0')
    {
    switch (eingabe[i])
      {
      case '&aluml;': tmp1[j++] = 'a'; tmp1[j++]='e'; break;
      case '&oluml;': tmp1[j++] = 'o'; tmp1[j++]='e'; break;
      case '&uluml;': tmp1[j++] = 'u'; tmp1[j++]='e'; break;
      case '&Aluml;': tmp1[j++] = 'A'; tmp1[j++]='e'; break;
      case '&Oluml;': tmp1[j++] = 'O'; tmp1[j++]='e'; break;
      case '&Uluml;': tmp1[j++] = 'U'; tmp1[j++]='e'; break;
      case 'ß': tmp1[j++] = 's'; tmp1[j++]='s'; break;
      default: tmp1[j++] = eingabe[i];
      }
     i++;
    }
  tmp1[j] = '\0';
  /* Nun den String genau passend zurueckgeben */
  tmp2 = malloc(strlen(tmp1) + 1);
  if(tmp2 == NULL) return(NULL);
  strcpy(tmp2,tmp1);
  free(tmp1);
  return(tmp2);
  }

Anwendung von atoi, atof,...

Die folgenden Funktionen wandeln eine Zeichenkette in eine Zahl um. Dabei werden führende Zwischenräume ignoriert. Die Umwandlung erfolgt, bis im String ein ungültiges Zeichen angetroffen wird. Bei über- oder Unterlauf wird errno = ERANGE und ein spezieller Wert zurückgegeben.

double atof(const char *s)

wandelt Zeichenkette in double float um.
#include <stdio.h>
#include <string.h>
#include <math.h>     /* wegen atof */

void main (void)
  {
  char a[80] = "123456.7899999"; 
  double z;

  printf ("a = %s\n",a);
  z = atof(a);
  printf ("z = %lf\n",z);
  }

int atoi(const char *s)

wandelt Zeichenkette in int um.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[80] = "123"; 
  int i;

  printf ("a = %s\n",a);
  i = atoi(a);
  printf ("i = %d\n",i);
  }

int atol(const char *s)

wandelt Zeichenkette in long int um
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char a[80] = "123"; 
  long int ii;

  printf ("a = %s\n",a);
  ii = atol(a);
  printf ("ii = %lu\n",ii);
  }

Anwendung von sprintf

int sprintf(char *string, const char *format, ...)

Der Formatstring der Funktion hat den gleichen Aufbau wie bei printf(), die Ausgabe wird jedoch in string abgelegt. Daher eignet sich diese Funktion, um Zahlen in Zeichenketten umzuwandeln.
Warnung:Der Rückgabewert von sprintf() ist implementierungsabhängig. Es sollte die Zahl der Zeichen im String zurückgegeben werden. Manche Systeme liefern aber einen Zeiger auf den String.
#include <stdio.h>
#include <string.h>

void main (void)
  {
  char s[] = "*****";
  int i = 123;
  float x = 3.1415;
  char string[50];

  /* Resultat interessiert nicht */
  (void) sprintf (string, "Zeichenkette: %s Int: %d Float: %f", s, i, x);
  }

Römische Zahlen

Das folgende Programm zeigt, wie man eine positive ganze Zahl in römischen Ziffern darstellt. Das Schema ist identisch für Einer-, Zehner- und Hunderterstelle. Der Unterschied besteht nur in den Auszugebenden Zeichen: Ab Tausend aufwärts wird für jeden Tausender ein "M" gedruckt. Die Funktion behandelt weiterhin die Besonderheit der Schreibweise für 9 und 4 (z. B. IV, IX, CM).

#include <stdio.h>
#include <string.h>

void RZiffer(int x, char e, char f, char z)
  /* druckt x als roemische Zahl */
  {
  int i;
  switch (x)
    {
    case 9: printf("%c%c",e,z); break;
    case 8: printf("%c%c%c%c",v,e,e,e); break;
    case 7: printf("%c%c%c",v,e,e); break;
    case 6: printf("%c%c",v,e); break;
    case 5: printf("%c",v); break;
    case 4: printf("%c%c",e,v); break;
    case 3: printf("%c%c%c",e,e,e); break;
    case 2: printf("%c%c",e,e); break;
    case 1: printf("%e",e); break;
    default: break;
    }
  }
  
void main (void)
  {
  int x = 1999;

  while (x > 999)
    {
    printf("M");
    x = x - 1000;
    }
  RZiffer(x/100,'C','D','M');
  x = x%100;
  RZiffer(x/10,'X','L','C');
  RZiffer(x%10,'I','V','X');
  printf("\n");
  }

E/A-Funktionen für Zeichenketten

Die Funktion get_line liest eine Textzeile in ein Zeichenfeld ein und ergaenzt das '\0' Zeichen. Das Ergebnis der Funktion ist die Laenge des gelesenen Strings oder EOF bei Dateiende.

#include <stdio.h>
#define MAXLAENGE 80

int getline(char line[]); 

int main(void)
  { 
  char zeile[MAXLAENGE];
  int laenge, i;
  while ((laenge = getline(zeile)) > 0)
     { 
     printf("%d Zeichen gelesen\n",laenge)
     for(i=0; i<laenge; i++)
       printf("%c",zeile[i]); 
     printf("\n");
     }
  }

int getline(char line[])
  { 
  int c;
  int i = 0;
  while ((c = getchar()) != '\n' && c != EOF)
    if( i < MAXLAENGE-1)
      line[i++] = c;
  line[i++] = '\0';
  if( c == EOF)
    return (EOF);
  else
    return (i);
  }

In der Standardbibliothek sind neben printf und scanf zwei nützliche Routinen zum Einlesen bzw. Drucken von Strings vorhanden:

Beispiel: Einlesen und Ausgeben eines Strings mit Hilfe von Bibliotheksroutinen:

#include <stdio.h>
#define MAXLAENGE 80

void main(void)
  { 
  int i, laenge = 0;
  char zeile[MAXLAENGE];
  char *zeilenzeiger;

  zeilenzeiger = zeile;
  gets(zeilenzeiger);
  puts(zeile);
  }

Das folgende Beispiel zeigt, wie man mit printf Tabellen ausgeben kann. Der Trick ist darin verborgen, daß man bei jeder Druckzeile die Anzahl der Leerstellen ermittelt, die zwischen zwei Feldern liegen sollen. Durch einen * in der Formatangabe (z. B. "%-*s") kann die Ausgabebreite variabel gestaltet werden. Alles weitere zeigt das folgende Beispiel:


#include <stdio.h>
#include <string.h>

int main(void)
  {
  char *firstname[] = { "Sepp",      "Jan",   "Dennis",  NULL };
  char *lastname[]  = { "Langnamer", "Kurz",  "Ritchie", NULL };
  int   note[]      = {   4,           2,        1,       0   };
  int   i, tabwidth;

  printf("%15sStudentenname%30s\n\n", "", "Note");
     
  for (i = 0; NULL != lastname[i]; ++i)
    {
    tabwidth = 36                             /* Leerzeichen insgesamt */
               -2                             /* fuer  ", " */
               -strlen(lastname[i]);          /* laenge lastname */
    
    printf("%s, %-*s%3d\n",
            lastname[i], tabwidth, firstname[i], note[i]);
    }

  return 0;
  }

Das Ergebnis sieht dann etwa so aus:

Studentenname                    Note

Langnamer, Sepp                    4
Kurz, Jan                          2
Ritchie, Dennis                    1

BASIC-Stringfunktionen

Wer die Programmiersprache BASIC kennt, vermisst manchmal komfortable Funktionen zur Bearbeitung von Zeichenketten. Die folgenden Funktionen realisieren einige aus BASIC bekannte und vertraute Funktionen. Mit dabei ist auch eine einfache Speicherverwaltung.
/*
**  public domain by Bob Stout
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <limits.h>
#include <assert.h>

#define NUL '\0'
#define LAST_CHAR(s) (((char *)s)[strlen(s) - 1])


char *stralloc(size_t length);
     /* Belegen von Speicher fuer einen String */

void  str_free(char *string);
     /* Freigabe des mit stralloc belegten Platzes */

char *left(char *string, size_t N);
     /* liefert die linken N Zeichen des Strings */

char *right(char *string, size_t N);
     /* liefert die rechten N Zeichen des Strings */

char *mid(char *string, size_t M, size_t N);
     /* liefert einen Teilstring, N Zeichen lang, 
        beginnend ab Position M */

char *string_add(char *string, ...);
     /* konkateniert mehrere Strings zu einem */

char *string(int ch, size_t N);
     /* liefert eine N Zeichen langen String, der
        aus den durch ch vorgegebenen Zeichen besteht */

unsigned int instr(const unsigned int start_pos,
                   const char        *string,
                   const char        *findstr);
      /* liefert die Position von findstr (beginnend ab 1) in
         string. Die Suche beginnt bei Zeichenposition start_pos
         (beginnend bei 1). */


static int stralloc_ptr;
static char *strings[8];
static int str_tag[8];

/*
**  stralloc() is the key function in this package, maintaining a pool of
**  reusable strings.
*/

char *stralloc(size_t length)
{
      register int i;

      if (UINT_MAX == length)       /* Assume size_t == unsigned int    */
            return NULL;

      i = stralloc_ptr++;
      ++length;                     /* Allow for terminating NUL        */

      if ((!strings[i]) || (length > strlen(strings[i])))
      {
            strings[i] = (char *)realloc(strings[i], length);
            assert(NULL != strings[i]);
            str_tag[i] = -1;
      }
      else  str_tag[i] = 0;
      stralloc_ptr &= 7;
      return (strings[i]);
      /* Maintains 8 strings in a circular buffer */
}

/*
**  free the string pool.
*/

void str_free(char *string)
{
      register int i;

      for (i = 0; i < 8; ++i)
      {
            if (strings[i] == string)
            {
                  if (str_tag[i])
                        free(strings[i]);
                  return;
            }
      }
}

/*
**  return the leftmost N characters from a string, equivalent to LEFT$
*/

char *left(char *string, size_t N)
{
      char *buf;
      size_t strlength = strlen(string);

      if (N > strlength)
            N = strlength;
      buf = stralloc(N);
      memcpy(buf, string, N);
      buf[N] = NUL;
      return buf;
}

/*
**  return the rightmost N characters from a string, equivalent to RIGHT$
*/

char *right(char *string, size_t N)
{
      char *buf;
      size_t strlength = strlen(string);

      if (N > strlength)
            N = strlength;
      buf = stralloc(N);
      strcpy(buf, &string[strlength-N]);
      return buf;
}

/*
**  return a substring, N characters long beginning at position M,
**  equivalent to MID$
*/

char *mid(char *string, size_t M, size_t N)
{
      char *buf;
      size_t strlength = strlen(string);

      if (M > strlength)
            return NULL;
      if (N > (strlength - M))
            N = strlength - M;
      buf = stralloc(N);
      memcpy(buf, &string[M-1], N);
      buf[N] = NUL;
      return buf;
}

/*
**  string concatenation function, equivalent to A$=B$+C$+...
*/

char *string_add(char *string, ...)
{
      va_list arg_ptr;
      char *temp1, *temp2, *buf;

      va_start(arg_ptr, string);
      temp1 = string;
      do
      {
            if(NULL == (temp2 = va_arg(arg_ptr, char *)))
                  break;
            buf = stralloc(strlen(temp1) + strlen(temp2));
            temp1 = strcat(strcpy(buf, temp1), temp2);
      } while (NULL != temp2);
      return temp1;
}

/*
**  create a string of repeated characters, equivalent to STRING$()
*/

char *string(int ch, size_t N)
{
      char *buf;

      if (N)
      {
            buf = stralloc(N);
            memset(buf, ch, N);
      }
      buf[N] = NUL;
      return buf;
                  
}

/*
**  BASIC INSTR$() work alike - returns position (1-based) of findstr in
**  string, starting search at position start_pos (also 1-based).
**
**  Function suggested by Tika Carr
*/

unsigned int instr(const unsigned int start_pos,
                   const char        *string,
                   const char        *findstr)
{
      char *p;
      unsigned int pos;

      if (start_pos > strlen(string))
            return 0;
      else  pos = start_pos - 1;    /* BASIC offsets are one-based, not
                                       zero-based as in C               */

      if (NULL != (p = strstr(string + pos, findstr)))
            return (int)(p - (char *)string) + 1; /* Position 0 = position 1 */
      else  return 0;
}

8.2 Uhrzeit- und Datumsarithmetik

Datums- und Zeitfunktionen der Standard-Bibliothek

In C bezieht sich der Begriff Zeit auf Datum und Uhrzeit. Die Funktionsprototypen und die Definition der Struktur tm, die von vielen Zeitfunktionen verwendet werden, stehen in der Header-Datei time.h.

Die Zeitfunktionen von C stellen Zeitangaben auf zwei verschiedene Weisen dar.

Um die aktuelle Zeit der internen Uhr Ihres Systems abzufragen, verwenden Sie die Funktion time():
time_t time(time_t *ptr);
Die Funktion liefert die Anzahl der Sekunden zurück, die seit Mitternacht des 1. Januar 1970 verstrichen sind. Wenn der Funktion ein Nicht-NULL-Zeiger übergeben wird, speichert time() diesen Wert in der Variablen vom Typ time_t, auf die der Zeiger ptr zeigt. Die beiden folgenden Anweisunge speichern die aktuelle Zeit in der Variablen jetzt:
time_t jetzt;
jetzt = time(NULL);
Oder Sie verwenden die Rückgabe über das Argument:
time_t jetzt;
time(&jetzt);
Mit der Funktion localtime() wandeln Sie einen time_t-Wert in eine tm-Struktur um.
struct tm *localtime(time_t *ptr);
Diese Funktion liefert einen Zeiger auf eine statische Variable vom Typ tm zurück, so dass Sie keine Variable vom Typ tm deklarieren müssen, sondern nur einen Zeiger auf den Typ tm. Die statische Variable wird bei jedem Aufruf von localtime() wieder verwendet und überschrieben. Wenn Sie den zurückgegebenen Wert sichern wollen, muß das Programm eine separate Variable vom Typ tm deklarieren und in diese die Werte der statischen Variablen kopieren.

Die umgekehrte Konvertierung - von einer Variablen vom Typ tm in einen Wert vom Typ time_t - erfolgt mit Hilfe der Funktion mktime(). Der Prototyp lautet:

time_t mktime(struct tm *ntime);
Diese Funktion liefert die Anzahl der Sekunden, die zwischen Mitternacht des 1. Januar 1970 und der Zeit verstrichen sind, die durch die Variable vom Typ tm, auf die ntime zeigt, dargestellt wird.

Um Zeitangaben in formatierte Strings zu konvertieren, die ausgegeben werden können, gibt es die Funktionen ctime() und asctime(). Beide Funktionen liefern die Zeit als einen String in einem vordefinierten Format zurück. ctime() wird die Zeit als ein Wert vom Typ time_t übergeben wird, während asctime() die Zeit als eine Strukturvariable vom Typ tm entgegennimmt:

char *asctime(struct tm *ptr);
char *ctime(time_t *ptr);
Beide Funktionen liefern einen Zeiger auf einen statischen, nullterminierten 26-Zeichen-String zurück, der die Zeit des Funktionsarguments im folgenden 24-Stunden-Format angibt:
Thu Apr 30 11:22:15 2003
Beide Funktionen verwenden einen statischen String, der bei jedem Aufruf der Funktion überschrieben wird.

Wenn Sie das Format der Zeit ändern wollen, steht Ihnen dazu die Funktion strftime() zur Verfügung. Dieser Funktion wird die zu formatierende Zeitangabe als Strukturvariable vom Typ tm übergeben. Die Formatierung erfolgt anhand eines Formatstrings. Der Prototyp der Funktion lautet:

size_t strftime(char *s, size_t max, char *fmt, struct tm *ptr);
Die Funktion nimmt die zu formatierende Zeitangabe über die tm-Strukturvariable entgegen, auf die der Zeiger ptr weist, formatiert sie nach Vorgabe des Formatstrings fmt und schreibt das Ergebnis als nullterminierten String an die Speicherposition, auf die s zeigt. Das Argument max sollte die Größe des Speicherbereichs angeben, der für s reserviert wurde. Wenn der resultierende String (einschließlich des abschließenden Nullzeichens) mehr als max Zeichen enthält, liefert die Funktion 0 zurück, und der String s ist ungültig. Im anderen Fall liefert die Funktion die Anzahl der geschriebenen Zeichen zurück. Der Formatstring besteht aus einem oder mehreren der folgenden Konvertierungsspezifizierer:

Spezifiziererwird ersetzt durch:
%aAbgekürzter Wochentagsname
%AVoller Wochentagsname
%bAbgekürzter Monatsname
%BVoller Monatsname
%cDatums- und Zeitdarstellung (zum Beispiel, Tue Apr 18 10:41:50 2000)
%dTag des Monats als Dezimalzahl von 01 bis 31
%HDie Stunde als Dezimalzahl von 00 bis 23
%IDie Stunde als Dezimalzahl von 00 bis 11
%jDer Tag des Jahres als Dezimalzahl von 001 bis 366
%mDer Monat als Dezimalzahl von 01 bis 12
%MDie Minute als Dezimalzahl von 00 bis 59
%pAM oder PM
%SDie Sekunde als Dezimalzahl von 00 bis 59
%UDie Woche des Jahres als Dezimalzahl von 00 bis 53. Der Sonntag wird als erster Tag der Woche betrachtet
%wDer Wochentag als Dezimalzahl von 0 bis 6 (Sonntag = 0)
%WDie Woche des Jahres als Dezimalzahl von 00 bis 53. Der Montag wird als erster Tag der Woche betrachtet
%xDatumsdarstellung (zum Beispiel, 30-Jun-91)
%XZeitdarstellung (zum Beispiel, 10:41:50)
%yDas Jahr, ohne Jahrhundert, als Dezimalzahl von 00 bis 99
%YDas Jahr, mit Jahrhundert, als Dezimalzahl
%ZDer Name der Zeitzone, wenn die Information verfügbar ist, oder leer, wenn er nicht bekannt ist
%%Ein einfaches Prozentzeichen %

Sie können den Zeitunterschied zwischen zwei Zeitangaben mit dem Makro difftime() in Sekunden berechnen. Dieses Makro subtrahiert zwei time_t-Werte und liefert die Differenz zurück. Der Prototyp lautet:

double difftime(time_t zeit1, time_t zeit0);
Diese Funktion subtrahiert zeit0 von zeit1 und liefert die Differenz, das heißt die Anzahl der Sekunden zwischen den beiden Zeiten, zurück. Häufig wird difftime() dazu verwendet, die verstrichene Zeit zu berechnen.

Die Funktion clock() gibt an, wie viel Millionstelsekunde seit Beginn der Programmausführung verstrichen sind. Der Prototyp der Funktion lautet:

clock_t clock(void);
Um herauszufinden, wie viel Zeit für die Ausführung eines bestimmten Programmabschnitts benötigt wird, müssen Sie clock() zweimal aufrufen - vor und nach dem betreffenden Codeblock - und dann die beiden Rückgabewerte voneinander subtrahieren.
/* Beispiele fr die Verwendung der Zeitfunktionen. */

#include <stdio.h>
#include <time.h>

int main(void)
  {
  time_t beginn, ende, jetzt;
  struct tm *zgr;
  char *c, string[80];
  double dauer;

  /* Die Zeit des Programmstarts festhalten. */
  beginn = time(0);

  /* Die aktuelle Zeit festhalten. */
  time(&jetzt);

  /* Konvertiert den time_t-Wert in eine Struktur vom Typ tm. */
  zgr = localtime(&jetzt);

  /* Erzeugt einen formatierten String mit der aktuellen */
  c = asctime(zgr);
  puts(c);

  /* Verwendet die strftime()-Funktion, um verschiedene */
  /* formatierte Versionen der Zeit zu erzeugen. */

  strftime(string,80,"Dies ist %U. Woche des Jahres %Y",zgr);
  puts(string);
  
  strftime(string, 80, "Heute ist %A, %m/%d/%Y", zgr);
  puts(string);
  
  strftime(string, 80, "Es ist %M Minuten nach %I.", zgr);
  getc(stdin);

  /* Liest die aktuelle Zeit ein u. berechnet die Programmdauer. */

  ende = time(0);
  dauer = difftime(ende, beginn);
  printf("Ausfhrungszeit mit time() = %f Sekunden.\n", dauer);

  /* Gibt die Programmdauer mit clock() in Mikrosekunden an */
  printf("Ausfhrungszeit mit clock() = %ld .\n", clock());
      
  return(0);
  }

Datumsarithmetik

Die meisten dieser Algorithmen sind in der Literatur seit langem festgelegt. Es handelt sich um relativ einfache Berechnungen, weshalb keine Struktogramme beigefügt sind. Meine Informationen stammen aus einer Artikelserie von Heinz Zemanek in "Elektronische Rechenanlagen" 6/78, 4/79 und 6/79 und aus Jean Meeus: Astronomische Algorithmen, Barth, 1992, ISBN 3-335-00318-7.

Julianischer Tag

Es gäbe kein Jahr-2000-Problem, wenn alle Programmierer und Computerhersteller die Julianische Tageszahl für das Datum verwendet hätten. Stattdessen wurde das Jahr als Zeichenkette (TTMMJJ) gespeichert und das führte dazu, daß nach 99 das Jahr 00 kommt. Grund für die zweistellige Speicherung der Jahreszahl war der Zwang zum Speichersparen. Auch hier liegt die Julianische Tageszahl vorne, sie hätte noch weniger Speicher gebraucht.
Die Julianische Tageszahl - oder einfacher der Julianische Tag - ist eine fortlaufende Zählung der Tage, beginnend mit dem Tag 0, der am 1. Januar 4713 v. Chr. (im proleptischen Julianischen Kalender) um 12 Uhr Mittags begann. Dementsprechend beginnt ein neuer Julianischer Tag auch immer um 12 Uhr Mittags, was ursprünglich für die europäische Astronomie den Vorteil besaß, daß alle Beobachtungen einer Nacht an einem einzigen Julianischen Tag stattfanden.
Die Julianische Tageszählung läßt sich durch Anhängen des seit 12 Uhr Mittags verflossenen Tagesbruchteils leicht zu einer genauen Zeitangabe erweitern. So bezeichnet JD 2'451'605 den Tag, der am 1. März 2000 um 12 Uhr beginnt, während JD 2'451'605.25 den Zeitpunkt um 18 Uhr desselben Tages bestimmt. Diese Erweiterung wird in vielen Quellen als Julianisches Datum bezeichnet.
Julianische Tage wurden früher in der Regel (sofern nichts anderes spezifiziert wurde) nach mittlerer Sonnenzeit gezählt, heute nach UT. Die Weltzeit oder Universal Time (UT) wurde 1926 als Ersatz für die Greenwich Mean Time (GMT) eingeführt. Zur dieser Zeit waren mehrere, zum Teil deutlich unterschiedliche Bedeutungen von GMT im Gebrauch. UT ist für meisten praktische Belange gleichzusetzen mit der mittleren Sonnenzeit bezogen auf den Nullmeridian von Greenwich. Alternativ wurden Angaben auch in Ephemeridenzeit gemacht, was durch die Bezeichnung JED oder JDE gekennzeichnet wurde. Auch heute ist gelegentlich sinnvoll, Julianische Tage in einer anderen als der UT-Skala anzugeben. Die verwendete Zeitskala ist dann an die Zeitangabe anzuhängen, z.B. als JD 2 451 545.0 TDT für den 1.Januar 2000, 12 Uhr Mittags nach TDT-Zeit.
Häufig finden sich auch Zeitangaben in einem Modifizierten Julianischen Datumsformat (MJD). Die gebräuchlichste Definition eines MJD folgt aus MJD = JD - 2400000.5 der Nullpunkt liegt daher beim 17. November 1858 um 0 Uhr (!) Weltzeit. Andere Definitionen existieren allerdings auch, so daß bei der Verwendung von Daten in MJD Vorsicht geboten ist. Aus diesem Grunde wird MJD auch von der Internationalen Astronomischen Union nicht anerkannt.
Die Bedeutung der Julianischen Tagesangabe in der heutigen Astronomie liegt zum einen in der Möglichkeit einer kompakten, eindeutigen Zeitangabe, zum anderen in der einfachen Angabe und Berechnung von Zeitdifferenzen, Perioden usw.
Die Julianische Tageszählung wurde 1581 von dem französischen Gelehrten Joseph Justus Scaliger (in seinem Werk 'Opus novum de emendatione temporum') eingeführt, um eine eindeutige Zeitzählung ohne negative Jahreszahlen zu erhalten. Dazu mußte der Anfang der Zeitzählung genügend weit in der Vergangenheit in vorhistorischen Zeiten liegen. Scaliger konstruierte zunächst eine 7980 Jahre währende Julianische Periode, indem er folgende Zyklen kombinierte: Das letzte Jahr, in dem alle drei Zyklen gemeinsam einen neuen Durchlauf begannen, war 4713 v. Chr. Den 1. Januar dieses Jahres legte Scaliger als Beginn seiner Zeitrechnung fest. Für die meisten Menschen der damaligen Epoche war dieses Datum allerdings fiktiv, da nach ihrem Glauben die Welt erst wesentlich später erschaffen wurde. Scaliger selbst datierte die Erschaffung der Erde auf das Jahr 3267 v.Chr.

Der Algorithmus stellt sich dann folgendermaßen dar:
Y = Jahr, M = Monat (Januar = 1, Februar = 2, etc.),
D = Tag (eventuell mit Tagesbruchteilen)

Ist M > 2 lasse Y und M unverändert. 
Ist M = 1 oder M = 2 dann ist
   Y = Y - 1 
   M = M + 13 

Im Gregorianischen Kalender rechnet man weiter:
   A = INT (Y/100) 
   B = 2 - A + INT (A/4) 
Im Julianischen Kalender ist B = 0! 

Das gesuchte JD ist dann:
   JD = INT (365.25 * (Y + 4716)) 
   + INT (30.6001 * (M + 1)) + D 
   + B - 1524.5 
Das Programm zum Berechnen des Julianischen Datums stellt sich dann folgendermaßen dar. Beachten Sie, daß wegen der Besonderheit des Julianischen Datums der Tag nicht als Integer, sondern als Gleitpunktzahl dargestellt wird. Im Programm ist noch ein zweites Unterprogramm enthalten, das den JD wieder in unser gewohntes Datumsformat, das Gregorianische Datum, umrechnet:
#include <stdio.h>
#include <stdlib.h>

struct datum 
          {
          double tag;
          int monat;
          int jahr;
          };

double julian_day(double day, int month, int year);
struct datum gregorian_date(double jday);

int main(void)
  {
  int tag, monat, jahr;
  double jd;
  struct datum dat;

  printf("Tag, Monat und Jahr eingeben: ");
  scanf("%d %d %d",&tag, &monat, &jahr);
  jd = julian_day(1.0*tag, monat, jahr);
  printf("\nJulianischer Tag fuer den %d.%d.%d = %1.2f\n", 
         tag, monat, jahr, jd);
  dat = gregorian_date(jd);
  printf("\nZurueckgerechnet: %1.0f.%d.%d\n", dat.tag, dat.monat, dat.jahr);
  return 0;
  }


/* Julianischer Tag (>=1):
 * Gegeben: tag, monat, jahr
 *
 * Die gregorianische Kalenderreform wird beruecksichtigt (der Tag, der
 * auf den 4. Oktober 1582 folgt ist der 15. October 1582
 * Tage nach dem 15. Oktober 1582 werden als "Gregorianischer Kalender"
 * bezeichnet. Der Julianische Tag beginnt um 12 Uhr GMT (Mittag).
 * Um beliebige Uhrzeiten beruecksichtigen zu koennen, werden die Tage
 * nicht als Integer- sondern als Gleitpunktzahlen angegeben.
 */
double julian_day(double day, int month, int year)
  {
  int atmp, btmp, monthp, yearp;
  double ctmp;
  if (month > 2) 
    {
    monthp = month + 1;
    yearp = year;
    }
  else
    {
    monthp = month + 13;
    yearp = year - 1;
    }
  if ((year > 1582) || (year == 1582 && month >= 10)
     || (year == 1582 && month ==10 && day >= 15))
    {
    atmp = year / 100;
    btmp = 2 - atmp + (atmp / 4);
    }
  else
    btmp = 0;
  atmp = 365.25 * yearp;
  ctmp = atmp;
  atmp = 30.6001 * monthp;
  ctmp =  ctmp + atmp;
  return (ctmp + day + 1720994.5 + btmp);
  }

/* gregorian_date: Umrechnung Julianischer Tag 
   in (Tag, Monat, Jahr) */
struct datum gregorian_date(double jday)
  {
  int atmp, btmp, ctmp, dtmp, etmp, gtmp, ztmp;
  double ftmp;
  struct datum dd;

  ztmp = jday + 0.5;
  ftmp = (jday + 0.5) - ztmp;
  if (ztmp >= 2299161) 
    {
    gtmp = (ztmp - 1867216.25) / 36524.25;
    ctmp = gtmp / 4;
    atmp = ztmp + 1 + gtmp - ctmp;
    }
  else
    atmp = ztmp;
  btmp = atmp + 1524;
  ctmp = (btmp - 122.1) / 365.25;
  dtmp = 365.25 * ctmp;
  etmp = ((btmp - dtmp) / 30.6001);
  ztmp = 30.6001 * etmp;
  dd.tag = btmp - dtmp - ztmp + ftmp;
  if (etmp > 13.5)
    dd.monat = etmp - 13;
  else
    dd.monat = etmp - 1;
  if (dd.monat > 2.5)
    dd.jahr = ctmp - 4716;
  else
    dd.jahr = ctmp - 4715;
  return(dd);
  }

Datumsrechnung

Mit Hilfe des Julianischen Tages wir die Datumsrechnung relativ einfach. Zuerst die wichtigste Funktion, die Schaltjahresberechnung:

Schaltjahr

#include <stdio.h>
#include <stdlib.h>

int schaltjahr(int YY);

int main(void)
  {
  int jahr;
  printf("Jahr eingeben: ");
  scanf("%d", &jahr);
  if (schaltjahr(jahr))
    printf("\n%d ist Schaltjahr\n", jahr);
  else
    printf("\n%d ist KEIN Schaltjahr\n", jahr);
  return 0;
  }

int schaltjahr(int yy)
  /* 1, falls Schaltjahr, sonst 0 */
  {
  int sj;
  if      ((yy%400) == 0) sj = 1;
  else if ((yy%100) == 0) sj = 0;
  else if ((yy%4) == 0) sj = 1;
  else    sj = 0;
  return(sj);
  }

Die Funktion liefert einen Integerwert und man kann dann beispielsweise auch die Anzahl der Tage eines Jahres berechnen:
tage_im_jahr = 365 + schaltjahr(jahr);

Wochentagsberechnung

Die Wochentagsberechnung ist absolut einfach, wenn man den Julianischen Tag hat. Ergebnis: Wochentag 0=So, 1=Mo, 2=Di, 3=Mi, 4=Do, 5=Fr, 6=Sa:
wochentag = (Julianischer Tag + 1.5) % 7
Fur Daten nach dem 15.10.1582 gibt es noch einen andere Berechnungsweise:
/* Wochentagsberechnung aus Tag, Monat, Jahr (4-stellig) */
/* Ergebnis: Wochentag 0=So, 1=Mo, 2=Di, 3=Mi, 4=Do, 5=Fr, 6=Sa */
#include <stdio.h>
#include <stdlib.h>

int wota(int tt, int mm, int yy);

int main(void)
  {
  int tag, monat, jahr;
  printf("Tag, Monat und Jahr eingeben: ");
  scanf("%d %d %d",&tag, &monat, &jahr);

  printf("\nDer Wochentag ist %d\n", wota(tag, monat, jahr));

  return 0;
  }

int wota(int tt, int mm, int yy)
  {
  int wt, y, c;

  if (mm < 3)
    {
    mm = mm + 12;
    yy = yy - 1;
    }
  y = yy%100;
  c = yy/100;
  wt = (tt + (mm+1)*13/5 + y + y/4 + c/4 -2*c)%7;
  if (wt < 0) wt = wt + 7;
  return(wt);
  }

Jahrestag

Der Jahrestag reduziert sich auf eine effektive Zeile:
int day_of_year(double day, int month, int year)
  {
  return ((int) julian_day(day, month, year)
          - (int) julian_day(0.0, 1, year));
  }

Ebenso kann man auch ausrechen, wieviele Tage das Jahr noch hat:
days_remaining_in_year(double day, int month, int year)
  {
  return (365 + schaltjahr(year) - day_of_year(day, month, year));
  }

Der n-te Wochentag im Monat

Oft richten sich bestimmte Termine nicht nach einem Datum, sondern nach einem bestimmten Wochentag in der Nähe eines Datums, z. B. der erste Montag im Monat oder für Gehaltszahlung der letzte Donnerstag des Monats. Die Berechnung dafür liefert folgendes Unterprogramm. Gegeben wird der Wochentag (0 = Sonntag, 1 = Montag, usw.), der wievielte Wochentag es sein soll (1 - 5), Monat und Jahr (ab 1583).
int nth_day_of_week(int n, int day_of_week, int month, int year)
  {
  int atmp, btmp, ctmp, dtmp, etmp, ftmp, tmonth, tyear;

  if (month > 2) 
    {
    tmonth = month + 1;
    tyear = year;
	}
  else
    {
    tmonth = month + 13;
    tyear = year - 1;
    }
  atmp = 2.6 * tmonth;
  btmp = 1.25 * tyear;
  ctmp = (tyear / 100) - 7;
  dtmp = 0.75 * ctmp;
  etmp = (day_of_week - atmp - btmp + dtmp) % 7;
  if (etmp == 0)
    { 
    ftmp = 7; 
    n--; 
    }
  else
    ftmp = etmp;
  return (ftmp + (n * 7));
  }

Osterdatum (und bewegl. Feste)

Das christliche Osterfest ist aus dem jüdischen Passahfest abgeleitet, das am ersten Frühlingsvollmond beginnt. Dieser Tag kann offensichtlich auf einen beliebigen Wochentag fallen, Ostern beginnt dagegen definitionsgemäß am einem Sonntag. Ursprünglich war die Festlegung des Ostertermins sehr uneinheitlich geregelt in den verschiedenen christlichen Gemeinden. Erst im 1. Konzil von Nicäa im Jahre 325 n. Chr. einigte man sich auf die Formel, daß Ostern auf den ersten Sonntag nach dem Frühlingsvollmond nach der Frühjahrs-Tagundnachtgleiche fällt. Der erste Frühlingsvollmond ist dabei der erste Vollmond, der am Tag der Frühjahrs-Tagundnachtgleiche oder danach stattfindet.
Mit dem Beschluß von Nicäa waren aber die Schwierigkeiten nicht endgültig beseitigt, weil die genaue Festlegung des ersten Frühlingsvollmonds eigene Probleme mit sich brachte. Schließlich setzte der römische Abt Dionysius Exiguus auf Veranlassung von Papst Johannes I im Jahre 525 n. Chr. die in Alexandria übliche Rechnung durch. Danach wird
  1. der Frühlingsbeginn auf den 21. März 0 Uhr festgesetzt und
  2. von einem gleichmäßig auf einer Kreisbahn umlaufenden Mond ausgegangen.
Beide Annahmen sind Vereinfachungen, die zu Abweichungen von den wahren astronomischen Gegebenheiten führen. So findet der wahre Frühlingsbeginn etwa zwischen dem 19. März 8 Uhr und dem 21. März 20 Uhr UT statt. Berücksichtigung der wahren Mondbahn liefert Differenzen von bis zu +/- 0.7 Tagen gegenüber einer kreisförmigen Bahn. Ferner sind seit der Gregorianischen Kalenderreform zusätzliche Datumsbeschränkungen zu berücksichtigen, denen zufolge Ostern zwischen dem 22. März und dem 25. April (jeweils einschließlich) liegen muß. Aus diesen Gründen kommt es zu Verschiebungen des faktischen Osterdatums gegenüber dem astronomisch korrekt berechneten Datum, die als 'Osterparadoxien' bezeichnet werden. Die letzte Paradoxie fand im Jahre 1974 statt (Ostern war am 14. April statt am 7. April), die nächste findet im Jahr 2000 statt (23. April statt 26. März).
Durchgeführt wird die Osterrechnung heute durch die kirchlichen Ostertafeln (Tabellenwerke, die zu diesem Zwecke angelegt wurden) oder durch die Osterformel von Carl Friedrich Gauß. Beide Verfahren gelten für alle Jahre ab 532 n.Chr. Hier das Unterprogramm zu Berechnung des Osterdatums und ein Testprogramm dazu.
/* Berechnung des Osterdatums nach C. F. Gauss */
#include <stdio.h>
#include <stdlib.h>

struct datum 
          {
          int tag;
          int monat;
          int jahr;
          };
  
struct datum ostern(int yy);

int main(void)
  {
  int jahr;
  struct datum dat;
  printf("Jahr eingeben: ");
  scanf("%d", &jahr);
  dat = ostern(jahr);
  printf("\nOstern faellt %d auf den %d.%d.\n",dat.jahr, dat.tag, dat.monat);
  return 0;
  }

struct datum ostern(int year)
 /* Osterdatum */
  {
  int atmp, btmp, ctmp, dtmp, etmp, ftmp,
      gtmp, htmp, itmp, ktmp, ltmp, mtmp;
  struct datum dd;

  atmp = year % 19;
  btmp = year / 100;
  ctmp = year % 100;
  dtmp = btmp / 4;
  etmp = btmp % 4;
  ftmp = (btmp + 8) / 25;
  gtmp = (btmp - ftmp + 1) / 3;
  htmp = ((19 * atmp) + btmp - dtmp - gtmp + 15) % 30;
  itmp = ctmp / 4;
  ktmp = ctmp % 4;
  ltmp = (32 + (2 * etmp) + (2 * itmp) - htmp - ktmp) % 7;
  mtmp = (atmp + (11 * htmp) + (22 * ltmp)) / 451;
  dd.monat = (htmp + ltmp - (7 * mtmp) + 114) / 31;
  dd.tag = ((htmp + ltmp - (7 * mtmp) + 114) % 31) + 1;
  dd.jahr = year;
  return(dd);
  }

Die anderen beweglichen Feste lassen sich aus dem Osterdatum ableiten: Das Vorgehen ist ganz einfach. Man rechnet das Osterdatum in den Julianischen Tag um, addiert die o. a. Verschiebung und rechnet das Ergebnis wieder ins Gregorianische Datum um.

Datums- und Zeitfunktionen

Die ANSI-C-Standard-Bibliothek stellt Funktionen zur Verfügung, die einen Zugriff zur Systemuhr ermöglichen: Es werden drei verschiedenen Zeitdarstellungen verwendet: Die entsprechenden Funktionsdefinitionen befinden sich in der Standard-Header-Datei <time.h>. Diese ist daher bei Verwendung der Funktionen mittels #include <time.h> einzubinden. In <time.h> sind außerdem definiert:

Konstante CLK_TCK Anzahl der Perioden (ticks) der Systemzeit pro Sekunde

Typ clock_t

Arithmetischer Datentyp zur Darstellung der Zeit

Typ time_t

Arithmetischer Datentyp zur Darstellung der Zeit
Typ tm Structure-Typ zur Darstellung der Kalenderzeit

Der Typ tm muß wenigstens die folgenden Komponenten enthalten :

   int tm_sec;   /* Sekunden nach der Minute  [0 .. 59] */
   int tm_min;   /* Minuten nach der Stunde   [0 .. 59] */
   int tm_hour;  /* Stunden seit Mitternacht  [0 .. 23] */
   int tm_mday;  /* Tag des Monats            [0 .. 31] */
   int tm_mon;   /* Monate seit Januar        [0 .. 11] */
   int tm_year;  /* Jahre seit 1900                     */
   int tm_wday;  /* Tage seit Sonntag         [0 ..  6] */
   int tm_yday;  /* Tage seit 1.Januar        [0 ..365] */
   int tm_isdst; /* Sommerzeit-Flag                     */

Für den Wert des Sommerzeit-Flags tm_isdst gilt:

Funktionen zur Ermittlung von Zeiten und Zeitdifferenzen

clock_t clock(void); Ermittlung der Prozessorzeit, die seit Programmstart vergangen ist, in Systemzeit-Perioden (ticks).
clock()/CLK_TCK ist die Zeit in Sekunden.
Funktionswert = (clock_t)-1, wenn die Prozessorzeit nicht verfügbar ist.
time_t time(time_t *timer); Ermittlung der Kalenderzeit in einer implementierungsspezifischen Darstellung.
Falls timer!=NULL wird der Funktionswert auch *timer zugewiesen.
Funktionswert = (time_t)-1, wenn Kalenderzeit nicht verfügbar ist.
double difftime(time_t t2, time_t t1); Bildung der Zeitdifferenz t2 - t1 ausgedrückt in Sekunden (t2 und t1 enthalten Zeiten in implementierungsspezifischer Darstellung!)

Funktionen zur Umwandlung von Zeitdarstellungen

struct tm *gmtime(const time_t *tp); Umwandlung der in implementierungsspezifischer Darstellung vorliegenden Kalenderzeit *tp in eine strukturierte Darstellung der Coordinated Universal Time (UTC).
Falls die UTC nicht ermittelt werden kann, ist der Funktionswert = NULL
struct tm *localtime(const time_t *tp); Umwandlung der in implementierungsspezifischer Darstellung vorliegenden Kalenderzeit *tp in eine strukturierte Darstellung der Ortszeit.
time_t mktime(struct tm *tptr); Umwandlung der in strukturierter Darstellung vorliegenden Ortszeit *tptr in die implementierungsspezifische Darstellung der Kalenderzeit.
Die Werte der einzelnen Strukturkomponenten müssen nicht normiert sein. Die Funktion führt zusätzlich ihre Normierung durch und berechnet die Werte für die Komponenten tm_wday und tm_yday.
Falls keine darstellbare Kalenderzeit ermittelt werden kann, erzeugt die Funktion den Wert (time_t)-1.

Funktionen zur Darstellung von Zeiten als Strings

char *asctime(const struct tm *tptr); Umwandlung der in strukturierter Darstellung vorliegenden Ortszeit *tptr in einen String der folgenden Form:
"Sun Apr 14 11:23:22 1991\n\0"
Der Funktionswert ist Pointer auf diesen String.
char *ctime(const time_t *tp); Umwandlung der in implementierungsspezifischer Darstellung vorliegenden Kalenderzeit *tp in einen String der folgenden Form:
"Sun Apr 14 11:23:22 1991\n\0"
Der Funktionswert ist Pointer auf diesen String. ctime(tp) ist äquivalent zu asctime(localtime(tp)).
size_t strftime( char *s, size_t smax, const char *fmt, const struct tm *tptr); Ablage von in *tptr enthaltenen Zeit- und Datumsinformationen in den String s gemäß den im String fmt enthaltenen Formatspezifikationen.
Jeder sonstiger Text in fmt wird nach s übernommen. Der String s darf maximal smax Zeichen lang werden.
Funktionswert:
  • Anzahl der in s abgelegten Zeichen ohne abschließendes '\0'), wenn diese <=smax ist.
  • 0, wenn s länger als smax Zeichen werden würde. Der Inhalt von s ist in diesem Fall undefiniert.

Format-Spezifier für strftime():
%a    abgekürzter Name des Wochentags
%A    ausgeschriebener Name des Wochentags
%b    abgekürzter Name des Monats
%B    ausgeschriebener Name des Monats
%c    geeignete Darstellung der lokalen Zeit (Datum und Zeit)
%d    Tag des Monats als Dezimalzahl (01 .. 31)
%H    Stunde als Dezimalzahl (24-Std-Uhr) (00 .. 23)
%I    Stunde als Dezimalzahl (12-Std-Uhr) (01 .. 12)
%j    Tag des Jahres als Dezimalzahl (001 .. 386)
%m    Monat als Dezimalzahl (01 .. 12)
%M    Minute als Dezimalzahl (00 .. 59)
%p    PM oder AM (oder landesspezifisches Žquivalent)
%S    Sekunde als Dezimalzahl (00 .. 59)
%U    Woche des Jahres dezimal (Sonntag ist 1. Tag der Woche) (00 .. 53)
%w    Wochentag als Dezimalzahl (Sonntag = 0) (0 .. 6)
%W    Woche des Jahres dezimal(Montag ist 1. Tag der Woche) (00 .. 53)
%x    geeignete Darstellung des lokalen Datums
%X    geeignete Darstellung der lokalen Zeit
%y    (Jahr - Jahrhundert) als Dezimalzahl (00 .. 99)
%Y    Jahr als Dezimalzahl (einschließlich Jahrhundert)
%Z    Name der Zeitzone, falls ein Name existiert
%%    Das Zeichen %

8.3 Numerik

Primzahlen

Zerlegung in Primfaktoren oder die Aussage, ob eine Zahl Primzahl ist, wird bei verschiedenen Algorithmen genötigt, z. B. bei Kryptoverfahren. Um festzustellen, ob es sich um eine Primzahl handelt, kann man die Zahl X durch alle Zahlen teilen, die kleiner oder gleich X/2 sind; also durch 1, 2, 3, 4, 5, 6, 7, usw. Ist die Zahl durch keine der Zahlen teilbar, muß sie prim sein. Der Algorithmus kann dahingehnd verbessert werden, daß nur die ungeraden Faktoren und die 2 geprüft werden. im folgenden Programm liefert die Funktion is_prim() nur die Information darüber, ob eine Zahl prim ist, die Funktion primfaktoren() druckt die Primfaktoren einer Zahl.
#include <stdio.h>
#include <ctype.h>
#define TRUE 1
#define FALSE 0

long int getlong();
int is_prim(long int i);  
void primfaktoren(long int i);  

void main(void) 
  {	
  long int n, von, bis;
  printf("Berechnung der Primzahlen des Zahlenintervalls\n");

  printf("\nGeben Sie die untere Intervallgrenze ein:");
  von = getlong();
  
  printf("\nGeben Sie die obere Intervallgrenze ein:");
  bis = getlong();
  
  n = von;
  while(n <= bis) 
    {
    if (is_prim(n))
      printf(" %ld\n", n);
    n++;
    }
  }

int is_prim(long int i)  
  /* liefert TRUE, falls i eine Primzahl ist */
  {
  long int i;
  long int j;
  if (i < 2) return(FALSE);
  j = 2;
  while (j*j <= i) 
    {
    if (i % j == 0) 
      return (FALSE);
    else if (j == 2)
      j = 3;
    else 
      j = j + 2;
	}
	return (TRUE);
  }

void primfaktoren(long int i)  
  /* Druckt Primfaktoren einer Zahl */
  {
  long int i;
  long int j = 2;
  while (j*j <= i) 
    {
    while ((i%j) != 0)
      {
      if (j == 2)
        j = 3;
      else 
        j = j + 2;
      }
    i = i/j;
    printf(" %ld", j);
	}
  if (i > 1)
    printf(" %ld", i);
  printf("\n");
  }

long int getlong () 
  /* Ziffernfolge lesen und in long konvertieren */
  {
  long int i = 0;
  int c;
  c = getchar();
  while (isdigit(c)) 
    {
    i = 10*i + c - '0';
    c = getchar();
    }
  return(i);
  }

Auflösen arithmetischer Ausdrücke

U. a. ist die Aufgabe von Compilern das Aufbrechen von Formeln in eine für die lineare Maschinensprache geeignete klammerlose Form (Postfix-Notation). Auch die direkte Interpretation einer Formeleingabe ist eine häfig gestellte aufgabe, z.B. bei Kalkulationsprogrammen oder Formelinterpretern. Wie ein Arithmetischer Ausdruck aufgebaut ist, kann mit dem Schon bekannten Syntaxdiagrammen dargestellt werden - oder man verwendet die Textvariante, die Backus-Naur-Darstellung. Beim vorgesehenen Formelinterpreter sollen die vier Grundrechenarten sowie Klammerung realisiert werden. Natürlich gilt auch hier "Punkt vor Strich". Eine Formel kann dann folgendermaßen definiert werden:
     Ausdruck ::= Term { + Term | - Term }
     Term     ::= Faktor { * Faktor | / Faktor }
     Faktor   ::= Zahl | ( Ausdruck )
     Zahl     ::= [-] Ziffer {Ziffer} [. {Ziffer}]
     Ziffer   ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Dabei bedeutet: Die Ziffern sowie die Zeichen '(', ')', '+', '-', '*', '/' sind Terminalsymbole.

Zur Programmierung: Die folgenden Funktionen geben genau die Definition wieder ­ für Ausdruck, Term und Faktor müssen jeweils die Prozeduren definiert werden, die sich dann gegenseitig rekursiv aufrufen.
Es muß immer nur das nächste Eingabezeichen betrachtet werden, um die Aufgabe zu lösen ­ daher reicht getchar() aus. Das gelesene Zeichen c ist global definiert. Die folgenden Ansätze sind in einer Art "Pseudocode" skizziert, und ohne Deklarationen.

Ausdruck:
  {
  wert = term;
  solange (c in ['+','-'])
    {
    falls (c = '+') wert = wert + term;
    falls (c = '-') Wert = wert ­ term;
    }
  Ausdruck = wert;
  }

Term:
  {
  wert = faktor;
  solange (c in ['*','/'])
    {
    falls (c = '*') wert = wert * faktor;
    falls (c = '/') Wert = wert / faktor;
    }
  Term = wert;
  }
  
Faktor:
  {
  Lies ein Zeichen c von der Eingabe;
  wenn (c in ['0'..'9']) wert = Zahl;
  wenn (c = '(') 
    { 
    wert = Ausdruck; 
    Lies schlieîende Klammer;
    }
  Faktor = Wert;
  }
 
Zahl:
  { 
  Lies eine Zahl mit Vorzeichen zeichenweise ein;
  }

Damit das Bearbeiten eines Ausdrucks nicht nur von der Eingabe, sondern auch z. B. aus einem String heraus leicht möglich ist, definieren wir noch eine Funktion int lies() die dem Einlesen eines Zeichens dient.

#include <stdio.h>
#define FALSE 0
#define TRUE 1

int   c;            /* eingelesenes Zeichen */
float wert;         /* Ergebniswert */

/* Funktionen-Vorwaertsdeklaration */
float ausdruck(); 
float term();     
float faktor(); 
float zahl();   
int   lies(); 

main()
  {
  do
    {                                  
    printf("Eingabe : ");               /* Eingabeaufforderung */
    wert = ausdruck();                  /* Einlesen und Berechnen */
    printf("Ergebnis: %0.5f\n\n",wert); /* 5 Nachkommastellen */
    }
  while (c != EOF);                    /* Ende mit Ctrl-D oder Ctrl-Z */
  }


int lies()
  /* Einlesen eines Zeichens, Leerzeichen übergehen */
  {
  while((c = getchar()) == ' ');
  return(c);
  }

float ausdruck()
  /* Ausdruck ::= Term { +Term || -Term } */
  {  
  float wert;
  wert = term();                
  while (c == '+' || c == '-')  
    {
    if (c == '+') wert = wert + term();
    if (c == '-') wert = wert - term();
    }
  return(wert);
  } 

float term()
  /* Term ::= Faktor { *Faktor || /Faktor } */
  {  
  float wert;
  wert = faktor();
  while (c == '*' || c == '/')
    {
    if (c == '*') wert = wert * faktor();
    if (c == '/') wert = wert / faktor();
    }
  return(wert);
  }

float faktor()
  /* Faktor :== Zahl || ( Ausdruck ) */
  {  
  float wert;
  c = lies();                          /* erst hier wird gelesen */
  if (('0' <= c && c <= '9') || (c == '-'))
    wert = zahl();
  if (c == '(')                        /* rekursiver Aufruf! */
    {
    wert = ausdruck();
    c = lies();                        /* ')' wird ueberlesen */
    }
  return(wert);
  }

float zahl()
  /* Zahl ::= [-] Ziffer {Ziffer} [.{Ziffer}] */
  {  
  float wert, teiler;
  int negativ = FALSE;                /* Merker fr neg. Vorzeichen */
  int bruch   = FALSE;                /* Merker fr Dezimalpunkt */
  wert = 0.0; teiler = 1.0;
 if (c == '-')                        /* Vorzeichen merken */
    {
    negativ = TRUE;
    c = lies();
    }
  while (('0' <= c && c <= '9') || (c == '.'))
    {                                  /* Zahl konvertieren */
    if (c == '.')                      /* Dezimalpunkt */
      bruch = TRUE;
    else
      {
      wert = wert * 10.0 + (c - '0');  /* weitere Stelle dazu */
      if (bruch) teiler = teiler*10.0; /* fr Nachkommastellen */
      }
    c = lies();                        /* n„chstes Zeichen */
    }
  if (negativ) wert = -wert;
  if (bruch)                           /* nur dann ist teiler != 0 */
    return(wert/teiler);               /* Dezimalpunkt beachten */
  else
    return(wert);
  }

Nullstellenberechnung

Die Nullstelle einer Funktion f(x) soll näherungsweise bestimmt werden. Das Aufsuchen der Nullstelle einer Funktion f(x) entspricht der Lösung der Gleichung f(x) = O. Ist das Intervall [a, b] bekannt, in dem sich die Nullstelle befindet, so kann dieses halbiert werden und aufgrund des Funktionswertes an der Halbierungsstelle überprüft werden, ob sich die Nullstelle im linken oder im rechten Teilintervall befindet. Dieses Intervall wird nach dem gleichen Verfahren wieder halbiert. Dieses als Bisektion bezeichnete Verfahren wird solange wiederholt, bis das Intervall genügend klein ist.

Zur Programmierung dieses Verfahrens ist es günstig, eine Funktion zu vereinbaren, die als Ergebnis den Näherungswert der Nullstelle einer Funktion float f() liefert:

float nullstelle(float a, float b)
  {
  float X;
  do
    {
    X = (a + b )/2.0;
    if (f(a)*f(b) > 0)
      a = X;
    else
      b = X;
    }
  while ((b - a) > EPS);
  return(X);

Die Parameter a und b werden hier als lokale Hilfsvariable verwendet. Mit EPS wird dabei die Genauigkeit bezeichnet, ein Wert, der nahe beim kleinsten darstellbaren Wert für float liegt. Definition beispielsweise über #define EPS 1.0-10.

Anstatt das Intervall in jedem Iterationsschritt zu halbieren, kann es auch im Verhältnis der Funktionswerte an den Intervallgrenzen geteilt werden. Dieses als Regula Falsi bezeichnete Verfahren führt im allgemeinen rascher zum Ziel. In der obigen Funktion muß zu diesem Zweck die Anweisung

X = (a + b)/2.0;
durch
X = a - f(a)*(b-a)/(f(b)-f(a))
ersetzt werden. Um die wiederholte Berechnung der Funktionswerte f(a) und f(b) zu vermeiden, wurden im nachfolgenden Programmbeispiel Hilfsvariable fa, fb und fx verwendet.
float nullstelle(float a, float b)
  {
  float X, fa, fb, fx;
  fa = f(a);
  fb = f(b);
  do
    {
    X = a - fa*(b - a)/(fb - fa);
    fx = f(X);
    if (fa*fx > 0)
      a = X; fa = fx;
    else
      b = X; fb = fx;
    }
  while ((b - a) > EPS);
  return(X);

Das Verfahren von NEWTON erlaubt die näherungsweise Bestimmung der Nullstelle einer Funktion, deren Ableitung bekannt ist. Von einem Näherungswert X0 ausgehend, wird eine verbesserte Näherung X1 bestimmt, indem die Tangente an die Funktion an der Stelle f(X0) mit der X-Achse geschitten wird:

Die verbesserte Näherung X1 berechnet sich zu

X1 = X0 - f(X0)/f'X0

Die Vorschrift wird solange wiederholt, bis sich keine nennenswerte Verbesserung mehr ergibt. Man kann die Ableitung der Funktion annähern, indem man den Differenzenquotienten verwendet (df = (f(X+dH) - f(X))/dH;). Bei Funktionen, deren Ableitungsfunktion bekannt ist, kann man bei der Zuweisung an Fs auch der Funktionswert der Ableitungsfunktion berechnet werden.

double Newton (double X0, double Eps, int ItMax)
  /* X0: Startwert der Nullstellenbestimmung    */    
  /* Eps: Genauigkeit der Nullstellenbestimmung */    
  /* ItMax: Maximale Anzahl der Iterationen     */    
  {
  double Xi, Fi, Fs;
  double dH = 1.e-6;
  int    it = 0; 

  Xi = X0;                    /* Startwert */
  Fi = f(X0);                 /* Funktionswert bei X0 */
  Fs = (f(X0+dH) - f(X0))/dH; /* numerische Ableitung f'(X0) */
  do
    {
    /* verschwindende Ableitung abfangen */
    if (fabs(Fs) < Eps)
      return Xi;
    /* Begrenzung der Iterationen */
    if (it > ItMax)
      return Xi;
    /* Genauigkeit ueberpruefen */
    if (fabs(Fi) < Eps)
      return Xi;
    /* Berechnung des neuen Funktionswerts */
    Fi = f(Xi);
    Fs = (f(Xi+dH) - f(Xi))/dH; /* numerische Ableitung */
    Xi = Xi - Fi/Fs;
    it++;
    }
  while (1);
  return 0.0;
  }

Numerische Integration

Zur numerischen Integration einer Funktion kann diese in kleine Intervalle unterteilt werden. Innerhalb der Intervalle vird der Funktionsgraph durch Geraden angenähert.

Der Wert des Integrals wird durch die Summe der Tapezfl„chen angenähert:

Zur Programmierung dieses Näherungsverfahrens wird eine FUnktion verwendet, bei der die Intervallgrenzen sowie die Anzahl der Teilintervalle vorgegeben werden. Die zu integrierende Funktion wird definiert als Funktion float f().

float integral(float a, float b, int anz)
  {
  float sum, h;
  int i;
  
  h = (b - a)/anz;
  sum = (f(a) + f(b))/2.0;
  for(i = 1; i < anz; i++)
    sum = sum + f(a + float(i)*h);
  return(h * sum);
  }

Genauere Resultate erzielt man, ween beim Integreren nicht Geraden, sondern Parabelstücke verwendet werden. Die von SIMPSON gefundene Näherungsformel lautet dann

Die folgende Programmvariante verwendet die Simpson-Formel:

float integral(float a, float b, int anz)
  {
  float sum, h;
  int i, k;
  
  h = (b - a)/anz;
  k = 2;
  sum = f(a) + f(b);
  for(i = 1; i < anz; i++)
    {
    k = 6 - k;   /* 4, 2, 4, 2, ... */
    sum = sum + float(k)*f(a + float(i)*h);
    }
  return(h/3.0 * sum);
  }

Gausselimination (lin. Gleichungen)

Eine der ältesten direkten Methoden zur Lösung von linearen Gleichungssystemen ist die Gauss-Elimination. Gegeben ist ein Gleichungssystem

Ax = b

Wird die k-te Gleichung nach der Unbekannten xk aufgelöst, gilt für akk ungleich 0:

Bei der Gaußschen Eliminationsmethode werden die Ausgangsgleichungen so manipuliert, daß die Koeffizientenmatrix in ihrer Hauptdiagonalen den Wert 1 und unterhalb der Hauptdiagonalen nur Nullen enthält. Zwei Grundtypen von Matrixoperationen werden in der Gaußsehen Eliminationsmethode benutzt: skalare Multiplikation und Addition. Jede Gleichung kann mit einer Konstanten multipliziert werden, ohne das Ergebnis zu verändern. Dies ist äquivalent mit der Multiplikation einer Zeile der Koeffizientenmatrix und des zugehörigen Elementes im Konstantenvektor mit demselben Wert. Jede Gleichung kann auch durch die Summe zweier Gleichungen ersetzt werden.
Die folgenden Schritte werden die Benutzung der Gaußschen Ellmination anhand der folgenden Gleichungen erläutert:

  
        Koeffizienten-  Lösungs-
          Matrix A        Vektor b

        13   -8   -3        20
        -8   10   -1        -5
        -3   -1   11         0
Ziel der Elimination ist es, aus Ax = b eine äquivalentes System Cx = d mit C als obere Dreiecksmatix zu erzeugen:

Dieses System kann dann durch Rückwärtseinseten gelöst werden:

Rechnen wir das Beispiel einmal durch. Die erste Variable wird von allen außer der ersten Gleichung eliminiert. Ziel dieser Manipulation ist es, den Wert 1 als oberstes Element der ersten Spalte zu erzeugen. Die übrigen Positionen der Spalte werden 0. Dazu wird die gesamte erste Zeile durch das erste Element in der Zeile (das Pivot-Element) dividiert. Dies erzeugt den Wert 1 in der ersten Position der Diagonalen. Die erste Gleichung erhält dann folgendes Aussehen:

    [ 1  -0.61  -0.23] [1.5]
Die erste Unbekannte wird von der zweiten Zeile eliminiert, indem die neue erste Zeile mit dem ersten Element in der zweiten Zelle multipliziert und dann von der zweiten Zeile subtrahiert wird. Als neue zweite Zeile ergibt sich dann:
 Zeile 2:                 [ -8.00 10.00 -1.00 ]  [- 5]
 Minus Zeile 1 mal -8:    [ -8.00  4.88  1.84 ]  [ 12]
                         -----------------------------
 Ergibt:                  [  0.00  5.12  0.84 ]  [  7]
In ähnlicher Weise eliminiert man die erste Variable aus der dritten Gleichung. Die drei Gleichungen haben nun folgendes Aussehen:
   [ 1.00  -0.61  -0.23 ]  [1.5]
   [ 0      5.12   2.84 ]  [  7]
   [ 0      2.83  10.31 ]  [4.6]
Im nächsten Schritt wird der Wert 1 in der zweiten Position der zweiten Zeile (dem neuen Pivot-Element) erzeugt. Die zweite Zeile wird dazu durch das zweite Element dividiert. Die zweite Variable wird von der dritten Gleichung eliminiert, indem eine Null in der zweiten Position direkt unter dem Pivot-Element erzeugt wird. Die drei Gleichungen sehen dann so aus:
   [ 1.00  -0.61  -0.23 ]  [1.5]
   [ 0      1.00  -0.56 ]  [1.4]
   [ 0      0      8.7  ]  [8.7]
Der letzte Schritt dieser Phase besteht darin, den Wert 1 in der dritten Pivot-Position zu erhalten. Dies erreicht man durch Division der dritten Gleichung durch den Pivot-Wert. Insgesamt erhält man dann:
   [ 1.00  -0.61  -0.23 ]  [1.5]
   [ 0      1.00  -0.56 ]  [1.4]
   [ 0      0      1.00 ]  [1.0]
Das Ergebnis entspricht den drei Gleichungen:
    x - 0.6ly - 0.23z = 1.5
            y - 0.56z = 1.4
                    z = 1
Die dritte Gleichung kann direkt gelöst werden, da sie nur eine Unbekannte hat. Das Ergebnis lautet:
    z = 1
Die zweite Gleichung kann folgendermaßen umgeformt werden:
    y = 1.4 + 0.56z
Durch Einsetzung des Wertes von z ergibt sich für y der Wert 2. Die Einsetzung von y und z in die erste Gleichung liefert für x den Wert 3. Diese Phase der Berechnung Wird als "Rücksubstitution bezeichnet. Das Programm stellt sich dann so dar:

#include <stdio.h>

#define N 3                             /* Spalten/Zeilen der Matrix */
                                        /* = Anzahl der Gleichungen */
int main(void)
  { 
  int i, j, k;                          /* Schleifenzaehler */ 
  double f;                             /* Hilfsvariable */
  double a[N][N];                       /* Koeffizientenmatrix */
  double b[N];                          /* rechte Seite */
  double x[N];                          /* Loesung */

  /* Einlesen */
  for(i = 1; i <= N; i++)           
    { 
    for(j = 1; j <= N; j++) 
      scanf("%lg", &a[i-1][j-1]);
    scanf("%lg", &b[i-1]);
    }

  /* Elimination */
  for(i = 1; i <= (N-1); i++)                  
    { 
    for(k = i+1; k <= N; k++)
      { 
      f = a[k-1][i-1] / a[i-1][i-1];
      for(j = i+1; j <= N; j++) 
        a[k-1][j-1] -= f*a[i-1][j-1];
      b[k-1] -= f*b[i-1];
      }
    }

  /* Ruecksubstitution */
  for(i = N; i >= 1; i--)
    { 
    for(j = i+1; j <= N; j++) 
      b[i-1] -= a[i-1][j-1] * x[j-1];
    x[i-1] = b[i-1] / a[i-1][i-1];
    }

  /* Ausgabe */
  for(i = 1; i <= N; i++) 
    printf("x%d = %g\n", i, x[i-1]);

  return 0;
  }

Ein Problem ergibt sich, wenn die Pivot-Elemente relativ klein sind, weil dann - bedingt durch die begrenzte Rechengenauigkeit - sich die Rundungsfehler relativ stark auf das Endergebnis auswirken. Die Genauigkeit der Gaußschen Eliminationsmethode kann dadurch verbessert werden, daß man zwei Zeilen derart vertauscht, daß das Element mit dem größten absoluten Wert das Pivot-Element wird. Nehmen wir z. B. an, daß die vorhergehenden drei Gleichungen ursprünglich in einer anderen Reihenfolge geschrieben worden wären:

  
   [ -3   -1   11 ]  [  0 ]
   [ 13   -8   -3 ]  [ 20 ]
   [ -8   10   -1 ]  [ -5 ]
Die oberste Gleichung könnte dann durch -3 dividiert werden, um den Wert 1 in die Plvot-Position zu bringen. Das Ergebnis wird jedoch genauer, wenn wir zuerstst die erste und zweite Zeile vertauschen. Dies bringt den größeren Wert 13 in die Plvot-Position. Nach Elimination der ersten Variablen aus der zweiten und dritten Gleichung lautet das Ergebnis:
   [ 1 -0.6  -0.2 ]  [1.5]
   [ 0 -2.8  10.3 ]  [4.6]
   [ 0  5.1  -2.8 ]  [7.3]
Nun sollte man die zweite und dritte Gleichung vertauschen, um das größere Element 5.1 in die zweite Pivot-Position zu bringen. Es gibt einen weiteren Grund, warum Zeilen vertauscht werden müssen. Erscheint nämlich ein Null-Element (oder ein Wert nahe Null) in der Hauptdiagonalen, dann ist es nicht möglich, die Zeile durch dieses Pivot-Element zu dividieren ohne große Rundungsfehler zu erhalten. Der Austausch dieser Zeile mit einer darunterliegenden entfernt die Null aus der Pivot-Position.

Das Programm kann mit relativ wenig Aufwand erweitert werden:


#include <stdio.h>

#define N 3                             /* Spalten/Zeilen der Matrix */
                                        /* = Anzahl der Gleichungen */
int main(void)
  { 
  int i, j, k;                          /* Schleifenzaehler */ 
  int merk;                             /* Merker fuer Zeilentausch */
  double f;                             /* Hilfsvariable */
  double a[N][N];                       /* Koeffizientenmatrix */
  double b[N];                          /* rechte Seite */
  double x[N];                          /* Loesung */

  /* Einlesen */
  for(i = 1; i <= N; i++)
    { 
    for(j = 1; j <= N; j++) 
      scanf("%lg", &a[i-1][j-1]);
    scanf("%lg", &b[i-1]);
    }

  /* Elimination */
  for(i = 1; i <= (N-1); i++)
    { 
    merk = i;
    for(k = i+1; k <= N; k++)
      if (abs(a[k-1][i-1]) > abs(a[merk-1][i-1]))
        merk = k;
    if (merk != i) /* vertauschen */
      {
      for (j = i+1; j <= N; j++)
        {
        f = a[i-1][j-1]; 
        a[i-1][j-1] = a[merk-1][j-1];
        a[merk-1][j-1] = f;
        }
      f = b[i-1]; 
      b[i-1] = b[merk-1];
      b[merk-1] = f;
      }
    for(k = i+1; k <= N; k++)
      { 
      f = a[k-1][i-1] / a[i-1][i-1];
      for(j = i+1; j <= N; j++) 
        a[k-1][j-1] -= f*a[i-1][j-1];
      b[k-1] -= f*b[i-1];
      }
    }

  /* Ruecksubstitution */
  for(i = N; i >= 1; i--)
    { 
    for(j = i+1; j <= N; j++) 
      b[i-1] -= a[i-1][j-1] * x[j-1];
    x[i-1] = b[i-1] / a[i-1][i-1];
    }

  /* Ausgabe */
  for(i = 1; i <= N; i++) 
    printf("x%d = %g\n", i, x[i-1]);
 
 return 0;
 }

Um alle Fährnissen standzuhalten, sollte bei der Suche nach dem größten Pivot-Element auch gleich noch geprüft werden, ob die Matrix nicht singulär wird, d. h. ob es betragsmäßig größer als eine vorgegebene untere Schranke ist.

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


Copyright © FH München, FB 04, Prof. Jürgen Plate