TS类型体操实现两数之和

1,055 阅读3分钟

前言

两数之和是一道非常经典的面试算法题(程序员梦的起点),题意大概是这样:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数。

普通版本的解法我们可以使用哈希表,如果整数数组 nums是有序的,我们还可以使用双指针解法。

// 哈希表
// https://leetcode-cn.com/problems/two-sum
const twoSum = function(nums, target) {
    let map = {};
    let arr = [];
    nums.some((item, idx) => {
        if (typeof map[target-item] !== 'undefined') {
            arr.push(...[map[target - item], idx]);
            return true;
        }
        map[item] = idx;
        return false;
    });

    return arr;
};

// 双指针
// https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted
const twoSum = function(numbers, target) {
    let left = 0;
    let right = numbers.length - 1;
    while(left < right) {
        let num1 = numbers[left];
        let num2 = numbers[right];
        if (num1 + num2 === target) {
            return [left+1, right+1];
        }
        if (num1 + num2 < target) {
            left++;
        } else {
            right--;
        }
    }
};

在AnythonyFu(面试者)和Herrington(面试官)的技术二面模拟直播中,有一个面试题目是要求AnythonyFu实现TS类型体操版本的两数之和,相比普通版本的两数之和,这道面试题难度确实拔高了不少(面试一般也不会去问这种题目),要知道在TS类型编程中,实现两个数的加法都费劲,这一度考倒了AnythonyFu大佬,而视频直播的后面,Herrington也展示了两数之和的类型体操版本,以及做TS类型体操时的解题思路。本文就以技术探讨的心态,来讲讲TS类型体操如何实现两数之和。

开始表演

首先,在做类型体操之前,我们要先写出两数之和的functional版本(采用深递归):

function twoSumFunctional (nums, target, dict = {}) {
  if (nums.length <= 0)
    return false
  if (dict[target-nums[0]])
    return true
  return twoSumRe(nums.slice(1), target, { ...dict, [nums[0]]: true })
}

然后,我们根据functional版本的思路去写出类型体操版本:

type TwoSum<Nums extends number[], Target extends number, Dict = never> =
    Nums['length'] extends 0
        ? false :
        Sub<Target, Nums[0]> extends Dict
            ? true :
            TwoSum<Tail<Nums>, Target, Dict | Nums[0]>

好了,类型体操版本的两数之和整体思路就写出来了,接下来就是实现DictSubTail

Dict使用联合类型实现,Dict初始为never,因为任何类型与never联合都是它自己,例如never | number | string的结果为number | string

image.png

Sub就是实现加减法,在TS类型体操里无法直接实现数字的加减法,需要间接把数字转为数组,去求数组长度来求加减法的结果。

// 数组一开始是空数组,然后一直添加any进去,直到数组长度等于N
type ToTuple<N extends number, List extends any[] = []> = List['length'] extends N
    ? List : ToTuple<N, [...List, any]>

image.png

// 实现加法
type Add<A extends number, B extends number> = [...ToTuple<A>, ...ToTuple<B>]['length']

image.png

// 实现减法
type Sub<A extends number, B extends number> = ToTuple<A> extends [...ToTuple<B>, ...infer Rest] ? Rest['length'] : -1

image.png

需要注意的是,这里实现的加减法类型体操只支持正整数相加、正整数相减,并且减法必须是大数减去小数。

Tail的实现就是去除数组的第一个元素,保留剩下的元素:

type Tail<Nums extends any[]> = Nums extends [Nums[0], ...infer Rest] ? Rest : []

image.png

或者这样子实现也是可以的:

type Tail<Nums extends any[]> = Nums extends [infer First, ...infer Rest] ? Rest : []

image.png

最后,整体思路加上各个类型的实现细节就完成了类型体操实现两数之和这道题目了。完整版:

type ToTuple<N extends number, List extends any[] = []> = List['length'] extends N
    ? List : ToTuple<N, [...List, any]>
    
type Sub<A extends number, B extends number> = ToTuple<A> extends [...ToTuple<B>, ...infer Rest] ? Rest['length'] : -1

type Tail<Nums extends any[]> = Nums extends [Nums[0], ...infer Rest] ? Rest : []

type TwoSum<Nums extends number[], Target extends number, Dict = never> =
    Nums['length'] extends 0
        ? false :
        Sub<Target, Nums[0]> extends Dict
            ? true :
            TwoSum<Tail<Nums>, Target, Dict | Nums[0]>

image.png

我们也是可以把具体的两个值给求出来的:

image.png

当然,不同的人会提出不同的解法,如果依照AnythonyFu老师的functional版本去解题:

image.png

function twoSumFunctional (nums, target) {
  return nums.some(item => nums.includes(target - nums));
}
type ToTuple<N extends number, List extends any[] = []> = List['length'] extends N
    ? List : ToTuple<N, [...List, any]>
    
type Sub<A extends number, B extends number> = ToTuple<A> extends [...ToTuple<B>, ...infer Rest] ? Rest['length'] : -1

type Tail<Nums extends any[]> = Nums extends [Nums[0], ...infer Rest] ? Rest : []

type TwoSum<Nums extends number[], Target extends number, Dict = Nums[number]> =
    Nums['length'] extends 0
        ? [] :
        Sub<Target, Nums[0]> extends Dict
            ? [Sub<Target, Nums[0]>, Nums[0]] :
            TwoSum<Tail<Nums>, Target, Dict | Nums[0]>

type Cases = [
    TwoSum<[2,7,11,15], 9>,
    TwoSum<[2,7,11,15], 1>,
    TwoSum<[], 9>,
    TwoSum<[9], 9>,
    TwoSum<[3,2,4], 6>,
    TwoSum<[3,3], 6>
]

image.png

你会发现AnthonyFu老师的版本其实是有bug的,不满足所有测试用例。当用例为[3,2,4], 6时,期望是[2, 4],而不是[3, 3]。哈哈,菜狗如我,也能捕捉到AnthonyFu老师的一点点小问题。

课外题

找出给定数组中仅出现过一次的元素。 例子:输入[1,2,2,3,3,4,5,6,6,6],输出[1,4,5] 用类型体操实现

type Tail<T extends any[]> = T extends [T[0], ...infer Rest] ? Rest : []

type RepeatSet<T extends any[], Total = never, Ret = never> = T['length'] extends 0 ? Ret :
    T[0] extends Total ? RepeatSet<Tail<T>, T[0] | Total, T[0] | Ret> : RepeatSet<Tail<T>, T[0] | Total, Ret>

type Result<T extends any[], S = RepeatSet<T>, List extends any[] = []> = T['length'] extends 0 ? List :
    T[0] extends S ? Result<Tail<T>, S, List> : Result<Tail<T>, S, [...List, T[0]]>

type Cases = [
    Result<[1,2,2,3,3,4,5,6,6,6]>,
    Result<[2,2,3,3,6,6,6]>,
    Result<[1,2,3]>,
]

image.png

相关链接