文章目录
1.链表
1.1 链表的定义
链表是由一组不必相连(不必相连:可以连续也可以不连续)的内存结构(链式存储结构),按特定的顺序链接在一起的抽象数据类型。
1.2 链表的种类
链表常用的有 3 类: 单链表、双向链表、循环链表。
1.3 链表和数组的区别
- 数组是一种连续存储线性结构,元素类型相同,大小相等;链表是离散存储线性结构,n 个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点,每个节点只有一 个后续节点,首节点没有前驱节点,尾节点没有后续节点。
- 数组存取速度快,插入删除元素很慢;链表存取速度很慢,插入删除元素很快
- 数组的空间通常是有限制的,需要大块连续的内存块;链表空间没有限制
- 数组事先必须知道数组的长度,链表不需要知道
1.4 单链表操作
1.4.1 链表的节点对象
Class Node<T>{
T data;//数据域
Node next;//指针域,如果是双向那就还有Node previous;
public Node{
}
public Node(T data, Node node){
this.data = data;
this.next = node;
}
}
1.4.2 单链表插入
//假设要插入的是节点p,位于节点q之后
Node tmp = q.next;
q.next = p;
p.next = tmp;
1.4.3 单链表删除
//假设要删除的是节点p之后的节点q
Node tmp = p.next.next;//q.next
p.next = tmp;
1.5 双向链表操作
1.5.1 双向链表插入
//假设要插入的是节点p,位于节点q之后
Node tmp = q.next;
q.next = p;
p.pre = q;
tmp.pre = p;
p.next = tmp;
1.5.2 双向链表删除
//假设要删除的是节点p之后的节点q
Node tmp = p.next.next;//q.next
p.next = tmp;
tmp.pre = p;
2.二叉树
2.1 二叉树的定义
二叉树是树的一种,每个节点最多可具有两个子树,即结点的度最大为 2
(结点度:结点拥有的子树数)。
2.1.1 二叉树节点对象
Class Node<T>{
T data;//数据域
//指针域
Node leftChild;
Node rightChild;
public Node{
}
public Node(T data, Node leftChild, Node rightChild){
this.data = data;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
}
2.2 二叉树的种类
2.2.1 斜树
所有结点都只有左子树,或者右子树。
2.2.2 满二叉树
所有的分支节点都具有左右节点,h层的满二叉树共有2^h-1 个结点(h≥1) 。
2.2.3 完全二叉树
若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
2.2.4 红黑树
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。当前根节点的左边全部比根节点小,当前根节点的右边全部比根节点大。
红黑树的特点:
- 节点可以是红色的或者黑色的
- 根节点是黑色的
- 叶子节点(特指空节点)是黑色的
- 每个红色节点的子节点都是黑色的
- 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同
- 速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍
2.3 二叉树的性质
- 二叉树第 i 层上的结点数目最多为 2^(i-1) (i≥1)
- 深度为 h 的二叉树至多有 2^h-1 个结点(h≥1)
- 包含 n 个结点的二叉树的高度至少为 log2 (n+1)
- 在任意一棵二叉树中,若终端结点的个数为 n0,度为 2 的结点数为 n2,则 n0=n2+1
2.4 二叉树的遍历
- 先序遍历
- 先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
- 中序遍历
- 先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
- 后序遍历
- 先访问左节点,然后访问右节点,最后访问根节点(左->右->根)
3.栈
3.1 栈的定义
又称堆栈, 栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为先进后出的线性表 。
压栈:存元素。
弹栈:取元素
3.2 栈的特点
- 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。
- 栈的入口、出口的都是栈的顶端位置。
4.队列
4.1 队列的定义
queue,简称队, 队列是一种特殊的线性表,是运算受到限制的一种线性表,只允许在表的一端进行插入,而在另一端进行删除元素的线性表。(当然也有双向队列和循环队列)
队尾(rear)是允许插入的一端。队头(front)是允许删除的一端。空队列是不含元素的空表。
4.2 队列的特点
- 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。
- 队列的入口、出口各占一侧。
5.类集设置的目的
普通的对象数组的最大问题在于数组中的元素个数是固定的,不能动态扩充大小,所以最早可以通过链表实现一个动态对象数组。但是这种也太复杂了。
所以为了方便用户操作各个数据结构,java引入了类集,有时候可以将类集看作是java对数据结构的实现。类集中最大的几个操作接口:Collection、Map、Iterator,这三个接口为以后要使用的最重点的接口。 所有的类集操作的接口或类都在 java.util 包中。
6.Collection接口
6.1 Collection接口的定义
Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。 此接口定义在 java.util 包中。
6.2 Collection的常用抽象方法
注:
- 集合转数组
必须使用集合的toArray(T[] array)
方法,传入类型完全一致的数组,大小最好是集合的长度,因为如果入参分配不够大的数组空间时,toArray方法自动重新分配内存空间,并返回新的数组地址;反之,如果数组空间过大,多余的部分会被置为null。
特别地,不要去使用去使用集合的toArray()
无参方法,否则它返回的只能是Object类,若是要强转其他类型,就会报转换异常。- 数组转集合 在使用工具类
Arrays.asList()
把数组转化成集合的时候,不能使用其修改集合相关的方法。因为这个方法返回的是Arrays的内部类,并没有实现集合的修改方法。该方法体现的是适配器模式,只是转换接口,后台的数据仍是数组,如果我们修改数组的数据,转换的集合的数据也会随之改变。
6.3 Collection接口的常用子接口
- List接口
- Set接口
7.List接口
7.1 List接口的定义
在整个集合中 List 是 Collection 的子接口,只能存取单个元素。
7.2 List接口的特点
- 有序的集合(存储和取出的元素顺序相同)
- 允许存入重复的元素
- 有索引,可以用一般的for循环
7.3 List接口在Collection接口基础上的扩充方法
7.4 List接口的实现类
7.4.1 ArrayList(最常用,95%)
此类继承了 AbstractList 类。AbstractList 是 List 接口的子抽象类。适配器设计模式。
注意:不要什么都想着用ArrayList!!
7.4.1.1 ArrayList的特点
- 效率高
- 底层是一个数组结构,元素增删慢,查取快
- 不是同步的,线程不安全。
- 不指定长度的话刚开始是0,然后往里添加的时候是默认的10,1.5倍左右扩容(扩容的情况可以去看源码)
注意:ArrayList的subList方法可以获取结果子集,它返回的是ArrayList的内部类SubList并不是ArrayList,因此不可以强制转换,也不要尝试去增加或删除元素。
7.4.2 Vector(ArrayList的早期实现,4%)
此类与 ArrayList 类一样,都是 AbstractList 的子类,可以与ArrayList对比来看。
7.4.2.1 Vector的特点
- 是同步的,线程安全
- 效率高,底层是一个数组结构,元素增删慢,查取快
- 指定大小扩容,不指定就是翻一倍容量
7.4.3 LinkedList(1%)
此类继承了 AbstractList类,所以是 List 的子类。
但是此类也是 Queue 、Deque接口的子类,可以作为队列使用,实现了Cloneable接口可以实现克隆,实现了java.io.Serializable接口,支持序列化。
7.4.3.1 LinkedList的特点
- 不是同步的,线程不安全
- 效率高,底层是一个双向链表,增删快,查取慢。
- 可以作为队列或者栈去使用
8. Set接口
8.1 Set接口的定义
继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。
8.2 Set接口的特点
-
元素无序;
-
都会以某种规则保证存入的元素不出现重复,最多一个null;
-
没有索引(不能使用普通的for循环遍历)。
8.3 Set接口的实现类
8.3.1 HashSet
java.util.HashSet 是 Set 接口的一个实现类,底层的实现其实是一个 java.util.HashMap 支持。
8.3.1.1 HashSet的特点
-
元素无序;
-
都会以某种规则保证存入的元素不出现重复,最多一个null;
-
根据对象的哈希值来确定元素在集合中的存储位置,具有良好的存取和查找性能;
-
如果需要使用HashSet存储自定义类型元素,那么必须保证存储的元素重写hashCode 与 equals 方法。
因为set集合需要保证元素唯一),如果不重写HashCode方法,那么Object类默认的HashCode方法会根据对象的存储地址来得出散列值,那散列值基本就不可能相等,set可能会被存入同一个数据,但是它认为是不同的。
8.3.2 TreeSet
java.util.TreeSet是Set接口的一个实现类,采用有序的二叉树进行存储,底层的实现其实是一个 java.util.TreeMap 支持。
所提供的方法和HashSet的方法基本一致,只不过存储的方式不一样。
需要注意的是,如果要比较自定义元素,需要对这个元素类型实现Comparble接口定义compareTo方法,或者传入一个Comparator比较器。
8.3.3 LinkedHashSet
java.util.LinkedHashSet集合继承于HashSet集合,是Set接口的实现类的子类。
8.3.3.1 LinkedHashSet的特点
-
不允许存储重复的元素;
-
底层是一个哈希表(数组+链表or红黑树)+链表结构
-
比HashSet多了一条双向链表(记录元素的存储顺序,保证元素有序,如果插入同样的值,那么这个key的插入顺序就变了)。
如果元素已存在,重新插入后,会被重新插入的set中,比如一共5个元素,原来的元素1是第2个插入的,现在再插入元素1,元素1的插入顺序就是第5.
8.4 安全失败和快速失败
- 安全失败: Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响,java.util.concurrent包下面的所有的类都是安全失败的。
- 快速失败: 操作的是底层集合本身,java.util包下面的所有的集合类都是快速失败的。
9. Iterator接口
9.1 Iterator接口的定义
在程序开发中,经常需要遍历集合中的所有元素。Iterator 接口也是Java集合中的一员,但它与 Collection 、 Map 接口有所不同, Collection 接口与 Map 接口主要用于存储元素,而 Iterator 主要用于迭代访问(即遍历) Collection 中的元素,因此 Iterator 对象也被称为迭代器。
9.2 迭代器操作
-
public Iterator iterator() :
获取集合对应的迭代器,用来遍历集合中的元素的。
-
public boolean hasNext() :
如果仍有元素可以迭代,则返回 true。
-
public E next() :
返回迭代的下一个元素。
注意:
不同的集合可以获取对应的不同的迭代器,这些迭代器实现了Iterator接口,拥有更多的操作方法:add()、previous()……
9.3 增强for
最早出现在C#,增强for循环(也称for each循环)是JDK 1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。
它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
注意:不要在foreach里面循环中进行元素的增加或删除操作,remove元素请使用
Iterator
方式,如果并发操作,需要对Iterator
对象加锁。参考一位仁兄的解释。
10. Comparator比较器
10.1 什么是Comparator?
排序用来比较的,简单的说就是两个对象之间比较大小。那么在JAVA中提供了两种比较实现的方式:
- 一种是 比较死板的采用 java.lang.Comparable 接口去实现;(每次排序都按照既定规则去走)
- 一种是灵活的,当我需要做排序的时候在去选择的 java.util.Comparator 接口完成。(排序的时候给他传过去即可,每次排序都可以重新定义排序规则)
10.2 Comparable和Comparator两个接口的区别?
-
Comparable强行对实现它的类对象(这个类实现了Comparable,重写了compareTo方法)进行整体排序;Comparator强行对某些对象(对象所属的类可以不实现,用的时候传入即可)进行整体排序。
(也就是说Comparble对实现了它的这个类的所有对象都定义一套规则去排序,Comparator只是设计一套规则对于这个类的某些对象去排序)
-
Comparable只能在类中实现compareTo()一次;可以将Comparator 传递给sort方法(如Collections.sort 或 Arrays.sort,也就是可以在不同使用时定义不同规则),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构 (如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
11. Map接口
11.1 Map接口的定义
Collection 中,每次操作的都是一个对象,如果现在假设要操作一对对象(键值对),则就必须使用 Map 了。里面的所有内容都按照 key->value 的形式保存,也称为二元偶对象。
11.2 Map接口的特点
- 键KEY唯一,值VALUE可以重复,每个键对应一个值(Set接口的实现类底层实际调用了Map接口的实现类,用键去存数据)
- 键只允许一个为null
- 无序
- Map 接口本身是不能直接使用 Iterator 进行输出的,而是使用 Map 接口中的 entrySet()方法将 Map 接口的全部内容变为 Set 集合,然后使用 Set 接口中定义的 iterator()方法为 Iterator 接口进行实例化
11.3 Map接口的常用方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LGoySUv7-1616253311918)(C:\Users\82169\AppData\Roaming\Typora\typora-user-images\image-20210312180647314.png)]
11.4 Map接口的实现类
11.4.1 HashMap
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。 但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
扩容机制:如果对象数组长度不到64,链表的长度超过8的时候,会去扩容对象数组;只有数组长度到达64之后,链表的长度再超过8的时候,会去把链表转换成红黑树。
注意:
- JDK1.8之前,HashMap插入链表是采用的头插法,后来用的是尾插法,头插法效率高于尾插法。之所以改成尾插法,是因为需要判断链表的长度来决定是否树化。而且头插法在多线程中可能会造成链表回环(多线程情况下)。
- HashMap的默认长度是16(注意不是说一开始就是,如果没有指定初始长度,那么一开始长度是0,往里添加数据的时候才会触发扩容机制),默认扩充因子是0.75F(超过75%的桶使用时),扩充一倍的空间。
- 如果HashMap的对象数组中某个链表的数据量减少到6的时候,原来是红黑树的,则会从红黑树转换成链表,如果原来不是红黑树就不会。
- 如果HashMap的键设置的数据类型是自定义类型的时候一定要注意,存入后这个自定义类型的对象不要轻易改变:
- 因为一改变,再去找的时候,hashcode发生了变化,就没办法再去根据hashcode去找到原来存储的位置了
- 如果再创建一个内容相同的对象,用这个对象作为键去找,确实可以找到,但是内部会调用equals方法,也就是说hashcode一样了,equals不一样(原来的对象里的数据已改变,和新创建的这个对象匹配不上)
- 在集合初始化的时候,能确定初始化大小的,就指定集合初始化大小,因为扩容resize需要重建hash表,严重影响性能。
- 使用
entrySet
或者Map.forEach(jdk1.8)
方法遍历Map类集合,而不是使用keySet,因为keySet方法其实遍历了2次,一次是转为Iterator对象,另一次是从集合中取出key对应的value。
//源码
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,默认长度
static final int MAXIMUM_CAPACITY = 1 << 30;//2^30,int是四个字节
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认扩容因子
static final int TREEIFY_THRESHOLD = 8;//树化的阈值
static final int UNTREEIFY_THRESHOLD = 6;//重新链化的阈值
static final int MIN_TREEIFY_CAPACITY = 64;//只有数组长度到达64之后,链表的长度超过8的时候,会去把链表转换成红黑树
注:ConcurrentHashMap的key和Value都不能为null
补充:https://blog.csdn.net/weixin_44460333/article/details/86770169
11.4.2 Hashtable
11.4.3 LinkedHashMap
与HashMap最大的不同在于,后者维护了一个运行于所有条目的双向链表,此链表定义了迭代顺序,如果重新插入一个键值对(键已经存在,那么这个键的插入顺序就会改变)
11.4.4 ConcurrentHashMap
-
JDK 1.7 数组+Segment+分段锁的方式实现。
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
-
坏处是这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长。
-
好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。
所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。
-
-
JDK 1.8 彻底放弃了Segment转而采用的是Node,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。
- Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。
- CAS是compare and swap的缩写,cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源。
- CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。