WebGL2.0从入门到精通-2、数据结构与算法(二、关联数组/字典的封装)

二、关联数组/字典的封装

在 JS/TS 中,一切都是对象,函数是对象,数组是对象,object、string 和 number 都是对象,自定义的类型更是对象。

集合表示一组互不相同的元素(不重复的元素)。在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。

字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。

字典也称作映射、符号表或关联数组。

在 TS 语言中,内置了索引签名的功能,该功能可以看成关联数组的经典应用。TS 中声明索引签名的方法:

        // TS中声明一个键为string类型,值为string类型的索引签名对象
        let strStrDictionary: { [ index : string ] : string };
        // TS中声明一个键为number类型,值为string类型的索引签名对象
        let numStrDictionary: { [ index: number ]: string };
        // 在使用上述两个索引签名对象前,一定要初始化这两个对象,否则报错
        strStrDictionray= { };
        numStrDictionary= { };
        // 接下来就可以使用键值对了
        strStrDictionray[ "a" ] ="a";
        strStrDictionray[ 'd' ] ="d";
        strStrDictionray[ "c" ] ='c';
        strStrDictionray[ 'b' ] ="b";
    ​
        numStrDictionary[ 0 ] ='a';
        numStrDictionary[ 3 ] ='d';
        numStrDictionary[ 2 ] ='c';
        numStrDictionary[ 1 ] ='b';
        // 输出结果:{"a":"a", "d":"d", "c":"c", "b":"b"}
        console.log( JSON.stringify( strStrDictionray ) );
        // 输出结果: {"0":"a", "1":"b", "2":"c", "3":"d"}
        console.log( JSON.stringify( numStrDictionary ) );

注意:在 TS 索引签名中,作为键的数据类型只能是 string 或 number 类型(如上代码所示),而对应的值可以是任意类型。

在 ES6(ES 2015)规范中新提供了一个关联数组结构,即 Map 对象。

1、Object、索引签名、ES6 Map 对象之间的区别

  • 在 JS/TS 中,可以直接用 object 对象,通过字符串的键查找到对应的值,但是如果使用 object 的话,则键只能使用 string 类型。

  • TS 中的索引签名只能使用 string 及 number 类型作为键的数据类型。

  • ES6 规范中的 Map 对象的键的数据类型可以是 JS/TS 中的任意类型。例如键可以是 string、number 等基本数据类型,也可以是 object、String、Number,以及各种自定义的引用类型。

  • ES6 中的 WeakMap 对象的键只能使用引用类型。例如,string、number 等基本数据类型不能作为键的类型。

如果要在 TS 中使用 Map 对象,则 tsconfig.json 中的 target 项的值需要设置为 ES 2015 及以上。

2、索引签名

索引:对象或数组的对应位置的名字

数组的索引就是 number 类型的 0,1,2,3...对象的索引就是 string 类型的属性名

数字索引签名:通过定义接口用来约束数组
    type numberIndex{
        [index:number]:string
    }
    consttestArray:numberIndex= ["1","2",3]// 不能将类型“number”分配给类型“string”。ts(2322) 所需类型来自此索引签名

可以看到 testArray 数组的第三位不符合 numberIndex 的约束.

索引签名的名称如[index:number]:string里的index除了可读性外,并无任何意义.但有利于下一个开发者理解你的代码.

字符串索引签名:用于约束对象
    type objectType{
        [propName:string]:number
    }
    consttestObj:objectType= {
        "name":100,
        "age":"200"// 不能将类型“string”分配给类型“number”。ts(2322) 所需类型来自此索引签名。
    }

可以看到 testObj 的第二个对象不符合 objectType 的约束.

注意事项

可以看到上述的例子我都没有在类型别名中添加其他的约束条件,仅写了一个索引签名约束

    type attentionType{
        name: string; // Ok
        age?: number; // 类型“number | undefined”的属性“age”不能赋给“string”索引类型“string”。ts(2411)
        sex?: undefined; // OK
        [propName: string]: string|undefined;
    }

上述例子说明了,一旦定义了索引签名,那么确定属性和可选属性的类型都必须是它的类型的子集

可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。

    interface Animal {
      name: string;
    }
    interface Dog extends Animal {
      breed: string;
    }
    ​
    interface NotOkay {
      [x: string]: Dog;
      [x: number]: Animal; // Error
    }
    ​
    interface Okay {
      [x: string]: Animal;
      [x: number]: Dog; // OK
    }

3、关联数组底层实现的方法

一般而言,关联数组基本都是使用红黑树(Red-Black Tree)或 哈希表(Hash Table)这两个数据结构实现的。

在 JS 语言中:

        // TS中声明一个键为string类型,值为string类型的索引签名对象
        let strStrDictionary: { [ index : string ] : string };
        // 在使用上述索引签名对象前,一定要初始化这两个对象,否则报错
        strStrDictionray = { };
        // 接下来就可以使用键值对了
        strStrDictionray[ "a" ] = "a";
        strStrDictionray[ 'd' ] = "d";
        strStrDictionray[ "c" ] = 'c';
        strStrDictionray[ 'b' ] = "b";
    ​
    ​
        // 输出结果:{"a":"a", "d":"d", "c":"c", "b":"b"}
        console.log( JSON.stringify( strStrDictionray ) );

可以看到打印结果为 a、d、c、b,而不是(a、b、c、d)

在 C++ 语言中:

看一下C++ STL(标准模板库)中基于红黑树和哈希表实现的关联数组之间的区别与联系

using namespace std;
    typedef map<string, string> StringMap;
    typedef StringMap::iterator StringMapIter;
    typedef unordered_map<string, string> StringHashMap;
    typedef StringHashMap::iterator StringHashMapIter;
    int main()
    {
        printf("使用红黑树\n");
        StringMap strmap;
        // 乱序插入字母
        strmap["a"] = "a";
        strmap["d"] = "d";
        strmap["c"] = "c";
        strmap["b"] = "b";
        // 使用迭代器遍历所有key
        StringMapIter mapIter = strmap.begin();
        while (mapIter ! = strmap.end()) {
          printf("key = %s\n", mapIter->first.c_str());
          ++mapIter;
        }
        printf("使用哈希表\n");
        StringHashMap hashmap;
        hashmap["a"] = "a";
        hashmap["d"] = "d";
        hashmap["c"] = "c";
        hashmap["b"] = "b";
        StringHashMapIter hashIter = hashmap.begin();
        while (hashIter ! = hashmap.end()) {
          printf("key = %s\n", hashIter->first.c_str());
          ++hashIter;
        }
        getchar();
        return 0;
    }

通过上述两个Demo,我们可以清晰地知道如何区分关联数组底层所使用的数据结构:

  • 如果关联数组的键输出顺序自动排序,则肯定是红黑树实现;

  • 如果关联数组的键输出与插入时保持一致,则肯定是哈希表实现。

4、Dictionary 字典对象的封装实现

新建了 Dictionary.ts 文件,文件内容如下:

/*
 * @Description:
 * @Author: tianyw
 * @Date: 2023-01-27 11:03:44
 * @LastEditTime: 2023-01-27 11:20:23
 * @LastEditors: tianyw
 */
export class Dictionary<T> {
  // 内部封装了索引签名或 ES6 Map 对象,其键的数据类型为 string,泛型参数可以是任意类型
  // 强制键的类型必须为 string 类型
  private _items: { [k: string]: T } | Map<string, T>;
  // 用来跟踪目前的元素个数,在成功调用 insert 方法后递增,在 remove 方法后递减
  private _count: number = 0;

  // 默认使用 ES6 Map 对象来管理相关数据
  // 构造函数 根据参数 uesES6Map 决定内部使用哪个关联数组
  public constructor(useES6Map: boolean = true) {
    if (useES6Map === true) {
      this._items = new Map<string, T>();
    } else {
      // 如果不使用 Map,则选择使用索引签名类型来管理相关数据
      this._items = {}; // 初始化索引签名
    }
  }
  // 只读属性:获取字典的元素个数
  public get length(): number {
    return this._count;
  }
  // 判断某个键是否存在
  public contains(key: string): boolean {
    if (this._items instanceof Map) {
      return this._items.has(key);
    } else {
      return this._items[key] !== undefined;
    }
  }
  // 给定一个键,返回对应的值对象
  public find(key: string): T | undefined {
    if (this._items instanceof Map) {
      return this._items.get(key);
    } else {
      return this._items[key];
    }
  }
  // 插入一个键值对
  public insert(key: string, value: T): void {
    if (this._items instanceof Map) {
      this._items.set(key, value);
    } else {
      this._items[key] = value;
    }
    this._count++;
  }
  // 删除
  public remove(key: string): boolean {
    let ret: T | undefined = this.find(key);
    if (ret === undefined) {
      return false;
    }
    if (this._items instanceof Map) {
      this._items.delete(key);
    } else {
      delete this._items[key];
    }
    this._count--;
    return true;
  }
  // 获取所有键
  public get keys(): string[] {
    let keys: string[] = [];
    if (this._items instanceof Map) {
      let keyArray = this._items.keys();
      // 返回的是 IterableIterator<T> 类型,该类型能别用于 for...of... 语句,由于设计的 Dictionary 对象的 keys 属性返回的是数组对象 因此需要再次包装一下
      for (let key of keyArray) {
        keys.push(key);
      }
    } else {
      for (let prop in this._items) {
        if (this._items.hasOwnProperty(prop)) {
          keys.push(prop);
        }
      }
    }
    return keys;
  }
  // 获取所有值
  public get values(): T[] {
    let values: T[] = [];
    if (this._items instanceof Map) {
      // 一定要用of,否则出错
      // 返回的是 IterableIterator<T> 类型,该类型能别用于 for...of... 语句,由于设计的 Dictionary 对象的 values 属性返回的是数组对象 因此需要再次包装一下
      let vArray = this._items.values();
      for (let value of vArray) {
        values.push(value);
      }
    } else {
      for (let prop in this._items) {
        if (this._items.hasOwnProperty(prop)) {
          values.push(this._items[prop]);
        }
      }
    }
    return values;
  }
}

使用 demo,方法如下:

DSDictionary.ts 文件内容:

/*
 * @Description:
 * @Author: tianyw
 * @Date: 2023-01-26 18:49:30
 * @LastEditTime: 2023-01-27 14:22:41
 * @LastEditors: tianyw
 */
import { Dictionary } from "../../src/dataStructures/Dictionary";
function run() {
  let dict: Dictionary<string> = new Dictionary(false);
  dict.insert("a", "a");
  dict.insert("d", "d");
  dict.insert("c", "c");
  dict.insert("b", "b");

  console.log(JSON.stringify(dict));

  dict.remove("c");

  console.log(JSON.stringify(dict));

  console.log(dict.contains("c"));
  console.log(dict.find("b"));
  console.log(JSON.stringify(dict.keys));
  console.log(JSON.stringify(dict.values));
}
run();

项目代码对应版本地址

本章参考如下:

《TypeScript 图形渲染实战——基于WebGL的3D架构与实现》

猜你喜欢

转载自blog.csdn.net/yinweimumu/article/details/128770604
今日推荐