forked from HappySnailSunshine/JavaInterview
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Collection.md
1538 lines (813 loc) · 84.3 KB
/
Collection.md
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Collection
集合 数据结构
> 参考自:https://github.com/Snailclimb/JavaGuide.git
>
> 剑指Offer
>
> 牛客网上面的一些习题
# Interview
### 1. HashTable和HashMap区别
**concurrent 的读音 英 [kənˈkʌrənt] 美 [kənˈkɜːrənt]**
1. **线程是否安全:** HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
2. **效率:** 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
3. **对Null key 和Null value的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
4. **初始容量大小和每次扩充容量大小的不同 :** ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
**HashMap 中带有初始容量的构造函数:**
```java
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
```
下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。
```java
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
```
补充:
他们都完成了 Map 接口,主要区别在于 ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。
HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsvalue 和 containsKey。因为
contains 方法容易让人引起误解。
牛客一道题:
HashTable是**散列表**,存储的内容是键值对(key-value的映射,而不是哈希表)
### 2. HashTable和ConCurrentHashMap的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
**两者的对比图:**
图片来源:<http://www.cnblogs.com/chengxiao/p/6842045.html>
**HashTable:**
![1](../media/pictures/Collection.assets/1591855019306.png)
**JDK1.7的ConcurrentHashMap:**
![1591855044381](../media/pictures/Collection.assets/1591855044381.png)
**JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):**
![1591855058758](../media/pictures/Collection.assets/1591855058758.png)
### 3.HashMap在执行put的时候,是如何避免重复的?
内容参考于:https://www.cnblogs.com/developer_chan/p/9153490.html
HashMap在进行put操作时,会有如下操作:(注意,下面所谓的头插法,是JDK1.7之前的,JDK1.8是尾插法)
1)在**key的hashCode相等**,且**key的内容也相同**的情况下(两次"Aa"),就会对value值进行**覆盖**。
2)在**key的hashCode相等**,但**key的内容不相同**的情况下("Aa","BB"),会进行**链式存储**,并且是**头插法**,新加的值("BB")在链表头,最开始("Aa")的值在链表尾。
3)在key的hashCode不相等的情况,直接进行散列存储。
4)从上述调试过程也发现,HashMap主要是**以key为主**,value相当于key的一个附属值,因为value随key走的。
### 4.说一下ConcurrentHashMap
具体概念参考上面区别里面的。
项目中用到的:
```java
//撮合交易队列
public static Map<String,LinkedBlockTrade> blockTradeMap = new ConcurrentHashMap<>();
```
```java
/**
* 聊天的地方用到啦
* key 用户id
* value orderId
*/
public static ConcurrentHashMap<Integer, Session> merchant = new ConcurrentHashMap<>();
```
### 5.遍历key,遍历value,同时遍历key和value和其他操作
```java
/**
* 测试HashMap 遍历key,遍历value,同时遍历key和value
*/
public class TestHashMapKeyAndValue {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<String, String>();
// 键不能重复,值可以重复
map.put("san", "张三");
map.put("si", "李四");
map.put("wu", "王五");
map.put("wang", "老王");
map.put("wang", "老王2");// 老王被覆盖
map.put("lao", "老王");
System.out.println("-------直接输出hashmap:-------");
System.out.println(map);
/**
* 遍历HashMap
*/
// 1.获取Map中的所有键
System.out.println("-------foreach获取Map中所有的键:------");
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.print(key+" ");
}
System.out.println();//换行
// 2.获取Map中所有值
System.out.println("-------foreach获取Map中所有的值:------");
Collection<String> values = map.values();
for (String value : values) {
System.out.print(value+" ");
}
System.out.println();//换行
// 3.得到key的值的同时得到key所对应的值
System.out.println("-------得到key的值的同时得到key所对应的值:-------");
Set<String> keys2 = map.keySet();
for (String key : keys2) {
System.out.print(key + ":" + map.get(key)+" ");
}
/**
* 如果既要遍历key又要value,那么建议这种方式,应为如果先获取keySet然后再执行map.get(key),map内部会执行两次遍历。
* 一次是在获取keySet的时候,一次是在遍历所有key的时候。
*/
// 当我调用put(key,value)方法的时候,首先会把key和value封装到
// Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取
// map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来
// 调用Entry对象中的getKey()和getValue()方法就能获取键值对了
Set<java.util.Map.Entry<String, String>> entrys = map.entrySet();
for (java.util.Map.Entry<String, String> entry : entrys) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
}
}
```
### 6.hashMap的底层实现原理
<!-- MarkdownTOC -->
- [HashMap 简介](#hashmap-简介)
- [底层数据结构分析](#底层数据结构分析)
- [JDK1.8之前](#jdk18之前)
- [JDK1.8之后](#jdk18之后)
- [HashMap源码分析](#hashmap源码分析)
- [构造方法](#构造方法)
- [put方法](#put方法)
- [get方法](#get方法)
- [resize方法](#resize方法)
- [HashMap常用方法测试](#hashmap常用方法测试)
<!-- /MarkdownTOC -->
#### HashMap 简介
HashMap 主要用来存放键值对,它基于哈希表的Map接口实现</font>,是常用的Java集合之一。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 `treeifyBin`方法。
#### 底层数据结构分析
##### JDK1.8之前
JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。**
**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。**
**JDK 1.8 HashMap 的 hash 方法源码:**
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
```java
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
```java
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
```
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
![1591855117657](../media/pictures/Collection.assets/1591855117657.png)
##### JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
![JDK1.8之后的HashMap底层数据结构](../media/pictures/Collection.assets/67233764.jpg)
**类的属性:**
```java
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 加载因子
final float loadFactor;
}
```
- **loadFactor加载因子**
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
**loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值**。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
- **threshold**
**threshold = capacity * loadFactor**,**当Size>=threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。
**Node节点类源码:**
```java
// 继承自 Map.Entry<K,V>static class Node<K,V> implements Map.Entry<K,V> { final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较 final K key;//键 V value;//值 // 指向下一个节点 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } // 重写hashCode()方法 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 重写 equals() 方法 public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; }}
```
**树节点类源码:**
```java
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // 父 TreeNode<K,V> left; // 左 TreeNode<K,V> right; // 右 TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; // 判断颜色 TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } // 返回根节点 final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; }
```
#### HashMap源码分析
##### 构造方法
HashMap 中有四个构造方法,它们分别如下:
```java
// 默认构造函数。 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } // 包含另一个“Map”的构造函数 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false);//下面会分析到这个方法 } // 指定“容量大小”的构造函数 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 指定“容量大小”和“加载因子”的构造函数 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
```
**putMapEntries方法:**
```java
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 判断table是否已经初始化 if (table == null) { // pre-size // 未初始化,s为m的实际元素个数 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 计算得到的t大于阈值,则初始化阈值 if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,并且m元素个数大于阈值,进行扩容处理 else if (s > threshold) resize(); // 将m中的所有元素添加至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } }}
```
##### put方法
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
**对putVal方法添加元素的分析如下:**
- ①如果定位到的数组位置没有元素 就直接插入。
- ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用`e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
ps:下图有一个小问题,来自 [issue#608](https://github.com/Snailclimb/JavaGuide/issues/608)指出:直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行。
![1591855155154](../media/pictures/Collection.assets/1591855155154.png)
```java
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素 else { Node<K,V> e; K k; // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 将第一个元素赋值给e,用e来记录 e = p; // hash值不相等,即key不相等;为红黑树结点 else if (p instanceof TreeNode) // 放入树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 为链表结点 else { // 在链表最末插入结点 for (int binCount = 0; ; ++binCount) { // 到达链表的尾部 if ((e = p.next) == null) { // 在尾部插入新结点 p.next = newNode(hash, key, value, null); // 结点数量达到阈值,转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循环 break; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } // 表示在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null;}
```
**我们再来对比一下 JDK1.7 put方法的代码**
**对于put方法的分析如下:**
- ①如果定位到的数组位置没有元素 就直接插入。
- ②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。
```java
public V put(K key, V value) if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 先遍历 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); // 再插入 return null;}
```
##### get方法
```java
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 数组元素相等 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 桶中不止一个节点 if ((e = first.next) != null) { // 在树中get if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 在链表中get do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}
```
##### resize方法
进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
```java
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 超过最大值就不再扩充了,就只好随你碰撞去吧 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 没超过最大值,就扩充为原来的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 计算新的resize上限 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 把每个bucket都移动到新的buckets中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 原索引 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 原索引+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket里 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket里 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab;}
```
##### HashMap常用方法测试
```java
package map;import java.util.Collection;import java.util.HashMap;import java.util.Set;public class HashMapDemo { public static void main(String[] args) { HashMap<String, String> map = new HashMap<String, String>(); // 键不能重复,值可以重复 map.put("san", "张三"); map.put("si", "李四"); map.put("wu", "王五"); map.put("wang", "老王"); map.put("wang", "老王2");// 老王被覆盖 map.put("lao", "老王"); System.out.println("-------直接输出hashmap:-------"); System.out.println(map); /** * 遍历HashMap */ // 1.获取Map中的所有键 System.out.println("-------foreach获取Map中所有的键:------"); Set<String> keys = map.keySet(); for (String key : keys) { System.out.print(key+" "); } System.out.println();//换行 // 2.获取Map中所有值 System.out.println("-------foreach获取Map中所有的值:------"); Collection<String> values = map.values(); for (String value : values) { System.out.print(value+" "); } System.out.println();//换行 // 3.得到key的值的同时得到key所对应的值 System.out.println("-------得到key的值的同时得到key所对应的值:-------"); Set<String> keys2 = map.keySet(); for (String key : keys2) { System.out.print(key + ":" + map.get(key)+" "); } /** * 如果既要遍历key又要value,那么建议这种方式,应为如果先获取keySet然后再执行map.get(key),map内部会执行两次遍历。 * 一次是在获取keySet的时候,一次是在遍历所有key的时候。 */ // 当我调用put(key,value)方法的时候,首先会把key和value封装到 // Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取 // map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来 // 调用Entry对象中的getKey()和getValue()方法就能获取键值对了 Set<java.util.Map.Entry<String, String>> entrys = map.entrySet(); for (java.util.Map.Entry<String, String> entry : entrys) { System.out.println(entry.getKey() + "--" + entry.getValue()); } /** * HashMap其他常用方法 */ System.out.println("after map.size():"+map.size()); System.out.println("after map.isEmpty():"+map.isEmpty()); System.out.println(map.remove("san")); System.out.println("after map.remove():"+map); System.out.println("after map.get(si):"+map.get("si")); System.out.println("after map.containsKey(si):"+map.containsKey("si")); System.out.println("after containsValue(李四):"+map.containsValue("李四")); System.out.println(map.replace("si", "李四2")); System.out.println("after map.replace(si, 李四2):"+map); }}
```
### 6.1.Hashmap中value放在那里
放在Node节点里面
```java
//Node 节点private static class Node<K, V> { int hash; K key; V value; Node<K, V> next; public Node(int hash, K key, V value, Node<K, V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } @Override public String toString() { return key + "=" + value; }}
```
put的时候,参考Project里面的put方法。
### 7.HashMap的默认长度是多少?如果我new HashMap,指定其长度为15,那么其底层长度到底是多少?
默认长度(容量)为16,而且扩容以后长度始终是2的幂次数。
默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(3->4、7->8、9->16)
答案参考:https://blog.csdn.net/qq_27731689/article/details/95865083
### 8.hashmap键可以放空值进去吗?值可以为空吗?如果都为空之后再放新的为空的值进来会发生什么?
key和value都可以为空,如果两个键都为null的话,后面加进来的会覆盖掉前面一个。
### 9.Hashmap底层是如何实现的?什么情况下会变成红黑树?为什么阈值是8?
底层实现看上面。当阈值大于8的时候,会变为红黑树。
**TreeNodes占用空间是普通Nodes的两倍**,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由**TREEIFY_THRESHOLD**的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。
当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循**泊松分布**,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。由此可见,发展30年的Java每一项改动和优化都是非常严谨和科学的。
参考答案:https://mp.weixin.qq.com/s/QgkBRoADcO8Wgj0dHCc9dw
### 10.Hashmap的自动扩容机制?
参考上面resize()。
### 11.Map的实现类,HashMap的底层怎么实现,如果hash冲突链表会一直增加下去吗?除了链表变红黑树还有其他原因导致HashMap底层数据结构变化吗?
**Map实现类**:**HashMap**,**HashTable**,**ConcurrentHashMap,TreeMap**(对Key进行排序,使用了TreeMap存储键值对,再使用iterator进行输出时,会发现其默认采用key由小到大的顺序输出键值对),**LinkedHashMap**(采用双向列表)
不会一直增加下去,当阈值达到8以后,会变成红黑树。
### 12.为什么HashSet和HashMap在put的时候除了判断hashcode还要判断equals
阿里开发手册P27:
1)只要重写equal方法就必须重写hashCode。
2)因为Set存储的是不重复的对象,依据hashCode和equal进行判断,所以Set必须重写这两个方法。
3)如果自定义对象作为Map的对象,那么必须重写hashCode和equal方法。
**说明:真是由于String重写了hashCode方法和equal方法,所以我们可以非常愉快的将String作为Map的key来使用。**
### 13.HashMap的查询和插入的时间复杂度
理想情况下才可以是O(1)。
用HashMap查找分四步:
1.判断key,根据key算出索引。
2.根据索引获得索引位置所对应的键值对链表。
3.遍历键值对链表,根据key找到对应的Entry键值对。
4.拿到value。
分析:
以上四步要保证HashMap的时间复杂度O(1),需要保证每一步都是O(1),现在看起来就第三步对链表的循环的时
间复杂度影响最大,链表查找的时间复杂度为O(n),与链表长度有关。我们要保证那个链表长度为1,才可以说时
间复杂度能满足O(1)。但这么说来只有那个hash算法尽量减少冲突,才能使链表长度尽可能短,理想状态为1。因
此可以得出结论:HashMap的查找时间复杂度只有在最理想的情况下才会为O(1),而要保证这个理想状态不是我
们开发者控制的。
参考:https://blog.csdn.net/donggua3694857/article/details/64127131/
### 14.树的结构了解吗,HashMap?红黑树?平衡二叉树怎么平衡,怎么调节平衡因子?
红黑树的特性:
1)每个节点或者是黑色,或者是红色。
2)根节点是黑色。
3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
4)如果一个节点是红色的,则它的子节点必须是黑色的。
5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]
平衡二叉树:
它是一 棵空树或它的**左右两个子树的高度差的绝对值不超过1**,并且**左右两个子树都是一棵平衡二叉树**。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。
**平衡二叉树的平衡因子:**
**左子树深度-右子树深度=平衡因子**
怎么调节平衡因子?
常见四种LL,RR,LR,RL 参考:https://blog.csdn.net/weixin_40374341/article/details/87886807
### 15.集合框架
![1586522289820](D:/Code/Blog/JavaInterview/media/pictures/Collection.assets/Typora)
![1586522321227](../media/pictures/Collection.assets/1586522321227.png)
这个图显示,**而 Hashtable 类则继承Dictionary 类**,这句话是对的。(牛客上面原题)
![1586530161293](../media/pictures/Collection.assets/1586530161293.png)
集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。
数组与集合的区别如下:
1)数组长度不可变化而且无法保存具有映射关系的数据;集合类用于保存数量不确定的数据,以及保存具有映射关系的数据。
2)数组元素既可以是基本类型的值,也可以是对象;集合只能保存对象。
Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系,(**注意:Map不是Collection的子接口**)。
其中List代表了有序可重复集合,可直接根据元素的索引来访问;Set代表无序不可重复集合,只能根据元素本身来访问;Queue是队列集合;Map代表的是存储key-value对的集合,可根据元素的key来访问value。
上图中淡**绿色背景覆盖的是集合体系中常用的实现类**,分别是ArrayList、LinkedList、ArrayQueue、HashSet、TreeSet、HashMap、TreeMap等实现类。
### 16、ArrayList和HashMap是如何实现的?既然是数组实现的,为何数组长度固定,而ArrayList和HashMap不会越界?有了解过他们的这种自动扩容机制吗?
ArrayList是用数组实现的,HashMap是用数组+链表+红黑树实现。
不会越界,他们都有自动扩容机制。
HashMap的扩容机制:
resize()方法,首先判断超没超过最大值,如果超过最大值就不在扩容啦,如果没有超过最大值,就进容量扩大到原来的两倍。
```java
// 没超过最大值,就扩充为原来的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 移位运算符是亮点 }
```
ArrayList的扩容机制:
```java
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1);//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
```
### 17.list有100个元素,取前十个怎么取(sublist方法 用的index)
### 18. List,Set,Map三者的区别?
- **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
- **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。
- **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
### 19. ArrayList和HashMap的默认大小,各种集合常见扩容机制
**在 Java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)**
**这就是 Java 7 中 ArrayList 和 HashMap 类 的代码片段:**
```java
// from ArrayList.java JDK 1.7private static final int DEFAULT_CAPACITY = 10; //from HashMap.java JDK 7static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
```
**这里要讨论这些常用的默认初始容量和扩容的原因是:**
**当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复制到新的内存上,这无疑使效率大大降低。**
**加载因子的系数小于等于1,意指 即当 元素个数 超过 容量长度\*加载因子的系数 时,进行扩容。**
**另外,扩容也是有默认的倍数的,不同的容器扩容情况不同。**
**List 元素是有序的、可重复**
**ArrayList、Vector默认初始容量为10**
**Vector:线程安全,但速度慢**
**底层数据结构是数组结构**
**加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容**
**扩容增量:原容量的 1倍**
**如 Vector的容量为10,一次扩容后是容量为20**
**ArrayList:线程不安全,查询速度快**
**底层数据结构是数组结构**
**扩容增量:原容量的 0.5倍+1**
**如 ArrayList的容量为10,一次扩容后是容量为15*
**Set(集) 元素无序的、不可重复。**
**HashSet:线程不安全,存取速度快**
**底层实现是一个HashMap(保存数据),实现Set接口**
**默认初始容量为16(为何是16,见下方对HashMap的描述)**
**加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容**
**扩容增量:原容量的 1 倍**
**如 HashSet的容量为16,一次扩容后是容量为32**
**Map是一个双列集合**
**HashMap:默认初始容量为16**
**(为何是16:16是2^4,可以提高查询效率,另外,32=16<<1 -->至于详细的原因可另行分析,或分析源代码)**
**加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容**
**扩容增量:原容量的 1 倍**
**如 HashSet的容量为16,一次扩容后是容量为32**
### 20. ArrayList list = new ArrayList(20)需要扩容几次
```java
/*** 带初始容量参数的构造函数。(用户自己指定容量)*/public ArrayList(int initialCapacity) { if (initialCapacity > 0) {//初始容量大于0 //创建initialCapacity大小的数组 this.elementData = new Object[initialCapacity]; //###这就是答案 扩容0次 } else if (initialCapacity == 0) {//初始容量等于0 //创建空数组 this.elementData = EMPTY_ELEMENTDATA; } else {//初始容量小于0,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); }}
```
这种是指定数组大小的创建,创建时直接分配其大小,没有扩充。一次性为创建了传入的数字的长度的数组,所以,扩充为0次。
###
### 21. 单链表头尾翻转?
参考答案: https://www.cnblogs.com/mwl523/p/10749144.html
提供了两种思路,这里用其中一种思路:
方法:就地反转法
**思路**
把当前链表的下一个节点pCur插入到头结点dummy的下一个节点中,就地反转。
dummy->1->2->3->4->5的就地反转过程:
dummy->2->1->3->4->5
dummy->3->2->1->4->5
dummy->4>-3->2->1->5
dummy->5->4->3->2->1
**解释**
**初始状态**
![img](../media/pictures/Collection.assets/031542494733474.png)
**过程**
pCur是需要反转的节点。
1. prev连接下一次需要反转的节点
2. 反转节点pCur
3. 纠正头结点dummy的指向
4. pCur指向下一次要反转的节点
**伪代码**
```java
1 prev.next = pCur.next;2 pCur.next = dummy.next;3 dummy.next = pCur;4 pCur = prev.next;
```
今天在leetcode看到一个单链表翻转的原题:
```java
public class ListNode { //运行的时候 要把这里注释掉 int val; ListNode next; ListNode(int x) { val = x; }}class Solution { public int[] reversePrint(ListNode head) { Stack<ListNode> stack = new Stack<>(); ListNode temp = head; while (temp != null) { stack.push(temp); temp = temp.next; } int[] arr = new int[stack.size()]; for (int i = 0; i < arr.length; i++){ arr[i] = stack.pop().val; //栈顶端的元素 同时出栈 } return arr; }
```
### 22.青蛙跳台阶问题和斐波那契数列(兔子出生问题)比较:
#### 递归思想:
斐波那契数列:
```java
public static int fibSeq(int n){ if(n<0){ throw new IllegalArgumentException("the param is less than 0"); } if(n==0) return 0; if(n==1) return 1; return fibSeq(n-1);+fibSeq(n-2);}//王道的经典50道java题写法:private static int fun(int n){ if(n==1 || n==2) return 1; else return fun(n-1)+fun(n-2);}
```
爬楼梯:
```
public static int fibSeq(int n){ if(n<0){ throw new IllegalArgumentException("the param is less than 0"); } if(n==1) return 1; if(n==2) return 2; return fibSeq(n-1);+fibSeq(n-2);}
```
#### 动态规划思想:
斐波那契数列:
```java
public int fib(int n) { //动态规划做 int a = 0; int b = 1; int sum; for (int i = 0;i<n;i++) { sum = (a + b) % 1000000007; a = b; b = sum; } return a; }
```
青蛙跳台阶:
```java
public int fib(int n) { //动态规划做 int a = 1; int b = 2; int sum; for (int i = 0;i<n;i++) { sum = (a + b) % 1000000007; a = b; b = sum; } return a;
```
上面两个是不同的,主要体现在初始值a,b不同,Leetcode剑指offer上面指出:
![1586600321902](../media/pictures/Collection.assets/1586600321902.png)
由实际情况可知,青蛙跳台阶,f(1)=1,f(2)=2,才推算出f(0)=1 (个人认为),而题中所写的a,b指的是这个里面的f(0),和f(1)。
斐波那契数列为什么开始是0和1? 因为斐波那契数列前两项是默认指定的。(个人理解)
补充**动态规划**:
参考(写的非常好):https://www.cnblogs.com/cthon/p/9251909.html
### 23.时间复杂度空间复杂度
时间复杂度:算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示。
记作**T(n)=O(f(n)),**称**O(f(n))** 为算法的渐进时间复杂度,简称时间复杂度。
空间复杂度:
一个算法的空间复杂度(Space Complexity)S(n)定义为该算法所耗费的存储空间。
### 25.ArrayList和LinkedList区别:
- **1. 是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全;
- **2. 底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
- **3. 插入和删除是否受元素位置的影响:**
① **`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e) `方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。**
- **4. 是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。
- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
### 26.ArrayList 线程安全吗?为什么不安全?读过源码吗?
原码中add方法,没有加锁,如果现在数组中有9个元素,数组容量是10,那么多个线程同时操作的时候,会出现数组越界。
### 27.ArrayList和LinkedList区别,在同等数据量的情况下两者谁占的内存大?
LinkedList占用的内存大,以为链表我本身比数组占内存。
### 28.ArrayList底层原理是动态数组,查询效率比较高,但是在插入记录和删除记录的时候,效率会低一些。所以ArrayList可以初始化声明大小,和不声明有什么区别?
指定大小的话,就会用ArrayList默认的构造方法,不会再执行扩容机制(ArrayList扩容机制默认是1.5倍扩容)。
如果不指定大小的话,那么只能扩容,扩容设计到数组的复制等操作,很消耗性能。
### 29.ArrayList增加操作,扩容,为什么增删慢?
ArrayList执行`add(E e) `方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。删除也一样。
### 30.集合框架常用的数据结构
#### Collection
##### 1. List
- **Arraylist:** Object数组
- **Vector:** Object数组
- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
##### 2. Set
- **HashSet(无序,唯一):** 基于 HashMap 实现的,底层采用 HashMap 来保存元素
- **LinkedHashSet:** LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
- **TreeSet(有序,唯一):** 红黑树(自平衡的排序二叉树)
#### Map
- **HashMap:** JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
- **LinkedHashMap:** LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931)
- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- **TreeMap:** 红黑树(自平衡的排序二叉树)
### 31.HashSet的实现
HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变,此类允许使用null元素。
**在HashSet中,元素都存到HashMap键值对的Key上面,而Value时有一个统一的值private static final Object PRESENT = new Object()。**
### 32.什么样的哈希函数是一个好的哈希函数,怎么样解决哈希冲突?
参考 :https://blog.csdn.net/qq_40803710/article/details/80945617?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1
**哈希函数构造方法:**
**除留余数法**:哈希表长为m,p为小于等于m的最大素数,则哈希函数为 h(k)=k % p ,其中%为模p取余运算
**处理哈希冲突的方法**:
拉链法(HashMap的冲突处理方式,还有其它方法。
### 33.常见的数据结构有哪几种?
数据结构:是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。
包括三个组成成分:**数据的逻辑结构**、**物理结构(存储结构)**、**数据运算结构**。
数据的**逻辑结构**:
1、集合(数据之间无关系)
2、线性结构(一对一)
3、树形结构(一对多)
4、图形结构(多对多)
数据的**物理结构**:指数据在计算机存储空间的存放形式;
顺序存储、链表存储、索引存储、散列存储
**常用的数据结构**:
1、数组
2、栈(先进后出、线性表)
3、队列(先进先出、后进后出、线性表)
4、链表(每个节点包括两个部分:一个存储数据元素的数据域、另一个存储下一个节点地址的指针域)
5、树
6、图
7、堆(是一种动态的树形结构)
8、散列表
### 33.堆是什么数据结构?
堆可以看做是一种动态的树形结构。 常见有大顶堆小顶堆等。
### 34.什么是B+树,一些底层原理
B+树特征
B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点。B+ 树通常用于**数据库**和**操作系统的文件系统中**。
B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。 B+ 树元素自底向上插入。
**一个m阶的B树具有如下几个特征:**
1.根结点至少有两个子女。
2.每个中间节点都至少包含`ceil(m / 2)`个孩子,最多有m个孩子。
3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m。
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
![1586673715667](../media/pictures/Collection.assets/1586673715667.png)
而**只有叶子节点才会有data,其他都是索引**。
**B+树的优势:**
1.单一节点存储更多的元素,使得查询的IO次数更少。
2.所有查询都要查找到叶子节点,查询性能稳定。
3.所有叶子节点形成有序链表,便于范围查询。
补充:这个文章讲的很好,通俗易懂。
https://blog.csdn.net/qq_26222859/article/details/80631121 (程序员小灰 图文)
### 35.B+树与B树的区别
- 有k个子结点的结点必然有k个关键码;
- 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
- 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录
### 36.B-树,B+树与B*树的优缺点比较
首先注意:B树就是B-树,"-"是个连字符号,不是减号。
B-树是一种**平衡**的多路**查找(又称排序)**树,在文件系统中有所应用。主要用作文件的索引。**其中的B就表示平衡(Balance) **
**B+树有一个最大的好处,方便扫库,B树必须用中序遍历的方法按序扫库**,而B+树直接从叶子结点挨个扫一遍就完了。
**B+树支持range-query(区间查询)非常方便,而B树不支持**。这是数据库选用B+树的最主要原因。
**B*树 是B+树的变体**,**在B+树的非根和非叶子结点再增加指向兄弟的指针**
参考:https://blog.csdn.net/bigtree_3721/article/details/73632405
### 37.B树的查询复杂度?
logB(N)
参考:https://blog.csdn.net/houhouzhe/article/details/8556707
### 38.HashMap的插入流程是什么?并发情况是什么样的?
首先说,HashMap在高并发的时候会出现三个问题:
1)、同时put造成数据丢失
2)、扩容机制造成的数据丢失问题
3)、扩容机制造成死循环