রিভার্সিং লিংকড লিস্ট (Reversing a Linked List)
ধরুন গুপ্তধন খোঁজার একটা ম্যাপ: ক্লু ১ → ক্লু ২ → ক্লু ৩ → গুপ্তধন। এবার এটাকে পুরোপুরি উল্টে দিন: গুপ্তধন → ক্লু ৩ → ক্লু ২ → ক্লু ১। অর্থাৎ, যে তীর চিহ্নগুলো আগে সামনের দিকে নির্দেশ করত, সেগুলো এখন পেছনের দিকে পয়েন্ট করছে। আর একদম শেষের নোডটা হয়ে গেছে নতুন হেড (head) বা শুরুর মাথা।
একটা লিংকড লিস্টকে উল্টে দেওয়া (reverse করা) হলো এই ডেটা স্ট্রাকচারের সবচেয়ে মৌলিক বা বেসিক একটা কাজ। ইন্টারভিউতে এটা খুব ফেমাস একটা প্রশ্ন, কারণ এটা দিয়ে যাচাই করা হয় আপনি পয়েন্টারের (pointer) খেলাটা ঠিকমতো বোঝেন কি না। খুশির খবর হলো: কাজটা দেখতে যতটা কঠিন মনে হয়, আসলে ততটা না।
ইটারেটিভ পদ্ধতি (লুপ চালিয়ে)
লিস্টের শুরু থেকে শেষ পর্যন্ত একবার হেঁটে যান, আর যানয়ার পথে প্রতিটা পয়েন্টারের ডিরেকশন উল্টে দিন। এর জন্য আপনার শুধু তিনটা ভেরিয়েবল লাগবে:
prev— একটু আগে যে নোডের কাজ শেষ করেছেন (শুরুতে এটাnullথাকে)curr— বর্তমানে যে নোডে দাঁড়িয়ে আছেন (শুরুতে এটাheadথাকে)next—curr.next-কে চেঞ্জ করার আগে সেটাকে সেভ করে রাখার একটা টেম্পোরারি বা অস্থায়ী জায়গা
প্রতিটা পদক্ষেপে আপনাকে যা করতে হবে:
next = curr.nextলিখে সামনের নোডের ঠিকানাটা সেভ করুন (তা না হলে লিস্টের বাকি অংশ হারিয়ে যাবে)।- পয়েন্টার উল্টে দিন:
curr.next = prev(অর্থাৎ, সামনের দিকে না তাকিয়ে এখন পেছনের দিকে তাকাও)। - এক ধাপ সামনে এগোন:
prev = curr,curr = next।
যখন curr-এর মান null বা ফাঁকা হয়ে যাবে, তার মানে সব নোডের কাজ শেষ। তখন prev-ই হবে আপনার রিভার্স করা লিস্টের একদম নতুন হেড (head)।
← → অ্যারো কি (arrow key) ব্যবহার করুন · উপাদানগুলোতে ক্লিক করুন
curr.next = prev লিখে পয়েন্টার ঘুরিয়ে দিলে, লিস্টের বাকি অংশের সাথে আপনার যোগসূত্র একদম চিরতরে বিচ্ছিন্ন হয়ে যাবে। তাই পয়েন্টার ঘোরানোর আগে সবসময় সামনের ঠিকানাটা সেভ করে রাখবেন।উদাহরণের সাহায্যে দেখি
ধরি লিস্টটা হলো: ১ → ২ → ৩ → ৪ → null
প্রাথমিক অবস্থা: prev = null, curr = ১
- ধাপ ১: next=২ সেভ করি, পয়েন্টার উল্টাই ১→null, এক ধাপ এগোই। বর্তমান অবস্থা: prev=১, curr=২
- ধাপ ২: next=৩ সেভ করি, পয়েন্টার উল্টাই ২→১, এক ধাপ এগোই। বর্তমান অবস্থা: prev=২, curr=৩
- ধাপ ৩: next=৪ সেভ করি, পয়েন্টার উল্টাই ৩→২, এক ধাপ এগোই। বর্তমান অবস্থা: prev=৩, curr=৪
- ধাপ ৪: next=null সেভ করি, পয়েন্টার উল্টাই ৪→৩, এক ধাপ এগোই। বর্তমান অবস্থা: prev=৪, curr=null
curr এখন null — কাজ শেষ। নতুন হেড: ৪ → ৩ → ২ → ১ → null
রিকার্সিভ পদ্ধতি (Recursion দিয়ে)
রিকার্সন (Recursion) দিয়ে এই কাজটা লিখলে কোডটা দেখতে অনেক সুন্দর লাগে, কিন্তু এতে O(n) পরিমাণ স্ট্যাক স্পেস (stack space) নষ্ট হয় — অর্থাৎ প্রতিটা নোডের জন্য মেমোরিতে একটা করে স্ট্যাক ফ্রেম তৈরি হয়:
রিকার্সন একদম শেষের নোড পর্যন্ত যায়, তারপর আবার উল্টোদিক থেকে ফেরত আসা শুরু করে। ফেরত আসার পথে, প্রতিটা কল তার বাচ্চাকে (child) ঘুরিয়ে নিজের দিকে পয়েন্ট করায়, আর নিজের সামনের পয়েন্টারটা null করে দেয়। একদম শেষের নোডটা (যেটা বেস কেস ছিল) নতুন হেড হিসেবে আনচেঞ্জড অবস্থায় সব রিকার্সিভ কল পার হয়ে একদম ওপরে রিটার্ন হয়ে আসে।
লুপ নাকি রিকার্সন — কোনটা ব্যবহার করবেন?
- ইটারেটিভ বা লুপ: মেমোরি লাগে মাত্র O(1) — প্রডাকশন কোডে আর অনেক লম্বা লিস্টের জন্য এটাই বেস্ট চয়েস (এতে স্ট্যাক ওভারফ্লোর রিস্ক থাকে না)।
- রিকার্সিভ: মেমোরি লাগে O(n) — কোড পড়তে এবং বুঝতে সুবিধা হয়, কিন্তু লাখ লাখ নোডের লম্বা লিস্টের জন্য এটা বিশাল রিস্কি।
দুটো পদ্ধতিই O(n) সময়ে বা স্পিডে কাজ করে। ইন্টারভিউতে দুটো পদ্ধতিই লিখে তাদের সুবিধা-অসুবিধা বুঝিয়ে বলাটাই হলো একটা পারফেক্ট উত্তর।
অন্যান্য ভেরিয়েশন
পয়েন্টার নিয়ে এই একই কারসাজি ব্যবহার করে আরও অনেক জটিল কাজ করা যায়:
- সাবলিস্ট রিভার্স করা — পুরোটা উল্টে না দিয়ে শুধু m পজিশন থেকে n পজিশন পর্যন্ত উল্টে দেওয়া।
- k-গ্রুপে রিভার্স করা — প্রতি k টা এলিমেন্টের এক একটা গ্রুপ বা চাংক (chunk) ধরে ধরে উল্টে দেওয়া।
- প্যালিনড্রোম চেক করা — লিস্টের অর্ধেক অংশকে রিভার্স করে প্রথম অর্ধেকের সাথে মিলিয়ে দেখা।
Complexity
ছোট কুইজ
পড়া চালিয়ে যান