剑指offer(第二版)刷题记录
03、数组中重复的数字
方法一:使用Set
class Solution {
public int findRepeatNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
int flag = -1;
for (int num : nums) {
if (!set.add(num)){ //添加成功返回true,否失败返回false
flag = num;
break;
}
}
return flag;
}
}
方法二:原地交换
利用条件:在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。
class Solution {
public int findRepeatNumber(int[] nums) {
int index = 0;
while (index < nums.length){
if (nums[index] == index){ //数字已在对应索引位置,无需交换
index++;
continue;
}
if (nums[nums[index]] == nums[index]){ //找到一组重复的值
return nums[index];
}
//将此数字交换至对应索引位置
int temp = nums[index];
nums[index] = nums[temp];
nums[temp] = temp;
}
return -1;
}
}
04、二维数组中的查找
方法一:直接遍历查找
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
for (int[] row : matrix){
for (int val : row){
if (val == target){
return true;
}
}
}
return false;
}
}
方法二:二分查找
对每一行进行二分查找。
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
for (int[] row : matrix) {
int index = search(row, target);//对每一行进行二分查找
if (index >= 0){ //找到的话返回对应的下标,没找到返回-1
return true;
}
}
return false;
}
//二分查找
public int search(int[] nums, int target){
int left = 0, right = nums.length - 1;
while (left <= right){
int mid = (left + right) / 2;
int num = nums[mid];
if (num == target){
return mid;
}else if (num < target){
left = mid + 1; //向右查找
}else {
right = mid - 1; //向左查找
}
}
return -1;
}
}
方法三:Z 字形查找
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
//判断数组是否合法
if (matrix.length == 0 || matrix == null){
return false;
}
int m = matrix.length, n = matrix[0].length;
int x = 0, y = n - 1; //矩阵的右上角开始查找
while (x < m && y >=0){ //左下角为边界
if (matrix[x][y] == target){ //找到
return true;
}else if (matrix[x][y] < target){ //每一行都是升序的,这一行左边的元素不用找了
x++;
}else { //每一列都是升序的,这一列下边的元素不用找了
y--;
}
}
return false;
}
}
05、替换空格
方法一:使用可变长字符串StringBuilder
class Solution {
public String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
int len = s.length();
for (int i = 0; i < len; i++) {
if (s.charAt(i) == ' '){
sb.append("%20");
}else {
sb.append(s.charAt(i));
}
}
return sb.toString();
}
}
06、从尾到头打印链表
方法一:使用集合(也可以使用栈)
class Solution {
public int[] reversePrint(ListNode head) {
if (head == null){
return new int[0];
}
List<Integer> list = new ArrayList<>();
ListNode temp = head;
while (temp != null){
list.add(temp.val);
temp = temp.next;
}
int[] arr = new int[list.size()];
int index = arr.length - 1;
for (int value : list){
arr[index--] = value;
}
return arr;
}
}
07、重建二叉树
方法一:分治算法
class Solution {
int[] preOrder;//前序遍历的数据
Map<Integer, Integer> map = new HashMap<>();//存放中序遍历的节点值和对应的下标
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preOrder = preorder;
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return recur(0, 0, inorder.length - 1);
}
public TreeNode recur(int root, int left, int right) {
if (left > right){ //停止递归
return null;
}
TreeNode node = new TreeNode(preOrder[root]); //建立根节点
int rootIndex = map.get(preOrder[root]); //找到根节点在中序遍历数组中的下标
node.left = recur(root + 1, left, rootIndex - 1); //向左递归,left~right为左子树的的范围
//root + rootIndex - left + 1 : 跟节点索引+左子树长度+1
node.right = recur(root + rootIndex - left + 1, rootIndex + 1, right); //向右递归,left~right为右子树的的范围
return node; // 回溯返回根节点
}
}
09、用两个栈实现队列
方法一:双栈
class CQueue {
LinkedList<Integer> inStack;
LinkedList<Integer> outStack;
public CQueue() {
inStack = new LinkedList<Integer>();
outStack = new LinkedList<Integer>();
}
public void appendTail(int value) {
inStack.addLast(value);
}
public int deleteHead() {
if (!outStack.isEmpty()){
return outStack.removeLast();//outStack中有元素,直接弹出最上面的元素
}
if (inStack.isEmpty()){//inStack和outStack中都没有元素,返回-1
return -1;
}else {//inStack中有元素,弹出一次入outStack,再弹出outStack最上面的元素
while (!inStack.isEmpty()){
outStack.addLast(inStack.removeLast());
}
return outStack.removeLast();
}
}
}
10- I、斐波那契数列
方法一:动态规划
用三个变量来代替前两项和当前项。
边计算边取模。
class Solution {
public int fib(int n) {
int a = 0, b = 1, sum = 0;
for (int i = 0; i < n; i++) {
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
}
10- II. 青蛙跳台阶问题
方法一:动态规划
当最后一步剩下一个台阶时共f(n-1)种情况;当最后一步剩下两个台阶时共f(n-2)种情况。所以一共f(n-1)+f(n-2)种情况。类似于斐波那契数列。
class Solution {
public int numWays(int n) {
int a = 1, b = 1, sum = 0;
for (int i = 0; i < n; i++) {
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
}
11、旋转数组的最小数字
方法一:二分查找
class Solution {
public int minArray(int[] numbers) {
int left = 0, right = numbers.length - 1, mid;
while (left <= right){
mid = (left + right) / 2;
if (numbers[mid] < numbers[right]){ //向左查找
right = mid;
}else if (numbers[mid] > numbers[right]){ //向右查找
left = mid + 1;
}else {
right--;//缩小范围
}
}
return numbers[left];
}
}
12、矩阵中的路径
方法一:dfs+剪枝
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (dfs(board, words, i, j, 0)){
return true;
}
}
}
return false;
}
//dfs
public boolean dfs(char[][] board, char[] words, int i, int j, int index){
//如果超了边界或者匹配失败
if (i < 0 || i > board.length - 1 || j < 0 || j > board[0].length - 1 || board[i][j] != words[index]){
return false;
}
//匹配到了字符串的最后
if (index == words.length - 1){
return true;
}
board[i][j] = '\0';//标记,'\0'是为了不和字符重复
//向四个方向递归
boolean flag = dfs(board, words, i + 1, j, index + 1) || dfs(board, words, i, j + 1, index + 1) ||
dfs(board, words, i - 1, j, index + 1) || dfs(board, words, i, j - 1, index + 1);
board[i][j] = words[index];//剪纸回溯
return flag;
}
}
13、机器人的运动范围
方法一:dfs
class Solution {
int m, n, k;
boolean[][] visited;//标记位置是否已经访问
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
this.visited = new boolean[m][n];
return dfs(0, 0, 0, 0);
}
/**
* @param x :纵坐标的数位和
* @param y :横坐标的数位和
*/
public int dfs(int i, int j, int x, int y){
//当到达边界,x+y大于k,被标记过时,返回
if (i >= m || j >= n || x + y > k || visited[i][j]){
return 0;
}
visited[i][j] = true;//标记已经访问
int x2 = (i + 1) % 10 == 0 ? x - 8 : x + 1;//向下走一步后纵坐标的位数和
int y2 = (j + 1) % 10 == 0 ? y - 8 : y + 1;//向右走一步后横坐标的位数和
return 1 + dfs(i + 1, j, x2, y) + dfs(i, j + 1, x, y2);//向下和向右递归
}
}
方法二:bfs
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];//标记位置是否已经访问,没有赋值默认全是false
int res = 0;
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{0, 0, 0, 0});
while (!queue.isEmpty()){
int[] val = queue.poll();
int i = val[0], j = val[1], x = val[2], y = val[3];
//当到达边界,x+y大于k,被标记过时,返回
if (i >= m || j >= n || x + y > k || visited[i][j]){
continue;
}
visited[i][j] = true;
res++;
int x2 = (i + 1) % 10 == 0 ? x - 8 : x + 1;//向下走一步后纵坐标的位数和
int y2 = (j + 1) % 10 == 0 ? y - 8 : y + 1;//向右走一步后横坐标的位数和
queue.offer(new int[]{i + 1, j, x2, y});
queue.offer(new int[]{i, j + 1, x ,y2});
}
return res;
}
}
14- I、剪绳子
方法一:数学推导、贪心
以下数学推导总体分为两步:① 当所有绳段长度相等时,乘积最大。② 最优的绳段长度为 3 。
class Solution {
public int cuttingRope(int n) {
if (n <= 3){ //当绳子长度小于等于3时,直接剪出一段长度为1的绳子
return n - 1;
}else {
//按每段绳子长度为3来分
int x = n / 3;
int y = n % 3;
if (y == 0){ //剩下长度为0时,为3的x次方
return (int)Math.pow(3, x);
}else if (y == 1){ //剩下长度为1时,把最后的1+3换成2+2
return (int)Math.pow(3, x - 1) * 2 * 2;
}else{ //剩下长度为2时,直接在最后*2
return (int)Math.pow(3, x) * 2;
}
}
}
}
方法二:动态规划
转移方程:dp[i]=max(j×(i−j),j×dp[i−j])
class Solution {
public int cuttingRope(int n) {
if (n <= 3) { //当绳子长度小于等于3时,直接剪出一段长度为1的绳子
return n - 1;
}
int[] dp = new int[n + 1];
dp[2] = 1; //初始化前三项
for (int i = 3; i <= n; i++) { //i为绳子的长度
int curMax = 0;
for (int j = 1; j < i; j++) { //剪去绳子的长度
curMax = Math.max(curMax, Math.max(j * (i - j), j * dp[i - j]));
}
dp[i] = curMax;
}
return dp[n];
}
}
14- II、剪绳子II
方法一:循环求余
class Solution {
public int cuttingRope(int n) {
if (n <= 3){ //当绳子长度小于等于3时,直接剪出一段长度为1的绳子
return n - 1;
}
long res = 1;//最后的乘积
if (n % 3 == 1){
res = 4;
n -= 4;
}else if (n % 3 == 2){
res = 2;
n -= 2;
}
while (n > 0){
res = res * 3 % 1000000007;
n -= 3;
}
return (int)res;
}
}
15、二进制中 1 的个数
方法一:位运算
class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count = 0;
while (n != 0){
count += 1 & n;
n >>>= 1; //>>>表示无符号右移
}
return count;
}
}
方法二:巧用 n&(n-1)
n-1:二进制数字 n 最右边的 1 变成 0 ,此 1 右边的 0 都变成 1 。
n&(n-1):二进制数字 n最右边的 1 变成 0 ,其余不变。
class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count = 0;
while (n != 0){
n &= n-1;//消去数字n最右边的1
count++;//消除一个,总数加一
}
return count;
}
}
16、数值的整数次方
方法一:快速幂
class Solution {
public double myPow(double x, int n) {
if (x == 0){ //直接返回0,防止后面1/x时报错
return 0;
}
long m = n;//防止遇见n=-2147483648时n=-n报错,先存入long类型中
double res = 1.0;
if (m < 0){ //当n<0的情况
m = -m;
x = 1 / x;
}
while (m > 0){
if ((m & 1) == 1){
res *= x;
}
x *= x;//更新x
m >>= 1;//更新m右移
}
return res;
}
}
17、打印从 1 到最大的 n 位数
方法一:普通解法
class Solution {
public int[] printNumbers(int n) {
int len = (int)Math.pow(10, n) - 1; //数组的长度为10的n次方-1
int[] arr = new int[len];
for (int i = 0; i < len; i++) {
arr[i] = i + 1;
}
return arr;
}
}
方法二:dfs+全排列+大数
class Solution {
char[] num;//存放数组中每个数的每位的字符
int[] ans;//需要返回的数组
int count = 0;//下标
int n;
public int[] printNumbers(int n) {
this.n = n;
num = new char[n];
ans = new int[(int) (Math.pow(10, n) - 1)];
dfs(0);
return ans;
}
private void dfs(int n) {
if (n == this.n) {
String tmp = String.valueOf(num); //char数组转字符串
int curNum = Integer.parseInt(tmp);//字符串转数组,可以去除最前面的0
if (curNum!=0) { //去掉值为0的数
ans[count++] = curNum;
}
return;
}
for (char i = '0'; i <= '9'; i++) {
num[n] = i;//固定每一位的值
dfs(n + 1);//固定下一位的值
}
}
}
18、删除链表的节点
方法一:
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if (head.val == val){ //如果要删除的是head,直接返回head.next
return head.next;
}
ListNode temp = head;
while (temp.next != null){
if (temp.next.val == val){
temp.next = temp.next.next;
}else {
temp = temp.next;
}
}
return head;
}
}
21、调整数组顺序使奇数位于偶数前面
方法一:双指针
class Solution {
public int[] exchange(int[] nums) {
int left = 0, right = nums.length - 1;
int temp = 0;
while (left < right){ //left=right时退出循环
while (left < right && nums[left] % 2 == 1){//左边指针找一个偶数
left++;
}
while (left < right && nums[right] % 2 == 0){//右边指针找一个奇数
right--;
}
//交换
temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
return nums;
}
}
22、链表中倒数第 k 个节点
方法一:顺序查找
先计算链表的长度n,在遍历到第n-k个节点处,返回该节点。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
int len = listLen(head);
ListNode temp = head;
for (int i = 0; i < len - k; i++) {
temp = temp.next;
}
return temp;
}
//计算链表的长度
public int listLen(ListNode head){
int len = 0;
ListNode temp = head;
while (temp != null){
len++;
temp = temp.next;
}
return len;
}
}
方法二:快慢指针
快指针比慢指针多k个节点。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head, slow = head;
for (int i = 0; i < k; i++) {
fast = fast.next;
}
while (fast != null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
24、反转链表
方法一:迭代+双指针
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null, cur = head;
ListNode temp;
while (cur != null){
temp = cur.next; //暂存后继节点cur.next
cur.next = pre; //修改next引用指向
pre = cur; //更新pre
cur = temp; //更新cur
}
return pre;
}
}
方法二:递归
class Solution {
ListNode head2;//反转后链表的头节点
public ListNode reverseList(ListNode head) {
dfs(head, null);
return head2;
}
public void dfs(ListNode cur, ListNode pre){
if (cur == null){ //终止条件
head2 = pre; //链表的最后一个节点为头节点
return;
}
dfs(cur.next, cur); //往后递归
cur.next = pre; //反转指向
}
}
25、合并两个排序的链表
方法一:迭代
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode();
ListNode temp = head;
while (l1 != null || l2 != null){
if (l1 == null){
temp.next = l2;
l2 = l2.next;
}else if (l2 == null){
temp.next = l1;
l1 = l1.next;
}else if (l1.val <= l2.val){
temp.next = l1;
l1 = l1.next;
}else {
temp.next = l2;
l2 = l2.next;
}
temp = temp.next;
}
return head.next;
}
}
方法二:递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null){
return l2;
}else if (l2 == null){
return l1;
}else if (l1.val <= l2.val){
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
26、树的子结构
方法一:
class Solution {
public boolean isSubStructure(TreeNode root, TreeNode subRoot) {
boolean dfs = dfs(root, subRoot);
return dfs;
}
public boolean dfs(TreeNode root, TreeNode subRoot){
if (root == null || subRoot == null){
return false;
}
return check(root, subRoot) || dfs(root.left, subRoot) || dfs(root.right, subRoot);
}
//查看root是否包含subRoot的结构
public boolean check(TreeNode root, TreeNode subRoot){
if (subRoot == null){ //如果subRoot为空了,说明匹配成功了
return true;
}
if (root == null || root.val != subRoot.val){ //如果root为空,或者值不想等了,说明匹配失败
return false;
}
return check(root.left, subRoot.left) && check(root.right, subRoot.right);//匹配左右节点
}
}
27、二叉树的镜像
方法一:递归
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
TreeNode tmp = root.left;
root.left = mirrorTree(root.right);
root.right = mirrorTree(tmp);
return root;
}
}
方法二:栈
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if (root == null){
return null;
}
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.push(root); //跟节点入栈
while (!stack.isEmpty()){ //栈空了停止循环
TreeNode node = stack.pop();
//让该节点的左右字节点入栈
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
//交换左右子节点
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
}
return root;
}
}
28、对称的二叉树
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null){
return true;
}
return check(root.left, root.right);
}
public boolean check(TreeNode root1, TreeNode root2){
if (root1 == null && root2 == null){ //匹配成功
return true;
}else if (root1 == null || root2 == null || root1.val != root2.val){ //匹配失败
return false;
}else {
return check(root1.left, root2.right) && check(root1.right, root2.left);
}
}
}
29、顺时针打印矩阵
方法一:
class Solution {
public int[] spiralOrder(int[][] matrix) {
if (matrix == null || matrix.length == 0){
return new int[0];
}
int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1;//矩阵的左、右、上、下边界
int[] arr = new int[(r + 1) * (b + 1)];
int index = 0;
while (true){
for (int i = l; i <= r; i++) { //从左向右打印
arr[index++] = matrix[t][i];
}
if (++t > b){ //边界内缩1;若边界相遇则打印完毕
break;
}
for (int i = t; i <= b; i++) { //从上向下打印
arr[index++] = matrix[i][r];
}
if (--r < l){
break;
}
for (int i = r; i >= l; i--) { //从右向左打印
arr[index++] = matrix[b][i];
}
if (t > --b){
break;
}
for (int i = b; i >= t; i--) { //从下向上打印
arr[index++] = matrix[i][l];
}
if (r < ++l){
break;
}
}
return arr;
}
}
30、包含 min 函数的栈
方法一:使用栈+优先队列
class MinStack {
Stack<Integer> stack;
Queue<Integer> queue;//优先队列
/** initialize your data structure here. */
public MinStack() {
stack = new Stack<Integer>();
queue = new PriorityQueue<Integer>();
}
public void push(int x) {
stack.push(x);
queue.add(x);
}
public void pop() {
int val = stack.pop();
queue.remove(val);
}
public int top() {
return stack.peek();
}
public int min() {
return queue.peek();
}
}
方法二:双栈
栈A中存储所有元素。
栈B中存储A中所有非严格降序的元素。
class MinStack {
Stack<Integer> A, B;
public MinStack() {
A = new Stack<Integer>();
B = new Stack<Integer>();
}
public void push(int x) {
A.push(x);
if(B.empty() || x <= B.peek()){ //B中只压入比A栈顶小或者相等的元素
B.push(x);
}
}
public void pop() {
int val = A.pop();
if(val == B.peek()){ //A中该元素出栈了,B也出栈
B.pop();
}
}
public int top() {
return A.peek();
}
public int min() {
return B.peek();
}
}
31、栈的压入、弹出序列
方法一:自己写的,效率较低
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack = new Stack<>();
int index1 = 0, index2 = 0;
while (index1 < pushed.length){
if (pushed[index1] == popped[index2]){ //要入栈的数正好是要弹出的数
index1++;
index2++;
}else if (!stack.isEmpty() && stack.peek() == popped[index2]){ //需要弹出栈顶的元素
stack.pop();
index2++;
}else { //入栈
stack.push(pushed[index1]);
index1++;
}
}
//把栈中剩下的元素一次弹出比较
for (int i = 0; i < stack.size(); i++) {
int val = stack.pop();
if (val != popped[index2]){
return false;
}
index2++;
}
return true;
}
}
方法二:官方答案,感觉效率更慢
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack = new Stack<>();
int index = 0;
for (int value : pushed){
stack.push(value); //入栈
while (!stack.isEmpty() && stack.peek() == popped[index]){ //循环判断与出栈
stack.pop();
index++;
}
}
return stack.isEmpty(); //最后看栈是不是为空
}
}
32 - I、从上到下打印二叉树
方法一:bfs
class Solution {
public int[] levelOrder(TreeNode root) {
if (root == null){
return new int[0];
}
Queue<TreeNode> queue = new LinkedList<>();
List<Integer> list = new ArrayList<>();
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[] arr = new int[list.size()];
int index = 0;
for (int val : list){
arr[index++] = val;
}
return arr;
}
}
32 - II、从上到下打印二叉树 II
方法一:bfs
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null){
return new ArrayList<>();
}
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> list = new ArrayList<>();
queue.offer(root);
while (!queue.isEmpty()){
List<Integer> temp = new ArrayList<>();
int len = queue.size();//queue.size()会变化,先赋值给len
for (int i = 0; i < len; i++) { //此时queue中的所有元素为当前层的元素
TreeNode node = queue.poll();
temp.add(node.val);
if (node.left != null){
queue.offer(node.left);
}
if (node.right != null){
queue.offer(node.right);
}
}
list.add(temp);
}
return list;
}
}
32 - III、从上到下打印二叉树 III
方法一:bfs+双端队列
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null){
return new ArrayList<>();
}
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> list = new ArrayList<>();
queue.offer(root);
while (!queue.isEmpty()){
LinkedList<Integer> temp = new LinkedList<>(); //使用LinkedList作为双端队列使用
int len = queue.size();
for (int i = 0; i < len; i++) {
TreeNode node = queue.poll();
if (list.size() % 2 == 0){ //如果为偶数层,加入队列的尾部
temp.addLast(node.val);
}else { //如果为奇数层,加入队列的头部
temp.addFirst(node.val);
}
if (node.left != null){
queue.offer(node.left);
}
if (node.right != null){
queue.offer(node.right);
}
}
list.add(temp);
}
return list;
}
}
方法二:bfs+倒序
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null){
return new ArrayList<>();
}
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> list = new ArrayList<>();
queue.offer(root);
while (!queue.isEmpty()){
List<Integer> temp = new ArrayList<>();
int len = queue.size();
for (int i = 0; i < len; i++) {
TreeNode node = queue.poll();
temp.add(node.val);
if (node.left != null){
queue.offer(node.left);
}
if (node.right != null){
queue.offer(node.right);
}
}
if (list.size() % 2 == 1){
Collections.reverse(temp); //奇数层进行反转
}
list.add(temp);
}
return list;
}
}
33、二叉搜索树的后序遍历序列
方法一:递归+分治
class Solution {
public boolean verifyPostorder(int[] postorder) {
return recur(postorder, 0, postorder.length - 1);
}
public boolean recur(int[] postorder, int i, int j){
if (i >= j){ //递归结束
return true;
}
int p = i;
while (postorder[p] < postorder[j]){ //找到一个比头节点大的数
p++;
}
int m = p; //把该数赋值给m
while (postorder[p] > postorder[j]){ //找到一个小于等于头节点的数
p++;
}
//如果p指导了j,说明m到j-1全都比头节点大,符合条件
return p == j && recur(postorder, i, m - 1) && recur(postorder, m, j - 1);
}
}
方法二:单调栈
class Solution {
public boolean verifyPostorder(int[] postorder) {
Stack<Integer> stack = new Stack<>();
int root = Integer.MAX_VALUE;
//倒序遍历
for(int i = postorder.length - 1; i >= 0; i--) {
if(postorder[i] > root) return false;//不满足二叉搜索树
//发现递减序列
while(!stack.isEmpty() && stack.peek() > postorder[i])
root = stack.pop();//更新root
stack.add(postorder[i]);
}
return true;
}
}
34、二叉树中和为某一值的路径
方法一:回溯法
class Solution {
LinkedList<List<Integer>> lists = new LinkedList<>();//保存最后结果
LinkedList<Integer> list = new LinkedList<>();//保存当前的路径
public List<List<Integer>> pathSum(TreeNode root, int target) {
dfs(root, target);
return lists;
}
public void dfs(TreeNode node, int sum){
if (node == null){
return;
}
list.add(node.val);//记录当前节点
//判断当前节点是不是叶子节点,并且路径满足条件
if (sum == node.val && node.left == null && node.right == null){
lists.add(new LinkedList(list));
list.removeLast(); //回溯
return;
}
sum -= node.val;//更新sum
dfs(node.left, sum); //向左递归
dfs(node.right, sum); //向右递归
list.removeLast(); //回溯
}
}
35、复杂链表的复制
方法一:哈希表
class Solution {
Map<Node, Node> map = new HashMap<>();//存放链表的节点和复制链表的节点
public Node copyRandomList(Node head) {
if (head == null){
return null;
}
if (!map.containsKey(head)){ //如果该节点还没有创造
Node headnew = new Node(head.val);
map.put(head, headnew);
headnew.next = copyRandomList(head.next);
headnew.random = copyRandomList(head.random);
}
return map.get(head);//该节点已经创造过
}
}
方法二:拼接+拆分
class Solution {
public Node copyRandomList(Node head) {
if (head == null){
return null;
}
Node cur = head;
//1.复制各节点,并构建拼接链表
while (cur != null){
Node temp = new Node(cur.val);
temp.next = cur.next;
cur.next = temp;
cur = cur.next.next;
}
//2.构建各新节点的 random 指向
cur = head;
while (cur != null) {
if (cur.random != null){
cur.next.random = cur.random.next;
}
cur = cur.next.next;
}
//3.拆分两链表
Node res = head.next;
Node pre = head;
cur = head.next;
while (cur.next != null){
pre.next = pre.next.next;
cur.next = cur.next.next;
pre = pre.next;
cur = cur.next;
}
pre.next = null;//单独处理原链表尾节点
return res;//返回新链表头节点
}
}
36、二叉搜索树与双向链表
方法一:中序遍历
class Solution {
Node pre, head;
public Node treeToDoublyList(Node root) {
if (root == null){
return null;
}
mixDfs(root);
//最后处理头和尾节点,形成循环双向链表
head.left = pre;
pre.right = head;//此时pre已经指向链表尾部
return head;
}
//中序遍历
public void mixDfs(Node curNode){
if (curNode == null){
return;
}
mixDfs(curNode.left);
if (pre != null){ //如果pre不是空
pre.right = curNode;
}else { //否则,把当前节点为头节点
head = curNode;
}
curNode.left = pre;
pre = curNode;//更新pre
mixDfs(curNode.right);
}
}
37、序列化二叉树
方法一:BFS
class Codec {
public String serialize(TreeNode root) {
if(root == null) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(node != null) { //如果节点不为空
sb.append(node.val + ",");
queue.add(node.left);
queue.add(node.right);
}else { //如果节点为空
sb.append("null,");
}
}
sb.deleteCharAt(sb.length() - 1);//删除最后一个逗号
sb.append("]");
return sb.toString();
}
public TreeNode deserialize(String data) {
if(data.equals("[]")) {
return null;
}
String[] vals = data.substring(1, data.length() - 1).split(",");//去掉首位的括号,并根据逗号隔开
TreeNode root = new TreeNode(Integer.parseInt(vals[0]));//字符串转数字
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
int index = 1;
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(!vals[index].equals("null")) {
node.left = new TreeNode(Integer.parseInt(vals[index]));
queue.add(node.left);
}
index++;
if(!vals[index].equals("null")) {
node.right = new TreeNode(Integer.parseInt(vals[index]));
queue.add(node.right);
}
index++;
}
return root;
}
}
38、字符串的排列
方法一:全排列+回溯+剪枝
class Solution {
List<String> list = new LinkedList<>();//存放排列好的字符串
char[] chars;//存放字符串的每一个字符
public String[] permutation(String s) {
chars = s.toCharArray();
dfs(0);
return list.toArray(new String[list.size()]);//把集合转成数组
}
public void dfs(int index){
if (index == chars.length - 1){ //找到一个排列
list.add(String.valueOf(chars));//字符数组转成字符串
return;
}
//防止同一层的元素重复
Set<Character> set = new HashSet<>();
//让每一个字符都作为这一层的字符
for (int i = index; i < chars.length; i++) {
if (set.contains(chars[i])){ //存在重复元素,剪枝
continue;
}
set.add(chars[i]);
swap(i, index);//将第i个元素固定在index位置
dfs(index + 1);//处理下一层
swap(i, index);//回溯
}
}
//交换
public void swap(int x, int y){
char temp = chars[x];
chars[x] = chars[y];
chars[y] = temp;
}
}
39、数组中出现次数超过一半的数字
方法一:哈希表
class Solution {
public int majorityElement(int[] nums) {
int len = nums.length;
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < len; i++) {
int val = map.getOrDefault(nums[i], 0);
//边添加边判断
if (val + 1 > len / 2){
return nums[i];
}else {
map.put(nums[i], val + 1);
}
}
return -1;
}
}
方法二:排序
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];//排序后数组中间那个数一定是结果
}
}
方法三;摩尔投票法
class Solution {
public int majorityElement(int[] nums) {
int res = 0, votes = 0;
for (int num : nums){
if (votes == 0){ //票数抵消了,假设当前数为众数
res = num;
}
if (num == res){ //相同票数+1
votes += 1;
}else { //否则-1
votes -= 1;
}
}
return res;
}
}
40、最小的 k 个数
方法一:排序
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
Arrays.sort(arr);
return Arrays.copyOf(arr, k);
}
}
方法二:优先队列
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] nums = new int[k];
PriorityQueue<Integer> queue = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
queue.offer(arr[i]);
}
for (int i = 0; i < k; i++) {
nums[i] = queue.poll();
}
return nums;
}
}
方法三:快速排序
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
quickSort(arr, 0, arr.length - 1);//快速排序
return Arrays.copyOf(arr, k);
}
//快速排序
public void quickSort(int[] arr, int l, int r){
if (l >= r){ //子数组长度为1时停止递归
return;
}
int i = l, j = r;//哨兵划分(以 arr[l] 作为基准数)
while (i < j){
while (i < j && arr[j] >= arr[l]){ //右边找一个小于哨兵的数
j--;
}
while (j < j && arr[i] <= arr[l]){ //左边找一个大于哨兵的数
i++;
}
swap(arr, i, j);//交换
}
swap(arr, i, l);//与哨兵交换
quickSort(arr, l, i - 1);
quickSort(arr, i + 1, r);
}
//交换
public void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
方法四:快速排序+数组划分
若 k < i,则前k小的数组在 i 左边,递归左子数组。
若 k > i,则前k小的数组在 i 右边,递归右子数组。
若 k = i,则前k小的数组已经找到,直接返回。
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (k >= arr.length){ //特殊情况,直接返回
return arr;
}
return quickSort(arr, 0, arr.length - 1, k);//快速排序
}
//快速排序
public int[] quickSort(int[] arr, int l, int r, int k){
int i = l, j = r;//哨兵划分(以 arr[l] 作为基准数)
while (i < j){
while (i < j && arr[j] >= arr[l]){ //右边找一个小于哨兵的数
j--;
}
while (j < j && arr[i] <= arr[l]){ //左边找一个大于哨兵的数
i++;
}
swap(arr, i, j);//交换
}
swap(arr, i, l);//与哨兵交换
if (i > k){ //向左找
return quickSort(arr, l, i - 1, k);
}else if (i < k){ //向右找
return quickSort(arr, i + 1, r, k);
}else { //找到
return Arrays.copyOf(arr, k);
}
}
//交换
public void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
41、数据流中的中位数
方法一:优先队列、大顶堆、小顶堆
class MedianFinder {
Queue<Integer> A;
Queue<Integer> B;
/** initialize your data structure here. */
public MedianFinder() {
A = new PriorityQueue<>();//小顶堆,从小到大排
B = new PriorityQueue<>((x, y) -> (y - x));//大顶堆,从大到小排
}
public void addNum(int num) {
if (A.size() == B.size()){ //如果A和B大小相同,添加进B中,再从B中拿出一个最大的放入A中
B.add(num);
A.add(B.poll());
}else { //否则,放入A中,再从A中拿出一个最小的放入B中
A.add(num);
B.add(A.poll());
}
}
public double findMedian() {
if (A.size() == B.size()){ //结果为A和B顶的元素的平均值
return (double) (A.peek() + B.peek()) / 2;
}else { //结果为A顶的元素
return (double)A.peek();
}
}
}
42、连续子数组的最大和
方法一:动态规划
class Solution {
public int maxSubArray(int[] nums) {
int res = nums[0];//nums中可能只有一个元素
for (int i = 1; i < nums.length; i++) {
if (nums[i - 1] > 0){ //如果上一个元素为负的,则不加
nums[i] += nums[i - 1];
}
res = Math.max(res, nums[i]);//最大结果
}
return res;
}
}
43、1~n 整数中 1 出现的次数
方法一:
当 cur=0 时,此时1出现的次数为 high * digit ;
当 cur=1 时,此时1出现的次数为 high * digit + low + 1 ;
当 cur 为其他数时,此时1出现的次数为 (high + 1) * digit ;
class Solution {
public int countDigitOne(int n) {
int count = 0;
int high = n / 10, cur = n % 10, low = 0, digit = 1;
while (high != 0 || cur != 0){ //如果high和cur都为0时,说明已经越过最高位,因此跳出
if (cur == 0){ //当cur为0时
count += high * digit;
}else if (cur == 1){ //当cur为1时
count += high * digit + low + 1;
}else { //当cur为其他时
count += (high + 1) * digit;
}
//更新
low += cur * digit;
cur = high % 10;
high /= 10;
digit *= 10;
}
return count;
}
}
44、数字序列中某一位的数字
方法一:(太难了)
确定所求数位的所在数字的位数。
确定所求数位所在的数字。
确定所求数位在 num 的哪一数位。
class Solution {
public int findNthDigit(int n) {
int digit = 1;//数位(这个数字有多少位)
long start = 1;//数位开始的数字
long count = 9;//这个数位有多少数字
//1.确定所求数位的所在数字的位数
while (n > count) {
n -= count;
//更新
digit += 1;//加一
start *= 10;//乘10
count = digit * start * 9;//digit * start * 9
}
//2.确定所求数位所在的数字
long num = start + (n - 1) / digit;
//3.确定所求数位在 num 的哪一数位
return Long.toString(num).charAt((n - 1) % digit) - '0';
}
}
45、把数组排成最小的数
方法一:内置函数
class Solution {
public String minNumber(int[] nums) {
int len = nums.length;
String[] str = new String[len];
for (int i = 0; i < len; i++) {
str[i] = String.valueOf(nums[i]);
}
//重新定义排序规则,把字符串进行排序
Arrays.sort(str, (x, y) -> (x + y).compareTo(y + x));
StringBuilder sb = new StringBuilder();
for (String s : str){
sb.append(s);
}
return sb.toString();
}
}
方法二:快速排序
class Solution {
public String minNumber(int[] nums) {
int len = nums.length;
String[] str = new String[len];
for (int i = 0; i < len; i++) {
str[i] = String.valueOf(nums[i]);
}
//重新定义排序规则,把字符串进行排序
quickSort(str, 0, len - 1);
StringBuilder sb = new StringBuilder();
for (String s : str){
sb.append(s);
}
return sb.toString();
}
//快速排序
public void quickSort(String[] str, int l, int r){
if (l >= r){
return;
}
int i = l, j = r;
while (i < j){
//注意快速排序这里是右边先动
while (i < j && (str[j] + str[l]).compareTo(str[l] + str[j]) >= 0){
j--;
}
while (i < j && (str[i] + str[l]).compareTo(str[l] + str[i]) <= 0){
i++;
}
swap(str, i, j);
}
swap(str, i, l);
quickSort(str, l, i - 1);
quickSort(str, i + 1, r);
}
public void swap(String[] str, int i, int j){
String temp = str[i];
str[i] = str[j];
str[j] = temp;
}
}
46、把数字翻译成字符串
方法一:动态规划
空间优化:使用a和b代替动态规划的数组。
class Solution {
public int translateNum(int num) {
String str = String.valueOf(num);
int a = 1, b = 1;//作为dp[1]和dp[0]
for (int i = 2; i <= str.length(); i++) {
String temp = str.substring(i - 2, i);
int c = 0;
//temp.compareTo:首先逐个字符比较ascll码值,一样的话比较字符串长度。
if (temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0){ //在[10, 25]范围,dp[i] = dp[i - 1] + dp[i - 2]
c = a + b;
}else { //否则,dp[i] = dp[i - 1]
c = a;
}
//更新
b = a;
a = c;
}
return a;
}
}
47、 礼物的最大价值
方法一:动态规划
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length, n = grid[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0){//左上角
continue;
}else if (i == 0 && j != 0){//最上边
grid[i][j] += grid[i][j - 1];
}else if (i != 0 && j == 0){//最左边
grid[i][j] += grid[i - 1][j];
}else {//其他情况
grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
}
}
}
return grid[m - 1][n - 1];
}
}
方法二:动态规划优化
当矩阵特别大时,可先初始化矩阵第一行和第一列,再开始遍历递推。
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 初始化第一列
for (int i = 1; i < m; i++) {
grid[i][0] += grid[i - 1][0];
}
// 初始化第一行
for (int j = 1; j < n; j++) {
grid[0][j] += grid[0][j - 1];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
}
}
return grid[m - 1][n - 1];
}
}
48、最长不含重复字符的子字符串
方法一:动态规划(太难了)
dp[i] 表示以第 i 个字符为结尾的 “最长不重复子字符串” 的长度。
空间优化:用 temp 代替 dp 数组。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>();
int res = 0, temp = 0;
for(int j = 0; j < s.length(); j++) {
int i = map.getOrDefault(s.charAt(j), -1); // 获取索引i
if (temp >= j - i){ //该字符在dp[i - 1]的范围内,dp[i] = j - i
temp = j - i;
}else { //该字符不在dp[i - 1]的范围内,dp[i] = dp[i - 1] + 1
temp = temp + 1;
}
res = Math.max(res, temp); //更新最大值
map.put(s.charAt(j), j); //更新哈希表,把最近相同字符的索引放进去
}
return res;
}
}
49、丑数
方法一:动态规划
dp[i] = min(a * 2, b * 3, c * 4);
class Solution {
public int nthUglyNumber(int n) {
int a = 1, b = 1, c = 1;//前面某个丑数的下标
int[] dp = new int[n + 1];
dp[1] = 1;//1为第一个丑数
for (int i = 2; i <= n; i++) {
int n1 = dp[a] * 2, n2 = dp[b] * 3, n3 = dp[c] * 5;//当前丑数一定为前面某个丑数乘2或3或5。
dp[i] = Math.min(Math.min(n1, n2), n3);//取最小值
//取的那个值,那么这个值就不会再用了,下标加一
if (dp[i] == n1) a++;
if (dp[i] == n2) b++;
if (dp[i] == n3) c++;
}
return dp[n];
}
}
50、第一个只出现一次的字符
方法一:哈希表
class Solution {
public char firstUniqChar(String s) {
Map<Character, Integer> map = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
map.put(c, map.getOrDefault(c, 0) + 1);
}
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (map.get(c) == 1){
return c;
}
}
return ' ';
}
}
方法二:比较每个字符第一次出现和最后一次出现的下标
class Solution {
public char firstUniqChar(String s) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (s.indexOf(c) == s.lastIndexOf(c)){
return c;
}
}
return ' ';
}
}
方法三:哈希表存放字符的下标
class Solution {
public char firstUniqChar(String s) {
Map<Character, Integer> map = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
if (map.containsKey(s.charAt(i))){ //如果字符存在就存放-1
map.put(s.charAt(i), -1);
}else { //如果不存在就存放下标
map.put(s.charAt(i), i);
}
}
int index = s.length();
for (Map.Entry<Character, Integer> entry : map.entrySet()) {
int value = entry.getValue();
if (value != -1 && value < index){ //找到map中不是-1,并且最小的下标
index = value;
}
}
//如果index没变,说明map中全为-1,全重复过
//否则,返回找到的下标
return index == s.length() ? ' ' : s.charAt(index);
}
}
51、数组中的逆序对
方法一:归并排序
在每次合并时,统计逆序的个数。
class Solution {
int[] arr, temp;//temp为一个额外空间
int count;//统计结果的个数
public int reversePairs(int[] nums) {
this.arr = nums;
temp = new int[nums.length];
mergeSort(0, nums.length - 1);//归并排序
return count;
}
//分解+合并方法
public void mergeSort(int left, int right){
if (left < right){ //终止条件
int mid = (left + right) / 2;
mergeSort(left, mid);//向左递归
mergeSort(mid + 1, right);//向右递归
merge(left, mid, right);//合并
}
}
//合并方法
public void merge(int left, int mid, int right){
int i = left;//左边序列的初始索引
int j = mid + 1;//右边序列的初始索引
int index = 0;//临时数组的索引
//把左边和右边序列放入到临时数组
while (i <= mid && j <= right){//遍历两个序列,直到一边处理完毕停止
if (arr[i] <= arr[j]){
temp[index++] = arr[i];
i++;
}else {
temp[index++] = arr[j];
j++;
count += mid + 1 - i;//统计逆序的个数
}
}
//如果左边序列有剩余的元素,填充到temp中
while (i <= mid){
temp[index++] = arr[i];
i++;
}
//如果右边序列有剩余的元素,填充到temp中
while (j <= right){
temp[index++] = arr[j];
j++;
}
//将temp中的元素拷贝到原始数组中,注意不是每次都拷贝所有的元素
index = 0;
int index2 = left;
while (index2 <= right){
arr[index2++] = temp[index++];
}
}
}
52、两个链表的第一个公共节点
方法一:哈希表
class Solution {
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
ListNode temp = headA;
while (temp != null){
set.add(temp);
temp = temp.next;
}
temp = headB;
while (temp != null){
if (set.add(temp)){
temp = temp.next;
}else {
return temp;
}
}
return null;
}
}
方法二:双指针
遍历的长度为第一个链表的长度加上第二个链表的开头到相交节点或者末尾的长度。
class Solution {
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode PA = headA, PB = headB;
while (PA != PB){
PA = PA == null ? headB : PA.next;
PB = PB == null ? headA : PB.next;
}
return PA;
}
}
53 - I、在排序数组中查找数字 I
方法一:二分查找
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
int mid;
int index = -1;//target的下标
//二分查找
while (left <= right){
mid = (left + right) / 2;
if (target == nums[mid]){
index = mid;
break;
}else if (target < nums[mid]){
right = mid - 1;
}else {
left = mid + 1;
}
}
if (index == -1){//如果不存在target
return 0;
}
int res = 1;
left = index - 1;
right = index + 1;
//从index向左找
while (left >= 0 && nums[left] == nums[index]){
res++;
left--;
}
//从index向右找
while (right <= nums.length - 1 && nums[right] == nums[index]){
res++;
right++;
}
return res;
}
}
53 - II、0~n-1 中缺失的数字
方法一:哈希表
class Solution {
public int missingNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
set.add(nums[i]);
}
int res = 0;
for (int i = 0; i <= nums.length; i++) {
if (!set.contains(i)){
res = i;
break;
}
}
return res;
}
}
方法二:直接遍历
class Solution {
public int missingNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i){
return i;
}
}
return nums.length;//第[0,n)个数没有缺失,缺失的是数字n
}
}
方法三:位运算
x^x = 0 ; x^0 = x .
class Solution {
public int missingNumber(int[] nums) {
int x = 0;
for (int i = 0; i < nums.length; i++) {
x ^= nums[i];
}
for (int i = 0; i <= nums.length; i++) {
x ^= i;
}
return x;
}
}
方法四:数学
class Solution {
public int missingNumber(int[] nums) {
int len = nums.length;
int count = 0;
//计算1-n的总和
for (int i = 1; i <= len; i++) {
count += i;
}
//总和减去数组中的每个数字
for (int i = 0; i < len; i++) {
count -= nums[i];
}
return count;
}
}
方法五:二分查找
class Solution {
public int missingNumber(int[] nums) {
int left = 0, right = nums.length - 1;
int mid;
while (left <= right){
mid = (left + right) / 2;
if (nums[mid] == mid){
left = mid + 1;
}else {
right = mid - 1;
}
}
//跳出循环时,left和right分别指向“右子数组的首位元素” 和 “左子数组的末位元素” 。
return left;
}
}
54、二叉搜索树的第 k 大节点
方法一:优先队列
class Solution {
PriorityQueue<Integer> queue;
int k;
public int kthLargest(TreeNode root, int k) {
this.k = k;
queue = new PriorityQueue<>();
preList(root);
return queue.peek();//返回队列的头部元素
}
public void preList(TreeNode node){
if (node == null){
return;
}
queue.offer(node.val);
if (queue.size() > k){ //保持队列里放的是k个数
queue.poll();
}
preList(node.left);
preList(node.right);
}
}
方法二:中序遍历+提前返回
class Solution {
int k, res;
public int kthLargest(TreeNode root, int k) {
this.k = k;
mixList(root);
return res;
}
public void mixList(TreeNode node){
if (node == null){
return;
}
mixList(node.right);//先遍历右子树
//处理当前节点
if (k == 0){ //如果k=0了,后面的遍历就不执行了
return;
}
k--;
if (k == 0){
res = node.val;//找到
}
mixList(node.left);//再遍历左子树
}
}
55 - I、 二叉树的深度
方法一:dfs
class Solution {
public int maxDepth(TreeNode root) {
if (root == null){
return 0;
}
return Math.max(maxDepth(root.left) + 1, maxDepth(root.right) + 1);
}
}
55 - II、平衡二叉树
方法一:前序遍历+判断深度(从顶至底)
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null){
return true;
}
//判断当前节点,并向左和向右递归
return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
//计算树的深度
public int height(TreeNode node){
if (node == null){
return 0;
}
return Math.max(height(node.left) + 1, height(node.right) + 1);
}
}
方法二:后序遍历+剪枝(从底至顶)
class Solution {
public boolean isBalanced(TreeNode root) {
return postList(root) != -1;
}
//后序遍历
public int postList(TreeNode root){
if (root == null){
return 0;
}
int left = postList(root.left);
int right = postList(root.right);
//剪枝,如果子树不是平衡数,直接返回
if (left == -1 || right == -1 || Math.abs(left - right) > 1){
return -1;
}else {
return Math.max(left, right) + 1;
}
}
}
56 - I、数组中数字出现的次数
方法一:位运算
class Solution {
public int[] singleNumbers(int[] nums) {
int n = 0;
for (int i = 0; i < nums.length; i++) { //得到x异或y的结果
n ^= nums[i];
}
int m = 1;
//结果的两个数一定有一个二进制位不同,因此n中一定存在一个二进制位为1,找到n中为1的二进制位
while ((m & n) == 0){
m = m << 1;
}
int x =0, y = 0;
//再把nums中的元素根据m分成两部分,转换成数组中只有一个元素不重复的情况
for (int i = 0; i < nums.length; i++) {
if ((nums[i] & m) == 0){
x ^= nums[i];
}else {
y ^=nums[i];
}
}
return new int[]{x, y};
}
}
56 - II、数组中数字出现的次数 II
方法一:哈希表
class Solution {
public int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
int res = 0;
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
for (Map.Entry<Integer, Integer> value : map.entrySet()) {
if (value.getValue() == 1){
res = value.getKey();
}
}
return res;
}
}
方法二:位运算
class Solution {
public int singleNumber(int[] nums) {
int[] arr = new int[32]; //int有32位,arr代表每一位的值
int m = 1;
int sum = 0;
for (int i = 0; i < 32; i++) {
for (int j = 0; j < nums.length; j++) {
if ((m & nums[j]) != 0){ //判断多少个数在此位上是1
arr[i]++;
}
}
arr[i] %= 3; //得到结果在此位上是不是1
sum += arr[i] * m;//同时统计结果
m = m << 1;
}
return sum;
}
}
57、和为 s 的两个数字
方法一:双指针
原数组为递增的,可以让两个指针分别指向数组的最左边和最右边。
若 nums[left] + nums[right] < target,left左移;
若 nums[left] + nums[right] > target,right右移;
若 nums[left] + nums[right] = target,找到答案。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
int left = 0, right = nums.length - 1;
int sum;
while (left < right){
sum = nums[left] + nums[right];
if (sum == target){ //找到结果
res[0] = nums[left];
res[1] = nums[right];
break;
}else if (sum < target){ //left左移
left += 1;
}else { //right右移
right -= 1;
}
}
return res;
}
}
57 - II、和为 s 的连续正数序列
方法一:滑动窗口
当 sum > target时,sum 先减 left , left 再右移;
当 sum < target时,right 先右移, sum 再加right;
当 sum = target时,记录数组,并且 sum 先减 left ,left 再右移。
class Solution {
public int[][] findContinuousSequence(int target) {
int left = 1, right = 2;//滑动窗口的左边界和右边界
int sum = 3;//滑动窗口中元素总和
List<int[]> list = new ArrayList<>();
while (left < right){ //left = right时退出循环
if (sum == target){
int[] arr = new int[right - left + 1];
for (int i = left; i <= right; i++) {
arr[i - left] = i;
}
list.add(arr);
//此时left也右移
sum -= left;
left++;
}else if (sum >= target){ //先减left再右移
sum -= left;
left++;
}else { //先右移再加right
right++;
sum += right;
}
}
//list.size()可以填[0,list.size()]的数,小于list.size()的话会自动变为size
return list.toArray(new int[list.size()][]);
}
}
58 - I、翻转单词顺序
方法一:分割
trim():移除字符串两侧的空白字符或其他预定义字符
class Solution {
public String reverseWords(String s) {
//trim():移除字符串两侧的空白字符或其他预定义字符
String[] strs = s.trim().split(" ");
StringBuilder sb = new StringBuilder();
for (int i = strs.length - 1; i >= 0 ; i--) {
if (!strs[i].equals("")){
sb.append(strs[i]);
sb.append(" ");
}
}
return sb.toString().trim();
}
}
58 - II、左旋转字符串
方法一:字符串切片
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n, s.length()) + s.substring(0, n);
}
}
59 - I、滑动窗口的最大值
方法一:单调队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
int index = 1;
Deque<Integer> queue = new LinkedList<>();
//先处理前k个元素
for (int i = 0; i < k; i++) {
//保证队列是单调队列
while(!queue.isEmpty() && queue.peekLast() < nums[i]){
queue.removeLast();
}
queue.addLast(nums[i]);
}
res[0] = queue.peekFirst();//第一个元素为最大的
// 形成窗口后
for (int i = k; i < nums.length; i++) {
if (queue.peekFirst() == nums[i - k]){ //删除窗口外的元素
queue.removeFirst();
}
while(!queue.isEmpty() && queue.peekLast() < nums[i]){
queue.removeLast();
}
queue.addLast(nums[i]);
res[index++] = queue.peekFirst();//记录最大值
}
return res;
}
}
59 - II. 队列的最大值
方法一:维护一个单调递减的双端队列
class MaxQueue {
Queue<Integer> queue;//普通队列
Deque<Integer> deque;//单调递减的队列
public MaxQueue() {
queue = new LinkedList<>();
deque = new LinkedList<>();
}
public int max_value() {
if (queue.isEmpty()){
return -1;
}else {
return deque.peekFirst();
}
}
public void push_back(int value) {
//保证deque单调递减
while (!deque.isEmpty() && deque.peekLast() < value){
deque.pollLast();
}
queue.offer(value);
deque.addLast(value);
}
public int pop_front() {
if (queue.isEmpty()){
return -1;
}else {
int val = queue.poll();
if (val == deque.peekFirst()){
deque.removeFirst();
}
return val;
}
}
}
60、n 个骰子的点数
方法一:动态规划
dp[i] [j] :i 代表骰子的个数,j 代表点数的总和。
初始状态:dp[1] [1] ~ dp[1] [6] = 1;
转移方程:dp[i] [j] = dp[i - 1] [j - 1] + ··· + dp[i - 1] [j - 6];
class Solution {
public double[] dicesProbability(int n) {
int[][] dp = new int[n + 1][6 * n + 1];
//初始化当只有一个骰子时的情况
for (int i = 1; i <= 6; i++) {
dp[1][i] = 1;
}
for (int i = 2; i <= n; i++) { //2到n个骰子时的情况
for (int j = i; j <= 6 * i; j++) { //所有可能的点数
for (int k = 1; k <= 6; k++) { //dp[i][j] = dp[i - 1][j - 1] + ··· + dp[i - 1][j - 6]; 上一个骰子可能有6个点数
if (j <= k){ // 不存在的情况
break;
}
dp[i][j] += dp[i - 1][j - k];
}
}
}
double[] res = new double[5 * n + 1];//可能的点数为[n, 6n],一共5*n+1种情况
int index = 0;
for (int i = n; i <= 6 * n; i++) { //dp数组的最后一行是答案
res[index++] = dp[n][i] / (double)Math.pow(6, n); //所有可能的情况为6的n次方
}
return res;
}
}
61、扑克牌中的顺子
方法一:Set集合
构成顺子的条件:
- 无重复的牌(大小王除外)
- 最大牌 - 最小牌 < 5(大小王除外)。
class Solution {
public boolean isStraight(int[] nums) {
Set<Integer> set = new HashSet<>();
int max = 0, min = 14;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == 0) continue; //跳过大小王
if (!set.add(nums[i])){ //若有重复,提前返回false
return false;
}
max = Math.max(max, nums[i]); //最大牌
min = Math.min(min, nums[i]); //最小牌
}
return max - min < 5; //最大牌 - 最小牌 < 5 则可构成顺子
}
}
方法二:排序
class Solution {
public boolean isStraight(int[] nums) {
int king = 0;
Arrays.sort(nums);
for (int i = 0; i < nums.length - 1; i++) {
if (nums[i] == 0){
king++; //统计大小王数量
continue;
}
if (nums[i] == nums[i + 1]){ //最大牌 - 最小牌 < 5 则可构成顺子
return false;
}
}
return nums[nums.length - 1] - nums[king] < 5; //最大牌 - 最小牌 < 5 则可构成顺子
}
}
62、圆圈中最后剩下的数字
方法一:集合(效率低)
class Solution {
public int lastRemaining(int n, int m) {
int index = 0;
List<Integer> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
list.add(i);
}
while (list.size() != 1){
index = (index + m - 1) % list.size();
list.remove(index);
}
return list.get(0);
}
}
方法二:递归
new = (old + m) % n;
class Solution {
public int lastRemaining(int n, int m) {
if (n == 0){ //只有一个士兵时,编号为0
return 0;
}
return (lastRemaining(n - 1, m) + m) % n; //new = (old + m) % n;
}
}
方法二:迭代
class Solution {
public int lastRemaining(int n, int m) {
int res = 0;//只有一个士兵,编号为0的情况
for (int i = 2; i <= n; i++) { //有n个士兵
//更新士兵的编号
res = (res + m) % i;//new = (old + m) % i; i为剩余的士兵个数
}
return res;
}
}
63、股票的最大利润
方法一:一次遍历
class Solution {
public int maxProfit(int[] prices) {
int maxProfit = 0;//最大利润
int minValue = Integer.MAX_VALUE;//最小的价格
for (int i = 0; i < prices.length; i++){
if (prices[i] < minValue){
minValue = prices[i];//找最小价格
}else {
maxProfit = Math.max(prices[i] - minValue, maxProfit);//找最大利润
}
}
return maxProfit;
}
}
64、求 1 + 2 + … + n
方法一:逻辑符运算
class Solution {
public int sumNums(int n) {
boolean x = n > 1 && (n += sumNums(n - 1)) > 0;//如果左边成立,右边不执行,会被短路。
return n;
}
}
65、不用加减乘除做加法
方法一:位运算
class Solution {
public int add(int a, int b) {
while (b != 0){ //进位为0时,退出循环
int c = (a & b) << 1;//计算求出进位
a ^= b;//计算非进位和,赋值给a
b = c;//进位赋值给b
}
return a;
}
}
66、构建乘积数组
方法一:
class Solution {
public int[] constructArr(int[] a) {
int len = a.length;
if (len == 0){
return new int[0];
}
int[] res = new int[len];
res[0] = 1;//初始化左上角的元素
int temp = 1;//初始化右下角的元素
for (int i = 1; i < len; i++) { //计算左下三角部分
res[i] = res[i - 1] * a[i - 1];
}
for (int i = len - 1; i >= 0; i--) { //计算右上三角部分
res[i] *= temp;
temp *= a[i];
}
return res;
}
}
68 - I、二叉搜索树的最近公共祖先
方法一:迭代
两个条件:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉搜索树中。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p.val > q.val){ //优化:若保证 p.val < q.val,可减少循环中判断的次数
TreeNode temp = p;
p = q;
q = temp;
}
while (true){
if (root.val > q.val){ //p,q 都在 root 的左子树中
root = root.left;
}else if (root.val < p.val){ //p,q 都在 root 的右子树中
root = root.right;
}else { //其他情况都满足结果
break;
}
}
return root;
}
}
方法二:递归
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val < p.val && root.val < q.val){
return lowestCommonAncestor(root.right, p, q);
}else if (root.val > p.val && root.val > q.val){
return lowestCommonAncestor(root.left, p, q);
}else {
return root;
}
}
}
68 - II. 二叉树的最近公共祖先
方法一:dfs
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root.val == p.val || root.val == q.val){ //遇见目标节点,或者到null了,直接返回
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left == null && right == null) return null; //如果left和right都为空
if (left == null) return right; //如果一边空,返回另一边的节点
if (right == null) return left;
return root; //如果left和right都不为空,该节点为公共祖先节点
}
}
713

被折叠的 条评论
为什么被折叠?



