sábado, 30 de novembro de 2013

Árvore Binária Genérica 2ª Parte

No primeiro passo para montar a aplicação através das primitivas apresentadas para árvore binária, cria a estrutura SBINTREE como mostrado a seguir, que será referenciado no código pelo ponteiro BT.

int main(int argc, char *argv[])
{
      SBINTREE *BT;

      // configura a estrutura de dados da arvore binaria
      BT = CreateBinTree(     CreateData,
                             DeleteData,
                             DuplicatedNode,
                             NodeDataCmp,
                             PrintData);
. . .

                Na lista abaixo de primitivas genéricas, exceto pela rotina CreateBinTree (responsável por iniciar a árvore), todas as rotinas recebem como primeiro parâmetro o endereço (ponteiro BT) da estrutura que abriga os itens pertinentes à árvore.

Primitivas Genéricas:
SBINTREE *CreateBinTree(  
  void *( * fCreateData )    ( void * ),
  BOOL ( * fDeleteData )     ( void * ),        
  int  ( * fDuplicatedNode ) ( SLINK, SLINK ),
  int  ( * fNodeDataCmp )    ( void *, void *), 
  int  ( * fTraversal )    ( void *, void *, int));



BOOL AddNode( SBINTREE *, void *);

BOOL DeleteNode( SBINTREE *, void * );

SLINK FindNode( SBINTREE *, void * );

BOOL DestroyBinTree( SBINTREE * );



BOOL TraversalTree(void *, SBINTREE *, ETRAVERSAL );

BOOL PrintForest(FILE *, SBINTREE *);

Com a lógica da árvore concluída, basta incluir o tratamento dos especifico dos dados a serem utilizados, as primitivas FindNode, DeleteNode e AddNode precisam saber como comparar dados, pois dependem de tal mecanismo para percorrer a arvore, DeleteNode precisa tratar a liberação de memória do nó e AddNode, precisa saber o quanto reservar de memória como também o que fazer com informação duplicada.

Com isso fica claro entender o porquê das primitivas sempre receber como parâmetro a estrutura SBINTREE, através dela será possível acessar rotinas especificas para o tratamento dos dados.

Por exemplo, a função AddNode(), precisa ser capaz de comparar os dados do nó a ser incluído com o nó que já se encontra na árvore,  para isto, chama a rotina de comparação indicada por fNodeDataCmp em BT, BT é um ponteiro para a estrutura SBINTREE que estamos acessando, portanto, BT->fNodeDataCmp é o atual ponteiro de função. Para chamar a função se utiliza o símbolo * de referencia.

       BT = CreateBinTree( CreateData,
                           DeleteData,
                           DuplicatedNode,
                           NodeDataCmp,
                           PrintData);
. . .
       *(BT->fNodeDataCmp)(slkNew, slkCurr);


A sintaxe para chamar a rotina através do ponteiro foi simplificada pelo uso de macro de (*(L->fNodeDataCmp)) para NodeDataCmp. Portanto, ao usar a macro NodeDataCmp(a, b), tem o efeito de chamar a função e passar os argumentos a e b. Todas as rotinas especificas da aplicação seguem esse modelo.

#define CreateData                (*(BT->fCreateData))
#define DeleteData                (*(BT->fDeleteData))
#define DuplicatedNode            (*(BT->fDuplicatedNode))
#define NodeDataCmp               (*(BT->fNodeDataCmp))

Todas as funções que seguem com o destaque em branco, se referem às mascaras criadas pela macro para indicar o momento em que o trabalho dependente do tipo de informação é incluso no código de tratamento da árvore.

PRIVATE SLINK CreateNode(SBINTREE *BT, void *pData)
{
. . .
       // agora chama a aplicacaoh especifica para alocacah de dados
       slkNewNode->pData = CreateData(pData);
. . .


BOOL AddNode(SBINTREE *BT, void *pData)
{
. . .

       slkNew = CreateNode(BT, pData);   // cria o noh
. . .
             // Decide qual direcaoh seguir
             iCompare = NodeDataCmp(slkNew->pData, slkCurr->pData);     
             if (iCompare == 0)         // noh duplicado
             {
                    // o que fazer com noh duplicado
                    iAction = DuplicatedNode(slkNew, slkCurr);

. . .

BOOL DeleteNode( SBINTREE *BT, void *pData )
{
. . .
       while (slkCurr != NULL &&
             (iCompare = NodeDataCmp(slkTmp->pData, slkCurr->pData)))
       {

. . .

       DeleteData(slkDel->pData);        // apaga o dado do noh
       free(slkDel);                     // libera a memoria do noh

. . .

SLINK FindNode( SBINTREE *BT, void *pData )
{
. . .
       while (slkCurr != NULL &&
             (iCompare = NodeDataCmp(slkTmp->pData, slkCurr->pData)))
. . .


Toda a parte dependente dos dados fica nos arquivos btapp.h e btapp.c, onde fica declarada que tipo de dados será tratado e como suas rotinas funcionam, para o aplicativo exemplo será utilizado uma estrutura para armazenar uma palavra e um contador de duplicados, serve ao propósito de mostrar o armazenamento e tratamento de copias, além de ser simples de adaptar para outras soluções, portanto no arquivo cabeçalho temos:

// noh consiste em:
typedef struct tagSNODEDATA
{
    char *pWord;           // ponteiros para palavras
    unsigned int uCont;    // e a contagem de ocorrencias
}SNODEDATA;

typedef SNODEDATA *      pND;

void *CreateData( void * );
BOOL  DeleteData( void * );
int   DuplicatedNode( SLINK, SLINK );
int   NodeDataCmp( void *, void * );
int   PrintData(FILE *, SLINK, int);

                Todo o trabalho que for necessário para gerenciar e tratar a informação dentro do nó fica no arquivo btapp.c, neste arquivo se encontram todas as rotinas utilizadas pelas primitivas.

                NodeDataCmp deve retornar o resultado da comparação entre dois strings, basta retornar o resultado da comparação dos dados através do da função strcmp.

int NodeDataCmp(void *first, void *second)
{
    return (strcmp( ((pND) first)->pWord, ((pND) second)->pWord));
}

                DuplicateNode define o tratamento da árvore para dados duplicados, a função deve retornar um dos seguintes valores, zero apagar o nó duplicado ou um inserir o nó duplicado. Neste caso estamos contando as palavras, se uma palavra duplicada é encontrada, incrementa-se o contador e não inclui o dado duplicado.

int DuplicatedNode(SLINK slkNewNode, SLINK slkListNode)
{
    pND pnd;

    // transforma o ponteiro de dados do noh, no da aplicacaoh
    pnd = slkListNode->pData;
    // adiciona-se uma ocorrencia para a palavra existente
    pnd->uCont += 1;

    return 0;
}

                Todo o processo de adicionar um nó passa por três alocações de memória, na primeira AddNode reserva o nó genérico de uma árvore, que consiste nos ponteiros esquerdo e direito da subarvore, mais um ponteiro genérico para os dados, CreateData irá montar a informação desejada e retornar o endereço para o nó genérico.

SNODE






LEFT
RIGHT






POINTER
à
SNODEDATA






INT






POINTER
à
STRING

CreateData aloca memória para a estrutura SNODEDATA que consiste em um ponteiro e um inteiro, e para a string que foi copiada através da rotina _strdup que aloca e copia a informação de apontada por data retornando o endereço para pWord.

void *CreateData(void *data)
{
    SNODEDATA *pNewData;

    // aloca sua estrutura de dados
    pNewData = (SNODEDATA *) malloc(sizeof(SNODEDATA));
    if (pNewData == NULL)
        return (NULL);

    // move os valores para a estrutura de dados
    pNewData->uCont = 1;
    pNewData->pWord = _strdup((char *) data);

    if (pNewData->pWord == NULL)   // erro copiando a string
    {
        free(pNewData);
        return (NULL);
    }
    else
    {
        return (pNewData); // retorna o endereco da estrutura
    }
}

DeleteData tem de lidar com as duas reservas de memória que foram criadas por CreateData, uma para a estrutura SNODEDATA e a outra para a string, neste caso SNODEDATA consiste em: um ponteiro e um inteiro como o ponteiro que indica a string estão armazenados na estrutura apontada por pND, é obrigatório liberar a string antes da estrutura de tipo SNODEDATA.

BOOL  DeleteData(void *data)
{
       // duas reservas de memoria foram criadas
       // uma para a estrutura SNODEDATA,
       // outra para a string
       // SNODEDATA consiste em: 1 ponteiro e 1 inteiro.
       // O inteiro junto do ponteiro retorna para a memoria
       // quando a estrutura for liberada
       // nescessario liberar a string antes do ponteiro
       free( ((pND) data)->pWord );
       free(data);
       return TRUE;
}

                PrintData será utilizada como a função recursiva utilizada pelas rotinas que percorrem e imprimem a árvore, nela quando solicitado, imprime as informações dos dados na saída desejada.

int PrintData(FILE *fOut, SLINK slkNode, int iLevel)
{
      return fprintf(   fOut,
                        "(%i) %s %u",
                        iLevel,
                        ((pND) slkNode->pData)->pWord,
                        ((pND) slkNode->pData)->uCont);
}

                Fica assim completa as rotinas de especificas para árvore binária genérica, faltando agora o aplicativo em si, para facilitar o exemplo, nos arquivos btdriver.h e btdriver.c foram incluídas toda a interface com o usuário, assim quando for o núcleo do programa rodar teremos o seguinte código:

int main(int argc, char *argv[])
{
       SBINTREE *BT;

       // configura a estrutura de dados da arvore binaria
       BT = CreateBinTree( CreateData,
                                        DeleteData,
                                        DuplicatedNode,
                                        NodeDataCmp,
                                        PrintData);

       if (BT == NULL)
       {
             fprintf(stderr, "Erro criando arvore binaria\n");
             exit(EXIT_FAILURE);
       }

       // controla as opcoes para teste
       // na arvore criada
       // possui um loop interno que serah finalizado
       // quando o usuario escolher a opcaoh
       Menu(BT);

       // libera toda a memoria utilizada pela arvore binaria
       DestroyBinTree(BT);

       return (EXIT_SUCCESS);
}

                É criada a arvore BT, e passada para a rotina Menu() que irá permitir ao usuário escolher que operações deseja realizar na árvore indicada por BT.

void Menu( SBINTREE *BT )
      {
      char cTmp[10];
      int iChoice;
      BOOL bLoop = TRUE;

      while (bLoop)
      {
            printf("\n\n( 1 ) Inserir\n");
            printf(    "( 2 ) Apagar\n");
            printf(    "( 3 ) Busca\n");
            printf(    "( 4 ) Imprime\n");
            printf(    "( 5 ) Carrega arquivo\n");
            printf(    "( 6 ) Salva\n");
            printf(    "( 0 ) Sair\n\n");


            Input("Escolha umas das alternativas: " , cTmp, 10);
            if ( cTmp[0] != '\0' && (isdigit(*cTmp)) )
            {
                  iChoice = atoi(cTmp) ;
                  switch ( iChoice ) {
                        case 1:     Insert(BT);       break;
                        case 2:     Delete(BT);       break;
                        case 3:     Search(BT);       break;
                        case 4:     Display(BT);      break;
                        case 5:     LoadFile(BT);     break;
                        case 6:     SaveLog(BT);      break;
                        case 0:     bLoop = FALSE;    break;
                  }
            }
      }
}

                Dentre as rotinas Insert (inserir), Delete (Apagar) e Search (Procurar) serve como chamada para as primitivas da arvore binária.

PRIVATE void Insert( SBINTREE *BT )
{
       char strWord[STR_SIZE];

       for ( ; ; ) {
             Input("Incluir palavra : " , strWord, STR_SIZE );
             if ( strWord[0] == '\0' ) break;

             // insere na arvore
             if (AddNode( BT, strWord ) == FALSE)
                    fprintf(stderr, "ERRO! naoh adicionou noh.\n");
       }
}

PRIVATE void Delete( SBINTREE *BT )
{
       char strWord[STR_SIZE] ;

       Input("Excluir palavra : " , strWord, STR_SIZE );

       // apaga
       if (DeleteNode( BT, strWord ))
             printf("\nNoh removido.");
       else
             printf("\nNaoh encontrou noh para remover.");
}

PRIVATE void Search( SBINTREE *BT )
{
       char strWord[STR_SIZE] ;

       Input("Procurar palavra : " , strWord, STR_SIZE );

       // apaga
       if (FindNode( BT, strWord ))
             printf("\nEncontrou noh.");
       else
             printf("\nNaoh encontrou noh.");
}


                Para as rotinas de Display (Imprime) e SaveLog (Arquivo) usam a rotina que desenha a árvore resultante PrintForest, a diferença fica por conta dos parâmetros de entrada aonde Display utiliza a tela através de stdout e SaveLog passa o arquivo o ponteiro para o arquivo log.txt, como é esperado, stdout direciona a saída para a tela enquanto fLog salva em arquivo.

PRIVATE void Display( SBINTREE *BT )
{
       PrintForest(stdout, BT);
}

PRIVATE void SaveLog( SBINTREE *BT )
{
       FILE *fLog;

       fLog = fopen("log.txt", "at");
       if (fLog)
       {
             PrintForest(fLog, BT);

             fclose(fLog);
       }
       else
             fprintf(stderr, "ERRO AO CRIAR LOG\n");
}

                Por fim, ainda está anexa a possibilidade de utilizar um arquivo texto para adicionar palavras na árvore, semelhante ao utilizado para testar a tabela de dispersão encadeada, a rotina LoadFile (Carrega arquivo) cuida de percorrer a entrada escolhida pelo usuário.

PRIVATE void LoadFile( SBINTREE *BT )
{
       FILE *fin;                 // arquivo de entrada
       char strWord[STR_SIZE] ;
       char *ptrStr;
       int iChar;
       int i = 0;

       Input("Nome do arquivo : " , strWord, STR_SIZE );


       fin = fopen(strWord, "rt");
       if (fin)
       {
             // comecah a processar o arquivo
             while (!feof(fin))
             {
                    // percorre pulando espacosh e pontuncaoesh
                    do {
                           iChar = getc(fin);
                    } while ( iChar != EOF &&
                          (isspace(iChar) || ispunct(iChar)));
                                
                    // tendo um caracter valido
                    // comecah a montar a palavra
                    ptrStr = strWord;
                    do {
                           *ptrStr++ = iChar;
                           iChar = getc(fin);
                    } while (    iChar != EOF &&
                                  !isspace(iChar) &&
                                 !ispunct(iChar) );

                    // fim do arquivo, sai do loop
                    if (iChar == EOF)
                           break;

                    // fecha a palavra com o terminador nulo
                    *ptrStr = '\0';

                    // todos os caracteres em maiuscula
                    ptrStr = strupr(strWord);

                    // insere na tabela
                    AddNode( BT, ptrStr);
                    i++;

                    if ((i %1000) == 0)
                           fprintf(stdout, "Tratou palavras %u\n", i);

             }      // while (!feof(fin))

             fclose(fin);
       }
}

                A interface, mesmo sendo em modo texto é bem simples e permite salvar a árvore no arquivo log.txt, o tratamento da rotina faz com que as impressões sejam sempre anexadas no arquivo, assim cada vez que incluir ou excluir dados, peça para salvar a árvore e será possível visualizar a movimentação dos nós dentro da árvore.

( 1 ) Inserir
( 2 ) Apagar
( 3 ) Busca
( 4 ) Imprime
( 5 ) Carrega arquivo
( 6 ) Salva
( 0 ) Sair

Escolha umas das alternativas anteriores : 1
Incluir palavra : 5
Incluir palavra : 3
Incluir palavra : 1
Incluir palavra : 4
Incluir palavra : 2
Incluir palavra : 0
Incluir palavra : 7
Incluir palavra : 9
Incluir palavra : 6
Incluir palavra : 8
Incluir palavra :


( 1 ) Inserir
( 2 ) Apagar
( 3 ) Busca
( 4 ) Imprime
( 5 ) Carrega arquivo
( 6 ) Salva
( 0 ) Sair

Escolha umas das alternativas anteriores : 4
    +-(2) 9 1
    | +-(3) 8 1
  +-(1) 7 1
  | +-(2) 6 1
+-(0) 5 1
  | +-(2) 4 1
  +-(1) 3 1
    | +-(3) 2 1
    +-(2) 1 1
      +-(3) 0 1

Total de nohs: 10
Profundidade: 3


( 1 ) Inserir
( 2 ) Apagar
( 3 ) Busca
( 4 ) Imprime
( 5 ) Carrega arquivo
( 6 ) Salva
( 0 ) Sair

Escolha umas das alternativas anteriores : 2
Excluir palavra : 5

Noh removido.

( 1 ) Inserir
( 2 ) Apagar
( 3 ) Busca
( 4 ) Imprime
( 5 ) Carrega arquivo
( 6 ) Salva
( 0 ) Sair

Escolha umas das alternativas anteriores : 4
    +-(2) 9 1
    | +-(3) 8 1
  +-(1) 7 1
+-(0) 6 1
  | +-(2) 4 1
  +-(1) 3 1
    | +-(3) 2 1
    +-(2) 1 1
      +-(3) 0 1

Total de nohs: 9
Profundidade: 3

Novamente deixo como sugestão para o teste que acesse o Projeto Gutenberg, um esforço voluntário para arquivar e distribuir obras culturais através da digitalização de livros. Nele é possível encontrar obras que estão em domínio publico em vários formatos, para teste utilize a obra de Luis Vaz de Camões, Os Lusíadas cujo arquivo texto pode ser encontrado neste LINK.

Os arquivos do projeto se encontram neste LINK compactados com 7z.