使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中。
一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。这种方式的主要用法就是堆的表示。
parent
)的下标,则:(left)
下标 = 2 * parent + 1
;(right)
下标 = 2 * parent + 2
;child
)下标,则:parent
)下标 = (child - 1) / 2
;前提:左右子树必须已经是一个堆,才能调整。
建堆是自底向上的建堆方式。
以大根堆为例,首先得创建一个大根堆:
public class TestHeap { public int[] elem; public int usedSize; public TestHeap(){ this.elem = new int[10]; } /** * 创建大根堆 * @param array */ public void creatHeap(int[] array){ for (int i = 0; i < array.length; i++) { this.elem[i] = array[i]; usedSize++; } //parent代表每颗子树的根节点 ,(array.length - 1)最后一个节点的下标,(array.length - 1 - 1) / 2最后一个节点的根节点 for(int parent= (array.length - 1 - 1) / 2; parent >= 0; parent--) //第二个参数每次调整的结束位置是不确定的 adjustDown(parent,this.usedSize); }
测试代码:
public static void main(String[] args) { int[] arr = {1,5,3,8,7,6}; TestHeap testHeap = new TestHeap(); testHeap.creatHeap(arr); //在此处打断点调试 System.out.println("dvsfb"); }
输出结果:
// 建堆前 int[] array = { 1,5,3,8,7,6 }; // 建堆后 int[] array = { 8,7,6,5,1,3 };
时间复杂度详解
自底向上的建堆方式,即 Floyd
建堆算法。因为方向相反、自顶向下的建堆方式的时间复杂度为 O(n·logn)
。
假设目标堆是一个满堆,即第 k
层节点数为 2ᵏ
。输入数组规模为 n
, 堆的高度为 h
, 那么 n
与 h
之间满足 n = 2ʰ⁺¹ - 1
,可化为 h = log₂(n+1) - 1
。 (层数 k
和高度 h
均从 0
开始,即只有根节点的堆高度为0
,空堆高度为 -1
)。建堆过程中每个节点需要一次下滤操作,交换的次数等于该节点到叶节点的深度。那么每一层中所有节点的交换次数为节点个数乘以叶节点到该节点的深度(如第一层的交换次数为 2⁰ · h,第二层的交换次数为 2¹ · (h-1),如此类推)。从堆顶到最后一层的交换次数 Sn 进行求和:
Sn = 2⁰ · h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹ · 1 + 2ʰ · 0
;记为①;
①经化简为: Sn = h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹
;
对①等于号左右两边乘以2,记为②式:
②: 2Sn = 2¹ · h + 2² · (h - 1) + 2³ · (h - 2) + ...... + 2ʰ⁻¹ · 2 + 2ʰ
;
用②式减去①式,其中②式的操作数右移一位使指数相同的部分对齐(即错位相减法):
化简可得③式:
③ : Sn = -h + 2¹ + 2² + 2³ + ...... + 2ʰ⁻¹ + 2ʰ
;
对指数部分使用等比数列求和公式:
得:
Sn =2ʰ⁺¹ - (h + 2)
在上述过程中,已经达到n
和h
的关系为: h = log₂(n+1) - 1
,将其代入Sn
中得:Sn =(n+1)(log2(n+1)-1+2)
化简后为:Sn = n - log₂(n + 1)
而对于对数函数,当n趋近于一定值时,其结果相对于x轴趋于平缓,并且变化幅度不大,因此最终可得渐进复杂度为 O(n)
。
parent
如果已经是叶子结点,则整个调整过程结束。parent
,则其左孩子节点为:child = 2 * parent+ 1
;child+ 1< len
表明未越界,如果有右孩子并且左孩子比右孩子小,则child++
;即右孩子为child
;parent
和 child
,孩子和父亲的节点大小,如果孩子节点大,则进行交换;parent
位置的堆的性质可能被破坏,所以把 child
视作 parent
,在parent
的基础上重新定义child
,向下重复以上过程。代码示例:
public void adjustDown(int root,int len){ int parent = root; int child = 2 * parent+ 1;//左孩子 while(child < len){ //找到左右孩子的最大值,前提是的有右孩子 //如果有右孩子并且左孩子比右孩子小,则child++ if(child + 1 < len && elem[child] < elem[child + 1]){ child++; } //判断孩子和父亲的节点大小,如果孩子节点大,则进行交换 if(elem[child] > elem[parent]){ int tmp = elem[child]; elem[child] = elem[parent]; elem[parent] = tmp; parent = child; child = 2* parent +1; }else{ //当调整过程中,已经没有可以继续再进行调整的节点了,直接break结束 break; } } }
数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue
)。利用堆来构建优先级队列。
入队列过程(以大堆为例):
代码示例:
/** * 入堆 向上调整 * @param child */ public void adjustUp(int child){ int parent = (child - 1) / 2; while (child > 0){ if(this.elem[child] > this.elem[parent]){ int tmp = this.elem[child]; this.elem[child] = this.elem[parent]; this.elem[parent] = tmp; child = parent; parent = (child - 1) / 2; }else { break; } } } public void push(int val){ if(isFull()){ this.elem = Arrays.copyOf(this.elem,this.elem.length * 2); } this.elem[this.usedSize] = val;//10 usedSize++;//11 adjustUp(this.usedSize - 1);//10 } //判断是否满了 public boolean isFull(){ return this.usedSize == this.elem.length; }
为了防止破坏堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整方式重新调整成堆。
public void pop(){ if(isEmpty()){ return; } //让0下标的元素和最后一个元素进行交换 int tmp = this.elem[0]; this.elem[0] = this.elem[usedSize - 1]; this.elem[usedSize - 1] = tmp; usedSize--; //向下调整 adjustDown(0,usedSize); } //判断是否为空 public boolean isEmpty(){ return this.usedSize == 0; }
返回堆顶元素即可。
//返回队首元素 public int peek(){ if(isEmpty()){ return -1; } //得到队头元素 return this.elem[0]; }
3.4 堆排序
//堆排序 //先创建大堆,调整每棵树 //开始堆排序,先交换,再调整,直到end为0 public void heapSort(){ int end = this.usedSize - 1; while (end > 0){ int tmp = this.elem[0]; this.elem[0] = this.elem[end]; this.elem[end] = tmp; adjustDown(0,end); end--; } }
equal只能按照相等进行比较,不能按照大于、小于的方式进行比较.
public static void main(String[] args) { Student student1 = new Student("zhang",12); Student student2 = new Student("li",89); System.out.println(student1.equals(student2));//false 重写equals方法,如果不重写则默认调用的是Object的equals方法 }
对于用户自定义的类型,如果要想按照大小的方式进行比较时:在定义类时,实现Comparble
接口即可,然后在类中重写compareTo
方法。
Compareble
是java.lang
中的接口类,可以直接使用。
class Student implements Comparable<Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override //重写compareTo方法 public int compareTo(Student o) { return this.age - o.age; } }
需覆写Comparator
中的compare
方法;
Comparator
是java.util
包中的泛型接口类,使用时必须导入对应的包。
class Student{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name, age); } } class AgeComparator implements Comparator<Student> { @Override //compare的第一个参数是插入的元素 public int compare(Student o1, Student o2) { return o1.getAge() - o2.getAge();//默认小堆 //return o2.getAge() - o1.getAge();//大堆 } } class NameComparator implements Comparator<Student>{ @Override public int compare(Student o1, Student o2) { return o1.getName().compareTo(o2.getName()); } }
测试代码:
public static void main(String[] args) { Student[] students = new Student[2]; students[0] = new Student("ang",102); students[1] = new Student("za",92); //按照年龄排序 Arrays.sort(students, new AgeComparator()); //按照名字首字母排序 // Arrays.sort(students, new NameComparator()); System.out.println(Arrays.toString(students)); }
输出结果:
覆写的方法 | 说明 |
---|---|
Object.equals | 因为所有类都是继承自 Object 的,所以直接覆写即可,不过只能比较相等与否 |
Comparable.compareTo | 需要手动实现接口,侵入性比较强,但一旦实现,每次用该类都有顺序,属于内部顺序 |
Comparator.compare | 需要实现一个比较器对象,对待比较类的侵入性弱,但对算法代码实现侵入性强 |
集合框架中的PriorityQueue
底层使用堆结构,因此其内部的元素必须要能够比大小,PriorityQueue
采用了:Comparble
和Comparator
两种方式。
Comparble
是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble
接口,并覆写compareTo
方法。Comparator
接口并覆写compare
方法。public static void main(String[] args) { //堆默认大小是10 PriorityQueue<Integer> queue = new PriorityQueue<>(); //默认是小堆排序 queue.offer(61); queue.offer(21); queue.offer(1); System.out.println(queue);//[1, 61, 21] }
从输出队列可看出其优先级队列默认为小堆排序。
结合4.3
方法 PriorityQueue
的比较结果:
public static void main(String[] args) { //按照名字首字母进行比较 // PriorityQueue<Student> priorityQueue = new PriorityQueue<>(new NameComparator()); //按照年龄比较 PriorityQueue<Student> priorityQueue = new PriorityQueue<>(new AgeComparator()); priorityQueue.offer(new Student("ang",12)); priorityQueue.offer(new Student("li",4)); System.out.println(priorityQueue); }
输出结果:
TopK问题
在一个数组中找出最大的K个或者最小的K个。
若想找出最大的k
个,先用前k
个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k
个元素。接着,从第k+1
个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k
个元素,总是当前最大的k
个元素。直到,扫描完所有n-k
个元素,最终堆中的k
个元素,就是所求的TopK
。
代码示例:
找出前三个最大的元素:
public static void TopK(int arr[],int k){ PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { // return o2 - o1;//大堆 return o1 - o2;//小堆 } }); for (int i = 0; i < arr.length; i++) { //先向队列中放入前k个元素 if (maxHeap.size() < k){ maxHeap.offer(arr[i]); }else { int top = maxHeap.peek(); // if (top > arr[i]){ //找出前三个最小的 if (top < arr[i]){ //找出前三个最大的 maxHeap.poll(); maxHeap.offer(arr[i]); } } } System.out.println(maxHeap); } public static void main(String[] args) { //找到前三个最小的数 //建立大小为3的大堆 int[] arr = {1,31,2,10,7,35,21,19,56}; TopK(arr,3); }
输出结果:
例:力扣373.查找和最小的K对数字
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) { PriorityQueue<List<Integer>> maxHeap = new PriorityQueue<>(k, new Comparator<List<Integer>>() { @Override public int compare(List<Integer> o1, List<Integer> o2) { return (o2.get(0) + o2.get(1) - (o1.get(0) + o1.get(1)));//大堆 } }); for (int i = 0; i <nums1.length; i++) { for (int j = 0; j < nums2.length; j++) { if(maxHeap.size() < k){ List<Integer> pair = new ArrayList<>(); pair.add(nums1[i]); pair.add(nums2[j]); maxHeap.offer(pair); }else { List<Integer> top = maxHeap.peek(); //如果当前元素比堆顶小,就弹出堆顶,然后将小的数据入堆 int topValue = top.get(0)+top.get(1); if (topValue > nums1[i] +nums2[j]){ maxHeap.poll(); List<Integer> pair = new ArrayList<>(); pair.add(nums1[i]); pair.add(nums2[j]); maxHeap.offer(pair); } } } } //堆当中放的就是最后的结果 List<List<Integer>> ret = new ArrayList<>(); for (int i = 0; i < k && !maxHeap.isEmpty(); i++) { ret.add(maxHeap.poll()); } return ret; }
以上。