补充了跳表的逻辑

This commit is contained in:
estom
2025-07-13 07:20:03 -04:00
parent ee4ad3488c
commit 9ed4820cd3
17 changed files with 460 additions and 70 deletions

51
TODO.md
View File

@@ -1,51 +0,0 @@
## 技术锤炼
目标注册中心、RPC、消息队列专家。
认识程度:入门教程、精通使用、阅读源码
### 教程
- [X] js/ts/vue的快速入门
- [ ] jdbc&mybatis
- [ ] SpringMVC。产出思维导图。时间5.27
- [ ] SpringCloud -> 笔记+标准工程
- [ ] SpringCloudAlibaba -> 笔记+标准工程
- [ ] vim/idea最佳实践 -> 思维导图
- [ ] 底层探索kubeflow
- [ ] Pipelines与argo企业级流水线方案。
- [ ] kserv与torchserv模型部署
- [ ] huggingface系列——evaluate学习
- [ ] huggingface系列——transformer学习
- [ ] 上层构建langchain
- [ ] langchain教程,阅读教程、精通使用、阅读源码
- [ ] langflow教程阅读教程、精通使用、阅读源码
- [ ] antflow教程
- [ ] reg实现方案
### 书籍
- [ ] nacos平台使用手册和设计原理
- [ ] 微服务架构设计。产出:相关博客
- [ ] jvm原理书本阅读。产出相关博客。时间5.20
- [ ] 微服务架构设计
- [ ] 数据密集型系统设计
### 源码
- [ ] 开源nacos/client源码阅读。产-> LiteRegistry
- [ ] sofa-rpc源码阅读和框架分析。-> LiteRpc
- [ ] Netty源码阅读。产出LiteNetty和博客。时间6.2
- [ ] jdk源码。产出博客。时间5.13
- [ ] springboot源码阅读
- [ ] springcloud源码阅读
- [ ] 消息队列源码阅读rabbit
- [ ] 数据库源码阅读redis
- [ ] langchain源码阅读
### 博客
- [ ] Java问题排查和性能分析系列博客
- [ ] JDK源码阅读系列博客
### 项目
- [X] 知识库项目完成。-> docsify知识库
- [X] 博客项目完成。-> 吸收归纳新博客系统
- [X] 一汽项目
- [ ] 整理微服务的产品文档用于面试
### 考试
- [x] 软考
- [ ] 阿里云技术认证

View File

@@ -1,13 +0,0 @@
Serving /root/gitee/notes now.
Listening at http://localhost:80
TypeError: Cannot read properties of undefined (reading 'split')
at livereload (/root/.nvm/versions/node/v20.10.0/lib/node_modules/docsify-cli/node_modules/connect-livereload/index.js:95:41)
at call (/root/.nvm/versions/node/v20.10.0/lib/node_modules/docsify-cli/node_modules/connect/index.js:239:7)
at next (/root/.nvm/versions/node/v20.10.0/lib/node_modules/docsify-cli/node_modules/connect/index.js:183:5)
at Function.handle (/root/.nvm/versions/node/v20.10.0/lib/node_modules/docsify-cli/node_modules/connect/index.js:186:3)
at Server.app (/root/.nvm/versions/node/v20.10.0/lib/node_modules/docsify-cli/node_modules/connect/index.js:51:37)
at Server.emit (node:events:514:28)
at parserOnIncoming (node:_http_server:1143:12)
at HTTPParser.parserOnHeadersComplete (node:_http_common:119:17)

View File

@@ -1,5 +1,7 @@
# 跳表
## 跳表的基本概念
> 经典实现Redis 的 Sorted Set、JDK 的 ConcurrentSkipListMap 和 ConcurrentSkipListSet 都是基于跳表实现。
只需要对链表稍加改造,就可以支持类似“二分”的查找算法。我们把改造之后的数据结构叫作**跳表**Skip list
@@ -10,4 +12,233 @@
在一个具有多级索引的跳表中,第一级索引的结点个数大约就是 n/2第二级索引的结点个数大约就是 n/4第三级索引的结点个数大约就是 n/8依次类推也就是说第 k 级索引的结点个数是第 k-1 级索引的结点个数的 1/2那第 k 级索引结点的个数就是 $n/(2^k)$
## 参考资料
## 跳表的原理
### 跳表的结构
![跳表结构](image/2025-07-13T07:13:21.436Z.png)
跳表的特点,可以概括如下。根据以下原则构建跳表。
* 跳表是多层level链表结构跳表中的每一层都是一个有序链表并且按照元素升序默认排列
* 跳表中的元素会在哪一层出现是随机决定的但是只要元素出现在了第k层那么k层以下的链表也会出现这个元素
* 跳表的底层的链表包含所有元素;
* 跳表头节点和尾节点不存储元素,且头节点和尾节点的层数就是跳表的最大层数;
* 跳表中的节点包含两个指针,一个指针指向同层链表的后一节点,一个指针指向下层链表的同元素节点。
### 跳表中如何查找
![alt text](image/2025-07-13T08:18:19.837Z.png)
从顶层链表的头节点开始查找查找到元素71的节点时一共遍历了4个节点但是如果按照传统链表的方式即从跳表的底层链表的头节点开始向后查找那么就需要遍历7个节点所以跳表以空间换时间缩短了操作跳表所需要花费的时间。
## 构建一个跳表
### 跳表中的节点
已知跳表中的节点,需要有指向当前层链表后一节点的指针,和指向下层链表的同元素节点的指针,所以跳表中的节点,定义如下。
```java
class Node {
public Integer value;
public Node next;
public Node down;
public Node(int val) {
this.value = val;
}
@Override
public String toString() {
return String.valueOf(this.value);
}
}
```
跳表中的节点的最简单的定义方式存储的元素data为整数节点之间进行比较时直接比较元素data的大小。
### 跳表的初始化
跳表初始化时,将每一层链表的头尾节点创建出来并使用集合将头尾节点进行存储,头尾节点的层数随机指定,且头尾节点的层数就代表当前跳表的层数。初始化后,跳表结构如下所示。
![跳表的初始化](image/2025-07-13T10:49:29.844Z.png)
```java
public SkipList() {
// 初始化节点,只有一层
Node head = new Node(-1);
Node end = new Node(Integer.MAX_VALUE);
head.next = end;
curHead = head;
}
// 网页中给的方案过于复杂,已经优化
```
直接使用最小值节点和最大值节点作为头尾节点,可以兼容比较操作中的逻辑,不需要判断是否为空节点。
### 跳表的查找方法
在跳表中搜索一个元素时,需要从顶层开始,逐层向下搜索。搜索时遵循如下规则。
1. 目标值大于后一节点值时,继续在本层向后搜索;
2. 目标值小于后一节点值时,向下移动一层,从下层链表的同节点位置向后搜索;
3. 目标值等于当前节点值,搜索结束。
下边是一个搜索示意图
![跳表的搜索](image/2025-07-13T10:54:56.750Z.png)
搜索的代码如下
```java
public Node find(int val) {
Node point = curHead;
while (point != null) {
if (point.next.value == val) {
return point.next;
}
if (point.next.value < val) {
point = point.next;
} else {
point = point.down;
}
}
// 没有找到节点
return null;
}
```
### 跳表的插入方法
每一个元素添加到跳表中时,首先需要随机指定这个元素在跳表中的层数,如果随机指定的层数大于了跳表的层数,则在将元素添加到跳表中之前,还需要扩大跳表的层数,而扩大跳表的层数就是将头尾节点的层数扩大。下面给出需要扩大跳表层数的一次添加的过程。
初始状态时跳表的层数为2如下图所示。
![初始状态](image/2025-07-13T10:58:12.353Z.png)
现在要往跳表中添加元素120并且随机指定的层数为3大于了当前跳表的层数2此时需要先扩大跳表的层数如下图所示。
![插入新的节点和层数](image/2025-07-13T10:58:34.986Z.png)
将元素120插入到跳表中时从顶层开始逐层向下插入如下图所示。
![逐层向下插入](image/2025-07-13T10:59:03.979Z.png)
```java
public void insert(int val) {
int level = getLevel();
expandLevel(level);
Node point = curHead;
// 定位要插入的level
for (int i = 0; i < curLevel - level; i++) {
point = curHead.down;
}
// 逐层插入
Node upNode = null;
while(point != null){
// 创建节点
Node newNode = new Node(val);
// 定位要插入的元素
while(point.next.value < val){
point = point.next;
}
newNode.next = point.next;
point.next = newNode;
if (upNode !=null) {
upNode.down = newNode;
}
upNode = newNode;
point = point.down;
}
}
public void expandLevel(int level) {
while (curLevel < level) {
Node newHead = new Node(-1);
Node newEnd = new Node(Integer.MAX_VALUE);
newHead.next = newEnd;
Node curEnd = curHead.next;
while (curEnd.next != null) {
curEnd = curEnd.next;
}
newHead.down = curHead;
newEnd.down = curEnd;
curHead = newHead;
curLevel += 1;
}
}
private int getLevel() {
int level = 0;
while (random.nextDouble() < ratio && level <= MAX_LEVEL) {
level += 1;
}
return level;
}
```
### 跳表的删除
当在跳表中需要删除某一个元素时,则需要将这个元素在所有层的节点都删除,具体的删除规则如下所示。
* 首先按照跳表的搜索的方式,搜索待删除节点,如果能够搜索到,此时搜索到的待删除节点位于该节点层数的最高层;
* 从待删除节点的最高层往下,将每一层的待删除节点都删除掉,删除方式就是让待删除节点的前一节点直接指向待删除节点的后一节点。
下图是一个删除过程的示意图。
![删除过程示意图](image/2025-07-13T11:08:07.984Z.png)
```java
public void delete(int val) {
// 查找元素节点
Node point = curHead;
while (point != null) {
if (point.next.value == val) {
break;
}
if (point.next.value < val) {
point = point.next;
} else {
point = point.down;
}
}
if (point == null) {
return;
}
// 删除下一个元素
while (point != null) {
// 找到当前层的目标节点
while (point.next.value < val) {
point = point.next;
}
point.next = point.next.next;
point = point.down;
}
}
```
### 总结
主要的优化点:
1. 提那家后置节点,存放最大值,省去大量的判空逻辑,依赖正常的比较逻辑即可判断是否应该结束,大大简化了代码
2. 延迟初始化跳表。只有用到某一层的时候才会进行初始化,优化了存储空间。
3. 一个节点如果层数较高会冗余存储多分。当然在java中value本身也是个索引不会占用大量的空间可以优化一下一个值仅创建一个Node每个Node中存储每层的索引。应该会更节约空间一些[避免冗余创建Node的方案](https://zhuanlan.zhihu.com/p/68516038)

View File

@@ -0,0 +1,180 @@
import java.util.Random;
class Node {
public Integer value;
public Node next;
public Node down;
public Node(int val) {
this.value = val;
}
@Override
public String toString() {
return String.valueOf(this.value);
}
}
class SkipList {
private Integer MAX_LEVEL = 10;
private Node curHead;
private Integer curLevel = 0;
private Random random = new Random();
private double ratio = 0.5;
// 做了一层优化,层数不需要预先创建好,而是在插入过程中动态扩展
public SkipList() {
// 初始化节点,只有一层
Node head = new Node(-1);
Node end = new Node(Integer.MAX_VALUE);
head.next = end;
curHead = head;
}
public Node find(int val) {
Node point = curHead;
while (point != null) {
if (point.next.value == val) {
return point.next;
}
if (point.next.value < val) {
point = point.next;
} else {
point = point.down;
}
}
// 没有找到节点
return null;
}
public void insert(int val) {
int level = getLevel();
expandLevel(level);
Node point = curHead;
// 定位要插入的level
for (int i = 0; i < curLevel - level; i++) {
point = curHead.down;
}
// 逐层插入
Node upNode = null;
while(point != null){
// 创建节点
Node newNode = new Node(val);
// 定位要插入的元素
while(point.next.value < val){
point = point.next;
}
newNode.next = point.next;
point.next = newNode;
if (upNode !=null) {
upNode.down = newNode;
}
upNode = newNode;
point = point.down;
}
}
public void expandLevel(int level) {
while (curLevel < level) {
Node newHead = new Node(-1);
Node newEnd = new Node(Integer.MAX_VALUE);
newHead.next = newEnd;
Node curEnd = curHead.next;
while (curEnd.next != null) {
curEnd = curEnd.next;
}
newHead.down = curHead;
newEnd.down = curEnd;
curHead = newHead;
curLevel += 1;
}
}
public void delete(int val) {
// 查找元素节点
Node point = curHead;
while (point != null) {
if (point.next.value == val) {
break;
}
if (point.next.value < val) {
point = point.next;
} else {
point = point.down;
}
}
if (point == null) {
return;
}
// 删除下一个元素
while (point != null) {
// 找到当前层的目标节点
while (point.next.value < val) {
point = point.next;
}
point.next = point.next.next;
point = point.down;
}
}
private int getLevel() {
int level = 0;
while (random.nextDouble() < ratio && level <= MAX_LEVEL) {
level += 1;
}
return level;
}
public void printSkipList(){
Node level = curHead;
int i = curLevel;
while (level != null) {
Node point = level.next;
System.out.print("level " + i + ":");
while(point != null){
System.out.print(point + ",");
point = point.next;
}
System.out.println("");
i--;
level = level.down;
}
}
}
public class SkipListSolution{
public static void main(String[] args) {
SkipList skipList = new SkipList();
// 创建skiplist
int[] testVal = {1,8,4,6,7,3,2};
for (int i = 0; i < testVal.length; i++) {
skipList.insert(testVal[i]);
}
skipList.printSkipList();
// 查找元素
Node result = skipList.find(3);
System.out.println(result);
result = skipList.find(11);
System.out.println(result);
skipList.delete(7);
skipList.printSkipList();
skipList.delete(11);
skipList.printSkipList();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -154,7 +154,7 @@
* 顺序查找Sequential Search
* 暴力字符串匹配Naive String Match
## 5.3 分治法Divide and Conquer Paradigm
## 5.2 分治法Divide and Conquer Paradigm
* 分治法Divide-and-Conquer即 "分而治之",是将原问题划分成 n 个规模较小而结构与原问题相似的子问题,递归地解决这些问题,然后再合并其结果,以得到原问题的解。
* 当我们遇到一个大问题时,总是习惯把问题的规模变小,这样便于分析讨论。这些规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的,本质上它还是同一个问题,规模变小后的问题其实是原问题的子问题。

View File

@@ -294,7 +294,29 @@ int main()
* 下图为二叉树查找和顺序查找以及二分查找性能的对比图:
![](image/查找算法-二叉树与二分法.png)
## 5.2 平衡查找树之2-3查找树2-3 Tree
## 5.2 平衡查找树之二叉树
### 基本思想
平衡二叉查找树在在二叉查找树的基础上引入了平衡策略使得平衡因子的绝对值小于等于1。
* AVL树能防止二叉树偏斜控制二叉搜索树的高度。高度为h的二叉搜索树中的所有操作所花费的时间是O(h)。
* 如果普通二叉搜索树变得偏斜(即最坏的情况)它可以扩展到O(n)。
* 通过将该高度限制为log nAVL树将每个操作的上限强加为O(log n)其中n是节点的数量
### 复杂性
| 算法 | 平均情况 | 最坏情况 |
|----|----------|----------|
| 空间 | o(n) | o(n) |
| 搜索 | o(log n) | o(log n) |
| 插入 | o(log n) | o(log n) |
| 删除 | o(log n) | o(log n) |
## 5.3 平衡查找树之2-3查找树2-3 Tree
### 基本思想
* 2-3查找树定义和二叉树不一样2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node)他保存1个key和左右两个子节点。对应3节点(3-node)保存两个Key2-3查找树的定义如下
@@ -320,7 +342,7 @@ int main()
对于插入来说只需要常数次操作即可完成因为他只需要修改与该节点关联的节点即可不需要检查其他节点所以效率和查找类似。下面是2-3查找树的效率
![](image/查找算法-2-3树效率.png)
## 5.3 平衡查找树之红黑树Red-Black Tree
## 5.4 平衡查找树之红黑树Red-Black Tree
* 2-3查找树能保证在插入元素之后能保持树的平衡状态最坏情况下即所有的子节点都是2-node树的高度为lgn从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂于是就有了一种简单实现2-3树的数据结构即红黑树Red-Black Tree
### 基本思想
@@ -351,7 +373,7 @@ int main()
![](image/查找算法-红黑树效率.png)
## 5.4 B树和B+树B Tree/B+ Tree
## 5.5 B树和B+树B Tree/B+ Tree
### 基本思想
* B树定义B树可以看作是对2-3查找树的一种扩展即他允许每个节点有M-1个子节点。B树的插入及平衡化操作和2-3树很相似。
@@ -382,6 +404,20 @@ B+树定义:
* 但是B树也有优点
* 由于B树的每一个节点都包含key和value因此经常访问的元素可能离根节点更近因此访问也更迅速。
## 5.6 跳表查找
### 基本思想
跳表全称为跳跃列表它允许快速查询插入和删除一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集(见右边的示意图)。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。
![alt text](image/2025-07-13T07:06:29.730Z.png)
### 时间复杂度
如果一个链表有 n 个结点,如果每两个结点抽取出一个结点建立索引的话,那么第一级索引的结点数大约就是 n/2第二级索引的结点数大约为 n/4以此类推第 m 级索引的节点数大约为 n/(2^m)。
假如一共有 m 级索引,第 m 级的结点数为两个,通过上边我们找到的规律,那么得出 n/(2^m)=2从而求得 m=log(n)-1。如果加上原始链表那么整个跳表的高度就是 log(n)。我们在查询跳表的时候,如果每一层都需要遍历 k 个结点,那么最终的时间复杂度就为 O(k*log(n))。
## 6 分块查找
> 分块查找又称索引顺序查找,它是顺序查找的一种改进方法。

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

View File

@@ -12,4 +12,4 @@
## 命名说明
* 以某一类算法的名称明明,而不是某一个算法明明。收集找到该类别下的相关算法。
* 以某一类算法的名称命名,而不是某一个算法命名。收集找到该类别下的相关算法。

View File

@@ -0,0 +1,7 @@
最近最少使用算法LRU
参考
[text](<../../C++/典型实现/6 LRU算法.md>)