Struktur data teoritis dan aplikasinya dalam JavaScript. P1. Pasangan

“Pemrogram yang buruk memikirkan kode. Pemrogram yang baik berpikir tentang struktur data dan hubungannya, ”Linus Torvalds, pencipta Linux.
Kami menganggap sebagai aksioma bahwa seringkali solusi untuk masalah apa pun dalam pemrograman datang dengan memilih struktur data yang tepat. Aksioma ini dapat dibuktikan, tetapi itu adalah waktu yang lama dan artikelnya sedikit tentang yang lain. Hari ini kita akan berbicara tentang salah satu struktur teoretis paling sederhana, tentang pasangan.

Sepasang adalah struktur data yang menyimpan dua elemen data dalam dirinya sendiri, menyiratkan bahwa kami ingin menggabungkan keduanya secara logis menjadi satu. Ini memecahkan masalah ini dengan tepat, mis. jika ada beberapa elemen data dan bkami perlu menyajikannya dalam bentuk b, maka kami dapat mengimplementasikan desain ini berpasangan.

Jika Anda berpikir bahwa Anda belum pernah menemukan atau tidak akan menemukan sesuatu seperti ini, maka ini tidak benar.

Berikut adalah contoh paling sederhana dari properti yang dihitung dari dokumentasi VUE:

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

Dari sudut pandang teori, fullNameini adalah pasangan. Dan ada banyak contoh seperti itu dalam teknologi dan dunia nyata, yang sering kita modelkan dengan kode program.

Anda dapat melihat bahwa kami secara logis dapat menggabungkan sejumlah elemen dan Anda akan benar. Ini membawa kita ke konsep struktural yang lebih kompleks dari data komposit , salah satunya adalah pasangan . Kami akan menangani data komposit sedikit kemudian, sekarang kami akan fokus pada pasangan.

Teori memberi tahu kita bahwa pasangan memiliki sifat-sifat berikut:

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

    :

    pair = { a, b }
    

    Jika dalam kode panggilan kita akan bekerja dengan pasangan secara langsung dalam gaya ini: pair.akemudian mengubah implementasi dari pasangan kita akan dipaksa untuk menulis ulang kode panggilan di mana pun panggilan ke pasangan muncul. Ini tidak bagus!

    Pasangan adalah abstraksi (bahkan level rendah), sehingga akan salah untuk bekerja secara langsung dengan komponen-komponennya. Ketika bekerja dengan komponen abstraksi apa pun, kita harus menggunakan antarmuka abstraksi, jika tidak maka kode akan berubah menjadi kekacauan (akan menjadi lebih sulit dibaca, lebih mudah untuk membuat kesalahan, tetapi yang paling penting lebih sulit untuk diubah, karena satu perubahan harus dilakukan di berbagai bagian kode).


Pasangan dapat diimplementasikan dengan cara yang sangat berbeda. Dalam bahasa pemrograman apa pun, ada lebih dari satu peluang untuk mengimplementasikan struktur data ini.
Misalnya, Anda bisa menerapkan pasangan untuk tipe data sewenang-wenang:

//       
//      
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'));

Cara kerja ini disebut mengirim pesan. Anehnya, tapi ini cara interaksi antara entitas yang pernah disebut OOP sebelum C / C ++.

Apakah kita harus menggunakan desain switch ? Tentu saja belum tentu. Ini adalah detail dari implementasi teknis, dan mungkin ada banyak sekali implementasi!

Yang paling penting adalah membuat pasangan bisa berubah!

Misalnya, Anda dapat menerapkan fungsi yang sama menggunakan Peta

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

Perhatikan bahwa implementasi pertama dapat dengan mudah diganti dengan yang kedua. dan yang kedua ke yang pertama! Kode panggilan tidak akan berubah. Ini adalah keuntungan penting dari abstraksi. Kita dapat dengan mudah mengubah implementasi abstraksi dalam program, tetapi kode yang bekerja dengan abstraksi ini tidak akan tahu tentang perubahan ini. Kami tidak perlu mengedit kode yang berfungsi dengan abstraksi ini jika kami ingin mengubah implementasi abstraksi itu sendiri. Ini sangat penting karena menghemat uang pelanggan dan meningkatkan bonus pengembang.

Ngomong-ngomong, katakanlah kita tidak tahu tentang keberadaan Mapov di js, tetapi kita dapat bekerja dengan objek.

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

Seperti yang dapat Anda tebak dengan mudah, kedua implementasi sebelumnya juga dapat diganti dengan yang ketiga dan tidak ada yang akan berubah dari ini ( Sebenarnya, ada beberapa perbedaan. Peta tidak melempar pengecualian ketika mencoba mengubah properti secara langsung, tetapi mereka melempar "objek beku" TypeError: Cannot assign to read only property. Namun, dalam konteks artikel ini penting. ).

Mengapa kita membutuhkan pengetahuan tentang struktur data teoritis?


Jika kita melihat karya seorang programmer dari pandangan mata burung, kita akan melihat bahwa pada dasarnya dia terlibat di dalamnya. yang menciptakan alat yang memanipulasi set data tertentu. Oleh karena itu, kami terus-menerus harus memilih beberapa jenis struktur penyimpanan untuk data dan menemukan cara untuk bekerja dengannya atau memberikan data kacau mengatur struktur tertentu. Memahami struktur data yang khas memungkinkan kita memiliki seperangkat solusi siap pakai untuk tugas-tugas tipikal yang berbeda dan cukup memilih cara yang paling nyaman untuk tugas tertentu.

Mari kita

analisis sebuah contoh: “Terapkan mekanisme untuk bekerja dengan pecahan. Pecahan harus disimpan dalam format yang biasa. itu. dalam bentuk 1/2. Juga diperlukan untuk mengimplementasikan operasi dasar (penambahan, pengurangan, penggandaan, pembagian). Normalisasi perlu disediakan. "

Mari kita pahami kondisi masalahnya! Kita perlu mengimplementasikan abstraksi untuk beberapa entitas matematika, oleh karena itu masuk akal untuk merujuk ke sumber aslinya . Tentu saja, jika programmer berpengalaman membaca teks ini, maka mereka harus segera membayangkan solusi dalam kode tanpa membaca artikel tentang pecahan dalam matematika, tetapi kami akan menganggap. bahwa kita samar-samar membayangkan bagaimana pecahan bekerja dan menggambarkan seluruh rangkaian penalaran.

Materi Wikipedia memberi tahu kita bahwa pecahan biasa (1/4) dan desimal (0,1). Jelas, dengan kondisi masalah, kita perlu bekerja dengan pecahan dalam format presentasi biasa.

Kita juga melihat bahwa fraksi biasa adalah gabungan logis dari keduanyaangka (pembilang dan penyebut), ini adalah sinyal paling pasti tentang perlunya menggunakan pasangan sebagai struktur data untuk tugas ini.

Dalam kode, kita dapat menggambarkan struktur ini sebagai berikut:

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

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

Selanjutnya, kita harus menganalisis situasi dengan pecahan dari rencana seperti itu 8/4, 6/9. Kondisi tugas mengatakan tentang perlunya menyediakan normalisasi. Normalisasi fraksi adalah penghapusan pembagi umum terbesar (GCD) darinya.

Kami menerapkan fungsi untuk mencari dua nomor GCD:

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

Jika teks fungsi tidak jelas bagi Anda, maka saya menyarankan Anda untuk membaca tentang rekursi .

Untuk menormalkan fraksi, kita perlu membagi pembilang dan penyebutnya dengan GCD. Kami menulis fungsi normalisasi:

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

Akan logis untuk meletakkan normalisasi di konstruktor makeRational untuk selalu menormalkan data saat membuat fraksi.

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

Selanjutnya, kami akan bekerja pada antarmuka operasi.

1) Tambahan

Untuk menambahkan dua pecahan biasa, Anda harus membawanya ke penyebut yang sama. Kemudian tambahkan pembilang dan biarkan penyebutnya tidak berubah.

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

2) Pengurangan

Untuk mendapatkan perbedaan fraksi, mereka juga perlu direduksi menjadi penyebut umum, dan kemudian kurangi pembilangnya, penyebut harus dibiarkan tidak berubah.

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

3) Perkalian

Untuk mengalikan dua pecahan biasa, Anda perlu mengalikan pembilang dan penyebutnya.

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


4) Pembagian

Untuk membagi satu pecahan biasa menjadi pecahan lain, Anda perlu mengalikan pecahan pertama dengan pecahan pecahan pecahan kedua. Invers disebut pecahan yang pembilangnya sama dengan penyebut aslinya, dan penyebutnya adalah pembilang yang asli.

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

Seperti yang Anda lihat, dalam setiap operasi kami membuat entitas baru. Ini adalah konsekuensi dari aturan ketetapan. Perhatikan bahwa dalam matematika operasi tidak mengubah nomor asli angka, dan membuat baru: 1 + 2 = 3.

Harus memperhatikan. bahwa kita dapat mengubah implementasi makeRationalke yang lain, tetapi kode panggilan tidak akan mengetahuinya dan akan terus bekerja. Ini adalah konsekuensi dari fakta bahwa kami mengimplementasikan abstraksi dengan benar dan bekerja dengan komponen-komponennya melalui antarmuka, dan tidak secara langsung.

Penting juga bagi pengguna untuk mendapatkan datanya dengan cara biasa, jadi kami memperkenalkan satu fungsi tambahan.

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

Saya akan melampirkan daftar tes:

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');
  });
});

Kesimpulan


Teksnya ternyata sangat banyak, tapi saya harap itu jelas. Untuk meringkas:

  1. Kita dapat menggunakan struktur data uap setiap kali kita perlu secara logis menggabungkan dua nilai yang tidak terkait
  2. Pasangan itu harus kebal
  3. Pasangan harus menyediakan antarmuka untuk bekerja dengan komponen mereka

Saya minta maaf sebelumnya jika artikel tersebut kelihatannya kelebihan beban kepada seseorang. Saya terutama berfokus pada pengembang yang tidak terlalu berpengalaman, jadi saya mencoba menulis sedemikian rupa sehingga logika alasan saya dan pilihan solusi ini atau itu sangat jelas.

Jika artikel tersebut menarik bagi masyarakat. maka arah struktur data yang khas akan berlanjut. Topik selanjutnya adalah daftar terkait.

All Articles