Viễn Đinh

Viễn Đinh

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

Cách Giải Đống Nhị Phân (Heap), Dể Hiểu

Bạn đã bao giờ tự hỏi làm thế nào để lưu trữ và truy cập dữ liệu sao cho tối ưu nhất khi phải sắp xếp và truy vấn nhanh? Từ nhu cầu đó, một cấu trúc dữ liệu mang tên Đống Nhị Phân ra đời. Nó không chỉ giúp bạn sắp xếp dữ liệu hiệu quả mà còn làm nền tảng cho nhiều thuật toán như sắp xếp Heap Sort hay quản lý hàng đợi ưu tiên.

Định Nghĩa

Đống Nhị Phân (Heap) là một cấu trúc dữ liệu dựa trên cây nhị phân, nơi mỗi nút (node) trong cây phải tuân theo một nguyên tắc nhất định. Trong Max Heap, giá trị của mỗi nút luôn lớn hơn hoặc bằng giá trị của các nút con. Ngược lại, trong Min Heap, giá trị của mỗi nút luôn nhỏ hơn hoặc bằng giá trị của các nút con.

Đống Nhị Phân không cần phải là cây nhị phân hoàn chỉnh, nhưng thường được biểu diễn dưới dạng mảng để tối ưu không gian lưu trữ và các thao tác truy xuất.

Mỗi phần tử trong mảng đại diện cho một nút của cây, với các quy tắc như: phần tử ở vị trí chỉ số i có nút con trái ở vị trí 2i + 1 và nút con phải ở vị trí 2i + 2. Điều này giúp chúng ta dễ dàng thao tác mà không cần các cấu trúc phức tạp hơn.

Nói cách khác, Đống Nhị Phân là một cách quản lý dữ liệu sao cho ưu tiên phần tử lớn nhất hoặc nhỏ nhất, tùy thuộc vào loại đống mà bạn chọn sử dụng.

Cách Giải

Cấu trúc dữ liệu Đống Nhị Phân hoạt động trên nguyên tắc “ưu tiên phần tử gốc”. Sau đây là các phương thức cơ bản của nó:

Bạn thấy đó, mọi thao tác trong Đống Nhị Phân đều được tối ưu để chạy nhanh nhất có thể, thường chỉ mất thời gian O(log n) nhờ cấu trúc cây nhị phân cân bằng.

Triển Khai Max Heap

Dưới đây là một cách triển khai Đống Nhị Phân (Heap) bằng TypeScript:

class MaxHeap {
  heap: (number | null)[];

  constructor() {
    this.heap = [null];
  }

  private leftChildIndex(index: number): number {
    return index * 2;
  }

  private rightChildIndex(index: number): number {
    return index * 2 + 1;
  }

  private parentIndex(index: number): number {
    return Math.floor(index / 2);
  }

  print() {
    return [...this.heap];
  }

  insert(value: number) {
    this.heap.push(value);

    const bubbleUp = (current: number) => {
      const parent = this.parentIndex(current);

      if (current > 1 && this.heap[parent]! < this.heap[current]!) {
        [this.heap[current], this.heap[parent]] = [this.heap[parent], this.heap[current]];

        bubbleUp(parent);
      }
    };

    bubbleUp(this.heap.length - 1);
  }

  remove() {
    if (this.heap.length === 1) {
      throw new Error('Heap is empty, cannot remove elements.');
    }

    if (this.heap.length === 2) {
      return this.heap.pop();
    }

    const result = this.heap[1];

    this.heap[1] = this.heap.pop()!;

    const bubbleDown = (current: number) => {
      const left = this.leftChildIndex(current);
      const right = this.rightChildIndex(current);

      let largest = current;

      if (left < this.heap.length && this.heap[left]! > this.heap[largest]!) {
        largest = left;
      }

      if (right < this.heap.length && this.heap[right]! > this.heap[largest]!) {
        largest = right;
      }

      if (largest !== current) {
        [this.heap[current], this.heap[largest]] = [this.heap[largest], this.heap[current]];

        bubbleDown(right);
      }
    };

    bubbleDown(1);

    return result;
  }
  peek(): number | null {
    return this.heap.length > 1 ? this.heap[1] : null;
  }

  size(): number {
    return this.heap.length - 1;
  }

  sort(): number[] {
    const temp = [...this.heap];

    const result: number[] = [];

    while (this.heap.length > 1) {
      result.unshift(this.remove()!);
    }

    this.heap = temp;

    return result;
  }
}

Triển Khai Min Heap

Dưới đây là một cách triển khai Min Heap bằng TypeScript:

class MinHeap {
  heap: (number | null)[];

  constructor() {
    this.heap = [null];
  }

  private leftChildIndex(index: number): number {
    return index * 2;
  }

  private rightChildIndex(index: number): number {
    return index * 2 + 1;
  }

  private parentIndex(index: number): number {
    return Math.floor(index / 2);
  }

  print() {
    return [...this.heap];
  }

  insert(value: number) {
    this.heap.push(value);

    const bubbleUp = (current: number) => {
      const parent = this.parentIndex(current);

      if (current > 1 && this.heap[parent]! > this.heap[current]!) {
        [this.heap[current], this.heap[parent]] = [this.heap[parent], this.heap[current]];

        bubbleUp(parent);
      }
    };

    bubbleUp(this.heap.length - 1);
  }

  remove() {
    if (this.heap.length === 1) {
      throw new Error('Heap is empty, cannot remove elements.');
    }

    if (this.heap.length === 2) {
      return this.heap.pop();
    }

    const result = this.heap[1];

    this.heap[1] = this.heap.pop()!;

    const bubbleDown = (current: number) => {
      const left = this.leftChildIndex(current);
      const right = this.rightChildIndex(current);

      let smallest = current;

      if (left < this.heap.length && this.heap[left]! < this.heap[smallest]!) {
        smallest = left;
      }

      if (right < this.heap.length && this.heap[right]! < this.heap[smallest]!) {
        smallest = right;
      }

      if (smallest !== current) {
        [this.heap[current], this.heap[smallest]] = [this.heap[smallest], this.heap[current]];

        bubbleDown(right);
      }
    };

    bubbleDown(1);

    return result;
  }

  peek(): number | null {
    return this.heap.length > 1 ? this.heap[1] : null;
  }

  size(): number {
    return this.heap.length - 1;
  }

  sort() {
    const temp = [...this.heap];

    const result: number[] = [];

    while (this.heap.length > 1) {
      result.push(this.remove()!);
    }

    this.heap = temp;

    return result;
  }
}

Toàn Cảnh

Đống Nhị Phân nằm ở trung tâm của các thuật toán xử lý dữ liệu ưu tiên. Từ quản lý hàng đợi trong hệ điều hành, đến lập lịch công việc, bạn sẽ thấy cấu trúc này xuất hiện.

Nó cũng là một phần không thể thiếu trong các thuật toán sắp xếp, đặc biệt là Heap Sort, nơi đống được sử dụng để liên tục chọn phần tử lớn nhất (hoặc nhỏ nhất).

Thêm vào đó, các cấu trúc dữ liệu nâng cao như Fibonacci Heap hay Binomial Heap cũng dựa trên nguyên lý tương tự để tối ưu hóa hơn nữa.

Nhìn chung, Đống Nhị Phân không chỉ là một công cụ, mà còn là nền tảng cho nhiều giải pháp thuật toán hiện đại.

Ứng Dụng

Trong thực tế, Đống Nhị Phân được dùng trong việc quản lý hàng đợi ưu tiên, nơi các tác vụ cần được xử lý theo mức độ quan trọng.

Trong lĩnh vực tài chính, đống hỗ trợ xử lý dữ liệu giao dịch lớn, sắp xếp theo thời gian thực.

Các game engine cũng sử dụng đống để quản lý các sự kiện xảy ra liên tiếp, đảm bảo hiệu suất tốt nhất khi xử lý hàng ngàn sự kiện mỗi giây.

Nói cách khác, nếu bạn làm việc với dữ liệu cần tối ưu hóa thời gian truy cập, bạn sẽ tìm thấy ứng dụng của Đống Nhị Phân ở khắp nơi.

Hiểu Lầm

Một hiểu lầm phổ biến là Đống Nhị Phân là một cấu trúc phức tạp và khó học. Thực tế, khi bạn đã hiểu nguyên lý cơ bản, việc triển khai rất đơn giản.

Nhiều người cũng nhầm lẫn giữa Đống Nhị Phân và cây nhị phân tìm kiếm (Binary Search Tree). Hai cấu trúc này có mục đích và cách tổ chức dữ liệu hoàn toàn khác nhau.

Một giới hạn của Đống Nhị Phân là khả năng tìm kiếm kém, bởi vì nó không đảm bảo thứ tự trong toàn bộ cây.

Bạn nên hiểu rõ điểm mạnh và điểm yếu này để sử dụng đúng trong từng trường hợp.

Tóm Tắt

Đống Nhị Phân là một cấu trúc dữ liệu mạnh mẽ, với khả năng quản lý dữ liệu ưu tiên và sắp xếp hiệu quả.

Thông qua Max Heap và Min Heap, bạn có thể chọn cách tổ chức dữ liệu phù hợp nhất với nhu cầu.

Việc triển khai đống đơn giản hơn bạn nghĩ, với vài phương thức cơ bản và một số quy tắc cần tuân theo.

Cuối cùng, để làm chủ Đống Nhị Phân, bạn cần luyện tập và hiểu cách nó phù hợp với bài toán thực tế của bạn.

Nguồn: Viễn Đinh - Cách Giải Đống Nhị Phân