اذهب إلى المحتوى

السؤال

نشر

سلام عليكم.

لدي أستفسار بخصوص ال Hash Table: إذا كان لدينا قاموس ضخم (فلنأخذ قاموس اللغة الأنجليزية كمثال) فإن أنسب هيكل بيانات لتخزين القاموس هو ال Hash Table لما يوفر من سرعة ثابتة علي حساب الذاكرة. كالمثال التالي:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cs50.h>
#include <ctype.h>

// Global variables
#define ALPHABETS 26
#define MAX 35

// Struct node
typedef struct node
{
    string word;
    struct node* next;
} node;

// Functions prototypes
void EXIT(string msg);
node* find_tail(node* head);
void insert_node(node** head, string word);
void print_linked_list(node* head);
void free_linked_list(node* head);

int main(int argc, string argv[])
{
    if (argc != 2)
    {
        EXIT("Usage: ./dictionary [N of words]\n");
    }

    short n_of_words = atoi(argv[1]);
    if (!strcmp(argv[1], "0"))
    {
        EXIT("No words added\n");
    }
    else if (!n_of_words)
    {
        EXIT("Invalid input\n");
    }
    else if (n_of_words > MAX)
    {
        printf("%i ", MAX);
        EXIT("is the max number\n");
    }
    // Hash table starts here
    node* dictionary[ALPHABETS] = {NULL};

    // Take vocabularies & insert them
    for (int i = 0; i < n_of_words; i++)
    {
        string vocabulary = get_string("Word: ");
        short hash = toupper(vocabulary[0]) - 'A';

        insert_node(&dictionary[hash], vocabulary);
    }

    // Print & Free dictionary
    for (int i = 0; i < ALPHABETS; i++)
    {
        printf("%c: ", i + 'A');
        if (dictionary[i])
        {
            // print current bucket
            print_linked_list(dictionary[i]);
            printf("\n");
            // free current bucket
            free_linked_list(dictionary[i]);
        }
        else
        {
            printf("\n");
        }
    }

}

// func1: Exit program function
void EXIT(string msg)
{
    printf("%s", msg);
    exit(0);
}

// func2: To find the tail of a linked list
node* find_tail(node* head)
{
    if (!head)
    {
        return NULL;
    }

    node* temp = head;
    while (temp->next)
    {
        temp = temp->next;
    }
    return temp;
}

// func3: To insert a node to a linked list
void insert_node(node** head, string vocabulary)
{
    node* n = malloc(sizeof(node));
    if (!n)
    {
        return;
    }
    n->word = vocabulary;
    n->next = NULL;

    if (!*head)
    {
        *head = n;
        return;
    }

    find_tail(*head)->next = n;
}

// func4: To print linked list items
void print_linked_list(node* head)
{
    for (node* temp = head; temp; temp = temp->next)
    {
        printf("%s ", temp->word);
    }
}

// func4: To free linked list
void free_linked_list(node* head)
{
    while (head)
    {
        node* temp = head->next;
        free(head);
        head = temp;
    }
}

أعرف أنها طريقة سيئة لتخزين الكلمات؛ إذ أردنا البحث عن كلمة ما في القاموس, فنعم سنختصر الكثير من الوقت لأننا نعلم في أي Bucket سنبحث, لكن ما زال البحث بطئ (أو حتي بنفس البطئ) لأن البرنامج سيضطر للمرور علي عناصر ال Linked List واحدة تلو الأخري. لذلك قاموا بتوسيع الجدول كي تتقسم العناصر أكثر و أكثر (كل هذا علي حساب الذاكرة).

هل يمكن لأحدكم أن يشرح لي هذا التقسيم (في مثال القاموس تحديدا). كما أن هناك عملية تتم علي الكلمات كي نجد ال Bucket بنفس الوقت, ما هي هذه العملية.

Recommended Posts

  • 0
نشر

المشكلة ليست في فكرة الـ Hash Table نفسها، بل في دالة التجزئة التي استخدمتها وحجم الجدول الصغير، فالدالة ليست جيدة:

short hash = toupper(vocabulary[0]) - 'A';

لأنها تعتمد فقط على الحرف الأول من الكلمة، بالتالي كل الكلمات التي تبدأ بنفس الحرف مثل Apple, Ant, Art, Around وستذهب إلى نفس الـ Bucket أي نفس الخانة في المصفوفة dictionary، وفي اللغة الإنجليزية، هناك حروف تبدأ بها كلمات كثيرة جداً مثل S, T, A وحروف أخرى نادرة كـ X, Z, Q، لذا سيحدث تكتل أي Clustering فبعض الـ Buckets ستحتوي على Linked Lists طويلة جدًا.

وعند البحث عن كلمة تبدأ بحرف شائع، ستضطر للمرور على قائمة طويلة، ويصبح أداء البحث يقترب من O(n) في أسوأ الحالات، ويلغي الفائدة من استخدام الـ Hash Table.

أيضًا حجم الجدول صغير، فلديك 26 Bucket فقط، وهو عدد قليل جدًا بالنسبة لقاموس ضخم يحتوي على آلاف الكلمات.

وبالنسبة للتقسيم فالأفضل للكلمات يعتمد بشكل مباشر على العملية التي تتم على الكلمات أي دالة التجزئة، والجيد منها يعمل على توزيع الكلمات بشكل عشوائي ومتساوٍا قدر الإمكان على جميع الـ Buckets المتاحة، ولفعل ذلك يجب أن تضع في الاعتبار الكلمة بأكملها، وليس فقط الحرف الأول.

بيحث نفس الكلمة يجب أن تعطي نفس قيمة الـ Hash في كل مرة، وتعتمد على كل حروف الكلمة وأي تغيير بسيط في الكلمة مثل cat و car يؤدي إلى قيمة Hash مختلفة.

وأن توزع الكلمات بشكل متساوٍا على الـ Buckets المتاحة لتجنب التكتل.

مثال بسيط للتوضيح مع الوضع في الإعتبار ما سبق، وهي دالة تستقبل مجموع قيم ASCII لكل الحروف في الكلمة.

unsigned int hash(const char* word, unsigned int N)
{
    unsigned int sum = 0;
    for (int i = 0; word[i] != '\0'; i++)
    {
        sum += word[i];
    }
    return sum % N; 
}

لكن ما زالت ليس جيدة كفاية، فالكلمتان cat و act لهما نفس الحروف وبالتالي ستحصلان على نفس قيمة الـ Hash وذاك يسمى تصادم - Collision.

وذلك ما تم تحسينه في الدالة التالية:

unsigned int hash_djb2(const char *word, unsigned int N)
{
    unsigned long hash_value = 5381; 
    int c;

    while ((c = *word++)) 
    {
        hash_value = ((hash_value << 5) + hash_value) + c;
    }

    return hash_value % N; 
}

توفر قيم مختلفة لكلمتي cat و act، وستوزع الكلمات بشكل ممتاز.

بتاريخ 3 ساعة قال Abdelrehman Elsied:

هل يمكن لأحدكم أن يشرح لي هذا التقسيم (في مثال القاموس تحديدا). كما أن هناك عملية تتم علي الكلمات كي نجد ال Bucket بنفس الوقت, ما هي هذه العملية.

بعد الدالة السابقة لم يعد من المنطقي استخدام جدول بحجم 26 فقط، فلو القاموس يحتوي على 140,000 كلمة، فالأفضل استخدام جدول بحجم كبير لتقليل احتمالية التصادمات وجعل القوائم المتصلة قصيرة جدًا.

أي بدلاً من تعريف الجدول هكذا:

#define ALPHABETS 26
node* dictionary[ALPHABETS] = {NULL};

نقوم بتعريفه بحجم أكبر بكثير، وكقاعدة عامة حجم الجدول يكون عدد أولي قريب من عدد العناصر التي تتوقع تخزينها أو أكبر، لأنّ استخدام عدد أولي يساعد في تحسين التوزيع عند استخدام عملية باقي القسمة %.

#define N_BUCKETS 10007 

node* dictionary[N_BUCKETS] = {NULL};

وبالتالي بدلاً من حساب الـ Hash بناءًا على الحرف الأول، نستخدم الدالة الجديدة:

for (int i = 0; i < n_of_words; i++)
{
    string vocabulary = get_string("Word: ");

    // بدلاً من
    // short hash = toupper(vocabulary[0]) - 'A';

    unsigned int hash_index = hash_djb2(vocabulary, N_BUCKETS);

    insert_node(&dictionary[hash_index], vocabulary);
}

 

انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أجب على هذا السؤال...

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.

  • إعلانات

  • تابعنا على



×
×
  • أضف...