Hash Map ব্যবহার করে Key এবং সংশ্লিষ্ট Value স্টোর করা
আমাদের সাধারণ collection-গুলোর মধ্যে সর্বশেষটি হলো hash map। HashMap<K, V>
টাইপটি K
টাইপের key-এর সাথে V
টাইপের value-এর একটি ম্যাপিং সংরক্ষণ করে। এটি একটি hashing function ব্যবহার করে নির্ধারণ করে কীভাবে মেমরিতে এই key এবং value-গুলো রাখা হবে। অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজ এই ধরনের ডেটা স্ট্রাকচার সমর্থন করে, কিন্তু তারা প্রায়শই ভিন্ন নাম ব্যবহার করে, যেমন hash, map, object, hash table, dictionary, বা associative array ইত্যাদি।
Hash map তখন দরকারী যখন আপনি কোনো index ব্যবহার করে ডেটা খুঁজতে চান না (যেমনটা vector-এর ক্ষেত্রে করা হয়), বরং একটি key ব্যবহার করে ডেটা খুঁজতে চান যা যেকোনো টাইপের হতে পারে। উদাহরণস্বরূপ, একটি গেমে, আপনি প্রতিটি দলের স্কোর একটি hash map-এ রাখতে পারেন, যেখানে প্রতিটি key হলো দলের নাম এবং value হলো সেই দলের স্কোর। একটি দলের নাম দিয়ে আপনি তার স্কোর পুনরুদ্ধার করতে পারবেন।
এই বিভাগে আমরা hash map-এর বেসিক API নিয়ে আলোচনা করব, কিন্তু standard library দ্বারা HashMap<K, V>
-তে সংজ্ঞায়িত ফাংশনগুলিতে আরও অনেক সুবিধা লুকিয়ে আছে। বরাবরের মতো, আরও তথ্যের জন্য standard library-এর ডকুমেন্টেশন দেখুন।
নতুন Hash Map তৈরি করা
একটি খালি hash map তৈরি করার একটি উপায় হলো new
ব্যবহার করা এবং insert
দিয়ে element যোগ করা। লিস্টিং ৮-২০-এ, আমরা দুটি দলের স্কোর ট্র্যাক করছি যাদের নাম Blue এবং Yellow। Blue দলের স্কোর ১০ দিয়ে শুরু হয় এবং Yellow দলের ৫০ দিয়ে।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); }
লক্ষ্য করুন যে আমাদের প্রথমে standard library-এর collections অংশ থেকে HashMap
use
করতে হবে। আমাদের তিনটি সাধারণ collection-এর মধ্যে এটি সবচেয়ে কম ব্যবহৃত হয়, তাই এটি prelude-এ স্বয়ংক্রিয়ভাবে স্কোপে আনা ফিচারগুলোর অন্তর্ভুক্ত নয়। Hash map-এর জন্য standard library থেকে কম সমর্থনও রয়েছে; উদাহরণস্বরূপ, এটি তৈরি করার জন্য কোনো বিল্ট-ইন ম্যাক্রো নেই।
Vector-এর মতোই, hash map তাদের ডেটা heap-এ স্টোর করে। এই HashMap
-এর key-গুলো String
টাইপের এবং value-গুলো i32
টাইপের। Vector-এর মতোই, hash map-ও সমজাতীয় (homogeneous): সমস্ত key-এর টাইপ একই হতে হবে এবং সমস্ত value-এর টাইপও একই হতে হবে।
Hash Map-এর Value অ্যাক্সেস করা
আমরা hash map থেকে একটি value পেতে পারি তার key get
মেথডে সরবরাহ করে, যেমনটি লিস্টিং ৮-২১-এ দেখানো হয়েছে।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name).copied().unwrap_or(0); }
এখানে, score
-এর ভ্যালু হবে Blue দলের সাথে যুক্ত ভ্যালুটি, এবং ফলাফল হবে 10
। get
মেথডটি একটি Option<&V>
রিটার্ন করে; যদি hash map-এ সেই key-এর জন্য কোনো ভ্যালু না থাকে, get
None
রিটার্ন করবে। এই প্রোগ্রামটি Option
-কে copied
কল করে একটি Option<i32>
পায় (Option<&i32>
-এর পরিবর্তে), তারপর unwrap_or
ব্যবহার করে score
-কে শূন্যতে সেট করে যদি scores
-এ key-টির জন্য কোনো এন্ট্রি না থাকে।
আমরা একটি for
লুপ ব্যবহার করে hash map-এর প্রতিটি key-value পেয়ারের উপর দিয়ে ইটারেট করতে পারি, যেমনটা আমরা vector-এর সাথে করি:
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{key}: {value}"); } }
এই কোডটি প্রতিটি জোড়া একটি অনির্দিষ্ট ক্রমে প্রিন্ট করবে:
Yellow: 50
Blue: 10
Hash Map এবং Ownership
যেসব টাইপ Copy
trait ইমপ্লিমেন্ট করে, যেমন i32
, সেগুলোর ভ্যালু hash map-এ কপি হয়। String
-এর মতো owned ভ্যালুর ক্ষেত্রে, ভ্যালুগুলো মুভ (move) হবে এবং hash map সেই ভ্যালুগুলোর মালিক হবে, যেমনটি লিস্টিং ৮-২২-এ দেখানো হয়েছে।
fn main() { use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name and field_value are invalid at this point, try using them and // see what compiler error you get! }
insert
কলের মাধ্যমে field_name
এবং field_value
ভ্যারিয়েবলগুলো hash map-এ মুভ হয়ে যাওয়ার পরে আমরা আর সেগুলো ব্যবহার করতে পারি না।
যদি আমরা hash map-এ ভ্যালুর রেফারেন্স যুক্ত করি, তবে ভ্যালুগুলো hash map-এ মুভ হবে না। রেফারেন্সগুলো যে ভ্যালুকে নির্দেশ করে, সেই ভ্যালুগুলো অন্তত hash map যতদিন বৈধ থাকবে, ততদিন বৈধ থাকতে হবে। আমরা এই বিষয়গুলো নিয়ে অধ্যায় ১০-এর “Validating References with Lifetimes” বিভাগে আরও আলোচনা করব।
একটি Hash Map আপডেট করা
যদিও key-value পেয়ারের সংখ্যা বাড়ানো যায়, প্রতিটি স্বতন্ত্র key-এর সাথে একবারে কেবল একটিই value যুক্ত থাকতে পারে (কিন্তু এর বিপরীতটি সত্য নয়: উদাহরণস্বরূপ, Blue এবং Yellow উভয় দলেরই scores
hash map-এ 10
ভ্যালুটি থাকতে পারে)।
যখন আপনি একটি hash map-এর ডেটা পরিবর্তন করতে চান, তখন আপনাকে সিদ্ধান্ত নিতে হবে যে একটি key-তে যখন আগে থেকেই একটি value বরাদ্দ থাকে তখন কী করবেন। আপনি পুরানো ভ্যালুটিকে সম্পূর্ণ উপেক্ষা করে নতুন ভ্যালু দিয়ে প্রতিস্থাপন করতে পারেন। আপনি পুরানো ভ্যালুটি রেখে নতুন ভ্যালুটি উপেক্ষা করতে পারেন, এবং শুধুমাত্র যদি key-টির কোনো ভ্যালু না থাকে তবেই নতুন ভ্যালু যোগ করতে পারেন। অথবা আপনি পুরানো এবং নতুন ভ্যালু একত্রিত করতে পারেন। আসুন দেখি কীভাবে এর প্রতিটি করা যায়!
একটি ভ্যালু ওভাররাইট করা
যদি আমরা একটি hash map-এ একটি key এবং একটি value যুক্ত করি এবং তারপরে একই key দিয়ে ভিন্ন একটি value যুক্ত করি, তবে সেই key-এর সাথে যুক্ত value প্রতিস্থাপিত হবে। যদিও লিস্টিং ৮-২৩-এর কোডটি দুবার insert
কল করে, hash map-এ কেবল একটি key-value পেয়ার থাকবে কারণ আমরা উভয়বারই Blue দলের key-এর জন্য value যুক্ত করছি।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{scores:?}"); }
এই কোডটি {"Blue": 25}
প্রিন্ট করবে। আসল 10
ভ্যালুটি ওভাররাইট করা হয়েছে।
শুধুমাত্র Key উপস্থিত না থাকলে একটি Key এবং Value যোগ করা
একটি সাধারণ কাজ হলো hash map-এ একটি নির্দিষ্ট key-এর জন্য কোনো value আছে কিনা তা পরীক্ষা করা এবং তারপরে নিম্নলিখিত পদক্ষেপ নেওয়া: যদি key-টি hash map-এ থাকে, তবে বিদ্যমান value অপরিবর্তিত থাকবে; যদি key-টি না থাকে, তবে এটি এবং এর জন্য একটি value যুক্ত করা হবে।
Hash map-এর এর জন্য একটি বিশেষ API আছে যার নাম entry
, যা প্যারামিটার হিসাবে আপনি যে key পরীক্ষা করতে চান তা নেয়। entry
মেথডের রিটার্ন ভ্যালু হলো Entry
নামের একটি enum, যা এমন একটি ভ্যালুকে প্রতিনিধিত্ব করে যা থাকতেও পারে বা নাও থাকতে পারে। ধরা যাক আমরা পরীক্ষা করতে চাই Yellow দলের key-এর সাথে কোনো value যুক্ত আছে কিনা। যদি না থাকে, আমরা 50
ভ্যালুটি যুক্ত করতে চাই, এবং Blue দলের জন্যও একই কাজ করতে চাই। entry
API ব্যবহার করে কোডটি লিস্টিং ৮-২৪-এর মতো দেখায়।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Blue")).or_insert(50); println!("{scores:?}"); }
Entry
-এর উপর or_insert
মেথডটি এমনভাবে সংজ্ঞায়িত করা হয়েছে যে এটি সংশ্লিষ্ট Entry
key-এর ভ্যালুর একটি mutable রেফারেন্স রিটার্ন করে যদি সেই key বিদ্যমান থাকে, এবং যদি না থাকে, তবে এটি প্যারামিটারটিকে এই key-এর নতুন ভ্যালু হিসাবে যুক্ত করে এবং নতুন ভ্যালুর একটি mutable রেফারেন্স রিটার্ন করে। এই কৌশলটি নিজেরা লজিক লেখার চেয়ে অনেক পরিষ্কার এবং borrow checker-এর সাথে আরও ভালোভাবে কাজ করে।
লিস্টিং ৮-২৪-এর কোডটি চালালে {"Yellow": 50, "Blue": 10}
প্রিন্ট হবে। প্রথম entry
কলটি Yellow দলের key 50
ভ্যালুসহ যুক্ত করবে কারণ Yellow দলের আগে থেকে কোনো ভ্যালু নেই। দ্বিতীয় entry
কলটি hash map পরিবর্তন করবে না কারণ Blue দলের আগে থেকেই 10
ভ্যালুটি রয়েছে।
পুরানো ভ্যালুর উপর ভিত্তি করে একটি ভ্যালু আপডেট করা
Hash map-এর আরেকটি সাধারণ ব্যবহার হলো একটি key-এর ভ্যালু খুঁজে বের করা এবং তারপর পুরানো ভ্যালুর উপর ভিত্তি করে এটি আপডেট করা। উদাহরণস্বরূপ, লিস্টিং ৮-২৫-এর কোডটি গণনা করে যে কিছু টেক্সটে প্রতিটি শব্দ কতবার আসে। আমরা শব্দগুলোকে key হিসাবে এবং তাদের সংখ্যা ট্র্যাক করার জন্য ভ্যালু বৃদ্ধি করে একটি hash map ব্যবহার করি। যদি আমরা প্রথমবার কোনো শব্দ দেখি, আমরা প্রথমে 0
ভ্যালুটি যুক্ত করব।
fn main() { use std::collections::HashMap; let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!("{map:?}"); }
এই কোডটি {"world": 2, "hello": 1, "wonderful": 1}
প্রিন্ট করবে। আপনি একই key-value পেয়ারগুলো ভিন্ন ক্রমে প্রিন্ট করা দেখতে পারেন: “Accessing Values in a Hash Map” থেকে মনে করুন যে একটি hash map-এর উপর ইটারেট করা একটি অনির্দিষ্ট ক্রমে ঘটে।
split_whitespace
মেথডটি text
-এর ভ্যালুর হোয়াইটস্পেস দ্বারা পৃথক করা সাবস্লাইসের উপর একটি ইটারেটর রিটার্ন করে। or_insert
মেথডটি নির্দিষ্ট key-এর ভ্যালুর একটি mutable রেফারেন্স (&mut V
) রিটার্ন করে। এখানে, আমরা সেই mutable রেফারেন্সটি count
ভ্যারিয়েবলে সংরক্ষণ করি, তাই সেই ভ্যালুতে অ্যাসাইন করার জন্য, আমাদের প্রথমে অ্যাস্টারিস্ক (*
) ব্যবহার করে count
-কে dereference করতে হবে। Mutable রেফারেন্সটি for
লুপের শেষে স্কোপের বাইরে চলে যায়, তাই এই সমস্ত পরিবর্তনগুলি নিরাপদ এবং borrowing rules দ্বারা অনুমোদিত।
হ্যাশিং ফাংশন (Hashing Functions)
ডিফল্টরূপে, HashMap
SipHash নামের একটি হ্যাশিং ফাংশন ব্যবহার করে যা denial-of-service (DoS) আক্রমণের বিরুদ্ধে প্রতিরোধ প্রদান করতে পারে1। এটি উপলব্ধ দ্রুততম হ্যাশিং অ্যালগরিদম নয়, কিন্তু পারফরম্যান্স হ্রাসের বিনিময়ে যে উন্নত নিরাপত্তা পাওয়া যায় তা মূল্যবান। যদি আপনি আপনার কোড প্রোফাইল করেন এবং দেখেন যে ডিফল্ট হ্যাশ ফাংশনটি আপনার উদ্দেশ্যের জন্য খুব ধীর, আপনি একটি ভিন্ন হ্যাশার নির্দিষ্ট করে অন্য ফাংশনে স্যুইচ করতে পারেন। একটি hasher হলো এমন একটি টাইপ যা BuildHasher
trait ইমপ্লিমেন্ট করে। আমরা অধ্যায় ১০-এ trait এবং কীভাবে সেগুলি ইমপ্লিমেন্ট করতে হয় সে সম্পর্কে কথা বলব। আপনাকে স্ক্র্যাচ থেকে নিজের হ্যাশার ইমপ্লিমেন্ট করতে হবে না; crates.io-তে অন্যান্য Rust ব্যবহারকারীদের দ্বারা শেয়ার করা লাইব্রেরি রয়েছে যা অনেক সাধারণ হ্যাশিং অ্যালগরিদম ইমপ্লিমেন্ট করে এমন হ্যাশার সরবরাহ করে।
সারসংক্ষেপ
Vector, string, এবং hash map প্রোগ্রামগুলিতে যখন ডেটা সংরক্ষণ, অ্যাক্সেস এবং পরিবর্তন করার প্রয়োজন হয় তখন একটি বিশাল পরিমাণ কার্যকারিতা প্রদান করবে। এখানে কিছু অনুশীলন রয়েছে যা আপনি এখন সমাধান করার জন্য সজ্জিত থাকা উচিত:
১. পূর্ণসংখ্যার একটি তালিকা দেওয়া হলে, একটি vector ব্যবহার করে তালিকাটির মিডিয়ান (median - সাজানো হলে মাঝের অবস্থানের মান) এবং মোড (mode - যে মানটি সবচেয়ে বেশিবার ঘটে; এখানে একটি hash map সহায়ক হবে) রিটার্ন করুন। ২. স্ট্রিংগুলিকে পিগ ল্যাটিনে (pig latin) রূপান্তর করুন। প্রতিটি শব্দের প্রথম কনসোনেন্ট (consonant) শব্দের শেষে সরানো হয় এবং ay যোগ করা হয়, তাই first হয়ে যায় irst-fay। যে শব্দগুলি ভাওয়েল (vowel) দিয়ে শুরু হয় সেগুলির শেষে hay যোগ করা হয় (apple হয়ে যায় apple-hay)। UTF-8 এনকোডিং সম্পর্কে বিস্তারিত মনে রাখবেন! ৩. একটি hash map এবং vector ব্যবহার করে, একটি টেক্সট ইন্টারফেস তৈরি করুন যা একজন ব্যবহারকারীকে একটি কোম্পানির একটি বিভাগে কর্মচারীর নাম যুক্ত করার অনুমতি দেয়; উদাহরণস্বরূপ, “Add Sally to Engineering” বা “Add Amir to Sales”। তারপরে ব্যবহারকারীকে একটি বিভাগের সমস্ত ব্যক্তির তালিকা বা বিভাগ অনুসারে কোম্পানির সমস্ত ব্যক্তির তালিকা বর্ণানুক্রমিকভাবে সাজানো অবস্থায় পুনরুদ্ধার করার অনুমতি দিন।
Standard library API ডকুমেন্টেশন vector, string, এবং hash map-এর এমন মেথডগুলো বর্ণনা করে যা এই অনুশীলনগুলির জন্য সহায়ক হবে!
আমরা আরও জটিল প্রোগ্রামগুলিতে প্রবেশ করছি যেখানে অপারেশন ব্যর্থ হতে পারে, তাই error handling নিয়ে আলোচনা করার জন্য এটি একটি উপযুক্ত সময়। আমরা এর পরেই তা করব!