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)还提供其他的插入、提取和检查操作。每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(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:

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.数据结构下的主要内容,如果未能解决你的问题,请参考以下文章

Java全栈JavaSE:2.数据库之Mysql下

Java全栈JavaSE:15.异常

Java全栈JavaSE:14.面向对象下

Java全栈JavaSE:8.Java方法详解

Java全栈JavaSE:6.java包机制,javadoc命令

Java全栈JavaSE:3.数据库之JDBC上