ハッシュテーブルは、キーと値のペアに高速にアクセスできるデータ構造である。基本的な考え方は、ハッシュ関数を使ってキーを配列のインデックスにマッピングし、そこに値を格納するというものだ。この方法の利点は、挿入、削除、検索を平均して一定の時間で実行できることである。

C言語でハッシュ・テーブルを実装するには、まずハッシュ関数を書く必要がある。一般的な方法としては、素数をテーブルのサイズとするモジュロ演算を使う方法がある：

```c
int hash(char* key, int table_size) {
    int hash_value = 0;
    while (*key) {
        hash_value = (hash_value * 31 + *key) % table_size;
        key++;
    }
    return hash_value;
}
```

この関数は、文字列 `key` と `table_size` パラメータ（これは素数でなければならない）を受け取り、キーの各文字を繰り返し処理し、それらのASCII値を累積することで、テーブルへのインデックスを計算する。

次に、ハッシュテーブルの各要素に対して構造体を作成する必要がある：

```c
struct entry {
    char* key;
    int value;
};
```

これで、ハッシュテーブル構造体をエントリーの配列として定義できるようになった：size`はテーブルの現在の要素数を格納し、`capacity`はテーブルの総容量を格納する：

```c
struct hashtable {
    struct entry* entries;
    int size;
    int capacity;
};
```

ハッシュテーブルに要素を挿入するには、定義した関数を使えば簡単だ：

```c
void insert(struct hashtable* ht, char* key, int value) {
  	int idx = hash(key, ht->capacity);
    
  	while(ht->entries[idx].key != NULL){
        
      	if(strcmp(ht->entries[idx].key,key)==0){
      		// Found matching key so updating.
        		ht->entries[idx].value=value;
        		return ;
      	}
  		
    		idx = (idx+1)%ht->capacity;  
        
  	}
    ht->entries[idx]=(struct entry){key,value};
  	ht->size++;
    if(ht->capacity*0.7 <= ht->size){
        ht=grow_ht(ht);
    }
}
```

この関数は、与えられたキーのハッシュ値を計算し、[リニア・プロービング](https://en.wikipedia.org/wiki/Linear_probing)であるハッシュテーブル内の空の場所を検索する。もし同じキーが見つかれば、それを更新し、そうでなければ新しいレコードを挿入する。ハッシュテーブルの容量がELEM_SIZEより小さくなった場合、ハッシュテーブルを大きくする。

では、ハッシュ・テーブルからデータを取り出す関数を作ってみよう：

```c
int* search(struct hashtable* ht, char* key) {
    int idx = hash(key, ht->capacity);
    
    while (ht->entries[idx].key != NULL) {
        if (strcmp(ht->entries[idx].key, key) == 0) {
            return &ht->entries[idx].value;
        }
        
        idx = (idx + 1) % ht->capacity;
    }
    
    return NULL;
}
```

search`関数は、`hash`関数を使用してインデックスを計算し、一致するキーを持つエントリが見つかるか、空のセルに遭遇するまでリニアプローブすることで動作する。

最後に削除関数を作成する：

```c
void delete(struct hashtable* ht, char *key){
  int idx=hash(key,ht->capcity);
  while(ht.entries[idx].key!=NULL){
      if(strcmp(key,ht.entries[idx].key)){
          // Found matching element so deleting.
          free(ht.entries[i].name); 
          memset(&ht.entries[i], '\0', sizeof(struct entry));
          return i;
      }
      idx = (idx + 1) % ht.capacity;

  }
}
```

この`delete`関数は、ハッシュ・テーブルからキーのインデックスを探し、もし見つかったら、そのエントリーのメモリ領域を解放し、NULLとしてマークする。

利点としては、ハッシュテーブルは他のデータ構造と比較して、挿入、削除、検索にかかる時間が速いことです。キーによってデータを素早く検索する必要がある場合に最適です。

しかし、不正なキーによる衝突や問題が発生したり、要素が多すぎるハッシュテーブルが混雑したりするデメリットもあります。しかし、適切なハッシュの選択と衝突の防止に努めることで、これらの問題を効果的に軽減することができる。
Hash tables are a data structure that allows fast access to key/value pairs. The basic idea is to use a hash function to map the keys to array indices, where the values can be stored. The advantage of this approach is that you can perform insertions, deletions, and searches in constant time on average.

To implement a hash table in C, you first need to write the hash function. One common strategy is to use modulo arithmetic with a prime number as the size of the table:

```c
int hash(char* key, int table_size) {
    int hash_value = 0;
    while (*key) {
        hash_value = (hash_value * 31 + *key) % table_size;
        key++;
    }
    return hash_value;
}
```

This function takes a string `key` and a `table_size` parameter (which should be a prime number), and computes an index into the table by iterating over each character in the key and accumulating their ASCII values.

Next you need to create a struct for each element of the hashtable:

```c
struct entry {
    char* key;
    int value;
};
```

Now we can define our hashtable structure as an array of entries, along with two more fields: `size`, which stores the current number of elements in the table, and `capacity`, which stores its total capacity:

```c
struct hashtable {
    struct entry* entries;
    int size;
    int capacity;
};
```

To insert an element into our hashtable is simple using our defined functions:

```c
void insert(struct hashtable* ht, char* key, int value) {
  	int idx = hash(key, ht->capacity);
    
  	while(ht->entries[idx].key != NULL){
        
      	if(strcmp(ht->entries[idx].key,key)==0){
      		// Found matching key so updating.
        		ht->entries[idx].value=value;
        		return ;
      	}
  		
    		idx = (idx+1)%ht->capacity;  
        
  	}
    ht->entries[idx]=(struct entry){key,value};
  	ht->size++;
    if(ht->capacity*0.7 <= ht->size){
        ht=grow_ht(ht);
    }
}
```

This function calculates the hash value of the given key and then search for an empty place in hashtable which is [Linear probing](https://en.wikipedia.org/wiki/Linear_probing). If it finds the same key, it updates it else inserts a new record. It grow the hashtable when capacity is lower than number of ELEM_SIZE(percentage should be your decision).

Now let's create a function to retrieve data from our hash table:

```c
int* search(struct hashtable* ht, char* key) {
    int idx = hash(key, ht->capacity);
    
    while (ht->entries[idx].key != NULL) {
        if (strcmp(ht->entries[idx].key, key) == 0) {
            return &ht->entries[idx].value;
        }
        
        idx = (idx + 1) % ht->capacity;
    }
    
    return NULL;
}
```

The `search` function works by calculating the index using the `hash` function and linear probing until either an entry with a matching key is found or an empty cell is encountered.

Lastly we can create a delete function:

```c
void delete(struct hashtable* ht, char *key){
  int idx=hash(key,ht->capcity);
  while(ht.entries[idx].key!=NULL){
      if(strcmp(key,ht.entries[idx].key)){
          // Found matching element so deleting.
          free(ht.entries[i].name); 
          memset(&ht.entries[i], '\0', sizeof(struct entry));
          return i;
      }
      idx = (idx + 1) % ht.capacity;

  }
}
```

This `delete` function works by looking for the index of the key in a hash table, if it found then we release occupied memory space of that entry and mark is as null.

In terms of advantages hash tables have fast insertion, deletion, and search times compared to other data structures. They are great for situations where you need to look up data by key quickly.

However there can be some disadvantages with collisions/issues occurring from bad keys or a busy hash-table with too many elements. But with effort on choosing right hashing and collison practice can help mitigate those issues effectively.