60. 把二叉树打印成多行
题:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
思路:
1)
主要是要知道每一层的个数,然后按个数从队列中取,然后输出打印。
2)(难)
递归,前序递归保证先左后右的顺序,函数中传递深度,递归每深入一层,就对list数组扩容。
代码:
1)
ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> list = new ArrayList<>();
if (pRoot == null) {
return list;
}
// 每一行开始和结束,第一行只有一个
int start = 0;
int end = 1;
LinkedList<TreeNode> queue = new LinkedList<>();
ArrayList<Integer> layer = new ArrayList<>();
queue.add(pRoot);
while (!queue.isEmpty()) {
TreeNode pNode = queue.remove();
layer.add(pNode.val);
start++;
if (pNode.left != null) {
queue.add(pNode.left);
}
if (pNode.right != null) {
queue.add(pNode.right);
}
if (start == end) {
end = queue.size();
start = 0;
list.add(layer);
layer = new ArrayList<>();
}
}
return list;
}
2)递归
ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> list = new ArrayList<>();
if (pRoot == null) {
return list;
}
depth(pRoot,1,list);
return list;
}
private void depth(TreeNode pRoot, int depth, ArrayList<ArrayList<Integer>> list) {
if (pRoot == null) return;
if (depth > list.size()) {
// 扩容
list.add(new ArrayList<>());
}
list.get(depth-1).add(pRoot.val);
depth(pRoot.left,depth+1,list);
depth(pRoot.right,depth+1,list);
}
61. 序列化二叉树
题:
请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
思路:参考https://www.cnblogs.com/gzshan/p/10898708.html
序列化是指将结构化的对象转化为字节流以便在网络上传输或写到磁盘进行永久存储的过程。反序列化是指将字节流转回结构化的对象的过程,是序列化的逆过程。
受第4题:重建二叉树的启发,我们知道从前序遍历和中序遍历序列中可以构造出一棵二叉树,因此将一棵二叉树序列化为一个前序遍历序列和一个中序遍历序列,然后在反序列化时用第四题的思路重建二叉树。
这种思路是可行的,但是存在两个缺点:一是该方法要求二叉树中不能有重复的结点(比如我们通过前序知道根是1,若有重复的结点无法在中序序列中定位根的位置);二是只有当两个序列中所有的数据都读出后才能开始反序列化,如果两个遍历序列是从一个流中读出来的,那么可能需要等待较长的时间。
因此,这里我们采用另外一种方法,即只根据前序遍历的顺序来进行序列化,前序遍历是从根结点开始,在遍历二叉树碰到null指针时,就将其序列化为一个特殊字符,比如$
,另外,结点的数值之间用一个特殊字符(比如,
)进行分隔。比如对于以下的树,序列化为字符串:"1,2,4,$,$,$,3,5,$,$,6,$,$"
。
注意:
1)在节点之间必须用一个字符(,)来分割,因为有可能val=12,String之后是12,分不清是12还是1和2
代码:
String Serialize(TreeNode root) {
String str = "";
return Serialize(root, str);
}
String Serialize(TreeNode root,String str) {
if (root == null) {
str += "#,";
return str;
}
else {
str += root.val + ",";
}
str = Serialize(root.left,str);
str = Serialize(root.right,str);
return str;
}
TreeNode Deserialize(String str) {
if (str == null || str.length() == 0)
return null;
String[] strArr = str.split(",");
return Deserialize(strArr);
}
/**
*
* @param strArr,strArr中每个都是原始二叉树中的一个字符,包括$
* @return
*/
int start = -1;
private TreeNode Deserialize(String[] strArr) {
start++;
if (start < strArr.length && !strArr[start].equals("#")) {
TreeNode cur = new TreeNode(Integer.parseInt(strArr[start]));
cur.left = Deserialize(strArr);
cur.right = Deserialize(strArr);
return cur;
}
return null;
}
62. 二叉搜索树的第k个节点
题:给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
思路:二叉搜索树的中序遍历就是由小到大排序的序列,所以重点是中序比那里
代码:
1) 递归
TreeNode KthNode(TreeNode pRoot, int k)
{
if (pRoot == null || k <= 0) {
return null;
}
ArrayList<TreeNode> list = new ArrayList();
inOrder(pRoot,list);
if (k > list.size()) {
return null;
}
return list.get(k-1);
}
void inOrder (TreeNode pRoot,ArrayList list) {
if (pRoot == null) {
return;
}
inOrder(pRoot.left,list);
list.add(pRoot);
inOrder(pRoot.right,list);
}
2) 非递归
TreeNode KthNode(TreeNode pRoot, int k)
{
if (pRoot == null || k <= 0) {
return null;
}
Stack<TreeNode> stack = new Stack<>();
// stack.push(pRoot);
TreeNode pNode = pRoot;
int count = 0;
while (pNode != null || !stack.isEmpty()) {
if (pNode != null) {
stack.push(pNode);
pNode = pNode.left;
}
else {
TreeNode tmp = stack.pop();
count++;
if (count == k) {
return tmp;
}
pNode = tmp.right;
}
}
return null;
}
63. ※数据流中的中位数
题:如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
思路:重点是如何存储。
参考https://www.cnblogs.com/gzshan/p/10904254.html
首先要正确理解此题的含义,数据是从一个数据流中读出来的,因此数据的数目随着时间的变化而增加。对于从数据流中读出来的数据,当然要用一个数据容器来保存,也就是当有新的数据从流中读出时,需要插入数据容器中进行保存。那么我们需要考虑的主要问题就是选用什么样的数据结构来保存。
方法一:用数组保存数据。数组是最简单的数据容器,如果数组没有排序,在其中找中位数可以使用类比快速排序的partition函数,则插入数据需要的时间复杂度是O(1),找中位数需要的复杂度是O(n)。除此之外,我们还可以想到用直接插入排序的思想,在每到来一个数据时,将其插入到合适的位置,这样可以使数组有序,这种方法使得插入数据的时间复杂度变为O(n),因为可能导致n个数移动,而排序的数组找中位数很简单,只需要O(1)的时间复杂度。
方法二:用链表保存数据。用排序的链表保存从流中的数据,每读出一个数据,需要O(n)的时间找到其插入的位置,然后可以定义两个指针指向中间的结点,可以在O(1)的时间内找到中位数,和排序的数组差不多。
方法三:用二叉搜索树保存数据。在二叉搜索树种插入一个数据的时间复杂度是O(logn),为了得到中位数,可以在每个结点增加一个表示子树结点个数的字段,就可以在O(logn)的时间内找到中位数,但是二叉搜索树极度不平衡时,会退化为链表,最差情况仍需要O(n)的复杂度。
方法四:用AVL树保存数据。由于二叉搜索树的退化,我们很自然可以想到用AVL树来克服这个问题,并做一个修改,使平衡因子为左右子树的结点数之差,则这样可以在O(logn)的时间复杂度插入数据,并在O(1)的时间内找到中位数,但是问题在于AVL树的实现比较复杂。
注意:
AVL树:平衡二叉搜索树。它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
方法五:最大堆和最小堆。我们注意到当数据保存到容器中时,可以分为两部分,左边一部分的数据要比右边一部分的数据小。如下图所示,P1是左边最大的数,P2是右边最小的数,即使左右两部分数据不是有序的,我们也有一个结论就是:左边最大的数小于右边最小的数。
因此,我们可以有如下的思路:用一个最大堆实现左边的数据存储,用一个最小堆实现右边的数据存储,向堆中插入一个数据的时间是O(logn),而中位数就是堆顶的数据,只需要O(1)的时间就可得到。
而在具体实现上,首先要保证数据平均分配到两个堆中,两个堆中的数据数目之差不超过1,为了实现平均分配,可以在数据的总数目是偶数时,将数据插入最小堆,否则插入最大堆。
此外,还要保证所有最大堆中的数据要小于最小堆中的数据。所以,新传入的数据要和最大堆中最大值或者最小堆中的最小值比较。当总数目是偶数时,我们会插入最小堆,但是在这之前,我们需要判断这个数据和最大堆中的最大值哪个更大,如果最大值中的最大值比较大,那么将这个数据插入最大堆,并把最大堆中的最大值弹出插入最小堆。由于最终插入到最小堆的是原最大堆中最大的,所以保证了最小堆中所有的数据都大于最大堆中的数据。
为了保证插入新数据和取中位数的时间效率都高效,这里使用大顶堆+小顶堆的容器,并且满足:
1、两个堆中的数据数目差不能超过1,这样可以使中位数只会出现在两个堆的交接处;
2、大顶堆的所有数据都小于小顶堆,这样就满足了排序要求。
Java的PriorityQueue 是从JDK1.5开始提供的新的数据结构接口,默认内部是自然排序,结果为小顶堆(堆顶最小,默然从小到大),也可以自定义排序器,比如下面反转比较,完成大顶堆。
代码:
1)大顶堆、小顶堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
int count = 0;
public void Insert(Integer num) {
// 第一个数在小顶堆
if (count % 2 == 0) {
maxHeap.add(num);
int max = maxHeap.poll();
minHeap.add(max);
}
else {
minHeap.add(num);
int min = minHeap.poll();
maxHeap.add(min);
}
count++;
}
public Double GetMedian() {
if (count % 2 == 0) {
return new Double(minHeap.peek()+maxHeap.peek())/2;
}
else {
return new Double(minHeap.peek());
}
}
2) AVL树
64. 滑动窗口的最大值
题:给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
思路:
1) 其实就是求每size个的最大值,利用数组下标实现回溯(每次count==size之后,i回溯到i-size+1)
2) 双端队列(LinkedList实现),队列中保存每size个的最大值的下标,不用回溯,每次判断队首元素是不是过期
代码:
1)
public static ArrayList<Integer> maxInWindows(int[] num, int size)
{
ArrayList<Integer> list = new ArrayList<>();
int length = num.length;
int count = 0;
int max = -1;
for (int i = 0 ; i < length ; i++) {
if (num[i] > max) {
max = num[i];
}
count++;
if (count == size) {
list.add(max);
max = -1;
i = i - (size-1);
count = 0;
continue;
}
}
return list;
}
2) 双端队列
public ArrayList<Integer> maxInWindows(int [] num, int size)
{
ArrayList<Integer> list = new ArrayList<>();
if (num == null || num.length <= 0 || size <= 0) {
return list;
}
// 双端队列
LinkedList<Integer> deQueue = new LinkedList<>();
int begin = 0;
for (int i = 0 ;i < num.length; i++) {
while (!deQueue.isEmpty() && num[deQueue.peekLast()] < num[i]) {
deQueue.pollLast();
}
deQueue.add(i);
// 判断是否过期
if (deQueue.peekFirst() < i-size+1) {
deQueue.pollFirst();
}
// 从第size-1个开始,每个都要加一个进list
if (i >= size - 1) {
list.add(num[deQueue.peekFirst()]);
}
}
return list;
}
65. 矩阵中的路径
题:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
思路:
1)递归。
2) 栈.
1.DFS深度优先,
进:peek一次,str的位子index++,对应位子visited[i+j*rows]=true,
并且把周围合适的点(上下左右&&字符匹配&&未遍历)加入到stack中
退:当前遍历过,复位:visited设为false,并且s.pop移除当前元素,str的位置减一
2.如果str匹配成功就返回true
注意:
1) 每次可以递归的一般需要重新写一个携带参数更多的helper
2) 由于不能重复走格子,需要定义一个和matrix大小相同的boolean数组
3) 非递归的时候,一定要防止死循环(a是b左边一个,这一次就是a的右边是b,所以b进栈,如果下一个刚好又是a,然后a又进栈)
代码:
1) 递归
public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
{
if (matrix == null || str.length > matrix.length || rows <= 0 || cols <= 0) {
return false;
}
boolean[] isPassed = new boolean[matrix.length];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (helper(matrix,rows,cols,i,j,str,isPassed,0)){
return true;
}
}
}
return false;
}
private boolean helper(char[] matrix, int rows, int cols, int i, int j, char[] str, boolean[] isPassed, int start) {
int index = i * cols + j;
if (i < 0 || i >= rows || j < 0 || j >= cols || matrix[index] != str[start] || isPassed[index] == true) {
return false;
}
if (start == str.length-1) {
return true;
}
isPassed[index] = true;
if (helper(matrix,rows,cols,i+1,j,str,isPassed,start+1) ||
helper(matrix,rows,cols,i-1,j,str,isPassed,start+1) ||
helper(matrix,rows,cols,i,j+1,str,isPassed,start+1) ||
helper(matrix,rows,cols,i,j-1,str,isPassed,start+1)) {
return true;
}
isPassed[index] = false;
return false;
}
2) 非递归DFS
public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
{
if (matrix == null || str.length > matrix.length || rows <= 0 || cols <= 0) {
return false;
}
boolean[] isPassed = new boolean[matrix.length];
for (int i = 0; i < rows; i++) {
for (int j = 0;j < cols; j++) {
// int index = i * cols + j;
if (dfsHelper(matrix,rows,cols,i,j,isPassed,str)){
return true;
}
}
}
return false;
}
private static int[] x = {0,1,0,-1};//顺时针,第一个是原来的基础上加[0,1],即右,所以四个分别代表 右、下、左、上
private static int[] y = {1,0,-1,0};//顺时针
private boolean dfsHelper(char[] matrix, int rows, int cols, int i, int j, boolean[] isPassed, char[] str) {
int indexMatrix = i * cols + j;
int indexStr = 0;
if (matrix[indexMatrix] != str[indexStr]) {
return false;
}
// 在matrix中的下标
Stack<Integer> stack = new Stack<>();
stack.push(indexMatrix);
while (!stack.isEmpty()) {
int loc = stack.peek();
// 之前访问过,需要回溯
if (isPassed[loc] == true) {
isPassed[loc] = false;
stack.pop();
indexStr--;
if (indexStr < 0) {
return false;
}
continue;
}
else {
isPassed[loc] = true;
if (indexStr == str.length-1) {
return true;
}
indexStr++;
for (int k = 0 ;k < 4; k++) {
int rowCur = loc / cols + x[k];
int colCur = loc % cols + y[k];
int indexCur = rowCur * cols + colCur;
if (rowCur >= 0 && rowCur < rows && colCur >= 0 && colCur < cols && matrix[indexCur] == str[indexStr]
&& isPassed[indexCur] == false) { // 为了防止循环,本来就是上一步的右边进栈,然后下一次遍历下一个的时候会这一个的左边(即刚刚的那个)
stack.push(indexCur);
}
}
}
}
return false;
}
66. 机器人的运动范围
题:地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
思路:
1)递归
2) DFS+栈
不带记忆的DFS搜索 + 限定条件 = 普通的DSF例题
入栈条件:
1.每一位的和小于等于threshold:
2.x和 y 的边界条件
3.没有遍历过
注意:
1)非递归DFS方法和上一题的区别:严格来说这一题不算是回溯,所以直接可以pop,因为只需要记录个数,不用找出路径,所以每次遍历该节点以及其全部上下左右节点;而上一题需要记录路径,所以要严格回溯,只能peek;
代码:
1) 递归
public int movingCount(int threshold, int rows, int cols)
{
if (rows <= 0 || cols <= 0) {
return 0;
}
boolean[][] visited = new boolean[rows][cols];
return helper(threshold,rows,cols,0,0,visited);
}
private int helper(int threshold, int rows, int cols, int i, int j, boolean[][] visited) {
if (i < 0 || j < 0 || i >= rows || j >= cols || visited[i][j] == true || numSum(i) + numSum(j) > threshold) {
return 0;
}
visited[i][j] = true;
return 1 + helper(threshold,rows,cols,i+1,j,visited) + helper(threshold,rows,cols,i-1,j,visited)
+ helper(threshold,rows,cols,i,j-1,visited) + helper(threshold,rows,cols,i,j+1,visited);
}
/**
* 计算各位之和
* @param num
* @return
*/
private int numSum(int num) {
int sum = 0;
while (num > 0) {
sum += num % 10;
num /= 10;
}
return sum;
}
2) 非递归
public int movingCount(int threshold, int rows, int cols)
{
if (threshold<0 || rows<=0 || cols<=0) //robust
return 0;
boolean[][] visited = new boolean[rows][cols];
Stack<Integer> stack = new Stack<>();
// int[][] xoy = {{1,0,-1,0},{0,-1,0,1}}; //顺时针
int[] x = {0,1,0,-1};//顺时针,第一个是原来的基础上加[0,1],即右,所以四个分别代表 右、下、左、上
int[] y = {1,0,-1,0};
int count = 0;
stack.push(0);
visited[0][0] = true;
while (!stack.isEmpty()) {
int index = stack.pop();
count++;
for (int k = 0; k < 4; k++) {
int rowCur = index / cols + x[k];
int colCur = index % cols + y[k];
if (rowCur >= 0 && rowCur < rows && colCur >= 0 && colCur < cols
&& visited[rowCur][colCur] == false && numSum(rowCur)+numSum(colCur) <= threshold) {
stack.push(rowCur*cols+colCur);
visited[rowCur][colCur] = true;
}
}
}
return count;
}
private int numSum(int num) {
int sum = 0;
while (num > 0) {
sum += num % 10;
num /= 10;
}
return sum;
}
67. 剪绳子
题:给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],...,k[m]。请问k[0]xk[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
思路:
动态规划
重点:构建dp方程,最后返回dp[target]
dp[]的大小一定是target+1(确保最后的下标和数字一样)
1) 首先判断k[0]到k[m]可能有哪些数字,实际上只可能是2或者3。(因为更大的总能再被分成2和3)
4(2+2), 5(2+3), 6(3+3), 7(2+2+3), 9(4+5 = 2+2+2+3)...
动态规划,先自上而下分析,在长度为n的绳子所求为f(n),剪下一刀后剩下的两段长度是i和n-i,在这个上面还可能继续减(子问题)。
n = 1,0;n=2, 1; n = 3, 2;
当n>=4时,可以分为更小的;
dp[1] = 1, dp[2] = 2, dp[3] = 3;
dp[n]=max(dp[i]*dp[n-i]) n>=4
2) 复杂度O(logn)(主要是pow运算复杂度)
看2和3的数量,2的数量肯定小于3个,为什么呢?因为2*2*2<3*3,那么题目就简单了。
直接用n除以3,根据得到的余数判断是一个2还是两个2还是没有2就行了
代码:
1)
public int cutRope(int target) {
// 基本
if (target == 2) {
return 1;
}
if (target == 3) {
return 2;
}
int[] dp = new int[target+1];
// 以下是target>=4的情况,大于=4可以分成2和3,但是这时候2和3不再分就是最大的情况
// 跟n<=3不同,4可以分很多段,比如分成1、3,
// 这里的3可以不需要再分了,因为3分段最大才2,不分就是3。记录最大的。
dp[1] = 0;
dp[2] = 2;
dp[3] = 3;
int max = 0;
// 当n大于等于5时,我们尽可能多的剪长度为3的绳子;
// 当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。
for (int i = 4; i <= target;i++) {
max = 0;
for (int j = 1; j <= i/2; j++) {
max = Max(max,dp[j]*dp[i-j]);
}
dp[i] = max;
}
return dp[target];
}
private int Max(int i, int j) {
return (i >= j) ? i : j;
}
2)
public int cutRope(int target) {
if (target == 2) {
return 1;
}
if (target == 3) {
return 2;
}
int x = target % 3;//取值0,1,2
int y = target / 3;
if (x == 0) {
// 没有2
return (int)Math.pow(3,y);
}
else if (x == 1) {
// 没有2,但是余数1和一个3可以组成4,2*2>1*3;所以采用两个2的方式
return (int) (2 * 2 * Math.pow(3,y-1));
}
else {
// 一个2
return (int) (2 * Math.pow(3,y));
}
}