面试季,覆盖70%-80%的面经基础题(java及安卓)-------数据结构与算法篇
- 数据结构
- 队列 Queue
- 什么是队列
- 队列的种类
- Java 集合框架中的队列 Queue
- Set
- 什么是 Set
- 补充:有序集合与无序集合说明
- HashSet 和 TreeSet 底层数据结构
- List
- 什么是List
- List的常见实现类
- 树
- 1. 二叉树
- 2. 堆
- 3. 二叉查找树(BST)
- 4. 红黑树
- 5. B树家族
- B树
- B+树:
- B*树:
- 图
- 队列 Queue
- 算法
- 贪心
- 背包问题
- 递归法
- 记忆化搜索
- 活动选择问题
- 背包问题
- 二叉树遍历
- 广度优先遍历(BFS)
- 深度优先遍历(DFS)
- 前序遍历
- 中序遍历
- 后序遍历
- 二叉树
- 镜像/反转二叉树
- 树的直径
- 从根节点到叶子节点路径总和等于给定目标和的路径
- 二叉树剪枝
- 两个节点的最近公共祖先
- 堆排序
- 快速排序
- 归并排序
- 选择排序
- 插入排序
- 链表
- 判断回文
- 每K个节点反转链表
- 环形链表入口节点
- 数组
- 数组的局部峰值
- TOP K问题
- 冒泡 O(n*k)
- 堆 O(n*log(k))
- 快速排序Partition O(n)
- 数组内最大顺序差值(股票问题)
- 二维数组查找
- 数组的所有子集
- 数组移动K位
- 有序数组内target第一次出现的下标
- 合并有序数组
- 有序数组中和为K的两数下标
- 数组中只有0,1,2三个元素,进行排序,要求时间复杂度为O(n)
- 栈
- 单调栈
- 栈实现队列
- 队列
- 单调队列
- 队列实现栈
- 动态规划
- 连续子数组最大和
- 最长递增子序列
- 扔鸡蛋
- 用1元 3元 5元组成n元
- 多线程交替打印ABC
- synchronized
- reentrantlock
- reentrantlock + condition
- semaphore
- 给定一个函数rand()能产生0到n-1之间的等概率随机数,问如何产生0到m-1之间等概率的随机数?
- LRU
- 手动实现 O(1)
- 双向链表 O(n)
- LinkedHashMap O(1)
- LFU
- 斐波那契(走台阶)
- 变态跳台阶
- 最长不重复子串长度
- 大数相加
- N对括号全部有效组合
- 最长回文子串
- 数列中消失的元素
- 缺失和重复的元素
- 贪心
数据结构
队列 Queue
什么是队列
队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。
队列的种类
- 单队列(单队列就是常见的队列, 每次添加元素时,都是添加到队尾,存在“假溢出”的问题也就是明明有位置却不能添加的情况)
- 循环队列(避免了“假溢出”的问题)
Java 集合框架中的队列 Queue
Java 集合中的 Queue 继承自 Collection 接口 ,Deque, LinkedList, PriorityQueue, BlockingQueue 等类都实现了它。 Queue 用来存放 等待处理元素 的集合,这种场景一般用于缓冲、并发访问。 除了继承 Collection 接口的一些方法,Queue 还添加了额外的 添加、删除、查询操作。
添加、删除、查询这些个操作都提供了两种形式,其中一种在操作失败时直接抛出异常,而另一种则返回一个特殊的值:
Throws exception | Returns special value | |
---|---|---|
Insert | add(e) | offer(e) |
Remove | remove() | poll() |
Examine | element() | peek() |
Set
什么是 Set
Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现类。
在判断重复元素的时候,HashSet 集合会调用 hashCode()和 equal()方法来实现;TreeSet 集合会调用compareTo方法来实现。
补充:有序集合与无序集合说明
- 有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map)
- 无序集合:集合里的元素只能遍历。(Set)
HashSet 和 TreeSet 底层数据结构
HashSet 是哈希表结构,主要利用 HashMap 的 key 来存储元素,计算插入元素的 hashCode 来获取元素在集合中的位置
具有如下特点:
- 不允许出现重复因素;
- 允许插入Null值;
- 元素无序(添加顺序和遍历顺序不一致);
- 线程不安全,若2个线程同时操作HashSet,必须通过代码实现同步;
TreeSet 是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序
具有如下特点:
- 对插入的元素进行排序,是一个有序的集合(主要与HashSet的区别);
- 允许插入Null值;
- 不允许插入重复元素;
- 线程不安全;
List
什么是List
在 List 中,用户可以精确控制列表中每个元素的插入位置,另外用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。 与 Set 不同,List 通常允许重复的元素。 另外 List 是有序集合而 Set 是无序集合。
List的常见实现类
ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。
Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。
树
1. 二叉树
完全二叉树
若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
(叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。)
满二叉树
除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
(一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。)
平衡二叉树
平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
2. 堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
3. 二叉查找树(BST)
二叉查找树的特点:
- 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 任意节点的左、右子树也分别为二叉查找树;
- 没有键值相等的节点(no duplicate nodes)。
4. 红黑树
红黑树特点:
- 每个节点非红即黑;
- 根节点总是黑色的;
- 每个叶子节点都是黑色的空节点(NIL节点);
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
- 从根到叶子的最长路径不会超过最短路径的两倍
红黑树的应用:
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。
为什么要用红黑树
简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
自平衡方法:变色、旋转
与AVL树的差异
红黑树的查询性能略微逊色于AVL树,因为其比AVL树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较,但是,红黑树在插入和删除上优于AVL树,AVL树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于AVL树为了维持平衡的开销要小得多。(红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。)
实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。
5. B树家族
B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance)
B+树的叶子节点链表结构相比于B树便于扫库,和范围检索。
B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。
比如要查 5-10之间的,B+树一把到5这个标记,再一把到10,然后串起来就行了,B树就非常麻烦。B树的好处,就是成功查询特别有利,因为树的高度总体要比B+树矮。不成功的情况下,B树也比B+树稍稍占一点点便宜。
B树的优势是当你要查找的值恰好处在一个非叶子节点时,查找到该节点就会成功并结束查询,而B+树由于非叶节点只是索引部分,这些节点中只含有其子树中的最大(或最小)关键字,当非终端节点上的关键字等于给点值时,查找并不终止,而是继续向下直到叶子节点。因此在B+树中,无论查找成功与否,都是走了一条从根到叶子节点的路径。
有很多基于频率的搜索是选用B树,越频繁query的结点越往根上走,前提是需要对query做统计,而且要对key做一些变化。
另外B树也好B+树也好,根或者上面几层因为被反复query,所以这几块基本都在内存中,不会出现读磁盘IO,一般已启动的时候,就会主动换入内存。 mysql底层存储是用B+树实现的,因为内存中B+树是没有优势的,但是一到磁盘,B+树的威力就出来了。
B*树是B+树的变体,B*树分配新结点的概率比B+树要低,空间使用率更高
B树
- 多路搜索树,每个结点存储M/2到M个关键字(M指M路),非叶子结点存储指向关键字范围的子结点的索引
- 所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中
B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
B树的特性:
关键字集合分布在整颗树中;
任何一个关键字出现且只出现在一个结点中;
搜索有可能在非叶子结点结束;
其搜索性能等价于在关键字全集内做一次二分查找;
自动层次控制;
B+树:
- 在B树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引
- B+树总是到叶子结点才命中
B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一棵B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点,也可能是一个包含两个或两个以上孩子节点的节点。
B+ 树通常用于数据库和操作系统的文件系统中。NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等文件系统都在使用B+树作为元数据索引。B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入。
B+树的特性:
所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
不可能在非叶子结点命中;
非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
更适合文件索引系统;
B*树:
- 是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针,将结点的最低利用率从1/2提高到2/3
图
图和树的最大区别在于图的下一个节点可能指向已访问过的节点。因此在使用BFS及DFS遍历时,应维护一个Set,Set中存放已被访问过的节点,在遍历时先判断节点未被访问过再遍历即可。
举例
public void BFSWithQueue(Node root) {Queue<Node> queue = new LinkedList<>();if (root != null)queue.add(root);Set<Node> visited = new HashSet<>();while (!queue.isEmpty()) {Node node = queue.poll();visited.add(node);//在这里处理遍历到的Node节点if (node.children != null) {for (Node child : node.children) {if (child != null && !visited.contains(child){queue.add(child);}}}}
}
算法
贪心
背包问题
递归法
public class KnapSack01 {/*** 解决背包问题的递归函数** @param w 物品的重量数组* @param v 物品的价值数组* @param index 当前待选择的物品索引* @param capacity 当前背包有效容量* @return 最大价值*/private static int solveKS(int index, int capacity) {//基准条件:如果索引无效或者容量不足,直接返回当前价值0if (index < 0 || capacity <= 0)return 0;//不放第index个物品所得价值int res = solveKS(index-1, capacity);//放第index个物品所得价值(前提是:第index个物品可以放得下)if (w[index] <= capacity) {res = Math.max(res, v[index] + solveKS(index-1, capacity-w[index]));}return res;}private int[] w = {2,1,3,2};private int[] v = {12,10,20,15};public static void main(String[] args){solveKS(w.length-1, Capacity);}
}
记忆化搜索
public class KnapSack02 {/*** 解决背包问题的递归函数** @param w 物品的重量数组* @param v 物品的价值数组* @param index 当前待选择的物品索引* @param capacity 当前背包有效容量* @return 最大价值*/private static int solveKS(int index, int capacity) {//基准条件:如果索引无效或者容量不足,直接返回当前价值0if (index < 0 || capacity <= 0)return 0;//如果此子问题已经求解过,则直接返回上次求解的结果if (memo[index][capacity] != 0) {return memo[index][capacity];}//不放第index个物品所得价值int res = solveKS(index-1, capacity);//放第index个物品所得价值(前提是:第index个物品可以放得下)if (w[index] <= capacity) {res = Math.max(res, v[index] + solveKS(index-1, capacity-w[index]));}//添加子问题的解,便于下次直接使用memo[index][capacity] = res;return res;}private static int[] w = {2,1,3,2};private static int[] v = {12,10,20,15};private static int[][] memo;public static void main(String[] args){int Capacity = 10;memo = new int[w.length][Capacity+1];solveKS(w.length-1, Capacity);}
}
活动选择问题
二叉树遍历
广度优先遍历(BFS)
BFS的概念,即一层一层的遍历,在使用BFS解决问题的时候最先想到的方式应该是队列(Queue,FIFO)。其主要思想是从起始点开始,将其邻近的所有顶点都加到一个队列(FIFO)中去,然后标记下这些顶点离起始顶点的距离为1。最后将起始顶点标记为已访问,今后就不会再访问。然后再从队列中取出最先进队的顶点A,也取出其周边邻近节点,加入队列末尾,最后离开这个顶点A。依次下去,直到队列为空为止。从上面描述的过程我们知道每个顶点被访问的次数最多一次(已访问的节点不会再访问)。
public void BFSWithQueue(TreeNode root) {Queue<TreeNode> queue = new LinkedList<>();if (root != null)queue.add(root);while (!queue.isEmpty()) {TreeNode treeNode = queue.poll();//在这里处理遍历到的TreeNode节点if (treeNode.left != null)queue.add(treeNode.left);if (treeNode.right != null)queue.add(treeNode.right);}
}
深度优先遍历(DFS)
使用DFS解决问题时最先想到的应该是递归和栈(Stack)。DFS是从起始顶点开始,递归访问其所有邻近节点,比如A节点是其第一个邻近节点,而B节点又是A的一个邻近节点,则DFS访问A节点后再访问B节点,如果B节点有未访问的邻近节点的话将继续访问其邻近节点,否则继续访问A的未访问邻近节点,当所有从A节点出去的路径都访问完之后,继续递归访问除A以外未被访问的邻近节点。
递归法
public void DFSWithRecursion(TreeNode root) {if (root == null) {return;}//在这里处理遍历到的TreeNode节点if (root.left != null)DFSWithRecursion(root.left);if (root.right != null)DFSWithRecursion(root.right);
}
Stack法
public void DFSWithStack(TreeNode root) {if (root == null)return;Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()) {TreeNode treeNode = stack.pop();//在这里处理遍历到的TreeNode if (treeNode.right != null)stack.push(treeNode.right);if (treeNode.left != null)stack.push(treeNode.left);}
}
前序遍历
//根左右
//递归
public void preOrder(TreeNode node) {if(node == null) {return;}System.out.print(node.val);preOrder(node.left);preOrder(node.right);
}
//非递归:栈——先入根,循环内先弹出再入左右节点,同DFS
中序遍历
//左根右
//递归
public void inOrder(TreeNode node) {if(node == null) {return;}inOrder(node.left);System.out.print(node.val);inOrder(node.right);
}
//非递归
public static void InOrder2(TreeNode root) {if(root==null)return;Stack<TreeNode> stk = new Stack<TreeNode>();TreeNode p = root;//辅助节点stk.push(p);while(stk.isEmpty() == false) {//只要你有左孩子,就将左孩子压入栈中if(p!=null && p.left!=null) {stk.push(p.left);p = p.left;}else {p = stk.pop();//弹出栈顶节点 左孩子--->根节点System.out.print(p.val);//访问if(p!=null && p.right!=null) {//如果栈点元素有右孩子的话,将有节点压入栈中stk.push(p.right);p = p.right;} else {p = null;//p=stk.pop;已经访问过p了,p设置为null}}}
}
后序遍历
//左右根
//递归
public void inOrder(TreeNode node) {if(node == null) {return;}inOrder(node.left);inOrder(node.right);System.out.print(node.val);
}
//非递归
public void postorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<TreeNode>();TreeNode cur = root;/* 用来记录最新出栈的节点,* 如果当前节点的右儿子与flag相同,说明当前节点右子树已完成遍历*/TreeNode flag = null;while (cur != null) {stack.push(cur);cur = cur.left;}while (!stack.isEmpty()) {cur = stack.pop();if (cur.right == null || cur.right == flag) {System.out.print(cur.val);flag = cur;} else {stack.push(cur);cur = cur.right;while(cur != null) {stack.push(cur);cur = cur.left;}}}
}
二叉树
镜像/反转二叉树
public void Mirror(TreeNode root) {if(root == null){return;}if(root.left==null && root.right==null){return;}TreeNode temp = null;temp = root.left;root.left = root.right;root.right = temp;if(root.left != null){Mirror(root.left);}if(root.right != null){Mirror(root.right);}
}
树的直径
二叉树的直径就是任意两点之间的最大距离,因此可以将这个问题转化为:求左右子树最大深度之和。使用两次DFS
class Solution {int maxd = 0;public int diameterOfBinaryTree(TreeNode root) {depth(root);return maxd;}public int depth(TreeNode node){if(node == null){return 0;}int Left = depth(node.left);int Right = depth(node.right);maxd = Math.max(Left+Right, maxd); //将每个节点最大直径(左子树深度+右子树深度)当前最大值比较并取大者return Math.max(Left, Right) + 1; //返回节点深度}
}
从根节点到叶子节点路径总和等于给定目标和的路径
public List<List<Integer>> pathSum(TreeNode root, int sum){List<List<Integer>> list = new ArrayList<List<Integer>>();if(root==null){return list;}List<Integer> temp= new ArrayList<Integer>();getPass(root,sum,list,temp,0);return list;
}
public void getPass(TreeNode root,int sum,List<List<Integer>> list, List<Integer> temp,int add){/*如果是负数呢if(add>sum){return;}*/if(root.left==null && root.right==null){if(add+root.val==sum){temp.add(root.val);list.add(new ArrayList<Integer>(temp)); temp.remove(temp.size()-1);}return;}else{if(root.left!=null){temp.add(root.val);getPass(root.left,sum,list,temp,add+root.val);temp.remove(temp.size()-1);}if(root.right!=null){temp.add(root.val);getPass(root.right,sum,list,temp,add+root.val);temp.remove(temp.size()-1);}}
}
二叉树剪枝
public boolean prune(TreeNode root) {if(root == null) {return false;}boolean left = prune(root.left);boolean right = prune(root.right);if(!left) {root.left = null;}if(!right) {root.right = null;}if(root.val == 1 || right || left) {return true;} else {return false;}
}
两个节点的最近公共祖先
//递归
public static TreeNode getParent(TreeNode root, TreeNode node1, TreeNode node2) {if(root == null || node1 == null || node2 == null) {return null;}if(root == node1 || root == node2) {return root;}TreeNode left = getParent(root.left, node1, node2);TreeNode right = getParent(root.right, node1, node2);//如果左右子树都能找到,那么当前节点就是最近的公共祖先节点if(left != null && right != null) {return root;}//如果左子树上没有,那么返回右子树的查找结果if(left == null) {return right;}//否则返回左子树的查找结果return left;
}
堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种**选择排序,**它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
将堆从上到下、从左到右编号,便可用数组映射出堆结构,定义为:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
将其与末尾元素进行交换,此时末尾就为最大值。
然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
int[] arr = {9,8,7,6,5,4,3,2,1};
public static void main(String []args){sort(arr);System.out.println(Arrays.toString(arr));
}
public static void sort(){//1.构建大顶堆//从第一个非叶子结点从下至上,从右至左调整结构for(int i = arr.length/2-1; i >= 0; i--){adjustHeap(i, arr.length);}//2.调整堆结构+交换堆顶元素与末尾元素for(int i = arr.length-1; i > 0; i--){swap(0, i); //将堆顶元素与末尾元素进行交换adjustHeap(0, i); //重新对堆进行调整}
}
/*** 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)*/
public static void adjustHeap(int i, int length){int left = i*2+1, right = left+1, max = i;if (left < length && arr[left] > arr[max]) {max = left;}if (right < length && arr[right] > arr[max]) {max = right;}if (max != i) {swap(max, i);adjustHeap(max, length);}
}public static void swap(int a , int b){int temp = arr[a];arr[a] = arr[b];arr[b] = temp;
}
快速排序
先从数列中取出一个数作为基准数。
分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
再对左右区间重复第二步,直到各区间只有一个数。
int[] nums = {9,8,7,6,5,4,3,2,1};public static void main(String[] args) {int left = 0;int right = nums.size()-1;QSort(left,right);System.out.println(nums.toString());
}public void QSort(int left, int right) {if(left >= right) {return; }int index = getIndex(left,right); //获得相遇点,将原区间分为两个子区间进行递归分治QSort(left,index-1); //左子区间递归QSort(index+1,right); //右子区间递归
}public int getIndex(int left, int right) { //获取相遇点int base = nums[left];int start = left; //存储基准数的位置和大小while(left < right) { //左右哨兵未相遇while(left < right && nums[right] >= base) { //从右边开始往左寻找小于基准数的数right--; }while(left < right && nums[left] <= base) { //从左边开始往右寻找大于基准数的数left++; }//两数交换位置int temp = nums[right];nums[right] = nums[left];nums[left] = temp;}//哨兵相遇,则将基准数换到相遇点来nums[start] = nums[left];nums[left] = base;return left; //返回相遇点
}
防止最坏情况发生
- 通过采用随机方法。在原方法中,选择比较元素为数组最后一个元素。通过随机函数,生成位于p~r之间的随机数为比较项位置(A[random(p,r)]),与原先比较元素(A[r])位置交换,再调用PARTITION方法
- 当问题规模小于某一k值时,采用插入排序,提高算法效率
归并排序
归并排序,是创建在归并操作上的一种有效的排序算法。算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
时间复杂度:O(nlogn),空间复杂度:O(n)
基本思想
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
- 分解(Divide):将n个元素分成个含n/2个元素的子序列。
- 解决(Conquer):用合并排序法对两个子序列递归的排序。
- 合并(Combine):合并两个已排序的子序列已得到排序结果。
实现逻辑
迭代法
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤③直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
public static void merge_sort(int[] arr) {int len = arr.length;int[] result = new int[len];int block, start;// 原版代码的迭代次数少了一次,没有考虑到奇数列数组的情况for(block = 1; block < len*2; block *= 2) {for(start = 0; start <len; start += 2 * block) {int low = start;int mid = (start + block) < len ? (start + block) : len;int high = (start + 2 * block) < len ? (start + 2 * block) : len;//两个块的起始下标及结束下标int start1 = low, end1 = mid;int start2 = mid, end2 = high;//开始对两个block进行归并排序while (start1 < end1 && start2 < end2) {result[low++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];}while(start1 < end1) {result[low++] = arr[start1++];}while(start2 < end2) {result[low++] = arr[start2++];}}int[] temp = arr;arr = result;result = temp;}result = arr;
}
递归法
- 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
- 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
- 重复步骤②,直到所有元素排序完毕
public static int[] sort(int[] sourceArray) {// 对 arr 进行拷贝,不改变参数内容int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);if (arr.length < 2) {return arr;}int middle = arr.length / 2;int[] left = Arrays.copyOfRange(0, middle);int[] right = Arrays.copyOfRange(middle, arr.length);return merge(sort(left), sort(right));
}protected static int[] merge(int[] left, int[] right) {int[] result = new int[left.length + right.length];int i = 0;while (left.length > 0 && right.length > 0) {if (left[0] <= right[0]) {result[i++] = left[0];left = Arrays.copyOfRange(left, 1, left.length);} else {result[i++] = right[0];right = Arrays.copyOfRange(right, 1, right.length);}}while (left.length > 0) {result[i++] = left[0];left = Arrays.copyOfRange(left, 1, left.length);}while (right.length > 0) {result[i++] = right[0];right = Arrays.copyOfRange(right, 1, right.length);}return result;
}
选择排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
public int[] sort(int[] sourceArray) throws Exception {int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);// 总共要经过 N-1 轮比较for (int i = 0; i < arr.length - 1; i++) {int min = i;// 每轮需要比较的次数 N-ifor (int j = i + 1; j < arr.length; j++) {if (arr[j] < arr[min]) {// 记录目前能找到的最小值元素的下标min = j;}}// 将找到的最小值和i位置所在的值进行交换if (i != min) {int tmp = arr[i];arr[i] = arr[min];arr[min] = tmp;}}return arr;
}
插入排序
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
public int[] sort(int[] sourceArray) throws Exception {// 对 arr 进行拷贝,不改变参数内容int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的for (int i = 1; i < arr.length; i++) {// 记录要插入的数据int tmp = arr[i];// 从已经排序的序列最右边的开始比较,找到比其小的数int j = i;while (j > 0 && tmp < arr[j - 1]) {arr[j] = arr[j - 1];j--;}// 存在比其小的数,插入if (j != i) {arr[j] = tmp;}}return arr;
}
链表
判断回文
反转一半链表再对比
public static void main() {ListNode tail = reverseHalf(head);while(tail != null) {if(tail.val != head.val) {return false;}tail = tail.next;head = head.next;}
}public ListNode reverseHalf(ListNode head) {ListNode tail = head, half = head;while(tail != null && tail.next != null) {tail = tail.next.next;half = half.next;}if(tail != null) {half = half.next;}ListNode pre = new ListNode();pre.next = null;ListNode temp;while(half != null) {temp = half.next;half.next = pre;pre = half;half = temp;}return pre;
}
每K个节点反转链表
//不停地取k个进行翻转,如果不够k个,就直接返回,结束
public static ListNode reverseGroup(ListNode head, int k) {if (head == null || head.next == null || k <= 1)return head;ListNode currentNode = head;//获取k个元素的首尾节点for (int count = 1; count < k; count++) {currentNode = currentNode.next;//不够K个则返回if(currentNode == null)return head;}ListNode next = currentNode.next;//对局部链表进行反转reverse(head, currentNode);head.next = reverseGroup(next, k);return currentNode;
}//写一个头尾节点反转的局部函数
public static void reverse(ListNode head, ListNode tail) {if (head == null || head.next == null)return;ListNode pre = null;ListNode next = null;while (pre != tail) {next = head.next;head.next = pre;pre = head;head = next;}
}
环形链表入口节点
public ListNode EntryNodeOfLoop(ListNode pHead) {//给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null//找出链表的环的入口。//1.要判断链表是否有环--快慢指针//2.找环的入口节点()//1.是否有环//定义标识符boolean circle=false;//因为要移动两步,判断这个和下一个,以及下一个if(pHead==null || pHead.next==null || pHead.next.next==null){return null;}//快慢指针ListNode p1=pHead;ListNode p2=pHead;//循环链表while(p1!=null && p1.next!=null && p1.next.next!=null){//快指针移动,慢指针移动p1=p1.next.next;p2=p2.next;//相遇就是有环if(p1==p2){circle=true;break;}}//如果有环。快指针从开始的点开始,慢指针从相遇点开始.各自都走一步if(circle){ListNode q=pHead;//当快指针和慢指针不相遇一直移动,相遇就返回快指针。while(q!=p2){q=q.next;p2=p2.next;}return q;}//没有就是返回nullreturn null;
}
数组
数组的局部峰值
//找一个峰值index
public int findPeakElement(int[] nums) {//从第二个数字开始往后遍历,如果第二个数字比第一个数字小,说明此时第一个数字就是一个局部峰值for(int i = 1; i < nums.length; i++) {if(nums[i] < nums[i-1]) {return i-1;}}return nums.length-1;
}
//找多个峰值index
public int findPeakElement(int[] nums) {if(nums[1] < nums[0]) {System.out.print(0);}for(int i = 1; i < nums.length-1; i++) {if(nums[i] > nums[i-1] && nums[i] < nums[i+1]) {System.out.print(i);}}if(nums[nums.length-1] > nums[nums.length-2]){System.out.print(i);}
}
TOP K问题
冒泡 O(n*k)
public int findK(int[] nums, int k) {int s = 0;for(int i = 0; i < nums.length-1; i++) {for(int j = i+1; j < nums.length; j++) {if(nums[i] < nums[j]) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;}}if(s++ >= k) {break;}}return nums[k-1];
}
堆 O(n*log(k))
优先队列
//小顶堆保存最大k个数,大顶堆保存最小k个数(倒数第k大数)(对于弹出法而言)
public int findK(int[] nums, int k) {Queue<Integer> queue = new PriorityQueue<>((i1, i2) -> i1-i2);for(int num : nums) {queue.add(num);if (queue.size() > k) {queue.poll();}}return queue.peek();
}
快速排序Partition O(n)
int[] nums = {9,8,7,6,5,4,3,2,1};public int[] TopKInQuick(int k, int len) {if(len == k) {return nums;}int[] res;int index = findTopKthIndex(k, 0, len-1); //通过快排找到第K+1大的数的位置for(int i = len-1, j = 0; i > index; i--, j++) {res[j] = nums[i]; //取出TopK返回}return res;
}public int findTopKthIndex(int k, int left, int right) {int index = getIndex(left,right); //获取基准数位置int NumOverBase = right-index; //比基准数大的数的个数if(NumOverBase == k){return index; //比基准数大的刚好有K个} else if(NumOverBase > k) { //比基准数大的多于K个,就在右边子区间寻找TopKreturn findTopKthIndex(k, index+1, right);} else { //比基准数大的少于K个,就在左边找剩下的return findTopKthIndex(k-NumOverBase, left, index);}
}public int getIndex(int left, int right) { //获取相遇点int base = nums[left];int start = left; //存储基准数的位置和大小while(left < right) { //左右哨兵未相遇while(left < right && nums[right] >= base) { //从右边开始往左寻找小于基准数的数right--; }while(left < right && nums[left] <= base) { //从左边开始往右寻找大于基准数的数left++; }//两数交换位置int temp = nums[right];nums[right] = nums[left];nums[left] = temp;}//哨兵相遇,则将基准数换到相遇点来nums[start] = nums[left];nums[left] = base;return left; //返回相遇点
}
数组内最大顺序差值(股票问题)
public static int test(int[] list) {int min = list[0];int profit = 0;for (int i : list) {if (i < min) {min = i;} else if (profit < i-min) {profit = i-min;}}return profit;
}//累积差值
public static int test(int[] list) {int cur = list[0];int profit = 0;for (int i : list) {if (cur < i) {profit += i-cur;}cur = i;}return profit;
}
二维数组查找
public boolean Find(int target, int [][] array) {int row = array.length-1, col = 0;while (row >= 0 && col <= array[0].length-1) {if (target > array[row][col]) {col++;} else if (target < array[row][col]) {row--;} else if (target == array[row][col]) {return true;}}return false;
}
数组的所有子集
//数组模拟位运算
int[] brr = new int[3];
int[] arr =new int[]{1,2,3};
public static void backstrace(int i){if(i == arr.length){for (int j = 0; j < brr.length; j++) {if(brr[j] == 1) {System.out.print(arr[j]+" ");}}System.out.println();}else{brr[i] = 1;backstrace(i+1);brr[i] = 0;backstrace(i+1);}
}
//真·位运算法
public static void backstrace(int length){int nEnd = 1<<length;boolean bNullSet = false;for(int mark = 0; mark < nEnd; mark++){bNullSet = true;for(int i = 0; i < length; i++){if(((1<<i)&mark) != 0){ //该位有元素输出bNullSet=false;System.out.print(arr[i]+",");}}if(bNullSet){ //空集合System.out.print("@");}System.out.println();}
}
数组移动K位
翻转3次
public void rotate(int[] nums, int k) {k %= nums.length;reverse(nums, 0, nums.length - 1);reverse(nums, 0, k - 1);reverse(nums, k, nums.length - 1);
}
public void reverse(int[] nums, int start, int end) {while (start < end) {int temp = nums[start];nums[start] = nums[end];nums[end] = temp;start++;end--;}
}
有序数组内target第一次出现的下标
public static int binarySearchLeft(int[] arr,int target){if(arr==null||arr.length==0){return -1;}int left = 0;int right = arr.length-1;while(left<right){int mid = (left+right)/2;if(arr[mid]<target){left = mid+1;}else {right = mid;}}if(arr[right]==target){return right;} else {return -1;}
}
合并有序数组
public void merge(int nums1 [], int m, int nums2 [], int n){int i = m-1, j = n-1, k = m+n-1;while(i>=0 && j>=0){if(nums1[i] > nums2[j])nums1[k--] = nums1[i--];elsenums1[k--] = nums2[j--]; }while(j>=0){nums[k--] = nums[j--];}
}
有序数组中和为K的两数下标
//双指针
public static int[] getSumToANum(int arr[], int num){int start = 0, end = arr.length;int[] res=new int[2];while (start < end){if (num==(arr[start]+arr[end])){res[0]=start;res[1]=end;break;}else if (num>(arr[start]+arr[end]))start++;elseend--;}return res;
}
数组中只有0,1,2三个元素,进行排序,要求时间复杂度为O(n)
设置三个标记指针,pos0,pos2,cur
令pos0从前往后遍历,指向第一个非0的位置,pos2从后往前遍历,指向第一个非2的位置
然后cur从pos0开始往后遍历:
遇到0就和pos0交换,pos0++;
遇到1什么也不做;
遇到2就和pos2交换,pos2向前滑动到下一个非2的位置,交换后还要重新检查cur的值;
直到cur与pos2相遇。
public void func(int[] arr) {int pos0 = 0, pos2 = arr.length-1, cur;while(0 == arr[pos0]) {pos0++;}while(2 == arr[pos2]) {pos2--;}cur = pos0;while(cur <= pos2){ if(0 == arr[cur]) {swap(arr, cur, pos0);pos0++;} else if(2 == arr[cur]) { swap(arr, cur, pos2);if(0 == arr[cur]){ //若交换之后,cur当前指向的元素为0,则继续将cur指向的元素和pos0指向的元素进行交换swap(arr, cur, pos0); pos0++; //交换之后,将pos0向后移动一位} pos2--; //pos2向前移动一位while(arr[pos2] == 2) { //若移动之后指向的元素还是2,则继续向前移动,直到指向第一个非2的元素pos2--;}} cur++;//将pcur向前移动}
}
栈
单调栈
public static Stack<Integer> linearStack(int[] nums) {Stack<Integer> s1 = new Stack<>();Stack<Integer> s2 = new Stack<>();for (int i = nums.length - 1; i >= 0; i--) { // 倒着往栈里放while (!s1.empty() && s1.peek() <= nums[i]) { // 判定个子高矮s2.push(s1.pop()); // 矮个起开,反正也被挡着了。。。}s1.push(nums[i]); // 进队,接受之后的身高判定吧!while (!s2.empty()) {s1.push(s2.pop());}}return s1;
}
栈实现队列
private Stack<Integer> s1 = new Stack<>(), s2 = new Stack<>();
/** 添加元素到队尾 */
public void push(int x) {s1.push(x);
}
/** 返回队头元素 */
public int peek() {if (s2.isEmpty()) {// 把 s1 元素压入 s2while (!s1.isEmpty()) {s2.push(s1.pop());}}return s2.peek();
}
/** 删除队头的元素并返回 */
public int pop() {// 先调用 peek 保证 s2 非空peek();return s2.pop();
}
/** 判断队列是否为空 */
public boolean empty() {return s1.isEmpty() && s2.isEmpty();
}
队列
单调队列
队列实现栈
int top_elem = 0;
Queue<Integer> q = new LinkedList<>();
/** 添加元素到栈顶 */
public void push(int x) {// x 是队列的队尾,是栈的栈顶q.offer(x);top_elem = x;
}
/** 返回栈顶元素 */
public int top() {return top_elem;
}
/** 删除栈顶的元素并返回 */
public int pop() {int size = q.size();// 留下队尾 2 个元素while (size > 2) {q.offer(q.poll());size--;}// 记录新的队尾元素top_elem = q.peek();q.offer(q.poll());// 删除之前的队尾元素return q.poll();
}
/** 判断栈是否为空 */
public boolean empty() {return q.isEmpty();
}
动态规划
连续子数组最大和
public static int FindGreatestSumOfSubArray(int[] array) {if (array.length == 0){return 0;}int currentsum = array[0];int greatsetsum = array[0];for(int i = 1; i < array.length; i++){if(currentsum > 0){currentsum += array[i];}else{currentsum = array[i];}if(currentsum > greatsetsum){greatsetsum = currentsum;}}return greatsetsum;
}
//或者
public int FindGreatestSumOfSubArray(int[] array) {int max = array[0];//因为这个dp[i]老是变,所以比如你dp[4]是8 dp[5]就变成-7了,所以需要res保存一下int res = array[0];for (int i = 1; i < array.length; i++) {max = Math.max(max + array[i], array[i]);res = Math.max(res, max);}return res;}
最长递增子序列
public int lengthOfLIS(int[] nums) {int[] dp = new int[nums.length];// dp 数组全都初始化为 1Arrays.fill(dp, 1);for (int i = 0; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);}}int res = 0;for (int i = 0; i < dp.length; i++) {res = Math.max(res, dp[i]);}return res;
}
扔鸡蛋
static int[][] memo;
public static int dropEgg(int egg, int level){if (egg == 1) {return level;}if (level == 0) {return 0;}if (memo[egg][level] != 0) {return memo[egg][level];}int res = 99999;for (int i = 1; i <= level; i++) {res = Math.min(res, Math.max(dropEgg(egg, level-i), dropEgg(egg-1, i-1))+1);}memo[egg][level] = res;return res;
}
用1元 3元 5元组成n元
private static int coinCoin(int m) {int[] a = {1, 3, 5}; //硬币面值int[] temp = new int[m + 1]; //存储所需硬币的数目for(int i = 0; i <= m; i++) {temp[i] = i; //默认全部使用1元,则i元最多需要使用i个银币。}//这个外层循坏,依次对1到m个钱数,进行凑数for(int i = 1; i <= m; i++) {//这个内层循环,每次都会固定执行3次for(int j = 0; j < 3; j++) {if(a[j] <= i && temp[i - a[j]] + 1 < temp[i]) {temp[i] = temp[i - a[j]] + 1; }}}return temp[temp.length-1];
}
多线程交替打印ABC
synchronized
从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能进行打印操作。一个对象锁是prev,就是前一个线程所对应的对象锁,其主要作用是保证当前线程一定是在前一个线程操作完成后(即前一个线程释放了其对应的对象锁)才开始执行。还有一个锁就是自身对象锁。
主要的思想就是,为了控制执行的顺序,必须要先持有prev锁(也就前一个线程要释放其自身对象锁),然后当前线程再申请自己对象锁,两者兼备时打印。之后首先调用self.notify()唤醒下一个等待线程(注意notify不会立即释放对象锁,只有等到同步块代码执行完毕后才会释放),再调用prev.wait()立即释放prev对象锁,当前线程进入休眠,等待其他线程的notify操作再次唤醒。
public class ABC_Synch {public static class ThreadPrinter implements Runnable {private String name;private Object prev;private Object self;private ThreadPrinter(String name, Object prev, Object self) {this.name = name;this.prev = prev;this.self = self;}@Overridepublic void run() {int count = 10;while (count > 0) {// 多线程并发,不能用if,必须使用whil循环synchronized (prev) { // 先获取 prev 锁synchronized (self) {// 再获取 self 锁System.out.print(name);// 打印count--;self.notifyAll();// 唤醒其他线程竞争self锁,注意此时self锁并未立即释放。}// 此时执行完self的同步块,这时self锁才释放。try {if (count == 0) {// 如果count==0,表示这是最后一次打印操作,通过notifyAll操作释放对象锁。prev.notifyAll();} else {prev.wait(); // 立即释放 prev锁,当前线程休眠,等待唤醒}} catch (InterruptedException e) {e.printStackTrace();}}}}}public static void main(String[] args) throws Exception {Object a = new Object();Object b = new Object();Object c = new Object();ThreadPrinter pa = new ThreadPrinter("A", c, a);ThreadPrinter pb = new ThreadPrinter("B", a, b);ThreadPrinter pc = new ThreadPrinter("C", b, c);new Thread(pa).start();Thread.sleep(10);// 保证初始ABC的启动顺序new Thread(pb).start();Thread.sleep(10);new Thread(pc).start();Thread.sleep(10);}
}
reentrantlock
通过ReentrantLock我们可以很方便的进行显式的锁操作,即获取锁和释放锁,对于同一个对象锁而言,同一时刻只可能有一个线程拿到了这个锁,此时其他线程通过lock.lock()来获取对象锁时都会被阻塞,直到这个线程通过lock.unlock()操作释放这个锁后,其他线程才能拿到这个锁。
public class ABC_Lock {private static Lock lock = new ReentrantLock();// 通过JDK5中的Lock锁来保证线程的访问的互斥private static int state = 0;//通过state的值来确定是否打印static class MyThread extends Thread{int num;String letter;public MyThread(String letter, int num) {this.num = num;this.letter = letter;}public void run() {for (int i = 0; i < 10; ) {try {lock.lock();while (state % 3 == num){System.out.print(letter);state++;i++;//变量自增必须写在这}}finally {lock.unlock();}}}}public static void main(String[] args) {new MyThread("A",0).start();new MyThread("B",1).start();new MyThread("C",2).start();}
}
reentrantlock + condition
与synchronized类似
public class ABC_Condition {private static Lock lock = new ReentrantLock();private static Condition A = lock.newCondition();private static Condition B = lock.newCondition();private static Condition C = lock.newCondition();private static int count = 0;static class ThreadA extends Thread {@Overridepublic void run() {try {lock.lock();for (int i = 0; i < 10; i++) {while (count % 3 != 0)//注意这里是不等于0,也就是说在count % 3为0之前,当前线程一直阻塞状态A.await(); // A释放lock锁System.out.print("A");count++;B.signal(); // A执行完唤醒B线程}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}static class ThreadB extends Thread {@Overridepublic void run() {try {lock.lock();for (int i = 0; i < 10; i++) {while (count % 3 != 1)B.await();// B释放lock锁,当前面A线程执行后会通过B.signal()唤醒该线程System.out.print("B");count++;C.signal();// B执行完唤醒C线程}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}static class ThreadC extends Thread {@Overridepublic void run() {try {lock.lock();for (int i = 0; i < 10; i++) {while (count % 3 != 2)C.await();// C释放lock锁System.out.print("C");count++;A.signal();// C执行完唤醒A线程}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}public static void main(String[] args) throws InterruptedException {new ThreadA().start();new ThreadB().start();new ThreadC().start();}
}
semaphore
Semaphore是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
Semaphore使用时需要先构建一个参数来指定共享资源的数量,Semaphore构造完成后即是获取Semaphore、共享资源使用完毕后释放Semaphore。
public class ABC_Semaphore {// 以A开始的信号量,初始信号量数量为1private static Semaphore A = new Semaphore(1);// B、C信号量,A完成后开始,初始信号数量为0private static Semaphore B = new Semaphore(0);private static Semaphore C = new Semaphore(0);static class ThreadA extends Thread {@Overridepublic void run() {try {for (int i = 0; i < 10; i++) {A.acquire();// A获取信号执行,A信号量减1,当A为0时将无法继续获得该信号量System.out.print("A");B.release();// B释放信号,B信号量加1(初始为0),此时可以获取B信号量}} catch (InterruptedException e) {e.printStackTrace();}}}static class ThreadB extends Thread {@Overridepublic void run() {try {for (int i = 0; i < 10; i++) {B.acquire();System.out.print("B");C.release();}} catch (InterruptedException e) {e.printStackTrace();}}}static class ThreadC extends Thread {@Overridepublic void run() {try {for (int i = 0; i < 10; i++) {C.acquire();System.out.println("C");A.release();}} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {new ThreadA().start();new ThreadB().start();new ThreadC().start();}
}
给定一个函数rand()能产生0到n-1之间的等概率随机数,问如何产生0到m-1之间等概率的随机数?
例:rand5()产生1-5的数,现需使用rand5()构建能等概率生成1-7的函数rand7()
rand5() 它能够等概率生成 1-5 之间的整数。所谓等概率就是1,2,3,4,5 生产的概率均为 0.2 。现在利用rand5(), 构造一个能够等概率生成 1- 7 的方法。这里有两个特别重要的点,一是 如果 rand5() + rand5(), 我们能够产生一个均匀分布的 1 - 10 吗? 答案是否定的。比如对于 6来讲(4+2, 2+4, 3+3),它被生成的生成的概率比1 (1+0,0+1)要大。
第二个点就是我们不可能用rand5()直接产生 1- 7 的数,不管你用加减乘除都不行。所以,我们要构造一个更大的范围,使得范围里每一个值被生成的概率是一样的,而且这个范围是7的倍数。
正确的方法是利用rand5()函数生成1-25之间的数字,然后将其中的1-21映射成1-7,丢弃22-25。例如生成(1,1),(1,2),(1,3),则看成rand7()中的1,如果出现剩下的4种,则丢弃重新生成。先产生一个均匀分布的 0, 5, 10, 15, 20的数,再产生一个均匀分布的 0, 1, 2, 3, 4 的数。相加以后,会产生一个 0到24的数,而且每个数(除0外)生成的概率是一样的。我们只取 1 - 21 这一段,和7 取余以后+1就能得到完全均匀分布的1-7的随机数了
public int rand7() { int x = 22; while(x > 21) { x = rand5() + (rand5() - 1)*5; } return 1 + x%7;
}
通用情况:
给定随机数randA(),生成randB()
//当A>B
public void randB() {int x = B*(A/B)+1; // max intwhile(x > B*(A/B)) { // b*(A/b)表示最接近A且小于A的b的倍数x = RandA();}return x%B + 1;
}
//当A<B
public void randB() {int x = B*(A/B)+1; // max intwhile(x > B*(A/B)) { // b*(A/b)表示最接近A且小于A的b的倍数x = A * (RandA() - 1) + RandA();}return x%B + 1;
}
LRU
LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。
LRU 算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。
分析上面的操作过程,要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分。因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。
首先,我们把双链表的节点类写出来,为了简化,key 和 val 都认为是 int 类型:
class Node {public int key, val;public Node next, prev;public Node(int k, int v) {this.key = k;this.val = v;}
}
然后依靠我们的 Node 类型构建一个双链表,实现几个需要的 API(这些操作的时间复杂度均为 O ( 1 ) O(1) O(1)):
class DoubleList { // 在链表头部添加节点 x,时间 O(1)public void addFirst(Node x);// 删除链表中的 x 节点(x 一定存在)// 由于是双链表且给的是目标 Node 节点,时间 O(1)public void remove(Node x);// 删除链表中最后一个节点,并返回该节点,时间 O(1)public Node removeLast();// 返回链表长度,时间 O(1)public int size();
}
PS:这就是普通双向链表的实现,为了让读者集中精力理解 LRU 算法的逻辑,就省略链表的具体代码。
有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可。我们先把逻辑理清楚:
// key 映射到 Node(key, val)
HashMap<Integer, Node> map;
// Node(k1, v1) <-> Node(k2, v2)...
DoubleList cache;int get(int key) {if (key 不存在) {return -1;} else { 将数据 (key, val) 提到开头;return val;}
}void put(int key, int val) {Node x = new Node(key, val);if (key 已存在) {把旧的数据删除;将新节点 x 插入到开头;} else {if (cache 已满) {删除链表的最后一个数据腾位置;删除 map 中映射到该数据的键;} 将新节点 x 插入到开头;map 中新建 key 对新节点 x 的映射;}
}
如果能够看懂上述逻辑,翻译成代码就很容易理解了:
class LRUCache {// key -> Node(key, val)private HashMap<Integer, Node> map;// Node(k1, v1) <-> Node(k2, v2)...private DoubleList cache;// 最大容量private int cap;public LRUCache(int capacity) {this.cap = capacity;map = new HashMap<>();cache = new DoubleList();}public int get(int key) {if (!map.containsKey(key))return -1;int val = map.get(key).val;// 利用 put 方法把该数据提前put(key, val);return val;}public void put(int key, int val) {// 先把新节点 x 做出来Node x = new Node(key, val);if (map.containsKey(key)) {// 删除旧的节点,新的插到头部cache.remove(map.get(key));cache.addFirst(x);// 更新 map 中对应的数据map.put(key, x);} else {if (cap == cache.size()) {// 删除链表最后一个数据Node last = cache.removeLast();map.remove(last.key);}// 直接添加到头部cache.addFirst(x);map.put(key, x);}}
}
这里就能回答之前的问答题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”,注意这段代码:
if (cap == cache.size()) {// 删除链表最后一个数据Node last = cache.removeLast();map.remove(last.key);
}
当缓存容量已满,我们不仅仅要删除最后一个 Node 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。
至此,你应该已经掌握 LRU 算法的思想和实现了,很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。
手动实现 O(1)
public class LRUCacheByself {// 双向链表节点结构private class Node {public Node pre;public Node next;public int key;public int val;public Node(int k, int v) {this.key = k;this.val = v;this.pre = null;this.next = null;}}// 双向链表 头部是最老的private class DoublyLinkedList {public Node head;public Node tail;public DoublyLinkedList() {this.head = null;this.tail = null;}public void moveToTail(Node n) {// 将节点移动至尾部if (n == null || n == tail) return;if (head == n) {head = n.next;head.pre = null;} else {n.pre.next = n.next;n.next.pre = n.pre;}tail.next = n;n.pre = tail;n.next = null;tail = tail.next;}public void addToTail(Node n) {if (n == null) return;// 添加新的节点if (head == null) {head = n;tail = n;} else {tail.next = n;n.pre = tail;tail = n;}}public Node removeHead() {// 删除头部(最老的)节点if (head == null) return null;Node n = head;if (head == tail) {head = null;tail = null;} else {head = head.next;head.pre = null;}return n;}}private DoublyLinkedList list;private HashMap<Integer, Node> map;private int capacity;public LRUCacheByself(int capacity) {this.list = new DoublyLinkedList();this.map = new HashMap<>();this.capacity = capacity;}public int get(int key) {if (!map.containsKey(key)) {return -1;}Node n = map.get(key);list.moveToTail(n);return n.val;}public void put(int key, int value) {if (!map.containsKey(key)) {Node n = new Node(key, value);map.put(key, n);list.addToTail(n);if (map.size() > capacity) {Node rmv = list.removeHead();map.remove(rmv.key);}} else {Node n = map.get(key);n.val = value;list.moveToTail(n);}}
}
双向链表 O(n)
public class LRUCache {class Node {int key;int val;public Node(int key, int val) {this.key = key;this.val = val;}}LinkedList<Node> cache;int capacity;public LRUCache(int capacity) {this.cache = new LinkedList<>();this.capacity = capacity;}// -1 表示没找到public int get(int key) {Iterator<Node> iterator = cache.descendingIterator();int result = -1;while (iterator.hasNext()) {Node node = iterator.next();if (node.key == key) {result = node.val;iterator.remove();put(key, result); //添加到链表尾部break;}}return result;}public void put(int key, int value) {//先遍历查找是否有key 的元素, 有则删除,重新添加到链尾Iterator<Node> iterator = cache.iterator();while (iterator.hasNext()) {Node node = iterator.next();if (node.key == key) {iterator.remove();break;}}if (capacity == cache.size()) {//缓存已满,删除一个 最近最少访问的元素(链表头)cache.removeFirst();}cache.add(new Node(key, value));}
}
LinkedHashMap O(1)
public class LRUCache2 {LinkedHashMap<Integer, Integer> cache;int capacity;public LRUCache2(int capacity) {cache = new LinkedHashMap<>(capacity);this.capacity = capacity;}public int get(int key) {if (!cache.containsKey(key)) {return -1;}int val = cache.get(key);cache.remove(key); // 从链表中删除cache.put(key, val); // 添加到链尾return val;}public void put(int key, int value) {if (cache.containsKey(key)) {cache.remove(key); // 已经存在,链表中删除}if (capacity == cache.size()) {// cache 已满,删除链表头Set<Integer> keySet = cache.keySet();Iterator<Integer> iterator = keySet.iterator();cache.remove(iterator.next());}cache.put(key, value);// 添加到链尾}
}//或者使用已实现方法
class LRUCache3 {private Map<Integer, Integer> map;private final int capacity;public LRUCache3(int capacity) {this.capacity = capacity;map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {return size() > capacity; // 容量大于capacity 时就删除}};}public int get(int key) {return map.getOrDefault(key, -1);}public void put(int key, int value) {map.put(key, value);}
}
LFU
LFU(Least Frequently Used) 淘汰一定时期内被访问次数最少的元素。如果元素的一定时间内的访问次数相同时,则比较他们的最新一次的访问时间。
public class LFU<k, v> {//内部类class HitRate implements Comparable<HitRate> {private k key;private int hitCount;private long lastTime;private HitRate(k key, int hitCount, long lastTime) {this.key = key;this.hitCount = hitCount;this.lastTime = lastTime;}@Overridepublic int compareTo(HitRate o) {int compare = Integer.compare(this.hitCount, o.hitCount);return compare == 0 ? Long.compare(this.lastTime, o.lastTime) : compare;}}private final int capcity;private Map<k, v> cache = new HashMap<>();private Map<k, HitRate> count = new HashMap<>();public LFU(int capcity) {this.capcity = capcity;}public void put(k key, v value) {v v = cache.get(key);if (v == null) {if (cache.size() == capcity) {removeElement();}count.put(key, new HitRate(key, 1, System.nanoTime()));} else {addHitCount(key);}cache.put(key, value);}public v get(k key) {v value = cache.get(key);if (value != null) {addHitCount(key);return value;}return null;}//移除元素private void removeElement() {HitRate hr = Collections.min(count.values());cache.remove(hr.key);count.remove(hr.key);}//更新访问元素状态private void addHitCount(k key) {HitRate hitRate = count.get(key);hitRate.hitCount += 1;hitRate.lastTime = System.nanoTime();}
}
斐波那契(走台阶)
递归
public int Fibonacci(int n) {if(n <= 1){return n;}return Fibonacci(n-1) + Fibonacci(n-2);
}
最优
public int Fibonacci(int n) {if(n == 0 || n == 1){return n;}int first = 0, second = 1, result = 0;for (int i = 2; i <= n; i++) {result = first+second;first = second;second = result;}return result;
}
变态跳台阶
public int JumpFloorII(int target) {return (int)Math.pow(2, target-1);
}
最长不重复子串长度
public int lengthOfLongestSubstring(String s) {int n = s.length(), res = i = j = 0;Set<Character> set = new HashSet<>();while (i < n && j < n) {if (!set.contains(s.charAt(j))){set.add(s.charAt(j));j++;res = Math.max(res, j - i);} else {set.remove(s.charAt(i));i++;}}return res;
}
大数相加
public static String bigNumberSum(String bigNumberA, String bigNumberB) {//1.把两个大整数用数组逆序存储,数组长度等于较大整数位数+1int maxLength = Math.max(bigNumberA.length(), bigNumberB.length());int[] arrayA = new int[maxLength+1];for(int i = 0; i < bigNumberA.length(); i++){arrayA[i] = bigNumberA.charAt(bigNumberA.length()-1-i) - '0'; //“ - '0'”是将Char型转化为int型}int[] arrayB = new int[maxLength+1];for(int i=0; i< bigNumberB.length(); i++){arrayB[i] = bigNumberB.charAt(bigNumberB.length()-1-i) - '0';}//2.构建result数组,数组长度等于较大整数位数+1int[] result = new int[maxLength+1];//3.遍历数组,按位相加for(int i = 0; i < result.length; i++){int temp = result[i]; //加上前一位的进位temp += arrayA[i];temp += arrayB[i];//判断是否进位if(temp >= 10){temp = temp-10; //有进位的话将temp化为一位数result[i+1] = 1; //将进位1存储到结果数组的下一位}result[i] = temp; //将1位数存储到结果数组对应位}//4.把result数组再次逆序并转成StringStringBuilder sb = new StringBuilder();//用于标记是否找到大整数的最高有效位boolean findFirst = false;for (int i = result.length - 1; i >= 0; i--) {//从后往前if(!findFirst){if(result[i] == 0){ //用于跳过结果数组末尾的0continue;} else {findFirst = true;}}sb.append(result[i]);}return sb.toString();
}
N对括号全部有效组合
//仅算数量
int validCnt = 0;
public void findParens(int l, int r) {if(l > r){ //剩余的左括号大于了右括号,非法casereturn;} if(l == 0){ //剩余的左括号肯定先被减少到0,剩余全是右括号validCnt++;return;}findParens(l-1, r); //将当前位置赋值为左括号findParens(l, r-1);//将当前位置赋值为右括号
}
//输出结果
static int n = 3;
public static void findParens(int l, int r, char[] res) {if(r == 0){ for (char c : res) {System.out.print(c);}System.out.println();return;}if (l > 0) {res[2*n-l-r] = '(';findParens(l - 1, r, res);}if (l < r) {res[2*n-l-r] = ')';findParens(l, r - 1, res);}
}
最长回文子串
public String longestPalindrome(String s) {String res = "";for (int i = 0; i < s.length(); i++) {// 以 s[i] 为中心的最长回文子串String s1 = palindrome(s, i, i);// 以 s[i] 和 s[i+1] 为中心的最长回文子串String s2 = palindrome(s, i, i + 1);res = res.length() > s1.length() ? res : s1;res = res.length() > s2.length() ? res : s2;}return res;
}public String palindrome(String s, int l, int r) {// 防止索引越界while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {// 向两边展开l--; r++;}// 返回以 s[l] 和 s[r] 为中心的最长回文串return s.substring(l+1, r);
}
数列中消失的元素
public int missingNumber(int[] nums) {int n = nums.length;int res = 0;// 先和新补的索引异或一下res ^= n;// 和其他的元素、索引做异或for (int i = 0; i < n; i++) {res ^= i; res ^= nums[i];}return res;
}
缺失和重复的元素
public int[] findErrorNums(int[] nums) {int n = nums.length;int dup = -1;for (int i = 0; i < n; i++) {// 现在的元素是从 1 开始的int index = Math.abs(nums[i]) - 1;if (nums[index] < 0)dup = Math.abs(nums[i]);elsenums[index] *= -1;}int missing = -1;for (int i = 0; i < n; i++)if (nums[i] > 0) {// 将索引转换成元素missing = i + 1;}return new int[]{dup, missing};
}
面试季,覆盖70%-80%的面经基础题(java及安卓)-------数据结构与算法篇相关推荐
- 面试季,覆盖70%-80%的面经基础题(java及安卓)-------网络篇
一般 OSI与TCP/IP各层的结构与功能,都有哪些协议? 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 子网掩码 在浏览器中输入url地址 -> 显示主页的过程 DNS 域名 组 ...
- 【前端 · 面试 】JavaScript 之你不一定会的基础题(二)
最近我在做前端面试题总结系列,感兴趣的朋友可以添加关注,欢迎指正.交流. 争取每个知识点能够多总结一些,至少要做到在面试时,针对每个知识点都可以侃起来,不至于哑火. 前言 在上一篇文章[前端 · 面试 ...
- 全面详细的面试指南:数据结构与算法篇 (附答案)
前言 背景:算法与数据结构作为技术开发者最基本的技术修养,在面试中出现的频率非常高 本文目的:花尽可能短的时间,快速学习面试中常见常考的数据结构知识及算法题 适用阅读人群:技术岗位面试开发者 本文主要 ...
- 前端面试 --数据结构与算法篇
一. 各种数据结构汇总 (内容深度只涉及到前端水平,毕竟我水平也一般) 1. 线性表 数据元素间是线性关系,数据元素在表中的位置只取决于 其序号 存在唯一的一个被称作"第一个&quo ...
- 面试过阿里的P7大佬分享:180+道Java面试题目!含答案解析!
大厂常见问题 写视频点播网站文件下载接口 基础变量/数组写出模拟maven导入包过程 写出新变脸内存分配,模拟垃圾回收过程 50个白球50个红球,两个盒子,怎么放让人随机在一个盒子里抽到红球概率最高 ...
- 年后跳槽BAT必看:10种数据结构、算法和编程课助你面试通关
作者 | javinpaul 译者 | 大鱼 编辑 | 一一 出品 | AI 科技大本营 进入 BAT 这样的巨头企业工作,无疑是很多程序员的梦想.但事实上,能通过这些公司高难度编程面试的只是一小撮人 ...
- 互联网日报 | 京东PLUS会员数超2000万;滴滴实时公交业务覆盖超80城;小米数科品牌升级天星数科...
今日看点 ✦ 小米数科品牌升级为天星数科,战略聚焦产业数字金融 ✦ 京东PLUS会员数超2000万,预计今年双11为会员省160亿元 ✦ 滴滴实时公交业务覆盖超80城市,用户可轻松"掐点&q ...
- 面试季:如何在面试中介绍自己的项目经验
点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 来源:https://dwz.cn/2PrmlZCX 现在已经是7月份,一些互联网大厂已经开始 ...
- 循环队列,定义循环队列长度为10,编写初始化队列、入队、出队、求队长,实现10,20,30,40,50,60,70,80入队,10,20,30出队,56,67入队的算法功能。
循环队列,定义循环队列长度为10,编写初始化队列.入队.出队.求队长,实现10,20,30,40,50,60,70,80入队,10,20,30出队,56,67入队的算法功能. #include< ...
最新文章
- 阿里全球数学竞赛第二届
- GPU、FPGA芯片成为增强机器学习能力的“左膀右臂”
- java的jdk和jre_Java的JDK和JRE
- linux scull 的内存使用
- JS基础语法(05)-隐式数据类型转换
- 2020春招机考汇总1(Python):农场养鸡、连续子序列最大值的期望
- 排除网络故障课后习题参考答案
- 将自己的女朋友的话和视频做为启动音乐和启动动画(XP)
- 外国小哥,把整个 CNN 都给可视化了,卷积、池化清清楚楚!
- 烟雾传感器应用程序linux,各种类型烟雾传感器的应用及原理解析
- JavaScript笔记(菜鸟教程)
- java jcombobox 联动_java的JCombobox实现中国省市区三级联动
- 中小学机器人类竞赛活动2020-2021
- 大数据给交通行业带来的五大变革
- 智能PID软件-AVEVA Diagrams 快速复制流程图【图瓦软件出品】
- 华为Linux改装windows的弊端,试用华为的linux版电脑,不安装windows,能不能胜任工作? - 区块网...
- HDU 5514 容斥原理
- 极简的书签管理器Shaarli
- 如何爬取当当网畅销书排行榜信息? requests + pyquery
- LeetCode解题(1)----------8 字符串转换整数[go]