math

总览

这篇博客用于记录数学库的实现,

项目地址

链接

先介绍我们的数学函数库 math_function

这里记录了很多数学函数,

math_function代码

重大问题

这些算法的复杂度感觉都是$lgc$级别的,应该是可以通过倍增来达到$lg(lgc)$的复杂度,我下次再仔细思考思考。

介绍我们的牛顿迭代法求$\sqrt{c}$

$$
\begin{aligned}
&x^2=c\
&f(x) = x^2-c\
&f’(x) = 2x \
&g(x) = x-\frac{f(x)}{f’(x)} = x-\frac{x^2-c}{2x} =\frac{x^2+c}{2x}
\end{aligned}
$$
按照$x_i=g(x_{i-1})$进行迭代即可求出结果。
更新: 我下次找个机会用下面求$e^c$的方法实现一下先让c变小,看看能不能加速。

介绍我们的泰勒展开求$e^c$

首先根据公式$e^{-t}=\frac{1}{e^t}$,可以递归为c恒非负
然后根据公式$e^t=(e^\frac{t}{2})^2$, 可以递归为c在范围$[0,0.001]$上
最后使用泰勒展开,$e^x=1+x+\frac{x^2}{2!}+…$,这里我们取前10项就能够达到很高的精度了。
为什么要用第二步将c保证在范围$[0,0.001]$上? 因为如果c过大,我们的第三部需要展开更多的项才够,这在c达到10000这种,你至少要展开10000项,这不现实。

介绍我们的牛顿迭代法求$ln(c)$

$$
\begin{aligned}
&e^x=c
\&f(x)=e^x-c
\&f’(x) = e^x
\&g(x)=x-\frac{f(x)}{f’(x)} = x-1+\frac{c}{e^x}
\end{aligned}
$$
还是一样的,为了减少迭代次数,我们先对c进行变小,根据公式$ln(x)=ln(\frac{x}{e})+1$,我们可以保证c的值在e附近,
最后使用迭代,$x_i=g(x_{i-1})$,
更新: 我刚刚突然想到如果第二步使用泰勒展开而不是牛顿迭代,可能会快很多,考虑到这一点,我们有时间去实现一下泰勒展开求对数函数。

vim入门教程

vim

vim是一款强大的文本编辑器,如果配置到位,真的真的非常漂亮,如下图violet主题的浅色和深色

还有经典的molokai配色主题

还有c++高亮配色

c++补全

多行编辑

笔者的vim经历

先后尝试vim,笔者已经度过了两年,学到了很多,却也很少,

vim安装

以前笔者使用过linux下的vim,现在正使用的mac下的vim,这里只讲mac如何安装vim,mac本身自带vim,当然mac也可以使用指令

1
brew install vim

vim基本配置

vim是需要简单配置一下的,对于没有配置的vim而言,会很难受,下面我先发一下我的vim配置

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
set et "tab用空格替换

set tabstop=2
set expandtab
" Tab键的宽度

set softtabstop=2
set shiftwidth=2
" 统一缩进为2

set number
" 显示行号

set history=10000
" 历史纪录数

set hlsearch
set incsearch
" 搜索逐字符高亮

set encoding=utf-8
set fileencodings=utf-8,ucs-bom,shift-jis,gb18030,gbk,gb2312,cp936,utf-16,big5,euc-jp,latin1
" 编码设置

" set mouse=a
" use mouse

set langmenu=zn_CN.UTF-8
set helplang=cn
" 语言设置

set laststatus=2
" 总是显示状态行 就是那些显示 --insert-- 的怪东西

set showcmd
" 在状态行显示目前所执行的命令,未完成的指令片段亦会显示出来

set scrolloff=3
" 光标移动到buffer的顶部和底部时保持3行距离

set showmatch
" 高亮显示对应的括号

set matchtime=1
" 对应括号高亮的时间(单位是十分之一秒)

vim的插件

这里推荐vundle,安装vundle后,我们的配置前面就多了一些东西

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
" Vundle set nocompatible
filetype off
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()
Plugin 'VundleVim/Vundle.vim'
Plugin 'The-NERD-Tree'
Plugin 'gdbmgr'
Plugin 'mbbill/undotree'
Plugin 'majutsushi/tagbar'
Plugin 'vim-airline/vim-airline' " 状态栏
Plugin 'vim-airline/vim-airline-themes' "状态栏
Plugin 'cohlin/vim-colorschemes' " 主题
Plugin 'tomasr/molokai' " molokai
Plugin 'jiangmiao/auto-pairs' " 括号补全
Plugin 'plasticboy/vim-markdown'
Plugin 'iamcco/mathjax-support-for-mkdp' " 数学公式
Plugin 'iamcco/markdown-preview.vim' " markdown预览
Plugin 'Valloric/YouCompleteMe'
Plugin 'zxqfl/tabnine-vim'
Plugin 'w0rp/ale' " 语法纠错
Plugin 'octol/vim-cpp-enhanced-highlight' " c++语法高亮
Plugin 'Shougo/echodoc.vim' " c++函数提示
Plugin 'Chiel92/vim-autoformat' " c++代码格式化
Plugin 'scrooloose/nerdcommenter' " c++代码注释
Plugin 'ashfinal/vim-colors-violet' " 配色
Plugin 'terryma/vim-multiple-cursors' " vim 多行编辑
Plugin 'mhinz/vim-startify'
call vundle#end()
filetype plugin indent on

这里的Plugin “…”指的是使用啥啥啥插件的意思。

vim基础操作

下面我们进入到最核心的地方,vim的快捷操作

vim 基本移动操作

基本跳转

jkhl分别对应了上下左右

字符串跳转

b是向前跳转一个单词,w是向后跳转一个单词

行内跳转

$跳转到行末,A跳转到行末并输入,0跳转到行首,^跳转到行首非空字符,I跳转到行首非空字符并输入

f+a跳转到后面的第一个a, F+a跳转到前面第一个a

行间跳转

gg到首行,G到尾行, :100到100行

H到屏幕顶,M到屏幕中,L到屏幕底

屏幕跳转

zz把当前行变为屏幕正中间。

向上移动一行,向下移动一行

向上整个屏幕, 向下整个屏幕

文件跳转

:bn到缓冲区下一个文件,bp到前一个

:A .c与.h文件跳转

:IH 到光标指向文件

vim 多行操作

用插件会卡,这里我们可以, 移动,I,写,ESC

指令10,20s/^/#/g

vim 高质量跳转

% 跳转括号

vim 高质量组合操作

c 删除当前字符并插入

caw change a word删除当前单词并插入

1
2
3
4
one
two
three
four

渴望变成

1
one,two,three,four

先定位到one的o,然后 进入列选择,3j将列选择光标移动到four的f,$将光标移动到尾部,A进入插入模式,,添加逗号,Esc作用与所有列,V进入块选择,3j定位到four,J将行合并,结束了。

heap

总览

这篇博客将用于整理各种堆的数据结构代码以及复杂度证明: 二叉堆、二项堆、斐波拉契堆、配对堆、左偏树、斜堆、bordal队列、B堆

注意

全部代码单对象测试通过,部分代码未实现拷贝构造函数达到深拷贝。

heap

堆是一种非常重要的数据结构,在计算机科学中,堆一般指堆是根节点比子孙后代都要大(小)的一种数据结构。

项目地址

链接

前置条件

基本数据结构:变长数组、栈、队列、字符串的实现(此时暂未实现,使用STL代替,后面有时间会自己实现)
内存池机制
{ post_link 势能分析}

基类设计

在这里我们暂且只设置三个接口,如果不够,我们再补充。

heap代码

binary heap

二叉堆,就是我们常见的堆,也是大多数人常用的堆,二叉堆是一个完全二叉树,满足根节点的值大于(小于)子孙的值。我们设计他的时候,采取下标从1开始的数组来直接模拟,使用i,2i,2i+1之间的关系来完成边的构建。

push

我们将数放入数组尾部,并不断上浮即可。细节稍微推一下就出来了。每个元素最多上浮堆的高度次,复杂度$O(lgn)$

pop

我们直接删掉第一个元素,并让最后一个元素顶替他,然后下沉即可。这里个细节也是稍微推一下就出来了。每个元素最多下沉堆的高度次,复杂度$O(lgn)$

top

就是第一个元素,复杂度$O(1)$

代码如下:

binary heap代码

binomial heap

二项堆,是一个堆森林,其中每个堆以及各自的子堆的元素个数都是2的幂,并且森林中没有两个堆元素个数相同的堆。举个简单的例子,一个包含了6个元素的二项堆,这个堆森林中一定是两个堆组成,因为6=2+4,他不能是6=2+2+2由三个堆组成,因为森林中不允许出现元素相同的堆。

堆的具体形状

*** 图片源于wiki***

merge

二项堆天然支持合并,即可并堆,当合并的时候,我们很容易发现,只要将森林合并即可,而对于哪些出现的元素个数相同的堆,我们可以两两合并,让其中一个作为另一个的根的直接儿子即可。每次合并的时候,两两合并的复杂度是$O(1)$,最多合并的次数等于森林中元素的个数减1,而森林中堆的个数等于二进制中1的个数,这个是O(lgn)级别的,所以总体复杂度O(lgn)

push

可以看作与一个只包含了一个元素的堆合并,每次push,最坏的情况下时间复杂度为$O(lgn)$,但是多次连续的push,均摊时间复杂度就不一样了,我们来分析一下n次连续push的情况,森林中的堆两两合并的次数等于时间复杂度,定义函数$f(x)$,表示在森林中所有堆中的元素个数的总和为$x$的情况下,push一个值以后,堆中合并发生的次数,显然$f(x)=$x的二进制表示中末尾连续的1的个数,不难发现
$f(x)>=1$的时候$x%2=1$,
$f(x)>=2$的时候$x%4=3$,
$f(x)>=3$的时候$x%8=7$
这里我们通过计数原理推算出
$$
\begin{aligned}
\sum_{i=1}^n{f(i)}=\lfloor\frac{x+1}{2}\rfloor+\lfloor\frac{x+1}{4}\rfloor+\lfloor\frac{x+1}{8}\rfloor+…+\lt x+1
\end{aligned}
$$
所以在大量连续的push过程中,均摊时间复杂度为O(1)

pop

先遍历各个根,找出最值,不难发现,森林中,任意一个堆去掉根以后,恰好也是一个满足条件森林,这里也可以用合并处理,时间复杂度$O(lgn)$

top

遍历所有堆即可,时间复杂度O(lgn)

程序设计

很多人说用链表实现链接,这确实是一个好方法,但是如果用单链表或循环链表或双向链表实现,则有很多局限性,下面代码中也提及了。我这里采取的是使用数组存森林,使用左儿子右兄弟的手段,将多叉树用二叉树来表示。这个方法非常棒。

代码

binary heap代码

fibonacci heap

斐波拉契堆,是目前理论上最强大的堆,他和二项堆很长得很相似。和二项堆一样,斐波拉契堆也是一个堆森林,斐波拉契堆简化了几乎所有的堆操作为懒惰性操作,这极大的提升了很多操作的时间复杂度。

potential method

对于一个斐波拉契堆$H$,我们定义势能函数为$\Phi(H) = t(H) + 2m(H)$, 其中$t(H)$是斐波拉契堆$H$的森林中堆的个数,$m(H)$是斐波拉契堆中被标记的点的数量。

push

当我们向一个斐波拉契堆中添加元素的时候,我们会选择将这个元素做成一个堆,然后链入森林的根集和,常常选择链表维护根集合,同时更新斐波拉契堆中最小值的指针,实际时间复杂度显然是$O(1)$,势能变化为1,因为堆的个数变大了1,均摊复杂度为$O(1)+1=O(1)$

merge

当我们合并两个斐波拉契堆的时候,我们是懒惰操作,直接将这两个堆森林的根集合并为一个根集,常常选择链表来维护根集合,同时更新新的最小值指针,实际实际复杂度为$O(1)$,势能无变化,均摊复杂度为$O(1)$

top

$O(1)$

decrease

当我们想要减小一个节点的值堆时候,我们直接将他和父亲断开,然后将他链入森林并减小值,然后标记父亲,如果父亲被标记过一次,则将父亲和爷爷也断开并链入森林,并清除标记,一直递归下去,这里我们不要太认真,加上这条路上一个有$c$个,则我们一共断开了c次,实际复杂度为$O(c)$,势能的第一项变为了$t(H)+c$,第二项变为了$2(m(H)-c)$,于是势能的变化为$c-2c=-c$,于是均摊复杂度为$O(c)-c$,这里本来并不等于$O(1)$,但是我们可以增大势的单位到和这里的$O(c)$同级,这样就得到了$O(1)$

erase

当我们想要删除一个节点的时候,先将其设为无穷小,然后在调用pop

pop

前面偷了很多懒,导致除了erase以外,其他操作的均摊复杂度均为$O(1)$,这里就要好好地操作了,我们是这样来操作的,删掉最小值以后,将他的儿子都链入森林,这里花费了$O(D(H))$的实际代价,这里的$D(H)$指的是斐波拉契堆$H$中堆的最大度数。然后我们更新top的时候,不得不遍历所有的根,这时候我们就顺便调整一下堆。我们遍历所有的根,依次对森林中所有的堆进行合并,直到没有任意两个堆的度数相同,假设最后我们得到了数据结构$H’$,那么这个过程是$O(t(H)-t(H’))$的,于是时间复杂度为$O(t(H)-t(H’))+O(D(H))$,然后我们观察堆的势能变化,显然第一项的变化量为$t(H’)-t(H)$,第二项无变化,即势能总变化为$t(H’)-t(H)$,则均摊复杂度为$O(t(H)-t(H’))+O(D(H))+(t(H’)-t(H))$,这里依然不一定等于$O(D(H))$,但是我们依然可以增大势的单位到能够和$O(t(H)-t(H’))$抵消,最终,均摊复杂度成了$O(D(H))$

D(H)

现在我们进入最高潮的地方。我们考虑斐波拉契堆中一个度数为k的堆,若不考虑丢失儿子这种情况发生,我们对他的儿子按照儿子的度数进行排序,显然第i个儿子的度数为i-1,$i=1,2,3…k$,此时考虑儿子们会丢掉自己的儿子,则有第i个儿子的度数$\ge i-2$,在考虑他自己也会丢失儿子,但这不会影响到第i个儿子的度数$\ge i-2$这个结论。

斐波拉契数列

$$
\begin{aligned}
F_i =
\left{
\begin{aligned}
&0&i=0\
&1&i=1\
&F_{i-2}+F_{i-1]}&i\ge 2\
\end{aligned}
\right.
\end{aligned}
$$

斐波拉契数列的两个结论

$F_{n+2}=1+\sum_{i=1}^nF_i$
$F_{n+2}\ge \phi^n,\phi^2=\phi+1,\phi大约取1.618$

比斐波拉契数更大

容易用数学归纳法证明对于一个度数为k的堆,他里面的节点的个数$\ge F_{k+2}$,这里$F_{i}$是斐波拉契数列的第i项。
当k=0的时候,节点数数目为1,大于等于1
当k=1的时候,节点数数目至少为2,大于等于2
若$k\le k_0$的时候成立, 则当$k=k_0+1$的时候,节点数目至少为$1+F_1+F_2+…+F_{k_0+1}=F_{k_0+3}=F_{k+2}$

比黄金分割值的幂更大

现在我们就能够得到一个结果了,一个度数为k的堆,他的节点个数至少为$\Phi^k$,这里我们很容易就能得到这样一个结果,$D(H)\le \log_\Phi 最大的堆内元素的个数$

结尾

至此我们已经全部证明完成,读者也应该知道为什么斐波拉契堆要叫这个名字。

fibonacci heap 代码

fibonacci heap代码

pairing heap

配对堆,名字来源于其中的一个匹配操作。很有趣,他的定义就是一个普通多叉堆,但使用特殊的删除方式来避免复杂度退化。是继Michael L. Fredman和Robert E.Tarjan发明斐波拉契堆以后,由于该数据结构实现难度高以及不如理论上那么有效率,Fredma、Sedgewick、Sleator和Tarjan一起发明的。

potential method

我们有一个配对堆$H$,其中有n节点$node$,$node_i$有$d_i$个儿子,
则有
$F(H) = \sum F(node)$,
$F(node_i)=1-min(d_i,\sqrt{n})$
复杂度证明方面,等我把论文看完再来整这个,感觉证明比斐波拉契堆更复杂。

merge

合并的时候,让次大堆做最大堆的儿子,显然时间复杂度$O(1)$

push

插入的时候,看作与只包含了一个元素的堆合并,所以$O(1)$

top

就是根 $O(1)$

pop

当我们删除根以后,会形成一个堆森林,这时我们从左到右,每两个连续的堆配对合并一次,然后从右到左依次合并。比方说有这样一个情况AABBCCDDEEFFGGH,我们先将其从左到右合并AA->A,BB->B…得到ABCDEFG,->ABCDEH -> ABCDI -> ABCJ -> ABK -> AL -> M

程序设计

同样的左儿子右兄弟

代码

pairing heap代码

leftist heap

左偏树、左式堆、左翼堆是一个堆,除此以外,他定义了距离,没有右子节点的节点的距离为0,其他节点的距离为右子节点的距离加1,在这个定义下,左偏树的左偏体现着每个节点的左子节点的距离不小于右子节点的距离。

push

为新节点建立堆,然后与堆合并 $O(lgn)$

pop

删除根节点,合并左右子树 $O(lgn)$

top

根节点 $O(1)$

merge

$O(lgn)$, 当我们合并两个堆$H1,H2$的时候,我们只需要比较这两个堆顶的大小,不妨设H1小,并设H3、H4为H1的左右儿子,则我们可以这样来看待,我们将H3,H4从H1中断开,递归合并H4和H2位H5,这时候我们还剩下H3、H5以及H1的堆顶,我们根据左偏树的定义,选择H3、H5分别作为左右或右左儿子即可,

复杂度证明

算法中每次均选择右儿子递归向下,这导致时间复杂度与右儿子的右儿子的右儿子的…有关,这里不难发现递归的次数就是节点的距离。根据左距离不小于右距离,我们很容易就能得到这个距离是$O(lgn)$级别的。

leftist heap 代码

leftist heap代码

skew heap

我们的左偏树不记录距离,并且每次递归的时候无条件交换左右儿子,则成了斜堆。

复杂度证明

potential method

定义斜堆中的右子树距离比左子树大的节点为重节点,否则为轻节点。
定义势能函数为重节点的个数。

merge

当我们合并两个斜堆$H_1,H_2$的时候,不妨设他们的右子节点链中的轻重儿子为$l_1,h_1,l_2,h_2$,则时间时间复杂度为$O(l_1+h_1+l_2+h_2)$,经过交换以后,链上的重节点一定会变成轻节点,轻节点可能会变为重节点,我们取最坏的情况,即轻节点全部变为重节点,这时势能的变化量为$l_1+l_2-h_1-h_2$,最后我们的均摊复杂度为$O(l_1+h_1+l_2+h_2)+l_1+l_2-h_1-h_2$,我们依然可以增大势的单位,直到足以抵消所有的h,最终均摊复杂度为$O(l_1+l_2)$,这里不难证明,一条右儿子构成的链上,轻节点的个数是对数级别。

skew heap代码

skew heap代码

bordal heap

这里已经涉及到一些冷门的东西了。暂时先放一下

B heap

是一种和B树一样利用内存页的东西。冷门,先放一下

wiki上还有数不清的堆,学到这里暂停一下

势能分析

势能分析简介

势能分析是一种常用的数据结构时间复杂度分析的手段,我们常常会定义一个势能函数,用于评价数据结构某个状态的势能,把每个操作的时间复杂度加上操作导致的势能变化作为摊还复杂度,如果经过了一系列操作以后,势能不减少,这一系列操作的时间复杂度之和不大于这一系列操作的摊还复杂度之和。

势能分析更加严谨的简介

我们对一个初始数据结构$D_0$执行$n$个操作,对于每个$i=1,2,…,n$令$C_i$为第$i$个操作的实际代价,令$D_i$为在数据结构$D_{i-1}$上执行第$i$个操作后得到的结果数据结构,势能函数$\Phi$将每个数据结构$D_i$映射到一个实数$\Phi(D_i)$,此值即为关联到数据结构$D_i$的势,第$i$个操作的摊还代价$\hat{C_i}=C_i+\Phi(D_i)-\Phi(D_{i-1})$,则$n$个操作的总摊还代价为$\sum_{i=1}^n\hat{C_i}=\sum_{i=1}^n{C_i}+\Phi(D_n)-\Phi(D_0)$,如果势能函数满足$\Phi(D_n)\ge\Phi(D_0)$,则总摊还代价$\sum_{i=1}^n\hat{C_i}$是总实际代价$\sum_{i=1}^nC_i$的一个上界

后记

笔者在此不会做势能分析,能进行势能分析的东西太多了,例如splay、pairing heap、fibonacci heap、link cut tree等等,我们将其留在后边的博文中详细介绍。

矩阵的特征值与特征向量

### 矩阵特征值的与特征向量 若矩阵$A$,列向量$X$,常数$\lambda$满足$AX=\lambda X$,则我们称$\lambda$是$A$的一个特征值,$X$是$A$的一个特征向量

解析解

一元高次方程$det(X-\lambda E)=0$,这在X的阶很高的时候,几乎是无用的。

近似解

因为我们难以得到矩阵特征值的解析解,所以这里使用近似解来逼近。

Given变换

Given变换是一种旋转变换,他的变换矩阵与单位矩阵相比只有四个元素不一样,变换矩阵如下
$$
\begin{aligned}
\left[\begin{matrix}

&1\
&&.\
&&&.\
&&&&.\
&&&&&\cos\theta &&&&\sin\theta\
&&&&&&.\
&&&&&&&.\
&&&&&&&&.\
&&&&&-\sin\theta &&&&\cos\theta\
&&&&&&&&&&.&\
&&&&&&&&&&&.&\
&&&&&&&&&&&&.&\
&&&&&&&&&&&&&1&\
\end{matrix}\right]
\end{aligned}
$$
不难证明这个矩阵是正交矩阵,不难证明左乘这个变换只会改变两行,右乘这个矩阵只会改变两列

Hessenberg矩阵

次对角线下方元素为0
$$
\begin{aligned}
\left[\begin{matrix}
&x&x&x&x&x\
&x&x&x&x&x\
&&x&x&x&x\
&&&x&x&x\
&&&&x&x\
\end{matrix}\right]
\end{aligned}
$$
任何一个方阵都有上海森伯格形式的相似矩阵,

幂法

幂法是最基础的算法,我们先来描述一下这个过程
我们随机选择一个初始列向量$Y$,假设它能够被矩阵A的特征向量线性组合出来,则$$\begin{aligned}\lim_{N\to\infty}A^NY\end{aligned}=一个特征向量$$
这里使用快速幂算法就亏大了快速幂迭代一次$O(N^3)$,普通迭代一次$O(N^2)$,所以普通迭代就行了,
证明: 对于大部分$Y$来说,如果它能够被组合出来即$Y=k_1X_1+K_2X_2+K_3X_3+…$,且满足特征值满足条件$\lambda_1>\lambda_2>…$
则有$A^NY=k_1A^NX_1+k_2A^NX_2+…=k_1\lambda_1^NX_1+k_2\lambda_2^NX_2+…$,所以这个极限是显然趋近于特征值绝对值最大的特征向量的。
所以这个算法在大多数情况下都能成功。考虑到幂法会增长很快,我们可以在迭代过程中单位化。

反幂法

求逆以后在用幂法,我们会得到特征值最小的特征向量,这很容易证明。

jacobi迭代法

只能处理对称矩阵
这个算法使用相似矩阵,每次使用一个Given变换,让绝对值最大的非对角线上的元素变为0,这导致了整体势能的下降,最终相似矩阵的非对角线元素会趋近于0,Given变换是一个稀疏矩阵,他和单位矩阵只有四个元素不同,是一种旋转矩阵,加上相似变换以后,这导致他只会改变两行和两列,最终我们就得出了特征值。

QR迭代法

还是先说做法,再给出证明,根据QR分解我们有$A=QR$,构造$A_2 = RQ = Q^{-1}AQ$,我们就不难发现$A_2$与$A$相似,我们用同样的办法,从$A_1$得到$A_2$,从$A_2$得到$A_3$…不断的迭代下去,最终$A_i$对角线一下的元素会趋近于0,这是特征值就算出来了.QR算法的本质其实还是幂法,
我们考虑幂法的过程,他可以求出一个特征向量,如果我们在幂法结束以后,得到了$X_1$,然后我们再随机选择一个$Y_2$,把$Y_2$中$X_1$的分量去掉,然后进行幂法迭代,这时候我们会得到特征值第二大的特征向量,因为$Y_2$再去掉$X_1$方向上的分量以后,已经不再包含$X_1$方向上的值了,也即$k_1$为0,这时候幂法得到的极限是第二大特征向量,随后我们可以顺序得到第三大、第四大、、、,这样太蠢了,我们考虑一次性把他们呢求出来,我们一次性就选择n个随机向量构成矩阵Z,然后用A左乘得到AZ,然后对AZ
使用斯密斯正交化得到$Z_2$,可以证明$Z_n$将趋近于A的所有特征向量构成的矩阵。证明很多细节地方没有处理,这也就是为什么QR算法会失败的原因,但QR算法在大多数情况下是能够成功的,
即我们得到了算法迭代$Y_i=GramSchmidt(AY_{i-1})$,这个算法叫归一化算法,和上面那个算法优点小区别,但本质上是一样的,只是标记不一样而已。
如果我们能够提前把矩阵变为上海森伯格形式,QR算法的速度将大大提高。

矩阵分解

矩阵的分解

矩阵的分解非常重要,很多时候我们都需要使用到矩阵的分解,这会给我们提供极大的方便,笔者学习这一类问题花费了很多时间,想要看懂这一章,需要先看{ post_link 矩阵的类型及性质 }

矩阵的特征值分解

要求$n*n$矩阵拥有$n$个线性无关的特征向量
矩阵的特征值分解指的是利用特征值构造的矩阵进行分解。特征值与特征向量是这样定义的
$$
\begin{aligned}
&若矩阵A,列向量X,常数\lambda满足
\&AX = \lambda X
\&则X为A的特征向量,\lambda为A的特征值
\end{aligned}
$$

Read More

矩阵的类型以及性质

实矩阵

常见的几种实矩阵有: 实对称矩阵、实反对称矩阵、厄米特矩阵、反厄米特矩阵、正交矩阵、对角矩阵、酉矩阵、正规矩阵

实对称矩阵

定义

若$A$为对称矩阵、则:
$$
\begin{aligned}
A = A^{T}
\end{aligned}
$$
这里左边为矩阵本身,右边为矩阵的转置

性质

对称矩阵必然有$n$个实特征向量,并两两正交

实反对称矩阵

定义

若$A$为对称矩阵,则:
$$
\begin{aligned}
A = -A^{T}
\end{aligned}
$$

厄米特矩阵

定义

若$A$为厄米特矩阵,则
$$
\begin{aligned}
A = A^H
\end{aligned}
$$
右边是矩阵的转置共轭矩阵

反厄米特矩阵

定义

若$A$为反厄米特矩阵,则
$$
\begin{aligned}
A = -A^H
\end{aligned}
$$

正交矩阵

定义

若$A$为正交矩阵,则
$$
\begin{aligned}
A * A^{T} = \lambda E
\end{aligned}
$$
这里右边为单位矩阵乘一个常数

对角矩阵

定义

若$A$为对角矩阵,则矩阵仅仅在对角线上对值非零

性质

对角矩阵一定是对称矩阵,对角矩阵的特征值即为对角线上的元素

酉矩阵

定义

若$A$为酉矩阵,则
$$
\begin{aligned}
AA^H = A^HA = E
\end{aligned}
$$

正规矩阵

定义

若$A$为正规矩阵,则
$$
\begin{aligned}
AA^H = A^HA
\end{aligned}
$$
实对称矩阵、实反对称矩阵、厄米特矩阵、反厄米特矩阵、正交矩阵、对角矩阵、酉矩阵都是正规矩阵,但正规矩阵远不止这些

矩阵的相似

定义

若满足
$$
\begin{aligned}
A = B^{-1}CB
\end{aligned}
$$
则AC相似

性质

若两个矩阵相似,则他们的特征值相同

晚期(运行期)优化

解释器与编译器

Java在运行的时候,他的解释器和编译器会同时工作,解释器解释运行代码,编译器有选择性地编译部分代码为机器代码,以加速Java代码的执行过程

编译器

Java编译器有两种,一种是客户端编译器,他进行简单的可靠的优化,并适时加入性能检测的逻辑,另一种是服务器编译器,他进行耗时较长的优化,甚至根据性能检测信息,加入一些不可靠的激进的优化

编译器编译的对象

  • 被多次调用的方法
  • 被多次执行的循环体

方法

  • 基于采样的热点探测:虚拟机周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这就是一个热点方法,优点是简单高效,缺点是容易受到线程阻塞等其他因素的影响。
  • 基于计数器的热点探测:为每一个方法添加一个计数器,每当方法被调用,则计数器增大1,每经过一定时间(可以与gc相关)就让计数器变为原来的一半,这是一种和式增加,积式减少的策略,这个在计算机网络中的滑动窗口大小控制也有应用,当计数器超过某个阈值的时候,就让编译器来把这个方法编译成机器码即可。

循环体

和上文的计数器热点探测相似,但计数器永远不会变小。若超过一个阈值,整个方法都会被编译成机器码

编译优化

各语言通用优化

内联、冗余访问清除、复写传播、无用代码清除、公共子表达式消除

Java编译器优化

  • 隐式异常优化: 使用Segment Fault信号的异常处理器代替数组越界异常、空指针异常、除0异常等
  • 方法内联: 由于Java基本都是虚函数,这导致方法内联不太容易实现,对于非虚函数,直接内联,对于虚函数,CHA(类型继承关系分析)会检测,当前程序的这个方法是否对应了多个版本,若没有,则进行激进优化,强行内联并预留一个逃生门,以便在新类加载的时候,抛弃此代码,使用解释器,如果CHA查出有多个版本,也会为进行一个缓存处理,保留上一次的信息,若下一次进来的版本相同,则内联可以继续使用,否则就只能回到解释器了。

逃逸分析

逃逸分析是一种分析技术,而不是直接优化代码的手段。

栈上分配

如果分析出一个对象不会被他被定义的方法以外的方法用到,那个这个对象会被分配到栈上。

同步消除

如果分析出一个对象不会被他被定义的所在线程以外的线程用到,那么这个对象的同步指令会被清除。

标量替换

如果分析出一个对象不会被外部访问,他甚至会被拆成多个基本数据类型,分配到栈上,甚至被虚拟机直接分配到物理机的高速寄存器中

LDA

LDA

算法输入与目的

有很多有标签的高纬数据,他们的格式为列向量:

$$
x = [x_1,x_2,x_3…]^T
$$

我们想要找到一个高纬空间到一维空间的映射,其中$w$是一个列向量

$$
y=w^T x
$$

使得我们在这个新的维度上,能够完成分类,类内紧凑,类间松散

模型建立

假设映射后类别i的数据集合为:

$$
Y_i = {y_1,y_2,y_3…}
$$

分别根据求映射后数据集合的期望

$$
\tilde{m_i}=\frac{1}{n_i}\sum_{y\in Y_i}y
$$

和方差

$$
\tilde{S_i}^2 = \sum_{y \in Y_i}(y-\tilde{m_i})^2
$$

模型建立,把类中心点间的方差作为分子,把类内方差和作为分母,我们的目标就是最大化整个分式

$$
\tilde{m}=\frac{1}{n}\sum n_i*\tilde{m_i}
\\max\quad J(w)=\frac{ \sum n_i(\tilde{m_i}-\tilde{m})^2}{\sum{\tilde{s_i}^2}}
$$

变形

假设映射前类别i的数据集合为:

$$
D_i = {x_1,x_2,x_3…}
$$

为了能够同时描述类内距离和类间距离,我们需要对映射后的各类集合求期望和方差

$$
m_i=\frac{1}{n_i}\sum_{x\in D_i}x
$$

则映射后的期望为

$$
\tilde{m_i}=\frac{1}{n_i}\sum_{y\in Y_i}y=\frac{1}{n_i}\sum_{x\in D_i}w^Tx=w^T\frac{1}{n_i}\sum_{x\in D_i}x=w^Tm_i
$$

$$
S_i = \sum_{x \in D_i}(x-m_i)(x-m_i)^T
$$

则映射后的方差

$$
\begin{aligned}
\tilde{S_i}^2&= \sum_{y \in Y_i}(y-\tilde{m_i})^2
\&= \sum_{x \in D_i}(w^Tx-w^Tm_i)^2
\&=\sum_{x \in D_i}(w^T(x-m_i))(w^T(x-m_i))
\&=\sum_{x \in D_i}(w^T(x-m_i))
((x-m_i)^Tw)
\&=w^TS_iw
\end{aligned}
$$

模型

$$
\tilde{m}=\frac{1}{n}\sum n_i\tilde{m_i}=w^T\frac{1}{n}\sum n_im_i=w^Tm
$$

模型的分子

$$
\begin{aligned}
\&\sum n_i(\tilde{m_i}-\tilde{m})^2
\=&\sum n_i(w^Tm_i-w^Tm)^2
\=&w^T(\sum n_i(m_i-m)(m_i-m)^T) w
\end{aligned}
$$

模型总结

$$
\begin{aligned}
&S_B=\sum n_i(m_i-m)(m_i-m)^T
\&S_W=\sum S_i
\&\max\quad J(w)=\frac{ w^TS_Bw}{w^TS_ww}
\end{aligned}
$$

拉格朗日乘子法求极值

J(w)的值与w的长度无关,只和w的方向有关,我们不妨固定分子为1,则变为了

$$
\max\quad J_2(w)=w^TS_Bw\quad,\quad w^TS_ww=1
$$

构建拉格朗日函数,并使用标量对列向量求导法则,求偏导

$$
L(w,\lambda) = w^TS_Bw+\lambda(1-w^TS_ww)
$$

一个求导法则:($x$是列向量,$A$是方阵)

$$
\begin{aligned}
&\frac{\partial x^TAx}{\partial x} = (A^T+A)x
\end{aligned}
$$

开始求导

$$
\begin{aligned}
&\frac{\partial L}{\partial w}=(S_B+S_B^T)w-\lambda(S_w+S_w^T)w=0
\end{aligned}
$$

其中S_B和S_w是对称矩阵

$$
\begin{aligned}
&2S_Bw-2\lambda S_ww=0
\\to&S_Bw=\lambda S_ww
\\to&(S_w^{-1}S_B)w=\lambda w
\end{aligned}
$$

因此$w$取矩阵$S_w^{-1}S_B$的特征向量即可

早期(编译期)优化

Java 编译

  • 解析与填充符号表过程
  • 插入式注解处理器的注解处理过程
  • 分析与字节码生成过程

Java 语法糖

  • 自动拆箱装箱
  • 遍历循环
  • 条件编译 : 类似c++的宏
  • 范型与类型擦除 : Java的范性和c++范型不一样,Java是用类型擦除来实现的,然后使用类型强制转化。

拆箱陷阱

先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class test{
public static void main(String[] args){
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a + b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));
}
}

输出

1
2
3
4
5
6
true
false
true
true
true
false

解释

  • = 会导致自动拆箱和装箱
  • +,-,*,/混合运算会拆箱
  • >,<,==比较运算会拆箱
  • euqals会装箱
  • 向集合类添加数据会装箱
  • Integer类有缓存区域,储存了[-128,127],所有这些值都共享缓存区的对象指针