JavaScript数据结构 - 集合

1,111 阅读7分钟

前言

大家好,我是图图。上一篇文章聊了链表的数据结构,链表包括:单向链表、双向链表、循环链表和有序链表这几个常见的链表。而在操作的过程中,也是比较复杂的。因为它具备有一个指向下一个节点的指针,因此在操作的过程中要多加小心。那么这一章我们就来聊聊集合。下面我们废话不多说,一切尽在代码中。

集合

集合是由一组无序并且不能存在重复的元素组成的。你可以把集合当成一个既没有重复元素,又没有顺序的数组。

创建集合

学过 ES6 的同学都知道,加入了一种新的数据结构Set。也是本章所说的集合。下面将基于 ES6 的Set数据结构来实现一个属于自己的Set类。

下面来声明一个Set类。

class Set {
  constructor() {
    this.items = {};
  }
}

这里用一个对象items来表示集合,用对象的主要原因是,对象不允许用一个键指向两个不同的属性。这样也能确保集合中的元素是唯一的,不过也可以用数组来表示。

下面是集合的方法。

  • add(ele):向集合中添加一个元素。
  • delete(ele):从集合中删除一个元素。
  • has(ele):查看集合中是否有该元素存在,如果存在返回true,否则返回false
  • clear():删除集合中的所有元素。
  • size():返回集合中元素的数量。
  • values():返回一个包含集合中所有元素的数组。

has方法

首先来实现has方法,因为它会被adddelete方法调用。

has(ele) {
    return Object.prototype.hasOwnProperty.call(this.items, ele);
}

这里使用了Object原型上的hasOwnProperty方法。该方法返回一个布尔值,查看对象上是否具有指定的属性。当然你也可以用in操作符。这两个方法的区别在于hasOwnProperty是检查实例中是否具有指定的属性。而in操作符是无论指定的属性是在原型中还是在实例中都会返回true

add方法

下面是添加元素的方法。

add(ele) {
    if (!this.has(ele)) {
      this.items[ele] = ele;
      return true;
    }
    return false;
}

add方法相对于比较简单。首先,检查要添加的元素是否存在于集合中,没有就添加并返回true,有就直接返回false

添加元素时,最好用它本身来当作键和值来保存,这样有利于查找该元素。

delete、clear方法

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

delete方法首先检查元素是否存在于集合中,存在就删除并返回true表示已删除,不存在直接返回false

clear方法和其他数据结构的clear方法一样。

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

直接将items初始化就可以了。还有一种方法是通过迭代集合,使用delete操作符逐个将元素删除,不过这样显得麻烦。

size方法

实现size方法有三种:

  1. 像栈、队列那样,在使用adddelete方法是用一个count变量控制它。
  2. 使用Object.keys方法,该方法返回对象中可枚举属性组成的数组。然后用这个数组的长度length返回items对象中的属性个数。
  3. for-in遍历对象中的属性,用一个count变量记录属性的个数并返回count变量。
size() {
    return Object.keys(this.items).length;
}

theOtherOneSize() {
    let count = 0;
    for (const key in this.items) {
      if (this.has(key)) {
        count++;
      }
    }
    return count;
}

迭代items对象的所有属性,并用has方法检查是否为自身的属性。如果是,就递增count变量。

has方法这步的原因在于,假设在对象的原型上有额外的属性,也会导致count递增。

values方法

对于values方法,用Object.valuesfor-in迭代都可以。

values() {
    return Object.values(this.items);
}

theOtherOneValues() {
    const arr = [];

    for (const key in this.items) {
      if (this.has(key)) {
        arr.push(key);
      }
    }
    return arr;
}

for-in迭代就和上面的size方法一样,只不过这里不是计算属性的个数。

测试Set类

const set = new Set();

console.log(set.size()); // 0

console.log(set.add(1)); // true
console.log(set.add(2)); // true
console.log(set.add(3)); // true

console.log(set.size()); // 3

console.log(set.has(1)); // true

console.log(set.values()); // [1, 2, 3]

console.log(set.delete(3)); // true

整体代码

class Set {
  constructor() {
    this.items = {};
  }

  has(ele) {
    return Object.prototype.hasOwnProperty.call(this.items, ele);
  }

  add(ele) {
    if (!this.has(ele)) {
      this.items[ele] = ele;
      return true;
    }
    return false;
  }

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

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

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

  theOtherOneSize() {
    let count = 0;
    for (const key in this.items) {
      if (this.has(key)) {
        count++;
      }
    }
    return count;
  }

  values() {
    return Object.values(this.items);
  }

  theOtherOneValues() {
    const arr = [];

    for (const key in this.items) {
      if (this.has(key)) {
        arr.push(key);
      }
    }
    return arr;
  }
}

集合运算

并集

给定两个集合,返回一个包含两个集合中所有的元素的集合。

// 在Set类中添加方法
union(otherSet) {
    const unionSet = new Set();
    this.values().forEach((item) => unionSet.add(item));
    otherSet.values().forEach((item) => unionSet.add(item));
    return unionSet;
}

测试代码

const setA = new Set();
const setB = new Set();

setA.add(1);
setA.add(3);
setA.add(5);
setA.add(6);

setB.add(2);
setB.add(4);
setB.add(6);

const otherSetB = setA.union(setB);

console.log(otherSetB.values());
// [1, 2, 3, 4, 5, 6]

需要注意的是,在setAsetB中都添加了元素6,但是在下面打印出来的数据中只出现一个6

交集

给定的两个集合,返回一个包含两个集合中共有元素的集合。

intersectionFn(otherSet) {
    const intersection = new Set();

    const values = this.values();
    for (let i = 0; i < values.length; i++) {
      if (otherSet.has(values[i])) {
        intersection.add(values[i]);
      }
    }
    return intersection.values();
}

首先创建一个Set实例,然后迭代当前Set实例中的所有值(values)。用has方法验证是否存在otherSet集合当中,如果存在,就添加到interseciton集合中。最后以数组的形式返回它。

测试代码

const setA = new Set();
const setB = new Set();

setA.add(1);
setA.add(3);
setA.add(5);
setA.add(6);

setB.add(2);
setB.add(4);
setB.add(6);

const otherSetB = setA.intersectionFn(setB);

console.log(otherSetB);
// [6]

改进版

假设现在有两个这样的集合:

  • setA的值是[1, 2, 3, 4, 5, 6];
  • setB的值是[2, 6]; 用刚才的intersectionFn方法,就要迭代六次setA的值,然后还要用这个六个值和setB的两个值去作比较。那么换过来,只要用setB去和setA做比较的话,这样就减少了性能的消耗。因为只需要迭代两次setB。下面来修改之前的intersectionFn方法。
otherIntersection(otherSet) {
    const intersection = new Set();
    const values = this.values();
    const otherVal = otherSet.values();
    // 数组长度大的集合
    let biggerSet = values;
    // 数组长度小的集合
    let smallerSet = otherVal;

    /*
      如果传入的Set集合的元素个数比当前Set集合的元素个数多,那么就交换
      如果当前的集合元素个数比传入的Set集合的元素个数多,就不会走这一步
    */
    if (otherVal.length - values.length > 0) {
      biggerSet = otherVal;
      smallerSet = values;
    }

    // 迭代较小的集合
    smallerSet.forEach((item) => {
      if (biggerSet.includes(item)) {
        intersection.add(item);
      }
    });

    return intersection;
}

差集

给定两个集合,返回一个包含所有存在于第一个集合中但不存在于第二个集合中的元素的集合。

difference(otherSet) {
    const differenceSet = new Set();

    this.values().forEach((item) => {
      if (!otherSet.has(item)) {
        differenceSet.add(item);
      }
    });

    return differenceSet.values();
}

difference方法会返回一个存在于集合setA中但不存在于集合setB的元素数组。首先创建结果集合,然后迭代集合中的所有值。检查当前值是否存在于给定集合中,存在的话,就把值添加到结果集合中。

测试代码

const setA = new Set();
const setB = new Set();

setA.add(1);
setA.add(3);
setA.add(5);
setA.add(6);

setB.add(2);
setB.add(4);
setB.add(6);

const otherSetB = setA.difference(setB);

console.log(otherSetB);
// [1, 3, 5]

这里输出了[1, 3, 5],因为[1, 3, 5]只存在于setA中。如果执行setB.difference(setA),会输出[2, 4],因为[2, 4]只存在于setB中。

这里不能像优化intersecton方法那样去优化difference方法,因为setAsetB之间的差集可能与setBsetA之间的差集不一样。

子集

验证一个集合是否是另一个集合的子集。

isSubsetOf(otherSet) {
    if (this.size() > otherSet.size()) {
      return false;
    }

    let isSubset = true;

    this.values().every((value) => {
      if (!otherSet.has(value)) {
        isSubset = false;
        return false;
      }

      return true;
    });
    return isSubset;
}

首先验证当前Set实例的元素个数大小,如果当前实例中的元素比otherSet实例多,那就不是子集。直接返回false。记住,子集的元素个数始终小于或等于要比较的集合的。

上面的代码中,假如当前实例是给定集合的子集(isSubset = true)。就迭代当前集合中的所有元素,验证这些元素是否存在于otherSet中。如果都不存在,就说明它不是一个子集,返回false。如果都存在于otherSet中,那么isSubset的值就不会改变。返回true

使用every方法的原因在于,一个值不存在于otherSet中时,可以停止迭代,表示这不是一个子集。只要回调函数返回true,就会执行下去。如果返回false,循环直接停止。

测试代码

const setA = new Set();
const setB = new Set();
const setC = new Set();

setA.add(1);
setA.add(2);
setA.add(3);

setB.add(1);
setB.add(2);
setB.add(3);
setB.add(4);

setC.add(2);
setC.add(3);
setC.add(4);
setC.add(5);

console.log(setA.isSubsetOf(setB)); // true
console.log(setA.isSubsetOf(setC)); // false

这里有三个集合:setAsetB的子集,所以输出了truesetA不是setC的子集,因为setC中只包含了setA中的2,所以输出了false

ES6中的Set类

下面我们用 ES6 的 Set类来模拟并集、交集、差集、子集

首先来定义两个集合,因为后面会用到。

const setA = new Set();
const setB = new Set();
setA.add(10);
setA.add(20);
setA.add(30);

setB.add(20);
setB.add(30);
setB.add(40);
setB.add(50);

模拟并集

const union = (setA, setB) => {
  let values = new Set();
  setA.forEach((item) => values.add(item));
  setB.forEach((item) => values.add(item));
  return values.values();
};

console.log(union(setA, setB));
// {10, 20, 30, 40, 50}

模拟交集

const intersection = (setA, setB) => {
  let values = new Set();

  setA.forEach((item) => {
    if (setB.has(item)) {
      values.add(item);
    }
  });
  return values.values();
};
console.log(intersection(setA, setB));
// {20, 30}

模拟差集

const difference = (setA, setB) => {
  const values = new Set();
  setA.forEach((item) => {
    if (!setB.has(item)) {
      values.add(item);
    }
  });

  return values.values();
};

console.log(difference(setA, setB));
// {10}

模拟子集

const isSubsetOf = (setA, setB) => {
  if (setA.size > setB.size) {
    return false;
  }

  let isSubset = true;
  let arr = [...setA];
  arr.every((value) => {
    if (!setB.has(value)) {
      isSubset = false;
      return false;
    }

    return true;
  });
  return isSubset;
};

console.log(isSubsetOf(setA, setB)); // false

结尾

如果文中出现错误,欢迎各位大佬指点。如果本章内容对你有收获,欢迎点赞加关注哦!