[关闭]
@adamhand 2019-03-03T12:19:33.000000Z 字数 85781 阅读 1254

《剑指offer》题目小结



1. 数组中重复元素问题

1.1 Find All Duplicates in an Array



  1. class Solution {
  2. public List<Integer> findDuplicates(int[] nums) {
  3. List<Integer> res = new ArrayList<>();
  4. for (int i = 0; i < nums.length; ++i) {
  5. int index = Math.abs(nums[i])-1;
  6. if (nums[index] < 0)
  7. res.add(Math.abs(index+1));
  8. nums[index] = -nums[index];
  9. }
  10. return res;
  11. }
  12. }

1.2 数组中的重复数字



  1. public boolean duplicate(int numbers[],int length,int [] duplication) {
  2. boolean flag = false;
  3. if(numbers==null || length==0){
  4. return flag;
  5. }
  6. HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
  7. for(int num: numbers){
  8. if(map.containsKey(num)){
  9. flag = true;
  10. duplication[0] = num;
  11. break;
  12. }
  13. map.put(num, 0);
  14. }
  15. return flag;
  16. }

2. 二维数组中的查找



  1. public boolean Find(int target, int[][] matrix) {
  2. if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
  3. return false;
  4. int rows = matrix.length, cols = matrix[0].length;
  5. int r = 0, c = cols - 1; // 从右上角开始
  6. while (r <= rows - 1 && c >= 0) {
  7. if (target == matrix[r][c])
  8. return true;
  9. else if (target > matrix[r][c])
  10. r++;
  11. else
  12. c--;
  13. }
  14. return false;
  15. }

3. 替换空格



从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。

  1. public String replaceSpace(StringBuffer str) {
  2. int oldLen = str.length();
  3. for (int i = 0; i < oldLen; i++)
  4. if (str.charAt(i) == ' ')
  5. str.append(" ");
  6. int P1 = oldLen - 1, P2 = str.length() - 1;
  7. while (P1 >= 0 && P2 > P1) {
  8. char c = str.charAt(P1--);
  9. if (c == ' ') {
  10. str.setCharAt(P2--, '0');
  11. str.setCharAt(P2--, '2');
  12. str.setCharAt(P2--, '%');
  13. } else {
  14. str.setCharAt(P2--, c);
  15. }
  16. }
  17. return str.toString();
  18. }
  1. StringBuilder sb = new StringBuilder("1234");
  2. sb.replace(1, 3, "nba");
  3. System.out.println(sb.toString());
  4. //结果为:1nba4
  1. public String replaceSpace(StringBuffer str) {
  2. for(int i = 0; i < str.length(); i++){
  3. if(str.charAt(i) == ' '){
  4. str.replace(i, i+1, "%20");
  5. }
  6. }
  7. return str.toString();
  8. }

4. 从尾到头打印链表



  1. public class Solution {
  2. public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
  3. Stack<Integer> stack = new Stack<>();
  4. while(listNode != null){
  5. stack.add(listNode.val);
  6. listNode = listNode.next;
  7. }
  8. ArrayList<Integer> list = new ArrayList<>();
  9. while(!stack.isEmpty()){
  10. list.add(stack.pop());
  11. }
  12. return list;
  13. }
  14. }
  1. public class Solution {
  2. public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
  3. ArrayList<Integer> list = new ArrayList<>();
  4. if(listNode != null){
  5. list.addAll(printListFromTailToHead(listNode.next));
  6. list.add(listNode.val);
  7. }
  8. return list;
  9. }
  10. }
  1. public class Solution {
  2. public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
  3. //逆序构建链表
  4. ListNode head = new ListNode(-1);
  5. while(listNode != null){
  6. ListNode memo = listNode.next;
  7. listNode.next = head.next;
  8. head.next = listNode;
  9. listNode = memo;
  10. }
  11. ArrayList<Integer> list = new ArrayList<>();
  12. head = head.next;
  13. while(head != null){
  14. list.add(head.val);
  15. head = head.next;
  16. }
  17. return list;
  18. }
  19. }
  1. public class Solution {
  2. public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
  3. ArrayList<Integer> list = new ArrayList<>();
  4. while(listNode != null){
  5. list.add(listNode.val);
  6. listNode = listNode.next;
  7. }
  8. Collections.reverse(list);
  9. return list;
  10. }
  11. }

5. 重建二叉树



  1. public class Solution {
  2. private Map<Integer, Integer> inOrderNumsIndex = new HashMap<>();
  3. public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
  4. for(int i = 0; i < in.length; i++){
  5. inOrderNumsIndex.put(in[i], i);
  6. }
  7. return reBuildBinaryTreeCore(pre, 0, pre.length-1, 0, in.length-1);
  8. }
  9. private TreeNode reBuildBinaryTreeCore(int preorder[], int preStartIndex, int preEndIndex, int inStartIndex, int inEndIndex){
  10. //这种情况下说明左子树已经排完,返回继续排右子树。
  11. if(preStartIndex > preEndIndex){
  12. return null;
  13. }
  14. TreeNode root = new TreeNode(preorder[preStartIndex]);
  15. int rootIndex = inOrderNumsIndex.get(root.val);
  16. int leftTreeSize = rootIndex - inStartIndex;
  17. root.left = reBuildBinaryTreeCore(preorder, preStartIndex+1, preStartIndex+leftTreeSize, inStartIndex, inStartIndex+leftTreeSize-1);
  18. root.right = reBuildBinaryTreeCore(preorder, preStartIndex+leftTreeSize+1, preEndIndex, inStartIndex+leftTreeSize+1, inEndIndex);
  19. return root;
  20. }
  21. }
  1. public class Solution {
  2. //存放中序遍历的值和索引
  3. Map<Integer, Integer> inIndex = new HashMap<>();
  4. public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
  5. if(pre.length <= 0 || in.length <= 0 || pre.length != in.length){
  6. return null;
  7. }
  8. for(int i = 0; i < in.length; i++){
  9. inIndex.put(in[i], i);
  10. }
  11. int len = pre.length;
  12. return constructorCore(pre, in, 0, len-1, 0, len-1);
  13. }
  14. private TreeNode constructorCore(int[] pre, int[] in, int preStart, int preEnd, int inStart, int inEnd){
  15. TreeNode root = new TreeNode(pre[preStart]);
  16. root.left = null;
  17. root.right = null;
  18. if(preStart == preEnd){
  19. if(inStart == inEnd && pre[preStart] == in[inStart]){
  20. return root;
  21. }else{
  22. //如果输入错误,前序遍历和中序遍历结果不一致,则返回错误
  23. System.out.println("wrong input");
  24. return null;
  25. }
  26. }
  27. int index = inIndex.get(pre[preStart]);
  28. //int index = inIndex.get(root.val);
  29. int leftLen = index - inStart;
  30. if(leftLen > 0){
  31. root.left = constructorCore(pre, in, preStart+1, preStart+leftLen, inStart, index-1);
  32. }
  33. if(inEnd - index > 0){
  34. root.right = constructorCore(pre, in, preStart+leftLen+1, preEnd, index+1, inEnd);
  35. }
  36. return root;
  37. }
  38. }

对解法2的理解:由于递归可以转换为栈,所以可以用栈来解释一下,画出如下的栈图。





6. 二叉树的下一个节点



思路:
要找到中序遍历下的下一个节点。这个节点可以分为两种情况:

  1. public class Solution {
  2. public TreeLinkNode GetNext(TreeLinkNode pNode)
  3. {
  4. if(pNode == null){
  5. return null;
  6. }
  7. if(pNode.right != null){
  8. pNode = pNode.right;
  9. while(pNode.left != null){
  10. pNode = pNode.left;
  11. }
  12. return pNode;
  13. }else{
  14. while(pNode.next != null){
  15. TreeLinkNode parent = pNode.next;
  16. if(parent.left == pNode){
  17. return parent;
  18. }
  19. pNode = pNode.next;
  20. }
  21. }
  22. return null;
  23. }
  24. }
  1. public class Solution {
  2. public TreeLinkNode GetNext(TreeLinkNode pNode)
  3. {
  4. if(pNode == null){
  5. return null;
  6. }
  7. if(pNode.right != null){
  8. return getNextNode(pNode.right);
  9. }else{
  10. if(pNode.next == null){
  11. return null;
  12. }else if(pNode.next.left == pNode){
  13. return pNode.next;
  14. }else{
  15. while(pNode.next.right == pNode){
  16. pNode = pNode.next;
  17. if(pNode.next == null){
  18. return null;
  19. }
  20. }
  21. return pNode.next;
  22. }
  23. }
  24. }
  25. private TreeLinkNode getNextNode(TreeLinkNode pNode){
  26. if(pNode == null){
  27. return null;
  28. }
  29. TreeLinkNode result = null;
  30. result = getNextNode(pNode.left);
  31. if(result == null){
  32. result = pNode;
  33. }
  34. return result;
  35. }
  36. }

7. 用两个栈实现队列

用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。

思路:stack1完成push操作,stack2完成pop操作。每次pop时,都要将stack1中的元素放入stack2中。

  1. public class Solution {
  2. Stack<Integer> stack1 = new Stack<Integer>();
  3. Stack<Integer> stack2 = new Stack<Integer>();
  4. public void push(int node) {
  5. stack1.push(node);
  6. }
  7. public int pop() {
  8. if(stack2.isEmpty()){
  9. while(!stack1.isEmpty()){
  10. stack2.push(stack1.pop());
  11. }
  12. }
  13. return stack2.pop();
  14. }
  15. }

8.斐波那契数列

  1. public class Solution {
  2. public int Fibonacci(int n) {
  3. if(n < 0){
  4. return -1;
  5. }else if(n <= 1){
  6. return n;
  7. }else{
  8. return Fibonacci(n - 1) + Fibonacci(n - 2);
  9. }
  10. }
  11. }
  1. public class Solution {
  2. public int Fibonacci(int n) {
  3. if(n < 0){
  4. return -1;
  5. }else if(n <= 1){
  6. return n;
  7. }
  8. int pre2 = 0, pre1 = 1;
  9. int fib = 0;
  10. for(int i = 2; i <=n; i++){
  11. fib = pre2 + pre1;
  12. pre2 = pre1;
  13. pre1 = fib;
  14. }
  15. return fib;
  16. }
  17. }

9. 跳台阶

一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

解法:当n=1,只有一种跳法;当n=2,有两种跳法;当n>2,记f(n)为n阶台阶的跳法,则可以分为两种情况:第一次跳一个台阶,则跳法数为f(n-1);第一次跳两个台阶,则跳法数为f(n-2)。则f(n)=f(n-1)+f(n-2),不难看出,这是一个斐波那契数列问题。

  1. public class Solution {
  2. public int JumpFloor(int target) {
  3. if(target < 0){
  4. return -1;
  5. }else if(target <= 2){
  6. return target;
  7. }
  8. int pre2 = 1, pre1 = 2;
  9. int result = 1;
  10. for(int i = 2; i < target; i++){
  11. result = pre2 + pre1;
  12. pre2 = pre1;
  13. pre1 = result;
  14. }
  15. return result;
  16. }
  17. }

10. 变态跳台阶

一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

  1. public class Solution {
  2. public int JumpFloorII(int target) {
  3. if(target == 0 || target == 1){
  4. return 1;
  5. }else if(target == 2){
  6. return 2;
  7. }
  8. int result = 0;
  9. for(int i = 0; i < target; i++){
  10. result += JumpFloorII(i);
  11. }
  12. return result;
  13. }
  14. }
  1. public class Solution {
  2. public int JumpFloorII(int target) {
  3. int jumpFlo=1;
  4. while((--target) >0)
  5. {
  6. jumpFlo*=2;
  7. }
  8. return jumpFlo;
  9. }
  10. }
  1. public class Solution {
  2. public int JumpFloorII(int target) {
  3. return 1<<--target;
  4. }
  5. }

11. 矩形覆盖问题

我们可以用 2*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2*1 的小矩形无重叠地覆盖一个 2*n 的大矩形,总共有多少种方法?

解题思路:显然这又是一个斐波那契数列问题。

  1. public class Solution {
  2. public int RectCover(int target) {
  3. if(target <= 2){
  4. return target;
  5. }
  6. int pre1 = 1, pre2 = 2;
  7. int result = 0;
  8. for(int i = 3; i <= target; i++){
  9. result = pre1 + pre2;
  10. pre1 = pre2;
  11. pre2 = result;
  12. }
  13. return result;
  14. }
  15. }

12. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。NOTE:给出的所有元素都大于 0,若数组大小为 0,请返回 0

解题思路:可以采用二分查找的方法。但是有一个问题,如果出现array[leftIndex]==array[minIndex]==array[rightIndex]的情况,无法确定解在哪个区间,需要切换到顺序查找。

  1. public class Solution {
  2. public int minNumberInRotateArray(int [] array) {
  3. if(array == null || array.length <= 0){
  4. throw new RuntimeException("输入数组不符合要求");
  5. }
  6. int leftIndex = 0;
  7. int rightIndex = array.length - 1;
  8. int midIndex = 0;
  9. while(array[leftIndex] >= array[rightIndex]){
  10. if(rightIndex - leftIndex == 1){
  11. midIndex = rightIndex;
  12. break;
  13. }
  14. midIndex = (leftIndex + rightIndex) / 2;
  15. //如果leftIndex minIndex rightIndex指向的三个数的值相同,则只能采取顺序查找的方法。
  16. if(array[leftIndex] == array[midIndex] && array[midIndex] == array[rightIndex]){
  17. return minInOrder(array, leftIndex, rightIndex);
  18. }
  19. if(array[midIndex] >= array[leftIndex]){
  20. leftIndex = midIndex;
  21. }else if(array[midIndex] <= array[rightIndex]){
  22. rightIndex = midIndex;
  23. }
  24. }
  25. return array[midIndex];
  26. }
  27. //顺序查找
  28. private int minInOrder(int[] array, int index1, int index2){
  29. int result = array[index1];
  30. for(int i = index1+1; i < index2; i++){
  31. if(result > array[i]){
  32. result = array[i];
  33. }
  34. }
  35. return result;
  36. }
  37. }

13. 矩阵中的路径

14. 机器人的运动范围

15. 剪绳子

16. 二进制中1的个数

题目描述

输入一个整数,输出该数二进制表示中 1 的个数(符号位不算)。


  1. public static int numberOf1_1(int n){
  2. int count;
  3. if(n < 0) //去掉符号位
  4. count = -1;
  5. else
  6. count = 0;
  7. while(n != 0){
  8. if((n & 0x01) != 0){
  9. count++;
  10. }
  11. n >>>= 1; //使用逻辑右移,可以避免死循环
  12. //n >>= 1; //使用算数右移,输入为负数时会产生死循环
  13. }
  14. return count;
  15. }
  1. public static int numberOf1_2(int n){
  2. int count = 0;
  3. int flag = 1;
  4. while(flag > 0){
  5. if((n & flag) != 0){
  6. count++;
  7. }
  8. flag <<= 1;
  9. }
  10. return count;
  11. }
  1. public static int numberOf1_3(int n){
  2. int count = 0;
  3. if(n < 0) //如果n小于0的话,要剪掉符号位的1
  4. count -= 1;
  5. while(n != 0){
  6. ++count;
  7. n &= (n - 1);
  8. }
  9. return count;
  10. }
  1. public static int numberOf1_4(int n){
  2. if(n < 0) //去掉符号位
  3. return Integer.bitCount(n) - 1;
  4. else
  5. return Integer.bitCount(n);
  6. }

小结:Java中的左移和右移
  在计算机中,移位操作可以被总结如下:


  对于其他语言比如C/C++,右移时选择逻辑右移还是算数右移是由编译器自己确定的,有符号数进行算数右移,无符号数进行逻辑右移。但是Java提供了一个逻辑右移运算符 >>>,可以人为指定右移方式。Java中的移位运算可以被总结为如下:

补充1:Java中的左移

  看下列操作:

  • System.out.println(1<<3);结果为8(1*2^3)
  • System.out.println(1<<31);结果为-2147483648(符号位变为1,为什么变为1呢?因为这是个临界值
  • System.out.println(1<<32);结果为1(不是0,<<n实际是<<(n%32),对于long<<n实际是<<n%64))

补充2:源码、反码和补码

举例:

那么计算机为什么要使用补码呢?

首先,根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1+(-1), 所以计算机被设计成只有加法而没有减法, 而让计算机辨别”符号位”会让计算机的基础电路设计变得十分复杂,于是就让符号位也参与运算,从而产生了反码。

用反码计算, 出现了”0”这个特殊的数值, 0带符号是没有任何意义的。 而且会有[0000 0000]和[1000 0000]两个编码表示0。于是设计了补码, 负数的补码就是反码+1,正数的补码就是正数本身,从而解决了0的符号以及两个编码的问题: 用[0000 0000]表示0,用[1000 0000]表示-128。

注意:-128实际上是使用以前的-0的补码来表示的, 所以-128并没有原码和反码。使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数。 这就是为什么8位二进制, 使用补码表示的范围为[-128, 127]。

求补码的两种方式:

补充3:循环左移
整数的循环移位:

代码如下:

  1. public class LoopLeftShift {
  2. public static void main(String[] args) {
  3. int a=0xD6485F0F;//转为2进制是32位
  4. //循环左移7位
  5. int temp=a<<7|a>>>(32-7);//这里注意右移用的是无符号右移
  6. System.out.println(Integer.toHexString(temp));//正确答案是0x242F87EB
  7. //循环右移7位
  8. temp=a>>>7|a<<(32-7);//这里注意右移用的是无符号右移
  9. System.out.println(Integer.toHexString(temp));//正确答案是0x1fac90be
  10. }
  11. }

字符串的循环移位:
核心思想:三次翻转

循环左移n位:

循环右移n位:

  1. //字符串翻转
  2. public static String reverse(String str){
  3. char[] strs = str.toCharArray();
  4. for(int i = 0; i < str.length() / 2; i++){
  5. char temp = strs[i];
  6. strs[i] = strs[str.length() - i - 1];
  7. strs[str.length() - i - 1] = temp;
  8. }
  9. return String.valueOf(strs);
  10. }
  11. //循环左移
  12. public static String loopLeftShift(String str, int index){
  13. str = reverse(str);
  14. String left = reverse(str.substring(0, str.length() - index)); //substring(int beginIndex, int endIndex)
  15. String right = reverse(str.substring(str.length() - index)); //substring(int beginIndex)
  16. str = left + right;
  17. return str;
  18. }
  19. //循环右移
  20. public static String loopRightShift(String str, int index){
  21. String left = reverse(str.substring(0, str.length() - index));
  22. String right = reverse(str.substring(str.length() - index));
  23. str = left + right;
  24. str = reverse(str);
  25. return str;
  26. }

16.1 相关题目1

题目描述:用一条语句判断一个整数是不是2的整数次方。

分析:一个整数如果是2的整数次方,那么它的二进制表示中有且只有一位是1,其它位都是0;那么将这个数减去1之后和自身做与运算,这个整数就会变为0。代码如下:

  1. public static boolean isIntegerMultipleOf2(int num){
  2. if((num & (num - 1)) == 0)
  3. return true;
  4. else
  5. return false;
  6. }

16.2 相关题目2

题目描述:输入两个整数m和n,计算需要改变m的二进制表示中的多少位才能得到n。比如10的二进制表示为1010,13的二进制表示为1101,需要改变1010中的3位才能得到1101。

分析:可以分两步解决这个问题,第一步求这两个数的异或,第二步统计异或结果中1的位数。代码如下:

  1. public static int changeNumber(int m, int n){
  2. int count = 0; //需要改变的次数
  3. int xor = m ^ n;
  4. System.out.println(xor);
  5. while(xor != 0){
  6. count++;
  7. xor &= (xor - 1);
  8. }
  9. return count;
  10. }

注意:由于负数在计算机中是以补码的形式存储的,所以如果输入m=-1,n=10,输入结果为30。-7在计算机中的表示为1111 1111 1111 1111 1111 1111 1111 1001,10表示为0000 0000 0000 0000 0000 0000 0000 1010

17. 数值的整数次方

题目描述
实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不要考虑大数问题。


思路:本体需要注意以下四点:

根据以上的思考,可以写出如下程序:

  1. private static double powerWithUnsignedExponent(double base, int exponent){
  2. double result = 1.0;
  3. for(int i = 1; i <= exponent; i++)
  4. result *= base;
  5. return result;
  6. }
  7. public static double power(double base, int exponent){
  8. boolean isNegtive = false;
  9. //底数为0且指数为负数,为了避免对0求导数出现的错误,直接返回0.0
  10. if(Math.abs(base - 0.0) < Double.MIN_VALUE && exponent < 0){
  11. return 0.0;
  12. }
  13. if(exponent < 0){
  14. exponent = -exponent;
  15. isNegtive = true;
  16. }
  17. double result = powerWithUnsignedExponent(base, exponent);
  18. return isNegtive ? 1.0 / result : result;
  19. }
  1. private static double powerWithUnsignedExponent(double base, int exponent) {
  2. //0的0次方也输出1
  3. if(exponent == 0)
  4. return 1;
  5. if(exponent == 1)
  6. return base;
  7. //result相当于base^n/2
  8. //用移位运算代替除以2,效率高
  9. double result = powerWithUnsignedExponent(base, exponent >> 1);
  10. result *= result;
  11. //如果exponent为奇数,还要乘以base
  12. //用位运算代替求余操作(%)来判断一个数是奇数还是偶数
  13. if((exponent & 0x01) == 1)
  14. result *= base;
  15. return result;
  16. }
  17. public static double power(double base, int exponent){
  18. boolean isNegtive = false;
  19. //底数为0且指数为负数,为了避免对0求导数出现的错误,直接返回0.0
  20. if(Math.abs(base - 0.0) < Double.MIN_VALUE && exponent < 0){
  21. return 0.0;
  22. }
  23. if(exponent < 0){
  24. exponent = -exponent;
  25. isNegtive = true;
  26. }
  27. double result = powerWithUnsignedExponent(base, exponent);
  28. return isNegtive ? 1.0 / result : result;
  29. }

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

题目描述
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。


本题需要注意的是“大数问题”,如果n非常大,用int或long型变量存储会产生溢出。

  1. public static void Print1ToMaxOfNDigit(int n){
  2. int number = 1;
  3. int i = 0;
  4. while(i++ < n){
  5. number *= 10;
  6. }
  7. //number-1是最大的n位数
  8. for(int j = 1; j < number; j++){
  9. System.out.println(j);
  10. }
  11. }
  1. //打印1到最大的n位数
  2. public class OneToN {
  3. public static void main(String[] args) {
  4. print1ToMaxOfN(2);
  5. }
  6. public static void print1ToMaxOfN(int n){
  7. if(n < 0)
  8. return;
  9. StringBuilder number = new StringBuilder();
  10. System.out.println(number.length());
  11. for(int i = 0; i < n; i++)
  12. number.append('0');
  13. while(!increment(number)){
  14. printNumber(number);
  15. }
  16. }
  17. public static boolean increment(StringBuilder sb){
  18. boolean isOverflow = false; //是否最高位溢出
  19. int nTakeOver = 0; //进位
  20. for(int i = sb.length() - 1; i >= 0; i--){
  21. int nSum = sb.charAt(i) - '0' + nTakeOver; //某位上的数加上进位
  22. if(i == sb.length() - 1) //如果当前位为最低位,最低位加一
  23. nSum++;
  24. if(nSum >= 10){ //要产生进位
  25. if(i == 0)
  26. isOverflow = true;
  27. else {
  28. nSum -= 10;
  29. nTakeOver = 1; //进位
  30. sb.setCharAt(i, (char)('0'+nSum));
  31. }
  32. }
  33. else {
  34. sb.setCharAt(i, (char)('0'+ nSum)); //不产生进位,也就不需要运算高位,直接跳出
  35. break;
  36. }
  37. }
  38. return isOverflow;
  39. }
  40. public static void printNumber(StringBuilder sb){
  41. boolean isBegin0 = true;
  42. //因为字符串前面补得是'0',打印时要跳过这些0
  43. for(int i = 0; i < sb.length(); i++){
  44. if(isBegin0 && sb.charAt(i) != '0')
  45. isBegin0 = false;
  46. if(!isBegin0)
  47. System.out.print(sb.charAt(i));
  48. }
  49. System.out.print(" ");
  50. }
  51. }
  1. public static void Print1ToMaxOfNDigits_3(int n){
  2. if(n < 0){
  3. return;
  4. }
  5. StringBuffer s = new StringBuffer(n);
  6. for(int i = 0; i < n; i++){
  7. s.append('0');
  8. }
  9. for(int i = 0; i < 10; i++){
  10. s.setCharAt(0, (char) (i+'0'));
  11. Print1ToMaxOfNDigits_3_Recursely(s, n, 0);
  12. }
  13. }
  14. public static void Print1ToMaxOfNDigits_3_Recursely(StringBuffer s, int n , int index){
  15. if(index == n - 1){
  16. PrintNumber(s);
  17. return;
  18. }
  19. for(int i = 0; i < 10; i++){
  20. //设置某一位的数
  21. s.setCharAt(index+1, (char) (i+'0'));
  22. //第一次递归设置十位数,第二次设置百位数......
  23. Print1ToMaxOfNDigits_3_Recursely(s, n, index+1);
  24. }
  25. }
  26. public static void PrintNumber(StringBuffer s){
  27. boolean isBeginning0 = true;
  28. for(int i = 0; i < s.length(); i++){
  29. if(isBeginning0 && s.charAt(i) != '0'){
  30. isBeginning0 = false;
  31. }
  32. if(!isBeginning0){
  33. System.out.print(s.charAt(i));
  34. }
  35. }
  36. System.out.println();
  37. }

补充:排列组合的递归实现

  1. public static void permutation(char[] array, int index){
  2. if(index==array.length){
  3. System.out.println(array);
  4. return;
  5. }
  6. if(array.length==0||index<0||index>array.length){
  7. return;
  8. }
  9. for(int j=index;j<array.length;j++){
  10. char temp=array[j];
  11. array[j]=array[index];
  12. array[index]=temp;
  13. permutation(array, index+1);
  14. temp=array[j];
  15. array[j]=array[index];
  16. array[index]=temp;
  17. }
  18. }
  19. 调用:permutation(chars, 0)
  1. public static void combiantion(char chs[]){
  2. if(chs==null||chs.length==0){
  3. return ;
  4. }
  5. List<Character> list=new ArrayList();
  6. for(int i=1;i<=chs.length;i++){
  7. combine(chs,0,i,list);
  8. }
  9. }
  10. //从字符数组中第begin个字符开始挑选number个字符加入list中
  11. public static void combine(char []cs,int begin,int number,List<Character> list){
  12. if(number==0){
  13. System.out.println(list.toString());
  14. return ;
  15. }
  16. if(begin==cs.length){
  17. return;
  18. }
  19. list.add(cs[begin]);
  20. combine(cs,begin+1,number-1,list);
  21. list.remove((Character)cs[begin]);
  22. combine(cs,begin+1,number,list);
  23. }

19. 在O(1)的时间内删除链表节点

题目描述:
给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该结点。


思路:

  1. class ListNode{
  2. int value;
  3. ListNode nextNode;
  4. }
  5. public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
  6. if (head == null || head.next == null || tobeDelete == null)
  7. return null;
  8. if (tobeDelete.next != null) {
  9. // 要删除的节点不是尾节点
  10. ListNode next = tobeDelete.next;
  11. tobeDelete.val = next.val;
  12. tobeDelete.next = next.next;
  13. } else {
  14. ListNode cur = head;
  15. while (cur.next != tobeDelete)
  16. cur = cur.next;
  17. cur.next = null;
  18. }
  19. return head;
  20. }

20. 删除链表中的重复元素

题目描述
给定排序的链表,删除重复元素,只保留重复元素第一次出现的结点;


  1. /**
  2. * 删除链表中重复的结点值,只保留第一个重复结点的值
  3. * @param head 链表的头结点
  4. */
  5. public static void deleteRepeteNode(Node head) {
  6. Node pre = head.next;
  7. Node cur;
  8. while (pre != null) {
  9. cur = pre.next;
  10. if (cur != null && (pre.value == cur.value)) {
  11. pre.next = cur.next;
  12. } else {
  13. pre = cur;
  14. }
  15. }
  16. }
  1. public static Node deleteDuplication(Node pHead)
  2. {
  3. if(pHead == null||pHead.next == null)
  4. return pHead;
  5. if(pHead.value == pHead.next.value){//第一个节点是重复节点,则跳过重复节点
  6. Node node = pHead.next;
  7. while(node != null&&node.value == pHead.value)
  8. node = node.next;
  9. return deleteDuplication(node);
  10. }else{
  11. //第一个节点不是重复节点
  12. pHead.next = deleteDuplication(pHead.next);
  13. return pHead;
  14. }
  15. }
  1. public static void deleteDuplication(Node head){
  2. Node pre = head.next;
  3. Node cur = pre.next;
  4. while(pre != null && cur != null){
  5. if(pre.value == cur.value){
  6. while(cur.value == cur.next.value)
  7. cur = cur.next;
  8. pre.next = cur.next;
  9. cur = pre.next;
  10. }else {
  11. pre = pre.next;
  12. cur = cur.next;
  13. }
  14. }
  15. }

测试函数和Node结构。

  1. public static void test(){
  2. int[] num = { 2, 3, 3, 5, 7, 8, 8, 8, 9, 9, 10 };
  3. Node head = new Node();
  4. Node pre = head;
  5. for (int i = 0; i < num.length; i++) {
  6. Node node = new Node(num[i]);
  7. pre.next = node;
  8. pre = node;
  9. }
  10. System.out.print("删除重复结点前的链表:");
  11. print(head.next);
  12. deleteRepeteNode(head);
  13. // Node delete = deleteDuplication(head);
  14. System.out.print("删除重复结点后的链表:");
  15. print(head.next);
  16. }
  17. class Node {
  18. public int value;
  19. public Node next;
  20. public Node() {}
  21. public Node(int value) {
  22. this.value = value;
  23. }
  24. }

21. 正则表达式匹配

题目描述:
  请实现一个函数用来匹配包含'.''*'的正则表达式。模式中的字符‘.’表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a""ab*ac*a"匹配,但与"aa.a""ab*a"均不匹配。


分析:这道题的核心其实在于分析'*',对于'.'来说,它和任意字符都匹配,可把其当做普通字符。对于'*'的分析,我们要进行分情况讨论,当所有的情况都搞清楚了以后,就可以写代码了。

  • 在每轮匹配中,Patttern第二个字符是''时:
    • 第一个字符不匹配('.'与任意字符视作匹配),那么''只能代表匹配0次,比如'ba'与'a*ba',字符串不变,模式向后移动两个字符,然后匹配剩余字符串和模式
    • 第一个字符匹配,那么'*'可能代表匹配0次,1次,多次,比如'aaa'与'a*aaa'、'aba'与'a*ba'、'aaaba'与'a*ba'。匹配0次时,字符串不变,模式向后移动两个字符,然后匹配剩余字符串和模式;匹配1次时,字符串往后移动一个字符,模式向后移动2个字符;匹配多次时,字符串往后移动一个字符,模式不变;
  • 而当Patttern第二个字符不是'*'时,情况就简单多了:
    • 如果字符串的第一个字符和模式中的第一个字符匹配,那么在字符串和模式上都向后移动一个字符,然后匹配剩余字符串和模式。
    • 如果字符串的第一个字符和模式中的第一个字符不匹配,那么直接返回false。
  1. //正则表达式匹配
  2. public class Regex {
  3. public static boolean match(String input,String pattern){
  4. if(input==null||pattern==null) return false;
  5. return matchCore(input,0,pattern,0);
  6. }
  7. private static boolean matchCore(String input,int i,String pattern,int p){
  8. if((input.length()==i)&&(pattern.length()==p)){
  9. //出口1,input和pattern都到了字符串末尾
  10. return true;
  11. }
  12. if((i!=input.length())&&(pattern.length()==p)){
  13. //出口2,字符串input没有到末尾,pattern到了末尾
  14. return false;
  15. }
  16. if((input.length()==i)&&(pattern.length()!=p)){
  17. //出口3,字符串input到末尾,pattern还没有到末尾
  18. return false;
  19. }
  20. if((p+1<pattern.length())&&(pattern.charAt(p+1)=='*')){//pattern第二个字符为*
  21. if((input.charAt(i)==pattern.charAt(p))||(pattern.charAt(p)=='.')){
  22. //首字母相匹配
  23. return matchCore(input,i+1,pattern,p+2) //*表示出现1次
  24. ||matchCore(input,i+1,pattern,p) //*表示出现多次
  25. ||matchCore(input,i,pattern,p+2); //*表示出现0次 , a ... p* ...
  26. }else{
  27. //首字母不匹配
  28. return matchCore(input,i,pattern,p+2);
  29. }
  30. } //end pattern.charAt(p+1)=='*'
  31. if((input.charAt(i)==pattern.charAt(p))||(pattern.charAt(p)=='.')){
  32. //pattern第二个字母不是*,且首字母匹配
  33. return matchCore(input,i+1,pattern,p+1);
  34. }
  35. return false; //其余情况全部不匹配
  36. }
  37. public static void main(String[] args) {
  38. // TODO Auto-generated method stub
  39. Scanner scanner = new Scanner(System.in); //扫描键盘输入
  40. System.out.println(" 请输入第一个字符串:");
  41. String str1 = scanner.nextLine();
  42. System.out.println(" 请输入第二个字符串:");
  43. String str2 = scanner.nextLine();
  44. scanner.close();
  45. System.out.print("匹配的结果为:");
  46. System.out.println(match(str1, str2));
  47. }
  48. }

22. 表示数值的字符串

题目描述:
  请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。


思路一:从头到尾比较。

  1. private static int inx;
  2. public static void main(String[] args) {
  3. test("Test1", "100", true);
  4. test("Test2", "123.45e+6", true);
  5. test("Test3", "+500", true);
  6. test("Test4", "5e2", true);
  7. test("Test5", "3.1416", true);
  8. test("Test6", "600.", true);
  9. test("Test7", "-.123", true);
  10. test("Test8", "-1E-16", true);
  11. test("Test9", "1.79769313486232E+308", true);
  12. System.out.println();
  13. test("Test10", "12e", false);
  14. test("Test11", "1a3.14", false);
  15. test("Test12", "1+23", false);
  16. test("Test13", "1.2.3", false);
  17. test("Test14", "+-5", false);
  18. test("Test15", "12e+5.4", false);
  19. test("Test16", ".", false);
  20. test("Test17", ".e1", false);
  21. test("Test18", "+.", false);
  22. }
  23. public static void test(String testName, String str, boolean expected){
  24. if(isNumeric(str.toCharArray()) == expected)
  25. System.out.println(testName+"+Passed.");
  26. else
  27. System.out.println(testName+"+Failed.");
  28. }
  29. public static boolean isNumeric(char[] str) {
  30. if(str==null || str.length==0){
  31. return false;
  32. }
  33. inx = 0;
  34. boolean flag = scanInteger(str);
  35. //判断小数部分
  36. if(inx<str.length && str[inx]=='.'){
  37. inx = inx+1;
  38. flag = scanUInteger(str)||flag; //解释a,见代码下方
  39. }
  40. //判断指数部分
  41. if(inx<str.length && (str[inx]=='e' || str[inx]=='E')){
  42. inx = inx+1;
  43. flag = flag && scanInteger(str);
  44. }
  45. return flag && inx>=str.length;
  46. }
  47. //判断是否是整数
  48. public static boolean scanInteger(char[] str){
  49. if(inx<str.length &&(str[inx]=='+' || str[inx]=='-')){
  50. inx = inx+1;
  51. }
  52. return scanUInteger(str);
  53. }
  54. //判断是否是无符号整数
  55. public static boolean scanUInteger(char[] str){
  56. int inx1=inx;
  57. while(inx<str.length && str[inx]>='0' && str[inx]<='9'){
  58. inx = inx + 1;
  59. }
  60. return inx>inx1;
  61. }

思路二:使用库函数

  1. public static boolean isNumeric_1(char[] str){
  2. try {
  3. Double number = Double.parseDouble(new String(str));
  4. }catch (NumberFormatException e){
  5. return false;
  6. }
  7. return true;
  8. }

思路三:使用正则表达式。
[] : 字符集合
() : 分组
? : 重复 0 ~ 1
+ : 重复 1 ~ n
* : 重复 0 ~ n
. : 任意字符
\. : 转义后的 .
\d : 数字

  1. public static boolean isNumeric_2(char[] str){
  2. if (str == null || str.length == 0)
  3. return false;
  4. return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
  5. }

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

题目描述:
  请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符”go”时,第一个只出现一次的字符是”g”。当从该字符流中读出前六个字符“google”时,第一个只出现一次的字符是”l”。


解题思路:可以使用HashMap存储字符和其出现的次数,但是因为HashMap遍历是无序的,还需要用用ArrayList来确定字符输入的顺序,从而得到第一个只初夏一次的字符。

  1. static HashMap<Character, Integer> map = new HashMap<>();
  2. static ArrayList<Character> list = new ArrayList<>();
  3. public static void main(String[] args) {
  4. char[] chars = "google".toCharArray();
  5. for(int i = 0; i < chars.length; i++)
  6. insert(chars[i]);
  7. System.out.println(firstAppearOnce());
  8. }
  9. public static char firstAppearOnce(){
  10. for(char key : list){
  11. if(map.get(key) == 1)
  12. return key;
  13. }
  14. return '#';
  15. }
  16. public static void insert(char ch){
  17. if(map.containsKey(ch))
  18. map.put(ch, map.get(ch)+1);
  19. else
  20. map.put(ch, 1);
  21. list.add(ch);
  22. }

24. 调整数组顺序是奇数位于偶数前面

题目描述:
  输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分(两种情况的代码:不考虑调整后奇数的之间和偶数之间的相对位置;考虑调整后奇数之间和偶数之间的相对位置不变)。


思路一:使用双指针法。这种情况没有考虑调整后奇数之间和偶数之间的行对位置。

  1. public static void reOrderArray(int[] arr){
  2. if(arr == null || arr.length < 0)
  3. return;
  4. int head = 0, tail = arr.length - 1;
  5. while(head < tail){
  6. while(head < tail && (arr[head] & 0x01) != 0)
  7. head++;
  8. while(head < tail && (arr[tail] & 0x01) == 0)
  9. tail--;
  10. if(head < tail){
  11. int temp = arr[head];
  12. arr[head] = arr[tail];
  13. arr[tail] = temp;
  14. }
  15. }
  16. }

思路二:要求保证调整后奇数之间和偶数之间的相对位置不变。需要一个辅助数组。

  1. public static void reOrderArray_1(int[] nums) {
  2. // 奇数个数
  3. int oddCnt = 0;
  4. for (int val : nums)
  5. if ((val & 0x01) != 0)
  6. oddCnt++;
  7. int[] copy = nums.clone();
  8. int i = 0, j = oddCnt;
  9. for (int num : copy) {
  10. if ((num & 0x01) != 0)
  11. nums[i++] = num;
  12. else
  13. nums[j++] = num;
  14. }
  15. }

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

题目描述:
  输入一个链表,输出该链表中倒数第K个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第一个节点。


思路:常规的方法是先遍历一次链表,得到链表的长度,然后再从头到尾遍历到n-k+1的地方就行了。但是这个方法显然不是最简单的方法,因为它遍历了两次。
当用一个指针遍历两次才能够完成工作的情况下,往往用两个指针遍历一次就能实现,这就是“双指针法”。这里定义连个指针,第一个指针从头到尾走k-1步,第二个指针保持不动。从第k步开始,第二个指针也从头指针开始遍历,它们之间的距离适中为k-1,当第一个指针走到链表尾部的时候,第二个指针指向的就是倒数第k个节点。
这里需要注意几个问题,保证代码的健壮性:

  • 输入的链表为空。
  • 输入的链表总的节点数小于k。
  • 输入的参数k为0
  1. public static ListNode Solution(ListNode head, int k){
  2. if(head == null || k == 0)
  3. return null;
  4. ListNode ahead = head;
  5. ListNode behind = null;
  6. for(int i = 0; i < k - 1; i++){
  7. if(ahead.next != null)
  8. ahead = ahead.next;
  9. else
  10. return null;
  11. }
  12. behind = head;
  13. while(ahead.next != null){
  14. ahead = ahead.next;
  15. behind = behind.next;
  16. }
  17. return behind;
  18. }

25.1 相关题目1

题目描述:
  求链表的中间节点。如果链表中节点的总数为奇数,返回中间节点;如果为偶数,返回中间两个节点的任意一个。


思路:参考双指针法,定义两个指针,同时从头结点开始遍历,一个指针一次走一步,一个指针一次走两步。当快指针走到链表末尾的时候,走得慢的指针正好在链表的中间。注意程序的健壮性。

  1. public static ListNode findCenterNode(ListNode head){
  2. //链表为空或只有头结点
  3. if(head == null || head.next == null)
  4. return null;
  5. //链表只存在一个节点
  6. if(head.next.next == null)
  7. return head.next;
  8. ListNode fast = head;
  9. ListNode slow = head;
  10. while(fast.next != null){
  11. fast = fast.next.next;
  12. slow = slow.next;
  13. }
  14. return slow;
  15. }

25.2 相关题目2

题目描述:
  判断一个单向链表是否形成了环形结构。


方法1:使用HashSet。设置一个Hashset,顺序读取链表中的节点,判断Hashset中是否有该节点的唯一标识(ID)。如果在Hashset中,说明有环;如果不在Hashset中,将节点的ID存入Hashset。
这种方法时间复杂度已经最优,但是因为额外申请了Hashset,所以空间复杂度不算最优。

  1. //返回true说明没有环,否则说明有环。
  2. public static boolean hsCycleOrNot(ListNode head){
  3. if(head == null)
  4. return true;
  5. HashSet<Integer> set = new HashSet<>();
  6. ListNode cur = head;
  7. while(cur.next != null){
  8. if(set.contains(cur.next.value))
  9. return false;
  10. else
  11. set.add(cur.next.value);
  12. cur = cur.next;
  13. }
  14. return true;
  15. }

方法2:使用双指针。定义一个快指针和一个慢指针同时从链表的头结点出发,快指针一次走两步,慢指针一次走一步。如果快指针能够追上慢指针,那么链表就是有环的;如果快指针走到链表末尾都没有追上慢指针,说明没有环。

  1. public static boolean hasCycleOrNot(ListNode head){
  2. if(head == null)
  3. return true;
  4. ListNode fast = head;
  5. ListNode slow = head;
  6. while(fast.next != null){
  7. if(fast.next.next != null)
  8. fast = fast.next.next;
  9. else
  10. return true;
  11. if(fast == slow)
  12. return false;
  13. slow = slow.next;
  14. }
  15. return true;
  16. }

26. 链表中环的入口节点

题目描述:
  一个链表中包含环,请找出该链表的环的入口结点。


思路1:使用哈希表。参看上题。


思路2:看下图,如果链表中存在环,且环的长度为k,那么环的入口就是倒数第k个节点。现在,只要求得环的长度,就能够根据前面的链表中倒数第k个节点的思路进行求解,定义两个指针指向头结点,先让第一个指针在链表上移动n步,然后两个指针以相同的速度向前移动,当第二个指针指向环的入口结点时,第一个指针已经围绕着环走了一圈又回到了入口结点。
在前面的题目中判断链表中是否有环的时候,用到一快一慢两个指针,如果两个指针相遇,说明有环,且相遇的结点一定在环内。可以从这个结点出发,一边继续向前移动一边计数,当再次回到这个结点时,就可以得到环中的结点数了。

  1. //找到相遇点
  2. public static ListNode meetingNode(ListNode head){
  3. if(head == null)
  4. return null;
  5. ListNode slow = head.next;
  6. if(slow == null)
  7. return null;
  8. ListNode fast = slow.next;
  9. while(fast != null && slow != null){
  10. if(fast == slow)
  11. return fast;
  12. slow = slow.next;
  13. fast = fast.next;
  14. if(fast != null)
  15. fast = fast.next;
  16. }
  17. return null;
  18. }
  19. public static ListNode findEntryNode(ListNode head){
  20. ListNode meetingNode = meetingNode(head);
  21. if(meetingNode == null)
  22. return null;
  23. int nodeInLoop = 1;
  24. ListNode node1 = meetingNode;
  25. while(node1.next != meetingNode){
  26. node1 = node1.next;
  27. nodeInLoop++;
  28. }
  29. node1 = head;
  30. for(int i = 0; i < nodeInLoop; i++)
  31. node1 = node1.next;
  32. ListNode node2 = head;
  33. while(node1 != node2){
  34. node1 = node1.next;
  35. node2 = node2.next;
  36. }
  37. return node1;
  38. }

思路三:不用求得环的长度,只需在相遇时,让一个指针在相遇点出发,另一个指针在链表首部出发,然后两个指针一次走一步,当它们相遇时,就是环的入口处。
证明如下:

  • 假设存在环,fast以速度2运行,slow以速度1运行,在slow走到入口t时,如图(m1为在slow首次到t时fast的位置,a为h到t的距离,b为t到m1的距离,n为环的周长):

    由图知fast走的距离为a+b+xn,slow走的距离为a,又v(fast) = 2*v(slow),所以x(fast) = 2*x(slow),即2a = a+b+xn,因此a = b+xn。
    m1逆时针到t的距离为n-b。
  • 在首次相遇时,如图(m2为相遇点):

    由于m1逆时针到t的距离为n-b,即要达到相遇需要追赶n-b的距离,由于两者速度差为1,因此需要n-b的时间才能相遇,此时slow再次向后n-b距离,即到达m2位置与fast相遇,因为一周长度为n,因此到t的距离为 n-(n-b) = b。
  • 为何令slow重新从pHead以速度1开始走,令fast从m2以速度1走?要想在入口t相遇,则需要从m2处再走b+xn的距离,刚好pHead处符合(由1)可知),所以令slow从pHead开始走。在相遇后就是入口t的位置。
  1. public static ListNode findEntryNode_1(ListNode head){
  2. ListNode meetingNode = meetingNode(head);
  3. if(meetingNode == null)
  4. return null;
  5. ListNode node1 = meetingNode;
  6. ListNode node2 = head;
  7. while(node1 != node2){
  8. node1 = node1.next;
  9. node2 = node2.next;
  10. }
  11. return node1;
  12. }

27. 反转链表

题目描述:
  定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。


思路一:迭代。

  1. public static ListNode reverseList(ListNode head){
  2. ListNode reverseHead = null;
  3. ListNode node = head;
  4. ListNode preNode = null;
  5. while(node != null){
  6. ListNode next = node.next;
  7. if(next == null)
  8. reverseHead = node;
  9. node.next = preNode;
  10. preNode = node;
  11. node = next;
  12. }
  13. return reverseHead;
  14. }

思路二:递归。

  1. public ListNode ReverseList(ListNode head) {
  2. if (head == null || head.next == null)
  3. return head;
  4. ListNode next = head.next;
  5. head.next = null;
  6. ListNode newHead = ReverseList(next);
  7. next.next = head;
  8. return newHead;
  9. }

28. 合并两个排序的链表

题目描述:
  输入两个递增排序的链表,合并这两个链表并使得新链表中的结点仍然是按照递增排序的。


思路一:迭代。

  1. //新建一个链表存放排序后的链表
  2. public ListNode Merge(ListNode list1, ListNode list2) {
  3. ListNode head = new ListNode(-1);
  4. ListNode cur = head;
  5. while (list1 != null && list2 != null) {
  6. if (list1.val <= list2.val) {
  7. cur.next = list1;
  8. list1 = list1.next;
  9. } else {
  10. cur.next = list2;
  11. list2 = list2.next;
  12. }
  13. cur = cur.next;
  14. }
  15. if (list1 != null)
  16. cur.next = list1;
  17. if (list2 != null)
  18. cur.next = list2;
  19. return head.next;
  20. }

思路二:递归。

  1. public ListNode Merge(ListNode list1, ListNode list2) {
  2. if (list1 == null)
  3. return list2;
  4. if (list2 == null)
  5. return list1;
  6. if (list1.val <= list2.val) {
  7. list1.next = Merge(list1.next, list2);
  8. return list1;
  9. } else {
  10. list2.next = Merge(list1, list2.next);
  11. return list2;
  12. }
  13. }

29. 树的子结构

题目描述:
  输入两个二叉树A和B,判断B是不是A的子结构。


思路:

  • 首先我们的思路应该是从二叉树A的根结点开始递归遍历整棵树,每访问到一个结点,都要检查当前结点是否已经是子树的开始结点,否则传入该结点的左右孩子继续检查
  • 在判断当前结点是否已经是子树的开始结点时,首先判断结点值是否相等,相等的话再判断各自的左右孩子是否也对应相等(此时要注意,子树可以先为空,但二叉树A不能先为空)
  1. public boolean HasSubtree(TreeNode root1, TreeNode root2) {
  2. if(root1 == null || root2 == null)
  3. return false;
  4. //要么当前结点已经是子树 要么当前结点的左孩子或右孩子存在子树
  5. return IsSubtree(root1,root2) || HasSubtree(root1.left,root2) || HasSubtree(root1.right,root2);
  6. }
  7. public boolean IsSubtree(TreeNode root1,TreeNode root2){
  8. if(root2 == null)
  9. return true;
  10. if(root1 == null)
  11. return false;
  12. if(root1.val == root2.val)
  13. return IsSubtree(root1.left,root2.left) && IsSubtree(root1.right,root2.right);
  14. else
  15. return false;
  16. }

30. 二叉树的镜像

题目描述:
  请完成一个函数,输入一个二叉树,输出该二叉树的镜像。




思路一:使用递归。先前序遍历树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点,当交换玩素有的非叶子结点的左右子节点知乎,就得到了树的镜像。

  1. //递归
  2. public static void mirrorBiTreeRecursively(BiTreeNode root){
  3. if(root == null)
  4. return;
  5. if(root.getLeft() == null && root.getRight() == null)
  6. return;
  7. BiTreeNode node = root.getLeft();
  8. root.setLeft(root.getRight());
  9. root.setRight(node);
  10. if(root.getLeft() != null)
  11. mirrorBiTreeRecursively(root.getLeft());
  12. if(root.getRight() != null)
  13. mirrorBiTreeRecursively(root.getRight());
  14. }

思路二:非递归,使用栈。层次遍历,根节点不为 null 将根节点入队,判断队不为空时,节点出队,交换该节点的左右孩子,如果左右孩子不为空,将左右孩子入队。

  1. //迭代。层次遍历
  2. public static void mirrorBiTreeLevel(BiTreeNode root){
  3. if(root == null)
  4. return;
  5. if(root.getLeft() == null && root.getRight() == null)
  6. return;
  7. Stack<BiTreeNode> stack = new Stack<>();
  8. stack.push(root);
  9. while(!stack.isEmpty()){
  10. BiTreeNode node = stack.pop();
  11. swapSonNode(node);
  12. if(node.getRight() != null)
  13. stack.push(node.getRight());
  14. if(node.getLeft() != null)
  15. stack.push(node.getLeft());
  16. }
  17. }
  18. public static void swapSonNode(BiTreeNode root){
  19. BiTreeNode node = root.getLeft();
  20. root.setLeft(root.getRight());
  21. root.setRight(node);
  22. }

  树的节点结构如下:

  1. public class BiTreeNode {
  2. private int value;
  3. private BiTreeNode left;
  4. private BiTreeNode right;
  5. public BiTreeNode(){}
  6. public BiTreeNode(int value){
  7. this.value = value;
  8. }
  9. public BiTreeNode(int value, BiTreeNode left, BiTreeNode right){
  10. this.value = value;
  11. this.left = left;
  12. this.right = right;
  13. }
  14. public int getValue() {
  15. return value;
  16. }
  17. public void setValue(int value) {
  18. this.value = value;
  19. }
  20. public BiTreeNode getLeft() {
  21. return left;
  22. }
  23. public void setLeft(BiTreeNode left) {
  24. this.left = left;
  25. }
  26. public BiTreeNode getRight() {
  27. return right;
  28. }
  29. public void setRight(BiTreeNode right) {
  30. this.right = right;
  31. }
  32. }

31. 二叉树的镜像

题目描述:
  请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,他就是对称的。


思路:可以通过比较二叉树的前序遍历序列和对称前序遍历序列(即先遍历父节点,然后右子树,最后左子树)来判断二叉树是不是对称的。

  1. public static boolean isSymmetrical(BiTreeNode root1, BiTreeNode root2){
  2. if(root1 == null && root2 == null)
  3. return true;
  4. if(root1 == null || root2 == null)
  5. return false;
  6. if(root1.getValue() != root2.getValue())
  7. return false;
  8. return isSymmetrical(root1.getLeft(), root2.getRight()) &&
  9. isSymmetrical(root1.getRight(), root2.getLeft());
  10. }
  11. public static boolean isSymmetrical(BiTreeNode root){
  12. return isSymmetrical(root.getLeft(), root.getRight());
  13. }

32. 顺时针打印矩阵

题目描述:
  输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。如下矩阵的打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10


思路:注意边界条件的判断。

  1. public ArrayList<Integer> printMatrix(int[][] matrix) {
  2. ArrayList<Integer> ret = new ArrayList<>();
  3. int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1;
  4. while (r1 <= r2 && c1 <= c2) {
  5. for (int i = c1; i <= c2; i++)
  6. ret.add(matrix[r1][i]);
  7. for (int i = r1 + 1; i <= r2; i++)
  8. ret.add(matrix[i][c2]);
  9. if (r1 != r2)
  10. for (int i = c2 - 1; i >= c1; i--)
  11. ret.add(matrix[r2][i]);
  12. if (c1 != c2)
  13. for (int i = r2 - 1; i > r1; i--)
  14. ret.add(matrix[i][c1]);
  15. r1++; r2--; c1++; c2--;
  16. }
  17. return ret;
  18. }

33. 包含min函数的栈

题目描述:
  定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的min函数。在该栈中,调用min、push、pop的时间复杂度都是o(1)。


思路:题目要求复杂度为O(1),所以遍历的话肯定满足不了需求,一个想法就是用空间来换取时间。定义一个辅助栈来存放最小值。
栈 3,4,2,5,1
辅助栈 3,3,2,2,1
每入栈一次,就与辅助栈顶比较大小,如果小就入栈,如果大就入栈当前的辅助栈顶
当出栈时,辅助栈也要出栈
这种做法可以保证辅助栈顶一定都当前栈的最小值

  1. private Stack<Integer> dataStack = new Stack<>();
  2. private Stack<Integer> minStack = new Stack<>();
  3. public void push(int data){
  4. dataStack.push(data);
  5. minStack.push(minStack.isEmpty() ? data : Math.min(data, minStack.peek()));
  6. }
  7. public void pop(){
  8. if(!dataStack.isEmpty() && !minStack.isEmpty()){
  9. dataStack.pop();
  10. minStack.pop();
  11. }
  12. }
  13. public int top(){
  14. if(!dataStack.isEmpty())
  15. return dataStack.peek();
  16. return -1;
  17. }
  18. public int min(){
  19. if(!minStack.isEmpty())
  20. return minStack.peek();
  21. return -1;
  22. }

另一种思路,只使用一个栈。

  1. Stack<Integer> stack;
  2. int min = Integer.MAX_VALUE;
  3. public MinStack() {
  4. stack = new Stack<>();
  5. }
  6. public void push(int x) {
  7. if(x <= min){
  8. stack.push(min);
  9. min = x;
  10. }
  11. stack.push(x);
  12. }
  13. public void pop() {
  14. if(stack.pop() == min)
  15. min = stack.pop();
  16. }
  17. public int top() {
  18. return stack.peek();
  19. }
  20. public int getMin() {
  21. return min;
  22. }

34. 栈的压入、弹出序列

题目描述:
  输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。
  例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。


思路:使用一个栈来模拟压入弹出操作。按照入栈的顺序将元素压入模拟栈中,每压入一个元素,都要将当前栈顶元素和下一个出栈数字比较,如果相等,直接弹出;如果不相等,把压栈顺序中还没有入栈的数字压入模拟栈,知道把下一个需要弹出的数字压入栈顶位置。如果所有的数字都压入栈了仍然没有找到下一个弹出数字,那么该序列不可能是一个弹栈序列。

  1. public static boolean isAPopOrder(int[] pushSequence, int[] popSequence){
  2. if(pushSequence == null || popSequence == null)
  3. return false;
  4. Stack<Integer> stack = new Stack<>();
  5. int n = pushSequence.length;
  6. for(int pushInx = 0, popInx = 0; pushInx < n; pushInx++){
  7. stack.push(pushSequence[pushInx]);
  8. while(popInx < n && !stack.isEmpty() && stack.peek() == popSequence[popInx]){
  9. stack.pop();
  10. popInx++;
  11. }
  12. }
  13. return stack.isEmpty();
  14. }

35. 从上往下打印二叉树

题目描述:
  从上往下打印出二叉树的每个节点,同层节点从左至右打印。


思路:二叉树的层次遍历。

  1. public static void levelOrder(BiTreeNode root){
  2. Queue<BiTreeNode> queue = new LinkedList<>();
  3. BiTreeNode temp;
  4. if(root != null)
  5. queue.offer(root);
  6. while(!queue.isEmpty()){
  7. temp = queue.poll();
  8. System.out.print(temp.getValue()+" ");
  9. if(null != temp.getLeft())
  10. queue.offer(temp.getLeft());
  11. if(null != temp.getRight())
  12. queue.offer(temp.getRight());
  13. }
  14. }

36. 把二叉树打印成多行

题目描述:
  从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。


思路:和上一个题差不多,再定义两个变量toBePrinted和nextLevel,前者表示本层中还需打印的节点数,后者表示下一层的节点数。

  1. public static void printWithMultiline(BiTreeNode root){
  2. Queue<BiTreeNode> queue = new LinkedList<>();
  3. BiTreeNode temp;
  4. int nextLevel = 0; //下一层需要打印的节点数
  5. int toBePrinted = 1; //本层还需要打印的节点数
  6. if(root != null)
  7. queue.offer(root);
  8. while(!queue.isEmpty()){
  9. temp = queue.poll();
  10. System.out.print(temp.getValue()+" ");
  11. if(null != temp.getLeft()){
  12. nextLevel++;
  13. queue.offer(temp.getLeft());
  14. }
  15. if(null != temp.getRight()){
  16. nextLevel++;
  17. queue.offer(temp.getRight());
  18. }
  19. toBePrinted--;
  20. if(toBePrinted == 0){
  21. System.out.println();
  22. toBePrinted = nextLevel;
  23. nextLevel = 0;
  24. }
  25. }
  26. }

另一种选择,不打印,而是存储起来。

  1. ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
  2. ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
  3. Queue<TreeNode> queue = new LinkedList<>();
  4. queue.add(pRoot);
  5. while (!queue.isEmpty()) {
  6. ArrayList<Integer> list = new ArrayList<>();
  7. int cnt = queue.size();
  8. while (cnt-- > 0) {
  9. TreeNode node = queue.poll();
  10. if (node == null)
  11. continue;
  12. list.add(node.val);
  13. queue.add(node.left);
  14. queue.add(node.right);
  15. }
  16. if (list.size() != 0)
  17. ret.add(list);
  18. }
  19. return ret;
  20. }

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

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


思路一:使用两个栈。在打印某一行的结点时,把下一层的子节点保存到响应的栈里。如果当前打印的是奇数层,则先保存左子结点再保存右子结点到第一个栈里;如果当前打印的是偶数层,则先保存右子结点再保存左子结点到第二个栈里。

  1. //用栈实现
  2. public static void printMultiline(BiTreeNode root){
  3. if(root == null)
  4. return;
  5. Stack<BiTreeNode>[] levels = new Stack[2];
  6. //注意:如果没有下面三行的话,levels[current].push(root);会报空指针异常,
  7. //因为只是定义了一个存放Stack的容器,并没有对其进行初始化
  8. Stack<BiTreeNode> stack1 = new Stack<>();
  9. Stack<BiTreeNode> stack2 = new Stack<>();
  10. levels[0] = stack1;
  11. levels[1] = stack2;
  12. int current = 0;
  13. int next = 1;
  14. levels[current].push(root);
  15. while(!levels[0].isEmpty() || !levels[1].isEmpty()){
  16. BiTreeNode node = levels[current].peek();
  17. levels[current].pop();
  18. System.out.print(node.getValue()+" ");
  19. if(current == 0){
  20. if(node.getLeft() != null)
  21. levels[next].push(node.getLeft());
  22. if(node.getRight() != null)
  23. levels[next].push(node.getRight());
  24. }else {
  25. if(node.getRight() != null)
  26. levels[next].push(node.getRight());
  27. if(node.getLeft() != null)
  28. levels[next].push(node.getLeft());
  29. }
  30. if(levels[current].isEmpty()){
  31. System.out.println();
  32. current = 1 - current;
  33. next = 1 - next;
  34. }
  35. }
  36. }

思路二:使用一个队列加一个ArrayList,调用Collections的reverse()方法对ArrayList中的元素进行翻。

  1. public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
  2. ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
  3. Queue<TreeNode> queue = new LinkedList<>();
  4. queue.add(pRoot);
  5. boolean reverse = false;
  6. while (!queue.isEmpty()) {
  7. ArrayList<Integer> list = new ArrayList<>();
  8. int cnt = queue.size();
  9. while (cnt-- > 0) {
  10. TreeNode node = queue.poll();
  11. if (node == null)
  12. continue;
  13. list.add(node.val);
  14. queue.add(node.left);
  15. queue.add(node.right);
  16. }
  17. if (reverse)
  18. Collections.reverse(list);
  19. reverse = !reverse;
  20. if (list.size() != 0)
  21. ret.add(list);
  22. }
  23. return ret;
  24. }

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

题目描述:
  输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。


在后序遍历序列中,最后一个数字是树的根节点的值。数组中前面的数字可以分为两部分:第一部分是左子树节点的值,它们都比根节点的值小;第二部分是右子树节点的值,它们都比根节点的值大。
所以先取数组中最后一个数,作为根节点。然后从数组开始计数比根节点小的数,并将这些记作左子树,然后判断后序的树是否比根节点大,如果有点不满足,则跳出,并判断为不成立。全满足的话,依次对左子树和右子树递归判断。

  1. public static boolean VerifySquenceOfBST(int[] sequence) {
  2. if (sequence == null || sequence.length == 0)
  3. return false;
  4. return verify(sequence, 0, sequence.length - 1);
  5. }
  6. private static boolean verify(int[] sequence, int first, int last) {
  7. //递归终止条件
  8. if (last - first <= 1)
  9. return true;
  10. int rootVal = sequence[last];
  11. int cutIndex = first;
  12. while (cutIndex < last && sequence[cutIndex] <= rootVal)
  13. cutIndex++;
  14. for (int i = cutIndex; i < last; i++)
  15. if (sequence[i] < rootVal)
  16. return false;
  17. return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1);
  18. }

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

题目描述:
  输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
  下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12


思路一:当用前序遍历的方式访问到某一节点时,我们把该结点添加到路径上,并累加该结点的值。如果该结点为叶结点并且路径中结点值的和刚好为输入的整数,则当前的路径符合要求,我们把它打印出来。如果当前的结点不是叶结点,则继续访问它的子节点。当前结点访问结束后,递归函数将自动回到它的父节点。因此我们在函数退出之前要在路径上删除当前结点并减去当前结点的值,以确保返回父节点时路径刚好是从根节点到父节点的路径。我们不难看出保存路径的数据结构实际上是一个栈,因此路径要与递归调用状态一致,而递归调用的本质上是一个压栈和出栈的过程。

  1. static Stack<Integer> path = new Stack<>();
  2. public static void findPath(BiTreeNode root, int sum){
  3. backtracking(root, sum, path);
  4. }
  5. public static void backtracking(BiTreeNode root, int target, Stack<Integer> stack){
  6. if(root == null)
  7. return;
  8. stack.push(root.getValue());
  9. target -= root.getValue();
  10. //符合条件就将路径输出
  11. if(target == 0 && root.getLeft() == null && root.getRight() == null){
  12. System.out.println("A path is found:");
  13. for(Integer i : stack)
  14. System.out.print(i+" ");
  15. System.out.println();
  16. }
  17. //如果不是叶子节点,则遍历它的子节点
  18. if(root.getLeft() != null)
  19. backtracking(root.getLeft(), target, stack);
  20. if(root.getRight() != null)
  21. backtracking(root.getRight(), target, stack);
  22. //返回父节点之前在路径上删除当前节点
  23. stack.pop();
  24. }

思路二:和思路一样,不过实现过程使用ArrayList。

  1. private ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
  2. public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
  3. backtracking(root, target, new ArrayList<>());
  4. return ret;
  5. }
  6. private void backtracking(TreeNode node, int target, ArrayList<Integer> path) {
  7. if (node == null)
  8. return;
  9. path.add(node.val);
  10. target -= node.val;
  11. if (target == 0 && node.left == null && node.right == null) {
  12. ret.add(new ArrayList<>(path));
  13. } else {
  14. backtracking(node.left, target, path);
  15. backtracking(node.right, target, path);
  16. }
  17. path.remove(path.size() - 1);
  18. }

40. 复杂链表的复制

题目描述:
  输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。链表结构定义如下:

  1. class RandomListNode{
  2. int value;
  3. RandomListNode next;
  4. RandomListNode random;
  5. public RandomListNode(int value) {
  6. this.value = value;
  7. }
  8. }




思路一:分为两步:

  • 第一步是复制原始链表上的每个结点N创建N',然后把这些创建出来的结点用next链接起来。同时我们把的配对信息放到一个哈希表中。
  • 第二步还是设置复制链表上的每个结点的random。如果在原始链表中结点N的random指向结点S,那么在复制链表中,对应的N'应该指向S'。由于有了哈希表,我们可以在O(1)的时间根据S找到S’。
  1. public static RandomListNode cloneRandomList(RandomListNode root){
  2. if(root == null)
  3. return null;
  4. HashMap<RandomListNode, RandomListNode> map = new HashMap<>();
  5. RandomListNode cloneRoot = new RandomListNode(root.value);
  6. RandomListNode node = root, cloneNode = cloneRoot;
  7. map.put(node, cloneNode);
  8. while(node.next != null){
  9. cloneNode.next = new RandomListNode(node.next.value);
  10. node = node.next;
  11. cloneNode = cloneNode.next;
  12. map.put(node, cloneNode);
  13. }
  14. node = root;
  15. cloneNode = cloneRoot;
  16. while(cloneNode != null){
  17. cloneNode.random = map.get(node.random);
  18. node = node.next;
  19. cloneNode = cloneNode.next;
  20. }
  21. return cloneRoot;
  22. }

思路二:分为三步:

  • 第一步:根据原始链表的每个结点N创建对应的N'。不过我们把N’链接在N的后面。
  • 第二步:设置复制出来的结点的random。原始链表上的A的random指向结点C,那么其对应复制出来的A’是A的next指向的结点,同样C’也是C的next指向的结点。即A' = A.next,A'.random = A.random.next;故像这样就可以把每个结点的random设置完毕。
  • 第三步:将这个长链表拆分成两个链表:把奇数位置的结点用next链接起来就是原始链表,把偶数位置的结点用next链接起来就是复制出来的链表。
  1. public static RandomListNode cloneRandomList(RandomListNode pHead) {
  2. if (pHead == null)
  3. return null;
  4. // 插入新节点
  5. RandomListNode cur = pHead;
  6. while (cur != null) {
  7. RandomListNode clone = new RandomListNode(cur.value);
  8. clone.next = cur.next;
  9. cur.next = clone;
  10. cur = clone.next;
  11. }
  12. // 建立 random 链接
  13. cur = pHead;
  14. while (cur != null) {
  15. RandomListNode clone = cur.next;
  16. if (cur.random != null)
  17. clone.random = cur.random.next;
  18. cur = clone.next;
  19. }
  20. // 拆分
  21. cur = pHead;
  22. RandomListNode pCloneHead = pHead.next;
  23. while (cur.next != null) {
  24. RandomListNode next = cur.next;
  25. cur.next = next.next;
  26. cur = next;
  27. }
  28. return pCloneHead;
  29. }

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

题目描述:
  输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。




思路一:由于要求转化后的链表是一个排序的链表,而二叉搜索树的中序遍历结果是排序的,所以自然而然想到> 中序遍历二叉树的方法,而遍历最简单的方法就是递归。步骤如下:

  • 将左子树构造成双链表,并返回链表头节点。
  • 定位至左子树双链表最后一个节点。
  • 如果左子树链表不为空的话,将当前root追加到左子树链表。
  • 将右子树构造成双链表,并返回链表头节点。
  • 如果右子树链表不为空的话,将该链表追加到root节点之后。
  • 根据左子树链表是否为空确定返回的节点。
  1. public static BiTreeNode Convert(BiTreeNode root) {
  2. if(root==null)
  3. return null;
  4. if(root.getLeft()==null&&root.getRight()==null)
  5. return root;
  6. // 1.将左子树构造成双链表,并返回链表头节点
  7. BiTreeNode left = Convert(root.getLeft());
  8. BiTreeNode p = left;
  9. // 2.定位至左子树双链表最后一个节点
  10. while(p!=null&&p.getRight()!=null){
  11. p = p.getRight();
  12. }
  13. // 3.如果左子树链表不为空的话,将当前root追加到左子树链表
  14. if(left!=null){
  15. p.setRight(root);
  16. root.setLeft(p);
  17. }
  18. // 4.将右子树构造成双链表,并返回链表头节点
  19. BiTreeNode right = Convert(root.getRight());
  20. // 5.如果右子树链表不为空的话,将该链表追加到root节点之后
  21. if(right!=null){
  22. right.setLeft(root);
  23. root.setRight(right);
  24. }
  25. return left!=null?left:root;
  26. }

思路二:改进递归版。思路与方法二中的递归版一致,仅对第2点中的定位作了修改,新增一个全局变量记录左子树的最后一个节点。

  1. // 记录子树链表的最后一个节点,终结点只可能为只含左子树的非叶节点与叶节点
  2. protected static BiTreeNode leftLast = null;
  3. public static BiTreeNode Convert_1(BiTreeNode root) {
  4. if(root==null)
  5. return null;
  6. if(root.getLeft()==null&&root.getRight()==null){
  7. leftLast = root;// 最后的一个节点可能为最右侧的叶节点
  8. return root;
  9. }
  10. // 1.将左子树构造成双链表,并返回链表头节点
  11. BiTreeNode left = Convert_1(root.getLeft());
  12. // 3.如果左子树链表不为空的话,将当前root追加到左子树链表
  13. if(left!=null){
  14. leftLast.setRight(root);
  15. root.setLeft(leftLast);
  16. }
  17. leftLast = root;// 当根节点只含左子树时,则该根节点为最后一个节点
  18. // 4.将右子树构造成双链表,并返回链表头节点
  19. BiTreeNode right = Convert_1(root.getRight());
  20. // 5.如果右子树链表不为空的话,将该链表追加到root节点之后
  21. if(right!=null){
  22. right.setLeft(root);
  23. root.setRight(right);
  24. }
  25. return left!=null?left:root;
  26. }

思路三:递归简化版,直接用中序遍历。

  1. static BiTreeNode head = null;
  2. static BiTreeNode realHead = null;
  3. public static BiTreeNode Convert(BiTreeNode pRootOfTree) {
  4. ConvertSub(pRootOfTree);
  5. return realHead;
  6. }
  7. private static void ConvertSub(BiTreeNode pRootOfTree) {
  8. if(pRootOfTree==null) return;
  9. ConvertSub(pRootOfTree.getLeft());
  10. if (head == null) {
  11. head = pRootOfTree;
  12. realHead = pRootOfTree;
  13. } else {
  14. head.setRight(pRootOfTree);
  15. pRootOfTree.setLeft(head);
  16. head = pRootOfTree;
  17. }
  18. ConvertSub(pRootOfTree.getRight());
  19. }

思路四:非递归版。中序遍历既可以使用递归实现也可以使用非递归实现。

  1. public static BiTreeNode ConvertBSTToBiList(BiTreeNode root) {
  2. if(root==null)
  3. return null;
  4. Stack<BiTreeNode> stack = new Stack<BiTreeNode>();
  5. BiTreeNode p = root;
  6. BiTreeNode pre = null;// 用于保存中序遍历序列的上一节点
  7. boolean isFirst = true;
  8. while(p != null || !stack.isEmpty()){
  9. while(p != null){
  10. stack.push(p);
  11. p = p.getLeft();
  12. }
  13. p = stack.pop();
  14. if(isFirst){
  15. root = p;// 将中序遍历序列中的第一个节点记为root
  16. pre = root;
  17. isFirst = false;
  18. }else{
  19. pre.setRight(p);
  20. p.setLeft(pre);
  21. pre = p;
  22. }
  23. p = p.getRight();
  24. }
  25. return root;
  26. }

42. 序列化二叉树

题目描述:
  请实现两个函数,分别用来序列化和反序列化二叉树


思路:序列化的过程其实就是前序遍历的过程,将二叉树序列化为一个字符串,null节点用特殊字符(比如#来代替);反序列化的过程其实就是先序构建二叉树的过程,将序列化后字符串中的字符依次读入,就可以构建成一个二叉树。

  1. /**
  2. * 序列化
  3. */
  4. //递归法序列化
  5. private static void serializeBiTree(BiTreeNode root, StringBuilder sb){
  6. if(root == null){
  7. sb.append("# ");
  8. return;
  9. }
  10. sb.append(root.getValue());
  11. serializeBiTree(root.getLeft(), sb);
  12. serializeBiTree(root.getRight(), sb);
  13. }
  14. public static String serializeBiTree(BiTreeNode root){
  15. StringBuilder sb = new StringBuilder("");
  16. serializeBiTree(root, sb);
  17. return sb.toString();
  18. }
  19. //非递归法序列化
  20. private static void nrSerializeBiTree(BiTreeNode root, StringBuilder sb){
  21. if(root == null)
  22. return;
  23. Stack<BiTreeNode> stack = new Stack<>();
  24. BiTreeNode pNode = root;
  25. stack.push(pNode);
  26. while(!stack.isEmpty()){
  27. BiTreeNode temp = stack.pop();
  28. if(temp == null)
  29. sb.append("# ");
  30. else {
  31. sb.append(temp.getValue()).append(" ");
  32. nrSerializeBiTree(root.getLeft(), sb);
  33. nrSerializeBiTree(root.getRight(), sb);
  34. }
  35. }
  36. }
  1. /**
  2. * 反序列化
  3. */
  4. //递归法反序列化
  5. private String deserializeStr;
  6. public BiTreeNode deserialize(String str) {
  7. deserializeStr = str;
  8. return deserialize();
  9. }
  10. private BiTreeNode deserialize() {
  11. if (deserializeStr.length() == 0)
  12. return null;
  13. int index = deserializeStr.indexOf(" ");
  14. String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index);
  15. deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1);
  16. if (node.equals("#"))
  17. return null;
  18. int val = Integer.valueOf(node);
  19. BiTreeNode t = new BiTreeNode(val);
  20. t.setLeft(deserialize());
  21. t.setRight(deserialize());
  22. return t;
  23. }

43. 字符串的排列

题目描述:
  输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。


思路:字符串的排列问题,可以使用递归来完成,这个之前已经总结过了。但是这个题目的要求是按照字典顺序进行排列,所以需要对输入的字符串数组进行一次排序。

  1. private ArrayList<String> ret = new ArrayList<>();
  2. public ArrayList<String> permutation(String str) {
  3. if (str.length() == 0)
  4. return ret;
  5. char[] chars = str.toCharArray();
  6. //对输入的数组进行排序
  7. Arrays.sort(chars);
  8. backtracking(chars, new boolean[chars.length], new StringBuilder());
  9. return ret;
  10. }
  11. private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) {
  12. if (s.length() == chars.length) {
  13. ret.add(s.toString());
  14. return;
  15. }
  16. for (int i = 0; i < chars.length; i++) {
  17. if (hasUsed[i])
  18. continue;
  19. if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */
  20. continue;
  21. hasUsed[i] = true;
  22. s.append(chars[i]);
  23. backtracking(chars, hasUsed, s);
  24. s.deleteCharAt(s.length() - 1);
  25. hasUsed[i] = false;
  26. }
  27. }

44. 数组中超过一半的数

题目描述:
  题目:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现5次,超过数组长度的一半,因此输出2


思路一:数组排序,然后中间值肯定是要查找的值。排序最小的时间复杂度(快速排序)O(NlogN),加上遍历。

  1. public static int moreThanHalf(int[] nums){
  2. if(nums == null || nums.length <= 0)
  3. return 0;
  4. Arrays.sort(nums);
  5. int result = nums[nums.length >> 1]; //用右移代替除法
  6. int count = 0;
  7. //判断数组中是不是存在出现次数超过一半的数
  8. for(int i : nums){
  9. if(i == result)
  10. count++;
  11. }
  12. return count > (nums.length >> 1) ? result : 0;
  13. }

思路二:使用散列表的方式,也就是统计每个数组出现的次数,输出出现次数大于数组长度的数字。

  1. public static int moreThanHalf(int[] nums){
  2. if(nums == null || nums.length <= 0)
  3. return 0;
  4. HashMap<Integer, Integer> map = new HashMap<>();
  5. for(int i = 0; i < nums.length; i++){
  6. if(map.containsKey(nums[i]))
  7. map.put(nums[i], map.get(nums[i]) + 1);
  8. else
  9. map.put(nums[i], 1);
  10. }
  11. for(int key : map.keySet()){
  12. if(map.get(key) > (nums.length >> 1))
  13. return key;
  14. }
  15. return 0;
  16. }

思路三:出现的次数超过数组长度的一半,表明这个数字出现的次数比其他数出现的次数的总和还多。
  考虑每次删除两个不同的数,那么在剩下的数中,出现的次数仍然超过总数的一般,不断重复该过程,排除掉其他的数,最终找到那个出现次数超过一半的数字。这个方法的时间复杂度是O(N),空间复杂度是O(1)。
  换个思路,这个可以通过计数实现,而不是真正物理删除。在遍历数组的过程中,保存两个值,一个是数组中数字,一个是出现次数。当遍历到下一个数字时,如果这个数字跟之前保存的数字相同,则次数加1,如果不同,则次数减1。如果次数为0,则保存下一个数字并把次数设置为1,由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。

  1. public static int moreThanHalf(int[] nums){
  2. if(nums == null || nums.length <= 0)
  3. return 0;
  4. int result = nums[0];
  5. //如果times==0,说明遍历过的数中,有一半的数是相同的。
  6. int times = 1;
  7. for(int i = 1; i < nums.length; i++){
  8. if(times == 0){
  9. result = nums[i];
  10. times = 1;
  11. }
  12. else if(result == nums[i])
  13. times++;
  14. else
  15. times--;
  16. }
  17. int count = 0;
  18. for(int i : nums){
  19. if(i == result)
  20. count++;
  21. }
  22. return count > (nums.length >> 1) ? result : 0;
  23. }

思路四:排序算法的改进。如果对一个数组进行排序,位于中间位置的那个数字肯定是所求的值。对数组排序的时间复杂度是O(nlog(n)),但是对于这道题目,还有更好的算法,能够在时间复杂度O(n)内求出。
  借鉴快速排序算法,其中的Partition()方法是一个最重要的方法,该方法返回一个index,能够保证index位置的数是已排序完成的,在index左边的数都比index所在的数小,在index右边的数都比index所在的数大。那么本题就可以利用这样的思路来解。
  通过Partition()返回index,如果index==mid,那么就表明找到了数组的中位数;如果indexmid,表明中位数在[start,index-1]之间。知道最后求得index==mid循环结束。

  1. private static int partition(int[] nums,int start,int end){
  2. int pivotkey = nums[start];
  3. int origin = start;
  4. while(start < end){
  5. while(start < end && nums[end] >= pivotkey) end--;
  6. while(start < end && nums[start] < pivotkey) start++;
  7. swap(nums, start, end);
  8. }
  9. swap(nums, start, end);
  10. swap(nums, origin, end);
  11. return end;
  12. }
  13. private static int[] swap(int[] ints, int x, int y) {
  14. int temp = ints[x];
  15. ints[x] = ints[y];
  16. ints[y] = temp;
  17. return ints;
  18. }
  19. public static int moreThanHalf(int[] nums){
  20. if(nums == null || nums.length == 0)
  21. return -1;
  22. int start = 0;
  23. int end = nums.length-1;
  24. int index = partition(nums, start, end);
  25. int mid = nums.length / 2;
  26. while(index != mid){
  27. if(index > mid)
  28. //如果调整数组以后获得的index大于middle,则继续调整start到index-1区段的数组
  29. index = partition(nums, start, index-1);
  30. else{
  31. //否则调整index+1到end区段的数组
  32. index = partition(nums, index+1, end);
  33. }
  34. }
  35. int count = 0;
  36. for(int i : nums){
  37. if(i == nums[index])
  38. count++;
  39. }
  40. return count > (nums.length >> 1) ? nums[index] : -1;
  41. }

45. 最小的K个数

题目描述:
  输入n数字,找出其中最小k个数字,例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4


思路一:最容易想到的方法就是全排序,然后取前K个数,但是这显然不是最简单的方法。借鉴快速排序的思想,快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素。复杂度:O(N) + O(1)
且只有当允许修改数组元素时才可以使用。

  1. public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
  2. ArrayList<Integer> ret = new ArrayList<>();
  3. if (k > nums.length || k <= 0)
  4. return ret;
  5. findKthSmallest(nums, k - 1);
  6. /* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */
  7. for (int i = 0; i < k; i++)
  8. ret.add(nums[i]);
  9. return ret;
  10. }
  11. public void findKthSmallest(int[] nums, int k) {
  12. int l = 0, h = nums.length - 1;
  13. while (l < h) {
  14. int j = partition(nums, l, h);
  15. if (j == k)
  16. break;
  17. if (j > k)
  18. h = j - 1;
  19. else
  20. l = j + 1;
  21. }
  22. }
  23. private int partition(int[] nums, int l, int h) {
  24. int p = nums[l]; /* 切分元素 */
  25. int i = l, j = h + 1;
  26. while (true) {
  27. while (i != h && nums[++i] < p) ;
  28. while (j != l && nums[--j] > p) ;
  29. if (i >= j)
  30. break;
  31. swap(nums, i, j);
  32. }
  33. swap(nums, l, j);
  34. return j;
  35. }
  36. private void swap(int[] nums, int i, int j) {
  37. int t = nums[i];
  38. nums[i] = nums[j];
  39. nums[j] = t;
  40. }

思路二:使用堆。构建一个大小为K的大顶堆,堆不满时,就往堆里放数据;当堆满,放数据之前要和堆顶元素比较,如果大于堆顶元素就不放,否则取出堆顶元素,并将新元素放入。时间复杂度为O(nlogk)。

  1. public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
  2. ArrayList<Integer> result = new ArrayList<Integer>();
  3. int length = input.length;
  4. if(k > length || k == 0){
  5. return result;
  6. }
  7. PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k, new Comparator<Integer>() {
  8. @Override
  9. public int compare(Integer o1, Integer o2) {
  10. return o2.compareTo(o1);
  11. }
  12. });
  13. for (int i = 0; i < length; i++) {
  14. if (maxHeap.size() != k) {
  15. maxHeap.offer(input[i]);
  16. } else if (maxHeap.peek() > input[i]) {
  17. Integer temp = maxHeap.poll();
  18. temp = null;
  19. maxHeap.offer(input[i]);
  20. }
  21. }
  22. for (Integer integer : maxHeap) {
  23. result.add(integer);
  24. }
  25. return result;
  26. }

思路三:使用红黑树(java可以使用TreeSet实现),和使用堆的思想一样。

  1. public ArrayList<Integer> GetLeastKNumbers(int[] input, int k) {
  2. ArrayList<Integer> result = new ArrayList<>();
  3. TreeSet<Integer> treeset = new TreeSet<>();
  4. treeset.clear();
  5. if(input == null || input.length == 0)
  6. return null;
  7. if (k < 1 || input.length < k)
  8. return result;
  9. for (int i = 0; i < input.length; i++) {
  10. if (treeset.size() < k)
  11. treeset.add(input[i]);
  12. else {
  13. int a = treeset.last();
  14. if (input[i] < a) {
  15. treeset.remove(a);
  16. treeset.add(input[i]);
  17. }
  18. }
  19. }
  20. Iterator<Integer> it = treeset.iterator();
  21. while (it.hasNext()) {
  22. result.add(it.next());
  23. }
  24. return result;
  25. }

46. 数据流的中位数

题目描述:
  如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。


思路:用两个推保存数据,保持两个堆的数据保持平衡(元素个数相差不超过1)。大顶堆存放的数据要比小顶堆的数据小。当两个推中元素为奇数个,将新加入元素加入到大顶堆,如果要加入的数据,比小顶堆的最小元素大,先将该元素插入小顶堆,然后将小顶堆的最小元素插入到大顶堆。当两个推中元素为偶数个,将新加入元素加入到小顶堆,如果要加入的数据,比大顶堆的最大元素小,先将该元素插入大顶堆,然后将大顶堆的最大元素插入到小顶堆。

  1. //右边是小顶堆
  2. private static PriorityQueue<Integer> minHeap = new PriorityQueue<>();
  3. //左边是大顶堆,右半边元素全部大于左半边元素。
  4. private static PriorityQueue<Integer> maxHeap = new PriorityQueue<>(15, new Comparator<Integer>() {
  5. @Override
  6. public int compare(Integer o1, Integer o2) {
  7. return o2 - o1;
  8. }
  9. });
  10. //当前数据流中读入的元素个数
  11. private static int count = 0;
  12. public static void insert(int value){
  13. if((count & 0x01) == 0){
  14. /* count 为偶数的情况下插入到右半边。
  15. * 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大,
  16. * 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */
  17. maxHeap.add(value);
  18. minHeap.add(maxHeap.poll());
  19. }else {
  20. minHeap.add(value);
  21. maxHeap.add(minHeap.poll());
  22. }
  23. count++;
  24. }
  25. public static double getMidian(){
  26. if((count & 0x01) == 0)
  27. return (minHeap.peek() + maxHeap.peek()) / 2.0; //偶数个,返回平均数
  28. else
  29. return (double)minHeap.peek(); //奇数个,返回小顶堆堆顶元素。因为一开始先插入的是小顶堆,小顶堆多一个元素
  30. }

47. 连续子数组的最大和

题目描述:
  {6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。


思路一:累加法。首先,我们需要定义一个变量curSum,用for循环来记录前i项的和,curSum每次都会更改,如果curSum的值小于0,我们再往后加只有减小最大和,所以我们需要将array[i+1]项的值重新赋值给curSum。
  另外,我们需要定义一个最大值greatestSum,每次改变curSum的值时,我们都需要将greatestSum和curSum进行比较,如果curSum大于greatestSum,我们则将curSum的值赋值给greatestSum。

  1. public static int greatestSumOfSubArray(int[] nums){
  2. if(nums == null || nums.length <= 0){
  3. System.out.println("Invalid input");
  4. return -1;
  5. }
  6. int curSum = 0;
  7. //0x80000000 = -2147483648
  8. int greatestSum = 0x80000000;
  9. for(int i = 0; i < nums.length; i++){
  10. if(curSum <= 0)
  11. curSum = nums[i];
  12. else
  13. curSum += nums[i];
  14. if(curSum > greatestSum)
  15. greatestSum = curSum;
  16. }
  17. return greatestSum;
  18. }

思路二:分治法。考虑将数组从中间分为两个子数组,则最大子数组必然出现在以下三种情况之一:
1、完全位于左边的数组中。
2、完全位于右边的数组中。
3、跨越中点,包含左右数组中靠近中点的部分。 递归将左右子数组再分别分成两个数组,直到子数组中只含有一个元素,退出每层递归前,返回上面三种情况中的最大值。

  1. public static int greatestSum(int[] nums){
  2. return greatestSumSub(nums, 0, nums.length - 1);
  3. }
  4. private static int greatestSumSub(int[] nums, int left, int right){
  5. if(nums == null || nums.length < 0)
  6. return -1;
  7. if(left == right)
  8. return Math.max(0, nums[left]);
  9. int maxLeftSum = 0, maxRightSum = 0; //左右边不包含中间的最大和
  10. int maxLeftBorderSum = 0, maxRightBorderSum = 0; //包含中间边界的左右边最大和
  11. int curLeftBorderSum = 0, curRightBorderSum = 0; //包含中间边界的左右边当前和
  12. //求含中间边界的左右部分的最大值
  13. int mid = (left + right) >> 1;
  14. for(int i = mid; i >= left; i--){
  15. curLeftBorderSum += nums[i];
  16. maxLeftBorderSum = Math.max(curLeftBorderSum, maxLeftBorderSum);
  17. }
  18. for(int i = mid + 1; i <= right; i++){
  19. curRightBorderSum += nums[i];
  20. maxRightBorderSum = Math.max(curRightBorderSum, maxRightBorderSum);
  21. }
  22. //递归求左右部分最大值
  23. maxLeftSum = greatestSumSub(nums, left, mid);
  24. maxRightSum = greatestSumSub(nums, mid + 1, right);
  25. //返回三者中的最大值
  26. return maxOfThree(maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum);
  27. }
  28. private static int maxOfThree(int a, int b, int c){
  29. int max = a;
  30. if(b > max)
  31. max = b;
  32. if(c > max)
  33. max = c;
  34. return max;
  35. }

48. 从 1 到 n 整数中 1 出现的次数

题目描述:
  输入一个整数n,求1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11,12,1一共出现了5次。


思路一:不考虑时间效率的做法。循环查找1~n中每一个数的每一位。如果输入数字为n,n有O(logn)位,该算法的时间复杂度为O(n*logn)。

  1. public static int numOfOneFrom1ToN(int n){
  2. int number = 0;
  3. //检查不大于n的所有整数
  4. for(int i = 1; i <= n; i++)
  5. number += numOf1(i);
  6. return number;
  7. }
  8. private static int numOf1(int n){
  9. int number = 0;
  10. //检查数的每一位是不是为1
  11. while (n != 0) {
  12. if ((n % 10) == 1)
  13. number++;
  14. n /= 10;
  15. }
  16. return number;
  17. }

思路二:分析数字的规律。考虑将n的十进制的每一位单独拿出讨论,每一位的值记为weight。

  • 个位:从1到n,每增加1,weight就会加1,当weight加到9时,再加1又会回到0重新开始。那么weight从0-9的这种周期会出现多少次呢?这取决于n的高位是多少,看图:

    以534为例,在从1增长到n的过程中,534的个位从0-9变化了53次,记为round。每一轮变化中,1在个位出现一次,所以一共出现了53次。
    再来看weight的值。weight为4,大于0,说明第54轮变化是从0-4,1又出现了1次。我们记1出现的次数为count,所以:
    count = round+1 = 53 + 1 = 54
    如果此时weight为0(n=530),说明第54轮到0就停止了,那么:
    count = round = 53
  • 十位:对于10位来说,其0-9周期的出现次数与个位的统计方式是相同的,见图:

    不同点在于:从1到n,每增加10,十位的weight才会增加1,所以,如果十位出现1,就会连续出现10次,并且,只有当个位数从0到9变化10圈,即个位数加100次,十位才得到一次出现1的机会。所以,十位出现1的次数为(534 / 100 * 10),即(5 / 100 *10),也即round * 10。
    再来看weight的值。当此时weight为3,大于1,说明第6轮出现了10次1,则:
    count = round*10+10 = 5*10+10 = 60
    如果此时weight的值等于0(n=504),说明第6轮到0就停止了,所以:
    count = round*10+10 = 5*10 = 50
    如果此时weight的值等于1(n=514),那么第6轮中1出现了多少次呢?很明显,这与个位数的值有关,个位数为k,第6轮中1就出现了k+1次(可以把0~534的数字依次打印出来,比较容易找规律)。我们记个位数为former,则:
    count = round*10+former +1= 5*10+4 = 55
  • 更高位:更高位的计算方式其实与十位是一致的,不再阐述。
  • 总结:
    将n的各个位分为两类:个位与其它位。
    对个位来说:
    • 若个位大于0,1出现的次数为round*1+1
    • 若个位等于0,1出现的次数为round*1
      对其它位来说,记每一位的权值为base,位值为weight,该位之前的数是former,举例如图:

      则:
    • 若weight为0,则1出现次数为round*base
    • 若weight为1,则1出现次数为round*base+former+1
    • 若weight大于1,则1出现次数为rount*base+base
      参考:https://blog.csdn.net/yi_afly/article/details/52012593
  1. public static int numOfOneFrom1ToN(int n){
  2. if(n < 1)
  3. return 0;
  4. int count = 0;
  5. int base = 1;
  6. int round = n;
  7. while(round > 0){
  8. int weight = round % 10;
  9. round /= 10;
  10. count += round * base;
  11. if(weight == 1)
  12. count+=(n % base) + 1;
  13. else if(weight > 1)
  14. count += base;
  15. base *= 10;
  16. }
  17. return count;
  18. }

思路三:也是分析规律,不过代码更简洁。参考:https://www.cnblogs.com/xuanxufeng/p/6854105.html。后面细看。

  1. public int NumberOf1Between1AndN_Solution(int n) {
  2. int cnt = 0;
  3. for (int m = 1; m <= n; m *= 10) {
  4. int a = n / m, b = n % m;
  5. cnt += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0);
  6. }
  7. return cnt;
  8. }

49. 数字序列中的某一位数字

题目描述:
  数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从0开始计数)是5,第13位是1,第19位是4,等等。请写一个函数求任意位对应的数字。


思路:举例分析,比如找第1001位数字,

  • 1位数的数值有10个:0~9,数字为10×1=10个,显然1001>10,跳过这10个数值,在后面找第991(1001-10)位数字。
  • 2位数的数值有90个:10~99,数字为90×2=180个,且991>180,继续在后面找第811(991-180)位数字。
  • 3位数的数值有900个:100~999,数字为900×3=2700个,且811<2700,说明第811位数字在位数为3的数值中。由于811=270×3+1,所以第811位是从100开始的第270个数值即370的第二个数字,就是7。
    按此规律,可求出其他数字。时间复杂度为O(logn)。
  1. private static int digitAtIndex(int index)
  2. {
  3. if (index < 0) return -1;
  4. int digits = 1;
  5. while (true)
  6. {
  7. int digitNumbers = countOfNumbersFor(digits); //当前位数的数值个数
  8. //数值乘上它的位数等于数字个数,
  9. //比如,两位数有90个(10~99),每个数值有2个数字,总数字个数为180
  10. int countOfNumbers = digitNumbers * digits;
  11. if (index < countOfNumbers)
  12. {
  13. return digitAtIndex(index, digits);
  14. } else
  15. {
  16. //在下一位中查找
  17. index -= countOfNumbers;
  18. digits++;
  19. }
  20. }
  21. }
  22. //digits位数的数字个数,
  23. //两位数有9*10=90个(10~99),三位数有9*100=900个(100~999)
  24. private static int countOfNumbersFor(int digits)
  25. {
  26. if (digits == 1)
  27. return 10;
  28. int count = (int) Math.pow(10, digits - 1);
  29. return 9 * count;
  30. }
  31. private static int digitAtIndex(int index, int digits)
  32. {
  33. //对应的数值
  34. int number = beginNumberFor(digits) + index / digits;
  35. //从数值右边开始算的位置
  36. int indexFromRight = digits - index % digits;
  37. //去除右边的indexFromRight-1个数字
  38. for (int i = 1; i < indexFromRight; i++)
  39. number /= 10;
  40. //求个位数字
  41. return number % 10;
  42. }
  43. //digits位数的第一个数字,两位数从10开始,三位数从100开始
  44. private static int beginNumberFor(int digits) {
  45. if (digits == 1)
  46. return 0;
  47. return (int) Math.pow(10, digits - 1);
  48. }

50. 把数组排成最小的数

题目描述:
  输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。


思路一:最容易想到的方法就是求出数组中所有数的全排列,然后将每个排列拼接起来(需要注意的是,两个整数拼接的结果可能超过整数的范围,所以这里隐藏着一个大数问题),最后比较拼接后的数字大小。这显然不是最简单的方法,这里不再介绍。

思路二:可以联想到字符串的字典排,将数字问题转化成字符串问题,也是解决大数问题的一个好方法。

  1. public static String minNumber(int[] nums){
  2. if(nums == null || nums.length == 0)
  3. return "";
  4. String[] strs = new String[nums.length];
  5. for(int i = 0; i < nums.length; i++){
  6. strs[i] = String.valueOf(nums[i]);
  7. }
  8. //重写compareTo方法
  9. Arrays.sort(strs, new Comparator<String>() {
  10. @Override
  11. public int compare(String o1, String o2) {
  12. return (o1 + o2).compareTo(o2 + o1);
  13. }
  14. });
  15. StringBuilder sb = new StringBuilder();
  16. for(String str : strs)
  17. sb.append(str);
  18. return sb.toString();
  19. }

51. 把数字翻译成字符串

题目描述:
  给定一个数字,按照如下规则翻译成字符串:0翻译成“a”,1翻译成“b”...25翻译成“z”。一个数字有多种翻译可能,例如12258一共有5种,分别是bccfi,bwfi,bczi,mcfi,mzi。实现一个函数,用来计算一个数字有多少种不同的翻译方法。


思路:用递归自顶向下分析,用动态规划自低向上求解(因为递归的解法有很多重复计算的情况)。
自上而下,从最大的问题开始,递归 :

有很多子问题被多次计算,比如258被翻译成几种这个子问题就被计算了两次。
自下而上,动态规划,从最小的问题开始 :
f(r)表示以r为开始(r最小取0)到最右端所组成的数字能够翻译成字符串的种数。对于长度为n的数字,f(n)=0,f(n-1)=1,求f(0)。
递推公式为 f(r-2) = f(r-1)+g(r-2,r-1)*f(r);
其中,如果(r-2),(r-1)处的数字连接起来能够翻译成字符,则g(r-2,r-1)=1,否则为0。
因此,对于12258:
f(5) = 0
f(4) = 1
f(3) = f(4)+0 = 1
f(2) = f(3)+f(4) = 2
f(1) = f(2)+f(3) = 3
f(0) = f(1)+f(2) = 5

  1. public static int getTranslationCount(int number){
  2. if(number < 0)
  3. return 0;
  4. if(number == 1)
  5. return 1;
  6. return getTranslationCount(Integer.toString(number));
  7. }
  8. /**
  9. * 动态规划,从右到左计算。
  10. * f(r-2) = f(r-1)+g(r-2,r-1)*f(r);
  11. * 如果r-2,r-1能够翻译成字符,则g(r-2,r-1)=1,否则为0
  12. * @param number
  13. * @return
  14. */
  15. public static int getTranslationCount(String number) {
  16. int f1 = 0, f2 = 1,g = 0;
  17. int temp;
  18. for(int i = number.length()-2; i >= 0; i--){
  19. if(Integer.parseInt(number.charAt(i)+""+number.charAt(i+1)) < 26)
  20. g = 1;
  21. else
  22. g = 0;
  23. temp = f2;
  24. f2 = f2 + g * f1;
  25. f1 = temp;
  26. }
  27. return f2;
  28. }

52. 礼物的最大值

题目描述:
  在一个m*n的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘:

  1. 1 10 3 8
  2. 12 2 9 6
  3. 5 7 4 11
  4. 3 7 16 5

  礼物的最大价值为1+12+5+7+7+16+5=53。


思路一:图的广度优先遍历。这个棋盘其实可以看成一个有向图,起点为左上角,终点为右下角,每一点仅仅指向右侧和下侧。因此我们可以从左上角开始进行广度优先遍历。此外,遍历过程中可以进行剪枝,最终移动到右下角时会仅剩下一个枝,该路径所经的点的数值之和即为所求。

  1. //有向图的遍历(广度优先,可再剪枝进行优化)
  2. public static int getMaxVaule2(int[][] data){
  3. if(data == null || data.length == 0 || data[0].length == 0)
  4. return 0;
  5. int maxRowIndex = data.length - 1;
  6. int maxColIndex = data[0].length - 1;
  7. Queue<Node> queue = new LinkedList<>();
  8. queue.offer(new Node(0,0,data[0][0]));
  9. Node nodeCur = null;
  10. while (!(queue.peek().row == maxRowIndex && queue.peek().col == maxColIndex)){
  11. nodeCur = queue.poll();
  12. if(nodeCur.row != maxRowIndex)
  13. queue.offer(new Node(nodeCur.row + 1,nodeCur.col,nodeCur.sum + data[nodeCur.row + 1][nodeCur.col]));
  14. if(nodeCur.col!=maxColIndex)
  15. queue.offer(new Node(nodeCur.row,nodeCur.col + 1,nodeCur.sum+data[nodeCur.row][nodeCur.col + 1]));
  16. }
  17. int maxSum = 0,temp = 0;
  18. while (!queue.isEmpty()){
  19. temp = queue.poll().sum;
  20. if(temp > maxSum)
  21. maxSum = temp;
  22. }
  23. return maxSum;
  24. }
  25. public static class Node{
  26. int row;
  27. int col;
  28. int sum;
  29. public Node(int r,int c,int s){
  30. row = r;col = c;sum = s;
  31. }
  32. }

思路二:动态规划,使用二维数组进行辅助。
  定义f(i,j)表示到达坐标为(i,j)的格子时能拿到的礼物总和的最大值;有两种路径到达(i,j):(i-1,j)或者(i,j-1)f(i,j) = max(f(i-1,j), f(i,j-1)) + gift[i,j];使用循环来计算避免重复子问题。

  1. public int getMaxValue(int[][] arr) {
  2. if (arr == null || arr.length == 0) {
  3. return 0;
  4. }
  5. int rows = arr.length;
  6. int cols = arr[0].length;
  7. int[][] maxValue = new int[rows][cols];
  8. for (int i = 0; i < rows; i++) {
  9. for (int j = 0; j < cols; j++) {
  10. int left = 0;
  11. int up = 0;
  12. if(i>0){
  13. up = maxValue[i-1][j];
  14. }
  15. if(j>0){
  16. left = maxValue[i][j-1];
  17. }
  18. maxValue[i][j] = Math.max(up, left) + arr[i][j];
  19. }
  20. }
  21. return maxValue[rows-1][cols-1];
  22. }

思路三:动态规划,使用一维数组进行辅助。
  题目中可知,坐标(i,j)的最大礼物价值仅仅取决于坐标为(i-1,j)(i,j-1)两个格子;因此第i-2行以上的最大价值没有必要保存下来。使用一维数组保存:(0,0)…(0,j-1)保存的是(i,0)…(i,j-1)的最大价值;(0,j)…(0,cols-1)保存的是(i-1,j)…(i-1,cols-1)的最大价值。每次计算新的(i,j)时,使用数组下标j-1j的最大值加当前礼物值即可。

  1. public int getMaxValue(int[][] arr) {
  2. if (arr == null || arr.length == 0) {
  3. return 0;
  4. }
  5. int rows = arr.length;
  6. int cols = arr[0].length;
  7. int[][] maxValue = new int[rows][cols];
  8. for (int i = 0; i < rows; i++) {
  9. for (int j = 0; j < cols; j++) {
  10. int left = 0;
  11. int up = 0;
  12. if(i>0){
  13. up = maxValue[i-1][j];
  14. }
  15. if(j>0){
  16. left = maxValue[i][j-1];
  17. }
  18. maxValue[i][j] = Math.max(up, left) + arr[i][j];
  19. }
  20. }
  21. return maxValue[rows-1][cols-1];
  22. }

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

题目描述:
  请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含从’a’到’z’的字符。例如,在字符串中”arabcacfr”,最长非重复子字符串为”acfr”,长度为4。


思路一:使用额外空间ArrayList,如果字符第一次出现,就将其加入ArrayList中,如果ArrayList中已经包含该字符,找到该字符的位置,将该位置以及之前的字符全部丢弃。

  1. public static int longestSubstringWithoutDuplication(String str){
  2. if(str == null || str.length() == 0)
  3. return -1;
  4. int maxLength = 1;
  5. List<Character> list = new ArrayList<>();
  6. list.add(str.charAt(0));
  7. for(int i = 0; i < str.length(); i++){
  8. if(list.contains(str.charAt(i))){
  9. int index = list.indexOf(str.charAt(i));
  10. list = list.subList(index + 1, list.size());
  11. list.add(str.charAt(i));
  12. maxLength = Math.max(maxLength, list.size());
  13. }else {
  14. list.add(str.charAt(i));
  15. //maxLength++;
  16. maxLength = Math.max(maxLength, list.size());
  17. }
  18. }
  19. return maxLength;
  20. }

思路二:使用动态规划。记录当前字符之前的最长非重复子字符串长度f(i-1),其中i为当前字符的位置。每次遍历当前字符时,分两种情况:
1)若当前字符第一次出现,则最长非重复子字符串长度f(i) = f(i-1)+1。
2)若当前字符不是第一次出现,则首先计算当前字符与它上次出现位置之间的距离d。若d大于f(i-1),即说明前一个非重复子字符串中没有包含当前字符,则可以添加当前字符到前一个非重复子字符串中,所以,f(i) = f(i-1)+1。若d小于或等于f(i-1),即说明前一个非重复子字符串中已经包含当前字符,则不可以添加当前字符,所以,f(i) = d。

  1. public static int longestSubString(String str){
  2. if(str == null || str.length() == 0)
  3. return -1;
  4. int curLength = 0, maxLength = 0;
  5. int[] position = new int[26];
  6. //初始化为-1,负数表示没出现过
  7. Arrays.fill(position, -1);
  8. for(int i = 0; i < str.length(); i++){
  9. int c = str.charAt(i) - 'a';
  10. int preIndex = position[c];
  11. //当前字符第一次出现,或者前一个非重复子字符串中没有包含当前字符
  12. if(preIndex == -1 || i - preIndex > curLength){
  13. curLength++;
  14. }else {
  15. //更新最长非重复子字符串的长度
  16. if(maxLength < curLength)
  17. maxLength = curLength;
  18. curLength = i - preIndex;
  19. }
  20. position[c] = i; //更新字符出现的位置
  21. }
  22. maxLength = Math.max(maxLength, curLength);
  23. return maxLength;
  24. }

54. 丑数

题目描述:
  我们把只包含因子2,3,5的数称为丑数(Ugly Number)。求按从小到大的顺序的第1500个丑数。例如6,8都是丑数,但14不是,因为它含有因子7.习惯上我们把1当作第一个丑数。


思路一:逐个判断每个整数是不是丑数的解法,直观但不够高效:
所谓一个数m是另一个数n的因子,是指n能被m整除,也就是说n%m==0.根据丑数的定义,丑数只能被2,3,5整除。也就是说如果一个数能被2整除,我们把它连续除以2;如果能被3整除,就连续除以3;如果能被5整除,就除以5.如果最后我们得到的是1,那么这个数就是丑数,否则不是。

  1. private static boolean isUgly(int num){
  2. if(num <= 0)
  3. return false;
  4. while (num % 2 == 0)
  5. num /= 2;
  6. while (num % 3 == 0)
  7. num /= 3;
  8. while (num % 5 == 0)
  9. num /= 5;
  10. return (num == 1) ? true : false;
  11. }
  12. //输入1500,找到第1500个丑数
  13. public static int getUglyNumber(int num){
  14. if(num <= 0)
  15. return 0;
  16. int number = 0;
  17. int uglyFound = 0; //第几个丑数
  18. while(uglyFound < num){
  19. ++number;
  20. if(isUgly(number))
  21. ++uglyFound;
  22. }
  23. return number;
  24. }

思路二:用空间换时间。前面的算法之所以效率低,很大程度上是因为不管一个数是不是丑数我们对它都要作计算。接下来我们试着找到一种只要计算丑数的方法,而不在非丑数的整数上花费时间。根据丑数的定义,丑数应该是另一个丑数乘以2,3,5的结果。因此我们可以创建一个数组,里面的数字是排序好的丑数,每一个丑数都是前面的丑数乘以2,3,5得到的。
  这种思路的关键在于怎样确定数组里面的丑数是排序好的。假设数组中已经有若干个丑数排好后存放在数组中,并且把已有的最大的丑数记作M,我们接下来分析如何生成下一个丑数。该丑数肯定是前面某个丑数乘以2,3,5的结果。所以我们首先考虑把已有的每个丑数乘以2.在乘以2的时候,能得到若干个小于或等于M的结果。由于是按照顺序生成的,小于或者等于M肯定已经在数组中了,我们不需要再次考虑;还会得到若干个大于M的结果,但我们只需要第一个大于M的结果,因为我们希望丑数是指按从小到大的顺序生成的,其他更大的结果以后再说。我们把得到的第一个乘以2后大于M的结果即为M2.同样,我们把已有的每一个丑数乘以3,5,能得到第一个大于M的结果M3和M5.那么下一个丑数应该是M2,M3,M5。这3个数的最小者。
  前面分析的时候,提到把已有的每个丑数分别都乘以2,3,5.事实上这不是必须的,因为已有的丑数都是按顺序存放在数组中的。对乘以2而言,肯定存在某一个丑数T2,排在它之前的每一个丑数乘以2得到的结果都会小于已有的最大丑数,在它之后的每一个丑数乘以2得到的结果都会太大。我们只需记下这个丑数的位置,同时每次生成新的丑数的时候,去更新这个T2.对乘以3和5而言,也存在这同样的T3和T5。

  1. public static int getUglyNumber(int index){
  2. if(index <= 0)
  3. return 0;
  4. int[] uglynumbers = new int[index];
  5. uglynumbers[0] = 1;
  6. int nextUglyNumber = 1;
  7. int pMultiply2 = 0;
  8. int pMultiply3 = 0;
  9. int pMultiply5 = 0;
  10. while(nextUglyNumber < index){
  11. int minUglyNumber = minOfThree(uglynumbers[pMultiply2] * 2, uglynumbers[pMultiply3] * 3, uglynumbers[pMultiply5] * 5);
  12. uglynumbers[nextUglyNumber] = minUglyNumber;
  13. while(uglynumbers[pMultiply2] * 2 <= uglynumbers[nextUglyNumber])
  14. pMultiply2++;
  15. while(uglynumbers[pMultiply3] * 3 <= uglynumbers[nextUglyNumber])
  16. pMultiply3++;
  17. while(uglynumbers[pMultiply5] * 5 <= uglynumbers[nextUglyNumber])
  18. pMultiply5++;
  19. nextUglyNumber++;
  20. }
  21. return uglynumbers[index - 1];
  22. }
  23. private static int minOfThree(int a, int b, int c){
  24. int min = (a < b) ? a : b;
  25. min = (min < c) ? min : c;
  26. return min;
  27. }

55. 第一个值出现一次的字符

题目描述:
  在一个字符串中找到第一个只出现一次的字符,并返回该字符。


方法一:使用HashMap。需要注意的是,HashMap存入和输出元素的顺序可能并不相同,所以,在找第一个出现一次的字符时还得根据给定字符串的顺序依次查找。

  1. public static char firstNotRepeatChar(String str){
  2. if(str == null || str.length() == 0)
  3. return '\0';
  4. HashMap<Character, Integer> map = new HashMap<>();
  5. for(int i = 0; i < str.length(); i++){
  6. if(map.containsKey(str.charAt(i)))
  7. map.put(str.charAt(i), map.get(str.charAt(i)) + 1);
  8. else
  9. map.put(str.charAt(i), 1);
  10. }
  11. for(int i = 0; i < str.length(); i++){
  12. if(map.get(str.charAt(i)) == 1)
  13. return str.charAt(i);
  14. }
  15. return '\0'; //没找到,返回空字符
  16. }

思路二:考虑到要统计的字符范围有限,因此可以使用整型数组代替 HashMap。这种方法其实是使用了ASCII码的编码十进制值做数组的下标。ASCII码标准码有128个,十进制为0~127;后来扩展为256个,新加的128个字符十进制为128~255(也有说是-1~-128)。于是用é(后128个字符中的一个)验证了一下,没有出现数组越界的情况,说明后128个字符的十进制是整数,这种算法没有问题。

  1. public static char firstNotRepeatingChar(String str) {
  2. int[] cnts = new int[256];
  3. for (int i = 0; i < str.length(); i++)
  4. cnts[str.charAt(i)]++;
  5. for (int i = 0; i < str.length(); i++)
  6. if (cnts[str.charAt(i)] == 1)
  7. return str.charAt(i);
  8. return '\0';
  9. }

思路三:使用位集。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。

  1. public static char firstNotRepeatingChar(String str) {
  2. if(str == null || str.length() == 0)
  3. return '\0';
  4. BitSet bs1 = new BitSet(256);
  5. BitSet bs2 = new BitSet(256);
  6. for (char c : str.toCharArray()) {
  7. //如果两个位集都没有,就放到bs1中
  8. if (!bs1.get(c) && !bs2.get(c))
  9. bs1.set(c); // 0 0 -> 0 1
  10. //如果只有bs1中有,bs2中也会放一个。经过这两步,只出现一次的字符放在bs1中,出现多次的字符bs1和bs2中都有
  11. else if (bs1.get(c) && !bs2.get(c))
  12. bs2.set(c); // 0 1 -> 1 1
  13. }
  14. for (int i = 0; i < str.length(); i++) {
  15. char c = str.charAt(i);
  16. if (bs1.get(c) && !bs2.get(c)) // 0 1
  17. return c;
  18. }
  19. return '\0';
  20. }

56. 数组中的逆序对

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

题目描述:
  输入两个链表,找到它们第一个公共接节点。


思路一:使用栈。定义连个栈,分别存放两个链表的元素。然后同时出栈,寻找最后一个相同节点。

  1. public static ListNode findFirstCommonNode(ListNode head1, ListNode head2){
  2. if(head1 == null || head2 == null)
  3. return null;
  4. ListNode pHead1 = head1, pHead2 = head2;
  5. Stack<ListNode> stack1 = new Stack<>();
  6. Stack<ListNode> stack2 = new Stack<>();
  7. while(pHead1 != null && pHead2 != null){
  8. stack1.push(pHead1);
  9. stack2.push(pHead2);
  10. pHead1 = pHead1.next;
  11. pHead2 = pHead2.next;
  12. }
  13. while(pHead1 != null){
  14. stack1.push(pHead1);
  15. pHead1 = pHead1.next;
  16. }
  17. while(pHead2 != null){
  18. stack2.push(pHead2);
  19. pHead2 = pHead2.next;
  20. }
  21. ListNode commonNode = null;
  22. while(!stack1.isEmpty() && !stack2.isEmpty()){
  23. if(stack1.peek() == stack2.peek()){
  24. commonNode = stack1.pop();
  25. stack2.pop();
  26. }
  27. else
  28. return commonNode;
  29. }
  30. //两个栈都空,说明两个链表完全重合
  31. if(stack1.isEmpty() && stack2.isEmpty())
  32. return commonNode;
  33. return null;
  34. }

思路二:不使用栈,遍历两次链表。第一次遍历得到两条链表的长度即其差值,然后进行第二次遍历,长链表先遍历差值步,接着两个链表同时开始遍历,找到第一个相同节点即为所求。

  1. public static ListNode findFirstCommonNode(ListNode head1, ListNode head2){
  2. if(head1 == null || head2 == null)
  3. return null;
  4. int length1 = getListLength(head1);
  5. int length2 = getListLength(head2);
  6. int lengthDif = length1 > length2 ? (length1 - length2) : (length2 - length1);
  7. ListNode longList = null, shortList = null;
  8. if(length1 > length2){
  9. longList = head1;
  10. shortList = head2;
  11. }
  12. else {
  13. longList = head2;
  14. shortList = head1;
  15. }
  16. for(int i = 0; i < lengthDif; i++)
  17. longList = longList.next;
  18. while(longList != null && shortList != null && longList != shortList){
  19. longList = longList.next;
  20. shortList = shortList.next;
  21. }
  22. ListNode commondNode = longList;
  23. return commondNode;
  24. }
  25. private static int getListLength(ListNode head){
  26. if(head == null)
  27. return -1;
  28. ListNode pHead = head;
  29. int length = 0;
  30. while(pHead != null){
  31. length++;
  32. pHead = pHead.next;
  33. }
  34. return length;
  35. }

思路三:设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。
  当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。

  1. public static ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
  2. ListNode l1 = pHead1, l2 = pHead2;
  3. while (l1 != l2) {
  4. l1 = (l1 == null) ? pHead2 : l1.next;
  5. l2 = (l2 == null) ? pHead1 : l2.next;
  6. }
  7. return l1;
  8. }

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

题目描述:
  统计一个数字在排序数组中出现的次数,比如排序数组为{1,2,3,3,3,4,5},那么数字3出现的次数就是3。


思路一:二分查找。假设我们需要找的数字是k,那么就需要找到数组中的第一个k和最后一个k出现的位置。如何通过二分查找得到第一个k的位置呢?取数组中间的数字与k作比较,如果该数字比k大,那么k只能出现在前半部分,那么下一轮只能在前半部分找;如果该数字比k小,那么k只能出现在后半部分,那么下一轮只能在后半部分找;如果该数字等于k,需要判断这是不是第一个k,如果该数字的前一个数字不是k,那么该数字就是第一个k,否则需要在前半部分继续寻找第一个k;寻找最后一个k的方法与寻找第一个k的方法一样。

  1. private static int getFirstK(int[] nums, int k, int start, int end){
  2. if(start > end)
  3. return -1;
  4. int mid = (start + end) >> 1;
  5. int midValue = nums[mid];
  6. if(midValue == k){
  7. if((mid > 0 && nums[mid - 1] != k) || mid == 0)
  8. return mid;
  9. else
  10. end = mid - 1;
  11. }else if(midValue > k)
  12. end = mid - 1;
  13. else
  14. start = mid + 1;
  15. return getFirstK(nums, k, start, end);
  16. }
  17. private static int getLastK(int[] nums, int k, int start, int end){
  18. if(start > end)
  19. return -1;
  20. int mid = (start + end) >> 1;
  21. int midValue = nums[mid];
  22. if(midValue == k){
  23. if((mid < nums.length - 1 && nums[mid + 1] != k) || mid == nums.length - 1)
  24. return mid;
  25. else
  26. start = mid + 1;
  27. }else if(midValue < k)
  28. start = mid + 1;
  29. else
  30. end = mid - 1;
  31. return getLastK(nums, k, start, end);
  32. }
  33. public static int getNumberOfK(int[] nums, int k){
  34. if(nums == null || nums.length == 0)
  35. return -1;
  36. int number = 0;
  37. int first = getFirstK(nums, k, 0, nums.length - 1);
  38. int last = getLastK(nums, k, 0, nums.length - 1);
  39. if(first > -1 && last > -1)
  40. number = last - first + 1;
  41. return number;
  42. }

思路二:不难发现,思路一种有很多重复的代码,其实可以简化一下,合并起来。

  1. public static int getNumberOfK(int[] nums, int K) {
  2. int first = binarySearch(nums, K);
  3. int last = binarySearch(nums, K + 1); //能找到最右边的K的右边的元素
  4. //return (first == nums.length || first == -1 || nums[first] != K) ? 0 : last - first;
  5. int number = 0;
  6. if(first > -1 && last > -1)
  7. number = last - first;
  8. return number;
  9. }
  10. private static int binarySearch(int[] nums, int K) {
  11. if(nums == null || nums.length == 0)
  12. return -1;
  13. int l = 0, h = nums.length;
  14. while (l < h) {
  15. int m = l + (h - l) / 2;
  16. //最终能找到最左边的K;如果不存在K,会找到比K小的一个数
  17. if (nums[m] >= K)
  18. h = m;
  19. else
  20. l = m + 1;
  21. }
  22. return l;
  23. }

59. 二叉搜索树的第K个节点

题目描述:
  给定一棵二叉搜索树,请找出其中的第K大的节点。


思路:利用二叉搜索树中序遍历有序的特点。

  1. private TreeNode ret;
  2. private int cnt = 0;
  3. public TreeNode KthNode(TreeNode pRoot, int k) {
  4. inOrder(pRoot, k);
  5. return ret;
  6. }
  7. private void inOrder(TreeNode root, int k) {
  8. if (root == null || cnt >= k)
  9. return;
  10. inOrder(root.left, k);
  11. cnt++;
  12. if (cnt == k)
  13. ret = root;
  14. inOrder(root.right, k);
  15. }

60. 二叉树的深度

题目描述:
  输入一棵二叉树的根节点,求二叉树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。


思路:如果只有根节点,则深度为1;如果只有左子树,则深度为左子树的深度加一;如果只有右子树,则深度为右子树的深度加一;如果既有左子树又有右子树,则深度为两者深度较大者加一。递归的思想。

  1. public static int biTreeDeepth(BiTreeNode root){
  2. if(root == null)
  3. return -1;
  4. int nLeft = biTreeDeepth(root.getLeft());
  5. int nRight = biTreeDeepth(root.getRight());
  6. return (nLeft > nRight) ? (nLeft + 1) : (nRight + 1);
  7. }

61. 二叉树的深度

题目描述:
  输入一棵二叉树,判断该二叉树是否是平衡二叉树。


思路:采用后序遍历的思想,可以避免重复遍历的问题。在遍历某节点的左右子节点之后,可以根据它的左右子节点的深度判断它是不是平衡的,并得到当前节点的深度。当最后遍历到树的根节点的时候,也就判断了整棵二叉树是不是平衡二叉树。

  1. private boolean isBalanced = true;
  2. public boolean IsBalanced_Solution(TreeNode root) {
  3. height(root);
  4. return isBalanced;
  5. }
  6. private int height(TreeNode root) {
  7. if (root == null || !isBalanced)
  8. return 0;
  9. int left = height(root.left);
  10. int right = height(root.right);
  11. if (Math.abs(left - right) > 1)
  12. isBalanced = false;
  13. return 1 + Math.max(left, right);
  14. }

62. 数组中只出现一次的数字

题目描述:
  一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度为O(n),空间复杂度为O(1)


思路一:使用HashMap,但是空间复杂度不是O(1)。

  1. public static int[] findNumberAppendOnce(int[] nums){
  2. if(nums == null || nums.length == 0)
  3. return null;
  4. HashMap<Integer, Integer> map = new HashMap<>();
  5. int[] result = new int[2];
  6. for(int i = 0; i < nums.length; i++){
  7. if(map.containsKey(nums[i]))
  8. map.put(nums[i], map.get(nums[i]) + 1);
  9. else
  10. map.put(nums[i], 1);
  11. }
  12. int i = 0;
  13. for(Integer key : map.keySet()){
  14. if(map.get(key) == 1)
  15. result[i++] = key;
  16. if(i >= 2) //题目中说是有两个,但还是判断一下,避免有错误输出
  17. break;
  18. }
  19. return result;
  20. }

思路二:使用异或的性质。如果只有一个数字出现一次,要找出这个数字,那直接依次异或,因为两个相同的数字亦或结果为0,0与任何数字异或结果还是那个数字。
  现在出现两个,那么考虑分成两个子数组,但是要求成对的数字分在一起,那两个不同的数字分别在两个子数组里。

  • 所有数字依次异或,结果为那两个不同的数字异或的结果,并且结果一定非0,记为a
  • 从左至右找出a第一个bit位非0的index,以此为依据将原数组分为两个子数组(这样的分类标准可以达到上述的目的:1)相同的数字一定落在同一个子数组里 2)那两个不同的数字分布在不同的子数组里)
  • 分成两个子数组后,分别异或即可。
  1. public static int[] findNumberAppendOnce(int[] nums){
  2. if(nums == null || nums.length < 2)
  3. throw new IllegalArgumentException("nums size must bigger than 2");
  4. int resultExclusiveOR = 0; //数组中所有值异或一次的结果
  5. for(int i = 0; i < nums.length; i++)
  6. resultExclusiveOR ^= nums[i];
  7. int indexOf1 = findFirstBitIs1(resultExclusiveOR); //最右边的1的位数
  8. int num1 = 0, num2 = 0;
  9. for(int i = 0; i < nums.length; i++){
  10. if(isBit1(nums[i], indexOf1))
  11. num1 ^= nums[i];
  12. else
  13. num2 ^= nums[i];
  14. }
  15. return new int[] {num1, num2};
  16. }
  17. //找到整数num的二进制表示中最右边是1的位
  18. private static int findFirstBitIs1(int num){
  19. int indexBit = 0;
  20. //8 * Integer.SIZE是整型数转化为二进制后最多有多少位
  21. while((num & 0x01) == 0 && (indexBit < 8 * Integer.SIZE)){
  22. num >>= 1;
  23. indexBit++;
  24. }
  25. return indexBit;
  26. }
  27. //判断整数num的二进制表示中从右边起的indexBit位是不是1
  28. private static boolean isBit1(int num, int indexBit){
  29. num >>= indexBit;
  30. return (num & 0x01) == 1;
  31. }

思路三:异或的简写。diff &= -diff 得到 diff 最右侧不为 0 的位,也就是不存在重复的两个元素在位级表示上最右侧不同的那一位

  1. public void findNumsAppearOnce(int[] nums, int num1[], int num2[]) {
  2. int diff = 0;
  3. for (int num : nums)
  4. diff ^= num;
  5. diff &= -diff;
  6. for (int num : nums) {
  7. if ((num & diff) == 0)
  8. num1[0] ^= num;
  9. else
  10. num2[0] ^= num;
  11. }
  12. }

63. 和为 S 的两个数字

题目描述:
  输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。


思路:使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。

  • 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
  • 如果 sum > target,移动较大的元素,使 sum 变小一些;
  • 如果 sum < target,移动较小的元素,使 sum 变大一些。
  1. public static int[] findNumberWithSum(int[] nums, int sum){
  2. if(nums == null || nums.length < 2)
  3. throw new IllegalArgumentException("nums size is not correct");
  4. int head = 0, tail = nums.length - 1;
  5. while(head < tail){
  6. if(nums[head] + nums[tail] == sum)
  7. return new int[]{nums[head], nums[tail]};
  8. else if(nums[head] + nums[tail] < sum)
  9. head++;
  10. else
  11. tail--;
  12. }
  13. return null;
  14. }

63.1 和为 S 的连续正数序列

题目描述:输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列1-5,,4-6和7-8。


思路:

  • 延续题目“和为S的两个数字”的思想。设置一大一小两个指针。
  • 初始状态下,small指向1,big指向2。如果从small到big的和大于S,则从序列中去掉较小的值,也就是small向后移动一个位置。若从small到big的序列和小于S,则big向后移动一个位置,以便序列包含更多的数字。
  • 因为题目中要求最少是两个数字,所以small最大为(s+1)/2。
  1. public static void findContinuousSequence(int sum){
  2. if(sum < 3)
  3. return;
  4. int left = 1;
  5. int right = 2;
  6. int mid = (1 + sum) >> 1;
  7. int curSum = left + right;
  8. while(left < mid){
  9. if(sum == curSum)
  10. printSequence(left, right);
  11. while(curSum > sum && left < mid){
  12. curSum -= left;
  13. left++;
  14. if(curSum == sum)
  15. printSequence(left, right);
  16. }
  17. right++;
  18. curSum += right;
  19. }
  20. }
  21. private static void printSequence(int left, int right){
  22. for(int i = left; i <= right; i++)
  23. System.out.print(i+" ");
  24. System.out.println();
  25. }

64. 翻转单词顺序

  题目描述:输入一个英文句子,翻转句子中单词的顺序,但单词内字符串的顺序不变。例如输入字符串:“I am a student”,则输出“student a am I”。


思路:先翻转整个句子,然后翻转每个单词。按照题目要求,如果输入的字符串只包含一个单词,输出结果为该字符串本身。

  1. public static String reverseSequence(String str){
  2. if(str == null || str.length() == 0)
  3. return null;
  4. char[] chars = str.toCharArray();
  5. //翻转整个句子
  6. reverse(chars, 0, chars.length - 1);
  7. int i = 0, j = 0;
  8. while(j <= chars.length){
  9. if(j == str.length() || chars[j] == ' '){
  10. reverse(chars, i, j - 1);
  11. i = j + 1;
  12. }
  13. j++;
  14. }
  15. // return chars.toString();
  16. return new String(chars);
  17. }
  18. public static void reverse(char[] chars, int i, int j){
  19. if(chars == null || chars.length == 0)
  20. return;
  21. while(i < j){
  22. char ch = chars[i];
  23. chars[i] = chars[j];
  24. chars[j] = ch;
  25. i++;
  26. j--;
  27. }
  28. }

64.1 左旋转字符串

  题目描述:定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。如把字符串abcdef左旋转2位得到字符串cdefab。请实现字符串左旋转的函数。


思路:字符串的左右旋转其实就是将字符串循环左右移,前面在总结左移和右移的时候总结了方法:两次翻转一次合并。这里提出了另一种方法:三次翻转。

  1. //左旋转字符串,index表示左旋转的数量
  2. public static String leftRotateString(String str, int index){
  3. if(str == null || str.length() == 0)
  4. return null;
  5. if(index <= 0 || index > str.length())
  6. return str;
  7. char[] chars = str.toCharArray();
  8. //翻转字符串前index个字符
  9. reverse(chars, 0, index - 1);
  10. //翻转字符串后面的部分
  11. reverse(chars, index, str.length() - 1);
  12. //翻转整个字符串
  13. reverse(chars, 0, str.length() - 1);
  14. return String.valueOf(chars);
  15. }
  16. //右旋转字符串,index右旋转的数量
  17. public static String rightRotateString(String str, int index){
  18. if(str == null || str.length() == 0)
  19. return null;
  20. if(index <= 0 || index > str.length())
  21. return str;
  22. char[] chars = str.toCharArray();
  23. //翻转字符串后index个字符
  24. reverse(chars, str.length() - index, str.length() - 1);
  25. //翻转前面的字符
  26. reverse(chars, 0, str.length() - index - 1);
  27. //翻转整个字符串
  28. reverse(chars, 0, str.length() - 1);
  29. return String.valueOf(chars);
  30. }
  31. //字符串反转
  32. public static void reverse(char[] chars, int i, int j){
  33. if(chars == null || chars.length == 0)
  34. return;
  35. while(i < j){
  36. char ch = chars[i];
  37. chars[i] = chars[j];
  38. chars[j] = ch;
  39. i++;
  40. j--;
  41. }
  42. }

65. 滑动窗口的最大值

  题目描述:给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。
  例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,它们的最大值分别为{4,4,6,6,6,5}。


思路一:构建一个窗口w大小的最大堆,每次从堆中取出窗口的最大值,随着窗口往右滑动,需要将堆中不属于窗口的堆顶元素删除。
  时间复杂度:正常情况下,往堆中插入数据为O(lgw),如果数组有序,则为O(lgn),因为滑动过程中没有元素从堆中被删除,滑动n-w+1次,复杂度为O(nlgn)。

  1. public ArrayList<Integer> maxInWindows(int[] num, int size) {
  2. ArrayList<Integer> ret = new ArrayList<>();
  3. if (size > num.length || size < 1)
  4. return ret;
  5. PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1); /* 大顶堆 */
  6. for (int i = 0; i < size; i++)
  7. heap.add(num[i]);
  8. ret.add(heap.peek());
  9. for (int i = 1, j = i + size - 1; j < num.length; i++, j++) { /* 维护一个大小为 size 的大顶堆 */
  10. heap.remove(num[i - 1]);
  11. heap.add(num[j]);
  12. ret.add(heap.peek());
  13. }
  14. return ret;
  15. }

思路二:使用双向队列。最大堆解法在堆中保存有冗余的元素,比如原来堆中元素为[10 5 3],新的元素为11,则此时堆中会保存有[11 5 3]。其实此时可以清空整个队列,然后再将11加入到队列即可,即只在队列中保持[11]。使用双向队列可以满足要求,滑动窗口的最大值总是保存在队列首部,队列里面的数据总是从大到小排列。当遇到比当前滑动窗口最大值更大的值时,则将队列清空,并将新的最大值插入到队列中。如果遇到的值比当前最大值小,则直接插入到队列尾部。每次移动的时候需要判断当前的最大值是否在有效范围,如果不在,则需要将其从队列中删除。由于每个元素最多进队和出队各一次,因此该算法时间复杂度为O(N)。

  1. public ArrayList<Integer> maxInWindows(int [] num, int size) {
  2. ArrayList<Integer> arr = new ArrayList<>();
  3. if (num == null)
  4. return arr;
  5. if (num.length < size || size <= 0)
  6. return arr;
  7. Deque<Integer> queue = new LinkedList<>();
  8. for (int i=0; i<num.length; i++){
  9. while (!queue.isEmpty() && num[i] >= num[queue.getLast()])
  10. queue.pollLast();
  11. while (!queue.isEmpty() && queue.getFirst() < i - (size-1))
  12. queue.pollFirst();
  13. queue.offerLast(i);
  14. if (i + 1 >= size)
  15. arr.add(num[queue.getFirst()]);
  16. }
  17. return arr;
  18. }

66. n 个骰子的点数

  题目描述:把n个骰子扔到地上,所有骰子朝上一面的点数之后为s. 输入n,打印出s所有可能的值出现的概率。(每个骰子6个面,点数从1到6)


思路一:使用递归。递归的思想一般是分而治之,把n个骰子分为第一个和剩下的n-1个。先计算第一个骰子每个点数出现的次数,再计算剩余n-1个骰子出现的点数之和。求n-1个骰子的点数之的方法和前面讲的一样,即再次把n-1个骰子分成两堆------第一个和剩下的n-2个。n个骰子,每个骰子6个面,总共有6n个组合。这6n个组合之中肯定有重复的,我们知道其范围是n~6n,对于每种情况我们可以用缓存机制记录下来,每当其发生一次我们令其对应的单元加1。
  我们定义一个长度为6n-n+1的数组,和为s的点数出现的次数保存到数组第s-n个元素里。为什么是6n-n+1呢?因为n个骰子的和最少是n,最大是6n,介于这两者之间的每一个情况都可能会发生,总共6n-n+1种情况。
  这种方法思路非常简洁,但是递归实现会存在子问题重复求解的情况发生,所以当number很大的时候,其性能会慢的让人不能接受。

  1. private static final int gMaxValue = 6;
  2. //number表示骰子的数量
  3. public static void printProbability(int number){
  4. if(number < 1)
  5. return;
  6. int maxSum = number * gMaxValue; //number个骰子点数的最大值
  7. int[] pProbabilities = new int[maxSum - number + 1];
  8. Arrays.fill(pProbabilities, 0);
  9. double total = Math.pow(gMaxValue, number);
  10. probability(number, pProbabilities); //这个函数计算n~6n每种情况出现的次数
  11. for(int i = number; i <= maxSum; i++){
  12. double ratio = pProbabilities[i - number] / total;
  13. System.out.println("sum: "+ i + ", ratio: "+ ratio);
  14. }
  15. }
  16. private static void probability(int number, int[] pProbabilities){
  17. for(int i = 1; i <= gMaxValue; i++) //从第一个骰子开始
  18. probability(number, number, i, pProbabilities);
  19. }
  20. //总共original个骰子,当前第 current个骰子,当前的和,贯穿始终的数组
  21. private static void probability(int original, int current, int sum, int[] pProbabilities){
  22. if(current == 1)
  23. pProbabilities[sum - original]++;
  24. else{
  25. for(int i = 1; i <= gMaxValue; i++)
  26. probability(original, current - 1, sum + i, pProbabilities);
  27. }
  28. }

思路二:使用循环。可以考虑用两个数组来存储骰子点数的每一个总数出现的次数。在一次循环中,每一个数组中的第n个数字表示骰子和为n出现的次数。在下一轮循环中,我们加上一个新的骰子,此时和为n的骰子出现的次数应该等于上一次循环中骰子点数和为n-1,n-2,n-3,n-4,n-5的次数之和,所以我们把另一个数组的第n个数字设为前一个数组对应的第n-1,n-2,n-3,n-4,n-5。

  1. private static final int gMaxValue = 6;
  2. public static void printProbability_1(int number){
  3. if(number<1)
  4. return;
  5. int[][] pProbabilities = new int[2][gMaxValue*number +1];
  6. for(int i = 0;i < gMaxValue; i++){//初始化数组
  7. pProbabilities[0][i] = 0;
  8. pProbabilities[1][i] = 0;
  9. }
  10. int flag = 0;
  11. for(int i = 1;i <= gMaxValue; i++){//当第一次抛掷骰子时,有6种可能,每种可能出现一次
  12. pProbabilities[flag][i] = 1;
  13. }
  14. //从第二次开始掷骰子,假设第一个数组中的第n个数字表示骰子和为n出现的次数,
  15. //在下一循环中,我们加上一个新骰子,此时和为n的骰子出现次数应该等于上一次循环中骰子点数和为n-1,n-2,n-3,n-4,n-5,
  16. //n-6的次数总和,所以我们把另一个数组的第n个数字设为前一个数组对应的n-1,n-2,n-3,n-4,n-5,n-6之和
  17. for(int k = 2; k <= number; k++){
  18. for(int i = 0; i < k; i++){//第k次掷骰子,和最小为k,小于k的情况是不可能发生的!所以另不可能发生的次数设置为0!
  19. pProbabilities[1 - flag][i] = 0;
  20. }
  21. for(int i = k;i <= gMaxValue * k; i++){//第k次掷骰子,和最小为k,最大为g_maxValue*k
  22. pProbabilities[1 - flag][i] = 0;//初始化,因为这个数组要重复使用,上一次的值要清0
  23. for(int j = 1;j <= i && j <= gMaxValue; j++){
  24. pProbabilities[1 - flag][i] += pProbabilities[flag][i - j];
  25. }
  26. }
  27. flag = 1 - flag;
  28. }
  29. double total = Math.pow(gMaxValue, number);
  30. for(int i=number; i <= gMaxValue * number; i++){
  31. double ratio = pProbabilities[flag][i] / total;
  32. System.out.println("sum: "+i+", ratio: "+ ratio);
  33. }
  34. }

67. 扑克牌的顺子

  题目描述:从扑克牌中随机抽出5张牌,判断是不是一个顺子,即这五张牌是不是连续的。2——10为数字本身,A为1,J为11,Q为12,K为13,而大小王为任意数字。


思路:因为大小王为特殊数字,不妨把它们统一看成0。首先把数组进行排序,再统计数组中0的个数,最后统计排序后的数组中相邻数字之间的空缺总数。如果空缺的总数小于或者等于0的个数,那么这个数组就是连续的,否则就是不连续的。

  1. public static boolean isContinuous(int[] nums){
  2. if(nums == null || nums.length < 1)
  3. return false;
  4. Arrays.sort(nums);
  5. int numberOfZero = 0;
  6. int numberOfGap = 0;
  7. //统计0的个数
  8. for(int i = 0; i < nums.length && nums[i] == 0; i++)
  9. numberOfZero++;
  10. //统计间隔的个数
  11. int left = numberOfZero; //跳过前面的0
  12. int right = left + 1;
  13. while(right < nums.length){
  14. if(nums[left] == nums[right])
  15. return false;
  16. numberOfGap += nums[right] - nums[left] - 1;
  17. left = right;
  18. right++;
  19. }
  20. return (numberOfGap > numberOfZero) ? false : true;
  21. }

68. 圆圈中最后剩下的数(有待测试)

  题目描述:让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0...m-1 报数 .... 这样下去 .... 直到剩下最后一个小朋友,可以不用表演。


思路一:使用模拟链表。

  1. public static void lastRemaining(int n, int m){
  2. if(n < 1 || m < 1)
  3. return;
  4. List<Integer> list = new ArrayList<>();
  5. for(int i = 0; i < n; i++)
  6. list.add(i);
  7. int k = 0;
  8. while(list.size() > 1){
  9. k = k + m;
  10. k = k % (list.size()) - 1;
  11. if(k < 0){
  12. System.out.print(list.get(list.size() - 1)+" ");
  13. list.remove(list.size() - 1);
  14. k = 0;
  15. }else {
  16. System.out.print(list.get(k)+" " +
  17. "");
  18. list.remove(list.get(k));
  19. }
  20. }
  21. }

思路二:分析法

  1. //https://blog.csdn.net/abc7845129630/article/details/52823135
  2. public static int lastRemaining_Solution(int n, int m) {
  3. if(n <= 0)
  4. return -1;
  5. int res = 0;
  6. for(int i = 2; i <= n; i++){
  7. res = (res + m) % i;
  8. // System.out.print(res + " ");
  9. }
  10. return res;
  11. }

69. 股票的最大利润

  题目描述:假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖交易该股票可能获得的利润是多少?例如一只股票在某些时间节点的价格为{9, 11, 8, 5,7, 12, 16, 14}。如果我们能在价格为5的时候买入并在价格为16时卖出,则能收获最大的利润11。


思路:使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。

  1. public int maxProfit(int[] prices) {
  2. if (prices == null || prices.length == 0)
  3. return 0;
  4. int soFarMin = prices[0];
  5. int maxProfit = 0;
  6. for (int i = 1; i < prices.length; i++) {
  7. soFarMin = Math.min(soFarMin, prices[i]);
  8. maxProfit = Math.max(maxProfit, prices[i] - soFarMin);
  9. }
  10. return maxProfit;
  11. }

补充:多次买入和卖出详见LeetCode笔记。

70. 求1+2+3+...+n

  题目描述:求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。


思路一:利用逻辑与的短路特性实现递归终止。当n==0时,(n>0)&&((sum+=sumSolution(n-1))>0)只执行前面的判断,为false,然后直接返回0;当n>0时,执行sum+=sumSolution(n-1),实现递归计算Sum_Solution(n)。

  1. /**
  2. * 使用逻辑与的短路特性
  3. */
  4. public static int sumSolution(int num){
  5. int sum = num;
  6. boolean b = (num > 0) && ((sum += sumSolution(num - 1)) > 0);
  7. return sum;
  8. }

思路二:利用异常退出递归。利用“除数不能为0”的机制,当num=0的时候,i = 1 / num的除数会变为0,就会抛出异常。

  1. /**
  2. * 使用异常机制退出递归
  3. */
  4. public static int sumSolution_1(int num){
  5. try {
  6. int i = 1 / num;
  7. return num + sumSolution_1(num - 1);
  8. }catch (Exception e){
  9. return 0;
  10. }
  11. }

71. 不用加减乘除做加法

&emtp; 题目描述:写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。


思路:
  首先看十进制是如何做的: 5+7=12,三步走:

  • 第一步:相加各位的值,不算进位,得到2。
  • 第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。
  • 第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。
      同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111
  • 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。
  • 第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。
  • 第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。
      继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。
  1. /**
  2. * 使用循环
  3. */
  4. public static int addTowNumber(int a, int b){
  5. int sum = 0;
  6. int carry = 0;
  7. do{
  8. sum = a ^ b;
  9. carry = (a & b) << 1;
  10. a = sum;
  11. b = carry;
  12. }while (b != 0);
  13. return a;
  14. }
  15. /**
  16. * 使用递归。递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。
  17. */
  18. public static int addTowNumbers_1(int a, int b) {
  19. return b == 0 ? a : addTowNumbers_1(a ^ b, (a & b) << 1);
  20. }

72. 构建乘积数组(待测试)

  题目描述:给定一个数组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]。不能使用除法。


思路一:矩阵法。

  1. public int[] multiply(int[] A) {
  2. if(A == null || A.length < 2)
  3. return null;
  4. int length = A.length;
  5. int[] B = new int[length];
  6. B[0] = 1;
  7. //计算左三角
  8. for(int i = 1; i < length; i++){
  9. B[i] = B[i - 1] * A[i - 1];
  10. }
  11. //计算右三角 temp用来记录有三角每一行的值
  12. int temp = 1;
  13. for(int i = length - 2; i >= 0; i--){
  14. temp *= A[i + 1];
  15. B[i] *= temp;
  16. }
  17. return B;
  18. }
  19. //https://blog.csdn.net/zjkc050818/article/details/72800856

思路二:两次连乘法。

  1. public int[] multiply(int[] A) {
  2. int n = A.length;
  3. int[] B = new int[n];
  4. for (int i = 0, product = 1; i < n; product *= A[i], i++) /* 从左往右累乘 */
  5. B[i] = product;
  6. for (int i = n - 1, product = 1; i >= 0; product *= A[i], i--) /* 从右往左累乘 */
  7. B[i] *= product;
  8. return B;
  9. }

73. 将字符串转换为整数(待验证)

  题目描述:将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 0,要求不能使用字符串转换整数的库函数。


思路:可能的输入:

  • 带符号数
  • 无符号数
  • 空指针
  • 超出表示范围 – 暂时仅仅是直接退出且设置最小 – 可以考虑此时抛个异常
  • 非法输入,比如并不是一个0-9或者+ -组成的字符串
  1. public static int strToInt(String str){
  2. if (str == null || str.length() == 0)
  3. return 0;
  4. boolean isNegative = str.charAt(0) == '-';
  5. int ret = 0;
  6. for (int i = 0; i < str.length(); i++) {
  7. char c = str.charAt(i);
  8. if (i == 0 && (c == '+' || c == '-')) /* 符号判定 */
  9. continue;
  10. if (c < '0' || c > '9') /* 非法输入 */
  11. return 0;
  12. ret = ret * 10 + (c - '0');
  13. }
  14. return isNegative ? -ret : ret;
  15. }

74. 树中两个节点的最低公共祖先(待验证)

  题目描述:输入两个树节点,求它们的最低公共祖先。


思路:二叉查找树中,两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val。

普通二叉树中,在左右子树中查找是否存在 p 或者 q,如果 p 和 q 分别在两个子树中,那么就说明根节点就是最低公共祖先。

  1. /**
  2. * 二叉查找树
  3. */
  4. public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
  5. if (root == null)
  6. return root;
  7. if (root.val > p.val && root.val > q.val)
  8. return lowestCommonAncestor(root.left, p, q);
  9. if (root.val < p.val && root.val < q.val)
  10. return lowestCommonAncestor(root.right, p, q);
  11. return root;
  12. }
  13. /**
  14. * 普通二叉树
  15. */
  16. public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
  17. if (root == null || root == p || root == q)
  18. return root;
  19. TreeNode left = lowestCommonAncestor(root.left, p, q);
  20. TreeNode right = lowestCommonAncestor(root.right, p, q);
  21. return left == null ? right : right == null ? left : root;
  22. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注