剑指offer 第六章

面试题目 53_I

题目描述

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

限制:
0 <= 数组长度 <= 50000

链接:https://leetcode-cn.com/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof

思路

分别对前半段和后半段用二分查找。二分查找总是先拿数组中间元素和target比较。如果中间元素大于target,那么下一轮只在数组的前半段查找即可;如果如果之间元素比target小,那么下一轮在后半段查找即可;如果中间元素等于target,需要先判断这个数字是不是第一个target。如果中间数字的前面一个数字不是target,那么此时的中间的数字刚好是第一个target;如果中间数字前面还有一个元素也是target,那么第一个target肯定在数组的前半段,下一轮仍然需要在数组的前半段查找。对于查找最后一个target位置的分析方法类似。

代码

public class Solution {
    public int search(int[] nums, int target){
        int number = 0;
        int length = nums.length;
        if (length > 0){
            int first = frontBinarySearch(nums,0,length - 1,target);
            int last = backBinarySearch(nums,length,0,length - 1,target);
            if (first > -1 && last > -1)
                number = last - first + 1;
        }
        return number;
    }
    // 对前半段二分查找
    public int frontBinarySearch(int[] num, int start, int end,int target){
        if (start > end) return -1;
        int midIndex = (start + end) >> 1; // 右移1位相当于除以2
        if (num[midIndex] == target){ // 判断是不是第一个target
            if ((midIndex > 0 && num[midIndex - 1] != target) || midIndex == 0)
                return midIndex; // 是数组中第一个target
            else
                end = midIndex - 1; // 对前半段继续用二分查找
        } else if (num[midIndex] > target){
            end = midIndex - 1;
        } else {
            start = midIndex + 1;
        }
        return frontBinarySearch(num,start,end,target);// 递归查找数组中第一个target
    }
    // 对后半段二分查找
    public int backBinarySearch(int[] num,int length,int start,int end,int target){
        if (start > end) return -1;
        int midIndex = (start + end) >> 1; // 右移1位相当于除以2
        // 判断是不是最后一个target
        if (num[midIndex] == target){
            if ((midIndex < length -1 && num[midIndex + 1] != target) || midIndex == length - 1)
                return midIndex; // 是数组中最后一个target
            else
                start = midIndex + 1; // 对后半段继续用二分查找
        } else if (num[midIndex] > target){
            end = midIndex - 1;
        } else {
            start = midIndex + 1;
        }
        return backBinarySearch(num,length,start,end,target);// 递归查找数组中最后一个target
    }
}

复杂度分析

时间复杂度:O(logn)

空间复杂度:O(1)

面试题目 53_II

题目描述

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:

输入: [0,1,3]
输出: 2

示例 2:

输入: [0,1,2,3,4,5,6,7,9]
输出: 8

限制:

  1. 1 <= 数组长度 <= 10000

链接:https://leetcode-cn.com/problems/que-shi-de-shu-zi-lcof

思路

长度为n-1的数组中元素范围是0n-1,也就是说在不缺元素的前提下数组中最大值为n-1,从0n-1之间n-1个数的和为sum1= (n-1)n/2;从头开始遍历数组,并将元素和累加得到sum2,最后用sum1 - sum2就是中间空缺的元素。

代码

class Solution {
    public int missingNumber(int[] nums) {
        int len = nums.length;
        int sum1 = 0, sum2 = 0;
        for (int ele: nums) {
            sum1 += ele;
        }
        sum2 += len * (len + 1) / 2;
        return sum2 - sum1;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)

面试题目 54

题目描述

给定一棵二叉搜索树,请找出其中第k大的节点。

示例 1:

输入: root = [3,1,4,null,2], k = 1
   3
  / \
 1   4
  \
   2
输出: 4

示例 2:

输入: root = [5,3,6,2,4,null,null,1], k = 3
       5
      / \
     3   6
    / \
   2   4
  /
 1
输出: 4

限制:

  • 1 ≤ k ≤ 二叉搜索树元素个数

链接:https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof

思路

中序遍历平衡二叉树得到一个递增序列 (左根右)

逆中序遍历平衡二叉树得到一个递减序列(右根左)

代码

import java.util.ArrayList;
import java.util.List;
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}
class Solution {
    public int kthLargest(TreeNode root, int k) {
        if(root == null || k ==0)
            return Integer.MIN_VALUE;
        List<Integer> list = new ArrayList<>();
        middle(root,list);
        return list.get(k -1);
    }
    public void middle(TreeNode root, List<Integer> list){
        if(root == null)
            return;
        middle(root.right, list);
        list.add(root.val);
        middle(root.left, list);
    }
}

面试题目 55_I

题目描述

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

例如:

给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。
提示:

  • 节点总数 <= 10000

链接:https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof

思路

先序遍历二叉树,分别统计左右子树的深度,最后比较左右子树深度,用深度大的加1就是答案。

代码

class TreeNode { 
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}
class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null)
            return 0;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);
        return left > right ? left + 1 : right + 1;
    }
}

面试题目 55_II

题目描述

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回 true 。
示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

       1
      / \
     2   2
    / \
   3   3
  / \
 4   4

返回 false 。
限制:

  • 0 <= 树的结点个数 <= 10000

链接:https://leetcode-cn.com/problems/ping-heng-er-cha-shu-lcof

思路

分别计算左右子树的深度,如果左右子树深度差的绝对值大于1肯定不是平衡二叉树;小于等于1则是平衡二叉树。

代码

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}
class Solution {
    public boolean isBalanced(TreeNode root){
        if(root == null)
            return true;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);
        int diff = Math.abs(left - right);
        if(diff > 1)
            return false;
        return isBalanced(root.left) && isBalanced(root.right);
    }
    public int maxDepth(TreeNode root) {
        if(root == null)
            return 0;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);
        return left > right ? left + 1 : right + 1;
    }
}

面试题目 56_I

题目描述

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

示例 1

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

示例 2

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

限制

  • 2 <= nums.length <= 10000

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路:异或运算

可以先考虑一个简单的问题:数组中只有一个数字出现了一次,其他数字都出现了两次,如何找出只出现一次的数字?

任何一个数异或本身都等于0,也就是说,如果从头到尾异或数组中每个数字,最终得到的是只出现一次的数字。回到本题,将数组中的数字从头到尾异或一遍,最后结果肯定是两个只出现一次的两个数异或的结果,这个结果不为0,也就是说这个结果的二进制表示中至少有一个为1,找到这个数二进制表示中最右边为1的位,假设为index,再遍历一遍原数组,如果原数组中元素index为1将其放在同一组内,index位为0的将其放在另一组内,这样就相当于用index位是否为1将原数组分为2组,而且可以保证这两组内只有一个出现一次的数,其他数都出现了两次。再用开始说得简单问题的方法就可以出来最终答案

代码

class Solution {
    public int[] singleNumbers(int[] nums) {
        if(nums.length == 2) return nums;
        int[] ans = new int[2];
        int xor = 0;
        for(int i = 0; i < nums.length; i++)
            xor = xor ^ nums[i];
        
        int indexOfOne = findFirstBitOne(xor);
        int first = 0, second = 0;
        for(int i = 0; i < nums.length; i++){
            if(isBitOne(nums[i], indexOfOne)){
                first = first ^ nums[i];
            } else{
                second = second ^ nums[i];
            }
        }
        ans[0] = first;
        ans[1] = second;
        return ans;
    }
    // 返回number的二进制表示中最右边为1的的位
    public int findFirstBitOne(int number){
        int index = 0;
        while((number & 1) == 0 && index < 32){
            number = number >> 1;
            index++;
        }
        return index;
    }
    // 判断number的二进制表示中从右边数起的index位是不是1
    public boolean isBitOne(int number, int index){
        number = number >> index;
        return (number & 1) == 1;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)

面试题目 56_II

题目描述

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1

输入:nums = [3,4,3,3]
输出:4

示例 2

输入:nums = [9,1,7,9,7,9,7]
输出:1

限制

  • 1 <= nums.length <= 10000
  • 1 <= nums[i] < 2^31

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

如果一个数字出现3次那么他的二进制位的每一位(0和1)也都出现3次,如果把所有出现三次的数字的二进制表示的每一位都加起来,那么每一位的和都能被3整除。因此,统计所有数字的各二进制位中 1 的出现次数,并对 3 求余,结果则为只出现一次的数字

代码

class Solution {
    public int singleNumber(int[] nums) {
        if(nums.length < 2) return nums[0];
        int[] bitSum = new int[32];
        for(int i = 0; i < nums.length; i++){
            int bitMask = 1;
            // 统计nums中各个元素二进制位中1出现的次数
            for(int j = 31; j >= 0; j--){
                int bit = bitMask & nums[i];
                if(bit != 0){
                    bitSum[j]++;
                }
                bitMask = bitMask << 1;
            }
        }
        int ans = 0;
        for(int i = 0; i < 32; i++){
            ans = ans << 1;
            ans += bitSum[i] % 3;
        }
        return ans;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)

面试题目 57_I

题目描述

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]

示例 2:

输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]

限制:
1 <= nums.length <= 10^5

1 <= nums[i] <= 10^6

链接:https://leetcode-cn.com/problems/he-wei-sde-liang-ge-shu-zi-lcof

思想

双指针法,设置2个指针,初始时候分别指向数组首尾元素。计算2个指针指向元素的和,如果它们的和刚好等于s,则找到所需的两个数,直接返回;如果它们的和大于s,则将尾指针减1;如果它们的和小于s,则将首指针加1。直到找到所需的两个数为止。

代码

public class Solution {
    public int[] twoSum(int[] nums, int target){
        int len = nums.length;
        if (len < 2) return new int[0];
        int left = 0, right = len -1;
        while ( left < right){
            if (nums[left] + nums[right] == target){
               return new int[]{nums[left],nums[right]};
            }
            else if (nums[left] + nums[right] > target) right--;
            else left++;
        }
        return new int[0];
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)

面试题目 57_II

题目描述

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]

示例 2:

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

限制:
1 <= target <= 10^5

链接:https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof

思想

滑动窗口,当窗口中元素和等于target时,将窗口中的元素作为一个结果保存起来,同时移动窗口的左边界,累加和减去溢出窗口的值;如果窗口小于target,窗口右边界向右滑动窗口,将sum加上滑入窗口的元素;否则窗口左边界向右滑动,同时sum减去滑出窗口的值

代码

import java.util.ArrayList;
import java.util.List;
class Solution {
    public int[][] findContinuousSequence(int target) {
        if(target < 3) return null;
        List<int[]> ans = new ArrayList<int[]>();
        int start = 1, end = 1;
        int sum = 0;
        while(start <= target / 2){
            if(sum > target){
                sum -= start;
                start++;
            } else if(sum < target){
                sum += end;
                end++;
            } else {
                int[] path = new int[end - start];
                for (int num = start ;num < end; num++)
                    path[num - start] = num;
                ans.add(path);
                sum -= start;
                start++;
            }
        }
        return ans.toArray(new int[ans.size()][]);
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

面试题目 58_I

题目描述

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"

示例 2:

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

说明:

  1. 无空格字符构成一个单词。
  2. 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
  3. 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

链接:https://leetcode-cn.com/problems/fan-zhuan-dan-ci-shun-xu-lcof

思想:双指针法

定义2个指针i和j,初始时均指向字符串末尾,指针由后往前遍历非空格字符,直到遇到第一个空格,这样可以找到一个完整单词,将[i+1,j]间字符加入结果str中;继续遍历,如果i指向位置为空格,继续移动i,如果i指向位置不为空,将i的值赋给j。继续上面循环

代码

public class Solution {
    public String reverseWords(String s){
        s = s.trim(); // 去掉首尾空格
        int j = s.length() -1, i = j;
        StringBuilder str = new StringBuilder();
        while (i >= 0){
            while (i >= 0 && s.charAt(i) != ' ') // 寻找第一个单词
                i--;
            str.append(s.substring(i + 1, j + 1) + " ");
            while (i >= 0 && s.charAt(i) == ' ') // 去掉单词间空格
                i--;
            j = i;
        }
        return str.toString().trim(); // 去掉末尾空格
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

面试题目 58_II

题目描述

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:

输入: s = "abcdefg", k = 2
输出: "cdefgab"

示例 2:

输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"

限制:
1 <= k < s.length <= 10000

链接:https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof

思想

定义一个StringBuilder类型的变量strs,从索引为n的位置开始遍历,依次将第n个位置之后的所有字符追加到strs;再从索引为0的位置遍历到索引为n-1的位置,依次将所有字符追加到strs之后。

代码

public class Solution {
    public String reverseLeftWords(String s, int n){
        int len = s.length();
        if (len < 1) return "";
        StringBuilder str = new StringBuilder();
        for (int i = n; i < len; i++)
            str.append(s.charAt(i));
        for (int i = 0; i < n; i++)
            str.append(s.charAt(i));
        return str.toString();
    }
}
API版本: 比上面的要快
public class Solution1 {
    public String reverseLeftWords(String s, int n){
        String sub = s.substring(0,n);
        s = s.substring(n);
        return s.concat(sub);
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

面试题目 59_I

题目描述

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 
 
  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

提示:
你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

链接:https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof

思想

采用单调递减队列实现。定义一个单调递减队列deque(本质是双端队列)保存滑动窗口中最大值得数字的下标,在存入一个数字的下标之前,首先要判断队列里已有数字是否小于待存入的数字。如果已有的数字小于待存入的数字,那么这些数字已经不可能是滑动窗口的最大值了,因此它们将会被依次从队列的尾部删除。同时,如果队列头部数字已经滑出了窗口,那么滑出的数字也需要从队列的头部删除。

代码

import java.util.Deque;
import java.util.LinkedList;
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
        if (len < 1) return nums; // 处理 len = 0 ,k = 0的情况
        int[] maxInWindows = new int[len - k + 1]; // 长度为len的数组,滑动窗口为k,可以滑动 len - k + 1 次
        int index = 0;
        if (len >= k && k >= 1){
            Deque<Integer> deque = new LinkedList<>();
            // 向单调队列中填充最初的k个元素
            for (int i = 0; i < k; i++){
                while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i])
                    deque.pollLast();
                deque.offerLast(i);
            }
            // 从第k个元素开始遍历
            for (int i = k; i < len; i++){
                maxInWindows[index++] = nums[deque.peekFirst()];
                // 去尾操作
                while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i])
                    deque.pollLast();
                // 删头操作
                if (!deque.isEmpty() && deque.peekFirst() <= i - k)
                    deque.pollFirst();
                deque.offerLast(i);
            }
            maxInWindows[index++] = nums[deque.peekFirst()];
        }
        return maxInWindows;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

面试题目 59_II

题目描述

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

示例 2:

输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

限制:
1 <= push_back,pop_front,max_value的总操作数 <= 10000

1 <= value <= 10^5

链接:https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof

思想:维护一个单调递减队列

从队列尾部插入元素时,我们可以提前取出队列中所有比这个元素小的元素,使得队列中只保留对结果有影响的数字。这样的方法等价于要求维持队列单调递减,即要保证每个元素的前面都没有比它小的元素。

代码

import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
public class MaxQueue{
    Deque<Integer> deque;
    Queue<Integer> queue;
    public MaxQueue(){
        deque = new LinkedList();
        queue = new LinkedList<>();
    }
    public int max_value(){
        if (deque.isEmpty()) return -1;
        return deque.peekFirst();
    }
    public void push_back(int value){
        while (!deque.isEmpty() && deque.peekLast() <= value)
            deque.pollLast();
        deque.offerLast(value);
        queue.offer(value);
    }
    public int pop_front(){
        if (queue.isEmpty()) return -1;
        int first = queue.poll();
        if (first == deque.peekFirst())
            deque.pollFirst();
        return first;
    }
}

复杂度分析

时间复杂度

max_value:O(1)

push_back: O(1)

pop_front:O(1)

空间复杂度:O(n)

面试题目 60

题目描述

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

示例 1:

输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]

示例 2:

输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]

限制

  • 1 <= n <= 11

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路:动态规划

  1. 确定dp数组及其下标含义

dp[i][j]表示i个骰子出现点数为j的次数

  1. 初始化dp数组

当只有一个骰子的时候,1-6点数只可能出现1一次

代码

class Solution {
    private int maxValue = 6; // 如果出现面数不为6的骰子,只需要修改一处即可,提高代码的扩展性
    public double[] dicesProbability(int n) {
        int[][] dp = new int[n + 1][maxValue * n + 1];
        // 只有一个骰子 [1,maxValue]的面数只可能出现一次
        for(int i = 1; i <= maxValue; i++){
            dp[1][i] = 1;
        }
        // 动态规划 就是填充dp的过程 骰子数为1的已经初始化了,所以从n = 2开始
        for(int i = 2; i <= n; i ++){
            // j 表示 i 个骰子可能出现的点数为j的次数
            for(int j = i; j <= maxValue * n; j++){
                // 上一个骰子只可能出现 [1,maxValue]次
                for(int k = 1; k <= maxValue; k++){
                    if(j - k <= 0)
                        break;
                    dp[i][j] += dp[i - 1][j - k];
                }
            }
        }
        // n个骰子出现点数总共有6^n种可能(如果骰子是6面的)
        int total = (int)Math.pow(maxValue, n);
        // n个面数为maxValue的骰子点数区间在[maxValue, maxValue * n] 
        // 所以最终数组长度是 maxValue * n - maxValue + 1 = (maxValue - 1) * n + 1
        double[] ans = new double[(maxValue - 1) * n + 1];
        int index = 0;
        for(int i = n; i <= maxValue * n; i++){
            ans[index++] = dp[n][i] * 1.0 / total;
        }
        return ans;
    }
}

复杂度分析

时间复杂度:O(n^2)

空间复杂度:O(n^2)

面试题目 61

题目描述

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

示例 1:

输入: [1,2,3,4,5]
输出: True

示例 2:

输入: [0,0,1,2,5]
输出: True

限制:
数组长度为 5

数组的数取值为 [0, 13] .

链接:https://leetcode-cn.com/problems/bu-ke-pai-zhong-de-shun-zi-lcof

思路

分3步实现

  1. 用快排将原数组排序
  2. 统计排序后的数组中0的个数
  3. 统计相连两个元素间空缺总数,如果零的个数大于或等于空缺数,则是顺子;否则不是顺子。除此之外还需要注意有没有除0外重复数字,如果有重复数字不是顺子而是对子。

代码

public class Solution {
    public boolean isStraight(int[] nums){
        int len = nums.length;
        if (len < 5) return false;
        fastSort(nums,0,len - 1);
        int numberOfZero = 0;
        int numberOfGap = 0;
        for (int i = 0; i < len && nums[i] == 0; i++) // 统计大、小王数量 <= 2
            numberOfZero++;
        int small = numberOfZero;
        int big = small + 1;
        while (big < len){
            if (nums[small] == nums[big]) // 有顺子
                return false;
            numberOfGap += nums[big] - nums[small] - 1; // 统计坑位
            small = big;
            big++;
        }
        return (numberOfGap <= numberOfZero) ? true : false;
    }
    public void fastSort(int[] nums,int start,int end){
        if (start >= end) return; // 这句必须要有
        int left = start, right = end;
        int key = nums[left];
        while (left < right){
            while (left < right && nums[right] >= key)
                right--;
            nums[left] = nums[right];
            while (left < right && nums[left] <= key)
                left++;
            nums[right] = nums[left];
        }
        nums[left] = key;
        fastSort(nums,start,left - 1);
        fastSort(nums,right + 1, end);
    }
}

复杂度分析

时间复杂度:O(nlogn)

空间复杂度:O(1)

面试题目 62

题目描述

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:

输入: n = 5, m = 3
输出: 3

示例 2:

输入: n = 10, m = 17
输出: 2

限制:
1 <= n <= 10^5

1 <= m <= 10^6

链接:https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof

思路

思路有点复杂,详细解析看《剑指offer》p302页

代码

public class Solution {
    public int lastRemaining(int n, int m){
        if (n < 1 || m < 1)
            return -1;
        int last = 0;
        for (int i = 2; i <= n; i++)
            last = (last + m) % i;
        return last;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)

面试题目 63

题目描述

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

限制

  • 0 <= 数组长度 <= 10^5

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/gu-piao-de-zui-da-li-run-lcof

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路:动态规划

用minPrices变量记录历史最低买入价格,初始时等于第0天的股票价格,定义dp数组,dp[i]表示第i天交易后的最大利润,dp[0]由于没有交易,所以初始化为0。如果第i天交易后的利润大于前一天的利润则交易,否则不交易,同时检查当前价格是否低于历史最低买入价格,以更新最低买入价格,所以状态转移方程为

dp[i] = Math.max(dp[i - 1], prices[i] - minPrices)

代码

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0) return 0;
        int[] dp = new int[prices.length];
        int minPrices = prices[0]; // 记录历史最低买入价格
        // 动态规划
        for(int i = 1; i < prices.length; i++){
            // 如果第i交易后的利润没有之前交易收益好,则不交易
            dp[i] = Math.max(dp[i - 1], prices[i] - minPrices);
            // 更新最低买入价
            if(prices[i] < minPrices) 
                minPrices = prices[i];
        }
        return dp[prices.length - 1];
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

面试题目 64

题目描述

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

示例 1:

输入: n = 3
输出: 6

示例 2:

输入: n = 9
输出: 45

限制:
1 <= n <= 10000

链接:https://leetcode-cn.com/problems/qiu-12n-lcof

思路:递归思想

递归需要用条件判断语句判断递归出口,但是题目明确规定不能用条件判断语句,所以需要通过其他方式达到条件判断的目的。可以用逻辑运算符的短路性质。以逻辑运算符 && 为例,对于 A && B 这个表达式,如果 A 表达式返回 False ,那么 A && B 已经确定为 False ,此时不会去执行表达式 B。同理,对于逻辑运算符 ||, 对于 A || B 这个表达式,如果 A 表达式返回 True ,那么 A || B 已经确定为 True ,此时不会去执行表达式 B。利用这一特性,我们可以将判断是否为递归的出口看作 A && B 表达式中的 A 部分,递归的主体函数看作 B 部分。如果不是递归出口,则返回 True,并继续执行表达式 B 的部分,否则递归结束。当然,你也可以用逻辑运算符 || 给出类似的实现,这里我们只提供结合逻辑运算符 && 的递归实现。

代码

public class Solution {
    public int sumNums(int n){
        int sum = 0;
        boolean flag = n > 0 && (sum += n + sumNums(n - 1)) > 0;
        return sum;
    }
    public static void main(String[] args){
        Solution solution = new Solution();
        System.out.println(solution.sumNums(9 ));
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

面试题目 65

题目描述

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

示例:

输入: a = 1, b = 1
输出: 2

提示:

  1. a, b 均可能是负数或 0
  2. 结果不会溢出 32 位整数

链接:https://leetcode-cn.com/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof

思路

计算2个数之和可以分3步:

  1. 只计算个位的和,不考虑进位
  2. 做进位
  3. 把前面2步结果相加

将这个规则迁移到二进制加法依然成立,可以将上面的3步简化为2步:

第一步中不考虑进位计算两个数的和可以用异或实现;

第二步中做进位,可以先让两个数按位与再左移一位。

循环执行上述2步操作,直到不产生进位为止。

代码

public class Solution {
    public int add(int a, int b) {
        int sum = 0, carry = 0;
        do {
            sum = a ^ b;
            carry = (a & b) << 1;
            
            a = sum;
            b = carry;
        }while (carry != 0);
        return sum;
    }
}

复杂度分析

时间复杂度:O(1)

空间复杂度:O(1)

面试题目 65_I

题目描述

不使用新的变量,交换两个数的值。比如有两个变量a,b,我们希望交换它们的值。

思路

有2种方法:

  1. 利用加减法:令 a + b的值赋值给a;令a - b的值赋值给b,这样就将b的值替换成了原先a的值;最后令a - b的值赋值给a,这样a的值替换成了b的值。
  2. 利用异或运算:先让 a ^ b的结果赋值给a;再让 a ^ b的结果赋值给b;最后让a ^ b的结果赋值给a

代码

public class Solution {
    // 基于加减法
    public static void exchange1(int a, int b)
    {
        System.out.printf("old: a = %d \t b = %d\n",a,b);
        a = a + b;
        b = a - b;
        a = a - b;
        System.out.printf("new: a = %d \t b = %d\n",a,b);
    }
    // 基于异或运算
    public static void exchange2(int a, int b){
        System.out.printf("old: a = %d \t b = %d\n",a,b);
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;
        System.out.printf("new: a = %d \t b = %d\n",a,b);
    }
    public static void main(String[] args){
        int a = 1, b = 2;
        exchange2(a,b);
    }
}

复杂度分析

时间复杂度:O(1)

空间复杂度:O(1)

面试题目 66

题目描述

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

示例:

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]

提示:

  1. 所有元素乘积之和不会溢出 32 位整数
  2. a.length <= 100000

链接:https://leetcode-cn.com/problems/gou-jian-cheng-ji-shu-zu-lcof

思路

分别计算元素i的前缀乘积和后缀乘积,最后再把元素i的前缀乘积和后缀乘积相乘即可。元素i的前缀乘积表示从第下标为0的元素开始累乘到下标为i``-1的元素,注意:下表为0的元素的前缀乘积是1;下标为i的元素的后缀乘积表示从最后一个元素开始累乘到下标为i``+ 1的元素,注意:最后一个元素的后缀乘积是1。

算法

  1. 初始化两个空数组mulPremulSuf。对于给定索引imulPre[i]代表的是i左侧所有数字的乘积,mulSuf[i]代表的是i右侧所有数字的乘积
  2. 我们需要用两个循环来填充mulPremulSuf数组的值。对于数组mulPremulPre[0]应该是 1 ,因为第一个元素的左边没有元素。对于其他元素:mulPre[i]=mulPre[i-1]*nums[i-1]
  3. 同理,对于数组mulSufmulSuf[length-1]应为 1。length指的是输入数组的大小。其他元素:mulSuf[i]=mulSuf[i+1]*nums[i+1]
  4. mulPremulSuf数组填充完成 ,我们只需要在输入数组上迭代,且索引i处的值为:mulPre[i] * mulSuf[i]

代码

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int len = nums.length;
        if (len <= 1)
            return nums;
        int[] mulPre = new int[len];
        int[] mutSuf = new int[len];
        int[] result = new int[len];
        // 计算前缀乘积
        for (int i = 0; i < len; i++){
            if (i == 0)
                mulPre[i] = 1;
            else
                mulPre[i] = mulPre[i - 1] * nums[i - 1];
        }
        // 计算后缀乘积
        for (int i = len - 1; i >= 0; i--){
            if (i == len - 1)
                mutSuf[i] = 1;
            else
                mutSuf[i] = nums[i + 1] * mutSuf[i + 1];
        }
        // 填充result
        for (int i = 0; i < len; i++)
            result[i] = mulPre[i] * mutSuf[i];
        return result;
    }
}

复杂度分析

时间复杂度:O(n)

空间复杂度:O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值