用笨办法应对充满不确定性的未来

【ARTS 周刊】第五期:人生天地间,忽如远行客

2023.08.29

零、碎碎念

我们时代的问题是,在第二等重要的事情上聪明,在头等重要的事情上全然无知;在小事上理智而冷静,在大事上像个疯子在赌博。我们零售的是理智,批发的是疯狂。

──列奥·施特劳斯《自然权利与历史》

我想人的确是会在某个时间点上意识到,人生可以在长达五到十年的尺度上演化。

阿里系企业常常有一个员工入职时长的纪念日,所谓“三年陈,五年醇”。这是时间在职场上的尺度。

惊闻以前高中同学因为抑郁症离世,这才发觉死亡已经逐步逼近。假设人生有百年时光,我可能已经走了三分之一了。

“答对了!” 老师说。“微积分也是如此。你学习微积分,不是因为要在日常生活中使用,而是因为它让你的思维变得更强大。”

一、Algorithm

每周至少做一个算法题。

计划每天做两道新题目,复习之前的题目,这样可以有更好的刷题效果。

本周主题是哈希表、树&二叉树&二叉搜索树,时间稍微有点紧。不过没关系,加加油。

1.1 有效的字母异位词 242. Valid Anagram

字母异位词是指两个单词由同样个数、种类的字母组成,但是顺序不一样的情况。

例如:s = “anagram”, t = “nagaram” 就是字母异位词。Anagram

题目分析:

由字母异位词的特征可以得到,两个单词的字母种类、数量一样,但是顺序不一致。

如何确定两个单词由相同的字母序列组成呢?

可以简化成两个单词中的字母数量都相同。此时只需遍历两个单词数组,对每个字母进行计数即可。

暴力法就我把两个单词的每个单词出现的数量都算出来,然后对比一下

做的过程中做了优化,直接用一个数组来计算,遍历两次,一次在数组上做加法,第二次在数组上做减法。并且使用了字符编码来做数组下标,因此时间复杂度到了 O(n),空间复杂度也是 O(n)

最终做到了接近最优的性能:

Beats 95.53%of users with JavaScript

var isAnagram = function(s, t) {
  const words = [];
  for (let i = 0; i < s.length; ++i) {
    const strCode = s[i].charCodeAt();
    words[strCode] = words[strCode] === undefined ? 1 : words[strCode] +1;
  }
  for (let i = 0; i < t.length; ++i) {
   const strCode = t[i].charCodeAt();

   words[strCode] = words[strCode] === undefined ? 1 : words[strCode] -1;
  }
  return words.every(it=>it===0||it === null);
};

const result = isAnagram('anagram','nagaram');
console.log(result)

1.2 两数之和 1. Two Sum

经典题目,这里用哈希表来实现

JavaScript 版本

var twoSum = function(nums,target){
    var map = new Map();
    for (var i = 0; i < nums.length; ++i) {
        var complement = target - nums[i];
        if(map.has(complement)){
            return [map.get(complement), i];
        }
        map.set(nums[i],i)
    }
    console.log('No complement');
}

Python 版本

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        num_dict = {}  # 哈希表,用于存储元素和索引的映射关系
        for i in range(len(nums)):
            complement = target - nums[i]  # 当前元素的补数
            if complement in num_dict:
                return [num_dict[complement], i]
            num_dict[nums[i]] = i  # 将当前元素及其索引存入哈希表

1.3 三数之和 15. 3Sum

TwoSum 的升级版本。

解析:要求从给定数组中找出三个元素,其值加起来等于 0.

思路一:双指针法

一个外层指针,两个内层指针。

const result = [];
  const n = nums.length;

  // 首先对数组进行排序
  nums.sort((a, b) => a - b);

  for (let i = 0; i < n - 2; i++) {
    // 跳过重复的外层指针元素
    if (i > 0 && nums[i] === nums[i - 1]) {
      continue;
    }

    let left = i + 1;
    let right = n - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]]);

        // 跳过重复的内层指针元素
        while (left < right && nums[left] === nums[left + 1]) {
          left++;
        }
        while (left < right && nums[right] === nums[right - 1]) {
          right--;
        }

        left++;
        right--;
      } else if (sum < 0) {
        left++;
      } else {
        right--;
      }
    }
  }

  return result;

思路二:分解成两次 Two Sum,即先遍历数组,对每个元素 a 都计算一遍 TwoSum,找到其对应的值 b,再对 b 和剩下的元素计算一次 TwoSum,这样将三数之和分解成找 2 个数字

1.4 验证二叉搜索树 98. Validate Binary Search Tree

这道题需要确定什么是二叉搜索树,它有什么性质。

二叉搜索树是二叉树的一种,他的每个节点的值都满足以下条件:

  • 左子树上所有节点都小于根节点的值
  • 右子树上的节点都大于跟节点的值
  • 左右子树本身也是二叉搜索树

而要验证是否是有效的二叉搜索树,只要进行一次中序遍历,验证每个子树是否符合上面三个条件即可。遍历二叉树通常可以使用递归方法。可以写出以下 js 代码

var isValidBST = function(root) {
  // 初始化成负无穷,避免第一个节点的值小于初始化的prev而引起错判。
  let prev = -Infinity;
    function inorderTraversal(node) {
        if (node === null) {
            return true;
        }
        // 遍历左子树
        if (!inorderTraversal(node.left)) {
            return false;
        }
        // 检查当前节点值是否大于前一个节点值
        if (node.val <= prev) {
            return false;
        }
        prev = node.val;
        // 遍历右子树
        return inorderTraversal(node.right);
    }

    return inorderTraversal(root);
}

时间复杂度是 O(n),因为在中序遍历二叉树时,每个节点都会被访问并且只被访问一次。

Python 版本

class Solution(object):
    def isValidBST(self, root):
        """
        :type root: TreeNode
        :rtype: bool
        """
        def inorder_traversal(node,prev):
            if node is None:
                return True
            if not inorder_traversal(node.left,prev):
                return False
            if node.val <= prev[0]:
                return False
            prev[0] = node.val

            return inorder_traversal(node.right,prev)
        prev = [float('-inf')]
        return inorder_traversal(root,prev)

也可以用迭代法来写中序遍历:

js 版本

1.5 二叉树的最近公共祖先

236. Lowest Common Ancestor of a Binary Tree

使用递归来实现

class Solution(object):
    def lowestCommonAncestor(self, root, p, q):
        """
        :type root: TreeNode
        :type p: TreeNode
        :type q: TreeNode
        :rtype: TreeNode
        """
        if root is None or root == p or root == q:
            return root

        left_LCA = self.lowestCommonAncestor(root.left, p, q)
        right_LCA = self.lowestCommonAncestor(root.right, p, q)

        if left_LCA and right_LCA:
            return root
        elif left_LCA:
            return left_LCA
        else:
            return right_LCA

迭代法实现

class Solution(object):
    def lowestCommonAncestor(self, root, p, q):
        """
        :type root: TreeNode
        :type p: TreeNode
        :type q: TreeNode
        :rtype: TreeNode
        """
        # 使用字典来存储每个节点的父节点
        parent = {root: None}

        # 使用栈进行迭代
        stack = [root]

        # 建立每个节点的父节点信息
        while p not in parent or q not in parent:
            node = stack.pop()
            if node.left:
                parent[node.left] = node
                stack.append(node.left)
            if node.right:
                parent[node.right] = node
                stack.append(node.right)

        # 存储所有p节点的祖先
        ancestors = set()
        while p:
            ancestors.add(p)
            p = parent[p]

        # 找到第一个同时也是q节点的祖先
        while q not in ancestors:
            q = parent[q]

        return q

1.6 二叉搜索树的最近公共祖先

235. Lowest Common Ancestor of a Binary Search Tree

解法和 236 一样,逐步去迭代即可,甚至解法都一样…

不知道为啥,可能有别的玄机?

1.7 Pow(x,n) 50. Pow(x, n)

使用一次循环实现,并且用二分法来计算幂,还需要考虑 n 为负数的情况

class Solution(object):
    def myPow(self, x, n):
        """
        :type x: float
        :type n: int
        :rtype: float
        """
        if n == 0:
            return 1.0

        if n < 0:
            x = 1 / x
            n = -n

        result = 1
        while n > 0:
            if n % 2 == 1:
                result *= x
            x *= x
            n //= 2
        return result

此时的时间复杂度是 O(log n)

快速幂方法

class Solution(object):
    def myPow(self, x, n):
        """
        :type x: float
        :type n: int
        :rtype: float
        """
        def fast_pow(x, n):
            if n == 0:
                return 1.0

            half_pow = fast_pow(x, n // 2)
            if n % 2 == 0:
                return half_pow * half_pow
            else:
                if n > 0:
                    return x * half_pow * half_pow
                else:
                    return (1 / x) * half_pow * half_pow

        if n < 0:
            x = 1 / x
            n = -n

        return fast_pow(x, n)

在 n 比较大时,快速幂算法比二分法更具优势

其他的优化方法:

分治法加速版,在分治的基础上,利用指数的奇偶性来进一步减少计算次数

def power(x, n):
    if n == 0:
        return 1.0
    if n < 0:
        x = 1 / x
        n = -n

    half_pow = power(x, n // 2)

    if n % 2 == 0:
        return half_pow * half_pow
    else:
        return x * half_pow * half_pow

时间复杂度仍然是 O(log n)

1.8 求众数 169. Majority Element

这里的众数指在长度为 n 的数组中出现次数大于 n/2 的元素

Boyer-Moore 投票算法

class Solution(object):
    def majorityElement(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        candidate = None
        count = 0
        for num in nums:
            if count == 0:
                candidate = num
                count = 1
            elif num == candidate:
                count += 1
            else:
                count -=1
        return candidate

这个方法只需要遍历一次,因此时间复杂度是 O(n)

哈希表统计法

将每个元素出现的个数存到哈希表里,然后取出出现次数超过一半的元素

class Solution(object):
    def majorityElement(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        count_map = {}
        threshold = len(nums) // 2
        for num in nums:
            if num in count_map:
                count_map[num] += 1
            else:
                count_map[num] = 1
            if count_map[num] > threshold:
                return num
        return None

这个方法的时间复杂度是 O(n),每个元素遍历一次

二、Review

每周阅读并点评至少一篇英文文章。

看别人写的文章并点评

为什么学微积分

链接

“答对了!” 老师说。“微积分也是如此。你学习微积分,不是因为要在日常生活中使用,而是因为它让你的思维变得更强大。”

刷算法题也一样,日常工作中不大会有需要你写算法的时候,但是算法练习可以让思维变得强大,让你更关注性能和优化。

How is LLaMa.cpp possible

https://finbarr.ca/how-is-llama-cpp-possible/

大语言模型运行起来往往需要昂贵的大显存 GPU,但是 llama.cpp 工程实现了仅使用 CPU 来实现推理,甚至能够在树莓派上以较低的速度运行。它是怎么做到的呢?

整个环节中的影响因素是显存带宽和计算单元。

三、Tip

每周学习并分享至少一个技术技巧

日常开发中的技巧

3.1 如何将数组转换成对象?

使用 for xxx in 配合 Object.entries()

export function convertArrayToObject(inputArray) {
    const licenseExtMap = {};

    for (const [index, item] of inputArray.entries()) {
        if (item && item.licenseUrl) {
            const licenseType: number = index;
            licenseExtMap[licenseType] = {
                'licenseUrl': item.licenseUrl,
                'licenseType': String(licenseType)
            };
        }
    }
    return licenseExtMap;
}

3.2 任务的优先级比单纯的追求做更多的事情更重要

最近看到有个说法:有些博主有空去研究效率工具的前提之一是"保持单身"。

原因是有女朋友或者结婚、有小孩后个人时间急剧减少,已经没有单身时有大把的个人时间花在折腾工具上。这似乎可以佐证为什么企业偏向招聘年轻人,因为他们皮实耐用,没有家庭需要照顾,可以花更多的精力在工作上。

当然也看到另一个角度的说法,有人认为结婚后的人会更注重有效率的工作,把精力花在真正重要的地方。因为精力的确已经不够了,在有限的时间精力里想要得到更多,就需要对爱好、工作目标等做最优的选择

四、Share

每周分享至少一篇有观点和思考的技术文章。

最好是自己写的文章。

Neuro 一个 AI Vtuber

这一篇不是技术文章,而是之前刷 B 站看到了这个 AI Vtuber,也就是所谓的人工智能虚拟 Vtuber,至于效果如何可以看下视频,我是觉得很惊艳。

这个 Vtuber 作者是 vedal,他写了一个对想要做类似的 AI 助手的一篇建议。

https://vedal.xyz/advice/

总体来看是比较概略性的,因为作者说自己是自学编程,需要从兴趣出发。当然也提到了 Python、OpenAI 等关键词。

个人博客的定位与信噪比

从信息消费者角度来看,如果一个博客老是在伤春悲秋而对我有用的内容非常少,除非我自己也是这方面的爱好者,想看一看不同的人的生活方式是怎样的,否则我是不会将其放到订阅列表的。

这种做法未免有些功利,然而就前文所说,一个中年人的时间总是有限的,其实没有那么多精力花在关注别人的事情上面。

不过作为自己写个人博客的话,似乎可以更自由一点,毕竟自己的博客自己做主。