面试笔记----优先级队列

286 阅读5分钟

1 介绍

1.1 理解

prioritu_queue优先级队列。优先级队列可以保证每次取出来的元素都是队列中的最小最大的元素<Java优先级队列默认每次取出来的为最小元素>。

优先级队列:是零个或多个元素的集合,优先级队列中每一个元素都有一个优先级,元素的先后的出队顺序是由优先级的高低决定的。优先级高的先出队,优先级低的后出队。

优先级队列的主要特点:从一个集合中能够快速的查找到和删除最大值和最小值的元素。

image

PS:因为数组索引是数字,为了方便区分,将字符作为数组元素。

你看到了,把 arr[1] 作为整棵树的根的话,每个节点的父节点和左右孩子的索引都可以通过简单的运算得到,这就是二叉堆设计的一个巧妙之处。为了方便讲解,下面都会画的图都是二叉树结构,相信你能把树和数组对应起来。

二叉堆还分为最大堆和最小堆。=最大堆的性质是:每个节点都大于等于它的两个子节点。类似的,最小堆的性质是:每个节点都小于等于它的子节点。

两种堆核心思路都是一样的,本文以最大堆为例讲解。

对于一个最大堆,根据其性质,显然堆顶,也就是 arr[1] 一定是所有元素中最大的元素。

1.2 优先级队列结构

我们了解堆排是将一个数组构建成一个二叉堆,每一个父节点都比子节点大或者小,左节点比右节点大或者小,这时由堆排的在构建堆时采用向下调整策略,在完成构建堆后就成一个小堆或者大堆,这也符合优先级队列的特点,每个父节点都比子节点大或者小,但是元素排序还不是有序的,大体上已经显得有序。堆顶是所有元素中最小活着最大的元素,在经过从最后一个元素到第一个元素的向上调整就会有序,这就是堆排序。所以优先级队列结构上是一个小堆或者大堆,不是有序但是接近有序

在优先队列添加一个元素都要按照堆的处理方式处理,在队尾插入一个元素时,就会破坏堆的结构,这时就要向上调整堆的结构。删除堆顶元素时将最后一个和堆顶元素交换删除队尾元素,向下调整堆结构

2常用函数介绍

2.1 push函数介绍

函数原型:在队列中插入一个元素val

void push (const value_type& val);

函数实现:在队尾插入一个元素时,就会破坏堆的结构,这时就要向上调整堆的结构

void push(const T& x) {
      con.push_back(x);
      //大堆向上调整
      Up(con.size()-1);

    }

2.2 pop函数介绍

函数原型:删除堆顶元素

void pop();

函数模拟实现:删除堆顶元素时将最后一个和堆顶元素交换删除队尾元素,向下调整堆结构

void pop() {
      swap(con[0], con[con.size() - 1]);
      con.pop_back();
      Down(0);

    }

3 堆的结构调整

为什么要有向下 和向上 的操作呢?为了维护堆结构。

我们要讲的是最大堆,每个节点都比它的两个子节点大,但是在插入元素和删除元素时,难免破坏堆的性质,这就需要通过这两个操作来恢复堆的性质了。

对于最大堆,会破坏堆性质的有有两种情况:

  1. 如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行下沉
  2. 如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的上浮

当然,错位的节点 A 可能要上浮(或下沉)很多次,才能到达正确的位置,恢复堆的性质。所以代码中肯定有一个while循环。

image

image

代码实现:

//答对向上调整
    void Up(int child) {
      size_t parent = (child - 1) / 2;
      while (child > 0) {
        if (less(con[parent] , con[child])) {
          swap(con[child], con[parent]);
          child = parent;
          parent = (child - 1) / 2;
        }
        else {
          break;
        }
      }
    }
//向下调整
    void Down(int parent) {

      size_t child = parent * 2+1;
      while (child<con.size()) {
        //直到子结点中较小的节点和父节点比较看是不是符合堆的结构
        if (child + 1 < con.size() && con[child] < con[child+1]) {
          ++child;
        }

         if (less(con[parent], con[child])) {
           swap(con[child], con[parent]);
            parent = child;
          child = parent * 2+1;
        }
        else {
        
          break;
        }
      
      }
    }
  };

4 总结

二叉堆就是一种完全二叉树,所以适合存储在数组中,而且二叉堆拥有一些特殊性质。

二叉堆的操作很简单,主要就是上浮和下沉,来维护堆的性质(堆有序),核心代码也就十行。

优先级队列是基于二叉堆实现的,主要操作是插入和删除。插入是先插到最后,然后上浮到正确位置;删除是把第一个元素 pq[1](最值)调换到最后再删除,然后把新的 pq[1] 下沉到正确位置。核心代码也就十行。

5 常见考点 :topK问题

求一组元素的第K大或者第k小元素?首先想到的是排序但是这只是求一个元素排序的话代价有些大,利用优先级队列将前k个元素存入到队列中,然后每次拿堆顶元素和新来的比较看是否要交换元素,等到遍历完所有元素后堆顶元素就是这组元素的最大或者最小第k大元素。