用typescript类型来实现快排

avatar
前端工程师 @字节跳动

作者:东东么么哒

写在前面 本文执行环境typescript,版本4.7.4


元组快排

能否将元组 [3, 1, 2, 4] 通过泛型转换成 [1, 2, 3, 4]

如何实现快排?

• 遍历元组

• 元组每个值的大小比较

• 每次比较中挑选出符合条件的值,也就是实现 Filter

实现逻辑

数字的大小比较

在typescript类型中没有比较符,那如何判断 5 和 6 谁更大?

typescript类型不知道,所以需要找到在typescript中已经存在的递增数列,通过这个数列来实现

怎么理解呢

类似有 张三 和 李四 两个人,要比较他们谁的位置靠前,需要有一个他们排队的数列,然后依次查看,先看到 张三,那么 张三 的位置明显靠前

typescript中有这样的递增数列吗?

有的:元组下标(取元祖长度,元祖push的时候长度是递增的数列),只需要递归元组,就可以实现依次点名

A 是否 小于或等于 B

• 无限递归,直到匹配到 A 或者 B

• 先匹配到 A 返回true(表示A小于或等于B),否则返回false

image.png

A 是否 大于或等于 B

逻辑同理

• 无限递归,直到匹配到 A 或者 B

• 先匹配到 A 返回false,否则返回true(表示A大于或等于B)

image.png 当然也可以依赖 SmallerThan 泛型来实现

• 与SmallerThan的布尔值取反

image.png

Filter

• 根据元组长度递归,直到递归次数与元祖长度相等

• 当满足条件(比如:大于等于某个值),将值存储到新元组中,否则不操作

• 依赖上面实现的大小值比较 分别实现 对应的Filter

基于已有的 LargerThan 泛型实现 FilterLargerThan

image.png

同理,基于已有的 SmallerThan 泛型实现 FilterSmallerThan

image.png

优化Filter

Filter写的很重复了,根据DRY原则(don't repeat yourself),需要将泛型作为参数传进去,来避免冗余重复的代码

重构数字的大小值比较

如何把泛型作为参数传入,然后在参数中限定?

嗯...好问题

// 目标是实现这种type Test<A extends number, T extends ?> = T<A>;

貌似不太行,那变个思路:

实现一个泛型对象,每个键值对实现对应的处理,最后只需要传入这个对象的key来获取泛

型,在参数的限定可以变成对key的限定,通过keyof 对象即可实现

• 实现一个泛型对象Demo

• 每个键值对实现对应的处理,如 a: F

• 传入这个对象的key来获取泛型,如 T extends keyof Demo

image.png •   根据上述原则,将对应的泛型组合成泛型对象 Compare •   SmallerThan 实现之前的 SmallerThan 泛型
•   LargerThan 实现之前的 LargerThan 泛型

image.png

重构Filter

• 将对应的泛型改成 Compare

• key需要手动传入,即为字符串 SmallerThan 和 LargerThan

image.png

快排

• 递归元组,直到拆解为长度 0 或者 1 的元祖

• 元组长度小于等于 1 的时候返回自身

• 默认取第一项作为对比值,并用泛型 UNSHIFT 移除第一项

• 通过filter和第一项比较筛选出对应的新元祖

image.png

测试快排

image.png

看起来一切正常,可以发现遗漏了负数

测试负数的时候问题出现了

因为最开始的大小值对比,是从0开始无限递归的

结束条件是命中其中一个数,然而负数是永远不会命中,这就是致命bug!

优化:负数

负数的判断

负数的特点:多了一个符号,也就是 "-"

• 转换成字符串后取第一个字符判断是否为 "-"

• 通过 infer 来获取第一个字符

image.png

字符串转数字

但是这样拿到的是字符串,还要把字符串转成数字

和大小比较的逻辑一样

• 无限递归,每次递归传入新元组

• 元组长度(模板字符串) 等于 参数后结束递归,并返回元组长度

image.png

获取负数的值

判断是负数后要拿到负数的值

• 和负数符号判断类似,获取除开符号之后的字符串

• 字符串通过泛型 ToNumber 转数字

image.png

完善获取绝对值

• 非负数返回自身,负数通过泛型 GetFushu 来获取

type GetAbs<T extends number> = isFuShu<T> extends true ? GetFushu<T> : T;

重构数字的大小比较

负数的对比和正数相反,且正数一定比负数大

• 判断比较的值是负数还是非负数

• 非负数通过泛型 CompareV2 比较大小

• 负数获取绝对值后,通过泛型 CompareV2 取反比较大小

• 非负数一定大于负数

• 泛型 SmallerThan 实现非负数的比较,泛型SmallerThanV2 实现负数和非负数的比较

• 泛型 LargerThan 和泛型 LargerThanV2 同理

image.png

测试用例

image.png

重构快排

重构泛型 FilterV2(更换 Compare -> CompareV2)

image.png

测试快排V2

image.png