共计 8894 个字符,预计需要花费 23 分钟才能阅读完成。
[TOC]
题目描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用 Insert()方法读取数据流,使用 GetMedian()方法获取当前读取数据的中位数。
public class Solution {public void Insert(Integer num) { } | |
public Double GetMedian() {} | |
} |
思路一:使用 partition 函数找出无序数组的中位数
从数据流中读出数据,首先需要一个容器来保存,如果采用数组作为容器,并且保存的时候直接放在数组末尾,不排序,读完之后,再通过 partition 函数从一个无序数组中寻找第 n / 2 大的数(即中位数),这里就可以借鉴“数组中出现次数超过一半的数字”那道题了
import java.util.ArrayList; | |
import static java.util.Collections.swap; | |
public class Solution {ArrayList<Integer> list = new ArrayList<>(); | |
public void Insert(Integer num) {list.add(num); | |
} | |
public int partition(ArrayList<Integer> list,int low,int high){int base = list.get(high); | |
int i = low - 1; | |
// 注意这里 for 循环 j 的起始位置为 low | |
for(int j = low; j < high; j++){if(list.get(j) <= base){ | |
i++; | |
swap(list,i,j); | |
} | |
} | |
swap(list,i+1,high); | |
return i+1; | |
} | |
public Double GetMedian() { | |
double result = 0; | |
int index = partition(list, 0, list.size()-1); | |
int middle = 0; | |
if((list.size() % 2) == 0) {middle = list.size() >> 1; | |
}else {middle = (int) Math.floor(list.size() >> 1); | |
} | |
while (index != middle){if(index > middle){index = partition(list, 0, index - 1); | |
}else {index = partition(list, index + 1, list.size()-1); | |
} | |
} | |
if(list.size() % 2 == 0 && index != 0 ){result = (list.get(index) + list.get(index - 1)) / 2.0; | |
}else {result = list.get(index); | |
} | |
return result; | |
} | |
} |
思路二:有序的插入到数组
在向数组中插入新的数据时让数组保持有序(可以采用插入排序)每插入一个数,可能需要移动 o(n)个数,对于插入 n 个数,时间复杂度为 o(n^2)
然后在排好序的数组中找出中位数,时间复杂度为 o(1)
思路三:有序的插入到链表
用链表作为保存数据流中的数据的容器,同样的,每插入一个数,需要 o(n)的时间复杂度才能找到合适的插入位置,使用链表和使用数组的时间复杂度一样
思路四:使用平衡二叉树 AVL 树保存数据
插入到 AVL 树中的时间复杂度为 o(logn),获得中位数的时间复杂度为 o(1)
思路五:使用最大堆和最小堆分别求出两部分的最大值和最小值
具体思路
在堆中插入一个数据的时间效率为 o(logn),得到堆顶的数据的时间复杂度为 o(1)
- 数据平均分配到两个堆中,两个堆中的数目只差不超过 1
- 保证最大堆的数据都小于最小堆的数据
依次将数据流中的数据依次放入最大堆和最小堆,当将要放入最小堆的数据小于最大堆的某些数,那么先将这个数插入最大堆中,然后取出最大堆中的最大值放入最小堆中。同理,如果将要放入最大堆的数据大于最小堆的某些数,那么就先将这个数放入最小堆中,然后取出最小堆的最小值放入最大堆
回顾堆的性质
在使用堆之前我们先来回顾一下堆的性质
1、什么是堆:堆其实就是一颗完全二叉树,除了最底层,其他层的节点都被元素填满,并且最底层尽可能从左到右填入
2、堆有哪些性质:堆一般用一维数组实现,并且该一维数组是堆的层次遍历对应的序列
1)堆的子节点和父节点之间的位置关系:对于节点 i, 它的的左右子节点在一维数组中的索引为:2i+ 1 和 2i+2
它的父节点的索引为 floor((i-1)/2)
2)堆的深度:同二叉树的深度一样为 θ(logn)
3)堆的叶节点的下标分别为:floor(n/2),floor(n/2)+1,floor(n/2)+2,floor(n/2)+3,floor(n/2)+4…… n-1
接下来就是回顾堆排序了,堆排序是在简单选择排序的基础上演化而来的
回顾简单选择排序
所以我们先回顾一下简单选择排序
- 简单选择排序的思想:每次从剩下的数中挑选一个最小的数放在已经排好序的末尾,这个最小的数需要找到它的下标,然后和 arr[i]进行交换
简单选择排序的实现
public void simpleSort(int[] arr){for(int i = 0; i < arr.length - 1; i++){ | |
int minIndex = i; | |
for(int j = i+1; j < arr.length; j++){if(arr[j] < arr[minIndex]){minIndex = j;} | |
} | |
swap(arr, i, minIndex); | |
} | |
} | |
public void swap(int[] arr,int i,int j){ | |
int temp = 0; | |
temp = arr[i]; | |
arr[i] = arr[j]; | |
arr[j] = temp; | |
} |
从代码中我们可以看出,在每一轮比较中(从第一个 for 循环看出一共需要比较 arr.length- 1 轮)通过两两比较寻找剩下的序列中最小的数的下标,然后将 arr[i]与 arr[minIndex]交换,然后下一轮又从 i + 1 开始继续寻找剩下的序列中最小数的下标,这个过程中有的数实际上互相比较了很多次,所以这是一个非常损耗性能的事情
堆排序如何实现
假如给了我们一个数组 arr={4,8,1,0,3,5}, 我们如何通过堆排序来对该数组进行排序呢(由大到小)
步骤如下:
1)将该数组构建成一个最大堆
2)取出最大堆的根节点,维护新堆的最大堆性质,构建一个新的最大堆(堆的大小减一)
3)重复步骤 2,直到取完堆中的所有节点
最后总结一下堆排序的伪代码
大顶堆排序的具体实现 ( 内部由大到小 ): 最后将数组顺序排列
import org.omg.CORBA.RepositoryIdHelper; | |
import java.util.Arrays; | |
public class HeapSort {public static void main(String[] args){HeapSort heapSort = new HeapSort(); | |
int[] arr = {5, 2, 7, 1, 9, 3}; | |
//[1, 2, 3, 5, 7, 9] | |
heapSort.heapSort(arr); | |
System.out.println(Arrays.toString(arr)); | |
} | |
private int heapSize = 0; | |
public void heapSort(int[] arr){ | |
// 构建一个最大堆 | |
buildMaxHeap(arr); | |
for(int i = arr.length-1; i > 0; i--){swap(arr,i,0); | |
heapSize--; | |
maxHeapify(arr, 0); | |
} | |
} | |
public void buildMaxHeap(int[] arr){ | |
heapSize = arr.length; | |
// 检查除了叶子节点以外的其他节点是否满足最大堆的性质 | |
for(int i = (arr.length >> 1) - 1; i >= 0; i--){maxHeapify(arr, i); | |
} | |
} | |
public void maxHeapify(int[] arr,int i){ | |
int leftIndex = 2 * i + 1; | |
int rightIndex = 2 * i + 2; | |
int largeIndex = i; | |
if(leftIndex <heapSize && arr[leftIndex] > arr[i]){largeIndex = leftIndex;} | |
if(rightIndex < heapSize && arr[rightIndex] > arr[largeIndex]){largeIndex = rightIndex;} | |
if(largeIndex != i){swap(arr, largeIndex, i); | |
maxHeapify(arr,largeIndex); | |
} | |
} | |
public void swap(int[] arr,int i,int j){ | |
int temp = 0; | |
temp = arr[i]; | |
arr[i] = arr[j]; | |
arr[j] = temp; | |
} | |
} |
小顶堆的实现 (* 内部实现 由小到大*):最后将数组降序排列
import sun.net.www.HeaderParser; | |
import java.util.Arrays; | |
public class SmallHeap {public static void main(String[] args){SmallHeap smallHeap = new SmallHeap(); | |
int[] arr = {5, 2, 7, 1, 9, 3}; | |
///[9, 7, 5, 3, 2, 1] | |
smallHeap.smallHeapSort(arr); | |
System.out.println(Arrays.toString(arr)); | |
} | |
int heapSize = 0; | |
public void smallHeapSort(int[] arr){buildSmallHeap(arr); | |
for(int i = arr.length - 1; i > 0; i--){swap(arr, i, 0); | |
heapSize--; | |
minHeapify(arr, 0); | |
} | |
} | |
public void swap(int[] arr,int i,int j){int temp = arr[i]; | |
arr[i] = arr[j]; | |
arr[j] = temp; | |
} | |
public void buildSmallHeap(int[] arr){ | |
heapSize = arr.length; | |
for(int i =( arr.length >> 1) - 1; i >= 0 ; i--){minHeapify(arr, i); | |
} | |
} | |
public void minHeapify(int[] arr,int i){ | |
int leftIndex = 2 * i + 1; | |
int rightIndex = 2 * i + 2; | |
int minIndex = i; | |
if (leftIndex < heapSize && arr[leftIndex] < arr[i]) {minIndex = leftIndex;} | |
if (rightIndex < heapSize && arr[rightIndex] < arr[minIndex]) {minIndex = rightIndex;} | |
if (minIndex != i) {swap(arr,i,minIndex); | |
minHeapify(arr,minIndex); | |
} | |
} | |
} |
堆的应用——优先队列
最大优先队列(最大堆)
1)insert(S,x): 把 x 插入集合 S 中
2)maximum(S): 返回 S 的最大值
3)extractMax(S):remove 并且返回 S 的最大值
4)increaseKey(S,i,k): 将 S[i]增加到 K,
应用
作业调度,选出最高优先级的作业执行,随时 insert 一个新的作业到队列
最小优先队列(最小堆)
1)insert(S,x): 把 x 插入集合 S 中
2)minimum(S): 返回 S 的最小值
3)extractMin(S):remove 并且返回 S 的最小值
4)decreaseKey(S,x,k): 将 S[i]减小到 K,
最大优先队列和最小优先队列的实现
本题的实现:通过最大堆和最小堆分别构建最大优先队列和最小优先队列
import java.util.ArrayList; | |
import static java.util.Collections.swap; | |
public class Solution { | |
class MaxHeap{ArrayList<Integer> maxHeapList = new ArrayList<Integer>(); | |
private int maxHeapSize ; | |
public MaxHeap() {this.maxHeapSize = maxHeapList.size(); | |
} | |
} | |
class MinHeap{ArrayList<Integer> minHeapList = new ArrayList<Integer>(); | |
private int minHeapSize ; | |
public MinHeap() {this.minHeapSize = minHeapList.size(); | |
} | |
} | |
private int count = 0; | |
public void Insert(Integer num,MaxHeap maxHeap,MinHeap minHeap){if(count == 0){maxHeap.maxHeapList.add(0,num); | |
maxHeap.maxHeapSize++; | |
}else {if(count % 2 == 0){if(num > minHeap.minHeapList.get(0)){int temp = extractiMin(minHeap); | |
insertMinHeap(minHeap, num); | |
inserMaxHeap(maxHeap, temp); | |
}else {inserMaxHeap(maxHeap,num); | |
} | |
}else {if(num < maxHeap.maxHeapList.get(0)){int temp = extractMax(maxHeap); | |
inserMaxHeap(maxHeap,num); | |
insertMinHeap(minHeap,temp); | |
}else {insertMinHeap(minHeap,num); | |
} | |
} | |
} | |
count++; | |
} | |
MaxHeap maxHeap = new MaxHeap(); | |
MinHeap minHeap = new MinHeap(); | |
public void Insert(Integer num) {Insert(num,maxHeap,minHeap); | |
} | |
public int extractiMin(MinHeap minHeap){if(minHeap.minHeapSize < 0) return 0; | |
int min = minHeap.minHeapList.get(0); | |
minHeap.minHeapList.remove(0); | |
// 注意处理 heapsize 的大小 | |
minHeap.minHeapSize--; | |
minHeapify(minHeap, 0); | |
return min; | |
} | |
// 这里维护最小堆的性质和最大堆不同,比较的是和父节点的大小,因为这里不断的插入新的节点到末尾,应该是和前面的父节点比较 | |
public void minHeapify(MinHeap minHeap,int i){ | |
int minIndex = i; | |
int parentIndex = (int) Math.floor((i - 1) / 2.0); | |
if(parentIndex >= 0 && minHeap.minHeapList.get(i) < minHeap.minHeapList.get(parentIndex) ){ | |
minIndex = parentIndex; | |
swap(minHeap.minHeapList, i, minIndex); | |
minHeapify(minHeap,minIndex); | |
} | |
} | |
public void insertMinHeap(MinHeap minHeap,int num){ | |
minHeap.minHeapSize++; | |
minHeap.minHeapList.add(minHeap.minHeapSize - 1,Integer.MAX_VALUE); | |
decreaseKey(minHeap.minHeapList, minHeap.minHeapSize - 1, num); | |
} | |
public void decreaseKey(ArrayList<Integer> list,int i,int key){if(list.get(i) < key)return; | |
list.remove(i); | |
list.add(i, key); | |
minHeapify(minHeap,i); | |
} | |
public void inserMaxHeap(MaxHeap maxHeap,int num){ | |
maxHeap.maxHeapSize++; | |
if(maxHeap.maxHeapSize > 0) {maxHeap.maxHeapList.add(maxHeap.maxHeapSize - 1, Integer.MIN_VALUE); | |
}else {maxHeap.maxHeapList.add(0,Integer.MAX_VALUE); | |
} | |
increaseKey(maxHeap, maxHeap.maxHeapSize - 1, num); | |
} | |
public void increaseKey(MaxHeap maxHeap,int i,int key){if(maxHeap.maxHeapList.get(i) > key)return; | |
maxHeap.maxHeapList.remove(i); | |
maxHeap.maxHeapList.add(i, key); | |
while (i > 0 && maxHeap.maxHeapList.get((int) Math.floor((i-1)/2.0)) < maxHeap.maxHeapList.get(i)){swap(maxHeap.maxHeapList,i,(int) Math.floor((i-1)/2.0)); | |
i = (int) Math.floor((i - 1) / 2.0); | |
} | |
} | |
public int extractMax(MaxHeap maxHeap){if(maxHeap.maxHeapSize < 0)return 0; | |
int max = maxHeap.maxHeapList.get(0); | |
maxHeap.maxHeapList.remove(0); | |
maxHeap.maxHeapSize--; | |
maxHeapify(maxHeap, 0); | |
return max; | |
} | |
public void maxHeapify(MaxHeap maxHeap,int i){ | |
int leftIndex = 2 * i +1; | |
int rightIndex = 2 * i +2; | |
int largeIndex = i; | |
if(leftIndex < maxHeap.maxHeapSize && maxHeap.maxHeapList.get(leftIndex) > maxHeap.maxHeapList.get(i)){largeIndex = leftIndex;} | |
if(rightIndex < maxHeap.maxHeapSize && maxHeap.maxHeapList.get(rightIndex) > maxHeap.maxHeapList.get(largeIndex)){largeIndex = rightIndex;} | |
if(largeIndex != i){swap(maxHeap.maxHeapList,i,largeIndex); | |
maxHeapify(maxHeap,largeIndex); | |
} | |
} | |
public Double GetMedian() {if(count % 2 == 0){return (maxHeap.maxHeapList.get(0) + minHeap.minHeapList.get(0)) / 2.0; | |
}else {return (double)maxHeap.maxHeapList.get(0); | |
} | |
} | |
} |
该题的实现:基于现有的类库:PriorityQueue 实现最大堆和最小堆
import java.util.Comparator; | |
import java.util.PriorityQueue; | |
public class Solution { | |
private int count = 0; | |
// 默认升序排列,也就是默认为最小堆 | |
private PriorityQueue<Integer> minHeap = new PriorityQueue<>(); | |
// 通过重写 Comparator 接口的 compare 方法,构建一个逆序排列的最大堆 | |
private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new Comparator<Integer>() { | |
@Override | |
public int compare(Integer o1, Integer o2) {return o2 - o1;} | |
}); | |
public void Insert(Integer num) {if (count %2 == 0) { | |
// 最大堆插入一个元素之后,内部会自动重新构建一个最大堆 maxheapify | |
maxHeap.offer(num); | |
//remove 并返回第一个元素 | |
int temp = maxHeap.poll(); | |
// 插入到最小堆,内部同样进行 minheapify | |
minHeap.offer(temp); | |
} else {minHeap.offer(num); | |
int temp = minHeap.poll(); | |
maxHeap.offer(temp); | |
} | |
count++; | |
} | |
public Double GetMedian() {if (count %2 == 0) {return new Double((minHeap.peek() + maxHeap.peek())) / 2; | |
} else {return new Double(minHeap.peek()); | |
} | |
} | |
} |
参考
《剑指 offer 纪念版》
《算法导论第三版》