主要内容:
集合
定义:是一组类似于数组的批量存储结构,这种存储结构是不限制存储长度的,而且还提供了丰富的方法,大大简化了批量存储的过程
集合类的体系结构:
按照元素的存储方式:
元素单个存储的:Collection
子接口List:有序可重复
子接口Set:无序不可重复
元素成对(键值对)存储的:Map
下面看一下详细的接口以及实现类
List接口及实现类
ArrayList
以ArrayList为例讲解List接口实现类的用法
①ArrayList的用法:
public class TestArrayList {
public static void main(String[] args) {
//[1]ArrayList的创建方式
/*
* 泛型:通过为ArrayList的对象添加泛型,来制定这个集合对象中能够保存的对象的类型
* 结合类通过添加泛型,在编译层面上限制加入集合类中元素的类型
*/
ArrayList<String> list1 = new ArrayList<String>(); //筐:容器对象
//[2]增
String str = "aaa";
list1.add(str); //向ArrayList对象中单个添加元素的方法
list1.add("ccc");
list1.add("bbb");
list1.add("aaa");
list1.add(1, "ddd"); //向指定下标位上插入元素
LinkedList<String> list2 = new LinkedList<String>();
list2.add("111");
list2.add("222");
list2.add("333");
list1.addAll(list2); //按照顺序向list1中批量添加list2中的元素,追加顺序按照list2集合中元素的顺序
ArrayList<String> list3 = new ArrayList<String>();
list3.add("444");
list3.add("555");
list3.add("666");
list1.addAll(2, list3); //按照指定的下标位,向list1中批量添加list3中的元素
/*
Integer i1 = 123;
list1.add(i1);
Boolean b1 = true;
list1.add(b1);
*/
//[3]删
String s1 = list1.remove(1); //按照指定下标进行元素删除
System.out.println("Removed string:" + s1);
System.out.println(list1.remove("aaa")); //删除指定元素,如果集合中没有这个元素,将删除失败;如果集合中有多个这样的元素,仅删除第一个
ArrayList<String> list4 = new ArrayList<String>();
list4.add("aaa");
list4.add("666");
list4.add("abc");
System.out.println(list1.removeAll(list4)); //首先让两个集合做交集,再从list1当中删除这部分交集
// list1.clear(); //删除集合中所有的元素
//[4]改
String s2 = list1.set(0, "666"); //将集合中指定下标位上的元素,用指定的新元素覆盖,被覆盖的原始元素作为返回值返回
System.out.println("Replaced String:" + s2);
//[5]其他方法
System.out.println("Contains:" + list1.contains("abc")); //判断当前集合中是否包含指定的元素
ArrayList<String> list5 = new ArrayList<String>();
list5.add("111");
list5.add("bbb");
list5.add("bcd");
System.out.println("Contains all:" + list1.containsAll(list5)); //判断当前集合中是否包含指定集合中的全部元素
list1.add("bbb");
list1.add("111");
System.out.println("Index of:" + list1.indexOf("abc")); //返回当前集合中指定元素第一次出现的对应下标
System.out.println("Last index of:" + list1.lastIndexOf("111")); //返回当前集合中指定元素最后一次出现的下标
// list1.clear();
System.out.println("Is empty:" + list1.isEmpty()); //判断当前集合是否为空集合
//List<String> list = new ArrayList<String>();
List<String> list6 = list1.subList(1, 7); //截取子集合,包含起点下标对应元素,不包含终点下标对应元素
System.out.println("Sub list:" + list6);
//[6]查
/*
for (String tmp : list1) {
System.out.println(tmp);
}
*/
if(list1 != null && !list1.isEmpty()) {
for(int i = 0; i < list1.size(); i++) { //size():用来返回当前集合对象中元素的数量
System.out.println(list1.get(i)); //get(int index):用来按照下标返回集合中的元素
}
}
}
}
迭代器的应用:
ArrayList中的BUG:按照下标位删除元素,连续的重复元素无法删除干净
迭代器的使用原理:
public class TestArrayList2 {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("aaa");
list1.add("aaa");
list1.add("aaa");
list1.add("aaa");
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
list1.add("ddd");
list1.add("aaa");
/*
* 遍历list1集合
* 找到集合中所有的字符串"aaa"并进行删除
*/
/*
if(list1 != null && !list1.isEmpty()) {
for(int i = 0; i < list1.size(); i++) {
if("aaa".equals(list1.get(i))) {
list1.remove(i);
}
}
}
*/
//[1]通过结合对象的iterator()方法,得到一个迭代器
Iterator<String> ite = list1.iterator(); //迭代器就是水壶的内胆,当得到一个集合对象的迭代器的时候,这个迭代器中就一定保存了当前集合中所有的元素
//[2]通过while循环遍历迭代器,判断其中的元素是不是"aaa",如果是,执行remove()删除方法
String str = "";
if(ite != null) {
while(ite.hasNext()) { //如果当前迭代器中海油下一个元素,循环继续
str = ite.next(); //①next()方法可以将迭代器中的箭头挪向下一位 ②next()方法还可以将箭头指向的元素返回
if("aaa".equals(str)) { //判断当前迭代器中的元素是不是字符串"aaa"
ite.remove(); //如果是,就通知迭代器删除当前箭头指向的这个元素
}
}
}
System.out.println("After remove:" + list1);
}
}
ArrayList的内部实现:
- ArrayList内部通过Object[]数组实现所以,ArrayList在不添加泛型约束的前提下,可以保存任意类型的对象ArrayList中其他类型的构造器:
public class TestArrayList3 {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>(100); //创建一个初始化内部数组大小为100的ArrayList
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
ArrayList<String> list2 = new ArrayList<String>(list1); //通过其他的Collection接口实现类对象创建当前ArrayList集合对象
System.out.println(list2);
}
}
- ArrayList内部数组的成长机制:
(1).调用add方法,将元素插入ArrayList内部
public boolean add(E e) {
/*
下面的两句代码根本没有考虑当前数组长度的问题
换句话说,下面的两句代码认为,任何情况下元素插入都能够成功
那就代表:第1句代码一定能够保证数组长度至少能够插入一个元素
*/
ensureCapacityInternal(size + 1);
elementData[size++] = e; //将新来的元素加入ArrayList内部的数组中,size+1
return true; //返回true代表元素添加成功
}
(2).ArrayList通过ensureCapacityInternal(size + 1)确保内部数组的长度,能够再多保存1个元素
/*
如果在数组还是默认为空的情况下
比较要求的最小容量和默认容量(10)之间的大小关系
取其大者,继续执行方法
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
(3).在通过比较,得到默认容量和需求最小容量之间的较大者之后,我们希望通过较大的容量去扩容数组(数组的成长)
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
/*
在此判断:
我们需求的最小容量,确实已经超过了当前数组已有的容量
此时进行真正的数组成长
*/
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
(4).数组成长机制的核心代码:grow()方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //oldCapacity:原始数组的容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //newCapacity:计算得到的新的数组容量,计算结果是原始容量的1.5倍
/*
如果通过计算得到的数组的新容量,依然没有达到最小容量的要求
扩容使用的新容量就是要求的最小容量
*/
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
/*
将原始数组中的内容拷贝到一个新数组当中去
新数组的长度就是newCapacity
然后让内部数组的引用变量指向这个新的数组
扩容完成
*/
elementData = Arrays.copyOf(elementData, newCapacity);
}
- ArrayList中其他和内部数组相关方法的运用:
ArrayList<String> list2 = new ArrayList<String>(list1); //通过其他的Collection接口实现类对象创建当前ArrayList集合对象
System.out.println(list2); //elementDate.length = 3; size = 3
//elementDate.length >= size
/*
* 手动指定一个ArrayList对象内部数组的容量(长度)
*/
list2.ensureCapacity(100); //elementDate.length = 100; size = 3
list2.add("ddd");
list2.add("eee"); //elementDate.length = 100; size = 5
/*
* 通过这个方法可以将内部数组中未使用的空位缩容
* 将空闲空间归还内存
* 达到ArrayList对象占用空间最小化的目的
*/
list2.trimToSize(); //elementDate.length = 5; size = 5
LinkedList
隶属于List接口的实现类,用法和ArrayList一样。
内部实现结构:链表
链表的特点:不定长且不连续
- 不定长:
- 一个链表的节点和节点之间,仅使用引用来进行维持,节点和节点之间的关系比较微弱,
- 同时这种关系也决定了一个链表的节点数量可以是无限多个
- 在任意两个节点之间插入一个新节点或者删除一个节点的话,仅需要改变引用变量即可
- 不涉及到新空间的开辟和原始成员的拷贝。所以,链表增删快
- 不连续:
- 链表的节点与节点之间的内存地址是不连续的,所以想用通过内存地址的方式访问节点的话
- 必须从链表头开始向后进行访问,每次访问节点都需要如上操作,无法有规律的访问链表的节点内存。所以,链表遍历慢
实验:
定义一个ArrayList(代表数组存储结构);
定义一个LinkedList(代表链表存储结构);
分别向两种存储结构中进行10W次添加操作和访问操作
比较运行时间。
其他链表变现形式:
总结:增删多于遍历,用链表结构;遍历多于增删,用数组结构
Set接口及实现类
HashSet
Set接口实现类的特点:元素无序不可重复
①以HashSet为例,学习其中的常用方法:
public class TestHashSet {
public static void main(String[] args) {
//[1]创建HashSet对象
HashSet<String> set1 = new HashSet<String>(); //使用HashSet的空构造器
ArrayList<String> list1 = new ArrayList<String>();
list1.add("a");
list1.add("b");
list1.add("c");
HashSet<String> set2 = new HashSet<String>(list1); //通过Collection接口实现类的对象实现一个HashSet对象
HashSet<String> set3 = new HashSet<String>();
set3.add("bbb");
set3.add("aaa");
//[2]增
set1.add("aaa"); //向HashSet中单个插入元素
set1.add("bbb");
set1.add("ccc");
set1.add("abc");
/*
* 如果向Set集合中添加重复的元素
* 重复元素将不会被保存起来
*/
set1.addAll(list1); //向Set集合中批量添加元素
/*
* Set集合中的元素是无序的,不存在下标一说
* 所以我们无法指定元素的插入位置
*/
//[3]删
System.out.println("Remove: " + set1.remove("abc")); //通过元素删除Set集合中的元素
System.out.println("Remove all: " + set1.removeAll(set2)); //通过另外一个集合实现批量元素删除
// set1.clear(); //清空set1集合
// set1.retainAll(set3); //通过这个方法,可以保留两个集合中的交集部分
//[4]改
//在Set集合中没有提供修改元素的方法,还是因为Set集合中元素是无序的
//[5]查
/*
* Set接口中的元素不是按照添加顺序保存的
* 所以元素没有固定下标
* 因此,不能通过元素下标进行元素访问,业绩没有get(i)这样的方法
* 我们只能够通过迭代器对Set集合中的元素进行访问
*/
Iterator<String> ite = set1.iterator(); //Iterator迭代器对象不是new出来的,因为Iterator是接口
String str = "";
while(ite.hasNext()) {
str = ite.next();
System.out.println(str);
}
//[6]其他方法
System.out.println("Contains: " + set1.contains("aaa")); //判断当前集合中是否包含某一元素
System.out.println("Is empty: " + set1.isEmpty()); //判断当前集合是否为空
System.out.println("Size: " + set1.size()); //返回当前集合中元素的个数
System.out.println("Contains all: " + set1.containsAll(set3));
}
}
LinkedHashSet
在LinkedHashSet中,元素是按照添加顺序保存的
但是其中的元素依然不可重复
规律:凡是以Linked开头的:
- 内部一定是通过链表实现的
- 这样的结构,其中的元素一定是按照添加顺序保存的
EnumSet
一种只能够保存枚举对象的Set集合
public class TestEnumSet {
public static void main(String[] args) {
EnumSet<Season> set1 = EnumSet.allOf(Season.class); //EnumSet不是new出来,是通过现有的枚举类创建出来的
System.out.println(set1);
ArrayList<Season> list1 = new ArrayList<Season>();
list1.add(Season.Summer);
EnumSet<Season> set2 = EnumSet.copyOf(list1);
EnumSet<Season> set3 = EnumSet.of(Season.Summer, Season.Spring);
}
}
SortedSet和TreeSet
-
SortedSet:这个借口的实现类当中的元素是“有序的”,但是这个“有序”,不是元素的添加顺序
-
TreeSet:TreeSet是SortedSet的实现类,这个实现类当中,可以在添加元素的时候,实现对元素的排序
-
自然排序:
- 定义:同一类元素之间,自发的进行比较
实现:实现Comparable接口,重写compareTo方法 - 优点:同一个类的对象之间,可以直接自发的通过compareTo方法比较大小,不需要其他类的辅助
- 缺点:自然排序,通过Comparable接口的compareTo方法,只能够定义一种排序准则,不能使用多准则的排序方式
- 注意:在TreeSet保存元素的时候,如果两个元素的compareTo方法返回0,TreeSet将会认为这两个对象是“相同的”,
后来的元素将不会被保存在集合中
- 定制排序:
- 定义:额外定义一种排序器类,每次向排序器传递两个待比较的对象,由排序器决定按照这两种对象的哪种准则进行排序
- 实现:排序器类实现Comparetor接口,重写其中的compare方法
- 优点:可以根据不同的排序器实现不同的比较方式,不需要改变被排序的元素类
- 缺点:不同的排序准则,对应不同的排序器,排序器的定义相对繁琐
底层实现数组、链表、哈希表和红黑树
数组、链表和红黑树都是数据结构中的只是。
什么叫做数据结构呢?
数据结构:描述的就是数据的存储和运算的结构性问题
方法的分类
方法的调用本身就是一个***请求(Request)-响应(Response)***的过程
这个程序中,请求(调用)一个方法帮助我们进行运算
在运算过程中使用的数据,应该由请求者(调用者)来准备提供
将“原材料”(参数)交给方法,让方法进行参数的运算
方法的返回值就是方法运行的结果,方法运行的结果就是方法的响应(Response)
响应的结果就是方法的返回值
所以方法的返回值应该不仅仅是一个数据,同时应该能够反映方法的运行状态
方法的分类:
- 函数式(Function)方法:有参数,有返回值
函数式方法一般的作用是通过对参数的加工,得到最终的返回值
f(x) = y = ax + b
f(x) = y = ax^2 + bx + c
public int getRandomIntrgrt(int start, int range) {
return (int)(Math.random() * range) + start;
}
- 消费式(Customer)方法:有参数,没有返回值
消费式方法一般都是讲参数在方法内部进行消耗,当参数消耗殆尽,方法也就运行完毕了。
消费式方法的参数就是“资源”(现金、组件),方法的运行过程就是消耗资源的过程,资源消耗殆尽,方法就运行完毕 - 生产式(提供式Provider)方法:没有参数,有返回值
生产式方法,在方法内部经过自行运算,得到一个结果,这个结果不收任何外界参数的影响,Math.random()就是典型的生产式方法。
数组
特点:定长且连续
优点:遍历快(连续)
- 同一个数组中,元素的内存地址是连续的,连续的内存地址之间具有规律,我们可以通过一个数组的元素首地址、访问元素的下标和单位元素大小。
计算出访问元素的内存地址,公式如下:
traget = base + (index * elementSize)
- target:目标元素地址
- base:基址,也就是数组的首元素地址
- index:访问元素的下标位
- elementSize:单位元素大小
这种访问方式不需要从头遍历一个数组,这种访问方式是很快捷的。
缺点:增删慢(定长)
- 一个数组在内存中确定之后,其长度是不可改变的,在针对一个数组进行元素的增删的过程当中,都涉及到新数组的开辟和原始数组中元素的拷贝开辟空间和拷贝元素都属于费时操作,所以数组的增删比较慢
Java中的典型应用:ArrayList
链表
特点:不定长且不连续
优点:增删快(不定长)
- 一个链表结构有多少个节点(长度)是不一定的,节点和节点之间通过引用变量来进行维持,在向两个检点之间加入新节点,或者删除一个原有节点时候,只要改变节点之间引用变量的关系即可,不涉及到新空间的开辟和原始元素的拷贝。
缺点:遍历慢(不连续)
- 在同一个链表结构当中,节点和节点之间的内存地址是不连续的,不连续的内存地址是没有规律的,所以即使节点存在于同一个链表中,也无法直接计算其节点的内存地址,如果想要找到某一个元素,每一次必须从链表头开始向下进行遍历,每次都进行遍历,就会导致遍历慢。
Java中的典型应用:LinkedList
链表的应用
- 一元多项式的相加问题:
A = 2x + 3x^2 + 4x^3 + 9x^5 - 7x^6
B = 4x^2 - 4x^3 + 2x^5 + 6x^6
A + B = 2x + 7x^2 + 11x^5 + (-1x^6)
思路分析:
每一个一元多项式可以看做一个链表结构
在这个多项式当中,每一项都是一个Node
一元多项式的相加问题,就是链表的合并问题的变种:
1.如果两个多项式中的对应项相加,和为零,在结果多项式当中,就不存在这一项
2.如果两个多项式之间,对应项相加不为零,在结果多项式当中,这一项的系数就是两个对应项系数的和
3.如果在一个多项式当中存在的项,在另一个多项式当中不存在,这个项直接加入结果多项式当中
链表结构:
- 节点的结构:
/**
* 多项式链表的节点类
* 在一元多项式中,一个节点就是一项
*/
public class Node {
public int mod; //一项的系数
public int power; //一项的幂数
public Node next; //下一个节点的指向域
}
- 多项式的结构:
/**
* 多项式链表类
* 一个多项式链表就是一个多项式
*/
public class Quantic {
private Node head = new Node(); //链表的头节点,不存储数据
/**
*
* @param modArray 系数数组
* @param powerArray 幂数数组
* 要求:modArray.length == powerArray.length
* modArray[i] * x ^ powerArray[i]
*/
public Quantic(int[] modArray, int[] powerArray) {
Node currentNode = head; //定义一个引用变量,指向当前链表的最后一个节点
//[1]确定循环的次数:modArray.length == powerArray.length == 循环次数
int count = modArray.length;
//[2]通过for循环遍历两个数组,分别将两个数组对应位上的元素取出来
int mod = 0;
int power = 0;
for(int i = 0; i < count; i++) {
//[3]从modArray中取出来的就是项的系数
mod = modArray[i];
//[4]从powerArray中取出来的就是项的幂数
power = powerArray[i];
//[5]将一个项的系数和幂数放在同一个Node节点中,构成一个项
Node node = new Node();
node.mod = mod;
node.power = power;
//[6]将所有的节点存放在同一个链表结构中,就代表同一个一元多项式
currentNode.next = node;
currentNode = currentNode.next;
}
}
@Override
public String toString() {
//链表的打印过程就是链表的遍历过程
String result = "";
//[1]获取链表的头结点,链表头结点的下一个节点就是多项式的第一项
Node currentNode = head.next; //currentNode指向当前正在遍历的节点
//[2]使用循环遍历整个链表
if(currentNode != null) {
while(currentNode != null) { //如果当前遍历的节点是null,说明链表已经结束了
//[3]取得当前节点的系数
int mod = currentNode.mod;
//[4]取得当前节点的幂数
int power = currentNode.power;
//[5]用字符串的形式拼出一个多项式:mode * x ^ power
if(mod > 0) {
if("".equals(result)) {
result += mod + "*x^" + power;
}else {
result += " + " + mod + "*x^" + power;
}
}else { //mode < 0
result += " - " + Math.abs(mod) + "*x^" + power;
}
//[6]让当前节点向下移动
currentNode = currentNode.next;
}
}
return result;
}
public static void main(String[] args) {
int[] modArray = new int[] {4, -4, 2, 6};
int[] powerArray = new int[] {2, 3, 5, 6};
Quantic q1 = new Quantic(modArray, powerArray);
System.out.println(q1);
}
}
多项式相加算法:
- 两个链表节点的幂数不同:取幂数小的项,直接加入结果多项式链表中,幂数小的那一项的指针向后挪一位。
- 两个链表节点的幂数相同:将两个节点的系数相加,系数为0,直接跳过,两个指针都向后一位,系数不是0,取系数相加的和作为结果的系数,幂数保持不变。
- 一个链表遍历完成了,另一个链表遍历未完成,将未完成链表后序所有的节点直接拷贝到结果链表中。
哈希表和红黑树
刚刚说过了数组和链表分别可以实现ArrayList和LinkedList。
那么HashSet就是哈希表实现的,也就是数组+链表+红黑树就组合成了HashSet。
我们知道了数组和链表都是什么了,那什么是哈希表呢?红黑树呢?
散列结构(Hash结构):
- 优点:综合了数组和链表的优点,但是在各自领域中,和单纯的数组和链表相比,又具有一定的劣势,但是,从折中的角度来讲,折中结构的增删效率和遍历效率都是相对较高的。
类当中的int hashCode()方法:
- 一个类的对象,一定都具有一个Hash值(hashCode),这个Hash值的来源就是类当中重写的hashCode方法
- 一个对象的Hash值,是通过对象的属性值计算得到的,这个值既不是随机数,也不是对象的内存地址
根据上述理论可知:如果两个对象的内容完全相同,这两个对象的Hash值一定是相同的
注意:两个内容完全不相同的对象,有可能通过hashCode()方法,得到值相同的两个hashCode,只不过概率很低
HashCode实际上是对对象属性的一种映射:我们将一个对象中所有的属性统一通过hashCode()方法,映射为这个对象的一个“值”
这个值,可以说明一部分对象的信息
下面着重讨论Hash表元素的添加过程:
- 得到一个新的,待加入Hash表中的元素,计算器hashCode值
- 将新对象的hashCode值和内存中Hash结构的数组长度进行计算,得到这个新对象应该存储的数组下标
- 如果当前新元素应该存储的数组位置上,没有元素,新数组直接进入这个数组单元即可,元素插入完毕
- 如果新元素需要存储的数组位置上,已经存在其他元素,我们将这种情况称之为发生了一次“碰撞”发生碰撞的元素之间,相互称之为“同义词”,同义词对象之间的HashCode不一定完全一致,但是同义词的HashCode针对数组长度运算,得到的存储下标一定是相同的
如果发生碰撞,新来的元素将会在原有的所有的同义词组成的链表中,逐一进行比较,判断这个元素是否已经出现过:
- 如果新元素,已经存在于同义词组成的链表中,放弃新元素,插入失败
- 如果新元素没有出现在同义词组成的链表中,将新元素插入到这个同义词链表的头部——头插入,元素添加成功
注意:如果元素之间发生碰撞,那么在遍历同义词链表的时候,我们使用对象之间的equals方法而不是==进行元素“是否相同”的判断。
上述存储过程存在的问题:
一个元素存入Hash表中数组的下标,是根据当前数组的长度计算得来的如果这个数组的长度维持不变,那么所有新来元素的下标将永远不会超过当前数组的长度,这也就导致数组不存在扩容的需求。
如果不进行数组扩容,那么这个数组一定会在某一时刻被装满,一旦这个数组被装满,那么之后的元素添加过程一定会产生碰撞,碰撞越频繁,链表遍历的次数也就越频繁,链表遍历的越频繁,元素的添加效率就越低。
这个问题的解决方案:
- 就是数组的扩容,数组扩容的空间越多,碰撞的频率就相对越低
在此引入“加载因子”概念:
加载因子的计算方式:
- loadFactor = 当前数组中已经被占用的容量 / 数组的总容量,加载因子就是一个界限,当现有的数组中,数组元素的占用率达到或超过这个加载因子的限制,就执行数组的扩容。
所以加载因子描述的是:
- Hash表中数组扩容的需求,当数组的占用率达到某一个界定值,Hash表的元素插入效率就低到了一个最低限度,此时需要对数组进行扩容,扩大容量,降低碰撞次数,提高元素插入效率。
在HashMap和HashSet中,加载因子的默认取值是0.75,也就是说,如果Hash表中数组的占用率大于等于75%,这个数组将会扩容。
如果Hash表结构在内存中进行了数组的扩容,那么面临一个问题:
原先存储在A位置上的元素,如果对新数组的长度进行计算,可能得到新的数组下标,需要移动到B位置,我们将这个过程称之为rehash,也就是在Hash表数组扩容之后,对其中元素位置的重新分配的步骤
最后讨论Hash表中元素的查询过程:
在Hash表中,只能够通过一个元素,查询另一个元素
那么查询的过程和元素存储的过程时相似的:
首先将待查询的元素进行计算,得到HashCode
根据待查询元素的HashCode进行运算,得到这个元素在Hash表中数组的对应下标
根据这个对应下标进行查询:
- 如果这个下标位对应的数组元素是null,直接查询失败
- 如果这个位置上的元素不是空,进行同义词链表遍历,遍历过程中,对链表的每一个元素进行eqauls比较,
- 如果有某一元素返回true,则返回这个元素;如果链表遍历结束,没有任何元素返回true,查询失败
Java 8中对Hash表结构的性能优化问题:
如果一个Hash表当中,元素的总量大于等于64,而且其中某一个链表的长度大于等于8,将这个单链表改变为一个红黑树结构,红黑树结构是一种插入效率相对较(单链表)低,但是元素有序,查询效率高的结构,所以这是一种牺牲插入效率,换取查询效率的做法。
对类中,hashCode()方法的要求:
- 如果两个同类型对象的属性值完全相同,要求hashCode方法返回的HashCode值也要相同
否则将会导致在Hash表当中存在重复的值 - 如果两个对象类型不同,或者其中属性值存在不同,那么我们要求这两个对象的HashCode值尽量不相同
否则会提高Hash表中元素的碰撞率,降低元素的存储效率
hashCode方法和equals方法之间的关系:
- 如果两个对象的equals方法返回true,那么我们要求这两个对象的hashCode方法返回相同的值
- 如果两个对象的equals方法返回false,那么我们要求两个对象的hashCode方法返回的值尽量不相同
红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。 这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为O(logn)。 它的统计性能要好于平衡二叉树(AVL树),因此,红黑树在很多地方都有应用。
所以红黑树的原理和二叉树一样,那什么是二叉树呢?
- 图:节点和路径构成的结构,称之为图
- 回路:从一个点出发,经过中间经过其他节点,最终回到这个起点
- 环路:从一个点出发,中间不经过任何其他节点,又回到自己本身
如果图中的路径存在长度,我们将这样的图称之为带权图,我们将边的长度称之为边权(路径的权值)
如果一个图中的路径带有方向,我们将这种图称之为有向图
如果一个图既是有向图,又是带权图,我们将这个图称之为有向带权图
树:树是一种特殊的图,树是一种没有回路和环路的图
将一个图中所有的回路和环路去掉,就得到一个树状图
在树结构中,有这样一种节点:如果找到这个节点,就能够得到这个树结构其他所有的节点,我们将这种类似于起点的节点,称之为树根节点(root),如果在一个树结构中,存在这终结点:这个节点只能被其他节点指向,但是不能够指向其他节点我们将这终结点称之为树的叶子节点(leaf)
如果在树状图中,存在这样的节点:
- 一个节点既能够被其他节点指向,又能够指向其他节点我们将这种节点称之为树的中间节点。
- 如果节点A指向节点B,我们成A节点是B节点的父节点(双亲节点parent)。
- 如果B节点被A节点指向,我们成B节点是A节点的子节点(孩子节点child)。
- 如果在一个树结构当中,要求每一个节点最多只能够具有两个子节点,
我们成这种特殊的树结构为二叉树(Binary Tree)。
在上述结构中: - 我们将B节点称之为A节点的左孩子节点(Left Child)
- 我们将C节点称之为A节点的右孩子节点(Right Child)
- 将A节点统称为B节点和C节点的双亲节点(Parent)
下面重点讨论二叉树的遍历问题:
二叉树的遍历,从总体上来讲,有两种大的方式:
-
广度优先遍历:
- 层序遍历:将处于同一层当中的所有兄弟节点纳入同一个序列
-
深度优先遍历:
- 先序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
下面我们讨论给定二叉树遍历三序中的两序(必然包含一个中序序列)(重点中的重点)
如何唯一确定一个二叉树的结构:
口诀:中序定左右,树根看先后
注意:如果仅给定一个前序序列和一个后序序列是不能够唯一确定一个二叉树结构的,有可能导致某些节点的镜像对称