Day14——数据结构和集合源码
1.数据结构
简单来说,数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,目的是加快程序的执行速度、减少内存占用的空间。
1.1 数据的逻辑结构
数据的逻辑结构指反映数据元素之间的逻辑关系,而与数据的存储无关,是独立于计算机的。
- 集合结构:数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系。集合元素之间没有逻辑关系。
- 线性结构:数据结构中的元素存在一对一的相互关系。比如:排队。结构中必须存在唯一的首元素和唯一的尾元素。体现为:一维数组、链表、栈、队列。
- 树形结构:数据结构中的元素存在一对多的相互关系。比如:家谱、文件系统、组织架构。
- 图形结构:数据结构中的元素存在多对多的相互关系。比如:全国铁路网、地铁图。
1.2 数据的存储结构(物理结构)
数据的物理结构/存储结构:包括数据元素的表示和关系的表示。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。
- 结构 1:顺序结构:顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素。
- 优点: 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现随机访问。
- 缺点: 必须静态分配连续空间,内存空间的利用率比较低。插入或删除可能需要移动大量元素,效率比较低。
- 结构 2:链式结构:不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。
- 优点:不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点。插入或删除元素时,不需要移动大量的元素。
- 缺点:需要额外的空间来表达数据之间的逻辑关系,不支持下标访问和随机访问。
- 结构 3:索引结构:除建立存储节点信息外,还建立附加的索引表来记录每个元素节点的地址。索引表由若干索引项组成。索引项的一般形式是:(关键字,地址)。
- 优点:用节点的索引号来确定结点存储地址,检索速度快。
- 缺点: 增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修改索引表,因而会花费较多的时间。
- 结构 4:散列结构:根据元素的关键字直接计算出该元素的存储地址,又称为 Hash 存储。
- 优点:检索、增加和删除结点的操作都很快。
- 缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。
1.3 运算结构
施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
- 分配资源,建立结构,释放资源
- 插入和删除
- 获取和遍历
- 修改和排序
2.一维数组
在 Java 中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型。
3.链表
链表中的基本单位是节点(Node)。
3.1 单向链表
class Node{
Object data;
Node next;
public Node(Object data){
this.data = data;
}
}
Node node1 = new Node("AA");
Node node2 = new Node("AA");
node1.next = node2;
3.2 双向链表
class Node{
Node prev;
Object data;
Node next;
public Node(Object data){
this.data = data;
}
public Node(Node prev,Object data,Node next){
this.prev = prev;
this.data = data;
this.next = next;
}
}
Node node1 = new Node(null,"AA",null);
Node node2 = new Node(node1,"BB",null);
Node node3 = new Node(node2,"CC",null);
node1.next = node2;
node2.next = node3;
4.二叉树
class TreeNode{
TreeNode left;
Object data;
TreeNode right;
public TreeNode(Object data){
this.data = data;
}
public TreeNode(TreeNode left,Object data,TreeNode right){
this.left = left;
this.data = data;
this.right = right;
}
}
TreeNode node1 = new TreeNode(null,"AA",null);
TreeNode leftNode = new TreeNode(null,"BB",null);
TreeNode rightNode = new TreeNode(null,"CC",null);
node1.left = leftNode;
node2.right = rightNode;
5.栈
- 特点为先进后出。
- 属于抽象数据类型ADT。
- 可以使用数组或链表来构建。
class Stack{
Object[] values;
int size;
public Stack(int length){
values = new Object[length];
}
//入栈
public void push(Object ele){
if(size >= values.length){
throw new RuntimeException("栈空间已满,入栈失败");
}
values[size] = ele;
size++;
}
//出栈
public Object pop(){
if(size <= 0){
throw new RuntimeException("栈空间已空,出栈失败");
}
Object obj = values[size - 1];
values[size - 1] = null;
size--;
return obj;
}
}
6.队列
- 特点为先进先出。
- 属于抽象数据类型ADT。
- 可以使用数组或链表来构建。
class Queue{
Object[] values;
int size;
public Queue(int length){
values = new Object[length];
}
//入队
public void add(Object ele){
if(size >= values.length){
throw new RuntimeException("队列空间已满,入队失败");
}
values[size] = ele;
size++;
}
//出队
public Object get(){
if(size <= 0){
throw new RuntimeException("队列空间已空,出队失败");
}
Object obj = values[0];
for(int i = 0;i < size - 1;i++){
values[i] = values[i + 1];
}
values[size - 1] = null;
size--;
return obj;
}
}
7.List实现类源码分析
7.1 ArrayList
7.1.1 ArrayList的特点
- 实现了List接口,存储有序的、可以重复的数据。
- 底层使用Object[]数组存储。
- 线程不安全的。
7.1.2 ArrayList源码解析
- JDK7.0版本:
//底层会初始化数组,数组的长度为10。Object[] elementData = new Object[10];
ArrayList<String> list = new ArrayList<>();
list.add("AA"); //elementData[0] = "AA";
list.add("BB"); //elementData[0] = "BB";
//当要添加第11个元素时,底层的elementData数组已满,则需要进行扩容。默认扩容到原来长度的1.5倍,并将原有数组中的元素复制到新的数组中。
- JDK8.0版本:
//底层会初始化数组,即:Object[] elementData = new Object[]{};
ArrayList<String> list = new ArrayList<>();
list.add("AA"); //首次添加元素时,会初始化数组elementData = new Object[10]。elementData[0] = "AA";
list.add("BB"); //elementData[0] = "BB";
//当要添加第11个元素时,底层的elementData数组已满,则需要进行扩容。默认扩容到原来长度的1.5倍,并将原有数组中的元素复制到新的数组中。
7.2 Vector
7.2.1 Vector的特点
- 实现了List接口,存储有序的、可以重复的数据。
- 底层使用Object[]数组存储。
- 线程安全的。
7.2.2 Vector源码解析(以JDK1.8.0_271为例)
//底层初始化数组,长度为10。Object[] elementData = new Object[10];
Vector v = new Vector();
v.add("AA"); //elementData[0] = "AA";
v.add("BB"); //elementData[1] = "BB";
//当添加到第11个元素时,需要扩容。默认扩容到原来的2倍。
7.3 LinkedList
7.3.1 LinkedList的特点
- 实现了List接口,存储有序的、可以重复的数据。
- 底层使用双向链表存储。
- 线程不安全的。
7.3.2 LinkedList在JDK8中的源码解析
LinkedList<String> list = new LinkedList<>();
list.add("AA"); //将"AA"封装到一个Node对象1中,list对象的属性first、last都指向此Node对象1。
list.add("BB"); //将"BB"封装到一个Node对象2中,对象1和对象2构成一个双向链表,同时last指向此Node对象2。
//因为LinkedList使用双向链表,不需要考虑扩容问题
//LinkedList内部声明:
private static class Node<E>{
E item;
Node<E> next;
Node<E> prev;
}
7.4 启示与开发建议
- 开发中基本不使用Vector。
- ArrayList底层使用数组结构:
- 查找和添加(尾部添加)操作效率高,时间复杂度为O(1)。
- 删除和插入操作效率低,时间复杂度为O(n)。
- LinkedList底层使用双向链表结构:
- 删除和插入操作效率高,时间复杂度为O(1)。
- 查找和添加(尾部添加)操作效率低,时间复杂度为O(n)。
- 在选择了ArrayList的前提下:
- new ArrayList():底层创建长度为10的数组。
- new ArrayList(int capacity):底层创建指定capacity长度的数组。
- 如果在开发中,大致可以确定数组的长度,则推荐使用ArrayList(int capacity)构造器,避免了底层的扩容和复制数组操作。
8.Map实现类源码分析
8.1 HashMap
8.1.1 HashMap中元素的特点
- HashMap中的所有的key彼此之间是不可重复的、无序的。所有的key构成一个Set集合。key所在的类要重写hashCode()和equals()方法。
- HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成了一个Collection集合。value所在的类要重写equals()方法。
- HashMap中的一个key-value构成一个entry。HashMap中所有的entry彼此之间是不可重复的、无序的。所有的entry构成了一个Set集合。
8.1.2 HashMap源码解析
- JDK7中创建对象和添加数据过程
//创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16];
HashMap<String,Integer> map = new HashMap<>();
//"AA"和11封装到一个Entry对象中,考虑将此对象添加到table数组中。
map.put("AA",11);
/**
* 添加/修改的过程:将(key1,value1)添加到当前的map中
* 首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2.
* 哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置1。
* 1.1 如果此索引位置i的数组上没有元素,则(key1,value1)添加成功。--->情况1
* 1.2 如果此索引位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2--->哈希冲突
* 2.1 如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功。--->情况2
* 2.2 如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals()。要调用key1所在类的equals(),将key2作为参数传递进去。
* 3.1 调用equals(),若返回false:则(key1,value1)添加成功。--->情况3
* 3.2 调用equals(),若返回true:则认为key1和key2是相同的,默认情况下value1替换原有的value2。
*
* 说明:情况1:将(key1,value1)存放到数组的索引i的位置
* 情况2和情况3:(key1,value1)元素与现有的(key2,value2)构成单向链表结构,(key1,value1)指向(key2,value2)
*
* 随着不断添加元素,在满足如下条件下考虑扩容:
* (size >= threshold) && (null != table[bucketIndex])
* 当元素个数达到临界值(->数组长度 * 加载因子)时,考虑扩容。默认临界值=16*0.75=12。
* 默认扩容到原来的2倍。
*/
-
JDK8与JDK7的不同之处
- 在JDK8中,当我们创建了HashMap实例之后,底层并没有初始化table数组。当首次添加(key,value)时进行判断,如果发现table尚未初始化,则对数组进行初始化。
- 在JDK8中,HashMap底层定义了Node内部类,替换JDK7中的Entry内部类。这意味着创建的数组为Node[]。
- 在JDK8中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标i中。若此时角标i位置上有元素,在JDK7中是将新的(key,value)指向已有的旧元素(头插法)。而在JDK8中是旧元素指向新的(key,value)元素(尾插法)。
- JDK7的结构组成为:数组+单向链表。
- JDK8的结构组成为:数组+单向链表+红黑树。
- 如果数组索引i位置上的元素个数达到8,并且数组的长度达到64时,我们就将此索引i位置上的多个元素改为使用红黑树的结构进行存储。因为在红黑树上进行put()/get()/remove()操作的时间复杂度为O(logn),比单向链表O(n)的性能较好。
-
属性/字段
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值 8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值 6,当树中结点的个数达到此阈值后,要考虑变为链表
//当单个的链表的结点个数达到 8,并且 table 的长度达到 64,才会树化。
//当单个的链表的结点个数达到 8,但是 table 的长度未达到 64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量 64
transient Node<K,V>[] table; //数组
transient int size; //记录有效映射关系的对数,也是 Entry 对象的个数
int threshold; //阈值,当 size 达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率
8.2 LinkedHashMap
8.2.1 LinkedHashMap与HashMap的关系
LinkedHashMap是HashMap的子类,LinkedHashMap在HashMap使用的数组+单向链表+红黑树的基础上,又增加了一对双向链表,记录添加的(key,value)的先后顺序。便于遍历所有的key-value。
LinkedHashMap重写了HashMap的方法:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash,key,value,e);
linkedNodeLast(p);
return p;
}
8.2.2 底层结构
LinkedHashMap内部定义了一个Entry:
static class Entry<K,V> extends HashMap.Node<K,V>{
Entry<K,V> before,after; //增加的双向链表
Entry(int hash,K key,V value,Node<K,V> next){
super(hash,key,value,next);
}
}