ไทย

สำรวจพื้นฐานของ Binary Search Trees (BSTs) และเรียนรู้วิธีการนำไปใช้งานอย่างมีประสิทธิภาพใน JavaScript คู่มือนี้ครอบคลุมโครงสร้าง การดำเนินการ และตัวอย่างสำหรับนักพัฒนาทั่วโลก

Binary Search Trees: คู่มือการนำไปใช้งานอย่างละเอียดใน JavaScript

Binary Search Trees (BSTs) เป็นโครงสร้างข้อมูลพื้นฐานในวิทยาการคอมพิวเตอร์ ซึ่งใช้กันอย่างแพร่หลายสำหรับการค้นหา การจัดเรียง และการดึงข้อมูลอย่างมีประสิทธิภาพ โครงสร้างแบบลำดับชั้นของมันช่วยให้การดำเนินการหลายอย่างมีความซับซ้อนทางเวลาแบบลอการิทึม (logarithmic time complexity) ทำให้เป็นเครื่องมือที่มีประสิทธิภาพในการจัดการชุดข้อมูลขนาดใหญ่ คู่มือนี้จะให้ภาพรวมที่ครอบคลุมของ BSTs และสาธิตการนำไปใช้งานใน JavaScript สำหรับนักพัฒนาทั่วโลก

ทำความเข้าใจเกี่ยวกับ Binary Search Trees

Binary Search Tree คืออะไร?

Binary Search Tree เป็นโครงสร้างข้อมูลแบบทรีที่แต่ละโหนดมีลูกได้ไม่เกินสองตัว ซึ่งเรียกว่าลูกด้านซ้ายและลูกด้านขวา คุณสมบัติที่สำคัญของ BST คือสำหรับโหนดใดๆ:

คุณสมบัตินี้ทำให้แน่ใจได้ว่าองค์ประกอบใน BST จะถูกจัดเรียงอยู่เสมอ ซึ่งช่วยให้การค้นหาและดึงข้อมูลมีประสิทธิภาพ

แนวคิดหลัก

การนำ Binary Search Tree ไปใช้งานใน JavaScript

การกำหนดคลาส Node

ขั้นแรก เราจะกำหนดคลาส `Node` เพื่อแทนแต่ละโหนดใน BST แต่ละโหนดจะประกอบด้วย `key` เพื่อเก็บข้อมูล และตัวชี้ `left` และ `right` ไปยังลูกของมัน


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

การกำหนดคลาส Binary Search Tree

ถัดไป เราจะกำหนดคลาส `BinarySearchTree` คลาสนี้จะประกอบด้วยโหนดราก (root) และเมธอดสำหรับการแทรก ค้นหา ลบ และการท่องไปในทรี


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // เมธอดจะถูกเพิ่มที่นี่
}

การแทรกข้อมูล (Insertion)

เมธอด `insert` จะเพิ่มโหนดใหม่พร้อมกับคีย์ที่กำหนดเข้าไปใน BST กระบวนการแทรกข้อมูลจะรักษาสมบัติของ BST โดยการวางโหนดใหม่ในตำแหน่งที่เหมาะสมเมื่อเทียบกับโหนดที่มีอยู่


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

ตัวอย่าง: การแทรกค่าลงใน BST


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

การค้นหา (Searching)

เมธอด `search` จะตรวจสอบว่ามีโหนดที่มีคีย์ที่กำหนดอยู่ใน BST หรือไม่ โดยจะท่องไปในทรี เปรียบเทียบคีย์กับคีย์ของโหนดปัจจุบัน และย้ายไปยังทรีย่อยด้านซ้ายหรือขวาตามความเหมาะสม


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  if (node === null) {
    return false;
  }

  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
}

ตัวอย่าง: การค้นหาค่าใน BST


console.log(bst.search(9));  // ผลลัพธ์: true
console.log(bst.search(2));  // ผลลัพธ์: false

การลบข้อมูล (Deletion)

เมธอด `remove` จะลบโหนดที่มีคีย์ที่กำหนดออกจาก BST นี่เป็นการดำเนินการที่ซับซ้อนที่สุด เนื่องจากต้องรักษาสมบัติของ BST ไว้ขณะลบโหนด มีสามกรณีที่ต้องพิจารณา:


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }

  if (key < node.key) {
    node.left = this.removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key);
    return node;
  } else {
    // key เท่ากับ node.key

    // กรณีที่ 1 - เป็นโหนดใบ
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // กรณีที่ 2 - โหนดมีลูกเพียง 1 ตัว
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // กรณีที่ 3 - โหนดมีลูก 2 ตัว
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

ตัวอย่าง: การลบค่าออกจาก BST


bst.remove(7);
console.log(bst.search(7)); // ผลลัพธ์: false

การท่องไปในทรี (Tree Traversal)

การท่องไปในทรีคือการเยี่ยมชมแต่ละโหนดในทรีตามลำดับที่เฉพาะเจาะจง มีวิธีการท่องไปในทรีที่นิยมใช้กันหลายวิธี:


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

ตัวอย่าง: การท่องไปใน BST


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // ผลลัพธ์: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // ผลลัพธ์: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // ผลลัพธ์: 3 8 10 9 5 12 14 13 18 25 20 15 11

ค่าต่ำสุดและค่าสูงสุด

การหาค่าต่ำสุดและสูงสุดใน BST นั้นทำได้ง่ายมาก ด้วยคุณสมบัติการจัดเรียงของมัน


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

max() {
  return this.maxNode(this.root);
}

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

ตัวอย่าง: การหาค่าต่ำสุดและสูงสุด


console.log(bst.min().key); // ผลลัพธ์: 3
console.log(bst.max().key); // ผลลัพธ์: 25

การประยุกต์ใช้งานจริงของ Binary Search Trees

Binary Search Trees ถูกนำไปใช้ในงานหลากหลายประเภท รวมถึง:

ข้อควรพิจารณาด้านประสิทธิภาพ

ประสิทธิภาพของ BST ขึ้นอยู่กับโครงสร้างของมัน ในสถานการณ์ที่ดีที่สุด BST ที่สมดุลจะช่วยให้การแทรก ค้นหา และลบข้อมูลมีความซับซ้อนทางเวลาแบบลอการิทึม อย่างไรก็ตาม ในสถานการณ์ที่แย่ที่สุด (เช่น ทรีที่เอียงไปข้างเดียว) ความซับซ้อนทางเวลาอาจลดลงไปเป็นแบบเชิงเส้น (linear time)

ทรีที่สมดุลและไม่สมดุล (Balanced vs. Unbalanced Trees)

BST ที่สมดุลคือทรีที่ความสูงของทรีย่อยด้านซ้ายและขวาของทุกโหนดต่างกันไม่เกินหนึ่ง อัลกอริทึมที่ปรับสมดุลด้วยตนเอง เช่น AVL trees และ Red-Black trees จะช่วยให้แน่ใจว่าทรียังคงสมดุลอยู่เสมอ ซึ่งให้ประสิทธิภาพที่สม่ำเสมอ ในแต่ละภูมิภาคอาจต้องการระดับการปรับให้เหมาะสมที่แตกต่างกันตามภาระงานบนเซิร์ฟเวอร์ การปรับสมดุลจะช่วยรักษาประสิทธิภาพภายใต้การใช้งานระดับโลกที่มีปริมาณมาก

ความซับซ้อนทางเวลา (Time Complexity)

แนวคิด BST ขั้นสูง

ทรีที่ปรับสมดุลด้วยตนเอง (Self-Balancing Trees)

ทรีที่ปรับสมดุลด้วยตนเองคือ BST ที่ปรับโครงสร้างของมันโดยอัตโนมัติเพื่อรักษาสมดุล สิ่งนี้ช่วยให้แน่ใจว่าความสูงของทรียังคงเป็นแบบลอการิทึม ทำให้การดำเนินการทั้งหมดมีประสิทธิภาพที่สม่ำเสมอ ทรีที่ปรับสมดุลด้วยตนเองที่พบบ่อย ได้แก่ AVL trees และ Red-Black trees

AVL Trees

AVL trees รักษาสมดุลโดยการทำให้แน่ใจว่าผลต่างของความสูงระหว่างทรีย่อยด้านซ้ายและขวาของโหนดใดๆ มีค่าไม่เกินหนึ่ง เมื่อความสมดุลนี้ถูกรบกวน จะมีการหมุน (rotations) เพื่อคืนความสมดุล

Red-Black Trees

Red-Black trees ใช้คุณสมบัติของสี (แดงหรือดำ) เพื่อรักษาสมดุล มีความซับซ้อนกว่า AVL trees แต่ให้ประสิทธิภาพที่ดีกว่าในบางสถานการณ์

ตัวอย่างโค้ด JavaScript: การนำ Binary Search Tree ไปใช้งานฉบับสมบูรณ์


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    if (node === null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    if (node === null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // key เท่ากับ node.key

      // กรณีที่ 1 - เป็นโหนดใบ
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // กรณีที่ 2 - โหนดมีลูกเพียง 1 ตัว
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // กรณีที่ 3 - โหนดมีลูก 2 ตัว
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// ตัวอย่างการใช้งาน
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("การท่องไปในทรีแบบ In-order:");
bst.inOrderTraverse(printNode);

console.log("การท่องไปในทรีแบบ Pre-order:");
bst.preOrderTraverse(printNode);

console.log("การท่องไปในทรีแบบ Post-order:");
bst.postOrderTraverse(printNode);

console.log("ค่าต่ำสุด:", bst.min().key);
console.log("ค่าสูงสุด:", bst.max().key);

console.log("ค้นหา 9:", bst.search(9));
console.log("ค้นหา 2:", bst.search(2));

bst.remove(7);
console.log("ค้นหา 7 หลังจากการลบ:", bst.search(7));

บทสรุป

Binary Search Trees เป็นโครงสร้างข้อมูลที่มีประสิทธิภาพและหลากหลายพร้อมการใช้งานมากมาย คู่มือนี้ได้ให้ภาพรวมที่ครอบคลุมของ BSTs ครอบคลุมถึงโครงสร้าง การดำเนินการ และการนำไปใช้งานใน JavaScript โดยการทำความเข้าใจหลักการและเทคนิคที่กล่าวถึงในคู่มือนี้ นักพัฒนาทั่วโลกสามารถใช้ BSTs เพื่อแก้ปัญหาที่หลากหลายในการพัฒนาซอฟต์แวร์ได้อย่างมีประสิทธิภาพ ตั้งแต่การจัดการฐานข้อมูลระดับโลกไปจนถึงการเพิ่มประสิทธิภาพอัลกอริทึมการค้นหา ความรู้เกี่ยวกับ BSTs เป็นทรัพย์สินอันล้ำค่าสำหรับโปรแกรมเมอร์ทุกคน

ในขณะที่คุณเดินทางต่อไปในสายวิทยาการคอมพิวเตอร์ การสำรวจแนวคิดขั้นสูงเช่นทรีที่ปรับสมดุลด้วยตนเองและการนำไปใช้งานในรูปแบบต่างๆ จะช่วยเพิ่มความเข้าใจและความสามารถของคุณให้มากยิ่งขึ้น หมั่นฝึกฝนและทดลองกับสถานการณ์ต่างๆ เพื่อฝึกฝนศิลปะการใช้ Binary Search Trees ให้เชี่ยวชาญ