0%

下面是一个基本的堆排序的Java代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.util.Arrays;

public class HeapSort {

public static void heapSort(int[] arr) {
int n = arr.length;

// 构建最大堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}

// 一个一个地从堆中取出元素,放到数组末尾
for (int i = n - 1; i > 0; i--) {
// 将当前根节点(最大值)与最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;

// 对交换后的堆重新进行堆化
heapify(arr, i, 0);
}
}

private static void heapify(int[] arr, int n, int root) {
int largest = root;
int left = 2 * root + 1;
int right = 2 * root + 2;

if (left < n && arr[left] > arr[largest]) {
largest = left;
}

if (right < n && arr[right] > arr[largest]) {
largest = right;
}

if (largest != root) {
// 交换根节点和最大值节点
int temp = arr[root];
arr[root] = arr[largest];
arr[largest] = temp;

// 对被交换的子树进行堆化
heapify(arr, n, largest);
}
}

public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6, 7};
System.out.println("Original array: " + Arrays.toString(arr));

heapSort(arr);

System.out.println("Sorted array: " + Arrays.toString(arr));
}
}

在这个示例中,heapSort 方法使用堆排序算法对整数数组进行排序。首先,构建一个最大堆,然后将根节点(最大值)与最后一个节点交换,然后再重新堆化剩余的部分。重复这个过程,直到整个数组有序。

基数排序(Radix Sort)是一种非比较性的排序算法,它将数据按照位数逐个分配到不同的桶中,从而实现排序。以下是一个基数排序的Java代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.Arrays;

public class RadixSort {

public static void radixSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}

int max = Arrays.stream(arr).max().getAsInt();
int exp = 1;

while (max / exp > 0) {
countingSortByDigit(arr, exp);
exp *= 10;
}
}

private static void countingSortByDigit(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10];

Arrays.fill(count, 0);

for (int i = 0; i < n; i++) {
int digit = (arr[i] / exp) % 10;
count[digit]++;
}

for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}

for (int i = n - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}

System.arraycopy(output, 0, arr, 0, n);
}

public static void main(String[] args) {
int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};
System.out.println("Original array: " + Arrays.toString(arr));

radixSort(arr);

System.out.println("Sorted array: " + Arrays.toString(arr));
}
}

在这个示例中,radixSort 方法使用基数排序算法来对整数数组进行排序。首先,找到数组中的最大值以确定需要进行几轮排序(根据位数)。然后,通过 countingSortByDigit 方法对数组按照当前位上的数字进行计数排序。最后,将排序后的结果拷贝回原始数组。

MD5 和SHA-1 是目前使用比较广泛的散列(Hash)函数,也是在消息认证和数字签名中普遍使用的两种加密算法。本文基于AVR 高速嵌入式单片机,实现了MD5和SHA-1 两种加密算法的比较,并对算法进行了汇编语言的优化和改进。根据实验结果,对两种算法的优缺点进行了比较和分析。 0 引言 随着信息技术和Internet 的迅速发展,信息安全和可靠性问题越来越重要。现在信息安全面临两大基本攻击:被动式攻击(获取消息的内容、业务流分析)和主动攻击(假冒、消息的篡改、业务拒绝)。前者主要靠加密和解密技术进行有效处理,而后者就要靠消息认证来处理。在金融交易、电子商务、电子信件、手机用户信息的确认等领域,数据完整性确认和数据来源的真伪鉴定都是很重要的安全服务。实现这些安全服务的最好方法就是使用加密函数中的单项散列(Hash)函数。单项散列(Hash)函数是一种单项密码体制,它是一个从明文到密文的不可逆函数,也就是说,是无法解密的。通常应用在只需要加密、不需要解密的特殊应用场合。单项散列(Hash)函数H(M)作用于一任意长度的消息M,它返回一固定长度的散列值h:h=H(M)作为初始消息的独一无二的“数字指纹”,从而能保证数据的完整性和惟一性。 3.1 MD5 与SHA-1 的比较 由于MD5 与SHA-1均是从MD4 发展而来,它们的结构和强度等特性有很多相似之处,表(1)是对MD5 与SHA-1 的结构比较。SHA-1与MD5 的最大区别在于其摘要比MD5 摘要长 32 比特。对于强行攻击,产生任何一个报文使之摘要等于给定报文摘要的难度:MD5 是2128 数量级的操作,SHA-1 是2160 数量级的操作。产生具有相同摘要的两个报文的难度:MD5是 264 是数量级的操作,SHA-1 是280 数量级的操作。因而,SHA-1 对强行攻击的强度更大。但由于SHA-1 的循环步骤比MD5 多(80:64)且要处理的缓存大(160 比特:128 比特),SHA-1 的运行速度比MD5 慢。 5 结束语 MD5 和SHA-1 是单项散列函数的典型代表,它们广泛地应用在信息安全和数字签名等各个领域。从而有效地抗击了信息的主动式攻击,本文基于AVR 单片机实现了这两种算法,并结合汇编语言尽心了优化,取得了较好的效果。根据信息安全的要求的不同层次可以灵活选择这两种算法从而达到实际目的。

显示关于redis服务器的状态报告和统计数值

    redis> INFO
    # Server
    redis_version:2.9.11 //服务器版本
    redis_git_sha1:937384d0
    redis_git_dirty:0
    redis_build_id:8e9509442863f22
    redis_mode:standalone  //mode
    os:Linux 3.13.0-35-generic x86_64
    arch_bits:64
    multiplexing_api:epoll //事件处理机制
    gcc_version:4.8.2
    process_id:4716 //进程ID
    run_id:26186aac3f2380aaee9eef21cc50aecd542d97dc  //随机标识符(用于 Sentinel 和集群)
    tcp_port:6379   //监听端口号
    uptime_in_seconds:362  //启动以来,经过的秒数
    uptime_in_days:0  //启动以来,经过的天数
    hz:10  //serverCron运行频率,此值越大表示redis对"间歇性task"的执行次数越频繁(次数/秒)
    lru_clock:1725349  //以分钟为单位进行自增的时钟,用于 LRU 管理
    config_file:
    
    # Clients
    connected_clients:1  //已连接客户端的数量(不包括通过从属服务器连接的客户端)
    client_longest_output_list:0  //当前连接的客户端当中,最长的输出列表
    client_biggest_input_buf:0  //当前连接的客户端当中,最大输入缓存
    blocked_clients:0 //正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量
    
    # Memory
    used_memory:508536  //由 Redis 分配器分配的内存总量,以字节(byte)为单位
    used_memory_human:496.62K  //以人类可读的格式返回 Redis 分配的内存总量
    used_memory_rss:7974912  //从操作系统的角度,返回 Redis 已分配的内存总量(俗称常驻集大小)。这个值和 top 、 ps 等命令的输出一致。
    //理想情况下, used_memory_rss 的值应该只比 used_memory 稍微高一点儿  
    //当 rss > used ,且两者的值相差较大时,表示存在(内部或外部的)内存碎片
    //当 used > rss 时,表示 Redis 的部分内存被操作系统换出到交换空间了,在这种情况下,操作可能会产生明显的延迟。
    used_memory_peak:508536 //Redis 的内存消耗峰值(以字节为单位)
    used_memory_peak_human:496.62K //以人类可读的格式返回 Redis 的内存消耗峰值
    used_memory_lua:33792  // Lua 引擎所使用的内存大小(以字节为单位)
    mem_fragmentation_ratio:15.68  //used_memory_rss 和 used_memory 之间的比率
    mem_allocator:jemalloc-3.2.0  //在编译时指定的, Redis 所使用的内存分配器。可以是 libc 、 jemalloc 或者 tcmalloc
    
    # Persistence
    loading:0  //一个标志值,记录了服务器是否正在载入持久化文件  
    rdb_changes_since_last_save:6 //距离最近一次成功创建持久化文件之后,经过了多少秒
    rdb_bgsave_in_progress:0 //一个标志值,记录了服务器是否正在创建 RDB 文件
    rdb_last_save_time:1411011131 //最近一次成功创建 RDB 文件的 UNIX 时间戳
    rdb_last_bgsave_status:ok //一个标志值,记录了最近一次创建 RDB 文件的结果是成功还是失败
    rdb_last_bgsave_time_sec:-1 //记录了最近一次创建 RDB 文件耗费的秒数
    rdb_current_bgsave_time_sec:-1 // 如果服务器正在创建 RDB 文件,那么这个域记录的就是当前的创建操作已经耗费的秒
    aof_enabled:0 //标志值,记录了 AOF 是否处于打开状态。
      //如果 AOF 持久化功能处于开启状态,那么这个部分还会加上以下域:    
      // aof_current_size : AOF 文件目前的大小。
      // aof_base_size : 服务器启动时或者 AOF 重写最近一次执行之后,AOF 文件的大小。
      // aof_pending_rewrite : 一个标志值,记录了是否有 AOF 重写操作在等待 RDB 文件创建完毕之后执行。
      // aof_buffer_length : AOF 缓冲区的大小。
      // aof_rewrite_buffer_length : AOF 重写缓冲区的大小。
      // aof_pending_bio_fsync : 后台 I/O 队列里面,等待执行的 fsync 调用数量。
      // aof_delayed_fsync : 被延迟的 fsync 调用数量。
    aof_rewrite_in_progress:0 //一个标志值,记录了服务器是否正在创建 AOF 文件。
    aof_rewrite_scheduled:0 //一个标志值,记录了在 RDB 文件创建完毕之后,是否需要执行预约的 AOF 重写操作
    aof_last_rewrite_time_sec:-1 //最近一次创建 AOF 文件耗费的时长。
    aof_current_rewrite_time_sec:-1 //如果服务器正在创建 AOF 文件,那么这个域记录的就是当前的创建操作已经耗费的秒
    aof_last_bgrewrite_status:ok //一个标志值,记录了最近一次创建 AOF 文件的结果是成功还是失败。
    aof_last_write_status:ok //
    
    # Stats
    total_connections_received:2 //服务器已接受的连接请求数量
    total_commands_processed:4 //服务器已执行的命令数量
    instantaneous_ops_per_sec:0 //服务器每秒钟执行的命令数量
    rejected_connections:0 //因为最大客户端数量限制而被拒绝的连接请求数量
    sync_full:0 
    sync_partial_ok:0 
    sync_partial_err:0
    expired_keys:0 //因为过期而被自动删除的数据库键数量
    evicted_keys:0 //因为最大内存容量限制而被驱逐(evict)的键数量。
    keyspace_hits:0 //查找数据库键成功的次数。
    keyspace_misses:0 //查找数据库键失败的次数。
    pubsub_channels:0 //目前被订阅的频道数量。
    pubsub_patterns:0 //目前被订阅的模式数量。
    latest_fork_usec:0 //最近一次 fork() 操作耗费的毫秒数。
    migrate_cached_sockets:0
    
    # Replication
    role:master //如果当前服务器没有在复制任何其他服务器,那么这个域的值就是 master ;否则的话,这个域的值就是 slave 。注意,在创建复制链的时候,一个从服务器也可能是另一个服务器的主服务器。
    connected_slaves:0
    master_repl_offset:0
    repl_backlog_active:0
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:0
    repl_backlog_histlen:0
    
    # CPU
    used_cpu_sys:0.21  // Redis 服务器耗费的系统 CPU 。
    used_cpu_user:0.17  // Redis 服务器耗费的用户 CPU 。
    used_cpu_sys_children:0.00 //后台进程耗费的系统 CPU 。
    used_cpu_user_children:0.00 //后台进程耗费的用户 CPU 。
    
    # Cluster
    cluster_enabled:0  //一个标志值,记录集群功能是否已经开启
    
    # Keyspace
    db0:keys=2,expires=0,avg_ttl=0 //部分记录了数据库相关的统计信息,比如数据库的键数量、数据库已经被删除的过期键数量等。

数学分析 概率论基础

导数与梯度

taylor展式的应用

机器学习

流程

  1. 数据收集
  2. 数据清洗
  3. 特征工程
  4. 数据建模

相关内容

线性回归、rate、loss

em code

em算法

gmm与图像

图像的卷积

去均值ICA分离

带噪声的信号分离

SVM: 高斯核函数的影响

HMM分词:MLE

LDA

舆情

最大熵模型

聚类

降维

SVM

主题模型pLASA/LDA

条件随机场

变分推导Variation Inference

深度学习

$$ S=\frac{1}{0!} + \frac{1}{1!} + \frac{1}{2!} + \frac{1}{3!} + \frac{1}{4!} + \frac{1}{4!} + \Lambda + \frac{1}{n!} $$

跳跃表分析

markdown优点是易读易写。格式化编辑,纯文本发布,无标签,但支持html标签。

区块元素

段落前后需要空行,若使用换行符<br/>,前面至少两个以上空格才会生效

标题

Setext 底线形式

高阶标题\次阶标题,大于等于2个=-

This is Setext H1
=================
This is Setext H2
-----------------

目录

[TOC] 目录 Markdown语法书写的话,可以自动生成层级目录

六级标题

atx 井号 1-6个

# This is H1
## This is H2

引用

可以加在每一行,也可以加在段落第一行,可以嵌套使用

> 引用文本-一级引用

>> 引用文本-二级引用

>>> 引用文本-三级引用

引用文本-一级引用

引用文本-二级引用

引用文本-三级引用

引用文本-一级引用

列表

无序、有序 <br/>
无序列表 使用 *、+、- <br/>
有序列表 使用 数字+英文句点

代码区块

缩进4个空格或1个制表符 或<pre>、<code>标签

分割线

三个以上*、-、_

区段元素

语法:行内式、参考式语法:行内式、参考式 [方括号]标记

This is 百度

This is [百度](https://www.baidu.com/ "title")

This is 关于

This is [关于](/about "about")

This is 百度

This is [百度][1]  <br>
[1]:https://www.baidu.com/ "title"
OR: [1]:<https://www.baidu.com/> "title"

隐式链接标记

Google

[Google][]
[Google]:https://www.google.com/

参考式:更易读

强调

使用 * 或者底线 _ <strong>

this is strong //*两侧不能有空白

*this is strong* //*两侧不能有空白

行内代码标记

反引号`````

图片

行内式 参考式 <img>

![alt text](/img/img1.jpg "title")

表格

状态集 A B
1 43 33
2 0 33
3 130 118
4 0 0
5 17 6
6 180 111
总数 370 225

标签

标签: MARKDOWN

Tags: MARKDOWN

删除线

这是一条删除线

注脚

这是一个注脚^1

页内跳转

跳转

你好

github文件

github文件地址格式

https://github.com/用户名/repository仓库名/raw/分支名master/图片文件夹名称/图片名称

插入公式

文本中加入公式 $ 数学公式 $

单独加入公式 $$ 数学公式 $$

\begin{equation}
数学公式
\label{eq:当前公式名}
\end{equation}
自动编号后的公式可在全文任意处使用 \eqref{eq:公式名} 语句引用。

$ J_\alpha(x) = \sum_{m=0}^\infty \frac{(-1)^m}{m! \Gamma (m + \alpha + 1)} {\left({ \frac{x}{2} }\right)}^{2m + \alpha} \text {,行内公式示例}

示例: $ J_\alpha(x) = \sum_{m=0}^\infty \frac{(-1)^m}{m! \Gamma (m + \alpha + 1)} {\left({ \frac{x}{2} }\right)}^{2m + \alpha} \text {,行内公式示例} $

$$ J_\alpha(x) = \sum_{m=0}^\infty \frac{(-1)^m}{m! \Gamma (m + \alpha + 1)} {\left({ \frac{x}{2} }\right)}^{2m + \alpha} \text {,独立公式示例} $$

示例:$$ J_\alpha(x) = \sum_{m=0}^\infty \frac{(-1)^m}{m! \Gamma (m + \alpha + 1)} {\left({ \frac{x}{2} }\right)}^{2m + \alpha} \text {,独立公式示例} $$

上下标

^表示上标, _ 表示下标。如果上下标的内容多于一个字符,需要用{}将这些内容括成一个整体。上下标可以嵌套,也可以同时使用

$$ x^{y^z}=(1+{\rm e}^x)^{-2xy^w} $$

$$ x^{y^z}=(1+{\rm e}^x)^{-2xy^w} $$

左右两边都有上下标,可以用\sideset 命令

$$ \sideset{^1_2}{^3_4}\bigotimes $$

$$ \sideset{^1_2}{^3_4}\bigotimes $$

括号和分隔符

()[]|表示符号本身,使用 \{\} 来表示 {}。当要显示大号的括号或分隔符时,要用 \left\right 命令

$$\langle表达式\rangle$$

$$\langle表达式\rangle$$

$$\lceil表达式\rceil$$

$$\lceil表达式\rceil$$

$$\lfloor表达式\rfloor$$

$$\lfloor表达式\rfloor$$

$$\lbrace表达式\rbrace$$

$$\lbrace表达式\rbrace$$

$$ f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right) $$

$$ f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right) $$

分数

通常使用 \frac {分子} {分母} 命令产生一个分数\frac {分子} {分母},分数可嵌套。
便捷情况可直接输入 \frac ab来快速生成一个\frac ab。
如果分式很复杂,亦可使用 分子 \over 分母 命令,此时分数仅有一层

$$\frac{a-1}{b-1} \quad and \quad {a+1\over b+1}$$

$$\frac{a-1}{b-1} \quad and \quad {a+1\over b+1}$$

开方

使用 \sqrt [根指数,省略时为2] {被开方数}命令输入开方。

$$\sqrt{2} \quad and \quad \sqrt[n]{3}$$

$$\sqrt{2} \quad and \quad \sqrt[n]{3}$$

省略号

省略号有两种,\ldots 表示与文本底线对齐的省略号,\cdots 表示与文本中线对齐的省略号

$$f(x_1,x_2,\underbrace{\ldots}_{\rm ldots} ,x_n) = x_1^2 + x_2^2 + \underbrace{\cdots}_{\rm cdots} + x_n^2$$

$$f(x_1,x_2,\underbrace{\ldots}{\rm ldots} ,x_n) = x_1^2 + x_2^2 + \underbrace{\cdots}{\rm cdots} + x_n^2$$

矢量

使用 \vec{矢量}来自动产生一个矢量。也可以使用 \overrightarrow等命令自定义字母上方的符号

$$\vec{a} \cdot \vec{b}=0$$

$$\vec{a} \cdot \vec{b}=0$$

$$\overleftarrow{xy} \quad and \quad \overleftrightarrow{xy} \quad and \quad \overrightarrow{xy}$$

$$\overleftarrow{xy} \quad and \quad \overleftrightarrow{xy} \quad and \quad \overrightarrow{xy}$$

积分

使用 \int_积分下限^积分上限 {被积表达式} 来输入一个积分

$$\int_0^1 {x^2} \,{\rm d}x$$

$$\int_0^1 {x^2} ,{\rm d}x$$

极限运算

使用\lim_{变量 \to 表达式} 表达式 来输入一个极限。如有需求,可以更改 \to 符号至任意符号

$$ \lim_{n \to +\infty} \frac{1}{n(n+1)} \quad and \quad \lim_{x\leftarrow{示例}} \frac{1}{n(n+1)} $$

$$ \lim_{n \to +\infty} \frac{1}{n(n+1)} \quad and \quad \lim_{x\leftarrow{示例}} \frac{1}{n(n+1)} $$

累加、累乘运算

使用 \sum_{下标表达式}^{上标表达式} {累加表达式}来输入一个累加。
与之类似,使用 \prod \bigcup \bigcap来分别输入累乘、并集和交集。
此类符号在行内显示时上下标表达式将会移至右上角和右下角

$$\sum_{i=1}^n \frac{1}{i^2} \quad and \quad \prod_{i=1}^n \frac{1}{i^2} \quad and \quad \bigcup_{i=1}^{2} R$$

$$\sum_{i=1}^n \frac{1}{i^2} \quad and \quad \prod_{i=1}^n \frac{1}{i^2} \quad and \quad \bigcup_{i=1}^{2} R$$

希腊字母

输入 \小写希腊字母英文全称\首字母大写希腊字母英文全称来分别输入小写和大写希腊字母。

输入 显示 输入 显示
\alpha α A A
\beta β B B
\gamma γ \Gamma Γ
\delta δ \Delta Δ
\epsilon ϵ E E
\zeta ζ Z Z
\eta η H H
\theta θ \Theta Θ
\iota ι I I
\kappa κ K K
\lambda λ \Lambda Λ
\nu ν N N
\mu μ M M
\xi ξ \Xi Ξ
o o O O
\pi π \Pi Π
\rho ρ P P
\sigma σ \Sigma Σ
\tau τ T T
\upsilon υ \Upsilon Υ
\phi ϕ \Phi Φ
\chi χ X X
\psi ψ \Psi Ψ
\omega ω \Omega Ω

大括号和行标的使用

使用 \left\right来创建自动匹配高度的 (圆括号),[方括号] 和 {花括号}
在每个公式末尾前使用\tag{行标}来实现行标。

$$
f\left(
   \left[ 
     \frac{
       1+\left\{x,y\right\}
     }{
       \left(
          \frac{x}{y}+\frac{y}{x}
       \right)
       \left(u+1\right)
     }+a
   \right]^{3/2}
\right)
\tag{行标}
$$

$$
f\left(
\left[
\frac{
1+\left{x,y\right}
}{
\left(
\frac{x}{y}+\frac{y}{x}
\right)
\left(u+1\right)
}+a
\right]^{3/2}
\right)
\tag{行标}
$$

$\smash{\displaystyle\max_{0 \leq q \leq n-1}} f(q) \le n$

$\smash{\displaystyle\max_{0 \leq q \leq n-1}} f(q) \le n$

$f(x + \epsilon) \approx f(x) + f'(x) \epsilon + \mathcal{O}(\epsilon^2).$

$f(x + \epsilon) \approx f(x) + f’(x) \epsilon + \mathcal{O}(\epsilon^2).$

$\text{d}x$

$\text{d}x$

链接:Markdown公式编辑学习笔记
链接:The Case for Learned Index Structures

二分查找要求元素可以随机访问,所以决定了需要把元素存储在连续内存。这样查找确实很快,但是插入和删除元素的时候,为了保证元素的有序性,就需要大量的移动元素了。
如果需要的是一个能够进行二分查找,又能快速添加和删除元素的数据结构,首先就是二叉查找树,二叉查找树在最坏情况下可能变成一个链表。
于是,就出现了平衡二叉树,根据平衡算法的不同有AVL树,B-Tree,B+Tree,红黑树等,但是AVL树实现起来比较复杂,平衡操作较难理解,这时候就可以用SkipList跳跃表结构。

Key-Value数据结构
目前常用的key-value数据结构有三种:Hash表、红黑树、SkipList,它们各自有着不同的优缺点(不考虑删除操作):
Hash表:插入、查找最快,为O(1);如使用链表实现则可实现无锁;数据有序化需要显式的排序操作。
红黑树:插入、查找为O(logn),但常数项较小;无锁实现的复杂性很高,一般需要加锁;数据天然有序。
SkipList:插入、查找为O(logn),但常数项比红黑树要大;底层结构为链表,可无锁实现;数据天然有序。

如果要实现一个key-value结构,需求的功能有插入、查找、迭代、修改,那么首先Hash表就不是很适合了,因为迭代的时间复杂度比较高;而红黑树的插入很可能会涉及多个结点的旋转、变色操作,因此需要在外层加锁,这无形中降低了它可能的并发度。而SkipList底层是用链表实现的,可以实现为lock free,同时它还有着不错的性能(单线程下只比红黑树略慢),非常适合用来实现我们需求的那种key-value结构。
LevelDB、Reddis的底层存储结构就是用的SkipList。

基于锁的并发

优点:

  1. 编程模型简单,如果小心控制上锁顺序,一般来说不会有死锁的问题;
  2. 可以通过调节锁的粒度来调节性能。

缺点:

  1. 所有基于锁的算法都有死锁的可能;
  2. 上锁和解锁时进程要从用户态切换到内核态,并可能伴随有线程的调度、上下文切换等,开销比较重;
  3. 对共享数据的读与写之间会有互斥。

无锁编程(lock free)

常见的lock free编程一般是基于CAS(Compare And Swap)操作:CAS(void *ptr, Any oldValue, Any newValue);
即查看内存地址ptr处的值,如果为oldValue则将其改为newValue,并返回true,否则返回false。X86平台上的CAS操作一般是通过CPU的CMPXCHG指令来完成的。CPU在执行此指令时会首先锁住CPU总线,禁止其它核心对内存的访问,然后再查看或修改*ptr的值。简单的说CAS利用了CPU的硬件锁来实现对共享资源的串行使用。

优点

  1. 开销较小:不需要进入内核,不需要切换线程;
  2. 没有死锁:总线锁最长持续为一次read+write的时间;
  3. 只有写操作需要使用CAS,读操作与串行代码完全相同,可实现读写不互斥。

缺点

  1. 编程非常复杂,两行代码之间可能发生任何事,很多常识性的假设都不成立。
  2. CAS模型覆盖的情况非常少,无法用CAS实现原子的复数操作。

而在性能层面上,CAS与mutex/readwrite lock各有千秋,简述如下:

  1. 单线程下CAS的开销大约为10次加法操作,mutex的上锁+解锁大约为20次加法操作,而readwrite lock的开销则更大一些。
  2. CAS的性能为固定值,而mutex则可以通过改变临界区的大小来调节性能;
  3. 如果临界区中真正的修改操作只占一小部分,那么用CAS可以获得更大的并发度。
  4. 多核CPU中线程调度成本较高,此时更适合用CAS。
    跳表和红黑树的性能相当,最主要的优势就是当调整(插入或删除)时,红黑树需要使用旋转来维护平衡性,这个操作需要动多个节点,在并发时候很难控制。而跳表插入或删除时只需定位后插入,插入时只需添加插入的那个节点及其多个层的复制,以及定位和插入的原子性维护。所以它更加可以利用CAS操作来进行无锁编程。

跳表使用多级链表来实现,可以在 O(log n) 的平均时间内执行插入、删除和查找操作。

以下是一个简单的 Java 实现示例,用于构建和使用跳表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import java.util.Random;

class SkipListNode {
int value;
SkipListNode[] next;

public SkipListNode(int value, int level) {
this.value = value;
this.next = new SkipListNode[level + 1];
}
}

public class SkipList {
private int maxLevel;
private SkipListNode head;
private Random random;

public SkipList() {
this.maxLevel = 16; // 跳表的最大层级
this.head = new SkipListNode(Integer.MIN_VALUE, maxLevel);
this.random = new Random();
}

public int randomLevel() {
int level = 1;
while (random.nextDouble() < 0.5 && level < maxLevel) {
level++;
}
return level;
}

public void insert(int value) {
int level = randomLevel();
SkipListNode newNode = new SkipListNode(value, level);

SkipListNode current = head;
for (int i = maxLevel; i >= 0; i--) {
while (current.next[i] != null && current.next[i].value < value) {
current = current.next[i];
}
if (i <= level) {
newNode.next[i] = current.next[i];
current.next[i] = newNode;
}
}
}

public boolean search(int value) {
SkipListNode current = head;
for (int i = maxLevel; i >= 0; i--) {
while (current.next[i] != null && current.next[i].value < value) {
current = current.next[i];
}
if (current.next[i] != null && current.next[i].value == value) {
return true;
}
}
return false;
}

public void display() {
for (int i = maxLevel; i >= 0; i--) {
SkipListNode current = head.next[i];
System.out.print("Level " + i + ": ");
while (current != null) {
System.out.print(current.value + " ");
current = current.next[i];
}
System.out.println();
}
}

public static void main(String[] args) {
SkipList skipList = new SkipList();

skipList.insert(3);
skipList.insert(6);
skipList.insert(9);
skipList.insert(2);
skipList.insert(5);

skipList.display();

System.out.println("Search 6: " + skipList.search(6)); // 输出 true
System.out.println("Search 8: " + skipList.search(8)); // 输出 false
}
}

在上面的示例中,我们首先定义了 SkipListNode 类来表示跳表中的节点。每个节点包含一个值和一个指向下一级的数组。然后,我们创建了 SkipList 类来实现跳表。randomLevel 方法用于随机生成节点的层级,insert 方法用于插入节点,search 方法用于搜索节点,display 方法用于打印跳表的内容。

请注意,这只是一个简单的跳表实现示例。在实际应用中,您可能需要考虑更多的功能和优化,以满足不同的需求。

树状数组(Fenwick Tree,也称为Binary Indexed Tree,BIT)是一种用于高效处理区间求和(或其他类似操作)的数据结构。它具有较低的内存消耗,能够在 O(log n) 的时间内更新单个元素和计算前缀和。

树状数组适用于处理动态数组中的元素累加操作,如频繁更新某个位置的值以及查询某个区间的和等情况。

以下是一个简单的 Java 实现示例,用于构建和使用树状数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class FenwickTree {
private int[] fenwickTree;

public FenwickTree(int size) {
fenwickTree = new int[size + 1]; // 数组下标从1开始
}

public void update(int index, int value) {
while (index < fenwickTree.length) {
fenwickTree[index] += value;
index += index & -index; // 增加下一个相关的位置
}
}

public int query(int index) {
int sum = 0;
while (index > 0) {
sum += fenwickTree[index];
index -= index & -index; // 减去最后一个被包含的元素
}
return sum;
}

public static void main(String[] args) {
int[] input = {1, 3, 5, 7, 9, 11};

FenwickTree fenwickTree = new FenwickTree(input.length);

// 构建树状数组
for (int i = 0; i < input.length; i++) {
fenwickTree.update(i + 1, input[i]);
}

// 查询前缀和
System.out.println(fenwickTree.query(3)); // 输出 9 (1 + 3 + 5)
System.out.println(fenwickTree.query(6)); // 输出 36 (1 + 3 + 5 + 7 + 9 + 11)
}
}

在上述代码中,FenwickTree 类用于实现树状数组。update 方法用于更新树状数组中的一个元素,query 方法用于查询前缀和。树状数组中的数组大小比原始数组大小多1,因为树状数组的下标从1开始。

这只是一个简单的树状数组实现示例。在实际应用中,您可能需要考虑更多的功能和优化,以满足不同的需求。

哈希表(HashMap)是一种常见的数据结构,用于存储键值对。它提供了基于键的快速查找和插入操作,具有高效的查找性能。在 Java 中,HashMap 是基于哈希表实现的,用于存储键值对的无序集合。

下面将分别介绍 Java 1.7 和 Java 1.8 中 HashMap 的实现原理:

Java 1.7 中的 HashMap 实现原理:

在 Java 1.7 中,HashMap 使用拉链法解决哈希冲突。拉链法是一种处理冲突的方法,每个哈希桶(bucket)实际上是一个链表,相同哈希值的元素被放入同一个桶中。

HashMap 由一个哈希桶数组和哈希表的加载因子组成。加载因子是一个浮点数,表示哈希表在填满之前可以容纳的元素数量与当前桶数的比值。当元素数量超过加载因子与桶数的乘积时,HashMap 会自动进行扩容。

在 Java 1.7 的 HashMap 中,哈希值通过调用键对象的 hashCode() 方法得到,然后使用一个散列函数将哈希值映射到桶索引。每个桶中都是一个链表,存储具有相同哈希值的键值对。

Java 1.8 中的 HashMap 实现原理:

Java 1.8 中的 HashMap 在处理哈希冲突时引入了红黑树,以提高性能。当链表长度超过一定阈值时,链表会被转换为红黑树,这样可以减少查找操作的时间复杂度。

Java 1.8 的 HashMap 也引入了桶位(Node 数组和 TreeNode 数组)的概念,将链表和红黑树存储在同一个数组中。HashMap 在插入、删除和查找操作时,会根据桶位的类型(链表或红黑树)选择不同的操作方式,以提高性能。

此外,Java 1.8 的 HashMap 在遇到哈希冲突时会采用扰动函数,将哈希值再次散列,以减少冲突概率。

1
2
3
// Java 1.8 中的 HashMap 源码片段
final int hash = (key == null) ? 0 : hash(key.hashCode());
int index = (n - 1) & hash;

这是 Java 1.8 中的 HashMap 根据哈希值计算桶索引的代码片段,其中 n 是哈希桶数组的大小。

综上所述,Java 的 HashMap 在不同版本中采用了不同的优化策略,包括拉链法、红黑树、扰动函数等,以提供高效的查找和插入操作。当选择使用 HashMap 时,要考虑到哈希冲突、加载因子、性能等因素,以便正确使用和配置该数据结构。