2021-4-10
雁栖湖边绿柳新,
晨露花丛蜜蜂勤,
红步道,白亭台,
煦日清风把歌吟。
池清水浅几一米,
泳裤拖鞋俱备齐,
手脚划蹬鼻替换,
如鱼自在不知疲。
灯熄人未寐,夜寂月犹明。
两手持黑键,双瞳映白屏。
逻辑归正误,万物汇一零。
翌日查结果,中途错误停。
图像处理中, 为了方便处理,便于抽取特征,数据压缩等目的,常常要将图像进行变换。
一般有如下变换方法
这篇文章介绍一下傅里叶变换
积分形式
如果一个函数的绝对值的积分存在,即
并且函数是连续的或者只有有限个不连续点,则对于 x 的任何值, 函数的傅里叶变换存在
幅度
相位
对于图像的幅度谱显示,由于 |F(u,v)| 变换范围太大,一般显示 $D= log(|F(u,v)+1)$
用 <=>
表示傅里叶变换对
f,g,h 对应的傅里叶变换 F,G,H
$F^*$ 表示 $F$ 的共轭
进行多维变换时,可以依次对每一维进行变换。 下面在代码中就是这样实现的。
a)偶分量函数在变换中产生偶分量函数;
b)奇分量函数在变换中产生奇分量函数;
c)奇分量函数在变换中引入系数-j;
d)偶分量函数在变换中不引入系数.
若有
则
1.
2.
尺度变换
卷积定义
1d
2d
卷积定理
离散卷积
用
即两个周期为 N 的抽样函数, 他们的卷积的离散傅里叶变换等于他们的离散傅里叶变换的卷积
卷积的应用:
去除噪声, 特征增强
两个不同周期的信号卷积需要周期扩展的原因:如果直接进行傅里叶变换和乘积,会产生折叠误差(卷绕)。
下面用$ \infty$ 表示相关。
相关函数描述了两个信号之间的相似性,其相关性大小有相关系数衡量
其变换函数与原函数有相同的能量
由上面离散傅里叶变换的性质易知,直接计算 1维 dft 的时间复杂度维 $O(N^2)$。
利用到单位根的对称性,快速傅里叶变换可以达到 $O(nlogn)$的时间复杂度。
我们知道, 在复平面,复数 $cos\theta +i\ sin\theta$k可以表示成 $e^{i\theta}$, 可以对应一个向量。$\theta$即为幅角。
在单位圆中 ,单位圆被分成 $\frac{2\pi}{\theta}$ 份, 由单位圆的对称性
现在记 $ n =\frac{ 2\pi }{\theta}$ , 即被分成 n 份,幅度角为正且最小的向量称为 n 次单位向量, 记为$\omega _n$,
其余的 n-1 个向量分别为 $\omega_{n}^{2},\omega_{n}^{3},\ldots,\omega_{n}^{n}$ ,它们可以由复数之间的乘法得来 $w_{n}^{k}=w_{n}^{k-1}\cdot w_{n}^{1}\ (2 \leq k \leq n) $。
单位根的性质
容易看出 $w_{n}^{n}=w_{n}^{0}=1 $。
对于$ w_{n}^{k}$ , 它事实上就是 $e^{\frac{2\pi i}{n}k}$ 。
下面的推导假设 $n=2^k$,以及代码实现 FFT 部分也是 如此。
利用上面的对称性,
将傅里叶计算进行奇偶分组
$F_{even}$表示将 输入的次序中偶数点进行 Fourier 变换, $F_{odd}$ 同理,这样就形成递推公式。
现在还没有减少计算量,下面通过将分别计算的 奇项,偶项利用起来,只计算 前 $\frac{n}{2}-1$项,后面的一半可以利用此结果马上算出来。每一次可以减少一半的计算量。
对于 $\frac{n}{2}\leq i+\frac{n}{2}\leq n-1$
现在很清楚了,在每次计算 a[0..n-1] 的傅里叶变换F[0..n-1],分别计算出奇 odd[0..n/2-1],偶even[0..n/2-1](可以递归地进行),
那么傅里叶变换为:
下面是 python 实现
一维用 FFT 实现, 不过 只实现了 2 的幂。/ 对于非 2 的幂,用 FFT 实现有点困难,还需要插值,所以我 用$O(n^2)$ 直接实现。
二维的 DFT利用 分离性,直接调用 一维 FFT。
GitHub
1 | import numpy as np |
WSL 使用的不是系统剪切板,与系统剪切板通信,进行复制粘贴,是一个很棘手的问题。
问题:
vim --version | grep clipboard
没有 加号(系统)寄存器, 重新编译 vim(开启 featured)太麻烦我尝试了很多种方法,从操作的舒适程度,以及实现效果来得到最终最优的解决方法: 通过运行 windows 的 paste.exe
, clip.exe
程序进行复制粘贴
环境如下
说明
<c-r>
代表组合键ctrl
+r
<cr>
代表回车键<f1>
可以从 1到12, 代表F1
^J
代表换行的控制字符,而不是^
,J
的连接,在 linux 上 换行为^@
,
在 VIM 输入控制字符,比如^M
,需要按下<c-r><c-m>
从 VIM 中 复制文本到 Windoes 剪切板。
通过 VIM 寄存器实现: 将 visual 模式下选中的文本复制到 vim 寄存器,然后将寄存器内容通过 shell 处理进入到剪切板。
首先选中(在 visual 模式下),用"ay
将内容保存到 a
寄存器,然后在命令行模式下 !echo <c-r>a \| /mnt/c/Windows/System32/clip.exe
(执行 shell 命令。 a 寄存器的内容直接 作为参数文本传递(命令行模式下,
然而拷贝的文本很可能不能直接在shell 下作为参数,有特殊字符,比如"
,$
等等。
所以要进行转义,用 vim 的 escape
函数 (我试了shellescape
, 效果不怎么好)
把上面的操作映射到按键下, 我映射的是 ;y
, 就得到如下的 vim 键盘映射
在 visual 模式下选中,依次按下 ;y
即可复制
vmap ;y "ay: let @a="'".escape(@a,"\\'\"")."'" <cr>:!echo <c-r>a \|"/mnt/c/Windows/System32/clip.exe"<cr>
然而在复制多行时,寄存器中会包含换行控制字符^J
或^@
,^M
,这在传递到shell 中时执行会截断这个参数(在参数还没有输入完全按下 enter 回车),所以有时不会成功。
而且有些字符 escape 也很难转换为 shell 的原文本参数
所以,这个方法行不通
由于寄存器难以传到 shell 作为参数, 我就想到可以把寄存器的内容复制到一个新的 文件 buffer 中, 然后将文件内容拷贝到剪切板,然后删除文件。
如下,每个 <cr>
分隔开 两条 命令,
在 visual 模式下选中,依次按下 ;y
即可复制
vmap ;y "ay: vs vim-copy<cr>"aP:wq<cr>:call system("/mnt/c/Windows/System32/clip.exe < vim-copy && rm vim-copy")<cr><cr>
各部分解释如下
"ay
: 复制选中区域到 a
寄存器vs vim-copy
: 新建文件 vim-copy
到新窗口"aP:wq
: 拷贝 a
内容到 文件并保存退出call system("...")
: 执行 shell 命令, shell 命令的内容就是复制 文件内容到剪切板,再删除文件这个办法可以很好地复制, 唯一的缺点就是打开新buffer 窗口,再关闭,屏幕画面变化大,看着不舒服😲
write 命令缩写为 w, 直接使用就是 保存缓冲区
他后面可以接shell 命令与 shell 交互
如:w !echo
这是对于整个文件,也可以选择一部分,
而进入 visual 模式下选中,再按下:
, 则进入命令行且将选择的位置也输入进命令行
这是可以 直接 传递给 clip.exe
程序。 执行后,选中的部分备剪切掉了,可以按 u
恢复
在 visual 模式下选中,依次按下 ;y
即可复制vmap ;y : !/mnt/c/Windows/System32/clip.exe<cr>u''
这也是最优的方法了,如果你有更好的方法,欢迎赐教。
从 Windows 剪切板 粘贴到 VIM
如果 VIM 没有设置set mouse=a
, 那么可以直接右击粘贴,设置了之后要按住 shift
再右击粘贴
然而这样存在问题,就是 vim 设置了autoindent,它会错误的将粘贴进的文本进行缩进, 而不是粘贴原文。
这个办法可以 set paste
, set nopaste
解决,设置了paste
后,就可以原文粘贴,
而这样输入命令切换很麻烦, 可以set pastetoggle=<f12>
,或者其他按键,这样按一次就可以切换 paste 状态。
这样比平常的 paste 动作 要多一个pastetoggle
操作,所以不好
在 了解到上面 复制时使用的 clip.exe
程序,我就在想是不是 windows 有也专门paste
的程序 (这个程序是和 cmd 交互的,加之, wsl 也可以执行 exe
程序)
很遗憾,windows 没有
但是令人高兴的是,一个网站上有,点击这里下载, 然后解压放到 C:Windows/System32
目录下
使用 vim 的 read 命令进行与 shell 的交互, 即将 shell命令执行的输出 读到当前 buffer
映射如下
在任何模式下按下 ;p
即可粘贴1
2map ;p :read !/mnt/c/Windows/System32/paste.exe <cr>
map! ;p <esc>:read !/mnt/c/Windows/System32/paste.exe <cr>
这个是在新行开始粘贴, 如果在行内粘贴,粘贴的内容在一行内,可以按下i<bs><esc>l
执行退格操作(同样可以映射一下)
综上所述,最终解决方案为:
点击这里下载, 然后解压放到 C:Windows\System32
目录下
再在 .vimrc
文件中增加如下映射1
2
3vmap ;y : !/mnt/c/Windows/System32/clip.exe<cr>u''
map ;p :read !/mnt/c/Windows/System32/paste.exe <cr>
map! ;p <esc>:read !/mnt/c/Windows/System32/paste.exe <cr>
WSL 真香,强烈推荐入坑 😬
还想起一个 瑕疵, WSL 不支持32 位的程序, 不过可以安装 qemu 等解决。
另外 windows terminal 在今年 6月中旬也会来到,值得期待。
gcd
is short for greatest common divisor
If a
,b
are co-primes, we denote as $(a,b)=1, \text{which means } gcd(a,b)=1 $
We can use Euclid algorithm
to calculate gcd
of two numbers.1
2
3
4def gcd(a,b):
while b!=0:
a,b=b,a%b
return a
Let a and b be integers with greatest common divisor d. Then, there exist integers x and y such that ax + by = d. More generally, the integers of the form ax + by are exactly the multiples of d.
we can use extended euclid algorithm to calculate x,y,gcd(a,b)1
2
3
4
5def xgcd(a,b):
'''return gcd(a,b), x,y where ax+by=gcd(a,b)'''
if b==0:return a,1,0
g,x,y = xgcd(b,a%b)
return g,y,x-a//b*y
1 | class primeSieve: |
Excerpted from wikipedia:Miller_Rabin_primality_test
Just like the Fermat and Solovay–Strassen tests, the Miller–Rabin test relies on an equality or set of equalities that hold true for prime values, then checks whether or not they hold for a number that we want to test for primality.
First, a lemma “Lemma (mathematics)”) about square roots of unity in the finite field Z/p**Z*, where p is prime and p > 2. Certainly 1 and −1 always yield 1 when squared modulo p; call these trivial “Trivial (mathematics)”) square roots of 1. There are no nontrivial square roots of 1 modulo p (a special case of the result that, in a field, a polynomial has no more zeroes than its degree). To show this, suppose that x is a square root of 1 modulo p*. Then:
In other words, prime p divides the product (x − 1)(x + 1). By Euclid’s lemma it divides one of the factors x − 1 or x + 1, implying that x is congruent to either 1 or −1 modulo p.
Now, let n be prime, and odd, with n > 2. It follows that n − 1 is even and we can write it as 2s·d, where s and d are positive integers and d is odd. For each a in (Z/n**Z*), either
or
To show that one of these must be true, recall Fermat’s little theorem, that for a prime number n:
By the lemma above, if we keep taking square roots of an−1, we will get either 1 or −1. If we get −1 then the second equality holds and it is done. If we never get −1, then when we have taken out every power of 2, we are left with the first equality.
The Miller–Rabin primality test is based on the contrapositive of the above claim. That is, if we can find an a such that
and
then n is not prime. We call a a witness “Witness (mathematics)”) for the compositeness of n (sometimes misleadingly called a strong witness, although it is a certain proof of this fact). Otherwise a is called a strong liar, and n is a strong probable prime to base a. The term “strong liar” refers to the case where n is composite but nevertheless the equations hold as they would for a prime.
Every odd composite n has many witnesses a, however, no simple way of generating such an a is known. The solution is to make the test probabilistic: we choose a non-zero a in Z/n**Z* randomly, and check whether or not it is a witness for the compositeness of n. If n is composite, most of the choices for a will be witnesses, and the test will detect n as composite with high probability. There is, nevertheless, a small chance that we are unlucky and hit an a which is a strong liar for n. We may reduce the probability of such error by repeating the test for several independently chosen a*.
For testing large numbers, it is common to choose random bases a, as, a priori, we don’t know the distribution of witnesses and liars among the numbers 1, 2, …, n − 1. In particular, Arnault [4] gave a 397-digit composite number for which all bases aless than 307 are strong liars. As expected this number was reported to be prime by the Maple “Maple (software)”) isprime()
function, which implemented the Miller–Rabin test by checking the specific bases 2,3,5,7, and 11. However, selection of a few specific small bases can guarantee identification of composites for n less than some maximum determined by said bases. This maximum is generally quite large compared to the bases. As random bases lack such determinism for small n, specific bases are better in some circumstances.
python implementation1
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
45from random import sample
def quickMulMod(a,b,m):
'''a*b%m, quick'''
ret = 0
while b:
if b&1:
ret = (a+ret)%m
b//=2
a = (a+a)%m
return ret
def quickPowMod(a,b,m):
'''a^b %m, quick, O(logn)'''
ret =1
while b:
if b&1:
ret =quickMulMod(ret,a,m)
b//=2
a = quickMulMod(a,a,m)
return ret
def isPrime(n,t=5):
'''miller rabin primality test, a probability result
t is the number of iteration(witness)
'''
if n<2:
print('[Error]: {} can\'t be classed with prime or composite'.format(n))
return
if n==2: return True
d = n-1
r = 0
while d%2==0:
r+=1
d//=2
t = min(n-3,t)
for a in sample(range(2,n-1),t):
x= quickPowMod(a,d,n)
if x==1 or x==n-1: continue #success,
for j in range(r-1):
x= quickMulMod(x,x,n)
if x==n-1:break
else:
return False
return True
Excerpted from wikipedia:Pollard’s rho algorithm
Suppose we need to factorize a number
$n=pq$, where $p$ is a non-trivial factor. A polynomial modulo $n$, called
where c is a chosen number ,eg 1.
is used to generate a pseudo-random sequence: A starting value, say 2, is chosen, and the sequence continues as
,
The sequence is related to another sequence$\{x_k\ mod \ p\}$ . Since $p$ is not known beforehand, this sequence cannot be explicitly computed in the algorithm. Yet, in it lies the core idea of the algorithm.
Because the number of possible values for these sequences are finite, both the$\{x_n\}$ sequence, which is mod $n$ , and $\{x_n\ mod\ p\}$ sequence will eventually repeat, even though we do not know the latter. Assume that the sequences behave like random numbers. Due to the birthday paradox, the number of$x_k$before a repetition occurs is expected to be $O(\sqrt{N})$ , where $N$ is the number of possible values. So the sequence $\{x_n\ mod\ p\}$ will likely repeat much earlier than the sequence $x_k$. Once a sequence has a repeated value, the sequence will cycle, because each value depends only on the one before it. This structure of eventual cycling gives rise to the name “Rho algorithm”, owing to similarity to the shape of the Greek character ρ when the values $x_i\ mod \ p$ are represented as nodes in a directed graph.
This is detected by the Floyd’s cycle-finding algorithm: two nodes$i,j$ are kept. In each step, one moves to the next node in the sequence and the other moves to the one after the next node. After that, it is checked whether $\text{gcd}(x_i-x_j,n)\neq 1$.
If it is not 1, then this implies that there ris a repetition in the $\{x_k\ mod\ p\}$ swquence
This works because if the $x_i\ mod\ p$is the same as$x_j\ mod\ p$, the difference between$x_i,x_j$ is necessarily a multiple of $p$. Although this always happens eventually, the resulting GCD is a divisor of $n$ other than 1. This may be$n$ itself, since the two sequences might repeat at the same time. In this (uncommon) case the algorithm fails, and can be repeated with a different parameter.
python implementation1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23from random import randint
from isPrime import isPrime
from gcd import gcd
def factor(n):
'''pollard's rho algorithm'''
if n==1: return []
if isPrime(n):return [n]
fact=1
cycle_size=2
x = x_fixed = 2
c = randint(1,n)
while fact==1:
for i in range(cycle_size):
if fact>1:break
x=(x*x+c)%n
if x==x_fixed:
c = randint(1,n)
continue
fact = gcd(x-x_fixed,n)
cycle_size *=2
x_fixed = x
return factor(fact)+factor(n//fact)
Euler function, denoted as $\phi(n)$, mapping n as the number of number which is smaller than n and is the co-prime of n.
e.g.: $\phi(3)=2$ since 1,2 are coprimes of 3 and smaller than 3, $\phi(4)=2$ ,(1,3)
Euler function is a kind of productive function and has two properties as follows:
Thus, for every narural number n, we can evaluate $\phi(n)$ using the following method.
And , $\sigma(n)$ represents the sum of all factors of n.
e.g. : $\sigma(9) = 1+3+9 = 14$
A perfect number
_n_ is defined as $\sigma(n) = 2n$
The following is the implementation of this two functions.
1 | from factor import factor |
The following codes can solve a linear, group modulo equation. More details and explanations will be supplied if I am not too busy.
Note that I use --
to represent $\equiv$ in the python codes.
1 | import re |
See more on github
In this article, I will show you some kinds of popular string matching algorithm and dynamic programming algorithm for wildcard matching.
We can view a string of k characters (digits) as a length-k decimal number. E.g., the string “31425” corresponds to the decimal number 31,425.
d
be the radix of num, thus $d = len(set(s))$Namely,
However, it’s no need to calculate $t_{s+1}$ directly. We can use modulus operation to reduce the work of caculation.
We choose a small prime number. Eg 13 for radix( denoted as d) 10.
Generally, $d*q$ should fit within one computer word.
We firstly caculate $t_0$ mod q.
Then, for every $t_i (i>1)$
assume
denote $ d’ = d^{m-1}\ mod\ q$
thus,
So we can compare the modular value of each $t_i$ with p’s.
Only if they are the same, then we compare the origin chracters, namely
and the pattern characters.
Gernerally, this algorithm’s time approximation is O(n+m), and the worst case is O((n-m+1)*m)
Problem: this is assuming p and $t_s$ are small numbers. They may be too large to work with easily.
python implementation1
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#coding: utf-8
''' mbinary
#########################################################################
# File : rabin_karp.py
# Author: mbinary
# Mail: zhuheqin1@gmail.com
# Blog: https://mbinary.github.io
# Github: https://github.com/mbinary
# Created Time: 2018-12-11 00:01
# Description: rabin-karp algorithm
#########################################################################
'''
def isPrime(x):
for i in range(2,int(x**0.5)+1):
if x%i==0:return False
return True
def getPrime(x):
'''return a prime which is bigger than x'''
for i in range(x,2*x):
if isPrime(i):return i
def findAll(s,p):
'''s: string p: pattern'''
dic={}
n,m = len(s),len(p)
d=0 #radix
for c in s:
if c not in dic:
dic[c]=d
d+=1
sm = 0
for c in p:
if c not in dic:return []
sm = sm*d+dic[c]
ret = []
cur = 0
for i in range(m): cur=cur*d + dic[s[i]]
if cur==sm:ret.append(0)
tmp = n-m
q = getPrime(m)
cur = cur%q
sm = sm%q
exp = d**(m-1) % q
for i in range(m,n):
cur = ((cur-dic[s[i-m]]*exp)*d+dic[s[i]]) % q
if cur == sm and p==s[i-m+1:i+1]:
ret.append(i-m+1)
return ret
def randStr(n=3):
return [randint(ord('a'),ord('z')) for i in range(n)]
if __name__ =='__main__':
from random import randint
s = randStr(50)
p = randStr(1)
print(s)
print(p)
print(findAll(s,p))
A FSM can be represented as $(Q,q_0,A,S,C)$, where
Given a pattern string S, we can build a FSM for string matching.
Assume S has m chars, and there should be m+1 states. One is for the begin state, and the others are for matching state of each position of S.
Once we have built the FSM, we can run it on any input string.
Knuth-Morris-Pratt method
The idea is inspired by FSM. We can avoid computing the transition functions. Instead, we compute a prefix function P in O(m) time, which has only m entries.
Prefix funtion stores info about how the pattern matches against shifts of itself.
For example: p = ababaca,
Then,
$p_5$ = ababa, pre[5] = 3.
Namely $p_3$=aba is the longest prefix of p that is also a suffix of $p_5$.
Time approximation: finding prefix function take O(m), matching takes O(m+n)
python implementation1
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#coding: utf-8
''' mbinary
#########################################################################
# File : KMP.py
# Author: mbinary
# Mail: zhuheqin1@gmail.com
# Blog: https://mbinary.github.io
# Github: https://github.com/mbinary
# Created Time: 2018-12-11 14:02
# Description:
#########################################################################
'''
def getPrefixFunc(s):
'''return the list of prefix function of s'''
length = 0
i = 1
n = len(s)
ret = [0]
while i<n:
if s[i]==s[length]:
length +=1
ret.append(length)
i+=1
else:
if length==0:
ret.append(0)
i+=1
else:
length = ret[length-1]
return ret
def findAll(s,p):
pre = getPrefixFunc(p)
i = j =0
n,m = len(s),len(p)
ret = []
while i<n:
if s[i]==p[j]:
i+=1
j+=1
if j==m:
ret.append(i-j)
j=pre[j-1]
else:
if j==0: i+=1
else: j = pre[j-1]
return ret
def randStr(n=3):
return [randint(ord('a'),ord('z')) for i in range(n)]
if __name__ =='__main__':
from random import randint
s = randStr(50)
p = randStr(1)
print(s)
print(p)
print(findAll(s,p))
The bad-character shift of the present algorithm is slightly modified to take into account the last character of x as follows: for c in Sigma, qsBc[c]=min{i : 0 < i leq m and x[m-i]=c} if c occurs in x, m+1 otherwise (thanks to Darko Brljak).
The preprocessing phase is in O(m+sigma) time and O(sigma) space complexity.
During the searching phase the comparisons between pattern and text characters during each attempt can be done in any order. The searching phase has a quadratic worst case time complexity but it has a good practical behaviour.
For instance,
In this example, t0, …, t4 = a b c a b is the current text window that is compared with the pattern. Its suffix a b has matched, but the comparison c-a causes a mismatch. The bad-character heuristics of the Boyer-Moore algorithm (a) uses the “bad” text character c to determine the shift distance. The Horspool algorithm (b) uses the rightmost character b of the current text window. The Sunday algorithm (c) uses the character directly right of the text window, namely d in this example. Since d does not occur in the pattern at all, the pattern can be shifted past this position.
python implementation1
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''' mbinary
#########################################################################
# File : sunday.py
# Author: mbinary
# Mail: zhuheqin1@gmail.com
# Blog: https://mbinary.github.io
# Github: https://github.com/mbinary
# Created Time: 2018-07-11 15:26
# Description: 字符串模式匹配, sunday 算法, kmp 的改进
# pattern matching for strings using sunday algorithm
#########################################################################
'''
def getPos(pattern):
dic = {}
for i,j in enumerate(pattern[::-1]):
if j not in dic:
dic[j]= i
return dic
def find(s,p):
dic = getPos(p)
ps = pp = 0
ns = len(s)
np = len(p)
while ps<ns and pp<np:
if s[ps] == p[pp]:
ps,pp = ps+1,pp+1
else:
idx = ps+ np-pp
if idx >=ns:return -1
ch = s[idx]
if ch in dic:
ps += dic[ch]+1-pp
else:
ps = idx+1
pp = 0
if pp==np:return ps-np
else:
return -1
def findAll(s,p):
ns = len(s)
np = len(p)
i = 0
ret = []
while s:
print(s,p)
tmp = find(s,p)
if tmp==-1: break
ret.append(i+tmp)
end = tmp+np
i +=end
s = s[end:]
return ret
def randStr(n=3):
return [randint(ord('a'),ord('z')) for i in range(n)]
def test(n):
s = randStr(n)
p = randStr(3)
str_s = ''.join((chr(i) for i in s))
str_p = ''.join((chr(i) for i in p))
n1 = find(s,p)
n2 = str_s.find(str_p) # 利用已有的 str find 算法检验
if n1!=n2:
print(n1,n2,str_p,str_s)
return False
return True
if __name__ =='__main__':
from random import randint
n = 1000
suc = sum(test(n) for i in range(n))
print('test {n} times, success {suc} times'.format(n=n,suc=suc))
wild card:
*
matches 0 or any chars?
matches any single char.Given a string s
which doesn’t include wild card,
and a pattern p
which includes wild card,
Judge if they are matching.
Using dynamic programming.
n = length(s), m = length(p)
dp[m+1][n+1]: bool
i:n, j:m
dp[j][i] represents if s[:i+1] matches p[:j+1]
initialzation :
dp[0][0] = True, dp[0][i],dp[j][0] = False, only if p startswith ‘*’, dp[1][0] = True.
if p[j] = ‘*’: dp[j][i] = dp[j-1][i] or dp[j][i-1]
elif p[j] = ‘?’: dp[j][i] = dp[j-1][i-1]
else : dp[j][i] = dp[j-1][i-1] and s[i] == p[j]
1 | def isMatch(self, s, p): |
无圈连通图, $E = V-1$, 详细见树,
Introduction to algorithm1
1 | for v in V: |
$\Theta(V+E)$1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18def dfs(G):
time = 0
for v in V:
v.pre = None
v.isFind = False
for v in V : # note this,
if not v.isFind:
dfsVisit(v)
def dfsVisit(G,u):
time =time+1
u.begin = time
u.isFind = True
for v in Adj(u):
if not v.isFind:
v.pre = u
dfsVisit(G,v)
time +=1
u.end = time
利用 DFS, 结点的完成时间的逆序就是拓扑排序
在有向图中, 强连通分量中的结点互达
定义 $Grev$ 为 $G$ 中所有边反向后的图
将图分解成强连通分量的算法
在 Grev 上根据 G 中结点的拓扑排序来 dfsVisit, 即1
2
3
4compute Grev
initalization
for v in topo-sort(G.V):
if not v.isFind: dfsVisit(Grev,v)
然后得到的DFS 森林(也是递归树森林)中每个树就是一个强连通分量
利用了贪心算法,1
2
3
4
5Generate-Minimum-spanning-tree(G)
A = []
while len(A)!=len(G.V)-1:
add a safe edge for A to A
return A
总体上, 从最开始 每个结点就是一颗树的森林中(不相交集合, 并查集), 逐渐添加不形成圈的(两个元素不再同一个集合),最小边权的边.1
2
3
4
5
6edges=[]
for edge as u,v in sorted(G.E):
if find-set(u) != find-set(v):
edges.append(edge)
union(u,v)
return edges
如果并查集的实现采用了 按秩合并与路径压缩技巧, 则 find 与 union 的时间接近常数
所以时间复杂度在于排序边, 即 $O(ElgE)$, 而 $E\lt V^2$, 所以 $lgE = O(lgV)$, 时间复杂度为 $O(ElgV)$
用了 BFS, 类似 Dijkstra 算法
从根结点开始 BFS, 一直保持成一颗树1
2
3
4
5
6
7
8
9
10
11for v in V:
v.minAdjEdge = MAX
v.pre = None
root.minAdjEdge = 0
que = priority-queue (G.V) # sort by minAdjEdge
while not que.isempty():
u = que.extractMin()
for v in Adj(u):
if v in que and v.minAdjEdge>w(u,v):
v.pre = u
v.minAdjEdge = w(u,v)
//note it's v, not vlgv
综上, 时间复杂度为$O(ElgV)$
如果使用的是 斐波那契堆, 在 设置 minAdjEdge时 调用 decrease-key
, 这个操作摊还代价为 $O(1)$, 所以时间复杂度可改进到 $O(E+VlgV)$
求一个结点到其他结点的最短路径, 可以用 Bellman-ford算法, 或者 Dijkstra算法.
定义两个结点u,v间的最短路
问题的变体
$p=(v_0,v_1,\ldots,v_k)$为从结点$v_0$到$v_k$的一条最短路径, 对于任意$0\le i\le j \le k$, 记$p_{ij}=(v_i,v_{i+1},\ldots,v_j)$为 p 中 $v_i$到$v_j$的子路径, 则 $p_{ij}$为 $v_i$到$v_j$的一条最短路径
Dijkstra 算法不能处理负值边, 只能用 Bellman-Ford 算法,
而且如果有负值圈, 则没有最短路, bellman-ford算法也可以检测出来
1 | def initialaize(G,s): |
1 | def relax(u,v,w): |
性质
证明
$\Theta(V+E)$1
2
3
4
5def dag-shortest-path(G,s):
initialize(G,s)
for u in topo-sort(G.V):
for v in Adj(v):
relax(u,v,w(u,v))
$O(VE)$1
2
3
4
5
6
7
8
9def bellman-ford(G,s):
initialize(G,s)
for ct in range(|V|-1): # v-1 times
for u,v as edge in E:
relax(u,v,w(u,v))
for u,v as edge in E:
if v.distance > u.distance + w(u,v):
return False
return True
第一个 for 循环就是进行松弛操作, 最后结果已经存储在 结点的distance 和 pre 属性中了, 第二个 for 循环利用三角不等式检查有不有负值圈.
下面是证明该算法的正确性
$O(ElogV)$, 要求不能有负值边
Dijkstra算法既类似于广度优先搜索(,也有点类似于计算最小生成树的Prim算法。它与广度优先搜索的类似点在于集合S对应的是广度优先搜索中的黑色结点集合:正如集合S中的结点的最短路径权重已经计算出来一样,在广度优先搜索中,黑色结点的正确的广度优先距离也已经计算出来。Dijkstra算法像Prim算法的地方是,两个算法都使用最小优先队列来寻找给定集合(Dijkstra算法中的S集合与Prim算法中逐步增长的树)之外的“最轻”结点,将该结点加入到集合里,并对位于集合外面的结点的权重进行相应调整。
1 | def dijkstra(G,s): |
使用动态规划算法, 可以得到最短路径的结构
设 $l_{ij}^{(m)}$为从结点i 到结点 j 的至多包含 m 条边的任意路径的最小权重,当m = 0, 此时i=j, 则 为0,
可以得到递归定义
由于对于所有 j, 有 $w_{jj}=0$,所以上式后面的等式成立.
由于是简单路径, 则包含的边最多为 |V|-1 条, 所以
所以可以从自底向上计算, 如下
输入权值矩阵 $W(w_{ij})), L^{(m-1)}$,输出$ L^{(m)}$, 其中 $L^{(1)} = W$,1
2
3
4
5
6
7
8
9def f(L, W):
n = L.rows
L_new = new matrix(row=n ,col = n)
for i in range(n):
for j in range(n):
L_new[i][j] = MAX
for k in range(n):
L_new[i][j] = min(L_new[i][j], L[i][k]+w[k][j])
return L_new
可以看出该算法与矩阵乘法的关系
$L^{(m)} = W^m$,
所以可以直接计算乘法, 每次计算一个乘积是 $O(V^3)$, 计算 V 次, 所以总体 $O(V^4)$, 使用矩阵快速幂可以将时间复杂度降低为$O(V^3lgV)$1
2
3
4
5
6
7def f(W):
L = W
i = 1
while i<W.rows:
L = L*L
i*=2
return L
同样要求可以存在负权边, 但不能有负值圈. 用动态规划算法:
设 $ d_{ij}^{(k)}$ 为 从 i 到 j 所有中间结点来自集合 ${\{1,2,\ldots,k\}}$ 的一条最短路径的权重. 则有
而且为了找出路径, 需要记录前驱结点, 定义如下前驱矩阵 $\Pi$, 设 $ \pi_{ij}^{(k)}$ 为 从 i 到 j 所有中间结点来自集合 ${\{1,2,\ldots,k\}}$ 的最短路径上 j 的前驱结点
则
对 $k\geqslant 1$
由此得出此算法1
2
3
4
5
6
7
8
9
10
11
12
13def floyd-warshall(W):
n = len(W)
D= W
initialize pre
for k in range(n):
pre2 = pre.copy()
for i in range(n):
for j in range(n)
if d[i][j] > d[i][k]+d[k][j]:
d[i][j] =d[i][k]+d[k][j]
pre2[i][j] = pre[k][j]
pre = pre2
return d,pre
思路是通过重新赋予权重, 将图中负权边转换为正权,然后就可以用 dijkstra 算法(要求是正值边)来计算一个结点到其他所有结点的, 然后对所有结点用dijkstra
1 | JOHNSON (G, u) |
G 是弱连通严格有向加权图, s为源, t 为汇, 每条边e容量 c(e), 由此定义了网络N(G,s,t,c(e)),
由于其实现可以有不同的运行时间, 所以称其为方法, 而不是算法.
思路是 循环增加流的值, 在一个关联的”残存网络” 中寻找一条”增广路径”, 然后对这些边进行修改流量. 重复直至残存网络上不再存在增高路径为止.1
2
3
4
5def ford-fulkerson(G,s,t):
initialize flow f to 0
while exists an augmenting path p in residual network Gf:
augment flow f along p
return f
1 | def ford-fulkerson(G,s,t): |
1. 算法导论 ↩
2. 图论, 王树禾 ↩]]>
斐波那契堆是一系列具有最小堆序的有根树的集合, 同一代(层)结点由双向循环链表链接, 为了便于删除最小结点, 还需要维持链表为升序, 即nd<=nd.right(nd==nd.right时只有一个结点或为 None), 父子之间都有指向对方的指针.
结点有degree 属性, 记录孩子的个数, mark 属性用来标记(为了满足势函数, 达到摊还需求的)
还有一个最小值指针 H.min 指向最小根结点
下面用势函数来分析摊还代价, 如果你不明白, 可以看摊还分析
$\Phi(H) = t(H) + 2m(h)$
t 是根链表中树的数目,m(H) 表示被标记的结点数
结点的最大度数(即孩子数)$D(n)\leqslant \lfloor lgn \rfloor$, 证明放在最后
1 | nd = new node |
1 | def union(H1,H2): |
抽取最小值, 一定是在根结点, 然后将此根结点的所有子树的根放在 根结点双向循环链表中, 之后还要进行树的合并. 以使每个根结点的度不同,1
2
3
4
5
6
7
8
9
10
11
12
13
14def extract-min(H):
z = H.min
if z!=None:
for chd of z:
link chd to H.rootList
chd.prt = None
remove z from the rootList of H
if z==z.right:
H.min = None
else:
H.min = z.right
consolidate(H)
H.n -=1
return z
consolidate 函数使用一个 辅助数组degree来记录所有根结点(不超过lgn)对应的度数, degree[i] = nd 表示.有且只有一个结点 nd 的度数为 i.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22def consolidate(H):
initialize degree with None
for nd in H.rootList:
d = nd.degree
while degree[d] !=None:
nd2 = degree[d]
if nd2.degree < nd.degree:
nd2,nd = nd,nd2
make nd2 child of nd
nd.degree = d+1
nd.mark = False # to balace the potential
remove nd2 from H.rootList
degree[d] = None
d+=1
else: degree[d] = nd
for i in degree:
if i!=None:
link i to H.rootList
if H.min ==None: H.min = i
else if H.min>i: H.min = i
时间复杂度为$O(lgn)$ 即数组移动的长度, 而最多有 lgn个元素
1 | def decrease-key(H,x,k): |
1 | decrease(H,nd, MIN) |
这也是斐波那契
这个名字的由来,
$D(n)\leqslant \lfloor lgn \rfloor$
从此心里有了B数(●’◡’●)
当有大量数据储存在磁盘时,如数据库的查找,插入, 删除等操作的实现, 如果要读取或者写入, 磁盘的寻道, 旋转时间很长, 远大于在 内存中的读取,写入时间.
平时用的二叉排序树搜索元素的时间复杂度虽然是 $O(log_2n)$的, 但是底数还是太小, 树高太高.
所以就出现了 B 树(英文为B-Tree, 不是B减树), 可以理解为多叉排序树. 一个结点可以有多个孩子, 于是增大了底数, 减小了高度, 虽然比较的次数多(关键字数多), 但是由于是在内存中比较, 相较于磁盘的读取还是很快的.
度为 d(degree)的 B 树(阶(order) 为 2d) 定义如下,
每个结点中包含有 n 个关键字信息: $(n,P_0,K_1,P_1,K_2,\ldots,K_n,P_n)$。其中:
a) $K_i$为关键字,且关键字按顺序升序排序 $K_{i-1}< K_i$b) $P_i$ 为指向子树根的接点, $K_{i-1}<P(i-1) < Ki$c) 关键字的数 n 满足(由此也确定了孩子结点的个数): $d-1\leqslant n \leqslant 2d-1$ (根节点可以少于d-1)
树中每个结点最多含有 2d个孩子(d>=2);
性质:
$h\leq \left\lfloor \log _{d}\left({\frac {n+1}{2}}\right)\right\rfloor .$
如下是 度为2的 B 树, 每个结点可能有2,3或4 个孩子, 所以也叫 2,3,4树, 等价于红黑树
可以看成二叉排序树的扩展,二叉排序树是二路查找,B - 树是多路查找。
节点内进行查找的时候除了顺序查找之外,还可以用二分查找来提高效率。
下面是顺序查找的 python 代码1
2
3
4
5
6
7
8
9
10
11
12
13
14def search(self,key,withpath=False):
nd = self.root
fathers = []
while True:
i = nd.findKey(key)
if i==len(nd): fathers.append((nd,i-1,i))
else: fathers.append((nd,i,i))
if i<len(nd) and nd[i]==key:
if withpath:return nd,i,fathers
else:return nd,i
if nd.isLeafNode():
if withpath:return None,None,None
else:return None,None
nd = nd.getChd(i)
我实现时让 fathers 记录查找的路径, 方便在实现 delete 操作时使用(虽然有种 delete 方法可以不需要, 直接 from up to down with no pass by),
自顶向下地进行插入操作, 最终插入在叶子结点,
考虑到叶子结点如果有 2t-1 $(k_1,k_2,\ldots,k_{2t-1})$个 关键字, 则需要进行分裂,
一个有 2t-1$(k_1,k_2,\ldots,k_{2t-1})$个关键字 结点分裂是这样进行的: 此结点分裂为 两个关键字为 t-1个的结点, 分别为 $(k_1,k_2,\ldots,k_{t-1})$, $(k_{t+1},k_{t+2},\ldots,k_{2t-1})$, 然后再插入一个关键字$k_t$到父亲结点.
注意同时要将孩子指针移动正确.
所以自顶向下地查找到叶子结点, 中间遇到 2t-1个关键字的结点就进行分裂, 这样如果其子结点进行分裂, 上升来的一个关键字可以插入到父结点而不会超过2t-1
代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19def insert(self,key):
if len(self.root)== self.degree*2-1:
self.root = self.root.split(node(isLeaf=False),self.degree)
self.nodeNum +=2
nd = self.root
while True:
idx = nd.findKey(key)
if idx<len(nd) and nd[idx] == key:return
if nd.isLeafNode():
nd.insert(idx,key)
self.keyNum+=1
return
else:
chd = nd.getChd(idx)
if len(chd)== self.degree*2-1: #ensure its keys won't excess when its chd split and u
nd = chd.split(nd,self.degree)
self.nodeNum +=1
else:
nd = chd
删除操作是有点麻烦的, 有两种方法1
- Locate and delete the item, then restructure the tree to retain its invariants, OR
- Do a single pass down the tree, but before entering (visiting) a node, restructure the tree so that once the key to be deleted is encountered, it can be deleted without triggering the need for any further restructuring
有如下情况
删除结点在叶子结点上
结点内的关键字个数等于d-1(等于关键字个数下限,删除后将破坏 特性),此时需观察该节点左右兄弟结点的关键字个数:
a. 旋转: 如果其左右兄弟结点中存在关键字个数大于d-1 的结点,则从关键字个数大于 d-1 的兄弟结点中借关键字:(这里看了网上的很多说法, 都是在介绍关键字的操作,而没有提到孩子结点. 我实现的时候想了很久才想出来: 借关键字时, 比如从右兄弟借一个关键字(第一个$k_1$), 此时即为左旋, 将父亲结点对应关键字移到当前结点, 再将右兄弟的移动父亲结点(因为要满足排序性质, 类似二叉树的选择) 然后进行孩子操作, 将右兄弟的$p_0$ 插入到 当前结点的孩子指针末尾) 左兄弟类似, 而且要注意到边界条件, 比如当前结点是第0个/最后一个孩子, 则没有 左兄弟/右兄弟)
b. 合并: 如果其左右兄弟结点中不存在关键字个数大于 t-1 的结点,进行结点合并:将其父结点中的关键字拿到下一层,与该节点的左右兄弟结点的所有关键字合并
同样要注意到边界条件, 比如当前结点是第0个/最后一个孩子, 则没有 左兄弟/右兄弟
自底向上地检查来到这个叶子结点的路径上的结点是否满足关键字数目的要求, 只要关键字少于d-1,则进行旋转(2a)或者合并(2b)操作
python 代码如下, delete
函数中, 查找到结点, 用 fathers::[(父节点, 关键字指针, 孩子指针)]
记录路径, 如果不是叶子结点, 就再进行查找, 并记录结点, 转换关键字.
rebalance 就是从叶子结点自底向上到根结点, 只要遇到关键字数少于 2d-1 的,就进行平衡操作(旋转, 合并)
实现时要很仔细, 考虑边界条件, 还有当是左孩子的时候操作的是父结点的 chdIdx 的前一个, 是右孩子的时候是 chdIdx 的关键字. 具体实现完整代码见文末.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
60def delete(self,key):#to do
'''search the key, delete it , and form down to up to rebalance it '''
nd,idx ,fathers= self.search(key,withpath=True)
if nd is None : return
del nd[idx]
self.keyNum-=1
if not nd.isLeafNode():
chd = nd.getChd(idx) # find the predecessor key
while not chd.isLeafNode():
fathers.append((chd,len(chd)-1,len(chd)))
chd = chd.getChd(-1)
fathers.append((chd,len(chd)-1,len(chd)))
nd.insert(idx,chd[-1])
del chd[-1]
if len(fathers)>1:self.rebalance(fathers)
def rebalance(self,fathers):
nd,keyIdx,chdIdx = fathers.pop()
while len(nd)<self.degree-1: # rebalance tree from down to up
prt,keyIdx,chdIdx = fathers[-1]
lbro = [] if chdIdx==0 else prt.getChd(chdIdx-1)
rbro = [] if chdIdx==len(prt) else prt.getChd(chdIdx+1)
if len(lbro)<self.degree and len(rbro)<self.degree: # merge two deficient nodes
beforeNode,afterNode = None,None
if lbro ==[]:
keyIdx = chdIdx
beforeNode,afterNode = nd,rbro
else:
beforeNode,afterNode = lbro,nd
keyIdx = chdIdx-1 # important, when choosing
keys = beforeNode[:]+[prt[keyIdx]]+afterNode[:]
children = beforeNode.getChildren() + afterNode.getChildren()
isLeaf = beforeNode.isLeafNode()
prt.delChd(keyIdx+1)
del prt[keyIdx]
nd.update(keys,isLeaf,children)
prt.children[keyIdx]=nd
self.nodeNum -=1
elif len(lbro)>=self.degree: # rotate when only one sibling is deficient
keyIdx = chdIdx-1
nd.insert(0,prt[keyIdx]) # rotate keys
prt[keyIdx] = lbro[-1]
del lbro[-1]
if not nd.isLeafNode(): # if not leaf, move children
nd.insert(0,nd=lbro.getChd(-1))
lbro.delChd(-1)
else:
keyIdx = chdIdx
nd.insert(len(nd),prt[keyIdx]) # rotate keys
prt[keyIdx] = rbro[0]
del rbro[0]
if not nd.isLeafNode(): # if not leaf, move children
#note that insert(-1,ele) will make the ele be the last second one
nd.insert(len(nd),nd=rbro.getChd(0))
rbro.delChd(0)
if len(fathers)==1:
if len(self.root)==0:
self.root = nd
self.nodeNum -=1
break
nd,i,j = fathers.pop()
这是算法导论2上的
例如
1 | B-TREE-DELETE(T,k) |
B+ 树3是 B- 树的变体,与B树不同的地方在于:
所有关键字都在叶子结点出现
B+ 的搜索与 B- 树也基本相同,区别是 B+ 树只有达到叶子结点才命中(B- 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
下面摘自 wiki4
>
查找
查找以典型的方式进行,类似于二叉查找树。起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是二分查找来确定这个位置。
插入
节点要处于违规状态,它必须包含在可接受范围之外数目的元素。
- 首先,查找要插入其中的节点的位置。接着把值插入这个节点中。
- 如果没有节点处于违规状态则处理结束。
- 如果某个节点有过多元素,则把它分裂为两个节点,每个都有最小数目的元素。在树上递归向上继续这个处理直到到达根节点,如果根节点被分裂,则创建一个新根节点。为了使它工作,元素的最小和最大数目典型的必须选择为使最小数不小于最大数的一半。
删除
- 首先,查找要删除的值。接着从包含它的节点中删除这个值。
- 如果没有节点处于违规状态则处理结束。
- 如果节点处于违规状态则有两种可能情况:
- 它的兄弟节点,就是同一个父节点的子节点,可以把一个或多个它的子节点转移到当前节点,而把它返回为合法状态。如果是这样,在更改父节点和两个兄弟节点的分离值之后处理结束。
- 它的兄弟节点由于处在低边界上而没有额外的子节点。在这种情况下把两个兄弟节点合并到一个单一的节点中,而且我们递归到父节点上,因为它被删除了一个子节点。持续这个处理直到当前节点是合法状态或者到达根节点,在其上根节点的子节点被合并而且合并后的节点成为新的根节点。
由于叶子结点间有指向下一个叶子的指针, 便于遍历, 以及区间查找, 所以数据库的以及操作系统文件系统的实现常用 B+树,
B-tree 5 是 B+-tree 的变体,在 B+ 树的基础上 (所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),B 树中非根和非叶子结点再增加指向兄弟的指针;B 树定义了非叶子结点关键字个数至少为 (2/3)M,即块的最低使用率为 2/3(代替 B+ 树的 1/2)
1 |
|
1 | class node: |
1. B树 ↩
2. 算法导论 ↩
4. B+树 ↩
5. 从 B 树、B + 树、B * 树谈到 R 树 ↩]]>
本文整理自<<精通比特币>>
**
每一笔交易可以分为 输入, 输出, 其他
输入>=输出+其他(奖励给矿工),
而输入的比特币需要引用其来源(它作为上次交易的输出),称UTXO.
而且每笔交易作为输入不能分割.
新生成的钱包, 即未交易过的比特币地址对于比特币网络来说是不知道的,或者是未经注册到比特币系统中。它只是一个数字,对应于一个可以用来控制资金访问的密钥。由钱包独立生成的,还没有参考或注册任何服务。
事实上,在大多数钱包中,比特币地址和任何外部可识别的信息(包括用户的身份)之间没有关联。在该地址被引用作为比特币总帐的交易中的接收者之前,比特币地址只是在比特币中有效的大量可能的地址的一部分。只有一旦与交易相关联才能成为网络中已知地址的一部分。
许多比特币交易都会包括新所有者的地址(买方地址)和当前所有者的地址(称为找零地址)的输出。这是因为交易输入,就像纸币那样能够,不能再分割。如果您在商店购买了5美元的商品,但是使用20美元的美金来支付商品,您预计会收到15美元的找零。
相同的概念适用于比特币交易输入。如果您购买了一个价格为5比特币但只能使用20比特币输入的商品,那
么您可以将5个比特币的一个输出发送给商店所有者,并将一个15比特币的输出返回给您自己作为找零(减去任何适用的交易费用)。重要的是,找零地址不必与输入时提供的地址相同,出于隐私的原因,通常是所有者钱包中的新地址。
另外还有其他模式, 比如有一把零钱, 凑在一起,支付一次
还有就是一次有很大一笔钱, 分给很多人.
如下图所示
钱包应用可以在完全离线时建立交易。就像在家里写张支票, 之后放到信封发给银行一样,比特币交易建立和签名时不用连接比特币网络。只有在执行交易时才需要将交易发送到网络。
下载
1 | git clone |
检查./autogen.sh
配置
1 | ./configure |
编译
1 | make |
测试是否成功
1 | which bitcoind #/usr/local/bin/bitcoind |
设置 API 访问的密码(首次运行):
编辑bitcoin/bitcoin.conf 内容如下. rpc 即 remote procedure call
1 | rpcuser=bitcoinrpc |
启动守护进程后台运行 ,$ bitcoind -daemon
监视状态$ bitcoin-cli getinfo
RPC
1 | bitcoin-cli getinfo #return json |
比特币使用 secp256k1标准定义的一种特殊的椭圆曲线和一系列常数
secp256k1:
其中 $p=2^{256}-2^{32}-2^9-2^8-2^7-2^6-2^4-1$为素数
随机数私钥 k,
取椭圆曲线上的一点 G, 称为生成点,
公钥 $K = kG$, 注意是 mod p 的域内
这个过程是不可逆的
增加位权减少长度
使用二进制算术计算椭圆曲线的时候,y坐标可能是奇数或者偶数,分别对应前面所讲的y值的正负符号
压缩格式公钥和非压缩格式公钥看起来不同,但是对应着同样的一个私钥。更重要的是,如果我们使用双哈希函数(RIPEMD160(SHA256(K)))将压缩格式公钥转化成比特币地址,得到的地址将会不同于由非压缩格式公钥产生的地址。这种结果会让人迷惑,因为一个私钥可以生成两种不同格式的公钥——压缩格式和非压缩格式,而这两种格式的公钥可以生成两个不同的比特币地址。但是,这两个不同的比特币地址的私钥是一样的。
当一个私钥被使用WIF压缩格式导出时,不但没有压缩,而且比“非压缩格式”私钥长出一个字节。这个多出来的一个字节是私钥被加了后缀01,用以表明该私钥是来自于一个较新的钱包, 只能被用来生成压缩的公钥。
私钥是非压缩的,也不能被压缩。“压缩的私钥”实际上只是表示“用于生成压缩格式公钥的私钥”,而“非压缩格式私钥”用来表明“用于生成非压缩格式公钥的私钥”。
十六进制压缩私钥格式在末尾有一个额外的字节(十六进制为01)。虽然Base58编码版本前缀对于WIF和WIF压缩格式都是相同的(0x80),但在数字末尾添加一个字节会导致Base58编码的第一个字符从5变为K或 L,考虑到对于Base58这是十进制编码100号和99号之间的差别。对于100是一个数字长于99的数字,它有一个前缀1,而不是前缀9。当长度变化,它会影响前缀。 在Base58中,前缀5改变为K或L,因为数字的长度增加一个字节。
要注意的是,这些格式并不是可互换使用的。在实现了压缩格式公钥的较新的钱包中,私钥只能且永远被导出为WIF压 缩格式(以K或L为前缀)。对于较老的没有实现压缩格式公钥的钱包,私钥将只能被导出为WIF格式(以5为前缀)导 出。这样做的目的就是为了给导入这些私钥的钱包一个信号:是否钱包必须搜索区块链寻找压缩或非压缩公钥和地址。
最全面的比特币Python库是 Vitalik Buterin写的 pybitcointools
加密标准— BIP0038: 使用一个口令加密私钥并使用Base58Check对加密的私钥进行编码,这样加密的私钥就可以安全地保存在备份介质里,安全地在钱包间传输,保持密钥在任何可能被暴露情况下的安全性。(使用了 AES)
BIP0038加密方案:输入一个比特币私钥,通常使用WIF编码过,base58chek字符串的前缀“5”。此外BIP0038加密方案需要一个长密码作为口令,通常由多个单词或一段复杂的数字字母字符串组成。BIP0038加密方案的结果是一个由 base58check编码过的加密私钥,前缀为6P。如果你看到一个6P开头的的密钥,这就意味着该密钥是被加密过,并需要一个口令来转换(解码)该密钥回到可被用在任何钱包WIF格式的私钥(前缀为5)
以数字3开头的比特币地址是P2SH地址,有时被错误的称谓多重签名或多重签名地址。他们指定比特币交易中受益人为哈希的脚本,而不是公钥的所有者
不同于P2PKH交易发送资金到传统1开头的比特币地址,资金被发送到3开头的地址时,需要的不仅仅是一个公钥的哈希值和一个私钥签名作为所有者证明。在创建地址的时候,这些要求会被指定在脚本中,所有对地址的输入都会被这些要求阻隔。
一个P2SH地址从交易脚本中创建,它定义谁能消耗这个交易输出script hash =RIPEMD160(SHA256(script))
产生的脚本哈希由Base58Check编码前缀为5的版本、编码后得到开头为3的编码地址
P2SH函数最常见的实现是多重签名地址脚本。顾名思义,底层脚本需要多个签名来证明所有权,此后才能消费资金。设计比特币多重签名特性是需要从总共N个密钥中需要M个签名(也被称为“阈值”),被称为M-N多签名,其 中M是等于或小于N。例如,第一章中提到的咖啡店主Bob使用多重签名地址需要1-2签名,一个是属于他的密钥和一个属于他同伴的密钥,以确保其中一方可以签署消费一笔锁定到这个地址的输出。这类似于传统的银行中的一个“联合账户”,其中任何一方配偶可以单独签单消费。
将公钥和私钥(可以是加密过的)打印在纸上, 这期间都没有经过网络(直接用算法计算出), 所以又被称为冷钱包.
广义上,钱包是一个应用程序,为用户提供交互界面。钱包控制用户访问权限,管理密钥和地址,跟踪余额以及创建和签名交易。 狭义上,即从程序员的角度来看,“钱包”是指用于存储和管理用户密钥的数据结构。
一个常见误解是,比特币钱包里含有比特币。 事实上,钱包里只含有钥匙。 “钱币”被记录在比特币网络的区块链中。 用户通过钱包中的密钥签名交易,从而来控制网络上的钱币。 在某种意义上,比特币钱包是密钥链。
每个用户有一个包含多个密钥的钱包。根据包含的多个密钥是否相互关联,可以分为两类
其中每个密钥都是从随机数独立生成的。密钥彼此无关。这种钱包也被称为“Just a Bunch Of Keys(一堆密钥)”,简称 JBOK 钱包。
其中所有的密钥都是从一个主密钥派生出来,这个主密钥即为种子(seed)。该类型钱包中所有密钥都相互关联,如果有原始种子,则可以再次生成全部密钥。确定性钱包中使用了许多不同的密钥推导方法。最常用的推导方法是使用树状结构,称为分级确定性钱包或HD钱包。
HD钱包包含以树状结构衍生的密钥
优点:
由一系列英文单词生成种子是个标准化的方
法,这样易于在钱包中转移、导出和导入。
这些英文单词被称为助记词,标准由BIP-39定义
助记词是由钱包使用BIP-39中定义的标准化过程自动生成的。 钱包从熵源开始,增加校验和,然后将熵映射到单词列表:
1、创建一个128到256位的随机序列(熵)。
2、提出SHA256哈希前几位(熵长/ 32),就可以创造一个随机序列的校验和。
3、将校验和添加到随机序列的末尾。
4、将序列划分为包含11位的不同部分。
5、将每个包含11位部分的值与一个已经预先定义2048个单词的字典做对应。
6、生成的有顺序的单词组就是助记码。
助记词表示长度为128至256位的熵。 通过使用密钥延伸函数PBKDF2,熵被用于导出较长的
(512位)种子。将所得的种子用于构建确定性钱包并得到其密钥。
密钥延伸函数有两个参数:助记词和盐。其中盐的目的是增加构建能够进行暴力攻击的查找
表的困难度。
7、PBKDF2密钥延伸函数的第一个参数是从步骤6生成的助记符。
8、PBKDF2密钥延伸函数的第二个参数是盐。 由字符串常数“助记词”与可选的用户提供的密码字符串连接组成。
9、PBKDF2使用HMAC-SHA512算法,使用2048次哈希来延伸助记符和盐参数,产生一个512位的值作为其最终输出。 这个512位的值就是种子。
密钥延伸函数,使用2048次哈希是一种非常有效的保护,可以防止对助记词或密码短语的暴力攻击。 它使得攻击尝试非常昂贵(从计算的角度),需要尝试超过几千个密码和助记符组合,而这样可能产生的种子的数量是巨大的(2^512)。
BIP-39标准允许在推导种子时使用可选的密码短语。 如果没有使用密码短语,助记词是用由常量字符串“助记词”构成的盐进行延伸,从任何给定的助记词产生一个特定的512位种子。 如果使用密码短语,密钥延伸函数使用同样的助记词也会产生不同的种子。
BIP-39中没有“错误的”密码短语。 每个密码都会导致一些钱包,只是未使用的钱包是空的。
如果钱包所有者无行为能力或死亡,没有人知道密码,种子是无用的,所有存储在钱包中的资金都将永远丢失。相反,如果所有者将密码短语与种子备份在相同的地方,则违反了上述第二个因素的目的。虽然密码是非常有用的,但它们只能与仔细计划的备份和恢复流程结合使用,考虑到所有者个人风险的可能性,应该允许其家人恢复加密资产。
分层确定性钱包使用CKD(child key derivation)函数去从母密钥衍生出子密钥。
子密钥衍生函数是基于单项哈希函数。这个函数结合了:
链码是用来给这个过程引入确定性随机数据的,使得索引不能充分衍生其他的子密钥。因此,有了子密钥并不能让它发现自己的姊妹密钥,除非你已经有了链码。最初的链码种子(在密码树的根部)是用随机数据构成的,随后链码从各自的母链码中衍生出来。
母公共钥匙——链码——以及索引号合并在一起并且用HMAC-SHA512函数散列之后可以得到512位的散列。所得的散列可被拆分为两部分。散列右半部分的256位产出可以给子链当链码。左半部分256位散列以及索引码被加载在母私钥上来衍生子私钥。 如上图
改变索引可以让我们延长母密钥以及创造序列中的其他子密钥。比如子0,子1,子2等等。每一个母密钥可以有2,147,483,647 (2^31) 个子密钥。2^31是整个2^32范围可用的一半,因为另一半是为特定类型的推导而保留的.
如下则是扩展母公钥来衍生子公钥的传递机制。
密钥以及链码的结合,就叫做扩展密钥(extended key). 可以简单地被储存并且表示为简单的将256位密钥与256位链码所并联的512位序列。
扩展密钥通过Base58Check来编码,从而能轻易地在不同的BIP-32兼容钱包之间导入导出。扩展密钥编码用的 Base58Check使用特殊的版本号,这导致在Base58编码字符中,出现前缀“xprv”和“xpub”。
分层确定性钱包的一个很有用的特点就是可以不通过私钥而直接从公共母密钥派生出公共子密钥的能 力。所以有两种衍生子公钥的方法:通过子私钥,或者就是直接通过母公钥。
因此,扩展密钥可以在HD钱包结构的分支中,被用来衍生所有的公钥(且只有公钥)。
应用: 用来创造非常保密的只有公钥配置。在配置中,服务器或者应用程序不管有没有私钥,都可以有扩展公钥的副本。这种配置可以创造出无限数量的公钥以及比特币地址。但是发送到这个地址里的任何比特币都不能使用。与此同时,在另一种更保险的服务器上,扩展私钥可以衍生出所有的对应的可签署交易以及花钱的私钥。
扩展的私钥可以被储存在纸质钱包中或者硬件设备中(比如 Trezor 硬件钱包),与此同时扩展公钥可以在线保存。根据意愿创造“接收”地址而私钥可以安全地在线下被保存。为了支付资金,使用者可以使用扩展的私钥离线签署比特币客户或者通过硬件钱包设备(比如 Trezor)签署交易。
这种方案的常见应用是安装扩展公钥电商的网络服务器上。网络服务器可以使用这个公钥衍生函数去给每一笔交易(比如客户的购物车)创造一个新的比特币地址。但为了避免被偷,网络服务商不会有任何私钥。没有HD钱包的话,唯一的方法就是在不同的安全服务器上创造成千上万个比特币地址,之后就提前上传到电商服务器上。这种方法比较繁琐而且要求持续的维护来确保电商服务器不“用光”公钥。
访问扩展公钥并不能得到访问子私钥的途径。但是,因为扩展公钥包含有链码,如果子私钥被知道或者被泄漏的话,链码就可以被用来衍生所有的其他子私钥
为了应对这种风险,HD钱包使用一种叫做硬化衍生(hardened derivation)的替代衍生函数。
这就“打破”了母公钥以及子链码之间的关系。这个硬化衍生函数使用了母私钥去推导子链码,而不是母公钥。这就在母/子顺序中创造了一道“防火墙”——有链码但并不能够用来推算子链码或者姊妹私钥。强化衍生函数看起来几乎与一般的衍生的子私钥相同,不同的是母私钥被用来输入散列函数中而不是母公钥,
用在衍生函数中的索引号码是32位的整数。为了区分密钥是从正常衍生函数中衍生出来还是从强化衍生函数中产出,这个索引号被分为两个范围。索引号在0和2^31–1(0x0 to0x7FFFFFFF)之间的是只被用在常规衍生。索引号在2^31和2^32– 1(0x80000000 to 0xFFFFFFFF)之间的只被用在强化衍生。
HD钱包中的密钥是用“路径”命名的,且每个级别之间用斜杠(/)字符来表示。由主私钥衍生出的私钥起始以“m”打头。由主公钥衍生的公钥起始以“M“打头。因此,母密钥生成的第一个子私钥是m/0。第一个公钥是M/0。第一个子密钥的子密钥就是m/0/1,以此类推。
HD钱包树状结构提供了极大的灵活性。每一个母扩展密钥有40亿个子密钥:20亿个常规子密钥和20亿个强化子密钥。 而每个子密钥又会有40亿个子密钥并且以此类推。
由此带来问题,对无限的树状结构进行导航就变得异常困难。尤其是对于在不同的HD钱包之间进行转移交易,因为内部组织到内部分支以及亚分支的可能性是无穷的。
两个比特币改进建议(BIPs)
BIP-44指定了包含5个预定义树状层级的结构:m / purpose' / coin_type' / account' / change / address_index
第一层的purpose总是被设定为44’。
第二层的“coin_type”特指币种并且允许多元货币HD钱包中的货币在第二个层级下有自己的亚树状结构。
目前有三种货币被定义:Bitcoin is m/44’/0’、Bitcoin Testnet is m/44’/1’,以及 Litecoin is m/44’/2’。
第三层级是“account”,举个例子,一个HD钱包可能包含两个比特币“账户”:m/44’/0’/0’和 m/44’/0’/1’。每个账户都是它自己亚树的根。
第四层级就是“change”。注意无论先前的层级是否使用强化衍生,这一层级使用的都是常规衍生。这是为了允许这一层级的树可以在不安全环境下,输出扩展公钥。
被HD钱包衍生的可用的地址是第四层级的子级,就是第五层级的树的“address_index”
如
根据比特币系统的设计原理,系统中任何其他的部分都是为了确保比特币交易可以被生成、能在比特币网络中得以传播和通过验证,并最终添加入全球比特币交易总账簿(比特币区块链)。比特币交易的本质是数据结构,这些数据结构中含有比特币交易参与者价值转移的相关信息。比特币区块链是一本全球复式记账总账簿,每个比特币交易都是在比特币区块链上的一个公开记录。
比特币交易中的基础构建单元是交易输出。 交易输出是比特币不可分割的基本组合,记录在区块上,并被整个网络识别为有效。 比特币完整节点跟踪所有可找到的和可使用的输出,称为 “未花费的交易输出”(unspent transaction outputs),即UTXO。所有UTXO的集合被称为UTXO集。每一个交易都代表UTXO集的变化(状态转换)。
用户的比特币“余额”是指用户钱包中可用的UTXO总和. 比特币钱包通过扫描区块链并聚集所有属于该用户的UTXO来计算该用户的余额 。大多数钱包维护一个数据库或使用数据库服务来存储所有UTXO的快速参考集,这些UTXO由用户所有的密钥来控制花费行为。
一个UTXO只能在一次交易中作为一个整体被消耗。一定数量的比特币价值在不同所有者之间转移,并在交易链中消耗和创建UTXO。一笔比特币交易通过使用所有者的签名来解锁UTXO,并通过使用新的所有者的比特币地址来锁定并创建UTXO
它是每个区块中的第一笔交易,这种交易存在的原因是作为对挖矿的奖励,创造出全新的可花费比特币用来支付给“赢家”矿工。
输入和输出,哪一个是先产生的呢?先有鸡还是先有蛋呢?严格来讲,先产生输出,因为可以创造新比特币的 “币基交易”没有输入,但它可以无中生有地产生输出。
交易输出包含两部分:
这个加密难题也被称为锁定脚本(locking script), 见证脚本(witness script), 或脚本公钥(scriptPubKey)。
如下面的交易包含两个输出,
每个输出包含 比特币的值(本身编码是以聪为单位, 以json解码后单位是 比特币), 以及锁定脚本1
2
3
4
5
6
7
8
9
10
11
12
13"vout":[
truetrue{
truetruetruetrue"value":0.01500000,
truetruetruetrue"scriptPubKey":"OP_DUPOP_HASH160ab68025513c3dbd2f7b92a94e0581f5d50f654e7OP_EQU
ALVERIFY
OP_CHECKSIG"
truetrue},
truetrue{
truetruetruetrue"value":0.08450000,
truetruetruetrue"scriptPubKey":"OP_DUPOP_HASH1607f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8OP_EQU
ALVERIFYOP_CHECKSIG",
truetrue}
]
包含
如下面的交易包含一个输入
1 | "vin":[ |
首先检索引用的UTXO,检查其锁定脚本,然后使用它来构建所需的解锁脚本以满足此要求。
除了对包含它引用的交易之外,我们无从了解这个UTXO的任何内容。我们不知道它的价值(多少satoshi金额),我们不知道设置支出条件的锁定脚本。要找到这些信息,我们必须通过检索整个交易来检索被引用的UTXO。
然后运行解锁脚本与锁定脚本, 检查结果是否为True
大多数交易包含交易费(矿工费),这是为了确保网络安全而给比特币矿工的一种补偿。费用本身也作为一个安全机制,使经济上不利于攻击者通过交易来淹没网络。
大多数钱包自动计算并计入交易费。但是, 如果你以编程方式构造交易,或者使用命令行界面,你必须手动计算并计入这些费用。
任何创建交易的比特币服务,包括钱包,交易所,零售应用等,都必须实现动态收费。动态费用可以通过第三方费用估算服务或内置的费用估算算法来实现
费用估算算法根据网络能力和“竞争”交易提供的费用计算适当的费用。大多数服务为用户提供高、中、低优先费用的选择。高优先级意味着用户支付更高交易费.
交易费即输入总和减输出总和的余量:交易费 = 求和(所有输入) - 求和(所有输出)
如果你忘记了在手动构造的交易中增加找零的输出,系统会把找零当作交易费来处理。“不用找了!”也许不是你的真实意愿。
一般交易费是根据交易的数据正相关的, 而不是交易的比特币值,所以如果有很多个 输入(很多个 UTXO 零钱), 或很多输出, 造成数据量很大, 而使交易费很多.
有条件的流控制以外,没有循环或复杂流控制能力。这限制确保该语言不被用于创造无限循环或其它类型的逻辑炸弹,这样的炸弹可以植入在一笔交易中,引起针对比特币网络的“拒绝服务”攻击。因为每一笔交易都会被网络中的全节点验证,受限制的语言能防止交易验证机制被作为一个漏洞而加以利用。
没有任何中心主体能凌驾于脚本之上,也没有中心主体会在脚本被执行后对其进行保存。所以执行脚本所需信息都已包含在脚本中。一个脚本能在任何系统上以相同的方式执行。
一个放置在输出上面的花费条件.它指定了今后花费这笔输出必须要满足的条件。曾被称为脚本公钥(scriptPubKey) , 也被称为见证脚本(witness script),或者更一般地说,它是一个加密难题(cryptographic puzzle)。这些术语在不同的抽象层次上都意味着同样的东西。
一个“解决”或满足被锁定脚本在一个输出上设定的花费条件,从而允许输出被消费的脚本。解锁脚本是每一笔比特币交易输入的一部分,而且往往含有一个由用户的比特币钱包(通过用户的私钥)生成的数字签名,曾被称作ScriptSig。
每一个比特币验证节点会通过同时执行锁定和解锁脚本来验证一笔交易。每个输入都包含一个解锁脚本,并引用了之前存在的UTXO。 验证软件将复制解锁脚本,检索输入所引用的UTXO,并从该UTXO复制锁定脚本。 然后依次执行解锁和锁定脚本。如果解锁脚本满足锁定脚本条件,则输入有效。所有输入都是独立验证的,作为交易总体验证的一部分。
形式上两个脚本拼接如下, 如后用栈的方式执行, 右边为栈顶. 最终结果为 TRUE 则 满足条件
如
锁定脚本: 3 OP_ADD 5 OP_EQUAL
解锁脚本: 2
拼接后为: 2 3 OP_ADD 5 OP_EQUAL
实际过程
使用堆栈执行引擎执行解锁脚本。如果解锁脚本在执行过程中未报错(例如:没有“悬挂”操作码),则复制主堆栈(而不是备用堆栈),并执行锁定脚本。如果从解锁脚本中复制而来的堆栈数据执行锁定脚本的结果为“TRUE”,那么解锁脚本就成功地满足了锁定脚本所设条件
如
锁定脚本: OP_DUP OP_HASH160 <Cafe Public Key Hash> OP_EQUALVERIFY OP_CHECKSIG
解锁脚本: <Cafe Signature> <Cafe Public Key>
验证过程
数字签名在不揭示私钥的情况下提供私钥的所有权证明。
数字签名在比特币中的作用:
每个交易输入和它可能包含的任何签名完全独立于任何其他输入或签名。多方可以协作构建交易,并各自仅签一个输入。
公钥是一个二维数组, 图形上是一个点
比特币中使用的数字签名算法是椭圆曲线数字签名算法(Elliptic Curve Digital SignatureAlgorithm , ECDSA)
签名算法首先生成一个 ephemeral(临时)私钥(即随机数 $k_{tmp}$), 记临时私钥生成的临时公钥的 x坐标为 $x$, p 是椭圆曲线的主要顺序, 记用户的私钥为k,公钥为K(是一个点) , G是椭圆曲线发生器点.
则
得到签名为 $signature = (x,y)$
验证过程如下, 计算
如果 $x_{verify} = x$, 则签名有效
如果在两个不同的交易中,在签名算法中使用相同的值 k,则私钥可以被计算并暴露给世界!
重用 k 值的最常见原因是未正确初始化的随机数生成器。为了避免这个漏洞,业界最佳实践不是用熵播种的随机数生成器生成 k 值,而是使用交易数据本身播种的确定性随机进程。
如用 DER((Distinguished Encoding Rules)编码后的签名为3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e381301
这里 数字签名记为(R,S)
SIGHASH,指示交易数据的哪一部分.SIGHASH 标志是附加到签名的单个字节。每个签名都有一个SIGHASH标志,该标志在不同输入之间也可以不同。 有三个 标志 如下
另外还有一个修饰符标志SIGHASH_ANYONECANPAY,它可以与前面的每个标志组合。 当设置ANYONECANPAY时,只有一个输入被签名,其余的(及其序列号)打开以进行修改。ANYONECANPAY的值为0x80,并通过按位OR运算,得到如下所示的组合标志:
SIGHASH标志在签名和验证期间应用的方式是建立交易的副本和删节其中的某些字段(设置长度为零并清空),继而生成的交易被序列化,SIGHASH标志被添加到序列化交易的结尾,并将结果哈希化 ,得到的哈希值本身即是被签名的“消息”。 基于SIGHASH标志的使用,交易的不同部分被删节。 所得到的哈希值取决于交易中数据的不同子集。
例如
多重签名脚本设置了一个条件,其中N个公钥被记录在脚本中,并且至少有M个必须提供签名来解锁资金。这也称为M-N方案,其中N是密钥的总数,M是验证所需的签名的数量。例如,2-3的多重签名是三个公钥被列为潜在签名人,至少有2个有效的签名才能花费资金。
锁定脚本格式:1
M <PubKey 1> <Pubkey 2> ... <Pubkey N> N CHECKMULTISIG
1 | <Signature 1> <Signature 2> ... <Signature M> |
然而由于实施中 CHECKMULTISIG 的 bug: 会在弹出解锁脚本时从栈中多弹出一个, 所以
解锁脚本规定为1
0 <Signature 1> <Signature 2> ... <Signature M>
P2SH 是针对 多重签名 以下问题提出的
在P2SH 支付中锁定脚本由哈希运算后的20字节的散列值取代,被称为赎回脚本。当一笔交易试图支付UTXO时,要解锁支付脚本,它必须含有与哈希相匹配的脚本。
如下
赎回脚本本身之后作为解锁脚本在输出花费时的一部分出现。 这使得给矿工的交易费用从发送方转移到收款方,复杂的计算工作也从发送方转移到收款方。
P2SH旨在使复杂脚本的运用能与直接向比特币地址支付一样简单。
P2SH 能将脚本哈希编译为一个地址, 以“3”为前缀,该地址与一个脚本相对应而非与一个公钥相对应,但是它的效果与比特币地址支付别无二致。
不能将P2SH植入P2SH赎回脚本,因为P2SH不能自循环。虽然在技术上可以将RETURN包含在赎回脚本中,但由于规则中没有策略阻止来, 因此在验证期间执行RETURN将导致交易被标记为无效.
P2SH锁定脚本脚本对于赎回脚本本身未提供任何描述。P2SH交易即便在赎回脚本无效的情况下也会被认为有效, 这时可能会被锁死在P2SH这个交易中,导致不能花费这笔比特币.
运用比特币的区块链技术存储与比特币支付不相关数据, 例如,为文件记录电子指纹,则任何人都可以通过该机制在特定的日期建立关于文档存在性的证明。
此类交易仅将比特币地址当作自由组合的20个字节而使用,进而会产生不能用于交易的UTXO。因为比特币地址只是被当作数据使用,并不与私钥相匹配,所以会导致UTXO不能被用于交易,因而是一种伪支付行为。因此,这些交易永远不会被花费,所以永远不会从UTXO集中删除,并导致UTXO数据库的大小永远增加或“膨胀”
Return允许开发者在交易输出上增加80字节的非交易数据。与伪交易型的UTXO不同,Return创造了一种明确的可复查的非交易型输出,此类数据无需存储于UTXO集。Return输出被记录在区块链上,会消耗磁盘空间,也会导致区块链规模的增加,但它们不存储在UTXO集中,因此也不会使得UTXO内存膨胀.
RETURN 不涉及可用于支付的解锁脚本的特点, RETURN 不能使用其输出中所锁定的资金,因此没有必要记录在蕴含潜在成本的UTXO集中,所以 RETURN 实际是没有成本的。
RETURN 常为一个金额为0的比特币输出, 因为任何与该输出相对应的比特币都会永久消失。假如一笔 RETURN 被作为一笔交易的输入,脚本验证引擎将会阻止验证脚本的执行,将标记交易为无效
时间锁是只允许在一段时间后才允许支出的交易,锁定时间也称为nLocktime.
试想, 如果 A 支付 B 一个交易, nLocktime 为3个月后, 那么 B 3个月后才可用这个 UTXO , 如果 A这时再将原来输入的 UTXO 用于其他交易,那么 B 3个月后就不能用了.
因此,时间限制必须放在UTXO本身上,并成为锁定脚本的一部分,而不是交易。可通过时间锁定的一种形式检查锁定时间验证(CLTV)
来实现.
通过在输出的赎回脚本中添加CLTV操作码来限制输出,从而只能在指定的时间过后使用. CLTV不会取代nLocktime,而是限制特定的UTXO,并通过将nLocktim设置为更大或相等的值,从而达到在未来才能花费这笔钱的目的。
一个 P2SH 交易的赎回脚本如下: Alice 转给 Bob的钱, 3个月才到1
<now+3 months>CHECKLOCKTIMEVERIFYDROP DUP HASH160 <Bob'sPublicKeyHash> EQUALVERIFY CHECKSIG
如果 Bob 尝试引用(花费)这个 UTXO,他使用他的签名和公钥在该输入的解锁脚本,并将交易nLocktime设置为等于或更大于Alice设置的CHECKLOCKTIMEVERIFY 时间锁。然后Bob在比特币网络上广播交易。
矿工对交易评估如下:
如果Alice设置的CHECKLOCKTIMEVERIFY参数小于或等于支出交易的nLocktime,脚本执行将继续(就好像执行“无操作”或NOP操作码一样). 否则,CHECKLOCKTIMEVERIFY失败并停止执行,标记交易无效:
nLocktime和CLTV都是绝对时间锁定,它们指定绝对时间点。
它们允许将两个或多个相互依赖的交易链接在一起,同时对依赖于从先前交易的确认所经过的时间的一个交易施加时间约束。换句话说,在UTXO被记录在块状块之前,时钟不开始计数。这个功能在双向状态通道和闪电网络中特别有用
交易级相对时间锁定是作为对每个交易输入中设置的交易字段nSequence的值的共识规则实现的。脚本级相对时间锁定使用CHECKSEQUENCEVERIFY(CSV)操作码实现。
在每个输入中加多一个nSequence字段来设置此类相对时间锁. ,如果输入的交易的序列值小于2^32 (0xFFFFFFFF),就表示尚未“确定”的交易。
nSequence的原始含义从未被正确实现,并且在不利用时间锁定的交易中nSequence的值通常设置为$2^{32}$. 对于具有nLocktime或CHECKLOCKTIMEVERIFY的交易,nSequence值必须设置为小于$2^{32}$, 以使时间锁定器有效。通常设置为$2^{32}-1\
\ (0xFFFFFFFE)$。
一笔输入交易,当输入脚本中的nSequence值小于2^31时,就是相对时间锁定的输入交易。
交易可以包括时间锁定输入(nSequence <2^31)和没有相对时间锁定(nsequence> =2^31)的输入。 nSequence值以块或秒为单位, 类型标志用于区分计数块和计数时间(以秒为单位)的值。类型标志设置在第23个最低有效位(即值1 << 22)。如果设置了类型标志,则nSequence值将被解释为512秒的倍数。如果未设置类型标志,则nSequence值被解释为块数。2^31)和没有相对时间锁定(nsequence>
当将nSequence解释为相对时间锁定时,只考虑16个最低有效位。一旦评估了标志(位32和23),nSequence值通常用16位掩码(例如nSequence&0x0000FFFF)“屏蔽”。
脚本操作码, 在UTXO的赎回脚本中评估时,CSV操作码仅允许在输入nSequence值大于或等于CSV参数的交易中进行消耗。
在比特币中, 墙上时间(wall time)和共识时间之间存在微妙但非常显著的差异。比特币是一个分散的网络,这意味着每个参与者都有自己的时间观。网络上的事件不会随时随地发生。网络延迟必须考虑到每个节点的角度。最终,所有内容都被同步,以创建一个共同的分类帐。
通过取最后11个块的时间戳并计算其中位数作为“中位时间过去”的值,作为共识时间,并被用于所有的时间计算. 通过这个方法,没有一个矿工可以利用时间戳从具有尚未成熟的时间段的交易中获取非法矿工费。
可以控制流量.
由于比特币脚本语言是一种堆栈语言, 则其条件控制如下1
2
3
4
5
6
7truecondition
IF
truetruecodetorunwhenconditionistrue
ELSE
truetruecodetorunwhenconditionisfalse
ENDIF
codetorunineithercase
另外也有带有VERIFY操作码的条件子句
任何以VERIFY结尾的操作码。 VERIFY后缀表示如果评估的条件不为TRUE,脚本的执行将立即终止,并且该交易被视为无效。VERIFY后缀充当保护子句,只有在满足前提条件的情况下才会继续。
如1
HASH160<expectedhash>EQUALVERIFY<Bob'sPubkey>CHECKSIG
等同于1
2
3
4HASH160<expectedhash>EQUAL
IF
truetruetrue<Bob'sPubkey>CHECKSIG
ENDIF
使用IF的脚本与使用具有VERIFY后缀的操作码相同; 他们都作为保护条款。 然而,VERIFY的构造更有效率,使用较少的操作码。
诸如EQUAL之类的操作码会将结果(TRUE / FALSE)推送到堆栈上,留下它用于后续操作码的评估。 相比之下,操作码EQUALVERIFY后缀不会在堆栈上留下任何东西。 在VERIFY中结束的操作码不会将结果留在堆栈上。
在多重签名, 赎回脚本中使用
赎回脚本1
2
3
4
5IF
true<Alice'sPubkey>CHECKSIG
ELSE
<Bob'sPubkey>CHECKSIG
ENDIF
而条件应该在解锁脚本中,
Alice用解锁脚本<Alice's Sig> 1
,
Bob 用解锁脚本<Bob's Sig> 0
一个复杂的例子
多重签名的计划的参与者是Mohammed,他的两个合作伙伴Saeed和Zaira,以及他们的公司律师Abdul。三个合作伙伴根据多数规则作出决定,因此三者中的两个必须同意。然而,如果他们的钥匙有问题,他们希望他们的律师能够用三个合作伙伴签名之一收回资金。最后,如果所有的合作伙伴一段时间都不可用或无行为能力,他们希望律师能够直接管理该帐户。
具有时间锁定(Timelock)变量的多重签名1
2
3
4
5
6
7
8
9
10
11
12
13IF
truetrueIF
truetruetruetrue2
truetrueELSE
truetruetruetrue<30days>CHECKSEQUENCEVERIFYDROP
truetruetruetrue<AbdultheLawyer'sPubkey>CHECKSIGVERIFY
1
ENDIF
<Mohammed'sPubkey><Saeed'sPubkey><Zaira'sPubkey>3CHECKMULTISIG
ELSE
truetrue<90days>CHECKSEQUENCEVERIFYDROP
truetrue<AbdultheLawyer'sPubkey>CHECKSIG
ENDIF
第二个执行路径只能在UTXO创建30天后才能使用。 那时候,它需要签署Abdul(律师)和三个合作伙伴之一(三分之一)。
解锁第二个执行路径的脚本(Lawyer + 1-of-3)
1 | 0<Saeed'sSig><Abdul'sSig>FALSETRUE |
**
**
P2P是指位于同一网络中的每台计算机都彼此对等,各个节点共同提供网络服务,不存在任何“特殊”节点。每个网络节点以“扁平(flat)”的拓扑结构相互连通。 在P2P网络中不存在任何服务端(server)、中央化的服务、以及层级结构。P2P网络的节点之间交互运作、协同处理:每个节点在对外提供服务的同时也使用网络中其他节点所提供的服务。P2P网络也因此具有可靠性、去中心化,以 及开放性。
尽管比特币P2P网络中的各个节点相互对等,但是根据所提供的功能不同,各节点可能具有不同的角色。每个比特币节点都是路由、区块链数据库、挖矿、钱包服务的功能集合。
全节点含有 区块链的完整拷贝, 而轻量级结点只有一部分, 交易验证的方式是 简单支付验证(SPV)
常见结点类型
运行比特币P2P协议的比特币主网络由大约5000-8000个运行着不同版本比特币核心客户端(Bitcoin Core)的监听节 点、以及几百个运行着各类比特币P2P协议的应用(例如BitcoinClassic, Bitcoin Unlimited, BitcoinJ, Libbitcoin, btcd, and bcoin等)的节点组成。比特币P2P网络中的一小部分节点也是挖矿节点,它们竞争挖矿、验证交易、并创建新的区块。许多连接到比特币网络的大型公司运行 着基于Bitcoin核心客户端的全节点客户端,它们具有区块链的完整拷贝及网络节点,但不具备挖矿及钱包功能。这些节点是网络中的边缘路由器(edgerouters),通过它们可以搭建其他服务,例如交易所、钱包、区块浏览器、商家支付处理(merchant payment processing)等
当新的网络节点启动后,为了能够参与协同运作,它必须发现网络中的其他比特币节点。新的网络节点必须发现至少一个网络中存在的节点并建立连接。由于比特币网络的拓扑结构并不基于节点间的地理位置,因此各个节点之间的地理信息完全无关。在新节点连接时,可以随机选择网络中存在的比特币节点与之相连。
节点通常采用TCP协议、使用8333端口. 。在建立连接时,该节点会通过发送一条包含基本认证内容的version消息开始“握手”通信过 程. 包括如下内容:
接收版本消息的本地对等体将检查远程对等体报告的nVersion,并确定远端对等体是否兼容。 如果远程对等体兼容,则本地对等体将确认版本消息,并通过发送一个verack建立连接。
当建立一个或多个连接后,新节点将一条包含自身IP地址的addr消息发送给其相邻节点。相邻节点再将此条addr消息依 次转发给它们各自的相邻节点,从而保证新节点信息被多个节点所接收、保证连接更稳定。然后,新接入的节点可以向 它的相邻节点发送getaddr消息,要求它们返回其已知对等节点的IP地址列表。
节点必须连接到若干不同的对等节点才能在比特币网络中建立通向比特币网络的种类各异的路径(path)。由于节点可以随时加入和离开,通讯路径是不可靠的。因此,节点必须持续进行两项工作:在失去已有连接时发现新节点,并在其他节点启动时为其提供帮助。节点启动时只需要一个连接,因为第一个节点可以将它引荐给它的对等节点,而这些节点又会进一步提供引荐。一个节点,如果连接到大量的其他对等节点,这既没必要,也是对网络资源的浪费。在启动完成 后,节点会记住它最近成功连接的对等节点;因此,当重新启动后它可以迅速与先前的对等节点网络重新建立连接。如果先前的网络的对等节点对连接请求无应答,该节点可以使用种子节点进行重启动。
如果已建立的连接没有数据通信,所在的节点会定期发送信息以维持连接。如果节点持续某个连接长达90分钟没有任何通信,它会被认为已经从网络中断开,网络将开始查找一个新的对等节点
对于全节点, 需要同步备份整个区块链..
此过程从发送version消息开始,这是因为该消息中含有的BestHeight字段标示了一个节点当前的区块链高度(区块数量)。对等节点们会交换一个getblocks消息,其中包含他们本地区块链的顶端区块哈希值(指纹)。如果某个对等节点识别出它接收到的哈希值并不属于顶端区块,而是属于一个非顶端区块的旧区块,那么它就能推断出:其自身的本地区块链比其他对等节点的区块链更长。
拥有更长区块链的对等节点,识别出第 一批可供分享的500个区块,通过使用inv(inventory)消息把这些区块的哈希值传播出去。缺少这些区块的节点便可以 通过各自发送的getdata消息来请求得到全区块信息,用包含在inv消息中的哈希值来确认是否为正确的被请求的区块, 从而读取这些缺失的区块。
SPV节点只需下载区块头,而不用下载包含在每个区块中的交易信息。由此产生的不含交易信息的区块链,大小只有完整区块链的1/1000。SPV节点不能构建所有可用于消费的UTXO的全貌. SPV节点验证交易时依赖对等节点“按需”提供区块链相关部分的局部视图。
如要检查第300000号区块的某个交易, SPV节点会在该交易信息和它所在区块之间用merkle路径建立一条链接。然后SPV节点一直等待,直到序号从300,001到300,006的六个区块堆叠在该交易所在的区块之上,并通过确立交易的深度是在第300,006区块~第300,001区块之下来验证交易的有效性。
SPV节点可以证实某个交易的存在性,但它不能验证某个交易(譬如同一个UTXO的双重支付)不存在,这是因为SPV节点没有一份关于所有交易的记录。这个漏洞会被针对SPV节点的拒绝服务攻击或双重支付型攻击所利用。为了防御这些攻击,SPV节点需要随机连接到多个节点,以增加与至少一个可靠节点相连接的概率。这种随机连接的需求意味着SPV节 点也容易受到网络分区攻击或Sybil攻击。在后者情况中,SPV节点被连接到虚假节点或虚假网络中,没有通向可靠节点或真正的比特币网络的连接。
SPV节点对特定数据的请求可能无意中透露了钱包里的地址信息。例如,监控网络的第三方可以跟踪某个SPV节点上的钱包所请求的全部交易信息,并且利用这些交易信息把比特币地址和钱包的用户关联起来,从而损害了用户的隐私。
Bloom过滤器通过一个采用概率而不是固定模式的过滤机制,允许SPV节点只接收交易信息的子集,同时不会精确泄露哪些是它们感兴趣的地址。
Bloom过滤器可以让SPV节点指定交易的搜索模式,该搜索模式可以基于准确性或私密性的考虑被调节。如果过滤器只包含简单的关键词,更多相应的交易会被搜索出来,在包含若干无关交易的同时有着更高的私密性。
构成:
一个可变长(N)的 二进制数组, 数组初始值为0, . 一组数量可变(M)的哈希函数, 哈希函数输出为 1—-N, 对应数组,且为确定性函数.
算法如下
记数组 arr[N] , M个hash函数 $hs=\{h_1,h_2,\ldots,h_M\}$
关键字 $keys = \{k_1,k_2,\ldots,k_i\}$
过滤器记录关键字过程1
2
3
4arr[N]={0} # initialization
for key in keys:
for hash in hs:
arr[hash(key)] = 1
判断一个关键字是否被过滤器记录: 将关键字分别代入各hash 函数 计算对比 arr 对应的值, 如果有0, 则没有被记录, 如果全为1, 则 可能 被记录.(基于概率)
数组置0, 然后SPV节点将列出所有感兴趣的地址,密钥和散列,它将通过从其钱包控制的任何UTXO中提取公钥哈希和脚本哈希和交易ID来实现。 SPV节点然后将其中的每一个添加到Bloom过滤器,以便如果这些模式存在于交易中,则Bloom过滤器将“匹配”,而不会自动显示模式。
然后,SPV节点将向对等体发送一个过滤器加载消息,其中包含在连接上使用的bloom过滤器。在对等体上,针对每个传入交易检查Bloom过滤器。完整节点根据bloom过滤器检查交易的几个部分,寻找匹配,
只有与过滤器匹配的交易才会发送到节点。响应于来自节点的getdata消息,对等体将发送一个merkleblock消息,该消息仅包含与过滤器匹配的块和每个匹配交易的merkle路径。然后,对等体还将发送包含由过滤器匹配的交易的tx消息。
比特币网络中几乎每个节点都会维护一份未确认交易的临时列表,被称为内存池或交易池
有些节点的实现还维护一个单独的孤立交易池。如果一个交易的输入与某未知的交易有关,如与缺失的父交易相关,该 孤立交易就会被暂时储存在孤立交易池中直到父交易的信息到达。当一个交易被添加到交易池中,会同时检查孤立交易池,看是否有某个孤立交易引用了此交易的输出(子交易)。
交易池和孤立交易池(如有实施)都是存储在本地内存中,并不是存储在永久性存储设备(如硬盘)里。
平均每个区块至少包含超过500个交易
区块头由三组区块元数据组成:
因为创世区块
被编入到比特币客户端软件里,所以每一个节点都始于至少包含一个区块的区块链,这能确保创世区块不会被改变。每一个节点都“知道”创世区块的哈希值、结构、被创建的时间和里面的一个交易。因此,每个节点都把该区块作为区块链的首区块,从而构建了一个安全的、可信的区块链。
在比特币网络中,Merkle树
被用来归纳一个区块中的所有交易,同时生成整个交易集合的数字指纹,且提供了一种校验区块是否存在某交易的高效途径。。如果仅有奇数个交易需要归纳,那最后的交易就会被复制一份以构成偶数个叶子节点,
不需要下载整个区块而通过Merkle路径去验证交易的存在,又被称作简单支付验证.
一个SPV节点想知道它钱包中某个比特币地址即将到达的支付。该节点会在节点间的通信链接上建立起bloom过滤器,限制只接受含有目标比特币地址的交易。当对等体探测到某交易符合bloom过滤器,它将以Merkleblock消息的形式发送该区块。Merkleblock消息包含区块头和一条连接目标交易与Merkle根的Merkle路径。SPV节点能够使用该路径找到与该交易相关的区块,进而验证对应区块中该交易的有无。SPV节点同时也使用区块头去关联区块和区块链中的其余区块。这两种关联,交易与区块、区块和区块链,就可以证明交易存在于区块链。简而言之,SPV节点会收到少于1KB的有关区块头和Merkle路径的数据,其数据量比一个完整的区块(目前大约有1MB)少了一千多倍。\
比特币的测试区块链
开发过程:首先在regtest上部署每个变更,然后在testnet上进行测试,最后实现生产,部署到比特币网络上。
挖矿巩固了去中心化的清算交易机制,通过这种机制,交易得到验证和清算,实现去中心化的安全机制,是P2P数字货币的基础。
矿工们在挖矿过程中会得到两种类型的奖励:创建新区块的新币奖励,以及区块中所含交易的交易费。矿工们争相完成一种基于加密哈希算法的数学难题,这些难题的答案包括在新区块中,作为矿工的计算工作量的证明,被称为”“工作量证明”。该算法的竞争机制以及获胜者有权在区块链上进行交易记录的机制,这二者是比特币安全的基石。
比特币的去中心化共识由所有网络节点的4种独立过程相互作用而产生:
它们之间如何相互作用并达成全网的自发共识,从而使任意节点组合出 它自己的权威、可信、公开的总帐副本。
在交易传递到临近的节点前,每一个收到交易的比特币节点将会首先验证该交易,这将确保只有有效的交易才会 在网络中传播,而无效的交易将会在第一个节点处被废弃。
验证交易后,比特币节点会将这些交易添加到自己的内存池中。内存池也称作交易池,用来暂存尚未被加入到区块的交 易记录。
例如:
Jing节点的区块链已经收集到了区块277,314,并继续监听着网络上的交易,在尝试挖掘新区块的同时,也监 听着由其他节点发现的区块。这时他从比特币网络收到了区块277,315, 标志着终结了产出区块277,315竞赛,与此同时也是产出区块277,316竞赛的开始。
在上一个10分钟内,当Jing的节点正在寻找区块277,315的解的同时,他也在收集交易记录为下一个区块做准备。目前 它已经收到了几百笔交易记录,并将它们放进了内存池。直到接收并验证区块277,315后,Jing的节点会检查内存池中 的全部交易,并移除已经在区块277,315中出现过的交易记录,确保任何留在内存池中的交易都是未确认的,等待被记 录到新区块中。
Jing的节点立刻构建一个新的空区块,做为区块277,316的候选区块。称作候选区块是因为它还没有包含有效的工作量证明(计算出合适的 nonce),不是一个有效的区块,而只有在矿工成功找到一个工作量证明解之后,这个区块才生效。现在,Jing的节点从内存池中整合到了全部的交易,新的候选区块包含有418笔交易,总的矿工费为0.09094925个比特币。
每个区块中的第一笔交易是笔特殊交易,称为创币交易或者coinbase交易
与常规交易不同,创币交易没有输入,不消耗UTXO。它只包含一个被称作coinbase的输入,仅仅用来创建新的比特 币。创币交易有一个输出,支付到这个矿工的比特币地址。
为了构造创币交易,矿工节点需要计算如下
定义了所需满足的工作量证明的难度
。难度在区块中以“尾数-指数”的格式,编码并存储,这种格式称作target-bits(难度位)。首字节表示指数(exponent),后面的3字节表示尾数(系数)(coefficient)。则$\text{difficulty} = cofficient 2^{8{(exponent-3)} }$构造区块 nonce 如下1
2
3
4nonce = 0
while 1:
if hash(blockHead)<Target:break # nonce in blockHead
else: CHANGE NONCE # eg nonce+=1
构造好之后, 挖矿节点立刻将这个区块发给它的所有相邻节点。这些节点在接收并验证这个新区块后,也会继续传播此区块。当这个新区块在网络中扩散时,每个节点都会将它加入自己的区块链副本中。其他挖矿结点就放弃之前对构建这个相 同高度区块的计算,并立即开始计算区块链中下一个区块的工作。
前面清单列出了一些, 也可以通过 客户端的 CheckBlock, CheckBlockHead,查看
为什么矿工不为他们自己记录一笔交易去获得数以千计的比特币?这是因为每一个节点根据相同的规则对区块进行校验。一个无效的coinbase交易将使整个区块无效,这将导致该区块被拒 绝,因此,该交易就不会成为总账的一部分。矿工们必须构建一个完美的区块,基于所有节点共享的规则,并且根据正 确工作量证明的解决方案进行挖矿,他们要花费大量的电力挖矿才能做到这一点。如果他们作弊,所有的电力和努力都 会浪费。这就是为什么独立校验是去中心化共识的重要组成部分。
构建了一个候选区块,然后求解工作量证明算法以使这个区块有效。
每次改变 nonce, 尝试产生一个随机的结果,但是任何可能的结果的概率可以预先计算。因此,指定特定难度(Target)的结果构成了具体的工作量证明。
验证nonce 哈希值只需要一次计算,而我们找到它却花了很多次。知道目标值后,任何人都可以用统计学来估算其难度,因此就能知道找到这个nonce需要多少工作。
按当前比特币系统的难度,矿工得试$10^15$次才能找到一个合适的nonce使区块头信息哈希值足够小。
在验证过程中,一旦发现有不符合标准的地方,验证就会失败,这样区块会被节点拒绝,所以也不 会加入到任何一条链中。
任何时候,主链都是累计了最多难度的区块链。在一般情况下,主链也是包含最多区块的那个链,除非有两个等长的链 并且其中一个有更多的工作量证明。主链也会有一些分支,这些分支中的区块与主链上的区块互为“兄弟”区块。这些区 块是有效的,但不是主链的一部分。
新区块所延长的区块链并不是主链,节点将新的区块添加到备用链,同时比较备用链与主链的难度。如果备用链比主链积累了更多的难度,节点将收敛于备用链,意味 着节点将选择备用链作为其新的主链,而之前那个老的主链则成为了备用链。
如果节点收到了一个有效的区块,而在现有的区块链中却未找到它的父区块,那么这个区块被认为是“孤块”。孤块会被 保存在孤块池中,直到它们的父区块被节点收到。
比特币将区块间隔设计为10分钟,是在更快速的交易确认和更低的分叉概率间作出的妥协。更短的区块产生间隔会让交 易清算更快地完成,也会导致更加频繁地区块链分叉。
难度增长后, nonce 值不够, 可以延后时间戳来解决, 但是如果延后太久, 可能导致区块无效, 更好的解决方案是利用 coinbase 这笔交易中的空间(coinbase 脚本可以存储2-100bytes 数据), 而且这笔交易会影响 merkle 根的变化.
个人矿工在建立矿池账号后,设置他们的矿机连接到矿池服务器。他们的挖矿设备在挖矿时保持和矿池服务器的连接,和其他矿工同步各自的工作。这样,矿池中的矿工分享挖矿任务,之后分享奖励。成功出块的奖励支付到矿池的比特币地址,而不是单个矿工的。一旦奖励达到一个特定的阈值,矿池服务器便会定期支 付奖励到矿工的比特币地址。
大部分矿池是“托管的”,有一个公司或者个人经营一个矿池服务器。矿池服务器的所有者叫矿池管理员,同时他 从矿工的收入中收取一个百分比的费用。矿池服务器运行专业软件以及协调池中矿工们活动的矿池采矿协议。矿池服务器同时也连接到一个或更多比特币完全节点并直接访问一个块链数据库的完整副本。这使得矿池服务器可以代替矿池中的矿工验证区块和交易,缓解他们运行一个完整节点的负担.
托管矿池存在管理人作弊的可能,管理人可以利用矿池进行双重支付或使区块无效, 此外,中 心化的矿池服务器代表着单点故障。如果因为拒绝服务攻击服务器挂了或者被减慢,池中矿工就不能采矿。
P2Pool是一个点对点的矿池,没有中心管理 人。P2Pool通过将矿池服务器的功能去中心化,实现一个并行的类似区块链的系统,名叫份额链(share chain)
。
一个份额链是一个难度低于比特币区块链的区块链系统。份额链允许池中矿工在一个去中心化的池中合作,采矿,并获得份额。份额链上的区块记录了贡献工作的矿工的份额,并且继承了之前份额区块上的份额记录。当一 个份额区块上还实现了比特币网络的难度目标时,它将被广播并包含到比特币的区块链上,并奖励所有已经在份额链区块中取得份额的池中矿工。
比特币的共识机制的前提: 绝大多数的矿工,出于自己利益最大化的考虑,都会通过诚实地挖矿来维持整个比特币系统。
当一个或者一群拥有了整个系统中大量算力的矿工出现, 可以通过攻击比特币的共识机制来达到破坏比特币网络的安全性和可靠性的目的。
注意, ,共识攻击只能影响整个区块链未来的共识,即最多影响 过去10个块。而且随着时间的推移,整个比特币块链被篡改的可能性越来越低。
共识攻击也 不能从其他的钱包那里偷到比特币、不签名地支付比特币、重新分配比特币、改变过去的交易或者改变比特币持有纪 录。共识攻击能够造成的唯一影响是影响最近的区块(最多10个)并且通过拒绝服务来影响未来区块的生成。
区块链分叉/双重支付攻击指的是攻击者通过 不承认最近的某个交易,并在这个交易之前重构新的块,从而生成新的分叉,继而实现双重支付。双重支付只能在攻击者拥有的钱包所发生的交易上进行,因为只有钱包的拥有者才能生成一个合法的签名用 于双重支付交易。攻击者在自己的交易上进行双重支付攻击,如果可以通过使交易无效而实现对于不可逆转的购买行为不予付款,这种攻击就是有利可图的。
51%攻击并不是像它的命名里说的那样,攻击者需要至少51%的算力才能发起,实际上,即使其拥有不到51%的系统算力,依然可以尝试发起这种攻击。之所以命名为51%攻击,只是因为在攻击者的算力达到51%这个阈值的时候,其发起的攻击尝试几乎肯定会成功。
导致硬分叉:共识规则中的错误,以及对共识规则的故意修改。
对于硬分叉发生,必须是由于采取相互竞争的实施方案,并且规则需要由矿工,钱包和中间节点激活。相反,有许多比特币核心的替代实现方案,甚至还有软分叉,这些没有改变共识规则,阻止发生错误,可以在网络上共存并互操作,最终并未导致硬分叉。
可以将硬分叉子看成四个阶段:软分叉,网络分叉,挖矿分叉和区块链分叉
。该过程开始于开发人员创建的客户端,这个客户端对共识规则进行了修改。当这种新版本的客户端部署在网络中时,一定百分比的矿工,钱包用户和中间节点可以采用并运行该版本客户端。得到的分叉将取决于新的共识规则是否适用于区块,交易或系统其他方面。如果新的共识规则与交易有关,那么当交易被挖掘成一个块时,根据新规则创建交易的钱包可能会产生出一个网络分叉,这就是一个硬分叉。如果新规则与区块有关,那么当一个块根据新规则被挖掘时,硬分叉进程将开始。
一些开发商反对任何形式的硬叉,认为它太冒险了。另一些人认为硬分叉机制是提升共识规则的重要工具,避免了“技术债务”,并与过去提供了一个干净的了断
共识规则的改变也能够让未修改的客户端仍然按照先前的规则对待交易或者区块
软分叉级只能用于增加共识规则约束,而不是扩展它们。软叉可以通过多种方式实现,方法的共同点是不要求所有节点升级或强制非升级节点必须脱离共识。
如
对软分叉的批评
比特币的核心准则是去中心化,将责任和控制权都移交给了用户。由于网络的安全性是基于工作量证明而非访问控制,比特币网络可以对所有人开放,也无需对比特币传输进行加密。
一笔比特币交易只授权向指定接收方发送一个指定数额,并且不能被修改或伪造。它不会透露任何个人信息,例如当事人的身份,也不能用于权限外的支付。因此,比特币的支付网络并不需要加密或防窃听保护
比特币的安全性依赖于密钥的分散性控制,并且需要矿工们各自独立地进行交易验证。如果想利用好比特币的安全性,确保自己处于比特币的安全模型里。简而言之,不要将用户的密钥控制权拿走,不要接受非区块链交易信息。一个常见的错误是接受区块链离线交易,妄图减少交易费或加速交易处理速度。一个“区块链离线交易”系统将交易数据记录在一个内部的中心化账本上,然后偶尔将它们同步到比特币区块链中。这种做法,再一次,用专制和集中的方式取 代比特币的去中心化安全模型。当数据处于离线的区块链上的时候,保护不当的中心化账本里的资金可能会不知不觉被 伪造、被挪用、被消耗。
除非你是准备大力投资运营安全,叠加多层访问控制,或(像传统的银行那样)加强审计,否则将资金从比特币的去中心化安全场景中抽离出来这样的设计也仅仅是复制了一个脆弱不堪,深受账户盗窃威胁、贪污和挪用公款困扰的传统金融网络而已。要想充分利用比特币特有的去中心化安全模型,必须避免中心化架构的常见诱惑,因它最终将摧毁比特币的安全性。
传统的安全体系的基础,它指的总体系统或应用程序中一个可信赖的安全核心。安全体系像一圈同心圆一样围绕着信任根源来进行开发,像层层包裹的洋葱一样,信任从内至外依次延伸。
]]>商业活动参与者首先要寻找一个多方均信任的第三方来记账, 确保交易的准确.
可以很容易设计出一个简单粗暴的分布式记账结构,如下图。多方均允许对账本进行任意读写,一旦发生新的交易即追加到账本上。这种情况下,如果参与多方均诚实可靠,则该方案可以正常工作;但是一旦有参与方恶意篡改已发生过的记录,则无法确保账本记录的正确性。
为防止恶意篡改, 可以引入验证机制. 使用数字摘要技术(digital digest)
. 每当有新交易记录被追加到账本上, 记录前面交易历史的 hash 值, 此后每个时刻, 参与者都可以重新计算 hash, 看是否与记录的 hash 匹配. 不匹配说明修改过, 也可以容易地定位修改的交易记录了
不必要每次都计算前面所有历史的 hash, 可以计算 上次的 hash 加上当前交易 的 内容的 hash
这正是一个区块链结构.
分布式记账问题为何重要?可以类比互联网出现后对社会带来的重大影响。
互联网是人类历史上最大的分布式互联系统。作为信息社会的基础设施,它很好地解决了传递信息的问题。然而,由于早期设计上的缺陷,互联网无法确保所传递信息的可靠性,这大大制约了人们利用互联网进行大规模协作的能力。而以区块链为基础的分布式账本科技则可能解决传递可信信息的问题。这意味着基于分布式账本科技的未来商业网络,将成为新一代的文明基础设施——大规模的协作网络。
分布式账本科技的核心价值在于为未来多方协同网络提供可信基础。区块链引发的记账科技的演进,将促使商业协作和组织形态发生变革。
可能带来的业务特性
狭义上,区块链是一种以区块为基本单位的链式数据结构,区块中利用数字摘要对之前的交易历史进行校验,适合分布式记账场景下防篡改和可扩展性的需求。
广义上,区块链还指代基于区块链结构实现的分布式记账技术,还包括分布式共识、隐私与安全保护、点对点通信技术、网络协议、智能合约等。
在实现上, 首先假设存在一个分布式的数据记录账本,只允许添加,不允许删除.
首先,比特币客户端发起一项交易,广播到比特币网络中并等待确认。网络中的节点会将一些收到的等待确认的交易记录打包在一起(此外还要包括前一个区块头部的哈希值等信息),组成一个候选区块。然后,试图找到一个 nonce 串(随机串)放到区块里,使得候选区块的哈希结果满足一定条件(比如小于某个值)。这个nonce 串的查找需要一定的时间进行计算尝试。
一旦节点算出来满足条件的 nonce 串,这个区块在格式上就被认为是“合法”了,就可以尝试在网络中将它广播出去。其它节点收到候选区块,进行验证,发现确实符合约定条件了,就承认这个区块是一个合法的新区块,并添加到自己维护的区块链上。当大部分节点都将区块添加到自己维护的区块链结构上时,该区块被网络接受,区块中所包括的交易也就得到确认。
这种基于算力寻找 nonce 串的共识机制成为 PoW(Proof of Work). (还有很多其他共识机制 PoX, 如 PoS (stake)…)
比特币区块链支持简单的脚本计算, 仅限于数字画笔相关的处理. 还可以将区块链上执行的处理过程进一步泛化,即提供 智能合约 Smart Contract. 由此提供除货币交易功能外更灵活的合约共功能,执行更为复杂的操作.
指标: 容错的结点比例, 决策收敛速度, 出错后的恢复,动态特性等.
不能简单得增加结点来扩展整个系统的处理能力.
对于比特币和以太坊区块链而言,网络中每个参与维护的核心节点都要保持一份完整的存储,并且进行智能合约的处理。此时,整个网络的总存储和计算能力,取决于单个节点的能力。甚至当网络中节点数过多时,可能会因为一致性的达成过程延迟降低整个网络的性能。尤其在公有网络中,由于大量低性能处理节点的存在,问题将更加明显。
要解决这个问题,根本上是放松对每个节点都必须参与完整处理的限制(当然,网络中节点要能合作完成完整的处理),这个思路已经在超级账本中得到应用;同时尽量减少核心层的处理工作。
在联盟链模式下,还可以专门采用高性能的节点作为核心节点,用相对较弱的节点仅作为代理访问节点。
区块链网络中的大量信息需要写到文件和数据库中进行存储。
预测将可能出现更具针对性的“块数据库(BlockDB)”,专门服务类似区块链这样的新型数据业务,其中每条记录将包括一个完整的区块信息,并天然地跟历史信息进行关联,一旦写入确认则无法修改。所有操作的最小单位将是一个块。为了实现这种结构,需要原生支持高效的签名和加解密处理。
区块链不等于数据库。虽然区块链也可以用来存储数据,但它要解决的核心问题是多方的互信问题。单纯从存储数据角度,它的效率可能不高,不建议把大量的原始数据放到区块链系统上。
区块链带来的潜在优势包括降低交易成本、减少跨组织交易风险等。
区块链平台将可能提供前所未有规模的相关性极高的数据,这些数据可以在时空中准确定位,并严格关联到用户。因此,基于区块链提供数据进行征信管理,将大大提高信用评估的准确率,同时降低评估成本
另外,跟传统依靠人工的审核过程不同,区块链中交易处理完全遵循约定自动化执行。基于区块链的信用机制将天然具备稳定性和中立性。
区块链技术可以用于产权、版权等所有权的管理和追踪。其中包括汽车、房屋、艺术品等各种贵重物品的交易等,也包括数字出版物,以及可以标记的数字资源。
目前权属管理领域存在的几个难题是:
利用区块链技术,物品的所有权是写在数字链上的,谁都无法修改。并且一旦出现合同中约定情况,区块链技术将确保合同能得到准确执行。这能有效减少传统情况下纠纷仲裁环节的人工干预和执行成本
相比于依赖于中间方的资源共享模式,基于区块链的模式有潜力更直接的连接资源的供给方和需求方,其透明、不可篡改的特性有助于减小摩擦。
区块链技术可以帮助自动化国际贸易和物流供应链领域中繁琐的手续和流程。基于区块链设计的贸易管理方案会为参与的多方企业带来极大的便利。另外,贸易中销售和法律合同的数字化、货物监控与检测、实时支付等方向都可能成为创业公司的
物联网络中每一个设备分配地址,给该地址所关联一个账户,用户通过向账户中支付费用可以租借设备,以执行相关动作,从而达到租借物联网的应用。
典型的应用包括 PM2.5 监测点的数据获取、温度检测服务、服务器租赁、网络摄像头数据调用等等。
另外,随着物联网设备的增多、边沿计算需求的增强,大量设备之间形成分布式自组织的管理模式,并且对容错性要求很高。区块链自身分布式和抗攻击的特点可以很好地融合到这一场景中。
对于系统中的多个服务结点,给定一系列操作, 在协议(某种共识算法)保障下, 使得它们对处理结果达成某种程度的一致存在的问题
解决的基本思想: 将可能引发不一致的并行操作串行化
分布式系统一致性应满足
理想情况的强一致性是很难达到的. 其实实际需求并没有那么强,可以适当放宽一致性要求.
由于响应请求往往存在时延、网络会发生中断、节点会发生故障、甚至存在恶意节点故意要破坏系统, 不能简单地通过多播过程投票.
一般地,把故障(不响应)的情况称为“非拜占庭错误”,恶意响应的情况称为“拜占庭错误”(对应节点为拜占庭节点)。
这种算法解决的是对于 分布式系统中存在故障(fault), 但不存在恶意(corrupt)结点场景(即可能消息丢失或重复, 但无错误信息)下的共识达成(consensus)问题.
在网络可靠,存在节点失效(即便只有一个)的最小化异步模型系统中,不存在一个可以解决一致性问题的确定性算法。
即一个可扩展的分布式系统的共识问题的下限是无解
它告诉人们,不要浪费时间去为异步分布式系统设计在任意场景下都能实现共识的算法。
但是可以在付出一定代价下, 达到一定的目标.
分布式系统不可能同时确保一致性(Consistency)、可用性(Availablity)和分区容忍性(Partition),设计中往往需要弱化对某个特性的保证。
CAP 不能同时满足,设计系统时针对应用场景弱化对某个特性的支持
即 Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)。
ACID 原则描述了对分布式数据库的一致性需求,同时付出了可用性的代价。
Atomicity:每次操作是原子的,要么成功,要么不执行;
Consistency:数据库的状态是一致的,无中间状态;
Isolation:各种操作彼此互相不影响;
Durability:状态的改变是持久的,不会失效。
一个与之相对的原则是 BASE(Basic Availiability,Soft state,Eventually Consistency),牺牲掉对一致性的约束(最终一致性),来换取一定的可用性。
是一种信息摘要, 可以用于检验内容的完全性, 一致性等. 流行的有 md5, sha-1, sha-2(Secure Hash Algorithm), sha-1已被证明不具备”强抗碰撞性”
组件包括: 加解密算法,加密密钥,解密密钥.
根据加解密的密钥是否相同,可以分成对称加密与非对称加密(asymmetrix cryptography).
对称密码从实现原理上可以分为两种:分组密码和序列密码。前者将明文切分为定长数据块
作为加密单位,应用最为广泛。后者则只对一个字节进行加密,且密码不断变化,只用在一
些特定领域,如数字媒介的加密等。
代表算法
适用于大量数据的加解密;不能用于签名场景;需要提前分发密钥。
非对称加密是现代密码学历史上最为伟大的发明,可以很好的解决对称加密需要的提前分发密钥问题。
一般比对称加解密算法慢两到三个数量级;同时加密强度相比对称加密要差。
非对称加密算法的安全性往往需要基于数学问题来保障,目前主要有基于大数质因子分解、离散对数、椭圆曲线等几种思路。
代表算法:
RSA 算法等已被认为不够安全,一般推荐采用椭圆曲线系列算法。
即先用计算复杂度高的非对称加密协商一个临时的对称加密密钥(会话密钥,一般相对内容
来说要短的多),然后双方再通过对称加密对传递的大量数据进行加解密处理。
典型的场景是现在大家常用的 HTTPS 机制。HTTPS 实际上是利用了 Transport Layer
Security/Secure Socket Layer(TLS/SSL)来实现可靠的传输。TLS 为 SSL 的升级版本
类似在纸质合同上签名确认合同内容,数字签名用于证实某数字内容的完整性(integrity)和来源(或不可抵赖,non-repudiation)。
一个典型的场景是,A 要发给 B 一份信息.
A 先对文件进行摘要,然后用自己的私钥进行加密,将文件和加密串都发给B。B 收到文件和加密串后,用 A 的公钥来解密加密串,得到原始的数字摘要,跟对文件进行摘要后的结果进行比对。
数字证书用来证明某个公钥是谁的,并且内容是正确的。
数字证书内容可能包括版本、序列号、签名算法类型、签发者信息、有效期、被签发人、签发的公开密钥、CA 数字签名、其它信息等等,一般使用最广泛的标准为 ITU 和 ISO 联合制定的 X.509 规范。
其中,最重要的包括 签发的公开密钥 、 CA 数字签名 两个信息。因此,只要通过这个证书就能证明某个公钥是合法的,因为带有 CA 的数字签名。
默克尔树(又叫哈希树)是一种二叉树,由一个根节点、一组中间节点和一组叶节点组成。最下面的叶节点包含存储数据或其哈希值,每个中间节点是它的两个孩子节点内容的哈希值,根节点也是由它的两个子节点内容的哈希值组成。
同态加密(Homomorphic Encryption)是一种特殊的加密方法,允许对密文进行处理得到仍然是加密的结果,即对密文直接进行处理,跟对明文进行处理再加密,得到的结果相同。从代数的角度讲,即同态性。(保运算)
比特币网络是一个分布式的点对点网络,网络中的矿工通过“挖矿”来完成对交易记录的记账过程,维护网络的正常运行。
比特币中没有账户的概念。因此,每次发生交易,用户需要将交易记录写到比特币网络账本中,等网络确认后即可认为交易完成。
除了挖矿获得奖励的 coinbase 交易只有输出,正常情况下每个交易需要包括若干输入和输出,未经使用(引用)的交易的输出(Unspent Transaction Outputs,UTXO)可以被新的交易引用作为其合法的输入。被使用过的交易的输出(Spent Transaction Outputs,STXO),则无法被引用作为合法输入。
比特币采用了非对称的加密算法,用户自己保留私钥,对自己发出的交易进行签名确认,并公开公钥。
比特币的账户地址其实就是用户公钥经过一系列 Hash(HASH160,或先进行 SHA256,然后进行 RIPEMD160)及编码运算后生成的 160 位(20 字节)的字符串。
易可能包括如下信息:
付款人地址:合法的地址,公钥经过 SHA256 和 RIPEMD160 两次 Hash,得到 160 位Hash 串;
付款人对交易的签字确认:确保交易内容不被篡改;
付款人资金的来源交易 ID:从哪个交易的输出作为本次交易的输入;
交易的金额:多少钱,跟输入的差额为交易的服务费;
收款人地址:合法的地址;
时间戳:交易何时能生效
节点收到交易信息后,将进行如下检查:
交易是否已经处理过;
交易是否合法。包括地址是否合法、发起交易者是否是输入地址的合法拥有者、是否是UTXO;
交易的输入之和是否大于输出之和。
比特币区块链的一个区块不能超过 1 MB,将主要包括如下内容:
区块大小:4 字节;
区块头:80 字节:
交易个数计数器:1~9 字节;
所有交易的具体内容,可变长,匹配 Merkle 树叶子节点顺序。
比特币交易性能:全网每秒 7 笔左右的交易速度,远低于传统的金融交易系统
提出闪电网络的解决方法
主要通过引入智能合约的思想来完善链下的交易渠道。核心的概念主要有两个:RSMC(Recoverable Sequence Maturity Contract)和 HTLC(Hashed TimelockContract)。前者解决了链下交易的确认问题,后者解决了支付通道的问题。
以比特币区块链作为主链(Parent chain),其他区块链作为侧链,二者通过双向挂钩(Two-way peg),可实现比特币从主链转移到侧链进行流通。
侧链可以是一个独立的区块链,有自己按需定制的账本、共识机制、交易类型、脚本和合约的支持等。侧链不能发行比特币,但可以通过支持与比特币区块链挂钩来引入和流通一定数量的比特币。当比特币在侧链流通时,主链上对应的比特币会被锁定,直到比特币从侧链回到主链。可以看到,侧链机制可将一些定制化或高频的交易放到比特币主链之外进行,实现了比特币区块链的扩展。侧链的核心原理在于能够冻结一条链上的资产,然后在另一条链上产生,可以通过多种方式来实现, 如 SPV
以太坊区块链底层也是一个类似比特币网络的 P2P 网络平台,智能合约运行在网络中的以太坊虚拟机里。网络自身是公开可接入的,任何人都可以接入并参与网络中数据的维护,提供运行以太坊虚拟机的资源。
跟比特币项目相比
*采用账户系统和世界状态,而不是 UTXO,容易支持更复杂的逻辑;
账户: 比特币在设计中并没有账户(Account)的概念,而是采用了UTXO 模型记录整个系统的状态。任何人都可以通过交易历史来推算出用户的余额信息。而以太坊则直接用账户来记录系统状态。每个账户存储余额信息、智能合约代码和内部数据存储等。 以太坊账户分为两类
交易: 是指从一个账户到另一个账户的消息数据
包括如下字段:
* to:目标账户地址。* value:可以指定转移的以太币数量。* nonce:交易相关的字串,用于防止交易被重放。* gasPrice:执行交易需要消耗的 Gas 价 格。* startgas:交易消耗的最大 Gas 值。* signature:签名信息。
在发送交易时,用户需要缴纳一定的交易费用,通过以太币方式进行支付
和消耗。
Gas 可以跟以太币进行兑换。需要注意的是,以太币的价格是波动的,但运行某段智能合约的燃料费用可以是固定的,通过设定 Gas 价格等进行调节
以太坊虚拟机是一个隔离的轻量级虚拟机环境,运行在其中的智能合约代码无法访问本地网络、文件系统或其它进程。
对同一个智能合约来说,往往需要在多个以太坊虚拟机中同时运行多份,以确保整个区块链数据的一致性和高度的容错性。另一方面,这也限制了整个网络的容量。
以太坊客户端可用于接入以太坊网络,进行账户管理、交易、挖矿、智能合约等各方面操
作。
Hyperledger 项目是首个面向企业的开放区块链技术的重要探索.该项目试图打造一个透明、公开、去中心化的分布式账本项目,作为区块链技术的开源规范和标准,让更多的应用能更容易的建立在区块链技术之上。
]]>定义良好的计算过程,取输入,并产生输出. 即算法是一系列的计算步骤,将输入数据转化为输出结果
算法的特点:
衡量算法的优劣
$\log^{*}(\log x) = log^{\}x-1$
结构上是递归的,
步骤: 分解,解决, 合并
eg. 快排,归并排序, 矩阵乘法(Strassen $O(log_2 7)$
$T(n) = aT(\frac{n} {b})+f(n)$
$T(n) = 2T(\frac{n} {2})+n$
猜测$T(n) = O(nlogn)$
证明 $ T(n)\leqslant cnlogn$
归纳奠基 n=2,3
归纳假设 $T(\frac{n} {2}) \leqslant \frac{cn}{2}$
递归
$
\begin{aligned}
T(n) &\leqslant 2c\frac{n}{2}log(\frac{n}{2}) + n \leqslant cnlog(\frac{n}{2}) \\
\end{aligned}
$
对于 $T(n) = 2T(\frac{cn}{2}) + 1$
如果 直接猜测 $T(n) = O (n)$ 不能证明,
而且不要猜测更高的界 $O (n^2)$
可以放缩为 n-b
对于 $ T(n) = 2T(\sqrt{n})+logn $
可以 令 m = logn
, 得到
$T(2^m) = 2T(m^{\frac{m}{2}}) + m $
令 $S(m) = T(2^m)$
得到 $ S(m) = 2S(\frac{m}{2}) + m $
例如 $T(n) = 3T(\frac{n}{4}) + c n^2$
不妨假设 n 为4的幂, 则有如下递归树
每个结点是代价, 将每层加起来即可
对于 $T(n) = aT(\frac{n} {b})+f(n)$
直观上, 比较 $n^{log_b a}$ 和 $f(n)$, 谁大就是谁,
相等的话就是 $\Theta(f(n))\log n$
这里的大是多项式上的比较, 即比较次数, 而不是渐近上的
比如 $n$ 与 $nlogn$ 渐近上后者大, 但多项式上是不能比较的
主要是应用数学技巧来解决 floor, ceiling 函数的处理问题
给出初始数组, eg A={1,2,3}, 选择随机的优先级 P={16,4,10}
则得出 B={2,3,1},因为第二个(2)优先级最小, 为4, 接着第三个,最后第1个.
优先级数组的产生, 一般在 RANDOM(1,n^3), 这样优先级各不相同的概率至少为 1-1/n
由于要排序优先级数组, 所以时间复杂度 $O(nlogn)$
如果优先级唯一, 则此算法可以 shuffle 数组
应证明 同样排列的概率是 $\frac{1}{n!}$
1 | from random import randint |
时间复杂度 $O(n)$
证明
定义循环不变式: 对每个可能的 $A_n^{i-1}$ 排列, 其在 arr[1..i-1] 中的概率为 $\frac{1}{A_n^{i-1}}$
初始化: i=1 成立
保持 : 假设 在第 i-1 次迭代之前,成立, 证明在第 i 次迭代之后, 仍然成立,
终止: 在 结束后, i=n+1, 得到 概率为 $\frac{1}{n!}$
把相同的秋随机投到 b 个盒子里,问在每个盒子里至少有一个球之前,平均至少要投多少个球?
称投入一个空盒为击中, 即求取得 b 次击中的概率
设投 n 次, 称第 i 个阶段包括第 i-1 次击中到 第 i 次击中的球, 则第 i 次击中的概率为 $p_i=\frac{b-i+1}{b}$
用 $n_i$表示第 i 阶段的投球数,则 $n=\sum_{i=1}^b n_i$
且 $n_i$服从几何分布, $E(n_i)=\frac{b}{b-i+1}$,
则由期望的线性性,
这个问题又被称为 赠券收集者问题(coupon collector’s problem),即集齐 b 种不同的赠券,在随机情况下平均需要买 blnb 张
抛 n 次硬币, 期望看到的连续正面的次数
答案是 $\Theta(logn)$
记 长度至少为 k 的正面序列开始与第 i 次抛, 由于独立, 所有 k 次抛掷都是正面的 概率为
$P(A_{ik})=\frac{1}{2^k}$,对于 $k=2\lceil lgn\rceil$
一个 n 个操作的序列最坏情况下花费的总时间为$T(n)$, 则在最坏情况下, 每个操作的摊还代价为 $\frac{T(n)}{n}$
如栈中的 push, pop 操作都是 $O(1)$, 增加一个新操作 multipop
,1
2
3
4def multipop(stk,k):
while not stk.empty() and k>0:
stk.pop()
k-=1
multipop 的时间复杂度为 min(stk.size,k), 最坏情况为 $O(n)$, 则 n 个包含 push pop multipop 的操作列的最坏情况是 $O(n^2)$, 并不是这样, 注意到, 必须栈中有元素, 再 pop, 所以 push 操作与pop 操作(包含 multipop中的pop), 个数相当, 所以 实际上应为 $O(n)$, 每个操作的摊还代价 为$O(1)$
对不同操作赋予不同费用 cost (称为摊还代价 $c_i’$), 可能多于或者少于其实际代价 $c_i$
当 $c_i’>c_i$, 将 $c_i’-c_i$( credit
) 存入数据结构中的特定对象.. 对于后续 $c_i’<c_i$时, 可以使用这些credit来 支付差额.. 有要求
如栈
op | $c_i’$ | $c_i$ |
---|---|---|
push | 2 | 1 |
pop | 0 | 1 |
multipop | 0 | min(s,k) |
由核算法, 摊还代价满足要求, 所以 n 个操作总代价 $O(n)$, 每个操作摊还代价为 $O(1)$
势能释放用来支付未来操作的代价, 势能是整个数据结构的, 不是特定对象的(核算法是).
数据结构 $D_0$为初始状态, 依次 执行 n 个操作 $op_i$进行势能转换 $D_i =op_i(D_{i-1}), i=1,2,\ldots,n$ , 各操作代价为 $c_i$
势函数 $\Phi:D_i\rightarrow R$, $\Phi(D_i)$即为 $D_i$的势
则第 i 个操作的摊还代价
则
如果定义一个势函数$\Phi, st \ \Phi(D_i)\geqslant\Phi(D_0)$, 则总摊还代价给出了实际代价的一个上界
可以简单地以 $D_0 \text{为参考状态}, then \ \Phi(D_0)=0$
例如栈操作,
设空栈为 $D_0$, 势函数定义为栈的元素数
对于push, $ \Phi(D_i)-\Phi(D_{i-1})=1$
则 $c’ = c +\Phi(D_i)-\Phi(D_{i-1}) = c+1 = 2$
对于 multipop, $ \Phi(D_i)-\Phi(D_{i-1})=- min(k,s)$
则 $c’ = c - min(k,s) = 0$
同理 pop 的摊还代价也是0, 则总摊还代价的上界(最坏情况) 为 $O(n)$
]]>每个结点有 5 个数据域
满足下面的 红黑性质
的二叉查找树就是红黑树:
如,叶子结点 是 nil, 即不存储任何东西, 为了编程方便,相对的,存有数据的结点称为内结点
为了节省空间, 可以如下实现, 只需要一个 nil 结点
从某个结点 x 到叶结点的黑色结点数,称为此结点的黑高度, 记为 $h_b(x)$
树的黑高度是根的黑高度
- 以 x 为 根的子树至少包含 $2^{h_b(x)}-1$个结点
- 一颗有 n 个内结点的红黑树高度至多为$2lg(n+1)$
可用归纳法证明1
证明 2:
设树高 h
由红黑性质4, 根结点到叶子路径上的黑结点数至少 $\frac{h}{2}$,即 $h_b(root)\geqslant \frac{h}{2}$
再由1,
即 $ h\leqslant 2lg(n+1)$
由于上面证明的红黑树高为 $O(logn)$,红黑树的 insert, delete, search 等操作都是, $O(logn)$.
进行了 insert, delete 后可能破坏红黑性质, 可以通过旋转来保持.
下面是对结点 x 进行 左旋与右旋.
注意进行左旋时, 右孩子不是 nil(要用来作为旋转后 x 的双亲), 同理 右旋的结点的左孩子不是nil
总结起来就是: 父亲旋转,顺时针就是右旋,逆时针就是左旋, 旋转的结果是儿子成为原来父亲的新父亲, 即旋转的结点下降一层, 它的一个儿子上升一层.
插入的过程:
这是算法导论1上的算法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18RB-INSERT(T, z)
y ← nil[T] // 新建节点“y”,将y设为空节点。
x ← root[T] // 设“红黑树T”的根节点为“x”
while x ≠ nil[T] // 找出要插入的节点“z”在二叉树T中的位置“y”
do y ← x
if key[z] < key[x]
then x ← left[x]
else x ← right[x]
p[z] ← y // 设置 “z的父亲” 为 “y”
if y = nil[T]
then root[T] ← z // 情况1:若y是空节点,则将z设为根
else if key[z] < key[y]
then left[y] ← z // 情况2:若“z所包含的值” < “y所包含的值”,则将z设为“y的左孩子”
else right[y] ← z // 情况3:(“z所包含的值” >= “y所包含的值”)将z设为“y的右孩子”
left[z] ← nil[T] // z的左孩子设为空
right[z] ← nil[T] // z的右孩子设为空。至此,已经完成将“节点z插入到二叉树”中了。
color[z] ← RED // 将z着色为“红色”
RB-INSERT-FIXUP(T, z) // 通过RB-INSERT-FIXUP对红黑树的节点进行颜色修改以及旋转,让树T仍然是一颗红黑树
可以用python 实现如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25def insert(self,nd):
if not isinstance(nd,node):
nd = node(nd)
elif nd.isBlack: nd.isBlack = False
if self.root is None:
self.root = nd
self.root.isBlack = True
else:
parent = self.root
while parent:
if parent == nd : return None
if parent>nd:
if parent.left :
parent = parent.left
else:
parent.left = nd
break
else:
if parent.right:
parent = parent.right
else:
parent.right = nd
break
self.fixUpInsert(parent,nd)
在插入后,可以发现后破坏的红黑性质只有以下两条(且互斥)
所以下面介绍如何保持 红结点的孩子是黑 , 即插入结点的双亲结点是红的情况.
下面记 结点 x 的 双亲为 p(x), 新插入的结点为 x, 记 uncle 结点 为 u(x)
由于 p(x) 是红色, 而根结点是黑色, 所以 p(x)不是根, p(p(x))存在
有如下三种情况
每种情况的解决方案如下
这里只需改变颜色, 将 p(x)变为 黑, p(p(x))变为红, u(x) 变为黑色 (x为右孩子同样)
即 x,p(x), p(p(x)) 成直线状
当 x 为右孩子时, 通过旋转变成p(x) 的双亲, 然后相当于 新插入 p(x)作为左孩子, 再进行转换.
即将新结点的双亲向上一层旋转,颜色变为黑色, 而新节点的祖父向下一层, 颜色变为红色
我最开始也没有弄清楚, 有点绕晕的感觉, 后来仔细读了书上伪代码, 然后才发现就是一个状态机, 画出来就一目了然了.
现在算是知其然了, 那么怎样知其所以然呢? 即 为什么要分类这三个 case, 不重不漏了吗?
其实也简单, 只是太繁琐.
就是将各种情况枚举出来, 一一分析即可. 我最开始试过, 但是太多,写在代码里很容易写着写着就混了.
而算法导论上分成这三个case , 很简洁, 只是归纳了一下而已. 如果想看看枚举情况的图与说明,可以参考2 .
算法导论上的伪代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17RB-INSERT-FIXUP(T, z)
while color[p[z]] = RED // 若“当前节点(z)的父节点是红色”,则进行以下处理。
do if p[z] = left[p[p[z]]] // 若“z的父节点”是“z的祖父节点的左孩子”,则进行以下处理。
then y ← right[p[p[z]]] // 将y设置为“z的叔叔节点(z的祖父节点的右孩子)”
if color[y] = RED // Case 1条件:叔叔是红色
then color[p[z]] ← BLACK ▹ Case 1 // (01) 将“父节点”设为黑色。
color[y] ← BLACK ▹ Case 1 // (02) 将“叔叔节点”设为黑色。
color[p[p[z]]] ← RED ▹ Case 1 // (03) 将“祖父节点”设为“红色”。
z ← p[p[z]] ▹ Case 1 // (04) 将“祖父节点”设为“当前节点”(红色节点)
else if z = right[p[z]] // Case 2条件:叔叔是黑色,且当前节点是右孩子
then z ← p[z] ▹ Case 2 // (01) 将“父节点”作为“新的当前节点”。
LEFT-ROTATE(T, z) ▹ Case 2 // (02) 以“新的当前节点”为支点进行左旋。
color[p[z]] ← BLACK ▹ Case 3 // Case 3条件:叔叔是黑色,且当前节点是左孩子。(01) 将“父节点”设为“黑色”。
color[p[p[z]]] ← RED ▹ Case 3 // (02) 将“祖父节点”设为“红色”。
RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 // (03) 以“祖父节点”为支点进行右旋。
else (same as then clause with "right" and "left" exchanged) // 若“z的父节点”是“z的祖父节点的右孩子”,将上面的操作中“right”和“left”交换位置,然后依次执行。
color[root[T]] ← BLACK
我用python 实现如下. 由于左右方向不同, 如果向上面伪代码那样实现, fixup 代码就会有两份类似的(即 right left 互换), 为了减少代码冗余, 我就定义了 setChild
, getChild
函数, 传递左或是右孩子这个方向的数据(代码中是isLeft), 所以下面的就是完整功能的 fixup, 可以减少一般的代码量, haha😄,
(下文 删除结点同理)
其实阅读代码也简单, 可以直接当成 isLeft 取真值.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
35def fixUpInsert(self,parent,nd):
''' adjust color and level, there are two red nodes: the new one and its parent'''
while not self.checkBlack(parent):
grand = self.getParent(parent)
isLeftPrt = grand.left is parent
uncle = grand.getChild(not isLeftPrt)
if not self.checkBlack(uncle):
# case 1: new node's uncle is red
self.setBlack(grand, False)
self.setBlack(grand.left, True)
self.setBlack(grand.right, True)
nd = grand
parent = self.getParent(nd)
else:
# case 2: new node's uncle is black(including nil leaf)
isLeftNode = parent.left is nd
if isLeftNode ^ isLeftPrt:
# case 2.1 the new node is inserted in left-right or right-left form
# grand grand
# parent or parent
# nd nd
parent.setChild(nd.getChild(isLeftPrt),not isLeftPrt)
nd.setChild(parent,isLeftPrt)
grand.setChild(nd,isLeftPrt)
nd,parent = parent,nd
# case 2.2 the new node is inserted in left-left or right-right form
# grand grand
# parent or parent
# nd nd
grand.setChild(parent.getChild(not isLeftPrt),isLeftPrt)
parent.setChild(grand,not isLeftPrt)
self.setBlack(grand, False)
self.setBlack(parent, True)
self.transferParent(grand,parent)
self.setBlack(self.root,True)
算法导论上的算法
写的很简练👍
下面 z 是要删除的结点, y 是 其后继或者是它自己, x 是 y 的一个孩子(如果 y 的孩子为 nil,则为 nli, 否则 y 只有一个非 nil 孩子, 为 x)
即3 如果要删除有两个孩子的结点 z , 则找到它的后继y(前趋同理), 可以推断 y 一定没有左孩子, 右孩子可能有,可能没有. 也就是最多一个孩子.
所以将 y 的值复制到 x 位置, 现在相当于删除 y 处的结点.
这样就化为 删除的结点最多一个孩子的情况.
可以发现只有当 y 是黑色,才进行颜色调整以及旋转(维持红黑性质), 因为如果删除的是红色, 不会影响黑高度, 所有红黑性质都不会破坏
伪代码如下, (我的python代码见文末)
如果被删除的结点 y 是黑色的, 有三种破坏红黑性质的情况
修复3的思路:
如果可能,在兄弟一支,通过旋转,改变颜色修复
否则, 将红结点一直向上推(因为当前路径上少了一个黑结点,向上推的过程中使红结点所在的子树都少一个黑结点), 直到到达树根, 那么全部路径都少一个黑结点, 3就修复了, 这时只需将根设为黑就修复了 1
代码中的 while 循环的目的是将额外的黑色沿树上移,直到
在 while 中, x 总是指向具有双重黑色的那个非根结点, 在第 2 行中要判断 x 是其双亲的左右孩子
w 表示 x 的相抵. w 不能为 nil(因为 x 是双重黑色)
算法中的四种情况如图所示
即
x 的兄弟 w 是黑色的, w的两个孩子都是黑色的
x 的兄弟 w 是黑色的, w 的左孩子是红,右孩子是黑
注意上面都是先考虑的左边, 右边可以对称地处理.
同插入一样, 为了便于理解, 可以作出状态机.
而且这些情形都是归纳化简了的, 你也可以枚举列出基本的全部情形.
通过在平衡树(如红黑树上的每个结点 加上 一个数据域 size (表示以此结点为根的子树的结点数.) 可以使获得第 i 大的数
的时间复杂度为 $O(logn)$
在 $O(n)$ 时间内建立, python代码如下1
2
3def setSize(root):
if root is None:return 0
root.size = setSize(root.left) + setSize(root.right)+1
在$O(logn)$时间查找,1
2
3
4
5
6
7
8def find(root,i):
r = root.left.size +1
if r==i:
return root
if r > i:
return find(root.left,i)
else:
return find(root.right,i-r)
我用了 setChild, getChild 来简化代码量, 其他的基本上是按照算法导论上的伪代码提到的case 来实现的. 然后display 只是测试的时候,为了方便调试而层序遍历打印出来
效果如下
1 | ''' mbinary |
测试代码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
def genNum(n =10):
nums =[]
for i in range(n):
while 1:
d = randint(0,100)
if d not in nums:
nums.append(d)
break
return nums
def buildTree(n=10,nums=None,visitor=None):
if nums is None or nums ==[]: nums = genNum(n)
rbtree = redBlackTree()
print(f'build a red-black tree using {nums}')
for i in nums:
rbtree.insert(node(i))
if visitor:
visitor(rbtree,i)
return rbtree,nums
def testInsert(nums=None):
def visitor(t,val):
print('inserting', val)
print(t)
rbtree,nums = buildTree(visitor = visitor,nums=nums)
print('-'*5+ 'in-order visit' + '-'*5)
for i,j in enumerate(rbtree.sort()):
print(f'{i+1}: {j}')
def testSuc(nums=None):
rbtree,nums = buildTree(nums=nums)
for i in rbtree.sort():
print(f'{i}\'s suc is {rbtree.getSuccessor(i)}')
def testDelete(nums=None):
rbtree,nums = buildTree(nums = nums)
print(rbtree)
for i in sorted(nums):
print(f'deleting {i}')
rbtree.delete(i)
print(rbtree)
if __name__=='__main__':
lst =[45, 30, 64, 36, 95, 38, 76, 34, 50, 1]
lst = [0,3,5,6,26,25,8,19,15,16,17]
#testSuc(lst)
#testInsert(lst)
testDelete()
下面是利用红黑树进行扩展成区间树的代码
1 | from redBlackTree import redBlackTree |
1. 算法导论 ↩
2. https://www.jianshu.com/p/a5514510f5b9?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation ↩
3. https://www.jianshu.com/p/0b68b992f688?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation ↩]]>
又名排序二叉树,对于每个结点, 如果有,其左孩子不大于它,右孩子不小于它
通过前序遍历或者后序遍历就可以得到有序序列(升序,降序)
常用三种操作, 插入,删除,查找,时间复杂度是 $O(h)$
h是树高, 但是由于插入,删除而导致树不平衡, 即可能 $h\geqslant \lfloor logn \rfloor$
下面可以证明,随机构造,即输入序列有 $n!$中, 每种概率相同的情况下, 期望的树高 $h=O(logn)$
(直接搬运算法导论上面的啦>_<)
一个较 上面定理 弱的结论:
一棵随机构造的二叉查找树,n 个结点的平均深度为 $O(logn)$
类似 RANDOMIZED-QUICKSORT 的证明过程, 因为快排 递归的过程就是一个递归 二叉树.
随机选择枢纽元就相当于这里的某个子树的根结点 在所有结点的大小随机排名, 如 i. 然后根结点将剩下的结点划分为左子树(i-1)个结点, 右子树(n-i)个结点.
给定$\{1,2,\ldots,n\}$,组成二叉查找树的数目.
由上面的证明过程, 可以容易地分析得出, 任选第 i 个数作为根, 由于二叉查找树的性质, 其左子树
应该有 i-1个结点, 右子树有 n-i个结点.
如果记 n 个结点 的二叉查找树的数目为$b_n$
则有递推公式
然后我们来看<<算法导论>>
(p162,思考题12-4)上怎么求的吧( •̀ ω •́ )y
设生成函数
下面证明$B(x)=xB(x)^2+1$
易得
对比$B(x), xB(x)^2+1$的 x 的各次系数,分别是 $b_k,a_{k}$
当 k=0, $a_k=1=b_k$
当 k>0
所以$B(x)=xB(x)^2+1$
由此解得
在点 x=0 处,
用泰勒公式得
所以对应系数
王树禾的<<图论>>
(p42)上用另外的方法给出Catalan数, 并求出n结点 二叉查找数的个数
首先定义好括号列,有:
充要条件: 好括号列 $\Longleftrightarrow$ 左右括号数相等, 且从左向右看, 看到的右括号数不超过左括号数
定理: 由 n个左括号,n个右括号组成的好括号列个数为$c(n)=\frac{C_{2n}^{n}}{n+1}$
证明:
由 n左n右组成的括号列有 $\frac{2n}{n!n!}=C_{2n}^{n}$个.
设括号列$a_1a_2\ldots a_{2n}$为坏括号列,
由充要条件, 存在最小的 j, 使得$a_1a_2\ldots a_{j}$中右括号比左括号多一个,
由于是最小的 j, 所以 $a_j$为右括号, $a_{j+1}$为右括号
把$a_{j+1}a_{j+2}\ldots a_{2n}$中的左括号变为右括号, 右变左,记为$\bar a_{j+1}\bar a_{j+2}\ldots \bar a_{2n}$
则括号列$a_1a_2\ldots a_{j}\bar a_{j+1}$为好括号列
$a_1a_2\ldots a_{j}\bar a_{j+1}\bar a_{j+2}\ldots \bar a_{2n}$可好可坏,且有n-1个右,n+1个左, 共有$\frac{2n}{(n+1)!(n-1)!}=C_{2n}^{n+1}$个.
所以坏括号列$a_1a_2\ldots a_{2n}$ 与括号列 $a_1a_2\ldots a_{j}\bar a_{j+1}\bar a_{j+2}\ldots \bar a_{2n}$, 有$\frac{2n}{(n+1)!(n-1)!}=C_{2n}^{n+1}$个
那么好括号列有
推论: n个字符,进栈出栈(出栈可以在栈不为空的时候随时进行), 则出栈序列有 c(n)种
这种先入后出的情形都是这样
又叫前缀树
(preifx tree).适用于储存有公共前缀的字符串集合. 如果直接储存, 而很多字符串有公共前缀, 会浪费掉存储空间.
字典树可以看成是基数树的变形, 每个结点可以有多个孩子, 每个结点存储的是一个字符, 从根沿着结点走到一个结点,走过的路径形成字符序列, 如果有合适的单词就可以输出.
Aho-Corasick automation,是在字典树上添加匹配失败边(失配指针), 实现字符串搜索匹配的算法.
图中蓝色结点 表示存在字符串, 灰色表示不存在.
黑色边是父亲到子结点的边, 蓝色边就是失配指针
.
蓝色边(终点称为起点的后缀结点): 连接字符串终点到在图中存在的, 最长严格后缀的结点. 如 caa 的严格后缀为 aa,a, 空. 而在图中存在, 且最长的是字符串 a, 则连接到这个字符串的终点 a.
绿色边(字典后缀结点): 终点是起点经过蓝色有向边到达的第一个蓝色结点.
下面摘自 wiki
在每一步中,算法先查找当前节点的 “孩子节点”,如果没有找到匹配,查找它的后缀节点(suffix) 的孩子,如果仍然没有,接着查找后缀节点的后缀节点的孩子, 如此循环, 直到根结点,如果到达根节点仍没有找到匹配则结束。
当算法查找到一个节点,则输出所有结束在当前位置的字典项。输出步骤为首先找到该节点的字典后缀,然后用递归的方式一直执行到节点没有字典前缀为止。同时,如果该节点为一个字典节点,则输出该节点本身。
上面的二叉查找树不平衡,即经过多次插入,删除后, 其高度变化大, 不能保持$\Theta(n)$的性能
而平衡二叉树就能.
平衡二叉树都是经过一些旋转操作, 使左右子树的结点高度相差不大,达到平衡
有如下几种
平衡因子
: 右子树高度 - 左子树高度
定义: 每个结点的平衡因子属于{0,-1,1}
伸展树, 它的特点是每次将访问的结点通过旋转旋转到根结点.
其实它并不平衡. 但是插入,查找,删除操作 的平摊时间是$O(logn)$
有三种旋转,下面都是将访问过的 x 旋转到 根部
同样是平衡的二叉树, 以后单独写一篇关于红黑树的.
前面提到, 随机构造的二叉查找树高度为 $h=O(logn)$,以及在算法 general 中说明了怎样 随机化(shuffle)一个给定的序列.
所以,为了得到一个平衡的二叉排序树,我们可以将给定的序列随机化, 然后再进行构造二叉排序树.
但是如果不能一次得到全部的数据,也就是可能插入新的数据的时候,该怎么办呢? 可以证明,满足下面的条件构造的结构相当于同时得到全部数据, 也就是随机化的二叉查找树.
这种结构叫 treap
, 不仅有要排序的关键字 key, 还有随机生成的,各不相等的关键字priority
,代表插入的顺序.
插入的实现: 先进行二叉查找树的插入,成为叶子结点, 再通过旋转 实现 上浮
(堆中术语).
将先排序 key, 再排序 prority(排序prority 时通过旋转保持 key 的排序)
还有很多有趣的树结构,
比如斜堆, 竞赛树(赢者树,输者树,线段树, 索引树,B树, fingerTree(不知道是不是译为手指树233)…
这里就不详细介绍了, 如果以后有时间,可能挑几个单独写一篇文章
1 | from functools import total_ordering |
1 | class node: |
1 | class winnerTree: |
1 | from functools import total_ordering |
哈希表 (hash table) , 可以实现 $O(1)$ 的 read, write, update
相对应 python 中的 dict, c语言中的 map
其实数组也能实现, 只是数组用来索引的关键字是下标, 是整数.
而哈希表就是将各种关键字映射到数组下标的一种”数组”
由于关键字是用来索引数据的, 所以要求它不能变动(如果变动,实际上就是一个新的关键字插入了), 在python 中表现为 immutable. 常为字符串.
将关键字 k 进行映射, 映射函数 $h$, 映射后的数组地址 $h(k)$.
- 简单一致假设:元素散列到每个链表的可能性是相同的, 且与其他已被散列的元素独立无关.
- 简单一致散列(simple uniform hashing): 满足简单一致假设的散列
好的散列函数应 满足简单一致假设
例如
由于关键字值域大于映射后的地址值域, 所以可能出现两个关键字有相同的映射地址
可以先用 ascii 值,然后
将关键字直接对应到数组地址, 即 $h(k)=k$
缺点: 如果关键字值域范围大, 但是数量小, 就会浪费空间, 有可能还不能储存这么大的值域范围.
通过链接法来解决碰撞
记有 m 个链表, n 个元素 $\alpha = \frac{n}{m}$ 为每个链表的期望元素个数(长度)
则查找成功,或者不成功的时间复杂度为 $\Theta(1+\alpha)$
如果 $n=O(m), namely \quad \alpha=\frac{O(m)}{m}=O(1)$, 则上面的链接法满足 $O(1)$的速度
设一组散列函数 $H=\{h_1,h_2,\ldots,h_i\}$, 将 关键字域 U 映射到 $\{0,1,\ldots,m-1\}$ , 全域的函数组, 满足
即从 H 中任选一个散列函数, 当关键字不相等时, 发生碰撞的概率不超过 $\frac{1}{m}$
对于 m 个槽位的表, 只需 $\Theta(n)$的期望时间来处理 n 个元素的 insert, search, delete,其中 有$O(m)$个insert 操作
选择足够大的 prime p, 记 $Z_p=\{0,1,\ldots,p-1\}$, $Z_p^{}=\{1,\ldots,p-1\}$
令$h_{a,b}(k) = ((ak+b)mod\ p) mod\ m$
则 $H_{p,m}=\{h_{a,b}|a\in Z_p^{},b\in Z_p\}$
每一个散列函数 $h_{a,b}$ 都将 $Z_p$ 映射到 $Z_m$, m 可以是任意的, 不用是一个素数
所有表项都在散列表中, 没有链表.
且散列表装载因子$\alpha=\frac{n}{m}\leqslant1$
这里散列函数再接受一个参数, 作为探测序号
逐一试探 $h(k,0),h(k,1),\ldots,h(k,m-1)$,这要有满足的,就插入, 不再计算后面的 hash值
探测序列一般分有三种
存在一次聚集问题
存在二次聚集问题
$h(k,i) = (h_1(k)+i*h_2(k))mod\ m$
为了能查找整个表, 即要为模 m 的完系, 则 h_2(k)要与 m 互质.
如可以取 $h_1(k) = k\ mod \ m,h_2(k) = 1+(k\ mod\ {m-1})$
注意删除时, 不能直接删除掉(如果有元素插入在其后插入时探测过此地址,删除后就不能访问到那个元素了), 应该 只是做个标记为删除
对于开放寻址散列表,且 $\alpha<1$,一次不成功的查找,是这样的: 已经装填了 n 个, 总共有m 个,则空槽有 m-n 个.
不成功的探查是这样的: 一直探查到已经装填的元素(但是不是要找的元素), 直到遇到没有装填的空槽. 所以这服从几何分布, 即
有
所以, 插入一个关键字, 也最多需要 $\frac{1}{1-\alpha}$次, 因为插入过程就是前面都是被占用了的槽, 最后遇到一个空槽.与探查不成功是一样的过程
成功查找的探查过程与插入是一样的. 所以查找关键字 k 相当于 插入它, 设为第 i+1 个插入的(前面插入了i个,装载因子$\alpha=\frac{i}{m}$. 那么期望探查数就是
则成功查找的期望探查数为
代码
github地址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
80class item:
def __init__(self,key,val,nextItem=None):
self.key = key
self.val = val
self.next = nextItem
def to(self,it):
self.next = it
def __eq__(self,it):
'''using keyword <in> '''
return self.key == it.key
def __bool__(self):
return self.key is not None
def __str__(self):
li = []
nd = self
while nd:
li.append(f'({nd.key}:{nd.val})')
nd = nd.next
return ' -> '.join(li)
def __repr__(self):
return f'item({self.key},{self.val})'
class hashTable:
def __init__(self,size=100):
self.size = size
self.slots=[item(None,None) for i in range(self.size)]
def __setitem__(self,key,val):
nd = self.slots[self.myhash(key)]
while nd.next:
if nd.key ==key:
if nd.val!=val: nd.val=val
return
nd = nd.next
nd.next = item(key,val)
def myhash(self,key):
if isinstance(key,str):
key = sum(ord(i) for i in key)
if not isinstance(key,int):
key = hash(key)
return key % self.size
def __iter__(self):
'''when using keyword <in>, such as ' if key in dic',
the dic's __iter__ method will be called,(if hasn't, calls __getitem__
then ~iterate~ dic's keys to compare whether one equls to the key
'''
for nd in self.slots:
nd = nd.next
while nd :
yield nd.key
nd = nd.next
def __getitem__(self,key):
nd =self.slots[ self.myhash(key)].next
while nd:
if nd.key==key:
return nd.val
nd = nd.next
raise Exception(f'[KeyError]: {self.__class__.__name__} has no key {key}')
def __delitem__(self,key):
'''note that None item and item(None,None) differ with each other,
which means you should take care of them and correctly cop with None item
especially when deleting items
'''
n = self.myhash(key)
nd = self.slots[n].next
if nd.key == key:
if nd.next is None:
self.slots[n] = item(None,None) # be careful
else:self.slots[n] = nd.next
return
while nd:
if nd.next is None: break # necessary
if nd.next.key ==key:
nd.next = nd.next.next
nd = nd.next
def __str__(self):
li = ['\n\n'+'-'*5+'hashTable'+'-'*5]
for i,nd in enumerate(self.slots):
li.append(f'{i}: '+str(nd.next))
return '\n'.join(li)
排序的本质就是减少逆序数, 根据是否进行比较,可以分为如下两类.
如希尔排序,堆排序, 快速排序, 合并排序等
可以证明 比较排序的下界 是 $\Omega(nlogn)$
如 计数排序, 桶排序, 基数排序 不依靠比较来进行排序的, 可以达到 线性时间的复杂度
希尔排序是选择排序的改进, 通过在较远的距离进行交换, 可以更快的减少逆序数. 这个距离即增量, 由自己选择一组, 从大到小进行, 而且最后一个增量必须是 1. 要选得到好的性能, 一般选择$2^k-1$1
2
3
4
5
6
7
8
9
10
11
12def shellSort(s,inc = None):
if inc is None: inc = [1,3,5,7,11,13,17,19]
num = len(s)
inc.sort(reverse=True)
for i in inc:
for j in range(i,num):
cur = j
while cur>=i and s[j] > s[cur-i]:
s[cur] = s[cur-i]
cur-=i
s[cur] = s[j]
return s
可以证明 希尔排序时间复杂度可以达到$O(n^{\frac{4}{3}})$
是将一个数组(列表) heapify 的过程. 方法就是对每一个结点, 都自底向上的比较,然后操作,这个过程称为 上浮.
粗略的计算, 每个结点上浮的比较次数的上界是 层数, 即 logn, 则 n 个结点, 总的比较次数为 nlogn
但是可以发现, 不同高度 h 的结点比较的次数不同, 上界实际上应该是 $O(h)$,每层结点数上界 $\lfloor 2^h \rfloor$
则 总比较次数为
最大堆对应最大元,最小堆对于最小元, 可以 $O(1)$ 内实现
最大堆取最大元,最小堆取最小元,由于元素取出了, 要进行调整.
从堆顶开始, 依次和其两个孩子比较, 如果是最大堆, 就将此结点(父亲)的值赋为较大的孩子的值,最小堆反之.
然后对那个孩子进行同样的操作,一直到达堆底,即最下面的一层. 这个过程称为 下滤.
最后将最后一个元素与最下面一层那个元素(与上一层交换的)交换, 再删除最后一个元素.
时间复杂度为 $O(logn)$
建立堆之后, 一直进行 取出最元
操作, 即得有序序列
代码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
72from functools import partial
class heap:
def __init__(self,lst,reverse = False):
self.data= heapify(lst,reverse)
self.cmp = partial(lambda i,j,r:cmp(self.data[i],self.data[j],r),r= reverse)
def getTop(self):
return self.data[0]
def __getitem__(self,idx):
return self.data[idx]
def __bool__(self):
return self.data != []
def popTop(self):
ret = self.data[0]
n = len(self.data)
cur = 1
while cur * 2<=n:
chd = cur-1
r_idx = cur*2
l_idx = r_idx-1
if r_idx==n:
self.data[chd] = self.data[l_idx]
break
j = l_idx if self.cmp(l_idx,r_idx)<0 else r_idx
self.data[chd] = self.data[j]
cur = j+1
self.data[cur-1] = self.data[-1]
self.data.pop()
return ret
def addNode(self,val):
self.data.append(val)
self.data = one_heapify(len(self.data)-1)
def cmp(n1,n2,reverse=False):
fac = -1 if reverse else 1
if n1 < n2: return -fac
elif n1 > n2: return fac
return 0
def heapify(lst,reverse = False):
for i in range(len(lst)):
lst = one_heapify(lst,i,reverse)
return lst
def one_heapify(lst,cur,reverse = False):
cur +=1
while cur>1:
chd = cur-1
prt = cur//2-1
if cmp(lst[prt],lst[chd],reverse)<0:
break
lst[prt],lst[chd] = lst[chd], lst[prt]
cur = prt+1
return lst
def heapSort(lst,reverse = False):
lst = lst.copy()
hp = heap(lst,reverse)
ret = []
while hp:
ret.append(hp.popTop())
return ret
if __name__ == '__main__':
from random import randint
n = randint(10,20)
lst = [randint(0,100) for i in range(n)]
print('random : ', lst)
print('small-heap: ', heapify(lst))
print('big-heap : ', heapify(lst,True))
print('ascend : ', heapSort(lst))
print('descend : ', heapSort(lst,True))
1 | def quickSort(lst): |
快排大体结构就是这样,使用分治的思想, 在原地进行排列.
关键就在于选择枢纽元.
这里的 partition 就是根据枢纽元,分别将 大于,小于或等于的枢纽元的元素放在列表两边, 分割开.
partition 有不同的实现. 下面列出两种
1 | def partition(a,b): |
1 | def partition(a,b): |
第二种是算法导论上的,可以发现,第二种交换赋值的次数比第一种要多,而且如果序列的逆序数较大,第二种一次交换减少的逆序数很少, 而第一种就比较多(交换的两个元素相距较远)
然后我用随机数测试了一下, 确实是第一种较快, 特别是要排序的序列较长时,如在 5000 个元素时, 第一种要比第二种快几倍, Amazing!
完整代码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
26def quickSort(lst):
'''A optimized version of Hoare partition'''
def partition(a,b):
pivot = lst[a]
while a!=b:
while a<b and lst[b]>pivot: b-=1
if a<b:
lst[a] = lst[b]
a+=1
while a<b and lst[a]<pivot: a+=1
if a<b:
lst[b] = lst[a]
b-=1
lst[a] = pivot
return a
def _sort(a,b):
if a>=b:return
mid = (a+b)//2
# 三数取中值置于第一个作为 pivot
if (lst[a]<lst[mid]) ^ (lst[b]<lst[mid]): lst[a],lst[mid] = lst[mid],lst[a] # lst[mid] 为中值
if (lst[a]<lst[b]) ^ (lst[b]>lst[mid]): lst[a],lst[b] = lst[b],lst[a] # lst[b] 为中值
i = partition(a,b)
_sort(a,i-1)
_sort(i+1,b)
_sort(0,len(lst)-1)
return lst
快速排序性能取决于划分的对称性(即枢纽元的选择), 以及partition 的实现. 如果每次划分很对称(大概在当前序列的中位数为枢纽元), 则与合并算法一样快, 但是如果不对称,在渐近上就和插入算法一样慢
试想,如果每次划分两个区域分别包含 n-1, 1则易知时间复杂度为 $\Theta(n^2)$, 此外, 如果输入序序列已经排好序,且枢纽元没选好, 比如选的端点, 则同样是这样复杂, 而此时插入排序只需 $O(n)$.
有 $T(n) = 2T(\frac{n}{2})+\Theta(n)$
则由主方法为$O(nlogn)$
如果每次 9:1, $T(n) = T(\frac{9n}{10})+T(\frac{n}{10})+\Theta(n)$
用递归树求得在渐近上仍然是 $O(nlogn)$
所以任何比值 k:1, 都有如上的渐近时间复杂度
然而每次划分是不可能完全相同的
对于 randomized-quicksort, 即随机选择枢纽元
设 n 个元素, 从小到大记为 $z_1,z_2,\ldots,z_n$,指示器变量 $X_{ij}$表示 $z_i,z_j$是否进行比较
即
考察比较次数, 可以发现两个元素进行比较, 一定是一个是枢纽元的情况, 两个元素间不可能进行两次比较.
所有总的比较次数不超过,$\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij}$
求均值
再分析,$z_i,z_j$ 在$Z_{ij} = \{z_i,z_{i+1},\ldots,z_j\} $中, 如果集合中的非此两元素,$z_k, i< k< j$作为了枢纽元, 则$z_k$将集合划分{z_i,z_{i+1},\ldots,z_{k-1}},{z_{k+1},\ldots,z_j}, 这两个集合中的元素都不会再和对方中的元素进行比较,
所以要使 $z_i,z_j$进行比较, 则两者之一(只能是一个,即互斥)是 $Z_{ij}$上的枢纽元
则
注意第二步是因为两事件互斥才可以直接概率相加
然后就可以将此概率代入求期望比较次数了,
为 $O(nlogn)$ (由于是 O, 放缩一下就行)
考察快速排序的堆栈深度,可以从递归树思考,实际上的堆栈变化过程就是前序访问二叉树, 所以深度为 $O(logn)$
为了减少深度, 可以进行 尾递归优化, 将函数返回前的递归通过迭代完成1
2
3
4
5
6QUICKSORT(A,a,b)
while a<b:
#partition and sort left subarray
pos = partition(a,b)
QUICKSORT(A,a,pos-1)
a = pos+1
这是上面三个版本的简单测试结果,
前面测试的是各函数用的时间, 后面打印出来的是体现正确性,用的另外的序列了
需要知道元素的取值范围, 而且应该是有限的, 最好范围不大
不过需要额外的存储空间.
计算排序是稳定的: 具有相同值的元素在输出中是原来的相对顺序.1
2
3
4
5
6
7
8
9def countSort(lst,mn,mx):
mark = [0]*(mx-mn+1)
for i in lst:
mark[i-mn]+=1
ret =[]
for n,i in enumerate(mark):
for j in range(i):
ret.append(n+mn)
return ret
由我们平时的直觉, 我们比较两个数时, 是从最高位比较起, 一位一位比较, 直到不相等时就能判断大小,或者相等(位数比完了).
基数排序有点不一样, 它是从低位比到高位, 这样才能把相同位有相同值的不同数排序.
对于 n 个数, 最高 d 位, 用下面的实现, 可时间复杂度为 $\Theta((n+d)*d)$
下面是一个整数版本的基数排序,比较容易实现1
2
3
4
5
6
7
8
9
10
11def radixSort(lst,radix=10):
ls = [[] for i in range(radix)]
mx = max(lst)
weight = 1
while mx >= weight:
for i in lst:
ls[(i // weight)%radix].append(i)
weight *= radix
lst = sum(ls,[])
ls = [[] for i in range(radix)]
return lst
注意到如果有负数,要使用计数排序或者 基数排序,每个数需要加上最小值的相反数, 再排序, 最后再减去, 如果有浮点数, 就需要先乘以一个数, 使所有数变为整数.
我想过用 str 得到一个数的各位, 不过 str 可能比较慢. str 的实现应该也是先算术计算, 再生成 str 对象, 对于基数排序, 生成str 对象是多余的.
下面是 基数排序与快速排序的比较,测试代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16from time import time
from random import randint
def timer(funcs,span,num=1000000):
lst = [randint(0,span) for i in range(num)]
print('range({}), {} items'.format(span,num))
for func in funcs:
data = lst.copy()
t = time()
func(data)
t = time()-t
print('{}: {}s'.format(func.__name__,t))
if __name__ == '__main__':
timer([quickSort,radixSort],1000000000,100000)
timer([quickSort,radixSort],1000000000000,10000)
timer([quickSort,radixSort],10000,100000)
适用于均匀分布的序列
设有 n 个元素, 则设立 n 个桶
将各元素通过数值线性映射到桶地址,
类似 hash 链表.
然后在每个桶内, 进行插入排序($O(n_i^2)$)
最后合并所有桶.
这里的特点是 n 个桶实现了 $\Theta(n)$的时间复杂度, 但是耗费的空间 为 $\Theta(n)$
证明
将以上各部分加起来即得时间复杂度 $\Theta(n)$
输入个序列 lst, 以及一个数 i, 输出 lst 中 第 i 小的数,即从小到大排列第 i
解决方法
i 个元素的方法) 仍然 $O(nlogn)$
1 | from random import randint |
总线
根据连接部件
总线宽度:通常指数据总线的根数;
总线带宽:总线的数据传输率,指单位时间内总线上传输数据的位数;
总线复用:指同一条信号线可以分时传输不同的信号。
总线的主设备(主模块):指一次总线传输期间,拥有总线控制权的设备(模块);
总线的从设备(从模块):指一次总线传输期间,配合主设备完成数据传输的设备(模块),它只能被动接受主设备发来的命令;
总线的传输周期:指总线完成一次完整而可靠的传输所需时间;
总线的通信控制:指总线传送过程中双方的时间配合方式。
IO总线, DMA 总线, 主存总线
局部总线, 系统总线, 扩展总线
局部总线, 系统总线, 扩展总线, 高速总线 ( 适用高速 I/O 设备)
仲裁逻辑
链式查询
计数器定时查询
独立请求
比较
方式 | 每个设备用的总线数 | 实现 | 特点 | 原理 |
---|---|---|---|---|
链式查询 | 2 | 简单 | 近的优先,对电路故障最敏感 | BS总线忙,BR总线请求,BG总线同意. BG信号串行地从近到远传递到下一个IO接口, 如果此接口有总线请求, 总 BG 不再向下传,此接口得到总线使用权 |
计数器定时查询 | ~log2n | 稍复杂 | 平等,对故障不敏感 | 多了一组设备地址先,少了BG. 总线未被使用时,BS=0. 总线控制部件的计数器开始计数,然后通过设备地址先,向各设备发出一组地址信号. 到设备地址值与计数器值相同时,就获得总线使用权 |
独立请求 | 2n | 很复杂 | 响应速度快,优先次序灵活(通过程序改变) | 设备发出对应的请求信号,总线控制部件中有一个排队电路, 可根据优先次序确定响应设备. |
申请分配 -> 寻址 -> 传输/通信 -> 结束 -> 申请分配…
每个 PCI 设备配有此设备的 reg, 供 BIOS 自动获取, 无需手动设置
通信双方由统一时标控制数据传送
允许各模块速度不一致,更加灵活. 没有公共的时钟标准,不一颗球所有部件严格统一操作时间, 而是应用应答方式
(又称 握手方式
)
由第二点可见,对系统总线而言,从模块内部读数据过程并无实质性的信息传输,总线空闲。为了克服和利用这种消极等待,尤其在大型刘算机系统中,总线的负载已处于饱和状态,
充分挖掘系统总线每瞬间的潜力,对提高系统性能起到极大作用。
提出了“分离式”的通信方式
其基本思想是将一个传输周期(或总线周期)分解为两个子周期。在第一个子周期中,主模块A在得到总线使用权后将命令、地址以及其他有关信息,包括该主模块编号(当有多个主模块时,此号尤为重要)发到系统总线上,经总线传输后,由有关的从模块B接收下来。
主模块A向系统总线发布这些信息只占用总线很短的时间,一旦发送完,立即放弃总线使用权,
以便其他模块使用。在第二个子周期中,当B模块收到A模块发来的有关命令信号后,经选择、
译码、读取等一系列内部作,将A模块所需的数据准备好,使由B模块中请总线使用权,一旦
获准,B模块便将A模块的号、B模块的地址、A模块所需的数据等一系列信息送到总线上,供
A模块接收。很明显,上述两个传输子周期都只单方向的信息流,每个模块都变成了主模块。
等待对方的回答信号。
这种方式控制比较复杂,一般用于大型计算机系统
]]>如磁盘, 硬盘, 软盘, 常作为辅助存储器.
磁记录, 根据每个小磁针
的极性记录 0, 1. 写的时候, 改变电流方向利用电流的磁效应感性去磁性. 读的时候,利用电磁感应判断极性.
磁盘被组织成柱面, 每个柱面包含若干磁道,磁道数与垂直堆叠的磁头个数相同. 磁道被分成若干扇区.
重叠寻道(overlapped seek): 控制器同时操控多个驱动器进行寻道.
大多数磁盘都有一个虚拟的几何规格呈现给 OS, 控制器可以将虚拟的几何规格映射到实际的物理位置
(Redundant Array of Inexpensive Disk)
CPU 性能提升快于磁盘, 出现 并行 I/O
的思想—RAID ( 相对的, SLED(single large expensive disk)
RAID 背后的思想是将一个装满了的磁盘盒子安装到计算机上, 用RAID 控制器替换磁盘控制器卡,将数据复制到整个RAID 上, 然后继续常规的操作
对 RAID 的并行操作, 目前有0级到7级 RAID. 层级这个名称或许用词不当, 这里没有分层结构,只是不同的组织形式而已
0
0
~ k-1
扇区为 条带0, k
~2k-1
为条带1… 注意还未引入冗余, 实际上不是正真的 RAID真正的 RAID, 复制了所有的磁盘. 执行写, 每个 stripe 都被写了两次 执行读,可以任选一个副本. 具有很好的容错性(即时一个驱动器崩溃,有副本). 恢复简单:,安装一个新的驱动器复制到其上就可以了.
0和1操作的扇区条带, 而 2 是工作在字(甚至字节)的基础上. 如 将每个字节分割成 4位半字节对, 并形成 7 位的汉明码. 然后同步读写.
效率高,(即时损失一位, 汉明码可以轻松处理).
但是这要求所有驱动器旋转必须同步.
3 是 2 的简化, 为每个数据字计算奇偶校验位并写入即可.
使用stripe ,但是同样写有奇偶校验字., 然而计算 奇偶校验会降低效率.
在磁盘使用之前,每个盘片必须经由软件完成低级格式化(low-level format). 格式化的磁盘容量约为物理容量 70%.
时间, 主要是寻到时间较长.
算法
由于寻道和旋转延迟太影响性能了, 所以一次只读一两个扇区效率低下,. 许多磁盘控制器常读出多个扇区并进行高速缓存(独立于操作系统的 高速缓存).
制造时的瑕疵可能出现坏扇区, 厂商需要设置控制器来处理坏区, 有如下方法, 控制器需要维护一个映射表来替换坏区
也可以由操作系统软件来处理, 首先要获得坏区列表,然后建立重映射表.
另外的问题: 备份
操作系统要隐藏坏块, 使对备份应用程序不可见.
AV盘🙈😮
为了不丢失或损坏数据,保持磁盘的移植性,稳定存储器有必要的
它们是: 在写操作到来时, 要么成功执行, 要么对现有数据没有影响, 即时发生了磁盘或者 CPU 错误.并且是在软件中实现的
使用一对完全相同的磁盘,对应的块一同工作形成一个无差错的块. 定义如下三种操作
晶体震荡器, 计数器, 存储寄存器
可编程时钟的优点是其中断頻率可以由软件控制。可编程时钟芯片通常包含两个或三个独立的可编程时钟.
为了防止计算机的电源被切断时丢失当前时间,大多数计算机具有一个由电池供电的备份时钟,它是由在数字手表中使用的那种类型的低功耗电路实現的。电池时钟可以在系统启动的时候读出,如果不存在备份时钟,软件可能会向用户询问当前日期和时f对于一个连人网络的系统而言还有一种从远程主机获取当前时间的标群方法。无论是哪种情况,当前时间都要像UNIX所做的那样转换成自 1970 年 1 月 1 日 12 时 的UTC滴答数.
时间硬件所做的只是根据已知时间间隔产生中断,其他与时间有关的工作都是软件—时钟驱动程序完成的. 如
因为 32 位的寄存器以滴答计数最多计数 2 年
每启动一个进程,调度程序将一个计数器初始化为以滴答
为单位的该进程的时间片取值. 每次时钟中断时,时钟驱动程序将时间片计数器减少1,当为 0 , 时钟驱动程序就 调用调度程序
激活另一个进程
每启动一个程序, 需要对它使用的 CPU 时间计时,
注意: 在时钟中断期间, 时钟驱动程序需要做:
做很多事, 所以要仔细安排以加快速度
大多数计算机有辅助可编程时钟, 可以设置它以程序需要的任何速率引发定时器中断.
键盘包含一个嵌入式微处理器. 每按下一个键, 产生一个中断,释放键, 也产生一个中断.
I/O 端口中段数字是键标号, 称为 扫描码(scan code),而不是 ASCII 码.
键盘不超过128个, 所以只需7 个表示键编号, 第 8位为0 表示按下, 为 1 表示释放
键盘和监视器本来是两种 I/O 设备, 但用户的使用将他们联系起来, 即用户习惯 键入 , 然后在监视器上显示出来, 称为 echoing 回显
.
这里就需考虑回显带来的问题,
光学鼠标在其底部装备有一个或多个发光二极管和光电探瀏器。现代光学鼠标在其中有图像处理芯片并且获取处于它们下方的连续的低分辨率照片,寻找从图像到图像的变化。
鼠标可能具有一个,两个或者三个按钮,某些鼠标具有滚轮,可将额外的数据发送回计算机.
无线鼠标使用低功率无线电,例如使 Bluetooth将數据发这回计算机,而有线鼠标是通过导线将数据发送回。
当鼠标在随便哪个方向移动了一个确定的最小距离,或者按钮被接下或释放时。都会有一条消息发送给计算机。最小距离大约是0.1mm(尽管它可以在软件中设置)。有些人将这一单位称为一个鼠标步(mickey)
发送的消息为 (Δx,Δy,button)
. 通常,消息占3字节, 速度最多每秒40次.
注意,鼠标仅仅指出位上的变化,而不是绝对位置本身。如果轻轻地拿起鼠标并且轻轻地放下而
不导致橡皮球旋转.那么就不会有消息发出。
某些GUI 区分单击与双击鼠标接钮。如果两次点击在空间上〈鼠标步)足够接近,并且在时间上(亳秒)也足够接近,那么就会发出双击信号。最大的“足够接近”是软件的事情,并且这两个参数通常是用户可设置的。
几乎所有 UNIX 系统的用户界面都以 X 为基础. 如 GNOME, KDE
当 X 在一台机器上运行时, 采集键盘与鼠标输入并且输出到屏幕上的软件称为 X server
X server 常位于用户计算机的内部, 而 X 客户可能在远程计算服务器上.
注意 X 只是一个窗口系统, 不是一个完全的 GUI. 要获得完全的 GUI, 需要在器上运行其他软件层.
计算机状态: 工作, 睡眠, 休眠, 关闭
权衡: 消耗电量从左到右递减, 关闭状态不耗电量. 但是从睡眠, 休眠状态恢复到工作状态, 后者需要更多的时间和电量.
通过算法或者试探, 让 OS 对关于关闭什么设备以及何时关闭能够作出良好的决策
一段时间后可以关闭屏幕(是睡眠,可以立即唤醒))
改进: 将屏幕分成多个区域, 可以关闭当前窗口(或者用户自己定义)未覆盖的区域. 窗口管理器还可以使窗口与区域对齐, 进一步地, 部分照亮关闭的区域
即时不存在存取操作,硬盘也消耗大量的能量以保持高速旋转.(不然加速需要很长时间 呢(●ˇ∀ˇ●)). 但是注意 停止硬盘是休眠不是睡眠.
此外,重新硬盘启动会消耗更多的能量.
因此, 每个硬盘有一个特征时间 T, 为它的盈亏平衡点. 如果能预测将来多久才用到硬盘(可以基于存取历史), 那么如果将来时间讲个 ΔT > T, 就可以关闭.
睡眠中的 CPU 几乎不耗电, 只需等待中断的到来才唤醒.
CPU 电压可以用软件降低,但会降低时钟速度. 由于电能消耗与电压的平方成正比, 而电压与时钟速度成正比, 所以可以有降低的平衡点来盈利
cache 可以重新加载且不损失信息, 而且速度快,
I/O 硬件原理
把信息存储在固定大小的块中,每个块都有自己的地址. 每个块可以独立于其他块读写. 如 硬盘, CD-ROM , USB 盘 …
字符设备以字符为单位发送或接收一个字符流, 而不考虑任何块结构. 它是不可寻址的.
如打印机,网络接口, 鼠标(用作指点设备)…
I/O 设备一般由两部分组成: 机械部分和电子部分.
电子部分就是设备控制器. 常以插入(PCI)扩展槽中的印刷电路板的形式出现.
控制器与设备之间的接口是很低层次的接口. 它的任务就是把串行的位流转换为字节块,并进行必要的错误校正.
每个控制器有几个寄存器, OS 可以读写来了解,更改设备的状态信息. 控制器还有 OS 可以读写的 数据缓冲区.
问题来了: CPU 如何与设备的控制寄存器和数据缓冲区通信.
如 IN REG, PORT
将读取控制器寄存器 PORT 中的内容到 CPU 寄存器 REG
CPU 读入一个字时, 不论是从内存还是 I/O 端口, 都将目的地址放在总线的地址线上, 总线控制线置 READ 信号看. 还要用一条线表明是 I/O 空间 还是内存空间. 如果是 I/O空间, I/O设备将响应请求.
对于`内存映射 I/O ,设备控制寄存器只是内存中的变量, 和其他变量一样寻址,可以用 C 语言编写驱动程序
独立于 CPU 访问系统总线
也就是不用浪费 CPU 处理缓冲区到内存的时间, 相当于另有一个” CPU “ 专门处理 磁盘 到 内存 的 I/O
注意 上面的操作是字模式传送, 在 DMA 请求传送一个字并且得到这个字时, CPU 不能使用总线,必须等待.
上面是字传输模式, 对于块模式下的传送, DMA 会发起一连串的传送,然后才释放总线. 这比周期窃取效率更高.
上面 的模式是飞越模式(fly-by mode)
, 即 DMA 控制器直接通知设备控制器将数据传送到 主存, 只请求一次总线
某些 DMA 使用其他模式. 让设备控制器将字发送到 DMA, 然后 DMA 再 请求总线将数据发送到其他地方(其他设备, 主存…), 这样会多消耗一个总线周期, 但是更加灵活: 可以 设备->设备
, 内存->内存
(内存读, 然后 内存写)
不使用 DMA 的考虑:
当一个 I/O 设备完成它的工作后,它就产生一个中断, 通过在分配给它的一条总线信号线上置起信号.
如果有多个中断请求, 按优先级, 如果还没有被处理, 设备一直发出中断知道得到 CPU 服务
中断控制器通过在地址先上放置一个数字(中断向量 interrupt vector)
表明哪个设备需要关注,同时向 CPU 发出中断
中断信号导致 CPU 停止当前工作, 并处理其他事情. 根据中断向量跳转到需要的中断服务程序
如果在堆栈中, 使用谁的堆栈?
在流水线满的时候,如果出现一个中断, 由于许多指令处于不同的正在执行的截断. 程序计数器可能无法正确反应已经执行的指令和未执行之间的边界.
在超标量机器上, 指令可能分解成微操作, 为操作可能乱序执行
不满足上面的条件
]]>让 CPU 做全部 I/O工作,成为程序控制 I/O
CPU 要不断地查询设备, 这成为 polling
或 busy waiting
缺点是 中断发生在每个事件上, 同样要花一些时间,
如打印一个缓冲区的字符, 每个字符都要中断一次
需要特殊的硬件 DMA 控制器, 每个缓冲区中断一次
每个连接到计算机上的 I/O 设备都需要某些设备特定的代码来对其进行控制 , 注意 设备控制器是硬件上的, 驱动程序是软件上的.
为了访问设备的硬件(即设备控制器的寄存器), 设备驱动程序需要是系统内核的一部分.
其实也可以构造运行在用户空间的驱动程序,使用系统调用来读写设备寄存器. 这样可以使内核与驱动程序, 驱动程序之间隔离, 消除驱动程序干扰内核造成的系统崩溃.
驱动程序在执行期间动态地装在到系统
功能
如果一个进程打开它, 然后很长时间不使用, 则其他进程都无法打印 . 另外一种方法是 创建一个 守护进程(daemon)
和假脱机目录
. 一个进程要打印一个文件时, 首先生成要打印的整个文件, 并且放在假脱机目录, 由守护进程打印该目录下的文件, ,,守护进程是唯一允许使用打印机特殊文件的进程.
操作系统引论
抽象是管理复杂性的一个关键, 好的抽象可以把一个几乎不可能管理的任务划分为两个可管理的部分:
在相互竞争的程序之间有序地控制对处理器, 存储器以及其他 I/O 接口设备的分配
ENIAC , 程序设计是用纯粹的机器语言,
使用: 程序员在墙上的机时表上预约一段时间,,然后到机房中将他的插件板接到计算机中,在接下来的几小时等待(计算的都是简单的数字运算, 如制作对数表)
批处理系统(batch system), 在输入室中手机全部的作业 ,然后用一台相对便宜的计算机,读到磁带上. 磁带被送到机房里并装到磁带机上. 然后操作员装入一个特殊的程序(现代操作系统的前身), 它从磁带上读入第一个作业并运行. 如此反复
多道程序设计(multiprogramming): 若当前作业因等待磁带或其他 I/O 操作而暂停时, 为了不让 CPU 一直等待这一个作业, 将内存
分几个部分, 每一部分存放不同的作业, 在一个作业等待 I/O 时, 可以让另一个作业使用 CPU.
在内存中防止多个作业需要特殊的硬件来保护, 以免作业的信息被窃取或受到攻击.
程序员希望得到更快响应, 由此出现分时系统(timesharing)
.
大规模集成电路(LSI)的发展, 晶体管个数在单位平方厘米面积的芯片上可达数千个.
逐步发展处 网络操作系统, 分布式操作系统
传统上,所有的层都在内核中, 但是这样并没有必要. 尽可能减少内核态中功能的做法更好. 要知道, 代码量越大, bug 越多.
思想
为了实现高可靠性,将操作系统划分成更小的, 良好定义的模块. 只有其中一个模块—微内核
运行在内核态上.
将进程划分为两类,
通常在系统最底层是微内核
敏感指令( sensitive instruction )
.:有内核态
和用户态
的 CPU 的一组只能在内核态执行的指令集, 比如 I/O 指令, 改变 MMU 状态的指令等,特权指令( privileged insttruction)
: 在用户态下执行会引起陷入当 敏感指令是 特权指令的子集时, 机器才是可虚拟化的
解决的基本思想: 创建容器使得虚拟机在其内运行
在其上的 OS 称为客户操作系统
虚拟机在用户态以用户进程的身份运行, 因此不允许执行敏感指令,否则崩溃
在支持VT技术的 CPU 上, 客户操作系统执行敏感指令会发生陷入. 管理程序分析指令
在其上的 OS 称为宿主操作系统,如上图 b
VMware就是采用的这种管理程序:
当运行一个二进制文件, VMware 先浏览代码段以寻找基本块(basic block)
.
所谓基本块就是以 jump, call, trap 等改变控制流的指令结束的可顺序执行的指令序列, 而基本块中就不含其他改变 程序计数器 的指令
如果基本块中含有敏感指令, VMware 将其替换为相应的 VMware 过程调用, 基本块的最后一条指令也被过程调用替换
这种 找出, 仿真敏感指令 的技术称为 二进制翻译(binary translation)
所以, 即使在不可虚拟化的硬件上, Ⅱ型 也能正常: 因为所有的敏感指令被仿真, 不会被真正的硬件执行,管理程序的调用可以代替.
前面介绍的两种, 在其上的客户操作系统都是没有修改过的. 可以更改客户操作系统的源码, 将其中的敏感指令都转换为 管理程序调用. 这就要给管理 程序 定义 过程调用集合, 从而形成 API, 虽然这个接口是供客户 OS, 而不是 应用程序.
其实就将管理程序变成了一个微内核.
这种方法就是准虚拟化, 这样会使得虚拟机技术更容易被支持和使用
问题:
Amsden 的一个解决方案:
当内核需要执行一些敏感指令操作时会调用特殊的例程(称为 VMI 虚拟机接口), VMI 形成的底层与硬件或管理程序进行交互. 将 VMI 设计得通用化, 不依赖硬件或特定的管理程序