React 之最小堆min heap图文详解

目录
  • 二叉树
    • 完全二叉树
  • 二叉堆
  • 最小堆
  • React 采用原因
    • React 函数实现
  • 插入过程(push)
  • >>> 1
  • 删除过程(pop)
  • halfLength
  • peek

二叉树

二叉树(Binary tree),每个节点最多只有两个分支的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

完全二叉树

在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干节点,则此二叉树为完全二叉树(Complete Binary Tree)

以下都是完全二叉树:

二叉堆

二叉堆(binary heap)是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。

二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。

当父节点的键值总是大于或等于任何一个子节点的键值时为“最大堆”(max heap)。

当父节点的键值总是小于或等于任何一个子节点的键值时为“最小堆”(min heap)。

最小堆

今天我们只讲最小堆(min heap)。因为 React 的任务列表(taskQueue)用的就是最小堆。

React 用的是数组结构表示的最小堆,一张图带你明白最小堆如何映射为数组:

React 采用原因

React 为什么采用最小堆结构呢?

这是因为在最小堆结构中,最小值就在第一个,React 可以快速的取出最小值。

React 为什么要取出最小值而不是最大值呢?我们可以这样设想,React 将更新任务拆成多个小任务,每个小任务的数据结构是一个带着 expirationTime 的对象,expirationTime 表示这个任务的过期时间,expirationTime 越小就表示过期时间越近,该任务的优先级就越高,取出最小值就相当于取出优先级最高的任务。

React 函数实现

React 的最小堆涉及 5 个函数:

  • push,往最小堆插入新节点
  • pop,删除根节点,就是那个最小的值
  • siftUp,上浮,不停地交换节点和父节点
  • shiftDown,下沉,不停地交换节点和子节点
  • peek,获取根节点,也就是数组的第一个元素,也就是优先级最高的那个任务

接下来我们进行详细的讲解。

插入过程(push)

我们先讲二叉堆的插入过程:

当插入一个新节点的时候,我们会在二叉堆的最后添加,然后将其“上浮”到正确位置。举个例子:

我们尝试在下面这个二叉堆中,插入新节点,它的值为 1,我们会将这个值与父节点的值进行对比,如果小于父节点,就交换两个节点,就这样不断比较上浮,直到父节点比它小

React 的实现代码如下:

// 源码地址:https://github.com/facebook/react/blob/main/packages/scheduler/src/SchedulerMinHeap.js
function push(heap, node) {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}
function siftUp(heap, node, i) {
  let index = i;
  while (index > 0) {
    // 获取父节点的索引位置
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (compare(parent, node) > 0) {
      // 如果父节点更大,就交换位置
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // 直到父节点更小,就退出
      return;
    }
  }
}
function compare(a, b) {
  // 首先比较 sortIndex,其次是 id
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}
// 测试代码
let taskQueue = [{sortIndex: 2}, {sortIndex: 7}, {sortIndex: 5}, {sortIndex: 12}, {sortIndex: 22}, {sortIndex: 17}];
push(taskQueue, {sortIndex: 1})
console.log(JSON.stringify(taskQueue))

>>> 1

这个实现过程中,可能不熟悉的是这句:

const parentIndex = (index - 1) >>> 1;

这是用来获取父节点的索引值的。

我们先看下 >>> 这个运算符,引用 MDN 的介绍

无符号右移运算符(>>>)(零填充右移)将左操作数计算为无符号数,并将该数字的二进制表示形式移位为右操作数指定的位数,取模 32。向右移动的多余位将被丢弃,零位从左移入。其符号位变为 0,因此结果始终为非负数。与其他按位运算符不同,零填充右移返回一个无符号 32 位整数。

看起来有些复杂?没关系,我们直接讲过程,我们以 5 >>> 1为例:

首先将 5 转为 32 位的二进制数:00000000000000000000000000000101。

>>> 1表示将该二进制向右移动 1 位,向右移动出去的被丢弃,左边部零,于是变成了0000000000000000000000000000010,换算成十进制,就是 2,所以 5 >>> 1的结果就是 2

我们再举一个例子,4 >>> 1,4 是 00000000000000000000000000000101,向右移动一位变成 0000000000000000000000000000010,换算成十进制,就是 2,所以 4 >>> 1的结果也是 2

我们再试几个例子:

所以你可以简单理解为,x >>> 1表示的就是除以 2 后取整。

我们再看下最小堆和数组的映射图:

你看父节点的索引值是不是就是 (子节点的索引值 - 1) / 2 后取整。

删除过程(pop)

现在我们来看删除过程,因为我们删除的是根节点,它的具体流程是:

  • 取出最后一个节点,替换掉根节点
  • 将节点“下沉”到正确位置

我们举个例子:

现在我们要删除根节点 2 ,我们将最后一个节点 25,替换掉根节点 2,然后将新的根节点 25,与两个子节点进行比较,将节点与更小的那个子节点进行交换,然后这样不断比较下沉,直到子节点都比它大。

它的具体实现如下:

// 源码地址:https://github.com/facebook/react/blob/main/packages/scheduler/src/SchedulerMinHeap.js
function pop(heap) {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  // JavaScript 的 pop 方法删除并返回数组的最后一个元素
  const last = heap.pop();
  if (last !== first) {
    heap[0] = last;
    siftDown(heap, last, 0);
  }
  return first;
}
function siftDown(heap, node, i) {
  let index = i;
  const length = heap.length;
  const halfLength = length >>> 1;
  while (index < halfLength) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];
    // 如果 left 比 node 小
    if (compare(left, node) < 0) {
      // 如果 right 比 left 还小,说明 right 最小,right 与 node 交换
      if (rightIndex < length && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      }
      // 说明 left 最小,left 与 node 交换
      else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    }
    // 如果 left node 大,但 right 比 node 小,right 与 node 交换
    else if (rightIndex < length && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // 子元素都比 node 大
      return;
    }
  }
}
// 示例代码
let taskQueue = [{sortIndex: 2}, {sortIndex: 5}, {sortIndex: 7}, {sortIndex: 12}, {sortIndex: 22}, {sortIndex: 17}, {sortIndex: 25}];
pop(taskQueue)
// [{"sortIndex":5},{"sortIndex":12},{"sortIndex":7},{"sortIndex":25},{"sortIndex":22},{"sortIndex":17}]
console.log(JSON.stringify(taskQueue))

halfLength

siftDown 的实现中,我认为最有意思是在 halfLength 这里:

  const length = heap.length;
  const halfLength = length >>> 1;
  while (index < halfLength) {//...}

实际上 React 这里之前直接用的 index < length 而非 index < halfLength,我们可以查看当时的提交记录

那为什么只用比较一半就可以了呢?如果我们尝试自己去画几个最小堆,发现也确实如此,完全不用全部比较一遍。 如果非要从算术的角度来看的话,我们可以这样想: 假设父节点的 index 为 x,那么左子节点的 index 为 2x + 1,右子节点的 index 为 2x + 2,每一次 shiftDown,index 的最大变化就是 2x + 2,而 2x + 2 最大只能等于 length - 1,那么:

因为 2x + 2 <= length - 1 
所以 x <= length/2 - 1.5

我们知道 y >>> 1 ,在 y 为正数的情况下,计算的结果为 y/2 - 0.5 或者 y/2

如果 x <= length/2 - 1.5
那么肯定 x < length/2 - 0.5 以及 x < length/2
所以肯定 x < length >>> 1

peek

除此之外,还有一个 peek 方法,获取数组的第一个元素:

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

好了,React 的 SchedulerMinHeap.js 这个文件的所有代码就正式讲完了,它是一个几乎完全独立的实现,当然 Scheduler 也是独立的,下篇我们接着讲 Scheduler

以上就是React 之最小堆min heap图文详解的详细内容,更多关于React最小堆min heap的资料请关注我们其它相关文章!

(0)

相关推荐

  • React为什么需要Scheduler调度器原理详解

    目录 正文 我们为什么需要Scheduler(调度器) Scheduler如何进行工作 总结 正文 最近在重学React,由于近两年没使用React突然重学发现一些很有意思的概念,首先便是React的Scheduler(调度器) 由于我对React的概念还停留在React 15之前(就是那个没有hooks的年代),所以接触Scheduler(调度器) 让我感觉很有意思: 在我印象中React的架构分为两层(React 16 之前) Reconciler(协调器)—— 负责找出变化的组件 Rend

  • ReactNative支付密码输入框实现详解

    目录 正文 hooks版本 正文 项目中需求如果涉及钱包,支付等功能,可以大概率会用到输入密码组件,也算是个常见组件吧. 之前写过一个纯js的开源组件,使用的类的形式,也比较老了,可直接添加npm库到项目中进行使用. hooks版本 最近项目需要,又重新写了一个hooks版本的,现在直接上源码,对于不想添加依赖库的伙伴,可直接复制源码到项目中,直接使用. 'use strict'; import React, {useRef, useState} from 'react'; import {St

  • React不使用requestIdleCallback实现调度原理解析

    目录 1.起因 2.查找问题 3.解决问题 4.总结 5.吐槽 1.起因 最近在一边啃源码,一边手写fiber嘛,然后也看了很多博客和资料,基本上大伙好像都是说用requestIdleCallback来模拟react实现一个空闲时间调度.但我自己手写的时候把怎么用怎么怪,老是感觉有什么地方不对劲而且是在调度过程中,可能是因为我是想写出来来一个相对健全一点的模版方便我以后写源码的其他部分把,然后分析了一下所以有了这篇博客. 2.查找问题 1.requestIdleCallback是利用帧之间空闲时

  • react源码层分析协调与调度

    目录 requestEventTime requestUpdateLane findUpdateLane lanePriority LanePriority createUpdate enqueueUpdate 总结 协调与调度 reconciler流程 同步任务类型执行机制 异步任务类型执行机制 shouldYield performUnitOfWork beginWork completeUnitOfWork scheduler流程 performWorkUntilDeadline 总结 r

  • React 中的重新渲染类组件及函数组件

    目录 缘起 类组件 React 合成事件 定时器回调后触发 setState 异步函数后调触发 setState 原生事件触发 setState 修改不参与渲染的属性 只是调用 setState,页面会不会重新渲染 多次渲染的问题 测试代码 函数组件 React 合成事件 定时器回调 异步函数回调 原生事件 更新没使用的状态 小结 不同的情况 设置同样的 State React Hook 中避免多次渲染 将全部 state 合并成一个对象 使用 useReducer 状态直接用 Ref 声明,需

  • 深入理解React调度(Scheduler)原理

    目录 异步调度 时间分片 异步调度原理 总结 异步调度 问题:由于对于大型的 React 应用,会存在一次更新,递归遍历大量的虚拟 DOM ,造成占用 js 线程,使得浏览器没有时间去做一些动画效果,伴随项目越来越大,项目会越来越卡. 对比Vue: Vue 有这 template 模版收集依赖的过程,轻松构建响应式,使得在一次更新中,Vue 能够迅速响应,找到需要更新的范围,然后以组件粒度更新组件,渲染视图. React 中,一次更新 React 无法知道此次更新的波及范围,所以 React 选

  • React 之最小堆min heap图文详解

    目录 二叉树 完全二叉树 二叉堆 最小堆 React 采用原因 React 函数实现 插入过程(push) >>> 1 删除过程(pop) halfLength peek 二叉树 二叉树(Binary tree),每个节点最多只有两个分支的树结构.通常分支被称作“左子树”或“右子树”.二叉树的分支具有左右次序,不能随意颠倒. 完全二叉树 在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干节点,则此二叉树为完全二叉树(Complete Binar

  • React的diff算法核心复用图文详解

    目录 引言 Fiber 架构 React 的 diff 算法 总结 引言 React 是基于 vdom 的前端框架,组件 render 产生 vdom,然后渲染器把 vdom 渲染出来. state 更新的时候,组件会重新 render,产生新的 vdom,在浏览器平台下,为了减少 dom 的创建,React 会对两次的 render 结果做 diff,尽量复用 dom,提高性能. diff 算法是前端框架中比较复杂的部分,代码比较多,但今天我们不上代码,只看图来理解它. 首先,我们先过一下 r

  • MySQL的Query Cache图文详解

    目录 一.原理概述 二.Query Cache系统变量 1. have_query_cache 2. query_cache_limit 3. query_cache_min_res_unit 4. query_cache_size 5. query_cache_type 6. query_cache_wlock_invalidate 三.Query Cache状态变量 1. Qcache_free_blocks 2. Qcache_free_memory 3. Qcache_hits 4. Q

  • JavaScript中浅讲ajax图文详解

    1.ajax入门案例 1.1 搭建Web环境 ajax对于各位来说,应该都不陌生,正因为ajax的产生,导致前台页面和服务器之间的数据传输变得非常容易,同时还可以实现页面的局部刷新.通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新.这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新. 对于JavaWeb项目而言,ajax主要用于浏览器和服务器之间数据的传输. 如果是单单地堆砌知识点,会显得比较无聊,那么根据惯例,我先不继续介绍ajax,而是来写一个案例吧. 打开

  • Hadoop-3.1.2完全分布式环境搭建过程图文详解(Windows 10)

    一.前言 Hadoop原理架构本人就不在此赘述了,可以自行百度,本文仅介绍Hadoop-3.1.2完全分布式环境搭建(本人使用三个虚拟机搭建). 首先,步骤: ① 准备安装包和工具: hadoop-3.1.2.tar.gz ◦ jdk-8u221-linux-x64.tar.gz(Linux环境下的JDK) ◦ CertOS-7-x86_64-DVD-1810.iso(CentOS镜像) ◦工具:WinSCP(用于上传文件到虚拟机),SecureCRTP ortable(用于操作虚拟机,可复制粘

  • IDEA插件开发之环境搭建过程图文详解

    基于IntelliJ Platform Plugin搭建 环境步骤 File->New->Project 选择IntelliJ Platform Plugin 如果你这里没有SDK环境,则添加一个SDK环境,选择自己的idea的安装的根目录即可. 展示效果 基于Gradle搭建环境步骤 File->New->Project 选择Gradle next 进来以后大概是这样的一个界面,然后gradle会自动build项目,下载相关的依赖.(可能会失败) 遇到的问题一,依赖ideaIC-

  • MySQL与sqlyog安装教程图文详解

    1. MySQL1.1 MySQL安装 mysql-5.5.27-winx64下载 (1)欢迎安装 (2)协议接受 (3)安装模式选择 Typical:表示一般常用的组件都会被安装,默认情况下安装到C:\Program Files\MySQL\MySQL Server 5.5\下. Complete:表示会安装所有的组件.此套件会占用比较大的磁盘空间. Custom:表示用户可以选择要安装的组件,可以更改默认按照的路径.这种按照类型最灵活,适用于高级用户. (4)选择安装组件与路径选择 (5)安

  • python2利用wxpython生成投影界面工具的图文详解

    本投影界面工具的功能: 准备好.prj投影文件,将输入文件夹内的WGS84经纬度坐标shp文件,投影为平面文件,成果自动命名为prj_***并新建在输入文件夹同一路径下. 下一步目标: 利用pyinstaller或其他打包库生成exe文件,目前停滞在python2语法.arcpy打包出错相关问题上. 参考文献: <Using Py2exe with Arcpy- It can be done easily!> <如何使用py2exe打包arcpy脚本?> GUI界面示意图 投影文件

  • Android 图文详解Binder进程通信底层原理

    之前了解到进程与多进程,涉及多进程不可避免的遇到了进程间通信,说到进程间通信,Binder 成了一道绕不过的坎.接下来咱们逐一了解.

  • 图文详解梯度下降算法的原理及Python实现

    目录 1.引例 2.数值解法 3.梯度下降算法 4.代码实战:Logistic回归 1.引例 给定如图所示的某个函数,如何通过计算机算法编程求f(x)min? 2.数值解法 传统方法是数值解法,如图所示 按照以下步骤迭代循环直至最优: ① 任意给定一个初值x0: ② 随机生成增量方向,结合步长生成Δx: ③ 计算比较f(x0)与f(x0+Δx)的大小,若f(x0+Δx)<f(x0)则更新位置,否则重新生成Δx: ④ 重复②③直至收敛到最优f(x)min. 数值解法最大的优点是编程简明,但缺陷也很

随机推荐