数据结构|堆

完成阅读您将会了解堆的:

  1. 概念
  2. 构建方法
  3. 基本操作
  4. C++实现
  5. Rust实现

1. 概念

Heap)的概念最早可见于 J. W. J. Williams 在1964年发表的堆排序[1]一文当中。本文介绍的堆指的是一种数据结构,注意与内存模型中垃圾回收机制Garbage Collection Mechanism)区分。

不加限定时,堆作数据结构时通常指二叉堆Binary Heap)。二叉堆本质是一种特殊的完全二叉树Binary Tree),它满足以下基本性质: 若子节点恒小于等于母节点,则称其为最大堆Max Heap),如图1[2];若子节点恒大于等于母节点,则称其为最小堆Min Heap),如图2[2:1]。本文所介绍的堆即为二叉堆,具体编程示例为最大堆。

图1:最大堆

图1:最大堆

图2:最小堆

图2:最小堆

2. 构建方法

本文采用数组Array)作为构建堆的基础容器,为了更方便地阐述,我们在此约定堆的下标依旧从0开始。若给定下标\([i]\)节点存在子节点,那么其左右子节点的下标分别为\([2i+1]\)\([2i+2]\),除根节点外,其母节点下标为\([\lfloor\frac{i-1}{2}\rfloor]\)

构建堆常用的方法是逐个插入元素,并在每一次插入时维护堆的性质不变,直至完成所有元素插入,如图3[2:2]。在这个过程中,维护堆性质的方法叫做堆化Heapify),时间复杂度为\(O(\lg n)\),其具体过程见伪代码1。

图3:堆构建示意

图3:堆构建示意

伪代码1:堆化
变量说明:H\(\rightarrow\)堆数组;i\(\rightarrow\)堆化始发下标;left\(\rightarrow\)左子节点下标;right\(\rightarrow\) 右子节点下标

\[\begin{aligned} &HEAPIFY(H,i)\\ &~~~~~~left \leftarrow 2i + 1 \\ &~~~~~~right \leftarrow left + 1\\ &~~~~~~while ~~~left < H.size \\ &~~~~~~~~~~~~if ~~~H[i] < H[left]\\ &~~~~~~~~~~~~~~~~~~largest \leftarrow left\\ &~~~~~~~~~~~~if ~~~right<H.size~~~ and~~~ H[largest] < H[right]\\ &~~~~~~~~~~~~~~~~~~largest \leftarrow right\\ &~~~~~~~~~~~~if ~~~largest \ne i\\ &~~~~~~~~~~~~~~~~~~swap(H[i], H[largest])\\ &~~~~~~~~~~~~~~~~~~i \leftarrow largest\\ &~~~~~~~~~~~~left \leftarrow 2i + 1\\ &~~~~~~~~~~~~right \leftarrow left + 1 \end{aligned} \]


注意堆化是堆的基础上进行的,且堆化是向下进行的过程,只会影响堆化始发下标之后的元素。那么也就是说在每一次更新下标为\([i]\)的元素前,若\(A[2i+1]\)\(A[2i+2]\)存在,它们首先应该是合法堆的堆顶。当然空集合与单元素集合都被视作合法的堆。如果我们从最末母节点\([\lfloor \frac{A.size-1}{2}\rfloor]\)开始自底向上bottom-up)堆化,合法堆\(H\)规模将从最末母节点所领堆扩充到整个数组\(A\),从而实现原地的元素插入(位置更新),其具体过程见伪代码2。

显然,由于进行了\(O(n)\)次堆化调用,那么堆构建的时间复杂度为\(O(n\lg n)\)。这么判断堆构建的时间正确但不够紧确。实际上,我们可以通过更仔细的运算证明堆构建的时间复杂度为\(O(n)\)


伪代码2:堆构建
变量说明:A \(\rightarrow\) 无序数组

\[\begin{aligned} &BUILD\_HEAP(A)\\ &~~~~~~for~~~i \leftarrow \lfloor \frac{A.size - 1}{2} \rfloor~~~ downto ~~~0 \\ &~~~~~~~~~~~~HEAPIFY(A,i) \end{aligned} \]


3. 基本操作

操作 描述 时间复杂度 空间复杂度
TOP() 获取堆顶元素 \(\Theta(1)\) \(\Theta(1)\)
PUSH(value) 添加新元素 \(O(\lg n)\) \(\Theta(1)\)
POP() 弹出堆顶元素 \(O(\lg n)\) \(\Theta(1)\)
SIZE() 询问当前堆大小 \(\Theta(1)\) \(\Theta(1)\)

其中POP操作过程如图4[2:3]与伪代码3,PUSH(value)操作过程如伪代码4。


伪代码3:堆顶弹出
变量说明:H \(\rightarrow\) 堆数组;H.size\(\rightarrow\) 堆大小

\[\begin{aligned} &POP()\\ &~~~~~~if~~~H.size>0\\ &~~~~~~~~~~~~H.size \leftarrow H.size-1 \\ &~~~~~~~~~~~~swap(H[0],H[H.size])\\ &~~~~~~~~~~~~HEAPIFY(H,0)\\ &~~~~~~~~~~~~ \end{aligned} \]


图4:堆顶弹出示意

图4:堆顶弹出示意

伪代码4:堆压入
变量说明:H \(\rightarrow\) 堆数组;H.size\(\rightarrow\) 堆大小;H.capacity \(\rightarrow\) 堆容量;value \(\rightarrow\) 添加值

\[\begin{aligned} &PUSH(value)\\ &~~~~~~if~~~H.size<H.capacity\\ &~~~~~~~~~~~~H[H.size] \leftarrow value\\ &~~~~~~~~~~~~i \leftarrow H.size\\ &~~~~~~~~~~~~H.size \leftarrow H.size +1\\ &~~~~~~~~~~~~while ~~~i>0\\ &~~~~~~~~~~~~~~~~~~parent \leftarrow \frac{i-1}{2}\\ &~~~~~~~~~~~~~~~~~~if ~~~H[parent]>=H[i]\\ &~~~~~~~~~~~~~~~~~~~~~~~~break\\ &~~~~~~~~~~~~~~~~~~swap(H[parent],H[i])\\ &~~~~~~~~~~~~~~~~~~i \leftarrow parent\\ \end{aligned} \]


4. C++实现

template <class T, size_t N> class Heap {
  T H[N]; // N means capacity
  size_t len = N;
  size_t left, right;

  void _heapfy(auto i) {
    while (right = i + 1 << 1, left = right - 1, left < len) {
      auto largest = H[i] < H[left] ? left : i;
      largest = right < len && H[largest] < H[right] ? right : largest;
      if (largest == i)
        break;
      std::swap(H[i], H[largest]);
      i = largest;
    }
  }

public:
  Heap(T *A) {
    std::memcpy(H, A, N * sizeof(T));
    auto i = N >> 1;
    do {
      _heapfy(--i);
    } while (i > 0);
  }
  constexpr auto top() const noexcept { return *H; }
  inline void pop() noexcept {
    if (size()) {
      std::swap(*H, H[--len]);
      _heapfy(0);
    }
  }
  inline void push(auto val) noexcept {
    if (len < N) {
      H[len] = val;
      auto i = len;
      ++len;
      while (i) {
        auto parent = i - 1 >> 1;
        if (H[parent] >= H[i])
          break;
        std::swap(H[parent], H[i]);
        i = parent;
      }
    }
  }
  constexpr auto size() const noexcept { return len; }
};

此外,C++标准库提供了priority_queue直接使用。

5. Rust实现

impl<T: Clone + std::cmp::PartialOrd> Heap<T> {
    fn _heapfy(&mut self, mut i: usize) {
        let mut right = i + 1 << 1;
        let mut left = right - 1;
        while left < self.size {
            let mut largest = if self.H[i] < self.H[left] { left } else { i };
            largest = if right < self.size && self.H[largest] < self.H[right] { right } else { largest };
            if largest == i { break; }
            self.H.swap(i, largest);
            i = largest;
            right = i + 1 << 1;
            left = right - 1;
        }
    }
    pub fn new() -> Self { Self { size: 0, capacity: 0, H: vec![] } }
    pub fn from(mut self, A: Vec<T>) -> Self {
        self.size = A.len();
        self.capacity = self.size;
        self.H = A;
        let mut i = self.size >> 1;
        while i > 0 {
            i -= 1;
            self._heapfy(i);
        }
        self
    }
    pub fn top(&self) -> Option<T> { if self.size > 0 { Some(self.H[0].clone()) } else { None } }
    pub fn pop(&mut self) {
        if self.size > 0 {
            self.size -= 1;
            self.H.swap(0, self.size);
            self._heapfy(0);
        }
    }
    pub fn push(&mut self, val: T) {
        if self.size < self.capacity {
            self.H[self.size] = val;
            let mut i = self.size;
            self.size += 1;
            while i > 0 {
                let parent = i - 1 >> 1;
                if self.H[parent] >= self.H[i] { break; }
                self.H.swap(i, parent);
                i = parent;
            }
        }
    }
    pub fn len(&self) -> usize { self.size }
}

pub struct Heap<T> {
    H: Vec<T>,
    size: usize,
    capacity: usize,
}

此外,Rust标准库提供了BinaryHeap直接使用。

6. 自我测试

伪代码实践

  1. 寻找数据流S的中位数C。

LeetCode选荐

  1. Kth Largest Element in a Stream
  2. Network Delay Time
  3. Minimize Deviation in Array
  4. Furthest Building You Can Reach
  5. Minimum Number of Refueling Stops

测试参考解答

让每一天足够精致,期待与您的再次相遇! ^_^


  1. Williams JW. Algorithm 232: heapsort. Commun. ACM. 1964;7:347-8. ↩︎

  2. 图片引自tutorialspoint,在此鸣谢。 ↩︎ ↩︎ ↩︎ ↩︎

posted @ 2021-07-06 19:35  我的名字被占用  阅读(109)  评论(0)    收藏  举报