Java全栈(二)JavaSE:24.数据结构下

Java HashMap与队列详解:数据存储与冲突解决
本文详细介绍了Java中HashMap和队列(Stack与Queue)的工作原理。HashMap通过数组+链表/红黑树结构存储键值对,利用哈希算法确保高效查找。队列遵循先进先出(FIFO)原则,Stack是其变体,后进先出。文章深入解析了HashMap的扩容机制、冲突解决策略以及JDK1.7与1.8的区别,同时讨论了队列接口Queue和Deque的使用及其在不同场景下的应用。

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)还提供其他的插入、提取和检查操作。每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(nullfalse,具体取决于操作)。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”。此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(nullfalse,具体取决于操作)。

第一个元素(头部) 最后一个元素(尾部)
抛出异常 特殊值 抛出异常 特殊值
插入 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:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值