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

Queue, Deque আর PriorityQueue — কে আগে সেবা পাবে

Queue, Deque and PriorityQueue — FIFO, LIFO and Priority Order in Java

Taught in Bangla, this lesson covers order-based collections in Java: FIFO, LIFO and priority order, the two method families of Queue, ArrayDeque's circular buffer, using Deque as a stack instead of the legacy Stack class, PriorityQueue as a binary heap with Comparator and top-K, and classic uses like bracket matching, BFS and sliding window.

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

  • FIFO, LIFO আর priority — তিনটি ক্রমের ধারণা, আর Java-তে প্রতিটির জন্য কোন interface
  • Queue-এর দুই পরিবারের method, NoSuchElementException বনাম null
  • ArrayDeque-এর ভেতরের circular buffer, Deque-কে stack হিসেবে ব্যবহার, আর Stack class কেন বাদ
  • PriorityQueue-এর binary heap — sift-up, sift-down, জটিলতা, Comparator আর top-K কৌশল
  • বন্ধনী মেলানো, BFS, sliding window আর priority অনুযায়ী কাজ সাজানো — queue আর stack-এর ক্লাসিক ব্যবহার

১ · অবস্থান নয়, ক্রম — কে আগে সেবা পাবে

পর্ব ৩০-এ List দেখেছেন: প্রতিটি জিনিসের একটি index আছে, আর get(5) দিয়ে যেকোনো ঘরে সরাসরি যাওয়া যায়। পর্ব ৩১-এ Set আর Map দেখেছেন: সেখানে প্রশ্ন “এটা আছে কি না” বা “এই key-এর মান কী”. কিন্তু অনেক বাস্তব সমস্যায় এই দুই প্রশ্নের কোনোটিই জরুরি নয়। জরুরি শুধু একটি কথা — এরপর কাকে সেবা দেব?

তিনটি পরিচিত দৃশ্য ভাবুন:

🏦 ব্যাংকের টোকেন

যিনি আগে টোকেন নিয়েছেন, কাউন্টার তাঁকেই আগে ডাকে। মাঝখান থেকে কাউকে ডাকা হয় না। এর নাম FIFO — First In, First Out.

Java-তে: Queue

🌐 ব্রাউজারের Back বোতাম

Back চাপলে ফেরেন সবশেষে দেখা পাতায়, প্রথম পাতায় নয়। থালার স্তূপের মতো — ওপরে যেটা শেষে রাখা, সেটাই আগে তোলা হয়। এর নাম LIFO — Last In, First Out.

Java-তে: Deque (stack হিসেবে)

🏥 হাসপাতালের জরুরি বিভাগ

কে আগে এসেছেন তা নয়, কার অবস্থা সবচেয়ে গুরুতর — তিনিই আগে ডাক্তার পান। হাত মচকানো রোগী এক ঘণ্টা আগে এলেও বুকে ব্যথার রোগী আগে যাবেন।

Java-তে: PriorityQueue

তিনটিতেই কাজ দুটি মাত্র: একটি জিনিস ঢোকানো, আর “পরের জন”-কে বের করা। পার্থক্য শুধু “পরের জন” কে, সেই নিয়মে। এমন নিয়ম মেনে চলা collection-কে বলে queue (সাধারণ অর্থে), আর LIFO-নিয়মের রূপটিকে বলে stack. মনে রাখবেন, আগের পর্বগুলোতে (পর্ব ০৫, ১২) যে call stack দেখেছিলেন, সেটাও ঠিক এই LIFO নিয়মে চলে — শেষে ডাকা method আগে শেষ হয়।

Java এই তিন ধারণার প্রতিটির জন্য একটি করে interface আর বাস্তবায়ন দেয়:

ধারণাinterfaceসাধারণ পছন্দকবে থেকে
FIFO — আগে এলে আগেQueue<E>ArrayDequeQueue: Java 5 (২০০৪)
দুই মাথায় ঢোকানো-বের করা; LIFODeque<E> (Queue-এর subinterface)ArrayDequeJava 6 (২০০৬)
সবচেয়ে ছোট/জরুরি আগেQueue<E>PriorityQueueJava 5 (২০০৪)
পুরোনো LIFO (এড়িয়ে চলুন)— (class)Stack → VectorJDK 1.0 (১৯৯৬)

পর্ব ২১-এর “interface-এর দিকে তাকিয়ে লিখুন” নীতিটি এখানেও খাটে: ভ্যারিয়েবলের টাইপ লিখুন Queue<String> বা Deque<String>, আর ডান দিকে বাস্তবায়ন — new ArrayDeque<>(). তাহলে কোড পড়েই বোঝা যায় আপনি কোন নিয়মে কাজ করছেন, আর পরে বাস্তবায়ন বদলালে বাকি কোড ছুঁতে হয় না।

২ · Queue interface — একই কাজের দুই পরিবারের method

একটি queue-এর তিনটি মৌলিক কাজ: শেষে ঢোকানো, সামনে থেকে বের করা, আর বের না করে সামনেরটা দেখা। Queue interface প্রতিটি কাজের জন্য দুটি method দেয়। পার্থক্য হলো, কাজটি সম্ভব না হলে — queue খালি, বা সীমিত আকারের queue ভরা — তারা কী করে:

কাজexception ছোড়েবিশেষ মান ফেরত দেয়ব্যর্থ হলে
শেষে ঢোকানোadd(e)offer(e)ভরা সীমিত queue-এ add → IllegalStateException: Queue full; offer → false
সামনে থেকে বের করাremove()poll()খালি queue-এ remove → NoSuchElementException; poll → null
সামনেরটা দেখা (বের না করে)element()peek()খালি queue-এ element → NoSuchElementException; peek → null

খালি টোকেন কাউন্টারে নিজে চালিয়ে দেখা যায়:

import java.util.ArrayDeque;
import java.util.Queue;

class Main {
    public static void main(String[] args) {
        Queue<String> counter = new ArrayDeque<>();
        System.out.println(counter.poll());
        System.out.println(counter.peek());
        counter.remove();
    }
}
null
null
Exception in thread "main" java.util.NoSuchElementException
	at java.base/java.util.ArrayDeque.removeFirst(ArrayDeque.java:361)
	at java.base/java.util.ArrayDeque.remove(ArrayDeque.java:522)
	at Main.main(Main.java:9)

poll() আর peek() চুপচাপ null দিল; remove() লাইন ৯-এ NoSuchElementException ছুড়ল। Stack trace-এর প্রথম দুই লাইন JDK-র ভেতরের — ArrayDeque.remove আসলে removeFirst-কে ডাকে, আর সেখানেই exception তৈরি হয়। আপনার ভুলের ঠিকানা হলো শেষ লাইন, Main.java:9.

কোন পরিবার কখন

  • খালি হওয়া স্বাভাবিক ঘটনা হলে — offer/poll/peek. কাজের লাইন শেষ হয়ে যাওয়া কোনো ভুল নয়; তাই while ((job = queue.poll()) != null) ধাঁচের লুপ পরিষ্কার।
  • খালি থাকা মানে প্রোগ্রামের যুক্তিতে ভুল হলে — remove/element. যেমন আপনি নিশ্চিত যে এখানে অন্তত একটি জিনিস থাকবেই; না থাকলে সেখানেই থেমে যাওয়া ভালো (fail-fast নীতি)।
  • সীমিত আকারের queue-এ (যেমন পরে দেখবেন ArrayBlockingQueue) offer ভরা থাকলে false দেয় আর add ছোড়ে IllegalStateException: Queue full — দুটিই যাচাই করা আউটপুট। ArrayDeque, PriorityQueue নিজে থেকে বড় হয়, তাই সেখানে offer সবসময় true.

দুই পরিবার মেশালে কী হয়। ধরুন কেউ ঢোকানোর জন্য offer লিখলেন, কিন্তু বের করার জন্য remove(), আর কোথাও poll()-এর ফল null কি না যাচাই করলেন না। খালি অবস্থায় একটি পথ exception দেবে, আরেকটি পথ null ছড়িয়ে দেবে — যা কয়েক লাইন পরে NullPointerException হয়ে ফাটবে, ভুলের জায়গা থেকে দূরে। আরও একটি কারণে null-এর এই সংকেত গুরুত্বপূর্ণ: ArrayDeque আর PriorityQueue-এ null রাখাই যায় না (add(null) → NullPointerException), তাই poll()-এর null নিশ্চিতভাবে মানে “খালি”. LinkedList-এ কিন্তু null রাখা যায় — সেখানে poll()-এর null মানে “খালি” নাকি “কেউ একটি null রেখেছিল”, আলাদা করা যায় না।

৩ · ArrayDeque — গোল করে ঘোরানো একটি array

একটি queue বানাতে প্রথমে মনে হয় সাধারণ array বা ArrayList নিই: শেষে যোগ করি, আর remove(0) দিয়ে সামনের জনকে বের করি। সমস্যা হলো remove(0)-এর পর বাকি সবাইকে এক ঘর করে বাঁয়ে সরাতে হয় — দশ লাখ জিনিসের তালিকায় প্রতিটি বের করায় দশ লাখ কপি। এটা O(n), যেখানে আমরা চাই O(1).

মেট্রো রেলের টিকিট কাউন্টারের লাইন ভাবুন — মানুষ সামনে এগোয়, কাউন্টার এক জায়গায় থাকে। এখন উল্টোটা ভাবুন: মানুষ যে যেখানে দাঁড়িয়ে আছে সেখানেই থাকবে, আর কাউন্টারের লোক একটি খাতায় দুটি আঙুল রাখবেন — একটি “এরপর কাকে ডাকব” (head), আরেকটি “নতুন কেউ এলে কোথায় দাঁড়াবে” (tail). কাউকে ডাকা মানে শুধু head আঙুল এক ঘর সরানো; কাউকে সরাতে হয় না। আর জায়গা শেষ হলে নতুন মানুষ দাঁড়ায় একদম শুরুর খালি জায়গাগুলোতে — লাইনটা যেন গোল হয়ে ঘুরে আসে।

এই কৌশলের নাম circular buffer (ring buffer). ArrayDeque ভেতরে ঠিক এটাই করে: একটি array আর দুটি index — head আর tail. index শেষ ঘর পেরোলে আবার ০-তে ফিরে আসে (গাণিতিকভাবে, array-এর দৈর্ঘ্য দিয়ে ভাগশেষ)। ধাপে ধাপে দেখুন (বোঝার সুবিধায় ৮ ঘরের array ধরে নিয়েছি; JDK-র আসল শুরুর আকার ও বাড়ার নিয়ম আলাদা):

1Deque<String> q = new ArrayDeque<>();2offer A, B, C, D, E3poll() × 3     // A, B, C বের4offer F, G, H5offer I, J6offer K        // array ভরে গেল
ভেতরের array আর দুই index
ঘর ০–৭
head
tail
size
  1. শুরু। একটি খালি array, আর head ও tail দুটিই ০-তে। head দেখায় সামনের জনকে, tail দেখায় পরের খালি ঘর।
  2. পাঁচজন এল। প্রতিটি offer শুধু tail-এর ঘরে লেখে আর tail এক বাড়ায় — O(1).
  3. তিনজনকে ডাকা হলো। poll সামনের ঘরের মান ফেরত দেয়, ঘরটি খালি করে, আর head এক বাড়ায়। D আর E এক ঘরও নড়েনি — ArrayList.remove(0)-এর সাথে এখানেই পার্থক্য।
  4. শেষ প্রান্তে পৌঁছানো। H বসল শেষ ঘরে। tail এখন ৮, যা array-এর বাইরে — তাই সে ঘুরে ০-তে চলে আসে। সামনের তিনটি ঘর তো খালিই পড়ে আছে।
  5. ঘুরে এসে লেখা। I আর J বসল ঘর ০ ও ১-এ। Array-তে দেখতে এলোমেলো, কিন্তু head থেকে শুরু করে ঘুরে পড়লে ক্রম ঠিক আছে: D E F G H I J. এটাই circular buffer.
  6. ভরে গেলে বড় হওয়া। K বসার পর tail আর head এক জায়গায় — আর জায়গা নেই। তখন একটি বড় array তৈরি হয়, আর জিনিসগুলো সঠিক ক্রমে কপি হয়। এই ব্যয়বহুল কাজটি মাঝে মাঝে হয়, তাই অনেক কাজের গড়ে প্রতিটি offer O(1) — ArrayList যেভাবে বেড়ে ওঠে, সেই একই amortized হিসাব।

সামনের দিকে ঢোকানোও (addFirst) একই কৌশলে চলে: head এক ঘর পিছোয়, আর ০-এর আগে গেলে ঘুরে শেষ ঘরে চলে যায়। তাই দুই মাথাতেই ঢোকানো আর বের করা O(1).

কেন LinkedList নয়

LinkedList-ও Queue আর Deque মানে, আর পুরোনো বই ও AI-এর কোডে Queue<Integer> q = new LinkedList<>(); খুব দেখা যায়। কাজ করে, কিন্তু Java-র নিজের documentation-এ ArrayDeque সম্পর্কে লেখা: “This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.” কারণ তিনটি:

  • প্রতিটি জিনিসের জন্য আলাদা node object. LinkedList-এ প্রতিটি উপাদান একটি node-এ থাকে, যার ভেতরে আগের ও পরের node-এর reference — প্রতিটি offer মানে নতুন object, আর garbage collector-এর জন্য বাড়তি কাজ।
  • মেমরিতে ছড়ানো। Node-গুলো heap-এর যেখানে-সেখানে থাকে; array-এর ঘরগুলো পাশাপাশি। আধুনিক CPU-এর cache পাশাপাশি থাকা ডেটা অনেক দ্রুত পড়ে।
  • null রাখা যায় — তাই অংশ ২-এর poll()-এর সংকেত অস্পষ্ট হয়ে যায়।

LinkedList-এর সত্যিকারের সুবিধা হলো iterator দিয়ে হাঁটার সময় মাঝখানে ঢোকানো-মোছা; queue বা stack-এ সেটা লাগে না। একটি ব্যতিক্রম মনে রাখুন: queue-তে যদি সত্যিই null রাখার দরকার হয় (খুব বিরল, আর সাধারণত নকশার দুর্বলতা), তখন ArrayDeque চলবে না।

৪ · Deque — দুই মাথার লাইন, আর stack হিসেবে ব্যবহার

Deque (উচ্চারণ “ডেক”, double-ended queue-এর সংক্ষেপ) এমন লাইন, যার সামনে আর পেছনে — দুই মাথাতেই ঢোকানো আর বের করা যায়। Queue-এর মতোই প্রতিটি কাজের দুই পরিবারের method আছে, এবার প্রতিটি মাথার জন্য আলাদা:

কাজসামনে (first)পেছনে (last)
ঢোকানোaddFirst / offerFirstaddLast / offerLast
বের করাremoveFirst / pollFirstremoveLast / pollLast
দেখাgetFirst / peekFirstgetLast / peekLast

প্রতিটি ঘরে বাঁয়েরটি exception ছোড়ে, ডানেরটি false/null দেয়। Queue হিসেবে ব্যবহার করলে offer মানে offerLast আর poll মানে pollFirst — পেছনে ঢোকে, সামনে দিয়ে বের হয়।

Stack হিসেবে — push, pop, peek

একটি মাথা ব্যবহার না করলেই Deque হয়ে যায় stack. এর জন্য Deque-এ তিনটি পরিচিত নামের method আছে, যারা সবাই সামনের মাথায় কাজ করে:

  • push(e) = addFirst(e) — ওপরে রাখা
  • pop() = removeFirst() — ওপরেরটা তোলা; খালি হলে NoSuchElementException
  • peek() = peekFirst() — ওপরেরটা দেখা; খালি হলে null

ফলে একটি ArrayDeque-এ ১, ২, ৩ push করে ছাপালে আসে [3, 2, 1] — ওপরের জিনিস প্রথমে। কথাটা মনে রাখুন; অংশ ৫-এ এটার সাথে পুরোনো Stack-এর আউটপুট মেলাব।

ব্রাউজারের Back আর Forward — দুটি stack

ব্রাউজার Back আর Forward দুটোই সামলায় দুটি stack দিয়ে। নতুন পাতায় গেলে বর্তমান পাতা back-এ push হয়, আর forward খালি হয়ে যায় (নতুন পথে গেলে পুরোনো “সামনের” ইতিহাস অর্থহীন)। Back চাপলে বর্তমান পাতা forward-এ push হয়, আর back থেকে pop করা পাতা হয় নতুন বর্তমান। Forward ঠিক উল্টো। google.com → abcltech.com → abcltech.com/java ঘুরে একবার Back চাপার পর যাচাই করা অবস্থা: বর্তমান abcltech.com, back = [google.com], forward = [abcltech.com/java]. টেক্সট এডিটরের Undo/Redo-ও হুবহু এই নকশা: প্রতিটি বদল undo-stack-এ যায়, Undo চাপলে সেটা redo-stack-এ সরে, আর নতুন কিছু লিখলে redo-stack খালি হয়।

বন্ধনী মেলানো — stack-এর ক্লাসিক সমস্যা

{[()]} ঠিক আছে, ([)] ভুল — কীভাবে যাচাই করবেন? নিয়মটা হলো: যে বন্ধনী সবশেষে খোলা হয়েছে, সেটাকেই সবার আগে বন্ধ করতে হবে। “সবশেষে এসেছে, সবার আগে যাবে” — হুবহু LIFO. খোলা বন্ধনী পেলে push; বন্ধ বন্ধনী পেলে pop করে মেলান। Compiler আর IDE আপনার কোডের বন্ধনী এভাবেই মেলায়। "{[()]}("-এর ওপর ধাপে ধাপে:

1for (char c : s.toCharArray())2    if (খোলা) stack.push(c);3    else if (stack.isEmpty()) return false;4    else if (pop() জোড়া নয়) return false;5return stack.isEmpty();
stack (বাঁয়ে নিচ, ডানে ওপর)
পড়া অক্ষর
stack
pop হলো
  1. { খোলা। খোলা বন্ধনী মানে একটি “প্রতিশ্রুতি” — পরে এর জোড়া আসতে হবে। Stack-এ রাখা হলো।
  2. [ খোলা। আরেকটি প্রতিশ্রুতি, আগেরটার ওপরে।
  3. ( খোলা। এখন ওপরে ( — অর্থাৎ পরের বন্ধ বন্ধনীটি অবশ্যই ) হতে হবে।
  4. ) বন্ধ। Pop করে পাওয়া গেল ( — জোড়া মিলেছে। যদি এখানে ] আসত (যেমন ([)]-এ), pop দিত (, জোড়া মিলত না, ফল সাথে সাথে false.
  5. ] বন্ধ। Pop দিল [ — মিলেছে।
  6. } বন্ধ। Pop দিল { — মিলেছে। Stack খালি; "{[()]}" পর্যন্ত লেখা নিখুঁত।
  7. ( খোলা। শেষ অক্ষরটি আবার একটি খোলা বন্ধনী — push.
  8. শেষে যাচাই। লেখা শেষ, কিন্তু stack খালি নয় — একটি প্রতিশ্রুতি পূরণ হয়নি। ফল false. তিন ধরনের ভুল তিন জায়গায় ধরা পড়ে: জোড়া না মেলা (লাইন ৪), খোলার আগেই বন্ধ (লাইন ৩, যেমন "())"), আর খোলা রয়ে যাওয়া (লাইন ৫)।

এবার চালিয়ে দেখুন — একই ArrayDeque class দিয়ে একটি টোকেন কাউন্টার (queue) আর বন্ধনী যাচাই (stack):

import java.util.*;

class Main {
    static boolean balanced(String s) {
        Deque<Character> stack = new ArrayDeque<>();
        Map<Character, Character> pair = Map.of(')', '(', ']', '[', '}', '{');
        for (char c : s.toCharArray()) {
            if (c == '(' || c == '[' || c == '{') stack.push(c);
            else if (pair.containsKey(c)) {
                if (stack.isEmpty()) return false;       // খোলার আগেই বন্ধ
                char open = stack.pop();
                if (open != pair.get(c)) return false;   // জোড়া মেলেনি
            }
        }
        return stack.isEmpty();                          // খোলা কিছু বাকি?
    }
    public static void main(String[] args) {
        Deque<String> counter = new ArrayDeque<>();      // টোকেন কাউন্টার — FIFO
        counter.offer("T-101"); counter.offer("T-102"); counter.offer("T-103");
        System.out.println("ডাক: " + counter.poll() + ", লাইনে বাকি " + counter);
        System.out.println("পরের জন: " + counter.peek());
        for (String s : new String[]{"{[()]}", "{[()]}(", "([)]", "())"})
            System.out.println(s + " -> " + balanced(s));
    }
}

আউটপুট ছয় লাইন: ডাক: T-101, লাইনে বাকি [T-102, T-103], পরের জন: T-102, তারপর {[()]} -> true, {[()]}( -> false, ([)] -> false আর ()) -> false. টোকেন কাউন্টারে যে আগে এসেছে (T-101) সে আগে গেল, আর peek পরের জনকে দেখাল কিন্তু সরাল না। balanced-এ একটি সূক্ষ্ম বিষয় খেয়াল করুন: char open = stack.pop(); — pop করা Character-কে প্রথমে char-এ নামিয়ে তারপর != দিয়ে তুলনা। দুটি Character object সরাসরি != দিয়ে তুলনা করলে reference তুলনা হতো (পর্ব ২৮-এর Integer-এর ফাঁদ)। Map.of বন্ধ থেকে খোলা বন্ধনীর জোড়া রাখে; অন্য অক্ষর (অক্ষর, সংখ্যা) দুই শর্তের কোনোটিতে পড়ে না, তাই উপেক্ষিত হয়।

৫ · java.util.Stack — কেন আর ব্যবহার করবেন না

Java-তে Stack নামে একটি class আছে ১৯৯৬ সাল থেকে, আর নাম দেখে সেটাই প্রথমে মনে আসে। সমস্যা হলো এর জন্ম: Stack হলো Vector-এর subclass. পর্ব ১৯-এ inheritance শেখার সময় যে সাবধানবাণী শুনেছিলেন — “is-a সত্যি না হলে extends নয়” — এটা তার পাঠ্যপুস্তকীয় উদাহরণ। একটি stack কি সত্যিই একটি vector (যেকোনো index-এ পড়া-লেখা যায় এমন তালিকা)? না। কিন্তু extends করায় Vector-এর সব public method Stack-এ চলে এসেছে:

Stack<Integer> st = new Stack<>();
st.push(1); st.push(2); st.push(3);
System.out.println(st + " peek=" + st.peek());       // [1, 2, 3] peek=3

st.add(0, 99);                                       // নিচে ঢুকিয়ে দেওয়া!
System.out.println(st + " get(1)=" + st.get(1));     // [99, 1, 2, 3] get(1)=1

Deque<Integer> ds = new ArrayDeque<>();
ds.push(1); ds.push(2); ds.push(3);
System.out.println(ds + " peek=" + ds.peek());       // [3, 2, 1] peek=3

মন্তব্যে লেখা আউটপুটগুলো চালিয়ে যাচাই করা। তিনটি সমস্যা দেখা যাচ্ছে:

  1. LIFO নিয়ম ভাঙা যায়। add(0, 99) stack-এর একদম নিচে একটি জিনিস ঢুকিয়ে দিল, get(1) মাঝখান থেকে পড়ল। Stack-এর পুরো অর্থই হলো “শুধু ওপর দিয়ে” — সেই নিশ্চয়তা নেই। পর্ব ১৭-এর encapsulation-এর ভাষায়, ভেতরের গঠন বাইরে খুলে রাখা।
  2. ছাপা ও লুপের ক্রম উল্টো। একই তিনটি push-এর পর Stack দেখায় [1, 2, 3] (নিচ থেকে ওপর), ArrayDeque দেখায় [3, 2, 1] (ওপর থেকে নিচ)। for-each লুপেও একই পার্থক্য। Stack থেকে Deque-এ কোড সরানোর সময় এটা নীরব bug-এর উৎস।
  3. অকারণ synchronized. Vector-এর প্রায় সব method synchronized — একসাথে একাধিক thread যাতে না ঢোকে, তার জন্য প্রতিটি ডাকে একটি তালা (lock) নেওয়া হয়। এক thread-এর প্রোগ্রামে এটা শুধু খরচ। আর অনেক thread-এর জন্যও এটা যথেষ্ট নয়: “খালি কি না দেখে তারপর pop” — দুটি আলাদা ডাকের মাঝে অন্য thread ঢুকে পড়তে পারে। Thread-এর সমস্যা আর তার আসল সমাধান পর্ব ৪০-এ।

Java-র নিজের documentation-ও স্পষ্ট: Stack-এর পাতায় লেখা, “A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.” Stack মুছে ফেলা হয়নি শুধু পুরোনো কোডের সামঞ্জস্যের জন্য। খালি Stack-এ pop() ছোড়ে আলাদা একটি exception, java.util.EmptyStackException — Deque-এর NoSuchElementException নয়। এটাও পুরোনো যুগের চিহ্ন।

মূল ধারণা

নতুন কোডে stack লাগলে লিখুন Deque<T> stack = new ArrayDeque<>(); আর ব্যবহার করুন শুধু push, pop, peek, isEmpty. Queue লাগলে Queue<T> q = new ArrayDeque<>(); আর offer, poll, peek. ভ্যারিয়েবলের টাইপ যত সরু, ভুল method ডাকার সুযোগ তত কম।

৬ · PriorityQueue — সবচেয়ে জরুরি জন আগে, binary heap দিয়ে

জরুরি বিভাগের সমস্যায় ফিরি। প্রতিবার “সবচেয়ে গুরুতর রোগী কে” জানতে হবে, আর নতুন রোগী যেকোনো সময় আসতে পারেন। দুটি সহজ উপায় আর তাদের দাম:

  • এলোমেলো তালিকা রাখুন: ঢোকানো O(1), কিন্তু প্রতিবার সবচেয়ে জরুরিকে খুঁজতে পুরো তালিকা দেখা — O(n).
  • সবসময় সাজানো তালিকা রাখুন: বের করা O(1), কিন্তু প্রতিটি নতুন রোগীকে সঠিক জায়গায় বসাতে বাকিদের সরানো — O(n).

মাঝামাঝি একটি চমৎকার কাঠামো আছে, যা দুটি কাজই O(log n)-এ করে: binary heap. PriorityQueue ভেতরে এটাই। Heap হলো একটি গাছ (tree), যার দুটি নিয়ম:

  1. আকৃতির নিয়ম: প্রতিটি স্তর বাঁ থেকে ডানে পুরো ভরা থাকে, শুধু শেষ স্তর আংশিক হতে পারে। তাই গাছটিকে কোনো pointer ছাড়াই একটি array-তে রাখা যায়: ঘর i-এর সন্তান 2i+1 আর 2i+2, আর অভিভাবক (i-1)/2.
  2. ক্রমের নিয়ম: প্রতিটি অভিভাবক তার সন্তানদের চেয়ে ছোট বা সমান। ফলে সবচেয়ে ছোট জিনিস সবসময় শিকড়ে (root), অর্থাৎ ঘর ০-তে। এমন heap-কে বলে min-heap.

খেয়াল করুন, নিয়মটা শুধু অভিভাবক আর সন্তানের মধ্যে — দুই ভাইবোনের মধ্যে, বা গাছের বাঁ আর ডান অংশের মধ্যে কোনো ক্রম নেই। তাই heap “আংশিক সাজানো”: সবচেয়ে ছোটটা নিশ্চিতভাবে জানা, বাকিরা মোটামুটি এলোমেলো। ৮, ৫, ৩, ৯, ১ ঢুকিয়ে, তারপর একটি poll করে দেখুন — প্রতিটি ধাপের array চালিয়ে যাচাই করা:

ধাপ ১offer 8, offer 5
    8          5 এল ঘর ১-এ, অভিভাবক 8
   /           5 < 8 → অদলবদল
  5                  5
                    /
                   8
array: [5, 8]
ধাপ ২offer 3
    5             3 এল ঘর ২-এ, অভিভাবক ঘর ০ (5)
   / \            3 < 5 → অদলবদল
  8   3                3
                      / \
                     8   5
array: [3, 8, 5]
ধাপ ৩offer 9
      3           9 এল ঘর ৩-এ, অভিভাবক ঘর ১ (8)
     / \          9 > 8 → যেখানে আছে সেখানেই
    8   5
   /
  9
array: [3, 8, 5, 9]
ধাপ ৪offer 1 — দুই ধাপ ওপরে ওঠা
      3             1 এল ঘর ৪-এ, অভিভাবক 8 → অদলবদল
     / \            এবার অভিভাবক 3 → আবার অদলবদল
    8   5                 1
   / \                   / \
  9   1                 3   5
                       / \
                      9   8
array: [1, 3, 5, 9, 8]
ধাপ ৫poll() — শিকড় বের, শেষ জন ওপরে
poll() ফেরত দেবে 1
শেষ ঘরের 8 উঠে এল শিকড়ে:
      8
     / \
    3   5
   /
  9
ক্রমের নিয়ম ভাঙা — 8 তার সন্তানদের চেয়ে বড়
ধাপ ৬sift-down
8-এর সন্তান 3 আর 5 — ছোটটি 3 → অদলবদল
      3
     / \
    8   5
   /
  9
8-এর সন্তান এখন শুধু 9 → 8 < 9, থামো
array: [3, 8, 5, 9]
  1. নতুন জিনিস সবসময় শেষ খালি ঘরে। তাতে আকৃতির নিয়ম ঠিক থাকে। তারপর সে তার অভিভাবকের সাথে তুলনা করে; ছোট হলে জায়গা বদল। এই ওপরে ওঠার নাম sift-up.
  2. আবার sift-up. 3 শিকড়ের 5-এর চেয়ে ছোট, তাই শিকড়ে উঠল। লক্ষ করুন, 8 আর 5-এর মধ্যে কোনো ক্রম নেই — ভাইবোনের মধ্যে নিয়ম খাটে না।
  3. কখনো ওঠার দরকারই নেই। 9 তার অভিভাবক 8-এর চেয়ে বড়, তাই একটি তুলনাতেই কাজ শেষ। Array [3, 8, 5, 9] সাজানো নয়, কিন্তু heap-এর নিয়ম মানে।
  4. সবচেয়ে বেশি কাজ: শিকড় পর্যন্ত। 1 দুই ধাপে শিকড়ে উঠল। গাছের উচ্চতা log₂ n-এর কাছাকাছি, তাই সবচেয়ে খারাপ ক্ষেত্রেও offer O(log n) — দশ লাখ জিনিসে মোটামুটি ২০টি তুলনা।
  5. poll: শিকড় বের করো। ফেরত যাবে শিকড়ের 1. ফাঁকা শিকড় ভরাতে শেষ ঘরের জিনিস (8) তুলে আনা হয় — আকৃতির নিয়ম বাঁচে, কিন্তু ক্রমের নিয়ম ভাঙে।
  6. নিচে নামা — sift-down. 8 তার দুই সন্তানের মধ্যে ছোটটির (3) সাথে জায়গা বদলায়, যতক্ষণ না সে দুই সন্তানের চেয়ে ছোট হয় বা পাতায় (leaf) পৌঁছায়। এটাও O(log n). ছোট সন্তানটিই কেন? তাহলে যে ওপরে উঠবে, সে অন্য সন্তানের চেয়েও ছোট — নিয়ম বজায় থাকে।
কাজজটিলতাকেন
offer(e)O(log n)শেষে বসিয়ে sift-up
poll()O(log n)শিকড় বের, শেষ জন ওপরে, sift-down
peek()O(1)শুধু ঘর ০ পড়া
contains(o), remove(o)O(n)heap-এ খোঁজার শর্টকাট নেই, পুরো array দেখতে হয়
new PriorityQueue<>(collection)O(n)একে একে offer নয়, একসাথে heapify (ভাবনার প্রশ্ন ১)

এখানে O(log n), O(n) — এই চিহ্নগুলো বলে তালিকা বড় হলে কাজের সময় কীভাবে বাড়ে; পর্ব ৫৪-এ অ্যালগরিদম বিশ্লেষণে এর বিস্তারিত।

ক্রম কে ঠিক করে — natural ordering বনাম Comparator

কিছু না বললে PriorityQueue জিনিসগুলোর natural ordering ব্যবহার করে — সংখ্যায় ছোট থেকে বড়, String-এ অভিধানের ক্রম। অর্থাৎ ডিফল্ট হলো min-heap: poll সবচেয়ে ছোটটি দেয়। সবচেয়ে বড়টি আগে চাইলে (max-heap) constructor-এ একটি Comparator দিন: new PriorityQueue<Integer>(Comparator.reverseOrder()). Comparator কীভাবে লেখে আর জোড়া দেয়, পুরো ছবি পর্ব ৩৩-এ।

নিজের তৈরি টাইপ — যেমন একটি Patient record — রাখতে চাইলে সাবধান। Record নিজে থেকে Comparable নয়, আর Comparator না দিলে প্রথম offer-এই প্রোগ্রাম ভাঙে:

import java.util.PriorityQueue;

record Patient(String name, int level) { }

class Main {
    public static void main(String[] args) {
        PriorityQueue<Patient> er = new PriorityQueue<>();
        er.offer(new Patient("Karim", 3));
    }
}
Exception in thread "main" java.lang.ClassCastException: class Patient cannot be cast to class java.lang.Comparable (Patient is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
	at java.base/java.util.PriorityQueue.siftUpComparable(PriorityQueue.java:643)
	at java.base/java.util.PriorityQueue.siftUp(PriorityQueue.java:639)
	at java.base/java.util.PriorityQueue.offer(PriorityQueue.java:330)
	at Main.main(Main.java:8)

Code compile হলো, কারণ PriorityQueue<E>-এর type parameter-এ কোনো bound নেই (পর্ব ২৮) — Comparator দিলে যেকোনো টাইপই চলে, তাই compiler আগে থেকে আপত্তি করতে পারে না। চলার সময় Comparator না পেয়ে PriorityQueue জিনিসটিকে Comparable-এ cast করতে চেষ্টা করে, আর stack trace-এ সেই siftUpComparable দেখা যাচ্ছে। একটিমাত্র রোগী রাখতেও এটা ঘটে। সমাধান: Comparator দিন, অথবা record-কে Comparable<Patient> বানান।

সবচেয়ে বড় ফাঁদ — ছাপা বা লুপ চালালে সাজানো ক্রম নয়

৫, ১, ৪, ২, ৩ একটি PriorityQueue-এ offer করে System.out.println(pq) চালালে আসে [1, 2, 4, 5, 3]. for-each লুপ চালালেও ঠিক এই ক্রম। কিন্তু খালি না হওয়া পর্যন্ত বারবার poll() করলে আসে 1 2 3 4 5. তিনটিই চালিয়ে যাচাই করা।

কারণ toString() আর iterator শুধু ভেতরের array-টি ঘর ০ থেকে শেষ পর্যন্ত পড়ে — আর সেটা heap-এর ক্রম, সাজানো ক্রম নয়। একই offer-এর ক্রমে ফল প্রতিবার একই হয় (এলোমেলো নয়), কিন্তু সাজানো দেখালে সেটা কাকতালীয়। Java-র documentation সতর্ক করে যে PriorityQueue-এর iterator কোনো নির্দিষ্ট ক্রমের নিশ্চয়তা দেয় না, আর সাজানো ক্রমে হাঁটতে চাইলে Arrays.sort(pq.toArray()) ভাবতে বলে। নিয়ম মনে রাখুন: PriorityQueue থেকে সাজানো ক্রম পাওয়ার একমাত্র উপায় poll.

আরেকটি সূক্ষ্ম বিষয়: সমান priority-র দুজনের মধ্যে কে আগে আসবে, PriorityQueue তার নিশ্চয়তা দেয় না — যে আগে এসেছে সে আগে যাবে, এমন কোনো কথা নেই (heap “stable” নয়)। নির্দিষ্ট ক্রম চাইলে Comparator-এ দ্বিতীয় একটি মানদণ্ড যোগ করুন — নাম, বা আসার ক্রমিক নম্বর।

জরুরি বিভাগ আর শীর্ষ ৩ — একসাথে চালিয়ে দেখুন

নিচের প্রোগ্রামে দুটি ব্যবহার: জরুরি বিভাগের রোগী (level ১ সবচেয়ে জরুরি, সমান হলে নামের ক্রম), আর পরীক্ষার নম্বর থেকে শীর্ষ তিনজন বের করা। দ্বিতীয়টি একটি বিখ্যাত কৌশল — top-K: সবচেয়ে বড় K-টি চাইলে আকার K-এর একটি min-heap রাখুন। প্রতিটি নতুন সংখ্যা ঢোকান; আকার K ছাড়ালে সবচেয়ে ছোটটি (শিকড়) ফেলে দিন। শেষে heap-এ থাকে সবচেয়ে বড় K-টি।

import java.util.*;

record Patient(String name, int level) { }      // level 1 = সবচেয়ে জরুরি

class Main {
    public static void main(String[] args) {
        PriorityQueue<Patient> er = new PriorityQueue<>(
            Comparator.comparingInt(Patient::level).thenComparing(Patient::name));
        er.offer(new Patient("Karim", 3));
        er.offer(new Patient("Nusrat", 1));
        er.offer(new Patient("Rahim", 2));
        er.offer(new Patient("Ayesha", 1));
        while (!er.isEmpty()) System.out.println(er.poll());
        int[] scores = {72, 95, 61, 88, 79, 99, 54};
        PriorityQueue<Integer> top = new PriorityQueue<>();   // min-heap
        for (int s : scores) {
            top.offer(s);
            if (top.size() > 3) top.poll();                   // সবচেয়ে ছোটটি বাদ
        }
        System.out.println("heap-এর ভেতরের ক্রম: " + top);
        List<Integer> best = new ArrayList<>(top);
        best.sort(Comparator.reverseOrder());
        System.out.println("শীর্ষ ৩: " + best);
    }
}

আউটপুট: Patient[name=Ayesha, level=1], Patient[name=Nusrat, level=1], Patient[name=Rahim, level=2], Patient[name=Karim, level=3], তারপর heap-এর ভেতরের ক্রম: [88, 99, 95] আর শীর্ষ ৩: [99, 95, 88]. Nusrat আগে এলেও Ayesha আগে গেলেন — দুজনেরই level ১, আর tie-breaker নামের ক্রম। Karim সবার আগে এসেও সবার শেষে, কারণ তাঁর অবস্থা সবচেয়ে কম গুরুতর। Comparator.comparingInt(Patient::level).thenComparing(Patient::name) পড়ুন “level দিয়ে তুলনা, সমান হলে নাম দিয়ে” — এই লেখার ধরন (:: method reference) পর্ব ৩৩ ও ৩৪-এ বিস্তারিত। শীর্ষ-৩ অংশে heap কখনো চারটির বেশি জিনিস ধরেনি; সাত লাখ নম্বর হলেও মেমরি লাগত মাত্র তিনটির, আর সময় O(n log K). ছাপা heap [88, 99, 95] সাজানো নয় — ওপরের ফাঁদটি নিজের চোখে দেখুন — তাই শেষে একটি তালিকায় নিয়ে বড় থেকে ছোট সাজানো হয়েছে।

৭ · ক্লাসিক অ্যালগরিদম — BFS, sliding window আর কাজের সূচি

BFS — গোলকধাঁধায় সবচেয়ে ছোট পথ

একটি গ্রিড ভাবুন: S থেকে G-তে যেতে হবে, # দেয়াল, প্রতি ধাপে ওপর-নিচ-ডান-বাঁ এক ঘর। সবচেয়ে কম ধাপ কত? BFS (Breadth-First Search) এর উত্তর দেয় একটি queue দিয়ে: শুরুর ঘর queue-তে রাখুন; তারপর বারবার সামনের ঘর বের করুন আর তার যে প্রতিবেশীদের এখনো দেখা হয়নি, তাদের দূরত্ব “আমার দূরত্ব + ১” লিখে queue-এর পেছনে রাখুন। FIFO-র কারণে দূরত্ব ১-এর সব ঘর দূরত্ব ২-এর আগে বের হয়, দূরত্ব ২-এর সব ঘর দূরত্ব ৩-এর আগে — পুকুরে ঢিল ছুড়লে যেমন ঢেউ স্তরে স্তরে ছড়ায়। তাই কোনো ঘরে প্রথমবার পৌঁছানো মানেই সবচেয়ে ছোট পথে পৌঁছানো।

দূরত্ব ০শুরু
0 . # .
. # . .
. . . #
# . . G
queue: [(0,0)]
দূরত্ব ১(0,0)-এর প্রতিবেশী
0 1 # .
1 # . .
. . . #
# . . G
queue: [(1,0), (0,1)]
দূরত্ব ২ ও ৩একটাই পথ খোলা
0 1 # .
1 # . .
2 3 . #
# . . G
(0,1)-এর ডানে-নিচে দেয়াল — সেখান থেকে কেউ যোগ হলো না
queue: [(2,1)]
দূরত্ব ৪দুই দিকে ভাগ
0 1 # .
1 # . .
2 3 4 #
# 4 . G
queue: [(3,1), (2,2)]
দূরত্ব ৫আগে দেখা ঘর বাদ
0 1 # .
1 # 5 .
2 3 4 #
# 4 5 G
(3,2)-তে দুই দিক থেকেই পৌঁছানো যায়,
কিন্তু প্রথমবারই দূরত্ব লেখা হয় — দ্বিতীয়বার বাদ
queue: [(3,2), (1,2)]
দূরত্ব ৬লক্ষ্যে পৌঁছানো
0 1 # .
1 # 5 6
2 3 4 #
# 4 5 6
G পর্যন্ত সবচেয়ে ছোট পথ = 6 ধাপ
  1. শুরু। S-এর দূরত্ব ০; queue-তে শুধু সে। বাকি সব ঘর “এখনো দেখা হয়নি”.
  2. প্রথম ঢেউ। (0,0) বের হলো; তার ডান আর নিচের ঘর খোলা — দুজনের দূরত্ব ১, দুজনেই queue-এর পেছনে।
  3. সরু পথ। (0,1)-এর চারপাশে হয় দেয়াল নয়তো আগে দেখা ঘর, তাই সে কাউকে যোগ করল না। (1,0) থেকে নিচে (2,0) — দূরত্ব ২; সেখান থেকে ডানে (2,1) — দূরত্ব ৩.
  4. দুই দিকে ভাগ। (2,1) থেকে নিচে (3,1) আর ডানে (2,2) — দুজনেরই দূরত্ব ৪.
  5. দেখা ঘর আর নয়। (3,2)-তে দুজনেই পৌঁছাতে পারে; যে আগে queue থেকে বের হয় সে দূরত্ব লেখে, পরে যে আসে সে ঘরটি “দেখা” পেয়ে বাদ দেয়। এই চিহ্ন না রাখলে একই ঘর বারবার queue-তে ঢুকত।
  6. লক্ষ্য। (3,2) থেকে G-তে পৌঁছানো — দূরত্ব ৬. FIFO নিশ্চয়তা দেয় যে দূরত্ব ৫-এর আগে কেউ G-তে পৌঁছাতে পারত না। প্রোগ্রামেও উত্তর 6 আসে (যাচাই করা)।

কোডের মূল অংশ (একটি record Cell(int r, int c) ধরে নিয়ে):

Deque<Cell> q = new ArrayDeque<>();
q.offer(start);
dist[start.r()][start.c()] = 0;                // বাকি সব ঘরে -1 (দেখা হয়নি)
while (!q.isEmpty()) {
    Cell cur = q.poll();
    if (cur.equals(goal)) return dist[cur.r()][cur.c()];
    for (int[] d : dirs) {                     // ওপর, নিচ, বাঁ, ডান
        int nr = cur.r() + d[0], nc = cur.c() + d[1];
        if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue;
        if (g[nr][nc] == '#' || dist[nr][nc] != -1) continue;
        dist[nr][nc] = dist[cur.r()][cur.c()] + 1;
        q.offer(new Cell(nr, nc));
    }
}
return -1;                                     // পৌঁছানো যায় না

একই কৌশল সামাজিক নেটওয়ার্কে “কয়জন বন্ধুর মাধ্যমে পরিচয়”, মেট্রো রুটে সবচেয়ে কম স্টেশন বদল, বা ওয়েব crawler-এ লিংক ধরে ধরে পাতা খোঁজায় কাজে লাগে। Graph আর BFS-এর পুরো রূপ (আর queue-এর বদলে stack দিলে যে DFS হয়) পর্ব ৫৪-এ।

Sliding window — Deque-এর দুই মাথাই কাজে

একটি সংখ্যার তালিকায় প্রতিটি পরপর K-টি সংখ্যার “জানালা”-র সর্বোচ্চ মান চাই — যেমন প্রতি ৩ দিনের সর্বোচ্চ তাপমাত্রা। প্রতিটি জানালায় নতুন করে খুঁজলে O(n·K). একটি Deque দিয়ে O(n)-এ হয়: Deque-এ index রাখুন এমনভাবে যে তাদের মান সামনে থেকে পেছনে কমতে থাকে। নতুন সংখ্যা এলে পেছন থেকে তার চেয়ে ছোটদের ফেলে দিন (তারা আর কখনো সর্বোচ্চ হবে না), আর সামনের index জানালার বাইরে চলে গেলে সামনে থেকে ফেলুন। জানালার সর্বোচ্চ সবসময় সামনে।

Deque<Integer> dq = new ArrayDeque<>();       // index, মান কমতে থাকা ক্রমে
for (int i = 0; i < a.length; i++) {
    if (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst();     // জানালার বাইরে
    while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast();  // অকেজো ছোটরা
    dq.offerLast(i);
    if (i >= k - 1) result.add(a[dq.peekFirst()]);
}

a = {3, 1, 4, 1, 5, 9, 2, 6}, k = 3 হলে result হয় [4, 4, 5, 9, 9, 9] (চালিয়ে যাচাই করা)। প্রতিটি index Deque-এ একবার ঢোকে আর একবারই বের হয়, তাই মোট কাজ O(n). এটা ইন্টারভিউর জনপ্রিয় প্রশ্ন; বিস্তারিত অনুশীলন পর্ব ৫৯-এ।

Priority অনুযায়ী কাজের সূচি

সার্ভারে রাতের কাজগুলো ভাবুন: ডেটাবেস ব্যাকআপ, রিপোর্ট পাঠানো, পুরোনো লগ মোছা। প্রতিটি কাজের একটি priority আর আসার সময় আছে। একটি PriorityQueue<Task>, যার Comparator আগে priority আর তারপর আসার ক্রমিক নম্বর দেখে — এটাই একটি ছোট task scheduler-এর হৃদয়। একই ধারণায় Java-র ScheduledThreadPoolExecutor ভেতরে “কোন কাজের সময় সবার আগে আসবে” সেই ক্রমে কাজ রাখে (একটি heap-ভিত্তিক queue-তে) — পর্ব ৪১-এ এর ব্যবহার দেখবেন।

এক নজরে: BlockingQueue আর producer-consumer

এ পর্যন্ত সব queue এক thread-এর জন্য। বাস্তবে প্রায়ই একদল thread কাজ তৈরি করে (যেমন ওয়েব থেকে আসা অর্ডার) আর আরেকদল সেগুলো সামলায় — এই নকশার নাম producer-consumer. মাঝখানের লাইনটি হতে হবে thread-safe, আর খালি থাকলে consumer-কে অপেক্ষা করাতে হবে, ভরা থাকলে producer-কে। java.util.concurrent-এর BlockingQueue (Java 5 থেকে) ঠিক এটাই দেয়: Queue-এর দুই পরিবারের সাথে তৃতীয় একটি — put (জায়গা না হওয়া পর্যন্ত অপেক্ষা) আর take (কিছু না আসা পর্যন্ত অপেক্ষা)। ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue এর বাস্তবায়ন। Thread কী, আর কেন সাধারণ ArrayDeque দুই thread-এর মাঝে রাখা বিপজ্জনক, সেটা পর্ব ৪০-এ; BlockingQueue দিয়ে আসল producer-consumer পর্ব ৪১-এ।

৮ · কোনটা কখন — সিদ্ধান্তের টেবিল

আপনার দরকারলিখুনমনে রাখুন
আগে এলে আগে (টোকেন, কাজের লাইন, BFS)Queue<T> q = new ArrayDeque<>()offer/poll/peek; null নয়
শেষে এলে আগে (undo, back, বন্ধনী, DFS)Deque<T> st = new ArrayDeque<>()push/pop/peek; Stack নয়
দুই মাথাতেই কাজ (sliding window)Deque<T> + ArrayDequeofferFirst/Last, pollFirst/Last
সবচেয়ে ছোট/বড়/জরুরি আগেPriorityQueue<T> (+ Comparator)সাজানো ক্রম শুধু poll দিয়ে; tie-breaker দিন
শীর্ষ K-টিআকার K-এর heapবড় K-টি চাইলে min-heap, ছোট K-টি চাইলে max-heap
সব সময় সাজানো, যেকোনো জিনিস খোঁজা/মোছাTreeSet/TreeMap (পর্ব ৩১)সব কাজ O(log n), কিন্তু ডুপ্লিকেট রাখে না
অনেক thread-এর মাঝে লাইনBlockingQueueপর্ব ৪০–৪১
C/C++/Python জানলে

C-তে queue আর stack নিজের হাতে বানাতে হয় — array আর দুটি index দিয়ে circular buffer, ঠিক অংশ ৩-এর মতো (C-র স্ট্যাক ও কিউ, হিপ ও প্রায়োরিটি কিউ)। C++-এর std::priority_queue ডিফল্টে max-heap, Java-র PriorityQueue ডিফল্টে min-heap — এক ভাষা থেকে আরেকে যাওয়ার সময় এটা পরিচিত ভুল; আর C++-এ pop() কিছু ফেরত দেয় না, Java-র poll() দেয় (C++-এর deque আর sequence container). Python-এর collections.deque হলো Java-র ArrayDeque-এর সমতুল্য, আর heapq একটি সাধারণ list-এর ওপর min-heap চালায় (অ্যালগরিদম ও কনটেস্টে Python).

পরীক্ষা ও ইন্টারভিউতে
  • Queue-এর poll() আর remove()-এর পার্থক্য? — খালি হলে poll দেয় null, remove ছোড়ে NoSuchElementException; একইভাবে peek/element আর offer/add.
  • Java-তে stack-এর জন্য কোন class? — ArrayDeque (Deque interface দিয়ে); Stack হলো Vector-এর subclass, synchronized আর LIFO নিয়ম ভাঙতে দেয়।
  • PriorityQueue ভেতরে কী, আর জটিলতা? — array-তে রাখা binary min-heap; offer/poll O(log n), peek O(1), contains O(n).
  • PriorityQueue ছাপালে সাজানো দেখায় না কেন? — toString আর iterator ভেতরের heap array পড়ে; সাজানো ক্রম শুধু বারবার poll করলে।
  • দশ লাখ সংখ্যা থেকে সবচেয়ে বড় ১০টি? — আকার ১০-এর min-heap; আকার ছাড়ালে poll; সময় O(n log K), মেমরি O(K).

৯ · AI-এর যুগে — Stack, LinkedList আর “সাজানো” PriorityQueue

Queue আর stack-এর কোড AI খুব দ্রুত লেখে, আর প্রায়ই কাজও করে — ছোট পরীক্ষায়। সমস্যাগুলো লুকিয়ে থাকে পুরোনো অভ্যাসে, আর একটি ভুল ধারণায়: “PriorityQueue মানে সাজানো তালিকা।” নিচের কোডটি AI-এর সাধারণ ধাঁচের — compile হয়, প্রথম দেখায় ঠিকও মনে হয়:

Stack<String> history = new Stack<>();                 // ১
Queue<String> jobs = new LinkedList<>();               // ২
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(5, 1, 4, 2, 3));
for (int x : pq) System.out.print(x + " ");            // ৩ — "সাজানো" আশা
jobs.offer(next);
String j = jobs.remove();                              // ৪ — offer-এর সাথে remove

চারটি সমস্যা: (১) পুরোনো Stack, (২) queue হিসেবে LinkedList, (৩) PriorityQueue-এর ওপর লুপ — এটা heap-এর ক্রম ছাপে, সাজানো নয় (৫টি সংখ্যায় দেখতে প্রায় ঠিক লাগে বলে পরীক্ষায় ধরা পড়ে না), (৪) একই queue-তে null-পরিবার দিয়ে ঢোকানো আর exception-পরিবার দিয়ে বের করা — খালি হওয়ার পরিস্থিতি কেউ ভাবেনি।

AI যা করেকেন সমস্যাআপনি যা করবেন
new Stack<>()synchronized-এর খরচ, LIFO ভাঙা যায়, ছাপার ক্রম উল্টোDeque<T> = new ArrayDeque<>()
Queue q = new LinkedList<>()প্রতি জিনিসে node object, ধীর; null ঢুকতে পারেArrayDeque, যদি না সত্যিই null লাগে
PriorityQueue-এ for-each, toString বা stream দিয়ে “সাজানো” তালিকাheap-এর array ক্রম; ছোট ইনপুটে কাকতালীয়ভাবে ঠিক দেখায়বারবার poll, বা কপি করে sort
offer + remove, বা poll-এর ফল যাচাই ছাড়া ব্যবহারখালি অবস্থায় exception বা দূরে গিয়ে NullPointerExceptionএকটি পরিবার বেছে নিন; খালি অবস্থা স্পষ্টভাবে সামলান
নিজের record-এ Comparator ছাড়া PriorityQueueপ্রথম offer-এই ClassCastExceptionComparator দিন, সমান হলে tie-breaker সহ
max চাই, কিন্তু ডিফল্ট PriorityQueue (C++-এর অভ্যাসে)Java-র ডিফল্ট min-heap — উল্টো ফলComparator.reverseOrder() বা উপযুক্ত Comparator

Queue বা stack লাগে এমন কোড AI দিয়ে লেখানোর সময় এই কাঠামো ব্যবহার করুন — শর্তগুলো ওপরের প্রতিটি ভুলের দরজা বন্ধ করে:

আমি Java 21 শিখছি; Queue, Deque আর PriorityQueue জানি। কাজ: [যেমন — একটি হাসপাতালের জরুরি বিভাগের সিমুলেশন, যেখানে রোগী আসে আর সবচেয়ে গুরুতর জন আগে ডাক্তার পায়] শর্ত: ১. java.util.Stack বা Vector ব্যবহার করবে না; stack লাগলে Deque আর ArrayDeque. ২. Queue-র জন্য LinkedList নয়, ArrayDeque; LinkedList নিলে কারণ লিখবে। ৩. প্রতিটি queue-তে একটিই method-পরিবার: offer/poll/peek অথবা add/remove/element. কোনটা কেন, এক লাইনে বলো। ৪. poll বা peek null দিতে পারে — প্রতিটি জায়গায় খালি অবস্থা কীভাবে সামলাচ্ছ, দেখাও। ৫. PriorityQueue থেকে সাজানো ক্রম চাইলে শুধু poll লুপ; for-each, toString বা stream দিয়ে নয়। ৬. PriorityQueue-র Comparator-এ সমান priority হলে tie-breaker দেবে। ৭. প্রতিটি কাজের জটিলতা (O(1), O(log n), O(n)) মন্তব্যে লিখবে।

AI-এর queue কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Binary heap কে আবিষ্কার করেন, আর একসাথে অনেক জিনিস থেকে heap বানানো একে একে offer করার চেয়ে দ্রুত হয় কীভাবে?

Binary heap প্রথম প্রকাশিত হয় ১৯৬৪ সালে, J. W. J. Williams-এর লেখা “Algorithm 232: Heapsort” নামে, Communications of the ACM পত্রিকায়। Williams-এর উদ্দেশ্য ছিল একটি সাজানোর অ্যালগরিদম — heapsort: সব জিনিস একটি heap-এ রাখো, তারপর বারবার শিকড় বের করো; প্রতিবার সবচেয়ে ছোট (বা বড়) জিনিসটি বেরিয়ে আসে, তাই n বার বের করলেই সাজানো তালিকা। কিন্তু সেই সাজানোর পথে তিনি যে কাঠামোটি দিলেন — একটি array, যেখানে শুধু index-এর হিসাবে প্রতিটি ঘরের সন্তান খুঁজে পাওয়া যায় (০ থেকে গোনা Java-র array-তে ঘর i-এর সন্তান 2i+1 আর 2i+2) — সেটাই পরে আলাদা করে priority queue-এর মান হয়ে দাঁড়ায়। কোনো pointer নেই, বাড়তি মেমরি নেই, শুধু index-এর হিসাব।

Floyd-এর উন্নতি। একই বছর Robert W. Floyd একই পত্রিকায় “Treesort 3” প্রকাশ করেন, যেখানে heap বানানোর একটি চতুর পদ্ধতি ছিল। একে একে n-টি জিনিস offer করলে প্রতিটির sift-up O(log n), মোট O(n log n). Floyd উল্টো দিক থেকে ভাবলেন: পুরো array-কে আগেই একটি (নিয়ম-ভাঙা) গাছ ধরে নাও, তারপর নিচ থেকে ওপরে প্রতিটি অভিভাবকের ওপর sift-down চালাও। পাতাগুলোর (প্রায় অর্ধেক জিনিস) কিছুই করতে হয় না। তার এক স্তর ওপরের জিনিসগুলো সর্বোচ্চ এক ধাপ নামে, তার ওপরেরগুলো দুই ধাপ — বেশিরভাগ জিনিস থাকে নিচের দিকে, যেখানে কাজ কম। যোগ করলে মোট কাজ দাঁড়ায় O(n). Java-র new PriorityQueue<>(collection) ভেতরে ঠিক এই heapify চালায়, তাই অনেক জিনিস একসাথে থাকলে একে একে offer করার চেয়ে collection দিয়ে বানানো ভালো।

Heapsort-এর জায়গা। Heapsort সবসময় O(n log n), আর বাড়তি মেমরি লাগে না — তাত্ত্বিকভাবে চমৎকার। বাস্তবে সাধারণত quicksort বা merge sort-ভিত্তিক পদ্ধতি দ্রুত, কারণ heap-এর লাফানো index CPU cache-এর সাথে ভালো মেলে না। তবে introsort-এর মতো মিশ্র অ্যালগরিদম (C++-এর অনেক std::sort বাস্তবায়নে) quicksort খারাপ পথে গেলে heapsort-এ চলে যায় — সবচেয়ে খারাপ ক্ষেত্রের নিশ্চয়তার জন্য। Java-র Arrays.sort primitive-এর জন্য dual-pivot quicksort আর object-এর জন্য TimSort ব্যবহার করে।

মূল উপলব্ধি: ষাট বছর আগের একটি সাজানোর কৌশল থেকে আজকের প্রায় প্রতিটি priority queue এসেছে; আর Floyd দেখিয়েছিলেন, একই কাঠামোতে কাজের ক্রম উল্টে দিলেই O(n log n) কাজ O(n) হয়ে যেতে পারে।

প্র ০২Java-র Stack class-কে কেন নকশার ভুল বলা হয়, আর Deque কীভাবে তার জায়গা নিল?

১৯৯৬ সালের JDK 1.0-তে collection বলতে ছিল অল্প কয়েকটি class: Vector, Hashtable, Stack, Enumeration. সময়ের চাপে লেখা, আর তখনকার ধারণায় “নিরাপদ” মানে ছিল সব method synchronized. Stack লেখা হলো Vector-কে extends করে, পাঁচটি method যোগ করে: push, pop, peek, empty, search. কোড পুনর্ব্যবহারের দিক থেকে সহজ — Vector-এর ভেতরের array আর বেড়ে ওঠার যুক্তি বিনামূল্যে।

ভুলটা কোথায়। Inheritance মানে প্রতিশ্রুতি: subclass তার parent-এর সব কাজ করতে পারবে, আর parent যেখানে চলে সেখানে subclass-ও চলবে (পর্ব ১৯-এর Liskov substitution). Stack সেই প্রতিশ্রুতি রাখে ঠিকই — কিন্তু ঠিক সেটাই সমস্যা। একটি stack-এর সংজ্ঞা হলো যা করা যায় না (মাঝখানে ঢোকা, নিচ থেকে পড়া); Vector থেকে উত্তরাধিকারে সেগুলো সব খোলা। এখানে দরকার ছিল composition — ভেতরে একটি Vector রাখা, বাইরে শুধু stack-এর কাজ দেখানো। Joshua Bloch তাঁর Effective Java বইয়ে “inheritance-এর চেয়ে composition পছন্দ করুন” নীতি ব্যাখ্যা করতে Java platform-এর এমন উদাহরণ দেখান, যেখানে Stack-কে Vector-এর subclass বানানো নীতি লঙ্ঘনের নমুনা।

Collections Framework আর Deque. ১৯৯৮ সালের Java 1.2-তে Bloch-এর নেতৃত্বে আসে আজকের Collections Framework: interface (List, Set, Map) আর বাস্তবায়ন আলাদা, আর ডিফল্টে synchronized নয়। Java 5 (২০০৪)-এ Queue আর PriorityQueue যোগ হয়, আর একই সংস্করণে Doug Lea-র নেতৃত্বে JSR 166-এর java.util.concurrent — BlockingQueue সহ। Doug Lea তখন State University of New York at Oswego-র অধ্যাপক, আর তাঁর আগের concurrency library-ই এর ভিত্তি। Java 6 (২০০৬)-এ আসে Deque interface আর ArrayDeque; JDK-র source-এ ArrayDeque-এর লেখক হিসেবে Josh Bloch আর Doug Lea-র নাম। একই সংস্করণে LinkedList-ও Deque মানতে শুরু করে। এরপর থেকে Stack-এর documentation নিজেই Deque ব্যবহার করতে বলে।

কেন মুছে ফেলা হয় না। Java-র কঠোর backward compatibility: ১৯৯৬ সালের কোড আজও compile হওয়ার কথা। তাই Stack, Vector, Hashtable আজও আছে — ব্যবহারযোগ্য, কিন্তু নতুন কোডের জন্য নয়।

মূল উপলব্ধি: একটি abstraction-এর মূল্য অনেক সময় তার নিষেধে; Stack সেই নিষেধ হারিয়েছিল inheritance-এর কারণে, আর Deque প্রায় এক দশক পরে interface আর সরু ভ্যারিয়েবল টাইপ দিয়ে তা ফিরিয়ে আনে।

প্র ০৩অপারেটিং সিস্টেম আর ইন্টারনেট — দুই জায়গাতেই queue কেন সর্বত্র?

যেখানেই কাজ আসার গতি আর কাজ সামলানোর গতি সবসময় সমান থাকে না, সেখানেই queue লাগে। কম্পিউটার সিস্টেম এমন অমিলে ভরা।

CPU scheduling. আপনার ফোনে বা ল্যাপটপে একসাথে শত শত প্রোগ্রাম আর thread চলছে, কিন্তু CPU core হাতে গোনা কয়েকটি। অপারেটিং সিস্টেম “চলার জন্য প্রস্তুত” কাজগুলোকে একটি ready queue-তে রাখে। সবচেয়ে সরল নীতি হলো round-robin: queue-এর সামনের জনকে একটু সময় দাও, সময় শেষ হলে তাকে পেছনে পাঠাও — FIFO, ঠিক টোকেন কাউন্টারের মতো। বাস্তবের scheduler-রা priority-ও দেখে: কীবোর্ডে টাইপ করার সাড়া দেওয়া কাজ পেছনের ব্যাকআপের আগে যাবে। Linux দীর্ঘদিন (২০০৭ থেকে) CFS নামের scheduler ব্যবহার করেছে, যা কাজগুলোকে একটি red-black tree-তে “কে কত কম সময় পেয়েছে” অনুযায়ী সাজিয়ে রাখত — কার্যত একটি priority queue; সাম্প্রতিক Linux সংস্করণে তার জায়গা নিয়েছে EEVDF নামের আরেকটি নকশা। ডিস্কে পড়া-লেখার অনুরোধ, প্রিন্টারের কাজ, কীবোর্ডের চাপ — সবই নিজস্ব queue-তে অপেক্ষা করে।

নেটওয়ার্ক। একটি রাউটারে একসাথে অনেক তার দিয়ে প্যাকেট আসে, কিন্তু বের হওয়ার তার হয়তো একটিই। যে প্যাকেট এখনই পাঠানো যাচ্ছে না, সে রাউটারের buffer-এ — একটি queue-তে — অপেক্ষা করে। Buffer ভরে গেলে নতুন প্যাকেট ফেলে দেওয়া হয় (“tail drop”) — ঠিক সীমিত queue-এর offer যেভাবে false দেয়। মজার ব্যাপার হলো, buffer খুব বড় রাখলেও সমস্যা: প্যাকেট হারায় না ঠিকই, কিন্তু লম্বা লাইনে দাঁড়িয়ে দেরি বাড়ে — ভিডিও কলে কথা আটকে যায়। এই সমস্যার নাম দেওয়া হয়েছে bufferbloat, আর একে সামলাতে CoDel-এর মতো কৌশল এসেছে, যা লাইনে অপেক্ষার সময় মেপে আগেভাগে প্যাকেট ফেলে। ভিডিও কলের প্যাকেটকে ফাইল ডাউনলোডের আগে পাঠানো — এটা আবার priority queue.

আপনার কোডে। একটি ওয়েব সার্ভারে আসা অনুরোধ, একটি মেসেজ সিস্টেমে জমা বার্তা (Kafka, RabbitMQ-এর মতো সিস্টেমের নামেই “queue” বা log), একটি গেমের ইভেন্ট — সবই এই পর্বের ধারণার বড় রূপ।

মূল উপলব্ধি: Queue হলো দুই অসম গতির মাঝখানের ধাক্কা-শোষক; তার আকার আর ক্রমের নীতি (FIFO, priority, কখন ফেলে দেওয়া) ঠিক করে সিস্টেম কতটা দ্রুত আর কতটা ন্যায্য মনে হবে।

প্র ০৪Dijkstra-র সবচেয়ে ছোট পথের অ্যালগরিদমে priority queue কেন লাগে, আর Java-র PriorityQueue-তে “decrease-key” না থাকায় কী করতে হয়?

BFS সবচেয়ে ছোট পথ দেয় যখন প্রতিটি ধাপের “দাম” সমান। কিন্তু রাস্তার মানচিত্রে ঢাকা থেকে চট্টগ্রামের পথে প্রতিটি রাস্তার দৈর্ঘ্য আলাদা। তখন দরকার Dijkstra-র অ্যালগরিদম। ডাচ কম্পিউটার বিজ্ঞানী Edsger W. Dijkstra এটি ভাবেন ১৯৫৬ সালে — পরে তিনি বলেছেন, আমস্টারডামে একটি ক্যাফেতে বসে, কাগজ-কলম ছাড়াই, প্রায় বিশ মিনিটে — আর প্রকাশ করেন ১৯৫৯ সালে।

ধারণাটি। যে জায়গাগুলোতে এখনো চূড়ান্ত দূরত্ব জানা নেই, তাদের মধ্যে যার বর্তমান জানা দূরত্ব সবচেয়ে কম, তাকে বের করো — তার দূরত্ব এখন চূড়ান্ত (সব রাস্তার দৈর্ঘ্য শূন্য বা ধনাত্মক হলে)। তারপর তার প্রতিবেশীদের দূরত্ব হালনাগাদ করো। “বারবার সবচেয়ে ছোটটি বের করা” — এটাই priority queue-এর কাজ। BFS-এর FIFO queue-কে min-heap দিয়ে বদলালেই প্রায় Dijkstra.

Decrease-key সমস্যা। হালনাগাদের সময় একটি জায়গার দূরত্ব কমে যেতে পারে — যে ইতিমধ্যে heap-এ আছে। পাঠ্যবইয়ের heap-এ একটি decrease-key কাজ থাকে: জিনিসটির মান কমাও আর sift-up করো, O(log n). কিন্তু Java-র PriorityQueue-তে এমন method নেই। একটি জিনিস heap-এর কোন ঘরে আছে, PriorityQueue তা বাইরে জানায় না; remove(o) খুঁজতে O(n) নেয়। আর heap-এর ভেতরে থাকা অবস্থায় object-এর মান বদলে দিলে heap-এর নিয়ম নীরবে ভেঙে যায় — পরের poll ভুল জিনিস দিতে পারে।

বাস্তব সমাধান — lazy deletion. দূরত্ব কমলে পুরোনোটা খুঁজতে যেও না; নতুন দূরত্বসহ জায়গাটি আবার offer করো। Heap-এ একই জায়গার একাধিক রেকর্ড থাকবে। Poll করে যদি দেখো তার দূরত্ব ইতিমধ্যে চূড়ান্ত দূরত্বের চেয়ে বেশি, তাহলে সেটা পুরোনো রেকর্ড — বাদ দাও। Heap-এ সর্বোচ্চ রাস্তার সংখ্যার সমান রেকর্ড জমতে পারে, তাই মোট সময় O(E log E), যা O(E log V)-এর সমান মাত্রার। কনটেস্ট আর ইন্টারভিউতে প্রায় সবাই এই পথেই Dijkstra লেখে। তাত্ত্বিকভাবে আরও ভালো কাঠামো আছে — Fredman আর Tarjan-এর Fibonacci heap (১৯৮৪), যেখানে decrease-key গড়ে O(1) — কিন্তু তার জটিলতা আর ধ্রুবক খরচের কারণে বাস্তবে সাধারণ binary heap প্রায়ই জেতে।

মূল উপলব্ধি: একটি library-র নকশা ঠিক করে কোন অ্যালগরিদম কীভাবে লিখতে হবে; Java-র PriorityQueue সরল আর নিরাপদ থাকতে decrease-key বাদ দিয়েছে, আর প্রোগ্রামাররা “পুরোনোটা মুছো না, নতুনটা যোগ করো” কৌশলে সেই ফাঁক পূরণ করেন।

প্র ০৫ব্যাংকের লাইন আর সার্ভারের অনুরোধ — queueing theory আর Little-এর সূত্র কী বলে?

লাইন নিয়ে গণিতের একটি পুরো শাখা আছে — queueing theory. এর শুরু বিশ শতকের গোড়ায়, ডেনমার্কের প্রকৌশলী Agner Krarup Erlang-এর হাতে। তিনি কোপেনহেগেনের টেলিফোন কোম্পানিতে কাজ করতেন, আর প্রশ্ন ছিল: কতগুলো লাইন বসালে বেশিরভাগ কল আটকাবে না? ১৯০৯ সাল থেকে তাঁর প্রকাশিত কাজ থেকেই এই শাখার জন্ম; টেলিফোন ট্রাফিকের একক “erlang” তাঁর নামে।

Little-এর সূত্র। এই শাখার সবচেয়ে সরল আর আশ্চর্য ফলটি হলো Little’s law: L = λW. L হলো গড়ে সিস্টেমে কতজন আছে (লাইনে আর সেবা নিচ্ছে মিলিয়ে), λ (ল্যামডা) হলো গড়ে প্রতি মিনিটে কতজন আসে, আর W হলো প্রত্যেকে গড়ে কত সময় সিস্টেমে থাকে। John D. C. Little (পরে MIT-এর দীর্ঘদিনের অধ্যাপক) ১৯৬১ সালে এর সাধারণ প্রমাণ প্রকাশ করেন। আশ্চর্য দিক হলো, সূত্রটি প্রায় কোনো অনুমানের ওপর নির্ভর করে না — মানুষ কীভাবে আসে, সেবায় কত সময় লাগে, লাইন FIFO না priority — দীর্ঘ সময়ের গড় নিলে সবখানে খাটে।

একটি ব্যাংকের উদাহরণ। ধরুন একটি শাখায় ঘণ্টায় গড়ে ৩০ জন আসেন, আর একজন গ্রাহক গড়ে ২০ মিনিট (১/৩ ঘণ্টা) শাখায় থাকেন। তাহলে গড়ে শাখায় থাকেন ৩০ × ১/৩ = ১০ জন। বসার চেয়ার কতগুলো লাগবে, তার একটি ভালো অনুমান পাওয়া গেল কোনো জটিল হিসাব ছাড়াই। সার্ভারে একই কথা: প্রতি সেকেন্ডে ২০০ অনুরোধ আসে, প্রতিটির উত্তরে গড়ে ০.০৫ সেকেন্ড লাগে — তাহলে যেকোনো মুহূর্তে গড়ে ১০টি অনুরোধ চলমান। এটা থেকে বোঝা যায় thread বা connection pool কত বড় হওয়া দরকার।

আরেকটি কঠিন শিক্ষা। Queueing theory-র আরেকটি পরিচিত ফল হলো, কাউন্টার যত ব্যস্ত হয় — অর্থাৎ আসার হার সেবার ক্ষমতার যত কাছে যায় — অপেক্ষার সময় তত দ্রুত বাড়ে, সরলরেখায় নয়, প্রায় বিস্ফোরণের মতো। ৮০% ব্যস্ত কাউন্টার আর ৯৫% ব্যস্ত কাউন্টারের লাইনের দৈর্ঘ্যে বিশাল পার্থক্য। তাই সার্ভার সাধারণত পুরো ক্ষমতায় চালানো হয় না, কিছুটা ফাঁকা রাখা হয়। আর এজন্যই অনেক ব্যাংক ও বিমানবন্দর প্রতিটি কাউন্টারের আলাদা লাইনের বদলে একটি সাধারণ লাইন রাখে, যেখান থেকে যে কাউন্টার খালি হয় সে পরের জনকে ডাকে — গড় অপেক্ষা কমে, আর “আমার লাইনটাই ধীর” অভিযোগও থাকে না।

মূল উপলব্ধি: Queue শুধু একটি data structure নয়, একটি পরিমাপযোগ্য বাস্তবতা; L = λW-এর মতো সরল সূত্র দিয়ে ব্যাংকের চেয়ার থেকে সার্ভারের ক্ষমতা পর্যন্ত আগেভাগে হিসাব করা যায়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — হাতে-কলমে অবস্থা অনুসরণ করা আর ভাবাই আসল কাজ।

  1. আউটপুট বলুন: Deque<Integer> d = new ArrayDeque<>(); তারপর d.offerLast(1); d.offerFirst(2); d.push(3); d.offerLast(4); — এখন System.out.println(d) কী ছাপবে? তারপর d.pop(), d.pollLast() কী দেবে, আর শেষে d-তে কী থাকবে?

    ধাপে ধাপে: [1] → offerFirst(2) সামনে: [2, 1] → push(3) মানে addFirst: [3, 2, 1] → offerLast(4): [3, 2, 1, 4]. প্রথম ছাপা [3, 2, 1, 4]. pop() সামনে থেকে তোলে → 3; pollLast() পেছন থেকে → 4; শেষে [2, 1]. (চালিয়ে যাচাই করা।) মূল শিক্ষা: push/pop দুটিই সামনের মাথায় কাজ করে।

  2. ব্রাউজারের দুই stack অনুসরণ করুন: শুরুতে বর্তমান পাতা A, দুই stack খালি। কাজগুলো: B-তে যান, C-তে যান, Back, Back, D-তে যান, Forward চাপার চেষ্টা। প্রতিটি কাজের পর বর্তমান পাতা আর back, forward-এর অবস্থা লিখুন। শেষ Forward-এ কী হবে?

    B-তে: বর্তমান B, back [A], forward []. C-তে: বর্তমান C, back [B, A] (ওপর বাঁয়ে), forward []. Back: বর্তমান B, back [A], forward [C]. Back: বর্তমান A, back [], forward [B, C]. D-তে: বর্তমান D, back [A], আর forward খালি হয়ে গেল — নতুন পথে গেলে পুরোনো সামনের ইতিহাস মুছে যায়। শেষ Forward: forward খালি, তাই কিছু হয় না। কোডে forward.poll() বা peek() দিয়ে null যাচাই করুন, অথবা আগে isEmpty() দেখুন — pop() সরাসরি ডাকলে NoSuchElementException, আর আসল ব্রাউজারে বোতামটি তখন ধূসর থাকে।

  3. সবচেয়ে ছোট K-টি: kSmallest(int[] a, int k) লিখুন, যা heap দিয়ে সবচেয়ে ছোট k-টি সংখ্যা ছোট থেকে বড় ক্রমে ফেরত দেয়, আর heap-এ কখনো k+1-এর বেশি জিনিস রাখে না। {42, 7, 19, 3, 25, 11}, k = 3 হলে উত্তর [3, 7, 11]. কোন ধরনের heap — min না max — আর কেন?
    static List<Integer> kSmallest(int[] a, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>(Comparator.reverseOrder()); // max-heap
        for (int x : a) {
            heap.offer(x);
            if (heap.size() > k) heap.poll();          // সবচেয়ে বড়টি বাদ
        }
        List<Integer> out = new ArrayList<>(heap);
        Collections.sort(out);
        return out;
    }

    Max-heap — top-K-এর উল্টো রূপ। আমরা ছোটদের রাখতে চাই, তাই যাকে ফেলতে হবে সে হলো এখন পর্যন্ত রাখা সবচেয়ে বড়টি; max-heap ঠিক সেটাকেই শিকড়ে রাখে, তাই poll তাকে O(log k)-এ ফেলে দেয়। আউটপুট [3, 7, 11] (চালিয়ে যাচাই করা)। শেষে sort না করলে ছাপা ক্রম হতো heap-এর ক্রম — অংশ ৬-এর ফাঁদ।

  4. ভুল খুঁজুন: একজন সহকর্মী (বা AI) একটি প্রিন্টার-সার্ভারের জন্য লিখেছেন: PriorityQueue<String> tasks = new PriorityQueue<>(); tasks.addAll(List.of("deploy", "backup", "email", "audit")); তারপর বললেন, “System.out.println(tasks) দেখাচ্ছে [audit, backup, email, deploy] — মানে সাজানো হয়নি, PriorityQueue কাজ করছে না।” তিনি কী ভুল বুঝেছেন, আর ক্রম যাচাইয়ের সঠিক উপায় কী?

    আউটপুট [audit, backup, email, deploy] সত্যিই আসে (যাচাই করা), আর PriorityQueue ঠিকই কাজ করছে। toString ভেতরের heap array ছাপে: শিকড়ে সবচেয়ে ছোট “audit” ঠিক আছে, আর বাকিরা শুধু অভিভাবক-সন্তানের নিয়ম মানে — “email” (ঘর ২) আর “deploy” (ঘর ৩) দুজনের অভিভাবক আলাদা, তাদের মধ্যে কোনো ক্রমের দায় নেই। যাচাইয়ের সঠিক উপায়: while (!tasks.isEmpty()) System.out.println(tasks.poll()); — এতে আসবে audit, backup, deploy, email. আরও একটি বিষয়: আসল প্রিন্টার-সার্ভারে নাম দিয়ে নয়, priority আর আসার সময় দিয়ে সাজানো উচিত — একটি record আর Comparator দিয়ে।

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

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

ক্রমJava-র PriorityQueue (min-heap)-এ poll() ডাকলে ভেতরে যা ঘটে, ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📌 শিকড়ের (ঘর ০) জিনিসটি ফেরত দেওয়ার জন্য আলাদা করে রাখা হয়
  2. 📦 array-এর শেষ ঘরের জিনিসটি তুলে শিকড়ে বসানো হয়, আকার এক কমে
  3. 🔍 নতুন শিকড়কে তার দুই সন্তানের ছোটটির সাথে তুলনা করা হয়
  4. 🔽 সে বড় হলে জায়গা বদল — নিয়ম না মেলা পর্যন্ত নিচে নামা (sift-down)
  5. ✅ আলাদা করে রাখা সবচেয়ে ছোট জিনিসটি ফেরত দেওয়া হয়

Java-তে একটি খালি Queue<String> q = new ArrayDeque<>(); — প্রথমে q.poll() আর তারপর q.remove() ডাকলে কী ঘটে?

  • দুটিই null দেয়
  • দুটিই NoSuchElementException ছোড়ে
  • poll() দেয় null, remove() ছোড়ে NoSuchElementException
  • poll() ছোড়ে NullPointerException, remove() দেয় null

PriorityQueue<Integer> pq = new PriorityQueue<>(); তাতে একে একে 5, 1, 4, 2, 3 offer করে System.out.println(pq) চালালে কী ছাপা হয়?

  • [1, 2, 3, 4, 5]
  • [1, 2, 4, 5, 3]
  • [5, 1, 4, 2, 3]
  • প্রতিবার চালালে আলাদা, অনির্দিষ্ট ক্রম

আধুনিক Java কোডে একটি LIFO stack-এর জন্য কোনটি সবচেয়ে উপযুক্ত?

  • java.util.Stack
  • java.util.Vector
  • PriorityQueue
  • Deque<T> stack = new ArrayDeque<>()

n জিনিসের একটি Java PriorityQueue-তে peek() আর poll()-এর সময়-জটিলতা যথাক্রমে কত?

  • O(1) আর O(log n)
  • O(1) আর O(n)
  • O(log n) আর O(log n)
  • O(n) আর O(n log n)

এক কোটি পরীক্ষার নম্বর একে একে আসছে; সবচেয়ে কম মেমরিতে সর্বোচ্চ ১০টি নম্বর বের করার ভালো উপায় কোনটি?

  • সব নম্বর একটি ArrayList-এ রেখে sort করে শেষের ১০টি
  • সব নম্বর একটি max-heap-এ রেখে ১০ বার poll
  • আকার ১০-এর একটি min-heap; আকার ১০ ছাড়ালে poll
  • একটি ArrayDeque stack-এ রেখে শেষের ১০টি pop

Java-তে Deque<String> d = new ArrayDeque<>(); d.add(null); চালালে কী হয়?

  • null সফলভাবে যোগ হয়, size হয় 1
  • NullPointerException ছোড়ে
  • IllegalStateException: Queue full
  • compile error

শব্দকোষ

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

মনে রাখুন

  1. তিন ক্রম, তিন টাইপ: FIFO → Queue, LIFO → Deque stack হিসেবে, priority → PriorityQueue; ভ্যারিয়েবলের টাইপ interface, বাস্তবায়ন সাধারণত ArrayDeque.
  2. দুই পরিবার: add/remove/element exception ছোড়ে, offer/poll/peek দেয় false/null; এক queue-তে একটিই পরিবার।
  3. ArrayDeque = circular buffer: head আর tail ঘুরে আসে, দুই মাথায় O(1), null নয়; Stack (Vector-এর subclass) আর queue হিসেবে LinkedList এড়িয়ে চলুন।
  4. PriorityQueue = binary min-heap: offer/poll O(log n), peek O(1); max চাইলে Comparator.reverseOrder(); নিজের টাইপে Comparator আর tie-breaker; সাজানো ক্রম শুধু poll দিয়ে।
  5. ক্লাসিক ব্যবহার: বন্ধনী আর undo-তে stack, BFS-এ queue, sliding window-এ deque, top-K-তে আকার K-এর heap, আর অনেক thread-এর মাঝে BlockingQueue.

পরের পর্বে: Comparable আর Comparator. এই পর্বে বারবার “Comparator দিন” শুনেছেন — এবার জানবেন একটি class-এর নিজের স্বাভাবিক ক্রম কীভাবে ঠিক করে, বাইরে থেকে নতুন নিয়ম কীভাবে লেখে, comparing, thenComparing, reversed দিয়ে নিয়ম জোড়া দেওয়া, আর compareTo-এর যে চুক্তি ভাঙলে TreeMap ও PriorityQueue দুটিই অদ্ভুত আচরণ করে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান