数据结构和算法
1. 数据结构
1.1 数组(Array)
优点 | 缺点 |
---|---|
按照索引查询元素的速度快 | 数组的大小在创建后就确定了,无法扩容 |
按照索引遍历数组快 | 添加、删除元素的操作很耗时间,因为要移动其他元素 |
package com.qupeng.datastructure;
import java.util.Arrays;
import java.util.List;
public class TestArray {
public static void main(String[] args) {
TestObject[] array = {new TestObject(3.4), new TestObject(2.9), new TestObject(1.9), new TestObject(3.5)};
System.out.println("Original array: " + Arrays.toString(array));
Arrays.sort(array);
System.out.println("Sorted array: " + Arrays.toString(array));
System.out.println("Index of 1.9: " + Arrays.binarySearch(array, 1.9));
Arrays.fill(array, 1, 2, new TestObject(9.9));
System.out.println("Arrays.fill: " + Arrays.toString(array));
List<TestObject> list = Arrays.asList(array);
System.out.println(list.toString());
System.out.println("Arrays.hashCode: " + Arrays.hashCode(array));
System.out.println("Arrays.deepHashCode: " + Arrays.deepHashCode(array));
System.out.println("array.toString: " + array.toString());
System.out.println("Arrays.toString: " + Arrays.toString(array));
System.out.println("AArrays.deepToString: " + Arrays.deepToString(array));
TestObject[] newArray = Arrays.copyOf(array, array.length);
System.out.println("newArray by Arrays.copyOf: " + Arrays.toString(newArray));
System.out.println("If equals with newArray by Arrays.equals: " + Arrays.equals(array, newArray));
System.out.println("If equals with newArray by Arrays.deepEquals: " + Arrays.deepEquals(array, newArray));
TestObject[] newArray1 = Arrays.copyOfRange(array, 1, 2);
System.out.println("Copy range of [1, 2): " + Arrays.toString(newArray1));
}
static class TestObject implements Comparable {
Double value;
TestObject(double value) {
this.value = value;
}
@Override
public int compareTo(Object o) {
return this.value.compareTo((Double)value);
}
@Override
public String toString() {
return value.toString();
}
@Override
protected Object clone() throws CloneNotSupportedException {
return new Double(value);
}
}
}
输出:
Original array: [3.4, 2.9, 1.9, 3.5]
Sorted array: [3.4, 2.9, 1.9, 3.5]
Index of 1.9: 1
Arrays.fill: [3.4, 9.9, 1.9, 3.5]
[3.4, 9.9, 1.9, 3.5]
Arrays.hashCode: -1253382898
Arrays.deepHashCode: -1253382898
array.toString: [Lcom.qupeng.datastructure.TestArray$TestObject;@1abf7cb
Arrays.toString: [3.4, 9.9, 1.9, 3.5]
AArrays.deepToString: [3.4, 9.9, 1.9, 3.5]
newArray by Arrays.copyOf: [3.4, 9.9, 1.9, 3.5]
If equals with newArray by Arrays.equals: true
If equals with newArray by Arrays.deepEquals: true
Copy range of [1, 2): [9.9]
1.2 链表
链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该节点还有一个元素和一个指向另一条链表的引用。单向链表,双向链表。
优点 | 缺点 |
---|---|
不需要初始化容量 | 可以添加任意数量元素 |
含有大量的引用,占用的内存空间大 | 查找元素需要遍历整个链表,耗时多 |
1.3 栈
栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。 栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈。
1.4 队列
队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素,也就是:先进先出。从一端放入元素的操作称为入队,取出元素为出队,示例图如下:
1.5 散列表
哈希表(Hash Table),也叫散列表,是一种可以通过关键码值(key-value)直接访问的数据结构,它最大的特点就是可以快速实现查找、插入和删除。
数组的最大特点就是查找容易,插入和删除困难;而链表正好相反,查找困难,而插入和删除容易。哈希表很完美地结合了两者的优点, Java 的 HashMap 在此基础上还加入了树的优点。
- 散列算法
哈希函数在哈希表中起着⾮常关键的作⽤,它可以把任意长度的输入变换成固定长度的输出,该输出就是哈希值。哈希函数使得一个数据序列的访问过程变得更加迅速有效,通过哈希函数,数据元素能够被很快的进行定位。
若关键字为 k,则其值存放在 hash(k) 的存储位置上。由此,不需要遍历就可以直接取得 k 对应的值。
对于任意两个不同的数据块,其哈希值相同的可能性极小,也就是说,对于一个给定的数据块,找到和它哈希值相同的数据块极为困难。再者,对于一个数据块,哪怕只改动它的一个比特位,其哈希值的改动也会非常的大——这正是 Hash 存在的价值。
算法名称 | 描述 | 安全性 | 性能 |
---|---|---|---|
MD5(Message Digest Algorithm) | 对于长度小于2^64位的消息,会产生一个160位的消息摘要 | 中 | 快 |
SHA-1(Secure Hash Algorithm 1) | 由美国国家安全局 (NSA)研发,是一种安全散列算法,对于长度小于2^64位的消息,SHA-1会产生一个160位的消息摘要 | 高 | 慢 |
SHA-256(Secure Hash Algorithm 2) | 由美国国家安全局 (NSA)研发,是SHA-2的一个分支,是一种安全散列算法,对于任意长度的消息,SHA-256都会产生一个256位的消息摘要 | 更高 | 更慢 |
- 哈希碰撞
HashMap的快速高效,使其使用非常广泛。其原理是,调用hashCode()和equals()方法,并对hashcode进行一定的哈希运算得到相应value的位置信息,将其分到不同的桶里。桶的数量一般会比所承载的实际键值对多。当通过key进行查找的时候,往往能够在常数时间内找到该value。
但是,当某种针对key的hashcode的哈希运算得到的位置信息重复了之后,就发生了哈希碰撞。这会对HashMap的性能产生灾难性的影响。
在Java 8 之前, 如果发生碰撞往往是将该value直接链接到该位置的其他所有value的末尾,即相互碰撞的所有value形成一个链表。因此,在最坏情况下,HashMap的查找时间复杂度将退化到O(n)。
但是在Java 8 中,该碰撞后的处理进行了改进。当一个位置所在的冲突过多时,Java 的 HashMap 会在数组的同一个位置上增加链表,如果链表的长度大于 8,将会转化成红黑树进行处理,排序依据为key的hashcode。所以,在最坏情况下,HashMap的查找时间复杂度将从O(1)退化到O(logn)。
虽然是一个小小的改进,但意义重大:
1、O(n)到O(logn)的时间开销。
2、如果恶意程序知道我们用的是Hash算法,则在纯链表情况下,它能够发送大量请求导致哈希碰撞,然后不停访问这些key导致HashMap忙于进行线性查找,最终陷入瘫痪,即形成了拒绝服务攻击(DoS)。 - 填装因子
哈希表装填因子定义为:α= 填入表中的元素个数 / 哈希表的长度α是哈希表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。
为什么负载因子是0.75f
同样是个经验值,如果为1,则在扩容之前存在大量的hash碰撞。如果太小比如0.5,则桶可能是空的。同时为了保证HashMap的扩容机制中capacity(容量)一直是2的幂,所以提出了0.75f,这样 threshold = loadFactor * capacity 得到的结果就是一个整数。 - 容量
在创建HashMap时,我们可以指定其容量,默认为16(16可以认为是个经验值,太大了存在空间浪费,太小了又会发生频繁扩容)。
当我们使用HashMap(int initialCapacity)来初始化容量的时候,HashMap并不会使用我们传进来的initialCapacity直接作为初始容量。
JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。
也就是说,当我们new HashMap(7)创建HashMap的时候,JDK会通过计算,帮我们创建一个容量为8的Map;当我们new HashMap(9)创建HashMap的时候,JDK会通过计算,帮我们创建一个容量为16的Map。
但是,这个值看似合理,实际上并不尽然。因为HashMap在根据用户传入的capacity计算得到的默认容量,并没有考虑到loadFactor这个因素,只是简单机械的计算出第一个大约这个数字的2的幂。
也就是说,如果我们设置的默认值是7,经过JDK处理之后,HashMap的容量会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的。
那么,到底设置成什么值比较合理呢?参考JDK8中putAll方法中的实现::expectedSize / 0.75F + 1.0F
。 - 扩容
当HashMap中元素个数达到临界值后会触发自动扩容:threshold = loadFactor * capacity
。扩容容量为原来的两倍。
loadFactor 是装载因子,表示HashMap满的程度,默认为0.75f
capacity 是容量,一般是2的幂,默认为16 - rehash
当执行扩容操作时,会进行rehash操作,rehash的目的是降低因为元素增多带来的hash碰撞问题。如果HashMap中冲突太高,那么数组的链表会退化为链表,查询速度大大降低。
因为rehash过程相当于对于所有元素进行了一遍重新的hash分配,分配到目标桶中。
1.6 树
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
它具有以下的特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树;
相关概念:
度:结点拥有的子树数称为结点的度(Degree)
叶子节点:度为零的节点
1.6.1 二叉树
每个节点最多含有两个子树的树称为二叉树。
二叉树是树的特殊一种,具有如下特点:
- 每个结点最多有两颗子树,结点的度最大为2。
- 左子树和右子树是有顺序的,次序不能颠倒。
- 即使某结点只有一个子树,也要区分左右子树。
1.6.1.1 满二叉树
除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。
1.6.1.2 完全二叉树
若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。
1.6.1.3 二叉查找树
二叉查找树是二叉树的衍生概念:
二叉查找树(英语:Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树;
- 没有键值相等的节点。
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低为 O ( log n ) 。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。
1.6.1.4 平衡二叉树
平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;其中AVL树是最先发明的自平衡二叉查找树,是最原始典型的平衡二叉树。
平衡二叉树是基于二叉查找树的改进。由于在某些极端的情况下(如在插入的序列是有序的时),二叉查找树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。所以我们通过自平衡操作(即旋转)构建两个子树高度差不超过1的平衡二叉树。
1.6.1.5 红黑树
红黑树也是一种自平衡的二叉查找树。
- 每个结点要么是红的要么是黑的。(红或黑)
- 根结点是黑的。 (根黑)
- 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。 (叶黑)
- 如果一个结点是红的,那么它的两个儿子都是黑的。 (红子黑)
- 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。(路径下黑相同)
如图就是一棵典型的红黑树。保证红黑树满足它的基本性质,就是在调整数据结构自平衡。
而红黑树自平衡的调整操作方式就有旋转和变色两种。
红黑树是一种应用很广的数据结构,如在Java集合类中TreeSet和TreeMap的底层,C++STL中set与map,以及linux中虚拟内存的管理。
1.6.2 多路树
如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)。
二叉树的操作效率较高,但是也存在问题:
- 由于构建二叉树时需要把数据从某个地方(数据库、文件等)加载到内存,如果节点少,那没什么问题,但是如果节点海量(比如上亿),在构建二叉树时,每个节点都会进行一次io操作,那么构建二叉树的速度就会很慢
- 如果二叉树的节点海量,那么二叉树的高度也特别高,当进行增加、删除、左旋、右旋节点等操作时,速度也会变慢
由于多叉树每个节点右更多的数据项,那么相较于二叉树节点数量会变少;多叉树每个节点可以由多个子节点,那么相较于二叉树高度也会降低,通过多数据项、多子节点这种方式可以让上面提到的两个问题得到改善,提高检索和操作的效率。
1.6.2.1 23树
23树是一种多路树,也是一种平衡搜索树,是最简单的B树结构。
23树特点:
- 2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点,二节点有且仅有一个数据项
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点,三节点有且仅有两个数据项
- 2-3树是由二节点和三节点构成的树
- 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
234树:
1.6.2.2 B树
B-tree树即B树,B即Balanced,平衡的意思,B树也是一棵平衡搜索树。前面介绍了23树、234树,他们都是B树。B树通过重新组织节点,降低树的高度,提高树的构建效率,减少IO操作。我们在学习数据库时。也经常听到某种类型的索引是基于B树或者B+树的,如下图就是一颗B树:
- B树的阶:节点的最多子节点个数;比如23树的阶是3,234树的阶是4
- B树的搜索:B树搜索时,从根节点开始,对接点内的关键字(有序的)进行二分查询,如果命中则结束,否则进入查询关键字所属的范围的子节点进行查找;重复指向上一步,直到查找到叶子节点或子节点指针为空
- 关键字:关键字也就可以想象成每个节点的数据或者主键什么的,B树的关键字分布在整棵树中,也就是说叶子结点和非叶子节点都会存放数据
- 搜素性能等价于在全集内做一次二分查找
- 所有的叶子节点都在同一层
1.6.2.3 B+树
B+树是B树的变体,也是一种多路搜索树,与B树有如下区别:
- B+树的关键字都出现在叶子结点,叶子节点的关键字是一颗按关键字顺序组织的链表(关键字有序),而B树的关键字分布在整棵树,叶子结点和非叶子节点都有关键字
- 当搜索时,B+树只可能在叶子节点命中被查询的关键,不能在非叶子节点命中,而B树可以在非叶子节点命中关键字,性能也是等价于在关键字的全集内做一次二分查找
- B+树非叶子节点相当于是叶子节点的索引,称为稀疏索引,叶子节点相当于是存储关键字(数据)的数据层,叶子节点也叫稠密索引
- B+树更适合文件系统
- B+树和B树各有应用场景,没有优劣之分
1.6.2.3 B*树
B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针
- B*树规定了非叶子结点关键字个数至少为(2/3) * M(M为树的度),即块的最低使用率为2/3,而B+树的块的最低使用率为B*树的1/2
- 从上面的特点可以看出B*树分配新节点的概率要比B+树低,空间使用率更高
1.7 堆
堆是具有以下性质的完全二叉树:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆
;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
1.8 图
线性表和树两类数据结构,线性表中的元素是“一对一”的关系,树中的元素是“一对多”的关系,本章所述的图结构中的元素则是“多对多”的关系。
图(Graph)是一种复杂的非线性结构,在图结构中,每个元素都可以有零个或多个前驱,也可以有零个或多个后继,也就是说,元素之间的关系是任意的。
1.5.1 图的构成:
1.顶点(vertex):图中的数据元素
2.边(edge):图中连接这些顶点的线
所有的顶点构成一个顶点集合,所有的边构成边的集合,一个完整的图结构就是由顶点集合和边集合组成。图结构在数学上记为以下形式:
G=(V,E) 或者 G=(V(G),E(G))
其中 V(G)表示图结构所有顶点的集合,顶点可以用不同的数字或者字母来表示。E(G)是图结构中所有边的集合,每条边由所连接的两个顶点来表示。
图结构中顶点集合V(G)不能为空,必须包含一个顶点,而图结构边集合可以为空,表示没有边。
1.8.2 图的分类
1.8.2.1无向图(undirected graph)
如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。典型的无向图,如图所示。由于无向图中的边没有方向性,这样我们在表示边的时候对两个顶点的顺序没有要求。例如顶点VI和顶点V5之间的边,可以表示为(V2, V6),也可以表示为(V6,V2)。
V(G)= {V1,V2,V3,V4,V5,V6}
E(G)= {(V1,V2),(V1,V3),(V2,V6),(V2,V5),(V2,V4),(V4,V3),(V3,V5),(V5,V6)}
1.8.2.2 有向图(directed graph)
一个图结构中,边是有方向性的,那么这种图就称为有向图,如图所示。由于图的边有方向性,我们在表示边的时候对两个顶点的顺序就有要求。我们采用尖括号表示有向边,例如<V2,V6>表示从顶点V2到顶点V6,而<V6,V2>表示顶点V6到顶点V2。
对于图三有向图,对应的顶点集合和边集合如下:
V(G)= {V1,V2,V3,V4,V5,V6}
E(G)= {<V2,V1>,<V3,V1>,<V4,V3>,<V4,V2>,<V3,V5>,<V5,V3>,<V2,V5>,<V6,V5>,<V2,V6>,<V6,V2>}
注意:
无向图也可以理解成一个特殊的有向图,就是边互相指向对方节点,A指向B,B又指向A。
度
连接顶点的边的数量称为该顶点的度。顶点的度在有向图和无向图中具有不同的表示。对于无向图,一个顶点V的度比较简单,其是连接该顶点的边的数量,记为D(V)。 例如,图二所示的无向图中,顶点V5的度为3。而V6的度为2。对于有向图要稍复杂些,根据连接顶点V的边的方向性,一个顶点的度有入度和出度之分。
- 入度是以该顶点为端点的入边数量, 记为ID(V)。
- 出度是以该顶点为端点的出边数量, 记为OD(V)。
这样,有向图中,一个顶点V的总度便是入度和出度之和,即D(V) = ID(V) + OD(V)。例如,图三所示的有向图中,顶点V5的入度为3,出度为1,因此,顶点V5的总度为4。
邻接顶点
邻接顶点是指图结构中一条边的两个顶点。 邻接顶点在有向图和无向图中具有不同的表示。对于无向图,邻接顶点比较简单。例如,在图二所示的无向图中,顶点V2和顶点V6互为邻接顶点,顶点V2和顶点V5互为邻接顶点等。
对于有向图要稍复杂些,根据连接顶点V的边的方向性,两个顶点分别称为起始顶点(起点或始点)和结束顶点(终点)。有向图的邻接顶点分为两类:
- 入边邻接顶点:连接该顶点的边中的起始顶点。例如,对于组成<V2,V6>这条边的两个顶点,V2是V6的入边邻接顶点。
- 出边邻接顶点:连接该顶点的边中的结束顶点。例如,对于组成<V2,V6>这条边的两个顶点,V6是V2的出边邻接顶点。
1.8.2.3 混合图(mixed graph)
一个图结构中,边同时有的是有方向性有的是无方向型的图。
在生活中混合图这种情况比较常见,比如城市道路中有些道路是单向通行,有的是双向通行。
1.8.2.4 无向完全图
如果在一个无向图中, 每两个顶点之间都存在条边,那么这种图结构称为无向完全图。典型的无向完全图,如图四所示。
理论上可以证明,对于一个包含M个顶点的无向完全图,其总边数为M(M-1)/2。比如图四总边数就是5(5-1)/ 2 = 10。
1.8.2.5 有向完全图
如果在一个有向图中,每两个顶点之间都存在方向相反的两条边,那么这种图结构称为有向完全图。典型的有向完全图,如图五所示。
理论上可以证明,对于一个包含N的顶点的有向完全图,其总的边数为N(N-1)。这是无向完全图的两倍,这个也很好理解,因为每两个顶点之间需要两条边。
1.8.2.6 有向无环图(DAG图)
如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图。有向无环图可以利用在区块链技术中。
1.8.2.7 无权图和有权图
这里的权可以理解成一个数值,就是说节点与节点之间这个边是否有一个数值与它对应,对于无权图来说这个边不需要具体的值。对于有权图节点与节点之间的关系可能需要某个值来表示,比如这个数值能代表两个顶点间的距离,或者从一个顶点到另一个顶点的时间,所以这时候这个边的值就是代表着两个节点之间的关系,这种图被称为有权图;
1.5.2.8 连通图
图的每个节点不一定每个节点都会被边连接起来,所以这就涉及到图的连通性,如下图:
可以发现上面这个图不是完全连通的。
1.8.2.9 简单图 ( Simple Graph)
对于节点与节点之间存在两种边,这两种边相对比较特殊
自环边(self-loop):节点自身的边,自己指向自己。
平行边(parallel-edges):两个节点之间存在多个边相连接。
这两种边都是有意义的,比如从A城市到B城市可能不仅仅有一条路,比如有三条路,这样平行边就可以用到这种情况。不过这两种边在算法设计上会加大实现的难度。而简单图就是不考虑这两种边。
最短路径问题:查找图中两个节点之间可达的路径中,边数最少的。
广度优先搜索:优先查找每个节点所有邻居节点,可解决最短路径问题。
深度优先搜索:优先查找每一条路径上的所有结点。
最快路径问题:查找图中两个节点之间边的权重之和最小的路径:狄克斯特拉算法。
图的代码实现:
package com.qupeng.datastructure.graph;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class Graph<T, W> {
private List<GraphNode<T, W>> nodes = new LinkedList<GraphNode<T, W>>();
public Graph (GraphNode<T, W> node) {
this.nodes.add(node);
}
public Graph (List<GraphNode<T, W>> nodes) {
this.nodes.addAll(nodes);
}
public List<GraphNode<T, W>> getRootNodes() {
return Collections.unmodifiableList(this.nodes);
}
}
package com.qupeng.datastructure.graph;
import java.util.*;
public class GraphNode<T, W> {
private T data;
private boolean isVisited = false;
private List<GraphNode<T, W>> neighbors = new LinkedList<>();
private Map<GraphNode<T, W>, W> weights = new HashMap<>();
public GraphNode(T data) {
this.data = data;
}
public void addNeighbor(GraphNode<T, W> node) {
this.neighbors.add(node);
}
public void addNeighbor(GraphNode<T, W> node, W weight) {
this.neighbors.add(node);
this.weights.put(node, weight);
}
public List<GraphNode<T, W>> getNeighbors() {
return Collections.unmodifiableList(this.neighbors);
}
public boolean isVisited() {
return isVisited;
}
public void setVisited(boolean visited) {
isVisited = visited;
}
public W getWeight(GraphNode<T, W> node) {
return weights.get(node);
}
@Override
public String toString() {
return data.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GraphNode)) return false;
GraphNode<?, ?> graphNode = (GraphNode<?, ?>) o;
return Objects.equals(data, graphNode.data);
}
@Override
public int hashCode() {
return Objects.hash(data);
}
}
1.9 集合(set)
集合就是“一堆东西”。集合里的“东西”,叫作元素。若x是集合A的元素,则记作x∈A。集合是把人们的直观的或思维中的某些确定的能够区分的对象汇合在一起,使之成为一个整体(或称为单体),这一整体就是集合。组成一集合的那些对象称为这一集合的元素(或简称为元)。
集合有如下特性:
- 确定性
给定一个集合,任给一个元素,该元素或者属于或者不属于该集合,二者必居其一,不允许有模棱两可的情况出现。
- 互异性
一个集合中,任何两个元素都认为是不相同的,即每个元素只能出现一次。有时需要对同一元素出现多次的情形进行刻画,可以使用多重集,其中的元素允许出现多次。
- 无序性
一个集合中,每个元素的地位都是相同的,元素之间是无序的。集合上可以定义序关系,定义了序关系后,元素之间就可以按照序关系排序。但就集合本身的特性而言,元素之间没有必然的序。
Java中提供了Set接口及其实现类。
1.10 跳表(skip list)
跳表是一种随机化的数据结构,其数据元素按照key(键)排序,所以跳表是有序的集合,跳表为了提高查找、插入和删除操作的性能,在原有的有序链表之上随机的让一部分数据元素分布到更多层链表中,从而在查找、插入和删除操作时可以跳过一些不可能涉及的数据元素节点,从而提高效率。假设有如下一个有序链接(每个节点只有一个数据值和指向下一个节点的next指针):
如果我们需要查找12,22,36这三个元素我们每次只能从头开始遍历链表,直到查找到元素为止。明明是一个有序的链表我们却每次都只能一个一个的比较直到目标节点,调表的出现就解决了这种低效的查找方式。
跳表的查找:
跳表会(随机)把一些节点提取出来,做成索引节点,下面就是一个拥有一级索引(索引节点拥有原基层节点的数据,指向右边索引节点的right指针,指向指向下层节点的down指针)的结构:
例如,现在我们再查找12这个元素,我们只需要从一级索引开始往后遍历,即经过3,10,发现后面的17比12大,则一级索引在10处往下走,再从下面的10往右走,就找到12了,是不是比以效率更高,因为跳过了一些不会涉及的节点。同样的,一级索引还可以继续往上提取一批节点组成二级索引:
例如,这时候我们要查找32这个元素,只需从二级索引开始往后遍历,经过3,17,25,发现后面的39大于32,则二级索引25往下走,发现其右边的一级索引36依然大于32,继续往下走,到达基层链表,25的右边刚好就是32了。总之跳表的查找的时候从底层索引的开始往后遍历,一旦发现下一个节点比目标节点大就降到下一层索引,移除类推,直到找到。
跳表的索引链表中的节点称之为“索引节点”,基层链表中的节点是“基准节点”,每一层的第一个节点称之为headIndex,即索引头节点,例如上图的节点3。上面讲述了跳表的基本结构了查找,下面看看插入节点的过程。
跳表的插入:
假设我们现在要插入一个元素20,按照跳表的机制,它会先随机的确定该元素要占据的层数,假设level = 2,然后查找2在最下面2层(包含基层)的前置节点,就是刚好比它小一点的的节点,然后将其插入到它们的后面,最后就成这样了:
若level 大于当前链表的层,则需要添加新的层,例如level > 3, 则就成这样了:
多出来的一层索引,只有headIndex和新插入的20.
跳表的删除节点:
首先找到所有层中包含元素x的节点,然后使用标准的链表删除元素的方法删除即可。例如我们要删除元素25,则我们要删除三层中对应节点:
对比上面的图就可以发现,节点25被移除了。这里,若是我们要删除节点20,那么将会导致整个三级索引层都被移除,这种收缩性可以减少内存消耗。
通过上面对SkipList跳表的数据结构分析,我们可以看出它是一种以空间换时间的算法,它的效率和红黑树以及 AVL 树不相上下,但跳表的原理相当简单,只要你能熟练操作链表,就能轻松实现一个 SkipList,目前开源软件 Redis 和 LevelDB 都有用到它。
由上面对跳表的分析,可以得出其相关的如下特性:
- 跳表由许多层构成,并且每一层都是一个有序链表,但只有最底层(基层)包含所有元素。
- 如果一个元素出现在level(x)层,那么它肯定出现在x以下的所有层中;
- 每个元素插入时随机生成它将处于的level层;
- 每个索引节点包含两个指针,一个向下,一个向右;而每个基层节点则只包含一个next指针;
- 跳表是可以实现二分查找的有序链表;
2. 算法
2.1 大O表示法
- 大O表示法可以告诉我们算法的快慢。
- 大O比较的是操作数,它指出了算法运行时间的增速。
- O(n) 括号里的是操作数的个数。
- 大O表示法所表示的是一个算法在最糟糕情况下的运行时间。
f(n)=O(g(n))表达式的数学定义:
大O表示法f(n)=O(g(n))中的f(n)是g(n)的等阶无穷大或者是同阶无穷大。存在正常数c、n、n0,当n>n0的时,对于任意的f(n)符合0<=f(n)<= cg(n)。
一些常见的大O运行时间:
- O(log n),也叫对数时间,二分查找。
- O(n),也叫线性时间,简单查找。
- O(n * log n),快速排序——一种速度较快的排序算法。
- O(n²),选择排序——一种速度较慢的排序算法。
- O(n!),旅行商问题的解决方案——一种非常慢的算法。
2.1 排序算法
分类 | 排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|---|---|
比较排序 | 交换排序 | 冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(n2) | O(nlog2n) | O(nlog2n) | 不稳定 | ||
插入排序 | 简单插入排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 | |
希尔排序 | O(n1.3) | O(n2) | O(n) | O(1) | 不稳定 | ||
选择排序 | 简单选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 | |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 | ||
归并排序 | 二路归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 | |
多路归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 | ||
非比较排序 | 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 | |
桶排序 | O(n+k) | O(n2) | O(n) | O(n+k) | 稳定 | ||
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
相关概念:
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
2.1.1 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
package com.qupeng.sortalgorithm;
public abstract class AbstractSort {
abstract public void sort(int[] array);
protected void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
package com.qupeng.sortalgorithm;
public class BubbleSort extends AbstractSort{
@Override
public void sort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
}
}
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class BubbleSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[] { 6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78 };
private BubbleSort bubbleSort = new BubbleSort();
@Test
public void sort() {
bubbleSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.2 选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
package com.qupeng.sortalgorithm;
public class SelectionSort extends AbstractSort {
@Override
public void sort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
swap(array, i, minIndex);
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class SelectionSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[] { 6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78 };
private SelectionSort selectionSort = new SelectionSort();
@Test
public void sort() {
selectionSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.3 插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
package com.qupeng.sortalgorithm;
public class InsertionSort extends AbstractSort {
@Override
public void sort(int[] array) {
for (int i = 1; i < array.length; i++) {
int current = array[i];
int preIndex = i - 1;
while (preIndex >= 0 && current < array[preIndex]) {
array[preIndex + 1] = array[preIndex];
--preIndex;
}
array[preIndex + 1] = current;
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class InsertionSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[]{6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78};
private InsertionSort insertionSort = new InsertionSort();
@Test
public void sort() {
insertionSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.4 希尔排序
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。将元素按照指定的间隔分组,每组进行简单插入排序;循环缩小间隔,直到间隔为0,退化为简单插入排序。希尔排序又叫缩小增量排序。
package com.qupeng.sortalgorithm;
public class ShellSort extends AbstractSort {
@Override
public void sort(int[] array) {
for (int gap = array.length / 2; gap > 0 ; gap /= 2 ) {
for (int i = gap; i < array.length; i++) {
int j = i;
int current = array[i];
int preIndex = j - gap;
while (preIndex >= 0 && current < array[preIndex]) {
array[j] = array[preIndex];
j = preIndex;
preIndex -= gap;
}
array[j] = current;
}
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class ShellSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[]{6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78};
private ShellSort shellSort = new ShellSort();
@Test
public void sort() {
shellSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.5 归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
package com.qupeng.sortalgorithm;
public class MergingSort extends AbstractSort {
@Override
public void sort(int[] array){
int []temp = new int[array.length];
sort(array,0,array.length-1,temp);
}
private void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = (left+right)/2;
sort(arr,left,mid,temp);
sort(arr,mid+1,right,temp);
merge(arr,left,mid,right,temp);
}
}
private void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;
int j = mid+1;
int t = 0;
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){
temp[t++] = arr[i++];
}
while(j<=right){
temp[t++] = arr[j++];
}
t = 0;
while(left <= right){
arr[left++] = temp[t++];
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class MergingSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[]{6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78};
private MergingSort mergingSort = new MergingSort();
@Test
public void sort() {
mergingSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.6 快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
package com.qupeng.sortalgorithm;
public class QuickSort extends AbstractSort {
@Override
public void sort(int[] array) {
sort(array, 0, array.length - 1);
sortWithSimplePivot(array, 0, array.length - 1);
}
// 算法1:取第一个元素作为枢纽值
private void sortWithSimplePivot(int[] array, int left, int right) {
if (left >= right) {
return;
}
int pivot = left;
int i = left;
int j = right;
int pivotValue = array[pivot];
while (true) {
while (i < j) {
if (array[j] < pivotValue) {
array[i] = array[j];
break;
}
j--;
}
while (i < j) {
if (array[i] > pivotValue) {
array[j] = array[i];
break;
}
i++;
}
if (i == j) {
array[j] = pivotValue;
sortWithSimplePivot(array, left, j - 1);
sortWithSimplePivot(array, j + 1, right);
return;
}
}
}
// 算法2: 三位取中法计算枢纽值及将枢纽值放到右端,避免枢纽值偏向一边
private void sort(int[] arr, int left, int right) {
if (left < right) {
dealPivot(arr, left, right);
int pivot = right - 1;
int i = left;
int j = right - 1;
while (true) {
while (arr[++i] < arr[pivot]) {
}
while (j > left && arr[--j] > arr[pivot]) {
}
if (i < j) {
swap(arr, i, j);
} else {
break;
}
}
if (i < right) {
swap(arr, i, right - 1);
}
sort(arr, left, i - 1);
sort(arr, i + 1, right);
}
}
private void dealPivot(int[] arr, int left, int right) {
int mid = (left + right) / 2;
if (arr[left] > arr[mid]) {
swap(arr, left, mid);
}
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
if (arr[right] < arr[mid]) {
swap(arr, right, mid);
}
swap(arr, right - 1, mid);
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class QuickSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[]{6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78};
private QuickSort quickSort = new QuickSort();
@Test
public void sort() {
quickSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.7 堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
package com.qupeng.sortalgorithm;
public class HeapSort extends AbstractSort{
@Override
public void sort(int[] array){
for(int i = array.length/2-1;i>=0;i--){
adjustHeap(array,i,array.length);
}
for(int j=array.length-1;j>0;j--){
swap(array,0,j);
adjustHeap(array,0,j);
}
}
private void adjustHeap(int[] array,int i,int length){
int temp = array[i];
for(int k=i*2+1;k<length;k=k*2+1){
if(k+1<length && array[k]<array[k+1]){
k++;
}
if(array[k] >temp){
array[i] = array[k];
i = k;
}else{
break;
}
}
array[i] = temp;
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class HeapSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[] { 6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78 };
private HeapSort heapSort = new HeapSort();
@Test
public void sort() {
heapSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.8 计数排序
空间换时间的一种算法。计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
package com.qupeng.sortalgorithm;
public class CountingSort extends AbstractSort {
@Override
public void sort(int[] array) {
int max = array[0];
for (int i = 0; i < array.length; i++) {
max = array[i] > max ? array[i] : max;
}
int[] count = new int[max + 1];
for (int i = 0; i < array.length; i++) {
++count[array[i]];
}
int indexInResult = 0;
for (int i = 0; i < count.length; i++) {
int counter = count[i];
while (counter-- > 0) {
array[indexInResult++] = i;
}
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class CountingSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[] { 6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78 };
private CountingSort countingSort = new CountingSort();
@Test
public void sort() {
countingSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.9 桶排序
桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。
package com.qupeng.sortalgorithm;
import java.util.Arrays;
public class BucketSort extends AbstractSort {
@Override
public void sort(int[] array) {
int max = array[0];
int min = array[0];
for (int i = 0; i < array.length; i++) {
max = array[i] > max ? array[i] : max;
min = array[i] < min ? array[i] : min;
}
int[][] buckets = new int[(max - min) / array.length + 1][];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new int[max - min];
buckets[i][0] = 1;
}
for (int i = 0; i < array.length; i++) {
int bucketIndex = (array[i] - min) / array.length;
buckets[bucketIndex][buckets[bucketIndex][0]++] = array[i];
}
CountingSort countingSort = new CountingSort();
int mergeIndex = 0;
for (int i = 0; i < buckets.length; i++) {
if (1 == buckets[i][0]) {
continue;
}
buckets[i] = Arrays.copyOfRange(buckets[i], 1,buckets[i][0]);
countingSort.sort(buckets[i]);
System.arraycopy(buckets[i], 0, array, mergeIndex, buckets[i].length);
mergeIndex += buckets[i].length;
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class BucketSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[] { 6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78 };
private BucketSort bucketSort = new BucketSort();
@Test
public void sort() {
bucketSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.10 基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
package com.qupeng.sortalgorithm;
import java.util.Arrays;
public class RadixSort extends AbstractSort {
private int radix = 10;
@Override
public void sort(int[] array) {
// Step 1: find the max value
int max = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
// Step 2: find the max digital
int maxDigitLength = 1;
for (int mod = radix; max > 0 && 0 != max / mod; mod *= radix) {
++maxDigitLength;
}
// Step 3: create the buckets
int[][] buckets = new int[radix][];
// Step 4: loop times is max digit length
for (int i = 0, mod = radix, dev = 1; i < maxDigitLength; i++, mod *= radix, dev *= radix) {
// Put numbers to the buckets
for (int j = 0; j < array.length; j++) {
int bucketIndex = array[j] % mod / dev;
if (null == buckets[bucketIndex]) {
buckets[bucketIndex] = new int[array.length];
buckets[bucketIndex][0] = 1;
}
buckets[bucketIndex][buckets[bucketIndex][0]] = array[j];
++buckets[bucketIndex][0];
}
// Get numbers back to the original array from buckets
int mergeIndex = 0;
for (int j = 0; j <buckets.length ; j++) {
if (null == buckets[j]) {
continue;
}
int[] bucket = Arrays.copyOfRange(buckets[j], 1,buckets[j][0]);
System.arraycopy(bucket, 0, array, mergeIndex, bucket.length);
mergeIndex += bucket.length;
buckets[j][0] = 1;
}
}
}
}
package com.qupeng.sortalgorithm;
import org.junit.Test;
import static org.junit.Assert.*;
public class RadixSortTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 4, 5, 6, 9, 67, 78, 90, 99, 123, 834, 8076};
private int[] array = new int[]{6, 834, 2, 99, 4, 9, 123, 5, 90, 1, 8076, 67, 3, 78};
private RadixSort radixSort = new RadixSort();
@Test
public void sort() {
radixSort.sort(array);
assertArrayEquals(EXPECTED_RESULT, array);
}
}
2.1.11 Tim排序
待补充…
2.2 查找算法
2.2.1 二分查找算法
package com.qupeng.algorithm.search.binary;
public class BinarySearch {
public int recursionBinarySearch(int[] array, int dest) {
return recursionBinarySearch(array, dest,0, array.length -1);
}
public int recursionBinarySearch(int[] array, int dest, int lowIndex, int highIndex) {
if (lowIndex > highIndex) {
return -1;
}
int middleIndex = (lowIndex + highIndex) / 2;
if (dest < array[middleIndex]){
return recursionBinarySearch(array, dest, lowIndex, middleIndex - 1);
} else if(dest > array[middleIndex]){
return recursionBinarySearch(array, dest, middleIndex + 1, highIndex);
} else {
return middleIndex;
}
}
}
package com.qupeng.algorithm.search.binary;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.*;
public class BinarySearchTest {
private static final int[] EXPECTED_RESULT = new int[]{1, 2, 3, 3, 3, 4, 5, 6, 8, 9, 67, 78, 90, 99};
private int[] array = new int[]{6, 8, 2, 99, 4, 9, 3, 5, 90, 1, 3, 67, 3, 78};
private QuickSort quickSort = new QuickSort();
private BinarySearch binarySearch = new BinarySearch();
@Test
public void recursionBinarySearch() {
quickSort.sort(array);
Assert.assertEquals(10, binarySearch.recursionBinarySearch(array, 67));
}
}
2.2.2 广度优先遍历/最短路径查找
package com.qupeng.algorithm.search.breadthfirst;
import com.qupeng.datastructure.graph.Graph;
import com.qupeng.datastructure.graph.GraphNode;
import java.util.*;
public class BreadthFirstSearch<T, W> {
public List<GraphNode<T, W>> travel(Graph<T, W> graph) {
List<GraphNode<T, W>> path = new ArrayList<GraphNode<T, W>>();
Queue<GraphNode<T, W>> queue = new ArrayDeque<GraphNode<T, W>>();
queue.addAll(graph.getRootNodes());
while (!queue.isEmpty()) {
GraphNode<T, W> node = queue.poll();
if (node.isVisited()) {
continue;
}
path.add(node);
node.setVisited(true);
queue.addAll(node.getNeighbors());
}
return path;
}
public List<GraphNode<T, W>> search(Graph<T, W> graph, GraphNode<T, W> destNode) {
Map<GraphNode<T, W>, List<GraphNode<T, W>>> nodeToPath = new HashMap<GraphNode<T, W>, List<GraphNode<T, W>>>();
Queue<GraphNode<T, W>> queue = new ArrayDeque<GraphNode<T, W>>();
queue.addAll(graph.getRootNodes());
while (!queue.isEmpty()) {
GraphNode<T, W> node = queue.poll();
if (node.isVisited()) {
continue;
}
node.setVisited(true);
List<GraphNode<T, W>> path = nodeToPath.get(node);
if (null == path) {
path = new ArrayList<GraphNode<T, W>>();
nodeToPath.put(node, path);
}
path.add(node);
if (node.equals(destNode)) {
return path;
}
for (GraphNode<T, W> neighbor : node.getNeighbors()) {
List<GraphNode<T, W>> neighborPath = nodeToPath.get(neighbor);
if (null == neighborPath) {
neighborPath = new ArrayList<GraphNode<T, W>>();
neighborPath.addAll(path);
nodeToPath.put(neighbor, neighborPath);
}
}
queue.addAll(node.getNeighbors());
}
return null;
}
}
package com.qupeng.algorithm.search.breadthfirst;
import com.qupeng.datastructure.graph.Graph;
import com.qupeng.datastructure.graph.GraphNode;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
public class BreadthFirstSearchTest {
private Graph<String, Integer> graph;
@Before
public void setUp() {
GraphNode<String, Integer> nodeA = new GraphNode<String, Integer>("A");
GraphNode<String, Integer> nodeB = new GraphNode<String, Integer>("B");
GraphNode<String, Integer> nodeC = new GraphNode<String, Integer>("C");
GraphNode<String, Integer> nodeD = new GraphNode<String, Integer>("D");
GraphNode<String, Integer> nodeE = new GraphNode<String, Integer>("E");
GraphNode<String, Integer> nodeF = new GraphNode<String, Integer>("F");
GraphNode<String, Integer> nodeG = new GraphNode<String, Integer>("G");
GraphNode<String, Integer> nodeX = new GraphNode<String, Integer>("X");
nodeA.addNeighbor(nodeC);
nodeA.addNeighbor(nodeD);
nodeA.addNeighbor(nodeF);
nodeC.addNeighbor(nodeB);
nodeB.addNeighbor(nodeX);
nodeD.addNeighbor(nodeX);
nodeC.addNeighbor(nodeD);
nodeF.addNeighbor(nodeG);
nodeG.addNeighbor(nodeE);
nodeE.addNeighbor(nodeX);
this.graph = new Graph<String, Integer>(nodeA);
}
@Test
public void travel() {
BreadthFirstSearch<String, Integer> bfs = new BreadthFirstSearch<>();
List<GraphNode<String, Integer>> path = bfs.travel(graph);
Assert.assertEquals("[A, C, D, F, B, X, G, E]", path.toString());
}
@Test
public void search() {
BreadthFirstSearch<String, Integer> bfs = new BreadthFirstSearch<>();
GraphNode<String, Integer> node = new GraphNode<>("X");
List<GraphNode<String, Integer>> path = bfs.search(graph, node);
Assert.assertEquals("[A, D, X]", path.toString());
}
}
2.2.3 深度优先遍历
package com.qupeng.algorithm.search.depthfirst;
import com.qupeng.datastructure.graph.Graph;
import com.qupeng.datastructure.graph.GraphNode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DepthFirstSearch<T, W> {
public List<GraphNode<T, W>> travel(Graph<T, W> graph) {
List<GraphNode<T, W>> path = new ArrayList<GraphNode<T, W>>();
List<GraphNode<T, W>> roots = graph.getRootNodes();
for (GraphNode<T, W> root : roots) {
travel(root, path);
}
return path;
}
public void travel(GraphNode<T, W> node, List<GraphNode<T, W>> path) {
if (node.isVisited()) {
return;
}
path.add(node);
node.setVisited(true);
for (GraphNode<T, W> neighbor : node.getNeighbors()) {
travel(neighbor, path);
}
}
public List<GraphNode<T, W>> search(Graph<T, W> graph, GraphNode<T, W> destNode) {
Map<GraphNode<T, W>, List<GraphNode<T, W>>> nodeToPath = new HashMap<GraphNode<T, W>, List<GraphNode<T, W>>>();
List<GraphNode<T, W>> roots = graph.getRootNodes();
for (GraphNode<T, W> root : roots) {
search(root, destNode, nodeToPath);
}
return nodeToPath.get(destNode);
}
public void search(GraphNode<T, W> node, GraphNode<T, W> destNode, Map<GraphNode<T, W>, List<GraphNode<T, W>>> nodeToPath) {
if (node.isVisited()) {
return;
}
node.setVisited(true);
List<GraphNode<T, W>> path = nodeToPath.get(node);
if (null == path) {
path = new ArrayList<GraphNode<T, W>>();
nodeToPath.put(node, path);
}
path.add(node);
for (GraphNode<T, W> neighbor : node.getNeighbors()) {
List<GraphNode<T, W>> neighborPath = nodeToPath.get(neighbor);
if (null == neighborPath) {
neighborPath = new ArrayList<GraphNode<T, W>>();
neighborPath.addAll(path);
nodeToPath.put(neighbor, neighborPath);
}
search(neighbor, destNode, nodeToPath);
}
}
}
package com.qupeng.algorithm.search.depthfirst;
import com.qupeng.datastructure.graph.Graph;
import com.qupeng.datastructure.graph.GraphNode;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
public class DepthFirstSearchTest {
DepthFirstSearch<String, Integer> dfs = new DepthFirstSearch<>();
private Graph<String, Integer> graph;
@Before
public void setUp() {
GraphNode<String, Integer> nodeA = new GraphNode<String, Integer>("A");
GraphNode<String, Integer> nodeB = new GraphNode<String, Integer>("B");
GraphNode<String, Integer> nodeC = new GraphNode<String, Integer>("C");
GraphNode<String, Integer> nodeD = new GraphNode<String, Integer>("D");
GraphNode<String, Integer> nodeE = new GraphNode<String, Integer>("E");
GraphNode<String, Integer> nodeF = new GraphNode<String, Integer>("F");
GraphNode<String, Integer> nodeG = new GraphNode<String, Integer>("G");
GraphNode<String, Integer> nodeX = new GraphNode<String, Integer>("X");
nodeA.addNeighbor(nodeC);
nodeA.addNeighbor(nodeD);
nodeA.addNeighbor(nodeF);
nodeC.addNeighbor(nodeB);
nodeB.addNeighbor(nodeX);
nodeD.addNeighbor(nodeX);
nodeC.addNeighbor(nodeD);
nodeF.addNeighbor(nodeG);
nodeG.addNeighbor(nodeE);
nodeE.addNeighbor(nodeX);
this.graph = new Graph<String, Integer>(nodeA);
}
@Test
public void travel() {
List<GraphNode<String, Integer>> path = dfs.travel(graph);
Assert.assertEquals("[A, C, B, X, D, F, G, E]", path.toString());
}
@Test
public void search() {
GraphNode<String, Integer> node = new GraphNode<>("X");
List<GraphNode<String, Integer>> path = dfs.search(graph, node);
Assert.assertEquals("[A, C, B, X]", path.toString());
}
}
2.2.4 狄克斯特拉算法
查找加权无环图的最少路径。
package com.qupeng.algorithm.search.dijkstra;
import com.qupeng.datastructure.graph.Graph;
import com.qupeng.datastructure.graph.GraphNode;
import java.util.*;
public class DijkstraSearch<T> {
public List<GraphNode<T, Integer>> shortestPath(Graph<T, Integer> graph, GraphNode<T, Integer> dest) {
Map<GraphNode<T, Integer>, Integer> nodeToTotalWeight = new HashMap<>();
Map<GraphNode<T, Integer>, GraphNode<T, Integer>> nodeToTParent = new HashMap<>();
travel(graph, nodeToTotalWeight, nodeToTParent);
List<GraphNode<T, Integer>> path = new ArrayList<>();
path.add(dest);
GraphNode<T, Integer> parent = nodeToTParent.get(dest);
while (null != parent) {
path.add(0, parent);
parent = nodeToTParent.get(parent);
}
return path;
}
public int minimumWeight(Graph<T, Integer> graph, GraphNode<T, Integer> dest) {
Map<GraphNode<T, Integer>, Integer> nodeToTotalWeight = new HashMap<>();
Map<GraphNode<T, Integer>, GraphNode<T, Integer>> nodeToTParent = new HashMap<>();
travel(graph, nodeToTotalWeight, nodeToTParent);
return nodeToTotalWeight.get(dest);
}
private void travel(Graph<T, Integer> graph, Map<GraphNode<T, Integer>, Integer> nodeToTotalWeight, Map<GraphNode<T, Integer>, GraphNode<T, Integer>> nodeToTParent) {
Queue<GraphNode<T, Integer>> queue = new ArrayDeque<>();
for (GraphNode<T, Integer> root : graph.getRootNodes()) {
nodeToTotalWeight.put(root, 0);
queue.add(root);
while (!queue.isEmpty()) {
GraphNode<T, Integer> node = queue.poll();
if (node.isVisited()) {
continue;
}
node.setVisited(true);
Integer selfTotalWeight = nodeToTotalWeight.get(node);
for (GraphNode<T, Integer> neighbor : node.getNeighbors()) {
Integer neighborTotalWeight = nodeToTotalWeight.get(neighbor);
Integer newTotalWeight = selfTotalWeight + node.getWeight(neighbor);
if (null == neighborTotalWeight || neighborTotalWeight > newTotalWeight) {
nodeToTotalWeight.put(neighbor, newTotalWeight);
nodeToTParent.put(neighbor, node);
}
}
queue.addAll(node.getNeighbors());
}
}
}
}
```java
package com.qupeng.algorithm.search.dijkstra;
import com.qupeng.datastructure.graph.Graph;
import com.qupeng.datastructure.graph.GraphNode;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.*;
public class DijkstraSearchTest {
private Graph<String, Integer> graph;
private DijkstraSearch<String> search = new DijkstraSearch();;
@Before
public void setup() {
GraphNode<String, Integer> nodeStart = new GraphNode<String, Integer>("start");
GraphNode<String, Integer> nodeA = new GraphNode<String, Integer>("A");
GraphNode<String, Integer> nodeB = new GraphNode<String, Integer>("B");
GraphNode<String, Integer> nodeC = new GraphNode<String, Integer>("C");
GraphNode<String, Integer> nodeD = new GraphNode<String, Integer>("D");
GraphNode<String, Integer> nodeEnd = new GraphNode<String, Integer>("end");
nodeStart.addNeighbor(nodeA, 5);
nodeStart.addNeighbor(nodeB, 2);
nodeA.addNeighbor(nodeC, 4);
nodeA.addNeighbor(nodeD, 2);
nodeB.addNeighbor(nodeA, 8);
nodeB.addNeighbor(nodeD, 7);
nodeC.addNeighbor(nodeD, 6);
nodeC.addNeighbor(nodeEnd, 3);
nodeD.addNeighbor(nodeEnd, 1);
this.graph = new Graph(nodeStart);
}
@Test
public void shortestPath() {
List<GraphNode<String, Integer>> path = search.shortestPath(graph, new GraphNode<String, Integer>("end"));
Assert.assertEquals("[start, A, D, end]", path.toString());
}
@Test
public void minimumWeight() {
int minimumWeight = search.minimumWeight(graph, new GraphNode<String, Integer>("end"));
Assert.assertEquals(8, minimumWeight);
}
}
2.3 贪婪算法
贪婪算法每步都选择局部最优解,最终得到全局最优解。
2.3.1 教室调度问题
贪婪算法可用于计算教室调度问题的最优解。
教室调度问题是将尽可能多的课程安排在同一间教室。
课程 | 开始时间 | 结束时间 |
---|---|---|
美术 | 9AM | 10AM |
英语 | 9:30AM | 10:30AM |
数学 | 10AM | 11AM |
计算机 | 10:30AM | 11:30AM |
音乐 | 11AM | 12PM |
算法实现:
package com.qupeng.algorithm.greedy.roomscheduling;
import java.util.Date;
public class Class implements Comparable<Class> {
private String name;
private Date startTime;
private Date endTime;
public Class(String name, Date startTime, Date endTime) {
this.name = name;
this.startTime = startTime;
this.endTime = endTime;
}
public Date getStartTime() {
return startTime;
}
public Date getEndTime() {
return endTime;
}
@Override
public int compareTo(Class clazz) {
if (null == clazz) {
throw new RuntimeException("Can't be null.");
}
return this.endTime.compareTo(clazz.getEndTime());
}
}
package com.qupeng.algorithm.greedy.roomscheduling;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class RoomScheduling {
public List<Class> schedule(List<Class> classes) {
List<Class> classPlan = new ArrayList<>();
if (null == classes || classes.isEmpty()) {
return classPlan;
}
// step1: 按照结束时间排序,保证每次都找到最早结束的课程
List<Class> classList = new ArrayList<>(classes.size());
classList.addAll(classes);
Collections.sort(classList);
classPlan.add(classList.get(0));
classList.remove(0);
int lastIndex = 0;
// step2: 遍历所有的课程,选出在已选课程之后开始,且最早结束的课程
Iterator<Class> iter = classList.iterator();
while (iter.hasNext()) {
Class clazz = iter.next();
// 局部最优解:在最近一个已选课程之后开始,且最早结束的课程
if (clazz.getStartTime().compareTo(classPlan.get(lastIndex).getEndTime()) != -1) {
classPlan.add(clazz);
++ lastIndex;
}
}
return classPlan;
}
}
package com.qupeng.algorithm.greedy.roomscheduling;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
public class RoomSchedulingTest {
private RoomScheduling roomScheduling = new RoomScheduling();
private List<Class> classList = new ArrayList<>();
private Class art;
private Class english;
private Class mathematics;
private Class computer;
private Class music;
@Before
public void setup() throws ParseException {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
art = new Class("art", dateFormat.parse("0000-00-00 09:00:00"), dateFormat.parse("0000-00-00 10:00:00"));
english = new Class("english", dateFormat.parse("0000-00-00 09:30:00"), dateFormat.parse("0000-00-00 10:30:00"));
mathematics = new Class("mathematics", dateFormat.parse("0000-00-00 10:00:00"), dateFormat.parse("0000-00-00 11:00:00"));
computer = new Class("computer", dateFormat.parse("0000-00-00 10:30:00"), dateFormat.parse("0000-00-00 11:30:00"));
music = new Class("music", dateFormat.parse("0000-00-00 11:00:00"), dateFormat.parse("0000-00-00 12:00:00"));
classList.add(art);
classList.add(english);
classList.add(mathematics);
classList.add(computer);
classList.add(music);
}
@Test
public void schedule() {
List<Class> classPlan = roomScheduling.schedule(classList);
Class[] expecteds = new Class[] {art, mathematics, music};
Assert.assertArrayEquals(expecteds, classPlan.toArray());
}
}
2.3.2 NP完全问题
贪婪算法可用于计算NP完全问题的近似解。
NP完全问题(NP-C问题),是世界七大数学难题之一。 NP的英文全称是Non-deterministic Polynomial的问题,即多项式复杂程度的非确定性问题。简单的写法是 NP=P?,问题就在这个问号上,到底是NP等于P,还是NP不等于P。
- 旅行商问题
这个问题是说的一个销售员,要去5个城市,他想规划一下最短距离,然后选出最短的距离。5个城市一共有120种规划方案(5!)。n个城市就有n!种规划方案。旅行商问题在计算机科学领域是无解的。旅行商问题用大O表示法就是O(n!),没错,就是有这么慢的算法。 - 集合覆盖问题
假设你办了个广播节目,要让全美50个州的听众都收听得到。为此,你需要决定在哪些广播台播出。在每个广播台播出都需要要支付费用,因此你力图在尽可能少的广播台播出。
所有需要覆盖的州:a,b,c,d,e,f,g,h,i,j,k,l
广播台 | 覆盖的州 |
---|---|
A | a,e,i,m |
B | b,f,j,n |
C | b,g,k,o |
D | d,h,l,p |
E | a,b,c,d,q |
F | e,f,g,r |
算法实现:
package com.qupeng.algorithm.greedy.setcovering;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SetCovering {
public List<Set<String>> cover (Set<String> setNeedCovered, List<Set<String>> setList) {
List<Set<String>> fullCoveredSets = new ArrayList<>();
if (null == setNeedCovered || setNeedCovered.isEmpty() || null == setList || setList.isEmpty()) {
return fullCoveredSets;
}
Set<String> setNeedCoveredTmp = new HashSet<>();
setNeedCoveredTmp.addAll(setNeedCovered);
List<Set<String>> setListTmp = new ArrayList<>();
setListTmp.addAll(setList);
while (!setNeedCoveredTmp.isEmpty() && !setListTmp.isEmpty()) {
Set<String> setTmp = new HashSet<>();
int maxCoveredNumber = 0;
Set<String> maxCoveredSet = null;
for (Set<String> set : setListTmp) {
setTmp.addAll(set);
setTmp.retainAll(setNeedCoveredTmp);
if (maxCoveredNumber < setTmp.size()) {
maxCoveredNumber = setTmp.size();
maxCoveredSet = set;
}
setTmp.clear();
}
setNeedCoveredTmp.removeAll(maxCoveredSet);
fullCoveredSets.add(maxCoveredSet);
setListTmp.remove(maxCoveredSet);
}
return fullCoveredSets;
}
}
package com.qupeng.algorithm.greedy.setcovering;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.*;
import static org.junit.Assert.*;
public class SetCoveringTest {
private SetCovering setCovering = new SetCovering();
private Set<String> setNeedCovered = new HashSet<>();
private List<Set<String>> setList = new ArrayList<>();
private Set<String> setA = new HashSet<>();
private Set<String> setB = new HashSet<>();
private Set<String> setC = new HashSet<>();
private Set<String> setD = new HashSet<>();
private Set<String> setE = new HashSet<>();
private Set<String> setF = new HashSet<>();
@Before
public void setup() {
setNeedCovered.addAll(Arrays.asList(new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"}));
setA.addAll(Arrays.asList(new String[] {"a", "e", "i", "m"}));
setB.addAll(Arrays.asList(new String[] {"b", "f", "j", "n"}));
setC.addAll(Arrays.asList(new String[] {"c", "g", "k", "o"}));
setD.addAll(Arrays.asList(new String[] {"d", "h", "l", "p"}));
setE.addAll(Arrays.asList(new String[] {"a", "b", "c", "d", "q"}));
setF.addAll(Arrays.asList(new String[] {"e", "f", "g", "r"}));
setList.add(setA);
setList.add(setB);
setList.add(setC);
setList.add(setD);
setList.add(setE);
setList.add(setF);
}
@Test
public void cover() {
List<Set<String>> fullCoveredSets = setCovering.cover(setNeedCovered, setList);
Assert.assertArrayEquals(new Set[] {setE, setF, setD, setA, setB, setC}, fullCoveredSets.toArray());
}
}
没办法简单判断问题是不是NP完全问题,但还是有一些蛛丝马迹可循的。
- 元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变得非常慢。
- 涉及“所有组合”的问题通常是NP完全回题。要计算的可能路线过
- 不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题。
- 如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题。
- 如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题。
- 如果问题可转换为集合覆盖问题或旅行商间题;那它肯定是NP完全问题。
2.4 动态规划
借助网格将问题分成小问题,并先着手解决小问题
2.4.1 背包问题
假设你是个小偷,背着一个可装4磅东西的背包。你可盗窃的商品有4件。
商品 | 重量 | 价格 |
---|---|---|
吉他 | 1磅 | 1500美元 |
音响 | 4磅 | 3000美元 |
笔记本电脑 | 3磅 | 2000美元 |
iPhone | 1磅 | 2000美元 |
为了让盗窃的商品价值最高,你该选择那些商品 ?
package com.qupeng.algorithm.dynamicprogramming.knapsackproblem;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Knapsack {
int capacity;
List<Product> products = new ArrayList<>();
public Knapsack(int capacity) {
this.capacity = capacity;
}
public void addProduct(Product product) {
this.products.add(product);
}
public void addProducts(Collection<Product> products) {
this.products.addAll(products);
}
public int getTotalValue() {
int value = 0;
for (Product product : this.products) {
value += product.value;
}
return value;
}
}
package com.qupeng.algorithm.dynamicprogramming.knapsackproblem;
public class Product {
String name;
int weight;
int value;
public Product(String name, int weight, int value) {
this.name = name;
this.weight = weight;
this.value = value;
}
@Override
public String toString() {
return this.name;
}
}
package com.qupeng.algorithm.dynamicprogramming.knapsackproblem;
public class KnapsackProblem {
public int calculateValue(Knapsack knapsack, Product[] products) {
int[][] grid = new int[products.length][knapsack.capacity];
for (int i = 0; i < grid.length; i++) {
int[] currentProduct = grid[i];
if (0 == i) {
for (int j = 0; j < knapsack.capacity; j++) {
grid[i][j] = j + 1 >= products[i].weight ? products[i].value : 0;
}
} else {
int[] preProcuct = grid[i - 1];
for (int j = 0; j < knapsack.capacity; j++) {
if (j + 1 < products[i].weight) {
currentProduct[j] = preProcuct[j];
} else if (j + 1 == products[i].weight) {
currentProduct[j] = Math.max(products[i].value, preProcuct[j]);
} else if (j + 1 > products[i].weight) {
currentProduct[j] = Math.max(preProcuct[j], products[i].value + preProcuct[j - products[i].weight]);
}
}
}
}
return grid[products.length - 1][knapsack.capacity - 1];
}
public Knapsack calculateProducts(Knapsack knapsack, Product[] products) {
Knapsack[][] grid = new Knapsack[products.length][knapsack.capacity];
for (int i = 0; i < grid.length; i++) {
Knapsack[] currentProduct = grid[i];
if (0 == i) {
for (int j = 0; j < knapsack.capacity; j++) {
grid[i][j] = new Knapsack(j);
grid[i][j].addProduct(j + 1 >= products[i].weight ? products[i] : null);
}
} else {
Knapsack[] preProcuct = grid[i - 1];
for (int j = 0; j < knapsack.capacity; j++) {
grid[i][j] = new Knapsack(j);
if (j + 1 < products[i].weight) {
currentProduct[j] = preProcuct[j];
} else if (j + 1 == products[i].weight) {
if (products[i].value > preProcuct[j].getTotalValue()) {
currentProduct[j].addProduct(products[i]);
} else {
currentProduct[j].addProducts( preProcuct[j].products);
}
} else if (j + 1 > products[i].weight) {
if (preProcuct[j].getTotalValue() > products[i].value + preProcuct[j - products[i].weight].getTotalValue()) {
currentProduct[j].addProducts(preProcuct[j].products);
} else {
currentProduct[j].addProduct(products[i]);
currentProduct[j].addProducts(preProcuct[j - products[i].weight].products);
}
}
}
}
}
return grid[products.length - 1][knapsack.capacity - 1];
}
}
package com.qupeng.algorithm.dynamicprogramming.knapsackproblem;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public class KnapsackProblemTest {
private KnapsackProblem knapsackProblem = new KnapsackProblem();
private Knapsack knapsack = new Knapsack(4);
private Product[] products = new Product[] {new Product("guitar", 1, 1500), new Product("stereo", 4, 3000), new Product("laptop", 3, 2000), new Product("iPhone", 1, 2000)};
@Before
public void setup() {
}
@Test
public void calculate() {
int maxValue = knapsackProblem.calculateValue(knapsack, products);
assertEquals(4000, maxValue);
}
@Test
public void calculateProducts() {
Knapsack knapsack = knapsackProblem.calculateProducts(this.knapsack, products);
assertEquals(4000, knapsack.getTotalValue());
assertEquals("[iPhone, laptop]", knapsack.products.toString());
}
}
2.4.2 求最长公共子字符串/子序列
假设你管理这一个字典网站,用户输入单词时,你需要给出其定义。但是如果用户拼错了,你必须猜测他原本要输入的是什么单词。例如fish不小心输入了hish。在你的字典里,根本没有这样的单词,但是有几个类似的单词。如果能计算出两个单词的最长公共子字符串或者子序列,就可以找出与用户输入最接近的单词。
算法实现:
package com.qupeng.algorithm.dynamicprogramming.longestcommonstring;
public class LongestCommonSubString {
public String calcuteSubString(String str1, String str2) {
int rowIndex = -1;
int columnInex = -1;
int maxCommonSubStrLen = 0;
int[][] grid = new int[str1.length()][str2.length()];
for (int i = 0; i < str1.length(); i++) {
char charOfStr1 = str1.charAt(i);
for (int j = 0; j < str2.length(); j++) {
char charOfStr2 = str2.charAt(j);
if (charOfStr1 == charOfStr2) {
grid[i][j] = grid[0 == i ? 1 : i - 1][0 == j ? 1 : j - 1] + 1;
if (grid[i][j] > maxCommonSubStrLen) {
maxCommonSubStrLen = grid[i][j];
rowIndex = i;
columnInex = j;
}
} else {
grid[i][j] = 0;
}
}
}
if (-1 == rowIndex || -1 == columnInex) {
return "";
}
return str1.substring(rowIndex - grid[rowIndex][columnInex] + 1, rowIndex + 1);
}
public String calcuteSubSequence(String str1, String str2) {
int rowIndex = -1;
int columnInex = -1;
int maxCommonSubStrLen = 0;
int[][] grid = new int[str1.length()][str2.length()];
for (int i = 0; i < str1.length(); i++) {
char charOfStr1 = str1.charAt(i);
for (int j = 0; j < str2.length(); j++) {
char charOfStr2 = str2.charAt(j);
if (charOfStr1 == charOfStr2) {
grid[i][j] = grid[0 == i ? 1 : i - 1][0 == j ? 1 : j - 1] + 1;
if (grid[i][j] > maxCommonSubStrLen) {
maxCommonSubStrLen = grid[i][j];
rowIndex = i;
columnInex = j;
}
} else {
grid[i][j] = Math.max(grid[0 == i ? 0 : i - 1][j], grid[i][0 == j ? 0 :j - 1]);
}
}
}
if (-1 == rowIndex || -1 == columnInex) {
return "";
}
int commonSubSequenceLenght = grid[rowIndex][columnInex];
StringBuilder stringBuilder = new StringBuilder(commonSubSequenceLenght);
while (commonSubSequenceLenght > 0 && rowIndex >= 0 && columnInex >= 0) {
if (str1.charAt(rowIndex) == str2.charAt(columnInex)) {
stringBuilder.insert(0, str1.charAt(rowIndex));
commonSubSequenceLenght--;
} else {
stringBuilder.insert(0, "*");
}
rowIndex--;
columnInex--;
}
return stringBuilder.toString();
}
}
package com.qupeng.algorithm.dynamicprogramming.longestcommonstring;
import org.junit.Assert;
import org.junit.Test;
import static org.junit.Assert.*;
public class LongestCommonSubStringTest {
private LongestCommonSubString longestCommonSubString = new LongestCommonSubString();
@Test
public void calcute() {
String subString = longestCommonSubString.calcuteSubString("12345", "134567");
Assert.assertEquals("345", subString);
}
@Test
public void calcuteSubSequence() {
String subSequence = longestCommonSubString.calcuteSubSequence("12345", "010045678");
Assert.assertEquals("1**45", subSequence);
}
}
2.5 K最近邻算法
要对东西进行分类时,可首先尝试这种算法。例如用户喜好推荐,找出与目标用户喜好相近的其他用户,将这些用户的喜好推荐给目标用户。