专栏首页全部文章Day63:数据流中的中位数

Day63:数据流中的中位数

剑指Offer_编程题——数据流中的中位数

题目描述:

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

具体要求:

时间限制: C/C++ 1秒,其他语言2秒 空间限制: C/C++32M,其他语言64M

具体实现:

背景知识介绍:   在做题之前,首先给大家介绍堆排序。在维基百科中是这样介绍堆排序的。堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。若以升序排序说明,把数组转换成最大堆(Max-Heap Heap),这是一种满足最大堆性质(Max-Heap Property)的二叉树:对于除了根之外的每个节点i, A[parent(i)] ≥ A[i]。重复从最大堆取出数值最大的结点(把根结点和最后一个结点交换,把交换后的最后一个结点移出堆),并让残余的堆维持最大堆性质。通常堆是通过一维数组来实现堆节点的访问的。在数组起始位置为0的情形中:父节点i的左子节点在位置(2i+1);父节点i的右子节点在位置(2i+2);子节点i的父节点在位置floor((i - 1)/2);在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作: 1、最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点 2、创建最大堆(Build Max Heap):将堆中的所有数据重新排序 3、堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算   堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。以下就是大顶堆和小顶堆。

  接下来给大家介绍算法步骤: 1、创建一个堆 H[0……n-1]; 2、把堆首(最大值)和堆尾互换; 3、把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置; 4、重复步骤 2,直到堆的尺寸为 1。   接下来动态演示堆排序的过程,让大家对其排序有更深入的了解,具体过程如下:

  排序动画整个过程用语言描述如下: 1、首先,将所有的数字存储在堆中 2、按大顶堆构建堆,其中大顶堆的一个特性是数据将被从大到小取出,将取出的数字按照相反的顺序进行排列,数字就完成了排序 3、在这里数字 5 先入堆 4、数字 2 入堆 5、数字 7 入堆, 7 此时是最后一个节点,与最后一个非叶子节点(也就是数字 5 )进行比较,由于 7 大于 5 ,所以 7 和 5 交互 6、按照上述的操作将所有数字入堆,然后从左到右,从上到下进行调整,构造出大顶堆 7、入堆完成之后,将堆顶元素取出,将末尾元素置于堆顶,重新调整结构,使其满足堆定义 8、堆顶元素数字 7 取出,末尾元素数字 4 置于堆顶,为了维护好大顶堆的定义,最后一个非叶子节点数字 5 与 4 比较,而后交换两个数字的位置 9、反复执行调整+交换步骤,直到整个序列有序。   以上就是对堆排序的相关知识介绍,希望通过介绍后大家对堆排序有进一步的了解。接下来我们通过堆排序来解决该题。 思路一:   通过对堆排序的介绍,本题其实本意就是考察堆排序的。我们利用堆排序来解答该题,当读取数据的时候,先将数据取相反数插入大顶堆中,再将大顶堆的堆顶元素取相反数插入小顶堆中。进行完这一步操作之后,如果小顶堆的元素个数多于大顶堆的元素个数(即插入元素之后,所有的元素一共有奇数个)再把小顶堆的堆顶元素取相反数插入大顶堆中。如果小顶堆的元素个数等于大顶堆的元素个数(即当前元素的个数为偶数个),不再进行元素的变动。获取元素的中位数:利用小顶堆和大顶堆的元素个数来判断元素有奇数个还是偶数个(大顶堆和小顶堆的元素个数相等,偶数,大顶堆的元素个数多,奇数)。当有偶数个元素时,返回小顶堆和大顶堆堆顶元素的均值(别忘了大顶堆元素的负号),有奇数元素时,返回大顶堆的堆顶元素即可。 1、使用大顶堆+小顶堆的容器. 2、两个堆中的数据数目差不能超过1,这样可以使中位数只会出现在两个堆的交接处 3、大顶堆的所有数据都小于小顶堆,这样就满足了排序要求。平均数就在两个堆顶的数之中。   我们分别用java和python将其实现。 1、首先我们用java将其实现:

import java.util.*;
public class Solution{
	private int count = 0;
	private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
	private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(15, new Comparator<Integer>(){
		public int compare(Integer o1, Integer o2){
			return o2 - o1;
		}
	});
	public void Insert(Integer num){
		if(count % 2 == 0){
			maxHeap.offer(num);
			int filteredMaxNum = maxHeap.poll();
			minHeap.offer(filteredMaxNum);
		}else{
			minHeap.offer(num);
			int filteredMinNum = minHeap.poll();
			maxHeap.offer(filteredMinNum);
		}
		count++;
	}
	public Double GetMedian(){
		if(count % 2 == 0){
			return new Double((minHeap.peek() + maxHeap.peek())) / 2;
		}else{
			return new Double(minHeap.peek());
		}
	}
}

代码效果图如图所示:

  这里需要我们注意的是:首先为了保证两个堆中的数据数目差不能超过1,在Insert()方法中使用了count来辅助实现。其次为了保证小顶堆的元素都小于大顶堆的元素,借用优先队列PriorityQueue。其默认维持队列内升序排列。也可以像上面传入一个比较器,然后使其改变排列顺序。最后在具体的实施方案方面:当数据总数为偶数时,新加入的元素,应当进入小根堆,注意不是直接进入小根堆,而是经大根堆筛选后取大根堆中最大元素进入小根堆;当数据总数为奇数时,新加入的元素,应当进入大根堆。注意不是直接进入大根堆,而是经小根堆筛选后取小根堆中最大元素进入大根堆。   上面我们用到了PriorityQueue,接下来我们详细给大家介绍PriorityQueue对列。PriorityQueue 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。PriorityQueue 队列的头指排序规则最小那个元素。如果多个元素都是最小值则随机选一个。PriorityQueue 是一个无界队列,但是初始的容量(实际是一个Object[]),随着不断向优先级队列添加元素,其容量会自动扩容,无需指定容量增加策略的细节。PriorityQueue使用跟普通队列一样,唯一区别是PriorityQueue会根据排序规则决定谁在队头,谁在队尾。PriorityQueue优先级规则可以由我们根据具体需求而定制, 方式有2种:①、添加元素自身实现了Comparable接口,确保元素是可排序的对象;②、如果添加元素没有实现Comparable接口,可以在创建PriorityQueue队列时直接指定比较器。   代码中还用到了java中的Double类,这个类在jdk1.8中已经不用了,但代码中既然用到,我们就给大家解释一下:Double 类在对象中包装了一个基本类型 double 的值。Double 类对象包含一个 double 类型的字段。此外,该类还提供了多个方法,可以将 double 类型与 String 类型相互转换,同时 还提供了处理 double 类型时比较常用的常量和方法。Double 类中的构造方法有两个:首先Double(double value):构造一个新分配的 Double 对象,它表示转换为 double 类型的参数。其次:Double(String s):构造一个新分配的 Double 对象,它表示 String 参数所指示的 double 值。在 Double 类内部包含一些和 double 操作有关的方法,具体如下图所示:

2、接下来用python将其实现:

from heapq import *
class Solution:
	def __init__(self):
		self.small = []
		self.large = []
	def Insert(self, num):
		small, large = self.small, self.large
		heappush(small, -heappushpop(large, -num))
		if len(large) < len(small):
			heappush(large, -heappop(small))
	def GetMedian(self, n = 1):
		small, large = self.small, self.large
		if len(large) > len(small):
			return float(-large[0])
		return (small[0] - large[0]) / 2.0

代码效果图如图所示:

  但这里需要我们注意的是:在牛客网的python有个bug,在定义GetMedian函数的时候,它只是有一个self,这样就会造成缺参数的报错,因此我们得需要自己添加一个参数来避免这一报错。牛客网中python中定义的参数如下所示:

  本代码中用到了python自带的heapq库来实现的堆,接下来给大家介绍该函数的详细知识。一种著名的数据结构是堆(heap),它是一种优先队列。优先队列让你能够以任意顺序添加对象,并随时(可能是在两次添加对象之间)找出(并删除)最小的元素。相比于列表方法min,这样做的效率要高得多。实际上,Python没有独立的堆类型,而只有一个包含一些堆操作函数的模块。这个模块名为heapq(其中的q表示队列),它包含6个函数,其中前4个与堆操作直接相关。必须使用列表来表示堆对象本身。具体常用方法如下:

  函数heappush用于在堆中添加一个元素。请注意,不能将它用于普通列表,而只能用于使用各种堆函数创建的列表。原因是元素的顺序很重要(虽然元素的排列顺序看起来有点随意,并没有严格地排序)。函数heappop弹出最小的元素(总是位于索引0处),并确保剩余元素中最小的那个位于索引0处(保持堆特征)。虽然弹出列表中第一个元素的效率通常不是很高,但这不是问题,因为heappop会在幕后做些巧妙的移位操作。函数heapify通过执行尽可能少的移位操作将列表变成合法的堆(即具备堆特征)。如果你的堆并不是使用heappush创建的,应在使用heappush和heappop之前使用这个函数。函数heapreplace用得没有其他函数那么多。它从堆中弹出最小的元素,再压入一个新元素。相比于依次执行函数heappop和heappush,这个函数的效率更高。nlargest(n, iter)和nsmallest(n, iter),:分别用于找出可迭代对象iter中最大和最小的n个元素。这种任务也可通过先排序(如使用函数sorted)再切片来完成,但堆算法的速度更快,使用的内存更少(而且使用起来也更容易)。 思路二:   我们除了用大小堆的方法,也可以用递归将其实现。我们可以声明一个List,存储每次读入的字符;然后求当前list中的中位数。具体我们用java和python将其实现: 1、用java将其实现:

import java.util.*;
public class Solution{
	private ArrayList<Integer> arr = new ArrayList<>();
	public void Insert(Integer num){
		arr.add(num);
	}
	public Double GetMedian(){
		double middle = 0;
		int size = arr.size();
		if(size != 0){
			Integer[] array = (Integer[])arr.toArray(new Integer[size]);
			Arrays.sort(array);
			if(size % 2 == 0){
				middle = (array[size/2 - 1] + array[size/2]) / 2.0;
			}else{
				int inx = size / 2;
				middle = array[inx];
			}
		}
		return middle;
	}
}

代码效果图如图所示:

2、用python将其实现:

class Solution:
	def __init__(self):
		self.nums = []
	def Insert(self, num):
		self.nums.append(num)
	def GetMedian(self, middle):
		self.nums.sort()
		if len(self.nums) % 2 == 1:
			return self.nums[(len(self.nums) - 1) / 2]
		else:
			return(self.nums[len(self.nums) / 2] + self.nums[len(self.nums) / 2 - 1]) / 2.0

代码效果图如图所示:

总结

  本题主要通过数据流中的中位数来考察我们对堆排序的理解与掌握。本文在做题之前给大家介绍了堆排序的相关内容,另外本文给出了两种解题思路,并且分别用java和python两门编程语言将其实现。并且给大家介绍了代码中应用到的相关知识:比如优先队列、Double以及python中的Heapq库。另外需要我们特别注意的是:在写python程序的时候,牛客网给的参数有问题,需要我们自己添加一个参数,否则会报缺参数的错误。因此,我们在做题的时候,应该多次尝试各种方法,扩展自己的思维,写出优质的代码。总之,我们要继续加油,争取早日找到工作,Good Luck!!!

参考文献

[1] ouyangyanlan [2] 乖乖的函数 [3] 负雪明烛 [4] 堆排序 [5] 五分钟弄懂有点难度的排序:堆排序 [6] PriorityQueue详解 [7] Java Double类 [8] python高级(堆heapq模块)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 面试汇总(十):数据结构与算法常见面试总结(三)——哈希、链表、队列、查找、递归

      前两篇文章我们分别介绍了在面试中数据结构与算法中树和堆与栈、数组、排序的一些问题常见的面试题。这篇文章我们继续给大家介绍常见的问题。这篇文章将给大家介绍数据...

    stefan666
  • 第20天:NLP实战(四)——用GRU模型实现电影评论情感分析

      接着上次的项目,主要是为了更加熟悉我们对NLP知识的实际应用,接着上次对深度学习中的CNN的简单应用相信大家对深度学习的相关知识以及相应的实现流程有了一个更...

    stefan666
  • Day 3:从尾到头打印链表

    思路1: 1、先构造一个链表 2、定义一个栈,将链表元素放入到栈中 3、利用栈的先进后出,实现从尾到头返回 4、取出栈中元素放入到ArrayList,然...

    stefan666
  • vim 的python 语法高亮

    vim支持大部分文件格式的语法高亮,而且可以自定义。不过缺省的python语法高亮感觉太少,修改一下。

    py3study
  • DQN系列(2): Double DQN算法原理与实现

    论文地址: https://arxiv.org/pdf/1509.06461.pdf

    深度强化学习实验室
  • 工厂设计模式在自动化中的引用(一)

    在自动化测试的范围中,目前依据webdriver的,web应用测试框架有selenium2,对于移动app自动化的测试,有appium,selen...

    无涯WuYa
  • 7.01-beautiful_soup

    hankleo
  • AI 在筛查消化道早期癌症的应用

    聊起 AI,画面都充斥着机械语言:精密高级的芯片,光怪陆离的智能产业……你眼中的 AI 有什么样的能力?能给传统行业带来哪些变革与发展?基于此,云加社区联手知乎...

    云加社区
  • 如何超过医生准确率,做好医生第三只眼?腾讯医疗AI背后黑科技

    ? 关于作者 尚鸿,腾讯研发管理部\AI平台部\AI医疗中心 从最开始的每月一个版本,到后来的 2017年,再次兴起的人工智能还在寻找产业落地,而亟待信息化、...

    腾讯大讲堂
  • 教程 | 如何使用变分自编码器VAE生成动漫人物形象

    选自Medium 作者:Wuga 机器之心编译 参与:Geek Ai、李泽南 变分自编码器(VAE)与生成对抗网络(GAN)经常被相互比较,其中前者在图像生成上...

    机器之心

扫码关注云+社区

领取腾讯云代金券