集合与映射

什么是集合(Set)

简单来说,集合就是一个不能存放重复元素的数据结构,通常可以运用于客户统计(网站访问人数),词汇量统计等等。

定义Set接口

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
28
29
30
31
32
33
  public interface Set<E> {

/**
* 增加元素
* @param e 元素
*/
void add(E e);

/**
* 是否包含
* @param e 元素
* @return 布尔
*/
boolean contains(E e);

/**
* 移除元素
* @param e 元素
*/
void remove(E e);

/**
* 获取集合大小
* @return size大小
*/
int getSize();

/**
* set是否为空
* @return boolean
*/
boolean isEmpty();
}

使用二分搜索树实现集合

对于二分搜索树有一个很重要的特性就是它是根据元素的大小来放入元素到树中的,所以二分搜索树基本是不存在相同元素的,我们就可以通过二分搜索树底层实现集合

这是上一篇博客写的二分搜索树的相关代码:

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
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/**
* @author Lin
* @date 2019-05-05 20:50
**/
public class BinarySearchTree<E extends Comparable<E>> {
private class Node{
private E e;
private Node left,right;
private Node(E e){
this.e=e;
left=null;
right=null;
}
}

private Node root;
private int size;

public BinarySearchTree(){
root = null;
size = 0;
}

public int size(){
return size;
}

public boolean isEmpty(){
return size==0;
}

public void add(E e){
root = add(root,e);
}

private Node add(Node node,E e){
if (node == null){
size ++;
return new Node(e);
}else if (e.compareTo(node.e) < 0){
node.left = add(node.left,e);
}else if (e.compareTo(node.e) > 0){
node.right = add(node.right,e);
}
return node;
}

public boolean contains(E e){
return contains(root,e);
}

private boolean contains(Node node,E e){
if (root==null){
return false;
}
if (e.equals(node.e)){
return true;
}else if (e.compareTo(node.e)<0){
return contains(node.left,e);
}else if (e.compareTo(node.e)>0){
return contains(node.right,e);
}
return false;
}

public E minimum(){
if (size == 0){
throw new IllegalArgumentException("BST is empty");
}
return minimum(root).e;
}

private Node minimum(Node node) {
if (node.left==null){
return node;
}
return minimum(node.left);
}

public E maximum(){
if (size == 0){
throw new IllegalArgumentException("BST is empty");
}
return maximum(root).e;
}

private Node maximum(Node node){
if (node.right==null){
return node;
}
return maximum(node.right);
}

public E removeMin(){
if (size==0){
throw new IllegalArgumentException("BST is empty");
}
return removeMin(root).e;
}

private Node removeMin(Node node){
if (node.left==null){
Node rightNode=node.right;
node.right=null;
size--;
return rightNode;
}
node.left=removeMin(node.left);
return node;
}

public E removeMax(){
if (size==0){
throw new IllegalArgumentException("BST is empty");
}
return removeMax(root).e;
}

private Node removeMax(Node node){
if (node.right==null){
Node leftNode=node.left;
node.left=null;
size--;
return leftNode;
}
node.right=removeMax(node.right);
return node;
}

public void remove(E e){
root=remove(root,e);
}

private Node remove(Node node,E e){
if (node==null){
return null;
}
if (e.compareTo(node.e)<0){
node.left=remove(node.left,e);
return node;
} else if (e.compareTo(node.e)>0) {
node.right=remove(node.right,e);
return node;
}
//如果相等就要删除
else {
//首先判断这个树是不是只有一边的孩子
//如果只有左边有孩子或者只有右边有孩子
if (node.right==null){
size--;
return node.left;
}else if (node.left==null){
size--;
return node.right;
}
//如果左右都有孩子
//寻找该节点的后继节点
//后继结点就是离该结点最近的节点(数值最近而且是大于该节点)
//那么这个结点的后继结点就是该节点右孩子的最小结点
Node successorNode=minimum(node.right);
//我们需要删除该节点的后继结点并且将删除后的右孩子树赋值给我们获取到的successorNode的右孩子
successorNode.right=removeMin(node.right);
successorNode.left=node.left;
node.left=node.right=null;
return successorNode;
}
}

public void preOrder(){
preOrder(root);
}

private void preOrder(Node node){
if (node!=null){
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
}

/**
* 非递归实现前序遍历
*/
public void perOrderWithoutRecurrence(){
Stack<Node> stack=new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
Node currentNode=stack.pop();
System.out.println(currentNode.e);
if (currentNode.right!=null){
stack.push(currentNode.right);
}
if (currentNode.left!=null){
stack.push(currentNode.left);
}
}
}

/**
* 层序遍历(广度优先遍历)
*/
public void levelOrder(){
if (root!=null){
Queue<Node> queue=new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
Node currentNode=queue.remove();
System.out.println(currentNode.e);
if (currentNode.left!=null){
queue.add(currentNode.left);
}
if (currentNode.right!=null){
queue.add(currentNode.right);
}
}
}
}

public void inOrder(){
inOrder(root);
}

private void inOrder(Node node){
if (node!=null){
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
}

public void postOrder(){
postOrder(root);
}

private void postOrder(Node node){
if (node!=null){
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
}

@Override
public String toString(){
StringBuilder res = new StringBuilder();
generateBSTString(root, 0, res);
return res.toString();
}

/**
* 生成以node为根节点,深度为depth的描述二叉树的字符串
* @param node
* @param depth
* @param res
*/
private void generateBSTString(Node node, int depth, StringBuilder res){

if(node == null){
res.append(generateDepthString(depth) + "null\n");
return;
}

res.append(generateDepthString(depth) + node.e + "\n");
generateBSTString(node.left, depth + 1, res);
generateBSTString(node.right, depth + 1, res);
}

private String generateDepthString(int depth){
StringBuilder res = new StringBuilder();
for(int i = 0 ; i < depth ; i ++){
res.append("--");
}
return res.toString();
}

}

这时候我们就可以使用二分搜索树来实现集合了,很简单,直接封装一个二分搜索树然后调用方法就行了。

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
28
29
30
31
32
33
34
35
36
37
/**
* @author Lin
* @date 2019-05-05 21:08
**/
public class BinarySearchTreeSet<E extends Comparable<E>> implements Set<E>{

private BinarySearchTree<E> binarySearchTree;

public BinarySearchTreeSet(){
this.binarySearchTree = new BinarySearchTree<>();
}

@Override
public void add(E e) {
binarySearchTree.add(e);
}

@Override
public void remove(E e) {
binarySearchTree.remove(e);
}

@Override
public boolean contains(E e) {
return binarySearchTree.contains(e);
}

@Override
public int getSize() {
return binarySearchTree.size();
}

@Override
public boolean isEmpty() {
return binarySearchTree.isEmpty();
}
}

使用链表来实现集合

链表中有一个contains方法,我们可以在调用add方法的时候使用contains方法判断原来集合中是否已经存在该元素了,如果已经存在那么就添加失败。其他的也是直接调用方法。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @author Lin
* @date 2019-05-12 10:01
**/
public class LinkedListSet<E> implements Set<E> {

private LinkedList<E> linkedList;

public LinkedListSet(){
this.linkedList = new LinkedList<>();
}

@Override
public void add(E e) {
//判断是否已经存在该元素
if (!linkedList.contains(e)){
linkedList.addFirst(e);
}
}

@Override
public boolean contains(E e) {
return linkedList.contains(e);
}

@Override
public void remove(E e) {
linkedList.remove(e);
}

@Override
public int getSize() {
return linkedList.getSize();
}

@Override
public boolean isEmpty() {
return linkedList.isEmpty();
}
}

二分搜索树和链表实现集合的比较

在上面实现链表集合的时候我们很容易就会发现,链表集合的增加操作的时候需要先判断整个链表中是否存在该元素,那么这样就需要O(n)的时间复杂度了,在删除,查找元素的时候都是和普通链表一样O(n)的时间复杂度。可以说链表实现的集合的效率并不是很高。

那么我们再来看一看二分搜索树实现的集合的时间复杂度。当我们添加元素的时候我们是依靠二分搜索树的特性来添加的

二分搜索树

比如我们需要添加一个元素的时候,我们就需要先判断大小,然后根据树的特性一层一层遍历,这个时候我们就会发现,我们的时间复杂度是和这个二分搜索树的高度呈线性关系的。

二分搜索树

二分搜索树

然后我们通过运算就可以得出一个平衡二叉树h层一共有2的h次方-1个元素,我们就很容易得出时间复杂度为log2(n+1),忽略常数,那么就是logn。

二分搜索树

而logn和n的差距在基数很大的时候差距特别明显

二分搜索树

所以说二分搜索树实现的集合会比链表实现的集合快很多。当然这也是考虑树的最好情况。当树不为平衡二叉树的时候,最坏情况也会是O(n)。

二分搜索树

如上图右边,我们看到当树成一边排列的时候,它的时间复杂度就是O(n)。

LeetCode上解决关于集合的题目

题目是这样的:

LeetCode804题目

对于这道题,我们可以遍历这个words数组,然后得到其中的某个元素再次遍历字符串的每个字母,然后通过原先定义的摩尔斯密码表数组和字符相对应,之后我们就可以获取到这个单词的摩尔斯密码,我们循环完一次之后,我们就把得到的摩尔斯密码拼接成字符串然后存入到set中,最终循环完成之后我们只要返回set的size就行了。

实现代码:

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
  /**
* @author Lin
* @date 2019-05-12 13:39
**/
public class Solution {

public int uniqueMorseRepresentations(String[] words) {

//预先定义摩尔斯密码表
String[] codes = {".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."};

Set<String> set = new HashSet<>();

for (String word : words) {
StringBuilder stringBuilder = new StringBuilder();

for (int i = 0 ; i < word.length() ; i ++){
//拼接字符串
stringBuilder.append(codes[word.charAt(i) - 'a']);
}
//存入set中
set.add(stringBuilder.toString());
}

return set.size();
}
}

什么是映射(Map,字典)

  1. 映射,在定义域中每一个值在值域都有一个值与他对应

  2. 存储(键,值)数据对的数据结构(Key,Value)

  3. 根据键(Key),寻找值(Value)

    映射的概念

定义Map接口

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
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
/**
* @author Lin
* @date 2019-05-13 20:07
**/
public interface Map<K, V> {

/**
* 添加元素
* @param k key
* @param v value
*/
void add(K k, V v);

/**
* 删除元素
* @param k key
* @return key对应的value
*/
V remove(K k);

/**
* 设置相应key的value
* @param k key
* @param v value
*/
void set(K k, V v);

/**
* 获取某个key对应的value
* @param k key
* @return value
*/
V get(K k);

/**
* 判断是否包含
* @param k key
* @return 是否包含
*/
boolean contains(K k);

/**
* 获取映射的大小
* @return 映射大小
*/
int getSize();

/**
* 判断映射是否为空
* @return 是否为空
*/
boolean isEmpty();
}

链表实现Map

使用链表实现Map其实只需要将私有类Node里面的字段e更改为key和value就行。当然BinarySearchTree也是如此

实现代码:

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
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
111
112
113
114
115
116
117
118
119
/**
* @author Lin
* @date 2019-05-13 20:17
**/
public class LinkedListMap<K, V> implements Map<K, V> {

private class Node{

private K key;
private V value;
private Node next;

public Node(K key, V value, Node next){
this.key = key;
this.value = value;
this.next = next;
}

public Node(K key, V value){
this.key = key;
this.value = value;
}

public Node(){
this.key = null;
this.value = null;
this.next = null;
}

@Override
public String toString(){
return key.toString() + " : " + value.toString();
}

}

private Node dummyHead;
private int size;

public LinkedListMap(){
dummyHead = new Node();
size = 0;
}

private Node getNode(K key){
Node currentNode = dummyHead.next;

while (currentNode != null){
if (currentNode.key.equals(key)){
return currentNode;
}else {
currentNode = currentNode.next;
}
}
return null;
}

@Override
public void add(K k, V v) {
//首先判断是否已经存在该key的映射
Node node = getNode(k);
if (node == null){
dummyHead.next = new Node(k, v, dummyHead.next);
size ++ ;
}else {
//如果存在则更新
node.value = v;
}
}

@Override
public V remove(K k) {
Node preNode = dummyHead;
while (preNode.next != null){
if (preNode.next.key.equals(k)){
Node delNode = preNode.next;
preNode.next = preNode.next.next;
delNode.next = null;
size --;
break;
}else {
preNode = preNode.next;
}
}
return null;
}

@Override
public void set(K k, V v) {
//首先判断是否存在该key的映射,如果不存在则抛出异常
Node node = getNode(k);
if(node == null){
throw new IllegalArgumentException(k + " doesn't exist!");
}else {
node.value = v;
}
}

@Override
public V get(K k) {
Node node = getNode(k);
return node == null ? null : node.value;
}

@Override
public boolean contains(K k) {
return getNode(k) != null;
}

@Override
public int getSize() {
return this.size;
}

@Override
public boolean isEmpty() {
return this.size == 0;
}
}

二分搜索树实现映射(Map)

对于BinarySearchTree来说实现Map也是利用它原先的方法

代码实现:

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
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/**
* @author Lin
* @date 2019-05-13 20:51
**/
public class BinarySearchTreeMap<K extends Comparable<K>, V> implements Map<K, V> {

private class Node{
public K key;
public V value;
public Node left, right;

public Node(K key, V value){
this.key = key;
this.value = value;
left = null;
right = null;
}

public Node(){
left = null;
right = null;
this.value = null;
this.key = null;
}

}

private int size;
private Node root;

public BinarySearchTreeMap(){
root = null;
size = 0;
}

//辅助方法,在删除,修改映射的value时候会用到
//获取相应key对应的Node(递归)
private Node getNode(Node node, K key){
//如果递归到节点为null直接返回null
if (node == null){
return null;
}else if (key.compareTo(node.key) < 0){
//小于的时候继续递归该结点的左子树
return getNode(node.left, key);
}else if (key.compareTo(node.key) > 0){
//大于的时候继续递归该结点的右子树
return getNode(node.right, key);
}else {
//不然就是相等,则直接返回该结点
return node;
}
}

@Override
public void add(K key, V value){
root = add(root, key, value);
}


private Node add(Node node, K key, V value){
//递归到最底层,直接添加节点
if (node == null){
size ++;
return new Node(key, value);
}else if (key.compareTo(node.key) < 0){
//小于的时候直接递归该结点的左子树
node.left = add(node.left, key, value);
}else if (key.compareTo(node.key) > 0){
node.right = add(node.right, key, value);
}
return node;
}

/**
* 返回以node为根的二分搜索树的最小值所在的节点
* @param node
* @return
*/
private Node minimum(Node node){
if(node.left == null){
return node;
}
return minimum(node.left);
}


/**
* 删除掉以node为根的二分搜索树中的最小节点
* 返回删除节点后新的二分搜索树的根
* @param node
* @return
*/
private Node removeMin(Node node){

if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}

node.left = removeMin(node.left);
return node;
}

/**
* 从二分搜索树中删除键为key的节点
* @param key
* @return
*/
@Override
public V remove(K key){

Node node = getNode(root, key);
if(node != null){
root = remove(root, key);
return node.value;
}
return null;
}

private Node remove(Node node, K key){

if( node == null ){
return null;
}
if( key.compareTo(node.key) < 0 ){
node.left = remove(node.left , key);
return node;
}
else if(key.compareTo(node.key) > 0 ){
node.right = remove(node.right, key);
return node;
}
else{ // key.compareTo(node.key) == 0

// 待删除节点左子树为空的情况
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}

// 待删除节点右子树为空的情况
if(node.right == null){
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}

// 待删除节点左右子树均不为空的情况

// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;

node.left = node.right = null;

return successor;
}
}

@Override
public void set(K key, V newValue){
Node node = getNode(root, key);
if(node == null){
throw new IllegalArgumentException(key + " doesn't exist!");
}
node.value = newValue;
}

@Override
public V get(K k) {
Node node = getNode(root, k);
return node == null ? null : node.value;
}

@Override
public boolean contains(K k) {
return getNode(root, k) != null;
}

@Override
public int getSize() {
return this.size;
}

@Override
public boolean isEmpty() {
return this.size == 0;
}
}

链表Map和二分搜索树Map比较

测试代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class TestMap {

private static double testMap(Map<String, Integer> map, String filename){

long startTime = System.nanoTime();

System.out.println(filename);
ArrayList<String> words = new ArrayList<>();
if(FileOperation.readFile(filename, words)) {
System.out.println("Total words: " + words.size());

for (String word : words){
if(map.contains(word)){
map.set(word, map.get(word) + 1);
}else{
map.add(word, 1);
}
}

System.out.println("Total different words: " + map.getSize());
System.out.println("Frequency of PRIDE: " + map.get("pride"));
System.out.println("Frequency of PREJUDICE: " + map.get("prejudice"));
}

long endTime = System.nanoTime();

return (endTime - startTime) / 1000000000.0;
}

public static void main(String[] args) {

String filename = "I:\\data_structure\\src\\pride-and-prejudice.txt";

Map<String, Integer> bstMap = new BinarySearchTreeMap<>();
double time1 = testMap(bstMap, filename);
System.out.println("BST Map: " + time1 + " s");

System.out.println();

Map<String, Integer> linkedListMap = new LinkedListMap<>();
double time2 = testMap(linkedListMap, filename);
System.out.println("Linked List Map: " + time2 + " s");

}

}

两者时间差异:

测试结果

可以看出,二分搜索树实现的Map的时间性能是远高于链表实现的Map的。

因为对于链表实现修改某一映射对应的value值的时间复杂度是O(n),我们需要一个一个去遍历知道找到key符合的元素。

而对于二分搜索树实现的Map来说,它修改一个value的时间复杂度是跟二叉树的高度相关的,也就是说它的时间复杂度是O(logn)级别的,所以当数据量大的时候,二分搜索树的优势非常明显。当然我们还需要考虑最坏的情况,那就是这个二分搜索树变成了类似于链表的树,那这时候时间复杂度就是O(n)了。

两种map的时间复杂度比较

但是有个细节需要注意的是,链表实现的Map的key是无序的,上文中的set也是无序的。但是二分搜索树实现的set和map都是有序的(这也是它这么快的一部分原因)。

使用Map来实现Set

我们可以发现,其实Set就是一个没有value的的Map。所以,当我们底层实现了一个映射,集合我们就可以理解成Map<K, V>中value为null的情况,对于不管是k,它所对应的的value都为null。

所以当我们在Map中只考虑K的时候,整个Map数据结构就变成了K的集合。

-------------本文结束感谢阅读-------------