Розглянемо деякі приклади розв'язання задач

1. Хеш-таблиці та алгоритми хешування. Як приклад використання структур і списків наведемо програму, що ілюструє роботу з хеш-таблицею. Спочатку розглянемо, що таке хеш-таблиці та загальні принципи роботи з ними. Хеш-таблиця – це спосіб організації даних, що поєднує в собі переваги списків і масивів. Припустимо, що нам необхідно зберігати інформацію про набір деяких рядків, кожному з яких відповідає певний ключ (інший рядок, якась константа тощо). Іншими словами, потрібно зберігати пари str1 str2 str3... strnkey1 key2 key3... keyn Логічно для збереження кожного елемента пари відповідностей вибрати структуру, що матиме принаймні два поля: для збереження рядка та його ключа (корисним буде й поле-покажчик на саму структуру): struct cell {struct cell *next; /*покажчик на черговий елемент*/char* key; /*ключ*/char* val; /*значення*/};

Для розв'язання задач можна використовувати масив таких структур, списки, дерева різного типу тощо. Головним при цьому є питання швидкодії. Для організації даних у вигляді хеш-таблиці визначимо масив покажчиків, кожен елемент якого зберігатиме адресу першого елемента деякого однозв'язного списку.

hashtable[0]->hashtable[1]->hashtable[2]->hashtable[3]->hashtable[HASHSIZE]->

До одного списку записуватимуться елементи таблиці, для яких однакове значення має спеціальна хеш-функція. Хеш-функція, аналізуючи рядок, повертає деяке ціле значення, що є індексом відповідного елемента масиву покажчиків (hashtable). У найпростішому варіанті значення функції хешування отримується як залишок від ділення суми символьних значень рядка на розмір масиву. При цьому доступ до голови відповідного списку здійснюється моментально: hashtable[hashfunc(...)]. Розмір масиву фіксований (у нашій програмі HASHSIZE дорівнює 21). У загальному випадку його вибір – це досить складне питання, він залежить від розміру таблиці та інших факторів. Наведемо всю програму з коментарями за її окремими блоками:

#include <stdio.h>#include <string.h> /*прототип для strchr()*/extern void *malloc(unsigned size);/*типи ключа та значення: у нашому випадку – це рядки*/typedef unsigned char uchar;typedef uchar *VAL; typedef uchar *KEY; /*Для роботи необхідно реалізувати операціїint HASHFUNC(KEY); int EQKEY(KEY, KEY);void FREEVAL(VAL); void SETVAL(VAL, VAL);void FREEKEY(KEY); void SETKEY(KEY, KEY);*/#define HASHSIZE 21 /*обсяг масиву*/uchar *strudup(const uchar *s){/*створення копії рядка в "купі"*/ uchar *p=(uchar*) malloc(strlen(s)+1); strcpy(p,s); return p;}/*одна з можливих хеш-функцій*/unsigned int hash; /*останнє пораховане значення хеш-функції*/int HASHFUNC(KEY key){unsigned int i=0; uchar *keysrc=key;while(*key){ i=(i<<1)|(i>>15); /*ROL*/ i^=*key++;}hash=i% HASHSIZE;printf("hash(%s)=%d\n", keysrc, hash); return hash;}#define EQKEY(s1, s2) (strcmp(s1, s2)==0)#define FREEKEY(s) free(s)#define FREEVAL(s) free(s)#define SETVAL(at,s) at=strudup(s)#define SETKEY(at,s) at=strudup(s)#define KEYFMT "%s"#define VALFMT "%s" /*=========== типонезалежна частина ============*/struct cell {struct cell *next; /*покажчик на черговий елемент*/KEY key; /*ключ*/VAL val; /*значення*/}*hashtable[HASHSIZE]; /*хеш-таблиця*/ /*отримання значення за ключем*/struct cell *get(KEY key){struct cell *p;for(p=hashtable[HASHFUNC(key)]; p; p=p->next) if(EQKEY(p->key, key)) return p; return NULL; /*відсутнє*/} /*занести пару ключ: значення в таблицю*/void set(KEY key, VAL val){struct cell *p; /*перевірити, чи не було елемента з таким ключем*/if((p=get(key))==NULL){/*не було*/ if(!(p=(struct cell*) malloc(sizeof(*p)))) return; SETKEY(p->key, key); p->next=hashtable[hash]; /*hash обчислене в get()*/ hashtable[hash]=p;} else /*уже було: змінити значення*/ FREEVAL(p->val);SETVAL(p->val, val);} /*знищення за ключем*/int del(KEY key){int indx=HASHFUNC(key);struct cell *p, *prev=NULL; if((p=hashtable[indx])==NULL) return 0; for(;p;prev=p, p=p->next) if(EQKEY(p->key, key)){ FREEVAL(p->val); FREEKEY(p->key); if(p==hashtable[indx]) /*голова списку*/ hashtable[indx]=p->next; else prev->next=p->next; free((void*)p); return 1; /*знищений*/ } return 0; /*не було такого*/} /*роздрукувати пару ключ: значення*/void printcell(struct cell *ptr){ putchar('('); printf(KEYFMT, ptr->key); putchar(','); printf(VALFMT, ptr->val); putchar(')');} /*друк таблиці (для налагодження)*/void printtable(){ register i; struct cell *p; printf("----TABLE CONTENTS----\n"); for(i=0; i<HASHSIZE; i++) if((p=hashtable[i])!=NULL){ printf("%d: ", i); for(; p; p=p->next) printcell(p), putchar(' '); putchar('\n'); }} /*ітератор*/struct celliter { int index; struct cell *ptr;};/*видати чергове значення*/struct cell *nextpair(struct celliter *ci){ struct cell *result; while((result=ci->ptr)==NULL){ if(++(ci->index)>=HASHSIZE) return NULL; /*більше немає*/ ci->ptr=hashtable[ci->index]; } ci->ptr=result->next; return result;}/*ініціалізація ітератора*/struct cell *resetiter(struct celliter *ci){ ci->index=(-1); ci->ptr=NULL; return nextpair(ci); /*перше значення*/} void main(){ /*таблиця з імен і розмірів файлів поточного каталогу*/ struct celliter ci; struct cell *cl; char key[40], value[40]; struct cell *val; extern FILE *popen(); FILE *fp; char *s; /*popen() читає виведення команди, заданої в 1-му аргументі*/ fp=popen("ls -s", "r"); while(fscanf(fp, "%s%s", value, key)==2) set(key, value); pclose(fp); /*popen() потрібно закривати pclose();*/ for(;;){printf("-> "); /*запрошення*/if(!gets(key)) break; /*EOF*/if(*key=='-'){/*КЛЮЧ: видалити*/ printf(del(key+1)? "OK\n": "немає такого\n"); continue; } if(!*key||!strcmp(key,"=")){ /*=:роздрукувати таблицю*/ printtable(); continue; } if(s=strchr(key,'=')){ /*КЛЮЧ=ЗНАЧЕННЯ: додати*/ *s++='\0'; set(key, s); continue; } if((val=get(key))==NULL) /*КЛЮЧ: знайти значення*/ printf("немає такого ключа\n"); else{printf("значення"); printf(VALFMT, val->val); putchar('\n'); } } /*друк таблиці за допомогою ітератора*/ for(cl=resetiter(&ci); cl; cl=nextpair(&ci)) printcell(cl), putchar('\n');}

2. Складені описувачі. Напишемо програму, яка:

а) підраховує кількість усіх допустимих у мові С складених описувачів заданої довжини;

б) генерує довільний складений описувач заданої довжини;

в) друкує всі допустимі складені описувачі заданої довжини.

У допустимих описувачах ураховуватимемо лише тривіальні ознаки масивів і функцій – дужки [ ], (). Розглянемо поняття довжини як кількість кроків інтерпретації.

Якщо позначити через V ознаку покажчика, М – масиву, F – функції, то за правилами розшифровки довільна послідовність FFVVMFVV задає схему утворення описувача (інтерпретуємо її зліва направо). Описувач буде допустимим, якщо в цій послідовності немає комбінацій вигляду FM (масив функцій), FF (функція повертає функцію), MF. Схему створення допустимого складеного описувача можна подати так:

V – покажчик M-масив F-функція

V-V M-V F-V

V-M M-M

V-F

Якщо останньою була ознака покажчика, то вона може утворити всі три ознаки, масиву – дві й, нарешті, ознака функції – лише одну ознаку покажчика.

Для підрахунку кількості всіх можливих описувачів ми можемо використати лише три змінних – v, m, f типу long, які б зберігали в собі на кожному кроці кількість описувачів, у яких останньою при інтерпретації була б відповідно ознака покажчика, масиву та функції. Неважко помітити, що ці кількості за 1 крок зростають за правилом

v1=v+m+f;

m1=m+v;

f1=v;

Наведемо відповідний підрахунок у вигляді функції:

Long kilkist(int n)

{

long v=1, m=1, f=1, v1,m1,f1;

int i;

for(i=1;i<n;i++)

{

v1=v+m+f;

m1=m+v;

f1=v;

v=v1;

m=m1;

f=f1;

}

return (v+m+f);

}

Тоді main -функція має вигляд

#include <stdio.h>

Void main(void)

{

int n;

printf("введіть довжину процесу інтерпретації описувача n>=1\n');

scanf("%d',&n);

printf("шукана кількість: %ld\n',kilkist(n));

}

Нескладний аналіз показує, що код, записаний у тілі циклу for у функції kilkist(), може бути спрощений і записуватися без використання допоміжних змінних v1, m1, f1.

Тоді функція має вигляд

Long kilkist(int n)

{

long v=1, m=1, f=1, v1;

int i;

for(i=1;i<n;i++)

{

v+=m+f;

f=v-m-f;

m+=f;

}

return (v+m+f);

}

Для розв'язання другої частини задачі спочатку отримаємо всі можливі схеми інтерпретації указаної довжини та реалізуємо їх. Для цього нам необхідно мати:

a функцію, що генерує схеми;

a функцію-інтерпретатор, що будує конкретний описувач за заданою схемою;

a функцію, що формує список.

Ураховуючи вищезгадану схему, можемо вибрати різні структури даних, наприклад тернарні дерева. Однак ми зупинимось на однозв'язному списку, одночасно демонструючи технологію роботи зі списками. Виберемо таку структуру:

Struct inter

{

struct inter *next; // покажчик на наступний елемент

char *s; // поле, де зберігається інтерпретаційна схема


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: