堆、堆排序以及TopK问题


堆的定义

堆是一种特殊的数据结构,可以形象化的看成一颗完全二叉树,一般内部的存储结构为数组;堆中的某个节点总是不大于或者(不小于)其左右节点,其中前者为成为小顶堆(最小堆,堆顶为最小值),后者成为大顶堆(最大堆,堆顶为最大值)

堆的一些操作

  • build: 建立一个堆
  • insert: 往堆中插入一个新节点
  • update: 更新某个节点的值, 根据节点的新值重新调整堆,使其符合堆的特性
  • get_top : 获取堆顶的节点
  • delete_top : 删除堆顶的节点, 然后重新调整堆,使其满足堆的特性

下面以最大堆为例进行堆操作的说明

  • 建堆
    • 自上到下构建堆

      • 自上到下构建堆的流程是:假定数组arr的前i个节点已经满足堆的特性,然后新增一个节点arr[i]到数组尾,调整堆使得数组arr[0~i]满足堆的特性;当i = n -1(n是数组长度)时,则建堆完毕;
      • 新增节点arr[i]到数组尾,调整的过程:可以形象的描述为,该节点沿着到根节点的二叉树路径,进行上浮;如果该节点比其父节点大,则将该节点和父节点进行交换,然后将当前节点设置为其父节点继续进行上浮比较;如果该节点比其父节点小,则停止上浮;直到该节点上浮到根节点,则本次上浮调整结束
      • 代码示例
        struct max_heap_t {
        int32_t* arr;
        int32_t  n;
        
        max_heap_t (int32_t* input_arr, int32_t arr_size){
            arr = new int32_t[arr_size];
            memcpy(arr, input_arr, sizeof(int32_t) * arr_size);
            n = arr_size;
        }
        
        ~max_heap_t () {
            if (arr) {
                delete[] arr;
                arr = nullptr;
            }
        }
        
        /** time complexity => O(nlogn) */
        void    build_heap_from_top_to_bottom() {
          
            for (int32_t i = 1; i < n; i++) {
               heap_ajust_from_bottom_to_top(i);
            }
        }
        
        /* O(logn) */
        void    heap_ajust_from_bottom_to_top(int32_t bottom_index) {
            int32_t tmp = arr[bottom_index];
            while (bottom_index > 0) {
                int32_t parent_index = (bottom_index - 1) / 2;
                if (arr[parent_index] < tmp ) {
                    arr[bottom_index] = arr[parent_index];
                    bottom_index = parent_index;
                }
                else {
                    break;
                }
            }
            arr[bottom_index] = tmp;
        }
        
      • 时间复杂度
        每次上浮调整的时间复杂度为O(logn), 总共要调整n-1次,所以建堆的时间复杂度为 O(nlogn)
    • 自下到上构建堆

      • 自下到上构建堆的流程是: 从最后一个节点的父节点开始一直到到根节点,逐步进行以选中节点为起点,进行下沉调整;到根节点下沉调整结束,则建堆完毕
      • 下沉调整的过程,可以描述为:从当前节点以及其左右子节点中选出最大的一个节点,如果最大节点是该节点本身,则下沉调整结束;如果是其左子节点(或者右子节点),则将该节点与其左子节点(或者其右子节点)交换,然后将当前节点设置为其左子节点(后后者右子节点)继续下浮比较;
      • 代码示例
         /* O(n) */
        void    build_heap_from_bottom_to_top() {
            int32_t max_index = n - 1;
            for (int32_t i = (max_index - 1) / 2; i >= 0; i--) {
                heap_adjust_from_top_to_bottom(i, max_index);
            }
        }
        
        /* O(logn) */
        void    heap_adjust_from_top_to_bottom(int32_t top_index, int32_t bottom_index) {
            int32_t tmp = arr[top_index];
            while (top_index <= (bottom_index - 1) / 2) {
                int32_t max_one = tmp;
                int32_t child_idx = 0;
                int32_t left_child_idx = top_index * 2 + 1;
                int32_t right_child_idx = top_index * 2 + 2;
                
                if (left_child_idx <= bottom_index && max_one < arr[left_child_idx] ) {
                    max_one = arr[left_child_idx];
                    child_idx = left_child_idx;
                }
                if (right_child_idx <= bottom_index && max_one < arr[right_child_idx] ) {
                    max_one = arr[right_child_idx];
                    child_idx = right_child_idx;
                }
              
                if (max_one != tmp) {
                    arr[top_index] = max_one;
                    top_index = child_idx;
                }
                else {
                    break;
                }
            }
            arr[top_index] = tmp;
        }
        
        
      • 时间复杂度
        自下到上建堆的时间复杂度是O(n)
        证明方法如下:

        假定节点个数为n, 叶子高度为h = log2(n)
        1. 假设根节点的高度为0,叶子节点高度为h,那么每层包含的元素个数为2^x,x从0到h。
        2. 构建堆的过程是自下而上,对于每层非叶子节点需要调整的次数为h-x,因此很明显根节点需要调整(h- 0)次,第一层节点需要调整(h-1)次,最下层非叶子节点需要调整1次。
        3. 因此可知,构造树高为h的二叉堆精确时间复杂度为:
        s = 12^(h-1) + 22(h-2)+……+h*20
        可以看出以上公式是等差数列和等比数列乘积之和,可以通过错位相减求:
        2s = 2^h + 22^(h-1)+32(h-2)+……+h*21
        因此可得:
        s = 2s -s = 2^h + 2^(h-1) + 2^(h-2) +…… + 2^1 - h
        最终可以通过等比数列公式进行计算得到s = 2*2^h - 2 -h
        将代入的s = 2n - 2 - log2(n),近似的时间复杂度就是O(n)
        转载自: https://blog.csdn.net/hupenghui1224/article/details/57427045

堆排序

堆排序的话,无需额外的空间,直接可以在原堆内数组上进行堆排序;

  • 堆排序的过程: 对于给定的数组arr以及长度n,首先根据该数组,构建堆;然后依次将当前数组尾元素arr[i]( i => n -> 0)与堆顶元素交换,然后重新调整堆(从 arr[0] ~ arr[i - 1]);一直到当前i = 0, 则排序完成;
  • 代码实例:
      void    sort() {
    
          // build  heap first
          build_heap_from_bottom_to_top();
    
          // sort
          int32_t tmp = 0;
          for (int32_t i = n - 1; i > 0;) {
              // move heap top to end
              tmp = arr[0];
              arr[0] = arr[i];
              arr[i] = tmp;
    
              // adjust the heap
              heap_adjust_from_top_to_bottom(0, --i);
          }
      }
    

TopK问题

TopK问题描述:在N个无序元素中,找到最大的K个(或最小的K)

按照一般排序算法来寻找的话,时间复杂度为O(nlogn) + O(k); 当N很大,而K很小的时候,这种方法很慢;
可以采用堆来解决这个问题;下面以找到最大的K个值为例

  • 算法流程:取该数组的前K个元素,构建一个最小堆;然后从该无序数组的第k个元素开始,一直到数组尾,遍历该数组,将当前元素与最小堆的堆顶元素比较,如果当前元素大于堆顶元素(最大的K个值里面的最小的那个),那么可以认为当前元素可能是要寻找的最大的K个元素之一,则将当前元素替换堆顶,然后重新进行堆调整;当遍历结束的时候,最小堆中的K个元素就是要寻找的最大的K个元素

  • 代码示例

    void top_k(int32_t* input_arr, int32_t n, int32_t k) {
        printf("input array: (%d)\n", n);
        for (int32_t i = 0; i < n; ++i) {
            printf(", %d", input_arr[i]);
        }
        printf("\n");
        
        // O(k)
        // we suppose the k element of the min heap if the default top k element
        min_heap_t min_heap(input_arr, k);
        min_heap.build_heap_from_bottom_to_top();
        
        for (int32_t i = k; i < n; ++i) {
            // compare each element with the min element of the min heap
            // if the element > the min element of the min heap
            // we think may be the element is one of what we wanna to find in the top k
            if (input_arr[i] > min_heap.arr[0]){
                // swap
                min_heap.arr[0] = input_arr[i];
                
                // heap adjust
                min_heap.heap_adjust_from_top_to_bottom(0, k - 1);
            }
        }
        
        printf("top k: (%d)\n", k);
        min_heap.print_arr();
    }
    
  • 算法复杂度
    创建初始堆的过程O(KlogK), 每次堆调整的时间复杂度O(logK), 寻找最大K个值的过程(N-K) * O(logK) = O((N-K)logK), 总得时间复杂度O(NlogK)

推荐阅读更多精彩内容