数据结构与算法 - 分治

一、概述

分治思想

  • 将大问题划分为两个到多个子问题
  • 子问题可以继续拆分成更小的子问题,直到能简单求解
  • 如有必要,将子问题的解进行合并,得到原始问题的解

1. 二分查找

public static int binarySearch(int[] a, int target) {
    return recursion(a, target, 0, a.length - 1);
}

public static int recursion(int[] a, int target, int i, int j) {
    if (i > j) {
        return -1;
    }
    int m = (i + j) >>> 1;
    if (target < a[m]) {
        return recursion(a, target, i, m - 1);
    } else if (a[m] < target) {
        return recursion(a, target, m + 1, j);
    } else {
        return m;
    }
}

减而治之,每次搜索范围内元素减少一半。

2. 快速排序

public static void sort(int[] a) {
    quick(a, 0, a.length - 1);
}

private static void quick(int[] a, int left, int right) {
    if (left >= right) {
        return;
    }
    int p = partition(a, left, right);
    quick(a, left, p - 1);
    quick(a, p + 1, right);
}

分而治之,这次分区基准点,在划分之后两个区域分别进行下次分区。


3. 归并排序

public static void sort(int[] a1) {
    int[] a2 = new int[a1.length];
    split(a1, 0, a1.length - 1, a2);
}

private static void split(int[] a1, int left, int right, int[] a2) {
    int[] array = Arrays.copyOfRange(a1, left, right + 1);
    // 2. 治
    if (left == right) {
        return;
    }
    // 1. 分
    int m = (left + right) >>> 1;
    split(a1, left, m, a2);                 
    split(a1, m + 1, right, a2);       
    // 3. 合
    merge(a1, left, m, m + 1, right, a2);
    System.arraycopy(a2, left, a1, left, right - left + 1);
}

分而治之,分到区间内只有一个元素,合并区间。

4. 合并K个排序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:

输入:lists = []
输出:[]

示例 3:

输入:lists = [[]]
输出:[]

提示:

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i] 按 升序 排列
  • lists[i].length 的总和不超过 10^4
/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode s = new ListNode(-1, null);
        ListNode p = s;

        while (list1 != null && list2 != null) {
            // 谁小把谁链给p,p和小的都向后平移一位
            if (list1.val < list2.val) {
                p.next = list1;
                list1 = list1.next;
            } else {
                p.next = list2;
                list2 = list2.next;
            }
            p = p.next;
        }

        // 处理剩余节点
        if (list1 != null) {
            p.next = list1;
        }

        if (list2 != null) {
            p.next = list2;
        }

        return s.next;
    }

    public ListNode split(ListNode[] lists, int i, int j) {
        if (j == i) {
            return lists[i];
        }

        int m = (i + j) >>> 1;
        return mergeTwoLists(split(lists, i, m), split(lists, m + 1, j));
    }

    public ListNode mergeKLists(ListNode[] lists) {
        if (lists.length == 0) {
            return null;
        }

        return split(lists, 0, lists.length - 1);
    }
}

分而治之,分到区间内只有一个链表,合并区间

5. 对比动态规划

  • 都需要拆分子问题
  • 动态规划的子问题有重叠,因此需要记录之前子问题解,避免重复运算
  • 分而治之的子问题无重叠

二、快速选择算法

快速选择(Quickselect)算法是一种用于从未排序的列表中选择第 k 小(或第 k 大)元素的方法,它基于快速排序(Quicksort)算法的原理。其平均时间复杂度为 O(n),最坏情况下为 O(n^2),但在大多数情况下运行得很快。

package com.itheima.algorithms.divideandconquer;

import java.util.concurrent.ThreadLocalRandom;

/**
 * 快速选择算法 - 分而治之
 */
public class QuickSelect {

    /**
     * 求排在第i名的元素,i从0开始,由小到大排
     * 6 5 1 2 4
     */

    public static int quick(int[] array, int left, int right, int i) {
        /*
            6   5   1   2   [4]

                    2
            1   2   4   6   5

            1   2   4   6   [5]
                        3
            1   2   4   5   6
         */

        // 基准点元素索引值
        int p = partition(array, left, right);
        if(p == i) {
            return array[p];
        }
        if(i < p) {
            // 到左边找
            return quick(array, left, p - 1, i);
        } else {
            // 到右边找
            return quick(array, p + 1, right, i);
        }
    }

    private static int partition(int[] a, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(a, idx, left);
        int pivot = a[left];
        int i = left + 1, j = right;

        while (i <= j) {
            // 2. i从左向右找大的
            while (i <= j && a[i] < pivot) {
                i++;
            }
            // 1. j从右向左找小(等)的
            while (i <= j && a[j] > pivot) {
                j--;
            }

            if(i <= j) {
                swap(a, i, j);
                i++;
                j--;
            }
        }
        swap(a, j, left);

        return j;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }


    public static void main(String[] args) {
        int [] array = {6, 5, 1, 2, 4};
        System.out.println(quick(array, 0, array.length - 1, 0));  // 1
        System.out.println(quick(array, 0, array.length - 1, 1));  // 2
        System.out.println(quick(array, 0, array.length - 1, 2));  // 4
        System.out.println(quick(array, 0, array.length - 1, 3));  // 3
        System.out.println(quick(array, 0, array.length - 1, 4));  // 6
    }
}

1. 数组中第k个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入: [3,2,1,5,6,4], k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

提示:

  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

解法一:执行耗时6ms

class Solution {
    public int findKthLargest(int[] nums, int k) {
        return quick(nums, 0, nums.length - 1, nums.length - k);
    }

    public static int quick(int[] array, int left, int right, int i) {

        // 基准点元素索引值
        int p = partition(array, left, right);
        if (p == i) {
            return array[p];
        }
        if (i < p) {
            // 到左边找
            return quick(array, left, p - 1, i);
        } else {
            // 到右边找
            return quick(array, p + 1, right, i);
        }
    }

    private static int partition(int[] a, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(a, idx, left);
        int pivot = a[left];
        int i = left + 1, j = right;

        while (i <= j) {
            // 2. i从左向右找大的
            while (i <= j && a[i] < pivot) {
                i++;
            }
            // 1. j从右向左找小(等)的
            while (i <= j && a[j] > pivot) {
                j--;
            }
            
            if(i <= j) {
                swap(a, i, j);
                i++;
                j--;
            }
        }
        swap(a, j, left);

        return j;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

2. 数组中位数

package com.itheima.algorithms.divideandconquer;

import java.util.concurrent.ThreadLocalRandom;

class Solution {
    /*
        偶数个
            3   1   5   4
        奇数个
            4   5   1
            4   5   1   6   3
     */
    public static double findMedian(int[] nums) {
        if (nums.length % 2 != 0) {
            // 奇数个
            return findIndex(nums, 0, nums.length - 1, nums.length / 2);
        } else {
            int a = findIndex(nums, 0, nums.length - 1, nums.length / 2);
            int b = findIndex(nums, 0, nums.length - 1, nums.length / 2 - 1);

            return (a + b) / 2.0;
        }
    }


    public static int findIndex(int[] array, int left, int right, int i) {
        /*
            6   5   1   2   [4]

                    2
            1   2   4   6   5

            1   2   4   6   [5]
                        3
            1   2   4   5   6
         */

        // 基准点元素索引值
        int p = partition(array, left, right);
        if (p == i) {
            return array[p];
        }
        if (i < p) {
            // 到左边找
            return findIndex(array, left, p - 1, i);
        } else {
            // 到右边找
            return findIndex(array, p + 1, right, i);
        }
    }

    private static int partition(int[] a, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(a, idx, left);
        int pivot = a[left];
        int i = left + 1, j = right;

        while (i <= j) {
            // 2. i从左向右找大的
            while (i <= j && a[i] < pivot) {
                i++;
            }
            // 1. j从右向左找小(等)的
            while (i <= j && a[j] > pivot) {
                j--;
            }

            if (i <= j) {
                swap(a, i, j);
                i++;
                j--;
            }
        }
        swap(a, j, left);

        return j;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }


    public static void main(String[] args) {
        int[] nums = {1, 4, 5, 2, 7};
        System.out.println(findMedian(nums));
        int[] nums2 = {3, 2, 8, 5, 7, 10};
        System.out.println(findMedian(nums2));
    }
}

三、快速幂

实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,x^n )。

示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000

示例 2:

输入:x = 2.10000, n = 3
输出:9.26100

示例 3:

输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25

提示:

  • -100.0 < x < 100.0
  • -2^31 <= n <= 2^31-1
  • n 是一个整数
  • 要么 x 不为零,要么 n > 0 。
  • -10^4 <= xn <= 10^4

解法一:

class Solution {
    public static double myPow(double x, int n) {
        if(n == 0) {
            return 1.0;
        }
        if(n == 1) {
            return x;
        }
        double y = myPow(x, n / 2);

        /*
            1   001
            3   011
            5   101
            7   111
                001 &
                ---
                001

            2   010
            4   100
            6   110
            8  1000
            奇数的二进制末位都是1,偶数的二进制末位都是0
            与001进行按位与运算,如果结果为0,则为偶数
         */
        if((n & 1) == 0) {
            // 偶数次幂
            return  y * y;
        } else if(n > 0){
            // 奇数
            return x * y * y;
        } else {
            // n为负数
            return y * y / x;
        }
    }
}

四、平方根整数部分

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x * 0.5 。

示例 1:

输入:x = 4
输出:2

示例 2:

输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

提示:

  • 0 <= x <= 2^31 - 1

解法一:

class Solution {
    public int mySqrt(int x) {
        if(x == 1) {
            return 1;
        }
        
        int min = 0;
        int max = x;

        while(max - min > 1) {
            int mid = (max + min) >> 1;
            if(x / mid < mid) {
                // 平方根落在m的左侧,更新max
                max = mid;
            } else {
                // 平方根落在m的右侧,更新min
                min = mid;
            }
        }

        return min;
    }
}

五、至少k个重复字符的最长子串

给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。

如果不存在这样的子字符串,则返回 0。

示例 1:

输入:s = "aaabb", k = 3
输出:3
解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。

示例 2:

输入:s = "ababbc", k = 2
输出:5
解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。

提示:

  • 1 <= s.length <= 10^4
  • s 仅由小写英文字母组成
  • 1 <= k <= 10^5

解法一:分治

解题思路:①统计字符串中每个字符的出现次数,移除那些出现次数 < k 的字符;

②剩余的子串,递归做处理,直至(1)整个子串长度 < k(排除)(2)子串中没有出现次数 < k的字符

class Solution {
    public int longestSubstring(String s, int k) {
        return longestSubstringHelper(s, k);
    }

    private int longestSubstringHelper(String s, int k) {
        HashMap<Character, Integer> map = new HashMap<>();

        // 统计字符出现次数
        for (char ch : s.toCharArray()) {
            map.put(ch, map.getOrDefault(ch, 0) + 1);
        }

        // 检查是否所有字符都满足出现次数要求
        for (char ch : map.keySet()) {
            if (map.get(ch) < k) {
                // 如果某个字符出现次数小于 k,则分割字符串
                int maxLen = 0;
                for (String part : s.split(String.valueOf(ch))) {
                    maxLen = Math.max(maxLen, longestSubstringHelper(part, k));
                }
                return maxLen;
            }
        }

        // 所有字符出现次数都大于等于 k,返回当前字符串的长度
        return s.length();
    }
}

优化:

class Solution {
    public int longestSubstring(String s, int k) {
        // 基础判定,字符串长度小于k时返回0
        if (s.length() < k) {
            return 0;
        }

        // 统计每个字符出现的次数
        int[] counts = new int[26];
        for (char ch : s.toCharArray()) {
            counts[ch - 'a']++;
        }

        // 检查是否有字符出现次数小于k
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (counts[c - 'a'] < k) {
                // 统计到出现次数小于k的字符,进行分治
                int j = i + 1;
                while (j < s.length() && counts[s.charAt(j) - 'a'] < k) {
                    j++;
                }
                // 递归处理左右部分的子字符串
                return Math.max(longestSubstring(s.substring(0, i), k),
                        longestSubstring(s.substring(j), k));
            }
        }

        // 所有字符的出现次数都大于等于k,返回原字符串的长度
        return s.length();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值