هياكل البيانات النظرية وتطبيقها في جافا سكريبت. النقطة رقم 1. الأزواج

"يفكر المبرمجون السيئون في التعليمات البرمجية. يفكر المبرمجون الجيدون في تراكيب البيانات وعلاقاتهم ، ”لينوس تورفالدس ، مبتكر لينكس.
نحن نأخذ بديهية أن حل أي مشكلة في البرمجة غالبًا ما يرجع إلى اختيار بنية البيانات الصحيحة. يمكن إثبات هذه البديهية ، لكنها فترة طويلة والمقال عن الآخر قليلاً. سنتحدث اليوم عن أحد أبسط الهياكل النظرية حول الزوج.

الزوج هو بنية بيانات يخزن أي عنصرين من عناصر البيانات في حد ذاته ، مما يعني أننا نريد دمجهما بطريقة منطقية في عنصر واحد. يحل هذه المشكلة بدقة ، أي إذا كان هناك بعض عناصر البيانات و bنحن بحاجة إلى تقديمها في شكل b، وبعد ذلك يمكننا تنفيذ هذا التصميم في أزواج.

إذا كنت تعتقد أنك لم تواجه أو لم تواجه شيئًا من هذا القبيل ، فهذا ليس كذلك.

فيما يلي أبسط مثال لخاصية محسوبة من وثائق VUE:

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

من وجهة نظر نظرية ، fullNameهذا زوجان. وهناك عدد لا يحصى من هذه الأمثلة في التكنولوجيا والعالم الحقيقي ، والتي غالبًا ما نمثلها باستخدام رمز البرنامج.

يمكنك أن تلاحظ بشكل صحيح أنه يمكننا دمج أي عدد من العناصر بشكل منطقي وستكون على حق. وهذا يقودنا إلى مفهوم هيكلي أكثر تعقيدًا للبيانات المركبة ، أحدها زوجين . سنتعامل مع البيانات المركبة بعد ذلك بقليل ، والآن سنركز على الزوج.

تخبرنا النظرية أن للأزواج الخصائص التالية:

  • , .. - , , . , ( - . , )
  • . . . , , , , , .

    :

    pair = { a, b }
    

    إذا كنا في رمز الاتصال سنعمل مع الزوج مباشرة في هذا النمط: pair.aعندئذٍ سنضطر إلى إعادة كتابة رمز الاتصال في أي مكان يظهر فيه استدعاء الزوج. هذا ليس رائعا!

    الزوجان عبارة عن تجريد (حتى مستوى منخفض) ، لذلك سيكون من الخطأ العمل مباشرة مع مكوناته. عند العمل مع مكونات أي تجريد ، يجب علينا استخدام واجهة التجريد ، وإلا ستتحول الشفرة إلى فوضى (سيصبح من الصعب قراءتها ، أسهل في ارتكاب خطأ ، ولكن الأهم من ذلك التغيير أكثر صعوبة ، لأنه يجب إجراء تغيير واحد في أجزاء مختلفة من التعليمات البرمجية).


يمكن تنفيذ الأزواج بطرق مختلفة جدًا. في أي لغة برمجة ، هناك أكثر من فرصة لتطبيق هيكل البيانات هذا.
على سبيل المثال ، يمكنك تنفيذ زوج لأنواع البيانات العشوائية:

//       
//      
const pair = (first, second) => (elementType) => {
  switch(elementType) {
    case 'first':
     return first;
   case 'second':
     return second;
  }
};
//    ,      ,
//    
const getFirstElement = (pair) => (pair('first'));
const getSecondElement = (pair) => (pair('second'));

تسمى طريقة العمل هذه بإرسال الرسائل. من الغريب ، ولكن كانت طريقة التفاعل هذه بين الكيانات هي التي كانت تسمى OOP قبل C / C ++.

هل علينا استخدام التصميم switch ؟ بالطبع ، ليس بالضرورة. هذه هي تفاصيل التنفيذ الفني ، ويمكن أن يكون هناك العديد من التطبيقات!

الشيء الأكثر أهمية هو جعل الزوجين قابلة للتغيير!

على سبيل المثال ، يمكنك تنفيذ نفس الوظيفة باستخدام الخريطة

//     
//  ,       
const pair = (first, second) => (
  new Map([
    ['first', first],
    ['second', second],
  ])
);
//    
const getFirst = (pair) => (pair.get('first'));
const getSecond = (pair) => (pair.get('second'));

لاحظ أنه يمكن بسهولة استبدال التنفيذ الأول بالتنفيذ الثاني. والثاني إلى الأول! لن يتغير رمز الاتصال. هذه ميزة مهمة للتجريد. يمكننا بسهولة تغيير تنفيذ التجريد في البرنامج ، لكن الكود الذي يعمل مع هذا التجريد لن يعرف عن هذه التغييرات. لن نحتاج إلى تعديل الشفرة التي تعمل مع هذه التجريدات إذا أردنا تغيير تنفيذ التجريدات نفسها. هذا مهم جدا لأنه يوفر أموال العملاء ويرفع مكافآت المطورين.

بالمناسبة ، دعنا نقول أننا لا نعرف عن وجود مابوف في شبيبة ، ولكن يمكننا العمل مع الأشياء.

//       
//   
const pair = (first, second) => (
  Object.freeze({
    'first': first,
    'second': second,
  })
);
//    ,       
const getFirst = (pair) => (pair.first);
const getSecond = (pair) => (pair.second);

كما يمكنك أن تخمن بسهولة ، يمكن أيضًا استبدال كل من عمليات التنفيذ السابقة بأخرى ثالثة ولن يتغير شيء من هذا (في الواقع ، هناك بعض الاختلاف. الخرائط لا ترمي استثناءات عند محاولة تغيير الخصائص مباشرة ، لكنها ترمي "كائنات مجمدة" TypeError: Cannot assign to read only property. ومع ذلك ، في سياق هذه المقالة المهم. ).

لماذا نحتاج إلى معرفة هياكل البيانات النظرية؟


إذا نظرنا إلى عمل مبرمج من منظور عين الطائر ، فسوف نرى أنه في جوهره يشارك في ذلك. الذي ينشئ أدوات تعالج مجموعات بيانات معينة. لذلك ، يتعين علينا باستمرار تحديد نوع من هيكل التخزين للبيانات والتوصل إلى طرق للعمل معها أو إعطاء مجموعة البيانات الفوضوية بنية محددة. يتيح لنا فهم هياكل البيانات النموذجية امتلاك مجموعة من الحلول الجاهزة للمهام النموذجية المختلفة واختيار الطريقة الأكثر ملاءمة لمهمة معينة.

دعنا

نحلل مثال: "تنفيذ آلية للعمل مع الكسور. يجب تخزين الكسور بالتنسيق المعتاد. أولئك. في شكل 1/2. من الضروري أيضًا تنفيذ العمليات الأساسية (الجمع والطرح والضرب والقسمة). يجب توفير التطبيع ".

دعونا نفهم حالة المشكلة! نحن بحاجة إلى تنفيذ تجريد لبعض الكيانات الرياضية ، وبالتالي فمن المعقول الرجوع إلى المصدر الأصلي . بالطبع ، إذا قرأ المبرمجون ذوو الخبرة هذا النص ، فلا بد أنهم قد تخيلوا على الفور الحل في التعليمات البرمجية دون قراءة المقالة حول الكسور في الرياضيات ، لكننا سنفترض. أننا نتخيل بشكل ضعيف كيف تعمل الكسور وتوضح مجرى المنطق بأكمله.

تخبرنا مادة ويكيبيديا أن الكسور عادية (1/4) وعشرية (0.1). من الواضح ، حسب حالة المشكلة ، نحتاج إلى العمل مع الكسور بتنسيق العرض التقديمي المعتاد.

نرى أيضًا أن الكسر العادي هو اتحاد منطقي لاثنينالأرقام (البسط والمقام) ، هذه هي أضمن إشارة حول الحاجة إلى استخدام زوج كبنية بيانات لهذه المهمة.

في الكود ، يمكننا وصف هذا الهيكل على النحو التالي:

//        ,
//  ,    
//          ,
//     
const makeRational = (num, denom) => (
  new Map([
    ['num', num],
    ['denom', denom],
  ])
);

//  ,    
//   ,      ,    
//    
const getNumer = (rat) => (rat.get('num'));
const getDenom = (rat) => (rat.get('denom'));

بعد ذلك ، يجب أن نحلل الوضع مع كسور مثل هذه الخطة 8/4 ، 6/9. تقول حالة المهمة عن الحاجة إلى توفير التطبيع. تطبيع الكسر هو إزالة أكبر القاسم المشترك (GCD) منه.

نقوم بتنفيذ وظيفة للبحث عن رقمين GCD:

//Gcd     
const getGcd = (a, b) => ((a % b) ? getGcd(b, a % b) : Math.abs(b));

إذا لم يكن نص الوظيفة واضحًا لك ، فأنا أنصحك بالقراءة عن العودية .

لتطبيع الكسر ، نحتاج إلى تقسيم البسط والمقام على GCD. نكتب وظيفة التطبيع:

const normalize = (n1, n2) => {
  const commonDivisor = getGcd(n1, n2);
  return [n1 / commonDivisor, n2 / commonDivisor];
};

سيكون من المنطقي وضع التطبيع في منشئ makeRational لتطبيع البيانات دائمًا عند إنشاء الكسر.

const makeRational = (num, denom) => {
  const [normalizeNum, normalizeDenom] = normalize(num, denom);
  return new Map([
    ['num', normalizeNum],
    ['denom', normalizeDenom],
  ]);
};

بعد ذلك ، سنعمل على واجهة العمليات.

1)

إضافة لإضافة كسرين عاديين ، يجب عليك إحضارهما إلى قاسم مشترك. ثم أضف البسط واترك المقام دون تغيير.

//..       ,
//       ,
//         
const add = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getDenom(rat2) + getNumer(rat2) * getDenom(rat1),
    getDenom(rat1) * getDenom(rat2),
  ));

2) الطرح من

أجل الحصول على اختلاف الكسور ، يجب أيضًا اختزالهم إلى قاسم مشترك ، ثم يجب طرح البسط ، ويجب ترك المقام دون تغيير.

//    
//     
const sub = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getDenom(rat2) - getNumer(rat2) * getDenom(rat1),
    getDenom(rat1) * getDenom(rat2),
  ));

3) الضرب

لمضاعفة كسرين عاديين ، تحتاج إلى مضاعفة البسط والمقام.

const multi = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getNumer(rat2),
    getDenom(rat1) * getDenom(rat2),
  ));


4) القسمة من

أجل تقسيم جزء عادي إلى آخر ، تحتاج إلى ضرب الكسر الأول في الكسر العكسي للجزء الثاني. يُسمى العكسي كسرًا يساوي بسط مقامه الأصلي ، والمقام هو بسط الأصل.

const div = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getDenom(rat2),
    getDenom(rat1) * getNumer(rat2),
  ));

كما ترى ، نقوم في كل عملية بإنشاء كيان جديد. هذا نتيجة لقاعدة الثبات. لاحظ أنه في الرياضيات العمليات لا يتغير العدد الأصلي من الأرقام، وخلق جديد: 1 + 2 = 3.

يجب الانتباه. أنه يمكننا تغيير التطبيق makeRationalإلى أي تطبيق آخر ، لكن رمز الاتصال لن يعرف عنه وسيستمر في العمل. هذا نتيجة لحقيقة أننا نفذنا التجريد بشكل صحيح والعمل مع مكوناته من خلال الواجهة ، وليس بشكل مباشر.

من المهم أيضًا أن يتمكن المستخدم من الحصول على بياناته بالطريقة المعتادة ، لذلك نقدم وظيفة إضافية واحدة.

const ratToString = (rat) => `${getNumer(rat)}/${getDenom(rat)}`;

سوف أرفق قائمة الاختبارات:

describe('normalize', () => {
  test('should work', () => {
    expect(normalize(21, 6)).toEqual([7, 2]);
    expect(normalize(2, 3)).toEqual([2, 3]);
  });
});


describe('rational', () => {
  test('getters', () => {
    const rat1 = makeRational(3, 9);
    expect(getNumer(rat1)).toBe(1);
    expect(getDenom(rat1)).toBe(3);

    const rat3 = makeRational(-4, 16);
    expect(getNumer(rat3)).toBe(-1);
    expect(getDenom(rat3)).toBe(4);
  });

  test('add&sub', () => {
    const rat1 = makeRational(3, 9);
    const rat2 = makeRational(10, 3);
    expect(add(rat1, rat2)).toEqual(makeRational(11, 3));
    expect(sub(rat1, rat2)).toEqual(makeRational(-3, 1));

    const rat4 = makeRational(12, 5);
    const rat3 = makeRational(-4, 16);
    expect(add(rat3, rat4)).toEqual(makeRational(43, 20));
    expect(sub(rat3, rat4)).toEqual(makeRational(-53, 20));

    const rat5 = makeRational(1, 15);
    const rat6 = makeRational(4, 25);
    expect(add(rat5, rat6)).toEqual(makeRational(17, 75));
    expect(sub(rat5, rat6)).toEqual(makeRational(-7, 75));
  });

  test('multi&div', () => {
    const rat1 = makeRational(1, 2);
    const rat2 = makeRational(2, 3);
    expect(multi(rat1, rat2)).toEqual(makeRational(2, 6));

    const rat3 = makeRational(1, 3);
    const rat4 = makeRational(1, 2);
    expect(div(rat3, rat4)).toEqual(makeRational(2, 3));
  });

  test('ratToString', () => {
    const rat1 = makeRational(3, 9);
    const rat3 = makeRational(-4, 16);
    expect(ratToString(rat1)).toBe('1/3');
    expect(ratToString(rat3)).toBe('-1/4');
  });
});

استنتاج


تبين أن النص ضخم للغاية ، ولكن آمل أن يكون واضحًا. كي تختصر:

  1. يمكننا استخدام بنية بيانات البخار كلما احتجنا إلى الجمع بين قيمتين غير مرتبطين بشكل منطقي
  2. يجب أن يكون الزوجان محصنين
  3. يجب أن يوفر الزوجان واجهة للعمل مع مكوناتهما

أعتذر مسبقًا إذا بدت المقالة محملة بأكثر من شخص. أركز بشكل أساسي على المطورين غير المتمرسين ، لذا أحاول الكتابة بطريقة تجعل منطق تفكيري واختيار هذا الحل أو ذاك واضحًا للغاية.

إذا كانت المقالة تهم المجتمع. ثم سيستمر اتجاه هياكل البيانات النموذجية . الموضوع التالي هو القوائم ذات الصلة.

All Articles