集合的概念和用途
# 集合的概念和用途
集合是一种包含不同元素的数据结构。
在很多编程语言中并不把集合当成一种数据类型,当你想要创建一个数据结构,用来保存一段独一无二的文字的时候,集合就非常有用。
集合的成员是无序的。
集合中不允许相同成员存在。
# 集合的一些关键点
集合是一组无序但彼此间又有一定相关性的成员构成的,集合中的元素称为成员。
不包含任何成员的集合称为空集。全集则是包含一切可能成员的集合。
如果两个集合的成员完全相同,则称两个集合相等。
如果一个集合中的所有成员都属于另外一个集合,则前一集合称为后一集合的子集。
并集:将两个集合中的成员进行合并,得到一个新集合。
交集:两个集合中共同存在的成员组成一个新的集合。
差集:属于一个集合但不属于另一个集合的成员组成的集合。
# 集合的代码实现
function Set() {
this.dataStore = [];
this.add = add;
this.remove = remove;
this.has = has;
this.size = size;
this.show = show;
this.union = union;
this.intersect = intersect;
this.difference = difference;
this.subSet = subSet;
}
// 向集合中添加元素
function add(data) {
if (!this.has(data)) {
this.dataStore.push(data);
}
}
// 删除集合中的元素
function remove(data) {
var pos = this.dataStore.indexOf(data);
if (pos !== -1) {
this.dataStore.splice(pos, 1);
}
}
// 判断集合中是否有某个元素
function has(data) {
return this.dataStore.indexOf(data) > -1;
}
// 求集合中元素个数
function size() {
return this.dataStore.length;
}
// 展示整个集合内容
function show() {
return this.dataStore;
}
// 求并集
function union(set) {
var tempSet = new Set();
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.has(set.dataStore[i])) {
tempSet.add(set.dataStore[i]);
}
}
return tempSet;
}
// 求交集
function intersect(set) {
var tempSet = new Set();
for (var i = 0; i < this.dataStore.length; i++) {
if (set.has(this.dataStore[i])) {
tempSet.add(this.dataStore[i]);
}
}
return tempSet;
}
// 求差集
function difference(set) {
var tempSet = new Set();
for (var i = 0; i < this.dataStore.length; i++) {
if (!set.has(this.dataStore[i])) {
tempSet.add(this.dataStore[i]);
}
}
return tempSet;
}
// 判断一个集合是否是另一个集合的子集
function subSet(set) {
if (set.size() > this.size()) {
return false;
} else {
for (var i = 0; i < set.dataStore.length; i++) {
if (!this.has(set.dataStore[i])) {
return false;
}
}
return true;
}
}
var set = new Set();
set.add("first");
set.add("second");
set.add("third");
// set.remove('first');
var set1 = new Set();
set1.add("first");
set1.add("second");
set1.add("fourth");
console.log(set.show());
console.log("并集:", set.union(set1).show());
console.log("交集:", set.intersect(set1).show());
console.log("差集:", set.difference(set1).show());
var set2 = new Set();
set1.add("first");
set1.add("second");
console.log("是否是子集:", set.subSet(set2));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# 集合的另一种实现
var Set = function() {
var items = {};
// has 检查元素是否存在
this.has = function(value) {
return items.hasOwnProperty(value);
};
// add 添加元素,要注意集合具有不重复性
this.add = function(value) {
if (!this.has(value)) {
// 对象有两种访问方式:点语法和方括号语法,点语法后面的键不能是变量,只能是对象名;但是方括号语法可以是变量
items[value] = value;
return value;
}
return false;
};
// 移除元素
this.remove = function(value) {
if (this.has(value)) {
delete items[value];
return true;
} else {
return false;
}
};
// 清空集合
this.clear = function() {
items = {};
};
// 获取集合的大小
this.size = function() {
// 遍历集合
// var count = 0;
// for(var i in items){
// if(items.hasOwnProperty(i)){ // 判断对象是否包含特定的自身属性(非继承)
// count++;
// }
// }
// return count;
return Object.keys(items).length; // 静态方法Object.keys()返回的是一个数组,数组里的元素是键名,es6提出来的
};
// 提取集合的全部值并以数组形式返回
this.value = function() {
var values = [];
for (var i in items) {
if (items.hasOwnProperty(i)) {
values.push(items[i]);
}
}
return values;
};
// 并集
this.union = function(otherSet) {
var resultSet = new Set();
// 把自己的值提取出来
var arr = this.value();
for (var i = 0; i < arr.length; i++) {
resultSet.add(arr[i]);
}
// 把另一个集合的值提取出来
arr = otherSet.value();
for (var i = 0; i < arr.length; i++) {
resultSet.add(arr[i]);
}
return resultSet;
};
// 交集
this.intersection = function(otherSet) {
var resultSet = new Set();
// 把自己的值提取出来
var arr = this.value();
for (var i = 0; i < arr.length; i++) {
if (otherSet.has(arr[i])) {
resultSet.add(arr[i]);
}
}
return resultSet;
};
// 差集
this.difference = function(otherSet) {
var resultSet = new Set();
var arr = this.value();
for (var i = 0; i < arr.length; i++) {
if (!otherSet.has(arr[i])) {
resultSet.add(arr[i]);
}
}
return resultSet;
};
// 获取整个集合
this.getItems = function() {
return items;
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# HashMap、TreeMap、HashSet、TreeSet
Hash 和 Binary Search Tree 是 Map 和 Set 底层实现的两种方式。
HashMap 和 HashSet 效率更快,但是元素是乱序的;而 TreeMap 和 TreeSet 效率相对慢一点,但是元素是有序的。所以具体使用哪种要看具体的需求场景。
# 两数之和 ✅
// 时间和空间复杂度都是 O(n)
var twoSum = function(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const diff = target - nums[i];
if (map.has(diff)) {
return [map.get(diff), i];
}
map.set(nums[i], i);
}
return [];
};
2
3
4
5
6
7
8
9
10
11
12
# 两个数组的交集 ✅
// 两个集合,时间和空间复杂度都是 O(m+n)
var intersection = function(nums1, nums2) {
const set1 = new Set(nums1);
const res = [];
for (const num of nums2) {
if (set1.has(num)) {
res.push(num);
set1.delete(num);
}
}
return res;
};
2
3
4
5
6
7
8
9
10
11
12
// 排序+双指针,时间复杂度是 O(mlogm+nlogn),空间复杂度是 O(logm+logn)
var intersection = function(nums1, nums2) {
nums1.sort((a, b) => a - b);
nums2.sort((a, b) => a - b);
const len1 = nums1.length;
const len2 = nums2.length;
const res = [];
let i = 0;
let j = 0;
while (i < len1 && j < len2) {
if (nums1[i] === nums2[j]) {
if (!res.includes(nums1[i])) {
res.push(nums1[i]);
}
i++;
j++;
} else if (nums1[i] < nums2[j]) {
i++;
} else {
j++;
}
}
return res;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 赎金信
var canConstruct = function(ransomNote, magazine) {
if (ransomNote.length > magazine.length) {
return false;
}
const charArr = new Array(26).fill(0);
const baseCharCode = "a".charCodeAt();
for (const c of magazine) {
charArr[c.charCodeAt() - baseCharCode]++;
}
for (const c of ransomNote) {
charArr[c.charCodeAt() - baseCharCode]--;
if (charArr[c.charCodeAt() - baseCharCode] < 0) {
return false;
}
}
return true;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 有效的字母异位词
方法一:可以直接将两个字符串中的字符进行排序,然后比较排序后的结果是不是完全一样。时间复杂度为:O(nlogn)。
var isAnagram = function(s, t) {
return (
s.length === t.length && [...s].sort().join("") === [...t].sort().join("")
);
};
2
3
4
5
方法二:使用 map 来存储两个字符串中每个字符的出现的次数,然后比较这两个 map 是否一致。时间复杂度为:O(n)。
方法三:使用哈希表,思路跟使用 map 类似,维护一个长度为 26 的数组,先遍历记录字符串 s 中每个字符出现的次数,然后在遍历字符串 t,减去数组中相应字符的次数。遍历结束后,如果数组中有某一位次数不为 0,说明两者不一致,返回 false。时间复杂度为:O(n)。
charCodeAt 和 codePointAt 的作用都是获取字符的 Unicode 编码值,区别就是前者只能处理 16 位二进制数 0xffff 以内的值,而后者能正确处理到 32 位二进制数。
var isAnagram = function(s, t) {
if (s.length !== t.length) {
return false;
}
const arr = new Array(26).fill(0);
const baseCodePoint = "a".codePointAt(0);
for (let i = 0; i < s.length; i++) {
arr[s.codePointAt(i) - baseCodePoint]++;
}
for (let i = 0; i < t.length; i++) {
arr[t.codePointAt(i) - baseCodePoint]--;
if (arr[t.codePointAt(i) - baseCodePoint] < 0) {
return false;
}
}
return true;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 字母异位词分组
// 由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键
var groupAnagrams = function(strs) {
const map = new Map();
for (const str of strs) {
const arr = Array.from(str);
arr.sort();
const key = arr.toString();
const value = map.get(key) ? map.get(key) : [];
value.push(str);
map.set(key, value);
}
return Array.from(map.values());
};
2
3
4
5
6
7
8
9
10
11
12
13
# 存在重复元素
var containsDuplicate = function(nums) {
const set = new Set();
for (let i = 0; i < nums.length; i++) {
if (set.has(nums[i])) {
return true;
}
set.add(nums[i]);
}
return false;
};
2
3
4
5
6
7
8
9
10
// 另一种解法,先排序,排序后重复的元素肯定位于相邻的位置
var containsDuplicate = function(nums) {
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length - 1; i++) {
if (nums[i] === nums[i + 1]) {
return true;
}
}
return false;
};
2
3
4
5
6
7
8
9
10
# 缺失的第一个正数
// 将给定的数组设计成哈希表,将所有在 [1,N] 范围内的数放入哈希表
var firstMissingPositive = function(nums) {
const n = nums.length;
// 将数组中小于等于 0 的数修改成任意一个大于 n 的数,比如 n + 1
// 这样一来,数组中的所有数就都是正数了,方便后续将「标记」表示为「负号」
for (let i = 0; i < n; i++) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
// 遍历数组中的每一个数 x,它可能已经被打了标记,因此原本对应的数为 ∣x∣
// 如果 |x|∈[1, n],那么我们给数组中的第 ∣x∣−1 个位置的数添加一个负号
// 如果它已经有负号,不需要重复添加
for (let i = 0; i < n; i++) {
const x = Math.abs(nums[i]);
if (x >= 1 && x <= n) {
nums[x - 1] = nums[x - 1] < 0 ? nums[x - 1] : -nums[x - 1];
}
}
// 上面的工作做完之后,如果数组中的每一个数都是负数,那么答案是 N+1,否则答案是第一个正数的位置加 1
for (let i = 0; i < n; i++) {
if (nums[i] > 0) {
return i + 1;
}
}
return n + 1;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# LRU 缓存
var LRUCache = function(capacity) {
this.map = new Map();
this.capacity = capacity;
};
LRUCache.prototype.get = function(key) {
if (!this.map.has(key)) {
return -1;
}
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
};
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
if (this.map.size > this.capacity) {
this.map.delete(this.map.keys().next().value);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 单词替换
var replaceWords = function(dictionary, sentence) {
const set = new Set();
for (const root of dictionary) {
set.add(root);
}
const words = sentence.split(" ");
for (let i = 0; i < words.length; i++) {
const word = words[i];
for (let j = 0; j < word.length; j++) {
if (set.has(word.substring(0, j + 1))) {
words[i] = word.substring(0, j + 1);
break;
}
}
}
return words.join(" ");
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17