数据结构
-
什么是数据结构
是用来做计算存储,组织数据(插入数据,更新数据,删除数据,查询数据)的一种方式,是相互之间,存在一种或者多种特定关系的数据结合。数据结构需要高效的算法和索引技术。
-
常见的数据结构:数组Array 链表LinkedList 队列Queue 栈Statck 哈希表Hash 树Tree
-
意义: 带来高效的存储和运行效率
-
要求:高效的检索算法和索引技术
-
ArrayList LinkedList HashMap (高度封装,别人给我们提供好的)
一、集合
1.1 集合类简介
集合和数组一样,都是用来存储数据的结构,也可以称作容器。
为什么还要使用集合容器类呢?
-
数组弊端:
-
长度一但固定就不可变
-
很多地方需要操作数组的(增删改查)都需要去编写对应的方法(代码重复了—>封装)
-
每个人定义各自的方法,可能存在别人找不到 这种情况,实现也容易存在bug
-
无法保存具有映射关系的数据(语文-90 数学-80)
为了保存数量不确定的数据,以及具有映射关系的数据提供了集合类
集合主要负责保存,盛装其他数据
所有的集合类都在java.util包下,提供了一个表示和操作对象集合的统一架构。包含大量的接口和类,并且包含了这些接口和实现类的操作算法和数据结构。
和数组的区别
- 数组的长度不可改变,集合类的长度可变
- 数组提供的方法有限,对于添加,删除,插入数据操作非常不方便,并且效率不高
- 数组中存储数据的特点是:有序、可重复的 ,对于无序、不可重复的需求,不能满足。
- 数组中可以存储基本数据类型,也可以存储引用类型。在集合中只能保存引用类型(保存的是对象的引用地址)
在集合和数组中所指的存储,指在内存层面的存储,不涉及到持久化数据。
1.2 常见集合类
- List 列表 集合中对象按照索引位置排序,允许元素重复
- Set 集 结合中的元素不按特定方式排序,不允许元素重复
- Map 映射 集合中的元素(key - value) ,key不可以重复,值可以
1.3 自定义集合类
package com.coder.array;
import java.util.Arrays;
/**
* @author teacher_shi
* @project Core_Java
*/
public class MyArray {
private Object[] elementData;//用来存储数据的数组
private int size;//数组的实际长度(实际元素的个数)
private static final int DEFAULT_CAPACITY = 10;//默认初始空间
private static final Object[] EMPTY_ELEMENTDATA = {};
public MyArray() {
elementData = new Object[DEFAULT_CAPACITY];
}
public MyArray(int initialCapacity) {
if (initialCapacity > 0) {
elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("数组初始参数异常:" +initialCapacity);
}
}
//获取集合的实际大小
public int size() {
return size;
}
//向集合中填加数据
public boolean add(Object obj) {
add(obj, elementData, size);
return true;
}
//在索引位置插入数据
public void add(int index, Object obj) {
if (index > size || index < 0) {
throw new IndexOutOfBoundsException("索引下标越界:" + index);
}
if (size == elementData.length) {
elementData = grow();
}
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = obj;
size++;
}
private void add(Object obj, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = obj;
size = s + 1;
}
//扩容
public Object[] grow() {
return elementData = Arrays.copyOf(elementData,
elementData.length + (elementData.length >> 1));//9
}
//获取元素
public Object get(int index) {
if (index < elementData.length) {
return elementData[index];
} else {
throw new IndexOutOfBoundsException("索引长度异常");
}
}
//清除集合中所有元素
public void clear() {
//int to=size;
//size=0;
for (int to = size, i = size = 0; i < to; i++) {
elementData[i] = null;
}
}
//删除索引位置的数据,并返回删除掉的元素值
public Object remove(int index){
if (index > size || index < 0) {
throw new IndexOutOfBoundsException("索引下标越界:" + index);
}
Object obj=elementData[index];
size--;//将原有size-1,
if (size>index){
System.arraycopy(elementData,index+1,elementData,index,sizeindex);
}
elementData[size]=null;
return obj;
}
}
二、泛型
为什么使用泛型?
- 存在一定问题:
- 取集合元素时,取出来的是Object 类型,需要强制类型转换才能使用
- 添加元素时候,缺乏规范,导致可能需要使用时,会出现类型转换异常
- 设计原则,不要写重复的代码,能抽就抽
2.1 简介
在我们自定义的集合类中,底层是Object类型的数组,在设计和声明时,不能确定这个容器里到底要存储什么类型的数据。从JDK5版本之后,引入一个新的特性——泛型,提供了编译时类型安全检测机制, 该允许程序员在编译时检测到非法的数据类型。
泛型允许在定义类、接口时通过一个标识来表示其中某个属性的类型或者某个方法的返回值及参数类 型。本质是参数化类型,给类型指定一个参数,然后在使用时再指定参数具体的值,这样类型可以在使 用时决定了。
这种参数类型可以用在类、接口、方法中,分别称为泛型类、泛型接口和泛型方法。
泛型:是一种数据规范和约束,提供编译时期的安全检查机制,底层给我们做强制转换
2.2 使用泛型的好处
-
保证了类型的安全性
如果没有泛型,在集合中存取和读取数据,都是object类型,要将数据读取成特定类型,需要对每一个对象进行强制转换,如果存储的对象数据类型错误,在转换时会报异常
-
消除强制转换
使代码可读性更强,减少出错机会
-
避免了不必要的拆箱封箱操作
-
提高了代码的重用性
2.3 泛型的使用
泛型必须是引用类型,基本数据类型不可以
规范泛型使用字母的表示信息
T : Type(Java类)
E : Element (在集合中使用,指集合中存放的元素)
K : key(键)
V : Value (值)
N : Number (数值类型)
? : 表示不确定的java类型
-
使用
-
使用到类或接口上
/** * 类 */ public class User<T> { T obj; } //多个的方法 public class User<T,E,k> { T obj; E ele; k value; } public class GenericClass <T>{ private T value; public GenericClass() {\ } public GenericClass(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } } public class TestGenericClass { public static void main(String[] args) { //钻石符号 GenericClass<Integer> c=new GenericClass<>(123); Integer x=c.getValue(); System.out.println(c.getValue()); c.setValue(456); System.out.println(c.getValue()); GenericClass<String> c1=new GenericClass<>("这是一个字符串"); System.out.println(c1.getValue()); } } /** * 接口 */ public interface Usb<T> { void user(T t); } public interface GenericInterface <T>{ void showValue(T value); } class Impl implements GenericInterface<String>{ @Override public void showValue(String value) { System.out.println(value); } } class Impl1<T> implements GenericInterface<T>{ @Override public void showValue(T value) { System.out.println(value); } } public class TestGenericInterface { public static void main(String[] args) { //GenericInterface<String> x=new Impl(); //x.showValue("hello"); GenericInterface<Integer> y=new Impl1<>(); y.showValue(123); GenericInterface<String> z=new Impl1<>(); z.showValue("hello"); } }
-
使用到方法上
public static <T> T print(T t){ return t; } public class GenericMethod { public <T> void method1(T t){ System.out.println(t.toString()); } public <T> T method2(T t){ System.out.println(t.getClass().getName()); return t; } } class Student{ private String name; private String gender; public Student(String name, String gender) { this.name = name; this.gender = gender; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", gender='" + gender + '\'' + '}'; } } class Teacher{ private String name; private Integer age; public Teacher(String name, Integer age) { this.name = name; this.age = age; } @Override public String toString() { return "Teacher{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class TestGenericMethod { public static void main(String[] args) { Student student=new Student("李白","男"); GenericMethod method=new GenericMethod(); method.method1(student); Teacher teacher=new Teacher("杜甫",25); method.method1(teacher); System.out.println("__________________"); method.method2(student); method.method2(teacher); } }
-
泛型的继承
public class Mouth<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) { } }
2.4 泛型的通配符
用于解决泛型之间引用传递问题的特殊语法,主要分成三种情况:
-
< ? > :无边界的通配符,主要作用是让泛型能够接受未知类型的数据。在没有赋值前,表示可以接受任何的数据类型,赋值之后,不能往里面随便添加元素。因为不知道集合的数据类型,只能做读取操作,并且读到元素当成Object实例操作,但是可以去执行revome移除和clear清空操作。
- 用于编写可使用Object类中提供的功能使用方法时。
- 代码使用不依赖于类型参数的泛型类中的方法时。
-
< ? extends E >: 固定上边界的通配符
协变:在使用父类类型场景的地方可以改用子类类型
逆变:在使用子类类型场景的地方可以使用父类类型
不变:不能做到以上两点
数组是可以协变的。泛型不是协变的。这种设计降低了程序的灵活性,为了解决这个问题,设计出 固定上边界的通配符。能够接受指定类及其子类类型的数据。
虽然用的是extends关键字,但不限于继承了父类的子类,也可以使用接口的实现类
使用上限通配符只能从集合中获取值,而不能将值放入集合中。
- <? super E>:固定下边界的通配符
-
接受指定类及其父类类型(或接口)的数据
-
可以读取到集合的数据,按照Object类型处理
-
可以向方法中填加元素,填加的只能是指定类或其子类类型的对象,不能填加父类或接口类型的对 象
-
public class TestArray {
public static void main(String[] args) {
MyArray<MyInter1> array=new MyArray<>();
array.add(new MyImpl2());
array.add(new MyImpl2());
array.add(new MyImpl2());
test1(array);
}
public static void test1(MyArray<? super MyImpl1> array){
//消费是可以的
//填加元素时,不能加指定类的父类对象或接口对象
array.add(new MyImpl1());//填加当前指定类对象
array.add(new SubImpl());//填加当前指定类的子类对象
/*MyInter1 x=new MyImpl2();
array.add(x);*/
/* Object object = array.get(0);
System.out.println(object);*/
/*for (int i=0;i<array.size();i++){
System.out.println(array.get(i));
}*/
}
public static void test(MyArray<? extends MyInter1> array){
//array.add(new MyImpl2());
for (int i=0;i<array.size();i++){
System.out.println(array.get(i));
}
}
}
interface MyInter1{
}
class MyImpl1 implements MyInter1{}
class MyImpl2 implements MyInter1{}
class SubImpl extends MyImpl1{}
- 如果从集合中获取值,使用上限通配符
- 如果要向集合中放入数据值,使用下限通配符
- 可以为通配符指定上限,也可以指定下限,但不能同时指定两者
三、java集合类框架
Collection 常用方法
-
接口定义的常用方法规范
//集合容量大小 int size(); //判断集合是否为空 boolean isEmpty(); //包含是否有某一个元素 boolean contains(Object o); //迭代器 Iterator<E> iterator(); //转换成数组 Object[] toArray(); //添加元素的 boolean add(E e); //删除元素 boolean remove(Object o); //判断集合是否包含另一个集合 boolean containsAll(Collection<?> c); //添加一个集合 boolean addAll(Collection<? extends E> c); //清空 void clear(); //获取hashcode int hashCode();
-
通用迭代
Iterator it = 集合对象.iterator(); while(it.hasNext()){ Object ele = it.next(); } //迭代器接口 //判断是否有下一个元素 boolean hasNext(); //获取下一个元素 E next();
List 接口
允许元素重复,会记录添加顺序,具有很多共同方法
实现类的选用
-
ArrayList 取代 Vector 直接使用
-
LinkedList:
- 数组结构的算法:插入和删除速度慢,查询和更改较快
- 链表结构的算法:插入和删除速度快,查询和更改较慢
-
List 接口规范:
//根据索引获取元素值 E get(int index); //设置某一个索引位置元素值 E set(int index, E element); //删除某一个索引位置的值 E remove(int index); //截取 List<E> subList(int fromIndex, int toIndex);
Vector 实现类
-
看源码学习
public class Vector<E> extends AbstractList<E> implements List<E>{ //元素数组 protected Object[] elementData; //元素个数 protected int elementCount; //扩容容量 protected int capacityIncrement; public Vector(int initialCapacity, int capacityIncrement) { this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; } public Vector(int initialCapacity) { this(initialCapacity, 0); } public Vector() { this(10); } public synchronized void addElement(E obj) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = obj; } //扩容逻辑 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //如果你传进来扩容容量,新容量=老容量+传进来的扩容容量否则2倍老容量 int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) //给int类型的最大值 newCapacity = hugeCapacity(minCapacity); //数组的拷贝 elementData = Arrays.copyOf(elementData, newCapacity); } }
-
Vector 总结
- 底层使用 Object[] 数组(调用不带参数构造器时,默认长度为10,若不传扩容参数,扩容2倍)
- 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);修改某一个索引位置元素值
3.1 ArrayList
Collection: List 和 Set 父接口
List: 是一个有序的,不唯一接口
ArrayList :是List 的一个实现类,底层数据结构是数组
ArrayList 是用来取代 Vector.两者底层原理和算法,一模一样。
区别:
- Vector:所有的方法都是用 synchronized 修饰符,表示线程安全的,性能低,适用于多线程环境
- ArrayList:线程不安全,性能高,即使在多线程环境下也是用它(Collections.synchronizedList(list))
- ArrayList 底层扩容是1.5倍,Vector 是两倍
- 底层构造器ArrayList 优化了,默认创建对象的时候是给一个空数组,第一次调用add 方法时,采取重新初始化数组(创建对象时,如果不存任何值,也浪费了堆空间)
//ArrayList循环遍历
public class TestArrayList {
public static void main(String[] args) {
List<String> list=new ArrayList<>(10);
list.add("AAA");
list.add("BBB");
list.add("CCC");
//集合的循环遍历方式1
/*for (int i=0;i<list.size();i++){
System.out.println(list.get(i));
}*/
//遍历方式2
/*for (String s : list) {
System.out.println(s);
}*/
//遍历方式3
/*Iterator<String> iterator=list.iterator();
while (iterator.hasNext()){
String s=iterator.next();
System.out.println(s);
}*/
/*for(Iterator<String> iterator =
list.iterator();iterator.hasNext();){
System.out.println(iterator.next());
}*/
//遍历方式4 lambda
// list.forEach(System.out::println);
}
}
3.1.1 ArrayList 常用方法
add(Object obj):在集合后面加入元素,会返回一个boolean类型的值
add(int index,Object obj):在指定索引位置前面插入一个元素
size():获取当前集合中元素的个数 isEmpty():判断当前集合中是否为空
clear(): 从集合中删除所有元素
addAll(Collection c):在当前集合中加入另一个集合的元素,要求两个集合使用的泛型相同
addAll(int index,Collection c):在当前集合指定位置之前,加入另一个集合的元素
remove(int index):移除指定索引位置的元素,并将该元素返回
remove(Object obj): 移除对应元素,如果有多个相同值,只移除第一个找到的元素,如果是整数类型, 要封装成包装类。返回boolean类型的值,是否移除成功 removeAll(Collection c):从当前集合中移除参数集合中所有包含的元素
retainAll(Collection c):在当前集合中保留参数集合中所有包含的元素
conatins(Object o):判断当前集合中是否包含给定参数的元素,返回boolean类型的值
containsAll(Collection c):判断当前集合中是否包含给定参数集合的所有元素
toArray():以正序方式,返回一个包含所有元素的对象数组
indexOf(Object):查找参数在当前集合中第一次出现的索引位置
astIndexOf(Object):查找参数在当前集合最后一次出现的索引位置
subList(int index,int end):对当前集合进行截取,从起始位置(包含)截取到结束位置(不包含),返 回一个新的List集合
iterator():获取集合的迭代器
listIterator():获取集合的List迭代器
set(int index,Object obj):设置索引位置的元素为第2个参数数据
3.1.2 Iterator和ListIterator
- Iterator 可以遍历List集合,也可以遍历Set集合,ListIterator只能遍历List集合
- Iterator只能单向遍历(向后遍历),ListIterator双向遍历(向前/向后遍历)
- ListIterator继承Iterator接口,添加新的方法
常用方法:
add(E e):将指定的元素插入集合中,插入位置为迭代器当前位置之前
hasNext(): 正向遍历集合,判断后面是否还有元素,返回boolean类型值
next():返回集合中迭代器指向后面位置的元素
nextIndex():返回集合中迭代器后面位置元素的索引
hasPrevious():反向遍历集合,判断前面是否还有元素,返回boolean类型值
previous():返回集合中迭代器指向前面位置的元素
previousIndex():返回集合中迭代器前面位置元素的索引
set(E e):替换迭代器当前位置的元素
3.1.3 remove方法
对集合元素进行循环处理增加或删除时,不能使用foreach处理方式,要使用迭代器方式。
在foreach对集合中倒数第二个元素进行删除时,不会报错,其他位置的元素都会报错。
foreach底层也是通过迭代器实现的。
使用迭代器操作,有两个步骤:
iterator.hasNext();
item=iterator.next();
迭代器删除操作源码
当modCount变量和expectedModCount不相同时,抛出异常处理
modCount 变量:记录集合对象从new 出来到现在被修改的次数
expectedModCount变量:迭代器现在期望这个集合被修改的次数 从源码中可以看到,
使两个变量相等。
迭代器不会报错,使用foreach会报错,原因是在迭代器中,对这两个变量进行了同步处理,而foreach 没有进行同步处理,导致会出现CheckForComodification异常出现 。
通过查看源码,有一个hasNext方法,方法中要比较cursor和size
cursor是一下个元素的索引值
size是整个集合元素个数
当删除倒数第2个元素时,cursor通过计算之后,得到cursor=size,导致迭代器认为不存在下一个元 素,迭代结束。
3.2 链表
3.2.1 链表简介
是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点组成,结点可以运行时动态生成。
链表分类:单向链表、双向链表、循环链表
单向链表:每个结点包含两个部分,一部分是存储数据元素的数据域,另一部分存储下一个结点地址的指针域。
双向链表:每个结点包含三个部分,一部分是存储数据元素的数据域,一部分存储上一个结点地址的指针域,一部分存储下一个结点地址的指针域。
优点:克服数组需要预先知道数据大小的缺点。可以充分利用计算机内存空间,实现灵活的内存动态管理。由于链表不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度。
缺点:查找速度非常慢,每次查找元素,都要从头结点开始进行查找,或者采用二分查找的方式,从尾结点进行查找,效率很低。链表的结点,需要存储相邻结点的指针域,所以空间开销比较大。
3.2.2 自定义链表
package com.coder.link;
import java.util.NoSuchElementException;
/**
* @author teacher_shi
* @project Core_Java
*/
public class MyLinkedList<E> {
//结点
private class Node<E> {
Node<E> next;//指向后继结点
E data;//数据元素
Node<E> previous;//指向前驱结点
public Node(Node<E> next, E data, Node<E> previous) {
this.next = next;
this.data = data;
this.previous = previous;
}
}
//头结点
private Node<E> first;
//尾结点
private Node<E> last;
//有效元素个数
private int size;
public MyLinkedList() {
}
public MyLinkedList(Node<E> first, Node<E> last, int size) {
this.first = first;
this.last = last;
this.size = size;
}
//将元素加到链表的尾部
public boolean add(E e) {
Node<E> l = last;//将尾结点赋值给l
//新创建一个结点
Node<E> newNode = new Node<>(null, e, l);
//将新创建的结点置成尾结点
last = newNode;
//判断,如果没有尾结点,则新创建的结点就是第一个结点
if (l == null) {
first = newNode;
} else {
l.next = newNode;
}
size++;
return true;
}
//将元素插入到指定索引位置
public void add(int index,E e){
//要在链表头部插入数据
if (index==0){
first=new Node<>(first,e,null);
}else{
//获取上一个结点
Node<E> prev=node(index-1);
//获取下一个结点
Node<E> next=prev.next;
Node<E> newNode=new Node<>(next,e,prev);
prev.next=newNode;
}
size++;
}
Node<E> node(int index) {
//优化,判断查找的index和size的关系 100
Node<E> x = first;
for (int i = 0; i < index; i++) {
x = x.next;
}
return x;
}
public E get(int index){//2
return node(index).data;
}
public int size(){
return size;
}
public E getFirst(){
if (first==null)
throw new NoSuchElementException();
return first.data;
}
public E getLast(){
if (last==null)
throw new NoSuchElementException();
return last.data;
}
//移除结点
public E remove(int index){
Node<E> node=first;
if (index==0){
first=node.next;
}else{
//待删除元素的上一位
Node<E> prev=node(index-1);
//待删除元素
node=prev.next;
prev.next=node.next;
}
size--;
return node.data;
}
}
3.2.3 LinkedList
底层使用链表数据结构
addFirst(Object):将给定元素插入链表的开头
addLast(Object):将给定元素追加到链表的尾部
getFirst():获取链表的头结点元素值
getLast():获取链表的尾结点元素值
removeFirst():移除链表的头结点,并返回其中的元素值
removeLast(): 移除链表的尾结果,并返回其中的元素值
3.2.4 ArrayList 和 LinkedList比较
分析 ArrayList 性能
- 大O表示法(BigO):用来描述时间的复杂度,专门用来衡量计算机性能
- 分析 ArrayList 性能
- 新增操作:
- 把数据添加到最后一个元素,至少操作一次
- 如果把数据放到数组的第一个位置,现在有N个元素,此时需要操作N次(整体后移)
- 平均:O((N+1)/2 ) O(N)
- 删除操作:
- 如果是最后一个元素,操作一次
- 如果删除第一个元素,操作N次
- 平均 O((N+1)/2) O(N)
- 修改操作:
- 操作一次
- 查询操作:
- 根据索引去查,也是操作1次
- 根据元素来查,1-N次之间 O((N+1)/2 ) O(N)
- 新增操作:
- 基于数组结够,做查询和修改非常快,但是做删除和新增会慢一点
分析 LinkedList 性能
- 增加:
- 直接从头尾增加,操作1次
- 删除
- 如果删除头尾元素,操作1次
- 删除中间元素,(1+N)/2
- 查询
- 查询头尾 1次
- 查询中间 (1+N)/2
- 修改
- 头尾1次
- 中间 (1+N)/2
基于数组和链表的对比:
- ArrayList:查询更改较快,新增和删除较慢
- LinkedList:查询,更改较慢,新增和删除较快
存储结构:
ArrayList:底层是数组结构,线性顺序存储
LinkedList:底层是链表结构,非连续,非顺序的存储,对象间是依靠指针域串连起来
操作性能:
ArrayList: 适合随机查询数据的操作
LinkedList:适合元素的插入删除操作
注意:
- LinkedList 是非线程安全的,保证线程安全,使用Collections 线程安全方法
- 擅长操作头和尾,大多数你以后要用的方法, addFirst addLast removeFirst
- 链表不存在索引,但是调用get(index) 是因为底层提供了 ListItr 这个内部类 提供了一个int 的索引
- 擅长保存和删除操作
3.2.5 Vector向量
和ArrayList处理方式底层都是使用数组结构完成
比较:
1. ArrayList在构造方法时,创建的大小为0,当第一次加入元素时,进行扩容。Vector在构造方法时,创 建的大小为10.
2. ArrayList每次扩容都是原有大小的1.5倍,Vector扩容时,如果给定了capacityIncrement,则新的数 组大小为原有数组大小+capacityIncrement,否则扩容为原有大小的2倍
3. ArrayList非线程安全,Vector是线程安全的。
3.2.6 哈希表
散列表,也叫哈希表,是根据键和值而直接进行访问的数据结构,可以不经过任何比较,一次直接从表 中得到要搜索的元素。通过一种函数,使用元素的存储位置和它的键值建立一个映射关系,加快查找的 速度,这个函数就叫做哈希函数或散列函数,存放记录的数组就叫做哈希表。
由于哈希函数设计问题,可能会产生哈希冲突。
哈希函数:
- 除留余数法(常用)
- 直接定址法(常用)
- 平方取中法 折叠法
- 随机数法
- 数学分析法
哈希函数设计的越精妙合理,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突的问题:闭散列、开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中还有空位置,所 以,使用该方法,会找下一个空位置。使用线性探测,从发生冲突的位置开始,依次向后探测,直到寻 找到下一个空位置为止。
开散列:也叫链地址法或开链法,先是通过散列函数计算关键码,具有相同地址的关键码归于同一个子 集合,每一个子集合称为一个桶(哈希桶),各个桶中的元素通过一个单链表链接起来,各链表的头结点存 储在哈希表中
如果出现了极端情况,所有的数据都冲突到一个桶中,将链表改成红黑树结构
3.2.7 HashSet
底层使用HashMap,将数据存储到hashMap的key上面
特点:无序性,唯一性。
如果存储的数据具有相同的hashcode,则会调用equals方法再次进行比较
常用方法和List接口相同
没有List接口中的索引处理方式
3.2.8 LinkedHashSet
是HashSet的子类
底层是一个LinkedHashMap,维护了一个数组+双向链表
LinkedHashSet是根据元素HashCode值来决定元素的存储位置,同时,使用链表维护元素的次序,使用元素看起来是以插入顺序保存的。
LinkedHashSet中维护的双向链表,每一个节点,都有before和after属性,在添加一个元素时,先求hash值,再求索引,确定这个元素在hashtable中的位置,然后再将元素加入到双向链表。在遍历时, 可以确保和插入的顺序相一致。
不可以出现重复元素。
特性:有序性,唯一性。
3.2.9 TreeSet
底层数据结构是二叉树
在放入数据时,会根据二叉树算法,将数据进行排列
采用中序遍历,将数据读取出来。所以对于TreeSet来讲,不管放入元素的顺序是什么样的,读取出来时,都是以升序排列。
TreeSet泛型里的对象,是要具有排序能力的(Comparable)。如果没有实现Comparable接口的话,则需要在TreeSet构造方法中,传入Comparator接口的对象
3.2.10 排序接口
在做对象的比较排序时,使用两种方式,一种是Comparable接口,一种是Comparator接口。
使用Comparable接口的处理方式,让类实现接口,重写compareTo()方法,对其中的某个属性进行大小的比较,小于返回负数,相等返回0,大于返回正数
使用Comparator接口的处理方式,创建一个接口的实现类,重写compare()方法(传入两个对象),根据业务需求,对两个对象的属性进行比较。将实现类的对象放在TreeSet构造方法中。相当于一个外部的 比较器,比较灵活,耦合度更低。更多的情况,是可以使用匿名内部类或lambda表达式的方式实现。
如果业务逻辑是固定的,就是按照特定的属性进行排序处理,可以使用Comparable接口。如果业务逻辑不固定,使用Comparator接口来实现。
3.2.11 Collections工具类
sort(list):按升序排列
sort(list,Comparator): 按外部比较器规则进行排序
reverse(list):按降序排列
shuffle(list):随机排序
swap(list,int,int):交换两个索引位置的元素
max(list):获取集合中的最大值
min(list):获取集合中的最小值
binarySearch(list,Object):二分查找,返回索引(list必须是有序的)
fill(list,Object):填充,用指的值替换list中的值
replaceAll(list,Object,Object):将list集合中旧元素换成新的元素
requency(collection,Object):统计元素出现的次数
rotate(list,int)旋转,如果第2个参数为0,则没有改变,如果为正数,则将list集合最后的几位移动到集 合前面,如果为负数,则将list集合前几位元素移动到后面
线程同步处理:
- synchronizedCollection(collection)
- synchronizedMap(map)
- synchronizedList(list)
- synchronizedSet(set)
-
常用方法
//获取线程安全的集合 Collections.synchronizedList(new ArrayList()); //获取线程安全的集合 Collections.synchronizedCollection(Collection); //排序,好挺常用 public static <T extends Comparable<? super T>> void sort(List<T> list) { list.sort(null); }
-
其它常用
public static final List EMPTY_LIST = new EmptyList<>(); public static final Map EMPTY_MAP = new EmptyMap<>(); public static final Set EMPTY_SET = new EmptySet<>();
四、Map
4.1 简介
map接口:
- 存储的是键/值对 的对象组
- 不是Collection接口的子接口,本身就是一个顶级接口
4.2 HashMap
采用hash算法存储数据,key不可以重复,value可以重复
特性:无序性,key唯一性,value不唯一性
如果发生了重复的key,则后放入的会覆盖先放入的数据
默认初始空间大小16,负载因子默认是0.75
常用方法
- put(key,value):向hashMap中存入数据
- get(key):通过指定key,获取对应的value
循环遍历方式
/*Set<String> keys = map.keySet();//
for (String key : keys) {
System.out.println(key+"\t"+map.get(key));
}*/
//效率更高一些
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey()+"\t"+entry.getValue());
}
containsKey(key):判断map中是否包含指定的键,返回boolean类型
containsValue(value):判断map中是否包含指定的值,返回boolean类型
remove(key):按指定key将元素从map中移除
remove(key,value):按指定的key和value将元素从map中移除
size():获取有效元素的个数
isEmpty():判断是否为空
putAll(map):将一个指定map集合加入到当前map中
replace(key,value):将指定的key以给定value进行替换处理
使用
-
key — String
-
value 无所谓
-
注意,存String 类型无所谓,存对象类型,需要重写equals 方法和 hashCode();
Map<Student,String> mapStu = new HashMap<>(); mapStu.put(new Student(10),"sy"); mapStu.put(new Student(12),"zs"); mapStu.put(new Student(13),"ls"); mapStu.put(new Student(10),"zz"); Set<Map.Entry<Student, String>> entries = mapStu.entrySet(); System.out.println(entries); for (Map.Entry<Student, String> entry : entries) { System.out.println(entry.getKey()); System.out.println("-----"); System.out.println(entry.getValue()); }
4.3 LinkedHashMap
继承自HashMap,有顺序
特性:有序性,key唯一性,value不唯一性
4.4 TreeMap
底层采用树结构
不管放入时候的顺序,会按照key默认升序排列
4.5 Hashtable
和HashMap的区别
- 都是实现Map接口
- Hashtable是基于陈旧的Dictionary类的,在jdk1.0时加入。HashMap是jdk1.2时加入的
- Hashtable是线程安全的,HashMap是非线程安全的
- HashMap可以将null作为key或者value,而Hashtable不允许key或value为null值
- HashMap的初始容量16,Hashtable初始容量为11,加载因子都是0.75
- HashMap扩容时是capacity*2,Hashtable扩容是capacity *2+1
- HashMap数据结构:数组+链表+红黑树(当链表长度大于8时,转换为红黑树的结构);Hashtable数据结 构:数组+链表
- 计算hash的方法不同,Hashtable计算直接使用key的hashcode对table数组的长度进行取模, hashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后再对table数组长度取模
4.6 栈
栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。
允许插入和删除数据的一端,也就是变化的一端,称为栈顶(TOP),另外一端为固定的一端,称为栈底 (BOTTOM)
后进先出(LIFO)的原则(先进后出(FILO)),最先入栈的元素在栈底,最后进入的元素在栈顶,而取出元素时正好相反,最后入栈的元素最先取出,最先入栈的元素最后取出。
压栈:栈的插入操作 也叫作进栈/压栈/入栈
出栈:栈的删除操作叫作出栈,也叫作弹栈。
java 中的Stack类是继承了Vector来的。
常见方法:
push(E):压栈操作
pop():从栈顶获取一个元素并将这个元素移除
peek():从栈顶获取一个元素,不会移除这个元素
empty()/isEmpty():判断当前栈是否没有元素
search(Object): 查找一个元素在栈中的位置,如果没有找到,返回-1
//添加元素
public E push(E item) {
addElement(item);
return item;
}
//取元素并且删除
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
//查看栈顶元素
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
- 建议:建议使用ArrayDeque(方法会更加友好)
4.7 队列
4.7.1 简介
队列简称队,是一种操作受限的线性表,只允许在表的一端进行插入,在表的另一端进行删除。向队列 中插入元素称为入队或进队,删除元素称为出队或离队。
队头:(Front),允许删除的一端,也称为队首。
队尾:(Rear),允许插入的一端
操作特性是先进先出(FIFO)
队列可以按方向分为单向队列,双向队列,
还可以按阻塞情况分成阻塞队列和非阻塞队列,
还可以按是否有界分为有界队列和无界队列
4.7.2 Queue
是一个接口,单向队列,继承自Collection
除了基本的集合接口操作之外,队列还提供了特殊的插入、获取和移除的操作。这些操作都存在两种方法的形式,一种操作失败时抛出异常,另一种返回null或 false
抛出异常方法 | 返回null 或false的方法 | |
---|---|---|
插入 | add(e) | offer(e) |
移除 | remove(e) | poll() |
获取 | element() | peek |
4.7.3 Deque
是一个接口,双向队列,继承自Queue(Double-end queue)
具有先进先出或后进先出的特点,支持所有元素在头部、尾部进行插入、删除、获取
在使用到栈的结构时,推荐使用Deque,而不是Stack
- Deque是接口,Stack是类,针对接口编程,不针对具体实现编程,接口可以屏蔽实现细节
- Stack是继承自Vector,使用synchronized实现线程安全
Queue 单向队列方法 | DQueue 单向队列方法 | LinkedList 双链表 |
---|---|---|
boolean add(E e); | void addFirst(E e); | public E getFirst() |
E remove(); | void addLast(E e); | public E getLast() |
E poll(); | E removeFirst(); | public void addFirst(E e) |
E element(); | E removeLast(); | public void addLast(E e) |
E peek(); | E pollFirst(); | |
E pollLast(); | ||
E getFirst(); | ||
boolean add(E e); | ||
void push(E e); | ||
E pop(); |
五、Set
-
Set 接口是 Collection 的子接口,相当于数学上的集合
-
Set 存储元素的特点:
- 不允许元素重复,尝试添加相同元素,会返回false
- 不会记录元素的先后添加顺序
- 判断两个元素对象是否相等用的是equals 方法
-
hash表与数组对比
元素值与元素位置存在对应关系(hash)查找非常快
传统数组,元素值和索引之间没有必然的联系,遍历获取
HashSet
-
HashSet 是 Set 最常用的接口,底层使用 Hash(散列) 算法,查询速度和插入速度较快。
-
HashSet 判断两个对象是否相等,equasl 比较。返回 true 表示相等。
-
对象的 HashCode 值决定了在hash 表中的位置。
- 判断添加对象和集合元素对象HashCode值。
- 不等:直接将新添加对象存储导对应位置
- 相等:再继续判断新对象和集合中对象的具体值,equals 方法判断
- HashCode 相同,equals true ,则是同一个对象,则不保存hashtable 中
- HashCode 相同,equasl false,存储到同槽的链表上。
- 判断添加对象和集合元素对象HashCode值。
-
HashSet 基于 HashMap 实现的
//直接使用HashMap写好的代码,体现了封装的原则,减少重复代码 private transient HashMap<E,Object> map; //map.put value 永远设置同一块空间 new Object 的静态常量 public boolean add(E e) { return map.put(e, PRESENT)==null; } //得到 HashMap 的 key public Iterator<E> iterator() { return map.keySet().iterator(); }
-
注意:记得重写 equals 和 hashCode 方法
LinkedHashSet
-
底层使用哈希表算法,和链表算法,
- 哈希表用来保证唯一性,HashSet 里面是不记录添加先后顺序的。
- 链表,来记录元素的添加顺序
-
底层基于LinkedHashMap 实现
public LinkedHashSet() { super(16, .75f, true); } HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
TreeSet
-
TreeSet:底层使用红黑树算法,会对存储的元素做自然排序(小-大)
-
注意:使用TreeSet 时,必须使用同一种数据类型。因为需要比较,否则就会报类型转换异常
- 底层肯定实现了Comparable 接口
- 比较结果
- 大于0
- 等于0 则说明是同一个对象
- 小于0
-
底层都是基于 TreeMap 实现的
//底层使用 treeMap public TreeSet() { this(new TreeMap<E,Object>()); } //可以传一个自定义比较器 public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); }
数据类型 | 是否记录添加顺序 | 是否添加就排序 | 底层算法 |
---|---|---|---|
arrayList | true | false | 数组 |
linkedList | true | false | 链表 |
hashSet | false | false | 哈希算法,红黑树 |
linkedHashSet | true | fasle | 哈希算法,链表,红黑树 |
treeSet | false | true | 红黑树 |
hashMap | false | false | 哈希算法,红黑树 |
linkedHashMap | true | false | 哈希算法,链表,红黑树 |
treeMap | false | true | 红黑树 |