Day63:数据流中的中位数

时间:2022-07-24
本文章向大家介绍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模块)