一:Map
- Map:接口
HashMap:底层是哈希表,线程不安全
TreeMap:底层是二叉树,线程不安全
- Collection:直接存储的是值
- Map:本身是接口,存储的是键值对,一个元素就是一个键值对,key必须是唯一的,值随意,即可以重复
- Map接口的方法
- HashMap
1.增加:
2.删除:
3.获取:
4.常用的判断:
- Map的遍历
- HashMap存储数据的原理
图解:
- 代码
package com.qf.test; import java.security.KeyStore.Entry; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; //Set<K> keySet() 遍历方法一 //原理:先得到所有的key,放入一个Set中,利用Set的迭代器进行遍历得到key,在利用key获取value //Set<Map.Entry<K,V>> entrySet() 遍历方法二 //原理:先得到所有的entry,放入一个Set中,利用Set的迭代器进行遍历得到entry实体,在利用entry的方法获取key和value public class Demo2 { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); //1.增加 map.put("01", "java"); map.put("02", "html"); map.put("05", "iOS"); map.put("03", "BigData"); map.put("04", "iOS"); //方法一 test1(map); //方法二 test2(map); } //方法一 public static void test1(Map<String, String> map) { //第一步:先得到装着key的set Set<String> set = map.keySet(); //第二步:遍历set,得到key,再根据key获取value Iterator<String> iterator = set.iterator(); while (iterator.hasNext()) { String key = iterator.next(); System.out.println("key:"+key+" value:"+map.get(key)); } } //方法二 public static void test2(Map<String, String> map) { //第一步:先得到装着Entry实体的set Set<Map.Entry<String,String>> set = map.entrySet(); //第二步:遍历set,得到entry实体,再调用entry实体对象的方法获取key和value Iterator<Map.Entry<String,String>> iterator = set.iterator(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); //通过setValue可以将map的原始值改变,但是一般在使用entrySet的时候,是进行遍历.不进行值的改变. //entry.setValue("bingbing"); System.out.println("key1:"+entry.getKey()+" value1:"+entry.getValue()); } } }
- 自己定义的类通常要重写hashcode()方法和equals()方法
eg:常见面试题:
- TreeMap
public class Demo5 { public static void main(String[] args) { TreeMap<Dog, String> map = new TreeMap<>(); //如果没有重写compareTo或者compare方法,put内部无法调用元素的这两个方法.所以会报错 map.put(new Dog(), "haha"); } } class Dog implements Comparable<Dog>{ //2.元素可不可以作为key,跟元素内部的成员有没有关系 Object object; @Override public int compareTo(Dog o) { // TODO Auto-generated method stub return 0; } }
- 可变参数:参数的个数可以改变
作用:简化代码,简化操作
特点:1.给可变参数传值的实参可以直接写,个数不限制,内部会自动的将他们放入可变数组中.
2.当包括可变参数在内有多个参数时,可变参数必须放在最后面,并且一个方法中最多只能有一个可变参数
3.当可变参数的方法与固定参数的方法是重载关系时,调用的顺序,固定参数的优先于可变参数的
构成:数据类型+... ,实际上就是数据类型[ ] 。
package com.qf.test; /* * 可变参数:参数的个数可以改变 * 作用:简化代码,简化操作 */ public class Demo6 { public static void main(String[] args) { sum(2, 3);//值传递 //址传递 int[] arr = {3,4,5}; sum(arr); //可变参数 //可变参数的特点 //1.给可变参数传值的实参可以直接写,个数不限制,内部会自动的将他们放入可变数组中. sum1(5,6,7,8,9,3,3,4); //2.当包括可变参数在内有多个参数时,可变参数必须放在最后面,并且一个方法中最多只能有一个可变参数 //3.当可变参数的方法与固定参数的方法是重载关系时,调用的顺序,固定参数的优先于可变参数的. sum3(2,3); } //求两个数的和 //值传递 public static int sum(int a,int b) { return a+b; } //址传递 public static int sum(int[] a) { int sum = 0; for (int i = 0; i < a.length; i++) { sum+=a[i]; } return sum; } //通过可变参数 //构成:数据类型+... 实际上就是数据类型[] 即:int[] public static int sum1(int... a) { int sum = 0; for (int i = 0; i < a.length; i++) { sum+=a[i]; } return sum; } //2.当包括可变参数在内有多个参数时,可变参数必须放在最后面,并且一个方法中最多只能有一个可变参数 public static int sum2(float b,int... a) { int sum = 0; for (int i = 0; i < a.length; i++) { sum+=a[i]; } return sum; } //3.当可变参数的方法与固定参数的方法是重载关系时,调用的顺序,固定参数的优先于可变参数的. public static int sum3(int a, int b) { System.out.println("a"); int sum = 0; return sum; } public static int sum3(int... a) { System.out.println("b"); int sum = 0; return sum; } }
二:Hash表
#### 1.什么是Hash表
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,它是基于快速存取的角度设计的,也是一种典型的“空间换时间”的做法。顾名思义,该数据结构可以理解为一个线性表,但是其中的元素不是紧密排列的,而是可能存在空隙。
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。比如我们存储70个元素,但我们可能为这70个元素申请了100个元素的空间。70/100=0.7,这个数字称为负载(加载)因子。我们之所以这样做,也 是为了“快速存取”的目的。我们基于一种结果尽可能随机平均分布的固定函数H为每个元素安排存储位置,以达到快速存取。但是由于此随机性,也必然导致一个问题就是冲突。所谓冲突,即两个元素通过散列函数H得到的地址相同,那么这两个元素称为“同义词”。这类似于70个人去一个有100个椅子的饭店吃饭。散列函数的计算结果是一个存储单位地址,每个存储单位称为“桶”。设一个散列表有m个桶,则散列函数的值域应为[0,m-1]。

这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置
#### 2.hash表扩容的理解
可是当哈希表接近装满时,因为数组的扩容问题,性能较低(转移到更大的哈希表中).
Java默认的散列单元大小全部都是2的幂,初始值为16(2的4次幂)。假如16条链表中的75%链接有数据的时候,则认为加载因子达到默认的0.75。HahSet开始重新散列,也就是将原来的散列结构全部抛弃,重新开辟一个散列单元大小为32(2的5次幂)的散列结果,并重新计算各个数据的存储位置。以此类推下去.....
负载(加载)因子:0.75.-->hash表提供的空间是16 也就是说当到达12的时候就扩容
#### 3.排重机制的实现
假如我们有一个数据(散列码76268),而此时的HashSet有128个散列单元,那么这个数据将有可能插入到数组的第108个链表中(76268%128=108)。但这只是有可能,如果在第108号链表中发现有一个老数据与新数据equals()=true的话,这个新数据将被视为已经加入,而不再重复丢入链表。
#### 4.优点
哈希表的插入和查找是很优秀的.
对于查找:直接根据数据的散列码和散列表的数组大小计算除余后,就得到了所在数组的位置,然后再查找链表中是否有这个数据即可。因为数组本身查找速度快,所以查找的效率高低体现在链表中,但是真实情况下在一条链表中的数据又很少,有的甚至没有,所以几乎没有什么迭代的代价。所以散列表的查找效率建立在散列单元所指向的链表中数据的多少上.
对于插入:数组的插入速度慢,而链表的插入速度快.当我们使用哈希表时,不需要更改数组的结构,只需要在找到对应的数组下标后,进入对应的链表,操作链表即可.所以hash表的整体插入速度也很快.#### 5.模拟实现代码 Node类 ``` public class Node { // key、value模拟键值对的数据 public Integer key; public String value; // 下一节点的引用 public Node next; public Node() { } public Node(int key, String value) { this.key = key; this.value = value; } } ``` MyLinkedList类 ``` public class MyLinkedList { // 根节点 private Node root; public MyLinkedList() { root = new Node(); } /** * 添加数据,key值必须唯一,如果重复值将被覆盖 * @param key */ public void add(int key, String value) { Node newNode = new Node(key, value); Node current = root; while (current.next != null) { if(current.next.key == key) { current.next.value = value; return; } current = current.next; } current.next = newNode; } /** * 删除数据 * @param key * @return */ public boolean delete(int key) { Node current = root; while (current.next != null) { if(current.next.key == key) { current.next = current.next.next; return true; } current = current.next; } return false; } /** * 根据key获取value * @param key * @return */ public String get(int key) { Node current = root; while (current.next != null) { if(current.next.key == key) { return current.next.value; } current = current.next; } return null; } /** * 遍历链表,列出所有数据 * @return */ public String list() { String str = ""; Node current = root.next; while (current != null) { str += "(" + current.key + "," + current.value + "),"; current = current.next; } return str; } @Override public String toString() { return list(); } } ``` MyHashMap类 ``` // 哈希表 public class MyHashMap { // 链表数组,数组的每一项都是一个链表 private MyLinkedList[] arr; // 数组的大小 private int maxSize; /** * 空参构造,默认数组大小为10 */ public MyHashMap() { maxSize = 10; arr = new MyLinkedList[maxSize]; } /** * 带参构造,数组大小自定义 * @param maxSize */ public MyHashMap(int maxSize) { this.maxSize = maxSize; arr = new MyLinkedList[maxSize]; } /** * 添加数据,key值必须唯一 * @param key * @param value */ public void put(int key, String value) { int index = getHashIndex(key); if(arr[index] == null) { arr[index] = new MyLinkedList(); } arr[index].add(key, value); } /** * 删除数据 * @param key * @return */ public boolean delete(int key) { int index = getHashIndex(key); if(arr[index] != null) { return arr[index].delete(key); } return false; } /** * 根据key获取value * @param key * @return */ public String get(int key) { int index = getHashIndex(key); if(arr[index] != null) { return arr[index].get(key); } return null; } /** * 获取数组下标 * @param key * @return */ private int getHashIndex(Integer key) { return key.hashCode() % maxSize; } /** * 遍历数组中所有链表的数据 * @return */ public String list() { String str = "[ "; for (int i = 0; i < maxSize; i++) { if(arr[i] != null) { str += arr[i].toString(); } } str = str.substring(0, str.length()-1); str += " ]"; return str; } @Override public String toString() { return list(); } } ``` 测试类 ``` public class Test { public static void main(String[] args) { MyHashMap map = new MyHashMap(20); map.put(5, "aaa"); map.put(8, "bbb"); map.put(3, "ccc"); map.put(8, "bbb"); map.put(2, "ddd"); map.put(9, "eee"); System.out.println(map); System.out.println(map.get(3)); System.out.println(map.delete(2)); System.out.println(map); } }
三:HashMap和ConcurrentMap的区别
##### 1.为什么用HashMap?
HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改HashMap是非synchronized,所以HashMap很快HashMap可以接受null键和值,而Hashtable则不能(原因就是equlas()方法需要对象,因为HashMap是后出的API经过处理才可以)
##### 2.HashMap的工作原理是什么?
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node 。

以下是HashMap初始化 ,简单模拟数据结构**Node[] table=new Node[16]** 散列桶初始化,tableclass Node {hash;//hash值key;//键 value;//值 node next;//用于指向链表的下一层(产生冲突,用拉链法)}
以下是具体的put过程(JDK1.8版)
1、对Key求Hash值,然后再计算下标
2、如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)
3、如果碰撞了,以链表的方式链接到后面
4、如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表
5、如果节点已经存在就替换旧值
6、如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)
以下是具体get过程(考虑特殊情况如果两个键的hashcode相同,你如何获取值对象?)当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
##### 3.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置
##### **4.** HashMap和ConcurrentHashMap的区别
首先Map是接口,一般而言concurrentHashMap是线程安全的,具体实现
在1.7采取的segment分段锁,有点类似于16个线程安全的hashtable组合成了一个concurrenthashmap,不同分段操作不需要上锁,同一个分段才需要上锁,读不上锁,写上锁。锁的粒度更加精细,而在1.8中而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,而原有的Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本,反观HashMap是传统Map接口中所使用的实现了,操作速度快,但是线程是不安全的
##### **5.** segment分段锁
1、线程不安全的HashMap
因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
2、效率低下的HashTable容器
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
3、ConcurrentHashMap分段锁技术
ConcurrentHashMap是Java 5中支持高并发、高吞吐量的线程安全HashMap实现。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
##### 6.红黑树
红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。
特点:
1、每个节点非红即黑
2、根节点总是黑色的
3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
4、每个叶子节点都是黑色的空节点(NIL节点)
5、从根节点到叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
##### 7.大 **O** **表示法**
什么是大 O 表示法?
我们常常会听到有人说,“这个算法在最糟情况下的运行时间是 O(n^2) 而且占用了 O(n) 大小的空间”,他的意思是这个算法有点慢,不过没占多大空间。
这里的 `O(n^2)` 和 `O(n)` 就是我们通常用来描述算法复杂度的大 O 表示法。
大 O 表示法能让你对一个算法的**运行时间**和**占用空间**有个大概概念。
大 O 表示法怎么看?怎么用?
假设一个算法的时间复杂度是 O(n),n 在这里代表的意思就是数据的个数。举个例子,如果你的代码用一个循环遍历 100 个元素,那么这个算法就是 O(n),n 为 100,所以这里的算法在执行时就要做 100 次工作。
大O符号是关于一个算法的最坏情况的。比如说,你要从一个大小为 100 的数组(数组中存的都是数字)中找出一个数值等于 10 的元素,我们可以从头到尾扫描一遍,这个复杂度就是 O(n),这里 n 等于 100,实际上呢,有可能第 1 次就找到了,也有可能是第 100 次才找到,但是大 O 表示法考虑的是最坏的情况,也就是一个算法理论上要执行多久才能覆盖所有的情况。
常见的时间复杂度有:
常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),..., k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
说明:
大部分情况下你用直觉就可以知道一个算法的大 O 表示法
大 O 表示法只是一种估算,当数据量大的时候才有用
这种东西仅仅在比较两种算法哪种更好的时候才有点用。但归根结底,你还是要实际测试之后才能得出结论。而且如果数据量相对较小,哪怕算法比较慢,在实际使用也不会造成太大的问题。
要知道一个算法的大 O 表示法通常要通过数学分析。在这里我们不会涉及具体的数学,不过知道不同的值意味着什么会很有用。所以这里有一张方便的表。**n** 在这里代表的意思是数据的个数。举个例子,当对一个有 100 个元素的数组进行排序时,**n = 100**。
| **Big-O** | **名字** | **描述** |
| -------------- | ---------- | ------------------------------------------------------------ |
| **O(1)** | 常数级 | **最好的**。不论输入数据量有多大,这个算法的运行时间总是一样的。例子: 基于索引取出数组中对应的元素。 |
| **O(log n)** | 对数级 | **相当好**。这种算法每次循环时会把需要处理的数据量减半。如果你有 100 个元素,则只需要七步就可以找到答案。1000 个元素只要十步。100,0000 元素只要二十步。即便数据量很大这种算法也非常快。例子:二分查找。 |
| **O(n)** | 线性级 | **还不错**。如果你有 100 个元素,这种算法就要做 100 次工作。数据量翻倍那么运行时间也翻倍。例子:线性查找。 |
| **O(n log n)** | 线性对数级 | **还可以**。比线性级差了一些,不过也没那么差劲。例子:最快的通用排序算法。 |
| **O(n^2)** | 二次方级 | **有点慢**。如果你有 100 个元素,这种算法需要做 100^2 = 10000 次工作。数据量 x 2 会导致运行时间 x 4 (因为 2 的 2 次方等于 4)。例子:循环套循环的算法,比如插入排序。 |
| **O(n^3)** | 三次方级 | **特别慢**。如果你有 100 个元素,那么这种算法就要做 100^3 = 100,0000 次工作。数据量 x 2 会导致运行时间 x 8。例子:矩阵乘法。 |
| **O(2^n)** | 指数级 | **超级慢**。这种算法你要想方设法避免,但有时候你就是没得选。加一点点数据就会把运行时间成倍的加长。例子:旅行商问题。 |
| **O(n!)** | 阶乘级 | **比蜗牛还慢**!不管干什么都要跑个 N 年才能得到结果。 |
大部分情况下你用直觉就可以知道一个算法的大 O 表示法。比如说,如果你的代码用一个循环遍历你输入的每个元素,那么这个算法就是 **O(n)**。如果是循环套循环,那就是 **O(n^2)**。如果 3 个循环套在一起就是 **O(n^3)**,以此类推。
注意,大 O 表示法只是一种估算,当数据量大的时候才有用。
例如冒泡排序:
假设需要比较的数组中有N个元素,也就意味着外层循环会执行N次即O(n),而内层循环也是要执行N,所以此时冒泡排序的O(n^2)
最好情况下的时间复杂度:如果元素本来就是有序的,那么一趟冒泡排序既可以完成排序工作,比较和移动元素的次数分别是n-1和0,因此最好情况的时间复杂度为O(n)。
最差情况的时间复杂度:如果数据元素本来就是逆序的,进行n-1趟排序,所需比较和移动次数分别为n(n-1)/2和3n(n-1)/2。因此最坏情况子下的时间复杂度为O(n^2)。