• 一、题目
  • 二、解题思路
  • 三、解题代码

    一、题目

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

    二、解题思路

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