DSA IQ - Level 1 - Part 1

প্রশ্ন 1: বিগ-ও নোটেশন কী এবং এটি কেন গুরুত্বপূর্ণ? উত্তর: বিগ-ও নোটেশন (Big-O Notation) হল একটি এলগরিদমের টাইম কমপ্লেক্সিটি বা স্পেস কমপ্লেক্সিটি বর্ণনা করার একটি পদ্ধতি। এটি দেখায় একটি এলগরিদমের সময় বা স্পেস প্রয়োজন কীভাবে বাড়ে যখন আমরা ইনপুট সাইজ বাড়াই। উদাহরণ: O(1) - কনস্ট্যান্ট টাইম কমপ্লেক্সিটি (সবচেয়ে দ্রুত) O(log n) - লগারিদমিক কমপ্লেক্সিটি (খুব ভালো) O(n) - লিনিয়ার কমপ্লেক্সিটি (ভালো) O(n log n) - লিনিয়ার লগারিদমিক (মাঝামাঝি) O(n²) - কোয়াড্রাটিক কমপ্লেক্সিটি (ধীর) O(2^n) - এক্সপোনেনশিয়াল কমপ্লেক্সিটি (খুব ধীর) এটি গুরুত্বপূর্ণ কারণ এলগরিদমের দক্ষতা বুঝতে এবং বড় ডাটাসেট নিয়ে কাজ করার সময় সঠিক সিদ্ধান্ত নিতে সাহায্য করে। প্রশ্ন 2: অ্যারে এবং লিংকড লিস্টের মধ্যে পার্থক্য কী? উত্তর: অ্যারে: মেমোরিতে পাশাপাশি স্থান দখল করে। র‍্যান্ডম অ্যাকসেস O(1) সময়ে সম্ভব। সাইজ ফিক্সড হতে পারে (স্ট্যাটিক অ্যারে)। লিংকড লিস্ট: মেমরিতে বিচ্ছিন্নভাবে থাকতে পারে। প্রতিটি নোড পরবর্তী নোডের রেফারেন্স রাখে। র‍্যান্ডম অ্যাকসেস O(n) সময় নেয়, কিন্তু এলিমেন্ট যোগ/মুছে ফেলা অ্যারের তুলনায় সহজ। জাভাস্ক্রিপ্টে লিংকড লিস্ট: class Node { constructor(value) { this.value = value; this.next = null; } } class LinkedList { constructor() { this.head = null; this.tail = null; this.length = 0; } append(value) { const newNode = new Node(value); if (!this.head) { this.head = newNode; this.tail = newNode; } else { this.tail.next = newNode; this.tail = newNode; } this.length++; return this; } prepend(value) { const newNode = new Node(value); if (!this.head) { this.head = newNode; this.tail = newNode; } else { newNode.next = this.head; this.head = newNode; } this.length++; return this; } } প্রশ্ন 3: স্ট্যাক এবং কিউ কী? এদের মধ্যে পার্থক্য কী? উত্তর: স্ট্যাক: LIFO (Last-In-First-Out) ডাটা স্ট্রাকচার। যেমন প্লেটের স্তূপ - সর্বশেষ রাখা প্লেটটি প্রথমে নেওয়া হয়। কিউ: FIFO (First-In-First-Out) ডাটা স্ট্রাকচার। যেমন টিকেট লাইন - প্রথম আসা ব্যক্তি প্রথমে সেবা পায়। জাভাস্ক্রিপ্টে স্ট্যাক: class Stack { constructor() { this.items = []; } push(element) { this.items.push(element); } pop() { if (this.isEmpty()) return "Stack is empty"; return this.items.pop(); } peek() { if (this.isEmpty()) return "Stack is empty"; return this.items[this.items.length - 1]; } isEmpty() { return this.items.length === 0; } size() { return this.items.length; } } জাভাস্ক্রিপ্টে কিউ: class Queue { constructor() { this.items = []; } enqueue(element) { this.items.push(element); } dequeue() { if (this.isEmpty()) return "Queue is empty"; return this.items.shift(); } front() { if (this.isEmpty()) return "Queue is empty"; return this.items[0]; } isEmpty() { return this.items.length === 0; } size() { return this.items.length; } } প্রশ্ন 4: বাইনারি সার্চ কীভাবে কাজ করে? উত্তর: বাইনারি সার্চ হল একটি অ্যালগরিদম যা সর্টেড অ্যারেতে একটি নির্দিষ্ট উপাদান খুঁজে বের করতে ব্যবহৃত হয়। এটি প্রতিবার অর্ধেক অ্যারে চেক করে এবং লক্ষ্য উপাদানটি কোন অর্ধেকে আছে তা নির্ধারণ করে। এটি O(log n) সময় কমপ্লেক্সিটি দেয়। জাভাস্ক্রিপ্টে বাইনারি সার্চ: function binarySearch(arr, target) { let left = 0; let right = arr.length - 1; while (left arr[j + 1]) { // সোয়াপ করুন [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; } } } return arr; } জাভাস্ক্রিপ্টে কুইক সর্ট: function quickSort(arr) { if (arr.length pivot) { right.push(element); } else { equal.push(element); } } return [...quickSort(left), ...equal, ...quickSort(right)]; } প্রশ্ন 6: হ্যাশ টেবিল কী এবং কীভাবে কাজ করে? উত্তর: হ্যাশ টেবিল হল কী-ভ্যালু পেয়ার সংরক্ষণের ডাটা স্ট্রাকচার। হ্যাশ ফাংশন কী-কে একটি ইনডেক্সে রূপান্তর করে, যেখানে ভ্যালু সংরক্ষণ করা হয়। আদর্শ অবস্থায়, এর অ্যাকসেস, ইনসার্শন, এবং ডিলিশন O(1) টাইম কমপ্লেক্সিটি আছে। জাভাস্ক্রিপ্টে শেষ দিক থেকে ম্যাপ এবং অবজেক্ট হ্যাশ টেবিল হিসেবে কাজ করে: // অবজেক্ট ব্যবহার করে হ্যাশ টেবিল const hashTable = {}; // কী-ভ্যালু পেয়ার যোগ করা hashTable["name"] = "John"; hashTable["age"] = 30; hashTable["city"] = "New York"; // কী দিয়ে ভ্যালু অ্যাকসেস করা console.log(hashTable["name"]); // "John" // ম্যাপ ব্যবহার করে হ্যাশ টেবিল const map = new Map(); // কী-ভ্যালু পেয়ার যোগ করা map.set("name", "John"); map.set("age", 30); map.set("city", "New York"); // কী দিয়ে ভ্যালু অ্যাকসেস করা console.log(map.get("name")); // "John" প্রশ্ন 7: গ্রাফ কী এবং এটি কীভাবে প্রতিনিধিত্ব করা যায়? উত্তর: গ্রাফ হল নোড (বা ভার্টেক্স) এবং এজ (সংযোগ) দিয়ে গঠিত একটি ডাটা স্ট্রাকচার। গ্রাফ প্রতিনিধিত্ব করার দুটি সাধারণ উপায় হল: অ্যাডজেসেন্সি ম্যাট্রিক্স: একটি 2D অ্যারে যা দেখায় কোন নোড কোন নোডের সাথে সংযুক্ত অ্যাডজেসেন্সি লিস্ট: প্রতিটি নোডের জন্য সংযুক্ত নোডের একটি লিস্ট নিচে জাভাস্ক্রিপ্টে অ্যাডজেসেন্সি লিস্ট প্রতিনিধিত্ব: class Graph { constructor() { this.adjacencyList = {}; } addVertex(vertex) {

Apr 16, 2025 - 09:18
 0
DSA IQ - Level 1 - Part 1

প্রশ্ন 1:
বিগ-ও নোটেশন কী এবং এটি কেন গুরুত্বপূর্ণ?
উত্তর:

বিগ-ও নোটেশন (Big-O Notation) হল একটি এলগরিদমের টাইম কমপ্লেক্সিটি বা স্পেস কমপ্লেক্সিটি বর্ণনা করার একটি পদ্ধতি। এটি দেখায় একটি এলগরিদমের সময় বা স্পেস প্রয়োজন কীভাবে বাড়ে যখন আমরা ইনপুট সাইজ বাড়াই।
উদাহরণ:

  • O(1) - কনস্ট্যান্ট টাইম কমপ্লেক্সিটি (সবচেয়ে দ্রুত)
  • O(log n) - লগারিদমিক কমপ্লেক্সিটি (খুব ভালো)
  • O(n) - লিনিয়ার কমপ্লেক্সিটি (ভালো)
  • O(n log n) - লিনিয়ার লগারিদমিক (মাঝামাঝি)
  • O(n²) - কোয়াড্রাটিক কমপ্লেক্সিটি (ধীর)
  • O(2^n) - এক্সপোনেনশিয়াল কমপ্লেক্সিটি (খুব ধীর)

এটি গুরুত্বপূর্ণ কারণ এলগরিদমের দক্ষতা বুঝতে এবং বড় ডাটাসেট নিয়ে কাজ করার সময় সঠিক সিদ্ধান্ত নিতে সাহায্য করে।

প্রশ্ন 2:
অ্যারে এবং লিংকড লিস্টের মধ্যে পার্থক্য কী?
উত্তর:

  • অ্যারে: মেমোরিতে পাশাপাশি স্থান দখল করে। র‍্যান্ডম অ্যাকসেস O(1) সময়ে সম্ভব। সাইজ ফিক্সড হতে পারে (স্ট্যাটিক অ্যারে)।
  • লিংকড লিস্ট: মেমরিতে বিচ্ছিন্নভাবে থাকতে পারে। প্রতিটি নোড পরবর্তী নোডের রেফারেন্স রাখে। র‍্যান্ডম অ্যাকসেস O(n) সময় নেয়, কিন্তু এলিমেন্ট যোগ/মুছে ফেলা অ্যারের তুলনায় সহজ।

জাভাস্ক্রিপ্টে লিংকড লিস্ট:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  append(value) {
    const newNode = new Node(value);
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.length++;
    return this;
  }

  prepend(value) {
    const newNode = new Node(value);
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.next = this.head;
      this.head = newNode;
    }
    this.length++;
    return this;
  }
}

প্রশ্ন 3:
স্ট্যাক এবং কিউ কী? এদের মধ্যে পার্থক্য কী?
উত্তর:

  • স্ট্যাক: LIFO (Last-In-First-Out) ডাটা স্ট্রাকচার। যেমন প্লেটের স্তূপ - সর্বশেষ রাখা প্লেটটি প্রথমে নেওয়া হয়।
  • কিউ: FIFO (First-In-First-Out) ডাটা স্ট্রাকচার। যেমন টিকেট লাইন - প্রথম আসা ব্যক্তি প্রথমে সেবা পায়।

জাভাস্ক্রিপ্টে স্ট্যাক:

class Stack {
  constructor() {
    this.items = [];
  }

  push(element) {
    this.items.push(element);
  }

  pop() {
    if (this.isEmpty()) return "Stack is empty";
    return this.items.pop();
  }

  peek() {
    if (this.isEmpty()) return "Stack is empty";
    return this.items[this.items.length - 1];
  }

  isEmpty() {
    return this.items.length === 0;
  }

  size() {
    return this.items.length;
  }
}

জাভাস্ক্রিপ্টে কিউ:

class Queue {
  constructor() {
    this.items = [];
  }

  enqueue(element) {
    this.items.push(element);
  }

  dequeue() {
    if (this.isEmpty()) return "Queue is empty";
    return this.items.shift();
  }

  front() {
    if (this.isEmpty()) return "Queue is empty";
    return this.items[0];
  }

  isEmpty() {
    return this.items.length === 0;
  }

  size() {
    return this.items.length;
  }
}

প্রশ্ন 4:
বাইনারি সার্চ কীভাবে কাজ করে?
উত্তর:
বাইনারি সার্চ হল একটি অ্যালগরিদম যা সর্টেড অ্যারেতে একটি নির্দিষ্ট উপাদান খুঁজে বের করতে ব্যবহৃত হয়। এটি প্রতিবার অর্ধেক অ্যারে চেক করে এবং লক্ষ্য উপাদানটি কোন অর্ধেকে আছে তা নির্ধারণ করে। এটি O(log n) সময় কমপ্লেক্সিটি দেয়।

জাভাস্ক্রিপ্টে বাইনারি সার্চ:

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (arr[mid] === target) {
      return mid; // উপাদানটি পাওয়া গেছে, ইনডেক্স রিটার্ন করো
    } else if (arr[mid] < target) {
      left = mid + 1; // টার্গেট ডানদিকে আছে
    } else {
      right = mid - 1; // টার্গেট বামদিকে আছে
    }
  }

  return -1; // উপাদানটি পাওয়া যায়নি
}

প্রশ্ন 5:
বাবল সর্ট এবং কুইক সর্ট কী?
উত্তর:

  • বাবল সর্ট: সহজ সর্টিং অ্যালগরিদম যা পাশাপাশি উপাদানগুলি তুলনা করে এবং প্রয়োজনে তাদের বিনিময় করে। O(n²) সময় কমপ্লেক্সিটি।

  • কুইক সর্ট: ডিভাইড-এন্ড-কনকার সর্টিং অ্যালগরিদম। একটি পিভট উপাদান নির্বাচন করে, ছোট উপাদানগুলি বামদিকে এবং বড় উপাদানগুলি ডানদিকে সারিবদ্ধ করে। গড় কেসে O(n log n) সময় কমপ্লেক্সিটি।

জাভাস্ক্রিপ্টে বাবল সর্ট:

function bubbleSort(arr) {
  const n = arr.length;

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        // সোয়াপ করুন
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }

  return arr;
}

জাভাস্ক্রিপ্টে কুইক সর্ট:

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }

  const pivot = arr[Math.floor(arr.length / 2)];
  const left = [];
  const right = [];
  const equal = [];

  for (let element of arr) {
    if (element < pivot) {
      left.push(element);
    } else if (element > pivot) {
      right.push(element);
    } else {
      equal.push(element);
    }
  }

  return [...quickSort(left), ...equal, ...quickSort(right)];
}

প্রশ্ন 6:
হ্যাশ টেবিল কী এবং কীভাবে কাজ করে?
উত্তর:

হ্যাশ টেবিল হল কী-ভ্যালু পেয়ার সংরক্ষণের ডাটা স্ট্রাকচার। হ্যাশ ফাংশন কী-কে একটি ইনডেক্সে রূপান্তর করে, যেখানে ভ্যালু সংরক্ষণ করা হয়। আদর্শ অবস্থায়, এর অ্যাকসেস, ইনসার্শন, এবং ডিলিশন O(1) টাইম কমপ্লেক্সিটি আছে।

জাভাস্ক্রিপ্টে শেষ দিক থেকে ম্যাপ এবং অবজেক্ট হ্যাশ টেবিল হিসেবে কাজ করে:

// অবজেক্ট ব্যবহার করে হ্যাশ টেবিল
const hashTable = {};

// কী-ভ্যালু পেয়ার যোগ করা
hashTable["name"] = "John";
hashTable["age"] = 30;
hashTable["city"] = "New York";

// কী দিয়ে ভ্যালু অ্যাকসেস করা
console.log(hashTable["name"]); // "John"

// ম্যাপ ব্যবহার করে হ্যাশ টেবিল
const map = new Map();

// কী-ভ্যালু পেয়ার যোগ করা
map.set("name", "John");
map.set("age", 30);
map.set("city", "New York");

// কী দিয়ে ভ্যালু অ্যাকসেস করা
console.log(map.get("name")); // "John"

প্রশ্ন 7:
গ্রাফ কী এবং এটি কীভাবে প্রতিনিধিত্ব করা যায়?
উত্তর:

গ্রাফ হল নোড (বা ভার্টেক্স) এবং এজ (সংযোগ) দিয়ে গঠিত একটি ডাটা স্ট্রাকচার। গ্রাফ প্রতিনিধিত্ব করার দুটি সাধারণ উপায় হল:

  • অ্যাডজেসেন্সি ম্যাট্রিক্স: একটি 2D অ্যারে যা দেখায় কোন নোড কোন নোডের সাথে সংযুক্ত
  • অ্যাডজেসেন্সি লিস্ট: প্রতিটি নোডের জন্য সংযুক্ত নোডের একটি লিস্ট

নিচে জাভাস্ক্রিপ্টে অ্যাডজেসেন্সি লিস্ট প্রতিনিধিত্ব:

class Graph {
  constructor() {
    this.adjacencyList = {};
  }

  addVertex(vertex) {
    if (!this.adjacencyList[vertex]) {
      this.adjacencyList[vertex] = [];
    }
  }

  addEdge(vertex1, vertex2) {
    this.adjacencyList[vertex1].push(vertex2);
    this.adjacencyList[vertex2].push(vertex1); // অনিয়মিত গ্রাফের জন্য
  }

  removeEdge(vertex1, vertex2) {
    this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter(v => v !== vertex2);
    this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter(v => v !== vertex1);
  }

  removeVertex(vertex) {
    while (this.adjacencyList[vertex].length) {
      const adjacentVertex = this.adjacencyList[vertex].pop();
      this.removeEdge(vertex, adjacentVertex);
    }
    delete this.adjacencyList[vertex];
  }
}

প্রশ্ন 8:
BFS (ব্রেডথ ফার্স্ট সার্চ) এবং DFS (ডেপথ ফার্স্ট সার্চ) এর মধ্যে পার্থক্য কী?
উত্তর:

  • BFS: লেভেল দ্বারা গ্রাফ বা ট্রি ট্রাভার্স করে। প্রথমে সব শিশু নোড পরিদর্শন করে তারপর তাদের শিশুদের। কিউ ব্যবহার করে।
  • DFS: যতদূর সম্ভব একটি পাথে অগ্রসর হয়, তারপর ব্যাকট্র্যাক করে। স্ট্যাক বা রিকার্সন ব্যবহার করে।

জাভাস্ক্রিপ্টে BFS:

function bfs(graph, startVertex) {
  const visited = {};
  const queue = [startVertex];
  const result = [];
  visited[startVertex] = true;

  while (queue.length) {
    const currentVertex = queue.shift();
    result.push(currentVertex);

    graph.adjacencyList[currentVertex].forEach(neighbor => {
      if (!visited[neighbor]) {
        visited[neighbor] = true;
        queue.push(neighbor);
      }
    });
  }

  return result;
}

জাভাস্ক্রিপ্টে DFS (রিকার্সিভ):

function dfs(graph, startVertex) {
  const result = [];
  const visited = {};

  function traverse(vertex) {
    if (!vertex) return null;
    visited[vertex] = true;
    result.push(vertex);

    graph.adjacencyList[vertex].forEach(neighbor => {
      if (!visited[neighbor]) {
        return traverse(neighbor);
      }
    });
  }

  traverse(startVertex);
  return result;
}

প্রশ্ন 9:
ডাইনামিক প্রোগ্রামিং কী এবং কখন ব্যবহার করা উচিত?
উত্তর:

ডাইনামিক প্রোগ্রামিং হল একটি কৌশল যেখানে জটিল সমস্যাকে ছোট উপসমস্যায় ভাঙা হয় এবং পুনরাবৃত্তি এড়াতে উপসমস্যার সমাধান সংরক্ষণ করা হয়। এটি ব্যবহার করা উচিত যখন:

  • সমস্যাটি অভারল্যাপিং সাবপ্রবলেম আছে
  • ঑প্টিমাল সাবস্ট্রাকচার আছে (সমস্যার সমাধান উপসমস্যার সমাধান থেকে পাওয়া যায়)

ফিবোনাক্কি সিরিজের ডাইনামিক প্রোগ্রামিং বাস্তবায়ন:

// টপ-ডাউন মেমোইজেশন
function fibonacci(n, memo = {}) {
  if (n <= 1) return n;
  if (memo[n]) return memo[n];

  memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
  return memo[n];
}

// বটম-আপ টাবুলেশন
function fibonacciTabulation(n) {
  if (n <= 1) return n;

  const table = [0, 1];
  for (let i = 2; i <= n; i++) {
    table[i] = table[i - 1] + table[i - 2];
  }

  return table[n];
}

প্রশ্ন 10:
আপনি একটি সর্টেড অ্যারেতে একটি উপাদান খুঁজতে O(n) এর পরিবর্তে O(log n) সময় কমপ্লেক্সিটি কীভাবে অর্জন করবেন?
উত্তর:

সর্টেড অ্যারেতে O(log n) সময় কমপ্লেক্সিটি অর্জন করতে আমি বাইনারি সার্চ ব্যবহার করব। লিনিয়ার সার্চ (O(n)) প্রতিটি উপাদান চেক করে, কিন্তু বাইনারি সার্চ প্রতিবার অর্ধেক অ্যারে বাদ দেয়, যা লগারিদমিক সময় কমপ্লেক্সিটি O(log n) দেয়।

প্রশ্ন 11:
রিকার্সন এবং এর সম্পর্কিত সমস্যাগুলি কী?
উত্তর:

রিকার্সন হল যখন একটি ফাংশন নিজেই নিজেকে কল করে। এটি কোড পরিষ্কার রাখতে এবং জটিল সমস্যা সমাধানে সাহায্য করে।
সমস্যাগুলি:

  • স্ট্যাক ওভারফ্লো (যদি বেস কেস না থাকে বা গভীরতা খুব বেশি হয়)
  • মেমোরি ব্যবহার বেশি (প্রতিটি কল স্ট্যাকে স্পেস নেয়)
  • কিছু কেসে অদক্ষ হতে পারে (টেইল কল অপ্টিমাইজেশন না থাকলে)

ফ্যাক্টোরিয়াল গণনার রিকার্সিভ ফাংশন:

function factorial(n) {
  // বেস কেস
  if (n <= 1) {
    return 1;
  }

  // রিকার্সিভ কেস
  return n * factorial(n - 1);
}

প্রশ্ন 12:
প্রাইম নাম্বার চেক করার এলগরিদম?
উত্তর:

প্রাইম নাম্বার চেক করার জন্য, আমরা দেখি নাম্বারটি শুধু 1 এবং নিজের দ্বারা বিভাজ্য কিনা। সবচেয়ে সাধারণ উপায় হল 2 থেকে sqrt(n) পর্যন্ত মডুলাস অপারেশন চেক করা।

function isPrime(num) {
  if (num <= 1) return false;
  if (num <= 3) return true;

  // এটি বাড়তি চেক: যদি 2 বা 3 দ্বারা বিভাজ্য হয়
  if (num % 2 === 0 || num % 3 === 0) return false;

  // 6k±1 পরীক্ষা: সব প্রাইম 6k±1 ফর্ম নেয় (k>0)
  for (let i = 5; i * i <= num; i += 6) {
    if (num % i === 0 || num % (i + 2) === 0) {
      return false;
    }
  }

  return true;
}

প্রশ্ন 13:
ট্রি ডাটা স্ট্রাকচার কী এবং তার কী কী ধরন আছে?
উত্তর:

ট্রি হল একটি নন-লিনিয়ার হায়ারার্কিক্যাল ডাটা স্ট্রাকচার যা নোড এবং কানেকশন নিয়ে গঠিত। কমন ট্রি টাইপ:

  • বাইনারি ট্রি: প্রতিটি নোডের সর্বোচ্চ দুটি চাইল্ড আছে
  • বাইনারি সার্চ ট্রি (BST): বাম চাইল্ড < প্যারেন্ট < ডান চাইল্ড
  • অ্যাভিএল ট্রি: সেলফ-ব্যালেন্সিং BST
  • হিপ: কমপ্লিট বাইনারি ট্রি যেখানে প্যারেন্ট ভ্যালু সব চাইল্ডের চেয়ে ছোট/বড়
  • ট্রাই: ক্যারেক্টারগুলি স্টোর করে যাতে দ্রুত সার্চ করা যায়

জাভাস্ক্রিপ্টে বাইনারি সার্চ ট্রি:

class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(value) {
    const newNode = new Node(value);

    if (!this.root) {
      this.root = newNode;
      return this;
    }

    let current = this.root;

    while (true) {
      if (value === current.value) return undefined; // ডুপ্লিকেট ভ্যালু বাদ দেয়া

      if (value < current.value) {
        if (!current.left) {
          current.left = newNode;
          return this;
        }
        current = current.left;
      } else {
        if (!current.right) {
          current.right = newNode;
          return this;
        }
        current = current.right;
      }
    }
  }

  find(value) {
    if (!this.root) return false;

    let current = this.root;
    let found = false;

    while (current && !found) {
      if (value < current.value) {
        current = current.left;
      } else if (value > current.value) {
        current = current.right;
      } else {
        found = true;
      }
    }

    return found ? current : false;
  }
}

প্রশ্ন 14:
টাইম কমপ্লেক্সিটি এর মূল্যায়ন কিভাবে করবেন?
উত্তর:

টাইম কমপ্লেক্সিটি মূল্যায়ন করার জন্য:

  1. প্রাইমারি অপারেশন গুনুন (যেমন তুলনা, অ্যাসাইনমেন্ট)
  2. লুপ এর প্রভাব বিবেচনা করুন (প্রতি লুপ একটি গুণক)
  3. রিকার্সিভ কল ট্রি এনালাইজ করুন
  4. সর্বোচ্চ বৃদ্ধি হার নিন এবং কনস্ট্যান্ট বাদ দিন
  5. অ্যাসিমটোটিক নোটেশন (বিগ-ও) এ এক্সপ্রেস করুন

উদাহরণ:

// O(n) টাইম কমপ্লেক্সিটি
function linearSearch(arr, target) {
  for (let i = 0; i < arr.length; i++) { // n টাইম
    if (arr[i] === target) return i;     // প্রতিবার 1 তুলনা
  }
  return -1;
}

// O(n²) টাইম কমপ্লেক্সিটি
function bubbleSort(arr) {
  for (let i = 0; i < arr.length; i++) {         // n টাইম
    for (let j = 0; j < arr.length - i - 1; j++) { // (n-i) টাইম
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

প্রশ্ন 15:
স্ট্রিং ম্যানিপুলেশন - প্যালিনড্রোম চেক করার এলগরিদম?
উত্তর:

একটি প্যালিনড্রোম হল এমন একটি শব্দ বা বাক্য যা সামনে থেকে এবং পিছন থেকে পড়লে একই হয়।

function isPalindrome(str) {
  // কেস সেনসিটিভিটি এবং নন-আলফানিউমেরিক ক্যারেক্টার রিমুভ
  const cleanStr = str.toLowerCase().replace(/[^a-z0-9]/g, '');

  // 1. টু পয়েন্টার অ্যাপ্রোচ
  let left = 0;
  let right = cleanStr.length - 1;

  while (left < right) {
    if (cleanStr[left] !== cleanStr[right]) {
      return false;
    }
    left++;
    right--;
  }

প্রশ্ন 16:
হিপ ডাটা স্ট্রাকচার কী এবং এটির প্রকারভেদ কী কী?
উত্তর:

হিপ একটি বিশেষ প্রকারের কমপ্লিট বাইনারি ট্রি যেখানে প্রতিটি নোডের ভ্যালু তার চাইল্ড নোডের ভ্যালুর থেকে ছোট বা বড় হয়।

প্রধান দুই ধরনের হিপ:

মিন হিপ: প্রতিটি প্যারেন্ট নোডের ভ্যালু তার চাইল্ড নোডের ভ্যালুর থেকে ছোট বা সমান। রুট সবচেয়ে ছোট ভ্যালু হবে।

ম্যাক্স হিপ: প্রতিটি প্যারেন্ট নোডের ভ্যালু তার চাইল্ড নোডের ভ্যালুর থেকে বড় বা সমান। রুট সবচেয়ে বড় ভ্যালু হবে।

জাভাস্ক্রিপ্টে মিন হিপ বাস্তবায়ন:

class MinHeap {
  constructor() {
    this.values = [];
  }

  parent(index) {
    return Math.floor((index - 1) / 2);
  }

  leftChild(index) {
    return (2 * index) + 1;
  }

  rightChild(index) {
    return (2 * index) + 2;
  }

  insert(value) {
    this.values.push(value);
    this.bubbleUp();
    return this;
  }

  bubbleUp() {
    let index = this.values.length - 1;
    const element = this.values[index];

    while (index > 0) {
      let parentIndex = this.parent(index);
      let parent = this.values[parentIndex];

      if (element >= parent) break;

      this.values[parentIndex] = element;
      this.values[index] = parent;
      index = parentIndex;
    }
  }

  extractMin() {
    const min = this.values[0];
    const end = this.values.pop();

    if (this.values.length > 0) {
      this.values[0] = end;
      this.sinkDown();
    }

    return min;
  }

  sinkDown() {
    let index = 0;
    const length = this.values.length;
    const element = this.values[0];

    while (true) {
      let leftChildIdx = this.leftChild(index);
      let rightChildIdx = this.rightChild(index);
      let leftChild, rightChild;
      let swap = null;

      if (leftChildIdx < length) {
        leftChild = this.values[leftChildIdx];
        if (leftChild < element) {
          swap = leftChildIdx;
        }
      }

      if (rightChildIdx < length) {
        rightChild = this.values[rightChildIdx];
        if (
          (swap === null && rightChild < element) || 
          (swap !== null && rightChild < leftChild)
        ) {
          swap = rightChildIdx;
        }
      }

      if (swap === null) break;

      this.values[index] = this.values[swap];
      this.values[swap] = element;
      index = swap;
    }
  }
}

প্রশ্ন 17:
ট্রাই (Trie) ডাটা স্ট্রাকচার কী এবং এটি কেন ব্যবহার করা হয়?
উত্তর:

ট্রাই হল একটি ট্রি-বেজড ডাটা স্ট্রাকচার যা স্ট্রিং স্টোর করার জন্য ব্যবহৃত হয়, বিশেষ করে যখন স্ট্রিং-এর প্রিফিক্স সার্চ করতে হয়। এটি ডিকশনারি, অটোকমপ্লিট, স্পেল চেকার ইত্যাদিতে ব্যবহৃত হয়।

সুবিধা:

  • প্রিফিক্স সার্চের জন্য খুব দক্ষ (O(L) যেখানে L হল স্ট্রিং দৈর্ঘ্য)
  • স্ট্রিং মেটচিং অ্যালগরিদমে দ্রুত অপারেশন
  • স্পেস অপ্টিমাইজেশন (সাধারণ প্রিফিক্স শেয়ার করে)

জাভাস্ক্রিপ্টে ট্রাই বাস্তবায়ন:

class TrieNode {
  constructor() {
    this.children = {};
    this.isEndOfWord = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  insert(word) {
    let current = this.root;

    for (let char of word) {
      if (!current.children[char]) {
        current.children[char] = new TrieNode();
      }
      current = current.children[char];
    }

    current.isEndOfWord = true;
  }

  search(word) {
    let current = this.root;

    for (let char of word) {
      if (!current.children[char]) {
        return false;
      }
      current = current.children[char];
    }

    return current.isEndOfWord;
  }

  startsWith(prefix) {
    let current = this.root;

    for (let char of prefix) {
      if (!current.children[char]) {
        return false;
      }
      current = current.children[char];
    }

    return true;
  }
}

প্রশ্ন 18:
ডাইকস্ট্রা শর্টেস্ট পাথ অ্যালগরিদম কী এবং এটি কীভাবে কাজ করে?
উত্তর:

ডাইকস্ট্রা অ্যালগরিদম একটি ভারযুক্ত গ্রাফে একটি সোর্স ভার্টেক্স থেকে সকল ভার্টেক্সের শর্টেস্ট পাথ নির্ণয় করে। এটি "গ্রিডি" অ্যালগরিদম যা সবসময় বর্তমান শর্টেস্ট ডিস্ট্যান্স ভার্টেক্স নির্বাচন করে।

কাজের ধাপ:

  1. সকল ভার্টেক্সকে ইনফিনিটি ডিস্ট্যান্স দিয়ে ইনিশিয়ালাইজ করুন; সোর্স ভার্টেক্সকে 0 দিন
  2. প্রতিটি ভিজিট না করা ভার্টেক্সের মধ্যে সবচেয়ে কম ডিস্ট্যান্সের ভার্টেক্স নির্বাচন করুন
  3. এর প্রতিটি অবিজিটেড নেইবারের জন্য:
  • সোর্স থেকে বর্তমান ভার্টেক্সের মাধ্যমে নেইবারের নতুন ডিস্ট্যান্স ক্যালকুলেট করুন
  • যদি নতুন ডিস্ট্যান্স আগের ডিস্ট্যান্স থেকে কম হয়, তাহলে আপডেট করুন
  1. বর্তমান ভার্টেক্সকে "ভিজিটেড" হিসেবে মার্ক করুন
  2. সব ভার্টেক্স ভিজিট না হওয়া পর্যন্ত 2-4 ধাপ রিপিট করুন

জাভাস্ক্রিপ্টে ডাইকস্ট্রা অ্যালগরিদম:

function dijkstra(graph, start) {
  const distances = {};
  const previous = {};
  const nodes = new PriorityQueue();

  // ইনিশিয়ালাইজেশন
  for (let vertex in graph) {
    if (vertex === start) {
      distances[vertex] = 0;
      nodes.enqueue(vertex, 0);
    } else {
      distances[vertex] = Infinity;
      nodes.enqueue(vertex, Infinity);
    }
    previous[vertex] = null;
  }

  while (!nodes.isEmpty()) {
    let smallest = nodes.dequeue().val;

    if (smallest || distances[smallest] !== Infinity) {
      for (let neighbor in graph[smallest]) {
        // নেইবারের ডিস্ট্যান্স ক্যালকুলেট
        let alt = distances[smallest] + graph[smallest][neighbor];

        // যদি ছোট হয় তাহলে আপডেট
        if (alt < distances[neighbor]) {
          distances[neighbor] = alt;
          previous[neighbor] = smallest;
          nodes.enqueue(neighbor, alt);
        }
      }
    }
  }

  return { distances, previous };
}

// সিম্পল প্রায়োরিটি কিউ বাস্তবায়ন
class PriorityQueue {
  constructor() {
    this.values = [];
  }

  enqueue(val, priority) {
    this.values.push({ val, priority });
    this.sort();
  }

  dequeue() {
    return this.values.shift();
  }

  sort() {
    this.values.sort((a, b) => a.priority - b.priority);
  }

  isEmpty() {
    return this.values.length === 0;
  }
}

প্রশ্ন 19:
মার্জ সর্ট এবং এর টাইম কমপ্লেক্সিটি বিশ্লেষণ করুন।
উত্তর:

মার্জ সর্ট একটি ডিভাইড-এন্ড-কনকার সর্টিং অ্যালগরিদম যা অ্যারেকে ছোট ছোট অংশে ভাগ করে, তাদেরকে সর্ট করে, এবং তারপর মার্জ করে।

টাইম কমপ্লেক্সিটি বিশ্লেষণ:

  • ওয়ার্স্ট কেস: O(n log n)
  • অ্যাভারেজ কেস: O(n log n)
  • বেস্ট কেস: O(n log n)

স্পেস কমপ্লেক্সিটি: O(n)

জাভাস্ক্রিপ্টে মার্জ সর্ট:

function mergeSort(arr) {
  if (arr.length <= 1) return arr;

  // ডিভাইড স্টেপ
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  // কনকার স্টেপ (মার্জ)
  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let leftIndex = 0;
  let rightIndex = 0;

  // মার্জ দুটি সর্টেড অ্যারে
  while (leftIndex < left.length && rightIndex < right.length) {
    if (left[leftIndex] < right[rightIndex]) {
      result.push(left[leftIndex]);
      leftIndex++;
    } else {
      result.push(right[rightIndex]);
      rightIndex++;
    }
  }

  // অবশিষ্ট এলিমেন্ট যোগ করুন
  return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

প্রশ্ন 20:
স্লাইডিং উইন্ডো প্যাটার্ন কী এবং এটি কখন ব্যবহার করা হয়?
উত্তর:

স্লাইডিং উইন্ডো প্যাটার্ন একটি টেকনিক যা সাবঅ্যারে বা সাবস্ট্রিং-এর উপর অপারেশনের জন্য ব্যবহৃত হয়। এটি একটি "উইন্ডো" যা ডাটা সেটের উপর ধীরে ধীরে "স্লাইড" করে, যা O(n) টাইম কমপ্লেক্সিটি অর্জন করতে সাহায্য করে।

কখন ব্যবহার করবেন:

  • ক-সাইজের সাবঅ্যারে কোন প্রপার্টি ক্যালকুলেট করতে
  • ডাটা স্ট্রিমের উপর মুভিং অ্যাভারেজ ক্যালকুলেট করতে
  • সাবস্ট্রিং সার্চ

জাভাস্ক্রিপ্টে স্লাইডিং উইন্ডো উদাহরণ - k-সাইজের সাবঅ্যারের ম্যাক্সিমাম সাম:

function maxSubarraySum(arr, k) {
  if (arr.length < k) return null;

  // প্রথম উইন্ডোর সাম
  let maxSum = 0;
  for (let i = 0; i < k; i++) {
    maxSum += arr[i];
  }

  let currentSum = maxSum;

  // উইন্ডো স্লাইড করুন
  for (let i = k; i < arr.length; i++) {
    currentSum = currentSum - arr[i - k] + arr[i]; // উইন্ডো শিফট
    maxSum = Math.max(maxSum, currentSum);
  }

  return maxSum;
}

প্রশ্ন 21:
টপোলজিক্যাল সর্ট কী এবং এটি কীভাবে কাজ করে?
উত্তর:

টপোলজিক্যাল সর্ট হল ডিরেক্টেড অ্যাসাইক্লিক গ্রাফ (DAG) এর ভার্টেক্স গুলিকে সাজানোর একটি লিনিয়ার ক্রম, যেখানে প্রতিটি ডিরেক্টেড এজ u → v এর জন্য, u আগে আসে v-এর থেকে।

উদাহরণ: কোর্স স্কেজুলিং, প্যাকেজ ডিপেন্ডেন্সি রিজলভিং, বিল্ড সিস্টেম

কাজের ধাপ:

  1. ইনকামিং এজ কাউন্ট (ইনডিগ্রি) ট্র্যাক করুন
  2. 0 ইনডিগ্রি ভার্টেক্স কিউতে যোগ করুন
  3. প্রসেস করুন:
  • ভার্টেক্স রিমুভ করুন
  • রেজাল্টে যোগ করুন
  • চাইল্ড ভার্টেক্সের ইনডিগ্রি কমান
  • যদি কোন চাইল্ডের ইনডিগ্রি 0 হয়, কিউতে যোগ করুন
  1. কিউ খালি না হওয়া পর্যন্ত 3 রিপিট করুন

জাভাস্ক্রিপ্টে টপোলজিক্যাল সর্ট:

function topologicalSort(graph) {
  const result = [];
  const visited = {};
  const temp = {}; // সাইকেল ডিটেকট করার জন্য

  function dfs(node) {
    // সাইকেল চেক
    if (temp[node]) {
      throw new Error("Graph has a cycle, cannot perform topological sort");
    }

    if (!visited[node]) {
      temp[node] = true;

      // নেইবারদের ভিজিট
      const neighbors = graph[node] || [];
      for (let i = 0; i < neighbors.length; i++) {
        dfs(neighbors[i]);
      }

      visited[node] = true;
      temp[node] = false;

      // রেজাল্টের শুরুতে যোগ করুন
      result.unshift(node);
    }
  }

  // সব নোড প্রসেস
  for (let node in graph) {
    if (!visited[node]) {
      dfs(node);
    }
  }

  return result;
}

প্রশ্ন 22:
LRU কেশ ইমপ্লিমেন্টেশন কীভাবে হয়?
উত্তর:

LRU (Least Recently Used) কেশ হল একটি কেশিং সিস্টেম যা লিমিটেড সাইজের কেশ মেইনটেইন করে এবং যখন নতুন আইটেম যোগ করতে হয় এবং কেশ ফুল হয়, তখন সবচেয়ে কম সম্প্রতি ব্যবহৃত আইটেম বাদ দেয়।

LRU কেশের ইমপ্লিমেন্টেশন জাভাস্ক্রিপ্টে:

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map(); // হ্যাশম্যাপ ফর O(1) অ্যাকসেস
    this.head = { key: 0, value: 0 };  // ডাবলি লিংকড লিস্ট হেড
    this.tail = { key: 0, value: 0 };  // ডাবলি লিংকড লিস্ট টেইল
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  // নোড রিমুভ
  removeNode(node) {
    const prev = node.prev;
    const next = node.next;
    prev.next = next;
    next.prev = prev;
  }

  // লিস্টের শুরুতে নোড যোগ করুন (MRU পজিশন)
  addToHead(node) {
    node.next = this.head.next;
    node.prev = this.head;
    this.head.next.prev = node;
    this.head.next = node;
  }

  get(key) {
    if (this.cache.has(key)) {
      // কী পাওয়া গেছে, MRU হিসেবে মুভ করুন
      const node = this.cache.get(key);
      this.removeNode(node);
      this.addToHead(node);
      return node.value;
    }
    return -1; // কী পাওয়া যায়নি
  }

  put(key, value) {
    // আগে থেকেই আছে কিনা চেক করুন
    if (this.cache.has(key)) {
      const node = this.cache.get(key);
      node.value = value;
      this.removeNode(node);
      this.addToHead(node);
    } else {
      // LRU আইটেম রিমুভ করুন যদি ক্যাপাসিটি ফুল হয়
      if (this.cache.size === this.capacity) {
        // টেইলের আগের (LRU) নোড রিমুভ করুন
        const lru = this.tail.prev;
        this.removeNode(lru);
        this.cache.delete(lru.key);
      }

      // নতুন নোড তৈরি এবং হেডে যোগ করুন
      const newNode = { key, value, prev: null, next: null };
      this.cache.set(key, newNode);
      this.addToHead(newNode);
    }
  }
}

প্রশ্ন 23:
ইউনিয়ন-ফাইন্ড ডাটা স্ট্রাকচার কী এবং এটি কোথায় ব্যবহার করা হয়?
উত্তর:

ইউনিয়ন-ফাইন্ড (ডিসজয়েন্ট সেট) একটি ডাটা স্ট্রাকচার যা একসাথে যুক্ত উপাদান (কানেক্টেড কম্পোনেন্ট) ট্র্যাক করতে ব্যবহৃত হয়। এর দুটি মূল অপারেশন:

  • ফাইন্ড: কোন উপাদান কোন সেটের অন্তর্ভুক্ত তা নির্ধারণ করে
  • ইউনিয়ন: দুটি সেটকে একীভূত করে

ব্যবহার:

  • ক্রুস্কাল মিনিমাম স্প্যানিং ট্রি অ্যালগরিদম
  • সাইকেল ডিটেকশন
  • নেটওয়ার্ক কানেকশন প্রবলেম

জাভাস্ক্রিপ্টে ইউনিয়ন-ফাইন্ড:

class UnionFind {
  constructor(size) {
    this.parent = Array(size).fill().map((_, i) => i); // প্রত্যেক নোড নিজের প্যারেন্ট
    this.rank = Array(size).fill(0); // ইউনিয়ন বাই র‍্যাঙ্ক অপ্টিমাইজেশন
  }

  // x-এর রুট নোড খুঁজে বের করুন
  find(x) {
    if (this.parent[x] !== x) {
      // পাথ কম্প্রেশন - সরাসরি রুটের সাথে লিঙ্ক করুন
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x];
  }

  // x এবং y সম্বলিত সেট ইউনিয়ন করুন
  union(x, y) {
    const rootX = this.find(x);
    const rootY = this.find(y);

    if (rootX === rootY) return false; // ইতিমধ্যেই একই সেটে আছে

    // র‍্যাঙ্ক দ্বারা ইউনিয়ন
    if (this.rank[rootX] < this.rank[rootY]) {
      this.parent[rootX] = rootY;
    } else if (this.rank[rootX] > this.rank[rootY]) {
      this.parent[rootY] = rootX;
    } else {
      this.parent[rootY] = rootX;
      this.rank[rootX]++;
    }

    return true;
  }

  // x এবং y একই সেটে আছে কিনা
  connected(x, y) {
    return this.find(x) === this.find(y);
  }
}

প্রশ্ন 24:
কাউন্টিং সর্ট কী এবং এটি কখন বেস্ট?
উত্তর:

কাউন্টিং সর্ট একটি নন-কম্পারিজন বেজড সর্টিং অ্যালগরিদম যা ইনপুট ভ্যালুর ফ্রিকোয়েন্সি ট্র্যাক করে এবং সেই কাউন্ট ব্যবহার করে এলিমেন্টগুলো সর্ট করে।

বেস্ট যখন:

ভ্যালুর রেঞ্জ খুব বেশি বড় নয় (যেমন, 0-1000 মধ্যে)
উপাদানগুলি ইন্টিজার বা ইন্টিজারে ম্যাপ করা যায় এমন
সর্টিং স্টেবল প্রয়োজন

টাইম কমপ্লেক্সিটি: O(n + k) যেখানে n হল উপাদান সংখ্যা এবং k হল রেঞ্জ

জাভাস্ক্রিপ্টে কাউন্টিং সর্ট:

function countingSort(arr, min, max) {
  const count = {};

  // কাউন্ট ইনিশিয়ালাইজ
  for (let i = min; i <= max; i++) {
    count[i] = 0;
  }

  // কাউন্ট ফ্রিকোয়েন্সি
  for (let i = 0; i < arr.length; i++) {
    count[arr[i]]++;
  }

  // রিকনস্ট্রাক্ট অ্যারে
  let sortedIndex = 0;
  for (let i = min; i <= max; i++) {
    while (count[i] > 0) {
      arr[sortedIndex++] = i;
      count[i]--;
    }
  }

  return arr;
}

// উদাহরণ
const array = [4, 2, 2, 8, 3, 3, 1];
const sorted = countingSort(array, 1, 8); // [1, 2, 2, 3, 3, 4, 8]

প্রশ্ন 25:
দুটি লিংকড লিস্টে সাইকেল ডিটেকশন কীভাবে করবেন?
উত্তর:

সাইকেল ডিটেকশনের জন্য ("Floyd's Cycle-Finding Algorithm" বা "Tortoise and Hare Algorithm") ফ্লয়েড সাইকেল-ফাইন্ডিং অ্যালগরিদম (টর্টোইজ এন্ড হেয়ার) হল সবচেয়ে জনপ্রিয় পদ্ধতি। এখানে দুই পয়েন্টার (একটি ধীর, একটি দ্রুত) ব্যবহার করে সাইকেল সনাক্ত করা হয়।

জাভাস্ক্রিপ্টে সাইকেল ডিটেকশন:

// Node class for a basic linked list
class Node {
    constructor(value) {
        this.value = value;
        this.next = null;
    }
}

/**
 * Detects if a linked list has a cycle using Floyd's Cycle-Finding Algorithm
 * @param {Node} head - The head of the linked list
 * @return {boolean} - True if there is a cycle, false otherwise
 */
function hasCycle(head) {
    if (!head || !head.next) {
        return false; // Empty list or single node can't have a cycle
    }

    let slow = head; // Tortoise - moves one step at a time
    let fast = head; // Hare - moves two steps at a time

    while (fast && fast.next) {
        slow = slow.next;        // Move one step
        fast = fast.next.next;   // Move two steps

        // If there is a cycle, slow and fast will eventually meet
        if (slow === fast) {
            return true;
        }
    }

    // If fast reaches the end of the list, there's no cycle
    return false;
}

/**
 * Finds the intersection point of two linked lists, if they intersect
 * This can also detect if one or both lists have cycles
 * @param {Node} headA - Head of first linked list
 * @param {Node} headB - Head of second linked list
 * @return {Node|null} - The intersection node or null if no intersection
 */
function getIntersectionNode(headA, headB) {
    // First check if either list has a cycle
    const hasCycleA = hasCycle(headA);
    const hasCycleB = hasCycle(headB);

    // If only one list has a cycle, they cannot intersect
    if (hasCycleA !== hasCycleB) {
        return null;
    }

    // If both lists have cycles, it's more complex
    if (hasCycleA && hasCycleB) {
        return findIntersectionWithCycles(headA, headB);
    }

    // If neither has cycles, use the standard approach
    return findIntersectionNoCycles(headA, headB);
}

/**
 * Finds intersection of two acyclic linked lists
 * @param {Node} headA - Head of first linked list
 * @param {Node} headB - Head of second linked list
 * @return {Node|null} - The intersection node or null if no intersection
 */
function findIntersectionNoCycles(headA, headB) {
    if (!headA || !headB) return null;

    let pointerA = headA;
    let pointerB = headB;

    // If the lists are of different lengths, this approach will align the pointers
    while (pointerA !== pointerB) {
        pointerA = pointerA ? pointerA.next : headB;
        pointerB = pointerB ? pointerB.next : headA;
    }

    return pointerA; // Either the intersection point or null
}

/**
 * Finds the start of a cycle in a linked list
 * @param {Node} head - The head of the linked list
 * @return {Node|null} - The start node of the cycle or null if no cycle
 */
function detectCycleStart(head) {
    if (!head || !head.next) return null;

    let slow = head;
    let fast = head;
    let hasCycle = false;

    // Find meeting point inside the cycle
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;

        if (slow === fast) {
            hasCycle = true;
            break;
        }
    }

    if (!hasCycle) return null;

    // Reset slow to head and move both pointers at same speed
    slow = head;
    while (slow !== fast) {
        slow = slow.next;
        fast = fast.next;
    }

    return slow; // This is the start of the cycle
}

/**
 * Finds intersection of two linked lists that may have cycles
 * @param {Node} headA - Head of first linked list
 * @param {Node} headB - Head of second linked list
 * @return {Node|null} - The intersection node or null if no intersection
 */
function findIntersectionWithCycles(headA, headB) {
    // Find the start of cycles in both lists
    const cycleStartA = detectCycleStart(headA);
    const cycleStartB = detectCycleStart(headB);

    // Check if the cycles are the same by traversing one cycle
    let pointer = cycleStartA.next;
    while (pointer !== cycleStartA) {
        if (pointer === cycleStartB) {
            // The cycles are the same, return either cycle start
            return cycleStartA;
        }
        pointer = pointer.next;
    }

    // If we get here and cycles are different, check if cycleStartB is in cycleA
    if (cycleStartB === cycleStartA) {
        return cycleStartA; // Same cycle start point
    }

    // The lists don't intersect or have separate cycles
    return null;
}

// Example usage
function createTestLists() {
    // Create a list with a cycle
    const node1 = new Node(1);
    const node2 = new Node(2);
    const node3 = new Node(3);
    const node4 = new Node(4);
    const node5 = new Node(5);

    node1.next = node2;
    node2.next = node3;
    node3.next = node4;
    node4.next = node5;
    node5.next = node3; // Create cycle pointing to node3

    // Create a second list that joins the first list at node3
    const nodeA = new Node('A');
    const nodeB = new Node('B');
    nodeA.next = nodeB;
    nodeB.next = node3; // Connect to the cycle of the first list

    return { listA: node1, listB: nodeA };
}

// Test the functions
const { listA, listB } = createTestLists();
console.log("List A has cycle:", hasCycle(listA)); // Should be true
console.log("List B has cycle:", hasCycle(listB)); // Should be true

const intersection = getIntersectionNode(listA, listB);
console.log("Intersection exists:", !!intersection); // Should be true
if (intersection) {
    console.log("Intersection at node with value:", intersection.value); // Should be 3
}

ব্যাখ্যা:

  1. হ্যাজ সাইকেল (hasCycle) ফাংশন: Floyd's Cycle-Finding Algorithm ব্যবহার করে একটি লিংকড লিস্টে সাইকেল আছে কিনা তা নির্ধারণ করে। এটি "tortoise and hare" (কচ্ছপ ও খরগোশ) এলগোরিদম নামেও পরিচিত - যেখানে:
  • slow পয়েন্টার (কচ্ছপ) এক ধাপ এগোয়
  • fast পয়েন্টার (খরগোশ) দুই ধাপ এগোয়
  • যদি সাইকেল থাকে, এরা অবশ্যই কোন একসময় দেখা করবে
  1. দুটি লিংকড লিস্টের ইন্টারসেকশন ডিটেকশন: getIntersectionNode ফাংশন দুটি লিংকড লিস্টের মধ্যে ইন্টারসেকশন পয়েন্ট খুঁজে বের করে। এটি তিনটি কেস হ্যান্ডেল করে:
  • দুটি লিস্টেই সাইকেল নেই
  • শুধুমাত্র একটি লিস্টে সাইকেল আছে
  • দুটি লিস্টেই সাইকেল আছে
  1. সাইকেলের শুরু খুঁজে বের করা: detectCycleStart ফাংশন সাইকেলের শুরুর নোডটি খুঁজে বের করে।

  2. টেস্ট কেস: একটি টেস্ট কেসও দেওয়া আছে যেখানে দুটি লিংকড লিস্ট একটি সাধারণ নোডে মিলিত হয় যেটি একটি সাইকেলের অংশ।

এই কোডটি সব ধরণের কেস হ্যান্ডেল করতে পারে - সাইকেল আছে কিনা, সাইকেল থাকলে তার শুরু কোথায়, এবং দুটি লিস্ট ইন্টারসেক্ট করে কিনা।

প্রশ্ন 26:
কয়েন চেঞ্জ প্রবলেম (Coin Change Problem) কীভাবে সলভ করবেন?
উত্তর:

কয়েন চেঞ্জ প্রবলেম হল একটি ক্লাসিক ডাইনামিক প্রোগ্রামিং সমস্যা। এটি সর্বনিম্ন সংখ্যক কয়েন ব্যবহার করে একটি নির্দিষ্ট পরিমাণ তৈরি করার সমস্যা।

function minCoins(coins, amount) {
  // dp অ্যারে ইনিশিয়ালাইজ করি
  // dp[i] = amount i তৈরি করতে সর্বনিম্ন কয়েন সংখ্যা
  const dp = Array(amount + 1).fill(Infinity);
  dp[0] = 0; // 0 অ্যামাউন্ট এর জন্য 0 কয়েন লাগে

  for (let coin of coins) {
    for (let i = coin; i <= amount; i++) {
      dp[i] = Math.min(dp[i], dp[i - coin] + 1);
    }
  }

  return dp[amount] === Infinity ? -1 : dp[amount];
}

// উদাহরণ
console.log(minCoins([1, 2, 5], 11)); // আউটপুট: 3 (5 + 5 + 1)

প্রশ্ন 27:
লোয়েস্ট কমন অ্যানসেস্টর (LCA) কীভাবে খুঁজে বের করবেন?
উত্তর:

লোয়েস্ট কমন অ্যানসেস্টর (LCA) হল দুটি নোডের জন্য সবচেয়ে নিচের লেভেলের কমন প্যারেন্ট। এটি ট্রি প্রবলেমে অফটেন ব্যবহৃত হয়।

class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

// বাইনারি ট্রিতে LCA (রিকার্সিভ অ্যাপ্রোচ)
function lowestCommonAncestor(root, p, q) {
  // বেস কেসেস
  if (!root) return null;
  if (root === p || root === q) return root;

  // লেফট ও রাইট সাবট্রি সার্চ করি
  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);

  // যদি উভয় সাবট্রিতে ভ্যালু পাওয়া যায়, তাহলে বর্তমান নোড LCA
  if (left && right) return root;

  // অন্যথায় যে সাবট্রিতে একটি ভ্যালু পাওয়া গেছে তার রেজাল্ট রিটার্ন করি
  return left ? left : right;
}

// উদাহরণ ট্রি তৈরি
//       3
//     /   \
//    5     1
//   / \   / \
//  6   2 0   8
//     / \
//    7   4

const root = new TreeNode(3);
root.left = new TreeNode(5);
root.right = new TreeNode(1);
root.left.left = new TreeNode(6);
root.left.right = new TreeNode(2);
root.right.left = new TreeNode(0);
root.right.right = new TreeNode(8);
root.left.right.left = new TreeNode(7);
root.left.right.right = new TreeNode(4);

const p = root.left; // 5
const q = root.right; // 1
console.log(lowestCommonAncestor(root, p, q).value); // আউটপুট: 3

const p2 = root.left; // 5
const q2 = root.left.right.right; // 4
console.log(lowestCommonAncestor(root, p2, q2).value); // আউটপুট: 5

প্রশ্ন 28:
Union-Find (Disjoint Set) ডাটা স্ট্রাকচার কী এবং এর ব্যবহার?
উত্তর:

Union-Find (Disjoint Set) হল একটি ডাটা স্ট্রাকচার যা এলিমেন্টের একাধিক সেট ম্যানেজ করে এবং কোন দুটি এলিমেন্ট একই সেটে আছে কিনা তা দ্রুত বলতে পারে।

এটি নিম্নলিখিত অপারেশন সাপোর্ট করে:

  • Find: কোন এলিমেন্ট কোন সেটে আছে তা খুঁজে বের করা
  • Union: দুটি সেট একত্রিত করা

Union-Find ব্যবহারের কাজ:

  • Kruskal's MST অ্যালগরিদম
  • সাইকেল ডিটেকশন
  • নেটওয়ার্ক কানেকশন
class UnionFind {
  constructor(size) {
    this.parent = Array(size).fill().map((_, i) => i); // প্রতিটি নোড নিজের প্যারেন্ট
    this.rank = Array(size).fill(0); // র‍্যাঙ্ক/হাইট অপটিমাইজেশন
    this.count = size; // ডিসজয়েন্ট সেট সংখ্যা
  }

  // কোন এলিমেন্ট কোন সেটে আছে তা খুঁজে বের করে
  find(x) {
    // পাথ কম্প্রেশন অপ্টিমাইজেশন
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x];
  }

  // দুটি সেট একত্রিত করে
  union(x, y) {
    const rootX = this.find(x);
    const rootY = this.find(y);

    // আগে থেকেই একই সেটে থাকলে
    if (rootX === rootY) {
      return false;
    }

    // র‍্যাঙ্ক বাই ইউনিয়ন অপ্টিমাইজেশন
    if (this.rank[rootX] < this.rank[rootY]) {
      this.parent[rootX] = rootY;
    } else if (this.rank[rootX] > this.rank[rootY]) {
      this.parent[rootY] = rootX;
    } else {
      this.parent[rootY] = rootX;
      this.rank[rootX]++;
    }

    this.count--; // সেট সংখ্যা কমে যায়
    return true;
  }

  // একই সেটে আছে কিনা চেক করে
  connected(x, y) {
    return this.find(x) === this.find(y);
  }

  // মোট ডিসজয়েন্ট সেট সংখ্যা
  getCount() {
    return this.count;
  }
}

// উদাহরণ
const uf = new UnionFind(10);
uf.union(1, 2);
uf.union(2, 5);
uf.union(5, 6);
uf.union(3, 8);
uf.union(8, 9);

console.log(uf.connected(1, 5)); // আউটপুট: true
console.log(uf.connected(5, 9)); // আউটপুট: false
console.log(uf.getCount()); // আউটপুট: 5 (5টি ডিসজয়েন্ট সেট)

প্রশ্ন 29:
সেগমেন্ট ট্রি কী এবং কেন ব্যবহার করা হয়?
উত্তর:

সেগমেন্ট ট্রি হল একটি ট্রি ডাটা স্ট্রাকচার যা অ্যারের রেঞ্জ কোয়েরি হ্যান্ডল করতে ব্যবহার করা হয়, যেমন রেঞ্জ সাম, মিনিমাম, ম্যাক্সিমাম ইত্যাদি। এটি রেঞ্জ কোয়েরি O(log n) সময়ে সম্পন্ন করতে পারে।

class SegmentTree {
  constructor(arr) {
    this.arr = arr;
    this.n = arr.length;
    // সেগমেন্ট ট্রি আকার প্রায় 4 * n
    this.tree = new Array(4 * this.n).fill(0);
    this.buildTree(0, 0, this.n - 1);
  }

  // রিকার্সিভলি ট্রি বিল্ড করি
  buildTree(node, start, end) {
    // লিফ নোড
    if (start === end) {
      this.tree[node] = this.arr[start];
      return;
    }

    const mid = Math.floor((start + end) / 2);
    const leftChild = 2 * node + 1;
    const rightChild = 2 * node + 2;

    // বাম ও ডান সাবট্রি বিল্ড করি
    this.buildTree(leftChild, start, mid);
    this.buildTree(rightChild, mid + 1, end);

    // পেয়ারেন্ট নোড আপডেট করি
    this.tree[node] = this.tree[leftChild] + this.tree[rightChild];
  }

  // রেঞ্জ সাম কোয়েরি
  querySum(left, right) {
    return this._querySum(0, 0, this.n - 1, left, right);
  }

  _querySum(node, start, end, left, right) {
    // রেঞ্জের বাইরে
    if (start > right || end < left) {
      return 0;
    }

    // কমপ্লিট রেঞ্জ কভার করছে
    if (left <= start && end <= right) {
      return this.tree[node];
    }

    // পার্শিয়াল ওভারল্যাপ
    const mid = Math.floor((start + end) / 2);
    const leftChild = 2 * node + 1;
    const rightChild = 2 * node + 2;

    const leftSum = this._querySum(leftChild, start, mid, left, right);
    const rightSum = this._querySum(rightChild, mid + 1, end, left, right);

    return leftSum + rightSum;
  }

  // একক উপাদান আপডেট
  update(index, newValue) {
    const diff = newValue - this.arr[index];
    this.arr[index] = newValue;
    this._update(0, 0, this.n - 1, index, diff);
  }

  _update(node, start, end, index, diff) {
    // আপডেট প্রয়োজন নেই
    if (index < start || index > end) {
      return;
    }

    // নোড আপডেট করি
    this.tree[node] += diff;

    // লিফ নোড না হলে রিকার্সিভলি আপডেট করি
    if (start !== end) {
      const mid = Math.floor((start + end) / 2);
      const leftChild = 2 * node + 1;
      const rightChild = 2 * node + 2;

      this._update(leftChild, start, mid, index, diff);
      this._update(rightChild, mid + 1, end, index, diff);
    }
  }
}

// উদাহরণ
const arr = [1, 3, 5, 7, 9, 11];
const segTree = new SegmentTree(arr);

console.log(segTree.querySum(1, 3)); // আউটপুট: 15 (3+5+7)
segTree.update(2, 10);
console.log(segTree.querySum(1, 3)); // আউটপুট: 20 (3+10+7)

প্রশ্ন 30:
সাবওয়ে সার্জ (Subarray Sum) কীভাবে বের করবেন?
উত্তর:

সাবওয়ে সার্জ প্রবলেম হল একটি অ্যারের মধ্যে এমন একটি কন্টিনিউয়াস সাবওয়ে খোঁজা যার সাম একটি নির্দিষ্ট ভ্যালু।

// সাবওয়ে সাম ইকুয়ালস কে (k)
function subarraySum(nums, k) {
  const prefixSums = new Map();
  prefixSums.set(0, 1); // শূন্য সামের 1টি সাবওয়ে আছে (খালি সাবওয়ে)

  let count = 0;
  let currentSum = 0;

  for (let num of nums) {
    currentSum += num;

    // যদি currentSum - k আগে দেখা থাকে, তাহলে k সামের সাবওয়ে আছে
    if (prefixSums.has(currentSum - k)) {
      count += prefixSums.get(currentSum - k);
    }

    // প্রিফিক্স সাম অ্যাড করি
    prefixSums.set(currentSum, (prefixSums.get(currentSum) || 0) + 1);
  }

  return count;
}

// উদাহরণ
console.log(subarraySum([1, 1, 1], 2)); // আউটপুট: 2
console.log(subarraySum([1, 2, 3, 4, 5], 9)); // আউটপুট: 2

প্রশ্ন 31:
ডাইকস্ট্রা এলগরিদম কী এবং কীভাবে ইমপ্লিমেন্ট করবেন?
উত্তর:

ডাইকস্ট্রা এলগরিদম হল গ্রাফে সিঙ্গেল সোর্স শর্টেস্ট পাথ খুঁজে বের করার একটি এলগরিদম। এটি গ্রিডি এলগরিদম যা প্রতিবার সবচেয়ে কম ওজনের পাথ নির্বাচন করে।

function dijkstra(graph, startNode, endNode) {
  // প্রায়োরিটি কিউ ব্যবহার করা ভালো, এখানে সিম্পল অ্যারে দিয়ে দেখাচ্ছি
  const distances = {};
  const previous = {};
  const nodes = new Set();

  // ইনিশিয়ালাইজেশন
  for (let vertex in graph) {
    if (vertex === startNode) {
      distances[vertex] = 0;
    } else {
      distances[vertex] = Infinity;
    }
    previous[vertex] = null;
    nodes.add(vertex);
  }

  // সবচেয়ে কম দূরত্বের নোড খুঁজে বের করা
  function extractMinimum() {
    let minVertex = null;

    for (let vertex of nodes) {
      if (minVertex === null || distances[vertex] < distances[minVertex]) {
        minVertex = vertex;
      }
    }

    return minVertex;
  }

  // যতক্ষণ নোড আছে
  while (nodes.size > 0) {
    const current = extractMinimum();

    // যদি ডেস্টিনেশন পৌঁছে যাই বা অসীম দূরত্ব পাই
    if (current === endNode || distances[current] === Infinity) {
      break;
    }

    nodes.delete(current);

    // প্রতিটি প্রতিবেশী নোড এর জন্য
    for (let neighbor in graph[current]) {
      // আমরা current থেকে neighbor এ যাওয়ার দূরত্ব
      const alt = distances[current] + graph[current][neighbor];

      // যদি নতুন পাথ ছোট হয়
      if (alt < distances[neighbor]) {
        distances[neighbor] = alt;
        previous[neighbor] = current;
      }
    }
  }

  // শর্টেস্ট পাথ রিকনস্ট্রাক্ট
  const path = [];
  let current = endNode;

  while (current !== null) {
    path.unshift(current);
    current = previous[current];
  }

  return {
    distance: distances[endNode],
    path: path
  };
}

// উদাহরণ
const graph = {
  A: { B: 4, C: 2 },
  B: { A: 4, C: 1, D: 5 },
  C: { A: 2, B: 1, D: 8, E: 10 },
  D: { B: 5, C: 8, E: 2 },
  E: { C: 10, D: 2 }
};

console.log(dijkstra(graph, 'A', 'E'));
// আউটপুট: { distance: 9, path: ['A', 'C', 'B', 'D', 'E'] }

প্রশ্ন 32:
ট্রাই (Trie) ডাটা স্ট্রাকচার কী এবং কেন ব্যবহার করা হয়?
উত্তর:

ট্রাই হল একটি ট্রি-বেসড ডাটা স্ট্রাকচার যা শব্দের সমষ্টি স্টোর করতে ব্যবহৃত হয়। এটা পরিচিত "প্রিফিক্স ট্রি" হিসেবেও। এটি শব্দ খোঁজা, অটোকমপ্লিট, স্পেল চেকিং জন্য দক্ষ।

ট্রাই ব্যবহারের কারণ:

  • প্রিফিক্স সার্চ O(k) সময়ে করতে পারে, যেখানে k হল প্রিফিক্সের দৈর্ঘ্য
  • স্ট্রিং কালেকশন স্টোর করার স্পেস-এফিশিয়েন্ট উপায়
  • দ্রুত অটোকমপ্লিট সাজেশন
class TrieNode {
  constructor() {
    this.children = {};
    this.isEndOfWord = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  // শব্দ ইনসার্ট
  insert(word) {
    let current = this.root;

    for (let char of word) {
      if (!current.children[char]) {
        current.children[char] = new TrieNode();
      }
      current = current.children[char];
    }

    current.isEndOfWord = true;
  }

  // শব্দ সার্চ
  search(word) {
    let current = this.root;

    for (let char of word) {
      if (!current.children[char]) {
        return false;
      }
      current = current.children[char];
    }

    return current.isEndOfWord;
  }

  // প্রিফিক্স সার্চ
  startsWith(prefix) {
    let current = this.root;

    for (let char of prefix) {
      if (!current.children[char]) {
        return false;
      }
      current = current.children[char];
    }

    return true;
  }

  // প্রিফিক্স দিয়ে শব্দ সাজেশন
  findWordsWithPrefix(prefix) {
    const result = [];
    let current = this.root;

    // প্রিফিক্স নোড এ যাই
    for (let char of prefix) {
      if (!current.children[char]) {
        return result;
      }
      current = current.children[char];
    }

    // এই নোড থেকে DFS করে সব শব্দ খুঁজি
    this._collectWords(current, prefix, result);
    return result;
  }

  _collectWords(node, prefix, result) {
    if (node.isEndOfWord) {
      result.push(prefix);
    }

    for (let char in node.children) {
      this._collectWords(node.children[char], prefix + char, result);
    }
  }
}

// উদাহরণ
const trie = new Trie();
trie.insert("apple");
trie.insert("app");
trie.insert("application");
console.log(trie.search("app")); // true
console.log(trie.startsWith("ap")); // true
console.log(trie.findWordsWithPrefix("app")); // ["app", "apple", "application"]