使用 Typescript 实现 TreeMap

853 阅读5分钟

TreeMap 简介

首先看下列的 java 代码,你认为输入顺序会是什么?

import java.util.TreeMap;
import java.util.Set;;

public class UseTreeMap {
    public static void main(String[] args) {
        TreeMap<Integer, String> treeMap = new TreeMap<>();
        treeMap.put(5, "five");
        treeMap.put(3, "three");
        treeMap.put(7, "seven");
        Set<Integer> set = treeMap.keySet();
        for(Integer key: set) {
            System.out.println("treeMap value: " + treeMap.get(key));
        }
    }
}

以一个前端开发者看的话,应该是先 five ,然后 three ,最后 seven,可实际的顺序是先 three ,然后 five ,最后 seven 。原因是 java 的 TreeMap 会对传入的 key 进行大小排序。那么我们能不能使用 typescript 来实现一个 TreeMap 呢?答案当然是肯定的

实现增删改查

由于 Map 的使用类似于 class ,因此 TreeMap 也使用 class 来实现,首先我想到的是继承,至于调整 key 的顺序,我们在内部维护一个数组来记录 key ,并且由于 Map 的 key 是唯一的,数组的 key 也必须是唯一的。

class TreeMap<V> extends Map<number, V> {
  // 保证 key 的唯一性
  private readonly keysSet: number[] = [];
}

为什么不用 Set 这一数据结构呢?主要是数组提供了各种 api 方便对内部元素进行操作,而 Set 并没有

接着来考虑一个问题,哪些操作会对 Map 的 key 有影响,有三个 api : setdeleteclearconstructor ,其中 set 只有在 Map 中不存在传入的 key 时候才有影响

让我们先实现简单的 clear ,当调用该方便时,Map 的 size 也就为 0 了,那么相对应的,需要把 keysSet 清空

private clearKeys(): void {
    this.keysSet.length = 0
}

clear(): void {
    this.clearKeys();
    super.clear()
}

接着实现 set ,我们需要确保 keysSet 与 Map 的 set 在数量上和值是对应的,因此先判断 keysSet 有没有存在该 key ,存在就不需要操作了,然后根据情况插入到正确的位置

private insertKey(key: number): void {
    if (this.keysSet.includes(key)) {
        return
    }
    const index = this.keysSet.findIndex(k => k > key)
    if (index === -1) {
        this.keysSet.push(key)
    } else {
        this.keysSet.splice(index, 0, key)
    }
}

set(key: number, value: V): this {
    this.insertKey(key)
    return super.set(key, value)
}

再来实现 delete ,先判断要删除的 key 在 Map 中是否存在,存在执行删除操作

private deleteKey(key: number): void {
    const index = this.keysSet.findIndex(k => k === key)
    if (index !== -1) {
        this.keysSet.splice(index, 1)
    }
}

delete(key: number): boolean {
    if (this.has(key)) {
        this.deleteKey(key)
    }
    return super.delete(key)
}

最后再来实现 constructor ,其实可看做是初始进行了多次 set 操作,实现也很简单

constructor(entries?: readonly (readonly [number, V])[] | null) {
    super()
    for (const [key, value] of (entries ?? [])) {
        // 注意,这里不能使用 super.set ,而必须是 this.set ,因为需要对 keysSet 进行操作
        this.set(key, value)
    }
}

自此,我们已经实现了增删改的操作,还差个 get ,但 get 的逻辑与 Map 的逻辑并无任何不同,也不会对 keysSet 产生任何影响,因此可以不用实现

实现迭代功能

迭代首先想到的 forEach ,由于 keysSet 是有序且肯定有值的,因此可以直接遍历 keysSet,将值放入传入的回调函数就行了

forEach(callbackfn: (value: V, key: number, map: Map<number, V>) => void): void {
    const keysSet = this.keysSet
    for (let i = 0; i < keysSet.length; i++) {
        callbackfn(super.get(keysSet[i]) as V, keysSet[i], this)
    }
}

forEach 简单,但其他的就难了。以 keys 为例 ,这个方法需要返回 IterableIterator<number> ,那么能不能直接 return super.keys() 呢?

很遗憾,并不能,直接返回的话,key 的顺序是按插入的顺序来的,而不是正序返回,那么能不能手动实现接口呢?

这是 lib.es2015.iterable.d.ts 中的 IterableIterator 接口,实在是过于复杂,很显然,不好实现

interface SymbolConstructor {
    /**
     * A method that returns the default iterator for an object. Called by the semantics of the
     * for-of statement.
     */
    readonly iterator: unique symbol;
}

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface Iterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

但也不是没有曲线救国的方法,我在翻看上面那个 dts 文件的时候,也看到了下方这个

interface Array<T> {
    /**
     * Returns an iterable of values in the array
     */
    values(): IterableIterator<T>;
}

那么,TreeMap 的 keys 就好实现了,我们就有个现成的有序的 keysSet

keys(): IterableIterator<number> {
    return this.keysSet.values()
}

valuesentries 的实现同理

values(): IterableIterator<V> {
    return this.keysSet.map(set => super.get(set) as V).values()
}

entries(): IterableIterator<[number, V]> {
    return this.keysSet.map((set: number): [number, V] => [set, super.get(set) as V]).values()
}

实现 Object.keys() 的效果

说实话,Map 的 keys 和 Object 的 keys 在使用方便程度上根本不是同一等级,既然我们用了 keysSet 作为顺序的标识,那么当然可以把它导出,只要有这种需求。需要注意的是,虽然我用了 readonly 来限定它,但是 push shift pop unshift 等方法仍然可以改变它,如果外部改变了 keysSet 的话,这意味着我们上述的迭代功能将全部失效。解决方法是在返回类型前面加一个 readonly 限定,这样外界将无法对 keysSet 做任何操作,包括我前面提到的那些数组方法。假如使用 javascript 来实现的话,就无法保证有效性了

getKeysSet(): readonly number[] {
    return this.keysSet
}

那么,Object.values()Object.entries() 也是很容易实现的

单元测试

最后让我们来做个单测,由于 Map 中 key 的顺序是按插入顺序来的,我们按顺序升序插入 key ,它就应该符合 TreeMap 的特征,我们再不按升序顺序插入 key 在 TreeMap 中,然后比较两者的 api 的返回值,如果一致的话,说明我们的实现是成功的

// TreeMap.spec.ts
import TreeMap from "../data_structure/TreeMap";

const map = new Map<number, string>([[1, "first"]])
const treeMap = new TreeMap<string>([[5, "five"]])

map.set(3, "three").set(5, "five")
treeMap.set(3, "three").set(1, "first")

test("get", () => {
    [1, 3, 5, 7, 9].forEach(key => {
        expect(map.get(key)).toBe(treeMap.get(key))
    })
})

test("set", () => {
    map.set(7, "seven")
    treeMap.set(7, "seven")
    expect(map.get(7)).toBe(treeMap.get(7))
})

test("delete", () => {
    expect(treeMap.getKeysSet()).toEqual([1, 3, 5, 7])
    treeMap.delete(3)
    expect(treeMap.get(3)).toBe(undefined)
    expect(treeMap.getKeysSet()).toEqual([1, 5, 7])
    // 保持与 treeMap keys 数量一致
    map.delete(3)
})

test("forEach", () => {
    const kvs1: [number, string][] = []
    const kvs2: [number, string][] = []
    map.forEach((value, key) => {
        kvs1.push([key, value])
    })
    treeMap.forEach((value, key) => {
        kvs2.push([key, value])
    })
    expect(kvs1).toEqual(kvs2)
})

test("keys", () => {
    const iterator1 = map.keys()
    const iterator2 = treeMap.keys()
    expect(map.size).toBe(treeMap.size)
    for (let i = 0; i < map.size; i++) {
        expect(iterator1.next().value).toBe(iterator2.next().value)
    }
})

test("values", () => {
    const iterator1 = map.values()
    const iterator2 = treeMap.values()
    expect(map.size).toBe(treeMap.size)
    for (let i = 0; i < map.size; i++) {
        expect(iterator1.next().value).toBe(iterator2.next().value)
    }
})

test("entries", () => {
    const iterator1 = map.entries()
    const iterator2 = treeMap.entries()
    expect(map.size).toBe(treeMap.size)
    for (let i = 0; i < map.size; i++) {
        expect(iterator1.next().value).toEqual(iterator2.next().value)
    }
})

test("clear", () => {
    treeMap.clear()
    expect(treeMap.size).toBe(0)
    expect(treeMap.getKeysSet()).toEqual([])
})

下图是我跑的测试结果,全部通过

Snipaste_2022-08-02_01-02-15.png

优化点

可以看到,上面的 TreeMap 实现只能是 number 作为 key 值,而且是升序的。如果你有兴趣的话,可以去实现让它能够降序,或者是其他的比如 string 作为 key 值,或者是任意的排序规则,我相信这不难。附上 TreeMap 代码 ,也欢迎在评论区互相探讨。