Arts 第六十七周(7/27 ~8/2)

197 阅读9分钟

ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。

Algorithm

LC 264. Ugly Number II

题目解析

题目让你找出第 n 个丑数。对于丑数,题目也给出了定义,丑数是正整数,并且它的质数因子仅包含 2, 3, 5。另外,整数 1 算是一个最小的丑数。

一开始看到这道题,感觉就是一道简单的数学乘法题,一个循环不就搞定了?但其实并不是,如果仅仅使用简单的乘法循环,你并不知道下一个数所在的序列位置。另外,暴力的深度优先搜索遍历在这道题上也不适用,因为你并不确定搜索的结束条件,到底找到哪个数才停止递归呢?

但如果进一步思考,你会发现 后面的数可以由前面的数推出来。用通俗一点的话讲就是,问题的解可以由子问题的解推出来。那么这样的话,很自然就会往动态规划的方向去思考。这里比较难想到的地方是,子问题之间的联系是什么?比如我们定义动态规划状态,dp[i] 表示第 i 个丑数,那么这个状态怎么由前面的状态推导得出?这里的重点是,前面的每一个数(状态)都可以乘上 2, 3, 5 来形成一个新的状态,但是我们的关注点是下一个状态是什么,比如第一个状态是 1,它可以乘上上面 3 个数的其中一个。很明显,1 乘上 2 得到的状态更小,因此第二个状态就是 2。到这时,我们需要考虑第三个状态是什么,从 1 出发,我们得到了 2,但是从 1 出发,我们还可以得到 3,5。另外,从 2 出发我们可以得到 4。你可能会说,从 2 出发我们还可以得到 6,10。没错,但是乘上 3,5 在 1 的时候考虑比在 2 处考虑得出的状态更小。

通过上面的描述,你可能找到规律了。每个状态都可以乘上 2, 3, 5。但是状态乘上这些质数因子的时候,必须保证前面的状态已经乘过了对应的质数因子,因为前面会得到更小的值,我们需要的是按序查找。由此,我们可以创建 3 个指针(变量),分别表示这 3 个质数因子此时应该乘上第几个状态。这样的思路可以在 O(n) 的时间完成这道题目。


参考代码

func nthUglyNumber(n int) int {
    if n < 1 {
        return 0
    }

    p2, p3, p5 := 0, 0, 0

    dp := make([]int, n)

    dp[0] = 1
    for i := 1; i < n; i++ {
    	// 比较此时可能的状态,取最小的那个
        dp[i] = min(dp[p2] * 2, min(dp[p3] * 3, dp[p5] * 5))

		// 更新指向
        // 注意这里不能只更新一个指针
        // 比如 6,可以由 2 * 2 * 2 形成,也可以由 2 * 3 组成
        if dp[i] == dp[p2] * 2 {
            p2++
        }

        if dp[i] == dp[p3] * 3 {
            p3++
        }

        if dp[i] == dp[p5] * 5 {
            p5++
        }
    }

    return dp[n - 1]
}

func min(a, b int) int {
    if a < b {
        return a
    }

    return b
}

Review

React useRef Hook

这篇文章主要讲解了 React Ref 的用法和注意事项。很多刚接触 React 的人,包括我,可能并不怎么了解 React Ref 具体是什么。再讲这个之前,我们先来看一个文章中给出的例子:

import React, { useState, useEffect } from "react";

const Timer = () => {
  const [intervalState, setIntervalState] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log("A second has passed");
    }, 1000);
 
    setIntervalState(id);
    
    return () => clearInterval(intervalState);
  });

  return (
    <>
      //...
    </>
  );
}

在上面的这个例子中,每次页面重新渲染,就会先执行 setInterval 函数。然后,我们把 setInterval 的 id 保存在一个 state 中,然后返回清除这个 interval 的函数。但是这么写会有什么问题呢?我们知道,组件状态的改变肯定让组件重新渲染,由于我们把 id 保存在 state 中,因此保存 id 这个动作会导致组件重新渲染。组件重新渲染后,又会带来 id 的更新。这样组件的渲染就会无限循环下去。

那这个问题怎么解决呢?我们的目的很明显,想要保存组件的信息,但又不希望组件重新渲染。 React Ref 的产生就是为了解决这个问题的,它提供了在 Render 状态下,操作 DOM 节点和 React 元素的方式。更新 Ref 不会造成页面的重新渲染,并且 Ref 在 React 的整个生命周期上都是有效的。于是,上面的例子可以改写一下:

import React, { useRef, useEffect } from "react";

const Timer = () => {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      console.log("A second has passed");
    }, 1000);

    intervalRef.current = id;
    
    return () => clearInterval(intervalRef.current);
  });

  const handleCancel = () => clearInterval(intervalRef.current);
  
  return (
    <>
      //...
    </>
  );
}

另外,文章还说了一些使用 ref 的注意事项。ref 是在 render 后才进行创建或更新,下图展示了 react hook 的生命周期:

可以看到的是,useEffect 是在 commit 阶段进行的,但是 react 会在 render 后的一个阶段对 ref 进行一些内部操作。如果我们在 commit 阶段之前对 ref 进行更新,那么就有可能产生一些未知副作用,因此推荐在 useEffect 内部对 ref 进行创建和更新。


Tip

这周积累了一下 vim 下的一些命令,之前不知道如何记住,但是跟着极客时间上面的专栏,理解了每个基础指令对应的意义,然后组合起来的情况也就明白为什么可以这样用。

移动

  • b/w: 以单词为单位 向后/向前 移动。B/W 表示特殊字符也算做是单词的一部分。
  • f/t: 后面可加字符,表示这一行中下一个出现的字符,前面可加数字。f 表示光标去到单词上方,t 表示光标去到字符前一个字符上方。F/T 表示反向搜索。
  • (/): 移动到 上一句/下一句。在文字编辑的时候比较常用。
  • {/}: 移动到 上一段/下一段。在文字编辑的时候比较常用。
  • ctr + b/B: 向上翻页;ctr + f/F 向下翻页。
  • ctr + u/U: 向上翻半页;ctr + d/D 向下翻半页。相比上面的,这个更好记(u -> up, d -> down),也更实用。
  • 数字 + G: 跳转到指定行。
  • 数字 + |: 跳转到指定列。
  • H/M/L: 光标移动到当前屏幕的 上部/中部/下部 位置
  • ctr + e/E: 向下移动屏幕但不移动光标;ctr + y/Y 向下移动屏幕但不移动光标

文本编辑

  • d: 后加动作表示删除;D/d$ 表示删除至行尾
  • c: 后加动作进行修改;C/c$ 表示删除至行尾进入插入模式
  • s/cl: 删除一个字符然后进入插入模式;S/cc 表示删除整行进入插入模式
  • a: 在当前字符后面进入插入模式;A/$a 把光标移到行尾进入插入模式
  • i: 在当前字符前面进入插入模式;I/^i 把光标移到行首进入插入模式
  • o: 在当前行下方插入一个新行,然后在新行进入插入模式;O 在上方插入新行然后进入插入模式
  • r: 替换光标下的字符;R 表示进入替换模式(每次按键替换一个字符)
  • u: 撤销最近的一个操作;U 表示撤销当前行上的所有修改

组合操作/文本对象选择

选择动作的附加键是 i 和 a。i 表示 inside,表示在一个东西的内部。a 就是英文单词的 a,表示一个完整的内容。我们来看一个实际的例子

if (a == 'hello word') {
    for (int i = 0; i < 10; i++) {
        System.out.println(a);
    }
}

假如当前光标在 hello word 的 e 上

  • dw: 表示删除当前所在的单词右边的部分,包括单词后面的空格也会删除。结果如下:
    if (a == 'hword') {
        for (int i = 0; i < 10; i++) {
            System.out.println(a);
        }
    }
    
  • diw: 删除光标所在的单词,仅仅是单词,后面的空格不会被删除。结果如下:
    if (a == ' word') {
        for (int i = 0; i < 10; i++) {
            System.out.println(a);
        }
    }
    
  • daw: 删除光标所在的单词,后面的空格也会被删除。结果如下:
    if (a == 'word') {
        for (int i = 0; i < 10; i++) {
            System.out.println(a);
        }
    }
    
  • diW: 删除光标所在的单词(特殊字符也算单词的一部分),仅仅是单词,后面的空格不会被删除。结果如下:
    if (a ==  word') {
        for (int i = 0; i < 10; i++) {
            System.out.println(a);
        }
    }
    
  • daW: 删除光标所在的单词(特殊字符也算单词的一部分),后面的空格也会被删除。结果如下:
    if (a == word') {
        for (int i = 0; i < 10; i++) {
            System.out.println(a);
        }
    }
    
  • di': 删除引号里面的内容,不包括引号本身。结果如下:
    if (a == '') {
        for (int i = 0; i < 10; i++) {
            System.out.println(a);
        }
    }
    
  • di(: 删除括号里面的内容,不包括括号本身。结果如下:
    if () {
        for (int i = 0; i < 10; i++) {
            System.out.println(a);
        }
    }
    

除了上述之外,还可以在 a 和 i 之前加上数字对多个(层)文本对象进行操作,比如:

  • 把光标移至 for循环内部,然后 c2i{ 结果如下:
    if () {
    
    }
    

另外,c/d/y + a/i + s/p/t/`/"/[/]/</> 都可以模仿并套用上面的操作。

重复操作

  • ; -> 重复最近的字符查找(f/t 等)操作
  • , -> 重复最近的字符查找(f/t 等)操作,反方向
  • n -> 重复最近的字符串查找(/ 和 ? 等)操作
  • N -> 重复最近的字符串查找(/ 和 ? 等)操作,反方向
  • . -> 重复最近的修改操作

好了,先记到这边。总的来说,vim 的操作还是比较灵活的,很多基本操作都可以组合在一起在不同情况下解决不同的问题。因此,这种东西还是要靠多练,多用,这样很多命令就自然而然地记住了,不用特别地刻意去记。毕竟,刻意去记然后又不用也没有什么价值。现在准备把 vim 当成默认的编辑器,不说多了,实践才是王道。


Share

今天来说一个问题,或者说是一个现象吧。不管是在生活中,还是在工作中,我们都需要接收许许多多的信息。但很多时候我们不知道该如何去做取舍,这样则会导致大量的时间和精力的浪费。比如说,同事对一个人或者对一件事情的评价以及看法;上级给自己的绩效;周围人给自己的建议;甚至说是别人的一个眼神或者动作。这些都是我们需要去思考和接收的信息。但是你有没有想过自己具体该如何应对这些信息呢?如果仅仅是被动接收,选择相信自己看到的甚至是听到的,那么很多东西可能会弄得你不得安宁,你可能会花上大量时间去纠缠一个本就不存在的东西。

说了这么多,其实我想说的是,我们 需要学会区分感受和事实。那具体什么是 “感受”,什么又是 “事实” 呢?你可以看看下面这几句话:

  • 小明这次期末考试数学成绩很糟糕
  • 小明这次期末考试数学得了 80 分
  • 张三不被领导看重
  • 在这次考核中,张三得了 4.5 的绩效

这里有 2 个例子,4 句话。前面的是 “感受”,后面的则是 “事实”。可以看到的是感受更多的是一种主观的看法,它反应的是说话的人对事物的看法。而事实则是客观存在的一个东西。因此,好与坏,对与错,低或高,其实都是人的主观评价。因为每个人的教育背景,成长经历都不尽相同,因此对同样的事实会得出不一样的主观感受。知道了这个,很多情况下我们就不会被他人的评价或是看法所影响,因为我们知道这些不是事实,只是他人臆造的一些感受。我们也就能把自己的精力和时间集中在真正重要的地方。

有时想想也挺有意思,这个世界本来是挺简单的一个世界。但是人一多,就会有各种各样不同的看法,看似都挺有道理,这些看法或者是想法综合在一起就让在这之中的人不知如何做选择。