面试季,覆盖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) { //基准条件:如果索引无效或者容量不足,直接返回当前价值0 if (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) { //基准条件:如果索引无效或者容量不足,直接返回当前价值0 if (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-i for (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; } //没有就是返回null return null;}
数组
数组的局部峰值
//找一个峰值indexpublic 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;} //找多个峰值indexpublic 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个,就在右边子区间寻找TopK return 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--]; else nums1[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++; else end--; } 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 元素压入 s2 while (!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 数组全都初始化为 1 Arrays.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; } @Override public 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 { @Override public 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 { @Override public 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 { @Override public 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开始的信号量,初始信号量数量为1 private static Semaphore A = new Semaphore(1); // B、C信号量,A完成后开始,初始信号数量为0 private static Semaphore B = new Semaphore(0); private static Semaphore C = new Semaphore(0); static class ThreadA extends Thread { @Override public 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 { @Override public 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 { @Override public 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>Bpublic void randB() { int x = B*(A/B)+1; // max int while(x > B*(A/B)) { // b*(A/b)表示最接近A且小于A的b的倍数 x = RandA(); } return x%B + 1;}//当A<Bpublic void randB() { int x = B*(A/B)+1; // max int while(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) { @Override protected 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; } @Override public 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.把两个大整数用数组逆序存储,数组长度等于较大整数位数+1 int 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数组,数组长度等于较大整数位数+1 int[] 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数组再次逆序并转成String StringBuilder sb = new StringBuilder(); //用于标记是否找到大整数的最高有效位 boolean findFirst = false; for (int i = result.length - 1; i >= 0; i--) {//从后往前 if(!findFirst){ if(result[i] == 0){//用于跳过结果数组末尾的0 continue; } else { findFirst = true; } } sb.append(result[i]); } return sb.toString();}
N对括号全部有效组合
//仅算数量int validCnt = 0;public void findParens(int l, int r) { if(l > r){ //剩余的左括号大于了右括号,非法case return; } 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]); else nums[index] *= -1; } int missing = -1; for (int i = 0; i < n; i++) if (nums[i] > 0) { // 将索引转换成元素 missing = i + 1; } return new int[]{dup, missing};}