接上一篇,二叉堆实际的应用

一.java定时器对象Timer,里面就是维护着数组实现的二叉堆,最先要到期的任务放在数组中第一个位置,第一个位置出队后,剩下的数组再通过下沉上浮变形成新的二叉堆;依次下去成一个定时器。

1.问题:为什么不直接一下子排好顺序?

答案很简单,因为不需要,定时任务只要知道下一个要执行的任务就可以,没有必要浪费更高的时间复杂度去实现完全有序。

2.顺便记录一下,使用Timer对象,有一些隐形的坑:

 1 public static void main(String[] args) {
 2         //创建定时器对象
 3         Timer timer = new Timer();
 4 
 5         //添加任务1,延迟1000ms执行
 6         timer.schedule(new TimerTask() {
 7             @Override
 8             public void run() {
 9                 for (; ; ) {
10                     System.out.println("---one Task---");
11                     try {
12                         Thread.sleep(2000);
13                     } catch (InterruptedException e) {
14                         e.printStackTrace();
15                     }
16                 }
17                 //throw new RuntimeException("error ");
18             }
19         }, 1000);
20 
21         //添加任务2,延迟1500ms执行
22         timer.schedule(new TimerTask() {
23             @Override
24             public void run() {
25                 System.out.println("---two Task---");
26                 try {
27                     Thread.sleep(1000);
28                 } catch (InterruptedException e) {
29                     e.printStackTrace();
30                 }
31             }
32         }, 1500);
33     }
代码例子

1).上面例子中,永远只会打印"---one Task---",为什么呢?

因为task1和task2放进Timer对象内部数组的时候,用的是系统当前时间,task1延迟1000ms后执行,task2延迟1500ms后执行;而timer对象内部是单线程,不停的在死循环,去二叉堆中看第一个任务是不是可以到时间执行了,显然task1会先执行,串行顺序的执行,如果task1占着不放,task2将永远执行不了

2).同样因为单线程的串行顺序执行,如果有一个任务在执行过程中抛出异常,完了,整个后续链就over了

3).sched增加任务、cancel取消任务、purge清除任务、执行任务,都在操作同一个队列,Timer对象是怎样保证这个队列线程安全的呢?嗯,用悲观锁synchronized(queue)的方式,所以,你懂的,会出现竞争锁的问题

二.PriorityQueue优先队列

估计好多同学和我一样,之前只是听说过这个队列,尤其是在线程池相关的面试题中,哈哈,其实这个队列还是很牛的,在某些场景下;

1.leetcode 703:求数据流中的第K大元素,数据流中包含初始数组,每次调用add(int val)方法,增加新的元素val后再返回当前数据流中第K大的元素。当然解这道题时,可以不管三七二十一,整个数组全排列,然后在排好的数组中再找出第K大的元素,嗯,可以,时间复杂度呢?

 1 int k;
 2 
 3     public KthLargest703(int k, int[] nums) {
 4         queue = new PriorityQueue<>();
 5         for(int i=0;i<nums.length;i++) {
 6             queue.add(nums[i]);
 7         }
 8         this.k = k;
 9     }
10 
11     PriorityQueue<Integer> queue;//优先级队列:内容使用堆(默认二叉小顶堆)来实现的
12 
13     public int add(int val) {
14         if(queue.size()<k){//队列中还没有k个元素
15             queue.offer(val);//直接放入队列
16         } else if (queue.peek() < val){//当前值大于堆顶
17             queue.poll();//堆顶扔出来
18             queue.offer(val);//当前值放入队列,内容自动调整上浮或下沉
19         }
20         return queue.peek();//返回堆顶,默认使用了小顶堆
21     }
代码例子

2.leetcode 347:给定一个非空的整数数组,返回其中出现频率前 高的元素,仔细想想,其实和上面703的问题一样

 1 public static List<Integer> topKFrequent(int[] nums, int k) {
 2         //使用哈希存储元素与频率的映射
 3         Map<Integer, Integer> map = new HashMap<>();
 4         for (int i : nums) {
 5             Integer temp = map.get(i);
 6             if(temp==null){
 7                 map.put(i, 1);
 8             } else {
 9                 temp++;
10                 map.put(i, temp);
11             }
12         }
13 
14         //使用小顶堆筛选前k个高频的元素,不能使用自然排序,需要重写比较的类函数
15         PriorityQueue<Integer> pq = new PriorityQueue<>(k, new Comparator<Integer>() {
16             public int compare(Integer a, Integer b){
17                 return map.get(a) - map.get(b);    //堆中存放的是元素,但比较的应该是元素的频率
18             }
19         });
20         int size = 0;
21         for (Integer j : map.keySet()) {
22             if(size<k){
23                 pq.offer(j);//前k个元素直接放入,自动排成小顶堆
24                 size++;
25             }else if(map.get(j)>map.get(pq.peek())){
26                 pq.poll();
27                 pq.add(j);
28             }
29         }
30 
31         //返回结果
32         List<Integer> ans = new ArrayList<>();
33         while(!pq.isEmpty()){
34             ans.add(pq.poll());
35         }
36         return ans;
37     }
代码例子

 

3.leetcode 239:给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字,滑动窗口每次只向右移动一位;返回滑动窗口中的最大值