前言:题图无关,接下来开始简单学习学习优先队列和堆的相关数据结构的知识;java
前序文章:node
听这个名字就能知道,优先队列也是一种队列,只不过不一样的是,优先队列的出队顺序是按照优先级来的;在有些状况下,可能须要找到元素集合中的最小或者最大元素,能够利用优先队列ADT来完成操做,优先队列ADT是一种数据结构,它支持插入和删除最小值操做(返回并删除最小元素)或删除最大值操做(返回并删除最大元素);git
这些操做等价于队列的enQueue
和deQueue
操做,区别在于,对于优先队列,元素进入队列的顺序可能与其被操做的顺序不一样,做业调度是优先队列的一个应用实例,它根据优先级的高低而不是先到先服务的方式来进行调度;github
若是最小键值元素拥有最高的优先级,那么这种优先队列叫做升序优先队列(即老是先删除最小的元素),相似的,若是最大键值元素拥有最高的优先级,那么这种优先队列叫做降序优先队列(即老是先删除最大的元素);因为这两种类型时对称的,因此只须要关注其中一种,如升序优先队列;web
下面操做组成了优先队列的一个ADT;面试
1.优先队列的主要操做 优先队列是元素的容器,每一个元素有一个相关的键值;算法
insert(key, data)
:插入键值为key的数据到优先队列中,元素以其key进行排序;deleteMin/deleteMax
:删除并返回最小/最大键值的元素;getMinimum/getMaximum
:返回最小/最大剑指的元素,但不删除它;2.优先队列的辅助操做api
第k最小/第k最大
:返回优先队列中键值为第k个最小/最大的元素;大小(size)
:返回优先队列中的元素个数;堆排序(Heap Sort)
:基于键值的优先级将优先队列中的元素进行排序;实现 | 插入 | 删除 | 寻找最小值 |
---|---|---|---|
无序数组 | 1 | n | n |
无序链表 | 1 | n | n |
有序数组 | n | 1 | 1 |
有序链表 | n | 1 | 1 |
二叉搜索树 | logn(平均) | logn(平均) | logn(平均) |
平衡二叉搜索树 | logn | logn | logn |
二叉堆 | logn | logn | 1 |
堆是一颗具备特定性质的二叉树,堆的基本要求就是堆中全部结点的值必须大于或等于(或小于或等于)其孩子结点的值,这也称为堆的性质;堆还有另外一个性质,就是当 h > 0 时,全部叶子结点都处于第 h 或 h - 1 层(其中 h 为树的高度,彻底二叉树),也就是说,堆应该是一颗彻底二叉树;数组
在下面的例子中,左边的树为堆(每一个元素都大于其孩子结点的值),而右边的树不是堆(由于5大于其孩子结点2)微信
在二叉堆中,每一个结点最多有两个孩子结点,在实际应用中,二叉堆已经足够知足需求,所以接下来主要讨论二叉最小堆和二叉最大堆;
**堆的表示:**在描述堆的操做前,首先来看堆是怎样表示的,一种可能的方法就是使用数组,由于堆在形式上是一颗彻底二叉树,用数组来存储它不会浪费任何空间,例以下图:
用数组来表示堆不只不会浪费空间还具备必定的优点:
left child(i) = i * 2
;每一个结点的右孩子为下标i的2倍加1:right child(i) = i * 2 + 1
parent(i) = i / 2
,注意这里是整数除,2和3除以2都为1,你们能够验证一下;public class MaxHeap<E extends Comparable<E>> { private Array<E> data; public MaxHeap(int capacity){ data = new Array<>(capacity); } public MaxHeap(){ data = new Array<>(); } // 返回堆中的元素个数 public int size(){ return data.getSize(); } // 返回一个布尔值, 表示堆中是否为空 public boolean isEmpty(){ return data.isEmpty(); } // 返回彻底二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引 private int parent(int index){ if(index == 0) throw new IllegalArgumentException("index-0 doesn't have parent."); return (index - 1) / 2; } // 返回彻底二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引 private int leftChild(int index){ return index * 2 + 1; } // 返回彻底二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引 private int rightChild(int index){ return index * 2 + 2; } }
当插入一个元素到堆中时,它可能不知足堆的性质,在这种状况下,须要调整堆中元素的位置使之从新变成堆,这个过程称为堆化(heapifying);在最大堆中,要堆化一个元素,须要找到它的父亲结点,若是不知足堆的基本性质则交换两个元素的位置,重复该过程直到每一个结点都知足堆的性质为止,下面咱们来模拟一下这个过程:
下面咱们在该堆中插入一个新的元素26:
咱们经过索引(上面的公式)能够很容易地找到新插入元素的父亲结点,而后比较它们的大小,若是新元素更大则交换两个元素的位置,这个操做就至关于把该元素上浮了一下:
重复该操做直到26到了一个知足堆条件的位置,此时就完成了插入的操做:
对应的代码以下:
// 向堆中添加元素 public void add(E e){ data.addLast(e); siftUp(data.getSize() - 1); } private void siftUp(int k){ while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){ data.swap(k, parent(k)); k = parent(k); } }
若是理解了上述的过程,那么取出堆中的最大元素(堆顶元素)将变得容易,不过这里运用到一个小技巧,就是用最后一个元素替换掉栈顶元素,而后把最后一个元素删除掉,这样一来元素的总个数也知足条件,而后只须要把栈顶元素依次往下调整就行了,这个操做就叫作Sift Down(下沉):
用最后元素替换掉栈顶元素,而后删除最后一个元素:
而后比较其孩子结点的大小:
若是不知足堆的条件,那么就跟孩子结点中较大的一个交换位置:
重复该步骤,直到16到达合适的位置:
完成取出最大元素的操做:
对应的代码以下:
// 看堆中的最大元素 public E findMax(){ if(data.getSize() == 0) throw new IllegalArgumentException("Can not findMax when heap is empty."); return data.get(0); } // 取出堆中最大元素 public E extractMax(){ E ret = findMax(); data.swap(0, data.getSize() - 1); data.removeLast(); siftDown(0); return ret; } private void siftDown(int k){ while(leftChild(k) < data.getSize()){ int j = leftChild(k); // 在此轮循环中,data[k]和data[j]交换位置 if( j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0 ) j ++; // data[j] 是 leftChild 和 rightChild 中的最大值 if(data.get(k).compareTo(data.get(j)) >= 0 ) break; data.swap(k, j); k = j; } }
Replace这个操做其实就是取出堆中最大的元素以后再新插入一个元素,常规的作法是取出最大元素以后,再利用上面的插入新元素的操做对堆进行Sift Up操做,可是这里有一个小技巧就是直接使用新元素替换掉堆顶元素,以后再进行Sift Down操做,这样就把两次O(logn)的操做变成了一次O(logn):
// 取出堆中的最大元素,而且替换成元素e public E replace(E e){ E ret = findMax(); data.set(0, e); siftDown(0); return ret; }
Heapify翻译过来就是堆化的意思,就是将任意数组整理成堆的形状,一般的作法是遍历数组从0开始添加建立一个新的堆,可是这里存在一个小技巧就是把当前数组就看作是一个彻底二叉树,而后从最后一个非叶子结点开始进行Sift Down操做就能够了,最后一个非叶子结点也很好找,就是最后一个结点的父亲结点,你们能够验证一下:
从22这个节点开始,依次开始Sift Down操做:
重复该过程直到堆顶元素:
完成堆化操做:
将n个元素逐个插入到一个空堆中,算法复杂度是O(nlogn),而heapify的过程,算法复杂度为O(n),这是有一个质的飞跃的,下面是代码:
public MaxHeap(E[] arr){ data = new Array<>(arr); for(int i = parent(arr.length - 1) ; i >= 0 ; i --) siftDown(i); }
首先咱们的队列仍然须要继承咱们以前将队列时候声明的哪一个接口Queue
,而后实现这个接口中的方法就能够了,之类简单写一下:
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> { private MaxHeap<E> maxHeap; public PriorityQueue(){ maxHeap = new MaxHeap<>(); } @Override public int getSize(){ return maxHeap.size(); } @Override public boolean isEmpty(){ return maxHeap.isEmpty(); } @Override public E getFront(){ return maxHeap.findMax(); } @Override public void enqueue(E e){ maxHeap.add(e); } @Override public E dequeue(){ return maxHeap.extractMax(); } }
在Java中也实现了本身的优先队列java.util.PriorityQueue
,与咱们本身写的不一样之处在于,Java中内置的为最小堆,而后就是一些函数名不同,底层仍是维护了一个Object类型的数组,你们能够戳戳看有什么不一样,另外若是想要把最小堆变成最大堆能够给PriorityQueue传入本身的比较器,例如:
// 默认为最小堆 PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.add(5); pq.add(2); pq.add(1); pq.add(10); pq.add(3); while (!pq.isEmpty()) { System.out.println(pq.poll() + ", "); } System.out.println(); System.out.println("————————————————————————"); // 使用Lambda表达式传入本身的比较器转换成最大堆 PriorityQueue<Integer> pq2 = new PriorityQueue<>((a, b) -> b - a); pq2.add(5); pq2.add(2); pq2.add(1); pq2.add(10); pq2.add(3); while (!pq2.isEmpty()) { System.out.println(pq2.poll() + ", "); }
参考答案:(85ms)
public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) return null; PriorityQueue<ListNode> q = new PriorityQueue<>(Comparator.comparing(node -> node.val)); for (int i = 0; i < lists.length; i++) { if (lists[i] != null) { q.add(lists[i]); } } ListNode dummy = new ListNode(0); ListNode tail = dummy; while (!q.isEmpty()) { tail.next = q.poll(); tail = tail.next; if (tail.next != null) { q.add(tail.next); } } return dummy.next; }
个人答案:(75ms)
public int findKthLargest(int[] nums, int k) { // 正确性判断 if (0 == nums.length || null == nums || k <= 0 || k > nums.length) { return -1; } // 构造优先队列,默认为最小堆,传入自定义的比较器转换成最大堆 PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a); for (Integer num : nums) { pq.add(num); } for (int i = 0; i < k - 1; i++) { pq.remove(); } return pq.peek(); }
参考答案:(5ms)
public int findKthLargest(int[] nums, int k) { if (nums.length == 1) { return nums[0]; } int max = nums[0]; int min = nums[0]; for (int i : nums) { max = i > max ? i : max; min = i < min ? i : min; } int[] arrs = new int[max - min + 1]; for (int i : nums) { arrs[max - i]++; } int pos = 0; for (int i = 0; i < arrs.length; i++) { pos += arrs[i]; if (pos >= k) { return max - i; } } return nums[0]; }
还看到一个简单粗暴的,也是服了:(4ms)
public int findKthLargest(int[] nums, int k) { Arrays.sort(nums); return nums[nums.length - k]; }
并且我随机生成了一个100万数据的随机数组,来测试这个简单粗暴的方法的效率,发现当数据量上去以后,排序这个操做变得繁琐,我本身测试的时候,上面三个方法,第三个大概比第一个(我本身写的方法)多花仅4倍的时间;
参考答案:(88ms)
public int[] maxSlidingWindow(int[] nums, int k) { if (nums == null || k <= 0) return new int[0]; int[] res = new int[nums.length - k + 1]; ArrayDeque<Integer> deque = new ArrayDeque<Integer>(); int index = 0; for (int i = 0; i < nums.length; i++) { while (!deque.isEmpty() && deque.peek() < i - k + 1) // Ensure deque's size doesn't exceed k deque.poll(); // Remove numbers smaller than a[i] from right(a[i-1]) to left, to make the first number in the deque the largest one in the window while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) deque.pollLast(); deque.offer(i);// Offer the current index to the deque's tail if (i >= k - 1)// Starts recording when i is big enough to make the window has k elements res[index++] = nums[deque.peek()]; } return res; }
参考答案2:(9ms)
public int[] maxSlidingWindow(int[] nums, int k) { /* 思想:依次遍历数组,有效范围在长度k内寻找当前最大值,在用result数组来依次存储当前长度K内的最大值; 若在当前轮中出现新增的nums[end]大于curMax,直接替换便可; 若是当前轮curMax不是新增的nums[end],在新的范围内重置curMax. */ if (nums.length == 0 || k <= 0) return new int[0]; int curMax = Integer.MIN_VALUE; for (int i = 0; i < k; i++) { if (nums[i] > curMax) curMax = nums[i]; } int[] ans = new int[nums.length - k + 1]; ans[0] = curMax; for (int start = 1; start + k - 1 < nums.length; start++) { int end = start + k - 1; if (nums[end] > curMax) curMax = nums[end]; else if (nums[start - 1] == curMax) {//新增的不大于curMax,新范围内重置 curMax = Integer.MIN_VALUE; for (int i = start; i <= end; i++) { if (nums[i] > curMax) curMax = nums[i]; } } ans[start] = curMax; } return ans; }
参考答案:(7ms)
public int nthUglyNumber(int n) { // 正确性判断 if (n < 1 || n > 1690) { return -1; } int[] ugly = new int[n]; ugly[0] = 1; int index2 = 0, index3 = 0, index5 = 0; int factor2 = 2, factor3 = 3, factor5 = 5; for (int i = 1; i < n; i++) { int min = Math.min(Math.min(factor2, factor3), factor5); ugly[i] = min; if (factor2 == min) factor2 = 2 * ugly[++index2]; if (factor3 == min) factor3 = 3 * ugly[++index3]; if (factor5 == min) factor5 = 5 * ugly[++index5]; } return ugly[n - 1]; }
若是采用逐个判断每一个整数是否是丑数的解法,直观但不够高效,因此咱们就须要换一种思路,个人第一反应就是这其中必定有什么规律,可是尝试着找了一下,没找到...看了看答案才幡然醒悟,前面提到的算法之因此效率低,很大程度上是由于无论一个数是否是丑数,咱们都要对它进行计算,接下来咱们试着找到一种只计算丑数的方法,而不在非丑数的整数上花费时间,根据丑数的定义,丑数应该是另外一个丑数乘以二、3或者5的结果(1除外),所以,咱们能够建立一个数组,里面的数字是排好序的丑数,每一个丑数都是前面的丑数乘以二、3或者5获得的,也就是上面的算法了..
参考答案:(219ms)
public class MedianFinder { PriorityQueue<Integer> maxHeap; PriorityQueue<Integer> minHeap; /** * initialize your data structure here. */ public MedianFinder() { maxHeap = new PriorityQueue<>(Collections.reverseOrder()); minHeap = new PriorityQueue<>(); } public void addNum(int num) { maxHeap.add(num); minHeap.add(maxHeap.poll()); if (minHeap.size() - maxHeap.size() > 0) { maxHeap.add(minHeap.poll()); } } public double findMedian() { if (maxHeap.size() == minHeap.size()) { return (maxHeap.peek() + minHeap.peek()) / 2.0; } else { return maxHeap.peek(); } } }
**思路:**这道题的实现思路有不少,好比咱们能够在插入的时候就将每一个元素插入到正确的位置上,这样返回中位数的时候就会是一个O(1)的操做,下面列举一张表来讲明不一样实现的复杂度具体是多少:
数据结构 插入的时间复杂度 获得中位数的时间复杂度 没有排序的数组 O(1) O(n) 排序的数组 O(n) O(1) 排序的链表 O(n) O(1) 二叉搜索树 平均O(logn),最差O(n) 平均O(logn),最差O(n) AVL树 O(logn) O(logn) 最大堆和最小堆 O(logn) O(logn) AVL树是一种很高效的数据结构,可是在大多数的语言中都没有现成的实现,因此考虑用最大堆和最小堆,对于一个已经排好序的数据容器,咱们能够从中间分开分红两个部分,其中拿P1指向左半部分最大的元素,拿P2指向有半部分最小的元素,若是可以保证数据容器左边的数据都小于右边的数据,那么即便左、右两边内部的数据没有排序,咱们仍然能够根据左边最大的数和右边最大的数获得中位数:
如何快速从一个数据容器中找出最大数呢?咱们可使用最大堆来实现这个数据容器,由于堆顶的元素就是最大的元素;一样咱们可使用最小堆来快速找出一个数据容器中最小数。所以按照这个思路咱们就可使用一个最大堆实现左边的数据容器,使用一个最小堆实现右边的数据容器,可是须要注意的是这两个容器的大小差值不能超过1;
参考答案:(131ms)
public List<Integer> topKFrequent(int[] nums, int k) { TreeMap<Integer, Integer> map = new TreeMap<>(); // 保存频率 for (int num : nums) { if (map.containsKey(num)) { map.put(num, map.get(num) + 1); } else { map.put(num, 1); } } PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(map::get)); for (int key : map.keySet()) { if (pq.size() < k) { pq.add(key); } else if (map.get(key) > map.get(pq.peek())) { pq.remove(); pq.add(key); } } LinkedList<Integer> res = new LinkedList<>(); while (!pq.isEmpty()) { res.add(pq.remove()); } return res; }
参考答案:(72ms)
public List<String> topKFrequent(String[] words, int k) { Map<String, Integer> count = new HashMap(); for (String word: words) { count.put(word, count.getOrDefault(word, 0) + 1); } List<String> candidates = new ArrayList(count.keySet()); Collections.sort(candidates, (w1, w2) -> count.get(w1).equals(count.get(w2)) ? w1.compareTo(w2) : count.get(w2) - count.get(w1)); return candidates.subList(0, k); }
这道题相似于上面的第347题,可是问题出在返回的顺序上,须要本身来定义一个比较器来排序..而后也学到一个写法,就是上面的第一个for循环里,
getOrDefault()
方法,get√..
参考答案2:(91ms)
public List<String> topKFrequent(String[] words, int k) { Map<String, Integer> count = new HashMap(); for (String word: words) { count.put(word, count.getOrDefault(word, 0) + 1); } PriorityQueue<String> heap = new PriorityQueue<String>( (w1, w2) -> count.get(w1).equals(count.get(w2)) ? w2.compareTo(w1) : count.get(w1) - count.get(w2) ); for (String word: count.keySet()) { heap.offer(word); if (heap.size() > k) heap.poll(); } List<String> ans = new ArrayList(); while (!heap.isEmpty()) ans.add(heap.poll()); Collections.reverse(ans); return ans; }
这个解法就有点儿相似于上面的347题,实际上是大同小异,就是本身不会灵活使用比较器而已,学习到了学习到了√...
今天算是颇有收获的一天,由于这两种数据结构都是本身特别不熟悉的,特别是在刷了一些LeetCode相关题目以后,对这两种数据有了很不同的认识,特别是堆的应用,这是一种特别适合用来找第k小/大的特殊的数据结构,而且在Java中竟然有直接的实现,这可太棒了,并且今天的效率还算挺高的,知足;
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz 欢迎关注公众微信号:wmyskxz_javaweb 分享本身的Java Web学习之路以及各类Java学习资料