شبهکد، فلوچارت و اجرای دستی؛ الگوریتم را پیش از کدنویسی بررسی کنیم
در این درس ۷ بخش
شبهکد، نوشتن مراحل حل مسئله به زبانی است که انسان بتواند دقیق اجرا کند، بیآنکه درگیر همهٔ جزئیات زبان برنامهنویسی شود. عبارت «جواب مناسب را پیدا کن» کافی نیست؛ باید معلوم باشد چه مقداری خوانده و تغییر داده میشود و کار چه زمانی پایان مییابد. دو مسئلهٔ کوچکِ جابهجایی مقدارها و بستهبندی را بررسی میکنیم تا دقت مراحل را روی کاغذ بسنجیم؛ سپس همان مراحل را به کد قابل اجرا وصل میکنیم.
قرارداد نگارش این دوره را ثابت نگه داریم
شبهکد استاندارد جهانیِ یگانهای ندارد؛ هر منبع ممکن است نشانههای متفاوتی به کار ببرد. در این دوره، پیکان چپ یعنی مقدار سمت راست محاسبه و در نام سمت چپ قرار داده شود. علامت مساوی در توضیح رابطه یا پرسش شرطی، برابری را بیان میکند. تورفتگی، محدودهٔ بدنهٔ شرط و تکرار را نشان میدهد؛ «برگردان» جواب را به فراخواننده میدهد و «پایان» مسیر جاری را تمام میکند. اگر فهرست داشته باشیم، نخستین اندیس صفر است و آخرین اندیس برای طول n برابر n−1 خواهد بود. این قرارداد را هنگام ترجمه به زبان دیگر آگاهانه تطبیق دهید.
x ← 5 یعنی مقدار x اکنون 5 شود
x ← x + 2 یعنی مقدار قبلی x خوانده و 2 به آن افزوده شود
x = 7 ؟ یک پرسش دربارهٔ برابری مقدار فعلی است
A[0] خانهٔ نخستِ فهرست A در قرارداد این دوره
برگردان x نتیجه به فراخواننده داده شوددر JavaScript، انتساب با = و مقایسهٔ سخت با === نوشته میشود. بنابراین x = x + 2 تناقض ریاضی نیست؛ دستور تغییر وضعیت است. اگر مسئله ثابتبودن مقدار را میخواهد، تغییر بیدلیل آن طراحی را دشوار میکند. نام موقت، مالک داده و ترتیب دستورها باید روشن باشند. از #variable و #return برای مرور نحو کمک بگیرید، ولی علت نیاز به مقدار موقت را از خود ردگیری بفهمید. خواندن علامتها بدون دانستن این قرارداد ممکن است کل پاسخ یک سؤال امتحانی را عوض کند.
مثال اول: جابهجایی بدون گمکردن مقدار
دو مقدار first برابر ۱۷ و second برابر ۴۲ داریم؛ خروجی باید first برابر ۴۲ و second برابر ۱۷ باشد. ورودی و خروجی، دو نام با دو مقدارند و نمیخواهیم جمع یا میانگین آنها را حساب کنیم. اگر اول بنویسیم first ← second، مقدار قبلی first از آن نام قابل بازیابی نیست. دستور بعدی second ← first مقدار تازه را میخواند، نه نسخهٔ قبلی را. پس هر دو ۴۲ میشوند. برای نگهداری مقدار قدیمی first، یک نام موقت میسازیم؛ این نام نقش مشخص دارد و تزئین کد نیست.
temporary ← first
first ← second
second ← temporary
مرحله first second temporary
ابتدا 17 42 —
نگهداری first 17 42 17
قرار دادن second 42 42 17
بازیابی مقدار نگهداشته 42 17 17جدول ردگیری، وضعیت را پس از هر دستور ثبت میکند. در سطر سوم، یکسانبودن دو مقدار اشکال ندارد؛ هنوز مقدار ۱۷ در temporary حفظ شده است. اگر مقدارها از ابتدا برابر باشند، نسخهٔ خراب و درست خروجی ظاهراً یکسانی میدهند. بنابراین دادهٔ آزمون را طوری انتخاب کنید که اشتباه محتمل را نمایان کند. نامگذاری روشن هم کمک میکند: temporary یعنی نگهداری موقت، نه یک عدد سومِ مرتبط با مسئله. بعد از جابهجایی، رابطهٔ خروجی با ورودی اولیه را به زبان خود توضیح دهید.
import assert from 'node:assert/strict';
let first = 17;
let second = 42;
const temporary = first;
first = second;
second = temporary;
assert.equal(first, 42);
assert.equal(second, 17);
console.log(JSON.stringify({ first, second }));
let brokenFirst = 17;
let brokenSecond = 42;
brokenFirst = brokenSecond;
brokenSecond = brokenFirst;
assert.equal(brokenFirst, 42);
assert.equal(brokenSecond, 42);
console.log(JSON.stringify({ brokenFirst, brokenSecond }));فایل مستقل 03-swap.mjs هم نسخهٔ درست و هم نسخهٔ خراب را اجرا میکند؛ وجود نسخهٔ خراب، تمرین تشخیص خطاست و قرار نیست آن را پاسخ نهایی بدانید. دو سطر خروجی بهترتیب مقدارهای ۴۲ و ۱۷، سپس ۴۲ و ۴۲ را نشان میدهند. assertها همین رفتار مورد انتظار هر نمایش را بررسی میکنند. وقتی خروجی با انتظار مسئله مخالف است، نخستین سطر جدول که اطلاعات لازم را از دست میدهد پیدا کنید. اضافهکردن چاپهای پراکنده بدون مشخصکردن انتظار، بهتنهایی روش عیبیابی منظم نیست.
فلوچارت چه چیزی را نشان میدهد؟
فلوچارت یا روندنما، نمایش تصویری مسیر اجرای الگوریتم است. در قرارداد رایج آموزشی، آغاز و پایان شکل مشخص دارند؛ مستطیل برای پردازش، متوازیالاضلاع برای ورود و خروج، و لوزی برای پرسش شرطی به کار میرود. فلش، مسیر بعدی را نشان میدهد. از لوزیِ یک شرط بولی معمولاً دو مسیر با برچسب درست و نادرست بیرون میآید. شکلها بدون برچسب و فلشهای جهتدار کافی نیستند. اگر نمودار جایی به پرسش برمیگردد، باید روشن باشد چه دادهای تغییر کرده و چرا این بازگشت ممکن است سرانجام متوقف شود.
در جابهجایی، سه مستطیل انتساب بهترتیب به پایان میرسند؛ لوزی لازم نیست. در بستهبندی، لوزی میپرسد آیا باقیمانده برای یک جعبه کافی است؟ مسیر درست، یک جعبه اضافه و باقیمانده را کم میکند و به پرسش برمیگردد؛ مسیر نادرست، پاسخ را ارائه میکند. نمودار و شبهکد باید همین مسیر یکسان را بیان کنند.
در شکل زیر، برای تمرین خواندن روندنما از یک مثال مستقل نمره استفاده میکنیم: نخست عدد متناهی در محدودهٔ صفر تا بیست پذیرفته میشود و سپس آستانهٔ آموزشی ده بررسی میشود. رد ورودی نامعتبر با پاسخ عادی «نیازمند تمرین» متفاوت است. این شکل، روندنمای مسئلهٔ جعبهها نیست؛ آن مسئله را در بخش بعد جدا ردگیری میکنیم.
مثال دوم: جعبههای کامل و باقیمانده
۲۳ قلم کالا و جعبههای ششتایی داریم؛ چند جعبهٔ کامل و چند قلم باقی میماند؟ در مدل این تمرین، تعداد کالا صحیح نامنفی و اندازهٔ جعبه صحیح مثبت است. هدف، تعداد جعبههای کامل است؛ تعداد کل جعبهٔ لازم برای حمل همهٔ کالا پرسش دیگری است و در صورت باقیمانده، یک جعبه بیشتر میخواهد. با هر بار کمکردن شش، یک جعبهٔ کامل ثبت میکنیم: ۲۳، سپس ۱۷، سپس ۱۱، سپس ۵. اکنون پنج کمتر از شش است؛ پاسخ سه جعبهٔ کامل و پنج قلم باقیمانده است.
الگوریتم DivideWhole(items, boxSize)
اگر items عدد صحیح در بازهٔ 0..1000000 نیست، خطا بده
اگر boxSize عدد صحیح در بازهٔ 1..1000 نیست، خطا بده
remaining ← items
fullBoxes ← 0
تا وقتی remaining ≥ boxSize:
remaining ← remaining − boxSize
fullBoxes ← fullBoxes + 1
جفتِ fullBoxes و remaining را برگردانپس از تکرار fullBoxes remaining
0 0 23
1 1 17
2 2 11
3 3 5
کنترل پاسخ: 23 = 3 × 6 + 5
محدودهٔ باقیمانده: 0 ≤ 5 < 6جدول را پیش از کد بنویسید و بررسی شرط را بین سطرها تصور کنید. رابطهٔ items = fullBoxes × boxSize + remaining در هر مرحله حفظ میشود. این رابطه به توضیح درستی پاسخ کمک میکند، اما برای پایانپذیری باید نکتهٔ دیگری بگوییم: اندازهٔ جعبه مثبت است، پس remaining در هر تکرار کم میشود و نامنفی میماند. با اندازهٔ صفر، هیچ پیشرفتی نداریم. در فصل حلقهها همین نوع استدلال را منظمتر میکنیم؛ در اینجا کافی است علت هر بازگشت را بتوانید نشان دهید.
import assert from 'node:assert/strict';
function divideWhole(items, boxSize) {
if (!Number.isInteger(items) || items < 0 || items > 1000000) {
throw new RangeError('items: 0..1000000');
}
if (!Number.isInteger(boxSize) || boxSize < 1 || boxSize > 1000) {
throw new RangeError('boxSize: 1..1000');
}
let remaining = items;
let fullBoxes = 0;
while (remaining >= boxSize) {
remaining = remaining - boxSize;
fullBoxes = fullBoxes + 1;
}
return { fullBoxes, remaining };
}
assert.deepEqual(divideWhole(23, 6), { fullBoxes: 3, remaining: 5 });
assert.deepEqual(divideWhole(24, 6), { fullBoxes: 4, remaining: 0 });
assert.deepEqual(divideWhole(0, 6), { fullBoxes: 0, remaining: 0 });
assert.deepEqual(divideWhole(5, 6), { fullBoxes: 0, remaining: 5 });
assert.throws(() => divideWhole(5, 0), RangeError);
console.log(JSON.stringify(divideWhole(23, 6)));فایل 03-divide-whole.mjs خروجی {"fullBoxes":3,"remaining":5} دارد. برای محدودکردن زمان تمرین، تعداد کالا تا یک میلیون و اندازهٔ جعبه تا هزار پذیرفته میشود. این روش برای آموزش ردگیری روشن است، اما در برنامهٔ واقعی، محاسبهٔ خارجقسمت و باقیمانده میتواند مستقیم انجام شود؛ کمکردن تکراری لزوماً انتخاب کارآمدی نیست. در JavaScript عملگر % باقیمانده میدهد و برای عددهای منفی باید قرارداد علامت را جدا شناخت. ورودیهای این درس نامنفیاند تا همان رابطهٔ سادهٔ بستهبندی حفظ شود.
مرزها را در روندنما گم نکنید
با ۲۴ قلم، آخرین remaining برابر شش میشود و باید یک دور دیگر اجرا شود؛ به همین دلیل شرط بزرگتر یا مساوی است. با پنج قلم، حلقه اصلاً اجرا نمیشود و پاسخ صفر جعبه و پنج باقیمانده است. با صفر قلم نیز خروجی صفر و صفر داریم. این سه حالت، مرزهای مهمتری از اجرای دوبارهٔ نمونهٔ ۲۳ هستند. در سؤال تشریحی، فقط نوشتن خروجی نهایی کافی نیست: شرط آخرین بررسی و سطری را که باعث توقف میشود نشان دهید. همین عادت، خطاهای یک واحدی را سریعتر آشکار میکند.
نمونهسؤال و پاسخ تشریحی
در جابهجایی دو مقدار، اگر هر دو از ابتدا ۹ باشند، آیا الگوریتم خرابِ دو انتساب هم خطا را نشان میدهد؟ یک نمونهٔ تشخیصی پیشنهاد کنید.
پاسخ تشریحی: خیر. وقتی هر دو مقدار برابرند، نسخهٔ خراب نیز دو عدد ۹ میدهد و تفاوتی با پاسخ درست دیده نمیشود. دو مقدار متمایز، مانند ۳ و ۸، انتخاب کنید: پاسخ درست ۸ و ۳ است ولی نسخهٔ خراب ۸ و ۸ میسازد. سپس حالت دو مقدار برابر را بهعنوان آزمون تکمیلی نگه دارید، نه تنها شاهد درستی.
برای بستهبندی ۲۴ قلم در جعبههای ششتایی، چرا شرط remaining > boxSize اشتباه است؟ جدول آخرین مرحله را توضیح دهید.
پاسخ تشریحی: پس از سه تکرار، remaining برابر ۶ و fullBoxes برابر ۳ است. با شرط بزرگترِ سخت، حلقه متوقف میشود و یک جعبهٔ کامل را باقیمانده حساب میکند. شرط بزرگتر یا مساوی، بار چهارم را انجام میدهد و پاسخ چهار جعبه و صفر باقیمانده میشود. این نمونه مرز برابری را هدف میگیرد و حالت ۲۳ قلم بهتنهایی این خطا را آشکار نمیکند.
اگر اندازهٔ جعبه صفر باشد، در الگوریتم تفریق تکراری چه رخ میدهد؟ آیا اضافهکردن فلش پایان به فلوچارت مشکل را حل میکند؟
پاسخ تشریحی: با تعداد نامنفی، شرط remaining >= 0 درست میماند و کمکردن صفر هیچ پیشرفتی ایجاد نمیکند؛ الگوریتم خاتمه نمییابد. باید ورودی صفر پیش از ورود به حلقه رد شود و اندازهٔ جعبه مثبت باشد. رسم نماد پایان بدون مسیری که واقعاً به آن برسد، خاتمه را تضمین نمیکند. در دفاع از الگوریتم، مقدار کاهشی و حد پایین آن را مشخص کنید.
معیار تحویل و منابع
برای تحویل، جدول جابهجایی و جدول بستهبندی را بدون اجرای کد کامل کنید، سپس نتیجه را با فایلها بسنجید. یک فلوچارت با دو مسیر شرط بنویسید یا رسم کنید و کنار هر مسیر، شرط و تغییر داده را توضیح دهید. منابع رسمی برای بررسی مفاهیم و رفتار زبان معرفی شدهاند؛ تألیف فارسی، مسئلهها، جدولها و کدها: تحریریه درسآوند.
CS50x 2026 — Lecture 0: https://cs50.harvard.edu/x/notes/0/
MIT OCW 6.0001 — Lecture slides and code: https://ocw.mit.edu/courses/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/pages/lecture-slides-code/
MDN — Grammar and types: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Grammar_and_types
MDN — Remainder operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder
فایل مثالها، راهنمای فارسی اجرا و عیبیابی، پروژهٔ گزارش نمرهها و آزمون جمعبندی تألیفی با پاسخ تشریحی، رایگان در صفحهٔ بستهٔ تمرین همین دوره ارائه میشود:
https://www.darsavand.com/resources/computer-algorithms-practice-kit
تألیف و مثالهای اختصاصی: تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.