力扣记录:剑指offer(6)——JZ53-58

这篇博客详细介绍了《剑指Offer》中JZ53到JZ58的算法问题,包括在排序数组中查找数字、找寻0~n-1中缺失的数字、二叉搜索树的第k大节点、二叉树的深度、平衡二叉树、数组中数字出现的次数、和为s的两个数字、和为s的连续正数序列、翻转单词顺序以及左旋转字符串等。每道题目都给出了高效的解题思路和时间空间复杂度分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JZ53-I 在排序数组中查找数字 I

  • 先使用二分查找搜索目标数字位置,找到目标数字后再向两边计算目标数字出现的次数(优化:两次二分查找,分别找到左边界和右边界)
    • 时间复杂度O(logn),空间复杂度O(1)
class Solution {
    public int search(int[] nums, int target) {
        //二分查找,左闭右闭
        int left = 0;
        int right = nums.length - 1;
        int count = 0;
        while(left <= right){
            int mid = left + (right - left) / 2;
            if(nums[mid] > target){
                right = mid - 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                //向两边搜索
                int i = mid;
                int j = mid;
                count += 1;
                while(i >= 1 && nums[i - 1] == target){
                    count++;
                    i--;
                }
                while(j < nums.length - 1 && nums[j + 1] == target){
                    count++;
                    j++;
                }
                return count;
            }
        }
        return count;
    }
    //优化
    public int search(int[] nums, int target) {
        //二分查找,左闭右闭
        //搜索右边界
        int left = 0;
        int right = nums.length - 1;
        int rightBorder = 0;
        while(left <= right){
            int mid = left + (right - left) / 2;
            if(nums[mid] > target){
                right = mid - 1;
            }else if(nums[mid] <= target){
                left = mid + 1;
            }
        }
        if(left == 0 || nums[left - 1] != target) return 0;  //未搜索到目标值
        rightBorder = left;
        //搜索左边界
        int leftBorder = 0;
        left = 0;
        right = rightBorder;    //左边界肯定在右边界左边
        while(left <= right){
            int mid = left + (right - left) / 2;
            if(nums[mid] >= target){
                right = mid - 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }
        }
        leftBorder = right;
        return rightBorder - leftBorder - 1;
    }
}

JZ53-II 0~n-1中缺失的数字

  • 二分查找,二分后得到的数需要和下标一致,如果大于下标则向左边搜索(左边缺一位),如果等于下标则向右边搜索
    • 时间复杂度O(logn),空间复杂度O(1)
class Solution {
    public int missingNumber(int[] nums) {
        //二分查找
        int left = 0;
        int right = nums.length - 1;
        while(left <= right){
            int mid = left + (right - left) / 2;
            if(nums[mid] > mid){
                right = mid - 1;
            }else{
                left = mid + 1;
            }
        }
        return left;
    }
}

JZ54 二叉搜索树的第k大节点

  • 中序遍历(二叉搜索树中序遍历递增),计数返回结果
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    int res, count; //注意这里使用全局变量不容易混淆
    public int kthLargest(TreeNode root, int k) {
        //中序遍历的逆序右中左
        count = 0;
        reverseInorder(root, k);
        return res;
    }
    private void reverseInorder(TreeNode root, int k){
        if(root == null) return;
        if(count == k) return;
        reverseInorder(root.right, k);
        count++;
        if(count == k){
            res = root.val;
            return;
        }
        reverseInorder(root.left, k);
    }
}

JZ55-I 二叉树的深度

  • 前序遍历或后序遍历,到达叶子节点时计算当前深度
    • 时间复杂度O(n),空间复杂度O(二叉树深度)
class Solution {
    int res;
    public int maxDepth(TreeNode root) {
        //前序遍历
        res = 0;
        if(root == null) return res;
        preorder(root, 1);
        return res;
    }
    //前序遍历,输入根节点和当前深度
    private void preorder(TreeNode root, int depth){
        //到达叶子节点
        if(root != null && root.left == null && root.right == null){
            res = Math.max(res, depth);
            return;
        }
        depth++;
        if(root.left != null) preorder(root.left, depth);
        if(root.right != null) preorder(root.right, depth); 
    }
}
//优化
class Solution {
    public int maxDepth(TreeNode root) {
        //后序遍历
        if(root == null) return 0;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);
        return Math.max(left, right) + 1;
    }
}

JZ55-II 平衡二叉树

  • 递归,后序遍历,先判断左右子树是否平衡进行剪枝,若左右子树平衡,再判断当前节点是否平衡
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    public boolean isBalanced(TreeNode root) {
        //递归
        int res = getDepth(root);
        if(res == -1) return false;
        return true;
    }
    //递归函数
    private int getDepth(TreeNode root){
        if(root == null) return 0;
        //后序遍历
        //注意先判断左右子树是否平衡
        int left = getDepth(root.left);
        if(left == -1) return -1;
        int right = getDepth(root.right);
        if(right == -1) return -1;
        //若左右子树都平衡,判断当前节点是否平衡
        if(Math.abs(left - right) > 1) return -1;
        return Math.max(left, right) + 1;
    }
}

JZ56-I 数组中数字出现的次数

  • 如果数组中只有一个数字出现一次,则可以通过遍历数组进行异或运算得到该数字;本题中为两个数字x,y出现一次,因此先遍历数组进行异或运算得到x异或y,然后通过该结果将数组分为两部分,两部分分别包含x和y,再分别对两数组进行遍历异或得到x和y

    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int[] singleNumbers(int[] nums) {
        //遍历数组进行异或得到x⊕y
        int res = 0;
        for(int num : nums){
            res ^= num;
        }
        //取res中为1的二进制位作为分类依据
        int div = 1;    //从第一位开始与运算
        while((div & res) == 0){    //如果该位为0
            div <<= 1;  //左移一位(*2)
        }
        int x = 0, y = 0;
        //遍历数组将数组分为两部分进行异或
        for(int num : nums){
            if((div & num) == 0){
                x ^= num;
            }else{
                y ^= num;
            }
        }
        return new int[]{x, y};
    }
}

JZ56-II 数组中数字出现的次数II

  • 遍历数组中各个数字的二进制位,统计每一位中1的出现次数并对3求余,结果为出现一次的数字
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int singleNumber(int[] nums) {
        int[] count = new int[32];  //每个数32位二进制
        for(int num : nums){
            //对num的每一位统计右移(由低到高)
            for(int i = 0; i < 32; i++){
                count[i] += num & 1; //若该位为1
                num >>= 1;
            }
        }
        int res = 0;
        //对结果的每一位赋值左移(由高到低)
        for(int i = 0; i < 32; i++){
            res <<= 1;
            res |= count[31 - i] % 3;   //或运算,0|1 = 1;0|0 = 0
        }
        return res;
    }
}
  • 有限状态自动机,每一位的余数有三种状态:0,1,2,使用二位二进制(t,o)表示这三种状态,使用异或运算和与运算得到状态转换公式(o = o ^ n & ~t,t = t ^ n & ~ o),因为每一位的步骤都一样,因此直接对数字进行状态转换计算,即可得到每位的状态, 最后的状态一定是00或01,返回o为1的位(即o)
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int singleNumber(int[] nums) {
        //有限状态自动机
        int one = 0;
        int two = 0;
        for(int num : nums){
            one = one ^ num & ~two;
            two = two ^ num & ~one;
        }
        return one;
    }
}

JZ57 和为s的两个数字

  • 双指针,左右指针分别从数组开头和末尾出发,指向的数字和与目标值对比,小于目标值则左指针右移,大于目标值则右指针左移
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int[] twoSum(int[] nums, int target) {
        //双指针
        int left = 0;
        int right = nums.length - 1;
        while(left < right){
            if(target - nums[right] > nums[left]){
                left++;
            }else if(target - nums[right] < nums[left]){
                right--;
            }else{
                return new int[]{nums[left], nums[right]};
            }
        }
        return new int[2];
    }
}

JZ57-II 和为s的连续正数序列

  • 从1开始遍历求和,如果等于目标值,则记录到结果数组中,否则继续下一轮遍历,别用
    • 时间复杂度O(n√n),空间复杂度O(1)
class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> res = new LinkedList<>();
        int sum = 0;
        int n = 0;
        for(int i = 1; i <= target / 2; i++){
            sum += i;
            n = i;
            while(sum < target){
                n++;
                sum += n;
            }
            if(sum == target){
                int[] pathArr = new int[n - i + 1];
                for(int k = 0; k <= n - i; k++){
                    pathArr[k] = k + i;
                }
                res.add(pathArr);
            }
            sum = 0;
        }
        int[][] result = new int[res.size()][];
        for(int i = 0; i < res.size(); i++){
            result[i] = res.get(i);
        }
        return result;
    }
}
  • 使用求和公式,已知目标和、左边界求右边界,由求等差数列求和公式(首项加末项) * 项数 / 2,可以得到末项的值即右边界,如果该右边界存在(为整数且大于左边界),则将序列保存到结果数组,否则计算下一个首项
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> res = new LinkedList<>();
        //求和公式
        for(int left = 1; left <= target / 2; left++){
            //已知目标和、左边界求右边界
            //注意left*left可能会溢出,因此需要使用long
            double right = (Math.sqrt(1 + 4 * ((long)left * left - left + 2 * target)) - 1) / 2;
            //如果该右边界存在(为整数且大于左边界),则将序列保存到结果数组
            if(right == (int)right && (int)right > left){
                int[] path = new int[(int)right - left + 1];
                for(int i = 0; i <= (int)right - left; i++){
                    path[i] = i + left;
                }
                res.add(path);
            }
        }
        return res.toArray(new int[0][]);   //list转为array
    }
}
  • 双指针,左右指针都从数组开头出发(间隔1),指向的数字和与目标值对比,小于目标值则右指针右移,大于目标值则左指针左移
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> res = new LinkedList<>();
        //双指针
        int left = 1; 
        int right = 2;
        int sum = left + right;
        while(left < right && right <= target / 2 + 1){
            if(sum < target){   //小于目标值则右指针右移
                right++;
                sum += right;
            }else if(sum > target){ //大于目标值则左指针左移
                sum -= left;
                left++;
            }else{  //和等于目标值
                int[] path = new int[right - left + 1];
                for(int i = 0; i <= right - left; i++){
                    path[i] = i + left;
                }
                res.add(path);
                sum -= left;
                left++;
                right++;
                sum += right;
            }
        }
        // int[][] result = new int[res.size()][];
        // for(int i = 0; i < res.size(); i++){
        //     result[i] = res.get(i);
        // }
        // return result;
        return res.toArray(new int[0][]);   //list转为array
    }
}

JZ58-I 翻转单词顺序

  • 使用栈,先去除字符串前后的空格(trim),然后从后向前遍历,首先压入一个空格,再将字符压入栈,不管碰到多少个空格都弹出为新字符,直到遍历完字符串
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    public String reverseWords(String s) {
        //使用栈,先去除字符串前后的空格
        s = s.trim();
        if(s.length() == 0) return s;
        Deque<Character> stack = new LinkedList<>();
        StringBuilder sb = new StringBuilder("");
        //从后向前遍历,将字符压入栈
        char[] sArr = s.toCharArray();
        for(int i = sArr.length - 1; i >= 0; i--){
            if(sArr[i] != ' '){
                //首先压入一个空格,再将字符压入栈
                if(stack.isEmpty()) stack.push(' ');
                stack.push(sArr[i]);
            }else{  //不管碰到多少个空格都弹出为新字符
                while(!stack.isEmpty()){
                    sb.append(stack.pop());
                }
            }
        }
        //将最后一个单词弹出
        while(!stack.isEmpty() && stack.peek() != ' '){
            sb.append(stack.pop());
        }
        //去掉最后一个空格
        //sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }
}
  • 双指针法,可以调用api(trim(),substring()),也可以自定义函数去除多余的空格(字符串前后的空格,字符串中间部分的多余空格),直接在原字符串上操作。然后将整个字符串反转,最后再将每个单词反转
    • 时间复杂度O(n),空间复杂度O(n)
//调用api
class Solution {
    public String reverseWords(String s) {
        //双指针
        //移除前后空格
        s = s.trim();
        if(s.length() == 0) return s;
        //反向遍历
        StringBuilder sb = new StringBuilder("");
        int left = s.length() - 1;
        int right = s.length() - 1;
        while(left >= 0){
            //找到单词前的第一个空格
            while(left >= 0 && s.charAt(left) != ' ') left--;
            sb.append(s.substring(left + 1, right + 1));    //切割单词
            sb.append(" ");
            //定位下一个单词的最后一个字母
            while(left >= 0 && s.charAt(left) == ' ') left--;
            right = left;
        }
        //去除最后一个空格
        return sb.toString().trim();
    }
}
//自定义函数
class Solution {
    public String reverseWords(String s) {
        //双指针
        if(s.length() == 0) return s;
        //自定义函数移除多余空格
        String s2 = removeSpaces(s);
        //将字符串转为字符数组
        char[] sArr = s2.toCharArray();
        //将整个字符串反转
        reverseString(sArr, 0, sArr.length - 1);
        //将每个单词反转
        reverseWord(sArr);
        return new String(sArr);
    }
    //反转字符串,输入:字符数组,反转起始,反转终止
    private void reverseString(char[] sArr, int left, int right){
        while(left < right){
            //中间变量
            char temp = sArr[left];
            sArr[left] = sArr[right];
            sArr[right] = temp;
            //继续下一轮循环
            left++;
            right--;
        }
    }
    //移除多余空格
    private String removeSpaces(String s){
        //定义左右指针
        int left = 0;
        int right = s.length() - 1;
        //移除前后空格
        while(left <= right && s.charAt(left) == ' ') left++;
        while(left <= right && s.charAt(right) == ' ') right--;
        //定义字符串数组返回结果
        char[] res = new char[s.length()];
        int i = 0;
        //移除中间空格
        for(;left <= right; left++){
            //连续出现空格则跳过
            if(left > 0 && s.charAt(left) == ' ' && s.charAt(left-1) == ' '){
                continue;
            }
            //将字符填入数组
            res[i++] = s.charAt(left);
        }
        return new String(res, 0, i); //左闭右开
    }
    //将每个单词反转
    private void reverseWord(char[] sArr){
        //定义快慢指针
        int slow = 0;
        for(int fast = 0; fast < sArr.length; fast++){
            //快指针指向空格则进行反转
            if(sArr[fast] == ' '){
                reverseString(sArr, slow, fast - 1);
                slow = fast + 1;
            }
            //快指针指向最后一位时反转最后一个单词
            if(fast == sArr.length - 1){
                reverseString(sArr, slow, fast);
                slow = fast + 1;
            }
        }
    }
}

JZ58-II 左旋转字符串

  • 直接切片,偷懒做法
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    public String reverseLeftWords(String s, int n) {
        //切片
        return s.substring(n, s.length()) + s.substring(0, n);
    }
}
  • 定义StringBuilder,遍历字符串然后拼接(求余简化代码),也可以直接字符串相加(效率低)
    • 时间复杂度O(n),空间复杂度O(n)
class Solution {
    public String reverseLeftWords(String s, int n) {
        //定义StringBuilder,遍历字符串然后拼接(求余简化代码)
        StringBuilder sb = new StringBuilder();
        for(int i = n; i < s.length() + n; i++){
            sb.append(s.charAt(i % s.length()));
        }
        return sb.toString();
    }
}
  • 使用StringBuilder或字符数组进行三次翻转,先翻转n前字符串,再翻转n后字符串,最后翻转整个字符串
    • 时间复杂度O(n),空间复杂度O(1)
class Solution {
    public String reverseLeftWords(String s, int n) {
        //使用StringBuilder或字符数组进行三次翻转
        StringBuilder sb = new StringBuilder(s);
        //先翻转n前字符串
        reverseString(sb, 0, n - 1);
        //再翻转n后字符串
        reverseString(sb, n, s.length() - 1);
        //最后翻转整个字符串
        reverseString(sb, 0, s.length() - 1);
        return sb.toString();
    }
    //翻转字符串,输入待翻转字符串和左右区间(左闭右闭)
    private void reverseString(StringBuilder sb, int left, int right){
        while(left < right){
            char temp = sb.charAt(left);
            sb.setCharAt(left, sb.charAt(right));
            sb.setCharAt(right, temp);
            left++;
            right--;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值