目录
在堆中取出最大值(最大堆)使用siftDown():下沉操作
优先级队列
优先级队列的概念
优先级队列(堆):按照优先级的大小动态出队(动态指的是元素个数动态变化,而非固定)。
普通队列(FIFO):按照元素的入队顺序出队,先入先出。
现实生活中的优先级队列PriorityQueue举例
1. 医生根据病人病情进行手术排期
排期时,包括具体手术时,病人的人数都在动态变化。
病情相同的情况下按照先来先排,若病情较重,优先安排手术。
问题:优先级队列和排序有何区别?能否按照病情排序,然后排序后的病人数组安排手术?
答:不一样,排序是元素个数确定的情况,优先级队列是动态变化的。
如:医生原定16日安排3人手术,但17日突然来了一个病情严重的病人,此病人优先安排手术。此时数据在动态变化。
2. 操作系统的任务调度
也是优先级队列
系统的任务一般优先级都比普通的应用要高CPU、内存等资源有限,当资源不够用时,优先让优先级较高的应用获取资源。
优先级队列的时间复杂度
在计算机领域,若见到 logn 时间复杂度,近乎一定和'树"结构相关(并非一定要构造一棵树结构,而是算法过程逻辑上一定是一模树)。
如:归并排序,快速排序的递归过程也是—个"递归"树。
二叉堆(实现优先级队列前先引入堆)
基于二叉树的堆(二叉堆,应用最广泛的堆),如:d叉堆,索引堆。
1. 是一棵完全二叉树,基于数组存储(元素都是靠左排列,数组中存储时不会浪费空间),只有完全二叉树适合使用数组这种结构来存储其他的二叉树都要用链式结构。
2. 关于节点值
堆中根节点值 >= 子树节点中的值(最大堆,大根堆),堆中树根 >= 子树中所有节点,所有子树也仍然满足堆的定义。堆中根节点值 <= 子树中节点的值(最小堆,小根堆),堆中树根 <= 子树中所有节点,所有子树也仍然满足堆的定义。
JDK中的PriorityQueue默认是基于最小堆的实现。
问题:最大堆中是否越"高"的结点一定就比越"低"值大?
答:节点的层次和节点的大小没有任何关系。只能保证当前树中的树根是最大值。其他节点层次大小关系不确定。
3. 因为堆是基于数组来存储的,节点之间的关系通过数组下标来表示,从0开始编号,数组下标也是从0开始。
假设此时结点编号 i,且存在父子节点:
父节点编号:parent = (i - 1) / 2;
左子树的编号:left = 2 * i + 1;
右子树的编号:left = 2 * i + 2;节点之间通过数组的索引下标来找到父子节点。
二叉堆的相关操作
向堆中添加元素 使用siftUp():上浮操作,
向堆中添加─个新的元素val:
1. 在数组的末尾添加元素(尾插)。此时仍然是一颗完全二叉树,节点紧密排列。2. 此时的这个堆仍然满足完全二叉树的性质,但是它不再是一个最大堆了,因此需要进行元素的上浮操作,让新添加的元素上浮到合适位置。
不断将此时索引 k 和父节点的索引 i 对应的元素进行大小关系比较,若大于父节点就交换彼此的节点值,直到当前节点 <= 父节点为止或走到树根。
上浮操作的终止条件:
1. 当前已经上浮到树根——>这个元素—定是最大值
2. 当前元素 <= 父节点对应的元素值,此时元素落在正确位置public void add(int val) { //1.直接向数组末尾添加元素 elementData.add(val); size++; //2.进行上浮操作 siftUp(size - 1); } public void siftUp(int k) { while (k > 0 && elementData.get(k) > elementData.get(parent(k))) { swap(k, parent(k)); k = parent(k); } } private void swap(int k, int parent) { int child = elementData.get(k); int parentVal = elementData.get(parent); elementData.set(k,parentVal); elementData.set(parent,child); }
public class MaxheapTest { public static void main(String[] args) { MaxHeap maxHeap = new MaxHeap(); maxHeap.add(62); maxHeap.add(41); maxHeap.add(30); maxHeap.add(28); maxHeap.add(16); maxHeap.add(22); maxHeap.add(13); maxHeap.add(19); maxHeap.add(17); maxHeap.add(15); maxHeap.add(52); System.out.println(maxHeap.extraMax()); System.out.println(maxHeap); } }
在堆中取出最大值(最大堆)使用siftDown():下沉操作
1. 最大堆的最大值一定处在树的根节点。直接取出树根即可。
2. 需要融合左右两个子树,使得取出树根后这棵树仍然是最大堆。此时融合操作比较复杂,因为左右子树大小关系不定,且节点的大小和层次没有必然联系。要使用最低成本融合左右子树,使其仍满足最大堆的性质,应进行比较和移动最少的操作,即移动数组的末尾元素。
操作步骤:
1. 直接取数根节点就是当前堆的最大值。
2. 将堆中最后一个元素放到堆顶,然后进行元素的下沉操作。siftDown——>使其仍然满足最大堆的性质。例如:
16覆盖到树根,然后从树中删除16这个节点。
移除16之后这棵树仍然是完全二叉树,但此时不满足最大堆的性质。不断和左右子树的最大值进行交换直到16这个节点落在了最终位置。
siftUp():下沉操作
/** * 取出当前最大堆的最大值 */ public int extraMax() { if (size == 0) { throw new NoSuchElementException("heap is empty!canot extract!"); } //树根就是最大值节点 int max = elementData.get(0); //将数组末尾顶到堆顶 elementData.set(0, elementData.get(size - 1)); elementData.remove(size - 1); size--; //进行元素的下沉操作,从索引为0开始 siftDown(0); return max; } /** * 从索引k开始,进行元素的下沉操作 */ private void siftDown(int k) { //还存在左子树 while (leftChild(k) < size) { int j = leftChild(k); //此时还存在右子树 if (j + 1 < size && elementData.get(j + 1) > elementData.get(j)) { //此时存在右子树,右子树值大于左子树值 j++; } //索引j一定对应了左右子树 最大值索引 if (elementData.get(k) >= elementData.get(j)) { //当前元素 >= 左右子树的最大值,下沉结束,元素k落在了最终位置 break; } else { swap(k,j); k = j; } } }
public class MaxheapTest { public static void main(String[] args) { MaxHeap maxHeap = new MaxHeap(); maxHeap.add(62); maxHeap.add(41); maxHeap.add(30); maxHeap.add(28); maxHeap.add(16); maxHeap.add(22); maxHeap.add(13); maxHeap.add(19); maxHeap.add(17); maxHeap.add(15); maxHeap.add(52); System.out.println(maxHeap.extraMax()); System.out.println(maxHeap); } }
heapify - 堆化
给定一个任意的整型数组,都可以看做是一个完全二叉树,距离最大堆就差元素调整操作。
变为最大堆
方法一:
1. 将这n个元素依次调用add方法添加到一个新的最大堆中。
遍历原数组,创建一个新的最大堆,调用最大堆的add方法即可。时间复杂度:O(nlogn),空间复杂度:O(n)。
遍历原数组O(n),每次添加一个元素到堆中,在二叉树添加一个节点lognfor(int i = 0;i < n;i ++)( heap.add(data[i]); }
方法二:
原地heapify——时间复杂度:O(n)。
从最后一个非叶子结点开始进行元素siftDown操作。
从当前二叉树中最后一个小子树开始调整,不断向前,直到调整到根节点。
不断将子树调整为最大堆时,最终走到树根时,左右子树已经全都是最大堆,只需最后下沉根节点就能得到最终的最大堆。
/** * 将任意的整型数组arr调整为堆 */ public MaxHeap(int[] arr) { elementData = new ArrayList<>(arr.length); // 1.先将所有元素复制到data数组中 for (int i : arr) { elementData.add(i); size++; } size = elementData.size(); // 2. 从最后一个非叶子节点开始进行siftDown操作 for (int i = parent(size - 1); i >= 0; i--) { siftDown(i); } }
public class MaxheapTest { public static void main(String[] args) { int data[] = {15,17,19,13,22,16,28,30,41,62}; MaxHeap maxHeap = new MaxHeap(data); System.out.println(maxHeap); } }
在Java中比较两个元素大小关系
比较两个元素相等用equals
比较两个自定义对象的大小关系,两种方法:类覆写Comparable接口、实现compareTo方法。例如:若一个类Student implements Comparable,则这个Student类具备可比较的能力。
package bintree.heap.compare; import java.util.Arrays; public class Student implements Comparable<Student>{ private int age; private String name; public Student(int age, String name) { this.age = age; this.name = name; } public int getAge() { return age; } @Override public int compareTo(Student o) { return this.age - o.age; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } public static void main(String[] args) { Student[] students = new Student[]{ new Student(20, "张三"), new Student(18, "李四"), new Student(17, "王五") }; Arrays.sort(students); System.out.println(Arrays.toString(students)); } } //输出结果 [Student{age=17, name='王五'}, Student{age=18, name='李四'}, Student{age=20, name='张三'}]
比较当前对象和传入对象的大小关系
>0:当前元素"大于"传入对象o
<0:当前元素"小于"传入对象o=0:当前元素"等于"传入对象o
java.util.Comparator接口 比较器接口
一个类若实现了这个接口,表示这个类天生就是为别的类的大小关系服务的
package bintree.heap.compare; import java.util.Arrays; import java.util.Comparator; /** * 比较器 */ // 此时这个类是根据年龄越大,就越“大”进行比较,即升序 class StudentSec implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.getAge() - o2.getAge(); } } public class Student { private int age; private String name; public Student(int age, String name) { this.age = age; this.name = name; } public int getAge() { return age; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } public static void main(String[] args) { Student[] students = new Student[]{ new Student(20, "张三"), new Student(18, "李四"), new Student(17, "王五") }; //得到一个“升序”数组 //此时Student类不具备可比较的能力,可以传入一个比较器对象 Arrays.sort(students,new StudentSec()); System.out.println(Arrays.toString(students)); } } //输出结果 [Student{age=17, name='王五'}, Student{age=18, name='李四'}, Student{age=20, name='张三'}]
如StudentSec 这个类天生就是为了 Student 对象的排序而存在
覆写compare方法(o1,o2) ,返回值 int 表示o1和o2的大小关系:
> 0,o1 > o2
= 0,o1 = o2< 0,o1 < o2
package bintree.heap.compare; import java.util.Arrays; import java.util.Comparator; /** * 比较器 */ // 此时这个类是根据年龄越小,就越“大”进行比较,即降序 class StudentDesc implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o2.getAge() - o1.getAge(); } } public class Student { private int age; private String name; public Student(int age, String name) { this.age = age; this.name = name; } public int getAge() { return age; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } public static void main(String[] args) { Student[] students = new Student[]{ new Student(20, "张三"), new Student(18, "李四"), new Student(17, "王五") }; //得到一个“降序”数组 //此时Student类不具备可比较的能力,可以传入一个比较器对象 Arrays.sort(students,new StudentDesc()); System.out.println(Arrays.toString(students)); } } //输出结果 [Student{age=20, name='张三'}, Student{age=18, name='李四'}, Student{age=17, name='王五'}]
StudentDesc与StudentSec顺序相反。
当把Student类的大小关系比较从Student类中"解耦"。
此时的比较策略非常灵活,需要哪种方式,就创建一个新的类实现 Comparator 接口即可。
根据此时大小关系的需要传入比较器对象。即策略模式。
基于最大堆的优先级队列
package bintree.heap; import seqlist.queue.Queue; /** * 基于最大堆的优先级队列 */ public class PriorityQueue implements Queue<Integer> { private MaxHeap heap; public PriorityQueue() { heap = new MaxHeap(); } @Override public void offer(Integer val) { heap.add(val); } @Override public Integer poll() { return heap.extraMax(); } @Override public Integer peek() { return heap.peekMax(); } @Override public boolean isEmpty() { return heap.isEmpty(); } }
package bintree.heap.compare; import seqlist.queue.Queue; /** * 优先级队列测试 */ public class PriorityQueueTest { public static void main(String[] args) { int[] data = {3, 5, 7, 6, 2, 1, 9, 4}; Queue<Integer> queue = new PriorityQueue(); for (int i : data) { queue.offer(i); } System.out.println(queue.poll()); } }
出队输出最大值9
TopK问题——基于堆的优先级队列的实现与应用
要求时间复杂度优于O(nlogn)算法
思路:取大用小,取小用大。
若需要取出前k个最大元素,构造最小堆。
若需要取出前k个最小元素,构造最大堆。
面试题 17.14. 最小K个数
思路
1. 若此时队列的元素个数 < k,直接添加到队列中。
2. 若此时元素个数 == k
a. 新扫描到的元素val >= 堆顶元素,一定大于此时堆中的所有元素,则val一定不是我要的结果,直接跳过。
b. 若此时val < 堆顶元素,堆顶元素出队,将新元素val添加到队列中。
c. 重复上述过程,直到整个集合被我们扫描完毕,队列中恰好就保存了前k个最小值。随着堆顶元素不断交换,会把堆顶元素不断变小,最终队列扫描结束就存放了最小的k个数。public int[] smallestK(int[] arr, int k) { int[] ret = new int[k]; if (arr.length == 0 || k==0) { return ret; } //JDK默认是最小堆,需要改造为最大堆 Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); //遍历集合,队列只保存k个元素 for (int i = 0; i < arr.length; i++) { if (queue.size() < k) { queue.offer(arr[i]); } else { int max = queue.peek(); if (arr[i] < max) { queue.poll(); queue.offer(arr[i]); } } } //此时队列中就保存了前k个最小元素值,依次出队即可 int i = 0; while (!queue.isEmpty()) { ret[i++] = queue.poll(); } return ret; }
时间复杂度为O(nlogk),当k <<n时,logk 和logn 差距明显。
题解包含内部类的操作
所谓的内部类,就是一个类嵌套到另一个类的内部的操作。
匿名内部类的优化写法
上述内部类也可改写为如下
347. 前 K 个高频元素
给你一个整数数组
nums
和一个整数k
,请你返回其中出现频率前k
高的元素。你可以按 任意顺序 返回答案。
优先级队列要保存元素,需要保存键值对,即出现的元素以及其频次。
现有的类型无法解决,自定义一个类,优先级队列中就保存该类的对象即可。Freq{ int key; int value; }
题解
package bintree.heap.leetcode; import java.util.HashMap; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; // 这个类的对象就是数组中的元素以及其出现的频次 //优先级队列中就添加该类的对象 class Freq implements Comparable<Freq> { //数组中出现的元素 int key; //该元素出现的频次 int value; public Freq(int key, int value) { this.key = key; this.value = value; } @Override public int compareTo(Freq o) { return this.value - o.value; } } public class Num347_topKFrequent { public int[] topKFrequent(int[] nums, int k) { int[] ret = new int[k]; // 1.扫描原数组,将出现的元素以及其频次保存到Map集合中 // [1,1,1,2,2,3]——>{(1:3),(2:2),(3:1)} Map<Integer, Integer> map = new HashMap<>(); for (int i : nums) { if (map.containsKey(i)) { //此时只需要将频次++即可 int times = map.get(i); map.put(i, times + 1); } else { //此时i第一次出现,将i保存到Map中 map.put(i, 1); } } // 2.扫描Map集合,将出现频次最高的前k个元素添加到优先级队列中 Queue<Freq> queue = new PriorityQueue<>(); for (Map.Entry<Integer, Integer> entry : map.entrySet()) { if (queue.size() < k) { queue.offer(new Freq(entry.getKey(), entry.getValue())); } else { //判断堆顶元素和当前元素的出现频次 //只有当前元素的出现频次 > 堆顶元素,入队,打擂思想 //不断将出现频次大的元素对换入到队列中 Freq freq = queue.peek(); if (entry.getValue() > freq.value) { queue.poll(); queue.offer(new Freq(entry.getKey(), entry.getValue())); } } } // 3.此时队列就保存了出现频次最大的前k个数对——k个Freq对象 // 遍历队列将Freq对象的key取出放入结果集中即可 int i = 0; while (!queue.isEmpty()) { ret[i++] = queue.poll().key; } return ret; } }
373. 查找和最小的 K 对数字
给定两个以 升序排列 的整数数组 nums1 和 nums2 , 以及一个整数 k 。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。
请找到和最小的 k 个数对 (u1,v1), (u2,v2) ... (uk,vk) 。
题解
package bintree.heap.leetcode; import java.util.*; /** * 373. 查找和最小的 K 对数字,最大堆 */ class Pair { //u来自数组1 int u; //v来自数组2 int v; public Pair(int u, int v) { this.u = u; this.v = v; } } public class Num373_kSmallestPairs { public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) { // 1.创建优先级队列,其中队列中保存Pair对象,传入比较器,和越小反而越大 Queue<Pair> queue = new PriorityQueue<>(new Comparator<Pair>() { @Override public int compare(Pair o1, Pair o2) { return (o2.u + o2.v) - (o1.u + o1.v); } }); // 2.遍历两个数组,其中u来自第一个数组,v来自第二个数组 // 队列中保存和最小的k个数对。 for (int i = 0; i < Math.min(nums1.length, k); i++) { for (int j = 0; j < Math.min(nums2.length, k); j++) { if (queue.size() < k) { queue.offer(new Pair(nums1[i], nums2[j])); } else { int add = nums1[i] + nums2[j]; Pair pair = queue.peek(); if (add < (pair.u + pair.v)) { queue.poll(); queue.offer(new Pair(nums1[i], nums2[j])); } } } } // 3.此时队列中就保存了和最小的前k个Pair对象,取出其中u和v即可 List<List<Integer>> ret = new ArrayList<>(); int i = 0; while (!queue.isEmpty()) { List<Integer> temp = new ArrayList<>(); Pair pair = queue.poll(); temp.add(pair.u); temp.add(pair.v); ret.add(temp); } return ret; } }
原地堆排序
给定任意的数组,在这个数组的基础上进行堆排序,不创建任何额外空间。[15,19,11,18,14,17,6,3,8,1,9]
1. 任意数组其实就可以看做是一个完全二叉树,将这个数组调整为最大堆。heapify调整为最大堆。从最后一个非叶子结点开始进行元素siftDown操作。
2. 不断交换堆顶元素和最后一个元素的位置,将堆顶元素继续进行siftDown。最大值放在最终位置,直到数组剩下一个未排序的元素为止。package sort; import java.util.Arrays; public class SevenSort { public static void main(String[] args) { int[] arr = {15,19,11,18,14,17,6,3,8,1,9}; heapSort(arr); System.out.println(Arrays.toString(arr)); } // 原地堆排序 public static void heapSort(int[] arr) { //将任意数组进行heapify操作,调整为最大堆 for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) { siftDown(arr, i, arr.length); } // 2.不断交换堆顶元素到数组末尾, // 每交换一个元素就有一个元素落在了最终位置 for (int i = arr.length - 1; i > 0; i--) { //arr[i]就是未排序数组的最大值,交换到末尾 swap(arr, 0, i); siftDown(arr, 0, i); } } private static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } /** * 元素下沉操作,最大堆——顶大底小 */ private static void siftDown(int[] arr, int i, int length) { while ((2 * i + 1) < length) { int j = 2 * i + 1; if (j + 1 < length && arr[j + 1] > arr[j]) { j = j + 1; } if (arr[i] > arr[j]) { break; } else { swap(arr, i, j); i = j; } } } }