剑指offer题集

个人博客文章地址

文章目录

1. 二维数组中的查找 *

  • 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

  • 思路分析:

    1. 数组从上到下递增,从左到右递增,所以可以巧妙的从左下角开始查询。
    2. 左下角往上是递减的往右递增,当左下角的值小于target,就可以往右移,否则上移。
  • 代码实现:

public class Solution {

    public boolean Find(int target, int [][] array) {
        // 排除空元素的情况
        if (array.length == 0 || array[0].length == 0) {
            return false;
        }
        
        int i = array.length - 1;
        int j= 0;
        
        while (i >= 0 && j < array.length) {
            if (array[i][j] == target) {
                return true;
            } else if (array[i][j] < target) {
                j++;
            } else {
                i--;
            }
        }
        return false;
    }
}

2. 替换空格

  • 请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
  • 代码实现:
public class Solution {
    public String replaceSpace(StringBuffer str) {
    	return str.toString().replaceAll(" ", "%20");
    }
}

3. 从尾到头打印链表

  • 输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
  • 代码实现:
import java.util.ArrayList;
import java.util.Collections;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> list = new ArrayList<>();
        
        ListNode cur = listNode;
        
        while (cur != null) {
            list.add(cur.val);
            cur = cur.next;
        }
        // 反转
        Collections.reverse(list);
        return list;
    }
}

4. 重建二叉树 *

  • 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
  • 代码实现:
/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if (pre.length == 0) {
            return null;
        }
        
        return restoreTree(pre, 0, pre.length-1, in, 0, in.length-1);
        
    }
    
    /**
	 * 根据前序和中序构建二叉树
	 * @param pre 前序遍历序列
	 * @param start 前序起点下标
	 * @param end 前序终点下标
	 * @param in 后序序遍历序列
	 * @param left 后序起点下标
	 * @param right 后序终点下标
	 * @return 返回根节点
	 */
	public TreeNode restoreTree(int[] pre, int start, int end, int[] in, int left, int right) {
		if (start > end) {
			return null;
		}
		
		// 创建根节点,根节点的值维为前序遍历序列的第一个。
		TreeNode root = new TreeNode(pre[start]);

		// 在中序遍历序列中查找根节点下标,下标左边的为左子树节点,右边为右子树节点。
		int mid = findKey(in, pre[start]);

		// 计算左子树节点的个数
		int leftCount = mid - left;
		
		// 递归构建,确定左子树的前序遍历序列和中序遍历序列的范围。
		root.left = restoreTree(pre, start + 1, start + leftCount, in, left, mid - 1);
		root.right = restoreTree(pre, start + leftCount + 1, end, in, mid + 1, right);
		return root;
	}

	public int findKey(int[] arr, int key) {
		for (int i = 0; i < arr.length; i++) {
			if (arr[i] == key) {
				return i;
			}
		}
		return -1;
	}
}

5. 用两个栈实现队列

  • 用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
  • 思路分析:
    1. 栈1负责入队,栈2负责出队.
    2. 当出队时,栈2为空,则将栈1中元素依次弹栈压入栈2中,在由栈2弹出。
  • 代码实现:
import java.util.Stack;

public class Solution {
	// 栈1负责入队
    Stack<Integer> stack1 = new Stack<Integer>();
    // 栈2 负责出队,当出队时,栈2为空,则将栈1中元素依次弹栈压入栈2中,在由栈2弹出。
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        stack1.push(node);
    }
    
    public int pop() {
        if (!stack2.isEmpty()) {
        	return stack2.pop();
        } else {
            // 将栈1的元素转移到栈2
        	while (!stack1.isEmpty()) {
        		stack2.push(stack1.pop());
        	}
            
        	if (stack2.isEmpty()){
        		return 0;
        	} else {
        		return stack2.pop();
        	}
        }
    }
}

6. 旋转数组的最小数字 *

  • 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
  • 思路分析:
  • 根据题意说明是一个递增数组的旋转,所以如题所示【3,4,5】,【1,2】还是局部递增的,在这种的数组中查找,一般选择二分的方法;基本模型有了,下面试着分析:
    1. 先取出中间的数值,和最后一个比较5>2 说明mid之前的某些部分旋转到了后面,所以下次寻找 low = mid+1 开始;
    2. 取出的中间值要是小于high,说明mid-high之间都应为被旋转的部分,所以最小应该在mid的前面,但是也有可能当前的mid 就是最小的值 所以下次需找的应该 从mid开始,也即high = mid 开始
    3. 当mid == high的时候,说明数组中存在着相等的数值,可能是这样的形式 【2,2,2,2,1,2】所以应该选择的high 应该递减1 作为下次寻找的上界。
  • 代码实现:
import java.util.ArrayList;
public class Solution {
    public int minNumberInRotateArray(int[] array) {
        if (array.length == 0) {
            return 0;
        }
        
        int left = 0;
        int right = array.length - 1;
        int mid;
        while(left < right) {
            mid = (left + right) / 2;
            if (array[mid] > array[right]) { // 说明最小值一定在右边,不包含mid
                left = mid + 1;
            } else if (array[mid] < array[right]) { // 说明最小值一定在右边,包含mid
                right = mid;
            } else { // 说明存在相同的值,最小值还在右边,向右移一位
                left++;
            }
        }
        
        return array[left];
    } 
    
}

7. 斐波那契数列

  • 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
    n<=39
  • 代码实现:
public class Solution {
    public int Fibonacci(int n) {
        // F(n) = F(n-1) + F(n-2)
        // F(0) = 0; F(1) = 1;
        if (n == 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            return Fibonacci(n-1) + Fibonacci(n-2);
        }
    }
}

8. 跳台阶 *

  • 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
  • 思路分析:
    1. 假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1);
    2. 假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2);
    3. 由1和2假设可以得出总跳法为: f(n) = f(n-1) + f(n-2) ;
    4. n=1时1种,n=2时2种;
  • 代码实现:
public class Solution {
    
    public int JumpFloor(int target) {
        if (target < 3) {
            return target;
        } else {
            return 2 * JumpFloor(target-1);
        }
    }
}

9. 变态跳台阶 *

  • 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
  • 思路分析:
    1. n = 1时,只有1种跳法,f(1) = 1;
    2. n = 2时,会有两个跳得方式,一次1阶或者2阶,f(2) = f(2-1) + f(2-2);
    3. n = 3时,会有三种跳得方式,1阶、2阶、3阶,f(3) = f(3-1)+f(3-2)+f(3-3);
    4. n = n时,会有n中跳的方式,1阶、2阶…n阶,得出结论: f(n) = f(n-1)+f(n-2)+…+f(n-(n-1)) + f(n-n) => f(0) + f(1) + f(2) + f(3) + … + f(n-1);
    5. 简化:
      f(n-1) = f(0) + f(1)+f(2)+f(3) + … + f((n-1)-1) = f(0) + f(1) + f(2) + f(3) + … + f(n-2)
      f(n) = f(0) + f(1) + f(2) + f(3) + … + f(n-2) + f(n-1) = f(n-1) + f(n-1)
      可以得出: f(n) = 2*f(n-1)
  • 代码实现:
public class Solution {
    public int JumpFloorII(int target) {
         if (target < 3) {
            return target;
        } else {
            return 2 * JumpFloorII(target-1);
        }
    }
}

10. 矩形覆盖 *

  • 我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
  • 思路分析:
    1. 当target=0时,2*0的矩形有0种。
    2. 当target=1时,2*1的矩形有1种。
    3. 当target=2时,2*1的矩形有2种,一种是2个2*1的矩形左右摆,另一种是2个1*2的矩形上下摆。
    4. 当target=n时,分两步,第一步,先摆一个2*1的矩形,那么总共有f(n-1)种。第二步,先摆一个1*2的矩形,为了覆盖2*n的矩形必须在其下面在摆一个1*2的矩形,所以总共有f(n-2)种。则f(n)=f(n-1)+f(n-2);
  • 代码实现:
public class Solution {
    public int RectCover(int target) {
        if (target < 3) {
            return target;
        } else {
            return RectCover(target-1) + RectCover(target-2);
        }
    }
}

11. 二进制中1的个数

  • 输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
  • 思路分析:
    1. 如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1。
    2. 如一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
  • 代码实现:
public class Solution {
    public int NumberOf1(int n) {
    // 直接调javaapi
        return Integer.bitCount(n);
    }
    
    // 采用二进制来计算
    public int NumberOf1(int n) {
        int count = 0;
        while (n != 0) {
            count++;
            n = (n-1) & n;
        }
        return count;
    }
    
}

12. 数值的整数次方 * (简单快速幂)

  • 给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。保证base和exponent不同时为0。
  • 思路分析:
    1. 全面考察指数的正负、底数是否为零等情况。
    2. 考察幂运算,如13的二进制为1101,所以10^13 = 10^1000 * 10^0100 * 10^0001 = 10(23) * 10(22) * 10(20) = 10^8 * 10^4 * 10。
  • 代码实现:
public class Solution {
    public double Power(double base, int exponent) {
        int n = Math.abs(exponent);
        double res = 1;
        // 处理底数和指数为0的情况
        if (base == 0) {
            return 0;
        }
        if (exponent == 0) {
            return 1;
        }

        while (n != 0) {
            // 当二进制位上为1,则需要乘上当前的base
            if ((n & 1) != 0) {
                res *= base;
            }
            base *= base;
            n >>= 1;
        }
        
        // 如果指数小于0,则需要取倒数
        return exponent > 0 ? res : 1 / res;
        
  }
}

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

  • 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
  • 代码实现:
public class Solution {
    public void reOrderArray(int [] array) {
        // 采用插入排序的思想实现,遇到奇数就往前移
        
        int index = 0;    //记录已经摆好位置的奇数的个数
        for (int i = 0; i < array.length; i++) {
            if (array[i] % 2 == 1) { 
                int j = i;
                while (j > index) {
                    int temp = array[j];
                    array[j] = array[j-1];
                    array[j-1] = temp;
                    j--;
                }
                index++;
            }
        }
    }
}

14. 链表中倒数第k个节点* (快慢指针)

  • 输入一个链表,输出该链表中倒数第k个结点。
  • 思路分析:
    1. 定义快指针和慢指针。
    2. 快指针先走 k-1 步,到达第 k 个节点。
    3. 然后两指针同时齐步走,当快指针到达末尾时,慢指针在倒数第 k 个节点上。
  • 代码实现:
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
        // 暴力法
    public ListNode FindKthToTail(ListNode head,int k) {

        // 1. 先求出总的节点个数count。
        ListNode cur = head;
        int count = 0;
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        
        // 2. 用总的节点个数减去k得到要求节点是顺数第几个。
        int index = count - k;
        
        if (index < 0) {    // 处理小于0的情况
            return null;
        }
        
        cur = head;
        count = 0;
        while (count < index) {
            count++;
            cur = cur.next;
        }
        return cur;
    }
    
    // 快慢指针
    public ListNode FindKthToTail(ListNode head,int k) {
        // 排除为空和k<0的情况
        if (head == null || k <= 0) {
            return null;
        }
        
        int count = 1;
        // 定义快慢指针
        ListNode fast = head;
        ListNode slow = head;
        
        // 先让快指针走k-1步
        while (count < k) {
            count++;
            fast = fast.next;
            
            // 排除k-1步还没走完,快指针已经到达了终点
            if (fast == null) {
                return null;
            }
        }
        // 快指针到达终点结束循环
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

15. 反转链表

  • 输入一个链表,反转链表后,输出新链表的表头。
  • 思路分析:
    1. 创建一个新的头节点。
    2. 遍历原始链表,每遍历一个节点,就创建一个与该节点值一样的对象。
    3. 将这个对象插入到新的头节点的后面。
    4. 遍历完后即得到了反转链表。
  • 代码实现:
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode ReverseList(ListNode head) {
        ListNode newHead = new ListNode (0);
        ListNode cur = head;
        ListNode temp = null;
        
        while (cur != null) {
            if (newHead.next == null) { // 新节点next为空则直接插入
                temp = new ListNode(cur.val);
                newHead.next = temp;
            } else {
                newHead.next = new ListNode(cur.val);
                newHead.next.next = temp;
                temp = newHead.next;
            }
            cur = cur.next;
        }
        return newHead.next;
    }
}

16. 合并两个排序的链表

  • 输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
  • 思路分析:
    1. 其实这题就可以模仿归并排序合并的思想,将两个有序的数组合并成一个有序的数组。
  • 代码实现:
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        // 创建一个新的头节点便于操作
        ListNode head = new ListNode(0);
        ListNode cur = head;    // 遍历新链表的指针
        ListNode cur1 = list1;    //遍历链表1的指针
        ListNode cur2 = list2;    // 遍历链表2的指针
        
        // 同时遍历两个链表,将值小的插入到新链表的尾部
        while (cur1 != null && cur2 != null) {
            if (cur1.val < cur2.val) {
                cur.next = new ListNode(cur1.val);
                cur1 = cur1.next;
            } else {
                cur.next = new ListNode(cur2.val);
                cur2 = cur2.next;
            }
            cur = cur.next;
        }
        
        // 处理链表2遍历完,链表1没遍历完的情况
        while (cur1 != null) {
            cur.next = new ListNode(cur1.val);
            cur1 = cur1.next;
            cur = cur.next;
        }
        // 处理链表1遍历完,链表2没遍历完的情况
        while (cur2 != null) {
            cur.next = new ListNode(cur2.val);
            cur2 = cur2.next;
            cur = cur.next;
        }
        
        return head.next;
    }
}

17. 树的子结构 *

  • 输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
  • 代码实现:
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    public static boolean HasSubtree(TreeNode root1, TreeNode root2) {
        boolean result = false;
        //当Tree1和Tree2都不为零的时候,才进行比较。否则直接返回false
        if (root2 != null && root1 != null) {
            //如果找到了对应Tree2的根节点的点
            if(root1.val == root2.val){
                //以这个根节点为为起点判断是否包含Tree2
                result = doesTree1HaveTree2(root1,root2);
            }
            //如果找不到,那么就再去root的左儿子当作起点,去判断时候包含Tree2
            if (!result) {
                result = HasSubtree(root1.left,root2);
            }
             
            //如果还找不到,那么就再去root的右儿子当作起点,去判断时候包含Tree2
            if (!result) {
                result = HasSubtree(root1.right,root2);
               }
            }
            //返回结果
        return result;
    }
 
    public static boolean doesTree1HaveTree2(TreeNode node1, TreeNode node2) {
        //如果Tree2已经遍历完了都能对应的上,返回true
        if (node2 == null) {
            return true;
        }
        //如果Tree2还没有遍历完,Tree1却遍历完了。返回false
        if (node1 == null) {
            return false;
        }
        //如果其中有一个点没有对应上,返回false
        if (node1.val != node2.val) {  
                return false;
        }
         
        //如果根节点对应的上,那么就分别去子节点里面匹配
        return doesTree1HaveTree2(node1.left,node2.left) && doesTree1HaveTree2(node1.right,node2.right);
    }
}

18. 二叉树的镜像

  • 操作给定的二叉树,将其变换为源二叉树的镜像。
  • 思路分析:其实这题很简单就是递归的交互节点的左右子树
  • 代码实现:
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    public void Mirror(TreeNode root) {
        if (root == null) {
            return;
        }
        
        // 交换左右子树
        TreeNode temp = root.right;
        root.right = root.left;
        root.left = temp;
        
        // 继续递归交换子树
        Mirror(root.left);
        Mirror(root.right);
        
    }
}

19. 顺时针打印矩阵 *

  • 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
  • 思路分析:
    简单来说,就是不断地收缩矩阵的边界
    定义四个变量代表范围,up、down、left、right;
    1. 向右走存入整行的值,当存入后,该行再也不会被遍历,代表上边界的 up 加一,同时判断是否和代表下边界的 down 交错;
    2. 向下走存入整列的值,当存入后,该列再也不会被遍历,代表右边界的 right 减一,同时判断是否和代表左边界的 left 交错;
    3. 向左走存入整行的值,当存入后,该行再也不会被遍历,代表下边界的 down 减一,同时判断是否和代表上边界的 up 交错;
    4. 向上走存入整列的值,当存入后,该列再也不会被遍历,代表左边界的 left 加一,同时判断是否和代表右边界的 right 交错;
  • 代码实现:
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
        ArrayList<Integer> list = new ArrayList<>();
        if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return list;
        }
        int up = 0;
        int down = matrix.length-1;
        int left = 0;
        int right = matrix[0].length-1;
        while(true){
            // 最上面一行
            for(int col=left;col<=right;col++){
                list.add(matrix[up][col]);
            }
            // 向下逼近
            up++;
            // 判断是否越界
            if(up > down){
                break;
            }
            // 最右边一行
            for(int row=up;row<=down;row++){
                list.add(matrix[row][right]);
            }
            // 向左逼近
            right--;
            // 判断是否越界
            if(left > right){
                break;
            }
            // 最下面一行
            for(int col=right;col>=left;col--){
                list.add(matrix[down][col]);
            }
            // 向上逼近
            down--;
            // 判断是否越界
            if(up > down){
                break;
            }
            // 最左边一行
            for(int row=down;row>=up;row--){
                list.add(matrix[row][left]);
            }
            // 向右逼近
            left++;
            // 判断是否越界
            if(left > right){
                break;
            }
        }
        return list;
    }
}

20. 包含min函数的栈 *

  • 定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
  • 思路分析:
    1. 在看到这道题目的时候第一反应是要用一个最小值来保留当前栈中最小值,但是也能够很快地意识到比较麻烦的地方在于pop的时候怎么更新min值。
    2. 看了别人的题解之后都是使用了另外一个栈来保持在入栈过程中曾经做过最小值的值,pop的时候判断两个栈顶元素是否一致,一致的话都要pop,在这种情况下取最小值需要从保存最小值的栈顶元素取值。
    3. 不用另外一个栈,那么为了实现这一目的,在栈中需要保留冗余的曾经的最小值,这样能够比较方便到找到当前的最小值。
  • 代码实现:
import java.util.Stack;
 
public class Solution {
 
    //需要这样写来初始化stack,不然会报空指针push的时候
    private Stack<Integer> stack = new Stack<Integer>();
    private Integer minElement = Integer.MAX_VALUE;
 
    public void push(int node) {
        if(stack.empty()){
            minElement = node;
            stack.push(node);
        }else{
            if(node <= minElement){
                stack.push(minElement);//在push更小的值时需要保留在此值之前的最小值
                minElement = node;
            }
            stack.push(node);
        }
    }
 
    public void pop() {
 
       //增加最后一个元素以及栈为空时候的检测
        if(stack.size() == 0)return;
        if( minElement == stack.peek()){
            if(stack.size() >1){
                stack.pop();
                minElement = stack.peek();
            }else{
                minElement = Integer.MAX_VALUE;
            }
 
        }
        stack.pop();
    }
 
    public int top() {
        return stack.peek();
    }
 
    public int min() {
        return minElement;
    }
}

21. 栈的压入、弹出序列

  • 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
  • 思路分析:
    新建一个栈,遍历数组A,将元素依次压入栈中,入栈的过程中若栈顶元素等于数组B的当前元素,则出栈,直到不相等,则继续添加下一个元素。当数组A遍历完时,判断栈是否为空,若为空则返回true;
  • 代码实现:
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        // 1. 创建一个栈,模拟入栈出栈操作
        Stack<Integer> stack = new Stack<>();
        
        int cur = 0;
        
        for (int i = 0; i < popA.length; i++) {
            stack.push(pushA[i]);
            
            while (!stack.isEmpty() && stack.peek() == popA[cur]) {
                stack.pop();
                cur++;
            }
        }
        
        return stack.isEmpty() ? true : false;
        
        
    }
}

22. 从上往下打印二叉树

  • 从上往下打印出二叉树的每个节点,同层节点从左至右打印。
  • 思路分析:其实该题就是要写一个层次遍历,用队列实现即可。
  • 代码实现:
import java.util.ArrayList;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
import java.util.Queue;
import java.util.LinkedList;
public class Solution {
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        ArrayList<Integer> list = new ArrayList<>();
        if (root == null) {
            return list;
        }
        // 使用队列进行层次遍历
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);    // 根节点入队
        TreeNode cur;
        while (queue.size() > 0) { // 队列为空则遍历完成
            cur = queue.poll();    // 出队
            list.add(cur.val);
            if (cur.left != null) {    // 左节点不为空则入队
                queue.offer(cur.left);
            }
            if (cur.right != null) {
                queue.offer(cur.right);
            }
        }
        return list;
    }
}

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

  • 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
  • 思路分析:
    不断递归地确定出左子树区间和右子树区间,并且判断:左子树区间的所有结点值 < 根结点值 < 右子树区间所有结点值,这个条件是否满足即可。
  • 代码实现:
public class Solution {
    public boolean VerifySquenceOfBST(int[] sequence) {
        if (sequence == null || sequence.length == 0) {
            return false;
        }
        
        return isBST(sequence, 0, sequence.length - 1);
    }
 
    private boolean isBST(int[] seq, int start, int end) {
        if (start >= end) {
            return true;
        }
        
        int rootVal = seq[end];    // 根节点的值
        int split = start;
        // 因为根节点大于左子树的的值小于右子树的值,所以可以通过与根节点比较,来找到左右子树的分界线
        for (; split < end && seq[split] < rootVal; split++) ;
        
        // 判断右子树中的值是否全部大于根节点的值,出现一个不大于则说明该序列不是后序序列
        for (int i = split; i < end; i++) {
            if (seq[i] < rootVal) {
                return false;
            }
        }
        // 继续判断左子树和右子树是否满足后序遍历序列
        return isBST(seq, start, split - 1) && isBST(seq, split, end - 1);
    }
}

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

  • 输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前);
  • 思路分析:
    1. 路径一定是从根节点到叶子节点。所以就可以递归的找出所有的路径,将和等于target的存起来。
  • 代码实现:
import java.util.ArrayList;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    ArrayList<ArrayList<Integer>> ans;
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        ans = new ArrayList<ArrayList<Integer>>();
        
        if (root == null) {
            return ans;
        }
        
        dfs(new ArrayList<Integer>(), root, target);
        
        return ans;
    }
    
    public void dfs(ArrayList<Integer> list, TreeNode root, int target) {
        // 在调用该方法前,已经做了非空处理,所以可以直接将该节点的值添加到路径。
        list.add(root.val);
        target = target - root.val;    // 采用减的方式可以少传一个参数
        if (target < 0) {    // 优化,如果还未叶子节点,但target已经小于0了,则没必要继续递归下去
            return;
        } else if (0 == target && root.left == null && root.right == null) {
            // 当前路径总和等于target,并且当前节点是叶子节点,则将该路径保存
            ans.add(list);
            return;
        }
        if (root.left != null) { // 左子树不为空,则继续添加路径上的节点
            // 需要注意:这里需要重新new一个集合,因为集合为引用类型,如果不创建回溯的时候会产生影响。
            dfs(new ArrayList<>(list), root.left, target);
        }
        if (root.right != null) {
            dfs(new ArrayList<>(list), root.right, target);
        }

    }
}

25. 复杂链表的复制

  • 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
  • 思路分析:
    1. 用map保存旧节点和新节点之间的映射关系,如果节点已经存在那么使用存在的节点即可,如果不存在那么需要新开辟节点并存储他们之间的映射关系。
  • 代码实现:
/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
*/
import java.util.Map;
import java.util.HashMap;
public class Solution {
    public RandomListNode Clone(RandomListNode pHead)
    {
        if (pHead == null) {
            return null;
        }
        // map保存旧节点和新节点之间的映射关系
        Map<RandomListNode, RandomListNode> map = new HashMap<>();
        RandomListNode cur = pHead;    // 用来遍历旧链表的指针
        RandomListNode head = null;    // 新链表的头指针
        RandomListNode temp = null;    // 用来遍历新链表的指针
        
        while (cur != null) {
            if (head == null) {    // 复制头节点
                head = new RandomListNode(cur.label);
                temp = head;
                // 将旧节点和新节点的映射关系存入map中
                map.put(cur, temp);
                
                // 处理当前节点的random指针,注意random指针可能为空
                if (cur.random == null) {
                    temp.random = null;
                } else {
                    temp.random = new RandomListNode(cur.random.label);
                    map.put(cur.random, temp.random);
                }
            } else {
                // 处理非头节点,查看map中是否已经复制过了当前节点
                if (map.containsKey(cur)) {
                    // 复制过了直接指向
                    temp.next = map.get(cur);
                } else {
                    // 没有复制过,则新创建
                    temp.next = new RandomListNode(cur.label);
                    map.put(cur, temp.next);
                }
                
                // 将新链表指针后移,设置random对象
                temp = temp.next;
                
                // 也是先查看map
                if (map.containsKey(cur.random)) {
                    temp.random = map.get(cur.random);
                } else {
                    // 需要判断是否为空
                    if (cur.random == null) {
                        temp.random = null;
                    } else {
                        temp.random = new RandomListNode(cur.random.label);
                        map.put(cur.random, temp.random);
                    }
                }
                
            }
            
            cur = cur.next;
        }
        return head;
    }
}

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

  • 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
  • 思路分析:
    1. 二叉搜索树的中序遍历就是升序序列,所以先用集合存储二叉树的中序遍历。
    2. 遍历集合构建双向链表。
  • 代码实现:
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
import java.util.ArrayList;
public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null) {
            return null;
        }
        ArrayList<TreeNode> list = new ArrayList<>();
        // 将中序遍历的结果存入链表
        midOrder(list, pRootOfTree);
        
        TreeNode head = list.get(0);
        TreeNode temp = head;
        // 构建双向链表
        for (int i = 1; i < list.size(); i++) {
            temp.right = list.get(i);
            list.get(i).left = temp;
            temp = temp.right;
        }
        return head;
    }
    
    public void midOrder(ArrayList<TreeNode> list, TreeNode root) {
        if (root == null) {
            return;
        }
        midOrder(list, root.left);
        list.add(root);
        midOrder(list, root.right);
    }
}

27. 字符串的排列

  • 输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
  • 思路分析:
    1. 采用全排列的思想,对所有的字符进行交换,得到所有的排列结果。
    2. 优化,在全排列的过程中,如果两个字符相同则,则不进行交换。
  • 代码实现:
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Collections;
public class Solution {
    ArrayList<String> res;
    public ArrayList<String> Permutation(String str) {
        res = new ArrayList<>();
        if (str == null || "".equals(str)) {
            return res;
        }
        
        // 进行全排列
        dfs(str.toCharArray(), 0);
        
        Collections.sort(res); // 按字符序排序
        return res;
    }
    
    public void dfs(char[] cha, int step) {
        if (step == cha.length) {
            res.add(new String(cha));
        } else {
            // 优化,重复字符不进行交换
            HashSet<Character> set =  new HashSet<>();
            for (int i = step; i < cha.length; i++) {
                
                if (!set.contains(cha[i])) { // 重复直接跳过
                    set.add(cha[i]);
                    swap(cha, i, step);
                    dfs(cha, step+1);
                    swap(cha, i, step);
                }
                    
            }
        }
    }
    
    public void swap(char[] cha, int i, int j) {
        char temp = cha[i];
        cha[i] = cha[j];
        cha[j] = temp;
    }
}

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

  • 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
  • 思路分析:
    1. 遍历数组,用一个ans变量记录上次值和count变量记录出现的个数。
    2. 如果当前的值与ans的值相同则count加1,不同则count减1。因为答案出现的次数过半,所以最后ans记录的可能是答案。
    3. 做最后的判断。确定ans出现的次数是否大于一半。
  • 代码实现:
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        int ans = 0;
        int count = 0;
        for (int i = 0; i < array.length; i++) {
            if (count == 0) { // count为0,则记录当前的值
                ans = array[i];
                count = 1;
            } else {
                if (ans == array[i]) { // 相同则加1,不同则减1
                    count++;
                } else {
                    count--;
                }
            }
        }
        // 确定是否大于数组长度的一半
        count = 0;
        for (int i = 0; i < array.length; i++) {
            if (array[i] == ans) {
                count++;
            }
        }
        return count > (array.length/2) ? ans : 0;
    }
}

29. 最小的K个数 *(最大堆)

  • 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
  • 思路分析:
    1. 用最大堆(堆顶值最大,堆底最小)保存这k个数,每次只和堆顶比,如果比堆顶小,删除堆顶,新数入堆。
  • 代码实现:
import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        if (k == 0 || k > input.length) {
            return list;
        }
        // PriorityQueue使用跟普通队列一样,唯一区别是PriorityQueue会根据排序规则决定谁在队头,谁在队尾。
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                // 默认出队的是最小值,所以重写比较器,使出队的是最大值
                return o2.compareTo(o1);
            }
        });
        for (int i = 0; i < input.length; i++) {
            if (maxHeap.size() < k) {    // 队内元素小于k时,直接入队
                maxHeap.offer(input[i]);
            } else if (input[i] < maxHeap.peek()) {
                maxHeap.poll();
                maxHeap.offer(input[i]);
            }
        }
        for (Integer i: maxHeap) {
            list.add(i);
        }
        return list;
    }
}

30. 连续子数组的最大和*(动态规划)

  • HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

  • 思路分析:

    1. 典型的动态规划。dp[n]代表以当前元素为截止点的连续子序列的最大和,如果dp[n-1]>0,dp[n]=dp[n]+dp[n-1],因为当前数字加上一个正数一定会变大;
    2. 如果dp[n-1]<0,dp[n]不变,因为当前数字加上一个负数一定会变小。使用一个变量max记录最大的dp值返回即可。
  • 代码实现:

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            array[i] += array[i-1] > 0 ? array[i-1] : 0;
            max = Math.max(max, array[i]);
        }
        return max;
    }
}

31. 整数中1出现的次数(从1到n整数中1出现的次数)*

  • 求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
  • 思路分析:
    1. 遍历1-n的数,计算每个数中包含1的个数。
    2. 求1的个数用该数模于10等等于1,之后再除于10,直到等于0;
  • 代码实现:
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0;
        for (int i = n; i > 0; i--) {
            for (int j = i; j > 0; j/=10) {
                if (j % 10 == 1) {
                    count++;
                }
            }
        }
        return count;
    }
}

32. 把数组排成最小的数

  • 输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
  • 思路分析:
    1. 排序的思想,按特定的规则将这个数组排序,最后再将所有数拼起来。
    2. 排序规则是将两个数拼起来,比较大小。
  • 代码实现:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        
        ArrayList<Integer> list = new ArrayList<Integer>(numbers.length);
        
        for (int num: numbers) {
            list.add(num);
        }
        Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                String str1 = o1 + "" + o2;
                String str2 = o2 + "" + o1;
                return str1.compareTo(str2);
            }
        });
        StringBuilder sb = new StringBuilder();
        for (Integer num: list) {
            sb.append(num);
        }
        return sb.toString();
    }
}

33. 丑数 *

  • 把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
  • 思路分析:
    1. 丑数能够分解成2x3y5^z,所以只需要把得到的丑数不断地乘以2、3、5之后并放入他们应该放置的位置即可;
    2. 而此题的难点就在于如何有序的放在合适的位置。1乘以 (2、3、5)=2、3、5;2乘以(2、3、5)=4、6、10;3乘以(2、3、5)=6,9,15;5乘以(2、3、5)=10、15、25;
    3. 从这里我们可以看到如果不加策略地添加丑数是会有重复并且无序,而在2x,3y,5z中,如果x=y=z那么最小丑数一定是乘以2的,但关键是有可能存在x>y>z的情况,所以我们要维持三个指针来记录当前乘以2、乘以3、乘以5的最小值,然后当其被选为新的最小值后,要把相应的指针+1;因为这个指针会逐渐遍历整个数组,因此最终数组中的每一个值都会被乘以2、乘以3、乘以5,也就是实现了我们最开始的想法,只不过不是同时成乘以2、3、5,而是在需要的时候乘以2、3、5.
  • 代码实现:
public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index <= 0)return 0;
        int p2=0,p3=0,p5=0;//初始化三个指向三个潜在成为最小丑数的位置
        int[] result = new int[index];
        result[0] = 1;//
        for(int i=1; i < index; i++){
            result[i] = Math.min(result[p2]*2, Math.min(result[p3]*3, result[p5]*5));
            if(result[i] == result[p2]*2)p2++;//为了防止重复需要三个if都能够走到
            if(result[i] == result[p3]*3)p3++;//为了防止重复需要三个if都能够走到
            if(result[i] == result[p5]*5)p5++;//为了防止重复需要三个if都能够走到
        }
        return result[index-1];
    }
}

34. 第一个只出现一次的字符*

  • 在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
  • 思路分析:
    1. 遍历字符串使用哈希表记录每个字符出现的次数。
    2. 将次数等于1,且第一次出现的返回。
  • 代码实现:
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        
        char[] countArr = new char['z' - 'A' + 1];
        for (char c: str.toCharArray()) {
            countArr[c-'A']++;
        }
        
        for (int i = 0; i < str.length(); i++) {
            if (countArr[str.charAt(i) - 'A'] == 1) {
                return i;
            }
        }
        return -1;
    }
}

35.数组中的逆序对*

  • 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007;
    如 输入7,5,6,4 输出5
  • 思路分析:
    1. 顺序查找时间复杂为n^2,题解采用的是归并排序的思想。
    2. 再merge阶段。左边的值大于右边的值时,就可以统计一次逆序对。
  • 代码实现:
public class Solution {
    private int ans;
    public int InversePairs(int [] array) {
        
        mergeSort(array, 0, array.length-1);
        return ans%1000000007;
    }
    
    
    public void  mergeSort(int[] array, int left, int right) {
        if (left >= right) {
            return ;
        }
        int mid = ((right - left) >> 1) + left;
        mergeSort(array, left, mid);
        mergeSort(array, mid+1, right);
        merge(array, left, mid, right);
    }
    public void merge(int[] array, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = mid;
        int j = right;
        int index = temp.length - 1;
        // 这里为了统计逆序对的方便,从后往前填充,即从最大的开始
        while (i >= left && j > mid) {
            if (array[i] > array[j]){
                temp[index--] = array[i--];
                ans += j - mid; // 逆序对的数量就会等于右数组剩余的数量。
                if (ans >= 1000000007)// 数值过大求余
                {
                    ans %= 1000000007;
                }
            } else {
                temp[index--] = array[j--];
            }
            
        }
        while (i >= left) {
            temp[index--] = array[i--];
        }
        while (j > mid) {
            temp[index--] = array[j--];
        }
        for (index = 0; index < temp.length; index++) {
            array[left++] = temp[index];
        }
    }
}

36. 两个链表的第一个公共结点*

  • 输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
  • 思路分析:
    1. 双指针法。创建两个指针p1和p2,分别指向两个链表的头结点,然后依次往后遍历。如果某个指针到达末尾,则将该指针指向另一个链表的头结点;如果两个指针所指的节点相同,则循环结束,返回当前指针指向的节点。
    2. 比如两个链表分别为:1->3->4->5->6和2->7->8->9->5->6。短链表的指针p1会先到达尾部,然后重新指向长链表头部,当长链表的指针p2到达尾部时,重新指向短链表头部,此时p1在长链表中已经多走了k步(k为两个链表的长度差值),p1和p2位于同一起跑线,往后遍历找到相同节点即可。
    3. 其实该方法主要就是用链表循环的方式替代了长链表指针先走k步这一步骤。
  • 代码实现:
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
import java.util.HashSet;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        while (p1 != p2) {
            p1 = p1 == null ? pHead2 : p1.next;
            p2 = p2 == null ? pHead1 : p2.next;
        }
        return p1;
    }
}

37. 数字在排序数组中出现的次数

  • 统计一个数字在排序数组中出现的次数。
  • 思路分析:
    1. 因为已经排过序了,所以采用二分法,找到该数的小标,然后再向左向右查找出现的次数。
  • 代码实现:
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        if (array == null || array.length == 0) {
            return 0;
        }
        // 自己写一个二分,不过也有现成的 Arrays.binarySearch();
        return binarySearch(array, 0, array.length-1, k);
    }
    
    public int binarySearch(int[] array, int left, int right, int k) {
        if (left > right) {
            return 0;
        }
        int mid = (right - left) / 1 + left;
        if (k < array[mid]) {
            return binarySearch(array, left, mid-1, k);
        } else if (k > array[mid]) {
            return binarySearch(array, mid+1, right, k);
        } else {
            int count = 1;
            int index = mid - 1;
            while (index >= 0 && array[index] == k) {
                count++;
                index--;
            }
            index = mid + 1;
            while (index < array.length && array[index] == k) {
                count++;
                index++;
            }
            return count;
        }
    }
}

38. 二叉树的深度

  • 输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
  • 思路分析:
    1. 求一颗树的深度=左右子树的最大值+1
  • 代码实现:
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    public int TreeDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1;
    }
}

39. 平衡二叉树

  • 输入一棵二叉树,判断该二叉树是否是平衡二叉树。
  • 思路分析:
    1. 平衡二叉树就是左右子树的高度差不能超过1。
    2. 自顶向下的来判断当前树是否满足平衡二叉树。
  • 代码实现:
public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        if (root == null) {
            return true;
        }
        int left = getHeight(root.left);
        int right = getHeight(root.right);
        if (Math.abs(left - right) > 1) {
            return false;
        } else {
            return IsBalanced_Solution(root.left) && IsBalanced_Solution(root.right);
        }
    }
    
    public int getHeight(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
    }
}

40. 数组中只出现一次的数字*

  • 一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
  • 思路分析:
    1. 方法一:哈希表(较简单)
    2. 位运算:
      • 位运算中异或的性质:两个相同数字异或=0,一个数和0异或还是它本身。
      • 当只有一个数出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对儿出现的都抵消了。
      • 依照这个思路,我们来看两个数(我们假设是AB)出现一次的数组。我们首先还是先异或,剩下的数字肯定是A、B异或的结果,这个结果的二进制中的1,表现的是A和B的不同的位。
      • 我们就取第一个1所在的位数,假设是第3位,接着把原数组分成两组,分组标准是第3位是否为1。如此,相同的数肯定在一个组,因为相同数字所有位都相同,而不同的数,肯定不在一组。然后把这两个组按照最开始的思路,依次异或,剩余的两个结果就是这两个只出现一次的数字。
  • 代码实现:
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int temp = 0;
        // 计算两个只出现一次的数字的异化值
        for (int i = 0; i < array.length; i++) {
            temp ^= array[i];
        }
        
        // 计算这个异或值第一个1所在的下标
        int index = 1;
        while ((index & temp) == 0) {
            index <<=1;
        }
        
        // 将原数组按照这个小标进行分组
        int result1 = 0;
        int result2 = 0;
        for (int i = 0; i < array.length; i++) {
            if ((index & array[i]) == 0) {
                result1 ^= array[i];
            } else {
                result2 ^= array[i];
            }
        }
        num1[0] = result1;
        num2[0] = result2;
    }
}

41. 和为S的连续正数序列*

  • 小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
    输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
  • 思路分析:
    1. 采用窗口的方式,如果当前窗口中的数大于S,则缩小左边窗口。
    2. 如果大于,则扩大右边窗口。
    3. 等于,则记录当前序列,并将窗口整体右移一位。
  • 代码实现:
import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer> > ans = new ArrayList<ArrayList<Integer> >();
        int left = 1;
        int right = 2;
        while (right > left) {
            // 等差数列求和
            int cur = (left + right) * (right - left + 1) / 2;
            if (cur == sum) {    // 相等则保存该序列
                ArrayList<Integer> list = new ArrayList<>();
                for (int i = left; i <= right; i++) {
                    list.add(i);
                }
                ans.add(list);
                // 此时可以将窗口整体右移
                left++;
                right++;
            } else if (cur < sum) {    // 小于,则增大窗口
                right++;
            } else {    // 大于,减小窗口
                left++;
            }
        }
        return ans;
    }
}

42. 和为S的两个数字

  • 输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
  • 思路分析:
    1. 从左向右遍历数组中的每一个数,S减去这个数得到另一个数temp。
    2. 到数组中去找temp的小标,若找到了,并且和当前数不是同一个数则找到了答案。
  • 代码实现:
import java.util.ArrayList;
import java.util.Arrays;
public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> ans = new ArrayList<>();
        for (int i = 0; i < array.length; i++) {
            int temp = sum - array[i];
            int index = Arrays.binarySearch(array, temp);
            if (index >= 0 && index != i) {
                ans.add(array[i]);
                ans.add(temp);
                return ans;
            }
        }
        return ans;
    }
}

43. 左旋转字符串

  • 汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
  • 代码实现:
public class Solution {
    public String LeftRotateString(String str,int n) {
        if (str == null || str.length() <= 1) {
            return str;
        }
        n %= str.length();
        String temp = str.substring(0,n);
        return str.substring(n) + temp;
    }
}

44. 翻转单词顺序列

  • 牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
  • 代码实现:
import java.lang.StringBuilder;
public class Solution {
    public String ReverseSentence(String str) {
        if (str == null) {
            return str;
        }
        // 分割后反转拼接
        String[] strs = str.split(" ");
        if (strs.length > 0) {
        	StringBuilder sb = new StringBuilder();
        	for (int i = strs.length - 1; i >= 0; i--) {
        		sb.append(strs[i]+" ");
        	}
        	return sb.substring(0,sb.length() - 1);
        }
        return str;
    }
}

45. 扑克牌顺子

  • LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
  • 思路分析:
    1. 简单来说就是要是5个数字,最大和最小差值在5以内,并且没有重复数值。用一个set来填充数据,0不要放进去。
    2. set的大小加上0的个数必须为5个。此外set中数值差值在5以内。
  • 代码实现:
import java.util.TreeSet;
public class Solution {
    public boolean isContinuous(int [] numbers) {
        if (numbers .length > 5 || numbers.length <5) {
            return false;
        }
        TreeSet<Integer> set = new TreeSet<>();
        int count = 0;
        for (int num: numbers) {
            if (num == 0) {
                count++;
            } else {
                set.add(num);
            }
        }
        if ((count + set.size()) == 5) {
            if (set.last() - set.first() < 5) { // 注意小于5就行了
                return true;
            }
        }
        return false;
    }
}

46. 孩子们的游戏(圆圈中最后剩下的数)

  • 每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
    如果没有小朋友,请返回-1
  • 思路分析:
    1. 这题很明显,就是约瑟夫问题,采用循环链表来解决。
  • 代码实现:
public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        if (n < 1) {
            return -1;
        }
        Node head = new Node (0);    // 头节点
        Node pre = head;    // 指向要删除节点的前一个
        Node cur = head;    // 当前节点,即要被删除的节点
        for (int i = 1; i < n; i++) {    // 构建循环链表
            cur.next = new Node(i);
            if (i == n - 1) {    // 记录头节点的前一个
                pre = cur.next;
            }
            cur = cur.next;
            
        }
        pre.next = head;    // 首尾相接
        cur = head;// 从头节点开始报数
        
        // 直到只剩一个人,循环结束
        for (int count = n; count > 1; count--) {
            // 报数,直到m-1;
            for (int i = 0; i < m - 1; i++) {
                pre = pre.next;
                cur = cur.next;
            }
            // 从链表中删除cur节点,并让cur指向下一个节点
            pre.next = cur.next;
            cur = pre.next;
        }
        
        return cur.num;
    }
    
    class Node{
        int num;
        Node next;
        public Node(int num){
            this.num = num;
        }
    }
}

47. 求1+2+3+…+n

  • 求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
  • 思路分析:
    1. 由于题目限定,所以不能使用等差数列和循环。只能使用递归了。
  • 代码实现:
public class Solution {
    public int Sum_Solution(int n) {
        // f(n) = f(n-1) + n;
        if (n == 1) {
            return 1;
        }
        return Sum_Solution(n-1) + n;
    }
}

48. 不用加减乘除做加法*

  • 写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
  • 思路分析:
    1. 采用位运算来计算加法;
    2. 二位加法正确的加法计算:11+01 = 100 *使用位运算实现二位加法:
      • 按位加法: res1 = 11 ^ 01 = 10
      • 与运算进位: res2 = (11 & 01) << 1 = ( 01 ) << 1 = 010
      • res1 ^ res2 = 10 ^ 010 = 00
      • (10 & 10) << 1 = 100
    3. 更高位的加法,继续推理可以得出三位数的加法只需重复的计算三次得到第一个表达式的值就是计算出来的结果。
  • 代码实现:
public class Solution {
    public int Add(int num1,int num2) {
        int res1, res2;
        do{
            res1 = num1 ^ num2;    //执行加法 x ^ y
            res2 = (num1 & num2) << 1;    //进位操作 ( x & y ) << 1 
            num1 = res1;
            num2 = res2;
        } while (res2 != 0);    // 当不需要进位,则得到了最终结果
        return res1;
    }
}

49. 把字符串转换成整数

  • 将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0
  • 思路分析:
    • 这题还是比较简单,主要是运用字符来运算。
    • 但需要注意第一位的符号问题和整数越界问题。
  • 代码实现:
public class Solution {
    public int StrToInt(String str) {
        if (str == null || str.equals("")) {
            return 0;
        }
        
        char c = str.charAt(0);
        long ans = 0L;
        long temp = 1L;
        // 处理第一位的符号问题,有三种情况 + - 没有
        if (c >= '0' && c <= '9') {
            str = "+" + str;
        }
        
        for (int i = str.length() - 1; i > 0; i--) {
            c = str.charAt(i);
            if (c >= '0' && c <= '9') {
                ans += (c - '0') * temp;
                temp *= 10;
            } else {
                return 0;
            }
        }
        ans = str.charAt(0) == '-' ? -ans : ans;
        
        // 需要考虑整数越界问题
        if (ans > Integer.MAX_VALUE || ans < Integer.MIN_VALUE) {
            return 0;
        }
        return  (int)ans;
    }
}

50. 数组中重复的数字*

  • 在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
  • 思路分析:
    1. 首先最简单的可以通过排序和HashSet来解决。
    2. 这一种是利用题目的特性。
      • 题目里写了数组里数字的范围保证在0 ~ n-1 之间,所以可以利用现有数组设置标志,当一个数字被访问过后,可以设置对应位上的数 + n,之后再遇到相同的数时,会发现对应位上的数已经大于等于n了,那么直接返回这个数即可。
  • 代码实现:
public class Solution {
    // Parameters:
    //    numbers:     an array of integers
    //    length:      the length of array numbers
    //    duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
    //                  Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
    //    这里要特别注意~返回任意重复的一个,赋值duplication[0]
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        
        for (int i = 0; i < length; i++) {
            int index = numbers[i];
            if (index >= length) {    // 若长度大于length;需要计算出原始的数
                index -= length;
            }
            if (numbers[index] >= length) {    // 如果该索引的数大于n,之前有同样的索引值访问过
                duplication[0] = index;
                return true;
            }
            numbers[index] += length;
        }
        return false;
        
    }
}

51. 构建乘积数组*

  • 给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)
  • 思路分析:
  • 可以把B[i]=A[0]A[1]…A[i-1]A[i+1]…A[n-1]。看成A[0]A[1]…A[i-1]和A[i+1]…A[n-2]A[n-1]两部分的乘积。
  • 代码实现:
import java.util.ArrayList;
public class Solution {
    public int[] multiply(int[] A) {
        int[] B = new int[A.length];
        // 计算A[0]...A[i-1]
        int res = 1;
        for (int i = 0; i < B.length; i++) {
            B[i] = res;
            res *= A[i];
        }
        // 计算A[0]...A[i-1]
        res = 1;
        for (int i = B.length - 1; i >= 0; i--) {
            B[i] *= res;
            res *= A[i];
        }
        return B;
    }
}

52. 正则表达式匹配

  • 请实现一个函数用来匹配包括’.‘和’‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
  • 思路分析:
    1. 模式串中可能存在’.*’,它是贪婪匹配,在使整个表达式能得到匹配的前提下匹配尽可能多的字符。例如字符串"abcdeded"与模式"a.*d"匹配。
    2. 按下一个字符是否是’*'分情况讨论
  • 代码实现:
public class Solution {
    public boolean match(char[] str, char[] pattern)
    {
        return matchStr(str, 0, pattern, 0);
    }
    
    public boolean matchStr(char[] str, int i, char[] pattern, int j) {
        //字符串和模板都空了,说明匹配成功
        if (i >= str.length && j >= pattern.length) {
            return true;
        } else if (j >= pattern.length) {
            // 模板为空,匹配失败
            return false;
        }
        
        boolean next = (j + 1 < pattern.length && pattern[j+1] == '*');
        
        if (next) {
            if (i < str.length && (pattern[j] == '.' || str[i] == pattern[j])) {
                // *匹配成功,存在两种情况还能让它继续匹配,不能让它继续匹配 如: "aaa"与模式"a*a"最后一位就不能用a*来匹配
                return matchStr(str, i, pattern, j + 2) || matchStr(str, i+1, pattern, j);
            } else {
                // *匹配不成功直接跳两位
                return matchStr(str, i, pattern, j+2);
            }
            
        } else {    //  下一位不是*的情况
            if (i < str.length && (pattern[j] == '.' || str[i] == pattern[j])) {// 当前位为。或相等则匹配成功
                return matchStr(str, i+1, pattern, j+1);
            } else {
                return false;
            }
        }
    }
}

53. 表示数值的字符串*

  • 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。 但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。
  • 代码实现:
import java.util.regex.Pattern;
public class Solution {
    public boolean isNumeric(char[] str) {
        String pattern = "^[-+]?\\d*(\\.\\d*)?([eE][+\\-]?\\d+)?$";
        String s = new String(str);
        return Pattern.matches(pattern,s);
    }
}

54. 字符流中第一个不重复的字符*

  • 请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
    如果当前字符流没有存在出现一次的字符,返回#字符。
  • 代码实现:
import java.lang.StringBuilder;
public class Solution {
    
    private StringBuilder sb = new StringBuilder();
    int[] map = new int[128];   // 哈希表
    //Insert one char from stringstream
    public void Insert(char ch)
    {
        sb.append(ch);
        map[ch]++;
    }
  //return the first appearence once char in current stringstream
    public char FirstAppearingOnce()
    {
        for (char c: sb.toString().toCharArray()){
            if (map[c] == 1) {
                return c;
            }
        }
        return '#';
    }
}

55. 链表中环的入口结点

  • 给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
  • 方法一:采用HashSet存储
import java.util.HashSet;
public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead)
    {
        if (pHead == null) {
            return null;
        }
        
        HashSet<ListNode> set = new HashSet<>();
        ListNode cur = pHead;
        while (cur != null) {
            // 重复了就返回当前节点
            if (set.contains(cur)) {
                return cur;
            }
            set.add(cur);
            cur = cur.next;
        }
        return null;
    }
}
  • 方法二:快慢指针
  • 思路分析:
    1. 采用双指针解法,一快一慢指针。快指针每次跑两个element,慢指针每次跑一个。如果存在一个圈,总有一天,慢指针是能追上快指针的。
    2. 采用双指针解法,一快一慢指针。快指针每次跑两个element,慢指针每次跑一个。如果存在一个圈,总有一天,慢指针是能追上快指针的。
    3. 因为快指针是慢指针的两倍速,且他们在p点相遇,则我们可以得到等式 2(A+B) = A+B+C+B.
    4. 由3的等式,我们可得,C = A。
    5. 这时,因为我们的slow指针已经在p,则让快指针和慢指针一样的速度从头开始
    6. 当他们再次相遇,一定是q了。
      在这里插入图片描述
import java.util.HashSet;
public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead)
    {
        if (pHead == null) {
            return null;
        }
        
        ListNode slow = pHead;
        ListNode fast = pHead;
        while (slow.next != null && fast.next != null) {
            slow = slow.next;    // 慢指针走一步
            fast = fast.next.next;    // 快指针走两步
            
            if (slow == fast) {    //相等时,则让快指针和慢指针一样的速度从头开始
                fast = pHead;
                while (fast != slow) {    // 下次相遇的时候则为入口节点
                    fast = fast.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
        return null;
    }
}

56. 删除链表中重复的结点*

  • 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
  • 思路分析:
    1. 借助辅助头结点,可避免单独讨论头结点的情况。
    2. 设置两个结点 pre 和 cur,当 cur 和 cur.next 值相等,cur 一直向前走,直到不等退出循环,这时候 cur 指的值还是重复值,调整 cur 和 pre 的指针再次判断
  • 代码实现:
public class Solution {
    public ListNode deleteDuplication(ListNode pHead)
    {
        if (pHead == null || pHead.next == null) {
            return pHead;
        }
        // 自己构建头节点
        ListNode head = new ListNode(pHead.val);
        head.next = pHead;
        ListNode pre = head;
        ListNode cur = head.next;
        while (cur != null) {
            // 出现了重复元素
            if (cur.next != null && cur.next.val == cur.val) {
                // 继续往下查找是否还有重复元素
                while (cur.next != null && cur.val == cur.next.val) {
                    cur = cur.next;
                }
                cur = cur.next;    // 指向下一个非重复的元素
                pre.next = cur;    // 删除重复元素
            } else {
                pre = cur;
                cur = cur.next;
            }
        }
        return head.next;
    }
}

57. 二叉树的下一个结点

  • 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

  • 思路分析:
    在这里插入图片描述

    1. 有右子树,下一结点是右子树中的最左结点,例如 B,下一结点是 H
    2. 无右子树,且结点是该结点父结点的左子树,则下一结点是该结点的父结点,例如 H,下一结点是 E
    3. 无右子树,且结点是该结点父结点的右子树,则我们一直沿着父结点追朔,直到找到某个结点是其父结点的左子树,如果存在这样的结点,那么这个结点的父结点就是我们要找的下一结点。例如 I,下一结点是 A;例如 G,并没有符合情况的结点,所以 G 没有下一结点
  • 代码实现:

    扫描二维码关注公众号,回复: 10048292 查看本文章
/*
public class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode)
    {    
        // 当结点存在右子树的时候,中序遍历的下一个结点为右子树的最左节点
        if (pNode.right != null) {
            pNode = pNode.right;
            while (pNode.left != null) {
                pNode = pNode.left;
            }
            return pNode;
        }
        
        // 处理没有右子树情况
        while (pNode.next != null) {
            if (pNode.next.left == pNode) {// 找到某个结点是其父结点的左子树等于该节点
                return pNode.next;
            }
            pNode = pNode.next;
        }
        return null;
    }
}

58. 对称的二叉树

  • 请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

  • 思路分析:

    1. 对于这判断的二叉树的题,一般都是递归的判断左右子树是否满足要求。
  • 代码实现:

public class Solution {
    boolean isSymmetrical(TreeNode pRoot)
    {
        if (pRoot == null) {
            return true;
        }
        
        return isSymmetrical(pRoot.left, pRoot.right);
    }
    
    private boolean isSymmetrical(TreeNode left, TreeNode right) {
        if (left == null) {
            // left等于空right等于空则返回ture,否则返回false
            return right == null;
        }
        // 此时left不为空
        if (right == null) {
            return false;
        }
        // 两个节点的值不相等,返回false
        if (left.val != right.val) {
            return false;
        }
        // 递归的去判断是否为镜像二叉树
        return isSymmetrical(left.left, right.right) && isSymmetrical(left.right, right.left);
    }
}

59. 按之字形顺序打印二叉树

  • 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

  • 思路分析:

    1. 这题和leetcode上一题一样,采用双栈交替打印二叉树。
    2. 一个栈存储偶数层,一个栈存储奇数层,遍历栈的过程中,将下一层元素入栈。
  • 代码实现:

import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> ans = new ArrayList<ArrayList<Integer>>();
        if (pRoot == null) {
            return ans;
        }
        // 1. 存储奇数层的节点
        Stack<TreeNode> stack1 = new Stack<>();
        // 2. 存储偶数层的节点
        Stack<TreeNode> stack2 = new Stack<>();
        
        stack1.add(pRoot);
        
        ArrayList<Integer> list;
        TreeNode node;
        while (!stack1.isEmpty() || !stack2.isEmpty()){
            if (!stack1.isEmpty()) {    // 遍历奇数行从左到右打印
                list = new ArrayList<>();
                while (!stack1.isEmpty()) {
                    node = stack1.pop();
                    list.add(node.val);
                    if (node.left != null) {
                        stack2.push(node.left);
                    }
                    if (node.right != null) {
                        stack2.push(node.right);
                    }
                }
                ans.add(list);
            } else {    // 遍历偶数行从右到左打印
                list = new ArrayList<>();
                while (!stack2.isEmpty()) {
                    node = stack2.pop();
                    list.add(node.val);
                    if (node.right != null) {
                        stack1.push(node.right);
                    }
                    if (node.left != null) {
                        stack1.push(node.left);
                    }
                }
                ans.add(list);
            }
            
        }
        return ans;
    }

}

60. 把二叉树打印成多行

  • 从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

  • 思路分析:

    1. 非递归:核心在入队的时候,需要记录上一层的节点个数,进行输出;
    2. 递归:在递归的过程中需要记录当前是第几层;
  • 代码实现:

// 非递归实现
public class Solution {
    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> ans = new ArrayList<ArrayList<Integer> >();
        if (pRoot == null) {
            return ans;
        }
        LinkedList<TreeNode> queue = new LinkedList<>();
        ArrayList<Integer> list;
        TreeNode node;
        queue.offer(pRoot);
        
        while (!queue.isEmpty()){
            int size = queue.size();
            list = new ArrayList<>();
            // 打印上一层的节点,并将下一层的节点入队
            for (int i = 0; i < size; i++) {
                node = queue.poll();
                list.add(node.val);
                if (node.left != null) {
                    queue.add(node.left);
                }
                if (node.right != null) {
                    queue.add(node.right);
                }
            }
            ans.add(list);
        }
        return ans;
    }
    
}

// 递归版本
public class Solution {
    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> ans = new ArrayList<ArrayList<Integer> >();
        Print(pRoot, 0, ans);
        return ans;
    }
    
    void Print(TreeNode node, int level, ArrayList<ArrayList<Integer>> ans) {
        if (node == null) {
            return;
        }
        if (level >= ans.size()) { // 当层数大于size,需要开辟新的层
            ans.add(new ArrayList<Integer>());
        }
        ans.get(level).add(node.val);
        Print(node.left, level+1, ans);
        Print(node.right, level+1, ans);
    }
    
}

61. 序列化二叉树*

  • 请实现两个函数,分别用来序列化和反序列化二叉树
    二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
    二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
  • 思路分析:
    1. 序列化二叉树:递归遍历二叉树的节点,空节点使用#代替,节点之间使用逗号隔开,返回字符串
    2. 反序列化二叉树设置序号index,将字符串根据逗号分割为数组,根据index的值来设置树节点的val,如果节点的值为#,则返回空的树节点。
  • 代码实现:
import java.lang.StringBuilder;
public class Solution {
    private int index = -1;
    
    String Serialize(TreeNode root) {
        if (root == null) {
            return "#";
        } else {
            // 递归的序列化二叉树
            return root.val + "," + Serialize(root.left) + "," + Serialize(root.right);
        } 
    }
    
    TreeNode Deserialize(String str) {
        String[] strs = str.split(",");
       return Deserialize(strs);
    }
    
    TreeNode Deserialize(String[] strs){
        if (index > strs.length) {
            return null;
        }
        index++;
        TreeNode root = null;
        
        // 当前位置不为空,则继续构建子树
        if (!"#".equals(strs[index])) {
            root = new TreeNode(Integer.parseInt(strs[index]));
            root.left = Deserialize(strs);
            root.right = Deserialize(strs);
        }
        return root;
    }
}

62. 二叉搜索树的第k个结点

  • 给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

  • 思路分析:

    1. 因为二叉搜索树的中序遍历就是升序序列。所以当中序遍历第k个节点时,将该节点保存。
  • 代码实现:

public class Solution {
    private int index;
    private TreeNode ans;
    TreeNode KthNode(TreeNode pRoot, int k)
    {
        if (pRoot == null) {
            return null;
        }
        index = k;
        // 进行中序遍历,将第k个根节点保存
        midOrder(pRoot);
        return ans;
    }
    
    void midOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        midOrder(root.left);
        index--;
        if (index == 0) {    // 当k减为0时,该节点就是要求的节点
            ans = root;
            return;
        }
        midOrder(root.right);
    }


}

63. 数据流中的中位数

  • 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
  • 代码实现:
import java.util.Collections;
import java.util.ArrayList;
public class Solution {
    private ArrayList<Integer> list = new ArrayList<>();
    public void Insert(Integer num) {
        list.add(num);
    }

    public Double GetMedian() {
        Collections.sort(list);
        int size = list.size();
        if (size % 2 == 0) { // 偶数
            // 找出中间两个数求平均
            return (double)(list.get(size / 2 - 1) + list.get(size / 2)) / 2;
        } else {// 奇数
            return (double)list.get(size / 2);
        }
    }
}

64. 滑动窗口的最大值*

  • 给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

  • 思路分析:

    1. 暴力法:直接求每个窗口的最大值
    2. 大顶堆:用一个大顶堆,保存当前滑动窗口中的数据。滑动窗口每次移动一格,就将前面一个数出堆,后面一个数入堆。
  • 代码实现

// 暴力法
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        ArrayList<Integer> ans = new ArrayList<>();
        if (size <= 0) {
            return ans;
        }
        
        for (int i = 0; i <= num.length - size; i++) {
            int max = num[i];
            for (int j = i + 1; j < i + size; j++) {
                if (num[j] > max) {
                    max = num[j];
                }
            }
            ans.add(max);
        }
        return ans;
    }
}

// 大顶堆
import java.util.ArrayList;
import java.util.PriorityQueue;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        ArrayList<Integer> ans = new ArrayList<>();
        if (size <= 0 || size > num.length) {    // 排除不符合要求的情况
            return ans;
        }
        // 创建一个优先队列
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1,o2) -> o2 - o1);// 倒序
        // 初始化窗口
        int index = 0;
        for (; index < size; index++) {    // 初始化一个窗口
            maxHeap.offer(num[index]);
        }
        for (; index < num.length; index++) {
            // 添加窗口的最大值
            ans.add(maxHeap.peek());
            // 将窗口的第一个元素出队
            maxHeap.remove(num[index-size]);
            // 将窗口的下一个元素入队
            maxHeap.offer(num[index]);
        }
        // 注意还有最后一个窗口没有处理
        ans.add(maxHeap.peek());
        return ans;
    }
}

65. 矩阵中的路径

  • 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 [abcesfcsadee]\begin{bmatrix} a & b & c &e \ s & f & c & s \ a & d & e& e\ \end{bmatrix}\quad⎣⎡​asa​bfd​cce​ese​⎦⎤​ 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

  • 思路分析:

    1. 遍历矩阵,找到一个格子的字符和字符串首字符相等的格子作为起点,进行深搜判断。
    2. 如果满足要求则返回true,否则继续遍历。
  • 代码实现:

public class Solution {
    private boolean[] visited;    // 标记该格子是否被访问
    private char[] matrix;    // 存储该矩阵
    private int rows, cols;
    // 用于向四个方向深搜
    private int[] X = {0, 0, -1, 1};
    private int[] Y = {-1, 1, 0, 0};
    
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
    {
        this.matrix = matrix;
        this.rows = rows;
        this.cols = cols;
        
        // 遍历矩阵,
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 如果格子上字符和字符串首字符相等,则进行深搜
                if (matrix[i * cols + j] == str[0]) {
                    visited = new boolean[rows * cols];
                    if (dfs(i, j, str, 0)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    private boolean dfs(int i, int j, char[] str, int index) {
        if (index >= str.length) {// 结束条件,字符串都匹配上了
            return true;
        }
        if (matrix[i * cols + j] == str[index]) {    // 如果相等则向四个方向继续搜索
            
            // 排除矩阵的元素个数和字符串元素个数相等的情况
            if (index == str.length -1 && matrix.length - 1 == index) {
                return true;
            }
            
            // 标记为访问
            visited[i * cols + j] = true;
            for (int k = 0; k < 4; k++) {
                int x = i + X[k];
                int y = j + Y[k];
                
                // 检查要搜索的格子是否满足要求
                if (x >= 0 && x < rows && y >= 0 && y < cols && !visited[x * cols + y]) {
                    if (dfs(x, y, str, index + 1)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

}

66. 机器人的运动范围

  • 地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
  • 思路分析:从(0,0)处开始深搜这个地图,能到达的格子标记为1;
  • 代码实现:
public class Solution {
    private int[][] map;
    private int rows, cols;
    private int[] X = {0, 0, -1, 1};
    private int[] Y = {-1, 1, 0, 0};
    private int ans;
    
    public int movingCount(int threshold, int rows, int cols)
    {
        if (threshold < 0) {
            return 0;
        }
        map = new int[rows][cols];
        this.rows = rows;
        this.cols = cols;
        
        dfs(0, 0, threshold);
        return ans;
    }
    
    public void dfs(int i, int j, int threshold) {
        // 只要能进该方法,说明就能进入该方格
        map[i][j] = 1;
        ans++;
        // 向四个方向进行搜索,看是否有满足条件的格子
        for (int k = 0; k < 4; k++) {
            int x = i + X[k];
            int y = j + Y[k];
            // 先检查范围,并且该位置没有访问
            if (x >= 0 && x < rows && y >= 0 && y < cols && map[x][y] == 0) {
                int sum = 0;
                int temp = x;
                while (temp != 0) {
                    sum += temp % 10;
                    temp /= 10;
                }
                temp = y;
                while (temp != 0) {
                    sum += temp % 10;
                    temp /= 10;
                }
                // 位数之和小于等k则说明能进入
                if (sum <= threshold) {
                    dfs(x, y, threshold);
                }
            }
        }
    }
}

67. 剪绳子*

  • 给你一根长度为n(2 <= n <= 60)的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],…,k[m]。请问k[0]xk[1]x…xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
  • 思路分析:递归法+动态规划
    1. 我们先定义函数f(n)为把绳子剪成若干段之后的各段长度乘积的最大值.在剪第一刀的时候,我们会有n-1种可能的选择,也就是说剪出来的第一段绳子的长度可能为1,2,…n-1.因此就有了递归公式 f(n) = max(f(i)*f(n-i)),其中1 <= i <= n/2.
public class Solution {
    private int[] dp;
    public int cutRope(int target) {
        // f(n) = Math.max(f(i)*f(n-i)) (1 <= i <= n/ 2);
        if (target < 2) {
            return 0;
        } else if (target == 2) {
            return 1;
        } else if (target == 3) {
            return 2;
        }
        
        // 使用dp进行优化,不重复计算
        dp = new int[target + 1];
        return f(target);
    }
    
    public int f(int n) {
        if (n < 4) {
            return n;
        }
        if (dp[n] != 0) {
            return dp[n];
        }
        
        int max = 0;
        // 取最大值
        for (int i = 1; i <= n / 2; i++) {
            max = Math.max(max, f(i)*f(n - i));
        }
        dp[n] = max;
        return max;
    }
}
发布了95 篇原创文章 · 获赞 64 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_43115606/article/details/104723682