本文详细讲解如何在 JavaScript 中基于数组手动实现集合(Set)数据结构。内容涵盖集合的基本定义、构造函数初始化,以及 add、remove、show 等基础操作方法。重点解析并集、交集、子集判断和补集等高级集合运算的算法逻辑与代码实现,帮助开发者深入理解集合的特性及其在 JS 中的应用。
本文详细介绍 JavaScript 中算法与数据结构的核心概念——集合(Set)。以下内容为技术参考示例,旨在帮助读者理解相关实现原理。
集合(Set)
与数学定义一致,集合是由一组无序但具有特定关联性的成员组成的数据结构。每个成员在集合中仅出现一次。与字典或链表不同,集合是一种包含不同元素的数据结构,其核心特征有两个:成员无序且成员唯一,即集合内不存在重复元素。
在许多编程语言中,集合并非内置的数据类型。然而,当需要保存一组唯一元素时,集合数据结构便显得尤为重要。下文将探讨如何在 JavaScript 中实现这一结构。
集合的定义
在实现集合之前,需明确其基本定义:
- 不包含任何成员的集合被称为空集;包含所有可能成员的集合被称为全集。
- 若两个集合包含完全相同的成员,则称这两个集合相等。
- 若一个集合的所有成员均包含于另一个集合中,则前者称为后者的子集。
集合的操作
集合通常支持以下三种基本操作:
- 并集:合并两个集合的所有成员,生成一个新集合。
- 交集:由两个集合中共有的成员组成一个新集合。
- 补集:由属于一个集合但不属于另一个集合的成员组成一个新集合。
集合的实现
本文基于数组实现集合(Set),利用数组存储数据。结合前述概念,定义集合构造函数如下(为避免与 ES6 内置的 Set 类型混淆,此处命名为 MySet):
//构造函数
function MySet () {
this.dataStore = []; // 数据存储
this.add = add; // 添加成员
this.remove = remove; // 删除成员
this.size = size; // 集合元素个数
this.union = union; // 集合求并集
this.intersect = intersect; // 集合求交集
this.subset = subset; // 判断一个集合是否是另一集合的子集
this.difference = difference; // 集合求补集
this.contains = contains; // 判断某成员是否属于该集合
this.show = show; // 显示当前集合
}
首先实现向集合添加成员的方法,即 add 方法。
add:向集合中添加一个成员
//添加元素
function add (data) {
//判断元素是否存在集合当中
if( this.dataStore.indexOf( data ) < 0 ){
this.dataStore.push(data);
return true;
}else{
console.warn( 'Can not add ' + data + ', must already be in set');
return false;
}
}
由于集合元素具有唯一性,在将数据存储到数组前,需确保该数据不存在。通过 indexOf 方法检查新元素是否存在:若存在,返回其在数组中的位置;若不存在,返回 -1。因此,add 方法返回布尔值:添加成功返回 true,否则返回 false,以此明确指示元素是否成功插入。
remove:删除集合中某个成员
//删除元素
function remove (data) {
//判断元素是否存在集合当中
var pos = this.dataStore.indexOf(data);
if( pos > -1 ){
this.dataStore.splice(pos,1);
return true;
}else{
console.warn( data + ' is not in set');
return false;
}
}
实现删除方法时,逻辑与 add 类似。首先检查待删除元素是否存在于数组中。若存在,调用数组的 splice() 方法删除该元素并返回 true;若不存在,直接返回 false,表示集合中无该元素。
完成添加和删除功能后,在测试前需定义 show 方法以显示集合成员。该方法实现简单,直接返回存储数据的数组即可:
show:显示集合中的成员
// 显示集合成员
function show(){
console.log(this.dataStore);
return this.dataStore;
}
接下来进行功能测试:
var fruits = new MySet();
// 添加成员
fruits.add('Apple');
fruits.add('Banana');
fruits.add('Pear');
fruits.show(); // ["Apple", "Banana", "Pear"]
// 添加重复成员
fruits.add('Apple'); // Can not add Apple, must already be in set
// 删除成员
fruits.remove('Banana');
fruits.show(); // ["Apple", "Pear"]
// 删除不存在的成员
fruits.remove('Banana'); // Banana is not in set
基础功能运行正常,现在可以实现集合的高级操作,首先查看 union(并集)的实现。
union:求集合并集
求并集即合并两个集合并去除重复元素。实现思路是:将第一个集合的成员放入临时集合,然后判断第二个集合的成员是否已存在于临时集合中。若存在(即为重复元素),则跳过;否则将该成员加入临时集合,最后返回该集合。
为此,需要判断成员是否存在于集合中,因此引入辅助方法 contains()。其实现简单,直接使用 indexOf 判断即可。
//判断元素是否属于该集合
function contains (data) {
if( this.dataStore.indexOf(data) > -1 ){
return true;
}else{
return false;
}
}
现在可以定义 union 方法了。
//求集合的并集
function union ( set ) {
var tempSet = new MySet();
for( var i = 0 ; i < this.dataStore.length ; i++ ){
tempSet.add(this.dataStore[i]);
}
for( var i = 0 ; i< set.dataStore.length ; i++ ){
if( !tempSet.contains(set.dataStore[i])){
tempSet.dataStore.push(set.dataStore[i]);
}
}
return tempSet;
}
至此,集合的并集功能已完成。
var fruits1 = new MySet();
fruits1.add('Apple');
fruits1.add('Banana');
fruits1.add('Pear');
var fruits2 = new MySet();
fruits2.add('Grape');
fruits2.add('Banana');
fruits2.add('Pear');
fruits2.add('Orange');
var union = fruits1.union( fruits2 );
union.show(); // ["Apple", "Banana", "Pear", "Grape", "Orange"]
测试通过,接下来查看求集合交集的实现。
intersect:求集合的交集
基于并集的实现思路,交集的定义相对简单:遍历第一个集合的成员,若发现该成员也属于第二个集合,则将其加入新集合,最后返回新集合。
//求集合的交集
function intersect (set) {
var tempSet = new MySet();
for(var i = 0 ; i < this.dataStore.length ; i++ ){
if( set.contains(this.dataStore[i])){
tempSet.add(this.dataStore[i]);
}
}
return tempSet;
}
利用上述两个集合继续演示交集运算:
var intersect = fruits1.intersect( fruits2 );
intersect.show(); // ["Banana", "Pear"]
接下来定义 subset 操作。
subset:判断集合是否是另一集合的子集
该方法首先检查当前集合的长度是否小于待比较集合。若当前集合更大,则肯定不是子集。仅当待比较集合较大时,才判断当前集合的成员是否全部属于待比较集合。若有一个成员不属于,直接返回 false;仅当所有元素都属于待比较集合时,才返回 true。为方便查看,代码中加入了 console 打印。
//子集判断
function subset (set) {
if( this.size() > set.size() ){
console.log('not a subset');
return false;
}else{
for ( var i = 0 ; i < this.dataStore.length ; i++ ){
if( !set.contains(this.dataStore[i])){
console.log('not a subset');
return false;
}
}
}
console.log(' a subset');
return true;
}
上述逻辑用到了 size 方法,其定义如下:
//返回集合长度
function size () {
return this.dataStore.length;
}
保留之前的 fruits1 和 fruits2,新建 fruits3 来演示 subset 方法。
var fruits3 = new MySet();
fruits3.add('Apple');
fruits3.add('Banana');
fruits3.add('Pear');
fruits3.add('Grape');
fruits3.add('Orange');
//子集判断
fruits1.subset( fruits2 ); // not a subset
fruits2.subset( fruits2 ); // a subset
fruits1.subset( fruits3 ); // a subset
目前进展顺利,仅剩最后一个 difference 方法。该方法返回一个新集合,由属于第一个集合但不属于第二个集合的成员组成。
difference:补集
基于交集的思路,补集的实现逻辑自然清晰。
//补集
function difference (set) {
var tempSet = new MySet();
for( var i = 0 ; i < this.dataStore.length ; i ++ ){
if( !set.contains(this.dataStore[i])){
tempSet.dataStore.push( this.dataStore[i] );
}
}
return tempSet;
}
进行测试:
fruits1.difference(fruits2).show(); // ['Apple']
fruits1.difference(fruits3).show(); // []
fruits2.difference(fruits1).show(); // ["Grape", "Orange"]
至此,一个完整的 Set 集合实现完毕。
本文介绍的集合与 ES6 提供的 Set 数据结构略有差异。ES6 的 Set 提供了大量现成方法,使用便捷。不熟悉 ES6 特性的读者可参考关于 ES6 中 Symbol、Set 和 Map 的相关文章,相信会有所收获。
感兴趣的朋友可以使用在线 HTML/CSS/JavaScript 代码运行工具:http://tools.jb51.net/code/HtmlJsRun 测试上述代码的运行效果。
更多关于 JavaScript 相关内容可查看本站专题:《JavaScript数学运算用法总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript数组操作技巧总结》、《JavaScript排序算法总结》、《JavaScript遍历算法与技巧总结》、《JavaScript查找算法技巧总结》及《JavaScript错误与调试技巧总结》。
希望本文所述对大家 JavaScript 程序设计有所帮助。