数据结构
一:概要
1.什么是数据结构:
是用来做计算存储,组织数据的一种方式,是相互之间,存在一种或者多种特定关系的数据结合。数据结构需要高效的算法和索引技术。
2.常见的数据结构类型:
数组(Array),链表(LinkedList),队列(Queue),栈(Stack),哈希表(Hash),树(Tree),等等,而我们使用较多的一般为前几种
3.数据结构算法的重要性:
无非就是两个原因,高效的存储以及运行效率。数据结构的效率评估通常是通过与其他数据结构或实现方式的对比来完成的,目的是找到最适合特定场景的存储和操作方式。
ArrayList LinkedList HashMap已经被高度封装由Java标准库提供给我们使用,这些类的实现代码位于 java.util 包中,是 Java 核心库的一部分。
二:ArrayList的引出使用

在idea下我们可以很清晰的知道ArrayList是抽象List类的子类
private static final int DEFAULT_CAPACITY = 10;//初始默认容量
private int size;//ArrayList的元素个数(自己添加)
private static final Object[] EMPTY_ELEMENTDATA = {};//一个静态的空数组实例,用于表示空的ArrayList实例,作为一个共享常量,所有的空ArrayList都可以引用他,避免重复创建
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//与上者不同,当第一个元素被添加至ArrayList时会触发默认容量扩容
transient Object[] elementData;//这是 ArrayList 内部用于存储元素的数组缓冲区
对ArrayList构造器来说,如果参数传值为空则分配一个共享的空数组实例,在第一次被添加元素时才会分配一个默认值为10的容量,当然如果传参数为正常int数时则会被正常创建
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
对于ArrayList扩容来说,第一个方法扩容会至少扩充到minCapacity而更倾向于1.5倍原长度
如果没有传值则会默认对当前size+1
private Object[] grow(int minCapacity)
private Object[] grow()
对于添加元素和删除元素是由add和remove完成,add不多赘述,而remove需要考虑是直接设置为null还是需要删除元素,例子如下:
public static void remove(int site) {
if (site <= 0 || site > size) {
System.out.println("当前位置不存在");
return;
}
stus[--site] = null;
}
public static void removeCopy(int site) {
if (site <= 0 || site > size) {
System.out.println("当前位置不存在");
return;
}
for (int i = site - 1; i < size - 1; i++) {
//把索引位置为 site 位置的元素移动到 site -1 位置
stus[i] = stus[i + 1];
}
stus[--size] = null;
}
三 :LinkedList链表的使用
链表分为单向链表以及双向链表。

我们可以从图中了解到链表的继承关系,以及对于接口的实现
从代码里我们可以去观察一下链表的内部结构
链表的基本属性
transient int size = 0; transient Node<E> first; transient Node<E> last; Node类的属性 E item; Node<E> next; Node<E> prev;
这段代码定义了一个双向链表的基本结构,包括节点类和链表的属性。每个节点包含数据、指向前一个节点和后一个节点的引用,而链表则通过 first 和 last 来管理链表的头和尾,并通过 size 记录链表的长度。
对于链表的构造
仍然和ArrayList比较相同的提供无参构造器和带参构造器
//无参构造器,创建一个空链表
public LinkedList() {
}
//带参构造器,传入集合通过
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
链表操作的实现
//链表的添加或者是修改
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c)
//具体实现代码量比较多,后者不具体列出,总体思想为如果不带参数默认为在链表最后插入,如果带参数在链表参数位置插入,最后要连接链表,如果在链表头插入,则要多一次判断并设置first值为newNode。
//前者为对对象的插入,单add可以用addFirst或者addLast实现
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}//linkFirst和linkLast的代码实现比较简单,可以在结构体里自己查询
//clear为清空链表,remove为清空元素,具体实现方式比较多可通过remove查看
对于链表的查看可与队列相联系(后续会在队列提及)
四:队列以及栈
队列常用的为双向队列Deque,Deque继承于Queue,而Queue继承于Collection
不同的是,上述的三者都是接口,不同于前面ArrayList和LinkedList有具体的实现类

从图中的层次能清楚LinkedList同样实现了Deque接口,所以上述提及的对于链表的查看可以通过peek查看,而且同样能pop,pull以及push,所以对于前面的问题在此处能有更清晰的理解
对于栈来说,大多数的操作和队列都比较相似,图中为栈的继承结构,栈也是一种内部的实现类

栈的实际运用会比较多,栈实际为受限制的线性表,被限制只允许从表的一段进行插入和删除运算,这一端称为栈顶,另一端就称为栈底,先进后出(FILO)
具体的重要的操作:

从图中可以了解,栈的操作并不算多,但是栈的使用是比较常见的。
五:哈希表
一般数组中,通过索引操作的,索引和我们元素之间是不存在确定关系。查找值直接查索引就行
hashtable:元素值和我们的hashtable 是存在对应关系的
例如:在不使用任何的集合,也不使用数据库,添加学生信息,做到快速查询,哈希表能够做到高效率
以下是哈希表的继承结构(具体内部实现比较复杂)

哈希表的基本结构:
哈希表通常由一个数组(称为“桶数组”)和链表(或其他数据结构)组成。数组的每个位置称为一个“桶”,用于存储数据。
链地址法中,数组的每个桶里存储的是一个链表。当发生哈希冲突(即不同的键映射到同一个桶)时,冲突的元素会被添加到链表中。这是最常见的哈希表实现方式之一。
哈希表也可以用其他方式实现,比如开放地址法(Open Addressing),其中冲突的元素会被存储到数组的其他位置,而不是使用链表。
在 Java 中,HashMap 的实现早期确实使用了“数组 + 链表”的结构,但在 JDK 8 之后,当链表长度超过一定阈值时,链表会转换为红黑树,以提高查找效率。
所以其实哈希表的核心思想是通过哈希函数将键映射到数组的某个位置,而“数组 + 链表”只是其中一种实现方式,比较好理解
六:相关知识补充(集合,泛型)
其实到这一步,大体数据结构我们已经有一定认识了。接下来以一张图的形式大概了解一下具体的集合知识补充

为什么有数组还要有集合:
因为数组长度一但固定就不可变;很多地方需要操作数组的(增删改查)都需要去编写对应的方法 (代码重复了--->封装);每个人定义各自的方法,可能存在别人找不到这种情况,实现也容易存在bug
集合框架:容器类确实很好用,集合框架框架是为了提供一些规范和标准,任何实现类都需要包含对外的接口,接口的实现,对集合内部的算法(底层都是一种数据结构)
目的:提供代码复用(封装的思想),让使用者专注于业务开发,而不是数据结构和算法
常见集合
- List(列表):集合中对象按照索引位置排序,允许元素重复。
- Set(集):集合中的元素不按特定方式排序,不允许元素重复。(类似于高中学习的集合)
- Map(映射):集合中的元素(key-value),不允许key 重复,值可以重复。(1对多映射)
Vector实现类:
- 底层使用 Object[] 数组(调用不带参数构造器时,默认长度为10,若不传扩容参数,扩容2倍)//ArrayList倾向于扩容1.5倍
- toString 方法已经重写并且可以直接打印出数组的样子
常用的操作(增删改查):
- - add(E obj)
- addElement(E obj)
- 查询
- size() 查长度
- get(int index) 查具体索引位置的元素值
- isEmpty() 判断集合为空
- 删除
- remove(int index)删除具体索引位置的元素
- remove("A") 删除指定元素
- removeLast() 循环,设置 null ,等待gc 回收
- 修改
- set(int index,E obj);修改某一个索引位置元素值
ArrayList与Vector
ArrayList是用来取代Vector的,两者底层原理和算法都一模一样。
区别在于:
- Vector:所有的方法都是用 synchronized 修饰符,表示线程安全的,性能低,适用于多线程环境
- ArrayList:线程不安全,性能高,即使在多线程环境下也使用它(Collections.synchronizedList(list))//非安全->安全提供的一个方法
- ArrayList 底层扩容是1.5倍,Vector 是两倍
- 底层构造器ArrayList 优化了,默认创建对象的时候是给一个空数组,第一次调用add 方法时,采取重新初始化数组(创建对象时,如果不存任何值,也浪费了堆空间),当然以上有些已经在ArrayList里提及过
List的总结
根据 Vector ArrayList LinkedList 类的所有特点进行的一个抽象,定义一个规范
特点:
- 允许元素重复
- 会记录添加顺序
- 具有很多共同方法
所以例如List<Integer> list = new ArrayList();//或者是LinkedList;Vector
都建议声明为List接口类型;这种方式更灵活,因为以后可以轻松地更换实现类(比如换成 LinkedList 而不需要修改其他代码)。
它遵循了“面向接口编程,而不是面向实现编程”的原则。
如果直接绑定到 ArrayList 实现,它灵活性较低,但如果你需要用到 ArrayList 特有的方法(不在 List 接口中定义的方法),可以使用这种方式。第一种方式更灵活,通常更推荐使用。
实现类的选用
ArrayList 取代 Vector 直接使用;
LinkedList:- 数组结构的算法:插入和删除速度慢,查询和更改较快
- 链表结构的算法:插入和删除速度快,查询和更改较慢
泛型
从我给出的一些代码里,我们能观察到泛型的身影
为什么我们需要去使用泛型,问题多一种,解决方法当然也多一种:
问题在于取集合元素时,取出来的是Object 类型,需要强制类型转换才能使用;添加元素时候,缺乏规范,导致可能需要使用时,会出现类型转换异常
设计原则:不要写重复的代码,能抽就抽
而泛型是一种数据规范和约束,提供编译时期的安全检查机制,底层给我们做强制类型转换,减少了我们的工作量
怎么去使用:常见的字母:
- T type(类型,使用到类上面)
- K V (key value)
- E element(元素)
例子:
类:
public class User<T> {
T obj;
}
//多个的方法
public class User<T,E,k> {
T obj;
E ele;
k value;
}
接口:
public interface Usb<T> {
void user(T t);
}
方法:
public static <T> T print(T t){//返回类型T
return t;
}
泛型的继承:
public class Mouse<T,T1> implements Usb<T,T1>{
@Override
public void user(T t, T1 t1) {
}
}
public class Keyword<T> implements Usb<T,String>{
@Override
public void user(T t, String s) {
}
}
本文用于个人学习记录,欢迎各位指正
&spm=1001.2101.3001.5002&articleId=146177017&d=1&t=3&u=f95e37433f0f409f8b048d844a3547ce)
1519

被折叠的 条评论
为什么被折叠?



