任务调度

148 阅读3分钟

找到第k大元素

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。 int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

来源:力扣(LeetCode) 链接:leetcode.cn/problems/kt… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路:把前k大的元素找出来,并在这k个元素中找到最小值

一个无序的数组,第k大元素之前的数据怎么获得?

最小堆

终端节点小于左右节点,具体参考堆排序

根据父节点推断子节点; left = (parent+1)*2-1 right = left + 1

子推断父 parent = (childindex -1)>> 1

/**
 * @param {number} k
 * @param {number[]} nums
 */
var KthLargest = function (k, nums) {
    this.k = k;
    //这个最小堆只有k个元素,前k
    this.heap = new MinHeap()
    for (let item of nums) {
        this.add(item)
    }
};

/** 
 * @param {number} val
 * @return {number}
 */
KthLargest.prototype.add = function (val) {
    this.heap.push(val)
    //data长度大于k个元素,就删除堆顶元素
    if (this.heap.size() > this.k) {
        this.heap.pop()
    }

    return this.heap.peek()
};

class MinHeap {
    constructor() {
        this.data = [];
    }

    size() {
        return this.data.length;
    }

    //最小值
    peek() {
        return this.size() === 0 ? null : this.data[0]
    }
    compare(a, b) {
        return a - b;
    }

    swap(a, b) {
        [this.data[a], [this.data[b]]] = [this.data[b], [this.data[a]]]
    }
    shiftUp(item, i) {
        let index = i;
        while (index > 0) {
            //根据当前下标找到父节点进行对比
            const parentIndex = (index - 1) >> 1
            const parent = this.data[parentIndex]
            //如果(子 - 父)返回负数则互换位置
            if (this.compare(item, parent) < 0) {
                this.swap(index, parentIndex)
                index = parentIndex//继续向上调整
            } else {
                break;
            }
        }
    }

    shifDown(item, i) {
        let index = i;
        //因为本身就是最小堆,只会遍历左边或者右边,另一边是满足条件的
        while (index < (this.size() >> 1)) {
            //获取左右节点,然后对比
            const leftIndex = (index + 1) * 2 - 1
            const rightIndex = leftIndex + 1
            const left = this.data[leftIndex]
            const right = this.data[rightIndex]
            //左节点小于父节点
            if (this.compare(left, item) < 0) {
                //判断左右节点的大小,决定谁和father换
                if (rightIndex < this.size() && this.compare(right,left) < 0) {
                    this.swap(rightIndex,index)
                    //从右向下调整
                    index = rightIndex
                }else{
                       this.swap(leftIndex,index)
                         index = leftIndex
                }
            }else if(rightIndex < this.size() && this.compare(right,item) < 0){
                this.swap(rightIndex,index)
                    //从右向下调整
                    index = rightIndex
            }else{
                break;
            }

        }
    }
    push(item) {
        this.data.push(item)
        //向上调整最小堆
        this.shiftUp(item, this.size() - 1)

    }

    pop() {
        if (this.size() === 0) {
            return null
        }
            const last = this.data.pop()
            if (this.size() !== 0) {
                this.data[0] = last
                //删除第0个元素,最后一个元素补上,向下调整最小堆。
                this.shifDown(last, 0)

            }
    }
}

任务调度与最小堆

为了处理优先级高的任务,react定义了俩个任务池

var taskQueue = []//优先级高的
var timerQueue = []

为什么使用使用最小堆? 源码中任务初始结构:

var newTask = {
    id:taskIdCouter++ //标记任务的id
    callback,
    priorityLevel,//任务优先级
    startTime,//任务开始时间
    expirationTime,//过期时间
    sortIndex:-1//任务排序,取值来自过期时间,因此值越小,优先级越高
}

MessageChannel

创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据。

        var channel = new MessageChannel();
        var port1 = channel.port1;
        var port2 = channel.port2;
        port1.onmessage = function(event) {
            console.log("port1收到来自port2的数据:" + event.data);
        }
        port2.onmessage = function(event) {
            console.log("port2收到来自port1的数据:" + event.data);
        }

        port1.postMessage("发送给port2");//port2收到来自port1的数据:发送给port2
        port2.postMessage("发送给port1");//port1收到来自port2的数据:发送给port1

        /**
         * 
         * MessageChannel的postMessage传递的数据也是深拷贝的,这和web worker的postMessage一样。而且还可以拷贝undefined和循环引用的对象。
        */

window.performance.now()

和 JavaScript 中其他可用的时间类函数(比如Date.now)不同的是,window.performance.now()返回的时间戳没有被限制在一毫秒的精确度内,相反,它们以浮点数的形式表示时间,精度最高可达微秒级。

另外一个不同点是,window.performance.now()是以一个恒定的速率慢慢增加的,它不会受到系统时间的影响(系统时钟可能会被手动调整或被 NTP 等软件篡改)。另外,performance.timing.navigationStart + performance.now() 约等于 Date.now()。

开始编写任务调度代替window.requestIdleCallback

根据newTask的sortIndex来进行最小堆排序,taskQueue就是「k之前元素」的最小堆,拿到堆顶元素就是拿到优先级最高的元素

//返回最小堆堆顶元素,就是优先级最高的任务

function peek(heap) {
  return heap.length === 0 ? null : heap[0];
}

//往最小堆中插入元素
function push(heap, node) {
  let index = heap.length;
  heap.push(node);
  shifUp(heap, node, index);//根据当前节点坐标为原数组长度找父
}

//删除堆顶元素
function pop(heap) {
  if (heap.length === 0) {
    return null;
  }

  const first = heap[0];
  const last = heap.pop();

  if (first !== last) {
    heap[0] = last;
    shifDown(heap, last, 0);
  }

  return first;
}

//向上调整最小堆
function shifUp(heap, node, i) {
  let index = i;
  //因为向上调整,需要腹父节点,而0没有父,所以大于0
  while (index > 0) {
    //父节点下标
    const parentIndex = (index - 1) >> 1;
    const parent = heap[parentIndex];
    //开始比较父节点和子节点,因为push的时候就是一个最小堆,所以插入的元素只要和父节点比较就行
    if (compare(parent, node) > 0) {
      //父大于子,交换父子
      heap[parentIndex] = node;
      heap[index] = parent;
      //从父节点的位置向上调整
      index = parentIndex;
    } else {
      return;
    }
  }
}

//向下调整
function shifDown(heap, node, i) {
  let index = 0;
  let len = heap.length >> 1;
  while (index < len) {
    const leftIndex = (index + 1) * 2 - 1;
    const rightIndex = leftIndex + 1;
    const left = heap[leftIndex];
    const right = heap[rightIndex];
    //开始比较三个节点
    if (compare(left, node) < 0) {
      //left < parent,left < right??
      if (rightIndex < heap.length && compare(right, left) < 0) {
        //right最小
        heap[index] = right;
        heap[rightIndex] = node;
        //从右节点向下调整
        index = rightIndex;
      } else {
        //交换left和parent
        heap[index] = left;
        heap[leftIndex] = node;
        //从左节点向下调整
        index = leftIndex;
      }
      //比较right和parent
    } else if (rightIndex < heap.length && compare(right, node)) {
      heap[index] = right;
      heap[rightIndex] = node;
      //从右节点向下调整
      index = rightIndex;
    }else{
        //parent最小,本身就是最小堆了,无需向下调整。
        return
    }
  }
}

//比较
function compare(a, b) {
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

export default {
    peek,
    push,
    pop,
}

创建newTask然后进行最小堆排序,使用MessageChannel来确定执行优先级最高的任务

import { getCurrntTime } from "../utils";
import minHeap from "./minHeap";

//立即执行任务
let taskQueue = [];
//用来区分任务
let taskIdCounter = 1;

export function scedulerCallback(callback){
    //当前时间
    const currentTime = getCurrntTime();
    //等多久执行,暂时先全部设为最高优先级
    const timeout = -1;
    //过期时间
    const expirtationTime = currentTime - timeout;

    const newTask = {
        id:taskIdCounter++ ,//标记任务的id
        callback,
        // priorityLevel,//任务优先级
        // startTime,//任务开始时间
        expirtationTime,//过期时间
        sortIndex:expirtationTime//任务排序,取值来自过期时间,因此值越小,优先级越高
    }

    //放入任务池中,开始最小堆
        minHeap.push(taskQueue,newTask);
        //请求调度
        requestHostCallback()
}

function requestHostCallback(){
    port.postMessage(null)
}

const channel = new MessageChannel();
const port = channel.port2;

channel.port1.onmessage = function(){
    workloop()
}

function workloop(){
    //获取优先级最高的任务
    let currentTask = minHeap.peek(taskQueue);

    while(currentTask){
        const callback = currentTask.callback
        //防止重复执行
        currentTask.callback = null;
        callback()

        minHeap.pop(taskQueue);
        currentTask =minHeap.peek(taskQueue)
    }
}