Viễn Đinh

Viễn Đinh

Khá Hơn 0,5% Mỗi Ngày

Cách Giải Cây Tìm Kiếm Nhị Phân (Binary Search Tree), Dể Hiểu

Bạn có bao giờ tự hỏi làm thế nào máy tính tìm kiếm thông tin một cách nhanh chóng không? Từ điển, danh bạ điện thoại, hay thậm chí các bài đăng trên mạng xã hội đều được tổ chức và tìm kiếm dựa trên những cấu trúc dữ liệu. Trong số đó, Cây Tìm Kiếm Nhị Phân là một trong những cấu trúc phổ biến nhất, ra đời từ nhu cầu tổ chức dữ liệu theo cách tối ưu hóa tốc độ tìm kiếm, thêm, và xóa.

Định Nghĩa

Cây Tìm Kiếm Nhị Phân (Binary Search Tree), hay viết tắt là BST, là một loại cây nhị phân đặc biệt. Bạn có thể tưởng tượng BST như một bản đồ nhỏ, nơi mỗi điểm giao nhau được gọi là “nút” và mỗi nút lưu trữ một giá trị.

Đặc điểm chính của BST là tính chất sắp xếp: giá trị của nút con bên trái luôn nhỏ hơn giá trị của nút cha, và giá trị của nút con bên phải luôn lớn hơn giá trị của nút cha. Tính chất này giúp BST có thể tìm kiếm dữ liệu rất nhanh, tương tự như cách bạn tìm từ trong từ điển.

Cây có thể phát triển từ một nút gốc duy nhất và mở rộng không giới hạn, miễn là nó duy trì nguyên tắc trên. Nếu một cây không tuân thủ quy tắc này, nó không được coi là một BST.

BST không chỉ là một cách tổ chức dữ liệu thông minh mà còn là nền tảng của nhiều thuật toán, từ sắp xếp đến xử lý dữ liệu.

Cây Tìm Kiếm Nhị Phân
Cây Tìm Kiếm Nhị Phân

Cách Giải

Cấu trúc dữ liệu Cây Tìm Kiếm Nhị Phân hoạt động trên nguyên tắc so sánh và sắp xếp. Sau đây là các phương thức quan trọng của nó:

Bạn thấy đó, mỗi phương thức được thiết kế để tương tác với cây theo cách tối ưu và hiệu quả.

Triển Khai

Dưới đây là một cách triển khai Cây Tìm Kiếm Nhị Phân bằng TypeScript:

class Node {
  value: number;
  left: Node | null;
  right: Node | null;

  constructor(value: number) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  root: Node | null;

  constructor() {
    this.root = null;
  }

  // Thêm giá trị vào cây, duy trì quy tắc sắp xếp
  add(value: number) {
    const node = new Node(value);

    if (this.root === null) {
      this.root = node;
    }

    let current = this.root;

    while (current) {
      if (node.value === current.value) {
        return;
      }

      const dirrection = node.value < current.value ? 'left' : 'right';

      if (current[dirrection] === null) {
        current[dirrection] = node;
        return;
      }

      current = current[dirrection];
    }
  }

  // Tìm giá trị nhỏ nhất trong cây
  findMin(node = this.root) {
    if (node === null) {
      return null;
    }

    let current = node;

    while (current) {
      if (current.left === null) {
        return current.value;
      }

      current = current.left;
    }
  }

  // Tìm giá trị lớn nhất trong cây
  findMax(node = this.root) {
    if (node === null) {
      return null;
    }

    let current: Node | null = node;

    while (current) {
      if (current.right === null) {
        return current.value;
      }

      current = current.right;
    }
  }

  // Kiểm tra xem một giá trị có tồn tại trong cây không
  isPresent(value: number) {
    let current = this.root;

    while (current) {
      const dirrection = value < current.value ? 'left' : 'right';

      if (current.value === value) {
        return true;
      }

      current = current[dirrection];
    }

    return false;
  }

  // Tìm chiều cao nhỏ nhất của cây
  findMaxHeight() {
    const traverse = (node: Node | null): number => {
      if (node === null) return -1;

      return 1 + Math.max(traverse(node.left), traverse(node.right));
    };

    return traverse(this.root);
  }

  // Tìm chiều cao lớn nhất của cây
  findMinHeight() {
    const traverse = (node: Node | null): number => {
      if (node === null) {
        return -1;
      }

      return 1 + Math.min(traverse(node.left), traverse(node.right));
    };

    return traverse(this.root);
  }

  // Kiểm tra cây có cân bằng không
  isBalanced() {
    return this.findMinHeight() === this.findMaxHeight();
  }

  // Duyệt cây theo thứ tự tăng dần
  // https://www.youtube.com/watch?v=ne5oOmYdWGw
  inOrder = () => {
    if (this.root === null) return null;

    const traverse = (node: Node | null): number[] => {
      if (node === null) return [];

      const values: number[] = [];

      values.push(...traverse(node.left));
      values.push(node.value);
      values.push(...traverse(node.right));

      return values;
    };

    return traverse(this.root);
  };

  // Duyệt cây bắt đầu từ nút gốc
  // https://www.youtube.com/watch?v=gLx7Px7IEzg
  preOrder() {
    if (this.root === null) return null;

    const values: number[] = [];

    const traverse = (node: Node | null) => {
      if (node === null) return;

      values.push(node.value);
      traverse(node.left);
      traverse(node.right);
    };

    traverse(this.root);

    return values;
  }

  // Duyệt cây kết thúc tại nút gốc
  // https://www.youtube.com/watch?v=a8kmbuNm8Uo
  postOrder = () => {
    if (this.root === null) return null;

    const traverse = (node: Node | null): number[] => {
      if (node === null) return [];

      const values: number[] = [];

      values.push(...traverse(node.left));
      values.push(...traverse(node.right));
      values.push(node.value);

      return values;
    };

    return traverse(this.root);
  };

  // Duyệt cây theo tầng
  levelOrder = () => {
    if (this.root === null) return null;

    const results: number[] = [];
    const queue: Node[] = [this.root];

    const pushIfThere = (node: Node | null) => {
      if (node) queue.push(node);
    };

    while (queue.length) {
      const node = queue.shift()!;

      results.push(node.value);

      pushIfThere(node.left);
      pushIfThere(node.right);
    }

    return results;
  };

  // Duyệt cây theo tầng ngược
  reverseLevelOrder = () => {
    if (this.root === null) return null;

    const results: number[] = [];
    const queue: Node[] = [this.root];

    const pushIfThere = (node: Node | null) => {
      if (node) queue.push(node);
    };

    while (queue.length > 0) {
      const node = queue.shift()!;

      results.push(node.value);

      pushIfThere(node.right);
      pushIfThere(node.left);
    }

    return results;
  };

  // Xóa một giá trị khỏi cây
  remove = (value: number) => {
    const find = () => {
      if (this.root === null) return null;

      let parent: Node | null = null;
      let current = this.root;

      while (current) {
        if (current.value === value) {
          return { parent, current };
        }

        const dirrection = value < current.value ? 'left' : 'right';

        parent = current;
        current = current[dirrection]!;
      }

      return null;
    };

    const result = find();

    if (result === null) return null;

    const { parent, current } = result;

    const hasNoChildren = current.left === null && current.right === null;
    const hasOneChild = (current.left === null) !== (current.right === null);
    const hasTwoChilds = current.left && current.right;

    const dirrection = current === parent?.left ? 'left' : 'right';

    if (hasNoChildren) {
      if (current === this.root) {
        this.root = null;
      } else {
        parent![dirrection] = null;
      }

      return;
    }

    if (hasOneChild) {
      if (current === this.root) {
        this.root = this.root.left || this.root.right;
      } else {
        parent![dirrection] = current.left || current.right;
      }

      return;
    }

    if (hasTwoChilds) {
      const node = current === this.root ? this.root : current;

      const sucessorValue = this.findMin(node.right)!;

      this.remove(sucessorValue);

      node.value = sucessorValue;
    }
  };

  // Đảo ngược cây
  invert = () => {
    if (this.root === null) return null;

    const traverse = (node: Node | null) => {
      if (node === null) return;

      [node.left, node.right] = [node.right, node.left];

      traverse(node.left);
      traverse(node.right);
    };

    traverse(this.root);
  };

  // Kiểm tra xem cây có phải BST không
  static isBinarySearchTree(tree: BinarySearchTree) {
    const validate = (node: Node | null): boolean => {
      if (node === null) return true;

      if (node.left === null) {
        return true;
      } else if (node.left.value >= node.value) {
        return false;
      }

      if (node.right === null) {
        return true;
      } else if (node.right.value <= node.value) {
        return false;
      }

      return validate(node.left) && validate(node.right);
    };

    return validate(tree.root);
  }
}

Toàn Cảnh

Cây Tìm Kiếm Nhị Phân không chỉ là một cấu trúc độc lập mà còn là nền tảng cho nhiều cấu trúc khác như Cây AVL, Cây Đỏ-Đen, và Heap.

Nó giải quyết vấn đề tìm kiếm, không chỉ trong lập trình mà còn trong thực tế, như tổ chức thư viện sách hoặc quản lý dữ liệu khách hàng.

BST là một phần trong “bức tranh lớn” về thuật toán và cấu trúc dữ liệu. Mỗi phần trong bức tranh đó tương tác với BST để xử lý dữ liệu phức tạp hơn.

Hiểu BST là hiểu một mảnh ghép quan trọng trong hệ sinh thái công nghệ.

Ứng Dụng

Một ứng dụng thực tiễn của BST là trong các hệ thống cơ sở dữ liệu. Khi bạn tìm kiếm một khách hàng cụ thể trong hàng triệu dữ liệu, BST giảm đáng kể thời gian tìm kiếm.

BST còn được sử dụng để tạo ra bộ lọc email, tổ chức thư mục máy tính, hoặc thậm chí trong trò chơi điện tử để quản lý đối tượng trong thế giới ảo.

Trong lập trình, BST giúp tối ưu hóa thuật toán sắp xếp và tìm kiếm, hai bài toán thường xuyên xuất hiện.

Nhờ BST, chúng ta có thể tổ chức và truy cập dữ liệu nhanh chóng, chính xác.

Hiểu Lầm

Một hiểu lầm phổ biến là tất cả BST đều nhanh. Điều này không đúng nếu BST không được cân bằng, dẫn đến hiệu suất tìm kiếm giảm, tương tự như danh sách liên kết.

Một số người cũng nghĩ rằng BST là giải pháp tốt nhất cho mọi bài toán tìm kiếm. Tuy nhiên, trong một số trường hợp như dữ liệu không có thứ tự, Hash Table hiệu quả hơn.

BST cũng không phải lúc nào cũng dễ triển khai đúng cách, đặc biệt khi phải cân bằng cây hoặc xử lý dữ liệu động.

Hiểu nhầm này có thể khiến bạn áp dụng BST sai cách trong những bài toán không phù hợp.

Tóm Tắt

Cây Tìm Kiếm Nhị Phân là một cấu trúc dữ liệu mạnh mẽ, dựa trên nguyên tắc sắp xếp để tối ưu hóa tìm kiếm.

Nó không chỉ đơn giản mà còn là nền tảng của nhiều thuật toán và cấu trúc nâng cao.

Việc sử dụng BST đòi hỏi sự hiểu biết về lợi ích, hạn chế và ứng dụng phù hợp.

Hiểu BST là bạn đã bước thêm một bước vào thế giới của lập trình tối ưu hóa.

Nguồn: Viễn Đinh - Cách Giải Cây Tìm Kiếm Nhị Phân