【算法】---- Leetcode剑指offer-Java版题解

这篇博客详细解析了LeetCode中的多个算法题目,包括数组、链表、二叉树等数据结构的操作,如查找重复数字、重建二叉树、实现队列、计算斐波那契数列等,并涵盖了一些特定问题的解法,如正则表达式匹配、数据流中的中位数等。
摘要由CSDN通过智能技术生成

剑指offer题解

1. 数组中重复的数字

/**
	 * 思路: 原地哈希,将nums[i]不断与nums[nums[i]]交换,直到nums[i] = i为止
	 *      如果在交换过程中出现nums[nums[i]]位置已经有元素了,那么就返回结果nums[i]
	 * 时间复杂度: O(n)
	 * 空间复杂度: O(1)
	 */
	public int findRepeatNumber(int[] nums) {
		for (int i = 0; i < nums.length; i++) {
			while(nums[i] != i) {
				if(nums[nums[i]] == nums[i]) {
					return nums[i];
				}
				int temp = nums[i];
				nums[i] = nums[nums[i]];
				nums[temp] = temp;
			}
		}
		return -1;
	}

2. 二维数组中的查找

// 暴力解法,时间复杂度O(n*m) 空间复杂度O(1)
	/*public boolean findNumberIn2DArray(int[][] matrix, int target) {
		for (int i = 0; i < matrix.length; i++) {
			for (int j = 0; j < matrix[i].length; j++) {
				if(matrix[i][j] == target) {
					return true;
				}
			}
		}

		return false;
	}*/

	/**
	 * 思路: 二分查找
	 * 枚举每行的最后一个元素x,进行二分查找
	 * 如果 x == target, 则直接返回
	 * 如果 x > target, 则排除当前整一列(纵坐标减1)
	 * 如果 x < target, 则排除当前整一行(横坐标加1)
	 *
	 * 时间复杂度: O(n+m)
	 * 空间复杂度: O(1)
	 */
	public boolean findNumberIn2DArray(int[][] matrix, int target) {
		if(matrix == null || matrix.length == 0) {
			return false;
		}

		int i = 0, j = matrix[0].length-1;
		while(i <= matrix.length && j >= 0) {
			if(matrix[i][j] == target) {
				return true;
			} else if(matrix[i][j] > target) {
				j--;
			} else {
				i++;
			}
		}

		return false;
	}

3. 替换空格

/**
 * 思路: 遍历字符串,遇到空格,则添加%20
 * 时间: O(n)
 * 额外空间: O(n)
 */
public String replaceSpace(String s) {
   StringBuilder res = new StringBuilder();
   for(int i = 0; i < s.length(); i++) {
      if(s.charAt(i) == ' ') {
         res.append("%20");
      } else {
         res.append(s.charAt(i));
      }
   }
   return res.toString();
}

4. 从尾到头打印链表

/*
	 * 思路: 用栈存储遍历得到的数据,然后再依次出栈添加到结果数组中
	 * 时间: O(n)
	 * 空间:O(n)
	 */
	/*public int[] reversePrint(ListNode head) {
		Stack<Integer> stack = new Stack<>();
		while(head != null) {
			stack.push(head.val);
			head = head.next;
		}
		int[] res = new int[stack.size()];
		int idx = 0;
		while(!stack.isEmpty()) {
			res[idx++] = stack.pop();
		}

		return res;
	}*/

	/**
	 * 思路: 两次遍历链表
	 * 时间: O(n)
	 * 空间: O(1)
	 */
	public int[] reversePrint(ListNode head) {
		int count = 0;
		ListNode node = head;

		while(node != null) {
			count++;
			node = node.next;
		}

		int[] res = new int[count];
		node = head;

		for (int i = count-1; i >= 0; i--) {
			res[i] = node.val;
			node = node.next;
		}

		return res;
	}

5. 重建二叉树

/**
	 *  思路: 递归
	 *  1. 取前序遍历的第一个元素作为根节点
	 *  2. 先切割中序数组,根据根节点查找中序数组中的位置,切割成中序左数组[i_start,i_root_index)和中序右数组[i_root_index+1,i_end)   【左闭右开】
	 *  3. 再切割前序数组,根据中序数组中左数组的长度进行切割,切割成前序左数组[p_start+1, p_start+1+(i_root_index-i_start))和前序右数组[p_start+1+(i_root_index-i_start),p_end)
	 */

	// 存储前序遍历中的根节点在中序遍历的下标位置
	HashMap<Integer, Integer> map = new HashMap<>();
	public TreeNode buildTree(int[] preorder, int[] inorder) {

		// 初始化赋值
		for (int i = 0; i < inorder.length; i++) {
			map.put(inorder[i], i);
		}

		// 左闭右开区间
		return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length);
	}

	public TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) {
		// 递归终止条件,前序数组遍历完,直接返回null
		if(p_start == p_end) {
			return null;
		}

		int root_val = preorder[p_start];
		// 中序数组中根节点的下标位置, 根据此下标划分中序数组中的 中序左数组 和 中序右数组
		int i_root_index = map.get(root_val);
		// 构建根节点
		TreeNode root = new TreeNode(root_val);
		// 递归构造左子树
		root.left = buildTreeHelper(preorder, p_start+1, p_start + 1 + (i_root_index-i_start), inorder, i_start, i_root_index);
		// 递归构造右子树
		root.right = buildTreeHelper(preorder, p_start+1+(i_root_index-i_start), p_end, inorder, i_root_index+1, i_end);

		return root;
	}

6. 用两个栈实现队列

Stack<Integer> pushStack;
	Stack<Integer> popStack;

	public _剑指_Offer_09_用两个栈实现队列() {
		this.pushStack = new Stack<>();
		this.popStack  = new Stack<>();
	}

	public void appendTail(int value) {
		pushStack.add(value);
	}

	public int deleteHead() {
		// 弹出元素的栈中仍有元素,则返回出栈元素
		if(popStack.size() > 0) {
			return popStack.pop();
		}

		// 弹出元素的栈为空,入栈元素的栈也为空,返回-1
		if(popStack.size() == 0) return -1;

		// 入栈元素的栈为空,则不断从出栈的栈弹出元素
		while(pushStack.size() > 0) {
			popStack.add(pushStack.pop());
		}

		return popStack.pop();
	}

7. I_斐波那契数列

public int fib(int n) {
		if(n < 2) {
			return n;
		}
		int a = 0;
		int b = 1;
		for(int i = 2; i <= n; i++) {
			int sum = (a + b) % 1000000007;
			a = b;
			b = sum;
		}
		return b;
	}

8. II_青蛙跳台阶问题

/**
	 * 思路: 动态规划
	 * 1. dp[i] = dp[i-1] + dp[i-2]
	 * 2. 由于dp[i]只与前面两项相关,所以用三个变量sum,a,b记录,优化空间,降为O(1)
	 *
	 * 时间:O(n)
	 * 空间:O(1)
	 */
	public int numWays(int n) {
		if(n < 2) {
			return 1;
		}

		int a = 1;
		int b = 1;
		int sum = 0;
		for(int i = 2; i <= n; i++) {
			sum = (a + b) % 1000000007;
			a = b;
			b = sum;
		}

		return b;
	}

9. 旋转数组的最小数字

/**
	 * 思路: 二分查找
	 * 将中间元素mid和右边元素right相比较,缩小搜索的范围
	 * numbers[mid] < numbers[right], 则 right = mid
	 * numbers[mid] > numbers[right], 则 left = mid + 1
	 * numbers[mid] = numbers[right], 则 right--
	 */
	public int minArray(int[] numbers) {
		int left = 0;
		int right = numbers.length-1;
		while(left < right) {
			int mid = (left + right) >>> 1;
			if(numbers[mid] < numbers[right]) {
				right = mid;
			} else if(numbers[mid] > numbers[right]) {
				left = mid + 1;
			} else {
				right--;
			}
		}

		return numbers[left];
	}

10. 矩阵中的路径

/**
	 *
	 * 思路: 深度优先搜索
	 *  1. 枚举矩阵中每个位置
	 *  2. 然后从当前位置开始深度优先搜索,从上下左右四个方向开始搜索
	 *  3. 过程中把遍历过的位置设置为一个特殊的标志符
	 * 时间复杂度: O(MN * k^3)
	 * 		需要枚举MN个起点,时间复杂度为(MN)
	 * 		方案数计算: 设字符串长度为 K ,搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头(上个字符)的方向,剩下 3 种选择,因此方案数的复杂度为 O(3^K)
	 * 空间复杂度: O(K)
	 * 		最差K=MN
	 */
	int[] dx = {-1, 0, 1, 0}; // 上右下左
	int[] dy = {0, 1, 0, -1};
	public boolean hasPath (char[][] matrix, String word) {
		if(matrix == null || matrix.length == 0) {
			return false;
		}
		// 遍历矩阵中的每个元素,判断路径是否可达
		for(int i = 0; i< matrix.length; i++) {
			for(int j = 0; j < matrix[0].length; j++) {
				if(dfs(matrix, word, i, j, 0)) {
					return true;
				}
			}
		}

		return false;
	}

	private boolean dfs(char[][] matrix, String word, int x, int y, int k) {
		// 不满足
		if(matrix[x][y] != word.charAt(k)) {
			return false;
		}
		if(k == word.length() - 1) {
			return true;
		}
		char temp = matrix[x][y];
		matrix[x][y] = '/';
		// 上下左右四个方向
		for(int i = 0; i < 4; i++) {
			int a = x + dx[i];
			int b = y + dy[i];
			if(a >= 0 && a < matrix.length && b >= 0 && b < matrix[0].length) {
				if(dfs(matrix, word, a, b, k+1)) {
					return true;
				}
			}
		}
		// 若不满足,则进行回溯
		matrix[x][y] = temp;
		return false;
	}

11. 机器人的运动范围

/**
	 *  思路: 深度优先搜索
	 *
	 *  1. 设置一个visited布尔数组记录已经被访问过的节点
	 *  2. 从(0,0)位置开始出发,因此只会往右和往下两个方向走
	 *  3. 设置一个全局变量res,记录dfs过程中满足条件的位置
	 *
	 *  时间: O(n*m)
	 *  空间: O(n*m)
	 */
	/*int res = 0;
	int[] dx = {0,1}; // 向右 和 向下
	int[] dy = {1,0};
	public int movingCount(int m, int n, int k) {
		if(m <= 0 || n <= 0 || k < 0) {
			return 0;
		}
		boolean[][] visited = new boolean[m][n];
		dfs(visited, 0, 0, m, n, k);
		return res;
	}

	private void dfs(boolean[][] visited, int x, int y, int m, int n, int k) {
		// 已被遍历过 返回
		if(visited[x][y]) {
			return;
		}
		visited[x][y] = true;
		res++;
		for (int i = 0; i < 2; i++) {
			int a = x + dx[i];
			int b = y + dy[i];
			// 满足条件向右和向下搜索
			if(a >= 0 && a < m && b >= 0 && b < n && digitSum(a) + digitSum(b) <= k) {
				dfs(visited, a, b, m, n, k);
			}
		}
	}

	private int digitSum(int x) {
		int sum = 0;
		while(x > 0) {
			sum += x % 10;
			x /= 10;
		}
		return sum;
	}*/


	/**
	 *  思路: 宽度优先搜索(bfs)
	 *  1. 创建visited[][]数组记录已被访问过的节点
	 *  2. 队列中存放可被访问的位置,每次弹出一个位置坐标(x,y),判断是否被访问过,
	 *  如果被访问过,则continue(易错点,应该在这里),并将其下方和右下方可以被访问的位置入队
	 *  3. 全局变量res记录队列中可以到达的格子数量
	 *  4. 队列为空结束循环,返回res
	 *
	 *  时间: O(n*m)
	 *  空间: O(n*m)
	 */
	public int movingCount(int m, int n, int k) {
		if(m <= 0 || n <= 0 || k < 0) {
			return 0;
		}
		int res = 0;
		boolean[][] visited = new boolean[m][n];
		// 存储每个节点的横纵坐标
		Queue<int[]> queue = new LinkedList<>();
		queue.add(new int[]{0,0});
		int[] dx = {0,1};
		int[] dy = {1,0};
		while(!queue.isEmpty()) {
			int[] x = queue.poll();
			if(visited[x[0]][x[1]]) {
				continue;
			}
			res++;
			visited[x[0]][x[1]] = true;
			for(int i = 0; i < 2; i++) {
				int a = x[0] + dx[i];
				int b = x[1] + dy[i];
				if(a >= 0 && a < m && b >= 0 && b < n && digitSum(a) + digitSum(b) <= k) {
					queue.add(new int[]{a,b});
				}
			}
		}

		return res;
	}

	private int digitSum(int x) {
		int sum = 0;
		while(x > 0) {
			sum += x % 10;
			x /= 10;
		}
		return sum;
	}




	// 如果给的数组是一维数组,需要将二维数组转化为一维数组
	// 深度优先搜索
     /*int res = 0;
     int[] dx = {0, 1}; // 右下
     int[] dy = {1, 0};
     public int movingCount(int threshold, int rows, int cols) {
         if(rows <= 0 || cols <= 0 || threshold < 0) return 0;
         // 一维代替二维
         boolean[] visited = new boolean[rows * cols];
         dfs(visited, rows, cols, 0, 0, threshold);
         return res;
     }

     public void dfs(boolean[] visited, int rows, int cols, int row, int col, int threshold) {
         // 不满足条件
         int index = row * cols + col;
         visited[index] = true;
         res++;
         for(int i = 0; i < 2; i++) {
             int a = row + dx[i]; // 下一个位置的横坐标
             int b = col + dy[i]; // 下一个位置的纵坐标
             if(a >= 0 && a < rows && b >= 0 && b < cols && !visited[a * cols + b] && digitSum(a) + digitSum(b) <= threshold) {
                 dfs(visited, rows, cols, a, b, threshold);
             }
         }
     }

     private int digitSum(int x) {
         int sum = 0;
         while(x > 0) {
             sum += x % 10;
             x /= 10;
         }
         return sum;
     }*/

	// 宽度优先搜索
	/*public int movingCount(int threshold, int rows, int cols) {
		if(rows <= 0 || cols <= 0 || threshold < 0) {
			return 0;
		}
		int res = 0;
		// 存储每个点的x,y轴
		Queue<int[]> queue = new LinkedList<>();
		queue.add(new int[]{0,0});
		// 方向
		int[] dx = {0, 1};// 右 下
		int[] dy = {1, 0};
		// 访问过的标志
		boolean[] visited = new boolean[rows * cols];
		while(queue.size() > 0) {
			int[] x = queue.poll();
			if(visited[x[0] * cols + x[1]]) {
				continue;
			}
			visited[x[0] * cols + x[1]] = true;
			res++;
			for(int i = 0; i < 2; i++) {
				int a = x[0] + dx[i];
				int b = x[1] + dy[i];
				// 条件满足才能入队
				if(a >= 0 && a < rows && b >= 0 && b < cols && digitSum(a) + digitSum(b) <= threshold) {
					queue.add(new int[]{a,b});
				}
			}
		}

		return res;
	}

	private int digitSum(int x) {
		int sum = 0;
		while(x > 0) {
			sum += x % 10;
			x /= 10;
		}
		return sum;
	}*/

12. I_剪绳子

/**
	 *  思路: 动态规划
	 *  1. 定义dp数组以及下标含义
	 *  	dp[i]:表示长度为i剪成m段后的最大乘积
	 *  2. 确定递推公式
	 *  	先把绳子剪掉第一段(长度为j),如果只剪长度为1,对最后乘积无益处,所以从长度为2开始剪
	 *  	剪了第一段后,剩下(i-j)长度可以剪可以不剪,取两者最大值, max(j*(i-j), j * dp[i-j])
	 *  	第一段长度区间可以取值范围区间为[2,i),对所有j不同的情况取最大值
	 *  	最终dp[i]的转移方程为: dp[i] = max(dp[i],max(j*(i-j),j*dp[i-j]))
	 *  3. 初始化
	 *  	dp[2] = 1
	 *  4. 最后返回dp[n]即可
	 *
	 *  时间复杂度:O(n^2)
	 *  空间复杂度:O(n)
	 */
	/*public int cuttingRope(int n) {
		int[] dp = new int[n+1];

		dp[2] = 1;
		*//*for (int i = 3; i <= n; i++) {
			for (int j = 2; j < i; j++) {
				dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j]));
			}
		}*//*
		// 优化
		// i=3时,是特殊情况,要单独初始化
		if(n >= 3) {
			dp[3] = 2;
		}
		for (int i = 3; i <= n; i++) {
			for (int j = 2; j < i/2+1; j++) {
				dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j]));
			}
		}

		return dp[n];
	}*/

	/**
	 *	 思路2:  贪心
	 *	 尽可能将绳子长度分为长度为3的小段,这样乘积最大
	 *
	 *	 1. 当n=2时,返回1;当n=3时,返回2; 两个合并,n < 4时, return n-1
	 *	 2. 当n=4时,返回4
	 *	 3. 当n>4时,尽可能分为长度为3,累乘
	 *	 2和3步可以合并
	 */
	public int cuttingRope(int n) {
		if(n < 4) {
			return n-1;
		}
		int res = 1;
		while(n > 4) {
			res *= 3;
			n -= 3;
		}
		return res * n;
	}

13. II_剪绳子

/**
 * 思路: 动态规划
 * 大数取余
 * 1. 确定dp函数以及下标含义 dp[i]表示长度为i的最大乘积
 * 2. 确定递推公式
 *        先切割第一段长度为j,j的取值范围为[2,i)
 *        第二段长度为i-j可切,可不切中选择最大的
 *        dp[i] = Math.max(dp[i], Math.max(j * dp[i-j], j * (i-j)))
 */
/*public int cuttingRope(int n) {
   BigInteger[] dp = new BigInteger[n+1];
   // 初始化
   Arrays.fill(dp, BigInteger.valueOf(1));
   dp[2] = BigInteger.valueOf(1);

   for (int i = 3; i <= n; i++) {
      for (int j = 2; j < i; j++) {
         dp[i] = dp[i].max(BigInteger.valueOf(j).multiply(dp[i-j]).max( BigInteger.valueOf(j * (i-j))));
      }
   }

   return dp[n].mod(BigInteger.valueOf(1000000007)).intValue();
}*/

/**
 * 思路: 贪心
 * 尽可能将绳子分成长度为3的小段,这样乘积最大
 *
 */
public int cuttingRope(int n) {
   if(n < 4) {
      return n-1;
   }
   long res = 1;
   while(n > 4) {
      res = res * 3 % 1000000007;
      n -= 3;
   }

   return (int) (res * n % 1000000007);
}

14. 二进制中1的个数

/*public int hammingWeight(int n) {
		int res = 0;
		// 注意负数的情况
		while(n != 0){
			if((n & 1) == 1) {
				res++;
			}
			n >>= 1;
		}
		return res;
	}*/

	/**
	 * 思路: n & (n-1)每次都会吧最后一个1变成0
	 */
	public int hammingWeight(int n) {
		int res = 0;
		while(n != 0) {
			n = n & (n-1);
			res++;
		}
		return res;
	}

15. 数值的整数次方

/**
 *  思路: 快速幂(递归)
 *  1. 如果n == 0,返回1
 *  2. 如果n < 0,最终结果为  1 / (x * myPow(x, -n-1));  即 1/x^(-n)
 *  3. 如果n为奇数,最终结果为 x * myPow(x, n-1);  即x * x^(n-1)
 *  4. 如果n为偶数,最终结果为 myPow(x * x, n >> 1); 即 (x^2)^(n/2)
 *
 *  因为有n=Integer.MIN_VALUE的存在,取反后会溢出,所以提取一个x出来,这样就不会溢出了
 */
/*     public double myPow(double x, int n) {
      if(n == 0) {
         return 1;
      }
      if(n < 0) {
         // 如果x = Integer.MIN_VALUE -2147483648,则取反还是本身
//       return myPow(1/x, -n);

         return 1 / (x * myPow(x, -n-1));
      }
      // 偶数
      if((n & 1) == 0) {
         return myPow(x * x, n >> 1);
      } else {
         // 奇数
         return x * myPow(x, n-1);
      }
   }*/

/**
 *  思路: 快速幂(递归)
 *  1. 提前对n为Integer.MIN_VALUE作判断,如果x==±1,那么返回1,否则返回0
 *  2. 如果n == 0,返回1
 *  3. 如果n < 0,最终结果为  myPow(1/x, -n);  即 (1/x)^(-n)
 *  4. 如果n为奇数,最终结果为 x * myPow(x, n-1);  即x * x^(n-1)
 *  5. 如果n为偶数,最终结果为 myPow(x * x, n >> 1); 即 (x^2)^(n/2)
 *
 *  因为有n=Integer.MIN_VALUE的存在,取反后会溢出,所以提取一个x出来,这样就不会溢出了
 */
public double myPow(double x, int n) {

   // 除了1和-1都是为1外,其余都是0
   if(n == Integer.MIN_VALUE) {
      return (x == 1 || x == -1) ? 1 : 0;
   }

   if(n == 0) {
      return 1;
   }

   if(n < 0) {
      // 如果x = Integer.MIN_VALUE -2147483648,则取反还是本身,除了第一种方案往外提出一个数外,还可以单独对这个条件判断
      return myPow(1/x, -n);
   }

   // 偶数
   if((n & 1) == 0) {
      return myPow(x * x, n >> 1);
   } else {
      // 奇数
      return x * myPow(x, n-1);
   }
}

16. 打印从1到最大的n位数

/**
 *  思路: 如果String[], 则大数的话,要全排列
 *
 *  由于返回值是int,为了测试通过,最后把字符串变成int,其实应该返回字符串数组
 *
 *  1. 为了避免数字开头0出现,先把首位first固定,first取值范围是1~9
 *  2. 用digit表示要生成的数字的位数,从1位数一值生成n位数,对每种数字的位数都生成下一个首位,所以有个双重for循环,
 *   生成首位之后进入递归生成剩下的digit-1位数,从0~9中取值
 *  3. 递归终止条件是已生成了digit位的数字,即index=digit,将此时的数num转为int加到结果res中
 *        比如n=2,  digit = 1,2 当digit=1时,先固定首位,首位范围是1~9
 *                            当digit=2时,先固定首位(1~9),那么第二位对每种数字生成(0~9)
 * 时间复杂度: O(10^n) -> 1~10^n-1都遍历了一遍
 * 额外空间: O(n)   -> num数组和递归栈
 */

public static void main(String[] args) {
   _剑指_Offer_17_打印从1到最大的n位数 a = new _剑指_Offer_17_打印从1到最大的n位数();
   a.printNumbers(1);
}

int[] res;
int count = 0;
public int[] printNumbers(int n) {
   res = new int[(int)Math.pow(10,n) - 1];
   for (int digit = 1; digit <= n; digit++) {
      for (char first = '1'; first <= '9'; first++) {
         char[] num = new char[digit];
         num[0] = first;
         // 生成首位之后进入递归生成剩下的digit-1位数,从0~9中取值
         dfs(1, num, digit);
      }
   }

   return res;
}

private void dfs(int index, char[] num, int digit) {
   // 递归终止条件
   if(index == digit) {
      res[count++] = Integer.parseInt(String.valueOf(num));
      return;
   }

   // 从0~9中取值
   for (char i = '0'; i <= '9'; i++) {
      num[index] = i;
      dfs(index + 1, num, digit);
   }
}

17. 删除链表的节点

/**
 *  思路: 设置一个哑节点
 *  1. 设置一个哑节点dummy,新链表的头结点为dummy.next
 *  2. 初始化两个节点pre = dummy, cur = head;
 *  3. cur!=val时,一直移动两个指针,pre=cur, cur=cur.next
 *  4. cur==val时,pre.next = cur.next;
 *  5. 返回新链表头节点
 *
 *  时间:O(n)
 *  空间:O(1)
 */
public ListNode deleteNode(ListNode head, int val) {
   ListNode dummy = new ListNode();
   dummy.next = head;
   ListNode pre = dummy;
   ListNode cur = head;
   while(cur.val != val) {
      pre = cur;
      cur = cur.next;
   }
   return dummy.next;
}

18. 正则表达式匹配

/**
	 *  思路: 动态规划
	 *  1. dp[i][j]:表示s的前i个和p的前j个是否匹配
	 *  2. 手动求二维矩阵的每个值,通过计算可以发现:
	 *  	- 第0列,除了dp[0][0]=true,其余dp[i][0]=false
	 *  	- s从0开始算,p从1开始算
	 *  	- 过程中考虑dp[i][j]由哪个值得来
	 *  3. 需要考虑p的当前字符p[j]
	 *  	a. 当前字符是字母
	 *  	b. 当前字符是'.'
	 * 			由a和b判断两个字符是否匹配
	 * 			字符匹配: s[i] == p[j] || p[j] == '.'
	 * 			字符不匹配: s[i] != p[j]
 * 		   c. 当前字符是'*'
	 * 		   	'*'表示前面字符可以出现0次或者出现多次
	 * 		   	(注意j从2开始)
	 * 		   	前面字符与s[i]匹配时,则使用'*': 代表前面字符出现一次或者多次: s当前位置可以由s的前i-1和p的前j个是否可以匹配,即dp[i][j] = dp[i-1][j]
	 * 		   	前面字符与s[i]不匹配时,则不使用'*': 代表前面字符出现0次: s当前位置可以由s的前i和p的前j-2个是否可以匹配,即dp[i][j] = dp[i][j-2]
	 * 4. 初始化
	 * 	  dp[0][0] = true
	 * 	  初始化第0行,为了后面方便计算,所以当遇到'*'且j>=2时,则dp[0][j] = dp[0][j-2]
	 * 	  因为*前面的字符可以出现0次或者多次,匹配第一行,s为空,所以这里出现0次的话,则匹配j-2就可以
	 *
	 * 时间: O(n*m)
	 * 空间: O(n*m)
	 */
	public static boolean isMatch(String s, String p) {
		if(s == null && p == null) {
			return true;
		}
		if(p == null) {
			return false;
		}
		int m = s.length();
		int n = p.length();
		boolean[][] dp = new boolean[m+1][n+1];

		// 初始化
		dp[0][0] = true;
		for (int j = 2; j <= n; j++) {
			if(p.charAt(j-1) == '*') {
				dp[0][j] = dp[0][j-2];
			}
		}

		for (int i = 1; i <= m; i++) {
			char charS = s.charAt(i-1);
			for (int j = 1; j <= n; j++) {
				char charP = p.charAt(j-1);
				if(charP != '*') {
					if(charS == charP || charP == '.') {
						dp[i][j] = dp[i-1][j-1];
					}
				} else {
					// 不使用'*': 代表前面字符出现0次
					if(j >= 2) {
						dp[i][j] = dp[i][j-2];
					}
					// 使用'*': 代表当前位置s[i]匹配一次,然后匹配剩余的s[i-1]是否与p[j]匹配即可
					if(j >= 2 && (charS == p.charAt(j-2) || p.charAt(j-2) == '.')) {
						dp[i][j] = dp[i-1][j];
					}
				}
			}
		}

		return dp[m][n];
	}

19. 表示数值的字符串

/**
 * 思路: 判断否false而不是判断true,只要有一个条件不满足就可以判断false
 *
 * 1. 定义四个flag,对应四种字符
 *        - 是否有符号: hasSign
 *        - 是否有数字: hasNum
 *        - 是否有点: hasDot
 *        - 是否有e: hasE
 *     2. 还需要定义长度n和索引index
 *     3. 先处理开头空格,index后移
 *     4. 进入循环,遍历字符串
 *        - 当前字符c是'+'或'-': 如果已经出现过符号、出现过点、出现过数字,则返回false,否则,令hasSign = true
 *        - 当前字符c是数字: 令hasNum=true,一直移动index,直到出现非数字或者遍历到末尾,如果已遍历到末尾,返回true
 *        - 当前字符c是'.': 如果已经出现过'.'、出现过E(e),则返回false,否则,hasDot = true
 *        - 当前字符c是e或E: 如果已经出现e或者e之前没有出现过数字,则返回false,否则hasE = true,并将其它三个符号位设置为false,因为要开始遍历e后面的数字了
 *     5. 处理空格,index相应的后移
 *     6. 如果当前index与字符串长度相等,说明到达了末尾,还要满足hasNum为true才能最终返回true
 *
 *
 *  时间: O(n)
 *  空间: O(1)
 */
public boolean isNumber(String s) {
   if(s == null || s.length() == 0) {
      return false;
   }
   boolean hasSign = false;
   boolean hasNum = false;
   boolean hasDot = false;
   boolean hasE = false;
   int index = 0;
   int n = s.length();

   // 先处理空格
   while(index < n && s.charAt(index) == ' ') {
      index++;
   }

   while(index < n) {
      // 当前字符c是数字
      while(index < n && s.charAt(index) >= '0' && s.charAt(index) <= '9') {
         index++;
         hasNum = true;
      }
      if(index == n) {
         break;
      }
      char c = s.charAt(index);
      // 当前字符c是'+'或'-'
      if(c == '+' || c == '-') {
         if(hasSign || hasDot || hasNum) {
            return false;
         }
         hasSign = true;
      } else if(c == '.') {
         // 当前字符c是'.'
         if(hasDot || hasE) {
            return false;
         }
         hasDot = true;
      } else if(c == 'e' || c == 'E') {
         // 当前字符是'e'或'E'
         if(hasE || !hasNum) {
            return false;
         }
         hasE = true;
         // 开始遍历e后面的新数字
         hasNum = false;
         hasDot = false;
         hasSign = false;
      } else if(c == ' ') {
         // 结束当前循环,继续判断循环外的情况
         break;
      } else {
         // 出现其它字符,返回false
         return false;
      }
      index++;
   }

   while(index < n && s.charAt(index) == ' ') {
      index++;
   }
   return index == n && hasNum;
}

20. 调整数组顺序使奇数位于偶数前面

/**
 *  思路: 头尾双指针
 *  1. left指向数组头,right指向数组尾
 *  2. left右移直到遇到偶数
 *  3. right左移直到遇到奇数
 *  4. 如果此时left > right 则结束循环
 *  5. 交换left和right所指数字
 *  6. 继续以上步骤,直到left > right
 *  时间: O(n)
 *  空间: O(1)
 */
public static int[] exchange(int[] nums) {
   if(nums == null || nums.length == 0) {
      return new int[0];
   }
   int left = 0;
   int right = nums.length-1;
   while(left < right) {
      // 左边索引一直递增,直到找到偶数为止
      while(left < right && nums[left] % 2 == 1) {
         left++;
      }
      // 右边索引一直递减,直到找到奇数为止
      while(left < right && nums[right] % 2 == 0) {
         right--;
      }
      if(left > right) {
         break;
      }
      int temp = nums[left];
      nums[left] = nums[right];
      nums[right] = temp;
   }
   return nums;
}

21. 链表中倒数第k个节点

/**
	 *  思路: 快慢指针
	 *  1. 让fast指针先走k步
	 *  2. 然后while循环中fast和slow均一次走一步,直到fast为空
	 *  3. 此时slow指向倒数第k个位置元素
	 *
	 *  时间: O(n)
	 *  空间: O(1)
	 */
	public ListNode getKthFromEnd(ListNode head, int k) {
		if(head == null || head.next == null) {
			return head;
		}
		ListNode fast = head;
		ListNode slow = head;
		for(int i = 0; i < k; i++) {
			fast = fast.next;
		}
		while(fast != null) {
			fast = fast.next;
			slow = slow.next;
		}

		return slow;
	}

22. 反转链表

public ListNode reverseList(ListNode head) {
		if(head == null || head.next == null) {
			return head;
		}
		ListNode next = reverseList(head.next);
		head.next.next = head;
		head.next = null;
		return next;
	}

23. 合并两个排序的链表

/**
	 *  思路: 迭代
	 *  1. 设置dummy哑节点,放置新链表之前,cur为当前节点,从dummy开始
	 *  2. 当两个链表为非空时进入循环,令新链表的下一个节点cur.next为val更小的节点,相应的链表节点后移一位
	 *  3. 每次循环cur也要后移一位
	 *  4. 循环结束后还有链表非空,cur指向非空链表
	 *  5. 返回dummy.next
	 *
	 *  时间:O(n+m)
	 *  空间:O(1)
	 */
	/*public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
		if(l1 == null && l2 == null) {
			return null;
		}
		if(l1 == null) {
			return l2;
		}
		if(l2 == null) {
			return l1;
		}

		ListNode dummy = new ListNode();
		ListNode cur = dummy;
		while(l1 != null && l2 != null) {
			if(l1.val < l2.val) {
				cur.next = l1;
				l1 = l1.next;
			} else {
				cur.next = l2;
				l2 = l2.next;
			}
			cur = cur.next;
		}
		cur.next = l1 == null ? l2 : l1;
		return dummy.next;
	}*/

	/**
	 *  思路: 递归法
	 *  1. 递归终止条件: 有一个链表为空,则返回另一个链表
	 *  2. 比较两个链表头节点值,进行链表合并
	 *  时间: O(m+n)
	 *  空间: O(m+n)
	 */
	/*public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
		if(l1 == null || l2 == null) {
			return l1 == null ? l2 : l1;
		}
		if(l1.val <= l2.val) {
			l1.next = mergeTwoLists(l1.next, l2);
			return l1;
		} else {
			l2.next = mergeTwoLists(l1, l2.next);
			return l2;
		}
	}

24. 树的子结构

/**
	 *  思路: 递归法/DFS
	 *
	 *  1. 遍历树A的节点,寻找与B根节点相同的节点
	 *  2. 如果nodeA.val = nodeB.val,说明已经找到一个相同的节点,进入helper方法判断接下来的节点是否相同
	 *  	相同,则返回true,不相同则继续遍历A,找到下一个相同的节点
	 *  3. 继续遍历nodeA的左右节点,只要两者有一个为true则返回true
	 *  时间: O(n*m)  n为A树节点数量,m为B树节点数量
	 *  空间: O(m)
	 */
	/*TreeNode nodeB;
	public boolean isSubStructure(TreeNode A, TreeNode B) {
		if(B == null) {
			return false;
		}
		this.nodeB = B;
		return dfs(A);
	}

	// 找到A中与B的第一个节点相同的节点
	// 前序遍历: 根左右
	private boolean dfs(TreeNode nodeA) {
		if(nodeA == null) {
			return false;
		}

		if(nodeA.val == nodeB.val) {
			// 判断nodeA中是否存在与nodeB相同的结构
			if(helper(nodeA, nodeB)) {
				return true;
			}
		}

		return dfs(nodeA.left) || dfs(nodeA.right);
	}

	// 判断从A的子树是否有和B相同的部分
	private boolean helper(TreeNode nodeA, TreeNode nodeB) {
		// nodeB遍历完为空
		if(nodeB == null) {
			return true;
		}
		if(nodeA == null || nodeA.val != nodeB.val) {
			return false;
		}
		return helper(nodeA.left, nodeB.left) && helper(nodeA.right, nodeB.right);
	}*/

	/**
	 *  思路: BFS(广度优先搜索)
	 *
	 *  1. 先遍历树A,如果遍历到和B节点相同的节点,进入helper方法判断接下来的节点是否都相同
	 *  2. 节点都相同返回true,不相同则返回false,并且继续遍历树A找下一个相同的节点
	 *  3. 如果遍历完了A还没有返回过true,则返回false
	 *
	 *  时间: O(m*n)  m为A树总节点数,n为B数总节点数
	 *  空间: O(m) 最差情况下遍历A中所有节点入队
	 */
	public boolean isSubStructure(TreeNode A, TreeNode B) {
		if(B == null || A == null) {
			return false;
		}
		Queue<TreeNode> queue = new LinkedList<>();
		queue.offer(A);
		while(!queue.isEmpty()) {
			TreeNode node = queue.poll();
			if(node.val == B.val) {
				if(helper(node, B)) {
					return true;
				}
			}
			if(node.left != null) {
				queue.offer(node.left);
			}
			if(node.right != null) {
				queue.offer(node.right);
			}
		}
		return false;
	}

	private boolean helper(TreeNode nodeA, TreeNode nodeB) {
		Queue<TreeNode> queueA = new LinkedList<>();
		Queue<TreeNode> queueB = new LinkedList<>();
		queueA.offer(nodeA);
		queueB.offer(nodeB);
		while(!queueB.isEmpty()) {
			nodeA = queueA.poll();
			nodeB = queueB.poll();
			if(nodeA == null || nodeA.val != nodeB.val) {
				return false;
			}
			if(nodeB.left != null) {
				queueA.offer(nodeA.left);
				queueB.offer(nodeB.left);
			}
			if(nodeB.right != null) {
				queueA.offer(nodeA.right);
				queueB.offer(nodeB.right);
			}
		}

		return true;
	}

25. 二叉树的镜像

/**
 *  思路: dfs/递归
 *  后序遍历 先局部翻转后整体
 * 1. 特判: 如果root为空,则返回
 *  2. 把root的左子树放到mirrorTree中镜像一下
 *  3. 把root的右子树放到mirrorTree中镜像一下
 *  4. 交换左右子树
 *
 *  先交换还是先镜像都可以,先交换则是前序遍历,先镜像则是后序遍历
 *  时间:O(n) n为树的节点个数
 *  空间:O(h) h为树的深度
 */
/*public TreeNode mirrorTree(TreeNode root) {
   if(root == null) {
      return root;
   }
   TreeNode leftTree = mirrorTree(root.left);
   TreeNode rightTree = mirrorTree(root.right);
   root.left = rightTree;
   root.right = leftTree;
   return root;
}*/

/*public TreeNode mirrorTree(TreeNode root) {
   if(root == null) {
      return root;
   }

   root.left = mirrorTree(root.left);
   root.right = mirrorTree(root.right);
   TreeNode tmp = root.left;
   root.left = root.right;
   root.right = tmp;

   return root;
}*/

// 前序遍历 先整体后局部翻转
 /*public TreeNode mirrorTree(TreeNode root) {
   if(root == null) {
      return root;
   }
    TreeNode tmp = root.left;
   root.left = root.right;
   root.right = tmp;
   mirrorTree(root.left);
   mirrorTree(root.right);
   return root;
 }*/

/**
 *  思路: bfs
 *  层序遍历
 *  只要遍历到所有节点并且对每个节点都交换一下左右子树
 *  先交换还是先加入队列都可以,不影响最终结果
 *
 *  时间: O(n)
 *  空间: O(n)
 */
public TreeNode mirrorTree(TreeNode root) {
   if(root == null) {
      return root;
   }
   Queue<TreeNode> queue = new LinkedList<>();
   queue.offer(root);

   while(!queue.isEmpty()) {
      TreeNode node = queue.poll();
      if(node.left != null) {
         queue.offer(node.left);
      }
      if(node.right != null) {
         queue.offer(node.right);
      }
      TreeNode tmp = node.left;
      node.left = node.right;
      node.right = tmp;
   }

   return root;
}

26. 对称的二叉树

/**
	 *  思路: 递归
	 *  1. dfs如果两个节点都不存在,返回true
	 *  2. 如果有一个节点不存在而另一个节点存在 或者 两个节点值不相同,返回false
	 *  3. 递归进入左子树的左节点和右子树的右节点,以及左子树的右节点和右子树的左节点比较
	 *
	 *  时间: O(n)
	 *  空间: O(n)
	 */
	/*public boolean isSymmetric(TreeNode root) {
		if(root == null)
			return true;
		return dfs(root.left, root.right);
	}

	private boolean dfs(TreeNode left, TreeNode right) {
		if(left == null && right == null) {
			return true;
		}
		if(left == null || right == null || left.val != right.val) {
			return false;
		}
		return dfs(left.left, right.right) && dfs(left.right, right.left);
	}*/

	/**
	 * 思路: 迭代法
	 * 1. 维护两个队列q1,q2
	 * 2. 把root的左节点加入q1,右节点加入q2
	 *
	 * 时间: O(n)
	 * 空间: O(n)
	 */
	public boolean isSymmetric(TreeNode root) {
		if(root == null) {
			return true;
		}
		Queue<TreeNode> queue1 = new LinkedList<>();
		Queue<TreeNode> queue2 = new LinkedList<>();
		queue1.offer(root.left);
		queue2.offer(root.right);
		while(!queue1.isEmpty() && !queue2.isEmpty()) {
			TreeNode node1 = queue1.poll();
			TreeNode node2 = queue2.poll();
			if(node1 == null && node2 == null)
				continue;
			if(node1 == null || node2 == null || node1.val != node2.val) {
				return false;
			}
			queue1.offer(node1.left);
			queue1.offer(node1.right);
			queue2.offer(node2.right);
			queue2.offer(node2.left);
		}
		return true;
	}

27. 顺时针打印矩阵

/*public int[] spiralOrder(int[][] matrix) {
		if(matrix == null || matrix.length == 0)
			return new int[0];
		int n = matrix.length * matrix[0].length;
		int[] res = new int[n];
		int count = 0;
		int flag = 1; // 1向右  2向下 3向左 4向上
		int x = 0, y = 0; // 坐标
		boolean[][] visit = new boolean[matrix.length][matrix[0].length];
		while(count < n) {
			// 越界判断
			if(y >= matrix[0].length || y < 0 || x >= matrix.length || x < 0 || visit[x][y]) {
				if(flag == 1) {
					y--;
					x++;
					flag = 2;
				} else if(flag == 2) {
					x--;
					y--;
					flag = 3;
				} else if(flag == 3) {
					y++;
					x--;
					flag = 4;
				} else {
					x++;
					y++;
					flag = 1;
				}
			} else {
				res[count] = matrix[x][y];
				count++;
				visit[x][y] = true;
				if(flag == 1) {
					y++;
				} else if(flag == 2) {
					x++;
				} else if(flag == 3) {
					y--;
				} else {
					x--;
				}
			}
		}

		return res;
	}*/


	/**
	 *  思路: 模拟打印
	 *  1. 每次一旦遍历到边界就转换方向,并且边界紧缩
	 *  2. 总共遍历四个方向
	 *  	左->右, res.append(matrix[top][i]),遍历到边界后,top下移
	 *  	上->下, res.append(matrix[i][right]),遍历到边界后,right左移
	 *      右->左, res.append(matrix[bottom][i]),遍历到边界后,bottom上移
	 *      下->上, res.append(matrix[i][left]),遍历到边界后,left右移
	 *
	 * 	时间: O(m*n)
	 * 	空间: O(1)
	 */
	public int[] spiralOrder(int[][] matrix) {
		if(matrix == null || matrix.length == 0) {
			return new int[0];
		}
		int m = matrix.length;
		int n = matrix[0].length;
		int[] res = new int[m * n];
		int count = 0;
		int left = 0;
		int right = n-1;
		int top = 0;
		int bottom = m-1;
		while(count < res.length) {
			// 左->右
			for (int i = left; i <= right; i++) {
				res[count++] = matrix[top][i];
			}
			top++;
			if(top > bottom) {
				break;
			}
			// 上->下
			for (int i = top; i <= bottom; i++) {
				res[count++] = matrix[i][right];
			}
			right--;
			if(left > right) {
				break;
			}
			// 右->左
			for (int i = right; i >= left; i--) {
				res[count++] = matrix[bottom][i];
			}
			bottom--;
			if(top > bottom) {
				break;
			}
			// 下->上
			for (int i = bottom; i >= top; i--) {
				res[count++] = matrix[i][left];
			}
			left++;
			if(left > right) {
				break;
			}
		}

		return res;
	}

28. 栈的压入_弹出序列

/**
	 *  思路: 用一个新栈实时模拟进出栈操作
	 *  1. for循环入栈pushed元素,每push一次就检查能不能pop出来
	 *  2. 如果最后栈为空,说明一进一出刚刚好
	 *
	 *  时间: O(n)
	 *  空间: O(n)
	 */
	public static boolean validateStackSequences(int[] pushed, int[] popped) {
		Stack<Integer> stack = new Stack<>();
		int index = 0;
		for (int num : pushed) {
			stack.push(num);
			while(!stack.isEmpty() && stack.peek() == popped[index]) {
				stack.pop();
				index++;
			}
		}

		return stack.isEmpty();
	}

29. I_从上到下打印二叉树

// 层序遍历
	// 时间:O(n)
	// 空间:O(n)
	public int[] levelOrder(TreeNode root) {
		if(root == null) {
			return new int[0];
		}
		ArrayList<Integer> list = new ArrayList<>();
		Queue<TreeNode> queue = new LinkedList<>();
		queue.offer(root);
		while(!queue.isEmpty()) {
			TreeNode node = queue.poll();
			list.add(node.val);
			if(node.left != null) {
				queue.offer(node.left);
			}
			if(node.right != null) {
				queue.offer(node.right);
			}
		}
		int[] res = new int[list.size()];
		for(int i = 0; i < list.size(); i++) {
			res[i] = list.get(i);
		}
		return res;
	}

30. II_从上到下打印二叉树

public List<List<Integer>> levelOrder(TreeNode root) {
   List<List<Integer>> res = new ArrayList<>();
   if(root == null) {
      return res;
   }

   LinkedList<TreeNode> queue = new LinkedList<>();
   queue.add(root);
   int count = 0;
   while(!queue.isEmpty()) {
      count = queue.size();
      List<Integer> tmp = new ArrayList<>();
      for (int i = 0; i < count; i++) {
         TreeNode node = queue.pop();
         tmp.add(node.val);
         if(node.left != null) {
            queue.add(node.left);
         }
         if(node.right != null) {
            queue.add(node.right);
         }
      }
      res.add(tmp);
   }

   return res;
}

31. III_从上到下打印二叉树

public List<List<Integer>> levelOrder(TreeNode root) {
   if(root == null) {
      return new ArrayList<>();
   }
   List<List<Integer>> res = new ArrayList<>();
   Queue<TreeNode> queue = new LinkedList<>();
   queue.offer(root);
   int level = 0;
   while(!queue.isEmpty()) {
      int size = queue.size();
      List<Integer> tmp = new ArrayList<>();
      while(size > 0) {
         TreeNode node = queue.poll();
         if(level % 2 == 0) {
            tmp.add(node.val);
         } else {
            tmp.add(0, node.val);
         }
         if(node.left != null) {
            queue.offer(node.left);
         }
         if(node.right != null) {
            queue.offer(node.right);
         }
         size--;

      }
      level++;
      res.add(tmp);
   }

   return res;
}

32. 二叉搜索树的后序遍历序列

/**
 *  思路: 二叉搜索树的定义,左子树值都小于根节点,右子树值都大于根节点
 *  1. 设置k计数项,用于记录后序遍历序列中左子树的选取范围,以此划分左右子树
 *  2. postorder[k] > postorder[r],此时k为左子树数量,左子树范围是[l,k-1],右子树范围是[k,r-1]
 *  3. 判断右子树的值是否都大于postorder[r]根节点的值,不是则返回false
 *  4. 递归传入左子树和右子树,设置终止条件(当l >= r,则返回true)
 */
public boolean verifyPostorder(int[] postorder) {
   if(postorder == null || postorder.length == 0) {
      return false;
   }
   return dfs(0, postorder.length-1, postorder);
}

private boolean dfs(int l, int r, int[] postorder) {
   // 递归终止条件
   if(l >= r) {
      return true;
   }
   // 根节点的值
   int root = postorder[r];
   // 左子树起点下标
   int k = l;
   // 找到左子树节点的范围
   while(k < r && postorder[k] < root) {
      k++;
   }
   // 右子树起点,判断是否都大于根节点,否则直接返回false
   for (int i = k; i < r; i++) {
      if(postorder[i] < root) {
         return false;
      }
   }

   // 递归判断左子树和右子树是否满足
   return dfs(l, k-1, postorder) && dfs(k, r-1, postorder);
}

33. 二叉树中和为某一值的路径

/**
 * 思路: 遍历二叉树,并通过两个动态数组用于记录节点(path)和存储结果集(res)
 * 1. 读取当前节点值,将当前节点加入到记录节点动态数组中(path)
 * 2. 判断当前节点是不是叶子节点并且满足target目标值,如果是则加入到结果集中
 * 3. 递归先进入左子树,并且传入当前计算结果target
 * 4. 递归进入右子树,并且传入当前结算结果target
 * 5. 子树遍历完后,移除记录节点动态数组(path)中当前节点值(即最后一个元素),进行回溯
 */
// 结果集
List<List<Integer>> res = new ArrayList<>();
// 路径
List<Integer> path = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
   dfs(root, target);
   return res;
}

private void dfs(TreeNode root, int target) {
   if(root == null) {
      return;
   }
   // 添加当前二叉树的节点值
   path.add(root.val);
   // 计算target剩余值
   target -= root.val;
   // 满足条件,到达叶子节点并且taget==0时
   if(root.left == null && root.right == null && target == 0) {
      res.add(new ArrayList<>(path));
   }
   // 递归左子树
   dfs(root.left, target);
   // 递归右子树
   dfs(root.right, target);
   // 子树遍历完毕后,进行回溯,移除动态数组当前节点值
   path.remove(path.size()-1);
}

34. 复杂链表的复制

/**
 *  思路: 通过遍历当前链表,逐个复制当前遍历得到的节点
 *  1. 遍历当前链表,并依据得到的节点值,新建节点clone并添加到链表中
 *  2. 再次遍历链表,当指针cur指向的random不空为时,则为下一个节点(cur.next)的random指针赋予对应random指向对象的克隆
 *     cur.next.random = cur.random.next
 *     (cur = cur.next.next)
 * 3. 拆分链表,用res存储新链表的头节点,再创建指针pre/cur,通过修改pre与cur的next指向拆分链表,注意最后pre.next要指向空
 *
 */
public Node1 copyRandomList(Node1 head) {
   if(head == null) {
      return null;
   }
   Node1 cur = head;
   // 拷贝完全相同的节点添加到链表中
   while(cur != null) {
      Node1 clone = new Node1(cur.val);
      clone.next = cur.next;
      cur.next = clone;
      cur = clone.next;
   }

   cur = head;
   // 设置新节点的random值
   while(cur != null) {
      if(cur.random != null) {
         cur.next.random = cur.random.next;
      }
      cur = cur.next.next;
   }

   // 拆分链表
   Node1 pre = head;
   cur = head.next;
   Node1 res = head.next;
   while(cur.next != null) {
      pre.next = pre.next.next;
      cur.next = cur.next.next;
      cur = cur.next;
      pre = pre.next;
   }
   pre.next = null;
   return res;
}

35. 二叉搜索树与双向链表

Node pre;
Node head;
public Node treeToDoublyList(Node root) {
   if(root == null) {
      return null;
   }
   dfs(root);
   pre.right = head;
   head.left = pre;
   return head;
}

private void dfs(Node cur) {
   if(cur == null) {
      return;
   }
   dfs(cur.left);
   if(pre == null) {
      head = cur;
   } else {
      cur.left = pre;
      pre.right = cur;
   }
   pre = cur;
   dfs(cur.right);
}

36. 序列化二叉树

/**
 *  思路: BFS
 *  序列化:
 *     1. 用BFS遍历树,不管node的左右子节点是否存在,都加入到队列中
 *     2. 节点出队时,如果节点不存在,在返回值res加入一个"null",如果节点存在,则加入节点的字符串形式
 *  反序列化:
 *     1. 利用队列新建二叉树
 *     2. 将data转化为列表,然后遍历,只要不为null将节点按顺序加入二叉树中,同时还要将节点入队
 *     3. 队列为空时,遍历结束,返回根节点
 *
 *  时间: O(n)
 *  空间: O(n)
 */
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
   if(root == null) {
      return "[]";
   }
   StringBuilder res = new StringBuilder();
   res.append("[");
   Queue<TreeNode> queue = new LinkedList<>();
   queue.offer(root);
   while(!queue.isEmpty()) {
      TreeNode node = queue.poll();
      if(node == null) {
         res.append("null,");
      } else {
         res.append(node.val + ",");
         queue.offer(node.left);
         queue.offer(node.right);
      }
   }
   res.deleteCharAt(res.length()-1);
   res.append("]");
   return res.toString();
}

// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
   if(data == null || data.equals("[]") || data.length() == 0) {
      return null;
   }
   data = data.substring(1, data.length()-1);
   String[] nums = data.split(",");
   // 构建根节点
   TreeNode root = new TreeNode(Integer.valueOf(nums[0]));
   Queue<TreeNode> queue = new LinkedList<>();
   queue.offer(root);
   int index = 1;
   while(!queue.isEmpty()) {
      TreeNode node = queue.poll();
      if(!nums[index].equals("null")) {
         node.left = new TreeNode(Integer.valueOf(nums[index]));
         queue.offer(node.left);
      }
      index++;
      if(!nums[index].equals("null")) {
         node.right = new TreeNode(Integer.valueOf(nums[index]));
         queue.offer(node.right);
      }
      index++;
   }
   return root;
}

/**
 *  思路: DFS
 *
 *  序列化:
 *     1. 递归终止条件,为空时返回"null"
 *        2. 序列化结果: 根节点值 + "," + 左子节点值(进入递归) + "," + 右子节点值(进入递归)
 *
 *  比如 1
 *     / \
    *    2  3
 *    序列化结果是:1,2,null,null,3,null,null
 *     反序列:
 *        1. 先把字符串转换为队列
 *        2. 进入递归
 *           2.1 队列出队
 *           2.2 如果元素为"null",返回null
 *           2.3 不为"null",新建一个值为弹出元素的新节点
 *           2.4 其左子节点为队列的下一个元素,其右子节点为队列的下下个元素
 *
 *     时间: O(n)
 *     空间: O(n)
 */
/*public String serialize(TreeNode root) {
   if(root == null) {
      return "null";
   }
   return root.val + "," + serialize(root.left) + "," + serialize(root.right);
}

public TreeNode deserialize(String data) {
   if(data == null || data.equals("[]") || data.length() == 0) {
      return null;
   }
   Queue<String> queue = new LinkedList<>(Arrays.asList(data.split(",")));
   return dfs(queue);
}

private TreeNode dfs(Queue<String> queue) {
   String val = queue.poll();
   if("null".equals(val)){
      return null;
   }
   TreeNode root = new TreeNode(Integer.parseInt(val));
   root.left = dfs(queue);
   root.right = dfs(queue);
   return root;
}*/

37. 字符串的排列

/**
 * 思路: 全排列重复元素问题(回溯 + 排序 + 剪枝)
 *
 * 1. 对s字符串转化为数组后进行排序,比如aacb,排序后aabc
 * 2. 对aabc进行排列
 * 3. 设置布尔类型的used数组标记元素是否已被选择过
 * 4. 同一树层中如果arr[i] == arr[i-1] && used[i-1] == false,则进行剪枝,因为同一树层已使用过
 * 5. 如果同一树枝中没被使用过(used[i]=false]),则开始进行处理
 *       设置当前元素used[i]=true并且加入到路径path中,接着递归进入下一层,最后进行回溯,则删除路径中当前元素,used[i]=false
 */
// 存放符合条件结果的集合
List<String> res = new ArrayList<>();
public String[] permutation(String s) {
   if(s == null || s.length() == 0) {
      return new String[0];
   }
   // 用来存放符合条件结果
   StringBuilder path = new StringBuilder();
   boolean[] used = new boolean[s.length()];
   char[] newS = s.toCharArray();
   Arrays.sort(newS);
   backtrack(newS, path, used);
   return res.toArray(new String[0]);
}

private void backtrack(char[] arr, StringBuilder path, boolean[] used) {
   if(path.length() == arr.length) {
      res.add(new String(path));
      return;
   }

   for (int i = 0; i < arr.length; i++) {
      // used[i-1] == false,说明同一树层nums[i-1]使用过
      if(i > 0 && arr[i] == arr[i-1] && !used[i-1]) {
         continue;
      }
      // 同一树枝s.chatAt(i)没使用过开始处理
      if(!used[i]) {
         used[i] = true;
         path.append(arr[i]);
         backtrack(arr, path, used);
         path.deleteCharAt(path.length() - 1);
         used[i] = false;
      }
   }
}

38. 数组中出现次数超过一半的数字

/**
 *  思路: 摩尔投票法
 *
 *  时间: O(n)
 *  空间: O(1)
 */
public int majorityElement(int[] nums) {
   int vote = 0;
   int count = 0;
   for (int i = 0; i < nums.length; i++) {
      if(count == 0) {
         vote = nums[i];
      }
      count += vote == nums[i] ? 1 : -1;
   }
   return vote;
}

39. 最小的k个数

/**
 *  思路: 最大堆
 *
 *  时间: O(NlogK)
 *  空间: O(K)
 */
/*public int[] getLeastNumbers(int[] arr, int k) {
   if(arr == null || arr.length == 0) {
      return new int[0];
   }

   PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> (o2 - o1));

   for (int num : arr) {
      if(queue.isEmpty() || queue.size() < k) {
         queue.offer(num);
      } else if (queue.peek() > num) {
         queue.poll();
         queue.offer(num);
      } else {
         continue;
      }
   }

   int[] res = new int[k];
   for (int i = 0; i < k; i++) {
      res[i] = queue.poll();
   }

   return res;
}*/

/**
 *  思路: 快排
 *  时间: O(n)
 */
public int[] getLeastNumbers(int[] arr, int k) {
   if(arr == null || arr.length == 0 || k == 0) {
      return new int[0];
   }
   if(arr.length <= k) {
      return arr;
   }
   return quickSearch(arr, 0, arr.length-1, k-1);
}

private int[] quickSearch(int[] arr, int left, int right, int k) {
   int mid = partition(arr, left, right);

   if(k == mid) {
      return Arrays.copyOf(arr, k+1);
   }

   if(k < mid) {
      return quickSearch(arr, left, mid-1, k);
   } else {
      return quickSearch(arr, mid+1, right, k);
   }
}

private int partition(int[] arr, int left, int right) {
   // 备份轴点元素
   int pivot = arr[left];

   while(left < right) {
      while(left < right) {
         if(arr[right] > pivot) {
            right--;
         } else {
            arr[left++] = arr[right];
            break;
         }
      }

      while(left < right) {
         if(arr[left] < pivot ) {
            left++;
         } else {
            arr[right--] = arr[left];
            break;
         }
      }
   }

   arr[left] = pivot;
   return left;
}

40. 数据流中的中位数

/**
 * 思路: 堆的应用
 * 例如: 1 2 4 6 8 10 划分左右半区两个堆,4是最大堆中的最大值,6是最小堆的最小值
 * 1. 维护一个最大堆和一个最小堆
 *        - 最大堆: 最小的k个数
 *        - 最小堆: 最大的k个数
 * 2. 每次插入数据时保证两个堆的数量均衡
 * 3. 最大堆数量 == 最小堆数量,新的数据插入到最小堆中
 *    最大堆数量 != 最小堆数量,新的数据插入到最堆堆中
 *
 * 时间: O(logn)
 */
/** initialize your data structure here. */

PriorityQueue<Integer> minHeap;
PriorityQueue<Integer> maxHeap;
public _剑指_Offer_41_数据流中的中位数() {
   // 默认最小堆
   this.minHeap = new PriorityQueue<>();
   this.maxHeap = new PriorityQueue<>((o1, o2) -> (o2 - o1));
}

public void addNum(int num) {
   // 插入到最小堆中
   if(minHeap.size() == maxHeap.size()) {
      // 插入时,先插入到最大堆中,弹出最大堆最大值插入到最小堆中,因为最小堆维护最大的k个数
      maxHeap.offer(num);
      minHeap.offer(maxHeap.poll());
   } else {
      // 插入到最大堆中
      // 插入时,先插入到最小堆中,弹出最小堆最小值插入到最大堆中,因为最大堆维护最小的k个数
      minHeap.offer(num);
      maxHeap.offer(minHeap.poll());
   }
}

public double findMedian() {
   if(minHeap.size() == maxHeap.size()) {
      return (minHeap.peek() + maxHeap.peek()) / 2.0;
   } else {
      return minHeap.peek();
   }
}

41. 连续子数组的最大和

/**
    *  思路: 动态规划
    *
    *  1. sum表示存储以前一个nums[i]结尾的子数组中,和最大的是多少
    *  2. 如果sum < 0时, 将 sum = 0
    *  3. 如果sum >= 0时, 则 sum += num
    *  4. 每一次迭代,都要更新res,记录最大值
    *  时间: O(n)
    *  空间: O(1)
    */
   public static int maxSubArray(int[] nums) {
      if(nums == null || nums.length == 0) {
         return 0;
      }
      int sum = 0;
      int res = Integer.MIN_VALUE;

      for (int num : nums) {
         if(sum < 0) {
            sum = 0;
         }
         sum += num;
         res = Math.max(res, sum);
      }

      return res;
   }

   /**
    *  思路: 动态规划
    *
    *  1. 确定dp数组以及下标含义
    *     dp[i]:表示以nums[i]结尾的连续子数组的最大和
    *  2. 确定递推公式
    *     - dp[i-1] > 0, dp[i] = dp[i-1] + nums[i]
    *     - dp[i-1] <= 0, dp[i] = nums[i]
    *  3. 初始化
    *  4. 确定遍历顺序
    *
    *  时间: O(n)
    *  空间: O(n)
    */
   /*public static int maxSubArray(int[] nums) {
      if (nums == null || nums.length == 0) {
         return 0;
      }
      // dp[i]以nums[i]结尾的连续子数组的最大和
      int[] dp = new int[nums.length];

      // 初始化
      dp[0] = nums[0];

      int res = nums[0];

      for (int i = 1; i < nums.length; i++) {
         if(dp[i-1] > 0) {
            dp[i] = dp[i-1] + nums[i];
         } else {
            dp[i] = nums[i];
         }
         res = Math.max(res, dp[i]);
      }

      return res;
   }
*/

42. 1到n整数中1出现的次数

/**
 *  思路: 数学问题
 *  例子3 1 0  1  5 9 2
 *  high cur low
 *  左边high = n / base / 10, 当前位cur = n / base % 10, 右边low = n % base
 *
 *  1. cur此时指向3101(5)92 即百位时为5
 *  此时cur>1时, base=100,则假设百位为1时
 *  左边高位范围(0,3101),右边低位范围(0,99)
 *  当cur>1时,方案数(high + 1) * base 方案
 *
 *  2. cur此时指向310(1)592 即千位时为1
 *  此时cur==1时,base=1000,则假设千位为1时
 *  分两种情况讨论
 *   2.1 左边高位范围(0,309),右边低位范围(0,999) 方案数为 high * base
 *   2.2 左边高位范围(310,310),右边低位范围(0,592) 方案数为 low + 1
 *   综上,当cur==1时,方案数=hig * base + low + 1
 *
 *  3. cur此时指向31(0)1592 即万位时为0
 *  此时cur==0, base=10000,则假设万位为1时
 *  左边高位范围(0,30) 右边低位范围(0,9999)
 *  当cur==0时, 方案数 high * base
 */
public int countDigitOne(int n) {

   long base = 1;
   int res = 0;
   while(base <= n) {
      long high = n / base / 10;
      long low = n % base;
      long cur = n / base % 10;
      if(cur > 1) {
         res += (high + 1) * base;
      } else if(cur == 1) {
         res += high * base + low + 1;
      } else {
         res += high * base;
      }
      base *= 10;
   }
   return res;
}

43. 把数组排成最小的数

/**
 *  思路: 字符串的快排
 *  例子: [3,30]
 *  330 > 303,所以两者交换顺序,小的排在前面
 *  最后结果为303
 *
 *  因此,利用快速排序定义数组,传入比较规则
 *
 *  时间: O(nlogn)
 *  空间: O(n)
 */
public String minNumber(int[] nums) {
   String[] stringNums = new String[nums.length];
   for(int i = 0; i < nums.length; i++) {
      stringNums[i] = String.valueOf(nums[i]);
   }

   Arrays.sort(stringNums, (o1, o2) -> (o1 +  o2).compareTo(o2 + o1));

   StringBuilder res = new StringBuilder();
   for(String num : stringNums) {
      res.append(num);
   }

   return res.toString();
}

44. 把数字翻译成字符串

/**
 *  思路: 动态规划
 *
 *  x1x2....X(i-2) X(i-1) Xi ..... X(n-1) Xn
 *  比如 123
 *  递推公式: 如果X(i-1)和Xi可翻译,即整体被翻译,那么dp[i] = dp[i-2]
 *           如果Xi单独翻译,那么dp[i] = dp[i-1]
 *           所以综上,当Xi-1Xi能够被翻译时, dp[i] = dp[i-1] + dp[i-2],
 *           如果Xi-1Xi不能被翻译时(比如04、05、26、27等不满足条件的两位数),dp[i] = dp[i-1]
 *
 *  初始化时, dp[0] = 1, dp[1] = 1(一个数字时只有一种翻译方法)
 *  dp[0] = 1是为了推导dp[2]
 *
 *  时间: O(n)
 *  空间: O(n)
 */
/*public int translateNum(int num) {
   String s = String.valueOf(num);

   // dp[i]表示以Xi为结尾的数字的翻译方案数量
   int[] dp = new int[s.length() + 1];

   dp[0] = 1;
   dp[1] = 1;

   for (int i = 2; i <= s.length(); i++) {
      int curSum = Integer.valueOf(s.charAt(i-2) - '0') * 10 + Integer.valueOf(s.charAt(i-1) - '0');
      if(curSum >= 10 && curSum <= 25) {
         dp[i] = dp[i-1] + dp[i-2];
      } else {
         dp[i] = dp[i-1];
      }
   }

   return dp[s.length()];
}*/


// 空间优化
public int translateNum(int num) {
   String s = String.valueOf(num);

   int a = 1;
   int b = 1;

   for (int i = 2; i <= s.length(); i++) {
      int curSum = Integer.valueOf(s.charAt(i-2) - '0') * 10 + Integer.valueOf(s.charAt(i-1) - '0');
      int sum = 0;
      if(curSum >= 10 && curSum <= 25) {
         sum = a + b;
      } else {
         sum = b;
      }
      a = b;
      b = sum;
   }

   return b;
}

45. 礼物的最大价值

/**
 *  思路: 动态规划
 *
 *  1. 只能向下或向右走,所以(i,j)由左边和上边过来,因此dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1];
 *
 *  时间: O(m*n)
 *  空间: O(m*n)
 */
/*public int maxValue(int[][] grid) {
   if(grid == null || grid.length == 0) {
      return 0;
   }
   int m = grid.length;
   int n = grid[0].length;
   int[][] dp = new int[m + 1][n + 1];

   for (int i = 1; i <= m; i++) {
      for (int j = 1; j <= n; j++) {
         dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1];
      }
   }

   return dp[m][n];
}*/

// 优化到一维
public int maxValue(int[][] grid) {
   if(grid == null || grid.length == 0) {
      return 0;
   }
   int m = grid.length;
   int n = grid[0].length;
   int[] dp = new int[n + 1];

   for (int i = 1; i <= m; i++) {
      for (int j = 1; j <= n; j++) {
         dp[j] = Math.max(dp[j-1], dp[j]) + grid[i-1][j-1];
      }
   }

   return dp[n];
}

46. 最长不含重复字符的子字符串

/**
 *  思路: 滑动窗口,双指针法
 *
 *  1. left和right指针,最开始指向下标为0,然后right开始往右移动
 *  2. 把扫描过的元素放到map中,如果right扫描过的元素没有重复就一直后移,顺便记录一下最大值
 *  3. 如果right扫描过的元素有重复元素,则改变left指针,取Math.max(left, 原来元素下标值+1)
 *
 *  时间: O(n)
 *  空间: O(1) 字符最多128个
 *
 */
public int lengthOfLongestSubstring(String s) {
   if(s == null || s.length() == 0) {
      return 0;
   }
   int left = 0;
   int right = 0;
   int res = 0;
   // 存储元素对应的下标值
   Map<Character, Integer> map = new HashMap<>();
   while(right < s.length()) {
      if(map.containsKey(s.charAt(right))) {
         // 取最大值是因为可能出现 a b b a情况时, 当right = 3时, 此时left = 2,若不取最大值,则left会变为1
         left = Math.max(left, map.get(s.charAt(right)) + 1);
      }
      map.put(s.charAt(right), right);
      res = Math.max(res, right - left  +1);
      right++;
   }

   return res;
}

47. 丑数

/**
 *  思路: 三指针法
 *
 * 2x展开 (2 * 1) (2 * 2) (2 * 3) ......
 * 3x展开 (3 * 1) (3 * 2) (3 * 3) ......
 * 5x展开 (5 * 1) (5 * 2) (5 * 3) ......
 * 因此我们可以用三个指针来表示x, 通过动态规划来进行排序存储
 *
 * p2代表对应dp数组值 * 2
 * p3代表对应dp数组值 * 3
 * p5代表对应dp数组值 * 5
 */
public int nthUglyNumber(int n) {
   if(n == 1) {
      return 1;
   }
   int[] dp = new int[n];
   dp[0] = 1;
   int p2 = 0, p3 = 0, p5 = 0;
   for (int i = 1; i < n; i++) {
      dp[i] = Math.min(Math.min(dp[p2] * 2, dp[p3] * 3), dp[p5] * 5);
      if(dp[i] == dp[p2] * 2) {
         p2++;
      }
      if(dp[i] == dp[p3] * 3) {
         p3++;
      }
      if(dp[i] == dp[p5] * 5) {
         p5++;
      }
   }

   return dp[n-1];
}

48. 第一个只出现一次的字符

/**
 *  思路: 哈希表
 *
 */
public char firstUniqChar(String s) {
   if(s == null || s.length() == 0) {
      return ' ';
   }
   Map<Character, Boolean> map = new HashMap<>();

   char[] chars = s.toCharArray();

   for (char c : chars) {
      map.put(c, !map.containsKey(c));
   }

   for (char c : chars) {
      if(map.get(c)) {
         return c;
      }
   }

   return ' ';
}

49. 数组中的逆序对

int res = 0;
public int reversePairs(int[] nums) {
   if(nums == null || nums.length == 0) {
      return 0;
   }
   int[] tmp = new int[nums.length];
   mergeSort(nums, 0, nums.length-1, tmp);
   return res;
}

private void mergeSort(int[] nums, int left, int right, int[] tmp) {
   if(left < right) {
      int mid = (left + right) >> 1;
      // 递归划分左区间
      mergeSort(nums, left, mid, tmp);
      // 递归划分右区间
      mergeSort(nums, mid+1, right, tmp);
      // 合并已经排好序的部分
      merge(nums, left, mid, right, tmp);
   }
}

private void merge(int[] nums, int left, int mid,  int right, int[] tmp) {
   // 标记左半区第一个未排序的元素
   int l_pos = left;
   // 标记右半区第一个未排序的元素
   int r_pos = mid+1;
   // 临时数组下标
   int pos = left;

   while(l_pos <= mid && r_pos <= right) {
      if(nums[l_pos] <= nums[r_pos]) {
         tmp[pos++] = nums[l_pos++];
      } else {
         res += mid - l_pos + 1;
         tmp[pos++] = nums[r_pos++];
      }
   }

   // 合并左区间剩余元素
   while(l_pos <= mid) {
      tmp[pos++] = nums[l_pos++];
   }

   // 合并右区间剩余元素
   while(r_pos <= right) {
      tmp[pos++] = nums[r_pos++];
   }

   // 拷贝临时数组到原数组中
   while(left <= right) {
      nums[left] = tmp[left++];
   }
}

50. 两个链表的第一个公共节点

/**
 *  思路: 双指针法
 *  1. p、q同时走,p或q一旦遇到空时,就将指针指向对方的起始点
 *  2. 相当于p走了a+b步, q走了b+a步
 */
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
   ListNode p = headA;
   ListNode q = headB;
   while(p != q) {
      if(p == null) {
         p = headB;
      } else {
         p = p.next;
      }
      if(q == null) {
         q = headA;
      } else {
         q = q.next;
      }
   }
   return p;
}

51. I_在排序数组中查找数字

/**
 *  思路: 二分法
 *
 *  1. 通过二分法找到右边界j
 *     其中nums[mid] == target时, left = mid + 1
 *  2. 通过二分法找到左边界i
 *     其中nums[mid] == target时, right = mid - 1;
 *  3. 返回结果j - i - 1
 *
 *  时间: O(logn)
 *  空间: O(1)
 */
public int search(int[] nums, int target) {
   int left = 0;
   int right = nums.length-1;

   // 1. 二分找到右边界
   while(left <= right) {
      int mid = (left + right) >> 1;
      if(nums[mid] > target) {
         right = mid - 1;
      } else {
         left = mid + 1;
      }
   }
   int j = left;

   left = 0;
   right = nums.length-1;

   // 2. 二分找到左边界
   while(left <= right) {
      int mid = (left + right) >> 1;
      if(nums[mid] >= target) {
         right = mid - 1;
      } else {
         left = mid + 1;
      }
   }

   int i = right;

   return j - i - 1;
}

52. II_缺失的数字

/**
 * 思路: 二分法
 *
 * 1. 如果中间元素的值和下标相等,那么下一轮查找只需要查找右半边;
 * 2. 如果中间元素的值和下标不相等,那么下一轮就只查找左半边。
 *
 * 时间: O(logn)
 * 空间: O(1)
 */
public int missingNumber(int[] nums) {
   int left = 0;
   int rihgt = nums.length-1;
   while(left <= rihgt) {
      int mid = (left + rihgt) / 2;
      if(nums[mid] == mid) {
         left = mid + 1;
      } else {
         rihgt = mid - 1;
      }
   }

   return left;
}

53. 二叉搜索树的第k大节点

/**
 * 思路: 中序遍历
 *
 * 右根左遍历
 */
int k = 0;
int res = Integer.MIN_VALUE;
public int kthLargest(TreeNode root, int k) {
   this.k = k;
   dfs(root);
   return res;
}

private void dfs(TreeNode root) {
   if(root == null) return;

   dfs(root.right);
   k--;
   if(k == 0) {
      res = root.val;
   }
   dfs(root.left);
}

54. I_二叉树的深度

/**
 *  思路: 递归法
 */
/*public int maxDepth(TreeNode root) {
   if(root == null) return 0;
   return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}*/

/**
 * 思路: bfs迭代法
 */
public int maxDepth(TreeNode root) {
   if(root == null) return 0;
   Queue<TreeNode> queue = new LinkedList<>();
   queue.offer(root);
   int res = 0;
   while(!queue.isEmpty()) {
      int size = queue.size();
      while(size > 0) {
         TreeNode node = queue.poll();
         if(node.left != null) {
            queue.offer(node.left);
         }
         if(node.right != null) {
            queue.offer(node.right);
         }
         size--;
      }
      res++;
   }
   return res;
}

55. II_平衡二叉树

/**
 *  思路: 递归
 *  1. 根节点的左子树和右子树的高度差的绝对值要 <= 1
 *  2. 要保证左子树和右子树都是平衡!!
 */
/*public boolean isBalanced(TreeNode root) {
   if(root == null) {
      return true;
   }
   return Math.abs(getDepth(root.left) - getDepth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}

private int getDepth(TreeNode root) {
   if(root == null) {
      return 0;
   }
   return Math.max(getDepth(root.left), getDepth(root.right)) + 1;
}*/

/**
 *  思路: 后序 + 剪枝
 */
public boolean isBalanced(TreeNode root) {
    if(root == null) return true;
    if(dfs(root) == -1) {
      return false;
    } else {
      return true;
    }
}

private int dfs(TreeNode root) {
   if(root == null) {
      return 0;
   }
   int left = dfs(root.left);
   if(left == -1) {
      return -1;
   }
   int right = dfs(root.right);
   if(right == -1) {
      return -1;
   }
   return Math.abs(left - right) <= 1 ? Math.max(left, right) + 1 : -1;
}

56. I_数组中数字出现的次数

/**
 *  思路: 分组位运算
 *
 *  1. 遍历nums进行异或运算, 比如4 4 3 3 1 7 最后异或结果为1和7
 *  2. 循环左移计算m, 因为1和7肯定有一个二进制位不相同,将m与第一步异或结果进行与运算,结果为0时(二进制位相同),就左移m,直到不为0为止(二进制位不相同)
 *  3. 拆分nums为两个子数组, 根据第二步计算m的结果, 与num进行与运算,结果为0的划分为一个数组,不为0的划分为另一个数字,这样1和7就被划分到不同的数组中了
 *  4. 对拆分出来的数组进行异或运算,这样4 4 1结果为1, 3 3 7结果为7
 *
 *  时间: O(n)
 *  空间: O(1)
 */
public int[] singleNumbers(int[] nums) {
   int x = 0, y = 0;
   int m = 1;
   int z = 0;
   for (int num : nums) {
      z ^= num;
   }

   // z = x ^ y
   while((z & m) == 0)  {
      m <<= 1;
   }

   for (int num : nums) {
      if((num & m) == 0) {
         x ^= num;
      } else {
         y ^= num;
      }
   }

   return new int[]{x, y};
}

57. II_数组中数字出现的次数

/**
 *  思路: 位运算
 *  1. int类型是32位,统计所有数组在某一个位置的和能否被3整除,如果不能被3整除,说明那个只出现1次的数字对应的那个位置二进制位为1
 *  2. 所以统计所有的32位,遍历判断即可
 */
public int singleNumber(int[] nums) {

   int res = 0;
   for (int i = 0; i < 32; i++) {
      int oneCount = 0;
      for (int j = 0; j < nums.length; j++) {
         oneCount += (nums[j] >>> i) & 1;
      }

      if(oneCount % 3 == 1) {
         res |= 1 << i;
      }
   }

   return res;
}

58. II_和为s的连续正数序列

/**
 *  思路: 滑动窗口
 * 1. 使用两个指针left和right, left指向1,right指向1,分别表示窗口左边界和右边界,然后计算窗口内元素的和
 *     2. 如果窗口内的值大于target,说明窗口大了,left右移
 *        如果窗口内的值小于target,说明窗口小了,right右移
 *        如果窗口内的值等于target,说明找到了一组序列,加入到列表中
 *
 *  记住左闭右开区间,找到一组序列时,此时right值是右边界,不纳入范围内
 *
 *  左闭右开区间
 */
/*public int[][] findContinuousSequence(int target) {
   List<int[]> res = new ArrayList<>();
   int left = 1; // 滑动窗口左边界
   int right = 1; // 滑动窗口右边界
   int sum = 0;
   while(left <= target/2) {
      if(sum < target) {
         sum += right;
         right++;
      } else if(sum > target) {
         sum -= left;
         left++;
      } else {
         int[] tmp = new int[right-left];
         for (int k = left; k < right; k++) {
            tmp[k - left] = k;
         }
         res.add(tmp);
         sum -= left;
         left++;
      }
   }

   return res.toArray(new int[res.size()][]);
}*/


/**
 *  思路: 滑动窗口
 * 1. 使用两个指针left和right, left指向1,right指向2,分别表示窗口左边界和右边界,然后计算窗口内元素的和
 *     2. 如果窗口内的值大于target,说明窗口大了,left右移
 *        如果窗口内的值小于target,说明窗口小了,right右移
 *        如果窗口内的值等于target,说明找到了一组序列,加入到列表中
 *
 *  左闭右闭区间
 */
public int[][] findContinuousSequence(int target) {
   List<int[]> res = new ArrayList<>();
   int left = 1; // 滑动窗口左边界
   int right = 2; // 滑动窗口右边界
   int sum = left + right;
   while(left <= target/2) {
      if(sum < target) {
         sum += ++right;
      } else if(sum > target) {
         sum -= left;
         left++;
      } else {
         int[] tmp = new int[right-left+1];
         for (int k = left; k <= right; k++) {
            tmp[k - left] = k;
         }
         res.add(tmp);
         sum -= left;
         left++;
      }
   }

   return res.toArray(new int[res.size()][]);
}

59. 和为s的两个数字

/**
 *  思路: 双指针法
 *  1. 左指针与右指针的数字和如果大于target,则右指针--
 *  2. 左指针与右指针的数字和如果小于target,则左指针++
 *  3. 左指针与右指针的数字和如果等于target,则直接返回
 */
public int[] twoSum(int[] nums, int target) {
   int left = 0;
   int right = nums.length-1;
   while(left < right) {
      if(nums[left] + nums[right] > target) {
         right--;
      } else if(nums[left] + nums[right] < target) {
         left++;
      } else {
         return new int[]{nums[left], nums[right]};
      }
   }

   return new int[0];
}

60. I_翻转单词顺序

/**
 *  思路: 用StringBuilder拼接拆分后的字符串
 *  1. 从后往前添加字符串
 *  2. 遇到空字符串时,跳过
 *  3. 最后利用trim()函数去除拼接出来多余的" "
 */
public String reverseWords(String s) {
   String[] strs = s.split(" ");
   StringBuilder res = new StringBuilder();
   for(int i = strs.length-1; i >= 0; i--) {
      if(strs[i].equals("")) {
         continue;
      }
      res.append(strs[i] + " ");
   }
   return res.toString().trim();
}

61. II_左旋转字符串

/**
 * 思路: 直接拼接法
 * 时间: O(1)
 * 空间: O(n)
 */
/*public String reverseLeftWords(String s, int n) {
   return s.substring(n) + s.substring(0, n);
}*/

/**
 *  思路: 利用StringBuilder拼接每个字符
 *  时间: O(1)
 *  空间: O(n)
 */
public String reverseLeftWords(String s, int n) {
   StringBuilder res = new StringBuilder();
   for (int i = n; i < n + s.length(); i++) {
      res.append(s.charAt(i % s.length()));
   }
   return res.toString();
}

62. I_滑动窗口的最大值

/**
 *  思路: 滑动窗口 + 暴力
 *
 *  时间: O(n * k)
 *  空间: O(n)
 */
/*public static int[] maxSlidingWindow(int[] nums, int k) {
   int left = 0;
   int right = 0;
   List<Integer> res = new ArrayList<>();
   while(right < nums.length) {
      while(right - left < k) {
         right++;
      }
      int maxVal = Integer.MIN_VALUE;
      for (int i = left; i < right; i++) {
         maxVal = Math.max(maxVal, nums[i]);
      }
      res.add(maxVal);
      left++;
   }

   int[] result = new int[res.size()];
   for (int i = 0; i < res.size(); i++) {
      result[i] = res.get(i);
   }
   return result;
}*/


/**
 *  思路: 滑动窗口 + 单调队列
 *  1. 遍历给定的数组元素,如果队列不为空且待加入的元素大于队尾元素,则移除队尾元素,直到队尾元素大于等于待加入元素,满足后,向队尾添加当前元素的下标值
 *  2. 计算组成满足滑动窗口的左边界left值
 *  3. 如果发现队首元素下标值小于left值,则表明队首元素已经不在满足滑动窗口当中了,则需要移除对应在滑动窗口的下标值
 *  4. 如果left值大于等于0时,说明窗口刚好形成,向结果集中添加满足窗口的最大值,即队列中的首元素的下标值对应的元素值
 *  5. 移动右指针,重复1,2,3,4步骤
 *
 *  时间: O(n)
 *  空间: O(k) 队列的长度
 */
public static int[] maxSlidingWindow(int[] nums, int k) {
   // 滑动窗口个数
   int[] res = new int[nums.length-k+1];

   int left = 0;
   int right = 0;
   // 存储窗口单调递减下标值
   LinkedList<Integer> queue = new LinkedList<>();

   // 移动窗口右边界,直到小于数组元素大小,代表结束
   while(right < nums.length) {
      // 队列不为空且新加入的元素值大于队列队尾的元素时,需要一直出队尾元素,保持队列单调递减
      while(!queue.isEmpty() && nums[queue.peekLast()] < nums[right]) {
         queue.removeLast();
      }

      // 添加元素的下标值
      queue.addLast(right);

      // 计算每次待加入窗口的左边界
      left = right - k + 1;

      // 当队首元素的下标小于左边界时,表明队首元素已经不再滑动窗口中,需要移除掉
      if(queue.peekFirst() < left) {
         queue.removeFirst();
      }

      // 窗口形成,等同于 right + 1 >= k
      if(left >= 0) {
         res[left] = nums[queue.peekFirst()];
      }

      right++;
   }

   return res;
}

63. II_队列的最大值

// 封装一个队列实现基本的功能
Queue<Integer> queue;
// 用于实现O(1)取得最大值
Deque<Integer> deque;

public _剑指_Offer_59_II_队列的最大值() {
   this.queue = new LinkedList<>();
   this.deque = new LinkedList<>();
}

public int max_value() {
   // 最大值队列中不为空返回首个元素(单调递减)
   if(deque.size() != 0) {
      return deque.peekFirst();
   } else {
      // 为空时返回-1
      return -1;
   }
}

public void push_back(int value) {
   // 元素入队基本队列中
   queue.offer(value);
   // 保持最大值队列的单调性,保持单调递减
   while(!deque.isEmpty() && deque.peekLast() < value) {
      deque.pollLast();
   }
   // 元素入队最大值队列
   deque.offerLast(value);
}

public int pop_front() {
   // 若基本队列为空时,直接返回-1
   if(queue.isEmpty()) {
      return -1;
   }
   // 判断出队首元素是否和最大值队列的队首元素是否相等,若相等,最大值队列也要出队
   if(queue.peek().equals(deque.peekFirst())) {
      deque.pollFirst();
   }
   // 基本队列出队首元素
   return queue.poll();
}

64. n个骰子的点数

/**
 *  思路: 动态规划
 *
 *  1. 确定dp数组以及下标含义, 用二维数组表示dp[i][j]代表投掷完i个骰子后,点数为j出现的次数
 *  2. 确定递推公式, dp[i][j] 由前一阶段推出 即dp[i-1][j-{1,6}]
 *  3. 初始化,投掷一个骰子时, dp[1][j]
 *  4. 确定遍历顺序,从前往后,i从2开始递增到n
 *
 *  时间: O(n^2)
 *  空间: O(n^2)
 */
public double[] dicesProbability(int n) {
   // dp[i][j]表示投掷完i枚后,点数j出现的次数
   int[][] dp = new int[n + 1][6 * n + 1];

   // 递推公式
   // 最后一个阶段的骰子数 由 前一个阶段的骰子数转化过来
   // dp[i][j] = dp[i-1][j-1] + dp[i-1][j-2] + dp[i-1][j-3] + dp[i-1][j-4] + dp[i-1][j-5] + dp[i-1][j-6]

   // 初始化
   // 投掷一枚骰子时
   for (int j = 1; j <= 6; j++) {
      dp[1][j] = 1;
   }

   for (int i = 2; i <= n; i++) {
      for (int j = 1; j <= 6 * i; j++) {
         for (int k = 1; k <= 6; k++) {
            if(j - k <= 0) {
               break;
            }
            dp[i][j] += dp[i-1][j-k];
         }
      }
   }

   double[] res = new double[6 * n - n + 1];

   for (int i = n; i <= 6 * n; i++) {
      res[i - n] = (double)dp[n][i] / Math.pow(6, n);
   }

   return res;
}

65. 扑克牌中的顺子

/**
 * 思路: 模拟
 *
 * Set + 遍历
 * 1. 构成顺子的条件
 *        - 无重复元素(大小王除外)
 *        - 最大值 - 最小值 < 5 (大小王除外)
 *
 * 时间: O(N) = O(5) = O(1)
 * 空间: O(N) = O(5) = O(1)
 */
/*public boolean isStraight(int[] nums) {
   Set<Integer> set = new HashSet<>();
   int minVal = 14;
   int maxVal= 0;
   for (int num : nums) {
      // 遇到大小王时则跳过
      if(num == 0) {
         continue;
      }
      if(set.contains(num)) {
         return false;
      }
      set.add(num);
      minVal = Math.min(minVal, num);
      maxVal = Math.max(maxVal, num);
   }

   return maxVal - minVal < 5;
}*/


/**
 * 思路: 模拟
 *
 * 快排 + 遍历
 * 1. 构成顺子的条件
 *        - 无重复元素(大小王除外)
 *        - 最大值 - 最小值 < 5 (大小王除外)
 *
 * 时间: O(nlogn) = O(1)
 * 空间: O(logn) = O(1)
 */
public boolean isStraight(int[] nums) {
   Arrays.sort(nums);
   int jokers = 0;
   for (int i = 0; i < 4; i++) {
      if(nums[i] == 0) {
         jokers++;
      } else if(nums[i+1] == nums[i]) {
         return false;
      }
   }

   // 最大值 - 最小值
   return nums[4] - nums[jokers] < 5;
}

66. 圆圈中最后剩下的数字

/**
 *  思路: 模拟
 *  1. 将数据添加到ArrayList中,模拟链条循环
 *  2. 下一个删除位置下标是(idx + m - 1 ) % n, 减1的原因是idx在删除一个元素后,从下一个元素的当前位置出发,所以仅需要加上 m-1步即可
 *     %n表示求模剩下元素大小
 */
public int lastRemaining(int n, int m) {
   List<Integer> list = new ArrayList<>();
   for (int i = 0; i < n; i++) {
      list.add(i);
   }
   int idx = 0;
   while(n > 1) {
      // idx = 2 % 5 = 2; idx = 4 % 4 = 0; idx = 2 % 3 = 2; idx = 4 % 2 = 0
      idx = (idx + m - 1) % n;
      list.remove(idx);
      n--;
   }

   return list.get(0);
}

67. 股票的最大利润

// 贪心算法
/*public int maxProfit(int[] prices) {
   int left = Integer.MAX_VALUE;
   int res = 0;
   for (int i = 0; i < prices.length; i++) {
      if(prices[i] < left) {
         left = prices[i];
      }
      res = Math.max(res, prices[i]-left);
   }

   return res;
}*/

// 动态规划
public int maxProfit(int[] prices) {
   if(prices.length == 0)
      return 0;
   // 1. 确定dp数组以及下标含义
   // dp[i][j] 表示第i天 状态为j的最大现金价值
   // dp[i][0] 持有股票   dp[i][1] 不持有股票
   int[][] dp = new int[prices.length][2];

   // 2. 确定递推公式
   /*
      dp[i][0] = max(dp[i-1][0], -prices[i])
      dp[i][1] = max(dp[i-1][1], prices[i] + dp[i-1][0])
    */

   // 3. 初始化
   dp[0][0] = -prices[0];
   dp[0][1] = 0;

   // 4. 确定遍历顺序
   for (int i = 1; i < prices.length; i++) {
      dp[i][0] = Math.max(dp[i-1][0], -prices[i]);
      dp[i][1] = Math.max(dp[i-1][1], prices[i] + dp[i-1][0]);
   }

   return dp[prices.length-1][1];
}

68. 求1到n的和

/**
 * 思路: 逻辑符短路
 *
 * 1. 不能使用乘法和除法符号
 * 2. 不能使用if,for,while
 * 3. 所以使用&& 与逻辑符 判断终止条件,使得递归终止
 */
/*int sum = 0;
public int sumNums(int n) {
   boolean x = n > 1 && sumNums(n-1) > 0;
   sum += n;
   return sum;
}*/

public int sumNums(int n) {
   boolean x = n > 1 && (n += sumNums(n-1)) > 0;
   return n;
}

69. 不用加减乘除做加法

/**
 *  思路: 位运算
 *
 *  1. 将两个数的和拆为两步骤,
 *     第一步骤是忽略进位,计算直接相加(^异或运算)
 *     第二步骤是计算进位和
 *     最后将第一步骤和第二步骤的结果相加即可(又重新回到两个数相加,那么重复第一和第二步骤)
 *
 *  2. 对于直接相加,可以直接两个数进行异或运算得到结果
 *  3. 对于计算进位和, 先进行与运算,再左移一位
 *
 *  例子: 比如 1100(12) + 1111(15)
 *  1. 直接相加结果 = 0011
 *  2. 进位结果  = 11000
 *  3. 最后结果 11011 (十进制27)
 */
public int add(int a, int b) {
   while(b != 0) {
      int x = a ^ b;
      int y = (a & b) << 1;
      a = x;
      b = y;
   }
   return a;
}

70. 构建乘积数组

/**
 *  思路: 左边的乘积乘以右边的乘积
 *
 *  1. 构造左边乘积数组,不包含当前元素
 *  2. 构造右边乘积数组,不包含当前元素
 *  3. 构造结果集,将左边乘积数组 与 右边乘积数组 对应元素相乘得到结果
 */
public int[] constructArr(int[] a) {
   if(a == null || a.length == 0) {
      return new int[0];
   }
   int length = a.length;
   // 构造左边乘积
   int[] resLeft = new int[length];
   // 构造右边乘积
   int[] resRight = new int[length];

   resLeft[0] = 1;
   resRight[length-1] = 1;

   for (int i = 1; i < length; i++) {
      resLeft[i] = resLeft[i-1] * a[i-1];
   }

   int[] res = new int[length];

   for (int i = length-2; i >= 0; i--) {
      resRight[i] = resRight[i+1] * a[i+1];
   }


   for (int i = 0; i < length; i++) {
      res[i] = resLeft[i] * resRight[i];
   }

   return res;
}

71. 把字符串转换成整数

public static int strToInt(String str) {
   char[] chars = str.trim().toCharArray();

   if(chars.length == 0) {
      return 0;
   }

   int res = 0;
   // 1表示正数,-1表示负数
   int sign = 1;

   // 处理越界的情况
   int a = Integer.MAX_VALUE / 10;

   // 从非符号位开始
   int index = 1;

   if(chars[0] == '-') {
      sign = -1;
   } else if(chars[0] != '+') {
      // 说明没有符号位,则默认为正数,下标从0开始
      index = 0;
   }

   for (int i = index; i < chars.length; i++) {
      // 遇到非数字的时候,结束循环
      if(chars[i] < '0' || chars[i] > '9') {
         break;
      }
      // 越界
      // 若res = 214748365以上, 则 10×res > 2147483647
      // 若res = 214748364, 且 后面紧跟着数字 > 7, 则 10×res > 2147483647
      // 注意这里的 > 7 巧妙的把负数的情况也包含了进来,结果依然正确
      if(res > a || (res == a) && (chars[i] - '0') > Integer.MAX_VALUE % 10) {
         return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
      }
      res = res * 10 + (chars[i] - '0');
   }

   return res * sign;
}

72. I_二叉搜索树的最近公共祖先

/**
 *  思路: 递归法(做法与二叉树一样) (后序遍历回溯法)
 */
/*public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
   if (root == null || p == root || q == root) {
      return root;
   }

   TreeNode left = lowestCommonAncestor(root.left, p, q);
   TreeNode right = lowestCommonAncestor(root.right, p, q);

   if(left != null && right != null) {
      return root;
   } else if(left != null && right == null) {
      return left;
   } else if(left == null && right != null) {
      return right;
   } else {
      return null;
   }
}*/

/**
 *  思路: 递归法(利用二叉搜索树的特性,前序遍历法)
 * 这里递归函数有返回值,即标准的搜索一条边的写法,一旦遇到满足条件的情况,直接返回
 *
 */
/*public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
   // 递归终止条件 一定能找到祖先节点,无需处理空的情况

   // 根

   // 左子树 (左)
   if(p.val < root.val && q.val < root.val) {
      TreeNode left = lowestCommonAncestor(root.left, p, q);
      if(left != null) {
         return left;
      }
   } else if(p.val > root.val && q.val > root.val) {
      // 右子树 (右)
      TreeNode right = lowestCommonAncestor(root.right, p, q);
      if(right != null) {
         return right;
      }
   } else {
      // 若p,q分散在左右子树,则root即为公共祖先节点
      return root;
   }

   return null;
}*/

/**
 *  思路: 迭代法
 */
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {

   while(root != null) {
      if(root.val > p.val && root.val > q.val) {
         root = root.left;
      } else if(root.val < p.val && root.val < q.val) {
         root = root.right;
      } else {
         return root;
      }
   }

   return null;
}

73. 二叉树的最近公共祖先

/**
	 *  思路: (求最小公共祖先问题) 递归回溯法
	 *  1. 从底向上遍历,二叉树只能通过后序遍历实现
	 *  2. 回溯过程中,需要遍历整颗二叉树,即使已经找到结果了,依然要把其它节点遍历完,因为要使用递归函数的返回值做逻辑判断
	 *  (递归函数什么时候需要返回值?
	 *  遍历整颗二叉树,不需要返回值,如果要搜索其中一条符合条件的路径,递归函数需要返回值,因为遇到了
	 *  符合条件的路径了就需要及时返回, 有了返回值还要做进一步处理。)
	 *
	 *  对于本题来说,需要在回溯的过程中递归函数的返回值做判断路径符不符合,所以需要返回值,而且要遍历整颗二叉树,因为left和right后序还要做逻辑处理
	 *
	 *  3. 左为空,右不为空,则返回右节点
	 *     左不为空,右为空,则返回左节点
	 *     左不为空,右不为空,则返回根节点
	 *     左为空,右为空,则返回空节点
	 */
	public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
		if(root == null || root == p || root == q)
			return root;
		TreeNode left = lowestCommonAncestor(root.left, p, q);
		TreeNode right = lowestCommonAncestor(root.right, p, q);
		if(left != null && right != null) {
			return root;
		}
		if(left == null && right != null) {
			return right;
		}
		if(right == null && left != null) {
			return left;
		}
		return root;
	}

补充类似题

1. 合并K个升序链表

/**
 * 思路:逐一合并两条链表
 *
 * 时间: O(NK)   K条链表的总节点数是N
 * 空间: O(1)
 */
/*public ListNode mergeKLists(ListNode[] lists) {
   if(lists == null || lists.length == 0) {
      return new ListNode();
   }
   ListNode res = lists[0];

   for (int i = 1; i < lists.length; i++) {
      res = mergeTwoList(res, lists[i]);
   }

   return res;
}*/

/**
 *  思路: 归并排序
 *  1. 将lists列表不断进行二分划分,最后划分到只剩下一个链表
 *  2. 然后将划分出来的链表们进行两两合并,一直合并到生成最终的升序链表为止
 *
 *  时间: O(NlogK) 每一层归并的时间是O(N),归并层数最大为O(logK+1)
 *  空间: O(logN)  递归栈的大小
 */
public ListNode mergeKLists(ListNode[] lists) {
   if(lists.length == 0) {
      return null;
   }
   return merge(lists, 0, lists.length-1);
}

private ListNode merge(ListNode[] lists, int left, int right) {
   if(left == right) {
      return lists[left];
   }
   int mid = left + (right - left) / 2;
   ListNode l1 = merge(lists, left, mid);
   ListNode l2 = merge(lists, mid + 1, right);
   return mergeTwoList(l1, l2);
}

private ListNode mergeTwoList(ListNode res, ListNode list) {
   // 迭代法合并
   ListNode dummy = new ListNode();
   ListNode cur = dummy;

   while(res != null && list != null) {
      if(res.val <= list.val) {
         cur.next = res;
         res = res.next;
      } else {
         cur.next = list;
         list = list.next;
      }
      cur = cur.next;
   }

   cur.next = res == null ? list : res;

   return dummy.next;
}

2. 通配符匹配

/**
 * 思路: 动态规划(匹配问题、最长公共子串都可以用动态规划)
 * 1. dp[i][j] : s的前i个能否被p的前j个匹配
 * 2. 手动求二维矩阵的每个值,通过计算可以发现:
 *        - 第0列,除了dp[0][0]=true,其它的dp[i][0] = false
 *        - s从0开始算,p从1开始算
 *        - 过程中考虑dp[i][j]由哪个值得来
 * 3. 需要考虑p的当前字符p[j]:
 *        a. 当前字符是字母
 *        b. 当前字符是'?'
 *           由a和b判断两个字符是否匹配
 *           字符匹配: s[i] == p[j] || p[j] == '?'
 *         字符不匹配: s[i] != p[j]
*          c. 当前字符是'*'
 *         '*'可构造一切
 *         使用'*': 代表一个字符串: s的当前位置可以由'*'代替,所以看s的前i-1和p的前j个是否可以匹配,即dp[i][j] = dp[i-1][j]
 *         不使用'*': 代表空字符:   s的当前真假取决于s前i个,p的前j-1个,即dp[i][j] = dp[i][j-1]
*
 *
 *     综上a,b,c条件
 *     字符是字母或'?'时: 匹配时dp[i][j] = dp[i-1][j-1],不匹配时dp[i][j] = false
 *     字符是'*'时: dp[i][j] = dp[i-1][j] || dp[i][j-1]
 *
 *     时间: O(n*m)
 *     空间: O(n*m)
 */
/*public boolean isMatch(String s, String p) {
   if(s.length() == 0 && p.length() == 0) {
      return true;
   }
   if(p.length() == 0) {
      return false;
   }
   // 1. 定义dp数组
   int m = s.length();
   int n = p.length();
   boolean[][] dp = new boolean[m+1][n+1];

   // 2. 初始化  dp[i][0]=false
   dp[0][0] = true;

   // 为了处理边界,初始化0行
   for (int j = 1; j <= n; j++) {
      if(p.charAt(j-1) == '*') {
         // 取决于上一个
         dp[0][j] = dp[0][j-1];
      }
      // 不是'*'为false
   }

   // 3. 确定遍历顺序,先遍历s,后遍历p
   for (int i = 1; i <= m; i++) {
      char charS = s.charAt(i-1);
      for (int j = 1; j <= n; j++) {
         char charP = p.charAt(j - 1);
         if(charP != '*') {
            // p当前字符是字母或者?时
            // s和p匹配时取决于dp[i-1][j-1],不匹配为false
            if(charP == charS || charP == '?') {
               dp[i][j] = dp[i - 1][j - 1];
            }
         } else {
            // p字符是'*'时
            // 可以代表一个字符串或者代表空字符
            dp[i][j] = dp[i-1][j] || dp[i][j-1];
         }
      }
   }
   return dp[m][n];
}*/


/**
 *  思路: 动态规划 + 矩阵压缩为一维空间
 *  记录leftUp值,在更新dp[j]时,需要记录下原来的leftUp值
 *  时间: O(n*m)
 *  空间: O(n)
 */
public boolean isMatch(String s, String p) {
   if(s.length() == 0 && p.length() == 0) {
      return true;
   }
   if(p.length() == 0) {
      return false;
   }
   // 1. 定义dp数组
   int m = s.length();
   int n = p.length();
   boolean[] dp = new boolean[n+1];

   // 2. 初始化  dp[i][0]=false
   dp[0] = true;

   // 为了处理边界,初始化0行
   for (int j = 1; j <= n; j++) {
      if(p.charAt(j-1) == '*') {
         // 取决于上一个
         dp[j] = dp[j-1];
      }
      // 不是'*'为false
   }

   boolean leftUp = dp[0];
   dp[0] = false;
   // 3. 确定遍历顺序,先遍历s,后遍历p
   for (int i = 1; i <= m; i++) {
      char charS = s.charAt(i-1);
      for (int j = 1; j <= n; j++) {
         char charP = p.charAt(j - 1);
         if(charP != '*') {
            // p当前字符是字母或者?时
            // s和p匹配时取决于dp[i-1][j-1],不匹配为false
            if(charP == charS || charP == '?') {
               // 字符相等时,需要从左上角推出,所以需要一个临时变量存储当前位置的值
               boolean temp = dp[j];
               dp[j] = leftUp;
               leftUp = temp;
            } else {
               leftUp = dp[j];
               dp[j] = false;
            }
         } else {
            // p字符是'*'时
            // 可以代表一个字符串或者代表空字符
            // 从左或上推出
            leftUp = dp[j];
            dp[j] = dp[j-1] || dp[j];
         }
      }
      // 注意:每结束一行,leftUp值为上一行的最末尾,所以需要更新为dp[0]
      leftUp = dp[0];

      /*System.out.println(leftUp);
      for (int j = 0; j <= n; j++) {
         System.out.print(dp[j] + " ");
      }
      System.out.println();*/
   }
   return dp[n];
}

3. 爬楼梯

/**
 *  思路1: 动态规划求解
 *  dp[i] = dp[i-1] + dp[i-2]
 *  由于dp[i]只与前面两项相关,因此可以通过三个变量sum,a,b记录,优化空间
 *
 *  时间:O(n)
 *  空间:O(1)
 */
/*public int climbStairs(int n) {
   if(n < 2) {
      return 1;
   }

   int a = 1;
   int b = 1;
   int sum = 0;
   for(int i = 2; i <= n; i++) {
      sum = a + b;
      a = b;
      b = sum;
   }

   return b;
}*/

/**
 * 思路: 完全背包问题中的求排列问题
 *
 * n相当于背包容量
 * 每次爬1,2阶相当于物品
 *
 * 1. 确定dp数组以及下标含义 dp[j]表示爬到j层的排列数
 * 2. 确定递推公式, dp[j] += dp[j-nums[i]]  其中nums[i] = {1,2}
 * 3. 进行初始化, dp[0] = 1,是递推公式的前提
 * 4. 确定遍历顺序, 排列问题是先遍历背包容量,后遍历物品,外循环背包容量从0开始从前往后推增
 * 5. 返回结果值dp[j]
 *
 * 时间: O(n*m)  m=2
 * 空间: O(n)
 *
 */
public int climbStairs(int n) {
   // 1. 确定dp数组以及下标含义
   // dp[j]表示凑成n的排列数
   int[] dp = new int[n+1];

   // 2. 确定递推公式
   // 物品 nums[i] = {1,2}
   // dp[j] += dp[j-nums[i]]

   // 3. 初始化
   // dp[0]=1是递推公式的前提
   dp[0] = 1;

   int[] nums = {1,2};
   // 4. 确定遍历顺序
   // 排列问题: 先背包容量,后物品, 外循环背包容量从0从前往后遍历
   for(int j = 0; j <= n; j++) {
      for(int i = 0; i < nums.length; i++) {
         if(j >= nums[i]) {
            dp[j] += dp[j-nums[i]];
         }
      }
   }

   return dp[n];
}

4. 超级次方

/**
 *  快速幂
 *  比如a^[2,3,7,8]
 *     a^2378
 *  = (a^237)^10 * a^8
 *  = ((a^23)^10 * a^7) * a^8
 *  = ((a^2)^10 * a^3 * a^7) * a^8
 *  = a^0......
 */
public int superPow(int a, int[] b) {
   Deque<Integer> queue = new ArrayDeque<>();
   for (int i : b) {
      queue.add(i);
   }
   // 重载该方法
   return superPow(a, queue);
}

private int superPow(int a, Deque<Integer> queue) {
   // [2,3,7,8]从后往前依次出栈,为空时,为0次幂
   if(queue.isEmpty()) {
      return 1;
   }
   // 比如a^[2,3,7,8] = a^2378
   // 第一步(a^237)^10 * a^8
   int lastBit = queue.removeLast();
   // a^8
   int part1 = myPow(a, lastBit);
   // (a^237)^10
   int part2 = myPow(superPow(a, queue), 10);

   return (part1 * part2) % 1337;
}

private int myPow(int a, int b) {
   if(b == 0) {
      return 1;
   }

   // 奇数
   a %= 1337;
   if(b % 2 == 1) {
      return (a * myPow(a, b-1)) % 1337;
   } else {
      return myPow(a*a, b/2);
   }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值