牛客剑指offer:题解(31-40)

欢迎指正

题解(01-10):link

题解(11-20):link

题解(21-30):link

题解(31-40):link

题解(41-50):link

题解(51-60): link

题解(61-67): link


31.整数中1出现的次数

1. 解法思路:

我们将 f(n) 记为 1~n 这 n 个整数的十进制中 1 出现的次数,将 n 拆分成两个部分,最高一位的数字 high 和剩下位的数字 last,分别判断情况后将结果累加。

举两个栗子(也是两大种情况):

  1. n = 1234 ——> high = 1, pow = 1000, last = 234

我们将数字 1234 分成两个部分1 ~ 9991000 ~ 1234

  • 1 ~ 999 这个范围的 1 的个数为 f(pow - 1)
  • 1000 ~ 1234 这个范围的 1 的个数又要分成两个部分
    • 千位为 1 的个数(只看千位):这个个数就刚好是 234 + 1(last + 1)
    • 其他位(除去千位):即 2341 出现的次数,即 f(234)->f(last)
  • 上面两部分加起来 就是:f(pow - 1) + (last + 1) + f(last)
  1. n = 3234,high = 3, pow = 1000, last = 234

我们将数字 3234 分成后面几种部分1 ~ 9991000 ~ 19992000 ~ 29993000 ~ 3234

  • 1 ~ 999 这个范围的 1 的个数为 f(pow - 1)
  • 1000 ~ 1999 这个范围 1 的个数需要分为两部分
    • 千位为 1 的个数(只看千位):刚好就是 pow
    • 其他位是 1 的个数:即 999 中出现的 1 的个数,就是f(pow - 1)
  • 2000 ~ 2999 出现的 1 的个数就是 f(pow - 1),因为最高位已经不是 1
  • 3000 ~ 3234 出现的 1 的个数就是 f(last),同样因为最高位已经不是 1
  • 上面几种情况加起来: pow + high * f(pow - 1) + f(last)。因为这个 high 实际根据具体数字来获取
class Solution {
    public int countDigitOne(int n) {
        return f(n);
    }

    private int f(int n) {
        if (n <= 0) return 0;
        // 把 n 转换成字符串,方便取
        String s = String.valueOf(n);
        // 获取最高位的数字
        int high = s.charAt(0) - '0';
        // 1299 -> pow = 1000 
        int pow = (int)Math.pow(10, s.length() - 1);
        // last = 1299 - 1 * 1000 = 299
        int last = n - high * pow;
        // 如果高位为1,
        if (high == 1) {
            return f(pow - 1) + last + 1 + f(last);
        } else {
            return pow + f(pow - 1) * high + f(last);
        }
    }
}

32.把数组排成最小的数

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

1.解法一:找排序规则

  1. 对于两个数m,n,组合的方式就两种, mn或者 nm,在这两个中找出最小的
  2. 由此,这道题希望我们能够找出一种排序规则,将元素从小到大排列好然后连接起来构成最终的最小数
  3. 两个数字 m 和 n 能拼接成mnnm,如果mn < nm,那 m 应该在前;如果nm < mn,那么 n 应该在前。因此,我们得到的排序规则如下:
    1. 如果 mn > nm ,则 m > n
    2. 如果 mn < nm ,则 n > m
    3. 如果 mn = nm ,则 n = m
  4. 根据上述规则,因为需要拼接起来,所以我们需要先将数字转换成字符串再做比较。比较完之后按顺序连接起来即可。
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        String res = "";
        if (numbers == null || numbers.length == 0) return res;
        int N = numbers.length;
        String[] str = new String[N];
        // 将数字数字转化为字符串数据,方便后续操作
        for (int i = 0;i < N;i ++)
            str[i] = String.valueOf(numbers[i]);
        // 按规则将str按照从小到大排序。比较规则就是 mn > nm 是否成立
        Arrays.sort(str, new Comparator<String>() {
            @Override
            public int compare(String m, String n) {
                String s1 = m + n, s2 = n + m;
                return s1.compareTo(s2);
            }
        });
        // 下面这句使用lambda表达式
        // Arrays.sort(str, (m, n) -> ( (m + n).compareTo(n + m)));
        for (String s : str)
            res += s;
        return res;
    }
}

33.丑数

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

1.解法一

后面的丑数是由前面的丑数乘以 2、3、5 来获得的

参照牛客网的解答

import java.util.ArrayList;
public class Solution {
    public int GetUglyNumber_Solution(int index) {
        // 1-6 都是丑数
        if (index < 7) return index;
        ArrayList<Integer> list = new ArrayList<>();
        // 第一个丑数是1
        list.add(1);
        // p1,p2,p3分别记录乘以2,3,5的索引
        int p1 = 0, p2 = 0, p3 = 0;
        // ArrayList 可以根据下标随机访问
        while (list.size() < index) {
            int m1 = list.get(p1) * 2;
            int m2 = list.get(p2) * 3;
            int m3 = list.get(p3) * 5;
            // 要排序,所以每次选最小的出来加入结果队列
            int min = Math.min(m1, Math.min(m2, m3));
            list.add(min);
            if (min == m1) p1 ++;
            if (min == m2) p2 ++;
            if (min == m3) p3 ++;          
        }
        return list.get(index - 1);
    }
}

2.解法二:动态规划

还是使用一个额外的数组来存贮丑数

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if (index < 7) return index;
        int[] res = new int[index];
        // 第一个丑数是1
        res[0] = 1;
        // p1,p2,p3分别记录乘以2,3,5的索引
        int p1 = 0, p2 = 0, p3 = 0;
        // 因为 res 需要是有序的,所以每次只取乘以2或3或5中值的最小值
        for (int i = 1;i < index;i ++) {
            // 下一个丑数是由上一个丑数乘以2或3或5得来的,且需要有序,我们就取最小值
            res[i] = Math.min(res[p1] * 2, Math.min(res[p2] * 3, res[p3] * 5));
            if (res[i] == res[p1] * 2) p1 ++;
            if (res[i] == res[p2] * 3) p2 ++;
            if (res[i] == res[p3] * 5) p3 ++;
        }
        return res[index - 1];
    }
}

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

题目描述: 在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)

1.解法一:哈希表存储

  1. 遍历一遍这个字符数组,要是这个元素没有出现过,那么将这个元素和他所在的位置的映射存起来
  2. 如果已经存过这个元素,那么将映射关系的值改为 -1,标志这个元素是重复元素
  3. 最后遍历一遍 map 的数值集values,在大于等于 0 的结果中找到最小值,即为答案
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        if (str.length() == 0) return -1;
        HashMap<Character,Integer> map = new HashMap<>();
        char[] arr = str.toCharArray();
        // 遍历字符数组,存  char -> index 的映射关系
        for (int i = 0;i < arr.length;i ++) {
            if (!map.containsKey(arr[i]))
                map.put(arr[i], i);
            else
                map.put(arr[i], -1);
        }
        int min = Integer.MAX_VALUE;
        // 找到最小的结果
        for (int value : map.values()) {
            if (value >= 0)
                min = Math.min(min, value);
        }
        return min;
    }
}

2.解法二:使用数组存储

  1. 第一次遍历字符串,如果字符出现一次就将对应位置元素 ++
  2. 第二次遍历字符串,如果字符对应的下标存的值 ==1,说明这个元素只出现过一次,那么结束循环,返回这个位置即可
  3. 至于为什么数组长度为 58 :因为 A-Z(65-90),a-z(97-122) 中间有 6 个元素的其他字符,所以干脆将这几个字符位置算上,否则要分别判断是大写还是小写
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        int[] alpha = new int[58]; // 中间包括大写字母Z(90) - 小写字母a(97) 之间的其他字符
        for (int i = 0;i < str.length();i ++) {
            alpha[(int)str.charAt(i) - 'A'] ++;
        }
        for (int i = 0;i < str.length();i ++) {
            if (alpha[(int)str.charAt(i) - 'A'] == 1) return i;
        }
        return -1;
    }
}

35.数组中的逆序对

描述见链接

1.解法一:使用归并的思想

  1. 注意 res 的取值可能超过 int 范围,所以使用 long 来存储
public class Solution {
    // 结果可能超出 int 的范围
    private long res;
    private int[] aux;
    public int InversePairs(int [] array) {
        // 使用归并的思想
        if (array == null || array.length <= 1) return -1;
        aux = new int[array.length];
        mergeHelper(array, 0, array.length - 1);
        return (int)(res % 1000000007);
    }
    private void mergeHelper(int[] arr, int left, int right) {
        if (left >= right) return;
        int mid = left + (right - left) / 2;
        mergeHelper(arr, left, mid);
        mergeHelper(arr, mid + 1, right);
        // 这个地方算是一个优化,如果前半部分的最大值已经小于后半部分的最小值了,
        // 那么就没有必要再进行下面的操作,因为数组已经有序了
        if (arr[mid] > arr[mid + 1])
        	merge(arr, left, mid, right);
    }
    // 一边归并,一边记录逆序对的数量。几乎和归并排序一样,只是输出的是res
    private void merge(int[] arr, int left, int mid, int right) {
        // 还是需要辅助数组来帮忙
        for (int i = left;i <= right;i ++)
            aux[i - left] = arr[i];
        int i = left, j = mid + 1;
        for (int k = left;k <= right;k ++) {
            if (i > mid) {
                arr[k] = aux[j++ - left];
            } else if (j > right) {
                arr[k] = aux[i++ - left];
            } else if (aux[i - left] <= aux[j - left]) {
                arr[k] = aux[i++ - left];
            } else {
                // 当aux[i - left] > aux[j - left]时,前半部分i后面的元素都大于j对应的元素
                res += (mid - i + 1);
                arr[k] = aux[j++ - left];
            }
        }
    }
}

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

题目描述: 输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

1.解法一:利用两条链表长度加起来相等

  1. l1.size() + l2.size() = l2.size() + l1.size()
  2. 第一条链表要是遍历完了,就跑到第二条链表的头部重新开始
  3. 第二条链表要是遍历完了,就跑到第一条链表的头部重新开始
  4. 注意! :因为链表有公共节点的情况下,接一次对方就可以找到公共链表,如果没有公共节点,接多少次都找不到,为了避免死循环,我们引入一个计数器,l1 接到l2 时,计数器加一,反之也加一,一共就两次,如果超出两次,说明两个链表没有公共节点。
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        // 两条链表的长度加起来是相等的
        if (pHead1 == null || pHead2 == null) return null;
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        int count = 0;
        while (p1 != p2 && count <= 2) {
            p1 = p1.next;
            p2 = p2.next;
            if (p1 == null) {
                // 接到对方链表上去。计数器加一
                p1 = pHead2;
                count ++;
            }
            if (p2 == null) {
                // 接到对方链表上去,计数器加一
                p2 = pHead1;
                count ++;
            }
        }
        // p1 != p2 是由于 count 超过 2 循环结束导致的
        return p1 == p2 ? p1 : null;
    }
}

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

题目描述: 统计一个数字在排序数组中出现的次数

1.解法一:直接遍历计算

public class Solution {
    public int GetNumberOfK(int [] array , int k) {
        // 边界条件
        if (array == null || array.length == 0) return 0;
        if (k < array[0] || k > array[array.length - 1]) return 0;
        int count = 0;
        for (int i = 0;i < array.length;i ++) {
            if (k > array[i]) continue;
            // 相等之后再不相等就可以退出循环了
            else if (k == array[i]) {
                while (i < array.length && k == array[i]) {
                    count ++;
                    i ++;
                }
                break;
            }
        }
        return count;
    }
}

2.解法二:题意显然不是让我们直接去遍历,使用二分查找可以提高效率到 O(log n)

public class Solution {
    // 设计成全局变量,便于累加
    private int res = 0;
    public int GetNumberOfK(int [] array , int k) {
        if (array == null || array.length == 0) return 0;
        helper(array, k, 0, array.length - 1);
        return res;
    }
    private void helper(int[] array, int k, int l, int r) {
        if (l > r) return;
        int mid = l + (r - l) / 2;
        if (array[mid] < k) {
     		// k 在 mid 右边,去右边递归查找。左边同理
            helper(array, k, mid + 1, r);
        } else if (array[mid] > k) {
            helper(array, k, l, mid - 1);
        } else {
            // 在相等的情况下左边可能还有值,右边也可能还有值,所以继续递归调用
            res ++;
            helper(array, k, l, mid - 1);
            helper(array, k, mid + 1, r);
        }
    }
}

38.二叉树的深度

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

1.解法一:递归

深度 = max(左孩子深度,右孩子深度) + 1

public class Solution {
    public int TreeDepth(TreeNode root) {
        // 很显然想使用递归,深度 = max(左孩子深度,右孩子深度) + 1
        if (root == null) return 0;
        // 递归终止条件,只有一个根节点,所以返回1
        if (root.left == null && root.right == null) return 1;
        int leftH = 0, rightH = 0;
        // 左孩子深度
        // if (root.left != null)  可以省略,因为函数开头保证了根节点为空返回0
        leftH = TreeDepth(root.left);
        // 右孩子深度
        // if (root.right != null)
        rightH = TreeDepth(root.right);
        return Math.max(leftH, rightH) + 1;
    }
}

39.平衡二叉树

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树

1.解法一

public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        // 任意一个节点左右两个子树的高度差不超过1
        // 一边遍历节点,一边记录节点的高度
        if (root == null) return true;
        int res = getDepth(root);
        if (res == -1)
            return false;
        return true;
    }
    private int getDepth(TreeNode root) {
        if (root == null) return 0;
        // 记录左子树高度,如果左子树非平衡二叉树,则可以返回左子树高度为-1
        int left = getDepth(root.left);
        if (left == -1)		return left;
        // 记录右子树高度,如果右子树非平衡二叉树,则可以返回右子树高度为-1
        int right = getDepth(root.right);
        if (right == -1)	return right;
        // 记非平衡二叉树高度为 -1
        if (Math.abs(left - right) > 1)	   return -1;
        // 左右子树都是平衡二叉树,则返回该树的高度
        else	return left > right ? left + 1 : right + 1;
    }
}

2. 解法二

class Solution {
    public boolean isBalanced(TreeNode root) {
        if (root == null) return true;
        int leftH = getHeight(root.left);
        int rightH = getHeight(root.right);
        // 先判断根节点的左右是否满足
        if (Math.abs(leftH - rightH) > 1) {
            return false;
        }
        // 再递归判断左孩子和右孩子是不是平衡二叉树
        return isBalanced(root.left) && isBalanced(root.right);
    }
	// 获得树的高度,用来判断是不是平衡二叉树,用到了 38 求二叉树的高度
    private int getHeight(TreeNode root) {
        if (root == null) return 0;
        int left = getHeight(root.left);
        int right = getHeight(root.right);
        return Math.max(left, right) + 1;
    }
}

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

题目描述: 一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

1.解法一:使用一个哈希表来记录

  1. 如果元素没有出现过,就加入哈希表
  2. 如果出现过,那么删除对应的值,因为每个数字只出现两次
  3. 最后的 keySet 中只有两个元素,这两个元素就是要的结果
  4. 时空复杂度均为 O(n),因为使用了辅助数组
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        HashMap<Integer,Integer> map = new HashMap<>();
        for (int i = 0;i < array.length;i ++) {
            if (!map.containsKey(array[i])) map.put(array[i], 1);
            else map.remove(array[i]);
        }
        int[] aux = new int[2];
        int size = 0;
        for (int i : map.keySet()) 
            aux[size ++] = i;
        num1[0] = aux[0];
        num2[0] = aux[1];
    }
}

2.解法二:使用异或运算,这个666

  1. 利用题目中相同数字每一个仅出现两次的特点,使用异或运算 n ^ n = 0
  2. 我们首先仍然从前向后依次异或数组中的数字,那么得到的结果是两个只出现一次的数字的异或结果,其他成对出现的数字被抵消了。由于这两个数字不同,所以异或结果肯定不为0,也就是这个异或结果一定至少有一位是1,我们在结果中找到第一个为1的位的位置,记为第 n 位。接下来,以第 n 位是不是 1 为标准,将数组分为两个子数组,第一个数组中第 n 位都是 1,第二个数组中第 n 位都是 0。这样,便实现了我们的目标。最后,两个子数组分别异或则可以找到只出现一次的数字。
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        /*
        思路:数组中的元素先依次异或,相同为0,则得到的是两个只出现一次的数的异或结果
        对于得到的异或结果,找到其第一个为1的位
        该位为1,说明两个只出现一次的数该位不同,所以按照该位是0还是1将数组分成两部分
        这样,出现两次的数字都会分到同一个部分,而两个只出现一次的数正好被分开,再各自异或可得结果
        */
        if (array == null || array.length < 2) return;
        int res = 0;
        for (int num : array)
            res ^= num; // 异或得到的结果为只出现一次的两个元素的异或,必定至少有一位为1
        int index = 0;
        // 右移32是因为int就是32位的,找到从右向左第一个为1的位index
        for ( ;index < 32; index ++) {
            if (((res >> index) & 1) == 1)
                break;
        }
        // 根据index位为1还是0,将元素分为两组,那么仅有的两个唯一元素就被分开了
        for (int num : array) {
            if (((num >> index) & 1) == 1)
                num1[0] ^= num;
            else
                num2[0] ^= num;
        }
    }
}

你可能感兴趣的:(牛客剑指offer)