数据结构之Map和Set
系列文章目录
数据结构之ArrayList_arraylist o(1) o(n)-CSDN博客
数据结构之LinkedList-CSDN博客
数据结构之栈_栈有什么方法-CSDN博客
数据结构之队列-CSDN博客
数据结构之二叉树-CSDN博客
数据结构之优先级队列-CSDN博客
常见的排序方法-CSDN博客
目录
系列文章目录
前言
一、二叉搜索树
1. 查找节点
2. 插入节点
3. 删除节点
4. 二叉搜索树的性能
二、TreeMap和TreeSet
1. Map接口
1. 常用方法
2. 注意事项
3. TreeMap 和 HashMap 的区别
2. Set接口
1. 常用方法
2. 注意事项
3. TreeSet 和 HashSet 的区别
三、哈希表
1. 哈希冲突
2. 哈希桶的模拟实现
3. 泛型哈希桶的实现
4. 哈希表源码
前言
本文系统介绍了常见数据结构及其实现原理,主要包括二叉搜索树、哈希表、TreeMap/TreeSet等内容。详细讲解了二叉搜索树的查找、插入、删除操作及其时间复杂度分析;对比了TreeMap与HashMap、TreeSet与HashSet的特性差异;深入剖析了哈希冲突的解决策略,包括开散列法的具体实现。文章还解读了JDK中HashMap的核心源码,包括初始化、扩容机制、树化条件等关键实现细节。通过理论分析与代码示例相结合的方式,全面阐述了这些数据结构的底层实现原理和应用场景。
一、二叉搜索树
BinarySearchTree: 二叉搜索树的类;
TreeNode: 静态内部类,用于创建树的节点;
root: 表示二叉搜索树的根节点;
public class BinarySearchTree {static class TreeNode{public int val;public TreeNode left;public TreeNode right;public TreeNode(int val){this.val = val;}}public TreeNode root = null;// 二叉搜索树的方法// ......
}
1. 查找节点
二叉搜索树左子树的节点值都比根节点小,右子树的节点值都比根节点大;
search(int val): boolean
定义节点 cur 用于遍历二叉搜索树,如果要查找的值 val 大于当前节点 cur 的值,去右子树找,如果 val 小于当前节点 cur 的值,去左子树找,找到返回 true,没找到返回 false;
public boolean search(int val){TreeNode cur = root;while(cur != null){if(cur.val < val){cur = cur.right;}else if(cur.val > val){cur = cur.left;}else{return true;}}return false;}
2. 插入节点
insert(int val): boolean
定义节点 cur 用于遍历二叉搜索树,定义节点 parent,表示 cur 的父节点;
如果要查插入的值 val 大于当前节点 cur 的值,parent 指向 cur,cur 去右子树继续搜索;
如果要查插入的值 val 小于当前节点 cur 的值,parent 指向 cur,cur 去左子树继续搜索;
如果找到这个值为 val 的节点,表示这个节点已经存在了,不能再进行插入,因此返回 false;
如果 cur 为 null,表示这个位置就是新插入节点的位置,使用 parent 指向它即可;
public boolean insert(int val){TreeNode node = new TreeNode(val);if(root == null){root = node;return true;}TreeNode parent = null;TreeNode cur = root;while(cur != null){if(cur.val < val){parent = cur;cur = cur.right;}else if(cur.val > val){parent = cur;cur = cur.left;}else{return false;}}if(parent.val < val){parent.right = node;}else{parent.left = node;}return true;}
3. 删除节点
remove(int val): void 搜索值为 val 的节点,搜索到执行删除逻辑;
removeNode(TreeNode parent, TreeNode cur): void 删除 cur 节点;
删除逻辑:
找到要删除的节点 cur;
分以下三种情况进行讨论:
- 1. cur.left == null;
- 2. cur.right == null;
- 3. cur.left != null && cur.right != null;
每种情况又分三种情况进行讨论:
- cur 节点是二叉搜索树的根节点;
- cur 节点是 parent 节点的左子树;
- cur 节点是 parent 节点的右子树;
讨论第一种情况 cur.left == null:
- 如果 cur 是二叉搜索树的根节点,直接让 root = cur.right 即可;
- 如果 cur 是 parent 节点的左子树,直接让 parent.left = cur.right 即可;
- 如果 cur 是 parent 节点的右子树,直接让 parent.right = cur.right 即可;
讨论第二种情况 cur.right == null:
- 如果 cur 是二叉搜索树的根节点,直接让 root = cur.left 即可;
- 如果 cur 是 parent 节点的左子树,直接让 parent.left = cur.left 即可;
- 如果 cur 是 parent 节点的右子树,直接让 parent.right = cur.left 即可;
讨论第三种情况 cur.left != null && cur.right != null:
思路:以左子树最右边的节点或者右子树最左边的节点,代替 cur 节点,再删除左子树最右边的节点或者右子树最左边的节点;
下面以找右子树最左边的节点为例:
定义 tp 指向 cur,t 指向 cur.right,持续向左搜索,直至 t.left == null,表示 t 就是右子树最左边的节点了,更新 cur 的值;
此时分两种情况:
- 1. cur 右树的左子树不为空:此时 tp.left = t.right 即可;
- 2. cur 右树的左子树为空:tp.right = t.right 即可;
public void remove(int val){TreeNode cur = root;TreeNode parent = null;while(cur != null){if(cur.val > val){parent = cur;cur = cur.left;}else if(cur.val < val){parent = cur;cur = cur.right;}else{removeNode(parent, cur);}}}private void removeNode(TreeNode parent, TreeNode cur){if(cur.left == null){if(cur == root){root = cur.right;}else if(cur == parent.left){parent.left = cur.right;}else{parent.right = cur.right;}}else if(cur.right == null){if(cur == root){root = cur.left;}else if(cur == parent.left){parent.left = cur.left;}else{parent.right = cur.left;}}else{TreeNode tp = cur;TreeNode t = cur.right;while(t.left != null){tp = t;t = t.left;}cur.val = t.val;if(tp.right == t){tp.right = t.right;}else{tp.left = t.right;}}}
4. 二叉搜索树的性能
如果二叉搜索树是一棵单分支树(右单树),那么查询元素的效率就会退化为 O(n);
因此为了解决这个问题引入了高度平衡的二叉搜索树(AVL树),AVL 树在插入节点或删除节点的时候会进行左旋转或右旋转,维持高度平衡的特性,以保证查询效率;
Java 当中 Map 和 Set 接口的底层实际上是红黑树,红黑树是一棵近似平衡的二叉搜索树;
二、TreeMap和TreeSet
1. Map接口
Map 是一个接口类,存储的是一个 <K, V> 结构的键值对,K 唯一,不能重复;
1. 常用方法
get(Object key): V 返回 key 对应的 value;
getOrDefaultValue(Object key, V defaultValue): V 返回 key 对应的 value,如果 key 不存在,就返回 defaultValue;
put(K key, V value): V 设置 key 对应的值为 value;
remove(Object key): V 删除 key 对应的映射;
keySet(): Set<K> 返回不重复的 key 的集合;
values(): Collection<V> 返回 value 的可重复集合;
entrySet(): Set<Map.Entry<K, V>> 返回所有的 key-value 映射关系;
containsKey(Object key): boolean 查询是否包含 key;
containsValue(Object value): boolean 查询是否包含 value;
2. 注意事项
Map 是接口,不能实例化对象,TreeMap 和 HashMap 实现了这个接口,可以实例化对象;
Map 中 key 是唯一的,value 是可以重复的;
在 TreeMap 中插入键值对,key 不能为空,且必须是可比较的,value 可以为空,但 HashMap 中的 key 和 value 都可以为空;
Map 中的 key 不能修改,如果要修改,只能删除再重新添加;
3. TreeMap 和 HashMap 的区别
- TreeMap 底层是红黑树;
- HashMap 底层是哈希桶;
- TreeMap 插入,删除,查找元素的时间复杂度是 log(n);
- HashMap 插入,删除,查找元素的时间复杂度是 O(1);
- TreeMap 中的 key 是有序的;
- HashMap 中的元素是无序的;
- TreeMap 中元素的查找,插入,删除都是需要比较的;
- HashMap 中元素的查找,插入,删除不需要比较(通过哈希函数计算哈希地址);
- TreeMap 中的 key 必须要能够比较,否则会抛异常;
- HashMap 中的自定义类型需要重写 equals() 和 hashCode() 方法;
- TreeMap 适用于需要 key 有序的场景;
- HashMap 适用于更高的时间性能的场景;
2. Set接口
Set 是继承 Collection 的接口类,其中只存储 key;
1. 常用方法
add(E e): boolean 添加元素,重复元素不能被添加成功;
clear(): void 清空集合;
contains(Object 0): boolean 判断 o 是否在集合中;
remove(Object o): boolean 删除集合中的 o;
size(): int 返回 set 中元素的个数;
isEmpty(): boolean 检查 set 是否为空;
toArray(): Object[] 将 set 中的元素作为数组返回;
containsAll(Collection<?> c): boolean 判断 c 中的元素是否在 set 中全部存在;
addAll(Collection<? extends E> c): boolean 将 c 中的元素添加到 set 中;
2. 注意事项
Set 是继承 Collection 的接口类;
Set 中只能存储 key,并且 key 唯一;
TreeSet 的底层是使用 Map 实现的,使用 key 与 Object 的一个默认对象作为键值对插入到 Map 中;
Set 的最大作用就是对集合中的元素去重;
实现 Set 接口的有 TreeSet 和 HashSet;
Set 中的 key 不能修改,如果要修改,只能删除后再重新添加;
TreeSet 不能插入空元素,HashSet 可以;
3. TreeSet 和 HashSet 的区别
- TreeSet 底层是红黑树;
- HashSet 底层是哈希桶;
- TreeSet 插入,删除,查找元素的时间复杂度是 log(n);
- HashSet 插入,删除,查找元素的时间复杂度是 O(1);
- TreeSet 中的 key 是有序的;
- HashSet 中的元素是无序的;
- TreeSet 中元素的查找,插入,删除都是需要比较的;
- HashSet 中元素的查找,插入,删除不需要比较(通过哈希函数计算哈希地址);
- TreeSet 中的 key 必须要能够比较,否则会抛异常;
- HashSet 中的自定义类型需要重写 equals() 和 hashCode() 方法;
- TreeSet 适用于需要 key 有序的场景;
- HashSet 适用于更高的时间性能的场景;
三、哈希表
哈希表通过哈希函数,可以使 key 和 value 建立一一对应的映射关系,因此在哈希表中,插入,删除,更新和查询元素的时间复杂度为 O(1);
1. 哈希冲突
哈希冲突:当两个不同的元素,通过哈希函数的计算,生成了相同的哈希地址,这时候就会产生哈希冲突;
避免哈希冲突的方式通常是合理设计哈希函数;
解决哈希冲突的方式有两种:闭散列和开散列;
闭散列:也叫开放定址法。当发生哈希冲突时,如果哈希表没有被装满,说明哈希表中一定还有空位置,因此把 key 放到冲突位置的下一个空位置去;
闭散列寻找下一个空位置常用的方式有线性探测以及二次探测;但是占用下一个空位置,必然会引发新的哈希冲突;
开散列:又叫链地址法(开链法)。首先对 key 进行哈希计算,如果产生相同的哈希地址,就把产生这个地址的多个元素都放入到一个集合里面,每一个这样的集合都称为桶,桶中的元素通过一个单链表串联起来,头结点存储在哈希表中;
2. 哈希桶的模拟实现
Node: 静态内部类,表示哈希表的节点,包括 key,value,next;
array: 存放节点的数组;
usedSize: 哈希表中元素的个数;
hashBuck(): 哈希表的构造方法,默认开辟的数组长度是 10;
public class HashBuck {static class Node{public int key;public int val;public Node next;public Node(int key, int val){this.key = key;this.val = val;}}private Node[] array;private int usedSize;public HashBuck(){this.array = new Node[10];}// 哈希表的方法// ......
}
哈希表元素的插入逻辑:
首先使用哈希函数计算 key 对应的哈希值,哈希值模上数组的长度,得到存放的数组下标 ;
得到下标后,将元素插入到 index 位置的链表中,可以头插,也可以尾插;
插入完成后,计算负载因子 usedSize / array 数组的长度;
如果负载因子大于等于 0.75,进行扩容,扩容的时候要注意重新哈希;
如果负载因子小于 0.75,什么也不用做;
public void put(int key, int val){int index = key % this.array.length;Node cur = this.array[index];while(cur != null){if(cur.key == key){cur.val = val;return;}cur = cur.next;}Node node = new Node(key, val);node.next = this.array[index];this.array[index] = node;this.usedSize++;double loadFactor = loadFactor();if(loadFactor >= 0.75){resize();}}private double loadFactor(){return this.usedSize * 1.0 / this.array.length;}private void resize(){int size = 2 * this.array.length;Node[] tmpArr = new Node[size];for(int i = 0; i < array.length; i++){Node cur = array[i];while(cur != null){Node curNext = cur.next;int newIndex = cur.key % size;cur.next = tmpArr[newIndex];tmpArr[newIndex] = cur;cur = curNext;}}this.array = tmpArr;}
哈希表的查询逻辑:
根据给定的 key 计算数组的下标 ;
遍历下标 index 对应链表,判断是否存在 key,存在返回对应 val,不存在返回 -1;
public int get(int key){int index = key % this.array.length;Node cur = this.array[index];while(cur != null){if(cur.key == key){return cur.val;}cur = cur.next;}return -1;}
3. 泛型哈希桶的实现
泛型哈希桶的实现需要注意,元素之间比较不能使用等号,而是需要使用 equals() 方法;
如果是自定义类型,需要重写 equals() 方法 和 hashCode() 方法;
public class HashBuckUpgrade<K, V> {static class Node<K, V>{public K key;public V val;public Node<K, V> next;public Node(K key, V val){this.key = key;this.val = val;}}private Node<K, V>[] array;private int usedSize;public HashBuckUpgrade(){this.array = new Node[10];}public void put(K key, V val){int hash = key.hashCode();int index = hash % this.array.length;Node cur = this.array[index];while(cur != null){if(cur.key.equals(key)){cur.val = val;return;}cur = cur.next;}Node<K, V> node = new Node<>(key, val);if(this.array[index] == null){this.array[index] = node;}else{cur = this.array[index];while(cur.next != null){cur = cur.next;}cur.next = node;}this.usedSize++;double loadFactor = loadFactor();if(loadFactor >= 0.75){resize();}}private double loadFactor(){return this.usedSize * 1.0 / this.array.length;}private void resize(){int size = 2 * this.array.length;Node<K, V>[] tmp = new Node[size];for(int i = 0; i < this.array.length; i++){Node<K, V> cur = this.array[i];while(cur != null){int hash = cur.key.hashCode();int newIndex = hash % tmp.length;Node curNext = cur.next;if(tmp[newIndex] == null){tmp[newIndex] = cur;}else{Node<K, V> t = tmp[newIndex];while(t.next != null){t = t.next;}t.next = cur;}cur.next = null;cur = curNext;}}this.array = tmp;}public V get(K key){int hash = key.hashCode();int index = hash % this.array.length;Node<K, V> cur = this.array[index];while(cur != null){if(cur.key.equals(key)){return cur.val;}cur = cur.next;}return null;}
}
equals() 和 hashCode() 的区别:
hashCode 用于生成哈希值,哈希值用于计算哈希数组的下标;
按照下标找到哈希桶后,需要遍历,元素之间需要使用 equals() 方法进行比较;
4. 哈希表源码
属性:
DEFAULT_INITIAL_CAPACITY:哈希表的默认大小为 16;
MAXIMUM_CAPACITY:哈希表最大容量;
DEFAULT_LOAD_FACTOR:负载因子;
TREEIFY_THRESHOLD:链表的长度超过 8,数组的长度超过 64,会发生树化;
UNTREEIFY_THRESHOLD:当链表的长度小于 6,解树化;
MIN_TREEIFY_CAPACITY:当数组的长度超过 64,链表的长度超过 8,会发生树化;
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16static final int MAXIMUM_CAPACITY = 1 << 30;static final float DEFAULT_LOAD_FACTOR = 0.75f;static final int TREEIFY_THRESHOLD = 8;static final int UNTREEIFY_THRESHOLD = 6;static final int MIN_TREEIFY_CAPACITY = 64;
}
构造方法:
HashMap(int initialCapacity, float loadFactor):
如果默认容量小于 0,抛出异常;
如果默认容量超过最大容量,赋值为最大容量;
如果负载因子小于等于 0,抛出异常;
如果给定一个容量,会调用 tableSizeFor() 方法,给分配一个接近 2^n 的容量,2^n >= initialCapacity;
虽然计算完了容量,但是还是没有给数组分配内存,只有在添加元素的时候,才会调用 resize() 方法分配内存;
HashMap(): 调用无参的构造方法时,是不会分配内存的,只有在第一次插入元素的时候,才会调用 resize() 方法分配内存;
resize(): Node<K, V>[]:
如果数组为空,就根据容量进行初始化,给数组分配内存;
public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
在哈希表中放元素:
hash(Object key): int 调用对象的 hashCode() 方法计算 key 的哈希值,将高 16 位和低 16 位异或,目的是让哈希值在数组中的分布更加均匀;
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict): V
如果数组为空,会调用 resize() 方法进行扩容,给数组分配内存;
计算节点存放的下标,如果下标中的结点是空,直接把该节点放到数组中在,作为头结点;
如果下标中的节点不为空,判断是不是树的节点,如果是树的节点,就把新节点插入到树中;
如果是链表的节点,执行尾插法,把新节点插入到链表的后面;如果链表的长度大于等于 8,就调用 treeifBin() 方法,可能会进行树化;
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}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)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
treeifBin(Node<K, V>[] tab, int hash): void
如果数组为空或者数组的长度小于 64,就调用 resize() 方法,进行扩容,否则就执行树化操作;
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}}