一、题目

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有值排序之后位于中间的数值。如果数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

二、解题思路

如果能够保证数据容器左边的数据都小于右边的数据,这样即使左、右两边内部的数据没有排序,也可以根据左边最大的数及右边最小的数得到中位数。如何快速从一个容器中找出最大数?用最大堆实现这个数据容器,因为位于堆顶的就是最大的数据。同样,也可以快速从最小堆中找出最小数。  因此可以用如下思路来解决这个问题:用一个最大堆实现左边的数据容器,用最小堆实现右边的数据容器。往堆中插入一个数据的时间效率是O(logn).由于只需O(1)时间就可以得到位于堆顶的数据,因此得到中位数的时间效率是O(1).

接下来考虑用最大堆和最小堆实现的一些细节。首先要保证数据平均分配到两个堆中,因此两个堆中数据的数目之差不能超过1(为了实现平均分配,可以在数据的总数目是偶数时把新数据插入到最小堆中,否则插入到最大堆中)。

还要保证最大堆中里的所有数据都要小于最小堆中的数据。当数据的总数目是偶数时,按照前面分配的规则会把新的数据插入到最小堆中。如果此时新的数据比最大堆中的一些数据要小,怎么办呢?

可以先把新的数据插入到最大堆中,接着把最大堆中的最大的数字拿出来插入到最小堆中。由于最终插入到最小堆的数字是原最大堆中最大的数字,这样就保证了最小堆中的所有数字都大于最大堆中的数字。

当需要把一个数据插入到最大堆中,但这个数据小于最小堆里的一些数据时,这个情形和前面类似。

三、解题代码

  1. public class Test {
  2. private static class Heap<T> {
  3. // 堆中元素存放的集合
  4. private List<T> data;
  5. // 比较器
  6. private Comparator<T> cmp;
  7. /**
  8. * 构造函数
  9. *
  10. * @param cmp 比较器对象
  11. */
  12. public Heap(Comparator<T> cmp) {
  13. this.cmp = cmp;
  14. this.data = new ArrayList<>(64);
  15. }
  16. /**
  17. * 向上调整堆
  18. *
  19. * @param idx 被上移元素的起始位置
  20. */
  21. public void shiftUp(int idx) {
  22. // 检查是位置是否正确
  23. if (idx < 0 || idx >= data.size()) {
  24. throw new IllegalArgumentException(idx + "");
  25. }
  26. // 获取开始调整的元素对象
  27. T intent = data.get(idx);
  28. // 如果不是根元素,则需要上移
  29. while (idx > 0) {
  30. // 找父元素对象的位置
  31. int parentIdx = (idx - 1) / 2;
  32. // 获取父元素对象
  33. T parent = data.get(parentIdx);
  34. //上移的条件,子节点比父节点大,此处定义的大是以比较器返回值为准
  35. if (cmp.compare(intent, parent) > 0) {
  36. // 将父节点向下放
  37. data.set(idx, parent);
  38. idx = parentIdx;
  39. // 记录父节点下放的位置
  40. }
  41. // 子节点不比父节点大,说明父子路径已经按从大到小排好顺序了,不需要调整了
  42. else {
  43. break;
  44. }
  45. }
  46. // index此时记录是的最后一个被下放的父节点的位置(也可能是自身),
  47. // 所以将最开始的调整的元素值放入index位置即可
  48. data.set(idx, intent);
  49. }
  50. /**
  51. * 向下调整堆
  52. *
  53. * @param idx 被下移的元素的起始位置
  54. */
  55. public void shiftDown(int idx) {
  56. // 检查是位置是否正确
  57. if (idx < 0 || idx >= data.size()) {
  58. throw new IllegalArgumentException(idx + "");
  59. }
  60. // 获取开始调整的元素对象
  61. T intent = data.get(idx);
  62. // 获取开始调整的元素对象的左子结点的元素位置
  63. int leftIdx = idx * 2 + 1;
  64. // 如果有左子结点
  65. while (leftIdx < data.size()) {
  66. // 取左子结点的元素对象,并且假定其为两个子结点中最大的
  67. T maxChild = data.get(leftIdx);
  68. // 两个子节点中最大节点元素的位置,假定开始时为左子结点的位置
  69. int maxIdx = leftIdx;
  70. // 获取右子结点的位置
  71. int rightIdx = leftIdx + 1;
  72. // 如果有右子结点
  73. if (rightIdx < data.size()) {
  74. T rightChild = data.get(rightIdx);
  75. // 找出两个子节点中的最大子结点
  76. if (cmp.compare(rightChild, maxChild) > 0) {
  77. maxChild = rightChild;
  78. maxIdx = rightIdx;
  79. }
  80. }
  81. // 如果最大子节点比父节点大,则需要向下调整
  82. if (cmp.compare(maxChild, intent) > 0) {
  83. // 将较大的子节点向上移
  84. data.set(idx, maxChild);
  85. // 记录上移节点的位置
  86. idx = maxIdx;
  87. // 找到上移节点的左子节点的位置
  88. leftIdx = 2 * idx + 1;
  89. }
  90. // 最大子节点不比父节点大,说明父子路径已经按从大到小排好顺序了,不需要调整了
  91. else {
  92. break;
  93. }
  94. }
  95. // index此时记录是的最后一个被上移的子节点的位置(也可能是自身),
  96. // 所以将最开始的调整的元素值放入index位置即可
  97. data.set(idx, intent);
  98. }
  99. /**
  100. * 添加一个元素
  101. *
  102. * @param item 添加的元素
  103. */
  104. public void add(T item) {
  105. // 将元素添加到最后
  106. data.add(item);
  107. // 上移,以完成重构
  108. shiftUp(data.size() - 1);
  109. }
  110. /**
  111. * 删除堆顶结点
  112. *
  113. * @return 堆顶结点
  114. */
  115. public T deleteTop() {
  116. // 如果堆已经为空,就抛出异常
  117. if (data.isEmpty()) {
  118. throw new RuntimeException("The heap is empty.");
  119. }
  120. // 获取堆顶元素
  121. T first = data.get(0);
  122. // 删除最后一个元素
  123. T last = data.remove(data.size() - 1);
  124. // 删除元素后,如果堆为空的情况,说明删除的元素也是堆顶元素
  125. if (data.size() == 0) {
  126. return last;
  127. } else {
  128. // 将删除的元素放入堆顶
  129. data.set(0, last);
  130. // 自上向下调整堆
  131. shiftDown(0);
  132. // 返回堆顶元素
  133. return first;
  134. }
  135. }
  136. /**
  137. * 获取堆顶元素,但不删除
  138. *
  139. * @return 堆顶元素
  140. */
  141. public T getTop() {
  142. // 如果堆已经为空,就抛出异常
  143. if (data.isEmpty()) {
  144. throw new RuntimeException("The heap is empty.");
  145. }
  146. return data.get(0);
  147. }
  148. /**
  149. * 获取堆的大小
  150. *
  151. * @return 堆的大小
  152. */
  153. public int size() {
  154. return data.size();
  155. }
  156. /**
  157. * 判断堆是否为空
  158. *
  159. * @return 堆是否为空
  160. */
  161. public boolean isEmpty() {
  162. return data.isEmpty();
  163. }
  164. /**
  165. * 清空堆
  166. */
  167. public void clear() {
  168. data.clear();
  169. }
  170. /**
  171. * 获取堆中所有的数据
  172. *
  173. * @return 堆中所在的数据
  174. */
  175. public List<T> getData() {
  176. return data;
  177. }
  178. }
  179. /**
  180. * 升序比较器
  181. */
  182. private static class IncComparator implements Comparator<Integer> {
  183. @Override
  184. public int compare(Integer o1, Integer o2) {
  185. return o1 - o2;
  186. }
  187. }
  188. /**
  189. * 降序比较器
  190. */
  191. private static class DescComparator implements Comparator<Integer> {
  192. @Override
  193. public int compare(Integer o1, Integer o2) {
  194. return o2 - o1;
  195. }
  196. }
  197. private static class DynamicArray {
  198. private Heap<Integer> max;
  199. private Heap<Integer> min;
  200. public DynamicArray() {
  201. max = new Heap<>(new IncComparator());
  202. min = new Heap<>(new DescComparator());
  203. }
  204. /**
  205. * 插入数据
  206. *
  207. * @param num 待插入的数据
  208. */
  209. public void insert(Integer num) {
  210. // 已经有偶数个数据了(可能没有数据)
  211. // 数据总数是偶数个时把新数据插入到小堆中
  212. if ((min.size() + max.size()) % 2 == 0) {
  213. // 大堆中有数据,并且插入的元素比大堆中的元素小
  214. if (max.size() > 0 && num < max.getTop()) {
  215. // 将num加入的大堆中去
  216. max.add(num);
  217. // 删除堆顶元素,大堆中的最大元素
  218. num = max.deleteTop();
  219. }
  220. // num插入到小堆中,当num小于大堆中的最大值进,
  221. // num就会变成大堆中的最大值,见上面的if操作
  222. // 如果num不小于大堆中的最大值,num就是自身
  223. min.add(num);
  224. }
  225. // 数据总数是奇数个时把新数据插入到大堆中
  226. else {
  227. // 小堆中有数据,并且插入的元素比小堆中的元素大
  228. if (min.size() > 0 && num > min.size()) {
  229. // 将num加入的小堆中去
  230. min.add(num);
  231. // 删除堆顶元素,小堆中的最小元素
  232. num = min.deleteTop();
  233. }
  234. // num插入到大堆中,当num大于小堆中的最小值进,
  235. // num就会变成小堆中的最小值,见上面的if操作
  236. // 如果num不大于大堆中的最小值,num就是自身
  237. max.add(num);
  238. }
  239. }
  240. public double getMedian() {
  241. int size = max.size() + min.size();
  242. if (size == 0) {
  243. throw new RuntimeException("No numbers are available");
  244. }
  245. if ((size & 1) == 1) {
  246. return min.getTop();
  247. } else {
  248. return (max.getTop() + min.getTop()) / 2.0;
  249. }
  250. }
  251. }
  252. }