数据结构-----优先级队列的概念、模拟实现、特性、常用方法、扩容机制、初始化、堆的应用、Top-K问题

1. 优先级队列

1.1 概念

前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。

在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)

2. 优先级队列的模拟实现

JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整

2.1 堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

堆的物理结构本质上是顺序存储的,是线性的。但在逻辑上不是线性的,是完全二叉树的这种逻辑储存结构。 堆的这个数据结构,里面的成员包括一维数组,数组的容量,数组元素的个数,有两个直接后继。

在这里插入图片描述
在这里插入图片描述

2.2 堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储
在这里插入图片描述

注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。

将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:

  • 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
  • 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
  • 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

2.3 堆的创建

2.3.1 堆向下调整

对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成小根堆呢?
在这里插入图片描述
仔细观察上图后发现:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。

向下过程(以小堆为例):
1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
2. 如果parent的左孩子存在,即:child < size(节点个数), 进行以下操作,直到parent的左孩子不存在
    2.1 parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
    2.2 将parent与较小的孩子child比较,如果:
          - parent小于较小的孩子child,调整结束;
          - 否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent= child;child = parent*2+1; 然后继续2,直到当前子树满足最大堆的性质或没有左孩子存在为止。

这个过程实际上是将不满足最小堆性质的元素向下移动,以恢复堆的性质。通过不断比较 parent和其孩子的值,并交换它们的位置,确保较小的元素在父节点位置,从而满足最小堆的性质。

在这里插入图片描述

注意:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。

时间复杂度分析: 最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(h)= O( log ⁡ 2 n + 1 \log _2n +1 log2n+1) = O( log ⁡ n \log n logn)。

2.3.2 堆的创建

那对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右子树不满足堆的特性,又该如何调整呢?
在这里插入图片描述
问题:

  1. 每一棵子树调整的时候,结束的位置怎么确定?
    当走到最后一个节点之后(child >= len)时,结束
  2. 最后一棵子树的根怎么确定?
    假设最后一个节点的下标为i,那么最后一棵子树的根为(i-1)/2 ---->其中,i为二叉树结点个数-1;
public static void createHeap() {
    // 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
    int parent = ((usedSize-1-1)>>1);
    for (; parent >= 0; parent --) {
        shiftDown(parent,usedSize);
   }
}

public void shiftDown(int parent,int usedSize) {
    // child先标记parent的左孩子,因为parent可能右左没有右
    int child = 2 * parent + 1;
    int len = usedSize;
    
    while (child < len ) {
        // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
        if(child + 1 < len && array[child+1] < array[child]){
             child += 1;
       }
        // 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
        if (elem[parent] <= elem[child]) {
            break;
       }else{
            // 将双亲与较小的孩子交换
	       int t = elem[parent];
	       elem[parent] = elem[child];
	       elem[child] = t;
            
            // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
            parent = child;
            child = parent * 2 + 1;
       }
   }
}

首先,createHeap()方法找到倒数第一个非叶子节点,然后从该节点开始一直向根节点遍历,对每个节点进行向下调整操作。shiftDown()方法用于向下调整堆的元素,根据父节点和子节点的大小关系进行交换,直到满足堆的性质为止。如果父节点比子节点小,则交换两者的位置,并继续向下调整子树。整个过程保证了堆的特性,即父节点小于等于子节点。

2.3.3 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):
在这里插入图片描述
因此:建堆的时间复杂度为O(N)

2.4 堆的插入与删除

2.4.1 堆的插入

堆的插入总共需要两个步骤:

  1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
  2. 将最后新插入的节点向上调整,直到满足堆的性质
    在这里插入图片描述

向上调整的具体步骤如下:
将新插入的节点与其父节点进行比较。如果堆是最大堆,比较父节点与新节点的值,如果新节点的值比父节点大,则交换它们的位置。如果堆是最小堆,比较父节点与新节点的值,如果新节点的值比父节点小,则交换它们的位置。通过交换,新插入的节点被向上移动一层。重复进行上述比较和交换的步骤,直到新插入的节点已经成为堆的根节点(没有父节点),或者它的值满足堆的性质,不再需要交换。这样,通过向上调整,新插入的节点可以沿着堆的路径逐级向上移动,直到它找到适当的位置,以使整个堆恢复满足堆的性质。

    public void offer(int data) {
        //先判断是否要进行扩容
        if (isFull()) {
            elem = Arrays.copyOf(elem, elem.length * 2);
        }
        //将数据插入最后一个位置
        elem[usedSize] = data;
        usedSize++;

        //对二叉树进行向上调整
        shiftUp(usedSize - 1);
    }

    private void shiftUp(int child) {
    	if (child== 0) {
        	return; // 已经是堆的根节点,不需要再调整
  		}
        // 找到child的双亲
        int parent = (child - 1) / 2;
        //当child为0时,parent越界,此时调整结束
        while (child > 0) {
            // 如果双亲比孩子大,parent满足堆的性质,调整结束
            if (elem[parent] >= elem[child]) {
                break;
            } else {
                // 将双亲与孩子节点进行交换
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                //小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增,parent和child往上移动
                child = parent;
                parent = (child - 1) / 2;
            }
        }
    }

堆的插入操作的时间复杂度为:最坏情况下,会从叶子一路比较到根,比较的次数为完全二叉树的高度,即时间复杂度为O(h)= O( log ⁡ 2 n + 1 \log _2n +1 log2n+1) = O( log ⁡ n \log n logn)。

那我们可以用向上调整的方式创建堆吗?
当然可以,但是其时间复杂度会大于向下调整,为O ( n * log ⁡ 2 n \log_2n log2n)。因为向下调整,只需要自上而下调整到合适位置,倒数第二层,最后一层不需要进行调整,而向上调整,从底层开始,一直到第二层,每一层都要调整,并且底层的结点个数大约时总节点个数的一半。
在这里插入图片描述

2.4.2 堆的删除

注意:堆的删除一定删除的是堆顶元素。具体如下:

  1. 将堆顶元素与堆中最后一个元素交换:首先,将堆顶元素与堆中最后一个元素进行交换。这样可以将要删除的元素移到了堆的末尾。
  2. 将堆中有效数据个数减少一个(usedSize–),即将堆的大小减一。
  3. 对堆顶元素进行向下调整(shiftDown(0,usedSize)):对新的堆顶元素进行向下调整操作,使其满足堆的性质。通过比较堆顶元素与其子节点的值,并将其与较小(或较大,具体取决于是最小堆还是最大堆)的子节点进行交换,以保持堆的性质。

在这里插入图片描述

    public int poll() {
	    if (usedSize == 0) {//此时,数组中没有数据
	        throw new IllegalStateException("Heap is empty");
	    }
        //将堆顶元素和最后一个元素进行交换
        int tmp = elem[usedSize - 1];
        elem[usedSize - 1] = elem[0];
        elem[0] = tmp;
        //删除最后一个元素
        int del = elem[usedSize - 1];
        usedSize--;
        //将二叉树从0向下调整
        shiftDown(0, usedSize);
        return del;
    }

堆的删除操作的时间复杂度为 O( log ⁡ n \log n logn),其中 n 是堆中元素的个数。这是因为向下调整的过程中,新的堆顶元素最多需要与堆的高度成比例的次数进行比较和交换,而堆的高度为 O(h)= O( log ⁡ 2 n + 1 \log _2n +1 log2n+1) = O( log ⁡ n \log n logn)。

2.5 用堆模拟实现优先级队列(其他方法)

public class MyPriorityQueue {
    int[] elem;
    int usedSize;

    public MyPriorityQueue() {
        elem = new int[10];
    }

    public MyPriorityQueue(int capacity) {
        elem = new int[capacity];
    }

    public void initQueue(int[] array) {
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }
    }
    
    public int peek() {
       return elem[0];
   }
}

【习题练习】:

1.下列关键字序列为堆的是:()
A: 100,60,70,50,32,65 B: 60,70,65,50,32,100 C: 65,100,70,32,50,60
D: 70,65,100,32,50,60 E: 32,50,100,70,65,60 F: 50,100,70,65,60,32
答案:A

2.已知小根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的比较次数是()
A: 1 B: 2 C: 3 D: 4
答案:C
解析:在这里插入图片描述
删除8之后重建过程:
在这里插入图片描述

3.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A: [3,2,5,7,4,6,8] B: [2,3,5,7,4,6,8]
C: [2,3,4,5,7,8,6] D: [2,3,4,5,6,7,8]
答案:C
解析:将最后一个元素和第一个进行交换,再将最后一个与那苏删除,再对堆;进行重建,结果为C

3.常用接口介绍

3.1 PriorityQueue的特性

Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。

在这里插入图片描述

关于PriorityQueue的使用要注意:

  1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常.
class Student {
    public int id;
    public String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
public class Test2 {
    public static void main(String[] args) {
        PriorityQueue<Student> queue = new PriorityQueue<>();
        Student student1 = new Student(12,"张三");
        Student student2 = new Student(2,"李四");
        queue.offer(student1);
        queue.offer(student2);
    }
}

在这里插入图片描述
Student类需要实现Comparator接口,重写compare方法,才可以进行比较,插入堆中。

  1. 不能插入null对象,否则会抛出NullPointerException.
 		PriorityQueue<Integer> queue = new PriorityQueue<>();
        queue.offer(null);

在这里插入图片描述
在 offer ( ) 方法的源代码中也会提到:

if (e == null)
    throw new NullPointerException();
  1. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  2. 插入和删除元素的时间复杂度为O( log ⁡ 2 n \log _2n log2n)
  3. PriorityQueue底层使用了堆数据结构
  4. PriorityQueue默认情况下是小堆- - - - -> 即每次获取到的元素都是最小的元素

3.2 PriorityQueue常用接口介绍

3.2.1. 优先级队列的构造(默认小根堆)

在这里插入图片描述
此处只是列出了PriorityQueue中常见的几种构造方式,其他的可以参考帮助文档。
在这里插入图片描述

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常
PriorityQueue(Collection<? extends E> c)用一个集合来创建优先级队列
static void TestPriorityQueue(){
        // 1. 创建一个空的优先级队列,底层默认容量是11
        PriorityQueue<Integer> q1 = new PriorityQueue<>();
 
        // 2. 创建一个空的优先级队列,底层的容量为initialCapacity
        PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
 
		// 3. 用ArrayList对象来构造一个优先级队列的对象
        ArrayList<Integer> list = new ArrayList<>();
        list.add(4);
        list.add(3);
        list.add(2);
        list.add(1);
        
        // q3中已经包含了三个元素
        PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
        System.out.println(q3.size());
        System.out.println(q3.peek());
   }

注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器

3.2.2. 插入/删除/获取优先级最高的元素(优先队列的常见方法)

函数名功能介绍
boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 ,注意:空间不够时候会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()获取有效元素的个数
voidclear() 清空
boolean isEmpty()检测优先级队列是否为空,空返回true
static void TestPriorityQueue2(){
    int[] arr = {4,1,9,2,8,0,7,3,6,5};
 
    // 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
    // 否则在插入时需要不多的扩容
    // 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
    PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
    for (int e: arr) {
        q.offer(e);
   }
 
    System.out.println(q.size());   // 打印优先级队列中有效元素个数
    System.out.println(q.peek());   // 获取优先级最高的元素
 
    // 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
    q.poll();
    q.poll();
    System.out.println(q.size());   // 打印优先级队列中有效元素个数
    System.out.println(q.peek());   // 获取优先级最高的元素
 
    q.offer(0);
    System.out.println(q.peek());   // 获取优先级最高的元素
 
    // 将优先级队列中的有效元素删除掉,检测其是否为空
    q.clear();
    if(q.isEmpty()){
        System.out.println("优先级队列已经为空!!!");
     }else{       
     	System.out.println("优先级队列不为空");  
     }
}

offer()方法源代码:
在这里插入图片描述

3.2.3. 优先队列的构造(创建大根堆)

在上述offer()的方法源代码中,当存在比较器时,调用siftUpUsingComparator()方法,使用用户提供的比较器对象进行比较;如果用户没有提供比较器对象,调用siftUpComparable()方法,采用Comparable进行比较

    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

在siftUpComparable()方法中调用compareTo()方法来比较两个对象的大小

    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

在siftUpUsingComparator()方法中调用compare()方法来比较两个对象的大小,使用用户提供的比较器对象进行比较

 private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

总结:

集合框架中的PriorityQueue底层使用堆结构,因此其内部的元素必须要能够比大小,PriorityQueue采用了:Comparble和Comparator两种方式。

  1. Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接口,并覆写compareTo方法
  2. 用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现Comparator接口并覆写compare方法。

上述两个方式的比较默认都是实现小根堆的,当我们要创建一个大根堆时,那么便需要自己实现上述两种方式来创建大根堆:
1.创建Comparator比较器

// 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
class Imp implements Comparator<Integer> {

    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
}


public class Test {
        public static void main(String[] args) {
            //1.自定义比较器:实现接口Comparator,重写该接口中的compare方法
            PriorityQueue<Integer> q1 = new PriorityQueue<>(new Imp());
            q1.offer(3);
            q1.offer(9);
            q1.offer(2);
            q1.offer(19);
         }
}

还可以使用匿名内部类的方式来简化代码:

         //2.匿名内部类:实现比较器
         PriorityQueue<Integer> q2 = new PriorityQueue<>(new Comparator<Integer>() {
             @Override
             public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
             }
          });
         q2.offer(3);
         q2.offer(9);
         q2.offer(2);
         q2.offer(19);          

2.实现Comparble接口

public class Card implements Comparable<Card> {
    public int rank; // 数值
    public String suit; // 花色

    public Card(int rank, String suit) {
        this.rank = rank;
        this.suit = suit;
    }

    // 根据数值比较,不管花色
    // 这里我们认为 null 是最小的
    @Override
    public int compareTo(Card o) {
        if (o == null) {
            return 1;
        }
        return o.rank - rank;
    }
}
public class Test {
        public static void main(String[] args) {
        	//3. 基于Comparble接口类的比较
            PriorityQueue<Card> p3 = new PriorityQueue<>();
            p3.offer(new Card(12,"♠"));
            p3.offer(new Card(1,"♠"));
            p3.offer(new Card(13,"♠"));
       }
}

3.2.4. 扩容方式

以下是JDK 1.8中,PriorityQueue的扩容方式:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 

private void grow(int minCapacity) {    
	int oldCapacity = queue.length;    
	// Double size if small; else grow by 50%    
	int newCapacity = oldCapacity + ((oldCapacity < 64) ? 
					  				 (oldCapacity + 2) :
					  				 (oldCapacity >> 1));    
	// overflow-conscious code    
	if (newCapacity - MAX_ARRAY_SIZE > 0)        
		newCapacity = hugeCapacity(minCapacity);    
	queue = Arrays.copyOf(queue, newCapacity); 
} 

private static int hugeCapacity(int minCapacity) {    
	if (minCapacity < 0) // overflow        
		throw new OutOfMemoryError();    
	return (minCapacity > MAX_ARRAY_SIZE) ? 
			Integer.MAX_VALUE : 
			MAX_ARRAY_SIZE; 
}

优先级队列的扩容说明:

  • 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
  • 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
  • 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

4. 堆的应用

4.1 PriorityQueue的实现

用堆作为底层结构封装优先级队列(在 2. 优先级队列的模拟实现 中)

4.2 堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 建堆
    升序:建大堆
    降序:建小堆
  2. 利用堆删除思想来进行排序
    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
    在这里插入图片描述
    public void ascendingSort() {
        //使用end标记未排序的数据的边界值
        int end = usedSize - 1;
        while (end > 0) {
            //将堆顶元素和最后一个元素进行交换
            int tmp = elem[end];
            elem[end] = elem[0];
            elem[0] = tmp;
            //将二叉树从0到未排序的数据的边界值向下调整
            shiftDown(0, end);
            end--;
        }
    }

4.3 Top-k问题

TOP-K问题题目链接

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

代码思路:创建小根堆获取前K个元素(获取K次堆顶,每一次都向下调整)

class Solution {
    public int[] smallestK(int[] arr, int k) {
       // 参数检测
       if(null == arr || k <= 0)
           return new int[0];
           
       PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);        
       // 将数组中的元素依次放到堆中     时间复杂度:O(n * H)= O(n * log2n)
       for(int i = 0; i < arr.length; ++i){
            q.offer(arr[i]);
       }
 
       // 将优先级队列的前k个元素放到数组中
       int[] ret = new int[k];
       for(int i = 0; i < k; ++i){
           ret[i] = q.poll();
       }
       return ret;
   }
}

时间复杂度:
每在堆中插入一次数据,就会向上调整一次数据,时间复杂度为:O( H)= O(   l o g 2 n \ log_2n  log2n),该方法共插入了n个数据,总的时间复杂度为O(n * H)= O(   n ∗ l o g 2 n \ n * log_2n  nlog2n);在整理数据时获取K次堆顶,每一次都会向下调整,调整一次的时间复杂度为O( H)= O(   l o g 2 n \ log_2n  log2n),共调整了k次,时间复杂度为O(k * H)= O(   k ∗ l o g 2 n \ k * log_2n  klog2n)。
总的时间复杂度为:O(   n ∗ l o g 2 n \ n*log_2n  nlog2n)+ O(   k ∗ l o g 2 n \ k * log_2n  klog2n)= O((n + k)   l o g 2 n \ log_2n  log2n

该解法只是PriorityQueue的简单使用,把所有数据放到优先级队列,出队K次——>数据量非常大的时候,无法把所有数据放到优先级队列。所以该方法并不是topK最好的做法,那topk该如何实现?下面介绍:

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
       前k个最大的元素,则建小堆
       前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

求最小的K个数,通过比较器创建大根堆:

//使用比较器创建大根堆
class GreaterIntComp implements Comparator<Integer>{
    @Override    
    public int compare(Integer o1, Integer o2) {        
    	return o2 - o1;  
    } 
} 
public class TestDemo {        
	//求最小的K个数,通过比较器创建大根堆    
	public static int[] smallestK(int[] array, int k) {        
		if(k <= 0 || array == null) {            
			return new int[k];       
		}   
		//优先队列默认时小根堆,提供比较器,实现大根堆     
		GreaterIntComp greaterCmp = new GreaterIntComp();        		
		PriorityQueue<Integer> maxHeap = new PriorityQueue<>(greaterCmp);        
		
		//先将前K个元素,创建大根堆 时间复杂度:O(k * H)= O(k * log2k)       
		for(int i = 0; i < k; i++) {   
			//插入元素:优先队列向上调整  时间复杂度:O(H)= O(log2k)       	
			maxHeap.offer(array[i]);       
		}        
		
		//从第K+1个元素开始,每次和堆顶元素比较  时间复杂度:O((n - k) * H)= O((n-k) * log2k),其中n为数组中元素的个数       
		for (int i = k; i < array.length; i++) {            
			int top = maxHeap.peek();            
			if(array[i] < top) {                
				maxHeap.poll();                
				maxHeap.offer(array[i]);          
			}     
		}        

		//取出前K个元素,整理数据,不用算入总的时间复杂度
		int[] ret = new int[k];
        for (int i = 0; i < k; i++) {
            int val = maxHeap.poll();
            ret[i] = val;
        }
        return ret;
   }
    
    public static void main(String[] args) {
        int[] array = {4,1,9,2,8,0,7,3,6,5};
        int[] ret = smallestK(array,3);
        System.out.println(Arrays.toString(ret));
   }
}

求最大的K个数,通过比较器创建小根堆 :

//使用比较器创建小根堆
class LessIntComp implements Comparator<Integer>{
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;   
    } 
}
public class TestDemo {        
	//求最大的K个数,通过比较器创建小根堆    
	public static int[] biggestK(int[] array, int k) {        
		if(k <= 0) {            
			return new int[k];       
		}        
		LessIntComp lessIntComp= new LessIntComp();        		
		PriorityQueue<Integer> minHeap = new PriorityQueue<>(lessIntComp);        
		
		//先将前K个元素,创建小根堆        
		for(int i = 0; i < k; i++) {            	
			minHeap.offer(array[i]);       
		}        
		
		//从第K+1个元素开始,每次和堆顶元素比较        
		for (int i = k; i < array.length; i++) {            
			int top = minHeap.peek();            
			if(array[i] > top) {                
				minHeap.poll();                
				minHeap.offer(array[i]);          
			}     
		}        

		//取出前K个元素
		int[] ret = new int[k];
        for (int i = 0; i < k; i++) {
            int val = minHeap.poll();
            ret[i] = val;
        }
        return ret;
   }
    
    public static void main(String[] args) {
        int[] array = {4,1,9,2,8,0,7,3,6,5};
        int[] ret = biggestK(array,3);
        System.out.println(Arrays.toString(ret));
   }
}

时间复杂度:
每在堆中插入一次数据,就会向上调整一次数据,时间复杂度为:O( H)= O(   l o g 2 k \ log_2k  log2k),该方法在第一步共插入了k个数据,总的时间复杂度为O(k * H)= O(   k ∗ l o g 2 k \ k * log_2k  klog2k),在第二步遍历剩余(n - k)个数据,最差情况下,最大值(最小值)在数组的最后部分,每次都会插入遍历的数据,时间复杂度为O((n - k) * H)= O(   ( n − k ) ∗ l o g 2 k \ (n - k) * log_2k  (nklog2k)。
总的时间复杂度就是两个时间复杂度相加的结果O(n* H)= O(   n ∗ l o g 2 k \ n * log_2k  nlog2k

  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值