Dátové objekty 2 - stringy


Obsah tém. 

  1. Dátový objekt reťazec (STRING)
  2. Dátový objekt POLE
    1. Jedno rozmerné pole znakov
  3. Dátový objekt POINTER 
  4. Príklady práce so stringom 
    1. Čítanie reťazca z klávesnice
    2. Tlač reťazca na obrazovku
    3. Štandardné funkcie pre prácu s reťazcom
    4. Formátované čítanie a zápis z a do reťazca
    5. Riadkovo orientovaný vstup/výstup z/do súboru
  5. Pole reťazcov
    1. Parametre "main" - vstupné parametre programu 
  6. Úlohy pre zopakovanie 

Dátový objekt reťazec (STRING)

Reťazec znakov je zvláštnym druhom jednorozmerného poľa. Sú zložené z prvkov typu char. Osobitne sa nimi zaoberáme z jedinej príčiny: Reťazce sú základom textových a databázových súborov.  Reťazcové konštanty zapisujeme v programe ako postupnosť znakov uzavretých medzi úvodzovkami. V pamäti je reťazec ukončený znakom  '\0', ale pri vypisovaní to nepoznať, lebo tento znak je "neviditeľný". Naopak, v prípade, že omylom zmažeme tento znak, programy pracujúce s reťazcom dovtedy budú hľadať koniec reťazca (stringu) pokiaľ náhodne nenarazia na najbližší znak  '\0'.  

Reťazce môžeme spracovávať jednak poliami a jednak pomocou pointrov. Pointre a polia sú v podstate zameniteľné technológie. 

Dátový objekt POLE

Pole je dátový objekt zložený z prvkov rovnakého typu. Znamená to zároveň, že každý prvok poľa zaberá rovnaké miesto v pamäti a pozícia (adresa) každého prvku sa dá vypočítať z bázovej adresy (počiatočnej adresy poľa) a poradového čísla prvku. Počet prvkov poľa sa určí pri deklarácií a tým sa vymedzí veľkosť adresného miesta celého poľa.

       Jednorozmerné pole znakov

Deklarácia poľa:

Deklarácia poľa je podobná deklarácií jednoduchej premennej s tým rozdielom, že za menom premennej uvedieme v hranatých zátvorkách počet prvkov poľa.
 

    dátový_typ meno_poľa[rozmer];

  • dátový_typ všeobecne určuje z akých prvkov bude pole zložené - reťazce (stringy) pozostávajú z char
  • meno_poľa tvorí identifikátor premennej typu pole
  • rozmer je počet prvkov z ktorých sa pole skladá. Je to kladné číslo.
napr.:
 
    char meno[20];

Čo vieme zistiť z deklarácie?

  • deklarácia poľa 20 - tich znakov
  • rozmer poľa je 20 prvkov
  • veľkosť 1 prvku poľa 1B, teda veľkosť poľa 20B
  • index prvého prvku poľa je 0 a posledného 19 (posledný znak musí byť  '\0' t.j. údaj má max. 19 znakov 0-18)
  • meno poľa je meno, pole je typu char

Počet prvkov poľa sa určí pri deklarácií. Údaj o počte prvkov je možné v deklarácií vynechať ( ak je pole definované inde) alebo v definícií (kde inicializujeme všetky prvky).

           Inicializácia

Pri definícií je možné pole zároveň aj inicializovať. Vtom prípade nasleduje za menom poľa a hranatými zátvorkami znak rovnosti a za ním v zložených zátvorkách hodnoty, ktoré sa majú priradiť jednotlivým prvkom. V prípade stringov nahradia zložené zátvorky dvojité úvodzovky. Za platnými znakmi string sa automaticky doplní o koncový znak '\0'. Pozor na číslo v hranatých zátvorkách. Koncový znak musí byť súčasťou stringu (započítava sa!). 

Prvkom poľa čísel, ktoré neinicializujeme sa zvyčajne automaticky priradí hodnota 0. V prípade poľa znakov obsahujúceho kratší string než je dĺžka poľa (t.j. za hodnotou  '\0' do konca poľa budú voľné pozície) bude až do konca poľa obsahovať náhodné údaje. 
Ak nezadáme  údaj o rozmere poľa určí si ho prekladač na základe počtu platných prvkov uvedených pri inicializácii. V prípade reťazca znakov rozmer poľa sa nastaví na počet platných znakov  +1 (ukončovací  '\0') naviac. 

char meno[20]="Jano";

je to isté ako:

char  meno[20]={ 'J','a','n','o','\0' };

alebo

char  meno[]="Jano";  // v tomto prípade vzniklo pole meno[5] 


          Práca s poľom

K jednotlivým prvkom poľa sa pristupuje pomocou indexov, ktoré sa zapisujú do hranatých zátvoriek za menom poľa. Index môže byť konštanta, ale aj premenná.  Samotný identifikátor poľa predstavuje adresu 0-tého prvku poľa t.j. začiatok poľa.
Index prvku poľa určuje jeho poradie, pričom index prvého prvku poľa je 0 a posledného je rozmer-1. Prvky poľa sú uložené v pamäti za sebou.

Prvkom v poli priradzujeme hodnoty dvoma spôsobmi a to buď v cykle napr. pomocou funkcie scanf alebo  priamo, vymenovaním jednotlivých hodnôt. Tento spôsob si však vyžaduje poznať hodnoty vopred napr.:    


int    pole[10] = { 1,4,5,7,8,9,6,5,54,23}; // pole[9] == 23
int    pole[]   = { 1,4,5,7,8,9,6,5,54,23}; // identická definícia

Pri druhom spôsobe nemusíme uvádzať počet prvkov poľa v hranatých zátvorkách

 

Dátový objekt POINTER

Predstavuje premennú určenú výlučne na adresovanie pozície dát v pracovnej pamäti. T.j. jej obsahom je vždy ADRESA. Ak chceme pracovať so samotným údajom na danej adrese, predradíme pred názov pointra referenčný znak *. 


   int x;                // obyčajná premenná typu integer
   int  *p_x= &x;        // pointer p_x na premennú x (&x je adresa premennej)
   char *p_meno= "Jano"; // definícia pointra p_meno na typ char a string Jano\0

Pointer môže obdržať adresu už existujúcej premennej, alebo existujúceho poľa obyčajným priraďovacím príkazom. Podobne ako definícia poľa je spojená s typom premennej, aj pointer je deklarovaný na dátový typ (t.j. pointer na daný typ, resp. adresa na daný typ).  Vďaka možnosti zmeny adresy v obsahu pointra sú pointre predurčené pre prácu s dynamickými dátami - t.j. pridelenie pamäti, odobranie pamäti, zmena adresy v pointri, ... Tak ako polia aj pointre majú vždy určené koľko pamäťového priestoru zaberá typ premennej na ktorú pointer ukazuje. V predošlom príklade kompilátor uložil reťazcovú konštantu "Jano" do dátovej oblasti, na koniec doplnil \0, a priradil jej začiatok do pointra p_meno. 

Dynamické prideľovanie adresného priestoru zabezpečuje počas behu programu príkaz malloc() - memory allocation

   char *s_dyn = NULL;                        // vytvorenie prázdneho pointra
   s_dyn= (char*) malloc(10 * sizeof(char));  // pridelenie priestoru pre 10 znakov
   strcpy(s_dyn,"Ahoj");   // naplnenie priestoru stringom "Ahoj"
   s_dyn="JAJAJ";          // kompilátor uloží reťazcovú konštantu a naplní adresou
                           // pointer s_dyn !!! pritom sa stratí pôvodná oblasť
     // posledný príkaz nie je priradenie HODNOTY "JAJAJ" ale priradenie jeho adresy
  

 Pointer s_dyn z predošlého príkladu najprv ukazuje na pamäťovú oblasť vyčlenenú pre uloženie 10 znakov do ktorej sa skopíroval string "Ahoj". Neskôr obdrží adresu stringu "JAJAJ" (a to preto lebo tento reťazec je typu char*) , ale pritom sa stratí oblasť so stringom "Ahoj". Aby sme neprišli o časť pamäte východiskom je Uvolnenie pamäti

   free ((void *) s_dyn);  // uvolnenie pridelenej pamäti do HEAPU
   s_dyn=NULL;             // korektné nastavenie ak by sme s_dyn nepotrebovali
   s_dyn="JAJAJ";          // teraz je to OK aj keď NULL bolo zbytočné
   *s_dyn='x';             // string JAJAJ sa zmenil na xAJAJ

Ešte  raz si dobre všimnite rozdiel medzi s_dyn="JAJAJ";  a  *s_dyn='x';. Prvý príkaz hovorí. Budeme mať uložený string "JAJAJ" v dátovej oblasti. Objekt "JAJAJ" na pravej strane príkazu je vnímaný ako typ char*, a preto sa priradením naplní adresa s_dyn hodnotou ADRESY stringu "JAJAJ". Iný je prípad *s_dyn, ktorý  je typu char a preto priradenie naplní cieľ t.j. pozíciu na adrese s_dyn hodnotou 'x'. String "JAJAJ" sa zmenil na "xAJAJ". 

Príklady pre prácu so stringom

       Čítanie reťazca z klávesnice

    char *s1=NULL;
    s1=(char *)malloc(100 * sizeof(char));
    scanf("%s",s1);     // problém by bol s1=NULL  ak by sme nepoužili malloc
    s1="xxxxxxxxxxxx";
    scanf("%11s",s1);   // s1 ukazuje na string 11+1 do ktorého možno scanovať
    

Pri čítaní reťazca scanf() z klávesnice sa načíta celý úsek až po oddeľovač medzera, resp. ENTER.  Na začiatku sa vynechajú biele znaky a potom sa načítajú platné znaky až po prvý biely znak, zvyšok do konca riadka sa ignoruje. Ak by sme chceli načítať riadok aj s bielymi znakmi potrebujeme použiť miesto scnaf() funkciu  gets(). Format použitý na čítanie reťazca je "%11s". Načítavanie je obmedzené na 11 znakov kde sa 12. znak doplní \0. Pri načítavaní reťazca sa predpokladá, že zodpovedajúca premenná predstavuje adresu na ktorú sa uloží načítaný string.

         Riadkovo orientované čítanie klávesnice        

Ak by sme použili scanf() na reťazec "Ahoj clovece" načíta sa reťazec "Ahoj" zvyšok sa stratí. Načítanie celého riadka až po záverečný ukončovač riadku \n uskutoční gets(). 

   char *gets(char *str);  // prototyp načítania celého riadka

Funkcia gets() číta celý riadok až po znak '\n' a uloží ho do reťazca str, ktorý ukončí znakom  '\0'. Znak '\n' sa neukladá. Zároveň gets() vracia pointer na reťazec str. Pokiaľ bol riadok prázdny, vracia NULL a do str[0] vloží znak  '\0' t.j. nulový reťazec. 

       Tlač reťazca na obrazovku

    printf("%s",s1); 

Pri tlači sa uvádza najčastejšie hodnota vyjadrená premennou. Pri tlači napr. celého čísla pomocou pointra by najskôr bol použitý podobný tvar  *p_i  (tu *s1 ), ale pri tlači reťazca sa uvádza adresa, výnimka,  pretože formát  "%s" vyžaduje adresu. 

Nasledovný príklad ukazuje prácu s reťazcom pomocou pointra.

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


void main(void)
{
   char * str;
   if ( (str= (char *) malloc(11* sizeof(char))) == NULL )
   {
      printf("Málo pamäte\n");
      return ;
      }
    printf("Zadaj reťazec:");
    scanf("%10s",str);   
    printf("Načítaný reťazec je: %s\n",str);
   }

         Riadkovo orientované tlačenie na obrazovku        

Aj keď na tlač riadka dá sa použiť klasický príkaz printf("%s \n", str) nastáva problém v menšej efektivite výsledného kódu, existuje efektívnejší príkaz:

   int puts(char *str);  // prototyp tlačenia stringu do celého riadka

Príkaz puts() vytlačí string a doplní ho znakom '\n'. V prípade že vznikne problém tlače vráti kód EOF, inak vráti nezáporné číslo. 

 

       Štandardné funkcie pre prácu s reťazcom

C jazyk nedefinuje prácu s reťazcami, napriek tomu sú k dispozícii štandardné knižnice, ktoré obsahujú funkcie na prácu so stringami. Na tento účel je potrebné pripojiť hlavičkový súbor  string.h príkazom:

    #include < string.h >

   Dĺžka reťazca - strlen

   int strlen(char *s);
   dlzka=strlen("Ahoj");     // vráti do dlzka hodnotu 4

Vracia dĺžku reťazca bez ukončovacieho znaku \0. 

   Kopírovanie reťazca - strcpy

   char *strcpy(char *s1, char *s2);
   adr=strcpy(str,"Ahoj");      // v str bude "Ahoj"

Skopíruje obsah reťazca s2 do reťazca s1. Vracia pointer na prvý znak reťazca s1. 

   Spojenie reťazcov - strcat

   char *strcat(char *s1, char *s2);
   adr=strcat(str," + Nazdar");      // v str bude "Ahoj + Nazdar"

Pripojí reťazec s2 k reťazcu s1. Vracia pointer na prvý znak reťazca s1. 

   Nájdenie znaku v reťazci - strchr

   char *strchr(char *s, char c);
   adr=strchr(str,'x');    // v str "Ahoj + Nazdar" nenájde 'x' preto vráti NULL

Pokiaľ sa v reťazci s nachádza znak c, potom vracia pointer na jeho prvý výskyt. Inak vráti NULL. 

   Porovnanie dvoch reťazcov - strcmp

   int strcmp(char *s1, char *s2);
   cislo=strcmp(str1,str2);      // =0 ak sú rovnaké - ak str1<str2 + str1>=str2 

V prípade rovnosti vracia číslo 0, ak je str1 lexikograficky menšie než str2 vráti záporný výsledok inak kladný.  

   Nájdenie podreťazca v reťazci - strstr

   char * strstr(char *s1, char *s2);
   adr=strstr(str1,str2);      // ak nenájde vráti NULL inak adresu začiatku 

Pokiaľ sa v reťazci s1 nachádza podreťazec s2 vráti adresu prvého výskytu inak vráti NULL 

   Práca s obmedzenou časťou reťazca t.j. -n- znakov
   strnlen, strncpy, strncat, strnchr, strncmp, strnstr.

   char * strncpy(char *s1, char *s2, int MAX);
   adr=strncpy(str,"alkoholicke",7);  // z druheho stringu kopiruje MAX 7 znakov 

V príklade sa skopíruje do s1 reťazca 7 znakov z s2 reťazca. Ak by bol strlen(s2) <= max pridá sa na koniec  \0.

   Práca s reťazcami odzadu - reverse - "-r-"
   strrlen, strrcpy, strrcat, strrchr, strrcmp, strrstr.

   char *rstrchr(char *s, char c);
   adr=rstrchr(str,'x');    // v str "Ahoj + Nazdar" nenájde 'x' preto vráti NULL

V príklade ak sa znak 'x' nevyskytne pri prehľadávaní str odzadu vracia sa NULL inak sa vráti pointer na jeho posledný výskyt (prvý odzadu).

   Prevody reťazcov na čísla (stdlib.h)-   
   atoi /ASCII to Integer/, atof  /ASCII to Float/, .

   int atoi(char *string);    // konvertuje string na celé číslo
   long atol(char *string);   // konvertuje string na long - celé číslo
   double atof(char *string); // konvertuje string na double - reálne číslo
   intcislo=atoi("125");      // asci kódy '1','2','5','\0' vytvoria hodnotu 125 

Funkcia atoi konvertuje reťazec obsahujúci číslo na premennú typu integer. Funkcia atof konvertuje reťazec obsahujúci číslo na premennú typu float. 

   

       Formátované čítanie a zápis z a do reťazca

Podobne ako funkcia printf(), alebo ešte lepšie fprintf(),  určená na tlač resp. zápis do  existujú funkcie sprintf() "tlačiace" do reťazca umiestneného do operačnej pamäte.  Následne sa dajú použiť všetky reťazcové funkcie C jazyka. 

Analogicky k funkcii scnaf(), a súborovej fscanf(), existuje  aj jeho obdoba sscanf(). Úlohou xscanx  je zosnímať zo zdroja údaje a umiestniť ich na pozíciu uvedenej premennej. Funkcia sscanf() sníma údaje z reťazcovej premennej a umiestňuje do inej premenej. Preto kombinácia sprintf() a sscanf() vytvárajú ideálnu dvojicu na transformáciu údajov - reťazec na číslo a číslo na reťazec. 

Príklad: nasledovný program najprv načíta 4 hexadecimálne číslice a tieto potom skonvertuje na číslo. Následne číslo zkonvertuje na osmičkové číslo vo forme reťazca a reťazec vytlačí na obrazovku. 

#include <  stdio.h >

void main(void)
{
   int i;
   char s1[5],s2[10];

   printf("Zadaj 4 hexadecimálne číslice: ");
   scanf("%s",s1);
   sscanf(s1,"%x",&i);
   sprintf(s2,"%o",i);
   printf("%s \n",s2);
   }

Príklad: x

void uradnik(char * zpr, double polomer)
{
  double obsah, obvod;

  obsah= 3.14 * polomer * polomer ;
  obvod= 3.14 * 2 * polomer ;
  sprintf(zpr,"Kruh s polomerom %d má obsah %d a obvod %d \n",polomer,obsah,obvod);
  }

void sef(void)
{
  char sprava[120];

  uradnik(sprava,5.0);
  strcat(sprava,"\t\tVypracoval sef\n");
  printf("Dovoľujem si predniesť správu o kruhu: \n %s",sprava);
  }

       Riadkovo orientovaný vstup/výstup z/do súboru

        Čítanie riadka zo súboru

Načítanie riadka zo súboru sa uskutočňuje prototyp funkcie fgets(), ktorý číta zo súboru fr až do konca riadka nanajvýš max znakov včítane znaku '\n', a uloží ho do str. Funkcia vracia pointer na str, alebo pri dosiahnutí konca súboru vráti NULL. 

     char *fgets(char * str, int max, FILE * fr);

Poznámky:

  • Správne prečíta aj posledný riadok neukončený znakom  '\n',
  • Nakoľko fgets() vie určiť aj veľkosť načítavaného riadka, môže sa použiť pre načítavanie z terminálu: fgets(s,max,stdin); 
  • Po načítaní znaku End-Of-File tento sa zvyčajne nenačíta. 

Príklad:

Program prečíta obsah súboru DOPIS.TXT po riadkoch a vypíše jeho obsah na displej. Dĺžka načítavaného riadka je obmedzená na 80 znakov. 

#include < stdio.h >

int main(void)
{
  char riadok[80];
  FILE *fr;

  if ((fr=fopen("DOPIS.TXT","r"))==NULL)
  {
    printf("Súbor DOPIS.TXT sa nedá otvoriť \n");
    return;
    } 

  while(fgets(riadok,80,fr)!=NULL) printf("%s",riadok);

  if (fclose(fr)==EOF)
    printf("Súbor DOPIS.TXT sa nedá uzatvoriť  \n");
  }

        Zápis riadka do súboru

Ak chceme zapísať reťazec do súboru a neodriadkovať (odriadkovanie je v reťazci), potom použijeme funkciu fputs(), ktorá má nasledovný prototyp:

     int  fputs(char * str, FILE * fw);

Funkcia po zápise reťazca neodriadkuje ani nezapisuje do súboru ukončovací znak reťazca  '\0'. Jej návratová hodnota je nezáporné číslo. V prípade neúspechu vracia EOF. 

       Pole reťazcov

 Pole reťazcov je jedno z najčastejšie používaných dvojrozmerných polí (vid. p_text[1][2]). Prvý rozmer je zoznam (pole) pointrov na druhý rozmer - pole znakov (string). Polia znakov (t.j. stringy) môžu mať rôzne dĺžky. Takmer každý program pracujúci s textom môže využívať pole reťazcov. 

     char *p_text[4] ;           // definícia poľa 4 pointrov na reťazce

     p_text[0]="Jahoda";
     p_text[1]="Fero";
     p_text[2]=(char *)malloc(6*sizeof(char)); // vyclečleň z HEAP-u 5+1
     ctrcpy(p_text[3],"Marek");                // naplň pole ako kópiu
     p_text[3]="Eva";
//-------------------------------- A L T E R N A T Í V N E  R I E Š E N I E

     char *p_pole[]={"Jahoda","Fero","Marek","Eva"};  
     
     printf("Tretí znak druhého reťazca je %c", p_text[1][2]);

Výpis  obsahu hociktorého stringu môžeme realizovať jednak ako tlač znakov, jednak ako tlač celého stringu. V nasledovnom príklade prvá metóda je zameraná na tlač znaku a nasledovné dva na dva rôzne spôsoby tlačenie stringov  putchar, printf, puts. Puts je výhodnejší pri porovnaní s printf, nakoľko printf je univerzálnejší a potrebuje analyzovať a transformovať tlačený obsah. 

// 1. ----------------
   char *p_pom=p_text[0];          // pomocný pointer na adresu znaku
                                   // p_text[0] - adresa 0.-tého stringu
    while (*p_pom != '\0') putchar(*p_pom++); //tlac znak a adresuj nasledovný
                                   // podmienka ukončenia cyklu je nájdenie '\0'
// 2. ----------------
    printf("%s \n",p_text[1]);     // p_text[1] je adresa reťazca
// 3. ----------------
    puts(p_text[2]);               // p_text[2] je adresa reťazca

Pozor na umiestnenie niektorých operátorov pri využívaní "špecialít" C jazyka. V 4. príklade použijeme pointer na pole pointrov  na reťazce (string-y). Po nastavení p_pom na adresu p_text  pri jeho definícii výraz (++*p_pom)  by zapríčinil posun p_pom adresy na pozíciu druhého znaku (počítajú sa od 0 t.j. p_text[0][1]) od začiatku prvého reťazca  t.j. p_text[0]. Správne potrebujeme použiť výraz (*p_pom++), ktorý zaadresuje pozíciu prvého znaku druhého reťazca (t.j. p_text[1][0]).

// 4. ----------------
    char **p_pom=p_text;    // p_pom je pointer na pointer na znaky (pole stringov)
                            // p_text je adresa pola pointrov

    puts(++*p_pom);         // Pozor! výpisal by "ahoda" miesto "Jahoda". Zodpovedá
    puts(++(*p_pom));       // mu identický príkaz inkrementuj operand na adrese
                            // *p_pom a potom vypíš zvyšok ako string 

    for(i=0 ; i<4 ; i++)    // Správne vypíše obsah všetkých 4 reťazcov
      puts(*p_pom++);       // Výpíše reťazec a posunie sa na nasledovný 
                            // pointer v poli pointrov (p_pom++) 

       Parametre "main" - vstupné parametre programu 

Funkcia main je vstupnou aj výstupnou bránou programu. V prípade ak je main definované ako int vracia pri ukončení programu návratovú hodnotu ktorá sa zapíše do systemovej premennej ERRORLEVEL, ktorá je čitateľná v dávkových súboroch alebo v iných programoch, ktoré štartovali našu aplikáciu. 

Vstupné parametre funkcie main môžu byť void, alebo presne definované nasledovným spôsobom:

     int main(int argc, char * argv[])

Pri štarte vzorového programu 

   test.exe param1 param2

obdrží program nasledovné parametre argc bude rovný 3 t.j. počet prvkov poľa pointrov na  reťazce "test", "param1" a "param2" nasledovným spôsobom:

     argv[0]  - pointer na test
    
argv[1]  - pointer na param1
    
argv[2]  - pointer na param2

Príklad:  Volanie programu test bude obsahovať viacero parametrov uzavretých do úvodzoviek. Obsah v úvodzovkách sa bude ponímať ako jeden reťazec aj keby obsahovali v sebe oddeľovače. 

  test "Nazdar"  ako  " sa mas clovece?"

     argv[0]  - pointer na test
    
argv[1]  - pointer na Nazdar
    
argv[2]  - pointer na ako
     argv[3]  - pointer na sa mas clovece

Vstupné parametre programov sú teda normálne reťazce a dá sa s nimi pracovať funkciami na prácu s reťazcami.

V nasledovnom príklade vypíšeme vstupné parametre na obrazovke.

   #include < stdio.h >
   void main(int argc, char * argv[])
   {
     int i;
     
     printf("Vstupný riadok má %d parametrov \n", argc);
     for (i=1; i< argc; i++)  
        printf("%s \n", argv[i] ); 
     }

 

Tu budem ešte doplňovať ...

Príklad č._ _  :
Program, ktorý načíta z klávesnice n desatinných čísel a vypíše ich v obrátenom poradí a načítanom poradí. Kód \n pri vypisovaní znamená - prejdi na nový riadok.

#include<stdio.h>
#include<conio.h>

void main(void)      // void - znamená "žiaden typ - žiadna premenná"
{
   ...
   }

Tu budem ešte doplňovať ...

Úlohy pre zopakovanie

  1. N................
        1. x........
        2. v...
        3. u....
        4. v.....
  2. N.....
  3. N.....
        1. v....
        2. u...
        3. v....
        4. v.....
  4. N.....