0 课程地址
https://coding.imooc.com/lesson/207.html#mid=13744
1 重点关注
1.1 用手写的优先队列实现取前k个高频元素问题
见3.1
1.2 自定义compare
compare有默认的,但是也可以自定义,参考3.1
1.3 比较map的value的内部类
参考3.1
2 课程内容
3 Coding
3.1 LeetCode 347问题 取前K个高频元素(手写优先队列实现)
- 需求
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。 示例 1: 输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2] 示例 2: 输入: nums = [1], k = 1 输出: [1] 提示: 1 <= nums.length <= 105 k 的取值范围是 [1, 数组中不相同的元素的个数] 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的 进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。 来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/top-k-frequent-elements
- 设计思路:
组成一个元素数量为k的堆,进行上浮下沉,复杂度为O(nlogk),优于O(nlogn)
- coding:
import java.util.HashMap; import java.util.Map; /** * 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。 * 使用自己实现的堆(最大堆来实现) * @author weidoudou * @date 2023/1/12 7:34 **/ class Solution { /** * warning 定义这个类,是为了仿照Map进行比较 * @author weidoudou * @date 2023/1/13 6:56 **/ private class MapInnerClass implements Comparable<MapInnerClass>{ private int key,value; public MapInnerClass(int key,int value){ this.key = key; this.value = value; } /** * waring这边好好理解下,这里定义的应该是最小堆(把最大堆组装成了map根据value比较的最小堆),这块 * 用于两部分,1部分是上浮下沉操作,2部分是71行外部比较 * @author weidoudou * @date 2023/1/13 7:17 * @param o 请添加参数描述 * @return int **/ @Override public int compareTo(MapInnerClass o) { if(this.value<o.value){ return 1; }else if(this.value>o.value){ return -1; }else{ return 0; } } } 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 void enqueue(E e) { maxHeap.shiftup(e); } @Override public E dequeue() { return maxHeap.remove(); } @Override public E getFront() { return maxHeap.findMax(); } } /** * 优先队列接口 * @author weidoudou * @date 2023/1/11 5:46 **/ public interface Queue <E>{ /** * 获取大小 * @author weidoudou * @date 2023/1/11 5:46 * @return int **/ int getSize(); /** * 是否为空 * @author weidoudou * @date 2023/1/11 5:46 * @return boolean **/ boolean isEmpty(); /** * 入队操作 * @author weidoudou * @date 2023/1/11 5:47 * @param e 请添加参数描述 * @return void **/ void enqueue(E e); /** * 出队操作 * @author weidoudou * @date 2023/1/11 5:48 * @return E **/ E dequeue(); /** * 取出堆顶元素 * @author weidoudou * @date 2023/1/11 5:48 * @param * @return E **/ E getFront(); } /** * 最大堆 * @author weidoudou * @date 2023/1/3 12:35 **/ public class MaxHeap<E extends Comparable<E>> { private Array<E> data; public MaxHeap(){ data = new Array<E>(); } public MaxHeap(int capacity){ data = new Array<E>(capacity); } /** * heapify 将数组转化为堆(将数组可以看作完全二叉堆) * @author weidoudou * @date 2023/1/10 7:34 * @param arr 请添加参数描述 * @return null **/ public MaxHeap(E[] arr){ data = new Array<>(arr); for(int i = getParent(arr.length-1);i>=0;i--){ siftDown(i); } } /** * 获取最大堆的元素个数 * @author weidoudou * @date 2023/1/3 12:40 * @return int **/ public int size(){ return data.getSize(); } /** * 获取最大堆是否为空 * @author weidoudou * @date 2023/1/3 12:41 * @return boolean **/ public boolean isEmpty(){ return data.isEmpty(); } /** * 最大堆新增元素 * 1 先加入到最大二叉堆实现的 队列中 * 2 把新加入的元素和父元素对比,若大于父元素,则和父元素交换位置,以此为循环 * @author weidoudou * @date 2023/1/3 12:42 * @param e 请添加参数描述 * @return void **/ public void shiftup(E e){ //1 先加入到最大二叉堆实现的 队列中 data.addLast(e); if(data.getSize()==1){ return; } //2 把新加入的元素和父元素对比,若大于父元素,则和父元素交换位置,以此为循环 int k = data.getSize()-1; loop(k,getParent(k)); } /** * 递归调用 * @author weidoudou * @date 2023/1/4 8:09 * @param k 请添加参数描述 * @param j 请添加参数描述 * @return void **/ private void loop(int k,int j){ //终止条件 //由于j是父节点,索引总是比较小,如果小于等于0,说明已经是根节点 if(j<0||k<=0||k>data.getSize()-1){ return; } //最终循环的位置是该元素小于父元素 if(data.get(k).compareTo(data.get(j))<=0){ return; } //子元素和父元素交换位置 data.swap(k,j); //循环 loop(j,(j-1)/2); } /** * 基本方法获取父节点 * @author weidoudou * @date 2023/1/4 7:54 * @param child 请添加参数描述 * @return int **/ private int getParent(int child){ if(child==0){ throw new IllegalArgumentException("当前节点为根节点"); } return (child - 1) / 2; } /** * 基本方法获取左子节点 * @author weidoudou * @date 2023/1/4 7:56 * @param parent 请添加参数描述 * @return int **/ private int getLeftChild(int parent){ return 2 * parent + 1; } /** * 基本方法获取右子节点 * @author weidoudou * @date 2023/1/4 7:56 * @param parent 请添加参数描述 * @return int **/ private int getRightChild(int parent){ return 2 * parent + 2; } /** * 最大堆元素的下沉(删除堆顶元素) * @author weidoudou * @date 2023/1/5 7:59 * @return void **/ public E remove(){ //1 校验 E temp = findMax(); //2 删除元素(堆顶和堆的最小值进行交换,删除最大值后,递归堆顶的最小元素和左右子节点比较) //2.1 特殊化处理,如果只有一个元素 int size = data.getSize(); //2.2 多个元素 //2.2.1 首尾交换 data.swap(0,size-1); //2.2.2 删除堆的最大元素 data.removLast(); //2.2.3 递归调用比较堆顶和左右子节点 siftDown(0); return temp; } /** * 递归调用比较堆顶和左右子节点 * @author weidoudou * @date 2023/1/5 8:15 * @param i 请添加参数描述 * @return void **/ private void siftDown(int i){ int j = getLeftChild(i);//左子节点索引 int k = getRightChild(i);//右子节点索引 //1 终止条件 //1.1 无左子节点 if(j>data.getSize()-1){ return; } //1.2 左子节点一定有,若左子节点大于根节点,则比较右子节点和左子节点,否则,比较右子节点和根节点 if(data.get(j).compareTo(data.get(i))>0){//无右子节点或者左子节点比右子节点要大,则交换左子节点和父节点 if(k>data.getSize()-1||data.get(j).compareTo(data.get(k))>0){ data.swap(i,j); siftDown(j); }else{//左右节点都有并且右子节点大于左子节点 data.swap(i,k); siftDown(k); } }else{//右节点存在并且父节点小于右节点,更换位置,否则不更换 if(k<=data.getSize()-1&&data.get(i).compareTo(data.get(k))<0){ data.swap(i,k); siftDown(k); } } } /** * 取出堆顶元素 * @author weidoudou * @date 2023/1/10 7:10 * @param * @return E **/ public E findMax(){ if(isEmpty()){ throw new IllegalArgumentException("堆为空"); } return data.get(0); } /** * 替换元素:把堆顶最大的元素取出返回,堆顶放入传过来的元素,然后进行下沉。O(logN) * 这样比先删,在加2O(logN)复杂度小了一倍 * @author weidoudou * @date 2023/1/10 7:07 * @param e 请添加参数描述 * @return E **/ public E replace(E e){ E temp = findMax(); data.set(0,e); siftDown(0); return temp; } } public class Array<E> { private int size; //int类型的数组 private E[] data; //1.1 创建构造函数,传入容量,则新生成一个数组 public Array(int capacity){ data = (E[]) new Object[capacity]; size = 0; } //1.2 创建无参构造函数 public Array(){ this(10); } public Array(E[] arr){ data = (E[]) new Object[arr.length]; for(int i = 0;i<arr.length;i++){ data[i] = arr[i]; } size = arr.length; } //2.1 添加getSize,获取数组元素个数 public int getSize(){ return size; } //2.2 添加getCapacity,获取数组容量 public int getCapacity(){ return data.length; } //2.3 添加数组是否为空方法 public boolean isEmpty(){ return size==0; } //3.1 在数组末尾添加元素 public void addLast(E e){ addElement(size,e); } //3.2 在数组起始添加元素 public void addFirst(E e){ addElement(0,e); } //3.3 数组根据索引添加元素 public void addElement(int index,E e){ //1 校验异常 //1.1 如果数组已经满了,则禁止插入 if(size== data.length){ //todo 并不会,需要把值一条一条的赋进来 resize(2*size); //throw new IllegalArgumentException("数组已满,禁止插入"); } //1.2 如果传入的索引在已有数组的索引之外,则校验异常 if(index<0||index>size){ throw new IllegalArgumentException("索引应在已有数组的索引之间"); } //2 实现根据索引添加元素的逻辑 //2.1 data同步 for(int j = size-1;j>=index;j--){ data[j+1] = data[j]; } data[index] = e; //2.2 size同步 size++; } //6.1 数组动态伸缩 这里用size更好,想想为什么 private void resize(int capacity){ E[] newData = (E[]) new Object[capacity]; for(int i = 0;i < size;i++){ newData[i] = data[i]; } data = newData; } //4.1 数组 toString 范例 @Override public String toString() { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(String.format("Array:size = %d,capacity = %d\n",size,data.length)); stringBuffer.append("["); for(int i=0;i<size;i++){ stringBuffer.append(data[i]); if(i!=size-1){ stringBuffer.append(","); } } stringBuffer.append("]"); return stringBuffer.toString(); } //4.2 get获取元素 public E get(int index){ if(index<0||index>data.length){ throw new IllegalArgumentException("111"); } return data[index]; } //4.3 set获取元素 public void set(int index,E e){ if(index<0||index>data.length){ throw new IllegalArgumentException("111"); } data[index] = e; } //5.1 数组包含 public boolean contails(E e){ for(int i = 0;i<size;i++){ if(e.equals(data[i])){ return true; } } return false; } //5.2 数组搜索 public int search(E e){ for(int i = 0;i<size;i++){ if(e.equals(data[i])){ return i; } } return -1; } //5.3 数组删除,通常情况下做删除,会在出参把删除的值带出来 public E remove(int index){ if(index<0||index>=size){ throw new IllegalArgumentException("111"); } E outParm = data[index]; for(int i=index;i<size-1;i++){ data[i] = data[i+1]; } //这块不塞值也没有任何影响,因为size已经--了,不会访问到size之外的元素 data[size-1]= null; size--; if(size == data.length/2){ resize(data.length/2); } return outParm; } //5.4 删除首个元素 public E removFirst(){ return remove(0); } //5.5 删除最后的元素 public E removLast(){ return remove(size-1); } //5.6 删除指定的元素 public void removElement(E e){ int index = -1; //判断删除的元素是否存在 for(int i=0;i<size;i++){ if(e.equals(data[i])){ index = i; break; } } if(index>=0){ remove(index); }else{ throw new IllegalArgumentException("删除的元素未找到"); } } /** * 交换元素位置 * @author weidoudou * @date 2023/1/4 8:19 * @param k 请添加参数描述 * @param j 请添加参数描述 * @return void **/ public void swap(int k,int j){ if(k<0||j<0||k>=size||j>=size){ throw new IllegalArgumentException("索引不正确"); } E temp = data[k]; data[k] = data[j]; data[j] = temp; } } public int[] topKFrequent(int[] nums, int k) { //1 定义内部类,用于实现比较方法 //2 使用自己的优先队列实现这块功能 //2.1 使用映射,频率放入value Map<Integer,Integer> hashMap3 = new HashMap<Integer,Integer>(); for(int i=0;i<nums.length;i++){ if(hashMap3.containsKey(nums[i])){ hashMap3.put(nums[i],hashMap3.get(nums[i])+1); }else{ hashMap3.put(nums[i],1 ); } } //2.2 hashMap循环,取出前k个值放入堆中,大于k的部分和堆顶元素比较 PriorityQueue<MapInnerClass> priorityQueue = new PriorityQueue<>(); //warning,这里不能用for循环 int i = 0,因为映射中的key值无法确定 for(int key: hashMap3.keySet()){ if(priorityQueue.getSize()<k){ priorityQueue.enqueue(new MapInnerClass(key,hashMap3.get(key))); }else{ MapInnerClass temp = new MapInnerClass(key,hashMap3.get(key)); //如果 堆首的元素 比 外部的元素小,那么进行出堆入堆 if(priorityQueue.getFront().compareTo(temp)>0){ priorityQueue.dequeue(); priorityQueue.enqueue(temp); } } } int [] result = new int[k]; for(int i = 0;i<k;i++){ result[i] = priorityQueue.dequeue().key; } return result; } }
- 解题结果:
诸葛