Java全栈JavaSE:24.数据结构下
Posted new nm个对象
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java全栈JavaSE:24.数据结构下相关的知识,希望对你有一定的参考价值。
1 栈和队列
堆栈是一种先进后出(FILO:first in last out)或后进先出(LIFI:last in first out)的结构。
队列是一种(但并非一定)先进先出(FIFO)的结构。
1.1 Stack类
java.util.Stack是Vector集合的子类。所以Stack是一个List集合类。
1.1.1 Stack类继承树
1.1.2 Stack类的新增方法
比Vector多了几个方法
- (1)peek():查看栈顶元素,不弹出。最后添加的元素位于栈顶
- (2)pop():弹出栈。返回栈顶的元素,并从集合中将该元素删除
- (3)push():压入栈 即添加到链表的头(栈顶)
@Test
public void test3() {
Stack<Integer> list = new Stack<>();
list.push(1); // 入栈
list.push(2); // 入栈
list.push(3); // 入栈
System.out.println(list); // [1, 2, 3]
/*System.out.println(list.pop()); // 结果:3。弹出栈,返回最后添加的元素。并从集合中删除该元素
System.out.println(list.pop()); // 结果:2。弹出栈,返回最后添加的元素。并从集合中删除该元素
System.out.println(list.pop()); // 结果:1。弹出栈,返回最后添加的元素。并从集合中删除该元素
System.out.println(list.pop());//java.util.NoSuchElementException。该集合中的元素已经全部弹出栈了*/
System.out.println(list.peek()); // 结果3.返回最后添加的元素,但不会删除
System.out.println(list.peek()); // 结果3.返回最后添加的元素,但不会删除
System.out.println(list.peek()); // 结果3.返回最后添加的元素,但不会删除
}
}
1.2 Queue(队列)和Deque(双端队列)接口
队列的特点是先进先出。
Queue
除了基本的 Collection
操作外,队列(Queue)还提供其他的插入、提取和检查操作。每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(null
或 false
,具体取决于操作)。Queue
实现通常不允许插入 元素,尽管某些实现(如 )并不禁止插入 。即使在允许 null 的实现中,也不应该将 插入到 中,因为 也用作 方法的一个特殊返回值,表明队列不包含元素。
抛出异常 | 返回特殊值 | |
---|---|---|
插入 | add(e) | offer(e) |
移除 | remove() | poll() |
检查 | element() | peek() |
Queue接口方法测试:
public class Demo1 {
@Test
public void test01(){
// add()方法测试
Queue<Object> aa = new LinkedList<>();
System.out.println("aa.add(3) = " + aa.add(3)); // 添加元素到队列中,添加成功返回true
System.out.println("aa.add(2) = " + aa.add(2)); // 添加元素到队列中,添加成功返回true
System.out.println("aa.add(1) = " + aa.add(1)); // 添加元素到队列中,添加成功返回true
System.out.println("aa = " + aa); // aa = [3, 2, 1]
// offer()方法测试
Queue<Object> aa1 = new LinkedList<>();
System.out.println("aa1.offer(3) = " + aa1.offer(3)); // 添加元素到队列中,添加成功返回true
System.out.println("aa1.offer(2) = " + aa1.offer(2)); // 添加元素到队列中,添加成功返回true
System.out.println("aa1.offer(1) = " + aa1.offer(1)); // 添加元素到队列中,添加成功返回true
System.out.println("aa1 = " + aa); // aa1 = [3, 2, 1]
/*
// poll()方法测试
System.out.println("aa.poll() = " + aa.poll()); // 结果:3.返回最先添加的元素,并从队列中删除该元素
System.out.println("aa.poll() = " + aa.poll()); // 结果:2.返回最先添加的元素,并从队列中删除该元素
System.out.println("aa.poll() = " + aa.poll()); // 结果:1.返回最先添加的元素,并从队列中删除该元素
System.out.println("aa.poll() = " + aa.poll()); // 结果:null.当队列中已经没有元素时,返回null
*/
/*
// remove()方法测试
System.out.println("aa.remove() = " + aa.remove()); // 结果:3.返回最先添加的元素,并从队列中删除该元素
System.out.println("aa.remove() = " + aa.remove()); // 结果:2.返回最先添加的元素,并从队列中删除该元素
System.out.println("aa.remove() = " + aa.remove()); // 结果:1.返回最先添加的元素,并从队列中删除该元素
System.out.println("aa.remove() = " + aa.remove()); // 结果:抛出NoSuchElementException异常.当队列中没有元素时,抛出异常
*/
//element方法测试
System.out.println("aa.element() = " + aa.element()); // 结果3:返回最先添加的元素。但不删除
System.out.println("aa.element() = " + aa.element()); // 结果3:返回最先添加的元素。但不删除
// peek()方法测试
System.out.println("aa.peek() = " + aa.peek()); // 结果3:返回最先添加的元素。但不删除
System.out.println("aa.peek() = " + aa.peek()); // 结果3:返回最先添加的元素。但不删除
System.out.println("aa = " + aa);
}
}
Deque
,名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null
或 false
,具体取决于操作)。
第一个元素(头部) | 最后一个元素(尾部) | |||
---|---|---|---|---|
抛出异常 | 特殊值 | 抛出异常 | 特殊值 | |
插入 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
移除 | removeFirst() | pollFirst() | removeLast() | pollLast() |
检查 | getFirst() | peekFirst() | getLast() | peekLast() |
此接口扩展了 Queue
接口。在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。从 Queue
接口继承的方法完全等效于 Deque
方法,如下表所示:
Queue 方法 | 等效 Deque 方法 |
---|---|
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
双端队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack
类。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于 Deque
方法,如下表所示:
堆栈方法 | 等效 Deque 方法 |
---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
结论:Deque接口的实现类既可以用作FILO堆栈使用,又可以用作FIFO队列使用。
Deque接口的实现类有ArrayDeque和LinkedList,它们一个底层是使用数组实现,一个使用双向链表实现。
2 哈希表
HashMap和Hashtable都是哈希表。
2.1 hashCode值
hash算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的hash code永远是一样的。
2.2 哈希表的详解
- HashMap和Hashtable是散列表,其中维护了一个长度为2的幂次方的Entry类型的数组table,数组的每一个元素被称为一个桶(bucket),你添加的映射关系(key,value)最终都被封装为一个Map.Entry类型的对象,放到了某个table[index]桶中。使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。
- hash表 = 顺序表+链表
- 顺序表:每储存一个数据都有开辟一块空间,会造成内存的浪费
- 链表:链表是不限长度的,每增加元素直接在后面追加即可。但是查询速度慢
- 顺序表+链表:可以综合两者的优势
- hash表如何进行数据储存的:
1. 计算一个对象的hash码
2. 对hash码进行散列处理(防止两个对象的hash码一样)
3. (采用处理后的hash码) & (顺序表长度-1) // 转化为二进制,都是1才为1。其结果一定是`0-(顺序表长度-1)`。因为顺序表的长度是2的n次方
4. 第三步的结果就是该hash码在hash表中的位置
0.HashMap底层储存原理
JKD1.7中
注意:并不是直接使用key的hashCode值来确认元素中顺序表的位置,而是经过了一系列运算。目的是让元素能均匀的分布在顺序表各位置,防止个别链表过长,影响查找效率。
JDK1.8
- JDK1.7是将{key,value}封装为Entry对象,而JDK1.8是将{key,value}封装为Node对象或者TreeNode对象。
- JKD1.7的hash表是采用:顺序表+链表,而JDK1.8是采用顺序表+链表+红黑树
- JDK1.7添加数据时,是添加在链表的前面。JDK1.8是添加在链表的末尾
- JDK1.8中当链表长度超过某一阈值,且顺序表数组长度也超过某一阈值时,会将Node对象转变为TreeNode对象。来提高查询效率
1、数组元素类型:Map.Entry
JDK1.7:
映射关系被封装为HashMap.Entry类型,而这个类型实现了Map.Entry接口。
观察HashMap.Entry类型是个结点类型,即table[index]下的映射关系可能串起来一个链表。因此我们把table[index]称为“桶bucket"。
public class HashMap<K,V>{
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
//...省略
}
//...
}
JDK1.8:
映射关系被封装为HashMap.Node类型或HashMap.TreeNode类型,它俩都直接或间接的实现了Map.Entry接口。
存储到table数组的可能是Node结点对象,也可能是TreeNode结点对象,它们也是Map.Entry接口的实现类。即table[index]下的映射关系可能串起来一个链表或一棵红黑树(自平衡的二叉树)。
public class HashMap<K,V>{
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//...省略
}
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;//是红结点还是黑结点
//...省略
}
//....
}
public class LinkedHashMap<K,V>{
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//...
}
2、数组的长度始终是2的n次幂
table数组的默认初始化长度:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
如果你手动指定的table长度不是2的n次幂,会通过如下方法给你纠正为2的n次幂
JDK1.7:
HashMap处理容量方法:
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
Integer包装类:
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
JDK1.8:
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
如果数组不够了,扩容了怎么办?扩容了还是2的n次幂,因为每次数组扩容为原来的2倍
JDK1.7:
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);
}
JDK1.8:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap原来的容量
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}//newCap = oldCap << 1 新容量=旧容量扩容为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//......此处省略其他代码
}
那么为什么要保持table数组一直是2的n次幂呢?
3、那么HashMap是如何决定某个映射关系存在哪个桶的呢?
因为hash值是一个整数,而数组的长度也是一个整数,有两种思路:
①hash 值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算,不能保证均匀存放,可能会导致某些table[index]桶中的元素太多,而另一些太少,因此不合适。
②hash 值 & (table.length-1),因为table.length是2的幂次方,因此table.length-1是一个二进制低位全是1的数,所以&操作完,也会得到一个[0,table.length-1]范围的值。
JDK1.7:
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1); //此处h就是hash
}
JDK1.8:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // i = (n - 1) & hash
tab[i] = newNode(hash, key, value, null);
//....省略大量代码
}
4、hash是hashCode的再运算
不管是JDK1.7还是JDK1.8中,都不是直接用key的hashCode值直接与table.length-1计算求下标的,而是先对key的hashCode值进行了一个运算,JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。
JDK1.7:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK1.8:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
虽然算法不同,但是思路都是将hashCode值的高位二进制与低位二进制值进行了异或,然高位二进制参与到index的计算中。
为什么要hashCode值的二进制的高位参与到index计算呢?
因为一个HashMap的table数组一般不会特别大,至少在不断扩容之前,那么table.length-1的大部分高位都是0,直接用hashCode和table.length-1进行&运算的话,就会导致总是只有最低的几位是有效的,那么就算你的hashCode()实现的再好也难以避免发生碰撞,这时让高位参与进来的意义就体现出来了。它对hashcode的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。
5、解决[index]冲突问题
虽然从设计hashCode()到上面HashMap的hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的hashCode值相同,或者hashCode值就算不同,通过hash()函数计算后,得到的index也会存在大量的相同,因此key分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?
JDK1.8之间使用:数组+链表的结构。
JDK1.8之后使用:数组+链表/红黑树的结构。
即hash相同或hash&(table.lengt-1)的值相同,那么就存入同一个“桶”table[index]中,使用链表或红黑树连接起来。
6、为什么JDK1.8会出现红黑树和链表共存呢?
因为当冲突比较严重时,table[index]下面的链表就会很长,那么会导致查找效率大大降低,而如果此时选用二叉树可以大大提高查询效率。
但是二叉树的结构又过于复杂,如果结点个数比较少的时候,那么选择链表反而更简单。
所以会出现红黑树和链表共存。
7、什么时候树化?什么时候反树化?
static final int TREEIFY_THRESHOLD = 8;//树化阈值
static final int UNTREEIFY_THRESHOLD = 6;//反树化阈值
stati以上是关于Java全栈JavaSE:24.数据结构下的主要内容,如果未能解决你的问题,请参考以下文章