注意点:html
- 通俗的讲的时候,就是我的的理解了,仅做参考。
- 做为一个Java程序员,有必要了解算法,若是有成为一个优秀程序员的想法,算法和数据结构只是基础。固然对于非CS专业,计算机网络,操做系统,编译原理等也是后面须要补充的基础知识点。
- 关于阅读《算法导论》的一些建议:
没必要纠结于数学的证实,例如递归表达式的时间复杂度计算;把一些当前重要的知识点(好比从第一部分到动态规划、贪心算法,高级数据结构B树那里)先看了,完成从0到1的过程,本文也将记录到那里。- 好记性不如烂笔头,左思右想不如画个图...人的记忆力是有限的,关于计算机的知识点是不少的,把重要的记录下来,之后忘了,回过头再看看,因为你记录的是要点,因此没必要再次翻看一遍全书,便可很快的复习一遍。
- 看书没必要非得按照顺序来看,连JMM都知道,除非两个步骤都有依赖关系,不然能够乱序执行,称为重排序
- Good luck to everyone who wants to be a not-so-bad programmer.
一般状况下,当数据量足够大时,通常知足java
θ(1)>θ(N)>θ(NlogN)>θ(N^2) (>表明优于)程序员
时间复杂度:O(N^2)
思想:每次从右至左跟已排序数列进行对比,放入合适位置。两次遍历,一次至关于摸牌,另外一次至关于具体的查找算法。
算法
将问题分解为几个规模较小但相似于原问题的子问题,递归的求解这些子问题,而后再合并这些子问题的解来创建原问题的解。(分解-解决-合并)数据库
归并排序
时间复杂度O(NlogN)(O仍是θ——theta,都差很少)数组
归并排序算法以下:
缓存
O渐进上界Ω渐进下界,o和ω
具体看数学定义最清楚。
服务器
求解递归式的三种方法:网络
主方法
可求解形如
的函数。数据结构
思路简单,分红两个子序列,最大和要么全在左侧序列,要么全在右侧序列,要么跨中点;简单写法的参考简单版本,可是有个点须要注意一下,代码给出的复杂度是O(N)
若是输入数组中仅有常数个元素须要在排序过程当中存储在数组以外,则程排序算法是原址的(in place)。例如,排序算法中的swap操做。
顺序统计量:一个n个数的集合的第i个顺序统计量就是集合中第i小的数。
堆排序的时间复杂度是O(NlgN),具备空间原址性。
二叉堆是一个数组,它能够被当作是一个近似的彻底二叉树(按层序排列,各个节点的序号同满二叉树相同)
二叉堆能够分为两种形式:最大堆和最小堆(最大仍是最小取决于对应的二叉树的全部双亲节点是否都大于或小于其孩子节点的值)。堆排序算法中,使用的是最大堆。最小对一般用于构造优先队列。
维护堆的性质
通俗的讲,就是保持数组的最大堆性质。思路比较简单,比较parent节点和孩子节点,找到最大值,若是最大值是某个孩子节点,交换,递归运行。对于树高为h的节点来讲,该程序时间复杂度为O(h)
建堆
建堆过程,说白了就是对每个非叶节点进行(1)的操做。复杂度O(n)
堆排序算法
交换A[1]和A[n],此时最大的数已经位于最后一个位置。而后调整剩下的n-1个数据,调用(1)的方法;再交换A[1]和A[n-1],如此下去。复杂度O(lgn)
具体堆排序过程可参考图解堆排序(不过也注意,其中有一些问题,哪些节点有子节点,从1开始标,应该是(n/2)向下取整)
优先队列(priority queue)是一种用来维护由一组元素构成的集合S的数据结构,其中的每个元素都有一个相关的值,称为关键字(key)
最大优先队列应用:例如在共享计算机系统的做业调度。
最小优先队列应用:用于基于事件驱动的模拟器。
最坏状况复杂度θ(n^2),元素互异的状况下指望时间复杂度θ(NlgN),一般是实际排序应用中最好的选择。
因为是in place排序,因此不须要合并操做。
伪代码:
数组的划分
数组划分一点分析:默认以A[r]——最后一个元素做为比较中间点,for循环遍历A数组,j指向每一个被遍历元素下标;i指向小于A[r]的下标。
画个图理解一下最清楚了:
算导叫法,A[r]称为主元(pivot element)
最坏状况:划分产生子问题元素个数为n-1和0时。此时,T(n)=θ(n^2)。当输入数组彻底有序时,快排复杂度为θ(n^2),插排只有O(n)
最好状况:划分获得的两个子问题的规模都不大于n/2.复杂度θ(nlgn)
这个思路简单,随机选取主元,与A[r]交换,再利用上面的算法计算便可。此种版本主要解决,近乎有序的状况带来的原有快排最坏状况的发生。
在排序的最终结果中,各元素的次序依赖于它们之间的比较,这类排序算法称为比较排序。
三种线性时间复杂度的排序算法:计数排序、基数排序和桶排序。
比较算法采起决策树模型:这里的内部节点用i:j的形式表明A[i]和A[j]进行比较。≤则进左子树比较,反之右子树进行比较。叶子节点给出了最后的顺序。
支持插入删除操做的动态集合称为字典。
栈(stack),后进先出,LIFO
插入称为压入(push),删除称为弹出(pop)。能够简单的理解为有底无盖的杯子。
队列(queue)插入称为入队(enqueue),删除称为出队(dequeue)
链表:
单链表、双向链表、循环链表,建议看下《大话数据结构》了解一下就能够了。
算导以双向链表为例讲解增删查,虽然仍是能够经过画图,注意维护涉及节点的prev和next指针指向以及边界条件,可以明白其操做过程,但仍是记录下,以备复习之用。
哨兵(sentinel)
哨兵是一个哑对象,其做用是简化边界条件的处理。
相似于之前学习时讲到的头节点。加入哨兵将原来的双向链表转变成一个有哨兵的双向循环两表。L.nil表明哨兵。一图胜千言,以下:
若是有不少个很短的链表,慎用哨兵,由于哨兵所占用的额外存储空间会形成严重的存储浪费。哨兵并无优化太多的渐进时间界,只是可使代码更紧凑简洁。
对象的多数组表示:
next和prev中的数字指的是对应数字的下标,有趣!
单数组表示:
对象的分配和释放
略
有根树的表示
分支无限制的有根树能够用左孩子右兄弟表示法。
散列表是普通数组概念的推广。
直接寻址缺点:若是U全域很大,存储大小为U.size()的一张表T不太实际,并且对于T来说若是存储的关键字集合K相对于U来讲很凶,则T的大部分空间将会浪费掉。
在散列方式(hash)下,关键字k被放到槽(slot)h(k)中,及利用散列函数(hash function)h,根据k计算出槽的位置。这里,函数h将关键字的全域U映射到散列表T[0...m-1]的槽位上(|U|>m,正因如此,彻底避免冲突是不可能的)
若是h(k1) = h(k2),则称冲突(collision)
连接法(chaining)
关于连接法采用双链的一些解释:来自知乎
简单讲,删除就是从x对应的链表里删除x,双链表的删除加入哨兵只需两行,可是单链表的话只能指定x.next=null,可是在这以前须要先将x.prev.next指向x.next,因为是单链,因此没有prev,只能一直next找下去,相比双链多了查找的时间耗费。
给定一个能存放n个元素的、具备m个槽位的散列表T,定义T的装载因子(load factor)α为n/m,即一个链的平均存储元素数。
除法散列法
经过取k mod m得余数,将关键字k映射到m个slot上的某一个,h(k) = k mod m
一个不太接近2的整数幂的素数,经常是m的一个较好的选择。
为了使用开放寻址法插入一个元素,须要连续的检查散列表,或称为探查(probe),直到找到一个空槽来放置待插入的关键字为止。
三种技术计算开放寻址法中的探查序列:线性探查、二次探查和双重探查。
这个hash函数应该就是hash(k)= k mod m(m表明散列表的长度)
线性探查,通俗的讲,过程是这样的:利用一个hash函数,计算关键字key位于的槽位下标hash(key),若是T[hash(key)]已经有值,则探查T[hash(key)+1],T[hash(key) +2]直到最后。相似一人找厕所的过程,到一个厕所(hash(key)位置)跟前,看能不能打开门,打不开就挨着找下一个,直到找到对应的位置。(因为就只有m个元素须要hash,则每一个都是能找到对应位置的。)
可是,线性探查存在一个问题,称为一次群集(primary clustering)。例如100个槽位,假设后面95位已经被连续占用,下一次hash出来,若是不幸恰好计算出位置在第6位,则须要连续95次才能找到对应的存储槽位,剩下的4次一样也很耗费时间。
二次探查(quadratic probing)
相比于线性探查,在于偏移量采用的是ax^2+bx这种形式
双重散列(double hashing)
是用于开放寻址法的最好方法之一,由于它所产生的排列具备随机选择排列的许多特性。
彻底散列
采用两级散列。当关键字集合是静态(即关键字存到表中关键字集合就再也不变化了)的,采用彻底散列,一级散列于带连接的散列表基本一致,二级散列的长度是存储的关键字个数的平方(主要是为了确保第二级上不出现冲突)。
关于树:
彻底k叉树:全部叶节点深度相同,且全部内部节点度为k的k叉树(全部节点有k个叉)
注意:《算导》的彻底二叉树和满二叉树跟《大话数据结构》里的两者定义彻底不一样,具体以哪一个为准,暂不纠结,哪位朋友知道的,能够告知一下
二叉搜索树的性质:x是一个节点,则其左(右)子树任意节点.key 分别≤(≥)x.key
中序遍历(inorder tree walk)子树根的关键字位于左右子树的关键字之间。
前序遍历(preorder tree walk)子树根的关键字位于左右子树的关键字以前。
后序遍历(postorder tree walk)子树根的关键字位于左右子树的关键字以后。
这里区分一下二叉搜索树和最大堆,相同点:比较都是针对全部节点而言,不一样点:二叉搜索树,节点左子树值均小于该节点的值,右子树值均小于该节点的值;最大堆:节点值大于全部孩子的值。
O(h),h是这棵树的高度,下面五种操做都是这个复杂度
两种写法:递归和while
最大值和最小值:分别一直查左子树(右子树)便可
后继(successor)和前驱(predecessor):
后继:两种状况,若是x的右子树不为空,则右子树中的最小值就是x的后继; 反之,一直找x的双亲节点,直到x是y的左子树为止。
插入和删除会引发由二叉搜索树表示的动态集合的变化,必定要修改数据结构来反映这个变化,但修改要保持二叉搜索树性质的成立。
插入:
边界条件,二叉搜索树没有元素;不然找到新插入节点z插入的位置的双亲节点。
删除:
三种基本策略:
若是z有两个孩子,那么找z的后继y(必定在z的右子树中),并让y占据树中z的位置。z的原来右子树部分红为y的新的右子树,而且z的左子树成为y的新的左子树,这种状况稍显麻烦,由于还与y是否为z的右孩子相关。
略
BST(binary search tree)的基本操做大都能在O(h)时间内完成。
对每一个节点,从该节点到其全部后代叶节点的简单路径上,均包含相同数目的黑色节点。
使用一个哨兵(sentinel)T.NIL表明全部的NIL
从某个节点x出发(不含该节点)到达一个叶节点的任意一条简单路径上的黑色节点个数称为该节点的黑高(black-height),记做bh(x)
进行增删的时候可能会破坏上面提到的5条性质,所以为了维护这些性质,必须改变某些节点的颜色及指针结构。
指针结构的修改是经过旋转(rotation)来完成的。
这里的左旋和右旋彷佛跟《大话数据结构》里AVL树的左旋右旋有类似之处。
一图胜千言:
要理解各个指针的改变,下面这个图好好看下:
插入耗费时间O(lgN),且该程序选择不超过两次
插入一个节点z,并将其着色为红色。
插入后的修补工做:
while的结束条件是当z的双亲节点颜色是黑色时
fixup例子:(阴影部分为黑色)
插入操做只可能破坏红黑树性质2和性质4,而且只能破坏其中一条。
修补的三种状况分析:
case 1:z的叔节点y是红色的
此时,不须要旋转,只须要改变颜色,z指向z.p.p便可
z.p.color = y.color = black;
z.p.p.color = red;
case2:z的叔节点y是黑色的且z是一个右孩子
case3:z的叔节点y是黑色的且z是一个左孩子
状况2经过一次左旋转成case3。
z.p.color = black;
z.p.p.color=red;
再一次右旋
删除节点耗费O(lgN)时间。
须要提供一个让某节点孩子来接替老子位置的一个方法transplant
删除方法:
1-8行是子承父位,9行是找出z的后继,10-20行维护了相关的一些指针指向(将y的右孩子移到y的位置,y移到z的位置),21-22行若是z孩子小于2个,z的颜色是黑色(这种状况很简单,结合红黑树性质5分析)或者z孩子有两个,z的后继是黑色,则进行修正(画图理解最清楚了,比较简单就不画了)。为何修正呢,由于当是黑色的时候,会破坏红黑树性质5,影响黑高。
来看下删除修复过程:
删除修复例子:
删除的四种case:
case1:x的兄弟节点w是红色的
过程:w描黑,x.p描红,x.p左旋,维持w兄弟指针指向
case2:x的兄弟节点w是黑色的,w的两个子节点都是黑色的
w描红,x指向x.p
case3:x的兄弟节点w是黑色的,w的孩子左红右黑
w左孩子描黑,w描红,w右旋,w指向x.p.right,仍旧是维持兄弟指针指向
case4:x的兄弟节点w是黑色的,w的右孩子是红色的
修改w颜色同x.p颜色一致,x.p描黑,w右孩子描黑,结束循环。
关于删除的一些深刻理解,参考图解红黑树(别看评论,笑点低的会以为搞笑的^_^)
了解了散列(hash)和红黑树,就能够去愉快的看下Java里面HashMap的源码啦。
节点左右子树高度相差至多为1.
未深刻讲解。
为磁盘存储而专门设计的一类平衡搜索树。B树相似于红黑树,但它们在下降磁盘I/O操做数方面要更好一些,许多数据库系统使用B树或者B树变种来存储信息。好比MySQL数据库使用了B+树的数据结构。
B+相比B树来讲,主要有几个区别,B+树叶子节点存储了全部数据,能够只通过一次遍历;叶子节点构成了一个单向链表。至于B树的插入删除等,参考2-3树更容易理解一些。
关于B树和B+树的区别等能够参加B树和B+树的区别
B-tree,B+tree,想起之前还觉得是一个B+,一个B-呢,哈哈
计算机的主存(primary memory或main memory)一般由硅存储芯片组成。相比辅存好比磁盘磁带价格高,容量小,而辅存容量大价格低然而速度也要慢一些。(这方面的知识哪天还得看下计算机组成原理,虽然《计算机科学导论》也讲过一些,但总以为还差点东西)
磁盘慢,主要是由于有机械运动的部分:盘片旋转和磁臂移动。
本书第三版,2009年出版,这时磁盘旋转速度是5400~15000转/分钟(RPM),一般15000RPM的速度是用于服务器级的驱动器上,7200RPM的速度用于台式机的驱动器上,5400RPM的速度用于笔记本的驱动器上。随便在jd上看了机械硬盘和固态硬盘,机械硬盘缓存64MB左右,一款三星SSD缓存在512MB,读写在百兆/s。
7200RPM旋转一圈须要8.33ms,比硅存储的常见存取时间50ns要高出5个数量级(10的5次方)。也就是说,这个时间内,可能存取主存超过100000次。
为了瘫痪机械移动所花费的等待时间,磁盘会一次存取多个数据项而不是一个。信息被分为一系列相等大小的在柱面内连续出现的位页面(page),而且每一个磁盘读或写一个或多个完整的页面。对于一个典型的磁盘来讲,一晚上的长度可能为2^11~2^14字节。
这里,对运行时间的两个主要组成成分分别加以考虑:磁盘存取次数和CPU(计算)时间。
全部叶节点深度相同,即树高h;每一个节点所包含的关键字个数有上界和下界,用一个被称为B树的最小度数的固定证书t≥2来表示这些界:每一个节点除根节点外必须至少有t-1个关键字,至多能够包含2t-1个关键字。
B+tree将卫星数据存储到叶节点上,内部结点只存放关键字和孩子指针。对存储在磁盘上的一颗大的B树,一般看到分支因子在50~2000之间。
约定:1. B树的根节点始终在主存中,这样无需对根作DISK-READ操做;然而,当根节点被改变后,须要对根节点作一次DISK-WRITE操做。2. 任何被当作参数的节点在被传递以前,都要对他们先作一次DISK-READ操做。
搜索B树
先从x节点内部关键字查找,找不到,而且x有孩子的话或不是叶节点,再从x.ci孩子节点查找。
分裂B树中的节点
分裂是树长高的惟一途径。
以沿树单程下行方式向B树插入关键字
2-8行处理x是叶节点的状况,9-12行找到合适的位置,若是ci子节点已满,则进行split操做,15-16行肯定应该具体插入那个ci节点,17行递归插入。
这里提到一点,insert-nonfull是尾递归的,因此它能够用一个while循环来实现(这里也是一个重要的知识点,改天再找资料尝试一下)
图解插入:
根节点容许有比最少关键字数t-1还少的关键字个数。
当要删除关键字的路径上节点(非根)有最少的关键字个数时,也可能须要向上回溯。
删除实例:
删除比较复杂一点,case1,case2a,2b,2c,case3a,3b.
递归调用自身时,必须保证该节点至少有t个关键字
----
利用计算出的信息构造一个最优解
最优子结构(optimal substructure)性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题能够独立求解,主要缘由是:反复求解相同的子问题,同斐波那契数列基本递归同样。
动态规划方法仔细安排求解顺序,对每一个子问题只求解一次,并将结果保存下来。此乃时空权衡(time-memory-trade-off)。
自顶向下递归实现:时间复杂度(2^n)
动态规划有两种等价的实现方法:
public int getFibonacciWithTailRecursive(int num, int pp, int prev) { if (num == 0) { return pp; } return getFibonacciWithTailRecursive(num - 1, prev, pp + prev); }
public int getFibonacciNum(int num) { int[] tmp = new int[num + 1]; for (int i = 0; i <= num; i++) { switch (i) { case 0: tmp[i] = 0; break; case 1: tmp[i] = 1; break; default: tmp[i] = tmp[i - 1] + tmp[i - 2]; } } return tmp[num]; }
此种写法有改进空间,因为计算第n个数只须要前面n-1和n-2的值便可,因此能够改进为只用两个变量存储,相似于这位朋友所写点击查看
试了下a,b这种形式,发现速度更慢了,还不如原始上面这种版本。
关于斐波那契数列的这些写法,用额外的数组存储已经计算过的斐波那契数,利用额外的存储空间换来的是时间上的飞跃,所谓空间换时间。
public int getFibonacciNum2(int num) { int pp = 0, prev= 1; int tm = 0; for (int i = 2; i < num; i++) { tm = prev + pp; pp = prev; prev = tm; } return pp + prev; }
另外写了个测试类,比较了一下尾递归、自底向上和普通递归
@Test public void test() { int num = 300; long start2 = System.nanoTime(); int fibonacciNum2 = getFibonacciNum2(num); System.err.println(fibonacciNum2); System.err.println("自底向上花费:" + (System.nanoTime() - start2)); System.err.println("-----------------"); long start3 = System.nanoTime(); int fibonacciNum3 = getFibonacciWithTailRecursive(num, 0, 1); System.err.println(fibonacciNum3); System.err.println("尾递归花费:" + (System.nanoTime() - start3)); System.err.println("-----------------"); long start = System.nanoTime(); int fibonacciNum = getFibonacciNum(num); System.err.println(fibonacciNum); System.err.println("普通递归:" + (System.nanoTime() - start)); System.err.println("-----------------"); } /** * 自底向上 * @param num * @return */ public int getFibonacciNum2(int num) { int[] tmp = new int[num + 1]; for (int i = 0; i <= num; i++) { switch (i) { case 0: tmp[i] = 0; break; case 1: tmp[i] = 1; break; default: tmp[i] = tmp[i - 1] + tmp[i - 2]; } } return tmp[num]; } /** * 普通递归 * @param num * @return */ public int getFibonacciNum(int num) { if (num == 0) return 0; if (num == 1) return 1; return getFibonacciNum(num - 1) + getFibonacciNum(num - 2); } public int getFibonacciWithTailRecursive(int num, int pp, int prev) { if (num == 0) { return pp; } return getFibonacciWithTailRecursive(num - 1, prev, pp + prev); }
言归正传,继续学习。
简单讲,Z={B,C,D,B}是X={A,B,C,B,D,A,B}的子序列,对应的下标序列是{2,3,5,7}
最长公共子序列问题(longest-common-subsequence problem)
求解过程:
首先,解释一下“前缀”,给定一个序列X=(x1,x2,...,xm)对i=0,1,...m,定义X的第i前缀为Xi= (x1,x2,...,xi)X0为空串。
假定每一步都选取最优解,达到最终最优解。
《算法导论》阅读学习,至此完结。还剩下几个计数排序,再找时间学习了。