সি শেখার গাইডলাইন!

বাংলা ভাষায় সি প্রোগ্রামিং ভাষা দিয়ে প্রোগ্রামিং শেখার গাইডলাইন

শখের বশে কিংবা বাটে পড়ে – যেকারণেই হোক না কেন, প্রোগ্রামিং যারা শিখে, তাদের বেশিরভাগেরই “সি” প্রোগ্রামিং ভাষা শিখতে হয়। সি কেন শিখব, বা সি শিখে কী লাভ – এই আলোচনা করাটা এই লেখার উদ্দেশ্য নয়, বরং যারা ইতিমধ্যে সি শিখবে বলে মনস্থির করেছে এবং বাংলা ভাষায় সি শেখার রিসোর্স খুঁজছে, তাদের জন্যই এই লেখা। যেহেতু আমি সি প্রোগ্রামিং নিয়ে একাধিক বই লিখেছি, তাই সেগুলো কোনটা কাদের জন্য, সেটাও এই লেখায় তুলে ধরব।

সি শেখার শুরুতেই এখন যেটা করা উচিত, এক ঘণ্টার সি ভিডিওটা দেখে ফেলা উচিত। এখানে কিছু কিছু বুঝা যাবে, আবার কিছু কিছু জিনিস নতুনরা হয়ত একেবারেই বুঝবে না – তাতে ঘাবড়ে যাবার কিছু নেই। বরং সম্ভব হলে পুরো ভিডিওটা দুইবার দেখতে হবে। এই ভিডিও দেখলে সি নিয়ে একটা ধারণা তৈরি হবে।

তারপর সি শেখা শুরু করতে হবে। প্রোগ্রামিংয়ে যারা একেবারেই নতুন, তাদের জন্য আমি “কম্পিউটার প্রোগ্রামিং ১ম খণ্ড” নামে একটি বই লিখেছি। এখানে জটিল-কঠিন-ভয়াবহ জিনিসগুলো বাদ দিয়ে বরং মৌলিক জিনিসগুলোর ওপর জোর দেওয়া হয়েছে। আশা করি, বইটি কেউ যদি দুইবার করে পড়ে ফেলে, তাহলে তার বেসিক শক্ত হয়ে যাবে। বইটি রকমারি ডট কম থেকে অর্ডার করা যাবে, এছাড়া আর কোথা থেকে পাওয়া যাব সেটি জানা যাবে দ্বিমিক প্রকাশনীর ওয়েবসাইটে। আবার বইটি ইন্টারনেটে ফ্রিও পড়া যাবে, cpbook.subeen.com ওয়েবসাইট থেকে।

বেসিক তো শেখা হলো, কিন্তু প্রোগ্রামিং তো চর্চার বিষয়। নতুনরা অনুশীলন করবে কীভাবে? আমার মতে “কম্পিউটার প্রোগ্রামিং ১ম খণ্ড” বইটি পড়ার পরে সবার যেই বইটি পড়া উচিত, সেটি হচ্ছে তাহমিদ রাফির লেখা “প্রোগ্রামিং এক্সারসাইজ“। এখানের উদাহরণ ও অনুশীলনীগুলো করে ফেলতে হবে।

সি তো শিখতেছি, কিন্তু ভিডিও দেখে শিখতে পারলে ভালো হত – কারো কারো এমনটি মনে হতে পারে। তাদের জন্য আছে দ্বিমিক কম্পিউটিংয়ের ফ্রি অনলাইন কোর্স “প্রোগ্রামিংয়ে হাতেখড়ি”। ভিডিওগুলো আমার ইউটিউব চ্যানেলেও দেখা যাবে।

ওপরের কাজগুলো করার পরে আরো সি শিখতে হবে। এবারে একটু এডভান্সড বিষয়। এজন্য আমি লিখেছি “কম্পিউটার প্রোগ্রামিং ২য় খণ্ড“। এখানে পয়েন্টার, রিকার্শন, ফাইল, স্ট্রাকচার ইত্যাদি বিষয়গুলো আলোচনা করা হয়েছে। এই বইটিও কমপক্ষে দুবার পড়তে হবে।

এখন, প্রোগ্রামিং শেখার পাশাপাশি, প্রবলেম সলভিংও করতে হবে। শুরুতে সহজ প্রবলেম সলভ করাই ভালো। এজন্য তৈরি করা আছে দ্বিমিক অনলাইন জাজ। এই ভিডিওগুলো দেখলে সেখানে শুরু করা সহজ হবে। প্রবলেমগুলো নিজে নিজে সমাধান করতে পারলেই সবচেয়ে ভালো, আর তা না পারলে “৫২টি প্রোগ্রামিং সমস্যা ও সমাধান” বইটি পড়া যেতে পারে।

প্রবলেম সলভিং করতে করতে গিয়ে মনে হবে, একটু ডেটা স্ট্রাকচার ও অ্যালগরিদম জানলে ভালো হতো। সি দিয়ে ডেটা স্ট্রাকচার ও অ্যালগরিদমের সঙ্গে পরিচিত হওয়ার জন্য আমার লেখা “কম্পিউটার প্রোগ্রামিং ৩য় খণ্ড – ডেটা স্ট্রাকচার ও অ্যালগরিদম পরিচিতি” বইটি পড়তে পারো। বই পড়ার পাশাপাশি ইউটিউবে আমার ভিডিও লেকচারগুলোও কাজে আসবে বলে আমি মনে করি।

ওপরের কাজগুলো ঠিকঠাকভাবে করলে আমি মনে করি সি প্রোগ্রামিং ভাষা দিয়ে প্রোগ্রামিং শেখা – এই পথে তুমি বহুদূর এগিয়ে যাবে। তারপরও কেউ আরেকটি বই পড়তে চাইলে আমি বলব The C Programming Language বইটি পড়ার জন্য।

লিঙ্ক –

পাইথন দিয়ে সর্টিং – ২য় পর্ব

পাইথন প্রোগ্রামিং ভাষায় সর্ট করার পদ্ধতি।

আগের পর্বে আমরা দেখেছি কীভাবে পাইথনের বিল্টইন ফাংশন ব্যবহার করে সর্টিং করা যায়। এই লেখায় আমরা আরেকটু জটিল সর্টিংয়ের কাজ করব, তবে বিল্টইন ফাংশন ব্যবহার করেই কাজগুলো করা হবে।

ধরা যাক, একটি লিস্টে বিভিন্ন ফলের নাম এবং সেই ফল কতগুলো করে আছে, সেটি দেওয়া আছে – fruits = [(‘orange’, 3), (‘apple’, 3), (‘banana’, 2), (‘mango’, 10), (‘guava’, 5)]

এখন এই লিস্টকে আমরা যদি সর্ট করি, তাহলে ফলের নাম অনুসারে সর্ট হয়ে যাবে –

>>> fruits = [('orange', 3), ('apple', 3), ('banana', 2), ('mango', 10), ('guava', 5)]
>>> print(sorted(fruits))
[('apple', 3), ('banana', 2), ('guava', 5), ('mango', 10), ('orange', 3)]

ফলের নাম অনুসারে সর্ট হওয়ার কারণ কী? ফলের সংখ্যা অনুসারেও তো সর্ট হতে পারত। এখানে প্রতিটি টাপলের প্রথম উপাদান সেই টাপলের প্রতিনিধিত্ব করে। যেমন, টাপলে আমরা যদি আগে সংখ্যা লিখতাম, তারপরে ফলের নাম লিখতাম, তাহলে সংখ্যা অনুযায়ী সর্ট হতো।

কিন্তু আমরা যদি চাই, আমাদেরকে যেই লিস্ট দেওয়া আছে সেটি ফলের নাম নয়, বরং সংখ্যা অনুসারে সর্ট করা হবে, তখন কী করতে হবে? প্রতিটি টাপলের দ্বিতীয় উপাদানটি যদি সেই টাপলের প্রতিনিধিত্ব করত, তাহলে কিন্তু আমরা কাঙ্ক্ষিত উপায়ে সর্ট করতে পারতাম। sorted() ফাংশনে key নামে একটি প্যারামিটার আছে, যার মাধ্যমে আমরা বলে দিতে পারি, কোন উপাদানটির ওপর ভিত্তি করে সর্ট করার কাজটি হবে। key-তে আসলে একটি ফাংশন দেওয়া হয়, আর যেই লিস্ট সর্ট করতে হবে, তার প্রতিটি উপাদান সেই ফাংশনের মধ্যে পাঠানো হয়। ফাংশনটি একটি উপাদান রিটার্ন করবে, যার ওপর ভিত্তি করে সর্টিং হবে। তাহলে আমরা এখানে যেই কাজটি করতে চাচ্ছি, সেখানে এমন একটি ফাংশন লিখতে হবে, যা (‘apple’, 3) ইনপুট নিবে আর 3 রিটার্ন করবে।

def compare_fnc(item):
    return item[1]

fruits = [('orange', 3), ('apple', 3), ('banana', 2), ('mango', 10), ('guava', 5)]
print(sorted(fruits, key=compare_fnc))

ওপরের কোড রান করলে দেখা যাবে ফলের সংখ্যা অনুযায়ী ছোট থেকে বড় ক্রমে সর্ট করা হয়ে গিয়েছে।

[('banana', 2), ('orange', 3), ('apple', 3), ('guava', 5), ('mango', 10)]

বড় থেকে ছোট ক্রমে সর্ট করতে চাইলে লিখতে হবে sorted(fruits, key=compare_fnc, reverse=True).

পাইথনে operator মডিউলে একটি ফাংশন আছে itemgetter, যেটি ব্যবহার করে আমরা ওপরের কাজটি আরো সহজে করতে পারি, আমাদের নিজেদের কষ্ট করে ফাংশন তৈরি করতে হবে না।

from operator import itemgetter

fruits = [('orange', 3), ('apple', 3), ('banana', 2), ('mango', 10), ('guava', 5)]
print(sorted(fruits, key=itemgetter(1)))

ওপরের কোডে itemgetter(1) এর বদলে itemgetter(0) লিখলে ফলের নাম অনুযায়ী সর্ট হয়ে যাবে। এখন আমরা যদি চাই, প্রথমে ফলের সংখ্যা অনুযায়ী সর্ট হবে, তারপরে যেসব ফলের সংখ্যা সমান, তাদের মধ্যে নাম অনুযায়ী সর্ট হবে, তাহলে কী করতে হবে? মানে আমাদের আউটপুট (‘orange’, 3′), (‘apple’, 3) ক্রমে না এসে (‘apple’, 3), (‘orange’, 3) ক্রমে আসবে। কাজটি সহজেই করা যায় এভাবে –

>>> fruits = [('orange', 3), ('apple', 3), ('banana', 2), ('mango', 10), ('guava', 5)]
>>> print(sorted(fruits, key=itemgetter(1, 0)))
[('banana', 2), ('apple', 3), ('orange', 3), ('guava', 5), ('mango', 10)]

এখান আমরা itemgetter(1, 0) ব্যবহার করেছি। কিন্তু এখন আমরা যদি চাই, ফলের সংখ্যার বড় থেকে ছোট ক্রমে সর্ট হবে আর যেসব ফলের সংখ্যা সমান, তারা নাম অনুযায়ী ছোট থেকে বড় ক্রমে সর্ট হবে, তখন কী করতে হবে? তাহলে দুইবার সর্ট করার কাজটি করতে হবে –

>>> fruits = [('orange', 3), ('apple', 3), ('banana', 2), ('mango', 10), ('guava', 5)]
>>> print(fruits)
[('orange', 3), ('apple', 3), ('banana', 2), ('mango', 10), ('guava', 5)]
>>> fruits = sorted(fruits, key=itemgetter(0))
>>> print(fruits)
[('apple', 3), ('banana', 2), ('guava', 5), ('mango', 10), ('orange', 3)]
>>> fruits = sorted(fruits, key=itemgetter(1), reverse=True)
>>> print(fruits)
[('mango', 10), ('guava', 5), ('apple', 3), ('orange', 3), ('banana', 2)]

ওপরের পদ্ধতি কাজ করে, কারণ পাইথনের sorted() ফাংশন stable সর্ট করে। sort()-এর ক্ষেত্রেও একই কথা প্রযোজ্য।

পাইথন দিয়ে সর্টিং – ১ম পর্ব

এই টিউটোরিয়ালে পাইথন দিয়ে কিভাবে সর্ট করতে হয়, সেটি আলোচনা করা হবে। প্রোগ্রামিংয়ে সর্ট (sort) করা মানে হচ্ছে কোনো নির্দিষ্ট ক্রমে সাজানো। পাইথনে কোনো লিস্টকে সর্ট করার জন্য একটি মেথড দেওয়া আছে, যার নাম হচ্ছে sort()। নিচের কোড দেখলেই sort() কী করে, সেটি বুঝা যাবে –

>>> li = [1, 3, 4, 5, 6, 2, 3]
>>> li.sort()
>>> print(li)
[1, 2, 3, 3, 4, 5, 6]

প্রোগ্রামটি রান করলে দেখা যাচ্ছ যে, লিস্টের সংখ্যাগুলো ছোট থেকে বড় ক্রমে সাজানো হয়ে গিয়েছে। এখন, কেউ যদি চায় যে, সে বড় থেকে ছোট ক্রমে সাজাবে, তাহলে sort() মেথডের ভেতরে reverse নামে একটি প্যারামিটার আছে, সেখানে True পাঠাতে হবে –

>>> li = [1, 3, 4, 5, 6, 2, 3]
>>> li.sort(reverse=True)
>>> print(li)
[6, 5, 4, 3, 3, 2, 1]

একটি স্ট্রিংয়ের লিস্টকেও একইভাবে সর্ট করা যায়। যেমন –

>>> countries = ["Bangladesh", "Japan", "Australia", "Canada", "Singapore"]
>>> countries.sort()
>>> print(countries)
['Australia', 'Bangladesh', 'Canada', 'Japan', 'Singapore']

আমরা দেখতে পাচ্ছি, লিস্টকে সর্ট করা হলে লিস্টের ভেতরের উপাদানগুলো নির্দিষ্ট ক্রমে সাজানো হয়ে যায়, আগের লিস্টটি আর পাওয়া যায় না। কিন্তু আমরা যদি সেটি পেতে চাই, তাহলে কী করতে হবে? পাইথনে আরেকটি ফাংশন আছে, যার নাম sorted(). এটি সর্ট করার পরে একটি নতুন লিস্ট রিটার্ন করে। যেই লিস্টটি সর্ট করা হচ্ছে, সেটি পরিবর্তন হয় না। নিচের কোড দেখলেই এটি বুঝতে পারা যাবে –

>>> li = [1, 3, 4, 5, 6, 2, 3]
>>> nums = sorted(li)
>>> print(nums)
[1, 2, 3, 3, 4, 5, 6]
>>> print(li)
[1, 3, 4, 5, 6, 2, 3]

sorted() ফাংশনটি কেবল লিস্ট না, অন্য ইটারেবলের (iterable) ওপরও কাজ করে। যেমন, আমরা চাইলে একটি টাপলকে সর্ট করতে পারি। ফলাফল হিসেবে একটি লিস্ট রিটার্ন করা হবে।

>>> tpl = (3, 8, 1, 4, 6, 2)
>>> result = sorted(tpl)
>>> result
[1, 2, 3, 4, 6, 8]
>>> tpl
(3, 8, 1, 4, 6, 2)

sorted() ফাংশনেও উল্টো ক্রমে সর্ট করতে চাইলে reverse প্যারামিটার ব্যবহার করা যাবে।

>>> li = [1, 3, 4, 5, 6, 2, 3]
>>> nums = sorted(li, reverse=True)
>>> print(nums)
[6, 5, 4, 3, 3, 2, 1]
>>> print(li)
[1, 3, 4, 5, 6, 2, 3]

পাইথনের এই বিল্টইন সর্ট করার ফাংশনটিতে আসলে Timsort নামক একটি অ্যালগরিদম ব্যবহার করা হয়।

পাইথনের সর্টিং নিয়ে পরবর্তী লেখায় আমরা আরেকটু জটিল ডেটা স্ট্রাকচার কীভাবে সর্ট করতে হয়, সেটি দেখব।

অ্যারে থেকে ডুপ্লিকেট বাদ দেওয়া – প্রোগ্রামিং ইন্টারভিউ সমস্যা ১৬

সমস্যা – একটি অ্যারে দেওয়া থাকবে যার প্রতিটি উপাদান একটি ইন্টিজার এবং অ্যারের সংখ্যাগুলো ছোট থেকে বড় ক্রমে সাজানো আছে। ওই অ্যারেতে যেসব সংখ্যা একাধিকবার আছে, সেসব সংখ্যা একটি রেখে অতিরিক্তগুলো বাদ দিতে হবে। আর এজন্য অতিরিক্ত কোনো অ্যারে ব্যবহার করা যাবে না, অর্থাৎ ইনপুট অ্যারেতেই কাজ করতে হবে। যেমন, ইনপুট যদি হয় [1, 1, 1, 2, 3, 3], তাহলে ডুপ্লিকেট (duplicate)-গুলো বাদ দিলে অ্যারেটি হবে [1, 2, 3, …]. এক্ষেত্রে প্রথম তিনটি সংখ্যার পরে বাকিগুলো কী হবে, সেটি বিবেচনা করা হবে না। আর অ্যারেটি পরিবর্তন করার পরে অ্যারেতে মোট কয়টি উপাদান আছে সেটি রিটার্ন করতে হবে। অর্থাৎ এই ইনপুটের জন্য অ্যারেটি পরিবর্তন করার পরে 3 রিটার্ন করতে হবে।

সমাধান – সমস্যাটিতে যদি বলা হত অতিরিক্ত অ্যারে ব্যবহার করা যাবে, তাহলে আমরা কী করতাম? একটি নতুন অ্যারে তৈরি করে সেখানে সংখ্যাগুলো এমনভাবে রাখতাম যেন কোনো সংখ্যা একবারের বেশি না আসে।

পাইথন দিয়ে ইমপ্লিমেন্ট করতে চাইলে, প্রথমে একটি লিস্ট তৈরি করতে হবে। লিস্টের প্রথম উপাদানটি হবে ইনপুট লিস্টের প্রথম উপাদান। তারপর ইনপুট লিস্টের প্রতিটি সংখ্যা পরীক্ষা করে দেখতে হবে কোনো সংখ্যা তার আগের সংখ্যার সমান কী না। যদি সমান না হয়, তাহলে সেটি নতুন লিস্টে নেওয়া হবে, আর সমান হলে কিছু করা হবে না। নিচের কোড দেখলেই সহজে বুঝতে পারা যাবে –

def remove_duplicates(nums):
    unique_nums = []
    unique_nums.append(nums[0])
    
    n = len(nums)
    for i in range(1, n):
        if nums[i] != nums[i-1]:
            unique_nums.append(nums[i])

    return len(unique_nums)

প্রোগ্রামটি আরেকভাবে ইমপ্লিমেন্ট করা যায় –

def remove_duplicates(nums):
    unique_nums = list(set(nums))
    unique_nums.sort()
    return len(unique_nums)

এই ফাংশনটির কোড ছোট হলেও টাইম কমপ্লেক্সিটি বেশি। নতুন প্রোগ্রামাররা, বিশেষ করা যারা পাইথন দিয়ে কাজ করে, তারা অনেক সময় মনে করে যে, কোড কম লেখা মানে কোড বেশি ইফিশিয়েন্ট – এটি ভুল ধারণা। যাই হোক, আমাদের কিন্তু শর্ত ছিল যে, অতিরিক্ত মেমোরি ব্যবহার করা যাবে না। তাহলে কী করা যায়? প্রথম যেই ফাংশনটি লিখেছিলাম, সেটি আমরা এভাবেও লিখতে পারি –

def remove_duplicates(nums):
    n = len(nums)
    unique_nums = [0] * n
    unique_nums[0] = nums[0]
    current_index = 1
    for i in range(1, n):
        if nums[i] != nums[i-1]:
            unique_nums[current_index] = nums[i]
            current_index += 1

    return current_index

এই ফাংশনটি ভালোভাবে লক্ষ করলে বুঝে ফেলা উচিত যে, unique_nums ব্যবহার না করলেও চলে। বুঝতে না পারলে একটু চিন্তা করতে হবে, তাহলেই বুঝে ফেলা উচিত।

আর ইন্টারভিউতে কিন্তু স্পেশাল কেস ঠিকভাবে হ্যান্ডেল করতে হবে। যেমন, এই প্রোগ্রামে ইনপুট যদি ফাঁকা অ্যারে বা লিস্ট হয়, তখন প্রোগ্রামটা 0 রিটার্ন করার বদলে ক্র্যাশ করবে, এটি ঠিক করতে হবে।

আশা করি নিচের দুটি সমস্যা সমাধান করতে তেমন বেগ পেতে হবে না –

https://leetcode.com/problems/remove-duplicates-from-sorted-array/

https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/

একটি লিস্টে দ্বিতীয় লিস্টের সকল উপাদানের উপস্থিতি – প্রোগ্রামিং ইন্টারভিউ সমস্যা ১৫

সহজ প্রোগ্রামিং ইন্টারভিউ সমস্যা।

সমস্যা – একটি ফাংশন লিখতে হবে, যেখানে দুটি লিস্ট ইনপুট দেওয়া হবে। প্রথম লিস্টে যদি দ্বিতীয় লিস্টের সকল উপাদান থাকে তাহলে True রিটার্ন করতে হবে, আর নইলে False রিটার্ন করতে হবে।

সমাধান – সমাধান খুবই সহজ। দ্বিতীয় লিস্টের প্রতিটি উপাদান প্রথম লিস্টে আছে কী না, সেটি পরীক্ষা করতে হবে। যদি না থাকে, তাহলে False রিটার্ন করতে হবে, আর যদি সব উপাদানই প্রথম লিস্টে পাওয়া যায়, তাহলে True রিটার্ন করতে হবে।

def is_sublist(listA, listB):
    for item in listB:
        if item not in listA:
            return False
    return True

এখন প্রশ্ন হচ্ছে, ওপরের কোডের কমপ্লেক্সিটি কত? অতিরিক্ত কোনো মেমোরি ব্যবহার করা হচ্ছে না, তাই স্পেস কমপ্লেক্সিটি হচ্ছে O(1)। আর টাইম কমপ্লেক্সিটি হচ্ছে O(n * m), যেখানে n হচ্ছে listA-এর উপাদান সংখ্যা আর m হচ্ছে listB-এর উপাদান সংখ্যা। অনেকেই এখানে ভুল করে টাইম কমপ্লেক্সিটি বলবে O(n), কিন্তু item not in listA – এখানে কিন্তু লিস্টের সকল উপাদান পরীক্ষা করতে হতে পারে, তাই শুধু এই কাজটির টাইম কমপ্লেক্সিটি হচ্ছে O(m)।

পরবর্তী প্রশ্ন হচ্ছে, ওপরের কোডটি কীভাবে আরো ইফিশিয়েন্ট করা যায়? এজন্য সেট (set) ব্যবহার করা যেতে পারে। প্রথমে listA-কে সেটে রূপান্তর করতে হবে। তারপরে কোড আগের মতোই। আমি আর এখানে কোড লিখে দেখালাম না, এটি নিজে লিখতে হবে।

লিস্টে যদি n সংখ্যক উপাদান থাকে, তাহলে এটিকে সেটে রূপান্তর করার কমপ্লেক্সিটি হচ্ছে O(n)। আর তারপরে যেই হোড হবে, সেটি ঠিকঠাকমতো লিখলে তার কমপ্লেক্সিটি হবে O(m)। কারণ একটি উপাদান সেটে আছে কী না, সেটি পরীক্ষা করার জন্য O(1) সময় লাগে। তাহলে প্রোগ্রামটির টাইম কমপ্লেক্সিটি হবে O(n) + O(m)। তবে সেট ব্যবহার করায় কিন্তু এই প্রোগ্রামের স্পেস কমপ্লেক্সিটি বেড়ে যাচ্ছে, স্পেস কমপ্লেক্সিটি কত সেটি পাঠকের জন্য কুইজ (কমেন্টে উত্তর দেওয়া যাবে)।

দুটি বিষয় লক্ষ রাখতে হবে। ইন্টারভিউতে স্পেস কমপ্লেক্সিটি না জিজ্ঞাসা করলেও বলা উচিত। মানে কমপ্লেক্সিটি জিজ্ঞাসা করলে টাইম ও স্পেস কমপ্লেক্সিটি দুটোই বলা উচিত। আর দ্বিতীয়ত, ফাংশন লেখার পরে তার জন্য ইউনিট টেস্ট লিখতে পারলে ভালো হয়, এতে ইন্টারভিউয়ারকে একটু খুশি করা যায় আর কী।