JS算法之数据流中的中位数及连续子数组的最大和

550 阅读5分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

数据流中的中位数

剑指Offer 41.数据流中的中位数

难度:困难

题目:leetcode-cn.com/problems/sh…

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

例如,

[2,3,4]的中位数是33

[2,3]的中位数是(2+3)/2=2.5(2+3)/2 = 2.5

设计一种支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中
  • double findMedian() - 返回目前所有元素的中位数。

示例1:

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

示例2:

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

限制:最多会对addNum、findMedian进行50000次调用

题解

法一 直接法

对所有元素先排序,计算出中位数后才取出中位数。

/**
 * initialize your data structure here.
 */
var MedianFinder = function () {
  this.result = [];
};

/** 
 * @param {number} num
 * @return {void}
 */
MedianFinder.prototype.addNum = function (num) {
  this.result.push(num);
};

/**
 * @return {number}
 */
MedianFinder.prototype.findMedian = function () {
  const len = this.result.length;
  if (!len) return null;
  this.result.sort((a, b) => a - b);
  const mid = Math.floor((len - 1) / 2); // 向下取整
  if (len % 2) {
    return this.result[mid];
  }
  return (this.result[mid] + this.result[mid + 1]) / 2;
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * var obj = new MedianFinder()
 * obj.addNum(num)
 * var param_2 = obj.findMedian()
 */
  • 时间复杂度:O(NlogNNlogN),sort使用快排
  • 空间复杂度:O(NlogNNlogN)

法二 二分查找

每次添加元素前都保证数组有序,利用二分查找到方式找到正确位置并将其新元素插入。

var MedianFinder = function () {
  this.result = [];
};

MedianFinder.prototype.addNum = function (num) {
  const len = this.result.length;
  if (!len) {
    this.result.push(num);
    return;
  }

  let l = 0,
    r = len - 1;
  while (l <= r) {
    let mid = Math.floor((l + r) / 2);
    if (this.result[mid] === num) {
      this.result.splice(mid, 0, num);
      return;
    } else if (this.result[mid] > num) {
      r = mid - 1;
    } else {
      l = mid + 1;
    }
  }
  this.result.splice(l, 0, num);
};

MedianFinder.prototype.findMedian = function () {
  const len = this.result.length;
  if (!len) return null;
  const mid = Math.floor((len - 1) / 2);
  if (len % 2) {
    return this.result[mid];
  }
  return (this.result[mid] + this.result[mid + 1]) / 2;
};
  • 时间复杂度:O(NN),二分查找需要O(logNlogN),移动需要O(NN),故为O(NN)
  • 空间复杂度:O(NN)

法三 优先队列(堆)

用大小堆,最大堆存放数据流中较小的一半元素,最小堆存放数据流中较大堆一元素,需要保持两个堆的大小相等或者最大堆 = 最小堆 + 1。(奇数与偶数)

当调用findMedian时,中位数即为最大堆的堆顶元素或者(最大堆的堆顶 + 最小堆的堆顶)/ 2。(奇数与偶数)

保持相等的做法:

  • 先让num放入最大堆maxHeap,取出maxHeap的堆顶元素,放入最小堆minHeap。
  • 若最大堆的大小 < 最小堆的大小,取出minHeap的堆顶元素,放入maxHeap。

JavaScript没有堆的函数,需要自己手写,以下是二叉堆的实现代码:

const defaultCmp = (x, y) => x > y;// 用于控制判断,从而实现最大堆与最小堆的切换
const swap = (arr, i, j) => ([arr[i], arr[j]] = [arr[j], arr[i]]);// 用于交换两个节点
class Heap {
	constructor(cmp = defaultCmp){
    this.container = []; // 用于存放数据
    this.cmp = cmp;
  }
  
  push_heap(data) { // 用于添加节点
    const { container, cmp } = this;
    container.push(data); // 先将值插入到二叉堆的最后一位
    let index = container.length - 1;// data值的坐标
    while(index){
      let parent = Math.floor((index - 1) / 2); // data的父节点
      if(!cmp(container[index], container[parent])){ // 以最大堆为例,当子节点小于父节点,直接返回。
        return;
      }
      swap(container, index, parent);// 当子节点大于父节点,交换
      index = parent;// 继续递归对比
    }
  }
  
  pop_heap() { // 用于弹出节点,弹出堆顶元素后,需要调整堆,并返回堆顶堆值。
    const { container, cmp } = this;
    if(!container.length){
      return null;
    }
    swap(container, 0, container.length - 1);//先讲堆顶元素与尾部元素交换
    const res = container.pop();// 再弹出尾部元素
    const length = container.length;
    let index = 0, exchange = index * 2 + 1;// exchange为左节点下标
    
    while(exchange < length) {
      let right = index * 2 + 2;
      // 以最大堆的情况,如果有右节点,并且右节点的值大于左节点的值
      if(right < length && cmp(container[right], container[exchange])) {
        exchange = right;
      }
      if(!cmp(container[exchange], container[index])) {
        break;
      }
      swap(container, exchange, index);
      index = exchange;
      exchange = index * 2 + 1;
    }
    return res;
  }
  
  top() { // 输出堆顶值
    if(this.container.length) return this.container[0];
    return null;
  }
}                                                 

有了堆的实现后,接下来来看本题:

var MedianFinder = function() {
	this.maxHeap = new Heap();
  this.minHeap = new Heap((x, y) => x < y);
};

MedianFinder.prototype.addNum = function(num) {
	this.maxHeap.push_heap(num);
  this.minHeap.push_heap(this.maxHeap.pop_heap());
  
  if(this.maxHeap.container.length < this.minHeap.container.length){
    this.maxHeap.push_heap(this.minHeap.pop_heap());
  }
};

MedianFinder.prototype.findMedian = function() {
	return this.maxHeap.container.length > this.minHeap.container.length ? this.maxHeap.top() : (this.maxHeap.top() + this.minHeap.top()) / 2;
};
  • 时间复杂度:O(logNlogN)
  • 空间复杂度:O(NN)

连续子数组的最大和

剑指Offer 42.连续子数组的最大和

难度:简单

题目:leetcode-cn.com/problems/li…

输入一个整形数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(NN)

示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

提示:

  • 1 <= arr.length <= 10^5
  • -100 <= arr[i] <= 100

题解

法一 动态规划DP

开辟一个用于「记录nums数组中元素下标为[0, i]的连续子数组最大和」的数组dp[i]。

思路:

  • 初始值dp[0] = nums[0]
  • dp[i - 1] > 0,则dp[i] = nums[i] + dp[i - 1]
  • dp[i - 1] <= 0,则dp[i] = nums[i]
/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function (nums) {
  const dp = [];
  dp[0] = nums[0]
  let max = dp[0];
  for (let i = 1; i < nums.length; i++) {
    dp[i] = nums[i];
    if (dp[i - 1] > 0) {
      dp[i] += dp[i - 1];
    }
    max = max > dp[i] ? max : dp[i];
  }
  return max;
};
  • 时间复杂度:O(NN)
  • 空间复杂度:O(NN)

法二 动态规划空间优化

法一中新开辟了数组,我们可以对其进一步优化使之在原数组上操作。

var maxSubArray = function(nums) {
  let max = nums[0];
  for(let i = 1; i < nums.length; i++){
    if(nums[i - 1] > 0){
      nums[i] += nums[i - 1];
    }
    max = max > nums[i] ? max : nums[i];
  }
  return max;
}
  • 时间复杂度:O(NN)
  • 空间复杂度:O(11)

法三 使用reduce函数

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

callback函数包含四个参数:

  • accumulator:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue
  • currentValue:数组中正在处理的元素。
  • index(可选):数组中正在处理的当前元素的索引。如果提供了initialValue,则起始索引号为0,否则从1开始。
  • array(可选):调用reduce()的数组。
  • initialValue(可选):作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

返回值为函数累计处理的结果。

// 借用了动态规划的思想
var maxSubArray = function(nums) {
	let max = -Infinity;
  nums.reduce((total, cur, i) => {
    if(total > 0){
      total += cur;
    }else{
      total = cur;
    }
    max = max > total ? max : total;
    return total;
  },0) // total初始为0
  return max;
};

法四 贪心法

var maxSubArray = function(nums) {
  let max = nums[0];
  let cur = nums[0];
  for(let i = 1; i < nums.length; i++){
    cur = Math.max(nums[i], cur + nums[i]);
    max = Math.max(max, cur);
  }
  return max;
}
  • 时间复杂度:O(NN)
  • 空间复杂度:O(11)

坚持每日一练!前端小萌新一枚,希望能点个哇~