是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。
1.集合:数据结构中的元素之间除了“同属一个集合”的相互关系
2.线性结构:数据结构中的元素存在一对一的相互关系
3.树形结构:数据结构中的元素存在一对多的相互关系
4.图形结构:数据结构中的元素存在多对多的相互关系
常用的数据结构:数组、栈、队列、链表、树、图、堆、散列表
数组:将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少插入和删除元素,就应该用数组。
数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像一排出租屋,有固定的100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。
数组的特点:元素类型是固定的,长度是固定的,通过角标查询,查询快、增删慢。
线性结构。只有一个口,有顺序的进出,并且只能一端进与出,称为LIFO,先进后出或者后进先出。类似于坐电梯,先进去的人给不断挤到后面,此举称之为压栈。底层是用LinkedList实现的。
线性结构。有顺序的进出,并且只能一端进另一端出,称为FIFO,先进先出,像一条水管。底层是用LinkedList实现的。
链表的类型有多种:单链表、双链表和有序链表
链表:元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针。如果访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单,只要修改元素中的指针。如果需要经常插入和删除元素就需要使用链表。
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针链接次序实现的。链表由一系列结点组成(链表中每一个元素称为结点),结点可以在运行时动态生成。
哈希表,是根据关键码值(Key Value)而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
HashMap先在内存中创建一个数组,然后每个元素都会被计算Hash值,然后通过Hash值再计算一层,得到其在数组中位置,由于hash值的特殊性,可能会存在两个元素被安排在数组中重复的位置,这时候就会创建一条链表,来维护重复的数据。
数组从栈中分配空间,自由度小。
链表从堆中分配空间,自由度大但申请管理麻烦。
数组必须事先定义固定的长度,不能适应数据动态地增减的情况。当数据增加时,超出原先定义的元素个数;
链表动态地进行存储分配,可以适应数据动态地增减,可以方便插入、删除数据项。
(1)存取方式,数组可以顺序存取或者随机存取,而链表只能顺序存取。
(2)存取位置,数组逻辑上相邻的元素在物理存储位置上相邻。
(3)按序号(下标)查找时,数组可以随机访问,时间复杂度为O(1),链表的时间复杂度为O(n)
(4)按值查找时,若数组无序,数组和链表的时间复杂度为O(n),但是当数组有序时,可以采用折半查找将时间复杂度降为O(log n)
(5)插入和删除元素时,数组平均需要移动N/2个元素,而链表只需修改指针即可。
整体上两大类接口,一类是Collection接口,另一类是Map接口。
Collection接口包含两大类:List接口和Set接口。
List接口:存储一组不唯一且按插入顺序排序的对象,可以操作索引。
Set接口:存储一组唯一且无序的对象。
Map接口:以键值对的形式存储元素,键(key)是唯一的。
1.如果元素可以重复,实现List接口
(1)需要查询快
(a)线程安全
Vector:由数组实现,访问元素的效率比较高,删除和添加元素效率低。
(b)线程不安全
ArrayList:由数组实现,访问元素的效率比较高,删除和添加元素效率低。
(2)需要增删快
(a)线程不安全
LinkedList:由链表实现,插入、删除元素效率比较高,访问效率比较低。
2.如果元素需要唯一不重复,实现Set接口
(1)需要查询快
(a)线程不安全
HashSet:由哈希表实现,使用了HashTable。添加、查询、删除元素的效率都很高,缺点是元素无序。通过hashcode与equals方法确保元素的唯一。
TreeSet:由二叉树实现。查询效率高,且元素有序的。存放自定义类型的对象需要实现Comparable接口,重写compareTo方法,提供对象排序的方式。
LinkedHashSet:由哈希表实现元素的存储,由链表实现元素的顺序。添加、查询、删除元素的效率都高,且元素都是有序的。
(2)需要排序
(a)线程不安全 TreeSet
3.通过键值对存取,实现map接口
(1)需要查询快
(a)线程不安全
HashMap:由hash表实现,底层是Hashtable,添加、查询、删除元素的效率都很高。
LinkedHashMap:由哈希表实现元素的存储,由链表实现元素的顺序。添加、查询、删除元素的效率都高,且元素都是有序的。
TreeMap:由二叉树实现。查询效率高,且元素有序的。
Hashtable:任何非null对象都可以用作键或值。为了成功地在哈希表中存储和获取对象,用作键的对象必须实现hashCode和equals方法。
ArrayList和LinkedList的区别。
(1)ArryaList和LinkedList都是线程不安全的。
(2)Arraylist底层是由数组实现的,数组的优点是通过下标访问,查询效率高,时间复杂度为O(1);增删因为牵扯到数组复制操作,所以增删效率不高。
(3)LinkedList的底层是由链表实现的,链表的优点是每个结点都有指向下条数据的指针,所以增删时只需要修改指针地址,查询时因为链表的数据结构在内存空间内是不连续的的且没有索引,所以查询效率低,时间复杂度为O(n)。
HashMap先在内存中创建一个数组,然后每个元素都会被计算hash值,然后通过hash值再计算,得到其在数组中的位置,由于hash值的特殊性,可能会存在两个元素被安排在数组中重复的位置,这时候就创建一个链表,来维护重复的数据。
搞清楚HashMap,首先需要知道HashMap是什么,即它的存储结构-字段;其次弄明白它能干什么,即它的功能实现-方法。
1.从结构实现来讲,HashMap是数组+链表+红黑树实现的。
(1)从源码可知,HashMap类中有一个非常重要的字段,就是Node[]table,即哈希桶数组,是一个Node的数组。
static class Node implements Map.Entry {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node next; //链表的下一个node
Node(int hash, K key, V value, Node next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
Node是HashMap的一个内部类,实现了Map.Entry接口,本质上就是一个映射(键值对)。
(2)HashMap使用哈希表存储,使用哈希表来解决哈希冲突。Java中的HashMap采用了链地址法。数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表。例如程序执行下面代码:
map.put("美团","小美");
系统将调用"美团"这个key的hashCode()方法得到其hashCode()值,然后通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置,有时两个key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结构越分散均匀,Hash碰撞的概率就越小,map的存取效率就越高。好的Hash算法和扩容机制来控制map使得Hash碰撞的概率变小,哈希桶数组Node[]table占用空间又少。
HashMap的默认构造函数源码,
int threshold; //所能容纳的key-value的极限
final float loadFactor; //负载因子
int modCount;
int size;
Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值为0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold=length*Load factor。
在HashMap中,哈希桶数组table的长度length大小必须为2的n次方。
2.功能实现—方法
(1)确定哈希桶数组索引位置
使用hash算法避免了遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。
方法一:
static final int hash(Object key){
int h;
//h = key.hashCode();取hashcode值
//h^(h>>>16); 高位参与运算
return (key==null)?0:(h=key.hashCode())^(h>>>16);
}
方法二:
static int indexFor(int h,int length){
return h&(length-1);
}
Hash算法本质:取key的hashCode、高位运算、取模运算。
(2)put方法的详细执行
(a)判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;——判断数组是否为空
(b)根据键值key计算hash值得到插入的数组索引i,如果table[i]=null,直接新建节点添加,转向(f),如果table[i]不为空,转向(c)——计算hash值判断索引i的位置
(c)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向(d),相同指的是hashcode以及equals;——判断table[i]的首个元素是否和key一样
(d)判断table[i]是否为treeNode,判断是否是红黑树
(e)遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树
(f)插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。
(3)扩容过程