From 4050eb510da3411fd618dfedcd8f8ea420e89b92 Mon Sep 17 00:00:00 2001 From: sanwuhong Date: Sat, 9 Apr 2022 21:38:01 +0800 Subject: [PATCH] update toc format --- .../java/com/learning/algorithm/ALGORITHM.md | 56 +++- .../java/com/learning/basic/COLLECTION.md | 177 +++++++------ src/main/java/com/toc/ALGORITHM.md | 247 +++++++++++------- src/main/java/com/toc/COLLECTION.md | 177 +++++++------ 4 files changed, 383 insertions(+), 274 deletions(-) diff --git a/src/main/java/com/learning/algorithm/ALGORITHM.md b/src/main/java/com/learning/algorithm/ALGORITHM.md index 8fd62ee..0aa51bc 100644 --- a/src/main/java/com/learning/algorithm/ALGORITHM.md +++ b/src/main/java/com/learning/algorithm/ALGORITHM.md @@ -570,7 +570,9 @@ public class Solution { - [移除元素](https://leetcode-cn.com/problems/remove-element/) : 类似于快排的变种 - [移动零](https://leetcode-cn.com/problems/move-zeroes/) - [删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) -- [下一个排列](https://leetcode-cn.com/problems/next-permutation/): 查找的方法比较巧妙,递减区间的利用及 +- [下一个排列](https://leetcode-cn.com/problems/next-permutation/): 查找的方法比较巧妙,递减区间的利用 +- [删除有序数组中的重复项 II](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/): 快慢指针,慢指针为确定的区间 + ### 二分法 [一道可以考察「二分」本质的面试题](https://mp.weixin.qq.com/s/RW20ob2oO4Bfd-PcukTVJA) > 「⼆分」的本质是⼆段性,并⾮单调性。只要⼀段满⾜某个性质,另外⼀段不满⾜某个性质,就可以⽤「⼆分」 @@ -838,9 +840,58 @@ public class BinarySearchRight { ### 滑动窗口 +#### 指针移动缩小窗口 + +参考资料:[我写了套框架,把滑动窗口算法变成了默写题](https://mp.weixin.qq.com/s/ioKXTMZufDECBUwRRp3zaA) + +常规算法模版: +``` +/* 滑动窗口算法框架 */ +void slidingWindow(string s, string t) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + while (right < s.size()) { + // c 是将移入窗口的字符 + char c = s[right]; + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + ... + + /*** debug 输出的位置 ***/ + printf("window: [%d, %d)\n", left, right); + /********************/ + + // 判断左侧窗口是否要收缩 + while (window needs shrink) { + // d 是将移出窗口的字符 + char d = s[left]; + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + ... + } + } +} +``` + + +- [无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/): 快慢指针+hash表结合 +- [最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) - [找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/) +- [长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) + +#### 指针不动窗口平滑 +滑动窗口的一种变种为,窗口保持在一定的size。 +1. 第一种为size是题目要求的,如k长的窗口最大值 +2. 第二种为size是当前满足的最优接。left位置不匹配,则平滑窗口。result即为`right-left` +- [替换后的最长重复字符](https://leetcode-cn.com/problems/longest-repeating-character-replacement/) +- [滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) ## 队列 @@ -956,6 +1007,7 @@ class MaxQueue { - [剑指 Offer 59 - II. 队列的最大值](https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/) - [剑指 Offer 59 - I. 滑动窗口的最大值](https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/): review - [环形子数组的最大和](https://leetcode-cn.com/problems/maximum-sum-circular-subarray/) +- [滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) ## 栈 - 栈具有记忆的功能,由其数据的特殊性可以用来DFS搜索 @@ -1845,6 +1897,8 @@ class UnionFindSet { 位运算与运算转换: \ [Pow(x, n)](https://leetcode-cn.com/problems/powx-n/) :快速幂 +## KMP +[实现 strStr()](https://leetcode-cn.com/problems/implement-strstr/) ## 常用操作 ### 前缀和 diff --git a/src/main/java/com/learning/basic/COLLECTION.md b/src/main/java/com/learning/basic/COLLECTION.md index 8f53c02..3c9615a 100644 --- a/src/main/java/com/learning/basic/COLLECTION.md +++ b/src/main/java/com/learning/basic/COLLECTION.md @@ -8,11 +8,11 @@ - CopyOnWriteArrayList:使用了读写分离的思想,在写数据的时候上ReentrantLock锁并新建一个数组,读数据仍从旧数组中读取,而新数据在新增或删除完成之后直接替换旧数组。虽然线程安全,对于频繁写数据的场景效率很低。 - ListIterator: 更强大的Iterator的子类,用于各种List类的访问,并支持双向移动。 - LinkedList:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环) - - getFirst() 和element() 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. - - peek() 方法与上诉类似,只时列表为空返回null - - removeFirst() 和 remove() 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 - - poll() 同样移除并返回列表头,只是列表为空返回Null -- Stack:pop()、push()、 peek()方法,其中peek()返回栈顶元素,而不将其移除。 + - `getFirst()` 和`element()` 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. + - `peek()` 方法与上诉类似,只时列表为空返回null + -` removeFirst()` 和 `remove()` 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 + - `poll()` 同样移除并返回列表头,只是列表为空返回Null +- Stack:`pop()`、`push()`、 `peek()`方法,其中`peek()`返回栈顶元素,而不将其移除。 ### ArrayList 部分源码 #### Object[]数组 @@ -22,8 +22,8 @@ #### 扩容 添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。 -> - 主要一个超精度负数判断,如果经度过长,则默认使用当前长度 -> - 数据复制使用Arrays.copyOf(elementData, newCapacity); +> - 扩容操作中主要的是一个超精度负数判断,如果经度过长,则默认使用当前长度 +> - **数据复制**使用Arrays.copyOf(elementData, newCapacity); 因为是一步操作,所以用于快速失败的modCount+1 ```java @@ -70,12 +70,12 @@ private void grow(int minCapacity) { **思考:arrayList 为啥1.5倍扩容?** #### 删除元素 -调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。 +调用 `System.arraycopy()` 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为`O(N)`,可以看到 ArrayList 删除元素的代价是非常高的。 > `System.arraycopy(elementData, index+1, elementData, index, numMoved);` #### 快速失败 -快速失败(fail-fast) 是 Java 集合的一种错误检测机制。在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 ConcurrentModificationException 异常。 -> 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。 +快速失败(fail-fast) 是 Java 集合的一种错误检测机制。在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 `ConcurrentModificationException` 异常。 +> 在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。 modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 > 使用迭代器遍历时默认会传入当前的数组的modCount,每次操作进行检测 @@ -103,47 +103,47 @@ CopyOnWriteArrayList读写分离list 适用于读多写少的场景 ``` - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - Object[] elements = getArray(); - int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1); - newElements[len] = e; - setArray(newElements); - return true; - } finally { - lock.unlock(); - } +public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + Object[] elements = getArray(); + int len = elements.length; + Object[] newElements = Arrays.copyOf(elements, len + 1); + newElements[len] = e; + setArray(newElements); + return true; + } finally { + lock.unlock(); } +} - @SuppressWarnings("unchecked") - private E get(Object[] a, int index) { - return (E) a[index]; - } +@SuppressWarnings("unchecked") +private E get(Object[] a, int index) { + return (E) a[index]; +} ``` ### LinkedList LinkedList定义了一个内部的Node 节点,基于双向链表实现,使用 Node 存储链表节点信息。 相关操作: -- getFirst() 和element() 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. -- peek() 方法与上诉类似,只时列表为空返回null -- removeFirst() 和 remove() 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 -- poll() 同样移除并返回列表头,只是列表为空返回Null -``` - private static class Node { - E item; - Node next; - Node prev; - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } +- `getFirst()` 和`element()` 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. +- `peek()` 方法与上诉类似,只时列表为空返回null +- `removeFirst()` 和 `remove()` 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 +- `poll()` 同样移除并返回列表头,只是列表为空返回Null +``` +private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; } +} ``` @@ -163,14 +163,14 @@ LinkedList定义了一个内部的Node 节点,基于双向链表实现,使 数据结构:基础的数据节点Node 继承Map.Entry 接口实现的key-value的数据节点。 基本的存储的结构为`Key Value`的Node节点的数组 `transient Node[] table;` -threshold:临界值,当实际大小(容量*填充因子)超过临界值时,会进行扩容 - -TREEIFY_THRESHOLD:树化的最小长度8。 -> 为啥设定为8,TreeNodes占用空间是普通Nodes的两倍,建立树是需要时间及空间成本的。因此此处基于时间与空间的权衡定位8,具体可以看源码。 +相关参数: +- **threshold**:临界值,当实际大小(容量*填充因子)超过临界值时,会进行扩容 +- **UNTREEIFY_THRESHOLD**:树变成链表的阀值6。 +- **MIN_TREEIFY_CAPACITY**:hashMap进行树化的最低条件table的大小达到64,否则只是进行扩容。 +- **TREEIFY_THRESHOLD**:树化的最小长度8。 + > 为啥设定为8,TreeNodes占用空间是普通Nodes的两倍,建立树是需要时间及空间成本的。因此此处基于时间与空间的权衡定位8,具体可以看源码。 -UNTREEIFY_THRESHOLD:树变成链表的阀值6。 -MIN_TREEIFY_CAPACITY:hashMap进行树化的最低条件table的大小达到64,否则只是进行扩容。 Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; ``` @@ -183,8 +183,8 @@ Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; ``` 负载因子:hashMap的负载因子默认为0.75,当hashMap中的元素达到 3/4就进行元素的扩容。 -> 负载因子大小的关系,若负载因子为1,那么在出现大量的hash膨胀的情况下,元素会较密集,并且都是用链表或者红黑树的方式连接,导致查询效率较低。 -> 若负载因子为0.5 那么就会造成空间的浪费,元素分布较为稀疏。 +> 负载因子与大小的关系?\ +> 负载因子大小的关系,若负载因子为1,那么在出现大量的hash膨胀的情况下,元素会较密集,并且都是用链表或者红黑树的方式连接,导致查询效率较低。 若负载因子为0.5 那么就会造成空间的浪费,元素分布较为稀疏。 #### put 操作 1. 首先判断table是否需要扩容,若需要进行扩容操作 @@ -202,8 +202,7 @@ Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; 3. 出现哈希冲突的情况,由于每次扩容的大小默认为2的n次方,因此重散列的位置只会为当前位置或者当前位置+旧数组大小两个位置。 4. 如果节点存在哈希冲突,则根据位运算计算最新的位置是否为0,为0表示无需移动节点。为1表示移动到oldCap+j的位置。 5. 针对出现红黑树的哈希冲突,同理。此处针对红黑树冲突的需要判断重散列的节点是否需要重新建立红黑树。 - -- 如果初始化容量大小不为2的幂次方,那么在初始化的时候,会计算threshold为大于初始化数的最近2的幂次方数,在实际使用的时候声明为table的大小。 +> 如果初始化容量大小不为2的幂次方,那么在初始化的时候,会计算threshold为大于初始化数的最近2的幂次方数,在实际使用的时候声明为table的大小。 #### HashMap红黑树查找 @@ -254,15 +253,15 @@ Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; 针对建立红黑树或者添加树节点,若使用equal及class的compare 均无法确定添加节点的方向。 则使用对象的类名进行判断,若类名依然相同,则使用System根据对象地址换算的hashcode编码判断添加方向。 ``` - static int tieBreakOrder(Object a, Object b) { - int d; - if (a == null || b == null || - (d = a.getClass().getName(). - compareTo(b.getClass().getName())) == 0) - d = (System.identityHashCode(a) <= System.identityHashCode(b) ? - -1 : 1); - return d; - } +static int tieBreakOrder(Object a, Object b) { + int d; + if (a == null || b == null || + (d = a.getClass().getName(). + compareTo(b.getClass().getName())) == 0) + d = (System.identityHashCode(a) <= System.identityHashCode(b) ? + -1 : 1); + return d; +} ``` #### hash 方法 @@ -276,9 +275,10 @@ static final int hash(Object key) { } ``` -问题:为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode? -1. 上述解释的点,低位与高位混合,加大hash的随机性。 -2. key的hashcode可能被重写,重写的hash函数冲突的概率无法保证。因此hashMap需要在此基础使用自己的hash加大随机性。 +> 问题:为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode? +> 1. 上述解释的点,低位与高位混合,加大hash的随机性。 +> 2. key的hashcode可能被重写,重写的hash函数冲突的概率无法保证。因此hashMap需要在此基础使用自己的hash加大随机性。 + #### Java1.7并发下循环链表 Java1.7 中HashMap扩容是使用类似**头插法**的方式把旧节点转移到新的数组上。假设节点出现哈希冲突以链表的方式连接,且头节点1和节点2 扩容的位置仍然不变。 1. 当线程1与线程2新建完新数组,并且执行到上述链表节点的扩容,执行旧数组的头结点3。举个例子链表为 3->7 @@ -287,35 +287,34 @@ Java1.7 中HashMap扩容是使用类似**头插法**的方式把旧节点转移 4. 当前数组节点的链表顺序为 7->3,重新进行节点3的头插,就会导致一个循环链表的现象 ``` - 1 void transfer(Entry[] newTable) { - 2 Entry[] src = table; //src引用了旧的Entry数组 - 3 int newCapacity = newTable.length; - 4 for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组 - 5 Entry e = src[j]; //取得旧Entry数组的每个元素 - 6 if (e != null) { - 7 src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象) - 8 do { - 9 Entry next = e.next; -10 int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置 -11 e.next = newTable[i]; //标记[1] -12 newTable[i] = e; //将元素放在数组上 -13 e = next; //访问下一个Entry链上的元素 -14 } while (e != null); -15 } -16 } -17 } +void transfer(Entry[] newTable) { + Entry[] src = table; //src引用了旧的Entry数组 + int newCapacity = newTable.length; + for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组 + Entry e = src[j]; //取得旧Entry数组的每个元素 + if (e != null) { + src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象) + do { + Entry next = e.next; + int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置 + e.next = newTable[i]; //标记[1] + newTable[i] = e; //将元素放在数组上 + e = next; //访问下一个Entry链上的元素 + } while (e != null); + } + } +} ``` [美团关于HashMap的讲解](https://tech.meituan.com/2016/06/24/java-hashmap.html) #### Java 1.7与1.8区别 -1.8还有三点主要的优化: - -- 数组+链表改成了数组+链表或红黑树; -- 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后; -- 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小; -- 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; +java1.8主要的优化: +1. 数组+链表改成了数组+链表或红黑树; +2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后; +3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小; +4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; 好处: - 防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn); @@ -365,11 +364,11 @@ public V get(Object key) { ``` 一个基本的LRU队列需要两点: - - 添加元素添加在队头, + - 删除过期元素 ``` void afterNodeInsertion(boolean evict) {} ``` - - 访问元素后,元素移到队尾 + - 将新增的元素或访问后的元素,移到队尾 ``` void afterNodeAccess(Node e) { ``` diff --git a/src/main/java/com/toc/ALGORITHM.md b/src/main/java/com/toc/ALGORITHM.md index 7b9a10d..251a8ce 100644 --- a/src/main/java/com/toc/ALGORITHM.md +++ b/src/main/java/com/toc/ALGORITHM.md @@ -32,53 +32,56 @@    5.2.2. 经典问题   5.3. 左右指针   5.4. 滑动窗口 - 6. 队列 -  6.1. 广度优先搜索(BFS) -  6.2. 单调队列 - 7. 栈 -  7.1. 深度优先搜索(DFS) -  7.2. 单调栈 - 8. BFS 与 DFS - 9. 递归 -  9.1. 递归三要素 -  9.2. 递归的思想 - 10. 回溯 -  10.1. 伪代码模版 -   10.1.1. 回溯三部曲 -   10.1.2. startIndex使用 -  10.2. 问题场景 -  10.3. 重复问题 -  10.4. 组合问题 -  10.5. 切割问题 -  10.6. 排列问题 -  10.7. 子集问题 -  10.8. 去重问题横向对比 - 11. 贪心 -  11.1. 指针与区间局部最优 -  11.2. 区间问题 -  11.3. 其他 - 12. 动态规划 -  12.1. 基本思想 -  12.2. 相关问题 -  12.3. 字符串问题 -   12.3.1. 字符操作 -   12.3.2. 子序列问题 -   12.3.3. 子数组问题 -   12.3.4. 回文问题 -  12.4. 股票问题 -  12.5. 背包问题 -   12.5.1. 常见求解方式及疑难点 -   12.5.2. 典型背包问题 -   12.5.3. 背包场景问题 -  12.6. 扔鸡蛋问题 - 13. 前缀树 - 14. 拓扑排序 - 15. 并查集 - 16. 二进制 - 17. 常用操作 -  17.1. 前缀和 -  17.2. 求余数常见操作 -  17.3. Kanade 算法 +   5.4.1. 指针移动缩小窗口 +   5.4.2. 指针不动窗口平滑 + 6. 队列 +  6.1. 广度优先搜索(BFS) +  6.2. 单调队列 + 7. 栈 +  7.1. 深度优先搜索(DFS) +  7.2. 单调栈 + 8. BFS 与 DFS + 9. 递归 +  9.1. 递归三要素 +  9.2. 递归的思想 + 10. 回溯 +  10.1. 伪代码模版 +   10.1.1. 回溯三部曲 +   10.1.2. startIndex使用 +  10.2. 问题场景 +  10.3. 重复问题 +  10.4. 组合问题 +  10.5. 切割问题 +  10.6. 排列问题 +  10.7. 子集问题 +  10.8. 去重问题横向对比 + 11. 贪心 +  11.1. 指针与区间局部最优 +  11.2. 区间问题 +  11.3. 其他 + 12. 动态规划 +  12.1. 基本思想 +  12.2. 相关问题 +  12.3. 字符串问题 +   12.3.1. 字符操作 +   12.3.2. 子序列问题 +   12.3.3. 子数组问题 +   12.3.4. 回文问题 +  12.4. 股票问题 +  12.5. 背包问题 +   12.5.1. 常见求解方式及疑难点 +   12.5.2. 典型背包问题 +   12.5.3. 背包场景问题 +  12.6. 扔鸡蛋问题 + 13. 前缀树 + 14. 拓扑排序 + 15. 并查集 + 16. 二进制 + 17. KMP + 18. 常用操作 +  18.1. 前缀和 +  18.2. 求余数常见操作 +  18.3. Kanade 算法 # 算法[Top] ## 链表[Top] @@ -651,7 +654,9 @@ public class Solution { - [移除元素](https://leetcode-cn.com/problems/remove-element/) : 类似于快排的变种 - [移动零](https://leetcode-cn.com/problems/move-zeroes/) - [删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/) -- [下一个排列](https://leetcode-cn.com/problems/next-permutation/): 查找的方法比较巧妙,递减区间的利用及 +- [下一个排列](https://leetcode-cn.com/problems/next-permutation/): 查找的方法比较巧妙,递减区间的利用 +- [删除有序数组中的重复项 II](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array-ii/): 快慢指针,慢指针为确定的区间 + ### 二分法[Top] [一道可以考察「二分」本质的面试题](https://mp.weixin.qq.com/s/RW20ob2oO4Bfd-PcukTVJA) > 「⼆分」的本质是⼆段性,并⾮单调性。只要⼀段满⾜某个性质,另外⼀段不满⾜某个性质,就可以⽤「⼆分」 @@ -919,12 +924,61 @@ public class BinarySearchRight { ### 滑动窗口[Top] +#### 指针移动缩小窗口[Top] + +参考资料:[我写了套框架,把滑动窗口算法变成了默写题](https://mp.weixin.qq.com/s/ioKXTMZufDECBUwRRp3zaA) + +常规算法模版: +``` +/* 滑动窗口算法框架 */ +void slidingWindow(string s, string t) { + unordered_map need, window; + for (char c : t) need[c]++; + + int left = 0, right = 0; + int valid = 0; + while (right < s.size()) { + // c 是将移入窗口的字符 + char c = s[right]; + // 右移窗口 + right++; + // 进行窗口内数据的一系列更新 + ... + + /*** debug 输出的位置 ***/ + printf("window: [%d, %d)\n", left, right); + /********************/ + + // 判断左侧窗口是否要收缩 + while (window needs shrink) { + // d 是将移出窗口的字符 + char d = s[left]; + // 左移窗口 + left++; + // 进行窗口内数据的一系列更新 + ... + } + } +} +``` + + +- [无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/): 快慢指针+hash表结合 +- [最小覆盖子串](https://leetcode-cn.com/problems/minimum-window-substring/) - [找到字符串中所有字母异位词](https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/) +- [长度最小的子数组](https://leetcode-cn.com/problems/minimum-size-subarray-sum/) + +#### 指针不动窗口平滑[Top] +滑动窗口的一种变种为,窗口保持在一定的size。 +1. 第一种为size是题目要求的,如k长的窗口最大值 +2. 第二种为size是当前满足的最优接。left位置不匹配,则平滑窗口。result即为`right-left` +- [替换后的最长重复字符](https://leetcode-cn.com/problems/longest-repeating-character-replacement/) +- [滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) -## 队列[Top] +## 队列[Top] 队列是典型的 FIFO 数据结构: - 插入(insert)操作也称作入队(enqueue),新元素始终被添加在队列的末尾。 - 删除(delete)操作也被称为出队(dequeue)。 你只能移除第一个元素。 @@ -933,7 +987,7 @@ public class BinarySearchRight { - [剑指 Offer 09. 用两个栈实现队列](https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/) -### 广度优先搜索(BFS)[Top] +### 广度优先搜索(BFS)[Top] 广度优先搜索(BFS)的一个常见应用是找出从根结点到目标结点的最短路径。 注意点: 1. 初始入队列。 @@ -996,7 +1050,7 @@ public class Solution { ``` -### 单调队列[Top] +### 单调队列[Top] 单调队列,即单调递减或单调递增的队列。 需要使用 双向队列 ,假设队列已经有若干元素: @@ -1037,8 +1091,9 @@ class MaxQueue { - [剑指 Offer 59 - II. 队列的最大值](https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/) - [剑指 Offer 59 - I. 滑动窗口的最大值](https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/): review - [环形子数组的最大和](https://leetcode-cn.com/problems/maximum-sum-circular-subarray/) +- [滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/) -## [Top] +## [Top] - 栈具有记忆的功能,由其数据的特殊性可以用来DFS搜索 - [回文链表](https://leetcode-cn.com/problems/palindrome-linked-list/) @@ -1049,7 +1104,7 @@ class MaxQueue { - [二叉树展开为链表](https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/) -### 深度优先搜索(DFS)[Top] +### 深度优先搜索(DFS)[Top] 深度优先搜索(DFS)是用于 在树/图中遍历/搜索 的另一种重要算法。也可以在更抽象的场景中使用。\ 正如树的遍历中所提到的,我们可以用 DFS 进行 前序遍历,中序遍历 和 后序遍历。在这三个遍历顺序中有一个共同的特性:除非我们到达最深的结点,否则我们永远不会回溯 。\ 这也是 DFS 和 BFS 之间最大的区别,BFS永远不会深入探索,除非它已经在当前层级访问了所有结点。\ @@ -1117,7 +1172,7 @@ public class Solution { ``` -### 单调栈[Top] +### 单调栈[Top] 单调栈: 单调栈实际上就是栈, 只是利⽤了⼀些巧妙的逻辑, 使得每次新元素⼊栈后, 栈内的元素都保持有序(单调递增或单调递减) 。 ```java class Solution { @@ -1155,7 +1210,7 @@ class Solution { -## BFS 与 DFS[Top] +## BFS 与 DFS[Top] BFS与DFS相关的问题,经常都可以用两种方式求解,因此把相关问题放一起。 - [剑指 Offer 13. 机器人的运动范围](https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/) @@ -1173,8 +1228,8 @@ BFS与DFS相关的问题,经常都可以用两种方式求解,因此把相 - [判断二分图](https://leetcode-cn.com/problems/is-graph-bipartite/): 判断方法很特别,通过节点染色 -## 递归[Top] -### 递归三要素[Top] +## 递归[Top] +### 递归三要素[Top] 每次写递归,都按照这三要素思考: 1. 确定递归函数的参数和返回值\ 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。 @@ -1183,18 +1238,18 @@ BFS与DFS相关的问题,经常都可以用两种方式求解,因此把相 3. 确定单层递归的逻辑:\ 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。 -### 递归的思想[Top] +### 递归的思想[Top] - 自顶而下:通过全局变量传递递归值 - 自底而上:带返回值的递归,依次叠加 -## 回溯[Top] +## 回溯[Top] 回溯法⼀般是在集合中递归搜索,集合的⼤⼩构成了树的宽度,递归的深度构成的树的深度。 ![image](https://github.com/rbmonster/file-storage/blob/main/learning-note/learning/basic/backTracking.png) -### 伪代码模版[Top] +### 伪代码模版[Top] ``` void backtracking(参数) { if (终⽌条件) { @@ -1209,7 +1264,7 @@ void backtracking(参数) { } ``` -#### 回溯三部曲[Top] +#### 回溯三部曲[Top] 1. 回溯函数模板返回值以及参数。回溯算法中函数返回值⼀般为void。 ``` void backtracking(参数) @@ -1230,7 +1285,7 @@ for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的 } ``` -#### startIndex使用[Top] +#### startIndex使用[Top] 需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢? 1. 如果是⼀个集合来求组合的话,就需要startIndex 2. 如果是多个集合取组合,各个集合之间相互不影响,那么就不⽤startIndex,例如:回溯算法:电话号 码的字⺟组合 @@ -1239,7 +1294,7 @@ for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的 1. 每层都是从0开始搜索⽽不是startIndex 2. 需要used数组记录path⾥都放了哪些元素了 -### 问题场景[Top] +### 问题场景[Top] 回溯算法能解决如下问题: - 组合问题:N个数⾥⾯按⼀定规则找出k个数的集合 - 排列问题:N个数按⼀定规则全排列,有⼏种排列⽅式 @@ -1247,7 +1302,7 @@ for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的 - ⼦集问题:⼀个N个数的集合⾥有多少符合条件的⼦集 - 棋盘问题:N皇后,解数独等等 -### 重复问题[Top] +### 重复问题[Top] “树枝去重”和“树层去重” 组合问题可以抽象为树形结构,那么“使⽤过”在这个树形结构上是有两个维度的,⼀个维度是同⼀树枝上“使⽤过”,⼀个维度是同⼀树层上“使⽤过”。\ @@ -1319,23 +1374,23 @@ public class Soluction { } ``` -### 组合问题[Top] +### 组合问题[Top] - [组合](https://leetcode-cn.com/problems/combinations/submissions/) - [组合总和](https://leetcode-cn.com/problems/combination-sum/) - [组合总和 II](https://leetcode-cn.com/problems/combination-sum-ii/submissions/) - [组合总和 III](https://leetcode-cn.com/problems/combination-sum-iii/) -### 切割问题[Top] +### 切割问题[Top] - [电话号码的字母组合](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) - [字符串的排列]( https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/) - [复原IP地址](https://leetcode-cn.com/problems/restore-ip-addresses/) - [分割回文串](https://leetcode-cn.com/problems/palindrome-partitioning/) -### 排列问题[Top] +### 排列问题[Top] - [全排列](https://leetcode-cn.com/problems/permutations/) - [全排列 II](https://leetcode-cn.com/problems/permutations-ii/) -### 子集问题[Top] +### 子集问题[Top] - [子集](https://leetcode-cn.com/problems/subsets/): review - [子集 II](https://leetcode-cn.com/problems/subsets-ii/): review - [递增子序列](https://leetcode-cn.com/problems/increasing-subsequences/) @@ -1347,14 +1402,14 @@ public class Soluction { - [二叉树路径](https://leetcode-cn.com/problems/binary-tree-paths/) - [重新安排行程](https://leetcode-cn.com/problems/reconstruct-itinerary/) -### 去重问题横向对比[Top] +### 去重问题横向对比[Top] - [组合总和 II](https://leetcode-cn.com/problems/combination-sum-ii/submissions/): 排序后树层去重`if(i>index && candidates[i]== candidates[i-1])` - [全排列 II](https://leetcode-cn.com/problems/permutations-ii/): 排序后树层去重```if(i>0 && nums[i] == nums[i-1] && used[i-1] == false) ``` - [子集 II](https://leetcode-cn.com/problems/subsets-ii/): 树层去重`if (i != index && nums[i] == nums[i-1])` - [递增子序列](https://leetcode-cn.com/problems/increasing-subsequences/): 无序元素树层去重`if(used.contains(nums[i]))` -## 贪心[Top] +## 贪心[Top] 贪⼼的本质是选择每⼀阶段的局部最优,从⽽达到全局最优。 > 例如,有⼀堆钞票,你可以拿⾛⼗张,如果想达到最⼤的⾦额,你要怎么拿?\ 指定每次拿最⼤的,最终结果就是拿⾛最⼤数额的钱。 @@ -1368,7 +1423,7 @@ public class Soluction { - 求解每⼀个⼦问题的最优解 - 将局部最优解堆叠成全局最优解 -### 指针与区间局部最优[Top] +### 指针与区间局部最优[Top] - [跳跃游戏](https://leetcode-cn.com/problems/jump-game/) - [跳跃游戏II](https://leetcode-cn.com/problems/jump-game-II/) - [最大子序和](https://leetcode-cn.com/problems/maximum-subarray/) @@ -1376,14 +1431,14 @@ public class Soluction { - [划分字⺟区间](https://leetcode-cn.com/problems/partition-labels/) - [摆动序列](https://leetcode-cn.com/problems/wiggle-subsequence/) -### 区间问题[Top] +### 区间问题[Top] - [⽤最少数量的箭引爆⽓球](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/) - [合并区间](https://leetcode-cn.com/problems/merge-intervals/) - [无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/) - [根据身⾼重建队列](https://leetcode-cn.com/problems/queue-reconstruction-by-height/) -### 其他[Top] +### 其他[Top] - [分配饼干](https://leetcode-cn.com/problems/assign-cookies/description/) - [单调递增的数字](https://leetcode-cn.com/problems/monotone-increasing-digits/) - [分发糖果](https://leetcode-cn.com/problems/candy/) @@ -1398,9 +1453,9 @@ public class Soluction { - [买卖股票的最佳时机II](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/) -## 动态规划[Top] +## 动态规划[Top] -### 基本思想[Top] +### 基本思想[Top] 动态规划的⼀般流程优化三步: 1. 暴⼒的递归解法 2. 带备忘录的 递归解法 @@ -1456,7 +1511,7 @@ class Solution { ``` -### 相关问题[Top] +### 相关问题[Top] - [斐波那契数](https://leetcode-cn.com/problems/fibonacci-number/) - [爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) @@ -1475,7 +1530,7 @@ class Solution { -### 字符串问题[Top] +### 字符串问题[Top] 第⼀种思路模板是⼀个⼀维的 dp 数组 第二个思路模版是建立一个二维的dp数组 @@ -1487,11 +1542,11 @@ class Solution { - `dp[i][j-1]` 4. 初始化 -#### 字符操作[Top] +#### 字符操作[Top] - [两个字符串的删除操作](https://leetcode-cn.com/problems/delete-operation-for-two-strings/) - [编辑距离](https://leetcode-cn.com/problems/edit-distance/) -#### 子序列问题[Top] +#### 子序列问题[Top] - [最长递增子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) - [最长连续递增序列](https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/) @@ -1502,12 +1557,12 @@ class Solution { - [不同的子序列](https://leetcode-cn.com/problems/distinct-subsequences/): review - [最长递增子序列的个数](https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/): review -#### 子数组问题[Top] +#### 子数组问题[Top] - [最长重复子数组](https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/): review - [最大子序和](https://leetcode-cn.com/problems/maximum-subarray/) -#### 回文问题[Top] +#### 回文问题[Top] 回文问题demo: ```java class Solution { @@ -1546,7 +1601,7 @@ class Solution { - [最长回文子序列](https://leetcode-cn.com/problems/longest-palindromic-subsequence/) -### 股票问题[Top] +### 股票问题[Top] ```java public class StockTrading { @@ -1622,8 +1677,8 @@ public class StockTrading { - [最佳买卖股票时机含冷冻期](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/) - [买卖股票的最佳时机含手续费](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) -### 背包问题[Top] -#### 常见求解方式及疑难点[Top] +### 背包问题[Top] +#### 常见求解方式及疑难点[Top] 01背包问题 ```java @@ -1776,7 +1831,7 @@ for(int i =1; i< 背包.length;i++) { ``` -#### 典型背包问题[Top] +#### 典型背包问题[Top] - [01背包问题](https://www.acwing.com/problem/content/2/) - [完全背包问题](https://www.acwing.com/problem/content/3/) - [多重背包问题 I](https://www.acwing.com/problem/content/4/) @@ -1785,7 +1840,7 @@ for(int i =1; i< 背包.length;i++) { - [二维费用的背包问题](https://www.acwing.com/problem/content/8/) -#### 背包场景问题[Top] +#### 背包场景问题[Top] 01背包: - [分割等和子集](https://leetcode-cn.com/problems/partition-equal-subset-sum/) - [最后一块石头的重量 II](https://leetcode-cn.com/problems/last-stone-weight-ii/) @@ -1801,7 +1856,7 @@ for(int i =1; i< 背包.length;i++) { - *[单词拆分](https://leetcode-cn.com/problems/word-break/) -### 扔鸡蛋问题[Top] +### 扔鸡蛋问题[Top] [鸡蛋掉落](https://leetcode-cn.com/problems/super-egg-drop/) 该问题理解的关键为:因为我们要求的是**最坏情况下扔鸡蛋的次数**,所以鸡蛋在第 i 层楼碎没碎,最后搜索的取决于那种情况的结果更⼤。 @@ -1855,7 +1910,7 @@ class Solution { -## 前缀树[Top] +## 前缀树[Top] 前缀树又名字典树,单词查找树,Trie树,是一种多路树形结构,是哈希树的变种,和hash效率有一拼,是一种用于快速检索的多叉树结构。 典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计\ @@ -1863,12 +1918,12 @@ class Solution { -## 拓扑排序[Top] +## 拓扑排序[Top] - [课程表](https://leetcode-cn.com/problems/course-schedule/) - [课程表 II](https://leetcode-cn.com/problems/course-schedule-ii/) -## 并查集[Top] +## 并查集[Top] ```java class UnionFindSet { int[] rank; @@ -1919,21 +1974,23 @@ class UnionFindSet { -## 二进制[Top] +## 二进制[Top] 计算1的个数 位运算与运算转换: \ [Pow(x, n)](https://leetcode-cn.com/problems/powx-n/) :快速幂 +## KMP[Top] +[实现 strStr()](https://leetcode-cn.com/problems/implement-strstr/) -## 常用操作[Top] -### 前缀和[Top] +## 常用操作[Top] +### 前缀和[Top] 前缀和是一种重要的预处理,能大大降低查询的时间复杂度。两个位置的前缀和差值,能快速确定这段区间的`sumup` > 相关关键词:**连续子数组** - [寻找数组的中心下标](https://leetcode-cn.com/problems/find-pivot-index/) -### 求余数常见操作[Top] +### 求余数常见操作[Top] ```java public class Solution { private int getNext(int n) { @@ -1951,7 +2008,7 @@ public class Solution { } ``` -### Kanade 算法[Top] +### Kanade 算法[Top] 对于一个给定数组 A,Kadane 算法可以用来找到 A 的最大子段和。 - [最大子序和](https://leetcode-cn.com/problems/maximum-subarray/) ``` diff --git a/src/main/java/com/toc/COLLECTION.md b/src/main/java/com/toc/COLLECTION.md index 9557a2d..3cf6a8a 100644 --- a/src/main/java/com/toc/COLLECTION.md +++ b/src/main/java/com/toc/COLLECTION.md @@ -47,11 +47,11 @@ - CopyOnWriteArrayList:使用了读写分离的思想,在写数据的时候上ReentrantLock锁并新建一个数组,读数据仍从旧数组中读取,而新数据在新增或删除完成之后直接替换旧数组。虽然线程安全,对于频繁写数据的场景效率很低。 - ListIterator: 更强大的Iterator的子类,用于各种List类的访问,并支持双向移动。 - LinkedList:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环) - - getFirst() 和element() 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. - - peek() 方法与上诉类似,只时列表为空返回null - - removeFirst() 和 remove() 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 - - poll() 同样移除并返回列表头,只是列表为空返回Null -- Stack:pop()、push()、 peek()方法,其中peek()返回栈顶元素,而不将其移除。 + - `getFirst()` 和`element()` 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. + - `peek()` 方法与上诉类似,只时列表为空返回null + -` removeFirst()` 和 `remove()` 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 + - `poll()` 同样移除并返回列表头,只是列表为空返回Null +- Stack:`pop()`、`push()`、 `peek()`方法,其中`peek()`返回栈顶元素,而不将其移除。 ### ArrayList 部分源码[Top] #### Object[]数组[Top] @@ -61,8 +61,8 @@ #### 扩容[Top] 添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。 -> - 主要一个超精度负数判断,如果经度过长,则默认使用当前长度 -> - 数据复制使用Arrays.copyOf(elementData, newCapacity); +> - 扩容操作中主要的是一个超精度负数判断,如果经度过长,则默认使用当前长度 +> - **数据复制**使用Arrays.copyOf(elementData, newCapacity); 因为是一步操作,所以用于快速失败的modCount+1 ```java @@ -109,12 +109,12 @@ private void grow(int minCapacity) { **思考:arrayList 为啥1.5倍扩容?** #### 删除元素[Top] -调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。 +调用 `System.arraycopy()` 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为`O(N)`,可以看到 ArrayList 删除元素的代价是非常高的。 > `System.arraycopy(elementData, index+1, elementData, index, numMoved);` #### 快速失败[Top] -快速失败(fail-fast) 是 Java 集合的一种错误检测机制。在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 ConcurrentModificationException 异常。 -> 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。 +快速失败(fail-fast) 是 Java 集合的一种错误检测机制。在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 `ConcurrentModificationException` 异常。 +> 在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。 modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 > 使用迭代器遍历时默认会传入当前的数组的modCount,每次操作进行检测 @@ -142,47 +142,47 @@ CopyOnWriteArrayList读写分离list 适用于读多写少的场景 ``` - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock(); - try { - Object[] elements = getArray(); - int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1); - newElements[len] = e; - setArray(newElements); - return true; - } finally { - lock.unlock(); - } +public boolean add(E e) { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + Object[] elements = getArray(); + int len = elements.length; + Object[] newElements = Arrays.copyOf(elements, len + 1); + newElements[len] = e; + setArray(newElements); + return true; + } finally { + lock.unlock(); } +} - @SuppressWarnings("unchecked") - private E get(Object[] a, int index) { - return (E) a[index]; - } +@SuppressWarnings("unchecked") +private E get(Object[] a, int index) { + return (E) a[index]; +} ``` ### LinkedList[Top] LinkedList定义了一个内部的Node 节点,基于双向链表实现,使用 Node 存储链表节点信息。 相关操作: -- getFirst() 和element() 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. -- peek() 方法与上诉类似,只时列表为空返回null -- removeFirst() 和 remove() 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 -- poll() 同样移除并返回列表头,只是列表为空返回Null -``` - private static class Node { - E item; - Node next; - Node prev; - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } +- `getFirst()` 和`element()` 完全一样,都返回第一个元素。如果为空,抛NoSuchElementException. +- `peek()` 方法与上诉类似,只时列表为空返回null +- `removeFirst()` 和 `remove()` 类似,移除并返回列表的头,只是列表为空抛出NoSuchElementException。 +- `poll()` 同样移除并返回列表头,只是列表为空返回Null +``` +private static class Node { + E item; + Node next; + Node prev; + + Node(Node prev, E element, Node next) { + this.item = element; + this.next = next; + this.prev = prev; } +} ``` @@ -202,14 +202,14 @@ LinkedList定义了一个内部的Node 节点,基于双向链表实现,使 数据结构:基础的数据节点Node 继承Map.Entry 接口实现的key-value的数据节点。 基本的存储的结构为`Key Value`的Node节点的数组 `transient Node[] table;` -threshold:临界值,当实际大小(容量*填充因子)超过临界值时,会进行扩容 - -TREEIFY_THRESHOLD:树化的最小长度8。 -> 为啥设定为8,TreeNodes占用空间是普通Nodes的两倍,建立树是需要时间及空间成本的。因此此处基于时间与空间的权衡定位8,具体可以看源码。 +相关参数: +- **threshold**:临界值,当实际大小(容量*填充因子)超过临界值时,会进行扩容 +- **UNTREEIFY_THRESHOLD**:树变成链表的阀值6。 +- **MIN_TREEIFY_CAPACITY**:hashMap进行树化的最低条件table的大小达到64,否则只是进行扩容。 +- **TREEIFY_THRESHOLD**:树化的最小长度8。 + > 为啥设定为8,TreeNodes占用空间是普通Nodes的两倍,建立树是需要时间及空间成本的。因此此处基于时间与空间的权衡定位8,具体可以看源码。 -UNTREEIFY_THRESHOLD:树变成链表的阀值6。 -MIN_TREEIFY_CAPACITY:hashMap进行树化的最低条件table的大小达到64,否则只是进行扩容。 Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; ``` @@ -222,8 +222,8 @@ Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; ``` 负载因子:hashMap的负载因子默认为0.75,当hashMap中的元素达到 3/4就进行元素的扩容。 -> 负载因子大小的关系,若负载因子为1,那么在出现大量的hash膨胀的情况下,元素会较密集,并且都是用链表或者红黑树的方式连接,导致查询效率较低。 -> 若负载因子为0.5 那么就会造成空间的浪费,元素分布较为稀疏。 +> 负载因子与大小的关系?\ +> 负载因子大小的关系,若负载因子为1,那么在出现大量的hash膨胀的情况下,元素会较密集,并且都是用链表或者红黑树的方式连接,导致查询效率较低。 若负载因子为0.5 那么就会造成空间的浪费,元素分布较为稀疏。 #### put 操作[Top] 1. 首先判断table是否需要扩容,若需要进行扩容操作 @@ -241,8 +241,7 @@ Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; 3. 出现哈希冲突的情况,由于每次扩容的大小默认为2的n次方,因此重散列的位置只会为当前位置或者当前位置+旧数组大小两个位置。 4. 如果节点存在哈希冲突,则根据位运算计算最新的位置是否为0,为0表示无需移动节点。为1表示移动到oldCap+j的位置。 5. 针对出现红黑树的哈希冲突,同理。此处针对红黑树冲突的需要判断重散列的节点是否需要重新建立红黑树。 - -- 如果初始化容量大小不为2的幂次方,那么在初始化的时候,会计算threshold为大于初始化数的最近2的幂次方数,在实际使用的时候声明为table的大小。 +> 如果初始化容量大小不为2的幂次方,那么在初始化的时候,会计算threshold为大于初始化数的最近2的幂次方数,在实际使用的时候声明为table的大小。 #### HashMap红黑树查找[Top] @@ -293,15 +292,15 @@ Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30; 针对建立红黑树或者添加树节点,若使用equal及class的compare 均无法确定添加节点的方向。 则使用对象的类名进行判断,若类名依然相同,则使用System根据对象地址换算的hashcode编码判断添加方向。 ``` - static int tieBreakOrder(Object a, Object b) { - int d; - if (a == null || b == null || - (d = a.getClass().getName(). - compareTo(b.getClass().getName())) == 0) - d = (System.identityHashCode(a) <= System.identityHashCode(b) ? - -1 : 1); - return d; - } +static int tieBreakOrder(Object a, Object b) { + int d; + if (a == null || b == null || + (d = a.getClass().getName(). + compareTo(b.getClass().getName())) == 0) + d = (System.identityHashCode(a) <= System.identityHashCode(b) ? + -1 : 1); + return d; +} ``` #### hash 方法[Top] @@ -315,9 +314,10 @@ static final int hash(Object key) { } ``` -问题:为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode? -1. 上述解释的点,低位与高位混合,加大hash的随机性。 -2. key的hashcode可能被重写,重写的hash函数冲突的概率无法保证。因此hashMap需要在此基础使用自己的hash加大随机性。 +> 问题:为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode? +> 1. 上述解释的点,低位与高位混合,加大hash的随机性。 +> 2. key的hashcode可能被重写,重写的hash函数冲突的概率无法保证。因此hashMap需要在此基础使用自己的hash加大随机性。 + #### Java1.7并发下循环链表[Top] Java1.7 中HashMap扩容是使用类似**头插法**的方式把旧节点转移到新的数组上。假设节点出现哈希冲突以链表的方式连接,且头节点1和节点2 扩容的位置仍然不变。 1. 当线程1与线程2新建完新数组,并且执行到上述链表节点的扩容,执行旧数组的头结点3。举个例子链表为 3->7 @@ -326,35 +326,34 @@ Java1.7 中HashMap扩容是使用类似**头插法**的方式把旧节点转移 4. 当前数组节点的链表顺序为 7->3,重新进行节点3的头插,就会导致一个循环链表的现象 ``` - 1 void transfer(Entry[] newTable) { - 2 Entry[] src = table; //src引用了旧的Entry数组 - 3 int newCapacity = newTable.length; - 4 for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组 - 5 Entry e = src[j]; //取得旧Entry数组的每个元素 - 6 if (e != null) { - 7 src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象) - 8 do { - 9 Entry next = e.next; -10 int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置 -11 e.next = newTable[i]; //标记[1] -12 newTable[i] = e; //将元素放在数组上 -13 e = next; //访问下一个Entry链上的元素 -14 } while (e != null); -15 } -16 } -17 } +void transfer(Entry[] newTable) { + Entry[] src = table; //src引用了旧的Entry数组 + int newCapacity = newTable.length; + for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组 + Entry e = src[j]; //取得旧Entry数组的每个元素 + if (e != null) { + src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象) + do { + Entry next = e.next; + int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置 + e.next = newTable[i]; //标记[1] + newTable[i] = e; //将元素放在数组上 + e = next; //访问下一个Entry链上的元素 + } while (e != null); + } + } +} ``` [美团关于HashMap的讲解](https://tech.meituan.com/2016/06/24/java-hashmap.html) #### Java 1.7与1.8区别[Top] -1.8还有三点主要的优化: - -- 数组+链表改成了数组+链表或红黑树; -- 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后; -- 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小; -- 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; +java1.8主要的优化: +1. 数组+链表改成了数组+链表或红黑树; +2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后; +3. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小; +4. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; 好处: - 防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn); @@ -410,7 +409,7 @@ public V get(Object key) { ``` - 将新增的元素或访问后的元素,移到队尾 ``` - void afterNodeAccess(Node e) {} + void afterNodeAccess(Node e) { ``` 因此固定大小的LRU可以像这样构建: @@ -627,4 +626,4 @@ public class Arrays{ 3. 再哈希法。 > 这种方法是同时构造多个不同的哈希函数:Hi=RH1(key) i=1,2,…,k。当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。 -- [Hash冲突的四种解决办法](https://www.cnblogs.com/gongcheng-/p/10894205.html#_label1_0) +- [Hash冲突的四种解决办法](https://www.cnblogs.com/gongcheng-/p/10894205.html#_label1_0) \ No newline at end of file