我的技术博客:www.zhenganwen.topjava
public static class Node{
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
}
public static void preOrderRecursive(Node root) {
if (root != null) {
System.out.print(root.data+" ");
preOrderRecursive(root.left);
preOrderRecursive(root.right);
}
}
public static void medOrderRecursive(Node root) {
if (root != null) {
medOrderRecursive(root.left);
System.out.print(root.data+" ");
medOrderRecursive(root.right);
}
}
public static void postOrderRecursive(Node root) {
if (root != null) {
postOrderRecursive(root.left);
postOrderRecursive(root.right);
System.out.print(root.data+" ");
}
}
public static void main(String[] args) {
Node root = new Node(1);
root.left = new Node(2);
root.right = new Node(3);
root.left.left = new Node(4);
root.left.right = new Node(5);
root.right.left = new Node(6);
root.right.right = new Node(7);
preOrderRecursive(root); //1 2 4 5 3 6 7
System.out.println();
medOrderRecursive(root); //4 2 5 1 6 3 7
System.out.println();
postOrderRecursive(root); //4 5 2 6 7 3 1
System.out.println();
}
复制代码
以先根遍历二叉树为例,能够发现递归方式首先尝试打印当前结点的值,随后尝试打印左子树,打印完左子树后尝试打印右子树,递归过程的base case
是当某个结点为空时中止子过程的展开。这种递归尝试是由二叉树自己的结构所决定的,由于二叉树上的任意结点均可看作一棵二叉树的根结点(即便是叶子结点,也能够看作是一棵左右子树为空的二叉树根结点)。node
观察先序、中序、后序三个递归方法你会发现,不一样点在于打印当前结点的值这一操做的时机。你会发现每一个结点会被访问三次:进入方法时算一次、递归处理左子树完成以后返回时算一次、递归处理右子树完成以后返回时算一次。所以在preOrderRecursive
中将打印语句放到方法开始时就产生了先序遍历;在midOrderRecursive
中,将打印语句放到递归chu处理左子树完成以后就产生了中序遍历。面试
拿到一棵树的根结点后,首先打印该结点的值,而后将其非空右孩子、非空左孩子依次压栈。栈非空循环:从栈顶弹出结点(一棵子树的根节点)并打印其值,再将其非空右孩子、非空左孩子依次压栈。算法
public static void preOrderUnRecur(Node root) {
if (root == null) {
return;
}
Stack<Node> stack = new Stack<>();
stack.push(root);
Node cur;
while (!stack.empty()) {
cur = stack.pop();
System.out.print(cur.data+" ");
if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
System.out.println();
}
复制代码
你会发现压栈的顺序和打印的顺序是相反的,压栈是先根结点,而后有右孩子就压右孩子、有左孩子就压左孩子,这是利用栈的后进先出。每次获取到一棵子树的根节点以后就能够获取其左右孩子,所以无需保留其信息,直接弹出并打印,而后保留其左右孩子到栈中便可。数据库
对于一棵树,将该树的左边界所有压栈,root
的走向是只要左孩子不为空就走向左孩子。当左孩子为空时弹出栈顶结点(此时该结点是一棵左子树为空的树的根结点,根据中序遍历能够直接打印该结点,而后中序遍历该结点的右子树)打印,若是该结点的右孩子非空(说明有右子树),那么将其右孩子压栈,这个右孩子又多是一棵子树的根节点,所以将这棵子树的左边界压栈,这时回到了开头,以此类推。api
public static void medOrderUnRecur(Node root) {
if (root == null) {
return;
}
Stack<Node> stack = new Stack<>();
while (!stack.empty() || root != null) {
if (root != null) {
stack.push(root);
root = root.left;
} else {
root = stack.pop();
System.out.print(root.data+" ");
root = root.right;
}
}
System.out.println();
}
复制代码
思路一:准备两个栈,一个栈用来保存遍历时的结点信息,另外一个栈用来排列后根顺序(根节点先进栈,右孩子再进,左孩子最后进)。数组
public static void postOrderUnRecur1(Node root) {
if (root == null) {
return;
}
Stack<Node> stack1 = new Stack<>();
Stack<Node> stack2 = new Stack<>();
stack1.push(root);
while (!stack1.empty()) {
root = stack1.pop();
if (root.left != null) {
stack1.push(root.left);
}
if (root.right != null) {
stack1.push(root.right);
}
stack2.push(root);
}
while (!stack2.empty()) {
System.out.print(stack2.pop().data + " ");
}
System.out.println();
}
复制代码
思路二:只用一个栈。借助两个变量
h
和c
,h
表明最近一次打印过的结点,c
表明栈顶结点。首先将根结点压栈,此后栈非空循环,令c
等于栈顶元素(c=stack.peek()
)执行如下三个分支:缓存
c
的左右孩子是否与h
相等,若是都不相等,说明c
的左右孩子都不是最近打印过的结点,因为左右孩子是左右子树的根节点,根据后根遍历的特色,左右子树确定都没打印过,那么将左孩子压栈(打印左子树)。- 分支1没有执行说明
c
的左孩子要么不存在;要么左子树刚打印过了;要么右子树刚打印过了。这时若是是前两种状况中的一种,那就轮到打印右子树了,所以若是c
的右孩子非空就压栈。- 若是前两个分支都没执行,说明
c
的左右子树都打印完了,所以弹出并打印c
结点,更新一下h
。
public static void postOrderUnRecur2(Node root) {
if (root == null) {
return;
}
Node h = null; //最近一次打印的结点
Node c = null; //表明栈顶结点
Stack<Node> stack = new Stack<>();
stack.push(root);
while (!stack.empty()) {
c = stack.peek();
if (c.left != null && c.left != h && c.right != h) {
stack.push(c.left);
} else if (c.right != null && c.right != h) {
stack.push(c.right);
} else {
System.out.print(stack.pop().data + " ");
h = c;
}
}
System.out.println();
}
复制代码
这里的后继结点不一样于链表的后继结点。在二叉树中,前驱结点和后继结点是按照二叉树中两个结点被中序遍历的前后顺序来划分的。好比某二叉树的中序遍历是
2 1 3
,那么1
的后继结点是3
,前驱结点是2
安全
你固然能够将二叉树中序遍历一下,在遍历到该结点的时候标记一下,那么下一个要打印的结点就是该结点的后继结点。bash
咱们能够推测一下,当咱们来到二叉树中的某个结点时,若是它的右子树非空,那么它的后继结点必定是它的右子树中最靠左的那个结点;若是它的右孩子为空,那么它的后继结点必定是它的祖先结点中,把它当作左子孙(它存在于祖先结点的左子树中)的那一个,不然它没有后继结点。
这里若是它的右孩子为空的状况比较难分析,咱们能够借助一个指针parent
,当前来到的结点node
和其父结点parent
的parent.left
比较,若是相同则直接返回parent
,不然node
来到parent
的位置,parent
则继续向上追溯,直到parent
到达根节点为止若node
仍是不等于parent
的左孩子,则返回null
代表给出的结点没有后继结点。
public class FindSuccessorNode {
public static class Node{
int data;
Node left;
Node right;
Node parent;
public Node(int data) {
this.data = data;
}
}
public static Node findSuccessorNode(Node node){
if (node == null) {
return null;
}
if (node.right != null) {
node = node.right;
while (node.left != null) {
node = node.left;
}
return node;
} else {
Node parent = node.parent;
while (parent != null && parent.left != node) {
node = parent;
parent = parent.parent;
}
return parent == null ? null : parent;
}
}
public static void main(String[] args) {
Node root = new Node(1);
root.left = new Node(2);
root.left.parent = root;
root.left.left = new Node(4);
root.left.left.parent = root.left;
root.left.right = new Node(5);
root.left.right.parent = root.left;
root.right = new Node(3);
root.right.parent = root;
root.right.right = new Node(6);
root.right.right.parent = root.right;
if (findSuccessorNode(root.left.right) != null) {
System.out.println("node5's successor node is:"+findSuccessorNode(root.left.right).data);
} else {
System.out.println("node5's successor node doesn't exist");
}
if (findSuccessorNode(root.right.right) != null) {
System.out.println("node6's successor node is:"+findSuccessorNode(root.right.right).data);
} else {
System.out.println("node6's successor node doesn't exist");
}
}
}
复制代码
二叉树的序列化要注意的两个点以下:
- 每序列化一个结点数值以后都应该加上一个结束符表示一个结点序列化的终止,如
!
。- 不能忽视空结点的存在,可使用一个占位符如
#
表示空结点的序列化。
/** * 先根遍历的方式进行序列化 * @param node 序列化来到了哪一个结点 * @return */
public static String serializeByPre(Node node) {
if (node == null) {
return "#!";
}
//收集以当前结点为根节点的树的序列化信息
String res = node.data + "!";
//假设可以获取左子树的序列化结果
res += serializeByPre(node.left);
//假设可以获取右子树的序列化结果
res += serializeByPre(node.right);
//返回以当前结点为根节点的树的序列化结果
return res;
}
public static void main(String[] args) {
Node root = new Node(1);
root.left = new Node(2);
root.left.left = new Node(4);
root.left.right = new Node(5);
root.right = new Node(3);
root.right.right = new Node(6);
System.out.println(serializeByPre(root));
}
复制代码
怎么序列化的,就怎么反序列化
public static Node reconstrut(String serializeStr) {
if (serializeStr != null) {
String[] datas = serializeStr.split("!");
if (datas.length > 0) {
//借助队列保存结点数值
Queue<String> queue = new LinkedList<>();
for (String data : datas) {
queue.offer(data);
}
return recon(queue);
}
}
return null;
}
private static Node recon(Queue<String> queue) {
//依次出队元素重建结点
String data = queue.poll();
//重建空结点,也是base case,当要重建的某棵子树为空时直接返回
if (data.equals("#")) {
return null;
}
//重建头结点
Node root = new Node(Integer.parseInt(data));
//重建左右子树
root.left = recon(queue);
root.right = recon(queue);
return root;
}
public static void main(String[] args) {
Node root = new Node(1);
root.left = new Node(2);
root.left.left = new Node(4);
root.left.right = new Node(5);
root.right = new Node(3);
root.right.right = new Node(6);
String str = serializeByPre(root);
Node root2 = reconstrut(str);
System.out.println(serializeByPre(root2));
}
复制代码
平衡二叉树的定义:当二叉树的任意一棵子树的左子树的高度和右子树的高度相差不超过1时,该二叉树为平衡二叉树。
根据定义可知,要确认一个二叉树是不是平衡二叉树势必要遍历全部结点。而遍历到每一个结点时,要想知道以该结点为根结点的子树是不是平衡二叉树,咱们要收集两个信息:
那么咱们来到某个结点时(子过程),咱们须要向上层(父过程)返回的信息就是该结点为根结点的树是不是平衡二叉树以及该结点的高度,这样的话,父过程就能继续向上层返回应该收集的信息。
package top.zhenganwen.algorithmdemo.recursive;
/** * 判断是否为平衡二叉树 */
public class IsBalanceBTree {
public static class Node{
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
}
/** * 遍历时,来到某个结点须要收集的信息 * 一、以该结点为根节点的树是不是平衡二叉树 * 二、该结点的高度 */
public static class ReturnData {
public boolean isBalanced;
public int height;
public ReturnData(boolean isBalanced, int height) {
this.isBalanced = isBalanced;
this.height = height;
}
}
public static ReturnData isBalancedBinaryTree(Node node){
if (node == null) {
return new ReturnData(true, 0);
}
ReturnData leftData = isBalancedBinaryTree(node.left);
if (leftData.isBalanced == false) {
//只要有一棵子树不是平衡二叉树,则会一路返回false,该树的高度天然没必要收集了
return new ReturnData(false, 0);
}
ReturnData rightDta = isBalancedBinaryTree(node.right);
if (rightDta.isBalanced == false) {
return new ReturnData(false, 0);
}
//返回该层收集的结果
if (Math.abs(leftData.height - rightDta.height) > 1) {
return new ReturnData(false, 0);
}
//如果平衡二叉树,树高等于左右子树较高的那个加1
return new ReturnData(true, Math.max(leftData.height, rightDta.height) + 1);
}
public static void main(String[] args) {
Node root = new Node(1);
root.left = new Node(2);
root.left.left = new Node(4);
root.right = new Node(3);
root.right.right = new Node(5);
root.right.right.right = new Node(6);
System.out.println(isBalancedBinaryTree(root).isBalanced); //false
}
}
复制代码
递归很好用,该题中的递归用法也是一种经典用法,能够高度套路:
- 分析问题的解决须要哪些步骤(这里是遍历每一个结点,确认每一个结点为根节点的子树是否为平衡二叉树)
- 肯定递归:父问题是否和子问题相同
- 子过程要收集哪些信息
- 本次递归如何利用子过程返回的信息获得本过程要返回的信息
base case
搜索二叉树的定义:对于二叉树的任意一棵子树,其左子树上的全部结点的值小于该子树的根节点的值,而其右子树上的全部结点的值大于该子树的根结点的值,而且整棵树上任意两个结点的值不一样。
根据定义,搜索二叉树的中序遍历打印将是一个升序序列。所以咱们能够利用二叉树的中序遍历的非递归方式,比较中序遍历时相邻两个结点的大小,只要有一个结点的值小于其后继结点的那就不是搜索二叉树。
import java.util.Stack;
/** * 判断是不是搜索二叉树 */
public class IsBST {
public static class Node {
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
}
public static boolean isBST(Node root) {
if (root == null) {
return true;
}
int preData = Integer.MIN_VALUE;
Stack<Node> stack = new Stack<>();
while (root != null || !stack.empty()) {
if (root != null) {
stack.push(root);
root = root.left;
} else {
Node node = stack.pop();
if (node.data < preData) {
return false;
} else {
preData = node.data;
}
root = node.right;
}
}
return true;
}
public static void main(String[] args) {
Node root = new Node(6);
root.left = new Node(3);
root.left.left = new Node(1);
root.left.right = new Node(4);
root.right = new Node(8);
root.right.left = new Node(9);
root.right.right = new Node(10);
System.out.println(isBST(root)); //false
}
}
复制代码
根据彻底二叉树的定义,若是二叉树上某个结点有右孩子无左孩子则必定不是彻底二叉树;不然若是二叉树上某个结点有左孩子而没有右孩子,那么该结点所在的那一层上,该结点右侧的全部结点应该是叶子结点,不然不是彻底二叉树。
import java.util.LinkedList;
import java.util.Queue;
/** * 判断是否为彻底二叉树 */
public class IsCompleteBTree {
public static class Node {
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
}
public static boolean isCompleteBTree(Node root) {
if (root == null) {
return true;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while (!queue.isEmpty()) {
Node node = queue.poll();
//左空右不空
if (node.left == null && node.right != null) {
return false;
}
//若是开启了叶子结点阶段,结点不能有左右孩子
if (leaf &&
(node.left != null || node.right != null)) {
return false;
}
//将下一层要遍历的加入到队列中
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
} else {
//左右均为空,或左不空右空。该结点同层的右侧结点均为叶子结点,开启叶子结点阶段
leaf = true;
}
}
return true;
}
public static void main(String[] args) {
Node root = new Node(1);
root.left = new Node(2);
root.right = new Node(3);
root.left.right = new Node(4);
System.out.println(isCompleteBTree(root));//false
}
}
复制代码
若是咱们遍历二叉树的每一个结点来计算结点个数,那么时间复杂度将是O(N^2)
,咱们能够利用满二叉树的结点个数为2^h-1
(h为树的层数)来加速这个过程。
首先彻底二叉树,若是其左子树的最左结点在树的最后一层,那么其右子树确定是满二叉树,且高度为h-1
;不然其左子树确定是满二叉树,且高度为h-2
。也就是说,对于一个彻底二叉树结点个数的求解,咱们能够分解求解过程:1个根结点+ 一棵满二叉树(高度为h-1或者h-2)+ 一棵彻底二叉树(高度为h-1)。前二者的结点数是可求的(1+2^level -1=2^level),后者就又成了求一棵彻底二叉树结点数的问题了,可使用递归。
/** * 求一棵彻底二叉树的节点个数 */
public class CBTNodesNum {
public static class Node {
int data;
Node left;
Node right;
public Node(int data) {
super();
this.data = data;
}
}
// 获取彻底二叉树的高度
public static int getLevelOfCBT(Node root) {
if (root == null)
return 0;
int level = 0;
while (root != null) {
level++;
root = root.left;
}
return level;
}
public static int getNodesNum(Node node) {
//base case
if (node == null)
return 0;
int level = getLevelOfCBT(node);
if (getLevelOfCBT(node.right) == level - 1) {
// 左子树满,且高度为 level-1;收集左子树节点数2^(level-1)-1和头节点,对右子树重复此过程
int leftNodesAndRoot = 1 << (level - 1);
return getNodesNum(node.right) + leftNodesAndRoot;
} else {
// 右子树满,且高度为 level-2;收集右子树节点数2^(level-2)-1和头节点1,对左子树重复此过程
int rightNodesAndRoot = 1 << (level - 2);
return getNodesNum(node.left) + rightNodesAndRoot;
}
}
public static void main(String[] args) {
Node root = new Node(1);
root.left = new Node(2);
root.right = new Node(3);
root.left.left = new Node(4);
root.left.right = new Node(5);
root.right.left = new Node(6);
root.right.right = new Node(7);
System.out.println(getNodesNum(root));
}
}
复制代码
并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操做:
首先并查集自己是一个结构,咱们在构造它的时候须要将全部要操做的数据扔进去,初始时每一个数据自成一个结点,且每一个结点都有一个父指针(初始时指向本身)。
初始时并查集中的每一个结点都算是一个子集,咱们能够对任意两个元素进行合并操做。值得注意的是,union(nodeA,nodeB)
并非将结点nodeA
和nodeB
合并成一个集合,而是将nodeA
所在的集合和nodeB
所在的集合合并成一个新的子集:
那么合并两个集合的逻辑是什么呢?首先要介绍一下表明结点这个概念:找一结点所在集合的表明结点就是找这个集合中父指针指向本身的结点(并查集初始化时,每一个结点都是各自集合的表明结点)。那么合并两个集合就是将结点个数较少的那个集合的表明结点的父指针指向另外一个集合的表明结点:
还有一个find
操做:查找两个结点是否所属同一个集合。咱们只需判断两个结点所在集合的表明结点是不是同一个就能够了:
代码示例:
import java.util.*;
public class UnionFindSet{
public static class Node{
//whatever you like to store int , char , String ..etc
}
private Map<Node,Node> fatherMap;
private Map<Node,Integer> nodesNumMap;
//give me the all nodes need to save into the UnionFindSet
public UnionFindSet(List<Node> nodes){
fatherMap = new HashMap();
nodesNumMap = new HashMap();
for(Node node : nodes){
fatherMap.put(node,node);
nodesNumMap.put(node,1);
}
}
public void union(Node a,Node b){
if(a == null || b == null){
return;
}
Node rootOfA = getRoot(a);
Node rootOfB = getRoot(b);
if(rootOfA != rootOfB){
int numOfA = nodesNumMap.get(rootOfA);
int numOfB = nodesNumMap.get(rootOfB);
if(numOfA >= numOfB){
fatherMap.put(rootOfB , rootOfA);
nodesNumMap.put(rootOfA, numOfA + numOfB);
}else{
fatherMap.put(rootOfA , rootOfB);
nodesNumMap.put(rootOfB, numOfA + numOfB);
}
}
}
public boolean find(Node a,Node b){
if(a == null || b == null){
return false;
}
Node rootOfA = getRoot(a);
Node rootOfB = getRoot(b);
return rootOfA == rootOfB ? true : false;
}
public Node getRoot(Node node){
if(node == null){
return null;
}
Node father = fatherMap.get(node);
if(father != node){
father = fatherMap.get(father);
}
fatherMap.put(node, father);
return father;
}
public static void main(String[] args){
Node a = new Node();
Node b = new Node();
Node c = new Node();
Node d = new Node();
Node e = new Node();
Node f = new Node();
Node[] nodes = {a,b,c,d,e,f};
UnionFindSet set = new UnionFindSet(Arrays.asList(nodes));
set.union(a, b);
set.union(c, d);
set.union(b, e);
set.union(a, c);
System.out.println(set.find(d,e));
}
}
复制代码
你会发现union
和find
的过程当中都会有找一个结点所在集合的表明结点这个过程,因此我把它单独抽出来成一个getRoot
,并且利用递归作了一个优化:找一个结点所在集合的表明结点时,会不停地向上找父指针指向本身的结点,最后在递归回退时将沿途路过的结点的父指针改成直接指向表明结点:
诚然,这样作是为了提升下一次查找的效率。
并查集结构自己其实很简单,可是其应用却很难。这里以岛问题作引子,当矩阵至关大的时候,用单核CPU去跑这个遍历和感染效率是很低的,可能会使用并行计算框架来完成岛数量的统计。也就是说矩阵可能被分割成几个部分,逐个统计,最后在汇总。那么问题来了:
上面这个矩阵的岛数量是1;但若是从中间竖着切开,那么左边的岛数量是1,右边的岛数量是2,总数是3。如何处理切割后,相邻子矩阵之间的边界处的1相邻致使的重复统计呢?其实利用并查集的特性就很容易解决这个问题:
首先将切割边界处的数据封装成结点加入到并查集中并合并同一个岛上的结点,在分析边界时,查边界两边的1是否在同一个集合,若是不在那就union
这两个结点,并将总的岛数量减1;不然就跳过此行继续分析下一行边界上的两个点。
给定一个字符串类型的数组strs,找到一种拼接方式,使得把全部字符串拼起来以后造成的字符串具备最低的字典序。
此题不少人的想法是把数组按照字典序排序,而后从头至尾链接,造成的字符串就是全部拼接结果中字典序最小的那个。但这很容易证实是错的,好比
[ba,b]
的排序结果是[b,ba]
,拼接结果是bba
,但bab
的字典序更小。正确的策略是,将有序字符串数组从头至尾两两拼接时,应取两两拼接的拼接结果中字典序较小的那个。证实以下
若是令.
表明拼接符号,那么这里的命题是若是str1.str2 < str2.str2
且str2.str3 < str3.str2
,那么必定有str1.str3 < str3.str1
。这可使用数学概括法来证实。若是将a~z
对应到0~25
,比较两个字符串的字典序的过程,其实就比较两个26进制数大小的过程。str1.str2
拼接的过程能够看作两个26进制数拼接的过程,若将两字符串解析成数字int1
和int2
,那么拼接就对应int1 * 26^(str2的长度) + int2
,那么证实过程就变成了两个整数不等式递推另外一个不等式了。
一块金条切成两半,是须要花费和长度数值同样的铜板的。好比长度为20的 金条,无论切成长度多大的两半,都要花费20个铜板。一群人想整分整块金 条,怎么分最省铜板?
例如,给定数组{10,20,30},表明一共三我的,整块金条长度为10+20+30=60. 金条要分红10,20,30三个部分。 若是, 先把长度60的金条分红10和50,花费60 再把长度50的金条分红20和30,花费50 一共花费110铜板。可是若是, 先把长度60的金条分红30和30,花费60 再把长度30金条分红10和20,花费30 一共花费90铜板。
输入一个数组,返回分割的最小代价。
贪心策略,将给定的数组中的元素扔进小根堆,每次从小根堆中前后弹出两个元素(如10和20),这两个元素的和(如30)就是某次分割获得这两个元素的花费,再将这个和扔进小根堆。直到小根堆中只有一个元素为止。(好比扔进30以后,弹出30、30,这次花费为30+30=60,再扔进60,堆中只有一个60了,结束,总花费30+60-=90)
public stzuoatic int lessMoney(int arr[]){
if (arr == null || arr.length == 0) {
return 0;
}
//PriorityQueue是Java语言对堆结构的一个实现,默认将按天然顺序的最小元素放在堆顶
PriorityQueue<Integer> minHeap = new PriorityQueue();
for (int i : arr) {
minHeap.add(i);
}
int res = 0;
int curCost = 0;
while (minHeap.size() > 1) {
curCost = minHeap.poll() + minHeap.poll();
res += curCost;
minHeap.add(curCost);
}
return res;
}
public static void main(String[] args) {
int arr[] = {10, 20, 30};
System.out.println(lessMoney(arr));
}
复制代码
输入: 参数1:正数数组costs;参数2:正数数组profits;参数3:正数k;参数4:正数m。costs[i]表示i号项目的花费(成本),profits[i]表示i号项目作完后在扣除花费以后还能挣到的钱(利润),k表示你不能并行,只能串行的最多作k个项目 m表示你初始的资金。
说明:你每作完一个项目,立刻得到的收益,能够支持你去作下一个项目。
输出: 你最后得到的最大钱数。
贪心策略:借助两个堆,一个是存放各个项目花费的小根堆、另外一个是存放各个项目利润的大根堆。首先将全部项目放入小根堆而大根堆为空,对于手头上现有的资金(本金),将能作的项目(成本低于现有资金)从小根堆依次弹出并放入到大根堆,再弹出大根堆堆顶项目来完成,完成后根据利润更新本金。本金更新后,再将小根堆中能作的项目弹出加入到大根堆中,再弹出大根堆中的堆顶项目来作,重复此操做,直到某次本金更新和两个堆更新后大根堆无项目可作或者完成的项目个数已达k个为止。
import java.util.Comparator;
import java.util.PriorityQueue;
public class IPO {
public class Project{
int cost;
int profit;
public Project(int cost, int profit) {
this.cost = cost;
this.profit = profit;
}
}
public class MinCostHeap implements Comparator<Project> {
@Override
public int compare(Project p1, Project p2) {
return p1.cost-p2.cost; //升序,由此构造的堆将把花费最小项目的放到堆顶
}
}
public class MaxProfitHeap implements Comparator<Project> {
@Override
public int compare(Project p1, Project p2) {
return p2.profit-p1.profit;
}
}
public int findMaximizedCapital(int costs[], int profits[], int k, int m) {
int res = 0;
PriorityQueue<Project> minCostHeap = new PriorityQueue<>(new MinCostHeap());
PriorityQueue<Project> maxProfitHeap = new PriorityQueue<>(new MaxProfitHeap());
for (int i = 0; i < costs.length; i++) {
Project project = new Project(costs[i], profits[i]);
minCostHeap.add(project);
}
for (int i = 0; i < k; i++) {
//unlock project
while (minCostHeap.peek().cost < m) {
maxProfitHeap.add(minCostHeap.poll());
}
if (maxProfitHeap.isEmpty()) {
return m;
}
m += maxProfitHeap.poll().profit;
}
return m;
}
}
复制代码
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每个项目开始的时间和结束的时间(给你一个数组,里面 是一个个具体的项目),你来安排宣讲的日程,要求会议室进行 的宣讲的场次最多。返回这个最多的宣讲场次。
贪心策略:
一、开始时间最先的项目先安排。反例:开始时间最先,但持续时间占了一成天,其余项目没法安排。
二、持续时间最短的项目先安排。反例:这样安排会致使结束时间在此期间和开始时间在此期间的全部项目不能安排。
三、最优策略:最早结束的项目先安排。
import java.util.Arrays;
import java.util.Comparator;
public class Schedule {
public class Project {
int start;
int end;
}
public class MostEarlyEndComparator implements Comparator<Project> {
@Override
public int compare(Project p1, Project p2) {
return p1.end-p2.end;
}
}
public int solution(Project projects[],int currentTime) {
//sort by the end time
Arrays.sort(projects, new MostEarlyEndComparator());
int res = 0;
for (int i = 0; i < projects.length; i++) {
if (currentTime <= projects[i].start) {
res++;
currentTime = projects[i].end;
}
}
return res;
}
}
复制代码
经验:贪心策略相关的问题,累积经验就好,没必要花费大量精力去证实。解题的时候要么找类似点,要么脑补策略而后用对数器、测试用例去证。
暴力递归:
动态规划:
P和NP
P指的是我明确地知道怎么算,计算的流程很清楚;而NP问题指的是我不知道怎么算,但我知道怎么尝试(暴力递归)。
咱们知道n!
的定义,能够根据定义直接求解:
int getFactorial_1(int n){
int res=1;
for(int i = 1 ; i <= n ; n++){
res*=i;
}
return res;
}
复制代码
但咱们能够这样想,若是知道(n-1)!
,那经过(n-1)! * n
不就得出n!
了吗?因而咱们就有了以下的尝试:
int getFactorial_2(int n){
if(n=1)
return 1;
return getFactorial_2(n-1) * n;
}
复制代码
n!
的状态依赖(n-1)!
,(n-1)!
依赖(n-2)!
,就这样依赖下去,直到n=1
这个突破口,而后回溯,你会发现整个过程就回到了1 * 2 * 3 * …… * (n-1) * n
的计算过程。
该问题最基础的一个模型就是,一个竹竿上放了2个圆盘,须要先将最上面的那个移到辅助竹竿上,而后将最底下的圆盘移到目标竹竿,最后把辅助竹竿上的圆盘移回目标竹竿。
public class Hanoi {
public static void process(String source,String target,String auxiliary,int n){
if (n == 1) {
System.out.println("move 1 disk from " + source + " to " + target);
return;
}
//尝试把前n-1个圆盘暂时放到辅助竹竿->子问题
process(source, auxiliary, target, n - 1);
//将底下最大的圆盘移到目标竹竿
System.out.println("move 1 disk from "+source+" to "+target);
//再尝试将辅助竹竿上的圆盘移回到目标竹竿->子问题
process(auxiliary,target,source,n-1);
}
public static void main(String[] args) {
process("Left", "Right", "Help", 3);
}
}
复制代码
根据Master公式计算得T(N) = T(N-1)+1+T(N-1)
,时间复杂度为O(2^N)
字符串的子序列和子串有着不一样的定义。子串指串中相邻的任意个字符组成的串,而子序列能够是串中任意个不一样字符组成的串。
尝试:开始时,令子序列为空串,扔给递归方法。首先来到字符串的第一个字符上,这时会有两个决策:将这个字符加到子序列和不加到子序列。这两个决策会产生两个不一样的子序列,将这两个子序列做为这一级收集的信息扔给子过程,子过程来到字符串的第二个字符上,对上级传来的子序列又有两个决策,……这样最终能将全部子序列组合穷举出来:
/** * 打印字符串的全部子序列-递归方式 * @param str 目标字符串 * @param index 当前子过程来到了哪一个字符的决策上(要仍是不要) * @param res 上级扔给本级的子序列 */
public static void printAllSubSequences(String str,int index,String res) {
//base case : 当本级子过程来到的位置到达串末尾,则直接打印
if(index == str.length()) {
System.out.println(res);
return;
}
//决策是否要index位置上的字符
printAllSubSequences(str, index+1, res+str.charAt(index));
printAllSubSequences(str, index+1, res);
}
public static void main(String[] args) {
printAllSubSequences("abc", 0, "");
}
复制代码
/** * 本级任务:将index以后(包括index)位置上的字符和index上的字符交换,将产生的全部结果扔给下一级 * @param str * @param index */
public static void printAllPermutations(char[] chs,int index) {
//base case
if(index == chs.length-1) {
System.out.println(chs);
return;
}
for (int j = index; j < chs.length; j++) {
swap(chs,index,j);
printAllPermutations(chs, index+1);
}
}
public static void swap(char[] chs,int i,int j) {
char temp = chs[i];
chs[i] = chs[j];
chs[j] = temp;
}
public static void main(String[] args) {
printAllPermutations("abc".toCharArray(), 0);
}
复制代码
母牛每一年生一只母牛,新出生的母牛成长三年后也能每一年生一只母牛,假设不会死。求N年后,母牛的数量。
那么求第n年母牛的数量,按照此公式顺序计算便可,但这是O(N)
的时间复杂度,存在O(logN)
的算法(放到进阶篇中讨论)。
为何要改动态规划?有什么意义?
动态规划由暴力递归而来,是对暴力递归中的重复计算的一个优化,策略是空间换时间。
给你一个二维数组,二维数组中的每一个数都是正数,要求从左上角走到右下角,每一步只能向右或者向下。沿途通过的数字要累加起来。返回最小的路径和。
/** * 从矩阵matrix的(i,j)位置走到右下角元素,返回最小沿途元素和。每一个位置只能向右或向下 * * @param matrix * @param i * @param j * @return 最小路径和 */
public static int minPathSum(int matrix[][], int i, int j) {
// 若是(i,j)就是右下角的元素
if (i == matrix.length - 1 && j == matrix[0].length - 1) {
return matrix[i][j];
}
// 若是(i,j)在右边界上,只能向下走
if (j == matrix[0].length - 1) {
return matrix[i][j] + minPathSum(matrix, i + 1, j);
}
// 若是(i,j)在下边界上,只能向右走
if (i == matrix.length - 1) {
return matrix[i][j] + minPathSum(matrix, i, j + 1);
}
// 不是上述三种状况,那么(i,j)就有向下和向右两种决策,取决策结果最小的那个
int left = minPathSum(matrix, i, j + 1);
int down = minPathSum(matrix, i + 1, j);
return matrix[i][j] + Math.min(left,down );
}
public static void main(String[] args) {
int matrix[][] = {
{ 9, 1, 0, 1 },
{ 4, 8, 1, 0 },
{ 1, 4, 2, 3 }
};
System.out.println(minPathSum(matrix, 0, 0)); //14
}
复制代码
上述暴力递归的缺陷在于有些子过程是重复的。好比minPathSum(matrix,0,1)
和minPathSum(matrix,1,0)
都会依赖子过程minPathSum(matrix,1,1)
的状态(执行结果),那么在计算minPathSum(matrix,0,0)
时势必会致使minPathSum(matrix,1,1)
的重复计算。那咱们可否经过对子过程计算结果进行缓存,在再次须要时直接使用,从而实现对整个过程的一个优化呢。
由暴力递归改动态规划的核心就是将每一个子过程的计算结果进行一个记录,从而达到空间换时间的目的。那么minPath(int matrix[][],int i,int j)
中变量i
和j
的不一样取值将致使i*j
种结果,咱们将这些结果保存在一个i*j
的表中,不就达到动态规划的目的了吗?
观察上述代码可知,右下角、右边界、下边界这些位置上的元素是不须要尝试的(只有一种走法,不存在决策问题),所以咱们能够直接将这些位置上的结果先算出来:
而其它位置上的元素的走法则依赖右方相邻位置(i,j+1)走到右下角的最小路径和和下方相邻位置(i+1,j)走到右下角的最小路径和的大小比较,基于此来作一个向右走仍是向左走的决策。但因为右边界、下边界位置上的结果咱们已经计算出来了,所以对于其它位置上的结果也就不难肯定了:
咱们从base case
开始,倒着推出了全部子过程的计算结果,而且没有重复计算。最后minPathSum(matrix,0,0)
也迎刃而解了。
这就是动态规划,它不是凭空想出来的。首先咱们尝试着解决这个问题,写出了暴力递归。再由暴力递归中的变量的变化范围创建一张对应的结果记录表,以
base case
做为突破口肯定可以直接肯定的结果,最后解决广泛状况对应的结果。
给你一个数组arr,和一个整数aim。若是能够任意选择arr中的数字,能不能累加获得aim,返回true或者false。
此题的思路跟求解一个字符串的全部子序列的思路一致,穷举出数组中全部任意个数相加的不一样结果。
/** * 选择任意个arr中的元素相加是否能获得aim * * @param arr * @param aim * @param sum 上级扔给个人结果 * @param i 决策来到了下标为i的元素上 * @return */
public static boolean isSum(int arr[], int aim, int sum,int i) {
//决策完毕
if (i == arr.length) {
return sum == aim;
}
//决策来到了arr[i]:加上arr[i]或不加上。将结果扔给下一级
return isSum(arr, aim, sum + arr[i], i + 1) || isSum(arr, aim, sum, i + 1);
}
public static void main(String[] args) {
int arr[] = {1, 2, 3};
System.out.println(isSum(arr, 5, 0, 0));
System.out.println(isSum(arr, 6, 0, 0));
System.out.println(isSum(arr, 7, 0, 0));
}
复制代码
首先看递归函数的参数,找出变量。这里arr
和aim
是固定不变的,可变的只有sum
和i
。
对应变量的变化范围创建一张表保存不一样子过程的结果,这里i
的变化范围是0~arr.length-1
即0~2
,而sum
的变化范围是0~数组元素总和
,即0~6
。所以须要建一张3*7
的表。
从base case
入手,计算可直接计算的子过程,以isSum(5,0,0)
的计算为例,其子过程当中“是否+3”的决策以后的结果是能够肯定的:
按照递归函数中base case
下的尝试过程,推出其它子过程的计算结果,这里以i=1,sum=1
的推导为例:
看过上述例题以后你会发现只要你可以写出尝试版本,那么改动态规划是高度套路的。可是不是全部的暴力递归都可以改动态规划呢?不是的,好比汉诺塔问题和N皇后问题,他们的每一步递归都是必须的,没有多余。这就涉及到了递归的有后效性和无后效性。
无后效性是指对于递归中的某个子过程,其上级的决策对该级的后续决策没有任何影响。好比最小路径和问题中如下面的矩阵为例:
对于(1,1)位置上的8,不管是经过9->1->8
仍是9->4->8
来到这个8
上的,这个8
到右下角的最小路径和的计算过程不会改变。这就是无后效性。
只有无后效性的暴力递归才能改动态规划。
百科:散列函数(英语:Hash function)又称散列算法、哈希函数,是一种从任何一种数据中建立小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将输入域中的数据打乱混合,从新建立一个叫作散列值(hash values,hash codes,hash sums,或hashes)的指纹。
哈希函数的输入域能够是很是大的范围,好比,任意一个字符串,可是输出域是固定的范围(必定位数的bit),假设为S,并具备以下性质:
前3点性质是哈希函数的基础,第4点是评价一个哈希函数优劣的关键,不一样输入值所获得的全部返回值越均匀地分布在S上,哈希函数越优秀,而且这种均匀分布与输入值出现的规律无关。好比,“aaa1”、“aaa2”、“aaa3”三个输入值比较相似,但通过优秀的哈希函数计算后获得的结果应该相差很是大。
参考文献:哈希函数的介绍
好比使用MD5对“test”和“test1”两个字符串哈希的结果以下(哈希结果为128个bit,数据范围为0~(2^128)-1
,一般转换为32个16进制数显示):
test 098f6bcd4621d373cade4e832627b4f6
test1 5a105e8b9d40e1329780d62ea2265d8a
复制代码
百科:散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它经过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称作散列函数,存放记录的数组称作散列表。
哈希表初始会有一个大小,好比16,表中每一个元素均可以经过数组下标(0~15)访问。每一个元素能够看作一个桶,当要往表里放数据时,将要存放的数据的键值经过哈希函数计算出的哈希值模上16,结果正好对应0~15,将这条数据放入对应下标的桶中。
那么当数据量超过16时,势必会存在哈希冲突(两条数据经哈希计算后放入同一个桶中),这时的解决方案就是将后一条入桶的数据做为后继结点链入到桶中已有的数据以后,如此,每一个桶中存放的就是一个链表。那么这就是哈希表的经典结构:
当数据量较少时,哈希表的增删改查操做的时间复杂度都是O(N)
的。由于根据一个键值就能定位一个桶,即便存在哈希冲突(桶里不仅一条数据),但只要哈希函数优秀,数据量几乎均分在每一个桶上(这样不多有哈希冲突,即便有,一个桶里也只会有不多的几条数据),那就在遍历一下桶里的链表比较键值进一步定位数据便可(反正链表很短)。
若是哈希表大小为16,对于样本规模N(要存储的数据数量)来讲,若是N较小,那么根据哈希函数的散列特性,每一个桶会均分这N条数据,这样落到每一个桶的数据量也较小,不会影响哈希表的存取效率(这是由桶的链表长度决定的,由于存数据要往链表尾追加首先就要遍历获得尾结点,取数据要遍历链表比较键值);但若是N较大,那么每一个桶里都有N/16
条数据,存取效率就变成O(N)
了。所以哈希表哈须要一个扩容机制,当表中某个桶的数据量超过一个阀值时(O(1)
到O(N)
的转变,这须要一个算法来权衡),须要将哈希表扩容(通常是成倍的)。
扩容步骤是,建立一个新的较大的哈希表(假如大小为m),将原哈希表中的数据取出,将键值的哈希值模上m,放入新表对应的桶中,这个过程也叫rehash
。
如此的话,那么原来的O(N)
就变成了O(log(m/16,N))
,好比扩容成5倍那就是O(log(5,N))
(以5为底,N的对数)。当这个底数较大的时候就会将N的对数压得很是低而和O(1)
很是接近了,而且实际工程中基本是当成O(1)
来用的。
你也许会说rehash
很费时,会致使哈希表性能下降,这一点是能够侧面避免的。好比扩容时将倍数提升一些,那么rehash
的次数就会不多,平衡到整个哈希表的使用来看,影响就甚微了。或者能够进行离线扩容,当须要扩容时,原哈希表仍是供用户使用,在另外的内存中执行rehash
,完成以后再将新表替换原表,这样的话对于用户来讲,他是感受不到rehash
带来的麻烦的。
在Java
中,哈希表的实现是每一个桶中放的是一棵红黑树而非链表,由于红黑树的查找效率很高,也是对哈希冲突带来的性能问题的一个优化。
不安全网页的黑名单包含100亿个黑名单网页,每一个网页的URL最多占用64B。如今想要实现一种网页过滤系统,能够根据网页的URL判断该网页是否在黑名单上,请设计该系统。
要求以下:
若是将这100亿个URL经过数据库或哈希表保存起来,就能够对每条URL进行查询,可是每一个URL有64B,数量是100亿个,因此至少须要640GB的空间,不知足要求2。
若是面试者遇到网页黑名单系统、垃圾邮件过滤系统,爬虫的网页判重系统等题目,又看到系统容忍必定程度的失误率,可是对空间要求比较严格,那么极可能是面试官但愿面试者具有布隆过滤器的知识。一个布隆过滤器精确地表明一个集合,并能够精确判断一个元素是否在集合中。注意,只是精确表明和精确判断,到底有多精确呢?则彻底在于你具体的设计,但想作到彻底正确是不可能的。布隆过滤器的优点就在于使用不多的空间就能够将准确率作到很高的程度。该结构由
Burton Howard Bloom
于1970年提出。
那么什么是布隆过滤器呢?
假设有一个长度为m
的bit类型的数组,即数组的每一个位置只占一个bit,若是咱们所知,每个bit只有0和1两种状态,如图所示:
再假设一共有k
个哈希函数,这些函数的输出域S都大于或等于m,而且这些哈希函数都足够优秀且彼此之间相互独立(将一个哈希函数的计算结果乘以6除以7得出的新哈希函数和原函数就是相互独立的)。那么对同一个输入对象(假设是一个字符串,记为URL),通过k个哈希函数算出来的结果也是独立的。可能相同,也可能不一样,但彼此独立。对算出来的每个结果都对m取余(%m),而后在bit array 上把相应位置设置为1(咱们形象的称为涂黑)。如图所示
咱们把bit类型的数组记为bitMap
。至此,一个输入对象对bitMap
的影响过程就结束了,也就是bitMap
的一些位置会被涂黑。接下来按照该方法,处理全部的输入对象(黑名单中的100亿个URL)。每一个对象均可能把bitMap
中的一些白位置涂黑,也可能遇到已经涂黑的位置,遇到已经涂黑的位置让其继续为黑便可。处理完全部的输入对象后,可能bitMap
中已经有至关多的位置被涂黑。至此,一个布隆过滤器生成完毕,这个布隆过滤器表明以前全部输入对象组成的集合。
那么在检查阶段时,如何检查一个对象是不是以前的某一个输入对象呢(判断一个URL是不是黑名单中的URL)?假设一个对象为a,想检查它是不是以前的输入对象,就把a经过k个哈希函数算出k个值,而后把k个值都取余(%m),就获得在[0,m-1]范围伤的k个值。接下来在bitMap
上看这些位置是否是都为黑。若是有一个不为黑,说明a必定再也不这个集合里。若是都为黑,说明a在这个集合里,但可能误判。
再解释具体一点,若是a的确是输入对象 ,那么在生成布隆过滤器时,bitMap
中相应的k个位置必定已经涂黑了,因此在检查阶段,a必定不会被漏过,这个不会产生误判。会产生误判的是,a明明不是输入对象,但若是在生成布隆过滤器的阶段由于输入对象过多,而bitMap
太小,则会致使bitMap
绝大多数的位置都已经变黑。那么在检查a时,可能a对应的k个位置都是黑的,从而错误地认为a是输入对象(便是黑名单中的URL)。通俗地说,布隆过滤器的失误类型是“宁肯错杀三千,毫不放过一个”。
布隆过滤器到底该怎么生成呢?只需记住下列三个公式便可:
m = - (n*lnp)/(ln2*ln2)
,计算结果向上取整(这道题m=19.19n,向上取整为20n,即须要2000亿个bit,也就是25GB)k = ln2 * m/n = 0.7 * m/n
(这道题k = 0.7 * 20n/n = 14
)p = (1 - e^(-nk/m))^k
工程师常使用服务器集群来设计和实现数据缓存,如下是常见的策略:
key%N
的值,这个值就是该数据所属的机器编号,不管是添加、删除仍是查询操做,都只在这台机器上进行。请分析这种缓存策略可能带来的问题,并提出改进的方案。
题目中描述的缓存从策略的潜在问题是,若是增长或删除机器时(N变化)代价会很高,全部的数据都不得不根据id从新计算一遍哈希值,并将哈希值对新的机器数进行取模啊哦作。而后进行大规模的数据迁移。
为了解决这些问题,下面介绍一下一致性哈希算法,这时一种很好的数据缓存设计方案。咱们假设数据的id经过哈希函数转换成的哈希值范围是2^32,也就是0~(2^32)-1的数字空间中。如今咱们能够将这些数字头尾相连,想象成一个闭合的环形,那么一个数据id在计算出哈希值以后认为对应到环中的一个位置上,如图所示
接下来想象有三台机器也处在这样一个环中,这三台机器在环中的位置根据机器id(主机名或者主机IP,是主机惟一的就行)设计算出的哈希值对2^32取模对应到环上。那么一条数据如何肯定归属哪台机器呢?咱们能够在该数据对应环上的位置顺时针寻找离该位置最近的机器,将数据归属于该机器上:
这样的话,若是删除machine2
节点,则只需将machine2
上的数据迁移到machine3
上便可,而没必要大动干戈迁移全部数据。当添加节点的时候,也只需将新增节点到逆时针方向新增节点前一个节点这之间的数据迁移给新增节点便可。
但这时仍是存在以下两个问题:
机器较少时,经过机器id哈希将机器对应到环上以后,几个机器可能没有均分环
那么这样会致使负载不均。
增长机器时,可能会打破现有的平衡:
为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一台机器经过不一样的哈希函数计算出多个哈希值,对多个位置都放置一个服务节点,称为虚拟节点。具体作法:好比对于machine1
的IP192.168.25.132
(或机器名),计算出192.168.25.132-1
、192.168.25.132-2
、192.168.25.132-3
、192.168.25.132-4
的哈希值,而后对应到环上,其余的机器也是如此,这样的话节点数就变多了,根据哈希函数的性质,平衡性天然会变好:
此时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,好比上图的查找表。当某一条数据计算出归属于m2-1
时再根据查找表的跳转,数据将最终归属于实际的m1节点。
基于一致性哈希的原理有不少种具体的实现,包括Chord算法、KAD算法等,有兴趣的话能够进一步学习。
设计一种结构,在该结构中有以下三个功能:
要求:insert、delete和getRandom方法的时间复杂度都是O(1)
思路:使用两个哈希表和一个变量
size
,一个表存放某key
的标号,另外一个表根据根据标号取某个key
。size
用来记录结构中的数据量。加入key
时,将size
做为该key
的标号加入到两表中;删除key
时,将标号最大的key
替换它并将size--
;随机取key
时,将size
范围内的随机数做为标号取key
。
import java.util.HashMap;
public class RandomPool {
public int size;
public HashMap<Object, Integer> keySignMap;
public HashMap<Integer, Object> signKeyMap;
public RandomPool() {
this.size = 0;
this.keySignMap = new HashMap<>();
this.signKeyMap = new HashMap<>();
}
public void insert(Object key) {
//不重复添加
if (keySignMap.containsKey(key)) {
return;
}
keySignMap.put(key, size);
signKeyMap.put(size, key);
size++;
}
public void delete(Object key) {
if (keySignMap.containsKey(key)) {
Object lastKey = signKeyMap.get(--size);
int deleteSign = keySignMap.get(key);
keySignMap.put(lastKey, deleteSign);
signKeyMap.put(deleteSign, lastKey);
keySignMap.remove(key);
signKeyMap.remove(lastKey);
}
}
public Object getRandom() {
if (size > 0) {
return signKeyMap.get((int) (Math.random() * size));
}
return null;
}
}
复制代码
有时咱们对编写的算法进行测试时,会采用本身编造几个简单数据进行测试。然而别人测试时可能会将大数量级的数据输入进而测试算法的准确性和健壮性,若是这时出错,面对庞大的数据量咱们将无从查起(是在操做哪个数据时出了错,算法没有如期起做用)。固然咱们不可能对这样一个大数据进行断点调试,去一步一步的分析错误点在哪。这时 对数器 就粉墨登场了,对数器 就是经过随机制造出几乎全部可能的简短样本做为算法的输入样本对算法进行测试,这样大量不一样的样本从大几率上保证了算法的准确性,当有样本测试未经过时又能打印该简短样本对错误缘由进行分析。
对数器使用案例——对自写的插入排序进行测试:
void swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
//1.有一个自写的算法,但不知其健壮性(是否会有特殊状况使程序异常中断甚至崩溃)和正确性
void insertionSort(int arr[], int length){
if(arr==NULL || length<=1){
return;
}
for (int i = 1; i < length; ++i) {
for (int j = i - 1; j >= 0 || arr[j] <= arr[j + 1]; j--) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}
//二、实现一个功能相同、绝对正确但复杂度很差的算法(这里摘取你们熟知的冒泡排序)
void bubbleSort(int arr[], int length) {
for (int i = length-1; i > 0; i--) {
for (int j = 0; j < i; ++j) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}
//三、实现一个可以产生随机简短样本的方法
void generateSample(int arr[], int length){
for (int i = 0; i < length; ++i) {
arr[i] = rand() % 100-rand()%100;//控制元素在-100~100之间,考虑到零正负三种状况
}
}
//四、实现一个比对测试算法和正确算法运算结果的方法
bool isEqual(int arr1[],int arr2[],int length) {
if (arr1 != NULL && arr2 != NULL) {
for (int i = 0; i < length; ++i) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
return false;
}
void travels(int arr[], int length){
for (int i = 0; i < length; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
}
void copy(int source[], int target[],int length){
for (int i = 0; i < length; ++i) {
target[i] = source[i];
}
}
int main(){
srand(time(NULL));
int testTimes=10000;
//循环产生100000个样本进行测试
for (int i = 0; i < testTimes; ++i) {
int length = rand() % 10; //控制每一个样本的长度在10之内,便于出错时分析样本(由于简短)
int arr[length];
generateSample(arr, length);
//不要改变原始样本,在复制样本上改动
int arr1[length], arr2[length];
copy(arr, arr1, length);
copy(arr, arr2, length);
bubbleSort(arr1,length);
insertionSort(arr2, length);
// travels(arr, length);
// travels(arr1, length);
//五、比对两个算法,只要有一个样本没经过就终止,并打印原始样本
if (!isEqual(arr1, arr2, length)) {
printf("test fail!the sample is: ");
travels(arr, length);
return 0;
}
}
//六、测试所有经过,该算法大几率上正确
printf("nice!");
return 0;
}
复制代码
有时咱们不肯定二叉树中是否有指针连空了或者连错了,这时须要将二叉树具备层次感地打印出来,下面就提供了这样一个工具。你能够将你的头逆时针旋转90度看打印结果。v
表示该结点的头结点是左下方距离该结点最近的一个结点,^
表示该结点的头结点是左上方距离该结点最近的一个结点。
package top.zhenganwen.algorithmdemo.recursive;
public class PrintBinaryTree {
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
public static void printTree(Node head) {
System.out.println("Binary Tree:");
printInOrder(head, 0, "H", 17);
System.out.println();
}
public static void printInOrder(Node head, int height, String to, int len) {
if (head == null) {
return;
}
printInOrder(head.right, height + 1, "v", len);
String val = to + head.value + to;
int lenM = val.length();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
System.out.println(getSpace(height * len) + val);
printInOrder(head.left, height + 1, "^", len);
}
public static String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
public static void main(String[] args) {
Node head = new Node(1);
head.left = new Node(-222222222);
head.right = new Node(3);
head.left.left = new Node(Integer.MIN_VALUE);
head.right.left = new Node(55555555);
head.right.right = new Node(66);
head.left.left.right = new Node(777);
printTree(head);
head = new Node(1);
head.left = new Node(2);
head.right = new Node(3);
head.left.left = new Node(4);
head.right.left = new Node(5);
head.right.right = new Node(6);
head.left.left.right = new Node(7);
printTree(head);
head = new Node(1);
head.left = new Node(1);
head.right = new Node(1);
head.left.left = new Node(1);
head.right.left = new Node(1);
head.right.right = new Node(1);
head.left.left.right = new Node(1);
printTree(head);
}
}
复制代码
递归的实质就是系统在帮咱们压栈。首先让咱们来看一个递归求阶乘的例子:
int fun(int n){
if(n==0){
return 1;
}
return n*fun(n-1);
}
复制代码
课上老师通常告诉咱们递归就是函数本身调用本身。但这听起来很玄学。事实上,在函数执行过程当中若是调用了其余函数,那么当前函数的执行状态(执行到了第几行,有几个变量,各个变量的值是什么等等)会被保存起来压进栈(先进后出的存储结构,通常称为函数调用栈)中,转而执行子过程(调用的其余函数,固然也能够是当前函数)。若子过程当中又调用了函数,那么调用前子过程的执行状态也会被保存起来压进栈中,转而执行子过程的子过程……以此类推,直到有一个子过程没有调用函数、能顺序执行完毕时会从函数调用栈依次弹出栈顶被保存起来的未执行完的函数(恢复现场)继续执行,直到函数调用栈中的函数都执行完毕,整个递归过程结束。
例如,在main
中执行fun(3)
,其递归过程以下:
int main(){
int i = fun(3);
printf("%d",i);
return 0;
}
复制代码
不少时候咱们分析递归时都喜欢在心中模拟代码执行,去追溯、还原整个递归调用过程。但事实上没有必要这样作,由于每相邻的两个步骤执行的逻辑都是相同的,所以咱们只须要分析第一步到第二步是如何执行的以及递归的终点在哪里就能够了。
一切的递归算法均可以转化为非递归,由于咱们彻底能够本身压栈。只是说递归的写法更加简洁。在实际工程中,递归的使用是极少的,由于递归建立子函数的开销很大而且存在安全问题(stack overflow)。
包含递归的算法的时间复杂度有时很难经过算法表面分析出来, 好比 归并排序。这时Master公式就粉墨登场了,当某递归算法的时间复杂度符合T(n)=aT(n/b)+O(n^d)
形式时能够直接求出该算法的直接复杂度:
log(b,a) > d
时,时间复杂度为O(n^log(b,a))
log(b,a) = d
时,时间复杂度为O(n^d * logn)
log(b,a) < d
时,时间复杂度为O(n^d)
其中,
n
为样本规模,n/b
为子过程的样本规模(暗含子过程的样本规模必须相同,且相加之和等于总样本规模),a
为子过程的执行次数,O(n^d)
为除子过程以后的操做的时间复杂度。以归并排序为例,函数本体先对左右两半部分进行归并排序,样本规模被分为了左右各
n/2
即b=2
,左右各归并排序了一次,子过程执行次数为2
即a=2
,并入操做的时间复杂度为O(n+n)=O(n)
即d=1
,所以T(n)=2T(n/2)+O(n)
,符合log(b,a)=d=1
,所以归并排序的时间复杂度为O(n^1*logn)=O(nlogn)