js数据结构基础,集合/字典

92 阅读4分钟

之前写过一篇文章,讲的是es6的Map对象,而同为es6的新语法,Set却没有讲,如今看了数据结构,对Set/Map有了一些理解,正好就将之前所学联系起来,于是乎决定自己封装Set/Map

Set在我的理解中,完全符合数据结构集合的定义,即在集合中,每个值都是唯一值,且集合内的元素是无序的,如果我们想自己封装一个集合,可以参考Set

image.png

可以看到,封装一个Set,需要:

属性

  • size:   元素个数
  • Symbol(Symbol.iterator):   迭代器接口

方法

  • add():   添加元素
  • clear():   清空元素
  • delete():   删除元素
  • entries():   返回一个由所有元素组成的数组
  • forEach():   遍历元素
  • has():   判断是否存在该元素
  • keys():   返回其中元素的键的数组
  • values():   返回其中元素的键的数组

其他杂七杂八的算了,我们就封装这些主要功能好了

首先,我决定用对象来封装Set,因为我们知道对象的键是唯一的,因此,我们可以将键和值设置为相等的,这样我们就可以很轻易的实现集合 无序/元素唯一 的两大特点

image.png

Set本身是怎么实现的我也不懂,但道理是差不多的,可以看到内部的元素也是键值相等的键值对

// 使用class来封装MySet类
      class MySet {
            constructor(...element) {
              this.size = 0;
              this.items = {};
              element.forEach((item) => this.add(item));
            }

            [Symbol.iterator]() {
                let index = 0;
                const keys = Object.keys(this.items);
                const _this = this;
                return {
                    next() {
                        return {
                            done: index>=keys.length,
                            // 注意这里的后缀式自增,返回自增前的值
                            value: _this.items[keys[index++]], 
                        };
                    },
                };
            }

            has(element) {
                return !!this.items[element];
            }

            add(element) {
                if (this.has(element)) return false;

                Object.defineProperties(this,{
                    [element]:{
                        configurable: true,
                        get:(k)=>{
                            return this.items[element]
                        },
                    }
                })
                this.items[element] = element;

                this.size++;
                return this;
            }

            clear() {
                this.items = {};
                this.size = 0;
            }

            delete(element) {
                if (this.has(element)) {
                    delete this.items[element];
                    this.size--;
                    return true;
                }
                return false;
            }

            entries() {
                return Object.entries(this.items).map((item) => ({
                    key: item[0],
                    value: item[1],
                }));
            }

            keys() {
              return Object.entries(this.items).map((item) => item[0]);
            }

            values() {
              return Object.entries(this.items).map((item) => item[1]);
            }

            forEach(callback) {
                const iterator = this[Symbol.iterator]();
                while (!iterator.done) {
                    callback(iterator.value, iterator.key);
                    iterator.next();
                }
            }

            // 并集
            static union(a, b) {
                b.forEach((item) => {
                    a.add(item);
                });
                return a;
            }
        }

        const myset = new MySet()
        myset.add(10)
        myset.add(20)
        myset.add(30)

        for(let k of myset) {
            console.log(k);
        }

对于集合的应用是非常常见的,这里也使用类的静态方法写一下常用的求并集/交集/差集的方法

求并集

static union(a, b) {
   return new MySet(...a, ...b);
}

求交集

static intersect(a, b) {
    return new MySet(
        ...a.values().filter((item) => b.values().includes(item))
    );
}

求差集

static except(a, b) {
    return new MySet(
        ...a.values().filter((item) => !b.values().includes(item))
    );
}

写完试一下

      const mysetA = new MySet(10, 20, 30);
      const mysetB = new MySet(30, 40, 50);
      const unionSet = MySet.union(mysetA, mysetB);
      const intersectSet = MySet.intersect(mysetA, mysetB);
      const exceptSet = MySet.except(mysetA, mysetB);
      console.log(unionSet);
      console.log(intersectSet);
      console.log(exceptSet);

好的,MySet现在有了,再说说Map,Map显然是名为字典的数据结构,它的特点是他的键值都可以是任意数据类型,他也可以称之为映射/符号表/关联数组

他有的常见方法:

  • toStrFn()  将对象转换为字符串
  • hasKey()  判断是否已经有这个键
  • set()  添加/修改值
  • get()  获取键对应的值
  • remove()  删除对应键的键值对
  • keys()  获取键数组
  • values()  获取值数组
  • entries()  获取键值对数组
  • size()  获取实例的属性数量
  • isEmpty()  判断实例是否为空
  • clear()  清空实例中的属性
  • forEach()  遍历

注: es6的Map和我封装的不同,我使用对象转换为json字符串作为键,但Map使用的是引用地址作为键,所以必须保存键的引用才能通过键拿到值,同时,如果引用丢了,就会导致内存泄漏,为了解决这个问题,es6还有一个新的对象结构,weakMap,它的键必须是引用数据类型,并且一旦键的引用消失,其WeakMap内的键值就会被垃圾回收机制回收,也是因为这种自动的垃圾回收,WeakMap也无法使用各种遍历方法

      class MyMap {
        constructor(...element) {
          this.table = {};
        }

        toStrFn(obj) {
          if (obj === null) return "NULL";
          else if (obj === undefined) return "UNDEFINED";
          // 这里使用instanceof判断是否使用构造函数创建的字符串类型
          else if (obj instanceof String || typeof obj === "string") return obj;
          return JSON.stringify(obj);
        }

        hasKey(key) {
          return !!this.table[this.toStrFn(key)];
        }

        set(key, value) {
          // 为了保存初始的键,这里使用对象存储
          this.table[this.toStrFn(key)] = { key, value };
        }

        get(key) {
          if (this.hasKey(key)) return this.table[this.toStrFn(key)].value;
        }

        remove(key) {
          if (!this.hasKey(key)) return;
          delete this.table[this.toStrFn(key)];
          return true;
        }

        entries() {
          return Object.values(this.table);
        }

        keys() {
          return this.entries().map((item) => item.key);
        }

        values() {
          return this.entries().map((item) => item.value);
        }

        size() {
          return Object.keys(this.table).length;
        }

        isEmpty() {
          return this.size() === 0;
        }

        clear() {
          this.table = {};
        }

        forEach(callback) {
          const arr = this.entries();
          for (let i = 0; i < arr.length; i++) {
            callback(arr[i].key, arr[i].value);
          }
        }
      }

注释,为什么要同时使用typeof和instanceof:

image.png