সরাসরি পর্বে যান
পর্ব ৩৭ · ৫৬-এর মধ্যে · মডিউল ৫

হ্যাশ টেবিল — চাবি থেকে সরাসরি ঠিকানায় পৌঁছানো

Hash Tables — Hash Functions, Collisions, Chaining, Probing & Resizing

Taught in Bangla, this lesson builds hash tables in C: hash functions such as djb2, unavoidable collisions, separate chaining and open addressing, load factor and resizing, average versus worst-case cost, and hash-flooding attacks.

এই পর্বে যা শিখবেন

  • Hash function কীভাবে একটি নাম বা সংখ্যাকে array-র ঘরের নম্বরে বদলায় — হাতে হিসাব করে
  • Collision কেন অবধারিত, আর chaining ও linear probing কীভাবে তা সামলায়
  • C-তে নিজের একটি ছোট hash table — insert, lookup আর leak ছাড়া free করা
  • Load factor, resizing আর “গড়ে O(1)” কথাটির আসল অর্থ
  • Hash flooding আক্রমণ, আর AI-এর লেখা hash table যাচাইয়ের চেকলিস্ট

১ · স্বপ্নটা কী — খোঁজাখুঁজি ছাড়াই এক লাফে পৌঁছানো

একটি বিশ্ববিদ্যালয়ের হলে এক হাজার ছাত্র থাকেন। অফিসে একটি খাতায় সবার নাম আর রুম নম্বর লেখা। একজন অভিভাবক এসে বললেন, “রোল ২৩৪১-এর ছেলেটি কোন রুমে?” কেরানি খাতার প্রথম পাতা থেকে আঙুল চালিয়ে খুঁজতে শুরু করলেন। ভাগ্য খারাপ হলে এক হাজার নাম পড়তে হবে।

এবার অন্য একটি হলের কথা ভাবুন। সেখানে নিয়ম হলো — রোল নম্বরের শেষ তিন অঙ্কই রুম নম্বর। রোল ২৩৪১ মানে রুম ৩৪১. কেরানিকে খাতা খুলতেই হয় না। একটি ছোট হিসাব, তারপর সোজা হেঁটে সেই রুমে। একজন ছাত্র খুঁজতে যত সময়, এক লাখ ছাত্র থাকলেও প্রায় তত সময়।

ডাকঘরের চিঠি বাছাইও একই রকম। পোস্টমাস্টার প্রতিটি চিঠি পড়ে পুরো শহরের তালিকা মেলান না। তিনি শুধু পোস্টকোড দেখেন — ১২০৫ হলে ধানমন্ডির খোপে, ৪০০০ হলে চট্টগ্রামের খোপে। চিঠির ওপর লেখা তথ্য থেকেই খোপের নম্বর হিসাব হয়ে যায়। এই পর্বের পুরো ধারণাটি এটুকুই: ডেটার নিজের ভেতর থেকে তার ঠিকানা বের করা।

এ পর্যন্ত কোর্সে আমরা খোঁজার তিনটি উপায় দেখেছি। প্রতিটির দাম আলাদা:

কাঠামোখোঁজার পদ্ধতি১০ লাখ জিনিসে মোটামুটি কত ধাপ
সাজানো নয় এমন array বা linked listএকটা একটা করে দেখা (linear search)সবচেয়ে খারাপ ক্ষেত্রে ১০ লাখ
সাজানো array বা ভারসাম্যপূর্ণ BST (পর্ব ৩৫)প্রতিবার অর্ধেক বাদ (binary search)প্রায় ২০
Array, যদি index জানা থাকেসরাসরি a[i] (পর্ব ১৬)১
Hash tableচাবি থেকে index হিসাব করে সরাসরি যাওয়াগড়ে প্রায় ১–২

খেয়াল করুন, array-তে a[341] এক ধাপের কাজ, কারণ CPU base + 341 × size হিসাব করে সরাসরি সেই ঠিকানায় যায় (পর্ব ১৬)। সমস্যা হলো, বাস্তবের চাবি প্রায় কখনো সুন্দর ছোট সংখ্যা নয়। চাবি হতে পারে একটি নাম (“Mitu”), একটি মোবাইল নম্বর (01711-xxxxxx), একটি ওয়েবসাইটের ঠিকানা। a["Mitu"] লেখা যায় না।

মূল ধারণা

Hash table হলো একটি সাধারণ array, সাথে একটি হিসাবের নিয়ম। নিয়মটি যেকোনো চাবিকে (key) array-র একটি index-এ বদলে দেয়। প্রতিটি ঘরে রাখা হয় একটি key-value জোড়া — যেমন চাবি “Mitu”, মান 91 নম্বর। খুঁজতে চাইলে একই নিয়মে আবার index হিসাব করুন, সোজা সেই ঘরে যান।

চাবি থেকে ঠিকানা হিসাবের এই কৌশলের শুরু কম্পিউটারের একেবারে প্রথম যুগে। সাধারণত বলা হয়, ১৯৫৩ সালে IBM-এর Hans Peter Luhn একটি অভ্যন্তরীণ নোটে প্রথম এই ধারণা লেখেন — একই ঘরে একাধিক জিনিস পড়লে শিকল বানিয়ে রাখার কথাও সেখানে ছিল। পরে Donald Knuth linear probing-এর গাণিতিক বিশ্লেষণ করেন, যা অ্যালগরিদম বিশ্লেষণের ইতিহাসে একটি বিখ্যাত কাজ। আজ প্রায় প্রতিটি প্রোগ্রামিং ভাষার ভেতরে কোনো না কোনো hash table কাজ করছে।

২ · Hash function — চাবিকে ঘরের নম্বরে বদলানো

যে নিয়মটি চাবিকে সংখ্যায় বদলায়, তার নাম hash function. হলের উদাহরণে নিয়মটি ছিল “শেষ তিন অঙ্ক”। কিন্তু চাবি যদি লেখা হয়, যেমন “Dhaka”? পর্ব ১৯-এ দেখেছি, C-তে একটি string আসলে কয়েকটি byte-এর সারি — প্রতিটি অক্ষর একটি ছোট সংখ্যা (ASCII মান)। 'D' মানে 68, 'h' মানে 104. তাহলে অক্ষরগুলোর সংখ্যা মিলিয়ে একটি বড় সংখ্যা বানানো যায়।

সবচেয়ে বিখ্যাত সহজ string hash-গুলোর একটি হলো djb2. এটি তৈরি করেন গণিতবিদ ও ক্রিপ্টোগ্রাফার Daniel J. Bernstein; বহু বছর আগে তিনি এটি comp.lang.c নামের অনলাইন আলোচনা-গ্রুপে প্রকাশ করেছিলেন। নিয়মটি মাত্র দুই লাইনের: শুরুতে h = 5381, তারপর প্রতিটি অক্ষর c-এর জন্য h = h * 33 + c. শেষে মোট ঘরের সংখ্যা দিয়ে ভাগশেষ নিলে পাওয়া যায় ঘরের নম্বর। নিচে “Dhaka” চাবিটি ৮টি ঘরের একটি টেবিলে কোথায় যাবে, ধাপে ধাপে হিসাব করে দেখুন:

1size_t hash(const char *s) {2    unsigned long h = 5381;3    while (*s) {4        h = h * 33 + (unsigned char)*s;5        s++;6    }7    return h % 8;     /* 8 buckets */8}
হিসাবের অবস্থা
অক্ষর
ASCII
হিসাব
h
ঘর (bucket)
  1. শুরুর মান। h শুরু হয় 5381 দিয়ে — Bernstein-এর বেছে নেওয়া একটি সংখ্যা, পরীক্ষা করে যেটি ভালো ফল দিয়েছিল। এর পেছনে কোনো গভীর রহস্য নেই; শূন্য না হলেই শুরুটা কাজে লাগে।
  2. প্রথম অক্ষর 'D' = 68. আগের মানকে 33 দিয়ে গুণ, তারপর অক্ষরের মান যোগ: 5381 × 33 = 177573, তার সাথে 68 যোগ করে 177641.
  3. 'h' = 104. 177641 × 33 = 5862153, যোগ 104 = 5862257. খেয়াল করুন — প্রতিটি অক্ষর পুরো ফলাফলকে নাড়িয়ে দিচ্ছে, শুধু শেষ অঙ্ক নয়।
  4. 'a' = 97. 5862257 × 33 + 97 = 193454578. গুণ করার কারণে অক্ষরের অবস্থান গুরুত্বপূর্ণ হয়ে যায় — “ab” আর “ba” আলাদা ফল দেবে। শুধু যোগ করলে দুটো একই হতো।
  5. 'k' = 107. 193454578 × 33 + 107 = 6384001181. সংখ্যাটি দ্রুত বড় হচ্ছে। এখানে ধরে নিচ্ছি unsigned long ৬৪-বিট (যেমন ৬৪-বিট Linux-এ); Windows-এ এটি ৩২-বিট, তাই সেখানে এই ধাপেই সীমা ছাড়ায়, তবে শেষে % 8-এর ফল একই থাকে। লম্বা string-এ ৬৪-বিটের সীমাও ছাড়িয়ে যাবে — তখন মান ঘুরে আসে (wraparound, পর্ব ০৫)। unsigned টাইপে এটি নিয়মসম্মত ও নির্দিষ্ট, তাই সমস্যা নেই।
  6. শেষ অক্ষর 'a' = 97. 6384001181 × 33 + 97 = 210672039070. এটিই “Dhaka”-র hash মান। একই চাবি দিলে প্রতিবার ঠিক এই সংখ্যাটিই আসবে।
  7. ঘরের নম্বর। টেবিলে ঘর মাত্র ৮টি, তাই ভাগশেষ নিই: 210672039070 % 8 = 6 (পর্ব ০৭-এর % অপারেটর)। “Dhaka” যাবে ৬ নম্বর ঘরে। ভাগশেষ সবসময় 0 থেকে 7-এর মধ্যে থাকে — অর্থাৎ array-র বাইরে যাওয়ার কোনো সুযোগ নেই।

কোডে (unsigned char) কেন? কিছু compiler-এ সাধারণ char signed, তাই ASCII-র বাইরের অক্ষর (যেমন UTF-8-এ লেখা বাংলা) ঋণাত্মক সংখ্যা হয়ে আসতে পারে। Cast করলে প্রতিটি byte 0–255-এর মধ্যে থাকে, আর সব কম্পিউটারে একই ফল আসে। আর 33 দিয়ে গুণ CPU-র জন্য খুব সস্তা: h * 33 মানে (h << 5) + h — একটি shift আর একটি যোগ।

ভালো hash function-এর তিনটি গুণ

🔁 নির্দিষ্ট (deterministic)

একই চাবি প্রতিবার একই সংখ্যা দেবে। আজ “Dhaka” ৬ নম্বরে রেখে কাল ৩ নম্বরে খুঁজলে আর কখনো পাবেন না। তাই hash-এ এলোমেলো কিছু বা সময় ব্যবহার করা চলে না।

শর্ত, ঐচ্ছিক নয়

🌾 সমানভাবে ছড়ায়

চাবিগুলো সব ঘরে প্রায় সমান ভাগে পড়বে। কাছাকাছি চাবি (“user1”, “user2”) যেন দূরে দূরে যায়। সব এক কোণে জমলে টেবিলটা আবার একটা লম্বা তালিকা হয়ে যায়।

গতির আসল উৎস

⚡ দ্রুত

হিসাবটাই যদি এক হাজার ধাপ নেয়, তাহলে এক ধাপে খোঁজার লাভ শেষ। djb2 প্রতিটি অক্ষরে একটি গুণ আর একটি যোগ করে — এর চেয়ে সস্তা হওয়া কঠিন।

প্রতি byte-এ কয়েকটি নির্দেশ

একটি গুরুত্বপূর্ণ কথা: এখানে “ভালো” মানে টেবিলের জন্য ভালো। পাসওয়ার্ড বা ডিজিটাল স্বাক্ষরের জন্য যে hash লাগে (যেমন SHA-256), তার লক্ষ্য একেবারে আলাদা — সেটি ধীর হলেও চলে, কিন্তু উল্টো দিক থেকে ভাঙা অসম্ভব হতে হবে। ভাবনার প্রশ্ন ৫-এ এ নিয়ে বিস্তারিত আছে।

৩ · Collision — যা কখনো এড়ানো যায় না

এখন একটি অস্বস্তিকর সত্য। Hash function যত ভালোই হোক, দুটি আলাদা চাবি একই ঘরে পড়বেই। একে বলে collision. কারণটি সহজ গণিত — pigeonhole principle: ১০টি কবুতর আর ৯টি খোপ থাকলে অন্তত একটি খোপে দুটি কবুতর ঢুকবেই। সম্ভাব্য নাম অসংখ্য, আর আমাদের টেবিলে ঘর মাত্র কয়েকটি।

আমাদের ৮ ঘরের টেবিলেই দেখুন: djb2 দিয়ে “Rafi” আর “Joy” দুটোই যায় ৭ নম্বর ঘরে। “Dhaka”, “Sylhet” আর “Comilla” — তিনটিই ৬ নম্বরে। এটি hash-এর দোষ নয়; ৮টি ঘরে বেশি চাবি রাখলে এমন হবেই।

কত তাড়াতাড়ি collision শুরু হয় — জন্মদিনের ধাঁধা

একটি ক্লাসে কতজন থাকলে দুজনের জন্মদিন একই দিনে হওয়ার সম্ভাবনা ৫০%? অনেকে আন্দাজ করেন ১৮০ জনের মতো (৩৬৫-এর অর্ধেক)। আসল উত্তর মাত্র ২৩ জন। একে বলে birthday paradox. ধাঁধাটি আসলে ভুল আন্দাজের: আমরা ভাবি “আমার সাথে কারও মিলবে কি না”, অথচ প্রশ্ন হলো “যেকোনো দুজনের মিলবে কি না” — আর ২৩ জনে জোড়া হয় ২৫৩টি।

ঘর (বা দিন)কতগুলো চাবি রাখলে collision-এর সম্ভাবনা প্রায় ৫০%
৩৬৫ (বছরের দিন)২৩ জন মানুষ
১,০০০ ঘরপ্রায় ৩৮টি চাবি
১০ লাখ ঘরপ্রায় ১,১৮০টি চাবি

অর্থাৎ ১০ লাখ ঘরের টেবিল ১% ভরার অনেক আগেই প্রথম collision হয়ে যায়। ৫০ জনের ক্লাসে জন্মদিন মেলার সম্ভাবনা প্রায় ৯৭%, আর ৭০ জনে প্রায় ৯৯.৯%. তাই hash table নকশার প্রশ্ন কখনোই “collision কীভাবে এড়াব” নয় — প্রশ্ন হলো “collision হলে কী করব”।

🔗 উপায় ১ — Separate chaining

প্রতিটি ঘরে একটি linked list থাকে। একই ঘরে যত চাবি পড়ুক, সবাই সেই তালিকায় লাইন ধরে দাঁড়ায়। হলের এক রুমে বাড়তি খাট বসানোর মতো।

C++ unordered_map, Java HashMap

🚶 উপায় ২ — Open addressing

সবাই একই array-তে থাকে, একটি ঘরে একজন। ঘর ভরা থাকলে নিয়ম মেনে পরের ঘর দেখুন, তারপর তার পরেরটা — খালি পেলেই বসুন। ভরা বাসে পরের খালি সিট খোঁজার মতো।

Python dict

৪ · Separate chaining — প্রতিটি ঘরে একটি শিকল

পর্ব ৩৩-এ আমরা linked list বানিয়েছিলাম — প্রতিটি node নিজের ডেটা আর পরের node-এর ঠিকানা রাখে। Separate chaining-এ hash table হলো আসলে কয়েকটি linked list-এর মাথার একটি array. প্রতিটি ঘরকে বলে bucket. শুরুতে সব bucket-এ NULL — অর্থাৎ খালি শিকল।

নিচে ৮টি bucket-এর একটি টেবিলে ছাত্রদের নাম আর নম্বর রাখা হচ্ছে। চতুর্থ নামটিতে collision হবে। তারপর দুটি খোঁজা — একটি পাওয়া যাবে, একটি যাবে না:

1put("Rafi", 78);    /* hash -> 7 */2put("Mitu", 91);    /* hash -> 4 */3put("Tanvir", 65);  /* hash -> 1 */4put("Joy", 83);     /* hash -> 7 ! */5get("Rafi");6get("Sakib");       /* hash -> 7 */
৮টি bucket — প্রতিটি একটি শিকলের মাথা
[0]
[1]
[2] [3]
[4]
[5] [6]
[7]
তুলনা
  1. খালি টেবিল। ৮টি pointer-এর একটি array, সবগুলো NULL. এখনো heap-এ কোনো node নেই। Global array হলে C নিজেই সব শূন্য করে দেয়; heap-এ নিলে calloc দিয়ে নিন।
  2. Rafi রাখা। hash("Rafi") % 8 = 7. malloc দিয়ে একটি node বানানো হলো, ভেতরে চাবি “Rafi” আর মান 78, আর সেটি ৭ নম্বর শিকলের মাথায় বসল।
  3. Mitu রাখা। হিসাব দিল 4. ৪ নম্বর ঘর খালি ছিল, তাই নতুন শিকল শুরু হলো। কোথাও কিছু খুঁজতে হয়নি — হিসাব করে সোজা চলে এসেছি।
  4. Tanvir রাখা। হিসাব দিল 1. এখনো প্রতিটি নাম আলাদা ঘরে।
  5. Joy — collision! hash("Joy") % 8 = 7, যেখানে Rafi আগে থেকেই আছে। কোনো সমস্যা নেই: নতুন node শিকলের সামনে জুড়ে দেওয়া হলো (new->next = table[7]; table[7] = new;). সামনে জোড়া মানে শিকল যত লম্বাই হোক, কাজটি এক ধাপের।
  6. get("Rafi") — ঘর বের করা। খোঁজার শুরুও একই হিসাব দিয়ে: hash("Rafi") % 8 = 7. বাকি ৭টি ঘরের দিকে তাকানোরই দরকার নেই।
  7. শিকলের প্রথম node. ৭ নম্বরের প্রথম node-এ চাবি “Joy”. strcmp দিয়ে তুলনা — মেলেনি। খেয়াল করুন: একই ঘরে আছে বলেই একই চাবি নয়, তাই প্রতিটি node-এর আসল চাবি মিলিয়ে দেখতেই হবে।
  8. পরের node — পাওয়া গেছে। next ধরে এগিয়ে “Rafi” পাওয়া গেল, মান 78. মোট দুটি তুলনা। শিকল ছোট থাকলে খোঁজা প্রায় এক ধাপের কাজ।
  9. get("Sakib") — যা নেই। hash দিল 7. Joy নয়, Rafi নয়, তারপর NULL — শিকল শেষ। অর্থাৎ Sakib টেবিলে নেই। খুঁজে না পাওয়াটাও শুধু একটি শিকলের কাজ, পুরো টেবিলের নয়।

এবার পুরো জিনিসটি আসল C-তে। এটি একটি সম্পূর্ণ, ছোট hash table: নাম থেকে নম্বর। put রাখে, get খোঁজে, আর শেষে প্রতিটি শিকলের প্রতিটি node free করা হয়:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NB 8                                   /* number of buckets */
typedef struct Node { const char *key; int marks; struct Node *next; } Node;
Node *table[NB];                               /* 8 empty chains (all NULL) */
size_t hash(const char *s) {                   /* djb2 -> bucket index */
    unsigned long h = 5381;
    while (*s) h = h * 33 + (unsigned char)*s++;
    return h % NB;
}
void put(const char *k, int m) {               /* new node joins the chain front */
    Node *n = malloc(sizeof *n);  if (n == NULL) { perror("malloc"); exit(1); }
    size_t b = hash(k);  *n = (Node){ k, m, table[b] };  table[b] = n;
}
int get(const char *k) {                       /* -1 means not found */
    for (Node *n = table[hash(k)]; n != NULL; n = n->next)
        if (strcmp(n->key, k) == 0) return n->marks;
    return -1;
}
int main(void) {
    put("Rafi", 78); put("Mitu", 91); put("Joy", 83);   /* Rafi, Joy: bucket 7 */
    printf("Joy -> %d, Rafi -> %d, Sakib -> %d\n", get("Joy"), get("Rafi"), get("Sakib"));
    for (int i = 0; i < NB; i++)                /* free every node of every chain */
        while (table[i]) { Node *n = table[i]; table[i] = n->next; free(n); }
    return 0;
}

table একটি global array, তাই শুরুতে ৮টি pointer-ই NULL. put একটি node বানিয়ে compound literal দিয়ে তিনটি ঘর একসাথে ভরে — চাবি, মান, আর পুরোনো শিকলের মাথা — তারপর নিজেকেই নতুন মাথা বানায়। “Rafi” আর “Joy” একই bucket-এ পড়ে, তবুও দুটিই ঠিক মান ফেরত দেয়, কারণ get শিকলের প্রতিটি node-এর চাবি strcmp দিয়ে মেলায়। আউটপুট: Joy -> 83, Rafi -> 78, Sakib -> -1. শেষের লুপটি প্রতিটি শিকল মাথা থেকে খুলে খুলে free করে; এটি বাদ দিলে Valgrind তিনটি memory leak দেখাবে। একটি সীমাবদ্ধতা লক্ষ করুন: আমরা চাবির pointer রেখেছি, লেখার কপি নয় — তাই চাবিগুলো এখানে string literal. বাস্তবে ব্যবহারকারীর দেওয়া চাবি হলে malloc করে কপি রাখতে হবে, আর free-ও করতে হবে। আরেকটি বিষয়: একই চাবি দুবার put করলে এই কোড দুটি node বানায়; আসল টেবিলে আগে খুঁজে দেখে মান বদলে দিতে হয়।

String চাবি কখনো == দিয়ে তুলনা করবেন না। n->key == k দুটি ঠিকানা তুলনা করে, লেখা নয় (পর্ব ১৯)। ছোট পরীক্ষায় এটি কাজ করছে বলে মনে হতে পারে, কারণ compiler একই string literal প্রায়ই একই ঠিকানায় রাখে। কিন্তু চাবি যখন ব্যবহারকারীর ইনপুট বা ফাইল থেকে আসবে, তখন “Rafi” আর “Rafi” আলাদা ঠিকানায় থাকবে — আর টেবিল বলবে “পাওয়া যায়নি”। এই বাগটি AI-এর লেখা কোডে প্রায়ই দেখা যায়।

৫ · Open addressing — পাশের খালি ঘর খোঁজা

Chaining-এ প্রতিটি নতুন চাবির জন্য একটি malloc, আর প্রতিটি node মেমরির কোথায় পড়বে ঠিক নেই। Open addressing অন্য পথে যায়: সব চাবি একটিই array-তে, প্রতি ঘরে একটি করে। ঘর ভরা থাকলে একটি নির্দিষ্ট নিয়মে পরের ঘর দেখা হয়। সবচেয়ে সহজ নিয়মটি হলো linear probing — ঠিক পরের ঘর, তারপর তার পরেরটা, শেষে পৌঁছালে আবার শুরুতে ((i + 1) % 8).

নিচে ৮ ঘরের টেবিলে কয়েকটি জেলার নাম রাখা হচ্ছে। djb2 দিয়ে Dhaka, Sylhet আর Comilla তিনটিই ৬ নম্বরে পড়ে। শেষ ধাপে একটি লুকানো ফাঁদ আছে — খেয়াল রাখুন:

1put("Dhaka");     /* hash -> 6 */2put("Sylhet");    /* hash -> 6 */3put("Comilla");   /* hash -> 6 */4put("Khulna");    /* hash -> 0 */5get("Khulna");6get("Rajshahi");  /* hash -> 7 */7slot[7] = EMPTY;  /* delete Sylhet */8get("Comilla");
৮টি ঘরের একটি array
[0]
[1]
[2]–[5]
[6]
[7]
যে ঘর দেখা হলো
  1. খালি array. ৮টি ঘর, প্রতিটিতে সরাসরি একটি চাবি রাখা যাবে — কোনো pointer, কোনো আলাদা node নেই। — মানে ঘরটি কখনো ব্যবহার হয়নি।
  2. Dhaka → 6. ঘর খালি, সোজা বসে গেল। একবারই দেখতে হলো।
  3. Sylhet → 6, কিন্তু ভরা। নিয়ম অনুযায়ী পরের ঘর ৭ দেখা হলো — খালি, সেখানেই বসল। এখন Sylhet তার “নিজের” ঘরে নেই, এক ঘর সরে আছে।
  4. Comilla → 6, তারপর 7 — দুটোই ভরা। ৭-এর পরে array শেষ, তাই ঘুরে ০ নম্বরে ((7 + 1) % 8 = 0). খালি পেয়ে বসল। তিনবার দেখতে হলো।
  5. Khulna → 0 — অন্যের জায়গা দখল! Khulna-র নিজের hash 0, আর তার সাথে কারও collision হওয়ার কথাই ছিল না। কিন্তু Comilla সরে এসে ০ দখল করে আছে, তাই Khulna-কে ১-এ যেতে হলো। ভরা ঘরগুলো এভাবে একটানা জোট বাঁধে (৬, ৭, ০, ১) — একে বলে primary clustering. জোট যত বড়, নতুন চাবি তাতে ধাক্কা খাওয়ার সম্ভাবনাও তত বেশি, আর জোট আরও বড় হয়।
  6. get("Khulna"). রাখার সময়ের পথটিই আবার হাঁটতে হয়: ০-এ Comilla (মেলেনি), ১-এ Khulna — পাওয়া গেছে। দুবার দেখতে হলো।
  7. get("Rajshahi") — যা নেই। hash 7. ৭-এ Sylhet, ০-এ Comilla, ১-এ Khulna — কেউ মেলেনি। ২ নম্বর ঘর খালি, অর্থাৎ Rajshahi থাকলে এর আগেই পাওয়া যেত। চারবার দেখে তবে নিশ্চিত হওয়া গেল। Linear probing-এ “না পাওয়া” প্রায়ই সবচেয়ে ব্যয়বহুল কাজ।
  8. ফাঁদ — মুছে ফেলার সমস্যা। Sylhet মুছতে গিয়ে ৭ নম্বর ঘর সাধারণভাবে খালি করে দিলাম। এখন Comilla খুঁজুন: ৬-এ Dhaka, ৭ খালি — খোঁজা থেমে গেল! অথচ Comilla ০-তে দিব্যি বসে আছে। সমাধান: মুছলে ঘরটিকে “খালি” না করে একটি বিশেষ চিহ্ন দিন — tombstone (“এখানে কেউ ছিল”)। খোঁজার সময় tombstone পেরিয়ে এগোতে হয়, আর নতুন চাবি রাখার সময় সেই ঘর আবার ব্যবহার করা যায়।

তাহলে দুটির মধ্যে কোনটি ভালো? উত্তরটি নির্ভর করে মেমরির আচরণের ওপর। আগের পর্বগুলোতে (পর্ব ১৬ ও ২২) দেখেছি, CPU মেমরি থেকে একবারে একটি পুরো cache line আনে, তাই পাশাপাশি রাখা ডেটা খুব দ্রুত পড়া যায় (spatial locality). Linear probing-এর “পরের ঘর” প্রায়ই একই cache line-এ থাকে। Chaining-এর পরের node হয়তো heap-এর অন্য প্রান্তে।

বিষয়Separate chainingLinear probing
Collision হলেএকই ঘরের শিকলে যোগপরের খালি ঘরে বসা
মেমরিপ্রতি চাবিতে একটি node + একটি বাড়তি pointerশুধু একটি array, বাড়তি pointer নেই
Cache-এর সাথে সম্পর্কদুর্বল — node-গুলো ছড়িয়ে থাকেখুব ভালো — পরের ঘর পাশেই
টেবিল প্রায় ভরে গেলেধীরে ধীরে ধীর হয়, ভেঙে পড়ে নাদ্রুত খুব ধীর হয়ে যায় (cluster)
মুছে ফেলাসহজ — linked list থেকে node সরানোসাবধানে — tombstone লাগে
ঘরের চেয়ে বেশি চাবিরাখা যায় (শিকল লম্বা হবে)অসম্ভব — আগেই বড় করতে হবে

৬ · Load factor ও resizing — টেবিল ভরে এলে কী করবেন

টেবিল কতটা ভরা, তা মাপার সংখ্যাটির নাম load factor: মোট চাবি ÷ মোট ঘর। ৮ ঘরে ৬টি চাবি মানে load factor 0.75. Chaining-এ এটিই গড় শিকলের দৈর্ঘ্য। ১০ লাখ চাবি যদি ৮টি ঘরেই থাকে, প্রতিটি শিকল গড়ে সোয়া লাখ লম্বা — তখন “hash table” আসলে ৮টি লম্বা linked list.

Linear probing-এ load factor আরও নাটকীয় প্রভাব ফেলে। Donald Knuth-এর বিশ্লেষণ থেকে পাওয়া গড় হিসাব দেখুন (চাবি টেবিলে আছে বা নেই — দুই ক্ষেত্রে গড়ে কতগুলো ঘর দেখতে হয়):

Load factorযে চাবি আছে, তা পেতে গড়েযে চাবি নেই, তা নিশ্চিত হতে গড়ে
০.৫ (অর্ধেক ভরা)প্রায় ১.৫ ঘরপ্রায় ২.৫ ঘর
০.৭৫প্রায় ২.৫ ঘরপ্রায় ৮.৫ ঘর
০.৯প্রায় ৫.৫ ঘরপ্রায় ৫০ ঘর

অর্থাৎ ৯০% ভরা টেবিলে “নেই” বলতে গড়ে ৫০টি ঘর দেখতে হয়। তাই প্রতিটি বাস্তব hash table একটি সীমা ঠিক করে রাখে — যেমন Java-র HashMap-এর ডিফল্ট সীমা 0.75, আর CPython-এর dict প্রায় দুই-তৃতীয়াংশ ভরলেই বড় হয়। সীমা পেরোলে শুরু হয় rehashing: দ্বিগুণ বড় নতুন array নেওয়া, আর প্রতিটি চাবিকে নতুন ঘরের সংখ্যা দিয়ে আবার % করে নতুন জায়গায় বসানো।

পুরোনো টেবিল (৪ ঘর) ও নতুন টেবিল (৮ ঘর)
পুরোনো [0]
পুরোনো [1]
পুরোনো [2]
পুরোনো [3]
নতুন [0]
নতুন [1]
নতুন [2]
নতুন [3]
নতুন [4]
নতুন [5]
নতুন [6]
নতুন [7]
Load factor
  1. ভিড় জমেছে। ৪টি ঘরে ৮টি নাম — load factor 2.0, প্রতিটি শিকলে দুজন। খোঁজার সময় গড়ে বাড়তি তুলনা লাগছে। ধরুন আমাদের নিয়ম “load factor 1.0 পেরোলেই বড় করো”।
  2. নতুন, দ্বিগুণ বড় array. calloc(8, sizeof(Node *)) দিয়ে ৮টি pointer-এর একটি নতুন array নেওয়া হলো — সব NULL. এখানে realloc যথেষ্ট নয়: শুধু জায়গা বাড়ালে চাবিগুলো পুরোনো ঘরেই থেকে যাবে, অথচ এখন % 8 দিয়ে খুঁজতে হবে।
  3. পুরোনো [0]-এর দুজনকে সরানো। প্রতিটি চাবির hash আবার % 8 করা হলো: Lima → 0, Mitu → 4. যে দুজন আগে এক ঘরে ছিল, এখন আলাদা। খেয়াল করুন — node নতুন করে বানানো হয়নি; শুধু তাদের next pointer বদলে নতুন শিকলে জোড়া হয়েছে।
  4. পুরোনো [1]. Popy → 5, Tanvir → 1. একটি সুন্দর নিয়ম দেখা যাচ্ছে: ঘরের সংখ্যা দ্বিগুণ হলে পুরোনো ঘর i-এর প্রতিটি চাবি যায় হয় i-তে, নয়তো i + 4-এ।
  5. পুরোনো [2]. Shila → 6, Nusrat → 2.
  6. পুরোনো [3]. Tuli → 3, Rafi → 7. সব ৮টি চাবি নতুন জায়গায় পৌঁছেছে — এবার প্রতিটি ঘরে ঠিক একজন।
  7. পুরোনো array ফেরত। পুরোনো ৪টি pointer-এর array free করা হলো (node-গুলো নয় — সেগুলো এখন নতুন টেবিলের)। Load factor নেমে 1.0. এই পুরো কাজটি ৮টি চাবির প্রত্যেকটিকে একবার করে ছোঁয় — অর্থাৎ resize-এর খরচ O(n).

একটি insert-এ পুরো টেবিল নতুন করে সাজানো — শুনতে ভয়ংকর। কিন্তু পর্ব ২০-এর dynamic array-র যুক্তিটি এখানেও খাটে। প্রতিবার দ্বিগুণ করলে বড় কাজটি ঘটে খুব কম — ৮, ১৬, ৩২, ৬৪… চাবিতে একবার করে। n বার insert-এ মোট সরানো হয় n-এর দ্বিগুণেরও কম। ভাগ করে দিলে প্রতিটি insert-এর গড় খরচ ধ্রুব থাকে — একে বলে amortized O(1).

Amortized মানে “গড়ে সস্তা”, “প্রতিবার সস্তা” নয়। যে insert-টি resize ঘটায়, সেটি একাই অনেক সময় নেয়। ভিডিও গেম বা শেয়ারবাজারের লেনদেনের মতো সিস্টেমে হঠাৎ একটি লম্বা বিরতিও বিপদ। তাই কিছু সিস্টেম, যেমন Redis ডেটাবেস, একবারে সব না সরিয়ে প্রতিটি সাধারণ কাজের সাথে অল্প অল্প করে চাবি নতুন টেবিলে সরায় (incremental rehashing).

৭ · খরচের হিসাব — গড়ে O(1), সবচেয়ে খারাপে O(n)

এখন আমরা সৎভাবে খরচের হিসাব করতে পারি। Hash function ভালো আর load factor একটি সীমার নিচে থাকলে প্রতিটি শিকল গড়ে ছোট, তাই insert, lookup আর delete — তিনটিই average case-এ O(1). কিন্তু যদি কোনো কারণে সব চাবি একই ঘরে পড়ে? তখন একটি শিকলেই n টি node, আর খোঁজা আবার linear search — worst case O(n).

Hash function কতটা গুরুত্বপূর্ণ, তা নিজে মেপে দেখুন। নিচের প্রোগ্রাম ১,০০০টি চাবি (user0 থেকে user999) ১০১টি ঘরে বসায় — দুটি আলাদা hash দিয়ে। একটি “খেলনা” hash শুধু অক্ষরগুলো যোগ করে; অন্যটি djb2. দুটি ফাংশনই আমরা function pointer হিসেবে test-এ পাঠাচ্ছি (পর্ব ২৮):

#include <stdio.h>
#define NB 101                                  /* a prime number of buckets */
unsigned long toy(const char *s) { unsigned long h = 0; while (*s) h += (unsigned char)*s++; return h; }
unsigned long djb2(const char *s) {
    unsigned long h = 5381;
    while (*s) h = h * 33 + (unsigned char)*s++;
    return h;
}
void test(const char *name, unsigned long (*f)(const char *)) {
    int count[NB] = {0}, used = 0, worst = 0;  char key[16];
    for (int i = 0; i < 1000; i++) {
        snprintf(key, sizeof key, "user%d", i);       /* user0 ... user999 */
        int b = (int)(f(key) % NB);
        if (count[b]++ == 0) used++;
        if (count[b] > worst) worst = count[b];
    }
    printf("%-4s: %3d of %d buckets used, longest chain = %d\n", name, used, NB, worst);
}
int main(void) { test("toy", toy); test("djb2", djb2); return 0; }

আউটপুট: খেলনা hash ১০১টি ঘরের মধ্যে মাত্র 45টি ব্যবহার করে, আর সবচেয়ে লম্বা শিকলে 71টি চাবি। djb2 101টির সবগুলো ঘর ব্যবহার করে, সবচেয়ে লম্বা শিকল মাত্র 15 (আদর্শ গড় প্রায় 10). কারণটি সহজ: যোগফল অক্ষরের ক্রম দেখে না, তাই “user12” আর “user21” একই ফল দেয়। তা ছাড়া তিন অঙ্কের যোগফল 0 থেকে 27-এর বাইরে যায় না, তাই হাজার চাবি মাত্র কয়েক ডজন আলাদা মানে গাদাগাদি করে। খেলনা hash-এ “user” দিয়ে শুরু হওয়া কোনো নাম খুঁজতে ৭১টি পর্যন্ত তুলনা লাগতে পারে — টেবিলটি নামে hash table, কাজে প্রায় linked list. আরেকটি বিষয়: ঘরের সংখ্যা মৌলিক (prime) রাখলে দুর্বল hash-এর কিছু নিয়মিত প্যাটার্ন ভেঙে যায়; ঘর ২-এর ঘাত (৬৪, ১২৮) হলে % শুধু hash-এর নিচের কয়েকটি bit দেখে, তখন hash-এর মান আরও ভালোভাবে মেশানো দরকার হয়।

Hash table বনাম BST — কখন কোনটি

পর্ব ৩৫-এর BST-ও চাবি দিয়ে খোঁজে। তাহলে সবসময় hash table নয় কেন? কারণ hash table ইচ্ছে করেই চাবিগুলোর ক্রম ভেঙে দেয় — “Dhaka” আর “Dhakb” দুই প্রান্তে যায়। ফলে কিছু প্রশ্নের উত্তর সে দিতে পারে না:

প্রশ্নHash tableভারসাম্যপূর্ণ BST
“Mitu-র নম্বর কত?”গড়ে O(1) — সেরাO(log n)
“সব নাম বর্ণানুক্রমে দেখাও”সাজাতে আলাদা O(n log n)সরাসরি O(n) — ক্রমেই সাজানো
“৭০ থেকে ৮০ নম্বর পাওয়া সবাই”পুরো টেবিল ঘাঁটতে হয়শুধু সেই অংশটুকু
“সবচেয়ে ছোট চাবি কোনটি?”O(n)O(log n)
সবচেয়ে খারাপ ক্ষেত্রের নিশ্চয়তানেই — O(n) হতে পারেআছে — O(log n)
শুধু “আছে কি না, থাকলে কী” জানতে চাইলে — hash table.
ক্রম, পরিসর বা নিশ্চয়তা লাগলে — tree.

৮ · Hash flooding — যখন collision নিজেই অস্ত্র

অংশ ৭-এর worst case-কে আমরা “দুর্ভাগ্য” ভেবেছিলাম। কিন্তু যদি কেউ ইচ্ছে করে দুর্ভাগ্য তৈরি করে? djb2-এর মতো hash-এর সূত্র সবাই জানে। তাই একজন আক্রমণকারী আগে থেকেই হিসাব করে হাজার হাজার এমন চাবি বানাতে পারেন, যেগুলো সবই একই ঘরে পড়বে।

একটি ওয়েব সার্ভারের কথা ভাবুন। কোনো ফর্ম জমা দিলে সার্ভার সাধারণত সব ফিল্ডের নাম আর মান একটি hash table-এ রাখে। আক্রমণকারী একটি অনুরোধে এক লাখ ফিল্ড পাঠালেন, যাদের সবার নাম একই ঘরে পড়ে। প্রতিটি নতুন ফিল্ড রাখার আগে সার্ভার পুরো শিকল ঘেঁটে দেখে নামটি আগে আছে কি না — প্রথমটিতে ১টি তুলনা, দ্বিতীয়টিতে ২টি… মোট প্রায় ৫০০ কোটি তুলনা, একটিমাত্র অনুরোধে। কয়েকটি এমন অনুরোধেই সার্ভারের CPU পুরো ব্যস্ত, সাধারণ গ্রাহকেরা আর সাড়া পান না। এটি একধরনের denial-of-service আক্রমণ, যার নাম hash flooding.

২০১১ সালের ডিসেম্বরে জার্মানির হ্যাকার সম্মেলন 28C3-এ (Chaos Communication Congress) দুজন গবেষক প্রকাশ্যে দেখান, PHP, Java, Python, Ruby, ASP.NET সহ ওয়েবের অনেক জনপ্রিয় প্ল্যাটফর্ম এই আক্রমণের সামনে অসহায়। খুব ছোট একটি অনুরোধ পাঠিয়েই একটি সার্ভারের CPU দীর্ঘ সময় আটকে রাখা যেত। ধারণাটি নতুন ছিল না — এর আগেও গবেষণাপত্রে এমন “অ্যালগরিদমের জটিলতা দিয়ে আক্রমণ”-এর কথা লেখা হয়েছিল — কিন্তু এই উপস্থাপনার পর প্রায় সব বড় ভাষা ও প্ল্যাটফর্ম জরুরি সংশোধন প্রকাশ করে।

প্রতিরক্ষা — গোপন চাবিওয়ালা hash

সমস্যার মূল হলো, hash-এর হিসাব সবাই আগে থেকে করতে পারে। সমাধান: প্রোগ্রাম চালু হওয়ার সময় একটি এলোমেলো গোপন সংখ্যা (key/seed) বেছে নিন, আর hash-এর হিসাবে সেটি মিশিয়ে দিন। একই প্রোগ্রাম চলাকালীন hash নির্দিষ্ট থাকে (deterministic শর্ত ঠিক থাকে), কিন্তু বাইরের কেউ গোপন সংখ্যাটি না জানলে কোন চাবি কোন ঘরে যাবে আগে বলতে পারেন না।

শুধু একটি seed যোগ করাই যথেষ্ট নয় — কিছু দুর্বল hash-এ seed থাকা সত্ত্বেও collision বানানো যায়। এজন্য ২০১২ সালে Jean-Philippe Aumasson ও Daniel J. Bernstein (সেই djb2-এর স্রষ্টাই!) তৈরি করেন SipHash — ছোট চাবির জন্য দ্রুত, আর গোপন key ছাড়া এর ফল আন্দাজ করা কার্যত অসম্ভব হিসেবে নকশা করা। Python, Rust সহ বেশ কয়েকটি ভাষা তাদের ডিফল্ট hash table-এ SipHash বা এর কাছাকাছি কোনো keyed hash ব্যবহার করে। Java অন্য পথ নিয়েছে: HashMap-এ কোনো শিকল খুব লম্বা হলে সেটিকে একটি ভারসাম্যপূর্ণ tree-তে বদলে দেয়, ফলে সবচেয়ে খারাপ ক্ষেত্রেও খোঁজা O(log n).

নিজের সার্ভার বা যেকোনো প্রোগ্রাম যা বাইরের মানুষের দেওয়া চাবি hash table-এ রাখে (HTTP header, JSON-এর key, ব্যবহারকারীর নাম), সেখানে djb2-এর মতো প্রকাশ্য, seed ছাড়া hash ব্যবহার করবেন না। শেখার জন্য djb2 চমৎকার; ইন্টারনেটের সামনে নয়।

৯ · বাস্তবে hash table — আপনি প্রতিদিন যা ব্যবহার করেন

Hash table সম্ভবত সফটওয়্যারের সবচেয়ে বেশি ব্যবহৃত ডেটা স্ট্রাকচার। কয়েকটি জায়গা যেখানে এটি চুপচাপ কাজ করছে:

🐍 Python dict, JavaScript Map

Python-এ marks["Mitu"] লিখলে ভেতরে ঠিক এই পর্বের কাজটিই হয় — CPython-এর dict C-তে লেখা একটি open addressing hash table. JavaScript-এর Map আর Set-ও hash table দিয়ে তৈরি।

ভাষার ভেতরের ইঞ্জিন

⚡ Cache

একবার হিসাব করা বা ডাউনলোড করা জিনিস মনে রাখা, যাতে পরেরবার আর না লাগে। ব্রাউজার, ওয়েবসাইটের সার্ভার, এমনকি পর্ব ১৫-এর memoization — সবখানে চাবি দিয়ে উত্তর খোঁজা।

cache: Redis, Memcached

🛠️ Compiler-এর symbol table

Compiler আপনার কোডের প্রতিটি নাম — ভ্যারিয়েবল, ফাংশন — একটি symbol table-এ রাখে, যাতে total দেখলেই জানতে পারে এটি কী টাইপের, কোথায় ঘোষণা করা। পর্ব ২৯-এর linker-ও symbol মেলাতে একই কৌশল ব্যবহার করে।

প্রতিটি নাম দ্রুত খোঁজা

🗄️ ডেটাবেস

অনেক ডেটাবেস (যেমন PostgreSQL) একটি hash index বানাতে পারে, আর দুটি টেবিল জোড়া লাগাতে (join) প্রায়ই একটি অস্থায়ী hash table তৈরি করে — “hash join”.

“=” দিয়ে খোঁজার জন্য

একটি চেনা সমস্যা — ফোনবুকে ডুপ্লিকেট নম্বর

আপনার ফোনে ৫,০০০ কন্টাক্ট, অনেক নম্বর দুবার-তিনবার সেভ করা। কোনগুলো ডুপ্লিকেট? সহজ উপায়ে প্রতিটি নম্বরকে বাকি সবার সাথে মেলালে লাগে প্রায় ১.২৫ কোটি তুলনা (O(n²))। Hash table দিয়ে: প্রতিটি নম্বর একবার পড়ুন, টেবিলে আছে কি না দেখুন — থাকলে ডুপ্লিকেট, না থাকলে রেখে দিন। মোট মাত্র ৫,০০০ বার রাখা-খোঁজা, O(n). এই কাজটির নাম deduplication. Cloud storage-গুলোও এভাবে একই ফাইলের দুটি কপি চিনে ফেলে জায়গা বাঁচায় — সেখানে চাবি হলো ফাইলের ভেতরের ডেটার hash.

বাস্তবে একটি সতর্কতা: “01711-234567” আর “+8801711234567” একই নম্বর, কিন্তু string হিসেবে আলাদা — তাই আলাদা hash. Hash table-এ রাখার আগে চাবিকে একই রূপে আনুন (জায়গা ও ড্যাশ বাদ, দেশের কোড ঠিক করা)। Hash table শুধু হুবহু মিল চেনে; “প্রায় একই” চেনে না।

এই মডিউলে আমরা কাঠামোগুলো C-তে নিজ হাতে, মেমরির চোখ দিয়ে বানাচ্ছি। Cuckoo hashing, Robin Hood hashing বা consistent hashing-এর মতো উন্নত কৌশল আর তাদের গাণিতিক বিশ্লেষণ গভীরভাবে পাবেন ডেটা স্ট্রাকচার ও অ্যালগরিদম কোর্সে।

১০ · AI-এর যুগে hash table — যা যাচাই করবেন

“C-তে একটি hash table লিখে দাও” — AI এটি কয়েক সেকেন্ডে লিখবে, আর প্রথম দেখায় কোড পরিষ্কারও লাগবে। সমস্যা হলো, hash table-এর বাগগুলো ছোট পরীক্ষায় ধরা পড়ে না। দশটি চাবি দিয়ে পরীক্ষা করলে সব ঠিক; দশ লাখ চাবিতে বা বাইরের ইনপুটে গিয়ে সমস্যা দেখা দেয়।

AI-এর লেখা hash table-এ সাধারণ ভুল

AI-এর ভুলফলকীভাবে ধরবেন
নির্দিষ্ট আকারের টেবিল, কখনো resize হয় নাচাবি বাড়লে ধীরে ধীরে linked list-এর মতো ধীর১০ লাখ চাবি দিয়ে সময় মাপুন; count / size কোথাও যাচাই হচ্ছে কি না দেখুন
দুর্বল hash — অক্ষর যোগ, বা শুধু প্রথম অক্ষরঅনেক collision, কিছু শিকল বিশালউপরের spread.c-এর মতো শিকলের দৈর্ঘ্য গুনুন
key == k দিয়ে string তুলনাইনপুট থেকে আসা চাবি কখনো পাওয়া যায় নাfgets দিয়ে পড়া চাবিতে পরীক্ষা করুন
শিকলের node free না করা, শুধু array freeMemory leakValgrind বা AddressSanitizer
চাবির pointer রাখে, কপি নয়পরে buffer বদলালে টেবিলের চাবিও বদলে যায়একই buffer-এ দুটি আলাদা নাম পড়ে দেখুন
Linear probing-এ মুছলে ঘর একেবারে খালিঅন্য চাবি “হারিয়ে” যায়মোছার পর একই cluster-এর চাবি খুঁজুন
ঋণাত্মক hash দিয়ে %ঋণাত্মক index — array-র বাইরে লেখাhash টাইপ unsigned কি না দেখুন

AI-কে কাজ দেওয়ার সময় এই শর্তগুলো আগেই বলে দিন — তাহলে অর্ধেক ভুল শুরুতেই এড়ানো যায়:

C-তে string key থেকে int value রাখার একটি hash table লেখো (separate chaining). শর্ত: ১. hash function হিসেবে djb2 ব্যবহার করো, unsigned টাইপে। ২. load factor 0.75 পেরোলে bucket সংখ্যা দ্বিগুণ করে সব চাবি rehash করো। ৩. চাবি তুলনা strcmp দিয়ে; চাবির নিজস্ব কপি malloc করে রাখো। ৪. একই চাবি আবার দিলে নতুন node নয়, মান বদলাও। ৫. সব malloc-এর NULL যাচাই করো, আর একটি destroy ফাংশন দাও যা প্রতিটি node, প্রতিটি চাবি আর bucket array free করে। ৬. কোড gcc -Wall -Wextra-তে শূন্য warning দিয়ে compile হতে হবে। কোডের পরে সহজ বাংলায় বলো: worst case কখন O(n) হবে, আর বাইরের ইনপুটের জন্য এই hash কেন নিরাপদ নয়।

AI-এর দেওয়া hash table হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১সবাই বলে hash table-এ খোঁজা O(1). তাহলে এই “O(1)” কেন প্রতিশ্রুতি নয়, শুধু একটি গড়?

কারণ O(1) কথাটির পেছনে তিনটি শর্ত লুকিয়ে আছে, আর তিনটিই ভাঙতে পারে।

  • শর্ত ১ — চাবিগুলো “সাধারণ”: বিশ্লেষণটি ধরে নেয়, hash function চাবিগুলোকে প্রায় এলোমেলোভাবে ছড়িয়ে দেবে। কিন্তু hash function নির্দিষ্ট (deterministic). যেকোনো নির্দিষ্ট hash-এর জন্য এমন চাবির সেট অবশ্যই আছে, যার সবগুলো একই ঘরে পড়ে — pigeonhole principle থেকেই তা নিশ্চিত। স্বাভাবিক ডেটায় এমন সেট আসার সম্ভাবনা খুব কম; কিন্তু কেউ ইচ্ছে করে বানালে (hash flooding) সম্ভাবনার যুক্তি আর খাটে না।
  • শর্ত ২ — load factor নিয়ন্ত্রণে: resize না করলে n বাড়ার সাথে প্রতিটি শিকলও বাড়ে, খরচ আসলে O(n / m). m স্থির থাকলে এটি O(n).
  • শর্ত ৩ — গড় হিসাব: resize হওয়া insert একাই O(n). গড়ে সস্তা (amortized), কিন্তু সেই একটি মুহূর্তে নয়।

আরও একটি সূক্ষ্ম বিষয়: hash হিসাব করতে চাবির প্রতিটি অক্ষর পড়তে হয়। ১০ কিলোবাইটের একটি চাবির hash হিসাব “এক ধাপ” নয়। তাই সৎভাবে বললে খোঁজার খরচ চাবির দৈর্ঘ্যের সমানুপাতিক, আর ঘরের সংখ্যা থেকে প্রায় স্বাধীন। BST-তেও প্রতিটি তুলনায় string পড়তে হয়, তাই তুলনাটা ন্যায্যই থাকে।

এজন্য প্রকৌশলীরা দুই ধরনের নিশ্চয়তার পার্থক্য করেন: “expected” (সম্ভাবনার গড়ে) আর “worst-case” (যা-ই হোক না কেন)। বিমান নিয়ন্ত্রণ বা হৃদ্‌যন্ত্রের পেসমেকারের মতো real-time সিস্টেমে মাঝে মাঝে একটি ধীর কাজও বিপর্যয় ঘটাতে পারে — সেখানে প্রায়ই ভারসাম্যপূর্ণ tree বা আগে থেকে আকার ঠিক করা টেবিল বেছে নেওয়া হয়, যার খারাপতম সময় জানা।

মূল উপলব্ধি: Big-O-এর পাশে সবসময় জিজ্ঞেস করুন — “গড়ে, না সবচেয়ে খারাপে? কোন অনুমানের ওপর ভিত্তি করে?” একটি অ্যালগরিদমের প্রতিশ্রুতি ঠিক ততটাই শক্ত, যতটা তার অনুমানগুলো।

প্র ০২Chaining সহজ ও ভরা অবস্থায়ও স্থির। তবু Python-এর dict-এর মতো অনেক দ্রুত টেবিল open addressing বেছে নেয় কেন?

খাতা-কলমের হিসাবে chaining প্রায় সবদিকেই জেতে: মোছা সহজ, ভরা অবস্থায় ধীরে ধীরে খারাপ হয়, load factor ১-এর বেশিও চলে। কিন্তু আসল কম্পিউটারে গতি নির্ধারণ করে মেমরির নড়াচড়া, তুলনার সংখ্যা নয়।

  • Cache miss-এর দাম: আধুনিক CPU-তে RAM থেকে একটি নতুন cache line আনতে সময় লাগে কয়েকশো সাধারণ নির্দেশের সমান। Chaining-এ প্রতিটি node আলাদা malloc, heap-এর যেকোনো জায়গায় — শিকলের প্রতিটি ধাপে সম্ভবত একটি cache miss. Linear probing-এ পরের ঘর পাশেই, প্রায়ই একই cache line-এ — কার্যত বিনামূল্যে।
  • মেমরির খরচ: প্রতিটি node-এ একটি বাড়তি pointer (৬৪-বিট মেশিনে ৮ byte), সাথে malloc-এর নিজস্ব হিসাবের জায়গা। ছোট চাবি-মানের জোড়ায় এটি আসল ডেটার চেয়েও বেশি হতে পারে।
  • malloc-এর গতি: প্রতিটি insert-এ একটি malloc, প্রতিটি delete-এ একটি free — allocator-এর কাজও সস্তা নয়।

Open addressing-এর দুর্বলতা — clustering আর ভরা অবস্থায় ধস — সামলানো হয় load factor কম রেখে (CPython প্রায় দুই-তৃতীয়াংশে বড় করে) এবং চতুর probing নিয়মে। CPython সরাসরি পরের ঘরে না গিয়ে hash-এর বাকি bit মিশিয়ে লাফিয়ে লাফিয়ে ঘর বাছে, যাতে cluster কম জমে। Google-এর Abseil বা Rust-এর ডিফল্ট টেবিলের মতো আধুনিক নকশাগুলো আরও এক ধাপ এগিয়ে — একসাথে অনেকগুলো ঘরের ছোট চিহ্ন একটি CPU নির্দেশেই মিলিয়ে দেখে।

তবু chaining হারিয়ে যায়নি। C++-এর std::unordered_map chaining-এর ওপর দাঁড়িয়ে, কারণ ভাষার নিয়ম বলে কোনো উপাদানের ঠিকানা rehash-এর পরেও বদলাবে না — node আলাদা থাকলেই কেবল এটি সম্ভব। Java-ও chaining রেখে লম্বা শিকলকে tree বানায়।

মূল উপলব্ধি: অ্যালগরিদমের বইয়ের “ধাপ” আর হার্ডওয়্যারের “সময়” এক জিনিস নয়। একই Big-O-এর দুটি নকশার মধ্যে কয়েক গুণ পার্থক্য আসে শুধু মেমরিতে ডেটা কীভাবে সাজানো, তা থেকে — এই মডিউলের “মেমরির চোখে দেখা” ঠিক এ কারণেই।

প্র ০৩২০১১ সালের পর ভাষাগুলো randomised বা keyed hash-এ চলে গেল কেন? এতে কি কোনো দাম দিতে হয়েছে?

28C3-এর উপস্থাপনা দেখিয়ে দেয়, সমস্যাটি কোনো একটি ভাষার বাগ নয় — এটি একটি নকশাগত অনুমানের ব্যর্থতা। সবাই ধরে নিয়েছিল চাবিগুলো “প্রকৃতি” থেকে আসে, এলোমেলো। কিন্তু ইন্টারনেটে চাবি আসে প্রতিপক্ষের কাছ থেকে। যে সিস্টেম প্রতিপক্ষের ইনপুট নেয়, তার গড়-ক্ষেত্রের বিশ্লেষণ আর নিরাপদ নয়।

প্রথম জরুরি সংশোধনগুলো ছিল দ্রুত ও ব্যবহারিক — যেমন একটি অনুরোধে সর্বোচ্চ কতগুলো ফিল্ড থাকতে পারবে, তার সীমা বেঁধে দেওয়া। এরপর Python ও আরও কয়েকটি ভাষা hash-এ প্রোগ্রাম চালুর সময়ের এলোমেলো seed যোগ করে। কিন্তু গবেষকেরা পরে দেখান, কিছু পুরোনো hash-এ seed থাকলেও seed-নিরপেক্ষ collision বানানো যায়। তাই ভাষাগুলো SipHash-এর মতো নকশাগতভাবে শক্ত keyed hash-এ চলে যায়।

দাম কী দিতে হলো?

  • গতি: SipHash, djb2-এর চেয়ে ধীর। ছোট চাবিতে পার্থক্য সামান্য, তবে মাপা যায়। এজন্য কিছু ভাষা পরে আরও হালকা সংস্করণ বেছে নেয়, নিরাপত্তা ও গতির মাঝামাঝি।
  • পুনরুৎপাদনযোগ্যতা: প্রতিবার প্রোগ্রাম চালালে hash মান বদলায়। ফলে Python-এর set-এর ঘোরার ক্রম এক রান থেকে আরেক রানে বদলে যেতে পারে। যেসব পুরোনো পরীক্ষা নির্দিষ্ট ক্রম ধরে নিয়েছিল, সেগুলো ভেঙে যায়। ডিবাগ করার জন্য Python-এ PYTHONHASHSEED দিয়ে seed স্থির করা যায়।
  • সতর্কতার অভ্যাস: hash মান কখনো ফাইলে লিখে রাখা বা নেটওয়ার্কে পাঠানো যায় না, কারণ পরের রানে মিলবে না।

মূল উপলব্ধি: “গড়ে দ্রুত” আর “প্রতিপক্ষের সামনে নিরাপদ” দুটি আলাদা প্রতিশ্রুতি। ইনপুট যখনই অবিশ্বস্ত উৎস থেকে আসে, তখন প্রশ্ন করুন — “এই ইনপুট যদি কেউ সবচেয়ে খারাপভাবে সাজায়?” নিরাপত্তা প্রায়ই সামান্য গতি দিয়ে কেনা হয়, আর সেটি প্রায় সবসময় লাভের সওদা।

প্র ০৪Hash table এত দ্রুত — তাহলে ডেটাবেস ও ফাইল সিস্টেম কেন বেশিরভাগ ক্ষেত্রে tree (B-tree) ব্যবহার করে?

কারণ বাস্তবের প্রশ্নগুলোর বড় অংশ “হুবহু এই চাবিটি কোথায়?” নয়। ভাবুন একটি ব্যাংকের লেনদেনের ডেটাবেস:

  • “গত মাসের ১ থেকে ১৫ তারিখের সব লেনদেন” — পরিসর (range).
  • “সবচেয়ে বড় ১০টি লেনদেন” — ক্রম।
  • “যাদের নাম ‘Rah’ দিয়ে শুরু” — prefix.

Hash table চাবিগুলোর ক্রম ইচ্ছে করেই ভেঙে দেয় — ভালো hash-এর গুণই এটা। ১ তারিখ আর ২ তারিখ টেবিলের দুই প্রান্তে, তাই পরিসরের প্রশ্নে পুরো টেবিল ঘাঁটতে হয়। পর্ব ৩৫-এর BST-এর মতো tree-তে চাবিগুলো সাজানো থাকে, তাই প্রথম চাবিটি খুঁজে পেলে বাকিগুলো পাশাপাশি পড়ে যায়।

ডিস্কে আরেকটি কারণ যোগ হয়। ডিস্ক থেকে একবারে বড় একটি block পড়া হয়। B-tree প্রতিটি node-এ শত শত চাবি রাখে, ফলে tree খুব চ্যাপ্টা — কোটি কোটি সারির মধ্যেও ৩–৪টি block পড়লেই চাবি পাওয়া যায়। আর ডেটা বাড়লে B-tree অল্প অল্প করে বাড়ে; বিশাল ডিস্ক-টেবিলের একসাথে পুরো rehash খুব ব্যয়বহুল।

তবু hash-এর জায়গা আছে। “এই আইডির গ্রাহক” বা দুটি টেবিল জোড়া লাগানোর (hash join) মতো শুধু সমতার প্রশ্নে hash অপরাজেয়, আর তাই PostgreSQL-এর মতো ডেটাবেসে hash index বানানোর সুবিধাও আছে। প্রোগ্রামিং ভাষাতেও দুটি পাশাপাশি থাকে: C++-এ std::map (tree, সাজানো) আর std::unordered_map (hash). নামের মধ্যেই পার্থক্যটা লেখা — “unordered”.

মূল উপলব্ধি: ডেটা স্ট্রাকচার বাছাইয়ের আগে জিজ্ঞেস করুন, “আমি ডেটাকে কোন কোন প্রশ্ন করব?” শুধু সমতা হলে hash; ক্রম, পরিসর বা prefix লাগলে tree. সবচেয়ে দ্রুত কাঠামো নয়, বরং আপনার প্রশ্নের সাথে মানানসই কাঠামোই সেরা।

প্র ০৫SHA-256-ও তো “hash”. তাহলে hash table-এ SHA-256 ব্যবহার করি না কেন, আর পাসওয়ার্ড রাখতে djb2 কেন ভয়ংকর?

একই নাম, কিন্তু দুটি সম্পূর্ণ আলাদা কাজের যন্ত্র — যেমন রান্নাঘরের ছুরি আর সার্জনের স্ক্যালপেল।

  • Table hash (djb2, SipHash): লক্ষ্য গতি আর সমান বণ্টন। ফল ছোট (৩২ বা ৬৪ bit), আর শেষে % m দিয়ে আরও ছোট করা হয় — collision তো স্বাভাবিক, টেবিল নিজেই সেটা সামলায়। কেউ উল্টো দিকে হিসাব করে চাবি বের করতে পারলেও কিছু যায় আসে না (SipHash-এর ক্ষেত্রে শুধু গোপন key জানা না থাকাটাই জরুরি)।
  • Cryptographic hash (SHA-256): ফল ২৫৬ bit. লক্ষ্য তিনটি কঠিন প্রতিশ্রুতি — ফল থেকে মূল ইনপুট বের করা কার্যত অসম্ভব; একই ফলের দুটি ইনপুট খুঁজে বের করাও কার্যত অসম্ভব; ইনপুটের একটি bit বদলালে ফলের প্রায় অর্ধেক bit বদলায়। এর জন্য অনেকগুলো জটিল round চালাতে হয়।

Hash table-এ SHA-256 কেন নয়? কারণ টেবিলের কাছে ২৫৬ bit-এর বেশিরভাগই অপচয় — শেষে তো % m করে কয়েকটি bit-ই নেওয়া হবে। আর ছোট চাবিতে SHA-256 অনেক গুণ ধীর। টেবিলের যতটুকু নিরাপত্তা দরকার (flooding ঠেকানো), SipHash সেটুকু অনেক কম খরচে দেয়।

পাসওয়ার্ডে djb2 কেন ভয়ংকর? djb2 উল্টানো সহজ, collision বানানো সহজ, আর খুব দ্রুত — আক্রমণকারী সেকেন্ডে কোটি কোটি অনুমান যাচাই করতে পারবে। মজার ব্যাপার, পাসওয়ার্ডের জন্য SHA-256-ও যথেষ্ট নয়, কারণ সেটিও খুব দ্রুত। পাসওয়ার্ড রাখতে ইচ্ছে করে ধীর ও মেমরি-খেকো hash লাগে — bcrypt, scrypt বা Argon2 — প্রতিটি ব্যবহারকারীর জন্য আলাদা salt সহ। bKash বা ব্যাংকের অ্যাপের মতো কোনো সিস্টেম যদি ব্যবহারকারীর PIN দ্রুত hash দিয়ে রাখত, ডেটাবেস ফাঁস হলেই সব PIN কয়েক মিনিটে বের হয়ে যেত।

মূল উপলব্ধি: “hash” একটি পরিবার, একটি যন্ত্র নয়। প্রতিটি hash-এর প্রশ্ন আলাদা — টেবিলের জন্য “দ্রুত ও ছড়ানো?”, অখণ্ডতার জন্য “জাল করা অসম্ভব?”, পাসওয়ার্ডের জন্য “অনুমান করা যথেষ্ট ধীর?” ভুল কাজে ভুল hash — এটি বাস্তবের বহু নিরাপত্তা-বিপর্যয়ের কারণ।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে হিসাব করা আর ভাবাই আসল কাজ।

  1. হাতে hash: একটি সহজ hash ধরুন — অক্ষরের ASCII মান যোগ করে % 5. “ab” (97, 98) আর “ba”-র hash বের করুন। তারপর “Joy” (74, 111, 121) কোন ঘরে যায়? এই hash-এর সবচেয়ে বড় দুর্বলতা কী?

    “ab”: 97 + 98 = 195, 195 % 5 = 0. “ba”: 98 + 97 = 195 — একই, ঘর 0. “Joy”: 74 + 111 + 121 = 306, 306 % 5 = 1, ঘর ১. দুর্বলতা: যোগফল অক্ষরের ক্রম দেখে না, তাই একই অক্ষর দিয়ে তৈরি সব শব্দ (anagram) সবসময় collide করে। djb2-এর h * 33 প্রতিটি অক্ষরের অবস্থানকে আলাদা গুরুত্ব দেয় — এটাই পার্থক্য।

  2. Linear probing অনুসরণ: ৭ ঘরের একটি টেবিলে (০–৬) চাবিগুলোর hash: A → 3, B → 3, C → 4, D → 6, E → 3. এই ক্রমে রাখলে প্রতিটি কোন ঘরে বসবে? E রাখতে কতগুলো ঘর দেখতে হলো?

    A → ৩ (খালি)। B → ৩ ভরা, ৪ খালি → ৪. C → ৪ ভরা (B দখল করেছে!), ৫ খালি → ৫. D → ৬ (খালি)। E → ৩, ৪, ৫, ৬ সব ভরা, ঘুরে ০ — খালি → ০. E-র জন্য ৫টি ঘর দেখতে হলো। খেয়াল করুন, C-র নিজের hash-এ কারও সাথে collision ছিল না, তবুও সরতে হলো — primary clustering. ৭ ঘরে ৫টি চাবি মানে load factor প্রায় 0.71, আর এর মধ্যেই একটি চাবির জন্য ৫ বার দেখা লাগছে।

  3. Load factor ও resize: একটি chaining টেবিল ১৬ ঘর দিয়ে শুরু হয়, load factor 0.75 পেরোলেই দ্বিগুণ হয়। ১,০০০টি চাবি রাখলে কতবার resize হবে, শেষে ঘর কতটি থাকবে?

    সীমা: ১৬ × 0.75 = ১২, ৩২ → ২৪, ৬৪ → ৪৮, ১২৮ → ৯৬, ২৫৬ → ১৯২, ৫১২ → ৩৮৪, ১০২৪ → ৭৬৮, ২০৪৮ → ১৫৩৬. ১,০০০টি চাবিতে ৭৬৮-এর সীমা পেরোয়, কিন্তু ১৫৩৬ নয়। তাই resize হয় ৭ বার (১৬→৩২→৬৪→১২৮→২৫৬→৫১২→১০২৪→২০৪৮), শেষে ২,০৪৮টি ঘর, load factor প্রায় 0.49. সব resize মিলিয়ে মোট সরানো চাবি ১২ + ২৪ + … + ৭৬৮-এর কাছাকাছি, অর্থাৎ ১,৫০০-র মতো — চাবির সংখ্যার দেড় গুণের মতো। এটাই amortized O(1): গড়ে প্রতিটি চাবি দুবারেরও কম সরেছে।

  4. কোড বদলে দেখুন: spread.c-এ NB 101 থেকে 64 করে আবার চালান, তারপর 1 করে। ফলাফল কীভাবে বদলায়, আর কেন?

    NB = 64-এ djb2 এখনো ৬৪টি ঘরই ব্যবহার করে, কিন্তু সবচেয়ে লম্বা শিকল ১৫ থেকে বেড়ে প্রায় ৩৫ হয় (আদর্শ গড় প্রায় ১৬)। কারণ ৬৪ = 2⁶, তাই % 64 শুধু hash-এর নিচের ৬টি bit দেখে, আর একই রকম চাবিতে djb2-এর নিচের bit-গুলো তত ভালো মেশে না। খেলনা hash-এ দুটি ক্ষেত্রেই অবস্থা খারাপ। NB = 1-এ দুটি hash-ই একই ফল দেয় — ১টি ঘর, শিকল ১,০০০ লম্বা। Hash যত ভালোই হোক, ঘর না থাকলে ছড়ানোর জায়গা নেই — এজন্যই resize লাগে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমSeparate chaining টেবিলে একটি চাবি খোঁজার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔑 চাবিটি নিন, যেমন “Rafi”
  2. 🧮 Hash function দিয়ে চাবি থেকে একটি বড় সংখ্যা হিসাব করুন
  3. ➗ % bucket_count করে ঘরের নম্বর বের করুন
  4. 🔗 সেই ঘরের শিকলের প্রথম node-এ যান
  5. 🔍 প্রতিটি node-এর চাবি strcmp দিয়ে মেলান — মিললে মান, NULL পেলে “নেই”

একটি hash function-এর জন্য কোন গুণটি ঐচ্ছিক নয়, বাধ্যতামূলক?

  • সবসময় খুব ছোট সংখ্যা দেওয়া
  • একই চাবিতে প্রতিবার একই ফল দেওয়া (deterministic)
  • কখনো collision না হওয়া
  • ফল থেকে চাবি বের করা অসম্ভব হওয়া

৮ ঘরের chaining টেবিলে “Rafi” আর “Joy” দুটোই ৭ নম্বর ঘরে পড়ল। get("Rafi") কীভাবে সঠিক মানটি দেয়?

  • ঘরের প্রথম node-এর মানই ফেরত দেয়
  • Hash আবার হিসাব করে অন্য ঘর খোঁজে
  • ৭ নম্বর শিকলের প্রতিটি node-এর চাবি strcmp দিয়ে মেলায়
  • Collision হয়েছে, তাই error দেয়

Linear probing টেবিল থেকে একটি চাবি মুছতে ঘরটি সাধারণভাবে খালি করে দিলে কী সমস্যা হয়?

  • Load factor কমে যায়
  • Memory leak হয়
  • Hash function-এর ফল বদলে যায়
  • মোছা ঘরের পরে সরে বসা অন্য চাবি আর খুঁজে পাওয়া যায় না

টেবিল বড় করার সময় শুধু realloc দিয়ে bucket array বড় করলে কেন কাজ হয় না?

  • ঘরের সংখ্যা বদলালে প্রতিটি চাবির সঠিক ঘর বদলে যায়, তাই সবাইকে নতুন করে বসাতে হয়
  • realloc বড় জায়গা দিতে পারে না
  • realloc পুরোনো ডেটা মুছে ফেলে
  • নতুন ঠিকানায় hash function কাজ করে না

“৭০ থেকে ৮০ নম্বর পাওয়া সব ছাত্র, নম্বরের ক্রমে” — এই প্রশ্নের জন্য কোন কাঠামো ভালো?

  • Hash table, কারণ এটি সবচেয়ে দ্রুত
  • ভারসাম্যপূর্ণ BST
  • Linear probing টেবিল
  • সাজানো নয় এমন linked list

Hash flooding আক্রমণ ঠেকানোর মূল কৌশল কোনটি?

  • টেবিলের আকার অনেক বড় রাখা
  • djb2 ব্যবহার করা, কারণ এটি বিখ্যাত
  • প্রোগ্রাম চালুর সময় বাছা গোপন key দিয়ে hash করা (যেমন SipHash)
  • প্রতিটি চাবিতে SHA-256 ব্যবহার করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Hash table = array + hash function; চাবি থেকে index হিসাব করে সোজা সেই ঘরে যাওয়া।
  2. ভালো hash: নির্দিষ্ট, সমানভাবে ছড়ানো, দ্রুত — যেমন djb2: h = h * 33 + c, শেষে % bucket_count.
  3. Collision অবধারিত — সামলান chaining (প্রতি ঘরে linked list) বা linear probing (পরের খালি ঘর, মুছলে tombstone) দিয়ে।
  4. Load factor সীমা পেরোলে দ্বিগুণ বড় টেবিলে rehash; গড় খরচ amortized O(1), সবচেয়ে খারাপে O(n).
  5. String চাবি strcmp দিয়ে, প্রতিটি node free, আর বাইরের ইনপুটে keyed hash; ক্রম লাগলে hash নয়, tree.

পরের পর্বে: এ পর্যন্ত প্রতিটি কাঠামোতে ডেটা সাজানো ছিল একটি সারি বা একটি গাছের মতো। কিন্তু ঢাকার রাস্তা, বন্ধুদের নেটওয়ার্ক বা মেট্রো রেলের স্টেশন — এদের কোনো “মূল” নেই, যে কেউ যে কারও সাথে যুক্ত। পরের পর্বে গ্রাফ বানাব, আর BFS ও DFS দিয়ে তার ভেতর পথ খুঁজব — যেখানে “এই জায়গা আগে দেখেছি কি না” মনে রাখতে আবারও কাজে লাগবে hash table-এর ধারণা।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • পর্ব ৩৮ · গ্রাফ — BFS ও DFSপরবর্তী পর্বশহর ও রাস্তার মতো যুক্ত ডেটা — C-তে গ্রাফ বানানো আর পথ খোঁজা।
  • C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
  • Data Structures & Algorithmsগভীরে যানHashing-এর গাণিতিক বিশ্লেষণ, cuckoo ও Robin Hood hashing, আর আরও অনেক কাঠামো।
  • Cybersecurityপাশের ট্র্যাকDenial-of-service আক্রমণ, cryptographic hash আর পাসওয়ার্ড নিরাপদে রাখার নিয়ম।
  • Database Management & SQLপাশের ট্র্যাকIndex কীভাবে কাজ করে — hash index বনাম B-tree, আর hash join.
এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান