[大、小根堆应用总结一]堆排序的应用场景

前言

在整理算法题的时候发现,大根堆(小根堆)这种数据结构在各类算法中应用比较广泛,典型的堆排序,以及利用大小根堆这种数据结构来找出一个解决问题的算法最优解。因此,我打算单独将关于堆的应用独立总结出来,后面每遇到一种跟堆结构相关的应用都放到这个目录下。

堆的定义

n个关键字序列L[1…n]称为堆,当且仅当该序列满足:

1. L(i)<=L(2i)且L(i)<=L(2i+1)或

2. L(i)>=L(2i)且L(i)>=L(2i+1)

满足第一个条件的成为小根堆(即每个结点值小于它的左右孩子结点值),满足第二个添加的成为大根堆(即每个结点值大于它的左右孩子结点值)。

应用一:对一个基本有序的有序数组排序,选择哪种排序算法?

基本有序:指如果把数组排好序的话,每个元素移动的距离不超过K,并且K相对于数组长度很小。

分析

1、对于时间复杂度为O(N)的排序算法:

时间复杂度为O(N)的算法主要有基数排序、计数排序,但是这两种算法都需要提前知道数组中元素的范围,以此来建立桶的数量,因此并不合适来解决这个问题。排除!

2、对于时间复杂度为O(N2)的排序算法:

时间复杂度O(N2)常用的主要有冒泡、选择、插入,联系到题目所说的基本有序,插入排序是首选,每个元素移动的距离不超过K,因此插入排序中每个元素向前移动的距离也不会超过K,故此时插入排序的时间复杂度为O(N*K),所以插入排序可以列入考虑。

3、对于时间复杂度为O(N*logN)的排序算法

时间复杂度O(N2)常用的主要有快速排序、归并排序、堆排序,因为这两种排序方法跟数组元素的初始顺序无关,因此这两种方法也是比较好的一种。而堆排序在最好、最坏、平均情况下时间复杂度都为O(N*logN)。

较优方案

根据上面的分析,我们初步锁定在时间复杂度为O(N*logN)的排序算法。

1. 再根据题意,每个元素移动的距离不会超过k,说明前k个元素中一定有数组中最小的一个元素,即array[0]~array[k-1]中存在一个最小的元素,从这里面拿出最小的一个元素后,再往后移动一步,即数组array[1]~array[k]中存在一个次小的元素,以此下去…就可以得到n-k个排好序的元素,最后再对最后的k个元素排序即可。

2. 因此,我们可以先建立一个k个元素的小根堆,每次拿出堆顶元素,再后移一位重新调整小根堆,循环下去…最后k个元素再因此缩小调整的范围。代码如下:

public static int[] heapSort(int[] A, int n, int k) {
        if(A == null || A.length == 0 || n < k){
            return null;
        }
        int[] heap = new int[k];
        for(int i = 0; i < k; i++){
            heap[i] = A[i];
        }
        buildMinHeap(heap,k);//先建立一个小堆
        for(int i = k; i < n; i++){
            A[i-k] = heap[0];//难处堆顶最小元素
            heap[0] = A[i];
            adjust(heap,0,k);
        }
        for(int i = n-k;i < n; i++){
            A[i] = heap[0];
            heap[0] = heap[k-1];
            adjust(heap,0,--k);//缩小调整的范围
        }
        return A;
    }
    //建立一个小根堆
    private static void buildMinHeap(int[] a, int len) {
        for(int i = (len-1) / 2; i >= 0; i--){
            adjust(a,i,len);
        }
    }
    //往下调整,使得重新复合小根堆的性质
    private static void adjust(int[] a, int k, int len) {
        int temp = a[k];
        for(int i = 2 * k + 1; i < len; i = i * 2 + 1){
            if(i < len - 1 && a[i+1] < a[i])//如果有右孩子结点,并且右孩子结点值小于左海子结点值
                i++;//取K较小的子节点的下标
            if(temp <= a[i]) break;//筛选结束,不用往下调整了
            else{//需要往下调整
                a[k] = a[i];
                k = i;//k指向需要调整的新的结点
            }
        }
        a[k] = temp;//本趟需要调整的值最终放到最后一个需要调整的结点处
    }  

时间复杂度分析

每次调整heap中k个元素重新建立堆的时间复杂度与堆的高度有关,为O(logk),又需要对n个元素进行调整,时间复杂度为O(n),因此总的时间复杂度为O(nlogk),由于k相对于数组长度n来说较小,因此这种利用小根堆来进行排序的方法相对于O(nlogn)的快速排序、归并排序来说相对较优。

应用二:判断数组中是否有重复值,要求空间复杂度为O(1)?

思路一

如果没有空间复杂度限制,可以用哈希表实现。比如使用HashMap,将数据元素保存到key值当中,每次保存前判断是否存在该key值,如果存在说明有重复值。此时,时间复杂度为O(N),空间复杂度为O(N)。代码如下:

/**
     * 利用HashMap实现
     * @param a
     * @param n
     * @return
     */
    public static boolean checker2(int[] a ,int n){
        if(a == null || a.length == 0 || a.length == 1)
            return false;
        Map<Integer,Integer> map = new HashMap<Integer,Integer>();
        for(int i = 0; i < n; i++){
            if(map.containsKey(a[i]))
                return true;
            map.put(a[i], 1);
        }
        return false;
    }

思路二

  1. 对于有空间复杂度限制,我们可以先排序,再判断的思路。因为排好序后,重复值排在了相邻位置。此时,问题就转化为了,空间复杂度限制为O(1)的情况下,考察经典排序算法,怎么实现一个最快的算法。
  2. 空间复杂度为O(1)的有冒泡、选择、插入、希尔排序以及非递归形式的堆排序,再根据时间复杂度判断,堆排序为O(NlogN),可以知道使用非递归实现的堆排序最快。

    此时的步骤就是,先通过堆排序将数组排好序,然后在遍历一遍数组,比较相邻两元素值是否相等。代码如下:

public static boolean checkDuplicate(int[] a, int n) {
        if(a == null || a.length == 0 || a.length == 1)
            return false;
        heapSort(a,n);
        for(int i = 1; i < n; i++){
            if(a[i] == a[i-1]){
                return true;
            }
        }
        return false;
    }

    private static void heapSort(int[] a,int n){
        for(int i = (n-1) / 2; i >= 0; i--){
            adjustDown(a,i,n);
        }
        int temp;
        for(int i = n-1; i > 0; i--){//只需要n-1趟
            temp = a[0];//交换堆顶元素
            a[0] = a[i];
            a[i] = temp;
            adjustDown(a,0,i);
        }
    }

    private static void adjustDown(int[] a , int k,int n){
        int temp = a[k];
        for(int i = 2 * k + 1; i < n; i = i * 2 + 1){
            if(i < n-1 && a[i] < a[i+1])//有右孩子结点,并且有孩子结点值大于左海子结点值,将i指向右孩子
                i++;
            if(temp >= a[i])
                break;
            else{//需要向下调整
                a[k] = a[i];
                k = i;//指向新的可能需要调整的结点
            }
        }
        a[k] = temp;
    }

时间复杂度分析:

堆排序的时间复杂度为O(NlogN),后面只有一次遍历过程,因此总的时间复杂度还是O(NlogN)。

以上就是堆应用的两个场景,后面碰到了,继续总结。。。
时间: 2024-09-29 20:34:27

[大、小根堆应用总结一]堆排序的应用场景的相关文章

堆排序—大根堆,小根堆

1.小根堆 若根节点存在左子女则根节点的值小于左子女的值:若根节点存在右子女则根节点的值小于右子女的值. 2.大根堆 若根节点存在左子女则根节点的值大于左子女的值:若根节点存在右子女则根节点的值大于右子女的值. 3.结论 (1)堆是一棵完全二叉树(如果公有h层,那么1~h-1层均满,在h层连续缺失若干个右叶子). (2)小根堆的根节点的值是最小值,大根堆的根节点的值是最大值. (3)堆适合于采用顺序存储. 4.堆的插入算法 将一个数据元素插入到堆中,使之依然成为一个堆. 算法描述:先将结点插入到

堆排序——大根堆(大顶堆)

1.小根堆 若根节点存在左子女则根节点的值小于左子女的值:若根节点存在右子女则根节点的值小于右子女的值. 2.大根堆 若根节点存在左子女则根节点的值大于左子女的值:若根节点存在右子女则根节点的值大于右子女的值. 3.结论 (1)堆是一棵完全二叉树(如果公有h层,那么1~h-1层均满,在h层连续缺失若干个右叶子). (2)小根堆的根节点的值是最小值,大根堆的根节点的值是最大值. (3)堆适合于采用顺序存储. 4.堆的插入算法 将一个数据元素插入到堆中,使之依然成为一个堆. 算法描述:先将结点插入到

大顶堆第二弹----堆排序(递归实现)

1 package tooffer; 2 3 import java.util.ArrayList; 4 import java.util.Arrays; 5 6 public class BigHeap { 7 8 9 10 /* 11 *交换堆中的两个元素 12 */ 13 private void swap(ArrayList<Integer> heapList,int srcIndex,int dstIndex) 14 { 15 int tmp = heapList.get(srcIn

堆排序(小根堆)

#include <cstdio> #include <iostream> #include <cstring> using namespace std ; int h[100000] ; int n ; void siftdown(int i) //i为要调整的根节点 { int flag = 1,t ; //flag用来标记是否还需要继续调整 while(2*i <= n && flag) //是否至少有左子树 { if(h[2*i]<h

java 实现大顶堆

堆排序是一种树形选择排序方法,它的特点是:在排序的过程中,将array[0,...,n-1]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(最小)的元素. 1. 若array[0,...,n-1]表示一颗完全二叉树的顺序存储模式,则双亲节点指针和孩子结点指针之间的内在关系如下: 任意一节点指针 i:父节点:i==0 ? null : (i-1)/2  左孩子:2*i + 1  右孩子:2*i + 2 2. 堆的定义:n个关键字序

堆排序:什么是堆?什么是最大堆?二叉堆是什么?堆排序算法是怎么样的?PHP如何实现堆排序?

本文标签:  堆排序 php php算法 堆排序算法 二叉堆 数据结构 REST   服务器 什么是堆 这里的堆(二叉堆),指得不是堆栈的那个堆,而是一种数据结构. 堆可以视为一棵完全的二叉树,完全二叉树的一个"优秀"的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示,每一个结点对应数组中的一个元素. 数组与堆之间的关系 二叉堆一般分为两种:最大堆和最小堆. 什么是最大堆 堆中每个父节点的元素值都大于等于其孩子结点(如果存在),这样的堆就是一个最大堆 因此,最大堆中的

让priority_queue支持小根堆的几种方法

点击这里了解什么是priority_queue 前言 priority_queue默认是大根堆,也就是大的元素会放在前面 例如 #include<iostream> #include<cstdio> #include<queue> using namespace std; priority_queue<int>q; int a[15]={0,1,4,2,3,5}; const int n=5; int main() { for(int i=1;i<=n

剑指offer:数据流中的中位数(小顶堆+大顶堆)

1. 题目描述 /** 如何得到一个数据流中的中位数? 如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值. 如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值. 我们使用 Insert()方法读取数据流,使用 GetMedian()方法获取当前读取数据的中位数. */ 2. 思路 /** 最大堆和最小堆 * 每次插入小顶堆的是当前大顶堆中最大的数 * 每次插入大顶堆的是当前小顶堆中最小的数 * 这样保证小顶堆中的数永远大于等于大顶堆中的数(值

wikioi 1052 大顶堆

题目描述 Description 王钢是一名学习成绩优异的学生,在平时的学习中,他总能利用一切时间认真高效地学习,他不但学习刻苦,而且善于经常总结.完善自己的学习方法,所以他总能在每次考试中得到优异的分数,这一切很大程度上是由于他是一个追求效率的人. 但王钢也是一个喜欢玩的人,平时在学校学习他努力克制自己玩,可在星期天他却会抽一定的时间让自己玩一下,他的爸爸妈妈也比较信任他的学习能力和学习习惯,所以在星期天也不会象其他家长一样对他抓紧,而是允许他在星期天上午可以自由支配时间. 地鼠游戏是一项需要