数据结构与集合源码1
数据结构与集合源码1
1.数据结构概念:
总结:简单来说,数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,目的是加快程序的执行速度、减少内存占用的空间。
1.1研究对象一:数据间逻辑关系
数据的逻辑结构指反映数据元素之间的逻辑关系,而与数据的存储无关,是独立于计算机的。
- 集合结构:数据结构中的元素之间除了“同属一个集合”的相互关系外,别无其他关系。集合元素之间没有逻辑关系。
- 线性结构:数据结构中的元素存在一对一的相互关系。比如:排队。结构中必须存在唯一的首元素和唯一的尾元素。体现为:一维数组、链表、栈、队列
- 树形结构:数据结构中的元素存在一对多的相互关系。比如:家谱、文件系统
组织架构 - 图形结构:数据结构中的元素存在多对多的相互关系。比如:全国铁路网、地铁图
研究对象2:数据的存储结构(或物理结构)
- 顺序结构
- 链式结构
- 索引结构
- 散列结构
开发中,我们更习惯上如下的方式理解存储结构:
- 线性表(一对一关系):一维数组、单向链表、双向链表、栈、队列
- 树(一对多关系)∶各种树。比如:二叉树、B+树
- 图(多对多关系)
- 哈希表:比如: HashMap、HashSet
研究对象3:相关的算法操作
- 分配资源,建立结构,释放资源
- 插入和册除
- 获取和遍历
- 修改和排序
链表中的基本单位是:节点(Node)
4.1单向链表
class Node{
0bject data;
Node next;
public Node(Object data){
this.data = data;
}
}
创建对象:
Node node1 = new Node( "AA" );
Node node2 = new Node("BB");
node1.next = node2;
4.2 双向链表
6.常见存储结构之:栈(stack、先进后出、first in last out、FILO、LIF0)
- 属于抽象数据类型(ADT)
- 可以使用数组或链表来构建
class Stack{
0bject[] values;
int size;//记录存储的元素的个数
public Stack(int length){
values = new 0bject[length];
}
//入栈
public void push(Object ele){
if(size >= values.length){
throw new RuntimeException("栈空间已满,入栈失败");
}
values[size] = ele;size++;
}
//出栈
public 0bject pop(){
if(size <= 0){
throw new RuntimeException("栈空间已空,出栈失败");
}
0bject obj = values[size - 1];values[size - 1] = null;
size--;
return obj;
7.常见存储结构之:队列(queue、先进先出、first in first out、FIF0)
- 属于抽象数据类型(ADT)
- 可以使用数组或链表来构建
//数组实现队列class Queue{
0bject[ ] values;
int size;//记录存储的元素的个数
public Queue(int length){
values = new 0bject[length];
}
public void add(0bject ele){//添加
if(size >= values.length){
throw new RuntimeException("队列已满,添加失败");
}
values[size] = ele;size++;
}
public Object get(){1/获取
if(size <= 0){
throw new RuntimeException("队列已空,获取失败");
}
Object obj = values[0] ;
for(int i = 0; i < size - 1; i++){
values[i] = values[i + 1];
}
0bject obj = values[0] ;
//数据前移
for(int i = 0; i < size - 1; i++){
values[i] = values[i + 1];
}
//最后一个元素置空
vlaues[size - 1] = null;
size--;
return obj;
}
02-List实现类源码分析.
一、ArrayList
-
ArrayList的特点:
- 实现了List接口,存储有序的、可以重复的数据
- 底层使用0bject[]数组存储
- 线程不安全的
22 jdk8版本:(以jdk1.8.0_271为例)
//如下代码的执行:底层会初始化数组,即:0bject[] elementData = new 0bject[]0;lIArrayList<String> list = new ArrayList<>();
list.add("AA");//首次添加元素时,会初始化数组elementData = new 0bject[10];elementData[0] = "AA'
list.add("BB");l/elementData[1] ="BB";
. ..
当要添加第11个元素的时候,底层的elementData数组已满,则需要扩容。默认扩容为原来长度的1.5倍。并将原有数组中的元素复制到新的数组中。
二、Vector
- Vector的特点:
- 实现了List接口,存储有序的、可以重复的数据
- 底层使用0bject[]数组存储
- 线程安全的
三、LinkedList
- LinkedList的特点:
- 实现了List接口,存储有序的、可以重复的数据
- 底层使用双向链表存储
- 线程不安全的
四、启示与开发建议
- Vector基本不使用了。
- ArrayList底层使用数组结构,查找和添加(尾部添加)操作效率高,时间复杂度为0(1)
册除和插入操作效率低,时间复杂度为0(n)
LinkedList底层使用双向链表结构,删除和插入操作效率高,时间复杂度为0(1)
查找和添加(尾部添加)操作效率高,时间复杂度为0(n)(有可能添加操作是0(1) - 在选择了ArrayList的前提下,new ArrayList() ︰底层创建长度为10的数组。
new ArrayList(int capacity):底层创建指定capacity长度的数组。
如果开发中,大体确认数组的长度,则推荐使用ArrayList(int capacity)这个构造器,避免了底层的扩容、复制
03-Map实现类源码分析.
-
HashMap源码解析
2.1 jdk7中创建对象和添加数据过程(以JDK1.7.0_07为例说明):
//创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16];HashMap<String , Integer> map = new HashMap<>();
.. .
map.put("AA",78);//"AA"和78封装到一个Entry对象中,考虑将此对象添加到table数组中
添加/修改的过程:
将(key1,value1)添加到当前的map中:
首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2。哈希值2再经过某种算法(indexFor())之后,就确定了(key1, value1)在数组table中的索引位置i。
1.1如果此索引位置i的数组上没有元素,则(key1,value1)添加成功。---->情况1
1.2如果此索引位置i的数组上有元素(key2, value2),则需要继续比较key1和key2的哈希值
--->哈希冲突
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[i])
当元素的个数达到临界值(->数组的长度*加载因子)时,就考虑扩容。默认的临界值 = 16 * 0.75 -->12.默认扩容为原来的2倍。
JDK7中的HashMap的源码
1.实例化
HashMap<String, Integer> map = new HashMap<>( );
对应的源码:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
public HashMap( int initialCapacity,float loadFactor) {
l l ...略...
//通过此循环,得到capacity的最终值,此最终值决定了Entry数组的长度。此时的capacity一定是2的整数倍int capacity = 1;
I
while (capacity < initialCapacity)
capacity <<= 1;
this. loadFactor = loadFactor;//确定了加载因子的值
threshold = ( int)Math.min( capacity * loadFactor,MAXIMUM_CAPACITY +1);//确定了临界值
table = new Entry [ capacity ] ;//初始化数组,长度为capacity
其中:
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 8.75f;
final float loadFactor; //加载因子
int threshold ; //临界值
transient Entry<K,V>[ ] table;//存储数组的数组
2.put(key,value)的过程
public v put(K key,v value) {
//HashMap允许添加key为null的值。将此(key, value)存放到table索引8的位置。
if (key == null)
return putForNullKey( value) ;
//将key传入hash(),内部使用了key的哈希值1,此方法执行结束后,返回哈希值2
int hash = hash(key) ;//确定当前key,value在数组中的存放位置i
int i = indexFor (hash, table.length);
for (Entry<K,v> e = table[i]; e != null; e = e.next) {
0bject k ;
if ( e.hash == hash && ((k = e.key) == key ll key.equals(k) )) {
v oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;//如果put是修改操作,会返回原有旧的value值。
}
}
// ... . .
addEntry (hash,key,value,i);//将key , value封装为一个Entry对象,并将此对象保存在索引i位置
return null;//如果put是添加操作,会返回null.
static int indexFor( int h, int length) {
return h & ( length-1);
}
void addEntry(int hash,K key,v value,int bucketIndex) {
//扩容的条件
if ( (size >= threshold) && (null != table[ bucketIndex] ) ) {
resize(2 * table. length); /!默认扩容为原有容量的2倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor (hash,table.length) ;
}
createEntry ( hash, key, value,bucketIndex);
}
void createEntry(int hash,K key,v value,int bucketIndex) {
Entry<K, V> e = table [ bucketIndex ] ;
table[ bucketIndex] = new Entry<>( hash, key, value,e);
size++;
3.Entry的定义如下:
static class Entry<K,V> implements Map.Entry<K,V>{
final K key;
v value ;
Entry<K, V> next;
int hash;//使用key得到的哈希值2进行赋值。
// Creates new entry .
Entry (int h, K k, v v, Entry<K, V> n) {
value = v ;
next = n;key = k;hash = h ;
}
2.2 jdk8与jdk7的不同之处(以jdk1.8.0_271为例):
-
在jdk8中,当我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key , value)时,进行判断,如果发现table尚未初始化,则对数组进行初始化。
-
在jdk8中,HashMap底层定义了Node内部类,替换jdk7中的Entry内部类。意味着,我们创建的数组是Node[]
-
在jdk8中,如果当前的(key , value)经过一系列判断之后,可以添加到当前的数组角标i中。如果此时角标i位置上有元素。在jdk7中是将新的(key , value)指向已有的旧的元素(头插法),而在jdk8中是旧的元素指向新的(key , value)元素(尾插法)。"七上八下
-
jdk7:数组+单向链表
jk8:数组+单向链表+红黑树 -
什么时候会使用单向链表变为红黑树:
如果数组索引i位置上的元素的个数达到8,并且数组的长度达到64时,我们就将此索引i位置上的多个元素改为使用红黑树的结构进行存储。(为什么修改呢?红黑树进行put()/get() /remove操作的时间复杂度为O(logn),比单向链表的时间复杂度O(n)的好。性能更高。
-
什么时候会使用红黑树变为单向链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构退化为单向链表。
二、LinkedHashMap
1.LinkedHashMap 与 HashMap的关系:
- LinkedHashMap 是 HashMap的子类。
- LinkedHashNap在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);
linkNodeLast(p);
return p;
}
2.底层结构:LinkedHashMap内部定义了一个Entry
static class Entry<k,V> extends HashMap.Node<K,V> {
Entry<K,V> before,after;1/增加的一对双向链表
Entry(int hash,K key, v value,Node<k,V> next) {
super(hash,key, value, next);
}
}
三、HashSet和LinkedHashSet的源码分析
- HashSet底层使用的是HashMap
- LinkedHashSet底层使用的是LinkedHashMap
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?