剑指offer做题笔记java版(41-68)

本文详细介绍了《剑指Offer》中涉及的算法问题,包括数据流中中位数的计算、最大子数组和、1的出现次数、数字序列中的特定数字、最小数字排列、字符串转换、礼物最大价值、不含重复字符的子字符串、丑数、第一个只出现一次的字符、逆序对、链表交点、二叉搜索树节点、数组和字符串操作等。主要探讨了动态规划、堆、双指针、哈希表等数据结构和算法的应用。
摘要由CSDN通过智能技术生成

文章目录

笔者记

笔者目前大四,虽然已经保研了,但研究生阶段是两年制专硕。找工作的压力也感到了些许,在学习科研的同时,也抽空准备工作方面的事情,如:算法基础、深度学习强化学习理论基础与面试常用题等。目前个人感觉科研方面出成果进大厂实验室较为困难,所以暂定目标为强化学习/深度学习的开发岗,与生产实践相结合,找工作的难度也相对容易。
本篇是剑指offer的刷题笔记下半部分,在此做一个记录,也供大家参考。笔记的上半部分可以点击链接:链接

说明

题目旁边加*,表示该题较为有价值
题目旁边加#,表示该题有更好的解法,做完一遍之后必须回顾
题目旁边加&,表示该题还有知识点需要整理

总结

至此,剑指offer的题目已经过了一遍。我发现有以下几点需要整理:
1.java中lambda表达式如何使用
2.java中各种数据结构的特性需要整理,如:Stack、Queue、PriorityQueue、Deque、LinkedList、ArrayList等。
3.剑指offer共75道题目中,有价值的题目仍需回顾,部分题目的最优解法在下一轮复习中需要着重整理。

剑指 Offer 41. 数据流中的中位数(大顶堆+小顶堆 &)

解题思路:采用堆(优先队列)的数据结构进行解题,采用大顶堆存储数据中较小的半部分,采用小顶堆存储数据中较大的半部分。插入数据时保持有序性,并且从大顶堆开始插入。大顶堆记为maxHeap,小顶堆记为minHeap。最终时间复杂度为O(logN),空间复杂度为O(n)。本题还涉及java8中lambda表达式的使用,后期需要整理。

插入时(保证数据的有序性):
1.如果maxHeap.size() == minHeap.size(),则先将新的数num插入minHeap,并将minHeap的堆顶弹出插入到maxHeap中,该轮操作后maxHeap中的数据量+1;
2.如果不等,则表示maxHeap.size() > minHeap.size(),则将num插入maxHeap()中,再将maxHeap的堆顶插入到minHeap中,该轮操作后minHeap中的数据量+1。

寻找中位数时:
1.如果 maxHeap.size() == minHeap.size(),表示数组长度为偶数,返回(maxHeap.peek()+minHeap.peek())/2.0;
2.如果不等,则表示数组长度为奇数,返回maxHeap.peek()。

class MedianFinder {
    private PriorityQueue<Integer> minHeap, maxHeap;//用小顶堆存储较大的部分,用大顶堆存储较小的部分

    /** initialize your data structure here. */
    public MedianFinder() {
        minHeap = new PriorityQueue<Integer>();
        maxHeap = new PriorityQueue<Integer>(
            new Comparator<Integer>(){
                //原有比较器返回负值表示i1<i2,正值表示i1>i2,按照递增进行排序
                //重写之后返回负值表示i1>i2,正值表示i1<i2,按照递减进行排序
                public int compare(Integer i1, Integer i2){
                    return i2 - i1;
            }});
        //也可以采用lamda表达式简化为:maxHeap = new PriorityQueue<>((x, y) -> (y - x));
    }
    
    public void addNum(int num) {
        if(minHeap.size() == maxHeap.size()){
            //如果maxHeap和minHeap相等,则在maxHeap中添加数据
            minHeap.offer(num);
            maxHeap.offer(minHeap.poll());
        }else{
            //如果minHeap和maxHeap不相等,则表示minHeap<maxHeap,则在minHeap中添加数据
            maxHeap.offer(num);
            minHeap.offer(maxHeap.poll());
        }
    }
    
    public double findMedian() {
        return minHeap.size() == maxHeap.size()? (maxHeap.peek()+minHeap.peek())/2.0 : maxHeap.peek();
    }
}

剑指 Offer 42. 连续子数组的最大和

解题思路:
1.朴素解法:用sum记录求和值,每次叠加num。如果sum>max,则用max进行记录。如果sum<0,则表示当前num及其前驱对于之后的求和没有正向影响,则将sum置为0,重新开始记录。
2.动态规划,用nums[i]记录以nums[i]结尾的最大和,且nums[i] += Math.max(nums[i-1], 0)。用res记nums的最大值。

	public int maxSubArray(int[] nums) {
        //解法1:朴素解法,思路类似解法2
        int max = Integer.MIN_VALUE;
        int sum = 0;
        for(int num : nums){
            sum += num;
            if(sum>max)max = sum;
            if(sum<0)sum = 0;
        }
        return max;

        //解法2:动态规划
        int res = nums[0];
        for(int i = 1; i< nums.length; i++){
            nums[i] += Math.max(nums[i-1],0);
            res = Math.max(res,nums[i]);
        }
        return res;
    }

剑指 Offer 43. 1~n 整数中 1 出现的次数

解题思路:将数字分为最高位和剩余位两部分,先计算带有最高位部分数字中1出现的次数。如将21345分为:1346 ~ 21345和1345。
对于带有高位的部分:
1.先计算最高位为1的情况,如果最高位的数字 > 1,则有10^n(最高位的位数)个1,如(21345包含10000 ~ 19999);如果最高位数字等于1,则有(剩余位+1)个1,如(11345包含10000 ~ 11345)。
2.然后计算除最高位外剩余部分1的个数,可以进行依次取1计算,假设剩余部分有k位,则1的个数为:最高位数字* k *10^(k-1),k位数任意位置取1,其他位置有0~9共10种选择。如(1346 ~ 21345中可分为1346 ~11345和11346 ~ 21345进行剩余4位的计算,所以要乘以最高位数字,表示分为几段)。
对于剩余部分
进行递归计算,递归跳出的条件为 n为1位数时,n == 0 返回0,1<=n<=9,返回1。

	public int countDigitOne(int n) {//以n=21345为例
        if(n == 0)return 0;
        if(n >= 1 && n <= 9)return 1;
        String str = String.valueOf(n);
        int head = str.charAt(0) - '0', res = 0;
        if(head > 1){
            //如果首位大于1,则首位为1有10^(str.length()-1)个数,10000~19999
            res = (int)Math.pow(10,str.length()-1);
        }else{
            //如果首位为1,则首位为1有剩余部分数+1个数
            res = Integer.parseInt(str.substring(1,str.length())) + 1;
        }
        //计算大于去除首位数的剩余部分时,其他位置为1的情况
        res += head*(str.length()-1)*(int)Math.pow(10,str.length()-2);//1346~21345
        //首位不为0时,1存在的情况并加上递归求解首位为0时,1存在的情况
        return res+countDigitOne(Integer.parseInt(str.substring(1,str.length())));//1346~21345+0~1345
    }

剑指 Offer 44. 数字序列中某一位的数字

解题思路:用n>1位数、2位数、3位数…的总数,则依次减掉。如果n不满足i位数个数的总数,则表示n对应的数字是一个i位数中的一位,则从i位数中的最小值开始加上n/i,n指向的是其中的n%i位。所以需要记录位数i(digit),i位数的开始值start,以及i位数的总数count。
在代码中,对n<10的情况做了单独的讨论。

class Solution {
    public int findNthDigit(int n) {
        if(n<10)return n;
        int digit = 1;
        long count = 10;
        long start = 1;
        while(n > count){
            n -= count;
            start *=10;//10,100
            digit++;//1,2,3
            count = 9*start*digit;//10,180,2700
        }
        start = start + n/digit;
        n = n % digit;
        return String.valueOf(start).charAt(n)-'0';
    }
}

剑指 Offer 45. 把数组排成最小的数(&)

解题思路:自定义新的排序方法,然后将数组排序进行后,进行连接。在此,不得不提lambda表达式真的很好用,比我自己写Comparator中的compare方法要方便多了。

	public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for(int i = 0; i < nums.length; i++) 
            strs[i] = String.valueOf(nums[i]);
        Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));
        StringBuilder res = new StringBuilder();
        for(String s : strs)
            res.append(s);
        return res.toString();
	}

剑指 Offer 46. 把数字翻译成字符串(动态规划)

解题思路:用f[i]记录以str[i-1]结尾的数字串有多少种翻译方法,如果str[i-2],str[i-1]表示的数范围为10~25,则表示f[i] = f[i-1] + f[i-2]。否则当前只有一种表示方法,即f[i] = f[i-1]。

	public int translateNum(int num) {
        String str = String.valueOf(num);
        int len = str.length();
        int[] f = new int[len+1];
        f[0]=f[1] = 1;
        for(int i=2; i <= len ; i++){
            f[i] = f[i-1];
            int number = (str.charAt(i-2)-'0')*10+(str.charAt(i-1)-'0');
            if(number>25|| number<10)continue;
            f[i] += f[i-2];
        }
        return f[len];
    }

剑指 Offer 47. 礼物的最大价值(动态规划)

解题思路:用dp[i][j]记录位置(i,j)礼物的最大价值,则dp[i][j] = max(dp[i-1][j],dp[i][j-1])+grid[i][j],i>0,j>0。从i = 0,j = 0 ,依次按照行向右下角遍历。需要注意第一行和第一列的特殊处理。

	public int maxValue(int[][] grid) {
        int rows = grid.length, cols = grid[0].length;
        for(int j = 1; j < cols; j++){
            grid[0][j] += grid[0][j-1]; 
        }
        for(int i = 1; i < rows; i++){
            grid[i][0] += grid[i-1][0];
        }
        for(int i = 1; i < rows; i++){
            for(int j = 1; j < cols; j++){
                grid[i][j] += Math.max(grid[i-1][j],grid[i][j-1]);
            }
        }
        return grid[rows-1][cols-1];
    }

剑指 Offer 48. 最长不含重复字符的子字符串(动态规划+哈希表*)

解题思路: 采用动态规划思路,找到以当前字符s[j]结尾的最长字符串长度dp[j],然后找到dp中的最大值。需要记录s[j]左边距离最近的相同字符s[i],状态转移方程如下:
1.如果dp[j-1] < j - i,表示s[i]不包含在dp[j-1]所对应的字符串中,dp[j] = dp[j-1] + 1。
2.如果dp[j-1] >= j - i,表示s[i]包含在dp[j-1]所对应的字符串中,dp[j] = j - i。

	public int lengthOfLongestSubstring(String s) {
        HashMap<Character,Integer> map = new HashMap();
        int tmp = 0, res = 0;
        for(int j = 0; j < s.length(); j++){
            int i = map.getOrDefault(s.charAt(j),-1);
            map.put(s.charAt(j),j);
            tmp = tmp < j-i ? tmp+1 : j-i;
            res = Math.max(res,tmp);
        }
        return res;
    }

剑指 Offer 49. 丑数(动态规划)

解题思路:因为丑数定义是只包含2、3、5质因子,所以大的丑数一定是小的丑数*2/3/5。所以依次将目前的最小值加入数组中,目前的最小值=min(nums[i2]*2, nums[i3]*3, nums[i5]*5)。

	public int nthUglyNumber(int n) {
        int nums[] = new int[n+1];
        nums[1] = 1;
        int i2 = 1, i3 = 1, i5 = 1;
        for(int i = 2; i <= n; i++){
            int n2 = nums[i2]*2, n3 = nums[i3]*3, n5 = nums[i5]*5;
            int k = Math.min(Math.min(n2,n3),n5);
            nums[i] = k;
            if(k == n2)i2++;
            if(k == n3)i3++;
            if(k == n5)i5++;
        }
        return nums[n];
    }

剑指 Offer 50. 第一个只出现一次的字符(哈希表/有序哈希表)

解题思路:
1.无序哈希表解法:遍历一次字符串,用哈希表记录每个字符出现的次数(多于1次,置为false)。然后再次遍历字符串,找到第一个出现次数为1的字符。
2.有序哈希表解法:遍历一次字符串,按序将字符加入哈希表中,并记录出现次数(多于1次,置为false)。然后依次遍历有序哈希表,找到第一个出现次数为1的字符。

public char firstUniqChar(String s) {
        //java中用LinkedHashMap实现有序哈希表
        Map<Character, Boolean> dic = new LinkedHashMap<>();
        char[] sc = s.toCharArray();
        for(char c : sc)
            dic.put(c, !dic.containsKey(c));
        for(Map.Entry<Character, Boolean> d : dic.entrySet()){
           if(d.getValue()) return d.getKey();
        }
        return ' ';

        // 无序哈希表,两次遍历
        // HashMap<Character,Boolean> map = new HashMap();
        // char[] sc = s.toCharArray();
        // for(char c : sc){
        //     map.put(c,!map.containsKey(c));
        // }
        // for(char c : sc){
        //     if(map.get(c))return c;
        // }
        // return ' ';

        // 原书思路,因为C++中char为长度为8的数据类型,范围是0-255。
        // 但在java中,char是unicode编码,占两个字节,范围是0-65535,所以不一定合适
        // int map[] = new int[256];
        // for(int i = 0; i < s.length(); i++){
        //     map[s.charAt(i)]++;
        // }
        // for(int i = 0; i < s.length(); i++){
        //     if(map[s.charAt(i)] == 1)return s.charAt(i);
        // }
        // return ' ';
    }

剑指 Offer 51. 数组中的逆序对(归并/分治*)

解题思路:简单来说就是采用归并排序的思路进行解题,首先将原数组一份为二,递归计算左半部分中的逆序对个数、右半部分中的逆序对个数、以及左右部分之间的逆序对个数。左右部分中逆序对的个数为原问题的子问题,分治解决即可。
在计算左右部分间的逆序对个数时,首先保证左右两个部分是有序的,然后采用双指针依次从最后一位开始比较:
1.如果左指针大于右指针,则表示左指针对应值大于右指针对应值,则有k(右指针前数据的个数)个逆序对。将左指针对应值加入到合并数组中,左指针前移。
2.如果左边指针小于右指针,则未形成逆序对,将右指针对应值加入到合并数组中,右指针前移。
因为是有序的,所以不管是左指针还是右指针值加入合并数组中,都是当前剩余序列的最大值,因此保证了合并序列的有序性。

class Solution {
    public int reversePairs(int[] nums) {
        if(nums == null || nums.length == 0)return 0;
        int copy[] = new int[nums.length];
        for(int i = 0; i < nums.length; i++){
            copy[i] = nums[i];
        }
        return reversePairsCore(nums, 0, nums.length-1, copy);
    }

    private int reversePairsCore(int[] nums, int left, int right,int[] copy){
        if(left == right){
            return 0;
        }
        int mid = (right - left)/2;
        int l_num = reversePairsCore(copy, left, left+mid, nums);
        //copy和nums互换,保证递归后回传的数组是有序的nums
        int r_num = reversePairsCore(copy, left+mid+1, right, nums);
        int i = left+mid;
        int j = right;
        int count = 0;
        int indexCopy = right;
        while(i >= left && j >= left+mid+1){
            if(nums[i] > nums[j]){//左指针大于右指针,共有j-left-mid个逆序对
                count += j-left-mid;
                copy[indexCopy--] = nums[i--];
            }else{//左指针小于等于右指针,未形成逆序对
                copy[indexCopy--] = nums[j--];
            }
        }
        //将剩余部分加入到合并数组
        for(;i >= left; i--){
            copy[indexCopy--] = nums[i];
        }
        for(;j >= left+mid+1; j--){
            copy[indexCopy--] = nums[j];
        }
        return l_num + r_num + count;//返回逆序对的总数
    }
}

剑指 Offer 52. 两个链表的第一个公共节点

解题思路:两次遍历,第一次遍历分别计算两个链表的长度。因为两个单链表有公共部分,所有公共部分的长度相同。将第一次遍历计算出的长度较长的链表,从头开始依次后移,直至长度与较短的链表相等时,开始判断剩余部分是否有相同节点。

	public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = 0, lenB = 0;
        ListNode curA = headA, curB = headB;
        //计算链表长度
        while(curA != null){
            lenA++;
            curA = curA.next;
        }
        while(curB != null){
            lenB++;
            curB = curB.next;
        }
        curA = headA;
        curB = headB;
        //将较长链表依次后移,直至当前位置所剩链表长度与较短链表相等
        while(lenA != lenB){
            if(lenA > lenB){
                curA = curA.next;
                lenA--;
            }else{
                curB = curB.next;
                lenB--;
            }
        }
        //寻找公共节点
        while(curA != null && curB != null){
            if(curA == curB)return curA;
            curA = curA.next;
            curB = curB.next;
        }
        return null;
    }

剑指 Offer 53 - I. 在排序数组中查找数字 I(二分查找)

解题思路:采用二分查找,查找左右边界。需要注意最终边界值落在哪个变量上,对于临界值的判断需要仔细分析。

	public int search(int[] nums, int target) {
        int i = 0, j = nums.length - 1;
        while(i <= j){
            int mid = (i + j)/2;
            if(nums[mid] > target) j = mid - 1;
            else i = mid + 1;//寻找右边界
        }
        int right = i;
        if(j > 0 && nums[j] != target) return 0;
        i = 0;
        while(i <= j){
            int mid = (i + j)/2;
            if(nums[mid] >= target) j = mid - 1;//寻找左边界
            else i = mid + 1;
        }
        int left = j;
        return right - left - 1;
    }

剑指 Offer 53 - II. 0~n-1中缺失的数字(二分查找)

	public int missingNumber(int[] nums) {
        int left = 0, right = nums.length - 1;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] == mid){
                left = mid + 1;
            }else{
                right = mid;
            }
        }
        if(left == nums.length-1 && nums[nums.length-1] == nums.length-1)return nums.length;
        return left;
    }

剑指 Offer 54. 二叉搜索树的第k大节点

解题思路:采用“右子树-根-左子树”的顺序得到二叉搜索树节点的降序排列。

class Solution {
    int times,res;
    public int kthLargest(TreeNode root, int k) {
        times = k;
        inOrder(root);
        return res;
    }

    private void inOrder(TreeNode node){
        if(node == null || times <= 0)return;
        inOrder(node.right);
        times--;
        if(times == 0)res = node.val;
        inOrder(node.left);
    }
}

剑指 Offer 55 - I. 二叉树的深度

解题思路:找到叶子节点,比较深度,并记录最大值。

class Solution {
    int res = 0;
    public int maxDepth(TreeNode root) {
        preOrder(root, 1);
        return res;
    }

    private void preOrder(TreeNode root, int depth){
        if(root == null)return;
        if(root.left == null && root.right == null){
            if(depth > res)res = depth;
            return;
        }else{
            preOrder(root.left, depth+1);
            preOrder(root.right, depth+1);
        }
    }
}

剑指 Offer 55 - II. 平衡二叉树(#)

解题思路:先序遍历+深度判断。先判断根节点是否平衡,再递归判断左右子树是否平衡。本题中也给出了计算树的深度的另一种方法,详见代码中的depth()方法。

class Solution {
    //题解中后序遍历+剪枝的方法更为精妙,后期需要整理*
    public boolean isBalanced(TreeNode root) {
        if(root == null) return true;
        return Math.abs(depth(root.left) - depth(root.right)) <=1 && 
               isBalanced(root.left) && isBalanced(root.right);
    }

    private int depth(TreeNode node){
        if(node == null) return 0;
        return Math.max(depth(node.left),depth(node.right)) + 1;
    }
}

剑指 Offer 56 - I. 数组中数字出现的次数(位运算)

解题思路:只有两个数字只出现了1次,其他数字都出现了两次。如果问题是:只有一个数字只出现了1次,那么我们可以通过将所有的数字进行异或操作进行解题,因为出现2次的数字和本身进行异或的结果为0。
由此我们可以将原问题转化为:将原数组拆分为两个子数组,每个子数组中包含一个只出现一次的数字,其余数字出现两次。我们将所有数字进行异或操作,得到两个只出现1次的数字异或操作的结果,然后根据结果二进制最低的非零位进行子数组的分类。

	public int[] singleNumbers(int[] nums) {
        //采用异或操作进行解题
        int res = 0;
        //所有数字进行异或
        for(int num : nums){
            res ^= num;
        }
        //找到最低非零位
        int div = 1;
        while((res & div) == 0){
            div <<= 1;
        }
        int a = 0, b = 0;
        for(int num : nums){
            if((num & div) == 0){//分类
                a ^= num;//找到子数组中只出现一次的数
            }else{
                b ^= num;
            }
        }
        return new int[]{a,b};
    }

剑指 Offer 56 - II. 数组中数字出现的次数 II

解题思路:将所有数字的二进制树按位相加,最后结果按位%3,则可以消除出现3次的数字,得到只出现1次的数字。

	public int singleNumber(int[] nums) {
        int bitArr[] = new int[32];
        for(int num : nums){
            for(int j = 0; j < 32; j++){
                if((num & 1) != 0)
                    bitArr[j] += 1;
                num >>>= 1;
            }
        }
        int result = 0,bitMask = 1;
        for(int i = 31; i >= 0; i--){
            result <<= 1;
            result |= bitArr[i]%3;
        }
        return result;
    }

剑指 Offer 57. 和为s的两个数字(双指针)

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

剑指 Offer 57 - II. 和为s的连续正数序列(双指针)

	public int[][] findContinuousSequence(int target) {
        List<int []> res = new ArrayList<int[]>();
        int small = 1, big = 2;
        while(small < big){
            int sum = (small + big)*(big - small + 1)/2;
            if(sum == target){
                int arr[] = new int[big - small +1];
                for(int i = 0; i <= big - small; i++){
                    arr[i] = small+i;
                }
                res.add(arr);
                small++;
            }else if(sum > target){
                small++;
            }else{
                big++;
            }
        }
        return res.toArray(new int[res.size()][]);
    }

剑指 Offer 58 - I. 翻转单词顺序

	public String reverseWords(String s) {
        String arr[] = s.split(" ");
        StringBuilder res = new StringBuilder();
        for(int i = arr.length-1; i >= 0; i--){
            if(arr[i].equals(""))continue;
            res.append(arr[i]);
            res.append(" ");
        }
        return res.toString().trim();
    }

剑指 Offer 58 - II. 左旋转字符串

	public String reverseLeftWords(String s, int n) {
        String a = s.substring(0,n);
        String b = s.substring(n,s.length());
        return b+a;
    }

剑指 Offer 59 - I. 滑动窗口的最大值(双端队列/单调队列*)

解题思路:用双端队列记录当前窗口的最大值。在未形成窗口时,依次将值加入队列,在每个新的值加入队列前,需要把队列中小于当前值的元素依次弹出,以保证队头元素是最大值。形成窗口后,如果窗口最前面的元素是队列中的最大值(队头),则将队头弹出。然后采用与未形成窗口时相同的加入方式,将新的值插入到队列中。最后获得当前窗口的最大元素——队列头部元素。

	public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0)return new int[0];
        int[] res = new int[nums.length - k + 1];
        Deque<Integer> deque = new LinkedList<>();
        //未形成窗口时
        for(int i = 0; i < k; i++){
            //插入新元素,需要将队列中所有小于该元素的值从队尾弹出队列
            while(!deque.isEmpty() && deque.peekLast() < nums[i])
                deque.removeLast();
            deque.addLast(nums[i]);
        }
        res[0] = deque.peekFirst();
        //形成窗口后
        for(int i = k; i < nums.length; i++){
            //如果队头元素是窗口即将删除的元素,则将其从队头弹出队列
            if(deque.peekFirst() == nums[i-k])
                deque.removeFirst();
            while(!deque.isEmpty() && deque.peekLast() < nums[i])
                deque.removeLast();
            deque.addLast(nums[i]);
            res[i - k + 1] = deque.peekFirst();
        }
        return res;
    }

剑指 Offer 59 - II. 队列的最大值

解题思路:同 59 - I

class MaxQueue {
    Deque<Integer> deque;
    Queue<Integer> queue;
    public MaxQueue() {
        deque = new LinkedList<>();
        queue = new LinkedList<>();
    }
    
    public int max_value() {
        if(!deque.isEmpty())return deque.peekFirst();
        else return -1;
    }
    
    public void push_back(int value) {
        queue.offer(value);
        while(!deque.isEmpty() && deque.peekLast() < value){
            deque.removeLast();
        }
        deque.addLast(value);
    }
    
    public int pop_front() {
        if(!queue.isEmpty()){
            if(queue.peek().equals(deque.peekFirst())){
                deque.removeFirst();
            }
            return queue.poll();
        }else{
            return -1;
        }
    }
}

剑指 Offer 60. n个骰子的点数(动态规划)

解题思路:先计算1个骰子对应值出现的次数,然后是2、3、… 、n。递推式:dp(n)[k] = dp(n-1)[k-1]+dp(n-1)[k-2]+…+dp(n-1)[k-6]。

	public double[] dicesProbability(int n) {
        //取数的范围是n~6n,dp[k]表示总和为k的次数,最后返回的是dp[k]/6^n
        //下一轮dp(i+1)[k] = dp(i)[k-1]+dp(i)[k-2]+...+dp(i)[k-6],从后向前更新
        int dp[] = new int[6*n+1];
        for(int i = 1; i <= 6; i++){
            dp[i] = 1;
        }
        for(int i = 2; i <= n; i++){
            for(int j = 6*n; j > 6; j--){
                dp[j] = dp[j-1]+dp[j-2]+dp[j-3]+dp[j-4]+dp[j-5]+dp[j-6];
            }
            dp[6] = dp[5]+dp[4]+dp[3]+dp[2]+dp[1];
            dp[5] = dp[4]+dp[3]+dp[2]+dp[1];
            dp[4] = dp[3]+dp[2]+dp[1];
            dp[3] = dp[2]+dp[1];
            dp[2] = dp[1];
            dp[1] = dp[0];
        }
        double res[] = new double[5*n+1];
        double total = Math.pow(6,n);
        for(int i = 0; i <= 5 * n; i++){
            res[i] = dp[i+n]/total;
        }
        return res;
    }

剑指 Offer 61. 扑克牌中的顺子

	public boolean isStraight(int[] nums) {
        Arrays.sort(nums);
        int num_zero = 0;
        for(int i = 0; i < nums.length-1; i++){
            if(nums[i] == 0){
                num_zero++;
                continue;
            }
            int gap = nums[i+1] - nums[i];
            if(gap == 0)return false;
            if(gap != 1){
                num_zero = num_zero - (gap - 1);
                if(num_zero < 0)return false;
            }
        }
        return true;
    }

剑指 Offer 62. 圆圈中最后剩下的数字

解题思路:该题的方法十分巧妙,如果知道f(n-1,m)时最后所剩的数字为x,则f(n,m)时最后所剩数字为(x+m)%n。由此可以进行求解,具体理解过程官方题解给出了较为形象的解释。

	public int lastRemaining(int n, int m) {
        // if(n == 1)return 0;
        // else return (lastRemaining(n-1,m)+m)%n;

        int res = 0;
        for(int i = 2; i <= n; i++){
            res = (res + m) % i;
        }
        return res;
    }

剑指 Offer 63. 股票的最大利润

	public int maxProfit(int[] prices) {
        int res = 0;
        int sum = 0;
        for(int i = 0; i < prices.length - 1; i++){
            int gap = prices[i+1] - prices[i];
            if(sum + gap > 0)sum += gap;
            else sum = 0;
            if(sum > res)res = sum;
        }
        return res;
    }

剑指 Offer 64. 求1+2+…+n

解题思路:采用递归进行求解,采用逻辑运算进行判断

	public int sumNums(int n) {
        // return (int)n*(n+1)/2;
        boolean flag = n > 0 && (n += sumNums(n - 1)) > 0;
        return n;
    }

剑指 Offer 65. 不用加减乘除做加法(位运算)

解题思路:采用异或操作进行按位加法计算,采用与操作表示进位,然后再将进位部分左移1位,并且与异或操作的结果进行相加。

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

剑指 Offer 66. 构建乘积数组(*)

解题思路:将数组a扩展为一个矩阵,并把对角线置为1,则最终结果为矩阵每行的乘积和。两次遍历:第一次遍历,先计算下三角形对应的值,进行累乘,并且res[i] = res[i-1] * a[i-1]。第二次遍历,计算上三角形对应的值,进行累乘( tmp = tmp * a[i+1]),并乘到结果中(res[i] *= tmp)。

	public int[] constructArr(int[] a) {
        if(a.length == 0)return a;
        int res[] = new int[a.length];
        res[0] = 1;
        for(int i = 1; i < a.length; i++){
            res[i] = res[i-1] * a[i-1];
        }
        int tmp = 1;
        for(int i = a.length - 2; i >= 0; i --){
            tmp = tmp * a[i+1];
            res[i] *= tmp;
        }
        return res;

剑指 Offer 67. 把字符串转换成整数

解题思路:本题需要注意对于临界值的判断。

	public int strToInt(String str) {
        char[] c = str.trim().toCharArray();//去除首尾空格,转为char[]
        if(c.length == 0) return 0;
        int res = 0, bndry = Integer.MAX_VALUE / 10;
        int i = 1, sign = 1;
        if(c[0] == '-') sign = -1;//判断符号部分
        else if(c[0] != '+') i = 0;
        for(int j = i; j < c.length; j++) {
            if(c[j] < '0' || c[j] > '9') break;//非数字,跳出循环
            //对于越界进行讨论,int的最大值为21474836472147483647
            if(res > bndry || res == bndry && c[j] > '7') return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
            res = res * 10 + (c[j] - '0');//数字部分进行拼接
        }
        return sign * res;
    }

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

解题思路:依据二叉搜索树的性质进行解题。

	public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root.val > p.val && root.val > q.val)return lowestCommonAncestor(root.left,p,q);
        else if(root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right,p,q);
        else return root;
    }

剑指 Offer 68 - II. 二叉树的最近公共祖先

解题思路:从下到上,依次判断当前节点是否是最接近的公共祖先。最接近的公共祖先需要满足以下两个条件之一:
1.p和q分别在左右子树中,即flson && frson
2.root等于p或q,且剩余节点在左子树或右子树中,即 (root==p || root == q) && (flson || frson)

class Solution {
    private TreeNode res;
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        this.dfs(root, p, q);
        return this.res;
    }

    public boolean dfs(TreeNode root, TreeNode p, TreeNode q){
        if(root == null) return false;
        boolean flson = dfs(root.left, p, q);
        boolean frson = dfs(root.right, p, q);
        if((flson && frson) || ((root == p || root == q) && (flson || frson)))
            res = root;
        return flson || frson || (root == p || root == q);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小郁同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值