项目地址:https://github.com/SpecialYy/Sword-Means-Offer
问题
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
解析
这道题有2个要求:
- 能够存放输入的每一个数
- 能够时刻从当前已存的书中获取中间值,奇数时返回最中间那一个就行;偶数时位于中间有2个数,所以返回两者的平均值即可。
对于第一个要求,必然不能使用固定大小的容器,因为插入的操作次数是不确定的。这里我们可以使用自带扩容的数组或者无界的链表,当然我们还可以使用各个编程语言中已经实现的容器。第二要求才是本题的关键,如何快速获得中间值其实依赖于第一步我们选择的容器。
思路一
我们实现一个自动扩容的数组,在添加元素时若发当前数组已满,则会新建一个大小为当前数组长度为2倍的数组,然后把原数组的内容copy到新的数组上,最后把待插入的元素放到新的数组即可。
那么我们如何获得中间值呢?这里有2种做法:
- 利用快排的思想来获取第K大的值,选择一个pivot,通过partation来将数组分为两部分,使得左边的数都比pivot小,右边的数都比pivot大。然后查看pivot所处的位置是否是K,若是K,则pivot就是第K大数。若小于K,则说明第K大在右边;若大于K,则说明第K大在左边;递归处理即可,直到pivot的索引为K为止。
- 我们在插入的时候就保持数组有序,这样就可以根据数据随机访问的特性获得中间的值。插入排序的思路可以使得插入数据保持数组有序。
// 方法一: 自动扩容数组法
class AdaptiveArray {
int size = 0;
int[] value;
public AdaptiveArray() {
value = new int[10];
}
public AdaptiveArray(int capacity) {
value = new int[capacity];
}
public void addElementByOrder(int item) {
if (size == value.length) {
Expansion(size);
}
value[size++] = item;
for (int i = size - 1; i > 0 && value[i] < value[i - 1]; i--) {
int temp = value[i];
value[i] = value[i - 1];
value[i - 1] = temp;
}
}
public double getMiddleNumber() {
if (size == 0) {
return 0;
}
if((size & 1) == 1) {
return value[size / 2];
} else {
int mid = size / 2;
return ((double) value[mid] + value[mid - 1]) / 2;
}
}
public void Expansion(int length) {
int[] newValue = new int[length << 1];
System.arraycopy(value, 0, newValue, 0, length);
value = newValue;
}
}
AdaptiveArray adaptiveArray = new AdaptiveArray();
public void Insert(Integer num) {
adaptiveArray.addElementByOrder(num);
}
public Double GetMedian() {
return adaptiveArray.getMiddleNumber();
}
思路二
数组扩容的过程会涉及到复制操作,不仅浪费时间,而且还浪费空间。只有内存足够大,才能保证能够再申请额外的空间用于拷贝原数组。而链表是通过指针连接起来的,它不要求占用的内存是连续的,所以可以有效的解决内存碎片化问题。添加一个节点只需更改节点之间的指向关系即可,快速便捷。
我们这里还是以插入的时候保持有序来方便我们获取中间节点。注意在节点总数为偶数时,我们获得中间两个节点的第一个即可,然后根据指针即可获得中间节点的第二个节点。
获得中间节点的思路如下,利用2个快慢指针,慢指针一次走一步,快指针一次走2步。然后令快指针始终指向偶数位置,这样的话,当总节点数为奇数时,快指针走到空指针,慢指针正好指向最中间的节点。当总节点数为偶数时,快指针走到末尾时,慢指针正好指向两个中间节点的前一个。然后我们根据next指针即可获得两个中间节点的后一个。
// 方法二:链表法
class ListNode {
int value;
ListNode next;
public ListNode(int value) {
this.value = value;
}
}
ListNode head = new ListNode(0);
ListNode midLeft = null, midRight = null;
int size = 0;
public void Insert1(Integer num) {
ListNode listNode = new ListNode(num);
ListNode p = head;
while (p.next != null && p.next.value < num) {
p = p.next;
}
listNode.next = p.next;
p.next = listNode;
midLeft = findMiddleListNode(head.next);
size++;
if ((size & 1) == 1) {
midRight = midLeft;
} else {
midRight = midLeft.next;
}
}
public Double GetMedian1() {
return ((double) midLeft.value + midRight.value) / 2;
}
/**
* 寻找链表的中间节点
* @param node
* @return
*/
public ListNode findMiddleListNode(ListNode node) {
if (node == null || node.next == null) {
return node;
}
ListNode first = node;
ListNode second = node.next;
while(second != null && second.next != null) {
first = first.next;
second = second.next.next;
}
return first;
}
思路三
我们的目标是获得中间节点,那么根据中间节点可以把数组划分为2部分。中间节点正好是左边部分的最大值和右边部分的最小值。**所以我们只要能够快速获得左边部分最大值和右边部分的最小值即可。**这属于Top-K问题,所以首选堆排序,堆排序对应的一个很好的应用就是优先队列。该数据结构的实现在C++和Java中都有很好的实现。
申请一个最大堆和一个最小堆。我们规定当节点数为奇数时,中间节点位于最大堆中,表明左部分比右部分多一个节点。当节点数为偶数时,2个中间节点分别位于最大堆和最小堆中,表明左右部分节点数一样。
- 当前节点数为偶数个,所以需要在左部分新添加一个节点。这个节点还必须比右部分都小,所以我们可以把待插入的节点先插入最小堆,然后把最小堆的头部弹出并加入最大堆。
- 当前节点数为奇数个,所以需要在右部分添加一个节点。这个节点还必须比左部分都大,所以我们可以把待插入的节点先插入最大堆,然后把最大堆的头部弹出并加入到最小堆中。
//方法3:最大堆和最小堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
int size = 0;
/**
* 这里还是规定了当节点数为奇数时,中间节点归在最大堆里
* 运用了巧妙的方法:
* 1.当节点数为奇数,表明要在最小堆里增加一个节点
* 但是我们不能直接插入最小堆,先插入最大堆,然后弹出插入最小堆
* 2.当节点数为偶数,表明要在最大堆里增加一个节点
* 类似情况1,先插最小堆,然后再插最大堆
* @param num
*/
public void Insert3(Integer num) {
if ((size & 1) == 0) {
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
} else {
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
}
size++;
}
public Double GetMedian3() {
return maxHeap.size() == minHeap.size() ?
((double) maxHeap.peek() + minHeap.peek()) / 2 : (double) maxHeap.peek();
}
总结
多联想一些常用的数据结构来辅助解题。