【刷题系列】分治法

系列汇总:《刷题系列汇总》



——————《剑指offeer》———————

JZ30. 连续子数组的最大和

  • 题目描述:输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为 O(n)
  • 优秀思路:核心点在于理解到:若前面部分的和 < 0,说明前面的部分对于计算总和这件事来说只会拖后腿,那么便舍弃掉,将计算当前sum的起点重设为0。但同时要一直更新当前的最大和的值
public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        int maxSum = Integer.MIN_VALUE; // 因为有负数,所以不能设为0
        int sum = 0;
        for(int i = 0;i < array.length;i++){
            sum += array[i];
            maxSum = Math.max(maxSum,sum);
            if(sum < 0) sum = 0; // 核心!!若小于0,则计算和的起点重新置为0
        }
        return maxSum;
    }
}

JZ26. 二叉搜索树与双向链表(需重写)

  • 题目描述:输入一棵二叉搜索树(空 || 左<根 && 右>根),将该二叉搜索树转换成一个排序双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
  • 优秀思路:用list存储中序遍历的节点,根据list构建节点间的连接
import java.util.ArrayList;
public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null) return null;
        ArrayList<TreeNode> list = new ArrayList<>(); //存储中序遍历的节点
        tree2List(list,pRootOfTree);
        return list2SortTree(list);
    }
    // 用list存储中序遍历的节点:这个递归很妙
    public void tree2List(ArrayList<TreeNode> list,TreeNode root){
        if (root != null){
            tree2List(list,root.left);
            list.add(root);
            tree2List(list,root.right);
        }
    }
    // 根据list构建节点间的连接
    public TreeNode list2SortTree(ArrayList<TreeNode> list){
        TreeNode head = list.get(0); // 最左边角落的节点
        TreeNode cur = head;
        for (int i = 1;i < list.size();++i){
            TreeNode next = list.get(i);
            next.left = cur;
            cur.right = next;
            cur = next; // 更新当前节点
        }
        return head;
    }
}

JZ29. 最小的K个数

  • 题目描述:给定一个数组,找出其中最小的K个数。例如数组元素是4,5,1,6,2,7,3,88个数字,则最小的4个数字是1,2,3,4。如果K>数组的长度,那么返回一个空的数组
  • 优秀思路(代码自编写):快速排序,读取前K个即可
    在这里插入图片描述
import java.util.ArrayList;

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
        ArrayList<Integer> res = new ArrayList<>();
        if(k > input.length) return res;
        
        quickSort(input,0,input.length-1);
        for(int i = 0;i < k;i++){
            res.add(input[i]);
        }
        return res;
    }
    // 快速排序法
    private void quickSort(int[] input,int left,int right){
        if(left >= right) return;
        int tempLeft = left;
        int tempRight = right;
        int refer = input[left];// 中心轴
        // 这是左边left空出
        // 从最右边开始移动,让最右边也空出来
        int flag = 1;
        while(left <= right){
            if(left == right){
                input[left] = refer;
                break;
            }
            if(flag == 1){ // 移动右边
                if(input[right] <= refer){
                    input[left] = input[right];
                    left++;
                    flag = 2;
                }else right--;
            }
            if(flag == 2){ // 移动左边
                if(input[left] <= refer) left++;
                else{
                    input[right] = input[left];
                    right--;
                    flag = 1;
                }
            }
        }
        quickSort(input,tempLeft,left-1);
        quickSort(input,left+1,tempRight);
    }
}

——————《LeectCode》———————

53. 最大子序和(同上JZ30,略)


23. 合并K个升序链表

  • 题目描述:给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
  • 优秀思路
    在这里插入图片描述
  • 优秀思路1:【优先级队列+归并排序】利用了优先级队列自动排序的特性。
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0) return null;
        if(lists.length == 1) return lists[0];
        Queue<ListNode> q = new PriorityQueue<>((node1,node2) -> node1.val - node2.val);
        // 新建链表存储结果
        ListNode res = new ListNode(0);
        ListNode index = res;
        for(ListNode head:lists){// 因为是升序数组,最小值一定在三个头结点中
            if(head != null) q.add(head); 
        }
        // 下面的排序方式相当于归并排序
        while(!q.isEmpty()){
            ListNode minNode = q.poll(); // 最小节点
            // 下面这一步隐含的意思是,若个ListNode被遍历完了,则自动跳过了,无需其他处理
            if(minNode.next != null) q.add(minNode.next);
            index.next = minNode;
            index = index.next; // 更新索引
        }
        return res.next;
    }
}
  • 优秀思路2:【分治】将所有ListNode两两分治合并。
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0) return null;
        if(lists.length == 1) return lists[0];
        return merge(lists,0,lists.length-1);
    }
    // 分治
    private ListNode merge(ListNode[] lists,int left,int right){
        if(left == right) return lists[left]; // 此处不是返回lists[0]
        int mid = (left + right) >> 1;
        return merge2List(merge(lists,left,mid),merge(lists,mid+1,right));
    }
    // 合并两列表
    private ListNode merge2List(ListNode node1,ListNode node2){
        ListNode res = new ListNode(0);
        ListNode index = res;
        while(node1 != null && node2 != null){
            if(node1.val <= node2.val){
                index.next = node1;
                node1 = node1.next;
            }else{
                index.next = node2;
                node2 = node2.next;
            }
            index = index.next;
        }
        index.next = (node1==null? node2:node1);
        return res.next;
    }
}

4. 寻找两个正序数组的中位数

  • 题目描述:给定两个大小分别为 mn正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数 。
  • 我的思路:【归并排序】归并排序合并两数组,再根据合并数组的奇偶长度输出不同值
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len = nums1.length + nums2.length;
        int[] all = merge2num(nums1, nums2);
        if(len % 2 == 0) return (double) (all[len/2]+all[len/2-1])/2;
        else return (double) all[len/2];
    }
    // 归并排序——合并两数组
    private int[] merge2num(int[] nums1, int[] nums2){
        int len1 = nums1.length;
        int len2 = nums2.length;
        int[] res = new int[len1+len2];
        int i = 0;
        int j = 0;
        int count = 0;
        while(i<len1 && j<len2){
            if(nums1[i] <= nums2[j]){
                res[count++] = nums1[i];
                i++;
            }else{
                res[count++] = nums2[j];
                j++;
            }
        }
        // 贴上没统计完的
        if(i < len1) for(i = i;i<len1;i++) res[count++] = nums1[i];
        if(j < len2) for(j = j;j<len2;j++) res[count++] = nums2[j];
        return res;
    }
}
  • 我的优秀思路: 无需合并数组,找到索引 len/2 和 len/2-1 处的值即可
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // 无需合并数组,找到索引 len/2 和 len/2-1 处的值即可
        int len1 = nums1.length,len2 = nums2.length;
        int len = len1 + len2;
        int value1 = 0;
        int value2 = 0;
        int i = 0;
        int j = 0;
        int count = 0;
        int flag = 0;
        while(i<len1 && j<len2){
            if(nums1[i] <= nums2[j]){
                if(count == len/2){
                    value1 = nums1[i];
                    flag = 1;
                    break;
                }
                if(count == len/2 - 1){
                    value2 = nums1[i];
                }
                count++;
                i++;
            }else{
                if(count == len/2){
                    value1 = nums2[j];
                    flag = 1;
                    break;
                }
                if(count == len/2 - 1){
                    value2 = nums2[j];
                }
                count++;
                j++;
            }
        }
        if(flag == 0){
            if(i < len1){
                for(i = i;i<len1;i++){
                    if(count == len/2){
                        value1 = nums1[i];
                        break;
                    }
                    if(count == len/2 - 1){
                        value2 = nums1[i];
                    }
                    count++;
                }
            }
            if(j < len2){
                for(j = j;j<len2;j++){
                    if(count == len/2){
                        value1 = nums2[j];
                        break;
                    }
                    if(count == len/2 - 1){
                        value2 = nums2[j];
                    }
                    count++;
                }
            }
        }
        if(len % 2 == 0) return (double) (value1 + value2)/2;
        else return (double) value1;
    }
}
  • 优秀思路:二分查找,文中要求复杂度达到O(log(m+n)),故考虑二分查找
  •    /* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
       * 这里的 "/" 表示整除
       * nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
       * nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
       * 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
       * 这样 pivot 本身最大也只能是第 k-1 小的元素
       * 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
       * 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
       * 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
       */
    
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int length1 = nums1.length, length2 = nums2.length;
        int totalLength = length1 + length2;
        if (totalLength % 2 == 1) {
            int midIndex = totalLength / 2;
            double median = getKthElement(nums1, nums2, midIndex + 1);
            return median;
        } else {
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
            return median;
        }
    }

    public int getKthElement(int[] nums1, int[] nums2, int k) {
        int length1 = nums1.length, length2 = nums2.length;
        int index1 = 0, index2 = 0;
        int kthElement = 0;

        while (true) {
            // 边界情况
            if (index1 == length1) {
                return nums2[index2 + k - 1];
            }
            if (index2 == length2) {
                return nums1[index1 + k - 1];
            }
            if (k == 1) {
                return Math.min(nums1[index1], nums2[index2]);
            }
            
            // 正常情况
            int half = k / 2;
            int newIndex1 = Math.min(index1 + half, length1) - 1;
            int newIndex2 = Math.min(index2 + half, length2) - 1;
            int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
            if (pivot1 <= pivot2) {
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;
            } else {
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;
            }
        }
    }
}

240. 搜索二维矩阵 II

  • 题目描述:编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
    • 每行的元素从左到右升序排列。
    • 每列的元素从上到下升序排列。
  • 我的思路(优秀):【dfs】从左下角开始搜索
class Solution {
    boolean flag = false;
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix.length == 0 || matrix[0].length == 0) return false;
        search(matrix,matrix.length-1,0,target);
        return flag;
    }
    private void search(int[][] matrix,int i,int j,int target){
        if(i < 0 || j >= matrix[0].length) return;
        if(matrix[i][j] == target){
            flag = true;
            return;
        }
        if(matrix[i][j] > target) search(matrix,i-1,j,target);
        if(matrix[i][j] < target) search(matrix,i,j+1,target);
    }
}
  • 更优代码:dfs会增加无效路径开销
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if(matrix.length == 0 || matrix[0].length == 0) return false;
        int i = matrix.length-1;
        int j = 0;
        while(i >= 0 && j < matrix[0].length){
            if(matrix[i][j] == target) return true;
            else if(matrix[i][j] < target) j++;
            else i--;
        }
        return false;
    }
}
  • 优秀思路:二分查找

395. 至少有 K 个重复字符的最长子串

  • 题目描述:给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。
  • 我的思路(7% - 5%):递归地根据当前字串的第一个出现次数小于k的字母将字串分为两段处理
class Solution {
    int maxLen = 0;
    public int longestSubstring(String s, int k) {
        if(k==1) return s.length();
        dfs(s, 0, s.length() - 1, k);
        return maxLen;
    }

    private void dfs(String s, int left, int right, int k) {
        // 边界条件:该子串长度<k
        if(right - left + 1 < k) return;

        // 统计每次字幕出现的次数
        int[] count = new int[26];
        for(int i = left;i <= right;i++) count[s.charAt(i)-'a']++;

        // 找到的第一个出现字数<k的字母
        int split = -1;
        for(int i = left;i <= right;i++){
            if(count[s.charAt(i)-'a'] > 0 && count[s.charAt(i)-'a'] < k){
                split = i;
                break;
            }
        }
        // 根据该字母将s分为两段
        if(split == -1){  // 不存在该字母
            maxLen = Math.max(maxLen,right-left+1);
            return;
        }else{
            if(split == left) dfs(s,left+1,right,k); // 该字母出现在开头
            else if(split == right){ // 该字母出现在结尾
                maxLen = Math.max(maxLen,right-left);
                return;
            }else{ // 该字母出现在中间
                dfs(s,left,split-1,k);
                dfs(s,split+1,right,k);
            }
        }
    }
}
  • 我的思路改进(8% - 5%):上思路每次都得重新统计字母次数,效率很低,事实上每次找出的所有出现字数<k的位置都能用上
class Solution {
        int maxLen = 0;
    public int longestSubstring(String s, int k) {
        if(k==1) return s.length();
        dfs(s, 0, s.length() - 1, k);
        return maxLen;
    }

    private void dfs(String s, int left, int right, int k) {
        // 边界条件:该子串长度<k
        if(right - left + 1 < k) return;
        // 统计每次字幕出现的次数
        int[] count = new int[26];
        for(int i = left;i <= right;i++) count[s.charAt(i)-'a']++;

        // 找到的第一个出现字数<k的字母
        int preSplit = left;
        int index = preSplit;
        int split = -1;
        while(index <= right){
            if(right - preSplit + 1 < k) return;
            for(int i = preSplit;i <= right;i++){
                index++;
                if(count[s.charAt(i)-'a'] > 0 && count[s.charAt(i)-'a'] < k){
                    split = i;
                    break;
                }
            }
            // 根据该字母将s分为两段
            if(split == -1){  // 不存在该字母
                maxLen = Math.max(maxLen,right-preSplit+1);
                return;
            }
            // 最后一个分割点
            else if(preSplit == split + 1){
                dfs(s,preSplit,right,k);// 取出最后一段
                return;
            }else{
                if(split == left){ // 该字母出现在开头
                    dfs(s,preSplit+1,right,k); // 取出最后一段
                    return;
                }else if(split == right){ // 该字母出现在结尾
                    dfs(s,preSplit,split-1,k);
                    return;
                }else{ // 该字母出现在中间,划分两区域
                    dfs(s,preSplit,split-1,k);
                    preSplit = split+1;
                    index = preSplit;
                    continue;
                }
            }
        }
        return;
    }
}
  • 优秀思路(我的):递归+分治
  • 1.确定每个字母出现的次数
  • 2.以出现次数在(0,k)之间的字母作为子串分割点,分别取出各子串(最后一段单独取出)分别判断
  • 3.递归上过程,若某子串内无分割点,用其长度更新maxLen(题目要求的输出)
class Solution {
    int maxLen = 0; // 输出结果
    public int longestSubstring(String s, int k) {
        if(k == 1) return s.length();
        dfs(s, 0, s.length() - 1, k);
        return maxLen;
    }

    private void dfs(String s, int left, int right, int k) {
        // 边界条件:子串长度 < k
        if(right - left + 1 < k) return;

        // 统计每次字幕出现的次数
        int[] count = new int[26];
        for(int i = left;i <= right;i++) count[s.charAt(i)-'a']++;

        // 根据出现字数小于k的字母当做分割点,逐段递归
        int preSplit = left; // 上一个分割点
        int split = -1;
        for(int i = left;i <= right;i++){
            if(count[s.charAt(i)-'a'] > 0 && count[s.charAt(i)-'a'] < k){
                split = i;
                dfs(s,preSplit,split-1,k);
                preSplit = split+1; // 更新
            }
        }

        if(split == -1){ // 子串内无分割点
            maxLen = Math.max(maxLen,right-left+1);
        }else{  // 统计子串的最后一段
            dfs(s,preSplit,right,k);
        }
    }
}

169. 多数元素

  • 题目描述:给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
  • 优秀思路:【投票计数法】见代码
class Solution {
    public int majorityElement(int[] nums) {
        int candidate = 0; //备选人
        int vote = 0; // 备选人的票数
        for(int i = 0;i < nums.length;i++) {  
            if(vote == 0) candidate = nums[i]; // 当前无备选人/上一位备选人被淘汰,暂时确定当前位为备选人
            // 读票环节
            if(nums[i] == candidate) vote++;
            else vote--;       
        }
        return candidate;  
    }
} 

493. 翻转对(需重写)

  • 题目描述:给定一个数组 nums ,如果 i < jnums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对。你需要返回给定数组中的重要翻转对的数量。
    在这里插入图片描述

  • 优秀思路:在归并排序的过程中,假设对于数组 nums[l..r] 而言,我们已经分别求出了子数组nums[l..m]nums[m+1..r] 的翻转对数目,并已将两个子数组分别排好序,则nums[l..r] 中的翻转对数目,就等于两个子数组的翻转对数目之和,加上左右端点分别位于两个子数组的翻转对数目

    举个例子,对于 1 3 2 3 1,先分为1 3 23 1,count = 0 + 1 + count( 1 2 3 | 1 3) = 0 + 1 + 1 = 2
    
class Solution {
    int count = 0; // 重要翻转对数量
    public int reversePairs(int[] nums) {
        if(nums == null || nums.length < 2)  return 0; 
        mergeSort(nums, 0, nums.length - 1);      
        return count;
    }
    // 归并排序
    private void mergeSort(int[] nums, int start, int end) {
        if(start == end)  return;
        int mid = start + (end - start) / 2;
        mergeSort(nums, start, mid);
        mergeSort(nums, mid + 1, end);
        int i = start;
        int j = mid + 1;
        // 以中间为界限寻找重要翻转对
        // 注意:须排序前统计翻转对,否则数量就不对了
        while(i <= mid && j <= end) {
            if((long) nums[i] > 2 * (long) nums[j]) { // long时为了避免数字过大,int存不下
                count += mid - i + 1;
                j++; // 找到,后一段右移
            }else {
                i++; // 未找到,前一段后移
            }
        }
        // 注意统计完当前子数组的count后,需要进行排序,由于是void,排序后nums自动返回
        int[] tempArr = new int[end - start + 1]; // 排序后矩阵
        i = start;
        j = mid + 1;
        int idx = 0;
        while(i <= mid && j <= end) {
            tempArr[idx++] = nums[i] < nums[j] ? nums[i++] : nums[j++];
        }
        // 放置未排序部分
        while(i <= mid) tempArr[idx++] = nums[i++];
        while(j <= end) tempArr[idx++] = nums[j++];
        // 更新nums为排序好的矩阵
        for(i = 0, j = start; j <= end; i++, j++) {
            nums[j] = tempArr[i];
        }
    }
}

315. 计算右侧小于当前元素的个数(没看懂)

  • 题目描述:给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
    在这里插入图片描述

  • 优秀思路:利用归并排序

class Solution {
    private int[] index; // 存储排序数组元素的原索引
    private int[] temp;
    private int[] tempIndex;
    private int[] ans; //

    public List<Integer> countSmaller(int[] nums) {
        index = new int[nums.length];
        temp = new int[nums.length];
        tempIndex = new int[nums.length];
        ans = new int[nums.length];

        for (int i = 0; i < nums.length; ++i) {
            index[i] = i; // 索引数组
        }
        int l = 0, r = nums.length - 1;
        mergeSort(nums, l, r);
        List<Integer> list = new ArrayList<Integer>();
        for (int num : ans) {
            list.add(num);
        }
        return list;
    }

    public void mergeSort(int[] a, int l, int r) {
        if (l >= r) {
            return;
        }
        int mid = (l + r) >> 1;
        mergeSort(a, l, mid);
        mergeSort(a, mid + 1, r);
        merge(a, l, mid, r);
    }

    public void merge(int[] a, int l, int mid, int r) {
        int i = l, j = mid + 1, p = l;
        while (i <= mid && j <= r) {
            if (a[i] <= a[j]) {
                temp[p] = a[i];
                tempIndex[p] = index[i];
                ans[index[i]] += (j - mid - 1);
                ++i;
                ++p;
            } else {
                temp[p] = a[j];
                tempIndex[p] = index[j];
                ++j;
                ++p;
            }
        }
        while (i <= mid)  {
            temp[p] = a[i];
            tempIndex[p] = index[i];
            ans[index[i]] += (j - mid - 1);
            ++i;
            ++p;
        }
        while (j <= r) {
            temp[p] = a[j];
            tempIndex[p] = index[j];
            ++j;
            ++p;
        }
        for (int k = l; k <= r; ++k) {
            index[k] = tempIndex[k];
            a[k] = temp[k];
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值